Merge branch 'main' into feat-NATS

This commit is contained in:
Leath Cooper 2024-11-16 14:13:42 -05:00
commit 4ade957f62
11 changed files with 574 additions and 322 deletions

View file

@ -1,108 +1,108 @@
using System; using System;
using Xunit; using Xunit;
namespace Server.Tests namespace Server.Tests;
[Collection("Sequential Tests")]
public class TimerTests : IClassFixture<ServerFixture>
{ {
[Collection("Sequential Tests")] [Theory]
public class TimerTests : IClassFixture<ServerFixture> [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] var timerTicks = new TimerTicks();
[InlineData(0L, 8L)] Timer.Init(timerTicks.Ticks);
[InlineData(80L, 80L)]
[InlineData(65L, 72L)] Timer.StartTimer(TimeSpan.FromMilliseconds(ticks), action);
[InlineData(32767L, 32768L)]
[InlineData(32768L, 32768L)] var tickCount = expectedTicks / 8;
[InlineData(32833L, 32840L)]
[InlineData(134217729L, 134217736L)] for (int i = 1; i <= tickCount; i++)
public void TestVariousTimes(long ticks, long expectedTicks)
{ {
var timerTicks = new TimerTicks(); timerTicks.Ticks = i * 8;
void action()
{
Assert.Equal(expectedTicks, timerTicks.Ticks);
timerTicks.ExecutedCount++;
}
Timer.Init(timerTicks.Ticks); Timer.Slice(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);
} }
[Theory] Assert.Equal(1, timerTicks.ExecutedCount);
[InlineData(1000L, 1000L, 30000L, 30000L, 2)] return;
public void TestIntervals(long delay, long expectedDelayTicks, long interval, long expectedIntervalTicks, int count)
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++) var tickCount = (expectedDelayTicks + (expectedIntervalTicks * count - 1)) / 8;
{
timerTicks.Ticks = i * 8;
Timer.Slice(timerTicks.Ticks); for (int i = 1; i <= tickCount; i++)
} {
timerTicks.Ticks = i * 8;
Assert.Equal(count, timerTicks.ExecutedCount); Timer.Slice(timerTicks.Ticks);
return;
void action()
{
timerTicks.ExpectedTicks += timerTicks.ExecutedCount++ == 0 ? expectedDelayTicks : expectedIntervalTicks;
Assert.Equal(timerTicks.ExpectedTicks, timerTicks.Ticks);
}
} }
[Fact] Assert.Equal(count, timerTicks.ExecutedCount);
public void TestTimerStartedOnTick() return;
void action()
{ {
var timerTicks = new TimerTicks(); timerTicks.ExpectedTicks += timerTicks.ExecutedCount++ == 0 ? expectedDelayTicks : expectedIntervalTicks;
Assert.Equal(timerTicks.ExpectedTicks, timerTicks.Ticks);
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 [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; if (_timerTicks.ExecutedCount++ == 0)
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) Delay = TimeSpan.FromMilliseconds(100);
{ Start();
Delay = TimeSpan.FromMilliseconds(100);
Start();
}
} }
} }
} }

View file

@ -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<ushort> table = GC.AllocateUninitializedArray<ushort>(256 * 256);
Span<byte> output = GC.AllocateUninitializedArray<byte>(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<ushort> 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<byte> input)
{
Span<byte> symbolTable = stackalloc byte[256];
Span<byte> frequency = stackalloc byte[256];
Span<int> partialInput = stackalloc int[256 * 3];
for (var i = 0; i < 256; i++)
{
symbolTable[i] = (byte)i;
}
MemoryMarshal.Cast<byte, int>(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<byte>(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<int> input, Span<byte> output)
{
Span<int> 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<byte> input, int max)
{
var i = 0;
var vectorSize = Vector<byte>.Count;
while (i + vectorSize <= max)
{
var vector = new Vector<byte>(input[(i + 1)..]);
vector.CopyTo(input[i..]);
i += vectorSize;
}
while (i < max)
{
input[i] = input[i + 1];
i++;
}
}
}

View file

@ -14,6 +14,7 @@
*************************************************************************/ *************************************************************************/
using System; using System;
using System.Buffers.Binary;
using System.Collections.Generic; using System.Collections.Generic;
using System.IO; using System.IO;
using System.Runtime.CompilerServices; using System.Runtime.CompilerServices;
@ -96,28 +97,46 @@ public static class Localization
if (File.Exists(file)) if (File.Exists(file))
{ {
using var fs = new FileStream(file, FileMode.Open, FileAccess.Read, FileShare.Read); using var fs = new FileStream(file, FileMode.Open, FileAccess.Read, FileShare.Read);
using var bin = new BinaryReader(fs); Span<byte> header = stackalloc byte[6];
fs.Read(header);
bin.ReadInt32(); byte[] data;
bin.ReadInt16(); 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<byte>((int)fs.Length - 6);
fs.Read(data);
br = new BufferReader(data);
}
byte[] buffer = null; byte[] buffer = null;
while (bin.BaseStream.Length != bin.BaseStream.Position) while (br.Position < data.Length)
{ {
var number = bin.ReadInt32(); var number = br.ReadInt();
var flag = bin.ReadByte(); // Original, Custom, Modified var flag = br.ReadByte(); // Original, Custom, Modified
var length = bin.ReadInt16(); var length = br.ReadShort();
if (buffer == null || buffer.Length < length) if (buffer == null || buffer.Length < length)
{ {
buffer = GC.AllocateUninitializedArray<byte>(length); buffer = GC.AllocateUninitializedArray<byte>(length);
} }
var bytesRead = bin.Read(buffer, 0, length); br.Read(buffer.AsSpan(0, length));
if (bytesRead != length)
{
throw new Exception($"Could not read enough bytes from {file}");
}
var text = Encoding.UTF8.GetString(buffer.AsSpan(0, length)); var text = Encoding.UTF8.GetString(buffer.AsSpan(0, length));
entries[number] = new LocalizationEntry(lang, number, text); entries[number] = new LocalizationEntry(lang, number, text);

View file

@ -16,6 +16,7 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Diagnostics; using System.Diagnostics;
using System.Globalization;
using System.IO; using System.IO;
using System.Linq; using System.Linq;
using System.Reflection; using System.Reflection;
@ -353,6 +354,8 @@ public static class Core
public static void Setup(Assembly applicationAssembly, Process process) public static void Setup(Assembly applicationAssembly, Process process)
{ {
CultureInfo.DefaultThreadCurrentCulture = CultureInfo.InvariantCulture;
Process = process; Process = process;
ApplicationAssembly = applicationAssembly; ApplicationAssembly = applicationAssembly;
Assembly = Assembly.GetAssembly(typeof(Core)); Assembly = Assembly.GetAssembly(typeof(Core));

View file

@ -1,6 +1,6 @@
/************************************************************************* /*************************************************************************
* ModernUO * * ModernUO *
* Copyright 2019-2023 - ModernUO Development Team * * Copyright 2019-2024 - ModernUO Development Team *
* Email: hi@modernuo.com * * Email: hi@modernuo.com *
* File: Timer.TimerWheel.cs * * File: Timer.TimerWheel.cs *
* * * *
@ -18,6 +18,7 @@ using System.Collections.Generic;
using System.Diagnostics; using System.Diagnostics;
using System.IO; using System.IO;
using System.Linq; using System.Linq;
using System.Runtime.CompilerServices;
namespace Server; namespace Server;
@ -33,9 +34,9 @@ public partial class Timer
private const int _tickRate = 1 << _tickRatePowerOf2; // 8ms private const int _tickRate = 1 << _tickRatePowerOf2; // 8ms
private const long _maxDuration = (long)_tickRate << (_ringSizePowerOf2 * _ringLayers - 1); private const long _maxDuration = (long)_tickRate << (_ringSizePowerOf2 * _ringLayers - 1);
private static Timer[][] _rings = new Timer[_ringLayers][]; private static readonly Timer[][] _rings = new Timer[_ringLayers][];
private static int[] _ringIndexes = new int[_ringLayers]; private static readonly int[] _ringIndexes = new int[_ringLayers];
private static Timer[] _executingRings = new Timer[_ringLayers]; private static readonly Timer[] _executingRings = new Timer[_ringLayers];
private static long _lastTickTurned = -1; private static long _lastTickTurned = -1;
@ -155,9 +156,7 @@ public partial class Timer
if (!finished) if (!finished)
{ {
timer.Delay = timer.Interval; AddTimer(timer, (long)timer.Interval.TotalMilliseconds);
timer.Next = DateTime.UtcNow + timer.Interval;
AddTimer(timer, (long)timer.Delay.TotalMilliseconds);
} }
else else
{ {
@ -168,10 +167,21 @@ public partial class Timer
timer.Index++; 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) private static void AddTimer(Timer timer, long delay)
{ {
var originalDelay = delay; var actualDelay = delay;
delay = Math.Max(0, delay);
var resolutionPowerOf2 = _tickRatePowerOf2; var resolutionPowerOf2 = _tickRatePowerOf2;
for (var i = 0; i < _ringLayers; i++) for (var i = 0; i < _ringLayers; i++)
@ -205,7 +215,7 @@ public partial class Timer
logger.Error( logger.Error(
$"Timer {{Timer}} has a duration of {{Duration}}ms, more than max capacity of {{MaxDuration}}ms.{Environment.NewLine}{{StackTrace}}", $"Timer {{Timer}} has a duration of {{Duration}}ms, more than max capacity of {{MaxDuration}}ms.{Environment.NewLine}{{StackTrace}}",
timer.GetType(), timer.GetType(),
originalDelay, actualDelay,
_maxDuration, _maxDuration,
new StackTrace() new StackTrace()
); );
@ -214,18 +224,19 @@ public partial class Timer
} }
} }
timer.Next = Core.Now + timer.Delay;
timer.Attach(_rings[i][slot]); timer.Attach(_rings[i][slot]);
timer._remaining = remaining; timer._remaining = remaining;
timer._ring = i; timer._ring = i;
timer._slot = (int)slot; timer._slot = (int)slot;
_rings[i][slot] = timer; _rings[i][slot] = timer;
return; return;
} }
// The remaining amount until we turn this ring // The remaining amount until we turn this ring
delay -= resolution * (_ringSize - _ringIndexes[i]); var offsetDelay = resolution * (_ringSize - _ringIndexes[i]);
delay -= offsetDelay;
resolutionPowerOf2 = nextResolutionPowerOf2; resolutionPowerOf2 = nextResolutionPowerOf2;
} }
} }

View file

@ -1,6 +1,6 @@
/************************************************************************* /*************************************************************************
* ModernUO * * ModernUO *
* Copyright 2019-2023 - ModernUO Development Team * * Copyright 2019-2024 - ModernUO Development Team *
* Email: hi@modernuo.com * * Email: hi@modernuo.com *
* File: Timer.cs * * File: Timer.cs *
* * * *
@ -34,6 +34,8 @@ public partial class Timer
private long _remaining; private long _remaining;
private Timer _nextTimer; private Timer _nextTimer;
private Timer _prevTimer; private Timer _prevTimer;
private TimeSpan _delay;
private TimeSpan _interval;
public Timer(TimeSpan delay) => Init(delay, TimeSpan.Zero, 1); 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) protected void Init(TimeSpan delay, TimeSpan interval, int count)
{ {
Running = false;
Delay = delay; Delay = delay;
Index = 0; Next = DateTime.MinValue;
Interval = interval; Interval = interval;
Count = count; Count = count;
Running = false;
Index = 0;
_nextTimer = null; _nextTimer = null;
_prevTimer = null; _prevTimer = null;
Next = Core.Now + Delay;
_ring = -1; _ring = -1;
_slot = -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. 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 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 Index { get; private set; }
public int Count { get; private set; } public int Count { get; private set; }
public int RemainingCount => Count == 0 ? int.MaxValue : Count - Index; public int RemainingCount => Count == 0 ? int.MaxValue : Count - Index;

View file

@ -186,7 +186,7 @@ public class HonorContext
Source.Mana += restore; Source.Mana += restore;
} }
if (VirtueSystem.GetVirtues(Source).Honor > targetFame) if (VirtueSystem.GetVirtues(Source)?.Honor > targetFame)
{ {
return; return;
} }
@ -194,7 +194,7 @@ public class HonorContext
// Initial honor gain is 100th of the monsters honor // Initial honor gain is 100th of the monsters honor
var dGain = targetFame / 100.0 * (_honorDamage / _totalDamage); 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 dGain *= 1.5; // honor gain is increased a lot more if the combat was fully honorable
} }

View file

@ -227,7 +227,7 @@ public class JusticeVirtue
} }
else else
{ {
AddProtection(protectee, protector); AddProtection(protector, protectee);
var args = $"{protector.Name}\t{protectee.Name}"; var args = $"{protector.Name}\t{protectee.Name}";
@ -267,67 +267,75 @@ public class JusticeVirtue
} }
} }
public class AcceptProtectorGump : Gump public class AcceptProtectorGump : StaticGump<AcceptProtectorGump>
{ {
private readonly PlayerMobile _protectee;
private readonly PlayerMobile _protector; private readonly PlayerMobile _protector;
private readonly PlayerMobile _protectee;
public AcceptProtectorGump(PlayerMobile protector, PlayerMobile protectee) : base(150, 50) public AcceptProtectorGump(PlayerMobile protector, PlayerMobile protectee) : base(150, 50)
{ {
_protector = protector; _protector = protector;
_protectee = protectee; _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); builder.AddImageTiled(15, 15, 365, 190, 2624);
AddAlphaRegion(15, 15, 365, 190); builder.AddAlphaRegion(15, 15, 365, 190);
// Another player is offering you their <a href="?ForceTopic88">protection</a>: // Another player is offering you their <a href="?ForceTopic88">protection</a>:
AddHtmlLocalized(30, 20, 360, 25, 1049365, 0x7FFF); builder.AddHtmlLocalized(30, 20, 360, 25, 1049365, 0x7FFF);
AddLabel(90, 55, 1153, protector.Name); builder.AddLabelPlaceholder(90, 55, 1153, "protector");
AddImage(50, 45, 9005); builder.AddImage(50, 45, 9005);
AddImageTiled(80, 80, 200, 1, 9107); builder.AddImageTiled(80, 80, 200, 1, 9107);
AddImageTiled(95, 82, 200, 1, 9157); builder.AddImageTiled(95, 82, 200, 1, 9157);
AddRadio(30, 110, 9727, 9730, true, 1); builder.AddRadio(30, 110, 9727, 9730, true, 1);
AddHtmlLocalized(65, 115, 300, 25, 1049444, 0x7FFF); // Yes, I would like their protection. builder.AddHtmlLocalized(65, 115, 300, 25, 1049444, 0x7FFF); // Yes, I would like their protection.
AddRadio(30, 145, 9727, 9730, false, 0); builder.AddRadio(30, 145, 9727, 9730, false, 0);
AddHtmlLocalized(65, 148, 300, 25, 1049445, 0x7FFF); // No thanks, I can take care of myself. 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); builder.AddImageTiled(15, 14, 365, 1, 9107);
AddImageTiled(380, 14, 1, 190, 9105); builder.AddImageTiled(380, 14, 1, 190, 9105);
AddImageTiled(15, 205, 365, 1, 9107); builder.AddImageTiled(15, 205, 365, 1, 9107);
AddImageTiled(15, 14, 1, 190, 9105); builder.AddImageTiled(15, 14, 1, 190, 9105);
AddImageTiled(0, 0, 395, 1, 9157); builder.AddImageTiled(0, 0, 395, 1, 9157);
AddImageTiled(394, 0, 1, 217, 9155); builder.AddImageTiled(394, 0, 1, 217, 9155);
AddImageTiled(0, 216, 395, 1, 9157); builder.AddImageTiled(0, 216, 395, 1, 9157);
AddImageTiled(0, 0, 1, 217, 9155); 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) 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) if (info.IsSwitched(1)) // okay
{ {
JusticeVirtue.OnVirtueAccepted(_protector, _protectee); JusticeVirtue.OnVirtueAccepted(_protector, _protectee);
} }
else else
{ {
JusticeVirtue.OnVirtueRejected(_protector, _protectee); JusticeVirtue.OnVirtueRejected(_protector, _protectee);
}
} }
} }
} }

View file

@ -18,17 +18,19 @@ public abstract partial class MonsterAbility
public virtual TimeSpan MinTriggerCooldown => TimeSpan.Zero; public virtual TimeSpan MinTriggerCooldown => TimeSpan.Zero;
public virtual TimeSpan MaxTriggerCooldown => 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; public bool WillTrigger(MonsterAbilityTrigger trigger) => (AbilityTrigger & trigger) != 0;
/// <summary> /// <summary>
/// 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.
/// </summary> /// </summary>
/// <returns>Boolean indicating the ability can trigger.</returns> /// <returns>Boolean indicating the ability can trigger.</returns>
public virtual bool CanTrigger(BaseCreature source, MonsterAbilityTrigger 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) if (_nextTriggerTicks?.TryGetValue(source, out var nextTrigger) == true && nextTrigger - Core.TickCount > 0)
{ {
return false; return false;
@ -54,11 +56,11 @@ public abstract partial class MonsterAbility
/// <summary> /// <summary>
/// Triggers the monster's ability. Override this and call `base.Trigger(source);` to make sure /// Triggers the monster's ability. Override this and call `base.Trigger(source);` to make sure
/// the cooldown is tracked. /// the cooldown is tracked.
/// Note: This can fire after a monster is killed/deleted (map is null)
/// </summary> /// </summary>
public virtual void Trigger(MonsterAbilityTrigger trigger, BaseCreature source, Mobile target) public virtual void Trigger(MonsterAbilityTrigger trigger, BaseCreature source, Mobile target)
{ {
if (!CanTriggerAgainstSelf && target == source || if (MinTriggerCooldown <= TimeSpan.Zero && MaxTriggerCooldown <= TimeSpan.Zero)
MinTriggerCooldown <= TimeSpan.Zero && MaxTriggerCooldown <= TimeSpan.Zero)
{ {
return; return;
} }

View file

@ -612,217 +612,233 @@ namespace Server.Mobiles
} }
} }
public class BarkeeperTitleGump : Gump public class BarkeeperTitleGump : StaticGump<BarkeeperTitleGump>
{ {
private static readonly Entry[] m_Entries = private static readonly Entry[] _entries =
{ {
new("Alchemist"), new(1076083, "the alchemist"),
new("Animal Tamer"), new(1077244, "the animal tamer"),
new("Apothecary"), new(1078440, "the apothecary"),
new("Artist"), new(1078441, "the artist"),
new("Baker", true), new(1078442, "the baker", true),
new("Bard"), new(1073298, "the bard"),
new("Barkeep", "the barkeeper", true), new(1076102, "the barkeep", true),
new("Beggar"), new(1078443, "the beggar"),
new("Blacksmith"), new(1073297, "the blacksmith"),
new("Bounty Hunter"), new(1078444, "the bounty hunter"),
new("Brigand"), new(1078446, "the brigand"),
new("Butler"), new(1078447, "the butler"),
new("Carpenter"), new(1060774, "the carpenter"),
new("Chef", true), new(1078448, "the chef", true),
new("Commander"), new(1078449, "the commander"),
new("Curator"), new(1078450, "the curator"),
new("Drunkard"), new(1078451, "the drunkard"),
new("Farmer"), new(1078452, "the farmer"),
new("Fisherman"), new(1078453, "the fisherman"),
new("Gambler"), new(1078454, "the gambler"),
new("Gypsy"), new(1078455, "the gypsy"),
new("Herald"), new(1075996, "the herald"),
new("Herbalist"), new(1076107, "the herbalist"),
new("Hermit"), new(1078465, "the hermit"),
new("Innkeeper", true), new(1078466, "the innkeeper", true),
new("Jailor"), new(1078467, "the jailor"),
new("Jester"), new(1078468, "the jester"),
new("Librarian"), new(1078469, "the librarian"),
new("Mage"), new(1073292, "the mage"),
new("Mercenary"), new(1078470, "the mercenary"),
new("Merchant"), new(1060775, "the merchant"),
new("Messenger"), new(1078472, "the messenger"),
new("Miner"), new(1076093, "the miner"),
new("Monk"), new(1078475, "the monk"),
new("Noble"), new(1078476, "the noble"),
new("Paladin"), new(1073290, "the paladin"),
new("Peasant"), new(1078479, "the peasant"),
new("Pirate"), new(1078480, "the pirate"),
new("Prisoner"), new(1078481, "the prisoner"),
new("Prophet"), new(1078482, "the prophet"),
new("Ranger"), new(1078484, "the ranger"),
new("Sage"), new(1078487, "the sage"),
new("Sailor"), new(1078488, "the sailor"),
new("Scholar"), new(1078489, "the scholar"),
new("Scribe"), new(1060773, "the scribe"),
new("Sentry"), new(1078490, "the sentry"),
new("Servant"), new(1060795, "the servant"),
new("Shepherd"), new(1078491, "the shepherd"),
new("Soothsayer"), new(1078492, "the soothsayer"),
new("Stoic"), new(1078493, "the stoic"),
new("Storyteller"), new(1078494, "the storyteller"),
new("Tailor"), new(1076134, "the tailor"),
new("Thief"), new(1076096, "the thief"),
new("Tinker"), new(1076137, "the tinker"),
new("Town Crier"), new(1076097, "the town crier"),
new("Treasure Hunter"), new(1073291, "the treasure hunter"),
new("Waiter", true), new(1076112, "the waiter", true),
new("Warrior"), new(1077242, "the warrior"),
new("Watchman"), new(1078496, "the watchman"),
new("No Title", null) new(1078495, null) // No Title
}; };
private readonly PlayerBarkeeper m_Barkeeper; private static int _pageCount = (_entries.Length + 19) / 20;
private readonly Mobile m_From;
private readonly PlayerBarkeeper _barkeeper;
private readonly Mobile _from;
public override bool Singleton => true; public override bool Singleton => true;
public BarkeeperTitleGump(Mobile from, PlayerBarkeeper barkeeper) : base(0, 0) protected override void BuildLayout(ref StaticGumpBuilder builder)
{ {
m_From = from; RenderBackground(ref builder);
m_Barkeeper = barkeeper;
from.CloseGump<BarkeeperGump>(); for (var i = 0; i < _pageCount; ++i)
var entries = m_Entries;
RenderBackground();
var pageCount = (entries.Length + 19) / 20;
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); _from = from;
_barkeeper = barkeeper;
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");
} }
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); builder.AddHtmlLocalized(430, 70, 180, 25, pageCliloc);
AddHtml( }
120 + count / 10 * 260, else
100 + count % 10 * 30, {
entry.m_Vendor ? 148 : 180, // 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, 25,
entry.m_Description, entry.Description,
true true
); );
if (entry.m_Vendor) if (entry.Vendor)
{ {
AddImage(270 + count / 10 * 260, 98 + count % 10 * 30, 2151); builder.AddImage(270 + xOffset * 260, 98 + yOffset * 30, 2151);
AddItem(262 + count / 10 * 260, 106 + count % 10 * 30, 2543); builder.AddItem(262 + xOffset * 260, 106 + yOffset * 30, 2543);
} }
} }
AddButton(340, 400, 4005, 4007, 0, GumpButtonType.Page, 1 + (page + 1) % ((entries.Length + 19) / 20)); builder.AddButton(340, 400, 4005, 4007, 0, GumpButtonType.Page, currentPage + 1 % _pageCount);
AddHtml(380, 400, 180, 25, "More Job Titles"); builder.AddHtmlLocalized(380, 400, 180, 25, 1078456); // More Job Titles
AddButton(338, 437, 4014, 4016, 1); builder.AddButton(338, 437, 4014, 4016, 1);
AddHtml(290, 440, 35, 40, "Back"); builder.AddHtmlLocalized(290, 440, 35, 40, 1005007); // Back
} }
public override void OnResponse(NetState sender, in RelayInfo info) public override void OnResponse(NetState sender, in RelayInfo info)
{ {
if (_barkeeper.Deleted)
{
return;
}
var buttonID = info.ButtonID; var buttonID = info.ButtonID;
if (buttonID > 0) if (buttonID-- <= 0)
{ {
--buttonID; return;
}
if (buttonID > 0) if (buttonID-- <= 0)
{ {
--buttonID; _barkeeper.CancelChangeTitle(_from);
return;
}
if (buttonID < m_Entries.Length) if (buttonID < _entries.Length)
{ {
m_Barkeeper.EndChangeTitle(m_From, m_Entries[buttonID].m_Title, m_Entries[buttonID].m_Vendor); var entry = _entries[buttonID];
} _barkeeper.EndChangeTitle(_from, entry.Title, entry.Vendor);
}
else
{
m_Barkeeper.CancelChangeTitle(m_From);
}
} }
} }
private class Entry private class Entry
{ {
public readonly string m_Description; public readonly int Description;
public readonly string m_Title; public readonly string Title;
public readonly bool m_Vendor; 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)
{ {
} Description = desc;
Title = title;
public Entry(string desc, string title, bool vendor = false) Vendor = vendor;
{
m_Description = desc;
m_Title = title;
m_Vendor = vendor;
} }
} }
} }

View file

@ -1140,6 +1140,11 @@ namespace Server.Spells
protected override void OnTick() protected override void OnTick()
{ {
if (m_Target.Deleted || !m_Target.Alive)
{
return;
}
Damage( Damage(
m_Spell, m_Spell,
TimeSpan.Zero, TimeSpan.Zero,