## 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
14 KiB
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 (≈1998–2001, 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). 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
*Menuowns only its category tree and entry formatting. T2ACraftSystem.CanCraftItem/FilterEntries/AnyCraftableInCategoryown craft-eligibility (skill gate + material count, sub-resource aware, with resource-equivalence:Log↔Board,Cloth↔UncutCloth,Leather↔Hides).CraftItem/CraftSystemown 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.
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: setResource(and thus color) in all eras — colored-ore armor is colored even in T2A (authentic).BaseWeapon: setsResource/color only whenCore.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
- Ensure the item has a
CraftItemin the relevantDef*.cs(AddCraft(...)), as for gump crafting — the T2A menus read the sameCraftSystem.CraftItems. - Add the item's
Typeto the appropriate categoryType[]in the skill's*Menu.csand a matching staticItemListEntry(name +ItemID+CraftIndex). Entries are filtered at build time byT2ACraftSystem, so you don't repeat skill/material checks. - For a new category, add a
Categoryenum value, aGetQuestionarm, a static entries array, and the navigation case inOnResponse.BlacksmithMenu.csis the canonical template. - Jewelry: gem-bearing pieces flow through
TinkeringMenu.GemSelectTargetandBaseJewel.OnCraft; ensureBaseJewel.GetGemType/GetGemItemTypecover any new gem.
Gotchas
- Resource equivalence is era-gated.
CraftItem.InitTypesTable()only treatsBlankMap/BlankScrollas 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, andLastHueare plain properties (no[SerializableField]) — they exist only during a craft. BaseJewelis at serialization v5. Bumping it again requiresMigrateFrom(V5Content)per the serialization rules.- Menu entry creation uses reflection in one spot.
T2ACraftSystem.ShowMenuDirect<T>usesActivator.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(inEngines/Craft/T2A/T2ACraftSystem.cs), set fromProjects/UOContent/Configuration/ExpansionConfiguration.csviaServerConfiguration.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 |