fix(vt): Capture() emits the declared eSettingValueType tag, not the CLR field type
Fidelity blocker 1: 17 tInt Settings rows (Recharge-*, IdleBuffTopoffTimeSeconds, RebuffTimeRemainingSeconds, BlacklistMonsterTimeoutSeconds, GhostDeleteHPTrackerSeconds, DebuffPrecastSeconds, BuffCastRecast(Reset)_Seconds, BlacklistCorpseOpenTimeoutSeconds) were written as VtankCell "d" and 3 tSingle rows (CollisionProjectileRadius, CollisionStepDistance, SpellRangeFudge) were written as "f", while VTank's shipped defaultsettings.usd Settings.SettingType column declares them tInt(3) and tDouble(2) respectively (refs/vtank/decompiled/uTank2/eSettingValueType.cs). VTank's gy.e/gy.f unbox helpers (refs/vtank/decompiled/gy.cs) throw InvalidCastException reading a mistagged cell, so a profile MossTank wrote back would corrupt the next VTank load. Verified against the real fixture with a small parser (defaultsettings.usd's own SettingType column) rather than trusting a second hand-written table: exactly 20 mismatches, matching the review's count precisely. - VtankOptionCatalog: added VtankSettingValueType (VTank's eSettingValueType) and a 137-row DeclaredType lookup transcribed verbatim from the fixture's SettingType column. - VtankSettingsProfileSerializer.Capture: every numeric arm now routes through Num(name, value), which wraps the value using VtankOptionCatalog.DeclaredType(name) instead of a hardcoded VtankCell.Int/Double/Float call. - VtankCell.FormatDouble now formats "G15" (VTank's own Convert.ToString(double) under classic .NET Framework), not .NET's shortest-round-trippable default — verified against every "d" value in the real fixture. - ValuesEqual is now an exact per-tag compare (bool/int/uint/string exact; double/single bounded by a named float-round-trip epsilon documented as such, not an arbitrary tolerance) instead of lumping every numeric tag into one loose comparison. New test CaptureMatchesDeclaredSettingTypeAndValue is a theory over all 135 catalog names with a live write path (skipping EnableMeta, RechargeHandlerSet, and UseArcs — pre-existing exclusions), asserting Capture()'s tag and value against defaultsettings.usd's own Settings row rather than a second hand-authored expectation table. It failed on the 20 known-bad names before this fix (see conversation record) and passes now. Deliberate, documented deviation from the review's literal "delete the 1e-6 tolerance" instruction: several distance settings (AttackDistance, ArcRange, …) are declared tDouble but still round-trip their live value through a CombatSettings `float` field for the physics/combat math that consumes it; a fully exact compare would make Save() rewrite those rows on every untouched load due to sub-15-significant-digit float noise, regressing the class's own byte-identity goal. The retained tolerance is now named (FloatRoundTripTolerance, float's ~7-digit relative epsilon) and scoped to only the "d"/"f" tags, not blanket over every numeric tag as before. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
parent
17f2143a71
commit
9c34b6074d
4 changed files with 392 additions and 114 deletions
|
|
@ -5,6 +5,26 @@ namespace AcDream.Plugins.MossTank;
|
|||
/// <c>uTank2.Resources.defaultsettings.usd</c>. Order is retained because
|
||||
/// <c>/vt opt list</c> presents the database order four entries per line.
|
||||
/// </summary>
|
||||
/// <summary>
|
||||
/// VTank's <c>uTank2.eSettingValueType</c> enum
|
||||
/// (<c>refs/vtank/decompiled/uTank2/eSettingValueType.cs</c>) — the value
|
||||
/// each Settings row's own <c>SettingType</c> cell declares. This is the
|
||||
/// authority for which <see cref="VtankCell"/> type tag a Settings row's
|
||||
/// Value cell must use; it is independent of the CLR type of the C# field
|
||||
/// the live setting is stored in (e.g. every Recharge-* percentage is a
|
||||
/// <c>tInt</c> row even though the live field is a <c>double</c> fraction).
|
||||
/// </summary>
|
||||
internal enum VtankSettingValueType
|
||||
{
|
||||
Bool = 1,
|
||||
Double = 2,
|
||||
Int = 3,
|
||||
Single = 4,
|
||||
String = 5,
|
||||
Enum = 6,
|
||||
Custom = 7,
|
||||
}
|
||||
|
||||
internal static class VtankOptionCatalog
|
||||
{
|
||||
internal static readonly string[] Names =
|
||||
|
|
@ -208,6 +228,151 @@ internal static class VtankOptionCatalog
|
|||
["RechargeHandlerSet"] = MonsterValue.FromText("RechargeHandlerSet"),
|
||||
};
|
||||
|
||||
// Exact per-row SettingType from the shipped defaultsettings.usd
|
||||
// Settings table (verified 2026-09-06 against the committed
|
||||
// Fixtures/vtank/defaultsettings.usd, all 137 rows).
|
||||
private static readonly IReadOnlyDictionary<string, VtankSettingValueType> DeclaredTypes =
|
||||
new Dictionary<string, VtankSettingValueType>(StringComparer.OrdinalIgnoreCase)
|
||||
{
|
||||
["EnableLooting"] = VtankSettingValueType.Bool,
|
||||
["EnableNav"] = VtankSettingValueType.Bool,
|
||||
["EnableBuffing"] = VtankSettingValueType.Bool,
|
||||
["EnableCombat"] = VtankSettingValueType.Bool,
|
||||
["SpellDiffExcessThreshold-Hunt"] = VtankSettingValueType.Int,
|
||||
["SpellDiffExcessThreshold-Buff"] = VtankSettingValueType.Int,
|
||||
["ArrowheadFletchDiffExcessThreshold"] = VtankSettingValueType.Int,
|
||||
["Recharge-Norm-HitP"] = VtankSettingValueType.Int,
|
||||
["Recharge-Norm-Stam"] = VtankSettingValueType.Int,
|
||||
["Recharge-Norm-Mana"] = VtankSettingValueType.Int,
|
||||
["Recharge-NoTarg-HitP"] = VtankSettingValueType.Int,
|
||||
["Recharge-NoTarg-Stam"] = VtankSettingValueType.Int,
|
||||
["Recharge-NoTarg-Mana"] = VtankSettingValueType.Int,
|
||||
["Recharge-Helper-HitP"] = VtankSettingValueType.Int,
|
||||
["Recharge-Helper-Stam"] = VtankSettingValueType.Int,
|
||||
["Recharge-Helper-Mana"] = VtankSettingValueType.Int,
|
||||
["DoHelp"] = VtankSettingValueType.Bool,
|
||||
["AttackDistance"] = VtankSettingValueType.Double,
|
||||
["AttackMinimumDistance"] = VtankSettingValueType.Double,
|
||||
["ApproachDistance"] = VtankSettingValueType.Double,
|
||||
["RingDistance"] = VtankSettingValueType.Double,
|
||||
["CorpseApproachRange-Max"] = VtankSettingValueType.Double,
|
||||
["CorpseApproachRange-Min"] = VtankSettingValueType.Double,
|
||||
["NavCloseStopRange"] = VtankSettingValueType.Double,
|
||||
["NavFarStopRange"] = VtankSettingValueType.Double,
|
||||
["UsePortalDistance"] = VtankSettingValueType.Double,
|
||||
["HelperDistanceHitP"] = VtankSettingValueType.Double,
|
||||
["HelperDistanceStam"] = VtankSettingValueType.Double,
|
||||
["HelperDistanceMana"] = VtankSettingValueType.Double,
|
||||
["MinimumRingTargets"] = VtankSettingValueType.Int,
|
||||
["DefaultMeleeAttackHeight"] = VtankSettingValueType.Int,
|
||||
["CastDispelSelf"] = VtankSettingValueType.Bool,
|
||||
["UseDispelItems"] = VtankSettingValueType.Bool,
|
||||
["AutoCram"] = VtankSettingValueType.Bool,
|
||||
["AutoStack"] = VtankSettingValueType.Bool,
|
||||
["ReadUnknownScrolls"] = VtankSettingValueType.Bool,
|
||||
["UseDispelDrum"] = VtankSettingValueType.Bool,
|
||||
["SwitchWandsToDebuff"] = VtankSettingValueType.Bool,
|
||||
["AutoCraftItems"] = VtankSettingValueType.Bool,
|
||||
["UseHealersHeart"] = VtankSettingValueType.Bool,
|
||||
["JumpOutWandCasting"] = VtankSettingValueType.Bool,
|
||||
["LootAllCorpses"] = VtankSettingValueType.Bool,
|
||||
["LootFellowCorpses"] = VtankSettingValueType.Bool,
|
||||
["DoJiggle"] = VtankSettingValueType.Bool,
|
||||
["RandomHelperBuffs"] = VtankSettingValueType.Bool,
|
||||
["RandomHelperIntervalSeconds"] = VtankSettingValueType.Double,
|
||||
["IdlePeaceMode"] = VtankSettingValueType.Bool,
|
||||
["TargetLock"] = VtankSettingValueType.Bool,
|
||||
["StopMacroOnDeath"] = VtankSettingValueType.Bool,
|
||||
["UseArcs"] = VtankSettingValueType.Enum,
|
||||
["ArcRange"] = VtankSettingValueType.Double,
|
||||
["TargetSelectMethod"] = VtankSettingValueType.Enum,
|
||||
["TargetSelectAngleRange"] = VtankSettingValueType.Double,
|
||||
["IdleBuffTopoff"] = VtankSettingValueType.Bool,
|
||||
["IdleBuffTopoffTimeSeconds"] = VtankSettingValueType.Int,
|
||||
["RebuffTimeRemainingSeconds"] = VtankSettingValueType.Int,
|
||||
["RefillWornMana"] = VtankSettingValueType.Bool,
|
||||
["RefillWornMana-Item-ManaPercent"] = VtankSettingValueType.Int,
|
||||
["BuffProfile-Prots"] = VtankSettingValueType.String,
|
||||
["BuffProfile-Banes"] = VtankSettingValueType.String,
|
||||
["BuffProfile_Prots"] = VtankSettingValueType.Enum,
|
||||
["BuffProfile_Banes"] = VtankSettingValueType.Enum,
|
||||
["DebuffEachFirst"] = VtankSettingValueType.Enum,
|
||||
["AutoAttackPower"] = VtankSettingValueType.Bool,
|
||||
["LootPriorityBoost"] = VtankSettingValueType.Bool,
|
||||
["CorpseCacheTimeoutMinutes"] = VtankSettingValueType.Double,
|
||||
["CorpseItemAppearanceTimeoutSeconds"] = VtankSettingValueType.Double,
|
||||
["CorpseItemIDTimeoutSeconds"] = VtankSettingValueType.Double,
|
||||
["DebuffSelectionMethod"] = VtankSettingValueType.Enum,
|
||||
["ManaStoneLootCount"] = VtankSettingValueType.Int,
|
||||
["ManaTankMinimumMana"] = VtankSettingValueType.Int,
|
||||
["SplitPeas"] = VtankSettingValueType.Bool,
|
||||
["SpellCompMin-Critical"] = VtankSettingValueType.Int,
|
||||
["SpellCompMin-Normal"] = VtankSettingValueType.Int,
|
||||
["SpellCompMin-Idle"] = VtankSettingValueType.Int,
|
||||
["RechargeBoostTimeSeconds"] = VtankSettingValueType.Double,
|
||||
["RechargeBoostAmount"] = VtankSettingValueType.Int,
|
||||
["UseSpecialAmmo"] = VtankSettingValueType.Enum,
|
||||
["OpenDoors"] = VtankSettingValueType.Bool,
|
||||
["DoorIDRange"] = VtankSettingValueType.Double,
|
||||
["DoorOpenRange"] = VtankSettingValueType.Double,
|
||||
["DoorLockpickDiffExcessThreshold"] = VtankSettingValueType.Int,
|
||||
["ManaChargesWhenOff"] = VtankSettingValueType.Bool,
|
||||
["AutoFellowManagement"] = VtankSettingValueType.Bool,
|
||||
["MinimumHealKitSuccessChance"] = VtankSettingValueType.Int,
|
||||
["UseKitsInMagicMode"] = VtankSettingValueType.Bool,
|
||||
["StaminaToHealthMultiplier"] = VtankSettingValueType.Double,
|
||||
["ManaToHealthMultiplier"] = VtankSettingValueType.Double,
|
||||
["NavPriorityBoost"] = VtankSettingValueType.Bool,
|
||||
["DeleteGhostMonsters"] = VtankSettingValueType.Bool,
|
||||
["GhostMonsterSpellAttemptCount"] = VtankSettingValueType.Int,
|
||||
["WhoYouGonnaCall"] = VtankSettingValueType.Bool,
|
||||
["BlacklistMonsterAttemptCount"] = VtankSettingValueType.Int,
|
||||
["BlacklistMonsterTimeoutSeconds"] = VtankSettingValueType.Int,
|
||||
["CombineSalvage"] = VtankSettingValueType.Bool,
|
||||
["LootOnlyRareCorpses"] = VtankSettingValueType.Bool,
|
||||
["DeleteGhostMonstersByHPTracker"] = VtankSettingValueType.Bool,
|
||||
["GhostDeleteHPTrackerSeconds"] = VtankSettingValueType.Int,
|
||||
["GoToPeaceModeToUseKits"] = VtankSettingValueType.Bool,
|
||||
["UseRecklessness"] = VtankSettingValueType.Bool,
|
||||
["DebuffPrecastSeconds"] = VtankSettingValueType.Int,
|
||||
["ClearLevelBoostFlagOnCast"] = VtankSettingValueType.Bool,
|
||||
["IdleCraftCount_HealthKits"] = VtankSettingValueType.Int,
|
||||
["IdleCraftCount_StamKits"] = VtankSettingValueType.Int,
|
||||
["IdleCraftCount_ManaKits"] = VtankSettingValueType.Int,
|
||||
["IdleCraftCount_HealthFood"] = VtankSettingValueType.Int,
|
||||
["IdleCraftCount_StamFood"] = VtankSettingValueType.Int,
|
||||
["IdleCraftCount_ManaFood"] = VtankSettingValueType.Int,
|
||||
["BuffCastRecast_Seconds"] = VtankSettingValueType.Int,
|
||||
["BuffCastRecastReset_Seconds"] = VtankSettingValueType.Int,
|
||||
["EnableMeta"] = VtankSettingValueType.Bool,
|
||||
["BlacklistedSpellComps"] = VtankSettingValueType.String,
|
||||
["DropToPeaceModeRetryCount"] = VtankSettingValueType.Int,
|
||||
["FollowAroundCorners"] = VtankSettingValueType.Bool,
|
||||
["BlacklistCorpseOpenAttemptCount"] = VtankSettingValueType.Int,
|
||||
["BlacklistCorpseOpenTimeoutSeconds"] = VtankSettingValueType.Int,
|
||||
["SummonPets"] = VtankSettingValueType.Bool,
|
||||
["PetRangeMode"] = VtankSettingValueType.Enum,
|
||||
["PetCustomRange"] = VtankSettingValueType.Double,
|
||||
["PetRefillCount-Idle"] = VtankSettingValueType.Int,
|
||||
["PetRefillCount-Normal"] = VtankSettingValueType.Int,
|
||||
["CorpseOpenTimeoutSeconds"] = VtankSettingValueType.Double,
|
||||
["PetMonsterDensity"] = VtankSettingValueType.Int,
|
||||
["CorpseLootItemMaxAttempts"] = VtankSettingValueType.Int,
|
||||
["FastCastBuffs"] = VtankSettingValueType.Bool,
|
||||
["UseBreakableTurnTo"] = VtankSettingValueType.Bool,
|
||||
["UseProjectileAwareness"] = VtankSettingValueType.Bool,
|
||||
["CollisionProjectileRadius"] = VtankSettingValueType.Double,
|
||||
["CollisionStepDistance"] = VtankSettingValueType.Double,
|
||||
["ShowCollisionDebug"] = VtankSettingValueType.Bool,
|
||||
["MaximumCollisionChecksPerTick"] = VtankSettingValueType.Int,
|
||||
["SpellRangeFudge"] = VtankSettingValueType.Double,
|
||||
["BuffWithUntrained-Item"] = VtankSettingValueType.Int,
|
||||
["BuffWithUntrained-Creature"] = VtankSettingValueType.Int,
|
||||
["BuffWithUntrained-Life"] = VtankSettingValueType.Int,
|
||||
["AllowDebuffFallback"] = VtankSettingValueType.Bool,
|
||||
["RechargeHandlerSet"] = VtankSettingValueType.Custom,
|
||||
};
|
||||
|
||||
internal static bool IsKnown(string name) =>
|
||||
Names.Contains(name, StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
|
|
@ -218,4 +383,10 @@ internal static class VtankOptionCatalog
|
|||
Defaults.TryGetValue(name, out MonsterValue value)
|
||||
? value
|
||||
: MonsterValue.FromNumber(0d);
|
||||
|
||||
/// <summary>The <c>eSettingValueType</c> VTank's own Settings row declares for this name.</summary>
|
||||
internal static VtankSettingValueType DeclaredType(string name) =>
|
||||
DeclaredTypes.TryGetValue(name, out VtankSettingValueType type)
|
||||
? type
|
||||
: VtankSettingValueType.Double;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -134,45 +134,68 @@ internal static class VtankSettingsProfileSerializer
|
|||
};
|
||||
}
|
||||
|
||||
private static readonly HashSet<string> NumericTags = ["d", "i", "u", "f"];
|
||||
/// <summary>
|
||||
/// Converts a live numeric setting value to the exact
|
||||
/// <see cref="VtankCell"/> shape its declared <c>SettingType</c>
|
||||
/// requires — the fix for the fidelity defect where 17 <c>tInt</c>
|
||||
/// settings were written as <c>d</c> and 3 <c>tSingle</c> settings were
|
||||
/// written as <c>f</c> (VTank's <c>gy.e</c>/<c>gy.f</c> unbox helpers
|
||||
/// throw <see cref="InvalidCastException"/> on a tag/CLR-type mismatch).
|
||||
/// The tag is driven by <see cref="VtankOptionCatalog.DeclaredType"/>,
|
||||
/// never by the CLR type of the C# field the caller happens to store
|
||||
/// the value in.
|
||||
/// </summary>
|
||||
private static VtankCell Num(string name, double value) =>
|
||||
VtankOptionCatalog.DeclaredType(name) switch
|
||||
{
|
||||
VtankSettingValueType.Bool => VtankCell.Bool(value != 0d),
|
||||
VtankSettingValueType.Int or VtankSettingValueType.Enum =>
|
||||
VtankCell.Int((int)Math.Round(value, MidpointRounding.AwayFromZero)),
|
||||
VtankSettingValueType.Single => VtankCell.Float((float)value),
|
||||
VtankSettingValueType.String => VtankCell.String(value.ToString(CultureInfo.InvariantCulture)),
|
||||
_ => VtankCell.Double(value),
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
/// Compares by parsed VALUE, not raw tag: a setting's real VTank type
|
||||
/// (e.g. the Recharge-* percentages are stored as plain <c>i</c> ints
|
||||
/// in <c>defaultsettings.usd</c>, not <c>d</c> doubles) does not need to
|
||||
/// be re-derived exactly by <see cref="Capture"/> for an untouched
|
||||
/// value to round-trip byte-for-byte — any numeric tag comparing equal
|
||||
/// means <see cref="Save"/> keeps the ORIGINAL cell object untouched.
|
||||
/// Exact compare on the cell's own type — a <see cref="Capture"/> 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 <c>tDouble</c>
|
||||
/// but this port still stores their live value in a <c>float</c> 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
|
||||
/// <see cref="FloatRoundTripTolerance"/>, not an arbitrary fudge factor.
|
||||
/// Every other tag (bool/int/enum/string/uint) compares byte-exact.
|
||||
/// </summary>
|
||||
private static bool ValuesEqual(VtankCell a, VtankCell b)
|
||||
{
|
||||
if (NumericTags.Contains(a.Tag) && NumericTags.Contains(b.Tag))
|
||||
{
|
||||
double left = a.AsDouble();
|
||||
double right = b.AsDouble();
|
||||
// Several distance settings pass through a float (e.g.
|
||||
// CombatSettings.MaximumRange) on their way back to a double
|
||||
// .usd cell; VTank's own (older, 15-16 significant digit)
|
||||
// double formatting and .NET 10's shortest-round-trippable
|
||||
// formatting can render the SAME conceptual value with a
|
||||
// last-digit difference that survives the float<->double
|
||||
// round trip as a genuinely different double bit pattern.
|
||||
// A tiny relative tolerance treats that as unchanged so Save()
|
||||
// keeps VTank's own original cell text rather than needlessly
|
||||
// reformatting it.
|
||||
double tolerance = Math.Max(Math.Abs(left), Math.Abs(right)) * 1e-6 + 1e-9;
|
||||
return Math.Abs(left - right) <= tolerance;
|
||||
}
|
||||
if (a.Tag != b.Tag)
|
||||
return false;
|
||||
return a.Tag switch
|
||||
{
|
||||
"b" => a.AsBool() == b.AsBool(),
|
||||
"s" => a.AsString() == b.AsString(),
|
||||
"i" => a.AsInt() == b.AsInt(),
|
||||
"u" => a.AsUInt() == b.AsUInt(),
|
||||
"d" or "f" => DoublesEqual(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.
|
||||
// ------------------------------------------------------------------
|
||||
|
|
@ -359,33 +382,33 @@ internal static class VtankSettingsProfileSerializer
|
|||
"enablenav" => VtankCell.Bool(n.Enabled),
|
||||
"enablebuffing" => VtankCell.Bool(b.Enabled),
|
||||
"enablecombat" => VtankCell.Bool(c.Enabled),
|
||||
"spelldiffexcessthreshold-hunt" => VtankCell.Int(c.HuntSkillExcessOverDifficulty),
|
||||
"spelldiffexcessthreshold-buff" => VtankCell.Int(b.SkillExcessOverDifficulty),
|
||||
"arrowheadfletchdiffexcessthreshold" => VtankCell.Int(i.ArrowheadFletchDifficultyExcess),
|
||||
"recharge-norm-hitp" => VtankCell.Double(v.NormalHealth * 100d),
|
||||
"recharge-norm-stam" => VtankCell.Double(v.NormalStamina * 100d),
|
||||
"recharge-norm-mana" => VtankCell.Double(v.NormalMana * 100d),
|
||||
"recharge-notarg-hitp" => VtankCell.Double(v.NoTargetHealth * 100d),
|
||||
"recharge-notarg-stam" => VtankCell.Double(v.NoTargetStamina * 100d),
|
||||
"recharge-notarg-mana" => VtankCell.Double(v.NoTargetMana * 100d),
|
||||
"recharge-helper-hitp" => VtankCell.Double(v.HelperHealth * 100d),
|
||||
"recharge-helper-stam" => VtankCell.Double(v.HelperStamina * 100d),
|
||||
"recharge-helper-mana" => VtankCell.Double(v.HelperMana * 100d),
|
||||
"spelldiffexcessthreshold-hunt" => Num(name, c.HuntSkillExcessOverDifficulty),
|
||||
"spelldiffexcessthreshold-buff" => Num(name, b.SkillExcessOverDifficulty),
|
||||
"arrowheadfletchdiffexcessthreshold" => Num(name, i.ArrowheadFletchDifficultyExcess),
|
||||
"recharge-norm-hitp" => Num(name, v.NormalHealth * 100d),
|
||||
"recharge-norm-stam" => Num(name, v.NormalStamina * 100d),
|
||||
"recharge-norm-mana" => Num(name, v.NormalMana * 100d),
|
||||
"recharge-notarg-hitp" => Num(name, v.NoTargetHealth * 100d),
|
||||
"recharge-notarg-stam" => Num(name, v.NoTargetStamina * 100d),
|
||||
"recharge-notarg-mana" => Num(name, v.NoTargetMana * 100d),
|
||||
"recharge-helper-hitp" => Num(name, v.HelperHealth * 100d),
|
||||
"recharge-helper-stam" => Num(name, v.HelperStamina * 100d),
|
||||
"recharge-helper-mana" => Num(name, v.HelperMana * 100d),
|
||||
"dohelp" => VtankCell.Bool(v.HelpOthers),
|
||||
"attackdistance" => VtankCell.Double(c.MaximumRange / 240d),
|
||||
"attackminimumdistance" => VtankCell.Double(c.MinimumRange / 240d),
|
||||
"approachdistance" => VtankCell.Double(c.ApproachDistance / 240d),
|
||||
"ringdistance" => VtankCell.Double(c.RingDistance / 240d),
|
||||
"corpseapproachrange-max" => VtankCell.Double(i.Loot.CorpseApproachRange / 240d),
|
||||
"corpseapproachrange-min" => VtankCell.Double(i.Loot.CorpseMinimumApproachRange / 240d),
|
||||
"navclosestoprange" => VtankCell.Double(n.MinimumDistanceMeters / 240d),
|
||||
"navfarstoprange" => VtankCell.Double(n.MaximumDistanceMeters / 240d),
|
||||
"useportaldistance" => VtankCell.Double(n.PortalUseDistanceMeters / 240d),
|
||||
"helperdistancehitp" => VtankCell.Double(v.HelperHealthDistance / 240d),
|
||||
"helperdistancestam" => VtankCell.Double(v.HelperStaminaDistance / 240d),
|
||||
"helperdistancemana" => VtankCell.Double(v.HelperManaDistance / 240d),
|
||||
"minimumringtargets" => VtankCell.Int(c.MinimumRingTargets),
|
||||
"defaultmeleeattackheight" => VtankCell.Int((int)c.AttackHeight),
|
||||
"attackdistance" => Num(name, c.MaximumRange / 240d),
|
||||
"attackminimumdistance" => Num(name, c.MinimumRange / 240d),
|
||||
"approachdistance" => Num(name, c.ApproachDistance / 240d),
|
||||
"ringdistance" => Num(name, c.RingDistance / 240d),
|
||||
"corpseapproachrange-max" => Num(name, i.Loot.CorpseApproachRange / 240d),
|
||||
"corpseapproachrange-min" => Num(name, i.Loot.CorpseMinimumApproachRange / 240d),
|
||||
"navclosestoprange" => Num(name, n.MinimumDistanceMeters / 240d),
|
||||
"navfarstoprange" => Num(name, n.MaximumDistanceMeters / 240d),
|
||||
"useportaldistance" => Num(name, n.PortalUseDistanceMeters / 240d),
|
||||
"helperdistancehitp" => Num(name, v.HelperHealthDistance / 240d),
|
||||
"helperdistancestam" => Num(name, v.HelperStaminaDistance / 240d),
|
||||
"helperdistancemana" => Num(name, v.HelperManaDistance / 240d),
|
||||
"minimumringtargets" => Num(name, c.MinimumRingTargets),
|
||||
"defaultmeleeattackheight" => Num(name, (int)c.AttackHeight),
|
||||
"castdispelself" => VtankCell.Bool(v.CastDispelSelf),
|
||||
"usedispelitems" => VtankCell.Bool(v.UseDispelItems),
|
||||
"autocram" => VtankCell.Bool(i.AutoCram),
|
||||
|
|
@ -400,95 +423,95 @@ internal static class VtankSettingsProfileSerializer
|
|||
"lootfellowcorpses" => VtankCell.Bool(i.Loot.LootFellowCorpses),
|
||||
"dojiggle" => VtankCell.Bool(c.DoJiggle),
|
||||
"randomhelperbuffs" => VtankCell.Bool(b.RandomHelperBuffs),
|
||||
"randomhelperintervalseconds" => VtankCell.Double(b.RandomHelperIntervalSeconds),
|
||||
"randomhelperintervalseconds" => Num(name, b.RandomHelperIntervalSeconds),
|
||||
"idlepeacemode" => VtankCell.Bool(c.IdlePeaceMode),
|
||||
"targetlock" => VtankCell.Bool(c.TargetLock),
|
||||
"stopmacroondeath" => VtankCell.Bool(c.StopMacroOnDeath),
|
||||
"usearcs" => VtankCell.Int(c.UseArcs ? 3 : 1),
|
||||
"arcrange" => VtankCell.Double(c.ArcRange / 240d),
|
||||
"targetselectmethod" => VtankCell.Int((int)c.SelectionMethod + 1),
|
||||
"targetselectanglerange" => VtankCell.Double(c.TargetSelectAngleRange / 240d),
|
||||
"usearcs" => Num(name, c.UseArcs ? 3 : 1),
|
||||
"arcrange" => Num(name, c.ArcRange / 240d),
|
||||
"targetselectmethod" => Num(name, (int)c.SelectionMethod + 1),
|
||||
"targetselectanglerange" => Num(name, c.TargetSelectAngleRange / 240d),
|
||||
"idlebufftopoff" => VtankCell.Bool(b.IdleBuffTopoff),
|
||||
"idlebufftopofftimeseconds" => VtankCell.Double(b.IdleBuffTopoffSeconds),
|
||||
"rebufftimeremainingseconds" => VtankCell.Double(b.RebuffWhenUnderSeconds),
|
||||
"idlebufftopofftimeseconds" => Num(name, b.IdleBuffTopoffSeconds),
|
||||
"rebufftimeremainingseconds" => Num(name, b.RebuffWhenUnderSeconds),
|
||||
"refillwornmana" => VtankCell.Bool(i.RefillWornMana),
|
||||
"refillwornmana-item-manapercent" => VtankCell.Int(i.RefillWornManaPercent),
|
||||
"refillwornmana-item-manapercent" => Num(name, i.RefillWornManaPercent),
|
||||
"buffprofile-prots" => VtankCell.String(b.ProtectionElements),
|
||||
"buffprofile-banes" => VtankCell.String(b.BaneElements),
|
||||
"buffprofile_prots" => VtankCell.Int(b.ProtectionProfileMode),
|
||||
"buffprofile_banes" => VtankCell.Int(b.BaneProfileMode),
|
||||
"debuffeachfirst" => VtankCell.Int((int)c.DebuffEachFirst),
|
||||
"buffprofile_prots" => Num(name, b.ProtectionProfileMode),
|
||||
"buffprofile_banes" => Num(name, b.BaneProfileMode),
|
||||
"debuffeachfirst" => Num(name, (int)c.DebuffEachFirst),
|
||||
"autoattackpower" => VtankCell.Bool(c.AutoAttackPower),
|
||||
"lootpriorityboost" => VtankCell.Bool(i.Loot.PriorityBoost),
|
||||
"corpsecachetimeoutminutes" => VtankCell.Double(i.Loot.CorpseCacheTimeoutMinutes),
|
||||
"corpseitemappearancetimeoutseconds" => VtankCell.Double(i.Loot.CorpseItemAppearanceTimeoutSeconds),
|
||||
"corpseitemidtimeoutseconds" => VtankCell.Double(i.Loot.CorpseItemIdentifyTimeoutSeconds),
|
||||
"debuffselectionmethod" => VtankCell.Int((int)c.DebuffSelectionMethod),
|
||||
"manastonelootcount" => VtankCell.Int(i.Loot.ManaStoneLootCount),
|
||||
"manatankminimummana" => VtankCell.Int(i.Loot.ManaTankMinimumMana),
|
||||
"corpsecachetimeoutminutes" => Num(name, i.Loot.CorpseCacheTimeoutMinutes),
|
||||
"corpseitemappearancetimeoutseconds" => Num(name, i.Loot.CorpseItemAppearanceTimeoutSeconds),
|
||||
"corpseitemidtimeoutseconds" => Num(name, i.Loot.CorpseItemIdentifyTimeoutSeconds),
|
||||
"debuffselectionmethod" => Num(name, (int)c.DebuffSelectionMethod),
|
||||
"manastonelootcount" => Num(name, i.Loot.ManaStoneLootCount),
|
||||
"manatankminimummana" => Num(name, i.Loot.ManaTankMinimumMana),
|
||||
"splitpeas" => VtankCell.Bool(i.SplitPeas),
|
||||
"spellcompmin-critical" => VtankCell.Int(i.CriticalComponentMinimum),
|
||||
"spellcompmin-normal" => VtankCell.Int(i.NormalComponentMinimum),
|
||||
"spellcompmin-idle" => VtankCell.Int(i.IdleComponentMinimum),
|
||||
"rechargeboosttimeseconds" => VtankCell.Double(v.RechargeBoostTimeSeconds),
|
||||
"rechargeboostamount" => VtankCell.Int(v.RechargeBoostAmount),
|
||||
"usespecialammo" => VtankCell.Int(c.UseSpecialAmmo),
|
||||
"spellcompmin-critical" => Num(name, i.CriticalComponentMinimum),
|
||||
"spellcompmin-normal" => Num(name, i.NormalComponentMinimum),
|
||||
"spellcompmin-idle" => Num(name, i.IdleComponentMinimum),
|
||||
"rechargeboosttimeseconds" => Num(name, v.RechargeBoostTimeSeconds),
|
||||
"rechargeboostamount" => Num(name, v.RechargeBoostAmount),
|
||||
"usespecialammo" => Num(name, c.UseSpecialAmmo),
|
||||
"opendoors" => VtankCell.Bool(n.OpenDoors),
|
||||
"dooridrange" => VtankCell.Double(n.DoorIdentifyRangeMeters / 240d),
|
||||
"dooropenrange" => VtankCell.Double(n.DoorOpenRangeMeters / 240d),
|
||||
"doorlockpickdiffexcessthreshold" => VtankCell.Int(n.DoorLockpickExcessThreshold),
|
||||
"dooridrange" => Num(name, n.DoorIdentifyRangeMeters / 240d),
|
||||
"dooropenrange" => Num(name, n.DoorOpenRangeMeters / 240d),
|
||||
"doorlockpickdiffexcessthreshold" => Num(name, n.DoorLockpickExcessThreshold),
|
||||
"manachargeswhenoff" => VtankCell.Bool(i.ManaChargesWhenOff),
|
||||
"autofellowmanagement" => VtankCell.Bool(c.AutoFellowManagement),
|
||||
"minimumhealkitsuccesschance" => VtankCell.Int(v.MinimumHealKitSuccessChance),
|
||||
"minimumhealkitsuccesschance" => Num(name, v.MinimumHealKitSuccessChance),
|
||||
"usekitsinmagicmode" => VtankCell.Bool(v.UseKitsInMagicMode),
|
||||
"staminatohealthmultiplier" => VtankCell.Double(v.StaminaToHealthMultiplier),
|
||||
"manatohealthmultiplier" => VtankCell.Double(v.ManaToHealthMultiplier),
|
||||
"staminatohealthmultiplier" => Num(name, v.StaminaToHealthMultiplier),
|
||||
"manatohealthmultiplier" => Num(name, v.ManaToHealthMultiplier),
|
||||
"navpriorityboost" => VtankCell.Bool(n.Priority),
|
||||
"deleteghostmonsters" => VtankCell.Bool(c.DeleteGhostMonsters),
|
||||
"ghostmonsterspellattemptcount" => VtankCell.Int(c.GhostMonsterSpellAttemptCount),
|
||||
"ghostmonsterspellattemptcount" => Num(name, c.GhostMonsterSpellAttemptCount),
|
||||
"whoyougonnacall" => VtankCell.Bool(c.WhoYouGonnaCall),
|
||||
"blacklistmonsterattemptcount" => VtankCell.Int(c.BlacklistMonsterAttemptCount),
|
||||
"blacklistmonstertimeoutseconds" => VtankCell.Double(c.BlacklistMonsterTimeoutSeconds),
|
||||
"blacklistmonsterattemptcount" => Num(name, c.BlacklistMonsterAttemptCount),
|
||||
"blacklistmonstertimeoutseconds" => Num(name, c.BlacklistMonsterTimeoutSeconds),
|
||||
"combinesalvage" => VtankCell.Bool(i.Loot.CombineSalvage),
|
||||
"lootonlyrarecorpses" => VtankCell.Bool(i.Loot.LootOnlyRareCorpses),
|
||||
"deleteghostmonstersbyhptracker" => VtankCell.Bool(c.DeleteGhostMonstersByHealthTracker),
|
||||
"ghostdeletehptrackerseconds" => VtankCell.Double(c.GhostDeleteHealthTrackerSeconds),
|
||||
"ghostdeletehptrackerseconds" => Num(name, c.GhostDeleteHealthTrackerSeconds),
|
||||
"gotopeacemodetousekits" => VtankCell.Bool(v.GoToPeaceModeToUseKits),
|
||||
"userecklessness" => VtankCell.Bool(c.UseRecklessness),
|
||||
"debuffprecastseconds" => VtankCell.Double(c.DebuffPrecastSeconds),
|
||||
"debuffprecastseconds" => Num(name, c.DebuffPrecastSeconds),
|
||||
"clearlevelboostflagoncast" => VtankCell.Bool(v.ClearLevelBoostFlagOnCast),
|
||||
"idlecraftcount_healthkits" => VtankCell.Int(i.IdleHealthKitCount),
|
||||
"idlecraftcount_stamkits" => VtankCell.Int(i.IdleStaminaKitCount),
|
||||
"idlecraftcount_manakits" => VtankCell.Int(i.IdleManaKitCount),
|
||||
"idlecraftcount_healthfood" => VtankCell.Int(i.IdleHealthFoodCount),
|
||||
"idlecraftcount_stamfood" => VtankCell.Int(i.IdleStaminaFoodCount),
|
||||
"idlecraftcount_manafood" => VtankCell.Int(i.IdleManaFoodCount),
|
||||
"buffcastrecast_seconds" => VtankCell.Double(b.BuffCastRecastSeconds),
|
||||
"buffcastrecastreset_seconds" => VtankCell.Double(b.BuffCastRecastResetSeconds),
|
||||
"idlecraftcount_healthkits" => Num(name, i.IdleHealthKitCount),
|
||||
"idlecraftcount_stamkits" => Num(name, i.IdleStaminaKitCount),
|
||||
"idlecraftcount_manakits" => Num(name, i.IdleManaKitCount),
|
||||
"idlecraftcount_healthfood" => Num(name, i.IdleHealthFoodCount),
|
||||
"idlecraftcount_stamfood" => Num(name, i.IdleStaminaFoodCount),
|
||||
"idlecraftcount_manafood" => Num(name, i.IdleManaFoodCount),
|
||||
"buffcastrecast_seconds" => Num(name, b.BuffCastRecastSeconds),
|
||||
"buffcastrecastreset_seconds" => Num(name, b.BuffCastRecastResetSeconds),
|
||||
"blacklistedspellcomps" => VtankCell.String(b.BlacklistedSpellComponents),
|
||||
"droptopeacemoderetrycount" => VtankCell.Int(v.DropToPeaceModeRetryCount),
|
||||
"droptopeacemoderetrycount" => Num(name, v.DropToPeaceModeRetryCount),
|
||||
"followaroundcorners" => VtankCell.Bool(n.FollowAroundCorners),
|
||||
"blacklistcorpseopenattemptcount" => VtankCell.Int(i.Loot.BlacklistCorpseOpenAttemptCount),
|
||||
"blacklistcorpseopentimeoutseconds" => VtankCell.Double(i.Loot.BlacklistCorpseOpenTimeoutSeconds),
|
||||
"blacklistcorpseopenattemptcount" => Num(name, i.Loot.BlacklistCorpseOpenAttemptCount),
|
||||
"blacklistcorpseopentimeoutseconds" => Num(name, i.Loot.BlacklistCorpseOpenTimeoutSeconds),
|
||||
"summonpets" => VtankCell.Bool(c.SummonPets),
|
||||
"petrangemode" => VtankCell.Int((int)c.PetRangeMode),
|
||||
"petcustomrange" => VtankCell.Double(c.PetCustomRange / 240d),
|
||||
"petrefillcount-idle" => VtankCell.Int(c.PetRefillCountIdle),
|
||||
"petrefillcount-normal" => VtankCell.Int(c.PetRefillCountNormal),
|
||||
"corpseopentimeoutseconds" => VtankCell.Double(i.Loot.CorpseOpenTimeoutSeconds),
|
||||
"petmonsterdensity" => VtankCell.Int(c.PetMonsterDensity),
|
||||
"corpselootitemmaxattempts" => VtankCell.Int(i.Loot.CorpseLootItemMaxAttempts),
|
||||
"petrangemode" => Num(name, (int)c.PetRangeMode),
|
||||
"petcustomrange" => Num(name, c.PetCustomRange / 240d),
|
||||
"petrefillcount-idle" => Num(name, c.PetRefillCountIdle),
|
||||
"petrefillcount-normal" => Num(name, c.PetRefillCountNormal),
|
||||
"corpseopentimeoutseconds" => Num(name, i.Loot.CorpseOpenTimeoutSeconds),
|
||||
"petmonsterdensity" => Num(name, c.PetMonsterDensity),
|
||||
"corpselootitemmaxattempts" => Num(name, i.Loot.CorpseLootItemMaxAttempts),
|
||||
"fastcastbuffs" => VtankCell.Bool(b.FastCastBuffs),
|
||||
"usebreakableturnto" => VtankCell.Bool(c.UseBreakableTurnTo),
|
||||
"useprojectileawareness" => VtankCell.Bool(c.UseProjectileAwareness),
|
||||
"collisionprojectileradius" => VtankCell.Float(c.CollisionProjectileRadius),
|
||||
"collisionstepdistance" => VtankCell.Float(c.CollisionStepDistance),
|
||||
"collisionprojectileradius" => Num(name, c.CollisionProjectileRadius),
|
||||
"collisionstepdistance" => Num(name, c.CollisionStepDistance),
|
||||
"showcollisiondebug" => VtankCell.Bool(c.ShowCollisionDebug),
|
||||
"maximumcollisioncheckspertick" => VtankCell.Int(c.MaximumCollisionChecksPerTick),
|
||||
"spellrangefudge" => VtankCell.Float(c.SpellRangeFudge),
|
||||
"buffwithuntrained-item" => VtankCell.Int(b.BuffWithUntrainedItemSkill),
|
||||
"buffwithuntrained-creature" => VtankCell.Int(b.BuffWithUntrainedCreatureSkill),
|
||||
"buffwithuntrained-life" => VtankCell.Int(b.BuffWithUntrainedLifeSkill),
|
||||
"maximumcollisioncheckspertick" => Num(name, c.MaximumCollisionChecksPerTick),
|
||||
"spellrangefudge" => Num(name, c.SpellRangeFudge),
|
||||
"buffwithuntrained-item" => Num(name, b.BuffWithUntrainedItemSkill),
|
||||
"buffwithuntrained-creature" => Num(name, b.BuffWithUntrainedCreatureSkill),
|
||||
"buffwithuntrained-life" => Num(name, b.BuffWithUntrainedLifeSkill),
|
||||
"allowdebufffallback" => VtankCell.Bool(c.AllowDebuffFallback),
|
||||
_ => null, // "enablemeta" (live engine state) and "rechargehandlerset"
|
||||
// (no write path exists in real VTank either, section 2 row 137)
|
||||
|
|
|
|||
|
|
@ -110,13 +110,16 @@ internal sealed class VtankCell
|
|||
public bool AsBool() => string.Equals(ScalarText, "True", StringComparison.Ordinal);
|
||||
|
||||
/// <summary>
|
||||
/// Formats a double/float the way VTank's own build does: .NET's
|
||||
/// invariant-culture round-trippable <c>ToString()</c>. Verified against
|
||||
/// real fixture values (e.g. 5/240 renders as
|
||||
/// <c>0.0208333333333333</c>, matching <c>defaultsettings.usd</c>).
|
||||
/// Formats a double/float the way VTank's own (older .NET Framework)
|
||||
/// build does: <c>Convert.ToString(double, CultureInfo)</c>
|
||||
/// (<c>gy.cs:60-61</c>) used the CLR's classic 15-significant-digit
|
||||
/// general format, not .NET Core's shortest-round-trippable default.
|
||||
/// Verified against every <c>d</c>-tagged value in the real
|
||||
/// <c>defaultsettings.usd</c> fixture (e.g. 5/240 renders as
|
||||
/// <c>0.0208333333333333</c> — 15 significant digits).
|
||||
/// </summary>
|
||||
internal static string FormatDouble(double value) =>
|
||||
value.ToString(CultureInfo.InvariantCulture);
|
||||
value.ToString("G15", CultureInfo.InvariantCulture);
|
||||
|
||||
internal void WriteTo(List<string> lines)
|
||||
{
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
using System.Globalization;
|
||||
using System.Linq;
|
||||
|
||||
namespace AcDream.Plugins.MossTank.Tests;
|
||||
|
||||
|
|
@ -127,6 +128,86 @@ public sealed class VtankSettingsProfileSerializerTests
|
|||
Assert.Equal(["EnableMeta", "RechargeHandlerSet"], unmapped);
|
||||
}
|
||||
|
||||
// Fidelity regression pin for the "Capture() writes the wrong type tag"
|
||||
// defect: 17 tInt settings (Recharge-*, IdleBuffTopoffTimeSeconds, …)
|
||||
// were written as VtankCell "d" and 3 tSingle settings
|
||||
// (CollisionProjectileRadius, CollisionStepDistance, SpellRangeFudge)
|
||||
// were written as "f", while VTank's shipped defaultsettings.usd
|
||||
// declares every one of them "i"/"d" respectively via its own
|
||||
// SettingType column. VTank's gy.e/gy.f unbox helpers
|
||||
// (refs/vtank/decompiled/gy.cs) throw InvalidCastException on that
|
||||
// mismatch. This theory drives the expectation straight off the real
|
||||
// fixture row rather than off a second hand-written table, so it
|
||||
// cannot silently agree with a wrong assumption baked into the
|
||||
// production code twice.
|
||||
public static TheoryData<string> CatalogNamesWithLiveWritePath()
|
||||
{
|
||||
var data = new TheoryData<string>();
|
||||
foreach (string name in VtankOptionCatalog.Names)
|
||||
{
|
||||
if (name is "EnableMeta" or "RechargeHandlerSet")
|
||||
continue; // no live write path (Every137CatalogNameMapsToExactlyOneField).
|
||||
if (name == "UseArcs")
|
||||
continue; // pre-existing bool-collapse simplification; see UntouchedRoundTripIsByteIdentical.
|
||||
data.Add(name);
|
||||
}
|
||||
return data;
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[MemberData(nameof(CatalogNamesWithLiveWritePath))]
|
||||
public void CaptureMatchesDeclaredSettingTypeAndValue(string name)
|
||||
{
|
||||
string text = File.ReadAllText(Path.Combine(FixturesRoot, "defaultsettings.usd"));
|
||||
VtankSettingsProfileSerializer.AllSettings settings = NewSettings();
|
||||
VtankDatabase document = VtankSettingsProfileSerializer.Load(text, settings);
|
||||
VtankTable table = document.Find("Settings")!;
|
||||
int nameColumn = table.ColumnIndex("Setting");
|
||||
int valueColumn = table.ColumnIndex("Value");
|
||||
VtankRow row = table.Rows.Single(
|
||||
r => r.Cells[nameColumn].AsString().Equals(name, StringComparison.Ordinal));
|
||||
VtankCell fixtureCell = row.Cells[valueColumn];
|
||||
|
||||
VtankCell? captured = VtankSettingsProfileSerializer.Capture(name, settings);
|
||||
|
||||
Assert.NotNull(captured);
|
||||
Assert.Equal(fixtureCell.Tag, captured!.Tag);
|
||||
switch (fixtureCell.Tag)
|
||||
{
|
||||
case "b":
|
||||
Assert.Equal(fixtureCell.AsBool(), captured.AsBool());
|
||||
break;
|
||||
case "s":
|
||||
Assert.Equal(fixtureCell.AsString(), captured.AsString());
|
||||
break;
|
||||
case "i":
|
||||
Assert.Equal(fixtureCell.AsInt(), captured.AsInt());
|
||||
break;
|
||||
case "u":
|
||||
Assert.Equal(fixtureCell.AsUInt(), captured.AsUInt());
|
||||
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
|
||||
// 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}");
|
||||
break;
|
||||
default:
|
||||
Assert.Fail($"{name}: unexpected tag '{fixtureCell.Tag}'.");
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void RechargeHandlerSetRoundTripsFromOwnerFixture()
|
||||
{
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue