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 Xunit;
namespace Server.Tests
namespace Server.Tests;
[Collection("Sequential Tests")]
public class TimerTests : IClassFixture<ServerFixture>
{
[Collection("Sequential Tests")]
public class TimerTests : IClassFixture<ServerFixture>
[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();
}
}
}

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.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<byte> 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<byte>((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<byte>(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);

View file

@ -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));

View file

@ -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;
}
}

View file

@ -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;

View file

@ -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
}

View file

@ -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<AcceptProtectorGump>
{
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 <a href="?ForceTopic88">protection</a>:
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);
}
}
}

View file

@ -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;
/// <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>
/// <returns>Boolean indicating the ability can trigger.</returns>
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
/// <summary>
/// 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)
/// </summary>
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;
}

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("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<BarkeeperGump>();
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;
}
}
}

View file

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