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 <noreply@anthropic.com>
This commit is contained in:
Kamron Batman 2026-08-23 12:50:17 -07:00
parent f2f8313b42
commit 695efc7d6e
No known key found for this signature in database
GPG key ID: 7D81DF26D9A5D94A
3 changed files with 152 additions and 54 deletions

View file

@ -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<Type>()
});
NPCSpeeds.RegisterSpeed(new NPCSpeeds.SpeedClassEntry
{
Level = SpeedLevel.VeryFast, ActiveSpeed = 0.125, PassiveSpeed = 0.3,
ActiveMoveSpeed = 0.125, PassiveMoveSpeed = 0.6, Types = new HashSet<Type>()
});
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;

View file

@ -55,6 +55,12 @@
"usesSaveFlag": true,
"rule": "EnumMigrationRule"
},
{
"name": "SpeedClass",
"type": "Server.Mobiles.SpeedLevel",
"usesSaveFlag": true,
"rule": "EnumMigrationRule"
},
{
"name": "ActiveSpeed",
"type": "double",

View file

@ -316,8 +316,40 @@ namespace Server.Mobiles
private FightMode FightModeDefaultValue() => FightMode.Closest;
/// <summary>
/// The creature's npc-speeds bucket. Assigning applies the bucket's speeds; a
/// type's constant bucket belongs in <see cref="DefaultSpeedClass"/>.
/// </summary>
[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;
}
/// <summary>Seconds per AI decision while engaged; see <see cref="ActiveMoveSpeed"/> for movement pace.</summary>
[SerializableField(7)]
[SerializableField(8)]
[SaveFlag(nameof(ShouldSerializeActiveSpeed), nameof(ActiveSpeedDefaultValue))]
[SerializedCommandProperty(AccessLevel.GameMaster)]
private double _activeSpeed;
@ -335,7 +367,7 @@ namespace Server.Mobiles
}
/// <summary>Seconds per AI decision while idle; see <see cref="PassiveMoveSpeed"/> for movement pace.</summary>
[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
/// <see cref="ActiveSpeed"/>. <see cref="CurrentMoveSpeed"/> resolves the pace.
/// </summary>
[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
/// <see cref="PassiveSpeed"/>. <see cref="CurrentMoveSpeed"/> resolves the pace.
/// </summary>
[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<Mobile> _owners;
@ -680,13 +712,13 @@ namespace Server.Mobiles
private List<Mobile> 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<Mobile> _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.