## Problem `SmallBOD.EndCombine` validates an item's **type**, **material** and **exceptional quality**, but never checks that the item was actually crafted by a player. Any item matching the request is accepted, including one bought straight from an NPC vendor. https://github.com/modernuo/ModernUO/blob/main/Projects/UOContent/Engines/Bulk%20Orders/SmallBOD.cs#L117-L168 Where a vendor stocks a type a BOD can request, a player can fill the deed by buying the items instead of crafting them, and pocket the reward gold for the difference. Tailoring is the clearest case. `SmallTailorBOD.CreateRandomFor` guarantees `Material = None` and `RequireExceptional = false` below 70.1 skill, so the rolled deed asks for plain cloth items — and tailor vendors stock several of those directly. A qty-20 Bandana BOD can be filled entirely from vendor stock for a small fraction of the reward gold, with no crafting and no material cost. The same shape applies anywhere else a vendor-sold type overlaps a requestable BOD type; tailoring is simply where the low-skill deed generator and the vendor inventory overlap most. ## Fix Add a `PlayerConstructed` check alongside the existing material and quality checks. ```csharp var playerConstructed = armor?.PlayerConstructed ?? clothing?.PlayerConstructed ?? weapon?.PlayerConstructed ?? false; if (!playerConstructed) { from.SendLocalizedMessage(1045169); // The item is not in the request. } ``` This follows the pattern already used in `Engines/Craft/Core/Resmelt.cs` (L98-L100, L155-L160) to distinguish crafted from store-bought items, and reuses the same null-coalescing chain style as the adjacent `GetMaterial(armor?.Resource ?? clothing?.Resource ?? CraftResource.None)` line directly above it. `PlayerConstructed` is already set in `OnCraft` and serialized on all three bases (`BaseArmor`, `BaseWeapon`, `BaseClothing`), so the flag survives restarts and no serialization change is needed. ## Open question — the message There is no dedicated cliloc for "this item must be crafted", so I reused **1045169** (*"The item is not in the request."*). It is arguably accurate — a vendor-bought item genuinely is not what the deed asked for — but it is not precise, and a player who does not know the rule will find it confusing. I would rather flag this than invent a string. If there is a better cliloc, I am happy to switch it. ## Testing `dotnet build Projects/UOContent/UOContent.csproj` — **0 errors, 0 warnings**. Not covered: I have not added an automated test, as I could not find existing coverage for `EndCombine` to extend. Happy to add one if you would like it, with a pointer to the preferred pattern. ## Compatibility note Any *already-existing* vendor-bought item in a player's possession will now be rejected by a BOD. That is the intended behaviour, but it is a visible change for anyone mid-deed. Worth a line in release notes.
175 lines
5.9 KiB
C#
175 lines
5.9 KiB
C#
using System;
|
|
using ModernUO.Serialization;
|
|
using Server.Gumps;
|
|
using Server.Items;
|
|
using Server.Mobiles;
|
|
using Server.Systems.FeatureFlags;
|
|
|
|
namespace Server.Engines.BulkOrders;
|
|
|
|
[SerializationGenerator(0, false)]
|
|
public abstract partial class SmallBOD : BaseBOD
|
|
{
|
|
[InvalidateProperties]
|
|
[SerializableField(0)]
|
|
[SerializedCommandProperty(AccessLevel.GameMaster)]
|
|
private int _amountCur;
|
|
|
|
[SerializableField(1)]
|
|
[SerializedCommandProperty(AccessLevel.GameMaster)]
|
|
private Type _type;
|
|
|
|
[InvalidateProperties]
|
|
[SerializableField(2)]
|
|
[SerializedCommandProperty(AccessLevel.GameMaster)]
|
|
private int _number;
|
|
|
|
[SerializableField(3)]
|
|
[SerializedCommandProperty(AccessLevel.GameMaster)]
|
|
private int _graphic;
|
|
|
|
public SmallBOD(
|
|
int hue, int amountCur, int amountMax, Type type, int number, int graphic, bool requireExeptional,
|
|
BulkMaterialType material
|
|
) : base(hue, amountMax, requireExeptional, material)
|
|
{
|
|
_type = type;
|
|
_graphic = graphic;
|
|
_amountCur = amountCur;
|
|
_number = number;
|
|
}
|
|
|
|
public SmallBOD()
|
|
{
|
|
}
|
|
|
|
[CommandProperty(AccessLevel.GameMaster)]
|
|
public override bool Complete => _amountCur == AmountMax;
|
|
|
|
public override int LabelNumber => 1045151; // a bulk order deed
|
|
|
|
public override void GetProperties(IPropertyList list)
|
|
{
|
|
base.GetProperties(list);
|
|
|
|
list.Add(1060654); // small bulk order
|
|
|
|
if (RequireExceptional)
|
|
{
|
|
list.Add(1045141); // All items must be exceptional.
|
|
}
|
|
|
|
if (Material != BulkMaterialType.None)
|
|
{
|
|
list.Add(SmallBODGump.GetMaterialNumberFor(Material)); // All items must be made with x material.
|
|
}
|
|
|
|
list.Add(1060656, AmountMax); // amount to make: ~1_val~
|
|
list.Add(1060658, $"{_number:#}\t{_amountCur}"); // ~1_val~: ~2_val~
|
|
}
|
|
|
|
public override void OnDoubleClick(Mobile from)
|
|
{
|
|
if (!(IsChildOf(from.Backpack) || InSecureTrade || RootParent is PlayerVendor))
|
|
{
|
|
from.SendLocalizedMessage(1045156); // You must have the deed in your backpack to use it.
|
|
return;
|
|
}
|
|
|
|
if (!ContentFeatureFlags.BulkOrders && from.AccessLevel < AccessLevel.Administrator)
|
|
{
|
|
from.SendMessage(0x22, "Bulk orders are temporarily disabled.");
|
|
return;
|
|
}
|
|
|
|
from.SendGump(new SmallBODGump(this));
|
|
}
|
|
|
|
public override void OnDoubleClickNotAccessible(Mobile from)
|
|
{
|
|
OnDoubleClick(from);
|
|
}
|
|
|
|
public override void OnDoubleClickSecureTrade(Mobile from)
|
|
{
|
|
OnDoubleClick(from);
|
|
}
|
|
|
|
public static BulkMaterialType GetMaterial(CraftResource resource)
|
|
{
|
|
return resource switch
|
|
{
|
|
CraftResource.DullCopper => BulkMaterialType.DullCopper,
|
|
CraftResource.ShadowIron => BulkMaterialType.ShadowIron,
|
|
CraftResource.Copper => BulkMaterialType.Copper,
|
|
CraftResource.Bronze => BulkMaterialType.Bronze,
|
|
CraftResource.Gold => BulkMaterialType.Gold,
|
|
CraftResource.Agapite => BulkMaterialType.Agapite,
|
|
CraftResource.Verite => BulkMaterialType.Verite,
|
|
CraftResource.Valorite => BulkMaterialType.Valorite,
|
|
CraftResource.SpinedLeather => BulkMaterialType.Spined,
|
|
CraftResource.HornedLeather => BulkMaterialType.Horned,
|
|
CraftResource.BarbedLeather => BulkMaterialType.Barbed,
|
|
_ => BulkMaterialType.None
|
|
};
|
|
}
|
|
|
|
public override void EndCombine(Mobile from, Item item)
|
|
{
|
|
var objectType = item.GetType();
|
|
|
|
if (_amountCur >= AmountMax)
|
|
{
|
|
// The maximum amount of requested items have already been combined to this deed.
|
|
from.SendLocalizedMessage(1045166);
|
|
return;
|
|
}
|
|
var armor = item as BaseArmor;
|
|
var clothing = item as BaseClothing;
|
|
var weapon = item as BaseWeapon;
|
|
|
|
|
|
if (Type == null || objectType != Type && !objectType.IsSubclassOf(Type) || armor == null && clothing == null && weapon == null)
|
|
{
|
|
from.SendLocalizedMessage(1045169); // The item is not in the request.
|
|
}
|
|
else
|
|
{
|
|
var material = GetMaterial(armor?.Resource ?? clothing?.Resource ?? CraftResource.None);
|
|
var playerConstructed = armor?.PlayerConstructed ?? clothing?.PlayerConstructed ??
|
|
weapon?.PlayerConstructed ?? false;
|
|
|
|
if (!playerConstructed)
|
|
{
|
|
from.SendLocalizedMessage(1045169); // The item is not in the request.
|
|
}
|
|
else if (Material >= BulkMaterialType.DullCopper && Material <= BulkMaterialType.Valorite && material != Material)
|
|
{
|
|
from.SendLocalizedMessage(1045168); // The item is not made from the requested ore.
|
|
}
|
|
else if (Material >= BulkMaterialType.Spined && Material <= BulkMaterialType.Barbed && material != Material)
|
|
{
|
|
from.SendLocalizedMessage(1049352); // The item is not made from the requested leather type.
|
|
}
|
|
else if (RequireExceptional && armor?.Quality != ArmorQuality.Exceptional &&
|
|
clothing?.Quality != ClothingQuality.Exceptional &&
|
|
weapon?.Quality != WeaponQuality.Exceptional)
|
|
{
|
|
from.SendLocalizedMessage(1045167); // The item must be exceptional.
|
|
}
|
|
else
|
|
{
|
|
item.Delete();
|
|
++AmountCur;
|
|
|
|
from.SendLocalizedMessage(1045170); // The item has been combined with the deed.
|
|
from.SendGump(new SmallBODGump(this));
|
|
|
|
if (_amountCur < AmountMax)
|
|
{
|
|
BeginCombine(from);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|