refactor: collapse the move-speed properties into serialized fields

ActiveMoveSpeed/PassiveMoveSpeed become plain [SerializableField]s (not
virtual): the properties now read the raw override (0 = inheriting) and
CurrentMoveSpeed carries the inherit resolution - it was the only
production reader of the resolving getters. The <=0 coercion moves to an
allowFieldChange hook. Wire format unchanged (schema diff is empty).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Kamron Batman 2026-08-23 12:27:56 -07:00
parent f4327e6a3a
commit 963b9b3b85
No known key found for this signature in database
GPG key ID: 7D81DF26D9A5D94A
4 changed files with 51 additions and 47 deletions

View file

@ -57,8 +57,9 @@ public class MoveSpeedTests : IDisposable
{
var bc = NewCreature();
Assert.Equal(0.3, bc.ActiveMoveSpeed);
Assert.Equal(0.6, bc.PassiveMoveSpeed);
// 0 = no override; the resolved pace comes from CurrentMoveSpeed.
Assert.Equal(0, bc.ActiveMoveSpeed);
Assert.Equal(0, bc.PassiveMoveSpeed);
Assert.Equal(bc.CurrentSpeed, bc.CurrentMoveSpeed);
}
@ -96,8 +97,8 @@ public class MoveSpeedTests : IDisposable
bc.SetSpeed(0.2, 0.4);
Assert.Equal(0.2, bc.ActiveMoveSpeed);
Assert.Equal(0.4, bc.PassiveMoveSpeed);
Assert.Equal(0, bc.ActiveMoveSpeed);
Assert.Equal(0, bc.PassiveMoveSpeed);
}
[Fact]
@ -108,8 +109,10 @@ public class MoveSpeedTests : IDisposable
bc.ActiveMoveSpeed = 0;
Assert.Equal(0.3, bc.ActiveMoveSpeed); // inheriting again
Assert.Equal(0, bc.ActiveMoveSpeed); // inheriting again
Assert.Equal(0.9, bc.PassiveMoveSpeed); // other override untouched
bc.SetCurrentSpeedToActive();
Assert.Equal(0.3, bc.CurrentMoveSpeed); // resolves to the think clock
}
[Fact]
@ -121,7 +124,7 @@ public class MoveSpeedTests : IDisposable
bc.ScaleMoveSpeed(1.0 / 1.2);
Assert.Equal(0.5, bc.ActiveMoveSpeed);
Assert.Equal(bc.PassiveSpeed, bc.PassiveMoveSpeed); // still inheriting, not 0 * scalar
Assert.Equal(0, bc.PassiveMoveSpeed); // still inheriting, not 0 * scalar
}
[Fact]
@ -185,8 +188,8 @@ public class MoveSpeedTests : IDisposable
bc.MigrateMoveSpeeds();
Assert.Equal(0.35, bc.ActiveMoveSpeed);
Assert.Equal(0.6, bc.PassiveMoveSpeed);
Assert.Equal(0, bc.ActiveMoveSpeed); // still inheriting the (tuned) think clock
Assert.Equal(0, bc.PassiveMoveSpeed);
}
[Theory]
@ -213,7 +216,7 @@ public class MoveSpeedTests : IDisposable
// The v22 tail is the last block; exact consumption catches any offset mistake.
Assert.Equal(buffer.Length, reader.Position);
Assert.Equal(overridden ? 0.45 : 0.3, copy.ActiveMoveSpeed);
Assert.Equal(overridden ? 0.9 : 0.6, copy.PassiveMoveSpeed);
Assert.Equal(overridden ? 0.45 : 0, copy.ActiveMoveSpeed);
Assert.Equal(overridden ? 0.9 : 0, copy.PassiveMoveSpeed);
}
}

View file

@ -269,7 +269,8 @@ public class BaseCreatureSerializationTests : IDisposable
Assert.Equal(10, copy.FireResistSeed);
Assert.Equal(0.3, copy.ActiveSpeed);
// v22 wrote explicit zeros for the move overrides ("inherit"), so the resolved
// pace falls back to the think clock through the resolving getters.
Assert.Equal(0.3, copy.ActiveMoveSpeed);
// pace falls back to the think clock.
Assert.Equal(0, copy.ActiveMoveSpeed);
Assert.Equal(0.6, copy.CurrentMoveSpeed); // passive mode, inheriting
}
}

View file

@ -363,11 +363,30 @@ namespace Server.Mobiles
private void OnCurrentSpeedChange(double oldValue, double newValue) => AIObject?.OnCurrentSpeedChanged();
// Movement clock (seconds per step); 0 = inherit the matching think value.
// Serialized through the hand-written resolving properties (fields 10 and 11).
/// <summary>
/// Movement clock (seconds per step) while engaged; 0 = inherit
/// <see cref="ActiveSpeed"/>. <see cref="CurrentMoveSpeed"/> resolves the pace.
/// </summary>
[SerializableField(10, allowFieldChange: nameof(CoerceMoveSpeed))]
[SaveFlag(nameof(ShouldSerializeActiveMoveSpeed), nameof(ActiveMoveSpeedDefaultValue))]
[SerializedCommandProperty(AccessLevel.GameMaster)]
private double _activeMoveSpeed;
/// <summary>
/// Movement clock (seconds per step) while idle; 0 = inherit
/// <see cref="PassiveSpeed"/>. <see cref="CurrentMoveSpeed"/> resolves the pace.
/// </summary>
[SerializableField(11, allowFieldChange: nameof(CoerceMoveSpeed))]
[SaveFlag(nameof(ShouldSerializePassiveMoveSpeed), nameof(PassiveMoveSpeedDefaultValue))]
[SerializedCommandProperty(AccessLevel.GameMaster)]
private double _passiveMoveSpeed;
private bool CoerceMoveSpeed(ref double value)
{
value = Math.Max(0, value); // anything non-positive means "inherit"
return true;
}
private bool ShouldSerializeActiveMoveSpeed()
{
GetMoveSpeeds(out var activeMoveSpeed, out _);
@ -1075,34 +1094,6 @@ namespace Server.Mobiles
[CommandProperty(AccessLevel.GameMaster)]
public virtual int ChaseLeashRange => RangePerception * 2;
/// <summary>Seconds per step while engaged. Inherits <see cref="ActiveSpeed"/>; set 0 to re-inherit.</summary>
[SerializableProperty(10, useField: nameof(_activeMoveSpeed))]
[SaveFlag(nameof(ShouldSerializeActiveMoveSpeed), nameof(ActiveMoveSpeedDefaultValue))]
[CommandProperty(AccessLevel.GameMaster)]
public virtual double ActiveMoveSpeed
{
get => _activeMoveSpeed > 0 ? _activeMoveSpeed : _activeSpeed;
set
{
_activeMoveSpeed = value > 0 ? value : 0;
this.MarkDirty();
}
}
/// <summary>Seconds per step while idle. Inherits <see cref="PassiveSpeed"/>; set 0 to re-inherit.</summary>
[SerializableProperty(11, useField: nameof(_passiveMoveSpeed))]
[SaveFlag(nameof(ShouldSerializePassiveMoveSpeed), nameof(PassiveMoveSpeedDefaultValue))]
[CommandProperty(AccessLevel.GameMaster)]
public virtual double PassiveMoveSpeed
{
get => _passiveMoveSpeed > 0 ? _passiveMoveSpeed : _passiveSpeed;
set
{
_passiveMoveSpeed = value > 0 ? value : 0;
this.MarkDirty();
}
}
// Herded creatures walk at a fixed standard pace regardless of their own speed
// (RunUO's forced 0.3, without its TransformMoveDelay inflation to 0.6).
private const double HerdingMoveSpeed = 0.3;
@ -1129,9 +1120,17 @@ namespace Server.Mobiles
return HerdingMoveSpeed;
}
return _currentSpeed == _activeSpeed ? ActiveMoveSpeed
: _currentSpeed == _passiveSpeed ? PassiveMoveSpeed
: _currentSpeed;
if (_currentSpeed == _activeSpeed)
{
return _activeMoveSpeed > 0 ? _activeMoveSpeed : _activeSpeed;
}
if (_currentSpeed == _passiveSpeed)
{
return _passiveMoveSpeed > 0 ? _passiveMoveSpeed : _passiveSpeed;
}
return _currentSpeed;
}
}

View file

@ -262,8 +262,9 @@ All "speed" values are **delays in seconds** (smaller = faster). A creature runs
(combat decisions, target acquisition, spell timing).
- **Move clock**`ActiveMoveSpeed`/`PassiveMoveSpeed`/`CurrentMoveSpeed`: seconds per
step. Inherits the matching think value until overridden, so a creature configured with
only think speeds behaves as one clock. Any value is legal — steps are scheduled
independently of think ticks, so the two need not divide evenly.
only think speeds behaves as one clock. The properties read the raw override (`0` =
inheriting); `CurrentMoveSpeed` is the resolved pace. Any value is legal — steps are
scheduled independently of think ticks, so the two need not divide evenly.
Speeds normally come from `Distribution/Data/npc-speeds.json` (via `SpeedClass` or type
lists); `activeMove`/`passiveMove` are optional per bucket. Prefer data over code: