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;
+ }
}
}