Follow-up to #2573. That change made `SmallBOD.EndCombine` require a player-crafted item, but it could only read provenance off `BaseArmor`, `BaseWeapon` and `BaseClothing`, because those are the only three classes that track it — hence the hand-enumerated `armor?.PlayerConstructed ?? clothing?.PlayerConstructed ?? weapon?.PlayerConstructed ?? false`. The gap is structural rather than cosmetic. `PlayerConstructed` is set inside each base's `OnCraft`, so it can only ever reach types implementing `ICraftable`. Most craftables do not — the tinkering catalogue alone is largely plain `Item` subclasses — so any rule keyed on "was this actually crafted" has nothing to key on for those types. ## What changed Provenance moves to `Item` and is stamped centrally in `CraftItem`, immediately after the item is constructed and before the `ICraftable` dispatch, covering both the AOS and T2A craft paths. The three `OnCraft` overrides drop their now-redundant assignment and inherit `Item`'s property, so no call site outside them changes — `Resmelt` and `SalvageBag` still read `armor.PlayerConstructed` and still compile unchanged. `SmallBOD`'s three-way null-coalescing chain collapses to `item.PlayerConstructed`. `OnCraft` is only ever invoked from `CraftItem` (the other three call sites are `base.OnCraft` chaining), so removing those assignments has no other reachable effect. ## Storage cost: none `Item`'s `SaveFlag` word is written as a fixed-width `int`, not an encoded one, so occupying bit `0x08000000` changes no record lengths. Items that are not player-constructed serialize byte for byte as before, and crafted ones differ by a single bit in a field already being written. `Item` itself needs no version bump: a bare `SaveFlag` bit is self-describing, so records written before it existed lack it and read `false`. ## Version bumps The three content classes do need one, since removing a serialized field changes their layout: | Class | Version | Field removed | |---|---|---| | `BaseArmor` | 9 → 10 | 24 (was last, nothing renumbered) | | `BaseClothing` | 7 → 8 | 7 (fields 8–10 shift down) | | `BaseWeapon` | 10 → 11 | 26 (fields 27–30 shift down) | Each gets a `MigrateFrom` for its previous version that assigns the old bool to the inherited property, so existing crafted armour, weapons and clothing keep their provenance across the upgrade. `Item.Deserialize` runs first and reads the absent bit as `false`, then the migration overwrites it — the generated `Deserialize` calls `base.Deserialize` before dispatching, so the ordering holds. `BaseWeapon` had no migrations file and gains one. The renumbering is not stylistic: the generator requires contiguous field ordering and rejects a hole with `SG3005: Expected field 'Crafter' with order 7 but found 8`. New schema JSONs (`BaseArmor.v10`, `BaseClothing.v8`, `BaseWeapon.v11`) are generated by `ModernUOSchemaGenerator` and committed alongside. ## One thing worth a second opinion The new property is a plain auto-property on `Item`, so it does not call `this.MarkDirty()` the way the codegen setters it replaces did. `MarkDirty` is currently a no-op (`// TODO: Add dirty tracking back`) and no property in `Item.cs` calls it, so this matches the file as it stands — but it is worth noting if dirty tracking comes back. ## Verification Full solution builds in Release with 0 errors and 0 warnings; 1516 tests pass (815 `Server.Tests`, 701 `UOContent.Tests`).
1993 lines
62 KiB
C#
1993 lines
62 KiB
C#
using System;
|
|
using System.Collections.Generic;
|
|
using Server.Collections;
|
|
using Server.Commands;
|
|
using Server.Factions;
|
|
using Server.Gumps;
|
|
using Server.Items;
|
|
using Server.Logging;
|
|
using Server.Mobiles;
|
|
using Server.Engines.Craft.T2A;
|
|
|
|
namespace Server.Engines.Craft
|
|
{
|
|
public enum ConsumeType
|
|
{
|
|
All,
|
|
Half,
|
|
None
|
|
}
|
|
|
|
public interface ICraftable
|
|
{
|
|
int OnCraft(
|
|
int quality, bool makersMark, Mobile from, CraftSystem craftSystem, Type typeRes, BaseTool tool,
|
|
CraftItem craftItem, int resHue
|
|
);
|
|
}
|
|
|
|
public class CraftItem
|
|
{
|
|
private static readonly ILogger logger = LogFactory.GetLogger(typeof(CraftItem));
|
|
|
|
private static readonly Dictionary<Type, int> _itemIds = new();
|
|
|
|
private static readonly int[] m_HeatSources =
|
|
{
|
|
0x461, 0x48E, // Sandstone oven/fireplace
|
|
0x92B, 0x96C, // Stone oven/fireplace
|
|
0xDE3, 0xDE9, // Campfire
|
|
0xFAC, 0xFAC, // Firepit
|
|
0x184A, 0x184C, // Heating stand (left)
|
|
0x184E, 0x1850, // Heating stand (right)
|
|
0x398C, 0x399F, // Fire field
|
|
0x2DDB, 0x2DDC, // Elven stove
|
|
0x19AA, 0x19BB, // Veteran Reward Brazier
|
|
0x197A, 0x19A9, // Large Forge
|
|
0x0FB1, 0x0FB1, // Small Forge
|
|
0x2DD8, 0x2DD8 // Elven Forge
|
|
};
|
|
|
|
private static readonly int[] m_Ovens =
|
|
{
|
|
0x461, 0x46F, // Sandstone oven
|
|
0x92B, 0x93F, // Stone oven
|
|
0x2DDB, 0x2DDC // Elven stove
|
|
};
|
|
|
|
private static readonly int[] m_Mills =
|
|
{
|
|
0x1920, 0x1921, 0x1922, 0x1923, 0x1924, 0x1295, 0x1926, 0x1928,
|
|
0x192C, 0x192D, 0x192E, 0x129F, 0x1930, 0x1931, 0x1932, 0x1934
|
|
};
|
|
|
|
private static readonly Type[][] m_TypesTable = InitTypesTable();
|
|
|
|
private static Type[][] InitTypesTable()
|
|
{
|
|
List<Type[]> types =
|
|
[
|
|
[typeof(Log), typeof(Board)],
|
|
[typeof(HeartwoodLog), typeof(HeartwoodBoard)],
|
|
[typeof(BloodwoodLog), typeof(BloodwoodBoard)],
|
|
[typeof(FrostwoodLog), typeof(FrostwoodBoard)],
|
|
[typeof(OakLog), typeof(OakBoard)],
|
|
[typeof(AshLog), typeof(AshBoard)],
|
|
[typeof(YewLog), typeof(YewBoard)],
|
|
[typeof(Leather), typeof(Hides)],
|
|
[typeof(SpinedLeather), typeof(SpinedHides)],
|
|
[typeof(HornedLeather), typeof(HornedHides)],
|
|
[typeof(BarbedLeather), typeof(BarbedHides)],
|
|
[typeof(Cloth), typeof(UncutCloth)],
|
|
[typeof(CheeseWheel), typeof(CheeseWedge)],
|
|
[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 =
|
|
{
|
|
typeof(BaseWeapon), typeof(BaseArmor), typeof(BaseClothing),
|
|
typeof(BaseJewel), typeof(DragonBardingDeed)
|
|
};
|
|
|
|
private static readonly Type[] m_ColoredResourceTable =
|
|
{
|
|
typeof(BaseIngot), typeof(BaseOre),
|
|
typeof(BaseLeather), typeof(BaseHides),
|
|
typeof(UncutCloth), typeof(Cloth),
|
|
typeof(BaseGranite), typeof(BaseScales)
|
|
};
|
|
|
|
private static readonly Type[] m_MarkableTable =
|
|
{
|
|
typeof(BaseArmor),
|
|
typeof(BaseWeapon),
|
|
typeof(BaseClothing),
|
|
typeof(BaseInstrument),
|
|
typeof(DragonBardingDeed),
|
|
typeof(BaseTool),
|
|
typeof(BaseHarvestTool),
|
|
typeof(FukiyaDarts), typeof(Shuriken),
|
|
typeof(Spellbook), typeof(Runebook),
|
|
typeof(BaseQuiver)
|
|
};
|
|
|
|
private static readonly Type[] m_NeverColorTable =
|
|
{
|
|
typeof(OrcHelm)
|
|
};
|
|
|
|
private int m_ResAmount;
|
|
|
|
private int m_ResHue;
|
|
private CraftSystem m_System;
|
|
private int _itemId;
|
|
|
|
public CraftItem(Type type, TextDefinition groupName, TextDefinition name) : this(type, groupName, name, -1)
|
|
{
|
|
}
|
|
|
|
public CraftItem(Type type, TextDefinition groupName, TextDefinition name, int itemId)
|
|
{
|
|
Resources = new List<CraftRes>();
|
|
Skills = new List<CraftSkill>();
|
|
|
|
ItemType = type;
|
|
|
|
GroupNameString = groupName;
|
|
NameString = name;
|
|
|
|
GroupNameNumber = groupName;
|
|
NameNumber = name;
|
|
|
|
RequiredBeverage = BeverageType.Water;
|
|
_itemId = itemId;
|
|
}
|
|
|
|
public bool ForceNonExceptional { get; set; }
|
|
|
|
public Expansion RequiredExpansion { get; set; }
|
|
|
|
public Recipe Recipe { get; private set; }
|
|
|
|
public BeverageType RequiredBeverage { get; set; }
|
|
|
|
public int Mana { get; set; }
|
|
|
|
public TextDefinition NeedManaLabel { get; set; }
|
|
|
|
public int Hits { get; set; }
|
|
|
|
public TextDefinition NeedHitsLabel { get; set; }
|
|
|
|
public int Stam { get; set; }
|
|
|
|
public TextDefinition NeedStamLabel { get; set; }
|
|
|
|
public bool UseSubRes2 { get; set; }
|
|
|
|
public bool UseAllRes { get; set; }
|
|
|
|
public bool NeedHeat { get; set; }
|
|
|
|
public bool NeedOven { get; set; }
|
|
|
|
public bool NeedMill { get; set; }
|
|
|
|
public Type ItemType { get; }
|
|
|
|
public int ItemHue { get; set; }
|
|
|
|
public string GroupNameString { get; }
|
|
|
|
public int GroupNameNumber { get; }
|
|
|
|
public string NameString { get; }
|
|
|
|
public int NameNumber { get; }
|
|
|
|
public int ItemId => _itemId == -1 ? _itemId = ItemIDOf(ItemType) : _itemId;
|
|
|
|
public List<CraftRes> Resources { get; }
|
|
|
|
public List<CraftSkill> Skills { get; }
|
|
|
|
public void AddRecipe(int id, CraftSystem system)
|
|
{
|
|
if (Recipe != null)
|
|
{
|
|
logger.Warning(
|
|
"Attempted add of recipe #{Id} to the crafting of {ItemTypeName} in CraftSystem {CraftSystem}.",
|
|
id,
|
|
ItemType.Name,
|
|
system
|
|
);
|
|
return;
|
|
}
|
|
|
|
Recipe = new Recipe(id, system, this);
|
|
}
|
|
|
|
public static int LabelNumber(Type type)
|
|
{
|
|
var number = ItemIDOf(type);
|
|
|
|
return number + (number >= 0x4000 ? 1078872 : 1020000);
|
|
}
|
|
|
|
public static int ItemIDOf(Type type)
|
|
{
|
|
if (_itemIds.TryGetValue(type, out var itemId))
|
|
{
|
|
return itemId;
|
|
}
|
|
|
|
if (type == typeof(FactionExplosionTrap))
|
|
{
|
|
itemId = 14034;
|
|
}
|
|
else if (type == typeof(FactionGasTrap))
|
|
{
|
|
itemId = 4523;
|
|
}
|
|
else if (type == typeof(FactionSawTrap))
|
|
{
|
|
itemId = 4359;
|
|
}
|
|
else if (type == typeof(FactionSpikeTrap))
|
|
{
|
|
itemId = 4517;
|
|
}
|
|
|
|
if (itemId == 0)
|
|
{
|
|
var attrs = type.GetCustomAttributes(typeof(CraftItemIDAttribute), false);
|
|
|
|
if (attrs.Length > 0)
|
|
{
|
|
var craftItemID = (CraftItemIDAttribute)attrs[0];
|
|
itemId = craftItemID.ItemID;
|
|
}
|
|
}
|
|
|
|
if (itemId == 0)
|
|
{
|
|
Item item = null;
|
|
|
|
try
|
|
{
|
|
item = type.CreateInstance<Item>();
|
|
}
|
|
catch
|
|
{
|
|
// ignored
|
|
}
|
|
|
|
if (item != null)
|
|
{
|
|
itemId = item.ItemID;
|
|
item.Delete();
|
|
}
|
|
}
|
|
|
|
_itemIds[type] = itemId;
|
|
|
|
return itemId;
|
|
}
|
|
|
|
public void AddRes(Type type, TextDefinition name, int amount, TextDefinition message)
|
|
{
|
|
var craftRes = new CraftRes(type, name, amount, message);
|
|
Resources.Add(craftRes);
|
|
}
|
|
|
|
public void AddSkill(SkillName skillToMake, double minSkill, double maxSkill)
|
|
{
|
|
var craftSkill = new CraftSkill(skillToMake, minSkill, maxSkill);
|
|
Skills.Add(craftSkill);
|
|
}
|
|
|
|
public bool ConsumeAttributes(Mobile from, ref TextDefinition message, bool consume)
|
|
{
|
|
if (Hits > 0 && from.Hits < Hits)
|
|
{
|
|
message = NeedHitsLabel;
|
|
return false;
|
|
}
|
|
|
|
if (Mana > 0 && from.Mana < Mana)
|
|
{
|
|
message = NeedManaLabel;
|
|
return false;
|
|
}
|
|
|
|
if (Stam > 0 && from.Stam < Stam)
|
|
{
|
|
message = NeedStamLabel;
|
|
return false;
|
|
}
|
|
|
|
if (consume)
|
|
{
|
|
from.Mana -= Mana;
|
|
from.Hits -= Hits;
|
|
from.Stam -= Stam;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
public bool IsMarkable(Type type)
|
|
{
|
|
if (ForceNonExceptional) // Don't even display the stuff for marking if it can't ever be exceptional.
|
|
{
|
|
return false;
|
|
}
|
|
|
|
for (var i = 0; i < m_MarkableTable.Length; ++i)
|
|
{
|
|
if (type == m_MarkableTable[i] || type.IsSubclassOf(m_MarkableTable[i]))
|
|
{
|
|
return true;
|
|
}
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
public static bool RetainsColor(Type type)
|
|
{
|
|
var neverColor = false;
|
|
|
|
for (var i = 0; !neverColor && i < m_NeverColorTable.Length; ++i)
|
|
{
|
|
neverColor = type == m_NeverColorTable[i] || type.IsSubclassOf(m_NeverColorTable[i]);
|
|
}
|
|
|
|
if (neverColor)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
var inItemTable = false;
|
|
|
|
for (var i = 0; !inItemTable && i < m_ColoredItemTable.Length; ++i)
|
|
{
|
|
inItemTable = type == m_ColoredItemTable[i] || type.IsSubclassOf(m_ColoredItemTable[i]);
|
|
}
|
|
|
|
return inItemTable;
|
|
}
|
|
|
|
public bool RetainsColorFrom(CraftSystem system, Type type)
|
|
{
|
|
if (system.RetainsColorFrom(this, type))
|
|
{
|
|
return true;
|
|
}
|
|
|
|
var inItemTable = RetainsColor(ItemType);
|
|
|
|
if (!inItemTable)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
var inResourceTable = false;
|
|
|
|
for (var i = 0; !inResourceTable && i < m_ColoredResourceTable.Length; ++i)
|
|
{
|
|
inResourceTable = type == m_ColoredResourceTable[i] || type.IsSubclassOf(m_ColoredResourceTable[i]);
|
|
}
|
|
|
|
return inResourceTable;
|
|
}
|
|
|
|
public static bool Find(Mobile from, int[] itemIDs)
|
|
{
|
|
var map = from.Map;
|
|
|
|
if (map == null)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
foreach (var item in map.GetItemsInRange(from.Location, 2))
|
|
{
|
|
if (item.Z + 16 > item.Z && item.Z + 16 > item.Z && Find(item.ItemID, itemIDs))
|
|
{
|
|
return true;
|
|
}
|
|
}
|
|
|
|
for (var x = -2; x <= 2; ++x)
|
|
{
|
|
for (var y = -2; y <= 2; ++y)
|
|
{
|
|
var vx = from.X + x;
|
|
var vy = from.Y + y;
|
|
|
|
foreach (var tile in map.Tiles.GetStaticAndMultiTiles(vx, vy))
|
|
{
|
|
var z = tile.Z;
|
|
var id = tile.ID;
|
|
|
|
if (z + 16 > from.Z && from.Z + 16 > z && Find(id, itemIDs))
|
|
{
|
|
return true;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
public static bool Find(int itemID, int[] itemIDs)
|
|
{
|
|
var contains = false;
|
|
|
|
for (var i = 0; !contains && i < itemIDs.Length; i += 2)
|
|
{
|
|
contains = itemID >= itemIDs[i] && itemID <= itemIDs[i + 1];
|
|
}
|
|
|
|
return contains;
|
|
}
|
|
|
|
public static bool IsQuantityType(Type[][] types)
|
|
{
|
|
for (var i = 0; i < types.Length; ++i)
|
|
{
|
|
var check = types[i];
|
|
|
|
for (var j = 0; j < check.Length; ++j)
|
|
{
|
|
if (typeof(IHasQuantity).IsAssignableFrom(check[j]))
|
|
{
|
|
return true;
|
|
}
|
|
}
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
public int ConsumeQuantity(Container cont, Type[][] types, int[] amounts)
|
|
{
|
|
if (types.Length != amounts.Length)
|
|
{
|
|
throw new ArgumentOutOfRangeException(nameof(types));
|
|
}
|
|
|
|
var items = new List<Item>[types.Length];
|
|
var totals = new int[types.Length];
|
|
|
|
// First pass, make sure we have enough
|
|
for (var i = 0; i < types.Length; ++i)
|
|
{
|
|
var itemList = items[i] = new List<Item>();
|
|
var typeList = types[i];
|
|
|
|
// Since we are making our own list, we don't need to use EnumerateItems
|
|
foreach (var item in cont.FindItems())
|
|
{
|
|
if (!item.InTypeList(typeList))
|
|
{
|
|
continue;
|
|
}
|
|
|
|
if (item is not IHasQuantity hq)
|
|
{
|
|
totals[i] += item.Amount;
|
|
itemList.Add(item);
|
|
}
|
|
else if (hq is not BaseBeverage beverage || beverage.Content == RequiredBeverage)
|
|
{
|
|
totals[i] += hq.Quantity;
|
|
itemList.Add(item);
|
|
}
|
|
}
|
|
|
|
if (totals[i] < amounts[i])
|
|
{
|
|
return i;
|
|
}
|
|
}
|
|
|
|
// Second pass, consume
|
|
for (var i = 0; i < types.Length; ++i)
|
|
{
|
|
var need = amounts[i];
|
|
|
|
for (var j = 0; j < items[i].Count; ++j)
|
|
{
|
|
var item = items[i][j];
|
|
|
|
if (item is not IHasQuantity hq)
|
|
{
|
|
var theirAmount = item.Amount;
|
|
|
|
if (theirAmount < need)
|
|
{
|
|
item.Consume(theirAmount);
|
|
need -= theirAmount;
|
|
}
|
|
else
|
|
{
|
|
item.Consume(need);
|
|
break;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
var theirAmount = hq.Quantity;
|
|
|
|
if (theirAmount < need)
|
|
{
|
|
hq.Quantity -= theirAmount;
|
|
need -= theirAmount;
|
|
}
|
|
else
|
|
{
|
|
hq.Quantity -= need;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
return -1;
|
|
}
|
|
|
|
public int GetQuantity(Container cont, Type[] types)
|
|
{
|
|
var amount = 0;
|
|
foreach (var item in cont.FindItems())
|
|
{
|
|
if (!item.InTypeList(types))
|
|
{
|
|
continue;
|
|
}
|
|
|
|
if (item is not IHasQuantity hq)
|
|
{
|
|
amount += item.Amount;
|
|
}
|
|
else if (hq is not BaseBeverage bev || bev.Content == RequiredBeverage)
|
|
{
|
|
amount += hq.Quantity;
|
|
}
|
|
}
|
|
|
|
return amount;
|
|
}
|
|
|
|
public bool ConsumeRes(
|
|
Mobile from, Type typeRes, CraftSystem craftSystem, ref int resHue, ref int maxAmount,
|
|
ConsumeType consumeType, ref TextDefinition message
|
|
) => ConsumeRes(from, typeRes, craftSystem, ref resHue, ref maxAmount, consumeType, ref message, false);
|
|
|
|
public bool ConsumeRes(
|
|
Mobile from, Type typeRes, CraftSystem craftSystem, ref int resHue, ref int maxAmount,
|
|
ConsumeType consumeType, ref TextDefinition message, bool isFailure
|
|
)
|
|
{
|
|
var ourPack = from.Backpack;
|
|
|
|
if (ourPack == null)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
if (NeedHeat && !Find(from, m_HeatSources))
|
|
{
|
|
message = 1044487; // You must be near a fire source to cook.
|
|
return false;
|
|
}
|
|
|
|
if (NeedOven && !Find(from, m_Ovens))
|
|
{
|
|
message = 1044493; // You must be near an oven to bake that.
|
|
return false;
|
|
}
|
|
|
|
if (NeedMill && !Find(from, m_Mills))
|
|
{
|
|
message = 1044491; // You must be near a flour mill to do that.
|
|
return false;
|
|
}
|
|
|
|
var types = new Type[Resources.Count][];
|
|
var amounts = new int[Resources.Count];
|
|
|
|
maxAmount = int.MaxValue;
|
|
|
|
var resCol = UseSubRes2 ? craftSystem.CraftSubRes2 : craftSystem.CraftSubRes;
|
|
|
|
CraftRes res;
|
|
for (var i = 0; i < types.Length; ++i)
|
|
{
|
|
var craftRes = Resources[i];
|
|
var baseType = craftRes.ItemType;
|
|
|
|
// 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)
|
|
{
|
|
message = subResource.Message;
|
|
return false;
|
|
}
|
|
}
|
|
// ******************
|
|
|
|
for (var j = 0; types[i] == null && j < m_TypesTable.Length; ++j)
|
|
{
|
|
if (m_TypesTable[j][0] == baseType)
|
|
{
|
|
types[i] = m_TypesTable[j];
|
|
}
|
|
}
|
|
|
|
types[i] ??= [baseType];
|
|
amounts[i] = craftRes.Amount;
|
|
|
|
// For stackable items that can be crafted more than one at a time
|
|
if (UseAllRes)
|
|
{
|
|
var tempAmount = ourPack.GetAmount(types[i]);
|
|
tempAmount /= amounts[i];
|
|
if (tempAmount < maxAmount)
|
|
{
|
|
maxAmount = tempAmount;
|
|
|
|
if (maxAmount == 0)
|
|
{
|
|
res = Resources[i];
|
|
|
|
if (res.Message.Number > 0)
|
|
{
|
|
message = res.Message.Number;
|
|
}
|
|
else if (!string.IsNullOrEmpty(res.Message.String))
|
|
{
|
|
message = res.Message.String;
|
|
}
|
|
else
|
|
{
|
|
message = 502925; // You don't have the resources required to make that item.
|
|
}
|
|
|
|
return false;
|
|
}
|
|
}
|
|
}
|
|
// ****************************
|
|
|
|
if (isFailure && !craftSystem.ConsumeOnFailure(from, types[i][0], this))
|
|
{
|
|
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
|
|
if (UseAllRes)
|
|
{
|
|
for (var i = 0; i < amounts.Length; ++i)
|
|
{
|
|
amounts[i] *= maxAmount;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
maxAmount = -1;
|
|
}
|
|
|
|
RecallRune consumeExtra = null;
|
|
|
|
if (NameNumber == 1041267)
|
|
{
|
|
// Runebooks are a special case, they need a blank recall rune
|
|
foreach (var rune in ourPack.FindItemsByType<RecallRune>())
|
|
{
|
|
if (!rune.Marked)
|
|
{
|
|
consumeExtra = rune;
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (consumeExtra == null)
|
|
{
|
|
message = 1044253; // You don't have the components needed to make that.
|
|
return false;
|
|
}
|
|
}
|
|
|
|
int index;
|
|
|
|
if (consumeType == ConsumeType.None)
|
|
{
|
|
index = -1;
|
|
|
|
var isQuantityType = IsQuantityType(types);
|
|
|
|
// TODO: Optimize this
|
|
for (var i = 0; i < types.Length; i++)
|
|
{
|
|
var quantity = isQuantityType
|
|
? GetQuantity(ourPack, types[i])
|
|
: ourPack.GetBestGroupAmount(types[i], true, CheckHueGrouping);
|
|
|
|
if (quantity < amounts[i])
|
|
{
|
|
index = i;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
else
|
|
{
|
|
if (consumeType == ConsumeType.Half)
|
|
{
|
|
for (var i = 0; i < amounts.Length; i++)
|
|
{
|
|
amounts[i] = Math.Max(1, amounts[i] / 2);
|
|
}
|
|
}
|
|
|
|
m_ResHue = 0;
|
|
m_ResAmount = 0;
|
|
m_System = craftSystem;
|
|
|
|
index = IsQuantityType(types)
|
|
? ConsumeQuantity(ourPack, types, amounts)
|
|
: ourPack.ConsumeTotalGrouped(types, amounts, true, OnResourceConsumed, CheckHueGrouping);
|
|
|
|
resHue = m_ResHue;
|
|
}
|
|
|
|
if (index == -1)
|
|
{
|
|
if (consumeType != ConsumeType.None)
|
|
{
|
|
consumeExtra?.Delete();
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
res = Resources[index];
|
|
|
|
if (res.Message.Number > 0)
|
|
{
|
|
message = res.Message.Number;
|
|
}
|
|
else if (!string.IsNullOrEmpty(res.Message.String))
|
|
{
|
|
message = res.Message.String;
|
|
}
|
|
else
|
|
{
|
|
message = 502925; // You don't have the resources required to make that item.
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
private void OnResourceConsumed(Item item, int amount)
|
|
{
|
|
if (!RetainsColorFrom(m_System, item.GetType()))
|
|
{
|
|
return;
|
|
}
|
|
|
|
if (amount >= m_ResAmount)
|
|
{
|
|
m_ResHue = item.Hue;
|
|
m_ResAmount = amount;
|
|
}
|
|
}
|
|
|
|
private static int CheckHueGrouping(Item a, Item b) => b.Hue.CompareTo(a.Hue);
|
|
|
|
public double GetExceptionalChance(CraftSystem system, double chance, Mobile from)
|
|
{
|
|
if (ForceNonExceptional)
|
|
{
|
|
return 0.0;
|
|
}
|
|
|
|
var bonus = 0.0;
|
|
|
|
if (from.Talisman is BaseTalisman talisman && talisman.Skill == system.MainSkill)
|
|
{
|
|
chance -= talisman.SuccessBonus / 100.0;
|
|
bonus = talisman.ExceptionalBonus / 100.0;
|
|
}
|
|
|
|
chance = system.ECA switch
|
|
{
|
|
CraftECA.FiftyPercentChanceMinusTenPercent => chance * 0.5 - 0.1,
|
|
CraftECA.ChanceMinusSixtyToFortyFive => chance - Math.Clamp(
|
|
0.60 - (from.Skills[system.MainSkill].Value - 95.0) * 0.03,
|
|
0.45,
|
|
0.60
|
|
),
|
|
_ => chance - 0.6
|
|
};
|
|
|
|
return chance > 0 ? chance + bonus : chance;
|
|
}
|
|
|
|
public bool CheckSkills(
|
|
Mobile from, Type typeRes, CraftSystem craftSystem, ref int quality, out bool allRequiredSkills
|
|
) => CheckSkills(from, typeRes, craftSystem, ref quality, out allRequiredSkills, true);
|
|
|
|
public bool CheckSkills(
|
|
Mobile from, Type typeRes, CraftSystem craftSystem, ref int quality,
|
|
out bool allRequiredSkills, bool gainSkills
|
|
)
|
|
{
|
|
var chance = GetSuccessChance(from, typeRes, craftSystem, gainSkills, out allRequiredSkills);
|
|
|
|
if (GetExceptionalChance(craftSystem, chance, from) > Utility.RandomDouble())
|
|
{
|
|
quality = 2;
|
|
}
|
|
|
|
return chance > Utility.RandomDouble();
|
|
}
|
|
|
|
public double GetSuccessChance(
|
|
Mobile from, Type typeRes, CraftSystem craftSystem, bool gainSkills,
|
|
out bool allRequiredSkills
|
|
)
|
|
{
|
|
var minMainSkill = 0.0;
|
|
var maxMainSkill = 0.0;
|
|
var valMainSkill = 0.0;
|
|
|
|
allRequiredSkills = true;
|
|
|
|
for (var i = 0; i < Skills.Count; i++)
|
|
{
|
|
var craftSkill = Skills[i];
|
|
|
|
var minSkill = craftSkill.MinSkill;
|
|
var maxSkill = craftSkill.MaxSkill;
|
|
var valSkill = from.Skills[craftSkill.SkillToMake].Value;
|
|
|
|
if (valSkill < minSkill)
|
|
{
|
|
allRequiredSkills = false;
|
|
}
|
|
|
|
if (craftSkill.SkillToMake == craftSystem.MainSkill)
|
|
{
|
|
minMainSkill = minSkill;
|
|
maxMainSkill = maxSkill;
|
|
valMainSkill = valSkill;
|
|
}
|
|
|
|
if (gainSkills) // This is a passive check. Success chance is entirely dependant on the main skill
|
|
{
|
|
from.CheckSkill(craftSkill.SkillToMake, minSkill, maxSkill);
|
|
}
|
|
}
|
|
|
|
if (!allRequiredSkills)
|
|
{
|
|
return 0;
|
|
}
|
|
|
|
var minChance = craftSystem.GetChanceAtMin(this);
|
|
|
|
var chance = minChance + (valMainSkill - minMainSkill) / (maxMainSkill - minMainSkill) * (1.0 - minChance);
|
|
|
|
if (from.Talisman is BaseTalisman talisman && talisman.Skill == craftSystem.MainSkill)
|
|
{
|
|
chance += talisman.SuccessBonus / 100.0;
|
|
}
|
|
|
|
return chance;
|
|
}
|
|
|
|
public void Craft(Mobile from, CraftSystem craftSystem, Type typeRes, BaseTool tool)
|
|
{
|
|
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,
|
|
// The {0} expansion is required to attempt this item.
|
|
RequiredExpansionMessage(RequiredExpansion)
|
|
)
|
|
);
|
|
return;
|
|
}
|
|
|
|
var chance = GetSuccessChance(from, typeRes, craftSystem, false, out var allRequiredSkills);
|
|
|
|
if (!allRequiredSkills || chance <= 0.0)
|
|
{
|
|
from.EndAction<CraftSystem>();
|
|
if (T2ACraftSystem.Enabled)
|
|
{
|
|
from.SendAsciiMessage("You lack the required skill to craft this item.");
|
|
}
|
|
else
|
|
{
|
|
from.SendGump(
|
|
new CraftGump(
|
|
from,
|
|
craftSystem,
|
|
tool,
|
|
1044153 // You don't have the required skills to attempt 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;
|
|
}
|
|
|
|
var resHue = 0;
|
|
var maxAmount = 0;
|
|
TextDefinition message = null;
|
|
|
|
if (!ConsumeRes(from, typeRes, craftSystem, ref resHue, ref maxAmount, ConsumeType.None, ref message))
|
|
{
|
|
from.EndAction<CraftSystem>();
|
|
ShowCraftMenu(from, craftSystem, tool, message);
|
|
return;
|
|
}
|
|
|
|
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).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)
|
|
{
|
|
return expansion switch
|
|
{
|
|
Expansion.SE => 1063307, // The "Samurai Empire" expansion is required to attempt this item.
|
|
Expansion.ML => 1072650, // The "Mondain's Legacy" expansion is required to attempt this item.
|
|
_ => $"The \"{ExpansionInfo.GetInfo(expansion).Name}\" expansion is required to attempt this item."
|
|
};
|
|
}
|
|
|
|
public void CompleteCraft(
|
|
int quality, bool makersMark, Mobile from, CraftSystem craftSystem, Type typeRes,
|
|
BaseTool tool, CustomCraft customCraft
|
|
)
|
|
{
|
|
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;
|
|
}
|
|
|
|
var checkResHue = 0;
|
|
var checkMaxAmount = 0;
|
|
TextDefinition checkMessage = null;
|
|
|
|
var consumeRes = ConsumeRes(
|
|
from,
|
|
typeRes,
|
|
craftSystem,
|
|
ref checkResHue,
|
|
ref checkMaxAmount,
|
|
ConsumeType.None,
|
|
ref checkMessage
|
|
);
|
|
|
|
// Not enough resource to craft it
|
|
if (!(consumeRes && ConsumeAttributes(from, ref checkMessage, false)))
|
|
{
|
|
if (tool?.Deleted == false && tool.UsesRemaining > 0)
|
|
{
|
|
ShowCraftMenu(from, craftSystem, tool, checkMessage);
|
|
}
|
|
else if (checkMessage.Number > 0)
|
|
{
|
|
from.SendLocalizedMessage(checkMessage.Number);
|
|
}
|
|
else
|
|
{
|
|
from.SendMessage(checkMessage.String);
|
|
}
|
|
|
|
return;
|
|
}
|
|
|
|
var toolBroken = false;
|
|
|
|
var ignored = 1;
|
|
var endquality = 1;
|
|
var resHue = 0;
|
|
var maxAmount = 0;
|
|
TextDefinition message = null;
|
|
var num = 0;
|
|
|
|
if (CheckSkills(from, typeRes, craftSystem, ref ignored, out var allRequiredSkills))
|
|
{
|
|
consumeRes = ConsumeRes(
|
|
from,
|
|
typeRes,
|
|
craftSystem,
|
|
ref resHue,
|
|
ref maxAmount,
|
|
ConsumeType.All,
|
|
ref message
|
|
);
|
|
|
|
// Not enough resource to craft it
|
|
if (!(consumeRes && ConsumeAttributes(from, ref message, true)))
|
|
{
|
|
if (tool?.Deleted == false && tool.UsesRemaining > 0)
|
|
{
|
|
ShowCraftMenu(from, craftSystem, tool, message);
|
|
}
|
|
else if (message != null)
|
|
{
|
|
if (message.Number > 0)
|
|
{
|
|
from.SendLocalizedMessage(message.Number);
|
|
}
|
|
else
|
|
{
|
|
from.SendMessage(message.String);
|
|
}
|
|
}
|
|
|
|
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 if (typeof(MapItem).IsAssignableFrom(ItemType) && from.Map != Map.Trammel && from.Map != Map.Felucca)
|
|
{
|
|
item = new IndecipherableMap();
|
|
from.SendLocalizedMessage(1070800); // The map you create becomes mysteriously indecipherable.
|
|
}
|
|
else
|
|
{
|
|
item = ItemType.CreateInstance<Item>();
|
|
}
|
|
|
|
if (item != null)
|
|
{
|
|
// Stamped here, not in OnCraft: most craftables do not implement ICraftable.
|
|
item.PlayerConstructed = true;
|
|
|
|
if (item is ICraftable craftable)
|
|
{
|
|
endquality = craftable.OnCraft(quality, makersMark, from, craftSystem, typeRes, tool, this, resHue);
|
|
}
|
|
else if (item.Hue == 0)
|
|
{
|
|
item.Hue = resHue;
|
|
}
|
|
|
|
if (maxAmount > 0)
|
|
{
|
|
if (!item.Stackable && item is IUsesRemaining remaining)
|
|
{
|
|
remaining.UsesRemaining *= maxAmount;
|
|
}
|
|
else
|
|
{
|
|
item.Amount = maxAmount;
|
|
}
|
|
}
|
|
|
|
from.AddToBackpack(item);
|
|
|
|
if (from.AccessLevel > AccessLevel.Player)
|
|
{
|
|
CommandLogging.WriteLine(
|
|
from,
|
|
$"Crafting {CommandLogging.Format(item)} with craft system {craftSystem.GetType().Name}"
|
|
);
|
|
}
|
|
|
|
// from.PlaySound( 0x57 );
|
|
}
|
|
|
|
if (num == 0)
|
|
{
|
|
num = craftSystem.PlayEndingEffect(from, false, true, toolBroken, endquality, makersMark, this);
|
|
}
|
|
|
|
var queryFactionImbue = false;
|
|
var availableSilver = 0;
|
|
FactionItemDefinition def = null;
|
|
Faction faction = null;
|
|
|
|
if (item is IFactionItem)
|
|
{
|
|
def = FactionItemDefinition.Identify(item);
|
|
|
|
if (def != null)
|
|
{
|
|
faction = Faction.Find(from);
|
|
|
|
if (faction != null)
|
|
{
|
|
var town = Town.FromRegion(from.Region);
|
|
|
|
if (town?.Owner == faction)
|
|
{
|
|
var pack = from.Backpack;
|
|
|
|
if (pack != null)
|
|
{
|
|
availableSilver = pack.GetAmount(typeof(Silver));
|
|
|
|
if (availableSilver >= def.SilverCost)
|
|
{
|
|
queryFactionImbue = Faction.IsNearType(from, def.VendorType, 12);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// TODO: Scroll imbuing
|
|
|
|
if (queryFactionImbue)
|
|
{
|
|
from.SendGump(
|
|
new FactionImbueGump(
|
|
quality,
|
|
item,
|
|
from,
|
|
craftSystem,
|
|
tool,
|
|
num,
|
|
availableSilver,
|
|
faction,
|
|
def
|
|
)
|
|
);
|
|
}
|
|
else if (tool?.Deleted == false && tool.UsesRemaining > 0)
|
|
{
|
|
if (T2ACraftSystem.Enabled)
|
|
{
|
|
if (num > 0)
|
|
{
|
|
from.SendLocalizedMessage(num);
|
|
}
|
|
ShowCraftMenu(from, craftSystem, tool);
|
|
}
|
|
else
|
|
{
|
|
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); // You don't have the required skills to attempt this item.
|
|
}
|
|
return;
|
|
}
|
|
|
|
var consumeType = UseAllRes ? ConsumeType.Half : ConsumeType.All;
|
|
|
|
// Not enough resource to craft it
|
|
if (!ConsumeRes(from, typeRes, craftSystem, ref resHue, ref maxAmount, consumeType, ref message, true))
|
|
{
|
|
if (tool?.Deleted == false && tool.UsesRemaining > 0)
|
|
{
|
|
ShowCraftMenu(from, craftSystem, tool, message);
|
|
}
|
|
else if (message != null)
|
|
{
|
|
if (message.Number > 0)
|
|
{
|
|
from.SendLocalizedMessage(message.Number);
|
|
}
|
|
else
|
|
{
|
|
from.SendMessage(message.String);
|
|
}
|
|
}
|
|
|
|
return;
|
|
}
|
|
|
|
if (tool != null)
|
|
{
|
|
tool.UsesRemaining--;
|
|
|
|
if (tool.UsesRemaining < 1 && tool.BreakOnDepletion)
|
|
{
|
|
toolBroken = true;
|
|
}
|
|
|
|
if (toolBroken)
|
|
{
|
|
tool.Delete();
|
|
}
|
|
}
|
|
|
|
// SkillCheck failed.
|
|
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)
|
|
{
|
|
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)
|
|
{
|
|
// Stamped here, not in OnCraft: most craftables do not implement ICraftable.
|
|
item.PlayerConstructed = true;
|
|
|
|
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)
|
|
{
|
|
from.SendLocalizedMessage(num);
|
|
}
|
|
}
|
|
|
|
private class InternalTimer : Timer
|
|
{
|
|
private readonly CraftItem m_CraftItem;
|
|
private readonly CraftSystem m_CraftSystem;
|
|
private readonly Mobile m_From;
|
|
private readonly int m_iCountMax;
|
|
private readonly BaseTool m_Tool;
|
|
private readonly Type m_TypeRes;
|
|
private readonly int m_ResHue;
|
|
private int m_iCount;
|
|
|
|
public InternalTimer(
|
|
Mobile from, CraftSystem craftSystem, CraftItem craftItem, Type typeRes, BaseTool tool,
|
|
int iCountMax, int resHue = -1
|
|
) : base(TimeSpan.Zero, TimeSpan.FromSeconds(craftSystem.Delay), iCountMax)
|
|
{
|
|
m_From = from;
|
|
m_CraftItem = craftItem;
|
|
m_iCount = 0;
|
|
m_iCountMax = iCountMax;
|
|
m_CraftSystem = craftSystem;
|
|
m_TypeRes = typeRes;
|
|
m_Tool = tool;
|
|
m_ResHue = resHue;
|
|
}
|
|
|
|
protected override void OnTick()
|
|
{
|
|
m_iCount++;
|
|
|
|
m_From.DisruptiveAction();
|
|
|
|
if (m_iCount < m_iCountMax)
|
|
{
|
|
m_CraftSystem.PlayCraftEffect(m_From);
|
|
return;
|
|
}
|
|
|
|
m_From.EndAction<CraftSystem>();
|
|
|
|
var badCraft = m_CraftSystem.CanCraft(m_From, m_Tool, m_CraftItem.ItemType);
|
|
|
|
if (badCraft > 0)
|
|
{
|
|
if (m_Tool?.Deleted == false && m_Tool.UsesRemaining > 0)
|
|
{
|
|
ShowCraftMenu(m_From, m_CraftSystem, m_Tool, badCraft);
|
|
}
|
|
else
|
|
{
|
|
m_From.SendLocalizedMessage(badCraft);
|
|
}
|
|
|
|
return;
|
|
}
|
|
|
|
var quality = 1;
|
|
|
|
m_CraftItem.CheckSkills(m_From, m_TypeRes, m_CraftSystem, ref quality, out _, false);
|
|
|
|
var context = m_CraftSystem.GetContext(m_From);
|
|
|
|
if (context == null)
|
|
{
|
|
return;
|
|
}
|
|
|
|
if (typeof(CustomCraft).IsAssignableFrom(m_CraftItem.ItemType))
|
|
{
|
|
try
|
|
{
|
|
m_CraftItem.ItemType.CreateInstance<CustomCraft>(
|
|
m_From,
|
|
m_CraftItem,
|
|
m_CraftSystem,
|
|
m_TypeRes,
|
|
m_Tool,
|
|
quality
|
|
)?.EndCraftAction();
|
|
}
|
|
catch (Exception e)
|
|
{
|
|
Console.WriteLine(e);
|
|
}
|
|
|
|
return;
|
|
}
|
|
|
|
var makersMark = false;
|
|
|
|
if (quality == 2 && m_From.Skills[m_CraftSystem.MainSkill].Base >= 100.0)
|
|
{
|
|
makersMark = m_CraftItem.IsMarkable(m_CraftItem.ItemType);
|
|
}
|
|
|
|
// 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(
|
|
new QueryMakersMarkGump(
|
|
quality,
|
|
m_CraftItem,
|
|
m_CraftSystem,
|
|
m_TypeRes,
|
|
m_Tool,
|
|
m_ResHue
|
|
)
|
|
);
|
|
}
|
|
else
|
|
{
|
|
if (context.MarkOption == CraftMarkOption.DoNotMark)
|
|
{
|
|
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);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
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));
|
|
}
|
|
}
|
|
}
|