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)}]");
}
}