feat: Cleans up UOP file handling. Adds automatic bounds.bin generation (#1744)

### Summary
* Unifies reading UOP File indexes
* Adds ArtData file reader to get graphic bounds
* Automatically generates Bounds.bin from art file if missing
* Adds [GenBounds command to regenerate the bounds.bin file. Useful for when the art file changes.
This commit is contained in:
kaczy93 2024-05-02 05:02:06 +02:00 committed by GitHub
parent db0621b884
commit dc118d836f
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
10 changed files with 680 additions and 382 deletions

Binary file not shown.

View file

@ -1,4 +1,3 @@
using System;
using Server.Gumps;
namespace Server.Tests.Gumps;

View file

@ -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 <http://www.gnu.org/licenses/>. *
*************************************************************************/
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<int, UOPEntry> _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<ushort> 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<int, UOPEntry> 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<int, UOPEntry>(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<ushort> data)
{
ReadOnlySpan<ushort> 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();
}
}

View file

@ -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 <http://www.gnu.org/licenses/>. *
*************************************************************************/
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;
}
}

View file

@ -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 <http://www.gnu.org/licenses/>. *
*************************************************************************/
using System;
using System.Collections.Generic;
using System.IO;
namespace Server;
public static class UOPFiles
{
public static Dictionary<int, UOPEntry> 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<int, UOPEntry>(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<ulong, int> GenerateHashes(string filePath, string ext, int entryCount, int precision = 8)
{
var hashes = new Dictionary<ulong, int>();
Span<char> 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<char> 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;
}
}

View file

@ -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; }
}

View file

@ -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<int, MultiComponentList> _components = new();
private static readonly Dictionary<int, MultiComponentList> _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<byte>.Shared.Rent(0x10000);
var buffer = STArrayPool<byte>.Shared.Rent(0x10000);
foreach (var (i, entry) in uopEntries)
{
return;
}
stream.Seek(entry.Offset, SeekOrigin.Begin);
if (streamReader.ReadInt32() > 5) // Bad Version
{
return;
}
Span<byte> 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<byte> sourceData = GC.AllocateUninitializedArray<byte>(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<byte> data;
data = compressionBuffer.AsSpan(0, decompressedSize);
}
else
{
data = buffer.AsSpan(0, entry.Size);
}
if (compressionMethod == 1)
var tileList = new List<MultiTileEntry>();
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<byte>(decompressedSize);
Zlib.Unpack(data, ref decompressedSize, sourceData, compressedSize);
}
else
{
data = sourceData;
}
1 => TileFlag.None,
257 => TileFlag.Generic,
_ => TileFlag.Background // 0
};
var tileList = new List<MultiTileEntry>();
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<byte>.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<byte>.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<ulong, int> chunkIds)
{
const int maxId = 0x10000;
chunkIds = new Dictionary<ulong, int>();
for (var i = 0; i < maxId; ++i)
{
chunkIds[HashLittle2($"build/multicollection/{i:000000}.bin")] = i;
}
}
public static ulong HashLittle2(ReadOnlySpan<char> 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;
}
}

View file

@ -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<UOPEntry>(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<UOPEntry>
{
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<UOPEntry>
{
public static readonly IComparer<UOPEntry> 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<UOPEntry>();
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();
}
}

View file

@ -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)
{

Binary file not shown.