From e4dc2bdd7d421b26b9752b4d2afea8533093a0f7 Mon Sep 17 00:00:00 2001 From: skorpnok <6632336+skorpnok@users.noreply.github.com> Date: Tue, 2 Jun 2026 20:07:54 +0200 Subject: [PATCH 01/13] Split ClientConfig off of ServerConfig Synchronize runtime ServerConfig with Server when joining fix some behaviours getting added twice --- .../CastingTweaks/ToolMoldUnitsPatch.cs | 16 ----- .../ClientTweaks/AnvilRecipeSelectorPatch.cs | 2 +- SmithingPlus/Config/ClientConfig.cs | 13 ++++ SmithingPlus/Config/ConfigLoader.cs | 36 +++++++++-- SmithingPlus/Config/ServerConfig.cs | 10 +-- SmithingPlus/Core.cs | 61 +++++++++++++------ .../CollectibleBehaviorBrokenToolHead.cs | 2 +- .../CollectibleBehaviorRepairableTool.cs | 4 +- SmithingPlus/Util/TreeAttributeSerialize.cs | 48 +++++++++++++++ 9 files changed, 141 insertions(+), 51 deletions(-) create mode 100644 SmithingPlus/Config/ClientConfig.cs create mode 100644 SmithingPlus/Util/TreeAttributeSerialize.cs diff --git a/SmithingPlus/CastingTweaks/ToolMoldUnitsPatch.cs b/SmithingPlus/CastingTweaks/ToolMoldUnitsPatch.cs index 6d3c6ef..84ff379 100644 --- a/SmithingPlus/CastingTweaks/ToolMoldUnitsPatch.cs +++ b/SmithingPlus/CastingTweaks/ToolMoldUnitsPatch.cs @@ -37,22 +37,6 @@ public static void Initialize_Postfix(BlockEntityToolMold __instance, ref int __ ___requiredUnits = requiredUnitsRounded; } - [HarmonyPostfix] - [HarmonyPatch(nameof(BlockEntityToolMold.ToTreeAttributes))] - public static void ToTreeAttributes_Postfix(BlockEntityToolMold __instance, int ___requiredUnits, - ITreeAttribute tree) - { - tree.SetInt("requiredUnits", ___requiredUnits); - } - - [HarmonyPostfix] - [HarmonyPatch(nameof(BlockEntityToolMold.FromTreeAttributes))] - public static void FromTreeAttributes_Postfix(ITreeAttribute tree, ref int ___requiredUnits, - IWorldAccessor worldForResolve) - { - ___requiredUnits = tree.GetInt("requiredUnits"); - } - public static int GetPatchedRequiredUnits(ICoreAPI api, Block toolMold, ItemStack fromMetal) { var dropStacks = GetMoldedStacksStatic(api, toolMold, fromMetal); diff --git a/SmithingPlus/ClientTweaks/AnvilRecipeSelectorPatch.cs b/SmithingPlus/ClientTweaks/AnvilRecipeSelectorPatch.cs index 19e68fb..4b67051 100644 --- a/SmithingPlus/ClientTweaks/AnvilRecipeSelectorPatch.cs +++ b/SmithingPlus/ClientTweaks/AnvilRecipeSelectorPatch.cs @@ -83,7 +83,7 @@ int ___prevSlotOver { _defaultSkillItemCache = ___skillItems; var cellCount = Math.Max(1, ___skillItems.Count); - var columns = Math.Min(cellCount, Core.Config?.AnvilRecipeSelectionColumns ?? 8); + var columns = Math.Min(cellCount, Core.CConfig?.AnvilRecipeSelectionColumns ?? 8); var rows = (int)Math.Ceiling(cellCount / (double)columns); var slotSize = GuiElementPassiveItemSlot.unscaledSlotSize + GuiElementItemSlotGridBase.unscaledSlotPadding; var fixedWidth = Math.Max(300.0, columns * slotSize); diff --git a/SmithingPlus/Config/ClientConfig.cs b/SmithingPlus/Config/ClientConfig.cs new file mode 100644 index 0000000..717310f --- /dev/null +++ b/SmithingPlus/Config/ClientConfig.cs @@ -0,0 +1,13 @@ +namespace SmithingPlus.Config; + +public class ClientConfig +{ + public bool ShowRepairedCount { get; set; } = true; + public bool ShowBrokenCount { get; set; } = true; + public bool ShowRepairSmithName { get; set; } = false; + public bool AnvilShowRecipeVoxels { get; set; } = true; + public bool RememberHammerToolMode { get; set; } = true; + public bool ShowWorkableTemperature { get; set; } = true; + public bool HandbookExtraInfo { get; set; } = true; + public int AnvilRecipeSelectionColumns { get; set; } = 8; +} diff --git a/SmithingPlus/Config/ConfigLoader.cs b/SmithingPlus/Config/ConfigLoader.cs index 58bf4b4..a5cd07f 100644 --- a/SmithingPlus/Config/ConfigLoader.cs +++ b/SmithingPlus/Config/ConfigLoader.cs @@ -7,8 +7,10 @@ namespace SmithingPlus.Config; [UsedImplicitly] public class ConfigLoader : ModSystem { - private const string ConfigName = "SmithingPlus.json"; + private const string ServerConfigName = "SmithingPlus.json"; + private const string ClientConfigName = "SmithingPlusClient.json"; public static ServerConfig Config { get; private set; } + public static ClientConfig CConfig { get; private set; } public override double ExecuteOrder() { @@ -19,14 +21,39 @@ public override void StartPre(ICoreAPI api) { try { - Config = api.LoadModConfig(ConfigName); + CConfig = api.LoadModConfig(ClientConfigName); + if (CConfig == null) + { + // Try to load settings from old mixed file + CConfig = api.LoadModConfig(ServerConfigName); + if (CConfig == null) + { + CConfig = new ClientConfig(); + Mod.Logger.VerboseDebug("Client Config file not found, creating a new one..."); + } else + { + Mod.Logger.VerboseDebug("Client Config file not found, creating from old combined config"); + } + } + + api.StoreModConfig(CConfig, ClientConfigName); + } + catch (Exception e) + { + Mod.Logger.Error("Failed to load client config, you probably made a typo: {0}", e); + CConfig = new ClientConfig(); + } + + try + { + Config = api.LoadModConfig(ServerConfigName); if (Config == null) { Config = new ServerConfig(); Mod.Logger.VerboseDebug("Config file not found, creating a new one..."); } - api.StoreModConfig(Config, ConfigName); + api.StoreModConfig(Config, ServerConfigName); } catch (Exception e) { @@ -55,6 +82,7 @@ public override void Start(ICoreAPI api) public override void Dispose() { Config = null; + CConfig = null; base.Dispose(); } -} \ No newline at end of file +} diff --git a/SmithingPlus/Config/ServerConfig.cs b/SmithingPlus/Config/ServerConfig.cs index 835b4ad..5bc0146 100644 --- a/SmithingPlus/Config/ServerConfig.cs +++ b/SmithingPlus/Config/ServerConfig.cs @@ -25,9 +25,6 @@ public class ServerConfig public string WorkItemSelector { get; set; } = "@(.*):workitem-(.*)"; public bool DontRepairBrokenToolHeads { get; set; } = false; public bool CanRepairForlornHopeEstoc { get; set; } = true; - public bool ShowRepairedCount { get; set; } = true; - public bool ShowBrokenCount { get; set; } = true; - public bool ShowRepairSmithName { get; set; } = false; public float HelveHammerSmithingQualityModifier { get; set; } = 1; public bool ArrowsDropBits { get; set; } = true; public string ArrowSelector { get; set; } = "@(.*):arrow-(.*)"; @@ -36,11 +33,6 @@ public class ServerConfig public bool DynamicMoldUnits { get; set; } = false; public bool HammerTweaks { get; set; } = true; public bool RotationRequiresTongs { get; set; } = false; - public bool AnvilShowRecipeVoxels { get; set; } = true; - public bool RememberHammerToolMode { get; set; } = true; - public bool ShowWorkableTemperature { get; set; } = true; - public bool HandbookExtraInfo { get; set; } = true; - public int AnvilRecipeSelectionColumns { get; set; } = 8; // public bool StoneSmithing { get; set; } = false; [JsonIgnore] @@ -50,4 +42,4 @@ public class ServerConfig .Append(ModStackAttributes.RepairedToolStack) .Append(ModStackAttributes.CastTool) .ToArray(); -} \ No newline at end of file +} diff --git a/SmithingPlus/Core.cs b/SmithingPlus/Core.cs index 0e4b3eb..8d74a7f 100644 --- a/SmithingPlus/Core.cs +++ b/SmithingPlus/Core.cs @@ -26,7 +26,10 @@ public partial class Core : ModSystem public static ILogger Logger { get; private set; } public static ICoreAPI Api { get; private set; } public static Harmony HarmonyInstance { get; private set; } - public static ServerConfig Config => ConfigLoader.Config; + public static ServerConfig LocalConfig => ConfigLoader.Config; + public static ClientConfig CConfig => ConfigLoader.CConfig; + public static ServerConfig Config { get; private set; } + public static bool OnlyEnableClientside { get; private set; } = false; public override void StartPre(ICoreAPI api) { @@ -55,6 +58,20 @@ public override void Start(ICoreAPI api) api.RegisterItemClass($"{ModId}:ItemStoneHammer", typeof(ItemStoneHammer)); api.RegisterBlockEntityClass($"{ModId}:StoneAnvil", typeof(BlockEntityStoneAnvil)); + if (api.Side.IsServer()) + { + Config = LocalConfig; + TreeAttributeSerializer.ToTreeAttributes(LocalConfig,api.World.Config.GetOrAddTreeAttribute("SmithingPlus")); + } + else + { + Config = new(); + var tree = api.World.Config.GetTreeAttribute("SmithingPlus"); + if(tree is not null) + { + TreeAttributeSerializer.FromTreeAttributes(Config,tree); + } + } Patch(); } @@ -66,7 +83,7 @@ public override void StartServerSide(ICoreServerAPI api) private static void AddEntityBehaviors(Entity entity) { - if (!Config.ArrowsDropBits || entity is not EntityProjectile projectile) return; + if (!LocalConfig.ArrowsDropBits || entity is not EntityProjectile projectile) return; if (!RecyclableArrowBehavior.IsRecyclableArrow(projectile)) return; Logger.VerboseDebug("Adding RecyclableArrowBehavior to {0}", entity.Code); entity.AddBehavior(new RecyclableArrowBehavior(entity)); @@ -75,16 +92,22 @@ private static void AddEntityBehaviors(Entity entity) public override void AssetsFinalize(ICoreAPI api) { base.AssetsFinalize(api); - foreach (var collObj in api.World.Collectibles.Where(c => c?.Code != null)) + foreach (CollectibleObject collObj in api.World.Collectibles.Where(c => c?.Code != null)) { - collObj.AddBehaviorIf( - api.Side == EnumAppSide.Client && - Config.ShowWorkableTemperature && - collObj.GetCollectibleInterface() is not null); - collObj.AddBehaviorIf( - api.Side == EnumAppSide.Client && - Config.ShowWorkableTemperature && - collObj.HasBehavior()); + if (api.Side.IsClient()) + { + collObj.AddBehaviorIf( + CConfig.ShowWorkableTemperature && + collObj.GetCollectibleInterface() is not null); + + collObj.AddBehaviorIf( + CConfig.ShowWorkableTemperature && + collObj.HasBehavior()); + + continue; // These only apply to the client + } + // The rest apply to the server (which automatically adds them for the client as well) + collObj.AddBehaviorIf(Config.RecoverBitsOnSplit && collObj is ItemChisel); collObj.AddBehaviorIf(Config.RecoverBitsOnSplit && @@ -113,7 +136,7 @@ public override void AssetsFinalize(ICoreAPI api) // { "workableRecipe": true } // A better solution would be // to define the recipe with code instead of cloning an ingot recipe defined in the assets - if (api.Side.IsClient()) continue; + var ingotCode = new AssetLocation("game:ingot-copper"); var ingotRecipe = api.ModLoader.GetModSystem().SmithingRecipes .FirstOrDefault(r => @@ -154,23 +177,25 @@ public override void AssetsFinalize(ICoreAPI api) private static void Patch() { if (HarmonyInstance != null) return; + HarmonyInstance = new Harmony(ModId); Logger.VerboseDebug("Patching..."); AlwaysPatchCategory.PatchIfEnabled(true); ToolRecoveryCategory.PatchIfEnabled(Config.EnableToolRecovery); SmithingRecipeAttributesPatch.PatchIfEnabled( Config.SmithWithBits || Config.BitsTopUp || Config.EnableToolRecovery, HarmonyInstance); - ClientTweaksCategories.RememberHammerToolMode.PatchIfEnabled(Config.RememberHammerToolMode); - ClientTweaksCategories.AnvilShowRecipeVoxels.PatchIfEnabled(Config.AnvilShowRecipeVoxels); - ClientTweaksCategories.ShowWorkablePatches.PatchIfEnabled(Config.ShowWorkableTemperature); - ClientTweaksCategories.HandbookExtraInfo.PatchIfEnabled(Config.HandbookExtraInfo); + + ClientTweaksCategories.RememberHammerToolMode.PatchIfEnabled(CConfig.RememberHammerToolMode); + ClientTweaksCategories.AnvilShowRecipeVoxels.PatchIfEnabled(CConfig.AnvilShowRecipeVoxels); + ClientTweaksCategories.ShowWorkablePatches.PatchIfEnabled(CConfig.ShowWorkableTemperature); + ClientTweaksCategories.HandbookExtraInfo.PatchIfEnabled(CConfig.HandbookExtraInfo); + BitsRecoveryCategory.PatchIfEnabled(Config.RecoverBitsOnSplit); HelveHammerBitsRecoveryCategory.PatchIfEnabled(Config.HelveHammerBitsRecovery); CastingTweaksCategory.PatchIfEnabled(Config.MetalCastingTweaks); DynamicMoldsCategory.PatchIfEnabled(Config.DynamicMoldUnits); BitSmithingCategory.PatchIfEnabled(Config.SmithWithBits || Config.BitsTopUp); HammerTweaksCategory.PatchIfEnabled(Config.HammerTweaks); - //StoneSmithingCategory.PatchIfEnabled(true); } private static void Unpatch() @@ -187,4 +212,4 @@ public override void Dispose() Api = null; base.Dispose(); } -} \ No newline at end of file +} diff --git a/SmithingPlus/ToolRecovery/CollectibleBehaviorBrokenToolHead.cs b/SmithingPlus/ToolRecovery/CollectibleBehaviorBrokenToolHead.cs index 433eadf..639f53a 100644 --- a/SmithingPlus/ToolRecovery/CollectibleBehaviorBrokenToolHead.cs +++ b/SmithingPlus/ToolRecovery/CollectibleBehaviorBrokenToolHead.cs @@ -48,7 +48,7 @@ public override void GetHeldItemInfo(ItemSlot inSlot, StringBuilder dsc, IWorldA if (world.Api is not ICoreClientAPI) return; var brokenCount = inSlot.Itemstack.GetBrokenCount(); if (brokenCount <= 0) return; - if (Core.Config.ShowBrokenCount) dsc.AppendLine(Lang.Get($"{LangKey} {{0}} times", brokenCount)); + if (Core.CConfig.ShowBrokenCount) dsc.AppendLine(Lang.Get($"{LangKey} {{0}} times", brokenCount)); if (Core.Config.DontRepairBrokenToolHeads) dsc.AppendLine(Lang.Get($"{Core.ModId}:itemdesc-needschiseling")); } diff --git a/SmithingPlus/ToolRecovery/CollectibleBehaviorRepairableTool.cs b/SmithingPlus/ToolRecovery/CollectibleBehaviorRepairableTool.cs index 6bb4ee7..bfdc2e9 100644 --- a/SmithingPlus/ToolRecovery/CollectibleBehaviorRepairableTool.cs +++ b/SmithingPlus/ToolRecovery/CollectibleBehaviorRepairableTool.cs @@ -34,8 +34,8 @@ public override void GetHeldItemInfo(ItemSlot? inSlot, StringBuilder dsc, IWorld return; var brokenCount = inSlot.Itemstack.GetBrokenCount(); if (brokenCount <= 0) return; - if (Core.Config.ShowRepairedCount) dsc.AppendLine(Lang.Get($"{LangKey} {{0}} times", brokenCount)); - if (Core.Config.ShowRepairSmithName && inSlot.Itemstack.GetRepairSmith() is { } repairSmith) + if (Core.CConfig.ShowRepairedCount) dsc.AppendLine(Lang.Get($"{LangKey} {{0}} times", brokenCount)); + if (Core.CConfig.ShowRepairSmithName && inSlot.Itemstack.GetRepairSmith() is { } repairSmith) dsc.AppendLine(Lang.Get("Last repaired by {0}", repairSmith)); } } \ No newline at end of file diff --git a/SmithingPlus/Util/TreeAttributeSerialize.cs b/SmithingPlus/Util/TreeAttributeSerialize.cs new file mode 100644 index 0000000..ea5d266 --- /dev/null +++ b/SmithingPlus/Util/TreeAttributeSerialize.cs @@ -0,0 +1,48 @@ + + +using System.Reflection; +using Vintagestory.API.Datastructures; + +namespace SmithingPlus.Util; + +public class TreeAttributeSerializer +{ + public static void ToTreeAttributes(T obj, ITreeAttribute tree) + { + foreach (PropertyInfo fi in typeof(T).GetProperties()) + { + var type = fi.PropertyType; + var name = fi.Name; + if (type == typeof(bool)) { + tree.SetBool(name,(bool)fi.GetValue(obj)); + } else if (type==typeof(int)) { + tree.SetInt(name,(int)fi.GetValue(obj)); + } else if (type==typeof(float)) { + tree.SetFloat(name,(float)fi.GetValue(obj)); + } else if (type==typeof(string)) { + tree.SetString(name,(string)fi.GetValue(obj)); + } + } + } + + public static void FromTreeAttributes(T obj, ITreeAttribute tree) + { + foreach (PropertyInfo fi in typeof(T).GetProperties()) + { + var type = fi.PropertyType; + var name = fi.Name; + if (type == typeof(bool)) { + bool? v = tree.TryGetBool(name); + if (v.HasValue) fi.SetValue(obj,v.Value); + } else if (type==typeof(int)) { + int? v = tree.TryGetInt(name); + if (v.HasValue) fi.SetValue(obj,v.Value); + } else if (type==typeof(float)) { + float? v = tree.TryGetFloat(name); + if (v.HasValue) fi.SetValue(obj,v.Value); + } else if (type==typeof(string)) { + if(tree.HasAttribute(name)) fi.SetValue(obj,tree.GetString(name)); + } + } + } +} From c454eae2dca1069d36d40ba3d0c13a898004b8ea Mon Sep 17 00:00:00 2001 From: Maeyanie Date: Thu, 11 Jun 2026 03:40:35 -0400 Subject: [PATCH 02/13] 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 03/13] 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 +} From 15260877e84d476e857733972cb0732b63412d7b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C6=9B=CA=91=CA=8B=C9=8D=C9=9B=CF=AF=E1=BE=B0=C9=A8?= <6170786+AzureTai@users.noreply.github.com> Date: Tue, 18 Aug 2026 21:23:28 +0100 Subject: [PATCH 04/13] Document upstream PR integration decisions Record the disposition of every upstream SmithingPlus pull request reviewed for the performance fork, including superseded work, baseline inclusions, staged conflicts, integrated commits and the pre-rename validation gate. --- UPSTREAM_PR_AUDIT.md | 42 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 42 insertions(+) create mode 100644 UPSTREAM_PR_AUDIT.md diff --git a/UPSTREAM_PR_AUDIT.md b/UPSTREAM_PR_AUDIT.md new file mode 100644 index 0000000..dffa33c --- /dev/null +++ b/UPSTREAM_PR_AUDIT.md @@ -0,0 +1,42 @@ +# Upstream Pull Request Integration Audit + +Upstream repository: https://github.com/jayugg/SmithingPlus + +Fork integration branch: `agent/process-upstream-prs` + +Baseline: upstream `master` commit `241b9f294eb3cde29a9672b5ca2a52cc4d9ff5fd`. + +This audit records the disposition of every upstream pull request visible when the fork was established. A pull request marked as superseded or selectively extractable must not be applied wholesale without a new review. + +| PR | Upstream state | Fork disposition | Reason | +|---|---|---|---| +| [#5](https://github.com/jayugg/SmithingPlus/pull/5) | Closed, unmerged | Superseded | Historical tool-recovery correction duplicated by #6 and overtaken by later recipe/API changes. | +| [#6](https://github.com/jayugg/SmithingPlus/pull/6) | Closed, unmerged | Superseded | Same essential correction as #5; current recovery code no longer matches this patch. | +| [#36](https://github.com/jayugg/SmithingPlus/pull/36) | Closed, unmerged | Superseded; regression test required | Old chisel null guard. Later chisel correction #110 is already in the baseline. | +| [#60](https://github.com/jayugg/SmithingPlus/pull/60) | Closed, unmerged | Superseded; regression test required | Based on 1.5.7 handbook/material code that was later substantially reworked. | +| [#62](https://github.com/jayugg/SmithingPlus/pull/62) | Merged | Present in baseline | Bit-recovery method extraction for external compatibility. | +| [#76](https://github.com/jayugg/SmithingPlus/pull/76) | Closed, unmerged | Superseded | Partial 1.21 release-candidate port superseded by #77 and later releases. | +| [#77](https://github.com/jayugg/SmithingPlus/pull/77) | Merged | Present in baseline | Main 1.21 release-candidate upgrade. | +| [#78](https://github.com/jayugg/SmithingPlus/pull/78) | Merged | Historical only | Final 1.20.12 maintenance work; the fork targets 1.22.5 and later. | +| [#79](https://github.com/jayugg/SmithingPlus/pull/79) | Merged | Present in history | 1.21 release transition; GitHub reports no remaining changed files against its target. | +| [#85](https://github.com/jayugg/SmithingPlus/pull/85) | Closed, unmerged | Superseded | Hard-coded mold-unit feature with a known handbook issue; superseded by #101's dynamic system. | +| [#101](https://github.com/jayugg/SmithingPlus/pull/101) | Merged | Present in baseline | Major 1.8 architecture and dynamic mold-unit rework. | +| [#110](https://github.com/jayugg/SmithingPlus/pull/110) | Merged | Present in baseline | Chisel spam-click null correction. | +| [#116](https://github.com/jayugg/SmithingPlus/pull/116) | Closed, unmerged | Superseded by #130 | Earlier partial configuration synchronisation change. | +| [#118](https://github.com/jayugg/SmithingPlus/pull/118) | Merged | Present in baseline | Belarusian translation. | +| [#121](https://github.com/jayugg/SmithingPlus/pull/121) | Closed, unmerged | Superseded by #125 | Community 1.22 release-candidate port. | +| [#125](https://github.com/jayugg/SmithingPlus/pull/125) | Merged | Present in baseline | Official 1.22 update and fork baseline. | +| [#130](https://github.com/jayugg/SmithingPlus/pull/130) | Open upstream | Integrated through fork PR #1 | Client/server configuration split and synchronisation. Merge commit `704c5f5217bd7e927dff1859b6fd87fc4da5dc28`. | +| [#131](https://github.com/jayugg/SmithingPlus/pull/131) | Open upstream | Staged as fork PR #2; selective extraction only | Conflicts with #130 and mixes caching with unrelated removals, assets, compatibility and version changes. The upstream description acknowledges a smithing-with-bits regression during porting. | +| [#132](https://github.com/jayugg/SmithingPlus/pull/132) | Open upstream | Integrated through fork PR #3 | Profiled reverse-index and material-cache correction. Merge commit `567cc617b654d91a03e8925dbc422bea2ec14faa`. | +| [#139](https://github.com/jayugg/SmithingPlus/pull/139) | Open upstream | Staged as fork PR #4; corrected manual port required | Conflicts with the integrated branch. Exact 1.22.5 destruction/cancellation semantics must be verified before recovery is moved. | + +## Validation gate + +No mod-domain or mod-identifier rename is permitted until: + +1. all accepted changes pass the project coding-standard audit; +2. the project compiles against the exact Vintage Story 1.22.5 assemblies and .NET version; +3. cold and warm anvil interaction performance is measured; +4. Smith With Bits, cast heads, workable nuggets, handbook rendering, configuration synchronisation, chisel reclaim and broken-tool recovery receive runtime regression tests; +5. the separate SmithingPlusBugFix hotfix is removed during standalone-fork testing. From 3f9b81dbf59a98fecafdd351cce7d6236ef28a91 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C6=9B=CA=91=CA=8B=C9=8D=C9=9B=CF=AF=E1=BE=B0=C9=A8?= <6170786+AzureTai@users.noreply.github.com> Date: Wed, 19 Aug 2026 04:29:18 +0100 Subject: [PATCH 05/13] Resolve nullable recipe and item handling warnings Add explicit validation for unresolved recipe outputs, empty item slots, optional collectible attributes, voxel-pattern rows and JSON-populated material overrides. Replace warning-prone recipe ordering pipelines with guarded iteration while preserving the existing selection semantics. --- .../CollectibleBehaviorCastToolHead.cs | 246 ++++---- .../CastingTweaks/ToolMoldUnitsPatch.cs | 236 ++++---- .../CollectibleBehaviorAnvilWorkable.cs | 419 ++++++++------ .../CollectibleBehaviorJsonAnvilWorkable.cs | 291 +++++----- SmithingPlus/Common/Metal/MetalMaterial.cs | 168 +++--- .../CollectibleBehaviorRepairableTool.cs | 66 +-- SmithingPlus/Util/CollectibleExtensions.cs | 328 +++++------ SmithingPlus/Util/ItemStackExtensions.cs | 544 ++++++++++-------- 8 files changed, 1219 insertions(+), 1079 deletions(-) diff --git a/SmithingPlus/CastingTweaks/CollectibleBehaviorCastToolHead.cs b/SmithingPlus/CastingTweaks/CollectibleBehaviorCastToolHead.cs index 688c2fe..c21edfe 100644 --- a/SmithingPlus/CastingTweaks/CollectibleBehaviorCastToolHead.cs +++ b/SmithingPlus/CastingTweaks/CollectibleBehaviorCastToolHead.cs @@ -1,128 +1,132 @@ -#nullable enable -using System; -using System.Collections.Generic; -using System.Diagnostics; -using System.Text; -using SmithingPlus.Common.Metal; -using SmithingPlus.Metal; -using SmithingPlus.Util; -using Vintagestory.API.Common; -using Vintagestory.API.Config; -using Vintagestory.GameContent; - -namespace SmithingPlus.CastingTweaks; - -public class CollectibleBehaviorCastToolHead(CollectibleObject collObj) : CollectibleBehavior(collObj), IAnvilWorkable -{ - private ICoreAPI Api => collObj.GetField("api"); - - public int GetRequiredAnvilTier(ItemStack stack) - { - return stack.GetOrCacheMetalMaterial(Api)?.Tier ?? 0; - } - - public List GetMatchingRecipes(ItemStack stack) - { - var smithingRecipe = stack.GetSmithingRecipe(Api); - return smithingRecipe != null ? [smithingRecipe] : []; - } - - public bool CanWork(ItemStack stack) - { - if (!stack.IsCastTool()) - return false; - var temperature = stack.Collectible.GetTemperature(Api.World, stack); - var threshold = GetWorkableTemperature(stack); - Core.Logger.VerboseDebug( - $"[CollectibleBehaviorCastToolHead#CanWork] {stack.Collectible.Code} - Temperature: {temperature}, Threshold: {threshold}"); - return temperature >= threshold; - } - - public ItemStack? TryPlaceOn(ItemStack stack, BlockEntityAnvil beAnvil) - { - if (beAnvil.WorkItemStack != null || !CanWork(stack)) - return null; - var recipe = stack.GetSingleSmithingRecipe(Api); - var durabilityPercent = stack.GetDurabilityPercentage(); - if (recipe == null || durabilityPercent == null) return null; - var voxels = recipe.Voxels.ErodeToPercentage(durabilityPercent.Value); - var world = beAnvil.Api.World; - var random = world.Rand; - var slagCount = (int)Math.Ceiling(0.2f * voxels.MaterialCount()); - voxels.AddSlag(slagCount, random); - var workItemStack = stack.GetOrCacheMetalMaterial(beAnvil.Api)?.WorkItemStack; - if (workItemStack == null) - return null; - beAnvil.Voxels = voxels; - beAnvil.SelectedRecipeId = recipe.RecipeId; - var temperature = stack.Collectible.GetTemperature(world, stack); - workItemStack.Collectible.SetTemperature(world, workItemStack, temperature); - return workItemStack; - } - - public ItemStack? GetBaseMaterial(ItemStack stack) - { - var metalMaterial = stack.GetOrCacheMetalMaterial(Api); - Debug.Write( - $"[CollectibleBehaviorCastToolHead#GetBaseMaterial] {stack.Collectible.Code} -> {metalMaterial?.IngotCode}"); - return metalMaterial?.IngotStack; - } - - public EnumHelveWorkableMode GetHelveWorkableMode(ItemStack stack, BlockEntityAnvil beAnvil) - { - return EnumHelveWorkableMode.TestSufficientVoxelsWorkable; - } - - public int VoxelCountForHandbook(ItemStack stack) - { - var recipe = stack.GetSingleSmithingRecipe(Api); - var voxels = recipe?.Voxels.ToByteArray(); - return voxels?.MaterialCount() ?? 0; - } - - public override void GetHeldItemName(StringBuilder dsc, ItemStack itemStack) - { - base.GetHeldItemName(dsc, itemStack); - if (!itemStack.IsCastTool()) - return; - var toolName = dsc.ToString(); - dsc.Clear(); - dsc.AppendLine(Lang.Get("Cast {0}", toolName.ToLower())); - } - +#nullable enable +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Text; +using SmithingPlus.Common.Metal; +using SmithingPlus.Metal; +using SmithingPlus.Util; +using Vintagestory.API.Common; +using Vintagestory.API.Config; +using Vintagestory.GameContent; + +namespace SmithingPlus.CastingTweaks; + +public class CollectibleBehaviorCastToolHead(CollectibleObject collObj) : CollectibleBehavior(collObj), IAnvilWorkable +{ + private ICoreAPI Api => collObj.GetField("api"); + + public int GetRequiredAnvilTier(ItemStack stack) + { + return stack.GetOrCacheMetalMaterial(Api)?.Tier ?? 0; + } + + public List GetMatchingRecipes(ItemStack stack) + { + var smithingRecipe = stack.GetSmithingRecipe(Api); + return smithingRecipe != null ? [smithingRecipe] : []; + } + + public bool CanWork(ItemStack stack) + { + if (!stack.IsCastTool()) + return false; + var temperature = stack.Collectible.GetTemperature(Api.World, stack); + var threshold = GetWorkableTemperature(stack); + Core.Logger.VerboseDebug( + $"[CollectibleBehaviorCastToolHead#CanWork] {stack.Collectible.Code} - Temperature: {temperature}, Threshold: {threshold}"); + return temperature >= threshold; + } + + public ItemStack? TryPlaceOn(ItemStack stack, BlockEntityAnvil beAnvil) + { + if (beAnvil.WorkItemStack != null || !CanWork(stack)) + return null; + var recipe = stack.GetSingleSmithingRecipe(Api); + var durabilityPercent = stack.GetDurabilityPercentage(); + if (recipe == null || durabilityPercent == null) return null; + var voxels = recipe.Voxels.ErodeToPercentage(durabilityPercent.Value); + var world = beAnvil.Api.World; + var random = world.Rand; + var slagCount = (int)Math.Ceiling(0.2f * voxels.MaterialCount()); + voxels.AddSlag(slagCount, random); + var workItemStack = stack.GetOrCacheMetalMaterial(beAnvil.Api)?.WorkItemStack; + if (workItemStack == null) + return null; + beAnvil.Voxels = voxels; + beAnvil.SelectedRecipeId = recipe.RecipeId; + var temperature = stack.Collectible.GetTemperature(world, stack); + workItemStack.Collectible.SetTemperature(world, workItemStack, temperature); + return workItemStack; + } + + public ItemStack? GetBaseMaterial(ItemStack stack) + { + var metalMaterial = stack.GetOrCacheMetalMaterial(Api); + Debug.Write( + $"[CollectibleBehaviorCastToolHead#GetBaseMaterial] {stack.Collectible.Code} -> {metalMaterial?.IngotCode}"); + return metalMaterial?.IngotStack; + } + + public EnumHelveWorkableMode GetHelveWorkableMode(ItemStack stack, BlockEntityAnvil beAnvil) + { + return EnumHelveWorkableMode.TestSufficientVoxelsWorkable; + } + + public int VoxelCountForHandbook(ItemStack stack) + { + var recipe = stack.GetSingleSmithingRecipe(Api); + var voxels = recipe?.Voxels.ToByteArray(); + return voxels?.MaterialCount() ?? 0; + } + + public override void GetHeldItemName(StringBuilder dsc, ItemStack itemStack) + { + base.GetHeldItemName(dsc, itemStack); + if (!itemStack.IsCastTool()) + return; + var toolName = dsc.ToString(); + dsc.Clear(); + dsc.AppendLine(Lang.Get("Cast {0}", toolName.ToLower())); + } + public override void GetHeldItemInfo(ItemSlot inSlot, StringBuilder dsc, IWorldAccessor world, bool withDebugInfo) { base.GetHeldItemInfo(inSlot, dsc, world, withDebugInfo); - if (!inSlot.Itemstack.IsCastTool()) + ItemStack? itemStack = inSlot.Itemstack; + if (itemStack == null || !itemStack.IsCastTool()) + { return; + } + dsc.AppendLine(Lang.Get($"{Core.ModId}:setting-casttooldurabilitypenalty") + $": {100 * Core.Config.CastToolDurabilityPenalty}%"); dsc.AppendLine(Lang.Get($"{Core.ModId}:itemdesc-needsrefining")); - var workableTemp = GetWorkableTemperature(inSlot.Itemstack); - var temperature = inSlot.Itemstack?.Collectible.GetTemperature(world, inSlot.Itemstack); - dsc.AppendLine(Lang.Get("Workable Temperature: {0}", - workableTemp > 0 - ? temperature > workableTemp - ? $"{Math.Round(workableTemp)}\u00B0C" - : $"{Math.Round(workableTemp)}\u00B0C" - : Lang.Get($"{Core.ModId}:itemdesc-temp-always"))); - } - - private float GetWorkableTemperature(ItemStack itemStack) - { - var metalIngot = itemStack.GetOrCacheMetalMaterial(Api)?.IngotItem; - var querySlot = new DummySlot(itemStack); - - var meltingPoint = metalIngot? - .GetMeltingPoint(Api.World, null, querySlot) - ?? itemStack.Collectible - .GetMeltingPoint(Api.World, null, querySlot); - - var defaultWorkableTemp = meltingPoint / 2f; - var workableAttr = metalIngot?.Attributes?["workableTemperature"]; - - return workableAttr?.Exists == true - ? workableAttr.AsFloat(defaultWorkableTemp) - : defaultWorkableTemp; - } -} \ No newline at end of file + var workableTemp = GetWorkableTemperature(itemStack); + var temperature = itemStack.Collectible.GetTemperature(world, itemStack); + dsc.AppendLine(Lang.Get("Workable Temperature: {0}", + workableTemp > 0 + ? temperature > workableTemp + ? $"{Math.Round(workableTemp)}\u00B0C" + : $"{Math.Round(workableTemp)}\u00B0C" + : Lang.Get($"{Core.ModId}:itemdesc-temp-always"))); + } + + private float GetWorkableTemperature(ItemStack itemStack) + { + var metalIngot = itemStack.GetOrCacheMetalMaterial(Api)?.IngotItem; + var querySlot = new DummySlot(itemStack); + + var meltingPoint = metalIngot? + .GetMeltingPoint(Api.World, null, querySlot) + ?? itemStack.Collectible + .GetMeltingPoint(Api.World, null, querySlot); + + var defaultWorkableTemp = meltingPoint / 2f; + var workableAttr = metalIngot?.Attributes?["workableTemperature"]; + + return workableAttr?.Exists == true + ? workableAttr.AsFloat(defaultWorkableTemp) + : defaultWorkableTemp; + } +} diff --git a/SmithingPlus/CastingTweaks/ToolMoldUnitsPatch.cs b/SmithingPlus/CastingTweaks/ToolMoldUnitsPatch.cs index 84ff379..7616b46 100644 --- a/SmithingPlus/CastingTweaks/ToolMoldUnitsPatch.cs +++ b/SmithingPlus/CastingTweaks/ToolMoldUnitsPatch.cs @@ -1,121 +1,131 @@ -#nullable enable -using System; -using System.Collections.Generic; -using System.Linq; -using HarmonyLib; -using JetBrains.Annotations; -using Newtonsoft.Json; -using SmithingPlus.Util; -using Vintagestory.API.Common; -using Vintagestory.API.Datastructures; -using Vintagestory.GameContent; - -namespace SmithingPlus.CastingTweaks; - -[HarmonyPatchCategory(Core.DynamicMoldsCategory)] -[UsedImplicitly(ImplicitUseTargetFlags.WithMembers)] -[HarmonyPatch(typeof(BlockEntityToolMold))] -[HarmonyPriority(Priority.Last)] -public class ToolMoldUnitsPatch -{ - [HarmonyPostfix] - [HarmonyPatch(nameof(BlockEntityToolMold.Initialize))] - public static void Initialize_Postfix(BlockEntityToolMold __instance, ref int ___requiredUnits, ICoreAPI api) - { - // Assume copper stack as metal for unit calculation, - // This means the patch will only apply for standard molds! - // Either vanilla or vanilla-like ones - // It also means that having different smithing recipes - // for different metals will cause unexpected imbalances - var copperIngot = api.World.GetItem(new AssetLocation("game:ingot-copper")); - if (copperIngot == null) - return; - var copperStack = new ItemStack(copperIngot); - var requiredUnitsRounded = GetPatchedRequiredUnits(__instance.Api, __instance.Block, copperStack); - if (requiredUnitsRounded == 0) - return; - ___requiredUnits = requiredUnitsRounded; - } - - public static int GetPatchedRequiredUnits(ICoreAPI api, Block toolMold, ItemStack fromMetal) - { - var dropStacks = GetMoldedStacksStatic(api, toolMold, fromMetal); - if (dropStacks.Length == 0) - return - toolMold.Attributes["requiredUnits"] - .AsInt(); // <-- Patch will only apply for molds that work for copper! - var voxelCount = VoxelCountForStacks(api, dropStacks); - if (voxelCount is null or 0) - return toolMold.Attributes["requiredUnits"].AsInt(); - // These are all assumptions that have to be made, should implement warnings if weird values are found - const float voxelsPerIngot = 42f; - const float unitsPerIngot = 100f; - const float unitsPerVoxel = unitsPerIngot / voxelsPerIngot; - // Round to lowest 5 units to avoid annoying numbers and making players sad - return (int)MathF.Floor(voxelCount.Value * unitsPerVoxel / 5) * 5; - } - - public static int? VoxelCountForStacks(ICoreAPI api, ItemStack[] smithedItemStacks) - { - var voxelCounts = smithedItemStacks.Select(stack => - VoxelCountForStack(api, stack)).ToArray(); - return voxelCounts.All(count => count == null) ? null : voxelCounts.Sum(count => count ?? 0); - } - +#nullable enable +using System; +using System.Collections.Generic; +using System.Linq; +using HarmonyLib; +using JetBrains.Annotations; +using Newtonsoft.Json; +using SmithingPlus.Util; +using Vintagestory.API.Common; +using Vintagestory.API.Datastructures; +using Vintagestory.GameContent; + +namespace SmithingPlus.CastingTweaks; + +[HarmonyPatchCategory(Core.DynamicMoldsCategory)] +[UsedImplicitly(ImplicitUseTargetFlags.WithMembers)] +[HarmonyPatch(typeof(BlockEntityToolMold))] +[HarmonyPriority(Priority.Last)] +public class ToolMoldUnitsPatch +{ + [HarmonyPostfix] + [HarmonyPatch(nameof(BlockEntityToolMold.Initialize))] + public static void Initialize_Postfix(BlockEntityToolMold __instance, ref int ___requiredUnits, ICoreAPI api) + { + // Assume copper stack as metal for unit calculation, + // This means the patch will only apply for standard molds! + // Either vanilla or vanilla-like ones + // It also means that having different smithing recipes + // for different metals will cause unexpected imbalances + var copperIngot = api.World.GetItem(new AssetLocation("game:ingot-copper")); + if (copperIngot == null) + return; + var copperStack = new ItemStack(copperIngot); + var requiredUnitsRounded = GetPatchedRequiredUnits(__instance.Api, __instance.Block, copperStack); + if (requiredUnitsRounded == 0) + return; + ___requiredUnits = requiredUnitsRounded; + } + + public static int GetPatchedRequiredUnits(ICoreAPI api, Block toolMold, ItemStack fromMetal) + { + var dropStacks = GetMoldedStacksStatic(api, toolMold, fromMetal); + if (dropStacks.Length == 0) + return + toolMold.Attributes["requiredUnits"] + .AsInt(); // <-- Patch will only apply for molds that work for copper! + var voxelCount = VoxelCountForStacks(api, dropStacks); + if (voxelCount is null or 0) + return toolMold.Attributes["requiredUnits"].AsInt(); + // These are all assumptions that have to be made, should implement warnings if weird values are found + const float voxelsPerIngot = 42f; + const float unitsPerIngot = 100f; + const float unitsPerVoxel = unitsPerIngot / voxelsPerIngot; + // Round to lowest 5 units to avoid annoying numbers and making players sad + return (int)MathF.Floor(voxelCount.Value * unitsPerVoxel / 5) * 5; + } + + public static int? VoxelCountForStacks(ICoreAPI api, ItemStack[] smithedItemStacks) + { + var voxelCounts = smithedItemStacks.Select(stack => + VoxelCountForStack(api, stack)).ToArray(); + return voxelCounts.All(count => count == null) ? null : voxelCounts.Sum(count => count ?? 0); + } + private static int? VoxelCountForStack(ICoreAPI api, ItemStack stack) { - var cheapestRecipe = stack.GetCheapestSmithingRecipe(api); - if (cheapestRecipe == null) return null; - var cheapestOutput = cheapestRecipe.Output.ResolvedItemstack.StackSize; - var recipeMaterialVoxels = cheapestRecipe.Voxels.VoxelCount(); - var voxelsPerItem = Math.Max(recipeMaterialVoxels / cheapestOutput, 0); - return voxelsPerItem * stack.StackSize; - } - - private static ItemStack[] GetMoldedStacksStatic(ICoreAPI api, Block toolMold, ItemStack fromMetal) - { - // why try catch? vanilla code does this... - try + SmithingRecipe? cheapestRecipe = stack.GetCheapestSmithingRecipe(api); + if (cheapestRecipe == null) { - if (toolMold.Attributes["drop"].Exists) - { - var jStack = -#pragma warning disable CS8625 // Cannot convert null literal to non-nullable reference type. - toolMold.Attributes["drop"].AsObject(null, toolMold.Code.Domain); -#pragma warning restore CS8625 // Cannot convert null literal to non-nullable reference type. - if (jStack == null) - return []; - var itemStack = MoldOutputStackFromCode(jStack, api, toolMold, fromMetal); - return itemStack == null ? [] : [itemStack]; - } - - var jsonItemStackArray = - toolMold.Attributes["drops"].AsObject([], toolMold.Code.Domain); - var itemStackList = new List(); - foreach (var jStack in jsonItemStackArray) - { - var itemStack = MoldOutputStackFromCode(jStack, api, toolMold, fromMetal); - if (itemStack != null) - itemStackList.Add(itemStack); - } - - return itemStackList.ToArray(); + return null; } - catch (JsonReaderException ex) + + JsonItemStack? recipeOutput = cheapestRecipe.Output; + ItemStack? resolvedOutputStack = recipeOutput?.ResolvedItemstack; + if (resolvedOutputStack == null || resolvedOutputStack.StackSize <= 0) { - api.World.Logger.Error("Failed getting molded stacks from tool mold of block {0}, " + - "probably unable to parse drop or drops attribute", toolMold.Code); - api.World.Logger.Error(ex); - throw; + return null; } - } - private static ItemStack? MoldOutputStackFromCode(JsonItemStack jstack, ICoreAPI api, Block toolMold, - ItemStack fromMetal) - { - var newValue = fromMetal.Collectible.LastCodePart(); - jstack.Code.Path = jstack.Code.Path.Replace("{metal}", newValue); - jstack.Resolve(api.World, "tool mold drop for " + toolMold.Code); - return jstack.ResolvedItemstack; + int recipeMaterialVoxels = cheapestRecipe.Voxels.VoxelCount(); + int voxelsPerItem = Math.Max(recipeMaterialVoxels / resolvedOutputStack.StackSize, 0); + return voxelsPerItem * stack.StackSize; } -} \ No newline at end of file + + private static ItemStack[] GetMoldedStacksStatic(ICoreAPI api, Block toolMold, ItemStack fromMetal) + { + // why try catch? vanilla code does this... + try + { + if (toolMold.Attributes["drop"].Exists) + { + var jStack = +#pragma warning disable CS8625 // Cannot convert null literal to non-nullable reference type. + toolMold.Attributes["drop"].AsObject(null, toolMold.Code.Domain); +#pragma warning restore CS8625 // Cannot convert null literal to non-nullable reference type. + if (jStack == null) + return []; + var itemStack = MoldOutputStackFromCode(jStack, api, toolMold, fromMetal); + return itemStack == null ? [] : [itemStack]; + } + + var jsonItemStackArray = + toolMold.Attributes["drops"].AsObject([], toolMold.Code.Domain); + var itemStackList = new List(); + foreach (var jStack in jsonItemStackArray) + { + var itemStack = MoldOutputStackFromCode(jStack, api, toolMold, fromMetal); + if (itemStack != null) + itemStackList.Add(itemStack); + } + + return itemStackList.ToArray(); + } + catch (JsonReaderException ex) + { + api.World.Logger.Error("Failed getting molded stacks from tool mold of block {0}, " + + "probably unable to parse drop or drops attribute", toolMold.Code); + api.World.Logger.Error(ex); + throw; + } + } + + private static ItemStack? MoldOutputStackFromCode(JsonItemStack jstack, ICoreAPI api, Block toolMold, + ItemStack fromMetal) + { + var newValue = fromMetal.Collectible.LastCodePart(); + jstack.Code.Path = jstack.Code.Path.Replace("{metal}", newValue); + jstack.Resolve(api.World, "tool mold drop for " + toolMold.Code); + return jstack.ResolvedItemstack; + } +} diff --git a/SmithingPlus/Common/CollectibleBehaviorAnvilWorkable.cs b/SmithingPlus/Common/CollectibleBehaviorAnvilWorkable.cs index 3a659ba..396c027 100644 --- a/SmithingPlus/Common/CollectibleBehaviorAnvilWorkable.cs +++ b/SmithingPlus/Common/CollectibleBehaviorAnvilWorkable.cs @@ -1,204 +1,255 @@ -#nullable enable -using System; -using System.Collections.Generic; -using System.Linq; -using SmithingPlus.Common.Metal; -using SmithingPlus.Metal; -using SmithingPlus.Util; -using Vintagestory.API.Client; -using Vintagestory.API.Common; -using Vintagestory.API.Config; -using Vintagestory.GameContent; - -namespace SmithingPlus.Common; - -public abstract class CollectibleBehaviorAnvilWorkable(CollectibleObject collObj) : - CollectibleBehavior(collObj), IAnvilWorkable -{ - protected ICoreAPI? Api => collObj.GetField("api"); - protected abstract byte[,,] Voxels { get; } - - protected MetalMaterial? MetalMaterial => - Api != null - ? MetalMaterialLoader.GetMaterial(Api, collObj.GetMetalVariant()) ?? collObj.GetMetalMaterialSmelted(Api) - : null; - - protected virtual AnvilPlacementMode PlacementMode { get; set; } = AnvilPlacementMode.Normal; - - public virtual ItemStack? TryPlaceOn(ItemStack stack, BlockEntityAnvil beAnvil) +#nullable enable +using System; +using System.Collections.Generic; +using System.Linq; +using SmithingPlus.Common.Metal; +using SmithingPlus.Metal; +using SmithingPlus.Util; +using Vintagestory.API.Client; +using Vintagestory.API.Common; +using Vintagestory.API.Config; +using Vintagestory.GameContent; + +namespace SmithingPlus.Common; + +public abstract class CollectibleBehaviorAnvilWorkable(CollectibleObject collObj) : + CollectibleBehavior(collObj), IAnvilWorkable +{ + protected ICoreAPI? Api => collObj.GetField("api"); + protected abstract byte[,,] Voxels { get; } + + protected MetalMaterial? MetalMaterial => + Api != null + ? MetalMaterialLoader.GetMaterial(Api, collObj.GetMetalVariant()) ?? collObj.GetMetalMaterialSmelted(Api) + : null; + + protected virtual AnvilPlacementMode PlacementMode { get; set; } = AnvilPlacementMode.Normal; + + public virtual ItemStack? TryPlaceOn(ItemStack stack, BlockEntityAnvil beAnvil) + { + if (Api == null || !CanWork(stack) || beAnvil is { WorkItemStack: not null, CanWorkCurrent: false }) + return null; + var workItemStack = MetalMaterial?.WorkItemStack; + if (workItemStack == null) + return null; + var sourceTemp = stack.GetTemperature(Api.World); + workItemStack.SetTemperature(Api.World, sourceTemp); + + if (beAnvil.WorkItemStack == null && PlacementMode.AllowsEmpty()) + { + TryAddVoxelsFromWorkable(Api, ref beAnvil.Voxels); + return workItemStack; + } + + if (!PlacementMode.AllowsPresent()) + return null; + + var workItemMaterial = beAnvil.WorkItemStack?.GetMetalMaterialProcessed(Api); + Core.Logger.VerboseDebug( + $"[{nameof(CollectibleBehaviorAnvilWorkable)}#{nameof(TryPlaceOn)}] base material: {MetalMaterial?.IngotCode}, " + + $"workItem base material: {workItemMaterial?.IngotCode}"); + + if (workItemMaterial == null || !workItemMaterial.Equals(MetalMaterial)) + { + (Api as ICoreClientAPI)?.TriggerIngameError(this, "notequal", + Lang.Get("Must be the same metal to add voxels")); + return null; + } + + var didSucceed = TryAddVoxelsFromWorkable(Api, ref beAnvil.Voxels); + if (didSucceed) return workItemStack; + + (Api as ICoreClientAPI)?.TriggerIngameError(this, "requireshammering", + Lang.Get("Try hammering down before adding additional voxels")); + return null; + } + + public virtual bool CanWork(ItemStack stack) + { + var temperature = stack.Collectible.GetTemperature(Api?.World, stack); + var meltingPoint = stack.Collectible.GetMeltingPoint(Api?.World, null, new DummySlot(stack)); + if (stack.ItemAttributes?["workableTemperature"].Exists == true) + return stack.ItemAttributes["workableTemperature"].AsFloat(meltingPoint / 2) <= temperature; + return temperature >= meltingPoint / 2; + } + + public virtual int GetRequiredAnvilTier(ItemStack stack) + { + var defaultValue = 0; + if (MetalMaterial != null) + defaultValue = MetalMaterial.Tier - 1; + var attributes = stack.Collectible.Attributes; + if ((attributes != null ? attributes["requiresAnvilTier"].Exists ? 1 : 0 : 0) != 0) + defaultValue = stack.Collectible.Attributes["requiresAnvilTier"].AsInt(defaultValue); + return defaultValue; + } + + public virtual List GetMatchingRecipes(ItemStack stack) { - if (Api == null || !CanWork(stack) || beAnvil is { WorkItemStack: not null, CanWorkCurrent: false }) - return null; - var workItemStack = MetalMaterial?.WorkItemStack; - if (workItemStack == null) - return null; - var sourceTemp = stack.GetTemperature(Api.World); - workItemStack.SetTemperature(Api.World, sourceTemp); - - if (beAnvil.WorkItemStack == null && PlacementMode.AllowsEmpty()) + ICoreAPI? api = Api; + if (api == null) { - TryAddVoxelsFromWorkable(Api, ref beAnvil.Voxels); - return workItemStack; + return new List(); } - if (!PlacementMode.AllowsPresent()) - return null; - - var workItemMaterial = beAnvil.WorkItemStack?.GetMetalMaterialProcessed(Api); - Core.Logger.VerboseDebug( - $"[{nameof(CollectibleBehaviorAnvilWorkable)}#{nameof(TryPlaceOn)}] base material: {MetalMaterial?.IngotCode}, " + - $"workItem base material: {workItemMaterial?.IngotCode}"); - - if (workItemMaterial == null || !workItemMaterial.Equals(MetalMaterial)) + ItemStack? baseMetalStack = MetalMaterial?.IngotStack; + List matchingRecipes = new List(); + IEnumerable smithingRecipes = api.GetSmithingRecipes(); + foreach (SmithingRecipe? recipe in smithingRecipes) { - (Api as ICoreClientAPI)?.TriggerIngameError(this, "notequal", - Lang.Get("Must be the same metal to add voxels")); - return null; - } - - var didSucceed = TryAddVoxelsFromWorkable(Api, ref beAnvil.Voxels); - if (didSucceed) return workItemStack; - - (Api as ICoreClientAPI)?.TriggerIngameError(this, "requireshammering", - Lang.Get("Try hammering down before adding additional voxels")); - return null; - } - - public virtual bool CanWork(ItemStack stack) - { - var temperature = stack.Collectible.GetTemperature(Api?.World, stack); - var meltingPoint = stack.Collectible.GetMeltingPoint(Api?.World, null, new DummySlot(stack)); - if (stack.ItemAttributes?["workableTemperature"].Exists == true) - return stack.ItemAttributes["workableTemperature"].AsFloat(meltingPoint / 2) <= temperature; - return temperature >= meltingPoint / 2; - } - - public virtual int GetRequiredAnvilTier(ItemStack stack) - { - var defaultValue = 0; - if (MetalMaterial != null) - defaultValue = MetalMaterial.Tier - 1; - var attributes = stack.Collectible.Attributes; - if ((attributes != null ? attributes["requiresAnvilTier"].Exists ? 1 : 0 : 0) != 0) - defaultValue = stack.Collectible.Attributes["requiresAnvilTier"].AsInt(defaultValue); - return defaultValue; - } - - public virtual List GetMatchingRecipes(ItemStack stack) - { - return Api.GetSmithingRecipes() - .Where(r => - ((MetalMaterial?.IngotStack is { } baseMetal && r.Ingredient.SatisfiesAsIngredient(baseMetal)) - || r.Ingredient.SatisfiesAsIngredient(stack)) - && !r.Output.ResolvedItemstack.Collectible.Code.Equals(collObj.Code)) - .OrderBy(r => r.Output.ResolvedItemstack.Collectible.Code) - .ThenBy(r => r.Output.ResolvedItemstack.StackSize) - .DistinctBy(r => r.Output.ResolvedItemstack) - .ToList(); - } - - public virtual ItemStack? GetBaseMaterial(ItemStack stack) - { - return MetalMaterial?.IngotStack; - } - - public virtual EnumHelveWorkableMode GetHelveWorkableMode(ItemStack stack, BlockEntityAnvil beAnvil) - { - return EnumHelveWorkableMode.NotWorkable; - } + CraftingRecipeIngredient? ingredient = recipe?.Ingredient; + ItemStack? resolvedOutputStack = recipe?.Output?.ResolvedItemstack; + AssetLocation? outputCode = resolvedOutputStack?.Collectible?.Code; + if (ingredient == null || resolvedOutputStack == null || outputCode == null) + { + continue; + } - public virtual int VoxelCountForHandbook(ItemStack stack) - { - return Voxels.MaterialCount(); - } + bool matchesBaseMetal = baseMetalStack != null && ingredient.SatisfiesAsIngredient(baseMetalStack); + bool matchesInputStack = ingredient.SatisfiesAsIngredient(stack); + if ((!matchesBaseMetal && !matchesInputStack) || outputCode.Equals(collObj.Code)) + { + continue; + } - public override void OnLoaded(ICoreAPI api) - { - base.OnLoaded(api); - if (Api == null) - { - Core.Logger?.Error( - "[CollectibleBehaviorAnvilWorkable] Reflection failed: field 'api' in collectible class is null."); - return; + matchingRecipes.Add(recipe); } - if (Voxels.MaterialCount() == 0) - Api?.Logger.Error("CollectibleBehaviorAnvilWorkable for {0} has no voxels defined. " + - "Please check the 'voxels' attribute in the item JSON.", collObj.Code); - } - - protected virtual bool TryAddVoxelsFromWorkable(ICoreAPI api, ref byte[,,] beAnvilVoxels) - { - if (Voxels.MaterialCount() == 0) - return false; - - // Target (anvil) dims - var tx = beAnvilVoxels.GetLength(0); - var ty = beAnvilVoxels.GetLength(1); - var tz = beAnvilVoxels.GetLength(2); - - // Source (workable) dims (up to 16x16x6 typically) - var sx = Voxels.GetLength(0); - var sy = Voxels.GetLength(1); - var sz = Voxels.GetLength(2); + matchingRecipes.Sort(CompareMatchingRecipes); - var ox = Math.Min(tx, sx); - var oz = Math.Min(tz, sz); - - // Work on a copy so we can roll back on failure - var voxelsCopy = (byte[,,])beAnvilVoxels.Clone(); - - for (var x = 0; x < ox; x++) - for (var z = 0; z < oz; z++) + List distinctRecipes = new List(matchingRecipes.Count); + HashSet encounteredOutputs = new HashSet(); + foreach (SmithingRecipe recipe in matchingRecipes) { - // Find first empty Y in the target column - var y = 0; - while (y < ty && voxelsCopy[x, y, z] != 0) y++; - - // If column is full, fail (matches your early-return behavior) - if (y >= ty) return false; - - // Place the source column starting from ny = 0 upward - for (var ny = 0; ny < sy; ny++) + ItemStack? resolvedOutputStack = recipe.Output?.ResolvedItemstack; + if (resolvedOutputStack != null && encounteredOutputs.Add(resolvedOutputStack)) { - var val = Voxels[x, ny, z]; - if (val == 0) continue; - - var tyIndex = y + ny; - if (tyIndex >= ty) - // Would overflow this column — abort entirely - return false; - - voxelsCopy[x, tyIndex, z] = val; - if (val == 1) - { - } + distinctRecipes.Add(recipe); } } - // All columns fit — commit the copy - beAnvilVoxels = voxelsCopy; - return true; + return distinctRecipes; } -} -public enum AnvilPlacementMode -{ - None = -1, // No placement (why would you want this? ;p) - Normal = 0, // Can be placed both on a workitem and on an empty anvil - Empty = 1, // Cannot be placed when workitem is present - Present = 2 // Cannot be placed when workitem is missing -} - -public static class PlacementModeExtensions -{ - public static bool AllowsPresent(this AnvilPlacementMode mode) + private static int CompareMatchingRecipes(SmithingRecipe leftRecipe, SmithingRecipe rightRecipe) { - return mode is AnvilPlacementMode.Normal or AnvilPlacementMode.Present; - } + ItemStack? leftOutputStack = leftRecipe.Output?.ResolvedItemstack; + ItemStack? rightOutputStack = rightRecipe.Output?.ResolvedItemstack; + string leftCode = leftOutputStack?.Collectible?.Code?.ToString() ?? string.Empty; + string rightCode = rightOutputStack?.Collectible?.Code?.ToString() ?? string.Empty; + int codeComparison = string.CompareOrdinal(leftCode, rightCode); + if (codeComparison != 0) + { + return codeComparison; + } - public static bool AllowsEmpty(this AnvilPlacementMode mode) - { - return mode is AnvilPlacementMode.Normal or AnvilPlacementMode.Empty; + int leftStackSize = leftOutputStack?.StackSize ?? 0; + int rightStackSize = rightOutputStack?.StackSize ?? 0; + return leftStackSize.CompareTo(rightStackSize); } -} \ No newline at end of file + + public virtual ItemStack? GetBaseMaterial(ItemStack stack) + { + return MetalMaterial?.IngotStack; + } + + public virtual EnumHelveWorkableMode GetHelveWorkableMode(ItemStack stack, BlockEntityAnvil beAnvil) + { + return EnumHelveWorkableMode.NotWorkable; + } + + public virtual int VoxelCountForHandbook(ItemStack stack) + { + return Voxels.MaterialCount(); + } + + public override void OnLoaded(ICoreAPI api) + { + base.OnLoaded(api); + if (Api == null) + { + Core.Logger?.Error( + "[CollectibleBehaviorAnvilWorkable] Reflection failed: field 'api' in collectible class is null."); + return; + } + + if (Voxels.MaterialCount() == 0) + Api?.Logger.Error("CollectibleBehaviorAnvilWorkable for {0} has no voxels defined. " + + "Please check the 'voxels' attribute in the item JSON.", collObj.Code); + } + + protected virtual bool TryAddVoxelsFromWorkable(ICoreAPI api, ref byte[,,] beAnvilVoxels) + { + if (Voxels.MaterialCount() == 0) + return false; + + // Target (anvil) dims + var tx = beAnvilVoxels.GetLength(0); + var ty = beAnvilVoxels.GetLength(1); + var tz = beAnvilVoxels.GetLength(2); + + // Source (workable) dims (up to 16x16x6 typically) + var sx = Voxels.GetLength(0); + var sy = Voxels.GetLength(1); + var sz = Voxels.GetLength(2); + + var ox = Math.Min(tx, sx); + var oz = Math.Min(tz, sz); + + // Work on a copy so we can roll back on failure + var voxelsCopy = (byte[,,])beAnvilVoxels.Clone(); + + for (var x = 0; x < ox; x++) + for (var z = 0; z < oz; z++) + { + // Find first empty Y in the target column + var y = 0; + while (y < ty && voxelsCopy[x, y, z] != 0) y++; + + // If column is full, fail (matches your early-return behavior) + if (y >= ty) return false; + + // Place the source column starting from ny = 0 upward + for (var ny = 0; ny < sy; ny++) + { + var val = Voxels[x, ny, z]; + if (val == 0) continue; + + var tyIndex = y + ny; + if (tyIndex >= ty) + // Would overflow this column — abort entirely + return false; + + voxelsCopy[x, tyIndex, z] = val; + if (val == 1) + { + } + } + } + + // All columns fit — commit the copy + beAnvilVoxels = voxelsCopy; + return true; + } +} + +public enum AnvilPlacementMode +{ + None = -1, // No placement (why would you want this? ;p) + Normal = 0, // Can be placed both on a workitem and on an empty anvil + Empty = 1, // Cannot be placed when workitem is present + Present = 2 // Cannot be placed when workitem is missing +} + +public static class PlacementModeExtensions +{ + public static bool AllowsPresent(this AnvilPlacementMode mode) + { + return mode is AnvilPlacementMode.Normal or AnvilPlacementMode.Present; + } + + public static bool AllowsEmpty(this AnvilPlacementMode mode) + { + return mode is AnvilPlacementMode.Normal or AnvilPlacementMode.Empty; + } +} diff --git a/SmithingPlus/Common/CollectibleBehaviorJsonAnvilWorkable.cs b/SmithingPlus/Common/CollectibleBehaviorJsonAnvilWorkable.cs index 3ece70f..e95d6cc 100644 --- a/SmithingPlus/Common/CollectibleBehaviorJsonAnvilWorkable.cs +++ b/SmithingPlus/Common/CollectibleBehaviorJsonAnvilWorkable.cs @@ -1,136 +1,167 @@ -#nullable enable -using System; -using System.Linq; -using JetBrains.Annotations; -using SmithingPlus.Util; -using Vintagestory.API.Common; -using Vintagestory.API.Datastructures; -using Vintagestory.GameContent; - -namespace SmithingPlus.Common; - -public sealed class CollectibleBehaviorJsonAnvilWorkable(CollectibleObject collObj) - : CollectibleBehaviorAnvilWorkable(collObj) -{ - protected override byte[,,] Voxels => HasExtraVoxels - ? GenVoxelsFromJsonPatternWithExtra(Pattern, Api?.World.Rand, ExtraVoxelChance) - : GenVoxelsFromJsonPattern(Pattern); - - private string[][] Pattern { get; set; } = [[]]; - private bool HasExtraVoxels { get; set; } - private float ExtraVoxelChance { get; set; } = 0.5f; - private EnumHelveWorkableMode HelveWorkableMode { get; set; } = EnumHelveWorkableMode.NotWorkable; - - public override void Initialize(JsonObject properties) - { - base.Initialize(properties); - HasExtraVoxels = properties[PropertyKeys.HasExtraVoxels].Exists && - properties[PropertyKeys.HasExtraVoxels].AsBool(); - ExtraVoxelChance = properties[PropertyKeys.ExtraVoxelChance].Exists - ? properties[PropertyKeys.ExtraVoxelChance].AsFloat(0.5f) - : 0f; - HelveWorkableMode = properties[PropertyKeys.HelveWorkableMode].Exists - ? Enum.Parse( - properties[PropertyKeys.HelveWorkableMode].AsString(nameof(EnumHelveWorkableMode.NotWorkable))) - : EnumHelveWorkableMode.NotWorkable; - var jsonPattern = properties[PropertyKeys.Voxels].Exists ? properties[PropertyKeys.Voxels].AsArray() : null; - if (jsonPattern is not { Length: > 0 }) return; - var jsonArray = jsonPattern.Select(s => s.AsArray()).ToArray(); - Pattern = jsonArray - .Select(s => - s.Select(t => t.AsString()).ToArray() - ).ToArray(); - } +#nullable enable +using System; +using JetBrains.Annotations; +using SmithingPlus.Util; +using Vintagestory.API.Common; +using Vintagestory.API.Datastructures; +using Vintagestory.GameContent; + +namespace SmithingPlus.Common; + +public sealed class CollectibleBehaviorJsonAnvilWorkable(CollectibleObject collObj) + : CollectibleBehaviorAnvilWorkable(collObj) +{ + protected override byte[,,] Voxels => HasExtraVoxels + ? GenVoxelsFromJsonPatternWithExtra(Pattern, Api?.World.Rand, ExtraVoxelChance) + : GenVoxelsFromJsonPattern(Pattern); + + private string[][] Pattern { get; set; } = [[]]; + private bool HasExtraVoxels { get; set; } + private float ExtraVoxelChance { get; set; } = 0.5f; + private EnumHelveWorkableMode HelveWorkableMode { get; set; } = EnumHelveWorkableMode.NotWorkable; + + public override void Initialize(JsonObject properties) + { + base.Initialize(properties); + HasExtraVoxels = properties[PropertyKeys.HasExtraVoxels].Exists && + properties[PropertyKeys.HasExtraVoxels].AsBool(); + ExtraVoxelChance = properties[PropertyKeys.ExtraVoxelChance].Exists + ? properties[PropertyKeys.ExtraVoxelChance].AsFloat(0.5f) + : 0f; + HelveWorkableMode = properties[PropertyKeys.HelveWorkableMode].Exists + ? Enum.Parse( + properties[PropertyKeys.HelveWorkableMode].AsString(nameof(EnumHelveWorkableMode.NotWorkable))) + : EnumHelveWorkableMode.NotWorkable; + JsonObject[]? jsonPattern = properties[PropertyKeys.Voxels].Exists + ? properties[PropertyKeys.Voxels].AsArray() + : null; + if (jsonPattern == null || jsonPattern.Length == 0) + { + return; + } - public override EnumHelveWorkableMode GetHelveWorkableMode(ItemStack stack, BlockEntityAnvil beAnvil) - { - return HelveWorkableMode; - } + string[][] parsedPattern = new string[jsonPattern.Length][]; + for (int layerIndex = 0; layerIndex < jsonPattern.Length; layerIndex++) + { + JsonObject[]? jsonLayer = jsonPattern[layerIndex]?.AsArray(); + if (jsonLayer == null) + { + Core.Logger.Error( + "CollectibleBehaviorJsonAnvilWorkable contains an invalid voxel-pattern layer for {0}.", + collObj.Code); + return; + } - // Only use always present voxels for handbook - public override int VoxelCountForHandbook(ItemStack stack) - { - return GenVoxelsFromJsonPattern(Pattern).MaterialCount(); - } + string[] parsedLayer = new string[jsonLayer.Length]; + for (int rowIndex = 0; rowIndex < jsonLayer.Length; rowIndex++) + { + string? parsedRow = jsonLayer[rowIndex]?.AsString(); + if (parsedRow == null) + { + Core.Logger.Error( + "CollectibleBehaviorJsonAnvilWorkable contains an invalid voxel-pattern row for {0}.", + collObj.Code); + return; + } - /// - /// Generates voxels from a JSON pattern. - /// The pattern is expected to be a 3D array of strings, - /// where each string represents a layer of the recipe. - /// Each character in the string can be: - /// '#' for a full voxel, - /// '*' for a slag voxel, - /// '_' or any character for an empty voxel. - /// The generated voxels will be centered in a 16x6x16 array. - /// - /// The JSON pattern to generate voxels from. - private static byte[,,] GenVoxelsFromJsonPattern(string[][] pattern) - { - return GenVoxelsFromJsonPatternWithExtra(pattern); - } + parsedLayer[rowIndex] = parsedRow; + } - /// - /// Generates voxels from a JSON pattern with extra voxel chance. - /// The pattern is expected to be a 3D array of strings, - /// where each string represents a layer of the recipe. - /// Each character in the string can be: - /// '#' for a full voxel, - /// '*' for a slag voxel, - /// 'o' for a random full voxel (with a chance defined by extraVoxelChance), - /// 'x' for a random slag voxel (with a chance defined by extraVoxelChance), - /// '?' for a random voxel (either full or slag with 50% chance), - /// '_' or any character for an empty voxel. - /// The generated voxels will be centered in a 16x6x16 array. - /// - /// The JSON pattern to generate voxels from. - /// An optional random number generator. If null, a default one will be used. - /// The chance of generating extra voxels (for 'o' and 'x' characters). - public static byte[,,] GenVoxelsFromJsonPatternWithExtra( - string[][] pattern, - Random? rand = null, - float extraVoxelChance = 0.0f) - { - var hasExtraVoxels = rand != null; - // Fallback if api is not available - extraVoxelChance = hasExtraVoxels ? extraVoxelChance : 0; - var voxels = new byte[16, 6, 16]; - var length = pattern[0][0].Length; - var width = pattern[0].Length; - var height = pattern.Length; - // Center the recipe to the horizontal middle - var startX = (16 - width) / 2; - var startZ = (16 - length) / 2; - for (var x = 0; x < Math.Min(width, 16); x++) - for (var y = 0; y < Math.Min(height, 6); y++) - for (var z = 0; z < Math.Min(length, 16); z++) - { - var c = pattern[y][x][z]; - var b = c switch - { - '#' => EnumVoxelMaterial.Metal, // always full - '*' => EnumVoxelMaterial.Slag, // always slag - 'o' => rand?.NextDouble() < extraVoxelChance - ? EnumVoxelMaterial.Metal - : EnumVoxelMaterial.Empty, // random full - 'x' => rand?.NextDouble() < extraVoxelChance - ? EnumVoxelMaterial.Slag - : EnumVoxelMaterial.Empty, // random slag - '?' => hasExtraVoxels - ? rand?.NextDouble() < 0.5f ? EnumVoxelMaterial.Metal : EnumVoxelMaterial.Slag - : EnumVoxelMaterial.Empty, // random full/slag - _ => EnumVoxelMaterial.Empty // empty (_ or space or anything else) - }; - voxels[z + startZ, y, x + startX] = (byte)b; + parsedPattern[layerIndex] = parsedLayer; } - return voxels; - } - - private static class PropertyKeys - { - public const string HasExtraVoxels = "hasExtraVoxels"; - public const string ExtraVoxelChance = "extraVoxelChance"; - public const string HelveWorkableMode = "helveWorkableMode"; - public const string Voxels = "voxels"; - } -} \ No newline at end of file + Pattern = parsedPattern; + } + + public override EnumHelveWorkableMode GetHelveWorkableMode(ItemStack stack, BlockEntityAnvil beAnvil) + { + return HelveWorkableMode; + } + + // Only use always present voxels for handbook + public override int VoxelCountForHandbook(ItemStack stack) + { + return GenVoxelsFromJsonPattern(Pattern).MaterialCount(); + } + + /// + /// Generates voxels from a JSON pattern. + /// The pattern is expected to be a 3D array of strings, + /// where each string represents a layer of the recipe. + /// Each character in the string can be: + /// '#' for a full voxel, + /// '*' for a slag voxel, + /// '_' or any character for an empty voxel. + /// The generated voxels will be centered in a 16x6x16 array. + /// + /// The JSON pattern to generate voxels from. + private static byte[,,] GenVoxelsFromJsonPattern(string[][] pattern) + { + return GenVoxelsFromJsonPatternWithExtra(pattern); + } + + /// + /// Generates voxels from a JSON pattern with extra voxel chance. + /// The pattern is expected to be a 3D array of strings, + /// where each string represents a layer of the recipe. + /// Each character in the string can be: + /// '#' for a full voxel, + /// '*' for a slag voxel, + /// 'o' for a random full voxel (with a chance defined by extraVoxelChance), + /// 'x' for a random slag voxel (with a chance defined by extraVoxelChance), + /// '?' for a random voxel (either full or slag with 50% chance), + /// '_' or any character for an empty voxel. + /// The generated voxels will be centered in a 16x6x16 array. + /// + /// The JSON pattern to generate voxels from. + /// An optional random number generator. If null, a default one will be used. + /// The chance of generating extra voxels (for 'o' and 'x' characters). + public static byte[,,] GenVoxelsFromJsonPatternWithExtra( + string[][] pattern, + Random? rand = null, + float extraVoxelChance = 0.0f) + { + var hasExtraVoxels = rand != null; + // Fallback if api is not available + extraVoxelChance = hasExtraVoxels ? extraVoxelChance : 0; + var voxels = new byte[16, 6, 16]; + var length = pattern[0][0].Length; + var width = pattern[0].Length; + var height = pattern.Length; + // Center the recipe to the horizontal middle + var startX = (16 - width) / 2; + var startZ = (16 - length) / 2; + for (var x = 0; x < Math.Min(width, 16); x++) + for (var y = 0; y < Math.Min(height, 6); y++) + for (var z = 0; z < Math.Min(length, 16); z++) + { + var c = pattern[y][x][z]; + var b = c switch + { + '#' => EnumVoxelMaterial.Metal, // always full + '*' => EnumVoxelMaterial.Slag, // always slag + 'o' => rand?.NextDouble() < extraVoxelChance + ? EnumVoxelMaterial.Metal + : EnumVoxelMaterial.Empty, // random full + 'x' => rand?.NextDouble() < extraVoxelChance + ? EnumVoxelMaterial.Slag + : EnumVoxelMaterial.Empty, // random slag + '?' => hasExtraVoxels + ? rand?.NextDouble() < 0.5f ? EnumVoxelMaterial.Metal : EnumVoxelMaterial.Slag + : EnumVoxelMaterial.Empty, // random full/slag + _ => EnumVoxelMaterial.Empty // empty (_ or space or anything else) + }; + voxels[z + startZ, y, x + startX] = (byte)b; + } + + return voxels; + } + + private static class PropertyKeys + { + public const string HasExtraVoxels = "hasExtraVoxels"; + public const string ExtraVoxelChance = "extraVoxelChance"; + public const string HelveWorkableMode = "helveWorkableMode"; + public const string Voxels = "voxels"; + } +} diff --git a/SmithingPlus/Common/Metal/MetalMaterial.cs b/SmithingPlus/Common/Metal/MetalMaterial.cs index ea72d7b..1279b0c 100644 --- a/SmithingPlus/Common/Metal/MetalMaterial.cs +++ b/SmithingPlus/Common/Metal/MetalMaterial.cs @@ -1,84 +1,84 @@ -using System; -using Newtonsoft.Json; -using SmithingPlus.Util; -using Vintagestory.API.Common; -using Vintagestory.GameContent; - -namespace SmithingPlus.Metal; - -#nullable enable -[JsonObject(MemberSerialization.OptIn)] -public class MetalMaterial : IEquatable -{ - // These json properties might be null, fallback uses classic vanilla naming conventions - [JsonProperty("ingot")] private AssetLocation? _ingotCode; - [JsonProperty("metalbit")] private AssetLocation? _metalBitCode; - [JsonProperty("tier")] private int? _tier; - [JsonProperty("workitem")] private AssetLocation? _workItemCode; - [JsonProperty("code")] public required AssetLocation Code { get; init; } - public bool Resolved { get; private set; } - public string Variant => Code.Path; - public AssetLocation IngotCode => _ingotCode ?? new AssetLocation(Code.Domain, $"ingot-{Variant}"); - public AssetLocation MetalBitCode => _metalBitCode ?? new AssetLocation(Code.Domain, $"metalbit-{Variant}"); - public AssetLocation WorkItemCode => _workItemCode ?? new AssetLocation(Code.Domain, $"workitem-{Variant}"); - public ItemIngot? IngotItem { get; private set; } - public Item? MetalBitItem { get; private set; } - public ItemWorkItem? WorkItem { get; private set; } - public ItemStack? IngotStack => IngotItem != null ? new ItemStack(IngotItem) : null; - public ItemStack? MetalBitStack => MetalBitItem != null ? new ItemStack(MetalBitItem) : null; - public ItemStack? WorkItemStack => WorkItem != null ? new ItemStack(WorkItem) : null; - public int Tier { get; private set; } - - public bool Equals(MetalMaterial? other) - { - return other is not null && Code.Equals(other.Code); - } - - /// - /// Resolves the items and stacks using the provided API. - /// Sets the resolved flag to true if the ingot is successfully loaded. - /// - public void Resolve(ICoreAPI api) - { - IngotItem = api.World.GetItem(IngotCode) as ItemIngot; - MetalBitItem = api.World.GetItem(MetalBitCode); - WorkItem = api.World.GetItem(WorkItemCode) as ItemWorkItem; - Tier = _tier ?? GetTier(api); - if (IngotItem != null) - { - Resolved = true; - } - else - { - var ingot = api.World.GetItem(new AssetLocation("game:ingot-copper")); - Resolved = false; - api.Logger.Error( - $"[MetalMaterial] Failed to resolve ingot item {IngotCode} for metal material {Code}"); - } - - if (MetalBitItem == null) - api.Logger.Warning( - $"[MetalMaterial] Failed to resolve metal bit item {MetalBitCode} for metal material {Code}"); - if (WorkItem == null) - api.Logger.Warning( - $"[MetalMaterial] Failed to resolve work item {WorkItemCode} for metal material {Code}"); - } - - private int GetTier(ICoreAPI api) - { - return api.GetModSystem()?.metalsByCode - .TryGetValue(Variant, out var metalProperty) == true - ? metalProperty?.Tier ?? 0 - : 0; - } - - public override bool Equals(object? obj) - { - return Equals(obj as MetalMaterial); - } - - public override int GetHashCode() - { - return Code.GetHashCode(); - } -} \ No newline at end of file +using System; +using Newtonsoft.Json; +using SmithingPlus.Util; +using Vintagestory.API.Common; +using Vintagestory.GameContent; + +namespace SmithingPlus.Metal; + +#nullable enable +[JsonObject(MemberSerialization.OptIn)] +public class MetalMaterial : IEquatable +{ + // These json properties might be null, fallback uses classic vanilla naming conventions + [JsonProperty("ingot")] private AssetLocation? _ingotCode = null; + [JsonProperty("metalbit")] private AssetLocation? _metalBitCode = null; + [JsonProperty("tier")] private int? _tier = null; + [JsonProperty("workitem")] private AssetLocation? _workItemCode = null; + [JsonProperty("code")] public required AssetLocation Code { get; init; } + public bool Resolved { get; private set; } + public string Variant => Code.Path; + public AssetLocation IngotCode => _ingotCode ?? new AssetLocation(Code.Domain, $"ingot-{Variant}"); + public AssetLocation MetalBitCode => _metalBitCode ?? new AssetLocation(Code.Domain, $"metalbit-{Variant}"); + public AssetLocation WorkItemCode => _workItemCode ?? new AssetLocation(Code.Domain, $"workitem-{Variant}"); + public ItemIngot? IngotItem { get; private set; } + public Item? MetalBitItem { get; private set; } + public ItemWorkItem? WorkItem { get; private set; } + public ItemStack? IngotStack => IngotItem != null ? new ItemStack(IngotItem) : null; + public ItemStack? MetalBitStack => MetalBitItem != null ? new ItemStack(MetalBitItem) : null; + public ItemStack? WorkItemStack => WorkItem != null ? new ItemStack(WorkItem) : null; + public int Tier { get; private set; } + + public bool Equals(MetalMaterial? other) + { + return other is not null && Code.Equals(other.Code); + } + + /// + /// Resolves the items and stacks using the provided API. + /// Sets the resolved flag to true if the ingot is successfully loaded. + /// + public void Resolve(ICoreAPI api) + { + IngotItem = api.World.GetItem(IngotCode) as ItemIngot; + MetalBitItem = api.World.GetItem(MetalBitCode); + WorkItem = api.World.GetItem(WorkItemCode) as ItemWorkItem; + Tier = _tier ?? GetTier(api); + if (IngotItem != null) + { + Resolved = true; + } + else + { + var ingot = api.World.GetItem(new AssetLocation("game:ingot-copper")); + Resolved = false; + api.Logger.Error( + $"[MetalMaterial] Failed to resolve ingot item {IngotCode} for metal material {Code}"); + } + + if (MetalBitItem == null) + api.Logger.Warning( + $"[MetalMaterial] Failed to resolve metal bit item {MetalBitCode} for metal material {Code}"); + if (WorkItem == null) + api.Logger.Warning( + $"[MetalMaterial] Failed to resolve work item {WorkItemCode} for metal material {Code}"); + } + + private int GetTier(ICoreAPI api) + { + return api.GetModSystem()?.metalsByCode + .TryGetValue(Variant, out var metalProperty) == true + ? metalProperty?.Tier ?? 0 + : 0; + } + + public override bool Equals(object? obj) + { + return Equals(obj as MetalMaterial); + } + + public override int GetHashCode() + { + return Code.GetHashCode(); + } +} diff --git a/SmithingPlus/ToolRecovery/CollectibleBehaviorRepairableTool.cs b/SmithingPlus/ToolRecovery/CollectibleBehaviorRepairableTool.cs index bfdc2e9..245e205 100644 --- a/SmithingPlus/ToolRecovery/CollectibleBehaviorRepairableTool.cs +++ b/SmithingPlus/ToolRecovery/CollectibleBehaviorRepairableTool.cs @@ -1,41 +1,41 @@ -#nullable enable -using System.Text; -using SmithingPlus.Util; -using Vintagestory.API.Common; -using Vintagestory.API.Config; -using Vintagestory.API.Util; - -namespace SmithingPlus.ToolRecovery; - -public class CollectibleBehaviorRepairableTool : CollectibleBehavior -{ - public CollectibleBehaviorRepairableTool(CollectibleObject collObj) : base(collObj) - { - } - - protected virtual string LangKey => "Repaired"; - - public override void GetHeldItemInfo(ItemSlot? inSlot, StringBuilder dsc, IWorldAccessor world, - bool withDebugInfo) - { - base.GetHeldItemInfo(inSlot, dsc, world, withDebugInfo); - - var itemstack = inSlot?.Itemstack; - var collectible = itemstack?.Collectible; +#nullable enable +using System.Text; +using SmithingPlus.Util; +using Vintagestory.API.Common; +using Vintagestory.API.Config; +using Vintagestory.API.Util; + +namespace SmithingPlus.ToolRecovery; + +public class CollectibleBehaviorRepairableTool : CollectibleBehavior +{ + public CollectibleBehaviorRepairableTool(CollectibleObject collObj) : base(collObj) + { + } + + protected virtual string LangKey => "Repaired"; + + public override void GetHeldItemInfo(ItemSlot? inSlot, StringBuilder dsc, IWorldAccessor world, + bool withDebugInfo) + { + base.GetHeldItemInfo(inSlot, dsc, world, withDebugInfo); + + ItemStack? itemStack = inSlot?.Itemstack; + CollectibleObject? collectible = itemStack?.Collectible; var code = collectible?.Code; - if (code == null || inSlot == null) + if (code == null || itemStack == null) { - Core.Logger.Error("Failed to get code for itemstack {0}", itemstack); + Core.Logger.Error("Failed to get code for itemstack {0}", itemStack); return; } - - if (!WildcardUtil.Match(Core.Config.RepairableToolSelector, code.ToString())) - return; - var brokenCount = inSlot.Itemstack.GetBrokenCount(); + + if (!WildcardUtil.Match(Core.Config.RepairableToolSelector, code.ToString())) + return; + var brokenCount = itemStack.GetBrokenCount(); if (brokenCount <= 0) return; if (Core.CConfig.ShowRepairedCount) dsc.AppendLine(Lang.Get($"{LangKey} {{0}} times", brokenCount)); - if (Core.CConfig.ShowRepairSmithName && inSlot.Itemstack.GetRepairSmith() is { } repairSmith) + if (Core.CConfig.ShowRepairSmithName && itemStack.GetRepairSmith() is { } repairSmith) dsc.AppendLine(Lang.Get("Last repaired by {0}", repairSmith)); - } -} \ No newline at end of file + } +} diff --git a/SmithingPlus/Util/CollectibleExtensions.cs b/SmithingPlus/Util/CollectibleExtensions.cs index ff22a01..fcc8c67 100644 --- a/SmithingPlus/Util/CollectibleExtensions.cs +++ b/SmithingPlus/Util/CollectibleExtensions.cs @@ -1,166 +1,172 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using Newtonsoft.Json.Linq; -using Vintagestory.API.Common; -using Vintagestory.API.Datastructures; -using Vintagestory.API.MathTools; -using Vintagestory.API.Util; -using Vintagestory.GameContent; - -namespace SmithingPlus.Util; - -#nullable enable -public static class CollectibleExtensions -{ - private static readonly JToken ForgeTransformToken = JToken.FromObject( - new ModelTransform - { - Translation = new Vec3f(0, -0.1f, 0.35f), - Rotation = new Vec3f(0, 90f, 0), - Scale = 0.7f - } - ); - - private static void EnsureAttributesNotNull(this CollectibleObject obj) - { - obj.Attributes ??= new JsonObject(new JObject()); - } - +using System; +using System.Collections.Generic; +using System.Linq; +using Newtonsoft.Json.Linq; +using Vintagestory.API.Common; +using Vintagestory.API.Datastructures; +using Vintagestory.API.MathTools; +using Vintagestory.API.Util; +using Vintagestory.GameContent; + +namespace SmithingPlus.Util; + +#nullable enable +public static class CollectibleExtensions +{ + private static readonly JToken ForgeTransformToken = JToken.FromObject( + new ModelTransform + { + Translation = new Vec3f(0, -0.1f, 0.35f), + Rotation = new Vec3f(0, 90f, 0), + Scale = 0.7f + } + ); + + private static void EnsureAttributesNotNull(this CollectibleObject obj) + { + obj.Attributes ??= new JsonObject(new JObject()); + } + public static void MakeForgeable(this CollectibleObject collObj) { - collObj.EnsureAttributesNotNull(); - var token = collObj.Attributes.Token; - token["forgable"] = true; - token["inForgeTransform"] = ForgeTransformToken; - collObj.Attributes.Token = token; - } - - public static void AddBehavior(this CollectibleObject collectible) where T : CollectibleBehavior - { - var existingBehavior = collectible.CollectibleBehaviors.FirstOrDefault(b => b.GetType() == typeof(T)); - collectible.CollectibleBehaviors.Remove(existingBehavior); - if (Activator.CreateInstance(typeof(T), collectible) is not T behavior) - { - Core.Logger.Error("[CollectibleExtensions] Failed to create behavior {0} for {1}", typeof(T).Name, - collectible.Code); - return; - } - - collectible.CollectibleBehaviors = collectible.CollectibleBehaviors.Append(behavior); - } - - public static void AddBehaviorIf(this CollectibleObject collectible, bool condition) - where T : CollectibleBehavior - { - if (!condition) return; - collectible.AddBehavior(); - } - - public static bool IsRepairableTool(this CollectibleObject collObj, bool verbose = false) - { - var repairable = WildcardUtil.Match(Core.Config.RepairableToolSelector, collObj.Code.ToString()); - if (verbose && !repairable) Core.Logger.VerboseDebug("Not a repairable tool: {0}", collObj.Code); - return repairable; - } - - public static bool MatchesToolHeadSelector(this CollectibleObject collObj, bool verbose = false) - { - var repairable = WildcardUtil.Match(Core.Config.ToolHeadSelector, collObj.Code.ToString()); - if (verbose && !repairable) Core.Logger.VerboseDebug("Not a tool head: {0}", collObj.Code); - return repairable; - } - - public static SmithingRecipe? GetSmithingRecipe(this CollectibleObject collObj, ICoreAPI api) - { - 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 byIngredient = ObjectCacheUtil.GetOrCreate(api, $"{Core.ModId}:smithingRecipesByIngredient", () => + JsonObject? attributes = collObj.Attributes; + if (attributes == null) { - 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 = []; - // Prevent duplicate entries when a recipe has the same ingredient multiple times - 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 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) - { - var api = collObj.GetField("api"); - if (api == null) - { - Core.Logger.Error("[CollectibleWithVariant] Reflection failed to get collectible object api field"); - return null; - } - - var codeWithVariant = collObj.CodeWithVariant(type, value); - switch (collObj.ItemClass) - { - case EnumItemClass.Block: - return api.World.GetBlock(codeWithVariant); - case EnumItemClass.Item: - return api.World.GetItem(codeWithVariant); - default: - Core.Logger.Error( - $"[CollectibleWithVariant] Invalid ItemClass \"{collObj.ItemClass}\" for collectible {collObj.Code}"); - return null; + attributes = new JsonObject(new JObject()); + collObj.Attributes = attributes; } - } - public static T GetBehavior(this CollectibleObject collObj, bool withInheritance) where T : CollectibleBehavior - { - return (T)collObj.GetCollectibleBehavior(typeof(T), withInheritance); - } - - /// - /// Gets the metal properties variant from a CollectibleBehaviorQuenchable behavior. - /// - public static CollectibleBehaviorQuenchable.MetalPropertyVariant? GetMetalProps( - this CollectibleBehaviorQuenchable behavior) - { - return behavior?.GetField("metalProps"); - } -} + JToken token = attributes.Token; + token["forgable"] = true; + token["inForgeTransform"] = ForgeTransformToken; + attributes.Token = token; + } + + public static void AddBehavior(this CollectibleObject collectible) where T : CollectibleBehavior + { + var existingBehavior = collectible.CollectibleBehaviors.FirstOrDefault(b => b.GetType() == typeof(T)); + collectible.CollectibleBehaviors.Remove(existingBehavior); + if (Activator.CreateInstance(typeof(T), collectible) is not T behavior) + { + Core.Logger.Error("[CollectibleExtensions] Failed to create behavior {0} for {1}", typeof(T).Name, + collectible.Code); + return; + } + + collectible.CollectibleBehaviors = collectible.CollectibleBehaviors.Append(behavior); + } + + public static void AddBehaviorIf(this CollectibleObject collectible, bool condition) + where T : CollectibleBehavior + { + if (!condition) return; + collectible.AddBehavior(); + } + + public static bool IsRepairableTool(this CollectibleObject collObj, bool verbose = false) + { + var repairable = WildcardUtil.Match(Core.Config.RepairableToolSelector, collObj.Code.ToString()); + if (verbose && !repairable) Core.Logger.VerboseDebug("Not a repairable tool: {0}", collObj.Code); + return repairable; + } + + public static bool MatchesToolHeadSelector(this CollectibleObject collObj, bool verbose = false) + { + var repairable = WildcardUtil.Match(Core.Config.ToolHeadSelector, collObj.Code.ToString()); + if (verbose && !repairable) Core.Logger.VerboseDebug("Not a tool head: {0}", collObj.Code); + return repairable; + } + + public static SmithingRecipe? GetSmithingRecipe(this CollectibleObject collObj, ICoreAPI api) + { + 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 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 = []; + // Prevent duplicate entries when a recipe has the same ingredient multiple times + 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 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) + { + var api = collObj.GetField("api"); + if (api == null) + { + Core.Logger.Error("[CollectibleWithVariant] Reflection failed to get collectible object api field"); + return null; + } + + var codeWithVariant = collObj.CodeWithVariant(type, value); + switch (collObj.ItemClass) + { + case EnumItemClass.Block: + return api.World.GetBlock(codeWithVariant); + case EnumItemClass.Item: + return api.World.GetItem(codeWithVariant); + default: + Core.Logger.Error( + $"[CollectibleWithVariant] Invalid ItemClass \"{collObj.ItemClass}\" for collectible {collObj.Code}"); + return null; + } + } + + public static T GetBehavior(this CollectibleObject collObj, bool withInheritance) where T : CollectibleBehavior + { + return (T)collObj.GetCollectibleBehavior(typeof(T), withInheritance); + } + + /// + /// Gets the metal properties variant from a CollectibleBehaviorQuenchable behavior. + /// + public static CollectibleBehaviorQuenchable.MetalPropertyVariant? GetMetalProps( + this CollectibleBehaviorQuenchable behavior) + { + return behavior?.GetField("metalProps"); + } +} diff --git a/SmithingPlus/Util/ItemStackExtensions.cs b/SmithingPlus/Util/ItemStackExtensions.cs index e647817..5b0667f 100644 --- a/SmithingPlus/Util/ItemStackExtensions.cs +++ b/SmithingPlus/Util/ItemStackExtensions.cs @@ -1,270 +1,308 @@ -#nullable enable -using System; -using System.Collections.Generic; -using System.Linq; -using SmithingPlus.Common.Metal; -using Vintagestory.API.Common; -using Vintagestory.API.Datastructures; -using Vintagestory.GameContent; - -namespace SmithingPlus.Util; - -public static class ItemStackExtensions -{ - internal static int? GetRemainingDurability(this ItemStack itemStack) - { - return itemStack.Collectible.GetRemainingDurability(itemStack); - } - - internal static int? GetMaxDurability(this ItemStack itemStack) - { - return itemStack.Collectible.GetMaxDurability(itemStack); - } - - internal static float? GetDurabilityPercentage(this ItemStack itemStack) - { - if (itemStack.GetMaxDurability() == 0) - return null; - return itemStack.GetRemainingDurability() / itemStack.GetMaxDurability(); - } - - internal static void SetDurability(this ItemStack itemStack, int number) - { - itemStack.Collectible.SetDurability(itemStack, number); - } - - internal static void CloneBrokenCount(this ItemStack itemStack, ItemStack fromStack, int extraCount = 0) - { - var brokenCount = fromStack.GetBrokenCount(); - itemStack.Attributes.SetInt(ModStackAttributes.BrokenCount, brokenCount + extraCount); - } - - internal static void SetRepairedToolStack(this ItemStack itemStack, ItemStack fromStack) - { - itemStack.Attributes.SetItemstack(ModStackAttributes.RepairedToolStack, fromStack); - } - - // Note: On server item stack needs to be resolved! - internal static ItemStack? GetRepairedToolStack(this ItemStack itemStack) - { - return itemStack.Attributes?.GetItemstack(ModStackAttributes.RepairedToolStack); - } - - internal static string? GetRepairSmith(this ItemStack itemStack) - { - var repairedStack = itemStack.GetRepairedToolStack(); - return repairedStack?.GetRepairSmith() ?? itemStack.Attributes.GetString(ModStackAttributes.RepairSmith); - } - - internal static void SetRepairSmith(this ItemStack itemStack, string smith) - { - itemStack.Attributes.SetString(ModStackAttributes.RepairSmith, smith); - } - - internal static float GetSmithingQuality(this ItemStack itemStack) - { - return itemStack.Attributes?.GetFloat(ModStackAttributes.SmithingQuality, 1) ?? 1f; - } - - internal static void SetSmithingQuality(this ItemStack itemStack, float quality) - { - itemStack.Attributes?.SetFloat(ModStackAttributes.SmithingQuality, quality); - } - - internal static float GetToolRepairPenaltyModifier(this ItemStack itemStack) - { - return itemStack.Attributes?.GetFloat(ModStackAttributes.ToolRepairPenaltyModifier) ?? 0f; - } - - internal static void SetToolRepairPenaltyModifier(this ItemStack itemStack, float modifier) - { - itemStack.Attributes?.SetFloat(ModStackAttributes.ToolRepairPenaltyModifier, modifier); - } - - internal static void CloneRepairedToolStackOrAttributes(this ItemStack itemStack, ItemStack fromStack, - string[]? forgettableAttributes = null) +#nullable enable +using System; +using System.Collections.Generic; +using System.Linq; +using SmithingPlus.Common.Metal; +using Vintagestory.API.Common; +using Vintagestory.API.Datastructures; +using Vintagestory.GameContent; + +namespace SmithingPlus.Util; + +public static class ItemStackExtensions +{ + internal static int? GetRemainingDurability(this ItemStack itemStack) + { + return itemStack.Collectible.GetRemainingDurability(itemStack); + } + + internal static int? GetMaxDurability(this ItemStack itemStack) + { + return itemStack.Collectible.GetMaxDurability(itemStack); + } + + internal static float? GetDurabilityPercentage(this ItemStack itemStack) + { + if (itemStack.GetMaxDurability() == 0) + return null; + return itemStack.GetRemainingDurability() / itemStack.GetMaxDurability(); + } + + internal static void SetDurability(this ItemStack itemStack, int number) + { + itemStack.Collectible.SetDurability(itemStack, number); + } + + internal static void CloneBrokenCount(this ItemStack itemStack, ItemStack fromStack, int extraCount = 0) + { + var brokenCount = fromStack.GetBrokenCount(); + itemStack.Attributes.SetInt(ModStackAttributes.BrokenCount, brokenCount + extraCount); + } + + internal static void SetRepairedToolStack(this ItemStack itemStack, ItemStack fromStack) + { + itemStack.Attributes.SetItemstack(ModStackAttributes.RepairedToolStack, fromStack); + } + + // Note: On server item stack needs to be resolved! + internal static ItemStack? GetRepairedToolStack(this ItemStack itemStack) + { + return itemStack.Attributes?.GetItemstack(ModStackAttributes.RepairedToolStack); + } + + internal static string? GetRepairSmith(this ItemStack itemStack) + { + var repairedStack = itemStack.GetRepairedToolStack(); + return repairedStack?.GetRepairSmith() ?? itemStack.Attributes.GetString(ModStackAttributes.RepairSmith); + } + + internal static void SetRepairSmith(this ItemStack itemStack, string smith) + { + itemStack.Attributes.SetString(ModStackAttributes.RepairSmith, smith); + } + + internal static float GetSmithingQuality(this ItemStack itemStack) + { + return itemStack.Attributes?.GetFloat(ModStackAttributes.SmithingQuality, 1) ?? 1f; + } + + internal static void SetSmithingQuality(this ItemStack itemStack, float quality) + { + itemStack.Attributes?.SetFloat(ModStackAttributes.SmithingQuality, quality); + } + + internal static float GetToolRepairPenaltyModifier(this ItemStack itemStack) + { + return itemStack.Attributes?.GetFloat(ModStackAttributes.ToolRepairPenaltyModifier) ?? 0f; + } + + internal static void SetToolRepairPenaltyModifier(this ItemStack itemStack, float modifier) + { + itemStack.Attributes?.SetFloat(ModStackAttributes.ToolRepairPenaltyModifier, modifier); + } + + internal static void CloneRepairedToolStackOrAttributes(this ItemStack itemStack, ItemStack fromStack, + string[]? forgettableAttributes = null) + { + var repairedStack = fromStack.GetRepairedToolStack(); + if (forgettableAttributes != null) + foreach (var attributeKey in forgettableAttributes) + repairedStack?.Attributes?.RemoveAttribute(attributeKey); + if (repairedStack == null) + { + Core.Logger.VerboseDebug("No repaired tool stack found in {0}", fromStack.Collectible.Code); + return; + } + + if (itemStack.Satisfies(repairedStack)) + { + var repairedAttributes = repairedStack.Attributes ?? new TreeAttribute(); + foreach (var attribute in repairedAttributes) itemStack.Attributes[attribute.Key] = attribute.Value; + Core.Logger.VerboseDebug("Not a tool head. Cloned repaired tool stack attributes from {0} to {1}", + fromStack.Collectible.Code, itemStack.Collectible.Code); + } + else + { + itemStack.SetRepairedToolStack(repairedStack); + } + } + + internal static int GetBrokenCount(this ItemStack itemStack) + { + var repairedStack = itemStack.GetRepairedToolStack(); + return repairedStack?.GetBrokenCount() ?? itemStack.Attributes?.GetInt(ModStackAttributes.BrokenCount) ?? 0; + } + + public static bool CodeMatches(this ItemStack stack, ItemStack that) + { + return stack.Collectible.Code.Equals(that.Collectible.Code); + } + + public static float GetWorkableTemperature(this ItemStack stack) + { + var meltingPoint = stack.Collectible.CombustibleProps?.MeltingPoint + ?? stack.GetOrCacheMetalMaterial(Core.Api)?.MetalBitItem?.CombustibleProps?.MeltingPoint + ?? 0f; + var defaultTemperature = meltingPoint / 2f; + return stack.ItemAttributes?["workableTemperature"]?.AsFloat(defaultTemperature) ?? defaultTemperature; + } + + public static SmithingRecipe? GetSmithingRecipe(this ItemStack toolHead, ICoreAPI api) + { + var smithingRecipe = api.ModLoader + .GetModSystem()? + .SmithingRecipes? + .FirstOrDefault(r => r?.Output?.ResolvedItemstack?.Satisfies(toolHead) == true); + return smithingRecipe; + } + + public static SmithingRecipe? GetSmithingRecipe(this ItemStack toolHead, ICoreAPI api, int withOutputStackSize) + { + var smithingRecipe = api.ModLoader + .GetModSystem()? + .SmithingRecipes? + .FirstOrDefault(r => + r?.Output?.ResolvedItemstack?.Satisfies(toolHead) == true + && r.Output.ResolvedItemstack.StackSize == withOutputStackSize); + return smithingRecipe; + } + + // Gets the smithing recipe with the largest output stack that satisfies the tool head + public static SmithingRecipe? GetLargestSmithingRecipe(this ItemStack toolHead, ICoreAPI api) { - var repairedStack = fromStack.GetRepairedToolStack(); - if (forgettableAttributes != null) - foreach (var attributeKey in forgettableAttributes) - repairedStack?.Attributes?.RemoveAttribute(attributeKey); - if (repairedStack == null) + RecipeRegistrySystem? recipeRegistry = api.ModLoader.GetModSystem(); + if (recipeRegistry?.SmithingRecipes == null) { - Core.Logger.VerboseDebug("No repaired tool stack found in {0}", fromStack.Collectible.Code); - return; + return null; } - if (itemStack.Satisfies(repairedStack)) + SmithingRecipe? largestRecipe = null; + int largestOutputStackSize = 0; + foreach (SmithingRecipe? recipe in recipeRegistry.SmithingRecipes) { - var repairedAttributes = repairedStack.Attributes ?? new TreeAttribute(); - foreach (var attribute in repairedAttributes) itemStack.Attributes[attribute.Key] = attribute.Value; - Core.Logger.VerboseDebug("Not a tool head. Cloned repaired tool stack attributes from {0} to {1}", - fromStack.Collectible.Code, itemStack.Collectible.Code); - } - else - { - itemStack.SetRepairedToolStack(repairedStack); - } - } - - internal static int GetBrokenCount(this ItemStack itemStack) - { - var repairedStack = itemStack.GetRepairedToolStack(); - return repairedStack?.GetBrokenCount() ?? itemStack.Attributes?.GetInt(ModStackAttributes.BrokenCount) ?? 0; - } - - public static bool CodeMatches(this ItemStack stack, ItemStack that) - { - return stack.Collectible.Code.Equals(that.Collectible.Code); - } - - public static float GetWorkableTemperature(this ItemStack stack) - { - var meltingPoint = stack.Collectible.CombustibleProps?.MeltingPoint - ?? stack.GetOrCacheMetalMaterial(Core.Api)?.MetalBitItem?.CombustibleProps?.MeltingPoint - ?? 0f; - var defaultTemperature = meltingPoint / 2f; - return stack.ItemAttributes?["workableTemperature"]?.AsFloat(defaultTemperature) ?? defaultTemperature; - } - - public static SmithingRecipe? GetSmithingRecipe(this ItemStack toolHead, ICoreAPI api) - { - var smithingRecipe = api.ModLoader - .GetModSystem()? - .SmithingRecipes? - .FirstOrDefault(r => r?.Output?.ResolvedItemstack?.Satisfies(toolHead) == true); - return smithingRecipe; - } - - public static SmithingRecipe? GetSmithingRecipe(this ItemStack toolHead, ICoreAPI api, int withOutputStackSize) - { - var smithingRecipe = api.ModLoader - .GetModSystem()? - .SmithingRecipes? - .FirstOrDefault(r => - r?.Output?.ResolvedItemstack?.Satisfies(toolHead) == true - && r.Output.ResolvedItemstack.StackSize == withOutputStackSize); - return smithingRecipe; - } + ItemStack? resolvedOutputStack = recipe?.Output?.ResolvedItemstack; + if (resolvedOutputStack == null || !resolvedOutputStack.Satisfies(toolHead)) + { + continue; + } - // Gets the smithing recipe with the largest output stack that satisfies the tool head - public static SmithingRecipe? GetLargestSmithingRecipe(this ItemStack toolHead, ICoreAPI api) - { - var smithingRecipe = api.ModLoader - .GetModSystem()? - .SmithingRecipes? - .Where(r => r?.Output?.ResolvedItemstack?.Satisfies(toolHead) == true) - .OrderByDescending(r => r.Output.ResolvedItemstack.StackSize) - .FirstOrDefault() - ; - return smithingRecipe; - } + if (resolvedOutputStack.StackSize > largestOutputStackSize) + { + largestRecipe = recipe; + largestOutputStackSize = resolvedOutputStack.StackSize; + } + } - // Gets the smithing recipe with the least expensive output that satisfies the tool head + return largestRecipe; + } + + // Gets the smithing recipe with the least expensive output that satisfies the tool head public static SmithingRecipe? GetCheapestSmithingRecipe(this ItemStack toolHead, ICoreAPI api) { - var smithingRecipe = api.ModLoader - .GetModSystem()? - .SmithingRecipes? - .Where(r => r?.Output?.ResolvedItemstack?.Satisfies(toolHead) == true) - .OrderByDescending(r => r.Voxels.VoxelCount() / r.Output.ResolvedItemstack.StackSize) - .FirstOrDefault() - ; - return smithingRecipe; - } - - public static IEnumerable GetGridRecipes(this ItemStack itemStack, ICoreAPI api) - { - var gridRecipes = - from recipe in api.World.GridRecipes - where recipe.Output?.ResolvedItemStack?.Satisfies(itemStack) == true - select recipe; - return gridRecipes; - } - - // Gets a smithing recipe only if the output item stack has a single item - public static SmithingRecipe? GetSingleSmithingRecipe(this ItemStack toolHead, ICoreAPI api) - { - return toolHead.GetSmithingRecipe(api, 1); - } - - public static float GetSplitCount(this ItemStack stack) - { - var splitCount = stack.TempAttributes.GetFloat(ModStackAttributes.SplitCount); - return splitCount; - } - - public static void SetSplitCount(this ItemStack stack, float count) - { - stack.TempAttributes.SetFloat(ModStackAttributes.SplitCount, count); - } - - public static float GetTemperature(this ItemStack stack, IWorldAccessor world) - { - return stack.Collectible.GetTemperature(world, stack); - } - - public static void SetTemperatureFrom(this ItemStack stack, IWorldAccessor world, ItemStack fromStack) - { - var temperature = fromStack.GetTemperature(world); - stack.Collectible.SetTemperature(world, stack, temperature); - } - - public static void SetTemperature(this ItemStack stack, IWorldAccessor world, float count) - { - stack.Collectible.SetTemperature(world, stack, count); - } - - public static bool IsSmeltedContainer(this ItemStack stack) - { - return stack.Collectible is BlockSmeltedContainer; - } - - public static bool IsCastTool(this ItemStack stack) - { - return stack.Attributes.GetBool(ModStackAttributes.CastTool); - } - - /// - /// Get the metal bits that should be recovered when this broken/shattered item is destroyed. - /// Takes into account the item's durability percentage and metal bit ratio. - /// - public static ItemStack? GetShatteredBitsStack(this ItemStack brokenStack, ICoreAPI api) - { - var metalMaterial = brokenStack.GetOrCacheMetalMaterial(api); - if (metalMaterial?.MetalBitStack == null) - return null; - - var voxelsInStack = 0; - // Work item with serialized voxel field - if (brokenStack.Collectible is ItemWorkItem) + RecipeRegistrySystem? recipeRegistry = api.ModLoader.GetModSystem(); + if (recipeRegistry?.SmithingRecipes == null) { - var bytes = brokenStack.Attributes.GetBytes("voxels"); - var voxels = BlockEntityAnvil.deserializeVoxels(bytes); - voxelsInStack = voxels.MaterialCount(); + return null; } - // Finished smithed item -> get via cheapest smithing recipe to prevent abuse of the mechanic - else + + SmithingRecipe? selectedRecipe = null; + int selectedVoxelCost = int.MinValue; + foreach (SmithingRecipe? recipe in recipeRegistry.SmithingRecipes) { - var cheapestRecipe = brokenStack.GetCheapestSmithingRecipe(api); - if (cheapestRecipe is { Output.ResolvedItemStack: not null }) + ItemStack? resolvedOutputStack = recipe?.Output?.ResolvedItemstack; + if (resolvedOutputStack == null || resolvedOutputStack.StackSize <= 0) { - var cheapestOutput = Math.Max(cheapestRecipe.Output.ResolvedItemStack.StackSize, 1); - var recipeMaterialVoxels = cheapestRecipe.Voxels.VoxelCount(); - var voxelsPerItem = Math.Max(recipeMaterialVoxels / cheapestOutput, 0); - voxelsInStack = voxelsPerItem * brokenStack.StackSize; + continue; } - } - var durabilityPercentage = brokenStack.GetDurabilityPercentage() ?? 1f; - var reducedVoxels = voxelsInStack * durabilityPercentage; - var recoveredBits = (int)MathF.Floor(reducedVoxels / Core.Config.VoxelsPerBit); + if (!resolvedOutputStack.Satisfies(toolHead)) + { + continue; + } - if (recoveredBits <= 0) - return null; + int voxelCost = recipe.Voxels.VoxelCount() / resolvedOutputStack.StackSize; + if (voxelCost > selectedVoxelCost) + { + selectedRecipe = recipe; + selectedVoxelCost = voxelCost; + } + } - var bitsStack = metalMaterial.MetalBitStack.Clone(); - bitsStack.StackSize = recoveredBits; - return bitsStack; - } -} \ No newline at end of file + return selectedRecipe; + } + + public static IEnumerable GetGridRecipes(this ItemStack itemStack, ICoreAPI api) + { + var gridRecipes = + from recipe in api.World.GridRecipes + where recipe.Output?.ResolvedItemStack?.Satisfies(itemStack) == true + select recipe; + return gridRecipes; + } + + // Gets a smithing recipe only if the output item stack has a single item + public static SmithingRecipe? GetSingleSmithingRecipe(this ItemStack toolHead, ICoreAPI api) + { + return toolHead.GetSmithingRecipe(api, 1); + } + + public static float GetSplitCount(this ItemStack stack) + { + var splitCount = stack.TempAttributes.GetFloat(ModStackAttributes.SplitCount); + return splitCount; + } + + public static void SetSplitCount(this ItemStack stack, float count) + { + stack.TempAttributes.SetFloat(ModStackAttributes.SplitCount, count); + } + + public static float GetTemperature(this ItemStack stack, IWorldAccessor world) + { + return stack.Collectible.GetTemperature(world, stack); + } + + public static void SetTemperatureFrom(this ItemStack stack, IWorldAccessor world, ItemStack fromStack) + { + var temperature = fromStack.GetTemperature(world); + stack.Collectible.SetTemperature(world, stack, temperature); + } + + public static void SetTemperature(this ItemStack stack, IWorldAccessor world, float count) + { + stack.Collectible.SetTemperature(world, stack, count); + } + + public static bool IsSmeltedContainer(this ItemStack stack) + { + return stack.Collectible is BlockSmeltedContainer; + } + + public static bool IsCastTool(this ItemStack stack) + { + return stack.Attributes.GetBool(ModStackAttributes.CastTool); + } + + /// + /// Get the metal bits that should be recovered when this broken/shattered item is destroyed. + /// Takes into account the item's durability percentage and metal bit ratio. + /// + public static ItemStack? GetShatteredBitsStack(this ItemStack brokenStack, ICoreAPI api) + { + var metalMaterial = brokenStack.GetOrCacheMetalMaterial(api); + if (metalMaterial?.MetalBitStack == null) + return null; + + var voxelsInStack = 0; + // Work item with serialized voxel field + if (brokenStack.Collectible is ItemWorkItem) + { + var bytes = brokenStack.Attributes.GetBytes("voxels"); + var voxels = BlockEntityAnvil.deserializeVoxels(bytes); + voxelsInStack = voxels.MaterialCount(); + } + // Finished smithed item -> get via cheapest smithing recipe to prevent abuse of the mechanic + else + { + var cheapestRecipe = brokenStack.GetCheapestSmithingRecipe(api); + if (cheapestRecipe is { Output.ResolvedItemStack: not null }) + { + var cheapestOutput = Math.Max(cheapestRecipe.Output.ResolvedItemStack.StackSize, 1); + var recipeMaterialVoxels = cheapestRecipe.Voxels.VoxelCount(); + var voxelsPerItem = Math.Max(recipeMaterialVoxels / cheapestOutput, 0); + voxelsInStack = voxelsPerItem * brokenStack.StackSize; + } + } + + var durabilityPercentage = brokenStack.GetDurabilityPercentage() ?? 1f; + var reducedVoxels = voxelsInStack * durabilityPercentage; + var recoveredBits = (int)MathF.Floor(reducedVoxels / Core.Config.VoxelsPerBit); + + if (recoveredBits <= 0) + return null; + + var bitsStack = metalMaterial.MetalBitStack.Clone(); + bitsStack.StackSize = recoveredBits; + return bitsStack; + } +} From 4e5c63da47143e3a5112225e779c075086526861 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C6=9B=CA=91=CA=8B=C9=8D=C9=9B=CF=AF=E1=BE=B0=C9=A8?= <6170786+AzureTai@users.noreply.github.com> Date: Wed, 19 Aug 2026 04:30:38 +0100 Subject: [PATCH 06/13] Normalize modified source line endings Restore repository-standard LF line endings in the warning-cleanup files so subsequent review shows only substantive source changes. --- .../CollectibleBehaviorCastToolHead.cs | 234 ++++---- .../CastingTweaks/ToolMoldUnitsPatch.cs | 222 ++++---- .../CollectibleBehaviorAnvilWorkable.cs | 382 +++++++------- .../CollectibleBehaviorJsonAnvilWorkable.cs | 254 ++++----- SmithingPlus/Common/Metal/MetalMaterial.cs | 158 +++--- .../CollectibleBehaviorRepairableTool.cs | 52 +- SmithingPlus/Util/CollectibleExtensions.cs | 318 +++++------ SmithingPlus/Util/ItemStackExtensions.cs | 498 +++++++++--------- 8 files changed, 1059 insertions(+), 1059 deletions(-) diff --git a/SmithingPlus/CastingTweaks/CollectibleBehaviorCastToolHead.cs b/SmithingPlus/CastingTweaks/CollectibleBehaviorCastToolHead.cs index c21edfe..74b2dd7 100644 --- a/SmithingPlus/CastingTweaks/CollectibleBehaviorCastToolHead.cs +++ b/SmithingPlus/CastingTweaks/CollectibleBehaviorCastToolHead.cs @@ -1,95 +1,95 @@ -#nullable enable -using System; -using System.Collections.Generic; -using System.Diagnostics; -using System.Text; -using SmithingPlus.Common.Metal; -using SmithingPlus.Metal; -using SmithingPlus.Util; -using Vintagestory.API.Common; -using Vintagestory.API.Config; -using Vintagestory.GameContent; - -namespace SmithingPlus.CastingTweaks; - -public class CollectibleBehaviorCastToolHead(CollectibleObject collObj) : CollectibleBehavior(collObj), IAnvilWorkable -{ - private ICoreAPI Api => collObj.GetField("api"); - - public int GetRequiredAnvilTier(ItemStack stack) - { - return stack.GetOrCacheMetalMaterial(Api)?.Tier ?? 0; - } - - public List GetMatchingRecipes(ItemStack stack) - { - var smithingRecipe = stack.GetSmithingRecipe(Api); - return smithingRecipe != null ? [smithingRecipe] : []; - } - - public bool CanWork(ItemStack stack) - { - if (!stack.IsCastTool()) - return false; - var temperature = stack.Collectible.GetTemperature(Api.World, stack); - var threshold = GetWorkableTemperature(stack); - Core.Logger.VerboseDebug( - $"[CollectibleBehaviorCastToolHead#CanWork] {stack.Collectible.Code} - Temperature: {temperature}, Threshold: {threshold}"); - return temperature >= threshold; - } - - public ItemStack? TryPlaceOn(ItemStack stack, BlockEntityAnvil beAnvil) - { - if (beAnvil.WorkItemStack != null || !CanWork(stack)) - return null; - var recipe = stack.GetSingleSmithingRecipe(Api); - var durabilityPercent = stack.GetDurabilityPercentage(); - if (recipe == null || durabilityPercent == null) return null; - var voxels = recipe.Voxels.ErodeToPercentage(durabilityPercent.Value); - var world = beAnvil.Api.World; - var random = world.Rand; - var slagCount = (int)Math.Ceiling(0.2f * voxels.MaterialCount()); - voxels.AddSlag(slagCount, random); - var workItemStack = stack.GetOrCacheMetalMaterial(beAnvil.Api)?.WorkItemStack; - if (workItemStack == null) - return null; - beAnvil.Voxels = voxels; - beAnvil.SelectedRecipeId = recipe.RecipeId; - var temperature = stack.Collectible.GetTemperature(world, stack); - workItemStack.Collectible.SetTemperature(world, workItemStack, temperature); - return workItemStack; - } - - public ItemStack? GetBaseMaterial(ItemStack stack) - { - var metalMaterial = stack.GetOrCacheMetalMaterial(Api); - Debug.Write( - $"[CollectibleBehaviorCastToolHead#GetBaseMaterial] {stack.Collectible.Code} -> {metalMaterial?.IngotCode}"); - return metalMaterial?.IngotStack; - } - - public EnumHelveWorkableMode GetHelveWorkableMode(ItemStack stack, BlockEntityAnvil beAnvil) - { - return EnumHelveWorkableMode.TestSufficientVoxelsWorkable; - } - - public int VoxelCountForHandbook(ItemStack stack) - { - var recipe = stack.GetSingleSmithingRecipe(Api); - var voxels = recipe?.Voxels.ToByteArray(); - return voxels?.MaterialCount() ?? 0; - } - - public override void GetHeldItemName(StringBuilder dsc, ItemStack itemStack) - { - base.GetHeldItemName(dsc, itemStack); - if (!itemStack.IsCastTool()) - return; - var toolName = dsc.ToString(); - dsc.Clear(); - dsc.AppendLine(Lang.Get("Cast {0}", toolName.ToLower())); - } - +#nullable enable +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Text; +using SmithingPlus.Common.Metal; +using SmithingPlus.Metal; +using SmithingPlus.Util; +using Vintagestory.API.Common; +using Vintagestory.API.Config; +using Vintagestory.GameContent; + +namespace SmithingPlus.CastingTweaks; + +public class CollectibleBehaviorCastToolHead(CollectibleObject collObj) : CollectibleBehavior(collObj), IAnvilWorkable +{ + private ICoreAPI Api => collObj.GetField("api"); + + public int GetRequiredAnvilTier(ItemStack stack) + { + return stack.GetOrCacheMetalMaterial(Api)?.Tier ?? 0; + } + + public List GetMatchingRecipes(ItemStack stack) + { + var smithingRecipe = stack.GetSmithingRecipe(Api); + return smithingRecipe != null ? [smithingRecipe] : []; + } + + public bool CanWork(ItemStack stack) + { + if (!stack.IsCastTool()) + return false; + var temperature = stack.Collectible.GetTemperature(Api.World, stack); + var threshold = GetWorkableTemperature(stack); + Core.Logger.VerboseDebug( + $"[CollectibleBehaviorCastToolHead#CanWork] {stack.Collectible.Code} - Temperature: {temperature}, Threshold: {threshold}"); + return temperature >= threshold; + } + + public ItemStack? TryPlaceOn(ItemStack stack, BlockEntityAnvil beAnvil) + { + if (beAnvil.WorkItemStack != null || !CanWork(stack)) + return null; + var recipe = stack.GetSingleSmithingRecipe(Api); + var durabilityPercent = stack.GetDurabilityPercentage(); + if (recipe == null || durabilityPercent == null) return null; + var voxels = recipe.Voxels.ErodeToPercentage(durabilityPercent.Value); + var world = beAnvil.Api.World; + var random = world.Rand; + var slagCount = (int)Math.Ceiling(0.2f * voxels.MaterialCount()); + voxels.AddSlag(slagCount, random); + var workItemStack = stack.GetOrCacheMetalMaterial(beAnvil.Api)?.WorkItemStack; + if (workItemStack == null) + return null; + beAnvil.Voxels = voxels; + beAnvil.SelectedRecipeId = recipe.RecipeId; + var temperature = stack.Collectible.GetTemperature(world, stack); + workItemStack.Collectible.SetTemperature(world, workItemStack, temperature); + return workItemStack; + } + + public ItemStack? GetBaseMaterial(ItemStack stack) + { + var metalMaterial = stack.GetOrCacheMetalMaterial(Api); + Debug.Write( + $"[CollectibleBehaviorCastToolHead#GetBaseMaterial] {stack.Collectible.Code} -> {metalMaterial?.IngotCode}"); + return metalMaterial?.IngotStack; + } + + public EnumHelveWorkableMode GetHelveWorkableMode(ItemStack stack, BlockEntityAnvil beAnvil) + { + return EnumHelveWorkableMode.TestSufficientVoxelsWorkable; + } + + public int VoxelCountForHandbook(ItemStack stack) + { + var recipe = stack.GetSingleSmithingRecipe(Api); + var voxels = recipe?.Voxels.ToByteArray(); + return voxels?.MaterialCount() ?? 0; + } + + public override void GetHeldItemName(StringBuilder dsc, ItemStack itemStack) + { + base.GetHeldItemName(dsc, itemStack); + if (!itemStack.IsCastTool()) + return; + var toolName = dsc.ToString(); + dsc.Clear(); + dsc.AppendLine(Lang.Get("Cast {0}", toolName.ToLower())); + } + public override void GetHeldItemInfo(ItemSlot inSlot, StringBuilder dsc, IWorldAccessor world, bool withDebugInfo) { base.GetHeldItemInfo(inSlot, dsc, world, withDebugInfo); @@ -104,29 +104,29 @@ public override void GetHeldItemInfo(ItemSlot inSlot, StringBuilder dsc, IWorldA dsc.AppendLine(Lang.Get($"{Core.ModId}:itemdesc-needsrefining")); var workableTemp = GetWorkableTemperature(itemStack); var temperature = itemStack.Collectible.GetTemperature(world, itemStack); - dsc.AppendLine(Lang.Get("Workable Temperature: {0}", - workableTemp > 0 - ? temperature > workableTemp - ? $"{Math.Round(workableTemp)}\u00B0C" - : $"{Math.Round(workableTemp)}\u00B0C" - : Lang.Get($"{Core.ModId}:itemdesc-temp-always"))); - } - - private float GetWorkableTemperature(ItemStack itemStack) - { - var metalIngot = itemStack.GetOrCacheMetalMaterial(Api)?.IngotItem; - var querySlot = new DummySlot(itemStack); - - var meltingPoint = metalIngot? - .GetMeltingPoint(Api.World, null, querySlot) - ?? itemStack.Collectible - .GetMeltingPoint(Api.World, null, querySlot); - - var defaultWorkableTemp = meltingPoint / 2f; - var workableAttr = metalIngot?.Attributes?["workableTemperature"]; - - return workableAttr?.Exists == true - ? workableAttr.AsFloat(defaultWorkableTemp) - : defaultWorkableTemp; - } + dsc.AppendLine(Lang.Get("Workable Temperature: {0}", + workableTemp > 0 + ? temperature > workableTemp + ? $"{Math.Round(workableTemp)}\u00B0C" + : $"{Math.Round(workableTemp)}\u00B0C" + : Lang.Get($"{Core.ModId}:itemdesc-temp-always"))); + } + + private float GetWorkableTemperature(ItemStack itemStack) + { + var metalIngot = itemStack.GetOrCacheMetalMaterial(Api)?.IngotItem; + var querySlot = new DummySlot(itemStack); + + var meltingPoint = metalIngot? + .GetMeltingPoint(Api.World, null, querySlot) + ?? itemStack.Collectible + .GetMeltingPoint(Api.World, null, querySlot); + + var defaultWorkableTemp = meltingPoint / 2f; + var workableAttr = metalIngot?.Attributes?["workableTemperature"]; + + return workableAttr?.Exists == true + ? workableAttr.AsFloat(defaultWorkableTemp) + : defaultWorkableTemp; + } } diff --git a/SmithingPlus/CastingTweaks/ToolMoldUnitsPatch.cs b/SmithingPlus/CastingTweaks/ToolMoldUnitsPatch.cs index 7616b46..3764ca5 100644 --- a/SmithingPlus/CastingTweaks/ToolMoldUnitsPatch.cs +++ b/SmithingPlus/CastingTweaks/ToolMoldUnitsPatch.cs @@ -1,67 +1,67 @@ -#nullable enable -using System; -using System.Collections.Generic; -using System.Linq; -using HarmonyLib; -using JetBrains.Annotations; -using Newtonsoft.Json; -using SmithingPlus.Util; -using Vintagestory.API.Common; -using Vintagestory.API.Datastructures; -using Vintagestory.GameContent; - -namespace SmithingPlus.CastingTweaks; - -[HarmonyPatchCategory(Core.DynamicMoldsCategory)] -[UsedImplicitly(ImplicitUseTargetFlags.WithMembers)] -[HarmonyPatch(typeof(BlockEntityToolMold))] -[HarmonyPriority(Priority.Last)] -public class ToolMoldUnitsPatch -{ - [HarmonyPostfix] - [HarmonyPatch(nameof(BlockEntityToolMold.Initialize))] - public static void Initialize_Postfix(BlockEntityToolMold __instance, ref int ___requiredUnits, ICoreAPI api) - { - // Assume copper stack as metal for unit calculation, - // This means the patch will only apply for standard molds! - // Either vanilla or vanilla-like ones - // It also means that having different smithing recipes - // for different metals will cause unexpected imbalances - var copperIngot = api.World.GetItem(new AssetLocation("game:ingot-copper")); - if (copperIngot == null) - return; - var copperStack = new ItemStack(copperIngot); - var requiredUnitsRounded = GetPatchedRequiredUnits(__instance.Api, __instance.Block, copperStack); - if (requiredUnitsRounded == 0) - return; - ___requiredUnits = requiredUnitsRounded; - } - - public static int GetPatchedRequiredUnits(ICoreAPI api, Block toolMold, ItemStack fromMetal) - { - var dropStacks = GetMoldedStacksStatic(api, toolMold, fromMetal); - if (dropStacks.Length == 0) - return - toolMold.Attributes["requiredUnits"] - .AsInt(); // <-- Patch will only apply for molds that work for copper! - var voxelCount = VoxelCountForStacks(api, dropStacks); - if (voxelCount is null or 0) - return toolMold.Attributes["requiredUnits"].AsInt(); - // These are all assumptions that have to be made, should implement warnings if weird values are found - const float voxelsPerIngot = 42f; - const float unitsPerIngot = 100f; - const float unitsPerVoxel = unitsPerIngot / voxelsPerIngot; - // Round to lowest 5 units to avoid annoying numbers and making players sad - return (int)MathF.Floor(voxelCount.Value * unitsPerVoxel / 5) * 5; - } - - public static int? VoxelCountForStacks(ICoreAPI api, ItemStack[] smithedItemStacks) - { - var voxelCounts = smithedItemStacks.Select(stack => - VoxelCountForStack(api, stack)).ToArray(); - return voxelCounts.All(count => count == null) ? null : voxelCounts.Sum(count => count ?? 0); - } - +#nullable enable +using System; +using System.Collections.Generic; +using System.Linq; +using HarmonyLib; +using JetBrains.Annotations; +using Newtonsoft.Json; +using SmithingPlus.Util; +using Vintagestory.API.Common; +using Vintagestory.API.Datastructures; +using Vintagestory.GameContent; + +namespace SmithingPlus.CastingTweaks; + +[HarmonyPatchCategory(Core.DynamicMoldsCategory)] +[UsedImplicitly(ImplicitUseTargetFlags.WithMembers)] +[HarmonyPatch(typeof(BlockEntityToolMold))] +[HarmonyPriority(Priority.Last)] +public class ToolMoldUnitsPatch +{ + [HarmonyPostfix] + [HarmonyPatch(nameof(BlockEntityToolMold.Initialize))] + public static void Initialize_Postfix(BlockEntityToolMold __instance, ref int ___requiredUnits, ICoreAPI api) + { + // Assume copper stack as metal for unit calculation, + // This means the patch will only apply for standard molds! + // Either vanilla or vanilla-like ones + // It also means that having different smithing recipes + // for different metals will cause unexpected imbalances + var copperIngot = api.World.GetItem(new AssetLocation("game:ingot-copper")); + if (copperIngot == null) + return; + var copperStack = new ItemStack(copperIngot); + var requiredUnitsRounded = GetPatchedRequiredUnits(__instance.Api, __instance.Block, copperStack); + if (requiredUnitsRounded == 0) + return; + ___requiredUnits = requiredUnitsRounded; + } + + public static int GetPatchedRequiredUnits(ICoreAPI api, Block toolMold, ItemStack fromMetal) + { + var dropStacks = GetMoldedStacksStatic(api, toolMold, fromMetal); + if (dropStacks.Length == 0) + return + toolMold.Attributes["requiredUnits"] + .AsInt(); // <-- Patch will only apply for molds that work for copper! + var voxelCount = VoxelCountForStacks(api, dropStacks); + if (voxelCount is null or 0) + return toolMold.Attributes["requiredUnits"].AsInt(); + // These are all assumptions that have to be made, should implement warnings if weird values are found + const float voxelsPerIngot = 42f; + const float unitsPerIngot = 100f; + const float unitsPerVoxel = unitsPerIngot / voxelsPerIngot; + // Round to lowest 5 units to avoid annoying numbers and making players sad + return (int)MathF.Floor(voxelCount.Value * unitsPerVoxel / 5) * 5; + } + + public static int? VoxelCountForStacks(ICoreAPI api, ItemStack[] smithedItemStacks) + { + var voxelCounts = smithedItemStacks.Select(stack => + VoxelCountForStack(api, stack)).ToArray(); + return voxelCounts.All(count => count == null) ? null : voxelCounts.Sum(count => count ?? 0); + } + private static int? VoxelCountForStack(ICoreAPI api, ItemStack stack) { SmithingRecipe? cheapestRecipe = stack.GetCheapestSmithingRecipe(api); @@ -81,51 +81,51 @@ public static int GetPatchedRequiredUnits(ICoreAPI api, Block toolMold, ItemStac int voxelsPerItem = Math.Max(recipeMaterialVoxels / resolvedOutputStack.StackSize, 0); return voxelsPerItem * stack.StackSize; } - - private static ItemStack[] GetMoldedStacksStatic(ICoreAPI api, Block toolMold, ItemStack fromMetal) - { - // why try catch? vanilla code does this... - try - { - if (toolMold.Attributes["drop"].Exists) - { - var jStack = -#pragma warning disable CS8625 // Cannot convert null literal to non-nullable reference type. - toolMold.Attributes["drop"].AsObject(null, toolMold.Code.Domain); -#pragma warning restore CS8625 // Cannot convert null literal to non-nullable reference type. - if (jStack == null) - return []; - var itemStack = MoldOutputStackFromCode(jStack, api, toolMold, fromMetal); - return itemStack == null ? [] : [itemStack]; - } - - var jsonItemStackArray = - toolMold.Attributes["drops"].AsObject([], toolMold.Code.Domain); - var itemStackList = new List(); - foreach (var jStack in jsonItemStackArray) - { - var itemStack = MoldOutputStackFromCode(jStack, api, toolMold, fromMetal); - if (itemStack != null) - itemStackList.Add(itemStack); - } - - return itemStackList.ToArray(); - } - catch (JsonReaderException ex) - { - api.World.Logger.Error("Failed getting molded stacks from tool mold of block {0}, " + - "probably unable to parse drop or drops attribute", toolMold.Code); - api.World.Logger.Error(ex); - throw; - } - } - - private static ItemStack? MoldOutputStackFromCode(JsonItemStack jstack, ICoreAPI api, Block toolMold, - ItemStack fromMetal) - { - var newValue = fromMetal.Collectible.LastCodePart(); - jstack.Code.Path = jstack.Code.Path.Replace("{metal}", newValue); - jstack.Resolve(api.World, "tool mold drop for " + toolMold.Code); - return jstack.ResolvedItemstack; - } + + private static ItemStack[] GetMoldedStacksStatic(ICoreAPI api, Block toolMold, ItemStack fromMetal) + { + // why try catch? vanilla code does this... + try + { + if (toolMold.Attributes["drop"].Exists) + { + var jStack = +#pragma warning disable CS8625 // Cannot convert null literal to non-nullable reference type. + toolMold.Attributes["drop"].AsObject(null, toolMold.Code.Domain); +#pragma warning restore CS8625 // Cannot convert null literal to non-nullable reference type. + if (jStack == null) + return []; + var itemStack = MoldOutputStackFromCode(jStack, api, toolMold, fromMetal); + return itemStack == null ? [] : [itemStack]; + } + + var jsonItemStackArray = + toolMold.Attributes["drops"].AsObject([], toolMold.Code.Domain); + var itemStackList = new List(); + foreach (var jStack in jsonItemStackArray) + { + var itemStack = MoldOutputStackFromCode(jStack, api, toolMold, fromMetal); + if (itemStack != null) + itemStackList.Add(itemStack); + } + + return itemStackList.ToArray(); + } + catch (JsonReaderException ex) + { + api.World.Logger.Error("Failed getting molded stacks from tool mold of block {0}, " + + "probably unable to parse drop or drops attribute", toolMold.Code); + api.World.Logger.Error(ex); + throw; + } + } + + private static ItemStack? MoldOutputStackFromCode(JsonItemStack jstack, ICoreAPI api, Block toolMold, + ItemStack fromMetal) + { + var newValue = fromMetal.Collectible.LastCodePart(); + jstack.Code.Path = jstack.Code.Path.Replace("{metal}", newValue); + jstack.Resolve(api.World, "tool mold drop for " + toolMold.Code); + return jstack.ResolvedItemstack; + } } diff --git a/SmithingPlus/Common/CollectibleBehaviorAnvilWorkable.cs b/SmithingPlus/Common/CollectibleBehaviorAnvilWorkable.cs index 396c027..6a9723c 100644 --- a/SmithingPlus/Common/CollectibleBehaviorAnvilWorkable.cs +++ b/SmithingPlus/Common/CollectibleBehaviorAnvilWorkable.cs @@ -1,89 +1,89 @@ -#nullable enable -using System; -using System.Collections.Generic; -using System.Linq; -using SmithingPlus.Common.Metal; -using SmithingPlus.Metal; -using SmithingPlus.Util; -using Vintagestory.API.Client; -using Vintagestory.API.Common; -using Vintagestory.API.Config; -using Vintagestory.GameContent; - -namespace SmithingPlus.Common; - -public abstract class CollectibleBehaviorAnvilWorkable(CollectibleObject collObj) : - CollectibleBehavior(collObj), IAnvilWorkable -{ - protected ICoreAPI? Api => collObj.GetField("api"); - protected abstract byte[,,] Voxels { get; } - - protected MetalMaterial? MetalMaterial => - Api != null - ? MetalMaterialLoader.GetMaterial(Api, collObj.GetMetalVariant()) ?? collObj.GetMetalMaterialSmelted(Api) - : null; - - protected virtual AnvilPlacementMode PlacementMode { get; set; } = AnvilPlacementMode.Normal; - - public virtual ItemStack? TryPlaceOn(ItemStack stack, BlockEntityAnvil beAnvil) - { - if (Api == null || !CanWork(stack) || beAnvil is { WorkItemStack: not null, CanWorkCurrent: false }) - return null; - var workItemStack = MetalMaterial?.WorkItemStack; - if (workItemStack == null) - return null; - var sourceTemp = stack.GetTemperature(Api.World); - workItemStack.SetTemperature(Api.World, sourceTemp); - - if (beAnvil.WorkItemStack == null && PlacementMode.AllowsEmpty()) - { - TryAddVoxelsFromWorkable(Api, ref beAnvil.Voxels); - return workItemStack; - } - - if (!PlacementMode.AllowsPresent()) - return null; - - var workItemMaterial = beAnvil.WorkItemStack?.GetMetalMaterialProcessed(Api); - Core.Logger.VerboseDebug( - $"[{nameof(CollectibleBehaviorAnvilWorkable)}#{nameof(TryPlaceOn)}] base material: {MetalMaterial?.IngotCode}, " + - $"workItem base material: {workItemMaterial?.IngotCode}"); - - if (workItemMaterial == null || !workItemMaterial.Equals(MetalMaterial)) - { - (Api as ICoreClientAPI)?.TriggerIngameError(this, "notequal", - Lang.Get("Must be the same metal to add voxels")); - return null; - } - - var didSucceed = TryAddVoxelsFromWorkable(Api, ref beAnvil.Voxels); - if (didSucceed) return workItemStack; - - (Api as ICoreClientAPI)?.TriggerIngameError(this, "requireshammering", - Lang.Get("Try hammering down before adding additional voxels")); - return null; - } - - public virtual bool CanWork(ItemStack stack) - { - var temperature = stack.Collectible.GetTemperature(Api?.World, stack); - var meltingPoint = stack.Collectible.GetMeltingPoint(Api?.World, null, new DummySlot(stack)); - if (stack.ItemAttributes?["workableTemperature"].Exists == true) - return stack.ItemAttributes["workableTemperature"].AsFloat(meltingPoint / 2) <= temperature; - return temperature >= meltingPoint / 2; - } - - public virtual int GetRequiredAnvilTier(ItemStack stack) - { - var defaultValue = 0; - if (MetalMaterial != null) - defaultValue = MetalMaterial.Tier - 1; - var attributes = stack.Collectible.Attributes; - if ((attributes != null ? attributes["requiresAnvilTier"].Exists ? 1 : 0 : 0) != 0) - defaultValue = stack.Collectible.Attributes["requiresAnvilTier"].AsInt(defaultValue); - return defaultValue; - } - +#nullable enable +using System; +using System.Collections.Generic; +using System.Linq; +using SmithingPlus.Common.Metal; +using SmithingPlus.Metal; +using SmithingPlus.Util; +using Vintagestory.API.Client; +using Vintagestory.API.Common; +using Vintagestory.API.Config; +using Vintagestory.GameContent; + +namespace SmithingPlus.Common; + +public abstract class CollectibleBehaviorAnvilWorkable(CollectibleObject collObj) : + CollectibleBehavior(collObj), IAnvilWorkable +{ + protected ICoreAPI? Api => collObj.GetField("api"); + protected abstract byte[,,] Voxels { get; } + + protected MetalMaterial? MetalMaterial => + Api != null + ? MetalMaterialLoader.GetMaterial(Api, collObj.GetMetalVariant()) ?? collObj.GetMetalMaterialSmelted(Api) + : null; + + protected virtual AnvilPlacementMode PlacementMode { get; set; } = AnvilPlacementMode.Normal; + + public virtual ItemStack? TryPlaceOn(ItemStack stack, BlockEntityAnvil beAnvil) + { + if (Api == null || !CanWork(stack) || beAnvil is { WorkItemStack: not null, CanWorkCurrent: false }) + return null; + var workItemStack = MetalMaterial?.WorkItemStack; + if (workItemStack == null) + return null; + var sourceTemp = stack.GetTemperature(Api.World); + workItemStack.SetTemperature(Api.World, sourceTemp); + + if (beAnvil.WorkItemStack == null && PlacementMode.AllowsEmpty()) + { + TryAddVoxelsFromWorkable(Api, ref beAnvil.Voxels); + return workItemStack; + } + + if (!PlacementMode.AllowsPresent()) + return null; + + var workItemMaterial = beAnvil.WorkItemStack?.GetMetalMaterialProcessed(Api); + Core.Logger.VerboseDebug( + $"[{nameof(CollectibleBehaviorAnvilWorkable)}#{nameof(TryPlaceOn)}] base material: {MetalMaterial?.IngotCode}, " + + $"workItem base material: {workItemMaterial?.IngotCode}"); + + if (workItemMaterial == null || !workItemMaterial.Equals(MetalMaterial)) + { + (Api as ICoreClientAPI)?.TriggerIngameError(this, "notequal", + Lang.Get("Must be the same metal to add voxels")); + return null; + } + + var didSucceed = TryAddVoxelsFromWorkable(Api, ref beAnvil.Voxels); + if (didSucceed) return workItemStack; + + (Api as ICoreClientAPI)?.TriggerIngameError(this, "requireshammering", + Lang.Get("Try hammering down before adding additional voxels")); + return null; + } + + public virtual bool CanWork(ItemStack stack) + { + var temperature = stack.Collectible.GetTemperature(Api?.World, stack); + var meltingPoint = stack.Collectible.GetMeltingPoint(Api?.World, null, new DummySlot(stack)); + if (stack.ItemAttributes?["workableTemperature"].Exists == true) + return stack.ItemAttributes["workableTemperature"].AsFloat(meltingPoint / 2) <= temperature; + return temperature >= meltingPoint / 2; + } + + public virtual int GetRequiredAnvilTier(ItemStack stack) + { + var defaultValue = 0; + if (MetalMaterial != null) + defaultValue = MetalMaterial.Tier - 1; + var attributes = stack.Collectible.Attributes; + if ((attributes != null ? attributes["requiresAnvilTier"].Exists ? 1 : 0 : 0) != 0) + defaultValue = stack.Collectible.Attributes["requiresAnvilTier"].AsInt(defaultValue); + return defaultValue; + } + public virtual List GetMatchingRecipes(ItemStack stack) { ICoreAPI? api = Api; @@ -147,109 +147,109 @@ private static int CompareMatchingRecipes(SmithingRecipe leftRecipe, SmithingRec int rightStackSize = rightOutputStack?.StackSize ?? 0; return leftStackSize.CompareTo(rightStackSize); } - - public virtual ItemStack? GetBaseMaterial(ItemStack stack) - { - return MetalMaterial?.IngotStack; - } - - public virtual EnumHelveWorkableMode GetHelveWorkableMode(ItemStack stack, BlockEntityAnvil beAnvil) - { - return EnumHelveWorkableMode.NotWorkable; - } - - public virtual int VoxelCountForHandbook(ItemStack stack) - { - return Voxels.MaterialCount(); - } - - public override void OnLoaded(ICoreAPI api) - { - base.OnLoaded(api); - if (Api == null) - { - Core.Logger?.Error( - "[CollectibleBehaviorAnvilWorkable] Reflection failed: field 'api' in collectible class is null."); - return; - } - - if (Voxels.MaterialCount() == 0) - Api?.Logger.Error("CollectibleBehaviorAnvilWorkable for {0} has no voxels defined. " + - "Please check the 'voxels' attribute in the item JSON.", collObj.Code); - } - - protected virtual bool TryAddVoxelsFromWorkable(ICoreAPI api, ref byte[,,] beAnvilVoxels) - { - if (Voxels.MaterialCount() == 0) - return false; - - // Target (anvil) dims - var tx = beAnvilVoxels.GetLength(0); - var ty = beAnvilVoxels.GetLength(1); - var tz = beAnvilVoxels.GetLength(2); - - // Source (workable) dims (up to 16x16x6 typically) - var sx = Voxels.GetLength(0); - var sy = Voxels.GetLength(1); - var sz = Voxels.GetLength(2); - - var ox = Math.Min(tx, sx); - var oz = Math.Min(tz, sz); - - // Work on a copy so we can roll back on failure - var voxelsCopy = (byte[,,])beAnvilVoxels.Clone(); - - for (var x = 0; x < ox; x++) - for (var z = 0; z < oz; z++) - { - // Find first empty Y in the target column - var y = 0; - while (y < ty && voxelsCopy[x, y, z] != 0) y++; - - // If column is full, fail (matches your early-return behavior) - if (y >= ty) return false; - - // Place the source column starting from ny = 0 upward - for (var ny = 0; ny < sy; ny++) - { - var val = Voxels[x, ny, z]; - if (val == 0) continue; - - var tyIndex = y + ny; - if (tyIndex >= ty) - // Would overflow this column — abort entirely - return false; - - voxelsCopy[x, tyIndex, z] = val; - if (val == 1) - { - } - } - } - - // All columns fit — commit the copy - beAnvilVoxels = voxelsCopy; - return true; - } -} - -public enum AnvilPlacementMode -{ - None = -1, // No placement (why would you want this? ;p) - Normal = 0, // Can be placed both on a workitem and on an empty anvil - Empty = 1, // Cannot be placed when workitem is present - Present = 2 // Cannot be placed when workitem is missing -} - -public static class PlacementModeExtensions -{ - public static bool AllowsPresent(this AnvilPlacementMode mode) - { - return mode is AnvilPlacementMode.Normal or AnvilPlacementMode.Present; - } - - public static bool AllowsEmpty(this AnvilPlacementMode mode) - { - return mode is AnvilPlacementMode.Normal or AnvilPlacementMode.Empty; - } + + public virtual ItemStack? GetBaseMaterial(ItemStack stack) + { + return MetalMaterial?.IngotStack; + } + + public virtual EnumHelveWorkableMode GetHelveWorkableMode(ItemStack stack, BlockEntityAnvil beAnvil) + { + return EnumHelveWorkableMode.NotWorkable; + } + + public virtual int VoxelCountForHandbook(ItemStack stack) + { + return Voxels.MaterialCount(); + } + + public override void OnLoaded(ICoreAPI api) + { + base.OnLoaded(api); + if (Api == null) + { + Core.Logger?.Error( + "[CollectibleBehaviorAnvilWorkable] Reflection failed: field 'api' in collectible class is null."); + return; + } + + if (Voxels.MaterialCount() == 0) + Api?.Logger.Error("CollectibleBehaviorAnvilWorkable for {0} has no voxels defined. " + + "Please check the 'voxels' attribute in the item JSON.", collObj.Code); + } + + protected virtual bool TryAddVoxelsFromWorkable(ICoreAPI api, ref byte[,,] beAnvilVoxels) + { + if (Voxels.MaterialCount() == 0) + return false; + + // Target (anvil) dims + var tx = beAnvilVoxels.GetLength(0); + var ty = beAnvilVoxels.GetLength(1); + var tz = beAnvilVoxels.GetLength(2); + + // Source (workable) dims (up to 16x16x6 typically) + var sx = Voxels.GetLength(0); + var sy = Voxels.GetLength(1); + var sz = Voxels.GetLength(2); + + var ox = Math.Min(tx, sx); + var oz = Math.Min(tz, sz); + + // Work on a copy so we can roll back on failure + var voxelsCopy = (byte[,,])beAnvilVoxels.Clone(); + + for (var x = 0; x < ox; x++) + for (var z = 0; z < oz; z++) + { + // Find first empty Y in the target column + var y = 0; + while (y < ty && voxelsCopy[x, y, z] != 0) y++; + + // If column is full, fail (matches your early-return behavior) + if (y >= ty) return false; + + // Place the source column starting from ny = 0 upward + for (var ny = 0; ny < sy; ny++) + { + var val = Voxels[x, ny, z]; + if (val == 0) continue; + + var tyIndex = y + ny; + if (tyIndex >= ty) + // Would overflow this column — abort entirely + return false; + + voxelsCopy[x, tyIndex, z] = val; + if (val == 1) + { + } + } + } + + // All columns fit — commit the copy + beAnvilVoxels = voxelsCopy; + return true; + } +} + +public enum AnvilPlacementMode +{ + None = -1, // No placement (why would you want this? ;p) + Normal = 0, // Can be placed both on a workitem and on an empty anvil + Empty = 1, // Cannot be placed when workitem is present + Present = 2 // Cannot be placed when workitem is missing +} + +public static class PlacementModeExtensions +{ + public static bool AllowsPresent(this AnvilPlacementMode mode) + { + return mode is AnvilPlacementMode.Normal or AnvilPlacementMode.Present; + } + + public static bool AllowsEmpty(this AnvilPlacementMode mode) + { + return mode is AnvilPlacementMode.Normal or AnvilPlacementMode.Empty; + } } diff --git a/SmithingPlus/Common/CollectibleBehaviorJsonAnvilWorkable.cs b/SmithingPlus/Common/CollectibleBehaviorJsonAnvilWorkable.cs index e95d6cc..7a5a6b5 100644 --- a/SmithingPlus/Common/CollectibleBehaviorJsonAnvilWorkable.cs +++ b/SmithingPlus/Common/CollectibleBehaviorJsonAnvilWorkable.cs @@ -1,37 +1,37 @@ -#nullable enable -using System; -using JetBrains.Annotations; -using SmithingPlus.Util; -using Vintagestory.API.Common; -using Vintagestory.API.Datastructures; -using Vintagestory.GameContent; - -namespace SmithingPlus.Common; - -public sealed class CollectibleBehaviorJsonAnvilWorkable(CollectibleObject collObj) - : CollectibleBehaviorAnvilWorkable(collObj) -{ - protected override byte[,,] Voxels => HasExtraVoxels - ? GenVoxelsFromJsonPatternWithExtra(Pattern, Api?.World.Rand, ExtraVoxelChance) - : GenVoxelsFromJsonPattern(Pattern); - - private string[][] Pattern { get; set; } = [[]]; - private bool HasExtraVoxels { get; set; } - private float ExtraVoxelChance { get; set; } = 0.5f; - private EnumHelveWorkableMode HelveWorkableMode { get; set; } = EnumHelveWorkableMode.NotWorkable; - - public override void Initialize(JsonObject properties) - { - base.Initialize(properties); - HasExtraVoxels = properties[PropertyKeys.HasExtraVoxels].Exists && - properties[PropertyKeys.HasExtraVoxels].AsBool(); - ExtraVoxelChance = properties[PropertyKeys.ExtraVoxelChance].Exists - ? properties[PropertyKeys.ExtraVoxelChance].AsFloat(0.5f) - : 0f; - HelveWorkableMode = properties[PropertyKeys.HelveWorkableMode].Exists - ? Enum.Parse( - properties[PropertyKeys.HelveWorkableMode].AsString(nameof(EnumHelveWorkableMode.NotWorkable))) - : EnumHelveWorkableMode.NotWorkable; +#nullable enable +using System; +using JetBrains.Annotations; +using SmithingPlus.Util; +using Vintagestory.API.Common; +using Vintagestory.API.Datastructures; +using Vintagestory.GameContent; + +namespace SmithingPlus.Common; + +public sealed class CollectibleBehaviorJsonAnvilWorkable(CollectibleObject collObj) + : CollectibleBehaviorAnvilWorkable(collObj) +{ + protected override byte[,,] Voxels => HasExtraVoxels + ? GenVoxelsFromJsonPatternWithExtra(Pattern, Api?.World.Rand, ExtraVoxelChance) + : GenVoxelsFromJsonPattern(Pattern); + + private string[][] Pattern { get; set; } = [[]]; + private bool HasExtraVoxels { get; set; } + private float ExtraVoxelChance { get; set; } = 0.5f; + private EnumHelveWorkableMode HelveWorkableMode { get; set; } = EnumHelveWorkableMode.NotWorkable; + + public override void Initialize(JsonObject properties) + { + base.Initialize(properties); + HasExtraVoxels = properties[PropertyKeys.HasExtraVoxels].Exists && + properties[PropertyKeys.HasExtraVoxels].AsBool(); + ExtraVoxelChance = properties[PropertyKeys.ExtraVoxelChance].Exists + ? properties[PropertyKeys.ExtraVoxelChance].AsFloat(0.5f) + : 0f; + HelveWorkableMode = properties[PropertyKeys.HelveWorkableMode].Exists + ? Enum.Parse( + properties[PropertyKeys.HelveWorkableMode].AsString(nameof(EnumHelveWorkableMode.NotWorkable))) + : EnumHelveWorkableMode.NotWorkable; JsonObject[]? jsonPattern = properties[PropertyKeys.Voxels].Exists ? properties[PropertyKeys.Voxels].AsArray() : null; @@ -71,97 +71,97 @@ public override void Initialize(JsonObject properties) } Pattern = parsedPattern; - } - - public override EnumHelveWorkableMode GetHelveWorkableMode(ItemStack stack, BlockEntityAnvil beAnvil) - { - return HelveWorkableMode; - } - - // Only use always present voxels for handbook - public override int VoxelCountForHandbook(ItemStack stack) - { - return GenVoxelsFromJsonPattern(Pattern).MaterialCount(); - } - - /// - /// Generates voxels from a JSON pattern. - /// The pattern is expected to be a 3D array of strings, - /// where each string represents a layer of the recipe. - /// Each character in the string can be: - /// '#' for a full voxel, - /// '*' for a slag voxel, - /// '_' or any character for an empty voxel. - /// The generated voxels will be centered in a 16x6x16 array. - /// - /// The JSON pattern to generate voxels from. - private static byte[,,] GenVoxelsFromJsonPattern(string[][] pattern) - { - return GenVoxelsFromJsonPatternWithExtra(pattern); - } - - /// - /// Generates voxels from a JSON pattern with extra voxel chance. - /// The pattern is expected to be a 3D array of strings, - /// where each string represents a layer of the recipe. - /// Each character in the string can be: - /// '#' for a full voxel, - /// '*' for a slag voxel, - /// 'o' for a random full voxel (with a chance defined by extraVoxelChance), - /// 'x' for a random slag voxel (with a chance defined by extraVoxelChance), - /// '?' for a random voxel (either full or slag with 50% chance), - /// '_' or any character for an empty voxel. - /// The generated voxels will be centered in a 16x6x16 array. - /// - /// The JSON pattern to generate voxels from. - /// An optional random number generator. If null, a default one will be used. - /// The chance of generating extra voxels (for 'o' and 'x' characters). - public static byte[,,] GenVoxelsFromJsonPatternWithExtra( - string[][] pattern, - Random? rand = null, - float extraVoxelChance = 0.0f) - { - var hasExtraVoxels = rand != null; - // Fallback if api is not available - extraVoxelChance = hasExtraVoxels ? extraVoxelChance : 0; - var voxels = new byte[16, 6, 16]; - var length = pattern[0][0].Length; - var width = pattern[0].Length; - var height = pattern.Length; - // Center the recipe to the horizontal middle - var startX = (16 - width) / 2; - var startZ = (16 - length) / 2; - for (var x = 0; x < Math.Min(width, 16); x++) - for (var y = 0; y < Math.Min(height, 6); y++) - for (var z = 0; z < Math.Min(length, 16); z++) - { - var c = pattern[y][x][z]; - var b = c switch - { - '#' => EnumVoxelMaterial.Metal, // always full - '*' => EnumVoxelMaterial.Slag, // always slag - 'o' => rand?.NextDouble() < extraVoxelChance - ? EnumVoxelMaterial.Metal - : EnumVoxelMaterial.Empty, // random full - 'x' => rand?.NextDouble() < extraVoxelChance - ? EnumVoxelMaterial.Slag - : EnumVoxelMaterial.Empty, // random slag - '?' => hasExtraVoxels - ? rand?.NextDouble() < 0.5f ? EnumVoxelMaterial.Metal : EnumVoxelMaterial.Slag - : EnumVoxelMaterial.Empty, // random full/slag - _ => EnumVoxelMaterial.Empty // empty (_ or space or anything else) - }; - voxels[z + startZ, y, x + startX] = (byte)b; - } - - return voxels; - } - - private static class PropertyKeys - { - public const string HasExtraVoxels = "hasExtraVoxels"; - public const string ExtraVoxelChance = "extraVoxelChance"; - public const string HelveWorkableMode = "helveWorkableMode"; - public const string Voxels = "voxels"; - } + } + + public override EnumHelveWorkableMode GetHelveWorkableMode(ItemStack stack, BlockEntityAnvil beAnvil) + { + return HelveWorkableMode; + } + + // Only use always present voxels for handbook + public override int VoxelCountForHandbook(ItemStack stack) + { + return GenVoxelsFromJsonPattern(Pattern).MaterialCount(); + } + + /// + /// Generates voxels from a JSON pattern. + /// The pattern is expected to be a 3D array of strings, + /// where each string represents a layer of the recipe. + /// Each character in the string can be: + /// '#' for a full voxel, + /// '*' for a slag voxel, + /// '_' or any character for an empty voxel. + /// The generated voxels will be centered in a 16x6x16 array. + /// + /// The JSON pattern to generate voxels from. + private static byte[,,] GenVoxelsFromJsonPattern(string[][] pattern) + { + return GenVoxelsFromJsonPatternWithExtra(pattern); + } + + /// + /// Generates voxels from a JSON pattern with extra voxel chance. + /// The pattern is expected to be a 3D array of strings, + /// where each string represents a layer of the recipe. + /// Each character in the string can be: + /// '#' for a full voxel, + /// '*' for a slag voxel, + /// 'o' for a random full voxel (with a chance defined by extraVoxelChance), + /// 'x' for a random slag voxel (with a chance defined by extraVoxelChance), + /// '?' for a random voxel (either full or slag with 50% chance), + /// '_' or any character for an empty voxel. + /// The generated voxels will be centered in a 16x6x16 array. + /// + /// The JSON pattern to generate voxels from. + /// An optional random number generator. If null, a default one will be used. + /// The chance of generating extra voxels (for 'o' and 'x' characters). + public static byte[,,] GenVoxelsFromJsonPatternWithExtra( + string[][] pattern, + Random? rand = null, + float extraVoxelChance = 0.0f) + { + var hasExtraVoxels = rand != null; + // Fallback if api is not available + extraVoxelChance = hasExtraVoxels ? extraVoxelChance : 0; + var voxels = new byte[16, 6, 16]; + var length = pattern[0][0].Length; + var width = pattern[0].Length; + var height = pattern.Length; + // Center the recipe to the horizontal middle + var startX = (16 - width) / 2; + var startZ = (16 - length) / 2; + for (var x = 0; x < Math.Min(width, 16); x++) + for (var y = 0; y < Math.Min(height, 6); y++) + for (var z = 0; z < Math.Min(length, 16); z++) + { + var c = pattern[y][x][z]; + var b = c switch + { + '#' => EnumVoxelMaterial.Metal, // always full + '*' => EnumVoxelMaterial.Slag, // always slag + 'o' => rand?.NextDouble() < extraVoxelChance + ? EnumVoxelMaterial.Metal + : EnumVoxelMaterial.Empty, // random full + 'x' => rand?.NextDouble() < extraVoxelChance + ? EnumVoxelMaterial.Slag + : EnumVoxelMaterial.Empty, // random slag + '?' => hasExtraVoxels + ? rand?.NextDouble() < 0.5f ? EnumVoxelMaterial.Metal : EnumVoxelMaterial.Slag + : EnumVoxelMaterial.Empty, // random full/slag + _ => EnumVoxelMaterial.Empty // empty (_ or space or anything else) + }; + voxels[z + startZ, y, x + startX] = (byte)b; + } + + return voxels; + } + + private static class PropertyKeys + { + public const string HasExtraVoxels = "hasExtraVoxels"; + public const string ExtraVoxelChance = "extraVoxelChance"; + public const string HelveWorkableMode = "helveWorkableMode"; + public const string Voxels = "voxels"; + } } diff --git a/SmithingPlus/Common/Metal/MetalMaterial.cs b/SmithingPlus/Common/Metal/MetalMaterial.cs index 1279b0c..e4d154a 100644 --- a/SmithingPlus/Common/Metal/MetalMaterial.cs +++ b/SmithingPlus/Common/Metal/MetalMaterial.cs @@ -1,84 +1,84 @@ -using System; -using Newtonsoft.Json; -using SmithingPlus.Util; -using Vintagestory.API.Common; -using Vintagestory.GameContent; - -namespace SmithingPlus.Metal; - -#nullable enable -[JsonObject(MemberSerialization.OptIn)] -public class MetalMaterial : IEquatable -{ - // These json properties might be null, fallback uses classic vanilla naming conventions +using System; +using Newtonsoft.Json; +using SmithingPlus.Util; +using Vintagestory.API.Common; +using Vintagestory.GameContent; + +namespace SmithingPlus.Metal; + +#nullable enable +[JsonObject(MemberSerialization.OptIn)] +public class MetalMaterial : IEquatable +{ + // These json properties might be null, fallback uses classic vanilla naming conventions [JsonProperty("ingot")] private AssetLocation? _ingotCode = null; [JsonProperty("metalbit")] private AssetLocation? _metalBitCode = null; [JsonProperty("tier")] private int? _tier = null; [JsonProperty("workitem")] private AssetLocation? _workItemCode = null; - [JsonProperty("code")] public required AssetLocation Code { get; init; } - public bool Resolved { get; private set; } - public string Variant => Code.Path; - public AssetLocation IngotCode => _ingotCode ?? new AssetLocation(Code.Domain, $"ingot-{Variant}"); - public AssetLocation MetalBitCode => _metalBitCode ?? new AssetLocation(Code.Domain, $"metalbit-{Variant}"); - public AssetLocation WorkItemCode => _workItemCode ?? new AssetLocation(Code.Domain, $"workitem-{Variant}"); - public ItemIngot? IngotItem { get; private set; } - public Item? MetalBitItem { get; private set; } - public ItemWorkItem? WorkItem { get; private set; } - public ItemStack? IngotStack => IngotItem != null ? new ItemStack(IngotItem) : null; - public ItemStack? MetalBitStack => MetalBitItem != null ? new ItemStack(MetalBitItem) : null; - public ItemStack? WorkItemStack => WorkItem != null ? new ItemStack(WorkItem) : null; - public int Tier { get; private set; } - - public bool Equals(MetalMaterial? other) - { - return other is not null && Code.Equals(other.Code); - } - - /// - /// Resolves the items and stacks using the provided API. - /// Sets the resolved flag to true if the ingot is successfully loaded. - /// - public void Resolve(ICoreAPI api) - { - IngotItem = api.World.GetItem(IngotCode) as ItemIngot; - MetalBitItem = api.World.GetItem(MetalBitCode); - WorkItem = api.World.GetItem(WorkItemCode) as ItemWorkItem; - Tier = _tier ?? GetTier(api); - if (IngotItem != null) - { - Resolved = true; - } - else - { - var ingot = api.World.GetItem(new AssetLocation("game:ingot-copper")); - Resolved = false; - api.Logger.Error( - $"[MetalMaterial] Failed to resolve ingot item {IngotCode} for metal material {Code}"); - } - - if (MetalBitItem == null) - api.Logger.Warning( - $"[MetalMaterial] Failed to resolve metal bit item {MetalBitCode} for metal material {Code}"); - if (WorkItem == null) - api.Logger.Warning( - $"[MetalMaterial] Failed to resolve work item {WorkItemCode} for metal material {Code}"); - } - - private int GetTier(ICoreAPI api) - { - return api.GetModSystem()?.metalsByCode - .TryGetValue(Variant, out var metalProperty) == true - ? metalProperty?.Tier ?? 0 - : 0; - } - - public override bool Equals(object? obj) - { - return Equals(obj as MetalMaterial); - } - - public override int GetHashCode() - { - return Code.GetHashCode(); - } + [JsonProperty("code")] public required AssetLocation Code { get; init; } + public bool Resolved { get; private set; } + public string Variant => Code.Path; + public AssetLocation IngotCode => _ingotCode ?? new AssetLocation(Code.Domain, $"ingot-{Variant}"); + public AssetLocation MetalBitCode => _metalBitCode ?? new AssetLocation(Code.Domain, $"metalbit-{Variant}"); + public AssetLocation WorkItemCode => _workItemCode ?? new AssetLocation(Code.Domain, $"workitem-{Variant}"); + public ItemIngot? IngotItem { get; private set; } + public Item? MetalBitItem { get; private set; } + public ItemWorkItem? WorkItem { get; private set; } + public ItemStack? IngotStack => IngotItem != null ? new ItemStack(IngotItem) : null; + public ItemStack? MetalBitStack => MetalBitItem != null ? new ItemStack(MetalBitItem) : null; + public ItemStack? WorkItemStack => WorkItem != null ? new ItemStack(WorkItem) : null; + public int Tier { get; private set; } + + public bool Equals(MetalMaterial? other) + { + return other is not null && Code.Equals(other.Code); + } + + /// + /// Resolves the items and stacks using the provided API. + /// Sets the resolved flag to true if the ingot is successfully loaded. + /// + public void Resolve(ICoreAPI api) + { + IngotItem = api.World.GetItem(IngotCode) as ItemIngot; + MetalBitItem = api.World.GetItem(MetalBitCode); + WorkItem = api.World.GetItem(WorkItemCode) as ItemWorkItem; + Tier = _tier ?? GetTier(api); + if (IngotItem != null) + { + Resolved = true; + } + else + { + var ingot = api.World.GetItem(new AssetLocation("game:ingot-copper")); + Resolved = false; + api.Logger.Error( + $"[MetalMaterial] Failed to resolve ingot item {IngotCode} for metal material {Code}"); + } + + if (MetalBitItem == null) + api.Logger.Warning( + $"[MetalMaterial] Failed to resolve metal bit item {MetalBitCode} for metal material {Code}"); + if (WorkItem == null) + api.Logger.Warning( + $"[MetalMaterial] Failed to resolve work item {WorkItemCode} for metal material {Code}"); + } + + private int GetTier(ICoreAPI api) + { + return api.GetModSystem()?.metalsByCode + .TryGetValue(Variant, out var metalProperty) == true + ? metalProperty?.Tier ?? 0 + : 0; + } + + public override bool Equals(object? obj) + { + return Equals(obj as MetalMaterial); + } + + public override int GetHashCode() + { + return Code.GetHashCode(); + } } diff --git a/SmithingPlus/ToolRecovery/CollectibleBehaviorRepairableTool.cs b/SmithingPlus/ToolRecovery/CollectibleBehaviorRepairableTool.cs index 245e205..4b29708 100644 --- a/SmithingPlus/ToolRecovery/CollectibleBehaviorRepairableTool.cs +++ b/SmithingPlus/ToolRecovery/CollectibleBehaviorRepairableTool.cs @@ -1,25 +1,25 @@ -#nullable enable -using System.Text; -using SmithingPlus.Util; -using Vintagestory.API.Common; -using Vintagestory.API.Config; -using Vintagestory.API.Util; - -namespace SmithingPlus.ToolRecovery; - -public class CollectibleBehaviorRepairableTool : CollectibleBehavior -{ - public CollectibleBehaviorRepairableTool(CollectibleObject collObj) : base(collObj) - { - } - - protected virtual string LangKey => "Repaired"; - - public override void GetHeldItemInfo(ItemSlot? inSlot, StringBuilder dsc, IWorldAccessor world, - bool withDebugInfo) - { - base.GetHeldItemInfo(inSlot, dsc, world, withDebugInfo); - +#nullable enable +using System.Text; +using SmithingPlus.Util; +using Vintagestory.API.Common; +using Vintagestory.API.Config; +using Vintagestory.API.Util; + +namespace SmithingPlus.ToolRecovery; + +public class CollectibleBehaviorRepairableTool : CollectibleBehavior +{ + public CollectibleBehaviorRepairableTool(CollectibleObject collObj) : base(collObj) + { + } + + protected virtual string LangKey => "Repaired"; + + public override void GetHeldItemInfo(ItemSlot? inSlot, StringBuilder dsc, IWorldAccessor world, + bool withDebugInfo) + { + base.GetHeldItemInfo(inSlot, dsc, world, withDebugInfo); + ItemStack? itemStack = inSlot?.Itemstack; CollectibleObject? collectible = itemStack?.Collectible; var code = collectible?.Code; @@ -29,13 +29,13 @@ public override void GetHeldItemInfo(ItemSlot? inSlot, StringBuilder dsc, IWorld Core.Logger.Error("Failed to get code for itemstack {0}", itemStack); return; } - - if (!WildcardUtil.Match(Core.Config.RepairableToolSelector, code.ToString())) - return; + + if (!WildcardUtil.Match(Core.Config.RepairableToolSelector, code.ToString())) + return; var brokenCount = itemStack.GetBrokenCount(); if (brokenCount <= 0) return; if (Core.CConfig.ShowRepairedCount) dsc.AppendLine(Lang.Get($"{LangKey} {{0}} times", brokenCount)); if (Core.CConfig.ShowRepairSmithName && itemStack.GetRepairSmith() is { } repairSmith) dsc.AppendLine(Lang.Get("Last repaired by {0}", repairSmith)); - } + } } diff --git a/SmithingPlus/Util/CollectibleExtensions.cs b/SmithingPlus/Util/CollectibleExtensions.cs index fcc8c67..911e3db 100644 --- a/SmithingPlus/Util/CollectibleExtensions.cs +++ b/SmithingPlus/Util/CollectibleExtensions.cs @@ -1,32 +1,32 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using Newtonsoft.Json.Linq; -using Vintagestory.API.Common; -using Vintagestory.API.Datastructures; -using Vintagestory.API.MathTools; -using Vintagestory.API.Util; -using Vintagestory.GameContent; - -namespace SmithingPlus.Util; - -#nullable enable -public static class CollectibleExtensions -{ - private static readonly JToken ForgeTransformToken = JToken.FromObject( - new ModelTransform - { - Translation = new Vec3f(0, -0.1f, 0.35f), - Rotation = new Vec3f(0, 90f, 0), - Scale = 0.7f - } - ); - - private static void EnsureAttributesNotNull(this CollectibleObject obj) - { - obj.Attributes ??= new JsonObject(new JObject()); - } - +using System; +using System.Collections.Generic; +using System.Linq; +using Newtonsoft.Json.Linq; +using Vintagestory.API.Common; +using Vintagestory.API.Datastructures; +using Vintagestory.API.MathTools; +using Vintagestory.API.Util; +using Vintagestory.GameContent; + +namespace SmithingPlus.Util; + +#nullable enable +public static class CollectibleExtensions +{ + private static readonly JToken ForgeTransformToken = JToken.FromObject( + new ModelTransform + { + Translation = new Vec3f(0, -0.1f, 0.35f), + Rotation = new Vec3f(0, 90f, 0), + Scale = 0.7f + } + ); + + private static void EnsureAttributesNotNull(this CollectibleObject obj) + { + obj.Attributes ??= new JsonObject(new JObject()); + } + public static void MakeForgeable(this CollectibleObject collObj) { JsonObject? attributes = collObj.Attributes; @@ -40,133 +40,133 @@ public static void MakeForgeable(this CollectibleObject collObj) token["forgable"] = true; token["inForgeTransform"] = ForgeTransformToken; attributes.Token = token; - } - - public static void AddBehavior(this CollectibleObject collectible) where T : CollectibleBehavior - { - var existingBehavior = collectible.CollectibleBehaviors.FirstOrDefault(b => b.GetType() == typeof(T)); - collectible.CollectibleBehaviors.Remove(existingBehavior); - if (Activator.CreateInstance(typeof(T), collectible) is not T behavior) - { - Core.Logger.Error("[CollectibleExtensions] Failed to create behavior {0} for {1}", typeof(T).Name, - collectible.Code); - return; - } - - collectible.CollectibleBehaviors = collectible.CollectibleBehaviors.Append(behavior); - } - - public static void AddBehaviorIf(this CollectibleObject collectible, bool condition) - where T : CollectibleBehavior - { - if (!condition) return; - collectible.AddBehavior(); - } - - public static bool IsRepairableTool(this CollectibleObject collObj, bool verbose = false) - { - var repairable = WildcardUtil.Match(Core.Config.RepairableToolSelector, collObj.Code.ToString()); - if (verbose && !repairable) Core.Logger.VerboseDebug("Not a repairable tool: {0}", collObj.Code); - return repairable; - } - - public static bool MatchesToolHeadSelector(this CollectibleObject collObj, bool verbose = false) - { - var repairable = WildcardUtil.Match(Core.Config.ToolHeadSelector, collObj.Code.ToString()); - if (verbose && !repairable) Core.Logger.VerboseDebug("Not a tool head: {0}", collObj.Code); - return repairable; - } - - public static SmithingRecipe? GetSmithingRecipe(this CollectibleObject collObj, ICoreAPI api) - { - 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 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 = []; - // Prevent duplicate entries when a recipe has the same ingredient multiple times - 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 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) - { - var api = collObj.GetField("api"); - if (api == null) - { - Core.Logger.Error("[CollectibleWithVariant] Reflection failed to get collectible object api field"); - return null; - } - - var codeWithVariant = collObj.CodeWithVariant(type, value); - switch (collObj.ItemClass) - { - case EnumItemClass.Block: - return api.World.GetBlock(codeWithVariant); - case EnumItemClass.Item: - return api.World.GetItem(codeWithVariant); - default: - Core.Logger.Error( - $"[CollectibleWithVariant] Invalid ItemClass \"{collObj.ItemClass}\" for collectible {collObj.Code}"); - return null; - } - } - - public static T GetBehavior(this CollectibleObject collObj, bool withInheritance) where T : CollectibleBehavior - { - return (T)collObj.GetCollectibleBehavior(typeof(T), withInheritance); - } - - /// - /// Gets the metal properties variant from a CollectibleBehaviorQuenchable behavior. - /// - public static CollectibleBehaviorQuenchable.MetalPropertyVariant? GetMetalProps( - this CollectibleBehaviorQuenchable behavior) - { - return behavior?.GetField("metalProps"); - } -} + } + + public static void AddBehavior(this CollectibleObject collectible) where T : CollectibleBehavior + { + var existingBehavior = collectible.CollectibleBehaviors.FirstOrDefault(b => b.GetType() == typeof(T)); + collectible.CollectibleBehaviors.Remove(existingBehavior); + if (Activator.CreateInstance(typeof(T), collectible) is not T behavior) + { + Core.Logger.Error("[CollectibleExtensions] Failed to create behavior {0} for {1}", typeof(T).Name, + collectible.Code); + return; + } + + collectible.CollectibleBehaviors = collectible.CollectibleBehaviors.Append(behavior); + } + + public static void AddBehaviorIf(this CollectibleObject collectible, bool condition) + where T : CollectibleBehavior + { + if (!condition) return; + collectible.AddBehavior(); + } + + public static bool IsRepairableTool(this CollectibleObject collObj, bool verbose = false) + { + var repairable = WildcardUtil.Match(Core.Config.RepairableToolSelector, collObj.Code.ToString()); + if (verbose && !repairable) Core.Logger.VerboseDebug("Not a repairable tool: {0}", collObj.Code); + return repairable; + } + + public static bool MatchesToolHeadSelector(this CollectibleObject collObj, bool verbose = false) + { + var repairable = WildcardUtil.Match(Core.Config.ToolHeadSelector, collObj.Code.ToString()); + if (verbose && !repairable) Core.Logger.VerboseDebug("Not a tool head: {0}", collObj.Code); + return repairable; + } + + public static SmithingRecipe? GetSmithingRecipe(this CollectibleObject collObj, ICoreAPI api) + { + 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 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 = []; + // Prevent duplicate entries when a recipe has the same ingredient multiple times + 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 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) + { + var api = collObj.GetField("api"); + if (api == null) + { + Core.Logger.Error("[CollectibleWithVariant] Reflection failed to get collectible object api field"); + return null; + } + + var codeWithVariant = collObj.CodeWithVariant(type, value); + switch (collObj.ItemClass) + { + case EnumItemClass.Block: + return api.World.GetBlock(codeWithVariant); + case EnumItemClass.Item: + return api.World.GetItem(codeWithVariant); + default: + Core.Logger.Error( + $"[CollectibleWithVariant] Invalid ItemClass \"{collObj.ItemClass}\" for collectible {collObj.Code}"); + return null; + } + } + + public static T GetBehavior(this CollectibleObject collObj, bool withInheritance) where T : CollectibleBehavior + { + return (T)collObj.GetCollectibleBehavior(typeof(T), withInheritance); + } + + /// + /// Gets the metal properties variant from a CollectibleBehaviorQuenchable behavior. + /// + public static CollectibleBehaviorQuenchable.MetalPropertyVariant? GetMetalProps( + this CollectibleBehaviorQuenchable behavior) + { + return behavior?.GetField("metalProps"); + } +} diff --git a/SmithingPlus/Util/ItemStackExtensions.cs b/SmithingPlus/Util/ItemStackExtensions.cs index 5b0667f..29bed28 100644 --- a/SmithingPlus/Util/ItemStackExtensions.cs +++ b/SmithingPlus/Util/ItemStackExtensions.cs @@ -1,153 +1,153 @@ -#nullable enable -using System; -using System.Collections.Generic; -using System.Linq; -using SmithingPlus.Common.Metal; -using Vintagestory.API.Common; -using Vintagestory.API.Datastructures; -using Vintagestory.GameContent; - -namespace SmithingPlus.Util; - -public static class ItemStackExtensions -{ - internal static int? GetRemainingDurability(this ItemStack itemStack) - { - return itemStack.Collectible.GetRemainingDurability(itemStack); - } - - internal static int? GetMaxDurability(this ItemStack itemStack) - { - return itemStack.Collectible.GetMaxDurability(itemStack); - } - - internal static float? GetDurabilityPercentage(this ItemStack itemStack) - { - if (itemStack.GetMaxDurability() == 0) - return null; - return itemStack.GetRemainingDurability() / itemStack.GetMaxDurability(); - } - - internal static void SetDurability(this ItemStack itemStack, int number) - { - itemStack.Collectible.SetDurability(itemStack, number); - } - - internal static void CloneBrokenCount(this ItemStack itemStack, ItemStack fromStack, int extraCount = 0) - { - var brokenCount = fromStack.GetBrokenCount(); - itemStack.Attributes.SetInt(ModStackAttributes.BrokenCount, brokenCount + extraCount); - } - - internal static void SetRepairedToolStack(this ItemStack itemStack, ItemStack fromStack) - { - itemStack.Attributes.SetItemstack(ModStackAttributes.RepairedToolStack, fromStack); - } - - // Note: On server item stack needs to be resolved! - internal static ItemStack? GetRepairedToolStack(this ItemStack itemStack) - { - return itemStack.Attributes?.GetItemstack(ModStackAttributes.RepairedToolStack); - } - - internal static string? GetRepairSmith(this ItemStack itemStack) - { - var repairedStack = itemStack.GetRepairedToolStack(); - return repairedStack?.GetRepairSmith() ?? itemStack.Attributes.GetString(ModStackAttributes.RepairSmith); - } - - internal static void SetRepairSmith(this ItemStack itemStack, string smith) - { - itemStack.Attributes.SetString(ModStackAttributes.RepairSmith, smith); - } - - internal static float GetSmithingQuality(this ItemStack itemStack) - { - return itemStack.Attributes?.GetFloat(ModStackAttributes.SmithingQuality, 1) ?? 1f; - } - - internal static void SetSmithingQuality(this ItemStack itemStack, float quality) - { - itemStack.Attributes?.SetFloat(ModStackAttributes.SmithingQuality, quality); - } - - internal static float GetToolRepairPenaltyModifier(this ItemStack itemStack) - { - return itemStack.Attributes?.GetFloat(ModStackAttributes.ToolRepairPenaltyModifier) ?? 0f; - } - - internal static void SetToolRepairPenaltyModifier(this ItemStack itemStack, float modifier) - { - itemStack.Attributes?.SetFloat(ModStackAttributes.ToolRepairPenaltyModifier, modifier); - } - - internal static void CloneRepairedToolStackOrAttributes(this ItemStack itemStack, ItemStack fromStack, - string[]? forgettableAttributes = null) - { - var repairedStack = fromStack.GetRepairedToolStack(); - if (forgettableAttributes != null) - foreach (var attributeKey in forgettableAttributes) - repairedStack?.Attributes?.RemoveAttribute(attributeKey); - if (repairedStack == null) - { - Core.Logger.VerboseDebug("No repaired tool stack found in {0}", fromStack.Collectible.Code); - return; - } - - if (itemStack.Satisfies(repairedStack)) - { - var repairedAttributes = repairedStack.Attributes ?? new TreeAttribute(); - foreach (var attribute in repairedAttributes) itemStack.Attributes[attribute.Key] = attribute.Value; - Core.Logger.VerboseDebug("Not a tool head. Cloned repaired tool stack attributes from {0} to {1}", - fromStack.Collectible.Code, itemStack.Collectible.Code); - } - else - { - itemStack.SetRepairedToolStack(repairedStack); - } - } - - internal static int GetBrokenCount(this ItemStack itemStack) - { - var repairedStack = itemStack.GetRepairedToolStack(); - return repairedStack?.GetBrokenCount() ?? itemStack.Attributes?.GetInt(ModStackAttributes.BrokenCount) ?? 0; - } - - public static bool CodeMatches(this ItemStack stack, ItemStack that) - { - return stack.Collectible.Code.Equals(that.Collectible.Code); - } - - public static float GetWorkableTemperature(this ItemStack stack) - { - var meltingPoint = stack.Collectible.CombustibleProps?.MeltingPoint - ?? stack.GetOrCacheMetalMaterial(Core.Api)?.MetalBitItem?.CombustibleProps?.MeltingPoint - ?? 0f; - var defaultTemperature = meltingPoint / 2f; - return stack.ItemAttributes?["workableTemperature"]?.AsFloat(defaultTemperature) ?? defaultTemperature; - } - - public static SmithingRecipe? GetSmithingRecipe(this ItemStack toolHead, ICoreAPI api) - { - var smithingRecipe = api.ModLoader - .GetModSystem()? - .SmithingRecipes? - .FirstOrDefault(r => r?.Output?.ResolvedItemstack?.Satisfies(toolHead) == true); - return smithingRecipe; - } - - public static SmithingRecipe? GetSmithingRecipe(this ItemStack toolHead, ICoreAPI api, int withOutputStackSize) - { - var smithingRecipe = api.ModLoader - .GetModSystem()? - .SmithingRecipes? - .FirstOrDefault(r => - r?.Output?.ResolvedItemstack?.Satisfies(toolHead) == true - && r.Output.ResolvedItemstack.StackSize == withOutputStackSize); - return smithingRecipe; - } - - // Gets the smithing recipe with the largest output stack that satisfies the tool head +#nullable enable +using System; +using System.Collections.Generic; +using System.Linq; +using SmithingPlus.Common.Metal; +using Vintagestory.API.Common; +using Vintagestory.API.Datastructures; +using Vintagestory.GameContent; + +namespace SmithingPlus.Util; + +public static class ItemStackExtensions +{ + internal static int? GetRemainingDurability(this ItemStack itemStack) + { + return itemStack.Collectible.GetRemainingDurability(itemStack); + } + + internal static int? GetMaxDurability(this ItemStack itemStack) + { + return itemStack.Collectible.GetMaxDurability(itemStack); + } + + internal static float? GetDurabilityPercentage(this ItemStack itemStack) + { + if (itemStack.GetMaxDurability() == 0) + return null; + return itemStack.GetRemainingDurability() / itemStack.GetMaxDurability(); + } + + internal static void SetDurability(this ItemStack itemStack, int number) + { + itemStack.Collectible.SetDurability(itemStack, number); + } + + internal static void CloneBrokenCount(this ItemStack itemStack, ItemStack fromStack, int extraCount = 0) + { + var brokenCount = fromStack.GetBrokenCount(); + itemStack.Attributes.SetInt(ModStackAttributes.BrokenCount, brokenCount + extraCount); + } + + internal static void SetRepairedToolStack(this ItemStack itemStack, ItemStack fromStack) + { + itemStack.Attributes.SetItemstack(ModStackAttributes.RepairedToolStack, fromStack); + } + + // Note: On server item stack needs to be resolved! + internal static ItemStack? GetRepairedToolStack(this ItemStack itemStack) + { + return itemStack.Attributes?.GetItemstack(ModStackAttributes.RepairedToolStack); + } + + internal static string? GetRepairSmith(this ItemStack itemStack) + { + var repairedStack = itemStack.GetRepairedToolStack(); + return repairedStack?.GetRepairSmith() ?? itemStack.Attributes.GetString(ModStackAttributes.RepairSmith); + } + + internal static void SetRepairSmith(this ItemStack itemStack, string smith) + { + itemStack.Attributes.SetString(ModStackAttributes.RepairSmith, smith); + } + + internal static float GetSmithingQuality(this ItemStack itemStack) + { + return itemStack.Attributes?.GetFloat(ModStackAttributes.SmithingQuality, 1) ?? 1f; + } + + internal static void SetSmithingQuality(this ItemStack itemStack, float quality) + { + itemStack.Attributes?.SetFloat(ModStackAttributes.SmithingQuality, quality); + } + + internal static float GetToolRepairPenaltyModifier(this ItemStack itemStack) + { + return itemStack.Attributes?.GetFloat(ModStackAttributes.ToolRepairPenaltyModifier) ?? 0f; + } + + internal static void SetToolRepairPenaltyModifier(this ItemStack itemStack, float modifier) + { + itemStack.Attributes?.SetFloat(ModStackAttributes.ToolRepairPenaltyModifier, modifier); + } + + internal static void CloneRepairedToolStackOrAttributes(this ItemStack itemStack, ItemStack fromStack, + string[]? forgettableAttributes = null) + { + var repairedStack = fromStack.GetRepairedToolStack(); + if (forgettableAttributes != null) + foreach (var attributeKey in forgettableAttributes) + repairedStack?.Attributes?.RemoveAttribute(attributeKey); + if (repairedStack == null) + { + Core.Logger.VerboseDebug("No repaired tool stack found in {0}", fromStack.Collectible.Code); + return; + } + + if (itemStack.Satisfies(repairedStack)) + { + var repairedAttributes = repairedStack.Attributes ?? new TreeAttribute(); + foreach (var attribute in repairedAttributes) itemStack.Attributes[attribute.Key] = attribute.Value; + Core.Logger.VerboseDebug("Not a tool head. Cloned repaired tool stack attributes from {0} to {1}", + fromStack.Collectible.Code, itemStack.Collectible.Code); + } + else + { + itemStack.SetRepairedToolStack(repairedStack); + } + } + + internal static int GetBrokenCount(this ItemStack itemStack) + { + var repairedStack = itemStack.GetRepairedToolStack(); + return repairedStack?.GetBrokenCount() ?? itemStack.Attributes?.GetInt(ModStackAttributes.BrokenCount) ?? 0; + } + + public static bool CodeMatches(this ItemStack stack, ItemStack that) + { + return stack.Collectible.Code.Equals(that.Collectible.Code); + } + + public static float GetWorkableTemperature(this ItemStack stack) + { + var meltingPoint = stack.Collectible.CombustibleProps?.MeltingPoint + ?? stack.GetOrCacheMetalMaterial(Core.Api)?.MetalBitItem?.CombustibleProps?.MeltingPoint + ?? 0f; + var defaultTemperature = meltingPoint / 2f; + return stack.ItemAttributes?["workableTemperature"]?.AsFloat(defaultTemperature) ?? defaultTemperature; + } + + public static SmithingRecipe? GetSmithingRecipe(this ItemStack toolHead, ICoreAPI api) + { + var smithingRecipe = api.ModLoader + .GetModSystem()? + .SmithingRecipes? + .FirstOrDefault(r => r?.Output?.ResolvedItemstack?.Satisfies(toolHead) == true); + return smithingRecipe; + } + + public static SmithingRecipe? GetSmithingRecipe(this ItemStack toolHead, ICoreAPI api, int withOutputStackSize) + { + var smithingRecipe = api.ModLoader + .GetModSystem()? + .SmithingRecipes? + .FirstOrDefault(r => + r?.Output?.ResolvedItemstack?.Satisfies(toolHead) == true + && r.Output.ResolvedItemstack.StackSize == withOutputStackSize); + return smithingRecipe; + } + + // Gets the smithing recipe with the largest output stack that satisfies the tool head public static SmithingRecipe? GetLargestSmithingRecipe(this ItemStack toolHead, ICoreAPI api) { RecipeRegistrySystem? recipeRegistry = api.ModLoader.GetModSystem(); @@ -174,9 +174,9 @@ public static float GetWorkableTemperature(this ItemStack stack) } return largestRecipe; - } - - // Gets the smithing recipe with the least expensive output that satisfies the tool head + } + + // Gets the smithing recipe with the least expensive output that satisfies the tool head public static SmithingRecipe? GetCheapestSmithingRecipe(this ItemStack toolHead, ICoreAPI api) { RecipeRegistrySystem? recipeRegistry = api.ModLoader.GetModSystem(); @@ -209,100 +209,100 @@ public static float GetWorkableTemperature(this ItemStack stack) } return selectedRecipe; - } - - public static IEnumerable GetGridRecipes(this ItemStack itemStack, ICoreAPI api) - { - var gridRecipes = - from recipe in api.World.GridRecipes - where recipe.Output?.ResolvedItemStack?.Satisfies(itemStack) == true - select recipe; - return gridRecipes; - } - - // Gets a smithing recipe only if the output item stack has a single item - public static SmithingRecipe? GetSingleSmithingRecipe(this ItemStack toolHead, ICoreAPI api) - { - return toolHead.GetSmithingRecipe(api, 1); - } - - public static float GetSplitCount(this ItemStack stack) - { - var splitCount = stack.TempAttributes.GetFloat(ModStackAttributes.SplitCount); - return splitCount; - } - - public static void SetSplitCount(this ItemStack stack, float count) - { - stack.TempAttributes.SetFloat(ModStackAttributes.SplitCount, count); - } - - public static float GetTemperature(this ItemStack stack, IWorldAccessor world) - { - return stack.Collectible.GetTemperature(world, stack); - } - - public static void SetTemperatureFrom(this ItemStack stack, IWorldAccessor world, ItemStack fromStack) - { - var temperature = fromStack.GetTemperature(world); - stack.Collectible.SetTemperature(world, stack, temperature); - } - - public static void SetTemperature(this ItemStack stack, IWorldAccessor world, float count) - { - stack.Collectible.SetTemperature(world, stack, count); - } - - public static bool IsSmeltedContainer(this ItemStack stack) - { - return stack.Collectible is BlockSmeltedContainer; - } - - public static bool IsCastTool(this ItemStack stack) - { - return stack.Attributes.GetBool(ModStackAttributes.CastTool); - } - - /// - /// Get the metal bits that should be recovered when this broken/shattered item is destroyed. - /// Takes into account the item's durability percentage and metal bit ratio. - /// - public static ItemStack? GetShatteredBitsStack(this ItemStack brokenStack, ICoreAPI api) - { - var metalMaterial = brokenStack.GetOrCacheMetalMaterial(api); - if (metalMaterial?.MetalBitStack == null) - return null; - - var voxelsInStack = 0; - // Work item with serialized voxel field - if (brokenStack.Collectible is ItemWorkItem) - { - var bytes = brokenStack.Attributes.GetBytes("voxels"); - var voxels = BlockEntityAnvil.deserializeVoxels(bytes); - voxelsInStack = voxels.MaterialCount(); - } - // Finished smithed item -> get via cheapest smithing recipe to prevent abuse of the mechanic - else - { - var cheapestRecipe = brokenStack.GetCheapestSmithingRecipe(api); - if (cheapestRecipe is { Output.ResolvedItemStack: not null }) - { - var cheapestOutput = Math.Max(cheapestRecipe.Output.ResolvedItemStack.StackSize, 1); - var recipeMaterialVoxels = cheapestRecipe.Voxels.VoxelCount(); - var voxelsPerItem = Math.Max(recipeMaterialVoxels / cheapestOutput, 0); - voxelsInStack = voxelsPerItem * brokenStack.StackSize; - } - } - - var durabilityPercentage = brokenStack.GetDurabilityPercentage() ?? 1f; - var reducedVoxels = voxelsInStack * durabilityPercentage; - var recoveredBits = (int)MathF.Floor(reducedVoxels / Core.Config.VoxelsPerBit); - - if (recoveredBits <= 0) - return null; - - var bitsStack = metalMaterial.MetalBitStack.Clone(); - bitsStack.StackSize = recoveredBits; - return bitsStack; - } + } + + public static IEnumerable GetGridRecipes(this ItemStack itemStack, ICoreAPI api) + { + var gridRecipes = + from recipe in api.World.GridRecipes + where recipe.Output?.ResolvedItemStack?.Satisfies(itemStack) == true + select recipe; + return gridRecipes; + } + + // Gets a smithing recipe only if the output item stack has a single item + public static SmithingRecipe? GetSingleSmithingRecipe(this ItemStack toolHead, ICoreAPI api) + { + return toolHead.GetSmithingRecipe(api, 1); + } + + public static float GetSplitCount(this ItemStack stack) + { + var splitCount = stack.TempAttributes.GetFloat(ModStackAttributes.SplitCount); + return splitCount; + } + + public static void SetSplitCount(this ItemStack stack, float count) + { + stack.TempAttributes.SetFloat(ModStackAttributes.SplitCount, count); + } + + public static float GetTemperature(this ItemStack stack, IWorldAccessor world) + { + return stack.Collectible.GetTemperature(world, stack); + } + + public static void SetTemperatureFrom(this ItemStack stack, IWorldAccessor world, ItemStack fromStack) + { + var temperature = fromStack.GetTemperature(world); + stack.Collectible.SetTemperature(world, stack, temperature); + } + + public static void SetTemperature(this ItemStack stack, IWorldAccessor world, float count) + { + stack.Collectible.SetTemperature(world, stack, count); + } + + public static bool IsSmeltedContainer(this ItemStack stack) + { + return stack.Collectible is BlockSmeltedContainer; + } + + public static bool IsCastTool(this ItemStack stack) + { + return stack.Attributes.GetBool(ModStackAttributes.CastTool); + } + + /// + /// Get the metal bits that should be recovered when this broken/shattered item is destroyed. + /// Takes into account the item's durability percentage and metal bit ratio. + /// + public static ItemStack? GetShatteredBitsStack(this ItemStack brokenStack, ICoreAPI api) + { + var metalMaterial = brokenStack.GetOrCacheMetalMaterial(api); + if (metalMaterial?.MetalBitStack == null) + return null; + + var voxelsInStack = 0; + // Work item with serialized voxel field + if (brokenStack.Collectible is ItemWorkItem) + { + var bytes = brokenStack.Attributes.GetBytes("voxels"); + var voxels = BlockEntityAnvil.deserializeVoxels(bytes); + voxelsInStack = voxels.MaterialCount(); + } + // Finished smithed item -> get via cheapest smithing recipe to prevent abuse of the mechanic + else + { + var cheapestRecipe = brokenStack.GetCheapestSmithingRecipe(api); + if (cheapestRecipe is { Output.ResolvedItemStack: not null }) + { + var cheapestOutput = Math.Max(cheapestRecipe.Output.ResolvedItemStack.StackSize, 1); + var recipeMaterialVoxels = cheapestRecipe.Voxels.VoxelCount(); + var voxelsPerItem = Math.Max(recipeMaterialVoxels / cheapestOutput, 0); + voxelsInStack = voxelsPerItem * brokenStack.StackSize; + } + } + + var durabilityPercentage = brokenStack.GetDurabilityPercentage() ?? 1f; + var reducedVoxels = voxelsInStack * durabilityPercentage; + var recoveredBits = (int)MathF.Floor(reducedVoxels / Core.Config.VoxelsPerBit); + + if (recoveredBits <= 0) + return null; + + var bitsStack = metalMaterial.MetalBitStack.Clone(); + bitsStack.StackSize = recoveredBits; + return bitsStack; + } } From ac82ae58a6cc35caa52e32e3f0950b3dfefd68ab Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C6=9B=CA=91=CA=8B=C9=8D=C9=9B=CF=AF=E1=BE=B0=C9=A8?= <6170786+AzureTai@users.noreply.github.com> Date: Wed, 19 Aug 2026 04:36:05 +0100 Subject: [PATCH 07/13] Replace project scaffold with Vintage Story 1.22 template Adopt the supplied working Vintage Story 1.22/.NET 10 project skeleton, including the Anego ZZCakeBuild project, solution files, launch profiles and build wrappers. Pin development metadata to Vintage Story 1.22.5 and exclude generated IDE, build and release output through the project-specific gitignore. --- .gitignore | 40 +++-- CakeBuild/CakeBuild.csproj | 19 --- CakeBuild/Program.cs | 162 -------------------- Directory.Build.props | 20 --- SmithingPlus.sln | 26 ++-- SmithingPlus.slnx | 4 + SmithingPlus/Properties/launchSettings.json | 33 +--- SmithingPlus/SmithingPlus.csproj | 4 +- SmithingPlus/modinfo.json | 4 +- ZZCakeBuild/CakeBuild.csproj | 20 +++ ZZCakeBuild/Program.cs | 120 +++++++++++++++ build.ps1 | 2 + build.sh | 1 + 13 files changed, 198 insertions(+), 257 deletions(-) delete mode 100644 CakeBuild/CakeBuild.csproj delete mode 100644 CakeBuild/Program.cs delete mode 100644 Directory.Build.props create mode 100644 SmithingPlus.slnx create mode 100644 ZZCakeBuild/CakeBuild.csproj create mode 100644 ZZCakeBuild/Program.cs create mode 100644 build.ps1 create mode 100644 build.sh diff --git a/.gitignore b/.gitignore index 47dfddf..0d27c0a 100644 --- a/.gitignore +++ b/.gitignore @@ -1,13 +1,29 @@ -bin/ -obj/ -/test -/packages/ -riderModule.iml -/_ReSharper.Caches/ -*.idea -*.DS_Store -/.idea/**/* -/Releases/**/* +# .NET build output +[Bb]in/ +[Oo]bj/ +artifacts/ + +# Generated release packages +/Releases/ + +# Visual Studio +/.vs/ +*.suo +*.user +*.rsuser *.sln.DotSettings.user -/.vs/SmithingPlus -/.vs/ProjectEvaluation +*.binlog + +# JetBrains Rider and ReSharper +/.idea/ +*.sln.iml +riderModule.iml +_ReSharper.Caches/ + +# Restored packages +/packages/ + +# Operating-system metadata +.DS_Store +Thumbs.db +Desktop.ini \ No newline at end of file diff --git a/CakeBuild/CakeBuild.csproj b/CakeBuild/CakeBuild.csproj deleted file mode 100644 index e3977e5..0000000 --- a/CakeBuild/CakeBuild.csproj +++ /dev/null @@ -1,19 +0,0 @@ - - - Exe - net10.0 - $(MSBuildProjectDirectory) - - - - - - - - - - - $(VINTAGE_STORY)/VintagestoryAPI.dll - - - \ No newline at end of file diff --git a/CakeBuild/Program.cs b/CakeBuild/Program.cs deleted file mode 100644 index caa7a8b..0000000 --- a/CakeBuild/Program.cs +++ /dev/null @@ -1,162 +0,0 @@ -using System; -using System.IO; -using Cake.Common; -using Cake.Common.Diagnostics; -using Cake.Common.IO; -using Cake.Common.Tools.DotNet; -using Cake.Common.Tools.DotNet.Clean; -using Cake.Common.Tools.DotNet.Publish; -using Cake.Core; -using Cake.Core.IO; -using Cake.Frosting; -using Cake.Json; -using Newtonsoft.Json; -using Newtonsoft.Json.Linq; -using Vintagestory.API.Common; - -namespace CakeBuild; - -public static class Program -{ - public static int Main(string[] args) - { - return new CakeHost() - .UseContext() - .Run(args); - } -} - -public class BuildContext : FrostingContext -{ - public const string ProjectName = "SmithingPlus"; - - public BuildContext(ICakeContext context) - : base(context) - { - BuildConfiguration = context.Argument("configuration", "Release"); - SkipJsonValidation = context.Argument("skipJsonValidation", false); - var modInfo = context.DeserializeJsonFromFile($"../{ProjectName}/modinfo.json"); - Version = modInfo.Version; - Name = modInfo.ModID; - } - - public string BuildConfiguration { get; } - public string Version { get; } - public string Name { get; } - public bool SkipJsonValidation { get; } -} - -[TaskName("ValidateJson")] -public sealed class ValidateJsonTask : FrostingTask -{ - public override void Run(BuildContext context) - { - if (context.SkipJsonValidation) return; - - var jsonFiles = context.GetFiles($"../{BuildContext.ProjectName}/assets/**/*.json"); - foreach (var file in jsonFiles) - try - { - var json = File.ReadAllText(file.FullPath); - JToken.Parse(json); - } - catch (JsonException ex) - { - throw new Exception( - $"Validation failed for JSON file: {file.FullPath}{Environment.NewLine}{ex.Message}", ex); - } - } -} - -[TaskName("Build")] -[IsDependentOn(typeof(ValidateJsonTask))] -public sealed class BuildTask : FrostingTask -{ - public override void Run(BuildContext context) - { - context.DotNetClean($"../{BuildContext.ProjectName}/{BuildContext.ProjectName}.csproj", - new DotNetCleanSettings - { - Configuration = context.BuildConfiguration - }); - - - context.DotNetPublish($"../{BuildContext.ProjectName}/{BuildContext.ProjectName}.csproj", - new DotNetPublishSettings - { - Configuration = context.BuildConfiguration - }); - } -} - -[TaskName("Package")] -[IsDependentOn(typeof(BuildTask))] -public sealed class PackageTask : FrostingTask -{ - public override void Run(BuildContext context) - { - context.EnsureDirectoryExists("../Releases"); - context.CleanDirectory("../Releases"); - context.EnsureDirectoryExists($"../Releases/{context.Name}"); - context.CopyFiles($"../{BuildContext.ProjectName}/bin/{context.BuildConfiguration}/Mods/mod/publish/*", - $"../Releases/{context.Name}"); - if (context.DirectoryExists($"../{BuildContext.ProjectName}/assets")) - context.CopyDirectory($"../{BuildContext.ProjectName}/assets", $"../Releases/{context.Name}/assets"); - - context.CopyFile($"../{BuildContext.ProjectName}/modinfo.json", $"../Releases/{context.Name}/modinfo.json"); - if (context.FileExists($"../{BuildContext.ProjectName}/modicon.png")) - context.CopyFile($"../{BuildContext.ProjectName}/modicon.png", $"../Releases/{context.Name}/modicon.png"); - - context.Zip($"../Releases/{context.Name}", $"../Releases/{context.Name}_{context.Version}.zip"); - } -} - -[TaskName("Release")] -[IsDependentOn(typeof(PackageTask))] -public sealed class ReleaseTask : FrostingTask -{ - public override void Run(BuildContext context) - { - var version = context.Version; - var name = context.Name; - var tag = $"v{version}"; - var zipPath = $"../Releases/{name}_{version}.zip"; - - if (!context.FileExists(zipPath)) - throw new Exception($"Release asset not found at {zipPath}"); - - var ghExe = context.Tools.Resolve("gh") ?? "gh"; - - // create the release (will fail if already exists) - var createArgs = new ProcessArgumentBuilder() - .Append("release create") - .Append(tag) - .AppendQuoted(zipPath.Replace('\\', '/')) - .AppendSwitch("--title", " ", tag) - .Append("--generate-notes"); - - var exitCode = context.StartProcess(ghExe, new ProcessSettings { Arguments = createArgs }); - - if (exitCode != 0) - { - // If the release already exists, just upload/replace the asset - var uploadArgs = new ProcessArgumentBuilder() - .Append("release upload") - .Append(tag) - .AppendQuoted(zipPath.Replace('\\', '/')) - .Append("--clobber"); - - var uploadExit = context.StartProcess(ghExe, new ProcessSettings { Arguments = uploadArgs }); - if (uploadExit != 0) - throw new Exception("Failed to upload asset to existing release."); - } - - context.Information($"✅ Published GitHub release {tag} with asset {zipPath}"); - } -} - -[TaskName("Default")] -[IsDependentOn(typeof(PackageTask))] -public class DefaultTask : FrostingTask -{ -} \ No newline at end of file diff --git a/Directory.Build.props b/Directory.Build.props deleted file mode 100644 index 956e7f4..0000000 --- a/Directory.Build.props +++ /dev/null @@ -1,20 +0,0 @@ - - - - $(VINTAGE_STORY_1_22) - [TEST] SmithingPlus - $(SolutionDir)/test - $(TestDir)/VintagestoryData - $(HOME)/Library/Application Support/VintagestoryDev/Mods - - - - - true - true - - - - - - \ No newline at end of file diff --git a/SmithingPlus.sln b/SmithingPlus.sln index 17bee26..e07e192 100644 --- a/SmithingPlus.sln +++ b/SmithingPlus.sln @@ -1,22 +1,28 @@  Microsoft Visual Studio Solution File, Format Version 12.00 -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CakeBuild", "CakeBuild\CakeBuild.csproj", "{008A5C0F-3196-4E28-B037-07EB18C2EC2D}" +# Visual Studio Version 17 +VisualStudioVersion = 17.0.31903.59 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SmithingPlus", "SmithingPlus\SmithingPlus.csproj", "{CB2100BC-F653-402A-9FBA-EA463C2BD4FC}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SmithingPlus", "SmithingPlus\SmithingPlus.csproj", "{E59ECD07-2CF4-4BAF-B740-149A3676F3EF}" +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CakeBuild", "ZZCakeBuild\CakeBuild.csproj", "{BC68EDF6-C294-4819-B7F4-9EA99B73E69D}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU Release|Any CPU = Release|Any CPU EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection GlobalSection(ProjectConfigurationPlatforms) = postSolution - {008A5C0F-3196-4E28-B037-07EB18C2EC2D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {008A5C0F-3196-4E28-B037-07EB18C2EC2D}.Debug|Any CPU.Build.0 = Debug|Any CPU - {008A5C0F-3196-4E28-B037-07EB18C2EC2D}.Release|Any CPU.ActiveCfg = Release|Any CPU - {008A5C0F-3196-4E28-B037-07EB18C2EC2D}.Release|Any CPU.Build.0 = Release|Any CPU - {E59ECD07-2CF4-4BAF-B740-149A3676F3EF}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {E59ECD07-2CF4-4BAF-B740-149A3676F3EF}.Debug|Any CPU.Build.0 = Debug|Any CPU - {E59ECD07-2CF4-4BAF-B740-149A3676F3EF}.Release|Any CPU.ActiveCfg = Release|Any CPU - {E59ECD07-2CF4-4BAF-B740-149A3676F3EF}.Release|Any CPU.Build.0 = Release|Any CPU + {BC68EDF6-C294-4819-B7F4-9EA99B73E69D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {BC68EDF6-C294-4819-B7F4-9EA99B73E69D}.Debug|Any CPU.Build.0 = Debug|Any CPU + {BC68EDF6-C294-4819-B7F4-9EA99B73E69D}.Release|Any CPU.ActiveCfg = Release|Any CPU + {BC68EDF6-C294-4819-B7F4-9EA99B73E69D}.Release|Any CPU.Build.0 = Release|Any CPU + {CB2100BC-F653-402A-9FBA-EA463C2BD4FC}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {CB2100BC-F653-402A-9FBA-EA463C2BD4FC}.Debug|Any CPU.Build.0 = Debug|Any CPU + {CB2100BC-F653-402A-9FBA-EA463C2BD4FC}.Release|Any CPU.ActiveCfg = Release|Any CPU + {CB2100BC-F653-402A-9FBA-EA463C2BD4FC}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection EndGlobal diff --git a/SmithingPlus.slnx b/SmithingPlus.slnx new file mode 100644 index 0000000..a58f57a --- /dev/null +++ b/SmithingPlus.slnx @@ -0,0 +1,4 @@ + + + + diff --git a/SmithingPlus/Properties/launchSettings.json b/SmithingPlus/Properties/launchSettings.json index 64b79ab..194b549 100644 --- a/SmithingPlus/Properties/launchSettings.json +++ b/SmithingPlus/Properties/launchSettings.json @@ -2,40 +2,15 @@ "profiles": { "Client": { "commandName": "Executable", - "executablePath": "$(VINTAGE_STORY)/Vintagestory", - "commandLineArgs": "--tracelog --playStyle \"preset-surviveandbuild\" --addModPath \"$(ProjectDir)/bin/$(Configuration)/Mods\" \"$(DevModPath)\" \"$(TestDataPath)/Mods\" --openWorld \"$(WorldName)\" --dataPath \"$(TestDataPath)\"", + "executablePath": "dotnet", + "commandLineArgs": "\"$(VINTAGE_STORY)/Vintagestory.dll\" --tracelog --addModPath \"$(ProjectDir)/bin/$(Configuration)/Mods\" --addOrigin \"$(ProjectDir)/assets\"", "workingDirectory": "$(VINTAGE_STORY)" }, "Server": { "commandName": "Executable", - "executablePath": "$(VINTAGE_STORY)/VintagestoryServer", - "commandLineArgs": "--tracelog --addModPath \"$(ProjectDir)/bin/$(Configuration)/Mods\" \"$(DevModPath)\" --dataPath \"$(TestDataPath)\"", + "executablePath": "dotnet", + "commandLineArgs": "\"$(VINTAGE_STORY)/VintagestoryServer.dll\" --tracelog --addModPath \"$(ProjectDir)/bin/$(Configuration)/Mods\" --addOrigin \"$(ProjectDir)/assets\"", "workingDirectory": "$(VINTAGE_STORY)" - }, - "ClientFlat": { - "commandName": "Executable", - "executablePath": "$(VINTAGE_STORY)/Vintagestory", - "commandLineArgs": "--tracelog --addModPath \"$(ProjectDir)/bin/$(Configuration)/Mods\" \"$(DevModPath)\" -o \"$(WorldName) Flat\" --dataPath \"$(TestDataPath)\"", - "workingDirectory": "$(VINTAGE_STORY)" - }, - "ServerFlat": { - "commandName": "Executable", - "executablePath": "$(VINTAGE_STORY)/VintagestoryServer", - "commandLineArgs": "--tracelog --addModPath \"$(ProjectDir)/bin/$(Configuration)/Mods\" \"$(DevModPath)\" -o \"$(WorldName) Flat\" --dataPath \"$(TestDataPath)\"", - "workingDirectory": "$(VINTAGE_STORY)" - }, - "ClientDotnet": { - "commandName": "Executable", - "executablePath": "/usr/local/share/dotnet/x64/dotnet", - "commandLineArgs": "\"$(VINTAGE_STORY)/Vintagestory.dll\" --tracelog --addModPath \"$(ProjectDir)/bin/$(Configuration)/Mods\" \"$(DevModPath)\" --dataPath \"$(TestDataPath)\"", - "workingDirectory": "$(VINTAGE_STORY)", - "environmentVariables": { - "DOTNET_gcServer": "1", - "DOTNET_GCNoAffinitize": "1", - "DOTNET_TieredPGO": "1", - "DYLD_LIBRARY_PATH": "$(VINTAGE_STORY)/Lib", - "DOTNET_HOTRELOAD_ENABLED": "1" - } } } } \ No newline at end of file diff --git a/SmithingPlus/SmithingPlus.csproj b/SmithingPlus/SmithingPlus.csproj index 78175f1..9dd7efc 100644 --- a/SmithingPlus/SmithingPlus.csproj +++ b/SmithingPlus/SmithingPlus.csproj @@ -61,9 +61,7 @@ - + diff --git a/SmithingPlus/modinfo.json b/SmithingPlus/modinfo.json index da052c2..beef76c 100644 --- a/SmithingPlus/modinfo.json +++ b/SmithingPlus/modinfo.json @@ -7,8 +7,8 @@ "jayu" ], "description": "Metal tool repair, smithing tweaks and quality of life improvements.", - "version": "1.9.0-rc.1", + "version": "1.9.1-dev.1", "dependencies": { - "game": "" + "game": "1.22.5" } } diff --git a/ZZCakeBuild/CakeBuild.csproj b/ZZCakeBuild/CakeBuild.csproj new file mode 100644 index 0000000..7243da4 --- /dev/null +++ b/ZZCakeBuild/CakeBuild.csproj @@ -0,0 +1,20 @@ + + + Exe + net10.0 + $(MSBuildProjectDirectory) + false + + + + + + + + + + + $(VINTAGE_STORY)/VintagestoryAPI.dll + + + \ No newline at end of file diff --git a/ZZCakeBuild/Program.cs b/ZZCakeBuild/Program.cs new file mode 100644 index 0000000..ae7d769 --- /dev/null +++ b/ZZCakeBuild/Program.cs @@ -0,0 +1,120 @@ +using Cake.Common; +using Cake.Common.IO; +using Cake.Common.Tools.DotNet; +using Cake.Common.Tools.DotNet.Clean; +using Cake.Common.Tools.DotNet.Publish; +using Cake.Core; +using Cake.Frosting; +using Cake.Json; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; +using System; +using System.IO; +using Vintagestory.API.Common; + +namespace CakeBuild +{ + public static class Program + { + public static int Main(string[] args) + { + return new CakeHost() + .UseContext() + .Run(args); + } + } + + public class BuildContext : FrostingContext + { + public const string ProjectName = "SmithingPlus"; + public string BuildConfiguration { get; } + public string Version { get; } + public string Name { get; } + public bool SkipJsonValidation { get; } + + public BuildContext(ICakeContext context) + : base(context) + { + BuildConfiguration = context.Argument("configuration", "Release"); + SkipJsonValidation = context.Argument("skipJsonValidation", false); + var modInfo = context.DeserializeJsonFromFile($"../{ProjectName}/modinfo.json"); + Version = modInfo.Version; + Name = modInfo.ModID; + } + } + + [TaskName("ValidateJson")] + public sealed class ValidateJsonTask : FrostingTask + { + public override void Run(BuildContext context) + { + if (context.SkipJsonValidation) + { + return; + } + var jsonFiles = context.GetFiles($"../{BuildContext.ProjectName}/assets/**/*.json"); + foreach (var file in jsonFiles) + { + try + { + var json = File.ReadAllText(file.FullPath); + JToken.Parse(json); + } + catch (JsonException ex) + { + throw new Exception($"Validation failed for JSON file: {file.FullPath}{Environment.NewLine}{ex.Message}", ex); + } + } + } + } + + [TaskName("Build")] + [IsDependentOn(typeof(ValidateJsonTask))] + public sealed class BuildTask : FrostingTask + { + public override void Run(BuildContext context) + { + context.DotNetClean($"../{BuildContext.ProjectName}/{BuildContext.ProjectName}.csproj", + new DotNetCleanSettings + { + Configuration = context.BuildConfiguration + }); + + + context.DotNetPublish($"../{BuildContext.ProjectName}/{BuildContext.ProjectName}.csproj", + new DotNetPublishSettings + { + Configuration = context.BuildConfiguration + }); + } + } + + [TaskName("Package")] + [IsDependentOn(typeof(BuildTask))] + public sealed class PackageTask : FrostingTask + { + public override void Run(BuildContext context) + { + context.EnsureDirectoryExists("../Releases"); + context.CleanDirectory("../Releases"); + context.EnsureDirectoryExists($"../Releases/{context.Name}"); + context.CopyFiles($"../{BuildContext.ProjectName}/bin/{context.BuildConfiguration}/Mods/mod/publish/*", $"../Releases/{context.Name}"); + if (context.DirectoryExists($"../{BuildContext.ProjectName}/assets")) + { + context.CopyDirectory($"../{BuildContext.ProjectName}/assets", $"../Releases/{context.Name}/assets"); + } + context.CopyFile($"../{BuildContext.ProjectName}/modinfo.json", $"../Releases/{context.Name}/modinfo.json"); + if (context.FileExists($"../{BuildContext.ProjectName}/modicon.png")) + { + context.CopyFile($"../{BuildContext.ProjectName}/modicon.png", $"../Releases/{context.Name}/modicon.png"); + } + context.Zip($"../Releases/{context.Name}", $"../Releases/{context.Name}_{context.Version}.zip"); + } + } + + [TaskName("Default")] + [IsDependentOn(typeof(PackageTask))] + public class DefaultTask : FrostingTask + { + } +} \ No newline at end of file diff --git a/build.ps1 b/build.ps1 new file mode 100644 index 0000000..a2ee10a --- /dev/null +++ b/build.ps1 @@ -0,0 +1,2 @@ +dotnet run --project ZZCakeBuild/CakeBuild.csproj -- $args +exit $LASTEXITCODE; \ No newline at end of file diff --git a/build.sh b/build.sh new file mode 100644 index 0000000..1a40ae7 --- /dev/null +++ b/build.sh @@ -0,0 +1 @@ +dotnet run --project ./ZZCakeBuild/CakeBuild.csproj -- "$@" From b63b96f8d18afcee2f714c217984fe334a2a0776 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C6=9B=CA=91=CA=8B=C9=8D=C9=9B=CF=AF=E1=BE=B0=C9=A8?= <6170786+AzureTai@users.noreply.github.com> Date: Wed, 19 Aug 2026 04:54:12 +0100 Subject: [PATCH 08/13] Complete explicit nullable flow validation Validate recipe objects independently from their resolved outputs and handle an absent collectible attribute token before mutation. This resolves the remaining nullable-flow warnings reported by the Vintage Story 1.22.5 build. --- SmithingPlus/Common/CollectibleBehaviorAnvilWorkable.cs | 9 +++++++-- SmithingPlus/Util/CollectibleExtensions.cs | 7 ++++++- SmithingPlus/Util/ItemStackExtensions.cs | 7 ++++++- 3 files changed, 19 insertions(+), 4 deletions(-) diff --git a/SmithingPlus/Common/CollectibleBehaviorAnvilWorkable.cs b/SmithingPlus/Common/CollectibleBehaviorAnvilWorkable.cs index 6a9723c..4ac81e4 100644 --- a/SmithingPlus/Common/CollectibleBehaviorAnvilWorkable.cs +++ b/SmithingPlus/Common/CollectibleBehaviorAnvilWorkable.cs @@ -97,8 +97,13 @@ public virtual List GetMatchingRecipes(ItemStack stack) IEnumerable smithingRecipes = api.GetSmithingRecipes(); foreach (SmithingRecipe? recipe in smithingRecipes) { - CraftingRecipeIngredient? ingredient = recipe?.Ingredient; - ItemStack? resolvedOutputStack = recipe?.Output?.ResolvedItemstack; + if (recipe == null) + { + continue; + } + + CraftingRecipeIngredient? ingredient = recipe.Ingredient; + ItemStack? resolvedOutputStack = recipe.Output?.ResolvedItemstack; AssetLocation? outputCode = resolvedOutputStack?.Collectible?.Code; if (ingredient == null || resolvedOutputStack == null || outputCode == null) { diff --git a/SmithingPlus/Util/CollectibleExtensions.cs b/SmithingPlus/Util/CollectibleExtensions.cs index 911e3db..16a82bd 100644 --- a/SmithingPlus/Util/CollectibleExtensions.cs +++ b/SmithingPlus/Util/CollectibleExtensions.cs @@ -36,7 +36,12 @@ public static void MakeForgeable(this CollectibleObject collObj) collObj.Attributes = attributes; } - JToken token = attributes.Token; + JToken? token = attributes.Token; + if (token == null) + { + token = new JObject(); + } + token["forgable"] = true; token["inForgeTransform"] = ForgeTransformToken; attributes.Token = token; diff --git a/SmithingPlus/Util/ItemStackExtensions.cs b/SmithingPlus/Util/ItemStackExtensions.cs index 29bed28..fe4305e 100644 --- a/SmithingPlus/Util/ItemStackExtensions.cs +++ b/SmithingPlus/Util/ItemStackExtensions.cs @@ -189,7 +189,12 @@ public static float GetWorkableTemperature(this ItemStack stack) int selectedVoxelCost = int.MinValue; foreach (SmithingRecipe? recipe in recipeRegistry.SmithingRecipes) { - ItemStack? resolvedOutputStack = recipe?.Output?.ResolvedItemstack; + if (recipe == null) + { + continue; + } + + ItemStack? resolvedOutputStack = recipe.Output?.ResolvedItemstack; if (resolvedOutputStack == null || resolvedOutputStack.StackSize <= 0) { continue; From 10f6676a6e338f2b27bec8f5aa6feb970f21ff3f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C6=9B=CA=91=CA=8B=C9=8D=C9=9B=CF=AF=E1=BE=B0=C9=A8?= <6170786+AzureTai@users.noreply.github.com> Date: Wed, 19 Aug 2026 04:56:03 +0100 Subject: [PATCH 09/13] Remove disabled work item renderer patch Delete the unfinished stone-smithing renderer postfix whose entry point returned unconditionally and whose mesh helper had no callers. This removes the final compiler warning and avoids registering a no-op Harmony patch without changing reachable behaviour. --- .../StoneSmithing/WorkItemRendererPatches.cs | 79 ------------------- 1 file changed, 79 deletions(-) delete mode 100644 SmithingPlus/StoneSmithing/WorkItemRendererPatches.cs diff --git a/SmithingPlus/StoneSmithing/WorkItemRendererPatches.cs b/SmithingPlus/StoneSmithing/WorkItemRendererPatches.cs deleted file mode 100644 index 7f9fe7c..0000000 --- a/SmithingPlus/StoneSmithing/WorkItemRendererPatches.cs +++ /dev/null @@ -1,79 +0,0 @@ -using HarmonyLib; -using Vintagestory.API.Client; -using Vintagestory.API.Common; -using Vintagestory.GameContent; - -namespace SmithingPlus.StoneSmithing; - -[HarmonyPatchCategory(Core.StoneSmithingCategory)] -[HarmonyPatch(typeof(AnvilWorkItemRenderer), nameof(AnvilWorkItemRenderer.RegenMesh))] -public static class RegenMeshPostfixPatch -{ - public static void RegenMesh_Postfix( - AnvilWorkItemRenderer __instance, - ref MeshRef ___workItemMeshRef, - ref MeshRef ___recipeOutlineMeshRef, - ref ICoreClientAPI ___api, - ref int ___texId, - ItemStack workitemStack, - byte[,,] voxels, - bool[,,] recipeToOutlineVoxels - ) - { - return; - ___api?.Logger?.Warning("RegenMesh_Postfix called"); - var yOffset = 0.1f; // workitemStack.Attributes.GetFloat("yOffset"); - ___workItemMeshRef?.Dispose(); - ___recipeOutlineMeshRef?.Dispose(); - ___workItemMeshRef = null; - ___recipeOutlineMeshRef = null; - var workItemMeshData = ItemWorkItem.GenMesh(___api, workitemStack, voxels); - var recipeOutlineMeshData = GenOutlineMesh(___api, recipeToOutlineVoxels, voxels); - for (var i = 0; i < workItemMeshData.xyz.Length; i += 3) - workItemMeshData.xyz[i + 1] += yOffset; - ___workItemMeshRef = ___api?.Render.UploadMesh(workItemMeshData); - if (recipeOutlineMeshData.VerticesCount <= 0) - return; - ___recipeOutlineMeshRef = ___api?.Render.UploadMesh(recipeOutlineMeshData); - } - - public static MeshData GenOutlineMesh(ICoreClientAPI capi, bool[,,] recipeToOutlineVoxels, byte[,,] voxels) - { - var data = new MeshData(24, 36, withUv: false, withFlags: false); - data.SetMode(EnumDrawMode.Lines); - var color1 = capi.ColorPreset.GetColor("anvilColorGreen"); - var color2 = capi.ColorPreset.GetColor("anvilColorRed"); - var cube1 = LineMeshUtil.GetCube(color1); - var cube2 = LineMeshUtil.GetCube(color2); - for (var index = 0; index < cube1.xyz.Length; ++index) - { - cube1.xyz[index] = (float)(cube1.xyz[index] / 32.0 + 1.0 / 32.0); - cube2.xyz[index] = (float)(cube2.xyz[index] / 32.0 + 1.0 / 32.0); - } - - var sourceMesh = cube1.Clone(); - var length = recipeToOutlineVoxels.GetLength(1); - for (var index1 = 0; index1 < 16; ++index1) - for (var index2 = 0; index2 < 6; ++index2) - for (var index3 = 0; index3 < 16; ++index3) - { - var flag = index2 < length && recipeToOutlineVoxels[index1, index2, index3]; - var voxel = (EnumVoxelMaterial)voxels[index1, index2, index3]; - if ((flag && voxel == EnumVoxelMaterial.Metal) || (!flag && voxel == EnumVoxelMaterial.Empty)) continue; - var num1 = index1 / 16f; - var num2 = (float)(0.625 + index2 / 16.0); - var num3 = index3 / 16f; - for (var index4 = 0; index4 < cube1.xyz.Length; index4 += 3) - { - sourceMesh.xyz[index4] = num1 + cube1.xyz[index4]; - sourceMesh.xyz[index4 + 1] = num2 + cube1.xyz[index4 + 1]; - sourceMesh.xyz[index4 + 2] = num3 + cube1.xyz[index4 + 2]; - } - - sourceMesh.Rgba = !flag || voxel != EnumVoxelMaterial.Empty ? cube2.Rgba : cube1.Rgba; - data.AddMeshData(sourceMesh); - } - - return data; - } -} \ No newline at end of file From 830ae7cc6bcc6b2c50ffcf8176052df63ddbcf99 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C6=9B=CA=91=CA=8B=C9=8D=C9=9B=CF=AF=E1=BE=B0=C9=A8?= <6170786+AzureTai@users.noreply.github.com> Date: Wed, 19 Aug 2026 05:18:05 +0100 Subject: [PATCH 10/13] Harden nullable gameplay and lifecycle paths Add explicit guards for unresolved recipes, missing metal materials and bit items, empty slots, absent mold outputs, reflection failures, and mod lifecycle state. Remove the remaining null-forgiving recipe assertion and nullable warning pragma. ZZCakeBuild is unchanged. Static validation: git diff --check passed. Compilation was not available in the publishing container and requires verification against the local Vintage Story 1.22.5 installation. --- .../BitsRecovery/BitsRecoveryPatches.cs | 11 ++- .../CollectibleBehaviorScrapeCrucible.cs | 5 +- .../CollectibleBehaviorSmeltedContainer.cs | 12 ++- .../HelveHammerRecoveryPatches.cs | 11 ++- .../CastingTweaks/CastToolPenaltyPatch.cs | 12 ++- .../CastingTweaks/ToolMoldUnitsPatch.cs | 27 +++++-- .../ClientTweaks/HandbookInfoPatch.cs | 28 +++++-- .../Common/CollectibleBehaviorRecycledBit.cs | 15 ++-- .../Common/Metal/MetalMaterialExtensions.cs | 44 ++++++++--- SmithingPlus/Config/ConfigLoader.cs | 8 +- SmithingPlus/Core.cs | 74 ++++++++++++++----- .../SmithingRecipeAttributesPatch.cs | 25 +++++-- SmithingPlus/StoneSmithing/ItemStoneHammer.cs | 4 +- .../CollectibleBehaviorBrokenToolHead.cs | 16 ++-- .../ToolRecovery/ItemDamagedPatches.cs | 63 ++++++++++------ SmithingPlus/Util/CollectibleExtensions.cs | 15 +++- SmithingPlus/Util/HammerExtensions.cs | 13 +++- SmithingPlus/Util/ReflectionExtensions.cs | 57 +++++++++++--- 18 files changed, 323 insertions(+), 117 deletions(-) diff --git a/SmithingPlus/BitsRecovery/BitsRecoveryPatches.cs b/SmithingPlus/BitsRecovery/BitsRecoveryPatches.cs index a24b22c..9b99896 100644 --- a/SmithingPlus/BitsRecovery/BitsRecoveryPatches.cs +++ b/SmithingPlus/BitsRecovery/BitsRecoveryPatches.cs @@ -67,10 +67,15 @@ private static void RecoverBitsFromWorkItem(BlockEntityAnvil __instance, IPlayer return; } - var metalBitStack = metalMaterial.MetalBitStack; + ItemStack? metalBitStack = metalMaterial.MetalBitStack; + if (metalBitStack == null) + { + Core.Logger.VerboseDebug("[BitsRecovery] The resolved metal material has no metal bit item."); + return; + } var temperature = workItemStack.Collectible.GetTemperature(byPlayer.Entity.World, workItemStack); - metalBitStack?.Collectible.SetTemperature(byPlayer.Entity.World, metalBitStack, temperature); + metalBitStack.Collectible.SetTemperature(byPlayer.Entity.World, metalBitStack, temperature); if (byPlayer.InventoryManager.TryGiveItemstack(metalBitStack)) return; byPlayer.Entity.World.SpawnItemEntity(metalBitStack, byPlayer.Entity.Pos.XYZ); } -} \ No newline at end of file +} diff --git a/SmithingPlus/BitsRecovery/CollectibleBehaviorScrapeCrucible.cs b/SmithingPlus/BitsRecovery/CollectibleBehaviorScrapeCrucible.cs index 1d6dbd8..5d608f1 100644 --- a/SmithingPlus/BitsRecovery/CollectibleBehaviorScrapeCrucible.cs +++ b/SmithingPlus/BitsRecovery/CollectibleBehaviorScrapeCrucible.cs @@ -83,8 +83,11 @@ public override void OnHeldInteractStop(float secondsUsed, var firedCrucibleCode = crucibleStack.Collectible.CodeWithVariant("type", "fired"); var firedCrucibleItem = world.GetBlock(firedCrucibleCode); if (firedCrucibleItem == null) + { Core.Logger.Warning( $"[{nameof(OnHeldInteractStop)}] Something went wrong, cannot find fired crucible with code {firedCrucibleCode}"); + return; + } var emptyCrucibleStack = new ItemStack(firedCrucibleItem); if (!playerInventory.TryGiveItemstack(metalBitStack, true)) world.SpawnItemEntity(metalBitStack, blockSel.Position); @@ -142,4 +145,4 @@ private static bool CanAccessBlock(EntityPlayer entityPlayer, BlockSelection blo entityPlayer.World.Claims.TryAccess(entityPlayer.Player, blockSel.Position, EnumBlockAccessFlags.BuildOrBreak); } -} \ No newline at end of file +} diff --git a/SmithingPlus/BitsRecovery/CollectibleBehaviorSmeltedContainer.cs b/SmithingPlus/BitsRecovery/CollectibleBehaviorSmeltedContainer.cs index 21afa72..1604b70 100644 --- a/SmithingPlus/BitsRecovery/CollectibleBehaviorSmeltedContainer.cs +++ b/SmithingPlus/BitsRecovery/CollectibleBehaviorSmeltedContainer.cs @@ -5,13 +5,21 @@ namespace SmithingPlus.BitsRecovery; +#nullable enable + public class CollectibleBehaviorSmeltedContainer(CollectibleObject collObj) : CollectibleBehavior(collObj) { public override void GetHeldItemInfo(ItemSlot inSlot, StringBuilder dsc, IWorldAccessor world, bool withDebugInfo) { base.GetHeldItemInfo(inSlot, dsc, world, withDebugInfo); - var temp = inSlot.Itemstack.GetTemperature(world); + ItemStack? itemStack = inSlot?.Itemstack; + if (itemStack == null) + { + return; + } + + float temp = itemStack.GetTemperature(world); if (temp < CollectibleBehaviorScrapeCrucible.MaxScrapeTemperature) dsc.AppendLine(Lang.Get($"{Core.ModId}:heldhelp-scrapecrucible")); } -} \ No newline at end of file +} diff --git a/SmithingPlus/BitsRecovery/HelveHammerRecoveryPatches.cs b/SmithingPlus/BitsRecovery/HelveHammerRecoveryPatches.cs index 7a7b0d2..11be39d 100644 --- a/SmithingPlus/BitsRecovery/HelveHammerRecoveryPatches.cs +++ b/SmithingPlus/BitsRecovery/HelveHammerRecoveryPatches.cs @@ -57,9 +57,14 @@ public static void Postfix(BlockEntityAnvil __instance, ref int __state) return; } - var metalBitStack = metalMaterial.MetalBitStack; + ItemStack? metalBitStack = metalMaterial.MetalBitStack; + if (metalBitStack == null) + { + Core.Logger.VerboseDebug("[BitsRecovery] The resolved metal material has no metal bit item."); + return; + } var temperature = workItemStack.Collectible.GetTemperature(api.World, workItemStack); - metalBitStack?.Collectible.SetTemperature(api.World, metalBitStack, temperature); + metalBitStack.Collectible.SetTemperature(api.World, metalBitStack, temperature); __instance.Api.World.SpawnItemEntity(metalBitStack, __instance.Pos); } -} \ No newline at end of file +} diff --git a/SmithingPlus/CastingTweaks/CastToolPenaltyPatch.cs b/SmithingPlus/CastingTweaks/CastToolPenaltyPatch.cs index e264da0..51d50a8 100644 --- a/SmithingPlus/CastingTweaks/CastToolPenaltyPatch.cs +++ b/SmithingPlus/CastingTweaks/CastToolPenaltyPatch.cs @@ -9,6 +9,8 @@ namespace SmithingPlus.CastingTweaks; +#nullable enable + [UsedImplicitly(ImplicitUseTargetFlags.WithMembers)] [HarmonyPatchCategory(Core.CastingTweaksCategory)] public class CastToolPenaltyPatch @@ -22,6 +24,7 @@ public static void Postfix_GetMoldedStacks(ref ItemStack[] __result, BlockEntity return; foreach (var stack in __result) { + if (stack?.Collectible == null) continue; if (!stack.Collectible.HasBehavior()) continue; stack.Attributes ??= new TreeAttribute(); stack.Attributes.SetBool(ModStackAttributes.CastTool, true); @@ -36,7 +39,8 @@ public static void Postfix_OnCreatedByCrafting( ItemSlot outputSlot, IRecipeBase byRecipe) { - if (outputSlot.Itemstack == null) + ItemStack? outputStack = outputSlot?.Itemstack; + if (outputStack == null || allInputSlots == null) return; var castToolsHeads = allInputSlots .Where(slot => !slot.Empty) @@ -47,8 +51,8 @@ public static void Postfix_OnCreatedByCrafting( .ToArray(); var hasCastToolHead = castToolsHeads.Any(); if (!hasCastToolHead) return; - outputSlot.Itemstack.Attributes ??= new TreeAttribute(); - outputSlot.Itemstack.Attributes.SetBool(ModStackAttributes.CastTool, true); + outputStack.Attributes ??= new TreeAttribute(); + outputStack.Attributes.SetBool(ModStackAttributes.CastTool, true); } [HarmonyPostfix] @@ -60,4 +64,4 @@ public static void Postfix_GetMaxDurability(ref int __result, ItemStack itemstac var reducedDurability = __result * (1 - Core.Config.CastToolDurabilityPenalty); __result = (int)Math.Max(reducedDurability, 1); } -} \ No newline at end of file +} diff --git a/SmithingPlus/CastingTweaks/ToolMoldUnitsPatch.cs b/SmithingPlus/CastingTweaks/ToolMoldUnitsPatch.cs index 3764ca5..75a24df 100644 --- a/SmithingPlus/CastingTweaks/ToolMoldUnitsPatch.cs +++ b/SmithingPlus/CastingTweaks/ToolMoldUnitsPatch.cs @@ -89,14 +89,20 @@ private static ItemStack[] GetMoldedStacksStatic(ICoreAPI api, Block toolMold, I { if (toolMold.Attributes["drop"].Exists) { - var jStack = -#pragma warning disable CS8625 // Cannot convert null literal to non-nullable reference type. - toolMold.Attributes["drop"].AsObject(null, toolMold.Code.Domain); -#pragma warning restore CS8625 // Cannot convert null literal to non-nullable reference type. - if (jStack == null) - return []; - var itemStack = MoldOutputStackFromCode(jStack, api, toolMold, fromMetal); - return itemStack == null ? [] : [itemStack]; + JsonItemStack jStack = toolMold.Attributes["drop"].AsObject( + new JsonItemStack(), toolMold.Code.Domain); + if (jStack?.Code == null) + { + return Array.Empty(); + } + + ItemStack? itemStack = MoldOutputStackFromCode(jStack, api, toolMold, fromMetal); + if (itemStack == null) + { + return Array.Empty(); + } + + return new ItemStack[] { itemStack }; } var jsonItemStackArray = @@ -123,6 +129,11 @@ private static ItemStack[] GetMoldedStacksStatic(ICoreAPI api, Block toolMold, I private static ItemStack? MoldOutputStackFromCode(JsonItemStack jstack, ICoreAPI api, Block toolMold, ItemStack fromMetal) { + if (jstack?.Code == null) + { + return null; + } + var newValue = fromMetal.Collectible.LastCodePart(); jstack.Code.Path = jstack.Code.Path.Replace("{metal}", newValue); jstack.Resolve(api.World, "tool mold drop for " + toolMold.Code); diff --git a/SmithingPlus/ClientTweaks/HandbookInfoPatch.cs b/SmithingPlus/ClientTweaks/HandbookInfoPatch.cs index f7cf62e..6ed53aa 100644 --- a/SmithingPlus/ClientTweaks/HandbookInfoPatch.cs +++ b/SmithingPlus/ClientTweaks/HandbookInfoPatch.cs @@ -4,10 +4,13 @@ using Vintagestory.API.Client; using Vintagestory.API.Common; using Vintagestory.API.Config; +using Vintagestory.API.Datastructures; using Vintagestory.GameContent; namespace SmithingPlus.ClientTweaks; +#nullable enable + [HarmonyPatchCategory(Core.ClientTweaksCategories.HandbookExtraInfo)] public partial class HandbookInfoPatch { @@ -16,7 +19,13 @@ public static ItemStack[] StacksFromCode(ICoreClientAPI capi, ItemStack moldStac { existingMetalVariants = new List(); var stacks = new List(); - foreach (var metalVariant in capi.ModLoader.GetModSystem().metalsByCode.Keys) + SurvivalCoreSystem? survivalCoreSystem = capi.ModLoader.GetModSystem(); + if (survivalCoreSystem?.metalsByCode == null) + { + return stacks.ToArray(); + } + + foreach (string metalVariant in survivalCoreSystem.metalsByCode.Keys) { var stack = GetStackForVariant(capi, moldStack, metalVariant); if (stack == null) continue; @@ -27,12 +36,19 @@ public static ItemStack[] StacksFromCode(ICoreClientAPI capi, ItemStack moldStac return stacks.ToArray(); } - private static ItemStack GetStackForVariant(ICoreClientAPI capi, ItemStack moldStack, string metalVariant) + private static ItemStack? GetStackForVariant(ICoreClientAPI capi, ItemStack moldStack, string metalVariant) { - var mold = moldStack.Collectible; - var jstack = mold.Attributes["drop"]?.AsObject(null, mold.Code.Domain)?.Clone(); + CollectibleObject? mold = moldStack?.Collectible; + if (mold?.Code == null) + { + return null; + } + + JsonObject? dropAttribute = mold.Attributes?["drop"]; + JsonItemStack? jstack = dropAttribute?.AsObject(null, mold.Code.Domain)?.Clone(); if (jstack == null) return null; - var toolVariant = mold.LastCodePart(); + if (jstack.Code == null) return null; + string toolVariant = mold.LastCodePart(); jstack.Code.Path = jstack.Code.Path.Replace("{tooltype}", toolVariant).Replace("{metal}", metalVariant); jstack.Resolve(capi.World, "tool mold drop for " + mold.Code, false); return jstack.ResolvedItemstack; @@ -90,4 +106,4 @@ public static void AddSubHeading( cs => _ = openDetailPageFor(detailpage) ? 1 : 0)); } } -} \ No newline at end of file +} diff --git a/SmithingPlus/Common/CollectibleBehaviorRecycledBit.cs b/SmithingPlus/Common/CollectibleBehaviorRecycledBit.cs index 53f0748..43008f1 100644 --- a/SmithingPlus/Common/CollectibleBehaviorRecycledBit.cs +++ b/SmithingPlus/Common/CollectibleBehaviorRecycledBit.cs @@ -18,14 +18,14 @@ public override void OnCreatedByCrafting( ref EnumHandling bhHandling) { base.OnCreatedByCrafting(allInputSlots, outputSlot, byRecipe, ref bhHandling); - if (outputSlot?.Itemstack == null || - allInputSlots == null) + ItemStack outputStack = outputSlot?.Itemstack; + if (outputStack == null || allInputSlots == null || byRecipe?.RecipeIngredients == null) return; // Identify recipe tools from ingredients var toolIngredients = byRecipe.RecipeIngredients .Where(ing => - ing.ConsumeProperties is { Consume: false, DurabilityCost: > 0 } || + ing?.ConsumeProperties is { Consume: false, DurabilityCost: > 0 } || ing?.RecipeAttributes?[ModRecipeAttributes.RecyclingRecipe]?.AsBool() == true) .ToArray() ?? []; @@ -49,7 +49,8 @@ public override void OnCreatedByCrafting( 0; // Use this NOT stack.StackSize because that could have more items than the recipe requires foreach (var ingredient in byRecipe.RecipeIngredients) { - if (!ingredient.SatisfiesAsIngredient(stack) || ingredient.ResolvedItemStack == null) + if (ingredient == null || !ingredient.SatisfiesAsIngredient(stack) || + ingredient.ResolvedItemStack == null) continue; consumedStackSize = ingredient.ResolvedItemStack.StackSize; break; @@ -88,12 +89,12 @@ public override void OnCreatedByCrafting( // Scale output stack size by VoxelsPerBit var bits = Math.Max((int)(totalVoxels / Core.Config.VoxelsPerBit), 1); - outputSlot.Itemstack.StackSize = bits; - outputSlot.Itemstack.Collectible.SetTemperature(Api.World, outputSlot.Itemstack, temperature); + outputStack.StackSize = bits; + outputStack.Collectible.SetTemperature(Api.World, outputStack, temperature); } private static bool IsToolStack(ItemStack stack, IRecipeIngredient[] toolIngredients) { return stack != null && toolIngredients.Any(ing => ing?.SatisfiesAsIngredient(stack) == true); } -} \ No newline at end of file +} diff --git a/SmithingPlus/Common/Metal/MetalMaterialExtensions.cs b/SmithingPlus/Common/Metal/MetalMaterialExtensions.cs index 32efffe..7358f9c 100644 --- a/SmithingPlus/Common/Metal/MetalMaterialExtensions.cs +++ b/SmithingPlus/Common/Metal/MetalMaterialExtensions.cs @@ -80,16 +80,28 @@ private static bool TryGetMetalMaterial(IEnumerable gridRecipes, Func materialResolver, out MetalMaterial? metalMaterial) { metalMaterial = null; - foreach (var gridRecipe in gridRecipes) + foreach (GridRecipe gridRecipe in gridRecipes) { - var ingredients = - from ing in gridRecipe.RecipeIngredients - where ing is { ResolvedItemStack: not null, ConsumeProperties.Consume: false } || ing.ConsumeProperties.DurabilityCost == 0 && - ing.ResolvedItemStack?.Collectible != null - select ing.ResolvedItemStack?.Collectible; - foreach (var ingredient in ingredients) + if (gridRecipe == null || gridRecipe.RecipeIngredients == null) { - if (ingredient == null) continue; + continue; + } + + foreach (CraftingRecipeIngredient ingredientDefinition in gridRecipe.RecipeIngredients) + { + if (ingredientDefinition == null || ingredientDefinition.ResolvedItemStack?.Collectible == null) + { + continue; + } + + if (ingredientDefinition.ConsumeProperties == null || + (ingredientDefinition.ConsumeProperties.Consume && + ingredientDefinition.ConsumeProperties.DurabilityCost != 0)) + { + continue; + } + + CollectibleObject ingredient = ingredientDefinition.ResolvedItemStack.Collectible; metalMaterial = materialResolver(ingredient); if (metalMaterial != null) return true; } @@ -120,7 +132,7 @@ private static bool TryGetMetalMaterialFromIngredients(ICoreAPI api, IEnumerable MetalMaterial? metalMaterial = null; foreach (var recipe in smithingRecipes) { - var ingredient = recipe.Output.ResolvedItemstack?.Collectible; + var ingredient = recipe?.Output?.ResolvedItemstack?.Collectible; if (ingredient == null) continue; var variantCode = ingredient.GetMetalVariant(); metalMaterial = MetalMaterialLoader.GetMaterial(api, variantCode); @@ -150,7 +162,11 @@ public static bool HasMetalMaterialSimple(this CollectibleObject collObj) { var collObj = itemStack.Collectible; if (collObj is not IAnvilWorkable anvilWorkable) return collObj?.GetOrCacheMetalMaterial(api); - var ingotStack = anvilWorkable.GetBaseMaterial(itemStack); + ItemStack? ingotStack = anvilWorkable.GetBaseMaterial(itemStack); + if (ingotStack?.Collectible == null) + { + return collObj.GetOrCacheMetalMaterial(api); + } var metalMaterial = ingotStack.Collectible.GetOrCacheMetalMaterial(api); return metalMaterial ?? collObj.GetOrCacheMetalMaterial(api); } @@ -162,10 +178,14 @@ public static bool HasMetalMaterialSimple(this CollectibleObject collObj) // Resort to the CollectibleObject method for items that are not anvil workable if (collObj is not IAnvilWorkable anvilWorkable) return collObj?.GetOrCacheMetalMaterial(api); // Grab from IAnvilWorkable - var ingotStack = anvilWorkable.GetBaseMaterial(itemStack); + ItemStack? ingotStack = anvilWorkable.GetBaseMaterial(itemStack); + if (ingotStack?.Collectible == null) + { + return collObj.GetMetalMaterialProcessed(api) ?? collObj.GetOrCacheMetalMaterial(api); + } // Try to grab the processed material from the ingot stack return ingotStack.Collectible.GetMetalMaterialProcessed(api); } #endregion -} \ No newline at end of file +} diff --git a/SmithingPlus/Config/ConfigLoader.cs b/SmithingPlus/Config/ConfigLoader.cs index a5cd07f..844c16e 100644 --- a/SmithingPlus/Config/ConfigLoader.cs +++ b/SmithingPlus/Config/ConfigLoader.cs @@ -9,8 +9,8 @@ public class ConfigLoader : ModSystem { private const string ServerConfigName = "SmithingPlus.json"; private const string ClientConfigName = "SmithingPlusClient.json"; - public static ServerConfig Config { get; private set; } - public static ClientConfig CConfig { get; private set; } + public static ServerConfig Config { get; private set; } = new ServerConfig(); + public static ClientConfig CConfig { get; private set; } = new ClientConfig(); public override double ExecuteOrder() { @@ -81,8 +81,8 @@ public override void Start(ICoreAPI api) public override void Dispose() { - Config = null; - CConfig = null; + Config = new ServerConfig(); + CConfig = new ClientConfig(); base.Dispose(); } } diff --git a/SmithingPlus/Core.cs b/SmithingPlus/Core.cs index 8d74a7f..264a17d 100644 --- a/SmithingPlus/Core.cs +++ b/SmithingPlus/Core.cs @@ -1,4 +1,5 @@ -using System.Linq; +using System; +using System.Linq; using HarmonyLib; using JetBrains.Annotations; using SmithingPlus.BitsRecovery; @@ -22,19 +23,58 @@ namespace SmithingPlus; [UsedImplicitly(ImplicitUseKindFlags.InstantiatedNoFixedConstructorSignature)] public partial class Core : ModSystem { + private static ILogger logger; + private static ICoreAPI coreApi; + private static Harmony harmonyInstance; + public const string ModId = "smithingplus"; - public static ILogger Logger { get; private set; } - public static ICoreAPI Api { get; private set; } - public static Harmony HarmonyInstance { get; private set; } + public static ILogger Logger + { + get + { + if (logger == null) + { + throw new InvalidOperationException("SmithingPlus logging is unavailable outside the mod lifecycle."); + } + + return logger; + } + } + + public static ICoreAPI Api + { + get + { + if (coreApi == null) + { + throw new InvalidOperationException("SmithingPlus API access is unavailable outside the mod lifecycle."); + } + + return coreApi; + } + } + + public static Harmony HarmonyInstance + { + get + { + if (harmonyInstance == null) + { + throw new InvalidOperationException("SmithingPlus Harmony patches have not been initialized."); + } + + return harmonyInstance; + } + } public static ServerConfig LocalConfig => ConfigLoader.Config; public static ClientConfig CConfig => ConfigLoader.CConfig; - public static ServerConfig Config { get; private set; } + public static ServerConfig Config { get; private set; } = new ServerConfig(); public static bool OnlyEnableClientside { get; private set; } = false; public override void StartPre(ICoreAPI api) { - Logger = Mod.Logger; - Api = api; + logger = Mod.Logger; + coreApi = api; } public override void Start(ICoreAPI api) @@ -141,12 +181,12 @@ public override void AssetsFinalize(ICoreAPI api) var ingotRecipe = api.ModLoader.GetModSystem().SmithingRecipes .FirstOrDefault(r => r.Ingredient?.Code?.Equals(ingotCode) == true && - r.Output.ResolvedItemstack?.Collectible.Code.Equals(ingotCode) == true); + r?.Output?.ResolvedItemstack?.Collectible?.Code?.Equals(ingotCode) == true); if (ingotRecipe?.Ingredient == null) continue; if (!WildcardUtil.Match(Config.IngotSelector, collObj.Code.ToString())) continue; if (api.ModLoader.GetModSystem().SmithingRecipes .Any(r => r.Ingredient?.Code?.Equals(collObj.Code) == true && - r.Output.ResolvedItemstack?.Collectible.Code.Equals(collObj.Code) == true)) continue; + r?.Output?.ResolvedItemstack?.Collectible?.Code?.Equals(collObj.Code) == true)) continue; Logger.VerboseDebug($"Adding workable-only ingot recipe for {collObj.Code}"); var newRecipe = new SmithingRecipe { @@ -176,14 +216,14 @@ public override void AssetsFinalize(ICoreAPI api) private static void Patch() { - if (HarmonyInstance != null) return; + if (harmonyInstance != null) return; - HarmonyInstance = new Harmony(ModId); + harmonyInstance = new Harmony(ModId); Logger.VerboseDebug("Patching..."); AlwaysPatchCategory.PatchIfEnabled(true); ToolRecoveryCategory.PatchIfEnabled(Config.EnableToolRecovery); SmithingRecipeAttributesPatch.PatchIfEnabled( - Config.SmithWithBits || Config.BitsTopUp || Config.EnableToolRecovery, HarmonyInstance); + Config.SmithWithBits || Config.BitsTopUp || Config.EnableToolRecovery, harmonyInstance); ClientTweaksCategories.RememberHammerToolMode.PatchIfEnabled(CConfig.RememberHammerToolMode); ClientTweaksCategories.AnvilShowRecipeVoxels.PatchIfEnabled(CConfig.AnvilShowRecipeVoxels); @@ -200,16 +240,16 @@ private static void Patch() private static void Unpatch() { - Logger?.VerboseDebug("Unpatching..."); - HarmonyInstance?.UnpatchAll(ModId); - HarmonyInstance = null; + logger?.VerboseDebug("Unpatching..."); + harmonyInstance?.UnpatchAll(ModId); + harmonyInstance = null; } public override void Dispose() { Unpatch(); - Logger = null; - Api = null; + logger = null; + coreApi = null; base.Dispose(); } } diff --git a/SmithingPlus/SmithWithBits/SmithingRecipeAttributesPatch.cs b/SmithingPlus/SmithWithBits/SmithingRecipeAttributesPatch.cs index a5a1bc7..6875bf0 100644 --- a/SmithingPlus/SmithWithBits/SmithingRecipeAttributesPatch.cs +++ b/SmithingPlus/SmithWithBits/SmithingRecipeAttributesPatch.cs @@ -16,22 +16,29 @@ public class SmithingRecipeAttributesPatch public static void GetMatchingRecipes_Postfix(IAnvilWorkable __instance, ref List __result, ItemStack stack) { + if (__instance == null || __result == null) + { + __result = new List(); + return; + } + if (__instance is ItemWorkItem) return; // Return for existing work item if (__instance is not CollectibleBehaviorWorkableNugget) __result = __result.Where(r => - r.Ingredient.RecipeAttributes?[ModRecipeAttributes.NuggetRecipe]?.AsBool() != true + r?.Ingredient?.RecipeAttributes?[ModRecipeAttributes.NuggetRecipe]?.AsBool() != true ).ToList(); if (__instance is not CollectibleBehaviorAnvilWorkable) __result = __result.Where(r => - r.Ingredient.RecipeAttributes?[ModRecipeAttributes.WorkableRecipe]?.AsBool() != true + r?.Ingredient?.RecipeAttributes?[ModRecipeAttributes.WorkableRecipe]?.AsBool() != true ).ToList(); - __result = __result.Where(r => r.Ingredient.RecipeAttributes?[ModRecipeAttributes.RepairOnly]?.AsBool() != true + __result = __result.Where(r => r?.Ingredient?.RecipeAttributes?[ModRecipeAttributes.RepairOnly]?.AsBool() != true ).ToList(); } public static void PatchIfEnabled(bool condition, Harmony harmony) { if (!condition) return; + if (harmony == null) throw new ArgumentNullException(nameof(harmony)); var interfaceType = typeof(IAnvilWorkable); // Look through all loaded assemblies and their types @@ -62,11 +69,17 @@ public static void PatchIfEnabled(bool condition, Harmony harmony) var target = method.IsVirtual ? method.GetBaseDefinition() : method; - var postfix = - new HarmonyMethod(typeof(SmithingRecipeAttributesPatch).GetMethod(nameof(GetMatchingRecipes_Postfix))); + MethodInfo postfixMethod = typeof(SmithingRecipeAttributesPatch).GetMethod(nameof(GetMatchingRecipes_Postfix)); + if (postfixMethod == null) + { + throw new MissingMethodException(typeof(SmithingRecipeAttributesPatch).FullName, + nameof(GetMatchingRecipes_Postfix)); + } + + var postfix = new HarmonyMethod(postfixMethod); // Apply Harmony patch to it if (seen.Add((target.Module, target.MetadataToken))) harmony.Patch(target, postfix: postfix); } } -} \ No newline at end of file +} diff --git a/SmithingPlus/StoneSmithing/ItemStoneHammer.cs b/SmithingPlus/StoneSmithing/ItemStoneHammer.cs index cc8bbeb..5f71644 100644 --- a/SmithingPlus/StoneSmithing/ItemStoneHammer.cs +++ b/SmithingPlus/StoneSmithing/ItemStoneHammer.cs @@ -101,7 +101,7 @@ private static Dictionary GetVoxelHitCounts(ItemStack stack) { var byteArray = stack.TempAttributes.GetBytes("sp:voxelHitCounts", Array.Empty()); var hitCounts = new Dictionary(); - for (var i = 0; i < byteArray.Length; i += 2) + for (var i = 0; i + 1 < byteArray.Length; i += 2) { int selectionBoxIndex = byteArray[i]; int hitCount = byteArray[i + 1]; @@ -127,4 +127,4 @@ public static void InvokeOnUseOver(this BlockEntityAnvil blockEntityAnvil, IPlay if (onUseOverMethod != null) onUseOverMethod.Invoke(blockEntityAnvil, new object[] { player, selectionBoxIndex }); } -} \ No newline at end of file +} diff --git a/SmithingPlus/ToolRecovery/CollectibleBehaviorBrokenToolHead.cs b/SmithingPlus/ToolRecovery/CollectibleBehaviorBrokenToolHead.cs index 639f53a..149690e 100644 --- a/SmithingPlus/ToolRecovery/CollectibleBehaviorBrokenToolHead.cs +++ b/SmithingPlus/ToolRecovery/CollectibleBehaviorBrokenToolHead.cs @@ -12,6 +12,8 @@ namespace SmithingPlus.ToolRecovery; +#nullable enable + [HarmonyPatch(typeof(ItemWorkItem))] [HarmonyPatchCategory(Core.ToolRecoveryCategory)] public class CollectibleBehaviorBrokenToolHead(CollectibleObject collObj) : CollectibleBehaviorRepairableTool(collObj) @@ -33,8 +35,8 @@ public override void GetHeldItemName(StringBuilder dsc, ItemStack itemStack) () => { Core.Logger.VerboseDebug("Storing recipe output name: {0}", recipeId); - return Core.Api.GetSmithingRecipes().FirstOrDefault(r => r.RecipeId == recipeId)?.Output - .ResolvedItemstack.GetName(); + SmithingRecipe? recipe = Core.Api.GetSmithingRecipes().FirstOrDefault(r => r.RecipeId == recipeId); + return recipe?.Output?.ResolvedItemstack?.GetName(); }); dsc.Clear(); dsc.AppendLine(toolName == null @@ -44,9 +46,10 @@ public override void GetHeldItemName(StringBuilder dsc, ItemStack itemStack) public override void GetHeldItemInfo(ItemSlot inSlot, StringBuilder dsc, IWorldAccessor world, bool withDebugInfo) { - if (!IsBrokenToolHead(inSlot.Itemstack)) return; + ItemStack? itemStack = inSlot?.Itemstack; + if (itemStack == null || !IsBrokenToolHead(itemStack)) return; if (world.Api is not ICoreClientAPI) return; - var brokenCount = inSlot.Itemstack.GetBrokenCount(); + int brokenCount = itemStack.GetBrokenCount(); if (brokenCount <= 0) return; if (Core.CConfig.ShowBrokenCount) dsc.AppendLine(Lang.Get($"{LangKey} {{0}} times", brokenCount)); if (Core.Config.DontRepairBrokenToolHeads) dsc.AppendLine(Lang.Get($"{Core.ModId}:itemdesc-needschiseling")); @@ -61,7 +64,8 @@ public static void Postfix_GetHeldItemInfo( IWorldAccessor world, bool withDebugInfo) { - if (!IsBrokenToolHead(inSlot.Itemstack)) return; + ItemStack? itemStack = inSlot?.Itemstack; + if (itemStack == null || !IsBrokenToolHead(itemStack)) return; // Remove lines containing the respective language entries var unknownWorkItem = Lang.Get("Unknown work item"); var unfinished = $"@(.*){Lang.Get("Unfinished {0}", "(.*)")}(.*)"; @@ -72,4 +76,4 @@ public static void Postfix_GetHeldItemInfo( if (!line.Contains(unknownWorkItem) && !WildcardUtil.Match(unfinished, line)) dsc.AppendLine(line); } -} \ No newline at end of file +} diff --git a/SmithingPlus/ToolRecovery/ItemDamagedPatches.cs b/SmithingPlus/ToolRecovery/ItemDamagedPatches.cs index d68a4c8..c2d0260 100644 --- a/SmithingPlus/ToolRecovery/ItemDamagedPatches.cs +++ b/SmithingPlus/ToolRecovery/ItemDamagedPatches.cs @@ -10,6 +10,8 @@ namespace SmithingPlus.ToolRecovery; +#nullable enable + [UsedImplicitly(ImplicitUseTargetFlags.WithMembers)] [HarmonyPatch(typeof(CollectibleObject))] [HarmonyPatchCategory(Core.ToolRecoveryCategory)] @@ -23,8 +25,9 @@ public static void Postfix_OnCreatedByCrafting( ItemSlot outputSlot, IRecipeBase byRecipe) { - if (outputSlot.Itemstack == null) return; - var brokenStack = allInputSlots.FirstOrDefault(slot => + ItemStack? outputStack = outputSlot?.Itemstack; + if (outputStack == null || allInputSlots == null || byRecipe == null) return; + ItemStack? brokenStack = allInputSlots.FirstOrDefault(slot => slot.Itemstack?.GetBrokenCount() > 0 && slot.Itemstack?.Collectible.HasBehavior() == true )?.Itemstack; @@ -34,9 +37,10 @@ public static void Postfix_OnCreatedByCrafting( if (brokenStack.Item?.IsRepairableTool() is not true) return; var repairedStack = brokenStack.GetRepairedToolStack(); if (repairedStack == null) return; - repairedStack.ResolveBlockOrItem((allInputSlots.FirstOrDefault()?.Inventory?.Api ?? Core.Api) - .World); - if (repairedStack.Collectible.Code != byRecipe.RecipeOutput.ResolvedItemStack?.Collectible.Code) return; + ICoreAPI? coreApi = allInputSlots.FirstOrDefault()?.Inventory?.Api ?? Core.Api; + if (coreApi == null) return; + repairedStack.ResolveBlockOrItem(coreApi.World); + if (repairedStack.Collectible?.Code != byRecipe.RecipeOutput?.ResolvedItemStack?.Collectible?.Code) return; foreach (var attributeKey in Core.Config.GetToolRepairForgettableAttributes) repairedStack.Attributes?.RemoveAttribute(attributeKey); var repairSmith = brokenStack.GetRepairSmith(); @@ -47,7 +51,8 @@ public static void Postfix_OnCreatedByCrafting( if (toolRepairPenaltyModifier != 0) repairedStack.SetToolRepairPenaltyModifier(toolRepairPenaltyModifier); var repairedAttributes = repairedStack.Attributes ?? new TreeAttribute(); - var outputAttributes = outputSlot.Itemstack.Attributes; + ITreeAttribute outputAttributes = outputStack.Attributes ?? new TreeAttribute(); + outputStack.Attributes = outputAttributes; foreach (var attribute in repairedAttributes) outputAttributes[attribute.Key] = attribute.Value; } @@ -65,23 +70,31 @@ private static void Prefix_DamageItem( return; if (!destroyOnZeroDurability) return; - var durability = itemSlot?.Itemstack?.GetRemainingDurability(); + ItemStack? itemStack = itemSlot?.Itemstack; + int? durability = itemStack?.GetRemainingDurability(); if (!durability.HasValue || durability > amount) return; - if (itemSlot.Itemstack?.Collectible.HasBehavior() != true) return; + if (itemStack?.Collectible?.HasBehavior() != true) return; Core.Logger.VerboseDebug("Broken tool in InventoryID: {0}, Entity: {1}", itemSlot.Inventory?.InventoryID, byEntity.GetName()); var entityPlayer = byEntity as EntityPlayer; - var itemStack = itemSlot.Itemstack; - var toolCode = itemStack?.Collectible.Code.ToString(); - var smithingRecipe = CacheHelper.GetOrAdd(Core.ToolToRecipeCache, toolCode, - () => GetHeadSmithingRecipe(world.Api, itemStack)); + string? toolCode = itemStack.Collectible.Code?.ToString(); + if (toolCode == null) return; + SmithingRecipe? smithingRecipe; + if (!Core.ToolToRecipeCache.TryGetValue(toolCode, out smithingRecipe)) + { + smithingRecipe = GetHeadSmithingRecipe(world.Api, itemStack); + if (smithingRecipe != null) + { + Core.ToolToRecipeCache[toolCode] = smithingRecipe; + } + } if (smithingRecipe == null) { Core.Logger.VerboseDebug("Head or tool smithing recipe not found for: {0}", toolCode); return; } - var metalMaterial = itemStack?.GetOrCacheMetalMaterial(byEntity.Api); + var metalMaterial = itemStack.GetOrCacheMetalMaterial(byEntity.Api); var workItem = metalMaterial?.WorkItem; if (workItem is null) { @@ -93,12 +106,19 @@ private static void Prefix_DamageItem( Core.Logger.VerboseDebug("Found work item: {0}", workItem.Code); var wItemStack = new ItemStack(workItem); - Core.Logger.VerboseDebug("Found smithing recipe: {0}", - smithingRecipe.Output.ResolvedItemstack.Collectible.Code); - var byteVoxels = ByteVoxelsFromRecipe(smithingRecipe, smithingRecipe.Output.ResolvedItemstack.StackSize); + ItemStack? recipeOutputStack = smithingRecipe.Output?.ResolvedItemstack; + if (recipeOutputStack?.Collectible?.Code == null) + { + Core.Logger.VerboseDebug("The smithing recipe has no resolved output for: {0}", toolCode); + return; + } + + Core.Logger.VerboseDebug("Found smithing recipe: {0}", recipeOutputStack.Collectible.Code); + byte[,,] byteVoxels = ByteVoxelsFromRecipe(smithingRecipe, recipeOutputStack.StackSize); wItemStack.Attributes.SetBytes("voxels", BlockEntityAnvil.serializeVoxels(byteVoxels)); wItemStack.Attributes.SetInt("selectedRecipeId", smithingRecipe.RecipeId); - var cloneStack = itemStack?.Clone(); + ItemStack? cloneStack = itemStack.Clone(); + if (cloneStack == null) return; cloneStack.CloneBrokenCount(itemStack, 1); wItemStack.SetRepairedToolStack(cloneStack); @@ -110,11 +130,10 @@ private static void Prefix_DamageItem( itemSlot.MarkDirty(); } - private static SmithingRecipe GetHeadSmithingRecipe(ICoreAPI api, ItemStack itemStack) + private static SmithingRecipe? GetHeadSmithingRecipe(ICoreAPI api, ItemStack itemStack) { - var toolHead = GetToolHead(api, itemStack); - var smithingRecipe = toolHead.GetSmithingRecipe(api); - return smithingRecipe; + ItemStack toolHead = GetToolHead(api, itemStack); + return toolHead.GetSmithingRecipe(api); } private static ItemStack GetToolHead(ICoreAPI api, ItemStack itemStack) @@ -148,4 +167,4 @@ private static ItemStack GetToolHead(ICoreAPI api, ItemStack itemStack) var byteVoxels = recipeVoxels.ErodeToPercentage(Core.Config.BrokenToolVoxelPercent); return byteVoxels; } -} \ No newline at end of file +} diff --git a/SmithingPlus/Util/CollectibleExtensions.cs b/SmithingPlus/Util/CollectibleExtensions.cs index 16a82bd..7496933 100644 --- a/SmithingPlus/Util/CollectibleExtensions.cs +++ b/SmithingPlus/Util/CollectibleExtensions.cs @@ -90,7 +90,7 @@ public static bool MatchesToolHeadSelector(this CollectibleObject collObj, bool foreach (var recipe in api.ModLoader.GetModSystem().SmithingRecipes) { var code = recipe?.Output?.ResolvedItemstack?.Collectible?.Code; - if (code != null) dict.TryAdd(code, recipe!); + if (code != null && recipe != null) dict.TryAdd(code, recipe); } return dict; @@ -172,6 +172,17 @@ public static T GetBehavior(this CollectibleObject collObj, bool withInherita public static CollectibleBehaviorQuenchable.MetalPropertyVariant? GetMetalProps( this CollectibleBehaviorQuenchable behavior) { - return behavior?.GetField("metalProps"); + if (behavior == null) + { + return null; + } + + CollectibleBehaviorQuenchable.MetalPropertyVariant? metalProperties; + if (behavior.TryGetField("metalProps", out metalProperties)) + { + return metalProperties; + } + + return null; } } diff --git a/SmithingPlus/Util/HammerExtensions.cs b/SmithingPlus/Util/HammerExtensions.cs index 887b097..7578fac 100644 --- a/SmithingPlus/Util/HammerExtensions.cs +++ b/SmithingPlus/Util/HammerExtensions.cs @@ -1,3 +1,4 @@ +using System; using Vintagestory.API.Common; namespace SmithingPlus.Util; @@ -16,6 +17,16 @@ public enum HammerToolMode public static HammerToolMode GetHammerToolMode(this ItemSlot hotbarSlot, IPlayer byPlayer, BlockSelection blockSel) { + if (hotbarSlot == null) + { + throw new ArgumentNullException(nameof(hotbarSlot)); + } + + if (hotbarSlot.Itemstack?.Collectible == null) + { + throw new InvalidOperationException("A hammer tool mode cannot be read from an empty slot."); + } + return (HammerToolMode)hotbarSlot.Itemstack.Collectible.GetToolMode(hotbarSlot, byPlayer, blockSel); } -} \ No newline at end of file +} diff --git a/SmithingPlus/Util/ReflectionExtensions.cs b/SmithingPlus/Util/ReflectionExtensions.cs index e9d5220..bf1e1ba 100644 --- a/SmithingPlus/Util/ReflectionExtensions.cs +++ b/SmithingPlus/Util/ReflectionExtensions.cs @@ -3,24 +3,53 @@ namespace SmithingPlus.Util; +#nullable enable + public static class ReflectionExtensions { public static T GetField(this object obj, string fieldName) { if (obj == null) throw new ArgumentNullException(nameof(obj)); - var fi = obj.GetType().GetField(fieldName, BindingFlags.NonPublic | BindingFlags.Instance); - if (fi == null) return default; + FieldInfo? fieldInfo = obj.GetType().GetField(fieldName, BindingFlags.NonPublic | BindingFlags.Instance); + if (fieldInfo == null) + { + throw new MissingFieldException(obj.GetType().FullName, fieldName); + } + + object? fieldValue = fieldInfo.GetValue(obj); + if (fieldValue is T typedValue) + { + return typedValue; + } + + throw new InvalidCastException("The reflected field is null or has an unexpected type."); + } + + public static bool TryGetField(this object obj, string fieldName, out T? fieldValue) + { + if (obj == null) + { + throw new ArgumentNullException(nameof(obj)); + } + + FieldInfo? fieldInfo = obj.GetType().GetField(fieldName, BindingFlags.NonPublic | BindingFlags.Instance); + if (fieldInfo != null && fieldInfo.GetValue(obj) is T typedValue) + { + fieldValue = typedValue; + return true; + } - return (T)fi.GetValue(obj); + fieldValue = default; + return false; } public static void SetField(this object obj, string fieldName, T newValue) { if (obj == null) throw new ArgumentNullException(nameof(obj)); - var fi = obj.GetType().GetField(fieldName, BindingFlags.NonPublic | BindingFlags.Instance); - if (fi == null) throw new InvalidOperationException($"Field '{fieldName}' not found."); + FieldInfo? fi = obj.GetType().GetField(fieldName, BindingFlags.NonPublic | BindingFlags.Instance); + if (fi == null) throw new MissingFieldException(obj.GetType().FullName, fieldName); fi.SetValue(obj, newValue); } @@ -29,21 +58,27 @@ public static T GetInternalField(this object obj, string fieldName) { if (obj == null) throw new ArgumentNullException(nameof(obj)); - var fi = obj.GetType().GetField(fieldName, + FieldInfo? fi = obj.GetType().GetField(fieldName, BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.FlattenHierarchy); - if (fi == null) return default; + if (fi == null) throw new MissingFieldException(obj.GetType().FullName, fieldName); + + object? fieldValue = fi.GetValue(obj); + if (fieldValue is T typedValue) + { + return typedValue; + } - return (T)fi.GetValue(obj); + throw new InvalidCastException("The reflected field is null or has an unexpected type."); } public static void SetInternalField(this object obj, string fieldName, T newValue) { if (obj == null) throw new ArgumentNullException(nameof(obj)); - var fi = obj.GetType().GetField(fieldName, + FieldInfo? fi = obj.GetType().GetField(fieldName, BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.FlattenHierarchy); - if (fi == null) throw new InvalidOperationException($"Field '{fieldName}' not found."); + if (fi == null) throw new MissingFieldException(obj.GetType().FullName, fieldName); fi.SetValue(obj, newValue); } -} \ No newline at end of file +} From bfb8e2ca589515131fdf3c9324e561ca5a46ab87 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C6=9B=CA=91=CA=8B=C9=8D=C9=9B=CF=AF=E1=BE=B0=C9=A8?= <6170786+AzureTai@users.noreply.github.com> Date: Wed, 19 Aug 2026 05:20:50 +0100 Subject: [PATCH 11/13] Correct Vintage Story nullable API contracts Treat RecipeIngredientConsumeProperties as the non-null value supplied by the Vintage Story API, retain the resolved ingredient stack in a validated local, and match the nullable ItemSlot signature of CollectibleBehavior.GetHeldItemInfo. --- .../CollectibleBehaviorSmeltedContainer.cs | 2 +- .../Common/Metal/MetalMaterialExtensions.cs | 17 +++++++++++------ 2 files changed, 12 insertions(+), 7 deletions(-) diff --git a/SmithingPlus/BitsRecovery/CollectibleBehaviorSmeltedContainer.cs b/SmithingPlus/BitsRecovery/CollectibleBehaviorSmeltedContainer.cs index 1604b70..d78dd59 100644 --- a/SmithingPlus/BitsRecovery/CollectibleBehaviorSmeltedContainer.cs +++ b/SmithingPlus/BitsRecovery/CollectibleBehaviorSmeltedContainer.cs @@ -9,7 +9,7 @@ namespace SmithingPlus.BitsRecovery; public class CollectibleBehaviorSmeltedContainer(CollectibleObject collObj) : CollectibleBehavior(collObj) { - public override void GetHeldItemInfo(ItemSlot inSlot, StringBuilder dsc, IWorldAccessor world, bool withDebugInfo) + public override void GetHeldItemInfo(ItemSlot? inSlot, StringBuilder dsc, IWorldAccessor world, bool withDebugInfo) { base.GetHeldItemInfo(inSlot, dsc, world, withDebugInfo); ItemStack? itemStack = inSlot?.Itemstack; diff --git a/SmithingPlus/Common/Metal/MetalMaterialExtensions.cs b/SmithingPlus/Common/Metal/MetalMaterialExtensions.cs index 7358f9c..a3d4ce2 100644 --- a/SmithingPlus/Common/Metal/MetalMaterialExtensions.cs +++ b/SmithingPlus/Common/Metal/MetalMaterialExtensions.cs @@ -89,20 +89,25 @@ private static bool TryGetMetalMaterial(IEnumerable gridRecipes, foreach (CraftingRecipeIngredient ingredientDefinition in gridRecipe.RecipeIngredients) { - if (ingredientDefinition == null || ingredientDefinition.ResolvedItemStack?.Collectible == null) + if (ingredientDefinition == null) { continue; } - if (ingredientDefinition.ConsumeProperties == null || - (ingredientDefinition.ConsumeProperties.Consume && - ingredientDefinition.ConsumeProperties.DurabilityCost != 0)) + ItemStack? resolvedIngredientStack = ingredientDefinition.ResolvedItemStack; + CollectibleObject? ingredientCollectible = resolvedIngredientStack?.Collectible; + if (ingredientCollectible == null) { continue; } - CollectibleObject ingredient = ingredientDefinition.ResolvedItemStack.Collectible; - metalMaterial = materialResolver(ingredient); + if (ingredientDefinition.ConsumeProperties.Consume && + ingredientDefinition.ConsumeProperties.DurabilityCost != 0) + { + continue; + } + + metalMaterial = materialResolver(ingredientCollectible); if (metalMaterial != null) return true; } } From 9c36ac4fdbe9e4c0efce31b8f0c59824dbf6395e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C6=9B=CA=91=CA=8B=C9=8D=C9=9B=CF=AF=E1=BE=B0=C9=A8?= <6170786+AzureTai@users.noreply.github.com> Date: Wed, 19 Aug 2026 05:22:55 +0100 Subject: [PATCH 12/13] Complete nullable tool recovery guards Match the nullable ItemSlot contract for broken-tool item information and reject missing Harmony DamageItem context before accessing the entity or inventory slot. --- .../ToolRecovery/CollectibleBehaviorBrokenToolHead.cs | 2 +- SmithingPlus/ToolRecovery/ItemDamagedPatches.cs | 5 +++++ 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/SmithingPlus/ToolRecovery/CollectibleBehaviorBrokenToolHead.cs b/SmithingPlus/ToolRecovery/CollectibleBehaviorBrokenToolHead.cs index 149690e..a38381f 100644 --- a/SmithingPlus/ToolRecovery/CollectibleBehaviorBrokenToolHead.cs +++ b/SmithingPlus/ToolRecovery/CollectibleBehaviorBrokenToolHead.cs @@ -44,7 +44,7 @@ public override void GetHeldItemName(StringBuilder dsc, ItemStack itemStack) : Lang.Get($"{Core.ModId}:Broken {{0}}", toolName.ToLower())); } - public override void GetHeldItemInfo(ItemSlot inSlot, StringBuilder dsc, IWorldAccessor world, bool withDebugInfo) + public override void GetHeldItemInfo(ItemSlot? inSlot, StringBuilder dsc, IWorldAccessor world, bool withDebugInfo) { ItemStack? itemStack = inSlot?.Itemstack; if (itemStack == null || !IsBrokenToolHead(itemStack)) return; diff --git a/SmithingPlus/ToolRecovery/ItemDamagedPatches.cs b/SmithingPlus/ToolRecovery/ItemDamagedPatches.cs index c2d0260..5984e28 100644 --- a/SmithingPlus/ToolRecovery/ItemDamagedPatches.cs +++ b/SmithingPlus/ToolRecovery/ItemDamagedPatches.cs @@ -66,6 +66,11 @@ private static void Prefix_DamageItem( int amount = 1, bool destroyOnZeroDurability = true) { + if (world == null || byEntity == null || itemSlot == null) + { + return; + } + if (world.Api.Side.IsClient()) return; if (!destroyOnZeroDurability) From fba6d79ec0ba3fc80c58322bd4ede9ea5da2cb96 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C6=9B=CA=91=CA=8B=C9=8D=C9=9B=CF=AF=E1=BE=B0=C9=A8?= <6170786+AzureTai@users.noreply.github.com> Date: Wed, 19 Aug 2026 05:27:54 +0100 Subject: [PATCH 13/13] Declare nullable Harmony damage context Declare the Harmony-supplied world, entity, and item slot as nullable and validate all three before use. This gives nullable flow analysis the same contract enforced at runtime. --- SmithingPlus/ToolRecovery/ItemDamagedPatches.cs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/SmithingPlus/ToolRecovery/ItemDamagedPatches.cs b/SmithingPlus/ToolRecovery/ItemDamagedPatches.cs index 5984e28..9e4295d 100644 --- a/SmithingPlus/ToolRecovery/ItemDamagedPatches.cs +++ b/SmithingPlus/ToolRecovery/ItemDamagedPatches.cs @@ -60,9 +60,9 @@ public static void Postfix_OnCreatedByCrafting( [HarmonyPrefix] [HarmonyPatch(nameof(CollectibleObject.DamageItem))] private static void Prefix_DamageItem( - IWorldAccessor world, - Entity byEntity, - ItemSlot itemSlot, + IWorldAccessor? world, + Entity? byEntity, + ItemSlot? itemSlot, int amount = 1, bool destroyOnZeroDurability = true) { @@ -75,7 +75,7 @@ private static void Prefix_DamageItem( return; if (!destroyOnZeroDurability) return; - ItemStack? itemStack = itemSlot?.Itemstack; + ItemStack? itemStack = itemSlot.Itemstack; int? durability = itemStack?.GetRemainingDurability(); if (!durability.HasValue || durability > amount) return; if (itemStack?.Collectible?.HasBehavior() != true) return;