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

@ -26,6 +26,7 @@ Apply these when writing or reviewing `.cs` files under `Projects/`.
14. **PropertyList string literals must be holes**`$"{"Map"}\t{value}"` not `$"Map\t{value}"`. The handler treats bare text as delimiters, `{}` holes as arguments. Only `\t` should be a bare literal → `dev-docs/property-lists.md` 14. **PropertyList string literals must be holes**`$"{"Map"}\t{value}"` not `$"Map\t{value}"`. The handler treats bare text as delimiters, `{}` holes as arguments. Only `\t` should be a bare literal → `dev-docs/property-lists.md`
15. **Braces required on all control flow**`if`, `else`, `for`, `foreach`, `while`, `do`, `switch` must always have braces, even for single-line bodies → `dev-docs/code-standards.md` 15. **Braces required on all control flow**`if`, `else`, `for`, `foreach`, `while`, `do`, `switch` must always have braces, even for single-line bodies → `dev-docs/code-standards.md`
16. **Prefer switch expressions and switch-when** — use switch expressions for value mapping and switch-when for pattern matching where they improve readability. Exception: skip if unreadable or cold path → `dev-docs/code-standards.md` 16. **Prefer switch expressions and switch-when** — use switch expressions for value mapping and switch-when for pattern matching where they improve readability. Exception: skip if unreadable or cold path → `dev-docs/code-standards.md`
17. **No `System.Text.StringBuilder`** — use `ValueStringBuilder` with `stackalloc` (bounded output) or `ValueStringBuilder.Create()` (unbounded). Supports `$"..."` interpolation directly. Always use `using var` for disposal. Use `Reset()` instead of reassigning → `dev-docs/string-handling.md`
## Dev-Docs Reference ## Dev-Docs Reference
@ -45,6 +46,7 @@ Apply these when writing or reviewing `.cs` files under `Projects/`.
| Configuration system | `dev-docs/configuration.md` | | Configuration system | `dev-docs/configuration.md` |
| Networking & packets | `dev-docs/networking-packets.md` | | Networking & packets | `dev-docs/networking-packets.md` |
| Region system | `dev-docs/regions.md` | | Region system | `dev-docs/regions.md` |
| String handling & ValueStringBuilder | `dev-docs/string-handling.md` |
| RunUO migration (overview) | `dev-docs/runuo-migration-docs/00-overview.md` | | RunUO migration (overview) | `dev-docs/runuo-migration-docs/00-overview.md` |
| RunUO migration (all docs) | `dev-docs/runuo-migration-docs/` | | RunUO migration (all docs) | `dev-docs/runuo-migration-docs/` |
@ -73,6 +75,7 @@ Then copy only the relevant skill files based on the task:
| Timer work | `modernuo-timers`, `modernuo-serialization` | | Timer work | `modernuo-timers`, `modernuo-serialization` |
| Config system | `modernuo-configuration` | | Config system | `modernuo-configuration` |
| Era-conditional code | `modernuo-era-expansion` | | Era-conditional code | `modernuo-era-expansion` |
| String building / formatting | `modernuo-string-handling` |
| Code review / audit | `modernuo-code-audit` | | Code review / audit | `modernuo-code-audit` |
| Any `.cs` file edit | `modernuo-code-audit` (always offer for code changes) | | Any `.cs` file edit | `modernuo-code-audit` (always offer for code changes) |
| **RunUO Migration** | | | **RunUO Migration** | |

View file

@ -46,4 +46,249 @@ public class ValueStringBuilderTests
Assert.Equal($"Hi, this is {value}. I am a string.", sb.ToString()); Assert.Equal($"Hi, this is {value}. I am a string.", sb.ToString());
sb.Dispose(); 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)] [MethodImpl(MethodImplOptions.AggressiveInlining)]
@ -463,4 +454,128 @@ public ref struct ValueStringBuilder
_length -= length; _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() private string ToStringImpl()
{ {
using var builder = ValueStringBuilder.Create(); using var builder = new ValueStringBuilder(stackalloc char[32]);
if (Type == ClientType.SA) 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) 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(); using var queue = PooledRefQueue<string>.Create();
var hasMatch = false; var hasMatch = false;
@ -81,8 +81,6 @@ public class LocalizationEntry
textSlices = queue.ToArray(); textSlices = queue.ToArray();
stringFormatter = hasMatch ? sb.ToString() : null; stringFormatter = hasMatch ? sb.ToString() : null;
sb.Dispose();
} }
/// <summary> /// <summary>

View file

@ -42,7 +42,7 @@ public static class MapSelection
public static string ToCommaDelimitedString(this MapSelectionFlags flags) 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()) foreach (var flag in flags.GetEnumerable())
{ {

View file

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

View file

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

View file

@ -1,6 +1,6 @@
using System.IO; using System.IO;
using System.Text;
using Server.Accounting; using Server.Accounting;
using Server.Text;
namespace Server.Commands namespace Server.Commands
{ {
@ -110,11 +110,12 @@ namespace Server.Commands
return ip; 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) 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(); return sb.ToString();

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -45,7 +45,7 @@ public sealed class UnholySense : Power
++enemyCount; ++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")}"); sb.Append($"You sense {(enemyCount == 0 ? "no" : enemyCount.ToString())} {(enemyCount == 1 ? "enemy" : "enemies")}");
if (primary != null) if (primary != null)

View file

@ -45,7 +45,7 @@ public sealed class HolySense : Power
++enemyCount; ++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")}"); sb.Append($"You sense {(enemyCount == 0 ? "no" : enemyCount.ToString())} {(enemyCount == 1 ? "enemy" : "enemies")}");
if (primary != null) if (primary != null)

View file

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

View file

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

View file

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

View file

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

View file

@ -305,7 +305,7 @@ public partial class HouseRaffleStone : Item
public static string FormatLocation(Point3D loc, Map map, bool displayMap) 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 xLong = 0;
var yLat = 0; var yLat = 0;

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -0,0 +1,133 @@
# ModernUO String Handling Skill
## When This Skill Applies
- Any code that builds strings dynamically (concatenation, formatting, interpolation)
- Converting `System.Text.StringBuilder` to `ValueStringBuilder`
- Packet string construction
- Gump/message text building
## Core Rule
**Never use `System.Text.StringBuilder`**. Use `Server.Text.ValueStringBuilder` everywhere.
## Quick Reference
### Construction
```csharp
// Bounded output (preferred): zero heap allocation
using var sb = new ValueStringBuilder(stackalloc char[128]);
// Unbounded output: rents from STArrayPool
using var sb = ValueStringBuilder.Create(256);
using var sb = ValueStringBuilder.Create(); // default 64 chars
```
### String Interpolation
Works with stackalloc — writes directly into the builder's buffer:
```csharp
using var sb = new ValueStringBuilder(stackalloc char[64]);
sb.Append($"Player {name} has {kills} kills");
```
### Reuse via Reset
Use `Reset()` instead of creating a new builder:
```csharp
using var sb = new ValueStringBuilder(stackalloc char[128]);
foreach (var item in items)
{
sb.Reset();
sb.Append($"{item.Name}: {item.Value}");
Process(sb.ToString());
}
```
### Reading Results
- `sb.ToString()` — when you need a string (allocates)
- `sb.AsSpan()` — when consumer accepts `ReadOnlySpan<char>` (zero-alloc)
## Common Mistakes
### 1. Using StringBuilder
```csharp
// BAD
var sb = new StringBuilder();
sb.Append(name);
return sb.ToString();
// GOOD
using var sb = new ValueStringBuilder(stackalloc char[64]);
sb.Append(name);
return sb.ToString();
```
### 2. Forgetting `using var`
```csharp
// BAD: pooled array may leak if Grow() happened
var sb = ValueStringBuilder.Create();
return sb.ToString(); // never disposed!
// GOOD
using var sb = ValueStringBuilder.Create();
return sb.ToString();
```
### 3. Chaining Append calls
```csharp
// BAD: VSB Append returns void, not this
sb.Append("a").Append("b");
// GOOD
sb.Append("a");
sb.Append("b");
// BETTER: use interpolation
sb.Append($"a{value}b");
```
### 4. Reassigning a using variable
```csharp
// BAD: can't reassign using var
using var sb = ValueStringBuilder.Create();
sb = ValueStringBuilder.Create(); // CS1656!
// GOOD: use Reset()
using var sb = ValueStringBuilder.Create();
sb.Reset();
```
### 5. `using var` with `ref` extension methods
```csharp
// BAD: CS1657 — using var can't be passed by ref
using var sb = new ValueStringBuilder(stackalloc char[64]);
sb.AppendSpaceWithArticle(text, articleAn); // takes ref VSB
// GOOD: manual Dispose
var sb = new ValueStringBuilder(stackalloc char[64]);
sb.AppendSpaceWithArticle(text, articleAn);
var result = sb.ToString();
sb.Dispose();
```
### 6. No AppendFormat — use `$"..."` interpolation
```csharp
// BAD: AppendFormat doesn't exist on VSB (no object[] params equivalent)
sb.AppendFormat("{0:N0} points, {1:N0} kills", score, kills);
// GOOD: use interpolation with format specifiers (zero boxing, zero intermediate strings)
sb.Append($"{score:N0} points, {kills:N0} kills");
```
## Capacity Sizing Guide
| Content | Recommended |
|---|---|
| Version strings, coordinates | `stackalloc char[32-48]` |
| Player names, short messages | `stackalloc char[64]` |
| Item descriptions, titles | `stackalloc char[128]` |
| Paragraph text, HTML snippets | `stackalloc char[256]` |
| Large HTML, gump content | `Create(512)` or `Create()` |
| Unbounded (logs, file paths) | `Create()` |
## Related Docs
- `dev-docs/string-handling.md` — full reference
- `dev-docs/code-standards.md` — memory management rules
- `dev-docs/property-lists.md` — IPropertyList string interpolation (different handler)

View file

@ -486,7 +486,7 @@ finally
- Use `PooledRefList<T>` instead of `new List<T>()` - Use `PooledRefList<T>` instead of `new List<T>()`
- Use `stackalloc` for small fixed-size buffers - Use `stackalloc` for small fixed-size buffers
- Use `STArrayPool<T>` for larger buffers - Use `STArrayPool<T>` for larger buffers
- Avoid string concatenation in loops (use `StringBuilder` or string interpolation in `IPropertyList`) - Use `ValueStringBuilder` with `stackalloc` for string building (never `System.Text.StringBuilder`) → `dev-docs/string-handling.md`
--- ---

220
dev-docs/string-handling.md Normal file
View file

@ -0,0 +1,220 @@
# String Handling in ModernUO
This document covers the string building utilities in `Projects/Server/Text/` and `Projects/Server/Buffers/`, when to use each, and how to avoid common allocation pitfalls.
## Table of Contents
1. [ValueStringBuilder](#valuestringbuilder)
2. [RawInterpolatedStringHandler](#rawinterpolatedstringhandler)
3. [StringHelpers](#stringhelpers)
4. [TextEncoding](#textencoding)
5. [Decision Guide](#decision-guide)
---
## ValueStringBuilder
**Location**: `Projects/Server/Buffers/ValueStringBuilder.cs`
**Namespace**: `Server.Text`
A `ref struct` string builder that avoids heap allocations entirely when backed by `stackalloc`. This is the **preferred** string builder for all ModernUO code — do not use `System.Text.StringBuilder`.
### Construction Patterns
**Stackalloc (preferred for bounded output)**:
```csharp
// Best: zero heap allocation, zero pool overhead
using var sb = new ValueStringBuilder(stackalloc char[128]);
sb.Append($"Hello {name}, score: {score}");
return sb.ToString();
```
**Pooled (for unbounded or large output)**:
```csharp
// Rents from STArrayPool — returned on Dispose
using var sb = ValueStringBuilder.Create(256);
// or with default capacity (64):
using var sb = ValueStringBuilder.Create();
```
### Choosing Capacity
| Output size | Pattern |
|---|---|
| Known, <=256 chars | `new ValueStringBuilder(stackalloc char[N])` |
| Known, >256 chars | `ValueStringBuilder.Create(N)` |
| Unbounded/unknown | `ValueStringBuilder.Create()` (grows automatically) |
If the stackalloc buffer is too small, the builder automatically grows to a pooled array. This is safe but costs a pool rent — size the stackalloc to fit the expected output.
### String Interpolation (`$"..."`)
`ValueStringBuilder` supports `$"..."` syntax via a copy-and-reconcile `InterpolationHandler`. This writes directly into the builder's buffer — **no intermediate allocation**, even with stackalloc.
```csharp
// Works with stackalloc — zero allocation
using var sb = new ValueStringBuilder(stackalloc char[64]);
sb.Append($"Player {name} has {kills} kills");
sb.Append($" and {bounty} gold bounty");
```
**How it works**: The compiler passes a value copy of the builder to the handler. The copy shares the same underlying buffer (Span points to the same memory), so writes go to the original buffer. `Append()` reconciles by copying the handler's updated state back to the original. If the handler triggers a `Grow()`, the reconciliation updates the buffer reference.
### Reusing a Builder
Use `Reset()` to clear the builder for reuse instead of creating a new one:
```csharp
using var sb = new ValueStringBuilder(stackalloc char[128]);
foreach (var item in items)
{
sb.Reset(); // clear for next iteration
sb.Append($"{item.Name}: {item.Value}");
Process(sb.ToString());
}
```
### Reading the Result
| Method | Use when |
|---|---|
| `sb.ToString()` | You need a `string` (allocates) |
| `sb.AsSpan()` | You can consume a `ReadOnlySpan<char>` (zero-alloc) |
| `sb.AsSpan(terminate: true)` | You need a null-terminated span |
### Disposal
Always use `using var` for automatic disposal:
```csharp
using var sb = new ValueStringBuilder(stackalloc char[64]);
```
If `using var` is not possible (e.g., the builder is passed by `ref` to extension methods, or `goto case` in switch blocks), use manual `Dispose()`:
```csharp
var sb = new ValueStringBuilder(stackalloc char[64]);
sb.AppendSpaceWithArticle(text, articleAn); // takes ref ValueStringBuilder
var result = sb.ToString();
sb.Dispose();
```
For stackalloc-only builders that never grow, `Dispose()` is a no-op. But always call it defensively — if a future change triggers growth, the pooled array needs returning.
### Limitations
- **No `AppendFormat`**: Use `$"..."` interpolation instead — it's more readable and zero-allocation:
```csharp
// StringBuilder (old):
sb.AppendFormat("{0:N0} points, {1:N0} kills", score, kills);
// ValueStringBuilder:
sb.Append($"{score:N0} points, {kills:N0} kills");
```
There is no `object[] params` equivalent for format strings. All formatting goes through `$"..."` interpolation which uses `ISpanFormattable.TryFormat` directly — zero boxing, zero intermediate strings.
- **No chained Append**: `Append()` returns `void`, not `this`. Write `sb.Append(a); sb.Append(b);` instead of `sb.Append(a).Append(b)`.
- **Ref struct constraints**: Cannot be stored in fields, captured by lambdas, or used in `async` methods. Scoped to the declaring method.
- **`using var` + `ref` conflict**: A `using` variable cannot be passed by `ref`. If extension methods take `ref ValueStringBuilder`, use manual `Dispose()` instead.
---
## RawInterpolatedStringHandler
**Location**: `Projects/Server/Buffers/RawInterpolatedStringHandler.cs`
**Namespace**: `Server.Buffers`
A `[InterpolatedStringHandler]` ref struct used internally by `ValueStringBuilder`'s interpolation support. You should not need to use this directly — use `sb.Append($"...")` instead.
---
## StringHelpers
**Location**: `Projects/Server/Text/StringHelpers.cs`
**Namespace**: `Server.Text`
Extension methods for common string operations:
| Method | Description |
|---|---|
| `Wrap(string, int perLine, int maxLines)` | Word-wrap text into lines |
| `AppendSpaceWithArticle(ref ValueStringBuilder, string, bool)` | Append with "a"/"an" article prefix |
| `Remove(ReadOnlySpan<char>, ...)` | Filter substrings from spans |
| `Capitalize(string)` | Title-case with "the" handling |
| `TrimMultiline(string)` | Trim each line in multiline text |
---
## TextEncoding
**Location**: `Projects/Server/Text/TextEncoding.cs`
**Namespace**: `Server.Text`
UTF-8/Unicode encoding utilities used by the networking layer:
| Method | Description |
|---|---|
| `GetBytesUtf8(string, Span<byte>)` | Encode string to UTF-8 in buffer |
| `GetBytesUtf8(ReadOnlySpan<char>, Span<byte>)` | Encode char span to UTF-8 |
| `GetStringUtf8(Span<byte>)` | Decode UTF-8 bytes to string (with filtering) |
---
## Decision Guide
### When to use what
```
Need to build a string?
├── In a hot path (packets, ticks, spatial queries)?
│ └── ValueStringBuilder with stackalloc
├── In game content (gumps, messages, commands)?
│ └── ValueStringBuilder with stackalloc (bounded) or Create() (unbounded)
├── Building an ObjectPropertyList tooltip?
│ └── Use IPropertyList.Add($"...") — has its own handler
├── In async/multi-threaded code (rare)?
│ └── ValueStringBuilder.CreateMT() or System.Text.StringBuilder
└── Never → System.Text.StringBuilder
```
### Do NOT use `System.Text.StringBuilder`
`ValueStringBuilder` replaces `StringBuilder` in all game code. It avoids:
- GC pressure from `StringBuilder`'s internal `char[]` allocations
- The `StringBuilder` object allocation itself (24+ bytes on heap)
- Thread-safe overhead in `ArrayPool<char>.Shared` (VSB uses lock-free `STArrayPool`)
### Common patterns
**Instead of string concatenation**:
```csharp
// BAD: allocates intermediate strings
var msg = "Player " + name + " has " + kills + " kills";
// GOOD: zero allocation with stackalloc
using var sb = new ValueStringBuilder(stackalloc char[64]);
sb.Append($"Player {name} has {kills} kills");
var msg = sb.ToString(); // single allocation for the final string
```
**Instead of StringBuilder**:
```csharp
// BAD: StringBuilder allocates on heap
var sb = new StringBuilder();
sb.Append(name);
sb.Append(": ");
sb.Append(value);
return sb.ToString();
// GOOD: ValueStringBuilder with stackalloc
using var sb = new ValueStringBuilder(stackalloc char[64]);
sb.Append($"{name}: {value}");
return sb.ToString();
```
**For packet string construction** (hot path):
```csharp
// Use AsSpan() to avoid ToString() allocation when the consumer accepts spans
using var sb = new ValueStringBuilder(stackalloc char[32]);
sb.Append(bounty);
sb.Append(" gold");
writer.WriteString(sb.AsSpan(), textBuffer); // zero-copy
```