From e7a1bda2af049be2f910ed2bfe214b6641171e80 Mon Sep 17 00:00:00 2001 From: Quick <577652+markdwags@users.noreply.github.com> Date: Fri, 1 Nov 2024 11:44:32 -0500 Subject: [PATCH] Read updated cliloc format --- Razor/Razor.csproj | 19 + Razor/UltimaSDK/BwtDecompress.cs | 253 +++++++++++++ Razor/UltimaSDK/StackDataReader.cs | 495 ++++++++++++++++++++++++++ Razor/UltimaSDK/StringHelper.cs | 379 ++++++++++++++++++++ Razor/UltimaSDK/StringList.cs | 56 +++ Razor/UltimaSDK/ValueStringBuilder.cs | 378 ++++++++++++++++++++ Razor/packages.config | 7 + 7 files changed, 1587 insertions(+) create mode 100644 Razor/UltimaSDK/BwtDecompress.cs create mode 100644 Razor/UltimaSDK/StackDataReader.cs create mode 100644 Razor/UltimaSDK/StringHelper.cs create mode 100644 Razor/UltimaSDK/ValueStringBuilder.cs create mode 100644 Razor/packages.config diff --git a/Razor/Razor.csproj b/Razor/Razor.csproj index 1ca6fc77..12f762b4 100644 --- a/Razor/Razor.csproj +++ b/Razor/Razor.csproj @@ -156,9 +156,13 @@ .\cuoapi.dll + System + + ..\packages\System.Buffers.4.5.1\lib\net461\System.Buffers.dll + System.Data @@ -166,6 +170,16 @@ System.Drawing + + ..\packages\System.Memory.4.5.5\lib\net461\System.Memory.dll + + + + ..\packages\System.Numerics.Vectors.4.5.0\lib\net46\System.Numerics.Vectors.dll + + + ..\packages\System.Runtime.CompilerServices.Unsafe.4.5.3\lib\net461\System.Runtime.CompilerServices.Unsafe.dll + System.Windows.Forms @@ -493,6 +507,7 @@ + @@ -512,7 +527,9 @@ + + @@ -520,6 +537,7 @@ + @@ -627,6 +645,7 @@ + diff --git a/Razor/UltimaSDK/BwtDecompress.cs b/Razor/UltimaSDK/BwtDecompress.cs new file mode 100644 index 00000000..57ebdd6b --- /dev/null +++ b/Razor/UltimaSDK/BwtDecompress.cs @@ -0,0 +1,253 @@ +#region license +// Razor: An Ultima Online Assistant +// Copyright (c) 2024 Razor Development Community on GitHub +// +// 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. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . +#endregion +using System; +using System.IO; +using System.Linq; +using System.Runtime.InteropServices; + +namespace Ultima +{ + internal static class BwtDecompress + { + public static byte[] Decompress(byte[] buffer) + { + byte[] output = null; + + using (var reader = new BinaryReader(new MemoryStream(buffer))) + { + var header = reader.ReadUInt32(); + var len = 0u; + + var firstChar = reader.ReadByte(); + + Span table = new ushort[256 * 256]; + table = BuildTable(table, firstChar); + + var list = new byte[reader.BaseStream.Length - 4]; + var i = 0; + while (reader.BaseStream.Position < reader.BaseStream.Length) + { + var currentValue = firstChar; + var value = table[currentValue]; + if (currentValue > 0) + { + do + { + table[currentValue] = table[currentValue - 1]; + } while (--currentValue > 0); + } + + table[0] = value; + + list[i++] = (byte) value; + firstChar = reader.ReadByte(); + } + + output = InternalDecompress(list, len); + } + + return output; + } + + /// + static void MergeSort(Span span) + { + if (span.Length <= 1) + return; + + var list = span.ToArray().ToList(); + list.Sort(); + + int mid = span.Length / 2; + var left = span.Slice(0, mid); + var right = span.Slice(mid); + + MergeSort(left); + MergeSort(right); + + Merge(span, left, right); + } + + static void Merge(Span destination, Span left, Span right) + { + int i = 0, j = 0, k = 0; + + while (i < left.Length && j < right.Length) + { + if (left[i] <= right[j]) + { + destination[k++] = left[i++]; + } + else + { + destination[k++] = right[j++]; + } + } + + while (i < left.Length) + { + destination[k++] = left[i++]; + } + + while (j < right.Length) + { + destination[k++] = right[j++]; + } + } + + /// + + static Span BuildTable(Span table, byte startValue) + { + int index = 0; + byte firstByte = startValue; + byte secondByte = 0; + for (int i = 0; i < 256 * 256; i++) + { + var val = (ushort) (firstByte + (secondByte << 8)); + table[index++] = val; + + firstByte++; + if (firstByte == 0) + { + secondByte++; + } + } + + var list = table.ToArray().ToList(); + list.Sort(); + return list.ToArray(); + } + + static byte[] InternalDecompress(Span input, uint len) + { + Span symbolTable = stackalloc char[256]; + Span frequency = stackalloc char[256]; + Span partialInput = stackalloc int[256 * 3]; + partialInput.Clear(); + + for (var i = 0; i < 256; i++) + symbolTable[i] = (char) i; + + input.Slice(0, 1024).CopyTo(MemoryMarshal.AsBytes(partialInput)); + + var sum = 0; + for (var i = 0; i < 256; i++) + sum += partialInput[i]; + + if (len == 0) + { + len = (uint) sum; + } + + if (sum != len) + return Array.Empty(); + + var output = new byte[len]; + + var count = 0; + var nonZeroCount = 0; + + for (var i = 0; i < 256; i++) + { + if (partialInput[i] != 0) + nonZeroCount++; + } + + Frequency(partialInput, frequency); + + for (int i = 0, m = 0; i < nonZeroCount; ++i) + { + var freq = (byte) frequency[i]; + symbolTable[input[m + 1024]] = (char) freq; + partialInput[freq + 256] = m + 1; + m += partialInput[freq]; + partialInput[freq + 512] = m; + } + + var val = (byte) symbolTable[0]; + + if (len != 0) + { + do + { + ref var firstValRef = ref partialInput[val + 256]; + output[count] = val; + + if (firstValRef >= partialInput[val + 512]) + { + if (nonZeroCount-- > 0) + { + ShiftLeft(symbolTable, nonZeroCount); + val = (byte) symbolTable[0]; + } + } + else + { + var idx = (char) input[firstValRef + 1024]; + firstValRef++; + + if (idx != 0) + { + ShiftLeft(symbolTable, idx); + symbolTable[(byte) idx] = (char) val; + val = (byte) symbolTable[0]; + } + } + + count++; + } while (count < len); + } + + return output; + } + + static void Frequency(Span input, Span output) + { + Span tmp = stackalloc int[256]; + input.Slice(0, tmp.Length).CopyTo(tmp); + + for (var i = 0; i < 256; i++) + { + uint value = 0; + byte index = 0; + + for (var j = 0; j < 256; j++) + { + if (tmp[j] > value) + { + index = (byte) j; + value = (uint) tmp[j]; + } + } + + if (value == 0) + break; + + output[i] = (char) index; + tmp[index] = 0; + } + } + + static void ShiftLeft(Span input, int max) + { + for (var i = 0; i < max; ++i) + input[i] = input[i + 1]; + } + } +} \ No newline at end of file diff --git a/Razor/UltimaSDK/StackDataReader.cs b/Razor/UltimaSDK/StackDataReader.cs new file mode 100644 index 00000000..d04dcf7b --- /dev/null +++ b/Razor/UltimaSDK/StackDataReader.cs @@ -0,0 +1,495 @@ +#region license + +// Copyright (c) 2024, andreakarasho +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are met: +// 1. Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// 2. Redistributions in binary form must reproduce the above copyright +// notice, this list of conditions and the following disclaimer in the +// documentation and/or other materials provided with the distribution. +// 3. All advertising materials mentioning features or use of this software +// must display the following acknowledgement: +// This product includes software developed by andreakarasho - https://github.com/andreakarasho +// 4. Neither the name of the copyright holder nor the +// names of its contributors may be used to endorse or promote products +// derived from this software without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS ''AS IS'' AND ANY +// EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +// WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER BE LIABLE FOR ANY +// DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +// (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +// LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND +// ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +// SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +#endregion +using System; +using System.Buffers.Binary; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; +using System.Text; + +namespace Ultima +{ + unsafe ref struct StackDataReader + { + private readonly ReadOnlySpan _data; + public StackDataReader(ReadOnlySpan data) + { + _data = data; + Length = data.Length; + Position = 0; + } + + public int Position { get; private set; } + public long Length { get; } + public int Remaining => (int)(Length - Position); + + public IntPtr StartAddress => (IntPtr)Unsafe.AsPointer(ref GetPinnableReference()); + public IntPtr PositionAddress + { + get + { + return (IntPtr)((byte*)Unsafe.AsPointer(ref GetPinnableReference()) + Position); + } + } + + public byte this[int index] => _data[0]; + + public ReadOnlySpan Buffer => _data; + + + public ref byte GetPinnableReference() + { + return ref MemoryMarshal.GetReference(_data); + } + + public void Release() + { + // do nothing right now. + } + + public void Seek(long p) + { + Position = (int)p; + } + + public void Skip(int count) + { + Position += count; + } + + public byte ReadUInt8() + { + if (Position + 1 > Length) + { + return 0; + } + + return _data[Position++]; + } + + public sbyte ReadInt8() + { + if (Position + 1 > Length) + { + return 0; + } + + return (sbyte)_data[Position++]; + } + + public bool ReadBool() => ReadUInt8() != 0; + + public ushort ReadUInt16LE() + { + if (Position + 2 > Length) + { + return 0; + } + + BinaryPrimitives.TryReadUInt16LittleEndian(_data.Slice(Position), out ushort v); + + Skip(2); + + return v; + } + + public short ReadInt16LE() + { + if (Position + 2 > Length) + { + return 0; + } + + BinaryPrimitives.TryReadInt16LittleEndian(_data.Slice(Position), out short v); + + Skip(2); + + return v; + } + + public uint ReadUInt32LE() + { + if (Position + 4 > Length) + { + return 0; + } + + BinaryPrimitives.TryReadUInt32LittleEndian(_data.Slice(Position), out uint v); + + Skip(4); + + return v; + } + + public int ReadInt32LE() + { + if (Position + 4 > Length) + { + return 0; + } + + int v = BinaryPrimitives.ReadInt32LittleEndian(_data.Slice(Position)); + + Skip(4); + + return v; + } + + public ulong ReadUInt64LE() + { + if (Position + 8 > Length) + { + return 0; + } + + BinaryPrimitives.TryReadUInt64LittleEndian(_data.Slice(Position), out ulong v); + + Skip(8); + + return v; + } + + public long ReadInt64LE() + { + if (Position + 8 > Length) + { + return 0; + } + + BinaryPrimitives.TryReadInt64LittleEndian(_data.Slice(Position), out long v); + + Skip(8); + + return v; + } + + + + + + public ushort ReadUInt16BE() + { + if (Position + 2 > Length) + { + return 0; + } + + BinaryPrimitives.TryReadUInt16BigEndian(_data.Slice(Position), out ushort v); + + Skip(2); + + return v; + } + + public short ReadInt16BE() + { + if (Position + 2 > Length) + { + return 0; + } + + BinaryPrimitives.TryReadInt16BigEndian(_data.Slice(Position), out short v); + + Skip(2); + + return v; + } + + public uint ReadUInt32BE() + { + if (Position + 4 > Length) + { + return 0; + } + + BinaryPrimitives.TryReadUInt32BigEndian(_data.Slice(Position), out uint v); + + Skip(4); + + return v; + } + + public int ReadInt32BE() + { + if (Position + 4 > Length) + { + return 0; + } + + BinaryPrimitives.TryReadInt32BigEndian(_data.Slice(Position), out int v); + + Skip(4); + + return v; + } + + public ulong ReadUInt64BE() + { + if (Position + 8 > Length) + { + return 0; + } + + BinaryPrimitives.TryReadUInt64BigEndian(_data.Slice(Position), out ulong v); + + Skip(8); + + return v; + } + + public long ReadInt64BE() + { + if (Position + 8 > Length) + { + return 0; + } + + BinaryPrimitives.TryReadInt64BigEndian(_data.Slice(Position), out long v); + + Skip(8); + + return v; + } + + private string ReadRawString(int length, int sizeT, bool safe) + { + if (length == 0 || Position + sizeT > Length) + { + return string.Empty; + } + + bool fixedLength = length > 0; + int remaining = Remaining; + int size; + + if (fixedLength) + { + size = length * sizeT; + + if (size > remaining) + { + size = remaining; + } + } + else + { + size = remaining - (remaining & (sizeT - 1)); + } + + ReadOnlySpan slice = _data.Slice(Position, size); + + int index = GetIndexOfZero(slice, sizeT); + size = index < 0 ? size : index; + + string result; + + if (size <= 0) + { + result = String.Empty; + } + else + { + result = StringHelper.Cp1252ToString(slice.Slice(0, size)); + + if (safe) + { + Span buff = stackalloc char[256]; + ReadOnlySpan chars = result.AsSpan(); + + ValueStringBuilder sb = new ValueStringBuilder(buff); + + bool hasDoneAnyReplacements = false; + int last = 0; + for (int i = 0; i < chars.Length; i++) + { + if (!StringHelper.IsSafeChar(chars[i])) + { + hasDoneAnyReplacements = true; + sb.Append(chars.Slice(last, i - last)); + last = i + 1; // Skip the unsafe char + } + } + + if (hasDoneAnyReplacements) + { + // append the rest of the string + if (last < chars.Length) + { + sb.Append(chars.Slice(last, chars.Length - last)); + } + + result = sb.ToString(); + } + + sb.Dispose(); + } + } + + Position += Math.Max(size + (!fixedLength && index >= 0 ? sizeT : 0), length * sizeT); + + return result; + } + + public string ReadASCII(bool safe = false) + { + return ReadRawString(-1, 1, safe); + //return ReadString(StringHelper.Cp1252Encoding, -1, 1, safe); + } + + public string ReadASCII(int length, bool safe = false) + { + return ReadRawString(length, 1, safe); + + //return ReadString(StringHelper.Cp1252Encoding, length, 1, safe); + } + + public string ReadUnicodeBE(bool safe = false) + { + return ReadString(Encoding.BigEndianUnicode, -1, 2, safe); + } + + public string ReadUnicodeBE(int length, bool safe = false) + { + return ReadString(Encoding.BigEndianUnicode, length, 2, safe); + } + + public string ReadUnicodeLE(bool safe = false) + { + return ReadString(Encoding.Unicode, -1, 2, safe); + } + + public string ReadUnicodeLE(int length, bool safe = false) + { + return ReadString(Encoding.Unicode, length, 2, safe); + } + + public string ReadUTF8(bool safe = false) + { + return ReadString(Encoding.UTF8, -1, 1, safe); + } + + public string ReadUTF8(int length, bool safe = false) + { + return ReadString(Encoding.UTF8, length, 1, safe); + } + + public void Read(Span data, int offset, int count) + { + _data.Slice(Position + offset, count).CopyTo(data); + } + + // from modernuo <3 + private string ReadString(Encoding encoding, int length, int sizeT, bool safe) + { + if (length == 0 || Position + sizeT > Length) + { + return string.Empty; + } + + bool fixedLength = length > 0; + int remaining = Remaining; + int size; + + if (fixedLength) + { + size = length * sizeT; + + if (size > remaining) + { + size = remaining; + } + } + else + { + size = remaining - (remaining & (sizeT - 1)); + } + + ReadOnlySpan slice = _data.Slice(Position, size); + + int index = GetIndexOfZero(slice, sizeT); + size = index < 0 ? size : index; + + string result; + + fixed (byte* ptr = slice) + { + result = encoding.GetString(ptr, size); + } + + if (safe) + { + Span buff = stackalloc char[256]; + ReadOnlySpan chars = result.AsSpan(); + + ValueStringBuilder sb = new ValueStringBuilder(buff); + + bool hasDoneAnyReplacements = false; + int last = 0; + for (int i = 0; i < chars.Length; i++) + { + if (!StringHelper.IsSafeChar(chars[i])) + { + hasDoneAnyReplacements = true; + sb.Append(chars.Slice(last, i - last)); + last = i + 1; // Skip the unsafe char + } + } + + if (hasDoneAnyReplacements) + { + // append the rest of the string + if (last < chars.Length) + { + sb.Append(chars.Slice(last, chars.Length - last)); + } + + result = sb.ToString(); + } + + sb.Dispose(); + } + + Position += Math.Max(size + (!fixedLength && index >= 0 ? sizeT : 0), length * sizeT); + + return result; + } + + private static int GetIndexOfZero(ReadOnlySpan span, int sizeT) + { + switch (sizeT) + { + case 2: return MemoryMarshal.Cast(span).IndexOf('\0') * 2; + case 4: return MemoryMarshal.Cast(span).IndexOf((uint)0) * 4; + default: return span.IndexOf((byte)0); + } + } + } +} diff --git a/Razor/UltimaSDK/StringHelper.cs b/Razor/UltimaSDK/StringHelper.cs new file mode 100644 index 00000000..b0787c81 --- /dev/null +++ b/Razor/UltimaSDK/StringHelper.cs @@ -0,0 +1,379 @@ +#region license + +// Copyright (c) 2021, andreakarasho +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are met: +// 1. Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// 2. Redistributions in binary form must reproduce the above copyright +// notice, this list of conditions and the following disclaimer in the +// documentation and/or other materials provided with the distribution. +// 3. All advertising materials mentioning features or use of this software +// must display the following acknowledgement: +// This product includes software developed by andreakarasho - https://github.com/andreakarasho +// 4. Neither the name of the copyright holder nor the +// names of its contributors may be used to endorse or promote products +// derived from this software without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS ''AS IS'' AND ANY +// EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +// WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER BE LIABLE FOR ANY +// DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +// (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +// LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND +// ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +// SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +#endregion + +using System; +using System.Collections.Generic; +using System.Runtime.CompilerServices; + +namespace Ultima +{ + internal static class StringHelper + { + private static readonly char[] _dots = { '.', ',', ';', '!' }; + + public static IEnumerable StringToCp1252Bytes(string s, int length = -1) + { + length = length > 0 ? Math.Min(length, s.Length) : s.Length; + + for (int i = 0; i < length; i += char.IsSurrogatePair(s, i) ? 2 : 1) + { + yield return UnicodeToCp1252(char.ConvertToUtf32(s, i)); + } + } + + public static string Cp1252ToString(ReadOnlySpan strCp1252) + { + var sb = new ValueStringBuilder(strCp1252.Length); + + for (int i = 0; i < strCp1252.Length; ++i) + { + sb.Append(char.ConvertFromUtf32(Cp1252ToUnicode(strCp1252[i]))); + } + + var str = sb.ToString(); + + sb.Dispose(); + + return str; + } + + /// + /// Converts a unicode code point into a cp1252 code point + /// + private static byte UnicodeToCp1252(int codepoint) + { + if (codepoint >= 0x80 && codepoint <= 0x9f) + return (byte)'?'; + else if (codepoint <= 0xff) + return (byte)codepoint; + else + { + switch (codepoint) + { + case 0x20AC: return 128; //€ + case 0x201A: return 130; //‚ + case 0x0192: return 131; //ƒ + case 0x201E: return 132; //„ + case 0x2026: return 133; //… + case 0x2020: return 134; //† + case 0x2021: return 135; //‡ + case 0x02C6: return 136; //ˆ + case 0x2030: return 137; //‰ + case 0x0160: return 138; //Š + case 0x2039: return 139; //‹ + case 0x0152: return 140; //Œ + case 0x017D: return 142; //Ž + case 0x2018: return 145; //‘ + case 0x2019: return 146; //’ + case 0x201C: return 147; //“ + case 0x201D: return 148; //” + case 0x2022: return 149; //• + case 0x2013: return 150; //– + case 0x2014: return 151; //— + case 0x02DC: return 152; //˜ + case 0x2122: return 153; //™ + case 0x0161: return 154; //š + case 0x203A: return 155; //› + case 0x0153: return 156; //œ + case 0x017E: return 158; //ž + case 0x0178: return 159; //Ÿ + default: return (byte)'?'; + } + } + } + + /// + /// Converts a cp1252 code point into a unicode code point + /// + private static int Cp1252ToUnicode(byte codepoint) + { + switch (codepoint) + { + case 128: return 0x20AC; //€ + case 130: return 0x201A; //‚ + case 131: return 0x0192; //ƒ + case 132: return 0x201E; //„ + case 133: return 0x2026; //… + case 134: return 0x2020; //† + case 135: return 0x2021; //‡ + case 136: return 0x02C6; //ˆ + case 137: return 0x2030; //‰ + case 138: return 0x0160; //Š + case 139: return 0x2039; //‹ + case 140: return 0x0152; //Œ + case 142: return 0x017D; //Ž + case 145: return 0x2018; //‘ + case 146: return 0x2019; //’ + case 147: return 0x201C; //“ + case 148: return 0x201D; //” + case 149: return 0x2022; //• + case 150: return 0x2013; //– + case 151: return 0x2014; //— + case 152: return 0x02DC; //˜ + case 153: return 0x2122; //™ + case 154: return 0x0161; //š + case 155: return 0x203A; //› + case 156: return 0x0153; //œ + case 158: return 0x017E; //ž + case 159: return 0x0178; //Ÿ + default: return codepoint; + } + } + + public static string CapitalizeFirstCharacter(string str) + { + if (string.IsNullOrEmpty(str)) + { + return string.Empty; + } + + if (str.Length == 1) + { + return char.ToUpper(str[0]).ToString(); + } + + return char.ToUpper(str[0]) + str.Substring(1); + } + + + public static string CapitalizeAllWords(string str) + { + if (string.IsNullOrEmpty(str)) + { + return string.Empty; + } + + Span span = stackalloc char[str.Length]; + ValueStringBuilder sb = new ValueStringBuilder(span); + bool capitalizeNext = true; + + for (int i = 0; i < str.Length; i++) + { + sb.Append(capitalizeNext ? char.ToUpper(str[i]) : str[i]); + + if (!char.IsWhiteSpace(str[i])) + { + capitalizeNext = i + 1 < str.Length && char.IsWhiteSpace(str[i + 1]); + } + } + + string ss = sb.ToString(); + + sb.Dispose(); + + return ss; + } + + public static string CapitalizeWordsByLimitator(string str) + { + if (string.IsNullOrEmpty(str)) + { + return string.Empty; + } + + Span span = stackalloc char[str.Length]; + ValueStringBuilder sb = new ValueStringBuilder(span); + + bool capitalizeNext = true; + + for (int i = 0; i < str.Length; i++) + { + sb.Append(capitalizeNext ? char.ToUpper(str[i]) : str[i]); + capitalizeNext = false; + + for (int j = 0; j < _dots.Length; j++) + { + if (str[i] == _dots[j]) + { + capitalizeNext = true; + + break; + } + } + } + + string ss = sb.ToString(); + + sb.Dispose(); + + return ss; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static bool IsSafeChar(int c) + { + return c >= 0x20 && c < 0xFFFE; + } + + public static void AddSpaceBeforeCapital(string[] str, bool checkAcronyms = true) + { + for (int i = 0; i < str.Length; i++) + { + str[i] = AddSpaceBeforeCapital(str[i], checkAcronyms); + } + } + + public static string AddSpaceBeforeCapital(string str, bool checkAcronyms = true) + { + if (string.IsNullOrWhiteSpace(str)) + { + return ""; + } + + ValueStringBuilder sb = new ValueStringBuilder(str.Length * 2); + sb.Append(str[0]); + + for (int i = 1, len = str.Length - 1; i <= len; i++) + { + if (char.IsUpper(str[i])) + { + if (str[i - 1] != ' ' && !char.IsUpper(str[i - 1]) || checkAcronyms && char.IsUpper(str[i - 1]) && i < len && !char.IsUpper(str[i + 1])) + { + sb.Append(' '); + } + } + + sb.Append(str[i]); + } + + string s = sb.ToString(); + + sb.Dispose(); + + return s; + } + + public static string RemoveUpperLowerChars(string str, bool removelower = true) + { + if (string.IsNullOrWhiteSpace(str)) + { + return ""; + } + + Span span = stackalloc char[str.Length]; + ValueStringBuilder sb = new ValueStringBuilder(span); + + for (int i = 0; i < str.Length; i++) + { + if (char.IsUpper(str[i]) == removelower || str[i] == ' ') + { + sb.Append(str[i]); + } + } + + string ss = sb.ToString(); + + sb.Dispose(); + + return ss; + } + + public static string IntToAbbreviatedString(int num) + { + if (num > 999999) + { + return string.Format("{0}M+", num / 1000000); + } + + if (num > 999) + { + return string.Format("{0}K+", num / 1000); + } + + return num.ToString(); + } + + + public static string GetPluralAdjustedString(string str, bool plural = false) + { + if (str.Contains("%")) + { + string[] parts = str.Split(new[] { '%' }, System.StringSplitOptions.RemoveEmptyEntries); + + if (parts.Length < 2) + { + return str; + } + + Span span = stackalloc char[str.Length]; + ValueStringBuilder sb = new ValueStringBuilder(span); + + sb.Append(parts[0]); + + if (parts[1].Contains("/")) + { + string[] pluralparts = parts[1].Split('/'); + + if (plural) + { + sb.Append(pluralparts[0]); + } + else if (pluralparts.Length > 1) + { + sb.Append(pluralparts[1]); + } + } + else if (plural) + { + sb.Append(parts[1]); + } + + if (parts.Length == 3) + { + sb.Append(parts[2]); + } + + string ss = sb.ToString(); + + sb.Dispose(); + + return ss; + } + + return str; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static unsafe bool UnsafeCompare(char* buffer, string str, int length) + { + for (int i = 0; i < length && i < str.Length; ++i) + { + if (buffer[i] != str[i]) + { + return false; + } + } + + return true; + } + } +} \ No newline at end of file diff --git a/Razor/UltimaSDK/StringList.cs b/Razor/UltimaSDK/StringList.cs index db2dfd3d..198cc267 100644 --- a/Razor/UltimaSDK/StringList.cs +++ b/Razor/UltimaSDK/StringList.cs @@ -20,6 +20,7 @@ using System.Collections.Generic; using System.IO; using System.Text; +using Assistant; namespace Ultima { @@ -54,10 +55,23 @@ public StringList(string language) public StringList(string language, string path) { Language = language; + LoadEntry(path); } private void LoadEntry(string path) + { + if (Engine.ClientVersion.Major >= 7 && Engine.ClientVersion.Build >= 105) + { + LoadNewEntryFormat(path); + } + else + { + LoadOldEntryFormat(path); + } + } + + private void LoadOldEntryFormat(string path) { if (path == null) { @@ -95,6 +109,48 @@ private void LoadEntry(string path) } } } + + // Based on the implementation from Karasho @ ClassicUO + private void LoadNewEntryFormat(string path) + { + if (path == null) + { + Entries = new List(0); + return; + } + + Entries = new List(); + m_StringTable = new Dictionary(); + m_EntryTable = new Dictionary(); + + using (var fileStream = new FileStream(path, FileMode.Open, FileAccess.Read)) + { + int bytesRead; + var totalRead = 0; + var buf = new byte[fileStream.Length]; + while ((bytesRead = fileStream.Read(buf, totalRead, Math.Min(4096, buf.Length - totalRead))) > 0) + totalRead += bytesRead; + + var output = buf[3] == 0x8E ? BwtDecompress.Decompress(buf) : buf; + + var reader = new StackDataReader(output); + m_Header1 = reader.ReadInt32LE(); + m_Header2 = reader.ReadInt16LE(); + + while (reader.Remaining > 0) + { + var number = reader.ReadInt32LE(); + var flag = reader.ReadUInt8(); + var length = reader.ReadInt16LE(); + var text = string.Intern(reader.ReadUTF8(length)); + + m_StringTable[number] = text; + StringEntry se = new StringEntry(number, text, flag); + Entries.Add(se); + m_EntryTable[number] = se; + } + } + } /// /// Saves to FileName diff --git a/Razor/UltimaSDK/ValueStringBuilder.cs b/Razor/UltimaSDK/ValueStringBuilder.cs new file mode 100644 index 00000000..606c9489 --- /dev/null +++ b/Razor/UltimaSDK/ValueStringBuilder.cs @@ -0,0 +1,378 @@ +using System; +using System.Buffers; +using System.Diagnostics; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; + +namespace Ultima +{ + // https://github.com/Thealexbarney/LibHac/blob/master/src/LibHac/FsSystem/ValueStringBuilder.cs + internal ref struct ValueStringBuilder + { + private char[] _arrayToReturnToPool; + private Span _chars; + private int _pos; + + + // If this ctor is used, you cannot pass in stackalloc ROS for append/replace. + public ValueStringBuilder(ReadOnlySpan initialString) : this(initialString.Length) + { + Append(initialString); + } + + public ValueStringBuilder(ReadOnlySpan initialString, Span initialBuffer) : this(initialBuffer) + { + Append(initialString); + } + + public ValueStringBuilder(Span initialBuffer) + { + _arrayToReturnToPool = null; + _chars = initialBuffer; + _pos = 0; + } + + public ValueStringBuilder(int initialCapacity) + { + _arrayToReturnToPool = ArrayPool.Shared.Rent(initialCapacity); + _chars = _arrayToReturnToPool; + _pos = 0; + } + + public int Length + { + get => _pos; + set + { + Debug.Assert(value >= 0); + Debug.Assert(value <= _chars.Length); + _pos = value; + } + } + + public int Capacity => _chars.Length; + + public void EnsureCapacity(int capacity) + { + if (capacity > _chars.Length) + Grow(capacity - _chars.Length); + } + + /// + /// Get a pinnable reference to the builder. + /// Does not ensure there is a null char after + /// This overload is pattern matched in the C# 7.3+ compiler so you can omit + /// the explicit method call, and write eg "fixed (char* c = builder)" + /// + public ref char GetPinnableReference() + { + return ref MemoryMarshal.GetReference(_chars); + } + + /// + /// Get a pinnable reference to the builder. + /// + /// Ensures that the builder has a null char after + public ref char GetPinnableReference(bool terminate) + { + if (terminate) + { + EnsureCapacity(Length + 1); + _chars[Length] = '\0'; + } + return ref MemoryMarshal.GetReference(_chars); + } + + public ref char this[int index] + { + get + { + Debug.Assert(index < _pos); + return ref _chars[index]; + } + } + + public override string ToString() + { + string s = _chars.Slice(0, _pos).ToString(); + Dispose(); + return s; + } + + /// Returns the underlying storage of the builder. + public Span RawChars => _chars; + + /// + /// Returns a span around the contents of the builder. + /// + /// Ensures that the builder has a null char after + public ReadOnlySpan AsSpan(bool terminate) + { + if (terminate) + { + EnsureCapacity(Length + 1); + _chars[Length] = '\0'; + } + return _chars.Slice(0, _pos); + } + + public ReadOnlySpan AsSpan() => _chars.Slice(0, _pos); + public ReadOnlySpan AsSpan(int start) => _chars.Slice(start, _pos - start); + public ReadOnlySpan AsSpan(int start, int length) => _chars.Slice(start, length); + + public bool TryCopyTo(Span destination, out int charsWritten) + { + if (_chars.Slice(0, _pos).TryCopyTo(destination)) + { + charsWritten = _pos; + Dispose(); + return true; + } + else + { + charsWritten = 0; + Dispose(); + return false; + } + } + + public void Insert(int index, char value, int count) + { + if (_pos > _chars.Length - count) + { + Grow(count); + } + + int remaining = _pos - index; + _chars.Slice(index, remaining).CopyTo(_chars.Slice(index + count)); + _chars.Slice(index, count).Fill(value); + _pos += count; + } + + public void Insert(int index, ReadOnlySpan s) + { + int count = s.Length; + + if (_pos > (_chars.Length - count)) + { + Grow(count); + } + + int remaining = _pos - index; + _chars.Slice(index, remaining).CopyTo(_chars.Slice(index + count)); + s.CopyTo(_chars.Slice(index)); + _pos += count; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void Append(char c) + { + int pos = _pos; + if ((uint)pos < (uint)_chars.Length) + { + _chars[pos] = c; + _pos = pos + 1; + } + else + { + GrowAndAppend(c); + } + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void Append(string s) + { + int pos = _pos; + if (s.Length == 1 && (uint)pos < (uint)_chars.Length) // very common case, e.g. appending strings from NumberFormatInfo like separators, percent symbols, etc. + { + _chars[pos] = s[0]; + _pos = pos + 1; + } + else + { + AppendSlow(s); + } + } + + private void AppendSlow(string s) + { + int pos = _pos; + if (pos > _chars.Length - s.Length) + { + Grow(s.Length); + } + + s.AsSpan().CopyTo(_chars.Slice(pos)); + _pos += s.Length; + } + + public void Append(char c, int count) + { + if (_pos > _chars.Length - count) + { + Grow(count); + } + + Span dst = _chars.Slice(_pos, count); + for (int i = 0; i < dst.Length; i++) + { + dst[i] = c; + } + _pos += count; + } + + public void Append(ReadOnlySpan value) + { + int pos = _pos; + if (pos > _chars.Length - value.Length) + { + Grow(value.Length); + } + + value.CopyTo(_chars.Slice(_pos)); + _pos += value.Length; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public Span AppendSpan(int length) + { + int origPos = _pos; + if (origPos > _chars.Length - length) + { + Grow(length); + } + + _pos = origPos + length; + return _chars.Slice(origPos, length); + } + + public void Replace(ReadOnlySpan oldChars, ReadOnlySpan newChars) + { + Replace(oldChars, newChars, 0, _pos); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void Replace(ReadOnlySpan oldChars, ReadOnlySpan newChars, int startIndex, int count) + { + var slice = _chars.Slice(startIndex, count); + + var indexOf = slice.IndexOf(oldChars); + + if (indexOf == -1) + { + return; + } + + if (newChars.Length > oldChars.Length) + { + int i = 0; + + for (; i < oldChars.Length; ++i) + { + slice[indexOf + i] = newChars[i]; + } + + Insert(indexOf + i, newChars.Slice(i)); + } + else if (newChars.Length < oldChars.Length) + { + int i = 0; + + for (; i < newChars.Length; ++i) + { + slice[indexOf + i] = newChars[i]; + } + + Remove(indexOf + i, oldChars.Length - i); + } + else + { + newChars.CopyTo(slice.Slice(0, oldChars.Length)); + } + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void Replace(char oldChar, char newChar) + { + var slice = _chars; + + var indexOf = slice.IndexOf(oldChar); + if (indexOf == -1) + { + return; + } + + slice[indexOf] = newChar; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void Remove(int startIndex, int length) + { + if (length < 0) + { + throw new ArgumentOutOfRangeException(nameof(length)); + } + + if (startIndex < 0) + { + throw new ArgumentOutOfRangeException(nameof(startIndex)); + } + + if (length > _pos - startIndex) + { + throw new ArgumentOutOfRangeException(nameof(length)); + } + + if (startIndex == 0) + { + _chars = _chars.Slice(length); + } + else if (startIndex + length == _pos) + { + _chars = _chars.Slice(0, startIndex); + } + else + { + // Somewhere in the middle, this will be slow + _chars.Slice(startIndex + length).CopyTo(_chars.Slice(startIndex)); + } + + _pos -= length; + } + + [MethodImpl(MethodImplOptions.NoInlining)] + private void GrowAndAppend(char c) + { + Grow(1); + Append(c); + } + + [MethodImpl(MethodImplOptions.NoInlining)] + private void Grow(int requiredAdditionalCapacity) + { + Debug.Assert(requiredAdditionalCapacity > 0); + + char[] poolArray = ArrayPool.Shared.Rent(Math.Max(_pos + requiredAdditionalCapacity, _chars.Length * 2)); + + _chars.CopyTo(poolArray); + + char[] toReturn = _arrayToReturnToPool; + _chars = _arrayToReturnToPool = poolArray; + if (toReturn != null) + { + ArrayPool.Shared.Return(toReturn); + } + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void Dispose() + { + char[] toReturn = _arrayToReturnToPool; + this = default; // for safety, to avoid using pooled array if this instance is erroneously appended to again + if (toReturn != null) + { + ArrayPool.Shared.Return(toReturn); + } + } + } +} \ No newline at end of file diff --git a/Razor/packages.config b/Razor/packages.config new file mode 100644 index 00000000..1d538525 --- /dev/null +++ b/Razor/packages.config @@ -0,0 +1,7 @@ + + + + + + + \ No newline at end of file