// Copyright (c) 2023 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.Linq; using Mono.Cecil; using Mono.Cecil.Cil; using Mono.Cecil.Rocks; using PerformanceFish.Events; using PerformanceFish.Prepatching; using FieldAttributes = Mono.Cecil.FieldAttributes; using MethodImplAttributes = Mono.Cecil.MethodImplAttributes; namespace PerformanceFish.Defs; public sealed class ThingDefPatches : ClassWithFishPrepatches { public const string FISH_HASH_FIELD_NAME = "FishHash"; public sealed class GetHashCodePatch : FishClassPrepatch { public override string? Description { get; } #if V1_6 = "Disabled on 1.6 because Verse.Def now already caches defNameHash and overrides GetHashCode itself."; #else = "Def.GetHashCode is overridden to return defName.GetHashCode, which is rather slow as that's a string. " + "This patch caches the hashcode in a field added through prepatcher and simply returns that instead."; #endif public override Type Type { get; } = #if V1_6 typeof(Def); #else typeof(BuildableDef); #endif public override void FreePatch(TypeDefinition typeDefinition) { #if V1_6 return; #else var module = typeDefinition.Module; var fishHashField = new FieldDefinition(FISH_HASH_FIELD_NAME, FieldAttributes.Private, module.TypeSystem.Int32); typeDefinition.Fields.Insert(0, fishHashField); var constructorBody = typeDefinition.GetConstructors().First().Body; constructorBody.GetILProcessor().InsertRange(constructorBody.Instructions.Count - 2, OpCodes.Ldarg_0, (OpCodes.Ldc_I4, -42), (OpCodes.Stfld, fishHashField)); constructorBody.OptimizeMacros(); var getHashCodeBody = typeDefinition.GetMethod(nameof(BuildableDef.GetHashCode))!.Body; var variables = getHashCodeBody.Variables; variables.Clear(); var hashVariable = new VariableDefinition(module.TypeSystem.Int32); variables.Add(hashVariable); var instructions = getHashCodeBody.Instructions; instructions.Clear(); var ilProcessor = getHashCodeBody.GetILProcessor(); ilProcessor.InsertRange(0, OpCodes.Ldarg_0, (OpCodes.Ldfld, fishHashField), (OpCodes.Stloc, hashVariable), (OpCodes.Ldloc, hashVariable), (OpCodes.Ldc_I4, -42), // 5 bne_un_s ^2 OpCodes.Ldarg_0, (OpCodes.Call, methodof(CreateLongHash)), OpCodes.Ret, (OpCodes.Ldloc, hashVariable), OpCodes.Ret); ilProcessor.InsertAt(5, OpCodes.Bne_Un_S, instructions[^2]); getHashCodeBody.MaxStackSize = 2; getHashCodeBody.Method.ImplAttributes |= MethodImplAttributes.AggressiveInlining; getHashCodeBody.OptimizeMacros(); #endif } // [MethodImpl(MethodImplOptions.AggressiveInlining)] // public static int ReplacementBody(BuildableDef instance) // { // var hash = instance.FishHash; // return hash != -42 ? hash : CreateLongHash(instance); // } [MethodImpl(MethodImplOptions.NoInlining)] public static int CreateLongHash(BuildableDef instance) { var defName = instance.defName; if (defName is null) return 0; var defHash = defName.GetHashCode(); if (defName is "" or Def.DefaultDefName) return defHash; ref var cacheHash = ref Lazy.FishHashField(instance); cacheHash = defHash != -42 ? defHash : defHash + 1; return cacheHash; } private static class Lazy { internal static readonly AccessTools.FieldRef FishHashField = AccessTools.FieldRefAccess(FISH_HASH_FIELD_NAME); static Lazy() { // prevent beforefieldinit } } } public sealed class BaseMarketValuePatch : FishPrepatch { public override string? Description { get; } = "Caches the frequently accessed and never changing ThingDef.BaseMarketValue property. Also adds proper " + "exception handling in case of errors in the market value calculation."; public override MethodBase TargetMethodBase { get; } = AccessTools.DeclaredPropertyGetter(typeof(ThingDef), nameof(ThingDef.BaseMarketValue)); public override void Transpiler(ILProcessor ilProcessor, ModuleDefinition module) => ilProcessor.ReplaceBodyWith(ReplacementBody); [MethodImpl(MethodImplOptions.AggressiveInlining)] public static float ReplacementBody(ThingDef instance) { var cacheValue = instance.BaseStatsCache()[(int)Stats.MarketValue]; return !float.IsNaN(cacheValue) ? cacheValue : GetUpdatedValue(instance); } [MethodImpl(MethodImplOptions.NoInlining)] public static float GetUpdatedValue(ThingDef instance) => Current.Game is null ? GetValueSafely(instance) : instance.BaseStatsCache()[(int)Stats.MarketValue] = GetValueSafely(instance); public static float GetValueSafely(ThingDef instance) => ThingDefPatches.GetValueSafely(instance, StatDefOf.MarketValue); public static float LogAndFixMarketValue(BuildableDef def, ThingDef? stuff, Exception exception) { Guard.IsNotNull(def); Log.Error($"Exception thrown while calculating {StatDefOf.MarketValue.label} for def '{ def.defName}' from mod '{ def.modContentPack?.Name ?? "null"}'. Attempting to fix this now so it doesn't happen again.\n{ exception}"); RecipeDef? recipe; try { recipe = StatWorker_MarketValue.CalculableRecipe(def); // this is slow, but gets cached by perf optimizer. Might be worth replicating } catch (Exception ex) { Log.Error($"Exception thrown while calculating recipe for def '{def.defName}' from mod '{ def.modContentPack?.Name ?? "null"}'.\n{ex}"); recipe = null; } if (recipe?.ingredients is { } ingredients) ingredients.RemoveAll(static ingredientCount => ingredientCount is null); if (def.CostList is { } costList) costList.RemoveAll(static thingDefCountClass => thingDefCountClass?.thingDef is null); try { return def.GetStatValueAbstract(StatDefOf.MarketValue, stuff); } catch (Exception ex) { Log.Error($"Exception thrown while calculating market value for def '{def.defName}' from mod '{ def.modContentPack?.Name ?? "null"}' again. Fix failed, F.\n{ex}"); return 0f; } } static BaseMarketValuePatch() => StaticEvents.StaticConstructorOnStartupCalled += static () => StatCaching.StatExceptionHandlers[StatDefOf.MarketValue] = LogAndFixMarketValue; } public sealed class BaseMassPatch : FishPrepatch { public override string? Description { get; } = "Caches the frequently accessed and never changing ThingDef.BaseMass property. Also adds basic " + "exception handling in case of errors in the mass calculation."; public override MethodBase TargetMethodBase { get; } = AccessTools.DeclaredPropertyGetter(typeof(ThingDef), nameof(ThingDef.BaseMass)); public override void Transpiler(ILProcessor ilProcessor, ModuleDefinition module) => ilProcessor.ReplaceBodyWith(ReplacementBody); [MethodImpl(MethodImplOptions.AggressiveInlining)] public static float ReplacementBody(ThingDef instance) { var cacheValue = instance.BaseStatsCache()[(int)Stats.Mass]; return !float.IsNaN(cacheValue) ? cacheValue : GetUpdatedValue(instance); } [MethodImpl(MethodImplOptions.NoInlining)] public static float GetUpdatedValue(ThingDef instance) => Current.Game is null ? GetValueSafely(instance) : instance.BaseStatsCache()[(int)Stats.Mass] = GetValueSafely(instance); public static float GetValueSafely(ThingDef instance) => ThingDefPatches.GetValueSafely(instance, StatDefOf.Mass); } public sealed class BaseFlammabilityPatch : FishPrepatch { public override string? Description { get; } = "Caches the frequently accessed and never changing ThingDef.BaseFlammability property. Also adds basic " + "exception handling in case of errors in the mass calculation."; public override MethodBase TargetMethodBase { get; } = AccessTools.DeclaredPropertyGetter(typeof(ThingDef), nameof(ThingDef.BaseFlammability)); public override void Transpiler(ILProcessor ilProcessor, ModuleDefinition module) => ilProcessor.ReplaceBodyWith(ReplacementBody); [MethodImpl(MethodImplOptions.AggressiveInlining)] public static float ReplacementBody(ThingDef instance) { var cacheValue = instance.BaseStatsCache()[(int)Stats.Flammability]; return !float.IsNaN(cacheValue) ? cacheValue : GetUpdatedValue(instance); } [MethodImpl(MethodImplOptions.NoInlining)] public static float GetUpdatedValue(ThingDef instance) => Current.Game is null ? GetValueSafely(instance) : instance.BaseStatsCache()[(int)Stats.Flammability] = GetValueSafely(instance); public static float GetValueSafely(ThingDef instance) => ThingDefPatches.GetValueSafely(instance, StatDefOf.Flammability); } public sealed class BaseMaxHitPointsPatch : FishPrepatch { public override string? Description { get; } = "Caches the frequently accessed and never changing ThingDef.BaseMaxHitPoints property. Also adds basic " + "exception handling in case of errors in the mass calculation."; public override MethodBase TargetMethodBase { get; } = AccessTools.DeclaredPropertyGetter(typeof(ThingDef), nameof(ThingDef.BaseMaxHitPoints)); public override void Transpiler(ILProcessor ilProcessor, ModuleDefinition module) => ilProcessor.ReplaceBodyWith(ReplacementBody); [MethodImpl(MethodImplOptions.AggressiveInlining)] public static int ReplacementBody(ThingDef instance) { var cacheValue = instance.BaseStatsCache()[(int)Stats.MaxHitPoints]; return Mathf.RoundToInt(!float.IsNaN(cacheValue) ? cacheValue : GetUpdatedValue(instance)); } [MethodImpl(MethodImplOptions.NoInlining)] public static float GetUpdatedValue(ThingDef instance) => Current.Game is null ? GetValueSafely(instance) : instance.BaseStatsCache()[(int)Stats.MaxHitPoints] = GetValueSafely(instance); public static float GetValueSafely(ThingDef instance) => ThingDefPatches.GetValueSafely(instance, StatDefOf.MaxHitPoints); } [MethodImpl(MethodImplOptions.NoInlining)] public static float GetValueSafely(ThingDef instance, StatDef stat) { try { return instance.GetStatValueAbstract(stat); } catch (Exception ex) { return StatCaching.StatExceptionHandlers.GetOrAdd(stat)(instance, null, ex); } } public enum Stats { MarketValue, Mass, Flammability, MaxHitPoints } }