From 8fd3999616922b0c080bce0b756512ec0d5a5abb Mon Sep 17 00:00:00 2001 From: Erik Date: Sun, 6 Sep 2026 23:27:09 +0200 Subject: [PATCH] fix(vt): I widen tDouble-declared distance fields from float to double MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Item I (slice-1 fix round, finishing item 1). CaptureMatchesDeclaredSettingTypeAndValue's ~1.5e-7 float-round-trip tolerance was masking a real cause, not a real value change: every tDouble-declared distance setting whose live field was actually a float (CombatSettings.MaximumRange/MinimumRange/ ApproachDistance/RingDistance/ArcRange/TargetSelectAngleRange/ PetCustomRange/CollisionProjectileRadius/CollisionStepDistance/ SpellRangeFudge, Looting.CorpseApproachRange/CorpseMinimumApproachRange, VitalPlan.HelperHealthDistance/HelperStaminaDistance/HelperManaDistance) lost precision below float's ~7-significant-digit guarantee on every load, since a loaded .usd's real value is a double. - All 15 fields widened from float to double, matching their declared tDouble type. Every physics/combat call site that genuinely needs a float (IProjectileAutomation.EvaluatePath/EvaluatePathWithDiagnostics, ICombatAutomation.CaptureHostileTargets/CaptureCorpses, the fellow-distance Lowest() helper) now casts explicitly at that one use site (CombatController.cs, Looting.cs, VitalRecharge.cs, MossTankCommands.cs, PetAutomation.cs) instead of the field itself being narrowed everywhere it's stored. - MossTankProfileStore's JSON DTOs (InventoryProfileDocument. CorpseApproachRange, CombatProfileDocument.MaximumRange/ ApproachDistance/TargetSelectAngleRange/ArcRange/RingDistance/ PetCustomRange) widened to match, so MossTank's own persisted profiles keep full precision too — their Apply()-side Math.Clamp calls needed no changes (the float literal bounds like 2f/100f already widen to the double overload implicitly). - VtankSettingsProfileSerializer.Apply's `(float)(cell.AsDouble() * 240d)` casts and `cell.AsFloat()` calls for these 15 settings are now plain `cell.AsDouble()` / `cell.AsDouble() * 240d` — no narrowing at all. - ValuesEqual's "d"/"f" branch dropped DoublesEqual/FloatRoundTripTolerance entirely: `a.AsDouble() == b.AsDouble()`, exact, matching every other tag. CaptureMatchesDeclaredSettingTypeAndValue's own test-side tolerance (a second, independently-tolerant comparison) removed the same way — all 135 catalog names now pass under Assert.Equal(exact) with zero special-casing. Full MossTank suite: 581/581 (no count change — this is a precision fix, not new coverage; CaptureMatchesDeclaredSettingTypeAndValue's own 135 cases already existed and now pass exactly instead of within tolerance). App.Tests (Plugin|LaunchOptions filter): 84/84. Co-Authored-By: Claude Fable 5.1 --- .../CombatController.cs | 14 ++-- .../CombatSettings.cs | 25 ++++--- src/AcDream.Plugins.MossTank/Looting.cs | 8 ++- .../MossTankCommands.cs | 4 +- .../MossTankProfileStore.cs | 14 ++-- src/AcDream.Plugins.MossTank/PetAutomation.cs | 4 +- src/AcDream.Plugins.MossTank/VitalPlan.cs | 8 ++- src/AcDream.Plugins.MossTank/VitalRecharge.cs | 4 +- .../VtankSettingsProfileSerializer.cs | 66 ++++++++----------- .../VtankSettingsProfileSerializerTests.cs | 19 ++---- 10 files changed, 79 insertions(+), 87 deletions(-) diff --git a/src/AcDream.Plugins.MossTank/CombatController.cs b/src/AcDream.Plugins.MossTank/CombatController.cs index cf52b5eac..640434bc2 100644 --- a/src/AcDream.Plugins.MossTank/CombatController.cs +++ b/src/AcDream.Plugins.MossTank/CombatController.cs @@ -243,11 +243,11 @@ internal sealed class CombatController _untilScan -= Math.Max(0d, elapsedSeconds); if (_untilScan <= 0d) { - float acquisitionRange = navigationEnabled + double acquisitionRange = navigationEnabled ? Math.Max(_settings.MaximumRange, _settings.ApproachDistance) : _settings.MaximumRange; _targets = _host.Automation.Combat.CaptureHostileTargets( - acquisitionRange); + (float)acquisitionRange); foreach (uint ghost in _failures.ObserveTargets( _targets, _now, @@ -588,7 +588,7 @@ internal sealed class CombatController float maximumRange = spell.BaseRangeConstant + (spell.BaseRangeModifier * skill.Current) - - _settings.SpellRangeFudge; + - (float)_settings.SpellRangeFudge; return maximumRange <= 0f || target.ObjectId == 0u || target.Distance <= MathF.Min(75f, maximumRange); @@ -1087,15 +1087,15 @@ internal sealed class CombatController targetObjectId, kind, height, - _settings.CollisionProjectileRadius, - _settings.CollisionStepDistance, + (float)_settings.CollisionProjectileRadius, + (float)_settings.CollisionStepDistance, _settings.MaximumCollisionChecksPerTick) : _host.Automation.Projectiles.EvaluatePath( targetObjectId, kind, height, - _settings.CollisionProjectileRadius, - _settings.CollisionStepDistance, + (float)_settings.CollisionProjectileRadius, + (float)_settings.CollisionStepDistance, _settings.MaximumCollisionChecksPerTick); if (_settings.ShowCollisionDebug && result.DebugSamples.Count > 0) { diff --git a/src/AcDream.Plugins.MossTank/CombatSettings.cs b/src/AcDream.Plugins.MossTank/CombatSettings.cs index 54fce0447..d0a325fd5 100644 --- a/src/AcDream.Plugins.MossTank/CombatSettings.cs +++ b/src/AcDream.Plugins.MossTank/CombatSettings.cs @@ -69,25 +69,30 @@ internal sealed class CombatSettings public bool Enabled { get; set; } = true; /// VTank's hunt-cast skill margin. public int HuntSkillExcessOverDifficulty { get; set; } = 25; - public float MaximumRange { get; set; } = 5f; + // Declared tDouble in VTank's own Settings table (VtankOptionCatalog): + // double, not float, so a loaded .usd round-trips this setting exactly + // instead of losing precision below float's ~7-digit guarantee (item I, + // slice-1 fix round). Physics/combat consumers that want a float cast + // at their own use site. + public double MaximumRange { get; set; } = 5d; /// /// Monsters nearer than this are not valid attack targets. VTank applies /// this before priority and angle/range ranking. /// - public float MinimumRange { get; set; } + public double MinimumRange { get; set; } /// /// VTank's Approach Distance. Zero disables monster approach; otherwise /// navigation may close a selected target from this range down to /// . /// - public float ApproachDistance { get; set; } + public double ApproachDistance { get; set; } public bool IdlePeaceMode { get; set; } public bool StopMacroOnDeath { get; set; } = true; public bool JumpOutWandCasting { get; set; } public bool DoJiggle { get; set; } public TargetSelectionMethod SelectionMethod { get; set; } = TargetSelectionMethod.Both; - public float TargetSelectAngleRange { get; set; } = 5f; + public double TargetSelectAngleRange { get; set; } = 5d; public bool TargetLock { get; set; } public PluginAttackHeight AttackHeight { get; set; } = PluginAttackHeight.Medium; @@ -101,15 +106,15 @@ internal sealed class CombatSettings public double DebuffPrecastSeconds { get; set; } = 5d; public bool SwitchWandsToDebuff { get; set; } public UseArcsMode UseArcs { get; set; } = UseArcsMode.AtRange; - public float SpellRangeFudge { get; set; } = 1f; + public double SpellRangeFudge { get; set; } = 1d; public bool UseBreakableTurnTo { get; set; } = true; public bool UseProjectileAwareness { get; set; } = true; - public float CollisionProjectileRadius { get; set; } = 0.4f; - public float CollisionStepDistance { get; set; } = 0.7f; + public double CollisionProjectileRadius { get; set; } = 0.4d; + public double CollisionStepDistance { get; set; } = 0.7d; public bool ShowCollisionDebug { get; set; } public int MaximumCollisionChecksPerTick { get; set; } = 500; - public float ArcRange { get; set; } = 5f; - public float RingDistance { get; set; } = 5f; + public double ArcRange { get; set; } = 5d; + public double RingDistance { get; set; } = 5d; public int MinimumRingTargets { get; set; } = 4; public bool DeleteGhostMonsters { get; set; } = true; public int GhostMonsterSpellAttemptCount { get; set; } = 200; @@ -119,7 +124,7 @@ internal sealed class CombatSettings public double GhostDeleteHealthTrackerSeconds { get; set; } = 30d; public bool SummonPets { get; set; } = true; public PetRangeMode PetRangeMode { get; set; } = PetRangeMode.AttackDistance; - public float PetCustomRange { get; set; } = 5f; + public double PetCustomRange { get; set; } = 5d; public int PetMonsterDensity { get; set; } = 1; public int PetRefillCountIdle { get; set; } = 3; public int PetRefillCountNormal { get; set; } = 1; diff --git a/src/AcDream.Plugins.MossTank/Looting.cs b/src/AcDream.Plugins.MossTank/Looting.cs index 5474be545..36016d046 100644 --- a/src/AcDream.Plugins.MossTank/Looting.cs +++ b/src/AcDream.Plugins.MossTank/Looting.cs @@ -89,8 +89,10 @@ internal sealed class LootSettings public bool CombineSalvage { get; set; } = true; public int ManaStoneLootCount { get; set; } = 4; public int ManaTankMinimumMana { get; set; } = 1000; - public float CorpseApproachRange { get; set; } = 40f; - public float CorpseMinimumApproachRange { get; set; } = 3.36f; + // Declared tDouble in VTank's own Settings table (item I, slice-1 fix + // round) — see CombatSettings.MaximumRange's doc comment for why. + public double CorpseApproachRange { get; set; } = 40d; + public double CorpseMinimumApproachRange { get; set; } = 3.36d; public double CorpseOpenTimeoutSeconds { get; set; } = 1.5d; public double CorpseItemAppearanceTimeoutSeconds { get; set; } = 6d; public double CorpseItemIdentifyTimeoutSeconds { get; set; } = 60d; @@ -524,7 +526,7 @@ internal sealed class LootController _scanRemaining = Math.Clamp(_settings.ScanIntervalSeconds, 0.05d, 5d); IReadOnlyList corpses = loot.CaptureCorpses( - Math.Clamp(_settings.CorpseApproachRange, 2f, 100f)); + (float)Math.Clamp(_settings.CorpseApproachRange, 2d, 100d)); PruneCorpseCache(); foreach (PluginLootContainer seen in corpses) _corpseFirstSeen.TryAdd(seen.ObjectId, _lifetime); diff --git a/src/AcDream.Plugins.MossTank/MossTankCommands.cs b/src/AcDream.Plugins.MossTank/MossTankCommands.cs index 08a47647f..8dedbe613 100644 --- a/src/AcDream.Plugins.MossTank/MossTankCommands.cs +++ b/src/AcDream.Plugins.MossTank/MossTankCommands.cs @@ -754,9 +754,9 @@ internal sealed partial class MossTankPanel private void TestPet() { IReadOnlyList targets = _host.Automation.Combat - .CaptureHostileTargets(_combatSettings.PetRangeMode == PetRangeMode.Custom + .CaptureHostileTargets((float)(_combatSettings.PetRangeMode == PetRangeMode.Custom ? _combatSettings.PetCustomRange - : _combatSettings.MaximumRange); + : _combatSettings.MaximumRange)); PetAutomationChoice choice = PetAutomation.Select( _host.Automation.Items.CaptureOwnedItems(), targets, diff --git a/src/AcDream.Plugins.MossTank/MossTankProfileStore.cs b/src/AcDream.Plugins.MossTank/MossTankProfileStore.cs index 01dd5a764..a68b98677 100644 --- a/src/AcDream.Plugins.MossTank/MossTankProfileStore.cs +++ b/src/AcDream.Plugins.MossTank/MossTankProfileStore.cs @@ -430,7 +430,7 @@ internal sealed class MossTankProfileStore public bool CombineSalvage { get; set; } = true; public int ManaStoneLootCount { get; set; } = 4; public int ManaTankMinimumMana { get; set; } = 1000; - public float CorpseApproachRange { get; set; } = 40f; + public double CorpseApproachRange { get; set; } = 40d; public double CorpseOpenTimeoutSeconds { get; set; } = 1.5d; public int BlacklistCorpseOpenAttemptCount { get; set; } = 30; public double BlacklistCorpseOpenTimeoutSeconds { get; set; } = 200d; @@ -597,12 +597,12 @@ internal sealed class MossTankProfileStore private sealed class CombatProfileDocument { public bool Enabled { get; set; } = true; - public float MaximumRange { get; set; } = 5f; - public float ApproachDistance { get; set; } + public double MaximumRange { get; set; } = 5d; + public double ApproachDistance { get; set; } public bool IdlePeaceMode { get; set; } public TargetSelectionMethod SelectionMethod { get; set; } = TargetSelectionMethod.Both; - public float TargetSelectAngleRange { get; set; } = 5f; + public double TargetSelectAngleRange { get; set; } = 5d; public bool TargetLock { get; set; } public PluginAttackHeight AttackHeight { get; set; } = PluginAttackHeight.Medium; @@ -616,8 +616,8 @@ internal sealed class MossTankProfileStore public double DebuffPrecastSeconds { get; set; } = 5d; public bool SwitchWandsToDebuff { get; set; } public UseArcsMode UseArcs { get; set; } = UseArcsMode.AtRange; - public float ArcRange { get; set; } = 5f; - public float RingDistance { get; set; } = 5f; + public double ArcRange { get; set; } = 5d; + public double RingDistance { get; set; } = 5d; public int MinimumRingTargets { get; set; } = 4; public bool DeleteGhostMonsters { get; set; } = true; public int GhostMonsterSpellAttemptCount { get; set; } = 200; @@ -627,7 +627,7 @@ internal sealed class MossTankProfileStore public double GhostDeleteHealthTrackerSeconds { get; set; } = 30d; public bool SummonPets { get; set; } = true; public PetRangeMode PetRangeMode { get; set; } = PetRangeMode.AttackDistance; - public float PetCustomRange { get; set; } = 5f; + public double PetCustomRange { get; set; } = 5d; public int PetMonsterDensity { get; set; } = 1; public int PetRefillCountIdle { get; set; } = 3; public int PetRefillCountNormal { get; set; } = 1; diff --git a/src/AcDream.Plugins.MossTank/PetAutomation.cs b/src/AcDream.Plugins.MossTank/PetAutomation.cs index b70c5b42b..d710247e6 100644 --- a/src/AcDream.Plugins.MossTank/PetAutomation.cs +++ b/src/AcDream.Plugins.MossTank/PetAutomation.cs @@ -117,9 +117,9 @@ internal sealed class PetAutomation if (!settings.SummonPets || activeOwnedPetCount > 0) return PetAutomationChoice.None; - float range = settings.PetRangeMode == PetRangeMode.Custom + float range = (float)(settings.PetRangeMode == PetRangeMode.Custom ? settings.PetCustomRange - : settings.MaximumRange; + : settings.MaximumRange); int density = Math.Max(1, settings.PetMonsterDensity); var eligible = new List<(PluginCombatTarget Target, ResolvedMonsterRule Rule)>(); foreach (PluginCombatTarget target in targets) diff --git a/src/AcDream.Plugins.MossTank/VitalPlan.cs b/src/AcDream.Plugins.MossTank/VitalPlan.cs index 0b0f5d51d..d1bb3101f 100644 --- a/src/AcDream.Plugins.MossTank/VitalPlan.cs +++ b/src/AcDream.Plugins.MossTank/VitalPlan.cs @@ -35,9 +35,11 @@ public sealed class VitalSettings public double HelperHealth { get; set; } = 0.20; public double HelperStamina { get; set; } = 0.01; public double HelperMana { get; set; } = 0.01; - public float HelperHealthDistance { get; set; } = 59.6f; - public float HelperStaminaDistance { get; set; } = 59.6f; - public float HelperManaDistance { get; set; } = 32f; + // Declared tDouble in VTank's own Settings table (item I, slice-1 fix + // round) — see CombatSettings.MaximumRange's doc comment for why. + public double HelperHealthDistance { get; set; } = 59.6d; + public double HelperStaminaDistance { get; set; } = 59.6d; + public double HelperManaDistance { get; set; } = 32d; public bool HelpOthers { get; set; } = true; public bool UseHealersHeart { get; set; } = true; diff --git a/src/AcDream.Plugins.MossTank/VitalRecharge.cs b/src/AcDream.Plugins.MossTank/VitalRecharge.cs index ae863a71c..b87d3cca7 100644 --- a/src/AcDream.Plugins.MossTank/VitalRecharge.cs +++ b/src/AcDream.Plugins.MossTank/VitalRecharge.cs @@ -147,7 +147,7 @@ internal static class VitalRechargePlanner IReadOnlyList members = automation.Fellowship.CaptureMembers(); - foreach ((VitalKind vital, double threshold, float distance, uint baseSpell) + foreach ((VitalKind vital, double threshold, double distance, uint baseSpell) in new[] { (VitalKind.Health, settings.HelperHealth, @@ -158,7 +158,7 @@ internal static class VitalRechargePlanner settings.HelperManaDistance, (uint)SpellId.GiftOfEssence), }) { - PluginFellowMember? target = Lowest(members, vital, threshold, distance); + PluginFellowMember? target = Lowest(members, vital, threshold, (float)distance); if (target is not { } fellow) continue; if (vital == VitalKind.Health diff --git a/src/AcDream.Plugins.MossTank/VtankSettingsProfileSerializer.cs b/src/AcDream.Plugins.MossTank/VtankSettingsProfileSerializer.cs index 4e3df1b3b..584447dbe 100644 --- a/src/AcDream.Plugins.MossTank/VtankSettingsProfileSerializer.cs +++ b/src/AcDream.Plugins.MossTank/VtankSettingsProfileSerializer.cs @@ -147,17 +147,17 @@ internal static class VtankSettingsProfileSerializer }; /// - /// Exact compare on the cell's own type — a tag - /// mismatch is now a real bug (fixed above), not something to paper - /// over with a tolerance. The one narrow exception: several distance - /// settings (AttackDistance, ArcRange, …) are declared tDouble - /// but this port still stores their live value in a float field - /// (CombatSettings.MaximumRange etc.) for the physics/combat math that - /// consumes them; round-tripping through that float costs precision - /// below the 15th significant digit, which is exactly float's ~7-digit - /// guarantee, not a real value change. That residual is bounded by - /// , not an arbitrary fudge factor. - /// Every other tag (bool/int/enum/string/uint) compares byte-exact. + /// Exact compare on the cell's own type, with no tolerance anywhere — + /// a tag mismatch is a real bug, not something to + /// paper over. Item I (slice-1 fix round) removed the one narrow + /// exception this used to carry: every tDouble-declared distance + /// setting (AttackDistance, ArcRange, …) that used to round-trip + /// through a float-typed live field (CombatSettings.MaximumRange + /// etc., costing precision below float's ~7-digit guarantee) now stores + /// that value as a real double — the field widened to match its + /// declared type instead of the compare being loosened to match the + /// field. Physics/combat consumers that want a float cast it at + /// their own use site. /// private static bool ValuesEqual(VtankCell a, VtankCell b) { @@ -169,23 +169,11 @@ internal static class VtankSettingsProfileSerializer "s" => a.AsString() == b.AsString(), "i" => a.AsInt() == b.AsInt(), "u" => a.AsUInt() == b.AsUInt(), - "d" or "f" => DoublesEqual(a.AsDouble(), b.AsDouble()), + "d" or "f" => a.AsDouble() == b.AsDouble(), _ => ReferenceEquals(a, b), }; } - private static bool DoublesEqual(double left, double right) - { - double tolerance = Math.Max(Math.Abs(left), Math.Abs(right)) * FloatRoundTripTolerance; - return Math.Abs(left - right) <= tolerance; - } - - // ~1.2e-7 is float's relative machine epsilon (2^-23); a value that has - // been through one float round trip (Apply's (float)(... * 240d) cast) - // can differ from its original double by up to this much and still be - // the SAME value VTank wrote. - private const double FloatRoundTripTolerance = 1.5e-7; - // ------------------------------------------------------------------ // Apply: .usd cell -> live setting. // ------------------------------------------------------------------ @@ -219,18 +207,18 @@ internal static class VtankSettingsProfileSerializer case "recharge-helper-stam": v.HelperStamina = cell.AsDouble() / 100d; break; case "recharge-helper-mana": v.HelperMana = cell.AsDouble() / 100d; break; case "dohelp": v.HelpOthers = cell.AsBool(); break; - case "attackdistance": c.MaximumRange = (float)(cell.AsDouble() * 240d); break; - case "attackminimumdistance": c.MinimumRange = (float)(cell.AsDouble() * 240d); break; - case "approachdistance": c.ApproachDistance = (float)(cell.AsDouble() * 240d); break; - case "ringdistance": c.RingDistance = (float)(cell.AsDouble() * 240d); break; - case "corpseapproachrange-max": i.Loot.CorpseApproachRange = (float)(cell.AsDouble() * 240d); break; - case "corpseapproachrange-min": i.Loot.CorpseMinimumApproachRange = (float)(cell.AsDouble() * 240d); break; + case "attackdistance": c.MaximumRange = cell.AsDouble() * 240d; break; + case "attackminimumdistance": c.MinimumRange = cell.AsDouble() * 240d; break; + case "approachdistance": c.ApproachDistance = cell.AsDouble() * 240d; break; + case "ringdistance": c.RingDistance = cell.AsDouble() * 240d; break; + case "corpseapproachrange-max": i.Loot.CorpseApproachRange = cell.AsDouble() * 240d; break; + case "corpseapproachrange-min": i.Loot.CorpseMinimumApproachRange = cell.AsDouble() * 240d; break; case "navclosestoprange": n.MinimumDistanceMeters = cell.AsDouble() * 240d; break; case "navfarstoprange": n.MaximumDistanceMeters = cell.AsDouble() * 240d; break; case "useportaldistance": n.PortalUseDistanceMeters = cell.AsDouble() * 240d; break; - case "helperdistancehitp": v.HelperHealthDistance = (float)(cell.AsDouble() * 240d); break; - case "helperdistancestam": v.HelperStaminaDistance = (float)(cell.AsDouble() * 240d); break; - case "helperdistancemana": v.HelperManaDistance = (float)(cell.AsDouble() * 240d); break; + case "helperdistancehitp": v.HelperHealthDistance = cell.AsDouble() * 240d; break; + case "helperdistancestam": v.HelperStaminaDistance = cell.AsDouble() * 240d; break; + case "helperdistancemana": v.HelperManaDistance = cell.AsDouble() * 240d; break; case "minimumringtargets": c.MinimumRingTargets = cell.AsInt(); break; case "defaultmeleeattackheight": c.AttackHeight = (PluginAttackHeight)cell.AsInt(); break; case "castdispelself": v.CastDispelSelf = cell.AsBool(); break; @@ -252,9 +240,9 @@ internal static class VtankSettingsProfileSerializer case "targetlock": c.TargetLock = cell.AsBool(); break; case "stopmacroondeath": c.StopMacroOnDeath = cell.AsBool(); break; case "usearcs": c.UseArcs = (UseArcsMode)cell.AsInt(); break; - case "arcrange": c.ArcRange = (float)(cell.AsDouble() * 240d); break; + case "arcrange": c.ArcRange = cell.AsDouble() * 240d; break; case "targetselectmethod": c.SelectionMethod = (TargetSelectionMethod)(cell.AsInt() - 1); break; - case "targetselectanglerange": c.TargetSelectAngleRange = (float)(cell.AsDouble() * 240d); break; + case "targetselectanglerange": c.TargetSelectAngleRange = cell.AsDouble() * 240d; break; case "idlebufftopoff": b.IdleBuffTopoff = cell.AsBool(); break; case "idlebufftopofftimeseconds": b.IdleBuffTopoffSeconds = cell.AsDouble(); break; case "rebufftimeremainingseconds": b.RebuffWhenUnderSeconds = cell.AsDouble(); break; @@ -323,7 +311,7 @@ internal static class VtankSettingsProfileSerializer case "blacklistcorpseopentimeoutseconds": i.Loot.BlacklistCorpseOpenTimeoutSeconds = cell.AsDouble(); break; case "summonpets": c.SummonPets = cell.AsBool(); break; case "petrangemode": c.PetRangeMode = (PetRangeMode)cell.AsInt(); break; - case "petcustomrange": c.PetCustomRange = (float)(cell.AsDouble() * 240d); break; + case "petcustomrange": c.PetCustomRange = cell.AsDouble() * 240d; break; case "petrefillcount-idle": c.PetRefillCountIdle = cell.AsInt(); break; case "petrefillcount-normal": c.PetRefillCountNormal = cell.AsInt(); break; case "corpseopentimeoutseconds": i.Loot.CorpseOpenTimeoutSeconds = cell.AsDouble(); break; @@ -332,11 +320,11 @@ internal static class VtankSettingsProfileSerializer case "fastcastbuffs": b.FastCastBuffs = cell.AsBool(); break; case "usebreakableturnto": c.UseBreakableTurnTo = cell.AsBool(); break; case "useprojectileawareness": c.UseProjectileAwareness = cell.AsBool(); break; - case "collisionprojectileradius": c.CollisionProjectileRadius = cell.AsFloat(); break; - case "collisionstepdistance": c.CollisionStepDistance = cell.AsFloat(); break; + case "collisionprojectileradius": c.CollisionProjectileRadius = cell.AsDouble(); break; + case "collisionstepdistance": c.CollisionStepDistance = cell.AsDouble(); break; case "showcollisiondebug": c.ShowCollisionDebug = cell.AsBool(); break; case "maximumcollisioncheckspertick": c.MaximumCollisionChecksPerTick = cell.AsInt(); break; - case "spellrangefudge": c.SpellRangeFudge = cell.AsFloat(); break; + case "spellrangefudge": c.SpellRangeFudge = cell.AsDouble(); break; case "buffwithuntrained-item": b.BuffWithUntrainedItemSkill = cell.AsInt(); break; case "buffwithuntrained-creature": b.BuffWithUntrainedCreatureSkill = cell.AsInt(); break; case "buffwithuntrained-life": b.BuffWithUntrainedLifeSkill = cell.AsInt(); break; diff --git a/tests/AcDream.Plugins.MossTank.Tests/VtankSettingsProfileSerializerTests.cs b/tests/AcDream.Plugins.MossTank.Tests/VtankSettingsProfileSerializerTests.cs index 87d93c686..abe1ff1ce 100644 --- a/tests/AcDream.Plugins.MossTank.Tests/VtankSettingsProfileSerializerTests.cs +++ b/tests/AcDream.Plugins.MossTank.Tests/VtankSettingsProfileSerializerTests.cs @@ -170,19 +170,14 @@ public sealed class VtankSettingsProfileSerializerTests break; case "d": case "f": - // A handful of distance settings are declared tDouble but - // still round-trip through a CombatSettings `float` field - // for the physics math that consumes them (Apply's - // `(float)(cell.AsDouble() * 240d)`); that costs precision - // below float's ~7-significant-digit guarantee, not a real - // value change. Bounded exactly like + // Item I (slice-1 fix round): every tDouble-declared + // distance setting's live field was widened from float to + // double (CombatSettings.MaximumRange etc.), so this is now + // a real exact compare — no tolerance, matching // VtankSettingsProfileSerializer's own ValuesEqual. - double expected = fixtureCell.AsDouble(); - double actual = captured.AsDouble(); - double tolerance = Math.Max(Math.Abs(expected), Math.Abs(actual)) * 1.5e-7; - Assert.True( - Math.Abs(expected - actual) <= tolerance, - $"{name}: fixture={expected} captured={actual}"); + Assert.Equal( + fixtureCell.AsDouble(), + captured.AsDouble()); break; default: Assert.Fail($"{name}: unexpected tag '{fixtureCell.Tag}'.");