// 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 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 : FishPatch { public override string Description => "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 MethodBase TargetMethodInfo => AccessTools.FirstMethod(DelegateInstanceType, m => m.GetParameters() is { Length: 1 } parms && parms[0].ParameterType == typeof(Region) && m.ReturnType == typeof(bool) && PatchProcessor.ReadMethodBody(m).Any(pair => (pair.Key == OpCodes.Call || pair.Key == OpCodes.Callvirt) && pair.Value 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? || (BlackListedModExtensions.Exists(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.ShouldRefreshNow) { cache.thingDefs = UpdateCache(bill, r, cache.thingDefs); cache.ShouldRefreshNow = false; RecipeIngredientCache.Get[bill] = cache; } list = GetList(r.listerThings.listsByDef, cache.thingDefs); } } ActualLoop(list, r, instance.Pawn, instance.BaseValidator, instance.BillGiverIsPawn); //list.Clear(); instance.RegionsProcessed++; if (WorkGiver_DoBill.newRelevantThings.Count > 0 && instance.RegionsProcessed > instance.AdjacentRegionsAvailable) { //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)) { instance.FoundAll = true; return true; } } return false; } 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?.Exists(e => BlackListedModExtensions.Contains(e.GetType())) ?? false); 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)) { 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()) { if (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; var allowedDefCount = filter.allowedDefs.Count; if (allowedDefCount < 1) continue; if (allowedDefCount == 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.GetValue(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}."); // didn't trigger in 1.3, but now does. Fuck } 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), t => AccessTools.FirstMethod(t, 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), t => AccessTools.FirstMethod(t, 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."); [SuppressMessage("Style", "IDE1006:Naming Styles")] private static DelegateInstance _delegateInstance { get; } = (DelegateInstance)Activator.CreateInstance(typeof(DelegateInstanceImplementation<>).MakeGenericType(DelegateInstanceType)); [SuppressMessage("Style", "IDE1006:Naming Styles")] 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 => "Part of the DoBill optimization"; public override Delegate TargetMethodGroup => 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 => "Part of the DoBill optimization"; public override Delegate TargetMethodGroup => WorkGiver_DoBill.TryFindBestIngredientsInSet_NoMixHelper; public static void Prefix(ref bool alreadySorted, Bill? bill) => alreadySorted = bill?.billStack is { } stack #if V1_4 && stack.billGiver is not Building_MechGestator #endif ; public static CodeInstructions? Transpiler(CodeInstructions codes, MethodBase method) { var i_variable = FishTranspiler.FirstLocalVariable(method, typeof(int)); return codes.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[] { FishTranspiler.FirstLocalVariable(method, typeof(IngredientCount)) .WithLabels(code.ExtractLabels()), FishTranspiler.FirstArgument(method, typeof(Bill)), FishTranspiler.Call(MarkIngredientCountAsFound), code } ); } 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; public int Compare(Thing x, Thing y) => (x.Position - rootCell).LengthHorizontalSquared.CompareTo((y.Position - rootCell).LengthHorizontalSquared); } public struct RecipeIngredientCacheValue { public IngredientCacheList thingDefs; private int _nextRefreshTick; public RecipeIngredientCacheValue(IngredientCacheList thingDefs) { this.thingDefs = thingDefs; _nextRefreshTick = Current.Game.tickManager.TicksGame + 384 + Math.Abs(Rand.Int % 256); } public bool ShouldRefreshNow { get => _nextRefreshTick < Current.Game.tickManager.TicksGame; set => _nextRefreshTick = value ? 0 : Current.Game.tickManager.TicksGame + 384 + Math.Abs(Rand.Int % 256); } } }