From 695efc7d6e9873c4731db322d5369bbceeff69b5 Mon Sep 17 00:00:00 2001 From: Kamron Batman <3953314+kamronbatman@users.noreply.github.com> Date: Sun, 23 Aug 2026 12:50:17 -0700 Subject: [PATCH] feat: stateful SpeedClass; the type constant moves to DefaultSpeedClass A virtual SpeedClass could be overridden dynamically (boss state change) and silently diverge from the cached speed entry. SpeedClass is now non-virtual instance state: assigning it invalidates the cached entry, applies the new bucket's think and move speeds (preserving the active/passive mode), and serializes only when it differs from the type's DefaultSpeedClass - so a runtime bucket change survives a save while its (bucket-matching) speeds still elide. Works from [props too. Overrides become: DefaultSpeedClass for a type's constant bucket, SpeedClass assignment for state changes, GetSpeeds/GetMoveSpeeds to bypass the table entirely - none of which can leave the cache stale. SpeedClass deserializes before the speed fields (index 7; later indexes shift by one - v23 was never released, schema regenerated). Co-Authored-By: Claude Fable 5 --- .../Mobiles/BaseCreatureSerializationTests.cs | 57 +++++++ .../Server.Mobiles.BaseCreature.v23.json | 6 + Projects/UOContent/Mobiles/BaseCreature.cs | 143 +++++++++++------- 3 files changed, 152 insertions(+), 54 deletions(-) diff --git a/Projects/UOContent.Tests/Tests/Mobiles/BaseCreatureSerializationTests.cs b/Projects/UOContent.Tests/Tests/Mobiles/BaseCreatureSerializationTests.cs index 6a60f12d3..bf9dd8465 100644 --- a/Projects/UOContent.Tests/Tests/Mobiles/BaseCreatureSerializationTests.cs +++ b/Projects/UOContent.Tests/Tests/Mobiles/BaseCreatureSerializationTests.cs @@ -166,6 +166,63 @@ public class BaseCreatureSerializationTests : IDisposable Assert.Equal(master, copy.LastOwner); } + private sealed class BucketStub : BaseCreature + { + public BucketStub() : base(AIType.AI_Melee) => Body = 0xC9; + + public BucketStub(Serial serial) : base(serial) => Body = 0xC9; + + public override SpeedLevel DefaultSpeedClass => SpeedLevel.Fast; + } + + [Fact] + public void SpeedClass_Assignment_AppliesBucket_AndRoundTrips() + { + NPCSpeeds.RegisterSpeed(new NPCSpeeds.SpeedClassEntry + { + Level = SpeedLevel.Fast, ActiveSpeed = 0.2, PassiveSpeed = 0.4, + ActiveMoveSpeed = 0.3, PassiveMoveSpeed = 0.9, Types = new HashSet() + }); + NPCSpeeds.RegisterSpeed(new NPCSpeeds.SpeedClassEntry + { + Level = SpeedLevel.VeryFast, ActiveSpeed = 0.125, PassiveSpeed = 0.3, + ActiveMoveSpeed = 0.125, PassiveMoveSpeed = 0.6, Types = new HashSet() + }); + + var bc = new BucketStub(); + _created.Add(bc); + + Assert.Equal(0.2, bc.ActiveSpeed); // seeded from the default bucket + Assert.Equal(0.3, bc.ActiveMoveSpeed); + + bc.SpeedClass = SpeedLevel.VeryFast; // boss state change + + Assert.Equal(0.125, bc.ActiveSpeed); + Assert.Equal(0.3, bc.PassiveSpeed); + Assert.Equal(0.125, bc.ActiveMoveSpeed); + Assert.Equal(0.6, bc.PassiveMoveSpeed); + Assert.Equal(0.3, bc.CurrentSpeed); // stayed in the passive mode + + // The changed bucket persists; the (bucket-matching) speeds elide but restore + // through the new bucket - the consistency the stateful SpeedClass guarantees. + var writer = new BufferWriter(true); + bc.Serialize(writer); + var buffer = new byte[writer.Position]; + writer.Buffer.AsSpan(0, (int)writer.Position).CopyTo(buffer); + + var copy = new BucketStub(World.NewMobile); + _created.Add(copy); + var reader = new BufferReader(buffer); + copy.Deserialize(reader); + + Assert.Equal(buffer.Length, reader.Position); + Assert.Equal(SpeedLevel.VeryFast, copy.SpeedClass); + Assert.Equal(0.125, copy.ActiveSpeed); + Assert.Equal(0.3, copy.PassiveSpeed); + Assert.Equal(0.125, copy.ActiveMoveSpeed); + Assert.Equal(0.6, copy.PassiveMoveSpeed); + } + private sealed class MobileStub : Mobile { public MobileStub() => Body = 0xC9; diff --git a/Projects/UOContent/Migrations/Server.Mobiles.BaseCreature.v23.json b/Projects/UOContent/Migrations/Server.Mobiles.BaseCreature.v23.json index b700154ab..8fab5fb7a 100644 --- a/Projects/UOContent/Migrations/Server.Mobiles.BaseCreature.v23.json +++ b/Projects/UOContent/Migrations/Server.Mobiles.BaseCreature.v23.json @@ -55,6 +55,12 @@ "usesSaveFlag": true, "rule": "EnumMigrationRule" }, + { + "name": "SpeedClass", + "type": "Server.Mobiles.SpeedLevel", + "usesSaveFlag": true, + "rule": "EnumMigrationRule" + }, { "name": "ActiveSpeed", "type": "double", diff --git a/Projects/UOContent/Mobiles/BaseCreature.cs b/Projects/UOContent/Mobiles/BaseCreature.cs index efe0eb646..ab6bf1d94 100644 --- a/Projects/UOContent/Mobiles/BaseCreature.cs +++ b/Projects/UOContent/Mobiles/BaseCreature.cs @@ -316,8 +316,40 @@ namespace Server.Mobiles private FightMode FightModeDefaultValue() => FightMode.Closest; + /// + /// The creature's npc-speeds bucket. Assigning applies the bucket's speeds; a + /// type's constant bucket belongs in . + /// + [SerializableField(7, fieldChanged: nameof(OnSpeedClassChange))] + [SaveFlag(nameof(ShouldSerializeSpeedClass), nameof(SpeedClassDefaultValue))] + [SerializedCommandProperty(AccessLevel.GameMaster)] + private SpeedLevel _speedClass; + + private bool ShouldSerializeSpeedClass() => _speedClass != DefaultSpeedClass; + + private SpeedLevel SpeedClassDefaultValue() => DefaultSpeedClass; + + private void OnSpeedClassChange(SpeedLevel oldValue, SpeedLevel newValue) + { + _speedEntry = null; + ApplySpeedClass(); + } + + // Applies the current bucket's speeds, preserving the active/passive mode. + private void ApplySpeedClass() + { + var wasActive = _currentSpeed == _activeSpeed && _currentSpeed != _passiveSpeed; + + GetSpeeds(out var activeSpeed, out var passiveSpeed); + GetMoveSpeeds(out _activeMoveSpeed, out _passiveMoveSpeed); + + ActiveSpeed = activeSpeed; + PassiveSpeed = passiveSpeed; + CurrentSpeed = wasActive ? activeSpeed : passiveSpeed; + } + /// Seconds per AI decision while engaged; see for movement pace. - [SerializableField(7)] + [SerializableField(8)] [SaveFlag(nameof(ShouldSerializeActiveSpeed), nameof(ActiveSpeedDefaultValue))] [SerializedCommandProperty(AccessLevel.GameMaster)] private double _activeSpeed; @@ -335,7 +367,7 @@ namespace Server.Mobiles } /// Seconds per AI decision while idle; see for movement pace. - [SerializableField(8)] + [SerializableField(9)] [SaveFlag(nameof(ShouldSerializePassiveSpeed), nameof(PassiveSpeedDefaultValue))] [SerializedCommandProperty(AccessLevel.GameMaster)] private double _passiveSpeed; @@ -352,7 +384,7 @@ namespace Server.Mobiles return passiveSpeed; } - [SerializableField(9, fieldChanged: nameof(OnCurrentSpeedChange))] + [SerializableField(10, fieldChanged: nameof(OnCurrentSpeedChange))] [SaveFlag(nameof(ShouldSerializeCurrentSpeed), nameof(CurrentSpeedDefaultValue))] [SerializedCommandProperty(AccessLevel.GameMaster)] private double _currentSpeed; @@ -367,7 +399,7 @@ namespace Server.Mobiles /// Movement clock (seconds per step) while engaged; 0 = inherit /// . resolves the pace. /// - [SerializableField(10, allowFieldChange: nameof(CoerceMoveSpeed))] + [SerializableField(11, allowFieldChange: nameof(CoerceMoveSpeed))] [SaveFlag(nameof(ShouldSerializeActiveMoveSpeed), nameof(ActiveMoveSpeedDefaultValue))] [SerializedCommandProperty(AccessLevel.GameMaster)] private double _activeMoveSpeed; @@ -376,7 +408,7 @@ namespace Server.Mobiles /// Movement clock (seconds per step) while idle; 0 = inherit /// . resolves the pace. /// - [SerializableField(11, allowFieldChange: nameof(CoerceMoveSpeed))] + [SerializableField(12, allowFieldChange: nameof(CoerceMoveSpeed))] [SaveFlag(nameof(ShouldSerializePassiveMoveSpeed), nameof(PassiveMoveSpeedDefaultValue))] [SerializedCommandProperty(AccessLevel.GameMaster)] private double _passiveMoveSpeed; @@ -411,21 +443,21 @@ namespace Server.Mobiles return passiveMoveSpeed; } - [SerializableField(12)] + [SerializableField(13)] [SaveFlag(nameof(ShouldSerializeHome))] [SerializedCommandProperty(AccessLevel.GameMaster)] private Point3D _home; private bool ShouldSerializeHome() => _home != Point3D.Zero; - [SerializableField(13)] + [SerializableField(14)] [SaveFlag(nameof(ShouldSerializeHomeMap))] [SerializedCommandProperty(AccessLevel.GameMaster)] private Map _homeMap; private bool ShouldSerializeHomeMap() => _homeMap != null; - [SerializableField(14, fieldChanged: nameof(OnControlledChange))] + [SerializableField(15, fieldChanged: nameof(OnControlledChange))] [SaveFlag(nameof(ShouldSerializeControlled))] [SerializedCommandProperty(AccessLevel.GameMaster)] private bool _controlled; @@ -438,43 +470,43 @@ namespace Server.Mobiles InvalidateProperties(); } - // Field 15: ControlMaster (hand-written property; follower bookkeeping brackets the assignment) + // Field 16: ControlMaster (hand-written property; follower bookkeeping brackets the assignment) private Mobile _controlMaster; private bool ShouldSerializeControlMaster() => _controlMaster != null; - [SerializableField(16)] + [SerializableField(17)] [SaveFlag(nameof(ShouldSerializeControlTarget))] [SerializedCommandProperty(AccessLevel.GameMaster)] private Mobile _controlTarget; private bool ShouldSerializeControlTarget() => _controlTarget != null; - [SerializableField(17)] + [SerializableField(18)] [SaveFlag(nameof(ShouldSerializeControlDest))] [SerializedCommandProperty(AccessLevel.GameMaster)] private Point3D _controlDest; private bool ShouldSerializeControlDest() => _controlDest != Point3D.Zero; - // Field 18: ControlOrder (hand-written property; order logic must run on equal re-assignment) + // Field 19: ControlOrder (hand-written property; order logic must run on equal re-assignment) private OrderType _controlOrder; private bool ShouldSerializeControlOrder() => _controlOrder != OrderType.None; - [SerializableField(19)] + [SerializableField(20)] [SaveFlag(nameof(ShouldSerializeMinTameSkill))] [SerializedCommandProperty(AccessLevel.GameMaster)] private double _minTameSkill; private bool ShouldSerializeMinTameSkill() => _minTameSkill != 0; - // Field 20: Tamable (hand-written property; custom getter masks paragons) + // Field 21: Tamable (hand-written property; custom getter masks paragons) private bool _tamable; private bool ShouldSerializeTamable() => _tamable; - [SerializableField(21, fieldChanged: nameof(OnSummonedChange))] + [SerializableField(22, fieldChanged: nameof(OnSummonedChange))] [SaveFlag(nameof(ShouldSerializeSummoned))] [SerializedCommandProperty(AccessLevel.Administrator)] private bool _summoned; @@ -489,19 +521,19 @@ namespace Server.Mobiles } [AnchoredDateTime] - [SerializableField(22, getter: "protected", setter: "protected")] + [SerializableField(23, getter: "protected", setter: "protected")] [SaveFlag(nameof(ShouldSerializeSummonEnd))] private DateTime _summonEnd; private bool ShouldSerializeSummonEnd() => _summoned; - // Field 23: SummonMaster (hand-written property; follower bookkeeping brackets the assignment) + // Field 24: SummonMaster (hand-written property; follower bookkeeping brackets the assignment) private Mobile _summonMaster; private bool ShouldSerializeSummonMaster() => _summonMaster != null; [EncodedInt] - [SerializableField(24)] + [SerializableField(25)] [SaveFlag(nameof(ShouldSerializeControlSlots), nameof(ControlSlotsDefaultValue))] [SerializedCommandProperty(AccessLevel.Administrator)] private int _controlSlots = 1; @@ -511,7 +543,7 @@ namespace Server.Mobiles private int ControlSlotsDefaultValue() => 1; [EncodedInt] - [SerializableField(25, allowFieldChange: nameof(ClampLoyalty))] + [SerializableField(26, allowFieldChange: nameof(ClampLoyalty))] [SaveFlag(nameof(ShouldSerializeLoyalty), nameof(LoyaltyDefaultValue))] [SerializedCommandProperty(AccessLevel.GameMaster)] private int _loyalty; @@ -526,7 +558,7 @@ namespace Server.Mobiles return true; } - [SerializableField(26)] + [SerializableField(27)] [SaveFlag(nameof(ShouldSerializeCurrentWayPoint))] [SerializedCommandProperty(AccessLevel.GameMaster)] private WayPoint _currentWayPoint; @@ -534,7 +566,7 @@ namespace Server.Mobiles private bool ShouldSerializeCurrentWayPoint() => _currentWayPoint != null; [EncodedInt] - [SerializableField(27)] + [SerializableField(28)] [SaveFlag(nameof(ShouldSerializeHitsMaxSeed), nameof(HitsMaxSeedDefaultValue))] [SerializedCommandProperty(AccessLevel.GameMaster)] private int _hitsMaxSeed = -1; @@ -544,7 +576,7 @@ namespace Server.Mobiles private int HitsMaxSeedDefaultValue() => -1; [EncodedInt] - [SerializableField(28)] + [SerializableField(29)] [SaveFlag(nameof(ShouldSerializeStamMaxSeed), nameof(StamMaxSeedDefaultValue))] [SerializedCommandProperty(AccessLevel.GameMaster)] private int _stamMaxSeed = -1; @@ -554,7 +586,7 @@ namespace Server.Mobiles private int StamMaxSeedDefaultValue() => -1; [EncodedInt] - [SerializableField(29)] + [SerializableField(30)] [SaveFlag(nameof(ShouldSerializeManaMaxSeed), nameof(ManaMaxSeedDefaultValue))] [SerializedCommandProperty(AccessLevel.GameMaster)] private int _manaMaxSeed = -1; @@ -564,7 +596,7 @@ namespace Server.Mobiles private int ManaMaxSeedDefaultValue() => -1; [EncodedInt] - [SerializableField(30)] + [SerializableField(31)] [SaveFlag(nameof(ShouldSerializeDamageMin), nameof(DamageMinDefaultValue))] [SerializedCommandProperty(AccessLevel.GameMaster)] private int _damageMin = -1; @@ -574,7 +606,7 @@ namespace Server.Mobiles private int DamageMinDefaultValue() => -1; [EncodedInt] - [SerializableField(31)] + [SerializableField(32)] [SaveFlag(nameof(ShouldSerializeDamageMax), nameof(DamageMaxDefaultValue))] [SerializedCommandProperty(AccessLevel.GameMaster)] private int _damageMax = -1; @@ -584,7 +616,7 @@ namespace Server.Mobiles private int DamageMaxDefaultValue() => -1; [EncodedInt] - [SerializableField(32, fieldChanged: nameof(OnResistanceSeedChange))] + [SerializableField(33, fieldChanged: nameof(OnResistanceSeedChange))] [SaveFlag(nameof(ShouldSerializePhysicalResistanceSeed))] [SerializedCommandProperty(AccessLevel.GameMaster)] private int _physicalResistanceSeed; @@ -594,7 +626,7 @@ namespace Server.Mobiles private void OnResistanceSeedChange(int oldValue, int newValue) => UpdateResistances(); [EncodedInt] - [SerializableField(33, fieldChanged: nameof(OnResistanceSeedChange))] + [SerializableField(34, fieldChanged: nameof(OnResistanceSeedChange))] [SaveFlag(nameof(ShouldSerializeFireResistSeed))] [SerializedCommandProperty(AccessLevel.GameMaster)] private int _fireResistSeed; @@ -602,7 +634,7 @@ namespace Server.Mobiles private bool ShouldSerializeFireResistSeed() => _fireResistSeed != 0; [EncodedInt] - [SerializableField(34, fieldChanged: nameof(OnResistanceSeedChange))] + [SerializableField(35, fieldChanged: nameof(OnResistanceSeedChange))] [SaveFlag(nameof(ShouldSerializeColdResistSeed))] [SerializedCommandProperty(AccessLevel.GameMaster)] private int _coldResistSeed; @@ -610,7 +642,7 @@ namespace Server.Mobiles private bool ShouldSerializeColdResistSeed() => _coldResistSeed != 0; [EncodedInt] - [SerializableField(35, fieldChanged: nameof(OnResistanceSeedChange))] + [SerializableField(36, fieldChanged: nameof(OnResistanceSeedChange))] [SaveFlag(nameof(ShouldSerializePoisonResistSeed))] [SerializedCommandProperty(AccessLevel.GameMaster)] private int _poisonResistSeed; @@ -618,7 +650,7 @@ namespace Server.Mobiles private bool ShouldSerializePoisonResistSeed() => _poisonResistSeed != 0; [EncodedInt] - [SerializableField(36, fieldChanged: nameof(OnResistanceSeedChange))] + [SerializableField(37, fieldChanged: nameof(OnResistanceSeedChange))] [SaveFlag(nameof(ShouldSerializeEnergyResistSeed))] [SerializedCommandProperty(AccessLevel.GameMaster)] private int _energyResistSeed; @@ -626,7 +658,7 @@ namespace Server.Mobiles private bool ShouldSerializeEnergyResistSeed() => _energyResistSeed != 0; [EncodedInt] - [SerializableField(37)] + [SerializableField(38)] [SaveFlag(nameof(ShouldSerializePhysicalDamage), nameof(PhysicalDamageDefaultValue))] [SerializedCommandProperty(AccessLevel.GameMaster)] private int _physicalDamage = 100; @@ -636,7 +668,7 @@ namespace Server.Mobiles private int PhysicalDamageDefaultValue() => 100; [EncodedInt] - [SerializableField(38)] + [SerializableField(39)] [SaveFlag(nameof(ShouldSerializeFireDamage))] [SerializedCommandProperty(AccessLevel.GameMaster)] private int _fireDamage; @@ -644,7 +676,7 @@ namespace Server.Mobiles private bool ShouldSerializeFireDamage() => _fireDamage != 0; [EncodedInt] - [SerializableField(39)] + [SerializableField(40)] [SaveFlag(nameof(ShouldSerializeColdDamage))] [SerializedCommandProperty(AccessLevel.GameMaster)] private int _coldDamage; @@ -652,7 +684,7 @@ namespace Server.Mobiles private bool ShouldSerializeColdDamage() => _coldDamage != 0; [EncodedInt] - [SerializableField(40)] + [SerializableField(41)] [SaveFlag(nameof(ShouldSerializePoisonDamage))] [SerializedCommandProperty(AccessLevel.GameMaster)] private int _poisonDamage; @@ -660,7 +692,7 @@ namespace Server.Mobiles private bool ShouldSerializePoisonDamage() => _poisonDamage != 0; [EncodedInt] - [SerializableField(41)] + [SerializableField(42)] [SaveFlag(nameof(ShouldSerializeEnergyDamage))] [SerializedCommandProperty(AccessLevel.GameMaster)] private int _energyDamage; @@ -668,7 +700,7 @@ namespace Server.Mobiles private bool ShouldSerializeEnergyDamage() => _energyDamage != 0; [Tidy] - [SerializableField(42, setter: "private")] + [SerializableField(43, setter: "private")] [SaveFlag(nameof(ShouldSerializeOwners), nameof(OwnersDefaultValue))] private List _owners; @@ -680,13 +712,13 @@ namespace Server.Mobiles private List OwnersDefaultValue() => new(); - [SerializableField(43)] + [SerializableField(44)] [SaveFlag(nameof(ShouldSerializeIsDeadPet))] private bool _isDeadPet; private bool ShouldSerializeIsDeadPet() => _isDeadPet; - [SerializableField(44, fieldChanged: nameof(OnBondedChange))] + [SerializableField(45, fieldChanged: nameof(OnBondedChange))] [SaveFlag(nameof(ShouldSerializeIsBonded))] [SerializedCommandProperty(AccessLevel.GameMaster)] private bool _isBonded; @@ -695,33 +727,33 @@ namespace Server.Mobiles private void OnBondedChange(bool oldValue, bool newValue) => InvalidateProperties(); - [SerializableField(45)] + [SerializableField(46)] [SaveFlag(nameof(ShouldSerializeBondingBegin))] [SerializedCommandProperty(AccessLevel.GameMaster)] private DateTime _bondingBegin; private bool ShouldSerializeBondingBegin() => _bondingBegin != DateTime.MinValue; - [SerializableField(46)] + [SerializableField(47)] [SaveFlag(nameof(ShouldSerializeOwnerAbandonTime))] [SerializedCommandProperty(AccessLevel.GameMaster)] private DateTime _ownerAbandonTime; private bool ShouldSerializeOwnerAbandonTime() => _ownerAbandonTime != DateTime.MinValue; - [SerializableField(47)] + [SerializableField(48)] [SaveFlag(nameof(ShouldSerializeHasGeneratedLoot))] private bool _hasGeneratedLoot; private bool ShouldSerializeHasGeneratedLoot() => _hasGeneratedLoot; - // Field 48: IsParagon (hand-written property; the setter converts, which must not run at load) + // Field 49: IsParagon (hand-written property; the setter converts, which must not run at load) private bool _isParagon; private bool ShouldSerializeIsParagon() => _isParagon; [Tidy] - [SerializableField(49, setter: "private")] + [SerializableField(50, setter: "private")] [SaveFlag(nameof(ShouldSerializeFriends))] private List _friends; @@ -731,7 +763,7 @@ namespace Server.Mobiles return _friends?.Count > 0; } - [SerializableField(50)] + [SerializableField(51)] [SaveFlag(nameof(ShouldSerializeRemoveIfUntamed))] [SerializedCommandProperty(AccessLevel.GameMaster)] private bool _removeIfUntamed; @@ -739,14 +771,14 @@ namespace Server.Mobiles private bool ShouldSerializeRemoveIfUntamed() => _removeIfUntamed; [EncodedInt] - [SerializableField(51)] + [SerializableField(52)] [SaveFlag(nameof(ShouldSerializeRemoveStep))] [SerializedCommandProperty(AccessLevel.GameMaster)] private int _removeStep; private bool ShouldSerializeRemoveStep() => _removeStep != 0; - [SerializableField(52, setter: "private")] + [SerializableField(53, setter: "private")] [SaveFlag(nameof(ShouldSerializePendingDeleteTimer))] [DeserializeTimer(nameof(DeserializePendingDeleteTimer))] private Timer _pendingDeleteTimer; @@ -761,7 +793,7 @@ namespace Server.Mobiles _pendingDeleteTimer.Start(); } - [SerializableField(53)] + [SerializableField(54)] [SaveFlag(nameof(ShouldSerializeCorpseNameOverride))] [SerializedCommandProperty(AccessLevel.GameMaster)] private string _corpseNameOverride; @@ -814,6 +846,8 @@ namespace Server.Mobiles _currentAI = ai; _defaultAI = ai; + _speedClass = DefaultSpeedClass; + RangePerception = iRangePerception; RangeFight = iRangeFight; @@ -857,6 +891,7 @@ namespace Server.Mobiles public BaseCreature(Serial serial) : base(serial) { + _speedClass = DefaultSpeedClass; Debug = false; } @@ -911,7 +946,7 @@ namespace Server.Mobiles public virtual double WeaponAbilityChance => 0.4; - [SerializableProperty(48, useField: nameof(_isParagon))] + [SerializableProperty(49, useField: nameof(_isParagon))] [SaveFlag(nameof(ShouldSerializeIsParagon))] [CommandProperty(AccessLevel.GameMaster)] public bool IsParagon @@ -1122,7 +1157,7 @@ namespace Server.Mobiles } } - [SerializableProperty(15, useField: nameof(_controlMaster))] + [SerializableProperty(16, useField: nameof(_controlMaster))] [SaveFlag(nameof(ShouldSerializeControlMaster))] [CommandProperty(AccessLevel.GameMaster)] public Mobile ControlMaster @@ -1148,7 +1183,7 @@ namespace Server.Mobiles } } - [SerializableProperty(23, useField: nameof(_summonMaster))] + [SerializableProperty(24, useField: nameof(_summonMaster))] [SaveFlag(nameof(ShouldSerializeSummonMaster))] [CommandProperty(AccessLevel.GameMaster)] public Mobile SummonMaster @@ -1172,7 +1207,7 @@ namespace Server.Mobiles // Re-issuing the current order must still run the order logic (pet commands), so // this keeps a hand-written setter with no equality skip. - [SerializableProperty(18, useField: nameof(_controlOrder))] + [SerializableProperty(19, useField: nameof(_controlOrder))] [SaveFlag(nameof(ShouldSerializeControlOrder))] [CommandProperty(AccessLevel.GameMaster)] public OrderType ControlOrder @@ -1207,7 +1242,7 @@ namespace Server.Mobiles [CommandProperty(AccessLevel.GameMaster)] public DateTime BardEndTime { get; set; } - [SerializableProperty(20, useField: nameof(_tamable))] + [SerializableProperty(21, useField: nameof(_tamable))] [SaveFlag(nameof(ShouldSerializeTamable))] [CommandProperty(AccessLevel.GameMaster)] public bool Tamable @@ -5075,8 +5110,8 @@ namespace Server.Mobiles } } - // If this needs to be serialized, recommend creating a hash or registry id. Don't serialize strings. - public virtual SpeedLevel SpeedClass => SpeedLevel.None; + // A type's constant bucket; runtime state changes assign SpeedClass instead. + public virtual SpeedLevel DefaultSpeedClass => SpeedLevel.None; // Resolved once per creature; serialization consults the table four times per mob // per save (and again on elided loads), so the dictionary walk must not repeat.