From 0aa23af141acf3c6ffdda2557a7bbb14966a42d8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=82ngelo=20Tadeucci?= Date: Mon, 13 Oct 2025 13:04:02 -0300 Subject: [PATCH 1/5] Add LongExtensions.Truncate32 and refactor tick handling --- Maple2.Model/Game/User/SkillCooldown.cs | 3 +- Maple2.Server.Game/Manager/BuffManager.cs | 6 +- Maple2.Server.Game/Manager/DungeonManager.cs | 2 +- .../Field/FieldManager/FieldManager.State.cs | 6 +- .../Field/FieldManager/FieldManager.cs | 7 + .../Manager/Field/FieldManager/IField.cs | 1 + Maple2.Server.Game/Manager/FishingManager.cs | 2 +- Maple2.Server.Game/Model/Field/Buff.cs | 5 +- Maple2.Server.Game/Packets/BreakablePacket.cs | 9 +- .../Packets/InstrumentPacket.cs | 2 +- .../Packets/RegionSkillPacket.cs | 3 +- Maple2.Server.Game/Session/GameSession.cs | 37 +-- .../Tools/LongExtensionsTests.cs | 229 ++++++++++++++++++ Maple2.Tools/Extensions/LongExtensions.cs | 14 ++ 14 files changed, 293 insertions(+), 33 deletions(-) create mode 100644 Maple2.Server.Tests/Tools/LongExtensionsTests.cs create mode 100644 Maple2.Tools/Extensions/LongExtensions.cs diff --git a/Maple2.Model/Game/User/SkillCooldown.cs b/Maple2.Model/Game/User/SkillCooldown.cs index eb8e90efe..7d49e98b4 100644 --- a/Maple2.Model/Game/User/SkillCooldown.cs +++ b/Maple2.Model/Game/User/SkillCooldown.cs @@ -1,5 +1,6 @@ using Maple2.PacketLib.Tools; using Maple2.Tools; +using Maple2.Tools.Extensions; namespace Maple2.Model.Game; @@ -20,7 +21,7 @@ public SkillCooldown(int skillId, short level) { public void WriteTo(IByteWriter writer) { writer.WriteInt(SkillId); writer.WriteInt(GroupId); - writer.WriteInt((int) EndTick); + writer.WriteInt(EndTick.Truncate32()); writer.WriteInt(Charges); } } diff --git a/Maple2.Server.Game/Manager/BuffManager.cs b/Maple2.Server.Game/Manager/BuffManager.cs index dca80992e..83cc77e16 100644 --- a/Maple2.Server.Game/Manager/BuffManager.cs +++ b/Maple2.Server.Game/Manager/BuffManager.cs @@ -498,19 +498,19 @@ public void RemoveItemBuffs(Item item) { Remove(buffsToRemove.ToArray()); } - public void SetCacheBuffs(IList buffs, long currentTick) { + public void SetCacheBuffs(IList cacheBuffs, long currentTick) { if (Actor is not FieldPlayer player) { return; } - foreach (BuffInfo info in buffs) { + foreach (BuffInfo info in cacheBuffs) { if (!player.Field.SkillMetadata.TryGetEffect(info.Id, (short) info.Level, out AdditionalEffectMetadata? additionalEffect)) { logger.Error("Invalid buff: {SkillId},{Level}", info.Id, info.Level); continue; } if (additionalEffect.Property.UseInGameTime || info.MsRemaining > 0) { - AddBuff(Actor, Actor, info.Id, (short) info.Level, currentTick, info.MsRemaining); + AddBuff(Actor, Actor, info.Id, (short) info.Level, currentTick, info.Stacks, info.MsRemaining); } } } diff --git a/Maple2.Server.Game/Manager/DungeonManager.cs b/Maple2.Server.Game/Manager/DungeonManager.cs index 0e7ddafc1..71a725491 100644 --- a/Maple2.Server.Game/Manager/DungeonManager.cs +++ b/Maple2.Server.Game/Manager/DungeonManager.cs @@ -322,7 +322,7 @@ public void EnterInitField() { } if (firstField.DungeonRoomRecord.StartTick == 0) { - firstField.DungeonRoomRecord.StartTick = Environment.TickCount; + firstField.DungeonRoomRecord.StartTick = Lobby.FieldTick; } session.Send(session.PrepareField(firstField.MapId, roomId: firstField.RoomId) ? FieldEnterPacket.Request(session.Player) : FieldEnterPacket.Error(MigrationError.s_move_err_default)); diff --git a/Maple2.Server.Game/Manager/Field/FieldManager/FieldManager.State.cs b/Maple2.Server.Game/Manager/Field/FieldManager/FieldManager.State.cs index 93ba1eb9f..3e139af15 100644 --- a/Maple2.Server.Game/Manager/Field/FieldManager/FieldManager.State.cs +++ b/Maple2.Server.Game/Manager/Field/FieldManager/FieldManager.State.cs @@ -184,8 +184,8 @@ public FieldPortal SpawnPortal(QuestSummonPortal metadata, FieldNpc npc, FieldPl var fieldPortal = new FieldQuestPortal(owner, this, NextLocalId(), portal) { Position = portal.Position, Rotation = portal.Rotation, - EndTick = (int) (FieldTick + TimeSpan.FromSeconds(Constant.QuestPortalKeepTime).TotalMilliseconds), - StartTick = (int) FieldTick, + EndTick = FieldTickInt + (int) TimeSpan.FromSeconds(Constant.QuestPortalKeepTime).TotalMilliseconds, + StartTick = FieldTickInt, Model = Constant.QuestPortalKeepNif, }; fieldPortals[fieldPortal.ObjectId] = fieldPortal; @@ -625,7 +625,7 @@ private void SetBonusMapPortal(IList bonusMaps, Ms2RegionSpawn spaw Continent.Kritias => "Eff_ks_magichole_portal_A01", _ => "Eff_event_portal_A01", }; - fieldPortal.EndTick = (int) (FieldTick + TimeSpan.FromSeconds(30).TotalMilliseconds); + fieldPortal.EndTick = FieldTickInt + (int) TimeSpan.FromSeconds(30).TotalMilliseconds; Broadcast(PortalPacket.Add(fieldPortal)); Scheduler.Schedule(() => SetBonusMapPortal(bonusMaps, spawn), delay); } diff --git a/Maple2.Server.Game/Manager/Field/FieldManager/FieldManager.cs b/Maple2.Server.Game/Manager/Field/FieldManager/FieldManager.cs index f1f8de29c..0a9d78b63 100644 --- a/Maple2.Server.Game/Manager/Field/FieldManager/FieldManager.cs +++ b/Maple2.Server.Game/Manager/Field/FieldManager/FieldManager.cs @@ -261,6 +261,13 @@ public virtual void Init() { // Use this to keep systems in sync. Do not use Environment.TickCount directly public long FieldTick { get; private set; } + /// + /// FieldTick truncated to 32-bit int. Use this when sending tick values to the client or when + /// int tick values are required (e.g., TimeSync packets, performance stage). + /// This properly handles overflow to match Environment.TickCount behavior. + /// + public int FieldTickInt => FieldTick.Truncate32(); + public void QueuePacket(FieldPacketHandler handler, GameSession session, ByteReader reader) { lock (queuedPackets) { queuedPackets.Add((handler, session, reader)); diff --git a/Maple2.Server.Game/Manager/Field/FieldManager/IField.cs b/Maple2.Server.Game/Manager/Field/FieldManager/IField.cs index 96a3621fa..f5d6ec586 100644 --- a/Maple2.Server.Game/Manager/Field/FieldManager/IField.cs +++ b/Maple2.Server.Game/Manager/Field/FieldManager/IField.cs @@ -56,6 +56,7 @@ public interface IField : IDisposable { public int MapId { get; init; } public long FieldTick { get; } + public int FieldTickInt { get; } public virtual void Init() { } public void AddSkill(SkillMetadata metadata, int interval, in Vector3 position, in Vector3 rotation = default, int triggerId = 0); diff --git a/Maple2.Server.Game/Manager/FishingManager.cs b/Maple2.Server.Game/Manager/FishingManager.cs index 967adecc6..9f2afe5d9 100644 --- a/Maple2.Server.Game/Manager/FishingManager.cs +++ b/Maple2.Server.Game/Manager/FishingManager.cs @@ -254,7 +254,7 @@ public FishingError Start(Vector3 position) { fishingTick = Random.Shared.Next(fishingTick + 1, fishingTick * 2); // If tick is over bore duration, it will fail } - session.Send(FishingPacket.Start(((int) session.Field.FieldTick) + fishingTick, fishFightGame)); + session.Send(FishingPacket.Start(session.Field.FieldTickInt + fishingTick, fishFightGame)); return FishingError.none; } diff --git a/Maple2.Server.Game/Model/Field/Buff.cs b/Maple2.Server.Game/Model/Field/Buff.cs index 514a824b4..ddc7080d6 100644 --- a/Maple2.Server.Game/Model/Field/Buff.cs +++ b/Maple2.Server.Game/Model/Field/Buff.cs @@ -7,6 +7,7 @@ using Maple2.Server.Game.Packets; using Maple2.Server.Game.Util; using Maple2.Tools; +using Maple2.Tools.Extensions; using Serilog; namespace Maple2.Server.Game.Model; @@ -318,8 +319,8 @@ public void WriteTo(IByteWriter writer) { } public void WriteAdditionalEffect(IByteWriter writer) { - writer.WriteInt((int) StartTick); - writer.WriteInt((int) EndTick); + writer.WriteInt(StartTick.Truncate32()); + writer.WriteInt(EndTick.Truncate32()); writer.WriteInt(Id); writer.WriteShort(Level); writer.WriteInt(Stacks); diff --git a/Maple2.Server.Game/Packets/BreakablePacket.cs b/Maple2.Server.Game/Packets/BreakablePacket.cs index dd33b2bf3..5d26e9931 100644 --- a/Maple2.Server.Game/Packets/BreakablePacket.cs +++ b/Maple2.Server.Game/Packets/BreakablePacket.cs @@ -3,6 +3,7 @@ using Maple2.Server.Core.Constants; using Maple2.Server.Core.Packets; using Maple2.Server.Game.Model; +using Maple2.Tools.Extensions; namespace Maple2.Server.Game.Packets; @@ -23,8 +24,8 @@ public static ByteWriter Update(ICollection breakables) { pWriter.Write(breakable.State); pWriter.WriteBool(breakable.Visible); if (breakable.BaseTick > 0) { - pWriter.WriteInt((int) (currentTick - breakable.BaseTick)); - pWriter.WriteInt((int) breakable.BaseTick); + pWriter.WriteInt((currentTick - breakable.BaseTick).Truncate32()); + pWriter.WriteInt(breakable.BaseTick.Truncate32()); } else { pWriter.WriteInt(); pWriter.WriteInt(); @@ -41,8 +42,8 @@ public static ByteWriter Update(FieldBreakable breakable) { pWriter.Write(breakable.State); pWriter.WriteBool(breakable.Visible); if (breakable.BaseTick > 0) { - pWriter.WriteInt((int) (Environment.TickCount64 - breakable.BaseTick)); - pWriter.WriteInt((int) breakable.BaseTick); + pWriter.WriteInt((Environment.TickCount64 - breakable.BaseTick).Truncate32()); + pWriter.WriteInt(breakable.BaseTick.Truncate32()); } else { pWriter.WriteInt(); pWriter.WriteInt(); diff --git a/Maple2.Server.Game/Packets/InstrumentPacket.cs b/Maple2.Server.Game/Packets/InstrumentPacket.cs index cddc61d7c..96fc3318d 100644 --- a/Maple2.Server.Game/Packets/InstrumentPacket.cs +++ b/Maple2.Server.Game/Packets/InstrumentPacket.cs @@ -62,7 +62,7 @@ public static ByteWriter StartScore(FieldInstrument instrument, Item score) { pWriter.WriteInt(instrument.ObjectId); pWriter.WriteInt(instrument.OwnerId); pWriter.Write(instrument.Position); - pWriter.WriteInt((int) instrument.StartTick); + pWriter.WriteInt(instrument.StartTick.Truncate32()); pWriter.WriteInt(instrument.Value.MidiId); pWriter.WriteInt(instrument.Value.PercussionId); pWriter.WriteBool(instrument.Ensemble); diff --git a/Maple2.Server.Game/Packets/RegionSkillPacket.cs b/Maple2.Server.Game/Packets/RegionSkillPacket.cs index 8923092cf..b17583742 100644 --- a/Maple2.Server.Game/Packets/RegionSkillPacket.cs +++ b/Maple2.Server.Game/Packets/RegionSkillPacket.cs @@ -3,6 +3,7 @@ using Maple2.Server.Core.Constants; using Maple2.Server.Core.Packets; using Maple2.Server.Game.Model; +using Maple2.Tools.Extensions; namespace Maple2.Server.Game.Packets; @@ -17,7 +18,7 @@ public static ByteWriter Add(FieldSkill fieldSkill) { pWriter.Write(Command.Add); pWriter.WriteInt(fieldSkill.ObjectId); pWriter.WriteInt(fieldSkill.Caster.ObjectId); - pWriter.WriteInt((int) fieldSkill.NextTick); + pWriter.WriteInt(fieldSkill.NextTick.Truncate32()); pWriter.WriteByte((byte) fieldSkill.Points.Length); foreach (Vector3 point in fieldSkill.Points) { pWriter.Write(point); diff --git a/Maple2.Server.Game/Session/GameSession.cs b/Maple2.Server.Game/Session/GameSession.cs index 93bece767..093e145af 100644 --- a/Maple2.Server.Game/Session/GameSession.cs +++ b/Maple2.Server.Game/Session/GameSession.cs @@ -809,28 +809,33 @@ void SaveCacheConfig() { long stopTime = DateTime.Now.ToEpochSeconds(); long fieldTick = fieldTickSnapshot; + try { PlayerConfigResponse _ = World.PlayerConfig(new PlayerConfigRequest { Save = new PlayerConfigRequest.Types.Save { Buffs = { - buffs.Select(buff => new BuffInfo { - Id = buff.Id, - Level = buff.Level, - MsRemaining = (int) (buff.EndTick - fieldTick), - Stacks = buff.Stacks, - Enabled = buff.Enabled, - StopTime = stopTime, - }), + buffs + .Where(buff => buff.EndTick - fieldTick > 0) + .Select(buff => new BuffInfo { + Id = buff.Id, + Level = buff.Level, + MsRemaining = (int) (buff.EndTick - fieldTick), + Stacks = buff.Stacks, + Enabled = buff.Enabled, + StopTime = stopTime, + }), }, SkillCooldowns = { - skillCooldowns.Select(cooldown => new SkillCooldownInfo { - SkillId = cooldown.SkillId, - SkillLevel = cooldown.Level, - GroupId = cooldown.GroupId, - MsRemaining = (int) (cooldown.EndTick - fieldTick), - StopTime = stopTime, - Charges = cooldown.Charges, - }), + skillCooldowns + .Where(cooldown => cooldown.EndTick - fieldTick > 0) + .Select(cooldown => new SkillCooldownInfo { + SkillId = cooldown.SkillId, + SkillLevel = cooldown.Level, + GroupId = cooldown.GroupId, + MsRemaining = (int) (cooldown.EndTick - fieldTick), + StopTime = stopTime, + Charges = cooldown.Charges, + }), }, DeathInfo = new DeathInfo { Count = Config.DeathCount, diff --git a/Maple2.Server.Tests/Tools/LongExtensionsTests.cs b/Maple2.Server.Tests/Tools/LongExtensionsTests.cs new file mode 100644 index 000000000..73b50a18f --- /dev/null +++ b/Maple2.Server.Tests/Tools/LongExtensionsTests.cs @@ -0,0 +1,229 @@ +using Maple2.Tools.Extensions; +using System; + +namespace Maple2.Server.Tests.Tools; + +public class LongExtensionsTests { + [Test] + public void Truncate32_WithZero_ReturnsZero() { + long value = 0L; + int result = value.Truncate32(); + Assert.That(result, Is.EqualTo(0)); + } + + [Test] + public void Truncate32_WithPositiveValue_WithinIntRange_ReturnsSameValue() { + long value = 123456789L; + int result = value.Truncate32(); + Assert.That(result, Is.EqualTo(123456789)); + } + + [Test] + public void Truncate32_WithNegativeValue_WithinIntRange_ReturnsSameValue() { + long value = -123456789L; + int result = value.Truncate32(); + Assert.That(result, Is.EqualTo(-123456789)); + } + + [Test] + public void Truncate32_WithMaxInt_ReturnsMaxInt() { + long value = int.MaxValue; + int result = value.Truncate32(); + Assert.That(result, Is.EqualTo(int.MaxValue)); + } + + [Test] + public void Truncate32_WithMinInt_ReturnsMinInt() { + long value = int.MinValue; + int result = value.Truncate32(); + Assert.That(result, Is.EqualTo(int.MinValue)); + } + + [Test] + public void Truncate32_WithValueJustAboveMaxInt_Truncates() { + // int.MaxValue + 1 should wrap around + long value = (long) int.MaxValue + 1; + int result = value.Truncate32(); + // When we add 1 to MaxInt (0x7FFFFFFF), we get 0x80000000 + // Which is MinInt when interpreted as signed int + Assert.That(result, Is.EqualTo(int.MinValue)); + } + + [Test] + public void Truncate32_WithValueJustBelowMinInt_Truncates() { + // int.MinValue - 1 should wrap around + long value = (long) int.MinValue - 1; + int result = value.Truncate32(); + // When we subtract 1 from MinInt (0x80000000), we get 0x7FFFFFFF + // Which is MaxInt when truncated to 32 bits + Assert.That(result, Is.EqualTo(int.MaxValue)); + } + + [Test] + public void Truncate32_WithLargePositiveValue_TruncatesCorrectly() { + // Simulating a very large TickCount64 value + long value = 0x123456789ABCDEF0L; + int result = value.Truncate32(); + // Should only keep the lower 32 bits: 0x9ABCDEF0 + // When interpreted as signed int, this is negative + Assert.That(result, Is.EqualTo(unchecked((int) 0x9ABCDEF0))); + } + + [Test] + public void Truncate32_MatchesEnvironmentTickCountBehavior() { + // Test that Truncate32 matches what happens when Environment.TickCount wraps + // At exactly the boundary where int overflows + long value = 0x0000000100000000L; // Just past the 32-bit boundary + int result = value.Truncate32(); + Assert.That(result, Is.EqualTo(0)); + } + + [Test] + public void Truncate32_WithMultipleOf32BitBoundary_ReturnsZero() { + long value = 0x0000000200000000L; // 2 * 2^32 + int result = value.Truncate32(); + Assert.That(result, Is.EqualTo(0)); + } + + [Test] + public void Truncate32_WithTickCountScenario_30Minutes() { + // Simulate a tick count representing 30 minutes (1,800,000 milliseconds) + long startTick = 9000000000L; // A large starting tick + int duration = 1800000; // 30 minutes in milliseconds + long endTick = startTick + duration; + + int startTickInt = startTick.Truncate32(); + int endTickInt = endTick.Truncate32(); + + // The difference should be the duration (assuming no wrap-around) + long difference = (long) endTickInt - startTickInt; + if (difference < 0) { + // Handle wrap-around + difference += 0x100000000L; + } + + Assert.That(difference, Is.EqualTo(duration)); + } + + [Test] + public void Truncate32_WithOverflowScenario_HandlesCorrectly() { + // Test the scenario that was causing issues in production + // When FieldTick is very large and causes int overflow + long largeFieldTick = long.MaxValue - 1000000L; + int truncated = largeFieldTick.Truncate32(); + + // Should not throw an exception and should produce a valid int + Assert.That(truncated, Is.InRange(int.MinValue, int.MaxValue)); + } + + [Test] + public void Truncate32_ConsistentWithBitwiseAnd() { + // Verify that our implementation matches the expected bitwise AND behavior + long[] testValues = [ + 0L, + 1L, + -1L, + int.MaxValue, + int.MinValue, + (long) int.MaxValue + 1, + (long) int.MinValue - 1, + 0xFFFFFFFF, + 0x100000000L, + 0x123456789ABCDEF0L, + long.MaxValue, + long.MinValue, + ]; + + foreach (long value in testValues) { + int result = value.Truncate32(); + int expected = (int) (0xFFFFFFFF & value); + Assert.That(result, Is.EqualTo(expected), + $"Failed for value: {value} (0x{value:X})"); + } + } + + [Test] + public void Truncate32_SimulatesTickCountWrapAround() { + // Simulate what happens when Environment.TickCount wraps around + // This happens approximately every 49.7 days + long tickBeforeWrap = 0x00000000FFFFFFFEL; // Just before wrap + long tickAfterWrap = 0x0000000100000001L; // Just after wrap + + int beforeInt = tickBeforeWrap.Truncate32(); + int afterInt = tickAfterWrap.Truncate32(); + + // Before wrap: 0xFFFFFFFE as signed int is -2 + Assert.That(beforeInt, Is.EqualTo(-2)); + // After wrap should wrap to a small positive number + Assert.That(afterInt, Is.EqualTo(1)); + } + + [TestCase(0L, 0)] + [TestCase(1L, 1)] + [TestCase(-1L, -1)] + [TestCase(2147483647L, 2147483647)] // int.MaxValue + [TestCase(-2147483648L, -2147483648)] // int.MinValue + [TestCase(2147483648L, -2147483648)] // int.MaxValue + 1 wraps to int.MinValue + [TestCase(4294967295L, -1)] // uint.MaxValue becomes -1 as signed int + [TestCase(4294967296L, 0)] // 2^32 wraps to 0 + public void Truncate32_WithVariousValues_ReturnsExpectedResult(long input, int expected) { + int result = input.Truncate32(); + Assert.That(result, Is.EqualTo(expected)); + } + + [Test] + public void Truncate32_DifferentFromDirectCast_InCheckedContext() { + // In a checked context, direct casting throws OverflowException + // but Truncate32 handles it gracefully + long largeValue = 0x123456789ABCDEF0L; + + // This should NOT throw + Assert.DoesNotThrow(() => { + int result = largeValue.Truncate32(); + Assert.That(result, Is.EqualTo(unchecked((int) 0x9ABCDEF0))); + }); + + // But a direct cast in checked context WOULD throw + Assert.Throws(() => { + checked { + int result = (int) largeValue; + } + }); + } + + [Test] + public void Truncate32_DifferentFromDirectCast_WithLargePositiveValue() { + // Direct cast truncates to the most significant bits that fit + // Truncate32 explicitly takes the lower 32 bits + long value = 0x0000000280000001L; // Beyond int.MaxValue + + int truncated = value.Truncate32(); + int directCast = unchecked((int) value); // unchecked to avoid exception + + // Both should be the same in unchecked context + // This test documents that behavior + Assert.That(truncated, Is.EqualTo(directCast)); + Assert.That(truncated, Is.EqualTo(-2147483647)); // 0x80000001 as signed int + } + + [Test] + public void Truncate32_SafeForTickCountConversion_WhileDirectCastIsNot() { + // This demonstrates the practical reason for Truncate32: + // Environment.TickCount64 can be very large and would cause overflow with direct cast + long tickCount64 = long.MaxValue - 1000L; // A realistic large TickCount64 value + + // Truncate32 works safely + Assert.DoesNotThrow(() => { + int tick32 = tickCount64.Truncate32(); + // Should get lower 32 bits + Assert.That(tick32, Is.EqualTo(unchecked((int) 0xFFFFFC17))); + }); + + // Direct cast in checked context would throw + Assert.Throws(() => { + checked { + int tick32 = (int) tickCount64; + } + }); + } +} diff --git a/Maple2.Tools/Extensions/LongExtensions.cs b/Maple2.Tools/Extensions/LongExtensions.cs new file mode 100644 index 000000000..0b6e75acd --- /dev/null +++ b/Maple2.Tools/Extensions/LongExtensions.cs @@ -0,0 +1,14 @@ +namespace Maple2.Tools.Extensions; + +public static class LongExtensions { + /// + /// Truncates a long value to a 32-bit int, handling overflow properly. + /// This is useful for converting Environment.TickCount64 to match Environment.TickCount behavior. + /// + /// The long value to truncate + /// The truncated int value + public static int Truncate32(this long value) { + return (int)(0xFFFFFFFF & value); + } +} + From b665694b2411cb3c41b7db4d5ab5c5f21343f57d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=82ngelo=20Tadeucci?= Date: Mon, 13 Oct 2025 13:10:03 -0300 Subject: [PATCH 2/5] format --- Maple2.Tools/Extensions/LongExtensions.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Maple2.Tools/Extensions/LongExtensions.cs b/Maple2.Tools/Extensions/LongExtensions.cs index 0b6e75acd..e619a7c63 100644 --- a/Maple2.Tools/Extensions/LongExtensions.cs +++ b/Maple2.Tools/Extensions/LongExtensions.cs @@ -8,7 +8,7 @@ public static class LongExtensions { /// The long value to truncate /// The truncated int value public static int Truncate32(this long value) { - return (int)(0xFFFFFFFF & value); + return (int) (0xFFFFFFFF & value); } } From c0dca8fe23cb1c01b6fc1419a527870b24f3c42d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=82ngelo=20Tadeucci?= Date: Tue, 14 Oct 2025 09:25:49 -0300 Subject: [PATCH 3/5] fix(field): use 64-bit tick math then truncate to 32-bit for portal EndTick --- .../Manager/Field/FieldManager/FieldManager.State.cs | 4 ++-- Maple2.Server.Game/Manager/Field/PerformanceStageManager.cs | 1 + 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/Maple2.Server.Game/Manager/Field/FieldManager/FieldManager.State.cs b/Maple2.Server.Game/Manager/Field/FieldManager/FieldManager.State.cs index 3e139af15..bbd8a3e97 100644 --- a/Maple2.Server.Game/Manager/Field/FieldManager/FieldManager.State.cs +++ b/Maple2.Server.Game/Manager/Field/FieldManager/FieldManager.State.cs @@ -184,7 +184,7 @@ public FieldPortal SpawnPortal(QuestSummonPortal metadata, FieldNpc npc, FieldPl var fieldPortal = new FieldQuestPortal(owner, this, NextLocalId(), portal) { Position = portal.Position, Rotation = portal.Rotation, - EndTick = FieldTickInt + (int) TimeSpan.FromSeconds(Constant.QuestPortalKeepTime).TotalMilliseconds, + EndTick = (FieldTick + (long) TimeSpan.FromSeconds(Constant.QuestPortalKeepTime).TotalMilliseconds).Truncate32(), StartTick = FieldTickInt, Model = Constant.QuestPortalKeepNif, }; @@ -625,7 +625,7 @@ private void SetBonusMapPortal(IList bonusMaps, Ms2RegionSpawn spaw Continent.Kritias => "Eff_ks_magichole_portal_A01", _ => "Eff_event_portal_A01", }; - fieldPortal.EndTick = FieldTickInt + (int) TimeSpan.FromSeconds(30).TotalMilliseconds; + fieldPortal.EndTick = (FieldTick + (long) TimeSpan.FromSeconds(30).TotalMilliseconds).Truncate32(); Broadcast(PortalPacket.Add(fieldPortal)); Scheduler.Schedule(() => SetBonusMapPortal(bonusMaps, spawn), delay); } diff --git a/Maple2.Server.Game/Manager/Field/PerformanceStageManager.cs b/Maple2.Server.Game/Manager/Field/PerformanceStageManager.cs index c9ce34f19..4aad98a25 100644 --- a/Maple2.Server.Game/Manager/Field/PerformanceStageManager.cs +++ b/Maple2.Server.Game/Manager/Field/PerformanceStageManager.cs @@ -1,6 +1,7 @@ using Maple2.Model.Game; using Maple2.Server.Game.Model; using Maple2.Server.Game.Session; +using Maple2.Tools.Extensions; using Serilog; namespace Maple2.Server.Game.Manager.Field; From deed140f81f615fd80e84752ffe040f5697c6568 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=82ngelo=20Tadeucci?= Date: Tue, 14 Oct 2025 21:16:25 -0300 Subject: [PATCH 4/5] . --- Maple2.Server.Game/Manager/FishingManager.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Maple2.Server.Game/Manager/FishingManager.cs b/Maple2.Server.Game/Manager/FishingManager.cs index 9f2afe5d9..aebd61425 100644 --- a/Maple2.Server.Game/Manager/FishingManager.cs +++ b/Maple2.Server.Game/Manager/FishingManager.cs @@ -254,7 +254,7 @@ public FishingError Start(Vector3 position) { fishingTick = Random.Shared.Next(fishingTick + 1, fishingTick * 2); // If tick is over bore duration, it will fail } - session.Send(FishingPacket.Start(session.Field.FieldTickInt + fishingTick, fishFightGame)); + session.Send(FishingPacket.Start((session.Field.FieldTick + fishingTick).Truncate32(), fishFightGame)); return FishingError.none; } From d5aff14a526723f8c81f443473d667ed9c8e88fc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=82ngelo=20Tadeucci?= Date: Tue, 14 Oct 2025 22:01:12 -0300 Subject: [PATCH 5/5] . --- Maple2.Server.Game/Session/GameSession.cs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/Maple2.Server.Game/Session/GameSession.cs b/Maple2.Server.Game/Session/GameSession.cs index 093e145af..f4c80ef88 100644 --- a/Maple2.Server.Game/Session/GameSession.cs +++ b/Maple2.Server.Game/Session/GameSession.cs @@ -27,6 +27,7 @@ using Maple2.Server.Game.Util; using Maple2.Server.Game.Util.Sync; using Maple2.Server.World.Service; +using Maple2.Tools.Extensions; using Maple2.Tools.Scheduler; using MigrationType = Maple2.Model.Enum.MigrationType; using WorldClient = Maple2.Server.World.Service.World.WorldClient; @@ -819,7 +820,7 @@ void SaveCacheConfig() { .Select(buff => new BuffInfo { Id = buff.Id, Level = buff.Level, - MsRemaining = (int) (buff.EndTick - fieldTick), + MsRemaining = (buff.EndTick - fieldTick).Truncate32(), Stacks = buff.Stacks, Enabled = buff.Enabled, StopTime = stopTime, @@ -832,7 +833,7 @@ void SaveCacheConfig() { SkillId = cooldown.SkillId, SkillLevel = cooldown.Level, GroupId = cooldown.GroupId, - MsRemaining = (int) (cooldown.EndTick - fieldTick), + MsRemaining = (cooldown.EndTick - fieldTick).Truncate32(), StopTime = stopTime, Charges = cooldown.Charges, }),