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`).
173 lines
5.7 KiB
C#
173 lines
5.7 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);
|
|
|
|
if (!item.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);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|