diff --git a/Distribution/Data/Binary/Bounds.bin b/Distribution/Data/Binary/Bounds.bin index 45d9ae4a2..11431362e 100644 Binary files a/Distribution/Data/Binary/Bounds.bin and b/Distribution/Data/Binary/Bounds.bin differ diff --git a/Projects/Server.Tests/Tests/Gumps/TestGumps/StaticLayoutTestGump.cs b/Projects/Server.Tests/Tests/Gumps/TestGumps/StaticLayoutTestGump.cs index c80f88bb0..06679a48e 100644 --- a/Projects/Server.Tests/Tests/Gumps/TestGumps/StaticLayoutTestGump.cs +++ b/Projects/Server.Tests/Tests/Gumps/TestGumps/StaticLayoutTestGump.cs @@ -1,4 +1,3 @@ -using System; using Server.Gumps; namespace Server.Tests.Gumps; diff --git a/Projects/Server/Client/ArtData.cs b/Projects/Server/Client/ArtData.cs new file mode 100644 index 000000000..5294500ce --- /dev/null +++ b/Projects/Server/Client/ArtData.cs @@ -0,0 +1,185 @@ +/************************************************************************* + * ModernUO * + * Copyright 2019-2024 - ModernUO Development Team * + * Email: hi@modernuo.com * + * File: ArtData.cs * + * * + * This program is free software: you can redistribute it and/or modify * + * it under the terms of the GNU General Public License as published by * + * the Free Software Foundation, either version 3 of the License, or * + * (at your option) any later version. * + * * + * You should have received a copy of the GNU General Public License * + * along with this program. If not, see . * + *************************************************************************/ + +using System; +using System.Collections.Generic; +using System.IO; +using System.Runtime.InteropServices; + +namespace Server; + +public class ArtData : IDisposable +{ + private readonly FileStream _dataStream; + private readonly Dictionary _dataRanges; + + public bool IsInitialized => _dataRanges.Count > 0 && _dataStream != null; + + public ArtData() + { + var artPath = Core.FindDataFile("art.mul", false); + if (artPath != null) + { + var idxPath = Core.FindDataFile("artidx.mul", false); + if (idxPath != null) + { + _dataRanges = LoadMulRanges(idxPath); + _dataStream = new FileStream(artPath, FileMode.Open, FileAccess.Read, FileShare.Read); + return; + } + } + + artPath = Core.FindDataFile("artLegacyMUL.uop", false); + if (artPath != null) + { + + _dataStream = new FileStream(artPath, FileMode.Open, FileAccess.Read, FileShare.Read); + _dataRanges = UOPFiles.ReadUOPIndexes(_dataStream, ".tga", 0x14000, 5); + } + } + + public Rectangle2D GetStaticBounds(int index) + { + if (index is < 0 or > 0x10000) + { + return Rectangle2D.Empty; + } + + index += 16384; + + if (!_dataRanges.TryGetValue(index, out var entry)) + { + return Rectangle2D.Empty; + } + + Span buffer = stackalloc ushort[entry.Size / 2]; + _dataStream.Seek(entry.Offset, SeekOrigin.Begin); + _dataStream.Read(MemoryMarshal.AsBytes(buffer)); + + var width = buffer[2]; + var height = buffer[3]; + + if (width == 0 || height == 0) + { + return Rectangle2D.Empty; + } + + return GetBoundsFromRGBA1555Bitmap(width, height, buffer[4..]); + } + + private static Dictionary LoadMulRanges(string idxPath) + { + using var fs = new FileStream(idxPath, FileMode.Open, FileAccess.Read, FileShare.Read); + using var br = new BinaryReader(fs); + + var count = (int)(fs.Length / 12); + + var ranges = new Dictionary(count); + + for (var i = 0; i < count; i++) + { + var offset = br.ReadInt32(); + var length = br.ReadInt32(); + br.ReadInt32(); // Skip the Data field since we don't use it + + if (length > 0) + { + ranges[i] = new UOPEntry(offset, length); + } + } + + ranges.TrimExcess(); + return ranges; + } + + public static Rectangle2D GetBoundsFromRGBA1555Bitmap(int width, int height, ReadOnlySpan data) + { + ReadOnlySpan lookups = data[..height]; + data = data[height..]; + + int xMin = width; + int yMin = height; + int xMax = -1; + int yMax = -1; + + for (int y = 0; y < height; y++) + { + var i = lookups[y]; + var x = 0; + + while (true) + { + int startX = data[i++]; + var pixelCount = data[i++]; + + if (startX + pixelCount == 0 || startX > width) + { + break; + } + + x += startX; + for (var end = x + pixelCount; x < end; x++) + { + // Get the pixel value from the bitmap data + ushort pixel = data[i++]; + + // Check if the pixel is non-black (0 in 555 RGB is completely black) + if (pixel == 0) + { + continue; + } + + if (x < xMin) + { + xMin = x; + } + + if (x > xMax) + { + xMax = x; + } + + if (y < yMin) + { + yMin = y; + } + + if (y > yMax) + { + yMax = y; + } + } + } + } + + return xMax switch + { + // If no non-black pixels were found, return an empty rectangle + -1 => Rectangle2D.Empty, + _ => new Rectangle2D(xMin, yMin, xMax - xMin, yMax - yMin) + }; + } + + public void Dispose() + { + _dataStream?.Dispose(); + GC.SuppressFinalize(this); + } + + ~ArtData() + { + _dataStream?.Dispose(); + } +} diff --git a/Projects/Server/Client/UOPEntry.cs b/Projects/Server/Client/UOPEntry.cs new file mode 100644 index 000000000..a6e9a010e --- /dev/null +++ b/Projects/Server/Client/UOPEntry.cs @@ -0,0 +1,32 @@ +/************************************************************************* + * ModernUO * + * Copyright 2019-2024 - ModernUO Development Team * + * Email: hi@modernuo.com * + * File: UOPEntry.cs * + * * + * This program is free software: you can redistribute it and/or modify * + * it under the terms of the GNU General Public License as published by * + * the Free Software Foundation, either version 3 of the License, or * + * (at your option) any later version. * + * * + * You should have received a copy of the GNU General Public License * + * along with this program. If not, see . * + *************************************************************************/ + +namespace Server; + +public struct UOPEntry +{ + public readonly long Offset; + public readonly int Size; + public bool Compressed; + public int CompressedSize; + public int Extra; + + public UOPEntry(long offset, int length) + { + Offset = offset; + Size = length; + Extra = 0; + } +} diff --git a/Projects/Server/Client/UOPFiles.cs b/Projects/Server/Client/UOPFiles.cs new file mode 100644 index 000000000..e52d5ed86 --- /dev/null +++ b/Projects/Server/Client/UOPFiles.cs @@ -0,0 +1,238 @@ +/************************************************************************* + * ModernUO * + * Copyright 2019-2024 - ModernUO Development Team * + * Email: hi@modernuo.com * + * File: UOPFiles.cs * + * * + * This program is free software: you can redistribute it and/or modify * + * it under the terms of the GNU General Public License as published by * + * the Free Software Foundation, either version 3 of the License, or * + * (at your option) any later version. * + * * + * You should have received a copy of the GNU General Public License * + * along with this program. If not, see . * + *************************************************************************/ + +using System; +using System.Collections.Generic; +using System.IO; + +namespace Server; + +public static class UOPFiles +{ + public static Dictionary ReadUOPIndexes( + FileStream stream, string fileExt, int entryCount = 0x14000, int expectedVersion = -1, int precision = 8 + ) + { + var reader = new BinaryReader(stream); + var length = (int)stream.Length; + + // MYP\0 + if (reader.ReadUInt32() != 0x50594D) + { + throw new FileLoadException($"Error loading file {stream.Name}. The file is not a UOP file."); + } + + var version = reader.ReadInt32(); + if (expectedVersion > -1 && version != expectedVersion) + { + throw new FileLoadException($"Error loading file {stream.Name}. Expected version {expectedVersion}."); + } + + // Signature + if (reader.ReadUInt32() != 0xFD23EC43) + { + throw new FileLoadException($"Error loading file {stream.Name}. Invalid signature."); + } + + var hashes = GenerateHashes(stream.Name, fileExt, entryCount, precision); + + var nextBlock = reader.ReadInt64(); + + var entries = new Dictionary(0x10000); + + do + { + stream.Seek(nextBlock, SeekOrigin.Begin); + var fileCount = reader.ReadInt32(); + nextBlock = reader.ReadInt64(); + + for (var i = 0; i < fileCount; ++i) + { + var offset = reader.ReadInt64(); + var headerLength = reader.ReadInt32(); + var compressedLength = reader.ReadInt32(); + var decompressedLength = reader.ReadInt32(); + var fileNameHash = reader.ReadUInt64(); + + reader.ReadUInt32(); // Adler32 + + var compressed = reader.ReadInt16() == 1; + + if (offset != 0 && hashes.TryGetValue(fileNameHash, out var fileIndex) && compressedLength > 0 && + decompressedLength > 0) + { + entries[fileIndex] = new UOPEntry(offset + headerLength, decompressedLength) + { + Compressed = compressed, + CompressedSize = compressedLength + }; + } + } + } + while (nextBlock != 0 && nextBlock < length); + + entries.TrimExcess(); + return entries; + } + + private static Dictionary GenerateHashes(string filePath, string ext, int entryCount, int precision = 8) + { + var hashes = new Dictionary(); + Span buffer = stackalloc char[precision]; + var formatter = $"D{precision}".AsSpan(); + + var root = $"build/{Path.GetFileNameWithoutExtension(filePath).ToLowerInvariant()}"; + + for (var i = 0; i < entryCount; i++) + { + i.TryFormat(buffer, out _, formatter); + hashes[HashLittle2($"{root}/{buffer}{ext}")] = i; + } + + return hashes; + } + + public static ulong HashLittle2(ReadOnlySpan s) + { + var length = s.Length; + + uint b, c; + var a = b = c = 0xDEADBEEF + (uint)length; + + var k = 0; + + while (length > 12) + { + a += s[k]; + a += (uint)s[k + 1] << 8; + a += (uint)s[k + 2] << 16; + a += (uint)s[k + 3] << 24; + b += s[k + 4]; + b += (uint)s[k + 5] << 8; + b += (uint)s[k + 6] << 16; + b += (uint)s[k + 7] << 24; + c += s[k + 8]; + c += (uint)s[k + 9] << 8; + c += (uint)s[k + 10] << 16; + c += (uint)s[k + 11] << 24; + + a -= c; + a ^= (c << 4) | (c >> 28); + c += b; + b -= a; + b ^= (a << 6) | (a >> 26); + a += c; + c -= b; + c ^= (b << 8) | (b >> 24); + b += a; + a -= c; + a ^= (c << 16) | (c >> 16); + c += b; + b -= a; + b ^= (a << 19) | (a >> 13); + a += c; + c -= b; + c ^= (b << 4) | (b >> 28); + b += a; + + length -= 12; + k += 12; + } + + if (length != 0) + { + switch (length) + { + case 12: + { + c += (uint)s[k + 11] << 24; + goto case 11; + } + case 11: + { + c += (uint)s[k + 10] << 16; + goto case 10; + } + case 10: + { + c += (uint)s[k + 9] << 8; + goto case 9; + } + case 9: + { + c += s[k + 8]; + goto case 8; + } + case 8: + { + b += (uint)s[k + 7] << 24; + goto case 7; + } + case 7: + { + b += (uint)s[k + 6] << 16; + goto case 6; + } + case 6: + { + b += (uint)s[k + 5] << 8; + goto case 5; + } + case 5: + { + b += s[k + 4]; + goto case 4; + } + case 4: + { + a += (uint)s[k + 3] << 24; + goto case 3; + } + case 3: + { + a += (uint)s[k + 2] << 16; + goto case 2; + } + case 2: + { + a += (uint)s[k + 1] << 8; + goto case 1; + } + case 1: + { + a += s[k]; + break; + } + } + + c ^= b; + c -= (b << 14) | (b >> 18); + a ^= c; + a -= (c << 11) | (c >> 21); + b ^= a; + b -= (a << 25) | (a >> 7); + c ^= b; + c -= (b << 16) | (b >> 16); + a ^= c; + a -= (c << 4) | (c >> 28); + b ^= a; + b -= (a << 14) | (a >> 18); + c ^= b; + c -= (b << 24) | (b >> 8); + } + + return ((ulong)b << 32) | c; + } +} diff --git a/Projects/Server/Items/ItemBounds.cs b/Projects/Server/Items/ItemBounds.cs index 92758b37e..4b957b80e 100644 --- a/Projects/Server/Items/ItemBounds.cs +++ b/Projects/Server/Items/ItemBounds.cs @@ -15,6 +15,7 @@ using System; using System.IO; +using System.Threading; using Server.Logging; namespace Server; @@ -22,24 +23,133 @@ namespace Server; public static class ItemBounds { private static readonly ILogger logger = LogFactory.GetLogger(typeof(ItemBounds)); + private static readonly string _pathToBounds = Path.Combine(Core.BaseDirectory, "Data", "Binary", "Bounds.bin"); + private static bool _isGenerating; + + public static void Configure() + { + CommandSystem.Register("GenBounds", AccessLevel.Developer, GenBounds_OnCommand); + } + + [Usage("GenBounds")] + [Description("Asynchronously generates the bounds.bin file from art.mul/artidx.mul or artLegacyMUL.uop to determine container boundaries.")] + private static void GenBounds_OnCommand(CommandEventArgs e) + { + GenerateBoundsFileAsync(e.Mobile); + } static ItemBounds() { Table = new Rectangle2D[TileData.ItemTable.Length]; - if (!File.Exists("Data/Binary/Bounds.bin")) + if (!File.Exists(_pathToBounds)) { - logger.Error("Data/Binary/Bounds.bin does not exist"); + logger.Information("Generating {BoundsFilePath}...", "Bounds.bin"); + try + { + GenerateBoundsFile(); + logger.Information("Generated {BoundsFilePath} successfully.", "Bounds.bin"); + } + catch (Exception ex) + { + logger.Error(ex, "Failed to generate {BoundsFilePath}", "Bounds.bin"); + } + return; } - using var fs = new FileStream( - "Data/Binary/Bounds.bin", - FileMode.Open, - FileAccess.Read, - FileShare.Read + GenerateTable(); + } + + public static Rectangle2D[] Table { get; private set; } + + private static void GenerateBoundsFileAsync(Mobile m) + { + if (_isGenerating) + { + m?.SendMessage("Bounds file is already being generated."); + return; + } + + _isGenerating = true; + ThreadPool.QueueUserWorkItem( + state => + { + var from = state as Mobile; + logger.Information("Generating {BoundsFilePath}...", "Bounds.bin"); + if (from != null) + { + Core.LoopContext.Post(() => from?.SendMessage("Generating bounds file...")); + } + + try + { + var table = GenerateBoundsFile(); + Core.LoopContext.Post(() => Table = table); + } + catch (Exception ex) + { + if (from != null) + { + Core.LoopContext.Post(() => + { + from?.SendMessage("Failed to generate bounds file:"); + from?.SendMessage(ex.Message); + } + ); + } + + logger.Error(ex, "Failed to generate {BoundsFilePath}", "Bounds.bin"); + return; + } + + if (from != null) + { + Core.LoopContext.Post( + () => from?.SendMessage( + $"Bounds file saved to {Path.GetRelativePath(Core.BaseDirectory, _pathToBounds)}." + ) + ); + } + + logger.Information("Generated {BoundsFilePath} successfully.", "Bounds.bin"); + _isGenerating = false; + }, + m ); - var bin = new BinaryReader(fs); + } + + private static Rectangle2D[] GenerateBoundsFile() + { + var table = new Rectangle2D[TileData.ItemTable.Length]; + using var artData = new ArtData(); + if (!artData.IsInitialized) + { + throw new FileNotFoundException("Unable to load art.mul/artidx.mul or artLegacyMUL.uop"); + } + + using var fs = new FileStream(_pathToBounds, FileMode.Create, FileAccess.Write); + using var bw = new BinaryWriter(fs); + + for (var i = 0; i < table.Length; i++) + { + var bounds = artData.GetStaticBounds(i); + + bw.Write((short)bounds.X); + bw.Write((short)bounds.Y); + bw.Write((short)(bounds.X + bounds.Width + 1)); + bw.Write((short)(bounds.Y + bounds.Height + 1)); + + table[i] = bounds; + } + + return table; + } + + private static void GenerateTable() + { + using var fs = new FileStream(_pathToBounds, FileMode.Open, FileAccess.Read, FileShare.Read); + using var bin = new BinaryReader(fs); var count = Math.Min(Table.Length, (int)(fs.Length / 8)); @@ -50,11 +160,7 @@ public static class ItemBounds int xMax = bin.ReadInt16(); int yMax = bin.ReadInt16(); - Table[i].Set(xMin, yMin, xMax - xMin + 1, yMax - yMin + 1); + Table[i].Set(xMin, yMin, xMax - xMin, yMax - yMin); } - - bin.Close(); } - - public static Rectangle2D[] Table { get; } } diff --git a/Projects/Server/MultiData.cs b/Projects/Server/MultiData.cs index c1d7c8acc..e1f456722 100644 --- a/Projects/Server/MultiData.cs +++ b/Projects/Server/MultiData.cs @@ -18,6 +18,7 @@ using System.Buffers; using System.Collections.Generic; using System.IO; using System.IO.Compression; +using Server.Buffers; namespace Server; @@ -42,7 +43,7 @@ public static class MultiData LoadMul(postHSMulFormat); } - private static Dictionary _components = new(); + private static readonly Dictionary _components = new(); public static MultiComponentList GetComponents(int multiID) => _components.TryGetValue(multiID & 0x3FFF, out var mcl) ? mcl : MultiComponentList.Empty; @@ -50,114 +51,78 @@ public static class MultiData private static void LoadUOP(string path) { using var stream = new FileStream(path, FileMode.Open, FileAccess.Read, FileShare.Read); - var streamReader = new BinaryReader(stream); - // Head Information Start - if (streamReader.ReadInt32() != 0x0050594D) // Not a UOP File + // TODO: Find out if housing.bin is needed and read that. + var uopEntries = UOPFiles.ReadUOPIndexes(stream, ".bin", 0x10000, 4, 6); + + byte[] compressionBuffer = STArrayPool.Shared.Rent(0x10000); + var buffer = STArrayPool.Shared.Rent(0x10000); + + foreach (var (i, entry) in uopEntries) { - return; - } + stream.Seek(entry.Offset, SeekOrigin.Begin); - if (streamReader.ReadInt32() > 5) // Bad Version - { - return; - } + Span data; - UOPHash.BuildChunkIDs(out var chunkIds); - - streamReader.ReadUInt32(); // format timestamp? 0xFD23EC43 - var startAddress = streamReader.ReadInt64(); - - stream.Seek(startAddress, SeekOrigin.Begin); // End of head block - - long nextBlock; - - do - { - var blockFileCount = streamReader.ReadInt32(); - nextBlock = streamReader.ReadInt64(); - - var index = 0; - - do + if (entry.Compressed) { - var offset = streamReader.ReadInt64(); - - var headerSize = streamReader.ReadInt32(); // header length - var compressedSize = streamReader.ReadInt32(); // compressed size - var decompressedSize = streamReader.ReadInt32(); // decompressed size - - var filehash = streamReader.ReadUInt64(); // filename hash (HashLittle2) - streamReader.ReadUInt32(); - var compressionMethod = streamReader.ReadInt16(); // compression method (0 = none, 1 = zlib) - - index++; - - if (offset == 0 || decompressedSize == 0 || filehash == 0x126D1E99DDEDEE0A) // Exclude housing.bin + if (stream.Read(buffer.AsSpan( 0, entry.CompressedSize)) != entry.CompressedSize) { - continue; + throw new FileLoadException($"Error loading file {stream.Name}."); } - chunkIds.TryGetValue(filehash, out var chunkID); - - var position = stream.Position; // save current position - - stream.Seek(offset + headerSize, SeekOrigin.Begin); - - Span sourceData = GC.AllocateUninitializedArray(compressedSize); - - if (stream.Read(sourceData) != compressedSize) + var decompressedSize = entry.Size; + if (Zlib.Unpack(compressionBuffer, ref decompressedSize, buffer, entry.CompressedSize) != ZlibError.Okay + || decompressedSize != entry.Size) { - continue; + throw new FileLoadException($"Error loading file {stream.Name}. Failed to unpack entry {i}."); } - Span data; + data = compressionBuffer.AsSpan(0, decompressedSize); + } + else + { + data = buffer.AsSpan(0, entry.Size); + } - if (compressionMethod == 1) + var tileList = new List(); + + var reader = new SpanReader(data); + + reader.Seek(4, SeekOrigin.Begin); // Skip the first 4 bytes + var count = reader.ReadUInt32LE(); + + for (uint t = 0; t < count; t++) + { + var itemId = reader.ReadUInt16LE(); + var x = reader.ReadInt16LE(); + var y = reader.ReadInt16LE(); + var z = reader.ReadInt16LE(); + var flagValue = reader.ReadUInt16LE(); + + var tileFlag = flagValue switch { - data = GC.AllocateUninitializedArray(decompressedSize); - Zlib.Unpack(data, ref decompressedSize, sourceData, compressedSize); - } - else - { - data = sourceData; - } + 1 => TileFlag.None, + 257 => TileFlag.Generic, + _ => TileFlag.Background // 0 + }; - var tileList = new List(); + var clilocsCount = reader.ReadUInt32LE(); + var skip = (int)Math.Min(clilocsCount, int.MaxValue) * 4; // bypass binary block + reader.Seek(skip, SeekOrigin.Current); - var reader = new SpanReader(data); - reader.Seek(4, SeekOrigin.Begin); - var count = reader.ReadUInt32LE(); + tileList.Add(new MultiTileEntry(itemId, x, y, z, tileFlag)); + } - for (uint i = 0; i < count; i++) - { - var itemId = reader.ReadUInt16LE(); - var x = reader.ReadInt16LE(); - var y = reader.ReadInt16LE(); - var z = reader.ReadInt16LE(); - var flagValue = reader.ReadUInt16LE(); + _components[i] = new MultiComponentList(tileList); + } - var tileFlag = flagValue switch - { - 1 => TileFlag.None, - 257 => TileFlag.Generic, - _ => TileFlag.Background // 0 - }; + STArrayPool.Shared.Return(buffer); - var clilocsCount = reader.ReadUInt32LE(); - var skip = (int)Math.Min(clilocsCount, int.MaxValue) * 4; // bypass binary block - reader.Seek(skip, SeekOrigin.Current); - - tileList.Add(new MultiTileEntry(itemId, x, y, z, tileFlag)); - } - - _components[chunkID] = new MultiComponentList(tileList); - - stream.Seek(position, SeekOrigin.Begin); // back to position - } while (index < blockFileCount); - } while (stream.Seek(nextBlock, SeekOrigin.Begin) != 0); - - streamReader.Close(); + if (compressionBuffer != null) + { + STArrayPool.Shared.Return(compressionBuffer); + } } private static void LoadMul(bool postHSMulFormat) @@ -769,150 +734,3 @@ public sealed class MultiComponentList } } } - -public static class UOPHash -{ - public static void BuildChunkIDs(out Dictionary chunkIds) - { - const int maxId = 0x10000; - - chunkIds = new Dictionary(); - - for (var i = 0; i < maxId; ++i) - { - chunkIds[HashLittle2($"build/multicollection/{i:000000}.bin")] = i; - } - } - - public static ulong HashLittle2(ReadOnlySpan s) - { - var length = s.Length; - - uint b, c; - var a = b = c = 0xDEADBEEF + (uint)length; - - var k = 0; - - while (length > 12) - { - a += s[k]; - a += (uint)s[k + 1] << 8; - a += (uint)s[k + 2] << 16; - a += (uint)s[k + 3] << 24; - b += s[k + 4]; - b += (uint)s[k + 5] << 8; - b += (uint)s[k + 6] << 16; - b += (uint)s[k + 7] << 24; - c += s[k + 8]; - c += (uint)s[k + 9] << 8; - c += (uint)s[k + 10] << 16; - c += (uint)s[k + 11] << 24; - - a -= c; - a ^= (c << 4) | (c >> 28); - c += b; - b -= a; - b ^= (a << 6) | (a >> 26); - a += c; - c -= b; - c ^= (b << 8) | (b >> 24); - b += a; - a -= c; - a ^= (c << 16) | (c >> 16); - c += b; - b -= a; - b ^= (a << 19) | (a >> 13); - a += c; - c -= b; - c ^= (b << 4) | (b >> 28); - b += a; - - length -= 12; - k += 12; - } - - if (length != 0) - { - switch (length) - { - case 12: - { - c += (uint)s[k + 11] << 24; - goto case 11; - } - case 11: - { - c += (uint)s[k + 10] << 16; - goto case 10; - } - case 10: - { - c += (uint)s[k + 9] << 8; - goto case 9; - } - case 9: - { - c += s[k + 8]; - goto case 8; - } - case 8: - { - b += (uint)s[k + 7] << 24; - goto case 7; - } - case 7: - { - b += (uint)s[k + 6] << 16; - goto case 6; - } - case 6: - { - b += (uint)s[k + 5] << 8; - goto case 5; - } - case 5: - { - b += s[k + 4]; - goto case 4; - } - case 4: - { - a += (uint)s[k + 3] << 24; - goto case 3; - } - case 3: - { - a += (uint)s[k + 2] << 16; - goto case 2; - } - case 2: - { - a += (uint)s[k + 1] << 8; - goto case 1; - } - case 1: - { - a += s[k]; - break; - } - } - - c ^= b; - c -= (b << 14) | (b >> 18); - a ^= c; - a -= (c << 11) | (c >> 21); - b ^= a; - b -= (a << 25) | (a >> 7); - c ^= b; - c -= (b << 16) | (b >> 16); - a ^= c; - a -= (c << 4) | (c >> 28); - b ^= a; - b -= (a << 14) | (a >> 18); - c ^= b; - c -= (b << 24) | (b >> 8); - } - - return ((ulong)b << 32) | c; - } -} diff --git a/Projects/Server/TileMatrix/TileMatrix.cs b/Projects/Server/TileMatrix/TileMatrix.cs index 95f9ad4d3..635af16e5 100644 --- a/Projects/Server/TileMatrix/TileMatrix.cs +++ b/Projects/Server/TileMatrix/TileMatrix.cs @@ -33,7 +33,7 @@ public class TileMatrix private readonly LandTile[][][] _landTiles; private readonly LandTile[] _invalidLandBlock; private readonly StaticTile[][][] _emptyStaticBlock; - private readonly UOPIndex _mapIndex; + private readonly UOPEntry[] _uopMapEntries; private readonly int _fileIndex; private readonly Map _map; private readonly int[][] _staticPatches; @@ -104,7 +104,13 @@ public class TileMatrix if (mapPath != null) { MapStream = new FileStream(mapPath, FileMode.Open, FileAccess.Read, FileShare.Read); - _mapIndex = new UOPIndex(MapStream); + + var uopEntries = UOPFiles.ReadUOPIndexes(MapStream, ".dat", 0x14000, 5); + + _uopMapEntries = GC.AllocateUninitializedArray(uopEntries.Count); + uopEntries.Values.CopyTo(_uopMapEntries, 0); + + ConvertToMapEntries(MapStream); } else { @@ -390,7 +396,7 @@ public class TileMatrix private DateTime m_NextStaticWarning; private DateTime m_NextLandWarning; - public void Force() + public static void Force() { if (AssemblyHandler.Assemblies?.Length > 0) { @@ -404,11 +410,11 @@ public class TileMatrix { try { - var offset = (x * BlockHeight + y) * 196 + 4; + long offset = (x * BlockHeight + y) * 196 + 4; - if (_mapIndex != null) + if (_uopMapEntries != null) { - offset = _mapIndex.Lookup(offset); + offset = FindOffset(offset); } MapStream.Seek(offset, SeekOrigin.Begin); @@ -434,9 +440,45 @@ public class TileMatrix } } + public long FindOffset(long offset) + { + var total = 0; + + for (var i = 0; i < _uopMapEntries.Length; ++i) + { + var entry = _uopMapEntries[i]; + var newTotal = total + entry.Size; + + if (offset < newTotal) + { + return entry.Offset + (offset - total); + } + + total = newTotal; + } + + return -1; + } + + private void ConvertToMapEntries(FileStream stream) + { + // Sorting by offset to make seeking faster + Array.Sort(_uopMapEntries, (a, b) => a.Offset.CompareTo(b.Offset)); + + var reader = new BinaryReader(stream); + + for (var i = 0; i < _uopMapEntries.Length; ++i) + { + var entry = _uopMapEntries[i]; + stream.Seek(entry.Offset, SeekOrigin.Begin); + _uopMapEntries[i].Extra = reader.ReadInt32(); // order + } + + Array.Sort(_uopMapEntries, (a, b) => a.Extra.CompareTo(b.Extra)); + } + public void Dispose() { - _mapIndex?.Close(); MapStream?.Close(); DataStream?.Close(); IndexReader?.Close(); @@ -525,122 +567,3 @@ public struct StaticTile m_Hue = hue; } } - -public class UOPIndex -{ - private class UOPEntry : IComparable - { - public int m_Offset; - public readonly int m_Length; - public int m_Order; - - public UOPEntry(int offset, int length) - { - m_Offset = offset; - m_Length = length; - m_Order = 0; - } - - public int CompareTo(UOPEntry other) - { - return m_Order.CompareTo(other.m_Order); - } - } - - private class OffsetComparer : IComparer - { - public static readonly IComparer Instance = new OffsetComparer(); - - public int Compare(UOPEntry x, UOPEntry y) => x!.m_Offset.CompareTo(y!.m_Offset); - } - - private readonly BinaryReader m_Reader; - private readonly int m_Length; - private readonly UOPEntry[] m_Entries; - - public int Version { get; } - - public UOPIndex(FileStream stream) - { - m_Reader = new BinaryReader(stream); - m_Length = (int)stream.Length; - - if (m_Reader.ReadInt32() != 0x50594D) - { - throw new ArgumentException("Invalid UOP file."); - } - - Version = m_Reader.ReadInt32(); - m_Reader.ReadInt32(); - var nextTable = m_Reader.ReadInt32(); - - var entries = new List(); - - do - { - stream.Seek(nextTable, SeekOrigin.Begin); - var count = m_Reader.ReadInt32(); - nextTable = m_Reader.ReadInt32(); - m_Reader.ReadInt32(); - - for (var i = 0; i < count; ++i) - { - var offset = m_Reader.ReadInt32(); - - if (offset == 0) - { - stream.Seek(30, SeekOrigin.Current); - continue; - } - - m_Reader.ReadInt64(); - var length = m_Reader.ReadInt32(); - - entries.Add(new UOPEntry(offset, length)); - - stream.Seek(18, SeekOrigin.Current); - } - } - while (nextTable != 0 && nextTable < m_Length); - - entries.Sort(OffsetComparer.Instance); - - for (var i = 0; i < entries.Count; ++i) - { - stream.Seek(entries[i].m_Offset + 2, SeekOrigin.Begin); - - int dataOffset = m_Reader.ReadInt16(); - entries[i].m_Offset += 4 + dataOffset; - - stream.Seek(dataOffset, SeekOrigin.Current); - entries[i].m_Order = m_Reader.ReadInt32(); - } - - entries.Sort(); - m_Entries = entries.ToArray(); - } - - public int Lookup(int offset) - { - var total = 0; - - for (var i = 0; i < m_Entries.Length; ++i) - { - var newTotal = total + m_Entries[i].m_Length; - - if (offset < newTotal) - { - return m_Entries[i].m_Offset + (offset - total); - } - - total = newTotal; - } - - return m_Length; - } - - public void Close() - { - m_Reader.Close(); - } -} diff --git a/Projects/Server/TileMatrix/TileMatrixLoader.cs b/Projects/Server/TileMatrix/TileMatrixLoader.cs index 627ee8ca2..54a1095c4 100644 --- a/Projects/Server/TileMatrix/TileMatrixLoader.cs +++ b/Projects/Server/TileMatrix/TileMatrixLoader.cs @@ -32,10 +32,7 @@ internal static class TileMatrixLoader try { - foreach (var m in Map.AllMaps) - { - m.Tiles.Force(); // Forces the map file stream references to load - } + TileMatrix.Force(); // Forces the tile matrix file stream references to load } catch (Exception ex) { diff --git a/docs/commands/commands.7z b/docs/commands/commands.7z index 6ea931ecb..00e2f554f 100644 Binary files a/docs/commands/commands.7z and b/docs/commands/commands.7z differ