feat: Add zero-alloc interpolation handler to ValueStringBuilder, replace all StringBuilder usage (#2387)

## Summary

- **Add a self-referencing `InterpolationHandler` to `ValueStringBuilder`** that writes directly into the builder's buffer — zero intermediate allocation, works with `stackalloc`-backed builders
- **Replace all `System.Text.StringBuilder` usage** across the codebase with `ValueStringBuilder`
- **Convert `ValueStringBuilder.Create()` to `stackalloc`** at 10 sites where output length is provably bounded
- **Convert manual `Dispose()` to `using var`** where possible, and hoist loop-scoped builders outside loops with `Reset()`
- **Convert verbose `Append()` chains to `Append($"...")`** interpolation for readability
- **Add comprehensive documentation** for string handling patterns

## InterpolationHandler Design

`ValueStringBuilder` is a `ref struct`, which creates challenges for C#'s interpolated string handler pattern:

- **`ref` fields to ref structs are not allowed** (CS9050)
- **`[InterpolatedStringHandlerArgument("")]` passes struct receivers by value**, not by ref
- **`ISelfInterpolatedStringHandler` requires boxing** ref structs into interface fields

**Solution: Copy-and-reconcile pattern.** The handler receives a value copy of the builder. The copy shares the same underlying `char` buffer (`Span` points to the same `stackalloc`/pooled memory), so writes go to the original buffer. `Append()` reconciles by `this = handler._builder`, updating `_length` and any buffer references changed by `Grow()`.

This is safe because:
- The game loop is single-threaded — no concurrent access between handler construction and reconciliation
- If `Grow()` occurs in the copy, the original's stale buffer isn't accessed until `Append()` replaces it
- `Dispose()` correctly returns the reconciled buffer to the pool

## Changes by Category

### ValueStringBuilder (`Projects/Server/Buffers/ValueStringBuilder.cs`)
- Added nested `InterpolationHandler` ref struct with copy-and-reconcile pattern
- Added `Append([InterpolatedStringHandlerArgument("")] scoped ref InterpolationHandler)` method
- Removed `RawInterpolatedStringHandler` overloads (new handler replaces them)
- All `AppendFormatted` overloads delegate to existing `Append` methods (no code duplication)
- Alignment support via direct private field access (nested type privilege)

### StringBuilder → ValueStringBuilder (15 files)
Replaced all `new StringBuilder()` with `ValueStringBuilder.Create()` or `stackalloc`:
- ConPVP games: KingOfTheHill, DoubleDom, CTF, BombingRun, TourneyMatch
- ConPVP infrastructure: Tournament, Participant, TourneyParticipant
- ConPVP gumps: ArenaGump, TournamentBracketGump, AcceptTeamGump, ConfirmSignupGump
- Commands: Handlers, Logging, Add
- Other: TownCrier, SpeechLogGump, TestCenter

Key patterns:
- `sb = new StringBuilder()` reassignment → `sb.Reset()`
- `sb.AppendFormat("{0:N0}", value)` → `sb.Append($"{value:N0}")`
- `sb.Append(x).Append(y)` chains → separate statements (VSB returns void)

### Create() → stackalloc (10 files)
Converted heap-allocated builders to stackalloc where output is bounded:
- ClientVersion (32), MapSelection (160), HouseRaffleStone (48)
- HolySense (96), UnholySense (96), ClientVerification (192)
- AcceptTeamGump (64), ConfirmSignupGump (64)
- BaseWeapon (160), BaseArmor (128)

### Loop optimizations (2 files)
Hoisted `ValueStringBuilder` creation outside loops with `Reset()` per iteration:
- TourneyMatch.cs: `using var` inside for loop → stackalloc before loop
- ArenaGump.cs: `Create()` + `Dispose()` per iteration → stackalloc before loop

### Append chain → interpolation (5 files)
Converted multi-line `Append()` chains to `Append($"...")`:
- BountyMessage.cs: title switch (6 cases), paragraph (15→1 Append), description lines, closing
- AcceptTeamGump, ConfirmSignupGump, TournamentBracketGump: tournament type strings
- AdminGump: comment/tag formatting in loops

### Documentation
- `dev-docs/string-handling.md`: Full reference — construction, interpolation, disposal, decision guide
- `dev-docs/claude-skills/modernuo-string-handling.md`: Claude skill with quick reference
- `CLAUDE.md`: Added rule 17 (no StringBuilder), dev-docs table entry, skills table entry
- `dev-docs/code-standards.md`: Updated memory management section

## Test Plan

- [x] `dotnet build` — 0 errors, 0 warnings
- [x] `dotnet test` — 940/940 tests pass
- [x] 28 ValueStringBuilder tests covering all reconciliation scenarios:
  - Stackalloc no-grow, stackalloc with grow (→pool transition)
  - Heap no-grow, heap with grow, heap double grow
  - Pre-existing content with and without grow
  - Sequential multiple `Append($"...")` calls
  - Mixed plain + interpolated Append
  - Empty interpolation, literal-only, format specifiers
  - Null string holes, ISpanFormattable types
  - Dispose after stackalloc→pool grow
This commit is contained in:
Kamron Batman 2026-03-22 14:23:44 -07:00 committed by GitHub
parent 9f39198fab
commit 61e41df00c
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
38 changed files with 846 additions and 200 deletions

View file

@ -40,10 +40,255 @@ public class ValueStringBuilderTests
public void TestAppendInterpolation(object value)
{
var sb = ValueStringBuilder.Create();
sb.Append( $"Hi, this is {value}.");
sb.Append($"Hi, this is {value}.");
sb.Append(" I am a string.");
Assert.Equal($"Hi, this is {value}. I am a string.", sb.ToString());
sb.Dispose();
}
// --- InterpolationHandler reconciliation tests ---
// These validate the copy-and-reconcile pattern: the handler receives a VALUE COPY of the builder,
// writes into the shared buffer, and Append() reconciles via `this = handler._builder`.
// Critical to detect if C# compiler codegen changes break this assumption.
[Fact]
public void Interpolation_Stackalloc_NoGrow()
{
// Stackalloc buffer large enough — no Grow needed.
// Validates: copy's Span shares same stackalloc memory, _length is reconciled.
var sb = new ValueStringBuilder(stackalloc char[64]);
sb.Append($"Hello {42} world");
Assert.Equal("Hello 42 world", sb.ToString());
Assert.Equal(14, sb.Length);
sb.Dispose();
}
[Fact]
public void Interpolation_Stackalloc_WithGrow()
{
// Tiny stackalloc forces Grow inside the handler's copy.
// Validates: after Grow, copy moves to pooled array; reconciliation updates
// _chars, _arrayToReturnToPool, and _length on the original.
var sb = new ValueStringBuilder(stackalloc char[4]);
sb.Append($"This string is much longer than 4 chars: {12345}");
var expected = "This string is much longer than 4 chars: 12345";
Assert.Equal(expected, sb.ToString());
Assert.Equal(expected.Length, sb.Length);
// After grow, capacity should have expanded beyond 4
Assert.True(sb.Capacity >= 47);
sb.Dispose();
}
[Fact]
public void Interpolation_Stackalloc_PreExistingContent_NoGrow()
{
// Append plain text first, then interpolation. Buffer large enough.
// Validates: interpolation appends after existing content, doesn't overwrite.
var sb = new ValueStringBuilder(stackalloc char[64]);
sb.Append("prefix-");
sb.Append($"value={99}");
Assert.Equal("prefix-value=99", sb.ToString());
Assert.Equal(15, sb.Length);
sb.Dispose();
}
[Fact]
public void Interpolation_Stackalloc_PreExistingContent_WithGrow()
{
// Fill most of a small stackalloc, then interpolate enough to force Grow.
// Validates: existing content is preserved through the Grow, new content is appended.
var sb = new ValueStringBuilder(stackalloc char[16]);
sb.Append("0123456789"); // 10 chars, 6 remaining
sb.Append($"abcdefghij{42}"); // 12 chars, exceeds remaining — Grow
Assert.Equal("0123456789abcdefghij42", sb.ToString());
Assert.Equal(22, sb.Length);
Assert.True(sb.Capacity >= 22);
sb.Dispose();
}
[Fact]
public void Interpolation_Heap_NoGrow()
{
// Heap-allocated (Create) with sufficient capacity.
// Validates: handler works with pooled backing, _length is reconciled.
var sb = ValueStringBuilder.Create(64);
sb.Append($"Score: {100}, Name: {"Test"}");
Assert.Equal("Score: 100, Name: Test", sb.ToString());
Assert.Equal(22, sb.Length);
sb.Dispose();
}
[Fact]
public void Interpolation_Heap_WithGrow()
{
// Small heap allocation forces Grow.
// Validates: after Grow, copy's new pooled array replaces original's
// (original's old array was returned to pool by copy's Grow).
var sb = ValueStringBuilder.Create(4);
sb.Append($"This exceeds the initial 4 char capacity: {67890}");
var expected = "This exceeds the initial 4 char capacity: 67890";
Assert.Equal(expected, sb.ToString());
Assert.Equal(expected.Length, sb.Length);
Assert.True(sb.Capacity >= expected.Length);
sb.Dispose();
}
[Fact]
public void Interpolation_Heap_PreExistingContent_WithGrow()
{
// Heap with pre-existing content, then Grow via interpolation.
// Validates: existing content preserved, pooled array properly transitioned.
var sb = ValueStringBuilder.Create(8);
sb.Append("ABCD");
sb.Append($"EFGHIJKLMNOP{42}");
Assert.Equal("ABCDEFGHIJKLMNOP42", sb.ToString());
Assert.Equal(18, sb.Length);
sb.Dispose();
}
[Fact]
public void Interpolation_Sequential_MultipleAppends()
{
// Multiple sequential Append($"...") calls.
// Validates: each reconciliation correctly advances _length, subsequent calls
// see the updated state from prior reconciliations.
var sb = new ValueStringBuilder(stackalloc char[128]);
sb.Append($"A={1}");
sb.Append($" B={2}");
sb.Append($" C={3}");
sb.Append($" D={4}");
var expected = "A=1 B=2 C=3 D=4";
Assert.Equal(expected, sb.ToString());
Assert.Equal(expected.Length, sb.Length);
sb.Dispose();
}
[Fact]
public void Interpolation_Sequential_GrowOnSecondAppend()
{
// First Append fits, second triggers Grow.
// Validates: reconciliation after first Append leaves builder in a state
// that the second Append (with Grow) works correctly.
var sb = new ValueStringBuilder(stackalloc char[16]);
sb.Append($"Fits: {1}"); // 7 chars, fits in 16
sb.Append($" - Now this is a much longer string that forces growth: {999}");
Assert.Equal("Fits: 1 - Now this is a much longer string that forces growth: 999", sb.ToString());
sb.Dispose();
}
[Fact]
public void Interpolation_MixedAppendStyles()
{
// Mix plain Append with interpolated Append.
// Validates: reconciliation is compatible with non-interpolated Append calls.
var sb = new ValueStringBuilder(stackalloc char[64]);
sb.Append("plain-");
sb.Append($"interp={42}-");
sb.Append("plain2-");
sb.Append($"interp2={99}");
Assert.Equal("plain-interp=42-plain2-interp2=99", sb.ToString());
sb.Dispose();
}
[Fact]
public void Interpolation_EmptyInterpolation()
{
// Empty interpolation expression.
// Validates: handler with zero holes still reconciles correctly.
var sb = new ValueStringBuilder(stackalloc char[32]);
sb.Append("before");
sb.Append($"");
sb.Append("after");
Assert.Equal("beforeafter", sb.ToString());
sb.Dispose();
}
[Fact]
public void Interpolation_OnlyLiteral()
{
// Interpolation with no holes (just a literal).
// Validates: AppendLiteral-only path reconciles _length.
var sb = new ValueStringBuilder(stackalloc char[32]);
sb.Append($"just a literal");
Assert.Equal("just a literal", sb.ToString());
sb.Dispose();
}
[Fact]
public void Interpolation_FormatSpecifiers()
{
// Format specifiers in interpolation holes.
// Validates: AppendFormatted<T> with format string works through handler.
var sb = new ValueStringBuilder(stackalloc char[64]);
sb.Append($"pi={3.14159:F2}, hex={255:X4}, date={new System.DateTime(2025, 1, 15):yyyy-MM-dd}");
Assert.Equal("pi=3.14, hex=00FF, date=2025-01-15", sb.ToString());
sb.Dispose();
}
[Fact]
public void Interpolation_SpanFormattableTypes()
{
// Various ISpanFormattable types.
// Validates: int, double, DateTime all format directly via TryFormat (no ToString allocation).
var sb = new ValueStringBuilder(stackalloc char[64]);
sb.Append($"{42}{3.14}{true}");
Assert.Equal("423.14True", sb.ToString());
sb.Dispose();
}
[Fact]
public void Interpolation_NullString()
{
// Null string in interpolation hole.
// Validates: AppendFormatted(string?) handles null without crash.
string? name = null;
var sb = new ValueStringBuilder(stackalloc char[32]);
sb.Append($"Name: {name}!");
Assert.Equal("Name: !", sb.ToString());
sb.Dispose();
}
[Fact]
public void Interpolation_Stackalloc_DisposeAfterGrow()
{
// After Grow moves stackalloc to pool, Dispose should return the pooled array.
// Validates: _arrayToReturnToPool is correctly reconciled so Dispose works.
var sb = new ValueStringBuilder(stackalloc char[4]);
sb.Append($"grow beyond stackalloc: {12345}");
// If _arrayToReturnToPool wasn't reconciled, Dispose would either
// not return the pooled array (leak) or return null (no-op when it shouldn't be).
sb.Dispose();
// No assertion needed — if _arrayToReturnToPool was wrong, pool corruption
// would surface as test failures elsewhere. The test passing = no crash.
}
[Fact]
public void Interpolation_Heap_DoubleGrow()
{
// Force two consecutive Grows via two interpolated appends on a tiny buffer.
// Validates: reconciliation after first Grow leaves builder in valid state
// for the second Grow to succeed.
var sb = ValueStringBuilder.Create(4);
sb.Append($"First grow: {"ABCDEFGHIJ"}"); // forces first grow
sb.Append($"Second grow: {"KLMNOPQRSTUVWXYZ0123456789"}"); // forces second grow
Assert.Equal("First grow: ABCDEFGHIJSecond grow: KLMNOPQRSTUVWXYZ0123456789", sb.ToString());
sb.Dispose();
}
}

View file

@ -210,15 +210,6 @@ public ref struct ValueStringBuilder
}
}
// Compiler generated
public void Append(ref RawInterpolatedStringHandler handler) => Append(handler.Text);
// Compiler generated
public void Append(
IFormatProvider? formatProvider,
[InterpolatedStringHandlerArgument("formatProvider")]
ref RawInterpolatedStringHandler handler
) => Append(handler.Text);
[MethodImpl(MethodImplOptions.AggressiveInlining)]
@ -463,4 +454,128 @@ public ref struct ValueStringBuilder
_length -= length;
}
// Copy-and-reconcile interpolation handler.
// The handler receives a VALUE COPY of the builder (C# limitation: [InterpolatedStringHandlerArgument("")]
// passes struct receivers by value). The copy shares the same underlying char buffer (Span points to
// the same stackalloc/pooled memory), so writes go to the original buffer. If Grow() happens, the copy
// gets a new buffer. Append() reconciles by copying the handler's state back to the original.
// Safe because the game loop is single-threaded — no concurrent access between handler construction
// and reconciliation.
public void Append(
[InterpolatedStringHandlerArgument("")]
scoped ref InterpolationHandler handler)
{
// Reconcile: the handler's copy has the updated _length (and possibly new buffer from Grow).
this = handler._builder;
}
[InterpolatedStringHandler]
public ref struct InterpolationHandler
{
internal ValueStringBuilder _builder;
public InterpolationHandler(int literalLength, int formattedCount, ValueStringBuilder builder)
{
_builder = builder;
_builder.EnsureCapacity(_builder._length + literalLength + formattedCount * 11);
}
public void AppendLiteral(string value) => _builder.Append(value);
public void AppendFormatted<T>(T value) => _builder.Append(value);
public void AppendFormatted<T>(T value, string? format) => _builder.Append(value, format);
public void AppendFormatted<T>(T value, int alignment)
{
var startingPos = _builder._length;
_builder.Append(value);
if (alignment != 0)
{
AppendOrInsertAlignmentIfNeeded(startingPos, alignment);
}
}
public void AppendFormatted<T>(T value, int alignment, string? format)
{
var startingPos = _builder._length;
_builder.Append(value, format);
if (alignment != 0)
{
AppendOrInsertAlignmentIfNeeded(startingPos, alignment);
}
}
public void AppendFormatted(scoped ReadOnlySpan<char> value) => _builder.Append(value);
public void AppendFormatted(scoped ReadOnlySpan<char> value, int alignment = 0, string? format = null)
{
var leftAlign = false;
if (alignment < 0)
{
leftAlign = true;
alignment = -alignment;
}
var paddingRequired = alignment - value.Length;
if (paddingRequired <= 0)
{
_builder.Append(value);
return;
}
_builder.EnsureCapacity(_builder._length + value.Length + paddingRequired);
if (leftAlign)
{
_builder.Append(value);
_builder.AppendSpan(paddingRequired).Fill(' ');
}
else
{
_builder.AppendSpan(paddingRequired).Fill(' ');
_builder.Append(value);
}
}
public void AppendFormatted(string? value) => _builder.Append(value);
public void AppendFormatted(string? value, int alignment = 0, string? format = null) =>
AppendFormatted<string?>(value, alignment, format);
public void AppendFormatted(object? value, int alignment = 0, string? format = null) =>
AppendFormatted<object?>(value, alignment, format);
private void AppendOrInsertAlignmentIfNeeded(int startingPos, int alignment)
{
var charsWritten = _builder._length - startingPos;
var leftAlign = false;
if (alignment < 0)
{
leftAlign = true;
alignment = -alignment;
}
var paddingNeeded = alignment - charsWritten;
if (paddingNeeded <= 0)
{
return;
}
if (leftAlign)
{
_builder.AppendSpan(paddingNeeded).Fill(' ');
}
else
{
// Shift content right, fill padding on left
_builder.AppendSpan(paddingNeeded);
_builder._chars.Slice(startingPos, charsWritten)
.CopyTo(_builder._chars[(startingPos + paddingNeeded)..]);
_builder._chars.Slice(startingPos, paddingNeeded).Fill(' ');
}
}
}
}

View file

@ -210,7 +210,7 @@ public class ClientVersion : IComparable<ClientVersion>, IComparer<ClientVersion
private string ToStringImpl()
{
using var builder = ValueStringBuilder.Create();
using var builder = new ValueStringBuilder(stackalloc char[32]);
if (Type == ClientType.SA)
{

View file

@ -51,7 +51,7 @@ public class LocalizationEntry
private static void ParseText(string text, out string[] textSlices, out string stringFormatter)
{
var sb = ValueStringBuilder.Create(256);
using var sb = ValueStringBuilder.Create(256);
using var queue = PooledRefQueue<string>.Create();
var hasMatch = false;
@ -81,8 +81,6 @@ public class LocalizationEntry
textSlices = queue.ToArray();
stringFormatter = hasMatch ? sb.ToString() : null;
sb.Dispose();
}
/// <summary>

View file

@ -42,7 +42,7 @@ public static class MapSelection
public static string ToCommaDelimitedString(this MapSelectionFlags flags)
{
using var builder = ValueStringBuilder.Create();
using var builder = new ValueStringBuilder(stackalloc char[160]);
foreach (var flag in flags.GetEnumerable())
{

View file

@ -20,21 +20,21 @@ namespace Server.Text;
public interface ISelfInterpolatedStringHandler
{
public void Add([InterpolatedStringHandlerArgument("")] ref InterpolatedStringHandler handler);
public void InitializeInterpolation(int literalLength, int formattedCount);
public void AppendLiteral(string value);
public void AppendFormatted<T>(T value);
public void AppendFormatted<T>(T value, string? format);
public void AppendFormatted<T>(T value, int alignment);
public void AppendFormatted<T>(T value, int alignment, string? format);
public void AppendFormatted(ReadOnlySpan<char> value);
public void AppendFormatted(ReadOnlySpan<char> value, int alignment, string? format = null);
public void AppendFormatted(object? value, int alignment = 0, string? format = null);
public void AppendFormatted(string? value);
public void AppendFormatted(string? value, int alignment, string? format = null);
void Add([InterpolatedStringHandlerArgument("")] ref InterpolatedStringHandler handler);
void InitializeInterpolation(int literalLength, int formattedCount);
void AppendLiteral(string value);
void AppendFormatted<T>(T value);
void AppendFormatted<T>(T value, string? format);
void AppendFormatted<T>(T value, int alignment);
void AppendFormatted<T>(T value, int alignment, string? format);
void AppendFormatted(ReadOnlySpan<char> value);
void AppendFormatted(ReadOnlySpan<char> value, int alignment, string? format = null);
void AppendFormatted(object? value, int alignment = 0, string? format = null);
void AppendFormatted(string? value);
void AppendFormatted(string? value, int alignment, string? format = null);
[InterpolatedStringHandler]
public ref struct InterpolatedStringHandler
ref struct InterpolatedStringHandler
{
private ISelfInterpolatedStringHandler _parent;

View file

@ -1,6 +1,6 @@
using System.Collections.Generic;
using System.Text;
using Server.Commands.Generic;
using Server.Text;
using Server.Engines.Help;
using Server.Gumps;
using Server.Items;
@ -88,7 +88,7 @@ namespace Server.Commands
if (!reg.IsDefault)
{
var builder = new StringBuilder();
using var builder = ValueStringBuilder.Create(256);
builder.Append(reg);
reg = reg.Parent;
@ -99,7 +99,7 @@ namespace Server.Commands
reg = reg.Parent;
}
from.SendMessage($"Your region is {builder}.");
from.SendMessage($"Your region is {builder.ToString()}.");
}
}
}
@ -594,7 +594,7 @@ namespace Server.Commands
list.Sort();
var sb = new StringBuilder();
using var sb = ValueStringBuilder.Create(256);
if (list.Count > 0)
{
@ -608,7 +608,7 @@ namespace Server.Commands
if (sb.Length + 1 + v.Length >= 256)
{
m.SendAsciiMessage(0x482, sb.ToString());
sb = new StringBuilder();
sb.Reset();
sb.Append(v);
}
else

View file

@ -1,6 +1,6 @@
using System.IO;
using System.Text;
using Server.Accounting;
using Server.Text;
namespace Server.Commands
{
@ -110,11 +110,12 @@ namespace Server.Commands
return ip;
}
var sb = new StringBuilder(ip);
using var sb = ValueStringBuilder.Create(ip.Length);
sb.Append(ip);
for (var i = 0; i < m_NotSafe.Length; ++i)
{
sb.Replace(m_NotSafe[i], '_');
sb.Replace(m_NotSafe[i], '_', 0, sb.Length);
}
return sb.ToString();

View file

@ -2,7 +2,6 @@ using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Reflection;
using System.Text;
using Server.Gumps;
using Server.Items;
using Server.Text;
@ -355,7 +354,7 @@ public static class Add
var sendError = true;
var sb = new StringBuilder();
using var sb = ValueStringBuilder.Create();
sb.Append("Serials: ");
if (packs != null)

View file

@ -96,7 +96,7 @@ namespace Server.Compression
new FileInfo(destinationArchiveFileName).EnsureDirectory();
var sb = ValueStringBuilder.Create();
using var sb = ValueStringBuilder.Create();
var i = 0;
foreach (var path in paths)
{
@ -111,7 +111,6 @@ namespace Server.Compression
}
}
var pathsToCompress = sb.ToString();
sb.Dispose();
var tarFlags = compressCommand == null ? "-acf" : "-cf";
var useExternalCompression = compressCommand != null ? $"--use-compress-program \"{compressCommand}\" " : "";

View file

@ -1,12 +1,12 @@
using System;
using System.Collections.Generic;
using System.Runtime.CompilerServices;
using System.Text;
using ModernUO.Serialization;
using Server.Gumps;
using Server.Items;
using Server.Mobiles;
using Server.Targeting;
using Server.Text;
namespace Server.Engines.ConPVP;
@ -1692,7 +1692,7 @@ public sealed class BRGame : EventGame
var tourney = m_Context.m_Tournament;
var sb = new StringBuilder();
using var sb = ValueStringBuilder.Create(256);
if (tourney != null)
{
@ -1731,7 +1731,8 @@ public sealed class BRGame : EventGame
if (Controller != null)
{
sb.Append(' ').Append(Controller.Title);
sb.Append(' ');
sb.Append(Controller.Title);
}
var title = sb.ToString();
@ -1758,7 +1759,7 @@ public sealed class BRGame : EventGame
continue;
}
sb = new StringBuilder();
sb.Reset();
sb.Append(title);

View file

@ -1,12 +1,12 @@
using System;
using System.Collections.Generic;
using System.Text;
using ModernUO.Serialization;
using Server.Gumps;
using Server.Items;
using Server.Mobiles;
using Server.Network;
using Server.Targeting;
using Server.Text;
namespace Server.Engines.ConPVP;
@ -1072,7 +1072,7 @@ public sealed class CTFGame : EventGame
var tourney = m_Context.m_Tournament;
var sb = new StringBuilder();
using var sb = ValueStringBuilder.Create(256);
if (tourney != null)
{
@ -1111,7 +1111,8 @@ public sealed class CTFGame : EventGame
if (Controller != null)
{
sb.Append(' ').Append(Controller.Title);
sb.Append(' ');
sb.Append(Controller.Title);
}
var title = sb.ToString();
@ -1144,7 +1145,7 @@ public sealed class CTFGame : EventGame
// "Red v Blue CTF Champion"
sb = new StringBuilder();
sb.Reset();
sb.Append(title);

View file

@ -1,10 +1,10 @@
using System;
using System.Collections.Generic;
using System.Text;
using ModernUO.Serialization;
using Server.Gumps;
using Server.Items;
using Server.Mobiles;
using Server.Text;
namespace Server.Engines.ConPVP;
@ -648,7 +648,7 @@ public sealed class DDGame : EventGame
var tourney = m_Context.m_Tournament;
var sb = new StringBuilder();
using var sb = ValueStringBuilder.Create(256);
if (tourney != null)
{
@ -687,7 +687,8 @@ public sealed class DDGame : EventGame
if (Controller != null)
{
sb.Append(' ').Append(Controller.Title);
sb.Append(' ');
sb.Append(Controller.Title);
}
var title = sb.ToString();
@ -720,7 +721,7 @@ public sealed class DDGame : EventGame
// "Red v Blue DD Champion"
sb = new StringBuilder();
sb.Reset();
sb.Append(title);

View file

@ -1,10 +1,10 @@
using System;
using System.Collections.Generic;
using System.Text;
using ModernUO.Serialization;
using Server.Gumps;
using Server.Items;
using Server.Mobiles;
using Server.Text;
namespace Server.Engines.ConPVP;
@ -993,7 +993,7 @@ public sealed class KHGame : EventGame
var tourney = m_Context.m_Tournament;
var sb = new StringBuilder();
using var sb = ValueStringBuilder.Create(256);
if (tourney != null)
{
@ -1027,7 +1027,8 @@ public sealed class KHGame : EventGame
if (Controller != null)
{
sb.Append(' ').Append(Controller.Title);
sb.Append(' ');
sb.Append(Controller.Title);
}
var title = sb.ToString();
@ -1058,7 +1059,7 @@ public sealed class KHGame : EventGame
continue;
}
sb = new StringBuilder();
sb.Reset();
sb.Append(title);

View file

@ -1,7 +1,7 @@
using System;
using System.Collections.Generic;
using System.Text;
using Server.Network;
using Server.Text;
namespace Server.Engines.ConPVP
{
@ -11,21 +11,18 @@ namespace Server.Engines.ConPVP
{
Participants = participants;
using var sb = new ValueStringBuilder(stackalloc char[256]);
for (var i = 0; i < participants.Count; ++i)
{
var part = participants[i];
var sb = new StringBuilder();
sb.Reset();
sb.Append("Matched in a duel against ");
if (participants.Count > 2)
{
sb.AppendFormat(
"{0} other {1}: ",
participants.Count - 1,
part.Players.Count == 1 ? "players" : "teams"
);
sb.Append($"{participants.Count - 1} other {(part.Players.Count == 1 ? "players" : "teams")}: ");
}
var hasAppended = false;

View file

@ -82,7 +82,7 @@ namespace Server.Engines.ConPVP
AddImage(215, -43, 0xEE40);
using var sb = ValueStringBuilder.Create(128);
using var sb = new ValueStringBuilder(stackalloc char[64]);
if (tourney.TourneyType == TourneyType.FreeForAll)
{
@ -90,13 +90,11 @@ namespace Server.Engines.ConPVP
}
else if (tourney.TourneyType == TourneyType.RandomTeam)
{
sb.Append(tourney.ParticipantsPerMatch);
sb.Append("-Team");
sb.Append($"{tourney.ParticipantsPerMatch}-Team");
}
else if (tourney.TourneyType == TourneyType.Faction)
{
sb.Append(tourney.ParticipantsPerMatch);
sb.Append("-Team Faction");
sb.Append($"{tourney.ParticipantsPerMatch}-Team Faction");
}
else if (tourney.TourneyType == TourneyType.RedVsBlue)
{
@ -117,8 +115,7 @@ namespace Server.Engines.ConPVP
if (tourney.EventController != null)
{
sb.Append(' ');
sb.Append(tourney.EventController.Title);
sb.Append($" {tourney.EventController.Title}");
}
sb.Append(" Tournament Invitation");

View file

@ -1,9 +1,9 @@
using System.Collections.Generic;
using System.Text;
using ModernUO.Serialization;
using Server.Gumps;
using Server.Mobiles;
using Server.Network;
using Server.Text;
namespace Server.Engines.ConPVP;
@ -97,6 +97,8 @@ public class ArenaGump : Gump
AddButton(499 + 40 - 12 - 63 - 4 - 63, height - 12 - 24, 247, 248, 1);
AddButton(499 + 40 - 12 - 63, height - 12 - 24, 241, 242, 2);
var sb = new ValueStringBuilder(stackalloc char[256]);
for (var i = 0; i < list.Count; ++i)
{
var ar = list[i];
@ -112,7 +114,7 @@ public class ArenaGump : Gump
AddBorderedText(x + 5, y + 5, 115 - 5, ar.Name ?? "(no name)", color, 0);
x += 115;
var sb = new StringBuilder();
sb.Reset();
if (ar.Players.Count > 0)
{
@ -154,10 +156,10 @@ public class ArenaGump : Gump
}
}
Append(sb, p1);
Append(sb, p2);
Append(sb, p3);
Append(sb, p4);
Append(ref sb, p1);
Append(ref sb, p2);
Append(ref sb, p3);
Append(ref sb, p4);
if (ar.Players.Count > 4)
{
@ -174,9 +176,11 @@ public class ArenaGump : Gump
AddBorderedText(x, y + 5, 40, Html.Center($"{ar.Spectators}"), color, 0);
}
sb.Dispose();
}
private void Append(StringBuilder sb, LadderEntry le)
private void Append(ref ValueStringBuilder sb, LadderEntry le)
{
if (le == null)
{

View file

@ -92,7 +92,7 @@ namespace Server.Engines.ConPVP
AddImage(215, -43, 0xEE40);
// AddImage( 330, 141, 0x8BA );
using var sb = ValueStringBuilder.Create(128);
using var sb = new ValueStringBuilder(stackalloc char[64]);
if (tourney.TourneyType == TourneyType.FreeForAll)
{
@ -100,13 +100,11 @@ namespace Server.Engines.ConPVP
}
else if (tourney.TourneyType == TourneyType.RandomTeam)
{
sb.Append(tourney.ParticipantsPerMatch);
sb.Append("-Team");
sb.Append($"{tourney.ParticipantsPerMatch}-Team");
}
else if (tourney.TourneyType == TourneyType.Faction)
{
sb.Append(tourney.ParticipantsPerMatch);
sb.Append("-Team Faction");
sb.Append($"{tourney.ParticipantsPerMatch}-Team Faction");
}
else if (tourney.TourneyType == TourneyType.RedVsBlue)
{
@ -127,8 +125,7 @@ namespace Server.Engines.ConPVP
if (tourney.EventController != null)
{
sb.Append(' ');
sb.Append(tourney.EventController.Title);
sb.Append($" {tourney.EventController.Title}");
}
sb.Append(" Tournament Signup");

View file

@ -1,7 +1,6 @@
using System;
using System.Collections;
using System.Collections.Generic;
using System.Text;
using Server.Gumps;
using Server.Mobiles;
using Server.Network;
@ -60,8 +59,7 @@ namespace Server.Engines.ConPVP
}
else if (tourney.TourneyType == TourneyType.RandomTeam)
{
sb.Append(tourney.ParticipantsPerMatch);
sb.Append("-Team");
sb.Append($"{tourney.ParticipantsPerMatch}-Team");
}
else if (tourney.TourneyType == TourneyType.RedVsBlue)
{
@ -69,8 +67,7 @@ namespace Server.Engines.ConPVP
}
else if (tourney.TourneyType == TourneyType.Faction)
{
sb.Append(tourney.ParticipantsPerMatch);
sb.Append("-Team Faction");
sb.Append($"{tourney.ParticipantsPerMatch}-Team Faction");
}
else
{
@ -87,8 +84,7 @@ namespace Server.Engines.ConPVP
if (tourney.EventController != null)
{
sb.Append(' ');
sb.Append(tourney.EventController.Title);
sb.Append($" {tourney.EventController.Title}");
}
sb.Append(" Tournament Bracket");
@ -461,7 +457,7 @@ namespace Server.Engines.ConPVP
color = 0x666666;
}
var sb = new StringBuilder();
using var sb = ValueStringBuilder.Create(512);
if (m_Tournament.TourneyType == TourneyType.Standard)
{

View file

@ -1,6 +1,6 @@
using System;
using System.Text;
using Server.Mobiles;
using Server.Text;
namespace Server.Engines.ConPVP
{
@ -74,7 +74,7 @@ namespace Server.Engines.ConPVP
{
get
{
var sb = new StringBuilder();
using var sb = ValueStringBuilder.Create(256);
for (var i = 0; i < Players.Length; ++i)
{

View file

@ -1,9 +1,9 @@
using System;
using System.Collections.Generic;
using System.Text;
using Server.Factions;
using Server.Items;
using Server.Regions;
using Server.Text;
namespace Server.Engines.ConPVP
{
@ -237,7 +237,7 @@ namespace Server.Engines.ConPVP
return;
}
var sb = new StringBuilder();
using var sb = ValueStringBuilder.Create(512);
sb.Append("The match has ended in a tie ");
@ -284,7 +284,7 @@ namespace Server.Engines.ConPVP
{
case TieType.FullAdvancement:
{
sb.AppendFormat("In accordance with the rules, {0} parties are advanced.", whole);
sb.Append($"In accordance with the rules, {whole} parties are advanced.");
break;
}
case TieType.FullElimination:
@ -294,7 +294,7 @@ namespace Server.Engines.ConPVP
Undefeated.Remove(remaining[j]);
}
sb.AppendFormat("In accordance with the rules, {0} parties are eliminated.", whole);
sb.Append($"In accordance with the rules, {whole} parties are eliminated.");
break;
}
case TieType.Random:
@ -311,11 +311,7 @@ namespace Server.Engines.ConPVP
if (advanced != null)
{
sb.AppendFormat(
"In accordance with the rules, {0} {1} advanced.",
advanced.NameList,
advanced.Players.Count == 1 ? "is" : "are"
);
sb.Append($"In accordance with the rules, {advanced.NameList} {(advanced.Players.Count == 1 ? "is" : "are")} advanced.");
}
break;
@ -344,11 +340,7 @@ namespace Server.Engines.ConPVP
if (advanced != null)
{
sb.AppendFormat(
"In accordance with the rules, {0} {1} advanced.",
advanced.NameList,
advanced.Players.Count == 1 ? "is" : "are"
);
sb.Append($"In accordance with the rules, {advanced.NameList} {(advanced.Players.Count == 1 ? "is" : "are")} advanced.");
}
break;
@ -377,11 +369,7 @@ namespace Server.Engines.ConPVP
if (advanced != null)
{
sb.AppendFormat(
"In accordance with the rules, {0} {1} advanced.",
advanced.NameList,
advanced.Players.Count == 1 ? "is" : "are"
);
sb.Append($"In accordance with the rules, {advanced.NameList} {(advanced.Players.Count == 1 ? "is" : "are")} advanced.");
}
break;
@ -432,7 +420,7 @@ namespace Server.Engines.ConPVP
public void HandleWon(Arena arena, TourneyMatch match, TourneyParticipant winner)
{
var sb = new StringBuilder();
using var sb = ValueStringBuilder.Create(512);
sb.Append("The match is complete. ");
sb.Append(winner.NameList);
@ -448,11 +436,7 @@ namespace Server.Engines.ConPVP
if (match.Participants.Count > 2)
{
sb.AppendFormat(
"{0} other {1}: ",
match.Participants.Count - 1,
winner.Players.Count == 1 ? "players" : "teams"
);
sb.Append($"{match.Participants.Count - 1} other {(winner.Players.Count == 1 ? "players" : "teams")}: ");
}
var hasAppended = false;
@ -592,7 +576,7 @@ namespace Server.Engines.ConPVP
cash /= 1000;
cash *= 1000;
var sb = new StringBuilder();
using var sb = ValueStringBuilder.Create(256);
if (TourneyType == TourneyType.FreeForAll)
{
@ -628,7 +612,8 @@ namespace Server.Engines.ConPVP
if (EventController != null)
{
sb.Append(' ').Append(EventController.Title);
sb.Append(' ');
sb.Append(EventController.Title);
}
sb.Append(" Champion");

View file

@ -1,6 +1,6 @@
using System;
using System.Collections.Generic;
using System.Text;
using Server.Text;
namespace Server.Engines.ConPVP
{
@ -56,7 +56,7 @@ namespace Server.Engines.ConPVP
{
get
{
var sb = new StringBuilder();
using var sb = ValueStringBuilder.Create(256);
for (var i = 0; i < Players.Count; ++i)
{

View file

@ -45,7 +45,7 @@ public sealed class UnholySense : Power
++enemyCount;
}
using var sb = ValueStringBuilder.Create();
using var sb = new ValueStringBuilder(stackalloc char[96]);
sb.Append($"You sense {(enemyCount == 0 ? "no" : enemyCount.ToString())} {(enemyCount == 1 ? "enemy" : "enemies")}");
if (primary != null)

View file

@ -45,7 +45,7 @@ public sealed class HolySense : Power
++enemyCount;
}
using var sb = ValueStringBuilder.Create();
using var sb = new ValueStringBuilder(stackalloc char[96]);
sb.Append($"You sense {(enemyCount == 0 ? "no" : enemyCount.ToString())} {(enemyCount == 1 ? "enemy" : "enemies")}");
if (primary != null)

View file

@ -1,7 +1,7 @@
using System;
using System.Collections.Generic;
using System.Text;
using Server.Accounting;
using Server.Text;
using Server.Gumps;
using Server.Network;
@ -62,7 +62,7 @@ namespace Server.Engines.Help
var max = log.Count - (lastPage - page) * MaxEntriesPerPage;
var min = Math.Max(max - MaxEntriesPerPage, 0);
var builder = new StringBuilder();
using var builder = ValueStringBuilder.Create();
for (var i = min; i < max; i++)
{

View file

@ -163,43 +163,27 @@ public static class BountyMessage
{
case 0:
{
lineBuilder.Append("Bounty for");
lineBuilder.Append(player.RawName);
lineBuilder.Append("!");
break;
lineBuilder.Append($"Bounty for {player.RawName}!"); break;
}
case 1:
{
lineBuilder.Append(player.RawName);
lineBuilder.Append(" must die!");
break;
lineBuilder.Append($"{player.RawName} must die!"); break;
}
case 2:
{
lineBuilder.Append("A price on ");
lineBuilder.Append(player.RawName);
lineBuilder.Append('!');
break;
lineBuilder.Append($"A price on {player.RawName}!"); break;
}
case 3:
{
lineBuilder.Append(player.RawName);
lineBuilder.Append(" outlawed!");
break;
lineBuilder.Append($"{player.RawName} outlawed!"); break;
}
case 4:
{
lineBuilder.Append("Execute ");
lineBuilder.Append(player.RawName);
lineBuilder.Append('!');
break;
lineBuilder.Append($"Execute {player.RawName}!"); break;
}
default:
{
lineBuilder.Append("WANTED: ");
lineBuilder.Append(player.RawName);
lineBuilder.Append('!');
break;
lineBuilder.Append($"WANTED: {player.RawName}!"); break;
}
}
@ -248,21 +232,7 @@ public static class BountyMessage
_ => " Lord British's bounty "
};
paraBuilder.Append("The foul scum known as ");
paraBuilder.Append(player.RawName);
paraBuilder.Append(' ');
paraBuilder.Append(verb);
paraBuilder.Append(" For ");
paraBuilder.Append(pronoun);
paraBuilder.Append(" is responsible for ");
paraBuilder.Append(player.Kills);
paraBuilder.Append(" murders. ");
paraBuilder.Append(intro);
paraBuilder.Append(" of ");
paraBuilder.Append(bounty);
paraBuilder.Append(" gold pieces for ");
paraBuilder.Append(possessive);
paraBuilder.Append(" head!");
paraBuilder.Append($"The foul scum known as {player.RawName} {verb} For {pronoun} is responsible for {player.Kills} murders. {intro} of {bounty} gold pieces for {possessive} head!");
// Word-wrap at 28 chars and write each line directly (matching uo98 bountyboard.m CONST:28)
WriteWordWrappedLines(ref writer, paraBuilder.AsSpan(), 28, textBuffer, ref lineCount);
@ -276,22 +246,17 @@ public static class BountyMessage
lineCount++;
lineBuilder.Reset();
lineBuilder.Append(" - ");
lineBuilder.Append(GetHairStyle(player.HairItemID));
lineBuilder.Append($" - {GetHairStyle(player.HairItemID)}");
writer.WriteString(lineBuilder.AsSpan(), textBuffer, true);
lineCount++;
lineBuilder.Reset();
lineBuilder.Append(" - ");
lineBuilder.Append(GetHairColor(player.HairHue));
lineBuilder.Append(" hair");
lineBuilder.Append($" - {GetHairColor(player.HairHue)} hair");
writer.WriteString(lineBuilder.AsSpan(), textBuffer, true);
lineCount++;
lineBuilder.Reset();
lineBuilder.Append(" - ");
lineBuilder.Append(GetSkinTone(player.Hue));
lineBuilder.Append(" skin");
lineBuilder.Append($" - {GetSkinTone(player.Hue)} skin");
writer.WriteString(lineBuilder.AsSpan(), textBuffer, true);
lineCount++;
@ -300,9 +265,7 @@ public static class BountyMessage
lineCount++;
lineBuilder.Reset();
lineBuilder.Append("If you kill ");
lineBuilder.Append(objective);
lineBuilder.Append(", remove the");
lineBuilder.Append($"If you kill {objective}, remove the");
writer.WriteString(lineBuilder.AsSpan(), textBuffer, true);
lineCount++;

View file

@ -1119,12 +1119,7 @@ namespace Server.Gumps
}
var c = a.Comments[i];
sb.Append('[');
sb.Append(c.AddedBy);
sb.Append(" on ");
sb.Append(c.LastModified.ToString());
sb.Append("]<BR>");
sb.Append(c.Content);
sb.Append($"[{c.AddedBy} on {c.LastModified}]<BR>{c.Content}");
}
AddHtml(20, 180, 380, 190, sb.ToString(), true, true);
@ -1159,9 +1154,7 @@ namespace Server.Gumps
var tag = a.Tags[i];
sb.Append(tag.Name);
sb.Append(" = ");
sb.Append(tag.Value);
sb.Append($"{tag.Name} = {tag.Value}");
}
AddHtml(20, 180, 380, 190, sb.ToString(), true, true);

View file

@ -1561,7 +1561,7 @@ namespace Server.Items
if (isMagicItem)
{
var builder = ValueStringBuilder.Create(128);
var builder = new ValueStringBuilder(stackalloc char[128]);
var durabilityText = DurabilityText(out var articleAnDurability);
if (durabilityText != null)

View file

@ -305,7 +305,7 @@ public partial class HouseRaffleStone : Item
public static string FormatLocation(Point3D loc, Map map, bool displayMap)
{
using var result = ValueStringBuilder.Create();
using var result = new ValueStringBuilder(stackalloc char[48]);
var xLong = 0;
var yLat = 0;

View file

@ -3451,7 +3451,7 @@ public abstract partial class BaseWeapon
if (isMagicItem)
{
var builder = ValueStringBuilder.Create(128);
var builder = new ValueStringBuilder(stackalloc char[160]);
var durabilityText = DurabilityText(out var articleAnDurability);
if (durabilityText != null)

View file

@ -94,7 +94,7 @@ namespace Server.Misc
return "There are no clients supported at this time.";
}
using var builder = ValueStringBuilder.Create();
using var builder = new ValueStringBuilder(stackalloc char[192]);
builder.Append("Please connect with a ");
uint flags = 0;
var i = 0;
@ -115,7 +115,7 @@ namespace Server.Misc
public static void ClientVersionReceived(NetState state, ClientVersion version)
{
var sb = ValueStringBuilder.Create();
using var sb = ValueStringBuilder.Create();
if (!_enable || state.Mobile?.AccessLevel != AccessLevel.Player)
{
@ -202,8 +202,6 @@ namespace Server.Misc
}
}
}
sb.Dispose();
}
private static void OnKick(NetState ns)

View file

@ -1,7 +1,7 @@
using System;
using System.Collections.Generic;
using System.Text;
using ModernUO.Serialization;
using Server.Text;
using Server.Gumps;
using Server.Items;
using Server.Network;
@ -231,7 +231,7 @@ public class TownCrierGump : Gump
var toExpire = Utility.Max(tce.ExpireTime - Core.Now, TimeSpan.Zero);
var sb = new StringBuilder();
using var sb = ValueStringBuilder.Create(512);
sb.Append("[Expires: ");

View file

@ -71,7 +71,7 @@ namespace Server.SkillHandlers
if (c.Looters.Count > 0)
{
var sb = ValueStringBuilder.Create(128);
using var sb = new ValueStringBuilder(stackalloc char[128]);
var i = 0;
foreach (var looter in c.Looters)
{
@ -93,7 +93,6 @@ namespace Server.SkillHandlers
// This body has been disturbed by ~1_PLAYER_NAMES~
from.SendLocalizedMessage(1042752, sb.ToString());
sb.Dispose();
}
else
{

View file

@ -1,5 +1,4 @@
using System;
using System.Text;
using CommunityToolkit.HighPerformance;
using Server.Commands;
using Server.Factions;
@ -7,6 +6,7 @@ using Server.Gumps;
using Server.Items;
using Server.Mobiles;
using Server.Network;
using Server.Text;
namespace Server.Misc
{
@ -703,7 +703,7 @@ namespace Server.Misc
Array.Sort(strings);
var sb = new StringBuilder();
using var sb = ValueStringBuilder.Create(256);
if (strings.Length > 0)
{
@ -728,7 +728,7 @@ namespace Server.Misc
sb.ToString()
);
sb = new StringBuilder();
sb.Reset();
sb.Append(v);
}
else