From 331f24cb717a8b35d17e1c8556b1874b9971996a Mon Sep 17 00:00:00 2001 From: Kamron Batman <3953314+kamronbatman@users.noreply.github.com> Date: Wed, 22 Nov 2023 18:03:56 -0800 Subject: [PATCH] fix: Fixes TimeSpan rounding issues (#1610) ### Summary TLDR - .NET Framework rounded TimeSpan (and some DateTime) methods to the nearest millisecond. This was actually _expected_ accidentally for parts of RunUO. We are aiming to fix this and clean up some other bad assumptions. Closes #1604 --- .../Tests/Client/ClientVersionTests.cs | 1 + .../ConPVP/Gumps/TournamentBracketGump.cs | 5 +- .../UOContent/Engines/Spawners/BaseSpawner.cs | 6 +- .../Bulletin Boards/BulletinBoardPackets.cs | 5 +- .../Skill Items/Magical/Potions/BasePotion.cs | 4 +- .../BaseConflagrationPotion.cs | 2 +- .../BaseConfusionBlastPotion.cs | 2 +- .../Items/Skill Items/Misc/Bandage.cs | 43 +++++------ Projects/UOContent/Misc/BuffIcons.cs | 4 +- Projects/UOContent/Misc/ClientVerification.cs | 2 +- Projects/UOContent/Mobiles/PlayerMobile.cs | 18 ++++- .../UOContent/Mobiles/Vendors/BaseVendor.cs | 73 +++++++++---------- Projects/UOContent/Spells/Base/SpellHelper.cs | 2 +- .../Spellweaving/Items/TransientItem.cs | 4 +- Projects/UOContent/World Saves/AutoArchive.cs | 3 +- Projects/UOContent/World Saves/AutoSave.cs | 4 +- 16 files changed, 91 insertions(+), 87 deletions(-) diff --git a/Projects/Server.Tests/Tests/Client/ClientVersionTests.cs b/Projects/Server.Tests/Tests/Client/ClientVersionTests.cs index 5f8ddc5b9..6e0f3f039 100644 --- a/Projects/Server.Tests/Tests/Client/ClientVersionTests.cs +++ b/Projects/Server.Tests/Tests/Client/ClientVersionTests.cs @@ -2,6 +2,7 @@ using Xunit; namespace Server.Tests.Network; +[Collection("Sequential Tests")] public class ClientVersionTests { [Theory] diff --git a/Projects/UOContent/Engines/ConPVP/Gumps/TournamentBracketGump.cs b/Projects/UOContent/Engines/ConPVP/Gumps/TournamentBracketGump.cs index 8ff340ed0..018d80b3b 100644 --- a/Projects/UOContent/Engines/ConPVP/Gumps/TournamentBracketGump.cs +++ b/Projects/UOContent/Engines/ConPVP/Gumps/TournamentBracketGump.cs @@ -100,12 +100,11 @@ namespace Server.Engines.ConPVP { var until = m_Tournament.SignupStart + m_Tournament.SignupPeriod - Core.Now; string text; - var secs = (int)until.TotalSeconds; + var secs = (int)Math.Round(until.TotalSeconds); if (secs > 0) { - var mins = secs / 60; - secs %= 60; + var mins = Math.DivRem(secs, 60, out secs); if (mins > 0 && secs > 0) { diff --git a/Projects/UOContent/Engines/Spawners/BaseSpawner.cs b/Projects/UOContent/Engines/Spawners/BaseSpawner.cs index 27a49580b..7f48b19e5 100644 --- a/Projects/UOContent/Engines/Spawners/BaseSpawner.cs +++ b/Projects/UOContent/Engines/Spawners/BaseSpawner.cs @@ -797,10 +797,10 @@ public abstract class BaseSpawner : Item, ISpawner return; } - var minSeconds = (int)m_MinDelay.TotalSeconds; - var maxSeconds = (int)m_MaxDelay.TotalSeconds; + var min = (long)m_MinDelay.TotalMilliseconds; + var max = (long)m_MaxDelay.TotalMilliseconds; - var delay = TimeSpan.FromSeconds(Utility.RandomMinMax(minSeconds, maxSeconds)); + var delay = TimeSpan.FromMilliseconds(Utility.RandomMinMax(min, max)); DoTimer(delay); } diff --git a/Projects/UOContent/Items/Bulletin Boards/BulletinBoardPackets.cs b/Projects/UOContent/Items/Bulletin Boards/BulletinBoardPackets.cs index c6e45d1bd..a28ad7b43 100644 --- a/Projects/UOContent/Items/Bulletin Boards/BulletinBoardPackets.cs +++ b/Projects/UOContent/Items/Bulletin Boards/BulletinBoardPackets.cs @@ -31,9 +31,8 @@ namespace Server.Network public static string FormatTS(TimeSpan ts) { - var totalSeconds = (int)ts.TotalSeconds; - var seconds = totalSeconds % 60; - var minutes = totalSeconds / 60; + var totalSeconds = (int)Math.Round(ts.TotalSeconds); + var minutes = Math.DivRem(totalSeconds, 60, out var seconds); if (minutes != 0 && seconds != 0) { diff --git a/Projects/UOContent/Items/Skill Items/Magical/Potions/BasePotion.cs b/Projects/UOContent/Items/Skill Items/Magical/Potions/BasePotion.cs index 927c76fa2..9080686ef 100644 --- a/Projects/UOContent/Items/Skill Items/Magical/Potions/BasePotion.cs +++ b/Projects/UOContent/Items/Skill Items/Magical/Potions/BasePotion.cs @@ -212,9 +212,7 @@ public abstract partial class BasePotion : Item, ICraftable, ICommodity return v; } - var scalar = 1.0 + 0.01 * EnhancePotions(m); - - return TimeSpan.FromSeconds(v.TotalSeconds * scalar); + return v * (1.0 + 0.01 * EnhancePotions(m)); } public static double Scale(Mobile m, double v) diff --git a/Projects/UOContent/Items/Skill Items/Magical/Potions/Conflagration Potions/BaseConflagrationPotion.cs b/Projects/UOContent/Items/Skill Items/Magical/Potions/Conflagration Potions/BaseConflagrationPotion.cs index fc0e501a5..3004c9e56 100644 --- a/Projects/UOContent/Items/Skill Items/Magical/Potions/Conflagration Potions/BaseConflagrationPotion.cs +++ b/Projects/UOContent/Items/Skill Items/Magical/Potions/Conflagration Potions/BaseConflagrationPotion.cs @@ -100,7 +100,7 @@ public abstract partial class BaseConflagrationPotion : BasePotion { if (m_Delay.TryGetValue(m, out var timer) && timer.Next > Core.Now) { - return (int)(timer.Next - Core.Now).TotalSeconds; + return (int)Math.Round((timer.Next - Core.Now).TotalSeconds); } return 0; diff --git a/Projects/UOContent/Items/Skill Items/Magical/Potions/Confusion Blast Potions/BaseConfusionBlastPotion.cs b/Projects/UOContent/Items/Skill Items/Magical/Potions/Confusion Blast Potions/BaseConfusionBlastPotion.cs index fa8c91c79..77d812fcb 100644 --- a/Projects/UOContent/Items/Skill Items/Magical/Potions/Confusion Blast Potions/BaseConfusionBlastPotion.cs +++ b/Projects/UOContent/Items/Skill Items/Magical/Potions/Confusion Blast Potions/BaseConfusionBlastPotion.cs @@ -115,7 +115,7 @@ public abstract partial class BaseConfusionBlastPotion : BasePotion { if (_delay.TryGetValue(m, out var timer) && timer.Next > Core.Now) { - return (int)(timer.Next - Core.Now).TotalSeconds; + return (int)Math.Round((timer.Next - Core.Now).TotalSeconds); } return 0; diff --git a/Projects/UOContent/Items/Skill Items/Misc/Bandage.cs b/Projects/UOContent/Items/Skill Items/Misc/Bandage.cs index c4cb40af1..d729806f5 100644 --- a/Projects/UOContent/Items/Skill Items/Misc/Bandage.cs +++ b/Projects/UOContent/Items/Skill Items/Misc/Bandage.cs @@ -495,36 +495,33 @@ public class BandageContext : Timer seconds = 9.4 + 0.6 * ((double)(120 - dex) / 10); } } - else + else if (Core.AOS && GetPrimarySkill(patient) == SkillName.Veterinary) { - if (Core.AOS && GetPrimarySkill(patient) == SkillName.Veterinary) + seconds = 2.0; + } + else if (Core.AOS) + { + if (dex < 204) { - seconds = 2.0; - } - else if (Core.AOS) - { - if (dex < 204) - { - seconds = 3.2 - Math.Sin((double)dex / 130) * 2.5 + resDelay; - } - else - { - seconds = 0.7 + resDelay; - } - } - else if (dex >= 100) - { - seconds = 3.0 + resDelay; - } - else if (dex >= 40) - { - seconds = 4.0 + resDelay; + seconds = 3.2 - Math.Sin((double)dex / 130) * 2.5 + resDelay; } else { - seconds = 5.0 + resDelay; + seconds = 0.7 + resDelay; } } + else if (dex >= 100) + { + seconds = 3.0 + resDelay; + } + else if (dex >= 40) + { + seconds = 4.0 + resDelay; + } + else + { + seconds = 5.0 + resDelay; + } var context = GetContext(healer); diff --git a/Projects/UOContent/Misc/BuffIcons.cs b/Projects/UOContent/Misc/BuffIcons.cs index e639ae3cd..8e9bf0130 100644 --- a/Projects/UOContent/Misc/BuffIcons.cs +++ b/Projects/UOContent/Misc/BuffIcons.cs @@ -162,7 +162,7 @@ namespace Server } var hasArgs = args != null; - var length = hasArgs ? args.ToString().Length * 2 + 52 : 46; + var length = hasArgs ? args.ToString()!.Length * 2 + 52 : 46; var writer = new SpanWriter(stackalloc byte[length]); writer.Write((byte)0xDF); // Packet ID writer.Write((ushort)length); @@ -174,6 +174,8 @@ namespace Server writer.Write((short)iconID); writer.Write((short)0x1); // command (0 = remove, 1 = add, 2 = data) writer.Write(0); + + // Truncate to whole seconds - The packet should be delayed by the partial seconds and then sent "on the second" writer.Write((short)(ticks / 1000)); writer.Clear(3); writer.Write(titleCliloc); diff --git a/Projects/UOContent/Misc/ClientVerification.cs b/Projects/UOContent/Misc/ClientVerification.cs index af7555796..615afd5dd 100644 --- a/Projects/UOContent/Misc/ClientVerification.cs +++ b/Projects/UOContent/Misc/ClientVerification.cs @@ -181,7 +181,7 @@ namespace Server.Misc if (shouldKick) { - state.Mobile.SendMessage(0x22, $"You will be disconnected in {KickDelay.TotalSeconds} seconds."); + state.Mobile.SendMessage(0x22, $"You will be disconnected in {KickDelay.TotalSeconds:F0} seconds."); Timer.StartTimer(KickDelay, () => OnKick(state)); return; } diff --git a/Projects/UOContent/Mobiles/PlayerMobile.cs b/Projects/UOContent/Mobiles/PlayerMobile.cs index 64f1cdd47..940b9520f 100644 --- a/Projects/UOContent/Mobiles/PlayerMobile.cs +++ b/Projects/UOContent/Mobiles/PlayerMobile.cs @@ -4506,7 +4506,23 @@ namespace Server.Mobiles if (NetState?.BuffIcon == true) { - b.SendAddBuffPacket(NetState, Serial); + // Synchronize the buff icon as close to _on the second_ as we can. + var msecs = b.TimeLength.Milliseconds; + if (msecs >= 8) + { + Timer.DelayCall(TimeSpan.FromMilliseconds(msecs), (buffInfo, pm) => + { + // They are still online, we still have the buff icon in the table, and it is the same buff icon + if (pm.NetState != null && pm.m_BuffTable.TryGetValue(buffInfo.ID, out var checkBuff) && checkBuff == buffInfo) + { + buffInfo.SendAddBuffPacket(pm.NetState, pm.Serial); + } + }, b, this); + } + else + { + b.SendAddBuffPacket(NetState, Serial); + } } } diff --git a/Projects/UOContent/Mobiles/Vendors/BaseVendor.cs b/Projects/UOContent/Mobiles/Vendors/BaseVendor.cs index 874599cec..e8e9835c5 100644 --- a/Projects/UOContent/Mobiles/Vendors/BaseVendor.cs +++ b/Projects/UOContent/Mobiles/Vendors/BaseVendor.cs @@ -1451,56 +1451,51 @@ namespace Server.Mobiles public override void OnClick() { - if (m_Vendor.SupportsBulkOrders(m_From)) + if (!m_Vendor.SupportsBulkOrders(m_From)) { - var ts = m_Vendor.GetNextBulkOrder(m_From); + return; + } - var totalSeconds = (int)ts.TotalSeconds; - var totalHours = (totalSeconds + 3599) / 3600; - var totalMinutes = (totalSeconds + 59) / 60; + var ts = m_Vendor.GetNextBulkOrder(m_From); - if (Core.SE ? totalMinutes == 0 : totalHours == 0) + var totalSeconds = ts.TotalSeconds; + + // Let them get a bulk order if they are within 1 second. + if (totalSeconds < 1) + { + m_From.SendLocalizedMessage(1049038); // You can get an order now. + + if (Core.AOS) { - m_From.SendLocalizedMessage(1049038); // You can get an order now. + var bulkOrder = m_Vendor.CreateBulkOrder(m_From, true); - if (Core.AOS) + if (bulkOrder is LargeBOD bod) { - var bulkOrder = m_Vendor.CreateBulkOrder(m_From, true); - - if (bulkOrder is LargeBOD bod) - { - m_From.SendGump(new LargeBODAcceptGump(m_From, bod)); - } - else if (bulkOrder is SmallBOD smallBod) - { - m_From.SendGump(new SmallBODAcceptGump(m_From, smallBod)); - } + m_From.SendGump(new LargeBODAcceptGump(m_From, bod)); } + else if (bulkOrder is SmallBOD smallBod) + { + m_From.SendGump(new SmallBODAcceptGump(m_From, smallBod)); + } + } + } + else + { + var oldSpeechHue = m_Vendor.SpeechHue; + m_Vendor.SpeechHue = 0x3B2; + + if (Core.SE) + { + // An offer may be available in about ~1_minutes~ minutes. + m_Vendor.SayTo(m_From, 1072058, $"{Math.Ceiling(totalSeconds / 60):F0}"); } else { - var oldSpeechHue = m_Vendor.SpeechHue; - m_Vendor.SpeechHue = 0x3B2; - - if (Core.SE) - { - m_Vendor.SayTo( - m_From, - 1072058, - totalMinutes.ToString() - ); // An offer may be available in about ~1_minutes~ minutes. - } - else - { - m_Vendor.SayTo( - m_From, - 1049039, - totalHours.ToString() - ); // An offer may be available in about ~1_hours~ hours. - } - - m_Vendor.SpeechHue = oldSpeechHue; + // An offer may be available in about ~1_hours~ hours. + m_Vendor.SayTo(m_From, 1049039, $"{Math.Ceiling(totalSeconds / 3600):F0}"); } + + m_Vendor.SpeechHue = oldSpeechHue; } } } diff --git a/Projects/UOContent/Spells/Base/SpellHelper.cs b/Projects/UOContent/Spells/Base/SpellHelper.cs index 5f80855fa..3a813dafe 100644 --- a/Projects/UOContent/Spells/Base/SpellHelper.cs +++ b/Projects/UOContent/Spells/Base/SpellHelper.cs @@ -509,7 +509,7 @@ namespace Server.Spells if (scaleDuration) { - duration = TimeSpan.FromSeconds(duration.TotalSeconds * scale); + duration *= scale; } if (scaleStats) diff --git a/Projects/UOContent/Spells/Spellweaving/Items/TransientItem.cs b/Projects/UOContent/Spells/Spellweaving/Items/TransientItem.cs index d4d610039..300bcf106 100644 --- a/Projects/UOContent/Spells/Spellweaving/Items/TransientItem.cs +++ b/Projects/UOContent/Spells/Spellweaving/Items/TransientItem.cs @@ -51,7 +51,7 @@ namespace Server.Items { to.SendLocalizedMessage( 1072516, // ~1_name~ will expire in ~2_val~ seconds! - $"{Name ?? $"#{LabelNumber}"}\t{(int)LifeSpan.TotalSeconds}" + $"{Name ?? $"#{LabelNumber}"}\t{LifeSpan.TotalSeconds:F0}" ); } @@ -79,7 +79,7 @@ namespace Server.Items var remaining = CreationTime + LifeSpan - Core.Now; - list.Add(1072517, $"{(int)remaining.TotalSeconds}"); // Lifespan: ~1_val~ seconds + list.Add(1072517, $"{remaining.TotalSeconds:F0}"); // Lifespan: ~1_val~ seconds } public override void Serialize(IGenericWriter writer) diff --git a/Projects/UOContent/World Saves/AutoArchive.cs b/Projects/UOContent/World Saves/AutoArchive.cs index 0558c581b..1ac2701b1 100755 --- a/Projects/UOContent/World Saves/AutoArchive.cs +++ b/Projects/UOContent/World Saves/AutoArchive.cs @@ -330,12 +330,11 @@ namespace Server.Saves if (archiveCreated) { - var elapsed = stopWatch.Elapsed.TotalSeconds; logger.Information( "Created {Period} archive at {Path} ({Elapsed:F2} seconds)", archivePeriodStrLower, archiveFilePath, - elapsed + stopWatch.Elapsed.TotalSeconds ); var i = minimum; diff --git a/Projects/UOContent/World Saves/AutoSave.cs b/Projects/UOContent/World Saves/AutoSave.cs index 71a6ec891..f113a9068 100644 --- a/Projects/UOContent/World Saves/AutoSave.cs +++ b/Projects/UOContent/World Saves/AutoSave.cs @@ -95,9 +95,7 @@ namespace Server.Saves private static void BroadcastWarning() { - var s = (int)Warning.TotalSeconds; - var m = s / 60; - s %= 60; + var m = Math.DivRem((int)Math.Round(Warning.TotalSeconds), 60, out var s); if (m > 0 && s > 0) {