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:
parent
db0621b884
commit
dc118d836f
10 changed files with 680 additions and 382 deletions
Binary file not shown.
|
|
@ -1,4 +1,3 @@
|
|||
using System;
|
||||
using Server.Gumps;
|
||||
|
||||
namespace Server.Tests.Gumps;
|
||||
|
|
|
|||
185
Projects/Server/Client/ArtData.cs
Normal file
185
Projects/Server/Client/ArtData.cs
Normal 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();
|
||||
}
|
||||
}
|
||||
32
Projects/Server/Client/UOPEntry.cs
Normal file
32
Projects/Server/Client/UOPEntry.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
238
Projects/Server/Client/UOPFiles.cs
Normal file
238
Projects/Server/Client/UOPFiles.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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; }
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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.
Loading…
Add table
Add a link
Reference in a new issue