feat: Pre-Publish 14 Crafting (supersedes #2181, #2381) (#2476)

## Summary

Adds authentic **T2A-era (pre-UO:Third-Dawn) packet-based crafting menus**, enabled via the **`t2aCraftMenus` server setting** (read once at startup; default **`!Core.UOTD`**, so a pre-UO:TD shard gets them automatically). When enabled, double-clicking a crafting tool opens the classic `0x7C`/`0x7D` item-list menu — skill- and material-filtered — instead of the modern gump, covering all 8 tool/skill crafts (blacksmithy, tailoring, tinkering, carpentry, alchemy, bowcraft/fletching, inscription, cartography). It is **not** a runtime/admin-flippable feature flag.

This is the **definitive, reconciled** branch and **supersedes**:
- **#2181** (Delphi — `T2A_CraftingMenus`): the original effort.
- **#2381** (Jack/UOLL — `t2a_crafting_menus`): the research-grounded superset (Delphi's base + 12 corrections), rebased onto current `main`.

Original authorship is preserved across the cherry-picked history: foundation commit **@Delphi79**, mechanic fixes **@jackuoll (Jack Ward)**, reconciliation/fixes/docs mine.

## How it was built

1. Cherry-picked Jack's 13 commits onto current `main` (superset of Delphi's; only 2 trivial FeatureFlags conflicts).
2. Applied targeted fixes (below) with tests.
3. Full convention audit, build, and test pass.

Grounded in independent historical research plus Jack's deep dive. Maintainer reference: `dev-docs/t2a-crafting.md`.

## Mechanics (highlights)

- Double-click tool → target resource → skill/material-filtered menu → craft. Resource pre-selection per skill; make-last by targeting the tool.
- **Stacked-gem jewelry:** target a gem stack → the **full stack** is consumed and the piece is named by count ("a 1000 diamond ring"); count persists (`BaseJewel` serialization **v4 → v5**, new `_gemCount`).
- **Tool-less inscription & cartography** (skill-list invoked; no pen/sextant); inscription consumes reagents+scroll on success and failure, mana only on success.
- **Tailoring matching-hue consumption:** targeting hued cloth/leather consumes only that hue. Crafted items take color from their **`CraftResource`** (not the dyed hue), so dyed leather/cloth don't tint the product; in T2A only colored ingots/ore color items (metal armor/shields).
- **Half-resources on failed non-scroll crafts** (pre-UO:TD).
- **Maker's mark** always prompted for exceptional items, via the shared `QueryMakersMarkGump`.
- Server-side menu infra changes are additive (`ItemListEntry.CraftIndex`, `Entries` setter, `HasSent`).

## Notable changes on top of the cherry-pick

- **Toggle is a startup server setting, not a feature flag.** Removed `ContentFeatureFlags.T2ACraftMenus` (and its admin-flippable plumbing); the value is read once via `ServerConfiguration.GetSetting("t2aCraftMenus", !Core.UOTD)` into `T2ACraftSystem.Enabled`. Since the default tracks the era and it can't be flipped at runtime, there's no incoherent "menus-on / UO:TD-era" state.
- **Stacked-gem consumption (B3a/B3):** consume the full `PendingGemCount` (was deliberately consuming 1 while naming by the stack), null-safe gem type, plain-piece fallback + message. New `T2AJewelGemCraftTests`.
- **Convention audit:** `new List<Item>()` → `PooledRefList<Item>` on the hue-aware consume path; removed dead code.

## Decisions & deviations

- `make-last` kept as **QoL** (post-T2A gump-era feature).
- `half-on-failure` (non-scroll) kept as a **reconstruction** (not OSI-confirmed).
- **Stacked-gem** behavior set per shard authority (overrides the "single gem" reconstruction).
- **Cooking** out of scope (no T2A crafting menu existed for it).
- **No colored items from dyed materials:** crafted color comes from the `CraftResource` type. Pre-AOS leather has no colored variant, so leather is always uncolored; weapons retain resource color only in AOS+ (unchanged, intended).

## Test plan

- Automated: `dotnet build ModernUO.slnx -c Debug` clean; `dotnet test Projects/UOContent.Tests` → **421 passed** (incl. 3 new jewelry tests).
- Manual (needs a running T2A shard + client):
  - [ ] Each of the 8 skills opens the correct menu; empty-menu guard fires.
  - [ ] Make-last repeats the last craft (jewelry re-prompts gem).
  - [ ] Jewelry consumes the full targeted gem stack and names by count.
  - [ ] Cartography consumes blank maps only with T2A enabled / maps+scrolls when disabled.
  - [ ] Tailoring consumes only the targeted-hue material; crafted items are not tinted by dyed cloth/leather.
  - [ ] Maker's-mark prompt on exceptional.
  - [ ] Failed non-scroll craft consumes half resources.
  - [ ] Inscription: reagents+scroll on success/failure, mana only on success.
  - [ ] T2A disabled: gump crafting unchanged.

## Credits

Co-authored-by: @Delphi79
Co-authored-by: @jackuoll
This commit is contained in:
Kamron Batman 2026-06-07 20:27:22 -07:00 committed by GitHub
parent 2e93201e51
commit 16bf3016fb
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
50 changed files with 4483 additions and 219 deletions

View file

@ -0,0 +1,34 @@
using Server.Network;
namespace Server.Menus;
public abstract class BaseMenu : IMenu
{
private static int _nextSerial;
public int Serial { get; }
public abstract int EntryLength { get; }
public BaseMenu()
{
var serial = ++_nextSerial;
if (serial <= 0)
{
serial = 1;
_nextSerial = 1;
}
Serial = serial;
}
public abstract void SendTo(NetState state);
public virtual void OnCancel(NetState state)
{
}
public virtual void OnResponse(NetState state, int index)
{
}
}

View file

@ -4,11 +4,12 @@ namespace Server.Menus.ItemLists;
public class ItemListEntry public class ItemListEntry
{ {
public ItemListEntry(string name, int itemID, int hue = 0) public ItemListEntry(string name, int itemID, int hue = 0, int craftIndex = 0)
{ {
Name = name?.Trim() ?? ""; Name = name?.Trim() ?? "";
ItemID = itemID; ItemID = itemID;
Hue = hue; Hue = hue;
CraftIndex = craftIndex;
} }
public string Name { get; } public string Name { get; }
@ -16,43 +17,25 @@ public class ItemListEntry
public int ItemID { get; } public int ItemID { get; }
public int Hue { get; } public int Hue { get; }
public int CraftIndex { get; }
} }
public class ItemListMenu : IMenu public class ItemListMenu : BaseMenu
{ {
private static int m_NextSerial;
public ItemListMenu(string question, ItemListEntry[] entries) public ItemListMenu(string question, ItemListEntry[] entries)
{ {
Question = question.Trim(); Question = question.Trim();
Entries = entries; Entries = entries;
do
{
Serial = m_NextSerial++;
Serial &= 0x7FFFFFFF;
} while (Serial == 0);
Serial = (int)((uint)Serial | 0x80000000);
} }
public string Question { get; } public string Question { get; }
public ItemListEntry[] Entries { get; set; } public ItemListEntry[] Entries { get; set; }
public int Serial { get; } public override int EntryLength => Entries.Length;
public int EntryLength => Entries.Length; public override void SendTo(NetState state)
public virtual void OnCancel(NetState state)
{
}
public virtual void OnResponse(NetState state, int index)
{
}
public void SendTo(NetState state)
{ {
state.AddMenu(this); state.AddMenu(this);
state.SendDisplayItemListMenu(this); state.SendDisplayItemListMenu(this);

View file

@ -2,39 +2,21 @@ using Server.Network;
namespace Server.Menus.Questions; namespace Server.Menus.Questions;
public class QuestionMenu : IMenu public class QuestionMenu : BaseMenu
{ {
private static int m_NextSerial;
public QuestionMenu(string question, string[] answers) public QuestionMenu(string question, string[] answers)
{ {
Question = question?.Trim() ?? ""; Question = question?.Trim() ?? "";
Answers = answers; Answers = answers;
do
{
Serial = ++m_NextSerial;
Serial &= 0x7FFFFFFF;
} while (Serial == 0);
} }
public string Question { get; } public string Question { get; }
public string[] Answers { get; } public string[] Answers { get; }
public int Serial { get; } public override int EntryLength => Answers.Length;
public int EntryLength => Answers.Length; public override void SendTo(NetState state)
public virtual void OnCancel(NetState state)
{
}
public virtual void OnResponse(NetState state, int index)
{
}
public void SendTo(NetState state)
{ {
state.AddMenu(this); state.AddMenu(this);
state.SendDisplayQuestionMenu(this); state.SendDisplayQuestionMenu(this);

View file

@ -0,0 +1,134 @@
using Server;
using Server.Engines.Craft;
using Server.Items;
using Server.Mobiles;
using Xunit;
namespace UOContent.Tests;
[Collection("Sequential UOContent Tests")]
public class T2AJewelGemCraftTests
{
// Y=500 keeps us inside Felucca bounds; X offset avoids other sequential tests.
private static PlayerMobile CreatePlayerMobile(Map map, Point3D location)
{
var m = new PlayerMobile(World.NewMobile);
m.DefaultMobileInit();
m.MoveToWorld(location, map);
m.AddItem(new Backpack());
return m;
}
// Builds a minimal tinkering ring recipe (2 iron ingots) without relying on
// DefTinkering.InitCraftList, which is gated on the feature flag at startup.
private static CraftItem MakeRingRecipe()
{
var item = new CraftItem(typeof(GoldRing), "ring", "gold ring");
item.AddRes(typeof(IronIngot), "iron ingot", 2, "You do not have enough ingots.");
return item;
}
// Ensures DefTinkering.CraftSystem is available (not called by the test fixture).
private static CraftSystem GetOrInitTinkeringSystem()
{
if (DefTinkering.CraftSystem == null)
{
DefTinkering.Initialize();
}
return DefTinkering.CraftSystem;
}
[Fact]
public void OnCraft_ConsumesEntireTargetedGemStack_AndNamesByCount()
{
var map = Map.Felucca;
var player = CreatePlayerMobile(map, new Point3D(4100, 500, 0));
var ring = new GoldRing();
try
{
var pack = player.Backpack;
pack.AddItem(new IronIngot(10));
pack.AddItem(new Diamond(50)); // a stack of 50 diamonds
var system = GetOrInitTinkeringSystem();
var context = system.GetContext(player);
context.PendingGemType = GemType.Diamond;
context.PendingGemCount = 50;
ring.OnCraft(1, false, player, system, typeof(IronIngot), null, MakeRingRecipe(), 0);
Assert.Equal(0, pack.GetAmount(typeof(Diamond))); // all 50 consumed
Assert.Equal(GemType.Diamond, ring.GemType);
Assert.Equal(50, ring.GemCount);
// pending state cleared so the next craft starts fresh
Assert.Equal(GemType.None, context.PendingGemType);
Assert.Equal(0, context.PendingGemCount);
}
finally
{
ring.Delete();
player.Delete();
}
}
[Fact]
public void OnCraft_WithUnsetGemContext_LeavesPlainPiece()
{
var map = Map.Felucca;
var player = CreatePlayerMobile(map, new Point3D(4120, 500, 0));
var ring = new GoldRing();
try
{
player.Backpack.AddItem(new IronIngot(10));
var system = GetOrInitTinkeringSystem();
var context = system.GetContext(player);
context.PendingGemType = GemType.None; // no gem targeted
context.PendingGemCount = 0;
ring.OnCraft(1, false, player, system, typeof(IronIngot), null, MakeRingRecipe(), 0);
Assert.Equal(GemType.None, ring.GemType);
Assert.Equal(0, ring.GemCount);
}
finally
{
ring.Delete();
player.Delete();
}
}
[Fact]
public void OnCraft_WhenGemsUnavailableAtCraftTime_CraftsPlainPiece()
{
var map = Map.Felucca;
var player = CreatePlayerMobile(map, new Point3D(4140, 500, 0));
var ring = new GoldRing();
try
{
player.Backpack.AddItem(new IronIngot(10));
// deliberately do NOT add any diamonds
var system = GetOrInitTinkeringSystem();
var context = system.GetContext(player);
context.PendingGemType = GemType.Diamond;
context.PendingGemCount = 5;
ring.OnCraft(1, false, player, system, typeof(IronIngot), null, MakeRingRecipe(), 0);
Assert.Equal(GemType.None, ring.GemType);
Assert.Equal(0, ring.GemCount);
Assert.Equal(GemType.None, context.PendingGemType);
Assert.Equal(0, context.PendingGemCount);
}
finally
{
ring.Delete();
player.Delete();
}
}
}

View file

@ -1,4 +1,3 @@
using Server.Commands;
using Server.Network; using Server.Network;
using Server.Targeting; using Server.Targeting;

View file

@ -1,3 +1,4 @@
using Server.Engines.Craft.T2A;
using Server.Network; using Server.Network;
namespace Server namespace Server
@ -12,6 +13,7 @@ namespace Server
Mobile.VisibleDamageType = visibleDamage ? VisibleDamageType.Related : VisibleDamageType.None; Mobile.VisibleDamageType = visibleDamage ? VisibleDamageType.Related : VisibleDamageType.None;
Mobile.GuildClickMessage = ServerConfiguration.GetSetting("guildClickMessage", !Core.AOS); Mobile.GuildClickMessage = ServerConfiguration.GetSetting("guildClickMessage", !Core.AOS);
Mobile.AsciiClickMessage = ServerConfiguration.GetSetting("asciiClickMessage", !Core.AOS); Mobile.AsciiClickMessage = ServerConfiguration.GetSetting("asciiClickMessage", !Core.AOS);
T2ACraftSystem.Enabled = ServerConfiguration.GetSetting("t2aCraftMenus", !Core.UOTD);
Mobile.ActionDelay = ServerConfiguration.GetSetting("actionDelay", Core.AOS ? 1000 : 500); Mobile.ActionDelay = ServerConfiguration.GetSetting("actionDelay", Core.AOS ? 1000 : 500);

View file

@ -1,7 +1,5 @@
using System; using System;
using System.Collections.Generic;
using ModernUO.Serialization; using ModernUO.Serialization;
using Server.Collections;
using Server.Gumps; using Server.Gumps;
using Server.Network; using Server.Network;

View file

@ -1,5 +1,4 @@
using ModernUO.Serialization; using ModernUO.Serialization;
using Server.Gumps;
namespace Server.Engines.ConPVP; namespace Server.Engines.ConPVP;

View file

@ -1,4 +1,5 @@
using System.Collections.Generic; using System.Collections.Generic;
using Server.Items;
namespace Server.Engines.Craft namespace Server.Engines.Craft
{ {
@ -31,6 +32,13 @@ namespace Server.Engines.Craft
public CraftMarkOption MarkOption { get; set; } public CraftMarkOption MarkOption { get; set; }
// T2A: last hue used for hue-aware crafting (tailoring cloth)
public int LastHue { get; set; } = -1;
// T2A jewelry: transient gem info set by GemSelectTarget, consumed by BaseJewel.OnCraft
public GemType PendingGemType { get; set; }
public int PendingGemCount { get; set; }
public CraftItem LastMade public CraftItem LastMade
{ {
get get

View file

@ -673,7 +673,6 @@ public class CraftGump : DynamicGump
} }
context.DoNotColor = !context.DoNotColor; context.DoNotColor = !context.DoNotColor;
_from.SendGump(new CraftGump(_from, _craftSystem, _tool, null, _page)); _from.SendGump(new CraftGump(_from, _craftSystem, _tool, null, _page));
break; break;

View file

@ -310,8 +310,7 @@ public class CraftGumpItem : DynamicGump
// Back Button // Back Button
if (info.ButtonID == 0) if (info.ButtonID == 0)
{ {
var craftGump = new CraftGump(from, _craftSystem, _tool, null); CraftItem.ShowCraftMenu(from, _craftSystem, _tool, null);
from.SendGump(craftGump);
} }
else // Make Button else // Make Button
{ {
@ -319,7 +318,7 @@ public class CraftGumpItem : DynamicGump
if (num > 0) if (num > 0)
{ {
from.SendGump(new CraftGump(from, _craftSystem, _tool, num)); CraftItem.ShowCraftMenu(from, _craftSystem, _tool, num);
} }
else else
{ {

View file

@ -1,11 +1,13 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using Server.Collections;
using Server.Commands; using Server.Commands;
using Server.Factions; using Server.Factions;
using Server.Gumps; using Server.Gumps;
using Server.Items; using Server.Items;
using Server.Logging; using Server.Logging;
using Server.Mobiles; using Server.Mobiles;
using Server.Engines.Craft.T2A;
namespace Server.Engines.Craft namespace Server.Engines.Craft
{ {
@ -59,25 +61,37 @@ namespace Server.Engines.Craft
0x192C, 0x192D, 0x192E, 0x129F, 0x1930, 0x1931, 0x1932, 0x1934 0x192C, 0x192D, 0x192E, 0x129F, 0x1930, 0x1931, 0x1932, 0x1934
}; };
private static readonly Type[][] m_TypesTable = private static readonly Type[][] m_TypesTable = InitTypesTable();
private static Type[][] InitTypesTable()
{ {
new[] { typeof(Log), typeof(Board) }, List<Type[]> types =
new[] { typeof(HeartwoodLog), typeof(HeartwoodBoard) }, [
new[] { typeof(BloodwoodLog), typeof(BloodwoodBoard) }, [typeof(Log), typeof(Board)],
new[] { typeof(FrostwoodLog), typeof(FrostwoodBoard) }, [typeof(HeartwoodLog), typeof(HeartwoodBoard)],
new[] { typeof(OakLog), typeof(OakBoard) }, [typeof(BloodwoodLog), typeof(BloodwoodBoard)],
new[] { typeof(AshLog), typeof(AshBoard) }, [typeof(FrostwoodLog), typeof(FrostwoodBoard)],
new[] { typeof(YewLog), typeof(YewBoard) }, [typeof(OakLog), typeof(OakBoard)],
new[] { typeof(Leather), typeof(Hides) }, [typeof(AshLog), typeof(AshBoard)],
new[] { typeof(SpinedLeather), typeof(SpinedHides) }, [typeof(YewLog), typeof(YewBoard)],
new[] { typeof(HornedLeather), typeof(HornedHides) }, [typeof(Leather), typeof(Hides)],
new[] { typeof(BarbedLeather), typeof(BarbedHides) }, [typeof(SpinedLeather), typeof(SpinedHides)],
new[] { typeof(BlankMap), typeof(BlankScroll) }, [typeof(HornedLeather), typeof(HornedHides)],
new[] { typeof(Cloth), typeof(UncutCloth) }, [typeof(BarbedLeather), typeof(BarbedHides)],
new[] { typeof(CheeseWheel), typeof(CheeseWedge) }, [typeof(Cloth), typeof(UncutCloth)],
new[] { typeof(Pumpkin), typeof(SmallPumpkin) }, [typeof(CheeseWheel), typeof(CheeseWedge)],
new[] { typeof(WoodenBowlOfPeas), typeof(PewterBowlOfPeas) } [typeof(Pumpkin), typeof(SmallPumpkin)],
}; [typeof(WoodenBowlOfPeas), typeof(PewterBowlOfPeas)]
];
// Gump-based crafting allows blank scrolls as a substitute for blank maps in cartography
if (!T2ACraftSystem.Enabled)
{
types.Add([typeof(BlankMap), typeof(BlankScroll)]);
}
return types.ToArray();
}
private static readonly Type[] m_ColoredItemTable = private static readonly Type[] m_ColoredItemTable =
{ {
@ -668,6 +682,10 @@ namespace Server.Engines.Craft
{ {
amounts[i] = 0; amounts[i] = 0;
} }
else if (isFailure && !Core.UOTD)
{
amounts[i] -= amounts[i] / 2;
}
} }
// We adjust the amount of each resource to consume the max possible // We adjust the amount of each resource to consume the max possible
@ -921,6 +939,12 @@ namespace Server.Engines.Craft
if (!allRequiredSkills || chance <= 0.0) if (!allRequiredSkills || chance <= 0.0)
{ {
from.EndAction<CraftSystem>(); from.EndAction<CraftSystem>();
if (T2ACraftSystem.Enabled)
{
from.SendAsciiMessage("You lack the required skill to craft this item.");
}
else
{
from.SendGump( from.SendGump(
new CraftGump( new CraftGump(
from, from,
@ -929,6 +953,8 @@ namespace Server.Engines.Craft
1044153 // You don't have the required skills to attempt this item. 1044153 // You don't have the required skills to attempt this item.
) )
); );
}
return; return;
} }
@ -951,7 +977,7 @@ namespace Server.Engines.Craft
if (badCraft > 0) if (badCraft > 0)
{ {
from.EndAction<CraftSystem>(); from.EndAction<CraftSystem>();
from.SendGump(new CraftGump(from, craftSystem, tool, badCraft)); ShowCraftMenu(from, craftSystem, tool, badCraft);
return; return;
} }
@ -962,7 +988,7 @@ namespace Server.Engines.Craft
if (!ConsumeRes(from, typeRes, craftSystem, ref resHue, ref maxAmount, ConsumeType.None, ref message)) if (!ConsumeRes(from, typeRes, craftSystem, ref resHue, ref maxAmount, ConsumeType.None, ref message))
{ {
from.EndAction<CraftSystem>(); from.EndAction<CraftSystem>();
from.SendGump(new CraftGump(from, craftSystem, tool, message)); ShowCraftMenu(from, craftSystem, tool, message);
return; return;
} }
@ -971,7 +997,7 @@ namespace Server.Engines.Craft
if (!ConsumeAttributes(from, ref message, false)) if (!ConsumeAttributes(from, ref message, false))
{ {
from.EndAction<CraftSystem>(); from.EndAction<CraftSystem>();
from.SendGump(new CraftGump(from, craftSystem, tool, message)); ShowCraftMenu(from, craftSystem, tool, message);
return; return;
} }
@ -986,6 +1012,293 @@ namespace Server.Engines.Craft
new InternalTimer(from, craftSystem, this, typeRes, tool, iRandom).Start(); new InternalTimer(from, craftSystem, this, typeRes, tool, iRandom).Start();
} }
/// <summary>
/// Hue-aware craft entry point. Used by tailoring to carry the targeted resource hue
/// through the craft timer to CompleteCraft, ensuring only matching-hue resources are consumed.
/// </summary>
public void Craft(Mobile from, CraftSystem craftSystem, Type typeRes, BaseTool tool, int resHue)
{
if (!from.BeginAction<CraftSystem>())
{
from.SendLocalizedMessage(500119); // You must wait to perform another action
return;
}
if (RequiredExpansion != Expansion.None && from.NetState?.SupportsExpansion(RequiredExpansion) != true)
{
from.EndAction<CraftSystem>();
from.SendGump(
new CraftGump(
from,
craftSystem,
tool,
RequiredExpansionMessage(RequiredExpansion)
)
);
return;
}
var chance = GetSuccessChance(from, typeRes, craftSystem, false, out var allRequiredSkills);
if (!allRequiredSkills || chance <= 0.0)
{
from.EndAction<CraftSystem>();
from.SendAsciiMessage("You lack the required skill to craft this item.");
return;
}
if (Recipe != null && (from as PlayerMobile)?.HasRecipe(Recipe) == false)
{
from.EndAction<CraftSystem>();
from.SendGump(
new CraftGump(
from,
craftSystem,
tool,
1072847 // You must learn that recipe from a scroll.
)
);
return;
}
var badCraft = craftSystem.CanCraft(from, tool, ItemType);
if (badCraft > 0)
{
from.EndAction<CraftSystem>();
ShowCraftMenu(from, craftSystem, tool, badCraft);
return;
}
// Dry run: check hued resources are available
if (!CheckHuedRes(from, typeRes, craftSystem, resHue))
{
from.EndAction<CraftSystem>();
// You don't have the resources required to make that item.
from.SendLocalizedMessage(502925);
return;
}
TextDefinition message = null;
if (!ConsumeAttributes(from, ref message, false))
{
from.EndAction<CraftSystem>();
ShowCraftMenu(from, craftSystem, tool, message);
return;
}
var context = craftSystem.GetContext(from);
context?.OnMade(this);
var iMin = craftSystem.MinCraftEffect;
var iMax = craftSystem.MaxCraftEffect - iMin + 1;
var iRandom = Utility.Random(iMax);
iRandom += iMin + 1;
new InternalTimer(from, craftSystem, this, typeRes, tool, iRandom, resHue).Start();
}
/// <summary>
/// Checks if the backpack has enough of the specified resource type matching the target hue.
/// Used as a dry-run check before starting the craft timer.
/// </summary>
private bool CheckHuedRes(Mobile from, Type typeRes, CraftSystem craftSystem, int targetHue)
{
var ourPack = from.Backpack;
if (ourPack == null)
{
return false;
}
var resCol = UseSubRes2 ? craftSystem.CraftSubRes2 : craftSystem.CraftSubRes;
for (var i = 0; i < Resources.Count; i++)
{
var craftRes = Resources[i];
var baseType = craftRes.ItemType;
// Resource mutation
if (baseType == resCol.ResType && typeRes != null)
{
baseType = typeRes;
}
// For the primary resource, count only items matching the target hue
if (targetHue >= 0 && i == 0)
{
var amount = GetHuedAmount(ourPack, baseType, targetHue);
if (amount < craftRes.Amount)
{
return false;
}
}
else
{
if (ourPack.GetAmount(baseType) < craftRes.Amount)
{
return false;
}
}
}
return true;
}
/// <summary>
/// Consumes resources from the backpack, restricting the primary resource to items matching the target hue.
/// Returns true on success. On success, the exact targetHue is used as the resHue output.
/// </summary>
private bool ConsumeHuedRes(
Mobile from, Type typeRes, CraftSystem craftSystem, int targetHue,
ref int resHue, ConsumeType consumeType
)
{
var ourPack = from.Backpack;
if (ourPack == null)
{
return false;
}
if (NeedHeat && !Find(from, m_HeatSources))
{
return false;
}
if (NeedOven && !Find(from, m_Ovens))
{
return false;
}
if (NeedMill && !Find(from, m_Mills))
{
return false;
}
var resCol = UseSubRes2 ? craftSystem.CraftSubRes2 : craftSystem.CraftSubRes;
for (var i = 0; i < Resources.Count; i++)
{
var craftRes = Resources[i];
var baseType = craftRes.ItemType;
var amount = craftRes.Amount;
// Resource mutation
if (baseType == resCol.ResType && typeRes != null)
{
baseType = typeRes;
var subResource = resCol.SearchFor(baseType);
if (subResource != null && from.Skills[craftSystem.MainSkill].Base < subResource.RequiredSkill)
{
return false;
}
}
if (consumeType == ConsumeType.Half)
{
amount = Math.Max(1, amount / 2);
}
// For the primary resource, filter by hue
if (targetHue >= 0 && i == 0)
{
if (consumeType == ConsumeType.None)
{
// Dry run: just check amount
if (GetHuedAmount(ourPack, baseType, targetHue) < amount)
{
return false;
}
}
else
{
// Actual consumption: consume only matching-hue items
if (!ConsumeHuedAmount(ourPack, baseType, targetHue, amount))
{
return false;
}
}
}
else
{
// Non-hued resource: normal consumption
if (consumeType == ConsumeType.None)
{
if (ourPack.GetAmount(baseType) < amount)
{
return false;
}
}
else
{
if (!ourPack.ConsumeTotal(baseType, amount))
{
return false;
}
}
}
}
resHue = targetHue;
return true;
}
private static int GetHuedAmount(Container pack, Type type, int hue)
{
var total = 0;
foreach (var item in pack.FindItems(true))
{
if (item.Hue == hue && type.IsInstanceOfType(item))
{
total += item.Amount;
}
}
return total;
}
private static bool ConsumeHuedAmount(Container pack, Type type, int hue, int amount)
{
var remaining = amount;
using var toDelete = PooledRefList<Item>.Create();
foreach (var item in pack.FindItems(true))
{
if (remaining <= 0)
{
break;
}
if (item.Hue != hue || !type.IsInstanceOfType(item))
{
continue;
}
if (item.Amount <= remaining)
{
remaining -= item.Amount;
toDelete.Add(item);
}
else
{
item.Amount -= remaining;
remaining = 0;
}
}
if (remaining > 0)
{
return false;
}
for (var i = 0; i < toDelete.Count; i++)
{
toDelete[i].Delete();
}
return true;
}
private static TextDefinition RequiredExpansionMessage(Expansion expansion) private static TextDefinition RequiredExpansionMessage(Expansion expansion)
{ {
return expansion switch return expansion switch
@ -1007,7 +1320,7 @@ namespace Server.Engines.Craft
{ {
if (tool?.Deleted == false && tool.UsesRemaining > 0) if (tool?.Deleted == false && tool.UsesRemaining > 0)
{ {
from.SendGump(new CraftGump(from, craftSystem, tool, badCraft)); ShowCraftMenu(from, craftSystem, tool, badCraft);
} }
else else
{ {
@ -1036,7 +1349,7 @@ namespace Server.Engines.Craft
{ {
if (tool?.Deleted == false && tool.UsesRemaining > 0) if (tool?.Deleted == false && tool.UsesRemaining > 0)
{ {
from.SendGump(new CraftGump(from, craftSystem, tool, checkMessage)); ShowCraftMenu(from, craftSystem, tool, checkMessage);
} }
else if (checkMessage.Number > 0) else if (checkMessage.Number > 0)
{ {
@ -1076,7 +1389,7 @@ namespace Server.Engines.Craft
{ {
if (tool?.Deleted == false && tool.UsesRemaining > 0) if (tool?.Deleted == false && tool.UsesRemaining > 0)
{ {
from.SendGump(new CraftGump(from, craftSystem, tool, message)); ShowCraftMenu(from, craftSystem, tool, message);
} }
else if (message != null) else if (message != null)
{ {
@ -1093,6 +1406,8 @@ namespace Server.Engines.Craft
return; return;
} }
if (tool != null)
{
tool.UsesRemaining--; tool.UsesRemaining--;
if (craftSystem is DefBlacksmithy) if (craftSystem is DefBlacksmithy)
@ -1117,6 +1432,7 @@ namespace Server.Engines.Craft
{ {
tool.Delete(); tool.Delete();
} }
}
Item item; Item item;
if (customCraft != null) if (customCraft != null)
@ -1229,7 +1545,18 @@ namespace Server.Engines.Craft
} }
else if (tool?.Deleted == false && tool.UsesRemaining > 0) else if (tool?.Deleted == false && tool.UsesRemaining > 0)
{ {
from.SendGump(new CraftGump(from, craftSystem, tool, num)); if (T2ACraftSystem.Enabled)
{
if (num > 0)
{
from.SendLocalizedMessage(num);
}
ShowCraftMenu(from, craftSystem, tool);
}
else
{
ShowCraftMenu(from, craftSystem, tool, num);
}
} }
else if (num > 0) else if (num > 0)
{ {
@ -1243,13 +1570,12 @@ namespace Server.Engines.Craft
{ {
if (tool?.Deleted == false && tool.UsesRemaining > 0) if (tool?.Deleted == false && tool.UsesRemaining > 0)
{ {
from.SendGump(new CraftGump(from, craftSystem, tool, 1044153)); from.SendAsciiMessage("You lack the required skill to craft this item.");
} }
else else
{ {
from.SendLocalizedMessage(1044153); // You don't have the required skills to attempt this item. from.SendLocalizedMessage(1044153); // You don't have the required skills to attempt this item.
} }
return; return;
} }
@ -1260,7 +1586,7 @@ namespace Server.Engines.Craft
{ {
if (tool?.Deleted == false && tool.UsesRemaining > 0) if (tool?.Deleted == false && tool.UsesRemaining > 0)
{ {
from.SendGump(new CraftGump(from, craftSystem, tool, message)); ShowCraftMenu(from, craftSystem, tool, message);
} }
else if (message != null) else if (message != null)
{ {
@ -1277,6 +1603,8 @@ namespace Server.Engines.Craft
return; return;
} }
if (tool != null)
{
tool.UsesRemaining--; tool.UsesRemaining--;
if (tool.UsesRemaining < 1 && tool.BreakOnDepletion) if (tool.UsesRemaining < 1 && tool.BreakOnDepletion)
@ -1288,13 +1616,213 @@ namespace Server.Engines.Craft
{ {
tool.Delete(); tool.Delete();
} }
}
// SkillCheck failed. // SkillCheck failed.
num = craftSystem.PlayEndingEffect(from, true, true, toolBroken, endquality, false, this); num = craftSystem.PlayEndingEffect(from, true, true, toolBroken, endquality, false, this);
if (!tool.Deleted && tool.UsesRemaining > 0) if (tool?.Deleted == false && tool.UsesRemaining > 0)
{ {
from.SendGump(new CraftGump(from, craftSystem, tool, num)); ShowCraftMenu(from, craftSystem, tool, num);
}
else if (num > 0)
{
from.SendLocalizedMessage(num);
}
}
/// <summary>
/// Hue-aware CompleteCraft. Uses ConsumeHuedRes to consume only resources matching
/// the target hue, and applies that hue to the crafted item.
/// </summary>
public void CompleteCraft(
int quality, bool makersMark, Mobile from, CraftSystem craftSystem, Type typeRes,
BaseTool tool, CustomCraft customCraft, int targetHue
)
{
var badCraft = craftSystem.CanCraft(from, tool, ItemType);
if (badCraft > 0)
{
if (tool?.Deleted == false && tool.UsesRemaining > 0)
{
ShowCraftMenu(from, craftSystem, tool, badCraft);
}
else
{
from.SendLocalizedMessage(badCraft);
}
return;
}
// Dry-run check with hue filtering
var checkResHue = 0;
if (!ConsumeHuedRes(from, typeRes, craftSystem, targetHue, ref checkResHue, ConsumeType.None))
{
if (tool?.Deleted == false && tool.UsesRemaining > 0)
{
// You don't have the resources required to make that item.
ShowCraftMenu(from, craftSystem, tool, 502925);
}
else
{
from.SendLocalizedMessage(502925);
}
return;
}
TextDefinition checkMessage = null;
if (!ConsumeAttributes(from, ref checkMessage, false))
{
return;
}
var toolBroken = false;
var endquality = 1;
var resHue = 0;
var num = 0;
if (CheckSkills(from, typeRes, craftSystem, ref quality, out var allRequiredSkills))
{
var consumeType = UseAllRes ? ConsumeType.Half : ConsumeType.All;
if (!ConsumeHuedRes(from, typeRes, craftSystem, targetHue, ref resHue, consumeType))
{
if (tool?.Deleted == false && tool.UsesRemaining > 0)
{
ShowCraftMenu(from, craftSystem, tool, 502925);
}
else
{
from.SendLocalizedMessage(502925);
}
return;
}
if (tool != null)
{
tool.UsesRemaining--;
if (craftSystem is DefBlacksmithy)
{
var hammer = from.FindItemOnLayer<AncientSmithyHammer>(Layer.OneHanded);
if (hammer != null && hammer != tool)
{
hammer.UsesRemaining--;
if (hammer.UsesRemaining < 1)
{
hammer.Delete();
}
}
}
if (tool.UsesRemaining < 1 && tool.BreakOnDepletion)
{
toolBroken = true;
}
if (toolBroken)
{
tool.Delete();
}
}
Item item;
if (customCraft != null)
{
item = customCraft.CompleteCraft(out num);
}
else
{
item = ItemType.CreateInstance<Item>();
}
if (item != null)
{
if (item is ICraftable craftable)
{
endquality = craftable.OnCraft(quality, makersMark, from, craftSystem, typeRes, tool, this, resHue);
}
else if (item.Hue == 0)
{
item.Hue = resHue;
}
from.AddToBackpack(item);
num = craftSystem.PlayEndingEffect(from, false, true, toolBroken, endquality, false, this);
if (T2ACraftSystem.Enabled)
{
if (tool?.Deleted == false && tool.UsesRemaining > 0)
{
if (num > 0)
{
from.SendLocalizedMessage(num);
}
ShowCraftMenu(from, craftSystem, tool);
}
else if (num > 0)
{
from.SendLocalizedMessage(num);
}
}
else if (tool?.Deleted == false && tool.UsesRemaining > 0)
{
ShowCraftMenu(from, craftSystem, tool, num);
}
else if (num > 0)
{
from.SendLocalizedMessage(num);
}
return;
}
}
if (!allRequiredSkills)
{
if (tool?.Deleted == false && tool.UsesRemaining > 0)
{
from.SendAsciiMessage("You lack the required skill to craft this item.");
}
else
{
from.SendLocalizedMessage(1044153);
}
return;
}
var failConsumeType = UseAllRes ? ConsumeType.Half : ConsumeType.All;
// Failure: consume resources (half on failure)
ConsumeHuedRes(from, typeRes, craftSystem, targetHue, ref resHue, failConsumeType);
if (tool != null)
{
tool.UsesRemaining--;
if (tool.UsesRemaining < 1 && tool.BreakOnDepletion)
{
toolBroken = true;
}
if (toolBroken)
{
tool.Delete();
}
}
num = craftSystem.PlayEndingEffect(from, true, true, toolBroken, endquality, false, this);
if (tool?.Deleted == false && tool.UsesRemaining > 0)
{
ShowCraftMenu(from, craftSystem, tool, num);
} }
else if (num > 0) else if (num > 0)
{ {
@ -1310,11 +1838,12 @@ namespace Server.Engines.Craft
private readonly int m_iCountMax; private readonly int m_iCountMax;
private readonly BaseTool m_Tool; private readonly BaseTool m_Tool;
private readonly Type m_TypeRes; private readonly Type m_TypeRes;
private readonly int m_ResHue;
private int m_iCount; private int m_iCount;
public InternalTimer( public InternalTimer(
Mobile from, CraftSystem craftSystem, CraftItem craftItem, Type typeRes, BaseTool tool, Mobile from, CraftSystem craftSystem, CraftItem craftItem, Type typeRes, BaseTool tool,
int iCountMax int iCountMax, int resHue = -1
) : base(TimeSpan.Zero, TimeSpan.FromSeconds(craftSystem.Delay), iCountMax) ) : base(TimeSpan.Zero, TimeSpan.FromSeconds(craftSystem.Delay), iCountMax)
{ {
m_From = from; m_From = from;
@ -1324,6 +1853,7 @@ namespace Server.Engines.Craft
m_CraftSystem = craftSystem; m_CraftSystem = craftSystem;
m_TypeRes = typeRes; m_TypeRes = typeRes;
m_Tool = tool; m_Tool = tool;
m_ResHue = resHue;
} }
protected override void OnTick() protected override void OnTick()
@ -1346,7 +1876,7 @@ namespace Server.Engines.Craft
{ {
if (m_Tool?.Deleted == false && m_Tool.UsesRemaining > 0) if (m_Tool?.Deleted == false && m_Tool.UsesRemaining > 0)
{ {
m_From.SendGump(new CraftGump(m_From, m_CraftSystem, m_Tool, badCraft)); ShowCraftMenu(m_From, m_CraftSystem, m_Tool, badCraft);
} }
else else
{ {
@ -1395,7 +1925,9 @@ namespace Server.Engines.Craft
makersMark = m_CraftItem.IsMarkable(m_CraftItem.ItemType); makersMark = m_CraftItem.IsMarkable(m_CraftItem.ItemType);
} }
if (makersMark && context.MarkOption == CraftMarkOption.PromptForMark) // T2A menus always prompt for maker's mark (no auto-mark/don't-mark options)
if (makersMark &&
(T2ACraftSystem.Enabled || context.MarkOption == CraftMarkOption.PromptForMark))
{ {
m_From.SendGump( m_From.SendGump(
new QueryMakersMarkGump( new QueryMakersMarkGump(
@ -1403,7 +1935,8 @@ namespace Server.Engines.Craft
m_CraftItem, m_CraftItem,
m_CraftSystem, m_CraftSystem,
m_TypeRes, m_TypeRes,
m_Tool m_Tool,
m_ResHue
) )
); );
} }
@ -1414,9 +1947,41 @@ namespace Server.Engines.Craft
makersMark = false; makersMark = false;
} }
if (m_ResHue >= 0)
{
m_CraftItem.CompleteCraft(
quality, makersMark, m_From, m_CraftSystem, m_TypeRes, m_Tool, null, m_ResHue
);
}
else
{
m_CraftItem.CompleteCraft(quality, makersMark, m_From, m_CraftSystem, m_TypeRes, m_Tool, null); m_CraftItem.CompleteCraft(quality, makersMark, m_From, m_CraftSystem, m_TypeRes, m_Tool, null);
} }
} }
} }
} }
public static void ShowCraftMenu(Mobile from, CraftSystem system, BaseTool tool, TextDefinition message = null)
{
if (T2ACraftSystem.Enabled)
{
// T2A: Don't reopen menu. Player double-clicks tool to restart.
if (message != null)
{
if (message.Number > 0)
{
from.SendLocalizedMessage(message.Number);
}
else if (!string.IsNullOrEmpty(message.String))
{
from.SendMessage(message.String);
}
}
return;
}
from.SendGump(new CraftGump(from, system, tool, message));
}
}
} }

View file

@ -54,6 +54,8 @@ namespace Server.Engines.Craft
public virtual CraftECA ECA => CraftECA.ChanceMinusSixty; public virtual CraftECA ECA => CraftECA.ChanceMinusSixty;
public virtual bool RequiresTool => true;
public bool Resmelt { get; set; } public bool Resmelt { get; set; }
public bool Repair { get; set; } public bool Repair { get; set; }
@ -103,6 +105,17 @@ namespace Server.Engines.Craft
} }
} }
public void CreateItem(
Mobile from, Type type, Type typeRes, BaseTool tool, CraftItem realCraftItem, int hue
)
{
// Verify if the type is in the list of the craftable item
if (CraftItems.SearchFor(type) != null)
{
realCraftItem.Craft(from, this, typeRes, tool, hue);
}
}
public int RandomRecipe() public int RandomRecipe()
{ {
if (m_Recipes.Count == 0) if (m_Recipes.Count == 0)

View file

@ -1,5 +1,4 @@
using System; using System;
using Server.Gumps;
using Server.Items; using Server.Items;
using Server.Targeting; using Server.Targeting;
@ -348,7 +347,7 @@ namespace Server.Engines.Craft
if (from.Skills[craftSystem.MainSkill].Value < res.RequiredSkill) if (from.Skills[craftSystem.MainSkill].Value < res.RequiredSkill)
{ {
from.SendGump(new CraftGump(from, craftSystem, tool, res.Message)); CraftItem.ShowCraftMenu(from, craftSystem, tool, res.Message);
} }
else else
{ {
@ -362,29 +361,13 @@ namespace Server.Engines.Craft
} }
else else
{ {
from.SendGump( CraftItem.ShowCraftMenu(from, craftSystem, tool, 1061010);
new CraftGump(
from,
craftSystem,
tool,
// You must select a special material in order to enhance an item with its properties.
1061010
)
);
} }
} }
} }
else else
{ {
from.SendGump( CraftItem.ShowCraftMenu(from, craftSystem, tool, 1061010);
new CraftGump(
from,
craftSystem,
tool,
// You must select a special material in order to enhance an item with its properties.
1061010
)
);
} }
} }
@ -435,7 +418,7 @@ namespace Server.Engines.Craft
_ => message _ => message
}; };
from.SendGump(new CraftGump(from, m_CraftSystem, m_Tool, message)); CraftItem.ShowCraftMenu(from, m_CraftSystem, m_Tool, message);
} }
} }
} }

View file

@ -12,17 +12,20 @@ public class QueryMakersMarkGump : StaticGump<QueryMakersMarkGump>
private readonly int _quality; private readonly int _quality;
private readonly BaseTool _tool; private readonly BaseTool _tool;
private readonly Type _typeRes; private readonly Type _typeRes;
private readonly int _resHue;
public override bool Singleton => true; public override bool Singleton => true;
public QueryMakersMarkGump(int quality, CraftItem craftItem, CraftSystem craftSystem, Type typeRes, BaseTool tool) public QueryMakersMarkGump(
: base(100, 200) int quality, CraftItem craftItem, CraftSystem craftSystem, Type typeRes, BaseTool tool, int resHue = -1
) : base(100, 200)
{ {
_quality = quality; _quality = quality;
_craftItem = craftItem; _craftItem = craftItem;
_craftSystem = craftSystem; _craftSystem = craftSystem;
_typeRes = typeRes; _typeRes = typeRes;
_tool = tool; _tool = tool;
_resHue = resHue;
} }
protected override void BuildLayout(ref StaticGumpBuilder builder) protected override void BuildLayout(ref StaticGumpBuilder builder)
@ -55,6 +58,13 @@ public class QueryMakersMarkGump : StaticGump<QueryMakersMarkGump>
from.SendLocalizedMessage(501809); // Cancelled mark. from.SendLocalizedMessage(501809); // Cancelled mark.
} }
if (_resHue >= 0)
{
_craftItem.CompleteCraft(_quality, makersMark, from, _craftSystem, _typeRes, _tool, null, _resHue);
}
else
{
_craftItem.CompleteCraft(_quality, makersMark, from, _craftSystem, _typeRes, _tool, null); _craftItem.CompleteCraft(_quality, makersMark, from, _craftSystem, _typeRes, _tool, null);
} }
}
} }

View file

@ -1,5 +1,5 @@
using System; using System;
using Server.Gumps; using Server.Engines.Craft.T2A;
using Server.Items; using Server.Items;
using Server.Mobiles; using Server.Mobiles;
using Server.Targeting; using Server.Targeting;
@ -482,8 +482,15 @@ namespace Server.Engines.Craft
if (!usingDeed) if (!usingDeed)
{ {
var context = m_CraftSystem.GetContext(from); if (T2ACraftSystem.Enabled)
from.SendGump(new CraftGump(from, m_CraftSystem, m_Tool, number)); {
from.SendLocalizedMessage(number);
CraftItem.ShowCraftMenu(from, m_CraftSystem, m_Tool);
}
else
{
CraftItem.ShowCraftMenu(from, m_CraftSystem, m_Tool, number);
}
} }
else else
{ {

View file

@ -1,5 +1,4 @@
using Server.Ethics; using Server.Ethics;
using Server.Gumps;
using Server.Items; using Server.Items;
using Server.Targeting; using Server.Targeting;
@ -20,7 +19,7 @@ namespace Server.Engines.Craft
if (num > 0 && num != 1044267) if (num > 0 && num != 1044267)
{ {
from.SendGump(new CraftGump(from, craftSystem, tool, num)); CraftItem.ShowCraftMenu(from, craftSystem, tool, num);
} }
else else
{ {
@ -142,7 +141,7 @@ namespace Server.Engines.Craft
} }
} }
from.SendGump(new CraftGump(from, m_CraftSystem, m_Tool, num)); CraftItem.ShowCraftMenu(from, m_CraftSystem, m_Tool, num);
} }
else else
{ {
@ -173,7 +172,7 @@ namespace Server.Engines.Craft
_ => 1044272 _ => 1044272
}; };
from.SendGump(new CraftGump(from, m_CraftSystem, m_Tool, message)); CraftItem.ShowCraftMenu(from, m_CraftSystem, m_Tool, message);
} }
} }
} }

View file

@ -9,6 +9,11 @@ public class DefAlchemy : CraftSystem
public static void Initialize() public static void Initialize()
{ {
if (CraftSystem != null)
{
return; // Already initialized
}
CraftSystem = new DefAlchemy(); CraftSystem = new DefAlchemy();
} }

View file

@ -1,4 +1,5 @@
using System; using System;
using Server.Engines.Craft.T2A;
using Server.Items; using Server.Items;
namespace Server.Engines.Craft; namespace Server.Engines.Craft;
@ -20,9 +21,13 @@ public class DefCartography : CraftSystem
public static CraftSystem CraftSystem { get; private set; } public static CraftSystem CraftSystem { get; private set; }
public override bool RequiresTool => !T2ACraftSystem.Enabled;
public override double GetChanceAtMin(CraftItem item) => 0.0; public override double GetChanceAtMin(CraftItem item) => 0.0;
public override int CanCraft(Mobile from, BaseTool tool, Type itemType) public override int CanCraft(Mobile from, BaseTool tool, Type itemType)
{
if (RequiresTool)
{ {
if (tool?.Deleted != false || tool.UsesRemaining < 0) if (tool?.Deleted != false || tool.UsesRemaining < 0)
{ {
@ -33,6 +38,7 @@ public class DefCartography : CraftSystem
{ {
return 1044263; // The tool must be on your person to use. return 1044263; // The tool must be on your person to use.
} }
}
return 0; return 0;
} }

View file

@ -1,5 +1,6 @@
using System; using System;
using Server.Engines.BulkOrders; using Server.Engines.BulkOrders;
using Server.Engines.Craft.T2A;
using Server.Items; using Server.Items;
using Server.Spells; using Server.Spells;
@ -39,9 +40,13 @@ public class DefInscription : CraftSystem
public static CraftSystem CraftSystem { get; private set; } public static CraftSystem CraftSystem { get; private set; }
public override bool RequiresTool => !T2ACraftSystem.Enabled;
public override double GetChanceAtMin(CraftItem item) => 0.0; public override double GetChanceAtMin(CraftItem item) => 0.0;
public override int CanCraft(Mobile from, BaseTool tool, Type typeItem) public override int CanCraft(Mobile from, BaseTool tool, Type typeItem)
{
if (RequiresTool)
{ {
if (tool?.Deleted != false || tool.UsesRemaining < 0) if (tool?.Deleted != false || tool.UsesRemaining < 0)
{ {
@ -52,6 +57,7 @@ public class DefInscription : CraftSystem
{ {
return 1044263; // The tool must be on your person to use. return 1044263; // The tool must be on your person to use.
} }
}
var scroll = typeItem?.CreateEntityInstance<SpellScroll>(); var scroll = typeItem?.CreateEntityInstance<SpellScroll>();

View file

@ -664,6 +664,13 @@ public class DefTailoring : CraftSystem
AddSubRes(typeof(HornedLeather), 1049152, 80.0, 1044462, 1049311); AddSubRes(typeof(HornedLeather), 1049152, 80.0, 1044462, 1049311);
AddSubRes(typeof(BarbedLeather), 1049153, 99.0, 1044462, 1049311); AddSubRes(typeof(BarbedLeather), 1049153, 99.0, 1044462, 1049311);
// Add Bolt of Cloth for pre-AOS expansions only
if (!Core.AOS)
{
index = AddCraft(typeof(BoltOfCloth), 1015283, 1044286, 0.0, 25.0, typeof(Cloth), 1044286, 50, 1044287);
// 1015283: group (Sashes & Aprons), 1044286: name (Cloth), 0.0-25.0: skill, 50: amount, 1044287: message
}
MarkOption = true; MarkOption = true;
Repair = Core.AOS; Repair = Core.AOS;
CanEnhance = Core.AOS; CanEnhance = Core.AOS;

View file

@ -1,6 +1,6 @@
using System; using System;
using Server.Engines.Craft.T2A;
using Server.Factions; using Server.Factions;
using Server.Gumps;
using Server.Items; using Server.Items;
using Server.Targeting; using Server.Targeting;
@ -71,7 +71,7 @@ public class DefTinkering : CraftSystem
public override bool RetainsColorFrom(CraftItem item, Type type) public override bool RetainsColorFrom(CraftItem item, Type type)
{ {
if (!type.IsSubclassOf(typeof(BaseIngot))) if (!Core.UOTD || !type.IsSubclassOf(typeof(BaseIngot)))
{ {
return false; return false;
} }
@ -377,6 +377,9 @@ public class DefTinkering : CraftSystem
SetNeededExpansion(index, Expansion.SE); SetNeededExpansion(index, Expansion.SE);
} }
if (!T2ACraftSystem.Enabled)
{
// AOS+ jewelry with gem resources
AddJewelrySet(GemType.StarSapphire, typeof(StarSapphire)); AddJewelrySet(GemType.StarSapphire, typeof(StarSapphire));
AddJewelrySet(GemType.Emerald, typeof(Emerald)); AddJewelrySet(GemType.Emerald, typeof(Emerald));
AddJewelrySet(GemType.Sapphire, typeof(Sapphire)); AddJewelrySet(GemType.Sapphire, typeof(Sapphire));
@ -386,6 +389,18 @@ public class DefTinkering : CraftSystem
AddJewelrySet(GemType.Tourmaline, typeof(Tourmaline)); AddJewelrySet(GemType.Tourmaline, typeof(Tourmaline));
AddJewelrySet(GemType.Amber, typeof(Amber)); AddJewelrySet(GemType.Amber, typeof(Amber));
AddJewelrySet(GemType.Diamond, typeof(Diamond)); AddJewelrySet(GemType.Diamond, typeof(Diamond));
}
else
{
// T2A jewelry — ingot-only resources, gem selected via targeting
AddCraft(typeof(GoldNecklace), 1044049, "gold necklace", 40.0, 90.0, typeof(IronIngot), 1044036, 2, 1044037);
AddCraft(typeof(SilverNecklace), 1044049, "silver necklace", 40.0, 90.0, typeof(IronIngot), 1044036, 2, 1044037);
AddCraft(typeof(GoldEarrings), 1044049, "gold earrings", 40.0, 90.0, typeof(IronIngot), 1044036, 2, 1044037);
AddCraft(typeof(SilverEarrings), 1044049, "silver earrings", 40.0, 90.0, typeof(IronIngot), 1044036, 2, 1044037);
AddCraft(typeof(GoldRing), 1044049, "gold ring", 40.0, 90.0, typeof(IronIngot), 1044036, 2, 1044037);
AddCraft(typeof(SilverRing), 1044049, "silver ring", 40.0, 90.0, typeof(IronIngot), 1044036, 2, 1044037);
AddCraft(typeof(WeddingRing), 1044049, "wedding ring", 40.0, 90.0, typeof(IronIngot), 1044036, 2, 1044037);
}
index = AddCraft(typeof(AxleGears), 1044051, 1024177, 0.0, 0.0, typeof(Axle), 1044169, 1, 1044253); index = AddCraft(typeof(AxleGears), 1044051, 1024177, 0.0, 0.0, typeof(Axle), 1044169, 1, 1044253);
AddRes(index, typeof(Gears), 1044254, 1, 1044253); AddRes(index, typeof(Gears), 1044254, 1, 1044253);
@ -714,7 +729,7 @@ public abstract class TrapCraft : CustomCraft
if (tool?.Deleted == false && tool.UsesRemaining > 0) if (tool?.Deleted == false && tool.UsesRemaining > 0)
{ {
from.SendGump(new CraftGump(from, m_TrapCraft.CraftSystem, tool, message)); CraftItem.ShowCraftMenu(from, m_TrapCraft.CraftSystem, tool, message);
} }
else if (message > 0) else if (message > 0)
{ {

View file

@ -0,0 +1,262 @@
using System;
using Server.Items;
using Server.Menus.ItemLists;
using Server.Network;
namespace Server.Engines.Craft.T2A;
public class AlchemyMenu : ItemListMenu
{
private enum Category
{
Main,
Refresh,
Agility,
NightSight,
Heal,
Strength,
Poison,
Cure,
Explosion
}
private static readonly Type[] RefreshTypes = [typeof(RefreshPotion), typeof(TotalRefreshPotion)];
private static readonly Type[] AgilityTypes = [typeof(AgilityPotion), typeof(GreaterAgilityPotion)];
private static readonly Type[] NightSightTypes = [typeof(NightSightPotion)];
private static readonly Type[] HealTypes = [typeof(LesserHealPotion), typeof(HealPotion), typeof(GreaterHealPotion)];
private static readonly Type[] StrengthTypes = [typeof(StrengthPotion), typeof(GreaterStrengthPotion)];
private static readonly Type[] PoisonTypes =
[
typeof(LesserPoisonPotion), typeof(PoisonPotion), typeof(GreaterPoisonPotion), typeof(DeadlyPoisonPotion)
];
private static readonly Type[] CureTypes = [typeof(LesserCurePotion), typeof(CurePotion), typeof(GreaterCurePotion)];
private static readonly Type[] ExplosionTypes =
[
typeof(LesserExplosionPotion), typeof(ExplosionPotion), typeof(GreaterExplosionPotion)
];
private static ItemListEntry[] _mainEntries;
private static ItemListEntry[] _refreshEntries;
private static ItemListEntry[] _agilityEntries;
private static ItemListEntry[] _nightSightEntries;
private static ItemListEntry[] _healEntries;
private static ItemListEntry[] _strengthEntries;
private static ItemListEntry[] _poisonEntries;
private static ItemListEntry[] _cureEntries;
private static ItemListEntry[] _explosionEntries;
private readonly Category _category;
private readonly BaseTool _tool;
public AlchemyMenu(Mobile from, BaseTool tool) : this(from, tool, Category.Main)
{
}
private static string GetQuestion(Category category) => category switch
{
Category.Main => "What kind of potion?",
_ => "Which potion would you like to make?"
};
private AlchemyMenu(Mobile from, BaseTool tool, Category category)
: base(GetQuestion(category), BuildFilteredEntries(from, category))
{
_tool = tool;
_category = category;
}
private static string FormatItemName(Type type)
{
var name = type.Name;
if (name.EndsWith("Potion"))
{
name = name[..^6];
}
Span<char> buffer = stackalloc char[name.Length * 2];
var pos = 0;
for (var i = 0; i < name.Length; i++)
{
if (i > 0 && char.IsUpper(name[i]))
{
buffer[pos++] = ' ';
}
buffer[pos++] = char.ToLower(name[i]);
}
return new string(buffer[..pos]);
}
private static ItemListEntry[] BuildStaticEntries(Type[] types)
{
var entries = new ItemListEntry[types.Length];
var count = 0;
var craftItems = DefAlchemy.CraftSystem.CraftItems;
for (var i = 0; i < types.Length; i++)
{
var itemDef = craftItems.SearchFor(types[i]);
if (itemDef == null)
{
continue;
}
entries[count++] = new ItemListEntry(FormatItemName(types[i]), itemDef.ItemId, 0, i);
}
if (count < entries.Length)
{
Array.Resize(ref entries, count);
}
return entries;
}
private static ItemListEntry[] GetStaticEntries(Category category) => category switch
{
Category.Main => Main(),
Category.Refresh => _refreshEntries ??= BuildStaticEntries(RefreshTypes),
Category.Agility => _agilityEntries ??= BuildStaticEntries(AgilityTypes),
Category.NightSight => _nightSightEntries ??= BuildStaticEntries(NightSightTypes),
Category.Heal => _healEntries ??= BuildStaticEntries(HealTypes),
Category.Strength => _strengthEntries ??= BuildStaticEntries(StrengthTypes),
Category.Poison => _poisonEntries ??= BuildStaticEntries(PoisonTypes),
Category.Cure => _cureEntries ??= BuildStaticEntries(CureTypes),
Category.Explosion => _explosionEntries ??= BuildStaticEntries(ExplosionTypes),
_ => null
};
private static Type[] GetTypes(Category category) => category switch
{
Category.Refresh => RefreshTypes,
Category.Agility => AgilityTypes,
Category.NightSight => NightSightTypes,
Category.Heal => HealTypes,
Category.Strength => StrengthTypes,
Category.Poison => PoisonTypes,
Category.Cure => CureTypes,
Category.Explosion => ExplosionTypes,
_ => null
};
public static ItemListEntry[] Main() => _mainEntries ??=
[
new ItemListEntry("Refresh", 0xF0B, 0, (int)Category.Refresh),
new ItemListEntry("Agility", 0xF08, 0, (int)Category.Agility),
new ItemListEntry("Night Sight", 0xF06, 0, (int)Category.NightSight),
new ItemListEntry("Heal", 0xF0C, 0, (int)Category.Heal),
new ItemListEntry("Strength", 0xF09, 0, (int)Category.Strength),
new ItemListEntry("Poison", 0xF0A, 0, (int)Category.Poison),
new ItemListEntry("Cure", 0xF07, 0, (int)Category.Cure),
new ItemListEntry("Explosion", 0xF0D, 0, (int)Category.Explosion)
];
private static ItemListEntry[] BuildFilteredEntries(Mobile from, Category category)
{
if (category == Category.Main)
{
return BuildFilteredMainEntries(from);
}
var types = GetTypes(category);
var staticEntries = GetStaticEntries(category);
if (types == null || staticEntries == null)
{
return [];
}
return T2ACraftSystem.FilterEntries(from, staticEntries, types, DefAlchemy.CraftSystem);
}
private static ItemListEntry[] BuildFilteredMainEntries(Mobile from)
{
var system = DefAlchemy.CraftSystem;
var mainStatic = Main();
var filtered = new ItemListEntry[mainStatic.Length];
var count = 0;
for (var i = 0; i < mainStatic.Length; i++)
{
var entry = mainStatic[i];
var types = GetTypes((Category)entry.CraftIndex);
if (types != null && T2ACraftSystem.AnyCraftableInCategory(from, types, system))
{
filtered[count++] = entry;
}
}
if (count == 0)
{
return [];
}
if (count < filtered.Length)
{
Array.Resize(ref filtered, count);
}
return filtered;
}
private void CraftPotion(Mobile from, Type potionType)
{
if ((from.Backpack?.GetAmount(typeof(Bottle)) ?? 0) == 0)
{
from.SendAsciiMessage("You need an empty bottle to make a potion.");
return;
}
var itemDef = DefAlchemy.CraftSystem.CraftItems.SearchFor(potionType);
if (itemDef == null)
{
return;
}
var num = DefAlchemy.CraftSystem.CanCraft(from, _tool, itemDef.ItemType);
if (num > 0)
{
from.SendLocalizedMessage(num);
return;
}
var res = itemDef.Resources[0];
DefAlchemy.CraftSystem.CreateItem(from, itemDef.ItemType, res.ItemType, _tool, itemDef);
}
public override void OnResponse(NetState state, int index)
{
var from = state.Mobile;
var craftIndex = Entries[index].CraftIndex;
if (_category == Category.Main)
{
var menu = new AlchemyMenu(from, _tool, (Category)craftIndex);
if (menu.Entries.Length == 0)
{
from.SendAsciiMessage("You lack the skill and materials to craft anything in that category.");
return;
}
from.SendMenu(menu);
return;
}
var types = GetTypes(_category);
if (types != null && craftIndex >= 0 && craftIndex < types.Length)
{
CraftPotion(from, types[craftIndex]);
}
}
}

View file

@ -0,0 +1,624 @@
using System;
using Server.Items;
using Server.Menus.ItemLists;
using Server.Network;
using Server.Targeting;
namespace Server.Engines.Craft.T2A;
public class BlacksmithMenu : ItemListMenu
{
private enum Category
{
Main,
Shields,
Weapons,
Armor,
Blades,
Axes,
Maces,
Polearms,
Platemail,
Chainmail,
Ringmail,
Helmets
}
private static readonly Type[] RingmailTypes =
[
typeof(RingmailGloves), typeof(RingmailLegs), typeof(RingmailArms), typeof(RingmailChest)
];
private static readonly Type[] ChainmailTypes =
[
typeof(ChainCoif), typeof(ChainLegs), typeof(ChainChest)
];
private static readonly Type[] PlatemailTypes =
[
typeof(Bascinet), typeof(CloseHelm), typeof(Helmet), typeof(NorseHelm), typeof(PlateHelm),
typeof(PlateArms), typeof(PlateGloves), typeof(PlateGorget), typeof(PlateLegs),
typeof(PlateChest), typeof(FemalePlateChest)
];
private static readonly Type[] ShieldTypes =
[
typeof(Buckler), typeof(BronzeShield), typeof(HeaterShield), typeof(MetalShield),
typeof(MetalKiteShield), typeof(WoodenKiteShield)
];
private static readonly Type[] BladeTypes =
[
typeof(Broadsword), typeof(Cutlass), typeof(Dagger), typeof(Katana),
typeof(Kryss), typeof(Longsword), typeof(Scimitar), typeof(VikingSword)
];
private static readonly Type[] AxeTypes =
[
typeof(Axe), typeof(BattleAxe), typeof(DoubleAxe), typeof(ExecutionersAxe),
typeof(LargeBattleAxe), typeof(TwoHandedAxe), typeof(WarAxe)
];
private static readonly Type[] PolearmTypes =
[
typeof(Bardiche), typeof(Halberd), typeof(WarFork), typeof(ShortSpear), typeof(Spear)
];
private static readonly Type[] MaceTypes =
[
typeof(HammerPick), typeof(Mace), typeof(Maul), typeof(WarMace), typeof(WarHammer)
];
// Non-category actions at the top of the main menu (Repair, Smelt).
// Category CraftIndex values are offset by this count.
private const int MainActionCount = 2;
private static ItemListEntry[] _mainEntries;
private static ItemListEntry[] _weaponEntries;
private static ItemListEntry[] _armorEntries;
private static ItemListEntry[] _shieldEntries;
private static ItemListEntry[] _bladeEntries;
private static ItemListEntry[] _axeEntries;
private static ItemListEntry[] _maceEntries;
private static ItemListEntry[] _polearmEntries;
private static ItemListEntry[] _platemailEntries;
private static ItemListEntry[] _chainmailEntries;
private static ItemListEntry[] _ringmailEntries;
private readonly Category _category;
private readonly BaseTool _tool;
// Resource is always selected before any menu is shown (Lost Lands flow)
public BlacksmithMenu(Mobile from, BaseTool tool) : this(from, tool, Category.Main)
{
}
private static string GetQuestion(Category category) => category switch
{
Category.Main => "What would you like to do?",
Category.Armor => "What kind of armor?",
Category.Shields => "What kind of shield?",
Category.Weapons => "What kind of weapon?",
Category.Blades => "What kind of blade?",
Category.Axes => "What kind of axe?",
Category.Polearms => "What kind of pole arm?",
Category.Maces => "What kind of bludgeoning weapon?",
Category.Ringmail => "What kind of ring armor?",
Category.Chainmail => "What kind of chain armor?",
Category.Platemail => "What kind of plate armor?",
_ => "What would you like to make?"
};
private BlacksmithMenu(Mobile from, BaseTool tool, Category category)
: base(GetQuestion(category), BuildFilteredEntries(from, category))
{
_tool = tool;
_category = category;
}
private static string FormatItemName(Type type)
{
var name = type.Name;
Span<char> buffer = stackalloc char[name.Length * 2];
var pos = 0;
for (var i = 0; i < name.Length; i++)
{
if (i > 0 && char.IsUpper(name[i]))
{
buffer[pos++] = ' ';
}
buffer[pos++] = char.ToLower(name[i]);
}
return new string(buffer[..pos]);
}
private static ItemListEntry[] BuildStaticEntries(Type[] types, string resourceName)
{
var entries = new ItemListEntry[types.Length];
var count = 0;
var craftItems = DefBlacksmithy.CraftSystem.CraftItems;
for (var i = 0; i < types.Length; i++)
{
var itemDef = craftItems.SearchFor(types[i]);
if (itemDef == null)
{
continue;
}
var name = FormatItemName(types[i]);
var res = itemDef.Resources[0];
var itemId = itemDef.ItemId;
if (itemId == 7033)
{
itemId = 7032;
}
entries[count++] = new ItemListEntry($"{name} ({res.Amount} {resourceName})", itemId, 0, i);
}
if (count < entries.Length)
{
Array.Resize(ref entries, count);
}
return entries;
}
private static ItemListEntry[] GetStaticEntries(Category category) => category switch
{
Category.Main => Main(),
Category.Weapons => Weapons(),
Category.Armor => Armor(),
Category.Shields => Shields(),
Category.Blades => Blades(),
Category.Axes => Axes(),
Category.Maces => Maces(),
Category.Polearms => Polearms(),
Category.Platemail => Platemail(),
Category.Chainmail => Chainmail(),
Category.Ringmail => Ringmail(),
_ => null
};
private static Type[] GetTypes(Category category) => category switch
{
Category.Shields => ShieldTypes,
Category.Blades => BladeTypes,
Category.Axes => AxeTypes,
Category.Maces => MaceTypes,
Category.Polearms => PolearmTypes,
Category.Platemail => PlatemailTypes,
Category.Chainmail => ChainmailTypes,
Category.Ringmail => RingmailTypes,
_ => null
};
// Leaf categories under Weapons
private static readonly Category[] WeaponLeafCategories =
[
Category.Blades, Category.Axes, Category.Maces, Category.Polearms
];
// Leaf categories under Armor
private static readonly Category[] ArmorLeafCategories =
[
Category.Ringmail, Category.Chainmail, Category.Platemail
];
public static ItemListEntry[] Main() => _mainEntries ??=
[
new ItemListEntry("Repair", 0x0FAF, 0, 0),
new ItemListEntry("Smelt", 0x0FB1, 0, 1),
new ItemListEntry("Build Armor", 0x13EC, 0, (int)Category.Armor + MainActionCount),
new ItemListEntry("Build Shield", 0x1B74, 0, (int)Category.Shields + MainActionCount),
new ItemListEntry("Build Weapons", 0xF45, 0, (int)Category.Weapons + MainActionCount)
];
public static ItemListEntry[] Weapons() => _weaponEntries ??=
[
new ItemListEntry("Build Blades", 0xF61, 0, (int)Category.Blades),
new ItemListEntry("Build Axes", 0x13FB, 0, (int)Category.Axes),
new ItemListEntry("Build Pole Arms", 0xF4D, 0, (int)Category.Polearms),
new ItemListEntry("Build Bludgeoning Weapons", 0x1407, 0, (int)Category.Maces)
];
public static ItemListEntry[] Armor() => _armorEntries ??=
[
new ItemListEntry("Build Ring Armor", 0x13EC, 0, (int)Category.Ringmail),
new ItemListEntry("Build Chain Armor", 0x13BF, 0, (int)Category.Chainmail),
new ItemListEntry("Build Plate Armor", 0x1415, 0, (int)Category.Platemail)
];
public static ItemListEntry[] Shields() => _shieldEntries ??= BuildStaticEntries(ShieldTypes, "ingots");
public static ItemListEntry[] Blades() => _bladeEntries ??= BuildStaticEntries(BladeTypes, "ingots");
public static ItemListEntry[] Axes() => _axeEntries ??= BuildStaticEntries(AxeTypes, "ingots");
public static ItemListEntry[] Maces() => _maceEntries ??= BuildStaticEntries(MaceTypes, "ingots");
public static ItemListEntry[] Polearms() => _polearmEntries ??= BuildStaticEntries(PolearmTypes, "ingots");
public static ItemListEntry[] Platemail() => _platemailEntries ??= BuildStaticEntries(PlatemailTypes, "ingots");
public static ItemListEntry[] Chainmail() => _chainmailEntries ??= BuildStaticEntries(ChainmailTypes, "ingots");
public static ItemListEntry[] Ringmail() => _ringmailEntries ??= BuildStaticEntries(RingmailTypes, "ingots");
private static Type GetSelectedResourceType(Mobile from)
{
var context = DefBlacksmithy.CraftSystem.GetContext(from);
if (context?.LastResourceIndex >= 0)
{
var res = DefBlacksmithy.CraftSystem.CraftSubRes;
if (context.LastResourceIndex < res.Count)
{
return res[context.LastResourceIndex].ItemType;
}
}
return null;
}
private static ItemListEntry[] BuildFilteredEntries(Mobile from, Category category)
{
// Resource is always selected before any menu is shown
var selectedResType = GetSelectedResourceType(from);
if (category == Category.Main)
{
return BuildFilteredMainEntries(from, selectedResType);
}
if (category == Category.Weapons)
{
return BuildFilteredMidEntries(from, Weapons(), WeaponLeafCategories, selectedResType);
}
if (category == Category.Armor)
{
return BuildFilteredMidEntries(from, Armor(), ArmorLeafCategories, selectedResType);
}
// Leaf category: filter individual items
var types = GetTypes(category);
var staticEntries = GetStaticEntries(category);
if (types == null || staticEntries == null)
{
return [];
}
return T2ACraftSystem.FilterEntries(from, staticEntries, types, DefBlacksmithy.CraftSystem, selectedResType);
}
private static ItemListEntry[] BuildFilteredMainEntries(Mobile from, Type selectedResType)
{
var system = DefBlacksmithy.CraftSystem;
var mainStatic = Main();
var filtered = new ItemListEntry[mainStatic.Length];
var count = 0;
for (var i = 0; i < mainStatic.Length; i++)
{
var entry = mainStatic[i];
// Repair and Smelt are always shown
if (entry.CraftIndex < MainActionCount)
{
filtered[count++] = entry;
continue;
}
var cat = (Category)(entry.CraftIndex - MainActionCount);
if (cat == Category.Shields)
{
if (T2ACraftSystem.AnyCraftableInCategory(from, ShieldTypes, system, selectedResType))
{
filtered[count++] = entry;
}
}
else if (cat == Category.Weapons)
{
if (AnyLeafCraftable(from, system, WeaponLeafCategories, selectedResType))
{
filtered[count++] = entry;
}
}
else if (cat == Category.Armor)
{
if (AnyLeafCraftable(from, system, ArmorLeafCategories, selectedResType))
{
filtered[count++] = entry;
}
}
}
if (count == 0)
{
return [];
}
if (count < filtered.Length)
{
Array.Resize(ref filtered, count);
}
return filtered;
}
private static ItemListEntry[] BuildFilteredMidEntries(
Mobile from, ItemListEntry[] staticEntries, Category[] leafCategories, Type selectedResType
)
{
var system = DefBlacksmithy.CraftSystem;
var filtered = new ItemListEntry[staticEntries.Length];
var count = 0;
for (var i = 0; i < staticEntries.Length; i++)
{
var entry = staticEntries[i];
var leafCat = (Category)entry.CraftIndex;
var types = GetTypes(leafCat);
if (types != null && T2ACraftSystem.AnyCraftableInCategory(from, types, system, selectedResType))
{
filtered[count++] = entry;
}
}
if (count == 0)
{
return [];
}
if (count < filtered.Length)
{
Array.Resize(ref filtered, count);
}
return filtered;
}
private static bool AnyLeafCraftable(
Mobile from, CraftSystem system, Category[] leafCategories, Type selectedResType = null
)
{
for (var i = 0; i < leafCategories.Length; i++)
{
var types = GetTypes(leafCategories[i]);
if (types != null && T2ACraftSystem.AnyCraftableInCategory(from, types, system, selectedResType))
{
return true;
}
}
return false;
}
private void CraftItemFromType(Mobile from, Type itemType)
{
var itemDef = DefBlacksmithy.CraftSystem.CraftItems.SearchFor(itemType);
if (itemDef == null)
{
return;
}
var num = DefBlacksmithy.CraftSystem.CanCraft(from, _tool, itemDef.ItemType);
if (num > 0)
{
from.SendLocalizedMessage(num);
return;
}
var context = DefBlacksmithy.CraftSystem.GetContext(from);
var res = itemDef.UseSubRes2
? DefBlacksmithy.CraftSystem.CraftSubRes2
: DefBlacksmithy.CraftSystem.CraftSubRes;
var resIndex = itemDef.UseSubRes2 ? context.LastResourceIndex2 : context.LastResourceIndex;
var type = resIndex > -1 ? res[resIndex].ItemType : null;
DefBlacksmithy.CraftSystem.CreateItem(from, itemDef.ItemType, type, _tool, itemDef);
}
public override void OnResponse(NetState state, int index)
{
var from = state.Mobile;
var craftIndex = Entries[index].CraftIndex;
if (_category == Category.Main)
{
switch (craftIndex)
{
case 0:
Repair.Do(from, DefBlacksmithy.CraftSystem, _tool);
return;
case 1:
Resmelt.Do(from, DefBlacksmithy.CraftSystem, _tool);
return;
}
var menu = new BlacksmithMenu(from, _tool, (Category)(craftIndex - MainActionCount));
if (menu.Entries.Length == 0)
{
from.SendAsciiMessage("You lack the skill and materials to craft anything in that category.");
return;
}
from.SendMenu(menu);
return;
}
if (_category is Category.Weapons or Category.Armor)
{
var menu = new BlacksmithMenu(from, _tool, (Category)craftIndex);
if (menu.Entries.Length == 0)
{
from.SendAsciiMessage("You lack the skill and materials to craft anything in that category.");
return;
}
from.SendMenu(menu);
return;
}
var types = GetTypes(_category);
if (types != null && craftIndex >= 0 && craftIndex < types.Length)
{
CraftItemFromType(from, types[craftIndex]);
}
}
public override void OnCancel(NetState state)
{
base.OnCancel(state);
}
public static void ResourceSelection(
Mobile from, BaseTool tool, Action<Mobile, BaseTool> afterSelect, Item preTarget = null
)
{
var res = DefBlacksmithy.CraftSystem.CraftSubRes;
// Validate preTarget first — reject invalid targets before any auto-selection
if (preTarget != null)
{
if (TrySelectResource(from, preTarget, res, afterSelect, tool))
{
// Valid ingot — resource selected, menu will open
return;
}
if (preTarget is BaseArmor or BaseWeapon
&& DefBlacksmithy.CraftSystem.CraftItems.SearchForSubclass(preTarget.GetType()) != null)
{
// Repairable/smeltable item — auto-select default resource and open menu
SelectDefaultResource(from, res);
afterSelect(from, tool);
return;
}
// Invalid target — prompt for ingots
from.SendMessage("Target the ingots you wish to use.");
from.Target = new BlacksmithResourceTarget(tool, afterSelect);
return;
}
// No target (make-last failure path) — auto-select if only one ingot type available
var availableCount = 0;
var lastAvailable = -1;
for (var i = 0; i < res.Count; ++i)
{
if ((from.Backpack?.GetAmount(res[i].ItemType) ?? 0) > 0)
{
availableCount++;
lastAvailable = i;
}
}
if (availableCount <= 1)
{
var context = DefBlacksmithy.CraftSystem.GetContext(from);
if (context != null && lastAvailable != -1)
{
context.LastResourceIndex = lastAvailable;
}
afterSelect(from, tool);
}
else
{
from.SendMessage("Target the ingots you wish to use.");
from.Target = new BlacksmithResourceTarget(tool, afterSelect);
}
}
private static void SelectDefaultResource(Mobile from, CraftSubResCol res)
{
var context = DefBlacksmithy.CraftSystem.GetContext(from);
if (context == null)
{
return;
}
var pack = from.Backpack;
var firstAvailable = -1;
for (var i = 0; i < res.Count; ++i)
{
if ((pack?.GetAmount(res[i].ItemType) ?? 0) > 0)
{
if (res[i].ItemType == typeof(IronIngot))
{
context.LastResourceIndex = i;
return;
}
if (firstAvailable == -1)
{
firstAvailable = i;
}
}
}
if (firstAvailable != -1)
{
context.LastResourceIndex = firstAvailable;
}
}
private static bool TrySelectResource(
Mobile from, Item item, CraftSubResCol res, Action<Mobile, BaseTool> afterSelect, BaseTool tool
)
{
for (var i = 0; i < res.Count; ++i)
{
if (item.GetType() == res[i].ItemType)
{
var context = DefBlacksmithy.CraftSystem.GetContext(from);
if (context != null)
{
context.LastResourceIndex = i;
}
afterSelect(from, tool);
return true;
}
}
return false;
}
}
public class BlacksmithResourceTarget : Target
{
private readonly BaseTool _tool;
private readonly Action<Mobile, BaseTool> _afterSelect;
public BlacksmithResourceTarget(BaseTool tool, Action<Mobile, BaseTool> afterSelect)
: base(2, false, TargetFlags.None)
{
_tool = tool;
_afterSelect = afterSelect;
}
protected override void OnTarget(Mobile from, object targeted)
{
if (targeted is Item item)
{
var res = DefBlacksmithy.CraftSystem.CraftSubRes;
for (var i = 0; i < res.Count; ++i)
{
if (item.GetType() == res[i].ItemType)
{
var context = DefBlacksmithy.CraftSystem.GetContext(from);
context?.LastResourceIndex = i;
_afterSelect(from, _tool);
return;
}
}
}
from.SendMessage("That is not a valid ingot.");
from.Target = new BlacksmithResourceTarget(_tool, _afterSelect);
}
}

View file

@ -0,0 +1,95 @@
using System;
using Server.Items;
using Server.Menus.ItemLists;
using Server.Network;
namespace Server.Engines.Craft.T2A;
public class BowFletchingMenu : ItemListMenu
{
private static readonly Type[] ItemTypes =
[
typeof(Kindling), typeof(Shaft), typeof(Arrow), typeof(Bolt),
typeof(Bow), typeof(Crossbow), typeof(HeavyCrossbow)
];
private static readonly string[] ItemNames =
[
"Kindling", "Shafts", "Arrows", "Bolts", "Bow", "Crossbow", "Heavy Crossbow"
];
private static readonly int[] ItemGraphics = [0xDE1, 0x1BD4, 0xF3F, 0x1BFB, 0x13B2, 0xF50, 0x13FD];
private static ItemListEntry[] _cachedEntries;
private readonly BaseTool _tool;
public BowFletchingMenu(Mobile from, BaseTool tool)
: base("What would you like to make?", BuildFilteredEntries(from))
{
_tool = tool;
}
public static ItemListEntry[] Main()
{
if (_cachedEntries != null)
{
return _cachedEntries;
}
var entries = new ItemListEntry[ItemTypes.Length];
var count = 0;
var craftItems = DefBowFletching.CraftSystem.CraftItems;
for (var i = 0; i < ItemTypes.Length; i++)
{
if (craftItems.SearchFor(ItemTypes[i]) != null)
{
entries[count++] = new ItemListEntry(ItemNames[i], ItemGraphics[i], 0, i);
}
}
if (count < entries.Length)
{
Array.Resize(ref entries, count);
}
_cachedEntries = entries;
return entries;
}
private static ItemListEntry[] BuildFilteredEntries(Mobile from)
{
return T2ACraftSystem.FilterEntries(from, Main(), ItemTypes, DefBowFletching.CraftSystem);
}
public override void OnResponse(NetState state, int index)
{
var from = state.Mobile;
var craftIndex = Entries[index].CraftIndex;
if (craftIndex < 0 || craftIndex >= ItemTypes.Length)
{
return;
}
var itemDef = DefBowFletching.CraftSystem.CraftItems.SearchFor(ItemTypes[craftIndex]);
if (itemDef == null)
{
return;
}
var num = DefBowFletching.CraftSystem.CanCraft(from, _tool, itemDef.ItemType);
if (num > 0)
{
from.SendLocalizedMessage(num);
return;
}
var context = DefBowFletching.CraftSystem.GetContext(from);
var res = itemDef.UseSubRes2 ? DefBowFletching.CraftSystem.CraftSubRes2 : DefBowFletching.CraftSystem.CraftSubRes;
var resIndex = itemDef.UseSubRes2 ? context.LastResourceIndex2 : context.LastResourceIndex;
var type = resIndex > -1 ? res[resIndex].ItemType : null;
DefBowFletching.CraftSystem.CreateItem(from, itemDef.ItemType, type, _tool, itemDef);
}
}

View file

@ -0,0 +1,298 @@
using System;
using Server.Items;
using Server.Menus.ItemLists;
using Server.Network;
namespace Server.Engines.Craft.T2A;
public class CarpentryMenu : ItemListMenu
{
private enum Category
{
Main,
Furniture,
Containers,
Weapons,
Instruments,
Misc,
Addons
}
private static readonly Type[] FurnitureTypes =
[
typeof(FootStool), typeof(Stool), typeof(BambooChair), typeof(WoodenChair),
typeof(FancyWoodenChairCushion), typeof(WoodenChairCushion),
typeof(WoodenBench), typeof(WoodenThrone), typeof(Throne),
typeof(Nightstand), typeof(WritingTable), typeof(YewWoodTable), typeof(LargeTable)
];
private static readonly Type[] ContainerTypes =
[
typeof(WoodenBox), typeof(SmallCrate), typeof(MediumCrate), typeof(LargeCrate),
typeof(WoodenChest), typeof(EmptyBookcase), typeof(FancyArmoire), typeof(Armoire),
typeof(Keg)
];
private static readonly Type[] WeaponTypes =
[
typeof(ShepherdsCrook), typeof(QuarterStaff), typeof(GnarledStaff), typeof(WoodenShield)
];
private static readonly Type[] InstrumentTypes =
[
typeof(LapHarp), typeof(Harp), typeof(Drums), typeof(Lute),
typeof(Tambourine), typeof(TambourineTassel)
];
private static readonly Type[] MiscItemTypes =
[
typeof(FishingPole), typeof(BarrelStaves), typeof(BarrelLid),
typeof(ShortMusicStand), typeof(TallMusicStand), typeof(Easel)
];
private static readonly Type[] AddonTypes =
[
typeof(SmallBedSouthDeed), typeof(SmallBedEastDeed),
typeof(LargeBedSouthDeed), typeof(LargeBedEastDeed),
typeof(DartBoardSouthDeed), typeof(DartBoardEastDeed),
typeof(BallotBoxDeed),
typeof(PentagramDeed), typeof(AbbatoirDeed),
typeof(SmallForgeDeed), typeof(LargeForgeEastDeed), typeof(LargeForgeSouthDeed),
typeof(AnvilEastDeed), typeof(AnvilSouthDeed),
typeof(TrainingDummyEastDeed), typeof(TrainingDummySouthDeed),
typeof(PickpocketDipEastDeed), typeof(PickpocketDipSouthDeed),
typeof(Dressform),
typeof(SpinningWheelEastDeed), typeof(SpinningWheelSouthDeed),
typeof(LoomEastDeed), typeof(LoomSouthDeed),
typeof(StoneOvenEastDeed), typeof(StoneOvenSouthDeed),
typeof(FlourMillEastDeed), typeof(FlourMillSouthDeed),
typeof(WaterTroughEastDeed), typeof(WaterTroughSouthDeed)
];
private static ItemListEntry[] _mainEntries;
private static ItemListEntry[] _furnitureEntries;
private static ItemListEntry[] _containerEntries;
private static ItemListEntry[] _weaponEntries;
private static ItemListEntry[] _instrumentEntries;
private static ItemListEntry[] _miscEntries;
private static ItemListEntry[] _addonEntries;
private readonly Category _category;
private readonly BaseTool _tool;
public CarpentryMenu(Mobile from, BaseTool tool) : this(from, tool, Category.Main)
{
}
private static string GetQuestion(Category category) => category switch
{
Category.Main => "What would you like to make?",
Category.Furniture => "What kind of furniture?",
Category.Containers => "What kind of container?",
Category.Weapons => "What kind of weapon?",
Category.Instruments => "What kind of instrument?",
Category.Addons => "What kind of add-on?",
_ => "What would you like to make?"
};
private CarpentryMenu(Mobile from, BaseTool tool, Category category)
: base(GetQuestion(category), BuildFilteredEntries(from, category))
{
_tool = tool;
_category = category;
}
private static string FormatItemName(Type type)
{
var name = type.Name;
Span<char> buffer = stackalloc char[name.Length * 2];
var pos = 0;
for (var i = 0; i < name.Length; i++)
{
if (i > 0 && char.IsUpper(name[i]))
{
buffer[pos++] = ' ';
}
buffer[pos++] = char.ToLower(name[i]);
}
return new string(buffer[..pos]);
}
private static ItemListEntry[] BuildStaticEntries(Type[] types, string resourceName)
{
var entries = new ItemListEntry[types.Length];
var count = 0;
var craftItems = DefCarpentry.CraftSystem.CraftItems;
for (var i = 0; i < types.Length; i++)
{
var itemDef = craftItems.SearchFor(types[i]);
if (itemDef == null)
{
continue;
}
var name = FormatItemName(types[i]);
var res = itemDef.Resources;
string label;
if (res.Count > 1)
{
var secondName = res[1].ItemType == typeof(IronIngot) ? "ingots" : "cloth";
label = $"{name} ({res[0].Amount} {resourceName}, {res[1].Amount} {secondName})";
}
else
{
label = $"{name} ({res[0].Amount} {resourceName})";
}
entries[count++] = new ItemListEntry(label, itemDef.ItemId, 0, i);
}
if (count < entries.Length)
{
Array.Resize(ref entries, count);
}
return entries;
}
private static ItemListEntry[] GetStaticEntries(Category category) => category switch
{
Category.Main => Main(),
Category.Furniture => Furniture(),
Category.Containers => Containers(),
Category.Weapons => Weapons(),
Category.Instruments => Instruments(),
Category.Misc => Misc(),
Category.Addons => Addons(),
_ => null
};
private static Type[] GetTypes(Category category) => category switch
{
Category.Furniture => FurnitureTypes,
Category.Containers => ContainerTypes,
Category.Weapons => WeaponTypes,
Category.Instruments => InstrumentTypes,
Category.Misc => MiscItemTypes,
Category.Addons => AddonTypes,
_ => null
};
public static ItemListEntry[] Main() => _mainEntries ??=
[
new ItemListEntry("Furniture", 0xB57, 0, (int)Category.Furniture),
new ItemListEntry("Containers", 0x9AA, 0, (int)Category.Containers),
new ItemListEntry("Weapons", 0xE89, 0, (int)Category.Weapons),
new ItemListEntry("Instruments", 0xEB3, 0, (int)Category.Instruments),
new ItemListEntry("Miscellaneous", 0xDC0, 0, (int)Category.Misc),
new ItemListEntry("Add-Ons", 0x14F0, 0, (int)Category.Addons)
];
public static ItemListEntry[] Furniture() => _furnitureEntries ??= BuildStaticEntries(FurnitureTypes, "wood");
public static ItemListEntry[] Containers() => _containerEntries ??= BuildStaticEntries(ContainerTypes, "wood");
public static ItemListEntry[] Weapons() => _weaponEntries ??= BuildStaticEntries(WeaponTypes, "wood");
public static ItemListEntry[] Instruments() => _instrumentEntries ??= BuildStaticEntries(InstrumentTypes, "wood");
public static ItemListEntry[] Misc() => _miscEntries ??= BuildStaticEntries(MiscItemTypes, "wood");
public static ItemListEntry[] Addons() => _addonEntries ??= BuildStaticEntries(AddonTypes, "wood");
private static ItemListEntry[] BuildFilteredEntries(Mobile from, Category category)
{
if (category == Category.Main)
{
return BuildFilteredMainEntries(from);
}
var types = GetTypes(category);
var staticEntries = GetStaticEntries(category);
if (types == null || staticEntries == null)
{
return [];
}
return T2ACraftSystem.FilterEntries(from, staticEntries, types, DefCarpentry.CraftSystem);
}
private static ItemListEntry[] BuildFilteredMainEntries(Mobile from)
{
var system = DefCarpentry.CraftSystem;
var mainStatic = Main();
var filtered = new ItemListEntry[mainStatic.Length];
var count = 0;
for (var i = 0; i < mainStatic.Length; i++)
{
var entry = mainStatic[i];
var types = GetTypes((Category)entry.CraftIndex);
if (types != null && T2ACraftSystem.AnyCraftableInCategory(from, types, system))
{
filtered[count++] = entry;
}
}
if (count == 0)
{
return [];
}
if (count < filtered.Length)
{
Array.Resize(ref filtered, count);
}
return filtered;
}
private void CraftSelectedItem(Mobile from, Type itemType)
{
var itemDef = DefCarpentry.CraftSystem.CraftItems.SearchFor(itemType);
if (itemDef == null)
{
return;
}
var num = DefCarpentry.CraftSystem.CanCraft(from, _tool, itemDef.ItemType);
if (num > 0)
{
from.SendLocalizedMessage(num);
return;
}
var context = DefCarpentry.CraftSystem.GetContext(from);
var res = itemDef.UseSubRes2 ? DefCarpentry.CraftSystem.CraftSubRes2 : DefCarpentry.CraftSystem.CraftSubRes;
var resIndex = itemDef.UseSubRes2 ? context.LastResourceIndex2 : context.LastResourceIndex;
var type = resIndex > -1 ? res[resIndex].ItemType : null;
DefCarpentry.CraftSystem.CreateItem(from, itemDef.ItemType, type, _tool, itemDef);
}
public override void OnResponse(NetState state, int index)
{
var from = state.Mobile;
var craftIndex = Entries[index].CraftIndex;
if (_category == Category.Main)
{
var menu = new CarpentryMenu(from, _tool, (Category)craftIndex);
if (menu.Entries.Length == 0)
{
from.SendAsciiMessage("You lack the skill and materials to craft anything in that category.");
return;
}
from.SendMenu(menu);
return;
}
var types = GetTypes(_category);
if (types != null && craftIndex >= 0 && craftIndex < types.Length)
{
CraftSelectedItem(from, types[craftIndex]);
}
}
}

View file

@ -0,0 +1,110 @@
using System;
using Server.Items;
using Server.Menus.ItemLists;
using Server.Network;
namespace Server.Engines.Craft.T2A;
public class CartographyMenu : ItemListMenu
{
private static ItemListEntry[] _cachedEntries;
private readonly BaseTool _tool;
public CartographyMenu(Mobile from, BaseTool tool)
: base("What kind of map?", BuildFilteredEntries(from))
{
_tool = tool;
}
public static ItemListEntry[] Main()
{
if (_cachedEntries != null)
{
return _cachedEntries;
}
var craftItems = DefCartography.CraftSystem.CraftItems;
var entries = new ItemListEntry[craftItems.Count];
var count = 0;
for (var i = 0; i < craftItems.Count; i++)
{
var name = i switch
{
0 => "A map of the local environs.",
1 => "A map suitable for cities.",
2 => "A moderately sized sea chart.",
3 => "A map of the world.",
_ => craftItems[i].ItemType.Name
};
entries[count++] = new ItemListEntry(name, 6511 + i, 0, i);
}
if (count < entries.Length)
{
Array.Resize(ref entries, count);
}
_cachedEntries = entries;
return entries;
}
private static ItemListEntry[] BuildFilteredEntries(Mobile from)
{
var staticEntries = Main();
var craftItems = DefCartography.CraftSystem.CraftItems;
var filtered = new ItemListEntry[staticEntries.Length];
var count = 0;
for (var i = 0; i < staticEntries.Length; i++)
{
var entry = staticEntries[i];
var craftIndex = entry.CraftIndex;
if (craftIndex >= 0 && craftIndex < craftItems.Count)
{
var itemDef = craftItems[craftIndex];
if (T2ACraftSystem.CanCraftItem(from, itemDef, DefCartography.CraftSystem))
{
filtered[count++] = entry;
}
}
}
if (count == 0)
{
return [];
}
if (count < filtered.Length)
{
Array.Resize(ref filtered, count);
}
return filtered;
}
public override void OnResponse(NetState state, int index)
{
var from = state.Mobile;
var craftIndex = Entries[index].CraftIndex;
var craftItems = DefCartography.CraftSystem.CraftItems;
if (craftIndex < 0 || craftIndex >= craftItems.Count)
{
return;
}
var itemDef = craftItems[craftIndex];
var num = DefCartography.CraftSystem.CanCraft(from, _tool, itemDef.ItemType);
if (num > 0)
{
from.SendLocalizedMessage(num);
return;
}
DefCartography.CraftSystem.CreateItem(from, itemDef.ItemType, typeof(BlankMap), _tool, itemDef);
}
}

View file

@ -0,0 +1,334 @@
using System;
using System.Collections.Generic;
using System.Runtime.CompilerServices;
using Server.Items;
using Server.Menus.ItemLists;
using Server.Network;
namespace Server.Engines.Craft.T2A;
public class InscriptionMenu : ItemListMenu
{
private enum Category
{
Main,
Circle1,
Circle2,
Circle3,
Circle4,
Circle5,
Circle6,
Circle7,
Circle8,
Runebook
}
private static readonly ItemListEntry[][] _circleEntries = new ItemListEntry[8][];
private static ItemListEntry[] _mainEntries;
private readonly Category _category;
private readonly BaseTool _tool;
public InscriptionMenu(Mobile from, BaseTool tool) : this(from, tool, Category.Main)
{
}
private static string GetQuestion(Category category) => category switch
{
Category.Main => "Which circle of spells?",
_ => "Which spell would you like to scribe?"
};
private InscriptionMenu(Mobile from, BaseTool tool, Category category)
: base(GetQuestion(category), BuildFilteredEntries(from, category))
{
_tool = tool;
_category = category;
}
private static string FormatScrollName(Type type)
{
var name = type.Name;
if (name.EndsWith("Scroll"))
{
name = name[..^6];
}
Span<char> buffer = stackalloc char[name.Length * 2];
var pos = 0;
for (var i = 0; i < name.Length; i++)
{
if (i > 0 && char.IsUpper(name[i]))
{
buffer[pos++] = ' ';
}
buffer[pos++] = char.ToLower(name[i]);
}
return new string(buffer[..pos]);
}
public static ItemListEntry[] Main() => _mainEntries ??=
[
new ItemListEntry("Runebook", 0xEFA, 0, (int)Category.Runebook),
new ItemListEntry("First Circle", 8384, 0, (int)Category.Circle1),
new ItemListEntry("Second Circle", 8385, 0, (int)Category.Circle2),
new ItemListEntry("Third Circle", 8386, 0, (int)Category.Circle3),
new ItemListEntry("Fourth Circle", 8387, 0, (int)Category.Circle4),
new ItemListEntry("Fifth Circle", 8388, 0, (int)Category.Circle5),
new ItemListEntry("Sixth Circle", 8389, 0, (int)Category.Circle6),
new ItemListEntry("Seventh Circle", 8390, 0, (int)Category.Circle7),
new ItemListEntry("Eighth Circle", 8391, 0, (int)Category.Circle8)
];
private static ItemListEntry[] BuildCircleEntries(int circleIndex)
{
var offset = circleIndex * 8;
var craftItems = DefInscription.CraftSystem.CraftItems;
var entries = new ItemListEntry[8];
var count = 0;
for (var i = 0; i < 8; i++)
{
var itemDef = craftItems[offset + i];
var name = FormatScrollName(itemDef.ItemType);
entries[count++] = new ItemListEntry(name, 8320 + offset + i, 0, i);
}
if (count < entries.Length)
{
Array.Resize(ref entries, count);
}
return entries;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static ItemListEntry[] GetCircleEntries(int circleIndex) =>
_circleEntries[circleIndex] ??= BuildCircleEntries(circleIndex);
private static Dictionary<Type, int> _spellIds;
private static object[] _args;
private static bool HasSpellInBook(Mobile from, Type scrollType)
{
if (scrollType == null)
{
return false;
}
_spellIds ??= [];
if (!_spellIds.TryGetValue(scrollType, out var spellId))
{
try
{
_args ??= [1];
var scroll = scrollType.CreateInstance<SpellScroll>(_args);
if (scroll != null)
{
spellId = _spellIds[scrollType] = scroll.SpellID;
scroll.Delete();
}
else
{
return false;
}
}
catch
{
return false;
}
}
var book = Spellbook.Find(from, spellId);
return book?.HasSpell(spellId) ?? false;
}
private static bool CanScribeScroll(Mobile from, CraftItem itemDef)
{
// Skill check
if (!T2ACraftSystem.CanCraftItem(from, itemDef, DefInscription.CraftSystem))
{
return false;
}
// Must have blank scrolls
if ((from.Backpack?.GetAmount(typeof(BlankScroll)) ?? 0) == 0)
{
return false;
}
// Must have the spell in spellbook
return HasSpellInBook(from, itemDef.ItemType);
}
private static ItemListEntry[] BuildFilteredEntries(Mobile from, Category category)
{
if (category == Category.Main)
{
return BuildFilteredMainEntries(from);
}
var circleIndex = (int)category - 1;
var staticEntries = GetCircleEntries(circleIndex);
var craftItems = DefInscription.CraftSystem.CraftItems;
var offset = circleIndex * 8;
var filtered = new ItemListEntry[staticEntries.Length];
var count = 0;
for (var i = 0; i < staticEntries.Length; i++)
{
var entry = staticEntries[i];
var scrollIndex = entry.CraftIndex;
var itemDef = craftItems[offset + scrollIndex];
if (CanScribeScroll(from, itemDef))
{
filtered[count++] = entry;
}
}
if (count == 0)
{
return [];
}
if (count < filtered.Length)
{
Array.Resize(ref filtered, count);
}
return filtered;
}
private static ItemListEntry[] BuildFilteredMainEntries(Mobile from)
{
var craftItems = DefInscription.CraftSystem.CraftItems;
var system = DefInscription.CraftSystem;
var mainStatic = Main();
var filtered = new ItemListEntry[mainStatic.Length];
var count = 0;
for (var i = 0; i < mainStatic.Length; i++)
{
var entry = mainStatic[i];
var category = (Category)entry.CraftIndex;
if (category == Category.Runebook)
{
if (T2ACraftSystem.CanCraftItem(from, typeof(Runebook), system))
{
filtered[count++] = entry;
}
continue;
}
var circleIndex = (int)category - 1;
var offset = circleIndex * 8;
var hasAny = false;
for (var j = 0; j < 8; j++)
{
var itemDef = craftItems[offset + j];
if (CanScribeScroll(from, itemDef))
{
hasAny = true;
break;
}
}
if (hasAny)
{
filtered[count++] = entry;
}
}
if (count == 0)
{
return [];
}
if (count < filtered.Length)
{
Array.Resize(ref filtered, count);
}
return filtered;
}
private void CraftScroll(Mobile from, Category circle, int scrollIndex)
{
var itemIndex = ((int)circle - 1) * 8 + scrollIndex;
var craftItems = DefInscription.CraftSystem.CraftItems;
var itemDef = craftItems[itemIndex];
if (!HasSpellInBook(from, itemDef.ItemType))
{
from.SendAsciiMessage("You do not have that spell in your spellbook.");
return;
}
var context = DefInscription.CraftSystem.GetContext(from);
var res = itemDef.UseSubRes2 ? DefInscription.CraftSystem.CraftSubRes2 : DefInscription.CraftSystem.CraftSubRes;
var resIndex = itemDef.UseSubRes2 ? context.LastResourceIndex2 : context.LastResourceIndex;
var type = resIndex > -1 ? res[resIndex].ItemType : null;
DefInscription.CraftSystem.CreateItem(from, itemDef.ItemType, type, _tool, itemDef);
}
public override void OnResponse(NetState state, int index)
{
var from = state.Mobile;
if ((from.Backpack?.GetAmount(typeof(BlankScroll)) ?? 0) == 0)
{
from.SendAsciiMessage("You do not have enough blank scrolls to make that.");
return;
}
var craftIndex = Entries[index].CraftIndex;
if (_category == Category.Main)
{
var category = (Category)craftIndex;
if (category == Category.Runebook)
{
CraftRunebook(from);
return;
}
var menu = new InscriptionMenu(from, _tool, category);
if (menu.Entries.Length == 0)
{
from.SendAsciiMessage("You lack the skill and materials to scribe anything in that circle.");
return;
}
from.SendMenu(menu);
return;
}
CraftScroll(from, _category, craftIndex);
}
private void CraftRunebook(Mobile from)
{
var system = DefInscription.CraftSystem;
var itemDef = system.CraftItems.SearchFor(typeof(Runebook));
if (itemDef == null)
{
return;
}
var num = system.CanCraft(from, _tool, itemDef.ItemType);
if (num > 0)
{
from.SendLocalizedMessage(num);
return;
}
system.CreateItem(from, itemDef.ItemType, null, _tool, itemDef);
}
}

View file

@ -0,0 +1,351 @@
// T2A crafting system: packet-based, not gump-based
using System;
using Server.Items;
using Server.Menus.ItemLists;
using Server.Targeting;
namespace Server.Engines.Craft.T2A;
public static class T2ACraftSystem
{
/// <summary>
/// Whether T2A packet-based crafting menus are active. Set once at startup from the
/// "t2aCraftMenus" server setting (default: !Core.UOTD). Not flippable at runtime.
/// </summary>
public static bool Enabled { get; set; }
public static void ShowMenu(Mobile from, CraftSystem craftSystem, BaseTool tool, Item preTarget = null)
{
if (!Enabled)
{
return;
}
if (craftSystem == DefBlacksmithy.CraftSystem)
{
// Lost Lands flow: resource selection first, then menu
BlacksmithMenu.ResourceSelection(from, tool, (mob, t) =>
{
var menu = new BlacksmithMenu(mob, t);
if (menu.Entries.Length == 0)
{
mob.SendAsciiMessage("You lack the skill and materials to craft anything.");
return;
}
mob.SendMenu(menu);
}, preTarget);
}
else if (craftSystem == DefAlchemy.CraftSystem)
{
if (preTarget is BaseReagent or Bottle || preTarget == null)
{
ShowMenuDirect<AlchemyMenu>(from, tool);
}
else
{
PromptForResource(from, tool, craftSystem, "Target a reagent or empty bottle.",
item => item is BaseReagent or Bottle);
}
}
else if (craftSystem == DefBowFletching.CraftSystem)
{
if (preTarget is Log or Board or Feather or Shaft || preTarget == null)
{
ShowMenuDirect<BowFletchingMenu>(from, tool);
}
else
{
PromptForResource(from, tool, craftSystem, "Target the wood or feathers you wish to use.",
item => item is Log or Board or Feather or Shaft);
}
}
else if (craftSystem == DefCarpentry.CraftSystem)
{
if (preTarget is Log or Board || preTarget == null)
{
ShowMenuDirect<CarpentryMenu>(from, tool);
}
else
{
PromptForResource(from, tool, craftSystem, "Target the wood you wish to use.",
item => item is Log or Board);
}
}
else if (craftSystem == DefCartography.CraftSystem)
{
if (preTarget is BlankMap || preTarget == null)
{
ShowMenuDirect<CartographyMenu>(from, tool);
}
else
{
PromptForResource(from, tool, craftSystem, "Target a blank map.",
item => item is BlankMap);
}
}
else if (craftSystem == DefInscription.CraftSystem)
{
if (preTarget is BlankScroll or BaseReagent or RecallRune || preTarget == null)
{
ShowMenuDirect<InscriptionMenu>(from, tool);
}
else
{
PromptForResource(from, tool, craftSystem, "Target the blank scrolls you wish to use.",
item => item is BlankScroll or BaseReagent or RecallRune);
}
}
else if (craftSystem == DefTailoring.CraftSystem)
{
TailoringMenu.ResourceSelection(from, tool, preTarget);
}
else if (craftSystem == DefTinkering.CraftSystem)
{
TinkeringMenu.ResourceSelection(from, tool, preTarget);
}
}
private static void ShowMenuDirect<T>(Mobile from, BaseTool tool) where T : ItemListMenu
{
var menu = (T)Activator.CreateInstance(typeof(T), from, tool);
if (menu.Entries.Length == 0)
{
from.SendAsciiMessage("You lack the skill and materials to craft anything.");
return;
}
from.SendMenu(menu);
}
private static void PromptForResource(
Mobile from, BaseTool tool, CraftSystem system, string message, Func<Item, bool> isValid
)
{
from.SendAsciiMessage(message);
from.Target = new CraftResourceTarget(tool, system, message, isValid);
}
private class CraftResourceTarget : Target
{
private readonly BaseTool _tool;
private readonly CraftSystem _system;
private readonly string _message;
private readonly Func<Item, bool> _isValid;
public CraftResourceTarget(
BaseTool tool, CraftSystem system, string message, Func<Item, bool> isValid
) : base(12, false, TargetFlags.None)
{
_tool = tool;
_system = system;
_message = message;
_isValid = isValid;
}
protected override void OnTarget(Mobile from, object targeted)
{
if (targeted is Item item && _isValid(item))
{
ShowMenu(from, _system, _tool, item);
return;
}
from.SendAsciiMessage(_message);
from.Target = new CraftResourceTarget(_tool, _system, _message, _isValid);
}
}
/// <summary>
/// Persists the selected resource type as a LastResourceIndex on the craft context,
/// so that make-last can recall which resource was used.
/// </summary>
public static void SetLastResourceIndex(Mobile from, CraftSystem system, Type selectedResourceType)
{
if (selectedResourceType == null)
{
return;
}
var context = system.GetContext(from);
var resCol = system.CraftSubRes;
if (context == null || !resCol.Init)
{
return;
}
for (var i = 0; i < resCol.Count; i++)
{
if (resCol[i].ItemType == selectedResourceType)
{
context.LastResourceIndex = i;
return;
}
}
}
/// <summary>
/// Checks if a player can craft a specific item, accounting for sub-resource types.
/// When <paramref name="selectedResourceType"/> is non-null, checks against that specific sub-resource.
/// When null, checks against ANY available sub-resource the player has sufficient skill and materials for.
/// </summary>
public static bool CanCraftItem(
Mobile from, CraftItem itemDef, CraftSystem system, Type selectedResourceType = null
)
{
var pack = from.Backpack;
if (pack == null)
{
return false;
}
var chance = itemDef.GetSuccessChance(from, selectedResourceType, system, false, out var allRequiredSkills);
if (!allRequiredSkills || chance <= 0.0)
{
return false;
}
var resCol = system.CraftSubRes;
for (var i = 0; i < itemDef.Resources.Count; i++)
{
var res = itemDef.Resources[i];
var resType = res.ItemType;
// If this resource is the base sub-resource type (e.g. IronIngot for blacksmithing),
// handle sub-resource substitution
if (resCol.Init && resType == resCol.ResType)
{
if (selectedResourceType != null)
{
// Specific resource selected — check skill gate for this sub-resource
var subRes = resCol.SearchFor(selectedResourceType);
if (subRes != null && from.Skills[system.MainSkill].Value < subRes.RequiredSkill)
{
return false;
}
// Check if player has enough of the selected resource
if (GetResourceAmount(pack, selectedResourceType) < res.Amount)
{
return false;
}
}
else if (!HasAnySufficientSubResource(from, pack, res.Amount, system, resCol))
{
return false;
}
}
else if (GetResourceAmount(pack, resType) < res.Amount)
{
return false;
}
}
return true;
}
/// <summary>
/// Convenience overload that resolves a Type to a CraftItem first.
/// </summary>
public static bool CanCraftItem(Mobile from, Type itemType, CraftSystem system, Type selectedResourceType = null)
{
var itemDef = system.CraftItems.SearchFor(itemType);
return itemDef != null && CanCraftItem(from, itemDef, system, selectedResourceType);
}
/// <summary>
/// Filters static template entries to only those the player can craft.
/// </summary>
public static ItemListEntry[] FilterEntries(
Mobile from, ItemListEntry[] staticEntries, Type[] types, CraftSystem system, Type selectedResourceType = null
)
{
var filtered = new ItemListEntry[staticEntries.Length];
var count = 0;
for (var i = 0; i < staticEntries.Length; i++)
{
var entry = staticEntries[i];
var typeIndex = entry.CraftIndex;
if (typeIndex >= 0 && typeIndex < types.Length &&
CanCraftItem(from, types[typeIndex], system, selectedResourceType))
{
filtered[count++] = entry;
}
}
if (count == 0)
{
return [];
}
if (count < filtered.Length)
{
Array.Resize(ref filtered, count);
}
return filtered;
}
/// <summary>
/// Returns true if at least one item in the type array is craftable.
/// </summary>
public static bool AnyCraftableInCategory(
Mobile from, Type[] types, CraftSystem system, Type selectedResourceType = null
)
{
for (var i = 0; i < types.Length; i++)
{
if (CanCraftItem(from, types[i], system, selectedResourceType))
{
return true;
}
}
return false;
}
/// <summary>
/// Equivalent type pairs mirroring CraftItem.m_TypesTable — used so that menu filtering
/// counts boards when checking for logs, hides when checking for leather, etc.
/// </summary>
private static readonly Type[][] _equivalentTypes =
[
[typeof(Log), typeof(Board)],
[typeof(Cloth), typeof(UncutCloth)],
[typeof(Items.Leather), typeof(Hides)]
];
private static int GetResourceAmount(Container pack, Type type)
{
for (var i = 0; i < _equivalentTypes.Length; i++)
{
if (_equivalentTypes[i][0] == type)
{
return pack.GetAmount(_equivalentTypes[i]);
}
}
return pack.GetAmount(type);
}
private static bool HasAnySufficientSubResource(
Mobile from, Container pack, int amountNeeded, CraftSystem system, CraftSubResCol resCol
)
{
for (var j = 0; j < resCol.Count; j++)
{
var subRes = resCol[j];
if (from.Skills[system.MainSkill].Value >= subRes.RequiredSkill &&
GetResourceAmount(pack, subRes.ItemType) >= amountNeeded)
{
return true;
}
}
return false;
}
}

View file

@ -0,0 +1,71 @@
using Server.Items;
using Server.Targeting;
namespace Server.Engines.Craft.T2A;
public class T2ACraftToolTarget : Target
{
private readonly BaseTool _tool;
private readonly CraftSystem _system;
public T2ACraftToolTarget(BaseTool tool, CraftSystem system) : base(2, false, TargetFlags.None)
{
_tool = tool;
_system = system;
}
protected override void OnTarget(Mobile from, object targeted)
{
if (targeted == _tool)
{
// Make Last: repeat last crafted item
var context = _system.GetContext(from);
var lastMade = context?.LastMade;
if (lastMade != null)
{
var num = _system.CanCraft(from, _tool, lastMade.ItemType);
if (num > 0)
{
from.SendLocalizedMessage(num);
return;
}
var res = lastMade.UseSubRes2 ? _system.CraftSubRes2 : _system.CraftSubRes;
var resIndex = lastMade.UseSubRes2 ? context.LastResourceIndex2 : context.LastResourceIndex;
var type = resIndex > -1 ? res[resIndex].ItemType : null;
// Jewelry requires gem targeting — re-prompt instead of crafting directly
if (typeof(BaseJewel).IsAssignableFrom(lastMade.ItemType))
{
context.PendingGemType = GemType.None;
context.PendingGemCount = 0;
from.SendAsciiMessage("Target the gemstone you wish to use.");
from.Target = new TinkeringMenu.GemSelectTarget(from, _tool, lastMade.ItemType, type);
return;
}
if (context.LastHue >= 0)
{
_system.CreateItem(from, lastMade.ItemType, type, _tool, lastMade, context.LastHue);
}
else
{
_system.CreateItem(from, lastMade.ItemType, type, _tool, lastMade);
}
}
else
{
from.SendAsciiMessage("You have not yet crafted anything.");
T2ACraftSystem.ShowMenu(from, _system, _tool);
}
}
else
{
// Normal flow: pass the targeted object through so resource selection
// can use it directly instead of requiring a second target.
T2ACraftSystem.ShowMenu(from, _system, _tool, targeted as Item);
}
}
}

View file

@ -0,0 +1,375 @@
using System;
using Server.Items;
using Server.Menus.ItemLists;
using Server.Network;
using Server.Targeting;
namespace Server.Engines.Craft.T2A;
public class TailoringMenu : ItemListMenu
{
private enum Category
{
Main,
LeatherMain,
Hats,
Shirts,
Pants,
Misc,
Footwear,
Leather,
Studded,
Female
}
private static readonly Type[] HatsTypes =
[
typeof(SkullCap), typeof(Bandana), typeof(FloppyHat), typeof(Cap), typeof(WideBrimHat),
typeof(StrawHat), typeof(TallStrawHat), typeof(WizardsHat), typeof(Bonnet),
typeof(FeatheredHat), typeof(TricorneHat), typeof(JesterHat)
];
private static readonly Type[] ShirtsTypes =
[
typeof(Doublet), typeof(Shirt), typeof(FancyShirt), typeof(Tunic), typeof(Surcoat), typeof(PlainDress)
];
private static readonly Type[] PantsTypes = [typeof(ShortPants), typeof(LongPants), typeof(Kilt) ];
private static readonly Type[] MiscTypes =
[
typeof(Skirt), typeof(Cloak), typeof(Robe), typeof(JesterSuit), typeof(FancyDress),
typeof(BodySash), typeof(HalfApron), typeof(FullApron)
];
private static readonly Type[] FootwearTypes = [typeof(Sandals), typeof(Shoes), typeof(Boots), typeof(ThighBoots)];
private static readonly Type[] LeatherArmorTypes =
[
typeof(LeatherChest), typeof(LeatherGorget), typeof(LeatherGloves), typeof(LeatherCap),
typeof(LeatherArms), typeof(LeatherLegs)
];
private static readonly Type[] StuddedArmorTypes =
[
typeof(StuddedChest), typeof(StuddedGorget), typeof(StuddedGloves), typeof(StuddedArms), typeof(StuddedLegs)
];
private static readonly Type[] FemaleArmorTypes =
[
typeof(FemaleLeatherChest), typeof(FemaleStuddedChest), typeof(LeatherBustierArms), typeof(StuddedBustierArms),
typeof(FemalePlateChest), typeof(LeatherShorts), typeof(LeatherSkirt)
];
private static ItemListEntry[] _mainEntries;
private static ItemListEntry[] _leatherMainEntries;
private static ItemListEntry[] _hatsEntries;
private static ItemListEntry[] _shirtsEntries;
private static ItemListEntry[] _pantsEntries;
private static ItemListEntry[] _miscEntries;
private static ItemListEntry[] _footwearEntries;
private static ItemListEntry[] _leatherEntries;
private static ItemListEntry[] _studdedEntries;
private static ItemListEntry[] _femaleEntries;
private readonly Category _category;
private readonly BaseTool _tool;
private readonly int _hue;
private static string GetQuestion(Category category) => category switch
{
Category.Main => "What would you like to make?",
Category.LeatherMain => "What would you like to make?",
Category.Hats => "What kind of hat?",
Category.Shirts => "What kind of shirt?",
Category.Pants => "What kind of pants?",
Category.Misc => "What would you like to make?",
Category.Footwear => "What kind of footwear?",
Category.Leather => "What kind of leather armor?",
Category.Studded => "What kind of studded armor?",
Category.Female => "What kind of female leather?",
_ => "What would you like to make?"
};
private TailoringMenu(Mobile from, BaseTool tool, Category category, int hue = -1)
: base(GetQuestion(category), BuildFilteredEntries(from, category))
{
_tool = tool;
_category = category;
_hue = hue;
}
private static string FormatItemName(Type type)
{
var name = type.Name;
Span<char> buffer = stackalloc char[name.Length * 2];
var pos = 0;
for (var i = 0; i < name.Length; i++)
{
if (i > 0 && char.IsUpper(name[i]))
{
buffer[pos++] = ' ';
}
buffer[pos++] = char.ToLower(name[i]);
}
return new string(buffer[..pos]);
}
private static ItemListEntry[] BuildStaticEntries(Type[] types, string resourceName)
{
var entries = new ItemListEntry[types.Length];
var count = 0;
var craftItems = DefTailoring.CraftSystem.CraftItems;
for (var i = 0; i < types.Length; i++)
{
var itemDef = craftItems.SearchFor(types[i]);
if (itemDef == null)
{
continue;
}
var name = FormatItemName(types[i]);
var res = itemDef.Resources[0];
entries[count++] = new ItemListEntry($"{name} ({res.Amount} {resourceName})", itemDef.ItemId, 0, i);
}
if (count < entries.Length)
{
Array.Resize(ref entries, count);
}
return entries;
}
private static ItemListEntry[] GetStaticEntries(Category category) => category switch
{
Category.Main => Main(),
Category.LeatherMain => LeatherMain(),
Category.Hats => Hats(),
Category.Shirts => Shirts(),
Category.Pants => Pants(),
Category.Misc => Misc(),
Category.Footwear => Footwear(),
Category.Leather => Leather(),
Category.Studded => Studded(),
Category.Female => Female(),
_ => null
};
private static Type[] GetTypes(Category category) => category switch
{
Category.Hats => HatsTypes,
Category.Shirts => ShirtsTypes,
Category.Pants => PantsTypes,
Category.Misc => MiscTypes,
Category.Footwear => FootwearTypes,
Category.Leather => LeatherArmorTypes,
Category.Studded => StuddedArmorTypes,
Category.Female => FemaleArmorTypes,
_ => null
};
public static ItemListEntry[] Main() => _mainEntries ??=
[
new ItemListEntry("Build Hats", 0x1718, 0, (int)Category.Hats),
new ItemListEntry("Build Shirts", 0x1517, 0, (int)Category.Shirts),
new ItemListEntry("Build Pants", 0x1539, 0, (int)Category.Pants),
new ItemListEntry("Build Misc", 0x153D, 0, (int)Category.Misc)
];
public static ItemListEntry[] LeatherMain() => _leatherMainEntries ??=
[
new ItemListEntry("Build Shoes", 0x170f, 0, (int)Category.Footwear),
new ItemListEntry("Build Leather Armor", 0x13cc, 0, (int)Category.Leather),
new ItemListEntry("Build Studded Armor", 0x13db, 0, (int)Category.Studded),
new ItemListEntry("Build Female Armor", 0x1c06, 0, (int)Category.Female)
];
public static ItemListEntry[] Hats() => _hatsEntries ??= BuildStaticEntries(HatsTypes, "cloth");
public static ItemListEntry[] Shirts() => _shirtsEntries ??= BuildStaticEntries(ShirtsTypes, "cloth");
public static ItemListEntry[] Pants() => _pantsEntries ??= BuildStaticEntries(PantsTypes, "cloth");
public static ItemListEntry[] Misc() => _miscEntries ??= BuildStaticEntries(MiscTypes, "cloth");
public static ItemListEntry[] Footwear() => _footwearEntries ??= BuildStaticEntries(FootwearTypes, "leather");
public static ItemListEntry[] Leather() => _leatherEntries ??= BuildStaticEntries(LeatherArmorTypes, "leather");
public static ItemListEntry[] Studded() => _studdedEntries ??= BuildStaticEntries(StuddedArmorTypes, "leather");
public static ItemListEntry[] Female() => _femaleEntries ??= BuildStaticEntries(FemaleArmorTypes, "leather");
private static ItemListEntry[] BuildFilteredEntries(Mobile from, Category category)
{
if (category is Category.Main or Category.LeatherMain)
{
return BuildFilteredMainEntries(from, category);
}
var types = GetTypes(category);
var staticEntries = GetStaticEntries(category);
if (types == null || staticEntries == null)
{
return [];
}
return T2ACraftSystem.FilterEntries(from, staticEntries, types, DefTailoring.CraftSystem);
}
private static ItemListEntry[] BuildFilteredMainEntries(Mobile from, Category mainCategory)
{
var system = DefTailoring.CraftSystem;
var mainStatic = GetStaticEntries(mainCategory);
if (mainStatic == null)
{
return [];
}
var filtered = new ItemListEntry[mainStatic.Length];
var count = 0;
for (var i = 0; i < mainStatic.Length; i++)
{
var entry = mainStatic[i];
var types = GetTypes((Category)entry.CraftIndex);
if (types != null && T2ACraftSystem.AnyCraftableInCategory(from, types, system))
{
filtered[count++] = entry;
}
}
if (count == 0)
{
return [];
}
if (count < filtered.Length)
{
Array.Resize(ref filtered, count);
}
return filtered;
}
private void CraftItem(Mobile from, Type itemType)
{
var itemDef = DefTailoring.CraftSystem.CraftItems.SearchFor(itemType);
if (itemDef == null)
{
return;
}
var num = DefTailoring.CraftSystem.CanCraft(from, _tool, itemDef.ItemType);
if (num > 0)
{
from.SendLocalizedMessage(num);
return;
}
var context = DefTailoring.CraftSystem.GetContext(from);
var res = itemDef.UseSubRes2 ? DefTailoring.CraftSystem.CraftSubRes2 : DefTailoring.CraftSystem.CraftSubRes;
var resIndex = itemDef.UseSubRes2 ? context.LastResourceIndex2 : context.LastResourceIndex;
var resourceType = resIndex > -1 ? res[resIndex].ItemType : null;
if (_hue >= 0)
{
// Pipeline B: hue-aware crafting — only consumes resources matching this hue
context.LastHue = _hue;
DefTailoring.CraftSystem.CreateItem(from, itemDef.ItemType, resourceType, _tool, itemDef, _hue);
}
else
{
DefTailoring.CraftSystem.CreateItem(from, itemDef.ItemType, resourceType, _tool, itemDef);
}
}
public override void OnResponse(NetState state, int index)
{
var from = state.Mobile;
var craftIndex = Entries[index].CraftIndex;
if (_category is Category.Main or Category.LeatherMain)
{
// Carry hue through to child menus
var menu = new TailoringMenu(from, _tool, (Category)craftIndex, _hue);
if (menu.Entries.Length == 0)
{
from.SendAsciiMessage("You lack the skill and materials to craft anything in that category.");
return;
}
from.SendMenu(menu);
return;
}
var types = GetTypes(_category);
if (types != null && craftIndex >= 0 && craftIndex < types.Length)
{
CraftItem(from, types[craftIndex]);
}
}
public static void ResourceSelection(Mobile from, BaseTool tool, Item preTarget = null)
{
if (preTarget != null && TrySelectResource(from, tool, preTarget))
{
return;
}
from.SendAsciiMessage("Select the resource you wish to use (cloth, leather, or hides).");
from.Target = new ResourceSelectTarget(from, tool);
}
private static bool TrySelectResource(Mobile from, BaseTool tool, Item targeted)
{
if (targeted is Cloth or UncutCloth)
{
var menu = new TailoringMenu(from, tool, Category.Main, targeted.Hue);
if (menu.Entries.Length == 0)
{
from.SendAsciiMessage("You lack the skill and materials to craft anything.");
return true;
}
from.SendMenu(menu);
return true;
}
if (targeted is Items.Leather or Hides)
{
var menu = new TailoringMenu(from, tool, Category.LeatherMain);
if (menu.Entries.Length == 0)
{
from.SendAsciiMessage("You lack the skill and materials to craft anything.");
return true;
}
from.SendMenu(menu);
return true;
}
return false;
}
private class ResourceSelectTarget : Target
{
private readonly Mobile _from;
private readonly BaseTool _tool;
public ResourceSelectTarget(Mobile from, BaseTool tool) : base(12, false, TargetFlags.None)
{
_from = from;
_tool = tool;
}
protected override void OnTarget(Mobile from, object targeted)
{
if (targeted is Item item && TrySelectResource(from, _tool, item))
{
return;
}
from.SendAsciiMessage("That is not a valid resource. Please select cloth, leather, or hides.");
from.Target = new ResourceSelectTarget(_from, _tool);
}
}
}

View file

@ -0,0 +1,529 @@
using System;
using Server.Items;
using Server.Menus.ItemLists;
using Server.Network;
using Server.Targeting;
namespace Server.Engines.Craft.T2A;
public class TinkeringMenu : ItemListMenu
{
private enum Category
{
Main,
Wood,
Tools,
Parts,
Utensils,
Traps,
Misc,
Jewelry,
Necklaces,
Earrings,
Rings,
Keg
}
private static readonly Type[] WoodItemTypes =
[
typeof(JointingPlane), typeof(MouldingPlane), typeof(SmoothingPlane),
typeof(ClockFrame), typeof(Axle), typeof(RollingPin)
];
private static readonly Type[] ToolTypes =
[
typeof(SewingKit), typeof(TinkerTools),
typeof(DrawKnife), typeof(Froe), typeof(Inshave), typeof(Scorp),
typeof(Scissors), typeof(Tongs),
typeof(DovetailSaw), typeof(Saw), typeof(Hammer),
typeof(SmithHammer), typeof(SledgeHammer), typeof(Shovel),
typeof(MortarPestle), typeof(Hatchet), typeof(Pickaxe), typeof(Lockpick)
];
private static readonly Type[] PartTypes =
[
typeof(Gears), typeof(Springs), typeof(Hinge),
typeof(ClockParts), typeof(SextantParts),
typeof(BarrelTap), typeof(BarrelHoops), typeof(AxleGears)
];
private static readonly Type[] UtensilTypes =
[
typeof(ButcherKnife), typeof(Plate), typeof(Cleaver),
typeof(KnifeLeft), typeof(KnifeRight), typeof(SkinningKnife),
typeof(ForkLeft), typeof(ForkRight),
typeof(SpoonLeft), typeof(SpoonRight),
typeof(Goblet), typeof(PewterMug)
];
private static readonly Type[] TrapTypes =
[
typeof(DartTrapCraft), typeof(ExplosionTrapCraft), typeof(PoisonTrapCraft)
];
private static readonly Type[] MiscTypes =
[
typeof(KeyRing), typeof(Key),
typeof(Scales), typeof(Spyglass), typeof(Lantern), typeof(HeatingStand),
typeof(Globe), typeof(Candelabra), typeof(Sextant),
typeof(ClockRight), typeof(ClockLeft)
];
private static readonly Type[] NecklaceTypes = [typeof(GoldNecklace), typeof(SilverNecklace)];
private static readonly Type[] EarringTypes = [typeof(GoldEarrings), typeof(SilverEarrings)];
private static readonly Type[] RingTypes = [typeof(GoldRing), typeof(SilverRing), typeof(WeddingRing)];
// Combined for AnyCraftableInCategory check on the Jewelry parent entry
private static readonly Type[] AllJewelryTypes =
[
typeof(GoldNecklace), typeof(SilverNecklace),
typeof(GoldEarrings), typeof(SilverEarrings),
typeof(GoldRing), typeof(SilverRing), typeof(WeddingRing)
];
private static readonly Type[] KegItemTypes = [typeof(PotionKeg)];
private static ItemListEntry[] _mainEntries;
private static ItemListEntry[] _woodEntries;
private static ItemListEntry[] _toolEntries;
private static ItemListEntry[] _partEntries;
private static ItemListEntry[] _utensilEntries;
private static ItemListEntry[] _trapEntries;
private static ItemListEntry[] _miscEntries;
private static ItemListEntry[] _necklaceEntries;
private static ItemListEntry[] _earringEntries;
private static ItemListEntry[] _ringEntries;
private static ItemListEntry[] _kegEntries;
private readonly Category _category;
private readonly BaseTool _tool;
private readonly Type _selectedResourceType;
private static string GetQuestion(Category category) => category switch
{
Category.Main => "What would you like to make?",
Category.Wood => "What kind of wooden item?",
Category.Tools => "What kind of tool?",
Category.Parts => "What kind of part?",
Category.Utensils => "What kind of utensil?",
Category.Traps => "What kind of trap?",
Category.Misc => "What would you like to make?",
Category.Jewelry => "What kind of jewelry?",
Category.Necklaces => "What kind of necklace?",
Category.Earrings => "What kind of earrings?",
Category.Rings => "What kind of ring?",
Category.Keg => "What would you like to make?",
_ => "What would you like to make?"
};
private TinkeringMenu(Mobile from, BaseTool tool, Category category, Type selectedResourceType)
: base(GetQuestion(category), BuildFilteredEntries(from, category))
{
_tool = tool;
_category = category;
_selectedResourceType = selectedResourceType;
}
private static string FormatItemName(Type type)
{
var name = type.Name;
Span<char> buffer = stackalloc char[name.Length * 2];
var pos = 0;
for (var i = 0; i < name.Length; i++)
{
if (i > 0 && char.IsUpper(name[i]))
{
buffer[pos++] = ' ';
}
buffer[pos++] = char.ToLower(name[i]);
}
return new string(buffer[..pos]);
}
private static ItemListEntry[] BuildStaticEntries(Type[] types, string resourceName)
{
var entries = new ItemListEntry[types.Length];
var count = 0;
var craftItems = DefTinkering.CraftSystem.CraftItems;
for (var i = 0; i < types.Length; i++)
{
var itemDef = craftItems.SearchFor(types[i]);
if (itemDef == null)
{
continue;
}
var name = FormatItemName(types[i]);
var res = itemDef.Resources[0];
entries[count++] = new ItemListEntry($"{name} ({res.Amount} {resourceName})", itemDef.ItemId, 0, i);
}
if (count < entries.Length)
{
Array.Resize(ref entries, count);
}
return entries;
}
private static ItemListEntry[] GetStaticEntries(Category category) => category switch
{
Category.Main => Main(),
Category.Wood => Wood(),
Category.Tools => Tools(),
Category.Parts => Parts(),
Category.Utensils => Utensils(),
Category.Traps => Traps(),
Category.Misc => Misc(),
Category.Necklaces => Necklaces(),
Category.Earrings => Earrings(),
Category.Rings => Rings(),
Category.Keg => KegItems(),
_ => null
};
private static Type[] GetTypes(Category category) => category switch
{
Category.Wood => WoodItemTypes,
Category.Tools => ToolTypes,
Category.Parts => PartTypes,
Category.Utensils => UtensilTypes,
Category.Traps => TrapTypes,
Category.Misc => MiscTypes,
Category.Jewelry => AllJewelryTypes,
Category.Necklaces => NecklaceTypes,
Category.Earrings => EarringTypes,
Category.Rings => RingTypes,
Category.Keg => KegItemTypes,
_ => null
};
public static ItemListEntry[] Main() => _mainEntries ??=
[
new ItemListEntry("Wooden Items", 0x1BDD, 0, (int)Category.Wood),
new ItemListEntry("Tools", 0x1EB8, 0, (int)Category.Tools),
new ItemListEntry("Parts", 0x1053, 0, (int)Category.Parts),
new ItemListEntry("Utensils", 0x9D7, 0, (int)Category.Utensils),
new ItemListEntry("Traps", 0x1BFC, 0, (int)Category.Traps),
new ItemListEntry("Miscellaneous", 0xA25, 0, (int)Category.Misc),
new ItemListEntry("Jewelry", 0x1088, 0, (int)Category.Jewelry)
];
public static ItemListEntry[] Wood() => _woodEntries ??= BuildStaticEntries(WoodItemTypes, "logs");
public static ItemListEntry[] Tools() => _toolEntries ??= BuildStaticEntries(ToolTypes, "ingots");
public static ItemListEntry[] Parts() => _partEntries ??= BuildStaticEntries(PartTypes, "ingots");
public static ItemListEntry[] Utensils() => _utensilEntries ??= BuildStaticEntries(UtensilTypes, "ingots");
public static ItemListEntry[] Traps() => _trapEntries ??= BuildStaticEntries(TrapTypes, "ingots");
public static ItemListEntry[] Misc() => _miscEntries ??= BuildStaticEntries(MiscTypes, "ingots");
public static ItemListEntry[] Necklaces() => _necklaceEntries ??= BuildStaticEntries(NecklaceTypes, "ingots");
public static ItemListEntry[] Earrings() => _earringEntries ??= BuildStaticEntries(EarringTypes, "ingots");
public static ItemListEntry[] Rings() => _ringEntries ??= BuildStaticEntries(RingTypes, "ingots");
public static ItemListEntry[] KegItems() => _kegEntries ??= BuildStaticEntries(KegItemTypes, "kegs");
private static ItemListEntry[] BuildFilteredEntries(Mobile from, Category category)
{
if (category == Category.Main)
{
return BuildFilteredMainEntries(from);
}
if (category == Category.Jewelry)
{
return BuildFilteredJewelryEntries(from);
}
var types = GetTypes(category);
var staticEntries = GetStaticEntries(category);
if (types == null || staticEntries == null)
{
return [];
}
return T2ACraftSystem.FilterEntries(from, staticEntries, types, DefTinkering.CraftSystem);
}
private static readonly ItemListEntry[] JewelrySubcategoryEntries =
[
new("Necklaces", 0x1088, 0, (int)Category.Necklaces),
new("Earrings", 0x1087, 0, (int)Category.Earrings),
new("Rings", 0x108a, 0, (int)Category.Rings)
];
private static ItemListEntry[] BuildFilteredJewelryEntries(Mobile from)
{
var system = DefTinkering.CraftSystem;
var filtered = new ItemListEntry[JewelrySubcategoryEntries.Length];
var count = 0;
for (var i = 0; i < JewelrySubcategoryEntries.Length; i++)
{
var entry = JewelrySubcategoryEntries[i];
var types = GetTypes((Category)entry.CraftIndex);
if (types != null && T2ACraftSystem.AnyCraftableInCategory(from, types, system))
{
filtered[count++] = entry;
}
}
if (count == 0)
{
return [];
}
if (count < filtered.Length)
{
Array.Resize(ref filtered, count);
}
return filtered;
}
private static ItemListEntry[] BuildFilteredMainEntries(Mobile from)
{
var system = DefTinkering.CraftSystem;
var mainStatic = Main();
var filtered = new ItemListEntry[mainStatic.Length];
var count = 0;
for (var i = 0; i < mainStatic.Length; i++)
{
var entry = mainStatic[i];
var types = GetTypes((Category)entry.CraftIndex);
if (types != null && T2ACraftSystem.AnyCraftableInCategory(from, types, system))
{
filtered[count++] = entry;
}
}
if (count == 0)
{
return [];
}
if (count < filtered.Length)
{
Array.Resize(ref filtered, count);
}
return filtered;
}
public override void OnResponse(NetState state, int index)
{
var from = state.Mobile;
var craftIndex = Entries[index].CraftIndex;
// Navigation categories: Main → subcategory, Jewelry → subcategory
if (_category is Category.Main or Category.Jewelry)
{
var childCategory = (Category)craftIndex;
var menu = new TinkeringMenu(from, _tool, childCategory, _selectedResourceType);
if (menu.Entries.Length == 0)
{
from.SendAsciiMessage("You lack the skill and materials to craft anything in that category.");
return;
}
from.SendMenu(menu);
return;
}
// Jewelry leaf categories: prompt for gem targeting
if (_category is Category.Necklaces or Category.Earrings or Category.Rings)
{
var types = GetTypes(_category);
if (types == null || craftIndex < 0 || craftIndex >= types.Length)
{
return;
}
var itemType = types[craftIndex];
if (DefTinkering.CraftSystem.CraftItems.SearchFor(itemType) == null)
{
return;
}
// Clear stale gem state from any previous craft attempt (e.g. failed skill check)
var ctx = DefTinkering.CraftSystem.GetContext(from);
if (ctx != null)
{
ctx.PendingGemType = GemType.None;
ctx.PendingGemCount = 0;
}
from.SendAsciiMessage("Target the gemstone you wish to use.");
from.Target = new GemSelectTarget(from, _tool, itemType, _selectedResourceType);
return;
}
// Leaf categories: craft directly
var leafTypes = GetTypes(_category);
if (leafTypes == null || craftIndex < 0 || craftIndex >= leafTypes.Length)
{
return;
}
var leafItemType = leafTypes[craftIndex];
var system = DefTinkering.CraftSystem;
var itemDef = system.CraftItems.SearchFor(leafItemType);
if (itemDef == null || _selectedResourceType == null)
{
return;
}
// Persist selected resource index so make-last remembers it
T2ACraftSystem.SetLastResourceIndex(from, system, _selectedResourceType);
itemDef.Craft(from, system, _selectedResourceType, _tool);
}
public static void ResourceSelection(Mobile from, BaseTool tool, Item preTarget = null)
{
if (preTarget != null && TrySelectResource(from, tool, preTarget))
{
return;
}
from.SendAsciiMessage("Select the resource you wish to use (wood or ingots).");
from.Target = new ResourceSelectTarget(from, tool);
}
private static bool TrySelectResource(Mobile from, BaseTool tool, Item targeted)
{
if (targeted is Log or Board)
{
var menu = new TinkeringMenu(from, tool, Category.Wood, typeof(Log));
if (menu.Entries.Length == 0)
{
from.SendAsciiMessage("You lack the skill and materials to craft anything.");
return true;
}
from.SendMenu(menu);
return true;
}
if (targeted is BaseIngot)
{
var menu = new TinkeringMenu(from, tool, Category.Main, targeted.GetType());
if (menu.Entries.Length == 0)
{
from.SendAsciiMessage("You lack the skill and materials to craft anything.");
return true;
}
from.SendMenu(menu);
return true;
}
if (targeted is Keg)
{
var menu = new TinkeringMenu(from, tool, Category.Keg, typeof(Keg));
if (menu.Entries.Length == 0)
{
from.SendAsciiMessage("You lack the skill and materials to craft anything.");
return true;
}
from.SendMenu(menu);
return true;
}
return false;
}
private class ResourceSelectTarget : Target
{
private readonly Mobile _from;
private readonly BaseTool _tool;
public ResourceSelectTarget(Mobile from, BaseTool tool) : base(12, false, TargetFlags.None)
{
_from = from;
_tool = tool;
}
protected override void OnTarget(Mobile from, object targeted)
{
if (targeted is Item item && TrySelectResource(from, _tool, item))
{
return;
}
from.SendAsciiMessage("That is not a valid resource. Please select wood or ingots.");
from.Target = new ResourceSelectTarget(_from, _tool);
}
}
internal class GemSelectTarget : Target
{
private readonly Mobile _from;
private readonly BaseTool _tool;
private readonly Type _itemType;
private readonly Type _selectedResourceType;
public GemSelectTarget(Mobile from, BaseTool tool, Type itemType, Type selectedResourceType)
: base(12, false, TargetFlags.None)
{
_from = from;
_tool = tool;
_itemType = itemType;
_selectedResourceType = selectedResourceType;
}
protected override void OnTarget(Mobile from, object targeted)
{
if (targeted is not Item gemItem)
{
from.SendAsciiMessage("That is not a gemstone.");
return;
}
var gemType = BaseJewel.GetGemType(gemItem);
if (gemType == GemType.None)
{
from.SendAsciiMessage("That is not a gemstone.");
return;
}
var amount = gemItem.Amount;
if (amount < 1)
{
from.SendAsciiMessage("That gemstone stack is empty.");
return;
}
var system = DefTinkering.CraftSystem;
var itemDef = system.CraftItems.SearchFor(_itemType);
if (itemDef == null)
{
return;
}
// Store pending gem info in craft context
var ctx = system.GetContext(from);
if (ctx == null)
{
return;
}
ctx.PendingGemType = gemType;
ctx.PendingGemCount = amount;
T2ACraftSystem.SetLastResourceIndex(from, system, _selectedResourceType);
itemDef.Craft(from, system, _selectedResourceType, _tool);
}
protected override void OnTargetCancel(Mobile from, TargetCancelType cancelType)
{
if (cancelType == TargetCancelType.Canceled)
{
CraftItem.ShowCraftMenu(from, DefTinkering.CraftSystem, _tool);
}
}
}
}

View file

@ -96,7 +96,7 @@ public class FactionImbueGump : FactionGump
if (m_Tool?.Deleted == false && m_Tool.UsesRemaining > 0) if (m_Tool?.Deleted == false && m_Tool.UsesRemaining > 0)
{ {
m_Mobile.SendGump(new CraftGump(m_Mobile, m_CraftSystem, m_Tool, m_Notice)); CraftItem.ShowCraftMenu(m_Mobile, m_CraftSystem, m_Tool, m_Notice);
} }
else if (m_Notice != null) else if (m_Notice != null)
{ {

View file

@ -5,7 +5,6 @@ using Server.Engines.MLQuests.Gumps;
using Server.Engines.MLQuests.Objectives; using Server.Engines.MLQuests.Objectives;
using Server.Engines.MLQuests.Rewards; using Server.Engines.MLQuests.Rewards;
using Server.Engines.Spawners; using Server.Engines.Spawners;
using Server.Gumps;
using Server.Mobiles; using Server.Mobiles;
namespace Server.Engines.MLQuests namespace Server.Engines.MLQuests

View file

@ -2,7 +2,6 @@ using System;
using System.Collections.Generic; using System.Collections.Generic;
using Server.Engines.MLQuests.Gumps; using Server.Engines.MLQuests.Gumps;
using Server.Engines.MLQuests.Objectives; using Server.Engines.MLQuests.Objectives;
using Server.Gumps;
using Server.Mobiles; using Server.Mobiles;
namespace Server.Engines.MLQuests namespace Server.Engines.MLQuests

View file

@ -1,4 +1,3 @@
using Server.Gumps;
using Server.Targeting; using Server.Targeting;
namespace Server.Engines.Plants namespace Server.Engines.Plants

View file

@ -1,4 +1,3 @@
using Server.Gumps;
using Server.Targeting; using Server.Targeting;
namespace Server.Engines.Plants namespace Server.Engines.Plants

View file

@ -1,7 +1,6 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using Server.Gumps; using Server.Gumps;
using Server.Items;
using Server.Mobiles; using Server.Mobiles;
using Server.Network; using Server.Network;

View file

@ -1,7 +1,6 @@
using System; using System;
using ModernUO.CodeGeneratedEvents; using ModernUO.CodeGeneratedEvents;
using Server.Accounting; using Server.Accounting;
using Server.Gumps;
using Server.Items; using Server.Items;
using Server.Mobiles; using Server.Mobiles;

View file

@ -1,5 +1,3 @@
using System.Collections.Generic;
using System.Linq;
using Server.Mobiles; using Server.Mobiles;
using Server.Multis; using Server.Multis;
using Server.Network; using Server.Network;

View file

@ -18,7 +18,7 @@ public enum GemType
Diamond Diamond
} }
[SerializationGenerator(4, false)] [SerializationGenerator(5, false)]
public abstract partial class BaseJewel : Item, ICraftable, IAosItem public abstract partial class BaseJewel : Item, ICraftable, IAosItem
{ {
[EncodedInt] [EncodedInt]
@ -47,6 +47,11 @@ public abstract partial class BaseJewel : Item, ICraftable, IAosItem
[SerializedCommandProperty(AccessLevel.GameMaster, canModify: true)] [SerializedCommandProperty(AccessLevel.GameMaster, canModify: true)]
private AosSkillBonuses _skillBonuses; private AosSkillBonuses _skillBonuses;
[EncodedInt]
[SerializableField(7)]
[SerializedCommandProperty(AccessLevel.GameMaster)]
private int _gemCount;
public BaseJewel(int itemID, Layer layer) : base(itemID) public BaseJewel(int itemID, Layer layer) : base(itemID)
{ {
_attributes = new AosAttributes(this); _attributes = new AosAttributes(this);
@ -183,9 +188,129 @@ public abstract partial class BaseJewel : Item, ICraftable, IAosItem
} }
} }
// T2A jewelry: read gem info from craft context (set by GemSelectTarget).
// The entire targeted gem stack is consumed and the piece is named by that
// count (e.g. "a 1000 diamond ring").
if (context is { PendingGemType: not GemType.None, PendingGemCount: > 0 })
{
var gemItemType = GetGemItemType(context.PendingGemType);
var gemCount = context.PendingGemCount;
if (gemItemType != null && from.Backpack?.ConsumeTotal(gemItemType, gemCount) == true)
{
GemType = context.PendingGemType;
GemCount = gemCount;
}
else
{
// Gems were no longer available (or unknown type): craft a plain piece
// rather than naming it for gems that were never consumed.
from.SendAsciiMessage("You lack the gemstones to set into this piece.");
}
context.PendingGemType = GemType.None;
context.PendingGemCount = 0;
}
return 1; return 1;
} }
public override void OnSingleClick(Mobile from)
{
if (!Core.UOTD)
{
OnSingleClickPreUOTD(from);
return;
}
base.OnSingleClick(from);
}
public virtual void OnSingleClickPreUOTD(Mobile from)
{
var plural = _gemCount > 1;
string name;
if (this is WeddingRing)
{
name = $"a {Name}";
}
else
{
name = Name;
if (name == null)
{
var articleAnName = (TileData.ItemTable[ItemID].Flags & TileFlag.ArticleAn) != 0;
name = $"{(articleAnName ? "an" : "a")} {Localization.GetText(LabelNumber).ToLowerInvariant()}";
}
}
if (_gemType != GemType.None && _gemCount > 0)
{
var gemName = GetGemName(_gemType, plural);
LabelTo(from, plural
? $"{name} with {_gemCount} {gemName}"
: $"{name} with {gemName}");
}
else
{
LabelTo(from, name);
}
}
private static string GetGemName(GemType type, bool plural = false) => type switch
{
GemType.StarSapphire when plural => "star sapphires",
GemType.StarSapphire => "a star sapphire",
GemType.Emerald when plural => "emeralds",
GemType.Emerald => "an emerald",
GemType.Sapphire when plural => "sapphires",
GemType.Sapphire => "a sapphire",
GemType.Ruby when plural => "rubies",
GemType.Ruby => "a ruby",
GemType.Citrine when plural => "citrines",
GemType.Citrine => "a citrine",
GemType.Amethyst when plural => "amethysts",
GemType.Amethyst => "an amethyst",
GemType.Tourmaline when plural => "tourmalines",
GemType.Tourmaline => "a tourmaline",
GemType.Amber when plural => "ambers",
GemType.Amber => "an amber",
GemType.Diamond when plural => "diamonds",
GemType.Diamond => "a diamond",
_ when plural => "gems",
_ => "a gem"
};
internal static GemType GetGemType(Item item) => item switch
{
StarSapphire => GemType.StarSapphire,
Emerald => GemType.Emerald,
Sapphire => GemType.Sapphire,
Ruby => GemType.Ruby,
Citrine => GemType.Citrine,
Amethyst => GemType.Amethyst,
Tourmaline => GemType.Tourmaline,
Amber => GemType.Amber,
Diamond => GemType.Diamond,
_ => GemType.None
};
internal static Type GetGemItemType(GemType type) => type switch
{
GemType.StarSapphire => typeof(StarSapphire),
GemType.Emerald => typeof(Emerald),
GemType.Sapphire => typeof(Sapphire),
GemType.Ruby => typeof(Ruby),
GemType.Citrine => typeof(Citrine),
GemType.Amethyst => typeof(Amethyst),
GemType.Tourmaline => typeof(Tourmaline),
GemType.Amber => typeof(Amber),
GemType.Diamond => typeof(Diamond),
_ => null
};
public override void OnAfterDuped(Item newItem) public override void OnAfterDuped(Item newItem)
{ {
if (newItem is not BaseJewel jewel) if (newItem is not BaseJewel jewel)
@ -406,6 +531,18 @@ public abstract partial class BaseJewel : Item, ICraftable, IAosItem
_skillBonuses.Deserialize(reader); _skillBonuses.Deserialize(reader);
} }
private void MigrateFrom(V4Content content)
{
_maxHitPoints = content.MaxHitPoints;
_hitPoints = content.HitPoints;
_resource = content.Resource;
_gemType = content.GemType;
_attributes = content.Attributes;
_resistances = content.Resistances;
_skillBonuses = content.SkillBonuses;
// _gemCount defaults to 0
}
[AfterDeserialization] [AfterDeserialization]
private void AfterDeserialization() private void AfterDeserialization()
{ {

View file

@ -33,3 +33,16 @@ public partial class SilverRing : BaseRing
public override double DefaultWeight => 0.1; public override double DefaultWeight => 0.1;
} }
[SerializationGenerator(0, false)]
public partial class WeddingRing : BaseRing
{
public override string DefaultName => "wedding ring";
[Constructible]
public WeddingRing() : base(0x108a)
{
}
public override double DefaultWeight => 0.1;
}

View file

@ -1,7 +1,7 @@
using System; using System;
using ModernUO.Serialization; using ModernUO.Serialization;
using Server.Engines.Craft; using Server.Engines.Craft;
using Server.Gumps; using Server.Engines.Craft.T2A;
using Server.Network; using Server.Network;
namespace Server.Items; namespace Server.Items;
@ -159,9 +159,14 @@ public abstract partial class BaseTool : Item, IUsesRemaining, ICraftable
{ {
from.SendLocalizedMessage(num); from.SendLocalizedMessage(num);
} }
else if (T2ACraftSystem.Enabled)
{
from.Target = new T2ACraftToolTarget(this, system);
from.SendAsciiMessage("Target this tool to make last item, or any other target to begin crafting.");
}
else else
{ {
from.SendGump(new CraftGump(from, system, this, null)); CraftItem.ShowCraftMenu(from, system, this, null);
} }
} }
else else

View file

@ -0,0 +1,64 @@
{
"version": 5,
"type": "Server.Items.BaseJewel",
"properties": [
{
"name": "MaxHitPoints",
"type": "int",
"rule": "PrimitiveTypeMigrationRule",
"ruleArguments": [
"EncodedInt"
]
},
{
"name": "HitPoints",
"type": "int",
"rule": "PrimitiveTypeMigrationRule",
"ruleArguments": [
"EncodedInt"
]
},
{
"name": "Resource",
"type": "Server.Items.CraftResource",
"rule": "EnumMigrationRule"
},
{
"name": "GemType",
"type": "Server.Items.GemType",
"rule": "EnumMigrationRule"
},
{
"name": "Attributes",
"type": "Server.AosAttributes",
"rule": "RawSerializableMigrationRule",
"ruleArguments": [
"DeserializationRequiresParent"
]
},
{
"name": "Resistances",
"type": "Server.AosElementAttributes",
"rule": "RawSerializableMigrationRule",
"ruleArguments": [
"DeserializationRequiresParent"
]
},
{
"name": "SkillBonuses",
"type": "Server.AosSkillBonuses",
"rule": "RawSerializableMigrationRule",
"ruleArguments": [
"DeserializationRequiresParent"
]
},
{
"name": "GemCount",
"type": "int",
"rule": "PrimitiveTypeMigrationRule",
"ruleArguments": [
"EncodedInt"
]
}
]
}

View file

@ -0,0 +1,4 @@
{
"version": 0,
"type": "Server.Items.WeddingRing"
}

View file

@ -1,7 +1,6 @@
using ModernUO.Serialization; using ModernUO.Serialization;
using Server.Collections; using Server.Collections;
using Server.ContextMenus; using Server.ContextMenus;
using Server.Gumps;
using Server.Items; using Server.Items;
namespace Server.Mobiles namespace Server.Mobiles

View file

@ -287,17 +287,21 @@ public static class IncomingPlayerPackets
public static void MenuResponse(NetState state, SpanReader reader) public static void MenuResponse(NetState state, SpanReader reader)
{ {
var serial = reader.ReadUInt32(); var serial = reader.ReadUInt32();
int menuID = reader.ReadInt16(); // unused in our implementation int menuID = reader.ReadInt16();
int index = reader.ReadInt16(); int index = reader.ReadInt16();
int itemID = reader.ReadInt16(); int itemID = reader.ReadInt16();
int hue = reader.ReadInt16(); int hue = reader.ReadInt16();
index -= 1; // convert from 1-based to 0-based index -= 1; // convert from 1-based to 0-based
foreach (var menu in state.Menus) for (var i = 0; i < state.Menus.Count; i++)
{ {
if (menu.Serial == serial) var menu = state.Menus[i];
if ((uint)menu.Serial != serial)
{ {
continue;
}
state.RemoveMenu(menu); state.RemoveMenu(menu);
if (index >= 0 && index < menu.EntryLength) if (index >= 0 && index < menu.EntryLength)
@ -308,11 +312,9 @@ public static class IncomingPlayerPackets
{ {
menu.OnCancel(state); menu.OnCancel(state);
} }
break; break;
} }
} }
}
public static void Disconnect(NetState state, SpanReader reader) public static void Disconnect(NetState state, SpanReader reader)
{ {

View file

@ -0,0 +1,25 @@
using System;
using Server.Engines.Craft;
using Server.Engines.Craft.T2A;
namespace Server.SkillHandlers;
public static class Cartography
{
public static void Initialize()
{
SkillInfo.Table[(int)SkillName.Cartography].Callback = OnUse;
}
public static TimeSpan OnUse(Mobile m)
{
if (!T2ACraftSystem.Enabled)
{
m.SendLocalizedMessage(1046444); // Use a mapmaker's pen to draw maps.
return TimeSpan.Zero;
}
T2ACraftSystem.ShowMenu(m, DefCartography.CraftSystem, null);
return TimeSpan.FromSeconds(1.0);
}
}

View file

@ -1,5 +1,7 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using Server.Engines.Craft;
using Server.Engines.Craft.T2A;
using Server.Items; using Server.Items;
using Server.Targeting; using Server.Targeting;
@ -16,10 +18,19 @@ namespace Server.SkillHandlers
public static TimeSpan OnUse(Mobile m) public static TimeSpan OnUse(Mobile m)
{ {
Target target = new InternalTargetSrc(); if (T2ACraftSystem.Enabled)
{
var target = new T2AInscribeTarget();
m.Target = target; m.Target = target;
m.SendLocalizedMessage(1046295); // Target the book you wish to copy. m.SendAsciiMessage("Target the book you wish to copy or scroll you want to use.");
target.BeginTimeout(m, 60000); // 1 minute target.BeginTimeout(m, 60000); // 1 minute
return TimeSpan.FromSeconds(1.0);
}
Target uotdTarget = new InternalTargetSrc();
m.Target = uotdTarget;
m.SendLocalizedMessage(1046295); // Target the book you wish to copy.
uotdTarget.BeginTimeout(m, 60000); // 1 minute
return TimeSpan.FromSeconds(1.0); return TimeSpan.FromSeconds(1.0);
} }
@ -42,10 +53,12 @@ namespace Server.SkillHandlers
public static bool IsEmpty(BaseBook book) public static bool IsEmpty(BaseBook book)
{ {
foreach (var page in book.Pages) for (var i = 0; i < book.Pages.Length; i++)
{ {
foreach (var line in page.Lines) var page = book.Pages[i];
for (var j = 0; j < page.Lines.Length; j++)
{ {
var line = page.Lines[j];
if (!string.IsNullOrEmpty(line)) if (!string.IsNullOrEmpty(line))
{ {
return false; return false;
@ -78,6 +91,76 @@ namespace Server.SkillHandlers
} }
} }
private class T2AInscribeTarget : Target
{
public T2AInscribeTarget() : base(3, false, TargetFlags.None)
{
}
protected override void OnTarget(Mobile from, object targeted)
{
if (targeted is BlankScroll scroll)
{
if (!scroll.IsChildOf(from.Backpack))
{
from.SendAsciiMessage("That must be in your pack for you to use it.");
}
else
{
T2ACraftSystem.ShowMenu(from, DefInscription.CraftSystem, null);
}
}
else if (targeted is RecallRune)
{
var system = DefInscription.CraftSystem;
var itemDef = system.CraftItems.SearchFor(typeof(Runebook));
if (itemDef != null)
{
var num = system.CanCraft(from, null, itemDef.ItemType);
if (num > 0)
{
from.SendLocalizedMessage(num);
}
else
{
system.CreateItem(from, itemDef.ItemType, null, null, itemDef);
}
}
}
else if (targeted is BaseBook book)
{
if (IsEmpty(book))
{
from.SendLocalizedMessage(501611); // Can't copy an empty book.
}
else if (GetUser(book) != null)
{
from.SendLocalizedMessage(501621); // Someone else is inscribing that item.
}
else
{
Target target = new InternalTargetDst(book);
from.Target = target;
from.SendLocalizedMessage(501612); // Select a book to copy this to.
target.BeginTimeout(from, 60000);
SetUser(book, from);
}
}
else
{
from.SendLocalizedMessage(1046296); // That is not a book
}
}
protected override void OnTargetCancel(Mobile from, TargetCancelType cancelType)
{
if (cancelType == TargetCancelType.Timeout)
{
from.SendLocalizedMessage(501619);
}
}
}
private class InternalTargetSrc : Target private class InternalTargetSrc : Target
{ {
public InternalTargetSrc() : base(3, false, TargetFlags.None) public InternalTargetSrc() : base(3, false, TargetFlags.None)
@ -151,9 +234,7 @@ namespace Server.SkillHandlers
{ {
from.SendLocalizedMessage(501621); // Someone else is inscribing that item. from.SendLocalizedMessage(501621); // Someone else is inscribing that item.
} }
else else if (from.CheckTargetSkill(SkillName.Inscribe, bookDst, 0, 50))
{
if (from.CheckTargetSkill(SkillName.Inscribe, bookDst, 0, 50))
{ {
Copy(m_BookSrc, bookDst); Copy(m_BookSrc, bookDst);
@ -165,7 +246,6 @@ namespace Server.SkillHandlers
from.SendLocalizedMessage(501617); // You fail to make a copy of the book. from.SendLocalizedMessage(501617); // You fail to make a copy of the book.
} }
} }
}
protected override void OnTargetCancel(Mobile from, TargetCancelType cancelType) protected override void OnTargetCancel(Mobile from, TargetCancelType cancelType)
{ {

141
dev-docs/t2a-crafting.md Normal file
View file

@ -0,0 +1,141 @@
# T2A Packet-Based Crafting Menus
This document covers ModernUO's **T2A-era crafting menus** — the pre-UO:Third-Dawn, packet-based item-list crafting UI that replaces the modern gump crafting interface when enabled. It is the developer/AI reference for how the system is wired, how to extend it, and how it deviates from authentic T2A behavior.
## Overview
In the T2A era (≈19982001, before Publish 14 on 2001-11-30), UO crafting did not use gumps. The server sent the generic `0x7C` "Open Dialog" menu packet and the client replied with the 13-byte `0x7D` response. Double-clicking a crafting tool opened a **skill-and-material-filtered item-list menu**; the player picked a category/item and targeted a resource, and the item was made.
ModernUO reproduces this behind a single startup-read toggle. When `T2ACraftSystem.Enabled` is `false`, crafting uses the normal `CraftGump`. When `true`, the same `CraftSystem`/`CraftItem` definitions are presented through packet menus instead. The value is read once at startup from the `t2aCraftMenus` server setting (default `!Core.UOTD`), so a pre-UO:TD shard gets the T2A menus automatically and a UO:TD-or-later shard gets gumps — with no runtime/admin toggle.
The wire-level menu packets (`0x7C`/`0x7D`) already exist in the engine (`Projects/Server/Network/Packets/OutgoingMenuPackets.cs`, `Projects/UOContent/Network/Packets/IncomingPlayerPackets.cs`) and in `Server.Menus.ItemLists.ItemListMenu` / `Server.Menus.Questions.QuestionMenu`. The T2A feature is a *consumer* of that existing infrastructure, not a new protocol.
## Activation
Toggle: `T2ACraftSystem.Enabled` (static). It is set once in `ExpansionConfiguration.Configure()` from `ServerConfiguration.GetSetting("t2aCraftMenus", !Core.UOTD)` — a read-only setting (the default is **not** written back to the config file). It is intentionally **not** a runtime feature flag and cannot be flipped in-game by admins; change it via the `t2aCraftMenus` server setting and restart.
**Intended deployment:** because the default is `!Core.UOTD`, a pre-UO:TD shard gets T2A menus and the matching era mechanics automatically. The toggle controls the *UI system*; the expansion/era (`Core.UOTD`) controls *mechanics* (see [Gating model](#gating-model-toggle-vs-era)). Since the default tracks the era and there is no runtime override, the two cannot drift into an incoherent combination.
## Architecture
All T2A-specific code lives under `Projects/UOContent/Engines/Craft/T2A/`.
| Type | File | Responsibility |
|---|---|---|
| `T2ACraftSystem` (static) | `T2ACraftSystem.cs` | Central router. `ShowMenu(from, craftSystem, tool, preTarget)` dispatches per craft system to the right resource-selection / menu flow. Hosts shared filtering helpers. |
| `T2ACraftToolTarget` (Target) | `T2ACraftToolTarget.cs` | The first target after double-clicking a tool: target the **tool** → make-last; target **anything else** → begin crafting with that item as `preTarget`. |
| `*Menu : ItemListMenu` | `AlchemyMenu.cs`, `BlacksmithMenu.cs`, `BowFletchingMenu.cs`, `CarpentryMenu.cs`, `CartographyMenu.cs`, `InscriptionMenu.cs`, `TailoringMenu.cs`, `TinkeringMenu.cs` | One menu per skill. Builds filtered entries, drives category submenus, and on response either descends a category or crafts. |
**Separation of concerns:**
- Each `*Menu` owns only its category tree and entry formatting.
- `T2ACraftSystem.CanCraftItem` / `FilterEntries` / `AnyCraftableInCategory` own *craft-eligibility* (skill gate + material count, sub-resource aware, with resource-equivalence: `Log↔Board`, `Cloth↔UncutCloth`, `Leather↔Hides`).
- `CraftItem` / `CraftSystem` own *consumption and item creation*.
### Control flow
```
BaseTool.OnDoubleClick
└─ if T2ACraftSystem.Enabled:
from.Target = new T2ACraftToolTarget(tool, system)
"Target this tool to make last item, or any other target to begin crafting."
├─ target == tool → make-last (repeat context.LastMade; jewelry re-prompts the gem)
└─ target == item/null → T2ACraftSystem.ShowMenu(from, system, tool, preTarget)
├─ resource selected/targeted (per skill)
├─ build filtered menu; empty → "You lack the skill and materials…"
└─ ItemListMenu sent (0x7C) → player picks → 0x7D → OnResponse
├─ category → open submenu
└─ leaf → CraftItem.Craft(...)
```
Tool-less skills (Inscription, Cartography) enter `ShowMenu` from their **skill handler** (`Skills/Inscribe.cs`, `Skills/Cartography.cs`) instead of a tool double-click — see [Tool-less skills](#tool-less-skills).
### How a selection maps back to a craftable
`ItemListEntry` carries a `CraftIndex` (a 4th constructor arg added for this feature) — an index into the menu's parallel `Type[]`. When the `0x7D` response arrives, `OnResponse(state, index)` uses the entry's `CraftIndex` to resolve the chosen category or `CraftItem` type. Menus build their entries through `T2ACraftSystem.FilterEntries(from, staticEntries, types, system, selectedResourceType)` so only craftable rows appear.
## Key mechanics
### Resource pre-selection
Tool skills select the working resource **before** the menu opens. `T2ACraftToolTarget` passes whatever the player targeted as `preTarget`; `T2ACraftSystem.ShowMenu` validates it per skill (e.g. ingots for smithing, cloth/leather for tailoring, wood for carpentry/fletching, blank map for cartography, blank scroll/reagent/rune for inscription) and otherwise prompts for a valid resource. The selected sub-resource index is stored via `T2ACraftSystem.SetLastResourceIndex` (`context.LastResourceIndex` / `LastResourceIndex2`) for make-last.
### Make-last (QoL — see deviations)
`T2ACraftToolTarget`: targeting the tool repeats `context.LastMade` with the remembered resource (and hue). Jewelry re-prompts for a gem target (you cannot silently re-consume gems). **Not historically part of T2A packet menus** ("Make Last" was a Publish 14 gump feature) — kept as a quality-of-life convenience.
### Hue-aware tailoring
Targeting hued cloth/leather makes the craft consume **only matching-hue** material for the primary resource. Implemented by the `CraftItem.Craft(..., resHue)` overload and `CheckHuedRes`/`ConsumeHuedRes`/`GetHuedAmount`/`ConsumeHuedAmount`; the hue rides the `InternalTimer` (`m_ResHue`) into the hue-aware `CompleteCraft` overload, which sets `context.LastHue`. Secondary resources (e.g. ingots in mixed items) are consumed normally. This affects **consumption only**.
### How crafted items get their color
A crafted item's color comes from its **`CraftResource`**, not from the consumed item's (dyed) hue: `OnCraft` sets `Resource = CraftResources.GetFromType(resourceType)` and armor/clothing then take `Hue = CraftResources.GetHue(Resource)`. So dyeing raw leather/cloth does **not** tint the crafted piece (plain `Leather` maps to `RegularLeather`, hue 0) — **leather, cloth, and wood never produce colored items in T2A**. The only color-bearing resource in T2A is **colored ingots/ore** (→ colored metal armor and shields). Colored/special leather, hides, and scales that convey color via `CraftResource` are an **AOS+** addition and don't exist in the T2A era.
Era gating differs by item:
- **`BaseArmor`** / **`BaseClothing`**: set `Resource` (and thus color) in **all eras** — colored-ore armor is colored even in T2A (authentic).
- **`BaseWeapon`**: sets `Resource`/color **only when `Core.AOS`** — pre-AOS weapons are uncolored (and unnamed by resource). This is intended; weapons did not retain resource color until AOS/runic.
### Stacked-gem jewelry
Tinkered jewelry consumes ingots + a targeted gem **stack**. The player targets a stack of N gems; `TinkeringMenu.GemSelectTarget` captures `gemItem.Amount` into `context.PendingGemCount` and the gem type into `context.PendingGemType`. `BaseJewel.OnCraft` consumes the **entire** stack (`ConsumeTotal(gemItemType, PendingGemCount)`) and names the piece by count ("a 1000 diamond ring"). The count persists via `_gemCount` (`[SerializableField(7)]`, jewel serialization **v5**) and is shown in `OnSingleClickPreUOTD`. If the gems are unavailable at craft time the piece is left plain and the player is messaged.
### Half-resources on failure (era mechanic)
`CraftItem.ConsumeRes` reduces each resource by half on a failed craft when `!Core.UOTD` (`amounts[i] -= amounts[i] / 2`). Note integer division: amount-1 resources (e.g. each inscription reagent + the single blank scroll) are fully consumed, matching the confirmed scroll-scribing rule; multi-unit resources (e.g. runebook's 8 blank scrolls) lose half.
### Tool-less skills
`DefInscription` and `DefCartography` override `RequiresTool => !T2ACraftSystem.Enabled`, and `CanCraft` wraps tool validation in `if (RequiresTool)`. Inscription is invoked from the skill list (`Inscribe.cs``T2AInscribeTarget`: blank scroll opens the menu, recall rune crafts a runebook, a book enters the copy flow); cartography from `Cartography.cs`. `CraftItem` tool-null guards prevent `UsesRemaining` decrement when there is no tool.
### Maker's mark
Under T2A the system **always prompts** for the maker's mark (no auto/never toggle): `CompleteCraft` gates on `makersMark && (T2ACraftMenus || context.MarkOption == PromptForMark)`, using the shared `QueryMakersMarkGump` (the old `QueryMakersMarkMenu` was removed). Exceptional + mark are tied to GM/near-GM skill, as in the era.
## Gating model (toggle vs era)
| Switch | Meaning | Governs |
|---|---|---|
| `T2ACraftSystem.Enabled` (from `t2aCraftMenus` setting, default `!Core.UOTD`) | "Use packet menus instead of gumps." | Menu routing, `ShowCraftMenu` (message vs gump), tool-less inscription/cartography, jewelry gem-targeting flow, always-prompt maker's mark, `BlankMap`/`BlankScroll` equivalence suppression. |
| `Core.UOTD` (expansion/era) | T2A↔UO:TD era boundary (`false` = T2A or earlier). | Era mechanics: half-on-failure, tinkering metal-color suppression, pre-AOS recipe availability. |
Because the toggle's default **is** `!Core.UOTD` and there is no runtime override, the two move together by construction — a pre-UO:TD shard gets both the menus and the era mechanics, and there is no incoherent "menus on / UO:TD era" combination to guard against. An operator can still force the setting explicitly (e.g. menus on a later era) via `t2aCraftMenus`, but that is a deliberate, restart-time choice.
## Extending: add a craftable to a T2A menu
1. Ensure the item has a `CraftItem` in the relevant `Def*.cs` (`AddCraft(...)`), as for gump crafting — the T2A menus read the same `CraftSystem.CraftItems`.
2. Add the item's `Type` to the appropriate category `Type[]` in the skill's `*Menu.cs` and a matching static `ItemListEntry` (name + `ItemID` + `CraftIndex`). Entries are filtered at build time by `T2ACraftSystem`, so you don't repeat skill/material checks.
3. For a new **category**, add a `Category` enum value, a `GetQuestion` arm, a static entries array, and the navigation case in `OnResponse`. `BlacksmithMenu.cs` is the canonical template.
4. Jewelry: gem-bearing pieces flow through `TinkeringMenu.GemSelectTarget` and `BaseJewel.OnCraft`; ensure `BaseJewel.GetGemType`/`GetGemItemType` cover any new gem.
## Gotchas
- **Resource equivalence is era-gated.** `CraftItem.InitTypesTable()` only treats `BlankMap`/`BlankScroll` as interchangeable when `!T2ACraftMenus` (the gump clilocs reference both). Under T2A they are distinct, so cartography consumes blank *maps*, not scroll s.
- **Transient context fields are not serialized.** `CraftContext.PendingGemType`, `PendingGemCount`, and `LastHue` are plain properties (no `[SerializableField]`) — they exist only during a craft.
- **`BaseJewel` is at serialization v5.** Bumping it again requires `MigrateFrom(V5Content)` per the serialization rules.
- **Menu entry creation uses reflection in one spot.** `T2ACraftSystem.ShowMenuDirect<T>` uses `Activator.CreateInstance` (once per tool double-click). Fine for now; convert to a compiled factory if it ever shows up hot.
## Files
- T2A UI: `Projects/UOContent/Engines/Craft/T2A/*.cs`
- Engine glue: `Projects/UOContent/Engines/Craft/Core/{CraftItem,CraftContext,CraftSystem,Enhance,Repair,Resmelt,CraftGumpItem,QueryMakersMarkGump}.cs`
- Defs: `Projects/UOContent/Engines/Craft/Def{Alchemy,Cartography,Inscription,Tailoring,Tinkering}.cs`
- Skills: `Projects/UOContent/Skills/{Inscribe,Cartography}.cs`
- Items: `Projects/UOContent/Items/Jewels/{BaseJewel,Ring}.cs` (+ `Migrations/Server.Items.BaseJewel.v5.json`)
- Tool entry: `Projects/UOContent/Items/Skill Items/Tools/BaseTool.cs`
- Toggle: `T2ACraftSystem.Enabled` (in `Engines/Craft/T2A/T2ACraftSystem.cs`), set from `Projects/UOContent/Configuration/ExpansionConfiguration.cs` via `ServerConfiguration.GetSetting("t2aCraftMenus", !Core.UOTD)`
- Engine menus (additive): `Projects/Server/Menus/{BaseMenu,ItemListMenu,QuestionMenu}.cs`; response: `Projects/UOContent/Network/Packets/IncomingPlayerPackets.cs`
- Tests: `Projects/UOContent.Tests/Tests/Items/Jewels/T2AJewelGemCraftTests.cs`
## Testing
`BaseJewel.OnCraft`'s gem block is unit-testable directly (it keys off `CraftContext.PendingGem*`, not the flag): see `T2AJewelGemCraftTests.cs`. The packet-menu UX (double-click → window → target → craft) requires a running shard + T2A client and is covered by the manual checklist in the design spec (§12.1).
## Deviations from authentic T2A (summary)
Decided in the design spec §4; faithful to Jack's research except where shard authority overrode:
- **Make-last** — kept as QoL though it post-dates the T2A packet menus.
- **Half-on-failure** for non-scroll crafts — best-known reconstruction, not OSI-confirmed.
- **Hue-aware tailoring** — matching-hue *consumption* only (does **not** color the product); reconstruction, unverified by primary sources.
- **Stacked-gem jewelry** — the full targeted stack is consumed and named by count (shard-authoritative; overrides both the "single gem" reconstruction and Jack's deliberate "consume 1, name by stack").
- **Cooking** — out of scope (no T2A crafting menu existed for it).
## Related docs
| Topic | File |
|---|---|
| Serialization | `dev-docs/serialization.md` |
| Networking & packets | `dev-docs/networking-packets.md` |
| Era & expansion handling | `dev-docs/era-expansion.md` |
| Gumps (the non-T2A path) | `dev-docs/gump-system.md` |