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
This commit is contained in:
parent
98727d13b3
commit
331f24cb71
16 changed files with 91 additions and 87 deletions
|
|
@ -2,6 +2,7 @@ using Xunit;
|
||||||
|
|
||||||
namespace Server.Tests.Network;
|
namespace Server.Tests.Network;
|
||||||
|
|
||||||
|
[Collection("Sequential Tests")]
|
||||||
public class ClientVersionTests
|
public class ClientVersionTests
|
||||||
{
|
{
|
||||||
[Theory]
|
[Theory]
|
||||||
|
|
|
||||||
|
|
@ -100,12 +100,11 @@ namespace Server.Engines.ConPVP
|
||||||
{
|
{
|
||||||
var until = m_Tournament.SignupStart + m_Tournament.SignupPeriod - Core.Now;
|
var until = m_Tournament.SignupStart + m_Tournament.SignupPeriod - Core.Now;
|
||||||
string text;
|
string text;
|
||||||
var secs = (int)until.TotalSeconds;
|
var secs = (int)Math.Round(until.TotalSeconds);
|
||||||
|
|
||||||
if (secs > 0)
|
if (secs > 0)
|
||||||
{
|
{
|
||||||
var mins = secs / 60;
|
var mins = Math.DivRem(secs, 60, out secs);
|
||||||
secs %= 60;
|
|
||||||
|
|
||||||
if (mins > 0 && secs > 0)
|
if (mins > 0 && secs > 0)
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -797,10 +797,10 @@ public abstract class BaseSpawner : Item, ISpawner
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
var minSeconds = (int)m_MinDelay.TotalSeconds;
|
var min = (long)m_MinDelay.TotalMilliseconds;
|
||||||
var maxSeconds = (int)m_MaxDelay.TotalSeconds;
|
var max = (long)m_MaxDelay.TotalMilliseconds;
|
||||||
|
|
||||||
var delay = TimeSpan.FromSeconds(Utility.RandomMinMax(minSeconds, maxSeconds));
|
var delay = TimeSpan.FromMilliseconds(Utility.RandomMinMax(min, max));
|
||||||
DoTimer(delay);
|
DoTimer(delay);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -31,9 +31,8 @@ namespace Server.Network
|
||||||
|
|
||||||
public static string FormatTS(TimeSpan ts)
|
public static string FormatTS(TimeSpan ts)
|
||||||
{
|
{
|
||||||
var totalSeconds = (int)ts.TotalSeconds;
|
var totalSeconds = (int)Math.Round(ts.TotalSeconds);
|
||||||
var seconds = totalSeconds % 60;
|
var minutes = Math.DivRem(totalSeconds, 60, out var seconds);
|
||||||
var minutes = totalSeconds / 60;
|
|
||||||
|
|
||||||
if (minutes != 0 && seconds != 0)
|
if (minutes != 0 && seconds != 0)
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -212,9 +212,7 @@ public abstract partial class BasePotion : Item, ICraftable, ICommodity
|
||||||
return v;
|
return v;
|
||||||
}
|
}
|
||||||
|
|
||||||
var scalar = 1.0 + 0.01 * EnhancePotions(m);
|
return v * (1.0 + 0.01 * EnhancePotions(m));
|
||||||
|
|
||||||
return TimeSpan.FromSeconds(v.TotalSeconds * scalar);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public static double Scale(Mobile m, double v)
|
public static double Scale(Mobile m, double v)
|
||||||
|
|
|
||||||
|
|
@ -100,7 +100,7 @@ public abstract partial class BaseConflagrationPotion : BasePotion
|
||||||
{
|
{
|
||||||
if (m_Delay.TryGetValue(m, out var timer) && timer.Next > Core.Now)
|
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;
|
return 0;
|
||||||
|
|
|
||||||
|
|
@ -115,7 +115,7 @@ public abstract partial class BaseConfusionBlastPotion : BasePotion
|
||||||
{
|
{
|
||||||
if (_delay.TryGetValue(m, out var timer) && timer.Next > Core.Now)
|
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;
|
return 0;
|
||||||
|
|
|
||||||
|
|
@ -495,36 +495,33 @@ public class BandageContext : Timer
|
||||||
seconds = 9.4 + 0.6 * ((double)(120 - dex) / 10);
|
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;
|
seconds = 3.2 - Math.Sin((double)dex / 130) * 2.5 + resDelay;
|
||||||
}
|
|
||||||
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;
|
|
||||||
}
|
}
|
||||||
else
|
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);
|
var context = GetContext(healer);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -162,7 +162,7 @@ namespace Server
|
||||||
}
|
}
|
||||||
|
|
||||||
var hasArgs = args != null;
|
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]);
|
var writer = new SpanWriter(stackalloc byte[length]);
|
||||||
writer.Write((byte)0xDF); // Packet ID
|
writer.Write((byte)0xDF); // Packet ID
|
||||||
writer.Write((ushort)length);
|
writer.Write((ushort)length);
|
||||||
|
|
@ -174,6 +174,8 @@ namespace Server
|
||||||
writer.Write((short)iconID);
|
writer.Write((short)iconID);
|
||||||
writer.Write((short)0x1); // command (0 = remove, 1 = add, 2 = data)
|
writer.Write((short)0x1); // command (0 = remove, 1 = add, 2 = data)
|
||||||
writer.Write(0);
|
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.Write((short)(ticks / 1000));
|
||||||
writer.Clear(3);
|
writer.Clear(3);
|
||||||
writer.Write(titleCliloc);
|
writer.Write(titleCliloc);
|
||||||
|
|
|
||||||
|
|
@ -181,7 +181,7 @@ namespace Server.Misc
|
||||||
|
|
||||||
if (shouldKick)
|
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));
|
Timer.StartTimer(KickDelay, () => OnKick(state));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -4506,7 +4506,23 @@ namespace Server.Mobiles
|
||||||
|
|
||||||
if (NetState?.BuffIcon == true)
|
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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1451,56 +1451,51 @@ namespace Server.Mobiles
|
||||||
|
|
||||||
public override void OnClick()
|
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 ts = m_Vendor.GetNextBulkOrder(m_From);
|
||||||
var totalHours = (totalSeconds + 3599) / 3600;
|
|
||||||
var totalMinutes = (totalSeconds + 59) / 60;
|
|
||||||
|
|
||||||
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);
|
m_From.SendGump(new LargeBODAcceptGump(m_From, bod));
|
||||||
|
|
||||||
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));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
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
|
else
|
||||||
{
|
{
|
||||||
var oldSpeechHue = m_Vendor.SpeechHue;
|
// An offer may be available in about ~1_hours~ hours.
|
||||||
m_Vendor.SpeechHue = 0x3B2;
|
m_Vendor.SayTo(m_From, 1049039, $"{Math.Ceiling(totalSeconds / 3600):F0}");
|
||||||
|
|
||||||
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;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
m_Vendor.SpeechHue = oldSpeechHue;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -509,7 +509,7 @@ namespace Server.Spells
|
||||||
|
|
||||||
if (scaleDuration)
|
if (scaleDuration)
|
||||||
{
|
{
|
||||||
duration = TimeSpan.FromSeconds(duration.TotalSeconds * scale);
|
duration *= scale;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (scaleStats)
|
if (scaleStats)
|
||||||
|
|
|
||||||
|
|
@ -51,7 +51,7 @@ namespace Server.Items
|
||||||
{
|
{
|
||||||
to.SendLocalizedMessage(
|
to.SendLocalizedMessage(
|
||||||
1072516, // ~1_name~ will expire in ~2_val~ seconds!
|
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;
|
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)
|
public override void Serialize(IGenericWriter writer)
|
||||||
|
|
|
||||||
|
|
@ -330,12 +330,11 @@ namespace Server.Saves
|
||||||
|
|
||||||
if (archiveCreated)
|
if (archiveCreated)
|
||||||
{
|
{
|
||||||
var elapsed = stopWatch.Elapsed.TotalSeconds;
|
|
||||||
logger.Information(
|
logger.Information(
|
||||||
"Created {Period} archive at {Path} ({Elapsed:F2} seconds)",
|
"Created {Period} archive at {Path} ({Elapsed:F2} seconds)",
|
||||||
archivePeriodStrLower,
|
archivePeriodStrLower,
|
||||||
archiveFilePath,
|
archiveFilePath,
|
||||||
elapsed
|
stopWatch.Elapsed.TotalSeconds
|
||||||
);
|
);
|
||||||
|
|
||||||
var i = minimum;
|
var i = minimum;
|
||||||
|
|
|
||||||
|
|
@ -95,9 +95,7 @@ namespace Server.Saves
|
||||||
|
|
||||||
private static void BroadcastWarning()
|
private static void BroadcastWarning()
|
||||||
{
|
{
|
||||||
var s = (int)Warning.TotalSeconds;
|
var m = Math.DivRem((int)Math.Round(Warning.TotalSeconds), 60, out var s);
|
||||||
var m = s / 60;
|
|
||||||
s %= 60;
|
|
||||||
|
|
||||||
if (m > 0 && s > 0)
|
if (m > 0 && s > 0)
|
||||||
{
|
{
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue