fix: snap speeds to table values after paragon unconvert

Scaling down then back up drifts some values by an ulp (0.45 and 0.9 do
not survive /1.2 then *1.2 in IEEE doubles; the think values currently
do only by luck of the mantissas). Drifted speeds would read as
hand-tuned - notably by the planned skip-serializing-table-conformant
values optimization, which would then persist every former paragon
forever. UnConvert now snaps both clocks back to exact table values when
within rounding distance (1e-4, the existing speed epsilon); genuinely
tuned speeds are nowhere near it and keep.

Also removes MoveSpeedMod: never read, never written, never serialized.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Kamron Batman 2026-08-23 09:58:53 -07:00
parent 658735fd45
commit 41ae2d8ffe
No known key found for this signature in database
GPG key ID: 7D81DF26D9A5D94A
3 changed files with 53 additions and 3 deletions

View file

@ -124,6 +124,29 @@ public class MoveSpeedTests : IDisposable
Assert.Equal(bc.PassiveSpeed, bc.PassiveMoveSpeed); // still inheriting, not 0 * scalar
}
[Fact]
public void SnapSpeedsToTable_UndoesScalingDrift_KeepsTunedValues()
{
var bc = NewCreature();
bc.TableActiveMove = 0.45;
bc.TablePassiveMove = 0.9;
bc.SetMoveSpeed(0.45, 0.9);
// 0.45 and 0.9 do not survive /1.2 then *1.2 bit-exactly.
bc.ScaleMoveSpeed(1.0 / 1.2);
bc.ScaleMoveSpeed(1.2);
Assert.NotEqual(0.45, bc.ActiveMoveSpeed);
bc.SnapSpeedsToTable();
Assert.Equal(0.45, bc.ActiveMoveSpeed);
Assert.Equal(0.9, bc.PassiveMoveSpeed);
// A hand-tuned value is nowhere near the epsilon and must keep.
bc.SetMoveSpeed(0.7, 0.9);
bc.SnapSpeedsToTable();
Assert.Equal(0.7, bc.ActiveMoveSpeed);
}
[Fact]
public void Migration_MatchingThinkSpeeds_AdoptTableMoveValues()
{

View file

@ -773,9 +773,6 @@ namespace Server.Mobiles
}
}
[CommandProperty(AccessLevel.GameMaster)]
public double MoveSpeedMod { get; set; }
[CommandProperty(AccessLevel.GameMaster)]
public Point3D Home
{
@ -4691,6 +4688,35 @@ namespace Server.Mobiles
}
}
/// <summary>
/// Snaps speeds within rounding distance of the creature's table values back to
/// exact. A scaling buff that divides then multiplies can drift by an ulp (e.g.
/// 0.9 and 0.45 through 1.2), which would read as hand-tuned; call after undoing
/// such a buff. Genuinely tuned speeds are nowhere near the epsilon and keep.
/// </summary>
public void SnapSpeedsToTable()
{
GetSpeeds(out var activeSpeed, out var passiveSpeed);
if (Math.Abs(_activeSpeed - activeSpeed) < 0.0001 && Math.Abs(_passiveSpeed - passiveSpeed) < 0.0001)
{
_activeSpeed = activeSpeed;
_passiveSpeed = passiveSpeed;
}
GetMoveSpeeds(out var activeMoveSpeed, out var passiveMoveSpeed);
if (activeMoveSpeed > 0 && Math.Abs(_activeMoveSpeed - activeMoveSpeed) < 0.0001)
{
_activeMoveSpeed = activeMoveSpeed;
}
if (passiveMoveSpeed > 0 && Math.Abs(_passiveMoveSpeed - passiveMoveSpeed) < 0.0001)
{
_passiveMoveSpeed = passiveMoveSpeed;
}
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void SetCurrentSpeedToActive() => CurrentSpeed = ActiveSpeed;

View file

@ -145,6 +145,7 @@ public static class Paragon
bc.PassiveSpeed *= SpeedBuff;
bc.ActiveSpeed *= SpeedBuff;
bc.ScaleMoveSpeed(SpeedBuff);
bc.SnapSpeedsToTable(); // an ulp of scaling drift must not read as hand-tuned
bc.CurrentSpeed = bc.PassiveSpeed;
bc.DamageMin -= DamageBuff;