diff --git a/Projects/Server/Buffers/PooledArraySpanFormattable.cs b/Projects/Server/Buffers/PooledArraySpanFormattable.cs new file mode 100644 index 000000000..50e7df384 --- /dev/null +++ b/Projects/Server/Buffers/PooledArraySpanFormattable.cs @@ -0,0 +1,71 @@ +/************************************************************************* + * ModernUO * + * Copyright 2019-2022 - ModernUO Development Team * + * Email: hi@modernuo.com * + * File: PooledArraySpanFormattable.cs * + * * + * This program is free software: you can redistribute it and/or modify * + * it under the terms of the GNU General Public License as published by * + * the Free Software Foundation, either version 3 of the License, or * + * (at your option) any later version. * + * * + * You should have received a copy of the GNU General Public License * + * along with this program. If not, see . * + *************************************************************************/ + +#nullable enable +using System; +using System.Buffers; + +namespace Server.Buffers; + +public struct PooledArraySpanFormattable : ISpanFormattable, IDisposable +{ + private char[] _arrayToReturnToPool; + private int _pos; + + public PooledArraySpanFormattable(char[] arrayToReturnToPool, int length) + { + _arrayToReturnToPool = arrayToReturnToPool; + _pos = length; + } + + public ReadOnlySpan Chars => _arrayToReturnToPool.AsSpan(.._pos); + + public static implicit operator string(PooledArraySpanFormattable f) => f.ToString(); + + public string ToString(string? format = null, IFormatProvider formatProvider = null) + { + var result = new string(_arrayToReturnToPool.AsSpan(0, _pos)); + Dispose(); + + return result; + } + + public bool TryFormat( + Span destination, out int charsWritten, ReadOnlySpan format = default, + IFormatProvider provider = null + ) + { + if (destination.Length < _pos) + { + charsWritten = 0; + return false; + } + + _arrayToReturnToPool.AsSpan(0, _pos).CopyTo(destination); + Dispose(); + + charsWritten = _pos; + return true; + } + + public void Dispose() + { + if (_arrayToReturnToPool != null) + { + ArrayPool.Shared.Return(_arrayToReturnToPool); + _arrayToReturnToPool = null; + } + } +} diff --git a/Projects/Server/Buffers/ValueStringBuilder.cs b/Projects/Server/Buffers/ValueStringBuilder.cs index 316c2960e..99a400036 100644 --- a/Projects/Server/Buffers/ValueStringBuilder.cs +++ b/Projects/Server/Buffers/ValueStringBuilder.cs @@ -1,8 +1,8 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. +#nullable enable using System; -using System.Globalization; using System.Runtime.CompilerServices; using System.Runtime.InteropServices; @@ -14,6 +14,10 @@ public ref struct ValueStringBuilder private Span _chars; private int _length; + public ValueStringBuilder() : this(64) + { + } + // If this ctor is used, you cannot pass in stackalloc ROS for append/replace. public ValueStringBuilder(ReadOnlySpan initialString) : this(initialString.Length) { @@ -140,7 +144,7 @@ public ref struct ValueStringBuilder } [MethodImpl(MethodImplOptions.AggressiveInlining)] - public void Insert(int index, string s) + public void Insert(int index, string? s) { if (s == null) { @@ -160,67 +164,39 @@ public ref struct ValueStringBuilder _length += count; } - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public void Append(char c) + public void Append(T value, string? format = null) { - int pos = _length; - if ((uint)pos < (uint)_chars.Length) + if (value is IFormattable) { - _chars[pos] = c; - _length = pos + 1; - } - else - { - GrowAndAppend(c); - } - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public void Append(int value, NumberFormatInfo info = null) - { - if (value >= 0) - { - Append((uint)value); - return; - } - - Append((info ?? NumberFormatInfo.CurrentInfo).NegativeSign); - Append((uint)-value); - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public unsafe void Append(uint value) - { - int bufferLength = value.CountDigits(); - - int pos = _length; - if ((uint)pos + (uint)bufferLength >= _chars.Length) - { - Grow(bufferLength); - } - - if (bufferLength == 1) - { - _chars[pos] = (char)(value + '0'); - _length = pos + 1; - return; - } - - fixed (char* buffer = _chars[pos..]) - { - char* p = buffer + bufferLength; - do + if (value is ISpanFormattable) { - value = Utility.DivRem(value, 10, out uint remainder); - *--p = (char)(remainder + '0'); - } while (value != 0); - } + Span destination = _chars[_length..]; + int charsWritten; + while (!((ISpanFormattable)value).TryFormat(destination, out charsWritten, format, default)) + { + Grow(1); + } - _length = pos + bufferLength; + if ((uint)charsWritten > (uint)destination.Length) + { + throw new FormatException("Invalid string"); + } + + _length += charsWritten; + } + else + { + Append(((IFormattable)value).ToString(format, default)); // constrained call avoiding boxing for value types + } + } + else if (value is not null) + { + Append(value.ToString()); + } } [MethodImpl(MethodImplOptions.AggressiveInlining)] - public void Append(string s) + public void Append(string? s) { if (s == null) { @@ -240,7 +216,7 @@ public ref struct ValueStringBuilder } [MethodImpl(MethodImplOptions.AggressiveInlining)] - public void AppendLine(string s) + public void AppendLine(string? s) { if (s == null) { @@ -261,7 +237,7 @@ public ref struct ValueStringBuilder } [MethodImpl(MethodImplOptions.AggressiveInlining)] - private void AppendSlow(string s) + private void AppendSlow(string? s) { int pos = _length; if (pos > _chars.Length - s.Length) @@ -332,14 +308,6 @@ public ref struct ValueStringBuilder return _chars.Slice(origPos, length); } - [MethodImpl(MethodImplOptions.NoInlining)] - private void GrowAndAppend(char c) - { - Grow(1); - Append(c); - } - -#nullable enable /// /// Resize the internal buffer either by doubling current buffer size or /// by adding to @@ -469,4 +437,165 @@ public ref struct ValueStringBuilder _length -= length; } + + /// Provides a handler used by the language compiler to append interpolated strings into instances. + [InterpolatedStringHandler] + public ref struct AppendInterpolatedStringHandler + { + // Implementation note: + // As this type is only intended to be targeted by the compiler, public APIs eschew argument validation logic + // in a variety of places, e.g. allowing a null input when one isn't expected to produce a NullReferenceException rather + // than an ArgumentNullException. + + /// The associated StringBuilder to which to append. + internal ValueStringBuilder _stringBuilder; + + /// Creates a handler used to append an interpolated string into a . + /// The number of constant characters outside of interpolation expressions in the interpolated string. + /// The number of interpolation expressions in the interpolated string. + /// The associated StringBuilder to which to append. + /// This is intended to be called only by compiler-generated code. Arguments are not validated as they'd otherwise be for members intended to be used directly. + public AppendInterpolatedStringHandler(int literalLength, int formattedCount, ValueStringBuilder stringBuilder) + { + _stringBuilder = stringBuilder; + } + + /// Writes the specified string to the handler. + /// The string to write. + public void AppendLiteral(string value) => _stringBuilder.Append(value); + + // Design note: + // This provides the same set of overloads and semantics as DefaultInterpolatedStringHandler. + + /// Writes the specified value to the handler. + /// The value to write. + public void AppendFormatted(T value) => _stringBuilder.Append(value); + + /// Writes the specified value to the handler. + /// The value to write. + /// The format string. + public void AppendFormatted(T value, string? format) => _stringBuilder.Append(value, format); + + /// Writes the specified value to the handler. + /// The value to write. + /// Minimum number of characters that should be written for this value. If the value is negative, it indicates left-aligned and the required minimum is the absolute value. + public void AppendFormatted(T value, int alignment) => + AppendFormatted(value, alignment, format: null); + + /// Writes the specified value to the handler. + /// The value to write. + /// The format string. + /// Minimum number of characters that should be written for this value. If the value is negative, it indicates left-aligned and the required minimum is the absolute value. + public void AppendFormatted(T value, int alignment, string? format) + { + if (alignment == 0) + { + // This overload is used as a fallback from several disambiguation overloads, so special-case 0. + AppendFormatted(value, format); + } + else if (alignment < 0) + { + // Left aligned: format into the handler, then append any additional padding required. + int start = _stringBuilder.Length; + AppendFormatted(value, format); + int paddingRequired = -alignment - (_stringBuilder.Length - start); + if (paddingRequired > 0) + { + _stringBuilder.Append(' ', paddingRequired); + } + } + else + { + var startingPos = _stringBuilder._length; + AppendFormatted(value, format); + + InsertAlignment(startingPos, alignment); + } + } + + /// Writes the specified character span to the handler. + /// The span to write. + public void AppendFormatted(ReadOnlySpan value) => _stringBuilder.Append(value); + + /// Writes the specified string of chars to the handler. + /// The span to write. + /// Minimum number of characters that should be written for this value. If the value is negative, it indicates left-aligned and the required minimum is the absolute value. + /// The format string. + public void AppendFormatted(ReadOnlySpan value, int alignment = 0, string? format = null) + { + if (alignment == 0) + { + _stringBuilder.Append(value); + } + else + { + bool leftAlign = false; + if (alignment < 0) + { + leftAlign = true; + alignment = -alignment; + } + + int paddingRequired = alignment - value.Length; + if (paddingRequired <= 0) + { + _stringBuilder.Append(value); + } + else if (leftAlign) + { + _stringBuilder.Append(value); + _stringBuilder.Append(' ', paddingRequired); + } + else + { + _stringBuilder.Append(' ', paddingRequired); + _stringBuilder.Append(value); + } + } + } + + /// Writes the specified value to the handler. + /// The value to write. + public void AppendFormatted(string? value) => _stringBuilder.Append(value); + + /// Writes the specified value to the handler. + /// The value to write. + /// Minimum number of characters that should be written for this value. If the value is negative, it indicates left-aligned and the required minimum is the absolute value. + /// The format string. + public void AppendFormatted(string? value, int alignment = 0, string? format = null) => + // Format is meaningless for strings and doesn't make sense for someone to specify. We have the overload + // simply to disambiguate between ROS and object, just in case someone does specify a format, as + // string is implicitly convertible to both. Just delegate to the T-based implementation. + AppendFormatted(value, alignment, format); + + /// Writes the specified value to the handler. + /// The value to write. + /// Minimum number of characters that should be written for this value. If the value is negative, it indicates left-aligned and the required minimum is the absolute value. + /// The format string. + public void AppendFormatted(object? value, int alignment = 0, string? format = null) => + // This overload is expected to be used rarely, only if either a) something strongly typed as object is + // formatted with both an alignment and a format, or b) the compiler is unable to target type to T. It + // exists purely to help make cases from (b) compile. Just delegate to the T-based implementation. + AppendFormatted(value, alignment, format); + + private void InsertAlignment(int startingPos, int alignment) + { + var charsWritten = _stringBuilder._length - startingPos; + + var paddingNeeded = alignment - charsWritten; + if (paddingNeeded > 0) + { + var chars = _stringBuilder._chars; + if (chars.Length - _stringBuilder._length < paddingNeeded) + { + _stringBuilder.Grow(paddingNeeded); + } + + chars.Slice(startingPos, charsWritten).CopyTo(chars[(startingPos + paddingNeeded)..]); + chars.Slice(startingPos, paddingNeeded).Fill(' '); + + _stringBuilder._length += paddingNeeded; + } + } + } } diff --git a/Projects/Server/Buffers/ValueStringBuilderExtensions.cs b/Projects/Server/Buffers/ValueStringBuilderExtensions.cs new file mode 100644 index 000000000..1ec588e80 --- /dev/null +++ b/Projects/Server/Buffers/ValueStringBuilderExtensions.cs @@ -0,0 +1,31 @@ +/************************************************************************* + * ModernUO * + * Copyright 2019-2022 - ModernUO Development Team * + * Email: hi@modernuo.com * + * File: ValueStringBuilderExtensions.cs * + * * + * This program is free software: you can redistribute it and/or modify * + * it under the terms of the GNU General Public License as published by * + * the Free Software Foundation, either version 3 of the License, or * + * (at your option) any later version. * + * * + * You should have received a copy of the GNU General Public License * + * along with this program. If not, see . * + *************************************************************************/ + +using System.Runtime.CompilerServices; + +namespace Server.Buffers; + +public static class ValueStringBuilderExtensions +{ + // Compiler generated + public static void Append( + this ref ValueStringBuilder stringBuilder, + [InterpolatedStringHandlerArgument("stringBuilder")] + ref ValueStringBuilder.AppendInterpolatedStringHandler handler) + { + // Reassign since the string builder stored on the interpolated handler is by-value + stringBuilder = handler._stringBuilder; + } +} diff --git a/Projects/Server/Localization/Localization.cs b/Projects/Server/Localization/Localization.cs new file mode 100644 index 000000000..fe3128c67 --- /dev/null +++ b/Projects/Server/Localization/Localization.cs @@ -0,0 +1,201 @@ +/************************************************************************* + * ModernUO * + * Copyright 2019-2022 - ModernUO Development Team * + * Email: hi@modernuo.com * + * File: Localization.cs * + * * + * This program is free software: you can redistribute it and/or modify * + * it under the terms of the GNU General Public License as published by * + * the Free Software Foundation, either version 3 of the License, or * + * (at your option) any later version. * + * * + * You should have received a copy of the GNU General Public License * + * along with this program. If not, see . * + *************************************************************************/ + +using System; +using System.Collections.Generic; +using System.IO; +using System.Runtime.CompilerServices; +using System.Text; +using Server.Buffers; + +namespace Server; + +public static class Localization +{ + private const bool _loadLocalizationOnStartup = false; + public const string FallbackLanguage = "enu"; + + private static Dictionary _fallbackEntries; + private static Dictionary> _localizations = new(); + + public static void Configure() + { + if (_loadLocalizationOnStartup) + { + foreach (var file in Core.FindDataFileByPattern("cliloc.*")) + { + var fi = new FileInfo(file); + LoadClilocs(fi.Extension.ToLowerInvariant(), file); + } + } + } + + public static Dictionary LoadClilocs(string lang) => + LoadClilocs(lang, Core.FindDataFile($"cliloc.{lang}", false)); + + private static Dictionary LoadClilocs(string lang, string file) + { + Dictionary entries = _localizations[lang] = new Dictionary(); + if (lang == FallbackLanguage) + { + _fallbackEntries = entries; + } + + if (File.Exists(file)) + { + using var fs = new FileStream(file, FileMode.Open, FileAccess.Read, FileShare.Read); + using var bin = new BinaryReader(fs); + + bin.ReadInt32(); + bin.ReadInt16(); + + byte[] buffer = null; + while (bin.BaseStream.Length != bin.BaseStream.Position) + { + var number = bin.ReadInt32(); + var flag = bin.ReadByte(); // Original, Custom, Modified + var length = bin.ReadInt16(); + + if (buffer == null || buffer.Length < length) + { + buffer = GC.AllocateUninitializedArray(length); + } + + var bytesRead = bin.Read(buffer, 0, length); + if (bytesRead != length) + { + throw new Exception($"Could not read enough bytes from {file}"); + } + + var text = Encoding.UTF8.GetString(buffer.AsSpan(0, length)); + entries[number] = new LocalizationEntry(lang, number, text); + } + } + + return entries; + } + + /// + /// Returns the original text for a localization entry. + /// + /// Localization number + /// Language in ISO 639‑2 format + /// Original text for the localizaton entry + public static string GetText(int number, string lang = FallbackLanguage) => + TryGetLocalization(lang, number, out var entry) ? entry.Text : null; + + public static string Format(int number, string lang = FallbackLanguage) => GetText(number, lang); + + /// + /// Creates a formatted string of the localization entry using the . + /// Uses under the hood. + /// Note: This method is not recommended since it uses almost double the memory and 50% more processing. + /// Instead use Format with string interpolation. + /// + /// Localization number + /// An object array containing zero or more objects to format + /// A copy of the localization text where the placeholder arguments have been replaced with string representations of the provided arguments + public static string Format(int number, params object[] args) => + !TryGetLocalization(number, out var entry) ? null : string.Format(entry.StringFormatter, args); + + /// + /// Creates a formatted string of the localization entry using the specified language. + /// Uses under the hood. + /// Note: This method is not recommended since it uses almost double the memory and 50% more processing. + /// Instead use Format with string interpolation. + /// + /// Localization number + /// Language in ISO 639-2 format + /// An object array containing zero or more objects to format + /// A copy of the localization text where the placeholder arguments have been replaced with string representations of the provided arguments + public static string Format(int number, string lang, params object[] args) => + !TryGetLocalization(lang, number, out var entry) ? null : string.Format(entry.StringFormatter, args); + + /// + /// Gets a localization entry using the . + /// + /// Localization number + /// Localization entry retrieved + /// True if the entry exists, otherwise false. + public static bool TryGetLocalization(int number, out LocalizationEntry entry) => + TryGetLocalization(FallbackLanguage, number, out entry); + + /// + /// Gets a localization entry. + /// + /// Language in ISO 639-2 format + /// Localization number + /// Localization entry retrieved + /// True if the entry exists, otherwise false. + public static bool TryGetLocalization(string lang, int number, out LocalizationEntry entry) + { + if (lang != FallbackLanguage) + { + if (!_localizations.TryGetValue(lang, out var entries)) + { + entries = LoadClilocs(lang); + } + + if (entries.TryGetValue(number, out entry)) + { + return true; + } + } + + _fallbackEntries ??= LoadClilocs(FallbackLanguage); + return _fallbackEntries.TryGetValue(number, out entry); + } + + /// + /// Creates a formatted string of the localization entry using the specified language. + /// Uses string interpolation under the hood. This method is preferably relative to the object array method signature. + /// Example: + /// Localization.Format(1073841, "jpn", $"{totalItems}{maxItems}{totalWeight}"); + /// + /// Language in ISO 639-2 format + /// Localization number + /// interpolated string handler used by the compiler as a string builder during compilation + /// A copy of the localization text where the placeholder arguments have been replaced with string representations of the provided interpolation arguments + public static PooledArraySpanFormattable Format( + int number, string lang, + [InterpolatedStringHandlerArgument("number", "lang")] + ref LocalizationInterpolationHandler handler + ) + { + var chars = handler.ToPooledArray(out var length); + handler = default; // Defensive clear + return new PooledArraySpanFormattable(chars, length); + } + + /// + /// Creates a formatted string of the localization entry using the . + /// Uses string interpolation under the hood. This method is preferably relative to the object array method signature. + /// Example: + /// Localization.Format(1073841, $"{totalItems}{maxItems}{totalWeight}"); + /// + /// Localization number + /// interpolated string handler used by the compiler as a string builder during compilation + /// A copy of the localization text where the placeholder arguments have been replaced with string representations of the provided interpolation arguments + public static PooledArraySpanFormattable Format( + int number, + [InterpolatedStringHandlerArgument("number")] + ref LocalizationInterpolationHandler handler + ) + { + var chars = handler.ToPooledArray(out var length); + handler = default; // Defensive clear + return new PooledArraySpanFormattable(chars, length); + } +} diff --git a/Projects/Server/Localization/LocalizationEntry.cs b/Projects/Server/Localization/LocalizationEntry.cs new file mode 100644 index 000000000..61211fbdb --- /dev/null +++ b/Projects/Server/Localization/LocalizationEntry.cs @@ -0,0 +1,104 @@ +/************************************************************************* + * ModernUO * + * Copyright 2019-2022 - ModernUO Development Team * + * Email: hi@modernuo.com * + * File: LocalizationEntry.cs * + * * + * This program is free software: you can redistribute it and/or modify * + * it under the terms of the GNU General Public License as published by * + * the Free Software Foundation, either version 3 of the License, or * + * (at your option) any later version. * + * * + * You should have received a copy of the GNU General Public License * + * along with this program. If not, see . * + *************************************************************************/ + +using System.Runtime.CompilerServices; +using System.Text.RegularExpressions; +using Server.Buffers; +using Server.Collections; + +namespace Server; + +public class LocalizationEntry +{ + private static readonly Regex _textRegex = new( + @"~(\d+)[_\w]+~", + RegexOptions.Compiled | + RegexOptions.IgnoreCase | + RegexOptions.Singleline | + RegexOptions.CultureInvariant + ); + + public string Language { get; } + public int Number { get; } + public string Text { get; } + public string[] TextSlices { get; } + public string StringFormatter { get; } + + public LocalizationEntry(string lang, int number, string text) + { + Language = lang; + Number = number; + Text = text; + + ParseText(text, out var textSlices, out var stringFormatter); + TextSlices = textSlices; + StringFormatter = stringFormatter; + } + + private static void ParseText(string text, out string[] textSlices, out string stringFormatter) + { + bool hasMatch = false; + var prevIndex = 0; + var builder = new ValueStringBuilder(stackalloc char[256]); + using var queue = PooledRefQueue.Create(); + foreach (Match match in _textRegex.Matches(text)) + { + if (prevIndex < match.Index) + { + var substr = text[prevIndex..match.Index]; + builder.Append(substr); + + queue.Enqueue(substr); + } + + queue.Enqueue(null); + hasMatch = true; + builder.Append($"{{{int.Parse(match.Groups[1].Value) - 1}}}"); + prevIndex = match.Index + match.Length; + } + + if (prevIndex < text.Length - 1) + { + var substr = prevIndex == 0 ? text : text[prevIndex..]; + builder.Append(substr); + queue.Enqueue(substr); + } + + textSlices = queue.ToArray(); + stringFormatter = hasMatch ? builder.ToString() : null; + + builder.Dispose(); + } + + public string Format(params object[] args) => string.Format(StringFormatter, args); + + /// + /// Creates a formatted string of the localization entry. + /// Uses string interpolation under the hood. This method is preferably relative to the object array method signature. + /// Example: + /// Format($"{totalItems}{maxItems}{totalWeight}"); + /// + /// interpolated string handler used by the compiler as a string builder during compilation + /// A copy of the localization text where the placeholder arguments have been replaced with string representations of the provided interpolation arguments + public PooledArraySpanFormattable Format( + [InterpolatedStringHandlerArgument("")] + ref LocalizationInterpolationHandler handler + ) + { + var chars = handler.ToPooledArray(out var length); + handler = default; // Defensive clear + return new PooledArraySpanFormattable(chars, length); + } +} diff --git a/Projects/Server/Localization/LocalizationInterpolationHandler.cs b/Projects/Server/Localization/LocalizationInterpolationHandler.cs new file mode 100644 index 000000000..62908ea3c --- /dev/null +++ b/Projects/Server/Localization/LocalizationInterpolationHandler.cs @@ -0,0 +1,429 @@ +/************************************************************************* + * ModernUO * + * Copyright 2019-2022 - ModernUO Development Team * + * Email: hi@modernuo.com * + * File: LocalizationInterpolationHandler.cs * + * * + * This program is free software: you can redistribute it and/or modify * + * it under the terms of the GNU General Public License as published by * + * the Free Software Foundation, either version 3 of the License, or * + * (at your option) any later version. * + * * + * You should have received a copy of the GNU General Public License * + * along with this program. If not, see . * + *************************************************************************/ + +#nullable enable +using System; +using System.Buffers; +using System.Runtime.CompilerServices; + +namespace Server; + +[InterpolatedStringHandler] +public ref struct LocalizationInterpolationHandler +{ + private static string[] _empty = Array.Empty(); + + private char[]? _arrayToReturnToPool; + private Span _chars; + private int _pos; + + private int _index; + private string?[] _slices; + private string? _current; + + public LocalizationInterpolationHandler(int literalLength, int formattedCount, LocalizationEntry entry, out bool isValid) + { + _slices = entry.TextSlices; + _chars = _arrayToReturnToPool = ArrayPool.Shared.Rent(256); + isValid = true; + + _pos = 0; + _index = 0; + _current = null; + } + + public LocalizationInterpolationHandler(int literalLength, int formattedCount, int number, out bool isValid) + : this(literalLength, formattedCount, number, Localization.FallbackLanguage, out isValid) + { + } + + public LocalizationInterpolationHandler(int literalLength, int formattedCount, int number, string lang, out bool isValid) + { + if (Localization.TryGetLocalization(lang, number, out var entry)) + { + _slices = entry.TextSlices; + _chars = _arrayToReturnToPool = ArrayPool.Shared.Rent(256); + isValid = true; + } + else + { + _slices = _empty; + _chars = _arrayToReturnToPool = default; + isValid = false; + } + + _pos = 0; + _index = 0; + _current = null; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private bool MoveNext() + { + if ((uint)_index >= (uint)_slices.Length) + { + return false; + } + + _current = _slices[_index++]; + return true; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private bool ReadyToAppend() + { + if (!MoveNext()) + { + return false; + } + + if (_current == null) + { + return true; + } + + AppendStringDirect(_current); + return MoveNext(); + } + + public void AppendLiteral(string value) + { + } + + private void AppendStringDirect(string value) + { + if (value.TryCopyTo(_chars[_pos..])) + { + _pos += value.Length; + } + else + { + GrowThenCopyString(value); + } + } + + public void AppendFormatted(T value) + { + if (!ReadyToAppend()) + { + return; + } + + string? s; + if (value is IFormattable) + { + // If the value can format itself directly into our buffer, do so. + if (value is ISpanFormattable) + { + int charsWritten; + while (!((ISpanFormattable)value).TryFormat(_chars[_pos..], out charsWritten, default, default)) // constrained call avoiding boxing for value types + { + Grow(); + } + + _pos += charsWritten; + return; + } + + s = ((IFormattable)value).ToString(format: null, default); // constrained call avoiding boxing for value types + } + else + { + s = value?.ToString(); + } + + if (s is not null) + { + AppendStringDirect(s); + } + } + + public void AppendFormatted(T value, string? format) + { + if (!ReadyToAppend()) + { + return; + } + + string? s; + if (value is IFormattable) + { + // If the value can format itself directly into our buffer, do so. + if (value is ISpanFormattable) + { + int charsWritten; + while (!((ISpanFormattable)value).TryFormat(_chars[_pos..], out charsWritten, format, default)) // constrained call avoiding boxing for value types + { + Grow(); + } + + _pos += charsWritten; + return; + } + + s = ((IFormattable)value).ToString(format, default); // constrained call avoiding boxing for value types + } + else + { + s = value?.ToString(); + } + + if (s is not null) + { + AppendStringDirect(s); + } + } + + public void AppendFormatted(T value, int alignment) + { + if (!ReadyToAppend()) + { + return; + } + + var startingPos = _pos; + AppendFormatted(value); + if (alignment != 0) + { + AppendOrInsertAlignmentIfNeeded(startingPos, alignment); + } + } + + public void AppendFormatted(T value, int alignment, string? format) + { + if (!ReadyToAppend()) + { + return; + } + + var startingPos = _pos; + AppendFormatted(value, format); + if (alignment != 0) + { + AppendOrInsertAlignmentIfNeeded(startingPos, alignment); + } + } + + public void AppendFormatted(ReadOnlySpan value) + { + if (!ReadyToAppend()) + { + return; + } + + if (value.TryCopyTo(_chars[_pos..])) + { + _pos += value.Length; + } + else + { + GrowThenCopySpan(value); + } + } + + public void AppendFormatted(ReadOnlySpan value, int alignment, string? format = null) + { + if (!ReadyToAppend()) + { + return; + } + + var leftAlign = false; + if (alignment < 0) + { + leftAlign = true; + alignment = -alignment; + } + + var paddingRequired = alignment - value.Length; + if (paddingRequired <= 0) + { + // The value is as large or larger than the required amount of padding, + // so just write the value. + AppendFormatted(value); + return; + } + + // Write the value along with the appropriate padding. + EnsureCapacityForAdditionalChars(value.Length + paddingRequired); + if (leftAlign) + { + value.CopyTo(_chars[_pos..]); + _pos += value.Length; + _chars.Slice(_pos, paddingRequired).Fill(' '); + _pos += paddingRequired; + } + else + { + _chars.Slice(_pos, paddingRequired).Fill(' '); + _pos += paddingRequired; + value.CopyTo(_chars[_pos..]); + _pos += value.Length; + } + } + + public void AppendFormatted(object? value, int alignment = 0, string? format = null) => + AppendFormatted(value, alignment, format); + + public void AppendFormatted(string? value) + { + if (ReadyToAppend()) + { + if (value?.TryCopyTo(_chars[_pos..]) == true) + { + _pos += value.Length; + } + else + { + AppendFormattedSlow(value); + } + } + } + + public void AppendFormatted(string? value, int alignment, string? format = null) => + AppendFormatted(value, alignment, format); + + private void AppendOrInsertAlignmentIfNeeded(int startingPos, int alignment) + { + var charsWritten = _pos - startingPos; + + var leftAlign = false; + if (alignment < 0) + { + leftAlign = true; + alignment = -alignment; + } + + var paddingNeeded = alignment - charsWritten; + if (paddingNeeded > 0) + { + EnsureCapacityForAdditionalChars(paddingNeeded); + + if (leftAlign) + { + _chars.Slice(_pos, paddingNeeded).Fill(' '); + } + else + { + _chars.Slice(startingPos, charsWritten).CopyTo(_chars[(startingPos + paddingNeeded)..]); + _chars.Slice(startingPos, paddingNeeded).Fill(' '); + } + + _pos += paddingNeeded; + } + } + + [MethodImpl(MethodImplOptions.NoInlining)] + private void AppendFormattedSlow(string? value) + { + if (value is not null) + { + EnsureCapacityForAdditionalChars(value.Length); + value.CopyTo(_chars[_pos..]); + _pos += value.Length; + } + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private void EnsureCapacityForAdditionalChars(int additionalChars) + { + if (_chars.Length - _pos < additionalChars) + { + Grow(additionalChars); + } + } + + [MethodImpl(MethodImplOptions.NoInlining)] + private void GrowThenCopyString(string value) + { + Grow(value.Length); + value.CopyTo(_chars[_pos..]); + _pos += value.Length; + } + + [MethodImpl(MethodImplOptions.NoInlining)] + private void GrowThenCopySpan(ReadOnlySpan value) + { + Grow(value.Length); + value.CopyTo(_chars[_pos..]); + _pos += value.Length; + } + + [MethodImpl(MethodImplOptions.NoInlining)] // keep consumers as streamlined as possible + private void Grow(int additionalChars) + { + GrowCore((uint)_pos + (uint)additionalChars); + } + + [MethodImpl(MethodImplOptions.NoInlining)] // keep consumers as streamlined as possible + private void Grow() + { + GrowCore((uint)_chars.Length + 1); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private void GrowCore(uint requiredMinCapacity) + { + var newCapacity = Math.Max(requiredMinCapacity, Math.Min((uint)_chars.Length * 2, 1073741823)); + var arraySize = (int)Math.Clamp(newCapacity, 256, int.MaxValue); + + var newArray = ArrayPool.Shared.Rent(arraySize); + _chars[.._pos].CopyTo(newArray); + + var toReturn = _arrayToReturnToPool; + _chars = _arrayToReturnToPool = newArray; + + if (toReturn is not null) + { + ArrayPool.Shared.Return(toReturn); + } + } + + internal ReadOnlySpan Text => _chars[.._pos]; + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + internal void Clear() + { + var toReturn = _arrayToReturnToPool; + this = default; // defensive clear + if (toReturn is not null) + { + ArrayPool.Shared.Return(toReturn); + } + } + + public string ToStringAndClear() + { + if (MoveNext() && _current != null) + { + AppendStringDirect(_current); + } + + var result = new string(Text); + Clear(); + return result; + } + + public char[] ToPooledArray(out int length) + { + if (MoveNext() && _current != null) + { + AppendStringDirect(_current); + } + + length = _pos; + return _arrayToReturnToPool; + } +} diff --git a/Projects/Server/Main.cs b/Projects/Server/Main.cs index f9ab272b7..bd1316858 100644 --- a/Projects/Server/Main.cs +++ b/Projects/Server/Main.cs @@ -274,6 +274,21 @@ namespace Server return fullPath; } + public static IEnumerable FindDataFileByPattern(string pattern) + { + var options = new EnumerationOptions { MatchCasing = MatchCasing.CaseInsensitive }; + foreach (var p in ServerConfiguration.DataDirectories) + { + if (Directory.Exists(p)) + { + foreach (var file in Directory.EnumerateFiles(p, pattern, options)) + { + yield return file; + } + } + } + } + private static void CurrentDomain_UnhandledException(object sender, UnhandledExceptionEventArgs e) { Console.WriteLine(e.IsTerminating ? "Error:" : "Warning:"); diff --git a/Projects/Server/Utilities/Utility.cs b/Projects/Server/Utilities/Utility.cs index ba4877fc9..34e90c4d1 100644 --- a/Projects/Server/Utilities/Utility.cs +++ b/Projects/Server/Utilities/Utility.cs @@ -1284,96 +1284,6 @@ namespace Server return (value + mask) ^ mask; } - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static long Abs(this long value) - { - long mask = value >> 63; - return (value + mask) ^ mask; - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static int CountDigits(this uint value) - { - int digits = 1; - if (value >= 100000) - { - value /= 100000; - digits += 5; - } - - if (value < 10) - { - // no-op - } - else if (value < 100) - { - digits++; - } - else if (value < 1000) - { - digits += 2; - } - else if (value < 10000) - { - digits += 3; - } - else - { - digits += 4; - } - - return digits; - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static int CountDigits(this int value) - { - int absValue = Abs(value); - - int digits = 1; - if (absValue >= 100000) - { - absValue /= 100000; - digits += 5; - } - - if (absValue < 10) - { - // no-op - } - else if (absValue < 100) - { - digits++; - } - else if (absValue < 1000) - { - digits += 2; - } - else if (absValue < 10000) - { - digits += 3; - } - else - { - digits += 4; - } - - if (value < 0) - { - digits += 1; // negative - } - - return digits; - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static uint DivRem(uint a, uint b, out uint result) - { - uint div = a / b; - result = a - div * b; - return div; - } - [MethodImpl(MethodImplOptions.AggressiveInlining)] public static string GetTimeStamp() => Core.Now.ToTimeStamp();