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