Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
6fd073f
Add match-based X-axis toggle for VR History Graph
tinfoil766 Apr 14, 2026
9949825
Merge pull request #253 from tinfoil766/main
patchzyy Apr 23, 2026
a4e3efc
Full Patches Implementation
patchzyy Apr 27, 2026
c8f7270
Hide option if not avalible
patchzyy Apr 27, 2026
7fdde2e
Disable my-stuff
patchzyy Apr 27, 2026
bd286e6
.
patchzyy Apr 27, 2026
b0dba7f
Temporarily disable mod warning UI
patchzyy Apr 28, 2026
d7ed83c
.
patchzyy Apr 28, 2026
f89bd7d
Update SzsPatchConverter.cs
patchzyy Apr 28, 2026
def8f11
Return OperationResult in Szs archive decoder
patchzyy Apr 28, 2026
42e3db9
Update ModInstallationService.cs
patchzyy Apr 28, 2026
96cd021
Update ModManager.cs
patchzyy Apr 28, 2026
52e9b65
Thread locking for concurrent saving
patchzyy Apr 28, 2026
cb301ec
small things
patchzyy Apr 28, 2026
210501c
Add buffer validation and ensure output dirs
patchzyy Apr 28, 2026
28bb92e
Handle EnsureDirectory failures; minor cleanup
patchzyy Apr 28, 2026
d74e08f
Update RrLauncher.cs
patchzyy Apr 28, 2026
228f2e7
fix comment
patchzyy Apr 28, 2026
cae0ff5
Use OperationResult and improve SZS decoding
patchzyy Apr 28, 2026
bea4b4b
Update App.axaml.cs
patchzyy Apr 28, 2026
1fb8234
Translations
patchzyy Apr 28, 2026
21b93ac
stupid file formatsz
patchzyy Apr 28, 2026
2d961fa
warning
patchzyy Apr 30, 2026
20c2b71
Add U8 archive builder & tagged-archive support
patchzyy May 4, 2026
a319a0d
Merge pull request #257 from TeamWheelWizard/Patches-Converter
patchzyy May 17, 2026
4a20819
to 2.4.5
patchzyy May 17, 2026
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 Flatpak/io.github.TeamWheelWizard.WheelWizard.metainfo.xml
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@
</provides>

<releases>
<release version="2.4.5" date="2026-05-17"/>
<release version="2.4.4" date="2026-04-15"/>
<release version="2.4.3" date="2026-04-01"/>
<release version="2.4.2" date="2026-03-25"/>
Expand Down
10 changes: 10 additions & 0 deletions WheelWizard/Features/Archives/ArchivesExtensions.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
namespace WheelWizard.Features.Archives;

public static class ArchivesExtensions
{
public static IServiceCollection AddArchives(this IServiceCollection services)
{
services.AddSingleton<ISzsArchiveDecoder, SzsArchiveDecoder>();
return services;
}
}
3 changes: 3 additions & 0 deletions WheelWizard/Features/Archives/Domain/DecodedArchive.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
namespace WheelWizard.Features.Archives;

public sealed record DecodedArchive(IReadOnlyDictionary<string, byte[]> Files);
3 changes: 3 additions & 0 deletions WheelWizard/Features/Archives/Domain/U8Node.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
namespace WheelWizard.Features.Archives;

public sealed record U8Node(int Type, int NameOffset, int DataOffset, int Size);
8 changes: 8 additions & 0 deletions WheelWizard/Features/Archives/ISzsArchiveDecoder.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
namespace WheelWizard.Features.Archives;

public interface ISzsArchiveDecoder
{
OperationResult<DecodedArchive> TryDecodeU8Archive(byte[] bytes);

OperationResult<byte[]> DecompressYaz0IfNeeded(byte[] bytes);
}
176 changes: 176 additions & 0 deletions WheelWizard/Features/Archives/SzsArchiveDecoder.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,176 @@
using WheelWizard.Helpers;

namespace WheelWizard.Features.Archives;

public sealed class SzsArchiveDecoder : ISzsArchiveDecoder
{
private const uint U8Magic = 0x55aa382d;

public OperationResult<DecodedArchive> TryDecodeU8Archive(byte[] bytes)
{
try
{
var decompressResult = DecompressYaz0IfNeeded(bytes);
if (decompressResult.IsFailure)
return decompressResult.Error;

var raw = decompressResult.Value;

if (raw.Length < 8)
return new OperationError { Message = "The provided file is too small to contain a valid U8 archive header." };
if (BigEndianBinaryHelper.BufferToUint32(raw, 0) != U8Magic)
return new OperationError { Message = "The provided file is not a valid Yaz0/U8 archive." };

return ParseU8Archive(raw);
}
catch (Exception ex)
{
return new OperationError { Message = $"Failed to decode U8 archive: {ex.Message}", Exception = ex };
}
}

public OperationResult<byte[]> DecompressYaz0IfNeeded(byte[] bytes)
{
try
{
if (bytes.Length < 4)
return bytes;

if (BinaryStringHelper.ReadAscii(bytes, 0, 4) != "Yaz0")
return bytes;

if (bytes.Length < 8)
return new OperationError { Message = "Yaz0 header is truncated." };

var outputSize = checked((int)BigEndianBinaryHelper.BufferToUint32(bytes, 4));
var output = new byte[outputSize];
var src = 0x10;
var dst = 0;
var groupHeader = 0;
var bitsRemaining = 0;

while (dst < output.Length)
{
if (bitsRemaining == 0)
{
if (src >= bytes.Length)
return new OperationError { Message = "Yaz0 group header is truncated." };
groupHeader = bytes[src++];
bitsRemaining = 8;
}

if ((groupHeader & 0x80) != 0)
{
if (src >= bytes.Length)
return new OperationError { Message = "Yaz0 literal chunk is truncated." };
output[dst++] = bytes[src++];
}
else
{
if (src + 1 >= bytes.Length)
return new OperationError { Message = "Yaz0 backreference is truncated." };

var b1 = bytes[src++];
var b2 = bytes[src++];
var backOffset = (((b1 & 0x0f) << 8) | b2) + 1;
var length = b1 >> 4;
if (length == 0)
{
if (src >= bytes.Length)
return new OperationError { Message = "Yaz0 extended length byte is truncated." };
length = bytes[src++] + 0x12;
}
else
{
length += 2;
}

if (backOffset > dst)
return new OperationError { Message = "Yaz0 backreference offset is out of bounds." };

var copySrc = dst - backOffset;
for (var index = 0; index < length && dst < output.Length; index++)
output[dst++] = output[copySrc++];
}

groupHeader <<= 1;
bitsRemaining--;
}

return output;
}
catch (Exception ex)
{
return new OperationError { Message = $"Failed to decompress Yaz0 data: {ex.Message}", Exception = ex };
}
}

private static DecodedArchive ParseU8Archive(byte[] bytes)
{
var rootOffset = (int)BigEndianBinaryHelper.BufferToUint32(bytes, 4);
var rootNode = ReadU8Node(bytes, rootOffset);
var nodeCount = rootNode.Size;
var stringTableOffset = rootOffset + nodeCount * 12;
var files = new Dictionary<string, byte[]>(StringComparer.Ordinal);

if (rootNode.Type != 1)
throw new InvalidDataException("U8 root node is not a directory.");
if (nodeCount == 0 || stringTableOffset > bytes.Length)
throw new InvalidDataException("U8 node table is invalid or truncated.");

void Walk(int dirIndex, string prefix, int parentEndIndex)
{
var nodeIndex = dirIndex + 1;
var directoryNode = ReadU8Node(bytes, rootOffset + dirIndex * 12);
var endIndex = Math.Min(directoryNode.Size, parentEndIndex);

if (endIndex <= dirIndex)
return;

while (nodeIndex < endIndex)
{
var nodeOffset = rootOffset + nodeIndex * 12;
var node = ReadU8Node(bytes, nodeOffset);
var nameOffset = stringTableOffset + node.NameOffset;

if (nameOffset < stringTableOffset || nameOffset >= bytes.Length)
{
nodeIndex++;
continue;
}

var name = BinaryStringHelper.ReadNullTerminatedAscii(bytes, nameOffset);
var logicalPath = string.IsNullOrEmpty(prefix) ? name : $"{prefix}/{name}";

if (node.Type == 1)
{
Walk(nodeIndex, logicalPath, endIndex);
nodeIndex = Math.Min(Math.Max(node.Size, nodeIndex + 1), endIndex);
}
else
{
var endOffset = node.DataOffset + node.Size;
if (endOffset <= bytes.Length && endOffset >= node.DataOffset)
files[logicalPath] = bytes[node.DataOffset..endOffset];
nodeIndex++;
}
}
}

Walk(0, string.Empty, nodeCount);
return new(files);
}

private static U8Node ReadU8Node(byte[] bytes, int offset)
{
if (offset + 12 > bytes.Length)
throw new InvalidDataException("U8 node table is truncated.");

return new(
bytes[offset] == 0 ? 0 : 1,
(bytes[offset + 1] << 16) | (bytes[offset + 2] << 8) | bytes[offset + 3],
(int)BigEndianBinaryHelper.BufferToUint32(bytes, offset + 4),
(int)BigEndianBinaryHelper.BufferToUint32(bytes, offset + 8)
);
}
}
Loading
Loading