diff --git a/Projects/Server.Tests/Tests/Buffers/STArrayPoolTests.cs b/Projects/Server.Tests/Tests/Buffers/STArrayPoolTests.cs index 8b6ea0551..0b7ee8937 100644 --- a/Projects/Server.Tests/Tests/Buffers/STArrayPoolTests.cs +++ b/Projects/Server.Tests/Tests/Buffers/STArrayPoolTests.cs @@ -4,6 +4,7 @@ using Xunit; namespace Server.Tests.Tests.Buffers; +[Collection("Sequential Tests")] public class STArrayPoolTests { [Theory] @@ -48,7 +49,7 @@ public class STArrayPoolTests weakReferences1[i] = new WeakReference(arrays1[i]); arrays2[i] = STArrayPool.Shared.Rent(64); - weakReferences2[i] = new WeakReference(arrays1[i]); + weakReferences2[i] = new WeakReference(arrays2[i]); } for (var i = 0; i < arrays1.Length; i++) diff --git a/Projects/Server.Tests/Tests/Localization/LocalizationEntryTests.cs b/Projects/Server.Tests/Tests/Localization/LocalizationEntryTests.cs new file mode 100644 index 000000000..9b64535cd --- /dev/null +++ b/Projects/Server.Tests/Tests/Localization/LocalizationEntryTests.cs @@ -0,0 +1,19 @@ +using Xunit; + +namespace Server.Tests; + +public class LocalizationEntryTests +{ + [Fact] + public void TestClilocAsParameter() + { + Localization.Add("enu", 500002, "This tests ~1_NUMBER~ as parameters."); + Localization.Add("enu", 500003, "clilocs"); + + string numericFormatter = Localization.Format(500002, "enu", $"{500003:#}"); + string stringParam = Localization.Format(500002, "enu", $"{"#500003"}"); + + Assert.Equal("This tests clilocs as parameters", numericFormatter); + Assert.Equal("This tests clilocs as parameters", stringParam); + } +} diff --git a/Projects/Server/Buffers/PooledArraySpanFormattable.cs b/Projects/Server/Buffers/PooledArraySpanFormattable.cs index a210a7ff9..7290f250a 100644 --- a/Projects/Server/Buffers/PooledArraySpanFormattable.cs +++ b/Projects/Server/Buffers/PooledArraySpanFormattable.cs @@ -22,11 +22,13 @@ public struct PooledArraySpanFormattable : ISpanFormattable, IDisposable { private char[] _arrayToReturnToPool; private int _pos; + private string _value; public PooledArraySpanFormattable(char[] arrayToReturnToPool, int length) { _arrayToReturnToPool = arrayToReturnToPool; _pos = length; + _value = null; } public ReadOnlySpan Chars => _arrayToReturnToPool.AsSpan(.._pos); @@ -35,10 +37,12 @@ public struct PooledArraySpanFormattable : ISpanFormattable, IDisposable public string ToString(string? format = null, IFormatProvider formatProvider = null) { - var result = new string(_arrayToReturnToPool.AsSpan(0, _pos)); - Dispose(); + _value ??= new string(_arrayToReturnToPool.AsSpan(0, _pos)); - return result; + STArrayPool.Shared.Return(_arrayToReturnToPool); + _arrayToReturnToPool = null; + + return _value; } public bool TryFormat( @@ -53,18 +57,14 @@ public struct PooledArraySpanFormattable : ISpanFormattable, IDisposable } _arrayToReturnToPool.AsSpan(0, _pos).CopyTo(destination); - Dispose(); - charsWritten = _pos; return true; } public void Dispose() { - if (_arrayToReturnToPool != null) - { - STArrayPool.Shared.Return(_arrayToReturnToPool); - _arrayToReturnToPool = null; - } + STArrayPool.Shared.Return(_arrayToReturnToPool); + _arrayToReturnToPool = null; + this = default; // Defensive clear } } diff --git a/Projects/Server/Buffers/STArrayPool.cs b/Projects/Server/Buffers/STArrayPool.cs index 65e8a9d6c..a9ff65492 100644 --- a/Projects/Server/Buffers/STArrayPool.cs +++ b/Projects/Server/Buffers/STArrayPool.cs @@ -127,6 +127,11 @@ public class STArrayPool : ArrayPool buckets[i]?.Trim(ticks, pressure, GetMaxSizeForBucket(i)); } + if (_cacheBuckets == null) + { + return true; + } + // Under high pressure, release all cached buckets if (pressure == MemoryPressure.High) { diff --git a/Projects/Server/Localization/Localization.cs b/Projects/Server/Localization/Localization.cs index e178bbd95..ca92c06c2 100644 --- a/Projects/Server/Localization/Localization.cs +++ b/Projects/Server/Localization/Localization.cs @@ -42,6 +42,37 @@ public static class Localization } } + public static void Add(string lang, int number, string text) + { + var entry = new LocalizationEntry(lang, number, text); + if (!_localizations.TryGetValue(lang, out var entries)) + { + entries = new Dictionary(); + _localizations[lang] = entries; + if (lang == FallbackLanguage) + { + _fallbackEntries ??= entries; + } + } + + entries.Add(number, entry); + } + + public static bool Remove(string lang, int number) + { + if (!_localizations.TryGetValue(lang, out var entries) || !entries.Remove(number)) + { + return false; + } + + if (entries.Count == 0) + { + _localizations.Remove(lang); + } + + return true; + } + public static Dictionary LoadClilocs(string lang) => LoadClilocs(lang, Core.FindDataFile($"cliloc.{lang}", false)); @@ -96,19 +127,6 @@ public static class Localization public static string GetText(int number, string lang = FallbackLanguage) => TryGetLocalization(lang, number, out var entry) ? entry.Text : null; - /// - /// 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 = FallbackLanguage, params object[] args) => - TryGetLocalization(lang, number, out var entry) ? entry.Format(args) : null; - /// /// Gets a localization entry using the . /// diff --git a/Projects/Server/Localization/LocalizationEntry.cs b/Projects/Server/Localization/LocalizationEntry.cs index f91bcbce0..a2efd2236 100644 --- a/Projects/Server/Localization/LocalizationEntry.cs +++ b/Projects/Server/Localization/LocalizationEntry.cs @@ -84,24 +84,6 @@ public class LocalizationEntry builder.Dispose(); } - public string Format(params object[] args) - { - if (args == null || args.Length == 0 || StringFormatter == null) - { - return Text; - } - - for (var i = 0; i < args.Length; i++) - { - if (args[i] is string s && s[0] == '#' && int.TryParse(s.AsSpan(1), out var number)) - { - args[i] = Localization.GetText(number, Language); - } - } - - return 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. @@ -256,13 +238,69 @@ public class LocalizationEntry } } - public void AppendFormatted(T value, string? format) + // Each numeric needs its own override + public void AppendFormatted(int value, string? format) { if (!ReadyToAppend()) { return; } + if (!TryAppendCliloc(value, format)) + { + AppendFormattedDirect(value, format); + } + } + + public void AppendFormatted(uint value, string? format) + { + if (!ReadyToAppend()) + { + return; + } + + if (!TryAppendCliloc((int)value, format)) + { + AppendFormattedDirect(value, format); + } + } + + public void AppendFormatted(long value, string? format) + { + if (!ReadyToAppend()) + { + return; + } + + if (!TryAppendCliloc((int)value, format)) + { + AppendFormattedDirect(value, format); + } + } + + public void AppendFormatted(ulong value, string? format) + { + if (!ReadyToAppend()) + { + return; + } + + if (!TryAppendCliloc((int)value, format)) + { + AppendFormattedDirect(value, format); + } + } + + public void AppendFormatted(T value, string? format) + { + if (ReadyToAppend()) + { + AppendFormattedDirect(value, format); + } + } + + private void AppendFormattedDirect(T value, string? format) + { string? s; if (value is IFormattable) { @@ -307,6 +345,95 @@ public class LocalizationEntry } } + // Each numeric needs its own override + public void AppendFormatted(int value, int alignment, string? format) + { + if (!ReadyToAppend()) + { + return; + } + + var startingPos = _pos; + if (TryAppendCliloc(value, format)) + { + AppendFormatted(value, format); + if (alignment != 0) + { + AppendOrInsertAlignmentIfNeeded(startingPos, alignment); + } + } + else + { + AppendFormattedDirect(value, alignment, format); + } + } + + public void AppendFormatted(uint value, int alignment, string? format) + { + if (!ReadyToAppend()) + { + return; + } + + var startingPos = _pos; + if (TryAppendCliloc((int)value, format)) + { + AppendFormatted(value, format); + if (alignment != 0) + { + AppendOrInsertAlignmentIfNeeded(startingPos, alignment); + } + } + else + { + AppendFormattedDirect(value, alignment, format); + } + } + + public void AppendFormatted(long value, int alignment, string? format) + { + if (!ReadyToAppend()) + { + return; + } + + var startingPos = _pos; + if (TryAppendCliloc((int)value, format)) + { + AppendFormatted(value, format); + if (alignment != 0) + { + AppendOrInsertAlignmentIfNeeded(startingPos, alignment); + } + } + else + { + AppendFormattedDirect(value, alignment, format); + } + } + + public void AppendFormatted(ulong value, int alignment, string? format) + { + if (!ReadyToAppend()) + { + return; + } + + var startingPos = _pos; + if (TryAppendCliloc((int)value, format)) + { + AppendFormatted(value, format); + if (alignment != 0) + { + AppendOrInsertAlignmentIfNeeded(startingPos, alignment); + } + } + else + { + AppendFormattedDirect(value, alignment, format); + } + } + public void AppendFormatted(T value, int alignment, string? format) { if (!ReadyToAppend()) @@ -314,6 +441,11 @@ public class LocalizationEntry return; } + AppendFormattedDirect(value, alignment, format); + } + + private void AppendFormattedDirect(T value, int alignment, string? format) + { var startingPos = _pos; AppendFormatted(value, format); if (alignment != 0) @@ -324,7 +456,7 @@ public class LocalizationEntry public void AppendFormatted(ReadOnlySpan value) { - if (!ReadyToAppend() || TryAppendClilocNumber(value)) + if (!ReadyToAppend() || TryAppendClilocByNumericString(value)) { return; } @@ -380,12 +512,33 @@ public class LocalizationEntry } } - public void AppendFormatted(object? value, int alignment = 0, string? format = null) => - AppendFormatted(value, alignment, format); + public void AppendFormatted(object? value, int alignment = 0, string? format = null) + { + if (value is int i) + { + AppendFormatted(i, alignment, format); + } + else if (value is uint ui) + { + AppendFormatted(ui, alignment, format); + } + else if (value is long l) + { + AppendFormatted(l, alignment, format); + } + else if (value is ulong ul) + { + AppendFormatted(ul, alignment, format); + } + else + { + AppendFormatted(value, alignment, format); + } + } public void AppendFormatted(string? value) { - if (!ReadyToAppend() || TryAppendClilocNumber(value)) + if (!ReadyToAppend() || TryAppendClilocByNumericString(value)) { return; } @@ -403,13 +556,9 @@ public class LocalizationEntry public void AppendFormatted(string? value, int alignment, string? format = null) => AppendFormatted(value, alignment, format); - private bool TryAppendClilocNumber(ReadOnlySpan value) + public bool TryAppendCliloc(int number, string? format) { - if ( - value[0] != '#' || - !int.TryParse(value[1..], out var number) || - !Localization.TryGetLocalization(_lang, number, out var entry) - ) + if (format != "#" || !Localization.TryGetLocalization(_lang, number, out var entry)) { return false; } @@ -428,6 +577,9 @@ public class LocalizationEntry return true; } + private bool TryAppendClilocByNumericString(ReadOnlySpan value) => + value[0] == '#' && long.TryParse(value[1..], out var number) && TryAppendCliloc((int)number, "#"); + private void AppendOrInsertAlignmentIfNeeded(int startingPos, int alignment) { var charsWritten = _pos - startingPos;