diff --git a/Projects/Server/Buffers/PooledArraySpanFormattable.cs b/Projects/Server/Buffers/PooledArraySpanFormattable.cs index 7290f250a..58001ed4e 100644 --- a/Projects/Server/Buffers/PooledArraySpanFormattable.cs +++ b/Projects/Server/Buffers/PooledArraySpanFormattable.cs @@ -18,6 +18,12 @@ using System; namespace Server.Buffers; +/// +/// Wrapper for STArray backed strings and char buffers that will be used in InterpolatedStringHandlers. +/// The wrapper prevents intermediate strings from being created unnecessarily. +/// Note: TryFormat can only be called once. Using the PooledArraySpanFormattable after calling TryFormat will throw. +/// To use the span multiple times, use the Chars property directly instead. +/// public struct PooledArraySpanFormattable : ISpanFormattable, IDisposable { private char[] _arrayToReturnToPool; @@ -42,6 +48,7 @@ public struct PooledArraySpanFormattable : ISpanFormattable, IDisposable STArrayPool.Shared.Return(_arrayToReturnToPool); _arrayToReturnToPool = null; + // We don't dispose so we can call ToString() multiple times with idempotence. return _value; } @@ -58,6 +65,9 @@ public struct PooledArraySpanFormattable : ISpanFormattable, IDisposable _arrayToReturnToPool.AsSpan(0, _pos).CopyTo(destination); charsWritten = _pos; + + // Interpolated string handlers do not dispose, but we need to return the chars to the array. + Dispose(); return true; } diff --git a/Projects/Server/Mobiles/Mobile.cs b/Projects/Server/Mobiles/Mobile.cs index 7d85db542..b14496789 100644 --- a/Projects/Server/Mobiles/Mobile.cs +++ b/Projects/Server/Mobiles/Mobile.cs @@ -3228,8 +3228,6 @@ namespace Server public virtual void AddNameProperties(IPropertyList list) { - var name = Name ?? " "; - string prefix; if (ShowFameTitle && (m_Player || m_Body.IsHuman) && m_Fame >= 10000) @@ -3241,22 +3239,19 @@ namespace Server prefix = " "; } - var title = PropertyTitle && !string.IsNullOrEmpty(Title) ? Title : ""; - - string suffix; var guild = m_Guild; - if (guild != null && (m_Player || m_DisplayGuildTitle)) - { - suffix = title.Length > 0 - ? $"{title} [{Utility.FixHtml(guild.Abbreviation)}]" - : $"[{Utility.FixHtml(guild.Abbreviation)}]"; - } - else - { - suffix = " "; - } + var hasTitle = PropertyTitle && !string.IsNullOrEmpty(Title); + var hasGuild = guild != null && (m_Player || m_DisplayGuildTitle); - list.Add(1050045, $"{prefix}\t{name}\t{ApplyNameSuffix(suffix)}"); // ~1_PREFIX~~2_NAME~~3_SUFFIX~ + string suffix = hasTitle switch + { + true when hasGuild => $" {Title} [{Utility.FixHtmlFormattable(guild.Abbreviation)}]", + true => $" {Title}", + false when hasGuild => $" [{Utility.FixHtmlFormattable(guild.Abbreviation)}]", + _ => " " + }; + + list.Add(1050045, $"{prefix}\t{Name ?? " "}\t{ApplyNameSuffix(suffix)}"); // ~1_PREFIX~~2_NAME~~3_SUFFIX~ if (guild != null && (m_DisplayGuildTitle || m_Player && guild.Type != GuildType.Regular)) { @@ -3268,11 +3263,11 @@ namespace Server { if (NewGuildDisplay) { - list.Add($"{Utility.FixHtml(guildTitle)}, {Utility.FixHtml(guild.Name)}"); + list.Add($"{Utility.FixHtmlFormattable(guildTitle)}, {Utility.FixHtmlFormattable(guild.Name)}"); } else { - list.Add($"{Utility.FixHtml(guildTitle)}, {Utility.FixHtml(guild.Name)} Guild{type}"); + list.Add($"{Utility.FixHtmlFormattable(guildTitle)}, {Utility.FixHtmlFormattable(guild.Name)} Guild{type}"); } } else @@ -7850,14 +7845,7 @@ namespace Server prefix = m_Female ? "Lady" : "Lord"; } - var suffix = ""; - - if (ClickTitle && !string.IsNullOrEmpty(Title)) - { - suffix = Title; - } - - suffix = ApplyNameSuffix(suffix); + var suffix = ApplyNameSuffix(ClickTitle && !string.IsNullOrEmpty(Title) ? Title : ""); string val; diff --git a/Projects/Server/Text/StringHelpers.cs b/Projects/Server/Text/StringHelpers.cs index 7e22c6341..c467e4e99 100644 --- a/Projects/Server/Text/StringHelpers.cs +++ b/Projects/Server/Text/StringHelpers.cs @@ -303,4 +303,12 @@ public static class StringHelpers chars = chars[(indexOf + 1)..]; } } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static char[] ToPooledArray(this string str) + { + var chars = STArrayPool.Shared.Rent(str.Length); + str.CopyTo(chars); + return chars; + } } diff --git a/Projects/Server/Utilities/Utility.cs b/Projects/Server/Utilities/Utility.cs index 091919bc3..6c61e4b96 100644 --- a/Projects/Server/Utilities/Utility.cs +++ b/Projects/Server/Utilities/Utility.cs @@ -567,12 +567,11 @@ public static class Utility return str; } - using var sb = new ValueStringBuilder(str, stackalloc char[Math.Min(128, str.Length)]); - ReadOnlySpan invalid = stackalloc []{ '<', '>', '#' }; - ReadOnlySpan replacement = stackalloc []{ '(', ')', '-' }; - sb.ReplaceAny(invalid, replacement, 0, sb.Length); + var chars = str.ToPooledArray(); + var span = chars.AsSpan(0, str.Length); + FixHtml(span); - return sb.ToString(); + return span.ToString(); } public static void FixHtml(Span chars) @@ -588,6 +587,20 @@ public static class Utility chars.ReplaceAny(invalid, replacement); } + public static PooledArraySpanFormattable FixHtmlFormattable(string str) + { + var chars = str.ToPooledArray(); + var span = chars.AsSpan(0, str.Length); + var formattable = new PooledArraySpanFormattable(chars, str.Length); + + if (!string.IsNullOrEmpty(str)) + { + FixHtml(span); + } + + return formattable; + } + public static int InsensitiveCompare(string first, string second) => first.InsensitiveCompare(second); public static bool InsensitiveStartsWith(string first, string second) => first.InsensitiveStartsWith(second); diff --git a/Projects/UOContent/Engines/Help/SpeechLogGump.cs b/Projects/UOContent/Engines/Help/SpeechLogGump.cs index 591e3e6f7..e82ced9a4 100644 --- a/Projects/UOContent/Engines/Help/SpeechLogGump.cs +++ b/Projects/UOContent/Engines/Help/SpeechLogGump.cs @@ -46,7 +46,7 @@ namespace Server.Engines.Help 10, 280, 20, - $"
SPEECH LOG - {playerName} ({Utility.FixHtml(playerAccount)})
" + $"
SPEECH LOG - {playerName} ({Utility.FixHtmlFormattable(playerAccount)})
" ); var lastPage = (log.Count - 1) / MaxEntriesPerPage; diff --git a/Projects/UOContent/Items/Guilds/Guildstone.cs b/Projects/UOContent/Items/Guilds/Guildstone.cs index adce68a6c..767d02c58 100644 --- a/Projects/UOContent/Items/Guilds/Guildstone.cs +++ b/Projects/UOContent/Items/Guilds/Guildstone.cs @@ -114,11 +114,11 @@ public partial class Guildstone : Item, IAddon, IChoppable } // list.Add( 1060802, Utility.FixHtml( name ) ); // Guild name: ~1_val~ - list.Add(1060802, $"{Utility.FixHtml(name)} [{Utility.FixHtml(abbr)}]"); + list.Add(1060802, $"{Utility.FixHtmlFormattable(name)} [{Utility.FixHtmlFormattable(abbr)}]"); } else if (_guildName != null && _guildAbbrev != null) { - list.Add(1060802, $"{Utility.FixHtml(_guildName)} [{Utility.FixHtml(_guildAbbrev)}]"); + list.Add(1060802, $"{Utility.FixHtmlFormattable(_guildName)} [{Utility.FixHtmlFormattable(_guildAbbrev)}]"); } } @@ -254,11 +254,11 @@ public partial class GuildstoneDeed : Item } // list.Add( 1060802, Utility.FixHtml( name ) ); // Guild name: ~1_val~ - list.Add(1060802, $"{Utility.FixHtml(name)} [{Utility.FixHtml(abbr)}]"); + list.Add(1060802, $"{Utility.FixHtmlFormattable(name)} [{Utility.FixHtmlFormattable(abbr)}]"); } else if (_guildName != null && _guildAbbrev != null) { - list.Add(1060802, $"{Utility.FixHtml(_guildName)} [{Utility.FixHtml(_guildAbbrev)}]"); + list.Add(1060802, $"{Utility.FixHtmlFormattable(_guildName)} [{Utility.FixHtmlFormattable(_guildAbbrev)}]"); } }