diff --git a/Projects/Server/MultiData.cs b/Projects/Server/Client/MultiData.cs similarity index 97% rename from Projects/Server/MultiData.cs rename to Projects/Server/Client/MultiData.cs index e8884b141..43498614d 100644 --- a/Projects/Server/MultiData.cs +++ b/Projects/Server/Client/MultiData.cs @@ -1,6 +1,6 @@ /************************************************************************* * ModernUO * - * Copyright 2019-2023 - ModernUO Development Team * + * Copyright 2019-2026 - ModernUO Development Team * * Email: hi@modernuo.com * * File: MultiData.cs * * * @@ -25,6 +25,9 @@ namespace Server; public static class MultiData { + public static string HousingUOPPath { get; private set; } + public static UOPEntry HousingEntry { get; private set; } + public static void Configure() { var multiUOPPath = Core.FindDataFile("MultiCollection.uop", false); @@ -53,8 +56,10 @@ public static class MultiData { using var stream = new FileStream(path, FileMode.Open, FileAccess.Read, FileShare.Read); - // TODO: Find out if housing.bin is needed and read that. - var uopEntries = UOPFiles.ReadUOPIndexes(stream, ".bin", 0x10000, 4, 6); + var housingHash = UOPFiles.HashLittle2("build/multicollection/housing.bin"); + var additionalHashes = new Dictionary { [housingHash] = default }; + + var uopEntries = UOPFiles.ReadUOPIndexes(stream, ".bin", 0x10000, 4, 6, additionalHashes); var compressionBuffer = STArrayPool.Shared.Rent(0x10000); var buffer = STArrayPool.Shared.Rent(0x10000); @@ -120,6 +125,12 @@ public static class MultiData STArrayPool.Shared.Return(buffer); STArrayPool.Shared.Return(compressionBuffer); + + if (additionalHashes.TryGetValue(housingHash, out var housingEntry) && housingEntry.Size > 0) + { + HousingUOPPath = path; + HousingEntry = housingEntry; + } } private static void LoadMul(bool postHSMulFormat) diff --git a/Projects/Server/Client/UOPFiles.cs b/Projects/Server/Client/UOPFiles.cs index e52d5ed86..e71d2c5de 100644 --- a/Projects/Server/Client/UOPFiles.cs +++ b/Projects/Server/Client/UOPFiles.cs @@ -22,7 +22,8 @@ namespace Server; public static class UOPFiles { public static Dictionary ReadUOPIndexes( - FileStream stream, string fileExt, int entryCount = 0x14000, int expectedVersion = -1, int precision = 8 + FileStream stream, string fileExt, int entryCount = 0x14000, int expectedVersion = -1, int precision = 8, + Dictionary additionalHashes = null ) { var reader = new BinaryReader(stream); @@ -70,8 +71,12 @@ public static class UOPFiles var compressed = reader.ReadInt16() == 1; - if (offset != 0 && hashes.TryGetValue(fileNameHash, out var fileIndex) && compressedLength > 0 && - decompressedLength > 0) + if (offset == 0 || compressedLength <= 0 || decompressedLength <= 0) + { + continue; + } + + if (hashes.TryGetValue(fileNameHash, out var fileIndex)) { entries[fileIndex] = new UOPEntry(offset + headerLength, decompressedLength) { @@ -79,6 +84,14 @@ public static class UOPFiles CompressedSize = compressedLength }; } + else if (additionalHashes != null && additionalHashes.ContainsKey(fileNameHash)) + { + additionalHashes[fileNameHash] = new UOPEntry(offset + headerLength, decompressedLength) + { + Compressed = compressed, + CompressedSize = compressedLength + }; + } } } while (nextBlock != 0 && nextBlock < length); diff --git a/Projects/UOContent/Multis/ComponentVerification.cs b/Projects/UOContent/Multis/ComponentVerification.cs index 9942fc9a0..3246560a9 100644 --- a/Projects/UOContent/Multis/ComponentVerification.cs +++ b/Projects/UOContent/Multis/ComponentVerification.cs @@ -1,315 +1,368 @@ using System; +using System.Buffers; using System.Collections.Generic; using System.IO; +using System.IO.Compression; +using Server.Compression; -namespace Server.Multis +namespace Server.Multis; + +public static class ComponentVerification { - public class ComponentVerification + private static int[] _itemTable; + private static int[] _multiTable; + private static bool _loaded; + + public static bool IsItemValid(int itemID) { - private readonly int[] m_ItemTable; - private readonly int[] m_MultiTable; + EnsureLoaded(); + return itemID > 0 && itemID < _itemTable.Length && CheckValidity(_itemTable[itemID]); + } - public ComponentVerification() + public static bool IsMultiValid(int multiID) + { + EnsureLoaded(); + return multiID > 0 && multiID < _multiTable.Length && CheckValidity(_multiTable[multiID]); + } + + private static bool CheckValidity(int val) => + val != -1 && (val == 0 || ((int)ExpansionInfo.CoreExpansion.HousingFlags & val) != 0); + + private static void EnsureLoaded() + { + if (_loaded) { - m_ItemTable = CreateTable(TileData.MaxItemValue); - m_MultiTable = CreateTable(0x4000); - - LoadItems( - "Data/Components/walls.txt", - "South1", - "South2", - "South3", - "Corner", - "East1", - "East2", - "East3", - "Post", - "WindowS", - "AltWindowS", - "WindowE", - "AltWindowE", - "SecondAltWindowS", - "SecondAltWindowE" - ); - LoadItems( - "Data/Components/teleprts.txt", - "F1", - "F2", - "F3", - "F4", - "F5", - "F6", - "F7", - "F8", - "F9", - "F10", - "F11", - "F12", - "F13", - "F14", - "F15", - "F16" - ); - LoadItems( - "Data/Components/stairs.txt", - "Block", - "North", - "East", - "South", - "West", - "Squared1", - "Squared2", - "Rounded1", - "Rounded2" - ); - LoadItems( - "Data/Components/roof.txt", - "North", - "East", - "South", - "West", - "NSCrosspiece", - "EWCrosspiece", - "NDent", - "EDent", - "SDent", - "WDent", - "NTPiece", - "ETPiece", - "STPiece", - "WTPiece", - "XPiece", - "Extra Piece" - ); - LoadItems( - "Data/Components/floors.txt", - "F1", - "F2", - "F3", - "F4", - "F5", - "F6", - "F7", - "F8", - "F9", - "F10", - "F11", - "F12", - "F13", - "F14", - "F15", - "F16" - ); - LoadItems( - "Data/Components/misc.txt", - "Piece1", - "Piece2", - "Piece3", - "Piece4", - "Piece5", - "Piece6", - "Piece7", - "Piece8" - ); - LoadItems( - "Data/Components/doors.txt", - "Piece1", - "Piece2", - "Piece3", - "Piece4", - "Piece5", - "Piece6", - "Piece7", - "Piece8" - ); - - LoadMultis("Data/Components/stairs.txt", "MultiNorth", "MultiEast", "MultiSouth", "MultiWest"); + return; } - public bool IsItemValid(int itemID) => - itemID > 0 && itemID < m_ItemTable.Length && CheckValidity(m_ItemTable[itemID]); + _loaded = true; - public bool IsMultiValid(int multiID) => - multiID > 0 && multiID < m_MultiTable.Length && CheckValidity(m_MultiTable[multiID]); + _itemTable = CreateTable(TileData.MaxItemValue); + _multiTable = CreateTable(0x4000); - public bool CheckValidity(int val) => - val != -1 && (val == 0 || ((int)ExpansionInfo.CoreExpansion.HousingFlags & val) != 0); - - private int[] CreateTable(int length) + var housingPath = MultiData.HousingUOPPath; + if (housingPath != null) { - var table = new int[length]; + var entry = MultiData.HousingEntry; + LoadFromHousingBin(ReadUOPEntry(housingPath, entry)); + return; + } - for (var i = 0; i < table.Length; ++i) + LoadFromTxtFiles(); + } + + private static byte[] ReadUOPEntry(string path, UOPEntry entry) + { + using var stream = new FileStream(path, FileMode.Open, FileAccess.Read, FileShare.Read); + stream.Seek(entry.Offset, SeekOrigin.Begin); + + if (entry.Compressed) + { + var compressedData = new byte[entry.CompressedSize]; + stream.ReadExactly(compressedData); + var decompressedData = new byte[entry.Size]; + + if (Deflate.Standard.Unpack(decompressedData, compressedData, out var bytesDecompressed) != LibDeflateResult.Success + || entry.Size != bytesDecompressed) { - table[i] = -1; + return null; } - return table; + return decompressedData; } - private void LoadItems(string path, params ReadOnlySpan itemColumns) + var data = new byte[entry.Size]; + stream.ReadExactly(data); + return data; + } + + private static void LoadFromHousingBin(byte[] data) + { + var reader = new SpanReader(data); + + var fileTypesCount = (int)reader.ReadUInt32LE(); + + for (var f = 0; f < fileTypesCount; f++) { - LoadSpreadsheet(m_ItemTable, path, itemColumns); - } + var fileType = (int)reader.ReadUInt32LE(); + var entriesCount = (int)reader.ReadUInt32LE(); + var isWalls = fileType == 5; + var isStairs = fileType == 1; - private void LoadMultis(string path, params ReadOnlySpan multiColumns) - { - LoadSpreadsheet(m_MultiTable, path, multiColumns); - } - - private void LoadSpreadsheet(int[] table, string path, params ReadOnlySpan tileColumns) - { - var ss = new Spreadsheet(path); - - var tileCIDs = new int[tileColumns.Length]; - - for (var i = 0; i < tileColumns.Length; ++i) + for (var e = 0; e < entriesCount; e++) { - tileCIDs[i] = ss.GetColumnID(tileColumns[i]); - } + reader.ReadUInt32LE(); // category_id + reader.ReadUInt32LE(); // subcategory_id + var featureMask = (int)reader.ReadUInt32LE(); + reader.ReadUInt32LE(); // cliloc_id - var featureCID = ss.GetColumnID("FeatureMask"); - - for (var i = 0; i < ss.Records.Length; ++i) - { - var record = ss.Records[i]; - - var fid = record.GetInt32(featureCID); - - for (var j = 0; j < tileCIDs.Length; ++j) + // fields_1 + var fieldsCount1 = (int)reader.ReadUInt32LE(); + for (var i = 0; i < fieldsCount1; i++) { - var itemID = record.GetInt32(tileCIDs[j]); + reader.ReadUInt32LE(); // direction + var staticId = (int)reader.ReadUInt32LE(); - if (itemID <= 0 || itemID >= table.Length) + if (staticId > 0 && staticId < _itemTable.Length) { - continue; + _itemTable[staticId] = featureMask; } + } - table[itemID] = fid; + // unknown1 (only for walls, between fields_1 and fields_count_2) + if (isWalls) + { + reader.ReadUInt32LE(); + } + + // fields_2 + var fieldsCount2 = (int)reader.ReadUInt32LE(); + for (var i = 0; i < fieldsCount2; i++) + { + reader.ReadUInt32LE(); // direction + var staticId = (int)reader.ReadUInt32LE(); + + if (isStairs) + { + if (staticId > 0 && staticId < _multiTable.Length) + { + _multiTable[staticId] = featureMask; + } + } + else if (staticId > 0 && staticId < _itemTable.Length) + { + _itemTable[staticId] = featureMask; + } + } + + // unknown2 (only for non-walls, after fields_2) + if (!isWalls) + { + reader.ReadUInt32LE(); } } } } - public class Spreadsheet + private static void LoadFromTxtFiles() { - private readonly ColumnInfo[] m_Columns; + LoadItems( + "walls.txt", + "South1", "South2", "South3", "Corner", + "East1", "East2", "East3", "Post", + "WindowS", "AltWindowS", "WindowE", "AltWindowE", + "SecondAltWindowS", "SecondAltWindowE" + ); + LoadItems( + "teleprts.txt", + "F1", "F2", "F3", "F4", "F5", "F6", "F7", "F8", + "F9", "F10", "F11", "F12", "F13", "F14", "F15", "F16" + ); + LoadItems( + "stairs.txt", + "Block", "North", "East", "South", "West", + "Squared1", "Squared2", "Rounded1", "Rounded2" + ); + LoadItems( + "roof.txt", + "North", "East", "South", "West", + "NSCrosspiece", "EWCrosspiece", + "NDent", "EDent", "SDent", "WDent", + "NTPiece", "ETPiece", "STPiece", "WTPiece", + "XPiece", "Extra Piece" + ); + LoadItems( + "floors.txt", + "F1", "F2", "F3", "F4", "F5", "F6", "F7", "F8", + "F9", "F10", "F11", "F12", "F13", "F14", "F15", "F16" + ); + LoadItems( + "misc.txt", + "Piece1", "Piece2", "Piece3", "Piece4", + "Piece5", "Piece6", "Piece7", "Piece8" + ); + LoadItems( + "doors.txt", + "Piece1", "Piece2", "Piece3", "Piece4", + "Piece5", "Piece6", "Piece7", "Piece8" + ); - public Spreadsheet(string path) + LoadMultis("stairs.txt", "MultiNorth", "MultiEast", "MultiSouth", "MultiWest"); + } + + private static string ResolveTxtPath(string filename) + { + var clientPath = Core.FindDataFile(filename, false); + if (clientPath != null) { - using var ip = new StreamReader(path); - var types = ReadLine(ip); - var names = ReadLine(ip); - - m_Columns = new ColumnInfo[types.Length]; - - for (var i = 0; i < m_Columns.Length; ++i) - { - m_Columns[i] = new ColumnInfo(i, types[i], names[i]); - } - - var records = new List(); - - string[] values; - - while ((values = ReadLine(ip)) != null) - { - var data = new object[m_Columns.Length]; - - for (var i = 0; i < m_Columns.Length; ++i) - { - var ci = m_Columns[i]; - - switch (ci.m_Type) - { - case "int": - { - data[i] = Utility.ToInt32(values[ci.m_DataIndex]); - break; - } - case "string": - { - data[i] = values[ci.m_DataIndex]; - break; - } - } - } - - records.Add(new DataRecord(this, data)); - } - - Records = records.ToArray(); + return clientPath; } - public DataRecord[] Records { get; } + var bundledPath = Path.Combine("Data", "Components", filename); + return File.Exists(bundledPath) ? bundledPath : null; + } - public int GetColumnID(string name) + private static void LoadItems(string filename, params ReadOnlySpan itemColumns) => + LoadSpreadsheet(_itemTable, filename, itemColumns); + + private static void LoadMultis(string filename, params ReadOnlySpan multiColumns) => + LoadSpreadsheet(_multiTable, filename, multiColumns); + + private static void LoadSpreadsheet(int[] table, string filename, params ReadOnlySpan tileColumns) + { + var path = ResolveTxtPath(filename); + if (path == null) { - for (var i = 0; i < m_Columns.Length; ++i) - { - if (m_Columns[i].m_Name == name) - { - return i; - } - } - - return -1; + return; } - private string[] ReadLine(StreamReader ip) + var ss = new Spreadsheet(path); + + var tileCIDs = new int[tileColumns.Length]; + + for (var i = 0; i < tileColumns.Length; ++i) { - string line; - - while ((line = ip.ReadLine()) != null) - { - if (line.Length > 0) - { - return line.Split('\t'); - } - } - - return null; + tileCIDs[i] = ss.GetColumnID(tileColumns[i]); } - private class ColumnInfo + var featureCID = ss.GetColumnID("FeatureMask"); + + for (var i = 0; i < ss.Records.Length; ++i) { - public readonly int m_DataIndex; - public readonly string m_Name; + var record = ss.Records[i]; - public readonly string m_Type; + var fid = record.GetInt32(featureCID); - public ColumnInfo(int dataIndex, string type, string name) + for (var j = 0; j < tileCIDs.Length; ++j) { - m_DataIndex = dataIndex; + var itemID = record.GetInt32(tileCIDs[j]); - m_Type = type; - m_Name = name; + if (itemID <= 0 || itemID >= table.Length) + { + continue; + } + + table[itemID] = fid; } } } - public class DataRecord + private static int[] CreateTable(int length) { - public DataRecord(Spreadsheet ss, object[] data) + var table = new int[length]; + + for (var i = 0; i < table.Length; ++i) { - Spreadsheet = ss; - Data = data; + table[i] = -1; } - public Spreadsheet Spreadsheet { get; } - - public object[] Data { get; } - - public object this[string name] => this[Spreadsheet.GetColumnID(name)]; - - public object this[int id] => id < 0 ? null : Data[id]; - - public int GetInt32(string name) => GetInt32(this[name]); - - public int GetInt32(int id) => GetInt32(this[id]); - - public int GetInt32(object obj) => Convert.ToInt32(obj); - - public string GetString(string name) => this[name] as string; + return table; } } + +public class Spreadsheet +{ + private readonly ColumnInfo[] m_Columns; + + public Spreadsheet(string path) + { + using var ip = new StreamReader(path); + var types = ReadLine(ip); + var names = ReadLine(ip); + + m_Columns = new ColumnInfo[types.Length]; + + for (var i = 0; i < m_Columns.Length; ++i) + { + m_Columns[i] = new ColumnInfo(i, types[i], names[i]); + } + + var records = new List(); + + while (ReadLine(ip) is { } values) + { + var data = new object[m_Columns.Length]; + + for (var i = 0; i < m_Columns.Length; ++i) + { + var ci = m_Columns[i]; + + data[i] = ci.m_Type switch + { + "int" => Utility.ToInt32(values[ci.m_DataIndex]), + "string" => values[ci.m_DataIndex], + _ => data[i] + }; + } + + records.Add(new DataRecord(this, data)); + } + + Records = records.ToArray(); + } + + public DataRecord[] Records { get; } + + public int GetColumnID(string name) + { + for (var i = 0; i < m_Columns.Length; ++i) + { + if (m_Columns[i].m_Name == name) + { + return i; + } + } + + return -1; + } + + private static string[] ReadLine(StreamReader ip) + { + while (ip.ReadLine() is { } line) + { + if (line.Length > 0) + { + return line.Split('\t'); + } + } + + return null; + } + + private class ColumnInfo + { + public readonly int m_DataIndex; + public readonly string m_Name; + + public readonly string m_Type; + + public ColumnInfo(int dataIndex, string type, string name) + { + m_DataIndex = dataIndex; + + m_Type = type; + m_Name = name; + } + } +} + +public class DataRecord +{ + public DataRecord(Spreadsheet ss, object[] data) + { + Spreadsheet = ss; + Data = data; + } + + public Spreadsheet Spreadsheet { get; } + + public object[] Data { get; } + + public object this[string name] => this[Spreadsheet.GetColumnID(name)]; + + public object this[int id] => id < 0 ? null : Data[id]; + + public int GetInt32(int id) => GetInt32(this[id]); + + public int GetInt32(object obj) => Convert.ToInt32(obj); +} diff --git a/Projects/UOContent/Multis/Houses/HouseFoundation.cs b/Projects/UOContent/Multis/Houses/HouseFoundation.cs index 2c12373e0..11a387934 100644 --- a/Projects/UOContent/Multis/Houses/HouseFoundation.cs +++ b/Projects/UOContent/Multis/Houses/HouseFoundation.cs @@ -25,8 +25,6 @@ namespace Server.Multis public class HouseFoundation : BaseHouse { - private static ComponentVerification m_Verification; - public static readonly bool AllowStairSectioning = true; /* Stair block IDs @@ -201,8 +199,6 @@ namespace Server.Multis } } - public static ComponentVerification Verification => m_Verification ??= new ComponentVerification(); - public bool IsFixture(Item item) => Fixtures.Contains(item); public override int GetMaxUpdateRange() => 24; @@ -1358,7 +1354,7 @@ namespace Server.Multis public static bool ValidPiece(int itemID, bool roof = false) { itemID &= TileData.MaxItemValue; - return roof == TileData.ItemTable[itemID].Roof && Verification.IsItemValid(itemID); + return roof == TileData.ItemTable[itemID].Roof && ComponentVerification.IsItemValid(itemID); } public static bool IsStairBlock(int id) @@ -1638,7 +1634,7 @@ namespace Server.Multis // Validate stair multi ID var design = context.Foundation.DesignState; - if (!Verification.IsMultiValid(itemID)) + if (!ComponentVerification.IsMultiValid(itemID)) { /* Specified multi ID is not a stair * - Resend design state