diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 47a11edc..b2298cd7 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -3,9 +3,8 @@ name: Build .NET Framework and Publish to NuGet on: workflow_dispatch: push: - tags: - - 'release-*' - - 'hotfix-*' + tags: + - '^[0-9]+\.[0-9]+\.[0-9]+$' jobs: build: @@ -18,13 +17,13 @@ jobs: uses: microsoft/setup-msbuild@v1.3 - name: Setup NuGet - uses: NuGet/setup-nuget@v1 + uses: NuGet/setup-nuget@v2 - name: Restore dependencies run: nuget restore src/Eliot.UELib.csproj - name: Build - run: msbuild src/Eliot.UELib.csproj -t:rebuild -property:Configuration=Publish + run: msbuild src/Eliot.UELib.csproj -t:rebuild -property:Configuration=Remease - name: Publish Eliot.UELib uses: alirezanet/publish-nuget@v3.1.0 diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 06c469a8..e535d8e4 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -16,7 +16,7 @@ jobs: steps: - uses: actions/checkout@v4 - name: Setup .NET - uses: actions/setup-dotnet@v3 + uses: actions/setup-dotnet@v4 with: # Legacy versions not supported? :( # 5.0 will likely not work yet due legacy dependencies... diff --git a/CHANGELOG.md b/CHANGELOG.md index e68f3a0b..8bf9dd4a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,7 @@ +# [1.5.0](https://github.com/EliotVU/Unreal-Library/releases/tag/1.5.0) + +* 1ef135d Improved support for A Hat in Time (UE3), contributed by @Un-Drew + # [1.4.0](https://github.com/EliotVU/Unreal-Library/releases/tag/1.4.0) Notable changes that affect UnrealScript output: diff --git a/README.md b/README.md index b1167fc4..d544761c 100644 --- a/README.md +++ b/README.md @@ -8,11 +8,18 @@ Its main purpose is to decompile the UnrealScript byte-code to its original sour It accomplishes this by reading the necessary Unreal data classes such as: - UObject, UField, UConst, UEnum, UProperty, UStruct, UFunction, UState, - UClass, UTextBuffer, UMetaData, UFont, USound, UPackage + UObject, UField, UConst, UEnum, UProperty, UStruct, UFunction, UState, UClass, + UTextBuffer, UMetaData, UPackage Classes such as UStruct, UState, UClass, and UFunction contain the UnrealScript byte-code which we can deserialize in order to re-construct the byte-codes to its original UnrealScript source. +Additionally UELib is also capable of deserializing of many more data classes such as: + + UFont, USound, UPalette, UTexture, + UTexture2D, UTexture2DDynamic, UTexture2DComposite, UTexture3D, + UTextureCube, UTextureFlipBook, UTextureMovie + UPrimitive, UPolys + ## How to use To use this library you will need [.NET Framework 4.8](https://dotnet.microsoft.com/en-us/download/dotnet-framework/net48) (The library will move to .NET 6 or higher at version 2.0) @@ -178,6 +185,7 @@ This is a table of games that are confirmed to be compatible with the current st | Orcs Must Die! Unchained | 20430 | 870/000 | | | Gal\*Gun: Double Peace | 10897 | 871/000 | | | [Might & Magic Heroes VII](https://en.wikipedia.org/wiki/Might_%26_Magic_Heroes_VII) | 12161 | 868/004 | (Signature and custom features are not supported) +| A Hat in Time | 12097 | 877-893/005 | | | Shadow Complex Remastered | 10897 | 893/001 | | | Soldier Front 2 | 6712 | 904/009 | | | Rise of the Triad | 10508 | Unknown | | diff --git a/src/Branch/DefaultEngineBranch.cs b/src/Branch/DefaultEngineBranch.cs index 3054c87b..fe74cc6f 100644 --- a/src/Branch/DefaultEngineBranch.cs +++ b/src/Branch/DefaultEngineBranch.cs @@ -375,6 +375,8 @@ protected override TokenMap BuildTokenMap(UnrealPackage linker) tokenMap[0x4F] = typeof(LocalVariableToken); tokenMap[0x50] = typeof(LocalVariableToken); tokenMap[0x51] = typeof(LocalVariableToken); + + tokenMap[0x5B] = typeof(ByteConstToken); break; #endif #if BIOSHOCK diff --git a/src/Core/Classes/Props/UPropertyDecompiler.cs b/src/Core/Classes/Props/UPropertyDecompiler.cs index 4e14c173..922b72e1 100644 --- a/src/Core/Classes/Props/UPropertyDecompiler.cs +++ b/src/Core/Classes/Props/UPropertyDecompiler.cs @@ -302,6 +302,22 @@ public string FormatFlags() output += "serializetext "; copyFlags &= ~(ulong)Flags.PropertyFlagsLO.SerializeText; } + +#if AHIT + if (Package.Build == UnrealPackage.GameBuild.BuildName.AHIT) + { + if (HasPropertyFlag(Flags.PropertyFlagsHO.AHIT_Serialize)) + { + output += "serialize "; + copyFlags &= ~(ulong)Flags.PropertyFlagsHO.AHIT_Serialize << 32; + } + if (HasPropertyFlag(Flags.PropertyFlagsLO.AHIT_Bitwise)) + { + output += "bitwise "; + copyFlags &= ~(ulong)Flags.PropertyFlagsLO.AHIT_Bitwise; + } + } +#endif } if ((PropertyFlags & (ulong)Flags.PropertyFlagsLO.Native) != 0) @@ -393,7 +409,11 @@ public string FormatFlags() } } - if ((PropertyFlags & (ulong)Flags.PropertyFlagsLO.EdFindable) != 0) + if ((PropertyFlags & (ulong)Flags.PropertyFlagsLO.EdFindable) != 0 +#if AHIT + && Package.Build != UnrealPackage.GameBuild.BuildName.AHIT +#endif + ) { copyFlags &= ~(ulong)Flags.PropertyFlagsLO.EdFindable; output += "edfindable "; diff --git a/src/Core/Classes/UClass.cs b/src/Core/Classes/UClass.cs index 723aa3fb..54c44bb6 100644 --- a/src/Core/Classes/UClass.cs +++ b/src/Core/Classes/UClass.cs @@ -261,6 +261,16 @@ protected override void Deserialize() Record(nameof(classGeneratedBy), classGeneratedBy); } #endif + +#if AHIT + if (Package.Build == UnrealPackage.GameBuild.BuildName.AHIT && _Buffer.Version >= 878) + { + // AHIT auto-generates a list of unused function names for its optional interface functions. + // Seems to have been added in 878, during the modding beta between 1.Nov.17 and 6.Jan.18. + DeserializeGroup("UnusedOptionalInterfaceFunctions"); + } +#endif + if (!Package.IsConsoleCooked() && !Package.Build.Flags.HasFlag(BuildFlags.XenonCooked)) { if (_Buffer.Version >= 603 && _Buffer.UE4Version < 113 diff --git a/src/Core/Classes/UClassDecompiler.cs b/src/Core/Classes/UClassDecompiler.cs index 4530b7e5..754e770c 100644 --- a/src/Core/Classes/UClassDecompiler.cs +++ b/src/Core/Classes/UClassDecompiler.cs @@ -376,6 +376,20 @@ private string FormatFlags() } #endif +#if AHIT + if (Package.Build == UnrealPackage.GameBuild.BuildName.AHIT) + { + if ((ClassFlags & (uint)Flags.ClassFlags.AHIT_AlwaysLoaded) != 0) + { + output += "\r\n\tAlwaysLoaded"; + } + if ((ClassFlags & (uint)Flags.ClassFlags.AHIT_IterOptimized) != 0) + { + output += "\r\n\tIterationOptimized"; + } + } +#endif + output += FormatNameGroup("classgroup", ClassGroups); output += FormatNameGroup("autoexpandcategories", AutoExpandCategories); output += FormatNameGroup("autocollapsecategories", AutoCollapseCategories); diff --git a/src/Core/Classes/UFunctionDecompiler.cs b/src/Core/Classes/UFunctionDecompiler.cs index d4ff6bdd..bb7f2a3c 100644 --- a/src/Core/Classes/UFunctionDecompiler.cs +++ b/src/Core/Classes/UFunctionDecompiler.cs @@ -99,9 +99,34 @@ private string FormatFlags() { output += "noexport "; } + +#if AHIT + if (Package.Build == UnrealPackage.GameBuild.BuildName.AHIT) + { + if (HasFunctionFlag(Flags.FunctionFlags.AHIT_Optional)) + { + output += "optional "; // optional interface functions use this. + } + + if (HasFunctionFlag(Flags.FunctionFlags.AHIT_Multicast)) + { + output += "multicast "; + } + + if (HasFunctionFlag(Flags.FunctionFlags.AHIT_NoOwnerRepl)) + { + output += "NoOwnerReplication "; + } + } +#endif // FIXME: Version, added with one of the later UDK builds. - if (Package.Version >= 500) + if (Package.Version >= 500 +#if AHIT + // For AHIT, don't write these K2 specifiers, since they overlap with its custom flags. + && Package.Build != UnrealPackage.GameBuild.BuildName.AHIT +#endif + ) { if (HasFunctionFlag(Flags.FunctionFlags.K2Call)) { @@ -212,6 +237,14 @@ private string FormatFlags() output += "function "; } +#if AHIT + // Needs to be after function/event/operator/etc. + if (Package.Build == UnrealPackage.GameBuild.BuildName.AHIT && HasFunctionFlag(Flags.FunctionFlags.AHIT_EditorOnly)) + { + output += "editoronly "; + } +#endif + return output; } diff --git a/src/Core/Classes/UStruct.cs b/src/Core/Classes/UStruct.cs index e4222cba..9cebe08e 100644 --- a/src/Core/Classes/UStruct.cs +++ b/src/Core/Classes/UStruct.cs @@ -69,6 +69,7 @@ public partial class UStruct : UField //protected uint _CodePosition; public long ScriptOffset { get; private set; } + public int ScriptSize { get; private set; } public UByteCodeDecompiler ByteCodeManager; @@ -233,6 +234,7 @@ protected override void Deserialize() } _Buffer.ConformRecordPosition(); + ScriptSize = (int)(_Buffer.Position - ScriptOffset); #if DNF if (Package.Build == UnrealPackage.GameBuild.BuildName.DNF) { diff --git a/src/Core/Tokens/ContextTokens.cs b/src/Core/Tokens/ContextTokens.cs index 9f7b18d0..89cf0d42 100644 --- a/src/Core/Tokens/ContextTokens.cs +++ b/src/Core/Tokens/ContextTokens.cs @@ -110,32 +110,33 @@ public override void Deserialize(IUnrealStream stream) Debug.Assert(Struct != null); } #endif - // TODO: Corrigate version. Definitely didn't exist in Roboblitz(369) - if (stream.Version > 369) + // TODO: Corrigate version. Definitely didn't exist in Roboblitz(369), first seen in MOHA(421). + if (stream.Version > 374) { Struct = stream.ReadObject(); Decompiler.AlignObjectSize(); Debug.Assert(Struct != null); #if MKKE - if (Package.Build != UnrealPackage.GameBuild.BuildName.MKKE) + if (Package.Build == UnrealPackage.GameBuild.BuildName.MKKE) { -#endif - // Copy? - stream.ReadByte(); - Decompiler.AlignSize(sizeof(byte)); -#if MKKE + goto skipToNext; } #endif + // Copy? + stream.ReadByte(); + Decompiler.AlignSize(sizeof(byte)); } - - // TODO: Corrigate version. Definitely didn't exist in MKKE(472), first seen in SWG(486). - if (stream.Version > 472) + + // TODO: Corrigate version. Definitely didn't exist in MKKE(472), first seen in FFOW(433). + if (stream.Version >= 433) { // Modification? stream.ReadByte(); Decompiler.AlignSize(sizeof(byte)); } + skipToNext: + // Pre-Context DeserializeNext(); } diff --git a/src/Eliot.UELib.csproj b/src/Eliot.UELib.csproj index 0a0da087..57f0aec3 100644 --- a/src/Eliot.UELib.csproj +++ b/src/Eliot.UELib.csproj @@ -1,6 +1,6 @@ - + - DECOMPILE;BINARYMETADATA;Forms;UE1;UE2;UE3;UE4;VENGEANCE;SWAT4;UNREAL2;INFINITYBLADE;BORDERLANDS2;GOW2;APB;SPECIALFORCE2;XIII;SINGULARITY;THIEF_DS;DEUSEX_IW;BORDERLANDS;MIRRORSEDGE;BIOSHOCK;HAWKEN;UT;DISHONORED;REMEMBERME;ALPHAPROTOCOL;VANGUARD;TERA;MKKE;TRANSFORMERS;XCOM2;DD2;DCUO;AA2;SPELLBORN;BATMAN;MOH;ROCKETLEAGUE;DNF;LSGAME;UNDYING;HP;DEVASTATION;SPLINTERCELL + DECOMPILE;BINARYMETADATA;Forms;UE1;UE2;UE3;UE4;VENGEANCE;SWAT4;UNREAL2;INFINITYBLADE;BORDERLANDS2;GOW2;APB;SPECIALFORCE2;XIII;SINGULARITY;THIEF_DS;DEUSEX_IW;BORDERLANDS;MIRRORSEDGE;BIOSHOCK;HAWKEN;UT;DISHONORED;REMEMBERME;ALPHAPROTOCOL;VANGUARD;TERA;MKKE;TRANSFORMERS;XCOM2;DD2;DCUO;AA2;SPELLBORN;BATMAN;MOH;ROCKETLEAGUE;DNF;LSGAME;UNDYING;HP;DEVASTATION;SPLINTERCELL;AHIT net48 Library UELib @@ -32,6 +32,7 @@ GlobalizationRules.ruleset + True none $(DefineConstants);TRACE; true @@ -70,10 +71,9 @@ - True Eliot.UELib $(AssemblyName) - $(VersionPrefix)1.4.0 + $(VersionPrefix)1.5.0 EliotVU $(AssemblyName) UnrealScript decompiler library for Unreal package files (.upk, .u, .uasset; etc), with support for Unreal Engine 1, 2, and 3. @@ -86,5 +86,6 @@ True True latest-recommended + Improved support for A Hat in Time (UE3) \ No newline at end of file diff --git a/src/Engine/Types/Poly.cs b/src/Engine/Types/Poly.cs index 9757c1f5..08b276e4 100644 --- a/src/Engine/Types/Poly.cs +++ b/src/Engine/Types/Poly.cs @@ -74,7 +74,7 @@ public TResult Accept(IVisitor visitor) public void Deserialize(IUnrealStream stream) { // Always 16 - int numVertices = stream.Version < (uint)PackageObjectLegacyVersion.FixedVerticesToArrayFromPoly + int verticesCount = stream.Version < (uint)PackageObjectLegacyVersion.FixedVerticesToArrayFromPoly ? stream.ReadIndex() : -1; @@ -89,7 +89,7 @@ public void Deserialize(IUnrealStream stream) } else { - stream.ReadArray(out Vertex, numVertices); + stream.ReadArray(out Vertex, verticesCount); } PolyFlags = stream.ReadUInt32(); @@ -170,4 +170,4 @@ public void Serialize(IUnrealStream stream) throw new NotImplementedException(); } } -} \ No newline at end of file +} diff --git a/src/UnrealFlags.cs b/src/UnrealFlags.cs index 7300a041..4c612209 100644 --- a/src/UnrealFlags.cs +++ b/src/UnrealFlags.cs @@ -344,6 +344,12 @@ public enum FunctionFlags : ulong K2Call = 0x04000000U, K2Override = 0x08000000U, K2Pure = 0x10000000U, +#if AHIT + AHIT_Multicast = 0x04000000U, + AHIT_NoOwnerRepl = 0x08000000U, + AHIT_Optional = 0x10000000U, + AHIT_EditorOnly = 0x20000000U, +#endif } /// @@ -424,6 +430,9 @@ public enum FunctionFlags : ulong EditInline = 0x04000000U, EdFindable = 0x08000000U, +#if AHIT + AHIT_Bitwise = 0x08000000U, +#endif EditInlineUse = 0x10000000U, Deprecated = 0x20000000U, @@ -472,6 +481,9 @@ public enum FunctionFlags : ulong // GAP! CrossLevelPassive = 0x00001000U, CrossLevelActive = 0x00002000U, +#if AHIT + AHIT_Serialize = 0x00004000U, +#endif #if BIOSHOCK BIOINF_Unk1 = 0x00080000U, @@ -530,6 +542,11 @@ public enum StateFlags : uint CollapseCategories = 0x00002000U, ExportStructs = 0x00004000U, // @Removed(UE3 in early but not latest) +#if AHIT + AHIT_AlwaysLoaded = 0x00008000U, + AHIT_IterOptimized = 0x00010000U, +#endif + Instanced = 0x00200000U, // @Removed(UE3) HideDropDown = 0x00400000U, // @Redefined(UE3, HasComponents), @Moved(UE3, HideDropDown2) ParseConfig = 0x01000000U, // @Redefined(UE3, Deprecated) diff --git a/src/UnrealPackage.cs b/src/UnrealPackage.cs index 5a1f9f23..5e6880d7 100644 --- a/src/UnrealPackage.cs +++ b/src/UnrealPackage.cs @@ -586,6 +586,15 @@ public enum BuildName /// [Build(874, 78u)] Battleborn, + /// + /// A Hat in Time + /// + /// 877:893/005 + /// + /// The earliest available version with any custom specifiers is 1.0 (877) - Un-Drew. + /// + [Build(877, 893, 5, 5)] AHIT, + /// /// Special Force 2 /// @@ -1502,9 +1511,16 @@ public void Deserialize(UPackageStream stream) "Branch.Serializer cannot be null. Did you forget to initialize the Serializer in PostDeserializeSummary?"); // We can't continue without decompressing. - if (CompressedChunks != null && CompressedChunks.Any()) + if (Summary.CompressedChunks != null && + Summary.CompressedChunks.Any()) { - return; + if (Summary.CompressionFlags != 0) + { + return; + } + + // Flags 0? Let's pretend that we no longer possess any chunks. + Summary.CompressedChunks.Clear(); } #if TERA if (Build == GameBuild.BuildName.Tera) Summary.NameCount = Generations.Last().NameCount; @@ -2258,4 +2274,4 @@ public void Dispose() #endregion } -} \ No newline at end of file +}