From 41ae2d8ffee5bd2397fee783225302c640dc769d Mon Sep 17 00:00:00 2001 From: Kamron Batman <3953314+kamronbatman@users.noreply.github.com> Date: Sun, 23 Aug 2026 09:58:53 -0700 Subject: [PATCH] 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 --- .../Tests/Mobiles/AI/MoveSpeedTests.cs | 23 +++++++++++++ Projects/UOContent/Mobiles/BaseCreature.cs | 32 +++++++++++++++++-- Projects/UOContent/Mobiles/Special/Paragon.cs | 1 + 3 files changed, 53 insertions(+), 3 deletions(-) diff --git a/Projects/UOContent.Tests/Tests/Mobiles/AI/MoveSpeedTests.cs b/Projects/UOContent.Tests/Tests/Mobiles/AI/MoveSpeedTests.cs index 5fd594ad8..f22383cf3 100644 --- a/Projects/UOContent.Tests/Tests/Mobiles/AI/MoveSpeedTests.cs +++ b/Projects/UOContent.Tests/Tests/Mobiles/AI/MoveSpeedTests.cs @@ -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() { diff --git a/Projects/UOContent/Mobiles/BaseCreature.cs b/Projects/UOContent/Mobiles/BaseCreature.cs index 5e001fdbf..016fcaa27 100644 --- a/Projects/UOContent/Mobiles/BaseCreature.cs +++ b/Projects/UOContent/Mobiles/BaseCreature.cs @@ -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 } } + /// + /// 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. + /// + 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; diff --git a/Projects/UOContent/Mobiles/Special/Paragon.cs b/Projects/UOContent/Mobiles/Special/Paragon.cs index 12a21882e..cbd0a30c7 100644 --- a/Projects/UOContent/Mobiles/Special/Paragon.cs +++ b/Projects/UOContent/Mobiles/Special/Paragon.cs @@ -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;