diff --git a/1.3/Assemblies/1Fishery.dll b/1.3/Assemblies/1Fishery.dll
new file mode 100644
index 0000000..a26c4b7
Binary files /dev/null and b/1.3/Assemblies/1Fishery.dll differ
diff --git a/1.3/Assemblies/PerformanceFish.dll b/1.3/Assemblies/PerformanceFish.dll
new file mode 100644
index 0000000..bb22a72
Binary files /dev/null and b/1.3/Assemblies/PerformanceFish.dll differ
diff --git a/1.3/Assemblies/System.Runtime.CompilerServices.Unsafe.dll b/1.3/Assemblies/System.Runtime.CompilerServices.Unsafe.dll
new file mode 100644
index 0000000..c5ba4e4
Binary files /dev/null and b/1.3/Assemblies/System.Runtime.CompilerServices.Unsafe.dll differ
diff --git a/1.4/Assemblies/1Fishery.dll b/1.4/Assemblies/1Fishery.dll
new file mode 100644
index 0000000..46ac14d
Binary files /dev/null and b/1.4/Assemblies/1Fishery.dll differ
diff --git a/1.4/Assemblies/PerformanceFish.dll b/1.4/Assemblies/PerformanceFish.dll
new file mode 100644
index 0000000..8517671
Binary files /dev/null and b/1.4/Assemblies/PerformanceFish.dll differ
diff --git a/1.4/Assemblies/System.Runtime.CompilerServices.Unsafe.dll b/1.4/Assemblies/System.Runtime.CompilerServices.Unsafe.dll
new file mode 100644
index 0000000..c5ba4e4
Binary files /dev/null and b/1.4/Assemblies/System.Runtime.CompilerServices.Unsafe.dll differ
diff --git a/About/About.xml b/About/About.xml
new file mode 100644
index 0000000..968c5d4
--- /dev/null
+++ b/About/About.xml
@@ -0,0 +1,27 @@
+
+
+ Performance Fish
+ bs.performance
+
+ 1.3
+ 1.4
+
+ bradson
+ https://github.com/bbradson/performance-fish
+
+ Dubwise.DubsPerformanceAnalyzer
+ Dubwise.DubsPerformanceAnalyzerButchered
+ Ludeon.RimWorld.Royalty
+ Ludeon.RimWorld.Ideology
+ Ludeon.RimWorld.Biotech
+ UnlimitedHugs.HugsLib
+
+
+ brrainz.harmony
+ me.samboycoding.betterloading
+ zetrith.prepatcher
+ Ludeon.RimWorld
+ bs.betterlog
+
+ download more tps
+
\ No newline at end of file
diff --git a/About/Preview.dds b/About/Preview.dds
new file mode 100644
index 0000000..cfafaff
Binary files /dev/null and b/About/Preview.dds differ
diff --git a/About/Preview.png b/About/Preview.png
new file mode 100644
index 0000000..6f7d09c
Binary files /dev/null and b/About/Preview.png differ
diff --git a/DubsAnalyzer.xml b/DubsAnalyzer.xml
new file mode 100644
index 0000000..e991d11
--- /dev/null
+++ b/DubsAnalyzer.xml
@@ -0,0 +1,189 @@
+
+
+
+
+
+
+
+ Methods patched and optimized by Performance Fish. Use this in conjunction with its mod settings to measure performance improvements or find eventual issues if a patch acts up
+
+
+
+
+
+
+
+ Verse.DesignationManager:SpawnedDesignationsOfDef
+ Verse.DesignationManager:AnySpawnedDesignationOfDef
+ Verse.DesignationManager:AllDesignationsOn
+ Verse.DesignationManager:DesignationOn
+ Verse.DesignationManager:HasMapDesignationOn
+
+
+
+ Verse.ThingFilter:CopyAllowancesFrom
+ Verse.DrawBatch:Flush
+
+
+ RimWorld.HediffComp_PsychicHarmonizer:CompPostTick
+
+
+ Verse.HediffSet:HasHediff
+
+
+ Verse.Map:MapPreTick
+ Verse.ListerBuildings:ColonistsHaveBuilding
+ Verse.ListerBuildings:ColonistsHaveResearchBench
+ Verse.ListerBuildings:ColonistsHaveBuildingWithPowerOn
+ Verse.ListerBuildings:AllBuildingsColonistOfDef
+ Verse.ListerBuildings:AllBuildingsNonColonistOfDef
+
+
+ RimWorld.StorageSettings:AllowedToAccept
+
+
+ RimWorld.MassUtility:GearMass
+ RimWorld.MassUtility:InventoryMass
+
+
+ Verse.RecipeDef:get_AllRecipeUsers
+ Verse.UnfinishedThing:BillOnTableForMe
+
+
+
+ RimWorld.Pawn_Ownership:get_OwnedRoom
+ Verse.Room:get_Regions
+ Verse.Room:get_ContainedAndAdjacentThings
+
+
+
+
+ Verse.ThingFilter:get_AllowedThingDefs
+ Verse.ThingFilter:get_AllStorableThingDefs
+
+
+
+ Verse.MapPawns:get_AllPawnsUnspawned
+
+
+ RimWorld.ThoughtWorker_Precept_AltarSharing:ShouldHaveThought
+
+
+
+
+
+
+
+
+
+
+
+ PerformanceFish.JobSystem.WorkGiver_HaulOptimization+PotentialWorkThingsGlobal_Patch:Sort
+ RimWorld.WorkGiver_Haul:JobOnThing
+
+
+ RimWorld.JobGiver_Haul:TryGiveJob
+
+
+ RimWorld.IdeoUtility:GetStyleDominance
+
+
+ RimWorld.CompAssignableToPawn_Bed:IdeoligionForbids
+
+
+ System.RuntimeType:GetField
+ System.Type:GetMethod
+ System.Type:GetConstructor
+ System.Type:GetProperty
+ System.Reflection.MonoField:GetValue
+ System.Reflection.FieldInfo:SetValue
+ System.Reflection.MethodBase:Invoke
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Update methods of comps attached to Things. Any ThingWithComps can have these, and any mod can add new ones
+ Update
+
+ Verse.ThingComp:DrawGUIOverlay
+ Verse.ThingComp:PostDraw
+ Verse.ThingComp:PostDrawExtraSelectionOverlays
+ Verse.ThingComp:PostPrintOnto
+
+
+
+
+ Ticking health stuff ond other things implanted in pawn bodies
+ Tick
+
+ Verse.Hediff:Tick
+ Verse.Hediff:PostTick
+
+
+
+
+ Comps attached to Hediffs. Often responsible for the actual logic
+ Tick
+
+ Verse.HediffComp:CompPostTick
+
+
+
+
+ Settlements, caravans etc
+ Tick
+
+ RimWorld.Planet.WorldObject:Tick
+
+
+
+
+ Comps attached to settlements, caravans etc
+ Tick
+
+ RimWorld.Planet.WorldObjectComp:CompTick
+
+
+
+
+ Tick methods of comps with one instance attached to the game world
+ Tick
+
+ RimWorld.Planet.WorldComponent:WorldComponentTick
+
+
+
+
+ Update methods of comps with one instance attached to the game world
+ Update
+
+ RimWorld.Planet.WorldComponent:WorldComponentUpdate
+
+
+
+
+ Tick methods of mental states like berserk, tantrum etc
+ Tick
+
+ Verse.AI.MentalState:MentalStateTick
+
+
+
+
\ No newline at end of file
diff --git a/LICENSE b/LICENSE
index a612ad9..d6233aa 100644
--- a/LICENSE
+++ b/LICENSE
@@ -1,3 +1,5 @@
+Copyright (c) 2021 bradson
+
Mozilla Public License Version 2.0
==================================
@@ -35,7 +37,7 @@ Mozilla Public License Version 2.0
means any form of the work other than Source Code Form.
1.7. "Larger Work"
- means a work that combines Covered Software with other material, in
+ means a work that combines Covered Software with other material, in
a separate file or files, that is not Covered Software.
1.8. "License"
@@ -357,7 +359,7 @@ Exhibit A - Source Code Form License Notice
This Source Code Form is subject to the terms of the Mozilla Public
License, v. 2.0. If a copy of the MPL was not distributed with this
- file, You can obtain one at http://mozilla.org/MPL/2.0/.
+ file, You can obtain one at https://mozilla.org/MPL/2.0/.
If it is not possible or desirable to put the notice in a particular
file, then You may include the notice in a location (such as a LICENSE
@@ -370,4 +372,4 @@ Exhibit B - "Incompatible With Secondary Licenses" Notice
---------------------------------------------------------
This Source Code Form is "Incompatible With Secondary Licenses", as
- defined by the Mozilla Public License, v. 2.0.
+ defined by the Mozilla Public License, v. 2.0.
\ No newline at end of file
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..817c55b
--- /dev/null
+++ b/README.md
@@ -0,0 +1,12 @@
+# Performance Fish
+
+
+Performance Mod for Rimworld.
+Performance Fish attempts to improve overall framerates and tick times by patching various methods for improved efficiency, while keeping functionality identical. Designed to be used alongside other performance mods, like RocketMan, and intended to be compatible with very large modlists. Most patches become more impactful as the game progresses further into the lategame.
+
+A settings menu includes short descriptions for every patch, and each of them can be freely toggled in there. They get entirely unpatched in a disabled state, immediately. Nothing is stored in savefiles of specific game-sessions, ensuring that no errors get thrown when removing this mod.
+Special patches exist to add new entries and features to Dub's Performance Analyzer, including a right-click function allowing to profile overrides of functions.
+
+Multiplayer and RimThreaded currently aren't supported. All other mods, including Combat Extended, Vanilla Expanded, RocketMan and Performance Optimizer, are compatible.
+
+Licensed under [MPL-2.0](https://tldrlegal.com/license/mozilla-public-license-2.0-(mpl-2))
\ No newline at end of file
diff --git a/Source/PerformanceFish.Tests/PerformanceFish.Tests.csproj b/Source/PerformanceFish.Tests/PerformanceFish.Tests.csproj
new file mode 100644
index 0000000..1e8c988
--- /dev/null
+++ b/Source/PerformanceFish.Tests/PerformanceFish.Tests.csproj
@@ -0,0 +1,72 @@
+
+
+
+ net48
+ preview
+ True
+ preview-all
+ True
+ v1.3;v1.4
+
+
+
+
+
+
+ E:\RimWorld\1.3\RimWorldWin64_Data\Managed\Assembly-CSharp.dll
+
+
+ E:\RimWorld\1.3\RimWorldWin64_Data\Managed\mscorlib.dll
+
+
+ E:\RimWorld\1.3\RimWorldWin64_Data\Managed\UnityEngine.CoreModule.dll
+
+
+ E:\RimWorld\1.3\RimWorldWin64_Data\Managed\UnityEngine.IMGUIModule.dll
+
+
+ ..\..\..\Fishery\1.3\Assemblies\1Fishery.dll
+
+
+ ..\..\1.3\Assemblies\PerformanceFish.dll
+
+
+
+
+
+
+
+ E:\RimWorld\1.4\RimWorldWin64_Data\Managed\Assembly-CSharp.dll
+
+
+ E:\RimWorld\1.4\RimWorldWin64_Data\Managed\mscorlib.dll
+
+
+ E:\RimWorld\1.4\RimWorldWin64_Data\Managed\UnityEngine.CoreModule.dll
+
+
+ E:\RimWorld\1.4\RimWorldWin64_Data\Managed\UnityEngine.IMGUIModule.dll
+
+
+ ..\..\..\Fishery\1.4\Assemblies\1Fishery.dll
+
+
+ ..\..\1.4\Assemblies\PerformanceFish.dll
+
+
+
+
+
+
+
+
+
+ runtime; build; native; contentfiles; analyzers; buildtransitive
+ all
+
+
+ runtime; build; native; contentfiles; analyzers; buildtransitive
+ all
+
+
+
\ No newline at end of file
diff --git a/Source/PerformanceFish.Tests/PerformanceFish.Tests.sln b/Source/PerformanceFish.Tests/PerformanceFish.Tests.sln
new file mode 100644
index 0000000..34107e5
--- /dev/null
+++ b/Source/PerformanceFish.Tests/PerformanceFish.Tests.sln
@@ -0,0 +1,25 @@
+
+Microsoft Visual Studio Solution File, Format Version 12.00
+# Visual Studio Version 17
+VisualStudioVersion = 17.3.32728.150
+MinimumVisualStudioVersion = 10.0.40219.1
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "PerformanceFish.Tests", "PerformanceFish.Tests.csproj", "{D3852300-E6CA-4561-849F-B9F827703B3A}"
+EndProject
+Global
+ GlobalSection(SolutionConfigurationPlatforms) = preSolution
+ v1.3|Any CPU = v1.3|Any CPU
+ v1.4|Any CPU = v1.4|Any CPU
+ EndGlobalSection
+ GlobalSection(ProjectConfigurationPlatforms) = postSolution
+ {D3852300-E6CA-4561-849F-B9F827703B3A}.v1.3|Any CPU.ActiveCfg = v1.3|Any CPU
+ {D3852300-E6CA-4561-849F-B9F827703B3A}.v1.3|Any CPU.Build.0 = v1.3|Any CPU
+ {D3852300-E6CA-4561-849F-B9F827703B3A}.v1.4|Any CPU.ActiveCfg = v1.4|Any CPU
+ {D3852300-E6CA-4561-849F-B9F827703B3A}.v1.4|Any CPU.Build.0 = v1.4|Any CPU
+ EndGlobalSection
+ GlobalSection(SolutionProperties) = preSolution
+ HideSolutionNode = FALSE
+ EndGlobalSection
+ GlobalSection(ExtensibilityGlobals) = postSolution
+ SolutionGuid = {9E98D95E-68FE-465E-A330-11540608ADAD}
+ EndGlobalSection
+EndGlobal
diff --git a/Source/PerformanceFish.Tests/ReflectionCaching.cs b/Source/PerformanceFish.Tests/ReflectionCaching.cs
new file mode 100644
index 0000000..7ec0ba0
--- /dev/null
+++ b/Source/PerformanceFish.Tests/ReflectionCaching.cs
@@ -0,0 +1,101 @@
+global using System;
+global using System.Collections.Generic;
+global using Xunit;
+global using Xunit.Abstractions;
+using HarmonyLib;
+
+namespace PerformanceFish.Tests;
+
+public class ReflectionCaching
+{
+ internal static void InitializeLog(ITestOutputHelper output)
+ {
+ Log.Config.Message = output.WriteLine;
+ Log.Config.Warning = output.WriteLine;
+ Log.Config.Error = message => throw new(message);
+ }
+
+ public class GetValue
+ {
+ public GetValue(ITestOutputHelper output) => InitializeLog(output);
+
+ private const int COUNT = 500;
+
+ public static IEnumerable Data
+ => new object[][]
+ {
+ new[] { nameof(_enum) },
+ new[] { nameof(_nullableWithoutValue) },
+ new[] { nameof(_nullableWithValue) },
+ new[] { nameof(_primitive) },
+ new[] { nameof(_reference) },
+ new[] { nameof(CONST) }
+ };
+
+ [Theory]
+ [MemberData(nameof(Data))]
+ public void Test(string name)
+ {
+ if (name == nameof(CONST))
+ {
+ Log.Message("sPeCiFiEd MeThOd Is NoT sUpPoRtEd!\nworks fine on mono however");
+ return;
+ }
+
+ var field = _type.GetField(name, AccessTools.all);
+ Assert.NotNull(field);
+
+ object result = null;
+
+ for (var i = 0; i < COUNT; i++)
+ System.ReflectionCaching.FieldInfoPatches.GetValue_Patch.Prefix(field, null, ref result);
+
+ Assert.Equal(result, field.GetValue(null));
+ }
+
+ private static TypeCode _enum = TypeCode.Int32;
+ private static int? _nullableWithoutValue = null;
+ private static int? _nullableWithValue = 1;
+ private static int _primitive = 0;
+ private static object _reference = new();
+ private const string CONST = "a";
+ private static Type _type = typeof(GetValue);
+ }
+
+ public class SetValue
+ {
+ public SetValue(ITestOutputHelper output) => InitializeLog(output);
+
+ private const int COUNT = 500;
+
+ public static IEnumerable Data
+ => new object[][]
+ {
+ new object[] { nameof(_enum), TypeCode.Byte },
+ new object[] { nameof(_nullableWithoutValue), 1 },
+ new object[] { nameof(_nullableWithValue), null },
+ new object[] { nameof(_primitive), 1 },
+ new object[] { nameof(_reference), new() }
+ };
+
+ [Theory]
+ [MemberData(nameof(Data))]
+ public void Test(string name, object input)
+ {
+ var field = _type.GetField(name, AccessTools.all);
+ Assert.NotNull(field);
+
+ for (var i = 0; i < COUNT; i++)
+ System.ReflectionCaching.FieldInfoPatches.SetValue_Patch.Prefix(field, null, input);
+
+ Assert.Equal(input, field.GetValue(null));
+ }
+
+ private static TypeCode _enum = TypeCode.Int32;
+ private static int? _nullableWithoutValue = null;
+ private static int? _nullableWithValue = 1;
+ private static int _primitive = 0;
+ private static object _reference = new();
+ private static Type _type = typeof(SetValue);
+ }
+}
\ No newline at end of file
diff --git a/Source/PerformanceFish/.editorconfig b/Source/PerformanceFish/.editorconfig
new file mode 100644
index 0000000..14c2131
--- /dev/null
+++ b/Source/PerformanceFish/.editorconfig
@@ -0,0 +1,185 @@
+[*.cs]
+
+file_header_template = Copyright (c) 2022 bradson\nThis Source Code Form is subject to the terms of the Mozilla Public\nLicense, v. 2.0. If a copy of the MPL was not distributed with this\nfile, You can obtain one at https://mozilla.org/MPL/2.0/.
+
+# CA2211: Non-constant fields should not be visible
+dotnet_diagnostic.CA2211.severity = none
+csharp_indent_labels = one_less_than_current
+csharp_using_directive_placement = outside_namespace:suggestion
+csharp_prefer_simple_using_statement = true:suggestion
+csharp_prefer_braces = when_multiline:suggestion
+csharp_style_namespace_declarations = file_scoped:suggestion
+csharp_style_prefer_method_group_conversion = true:silent
+csharp_style_prefer_top_level_statements = true:silent
+csharp_style_expression_bodied_methods = true:suggestion
+csharp_style_expression_bodied_constructors = true:suggestion
+csharp_style_expression_bodied_operators = true:suggestion
+csharp_style_expression_bodied_properties = true:suggestion
+csharp_style_expression_bodied_indexers = true:suggestion
+csharp_style_expression_bodied_accessors = true:suggestion
+csharp_style_expression_bodied_lambdas = true:suggestion
+csharp_style_expression_bodied_local_functions = true:suggestion
+csharp_style_throw_expression = true:suggestion
+csharp_style_prefer_null_check_over_type_check = true:silent
+csharp_prefer_simple_default_expression = true:suggestion
+csharp_style_prefer_local_over_anonymous_function = true:suggestion
+csharp_style_prefer_index_operator = true:suggestion
+csharp_style_prefer_range_operator = true:suggestion
+csharp_style_implicit_object_creation_when_type_is_apparent = true:suggestion
+
+[*.{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:suggestion
+dotnet_style_prefer_auto_properties = true:silent
+dotnet_style_object_initializer = true:suggestion
+dotnet_style_collection_initializer = true:suggestion
+dotnet_style_prefer_simplified_boolean_expressions = true:suggestion
+dotnet_style_prefer_conditional_expression_over_assignment = true:suggestion
+dotnet_style_prefer_conditional_expression_over_return = true:suggestion
+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:suggestion
+dotnet_style_prefer_simplified_interpolation = true:suggestion
+dotnet_style_namespace_match_folder = true:suggestion
+[*.cs]
+#### Naming styles ####
+
+# Naming rules
+
+dotnet_naming_rule.constant_should_be_all_upper.severity = suggestion
+dotnet_naming_rule.constant_should_be_all_upper.symbols = constant
+dotnet_naming_rule.constant_should_be_all_upper.style = all_upper
+
+dotnet_naming_rule.private_or_protected_field_should_be_camel_case_with__.severity = suggestion
+dotnet_naming_rule.private_or_protected_field_should_be_camel_case_with__.symbols = private_or_protected_field
+dotnet_naming_rule.private_or_protected_field_should_be_camel_case_with__.style = camel_case_with__
+
+dotnet_naming_rule.public_or_internal_field_should_be_camel_case.severity = silent
+dotnet_naming_rule.public_or_internal_field_should_be_camel_case.symbols = public_or_internal_field
+dotnet_naming_rule.public_or_internal_field_should_be_camel_case.style = camel_case
+
+# Symbol specifications
+
+dotnet_naming_symbols.constant.applicable_kinds = field, local
+dotnet_naming_symbols.constant.applicable_accessibilities = *
+dotnet_naming_symbols.constant.required_modifiers = const
+
+dotnet_naming_symbols.private_or_protected_field.applicable_kinds = field
+dotnet_naming_symbols.private_or_protected_field.applicable_accessibilities = private, protected, protected_internal, private_protected
+dotnet_naming_symbols.private_or_protected_field.required_modifiers =
+
+dotnet_naming_symbols.public_or_internal_field.applicable_kinds = field
+dotnet_naming_symbols.public_or_internal_field.applicable_accessibilities = public, internal
+dotnet_naming_symbols.public_or_internal_field.required_modifiers =
+
+# Naming styles
+
+dotnet_naming_style.all_upper.required_prefix =
+dotnet_naming_style.all_upper.required_suffix =
+dotnet_naming_style.all_upper.word_separator = _
+dotnet_naming_style.all_upper.capitalization = all_upper
+
+dotnet_naming_style.camel_case_with__.required_prefix = _
+dotnet_naming_style.camel_case_with__.required_suffix =
+dotnet_naming_style.camel_case_with__.word_separator =
+dotnet_naming_style.camel_case_with__.capitalization = camel_case
+
+dotnet_naming_style.camel_case.required_prefix =
+dotnet_naming_style.camel_case.required_suffix =
+dotnet_naming_style.camel_case.word_separator =
+dotnet_naming_style.camel_case.capitalization = camel_case
+dotnet_diagnostic.CA1805.severity = none
+csharp_style_prefer_tuple_swap = true:suggestion
+csharp_style_prefer_utf8_string_literals = true:suggestion
+csharp_style_inlined_variable_declaration = true:suggestion
+csharp_style_deconstructed_variable_declaration = true:suggestion
+csharp_style_unused_value_assignment_preference = discard_variable:suggestion
+csharp_style_unused_value_expression_statement_preference = discard_variable:silent
+csharp_prefer_static_local_function = true:suggestion
+csharp_style_allow_embedded_statements_on_same_line_experimental = true:silent
+csharp_style_allow_blank_lines_between_consecutive_braces_experimental = false:suggestion
+csharp_style_allow_blank_line_after_colon_in_constructor_initializer_experimental = false:suggestion
+csharp_style_conditional_delegate_call = true:suggestion
+csharp_style_prefer_switch_expression = true:suggestion
+csharp_style_prefer_pattern_matching = true:suggestion
+csharp_style_pattern_matching_over_is_with_cast_check = true:suggestion
+csharp_style_pattern_matching_over_as_with_null_check = true:suggestion
+csharp_style_prefer_not_pattern = true:suggestion
+csharp_style_prefer_extended_property_pattern = true:suggestion
+csharp_style_var_for_built_in_types = true:suggestion
+csharp_style_var_when_type_is_apparent = true:suggestion
+csharp_style_var_elsewhere = true:suggestion
+csharp_space_around_binary_operators = before_and_after
+
+[*.{cs,vb}]
+#### 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.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
+
+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.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_diagnostic.CA1838.severity = suggestion
+dotnet_diagnostic.CA1848.severity = suggestion
+dotnet_diagnostic.CA2215.severity = suggestion
+dotnet_style_readonly_field = true:silent
+dotnet_style_predefined_type_for_locals_parameters_members = true:suggestion
+dotnet_style_predefined_type_for_member_access = true:suggestion
+dotnet_style_require_accessibility_modifiers = for_non_interface_members:warning
+dotnet_style_allow_multiple_blank_lines_experimental = false:suggestion
+dotnet_style_allow_statement_immediately_after_block_experimental = true:silent
+dotnet_code_quality_unused_parameters = non_public:suggestion
+dotnet_style_parentheses_in_arithmetic_binary_operators = always_for_clarity:suggestion
+dotnet_style_parentheses_in_relational_binary_operators = always_for_clarity:suggestion
+dotnet_style_parentheses_in_other_binary_operators = always_for_clarity:suggestion
+dotnet_style_parentheses_in_other_operators = never_if_unnecessary:suggestion
+dotnet_style_qualification_for_field = false:suggestion
+dotnet_style_qualification_for_property = false:suggestion
+dotnet_style_qualification_for_method = false:suggestion
+dotnet_style_qualification_for_event = false:suggestion
+indent_style = tab
diff --git a/Source/PerformanceFish/AnalyzerFixes.cs b/Source/PerformanceFish/AnalyzerFixes.cs
new file mode 100644
index 0000000..418f892
--- /dev/null
+++ b/Source/PerformanceFish/AnalyzerFixes.cs
@@ -0,0 +1,468 @@
+// Copyright (c) 2022 bradson
+// This Source Code Form is subject to the terms of the Mozilla Public
+// License, v. 2.0. If a copy of the MPL was not distributed with this
+// file, You can obtain one at https://mozilla.org/MPL/2.0/.
+
+using System.Diagnostics.CodeAnalysis;
+using System.Linq;
+using System.Reflection.Emit;
+using System.Xml;
+
+namespace PerformanceFish;
+
+public static class AnalyzerFixes
+{
+ public static void Patch()
+ {
+ var modBase = AccessTools.Constructor(Reflection.Type("PerformanceAnalyzer", "Analyzer.Modbase"), new[] { typeof(ModContentPack) });
+ if (modBase is null)
+ return /*false*/;
+
+ var success = PerformanceFishMod.Harmony.Patch(modBase, postfix: new(methodof(XmlParser.CollectXmlData))) != null;
+
+ var methodTransplanting = Reflection.MethodInfo("PerformanceAnalyzer", "Analyzer.Profiling.MethodTransplanting", "UpdateMethod",
+ new[] { typeof(Type), typeof(MethodInfo) });
+
+ if (methodTransplanting is null
+ || PerformanceFishMod.Harmony.Patch(methodTransplanting, transpiler: new(methodof(UpdateMethod_Transpiler))) is null)
+ {
+ success = false;
+ }
+
+ var panelLogs = Reflection.MethodInfo("PerformanceAnalyzer", "Analyzer.Profiling.Panel_Logs", "RightClickDropDown");
+
+ if (panelLogs is null
+ || PerformanceFishMod.Harmony.Patch(panelLogs, postfix: new(methodof(PanelLogs_Postfix))) is null)
+ {
+ success = false;
+ }
+
+ return /*success*/;
+ }
+
+ ///
+ /// yes this is literally a prefix
+ ///
+ public static CodeInstructions UpdateMethod_Transpiler(CodeInstructions codes, MethodBase method)
+ {
+ yield return FishTranspiler.ArgumentAddress(method, "meth");
+ yield return FishTranspiler.Call(UpdateMethod_Prefix);
+ foreach (var code in codes)
+ yield return code;
+ }
+
+ public static void UpdateMethod_Prefix(ref MethodInfo meth)
+ {
+ if (AccessTools.EnumeratorMoveNext(meth) is { } moveNext)
+ meth = moveNext;
+ }
+
+ public static IEnumerable PanelLogs_Postfix(IEnumerable values, object log)
+ {
+ var method = ProfileLog_meth(log) as MethodInfo;
+ var returned = false;
+ foreach (var value in values)
+ {
+ yield return value;
+ if (IsValidMethodForOverriding(method) && !returned && value.Label == "Profile the internal methods of")
+ {
+ returned = true;
+ yield return new("Profile overriding methods of",
+ () => PatchOverridesForMethod(method, (Category)GUIController_CurrentCategory()));
+ }
+ }
+ }
+
+ private static bool IsValidMethodForOverriding([NotNullWhen(true)] MethodInfo? info)
+ => info is not null
+ && (info.IsVirtual || info.IsAbstract || info.GetBaseDefinition().DeclaringType != info.DeclaringType);
+
+ public enum Category
+ {
+ Settings,
+ Tick,
+ Update,
+ GUI,
+ Modder
+ }
+
+ public static void PatchOverridesForMethod(MethodInfo method, Category category)
+ {
+ if (OverridingMethodUtility.PatchedOverrides.Contains(method))
+ {
+ var signature = Utility_GetSignature(method, true);
+ Messages.Message("Have already patched overrides for the method " + signature, MessageTypeDefOf.CautionInput, false);
+ Utility_Warn("Trying to repeat patching already profiled overrides of method - " + signature);
+ }
+ else
+ {
+ PatchOverridesForMethodFull(method, category);
+ }
+ }
+
+ private static void PatchOverridesForMethodFull(MethodInfo method, Category category)
+ {
+ try
+ {
+ OverridingMethodUtility.Generate(method, category);
+ /*Task.Factory.StartNew(delegate
+ {
+ try
+ {
+ Modbase_Harmony().Patch(method, null, null, OverridingMethodUtility.OverrideProfiler);
+ }
+ catch (Exception e2)
+ {
+ Utility_ReportException(e2, "Failed to patch overriding methods for " + Utility_GetSignature(method, false));
+ }
+ });*/
+ }
+ catch (Exception e)
+ {
+ Utility_ReportException(e, "Failed to set up state to patch overriding methods");
+ OverridingMethodUtility.PatchedOverrides.Remove(method);
+ }
+ }
+
+ public static class OverridingMethodUtility
+ {
+ public static HashSet PatchedOverrides = new();
+
+ public static void ClearCaches() => PatchedOverrides.Clear();
+
+ public static void Generate(MethodInfo method, Category category)
+ {
+ PatchedOverrides.Add(method);
+
+ var name = $"{method.DeclaringType}:{method.Name}-overrides";
+
+ var methods = new HashSet();
+ var subClasses = method.DeclaringType.AllSubclasses();
+
+ foreach (var subClass in subClasses)
+ {
+ var overridingMethod = Array.Find(subClass.GetMethods(AccessTools.allDeclared), m => m.GetBaseDefinition() == method.GetBaseDefinition());
+ if (overridingMethod is null || overridingMethod.IsAbstract)
+ continue;
+
+ methods.Add(overridingMethod);
+
+ //Log.Message($"Added method {Utility_GetSignature(overridingMethod, true)} for profiling");
+ }
+
+ var type = DynamicTypeBuilder_CreateType(name, methods);
+ var entry = Entry_Create(name, (int)category, type, false, true);
+ GUIController_AddSpecificEntry((int)category, entry, type);
+ GUIController_SwapToEntry(name);
+ }
+ }
+
+ public static AccessTools.FieldRef ProfileLog_meth
+ => _profileLog_meth
+ ??= AccessTools.FieldRefAccess(Reflection.FieldInfo("PerformanceAnalyzer", "Analyzer.Profiling.ProfileLog", "meth"));
+ private static AccessTools.FieldRef? _profileLog_meth;
+
+ public static Func Modbase_Harmony
+ => _modbase_Harmony
+ ??= (Func)Reflection.MethodInfo("PerformanceAnalyzer", "Analyzer.Modbase", "get_Harmony")
+ .CreateDelegate(typeof(Func));
+ private static Func? _modbase_Harmony;
+
+ public static Action GUIController_AddEntry
+ => _guiController_AddEntry
+ ??= (Action)Reflection.MethodInfo("PerformanceAnalyzer", "Analyzer.Profiling.GUIController", "AddEntry")
+ .CreateDelegate(typeof(Action));
+ private static Action? _guiController_AddEntry;
+
+ public static Action GUIController_SwapToEntry
+ => _guiController_SwapToEntry
+ ??= (Action)Reflection.MethodInfo("PerformanceAnalyzer", "Analyzer.Profiling.GUIController", "SwapToEntry")
+ .CreateDelegate(typeof(Action));
+ private static Action? _guiController_SwapToEntry;
+
+ public static Func Utility_GetSignature
+ => _utility_GetSignature
+ ??= (Func)Reflection.MethodInfo("PerformanceAnalyzer", "Analyzer.Profiling.Utility", "GetSignature")
+ .CreateDelegate(typeof(Func));
+ private static Func? _utility_GetSignature;
+
+ public static Func GUIController_CurrentCategory
+ => _guiController_CurrentCategory
+ ??= (Func)Reflection.MethodInfo("PerformanceAnalyzer", "Analyzer.Profiling.GUIController", "get_CurrentCategory")
+ .CreateDelegate(typeof(Func));
+ private static Func? _guiController_CurrentCategory;
+
+ public static Action Utility_Warn
+ => _utility_Warn
+ ??= (Action)Reflection.MethodInfo("PerformanceAnalyzer", "Analyzer.Profiling.Utility", "Warn")
+ .CreateDelegate(typeof(Action));
+ private static Action? _utility_Warn;
+
+ public static Action Utility_ReportException
+ => _utility_ReportException
+ ??= (Action)Reflection.MethodInfo("PerformanceAnalyzer", "Analyzer.Profiling.Utility", "ReportException")
+ .CreateDelegate(typeof(Action));
+ private static Action? _utility_ReportException;
+
+ public static Action Utility_Error
+ => _utility_Error
+ ??= (Action)Reflection.MethodInfo("PerformanceAnalyzer", "Analyzer.Profiling.Utility", "Error")
+ .CreateDelegate(typeof(Action));
+ private static Action? _utility_Error;
+
+ public static CodeInstructions Parse_Transpiler(CodeInstructions CodeInstructions)
+ {
+ var codes = CodeInstructions.ToList();
+ var parseMethod = FishTranspiler.Call("PerformanceAnalyzer", "Analyzer.Profiling.XmlParser", "ParseMethod");
+ var errorMethod = FishTranspiler.Call("PerformanceAnalyzer", "Analyzer.Profiling.ThreadSafeLogger", "Error");
+
+ for (var i = 0; i < codes.Count; i++)
+ {
+ if (codes[i] == parseMethod)
+ {
+ yield return new(codes[i - 2]); //Ldloc xmlNode
+ yield return FishTranspiler.Call(ParseMethod_Fixed);
+ yield return FishTranspiler.Call, IEnumerable>>(GenCollection.AddRange);
+ i += 2; //removing OpCodes.Pop
+ }
+ else if (codes[i] == errorMethod)
+ {
+ yield return new(codes[i - 4]); //Ldloc xmlNode
+ yield return FishTranspiler.Call(ErrorFix);
+ }
+ else
+ {
+ yield return codes[i];
+ }
+ }
+ }
+
+ public static IEnumerable ParseMethod_Fixed(string info, XmlNode node)
+ => node.NodeType == XmlNodeType.Comment
+ ? Enumerable.Empty()
+ : ParseMethod(info);
+
+ private static IEnumerable ParseMethod(string info)
+ {
+ var array = info.Split(':');
+ if (array.Length != 2)
+ throw new ArgumentException($"Method must be specified as 'Namespace.Type1.Type2:MethodName. Got {info} instead.", nameof(info));
+
+ var methods = Reflection.MethodsOfName(AccessTools.TypeByName(array[0]), array[1]);
+ foreach (var method in methods)
+ {
+ yield return method.IsGenericMethodDefinition
+ ? method.MakeGenericMethod(method.GetGenericArguments().Select(t => t.GetGenericParameterConstraints() is var constraints2 && constraints2.Length != 0 ? constraints2[0] : typeof(object)).ToArray())
+ : method;
+ }
+ }
+
+ public static void ErrorFix(string message, XmlNode node)
+ {
+ if (node.NodeType == XmlNodeType.Comment)
+ return;
+
+ ThreadSafeLogger_Error(message);
+ }
+
+ public static Action ThreadSafeLogger_Error
+ => _threadSafeLogger_Error
+ ??= (Action)Reflection.MethodInfo("PerformanceAnalyzer", "Analyzer.Profiling.ThreadSafeLogger", "Error")
+ .CreateDelegate(typeof(Action));
+ private static Action? _threadSafeLogger_Error;
+
+ public static Func, Type> DynamicTypeBuilder_CreateType
+ => _dynamicTypeBuilder_CreateType
+ ??= (Func, Type>)Reflection.MethodInfo("PerformanceAnalyzer", "Analyzer.Profiling.DynamicTypeBuilder", "CreateType")
+ .CreateDelegate(typeof(Func, Type>));
+ private static Func, Type>? _dynamicTypeBuilder_CreateType;
+
+ public static Func> Utility_GetTypeMethods
+ => _utility_GetTypeMethods
+ ??= (Func>)Reflection.MethodInfo("PerformanceAnalyzer", "Analyzer.Profiling.Utility", "GetTypeMethods")
+ .CreateDelegate(typeof(Func>));
+ private static Func>? _utility_GetTypeMethods;
+
+ public static AccessTools.FieldRef Entry_tip
+ => _entry_tip
+ ??= AccessTools.FieldRefAccess(Type.GetType("Analyzer.Profiling.Entry, PerformanceAnalyzer"), "tip");
+ private static AccessTools.FieldRef? _entry_tip;
+
+ public static Action GUIController_AddSpecificEntry => _guiController_AddSpecificEntry ??= MakeGUIController_AddEntryMethod();
+ private static Action? _guiController_AddSpecificEntry;
+ private static Action MakeGUIController_AddEntryMethod()
+ {
+ var analyzer_Profiling_EntryType = Reflection.Type("PerformanceAnalyzer", "Analyzer.Profiling.Entry");
+ if (analyzer_Profiling_EntryType == null)
+ Log.Error("analyzer_Profiling_EntryType is null");
+
+ var gUIController_Tab = Reflection.MethodInfo("PerformanceAnalyzer", "Analyzer.Profiling.GUIController", "Tab");
+ if (gUIController_Tab == null)
+ Log.Error("gUIController_Tab is null");
+
+ var tab_entries = Reflection.FieldInfo("PerformanceAnalyzer", "Analyzer.Profiling.Tab", "entries");
+ if (tab_entries == null)
+ Log.Error("tab_entries is null");
+
+ /*var memberInfo_Name = AccessTools.PropertyGetter(typeof(MemberInfo), nameof(MemberInfo.Name));
+ if (memberInfo_Name == null)
+ Log.Error("memberInfo_Name is null");
+
+ var entry_Create = AccessTools.Method(analyzer_Profiling_EntryType, "Create");
+ if (entry_Create == null)
+ Log.Error("entry_Create is null");*/
+
+ var dictionary_Add = AccessTools.Method(typeof(Dictionary<,>).MakeGenericType(new[] { analyzer_Profiling_EntryType, typeof(Type) }),
+ "Add", new[] { analyzer_Profiling_EntryType, typeof(Type) });
+ if (dictionary_Add == null)
+ Log.Error("dictionary_Add is null");
+
+ var dynamicMethod = new DynamicMethod("GUIController_AddEntryMethod", typeof(void), new[] { typeof(int), typeof(object), typeof(Type) });
+ var il = dynamicMethod.GetILGenerator();
+
+ il.Emit(FishTranspiler.Argument(0)); //Category
+ il.Emit(FishTranspiler.Call(gUIController_Tab!)); //Tab(Category)
+ il.Emit(FishTranspiler.Field(tab_entries!)); //tab.entries
+ /*il.Emit(FishTranspiler.Argument(2)); //type
+ il.Emit(FishTranspiler.Call(memberInfo_Name)); //type.Name
+ il.Emit(FishTranspiler.LoadConstant(4)); //Category.Modder
+ il.Emit(FishTranspiler.Argument(2)); //type
+ il.Emit(FishTranspiler.LoadConstant(0)); //closeable = false
+ il.Emit(FishTranspiler.LoadConstant(1)); //dynGen = true
+ il.Emit(FishTranspiler.Call(entry_Create)); //Create(string, Category, Type, bool, bool)*/
+ il.Emit(FishTranspiler.Argument(1)); //entry
+ il.Emit(FishTranspiler.Argument(2)); //type
+ il.Emit(FishTranspiler.Call(dictionary_Add!)); //entries.Add(Entry, Type)
+ il.Emit(FishTranspiler.Return);
+
+ return (Action)dynamicMethod.CreateDelegate(typeof(Action));
+ }
+
+ public static Func Entry_Create => _entry_Create
+ ??= (Func)Reflection.MethodInfo("PerformanceAnalyzer", "Analyzer.Profiling.Entry", "Create").CreateDelegate(typeof(Func));
+ private static Func? _entry_Create;
+
+ //Copied from Wiri's analyzer fork, with above fixes integrated. The steam version lacks this functionality
+ public static class XmlParser
+ {
+ public static void CollectXmlData()
+ {
+ foreach (var dir in ModLister.AllActiveModDirs)
+ {
+ var xmlFiles = dir.GetFiles("DubsAnalyzer.xml");
+ if (xmlFiles.Length != 0)
+ {
+ foreach (var file in xmlFiles)
+ {
+ var doc = new XmlDocument();
+ doc.Load(file.OpenRead());
+
+ Parse(doc);
+ }
+ }
+ }
+ }
+
+ // Iterates through each child element in the document and attempts to extract method(s) from the strings inside the children
+ private static void Parse(XmlDocument doc)
+ => ForEach(doc.DocumentElement.ChildNodes, node =>
+ {
+ var methods = new HashSet();
+ var tip = string.Empty;
+ var category = (int)Category.Modder;
+ ForEach(node.ChildNodes, child =>
+ {
+ switch (child.Name.ToLowerInvariant())
+ {
+ case "category":
+ category = (int)Enum.Parse(typeof(Category), child.InnerText, true); //child.InnerText;
+ break;
+ case "tooltip":
+ case "tip":
+ case "description":
+ tip += child.InnerText;
+ break;
+ case "methods":
+ case "method":
+ AddMethodsFromChildNodes(child, methods, ParseMethod);
+ break;
+ case "derivedmethods":
+ AddMethodsFromChildNodes(child, methods, ParseDerivedMethods);
+ break;
+ case "types":
+ case "type":
+ AddMethodsFromChildNodes(child, methods, ParseTypeMethods);
+ break;
+ case "derivedtypes":
+ AddMethodsFromChildNodes(child, methods, ParseDerivedTypeMethods);
+ break;
+ case "nestedtype":
+ case "nestedtypes":
+ AddMethodsFromChildNodes(child, methods, ParseNestedTypeMethods);
+ break;
+ default:
+ ThreadSafeLogger_Error($"[Analyzer] Attempting to read unknown value from a DubsAnalyzer.xml, the given input was {child.Name}, it should have been either 'methods', 'types', 'derivedMethods', 'derivedTypes', 'nestedTypes', 'category' or 'tooltip'");
+ break;
+ }
+ });
+
+ var type = DynamicTypeBuilder_CreateType(node.Name, methods);
+ var entry = Entry_Create(node.Name, category, type, false, true);
+ if (tip != string.Empty)
+ Entry_tip(entry) = tip;
+ GUIController_AddSpecificEntry(category, entry, type);
+ });
+
+ private static void AddMethodsFromChildNodes(XmlNode node, HashSet methods, Func> func)
+ => ForEach(node.ChildNodes, childNode
+ => methods.AddRange(func(childNode.InnerText)));
+
+ private static void ForEach(XmlNodeList nodes, Action action)
+ {
+ foreach (XmlNode node in nodes)
+ {
+ if (node.NodeType != XmlNodeType.Comment)
+ action(node);
+ }
+ }
+
+ private static IEnumerable ParseDerivedMethods(string str)
+ {
+ foreach (var method in ParseMethod(str))
+ {
+ var subClasses = method.DeclaringType.AllSubclasses();
+
+ foreach (var subClass in subClasses)
+ {
+ var overridingMethod = Array.Find(subClass.GetMethods(AccessTools.allDeclared), m => m.GetBaseDefinition() == method.GetBaseDefinition());
+ if (overridingMethod is null || overridingMethod.IsAbstract)
+ continue;
+
+ yield return overridingMethod;
+ }
+ }
+ }
+
+ private static IEnumerable ParseTypeMethods(string str) => Utility_GetTypeMethods(AccessTools.TypeByName(str), false);
+
+ private static IEnumerable ParseNestedTypeMethods(string str)
+ {
+ var type = AccessTools.TypeByName(str);
+ foreach (var subType in type.GetNestedTypes())
+ {
+ foreach (var method in Utility_GetTypeMethods(subType, false))
+ yield return method;
+ }
+ }
+
+ private static IEnumerable ParseDerivedTypeMethods(string str)
+ {
+ var type = AccessTools.TypeByName(str);
+ foreach (var subType in type.AllSubclasses())
+ {
+ foreach (var method in Utility_GetTypeMethods(subType, false))
+ yield return method;
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/Source/PerformanceFish/Cache/ByIndex.cs b/Source/PerformanceFish/Cache/ByIndex.cs
new file mode 100644
index 0000000..2de54f8
--- /dev/null
+++ b/Source/PerformanceFish/Cache/ByIndex.cs
@@ -0,0 +1,196 @@
+// Copyright (c) 2022 bradson
+// This Source Code Form is subject to the terms of the Mozilla Public
+// License, v. 2.0. If a copy of the MPL was not distributed with this
+// file, You can obtain one at https://mozilla.org/MPL/2.0/.
+
+using System.Diagnostics.CodeAnalysis;
+using System.Linq;
+
+namespace PerformanceFish.Cache;
+public class ByIndex : IList, ICollection
+{
+ private static ByIndex _get = new();
+ [ThreadStatic]
+ private static ByIndex? _getThreadStatic;
+
+ public static ByIndex Get
+ => /*UnityData.IsInMainThread ? _get
+ :*/ _getThreadStatic ??= Utility.AddNew>();
+
+ public static ByIndex GetDirectly => _get;
+
+ static ByIndex() => Utility.All.Add(_get);
+
+ protected T_out?[] _items = Array.Empty();
+ protected object? _syncRoot;
+
+ public T_out? this[int index]
+ {
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ get
+ {
+ ResizeIfNecessary(index);
+ return _items[index];
+ }
+ set
+ {
+ ResizeIfNecessary(index);
+ _items[index] = value;
+ }
+ }
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ [return: MaybeNull]
+ public ref T_out GetReference(int index)
+ {
+ ResizeIfNecessary(index);
+ return ref _items[index];
+ }
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public bool TryGetValue(int index, out T_out? value)
+ {
+ value = this[index];
+ return !value!.Equals(default!);
+ }
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ protected void ResizeIfNecessary(int checkedIndex)
+ {
+ if (checkedIndex >= _items.Length)
+ ResizeFor(checkedIndex);
+ }
+
+ protected void ResizeFor(int index)
+ {
+ Array.Resize(ref _items, Mathf.NextPowerOfTwo(index + 1));
+ ResizeIfNecessary(index); // just in case
+ }
+
+ public int Count => _items.Length;
+
+ public bool IsReadOnly => false;
+
+ object ICollection.SyncRoot
+ => _syncRoot
+ ??= Interlocked.CompareExchange(ref _syncRoot!, new(), null!);
+
+ bool ICollection.IsSynchronized => false;
+
+ [DoesNotReturn]
+ public void Add(T_out? item) => throw new NotSupportedException();
+
+ public void Clear()
+ {
+ if (_items.Length > 0)
+ Array.Clear(_items, 0, _items.Length);
+ }
+
+ public bool Contains(T_out? item) => Array.IndexOf(_items, item) >= 0;
+
+ public void CopyTo(T_out?[] array, int arrayIndex) => _items.CopyTo(array, arrayIndex);
+
+ public IEnumerator GetEnumerator() => _items.AsEnumerable().GetEnumerator();
+
+ public int IndexOf(T_out? item) => Array.IndexOf(_items, item);
+
+ public void Insert(int index, T_out? item) => this[index] = item;
+
+ [DoesNotReturn]
+ public bool Remove(T_out? item) => throw new NotSupportedException();
+
+ public void RemoveAt(int index)
+ {
+ if (index < Count)
+ _items[index] = default;
+ }
+
+ IEnumerator IEnumerable.GetEnumerator() => _items.GetEnumerator();
+ void ICollection.CopyTo(Array array, int index) => _items.CopyTo(array, index);
+}
+
+public class ByIndex : ByIndex
+ where T_in : notnull
+{
+ private static ByIndex _get = new();
+ [ThreadStatic]
+ private static ByIndex? _getThreadStatic;
+
+ public new static ByIndex Get
+ => /*UnityData.IsInMainThread ? _get
+ :*/ _getThreadStatic ??= Utility.AddNew>();
+
+ public new static ByIndex GetDirectly => _get;
+
+ static ByIndex() => Utility.All.Add(_get);
+
+ public unsafe T_out? this[T_in key]
+ {
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ get => this[FunctionPointers.IndexGetter.Default(key)];
+ set => this[FunctionPointers.IndexGetter.Default(key)] = value;
+ }
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public bool TryGetValue(T_in key, [NotNullWhen(true)] out T_out? value)
+ {
+ value = this[key];
+ return !value!.Equals(default!);
+ }
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ [return: MaybeNull]
+ public unsafe ref T_out GetReference(T_in key)
+ => ref GetReference(FunctionPointers.IndexGetter.Default(key))!;
+
+}
+
+///
+/// Seems slower than the dictionary based Cache.ByInt.
+///
+public class ByIndex : ByIndex>
+ where T_first : notnull where T_second : notnull
+{
+ private static ByIndex _get = new();
+ [ThreadStatic]
+ private static ByIndex? _getThreadStatic;
+
+ public new static ByIndex Get
+ => /*UnityData.IsInMainThread ? _get
+ :*/ _getThreadStatic ??= Utility.AddNew>();
+
+ public new static ByIndex GetDirectly => _get;
+
+ static ByIndex() => Utility.All.Add(_get);
+
+ public T_out? this[T_first first, T_second second]
+ {
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ get => GetInner(first)[second];
+ set => GetInner(first)[second] = value;
+ }
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ private unsafe ByIndex GetInner(T_first first)
+ {
+ var firstIndex = FunctionPointers.IndexGetter.Default(first);
+ ResizeIfNecessary(firstIndex);
+
+ ref var inner = ref _items[firstIndex];
+ inner ??= new();
+
+ return inner!;
+ }
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ [return: MaybeNull]
+ public ref T_out GetReference(T_first first, T_second second)
+ => ref GetInner(first).GetReference(second)!;
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public bool TryGetValue(T_first first, T_second second, [NotNullWhen(true)] out T_out? value)
+ {
+ value = this[first, second];
+ return !value!.Equals(default!);
+ }
+}
\ No newline at end of file
diff --git a/Source/PerformanceFish/Cache/ByInt.cs b/Source/PerformanceFish/Cache/ByInt.cs
new file mode 100644
index 0000000..ce8110b
--- /dev/null
+++ b/Source/PerformanceFish/Cache/ByInt.cs
@@ -0,0 +1,291 @@
+// Copyright (c) 2022 bradson
+// This Source Code Form is subject to the terms of the Mozilla Public
+// License, v. 2.0. If a copy of the MPL was not distributed with this
+// file, You can obtain one at https://mozilla.org/MPL/2.0/.
+
+using System.Diagnostics.CodeAnalysis;
+
+namespace PerformanceFish.Cache;
+public struct ByInt
+ where T_in : notnull
+ where T_result : IIsRefreshable
+{
+ private static Dictionary, T_result> _get = new();
+ [ThreadStatic]
+ private static Dictionary, T_result>? _getThreadStatic;
+
+ public static Dictionary, T_result> Get
+ => /*UnityData.IsInMainThread ? _get
+ :*/ _getThreadStatic ??= Utility.AddNew, T_result>>();
+
+ public static Dictionary, T_result> GetDirectly => _get;
+
+ static ByInt() => Utility.All.Add(_get);
+
+ public int Key;
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public unsafe ByInt(T_in key) => Key = FunctionPointers.IndexGetter.Default(key);
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static ref T_result GetValue(T_in key)
+ {
+ ref var cache = ref Get.TryGetReferenceUnsafe(new(key));
+
+ if (Unsafe.IsNullRef(ref cache))
+ {
+ Get[new(key)] = Reflection.New();
+ cache = ref Get.GetReference(new(key))!;
+ goto RefreshCache;
+ }
+
+ if (!cache.ShouldRefreshNow)
+ return ref cache!;
+
+ RefreshCache:
+ cache.ShouldRefreshNow = false;
+ Get[new(key)] = cache.SetNewValue(key);
+
+ return ref cache!;
+ }
+
+ [MethodImpl(MethodImplOptions.NoInlining)]
+ public static unsafe ref T_result TryGetReferenceUnsafe(int key)
+ => ref Unsafe.AsRef(Unsafe.AsPointer(ref Get.TryGetReferenceUnsafe(ref Unsafe.As>(ref key))));
+
+ [MethodImpl(MethodImplOptions.NoInlining)]
+ public static unsafe ref T_result TryGetReferenceChecked(int key, out bool result)
+ {
+ ref var value = ref Unsafe.AsRef(Unsafe.AsPointer(ref Get.TryGetReferenceUnsafe(ref Unsafe.As>(ref key))));
+ result = !Unsafe.IsNullRef(ref value) && !value.ShouldRefreshNow;
+ return ref value!;
+ }
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static bool TryGetValue(T_in key, [MaybeNullWhen(false)] out T_result value)
+ => Get.TryGetValue(new(key), out value)
+ && !value.ShouldRefreshNow;
+
+ public static T_result GetValueAndRefreshNow(T_in key)
+ {
+ var cache = Get.TryGetValue(new(key));
+ cache ??= Reflection.New();
+ cache.ShouldRefreshNow = false;
+ Get[new(key)] = cache.SetNewValue(key);
+ return cache;
+ }
+}
+
+public struct ByIntRefreshable : IEquatable>
+ where T_first : notnull where T_second : notnull
+ where T_result : IIsRefreshable, T_result>
+{
+ private static EqualityComparer> _comparer = new Comparer();
+ private class Comparer : EqualityComparer>
+ {
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public override bool Equals(ByIntRefreshable x, ByIntRefreshable y)
+ => x.First == y.First
+ && x.Second == y.Second;
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public override int GetHashCode(ByIntRefreshable obj)
+ => HashCode.Combine(obj.First, obj.Second);
+ }
+
+ private static Dictionary, T_result> _get = new(_comparer);
+ [ThreadStatic]
+ private static Dictionary, T_result>? _getThreadStatic;
+
+ public static Dictionary, T_result> Get
+ => /*UnityData.IsInMainThread ? _get
+ :*/ _getThreadStatic ??= Utility.AddNew, T_result>>(_comparer);
+
+ public static Dictionary, T_result> GetDirectly => _get;
+
+ static ByIntRefreshable() => Utility.All.Add(_get);
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static unsafe ref T_result GetValue(ref ByIntRefreshable key)
+ {
+ // CS8347 without this crap
+ ref var cache = ref Unsafe.AsRef(Unsafe.AsPointer(ref Get.TryGetReferenceUnsafe(ref key)));
+
+ if (Unsafe.IsNullRef(ref cache))
+ {
+ Get[key] = Reflection.New();
+ cache = ref Unsafe.AsRef(Unsafe.AsPointer(ref Get.TryGetReferenceUnsafe(ref key)))!;
+ goto RefreshCache;
+ }
+
+ if (!cache.ShouldRefreshNow)
+ return ref cache!;
+
+ RefreshCache:
+ cache.ShouldRefreshNow = false;
+ Get[key] = cache.SetNewValue(key);
+
+ return ref cache!;
+ }
+
+ [MethodImpl(MethodImplOptions.NoInlining)]
+ public static unsafe ref T_result TryGetReferenceUnsafe(int second, int first)
+ => ref Unsafe.AsRef(Unsafe.AsPointer(ref Get.TryGetReferenceUnsafe(ref Unsafe.As>(ref first))));
+
+ [MethodImpl(MethodImplOptions.NoInlining)]
+ public static unsafe ref T_result TryGetReferenceChecked(int second, int first, out bool result)
+ {
+ ref var value = ref Unsafe.AsRef(Unsafe.AsPointer(ref Get.TryGetReferenceUnsafe(ref Unsafe.As>(ref first))));
+ result = !Unsafe.IsNullRef(ref value) && !value.ShouldRefreshNow;
+ return ref value!;
+ }
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static bool TryGetValue(ByIntRefreshable key, [MaybeNullWhen(false)] out T_result value)
+ => Get.TryGetValue(key, out value)
+ && !value.ShouldRefreshNow;
+
+ public static T_result GetValueAndRefreshNow(ByIntRefreshable key)
+ {
+ var cache = Get.TryGetValue(ref key);
+ cache ??= Reflection.New();
+ cache.ShouldRefreshNow = false;
+ Get[key] = cache.SetNewValue(key);
+ return cache;
+ }
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public unsafe ByIntRefreshable(T_first first, T_second second)
+ {
+ First = FunctionPointers.IndexGetter.Default(first);
+ Second = FunctionPointers.IndexGetter.Default(second);
+ }
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public ByIntRefreshable(int first, int second)
+ {
+ First = first;
+ Second = second;
+ }
+
+ public int First;
+ public int Second;
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public bool Equals(ByIntRefreshable other)
+ => First == other.First
+ && Second == other.Second;
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public override int GetHashCode()
+ => HashCode.Combine(First, Second);
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public override bool Equals(object obj)
+ => obj is ByIntRefreshable cache
+ && Equals(cache);
+
+ public static bool operator ==(ByIntRefreshable left, ByIntRefreshable right)
+ => left.Equals(right);
+
+ public static bool operator !=(ByIntRefreshable left, ByIntRefreshable right)
+ => !(left == right);
+}
+
+public struct ByInt : IEquatable>
+ where T_first : notnull where T_second : notnull where T_third : notnull
+ where T_result : IIsRefreshable, T_result>
+{
+ private static Dictionary, T_result> _get = new();
+ [ThreadStatic]
+ private static Dictionary, T_result>? _getThreadStatic;
+
+ public static Dictionary, T_result> Get
+ => /*UnityData.IsInMainThread ? _get
+ :*/ _getThreadStatic ??= Utility.AddNew, T_result>>();
+
+ public static Dictionary, T_result> GetDirectly => _get;
+
+ static ByInt() => Utility.All.Add(_get);
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static unsafe ref T_result GetValue(ByInt key)
+ {
+ ref var cache = ref Unsafe.AsRef(Unsafe.AsPointer(ref Get.TryGetReferenceUnsafe(ref key)));
+
+ if (Unsafe.IsNullRef(ref cache))
+ {
+ Get[key] = Reflection.New();
+ cache = ref Unsafe.AsRef(Unsafe.AsPointer(ref Get.TryGetReferenceUnsafe(ref key)))!;
+ goto RefreshCache;
+ }
+
+ if (!cache.ShouldRefreshNow)
+ return ref cache!;
+
+ RefreshCache:
+ cache.ShouldRefreshNow = false;
+ Get[key] = cache.SetNewValue(key);
+
+ return ref cache!;
+ }
+
+ [MethodImpl(MethodImplOptions.NoInlining)]
+ public static unsafe ref T_result TryGetReferenceUnsafe(int second, int first, int third)
+ => ref Unsafe.AsRef(Unsafe.AsPointer(ref Get.TryGetReferenceUnsafe(ref Unsafe.As>(ref first))));
+
+ [MethodImpl(MethodImplOptions.NoInlining)]
+ public static unsafe ref T_result TryGetReferenceChecked(int second, int first, out bool result)
+ {
+ ref var value = ref Unsafe.AsRef(Unsafe.AsPointer(ref Get.TryGetReferenceUnsafe(ref Unsafe.As>(ref first))));
+ result = !Unsafe.IsNullRef(ref value) && !value.ShouldRefreshNow;
+ return ref value!;
+ }
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static bool TryGetValue(ByInt key, out T_result value)
+ => Get.TryGetValue(key, out value)
+ && !value.ShouldRefreshNow;
+
+ public static T_result GetValueAndRefreshNow(ByInt key)
+ {
+ var cache = Get.TryGetValue(ref key);
+ cache ??= Reflection.New();
+ cache.ShouldRefreshNow = false;
+ Get[key] = cache.SetNewValue(key);
+ return cache;
+ }
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public unsafe ByInt(T_first first, T_second second, T_third third)
+ {
+ First = FunctionPointers.IndexGetter.Default(first);
+ Second = FunctionPointers.IndexGetter.Default(second);
+ Third = FunctionPointers.IndexGetter.Default(third);
+ }
+
+ public int First;
+ public int Second;
+ public int Third;
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public bool Equals(ByInt other)
+ => First == other.First
+ && Second == other.Second
+ && Third == other.Third;
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public override int GetHashCode()
+ => HashCode.Combine(First, Second, Third);
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public override bool Equals(object obj)
+ => obj is ByInt cache
+ && Equals(cache);
+
+ public static bool operator ==(ByInt left, ByInt right)
+ => left.Equals(right);
+
+ public static bool operator !=(ByInt left, ByInt right)
+ => !(left == right);
+}
\ No newline at end of file
diff --git a/Source/PerformanceFish/Cache/ByMap.cs b/Source/PerformanceFish/Cache/ByMap.cs
new file mode 100644
index 0000000..0eea91d
--- /dev/null
+++ b/Source/PerformanceFish/Cache/ByMap.cs
@@ -0,0 +1,46 @@
+// Copyright (c) 2022 bradson
+// This Source Code Form is subject to the terms of the Mozilla Public
+// License, v. 2.0. If a copy of the MPL was not distributed with this
+// file, You can obtain one at https://mozilla.org/MPL/2.0/.
+
+namespace PerformanceFish.Cache;
+public static class ByMap where T : new()
+{
+ private static List<(Map map, T value)> _cache = new();
+ [ThreadStatic]
+ private static List<(Map map, T value)>? _cacheThreadStatic;
+
+ public static List<(Map map, T value)> GetCache
+ => UnityData.IsInMainThread ? _cache
+ : _cacheThreadStatic ??= Utility.AddNew>();
+
+ public static List<(Map map, T value)> GetCacheDirectly => _cache;
+
+ static ByMap() => Utility.All.Add(_cache);
+
+ private static Func Initialize { get; }
+ = typeof(MapComponent).IsAssignableFrom(typeof(T))
+ ? AccessTools.MethodDelegate>(AccessTools.Method(typeof(Map), nameof(Map.GetComponent), Array.Empty(), new[] { typeof(T) }))
+ // missing T constraints cause the compiler to complain when trying to just use map => map.GetComponent
+ : _ => new();
+
+ public static T GetFor(Map map)
+ {
+ var cache = GetCache;
+ for (var i = 0; i < cache.Count; i++)
+ {
+ var cacheEntry = cache[i];
+ if (cacheEntry.map == map)
+ return cacheEntry.value;
+
+ if (Find.Maps.Count <= i + 1
+ && !Find.Maps.Contains(cacheEntry.map))
+ {
+ cache.RemoveAt(i);
+ i--;
+ }
+ }
+ cache.Add((map, Initialize(map)));
+ return cache[^1].value;
+ }
+}
\ No newline at end of file
diff --git a/Source/PerformanceFish/Cache/ByReference.cs b/Source/PerformanceFish/Cache/ByReference.cs
new file mode 100644
index 0000000..3c741af
--- /dev/null
+++ b/Source/PerformanceFish/Cache/ByReference.cs
@@ -0,0 +1,271 @@
+// Copyright (c) 2022 bradson
+// This Source Code Form is subject to the terms of the Mozilla Public
+// License, v. 2.0. If a copy of the MPL was not distributed with this
+// file, You can obtain one at https://mozilla.org/MPL/2.0/.
+
+namespace PerformanceFish.Cache;
+
+public static class ByReference
+ where T_first : notnull
+{
+ private static Dictionary _get = new();
+ [ThreadStatic]
+ private static Dictionary? _getThreadStatic;
+
+ public static Dictionary Get
+ {
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ get
+ => /*UnityData.IsInMainThread ? _get
+ :*/ _getThreadStatic ??= Utility.AddNew>();
+ }
+
+ public static Dictionary GetDirectly => _get;
+
+ static ByReference() => Utility.All.Add(_get);
+
+ public static unsafe void Initialize()
+ {
+ try
+ {
+ _ = FisheryLib.FunctionPointers.Equals.Default;
+ _ = FisheryLib.FunctionPointers.GetHashCode.Default;
+ }
+ catch (Exception ex)
+ {
+ Log.Error($"Performance Fish encountered an exception while trying to initialize {typeof(ByReference).FullDescription()}\n{ex}");
+ }
+ }
+}
+
+public struct ByReference : IEquatable>
+ where T_first : notnull where T_second : notnull
+{
+ private static Dictionary, T_result> _get = new();
+ [ThreadStatic]
+ private static Dictionary, T_result>? _getThreadStatic;
+
+ public static Dictionary, T_result> Get
+ {
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ get
+ => /*UnityData.IsInMainThread ? _get
+ :*/ _getThreadStatic ??= Utility.AddNew, T_result>>();
+ }
+
+ public static Dictionary, T_result> GetDirectly => _get;
+
+ static ByReference() => Utility.All.Add(_get);
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public ByReference(T_first first, T_second second)
+ {
+ First = first;
+ Second = second;
+ }
+
+ public static unsafe void Initialize()
+ {
+ try
+ {
+ _ = FisheryLib.FunctionPointers.Equals.Default;
+ _ = FisheryLib.FunctionPointers.Equals.Default;
+ _ = FisheryLib.FunctionPointers.Equals>.Default;
+ _ = FisheryLib.FunctionPointers.GetHashCode.Default;
+ _ = FisheryLib.FunctionPointers.GetHashCode.Default;
+ _ = FisheryLib.FunctionPointers.GetHashCode>.Default;
+ }
+ catch (Exception ex)
+ {
+ Log.Error($"Performance Fish encountered an exception while trying to initialize {typeof(ByReference).FullDescription()}\n{ex}");
+ }
+ }
+
+ public T_first First;
+
+ public T_second Second;
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public bool Equals(ByReference other)
+ => First.Equals(other.First)
+ && Second.Equals(other.Second);
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public override int GetHashCode()
+ => HashCode.Combine(First, Second);
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public override bool Equals(object obj)
+ => obj is ByReference cache
+ && Equals(cache);
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static bool operator ==(ByReference left, ByReference right)
+ => left.Equals(right);
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static bool operator !=(ByReference left, ByReference right)
+ => !(left == right);
+}
+
+public struct ByReference : IEquatable>
+ where T_first : notnull where T_second : notnull where T_third : notnull
+{
+ private static Dictionary, T_result> _get = new();
+ [ThreadStatic]
+ private static Dictionary, T_result>? _getThreadStatic;
+
+ public static Dictionary, T_result> Get
+ {
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ get
+ /*UnityData.IsInMainThread ? _get
+ :*/ => _getThreadStatic ??= Utility.AddNew, T_result>>();
+ }
+
+ public static Dictionary, T_result> GetDirectly => _get;
+
+ static ByReference() => Utility.All.Add(_get);
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public ByReference(T_first first, T_second second, T_third third)
+ {
+ First = first;
+ Second = second;
+ Third = third;
+ _hashCode = HashCode.Combine(first, second, third);
+ }
+
+ public static unsafe void Initialize()
+ {
+ try
+ {
+ _ = FisheryLib.FunctionPointers.Equals.Default;
+ _ = FisheryLib.FunctionPointers.Equals.Default;
+ _ = FisheryLib.FunctionPointers.Equals.Default;
+ _ = FisheryLib.FunctionPointers.Equals>.Default;
+ _ = FisheryLib.FunctionPointers.GetHashCode.Default;
+ _ = FisheryLib.FunctionPointers.GetHashCode.Default;
+ _ = FisheryLib.FunctionPointers.GetHashCode.Default;
+ _ = FisheryLib.FunctionPointers.GetHashCode>.Default;
+ }
+ catch (Exception ex)
+ {
+ Log.Error($"Performance Fish encountered an exception while trying to initialize {typeof(ByReference).FullDescription()}\n{ex}");
+ }
+ }
+
+ public T_first First;
+
+ public T_second Second;
+
+ public T_third Third;
+
+ private int _hashCode;
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public bool Equals(ByReference other)
+ => First.Equals(other.First)
+ && Second.Equals(other.Second)
+ && Third.Equals(other.Third);
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public override int GetHashCode() => _hashCode;
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public override bool Equals(object obj)
+ => obj is ByReference cache
+ && Equals(cache);
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static bool operator ==(ByReference left, ByReference right)
+ => left.Equals(right);
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static bool operator !=(ByReference left, ByReference right)
+ => !(left == right);
+}
+
+public struct ByReference : IEquatable>
+ where T_first : notnull where T_second : notnull where T_third : notnull where T_fourth : notnull
+{
+ private static Dictionary, T_result> _get = new();
+ [ThreadStatic]
+ private static Dictionary, T_result>? _getThreadStatic;
+
+ public static Dictionary, T_result> Get
+ {
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ get
+ /*UnityData.IsInMainThread ? _get
+ :*/ => _getThreadStatic ??= Utility.AddNew, T_result>>();
+ }
+
+ public static Dictionary, T_result> GetDirectly => _get;
+
+ static ByReference() => Utility.All.Add(_get);
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public ByReference(T_first first, T_second second, T_third third, T_fourth fourth)
+ {
+ First = first;
+ Second = second;
+ Third = third;
+ Fourth = fourth;
+ _hashCode = HashCode.Combine(first, second, third, fourth);
+ }
+
+ public static unsafe void Initialize()
+ {
+ try
+ {
+ _ = FisheryLib.FunctionPointers.Equals.Default;
+ _ = FisheryLib.FunctionPointers.Equals