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
102 changes: 85 additions & 17 deletions Ultima/AnimationsUopLoader.cs
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,15 @@ internal static class AnimationsUopLoader
internal const int _maxAnimActions = 80;
private const int _maxDirections = 5;

/// <summary>
/// Size of the fixed part of one AnimationSequence group record: the four leading fields,
/// the per frame bytes and the reserved block. Two variable length lists follow it.
/// </summary>
private const int _sequenceGroupFixedSize = 64;

/// <summary>Size of one property record hanging off a group: six ints, two shorts and a float.</summary>
private const int _sequenceGroupPropSize = 32;

private static FileStream[] _uopFiles = new FileStream[6];
private static readonly Dictionary<ulong, UopEntry> _hashTable = new();
private static readonly Dictionary<int, int[]> _sequenceReplacements = new();
Expand Down Expand Up @@ -258,6 +267,21 @@ private static void LoadAnimationSequence()
}
}

/// <summary>
/// Builds the action replacement table for one body from its AnimationSequence entry.
/// </summary>
/// <remarks>
/// Group records are variable length. The 72 byte stride this used to assume is only the
/// degenerate case where both trailing lists are empty: the fixed part is 64 bytes and the
/// two counted lists add 8 more when both counts are zero. Three bodies of a shipped client
/// - 400, 666 and 1253 in 7.0.114.4 - carry property records and are longer than that.
/// <para>
/// The walk also used to be skipped outright when the group count was 48 or 68. Those are
/// legitimate counts, not sentinels - they are simply the counts of the bodies that carry
/// property records, which is what a fixed stride cannot walk. Reading them properly means
/// body 666 resolves 67 to 66, and body 1253 resolves 2 to 0, 3 to 1 and 42 to 38.
/// </para>
/// </remarks>
private static void ParseSequenceEntry(int animId, byte[] data)
{
if (data.Length < 56)
Expand All @@ -271,37 +295,81 @@ private static void ParseSequenceEntry(int animId, byte[] data)
binaryReader.ReadUInt32(); // animId stored in file
binaryReader.BaseStream.Seek(48, SeekOrigin.Current); // skip 12 × u32

int replaces = binaryReader.ReadInt32();
int groupCount = binaryReader.ReadInt32();

var replacements = new int[_maxAnimActions];
for (int i = 0; i < _maxAnimActions; i++)
{
replacements[i] = i;
}

if (replaces != 48 && replaces != 68)
for (int i = 0; i < groupCount; i++)
{
for (int k = 0; k < replaces; k++)
if (!ReadSequenceGroup(binaryReader, replacements))
{
if (binaryReader.BaseStream.Position + 72 > binaryReader.BaseStream.Length)
{
break;
}
break;
}
}

int oldGroup = binaryReader.ReadInt32();
uint frameCount = binaryReader.ReadUInt32();
int newGroup = binaryReader.ReadInt32();
_sequenceReplacements[animId] = replacements;
}

if (frameCount == 0 && oldGroup >= 0 && oldGroup < _maxAnimActions && newGroup >= 0)
{
replacements[oldGroup] = newGroup;
}
/// <summary>
/// Reads one group record, recording the alias it declares.
/// </summary>
/// <remarks>
/// A replacement group of -1 means the body has its own frames for that group; any other
/// value borrows another group's, and then the frame count is zero.
/// </remarks>
/// <returns>False when the record runs past the payload, which stops the walk.</returns>
private static bool ReadSequenceGroup(BinaryReader reader, int[] replacements)
{
Stream stream = reader.BaseStream;

binaryReader.BaseStream.Seek(60, SeekOrigin.Current); // skip remaining per-replacement fields
}
if (stream.Position + _sequenceGroupFixedSize > stream.Length)
{
return false;
}

_sequenceReplacements[animId] = replacements;
int group = reader.ReadInt32();
int frameCount = reader.ReadInt32();
int replacementGroup = reader.ReadInt32();

// The frame rate, the per frame bytes and the reserved ints that make up the rest of the
// fixed part carry nothing the replacement table needs.
stream.Seek(_sequenceGroupFixedSize - (3 * sizeof(int)), SeekOrigin.Current);

if (frameCount == 0 && group >= 0 && group < _maxAnimActions && replacementGroup >= 0)
{
replacements[group] = replacementGroup;
}

return TrySkipList(reader, _sequenceGroupPropSize) && TrySkipList(reader, sizeof(int));
}

/// <summary>
/// Skips one counted list, checking the count against the bytes left before seeking past it,
/// so a malformed entry cannot walk the reader off the payload.
/// </summary>
private static bool TrySkipList(BinaryReader reader, int itemSize)
{
Stream stream = reader.BaseStream;

if (stream.Position + sizeof(int) > stream.Length)
{
return false;
}

int count = reader.ReadInt32();

if (count < 0 || (long)count * itemSize > stream.Length - stream.Position)
{
return false;
}

stream.Seek((long)count * itemSize, SeekOrigin.Current);

return true;
}

public static bool IsUopBody(int body)
Expand Down
41 changes: 41 additions & 0 deletions Ultima/Art.cs
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,9 @@ public static class Art
private static bool[] _removed;
private static readonly Dictionary<int, bool> _patched = new Dictionary<int, bool>();
public static bool Modified;
// Indexes edited since load or since the last save, in the same combined index space the
// Replace/Remove methods use (land = index & 0x3FFF, static = legal item id + 0x4000).
private static readonly ModifiedIndexTracker _modified = new ModifiedIndexTracker();

private static readonly byte[] _validBuffer = new byte[4];

Expand Down Expand Up @@ -180,6 +183,7 @@ public static void Reload()
_replaced.Clear();
_removed = new bool[0x14000];
_patched.Clear();
_modified.Clear();
Modified = false;
}

Expand Down Expand Up @@ -208,6 +212,7 @@ public static void ReplaceStatic(int index, Bitmap bmp)

_patched.Remove(index);

_modified.Mark(index);
Modified = true;
}

Expand All @@ -225,6 +230,7 @@ public static void ReplaceLand(int index, Bitmap bmp)

_patched.Remove(index);

_modified.Mark(index);
Modified = true;
}

Expand All @@ -237,6 +243,7 @@ public static void RemoveStatic(int index)
index = GetLegalItemId(index);
index += 0x4000;
_removed[index] = true;
_modified.Mark(index);
Modified = true;
}

Expand All @@ -248,9 +255,41 @@ public static void RemoveLand(int index)
{
index &= 0x3FFF;
_removed[index] = true;
_modified.Mark(index);
Modified = true;
}

/// <summary>
/// Tests if the Static at <paramref name="index"/> was replaced or removed since the art was
/// loaded or last saved.
/// </summary>
public static bool IsStaticModified(int index)
{
return _modified.IsMarked(GetLegalItemId(index) + 0x4000);
}

/// <summary>
/// Tests if the Land tile at <paramref name="index"/> was replaced or removed since the art was
/// loaded or last saved.
/// </summary>
public static bool IsLandModified(int index)
{
return _modified.IsMarked(index & 0x3FFF);
}

/// <summary>
/// Number of Land tiles and Statics edited since the art was loaded or last saved.
/// </summary>
public static int ModifiedCount => _modified.Count;

/// <summary>
/// Drops every modified mark without touching the edits themselves.
/// </summary>
public static void ClearModified()
{
_modified.Clear();
}

/// <summary>
/// Tests if Static is defined (width and height check)
/// </summary>
Expand Down Expand Up @@ -1094,6 +1133,8 @@ public static unsafe void Save(string path)
memmul.WriteTo(fsmul);
}
}

_modified.Clear();
}

}
Expand Down
22 changes: 19 additions & 3 deletions Ultima/FileIndex.cs
Original file line number Diff line number Diff line change
Expand Up @@ -249,6 +249,22 @@ public FileIndex(string idxFile, string mulFile, int file)
}
}

/// <summary>
/// True when the entry carries the high bit that <see cref="IFileAccessor.ApplyPatch"/> sets
/// to mark it as coming from verdata.
/// </summary>
/// <remarks>
/// A length of -1 is the "unused entry" filler that real .idx files are padded with, and it
/// has that same high bit set. Without the -1 test every unused entry looks like a verdata
/// patch of length 0x7FFFFFFF, and the reader then tries to pull that many bytes out of
/// <see cref="Verdata.Stream"/> - which, with no verdata.mul present, is Stream.Null.
/// A real patch never reaches 0x7FFFFFFF bytes, so the two cases cannot be confused.
/// </remarks>
private static bool IsVerdataPatched(IEntry e)
{
return e.Length != -1 && (e.Length & (1 << 31)) != 0;
}

public Stream Seek(int index, out int length, out int extra, out bool patched)
{
if (FileAccessor is null)
Expand Down Expand Up @@ -277,7 +293,7 @@ public Stream Seek(int index, out int length, out int extra, out bool patched)
length = e.Length & 0x7FFFFFFF;
extra = e.Extra;

if ((e.Length & (1 << 31)) != 0)
if (IsVerdataPatched(e))
{
patched = true;
Verdata.Seek(e.Lookup);
Expand Down Expand Up @@ -343,7 +359,7 @@ public Stream Seek(int index, ref IEntry entry, out bool patched)

entry = e;

if ((e.Length & (1 << 31)) != 0)
if (IsVerdataPatched(e))
{
patched = true;
Verdata.Seek(e.Lookup);
Expand Down Expand Up @@ -443,7 +459,7 @@ public bool Valid(int index, out int length, out int extra, out bool patched)
length = e.Length & 0x7FFFFFFF;
extra = e.Extra;

if ((e.Length & (1 << 31)) != 0)
if (IsVerdataPatched(e))
{
patched = true;
return true;
Expand Down
5 changes: 5 additions & 0 deletions Ultima/Files.cs
Original file line number Diff line number Diff line change
Expand Up @@ -126,11 +126,13 @@ public static void FireFileSaveEvent()
"mapdif2.mul",
"mapdif3.mul",
"mapdif4.mul",
"mapdif5.mul",
"mapdifl0.mul",
"mapdifl1.mul",
"mapdifl2.mul",
"mapdifl3.mul",
"mapdifl4.mul",
"mapdifl5.mul",
"mobtypes.txt",
"multi.idx",
"multi.mul",
Expand All @@ -150,16 +152,19 @@ public static void FireFileSaveEvent()
"stadif2.mul",
"stadif3.mul",
"stadif4.mul",
"stadif5.mul",
"stadifi0.mul",
"stadifi1.mul",
"stadifi2.mul",
"stadifi3.mul",
"stadifi4.mul",
"stadifi5.mul",
"stadifl0.mul",
"stadifl1.mul",
"stadifl2.mul",
"stadifl3.mul",
"stadifl4.mul",
"stadifl5.mul",
"staidx0.mul",
"staidx1.mul",
"staidx2.mul",
Expand Down
Loading