From 65532ea887009dddada3e4cfca3ff826d8d6f53c Mon Sep 17 00:00:00 2001 From: Kamron Batman <3953314+kamronbatman@users.noreply.github.com> Date: Thu, 25 Apr 2024 22:40:30 -0700 Subject: [PATCH] feat: Adds optimized dynamic/static layout gumps (#1652) # New Gump API We are pleased to release a new API that is faster, allocates nearly zero memory, and still feels very similar to the original API. The API is broken into 3 types of gumps, dynamic, static with placeholders, and static without placeholders. ### Dynamic Gumps These gumps will inherit `DynamicGump` and are meant for gumps that have a dynamic layout. This includes specifying dynamic arguments to HtmlLocalized entries. ## Static Gumps Static gumps are those where the function to the build the layout is called only once and cached forever. They can optionally have placeholders. These placeholders allow the developer to specify the string values later, dynamically in a `BuildStrings` method on the gump. If a gump does not have any placeholders, the string entries will also be cached forever. ## Benchmarks To make sure we were going in the right direction and not wasting time, we took copious benchmarks. Here are the final benchmarks for a really simple gump. Note: * The majority of creating a gump is compressing the layout and the strings. Compressing each section takes ~6,000ns (12us total). ```cs | Method | Mean | Error | StdDev | Median | Ratio | RatioSD | Gen0 | Allocated | Alloc Ratio | |------------------------------- |-------------:|-------------:|-------------:|-------------:|------:|--------:|-------:|----------:|------------:| | OldGump | 13,308.29 ns | 1,059.695 ns | 1,883.608 ns | 14,330.72 ns | 1.000 | 0.00 | 0.1526 | 2400 B | 1.00 | | DynamicLayoutGump | 13,357.86 ns | 129.144 ns | 226.185 ns | 13,323.60 ns | 1.029 | 0.17 | - | 48 B | 0.02 | | StaticLayoutDynamicStringsGump | 6,653.10 ns | 81.815 ns | 143.292 ns | 6,617.45 ns | 0.514 | 0.09 | - | 40 B | 0.02 | | StaticLayoutGump | 86.33 ns | 0.760 ns | 1.350 ns | 86.07 ns | 0.007 | 0.00 | 0.0020 | 32 B | 0.01 | ``` # Non-Breaking Changes * All gump components in the core have been moved to `Gumps/Legacy`. * All legacy gumps will still inherit `Gump`, which now inherits `BaseGump` # Special Thanks Thank you to @stefanomerotta for considerable contributions/benchmarking/testing to make this effort a reality! We collectively went through over 10 iterations, but it is finally ready. --- .../Tests/Gumps/TestGumps/DynamicTestGump.cs | 35 + .../Tests/Gumps/TestGumps/LegacyTestGump.cs | 29 + .../Gumps/TestGumps/StaticLayoutTestGump.cs | 41 ++ .../Tests/Gumps/TestGumps/StaticTestGump.cs | 32 + .../Tests/Gumps/TestLayoutGumps.cs | 143 ++++ .../Packets/Outgoing/GumpPacketTests.cs | 1 + .../Buffers/RawInterpolatedStringHandler.cs | 2 +- Projects/Server/Gumps/BaseGump.cs | 73 ++ Projects/Server/Gumps/DynamicGump.cs | 77 +++ Projects/Server/Gumps/DynamicGumpBuilder.cs | 392 +++++++++++ Projects/Server/Gumps/GumpFlags.cs | 27 + Projects/Server/Gumps/GumpLayoutBuilder.cs | 642 ++++++++++++++++++ Projects/Server/Gumps/GumpStringsBuilder.cs | 127 ++++ Projects/Server/Gumps/{ => Legacy}/Gump.cs | 63 +- .../Gumps/{ => Legacy}/GumpAlphaRegion.cs | 2 +- .../Gumps/{ => Legacy}/GumpBackground.cs | 2 +- .../Server/Gumps/{ => Legacy}/GumpButton.cs | 2 +- .../Server/Gumps/{ => Legacy}/GumpCheck.cs | 2 +- .../Gumps/{ => Legacy}/GumpECHandleInput.cs | 2 +- .../Server/Gumps/{ => Legacy}/GumpEntry.cs | 0 .../Server/Gumps/{ => Legacy}/GumpGroup.cs | 2 +- .../Server/Gumps/{ => Legacy}/GumpHtml.cs | 2 +- .../Gumps/{ => Legacy}/GumpHtmlLocalized.cs | 2 +- .../Server/Gumps/{ => Legacy}/GumpImage.cs | 2 +- .../Gumps/{ => Legacy}/GumpImageTileButton.cs | 2 +- .../Gumps/{ => Legacy}/GumpImageTiled.cs | 2 +- .../Server/Gumps/{ => Legacy}/GumpItem.cs | 2 +- .../Gumps/{ => Legacy}/GumpItemProperty.cs | 2 +- .../Server/Gumps/{ => Legacy}/GumpLabel.cs | 2 +- .../Gumps/{ => Legacy}/GumpLabelCropped.cs | 2 +- .../Gumps/{ => Legacy}/GumpMasterGump.cs | 2 +- .../Server/Gumps/{ => Legacy}/GumpPage.cs | 2 +- .../Server/Gumps/{ => Legacy}/GumpRadio.cs | 2 +- .../Gumps/{ => Legacy}/GumpSpriteImage.cs | 2 +- .../Gumps/{ => Legacy}/GumpTextEntry.cs | 2 +- .../{ => Legacy}/GumpTextEntryLimited.cs | 2 +- .../Server/Gumps/{ => Legacy}/GumpTooltip.cs | 2 +- Projects/Server/Gumps/StaticGump.cs | 179 +++++ Projects/Server/Gumps/StaticGumpBuilder.cs | 511 ++++++++++++++ Projects/Server/Mobiles/Mobile.cs | 10 +- Projects/Server/Network/NetState/NetState.cs | 22 +- .../Advanced Search/AdvancedSearchGump.cs | 5 +- Projects/UOContent/Gumps/PetResurrectGump.cs | 138 ++-- .../Network/Packets/IncomingPlayerPackets.cs | 75 +- .../UOContent/Spells/Ninjitsu/AnimalForm.cs | 130 ++-- version.json | 2 +- 46 files changed, 2527 insertions(+), 273 deletions(-) create mode 100644 Projects/Server.Tests/Tests/Gumps/TestGumps/DynamicTestGump.cs create mode 100644 Projects/Server.Tests/Tests/Gumps/TestGumps/LegacyTestGump.cs create mode 100644 Projects/Server.Tests/Tests/Gumps/TestGumps/StaticLayoutTestGump.cs create mode 100644 Projects/Server.Tests/Tests/Gumps/TestGumps/StaticTestGump.cs create mode 100644 Projects/Server.Tests/Tests/Gumps/TestLayoutGumps.cs create mode 100644 Projects/Server/Gumps/BaseGump.cs create mode 100644 Projects/Server/Gumps/DynamicGump.cs create mode 100644 Projects/Server/Gumps/DynamicGumpBuilder.cs create mode 100644 Projects/Server/Gumps/GumpFlags.cs create mode 100644 Projects/Server/Gumps/GumpLayoutBuilder.cs create mode 100644 Projects/Server/Gumps/GumpStringsBuilder.cs rename Projects/Server/Gumps/{ => Legacy}/Gump.cs (84%) rename Projects/Server/Gumps/{ => Legacy}/GumpAlphaRegion.cs (96%) rename Projects/Server/Gumps/{ => Legacy}/GumpBackground.cs (96%) rename Projects/Server/Gumps/{ => Legacy}/GumpButton.cs (96%) rename Projects/Server/Gumps/{ => Legacy}/GumpCheck.cs (96%) rename Projects/Server/Gumps/{ => Legacy}/GumpECHandleInput.cs (95%) rename Projects/Server/Gumps/{ => Legacy}/GumpEntry.cs (100%) rename Projects/Server/Gumps/{ => Legacy}/GumpGroup.cs (95%) rename Projects/Server/Gumps/{ => Legacy}/GumpHtml.cs (96%) rename Projects/Server/Gumps/{ => Legacy}/GumpHtmlLocalized.cs (98%) rename Projects/Server/Gumps/{ => Legacy}/GumpImage.cs (97%) rename Projects/Server/Gumps/{ => Legacy}/GumpImageTileButton.cs (97%) rename Projects/Server/Gumps/{ => Legacy}/GumpImageTiled.cs (96%) rename Projects/Server/Gumps/{ => Legacy}/GumpItem.cs (96%) rename Projects/Server/Gumps/{ => Legacy}/GumpItemProperty.cs (95%) rename Projects/Server/Gumps/{ => Legacy}/GumpLabel.cs (96%) rename Projects/Server/Gumps/{ => Legacy}/GumpLabelCropped.cs (96%) rename Projects/Server/Gumps/{ => Legacy}/GumpMasterGump.cs (95%) rename Projects/Server/Gumps/{ => Legacy}/GumpPage.cs (95%) rename Projects/Server/Gumps/{ => Legacy}/GumpRadio.cs (96%) rename Projects/Server/Gumps/{ => Legacy}/GumpSpriteImage.cs (96%) rename Projects/Server/Gumps/{ => Legacy}/GumpTextEntry.cs (96%) rename Projects/Server/Gumps/{ => Legacy}/GumpTextEntryLimited.cs (96%) rename Projects/Server/Gumps/{ => Legacy}/GumpTooltip.cs (96%) create mode 100644 Projects/Server/Gumps/StaticGump.cs create mode 100644 Projects/Server/Gumps/StaticGumpBuilder.cs 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" }