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