Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
71 changes: 8 additions & 63 deletions SmithingPlus/ClientTweaks/ShowWorkablePatches.cs
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,8 @@
namespace SmithingPlus.ClientTweaks;

[HarmonyPatchCategory(Core.ClientTweaksCategories.ShowWorkablePatches)]
public partial class ShowWorkablePatches
public class ShowWorkablePatches
{
private const string TemperatureRegexPattern = @"(\d+(.*)°C)";
private const string TemperatureRangeRegexPattern = @"(\s*\(\d+°C\s*-\s*\d+°C\))";

[HarmonyPostfix]
[HarmonyPatch(typeof(BlockEntityAnvil), nameof(BlockEntityAnvil.GetBlockInfo))]
[HarmonyPriority(Priority.VeryLow)]
Expand All @@ -37,68 +34,16 @@ public static void Postfix_BlockEntityAnvil_GetBlockInfo(BlockEntityAnvil __inst
public static void Postfix_BlockEntityForge_GetBlockInfo(BlockEntityForge __instance, IPlayer forPlayer,
StringBuilder dsc)
{
if (__instance.WorkItemStack is not { } workItemStack) return;
if (__instance.WorkItemStack == null) return;
var temperature =
(int)workItemStack.Collectible.GetTemperature(__instance.Api.World, workItemStack);
var workableTemp = workItemStack.GetWorkableTemperature();

var metalProps = workItemStack.Collectible
.GetBehavior<CollectibleBehaviorQuenchable>()
?.GetMetalProps();
if (metalProps != null)
{
var quenchIteration = workItemStack.Attributes.GetInt("quenchIteration");
var temperIteration = workItemStack.Attributes.GetInt("temperIteration");

var localizedStringQ = Lang.Get("itemstack-quenchable",
metalProps.quenchMinTemp,
metalProps.quenchMaxTemp);

dsc.AppendLine(localizedStringQ);

if (temperature > metalProps.quenchMinTemp && temperature < metalProps.quenchMaxTemp)
{
var replacementQ = TemperatureRangeRegex().Replace(localizedStringQ,
SetColor("$1", Constants.QuenchableColor));

dsc.Replace(localizedStringQ, replacementQ);
}

if (quenchIteration > temperIteration)
{
var localizedStringT = Lang.Get("itemstack-temperable",
metalProps.temperMinTemp,
metalProps.temperMaxTemp);

dsc.AppendLine(localizedStringT);

if (temperature > metalProps.temperMinTemp && temperature < metalProps.temperMaxTemp)
{
var replacementT = TemperatureRangeRegex().Replace(localizedStringT,
SetColor("$1", Constants.QuenchableColor));

dsc.Replace(localizedStringT, replacementT);
}
}
}

if (temperature < workableTemp) return;
(int)__instance.WorkItemStack.Collectible.GetTemperature(__instance.Api.World, __instance.WorkItemStack);
var workableTemp = __instance.WorkItemStack.GetWorkableTemperature();
if (!(temperature > workableTemp)) return;
var localizedString = Lang.Get("forge-contentsandtemp", __instance.WorkItemStack.StackSize,
__instance.WorkItemStack.GetName(), temperature);
var replacement = TemperatureValueRegex().Replace(localizedString,
SetColor("$1", Constants.AnvilWorkableColor));
const string pattern = @"(\d+(.*)°C)";
var replacement = Regex.Replace(localizedString, pattern,
$"<font color=\"{Constants.AnvilWorkableColor}\">$1</font>");
dsc.Replace(localizedString, replacement);
}

[GeneratedRegex(TemperatureRegexPattern)]
public static partial Regex TemperatureValueRegex();

[GeneratedRegex(TemperatureRangeRegexPattern)]
public static partial Regex TemperatureRangeRegex();


public static string SetColor<T>(T value, string color)
{
return $"<font color=\"{color}\">{value}</font>";
}
}
21 changes: 11 additions & 10 deletions SmithingPlus/Common/CollectibleBehaviorRecycledBit.cs
Original file line number Diff line number Diff line change
Expand Up @@ -45,16 +45,6 @@ public override void OnCreatedByCrafting(
var stack = slot.Itemstack;
if (stack == null) continue;

var consumedStackSize =
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)
continue;
consumedStackSize = ingredient.ResolvedItemStack.StackSize;
break;
}

var voxelsForThisStack = 0;

// Work item with serialized voxel field
Expand All @@ -73,6 +63,17 @@ public override void OnCreatedByCrafting(
var cheapestOutput = Math.Max(cheapestRecipe.Output.ResolvedItemStack.StackSize, 1);
var recipeMaterialVoxels = cheapestRecipe.Voxels.VoxelCount();
var voxelsPerItem = Math.Max(recipeMaterialVoxels / cheapestOutput, 0);

var consumedStackSize =
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)
continue;
consumedStackSize = ingredient.ResolvedItemStack.StackSize;
break;
}

voxelsForThisStack = voxelsPerItem * consumedStackSize;
}
}
Expand Down
86 changes: 62 additions & 24 deletions SmithingPlus/Core.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using System.Linq;
using System.Collections.Generic;
using System.Linq;
using HarmonyLib;
using JetBrains.Annotations;
using SmithingPlus.BitsRecovery;
Expand Down Expand Up @@ -28,6 +29,10 @@ public partial class Core : ModSystem
public static Harmony HarmonyInstance { get; private set; }
public static ServerConfig Config => ConfigLoader.Config;

public static Dictionary<AssetLocation, SmithingRecipe> SmithingRecipesByOutputCode { get; } = new();
public static Dictionary<AssetLocation, List<SmithingRecipe>> SmithingRecipesByIngredientCode { get; } = new();
public static Dictionary<AssetLocation, List<GridRecipe>> GridRecipesByIngredientCode { get; } = new();

public override void StartPre(ICoreAPI api)
{
Logger = Mod.Logger;
Expand Down Expand Up @@ -75,16 +80,22 @@ private static void AddEntityBehaviors(Entity entity)
public override void AssetsFinalize(ICoreAPI api)
{
base.AssetsFinalize(api);

var recipeRegistry = api.ModLoader.GetModSystem<RecipeRegistrySystem>();
var recipes = recipeRegistry.SmithingRecipes;
var ingotCode = new AssetLocation("game:ingot-copper");
var ingotRecipe = api.Side.IsServer()
? recipes.FirstOrDefault(r =>
r.Ingredient?.Code?.Equals(ingotCode) == true &&
r.Output.ResolvedItemstack?.Collectible.Code.Equals(ingotCode) == true)
: null;

foreach (var collObj in api.World.Collectibles.Where(c => c?.Code != null))
{
collObj.AddBehaviorIf<CollectibleBehaviorDisplayWorkableTemp>(
api.Side == EnumAppSide.Client &&
Config.ShowWorkableTemperature &&
collObj.GetCollectibleInterface<IAnvilWorkable>() is not null);
collObj.AddBehaviorIf<CollectibleBehaviorQuenchableInfo>(
api.Side == EnumAppSide.Client &&
Config.ShowWorkableTemperature &&
collObj.HasBehavior<CollectibleBehaviorQuenchable>());
collObj.AddBehaviorIf<CollectibleBehaviorScrapeCrucible>(Config.RecoverBitsOnSplit &&
collObj is ItemChisel);
collObj.AddBehaviorIf<CollectibleBehaviorSmeltedContainer>(Config.RecoverBitsOnSplit &&
Expand All @@ -104,26 +115,11 @@ public override void AssetsFinalize(ICoreAPI api)
else if (WildcardUtil.Match(Config.WorkItemSelector, collObj.Code.ToString()))
collObj.AddBehavior<CollectibleBehaviorBrokenToolHead>();

// Adds workable-only smithing recipes to make ingots.
// This is a bit hacky as workable crafting uses the original
// ingot recipes, so to have these recipes be present we need ingot -> ingot recipes.
// These won't show up when smithing with
// ingots, however,
// since the original ingot recipe (in the smithingplus domain) has "recipeAttributes":
// { "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<RecipeRegistrySystem>().SmithingRecipes
.FirstOrDefault(r =>
r.Ingredient?.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<RecipeRegistrySystem>().SmithingRecipes
.Any(r => r.Ingredient?.Code?.Equals(collObj.Code) == true &&
r.Output.ResolvedItemstack?.Collectible.Code.Equals(collObj.Code) == true)) continue;
if (recipes.Any(r => r.Ingredient?.Code?.Equals(collObj.Code) == true &&
r.Output.ResolvedItemstack?.Collectible.Code.Equals(collObj.Code) == true)) continue;
Logger.VerboseDebug($"Adding workable-only ingot recipe for {collObj.Code}");
var newRecipe = new SmithingRecipe
{
Expand All @@ -143,11 +139,53 @@ public override void AssetsFinalize(ICoreAPI api)
Code = collObj.Code,
StackSize = 1
},
RecipeId = api.ModLoader.GetModSystem<RecipeRegistrySystem>().SmithingRecipes.Count + 1
RecipeId = recipes.Count + 1
};
newRecipe.Ingredient.Resolve(api.World, $"[{ModId}] add ingot smithing recipe");
newRecipe.Output.Resolve(api.World, $"[{ModId}] add ingot smithing recipe");
api.ModLoader.GetModSystem<RecipeRegistrySystem>().SmithingRecipes.Add(newRecipe);
recipes.Add(newRecipe);
}

SmithingRecipesByOutputCode.Clear();
foreach (var recipe in recipes)
{
var outputCode = recipe.Output?.ResolvedItemstack?.Collectible?.Code;
if (outputCode != null && !SmithingRecipesByOutputCode.ContainsKey(outputCode))
SmithingRecipesByOutputCode[outputCode] = recipe;
}

SmithingRecipesByIngredientCode.Clear();
foreach (var recipe in recipes)
{
if (recipe.Ingredients == null) continue;
foreach (var ing in recipe.Ingredients)
{
var ingCode = ing?.ResolvedItemStack?.Collectible?.Code;
if (ingCode == null) continue;
if (!SmithingRecipesByIngredientCode.TryGetValue(ingCode, out var list))
{
list = new List<SmithingRecipe>();
SmithingRecipesByIngredientCode[ingCode] = list;
}
list.Add(recipe);
}
}

GridRecipesByIngredientCode.Clear();
foreach (var recipe in api.World.GridRecipes)
{
if (recipe.RecipeIngredients == null) continue;
foreach (var ing in recipe.RecipeIngredients)
{
var ingCode = ing?.ResolvedItemStack?.Collectible?.Code;
if (ingCode == null) continue;
if (!GridRecipesByIngredientCode.TryGetValue(ingCode, out var list))
{
list = new List<GridRecipe>();
GridRecipesByIngredientCode[ingCode] = list;
}
list.Add(recipe);
}
}
}

Expand Down
3 changes: 1 addition & 2 deletions SmithingPlus/Properties/launchSettings.json
Original file line number Diff line number Diff line change
Expand Up @@ -33,8 +33,7 @@
"DOTNET_gcServer": "1",
"DOTNET_GCNoAffinitize": "1",
"DOTNET_TieredPGO": "1",
"DYLD_LIBRARY_PATH": "$(VINTAGE_STORY)/Lib",
"DOTNET_HOTRELOAD_ENABLED": "1"
"DYLD_LIBRARY_PATH": "$(VINTAGE_STORY)/Lib"
}
}
}
Expand Down
18 changes: 9 additions & 9 deletions SmithingPlus/SmithingPlus.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -11,39 +11,39 @@

<ItemGroup>
<Reference Include="VintagestoryAPI">
<HintPath>$(VINTAGE_STORY)/VintagestoryAPI.dll</HintPath>
<HintPath>$(VINTAGE_STORY)\VintagestoryAPI.dll</HintPath>
<Private>false</Private>
</Reference>
<Reference Include="VSSurvivalMod">
<HintPath>$(VINTAGE_STORY)/Mods/VSSurvivalMod.dll</HintPath>
<HintPath>$(VINTAGE_STORY)\Mods\VSSurvivalMod.dll</HintPath>
<Private>False</Private>
</Reference>
<Reference Include="VSEssentials">
<HintPath>$(VINTAGE_STORY)/Mods/VSEssentials.dll</HintPath>
<HintPath>$(VINTAGE_STORY)\Mods\VSEssentials.dll</HintPath>
<Private>False</Private>
</Reference>
<Reference Include="VSCreativeMod">
<HintPath>$(VINTAGE_STORY)/Mods/VSCreativeMod.dll</HintPath>
<HintPath>$(VINTAGE_STORY)\Mods\VSCreativeMod.dll</HintPath>
<Private>False</Private>
</Reference>
<Reference Include="Newtonsoft.Json">
<HintPath>$(VINTAGE_STORY)/Lib/Newtonsoft.Json.dll</HintPath>
<HintPath>$(VINTAGE_STORY)\Lib\Newtonsoft.Json.dll</HintPath>
<Private>False</Private>
</Reference>
<Reference Include="protobuf-net">
<HintPath>$(VINTAGE_STORY)/Lib/protobuf-net.dll</HintPath>
<HintPath>$(VINTAGE_STORY)\Lib\protobuf-net.dll</HintPath>
<Private>False</Private>
</Reference>
<Reference Include="0Harmony">
<HintPath>$(VINTAGE_STORY)/Lib/0Harmony.dll</HintPath>
<HintPath>$(VINTAGE_STORY)\Lib\0Harmony.dll</HintPath>
<Private>False</Private>
</Reference>
<Reference Include="VintagestoryLib">
<HintPath>$(VINTAGE_STORY)/VintagestoryLib.dll</HintPath>
<HintPath>$(VINTAGE_STORY)\VintagestoryLib.dll</HintPath>
<Private>false</Private>
</Reference>
<Reference Include="cairo-sharp">
<HintPath>$(VINTAGE_STORY)/Lib/cairo-sharp.dll</HintPath>
<HintPath>$(VINTAGE_STORY)\Lib\cairo-sharp.dll</HintPath>
<Private>False</Private>
</Reference>
</ItemGroup>
Expand Down
50 changes: 34 additions & 16 deletions SmithingPlus/Util/CollectibleExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -73,33 +73,52 @@ public static bool MatchesToolHeadSelector(this CollectibleObject collObj, bool

public static SmithingRecipe? GetSmithingRecipe(this CollectibleObject collObj, ICoreAPI api)
{
var smithingRecipe = api.ModLoader
var cache = Core.SmithingRecipesByOutputCode;
if (cache.Count > 0)
{
cache.TryGetValue(collObj.Code, out var cached);
return cached;
}
Comment on lines +76 to +81

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Don't treat a non-empty cache as a complete recipe index.

These methods return null/empty on any key miss as soon as one cache entry exists. Core.AssetsFinalize() only snapshots the registries once, so later recipe additions — or stale static cache entries that survive until the next rebuild — become permanent false negatives instead of falling back to the live registry.

Suggested fallback-on-miss change
     var cache = Core.SmithingRecipesByOutputCode;
-    if (cache.Count > 0)
-    {
-        cache.TryGetValue(collObj.Code, out var cached);
-        return cached;
-    }
+    if (cache.Count > 0 && cache.TryGetValue(collObj.Code, out var cached))
+        return cached;
+
     return api.ModLoader
         .GetModSystem<RecipeRegistrySystem>()
         .SmithingRecipes
         .FirstOrDefault(r => r.Output.ResolvedItemstack.Collectible.Code.Equals(collObj.Code));

     var cache = Core.SmithingRecipesByIngredientCode;
-    if (cache.Count > 0)
-    {
-        return cache.TryGetValue(collObj.Code, out var cached)
-            ? cached
-            : System.Linq.Enumerable.Empty<SmithingRecipe>();
-    }
+    if (cache.Count > 0 && cache.TryGetValue(collObj.Code, out var cached))
+        return cached;
+
     return
         from recipe in api.ModLoader.GetModSystem<RecipeRegistrySystem>().SmithingRecipes
         from ing in recipe.Ingredients
         where ing.ResolvedItemStack is not null &&
               ing.ResolvedItemStack.Collectible.Code.Equals(collObj.Code)
         select recipe;

     var cache = Core.GridRecipesByIngredientCode;
-    if (cache.Count > 0)
-    {
-        return cache.TryGetValue(collObj.Code, out var cached)
-            ? cached
-            : System.Linq.Enumerable.Empty<GridRecipe>();
-    }
+    if (cache.Count > 0 && cache.TryGetValue(collObj.Code, out var cached))
+        return cached;
+
     return
         from recipe in api.World.GridRecipes
         where recipe.RecipeIngredients != null
         from ing in recipe.RecipeIngredients
         where ing is { ResolvedItemStack.Collectible: not null } &&
               ing.ResolvedItemStack.Collectible.Code.Equals(collObj.Code)
         select recipe;

Also applies to: 91-97, 108-114

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@SmithingPlus/Util/CollectibleExtensions.cs` around lines 76 - 81, The current
logic treats a non-empty Core.SmithingRecipesByOutputCode as authoritative and
returns null on any key miss; change each occurrence (where cache is assigned
from Core.SmithingRecipesByOutputCode and you call
cache.TryGetValue(collObj.Code, out var cached)) to return cached only when
TryGetValue succeeds, and otherwise fall back to querying the live
registry/lookup path (i.e., the runtime/World or Registry method used elsewhere
for recipes) so late-added recipes or stale static caches don't become permanent
false negatives; keep the static snapshot usage for hits but ensure misses
delegate to the live registry instead of returning null (this applies to the
blocks at 76-81, 91-97, and 108-114).

return api.ModLoader
.GetModSystem<RecipeRegistrySystem>()
.SmithingRecipes
.FirstOrDefault(r => r.Output.ResolvedItemstack.Collectible.Code.Equals(collObj.Code));
return smithingRecipe;
}

public static IEnumerable<SmithingRecipe> GetSmithingRecipesAsIngredient(this CollectibleObject collObj,
ICoreAPI api)
{
var smithingRecipes =
var cache = Core.SmithingRecipesByIngredientCode;
if (cache.Count > 0)
{
return cache.TryGetValue(collObj.Code, out var cached)
? cached
: System.Linq.Enumerable.Empty<SmithingRecipe>();
}
return
from recipe in api.ModLoader.GetModSystem<RecipeRegistrySystem>().SmithingRecipes
from ing in recipe.Ingredients
where ing.ResolvedItemStack?.Collectible?.Code?.Equals(collObj.Code) is true
where ing.ResolvedItemStack is not null &&
ing.ResolvedItemStack.Collectible.Code.Equals(collObj.Code)
select recipe;
return smithingRecipes;
}

public static IEnumerable<GridRecipe> GetGridRecipesAsIngredient(this CollectibleObject collObj, ICoreAPI api)
{
var gridRecipes =
var cache = Core.GridRecipesByIngredientCode;
if (cache.Count > 0)
{
return cache.TryGetValue(collObj.Code, out var cached)
? cached
: System.Linq.Enumerable.Empty<GridRecipe>();
}
return
from recipe in api.World.GridRecipes
where recipe.RecipeIngredients != null
from ing in recipe.RecipeIngredients
where ing is { ResolvedItemStack.Collectible: not null } &&
ing.ResolvedItemStack?.Collectible?.Code?.Equals(collObj.Code) is true
ing.ResolvedItemStack.Collectible.Code.Equals(collObj.Code)
select recipe;
return gridRecipes;
}

public static CollectibleObject? CollectibleWithVariant(this CollectibleObject collObj, string type, string value)
Expand Down Expand Up @@ -130,12 +149,11 @@ public static T GetBehavior<T>(this CollectibleObject collObj, bool withInherita
return (T)collObj.GetCollectibleBehavior(typeof(T), withInheritance);
}

/// <summary>
/// Gets the metal properties variant from a CollectibleBehaviorQuenchable behavior.
/// </summary>
public static CollectibleBehaviorQuenchable.MetalPropertyVariant? GetMetalProps(
this CollectibleBehaviorQuenchable behavior)
{
return behavior?.GetField<CollectibleBehaviorQuenchable.MetalPropertyVariant>("metalProps");
}
/*
Regex matching is slow.
Only use when first assigning behaviors.
At runtime, check for CollectibleBehaviorRepairableTool instead.
*/

// Same as above, check for CollectibleBehaviorRepairableToolHead or CollectibleBehaviorCastToolHead instead
}
Loading