// 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 System.Reflection.Emit; using Verse.AI; using IngredientCacheList = PerformanceFish.Utility.KeyedList Defs, bool Found)>; using RecipeIngredientCache = PerformanceFish.Cache.ByIndex; namespace PerformanceFish.JobSystem; public class WorkGiver_DoBillOptimization : ClassWithFishPatches { public static List BlackListedModExtensions => _blackListedModExtensions ??= new() { Reflection.Type("VFEAncients", "VFEAncients.RecipeExtension_Mend") }; private static List? _blackListedModExtensions; public class TryFindBestIngredientsHelper_InnerDelegate_Patch : FirstPriorityFishPatch { public override string Description { get; } = "Optimizes WorkGiver_DoBill to only check things that get accepted by a bill's filter for its " + "ingredient lookup, instead of looping over everything and then checking filters afterwards. " + "Also caches these filter acceptances. Large performance impact."; public override bool Enabled { set { if (base.Enabled == value) return; Get().Enabled = Get().Enabled = base.Enabled = value; } } public override MethodBase TargetMethodInfo { get; } = AccessTools.FirstMethod(DelegateInstanceType, static m => m.GetParameters() is { Length: 1 } parameterInfos && parameterInfos[0].ParameterType == typeof(Region) && m.ReturnType == typeof(bool) && MethodBodyReader.GetInstructions(null, m).Any(static instruction => (instruction.opcode == OpCodes.Call || instruction.opcode == OpCodes.Callvirt) && instruction.operand is MethodInfo info && info == AccessTools.Method(typeof(ListerThings), nameof(ListerThings.ThingsMatching)))); public static CodeInstructions Transpiler(CodeInstructions codeInstructions, MethodBase method) => Reflection.MakeReplacementCall(AccessTools.Method( typeof(TryFindBestIngredientsHelper_InnerDelegate_Patch), nameof(Replacement), generics: new[] { DelegateInstanceType })); public static bool Replacement(T __instance, Region r) where T : class { var instance = _delegateInstance.Get(__instance); var thingValidator = _thingValidatorInstance.Get(instance.ThingValidator.Target); List list; if (thingValidator is null) // <-- TryFindBestFixedIngredients, used by biosculptors, goes here. Fuck them { list = GetDefaultList(r); } else { var bill = thingValidator.Bill; if (HasNoFilters(bill) // apparently applies to a rim reborn benches? #if V1_4 || bill.billStack?.billGiver is Building_MechGestator #endif || (BlackListedModExtensions.Exists(static b => b != null) && IsBlackListed(bill))) // <-- Fuck these too { list = GetDefaultList(r); } else { ref var cache = ref RecipeIngredientCache.Get.GetReference(bill); if (cache.thingDefs is null || cache.Dirty) { cache.thingDefs = UpdateCache(bill, r, cache.thingDefs); cache.Update(bill); } list = GetList(r.listerThings.listsByDef, cache.thingDefs); } } ActualLoop(list, r, instance.Pawn, instance.BaseValidator, instance.BillGiverIsPawn); instance.RegionsProcessed++; if (WorkGiver_DoBill.newRelevantThings.Count <= 0 || instance.RegionsProcessed <= instance.AdjacentRegionsAvailable) return false; //WorkGiver_DoBill.relevantThings.AddRange(WorkGiver_DoBill.newRelevantThings); InsertAtCorrectPosition(WorkGiver_DoBill.relevantThings, WorkGiver_DoBill.newRelevantThings, instance.Pawn); WorkGiver_DoBill.newRelevantThings.Clear(); if (!instance.FoundAllIngredientsAndChoose(WorkGiver_DoBill.relevantThings)) return false; instance.FoundAll = true; return true; } private static bool HasNoFilters(Bill bill) => bill.ingredientFilter is null || bill.recipe.fixedIngredientFilter is null; /*private static bool HasNoFilters(Bill bill) { if (bill.ingredientFilter is null || bill.recipe.fixedIngredientFilter is null) { return true; } var ingredients = bill.recipe.ingredients; for (var i = 0; i < ingredients.Count; i++) { if (ingredients[i].filter is null) return true; } return false; }*/ private static bool IsBlackListed(Bill? bill) => bill is null || bill.recipe.modExtensions.ExistsAndNotNull(static e => BlackListedModExtensions.Contains(e.GetType())); private static List GetDefaultList(Region r) => r.ListerThings.ThingsMatching(ThingRequest.ForGroup(ThingRequestGroup.HaulableEver)); public static void InsertAtCorrectPosition(List list, List thingsToInsert, Pawn pawn) { var count = thingsToInsert.Count; var comparer = _insertAtCorrectPositionComparer; for (var i = 0; i < count; i++) { var thing = thingsToInsert[i]; comparer.rootCell = WorkGiver_DoBill.GetBillGiverRootCell(thingsToInsert[i], pawn); var index = ~list.BinarySearch(thing, comparer); if (index < 0) index = ~index; list.Insert(index, thing); } } private static readonly ThingPositionComparer _insertAtCorrectPositionComparer = new(); public static void ActualLoop(List list, Region r, Pawn pawn, Predicate baseValidator, bool billGiverIsPawn) { var count = list.Count; for (var i = 0; i < count; i++) { var thing = list[i]; if (WorkGiver_DoBill.processedThings.Contains(thing) || !ReachabilityWithinRegion.ThingFromRegionListerReachable(thing, r, PathEndMode.ClosestTouch, pawn) || (billGiverIsPawn && thing.def.IsMedicine) || !baseValidator(thing)) { continue; } WorkGiver_DoBill.newRelevantThings.Add(thing); WorkGiver_DoBill.processedThings.Add(thing); } } public static List GetList(Dictionary> listsByDef, IngredientCacheList cacheList) { var list = _tempListForIngredients; list.Clear(); var set = _tempSetForIngredientDefs; set.Clear(); for (var i = 0; i < cacheList.Count; i++) { var (defs, found) = cacheList[i].Value; if (found) continue; var count = defs.Count; for (var j = 0; j < count; j++) { if (!listsByDef.TryGetValue(defs[j], out var listByDef) || !listByDef.Any() || (i > 0 && !set.Add(defs[j]))) { continue; } list.AddRange(listByDef); } } return list; } public static IngredientCacheList UpdateCache(Bill bill, Region r, IngredientCacheList? cacheList) { var recipe = bill.recipe; var ingredients = recipe.ingredients; var listsByDef = r.Map.listerThings.listsByDef; var result = cacheList ?? new(3); for (var i = 0; i < ingredients.Count; i++) // generally not larger than 3 in vanilla and anything that tries to be { if (result.TryGetValue(ingredients[i], out var ingredientList)) { ingredientList.Defs.Clear(); ingredientList.Found = false; } else { result[ingredients[i]] = ingredientList = new(new(), false); } var filter = ingredients[i].filter; switch (filter.allowedDefs.Count) { case < 1: continue; case 1: ingredientList.Defs.Add(filter.allowedDefs.First()); continue; } var smallestFilter = bill.ingredientFilter; var mediumFilter = recipe.fixedIngredientFilter; var largestFilter = filter; OrderFiltersBySize(ref smallestFilter, ref mediumFilter, ref largestFilter); var cachedSmallestList = AllowedDefsListCache .GetAndCheck(smallestFilter).Defs; if (cachedSmallestList.Count != smallestFilter.allowedDefs.Count) { Log.Warning($"Cached list count: {cachedSmallestList.Count}, actual filter list count: { smallestFilter.allowedDefs.Count}. Forcing a refresh now to fix."); ThingFilterPatches.ForceSynchronizeCache(smallestFilter); Log.Warning($"New cached list count: {cachedSmallestList.Count}, actual filter list count: { smallestFilter.allowedDefs.Count}."); } var smallestListCount = cachedSmallestList.Count; for (var k = 0; k < smallestListCount; k++) { var def = cachedSmallestList[k]; if (listsByDef.TryGetValue(def, out var listByDef) && listByDef.Any() && mediumFilter.Allows(def) && largestFilter.Allows(def)) { ingredientList.Defs.Add(def); } } } return result; } private static void OrderFiltersBySize(ref ThingFilter smallestFilter, ref ThingFilter mediumFilter, ref ThingFilter largestFilter) { if (smallestFilter.allowedDefs.Count > mediumFilter.allowedDefs.Count) (smallestFilter, mediumFilter) = (mediumFilter, smallestFilter); if (smallestFilter.allowedDefs.Count > largestFilter.allowedDefs.Count) (smallestFilter, largestFilter) = (largestFilter, smallestFilter); if (mediumFilter.allowedDefs.Count > largestFilter.allowedDefs.Count) (mediumFilter, largestFilter) = (largestFilter, mediumFilter); } private static readonly List _tempListForIngredients = new(); private static readonly HashSet _tempSetForIngredientDefs = new(); } private static Type DelegateInstanceType { get; } = AccessTools.FirstInner(typeof(WorkGiver_DoBill), static t => AccessTools.FirstMethod(t, static m => m.Name.Contains($"<{nameof(WorkGiver_DoBill.TryFindBestIngredientsHelper)}>")) != null && AccessTools.Field(t, "pawn") != null && AccessTools.Field(t, "baseValidator") != null && AccessTools.Field(t, "billGiverIsPawn") != null && AccessTools.Field(t, "regionsProcessed") != null && AccessTools.Field(t, "adjacentRegionsAvailable") != null && AccessTools.Field(t, "foundAllIngredientsAndChoose") != null && AccessTools.Field(t, "foundAll") != null && AccessTools.Field(t, "thingValidator") != null) ?? throw new("Performance Fish failed to find the target type for its WorkGiver_DoBill patch. " + "This means that specific patch won't apply."); private static Type ThingValidatorInstanceType { get; } = AccessTools.FirstInner(typeof(WorkGiver_DoBill), static t => AccessTools.FirstMethod(t, static m => m.Name.Contains($"<{nameof(WorkGiver_DoBill.TryFindBestBillIngredients)}>")) != null && AccessTools.Field(t, "bill") != null //&& AccessTools.Field(t, "chosen") != null //&& AccessTools.Field(t, "billGiver") != null /*&& AccessTools.Field(t, "pawn") != null*/) ?? throw new("Performance Fish failed to find the thingValidator type for its WorkGiver_DoBill patch. " + "This likely flat out breaks the entire patch."); // ReSharper disable once InconsistentNaming private static DelegateInstance _delegateInstance { get; } = (DelegateInstance)Activator.CreateInstance( typeof(DelegateInstanceImplementation<>).MakeGenericType(DelegateInstanceType)); // ReSharper disable once InconsistentNaming private static ThingValidatorInstance _thingValidatorInstance { get; } = (ThingValidatorInstance)Activator.CreateInstance( typeof(ThingValidatorInstanceImplementation<>).MakeGenericType(ThingValidatorInstanceType)); private abstract class DelegateInstance { public abstract DelegateInstance Get(object obj); public abstract Pawn Pawn { get; set; } public abstract Predicate BaseValidator { get; set; } public abstract bool BillGiverIsPawn { get; set; } public abstract int RegionsProcessed { get; set; } public abstract int AdjacentRegionsAvailable { get; set; } public abstract Predicate> FoundAllIngredientsAndChoose { get; set; } public abstract bool FoundAll { get; set; } public abstract Predicate ThingValidator { get; set; } } private class DelegateInstanceImplementation : DelegateInstance where T : class { #pragma warning disable CS8618 private T _instance; #pragma warning restore CS8618 public override DelegateInstance Get(object obj) { _instance = (T)obj; return this; } public override Pawn Pawn { get => _pawn(_instance); set => _pawn(_instance) = value; } public override Predicate BaseValidator { get => _baseValidator(_instance); set => _baseValidator(_instance) = value; } public override bool BillGiverIsPawn { get => _billGiverIsPawn(_instance); set => _billGiverIsPawn(_instance) = value; } public override int RegionsProcessed { get => _regionsProcessed(_instance); set => _regionsProcessed(_instance) = value; } public override int AdjacentRegionsAvailable { get => _adjacentRegionsAvailable(_instance); set => _adjacentRegionsAvailable(_instance) = value; } public override Predicate> FoundAllIngredientsAndChoose { get => _foundAllIngredientsAndChoose(_instance); set => _foundAllIngredientsAndChoose(_instance) = value; } public override bool FoundAll { get => _foundAll(_instance); set => _foundAll(_instance) = value; } public override Predicate ThingValidator { get => _thingValidator(_instance); set => _thingValidator(_instance) = value; } private static AccessTools.FieldRef _pawn = AccessTools.FieldRefAccess("pawn"); private static AccessTools.FieldRef> _baseValidator = AccessTools.FieldRefAccess>("baseValidator"); private static AccessTools.FieldRef _billGiverIsPawn = AccessTools.FieldRefAccess("billGiverIsPawn"); private static AccessTools.FieldRef _regionsProcessed = AccessTools.FieldRefAccess("regionsProcessed"); private static AccessTools.FieldRef _adjacentRegionsAvailable = AccessTools.FieldRefAccess("adjacentRegionsAvailable"); private static AccessTools.FieldRef>> _foundAllIngredientsAndChoose = AccessTools.FieldRefAccess>>("foundAllIngredientsAndChoose"); private static AccessTools.FieldRef _foundAll = AccessTools.FieldRefAccess("foundAll"); private static AccessTools.FieldRef> _thingValidator = AccessTools.FieldRefAccess>("thingValidator"); } private abstract class ThingValidatorInstance { public abstract ThingValidatorInstance? Get(object obj); public abstract Bill Bill { get; set; } //public abstract Thing BillGiver { get; set; } } private class ThingValidatorInstanceImplementation : ThingValidatorInstance where T : class { #pragma warning disable CS8618 private T _instance; #pragma warning restore CS8618 public override ThingValidatorInstance? Get(object obj) { if (obj is T t) { _instance = t; return this; } else { return null; } } public override Bill Bill { get => _bill(_instance); set => _bill(_instance) = value; } //public override Thing BillGiver { get => _billGiver(_instance); set => _billGiver(_instance) = value; } private static AccessTools.FieldRef _bill = AccessTools.FieldRefAccess("bill"); //private static AccessTools.FieldRef _billGiver = AccessTools.FieldRefAccess("billGiver"); } public class TryFindBestIngredientsHelper_Patch : FishPatch { public override string Description { get; } = "Part of the DoBill optimization"; public override bool Enabled { set { if (base.Enabled == value) return; Get().Enabled = base.Enabled = value; } } public override Delegate TargetMethodGroup { get; } = WorkGiver_DoBill.TryFindBestIngredientsHelper; public static void Postfix(Predicate thingValidator) { var thingValidatorInstance = _thingValidatorInstance.Get(thingValidator.Target); if (thingValidatorInstance is null) return; var cache = RecipeIngredientCache.Get[thingValidatorInstance.Bill].thingDefs; if (cache is null) return; var items = cache._items; for (var i = 0; i < items.Length; i++) { ref var item = ref items[i]; if (item.Value.Found) item = new(item.Key, new(item.Value.Defs, false)); } } } public class TryFindBestIngredientsInSet_NoMixHelper_Patch : FishPatch { public override string Description { get; } = "Part of the DoBill optimization. Bills that need multiple ingredients, like for example steel and " + "components for guns, normally scan their surroundings for all those ingredients and count them, until " + "enough of both is found. This patch removes ingredients from the lookup when enough of them is found, " + "allowing the patched method to skip counting them and go straight to the other missing ingredient. In " + "case of steel and components that could mean not having to go over 100s of steel stacks at times. " + "Prevents large spikes from these situations, does nothing for all other bills."; public override bool Enabled { set { if (base.Enabled == value) return; Get().Enabled = base.Enabled = value; } } public override Delegate TargetMethodGroup { get; } = WorkGiver_DoBill.TryFindBestIngredientsInSet_NoMixHelper; public static void Prefix(ref bool alreadySorted, Bill? bill) => alreadySorted = alreadySorted || (bill?.billStack is { } stack #if V1_4 // ReSharper disable once MergeIntoPattern && stack.billGiver is not Building_MechGestator #endif ); public static CodeInstructions Transpiler(CodeInstructions instructions, MethodBase method, ILGenerator generator) { var i_variable = FishTranspiler.FirstLocalVariable(method, typeof(int)); var new_i_label = generator.DefineLabel(); return instructions.ReplaceAt((codes, i) => i + 3 < codes.Count && codes[i] == i_variable.Load() && codes[i + 1] == FishTranspiler.Constant(1) // i++; && codes[i + 2] == FishTranspiler.Add && codes[i + 3] == i_variable.Store(), code => new[] { new(OpCodes.Br_S, new_i_label), // else FishTranspiler.FirstLocalVariable(method, typeof(IngredientCount)) .WithLabels(code.ExtractLabels()), FishTranspiler.FirstArgument(method, typeof(Bill)), FishTranspiler.Call(MarkIngredientCountAsFound), code.WithLabels(new_i_label) }); // if (!flag) // { // if (missingIngredients == null) // return false; // missingIngredients.Add(ingredientCount); // } // else // { // MarkIngredientCountAsFound(ingredientCount, bill); // } } public static void MarkIngredientCountAsFound(IngredientCount ingredientCount, Bill? bill) { if (bill?.billStack is not { } billStack #if V1_4 || billStack.billGiver is Building_MechGestator #endif ) { return; } var cache = RecipeIngredientCache.Get[bill].thingDefs; if (cache is null) return; ref var cacheItem = ref cache._items[cache.IndexOfKey(ingredientCount)]; if ((cache.Count == 1 /*&& cacheItem.Value.Defs.Count == 1*/) || bill.recipe?.workerCounterClass != typeof(RecipeWorkerCounter)) { return; } cacheItem = new(cacheItem.Key, new(cacheItem.Value.Defs, true)); } } public class ThingPositionComparer : IComparer { public IntVec3 rootCell; [MethodImpl(MethodImplOptions.AggressiveInlining)] public int Compare(Thing x, Thing y) => (x.Position - rootCell).LengthHorizontalSquared.CompareTo( (y.Position - rootCell).LengthHorizontalSquared); } public record struct RecipeIngredientCacheValue { public IngredientCacheList? thingDefs; private int _nextRefreshTick; public void Update(Bill bill) => _nextRefreshTick = TickHelper.Add(GenTicks.TickRareInterval * 2, bill.loadID); public bool Dirty { [MethodImpl(MethodImplOptions.AggressiveInlining)] get => TickHelper.Past(_nextRefreshTick); } } }