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
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;
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue