/************************************************************************* * 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, int 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; } }