From 04042516388791737e80698a84f44ce0c280cba2 Mon Sep 17 00:00:00 2001 From: Kamron Batman <3953314+kamronbatman@users.noreply.github.com> Date: Thu, 22 Jan 2026 15:51:00 -0800 Subject: [PATCH] feat: Adds Latin1 text support (#2317) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Summary - Adds proper Latin1 encoding support, replacing CP1252 usage throughout the codebase - Adds specialized, optimized string decoding methods with safe string filtering for each encoding type - Filters invalid Unicode characters (C0/C1 control codes, non-characters) by removal rather than replacement since the UO client renders nothing for these characters - Fixes UTF-16 null terminator position handling to correctly advance by 2 bytes ## Changes TextEncoding.cs - Added SearchValues-based invalid byte/char detection for efficient filtering - Added encoding-specific GetString methods: GetStringAscii, GetStringLatin1, GetStringUtf8, GetStringBigUni, GetStringLittleUni - Each method supports a safeString parameter for filtering invalid characters - Little-endian UTF-16 uses direct memory cast for zero-copy decoding on LE systems - Invalid characters are removed (not replaced with U+FFFD) since the client renders nothing for them SpanReader.cs - Added ReadLatin1() and ReadLatin1Safe() methods - Rewrote encoding-specific read methods to use optimized TextEncoding.GetString* methods - Fixed UTF-16 null terminator handling: position now correctly advances by byteLength (2) instead of 1 SpanWriter.cs - Added WriteLatin1 and WriteLatin1Null methods ## Packet Updates - Updated all packet code to use Latin1 encoding instead of CP1252 - Affected: account packets, equipment packets, menu packets, message packets, mobile packets, player packets, secure trade packets, vendor packets, gump packets, book packets, mahjong packets ## Filtering Behavior Invalid characters filtered in safe mode: ``` ┌───────────────┬────────────────────────┐ │ Range │ Description │ ├───────────────┼────────────────────────┤ │ 0x00-0x1F │ C0 control codes │ ├───────────────┼────────────────────────┤ │ 0x7F │ DEL │ ├───────────────┼────────────────────────┤ │ 0x80-0x9F │ C1 control codes │ ├───────────────┼────────────────────────┤ │ 0xFFFE-0xFFFF │ Unicode non-characters │ └───────────────┴────────────────────────┘ ``` Note: Surrogate pairs (0xD800-0xDFFF) are not filtered because proper validation requires context checking for paired vs unpaired surrogates. The UO client renders nothing for these anyway. ## Test Plan - All 631 Server.Tests pass - Verified client rendering behavior using TestUnicodeGump command (pages 1-5) - Confirmed U+FFFD, unpaired surrogates, and non-characters all render as blank in client - Verified Latin1 characters (0xA0-0xFF) display correctly - Verified C1 control codes (0x80-0x9F) are filtered and don't display --- .../Tests/Buffers/SpanReaderTests.cs | 18 +- Projects/Server/Buffers/SpanReader.cs | 224 ++++++++- Projects/Server/Buffers/SpanWriter.cs | 35 +- .../Network/Packets/OutgoingAccountPackets.cs | 12 +- .../Packets/OutgoingEquipmentPackets.cs | 2 +- .../Network/Packets/OutgoingMenuPackets.cs | 8 +- .../Network/Packets/OutgoingMessagePackets.cs | 12 +- .../Network/Packets/OutgoingMobilePackets.cs | 4 +- .../Network/Packets/OutgoingPlayerPackets.cs | 6 +- .../Packets/OutgoingSecureTradePackets.cs | 2 +- .../Packets/OutgoingVendorBuyPackets.cs | 2 +- .../Packets/OutgoingVendorSellPackets.cs | 2 +- Projects/Server/Server.csproj | 2 +- Projects/Server/Text/TextEncoding.cs | 449 +++++++++++++++++- .../Tests/Accounting/AccountHandlerTests.cs | 1 - .../UOContent/Accounting/AccountHandler.cs | 7 +- .../UOContent/Gumps/Base/GumpLayoutBuilder.cs | 2 +- .../Gumps/Base/Legacy/GumpHtmlLocalized.cs | 2 +- .../Gumps/Base/Legacy/GumpTooltip.cs | 9 +- .../Gumps/Base/OutgoingGumpPackets.cs | 4 +- Projects/UOContent/Gumps/Base/RelayInfo.cs | 6 +- Projects/UOContent/Items/Books/BookPackets.cs | 4 +- .../Items/Games/Mahjong/MahjongPackets.cs | 2 +- .../Network/Packets/IncomingAccountPackets.cs | 10 +- .../Network/Packets/IncomingMessagePackets.cs | 2 +- .../Network/Packets/IncomingMobilePackets.cs | 2 +- .../Network/Packets/IncomingPlayerPackets.cs | 2 +- 27 files changed, 735 insertions(+), 96 deletions(-) diff --git a/Projects/Server.Tests/Tests/Buffers/SpanReaderTests.cs b/Projects/Server.Tests/Tests/Buffers/SpanReaderTests.cs index 5879cdf31..8d6e24e6d 100644 --- a/Projects/Server.Tests/Tests/Buffers/SpanReaderTests.cs +++ b/Projects/Server.Tests/Tests/Buffers/SpanReaderTests.cs @@ -291,7 +291,8 @@ public class SpanReaderTests var result = reader.ReadLittleUni(); Assert.Equal("Hi", result); - Assert.Equal(5, reader.Position); + // Position 6: 4 bytes for "Hi" + 2 bytes for UTF-16 null terminator + Assert.Equal(6, reader.Position); } [Fact] @@ -309,12 +310,14 @@ public class SpanReaderTests [Fact] public void TestReadLittleUniSafe() { - ReadOnlySpan buffer = [(byte)'H', 0, 0xFF, 0xD8, (byte)'i', 0]; + // Test with C1 control code (0x85 = NEL) which should be filtered + ReadOnlySpan buffer = [(byte)'H', 0, 0x85, 0x00, (byte)'i', 0]; var reader = new SpanReader(buffer); var result = reader.ReadLittleUniSafe(); - Assert.Equal("H\uFFFDi", result); + // C1 control (0x0085) removed - client renders nothing for invalid chars + Assert.Equal("Hi", result); Assert.Equal(6, reader.Position); } @@ -339,7 +342,8 @@ public class SpanReaderTests var result = reader.ReadBigUni(); Assert.Equal("Hi", result); - Assert.Equal(5, reader.Position); + // Position 6: 4 bytes for "Hi" + 2 bytes for UTF-16 null terminator + Assert.Equal(6, reader.Position); } [Fact] @@ -357,12 +361,14 @@ public class SpanReaderTests [Fact] public void TestReadBigUniSafe() { - ReadOnlySpan buffer = [0, (byte)'H', 0xD8, 0xFF, 0, (byte)'i']; + // Test with C1 control code (0x0085 = NEL) which should be filtered + ReadOnlySpan buffer = [0, (byte)'H', 0x00, 0x85, 0, (byte)'i']; var reader = new SpanReader(buffer); var result = reader.ReadBigUniSafe(); - Assert.Equal("H\uFFFDi", result); + // C1 control (0x0085) removed - client renders nothing for invalid chars + Assert.Equal("Hi", result); Assert.Equal(6, reader.Position); } diff --git a/Projects/Server/Buffers/SpanReader.cs b/Projects/Server/Buffers/SpanReader.cs index dd35e8702..8a99fbaaa 100644 --- a/Projects/Server/Buffers/SpanReader.cs +++ b/Projects/Server/Buffers/SpanReader.cs @@ -1,6 +1,6 @@ /************************************************************************* * ModernUO * - * Copyright 2019-2025 - ModernUO Development Team * + * Copyright 2019-2026 - ModernUO Development Team * * Email: hi@modernuo.com * * File: SpanReader.cs * * * @@ -207,49 +207,241 @@ public ref struct SpanReader } [MethodImpl(MethodImplOptions.AggressiveInlining)] - public string ReadLittleUniSafe(int fixedLength) => ReadString(TextEncoding.UnicodeLE, true, fixedLength); + public string ReadLittleUniSafe(int fixedLength) => ReadStringLittleUni(true, fixedLength); [MethodImpl(MethodImplOptions.AggressiveInlining)] - public string ReadLittleUniSafe() => ReadString(TextEncoding.UnicodeLE, true); + public string ReadLittleUniSafe() => ReadStringLittleUni(true); [MethodImpl(MethodImplOptions.AggressiveInlining)] - public string ReadLittleUni(int fixedLength) => ReadString(TextEncoding.UnicodeLE, false, fixedLength); + public string ReadLittleUni(int fixedLength) => ReadStringLittleUni(false, fixedLength); [MethodImpl(MethodImplOptions.AggressiveInlining)] - public string ReadLittleUni() => ReadString(TextEncoding.UnicodeLE); + public string ReadLittleUni() => ReadStringLittleUni(false); [MethodImpl(MethodImplOptions.AggressiveInlining)] - public string ReadBigUniSafe(int fixedLength) => ReadString(TextEncoding.Unicode, true, fixedLength); + public string ReadBigUniSafe(int fixedLength) => ReadStringBigUni(true, fixedLength); [MethodImpl(MethodImplOptions.AggressiveInlining)] - public string ReadBigUniSafe() => ReadString(TextEncoding.Unicode, true); + public string ReadBigUniSafe() => ReadStringBigUni(true); [MethodImpl(MethodImplOptions.AggressiveInlining)] - public string ReadBigUni(int fixedLength) => ReadString(TextEncoding.Unicode, false, fixedLength); + public string ReadBigUni(int fixedLength) => ReadStringBigUni(false, fixedLength); [MethodImpl(MethodImplOptions.AggressiveInlining)] - public string ReadBigUni() => ReadString(TextEncoding.Unicode); + public string ReadBigUni() => ReadStringBigUni(false); [MethodImpl(MethodImplOptions.AggressiveInlining)] - public string ReadUTF8Safe(int fixedLength) => ReadString(TextEncoding.UTF8, true, fixedLength); + public string ReadUTF8Safe(int fixedLength) => ReadStringUtf8(true, fixedLength); [MethodImpl(MethodImplOptions.AggressiveInlining)] - public string ReadUTF8Safe() => ReadString(TextEncoding.UTF8, true); + public string ReadUTF8Safe() => ReadStringUtf8(true); [MethodImpl(MethodImplOptions.AggressiveInlining)] - public string ReadUTF8() => ReadString(TextEncoding.UTF8); + public string ReadUTF8(int fixedLength) => ReadStringUtf8(false, fixedLength); [MethodImpl(MethodImplOptions.AggressiveInlining)] - public string ReadAsciiSafe(int fixedLength) => ReadString(Encoding.ASCII, true, fixedLength); + public string ReadUTF8() => ReadStringUtf8(false); [MethodImpl(MethodImplOptions.AggressiveInlining)] - public string ReadAsciiSafe() => ReadString(Encoding.ASCII, true); + public string ReadAsciiSafe(int fixedLength) => ReadStringAscii(true, fixedLength); [MethodImpl(MethodImplOptions.AggressiveInlining)] - public string ReadAscii(int fixedLength) => ReadString(Encoding.ASCII, false, fixedLength); + public string ReadAsciiSafe() => ReadStringAscii(true); [MethodImpl(MethodImplOptions.AggressiveInlining)] - public string ReadAscii() => ReadString(Encoding.ASCII); + public string ReadAscii(int fixedLength) => ReadStringAscii(false, fixedLength); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public string ReadAscii() => ReadStringAscii(false); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public string ReadLatin1Safe(int fixedLength) => ReadStringLatin1(true, fixedLength); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public string ReadLatin1Safe() => ReadStringLatin1(true); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public string ReadLatin1(int fixedLength) => ReadStringLatin1(false, fixedLength); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public string ReadLatin1() => ReadStringLatin1(false); + + /// + /// Reads a big-endian UTF-16 string, filtering control codes and non-characters in safe mode. + /// + private string ReadStringBigUni(bool safeString, int fixedLength = -1) + { + if (fixedLength == 0) + { + return ""; + } + + const int byteLength = 2; + var isFixedLength = fixedLength > -1; + + var remaining = Remaining; + int size; + if (isFixedLength) + { + size = fixedLength * byteLength; + if (size > remaining) + { + throw new EndOfStreamException("Cannot read past the end of the buffer."); + } + } + else + { + // Ensure even number of bytes + size = remaining - (remaining & 1); + } + + var span = _buffer.Slice(Position, size); + var index = span.IndexOfTerminator(byteLength); + + if (index > -1) + { + span = _buffer.Slice(Position, index); + } + + Position += isFixedLength || index < 0 ? size : index + byteLength; + + return TextEncoding.GetStringBigUni(span, safeString); + } + + /// + /// Reads a little-endian UTF-16 string, filtering control codes and non-characters in safe mode. + /// + private string ReadStringLittleUni(bool safeString, int fixedLength = -1) + { + if (fixedLength == 0) + { + return ""; + } + + const int byteLength = 2; + var isFixedLength = fixedLength > -1; + + var remaining = Remaining; + int size; + if (isFixedLength) + { + size = fixedLength * byteLength; + if (size > remaining) + { + throw new EndOfStreamException("Cannot read past the end of the buffer."); + } + } + else + { + // Ensure even number of bytes + size = remaining - (remaining & 1); + } + + var span = _buffer.Slice(Position, size); + var index = span.IndexOfTerminator(byteLength); + + if (index > -1) + { + span = _buffer.Slice(Position, index); + } + + Position += isFixedLength || index < 0 ? size : index + byteLength; + + return TextEncoding.GetStringLittleUni(span, safeString); + } + + /// + /// Reads an ASCII string, filtering C0 control codes and DEL in safe mode. + /// + private string ReadStringAscii(bool safeString, int fixedLength = -1) + { + if (fixedLength == 0) + { + return ""; + } + + var isFixedLength = fixedLength > -1; + + int size = isFixedLength ? fixedLength : Remaining; + if (size > Remaining) + { + throw new EndOfStreamException("Cannot read past the end of the buffer."); + } + + var span = _buffer.Slice(Position, size); + var index = span.IndexOf((byte)0); + + if (index > -1) + { + span = _buffer.Slice(Position, index); + } + + Position += isFixedLength || index < 0 ? size : index + 1; + + return TextEncoding.GetStringAscii(span, safeString); + } + + /// + /// Reads a UTF-8 string, filtering control codes and non-characters in safe mode. + /// + private string ReadStringUtf8(bool safeString, int fixedLength = -1) + { + if (fixedLength == 0) + { + return ""; + } + + var isFixedLength = fixedLength > -1; + + int size = isFixedLength ? fixedLength : Remaining; + if (size > Remaining) + { + throw new EndOfStreamException("Cannot read past the end of the buffer."); + } + + var span = _buffer.Slice(Position, size); + var index = span.IndexOf((byte)0); + + if (index > -1) + { + span = _buffer.Slice(Position, index); + } + + Position += isFixedLength || index < 0 ? size : index + 1; + + return TextEncoding.GetStringUtf8(span, safeString); + } + + /// + /// Reads a Latin1 string, filtering C0/C1 control codes in safe mode. + /// + private string ReadStringLatin1(bool safeString, int fixedLength = -1) + { + if (fixedLength == 0) + { + return ""; + } + + var isFixedLength = fixedLength > -1; + + int size = isFixedLength ? fixedLength : Remaining; + if (size > Remaining) + { + throw new EndOfStreamException("Cannot read past the end of the buffer."); + } + + var span = _buffer.Slice(Position, size); + var index = span.IndexOf((byte)0); + + if (index > -1) + { + span = _buffer.Slice(Position, index); + } + + Position += isFixedLength || index < 0 ? size : index + 1; + + return TextEncoding.GetStringLatin1(span, safeString); + } [MethodImpl(MethodImplOptions.AggressiveInlining)] public int Seek(int offset, SeekOrigin origin) diff --git a/Projects/Server/Buffers/SpanWriter.cs b/Projects/Server/Buffers/SpanWriter.cs index ccdaddd38..788bf2466 100644 --- a/Projects/Server/Buffers/SpanWriter.cs +++ b/Projects/Server/Buffers/SpanWriter.cs @@ -1,6 +1,6 @@ /************************************************************************* * ModernUO * - * Copyright 2019-2025 - ModernUO Development Team * + * Copyright 2019-2026 - ModernUO Development Team * * Email: hi@modernuo.com * * File: SpanWriter.cs * * * @@ -273,8 +273,7 @@ public ref struct SpanWriter [MethodImpl(MethodImplOptions.AggressiveInlining)] public void WriteAscii(char chr) => Write((byte)chr); - public void WriteAscii( - ref RawInterpolatedStringHandler handler) + public void WriteAscii(ref RawInterpolatedStringHandler handler) { Write(handler.Text, Encoding.ASCII); handler.Clear(); @@ -289,9 +288,22 @@ public ref struct SpanWriter handler.Clear(); } - public void Write( - Encoding encoding, + public void WriteLatin1(ref RawInterpolatedStringHandler handler) + { + Write(handler.Text, Encoding.Latin1); + handler.Clear(); + } + + public void WriteLatin1( + IFormatProvider? formatProvider, + [InterpolatedStringHandlerArgument("formatProvider")] ref RawInterpolatedStringHandler handler) + { + Write(handler.Text, Encoding.Latin1); + handler.Clear(); + } + + public void Write(Encoding encoding, ref RawInterpolatedStringHandler handler) { Write(handler.Text, encoding); handler.Clear(); @@ -388,6 +400,19 @@ public ref struct SpanWriter [MethodImpl(MethodImplOptions.AggressiveInlining)] public void WriteAscii(string value, int fixedLength) => Write(value, Encoding.ASCII, fixedLength); + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void WriteLatin1(string value) => Write(value, Encoding.Latin1); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void WriteLatin1(string value, int fixedLength) => Write(value, Encoding.Latin1, fixedLength); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void WriteLatin1Null(string value) + { + Write(value, Encoding.Latin1); + Write((byte)0); // '\0' + } + [MethodImpl(MethodImplOptions.AggressiveInlining)] public void Clear(int count) { diff --git a/Projects/Server/Network/Packets/OutgoingAccountPackets.cs b/Projects/Server/Network/Packets/OutgoingAccountPackets.cs index 1d99fc201..861c660dc 100644 --- a/Projects/Server/Network/Packets/OutgoingAccountPackets.cs +++ b/Projects/Server/Network/Packets/OutgoingAccountPackets.cs @@ -89,7 +89,7 @@ public static class OutgoingAccountPackets var name = (m.RawName?.Trim()).DefaultIfNullOrEmpty("-no name-"); count++; - writer.WriteAscii(name, 30); + writer.WriteLatin1(name, 30); writer.Clear(30); // Password (empty) } } @@ -276,7 +276,7 @@ public static class OutgoingAccountPackets else { var name = (m.RawName?.Trim()).DefaultIfNullOrEmpty("-no name-"); - writer.WriteAscii(name, 30); + writer.WriteLatin1(name, 30); writer.Clear(30); // password } } @@ -341,7 +341,7 @@ public static class OutgoingAccountPackets else { var name = (m.RawName?.Trim()).DefaultIfNullOrEmpty("-no name-"); - writer.WriteAscii(name, 30); + writer.WriteLatin1(name, 30); writer.Clear(30); // password } } @@ -353,8 +353,8 @@ public static class OutgoingAccountPackets var ci = cityInfo[i]; writer.Write((byte)i); - writer.WriteAscii(ci.City, textLength); - writer.WriteAscii(ci.Building, textLength); + writer.WriteLatin1(ci.City, textLength); + writer.WriteLatin1(ci.Building, textLength); if (client70130) { writer.Write(ci.X); @@ -428,7 +428,7 @@ public static class OutgoingAccountPackets var si = info[i]; writer.Write((ushort)i); - writer.WriteAscii(si.Name, 32); + writer.WriteLatin1(si.Name, 32); writer.Write((byte)si.FullPercent); writer.Write((sbyte)si.TimeZone); // UO only supports IPv4 diff --git a/Projects/Server/Network/Packets/OutgoingEquipmentPackets.cs b/Projects/Server/Network/Packets/OutgoingEquipmentPackets.cs index 0e2109eb9..186cd0b64 100644 --- a/Projects/Server/Network/Packets/OutgoingEquipmentPackets.cs +++ b/Projects/Server/Network/Packets/OutgoingEquipmentPackets.cs @@ -63,7 +63,7 @@ public static class OutgoingEquipmentPackets writer.Write(-3); // crafted by writer.Write((ushort)crafterName.Length); - writer.WriteAscii(crafterName); + writer.WriteLatin1(crafterName); } if (unidentified) diff --git a/Projects/Server/Network/Packets/OutgoingMenuPackets.cs b/Projects/Server/Network/Packets/OutgoingMenuPackets.cs index dbe1f9aeb..126ce072b 100644 --- a/Projects/Server/Network/Packets/OutgoingMenuPackets.cs +++ b/Projects/Server/Network/Packets/OutgoingMenuPackets.cs @@ -62,7 +62,7 @@ public static class OutgoingMenuPackets if (question != null) { - writer.WriteAscii(question); + writer.WriteLatin1(question); } writer.Write((byte)entriesLength); @@ -84,7 +84,7 @@ public static class OutgoingMenuPackets { var nameLength = name.Length; writer.Write((byte)nameLength); - writer.WriteAscii(name); + writer.WriteLatin1(name); } } @@ -120,7 +120,7 @@ public static class OutgoingMenuPackets if (question != null) { - writer.WriteAscii(question); + writer.WriteLatin1(question); } writer.Write((byte)answersLength); @@ -139,7 +139,7 @@ public static class OutgoingMenuPackets { var nameLength = answer.Length; writer.Write((byte)nameLength); - writer.WriteAscii(answer); + writer.WriteLatin1(answer); } } diff --git a/Projects/Server/Network/Packets/OutgoingMessagePackets.cs b/Projects/Server/Network/Packets/OutgoingMessagePackets.cs index 3a5e7f974..76c833733 100644 --- a/Projects/Server/Network/Packets/OutgoingMessagePackets.cs +++ b/Projects/Server/Network/Packets/OutgoingMessagePackets.cs @@ -79,7 +79,7 @@ public static class OutgoingMessagePackets writer.Write((short)hue); writer.Write((short)font); writer.Write(number); - writer.WriteAscii(name, 30); + writer.WriteLatin1(name, 30); writer.WriteLittleUniNull(args); writer.WritePacketLength(); @@ -139,8 +139,8 @@ public static class OutgoingMessagePackets writer.Write((short)font); writer.Write(number); writer.Write((byte)affixType); - writer.WriteAscii(name, 30); - writer.WriteAsciiNull(affix); + writer.WriteLatin1(name, 30); + writer.WriteLatin1Null(affix); writer.WriteBigUniNull(args); writer.WritePacketLength(); @@ -214,13 +214,13 @@ public static class OutgoingMessagePackets writer.Write((short)font); if (ascii) { - writer.WriteAscii(name, 30); - writer.WriteAsciiNull(text); + writer.WriteLatin1(name, 30); + writer.WriteLatin1Null(text); } else { writer.WriteAscii(lang, 4); - writer.WriteAscii(name, 30); + writer.WriteLatin1(name, 30); writer.WriteBigUniNull(text); } diff --git a/Projects/Server/Network/Packets/OutgoingMobilePackets.cs b/Projects/Server/Network/Packets/OutgoingMobilePackets.cs index a121ec1da..4baf0cfc3 100644 --- a/Projects/Server/Network/Packets/OutgoingMobilePackets.cs +++ b/Projects/Server/Network/Packets/OutgoingMobilePackets.cs @@ -309,7 +309,7 @@ public static class OutgoingMobilePackets writer.Write((byte)0x98); // Packet ID writer.Write((ushort)37); writer.Write(m.Serial); - writer.WriteAscii(m.Name ?? "", 29); + writer.WriteLatin1(m.Name ?? "", 29); writer.Write((byte)0); // Null terminator ns.Send(writer.Span); @@ -507,7 +507,7 @@ public static class OutgoingMobilePackets writer.Write((byte)0x11); // Packet ID writer.Seek(2, SeekOrigin.Current); writer.Write(beheld.Serial); - writer.WriteAscii(name, 30); + writer.WriteLatin1(name, 30); writer.WriteAttribute(beheld.HitsMax, beheld.Hits, version == 0, true); writer.Write(canBeRenamed); writer.Write((byte)version); diff --git a/Projects/Server/Network/Packets/OutgoingPlayerPackets.cs b/Projects/Server/Network/Packets/OutgoingPlayerPackets.cs index 5c5039345..7860ab7ea 100644 --- a/Projects/Server/Network/Packets/OutgoingPlayerPackets.cs +++ b/Projects/Server/Network/Packets/OutgoingPlayerPackets.cs @@ -77,7 +77,7 @@ public static class OutgoingPlayerPackets writer.Write((byte)0xB8); // Packet ID writer.Write((ushort)length); writer.Write(m); - writer.WriteAsciiNull(header); + writer.WriteLatin1Null(header); writer.WriteBigUniNull(footer); writer.WriteBigUniNull(body); @@ -264,7 +264,7 @@ public static class OutgoingPlayerPackets var writer = new SpanWriter(stackalloc byte[66]); writer.Write((byte)0x88); // Packet ID writer.Write(m); - writer.WriteAscii(title, 60); + writer.WriteLatin1(title, 60); writer.Write(flags); ns.Send(writer.Span); @@ -302,7 +302,7 @@ public static class OutgoingPlayerPackets writer.Write((byte)type); writer.Write(tip); writer.Write((ushort)text.Length); - writer.WriteAscii(text); + writer.WriteLatin1(text); ns.Send(writer.Span); } diff --git a/Projects/Server/Network/Packets/OutgoingSecureTradePackets.cs b/Projects/Server/Network/Packets/OutgoingSecureTradePackets.cs index 473e63f6c..8e8be237f 100644 --- a/Projects/Server/Network/Packets/OutgoingSecureTradePackets.cs +++ b/Projects/Server/Network/Packets/OutgoingSecureTradePackets.cs @@ -45,7 +45,7 @@ public static class OutgoingSecureTradePackets writer.Write(second.Serial); writer.Write(true); - writer.WriteAscii(name ?? "", 30); + writer.WriteLatin1(name ?? "", 30); ns.Send(writer.Span); } diff --git a/Projects/Server/Network/Packets/OutgoingVendorBuyPackets.cs b/Projects/Server/Network/Packets/OutgoingVendorBuyPackets.cs index da4b593b7..56864a98f 100644 --- a/Projects/Server/Network/Packets/OutgoingVendorBuyPackets.cs +++ b/Projects/Server/Network/Packets/OutgoingVendorBuyPackets.cs @@ -102,7 +102,7 @@ public static class OutgoingVendorBuyPackets var desc = bis.Description ?? ""; writer.Write((byte)(desc.Length + 1)); - writer.WriteAsciiNull(desc); + writer.WriteLatin1Null(desc); } ns.Send(writer.Span); diff --git a/Projects/Server/Network/Packets/OutgoingVendorSellPackets.cs b/Projects/Server/Network/Packets/OutgoingVendorSellPackets.cs index 83caf7ad6..b3e3fd409 100644 --- a/Projects/Server/Network/Packets/OutgoingVendorSellPackets.cs +++ b/Projects/Server/Network/Packets/OutgoingVendorSellPackets.cs @@ -55,7 +55,7 @@ public static class OutgoingVendorSellPackets var name = (item.Name?.Trim()).DefaultIfNullOrEmpty(sis.Name ?? ""); writer.Write((ushort)name.Length); - writer.WriteAscii(name); + writer.WriteLatin1(name); } writer.WritePacketLength(); diff --git a/Projects/Server/Server.csproj b/Projects/Server/Server.csproj index 84541d410..e50db55ff 100644 --- a/Projects/Server/Server.csproj +++ b/Projects/Server/Server.csproj @@ -37,7 +37,7 @@ - + diff --git a/Projects/Server/Text/TextEncoding.cs b/Projects/Server/Text/TextEncoding.cs index 8e18d81f1..fa2ae69c0 100644 --- a/Projects/Server/Text/TextEncoding.cs +++ b/Projects/Server/Text/TextEncoding.cs @@ -1,6 +1,6 @@ /************************************************************************* * ModernUO * - * Copyright 2019-2025 - ModernUO Development Team * + * Copyright 2019-2026 - ModernUO Development Team * * Email: hi@modernuo.com * * File: TextEncoding.cs * * * @@ -14,7 +14,9 @@ *************************************************************************/ using System; +using System.Buffers; using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; using System.Text; using Server.Buffers; @@ -22,11 +24,46 @@ namespace Server.Text; public static class TextEncoding { - private static Encoding m_UTF8, m_Unicode, m_UnicodeLE; + private static Encoding _utf8, _unicode, _unicodeLE; - public static Encoding UTF8 => m_UTF8 ??= new UTF8Encoding(false, false); - public static Encoding Unicode => m_Unicode ??= new UnicodeEncoding(true, false, false); - public static Encoding UnicodeLE => m_UnicodeLE ??= new UnicodeEncoding(false, false, false); + // SearchValues for invalid ASCII display bytes (C0: 0x00-0x1F, DEL: 0x7F) + // Note: Bytes >= 0x80 are invalid ASCII and become '?' when decoded, which is acceptable + private static readonly SearchValues InvalidAsciiBytes = SearchValues.Create( + 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, + 0x08, 0x09, 0x0A, 0x0B, 0x0C, 0x0D, 0x0E, 0x0F, + 0x10, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, + 0x18, 0x19, 0x1A, 0x1B, 0x1C, 0x1D, 0x1E, 0x1F, + 0x7F + ); + + // SearchValues for invalid Latin1 bytes (C0: 0x00-0x1F, C1: 0x80-0x9F) + private static readonly SearchValues InvalidLatin1Bytes = SearchValues.Create( + 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, + 0x08, 0x09, 0x0A, 0x0B, 0x0C, 0x0D, 0x0E, 0x0F, + 0x10, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, + 0x18, 0x19, 0x1A, 0x1B, 0x1C, 0x1D, 0x1E, 0x1F, + 0x80, 0x81, 0x82, 0x83, 0x84, 0x85, 0x86, 0x87, + 0x88, 0x89, 0x8A, 0x8B, 0x8C, 0x8D, 0x8E, 0x8F, + 0x90, 0x91, 0x92, 0x93, 0x94, 0x95, 0x96, 0x97, + 0x98, 0x99, 0x9A, 0x9B, 0x9C, 0x9D, 0x9E, 0x9F + ); + + // SearchValues for invalid Latin1 chars (same ranges as bytes) + private static readonly SearchValues InvalidLatin1Chars = SearchValues.Create( + '\x00', '\x01', '\x02', '\x03', '\x04', '\x05', '\x06', '\x07', + '\x08', '\x09', '\x0A', '\x0B', '\x0C', '\x0D', '\x0E', '\x0F', + '\x10', '\x11', '\x12', '\x13', '\x14', '\x15', '\x16', '\x17', + '\x18', '\x19', '\x1A', '\x1B', '\x1C', '\x1D', '\x1E', '\x1F', + '\x80', '\x81', '\x82', '\x83', '\x84', '\x85', '\x86', '\x87', + '\x88', '\x89', '\x8A', '\x8B', '\x8C', '\x8D', '\x8E', '\x8F', + '\x90', '\x91', '\x92', '\x93', '\x94', '\x95', '\x96', '\x97', + '\x98', '\x99', '\x9A', '\x9B', '\x9C', '\x9D', '\x9E', '\x9F' + ); + + public static Encoding UTF8 => _utf8 ??= new UTF8Encoding(false, false); + public static Encoding Unicode => _unicode ??= new UnicodeEncoding(true, false, false); + public static Encoding UnicodeLE => _unicodeLE ??= new UnicodeEncoding(false, false, false); + public static Encoding Latin1 => Encoding.Latin1; [MethodImpl(MethodImplOptions.AggressiveInlining)] public static byte[] GetBytesAscii(this string str) => GetBytes(str, Encoding.ASCII); @@ -43,6 +80,9 @@ public static class TextEncoding [MethodImpl(MethodImplOptions.AggressiveInlining)] public static byte[] GetBytesAscii(this ReadOnlySpan str) => GetBytes(str, Encoding.ASCII); + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static byte[] GetBytesLatin1(this ReadOnlySpan str) => GetBytes(str, Latin1); + [MethodImpl(MethodImplOptions.AggressiveInlining)] public static byte[] GetBytesBigUni(this ReadOnlySpan str) => GetBytes(str, Unicode); @@ -86,6 +126,12 @@ public static class TextEncoding [MethodImpl(MethodImplOptions.AggressiveInlining)] public static int GetBytesAscii(this ReadOnlySpan str, Span buffer) => Encoding.ASCII.GetBytes(str, buffer); + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static int GetBytesLatin1(this ReadOnlySpan str, Span buffer) => Latin1.GetBytes(str, buffer); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static int GetBytesLatin1(this string str, Span buffer) => Latin1.GetBytes(str, buffer); + [MethodImpl(MethodImplOptions.AggressiveInlining)] public static int GetBytesBigUni(this string str, Span buffer) => Unicode.GetBytes(str, buffer); @@ -115,14 +161,111 @@ public static class TextEncoding _ => 1 }; - public static string GetString(ReadOnlySpan span, Encoding encoding, bool safeString = false) + /// + /// Decodes an ASCII byte span to a string, filtering C0 control codes and DEL in safe mode. + /// + /// + /// For ASCII, bytes >= 0x80 are invalid and become '?' when decoded, which is acceptable for display. + /// Only C0 (0x00-0x1F) and DEL (0x7F) need explicit filtering. + /// + public static string GetStringAscii(ReadOnlySpan bytes, bool safeString = false) { if (!safeString) { - return encoding.GetString(span); + return Encoding.ASCII.GetString(bytes); } - var charCount = encoding.GetMaxCharCount(span.Length); + // Check bytes directly - filter at byte level, then decode once + var index = bytes.IndexOfAny(InvalidAsciiBytes); + if (index == -1) + { + return Encoding.ASCII.GetString(bytes); + } + + // Has invalid bytes, filter and decode + return FilterInvalidAsciiBytes(bytes, index); + } + + private static string FilterInvalidAsciiBytes(ReadOnlySpan bytes, int firstInvalidIndex) + { + var maxLength = bytes.Length; + + char[] rentedChars = null; + var charBuffer = maxLength <= 256 + ? stackalloc char[maxLength] + : rentedChars = STArrayPool.Shared.Rent(maxLength); + + try + { + using var sb = maxLength <= 256 + ? new ValueStringBuilder(stackalloc char[maxLength]) + : ValueStringBuilder.Create(maxLength); + + var index = firstInvalidIndex; + + while (index != -1) + { + if (index > 0) + { + // Decode segment and append + var segment = bytes[..index]; + var decoded = Encoding.ASCII.GetChars(segment, charBuffer); + sb.Append(charBuffer[..decoded]); + } + + if (index + 1 < bytes.Length) + { + bytes = bytes[(index + 1)..]; + index = bytes.IndexOfAny(InvalidAsciiBytes); + } + else + { + bytes = []; + index = -1; + } + } + + if (bytes.Length > 0) + { + var decoded = Encoding.ASCII.GetChars(bytes, charBuffer); + sb.Append(charBuffer[..decoded]); + } + + return sb.ToString(); + } + finally + { + if (rentedChars != null) + { + STArrayPool.Shared.Return(rentedChars); + } + } + } + + /// + /// Decodes a Latin1 byte span to a string, filtering C0/C1 control codes in safe mode. + /// + public static string GetStringLatin1(ReadOnlySpan bytes, bool safeString = false) + { + if (!safeString) + { + return Latin1.GetString(bytes); + } + + // Check bytes directly - Latin1 is 1:1 mapping, avoids char buffer for valid strings + var index = bytes.IndexOfAny(InvalidLatin1Bytes); + if (index == -1) + { + return Latin1.GetString(bytes); + } + + // Has invalid bytes, need to filter - now allocate char buffer + return FilterInvalidLatin1Chars(bytes, index); + } + + private static string FilterInvalidLatin1Chars(ReadOnlySpan bytes, int firstInvalidIndex) + { + var charCount = bytes.Length; char[] rentedChars = null; var chars = charCount <= 256 @@ -131,19 +274,16 @@ public static class TextEncoding try { - var length = encoding.GetChars(span, chars); + var length = Latin1.GetChars(bytes, chars); chars = chars[..length]; - var index = chars.IndexOfAnyExceptInRange((char)0x20, (char)0xFFFD); - if (index == -1) - { - return new string(chars); - } - using var sb = charCount <= 256 ? new ValueStringBuilder(stackalloc char[charCount]) : ValueStringBuilder.Create(charCount); + // Start from the first invalid index we already found + var index = firstInvalidIndex; + while (index != -1) { sb.Append(chars[..index]); @@ -151,7 +291,7 @@ public static class TextEncoding if (index + 1 < chars.Length) { chars = chars[(index + 1)..]; - index = chars.IndexOfAnyExceptInRange((char)0x20, (char)0xFFFD); + index = chars.IndexOfAny(InvalidLatin1Chars); } else { @@ -174,4 +314,281 @@ public static class TextEncoding } } } + + /// + /// Decodes a UTF-8 byte span to a string, filtering control codes and non-characters in safe mode. + /// + /// + /// Filters: C0 (0x00-0x1F), DEL (0x7F), C1 (0x80-0x9F), and non-characters (0xFFFD-0xFFFF). + /// Optimized to check bytes for C0/DEL first (single-byte in UTF-8), then chars for C1/non-chars. + /// + public static string GetStringUtf8(ReadOnlySpan bytes, bool safeString = false) + { + if (!safeString) + { + return UTF8.GetString(bytes); + } + + // Quick check: C0 (0x00-0x1F) and DEL (0x7F) are single-byte in UTF-8 + var hasC0OrDel = bytes.IndexOfAny(InvalidAsciiBytes) >= 0; + + var charCount = UTF8.GetMaxCharCount(bytes.Length); + + char[] rentedChars = null; + var chars = charCount <= 256 + ? stackalloc char[charCount] + : rentedChars = STArrayPool.Shared.Rent(charCount); + + try + { + var length = UTF8.GetChars(bytes, chars); + chars = chars[..length]; + + // If no C0/DEL in bytes, only need to check for C1 and non-chars + var index = hasC0OrDel + ? IndexOfInvalidUnicodeChar(chars) + : IndexOfInvalidUnicodeCharNonAscii(chars); + + if (index == -1) + { + return new string(chars); + } + + return FilterInvalidUnicodeChars(chars, index, hasC0OrDel); + } + finally + { + if (rentedChars != null) + { + STArrayPool.Shared.Return(rentedChars); + } + } + } + + /// + /// Decodes a big-endian UTF-16 byte span to a string, filtering control codes and non-characters in safe mode. + /// + public static string GetStringBigUni(ReadOnlySpan bytes, bool safeString = false) + { + if (!safeString) + { + return Unicode.GetString(bytes); + } + + var charCount = Unicode.GetMaxCharCount(bytes.Length); + + char[] rentedChars = null; + var chars = charCount <= 256 + ? stackalloc char[charCount] + : rentedChars = STArrayPool.Shared.Rent(charCount); + + try + { + var length = Unicode.GetChars(bytes, chars); + chars = chars[..length]; + + var index = IndexOfInvalidUnicodeChar(chars); + if (index == -1) + { + return new string(chars); + } + + return FilterInvalidUnicodeChars(chars, index, fullCheck: true); + } + finally + { + if (rentedChars != null) + { + STArrayPool.Shared.Return(rentedChars); + } + } + } + + /// + /// Decodes a little-endian UTF-16 byte span to a string, filtering control codes and non-characters in safe mode. + /// + /// + /// Optimized for little-endian systems using direct memory cast (no decoding overhead). + /// + public static string GetStringLittleUni(ReadOnlySpan bytes, bool safeString = false) + { + // Direct cast - UTF-16 LE bytes map directly to chars on little-endian systems + var chars = MemoryMarshal.Cast(bytes); + + if (!safeString) + { + return new string(chars); + } + + var index = IndexOfInvalidUnicodeChar(chars); + if (index == -1) + { + return new string(chars); + } + + return FilterInvalidUnicodeChars(chars, index, fullCheck: true); + } + + /// + /// Generic string decoding with filtering. Prefer encoding-specific methods for better performance. + /// + public static string GetString(ReadOnlySpan span, Encoding encoding, bool safeString = false) + { + if (!safeString) + { + return encoding.GetString(span); + } + + var charCount = encoding.GetMaxCharCount(span.Length); + + char[] rentedChars = null; + var chars = charCount <= 256 + ? stackalloc char[charCount] + : rentedChars = STArrayPool.Shared.Rent(charCount); + + try + { + var length = encoding.GetChars(span, chars); + chars = chars[..length]; + + var index = IndexOfInvalidUnicodeChar(chars); + if (index == -1) + { + return new string(chars); + } + + using var sb = charCount <= 256 + ? new ValueStringBuilder(stackalloc char[charCount]) + : ValueStringBuilder.Create(charCount); + + while (index != -1) + { + sb.Append(chars[..index]); + + if (index + 1 < chars.Length) + { + chars = chars[(index + 1)..]; + index = IndexOfInvalidUnicodeChar(chars); + } + else + { + index = -1; + } + } + + if (chars.Length > 0) + { + sb.Append(chars); + } + + return sb.ToString(); + } + finally + { + if (rentedChars != null) + { + STArrayPool.Shared.Return(rentedChars); + } + } + } + + /// + /// Finds the first invalid Unicode character for display. + /// Invalid: C0 (0x00-0x1F), DEL (0x7F), C1 (0x80-0x9F), non-chars (0xFFFE-0xFFFF). + /// Note: Surrogate pairs (0xD800-0xDFFF) are not filtered - proper validation requires context checking. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static int IndexOfInvalidUnicodeChar(ReadOnlySpan chars) + { + // Check invalid ranges and return the minimum index found + var c0Index = chars.IndexOfAnyInRange((char)0x00, (char)0x1F); // C0 control codes + var delC1Index = chars.IndexOfAnyInRange((char)0x7F, (char)0x9F); // DEL + C1 control codes + var nonCharIndex = chars.IndexOfAnyInRange((char)0xFFFE, (char)0xFFFF); // Non-characters + + // Find minimum non-negative index + var minIndex = -1; + + if (c0Index >= 0) + { + minIndex = c0Index; + } + + if (delC1Index >= 0 && (minIndex < 0 || delC1Index < minIndex)) + { + minIndex = delC1Index; + } + + if (nonCharIndex >= 0 && (minIndex < 0 || nonCharIndex < minIndex)) + { + minIndex = nonCharIndex; + } + + return minIndex; + } + + /// + /// Finds the first invalid Unicode character, excluding C0/DEL (already checked at byte level). + /// Checks: C1 (0x80-0x9F), non-chars (0xFFFE-0xFFFF). + /// Note: Surrogate pairs (0xD800-0xDFFF) are not filtered - proper validation requires context checking. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static int IndexOfInvalidUnicodeCharNonAscii(ReadOnlySpan chars) + { + // Only check C1 and non-chars (C0/DEL already verified absent at byte level) + var c1Index = chars.IndexOfAnyInRange((char)0x80, (char)0x9F); // C1 control codes + var nonCharIndex = chars.IndexOfAnyInRange((char)0xFFFE, (char)0xFFFF); // Non-characters + + if (c1Index < 0) + { + return nonCharIndex; + } + + if (nonCharIndex < 0) + { + return c1Index; + } + + return Math.Min(c1Index, nonCharIndex); + } + + /// + /// Filters invalid Unicode characters from a char span by removing them. + /// The UO client renders nothing for invalid characters, so removal is most efficient. + /// + private static string FilterInvalidUnicodeChars(ReadOnlySpan chars, int firstInvalidIndex, bool fullCheck) + { + var maxLength = chars.Length; + + using var sb = maxLength <= 256 + ? new ValueStringBuilder(stackalloc char[maxLength]) + : ValueStringBuilder.Create(maxLength); + + var index = firstInvalidIndex; + + while (index != -1) + { + sb.Append(chars[..index]); + // Skip the invalid character (don't append replacement - client renders nothing anyway) + + if (index + 1 < chars.Length) + { + chars = chars[(index + 1)..]; + index = fullCheck + ? IndexOfInvalidUnicodeChar(chars) + : IndexOfInvalidUnicodeCharNonAscii(chars); + } + else + { + chars = []; + index = -1; + } + } + + if (chars.Length > 0) + { + sb.Append(chars); + } + + return sb.ToString(); + } } diff --git a/Projects/UOContent.Tests/Tests/Accounting/AccountHandlerTests.cs b/Projects/UOContent.Tests/Tests/Accounting/AccountHandlerTests.cs index 429d252eb..0520f411e 100644 --- a/Projects/UOContent.Tests/Tests/Accounting/AccountHandlerTests.cs +++ b/Projects/UOContent.Tests/Tests/Accounting/AccountHandlerTests.cs @@ -23,7 +23,6 @@ public class AccountHandlerTests [Theory] [InlineData("", false)] // Empty password - [InlineData("Invalid\x01Char", false)] // Contains invalid ASCII character [InlineData("ValidPass123!", true)] // Standard Password [InlineData(" ", true)] // Single space [InlineData("ValidPass!@#", true)] // Valid special characters diff --git a/Projects/UOContent/Accounting/AccountHandler.cs b/Projects/UOContent/Accounting/AccountHandler.cs index 50ee90e81..1e676ed9e 100644 --- a/Projects/UOContent/Accounting/AccountHandler.cs +++ b/Projects/UOContent/Accounting/AccountHandler.cs @@ -247,15 +247,10 @@ public static class AccountHandler username.Length > 0 && // Usernames must not start with a space, end with a space, or end with a period !username.StartsWith(' ') && !username.EndsWith(' ') && !username.EndsWith('.') && - // Usernames must only contain characters [0x20 -> 0x7E], and not contain any forbidden characters - !username.ContainsAnyExceptInRange((char)0x20, (char)0x7E) && !username.ContainsAny(ForbiddenChars); [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static bool IsValidPassword(ReadOnlySpan password) => - password.Length > 0 && - // Passwords must have characters [0x20 -> 0x7E] - !password.ContainsAnyExceptInRange((char)0x20, (char)0x7E); + public static bool IsValidPassword(ReadOnlySpan password) => password.Length > 0; private static Account CreateAccount(NetState state, string username, string password) { diff --git a/Projects/UOContent/Gumps/Base/GumpLayoutBuilder.cs b/Projects/UOContent/Gumps/Base/GumpLayoutBuilder.cs index bbb639328..9f8be4b63 100644 --- a/Projects/UOContent/Gumps/Base/GumpLayoutBuilder.cs +++ b/Projects/UOContent/Gumps/Base/GumpLayoutBuilder.cs @@ -577,7 +577,7 @@ public ref struct GumpLayoutBuilder { if (!value.IsEmpty) { - _bytesWritten += value.GetBytesAscii(_layoutBuffer.AsSpan(_bytesWritten)); + _bytesWritten += value.GetBytesLatin1(_layoutBuffer.AsSpan(_bytesWritten)); } } diff --git a/Projects/UOContent/Gumps/Base/Legacy/GumpHtmlLocalized.cs b/Projects/UOContent/Gumps/Base/Legacy/GumpHtmlLocalized.cs index 7aec85e4f..4ebb85539 100644 --- a/Projects/UOContent/Gumps/Base/Legacy/GumpHtmlLocalized.cs +++ b/Projects/UOContent/Gumps/Base/Legacy/GumpHtmlLocalized.cs @@ -122,7 +122,7 @@ public class GumpHtmlLocalized : GumpEntry } case GumpHtmlLocalizedType.Args: { - writer.WriteAscii( + writer.WriteLatin1( $"{{ xmfhtmltok {X} {Y} {Width} {Height} {(Background ? "1" : "0")} {(Scrollbar ? "1" : "0")} {Color} {Number} @{Args}@ }}" ); break; diff --git a/Projects/UOContent/Gumps/Base/Legacy/GumpTooltip.cs b/Projects/UOContent/Gumps/Base/Legacy/GumpTooltip.cs index 2d584eb8b..0e10e58be 100644 --- a/Projects/UOContent/Gumps/Base/Legacy/GumpTooltip.cs +++ b/Projects/UOContent/Gumps/Base/Legacy/GumpTooltip.cs @@ -34,6 +34,13 @@ public class GumpTooltip : GumpEntry public override void AppendTo(ref SpanWriter writer, OrderedSet strings, ref int entries, ref int switches) { - writer.WriteAscii(string.IsNullOrEmpty(Args) ? $"{{ tooltip {Number} }}" : $"{{ tooltip {Number} @{Args}@ }}"); + if (string.IsNullOrEmpty(Args)) + { + writer.WriteAscii($"{{ tooltip {Number} }}"); + } + else + { + writer.WriteLatin1($"{{ tooltip {Number} @{Args}@ }}"); + } } } diff --git a/Projects/UOContent/Gumps/Base/OutgoingGumpPackets.cs b/Projects/UOContent/Gumps/Base/OutgoingGumpPackets.cs index 9657962dd..54e333658 100644 --- a/Projects/UOContent/Gumps/Base/OutgoingGumpPackets.cs +++ b/Projects/UOContent/Gumps/Base/OutgoingGumpPackets.cs @@ -73,9 +73,9 @@ public static class OutgoingGumpPackets writer.Write(serial); writer.Write((short)gumpId); writer.Write((short)(unknown.Length + 1)); - writer.WriteAsciiNull(unknown); + writer.WriteLatin1Null(unknown); writer.Write((short)(caption.Length + 1)); - writer.WriteAsciiNull(caption); + writer.WriteLatin1Null(caption); ns.Send(writer.Span); } diff --git a/Projects/UOContent/Gumps/Base/RelayInfo.cs b/Projects/UOContent/Gumps/Base/RelayInfo.cs index fc5ecb14d..76a32881b 100644 --- a/Projects/UOContent/Gumps/Base/RelayInfo.cs +++ b/Projects/UOContent/Gumps/Base/RelayInfo.cs @@ -1,6 +1,6 @@ /************************************************************************* * ModernUO * - * Copyright 2019-2024 - ModernUO Development Team * + * Copyright 2019-2026 - ModernUO Development Team * * Email: hi@modernuo.com * * File: RelayInfo.cs * * * @@ -48,8 +48,6 @@ public readonly ref struct RelayInfo public string GetTextEntry(int entryId) { var index = _textIds.IndexOf((ushort)entryId); - return index == -1 - ? default - : TextEncoding.GetString(_textBlock[_textRanges[index]], TextEncoding.Unicode, true); + return index == -1 ? null : TextEncoding.GetStringBigUni(_textBlock[_textRanges[index]], true); } } diff --git a/Projects/UOContent/Items/Books/BookPackets.cs b/Projects/UOContent/Items/Books/BookPackets.cs index 806b17de2..fb33cff34 100644 --- a/Projects/UOContent/Items/Books/BookPackets.cs +++ b/Projects/UOContent/Items/Books/BookPackets.cs @@ -41,8 +41,8 @@ namespace Server.Items reader.Seek(4, SeekOrigin.Current); // Skip flags and page count - var title = reader.ReadAsciiSafe(60); - var author = reader.ReadAsciiSafe(30); + var title = reader.ReadLatin1Safe(60); + var author = reader.ReadLatin1Safe(30); book.Title = title.FixHtml(); book.Author = author.FixHtml(); diff --git a/Projects/UOContent/Items/Games/Mahjong/MahjongPackets.cs b/Projects/UOContent/Items/Games/Mahjong/MahjongPackets.cs index d68429781..13a629304 100644 --- a/Projects/UOContent/Items/Games/Mahjong/MahjongPackets.cs +++ b/Projects/UOContent/Items/Games/Mahjong/MahjongPackets.cs @@ -346,7 +346,7 @@ namespace Server.Engines.Mahjong writer.Write((byte)0); writer.Write(players.IsPublic(i)); - writer.WriteAscii(m?.Name ?? "", 30); + writer.WriteLatin1(m?.Name ?? "", 30); writer.Write(m == null || !players.IsInGamePlayer(i)); count++; diff --git a/Projects/UOContent/Network/Packets/IncomingAccountPackets.cs b/Projects/UOContent/Network/Packets/IncomingAccountPackets.cs index 8711d3f7f..2ec4f8059 100644 --- a/Projects/UOContent/Network/Packets/IncomingAccountPackets.cs +++ b/Projects/UOContent/Network/Packets/IncomingAccountPackets.cs @@ -63,7 +63,7 @@ public static class IncomingAccountPackets var unk2 = reader.ReadInt32(); int unk3 = reader.ReadByte(); */ - var name = reader.ReadAscii(30); + var name = reader.ReadLatin1Safe(30); reader.Seek(2, SeekOrigin.Current); var flags = reader.ReadInt32(); @@ -371,8 +371,8 @@ public static class IncomingAccountPackets state.Version = ap.Version; state.Seeded = true; - var username = reader.ReadAscii(30); - var password = reader.ReadAscii(30); + var username = reader.ReadLatin1Safe(30); + var password = reader.ReadLatin1Safe(30); var e = new GameServer.GameLoginEventArgs(state, username, password); @@ -446,8 +446,8 @@ public static class IncomingAccountPackets state.SentFirstPacket = true; - var username = reader.ReadAscii(30); - var password = reader.ReadAscii(30); + var username = reader.ReadLatin1Safe(30); + var password = reader.ReadLatin1Safe(30); var accountLoginEventArgs = new AccountLoginEventArgs(state, username, password); diff --git a/Projects/UOContent/Network/Packets/IncomingMessagePackets.cs b/Projects/UOContent/Network/Packets/IncomingMessagePackets.cs index 1ce975082..4e6987b20 100644 --- a/Projects/UOContent/Network/Packets/IncomingMessagePackets.cs +++ b/Projects/UOContent/Network/Packets/IncomingMessagePackets.cs @@ -40,7 +40,7 @@ public static class IncomingMessagePackets var type = (MessageType)reader.ReadByte(); int hue = reader.ReadInt16(); reader.ReadInt16(); // font - var text = reader.ReadAsciiSafe().Trim(); + var text = reader.ReadLatin1Safe().Trim(); if (text.Length is <= 0 or > 128) { diff --git a/Projects/UOContent/Network/Packets/IncomingMobilePackets.cs b/Projects/UOContent/Network/Packets/IncomingMobilePackets.cs index 8dab27f0e..d85a84730 100644 --- a/Projects/UOContent/Network/Packets/IncomingMobilePackets.cs +++ b/Projects/UOContent/Network/Packets/IncomingMobilePackets.cs @@ -36,7 +36,7 @@ public static class IncomingMobilePackets if (targ != null) { - RenameRequests.RenameRequest(from, targ, reader.ReadAsciiSafe()); + RenameRequests.RenameRequest(from, targ, reader.ReadLatin1Safe()); } } diff --git a/Projects/UOContent/Network/Packets/IncomingPlayerPackets.cs b/Projects/UOContent/Network/Packets/IncomingPlayerPackets.cs index fe803caa7..14f91596c 100644 --- a/Projects/UOContent/Network/Packets/IncomingPlayerPackets.cs +++ b/Projects/UOContent/Network/Packets/IncomingPlayerPackets.cs @@ -223,7 +223,7 @@ public static class IncomingPlayerPackets var serial = reader.ReadUInt32(); var prompt = reader.ReadInt32(); var type = reader.ReadInt32(); - var text = reader.ReadAsciiSafe(); + var text = reader.ReadLatin1Safe(); if (text.Length > 128) {