Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions Ultima/Files.cs
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,7 @@ public static void FireFileSaveEvent()
"mobtypes.txt",
"multi.idx",
"multi.mul",
"multicollection.uop",
"multimap.rle",
"radarcol.mul",
"skillgrp.mul",
Expand Down
9 changes: 7 additions & 2 deletions Ultima/Helpers/MythicDecompress.cs
Original file line number Diff line number Diff line change
Expand Up @@ -24,11 +24,16 @@ public static byte[] Decompress(byte[] buffer)
using (var reader = new BinaryReader(new MemoryStream(buffer)))
{
var header = reader.ReadUInt32();
uint dataLength = header ^ 0x8E2C9A3D; // Must be equal to output length, error otherwise
uint dataLength = header ^ 0x8E2C9A3D;

// MoveToFront decoding
var list = reader.ReadBytes((int)(reader.BaseStream.Length - 4));
output = InternalDecompress(MoveToFrontCoding.Decode(list));

if (output.Length != dataLength)
{
throw new InvalidDataException(
$"Decompressed length {output.Length} does not match expected {dataLength}. File is not in compressed cliloc format.");
}
}

return output;
Expand Down
37 changes: 36 additions & 1 deletion Ultima/MultiComponentList.cs
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,8 @@ public sealed class MultiComponentList
public MultiTileEntry[] SortedTiles { get; }
public int Surface { get; private set; }

public static HashSet<ushort> DynamicItemIds { get; set; }

public struct MultiTileEntry
{
public ushort ItemId;
Expand Down Expand Up @@ -433,7 +435,7 @@ public MultiComponentList(string fileName, Multis.ImportType type)
var tempItem = new MultiTileEntry
{
ItemId = 0xFFFF,
Flags = 1,
Flags = 0,
Unk1 = 0
};

Expand All @@ -444,10 +446,17 @@ public MultiComponentList(string fileName, Multis.ImportType type)
{
if (tempItem.ItemId != 0xFFFF)
{
if (DynamicItemIds != null && DynamicItemIds.Contains(tempItem.ItemId))
{
tempItem.Flags = 1;
}

SortedTiles[itemCount] = tempItem;
++itemCount;
}

tempItem.ItemId = 0xFFFF;
tempItem.Flags = 0;
}
else if (line.StartsWith("ID"))
{
Expand Down Expand Up @@ -496,11 +505,37 @@ public MultiComponentList(string fileName, Multis.ImportType type)
}
}
}

if (tempItem.ItemId != 0xFFFF)
{
if (DynamicItemIds?.Contains(tempItem.ItemId) == true)
{
tempItem.Flags = 1;
}

SortedTiles[itemCount] = tempItem;
}
}

// WSC files from a live UO world use absolute world coordinates.
// Tool-generated WSC files may already have relative offsets (possibly negative).
// Heuristic: if both min coords are positive AND each exceeds the multi's own
// extent, the file contains world coordinates and must be normalized.
int extentX = _max.X - _min.X;
int extentY = _max.Y - _min.Y;
if (_min.X > 0 && _min.Y > 0 && _min.X > extentX && _min.Y > extentY)
{
for (int i = 0; i < SortedTiles.Length; ++i)
{
SortedTiles[i].OffsetX = (short)(SortedTiles[i].OffsetX - _min.X);
SortedTiles[i].OffsetY = (short)(SortedTiles[i].OffsetY - _min.Y);
}
_max.X -= _min.X;
_max.Y -= _min.Y;
_min.X = 0;
_min.Y = 0;
}

break;
}
case Multis.ImportType.CSV:
Expand Down
165 changes: 165 additions & 0 deletions Ultima/Multis.cs
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,9 @@ public sealed class Multis
private static MultiComponentList[] _components = new MultiComponentList[MaximumMultiIndex];
private static FileIndex _fileIndex = new FileIndex("Multi.idx", "Multi.mul", MaximumMultiIndex, 14);

private static MultiComponentList[] _uopComponents = new MultiComponentList[MaximumMultiIndex];
private static bool _uopLoaded;

public enum ImportType
{
TXT,
Expand All @@ -33,6 +36,7 @@ public static void Reload()
{
_fileIndex = new FileIndex("Multi.idx", "Multi.mul", MaximumMultiIndex, 14);
_components = new MultiComponentList[MaximumMultiIndex];
ReloadUop();
}

/// <summary>
Expand Down Expand Up @@ -236,6 +240,167 @@ public static List<object[]> LoadFromDesigner(string fileName)
}
}

public static bool HasUopFile => !string.IsNullOrEmpty(Files.GetFilePath("multicollection.uop"));

public static void ReloadUop()
{
_uopComponents = new MultiComponentList[MaximumMultiIndex];
_uopLoaded = false;
}

public static MultiComponentList GetUopComponents(int index)
{
if (!_uopLoaded)
{
LoadUop();
}

if (index >= 0 && index < _uopComponents.Length)
{
return _uopComponents[index] ?? MultiComponentList.Empty;
}

return MultiComponentList.Empty;
}

private static void LoadUop()
{
_uopLoaded = true;

string path = Files.GetFilePath("multicollection.uop");
if (path == null)
{
return;
}

try
{
using var fileStream = new FileStream(path, FileMode.Open, FileAccess.Read, FileShare.Read);
using var reader = new BinaryReader(fileStream);

uint magic = reader.ReadUInt32();
if (magic != 0x0050594D)
{
return;
}

uint version = reader.ReadUInt32();
if (version > 5)
{
return;
}

reader.ReadUInt32(); // signature
ulong nextTableOffset = reader.ReadUInt64();
reader.ReadUInt32(); // block capacity
reader.ReadUInt32(); // file count
reader.ReadUInt32(); // reserved
reader.ReadUInt32(); // reserved
reader.ReadUInt32(); // reserved

var entries = new List<(long dataOffset, uint compressedSize, uint decompressedSize)>();

ulong next = nextTableOffset;
while (next != 0)
{
fileStream.Seek((long)next, SeekOrigin.Begin);
int count = reader.ReadInt32();
next = reader.ReadUInt64();

for (int i = 0; i < count; i++)
{
ulong dataOffset = reader.ReadUInt64();
uint headerSize = reader.ReadUInt32();
uint compressedSize = reader.ReadUInt32();
uint decompressedSize = reader.ReadUInt32();
reader.ReadUInt64(); // hash
reader.ReadUInt32(); // unknown
ushort flag = reader.ReadUInt16();

if (dataOffset == 0 || decompressedSize == 0)
{
continue;
}

if (flag == 0)
{
compressedSize = 0;
}

entries.Add(((long)(dataOffset + headerSize), compressedSize, decompressedSize));
}
}

foreach (var (dataOffset, compressedSize, decompressedSize) in entries)
{
fileStream.Seek(dataOffset, SeekOrigin.Begin);

byte[] raw;
if (compressedSize > 0)
{
byte[] compressed = reader.ReadBytes((int)compressedSize);
(bool ok, byte[] decompressed) = UopUtils.Decompress(compressed);
if (!ok)
{
continue;
}

raw = decompressed;
}
else
{
raw = reader.ReadBytes((int)decompressedSize);
}

using var memoryStream = new MemoryStream(raw);
using var binaryReader = new BinaryReader(memoryStream);

uint multiId = binaryReader.ReadUInt32();
int componentCount = binaryReader.ReadInt32();

if (multiId >= MaximumMultiIndex || componentCount <= 0)
{
continue;
}

var tiles = new List<MultiComponentList.MultiTileEntry>(componentCount);
for (int j = 0; j < componentCount; j++)
{
ushort graphic = binaryReader.ReadUInt16();
ushort ux = binaryReader.ReadUInt16();
ushort uy = binaryReader.ReadUInt16();
ushort uz = binaryReader.ReadUInt16();
ushort uflags = binaryReader.ReadUInt16();
int clilocsCount = binaryReader.ReadInt32();

if (clilocsCount > 0)
{
binaryReader.BaseStream.Seek(clilocsCount * 4L, SeekOrigin.Current);
}

tiles.Add(new MultiComponentList.MultiTileEntry
{
ItemId = graphic,
OffsetX = (short)ux,
OffsetY = (short)uy,
OffsetZ = (short)uz,
Flags = uflags != 0 ? 0 : 1,
Unk1 = 0
});
}

if (tiles.Count > 0)
{
_uopComponents[multiId] = new MultiComponentList(tiles);
}
}
}
catch
{
// leave array in its current partially-populated state
}
}

private static List<MultiComponentList.MultiTileEntry> RebuildTiles(MultiComponentList.MultiTileEntry[] tiles)
{
var newTiles = new List<MultiComponentList.MultiTileEntry>();
Expand Down
79 changes: 49 additions & 30 deletions Ultima/StringList.cs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Text;
Expand All @@ -7,7 +8,7 @@ namespace Ultima
{
public sealed class StringList
{
private readonly bool _decompress;
private bool _decompress;
private int _header1;
private short _header2;

Expand Down Expand Up @@ -51,45 +52,63 @@ private void LoadEntry(string path)
return;
}

Entries = new List<StringEntry>();
_stringTable = new Dictionary<int, string>();
_entryTable = new Dictionary<int, StringEntry>();
using var fileStream = new FileStream(path, FileMode.Open, FileAccess.Read, FileShare.Read);
byte[] buffer = new byte[fileStream.Length];
_ = fileStream.Read(buffer, 0, buffer.Length);

using (var fileStream = new FileStream(path, FileMode.Open, FileAccess.Read, FileShare.Read))
if (!TryParse(buffer, _decompress))
{
byte[] buffer = new byte[fileStream.Length];
_ = fileStream.Read(buffer, 0, buffer.Length);

byte[] clilocData = _decompress
? MythicDecompress.Decompress(buffer)
: buffer;

using (var reader = new BinaryReader(new MemoryStream(clilocData)))
bool fallback = !_decompress;
if (!TryParse(buffer, fallback))
{
_header1 = reader.ReadInt32();
_header2 = reader.ReadInt16();
throw new InvalidDataException($"Failed to parse cliloc file '{path}' in either compressed or uncompressed format.");
}
_decompress = fallback;
}
}

while (reader.BaseStream.Length != reader.BaseStream.Position)
{
int number = reader.ReadInt32();
byte flag = reader.ReadByte();
int length = reader.ReadInt16();
private bool TryParse(byte[] buffer, bool decompress)
{
try
{
byte[] clilocData = decompress ? MythicDecompress.Decompress(buffer) : buffer;

if (length > _buffer.Length)
{
_buffer = new byte[(length + 1023) & ~1023];
}
var entries = new List<StringEntry>();
var stringTable = new Dictionary<int, string>();
var entryTable = new Dictionary<int, StringEntry>();

reader.Read(_buffer, 0, length);
string text = Encoding.UTF8.GetString(_buffer, 0, length);
using var reader = new BinaryReader(new MemoryStream(clilocData));
_header1 = reader.ReadInt32();
_header2 = reader.ReadInt16();

var se = new StringEntry(number, text, flag);
Entries.Add(se);
while (reader.BaseStream.Length != reader.BaseStream.Position)
{
int number = reader.ReadInt32();
byte flag = reader.ReadByte();
int length = reader.ReadInt16();

_stringTable[number] = text;
_entryTable[number] = se;
if (length > _buffer.Length)
{
_buffer = new byte[(length + 1023) & ~1023];
}

reader.Read(_buffer, 0, length);
string text = Encoding.UTF8.GetString(_buffer, 0, length);

var se = new StringEntry(number, text, flag);
entries.Add(se);
stringTable[number] = text;
entryTable[number] = se;
}

Entries = entries;
_stringTable = stringTable;
_entryTable = entryTable;
return true;
}
catch
{
return false;
}
}

Expand Down
Loading