diff --git a/Test/Eliot.UELib.Test.csproj b/Test/Eliot.UELib.Test.csproj index 9f69e613..c4dcfcf0 100644 --- a/Test/Eliot.UELib.Test.csproj +++ b/Test/Eliot.UELib.Test.csproj @@ -6,6 +6,14 @@ false + + $(DefineConstants)TRACE;AA2 + + + + $(DefineConstants)TRACE;AA2 + + diff --git a/Test/Packages.Designer.cs b/Test/Packages.Designer.cs index c6ae937e..ee4ee55b 100644 --- a/Test/Packages.Designer.cs +++ b/Test/Packages.Designer.cs @@ -60,6 +60,15 @@ public class Packages { } } + /// + /// Looks up a localized string similar to D:\Downloads\AAA-2.6-Linux-All\System. + /// + public static string Packages_Path_AAA_2_6 { + get { + return ResourceManager.GetString("Packages_Path_AAA_2_6", resourceCulture); + } + } + /// /// Looks up a localized resource of type System.Byte[]. /// diff --git a/Test/Packages.resx b/Test/Packages.resx index 181351e9..90288e11 100644 --- a/Test/Packages.resx +++ b/Test/Packages.resx @@ -117,6 +117,9 @@ System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + D:\Downloads\AAA-2.6-Linux-All\System + upk\TestUC2\TestUC2.u;System.Byte[], mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 diff --git a/Test/upk/Builds/PackageTests.AA2.cs b/Test/upk/Builds/PackageTests.AA2.cs new file mode 100644 index 00000000..2b29bd2d --- /dev/null +++ b/Test/upk/Builds/PackageTests.AA2.cs @@ -0,0 +1,84 @@ +#if AA2 +using System; +using System.IO; +using System.Text; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using UELib; +using UELib.Decoding; + +namespace Eliot.UELib.Test.upk.Builds +{ + [TestClass] + public class PackageTestsAA2 + { + // Testing the "Arcade" packages only + private static readonly string PackagesPath = Packages.Packages_Path_AAA_2_6; + private static readonly string NoEncryptionCorePackagePath = Path.Join(PackagesPath, "AAA_Core.u"); + private static readonly string EncryptedCorePackagePath = Path.Join(PackagesPath, "Core.u"); + + [TestMethod] + public void TestPackageAAA2_6() + { + // Skip test if the dev is not in possess of this game. + if (!Directory.Exists(PackagesPath)) + { + Console.Error.Write($"Couldn't find packages path '{PackagesPath}'"); + return; + } + + var linker = UnrealLoader.LoadPackage(NoEncryptionCorePackagePath); + Assert.IsNotNull(linker); + Assert.AreEqual(UnrealPackage.GameBuild.BuildName.AA2, linker.Build.Name, "Incorrect package's build"); + + // Requires UELib to be built without "Forms" + //linker.InitializePackage(UnrealPackage.InitFlags.Construct | UnrealPackage.InitFlags.RegisterClasses); + //var fn = linker.FindObject("ResetScores", typeof(UFunction)); + //Debug.WriteLine($"Testing Object: {fn.Class.Name}'{fn.GetOuterGroup()}'"); + //fn.BeginDeserializing(); + //fn.Decompile(); + //Assert.IsNull(fn.ThrownException); + } + + [TestMethod] + public void TestPackageDecryptionAAA2_6() + { + // Skip test if the dev is not in possess of this game. + if (!Directory.Exists(PackagesPath)) + { + Console.Error.Write($"Couldn't find packages path '{PackagesPath}'"); + return; + } + + var linker = UnrealLoader.LoadPackage(EncryptedCorePackagePath); + Assert.IsNotNull(linker); + Assert.AreEqual(UnrealPackage.GameBuild.BuildName.AA2, linker.Build.Name, "Incorrect package's build"); + } + + [TestMethod("AA2 Decryption of string 'None'")] + public void TestDecryptionAAA2_6() + { + var decoder = new CryptoDecoderWithKeyAA2 + { + Key = 0x9F + }; + + // "None" when bits are scrambled (As serialized in Core.u). + var scrambledNone = new byte[] { 0x94, 0x3E, 0xBF, 0xB2 }; + decoder.DecodeRead(0x45, scrambledNone, 0, scrambledNone.Length); + string decodedString = Encoding.ASCII.GetString(scrambledNone); + Assert.AreEqual("None", decodedString); + + var i = (char)decoder.DecryptByte(0x44, 0xDE); + Assert.AreEqual(5, i); + var c = (char)decoder.DecryptByte(0x45, 0x94); + Assert.AreEqual('N', c); + var c2 = (char)decoder.DecryptByte(0x46, 0x3E); + Assert.AreEqual('o', c2); + var c3 = (char)decoder.DecryptByte(0x47, 0xBF); + Assert.AreEqual('n', c3); + var c4 = (char)decoder.DecryptByte(0x48, 0xB2); + Assert.AreEqual('e', c4); + } + } +} +#endif \ No newline at end of file diff --git a/src/ByteCodeDecompiler.cs b/src/ByteCodeDecompiler.cs index b93a6ed4..49f9ea63 100644 --- a/src/ByteCodeDecompiler.cs +++ b/src/ByteCodeDecompiler.cs @@ -99,6 +99,200 @@ private void AlignObjectSize() // TODO: Retrieve the byte-codes from a NTL file instead. [CanBeNull] private Dictionary _ByteCodeMap; +#if AA2 + private readonly Dictionary ByteCodeMap_BuildAa2_8 = new Dictionary + { + { 0x00, (byte)ExprToken.LocalVariable }, + { 0x01, (byte)ExprToken.InstanceVariable }, + { 0x02, (byte)ExprToken.DefaultVariable }, + { 0x03, (byte)ExprToken.Unused }, + { 0x04, (byte)ExprToken.Switch }, + { 0x05, (byte)ExprToken.ClassContext }, + { 0x06, (byte)ExprToken.Jump }, + { 0x07, (byte)ExprToken.GotoLabel }, + { 0x08, (byte)ExprToken.VirtualFunction }, + { 0x09, (byte)ExprToken.IntConst }, + { 0x0A, (byte)ExprToken.JumpIfNot }, + { 0x0B, (byte)ExprToken.LabelTable }, + { 0x0C, (byte)ExprToken.FinalFunction }, + { 0x0D, (byte)ExprToken.EatString }, + { 0x0E, (byte)ExprToken.Let }, + { 0x0F, (byte)ExprToken.Stop }, + { 0x10, (byte)ExprToken.New }, + { 0x11, (byte)ExprToken.Context }, + { 0x12, (byte)ExprToken.MetaCast }, + { 0x13, (byte)ExprToken.Skip }, + { 0x14, (byte)ExprToken.Self }, + { 0x15, (byte)ExprToken.Return }, + { 0x16, (byte)ExprToken.EndFunctionParms }, + { 0x17, (byte)ExprToken.Unused }, + { 0x18, (byte)ExprToken.LetBool }, + { 0x19, (byte)ExprToken.DynArrayElement }, + { 0x1A, (byte)ExprToken.Assert }, + { 0x1B, (byte)ExprToken.ByteConst }, + { 0x1C, (byte)ExprToken.Nothing }, + { 0x1D, (byte)ExprToken.DelegateProperty }, + { 0x1E, (byte)ExprToken.IntZero }, + { 0x1F, (byte)ExprToken.LetDelegate }, + { 0x20, (byte)ExprToken.False }, + { 0x21, (byte)ExprToken.ArrayElement }, + { 0x22, (byte)ExprToken.EndOfScript }, + { 0x23, (byte)ExprToken.True }, + { 0x24, (byte)ExprToken.Unused }, + { 0x25, (byte)ExprToken.FloatConst }, + { 0x26, (byte)ExprToken.Case }, + { 0x27, (byte)ExprToken.IntOne }, + { 0x28, (byte)ExprToken.StringConst }, + { 0x29, (byte)ExprToken.NoObject }, + { 0x2A, (byte)ExprToken.NativeParm }, + { 0x2B, (byte)ExprToken.Unused }, + { 0x2C, (byte)ExprToken.DebugInfo }, + { 0x2D, (byte)ExprToken.StructCmpEq }, + // FIXME: Verify IteratorNext/IteratorPop? + { 0x2E, (byte)ExprToken.IteratorNext }, + { 0x2F, (byte)ExprToken.DynArrayRemove }, + { 0x30, (byte)ExprToken.StructCmpNE }, + { 0x31, (byte)ExprToken.DynamicCast }, + { 0x32, (byte)ExprToken.Iterator }, + { 0x33, (byte)ExprToken.IntConstByte }, + { 0x34, (byte)ExprToken.BoolVariable }, + // FIXME: Verify IteratorNext/IteratorPop? + { 0x35, (byte)ExprToken.IteratorPop }, + { 0x36, (byte)ExprToken.UniStringConst }, + { 0x37, (byte)ExprToken.StructMember }, + { 0x38, (byte)ExprToken.Unused }, + { 0x39, (byte)ExprToken.DelegateFunction }, + { 0x3A, (byte)ExprToken.Unused }, + { 0x3B, (byte)ExprToken.Unused }, + { 0x3C, (byte)ExprToken.Unused }, + { 0x3D, (byte)ExprToken.Unused }, + { 0x3E, (byte)ExprToken.Unused }, + { 0x3F, (byte)ExprToken.Unused }, + { 0x40, (byte)ExprToken.ObjectConst }, + { 0x41, (byte)ExprToken.NameConst }, + { 0x42, (byte)ExprToken.DynArrayLength }, + { 0x43, (byte)ExprToken.DynArrayInsert }, + { 0x44, (byte)ExprToken.PrimitiveCast }, + { 0x45, (byte)ExprToken.GlobalFunction }, + { 0x46, (byte)ExprToken.VectorConst }, + { 0x47, (byte)ExprToken.RotatorConst }, + { 0x48, (byte)ExprToken.Unused }, + { 0x49, (byte)ExprToken.Unused }, + { 0x4A, (byte)ExprToken.Unused }, + { 0x4B, (byte)ExprToken.Unused }, + { 0x4C, (byte)ExprToken.Unused }, + { 0x4D, (byte)ExprToken.Unused }, + { 0x4E, (byte)ExprToken.Unused }, + { 0x4F, (byte)ExprToken.Unused }, + { 0x50, (byte)ExprToken.Unused }, + { 0x51, (byte)ExprToken.Unused }, + { 0x52, (byte)ExprToken.Unused }, + { 0x53, (byte)ExprToken.Unused }, + { 0x54, (byte)ExprToken.Unused }, + { 0x55, (byte)ExprToken.Unused }, + { 0x56, (byte)ExprToken.Unused }, + { 0x57, (byte)ExprToken.Unused }, + { 0x58, (byte)ExprToken.Unused }, + { 0x59, (byte)ExprToken.Unused } + }; + + /// + /// The shifted byte-code map for AAA 2.6 + /// + private readonly Dictionary ByteCodeMap_BuildAa2_6 = new Dictionary + { + { 0x00, (byte)ExprToken.LocalVariable }, + { 0x01, (byte)ExprToken.InstanceVariable }, + { 0x02, (byte)ExprToken.DefaultVariable }, + { 0x03, (byte)ExprToken.Unused }, + { 0x04, (byte)ExprToken.Jump }, + { 0x05, (byte)ExprToken.Return }, + { 0x06, (byte)ExprToken.Switch }, + { 0x07, (byte)ExprToken.Stop }, + { 0x08, (byte)ExprToken.JumpIfNot }, + { 0x09, (byte)ExprToken.Nothing }, + { 0x0A, (byte)ExprToken.LabelTable }, + { 0x0B, (byte)ExprToken.Assert }, + { 0x0C, (byte)ExprToken.Case }, + { 0x0D, (byte)ExprToken.EatString }, + { 0x0E, (byte)ExprToken.Let }, + { 0x0F, (byte)ExprToken.GotoLabel }, + { 0x10, (byte)ExprToken.DynArrayElement }, + { 0x11, (byte)ExprToken.New }, + { 0x12, (byte)ExprToken.ClassContext }, + { 0x13, (byte)ExprToken.MetaCast }, + { 0x14, (byte)ExprToken.LetBool }, + { 0x15, (byte)ExprToken.EndFunctionParms }, + { 0x16, (byte)ExprToken.Skip }, + { 0x17, (byte)ExprToken.Unused }, + { 0x18, (byte)ExprToken.Context }, + { 0x19, (byte)ExprToken.Self }, + { 0x1A, (byte)ExprToken.FinalFunction }, + { 0x1B, (byte)ExprToken.ArrayElement }, + { 0x1C, (byte)ExprToken.IntConst }, + { 0x1D, (byte)ExprToken.FloatConst }, + { 0x1E, (byte)ExprToken.StringConst }, + { 0x1F, (byte)ExprToken.VirtualFunction }, + { 0x20, (byte)ExprToken.IntOne }, + { 0x21, (byte)ExprToken.VectorConst }, + { 0x22, (byte)ExprToken.NameConst }, + { 0x23, (byte)ExprToken.IntZero }, + { 0x24, (byte)ExprToken.ObjectConst }, + { 0x25, (byte)ExprToken.ByteConst }, + { 0x26, (byte)ExprToken.RotatorConst }, + { 0x27, (byte)ExprToken.False }, + { 0x28, (byte)ExprToken.True }, + { 0x29, (byte)ExprToken.NoObject }, + { 0x2A, (byte)ExprToken.NativeParm }, + { 0x2B, (byte)ExprToken.Unused }, + { 0x2C, (byte)ExprToken.BoolVariable }, + { 0x2D, (byte)ExprToken.Iterator }, + { 0x2E, (byte)ExprToken.IntConstByte }, + { 0x2F, (byte)ExprToken.DynamicCast }, + { 0x30, (byte)ExprToken.Unused }, + { 0x31, (byte)ExprToken.StructCmpNE }, + { 0x32, (byte)ExprToken.UniStringConst }, + { 0x33, (byte)ExprToken.IteratorNext }, + { 0x34, (byte)ExprToken.StructCmpEq }, + { 0x35, (byte)ExprToken.IteratorPop }, + { 0x36, (byte)ExprToken.GlobalFunction }, + { 0x37, (byte)ExprToken.StructMember }, + { 0x38, (byte)ExprToken.PrimitiveCast }, + { 0x39, (byte)ExprToken.DynArrayLength }, + { 0x3A, (byte)ExprToken.Unused }, + { 0x3B, (byte)ExprToken.Unused }, + { 0x3C, (byte)ExprToken.Unused }, + { 0x3D, (byte)ExprToken.Unused }, + { 0x3E, (byte)ExprToken.Unused }, + { 0x3F, (byte)ExprToken.Unused }, + { 0x40, (byte)ExprToken.Unused }, + { 0x41, (byte)ExprToken.EndOfScript }, + { 0x42, (byte)ExprToken.DynArrayRemove }, + { 0x43, (byte)ExprToken.DynArrayInsert }, + { 0x44, (byte)ExprToken.DelegateFunction }, + { 0x45, (byte)ExprToken.DebugInfo }, + { 0x46, (byte)ExprToken.LetDelegate }, + { 0x47, (byte)ExprToken.DelegateProperty }, + { 0x48, (byte)ExprToken.Unused }, + { 0x49, (byte)ExprToken.Unused }, + { 0x4A, (byte)ExprToken.Unused }, + { 0x4B, (byte)ExprToken.Unused }, + { 0x4C, (byte)ExprToken.Unused }, + { 0x4D, (byte)ExprToken.Unused }, + { 0x4E, (byte)ExprToken.Unused }, + { 0x4F, (byte)ExprToken.Unused }, + { 0x50, (byte)ExprToken.Unused }, + { 0x51, (byte)ExprToken.Unused }, + { 0x52, (byte)ExprToken.Unused }, + { 0x53, (byte)ExprToken.Unused }, + { 0x54, (byte)ExprToken.Unused }, + { 0x55, (byte)ExprToken.Unused }, + { 0x56, (byte)ExprToken.Unused }, + { 0x57, (byte)ExprToken.Unused }, + { 0x58, (byte)ExprToken.Unused }, + { 0x59, (byte)ExprToken.Unused } + }; +#endif #if APB private static readonly Dictionary ByteCodeMap_BuildApb = new Dictionary { @@ -112,7 +306,45 @@ private void AlignObjectSize() #endif private void SetupByteCodeMap() { +#if AA2 + if (Package.Build == UnrealPackage.GameBuild.BuildName.AA2) + { + if (Package.LicenseeVersion >= 33) + { + _ByteCodeMap = ByteCodeMap_BuildAa2_8; + } + else + { + // FIXME: The byte-code shifted as of V2.6, but there is no way to tell which game-version the package was compiled with. + // This hacky-solution also doesn't handle UState nor UClass cases. + // This can be solved by moving the byte-code maps to their own NTL files. + + // Flags + //sizeof(uint) + + // Oper + //sizeof(byte) + // NativeToken + //sizeof(ushort) + + // EndOfScript ExprToken + //sizeof(byte), + long functionBacktrackLength = -8; + if (_Container is UFunction function && function.HasFunctionFlag(Flags.FunctionFlags.Net)) + // RepOffset + functionBacktrackLength -= sizeof(ushort); + + Buffer.StartPeek(); + Buffer.Seek(functionBacktrackLength, SeekOrigin.End); + byte valueOfEndOfScriptToken = Buffer.ReadByte(); + Buffer.EndPeek(); + + // Shifted? + if (valueOfEndOfScriptToken != (byte)ExprToken.FunctionEnd) + _ByteCodeMap = ByteCodeMap_BuildAa2_6; + } + return; + } +#endif #if APB if (Package.Build == UnrealPackage.GameBuild.BuildName.APB && Package.LicenseeVersion >= 32) diff --git a/src/Core/Tables/UExportTableItem.cs b/src/Core/Tables/UExportTableItem.cs index 3953fef9..91141ecf 100644 --- a/src/Core/Tables/UExportTableItem.cs +++ b/src/Core/Tables/UExportTableItem.cs @@ -1,3 +1,4 @@ +using System.Diagnostics; using System.Diagnostics.Contracts; using System.IO; @@ -101,6 +102,21 @@ public void Serialize(IUnrealStream stream) public void Deserialize(IUnrealStream stream) { +#if AA2 + // Not attested in packages of LicenseeVersion 32 + if (stream.Package.Build == UnrealPackage.GameBuild.BuildName.AA2 + && stream.Package.LicenseeVersion >= 33) + { + SuperIndex = stream.ReadObjectIndex(); + int unkInt = stream.ReadInt32(); + Debug.WriteLine(unkInt, "unkInt"); + ClassIndex = stream.ReadObjectIndex(); + OuterIndex = stream.ReadInt32(); + ObjectFlags = ~stream.ReadUInt32(); + ObjectName = stream.ReadNameReference(); + goto serializeSerialSize; + } +#endif ClassIndex = stream.ReadObjectIndex(); SuperIndex = stream.ReadObjectIndex(); OuterIndex = stream.ReadInt32(); // ObjectIndex, though always written as 32bits regardless of build. @@ -111,7 +127,6 @@ public void Deserialize(IUnrealStream stream) } #endif ObjectName = stream.ReadNameReference(); - if (stream.Version >= VArchetype) { ArchetypeIndex = stream.ReadInt32(); @@ -129,6 +144,7 @@ public void Deserialize(IUnrealStream stream) ObjectFlags = (ObjectFlags << 32) | stream.ReadUInt32(); } + serializeSerialSize: SerialSize = stream.ReadIndex(); if (SerialSize > 0 || stream.Version >= VSerialSizeConditionless) { diff --git a/src/Core/Tables/UImportTableItem.cs b/src/Core/Tables/UImportTableItem.cs index 6a76db1b..8e6bf41a 100644 --- a/src/Core/Tables/UImportTableItem.cs +++ b/src/Core/Tables/UImportTableItem.cs @@ -1,3 +1,4 @@ +using System.Diagnostics; using System.Diagnostics.Contracts; namespace UELib @@ -28,6 +29,22 @@ public void Serialize(IUnrealStream stream) public void Deserialize(IUnrealStream stream) { +#if AA2 + // Not attested in packages of LicenseeVersion 32 + if (stream.Package.Build == UnrealPackage.GameBuild.BuildName.AA2 + && stream.Package.LicenseeVersion >= 33) + { + PackageName = stream.ReadNameReference(); + _ClassName = stream.ReadNameReference(); + ClassIndex = (int)_ClassName; + byte unkByte = stream.ReadByte(); + Debug.WriteLine(unkByte, "unkByte"); + ObjectName = stream.ReadNameReference(); + OuterIndex = stream.ReadInt32(); // ObjectIndex, though always written as 32bits regardless of build. + return; + } +#endif + PackageName = stream.ReadNameReference(); _ClassName = stream.ReadNameReference(); ClassIndex = (int)_ClassName; diff --git a/src/Core/Tables/UNameTableItem.cs b/src/Core/Tables/UNameTableItem.cs index 9216ccaf..15aaf76f 100644 --- a/src/Core/Tables/UNameTableItem.cs +++ b/src/Core/Tables/UNameTableItem.cs @@ -1,3 +1,7 @@ +using System.Diagnostics; +using System.Runtime.CompilerServices; +using UELib.Decoding; + namespace UELib { /// @@ -46,6 +50,36 @@ private string DeserializeName(IUnrealStream stream) { // FIXME: DCUO doesn't null terminate name entry strings } +#endif +#if AA2 + // Names are not encrypted in AAA/AAO 2.6 (LicenseeVersion 32) + if (stream.Package.Build == UnrealPackage.GameBuild.BuildName.AA2 + && stream.Package.LicenseeVersion >= 33 + && stream.Package.Decoder is CryptoDecoderAA2) + { + // Thanks to @gildor2, decryption code transpiled from https://github.com/gildor2/UEViewer, + int length = stream.ReadIndex(); + Debug.Assert(length < 0); + int size = -length; + + const byte n = 5; + byte shift = n; + var buffer = new char[size]; + for (var i = 0; i < size; i++) + { + ushort c = stream.ReadUInt16(); + ushort c2 = CryptoCore.RotateRight(c, shift); + Debug.Assert(c2 < byte.MaxValue); + buffer[i] = (char)(byte)c2; + shift = (byte)((c - n) & 0x0F); + } + + var str = new string(buffer, 0, buffer.Length - 1); + // Part of name ? + int number = stream.ReadIndex(); + //Debug.Assert(number == 0, "Unknown value"); + return str; + } #endif return stream.ReadText(); } diff --git a/src/Decoding/CryptoCore.cs b/src/Decoding/CryptoCore.cs new file mode 100644 index 00000000..5f546c18 --- /dev/null +++ b/src/Decoding/CryptoCore.cs @@ -0,0 +1,27 @@ +using System.Runtime.CompilerServices; +using UELib.Annotations; + +namespace UELib.Decoding +{ + [PublicAPI] + public static class CryptoCore + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static byte RotateRight(byte value, int count) + { + return (byte)((byte)(value >> count) | (byte)(value << (8 - count))); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static ushort RotateRight(ushort value, int count) + { + return (ushort)((ushort)(value >> count) | (ushort)(value << (16 - count))); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static byte RotateLeft(byte value, int count) + { + return (byte)((value << count) | (value >> (8 - count))); + } + } +} \ No newline at end of file diff --git a/src/Decoding/CryptoDecoder.AA2.cs b/src/Decoding/CryptoDecoder.AA2.cs new file mode 100644 index 00000000..5d3a35a6 --- /dev/null +++ b/src/Decoding/CryptoDecoder.AA2.cs @@ -0,0 +1,75 @@ +using System.Runtime.CompilerServices; + +namespace UELib.Decoding +{ + // TODO: Re-implement as a BaseStream wrapper (in UELib 2.0) + public class CryptoDecoderAA2 : IBufferDecoder + { + public void PreDecode(IUnrealStream stream) + { + } + + public void DecodeBuild(IUnrealStream stream, UnrealPackage.GameBuild build) + { + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static byte DecryptByte(long position, byte scrambledByte) + { + long offsetScramble = (position >> 8) ^ position; + scrambledByte ^= (byte)offsetScramble; + return (offsetScramble & 0x02) != 0 + ? CryptoCore.RotateLeft(scrambledByte, 1) + : scrambledByte; + } + + public void DecodeRead(long position, byte[] buffer, int index, int count) + { + for (int i = index; i < count; ++i) buffer[i] = DecryptByte(position + i, buffer[i]); + } + + public unsafe void DecodeByte(long position, byte* b) + { + *b = DecryptByte(position, *b); + } + } + + public class CryptoDecoderWithKeyAA2 : IBufferDecoder + { + public byte Key = 0x05; + + public void PreDecode(IUnrealStream stream) + { + } + + public void DecodeBuild(IUnrealStream stream, UnrealPackage.GameBuild build) + { + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public byte DecryptByte(long position, byte scrambledByte) + { + long offsetScramble = (position >> 8) ^ position; + scrambledByte ^= (byte)offsetScramble; + if ((offsetScramble & 0x02) != 0) + { + if ((sbyte)scrambledByte < 0) + scrambledByte = (byte)((scrambledByte << 1) | 1); + else + scrambledByte <<= 1; + } + + return (byte)(Key ^ scrambledByte); + } + + public void DecodeRead(long position, byte[] buffer, int index, int count) + { + for (int i = index; i < count; ++i) buffer[i] = DecryptByte(position + i, buffer[i]); + } + + public unsafe void DecodeByte(long position, byte* b) + { + *b = DecryptByte(position, *b); + } + } +} \ No newline at end of file diff --git a/src/Decoding/IBufferDecoder.cs b/src/Decoding/IBufferDecoder.cs index 492dfd2d..a7364068 100644 --- a/src/Decoding/IBufferDecoder.cs +++ b/src/Decoding/IBufferDecoder.cs @@ -8,5 +8,6 @@ public interface IBufferDecoder void PreDecode(IUnrealStream stream); void DecodeBuild(IUnrealStream stream, UnrealPackage.GameBuild build); void DecodeRead(long position, byte[] buffer, int index, int count); + unsafe void DecodeByte(long position, byte* b); } } diff --git a/src/Eliot.UELib.csproj b/src/Eliot.UELib.csproj index 609f6a0d..dd7d3d25 100644 --- a/src/Eliot.UELib.csproj +++ b/src/Eliot.UELib.csproj @@ -166,6 +166,8 @@ + + diff --git a/src/Eliot.UELib.sln.DotSettings b/src/Eliot.UELib.sln.DotSettings index 4dc2a1de..01f43a3d 100644 --- a/src/Eliot.UELib.sln.DotSettings +++ b/src/Eliot.UELib.sln.DotSettings @@ -1,3 +1,4 @@  True - True \ No newline at end of file + True + True \ No newline at end of file diff --git a/src/UnrealPackage.cs b/src/UnrealPackage.cs index 701be38f..1ff22d3a 100644 --- a/src/UnrealPackage.cs +++ b/src/UnrealPackage.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.ComponentModel; +using System.Diagnostics; using System.Diagnostics.CodeAnalysis; using System.IO; using System.Linq; @@ -273,9 +274,9 @@ public enum BuildName // Built on UT2004 // Represents both AAO and AAA /// - /// 128/032 + /// 128/032:033 /// - [Build(128, 32)] AA2, + [Build(128, 128, 32u, 33u)] AA2, /// /// 129/027 @@ -1048,67 +1049,84 @@ public void Deserialize(UPackageStream stream) } } } - #if DCUO - if( Build == GameBuild.BuildName.DCUO ) + if (Build == GameBuild.BuildName.DCUO) { //We need to back up because our package has already been decompressed - stream.Position - -= 4; - - int unkCount - = stream.ReadInt32(); - - stream.Skip( 16 * unkCount ); - - stream.Skip( 4 ); + stream.Position -= 4; + int unkCount = stream.ReadInt32(); + stream.Skip(16 * unkCount); + stream.Skip(4); - if( Version >= 516 ) + if (Version >= 516) { - int textCount - = stream.ReadInt32(); - - var texts - = new List(); - for( int i - = 0; i < textCount; i++ ) + int textCount = stream.ReadInt32(); + var texts = new List(textCount); + for (var i = 0; i < textCount; i++) { - texts.Add( stream.ReadText() ); + texts.Add(stream.ReadText()); } } - - uint realNameOffset - = (uint)stream.Position; - - System.Diagnostics.Debug.Assert( realNameOffset <= _TablesData.NamesOffset, "realNameOffset is > the parsed name offset for a DCUO package, we don't know where to go now!" ); - - uint offsetDif - = _TablesData.NamesOffset - realNameOffset; - - //The offsets parsed above are off, maybe due to decompression? - _TablesData.NamesOffset - -= offsetDif; - _TablesData.ImportsOffset - -= offsetDif; - _TablesData.ExportsOffset - -= offsetDif; + + var realNameOffset = (uint)stream.Position; + System.Diagnostics.Debug.Assert( + realNameOffset <= _TablesData.NamesOffset, + "realNameOffset is > the parsed name offset for a DCUO package, we don't know where to go now!" + ); + + uint offsetDif = _TablesData.NamesOffset - realNameOffset; + _TablesData.NamesOffset -= offsetDif; + _TablesData.ImportsOffset -= offsetDif; + _TablesData.ExportsOffset -= offsetDif; } #endif - #if AA2 - if (Build == GameBuild.BuildName.AA2) + if (Build == GameBuild.BuildName.AA2 + // Note: Never true, AA2 is not a detected build for packages with LicenseeVersion 27 or less + // But we'll preserve this nonetheless + && LicenseeVersion >= 19) { bool isEncrypted = stream.ReadInt32() > 0; if (isEncrypted) { - throw new UnrealException("Package is encrypted, aborting!"); // TODO: Use a stream wrapper instead; but this is blocked by an overly intertwined use of PackageStream. - //var decoder = new AA2CryptoReader(); - //byte b = stream.ReadByte(); - //decoder.Key = 0x05; - //decoder.Key = decoder.DecryptByte((int)stream.Position, b); - //Decoder = decoder; + if (LicenseeVersion >= 33) + { + var decoder = new CryptoDecoderAA2(); + Decoder = decoder; + } + else + { + var decoder = new CryptoDecoderWithKeyAA2(); + Decoder = decoder; + + uint nonePosition = _TablesData.NamesOffset; + stream.Seek(nonePosition, SeekOrigin.Begin); + byte scrambledNoneLength = stream.ReadByte(); + decoder.Key = scrambledNoneLength; + stream.Seek(nonePosition, SeekOrigin.Begin); + byte unscrambledNoneLength = stream.ReadByte(); + Debug.Assert((unscrambledNoneLength & 0x3F) == 5); + } } + + // Always one + //int unkCount = stream.ReadInt32(); + //for (var i = 0; i < unkCount; i++) + //{ + // // All zero + // stream.Skip(24); + // // Always identical to the package's GUID + // var guid = stream.ReadGuid(); + //} + + //// Always one + //int unk2Count = stream.ReadInt32(); + //for (var i = 0; i < unk2Count; i++) + //{ + // // All zero + // stream.Skip(12); + //} } #endif diff --git a/src/UnrealStream.cs b/src/UnrealStream.cs index a6d6d3c6..c16607d1 100644 --- a/src/UnrealStream.cs +++ b/src/UnrealStream.cs @@ -198,9 +198,7 @@ public string ReadText() // No null-termination in Transformer games. if (_Archive.Package.Build == UnrealPackage.GameBuild.BuildName.Transformers && _Archive.Package.LicenseeVersion >= 181) - { return Encoding.ASCII.GetString(chars, 0, chars.Length); - } #endif return Encoding.ASCII.GetString(chars, 0, chars.Length - 1); } @@ -220,9 +218,7 @@ public string ReadText() // No null-termination in Transformer games. if (_Archive.Package.Build == UnrealPackage.GameBuild.BuildName.Transformers && _Archive.Package.LicenseeVersion >= 181) - { return new string(chars); - } #endif // Strip off the null return new string(chars, 0, chars.Length - 1); @@ -239,7 +235,7 @@ public string ReadAnsi() long lastPosition = BaseStream.Position; #endif var strBytes = new List(); - nextChar: + nextChar: byte c = ReadByte(); if (c != '\0') { @@ -259,7 +255,7 @@ public string ReadUnicode() long lastPosition = BaseStream.Position; #endif var strBytes = new List(); - nextWord: + nextWord: short w = ReadInt16(); if (w != 0) { @@ -440,10 +436,13 @@ public override int ReadByte() if (Package.Decoder == null) return base.ReadByte(); - long p = Position; - var buffer = new[] { (byte)base.ReadByte() }; - Package.Decoder?.DecodeRead(p, buffer, 0, 1); - return buffer[0]; + unsafe + { + long p = Position; + var b = (byte)base.ReadByte(); + Package.Decoder?.DecodeByte(p, &b); + return b; + } } }