diff --git a/Projects/Server.Tests/Tests/Gumps/TestGumps/DynamicTestGump.cs b/Projects/Server.Tests/Tests/Gumps/TestGumps/DynamicTestGump.cs
new file mode 100644
index 000000000..9ad3e0ae2
--- /dev/null
+++ b/Projects/Server.Tests/Tests/Gumps/TestGumps/DynamicTestGump.cs
@@ -0,0 +1,35 @@
+using Server.Gumps;
+
+namespace Server.Tests.Gumps;
+
+public class DynamicTestGump : DynamicGump
+{
+ private readonly string _petName;
+
+ public DynamicTestGump(string petName) : base(50, 50)
+ {
+ _petName = petName;
+ Serial = (Serial)0x123;
+ TypeID = 0x5345;
+ }
+
+ protected override void BuildLayout(ref DynamicGumpBuilder builder)
+ {
+ builder.AddPage();
+
+ builder.AddBackground(10, 10, 265, 140, 0x242C);
+
+ builder.AddItem(205, 40, 0x4);
+ builder.AddItem(227, 40, 0x5);
+
+ builder.AddItem(180, 78, 0xCAE);
+ builder.AddItem(195, 90, 0xCAD);
+ builder.AddItem(218, 95, 0xCB0);
+
+ builder.AddHtml(30, 30, 150, 75, "
Wilt thou sanctify the resurrection of:
");
+ builder.AddHtml(30, 70, 150, 25, $"{_petName}", true);
+
+ builder.AddButton(40, 105, 0x81A, 0x81B, 0x1); // Okay
+ builder.AddButton(110, 105, 0x819, 0x818, 0x2); // Cancel
+ }
+}
diff --git a/Projects/Server.Tests/Tests/Gumps/TestGumps/LegacyTestGump.cs b/Projects/Server.Tests/Tests/Gumps/TestGumps/LegacyTestGump.cs
new file mode 100644
index 000000000..668d7e8ae
--- /dev/null
+++ b/Projects/Server.Tests/Tests/Gumps/TestGumps/LegacyTestGump.cs
@@ -0,0 +1,29 @@
+using Server.Gumps;
+
+namespace Server.Tests.Gumps;
+
+public sealed class LegacyTestGump : Gump
+{
+ public LegacyTestGump(string petName) : base(50, 50)
+ {
+ Serial = (Serial)0x123;
+ TypeID = 0x5345;
+
+ AddPage(0);
+
+ AddBackground(10, 10, 265, 140, 0x242C);
+
+ AddItem(205, 40, 0x4);
+ AddItem(227, 40, 0x5);
+
+ AddItem(180, 78, 0xCAE);
+ AddItem(195, 90, 0xCAD);
+ AddItem(218, 95, 0xCB0);
+
+ AddHtml(30, 30, 150, 75, "Wilt thou sanctify the resurrection of:
");
+ AddHtml(30, 70, 150, 25, $"{petName}", true);
+
+ AddButton(40, 105, 0x81A, 0x81B, 0x1); // Okay
+ AddButton(110, 105, 0x819, 0x818, 0x2); // Cancel
+ }
+}
diff --git a/Projects/Server.Tests/Tests/Gumps/TestGumps/StaticLayoutTestGump.cs b/Projects/Server.Tests/Tests/Gumps/TestGumps/StaticLayoutTestGump.cs
new file mode 100644
index 000000000..c80f88bb0
--- /dev/null
+++ b/Projects/Server.Tests/Tests/Gumps/TestGumps/StaticLayoutTestGump.cs
@@ -0,0 +1,41 @@
+using System;
+using Server.Gumps;
+
+namespace Server.Tests.Gumps;
+
+public class StaticLayoutTestGump : StaticGump
+{
+ private readonly string _petName;
+
+ public StaticLayoutTestGump(string petName) : base(50, 50)
+ {
+ _petName = petName;
+ Serial = (Serial)0x123;
+ TypeID = 0x5345;
+ }
+
+ protected override void BuildLayout(ref StaticGumpBuilder builder)
+ {
+ builder.AddPage();
+
+ builder.AddBackground(10, 10, 265, 140, 0x242C);
+
+ builder.AddItem(205, 40, 0x4);
+ builder.AddItem(227, 40, 0x5);
+
+ builder.AddItem(180, 78, 0xCAE);
+ builder.AddItem(195, 90, 0xCAD);
+ builder.AddItem(218, 95, 0xCB0);
+
+ builder.AddHtml(30, 30, 150, 75, "Wilt thou sanctify the resurrection of:
");
+ builder.AddHtmlPlaceholder(30, 70, 150, 25, "petName", true);
+
+ builder.AddButton(40, 105, 0x81A, 0x81B, 0x1); // Okay
+ builder.AddButton(110, 105, 0x819, 0x818, 0x2); // Cancel
+ }
+
+ protected override void BuildStrings(ref GumpStringsBuilder builder)
+ {
+ builder.SetStringSlot("petName", $"{_petName}");
+ }
+}
diff --git a/Projects/Server.Tests/Tests/Gumps/TestGumps/StaticTestGump.cs b/Projects/Server.Tests/Tests/Gumps/TestGumps/StaticTestGump.cs
new file mode 100644
index 000000000..ce616116e
--- /dev/null
+++ b/Projects/Server.Tests/Tests/Gumps/TestGumps/StaticTestGump.cs
@@ -0,0 +1,32 @@
+using Server.Gumps;
+
+namespace Server.Tests.Gumps;
+
+public class StaticTestGump : StaticGump
+{
+ public StaticTestGump() : base(50, 50)
+ {
+ Serial = (Serial)0x123;
+ TypeID = 0x5345;
+ }
+
+ protected override void BuildLayout(ref StaticGumpBuilder builder)
+ {
+ builder.AddPage();
+
+ builder.AddBackground(10, 10, 265, 140, 0x242C);
+
+ builder.AddItem(205, 40, 0x4);
+ builder.AddItem(227, 40, 0x5);
+
+ builder.AddItem(180, 78, 0xCAE);
+ builder.AddItem(195, 90, 0xCAD);
+ builder.AddItem(218, 95, 0xCB0);
+
+ builder.AddHtml(30, 30, 150, 75, "Wilt thou sanctify the resurrection of:
");
+ builder.AddHtml(30, 70, 150, 25, "Test", true);
+
+ builder.AddButton(40, 105, 0x81A, 0x81B, 0x1); // Okay
+ builder.AddButton(110, 105, 0x819, 0x818, 0x2); // Cancel
+ }
+}
diff --git a/Projects/Server.Tests/Tests/Gumps/TestLayoutGumps.cs b/Projects/Server.Tests/Tests/Gumps/TestLayoutGumps.cs
new file mode 100644
index 000000000..8cea30251
--- /dev/null
+++ b/Projects/Server.Tests/Tests/Gumps/TestLayoutGumps.cs
@@ -0,0 +1,143 @@
+using System;
+using System.Buffers;
+using System.IO;
+using Server.Gumps;
+using Server.Network;
+using Server.Tests.Network;
+using Xunit;
+
+namespace Server.Tests.Gumps;
+
+[Collection("Sequential Tests")]
+public class TestLayoutGumps
+{
+ [Fact]
+ public void TestDynamicGumpPacket()
+ {
+ var legacyGump = new LegacyTestGump("Test");
+ var legacyPacketData = legacyGump.Compile().Compile();
+
+ var staticGump = new DynamicTestGump("Test");
+ var buffer = GC.AllocateUninitializedArray(512);
+ var writer = new SpanWriter(buffer);
+ staticGump.CreatePacket(ref writer);
+
+ AssertThat.Equal(writer.Span, legacyPacketData);
+ }
+
+ [Fact]
+ public void TestStaticLayoutGumpPacket()
+ {
+ var expectedLayout =
+ "{ page 0 }{ resizepic 10 10 9260 265 140 }{ tilepic 205 40 4 }{ tilepic 227 40 5 }{ tilepic 180 78 3246 }{ tilepic 195 90 3245 }{ tilepic 218 95 3248 }{ htmlgump 30 30 150 75 1 0 0 }{ htmlgump 30 70 150 25 00002 1 0 }{ button 40 105 2074 2075 1 0 1 }{ button 110 105 2073 2072 1 0 2 }\0"u8;
+
+ string[] strings =
+ [
+ "Wilt thou sanctify the resurrection of:
",
+ "Test"
+ ];
+
+ InternalTestStaticGump(expectedLayout, new StaticLayoutTestGump("Test"), strings);
+ }
+
+ [Fact]
+ public void TestStaticGumpPacket()
+ {
+ var expectedLayout =
+ "{ page 0 }{ resizepic 10 10 9260 265 140 }{ tilepic 205 40 4 }{ tilepic 227 40 5 }{ tilepic 180 78 3246 }{ tilepic 195 90 3245 }{ tilepic 218 95 3248 }{ htmlgump 30 30 150 75 1 0 0 }{ htmlgump 30 70 150 25 2 1 0 }{ button 40 105 2074 2075 1 0 1 }{ button 110 105 2073 2072 1 0 2 }\0"u8;
+
+ string[] strings =
+ [
+ "Wilt thou sanctify the resurrection of:
",
+ "Test"
+ ];
+
+ InternalTestStaticGump(expectedLayout, new StaticTestGump(), strings);
+ }
+
+ [Fact]
+ public void TestStaticGumpIsCached()
+ {
+ var gump = new CachedGump();
+ var buffer = GC.AllocateUninitializedArray(512);
+ var writer = new SpanWriter(buffer);
+ gump.CreatePacket(ref writer);
+
+ var packet = writer.Span.ToArray();
+
+ // Reset the writer
+ writer.Seek(0, SeekOrigin.Begin);
+
+ // Second call should not call BuildLayout
+ gump.CreatePacket(ref writer);
+
+ AssertThat.Equal(writer.Span, packet);
+ }
+
+ private static void InternalTestStaticGump(ReadOnlySpan expectedLayout, StaticGump staticGump, string[] strings)
+ where T : StaticGump
+ {
+ // Expected layout
+ var expectedBuffer = GC.AllocateUninitializedArray(512);
+ var expectedBufferWriter = new SpanWriter(expectedBuffer);
+ OutgoingGumpPackets.WritePacked(expectedLayout, ref expectedBufferWriter);
+ var layoutLength = expectedBufferWriter.BytesWritten;
+
+ var buffer = GC.AllocateUninitializedArray(512);
+ var writer = new SpanWriter(buffer);
+ staticGump.CreatePacket(ref writer);
+
+ // Assert layout is exactly what we are expecting
+ AssertThat.Equal(writer.Span.Slice(19, layoutLength), expectedBufferWriter.Span);
+
+ // Assert strings count
+ AssertThat.Equal(writer.Span.Slice(19 + layoutLength, 4), stackalloc byte[] { 0, 0, 0, 3 });
+
+ var expectedStringsBuffer = GC.AllocateUninitializedArray(512);
+ var expectedStringsWriter = new SpanWriter(expectedStringsBuffer);
+
+ // Empty string
+ expectedStringsWriter.Write((ushort)0);
+
+ // loop through the strings, write them to the strings writer
+ foreach (var str in strings)
+ {
+ expectedStringsWriter.Write((ushort)str.Length);
+ expectedStringsWriter.WriteBigUni(str);
+ }
+
+ // Reset buffer
+ expectedBufferWriter.Seek(0, SeekOrigin.Begin);
+
+ OutgoingGumpPackets.WritePacked(expectedStringsWriter.Span, ref expectedBufferWriter);
+
+ // Assert strings are exactly what we are expecting
+ AssertThat.Equal(writer.Span[(19 + layoutLength + 4)..], expectedBufferWriter.Span);
+ }
+
+ private class CachedGump : StaticGump
+ {
+ private bool _isCachedLayout;
+
+ public CachedGump() : base(50, 50)
+ {
+ Serial = (Serial)0x124;
+ TypeID = 0x5346;
+ }
+
+ protected override void BuildLayout(ref StaticGumpBuilder builder)
+ {
+ Assert.False(_isCachedLayout);
+ _isCachedLayout = true;
+
+ builder.AddPage();
+
+ builder.AddHtml(30, 30, 150, 75, "Some text");
+ }
+
+ protected override void BuildStrings(ref GumpStringsBuilder builder)
+ {
+ Assert.Fail("BuildStrings should not be called when the layout is cached.");
+ }
+ }
+}
diff --git a/Projects/Server.Tests/Tests/Network/Packets/Outgoing/GumpPacketTests.cs b/Projects/Server.Tests/Tests/Network/Packets/Outgoing/GumpPacketTests.cs
index e19af19e3..56a8c0ead 100644
--- a/Projects/Server.Tests/Tests/Network/Packets/Outgoing/GumpPacketTests.cs
+++ b/Projects/Server.Tests/Tests/Network/Packets/Outgoing/GumpPacketTests.cs
@@ -4,6 +4,7 @@ using Xunit;
namespace Server.Tests.Network;
+[Collection("Sequential Tests")]
public class GumpPacketTests : IClassFixture
{
[Theory]
diff --git a/Projects/Server/Buffers/RawInterpolatedStringHandler.cs b/Projects/Server/Buffers/RawInterpolatedStringHandler.cs
index 699b61792..5f29c594d 100644
--- a/Projects/Server/Buffers/RawInterpolatedStringHandler.cs
+++ b/Projects/Server/Buffers/RawInterpolatedStringHandler.cs
@@ -8,7 +8,7 @@ using System.Runtime.CompilerServices;
namespace Server.Buffers;
-/// Provides a handler to interpolate strings which UNSAFELY exposes it's internal character span.
+/// Provides a handler to interpolate strings which UNSAFELY exposes its internal character span.
[InterpolatedStringHandler]
public ref struct RawInterpolatedStringHandler
{
diff --git a/Projects/Server/Gumps/BaseGump.cs b/Projects/Server/Gumps/BaseGump.cs
new file mode 100644
index 000000000..9b3fc5d65
--- /dev/null
+++ b/Projects/Server/Gumps/BaseGump.cs
@@ -0,0 +1,73 @@
+/*************************************************************************
+ * ModernUO *
+ * Copyright 2019-2024 - ModernUO Development Team *
+ * Email: hi@modernuo.com *
+ * File: BaseGump.cs *
+ * *
+ * This program is free software: you can redistribute it and/or modify *
+ * it under the terms of the GNU General Public License as published by *
+ * the Free Software Foundation, either version 3 of the License, or *
+ * (at your option) any later version. *
+ * *
+ * You should have received a copy of the GNU General Public License *
+ * along with this program. If not, see . *
+ *************************************************************************/
+
+using Server.Network;
+using System;
+using System.Runtime.CompilerServices;
+
+namespace Server.Gumps;
+
+public abstract class BaseGump
+{
+ private static Serial nextSerial = (Serial)1;
+
+ public int TypeID { get; protected set; }
+ public Serial Serial { get; protected set; }
+
+ public abstract int Switches { get; }
+ public abstract int TextEntries { get; }
+
+ public int X { get; set; }
+
+ public int Y { get; set; }
+
+ public BaseGump(int x, int y) : this()
+ {
+ X = x;
+ Y = y;
+ }
+
+ public BaseGump()
+ {
+ Serial = nextSerial++;
+ TypeID = GetTypeId(GetType());
+ }
+
+ public abstract void SendTo(NetState ns);
+
+ public virtual void OnResponse(NetState sender, in RelayInfo info)
+ {
+ }
+
+ public virtual void OnServerClose(NetState owner)
+ {
+ }
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static int GetTypeId(Type type)
+ {
+ unchecked
+ {
+ // To use the original .NET Framework deterministic hash code (with really terrible performance)
+ // change the next line to use HashUtility.GetNetFrameworkHashCode
+ var hash = (int)HashUtility.ComputeHash32(type?.FullName);
+
+ const int primeMulti = 0x108B76F1;
+
+ // Virtue Gump
+ return hash == 461 ? hash * primeMulti : hash;
+ }
+ }
+}
diff --git a/Projects/Server/Gumps/DynamicGump.cs b/Projects/Server/Gumps/DynamicGump.cs
new file mode 100644
index 000000000..ab45e949f
--- /dev/null
+++ b/Projects/Server/Gumps/DynamicGump.cs
@@ -0,0 +1,77 @@
+/*************************************************************************
+ * ModernUO *
+ * Copyright 2019-2024 - ModernUO Development Team *
+ * Email: hi@modernuo.com *
+ * File: DynamicGump.cs *
+ * *
+ * This program is free software: you can redistribute it and/or modify *
+ * it under the terms of the GNU General Public License as published by *
+ * the Free Software Foundation, either version 3 of the License, or *
+ * (at your option) any later version. *
+ * *
+ * You should have received a copy of the GNU General Public License *
+ * along with this program. If not, see . *
+ *************************************************************************/
+
+using System;
+using System.Buffers;
+using System.IO;
+using Server.Network;
+
+namespace Server.Gumps;
+
+public abstract class DynamicGump : BaseGump
+{
+ private static readonly byte[] _packetBuffer = GC.AllocateUninitializedArray(0x10000);
+
+ private int _switches;
+ private int _textEntries;
+
+ public override int Switches => _switches;
+ public override int TextEntries => _textEntries;
+
+ public DynamicGump(int x, int y) : base(x, y)
+ {
+ }
+
+ protected abstract void BuildLayout(ref DynamicGumpBuilder builder);
+
+ public void CreatePacket(ref SpanWriter writer)
+ {
+ writer.Write((byte)0xDD); // Packet ID
+ writer.Seek(2, SeekOrigin.Current);
+
+ writer.Write(Serial);
+ writer.Write(TypeID);
+ writer.Write(X);
+ writer.Write(Y);
+
+ DynamicGumpBuilder gumpBuilder = new DynamicGumpBuilder();
+ BuildLayout(ref gumpBuilder);
+ gumpBuilder.FinalizeLayout();
+
+ _switches = gumpBuilder.Switches;
+ _textEntries = gumpBuilder.TextEntries;
+
+ OutgoingGumpPackets.WritePacked(gumpBuilder.LayoutData, ref writer);
+
+ writer.Write(gumpBuilder._stringsCount);
+ OutgoingGumpPackets.WritePacked(gumpBuilder.StringsData, ref writer);
+
+ gumpBuilder.Dispose();
+
+ writer.WritePacketLength();
+ }
+
+ public override void SendTo(NetState ns)
+ {
+ ns.AddGump(this);
+
+ var writer = new SpanWriter(_packetBuffer);
+ CreatePacket(ref writer);
+
+ ns.Send(writer.Span);
+
+ writer.Dispose();
+ }
+}
diff --git a/Projects/Server/Gumps/DynamicGumpBuilder.cs b/Projects/Server/Gumps/DynamicGumpBuilder.cs
new file mode 100644
index 000000000..ecf129205
--- /dev/null
+++ b/Projects/Server/Gumps/DynamicGumpBuilder.cs
@@ -0,0 +1,392 @@
+/*************************************************************************
+ * ModernUO *
+ * Copyright 2019-2024 - ModernUO Development Team *
+ * Email: hi@modernuo.com *
+ * File: DynamicGumpBuilder.cs *
+ * *
+ * This program is free software: you can redistribute it and/or modify *
+ * it under the terms of the GNU General Public License as published by *
+ * the Free Software Foundation, either version 3 of the License, or *
+ * (at your option) any later version. *
+ * *
+ * You should have received a copy of the GNU General Public License *
+ * along with this program. If not, see . *
+ *************************************************************************/
+
+using System;
+using System.Buffers.Binary;
+using System.Runtime.CompilerServices;
+using Server.Buffers;
+using Server.Text;
+
+namespace Server.Gumps;
+
+public ref struct DynamicGumpBuilder
+{
+ private static readonly byte[] _staticStringsBuffer = GC.AllocateUninitializedArray(0x80000);
+
+ private byte[] _stringsBuffer;
+ private int _stringBytesWritten;
+ internal int _stringsCount;
+ private GumpLayoutBuilder _gumpBuilder;
+
+ public ReadOnlySpan LayoutData => _gumpBuilder.LayoutData;
+
+ public ReadOnlySpan StringsData => _stringsBuffer.AsSpan(0, _stringBytesWritten);
+
+ public int Switches => _gumpBuilder._switches;
+ public int TextEntries => _gumpBuilder._textEntries;
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public DynamicGumpBuilder()
+ {
+ _stringsBuffer = _staticStringsBuffer;
+ _gumpBuilder = new GumpLayoutBuilder();
+ }
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public void SetNoClose() => _gumpBuilder.SetNoClose();
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public void SetNoMove() => _gumpBuilder.SetNoMove();
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public void SetNoResize() => _gumpBuilder.SetNoResize();
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public void SetNoDispose() => _gumpBuilder.SetNoDispose();
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public void AddAlphaRegion(int x, int y, int width, int height) =>
+ _gumpBuilder.AddAlphaRegion(x, y, width, height);
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public void AddBackground(int x, int y, int width, int height, int gumpID) =>
+ _gumpBuilder.AddBackground(x, y, width, height, gumpID);
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public void AddButton(
+ int x, int y, int normalID, int pressedId, int buttonId, GumpButtonType type = GumpButtonType.Reply, int param = 0
+ ) => _gumpBuilder.AddButton(x, y, normalID, pressedId, buttonId, type, param);
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public void AddCheckbox(int x, int y, int inactiveID, int activeID, bool selected, int switchId) =>
+ _gumpBuilder.AddCheckbox(x, y, inactiveID, activeID, selected, switchId);
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public void AddGroup(int groupId) => _gumpBuilder.AddGroup(groupId);
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public void AddHtml(
+ int x,
+ int y,
+ int width,
+ int height,
+ ReadOnlySpan text,
+ bool background = false,
+ bool scrollbar = false
+ )
+ {
+ WriteInternalizedString(text);
+ _gumpBuilder.AddHtml(x, y, width, height, _stringsCount++, background, scrollbar);
+ }
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public void AddHtml(
+ int x, int y, int width, int height, ref RawInterpolatedStringHandler handler,
+ bool background = false, bool scrollbar = false
+ )
+ {
+ AddHtml(x, y, width, height, handler.Text, background, scrollbar);
+ handler.Clear();
+ }
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public void AddHtml(
+ int x,
+ int y,
+ int width,
+ int height,
+ int color,
+ ReadOnlySpan text,
+ bool background = false,
+ bool scrollbar = false
+ ) => AddHtml(x, y, width, height, color, $"{text}", background, scrollbar);
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public void AddHtml(
+ int x,
+ int y,
+ int width,
+ int height,
+ int color,
+ ref RawInterpolatedStringHandler handler,
+ bool background = false,
+ bool scrollbar = false
+ )
+ {
+ AddHtml(x, y, width, height, color, handler.Text, background, scrollbar);
+ handler.Clear();
+ }
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public void AddHtmlCentered(
+ int x, int y, int width, int height, ReadOnlySpan text, bool background = false, bool scrollbar = false
+ ) => AddHtml(x, y, width, height, $"{text}", background, scrollbar);
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public void AddHtmlCentered(
+ int x,
+ int y,
+ int width,
+ int height,
+ ref RawInterpolatedStringHandler handler,
+ bool background = false,
+ bool scrollbar = false
+ )
+ {
+ AddHtml(x, y, width, height, $"{handler.Text}", background, scrollbar);
+ handler.Clear();
+ }
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public void AddHtmlCentered(
+ int x,
+ int y,
+ int width,
+ int height,
+ int color,
+ ReadOnlySpan text,
+ bool background = false,
+ bool scrollbar = false
+ ) => AddHtml(x, y, width, height, color, $"{text}", background, scrollbar);
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public void AddHtmlCentered(
+ int x,
+ int y,
+ int width,
+ int height,
+ int color,
+ ref RawInterpolatedStringHandler handler,
+ bool background = false,
+ bool scrollbar = false
+ )
+ {
+ AddHtmlCentered(x, y, width, height, color, handler.Text, background, scrollbar);
+ handler.Clear();
+ }
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public void AddHtmlLocalized(
+ int x, int y, int width, int height, int number, bool background = false, bool scrollbar = false
+ ) => _gumpBuilder.AddHtmlLocalized(x, y, width, height, number, background, scrollbar);
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public void AddHtmlLocalized(
+ int x, int y, int width, int height, int number, short color, bool background = false, bool scrollbar = false
+ ) => _gumpBuilder.AddHtmlLocalized(x, y, width, height, number, color, background, scrollbar);
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public void AddHtmlLocalized(
+ int x,
+ int y,
+ int width,
+ int height,
+ int number,
+ ReadOnlySpan args,
+ int color,
+ bool background = false,
+ bool scrollbar = false
+ ) => _gumpBuilder.AddHtmlLocalized(x, y, width, height, number, args, color, background, scrollbar);
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public void AddHtmlLocalized(
+ int x, int y, int width, int height, int number, ref RawInterpolatedStringHandler handler, int color,
+ bool background = false, bool scrollbar = false
+ ) => _gumpBuilder.AddHtmlLocalized(x, y, width, height, number, ref handler, color, background, scrollbar);
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public void AddImage(int x, int y, int gumpId, int hue = 0, ReadOnlySpan cls = default) =>
+ _gumpBuilder.AddImage(x, y, gumpId, hue, cls);
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public void AddImage(int x, int y, int gumpId, ref RawInterpolatedStringHandler handler) =>
+ _gumpBuilder.AddImage(x, y, gumpId, ref handler);
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public void AddImage(int x, int y, int gumpId, int hue, ref RawInterpolatedStringHandler handler) =>
+ _gumpBuilder.AddImage(x, y, gumpId, hue, ref handler);
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public void AddImageTiledButton(
+ int x,
+ int y,
+ int normalId,
+ int pressedId,
+ int buttonId,
+ GumpButtonType type,
+ int param,
+ int itemId,
+ int hue,
+ int width,
+ int height,
+ int localizedTooltip = -1
+ ) => _gumpBuilder.AddImageTiledButton(
+ x, y, normalId, pressedId, buttonId, type, param, itemId, hue, width, height, localizedTooltip
+ );
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public void AddImageTiled(int x, int y, int width, int height, int gumpId) =>
+ _gumpBuilder.AddImageTiled(x, y, width, height, gumpId);
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public void AddItem(int x, int y, int itemId, int hue = 0) => _gumpBuilder.AddItem(x, y, itemId, hue);
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public void AddItemProperty(Serial serial) => _gumpBuilder.AddItemProperty(serial);
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public void AddLabel(int x, int y, int hue, ReadOnlySpan text)
+ {
+ WriteInternalizedString(text);
+ _gumpBuilder.AddLabel(x, y, hue, _stringsCount++);
+ }
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public void AddLabel(int x, int y, int hue, ref RawInterpolatedStringHandler handler)
+ {
+ AddLabel(x, y, hue, handler.Text);
+ handler.Clear();
+ }
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public void AddLabelCropped(int x, int y, int width, int height, int hue, ReadOnlySpan text)
+ {
+ WriteInternalizedString(text);
+ _gumpBuilder.AddLabelCropped(x, y, width, height, hue, _stringsCount++);
+ }
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public void AddLabelCropped(int x, int y, int width, int height, int hue, ref RawInterpolatedStringHandler handler)
+ {
+ AddLabelCropped(x, y, width, height, hue, handler.Text);
+ handler.Clear();
+ }
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public void AddGumpIdOverride(int gumpId) => _gumpBuilder.AddGumpIdOverride(gumpId);
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public void AddPage(int page = 0) => _gumpBuilder.AddPage(page);
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public void AddRadio(int x, int y, int inactiveId, int activeId, bool selected, int switchId) =>
+ _gumpBuilder.AddRadio(x, y, inactiveId, activeId, selected, switchId);
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public void AddSpriteImage(int x, int y, int gumpId, int width, int height, int sx, int sy) =>
+ _gumpBuilder.AddSpriteImage(x, y, gumpId, width, height, sx, sy);
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public void AddTextEntry(
+ int x, int y, int width, int height, int hue, int entryId, ReadOnlySpan initialText = default
+ )
+ {
+ WriteInternalizedString(initialText);
+ _gumpBuilder.AddTextEntry(x, y, width, height, hue, entryId, _stringsCount++);
+ }
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public void AddTextEntry(
+ int x, int y, int width, int height, int hue, int entryId, ref RawInterpolatedStringHandler handler
+ )
+ {
+ AddTextEntry(x, y, width, height, hue, entryId, handler.Text);
+ handler.Clear();
+ }
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public void AddTextEntryLimited(
+ int x, int y, int width, int height, int hue, int entryId, ReadOnlySpan initialText = default, int size = 0
+ )
+ {
+ WriteInternalizedString(initialText);
+ _gumpBuilder.AddTextEntryLimited(x, y, width, height, hue, entryId, _stringsCount++, size);
+ }
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public void AddTextEntryLimited(
+ int x, int y, int width, int height, int hue, int entryId, ref RawInterpolatedStringHandler handler, int size = 0
+ )
+ {
+ AddTextEntryLimited(x, y, width, height, hue, entryId, handler.Text, size);
+ handler.Clear();
+ }
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public void AddTooltip(int number) => _gumpBuilder.AddTooltip(number);
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public void AddTooltip(int number, ReadOnlySpan args) => _gumpBuilder.AddTooltip(number, args);
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public void AddTooltip(int number, ref RawInterpolatedStringHandler handler)
+ {
+ _gumpBuilder.AddTooltip(number, handler.Text);
+ handler.Clear();
+ }
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public void FinalizeLayout() => _gumpBuilder.FinalizeLayout();
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ private void WriteInternalizedString(ReadOnlySpan text)
+ {
+ if (text.Length > ushort.MaxValue)
+ {
+ text = text[..ushort.MaxValue];
+ }
+
+ GrowStringsBufferIfNeeded(2 + text.Length * 2);
+ BinaryPrimitives.WriteUInt16BigEndian(_stringsBuffer.AsSpan(_stringBytesWritten), (ushort)text.Length);
+
+ _stringBytesWritten += 2;
+ _stringBytesWritten += text.GetBytesBigUni(_stringsBuffer.AsSpan(_stringBytesWritten));
+ }
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ private void GrowStringsBufferIfNeeded(int needed)
+ {
+ if (needed + _stringBytesWritten <= _stringsBuffer.Length)
+ {
+ return;
+ }
+
+ var newSize = Math.Max(_stringBytesWritten + needed, _stringsBuffer.Length * 2);
+ byte[] poolArray = STArrayPool.Shared.Rent(newSize);
+
+ _stringsBuffer.AsSpan(0, _stringBytesWritten).CopyTo(poolArray);
+
+ byte[] toReturn = _stringsBuffer;
+ _stringsBuffer = poolArray;
+
+ if (toReturn != null && toReturn != _staticStringsBuffer)
+ {
+ STArrayPool.Shared.Return(toReturn);
+ }
+ }
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public void Dispose()
+ {
+ _gumpBuilder.Dispose();
+
+ if (_stringsBuffer != _staticStringsBuffer)
+ {
+ STArrayPool.Shared.Return(_stringsBuffer);
+ }
+
+ this = default;
+ }
+}
diff --git a/Projects/Server/Gumps/GumpFlags.cs b/Projects/Server/Gumps/GumpFlags.cs
new file mode 100644
index 000000000..437e2b5ab
--- /dev/null
+++ b/Projects/Server/Gumps/GumpFlags.cs
@@ -0,0 +1,27 @@
+/*************************************************************************
+ * ModernUO *
+ * Copyright 2019-2023 - ModernUO Development Team *
+ * Email: hi@modernuo.com *
+ * File: GumpFlags.cs *
+ * *
+ * This program is free software: you can redistribute it and/or modify *
+ * it under the terms of the GNU General Public License as published by *
+ * the Free Software Foundation, either version 3 of the License, or *
+ * (at your option) any later version. *
+ * *
+ * You should have received a copy of the GNU General Public License *
+ * along with this program. If not, see . *
+ *************************************************************************/
+
+using System;
+
+namespace Server.Gumps;
+
+[Flags]
+public enum GumpFlags
+{
+ NoDispose = 0x1,
+ NoResize = 0x2,
+ NoMove = 0x4,
+ NoClose = 0x8
+}
diff --git a/Projects/Server/Gumps/GumpLayoutBuilder.cs b/Projects/Server/Gumps/GumpLayoutBuilder.cs
new file mode 100644
index 000000000..3d9d05605
--- /dev/null
+++ b/Projects/Server/Gumps/GumpLayoutBuilder.cs
@@ -0,0 +1,642 @@
+/*************************************************************************
+ * ModernUO *
+ * Copyright 2019-2024 - ModernUO Development Team *
+ * Email: hi@modernuo.com *
+ * File: GumpLayoutBuilder.cs *
+ * *
+ * This program is free software: you can redistribute it and/or modify *
+ * it under the terms of the GNU General Public License as published by *
+ * the Free Software Foundation, either version 3 of the License, or *
+ * (at your option) any later version. *
+ * *
+ * You should have received a copy of the GNU General Public License *
+ * along with this program. If not, see . *
+ *************************************************************************/
+
+using System;
+using System.Runtime.CompilerServices;
+using Server.Buffers;
+using Server.Text;
+
+namespace Server.Gumps;
+
+public ref struct GumpLayoutBuilder
+{
+ private static readonly byte[] _staticLayoutBuffer = GC.AllocateUninitializedArray(0x80000);
+
+ private byte[] _layoutBuffer;
+ private int _bytesWritten;
+
+ internal GumpFlags _flags;
+ internal int _switches;
+ internal int _textEntries;
+
+ internal Span LayoutData => _layoutBuffer.AsSpan(0, _bytesWritten);
+
+ // This struct uses static fields and is therefore not thread safe!
+ // Do not instantiate/process multiple gumps at the same time!
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public GumpLayoutBuilder() => _layoutBuffer = _staticLayoutBuffer;
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public void SetNoClose()
+ {
+ if ((_flags & GumpFlags.NoClose) == 0)
+ {
+ GrowIfNeeded(11);
+ Write("{ noclose }"u8);
+ _flags |= GumpFlags.NoClose;
+ }
+ }
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public void SetNoMove()
+ {
+ if ((_flags & GumpFlags.NoMove) == 0)
+ {
+ GrowIfNeeded(10);
+ Write("{ nomove }"u8);
+ _flags |= GumpFlags.NoMove;
+ }
+ }
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public void SetNoResize()
+ {
+ if ((_flags & GumpFlags.NoResize) == 0)
+ {
+ GrowIfNeeded(12);
+ Write("{ noresize }"u8);
+ _flags |= GumpFlags.NoResize;
+ }
+ }
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public void SetNoDispose()
+ {
+ if ((_flags & GumpFlags.NoDispose) == 0)
+ {
+ GrowIfNeeded(13);
+ Write("{ nodispose }"u8);
+ _flags |= GumpFlags.NoDispose;
+ }
+ }
+
+ public void AddAlphaRegion(int x, int y, int width, int height)
+ {
+ GrowIfNeeded(8 + 12 + 36);
+ WriteStart("checkertrans"u8);
+ WriteValue(x);
+ WriteValue(y);
+ WriteValue(width);
+ WriteValue(height);
+ WriteEnd();
+ }
+
+ public void AddBackground(int x, int y, int width, int height, int gumpId)
+ {
+ GrowIfNeeded(9 + 9 + 45);
+ WriteStart("resizepic"u8);
+ WriteValue(x);
+ WriteValue(y);
+ WriteValue(gumpId);
+ WriteValue(width);
+ WriteValue(height);
+ WriteEnd();
+ }
+
+ public void AddButton(
+ int x, int y, int normalId, int pressedId, int buttonId, GumpButtonType type = GumpButtonType.Reply, int param = 0
+ )
+ {
+ GrowIfNeeded(11 + 6 + 54 + 2);
+ WriteStart("button"u8);
+ WriteValue(x);
+ WriteValue(y);
+ WriteValue(normalId);
+ WriteValue(pressedId);
+ WriteValue(type == GumpButtonType.Reply);
+ WriteValue(param);
+ WriteValue(buttonId);
+ WriteEnd();
+ }
+
+ public void AddCheckbox(int x, int y, int inactiveId, int activeId, bool selected, int switchId)
+ {
+ GrowIfNeeded(10 + 8 + 45 + 2);
+ WriteStart("checkbox"u8);
+ WriteValue(x);
+ WriteValue(y);
+ WriteValue(inactiveId);
+ WriteValue(activeId);
+ WriteValue(selected);
+ WriteValue(switchId);
+ WriteEnd();
+
+ _switches++;
+ }
+
+ public void AddGroup(int groupId)
+ {
+ if (groupId == 1)
+ {
+ GrowIfNeeded(11);
+ Write("{ group 1 }"u8);
+ return;
+ }
+
+ GrowIfNeeded(5 + 7 + 9);
+ WriteStart("group"u8);
+ WriteValue(groupId);
+ WriteEnd();
+ }
+
+ public int AddHtmlPlaceholder(int x, int y, int width, int height,
+ bool background = false, bool scrollbar = false)
+ {
+ GrowIfNeeded(11 + 8 + 36 + 10);
+ WriteStart("htmlgump"u8);
+ WriteValue(x);
+ WriteValue(y);
+ WriteValue(width);
+ WriteValue(height);
+ var position = _bytesWritten;
+ Write(" "u8);
+ WriteValue(background);
+ WriteValue(scrollbar);
+ WriteEnd();
+
+ return position;
+ }
+
+ public void AddHtml(int x, int y, int width, int height, int text,
+ bool background = false, bool scrollbar = false)
+ {
+ GrowIfNeeded(11 + 8 + 45 + 4);
+ WriteStart("htmlgump"u8);
+ WriteValue(x);
+ WriteValue(y);
+ WriteValue(width);
+ WriteValue(height);
+ WriteValue(text);
+ WriteValue(background);
+ WriteValue(scrollbar);
+ WriteEnd();
+ }
+
+ public void AddHtmlLocalized(
+ int x, int y, int width, int height, int number, bool background = false, bool scrollbar = false
+ )
+ {
+ GrowIfNeeded(11 + 11 + 45 + 4);
+ WriteStart("xmfhtmlgump"u8);
+ WriteValue(x);
+ WriteValue(y);
+ WriteValue(width);
+ WriteValue(height);
+ WriteValue(number);
+ WriteValue(background);
+ WriteValue(scrollbar);
+ WriteEnd();
+ }
+
+ public void AddHtmlLocalized(
+ int x, int y, int width, int height, int number, short color, bool background = false, bool scrollbar = false
+ )
+ {
+ GrowIfNeeded(12 + 16 + 45 + 5 + 4);
+ WriteStart("xmfhtmlgumpcolor"u8);
+ WriteValue(x);
+ WriteValue(y);
+ WriteValue(width);
+ WriteValue(height);
+ WriteValue(number);
+ WriteValue(background);
+ WriteValue(scrollbar);
+ WriteValue(color);
+ WriteEnd();
+ }
+
+ public void AddHtmlLocalized(int x, int y, int width, int height, int number, ReadOnlySpan args, int color,
+ bool background = false, bool scrollbar = false)
+ {
+ GrowIfNeeded(12 + 10 + 45 + 5 + 4 + (args.Length > 0 ? 3 + args.Length : 0));
+ WriteStart("xmfhtmltok"u8);
+ WriteValue(x);
+ WriteValue(y);
+ WriteValue(width);
+ WriteValue(height);
+ WriteValue(background);
+ WriteValue(scrollbar);
+ WriteValue(color);
+ WriteValue(number);
+
+ if (args.Length > 0)
+ {
+ Write("@"u8);
+ WriteValue(args);
+ Write("@ "u8);
+ }
+ WriteEnd();
+ }
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public void AddHtmlLocalized(
+ int x, int y, int width, int height, int number, ref RawInterpolatedStringHandler handler, int color,
+ bool background = false, bool scrollbar = false
+ )
+ {
+ AddHtmlLocalized(x, y, width, height, number, handler.Text, color, background, scrollbar);
+
+ // Dispose of the handler
+ handler.Clear();
+ }
+
+ public void AddImage(int x, int y, int gumpId, int hue = 0, ReadOnlySpan cls = default)
+ {
+ GrowIfNeeded(7 + 7 + 36 + (hue != 0 ? 14 : 0) + (cls.Length > 0 ? 7 + cls.Length : 0));
+ WriteStart("gumppic"u8);
+ WriteValue(x);
+ WriteValue(y);
+ WriteValue(gumpId);
+
+ if (hue != 0)
+ {
+ Write("hue="u8);
+ WriteValue(hue);
+ Write(" "u8);
+ }
+
+ if (!cls.IsEmpty)
+ {
+ Write("class="u8);
+ WriteValue(cls);
+ Write(" "u8);
+ }
+
+ WriteEnd();
+ }
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public void AddImage(int x, int y, int gumpId, ref RawInterpolatedStringHandler handler)
+ {
+ AddImage(x, y, gumpId, 0, handler.Text);
+ handler.Clear();
+ }
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public void AddImage(int x, int y, int gumpId, int hue, ref RawInterpolatedStringHandler handler)
+ {
+ AddImage(x, y, gumpId, hue, handler.Text);
+ handler.Clear();
+ }
+
+ public void AddImageTiledButton(int x, int y, int normalId, int pressedId, int buttonId, GumpButtonType type, int param,
+ int itemId, int hue, int width, int height, int localizedTooltip = -1)
+ {
+ GrowIfNeeded(15 + 13 + 90 + 2);
+ WriteStart("buttontileart"u8);
+ WriteValue(x);
+ WriteValue(y);
+ WriteValue(normalId);
+ WriteValue(pressedId);
+ WriteValue(type == GumpButtonType.Reply);
+ WriteValue(param);
+ WriteValue(buttonId);
+ WriteValue(itemId);
+ WriteValue(hue);
+ WriteValue(width);
+ WriteValue(height);
+ WriteEnd();
+
+ if (localizedTooltip <= 0)
+ {
+ return;
+ }
+
+ AddTooltip(localizedTooltip);
+ }
+
+ public void AddImageTiled(int x, int y, int width, int height, int gumpId)
+ {
+ GrowIfNeeded(9 + 12 + 45);
+ WriteStart("gumppictiled"u8);
+ WriteValue(x);
+ WriteValue(y);
+ WriteValue(width);
+ WriteValue(height);
+ WriteValue(gumpId);
+ WriteEnd();
+ }
+
+ public void AddItem(int x, int y, int itemId, int hue = 0)
+ {
+ GrowIfNeeded(7 + 36 + (hue != 0 ? 20 : 7));
+ WriteStart(hue == 0 ? "tilepic"u8 : "tilepichue"u8);
+ WriteValue(x);
+ WriteValue(y);
+ WriteValue(itemId);
+
+ if (hue != 0)
+ {
+ WriteValue(hue);
+ }
+
+ WriteEnd();
+ }
+
+ public void AddItemProperty(Serial serial)
+ {
+ GrowIfNeeded(5 + 12 + 9);
+ WriteStart("itemproperty"u8);
+ WriteValue(serial.Value);
+ WriteEnd();
+ }
+
+ public int AddLabelPlaceholder(int x, int y, int hue)
+ {
+ GrowIfNeeded(8 + 4 + 27 + 6);
+ WriteStart("text"u8);
+ WriteValue(x);
+ WriteValue(y);
+ WriteValue(hue);
+ var position = _bytesWritten;
+ Write(" "u8);
+ WriteEnd();
+ return position;
+ }
+
+ public void AddLabel(int x, int y, int hue, int text)
+ {
+ GrowIfNeeded(8 + 4 + 36 + 6);
+ WriteStart("text"u8);
+ WriteValue(x);
+ WriteValue(y);
+ WriteValue(hue);
+ WriteValue(text);
+ WriteEnd();
+ }
+
+ public int AddLabelCroppedPlaceholder(int x, int y, int width, int height, int hue)
+ {
+ GrowIfNeeded(10 + 11 + 45 + 6);
+ WriteStart("croppedtext"u8);
+ WriteValue(x);
+ WriteValue(y);
+ WriteValue(width);
+ WriteValue(height);
+ WriteValue(hue);
+ var position = _bytesWritten;
+ Write(" "u8);
+ WriteEnd();
+ return position;
+ }
+
+ public void AddLabelCropped(int x, int y, int width, int height, int hue, int text)
+ {
+ GrowIfNeeded(10 + 11 + 54 + 6);
+ WriteStart("croppedtext"u8);
+ WriteValue(x);
+ WriteValue(y);
+ WriteValue(width);
+ WriteValue(height);
+ WriteValue(hue);
+ WriteValue(text);
+ WriteEnd();
+ }
+
+ public void AddGumpIdOverride(int gumpId)
+ {
+ GrowIfNeeded(5 + 10 + 9);
+ WriteStart("mastergump"u8);
+ WriteValue(gumpId);
+ WriteEnd();
+ }
+
+ public void AddPage(int page = 0)
+ {
+ if (page == 0)
+ {
+ GrowIfNeeded(10);
+ Write("{ page 0 }"u8);
+ return;
+ }
+
+ GrowIfNeeded(5 + 4 + 9);
+ WriteStart("page"u8);
+ WriteValue(page);
+ WriteEnd();
+ }
+
+ public void AddRadio(int x, int y, int inactiveId, int activeId, bool selected, int switchId)
+ {
+ GrowIfNeeded(10 + 5 + 45 + 2);
+ WriteStart("radio"u8);
+ WriteValue(x);
+ WriteValue(y);
+ WriteValue(inactiveId);
+ WriteValue(activeId);
+ WriteValue(selected);
+ WriteValue(switchId);
+ WriteEnd();
+
+ _switches++;
+ }
+
+ public void AddSpriteImage(int x, int y, int gumpId, int width, int height, int sx, int sy)
+ {
+ GrowIfNeeded(11 + 8 + 63);
+ WriteStart("picinpic"u8);
+ WriteValue(x);
+ WriteValue(y);
+ WriteValue(gumpId);
+ WriteValue(width);
+ WriteValue(height);
+ WriteValue(sx);
+ WriteValue(sy);
+ WriteEnd();
+ }
+
+ public int AddTextEntryPlaceholder(
+ int x, int y, int width, int height, int hue, int entryId
+ )
+ {
+ GrowIfNeeded(11 + 9 + 54 + 6);
+ WriteStart("textentry"u8);
+ WriteValue(x);
+ WriteValue(y);
+ WriteValue(width);
+ WriteValue(height);
+ WriteValue(hue);
+ WriteValue(entryId);
+ var position = _bytesWritten;
+ Write(" "u8);
+ WriteEnd();
+
+ _textEntries++;
+ return position;
+ }
+
+ public void AddTextEntry(
+ int x, int y, int width, int height, int hue, int entryId, int initialText
+ )
+ {
+ GrowIfNeeded(11 + 9 + 63 + 6);
+ WriteStart("textentry"u8);
+ WriteValue(x);
+ WriteValue(y);
+ WriteValue(width);
+ WriteValue(height);
+ WriteValue(hue);
+ WriteValue(entryId);
+ WriteValue(initialText);
+ WriteEnd();
+
+ _textEntries++;
+ }
+
+ public int AddTextEntryLimitedPlaceholder(
+ int x, int y, int width, int height, int hue, int entryId, int size = 0
+ )
+ {
+ GrowIfNeeded(12 + 16 + 63 + 6);
+ WriteStart("textentrylimited"u8);
+ WriteValue(x);
+ WriteValue(y);
+ WriteValue(width);
+ WriteValue(height);
+ WriteValue(hue);
+ WriteValue(entryId);
+ var position = _bytesWritten;
+ Write(" "u8);
+ WriteValue(size);
+ WriteEnd();
+
+ _textEntries++;
+ return position;
+ }
+
+ public void AddTextEntryLimited(
+ int x, int y, int width, int height, int hue, int entryId, int initialText, int size = 0
+ )
+ {
+ GrowIfNeeded(12 + 16 + 72);
+ WriteStart("textentrylimited"u8);
+ WriteValue(x);
+ WriteValue(y);
+ WriteValue(width);
+ WriteValue(height);
+ WriteValue(hue);
+ WriteValue(entryId);
+ WriteValue(initialText);
+ WriteValue(size);
+ WriteEnd();
+
+ _textEntries++;
+ }
+
+ public void AddTooltip(int number)
+ {
+ GrowIfNeeded(5 + 7 + 9);
+ WriteStart("tooltip"u8);
+ WriteValue(number);
+ WriteEnd();
+ }
+
+ public void AddTooltip(int number, ReadOnlySpan args)
+ {
+ GrowIfNeeded(5 + 7 + 9 + (args.Length > 0 ? 3 + args.Length : 0));
+ WriteStart("tooltip"u8);
+ WriteValue(number);
+
+ if (args.Length > 0)
+ {
+ Write("@"u8);
+ WriteValue(args);
+ Write("@ "u8);
+ }
+ WriteEnd();
+ }
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ private void WriteStart(ReadOnlySpan value)
+ {
+ Write("{ "u8);
+ Write(value);
+ Write(" "u8);
+ }
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ private void WriteEnd() => Write("}"u8);
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ private void WriteValue(bool value) => Write(value ? "1 "u8 : "0 "u8);
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ private void WriteValue(ReadOnlySpan value)
+ {
+ if (!value.IsEmpty)
+ {
+ _bytesWritten += value.GetBytesAscii(_layoutBuffer.AsSpan(_bytesWritten));
+ }
+ }
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ private void WriteValue(T value, ReadOnlySpan format = default) where T : IUtf8SpanFormattable
+ {
+ if (!value.TryFormat(_layoutBuffer.AsSpan(_bytesWritten), out int bytesWritten, format, null))
+ {
+ throw new InvalidOperationException($"Failed to format '{value}' with the given format '{format}'");
+ }
+
+ _bytesWritten += bytesWritten;
+ Write(" "u8);
+ }
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ private void Write(ReadOnlySpan span)
+ {
+ span.CopyTo(_layoutBuffer.AsSpan(_bytesWritten));
+ _bytesWritten += span.Length;
+ }
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ internal void FinalizeLayout()
+ {
+ GrowIfNeeded(1);
+ _layoutBuffer[_bytesWritten++] = 0;
+ }
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ private void GrowIfNeeded(int count)
+ {
+ if (_bytesWritten + count <= _layoutBuffer.Length)
+ {
+ return;
+ }
+
+ var newSize = Math.Max(_bytesWritten + count, _layoutBuffer.Length * 2);
+ byte[] poolArray = STArrayPool.Shared.Rent(newSize);
+
+ _layoutBuffer.AsSpan(0, _bytesWritten).CopyTo(poolArray);
+
+ byte[] toReturn = _layoutBuffer;
+ _layoutBuffer = poolArray;
+
+ if (toReturn != _staticLayoutBuffer)
+ {
+ STArrayPool.Shared.Return(toReturn);
+ }
+ }
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public void Dispose()
+ {
+ if (_layoutBuffer != _staticLayoutBuffer)
+ {
+ STArrayPool.Shared.Return(_layoutBuffer);
+ }
+
+ this = default;
+ }
+}
diff --git a/Projects/Server/Gumps/GumpStringsBuilder.cs b/Projects/Server/Gumps/GumpStringsBuilder.cs
new file mode 100644
index 000000000..497f8a094
--- /dev/null
+++ b/Projects/Server/Gumps/GumpStringsBuilder.cs
@@ -0,0 +1,127 @@
+/*************************************************************************
+ * ModernUO *
+ * Copyright 2019-2024 - ModernUO Development Team *
+ * Email: hi@modernuo.com *
+ * File: GumpStringsBuilder.cs *
+ * *
+ * This program is free software: you can redistribute it and/or modify *
+ * it under the terms of the GNU General Public License as published by *
+ * the Free Software Foundation, either version 3 of the License, or *
+ * (at your option) any later version. *
+ * *
+ * You should have received a copy of the GNU General Public License *
+ * along with this program. If not, see . *
+ *************************************************************************/
+
+using System;
+using System.Buffers.Binary;
+using System.Collections.Generic;
+using System.Runtime.CompilerServices;
+using Server.Buffers;
+using Server.Text;
+
+namespace Server.Gumps;
+
+public ref struct GumpStringsBuilder
+{
+ private static readonly byte[] _staticStringsBuffer = GC.AllocateUninitializedArray(0x80000);
+ private static readonly Dictionary _hashes = new();
+
+ private readonly bool _finalizeLayout;
+ private byte[] _stringsBuffer;
+ private int _stringBytesWritten;
+ internal int _stringsCount;
+
+ internal ReadOnlySpan StringsBuffer => _stringsBuffer.AsSpan(0, _stringBytesWritten);
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public GumpStringsBuilder(bool finalizeLayout)
+ {
+ _finalizeLayout = finalizeLayout;
+ _stringsBuffer = _staticStringsBuffer;
+ }
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ private void GrowStringsBufferIfNeeded(int needed)
+ {
+ var newLength = needed + _stringBytesWritten;
+ if (newLength <= _stringsBuffer.Length)
+ {
+ return;
+ }
+
+ var newSize = Math.Max(newLength, _stringsBuffer.Length * 2);
+ byte[] poolArray = STArrayPool.Shared.Rent(newSize);
+
+ _stringsBuffer.AsSpan(0, _stringBytesWritten).CopyTo(poolArray);
+
+ byte[] toReturn = _stringsBuffer;
+ _stringsBuffer = poolArray;
+
+ if (toReturn != _staticStringsBuffer)
+ {
+ STArrayPool.Shared.Return(toReturn);
+ }
+ }
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public void SetStringSlot(ReadOnlySpan slotKey, ref RawInterpolatedStringHandler handler)
+ {
+ SetStringSlot(slotKey, handler.Text);
+ handler.Clear();
+ }
+
+ public void SetStringSlot(ReadOnlySpan slotKey, ReadOnlySpan text)
+ {
+ var hash = HashUtility.ComputeHash64(slotKey);
+
+ if (text.Length > ushort.MaxValue)
+ {
+ text = text[..ushort.MaxValue];
+ }
+
+ GrowStringsBufferIfNeeded(2 + text.Length * 2);
+ BinaryPrimitives.WriteUInt16BigEndian(_stringsBuffer.AsSpan(_stringBytesWritten), (ushort)text.Length);
+ _stringBytesWritten += 2;
+
+ if (text.Length > 0)
+ {
+ _stringBytesWritten += text.GetBytesBigUni(_stringsBuffer.AsSpan(_stringBytesWritten));
+ }
+
+ if (_finalizeLayout)
+ {
+ _hashes[hash] = _stringsCount++;
+ }
+ }
+
+ public void FinalizeStrings(ref StaticGumpBuilder builder)
+ {
+ var startingIndex = builder._stringsCount;
+
+ var stringSlotOffsets = builder.StringSlotOffsets;
+ for (var i = 0; i < stringSlotOffsets.Length; i++)
+ {
+ var (hash, offset) = stringSlotOffsets[i];
+
+ if (hash != 0 && _hashes.TryGetValue(hash, out var index))
+ {
+ builder.WriteSlotIndex(offset, index + startingIndex);
+ }
+ }
+ }
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public void Dispose()
+ {
+ if (_finalizeLayout)
+ {
+ _hashes.Clear();
+ }
+
+ if (_stringsBuffer != _staticStringsBuffer)
+ {
+ STArrayPool.Shared.Return(_stringsBuffer);
+ }
+ }
+}
diff --git a/Projects/Server/Gumps/Gump.cs b/Projects/Server/Gumps/Legacy/Gump.cs
similarity index 84%
rename from Projects/Server/Gumps/Gump.cs
rename to Projects/Server/Gumps/Legacy/Gump.cs
index e598680ac..404db4a96 100644
--- a/Projects/Server/Gumps/Gump.cs
+++ b/Projects/Server/Gumps/Legacy/Gump.cs
@@ -1,6 +1,6 @@
/*************************************************************************
* ModernUO *
- * Copyright 2019-2023 - ModernUO Development Team *
+ * Copyright 2019-2024 - ModernUO Development Team *
* Email: hi@modernuo.com *
* File: Gump.cs *
* *
@@ -13,61 +13,30 @@
* along with this program. If not, see . *
*************************************************************************/
-using System;
using System.Collections.Generic;
using Server.Network;
using Server.Utilities;
namespace Server.Gumps;
-public partial class Gump
+public class Gump : BaseGump
{
- private static Serial _nextSerial = (Serial)1;
-
-
private int _switches;
private int _textEntries;
- public int Switches
- {
- get => _switches;
- set => _switches = value;
- }
-
- public int TextEntries
- {
- get => _textEntries;
- set => _textEntries = value;
- }
-
public Gump(int x, int y)
{
- do
- {
- Serial = _nextSerial++;
- } while (Serial == 0); // standard client apparently doesn't send a gump response packet if serial == 0
-
X = x;
Y = y;
- TypeID = GetTypeID(GetType());
-
- Entries = new List();
- Strings = new List();
+ Entries = [];
+ Strings = [];
}
public List Strings { get; }
- public int TypeID { get; }
-
public List Entries { get; }
- public Serial Serial { get; set; }
-
- public int X { get; set; }
-
- public int Y { get; set; }
-
public bool Disposable { get; set; } = true;
public bool Resizable { get; set; } = true;
@@ -76,15 +45,9 @@ public partial class Gump
public bool Closable { get; set; } = true;
- public static int GetTypeID(Type type)
- {
- unchecked
- {
- // To use the original .NET Framework deterministic hash code (with really bad performance)
- // change the next line to use HashUtility.GetNetFrameworkHashCode
- return (int)HashUtility.ComputeHash32(type?.FullName);
- }
- }
+ public override int Switches => _switches;
+
+ public override int TextEntries => _textEntries;
public void AddPage(int page)
{
@@ -278,17 +241,17 @@ public partial class Gump
return Strings.Count - 1;
}
- public void SendTo(NetState state)
+ public override void SendTo(NetState state)
{
state.AddGump(this);
state.SendDisplayGump(this, out _switches, out _textEntries);
}
- public virtual void OnResponse(NetState sender, in RelayInfo info)
- {
- }
-
- public virtual void OnServerClose(NetState owner)
+ protected void Reset()
{
+ _switches = 0;
+ _textEntries = 0;
+ Entries.Clear();
+ Strings.Clear();
}
}
diff --git a/Projects/Server/Gumps/GumpAlphaRegion.cs b/Projects/Server/Gumps/Legacy/GumpAlphaRegion.cs
similarity index 96%
rename from Projects/Server/Gumps/GumpAlphaRegion.cs
rename to Projects/Server/Gumps/Legacy/GumpAlphaRegion.cs
index 1794dce5f..b6bef4f90 100644
--- a/Projects/Server/Gumps/GumpAlphaRegion.cs
+++ b/Projects/Server/Gumps/Legacy/GumpAlphaRegion.cs
@@ -1,6 +1,6 @@
/*************************************************************************
* ModernUO *
- * Copyright 2019-2023 - ModernUO Development Team *
+ * Copyright 2019-2024 - ModernUO Development Team *
* Email: hi@modernuo.com *
* File: GumpAlphaRegion.cs *
* *
diff --git a/Projects/Server/Gumps/GumpBackground.cs b/Projects/Server/Gumps/Legacy/GumpBackground.cs
similarity index 96%
rename from Projects/Server/Gumps/GumpBackground.cs
rename to Projects/Server/Gumps/Legacy/GumpBackground.cs
index ab0b6b43e..eba28c240 100644
--- a/Projects/Server/Gumps/GumpBackground.cs
+++ b/Projects/Server/Gumps/Legacy/GumpBackground.cs
@@ -1,6 +1,6 @@
/*************************************************************************
* ModernUO *
- * Copyright 2019-2023 - ModernUO Development Team *
+ * Copyright 2019-2024 - ModernUO Development Team *
* Email: hi@modernuo.com *
* File: GumpBackground.cs *
* *
diff --git a/Projects/Server/Gumps/GumpButton.cs b/Projects/Server/Gumps/Legacy/GumpButton.cs
similarity index 96%
rename from Projects/Server/Gumps/GumpButton.cs
rename to Projects/Server/Gumps/Legacy/GumpButton.cs
index 98952496f..dc873a676 100644
--- a/Projects/Server/Gumps/GumpButton.cs
+++ b/Projects/Server/Gumps/Legacy/GumpButton.cs
@@ -1,6 +1,6 @@
/*************************************************************************
* ModernUO *
- * Copyright 2019-2023 - ModernUO Development Team *
+ * Copyright 2019-2024 - ModernUO Development Team *
* Email: hi@modernuo.com *
* File: GumpButton.cs *
* *
diff --git a/Projects/Server/Gumps/GumpCheck.cs b/Projects/Server/Gumps/Legacy/GumpCheck.cs
similarity index 96%
rename from Projects/Server/Gumps/GumpCheck.cs
rename to Projects/Server/Gumps/Legacy/GumpCheck.cs
index 270761d93..3664ee325 100644
--- a/Projects/Server/Gumps/GumpCheck.cs
+++ b/Projects/Server/Gumps/Legacy/GumpCheck.cs
@@ -1,6 +1,6 @@
/*************************************************************************
* ModernUO *
- * Copyright 2019-2023 - ModernUO Development Team *
+ * Copyright 2019-2024 - ModernUO Development Team *
* Email: hi@modernuo.com *
* File: GumpCheck.cs *
* *
diff --git a/Projects/Server/Gumps/GumpECHandleInput.cs b/Projects/Server/Gumps/Legacy/GumpECHandleInput.cs
similarity index 95%
rename from Projects/Server/Gumps/GumpECHandleInput.cs
rename to Projects/Server/Gumps/Legacy/GumpECHandleInput.cs
index bb62d2c99..680af6b41 100644
--- a/Projects/Server/Gumps/GumpECHandleInput.cs
+++ b/Projects/Server/Gumps/Legacy/GumpECHandleInput.cs
@@ -1,6 +1,6 @@
/*************************************************************************
* ModernUO *
- * Copyright 2019-2023 - ModernUO Development Team *
+ * Copyright 2019-2024 - ModernUO Development Team *
* Email: hi@modernuo.com *
* File: GumpECHandleInput.cs *
* *
diff --git a/Projects/Server/Gumps/GumpEntry.cs b/Projects/Server/Gumps/Legacy/GumpEntry.cs
similarity index 100%
rename from Projects/Server/Gumps/GumpEntry.cs
rename to Projects/Server/Gumps/Legacy/GumpEntry.cs
diff --git a/Projects/Server/Gumps/GumpGroup.cs b/Projects/Server/Gumps/Legacy/GumpGroup.cs
similarity index 95%
rename from Projects/Server/Gumps/GumpGroup.cs
rename to Projects/Server/Gumps/Legacy/GumpGroup.cs
index b758b6baa..e34d4c8ae 100644
--- a/Projects/Server/Gumps/GumpGroup.cs
+++ b/Projects/Server/Gumps/Legacy/GumpGroup.cs
@@ -1,6 +1,6 @@
/*************************************************************************
* ModernUO *
- * Copyright 2019-2023 - ModernUO Development Team *
+ * Copyright 2019-2024 - ModernUO Development Team *
* Email: hi@modernuo.com *
* File: GumpGroup.cs *
* *
diff --git a/Projects/Server/Gumps/GumpHtml.cs b/Projects/Server/Gumps/Legacy/GumpHtml.cs
similarity index 96%
rename from Projects/Server/Gumps/GumpHtml.cs
rename to Projects/Server/Gumps/Legacy/GumpHtml.cs
index 2f26b7c95..d9790c5cf 100644
--- a/Projects/Server/Gumps/GumpHtml.cs
+++ b/Projects/Server/Gumps/Legacy/GumpHtml.cs
@@ -1,6 +1,6 @@
/*************************************************************************
* ModernUO *
- * Copyright 2019-2023 - ModernUO Development Team *
+ * Copyright 2019-2024 - ModernUO Development Team *
* Email: hi@modernuo.com *
* File: GumpHtml.cs *
* *
diff --git a/Projects/Server/Gumps/GumpHtmlLocalized.cs b/Projects/Server/Gumps/Legacy/GumpHtmlLocalized.cs
similarity index 98%
rename from Projects/Server/Gumps/GumpHtmlLocalized.cs
rename to Projects/Server/Gumps/Legacy/GumpHtmlLocalized.cs
index 0d256c7f0..dba9394e3 100644
--- a/Projects/Server/Gumps/GumpHtmlLocalized.cs
+++ b/Projects/Server/Gumps/Legacy/GumpHtmlLocalized.cs
@@ -1,6 +1,6 @@
/*************************************************************************
* ModernUO *
- * Copyright 2019-2023 - ModernUO Development Team *
+ * Copyright 2019-2024 - ModernUO Development Team *
* Email: hi@modernuo.com *
* File: GumpHtmlLocalized.cs *
* *
diff --git a/Projects/Server/Gumps/GumpImage.cs b/Projects/Server/Gumps/Legacy/GumpImage.cs
similarity index 97%
rename from Projects/Server/Gumps/GumpImage.cs
rename to Projects/Server/Gumps/Legacy/GumpImage.cs
index 1258716ee..3e4bc59ad 100644
--- a/Projects/Server/Gumps/GumpImage.cs
+++ b/Projects/Server/Gumps/Legacy/GumpImage.cs
@@ -1,6 +1,6 @@
/*************************************************************************
* ModernUO *
- * Copyright 2019-2023 - ModernUO Development Team *
+ * Copyright 2019-2024 - ModernUO Development Team *
* Email: hi@modernuo.com *
* File: GumpImage.cs *
* *
diff --git a/Projects/Server/Gumps/GumpImageTileButton.cs b/Projects/Server/Gumps/Legacy/GumpImageTileButton.cs
similarity index 97%
rename from Projects/Server/Gumps/GumpImageTileButton.cs
rename to Projects/Server/Gumps/Legacy/GumpImageTileButton.cs
index b3a4db3f0..36f50d985 100644
--- a/Projects/Server/Gumps/GumpImageTileButton.cs
+++ b/Projects/Server/Gumps/Legacy/GumpImageTileButton.cs
@@ -1,6 +1,6 @@
/*************************************************************************
* ModernUO *
- * Copyright 2019-2023 - ModernUO Development Team *
+ * Copyright 2019-2024 - ModernUO Development Team *
* Email: hi@modernuo.com *
* File: GumpImageTileButton.cs *
* *
diff --git a/Projects/Server/Gumps/GumpImageTiled.cs b/Projects/Server/Gumps/Legacy/GumpImageTiled.cs
similarity index 96%
rename from Projects/Server/Gumps/GumpImageTiled.cs
rename to Projects/Server/Gumps/Legacy/GumpImageTiled.cs
index 51d8660cf..25e3390a2 100644
--- a/Projects/Server/Gumps/GumpImageTiled.cs
+++ b/Projects/Server/Gumps/Legacy/GumpImageTiled.cs
@@ -1,6 +1,6 @@
/*************************************************************************
* ModernUO *
- * Copyright 2019-2023 - ModernUO Development Team *
+ * Copyright 2019-2024 - ModernUO Development Team *
* Email: hi@modernuo.com *
* File: GumpImageTiled.cs *
* *
diff --git a/Projects/Server/Gumps/GumpItem.cs b/Projects/Server/Gumps/Legacy/GumpItem.cs
similarity index 96%
rename from Projects/Server/Gumps/GumpItem.cs
rename to Projects/Server/Gumps/Legacy/GumpItem.cs
index 714c0e12e..cfeeb6dac 100644
--- a/Projects/Server/Gumps/GumpItem.cs
+++ b/Projects/Server/Gumps/Legacy/GumpItem.cs
@@ -1,6 +1,6 @@
/*************************************************************************
* ModernUO *
- * Copyright 2019-2023 - ModernUO Development Team *
+ * Copyright 2019-2024 - ModernUO Development Team *
* Email: hi@modernuo.com *
* File: GumpItem.cs *
* *
diff --git a/Projects/Server/Gumps/GumpItemProperty.cs b/Projects/Server/Gumps/Legacy/GumpItemProperty.cs
similarity index 95%
rename from Projects/Server/Gumps/GumpItemProperty.cs
rename to Projects/Server/Gumps/Legacy/GumpItemProperty.cs
index 1cd8bb720..b6d465908 100644
--- a/Projects/Server/Gumps/GumpItemProperty.cs
+++ b/Projects/Server/Gumps/Legacy/GumpItemProperty.cs
@@ -1,6 +1,6 @@
/*************************************************************************
* ModernUO *
- * Copyright 2019-2023 - ModernUO Development Team *
+ * Copyright 2019-2024 - ModernUO Development Team *
* Email: hi@modernuo.com *
* File: GumpItemProperty.cs *
* *
diff --git a/Projects/Server/Gumps/GumpLabel.cs b/Projects/Server/Gumps/Legacy/GumpLabel.cs
similarity index 96%
rename from Projects/Server/Gumps/GumpLabel.cs
rename to Projects/Server/Gumps/Legacy/GumpLabel.cs
index b57df7324..06abd0137 100644
--- a/Projects/Server/Gumps/GumpLabel.cs
+++ b/Projects/Server/Gumps/Legacy/GumpLabel.cs
@@ -1,6 +1,6 @@
/*************************************************************************
* ModernUO *
- * Copyright 2019-2023 - ModernUO Development Team *
+ * Copyright 2019-2024 - ModernUO Development Team *
* Email: hi@modernuo.com *
* File: GumpLabel.cs *
* *
diff --git a/Projects/Server/Gumps/GumpLabelCropped.cs b/Projects/Server/Gumps/Legacy/GumpLabelCropped.cs
similarity index 96%
rename from Projects/Server/Gumps/GumpLabelCropped.cs
rename to Projects/Server/Gumps/Legacy/GumpLabelCropped.cs
index 91b8d7a08..f9a9ff187 100644
--- a/Projects/Server/Gumps/GumpLabelCropped.cs
+++ b/Projects/Server/Gumps/Legacy/GumpLabelCropped.cs
@@ -1,6 +1,6 @@
/*************************************************************************
* ModernUO *
- * Copyright 2019-2023 - ModernUO Development Team *
+ * Copyright 2019-2024 - ModernUO Development Team *
* Email: hi@modernuo.com *
* File: GumpLabelCropped.cs *
* *
diff --git a/Projects/Server/Gumps/GumpMasterGump.cs b/Projects/Server/Gumps/Legacy/GumpMasterGump.cs
similarity index 95%
rename from Projects/Server/Gumps/GumpMasterGump.cs
rename to Projects/Server/Gumps/Legacy/GumpMasterGump.cs
index 5dc39bfde..d754ae4b8 100644
--- a/Projects/Server/Gumps/GumpMasterGump.cs
+++ b/Projects/Server/Gumps/Legacy/GumpMasterGump.cs
@@ -1,6 +1,6 @@
/*************************************************************************
* ModernUO *
- * Copyright 2019-2023 - ModernUO Development Team *
+ * Copyright 2019-2024 - ModernUO Development Team *
* Email: hi@modernuo.com *
* File: GumpMasterGump.cs *
* *
diff --git a/Projects/Server/Gumps/GumpPage.cs b/Projects/Server/Gumps/Legacy/GumpPage.cs
similarity index 95%
rename from Projects/Server/Gumps/GumpPage.cs
rename to Projects/Server/Gumps/Legacy/GumpPage.cs
index fd7363ab7..a561125b8 100644
--- a/Projects/Server/Gumps/GumpPage.cs
+++ b/Projects/Server/Gumps/Legacy/GumpPage.cs
@@ -1,6 +1,6 @@
/*************************************************************************
* ModernUO *
- * Copyright 2019-2023 - ModernUO Development Team *
+ * Copyright 2019-2024 - ModernUO Development Team *
* Email: hi@modernuo.com *
* File: GumpPage.cs *
* *
diff --git a/Projects/Server/Gumps/GumpRadio.cs b/Projects/Server/Gumps/Legacy/GumpRadio.cs
similarity index 96%
rename from Projects/Server/Gumps/GumpRadio.cs
rename to Projects/Server/Gumps/Legacy/GumpRadio.cs
index eae474f19..73e314358 100644
--- a/Projects/Server/Gumps/GumpRadio.cs
+++ b/Projects/Server/Gumps/Legacy/GumpRadio.cs
@@ -1,6 +1,6 @@
/*************************************************************************
* ModernUO *
- * Copyright 2019-2023 - ModernUO Development Team *
+ * Copyright 2019-2024 - ModernUO Development Team *
* Email: hi@modernuo.com *
* File: GumpRadio.cs *
* *
diff --git a/Projects/Server/Gumps/GumpSpriteImage.cs b/Projects/Server/Gumps/Legacy/GumpSpriteImage.cs
similarity index 96%
rename from Projects/Server/Gumps/GumpSpriteImage.cs
rename to Projects/Server/Gumps/Legacy/GumpSpriteImage.cs
index 094aaebfc..89f390b39 100644
--- a/Projects/Server/Gumps/GumpSpriteImage.cs
+++ b/Projects/Server/Gumps/Legacy/GumpSpriteImage.cs
@@ -1,6 +1,6 @@
/*************************************************************************
* ModernUO *
- * Copyright 2019-2023 - ModernUO Development Team *
+ * Copyright 2019-2024 - ModernUO Development Team *
* Email: hi@modernuo.com *
* File: GumpSpriteImage.cs *
* *
diff --git a/Projects/Server/Gumps/GumpTextEntry.cs b/Projects/Server/Gumps/Legacy/GumpTextEntry.cs
similarity index 96%
rename from Projects/Server/Gumps/GumpTextEntry.cs
rename to Projects/Server/Gumps/Legacy/GumpTextEntry.cs
index a3cf72e9d..0bf60ab5b 100644
--- a/Projects/Server/Gumps/GumpTextEntry.cs
+++ b/Projects/Server/Gumps/Legacy/GumpTextEntry.cs
@@ -1,6 +1,6 @@
/*************************************************************************
* ModernUO *
- * Copyright 2019-2023 - ModernUO Development Team *
+ * Copyright 2019-2024 - ModernUO Development Team *
* Email: hi@modernuo.com *
* File: GumpTextEntry.cs *
* *
diff --git a/Projects/Server/Gumps/GumpTextEntryLimited.cs b/Projects/Server/Gumps/Legacy/GumpTextEntryLimited.cs
similarity index 96%
rename from Projects/Server/Gumps/GumpTextEntryLimited.cs
rename to Projects/Server/Gumps/Legacy/GumpTextEntryLimited.cs
index 4fc8a88ff..7c15274c9 100644
--- a/Projects/Server/Gumps/GumpTextEntryLimited.cs
+++ b/Projects/Server/Gumps/Legacy/GumpTextEntryLimited.cs
@@ -1,6 +1,6 @@
/*************************************************************************
* ModernUO *
- * Copyright 2019-2023 - ModernUO Development Team *
+ * Copyright 2019-2024 - ModernUO Development Team *
* Email: hi@modernuo.com *
* File: GumpTextEntryLimited.cs *
* *
diff --git a/Projects/Server/Gumps/GumpTooltip.cs b/Projects/Server/Gumps/Legacy/GumpTooltip.cs
similarity index 96%
rename from Projects/Server/Gumps/GumpTooltip.cs
rename to Projects/Server/Gumps/Legacy/GumpTooltip.cs
index 0ad6b8497..ffe8ccb12 100644
--- a/Projects/Server/Gumps/GumpTooltip.cs
+++ b/Projects/Server/Gumps/Legacy/GumpTooltip.cs
@@ -1,6 +1,6 @@
/*************************************************************************
* ModernUO *
- * Copyright 2019-2023 - ModernUO Development Team *
+ * Copyright 2019-2024 - ModernUO Development Team *
* Email: hi@modernuo.com *
* File: GumpTooltip.cs *
* *
diff --git a/Projects/Server/Gumps/StaticGump.cs b/Projects/Server/Gumps/StaticGump.cs
new file mode 100644
index 000000000..ace9c55ff
--- /dev/null
+++ b/Projects/Server/Gumps/StaticGump.cs
@@ -0,0 +1,179 @@
+/*************************************************************************
+ * ModernUO *
+ * Copyright 2019-2024 - ModernUO Development Team *
+ * Email: hi@modernuo.com *
+ * File: StaticGump.cs *
+ * *
+ * This program is free software: you can redistribute it and/or modify *
+ * it under the terms of the GNU General Public License as published by *
+ * the Free Software Foundation, either version 3 of the License, or *
+ * (at your option) any later version. *
+ * *
+ * You should have received a copy of the GNU General Public License *
+ * along with this program. If not, see . *
+ *************************************************************************/
+
+using System;
+using System.Buffers;
+using System.IO;
+using System.Runtime.CompilerServices;
+using Server.Buffers;
+using Server.Network;
+
+namespace Server.Gumps;
+
+public abstract class StaticGump : BaseGump where TSelf : StaticGump
+{
+ private static readonly byte[] _packetBuffer = GC.AllocateUninitializedArray(0x10000);
+
+ private static int _switches;
+ private static int _textEntries;
+ private static byte[] _compressedLayoutData;
+ private static byte[] _compressedStringsData;
+
+ private static bool _hasDynamicStrings;
+ private static int _staticStringsCount;
+ private static byte[] _staticStrings;
+
+ public override int Switches => _switches;
+ public override int TextEntries => _textEntries;
+
+ public StaticGump(int x, int y) : base(x, y)
+ {
+ }
+
+ protected abstract void BuildLayout(ref StaticGumpBuilder builder);
+
+ protected virtual void BuildStrings(ref GumpStringsBuilder builder)
+ {
+ }
+
+ public void CreatePacket(ref SpanWriter writer)
+ {
+ writer.Write((byte)0xDD); // Packet ID
+ writer.Seek(2, SeekOrigin.Current);
+
+ writer.Write(Serial);
+ writer.Write(TypeID);
+ writer.Write(X);
+ writer.Write(Y);
+
+ if (_compressedLayoutData != null)
+ {
+ writer.Write(_compressedLayoutData);
+
+ if (_compressedStringsData != null)
+ {
+ writer.Write(_staticStringsCount);
+ writer.Write(_compressedStringsData);
+ }
+ else if (_hasDynamicStrings)
+ {
+ var stringsBuilder = new GumpStringsBuilder(false);
+ BuildStrings(ref stringsBuilder);
+
+ if (_staticStringsCount == 0)
+ {
+ writer.Write(stringsBuilder._stringsCount);
+ OutgoingGumpPackets.WritePacked(stringsBuilder.StringsBuffer, ref writer);
+ }
+ else
+ {
+ writer.Write(_staticStringsCount + stringsBuilder._stringsCount);
+
+ var stringsData = stringsBuilder.StringsBuffer;
+ var buffer = STArrayPool.Shared.Rent(_staticStrings.Length + stringsData.Length);
+ _staticStrings.CopyTo(buffer.AsSpan());
+ stringsData.CopyTo(buffer.AsSpan(_staticStrings.Length));
+
+ OutgoingGumpPackets.WritePacked(buffer, ref writer);
+ STArrayPool.Shared.Return(buffer);
+ }
+ }
+ else if (_staticStrings != null)
+ {
+ writer.Write(_staticStringsCount);
+ OutgoingGumpPackets.WritePacked(_staticStrings, ref writer);
+ }
+ }
+ else
+ {
+ StaticGumpBuilder gumpBuilder = new StaticGumpBuilder();
+ BuildLayout(ref gumpBuilder);
+ gumpBuilder.FinalizeLayout();
+
+ _switches = gumpBuilder.Switches;
+ _textEntries = gumpBuilder.TextEntries;
+ _staticStringsCount = gumpBuilder._stringsCount;
+
+ var staticStringsData = gumpBuilder.StringsData;
+ var hasDynamicStrings = _hasDynamicStrings = gumpBuilder.StringSlotOffsets.Length > 0;
+
+ if (hasDynamicStrings)
+ {
+ _staticStrings = GC.AllocateUninitializedArray(staticStringsData.Length);
+ staticStringsData.CopyTo(_staticStrings);
+
+ var stringsBuilder = new GumpStringsBuilder(true);
+ BuildStrings(ref stringsBuilder);
+ stringsBuilder.FinalizeStrings(ref gumpBuilder); // Modifies the layout
+
+ WriteLayout(ref writer, ref gumpBuilder);
+
+ writer.Write(_staticStringsCount + stringsBuilder._stringsCount);
+
+ var stringsData = stringsBuilder.StringsBuffer;
+ var stringsLength = staticStringsData.Length + stringsData.Length;
+
+ var buffer = STArrayPool.Shared.Rent(stringsLength);
+ staticStringsData.CopyTo(buffer.AsSpan());
+ stringsData.CopyTo(buffer.AsSpan(staticStringsData.Length));
+
+ OutgoingGumpPackets.WritePacked(buffer.AsSpan(0, stringsLength), ref writer);
+
+ STArrayPool.Shared.Return(buffer);
+ stringsBuilder.Dispose();
+ }
+ else
+ {
+ WriteLayout(ref writer, ref gumpBuilder);
+
+ writer.Write(_staticStringsCount);
+
+ var stringsPos = writer.Position;
+ OutgoingGumpPackets.WritePacked(staticStringsData, ref writer);
+ var stringsLength = writer.Position - stringsPos;
+
+ _compressedStringsData = GC.AllocateUninitializedArray(stringsLength);
+ writer.Span.Slice(stringsPos, stringsLength).CopyTo(_compressedStringsData);
+ }
+
+ gumpBuilder.Dispose();
+ }
+
+ writer.WritePacketLength();
+ }
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ private static void WriteLayout(ref SpanWriter writer, ref StaticGumpBuilder gumpBuilder)
+ {
+ var layoutPos = writer.Position;
+ OutgoingGumpPackets.WritePacked(gumpBuilder.LayoutData, ref writer);
+ var layoutLength = writer.Position - layoutPos;
+
+ _compressedLayoutData = GC.AllocateUninitializedArray(layoutLength);
+ writer.Span.Slice(layoutPos, layoutLength).CopyTo(_compressedLayoutData);
+ }
+
+ public override void SendTo(NetState ns)
+ {
+ ns.AddGump(this);
+
+ var writer = new SpanWriter(_packetBuffer);
+ CreatePacket(ref writer);
+
+ ns.Send(writer.Span);
+
+ writer.Dispose();
+ }
+}
diff --git a/Projects/Server/Gumps/StaticGumpBuilder.cs b/Projects/Server/Gumps/StaticGumpBuilder.cs
new file mode 100644
index 000000000..05091c85e
--- /dev/null
+++ b/Projects/Server/Gumps/StaticGumpBuilder.cs
@@ -0,0 +1,511 @@
+/*************************************************************************
+ * ModernUO *
+ * Copyright 2019-2024 - ModernUO Development Team *
+ * Email: hi@modernuo.com *
+ * File: StaticGumpBuilder.cs *
+ * *
+ * This program is free software: you can redistribute it and/or modify *
+ * it under the terms of the GNU General Public License as published by *
+ * the Free Software Foundation, either version 3 of the License, or *
+ * (at your option) any later version. *
+ * *
+ * You should have received a copy of the GNU General Public License *
+ * along with this program. If not, see . *
+ *************************************************************************/
+
+using System;
+using System.Buffers.Binary;
+using System.Runtime.CompilerServices;
+using Server.Buffers;
+using Server.Text;
+
+namespace Server.Gumps;
+
+public ref struct StaticGumpBuilder
+{
+ private static readonly byte[] _staticStringsBuffer = GC.AllocateUninitializedArray(0x80000);
+
+ // Position in the layout and the hash of the keyslot
+ private static readonly (ulong, int)[] _stringSlotOffsets = GC.AllocateUninitializedArray<(ulong, int)>(0x8000); // Assume max 65535 strings
+
+ private byte[] _stringsBuffer;
+ private int _stringBytesWritten;
+ internal int _stringsCount;
+ private GumpLayoutBuilder _gumpBuilder;
+
+ private int _stringOffsetCount;
+
+ internal ReadOnlySpan LayoutData => _gumpBuilder.LayoutData;
+
+ internal ReadOnlySpan StringsData => _stringsBuffer.AsSpan(0, _stringBytesWritten);
+
+ internal ReadOnlySpan<(ulong, int)> StringSlotOffsets => _stringSlotOffsets.AsSpan(0, _stringOffsetCount);
+
+ public int Switches => _gumpBuilder._switches;
+ public int TextEntries => _gumpBuilder._textEntries;
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public StaticGumpBuilder()
+ {
+ _stringsBuffer = _staticStringsBuffer;
+
+ // For corner cases where slots are not filled, reserve the first string index for empty
+ _stringsBuffer.AsSpan(0, 2).Clear();
+ _stringBytesWritten = 2;
+ _stringsCount = 1;
+
+ _gumpBuilder = new GumpLayoutBuilder();
+ }
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public void SetNoClose() => _gumpBuilder.SetNoClose();
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public void SetNoMove() => _gumpBuilder.SetNoMove();
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public void SetNoResize() => _gumpBuilder.SetNoResize();
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public void SetNoDispose() => _gumpBuilder.SetNoDispose();
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public void AddAlphaRegion(int x, int y, int width, int height) =>
+ _gumpBuilder.AddAlphaRegion(x, y, width, height);
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public void AddBackground(int x, int y, int width, int height, int gumpID) =>
+ _gumpBuilder.AddBackground(x, y, width, height, gumpID);
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public void AddButton(
+ int x, int y, int normalID, int pressedId, int buttonId, GumpButtonType type = GumpButtonType.Reply, int param = 0
+ ) => _gumpBuilder.AddButton(x, y, normalID, pressedId, buttonId, type, param);
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public void AddCheckbox(int x, int y, int inactiveID, int activeID, bool selected, int switchId) =>
+ _gumpBuilder.AddCheckbox(x, y, inactiveID, activeID, selected, switchId);
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public void AddGroup(int groupId) => _gumpBuilder.AddGroup(groupId);
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public void AddHtmlPlaceholder(
+ int x,
+ int y,
+ int width,
+ int height,
+ ReadOnlySpan slotKey,
+ bool background = false,
+ bool scrollbar = false
+ )
+ {
+ var index = _gumpBuilder.AddHtmlPlaceholder(x, y, width, height, background, scrollbar);
+ WriteInternalizedStringSlot(slotKey, index);
+ }
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public void AddHtmlPlaceholder(
+ int x,
+ int y,
+ int width,
+ int height,
+ ref RawInterpolatedStringHandler handler,
+ bool background = false,
+ bool scrollbar = false
+ )
+ {
+ AddHtmlPlaceholder(x, y, width, height, handler.Text, background, scrollbar);
+ handler.Clear();
+ }
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public void AddHtml(
+ int x,
+ int y,
+ int width,
+ int height,
+ ReadOnlySpan text,
+ bool background = false,
+ bool scrollbar = false
+ )
+ {
+ WriteInternalizedString(text);
+ _gumpBuilder.AddHtml(x, y, width, height, _stringsCount++, background, scrollbar);
+ }
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public void AddHtml(
+ int x, int y, int width, int height, ref RawInterpolatedStringHandler handler,
+ bool background = false, bool scrollbar = false
+ )
+ {
+ AddHtml(x, y, width, height, handler.Text, background, scrollbar);
+ handler.Clear();
+ }
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public void AddHtml(
+ int x,
+ int y,
+ int width,
+ int height,
+ int color,
+ ReadOnlySpan text,
+ bool background = false,
+ bool scrollbar = false
+ ) => AddHtml(x, y, width, height, color, $"{text}", background, scrollbar);
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public void AddHtml(
+ int x,
+ int y,
+ int width,
+ int height,
+ int color,
+ ref RawInterpolatedStringHandler handler,
+ bool background = false,
+ bool scrollbar = false
+ )
+ {
+ AddHtml(x, y, width, height, color, handler.Text, background, scrollbar);
+ handler.Clear();
+ }
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public void AddHtmlCentered(
+ int x, int y, int width, int height, ReadOnlySpan text, bool background = false, bool scrollbar = false
+ ) => AddHtml(x, y, width, height, $"{text}", background, scrollbar);
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public void AddHtmlCentered(
+ int x,
+ int y,
+ int width,
+ int height,
+ ref RawInterpolatedStringHandler handler,
+ bool background = false,
+ bool scrollbar = false
+ )
+ {
+ AddHtml(x, y, width, height, $"{handler.Text}", background, scrollbar);
+ handler.Clear();
+ }
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public void AddHtmlCentered(
+ int x,
+ int y,
+ int width,
+ int height,
+ int color,
+ ReadOnlySpan text,
+ bool background = false,
+ bool scrollbar = false
+ ) => AddHtml(x, y, width, height, color, $"{text}", background, scrollbar);
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public void AddHtmlCentered(
+ int x,
+ int y,
+ int width,
+ int height,
+ int color,
+ ref RawInterpolatedStringHandler handler,
+ bool background = false,
+ bool scrollbar = false
+ )
+ {
+ AddHtmlCentered(x, y, width, height, color, handler.Text, background, scrollbar);
+ handler.Clear();
+ }
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public void AddHtmlLocalized(
+ int x, int y, int width, int height, int number, bool background = false, bool scrollbar = false
+ ) => _gumpBuilder.AddHtmlLocalized(x, y, width, height, number, background, scrollbar);
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public void AddHtmlLocalized(
+ int x, int y, int width, int height, int number, short color, bool background = false, bool scrollbar = false
+ ) => _gumpBuilder.AddHtmlLocalized(x, y, width, height, number, color, background, scrollbar);
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public void AddHtmlLocalized(
+ int x,
+ int y,
+ int width,
+ int height,
+ int number,
+ ReadOnlySpan args,
+ int color,
+ bool background = false,
+ bool scrollbar = false
+ ) => _gumpBuilder.AddHtmlLocalized(x, y, width, height, number, args, color, background, scrollbar);
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public void AddHtmlLocalized(
+ int x, int y, int width, int height, int number, ref RawInterpolatedStringHandler handler, int color,
+ bool background = false, bool scrollbar = false
+ ) => _gumpBuilder.AddHtmlLocalized(x, y, width, height, number, ref handler, color, background, scrollbar);
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public void AddImage(int x, int y, int gumpId, int hue = 0, ReadOnlySpan cls = default) =>
+ _gumpBuilder.AddImage(x, y, gumpId, hue, cls);
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public void AddImage(int x, int y, int gumpId, ref RawInterpolatedStringHandler handler) =>
+ _gumpBuilder.AddImage(x, y, gumpId, ref handler);
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public void AddImage(int x, int y, int gumpId, int hue, ref RawInterpolatedStringHandler handler) =>
+ _gumpBuilder.AddImage(x, y, gumpId, hue, ref handler);
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public void AddImageTiledButton(
+ int x,
+ int y,
+ int normalId,
+ int pressedId,
+ int buttonId,
+ GumpButtonType type,
+ int param,
+ int itemId,
+ int hue,
+ int width,
+ int height,
+ int localizedTooltip = -1
+ ) => _gumpBuilder.AddImageTiledButton(
+ x, y, normalId, pressedId, buttonId, type, param, itemId, hue, width, height, localizedTooltip
+ );
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public void AddImageTiled(int x, int y, int width, int height, int gumpId) =>
+ _gumpBuilder.AddImageTiled(x, y, width, height, gumpId);
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public void AddItem(int x, int y, int itemId, int hue = 0) => _gumpBuilder.AddItem(x, y, itemId, hue);
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public void AddItemProperty(Serial serial) => _gumpBuilder.AddItemProperty(serial);
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public void AddLabelPlaceholder(int x, int y, int hue, ReadOnlySpan slotKey)
+ {
+ var index = _gumpBuilder.AddLabelPlaceholder(x, y, hue);
+ WriteInternalizedStringSlot(slotKey, index);
+ }
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public void AddLabelPlaceholder(int x, int y, int hue, ref RawInterpolatedStringHandler handler)
+ {
+ AddHtmlPlaceholder(x, y, 0, 0, handler.Text);
+ handler.Clear();
+ }
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public void AddLabel(int x, int y, int hue, ReadOnlySpan text)
+ {
+ WriteInternalizedString(text);
+ _gumpBuilder.AddLabel(x, y, hue, _stringsCount++);
+ }
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public void AddLabel(int x, int y, int hue, ref RawInterpolatedStringHandler handler)
+ {
+ AddLabel(x, y, hue, handler.Text);
+ handler.Clear();
+ }
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public void AddLabelCroppedPlaceholder(int x, int y, int width, int height, int hue, ref RawInterpolatedStringHandler handler)
+ {
+ AddLabelCroppedPlaceholder(x, y, width, height, hue, handler.Text);
+ handler.Clear();
+ }
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public void AddLabelCroppedPlaceholder(int x, int y, int width, int height, int hue, ReadOnlySpan slotKey)
+ {
+ var index = _gumpBuilder.AddLabelCroppedPlaceholder(x, y, width, height, hue);
+ WriteInternalizedStringSlot(slotKey, index);
+ }
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public void AddLabelCropped(int x, int y, int width, int height, int hue, ReadOnlySpan text)
+ {
+ WriteInternalizedString(text);
+ _gumpBuilder.AddLabelCropped(x, y, width, height, hue, _stringsCount++);
+ }
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public void AddLabelCropped(int x, int y, int width, int height, int hue, ref RawInterpolatedStringHandler handler)
+ {
+ AddLabelCropped(x, y, width, height, hue, handler.Text);
+ handler.Clear();
+ }
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public void AddGumpIdOverride(int gumpId) => _gumpBuilder.AddGumpIdOverride(gumpId);
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public void AddPage(int page = 0) => _gumpBuilder.AddPage(page);
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public void AddRadio(int x, int y, int inactiveId, int activeId, bool selected, int switchId) =>
+ _gumpBuilder.AddRadio(x, y, inactiveId, activeId, selected, switchId);
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public void AddSpriteImage(int x, int y, int gumpId, int width, int height, int sx, int sy) =>
+ _gumpBuilder.AddSpriteImage(x, y, gumpId, width, height, sx, sy);
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public void AddTextEntryPlaceholder(
+ int x, int y, int width, int height, int hue, int entryId, ref RawInterpolatedStringHandler handler
+ )
+ {
+ AddTextEntryPlaceholder(x, y, width, height, hue, entryId, handler.Text);
+ handler.Clear();
+ }
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public void AddTextEntryPlaceholder(int x, int y, int width, int height, int hue, int entryId, ReadOnlySpan slotKey)
+ {
+ var index = _gumpBuilder.AddTextEntryPlaceholder(x, y, width, height, hue, entryId);
+ WriteInternalizedStringSlot(slotKey, index);
+ }
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public void AddTextEntry(
+ int x, int y, int width, int height, int hue, int entryId, ReadOnlySpan initialText = default
+ )
+ {
+ WriteInternalizedString(initialText);
+ _gumpBuilder.AddTextEntry(x, y, width, height, hue, entryId, _stringsCount++);
+ }
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public void AddTextEntry(
+ int x, int y, int width, int height, int hue, int entryId, ref RawInterpolatedStringHandler handler
+ )
+ {
+ AddTextEntry(x, y, width, height, hue, entryId, handler.Text);
+ handler.Clear();
+ }
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public void AddTextEntryLimitedPlaceholder(
+ int x, int y, int width, int height, int hue, int entryId, ReadOnlySpan slotKey, int size = 0
+ )
+ {
+ var index = _gumpBuilder.AddTextEntryLimitedPlaceholder(x, y, width, height, hue, entryId, size);
+ WriteInternalizedStringSlot(slotKey, index);
+ }
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public void AddTextEntryLimitedPlaceholder(
+ int x, int y, int width, int height, int hue, int entryId, ref RawInterpolatedStringHandler handler, int size = 0
+ )
+ {
+ AddTextEntryLimitedPlaceholder(x, y, width, height, hue, entryId, handler.Text, size);
+ handler.Clear();
+ }
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public void AddTextEntryLimited(
+ int x, int y, int width, int height, int hue, int entryId, ReadOnlySpan initialText = default, int size = 0
+ )
+ {
+ WriteInternalizedString(initialText);
+ _gumpBuilder.AddTextEntryLimited(x, y, width, height, hue, entryId, _stringsCount++, size);
+ }
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public void AddTextEntryLimited(
+ int x, int y, int width, int height, int hue, int entryId, ref RawInterpolatedStringHandler handler, int size = 0
+ )
+ {
+ AddTextEntryLimited(x, y, width, height, hue, entryId, handler.Text, size);
+ handler.Clear();
+ }
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public void AddTooltip(int number) => _gumpBuilder.AddTooltip(number);
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public void AddTooltip(int number, ReadOnlySpan args) => _gumpBuilder.AddTooltip(number, args);
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public void AddTooltip(int number, ref RawInterpolatedStringHandler handler)
+ {
+ _gumpBuilder.AddTooltip(number, handler.Text);
+ handler.Clear();
+ }
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public void FinalizeLayout() => _gumpBuilder.FinalizeLayout();
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ internal void WriteInternalizedString(ReadOnlySpan text)
+ {
+ if (text.Length > ushort.MaxValue)
+ {
+ text = text[..ushort.MaxValue];
+ }
+
+ GrowStringsBufferIfNeeded(2 + text.Length * 2);
+ BinaryPrimitives.WriteUInt16BigEndian(_stringsBuffer.AsSpan(_stringBytesWritten), (ushort)text.Length);
+
+ _stringBytesWritten += 2;
+ _stringBytesWritten += text.GetBytesBigUni(_stringsBuffer.AsSpan(_stringBytesWritten));
+ }
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ private void GrowStringsBufferIfNeeded(int needed)
+ {
+ if (needed + _stringBytesWritten <= _stringsBuffer.Length)
+ {
+ return;
+ }
+
+ var newSize = Math.Max(_stringBytesWritten + needed, _stringsBuffer.Length * 2);
+ byte[] poolArray = STArrayPool.Shared.Rent(newSize);
+
+ _stringsBuffer.AsSpan(0, _stringBytesWritten).CopyTo(poolArray);
+
+ byte[] toReturn = _stringsBuffer;
+ _stringsBuffer = poolArray;
+
+ if (toReturn != _staticStringsBuffer)
+ {
+ STArrayPool.Shared.Return(toReturn);
+ }
+ }
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ private void WriteInternalizedStringSlot(ReadOnlySpan slotKey, int index)
+ {
+ var hash = HashUtility.ComputeHash64(slotKey);
+ _stringSlotOffsets[_stringOffsetCount++] = (hash, index);
+ }
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ internal void WriteSlotIndex(int offset, int index)
+ {
+ // Left padded final index for the slot
+ index.TryFormat(_gumpBuilder.LayoutData[offset..], out _, "00000");
+ }
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public void Dispose()
+ {
+ _gumpBuilder.Dispose();
+
+ if (_stringsBuffer != _staticStringsBuffer)
+ {
+ STArrayPool.Shared.Return(_stringsBuffer);
+ }
+
+ this = default;
+ }
+}
diff --git a/Projects/Server/Mobiles/Mobile.cs b/Projects/Server/Mobiles/Mobile.cs
index 5f373105c..2850a65aa 100644
--- a/Projects/Server/Mobiles/Mobile.cs
+++ b/Projects/Server/Mobiles/Mobile.cs
@@ -8169,9 +8169,9 @@ public partial class Mobile : IHued, IComparable, ISpawnable, IObjectPro
return false;
}
- public Gump FindGump() where T : Gump => m_NetState?.Gumps.Find(g => g is T);
+ public BaseGump FindGump() where T : BaseGump => m_NetState?.Gumps.Find(g => g is T);
- public bool CloseGump() where T : Gump
+ public bool CloseGump() where T : BaseGump
{
if (m_NetState == null)
{
@@ -8199,7 +8199,7 @@ public partial class Mobile : IHued, IComparable, ISpawnable, IObjectPro
return false;
}
- var gumps = new List(ns.Gumps);
+ var gumps = new List(ns.Gumps);
ns.ClearGumps();
@@ -8213,9 +8213,9 @@ public partial class Mobile : IHued, IComparable, ISpawnable, IObjectPro
return true;
}
- public bool HasGump() where T : Gump => FindGump() != null;
+ public bool HasGump() where T : BaseGump => FindGump() != null;
- public bool SendGump(Gump g)
+ public bool SendGump(BaseGump g)
{
if (m_NetState == null)
{
diff --git a/Projects/Server/Network/NetState/NetState.cs b/Projects/Server/Network/NetState/NetState.cs
index 19208647e..75bd4d40a 100755
--- a/Projects/Server/Network/NetState/NetState.cs
+++ b/Projects/Server/Network/NetState/NetState.cs
@@ -127,10 +127,10 @@ public partial class NetState : IComparable, IValueLinkListNode();
- HuePickers = new List();
- Menus = new List();
- Trades = new List();
+ Gumps = [];
+ HuePickers = [];
+ Menus = [];
+ Trades = [];
RecvPipe = new Pipe(RecvPipeSize);
SendPipe = new Pipe(SendPipeSize);
_nextActivityCheck = Core.TickCount + 30000;
@@ -225,7 +225,7 @@ public partial class NetState : IComparable, IValueLinkListNode Gumps { get; private set; }
+ public List Gumps { get; private set; }
public List HuePickers { get; private set; }
@@ -381,7 +381,7 @@ public partial class NetState : IComparable, IValueLinkListNode();
+ Menus ??= [];
if (Menus.Count < MenuCap)
{
@@ -411,7 +411,7 @@ public partial class NetState : IComparable, IValueLinkListNode();
+ HuePickers ??= [];
if (HuePickers.Count < HuePickerCap)
{
@@ -439,9 +439,9 @@ public partial class NetState : IComparable, IValueLinkListNode();
+ Gumps ??= [];
if (Gumps.Count < GumpCap)
{
@@ -454,7 +454,7 @@ public partial class NetState : IComparable, IValueLinkListNode, IValueLinkListNode
{
- public class PetResurrectGump : Gump
+ private readonly double _hitsScalar;
+ private readonly BaseCreature _pet;
+
+ public PetResurrectGump(Mobile from, BaseCreature pet, double hitsScalar = 0.0) : base(50, 50)
{
- private readonly double m_HitsScalar;
- private readonly BaseCreature m_Pet;
+ from.CloseGump();
- public PetResurrectGump(Mobile from, BaseCreature pet, double hitsScalar = 0.0) : base(50, 50)
+ _pet = pet;
+ _hitsScalar = hitsScalar;
+ }
+
+ protected override void BuildLayout(ref StaticGumpBuilder builder)
+ {
+ builder.AddPage();
+
+ builder.AddBackground(10, 10, 265, 140, 0x242C);
+
+ builder.AddItem(205, 40, 0x4);
+ builder.AddItem(227, 40, 0x5);
+
+ builder.AddItem(180, 78, 0xCAE);
+ builder.AddItem(195, 90, 0xCAD);
+ builder.AddItem(218, 95, 0xCB0);
+
+ // Wilt thou sanctify the resurrection of:
+ builder.AddHtmlLocalized(30, 30, 150, 75, 1049665);
+ builder.AddHtmlPlaceholder(30, 70, 150, 25, "petName", true);
+
+ builder.AddButton(40, 105, 0x81A, 0x81B, 0x1); // Okay
+ builder.AddButton(110, 105, 0x819, 0x818, 0x2); // Cancel
+ }
+
+ protected override void BuildStrings(ref GumpStringsBuilder builder)
+ {
+ builder.SetStringSlot("petName", $"{_pet.Name}");
+ }
+
+ public override void OnResponse(NetState state, in RelayInfo info)
+ {
+ if (_pet.Deleted || !_pet.IsBonded || !_pet.IsDeadPet)
{
- from.CloseGump();
-
- m_Pet = pet;
- m_HitsScalar = hitsScalar;
-
- AddPage(0);
-
- AddBackground(10, 10, 265, 140, 0x242C);
-
- AddItem(205, 40, 0x4);
- AddItem(227, 40, 0x5);
-
- AddItem(180, 78, 0xCAE);
- AddItem(195, 90, 0xCAD);
- AddItem(218, 95, 0xCB0);
-
- AddHtmlLocalized(30, 30, 150, 75, 1049665); // Wilt thou sanctify the resurrection of:
- AddHtml(30, 70, 150, 25, $"{pet.Name}
", true);
-
- AddButton(40, 105, 0x81A, 0x81B, 0x1); // Okay
- AddButton(110, 105, 0x819, 0x818, 0x2); // Cancel
+ return;
}
- public override void OnResponse(NetState state, in RelayInfo info)
+ var from = state.Mobile;
+
+ if (info.ButtonID != 1)
{
- if (m_Pet.Deleted || !m_Pet.IsBonded || !m_Pet.IsDeadPet)
- {
- return;
- }
+ return;
+ }
- var from = state.Mobile;
+ if (_pet.Map?.CanFit(_pet.Location, 16, false, false) != true)
+ {
+ from.SendLocalizedMessage(503256); // You fail to resurrect the creature.
+ return;
+ }
- if (info.ButtonID == 1)
- {
- if (m_Pet.Map?.CanFit(m_Pet.Location, 16, false, false) != true)
- {
- from.SendLocalizedMessage(503256); // You fail to resurrect the creature.
- return;
- }
+ if (_pet.Region?.IsPartOf("Khaldun") == true) // TODO: Confirm for pets, as per Bandage's script.
+ {
+ // The veil of death in this area is too strong and resists thy efforts to restore life.
+ from.SendLocalizedMessage(1010395);
+ return;
+ }
- if (m_Pet.Region?.IsPartOf("Khaldun") == true) // TODO: Confirm for pets, as per Bandage's script.
- {
- from.SendLocalizedMessage(
- 1010395
- ); // The veil of death in this area is too strong and resists thy efforts to restore life.
- return;
- }
+ _pet.PlaySound(0x214);
+ _pet.FixedEffect(0x376A, 10, 16);
+ _pet.ResurrectPet();
- m_Pet.PlaySound(0x214);
- m_Pet.FixedEffect(0x376A, 10, 16);
- m_Pet.ResurrectPet();
+ var decreaseAmount = from == _pet.ControlMaster ? 0.1 : 0.2;
- double decreaseAmount;
+ for (var i = 0; i < _pet.Skills.Length; ++i) // Decrease all skills on pet.
+ {
+ _pet.Skills[i].Base -= decreaseAmount;
+ }
- if (from == m_Pet.ControlMaster)
- {
- decreaseAmount = 0.1;
- }
- else
- {
- decreaseAmount = 0.2;
- }
-
- for (var i = 0; i < m_Pet.Skills.Length; ++i) // Decrease all skills on pet.
- {
- m_Pet.Skills[i].Base -= decreaseAmount;
- }
-
- if (!m_Pet.IsDeadPet && m_HitsScalar > 0)
- {
- m_Pet.Hits = (int)(m_Pet.HitsMax * m_HitsScalar);
- }
- }
+ if (!_pet.IsDeadPet && _hitsScalar > 0)
+ {
+ _pet.Hits = (int)(_pet.HitsMax * _hitsScalar);
}
}
}
diff --git a/Projects/UOContent/Network/Packets/IncomingPlayerPackets.cs b/Projects/UOContent/Network/Packets/IncomingPlayerPackets.cs
index 85bc44bad..2b4809d30 100644
--- a/Projects/UOContent/Network/Packets/IncomingPlayerPackets.cs
+++ b/Projects/UOContent/Network/Packets/IncomingPlayerPackets.cs
@@ -350,7 +350,7 @@ public static class IncomingPlayerPackets
var typeId = reader.ReadInt32();
var buttonId = reader.ReadInt32();
- Gump gump = null;
+ BaseGump baseGump = null;
foreach (var g in state.Gumps)
{
@@ -359,61 +359,58 @@ public static class IncomingPlayerPackets
continue;
}
- gump = g;
+ baseGump = g;
break;
}
- if (gump != null)
+ if (baseGump != null)
{
- var buttonExists = buttonId == 0; // 0 is always 'close'
-
- if (!buttonExists)
+ if (baseGump is Gump gump)
{
- foreach (var e in gump.Entries)
- {
- if (e is GumpButton button && button.ButtonID == buttonId)
- {
- buttonExists = true;
- break;
- }
+ var buttonExists = buttonId == 0; // 0 is always 'close'
- if (e is GumpImageTileButton tileButton && tileButton.ButtonID == buttonId)
+ if (!buttonExists)
+ {
+ foreach (var e in gump.Entries)
{
- buttonExists = true;
- break;
+ if ((e as GumpButton)?.ButtonID == buttonId)
+ {
+ buttonExists = true;
+ break;
+ }
+
+ if ((e as GumpImageTileButton)?.ButtonID == buttonId)
+ {
+ buttonExists = true;
+ break;
+ }
}
}
- }
- if (!buttonExists)
- {
- state.LogInfo("Invalid gump response, disconnecting...");
- var exception = new InvalidGumpResponseException($"Button {buttonId} doesn't exist");
- exception.SetStackTrace(new StackTrace());
- NetState.TraceException(exception);
- state.Mobile?.SendMessage("Invalid gump response.");
-
- // state.Disconnect("Invalid gump response.");
- return;
+ if (!buttonExists)
+ {
+ state.LogInfo("Invalid gump response, disconnecting...");
+ var exception = new InvalidGumpResponseException($"Button {buttonId} doesn't exist");
+ exception.SetStackTrace(new StackTrace());
+ NetState.TraceException(exception);
+ return;
+ }
}
var switchCount = reader.ReadInt32();
- if (switchCount < 0 || switchCount > gump.Switches)
+ if (switchCount < 0 || switchCount > baseGump.Switches)
{
state.LogInfo("Invalid gump response, disconnecting...");
var exception = new InvalidGumpResponseException($"Bad switch count {switchCount}");
exception.SetStackTrace(new StackTrace());
NetState.TraceException(exception);
- state.Mobile?.SendMessage("Invalid gump response.");
-
- // state.Disconnect("Invalid gump response.");
return;
}
int switchByteCount = switchCount * 4;
- // Read in all of the integers
+ // Read all the integers
ReadOnlySpan switchBlock =
MemoryMarshal.Cast(reader.Buffer.Slice(reader.Position, switchByteCount));
@@ -434,15 +431,12 @@ public static class IncomingPlayerPackets
}
var textCount = reader.ReadInt32();
- if (textCount < 0 || textCount > gump.TextEntries)
+ if (textCount < 0 || textCount > baseGump.TextEntries)
{
state.LogInfo("Invalid gump response, disconnecting...");
var exception = new InvalidGumpResponseException($"Bad text entry count {textCount}");
exception.SetStackTrace(new StackTrace());
NetState.TraceException(exception);
- state.Mobile?.SendMessage("Invalid gump response.");
-
- // state.Disconnect("Invalid gump response.");
return;
}
@@ -461,9 +455,6 @@ public static class IncomingPlayerPackets
var exception = new InvalidGumpResponseException($"Text entry {i} is too long ({textLength})");
exception.SetStackTrace(new StackTrace());
NetState.TraceException(exception);
- state.Mobile?.SendMessage("Invalid gump response.");
-
- // state.Disconnect("Invalid gump response.");
return;
}
@@ -476,9 +467,9 @@ public static class IncomingPlayerPackets
var textBlock = reader.Buffer.Slice(textOffset, reader.Position - textOffset);
- state.RemoveGump(gump);
+ state.RemoveGump(baseGump);
- var prof = GumpProfile.Acquire(gump.GetType());
+ var prof = GumpProfile.Acquire(baseGump.GetType());
prof?.Start();
@@ -489,7 +480,7 @@ public static class IncomingPlayerPackets
textFields,
textBlock
);
- gump.OnResponse(state, relayInfo);
+ baseGump.OnResponse(state, relayInfo);
prof?.Finish();
}
diff --git a/Projects/UOContent/Spells/Ninjitsu/AnimalForm.cs b/Projects/UOContent/Spells/Ninjitsu/AnimalForm.cs
index d28affa7d..ebc9c2eea 100644
--- a/Projects/UOContent/Spells/Ninjitsu/AnimalForm.cs
+++ b/Projects/UOContent/Spells/Ninjitsu/AnimalForm.cs
@@ -380,56 +380,59 @@ namespace Server.Spells.Ninjitsu
public bool StealingBonus { get; }
}
- public class AnimalFormGump : Gump
+ public class AnimalFormGump : DynamicGump
{
// TODO: Convert this for ML to the BaseImageTileButtonsGump
- private readonly Mobile m_Caster;
- private readonly AnimalForm m_Spell;
+ private readonly Mobile _caster;
+ private readonly AnimalForm _spell;
+ private readonly AnimalFormEntry[] _entries;
- public AnimalFormGump(Mobile caster, AnimalFormEntry[] entries, AnimalForm spell)
- : base(50, 50)
+ public AnimalFormGump(Mobile caster, AnimalFormEntry[] entries, AnimalForm spell) : base(50, 50)
{
- m_Caster = caster;
- m_Spell = spell;
+ _caster = caster;
+ _spell = spell;
+ _entries = entries;
+ }
- AddPage(0);
+ protected override void BuildLayout(ref DynamicGumpBuilder builder)
+ {
+ builder.AddPage();
- AddBackground(0, 0, 520, 404, 0x13BE);
- AddImageTiled(10, 10, 500, 20, 0xA40);
- AddImageTiled(10, 40, 500, 324, 0xA40);
- AddImageTiled(10, 374, 500, 20, 0xA40);
- AddAlphaRegion(10, 10, 500, 384);
+ builder.AddBackground(0, 0, 520, 404, 0x13BE);
+ builder.AddImageTiled(10, 10, 500, 20, 0xA40);
+ builder.AddImageTiled(10, 40, 500, 324, 0xA40);
+ builder.AddImageTiled(10, 374, 500, 20, 0xA40);
+ builder.AddAlphaRegion(10, 10, 500, 384);
- AddHtmlLocalized(14, 12, 500, 20, 1063394, 0x7FFF); // Polymorph Selection Menu
+ builder.AddHtmlLocalized(14, 12, 500, 20, 1063394, 0x7FFF); // Polymorph Selection Menu
- AddButton(10, 374, 0xFB1, 0xFB2, 0);
- AddHtmlLocalized(45, 376, 450, 20, 1011012, 0x7FFF); // CANCEL
+ builder.AddButton(10, 374, 0xFB1, 0xFB2, 0);
+ builder.AddHtmlLocalized(45, 376, 450, 20, 1011012, 0x7FFF); // CANCEL
- var ninjitsu = caster.Skills.Ninjitsu.Value;
+ int ninjitsu = _caster.Skills[SkillName.Ninjitsu].Fixed;
+ int current = 0;
- var current = 0;
-
- for (var i = 0; i < entries.Length; ++i)
+ for (int i = 0; i < _entries.Length; ++i)
{
- var enabled = ninjitsu >= entries[i].ReqSkill && BaseFormTalisman.EntryEnabled(caster, entries[i].Type);
+ bool enabled = ninjitsu >= _entries[i].ReqSkill && BaseFormTalisman.EntryEnabled(_caster, _entries[i].Type);
- var page = current / 10 + 1;
- var pos = current % 10;
+ int page = current / 10 + 1;
+ int pos = current % 10;
if (pos == 0)
{
if (page > 1)
{
- AddButton(400, 374, 0xFA5, 0xFA7, 0, GumpButtonType.Page, page);
- AddHtmlLocalized(440, 376, 60, 20, 1043353, 0x7FFF); // Next
+ builder.AddButton(400, 374, 0xFA5, 0xFA7, 0, GumpButtonType.Page, page);
+ builder.AddHtmlLocalized(440, 376, 60, 20, 1043353, 0x7FFF); // Next
}
- AddPage(page);
+ builder.AddPage(page);
if (page > 1)
{
- AddButton(300, 374, 0xFAE, 0xFB0, 0, GumpButtonType.Page, 1);
- AddHtmlLocalized(340, 376, 60, 20, 1011393, 0x7FFF); // Back
+ builder.AddButton(300, 374, 0xFAE, 0xFB0, 0, GumpButtonType.Page, 1);
+ builder.AddHtmlLocalized(340, 376, 60, 20, 1011393, 0x7FFF); // Back
}
}
@@ -438,26 +441,16 @@ namespace Server.Spells.Ninjitsu
continue;
}
- var x = pos % 2 == 0 ? 14 : 264;
- var y = pos / 2 * 64 + 44;
+ AnimalFormEntry entry = _entries[i];
- var b = ItemBounds.Table[entries[i].ItemID];
+ int y = Math.DivRem(pos, 2, out var rem) * 64 + 44;
+ int x = rem == 0 ? 14 : 264;
+ Rectangle2D b = ItemBounds.Table[entry.ItemID];
- AddImageTiledButton(
- x,
- y,
- 0x918,
- 0x919,
- i + 1,
- GumpButtonType.Reply,
- 0,
- entries[i].ItemID,
- entries[i].Hue,
- 40 - b.Width / 2 - b.X,
- 30 - b.Height / 2 - b.Y
- );
- AddTooltip(entries[i].Tooltip);
- AddHtmlLocalized(x + 84, y, 250, 60, entries[i].Name, 0x7FFF);
+ builder.AddImageTiledButton(x, y, 0x918, 0x919, i + 1, GumpButtonType.Reply, 0, entry.ItemID,
+ entry.Hue, 40 - b.Width / 2 - b.X, 30 - b.Height / 2 - b.Y, entry.Tooltip);
+
+ builder.AddHtmlLocalized(x + 84, y, 250, 60, entry.Name, 0x7FFF);
current++;
}
@@ -467,39 +460,40 @@ namespace Server.Spells.Ninjitsu
{
var entryID = info.ButtonID - 1;
- if (entryID < 0 || entryID >= AnimalForm.Entries.Length)
+ if (entryID < 0 || entryID >= Entries.Length)
{
return;
}
- var mana = m_Spell.ScaleMana(m_Spell.RequiredMana);
- var entry = AnimalForm.Entries[entryID];
+ var mana = _spell.ScaleMana(_spell.RequiredMana);
+ var entry = Entries[entryID];
- if (mana > m_Caster.Mana)
+ if (!BaseFormTalisman.EntryEnabled(sender.Mobile, entry.Type))
+ {
+ return;
+ }
+
+ if (mana > _caster.Mana)
{
// You must have at least ~1_MANA_REQUIREMENT~ Mana to use this ability.
- m_Caster.SendLocalizedMessage(1060174, mana.ToString());
+ _caster.SendLocalizedMessage(1060174, mana.ToString());
}
- else if (m_Caster is PlayerMobile mobile && mobile.MountBlockReason != BlockMountType.None)
+ else if (_caster is PlayerMobile mobile
+ && (mobile.MountBlockReason != BlockMountType.None
+ || mobile.DuelContext?.AllowSpellCast(_caster, _spell) == false))
{
- mobile.SendLocalizedMessage(1063108); // You cannot use this ability right now.
+ _caster.SendLocalizedMessage(1063108); // You cannot use this ability right now.
}
- else if (BaseFormTalisman.EntryEnabled(sender.Mobile, entry.Type))
+ else if (Morph(_caster, entryID) == MorphResult.Fail)
{
- if ((m_Caster as PlayerMobile)?.DuelContext?.AllowSpellCast(m_Caster, m_Spell) == false)
- {
- }
- else if (Morph(m_Caster, entryID) == MorphResult.Fail)
- {
- m_Caster.LocalOverheadMessage(MessageType.Regular, 0x3B2, 502632); // The spell fizzles.
- m_Caster.FixedParticles(0x3735, 1, 30, 9503, EffectLayer.Waist);
- m_Caster.PlaySound(0x5C);
- }
- else
- {
- m_Caster.FixedParticles(0x3728, 10, 13, 2023, EffectLayer.Waist);
- m_Caster.Mana -= mana;
- }
+ _caster.LocalOverheadMessage(MessageType.Regular, 0x3B2, 502632); // The spell fizzles.
+ _caster.FixedParticles(0x3735, 1, 30, 9503, EffectLayer.Waist);
+ _caster.PlaySound(0x5C);
+ }
+ else
+ {
+ _caster.FixedParticles(0x3728, 10, 13, 2023, EffectLayer.Waist);
+ _caster.Mana -= mana;
}
}
}
diff --git a/version.json b/version.json
index 1477d35be..80683645f 100644
--- a/version.json
+++ b/version.json
@@ -1,4 +1,4 @@
{
"$schema": "https://raw.githubusercontent.com/dotnet/Nerdbank.GitVersioning/master/src/NerdBank.GitVersioning/version.schema.json",
- "version": "0.12.1"
+ "version": "0.13.1"
}