ModernUO/Projects/UOContent/Items/Clothing/BaseClothing.cs
Kamron Batman 55ac2c3d98
refactor: Move legacy deserialization into the .Migrations.cs partials (#2575)
Follow-up to #2574, which added a `.Migrations.cs` partial to `BaseWeapon`. Pure relocation — no behaviour change.

## The inconsistency

`BaseArmor` and `BaseClothing` already kept their pre-codegen `Deserialize(reader, version)` in a `.Migrations.cs` partial, but left the `OldSaveFlag` enum and the `GetSaveFlag` helper behind in the main class file — even though every call site is in the partial:

| Class | `Deserialize` | `GetSaveFlag` / `OldSaveFlag` | Call sites outside the partial |
|---|---|---|---|
| `BaseArmor` | already in partial | in main file | 0 of 26 |
| `BaseClothing` | already in partial | in main file | 0 of 12 |
| `BaseWeapon` | in main file | in main file | — |

`BaseWeapon` had all three still inline, with its new `.Migrations.cs` holding only a `MigrateFrom`.

## After

All three follow the same layout: `MigrateFrom` newest to oldest, then the pre-codegen `Deserialize`, then `GetSaveFlag`, then `OldSaveFlag`. That moves ~290 lines of legacy read path out of `BaseWeapon.cs` — the file that needed it most at ~3,900 lines — and leaves the main class files describing only how the type behaves today.

## Reviewing this

The diff is large and almost entirely noise, so it is probably not worth reading line by line. Two checks are stronger:

- **Nothing was lost or altered.** Across each `.cs` / `.Migrations.cs` pair, the multiset of non-blank source lines is identical to `main` except for one added comment (below). The relocation was done mechanically and asserted against that invariant rather than by hand.
- **Nothing about serialization moved with the code.** Running `ModernUOSchemaGenerator` after the move emits no new migration files.

The complete set of intentional additions:

- `using System;` in each of the three partials, for the `[Flags]` attribute (implicit usings are not enabled here).
- `// Version 9 (pre-codegen)` above `BaseWeapon`'s moved `Deserialize`, matching the marker `BaseArmor` and `BaseClothing` already carry. Version 9 is correct because `BaseWeapon.v10.json` is its earliest migration schema, so codegen began at 10.

Everything else is blank-line placement.

## Verification

Full solution builds in Release with 0 errors and 0 warnings; 1516 tests pass (815 `Server.Tests`, 701 `UOContent.Tests`).
2026-08-13 18:43:53 -07:00

904 lines
28 KiB
C#

using System;
using System.Collections.Generic;
using ModernUO.Serialization;
using Server.Engines.Craft;
using Server.Ethics;
using Server.Factions;
using Server.Network;
namespace Server.Items
{
public enum ClothingQuality
{
Low,
Regular,
Exceptional
}
public interface IArcaneEquip
{
bool IsArcane { get; }
int CurArcaneCharges { get; set; }
int MaxArcaneCharges { get; set; }
}
[SerializationGenerator(8, false)]
public abstract partial class BaseClothing
: Item, IDyable, IScissorable, IFactionItem, ICraftable, IWearableDurability, IAosItem
{
[SerializableFieldSaveFlag(0)]
private bool ShouldSerializeResource() => _resource != DefaultResource;
[SerializedIgnoreDupe]
[SerializableField(1, setter: "private")]
[SerializedCommandProperty(AccessLevel.GameMaster, canModify: true)]
private AosAttributes _attributes;
[SerializableFieldSaveFlag(1)]
private bool ShouldSerializeAttributes() => !_attributes.IsEmpty;
[SerializableFieldDefault(1)]
private AosAttributes AttributesDefaultValue() => new(this);
[SerializedIgnoreDupe]
[SerializableField(2, setter: "private")]
[SerializedCommandProperty(AccessLevel.GameMaster, canModify: true)]
private AosArmorAttributes _clothingAttributes;
[SerializableFieldSaveFlag(2)]
private bool ShouldSerializeClothingAttributes() => !_clothingAttributes.IsEmpty;
[SerializableFieldDefault(2)]
private AosArmorAttributes ClothingAttributesDefaultValue() => new(this);
[SerializedIgnoreDupe]
[SerializableField(3, setter: "private")]
[SerializedCommandProperty(AccessLevel.GameMaster, canModify: true)]
private AosSkillBonuses _skillBonuses;
[SerializableFieldSaveFlag(3)]
private bool ShouldSerializeSkillBonuses() => !_skillBonuses.IsEmpty;
[SerializableFieldDefault(3)]
private AosSkillBonuses SkillBonusesDefaultValue() => new(this);
[SerializedIgnoreDupe]
[SerializableField(4, setter: "private")]
[SerializedCommandProperty(AccessLevel.GameMaster, canModify: true)]
private AosElementAttributes _resistances;
[SerializableFieldSaveFlag(4)]
private bool ShouldSerializeResistances() => !_resistances.IsEmpty;
[SerializableFieldDefault(4)]
private AosElementAttributes ResistancesDefaultValue() => new(this);
[EncodedInt]
[InvalidateProperties]
[SerializableField(5)]
[SerializedCommandProperty(AccessLevel.GameMaster)]
private int _maxHitPoints;
[SerializableFieldSaveFlag(5)]
private bool ShouldSerializeMaxHitPoints() => _maxHitPoints != 0;
[InvalidateProperties]
[SerializableField(7)]
[SerializedCommandProperty(AccessLevel.GameMaster)]
private string _crafter;
[SerializableFieldSaveFlag(7)]
private bool ShouldSerializeCrafter() => !string.IsNullOrEmpty(_crafter);
[InvalidateProperties]
[SerializableField(8)]
[SerializedCommandProperty(AccessLevel.GameMaster)]
private ClothingQuality _quality = ClothingQuality.Regular;
[SerializableFieldSaveFlag(8)]
private bool ShouldSerializeQuality() => _quality != ClothingQuality.Regular;
// Field 9
private int _strReq = -1;
private FactionItem _factionState;
public BaseClothing(int itemID, Layer layer, int hue = 0) : base(itemID)
{
Layer = layer;
Hue = hue;
_resource = DefaultResource;
_hitPoints = _maxHitPoints = Utility.RandomMinMax(InitMinHits, InitMaxHits);
Attributes = new AosAttributes(this);
ClothingAttributes = new AosArmorAttributes(this);
SkillBonuses = new AosSkillBonuses(this);
Resistances = new AosElementAttributes(this);
}
[SerializableProperty(0)]
[CommandProperty(AccessLevel.GameMaster)]
public CraftResource Resource
{
get => _resource;
set
{
_resource = value;
Hue = CraftResources.GetHue(_resource);
InvalidateProperties();
this.MarkDirty();
}
}
[SerializableProperty(9, useField: nameof(_strReq))]
[CommandProperty(AccessLevel.GameMaster)]
public int StrRequirement
{
get => _strReq == -1 ? Core.AOS ? AosStrReq : OldStrReq : _strReq;
set
{
_strReq = value;
InvalidateProperties();
this.MarkDirty();
}
}
[SerializableFieldSaveFlag(9)]
private bool ShouldSerializeStrReq() => _strReq != -1;
public virtual CraftResource DefaultResource => CraftResource.None;
public virtual int BasePhysicalResistance => 0;
public virtual int BaseFireResistance => 0;
public virtual int BaseColdResistance => 0;
public virtual int BasePoisonResistance => 0;
public virtual int BaseEnergyResistance => 0;
public override int PhysicalResistance => BasePhysicalResistance + Resistances.Physical;
public override int FireResistance => BaseFireResistance + Resistances.Fire;
public override int ColdResistance => BaseColdResistance + Resistances.Cold;
public override int PoisonResistance => BasePoisonResistance + Resistances.Poison;
public override int EnergyResistance => BaseEnergyResistance + Resistances.Energy;
public virtual int ArtifactRarity => 0;
public virtual int BaseStrBonus => 0;
public virtual int BaseDexBonus => 0;
public virtual int BaseIntBonus => 0;
public virtual Race RequiredRace => null;
public virtual int AosStrReq => 10;
public virtual int OldStrReq => 0;
public virtual bool AllowMaleWearer => true;
public virtual bool AllowFemaleWearer => true;
public virtual bool CanBeBlessed => true;
public virtual int OnCraft(
int quality, bool makersMark, Mobile from, CraftSystem craftSystem, Type typeRes,
BaseTool tool, CraftItem craftItem, int resHue
)
{
Quality = (ClothingQuality)quality;
if (makersMark)
{
Crafter = from.RawName;
}
if (DefaultResource != CraftResource.None)
{
var resourceType = typeRes ?? craftItem.Resources[0].ItemType;
Resource = CraftResources.GetFromType(resourceType);
}
else
{
Hue = resHue;
}
var context = craftSystem.GetContext(from);
if (context?.DoNotColor == true)
{
Hue = 0;
}
return quality;
}
public virtual bool Dye(Mobile from, DyeTub sender)
{
if (Deleted)
{
return false;
}
if (RootParent is Mobile && from != RootParent)
{
return false;
}
Hue = sender.DyedHue;
return true;
}
public FactionItem FactionItemState
{
get => _factionState;
set
{
_factionState = value;
if (_factionState == null)
{
Hue = 0;
}
LootType = _factionState == null ? LootType.Regular : LootType.Blessed;
}
}
public virtual bool Scissor(Mobile from, Scissors scissors)
{
if (!IsChildOf(from.Backpack))
{
from.SendLocalizedMessage(502437); // Items you wish to cut must be in your backpack.
return false;
}
if (Ethic.IsImbued(this))
{
from.SendLocalizedMessage(502440); // Scissors can not be used on that to produce anything.
return false;
}
var system = DefTailoring.CraftSystem;
var item = system.CraftItems.SearchFor(GetType());
if (item?.Resources.Count == 1)
{
var resource = item.Resources[0];
if (resource.Amount >= 2)
{
try
{
var info = CraftResources.GetInfo(_resource);
Type resourceType = null;
if (info?.ResourceTypes.Length > 0)
{
resourceType = info.ResourceTypes[0];
}
var res = (resourceType ?? resource.ItemType).CreateInstance<Item>();
ScissorHelper(from, res, PlayerConstructed ? resource.Amount / 2 : 1);
res.LootType = LootType.Regular;
return true;
}
catch
{
// ignored
}
}
}
from.SendLocalizedMessage(502440); // Scissors can not be used on that to produce anything.
return false;
}
public virtual bool CanFortify => true;
[EncodedInt]
[SerializableProperty(6)]
[CommandProperty(AccessLevel.GameMaster)]
public int HitPoints
{
get => _hitPoints;
set
{
if (value != _hitPoints && MaxHitPoints > 0)
{
_hitPoints = value;
if (_hitPoints < 0)
{
Delete();
}
else if (_hitPoints > MaxHitPoints)
{
_hitPoints = MaxHitPoints;
}
InvalidateProperties();
this.MarkDirty();
}
}
}
[SerializableFieldSaveFlag(6)]
private bool ShouldSerializeHitPoints() => _hitPoints != 0;
public virtual int InitMinHits => 0;
public virtual int InitMaxHits => 0;
public virtual int OnHit(BaseWeapon weapon, int damageTaken)
{
var absorbed = Utility.RandomMinMax(1, 4);
// Don't go below zero
damageTaken = Math.Min(absorbed, damageTaken);
if (Utility.Random(100) < 25) // 25% chance to lower durability
{
if (Core.AOS && ClothingAttributes.SelfRepair > Utility.Random(10))
{
HitPoints += 2;
}
else
{
int wear;
if (weapon.Type == WeaponType.Bashing)
{
wear = absorbed / 2;
}
else
{
wear = Utility.Random(2);
}
if (wear > 0 && _maxHitPoints > 0)
{
if (_hitPoints >= wear)
{
HitPoints -= wear;
wear = 0;
}
else
{
wear -= HitPoints;
HitPoints = 0;
}
if (wear > 0)
{
if (_maxHitPoints > wear)
{
MaxHitPoints -= wear;
(Parent as Mobile)?.LocalOverheadMessage(
MessageType.Regular,
0x3B2,
1061121
); // Your equipment is severely damaged.
}
else
{
Delete();
}
}
}
}
}
return damageTaken;
}
public void UnscaleDurability()
{
var scale = 100 + ClothingAttributes.DurabilityBonus;
_hitPoints = (_hitPoints * 100 + (scale - 1)) / scale;
_maxHitPoints = (_maxHitPoints * 100 + (scale - 1)) / scale;
InvalidateProperties();
this.MarkDirty();
}
public void ScaleDurability()
{
var scale = 100 + ClothingAttributes.DurabilityBonus;
_hitPoints = (_hitPoints * scale + 99) / 100;
_maxHitPoints = (_maxHitPoints * scale + 99) / 100;
InvalidateProperties();
this.MarkDirty();
}
public override bool AllowSecureTrade(Mobile from, Mobile to, Mobile newOwner, bool accepted) =>
Ethic.CheckTrade(from, to, newOwner, this) && base.AllowSecureTrade(from, to, newOwner, accepted);
public override bool CanEquip(Mobile from)
{
if (!from.Player || from.AccessLevel >= AccessLevel.GameMaster)
{
return base.CanEquip(from);
}
if (!Ethic.CheckEquip(from, this))
{
return false;
}
if (RequiredRace != null && from.Race != RequiredRace)
{
if (RequiredRace == Race.Elf)
{
from.SendLocalizedMessage(1072203); // Only Elves may use this.
}
else
{
from.SendMessage($"Only {RequiredRace.PluralName} may use this.");
}
return false;
}
if (!AllowMaleWearer && !from.Female)
{
if (AllowFemaleWearer)
{
from.SendLocalizedMessage(1010388); // Only females can wear this.
}
else
{
from.SendMessage("You may not wear this.");
}
return false;
}
if (!AllowFemaleWearer && from.Female)
{
if (AllowMaleWearer)
{
from.SendLocalizedMessage(1063343); // Only males can wear this.
}
else
{
from.SendMessage("You may not wear this.");
}
return false;
}
var strBonus = ComputeStatBonus(StatType.Str);
var strReq = ComputeStatReq(StatType.Str);
if (from.Str < strReq || from.Str + strBonus < 1)
{
from.SendLocalizedMessage(500213); // You are not strong enough to equip that.
return false;
}
return base.CanEquip(from);
}
public int ComputeStatReq(StatType type) => AOS.Scale(StrRequirement, 100 - GetLowerStatReq());
public int ComputeStatBonus(StatType type) =>
type switch
{
StatType.Str => BaseStrBonus + Attributes.BonusStr,
StatType.Dex => BaseDexBonus + Attributes.BonusDex,
_ => BaseIntBonus + Attributes.BonusInt
};
public virtual void AddStatBonuses(Mobile parent)
{
if (parent == null)
{
return;
}
var strBonus = ComputeStatBonus(StatType.Str);
var dexBonus = ComputeStatBonus(StatType.Dex);
var intBonus = ComputeStatBonus(StatType.Int);
if (strBonus == 0 && dexBonus == 0 && intBonus == 0)
{
return;
}
var serial = Serial;
if (strBonus != 0)
{
parent.AddStatMod(new StatMod(StatType.Str, $"{serial}Str", strBonus, TimeSpan.Zero));
}
if (dexBonus != 0)
{
parent.AddStatMod(new StatMod(StatType.Dex, $"{serial}Dex", dexBonus, TimeSpan.Zero));
}
if (intBonus != 0)
{
parent.AddStatMod(new StatMod(StatType.Int, $"{serial}Int", intBonus, TimeSpan.Zero));
}
}
public static void ValidateMobile(Mobile m)
{
for (var i = m.Items.Count - 1; i >= 0; --i)
{
if (i >= m.Items.Count)
{
continue;
}
var item = m.Items[i];
if (item is BaseClothing clothing)
{
if (clothing.RequiredRace != null && m.Race != clothing.RequiredRace)
{
if (clothing.RequiredRace == Race.Elf)
{
m.SendLocalizedMessage(1072203); // Only Elves may use this.
}
else
{
m.SendMessage($"Only {clothing.RequiredRace.PluralName} may use this.");
}
m.AddToBackpack(clothing);
}
else if (!clothing.AllowMaleWearer && !m.Female && m.AccessLevel < AccessLevel.GameMaster)
{
if (clothing.AllowFemaleWearer)
{
m.SendLocalizedMessage(1010388); // Only females can wear this.
}
else
{
m.SendMessage("You may not wear this.");
}
m.AddToBackpack(clothing);
}
else if (!clothing.AllowFemaleWearer && m.Female && m.AccessLevel < AccessLevel.GameMaster)
{
if (clothing.AllowMaleWearer)
{
m.SendLocalizedMessage(1063343); // Only males can wear this.
}
else
{
m.SendMessage("You may not wear this.");
}
m.AddToBackpack(clothing);
}
}
}
}
public int GetLowerStatReq() => !Core.AOS ? 0 : ClothingAttributes.LowerStatReq;
public override void OnAdded(IEntity parent)
{
if (parent is Mobile mob)
{
if (Core.AOS)
{
SkillBonuses.AddTo(mob);
}
AddStatBonuses(mob);
mob.CheckStatTimers();
}
base.OnAdded(parent);
}
public override void OnRemoved(IEntity parent)
{
if (parent is Mobile mob)
{
if (Core.AOS)
{
SkillBonuses.Remove();
}
var serial = Serial;
mob.RemoveStatMod($"{serial}Str");
mob.RemoveStatMod($"{serial}Dex");
mob.RemoveStatMod($"{serial}Int");
mob.CheckStatTimers();
}
base.OnRemoved(parent);
}
public override void OnAfterDuped(Item newItem)
{
if (newItem is not BaseClothing clothing)
{
return;
}
clothing.Attributes = new AosAttributes(newItem, Attributes);
clothing.Resistances = new AosElementAttributes(newItem, Resistances);
clothing.SkillBonuses = new AosSkillBonuses(newItem, SkillBonuses);
clothing.ClothingAttributes = new AosArmorAttributes(newItem, ClothingAttributes);
// Set hue again because of resource
clothing.Hue = Hue;
// Set HP/Max again because of durability
clothing.HitPoints = HitPoints;
clothing.MaxHitPoints = MaxHitPoints;
}
public override bool AllowEquippedCast(Mobile from) =>
base.AllowEquippedCast(from) || Attributes.SpellChanneling != 0;
public override bool CheckPropertyConflict(Mobile m)
{
if (base.CheckPropertyConflict(m))
{
return true;
}
return Layer switch
{
Layer.Pants => m.FindItemOnLayer(Layer.InnerLegs) != null,
Layer.Shirt => m.FindItemOnLayer(Layer.InnerTorso) != null,
_ => false
};
}
public override void AddNameProperty(IPropertyList list)
{
var oreType = _resource switch
{
CraftResource.DullCopper => 1053108,
CraftResource.ShadowIron => 1053107,
CraftResource.Copper => 1053106,
CraftResource.Bronze => 1053105,
CraftResource.Gold => 1053104,
CraftResource.Agapite => 1053103,
CraftResource.Verite => 1053102,
CraftResource.Valorite => 1053101,
CraftResource.SpinedLeather => 1061118,
CraftResource.HornedLeather => 1061117,
CraftResource.BarbedLeather => 1061116,
CraftResource.RedScales => 1060814,
CraftResource.YellowScales => 1060818,
CraftResource.BlackScales => 1060820,
CraftResource.GreenScales => 1060819,
CraftResource.WhiteScales => 1060821,
CraftResource.BlueScales => 1060815,
_ => 0
};
var name = Name;
if (oreType != 0)
{
if (name != null)
{
list.Add(1053099, $"{oreType:#}\t{name}"); // ~1_oretype~ ~2_armortype~
}
else
{
list.Add(1053099, $"{oreType:#}\t{LabelNumber:#}"); // ~1_oretype~ ~2_armortype~
}
}
else if (name == null)
{
list.Add(LabelNumber);
}
else
{
list.Add(name);
}
}
public override void GetProperties(IPropertyList list)
{
base.GetProperties(list);
if (_crafter != null)
{
list.Add(1050043, _crafter); // crafted by ~1_NAME~
}
if (_factionState != null)
{
list.Add(1041350); // faction item
}
if (_quality == ClothingQuality.Exceptional)
{
list.Add(1060636); // exceptional
}
if (RequiredRaces == Race.AllowElvesOnly)
{
list.Add(1075086); // Elves Only
}
if (RequiredRaces == Race.AllowGargoylesOnly)
{
list.Add(1111709); // Gargoyles Only
}
SkillBonuses?.GetProperties(list);
int prop;
if ((prop = ArtifactRarity) > 0)
{
list.Add(1061078, prop); // artifact rarity ~1_val~
}
ClothingAttributes.GetProperties(list);
Attributes.GetProperties(list);
AddResistanceProperties(list);
if ((prop = ClothingAttributes.DurabilityBonus) > 0)
{
list.Add(1060410, prop); // durability ~1_val~%
}
var lowerStatReq = ClothingAttributes.LowerStatReq;
if (lowerStatReq != 0)
{
list.Add(1060435, lowerStatReq); // lower requirements ~1_val~%
}
if ((prop = ComputeStatReq(StatType.Str)) > 0)
{
list.Add(1061170, prop); // strength requirement ~1_val~
}
if (_hitPoints >= 0 && _maxHitPoints > 0)
{
list.Add(1060639, $"{_hitPoints}\t{_maxHitPoints}"); // durability ~1_val~ / ~2_val~
}
}
public override void OnSingleClick(Mobile from)
{
if (!Core.UOTD)
{
OnSingleClickPreUOTD(from);
return;
}
var attrs = new List<EquipInfoAttribute>();
AddEquipInfoAttributes(from, attrs);
int number;
if (Name == null)
{
number = LabelNumber;
}
else
{
LabelTo(from, Name);
number = 1041000;
}
if (attrs.Count == 0 && Crafter == null && Name != null)
{
return;
}
from.NetState.SendDisplayEquipmentInfo(Serial, number, _crafter, false, attrs);
}
public void OnSingleClickPreUOTD(Mobile from)
{
var name = Name;
if (name == null)
{
var articleAnName = (TileData.ItemTable[ItemID].Flags & TileFlag.ArticleAn) != 0;
name = $"{(articleAnName ? "an" : "a")} {Localization.GetText(LabelNumber).ToLowerInvariant()}";
}
if (Crafter == null)
{
LabelTo(from, Quality == ClothingQuality.Exceptional ? $"{name} of exceptional quality" : name);
return;
}
LabelTo(
from,
Quality == ClothingQuality.Exceptional
? $"{name} crafted with exceptional quality by {Crafter}"
: $"{name} crafted by {Crafter}"
);
}
public virtual void AddEquipInfoAttributes(Mobile from, List<EquipInfoAttribute> attrs)
{
if (DisplayLootType)
{
if (LootType == LootType.Blessed)
{
attrs.Add(new EquipInfoAttribute(1038021)); // blessed
}
else if (LootType == LootType.Cursed)
{
attrs.Add(new EquipInfoAttribute(1049643)); // cursed
}
}
if (_factionState != null)
{
attrs.Add(new EquipInfoAttribute(1041350)); // faction item
}
if (_quality == ClothingQuality.Exceptional)
{
attrs.Add(new EquipInfoAttribute(1018305 - (int)_quality));
}
}
public void DistributeBonuses(int amount)
{
for (var i = 0; i < amount; ++i)
{
switch (Utility.Random(5))
{
case 0:
++Resistances.Physical;
break;
case 1:
++Resistances.Fire;
break;
case 2:
++Resistances.Cold;
break;
case 3:
++Resistances.Poison;
break;
case 4:
++Resistances.Energy;
break;
}
}
InvalidateProperties();
}
[AfterDeserialization]
private void AfterDeserialization()
{
if (_maxHitPoints == 0 && _hitPoints == 0)
{
_hitPoints = _maxHitPoints = Utility.RandomMinMax(InitMinHits, InitMaxHits);
}
if (Parent is Mobile parent)
{
if (Core.AOS)
{
SkillBonuses.AddTo(parent);
}
AddStatBonuses(parent);
parent.CheckStatTimers();
}
}
}
}