mirror of
https://gitlab.com/Syroot/Worms.git
synced 2025-03-04 09:25:22 +03:00
Clone original WWPA compression as close as possible due to bug found in original implementation.
This commit is contained in:
parent
197a958a98
commit
2d5fa21c00
@ -248,6 +248,7 @@ namespace Syroot.Worms.Mgame.GameServer.Packets
|
|||||||
{
|
{
|
||||||
// Retrieve (decompressed) data size.
|
// Retrieve (decompressed) data size.
|
||||||
ReadOnlySpan<byte> decompressedData = _packetDataStream.GetSpan();
|
ReadOnlySpan<byte> decompressedData = _packetDataStream.GetSpan();
|
||||||
|
decompressedData = File.ReadAllBytes(@"D:\Pictures\data.bin");
|
||||||
ReadOnlySpan<byte> compressedData = PacketCompression.Compress(decompressedData);
|
ReadOnlySpan<byte> compressedData = PacketCompression.Compress(decompressedData);
|
||||||
|
|
||||||
// Send head.
|
// Send head.
|
||||||
|
@ -6,71 +6,119 @@ namespace Syroot.Worms.Mgame.GameServer.Packets.WorldPartyAqua
|
|||||||
/// Represents packet data encryption as used in WWPA. Note that most code is a direct translation from the original
|
/// Represents packet data encryption as used in WWPA. Note that most code is a direct translation from the original
|
||||||
/// game assembly.
|
/// game assembly.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// The original implementation has the following bugs caused by specific, but rare patterns (in 1000 random blobs
|
||||||
|
/// of sizes between 1 and 1000 bytes, ~0.5% were decompressed badly):
|
||||||
|
/// - Bad calculation of decompressed bytes. The same wrong bytes are calculated by this implementation. Such data
|
||||||
|
/// may be discarded by the game as soon as it causes the final length to differ.
|
||||||
|
/// - Bad negative source buffer indices cause out-of-bounds memory to be written. Combatted in this implementation
|
||||||
|
/// by ignoring such writes and keeping 0 bytes instead.
|
||||||
|
/// </remarks>
|
||||||
public static class PacketCompression
|
public static class PacketCompression
|
||||||
{
|
{
|
||||||
// ---- FIELDS -------------------------------------------------------------------------------------------------
|
// ---- FIELDS -------------------------------------------------------------------------------------------------
|
||||||
|
|
||||||
|
private static int _numBytesTransferred = 0;
|
||||||
|
|
||||||
|
private static int _field_4 = 4;
|
||||||
|
private static int _field_8 = 0;
|
||||||
|
|
||||||
|
private static int _field_C = 0;
|
||||||
|
private static int _field_10 = 0;
|
||||||
|
|
||||||
|
private static int[] _bufferDwords = new int[512];
|
||||||
|
|
||||||
private static byte[] _buffer = new byte[256];
|
private static byte[] _buffer = new byte[256];
|
||||||
private static int _cursor = 0;
|
private static int _bufferCursor = 0;
|
||||||
|
|
||||||
// ---- METHODS (INTERNAL) -------------------------------------------------------------------------------------
|
// ---- METHODS (INTERNAL) -------------------------------------------------------------------------------------
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Compresses the given <paramref name="decompressed"/> data.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="decompressed">The data to compress.</param>
|
||||||
|
/// <returns>The compressed data.</returns>
|
||||||
public static ReadOnlySpan<byte> Compress(ReadOnlySpan<byte> decompressed)
|
public static ReadOnlySpan<byte> Compress(ReadOnlySpan<byte> decompressed)
|
||||||
{
|
{
|
||||||
Compressor compressor = new Compressor();
|
Compressor compressor = new Compressor();
|
||||||
|
int idx;
|
||||||
|
int bytesToRepeat;
|
||||||
|
int idxDword;
|
||||||
|
int i;
|
||||||
|
int offset;
|
||||||
|
int v11;
|
||||||
|
int v12;
|
||||||
|
int field_8;
|
||||||
|
int field_10;
|
||||||
|
byte c;
|
||||||
|
int shiftValue1;
|
||||||
|
|
||||||
int idx = 0;
|
idx = 0;
|
||||||
while (idx < decompressed.Length)
|
while (idx < decompressed.Length)
|
||||||
{
|
{
|
||||||
int value1 = 0;
|
bytesToRepeat = idx - 0xFF;
|
||||||
int value2 = -1;
|
if (idx - 0xFF < 0)
|
||||||
int bytesToRepeat = Math.Max(0, idx - 0xFF);
|
bytesToRepeat = 0;
|
||||||
|
idxDword = 0;
|
||||||
|
shiftValue1 = -1;
|
||||||
if (bytesToRepeat >= idx)
|
if (bytesToRepeat >= idx)
|
||||||
{
|
{
|
||||||
Write(compressor, decompressed[idx++]);
|
c = decompressed[idx++];
|
||||||
|
Write(compressor, c);
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
do
|
do
|
||||||
{
|
{
|
||||||
int i;
|
for (i = idx; i < decompressed.Length; ++i)
|
||||||
for (i = idx; i < decompressed.Length; i++)
|
|
||||||
{
|
{
|
||||||
if (i - idx >= 0x111
|
if (i - idx >= 0x111)
|
||||||
|| decompressed[bytesToRepeat] != decompressed[i])
|
|
||||||
break;
|
break;
|
||||||
bytesToRepeat++;
|
if (decompressed[bytesToRepeat] != decompressed[i])
|
||||||
|
break;
|
||||||
|
++bytesToRepeat;
|
||||||
}
|
}
|
||||||
int offset = i - idx;
|
offset = idx - i;
|
||||||
if (offset >= 3 && offset > value1)
|
v11 = i - idx;
|
||||||
|
v12 = offset + bytesToRepeat;
|
||||||
|
if (v11 >= 3 && v11 > idxDword)
|
||||||
{
|
{
|
||||||
value1 = offset;
|
idxDword = v11;
|
||||||
value2 = idx - 12;
|
shiftValue1 = idx - 12;
|
||||||
if (offset == 0x111)
|
if (v11 == 0x111)
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
bytesToRepeat += idx - i + 1;
|
bytesToRepeat = v12 + 1;
|
||||||
} while (bytesToRepeat < idx);
|
}
|
||||||
if (value1 != 0)
|
while (bytesToRepeat < idx);
|
||||||
|
if (idxDword != 0)
|
||||||
{
|
{
|
||||||
TransferBuffer(compressor);
|
TransferBuffer(compressor);
|
||||||
compressor.Compress(true);
|
compressor.Compress(true);
|
||||||
if (value1 >= 18)
|
if (idxDword >= 18)
|
||||||
{
|
{
|
||||||
compressor.Shift(0, 4);
|
compressor.Shift(0, 4);
|
||||||
compressor.Shift(value2, 8);
|
compressor.Shift(shiftValue1, 8);
|
||||||
compressor.Shift(value1 - 18, 8);
|
compressor.Shift(idxDword - 18, 8);
|
||||||
|
field_10 = _field_10;
|
||||||
|
++_field_C;
|
||||||
|
_field_10 = idxDword - 3 + field_10;
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
compressor.Shift(value1 - 2, 4);
|
compressor.Shift(idxDword - 2, 4);
|
||||||
compressor.Shift(value2 - 1, 8);
|
compressor.Shift(shiftValue1 - 1, 8);
|
||||||
|
field_8 = _field_8;
|
||||||
|
++_field_4;
|
||||||
|
_field_8 = idxDword - 2 + field_8;
|
||||||
}
|
}
|
||||||
idx += value1;
|
idx += idxDword;
|
||||||
|
++_bufferDwords[idxDword];
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
Write(compressor, decompressed[idx++]);
|
c = decompressed[idx++];
|
||||||
|
Write(compressor, c);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -79,17 +127,22 @@ namespace Syroot.Worms.Mgame.GameServer.Packets.WorldPartyAqua
|
|||||||
compressor.Shift(0, 4);
|
compressor.Shift(0, 4);
|
||||||
compressor.Shift(0, 8);
|
compressor.Shift(0, 8);
|
||||||
|
|
||||||
return compressor.Buffer.AsSpan(0, compressor.Cursor);
|
return compressor.Compressed.AsSpan(0, compressor.Cursor);
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public static ReadOnlySpan<byte> Decompress(ReadOnlySpan<byte> compressed)
|
public static ReadOnlySpan<byte> Decompress(ReadOnlySpan<byte> compressed)
|
||||||
{
|
{
|
||||||
Decompressor decompressor = new Decompressor(compressed);
|
Decompressor decompressor = new Decompressor(compressed);
|
||||||
byte[] decompressed = new byte[2048];
|
byte[] decompressed = new byte[2048];
|
||||||
|
int idx;
|
||||||
int bytesToCopy;
|
|
||||||
int bytesToCopyRemain;
|
int bytesToCopyRemain;
|
||||||
int idx = 0;
|
int idxCopySrc;
|
||||||
|
int v12;
|
||||||
|
int v13;
|
||||||
|
int bytesToCopy;
|
||||||
|
|
||||||
|
idx = 0;
|
||||||
while (true)
|
while (true)
|
||||||
{
|
{
|
||||||
while (true)
|
while (true)
|
||||||
@ -99,42 +152,44 @@ namespace Syroot.Worms.Mgame.GameServer.Packets.WorldPartyAqua
|
|||||||
bytesToCopy = decompressor.GetBytesToCopy(8);
|
bytesToCopy = decompressor.GetBytesToCopy(8);
|
||||||
if (bytesToCopy != -1)
|
if (bytesToCopy != -1)
|
||||||
{
|
{
|
||||||
bytesToCopy++;
|
++bytesToCopy;
|
||||||
do
|
do
|
||||||
{
|
{
|
||||||
decompressed[idx++] = decompressor.Read();
|
decompressed[idx++] = decompressor.Read();
|
||||||
bytesToCopy--;
|
--bytesToCopy;
|
||||||
} while (bytesToCopy != 0);
|
} while (bytesToCopy != 0);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (decompressor.GetBytesToCopy(4) + 2 <= 2)
|
bytesToCopy = decompressor.GetBytesToCopy(4) + 2;
|
||||||
|
if (bytesToCopy <= 2)
|
||||||
break;
|
break;
|
||||||
bytesToCopy = decompressor.GetBytesToCopy(8) + 1;
|
bytesToCopy = decompressor.GetBytesToCopy(8) + 1;
|
||||||
bytesToCopyRemain = bytesToCopy - 1;
|
bytesToCopyRemain = bytesToCopy - 1;
|
||||||
if (bytesToCopy != 0)
|
if (bytesToCopy != 0)
|
||||||
{
|
{
|
||||||
bytesToCopy++;
|
++bytesToCopyRemain;
|
||||||
do
|
do
|
||||||
{
|
{
|
||||||
int idxCopySrc = idx++ - bytesToCopy;
|
idxCopySrc = idx++ - bytesToCopy;
|
||||||
bytesToCopyRemain--;
|
--bytesToCopyRemain;
|
||||||
decompressed[idx - 1] = decompressed[idxCopySrc];
|
if (idxCopySrc >= 0) // Bugfix: Original implementation may calculate negative indices.
|
||||||
|
decompressed[idx - 1] = decompressed[idxCopySrc];
|
||||||
} while (bytesToCopyRemain != 0);
|
} while (bytesToCopyRemain != 0);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
bytesToCopy = decompressor.GetBytesToCopy(8);
|
bytesToCopy = decompressor.GetBytesToCopy(8);
|
||||||
if (bytesToCopy == 0)
|
if (bytesToCopy == 0)
|
||||||
break;
|
break;
|
||||||
int v12 = decompressor.GetBytesToCopy(8);
|
v12 = decompressor.GetBytesToCopy(8);
|
||||||
int v13 = v12 + 18;
|
v13 = v12 + 18;
|
||||||
bytesToCopyRemain = v12 + 17;
|
bytesToCopyRemain = v12 + 17;
|
||||||
if (v13 != 0)
|
if (v13 != 0)
|
||||||
{
|
{
|
||||||
bytesToCopyRemain++;
|
++bytesToCopyRemain;
|
||||||
do
|
do
|
||||||
{
|
{
|
||||||
int idxCopySrc = idx++ - bytesToCopy;
|
idxCopySrc = idx++ - bytesToCopy;
|
||||||
bytesToCopyRemain--;
|
--bytesToCopyRemain;
|
||||||
decompressed[idx - 1] = decompressed[idxCopySrc];
|
decompressed[idx - 1] = decompressed[idxCopySrc];
|
||||||
} while (bytesToCopyRemain != 0);
|
} while (bytesToCopyRemain != 0);
|
||||||
}
|
}
|
||||||
@ -146,20 +201,25 @@ namespace Syroot.Worms.Mgame.GameServer.Packets.WorldPartyAqua
|
|||||||
|
|
||||||
private static void TransferBuffer(Compressor compressor)
|
private static void TransferBuffer(Compressor compressor)
|
||||||
{
|
{
|
||||||
if (_cursor != 0)
|
int bufferCursor;
|
||||||
|
int i;
|
||||||
|
|
||||||
|
bufferCursor = _bufferCursor;
|
||||||
|
if (bufferCursor != 0)
|
||||||
{
|
{
|
||||||
|
_numBytesTransferred += bufferCursor;
|
||||||
compressor.Compress(false);
|
compressor.Compress(false);
|
||||||
compressor.Shift(_cursor - 1, 8);
|
compressor.Shift(_bufferCursor - 1, 8);
|
||||||
for (int i = 0; i < _cursor; i++)
|
for (i = 0; i < _bufferCursor; ++i)
|
||||||
compressor.Buffer[compressor.Cursor++] = _buffer[i];
|
compressor.Write(_buffer[i]);
|
||||||
_cursor = 0;
|
_bufferCursor = 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void Write(Compressor compressor, byte c)
|
private static void Write(Compressor compressor, byte c)
|
||||||
{
|
{
|
||||||
_buffer[_cursor++] = c;
|
_buffer[_bufferCursor++] = c;
|
||||||
if (_cursor >= _buffer.Length)
|
if (_bufferCursor >= _buffer.Length)
|
||||||
TransferBuffer(compressor);
|
TransferBuffer(compressor);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -167,28 +227,41 @@ namespace Syroot.Worms.Mgame.GameServer.Packets.WorldPartyAqua
|
|||||||
|
|
||||||
private class Compressor
|
private class Compressor
|
||||||
{
|
{
|
||||||
internal byte[] Buffer = new byte[1024];
|
internal byte[] Compressed = new byte[1024];
|
||||||
internal int Cursor = 1;
|
internal int Cursor = 1;
|
||||||
|
|
||||||
private int _cursorCompress = 0;
|
private int _cursorCompress = 0;
|
||||||
private int _shift = 0;
|
private int _shift = 0;
|
||||||
|
|
||||||
internal void Compress(bool doShift)
|
internal void Compress(bool bShift)
|
||||||
{
|
{
|
||||||
if (doShift)
|
int shift;
|
||||||
Buffer[_cursorCompress] |= (byte)(0x80 >> _shift);
|
int cursor;
|
||||||
if (++_shift == 8)
|
int cursorCompress;
|
||||||
|
|
||||||
|
if (bShift)
|
||||||
{
|
{
|
||||||
|
Compressed[_cursorCompress] |= (byte)(0x80 >> _shift);
|
||||||
|
}
|
||||||
|
shift = _shift + 1;
|
||||||
|
_shift = shift;
|
||||||
|
if (shift == 8)
|
||||||
|
{
|
||||||
|
cursor = Cursor;
|
||||||
_cursorCompress = Cursor++;
|
_cursorCompress = Cursor++;
|
||||||
Buffer[_cursorCompress] = 0;
|
cursorCompress = _cursorCompress;
|
||||||
|
Cursor = cursor + 1;
|
||||||
|
Compressed[cursorCompress] = 0;
|
||||||
_shift = 0;
|
_shift = 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
internal void Shift(int value, int shiftPlus1)
|
internal void Shift(int value, int shiftPlus1)
|
||||||
{
|
{
|
||||||
int prevShift = 0;
|
int shift;
|
||||||
int shift = shiftPlus1 - 1;
|
int prevShift;
|
||||||
|
|
||||||
|
shift = shiftPlus1 - 1;
|
||||||
if (shiftPlus1 != 0)
|
if (shiftPlus1 != 0)
|
||||||
{
|
{
|
||||||
do
|
do
|
||||||
@ -198,11 +271,16 @@ namespace Syroot.Worms.Mgame.GameServer.Packets.WorldPartyAqua
|
|||||||
} while (prevShift != 0);
|
} while (prevShift != 0);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
internal void Write(byte c)
|
||||||
|
{
|
||||||
|
Compressed[Cursor++] = c;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private class Decompressor
|
private class Decompressor
|
||||||
{
|
{
|
||||||
private byte[] _compressed;
|
private readonly byte[] _compressed;
|
||||||
private int _compressedCursor = 0;
|
private int _compressedCursor = 0;
|
||||||
private byte _field_C;
|
private byte _field_C;
|
||||||
private byte _field_D;
|
private byte _field_D;
|
||||||
@ -212,7 +290,7 @@ namespace Syroot.Worms.Mgame.GameServer.Packets.WorldPartyAqua
|
|||||||
_compressed = compressed.ToArray();
|
_compressed = compressed.ToArray();
|
||||||
byte c = Read();
|
byte c = Read();
|
||||||
_field_C = (byte)(2 * c | 1);
|
_field_C = (byte)(2 * c | 1);
|
||||||
_field_D = (byte)(c >> 7);
|
_field_D = (byte)((uint)c >> 7);
|
||||||
}
|
}
|
||||||
|
|
||||||
internal byte Read()
|
internal byte Read()
|
||||||
|
65
src/Syroot.Worms.Mgame.Test/PacketCompressionTests.cs
Normal file
65
src/Syroot.Worms.Mgame.Test/PacketCompressionTests.cs
Normal file
@ -0,0 +1,65 @@
|
|||||||
|
using System;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Security.Cryptography;
|
||||||
|
using Microsoft.VisualStudio.TestTools.UnitTesting;
|
||||||
|
|
||||||
|
namespace Syroot.Worms.Mgame.Test
|
||||||
|
{
|
||||||
|
[TestClass]
|
||||||
|
public class PacketCompressionTests
|
||||||
|
{
|
||||||
|
// ---- FIELDS -------------------------------------------------------------------------------------------------
|
||||||
|
|
||||||
|
// Known to cause 3 wrong bytes:
|
||||||
|
private static readonly byte[] _origBadBytes = new byte[] { 0x68, 0x56, 0xF9, 0x1D, 0x39, 0x5F, 0xD0, 0x57, 0x13, 0x9B, 0xF3, 0x13, 0xF5, 0x97, 0x41, 0xD0, 0x57, 0x13, 0x1E, 0x6C, 0x4A };
|
||||||
|
private static readonly byte[] _rcmpBadBytes = new byte[] { 0x68, 0x56, 0xF9, 0x1D, 0x39, 0x5F, 0xD0, 0x57, 0x13, 0x9B, 0xF3, 0x13, 0xF5, 0x97, 0x41, 0xF5, 0x97, 0x41, 0x1E, 0x6C, 0x4A };
|
||||||
|
|
||||||
|
// Known to cause negative index bug writing undefined bytes:
|
||||||
|
private static readonly byte[] _origBadIndex = new byte[] { 0xFC, 0xFC, 0xC2, 0x92, 0x8F, 0x66, 0x33, 0x44, 0xEF, 0x40, 0xE9, 0xAB, 0x44, 0xEF, 0x40, 0x36 };
|
||||||
|
private static readonly byte[] _rcmpBadIndex = new byte[] { 0xFC, 0xFC, 0xC2, 0x92, 0x8F, 0x66, 0x33, 0x44, 0xEF, 0x40, 0xE9, 0xAB, 0x00, 0x00, 0x00, 0x36 };
|
||||||
|
|
||||||
|
// ---- METHODS (PUBLIC) ---------------------------------------------------------------------------------------
|
||||||
|
|
||||||
|
[TestMethod]
|
||||||
|
public void TestBadBytes()
|
||||||
|
{
|
||||||
|
Assert.IsTrue(Recompress(_origBadBytes).SequenceEqual(_rcmpBadBytes));
|
||||||
|
}
|
||||||
|
|
||||||
|
[TestMethod]
|
||||||
|
public void TestBadIndex()
|
||||||
|
{
|
||||||
|
var recompressed = Recompress(_origBadIndex);
|
||||||
|
Assert.IsTrue(recompressed.SequenceEqual(_rcmpBadIndex));
|
||||||
|
}
|
||||||
|
|
||||||
|
[TestMethod]
|
||||||
|
public void TestRandom()
|
||||||
|
{
|
||||||
|
const int runs = 1000;
|
||||||
|
int fails = 0;
|
||||||
|
Random random = new Random();
|
||||||
|
using (RandomNumberGenerator rng = RandomNumberGenerator.Create())
|
||||||
|
{
|
||||||
|
for (int i = 0; i < runs; i++)
|
||||||
|
{
|
||||||
|
byte[] data = new byte[random.Next(1, 1000)];
|
||||||
|
rng.GetBytes(data);
|
||||||
|
ReadOnlySpan<byte> recompressed = Recompress(data);
|
||||||
|
if (!data.AsSpan().SequenceEqual(recompressed))
|
||||||
|
fails++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// If less than 1% fails, the compression is seen as working as in the original implementation.
|
||||||
|
Assert.IsTrue(fails / (float)runs < 0.01);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- METHODS (PRIVATE) --------------------------------------------------------------------------------------
|
||||||
|
|
||||||
|
private static ReadOnlySpan<byte> Recompress(ReadOnlySpan<byte> data)
|
||||||
|
{
|
||||||
|
ReadOnlySpan<byte> compressed = PacketCompression.Compress(data);
|
||||||
|
return PacketCompression.Decompress(compressed);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
12
src/Syroot.Worms.Mgame.Test/Syroot.Worms.Mgame.Test.csproj
Normal file
12
src/Syroot.Worms.Mgame.Test/Syroot.Worms.Mgame.Test.csproj
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
<PropertyGroup>
|
||||||
|
<TargetFramework>netcoreapp2.1</TargetFramework>
|
||||||
|
<IsPackable>false</IsPackable>
|
||||||
|
</PropertyGroup>
|
||||||
|
<ItemGroup>
|
||||||
|
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="15.9.0" />
|
||||||
|
<PackageReference Include="MSTest.TestAdapter" Version="1.3.2" />
|
||||||
|
<PackageReference Include="MSTest.TestFramework" Version="1.3.2" />
|
||||||
|
<ProjectReference Include="..\Syroot.Worms.Mgame\Syroot.Worms.Mgame.csproj" />
|
||||||
|
</ItemGroup>
|
||||||
|
</Project>
|
332
src/Syroot.Worms.Mgame/PacketCompression.cs
Normal file
332
src/Syroot.Worms.Mgame/PacketCompression.cs
Normal file
@ -0,0 +1,332 @@
|
|||||||
|
using System;
|
||||||
|
|
||||||
|
namespace Syroot.Worms.Mgame
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Represents packet data encryption as used in WWPA. Note that most code is a direct translation from the original
|
||||||
|
/// game assembly.
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// The original implementation has the following bugs caused by specific, but rare patterns (in 1000 random blobs
|
||||||
|
/// of sizes between 1 and 1000 bytes, ~0.5% were decompressed badly):
|
||||||
|
/// - Bad calculation of decompressed bytes. The same wrong bytes are calculated by this implementation.
|
||||||
|
/// - Bad negative source buffer indices cause out-of-bounds memory to be written. Combatted in this implementation
|
||||||
|
/// by ignoring such writes and keeping 0 bytes instead.
|
||||||
|
/// The game discards decompression results of a length longer than specified in the packet header, but does not
|
||||||
|
/// check the data itself if the length is still fine.
|
||||||
|
/// </remarks>
|
||||||
|
public static class PacketCompression
|
||||||
|
{
|
||||||
|
// ---- FIELDS -------------------------------------------------------------------------------------------------
|
||||||
|
|
||||||
|
private static int _numBytesTransferred = 0;
|
||||||
|
|
||||||
|
private static int _field_4 = 4;
|
||||||
|
private static int _field_8 = 0;
|
||||||
|
|
||||||
|
private static int _field_C = 0;
|
||||||
|
private static int _field_10 = 0;
|
||||||
|
|
||||||
|
private static int[] _bufferDwords = new int[512];
|
||||||
|
|
||||||
|
private static byte[] _buffer = new byte[256];
|
||||||
|
private static int _bufferCursor = 0;
|
||||||
|
|
||||||
|
// ---- METHODS (INTERNAL) -------------------------------------------------------------------------------------
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Compresses the given <paramref name="decompressed"/> data.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="decompressed">The data to compress.</param>
|
||||||
|
/// <returns>The compressed data.</returns>
|
||||||
|
public static ReadOnlySpan<byte> Compress(ReadOnlySpan<byte> decompressed)
|
||||||
|
{
|
||||||
|
Compressor compressor = new Compressor();
|
||||||
|
int idx;
|
||||||
|
int bytesToRepeat;
|
||||||
|
int idxDword;
|
||||||
|
int i;
|
||||||
|
int offset;
|
||||||
|
int v11;
|
||||||
|
int v12;
|
||||||
|
int field_8;
|
||||||
|
int field_10;
|
||||||
|
byte c;
|
||||||
|
int shiftValue1;
|
||||||
|
|
||||||
|
idx = 0;
|
||||||
|
while (idx < decompressed.Length)
|
||||||
|
{
|
||||||
|
bytesToRepeat = idx - 0xFF;
|
||||||
|
if (idx - 0xFF < 0)
|
||||||
|
bytesToRepeat = 0;
|
||||||
|
idxDword = 0;
|
||||||
|
shiftValue1 = -1;
|
||||||
|
if (bytesToRepeat >= idx)
|
||||||
|
{
|
||||||
|
c = decompressed[idx++];
|
||||||
|
Write(compressor, c);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
do
|
||||||
|
{
|
||||||
|
for (i = idx; i < decompressed.Length; ++i)
|
||||||
|
{
|
||||||
|
if (i - idx >= 0x111)
|
||||||
|
break;
|
||||||
|
if (decompressed[bytesToRepeat] != decompressed[i])
|
||||||
|
break;
|
||||||
|
++bytesToRepeat;
|
||||||
|
}
|
||||||
|
offset = idx - i;
|
||||||
|
v11 = i - idx;
|
||||||
|
v12 = offset + bytesToRepeat;
|
||||||
|
if (v11 >= 3 && v11 > idxDword)
|
||||||
|
{
|
||||||
|
idxDword = v11;
|
||||||
|
shiftValue1 = idx - 12;
|
||||||
|
if (v11 == 0x111)
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
bytesToRepeat = v12 + 1;
|
||||||
|
}
|
||||||
|
while (bytesToRepeat < idx);
|
||||||
|
if (idxDword != 0)
|
||||||
|
{
|
||||||
|
TransferBuffer(compressor);
|
||||||
|
compressor.Compress(true);
|
||||||
|
if (idxDword >= 18)
|
||||||
|
{
|
||||||
|
compressor.Shift(0, 4);
|
||||||
|
compressor.Shift(shiftValue1, 8);
|
||||||
|
compressor.Shift(idxDword - 18, 8);
|
||||||
|
field_10 = _field_10;
|
||||||
|
++_field_C;
|
||||||
|
_field_10 = idxDword - 3 + field_10;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
compressor.Shift(idxDword - 2, 4);
|
||||||
|
compressor.Shift(shiftValue1 - 1, 8);
|
||||||
|
field_8 = _field_8;
|
||||||
|
++_field_4;
|
||||||
|
_field_8 = idxDword - 2 + field_8;
|
||||||
|
}
|
||||||
|
idx += idxDword;
|
||||||
|
++_bufferDwords[idxDword];
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
c = decompressed[idx++];
|
||||||
|
Write(compressor, c);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
TransferBuffer(compressor);
|
||||||
|
compressor.Compress(true);
|
||||||
|
compressor.Shift(0, 4);
|
||||||
|
compressor.Shift(0, 8);
|
||||||
|
|
||||||
|
return compressor.Compressed.AsSpan(0, compressor.Cursor);
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
public static ReadOnlySpan<byte> Decompress(ReadOnlySpan<byte> compressed)
|
||||||
|
{
|
||||||
|
Decompressor decompressor = new Decompressor(compressed);
|
||||||
|
byte[] decompressed = new byte[2048];
|
||||||
|
int idx;
|
||||||
|
int v5;
|
||||||
|
int v6;
|
||||||
|
int idxCopySrc;
|
||||||
|
int v11;
|
||||||
|
int v12;
|
||||||
|
int v13;
|
||||||
|
int bytesToCopy;
|
||||||
|
|
||||||
|
idx = 0;
|
||||||
|
while (true)
|
||||||
|
{
|
||||||
|
while (true)
|
||||||
|
{
|
||||||
|
while (decompressor.sub_4C0BA0() == 0)
|
||||||
|
{
|
||||||
|
bytesToCopy = decompressor.GetBytesToCopy(8);
|
||||||
|
if (bytesToCopy != -1)
|
||||||
|
{
|
||||||
|
++bytesToCopy;
|
||||||
|
do
|
||||||
|
{
|
||||||
|
decompressed[idx++] = decompressor.Read();
|
||||||
|
--bytesToCopy;
|
||||||
|
} while (bytesToCopy != 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
v5 = decompressor.GetBytesToCopy(4) + 2;
|
||||||
|
if (v5 <= 2)
|
||||||
|
break;
|
||||||
|
v6 = decompressor.GetBytesToCopy(8) + 1;
|
||||||
|
bytesToCopy = v5 - 1;
|
||||||
|
if (v5 != 0)
|
||||||
|
{
|
||||||
|
++bytesToCopy;
|
||||||
|
do
|
||||||
|
{
|
||||||
|
idxCopySrc = idx++ - v6;
|
||||||
|
--bytesToCopy;
|
||||||
|
if (idxCopySrc >= 0) // Bugfix: Original implementation may calculate negative indices.
|
||||||
|
decompressed[idx - 1] = decompressed[idxCopySrc];
|
||||||
|
} while (bytesToCopy != 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
v11 = decompressor.GetBytesToCopy(8);
|
||||||
|
if (v11 == 0)
|
||||||
|
break;
|
||||||
|
v12 = decompressor.GetBytesToCopy(8);
|
||||||
|
v13 = v12 + 18;
|
||||||
|
bytesToCopy = v12 + 17;
|
||||||
|
if (v13 != 0)
|
||||||
|
{
|
||||||
|
++bytesToCopy;
|
||||||
|
do
|
||||||
|
{
|
||||||
|
idxCopySrc = idx++ - v11;
|
||||||
|
--bytesToCopy;
|
||||||
|
decompressed[idx - 1] = decompressed[idxCopySrc];
|
||||||
|
} while (bytesToCopy != 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return decompressed.AsSpan(0, idx);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- METHODS (PRIVATE) --------------------------------------------------------------------------------------
|
||||||
|
|
||||||
|
private static void TransferBuffer(Compressor compressor)
|
||||||
|
{
|
||||||
|
int bufferCursor;
|
||||||
|
int i;
|
||||||
|
|
||||||
|
bufferCursor = _bufferCursor;
|
||||||
|
if (bufferCursor != 0)
|
||||||
|
{
|
||||||
|
_numBytesTransferred += bufferCursor;
|
||||||
|
compressor.Compress(false);
|
||||||
|
compressor.Shift(_bufferCursor - 1, 8);
|
||||||
|
for (i = 0; i < _bufferCursor; ++i)
|
||||||
|
compressor.Write(_buffer[i]);
|
||||||
|
_bufferCursor = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void Write(Compressor compressor, byte c)
|
||||||
|
{
|
||||||
|
_buffer[_bufferCursor++] = c;
|
||||||
|
if (_bufferCursor >= _buffer.Length)
|
||||||
|
TransferBuffer(compressor);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- CLASSES, STRUCTS & ENUMS -------------------------------------------------------------------------------
|
||||||
|
|
||||||
|
private class Compressor
|
||||||
|
{
|
||||||
|
internal byte[] Compressed = new byte[1024];
|
||||||
|
internal int Cursor = 1;
|
||||||
|
|
||||||
|
private int _cursorCompress = 0;
|
||||||
|
private int _shift = 0;
|
||||||
|
|
||||||
|
internal void Compress(bool bShift)
|
||||||
|
{
|
||||||
|
int shift;
|
||||||
|
int cursor;
|
||||||
|
int cursorCompress;
|
||||||
|
|
||||||
|
if (bShift)
|
||||||
|
{
|
||||||
|
Compressed[_cursorCompress] |= (byte)(0x80 >> _shift);
|
||||||
|
}
|
||||||
|
shift = _shift + 1;
|
||||||
|
_shift = shift;
|
||||||
|
if (shift == 8)
|
||||||
|
{
|
||||||
|
cursor = Cursor;
|
||||||
|
_cursorCompress = Cursor++;
|
||||||
|
cursorCompress = _cursorCompress;
|
||||||
|
Cursor = cursor + 1;
|
||||||
|
Compressed[cursorCompress] = 0;
|
||||||
|
_shift = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
internal void Shift(int value, int shiftPlus1)
|
||||||
|
{
|
||||||
|
int shift;
|
||||||
|
int prevShift;
|
||||||
|
|
||||||
|
shift = shiftPlus1 - 1;
|
||||||
|
if (shiftPlus1 != 0)
|
||||||
|
{
|
||||||
|
do
|
||||||
|
{
|
||||||
|
Compress(((value >> shift) & 1) != 0);
|
||||||
|
prevShift = shift--;
|
||||||
|
} while (prevShift != 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
internal void Write(byte c)
|
||||||
|
{
|
||||||
|
Compressed[Cursor++] = c;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private class Decompressor
|
||||||
|
{
|
||||||
|
private readonly byte[] _compressed;
|
||||||
|
private int _compressedCursor = 0;
|
||||||
|
private byte _field_C;
|
||||||
|
private byte _field_D;
|
||||||
|
|
||||||
|
internal Decompressor(ReadOnlySpan<byte> compressed)
|
||||||
|
{
|
||||||
|
_compressed = compressed.ToArray();
|
||||||
|
byte c = Read();
|
||||||
|
_field_C = (byte)(2 * c | 1);
|
||||||
|
_field_D = (byte)((uint)c >> 7);
|
||||||
|
}
|
||||||
|
|
||||||
|
internal byte Read()
|
||||||
|
{
|
||||||
|
return _compressed[_compressedCursor++];
|
||||||
|
}
|
||||||
|
|
||||||
|
internal int GetBytesToCopy(int numCalls)
|
||||||
|
{
|
||||||
|
if (numCalls == 0)
|
||||||
|
return 0;
|
||||||
|
int bytesToCopy = 0;
|
||||||
|
do
|
||||||
|
{
|
||||||
|
numCalls--;
|
||||||
|
bytesToCopy = sub_4C0BA0() + 2 * bytesToCopy;
|
||||||
|
} while (numCalls != 0);
|
||||||
|
return bytesToCopy;
|
||||||
|
}
|
||||||
|
|
||||||
|
internal int sub_4C0BA0()
|
||||||
|
{
|
||||||
|
byte field_D = _field_D;
|
||||||
|
_field_D = (byte)(_field_C >> 7);
|
||||||
|
_field_C *= 2;
|
||||||
|
if (_field_C == 0)
|
||||||
|
{
|
||||||
|
byte c = Read();
|
||||||
|
_field_C = (byte)(2 * c | 1);
|
||||||
|
_field_D = (byte)(c >> 7);
|
||||||
|
}
|
||||||
|
return field_D;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -1,10 +1,7 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Diagnostics;
|
|
||||||
using System.Drawing.Imaging;
|
using System.Drawing.Imaging;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
using Syroot.BinaryData;
|
|
||||||
using Syroot.Worms.Mgame;
|
using Syroot.Worms.Mgame;
|
||||||
using Syroot.Worms.Mgame.GameServer.Packets.WorldPartyAqua;
|
|
||||||
|
|
||||||
namespace Syroot.Worms.Scratchpad
|
namespace Syroot.Worms.Scratchpad
|
||||||
{
|
{
|
||||||
@ -14,19 +11,43 @@ namespace Syroot.Worms.Scratchpad
|
|||||||
|
|
||||||
private static void Main(string[] args)
|
private static void Main(string[] args)
|
||||||
{
|
{
|
||||||
DecompressWwpaPacketData();
|
TestWwpaPacketCompressionBug();
|
||||||
|
Console.ReadLine();
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void DecompressWwpaPacketData()
|
private static void TestWwpaPacketCompressionBug()
|
||||||
{
|
{
|
||||||
byte[] decompressed = new byte[sizeof(int)];
|
ReadOnlySpan<byte> recompress(ReadOnlySpan<byte> data)
|
||||||
ByteConverter.System.GetBytes(0x8001, decompressed);
|
{
|
||||||
ReadOnlySpan<byte> compressed = PacketCompression.Compress(decompressed);
|
ReadOnlySpan<byte> compressed = PacketCompression.Compress(data);
|
||||||
Debug.Assert(compressed.SequenceEqual(new byte[] { 0x01, 0xC0, 0x01, 0x80, 0x00, 0x00, 0x00 }),
|
return PacketCompression.Decompress(compressed);
|
||||||
"Compression failed");
|
}
|
||||||
|
|
||||||
ReadOnlySpan<byte> decompressedNew = PacketCompression.Decompress(compressed);
|
bool test(byte[] data)
|
||||||
Debug.Assert(decompressedNew.SequenceEqual(decompressed), "Decompression failed");
|
{
|
||||||
|
ReadOnlySpan<byte> recompressed = recompress(data);
|
||||||
|
if (!recompressed.SequenceEqual(data))
|
||||||
|
{
|
||||||
|
Console.WriteLine("Decompression failed.");
|
||||||
|
if (recompressed.Length < data.Length)
|
||||||
|
{
|
||||||
|
Console.WriteLine($" New length smaller by {data.Length - recompressed.Length} bytes.");
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
for (int i = 0; i < data.Length; i++)
|
||||||
|
{
|
||||||
|
if (data[i] != recompressed[i])
|
||||||
|
Console.WriteLine($" Byte 0x{i:X4} differs: 0x{data[i]:X2} != 0x{recompressed[i]:X2}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Known to cause negative index bug writing undefined bytes: ??, ??, ??, ??
|
||||||
|
test(new byte[] { 0xFC, 0xFC, 0xC2, 0x92, 0x8F, 0x66, 0x33, 0x44, 0xEF, 0x40, 0xE9, 0xAB, 0x44, 0xEF, 0x40, 0x36 });
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void ConvertIgdImages()
|
private static void ConvertIgdImages()
|
||||||
|
@ -9,7 +9,6 @@
|
|||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<ProjectReference Include="..\Syroot.Worms.Armageddon.ProjectX\Syroot.Worms.Armageddon.ProjectX.csproj" />
|
<ProjectReference Include="..\Syroot.Worms.Armageddon.ProjectX\Syroot.Worms.Armageddon.ProjectX.csproj" />
|
||||||
<ProjectReference Include="..\Syroot.Worms.Armageddon\Syroot.Worms.Armageddon.csproj" />
|
<ProjectReference Include="..\Syroot.Worms.Armageddon\Syroot.Worms.Armageddon.csproj" />
|
||||||
<ProjectReference Include="..\Syroot.Worms.Mgame.GameServer\Syroot.Worms.Mgame.GameServer.csproj" />
|
|
||||||
<ProjectReference Include="..\Syroot.Worms.Mgame\Syroot.Worms.Mgame.csproj" />
|
<ProjectReference Include="..\Syroot.Worms.Mgame\Syroot.Worms.Mgame.csproj" />
|
||||||
<ProjectReference Include="..\Syroot.Worms.WorldParty\Syroot.Worms.WorldParty.csproj" />
|
<ProjectReference Include="..\Syroot.Worms.WorldParty\Syroot.Worms.WorldParty.csproj" />
|
||||||
<ProjectReference Include="..\Syroot.Worms.Worms2\Syroot.Worms.Worms2.csproj" />
|
<ProjectReference Include="..\Syroot.Worms.Worms2\Syroot.Worms.Worms2.csproj" />
|
||||||
|
@ -37,6 +37,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Syroot.Worms.Test", "Syroot
|
|||||||
EndProject
|
EndProject
|
||||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Syroot.Worms.Mgame.GameServer", "Syroot.Worms.Mgame.GameServer\Syroot.Worms.Mgame.GameServer.csproj", "{392E4CA2-61D9-4BE1-B065-8801A9F102B8}"
|
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Syroot.Worms.Mgame.GameServer", "Syroot.Worms.Mgame.GameServer\Syroot.Worms.Mgame.GameServer.csproj", "{392E4CA2-61D9-4BE1-B065-8801A9F102B8}"
|
||||||
EndProject
|
EndProject
|
||||||
|
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Syroot.Worms.Mgame.Test", "Syroot.Worms.Mgame.Test\Syroot.Worms.Mgame.Test.csproj", "{212F8090-9775-4098-BD44-9ABC01FBE553}"
|
||||||
|
EndProject
|
||||||
Global
|
Global
|
||||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||||
Debug|Any CPU = Debug|Any CPU
|
Debug|Any CPU = Debug|Any CPU
|
||||||
@ -99,6 +101,10 @@ Global
|
|||||||
{392E4CA2-61D9-4BE1-B065-8801A9F102B8}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
{392E4CA2-61D9-4BE1-B065-8801A9F102B8}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||||
{392E4CA2-61D9-4BE1-B065-8801A9F102B8}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
{392E4CA2-61D9-4BE1-B065-8801A9F102B8}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||||
{392E4CA2-61D9-4BE1-B065-8801A9F102B8}.Release|Any CPU.Build.0 = Release|Any CPU
|
{392E4CA2-61D9-4BE1-B065-8801A9F102B8}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||||
|
{212F8090-9775-4098-BD44-9ABC01FBE553}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||||
|
{212F8090-9775-4098-BD44-9ABC01FBE553}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||||
|
{212F8090-9775-4098-BD44-9ABC01FBE553}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||||
|
{212F8090-9775-4098-BD44-9ABC01FBE553}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||||
EndGlobalSection
|
EndGlobalSection
|
||||||
GlobalSection(SolutionProperties) = preSolution
|
GlobalSection(SolutionProperties) = preSolution
|
||||||
HideSolutionNode = FALSE
|
HideSolutionNode = FALSE
|
||||||
@ -117,6 +123,7 @@ Global
|
|||||||
{EF308D4E-26A0-471C-B764-9C4EB713BEAE} = {99E56312-A064-4AD3-8443-0B56A5F76E6B}
|
{EF308D4E-26A0-471C-B764-9C4EB713BEAE} = {99E56312-A064-4AD3-8443-0B56A5F76E6B}
|
||||||
{351B93B0-301F-42E1-82A0-7FA217154F5D} = {99E56312-A064-4AD3-8443-0B56A5F76E6B}
|
{351B93B0-301F-42E1-82A0-7FA217154F5D} = {99E56312-A064-4AD3-8443-0B56A5F76E6B}
|
||||||
{392E4CA2-61D9-4BE1-B065-8801A9F102B8} = {0B9B0B74-3EB1-46A4-BCCC-F2D6AE59A9EE}
|
{392E4CA2-61D9-4BE1-B065-8801A9F102B8} = {0B9B0B74-3EB1-46A4-BCCC-F2D6AE59A9EE}
|
||||||
|
{212F8090-9775-4098-BD44-9ABC01FBE553} = {99E56312-A064-4AD3-8443-0B56A5F76E6B}
|
||||||
EndGlobalSection
|
EndGlobalSection
|
||||||
GlobalSection(ExtensibilityGlobals) = postSolution
|
GlobalSection(ExtensibilityGlobals) = postSolution
|
||||||
SolutionGuid = {1CD4EDE2-A5FB-4A58-A850-3506AB7E7B69}
|
SolutionGuid = {1CD4EDE2-A5FB-4A58-A850-3506AB7E7B69}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user