Skip to content

Commit

Permalink
Added P3D Compression
Browse files Browse the repository at this point in the history
  • Loading branch information
Hampo committed Aug 20, 2024
1 parent 9a7dd6f commit a2cdd96
Show file tree
Hide file tree
Showing 2 changed files with 287 additions and 4 deletions.
276 changes: 272 additions & 4 deletions NetP3DLib/NetP3DLib/P3D/LZR_Compression.cs
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
using System.Collections.Generic;
using NetP3DLib.P3D.Chunks;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;

namespace NetP3DLib.P3D;

public static class LZR_Compression
{
private const uint CompressedSignature = 0x5A443350;

private static List<byte> DecompressBlock(BinaryReader br, uint size)
{
List<byte> output = new((int)size);
Expand Down Expand Up @@ -113,10 +114,16 @@ private static List<byte> Decompress(BinaryReader file, uint UncompressedLength)
return output;
}

/// <summary>
/// Reads <paramref name="file"/>, and decompresses it if a compressed Pure3D File is detected.
/// <para>Returns the normal file bytes if not a compressed Pure3D File.</para>
/// </summary>
/// <param name="file">The <see cref="BinaryReader"/> to read.</param>
/// <returns>Returns <paramref name="file"/>'s bytes, decompressed if it was a compressed Pure3D File.</returns>
public static byte[] DecompressFile(BinaryReader file)
{
uint identifier = file.ReadUInt32();
if (identifier != CompressedSignature)
if (identifier != P3DFile.COMPRESSED_SIGNATURE)
{
file.BaseStream.Seek(0, SeekOrigin.Begin);
return file.ReadBytes((int)file.BaseStream.Length);
Expand All @@ -126,4 +133,265 @@ public static byte[] DecompressFile(BinaryReader file)
List<byte> decompressed = Decompress(file, length);
return [.. decompressed];
}

private const int MINIMUM_MATCH_LENGTH = 4;
private const int LZR_BLOCK_SIZE = 4096;
private const int TREE_ROOT = LZR_BLOCK_SIZE;
private const int UNUSED = LZR_BLOCK_SIZE + 0xffff;

private struct TreeNode
{
public uint Parent;
public uint Smaller;
public uint Larger;
}

private static readonly TreeNode[] tree = new TreeNode[LZR_BLOCK_SIZE + 1];

private static void CompressBlock(List<byte> input, uint inputSize, List<byte> output, ref uint outputSize, bool fastest)
{
uint inputCount = 0;
uint outputCount = 0;
uint literalStart = 0;
uint literalCount = 0;

InitTree();

uint offset = 0;
uint count = 0;

while (inputCount < inputSize)
{
if ((count > MINIMUM_MATCH_LENGTH) && ((offset & 15) != 0))
{
if (literalCount > 0)
{
outputCount += WriteCount(output, literalCount, 0);
output.AddRange(input.GetRange((int)literalStart, (int)literalCount));
outputCount += literalCount;
}

outputCount += WriteCount(output, count, (byte)((offset & 0xf) << 4));
output.Add((byte)((offset & 0xff0) >> 4));
outputCount++;

if (fastest)
{
inputCount += count;
}
else
{
uint total = count;
bool failed = false;
for (int c = 0; c < total; c++)
{
inputCount++;
failed = AddString(input, inputCount, inputSize, ref offset, ref count);
}
}
literalCount = 0;
literalStart = inputCount;
}
else
{
literalCount++;
inputCount++;
if (!fastest)
{
if (AddString(input, inputCount, inputSize, ref offset, ref count))
{
FindLongestMatch(input, inputCount, inputSize, ref offset, ref count);
}
}
}
if (fastest)
{
AddString(input, inputCount, inputSize, ref offset, ref count);
}
}

if (literalCount > 0)
{
outputCount += WriteCount(output, literalCount, 0);
output.AddRange(input.GetRange((int)literalStart, (int)literalCount));
outputCount += literalCount;
}

outputSize = outputCount;
}

/// <summary>
/// Converts a <see cref="List{T}"/> of bytes to a compressed <see cref="byte"/> array.
/// <para>Note: This will ONLY write <c>Little Endian</c> files.</para>
/// </summary>
/// <param name="input">The list of bytes to compress.</param>
/// <param name="fast">If <c>true</c>, a slightly faster algorithm will be used.<para>Defaults to <c>false</c>.</para></param>
/// <returns>A <see cref="byte"/> array containing the compressed bytes of <paramref name="input"/>.</returns>
/// <exception cref="InvalidDataException">Thrown if <paramref name="input"/> is not a Little Endian Pure3D file.</exception>
public static byte[] CompressFile(List<byte> input, bool fast = false)
{
uint identifier = BitConverter.ToUInt32(input.Take(4).ToArray(), 0);
if (identifier != P3DFile.SIGNATURE)
throw new InvalidDataException("The specified file is not a Little Endian P3D file.");

uint fileSize = (uint)input.Count;

List<byte> output = [];
output.AddRange(BitConverter.GetBytes(P3DFile.COMPRESSED_SIGNATURE));
output.AddRange(BitConverter.GetBytes(fileSize));

uint remaining = fileSize;

uint start = 0;
while (remaining > 0)
{
uint size = Math.Min(LZR_BLOCK_SIZE, remaining);
List<byte> comp = new((int)size);
uint compSize = 0;

CompressBlock(input.Skip((int)start).ToList(), size, comp, ref compSize, fast);

output.AddRange(BitConverter.GetBytes(compSize));
output.AddRange(BitConverter.GetBytes(size));
output.AddRange(comp);

start += size;
remaining -= size;
}

return [.. output];
}

/// <summary>
/// Converts a <see cref="P3DFile"/> to a compressed <see cref="byte"/> array.
/// <para>Note: This will ONLY write <c>Little Endian</c> files.</para>
/// </summary>
/// <param name="file">The file to compress.</param>
/// <param name="includeHistory">If <c>true</c>, a history chunk will be added to the start to indicate how and when it was compressed.<para>Defaults to <c>true</c>.</para></param>
/// <param name="fast">If <c>true</c>, a slightly faster algorithm will be used.<para>Defaults to <c>false</c>.</para></param>
/// <returns>A <see cref="byte"/> array containing the compressed bytes of <paramref name="file"/>.</returns>
public static byte[] CompressFile(P3DFile file, bool includeHistory = true, bool fast = false)
{
if (includeHistory)
file.Chunks.Insert(0, new HistoryChunk(["Compressed with NetP3DLib", $"Run at {DateTime.Now:R}"]));

using var stream = new MemoryStream();
file.Write(stream, Endianness.Little);
List<byte> input = stream.ToArray().ToList();

return CompressFile(input, fast);
}

private static void InitTree()
{
tree[TREE_ROOT].Larger = 0;
tree[0].Parent = TREE_ROOT;
tree[0].Larger = UNUSED;
tree[0].Smaller = UNUSED;
}

private static bool AddString(List<byte> input, uint inputCount, uint inputSize, ref uint offset, ref uint count)
{
uint testNode = tree[TREE_ROOT].Larger;
bool failure = false;
count = 0;

while (true)
{
uint i;
int delta = 0;

for (i = 0; i < inputSize - inputCount; i++)
{
delta = input[(int)(inputCount + i)] - input[(int)(testNode + i)];
if (delta != 0)
{
break;
}
}

if (i > count)
{
if (((inputCount - testNode) & 15) == 0)
{
failure = true;
}
else
{
count = i;
offset = inputCount - testNode;
}
}

ref uint child = ref (delta >= 0 ? ref tree[testNode].Larger : ref tree[testNode].Smaller);
if (child == UNUSED)
{
child = inputCount;
tree[inputCount].Parent = testNode;
tree[inputCount].Larger = UNUSED;
tree[inputCount].Smaller = UNUSED;
return failure;
}

testNode = child;
}
}

private static void FindLongestMatch(List<byte> input, uint inputCount, uint inputSize, ref uint offset, ref uint count)
{
offset = 0;
count = 0;

for (uint o = 0; o < inputCount; o++)
{
if (((o - inputCount) & 0xf) == 0)
{
continue;
}

uint c;
for (c = 0; (c < inputSize - o) && (c + inputCount < inputSize); c++)
{
if (input[(int)(o + c)] == input[(int)(inputCount + c)])
{
if (c + 1 > count)
{
count = c + 1;
offset = o;
}
}
else
{
break;
}
}
}

offset = inputCount - offset;
}

private static uint WriteCount(List<byte> output, uint count, byte highBits)
{
uint outCount = 0;
if (count > 15)
{
output.Add(highBits);
outCount++;
count -= 15;
while (count > 255)
{
output.Add(0);
outCount++;
count -= 255;
}
output.Add((byte)count);
outCount++;
}
else
{
output.Add((byte)(highBits | count));
outCount++;
}
return outCount;
}
}
15 changes: 15 additions & 0 deletions NetP3DLib/NetP3DLib/P3D/P3DFile.cs
Original file line number Diff line number Diff line change
Expand Up @@ -222,4 +222,19 @@ public void Write(Stream stream, Endianness endianness)
foreach (Chunk chunk in Chunks)
chunk.Write(bw);
}

public void Compress(string filePath, bool includeHistory = true)
{
using FileStream fs = File.Open(filePath, FileMode.Create, FileAccess.ReadWrite, FileShare.None);
Compress(fs, includeHistory);
}

public void Compress(Stream stream, bool includeHistory = true)
{
if (!stream.CanWrite)
throw new ArgumentException("Cannot write to stream.", nameof(stream));

var compressedBytes = LZR_Compression.CompressFile(this, includeHistory);
stream.Write(compressedBytes, 0, compressedBytes.Length);
}
}

0 comments on commit a2cdd96

Please sign in to comment.