From c454eae2dca1069d36d40ba3d0c13a898004b8ea Mon Sep 17 00:00:00 2001 From: Maeyanie Date: Thu, 11 Jun 2026 03:40:35 -0400 Subject: [PATCH 1/2] Fix multi-minute freeze generating anvil interaction help on large modlists BlockAnvil.GetPlacedBlockInteractionHelp queries GetRequiredAnvilTier for every handbook stack of every IAnvilWorkable item. With CollectibleBehaviorCastToolHead attached to everything matching ToolHeadSelector, each query resolved the metal material from scratch: - The ItemStack overload of GetOrCacheMetalMaterial bypassed the material cache for all behavior-based workables (any collectible whose class is not IAnvilWorkable), calling the uncached resolver every time. - Failed resolutions (null) were never cached, so items with no metal material re-ran the full fallback on every call. - The fallback linearly scanned all smithing recipes and all grid recipes x ingredients per call. Fix: route both fallback paths of the ItemStack overload through the collectible-level cache; cache negative results once MetalMaterialLoader has resolved its materials (new MaterialsResolved flag prevents premature nulls from poisoning the cache); and replace the linear recipe scans in GetSmithingRecipe, GetSmithingRecipesAsIngredient and GetGridRecipesAsIngredient with one-time reverse indexes keyed by collectible code, built lazily via ObjectCacheUtil. On a large modlist this cuts the first anvil tooltip from ~2 minutes of freeze to ~200 ms. Co-Authored-By: Claude Fable 5 --- .../Common/Metal/MetalMaterialExtensions.cs | 13 ++-- .../Common/Metal/MetalMaterialLoader.cs | 2 + SmithingPlus/Util/CollectibleExtensions.cs | 60 +++++++++++++------ 3 files changed, 53 insertions(+), 22 deletions(-) diff --git a/SmithingPlus/Common/Metal/MetalMaterialExtensions.cs b/SmithingPlus/Common/Metal/MetalMaterialExtensions.cs index 32a8749..32efffe 100644 --- a/SmithingPlus/Common/Metal/MetalMaterialExtensions.cs +++ b/SmithingPlus/Common/Metal/MetalMaterialExtensions.cs @@ -15,8 +15,13 @@ public static class MetalMaterialExtensions public static MetalMaterial? GetOrCacheMetalMaterial(this CollectibleObject collObj, ICoreAPI api) { - var metalMaterial = - CacheHelper.GetOrAdd(Core.MetalMaterialCache, collObj.Code, () => collObj.GetMetalMaterial(api)); + var cache = Core.MetalMaterialCache; + if (cache.TryGetValue(collObj.Code, out var cached)) return cached; + var metalMaterial = collObj.GetMetalMaterial(api); + // Negative results are only meaningful once the loader has resolved its materials; + // before that point every lookup returns null and must not poison the cache. + if (metalMaterial != null || api.GetModSystem()?.MaterialsResolved == true) + cache[collObj.Code] = metalMaterial; return metalMaterial; } @@ -144,10 +149,10 @@ public static bool HasMetalMaterialSimple(this CollectibleObject collObj) public static MetalMaterial? GetOrCacheMetalMaterial(this ItemStack itemStack, ICoreAPI api) { var collObj = itemStack.Collectible; - if (collObj is not IAnvilWorkable anvilWorkable) return collObj?.GetMetalMaterial(api); + if (collObj is not IAnvilWorkable anvilWorkable) return collObj?.GetOrCacheMetalMaterial(api); var ingotStack = anvilWorkable.GetBaseMaterial(itemStack); var metalMaterial = ingotStack.Collectible.GetOrCacheMetalMaterial(api); - return metalMaterial ?? collObj.GetMetalMaterial(api); + return metalMaterial ?? collObj.GetOrCacheMetalMaterial(api); } // Use when what matters is the processed result (e.g., iron bloom > iron, blister steel > steel) diff --git a/SmithingPlus/Common/Metal/MetalMaterialLoader.cs b/SmithingPlus/Common/Metal/MetalMaterialLoader.cs index c64c79c..caec8e6 100644 --- a/SmithingPlus/Common/Metal/MetalMaterialLoader.cs +++ b/SmithingPlus/Common/Metal/MetalMaterialLoader.cs @@ -14,6 +14,7 @@ public class MetalMaterialLoader : ModSystem { private readonly Dictionary _metalMaterials = new(); public Dictionary ResolvedMaterials { get; private set; } = new(); + public bool MaterialsResolved { get; private set; } public override double ExecuteOrder() { @@ -81,6 +82,7 @@ public override void AssetsFinalize(ICoreAPI api) ResolvedMaterials = _metalMaterials .Where(kvp => kvp.Value.Resolved) .ToDictionary(kvp => kvp.Key, kvp => kvp.Value); + MaterialsResolved = true; Core.Logger.Notification("[MetalMaterial] Done resolving metal materials."); Core.Logger.Notification( $"[MetalMaterial] Resolved {resolvedCount} out of {_metalMaterials.Count} metal materials."); diff --git a/SmithingPlus/Util/CollectibleExtensions.cs b/SmithingPlus/Util/CollectibleExtensions.cs index 30bec46..39b69d5 100644 --- a/SmithingPlus/Util/CollectibleExtensions.cs +++ b/SmithingPlus/Util/CollectibleExtensions.cs @@ -73,33 +73,57 @@ public static bool MatchesToolHeadSelector(this CollectibleObject collObj, bool public static SmithingRecipe? GetSmithingRecipe(this CollectibleObject collObj, ICoreAPI api) { - var smithingRecipe = api.ModLoader - .GetModSystem() - .SmithingRecipes - .FirstOrDefault(r => r.Output.ResolvedItemstack.Collectible.Code.Equals(collObj.Code)); - return smithingRecipe; + var byOutput = ObjectCacheUtil.GetOrCreate(api, $"{Core.ModId}:smithingRecipesByOutput", () => + { + var dict = new Dictionary(); + foreach (var recipe in api.ModLoader.GetModSystem().SmithingRecipes) + { + var code = recipe?.Output?.ResolvedItemstack?.Collectible?.Code; + if (code != null) dict.TryAdd(code, recipe!); + } + + return dict; + }); + return byOutput.TryGetValue(collObj.Code, out var smithingRecipe) ? smithingRecipe : null; } public static IEnumerable GetSmithingRecipesAsIngredient(this CollectibleObject collObj, ICoreAPI api) { - var smithingRecipes = - from recipe in api.ModLoader.GetModSystem().SmithingRecipes - from ing in recipe.Ingredients - where ing.ResolvedItemStack?.Collectible?.Code?.Equals(collObj.Code) is true - select recipe; - return smithingRecipes; + var byIngredient = ObjectCacheUtil.GetOrCreate(api, $"{Core.ModId}:smithingRecipesByIngredient", () => + { + var dict = new Dictionary>(); + foreach (var recipe in api.ModLoader.GetModSystem().SmithingRecipes) + foreach (var ing in recipe.Ingredients) + { + var code = ing?.ResolvedItemStack?.Collectible?.Code; + if (code == null) continue; + if (!dict.TryGetValue(code, out var list)) dict[code] = list = []; + if (list.Count == 0 || list[^1] != recipe) list.Add(recipe); + } + + return dict; + }); + return byIngredient.TryGetValue(collObj.Code, out var recipes) ? recipes : []; } public static IEnumerable GetGridRecipesAsIngredient(this CollectibleObject collObj, ICoreAPI api) { - var gridRecipes = - from recipe in api.World.GridRecipes - from ing in recipe.RecipeIngredients - where ing is { ResolvedItemStack.Collectible: not null } && - ing.ResolvedItemStack?.Collectible?.Code?.Equals(collObj.Code) is true - select recipe; - return gridRecipes; + var byIngredient = ObjectCacheUtil.GetOrCreate(api, $"{Core.ModId}:gridRecipesByIngredient", () => + { + var dict = new Dictionary>(); + foreach (var recipe in api.World.GridRecipes) + foreach (var ing in recipe.RecipeIngredients) + { + var code = ing?.ResolvedItemStack?.Collectible?.Code; + if (code == null) continue; + if (!dict.TryGetValue(code, out var list)) dict[code] = list = []; + if (list.Count == 0 || list[^1] != recipe) list.Add(recipe); + } + + return dict; + }); + return byIngredient.TryGetValue(collObj.Code, out var recipes) ? recipes : []; } public static CollectibleObject? CollectibleWithVariant(this CollectibleObject collObj, string type, string value) From 304db7a675af4e9559147a393bccd5ed51086bd6 Mon Sep 17 00:00:00 2001 From: Maeyanie Date: Thu, 11 Jun 2026 03:59:19 -0400 Subject: [PATCH 2/2] Add comment Add comment suggested by coderabbitai. --- SmithingPlus/Util/CollectibleExtensions.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/SmithingPlus/Util/CollectibleExtensions.cs b/SmithingPlus/Util/CollectibleExtensions.cs index 39b69d5..ff22a01 100644 --- a/SmithingPlus/Util/CollectibleExtensions.cs +++ b/SmithingPlus/Util/CollectibleExtensions.cs @@ -99,6 +99,7 @@ public static IEnumerable GetSmithingRecipesAsIngredient(this Co var code = ing?.ResolvedItemStack?.Collectible?.Code; if (code == null) continue; if (!dict.TryGetValue(code, out var list)) dict[code] = list = []; + // Prevent duplicate entries when a recipe has the same ingredient multiple times if (list.Count == 0 || list[^1] != recipe) list.Add(recipe); } @@ -162,4 +163,4 @@ public static T GetBehavior(this CollectibleObject collObj, bool withInherita { return behavior?.GetField("metalProps"); } -} \ No newline at end of file +}