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 -- "$@"