From 88859ac69ecc2dfde480b8e03625830ded8a6229 Mon Sep 17 00:00:00 2001 From: Kamron Batman <3953314+kamronbatman@users.noreply.github.com> Date: Tue, 20 Oct 2026 23:37:28 -0700 Subject: [PATCH] fix: T2A cooking works without a craft menu (#2693) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Summary With T2A packet crafting menus enabled (`t2aCraftMenus`, default for pre-UOTD eras), cooking did nothing. Double-clicking a Skillet set a target, and `T2ACraftSystem.ShowMenu` had no Cooking branch, so the target was accepted and silently dropped. Nothing else could cook either: the old RunUO "use food on a heat source" code was never hooked up and was removed in #1054. T2A cooking never used a menu, so this restores the item-on-item / item-on-heat-source flow: - **Per-item handling:** each ingredient (`CookableFood`, `Dough`, `SweetDough`, `SackFlour`, `BowlFlour`, `JarHoney`) implements `IT2ACombinable.TryCombine` and switches on the target. `T2ACooking` holds the shared target, water, combine and heat-timer helpers. - **Heat source:** all `CookableFood` (raw meats, fish, eggs, unbaked pies/quiche/pizzas, cookie and cake mix), dough → bread loaf, sweet dough → muffins. One unit is used up at the start, then a 5 s timer; the Cooking check runs from `CookingLevel` to 100 (bread and muffins use `DefCooking`'s range). The food burns on failure or if the cook moves more than 3 tiles away. - **Combinations:** - Open flour sack or bowl of flour + water → dough - Dough + pear / peach / apple / pumpkin / eggs / meat / sausage → the matching unbaked pie, quiche or pizza - Dough + honey → sweet dough - Sweet dough + honey → cookie mix - Flour + sweet dough → cake mix - The honey and flour + sweet dough recipes work in either direction. - **Water:** water drinks (pitchers), buckets, barrels, tubs and trough add-ons, in the pack, in hand, or on the ground within 2 tiles and in line of sight. - **Cooking tools are passive:** with T2A menus on, a tool whose system has no T2A menu (`T2ACraftSystem.HasMenu`) falls through to `ShowCraftMenu`, which does nothing under T2A. No dead target is left. Behaviour with T2A menus off is unchanged. ## Follow-ups - Static troughs/barrels and open water aren't water sources here or for pitcher filling (no static water ID list exists); to be fixed separately. - Whether a bowl of flour returns an empty bowl is unconfirmed (`//TODO`). - Cooking tools that must merely be in the pack for some recipes are not modelled yet. ## Testing - `dotnet build`: 0 warnings, 0 errors - New `T2ACookingTests`: flour + water, closed sack refused, dough + apple, honey in both directions, cookie mix, cake mix, wrong target consumes nothing, water in another player's pack refused - Not covered: the heat timer (untested); not yet exercised in game --- .../Tests/Engines/Craft/T2ACookingTests.cs | 145 ++++++++++ .../UOContent/Engines/Craft/T2A/T2ACooking.cs | 211 +++++++++++++++ .../Engines/Craft/T2A/T2ACraftSystem.cs | 6 + Projects/UOContent/Items/Food/CookableFood.cs | 18 +- Projects/UOContent/Items/Food/Cooking.cs | 252 +++++++++++++++++- .../Items/Skill Items/Tools/BaseTool.cs | 2 +- 6 files changed, 627 insertions(+), 7 deletions(-) create mode 100644 Projects/UOContent.Tests/Tests/Engines/Craft/T2ACookingTests.cs create mode 100644 Projects/UOContent/Engines/Craft/T2A/T2ACooking.cs diff --git a/Projects/UOContent.Tests/Tests/Engines/Craft/T2ACookingTests.cs b/Projects/UOContent.Tests/Tests/Engines/Craft/T2ACookingTests.cs new file mode 100644 index 000000000..e0d551bed --- /dev/null +++ b/Projects/UOContent.Tests/Tests/Engines/Craft/T2ACookingTests.cs @@ -0,0 +1,145 @@ +using Server; +using Server.Items; +using Server.Mobiles; +using Xunit; + +namespace UOContent.Tests; + +[Collection("Sequential UOContent Tests")] +public class T2ACookingTests +{ + private static PlayerMobile CreatePlayer() + { + var m = new PlayerMobile(World.NewMobile); + m.DefaultMobileInit(); + m.MoveToWorld(new Point3D(4200, 500, 0), Map.Felucca); + m.AddItem(new Backpack()); + return m; + } + + [Fact] + public void FlourAndWaterPitcher_MakeDough() + { + var player = CreatePlayer(); + var pack = player.Backpack; + var sack = new SackFlour { ItemID = 0x103A }; + var pitcher = new Pitcher(BeverageType.Water); + pack.AddItem(sack); + pack.AddItem(pitcher); + + var sackBefore = sack.Quantity; + var pitcherBefore = pitcher.Quantity; + + Assert.True(sack.TryCombine(player, pitcher)); + Assert.Equal(sackBefore - 1, sack.Quantity); + Assert.Equal(pitcherBefore - 1, pitcher.Quantity); + Assert.Equal(1, pack.GetAmount(typeof(Dough))); + } + + [Fact] + public void ClosedFlourSack_IsRefused() + { + var player = CreatePlayer(); + var sack = new SackFlour(); + var pitcher = new Pitcher(BeverageType.Water); + player.Backpack.AddItem(sack); + player.Backpack.AddItem(pitcher); + + Assert.False(sack.TryCombine(player, pitcher)); + Assert.Equal(20, sack.Quantity); + Assert.Equal(pitcher.MaxQuantity, pitcher.Quantity); + Assert.Equal(0, player.Backpack.GetAmount(typeof(Dough))); + } + + [Fact] + public void DoughAndApple_MakeUnbakedApplePie() + { + var player = CreatePlayer(); + var dough = new Dough(); + var apple = new Apple(); + player.Backpack.AddItem(dough); + player.Backpack.AddItem(apple); + + Assert.True(dough.TryCombine(player, apple)); + Assert.Equal(1, player.Backpack.GetAmount(typeof(UnbakedApplePie))); + Assert.Equal(0, player.Backpack.GetAmount(typeof(Apple))); + Assert.Equal(0, player.Backpack.GetAmount(typeof(Dough))); + } + + [Fact] + public void HoneyRows_WorkInEitherDirection() + { + var player = CreatePlayer(); + var pack = player.Backpack; + var dough = new Dough(); + var honey = new JarHoney(); + pack.AddItem(dough); + pack.AddItem(honey); + Assert.True(dough.TryCombine(player, honey)); + Assert.Equal(1, pack.GetAmount(typeof(SweetDough))); + + var dough2 = new Dough(); + var honey2 = new JarHoney(); + pack.AddItem(dough2); + pack.AddItem(honey2); + Assert.True(honey2.TryCombine(player, dough2)); + Assert.Equal(2, pack.GetAmount(typeof(SweetDough))); + + var sweet = pack.FindItemByType(); + var honey3 = new JarHoney(); + pack.AddItem(honey3); + Assert.True(sweet.TryCombine(player, honey3)); + Assert.Equal(1, pack.GetAmount(typeof(CookieMix))); + } + + [Fact] + public void WrongTarget_ConsumesNothing() + { + var player = CreatePlayer(); + var dough = new Dough(); + var honey = new JarHoney(); + player.Backpack.AddItem(dough); + player.Backpack.AddItem(honey); + var apple = new Apple(); + player.Backpack.AddItem(apple); + + var pitcher = new Pitcher(BeverageType.Water); + player.Backpack.AddItem(pitcher); + + Assert.False(honey.TryCombine(player, apple)); + Assert.False(dough.TryCombine(player, pitcher)); + Assert.Equal(1, player.Backpack.GetAmount(typeof(Dough))); + Assert.Equal(1, player.Backpack.GetAmount(typeof(JarHoney))); + Assert.Equal(1, player.Backpack.GetAmount(typeof(Apple))); + } + + [Fact] + public void BowlFlourAndSweetDough_MakeCakeMix() + { + var player = CreatePlayer(); + var bowl = new BowlFlour(); + var sweetDough = new SweetDough(); + player.Backpack.AddItem(bowl); + player.Backpack.AddItem(sweetDough); + + Assert.True(sweetDough.TryCombine(player, bowl)); + Assert.True(bowl.Deleted); + Assert.True(sweetDough.Deleted); + Assert.Equal(1, player.Backpack.GetAmount(typeof(CakeMix))); + } + + [Fact] + public void WaterInAnotherPlayersPack_IsRefused() + { + var player = CreatePlayer(); + var other = CreatePlayer(); + var sack = new SackFlour { ItemID = 0x103A }; + var pitcher = new Pitcher(BeverageType.Water); + player.Backpack.AddItem(sack); + other.Backpack.AddItem(pitcher); + + Assert.False(sack.TryCombine(player, pitcher)); + Assert.Equal(20, sack.Quantity); + Assert.Equal(pitcher.MaxQuantity, pitcher.Quantity); + } +} diff --git a/Projects/UOContent/Engines/Craft/T2A/T2ACooking.cs b/Projects/UOContent/Engines/Craft/T2A/T2ACooking.cs new file mode 100644 index 000000000..e8fbb05d6 --- /dev/null +++ b/Projects/UOContent/Engines/Craft/T2A/T2ACooking.cs @@ -0,0 +1,211 @@ +using System; +using Server.Items; +using Server.Targeting; + +namespace Server.Engines.Craft.T2A; + +/// An item that can be double-clicked under T2A to combine with, or cook over, a target. +public interface IT2ACombinable +{ + bool TryCombine(Mobile from, object target); +} + +/// +/// Shared helpers for T2A cooking, which is item-on-item and item-on-heat-source rather than a craft menu. +/// Each ingredient item decides its own results in . +/// +public sealed class T2ACooking +{ + private const int _heatSourceRange = 1; + private const int _waterRange = 2; + private const int _burnRange = 3; + + private T2ACooking() + { + } + + /// Asks the player for a target if the item is in their pack. + public static void Prompt(Mobile from, Item source) + { + if (!ValidateInPack(from, source)) + { + return; + } + + from.SendAsciiMessage("What do you wish to combine this with, or cook it over?"); + from.Target = new CookTarget(source); + } + + public static bool ValidateInPack(Mobile from, Item item) + { + if (item is { Deleted: false } && item.IsChildOf(from.Backpack)) + { + return true; + } + + from.SendLocalizedMessage(1042001); // That must be in your pack for you to use it. + return false; + } + + public static bool CannotCombine(Mobile from) + { + from.SendAsciiMessage("You cannot combine those."); + return false; + } + + //TODO Support static water troughs and barrels (no static water ID list exists yet) + public static IHasQuantity GetWaterSource(object o) => + o switch + { + BaseBeverage { Content: BeverageType.Water, Quantity: > 0 } bev => bev, + BaseWaterContainer { IsEmpty: false } container => container, + AddonComponent { Addon: IWaterSource { Quantity: > 0 } trough } => trough, + IWaterSource { Quantity: > 0 } source => source, + _ => null + }; + + // Ground water only, so a nearby open container (someone else's pack) cannot be drawn from. + public static bool CanReachWater(Mobile from, Item water) + { + if (water.IsChildOf(from.Backpack) || water.Parent == from || + water.RootParent == null && water.Map == from.Map && + from.InRange(water.GetWorldLocation(), _waterRange) && from.InLOS(water)) + { + return true; + } + + from.LocalOverheadMessage(MessageType.Regular, 0x3B2, 1019045); // I can't reach that. + return false; + } + + /// Spends one unit of each ingredient and gives the player the result. + public static bool Combine(Mobile from, object source, object target, Item result, string message) + { + ConsumeOne(source); + ConsumeOne(target); + + if (!GiveResult(from, result)) + { + return false; + } + + from.SendAsciiMessage(message); + return true; + } + + // AddToBackpack drops a result that does not fit at the mobile's feet (or logout location), + // but with no valid map either it would be left on the internal map forever. + private static bool GiveResult(Mobile from, Item result) + { + if (from.AddToBackpack(result) || result.Map != null && result.Map != Map.Internal) + { + return true; + } + + result.Delete(); + return false; + } + + //TODO Confirm whether an empty bowl is returned when a BowlFlour is used up + private static void ConsumeOne(object o) + { + if (o is IHasQuantity container) + { + container.Quantity--; + } + else if (o is Item item) + { + item.Consume(); + } + } + + /// Cooks the source over a heat source if it is close enough and the player is not already cooking. + public static bool BeginHeat(Mobile from, Item source, object target) + { + var point = target is IPoint3D p ? new Point3D(p) : from.Location; + if (from.Map == null || !from.InRange(point, _heatSourceRange)) + { + from.SendLocalizedMessage(500446); // That is too far away. + return false; + } + + if (!from.BeginAction()) + { + from.SendLocalizedMessage(500119); // You must wait to perform another action + return false; + } + + from.PlaySound(0x225); + source.Consume(); + + Timer.DelayCall(TimeSpan.FromSeconds(5.0), FinishHeat, from, source, from.Map, point); + return true; + } + + // The source is already consumed here and may be deleted, so only its type and CookingLevel are read. + private static void FinishHeat(Mobile from, Item source, Map map, Point3D point) + { + from.EndAction(); + + if (from.Deleted) + { + return; + } + + if (from.Map != map || !from.InRange(point, _burnRange)) + { + from.SendLocalizedMessage(500686); // You burn the food to a crisp! It's ruined. + return; + } + + var (min, max) = source switch + { + CookableFood food => (food.CookingLevel, 100.0), + Dough => CraftRange(typeof(BreadLoaf)), + SweetDough => CraftRange(typeof(Muffins)), + _ => (0.0, 100.0) + }; + + if (!from.CheckSkill(SkillName.Cooking, min, max)) + { + from.SendLocalizedMessage(500686); // You burn the food to a crisp! It's ruined. + return; + } + + Item result = source switch + { + CookableFood food => food.Cook(), + Dough => new BreadLoaf(), + SweetDough => new Muffins(), + _ => null + }; + + if (result != null && GiveResult(from, result)) + { + from.PlaySound(0x57); + } + } + + // Falls back to the full range when the cooking craft system has no entry or is not initialized. + private static (double Min, double Max) CraftRange(Type type) + { + var skills = DefCooking.CraftSystem?.CraftItems.SearchFor(type)?.Skills; + + return skills is { Count: > 0 } ? (skills[0].MinSkill, skills[0].MaxSkill) : (0.0, 100.0); + } + + private sealed class CookTarget : Target + { + private readonly Item _source; + + public CookTarget(Item source) : base(_waterRange, false, TargetFlags.None) => _source = source; + + protected override void OnTarget(Mobile from, object targeted) + { + if (ValidateInPack(from, _source) && _source is IT2ACombinable combinable) + { + combinable.TryCombine(from, targeted); + } + } + } +} diff --git a/Projects/UOContent/Engines/Craft/T2A/T2ACraftSystem.cs b/Projects/UOContent/Engines/Craft/T2A/T2ACraftSystem.cs index a0fff9d0f..4e50b9ce6 100644 --- a/Projects/UOContent/Engines/Craft/T2A/T2ACraftSystem.cs +++ b/Projects/UOContent/Engines/Craft/T2A/T2ACraftSystem.cs @@ -15,6 +15,12 @@ public static class T2ACraftSystem /// public static bool Enabled { get; set; } + public static bool HasMenu(CraftSystem system) => + system == DefBlacksmithy.CraftSystem || system == DefAlchemy.CraftSystem || + system == DefBowFletching.CraftSystem || system == DefCarpentry.CraftSystem || + system == DefCartography.CraftSystem || system == DefInscription.CraftSystem || + system == DefTailoring.CraftSystem || system == DefTinkering.CraftSystem; + public static void ShowMenu(Mobile from, CraftSystem craftSystem, BaseTool tool, Item preTarget = null) { if (!Enabled) diff --git a/Projects/UOContent/Items/Food/CookableFood.cs b/Projects/UOContent/Items/Food/CookableFood.cs index e9597fd03..1c426de88 100644 --- a/Projects/UOContent/Items/Food/CookableFood.cs +++ b/Projects/UOContent/Items/Food/CookableFood.cs @@ -1,10 +1,11 @@ using ModernUO.Serialization; +using Server.Engines.Craft.T2A; using Server.Targeting; namespace Server.Items; [SerializationGenerator(0, false)] -public abstract partial class CookableFood : Item +public abstract partial class CookableFood : Item, IT2ACombinable { [SerializableField(0)] [SerializedCommandProperty(AccessLevel.GameMaster)] @@ -14,6 +15,21 @@ public abstract partial class CookableFood : Item public abstract Food Cook(); + public override void OnDoubleClick(Mobile from) + { + if (T2ACraftSystem.Enabled) + { + T2ACooking.Prompt(from, this); + } + else + { + base.OnDoubleClick(from); + } + } + + public bool TryCombine(Mobile from, object target) => + IsHeatSource(target) ? T2ACooking.BeginHeat(from, this, target) : T2ACooking.CannotCombine(from); + public static bool IsHeatSource(object targeted) { int itemID; diff --git a/Projects/UOContent/Items/Food/Cooking.cs b/Projects/UOContent/Items/Food/Cooking.cs index cd010432d..22ce41837 100644 --- a/Projects/UOContent/Items/Food/Cooking.cs +++ b/Projects/UOContent/Items/Food/Cooking.cs @@ -1,11 +1,12 @@ using System; using ModernUO.Serialization; +using Server.Engines.Craft.T2A; using Server.Targeting; namespace Server.Items; [SerializationGenerator(0, false)] -public partial class Dough : Item +public partial class Dough : Item, IT2ACombinable { [Constructible] public Dough() : base(0x103d) @@ -14,10 +15,68 @@ public partial class Dough : Item } public override double DefaultWeight => 1.0; + + public override void OnDoubleClick(Mobile from) + { + if (T2ACraftSystem.Enabled) + { + T2ACooking.Prompt(from, this); + } + else + { + base.OnDoubleClick(from); + } + } + + public bool TryCombine(Mobile from, object target) + { + if (CookableFood.IsHeatSource(target)) + { + return T2ACooking.BeginHeat(from, this, target); + } + + if (target is not Item item) + { + return T2ACooking.CannotCombine(from); + } + + if (!T2ACooking.ValidateInPack(from, item)) + { + return false; + } + + Item result = item switch + { + JarHoney => new SweetDough(), + Pear => new UnbakedFruitPie(), + Peach => new UnbakedPeachCobbler(), + Apple => new UnbakedApplePie(), + Pumpkin or SmallPumpkin => new UnbakedPumpkinPie(), + Eggs => new UnbakedQuiche(), + Ham or Spam or CookedBird or FishSteak => new UnbakedMeatPie(), + Sausage => new UncookedSausagePizza(), + _ => null + }; + + if (result == null) + { + return T2ACooking.CannotCombine(from); + } + + return T2ACooking.Combine( + from, + this, + item, + result, + item is JarHoney + ? "You mix the dough with honey to make sweet dough." + : "You combine the dough with the ingredient." + ); + } } [SerializationGenerator(0, false)] -public partial class SweetDough : Item +public partial class SweetDough : Item, IT2ACombinable { [Constructible] public SweetDough() : base(0x103d) @@ -28,10 +87,79 @@ public partial class SweetDough : Item public override double DefaultWeight => 1.0; public override int LabelNumber => 1041340; // sweet dough + + public override void OnDoubleClick(Mobile from) + { + if (T2ACraftSystem.Enabled) + { + T2ACooking.Prompt(from, this); + } + else + { + base.OnDoubleClick(from); + } + } + + public bool TryCombine(Mobile from, object target) + { + if (CookableFood.IsHeatSource(target)) + { + return T2ACooking.BeginHeat(from, this, target); + } + + switch (target) + { + case JarHoney honey: + { + return T2ACooking.ValidateInPack(from, honey) && T2ACooking.Combine( + from, + this, + honey, + new CookieMix(), + "You mix the sweet dough with honey to make cookie mix." + ); + } + case SackFlour sack: + { + if (!T2ACooking.ValidateInPack(from, sack)) + { + return false; + } + + if (!sack.IsOpen) + { + from.SendAsciiMessage("You must open the sack of flour first."); + return false; + } + + return T2ACooking.Combine( + from, + this, + sack, + new CakeMix(), + "You mix the flour with sweet dough to make cake mix." + ); + } + case BowlFlour bowl: + { + return T2ACooking.ValidateInPack(from, bowl) && T2ACooking.Combine( + from, + this, + bowl, + new CakeMix(), + "You mix the flour with sweet dough to make cake mix." + ); + } + default: + { + return T2ACooking.CannotCombine(from); + } + } + } } [SerializationGenerator(0, false)] -public partial class JarHoney : Item +public partial class JarHoney : Item, IT2ACombinable { [Constructible] public JarHoney() : base(0x9ec) @@ -40,10 +168,44 @@ public partial class JarHoney : Item } public override double DefaultWeight => 1.0; + + public override void OnDoubleClick(Mobile from) + { + if (T2ACraftSystem.Enabled) + { + T2ACooking.Prompt(from, this); + } + else + { + base.OnDoubleClick(from); + } + } + + public bool TryCombine(Mobile from, object target) + { + return target switch + { + Dough dough => T2ACooking.ValidateInPack(from, dough) && T2ACooking.Combine( + from, + this, + dough, + new SweetDough(), + "You mix the dough with honey to make sweet dough." + ), + SweetDough sweetDough => T2ACooking.ValidateInPack(from, sweetDough) && T2ACooking.Combine( + from, + this, + sweetDough, + new CookieMix(), + "You mix the sweet dough with honey to make cookie mix." + ), + _ => T2ACooking.CannotCombine(from) + }; + } } [SerializationGenerator(0, false)] -public partial class BowlFlour : Item +public partial class BowlFlour : Item, IT2ACombinable { [Constructible] public BowlFlour() : base(0xa1e) @@ -51,6 +213,41 @@ public partial class BowlFlour : Item } public override double DefaultWeight => 1.0; + + public override void OnDoubleClick(Mobile from) + { + if (T2ACraftSystem.Enabled) + { + T2ACooking.Prompt(from, this); + } + else + { + base.OnDoubleClick(from); + } + } + + public bool TryCombine(Mobile from, object target) + { + return target switch + { + SweetDough sweetDough => T2ACooking.ValidateInPack(from, sweetDough) && T2ACooking.Combine( + from, + this, + sweetDough, + new CakeMix(), + "You mix the flour with sweet dough to make cake mix." + ), + Item item when T2ACooking.GetWaterSource(item) is { } water => + T2ACooking.CanReachWater(from, item) && T2ACooking.Combine( + from, + this, + water, + new Dough(), + "You mix the flour with water to make dough." + ), + _ => T2ACooking.CannotCombine(from) + }; + } } [SerializationGenerator(0, false)] @@ -66,7 +263,7 @@ public partial class WoodenBowl : Item [TypeAlias("Server.Items.SackFlourOpen")] [SerializationGenerator(0, false)] -public partial class SackFlour : Item, IHasQuantity +public partial class SackFlour : Item, IHasQuantity, IT2ACombinable { [Constructible] public SackFlour() : base(0x1039) @@ -104,7 +301,52 @@ public partial class SackFlour : Item, IHasQuantity { ++ItemID; } + + if (!T2ACraftSystem.Enabled || !T2ACooking.ValidateInPack(from, this)) + { + return; + } + + if (IsOpen) + { + T2ACooking.Prompt(from, this); + } + else + { + from.SendAsciiMessage("You must open the sack of flour first."); + } } + + public bool TryCombine(Mobile from, object target) + { + if (!IsOpen) + { + from.SendAsciiMessage("You must open the sack of flour first."); + return false; + } + + return target switch + { + SweetDough sweetDough => T2ACooking.ValidateInPack(from, sweetDough) && T2ACooking.Combine( + from, + this, + sweetDough, + new CakeMix(), + "You mix the flour with sweet dough to make cake mix." + ), + Item item when T2ACooking.GetWaterSource(item) is { } water => + T2ACooking.CanReachWater(from, item) && T2ACooking.Combine( + from, + this, + water, + new Dough(), + "You mix the flour with water to make dough." + ), + _ => T2ACooking.CannotCombine(from) + }; + } + + public bool IsOpen => ItemID is not (0x1039 or 0x1045); } [SerializationGenerator(0, false)] diff --git a/Projects/UOContent/Items/Skill Items/Tools/BaseTool.cs b/Projects/UOContent/Items/Skill Items/Tools/BaseTool.cs index 73482ccd0..074c29512 100644 --- a/Projects/UOContent/Items/Skill Items/Tools/BaseTool.cs +++ b/Projects/UOContent/Items/Skill Items/Tools/BaseTool.cs @@ -159,7 +159,7 @@ public abstract partial class BaseTool : Item, IUsesRemaining, ICraftable { from.SendLocalizedMessage(num); } - else if (T2ACraftSystem.Enabled) + else if (T2ACraftSystem.Enabled && T2ACraftSystem.HasMenu(system)) { from.Target = new T2ACraftToolTarget(this, system); from.SendAsciiMessage("Target this tool to make last item, or any other target to begin crafting.");