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/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..d78dd59 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) + 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/CollectibleBehaviorCastToolHead.cs b/SmithingPlus/CastingTweaks/CollectibleBehaviorCastToolHead.cs index 688c2fe..74b2dd7 100644 --- a/SmithingPlus/CastingTweaks/CollectibleBehaviorCastToolHead.cs +++ b/SmithingPlus/CastingTweaks/CollectibleBehaviorCastToolHead.cs @@ -93,13 +93,17 @@ public override void GetHeldItemName(StringBuilder dsc, ItemStack itemStack) 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); + var workableTemp = GetWorkableTemperature(itemStack); + var temperature = itemStack.Collectible.GetTemperature(world, itemStack); dsc.AppendLine(Lang.Get("Workable Temperature: {0}", workableTemp > 0 ? temperature > workableTemp @@ -125,4 +129,4 @@ private float GetWorkableTemperature(ItemStack itemStack) ? workableAttr.AsFloat(defaultWorkableTemp) : defaultWorkableTemp; } -} \ No newline at end of file +} diff --git a/SmithingPlus/CastingTweaks/ToolMoldUnitsPatch.cs b/SmithingPlus/CastingTweaks/ToolMoldUnitsPatch.cs index 6d3c6ef..75a24df 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); @@ -80,11 +64,21 @@ public static int GetPatchedRequiredUnits(ICoreAPI api, Block toolMold, ItemStac 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); + SmithingRecipe? cheapestRecipe = stack.GetCheapestSmithingRecipe(api); + if (cheapestRecipe == null) + { + return null; + } + + JsonItemStack? recipeOutput = cheapestRecipe.Output; + ItemStack? resolvedOutputStack = recipeOutput?.ResolvedItemstack; + if (resolvedOutputStack == null || resolvedOutputStack.StackSize <= 0) + { + return null; + } + + int recipeMaterialVoxels = cheapestRecipe.Voxels.VoxelCount(); + int voxelsPerItem = Math.Max(recipeMaterialVoxels / resolvedOutputStack.StackSize, 0); return voxelsPerItem * stack.StackSize; } @@ -95,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 = @@ -129,9 +129,14 @@ 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); return jstack.ResolvedItemstack; } -} \ No newline at end of file +} 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/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/CollectibleBehaviorAnvilWorkable.cs b/SmithingPlus/Common/CollectibleBehaviorAnvilWorkable.cs index 3a659ba..4ac81e4 100644 --- a/SmithingPlus/Common/CollectibleBehaviorAnvilWorkable.cs +++ b/SmithingPlus/Common/CollectibleBehaviorAnvilWorkable.cs @@ -86,15 +86,71 @@ public virtual int GetRequiredAnvilTier(ItemStack stack) 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(); + ICoreAPI? api = Api; + if (api == null) + { + return new List(); + } + + ItemStack? baseMetalStack = MetalMaterial?.IngotStack; + List matchingRecipes = new List(); + IEnumerable smithingRecipes = api.GetSmithingRecipes(); + foreach (SmithingRecipe? recipe in smithingRecipes) + { + 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) + { + continue; + } + + bool matchesBaseMetal = baseMetalStack != null && ingredient.SatisfiesAsIngredient(baseMetalStack); + bool matchesInputStack = ingredient.SatisfiesAsIngredient(stack); + if ((!matchesBaseMetal && !matchesInputStack) || outputCode.Equals(collObj.Code)) + { + continue; + } + + matchingRecipes.Add(recipe); + } + + matchingRecipes.Sort(CompareMatchingRecipes); + + List distinctRecipes = new List(matchingRecipes.Count); + HashSet encounteredOutputs = new HashSet(); + foreach (SmithingRecipe recipe in matchingRecipes) + { + ItemStack? resolvedOutputStack = recipe.Output?.ResolvedItemstack; + if (resolvedOutputStack != null && encounteredOutputs.Add(resolvedOutputStack)) + { + distinctRecipes.Add(recipe); + } + } + + return distinctRecipes; + } + + private static int CompareMatchingRecipes(SmithingRecipe leftRecipe, SmithingRecipe rightRecipe) + { + 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; + } + + int leftStackSize = leftOutputStack?.StackSize ?? 0; + int rightStackSize = rightOutputStack?.StackSize ?? 0; + return leftStackSize.CompareTo(rightStackSize); } public virtual ItemStack? GetBaseMaterial(ItemStack stack) @@ -201,4 +257,4 @@ public static bool AllowsEmpty(this AnvilPlacementMode mode) { return mode is AnvilPlacementMode.Normal or AnvilPlacementMode.Empty; } -} \ No newline at end of file +} diff --git a/SmithingPlus/Common/CollectibleBehaviorJsonAnvilWorkable.cs b/SmithingPlus/Common/CollectibleBehaviorJsonAnvilWorkable.cs index 3ece70f..7a5a6b5 100644 --- a/SmithingPlus/Common/CollectibleBehaviorJsonAnvilWorkable.cs +++ b/SmithingPlus/Common/CollectibleBehaviorJsonAnvilWorkable.cs @@ -1,6 +1,5 @@ #nullable enable using System; -using System.Linq; using JetBrains.Annotations; using SmithingPlus.Util; using Vintagestory.API.Common; @@ -33,13 +32,45 @@ public override void Initialize(JsonObject properties) ? 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(); + JsonObject[]? jsonPattern = properties[PropertyKeys.Voxels].Exists + ? properties[PropertyKeys.Voxels].AsArray() + : null; + if (jsonPattern == null || jsonPattern.Length == 0) + { + return; + } + + 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; + } + + 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; + } + + parsedLayer[rowIndex] = parsedRow; + } + + parsedPattern[layerIndex] = parsedLayer; + } + + Pattern = parsedPattern; } public override EnumHelveWorkableMode GetHelveWorkableMode(ItemStack stack, BlockEntityAnvil beAnvil) @@ -133,4 +164,4 @@ private static class PropertyKeys public const string HelveWorkableMode = "helveWorkableMode"; public const string Voxels = "voxels"; } -} \ 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/MetalMaterial.cs b/SmithingPlus/Common/Metal/MetalMaterial.cs index ea72d7b..e4d154a 100644 --- a/SmithingPlus/Common/Metal/MetalMaterial.cs +++ b/SmithingPlus/Common/Metal/MetalMaterial.cs @@ -11,10 +11,10 @@ namespace SmithingPlus.Metal; 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("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; @@ -81,4 +81,4 @@ public override int GetHashCode() { return Code.GetHashCode(); } -} \ No newline at end of file +} diff --git a/SmithingPlus/Common/Metal/MetalMaterialExtensions.cs b/SmithingPlus/Common/Metal/MetalMaterialExtensions.cs index 32a8749..a3d4ce2 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; } @@ -75,17 +80,34 @@ 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; - metalMaterial = materialResolver(ingredient); + continue; + } + + foreach (CraftingRecipeIngredient ingredientDefinition in gridRecipe.RecipeIngredients) + { + if (ingredientDefinition == null) + { + continue; + } + + ItemStack? resolvedIngredientStack = ingredientDefinition.ResolvedItemStack; + CollectibleObject? ingredientCollectible = resolvedIngredientStack?.Collectible; + if (ingredientCollectible == null) + { + continue; + } + + if (ingredientDefinition.ConsumeProperties.Consume && + ingredientDefinition.ConsumeProperties.DurabilityCost != 0) + { + continue; + } + + metalMaterial = materialResolver(ingredientCollectible); if (metalMaterial != null) return true; } } @@ -115,7 +137,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); @@ -144,10 +166,14 @@ 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); - var ingotStack = anvilWorkable.GetBaseMaterial(itemStack); + if (collObj is not IAnvilWorkable anvilWorkable) return collObj?.GetOrCacheMetalMaterial(api); + ItemStack? ingotStack = anvilWorkable.GetBaseMaterial(itemStack); + if (ingotStack?.Collectible == null) + { + return collObj.GetOrCacheMetalMaterial(api); + } 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) @@ -157,10 +183,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/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/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..844c16e 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"; - public static ServerConfig Config { get; private set; } + private const string ServerConfigName = "SmithingPlus.json"; + private const string ClientConfigName = "SmithingPlusClient.json"; + public static ServerConfig Config { get; private set; } = new ServerConfig(); + public static ClientConfig CConfig { get; private set; } = new ClientConfig(); 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) { @@ -54,7 +81,8 @@ public override void Start(ICoreAPI api) public override void Dispose() { - Config = null; + Config = new ServerConfig(); + CConfig = new ClientConfig(); 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..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,16 +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 ServerConfig Config => ConfigLoader.Config; + 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; } = 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) @@ -55,6 +98,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 +123,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 +132,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,17 +176,17 @@ 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 => 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 { @@ -153,38 +216,40 @@ public override void AssetsFinalize(ICoreAPI api) private static void Patch() { - if (HarmonyInstance != null) return; - HarmonyInstance = new Harmony(ModId); + 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); + Config.SmithWithBits || Config.BitsTopUp || Config.EnableToolRecovery, harmonyInstance); + + 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() { - 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(); } -} \ No newline at end of file +} 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/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/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/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/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 diff --git a/SmithingPlus/ToolRecovery/CollectibleBehaviorBrokenToolHead.cs b/SmithingPlus/ToolRecovery/CollectibleBehaviorBrokenToolHead.cs index 433eadf..a38381f 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 @@ -42,13 +44,14 @@ 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) { - 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.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")); } @@ -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/CollectibleBehaviorRepairableTool.cs b/SmithingPlus/ToolRecovery/CollectibleBehaviorRepairableTool.cs index 6bb4ee7..4b29708 100644 --- a/SmithingPlus/ToolRecovery/CollectibleBehaviorRepairableTool.cs +++ b/SmithingPlus/ToolRecovery/CollectibleBehaviorRepairableTool.cs @@ -20,22 +20,22 @@ public override void GetHeldItemInfo(ItemSlot? inSlot, StringBuilder dsc, IWorld { base.GetHeldItemInfo(inSlot, dsc, world, withDebugInfo); - var itemstack = inSlot?.Itemstack; - var collectible = itemstack?.Collectible; + 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(); + var brokenCount = 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 && itemStack.GetRepairSmith() is { } repairSmith) dsc.AppendLine(Lang.Get("Last repaired by {0}", repairSmith)); } -} \ No newline at end of file +} diff --git a/SmithingPlus/ToolRecovery/ItemDamagedPatches.cs b/SmithingPlus/ToolRecovery/ItemDamagedPatches.cs index d68a4c8..9e4295d 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; } @@ -55,33 +60,46 @@ 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) { + if (world == null || byEntity == null || itemSlot == null) + { + return; + } + if (world.Api.Side.IsClient()) 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 +111,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 +135,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 +172,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 30bec46..7496933 100644 --- a/SmithingPlus/Util/CollectibleExtensions.cs +++ b/SmithingPlus/Util/CollectibleExtensions.cs @@ -29,11 +29,22 @@ private static void EnsureAttributesNotNull(this CollectibleObject obj) public static void MakeForgeable(this CollectibleObject collObj) { - collObj.EnsureAttributesNotNull(); - var token = collObj.Attributes.Token; + JsonObject? attributes = collObj.Attributes; + if (attributes == null) + { + attributes = new JsonObject(new JObject()); + collObj.Attributes = attributes; + } + + JToken? token = attributes.Token; + if (token == null) + { + token = new JObject(); + } + token["forgable"] = true; token["inForgeTransform"] = ForgeTransformToken; - collObj.Attributes.Token = token; + attributes.Token = token; } public static void AddBehavior(this CollectibleObject collectible) where T : CollectibleBehavior @@ -73,33 +84,58 @@ 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 && recipe != 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 = []; + // 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 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) @@ -136,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; } -} \ No newline at end of file +} 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/ItemStackExtensions.cs b/SmithingPlus/Util/ItemStackExtensions.cs index e647817..fe4305e 100644 --- a/SmithingPlus/Util/ItemStackExtensions.cs +++ b/SmithingPlus/Util/ItemStackExtensions.cs @@ -150,27 +150,70 @@ public static float GetWorkableTemperature(this ItemStack stack) // 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; + RecipeRegistrySystem? recipeRegistry = api.ModLoader.GetModSystem(); + if (recipeRegistry?.SmithingRecipes == null) + { + return null; + } + + SmithingRecipe? largestRecipe = null; + int largestOutputStackSize = 0; + foreach (SmithingRecipe? recipe in recipeRegistry.SmithingRecipes) + { + ItemStack? resolvedOutputStack = recipe?.Output?.ResolvedItemstack; + if (resolvedOutputStack == null || !resolvedOutputStack.Satisfies(toolHead)) + { + continue; + } + + if (resolvedOutputStack.StackSize > largestOutputStackSize) + { + largestRecipe = recipe; + largestOutputStackSize = resolvedOutputStack.StackSize; + } + } + + 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; + RecipeRegistrySystem? recipeRegistry = api.ModLoader.GetModSystem(); + if (recipeRegistry?.SmithingRecipes == null) + { + return null; + } + + SmithingRecipe? selectedRecipe = null; + int selectedVoxelCost = int.MinValue; + foreach (SmithingRecipe? recipe in recipeRegistry.SmithingRecipes) + { + if (recipe == null) + { + continue; + } + + ItemStack? resolvedOutputStack = recipe.Output?.ResolvedItemstack; + if (resolvedOutputStack == null || resolvedOutputStack.StackSize <= 0) + { + continue; + } + + if (!resolvedOutputStack.Satisfies(toolHead)) + { + continue; + } + + int voxelCost = recipe.Voxels.VoxelCount() / resolvedOutputStack.StackSize; + if (voxelCost > selectedVoxelCost) + { + selectedRecipe = recipe; + selectedVoxelCost = voxelCost; + } + } + + return selectedRecipe; } public static IEnumerable GetGridRecipes(this ItemStack itemStack, ICoreAPI api) @@ -267,4 +310,4 @@ public static bool IsCastTool(this ItemStack stack) bitsStack.StackSize = recoveredBits; return bitsStack; } -} \ 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 +} 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)); + } + } + } +} 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/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. 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 -- "$@"