diff --git a/Projects/Server.Tests/Tests/Timer/TimerTests.cs b/Projects/Server.Tests/Tests/Timer/TimerTests.cs index 8888a93dc..6f00b6684 100644 --- a/Projects/Server.Tests/Tests/Timer/TimerTests.cs +++ b/Projects/Server.Tests/Tests/Timer/TimerTests.cs @@ -1,108 +1,108 @@ using System; using Xunit; -namespace Server.Tests +namespace Server.Tests; + +[Collection("Sequential Tests")] +public class TimerTests : IClassFixture { - [Collection("Sequential Tests")] - public class TimerTests : IClassFixture + [Theory] + [InlineData(0L, 8L)] + [InlineData(80L, 80L)] + [InlineData(65L, 72L)] + [InlineData(32767L, 32768L)] + [InlineData(32768L, 32768L)] + [InlineData(32833L, 32840L)] + [InlineData(134217729L, 134217736L)] + public void TestVariousTimes(long ticks, long expectedTicks) { - [Theory] - [InlineData(0L, 8L)] - [InlineData(80L, 80L)] - [InlineData(65L, 72L)] - [InlineData(32767L, 32768L)] - [InlineData(32768L, 32768L)] - [InlineData(32833L, 32840L)] - [InlineData(134217729L, 134217736L)] - public void TestVariousTimes(long ticks, long expectedTicks) + var timerTicks = new TimerTicks(); + Timer.Init(timerTicks.Ticks); + + Timer.StartTimer(TimeSpan.FromMilliseconds(ticks), action); + + var tickCount = expectedTicks / 8; + + for (int i = 1; i <= tickCount; i++) { - var timerTicks = new TimerTicks(); - void action() - { - Assert.Equal(expectedTicks, timerTicks.Ticks); - timerTicks.ExecutedCount++; - } + timerTicks.Ticks = i * 8; - Timer.Init(timerTicks.Ticks); - - Timer.StartTimer(TimeSpan.FromMilliseconds(ticks), action); - - var tickCount = expectedTicks / 8; - - for (int i = 1; i <= tickCount; i++) - { - timerTicks.Ticks = i * 8; - - Timer.Slice(timerTicks.Ticks); - } - - Assert.Equal(1, timerTicks.ExecutedCount); + Timer.Slice(timerTicks.Ticks); } - [Theory] - [InlineData(1000L, 1000L, 30000L, 30000L, 2)] - public void TestIntervals(long delay, long expectedDelayTicks, long interval, long expectedIntervalTicks, int count) + Assert.Equal(1, timerTicks.ExecutedCount); + return; + + void action() { - var timerTicks = new TimerTicks(); + Assert.Equal(expectedTicks, timerTicks.Ticks); + timerTicks.ExecutedCount++; + } + } - Timer.Init(timerTicks.Ticks); + [Theory] + [InlineData(1000L, 1000L, 30000L, 30000L, 2)] + public void TestIntervals(long delay, long expectedDelayTicks, long interval, long expectedIntervalTicks, int count) + { + var timerTicks = new TimerTicks(); - Timer.StartTimer(TimeSpan.FromMilliseconds(delay), TimeSpan.FromMilliseconds(interval), count, action); + Timer.Init(timerTicks.Ticks); - var tickCount = (expectedDelayTicks + (expectedIntervalTicks * count - 1)) / 8; + Timer.StartTimer(TimeSpan.FromMilliseconds(delay), TimeSpan.FromMilliseconds(interval), count, action); - for (int i = 1; i <= tickCount; i++) - { - timerTicks.Ticks = i * 8; + var tickCount = (expectedDelayTicks + (expectedIntervalTicks * count - 1)) / 8; - Timer.Slice(timerTicks.Ticks); - } + for (int i = 1; i <= tickCount; i++) + { + timerTicks.Ticks = i * 8; - Assert.Equal(count, timerTicks.ExecutedCount); - return; - - void action() - { - timerTicks.ExpectedTicks += timerTicks.ExecutedCount++ == 0 ? expectedDelayTicks : expectedIntervalTicks; - Assert.Equal(timerTicks.ExpectedTicks, timerTicks.Ticks); - } + Timer.Slice(timerTicks.Ticks); } - [Fact] - public void TestTimerStartedOnTick() + Assert.Equal(count, timerTicks.ExecutedCount); + return; + + void action() { - var timerTicks = new TimerTicks(); - - Timer.Init(timerTicks.Ticks); - - var timer = new SelfRunningTimer(timerTicks); - timer.Start(); - - Timer.Slice(128); - Assert.Equal(1, timerTicks.ExecutedCount); - Timer.Slice(256); - Assert.Equal(2, timerTicks.ExecutedCount); + timerTicks.ExpectedTicks += timerTicks.ExecutedCount++ == 0 ? expectedDelayTicks : expectedIntervalTicks; + Assert.Equal(timerTicks.ExpectedTicks, timerTicks.Ticks); } + } - private class TimerTicks + [Fact] + public void TestTimerStartedOnTick() + { + var timerTicks = new TimerTicks(); + + Timer.Init(timerTicks.Ticks); + + var timer = new SelfRunningTimer(timerTicks); + timer.Start(); + + Timer.Slice(128); + Assert.Equal(1, timerTicks.ExecutedCount); + Timer.Slice(256); + Assert.Equal(2, timerTicks.ExecutedCount); + } + + private class TimerTicks + { + public long ExpectedTicks; + public long Ticks; + public int ExecutedCount; + } + + private class SelfRunningTimer : Timer + { + private readonly TimerTicks _timerTicks; + public SelfRunningTimer(TimerTicks ticks) : base(TimeSpan.FromMilliseconds(100)) => _timerTicks = ticks; + + protected override void OnTick() { - public long ExpectedTicks; - public long Ticks; - public int ExecutedCount; - } - - private class SelfRunningTimer : Timer - { - private readonly TimerTicks _timerTicks; - public SelfRunningTimer(TimerTicks ticks) : base(TimeSpan.FromMilliseconds(100)) => _timerTicks = ticks; - - protected override void OnTick() + if (_timerTicks.ExecutedCount++ == 0) { - if (_timerTicks.ExecutedCount++ == 0) - { - Delay = TimeSpan.FromMilliseconds(100); - Start(); - } + Delay = TimeSpan.FromMilliseconds(100); + Start(); } } } diff --git a/Projects/Server/Client/BwtDecompress.cs b/Projects/Server/Client/BwtDecompress.cs new file mode 100644 index 000000000..414b278fa --- /dev/null +++ b/Projects/Server/Client/BwtDecompress.cs @@ -0,0 +1,175 @@ +using System; +using System.IO; +using System.Numerics; +using System.Runtime.InteropServices; + +namespace Server; + +public static class BwtDecompress +{ + public static byte[] Decompress(Stream stream, int length) + { + var firstChar = (byte)stream.ReadByte(); + + Span table = GC.AllocateUninitializedArray(256 * 256); + Span output = GC.AllocateUninitializedArray(length); + BuildTable(table, firstChar); + + var i = 0; + while (stream.Position < stream.Length) + { + var currentValue = firstChar; + var value = table[currentValue]; + if (currentValue > 0) + { + do + { + table[currentValue] = table[currentValue - 1]; + } while (--currentValue > 0); + } + + table[0] = value; + + output[i++] = (byte)value; + firstChar = (byte)stream.ReadByte(); + } + + return InternalDecompress(output); + } + + private static void BuildTable(Span table, byte startValue) + { + var index = 0; + var firstByte = startValue; + byte secondByte = 0; + for (var i = 0; i < 256 * 256; i++) + { + var val = (ushort)(firstByte + (secondByte << 8)); + table[index++] = val; + + firstByte++; + if (firstByte == 0) + { + secondByte++; + } + } + + table.Sort(); + } + + private static byte[] InternalDecompress(Span input) + { + Span symbolTable = stackalloc byte[256]; + Span frequency = stackalloc byte[256]; + Span partialInput = stackalloc int[256 * 3]; + + for (var i = 0; i < 256; i++) + { + symbolTable[i] = (byte)i; + } + + MemoryMarshal.Cast(input)[..256].CopyTo(partialInput); + + var sum = 0; + for (var i = 0; i < 256; i++) + { + sum += partialInput[i]; + } + + var nonZeroCount = 256 - partialInput[..256].Count(0); + + Frequency(partialInput, frequency); + + for (int i = 0, m = 0; i < nonZeroCount; ++i) + { + var freq = frequency[i]; + symbolTable[input[m + 1024]] = freq; + partialInput[freq + 256] = m + 1; + m += partialInput[freq]; + partialInput[freq + 512] = m; + } + + var val = symbolTable[0]; + var output = GC.AllocateUninitializedArray(sum); + + var count = 0; + do + { + ref var firstValRef = ref partialInput[val + 256]; + output[count] = val; + + if (firstValRef >= partialInput[val + 512]) + { + if (nonZeroCount-- > 0) + { + ShiftLeftSimd(symbolTable, nonZeroCount); + val = symbolTable[0]; + } + } + else + { + var idx = input[firstValRef + 1024]; + firstValRef++; + + if (idx != 0) + { + ShiftLeftSimd(symbolTable, idx); + symbolTable[idx] = val; + val = symbolTable[0]; + } + } + + count++; + } while (count < sum); + + return output; + } + + private static void Frequency(Span input, Span output) + { + Span tmp = stackalloc int[256]; + input[..256].CopyTo(tmp); + + for (var i = 0; i < 256; i++) + { + uint value = 0; + byte index = 0; + + for (var j = 0; j < 256; j++) + { + if (tmp[j] > value) + { + index = (byte)j; + value = (uint)tmp[j]; + } + } + + if (value == 0) + { + break; + } + + output[i] = index; + tmp[index] = 0; + } + } + + private static void ShiftLeftSimd(Span input, int max) + { + var i = 0; + var vectorSize = Vector.Count; + + while (i + vectorSize <= max) + { + var vector = new Vector(input[(i + 1)..]); + vector.CopyTo(input[i..]); + i += vectorSize; + } + + while (i < max) + { + input[i] = input[i + 1]; + i++; + } + } +} diff --git a/Projects/Server/Localization/Localization.cs b/Projects/Server/Localization/Localization.cs index cae4e93b5..a92d69f24 100644 --- a/Projects/Server/Localization/Localization.cs +++ b/Projects/Server/Localization/Localization.cs @@ -14,6 +14,7 @@ *************************************************************************/ using System; +using System.Buffers.Binary; using System.Collections.Generic; using System.IO; using System.Runtime.CompilerServices; @@ -96,28 +97,46 @@ public static class Localization if (File.Exists(file)) { using var fs = new FileStream(file, FileMode.Open, FileAccess.Read, FileShare.Read); - using var bin = new BinaryReader(fs); + Span header = stackalloc byte[6]; + fs.Read(header); - bin.ReadInt32(); - bin.ReadInt16(); + byte[] data; + BufferReader br; + if (BinaryPrimitives.ReadInt32LittleEndian(header) != 2 || BinaryPrimitives.ReadInt16LittleEndian(header[4..]) != 1) + { + // Skip header + fs.Position = 4; + data = BwtDecompress.Decompress(fs, (int)fs.Length - 4); + br = new BufferReader(data); + + var header2 = br.ReadInt(); // Header 2 + var header1 = br.ReadShort(); // Header 1 + + if (header2 != 2 || header1 != 1) + { + throw new Exception($"Invalid cliloc header in {file}"); + } + } + else + { + data = GC.AllocateUninitializedArray((int)fs.Length - 6); + fs.Read(data); + br = new BufferReader(data); + } byte[] buffer = null; - while (bin.BaseStream.Length != bin.BaseStream.Position) + while (br.Position < data.Length) { - var number = bin.ReadInt32(); - var flag = bin.ReadByte(); // Original, Custom, Modified - var length = bin.ReadInt16(); + var number = br.ReadInt(); + var flag = br.ReadByte(); // Original, Custom, Modified + var length = br.ReadShort(); if (buffer == null || buffer.Length < length) { buffer = GC.AllocateUninitializedArray(length); } - var bytesRead = bin.Read(buffer, 0, length); - if (bytesRead != length) - { - throw new Exception($"Could not read enough bytes from {file}"); - } + br.Read(buffer.AsSpan(0, length)); var text = Encoding.UTF8.GetString(buffer.AsSpan(0, length)); entries[number] = new LocalizationEntry(lang, number, text); diff --git a/Projects/Server/Main.cs b/Projects/Server/Main.cs index ab19073ff..83be71997 100644 --- a/Projects/Server/Main.cs +++ b/Projects/Server/Main.cs @@ -16,6 +16,7 @@ using System; using System.Collections.Generic; using System.Diagnostics; +using System.Globalization; using System.IO; using System.Linq; using System.Reflection; @@ -353,6 +354,8 @@ public static class Core public static void Setup(Assembly applicationAssembly, Process process) { + CultureInfo.DefaultThreadCurrentCulture = CultureInfo.InvariantCulture; + Process = process; ApplicationAssembly = applicationAssembly; Assembly = Assembly.GetAssembly(typeof(Core)); diff --git a/Projects/Server/Timer/Timer.TimerWheel.cs b/Projects/Server/Timer/Timer.TimerWheel.cs index ad4630a22..f8524a759 100644 --- a/Projects/Server/Timer/Timer.TimerWheel.cs +++ b/Projects/Server/Timer/Timer.TimerWheel.cs @@ -1,6 +1,6 @@ /************************************************************************* * ModernUO * - * Copyright 2019-2023 - ModernUO Development Team * + * Copyright 2019-2024 - ModernUO Development Team * * Email: hi@modernuo.com * * File: Timer.TimerWheel.cs * * * @@ -18,6 +18,7 @@ using System.Collections.Generic; using System.Diagnostics; using System.IO; using System.Linq; +using System.Runtime.CompilerServices; namespace Server; @@ -33,9 +34,9 @@ public partial class Timer private const int _tickRate = 1 << _tickRatePowerOf2; // 8ms private const long _maxDuration = (long)_tickRate << (_ringSizePowerOf2 * _ringLayers - 1); - private static Timer[][] _rings = new Timer[_ringLayers][]; - private static int[] _ringIndexes = new int[_ringLayers]; - private static Timer[] _executingRings = new Timer[_ringLayers]; + private static readonly Timer[][] _rings = new Timer[_ringLayers][]; + private static readonly int[] _ringIndexes = new int[_ringLayers]; + private static readonly Timer[] _executingRings = new Timer[_ringLayers]; private static long _lastTickTurned = -1; @@ -155,9 +156,7 @@ public partial class Timer if (!finished) { - timer.Delay = timer.Interval; - timer.Next = DateTime.UtcNow + timer.Interval; - AddTimer(timer, (long)timer.Delay.TotalMilliseconds); + AddTimer(timer, (long)timer.Interval.TotalMilliseconds); } else { @@ -168,10 +167,21 @@ public partial class Timer timer.Index++; } + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static long RoundTicksToNextPowerOfTwo(long value) + { + if (value <= 0) + { + return _tickRate; + } + + const long mask = _tickRate - 1; + return (value + mask) & ~mask; + } + private static void AddTimer(Timer timer, long delay) { - var originalDelay = delay; - delay = Math.Max(0, delay); + var actualDelay = delay; var resolutionPowerOf2 = _tickRatePowerOf2; for (var i = 0; i < _ringLayers; i++) @@ -205,7 +215,7 @@ public partial class Timer logger.Error( $"Timer {{Timer}} has a duration of {{Duration}}ms, more than max capacity of {{MaxDuration}}ms.{Environment.NewLine}{{StackTrace}}", timer.GetType(), - originalDelay, + actualDelay, _maxDuration, new StackTrace() ); @@ -214,18 +224,19 @@ public partial class Timer } } + timer.Next = Core.Now + timer.Delay; timer.Attach(_rings[i][slot]); timer._remaining = remaining; timer._ring = i; timer._slot = (int)slot; _rings[i][slot] = timer; - return; } // The remaining amount until we turn this ring - delay -= resolution * (_ringSize - _ringIndexes[i]); + var offsetDelay = resolution * (_ringSize - _ringIndexes[i]); + delay -= offsetDelay; resolutionPowerOf2 = nextResolutionPowerOf2; } } diff --git a/Projects/Server/Timer/Timer.cs b/Projects/Server/Timer/Timer.cs index 536faedf9..dd36c7551 100644 --- a/Projects/Server/Timer/Timer.cs +++ b/Projects/Server/Timer/Timer.cs @@ -1,6 +1,6 @@ /************************************************************************* * ModernUO * - * Copyright 2019-2023 - ModernUO Development Team * + * Copyright 2019-2024 - ModernUO Development Team * * Email: hi@modernuo.com * * File: Timer.cs * * * @@ -34,6 +34,8 @@ public partial class Timer private long _remaining; private Timer _nextTimer; private Timer _prevTimer; + private TimeSpan _delay; + private TimeSpan _interval; public Timer(TimeSpan delay) => Init(delay, TimeSpan.Zero, 1); @@ -43,14 +45,14 @@ public partial class Timer protected void Init(TimeSpan delay, TimeSpan interval, int count) { - Running = false; Delay = delay; - Index = 0; + Next = DateTime.MinValue; Interval = interval; Count = count; + Running = false; + Index = 0; _nextTimer = null; _prevTimer = null; - Next = Core.Now + Delay; _ring = -1; _slot = -1; } @@ -58,8 +60,19 @@ public partial class Timer protected int Version { get; set; } // Used to determine if a timer was altered and we should abandon it. public DateTime Next { get; private set; } - public TimeSpan Delay { get; set; } - public TimeSpan Interval { get; set; } + + public TimeSpan Delay + { + get => _delay; + set => _delay = TimeSpan.FromMilliseconds(RoundTicksToNextPowerOfTwo((long)value.TotalMilliseconds)); + } + + public TimeSpan Interval + { + get => _interval; + set => _interval = TimeSpan.FromMilliseconds(RoundTicksToNextPowerOfTwo((long)value.TotalMilliseconds)); + } + public int Index { get; private set; } public int Count { get; private set; } public int RemainingCount => Count == 0 ? int.MaxValue : Count - Index; diff --git a/Projects/UOContent/Engines/Virtues/HonorContext.cs b/Projects/UOContent/Engines/Virtues/HonorContext.cs index 8eab8bd1d..8e2ba1a13 100644 --- a/Projects/UOContent/Engines/Virtues/HonorContext.cs +++ b/Projects/UOContent/Engines/Virtues/HonorContext.cs @@ -186,7 +186,7 @@ public class HonorContext Source.Mana += restore; } - if (VirtueSystem.GetVirtues(Source).Honor > targetFame) + if (VirtueSystem.GetVirtues(Source)?.Honor > targetFame) { return; } @@ -194,7 +194,7 @@ public class HonorContext // Initial honor gain is 100th of the monsters honor var dGain = targetFame / 100.0 * (_honorDamage / _totalDamage); - if (_honorDamage == _totalDamage && _firstHit == FirstHit.Granted) + if (Math.Abs(_honorDamage - _totalDamage) < 0.01 && _firstHit == FirstHit.Granted) { dGain *= 1.5; // honor gain is increased a lot more if the combat was fully honorable } diff --git a/Projects/UOContent/Engines/Virtues/Justice.cs b/Projects/UOContent/Engines/Virtues/Justice.cs index 01afa4de6..abbb8da1e 100644 --- a/Projects/UOContent/Engines/Virtues/Justice.cs +++ b/Projects/UOContent/Engines/Virtues/Justice.cs @@ -227,7 +227,7 @@ public class JusticeVirtue } else { - AddProtection(protectee, protector); + AddProtection(protector, protectee); var args = $"{protector.Name}\t{protectee.Name}"; @@ -267,67 +267,75 @@ public class JusticeVirtue } } -public class AcceptProtectorGump : Gump +public class AcceptProtectorGump : StaticGump { - private readonly PlayerMobile _protectee; private readonly PlayerMobile _protector; + private readonly PlayerMobile _protectee; public AcceptProtectorGump(PlayerMobile protector, PlayerMobile protectee) : base(150, 50) { _protector = protector; _protectee = protectee; + } - Closable = false; + protected override void BuildLayout(ref StaticGumpBuilder builder) + { + builder.SetNoClose(); - AddPage(0); + builder.AddPage(); - AddBackground(0, 0, 396, 218, 3600); + builder.AddBackground(0, 0, 396, 218, 3600); - AddImageTiled(15, 15, 365, 190, 2624); - AddAlphaRegion(15, 15, 365, 190); + builder.AddImageTiled(15, 15, 365, 190, 2624); + builder.AddAlphaRegion(15, 15, 365, 190); // Another player is offering you their protection: - AddHtmlLocalized(30, 20, 360, 25, 1049365, 0x7FFF); - AddLabel(90, 55, 1153, protector.Name); + builder.AddHtmlLocalized(30, 20, 360, 25, 1049365, 0x7FFF); + builder.AddLabelPlaceholder(90, 55, 1153, "protector"); - AddImage(50, 45, 9005); - AddImageTiled(80, 80, 200, 1, 9107); - AddImageTiled(95, 82, 200, 1, 9157); + builder.AddImage(50, 45, 9005); + builder.AddImageTiled(80, 80, 200, 1, 9107); + builder.AddImageTiled(95, 82, 200, 1, 9157); - AddRadio(30, 110, 9727, 9730, true, 1); - AddHtmlLocalized(65, 115, 300, 25, 1049444, 0x7FFF); // Yes, I would like their protection. + builder.AddRadio(30, 110, 9727, 9730, true, 1); + builder.AddHtmlLocalized(65, 115, 300, 25, 1049444, 0x7FFF); // Yes, I would like their protection. - AddRadio(30, 145, 9727, 9730, false, 0); - AddHtmlLocalized(65, 148, 300, 25, 1049445, 0x7FFF); // No thanks, I can take care of myself. + builder.AddRadio(30, 145, 9727, 9730, false, 0); + builder.AddHtmlLocalized(65, 148, 300, 25, 1049445, 0x7FFF); // No thanks, I can take care of myself. - AddButton(160, 175, 247, 248, 2); + builder.AddButton(160, 175, 247, 248, 2); - AddImage(215, 0, 50581); + builder.AddImage(215, 0, 50581); - AddImageTiled(15, 14, 365, 1, 9107); - AddImageTiled(380, 14, 1, 190, 9105); - AddImageTiled(15, 205, 365, 1, 9107); - AddImageTiled(15, 14, 1, 190, 9105); - AddImageTiled(0, 0, 395, 1, 9157); - AddImageTiled(394, 0, 1, 217, 9155); - AddImageTiled(0, 216, 395, 1, 9157); - AddImageTiled(0, 0, 1, 217, 9155); + builder.AddImageTiled(15, 14, 365, 1, 9107); + builder.AddImageTiled(380, 14, 1, 190, 9105); + builder.AddImageTiled(15, 205, 365, 1, 9107); + builder.AddImageTiled(15, 14, 1, 190, 9105); + builder.AddImageTiled(0, 0, 395, 1, 9157); + builder.AddImageTiled(394, 0, 1, 217, 9155); + builder.AddImageTiled(0, 216, 395, 1, 9157); + builder.AddImageTiled(0, 0, 1, 217, 9155); + } + + protected override void BuildStrings(ref GumpStringsBuilder builder) + { + builder.SetStringSlot("protector", _protector.Name); } public override void OnResponse(NetState sender, in RelayInfo info) { - if (info.ButtonID == 2) + if (info.ButtonID != 2) { - var okay = info.IsSwitched(1); + return; + } - if (okay) - { - JusticeVirtue.OnVirtueAccepted(_protector, _protectee); - } - else - { - JusticeVirtue.OnVirtueRejected(_protector, _protectee); - } + if (info.IsSwitched(1)) // okay + { + JusticeVirtue.OnVirtueAccepted(_protector, _protectee); + } + else + { + JusticeVirtue.OnVirtueRejected(_protector, _protectee); } } } diff --git a/Projects/UOContent/Mobiles/Abilities/MonsterAbility.cs b/Projects/UOContent/Mobiles/Abilities/MonsterAbility.cs index abe9597ac..72fb61ec3 100644 --- a/Projects/UOContent/Mobiles/Abilities/MonsterAbility.cs +++ b/Projects/UOContent/Mobiles/Abilities/MonsterAbility.cs @@ -18,17 +18,19 @@ public abstract partial class MonsterAbility public virtual TimeSpan MinTriggerCooldown => TimeSpan.Zero; public virtual TimeSpan MaxTriggerCooldown => TimeSpan.Zero; - // To prevent reflect from harming the monster - public virtual bool CanTriggerAgainstSelf => false; - public bool WillTrigger(MonsterAbilityTrigger trigger) => (AbilityTrigger & trigger) != 0; /// - /// Returns true if ability is not on cooldown, and the change to trigger succeeds. + /// Returns true if ability is not on cooldown, and the chance to trigger succeeds. /// /// Boolean indicating the ability can trigger. public virtual bool CanTrigger(BaseCreature source, MonsterAbilityTrigger trigger) { + if (source is not { Alive: true, Deleted: false }) + { + return false; + } + if (_nextTriggerTicks?.TryGetValue(source, out var nextTrigger) == true && nextTrigger - Core.TickCount > 0) { return false; @@ -54,11 +56,11 @@ public abstract partial class MonsterAbility /// /// Triggers the monster's ability. Override this and call `base.Trigger(source);` to make sure /// the cooldown is tracked. + /// Note: This can fire after a monster is killed/deleted (map is null) /// public virtual void Trigger(MonsterAbilityTrigger trigger, BaseCreature source, Mobile target) { - if (!CanTriggerAgainstSelf && target == source || - MinTriggerCooldown <= TimeSpan.Zero && MaxTriggerCooldown <= TimeSpan.Zero) + if (MinTriggerCooldown <= TimeSpan.Zero && MaxTriggerCooldown <= TimeSpan.Zero) { return; } diff --git a/Projects/UOContent/Mobiles/Vendors/PlayerBarkeeper.cs b/Projects/UOContent/Mobiles/Vendors/PlayerBarkeeper.cs index 78e63846b..c658e0111 100644 --- a/Projects/UOContent/Mobiles/Vendors/PlayerBarkeeper.cs +++ b/Projects/UOContent/Mobiles/Vendors/PlayerBarkeeper.cs @@ -612,217 +612,233 @@ namespace Server.Mobiles } } - public class BarkeeperTitleGump : Gump + public class BarkeeperTitleGump : StaticGump { - private static readonly Entry[] m_Entries = + private static readonly Entry[] _entries = { - new("Alchemist"), - new("Animal Tamer"), - new("Apothecary"), - new("Artist"), - new("Baker", true), - new("Bard"), - new("Barkeep", "the barkeeper", true), - new("Beggar"), - new("Blacksmith"), - new("Bounty Hunter"), - new("Brigand"), - new("Butler"), - new("Carpenter"), - new("Chef", true), - new("Commander"), - new("Curator"), - new("Drunkard"), - new("Farmer"), - new("Fisherman"), - new("Gambler"), - new("Gypsy"), - new("Herald"), - new("Herbalist"), - new("Hermit"), - new("Innkeeper", true), - new("Jailor"), - new("Jester"), - new("Librarian"), - new("Mage"), - new("Mercenary"), - new("Merchant"), - new("Messenger"), - new("Miner"), - new("Monk"), - new("Noble"), - new("Paladin"), - new("Peasant"), - new("Pirate"), - new("Prisoner"), - new("Prophet"), - new("Ranger"), - new("Sage"), - new("Sailor"), - new("Scholar"), - new("Scribe"), - new("Sentry"), - new("Servant"), - new("Shepherd"), - new("Soothsayer"), - new("Stoic"), - new("Storyteller"), - new("Tailor"), - new("Thief"), - new("Tinker"), - new("Town Crier"), - new("Treasure Hunter"), - new("Waiter", true), - new("Warrior"), - new("Watchman"), - new("No Title", null) + new(1076083, "the alchemist"), + new(1077244, "the animal tamer"), + new(1078440, "the apothecary"), + new(1078441, "the artist"), + new(1078442, "the baker", true), + new(1073298, "the bard"), + new(1076102, "the barkeep", true), + new(1078443, "the beggar"), + new(1073297, "the blacksmith"), + new(1078444, "the bounty hunter"), + new(1078446, "the brigand"), + new(1078447, "the butler"), + new(1060774, "the carpenter"), + new(1078448, "the chef", true), + new(1078449, "the commander"), + new(1078450, "the curator"), + new(1078451, "the drunkard"), + new(1078452, "the farmer"), + new(1078453, "the fisherman"), + new(1078454, "the gambler"), + new(1078455, "the gypsy"), + new(1075996, "the herald"), + new(1076107, "the herbalist"), + new(1078465, "the hermit"), + new(1078466, "the innkeeper", true), + new(1078467, "the jailor"), + new(1078468, "the jester"), + new(1078469, "the librarian"), + new(1073292, "the mage"), + new(1078470, "the mercenary"), + new(1060775, "the merchant"), + new(1078472, "the messenger"), + new(1076093, "the miner"), + new(1078475, "the monk"), + new(1078476, "the noble"), + new(1073290, "the paladin"), + new(1078479, "the peasant"), + new(1078480, "the pirate"), + new(1078481, "the prisoner"), + new(1078482, "the prophet"), + new(1078484, "the ranger"), + new(1078487, "the sage"), + new(1078488, "the sailor"), + new(1078489, "the scholar"), + new(1060773, "the scribe"), + new(1078490, "the sentry"), + new(1060795, "the servant"), + new(1078491, "the shepherd"), + new(1078492, "the soothsayer"), + new(1078493, "the stoic"), + new(1078494, "the storyteller"), + new(1076134, "the tailor"), + new(1076096, "the thief"), + new(1076137, "the tinker"), + new(1076097, "the town crier"), + new(1073291, "the treasure hunter"), + new(1076112, "the waiter", true), + new(1077242, "the warrior"), + new(1078496, "the watchman"), + new(1078495, null) // No Title }; - private readonly PlayerBarkeeper m_Barkeeper; - private readonly Mobile m_From; + private static int _pageCount = (_entries.Length + 19) / 20; + + private readonly PlayerBarkeeper _barkeeper; + private readonly Mobile _from; public override bool Singleton => true; - public BarkeeperTitleGump(Mobile from, PlayerBarkeeper barkeeper) : base(0, 0) + protected override void BuildLayout(ref StaticGumpBuilder builder) { - m_From = from; - m_Barkeeper = barkeeper; + RenderBackground(ref builder); - from.CloseGump(); - - var entries = m_Entries; - - RenderBackground(); - - var pageCount = (entries.Length + 19) / 20; - - for (var i = 0; i < pageCount; ++i) + for (var i = 0; i < _pageCount; ++i) { - RenderPage(entries, i); + RenderPage(ref builder, i); } } - private void RenderBackground() + public BarkeeperTitleGump(Mobile from, PlayerBarkeeper barkeeper) : base(0, 0) { - AddPage(0); - - AddBackground(30, 40, 585, 410, 5054); - - AddImage(30, 40, 9251); - AddImage(180, 40, 9251); - AddImage(30, 40, 9253); - AddImage(30, 130, 9253); - AddImage(598, 40, 9255); - AddImage(598, 130, 9255); - AddImage(30, 433, 9257); - AddImage(180, 433, 9257); - AddImage(30, 40, 9250); - AddImage(598, 40, 9252); - AddImage(598, 433, 9258); - AddImage(30, 433, 9256); - - AddItem(30, 40, 6816); - AddItem(30, 125, 6817); - AddItem(30, 233, 6817); - AddItem(30, 341, 6817); - AddItem(580, 40, 6814); - AddItem(588, 125, 6815); - AddItem(588, 233, 6815); - AddItem(588, 341, 6815); - - AddImage(560, 20, 1417); - AddItem(580, 44, 4033); - - AddBackground(183, 25, 280, 30, 5054); - - AddImage(180, 25, 10460); - AddImage(434, 25, 10460); - - AddHtml(223, 32, 200, 40, "BARKEEP CUSTOMIZATION MENU"); - AddBackground(243, 433, 150, 30, 5054); - - AddImage(240, 433, 10460); - AddImage(375, 433, 10460); - - AddImage(80, 398, 2151); - AddItem(72, 406, 2543); - - AddHtml(110, 412, 180, 25, "sells food and drink"); + _from = from; + _barkeeper = barkeeper; } - private void RenderPage(Entry[] entries, int page) + private static void RenderBackground(ref StaticGumpBuilder builder) { - AddPage(1 + page); + builder.AddPage(); - AddHtml(430, 70, 180, 25, $"Page {page + 1} of {(entries.Length + 19) / 20}"); + builder.AddBackground(30, 40, 585, 410, 5054); - for (int count = 0, i = page * 20; count < 20 && i < entries.Length; ++count, ++i) + builder.AddImage(30, 40, 9251); + builder.AddImage(180, 40, 9251); + builder.AddImage(30, 40, 9253); + builder.AddImage(30, 130, 9253); + builder.AddImage(598, 40, 9255); + builder.AddImage(598, 130, 9255); + builder.AddImage(30, 433, 9257); + builder.AddImage(180, 433, 9257); + builder.AddImage(30, 40, 9250); + builder.AddImage(598, 40, 9252); + builder.AddImage(598, 433, 9258); + builder.AddImage(30, 433, 9256); + + builder.AddItem(30, 40, 6816); + builder.AddItem(30, 125, 6817); + builder.AddItem(30, 233, 6817); + builder.AddItem(30, 341, 6817); + builder.AddItem(580, 40, 6814); + builder.AddItem(588, 125, 6815); + builder.AddItem(588, 233, 6815); + builder.AddItem(588, 341, 6815); + + builder.AddImage(560, 20, 1417); + builder.AddItem(580, 44, 4033); + + builder.AddBackground(183, 25, 280, 30, 5054); + + builder.AddImage(180, 25, 10460); + builder.AddImage(434, 25, 10460); + + builder.AddHtmlLocalized(223, 32, 200, 40, 1078366); // BARKEEP CUSTOMIZATION MENU + builder.AddBackground(243, 433, 150, 30, 5054); + + builder.AddImage(240, 433, 10460); + builder.AddImage(375, 433, 10460); + + builder.AddImage(80, 398, 2151); + builder.AddItem(72, 406, 2543); + + builder.AddHtmlLocalized(110, 412, 180, 25, 1078445); // sells food and drink + } + + private static void RenderPage(ref StaticGumpBuilder builder, int page) + { + var currentPage = page + 1; + builder.AddPage(currentPage); + + if (_pageCount == 3 && currentPage is >= 1 and <= 3) { - var entry = entries[i]; + var pageCliloc = currentPage switch + { + 1 => 1078439, // Page 1 of 3 + 2 => 1078464, // Page 2 of 3 + 3 => 1078483, // Page 3 of 3 + }; - AddButton(80 + count / 10 * 260, 100 + count % 10 * 30, 4005, 4007, 2 + i); - AddHtml( - 120 + count / 10 * 260, - 100 + count % 10 * 30, - entry.m_Vendor ? 148 : 180, + builder.AddHtmlLocalized(430, 70, 180, 25, pageCliloc); + } + else + { + // Page ~1_CUR~ of ~2_MAX~ + builder.AddHtmlLocalized(430, 70, 180, 25, 1153561, $"{currentPage}\t{_pageCount}", 0x7FFF); + } + + for (int count = 0, i = page * 20; count < 20 && i < _entries.Length; ++count, ++i) + { + var entry = _entries[i]; + + var xOffset = Math.DivRem(count, 10, out var yOffset); + + builder.AddButton(80 + xOffset * 260, 100 + yOffset * 30, 4005, 4007, 2 + i); + builder.AddHtmlLocalized( + 120 + xOffset * 260, + 100 + yOffset * 30, + entry.Vendor ? 148 : 180, 25, - entry.m_Description, + entry.Description, true ); - if (entry.m_Vendor) + if (entry.Vendor) { - AddImage(270 + count / 10 * 260, 98 + count % 10 * 30, 2151); - AddItem(262 + count / 10 * 260, 106 + count % 10 * 30, 2543); + builder.AddImage(270 + xOffset * 260, 98 + yOffset * 30, 2151); + builder.AddItem(262 + xOffset * 260, 106 + yOffset * 30, 2543); } } - AddButton(340, 400, 4005, 4007, 0, GumpButtonType.Page, 1 + (page + 1) % ((entries.Length + 19) / 20)); - AddHtml(380, 400, 180, 25, "More Job Titles"); + builder.AddButton(340, 400, 4005, 4007, 0, GumpButtonType.Page, currentPage + 1 % _pageCount); + builder.AddHtmlLocalized(380, 400, 180, 25, 1078456); // More Job Titles - AddButton(338, 437, 4014, 4016, 1); - AddHtml(290, 440, 35, 40, "Back"); + builder.AddButton(338, 437, 4014, 4016, 1); + builder.AddHtmlLocalized(290, 440, 35, 40, 1005007); // Back } public override void OnResponse(NetState sender, in RelayInfo info) { + if (_barkeeper.Deleted) + { + return; + } + var buttonID = info.ButtonID; - if (buttonID > 0) + if (buttonID-- <= 0) { - --buttonID; + return; + } - if (buttonID > 0) - { - --buttonID; + if (buttonID-- <= 0) + { + _barkeeper.CancelChangeTitle(_from); + return; + } - if (buttonID < m_Entries.Length) - { - m_Barkeeper.EndChangeTitle(m_From, m_Entries[buttonID].m_Title, m_Entries[buttonID].m_Vendor); - } - } - else - { - m_Barkeeper.CancelChangeTitle(m_From); - } + if (buttonID < _entries.Length) + { + var entry = _entries[buttonID]; + _barkeeper.EndChangeTitle(_from, entry.Title, entry.Vendor); } } private class Entry { - public readonly string m_Description; - public readonly string m_Title; - public readonly bool m_Vendor; + public readonly int Description; + public readonly string Title; + public readonly bool Vendor; - public Entry(string desc, bool vendor = false) : this(desc, $"the {desc.ToLower()}", vendor) + public Entry(int desc, string title, bool vendor = false) { - } - - public Entry(string desc, string title, bool vendor = false) - { - m_Description = desc; - m_Title = title; - m_Vendor = vendor; + Description = desc; + Title = title; + Vendor = vendor; } } } diff --git a/Projects/UOContent/Spells/Base/SpellHelper.cs b/Projects/UOContent/Spells/Base/SpellHelper.cs index 14eb566a2..396e64fdb 100644 --- a/Projects/UOContent/Spells/Base/SpellHelper.cs +++ b/Projects/UOContent/Spells/Base/SpellHelper.cs @@ -1140,6 +1140,11 @@ namespace Server.Spells protected override void OnTick() { + if (m_Target.Deleted || !m_Target.Alive) + { + return; + } + Damage( m_Spell, TimeSpan.Zero,