ModernUO/Projects/UOContent/Items/Armor/BaseArmor.Migrations.cs
Kamron Batman bd79cb7759
fix: Consolidate PlayerConstructed onto Item, stamped by the craft system (#2574)
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`).
2026-08-13 18:34:59 -07:00

195 lines
6.3 KiB
C#

using AMA = Server.Items.ArmorMeditationAllowance;
namespace Server.Items;
public partial class BaseArmor
{
// PlayerConstructed moved onto Item
private void MigrateFrom(V9Content content)
{
_attributes = content.Attributes ?? AttributesDefaultValue();
_armorAttributes = content.ArmorAttributes ?? ArmorAttributesDefaultValue();
_physicalBonus = content.PhysicalBonus ?? 0;
_fireBonus = content.FireBonus ?? 0;
_coldBonus = content.ColdBonus ?? 0;
_poisonBonus = content.PoisonBonus ?? 0;
_energyBonus = content.EnergyBonus ?? 0;
_identified = content.Identified;
_maxHitPoints = content.MaxHitPoints ?? 0;
_hitPoints = content.HitPoints ?? 0;
_crafter = content.Crafter;
_quality = content.Quality ?? ArmorQuality.Regular;
_durability = content.Durability ?? ArmorDurabilityLevel.Regular;
_protectionLevel = content.ProtectionLevel ?? ArmorProtectionLevel.Regular;
_resource = content.Resource ?? DefaultResource;
_armorBase = content.BaseArmorRating ?? -1;
_strBonus = content.StrBonus ?? -1;
_dexBonus = content.DexBonus ?? -1;
_intBonus = content.IntBonus ?? -1;
_strReq = content.StrRequirement ?? -1;
_dexReq = content.DexRequirement ?? -1;
_intReq = content.IntRequirement ?? -1;
_meditate = content.MeditationAllowance ?? (AMA)(-1);
_skillBonuses = content.SkillBonuses ?? SkillBonusesDefaultValue();
PlayerConstructed = content.PlayerConstructed;
}
private void MigrateFrom(V8Content content)
{
_attributes = content.Attributes ?? AttributesDefaultValue();
_armorAttributes = content.ArmorAttributes ?? ArmorAttributesDefaultValue();
_physicalBonus = content.PhysicalBonus ?? 0;
_fireBonus = content.FireBonus ?? 0;
_coldBonus = content.ColdBonus ?? 0;
_poisonBonus = content.PoisonBonus ?? 0;
_energyBonus = content.EnergyBonus ?? 0;
_identified = content.Identified;
_maxHitPoints = content.MaxHitPoints ?? 0;
_hitPoints = content.HitPoints ?? 0;
Timer.DelayCall((item, crafter) => item._crafter = crafter?.RawName, this, content.Crafter);
_quality = content.Quality ?? ArmorQuality.Regular;
_durability = content.Durability ?? ArmorDurabilityLevel.Regular;
_resource = content.RawResource ?? DefaultResource;
_armorBase = content.BaseArmorRating ?? -1;
_strBonus = content.StrBonus ?? -1;
_dexBonus = content.DexBonus ?? -1;
_intBonus = content.IntBonus ?? -1;
_strReq = content.StrRequirement ?? -1;
_dexReq = content.DexRequirement ?? -1;
_intReq = content.IntRequirement ?? -1;
_meditate = content.MeditationAllowance ?? (AMA)(-1);
_skillBonuses = content.SkillBonuses ?? SkillBonusesDefaultValue();
PlayerConstructed = content.PlayerConstructed;
}
// Version 7 (pre-codegen)
private void Deserialize(IGenericReader reader, int version)
{
var flags = (OldSaveFlag)reader.ReadEncodedInt();
Attributes = new AosAttributes(this);
if (GetSaveFlag(flags, OldSaveFlag.Attributes))
{
Attributes.Deserialize(reader);
}
ArmorAttributes = new AosArmorAttributes(this);
if (GetSaveFlag(flags, OldSaveFlag.ArmorAttributes))
{
ArmorAttributes.Deserialize(reader);
}
if (GetSaveFlag(flags, OldSaveFlag.PhysicalBonus))
{
_physicalBonus = reader.ReadEncodedInt();
}
if (GetSaveFlag(flags, OldSaveFlag.FireBonus))
{
_fireBonus = reader.ReadEncodedInt();
}
if (GetSaveFlag(flags, OldSaveFlag.ColdBonus))
{
_coldBonus = reader.ReadEncodedInt();
}
if (GetSaveFlag(flags, OldSaveFlag.PoisonBonus))
{
_poisonBonus = reader.ReadEncodedInt();
}
if (GetSaveFlag(flags, OldSaveFlag.EnergyBonus))
{
_energyBonus = reader.ReadEncodedInt();
}
_identified = GetSaveFlag(flags, OldSaveFlag.Identified);
if (GetSaveFlag(flags, OldSaveFlag.MaxHitPoints))
{
_maxHitPoints = reader.ReadEncodedInt();
}
if (GetSaveFlag(flags, OldSaveFlag.HitPoints))
{
_hitPoints = reader.ReadEncodedInt();
}
if (GetSaveFlag(flags, OldSaveFlag.Crafter))
{
Timer.DelayCall((item, crafter) => item._crafter = crafter?.RawName, this, reader.ReadEntity<Mobile>());
}
if (GetSaveFlag(flags, OldSaveFlag.Quality))
{
_quality = (ArmorQuality)reader.ReadEncodedInt();
}
if (GetSaveFlag(flags, OldSaveFlag.Durability))
{
_durability = (ArmorDurabilityLevel)reader.ReadEncodedInt();
}
if (GetSaveFlag(flags, OldSaveFlag.Protection))
{
_protectionLevel = (ArmorProtectionLevel)reader.ReadEncodedInt();
}
if (GetSaveFlag(flags, OldSaveFlag.Resource))
{
_resource = (CraftResource)reader.ReadEncodedInt();
}
if (GetSaveFlag(flags, OldSaveFlag.BaseArmor))
{
_armorBase = reader.ReadEncodedInt();
}
if (GetSaveFlag(flags, OldSaveFlag.StrBonus))
{
_strBonus = reader.ReadEncodedInt();
}
if (GetSaveFlag(flags, OldSaveFlag.DexBonus))
{
_dexBonus = reader.ReadEncodedInt();
}
if (GetSaveFlag(flags, OldSaveFlag.IntBonus))
{
_intBonus = reader.ReadEncodedInt();
}
if (GetSaveFlag(flags, OldSaveFlag.StrReq))
{
_strReq = reader.ReadEncodedInt();
}
if (GetSaveFlag(flags, OldSaveFlag.DexReq))
{
_dexReq = reader.ReadEncodedInt();
}
if (GetSaveFlag(flags, OldSaveFlag.IntReq))
{
_intReq = reader.ReadEncodedInt();
}
if (GetSaveFlag(flags, OldSaveFlag.MedAllowance))
{
_meditate = (AMA)reader.ReadEncodedInt();
}
SkillBonuses = new AosSkillBonuses(this);
if (GetSaveFlag(flags, OldSaveFlag.SkillBonuses))
{
SkillBonuses.Deserialize(reader);
}
PlayerConstructed = GetSaveFlag(flags, OldSaveFlag.PlayerConstructed);
}
}