├── .gitignore ├── AESDecryptor.cs ├── BinaryHelper.cs ├── LICENSE ├── LocMetaReader.cs ├── LocResReader.cs ├── Pak ├── DefaultPakFilter.cs ├── IPakFilter.cs ├── PakFileReader.cs ├── PakFilter.cs ├── PakIndex.cs └── PakPackage.cs ├── PakReader.csproj ├── Parsers ├── Objects │ ├── EBulkDataFlags.cs │ ├── ECompressionFlags.cs │ ├── EDateTimeStyle.cs │ ├── EObjectFlags.cs │ ├── EPackageFlags.cs │ ├── EPakVersion.cs │ ├── EPixelFormat.cs │ ├── ETextFlag.cs │ ├── ETextHistoryType.cs │ ├── FByteBulkData.cs │ ├── FColor.cs │ ├── FCompressedChunk.cs │ ├── FCustomVersion.cs │ ├── FCustomVersionContainer.cs │ ├── FDateTime.cs │ ├── FEngineVersion.cs │ ├── FGameplayTagContainer.cs │ ├── FGenerationInfo.cs │ ├── FGuid.cs │ ├── FIntPoint.cs │ ├── FLinearColor.cs │ ├── FName.cs │ ├── FNameEntrySerialized.cs │ ├── FObjectExport.cs │ ├── FObjectImport.cs │ ├── FObjectResource.cs │ ├── FPackageFileSummary.cs │ ├── FPackageIndex.cs │ ├── FPakCompressedBlock.cs │ ├── FPakEntry.cs │ ├── FPakInfo.cs │ ├── FPropertyTag.cs │ ├── FQuat.cs │ ├── FRotator.cs │ ├── FSHAHash.cs │ ├── FSoftObjectPath.cs │ ├── FStripDataFlags.cs │ ├── FText.cs │ ├── FTextHistory.cs │ ├── FTextHistoryBase.cs │ ├── FTextHistoryDateTime.cs │ ├── FTextHistoryNone.cs │ ├── FTextKey.cs │ ├── FTexture2DMipMap.cs │ ├── FTexturePlatformData.cs │ ├── FVector.cs │ ├── FVector2D.cs │ ├── IUStruct.cs │ └── UScriptStruct.cs ├── PackageReader.cs ├── PropertyAttribute.cs ├── PropertyTagData │ ├── ArrayProperty.cs │ ├── BaseProperty.cs │ ├── BoolProperty.cs │ ├── ByteProperty.cs │ ├── DelegateProperty.cs │ ├── DoubleProperty.cs │ ├── EnumProperty.cs │ ├── FloatProperty.cs │ ├── Int16Property.cs │ ├── Int64Property.cs │ ├── Int8Property.cs │ ├── IntProperty.cs │ ├── InterfaceProperty.cs │ ├── LazyObjectProperty.cs │ ├── MapProperty.cs │ ├── MulticastDelegateProperty.cs │ ├── NameProperty.cs │ ├── ObjectProperty.cs │ ├── SetProperty.cs │ ├── SoftObjectProperty.cs │ ├── StrProperty.cs │ ├── StructProperty.cs │ ├── TextProperty.cs │ ├── UInt16Property.cs │ ├── UInt32Property.cs │ └── UInt64Property.cs ├── ReflectionHelper.cs ├── Texture2D.cs └── UObject.cs ├── README.md ├── ReaderExtensions.cs └── TextureDecoder.cs /.gitignore: -------------------------------------------------------------------------------- 1 | ## Ignore Visual Studio temporary files, build results, and 2 | ## files generated by popular Visual Studio add-ons. 3 | 4 | # User-specific files 5 | *.suo 6 | *.user 7 | *.userosscache 8 | *.sln.docstates 9 | 10 | # User-specific files (MonoDevelop/Xamarin Studio) 11 | *.userprefs 12 | 13 | # Build results 14 | [Dd]ebug/ 15 | [Dd]ebugPublic/ 16 | [Rr]elease/ 17 | [Rr]eleases/ 18 | x64/ 19 | x86/ 20 | bld/ 21 | [Bb]in/ 22 | [Oo]bj/ 23 | [Ll]og/ 24 | 25 | # Visual Studio 2015 cache/options directory 26 | .vs/ 27 | # Uncomment if you have tasks that create the project's static files in wwwroot 28 | #wwwroot/ 29 | 30 | # MSTest test Results 31 | [Tt]est[Rr]esult*/ 32 | [Bb]uild[Ll]og.* 33 | 34 | # NUNIT 35 | *.VisualState.xml 36 | TestResult.xml 37 | 38 | # Build Results of an ATL Project 39 | [Dd]ebugPS/ 40 | [Rr]eleasePS/ 41 | dlldata.c 42 | 43 | # DNX 44 | project.lock.json 45 | project.fragment.lock.json 46 | artifacts/ 47 | 48 | *_i.c 49 | *_p.c 50 | *_i.h 51 | *.ilk 52 | *.meta 53 | *.obj 54 | *.pch 55 | *.pdb 56 | *.pgc 57 | *.pgd 58 | *.rsp 59 | *.sbr 60 | *.tlb 61 | *.tli 62 | *.tlh 63 | *.tmp 64 | *.tmp_proj 65 | *.log 66 | *.vspscc 67 | *.vssscc 68 | .builds 69 | *.pidb 70 | *.svclog 71 | *.scc 72 | 73 | # Chutzpah Test files 74 | _Chutzpah* 75 | 76 | # Visual C++ cache files 77 | ipch/ 78 | *.aps 79 | *.ncb 80 | *.opendb 81 | *.opensdf 82 | *.sdf 83 | *.cachefile 84 | *.VC.db 85 | *.VC.VC.opendb 86 | 87 | # Visual Studio profiler 88 | *.psess 89 | *.vsp 90 | *.vspx 91 | *.sap 92 | 93 | # TFS 2012 Local Workspace 94 | $tf/ 95 | 96 | # Guidance Automation Toolkit 97 | *.gpState 98 | 99 | # ReSharper is a .NET coding add-in 100 | _ReSharper*/ 101 | *.[Rr]e[Ss]harper 102 | *.DotSettings.user 103 | 104 | # JustCode is a .NET coding add-in 105 | .JustCode 106 | 107 | # TeamCity is a build add-in 108 | _TeamCity* 109 | 110 | # DotCover is a Code Coverage Tool 111 | *.dotCover 112 | 113 | # NCrunch 114 | _NCrunch_* 115 | .*crunch*.local.xml 116 | nCrunchTemp_* 117 | 118 | # MightyMoose 119 | *.mm.* 120 | AutoTest.Net/ 121 | 122 | # Web workbench (sass) 123 | .sass-cache/ 124 | 125 | # Installshield output folder 126 | [Ee]xpress/ 127 | 128 | # DocProject is a documentation generator add-in 129 | DocProject/buildhelp/ 130 | DocProject/Help/*.HxT 131 | DocProject/Help/*.HxC 132 | DocProject/Help/*.hhc 133 | DocProject/Help/*.hhk 134 | DocProject/Help/*.hhp 135 | DocProject/Help/Html2 136 | DocProject/Help/html 137 | 138 | # Click-Once directory 139 | publish/ 140 | 141 | # Publish Web Output 142 | *.[Pp]ublish.xml 143 | *.azurePubxml 144 | # TODO: Comment the next line if you want to checkin your web deploy settings 145 | # but database connection strings (with potential passwords) will be unencrypted 146 | #*.pubxml 147 | *.publishproj 148 | 149 | # Microsoft Azure Web App publish settings. Comment the next line if you want to 150 | # checkin your Azure Web App publish settings, but sensitive information contained 151 | # in these scripts will be unencrypted 152 | PublishScripts/ 153 | 154 | # NuGet Packages 155 | *.nupkg 156 | # The packages folder can be ignored because of Package Restore 157 | **/packages/* 158 | # except build/, which is used as an MSBuild target. 159 | !**/packages/build/ 160 | # Uncomment if necessary however generally it will be regenerated when needed 161 | #!**/packages/repositories.config 162 | # NuGet v3's project.json files produces more ignoreable files 163 | *.nuget.props 164 | *.nuget.targets 165 | 166 | # Microsoft Azure Build Output 167 | csx/ 168 | *.build.csdef 169 | 170 | # Microsoft Azure Emulator 171 | ecf/ 172 | rcf/ 173 | 174 | # Windows Store app package directories and files 175 | AppPackages/ 176 | BundleArtifacts/ 177 | Package.StoreAssociation.xml 178 | _pkginfo.txt 179 | 180 | # Visual Studio cache files 181 | # files ending in .cache can be ignored 182 | *.[Cc]ache 183 | # but keep track of directories ending in .cache 184 | !*.[Cc]ache/ 185 | 186 | # Others 187 | ClientBin/ 188 | ~$* 189 | *~ 190 | *.dbmdl 191 | *.dbproj.schemaview 192 | *.jfm 193 | *.pfx 194 | *.publishsettings 195 | node_modules/ 196 | orleans.codegen.cs 197 | 198 | # Since there are multiple workflows, uncomment next line to ignore bower_components 199 | # (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) 200 | #bower_components/ 201 | 202 | # RIA/Silverlight projects 203 | Generated_Code/ 204 | 205 | # Backup & report files from converting an old project file 206 | # to a newer Visual Studio version. Backup files are not needed, 207 | # because we have git ;-) 208 | _UpgradeReport_Files/ 209 | Backup*/ 210 | UpgradeLog*.XML 211 | UpgradeLog*.htm 212 | 213 | # SQL Server files 214 | *.mdf 215 | *.ldf 216 | 217 | # Business Intelligence projects 218 | *.rdl.data 219 | *.bim.layout 220 | *.bim_*.settings 221 | 222 | # Microsoft Fakes 223 | FakesAssemblies/ 224 | 225 | # GhostDoc plugin setting file 226 | *.GhostDoc.xml 227 | 228 | # Node.js Tools for Visual Studio 229 | .ntvs_analysis.dat 230 | 231 | # Visual Studio 6 build log 232 | *.plg 233 | 234 | # Visual Studio 6 workspace options file 235 | *.opt 236 | 237 | # Visual Studio LightSwitch build output 238 | **/*.HTMLClient/GeneratedArtifacts 239 | **/*.DesktopClient/GeneratedArtifacts 240 | **/*.DesktopClient/ModelManifest.xml 241 | **/*.Server/GeneratedArtifacts 242 | **/*.Server/ModelManifest.xml 243 | _Pvt_Extensions 244 | 245 | # Paket dependency manager 246 | .paket/paket.exe 247 | paket-files/ 248 | 249 | # FAKE - F# Make 250 | .fake/ 251 | 252 | # JetBrains Rider 253 | .idea/ 254 | *.sln.iml 255 | 256 | # CodeRush 257 | .cr/ 258 | 259 | # Python Tools for Visual Studio (PTVS) 260 | __pycache__/ 261 | *.pyc 262 | 263 | lib/ 264 | -------------------------------------------------------------------------------- /AESDecryptor.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Security.Cryptography; 4 | 5 | namespace PakReader 6 | { 7 | static class AESDecryptor 8 | { 9 | public const int BLOCK_SIZE = 16 * 8; // 128 10 | static readonly Rijndael Cipher; 11 | static readonly Dictionary CachedTransforms = new Dictionary(); 12 | 13 | static AESDecryptor() 14 | { 15 | Cipher = Rijndael.Create(); 16 | Cipher.Mode = CipherMode.ECB; 17 | Cipher.Padding = PaddingMode.Zeros; 18 | Cipher.BlockSize = BLOCK_SIZE; 19 | } 20 | 21 | static ICryptoTransform GetDecryptor(byte[] key) 22 | { 23 | if (!CachedTransforms.TryGetValue(key, out var ret)) 24 | { 25 | CachedTransforms[key] = ret = Cipher.CreateDecryptor(key, null); 26 | } 27 | return ret; 28 | } 29 | 30 | public static int FindKey(byte[] data, IList keys) 31 | { 32 | byte[] block = new byte[BLOCK_SIZE]; 33 | for (int i = 0; i < keys.Count; i++) 34 | { 35 | using (var crypto = GetDecryptor(keys[i])) 36 | crypto.TransformBlock(data, 0, BLOCK_SIZE, block, 0); 37 | int stringLen = BitConverter.ToInt32(block, 0); 38 | if (stringLen > 512 || stringLen < -512) 39 | continue; 40 | if (stringLen < 0) 41 | { 42 | if (BitConverter.ToUInt16(block, (stringLen - 1) * 2 + 4) != 0) 43 | continue; 44 | } 45 | else 46 | { 47 | if (block[stringLen - 1 + 4] != 0) 48 | continue; 49 | } 50 | return i; 51 | } 52 | return -1; 53 | } 54 | 55 | public static byte[] DecryptAES(byte[] data, byte[] key) => 56 | GetDecryptor(key).TransformFinalBlock(data, 0, data.Length); 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /BinaryHelper.cs: -------------------------------------------------------------------------------- 1 | using System.Linq; 2 | 3 | namespace PakReader 4 | { 5 | static class BinaryHelper 6 | { 7 | public static uint Flip(uint value) => (value & 0x000000FFU) << 24 | (value & 0x0000FF00U) << 8 | 8 | (value & 0x00FF0000U) >> 8 | (value & 0xFF000000U) >> 24; 9 | 10 | static readonly uint[] _Lookup32 = Enumerable.Range(0, 256).Select(i => { 11 | string s = i.ToString("x2"); 12 | return s[0] + ((uint)s[1] << 16); 13 | }).ToArray(); 14 | public static string ToHex(byte[] bytes) 15 | { 16 | if (bytes == null) return null; 17 | var length = bytes.Length; 18 | var result = new char[length * 2]; 19 | for (int i = 0; i < length; i++) 20 | { 21 | var val = _Lookup32[bytes[i]]; 22 | result[2 * i] = (char)val; 23 | result[2 * i + 1] = (char)(val >> 16); 24 | } 25 | return new string(result); 26 | } 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Aleks Margarian 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /LocMetaReader.cs: -------------------------------------------------------------------------------- 1 | using System.IO; 2 | using PakReader.Parsers.Objects; 3 | 4 | namespace PakReader 5 | { 6 | public class LocMetaReader 7 | { 8 | static readonly FGuid Magic = new FGuid(0xA14CEE4F, 0x83554868, 0xBD464C6C, 0x7C50DA70); 9 | 10 | public readonly string NativeCulture; 11 | public readonly string NativeLocRes; 12 | 13 | public LocMetaReader(string path) : this(File.OpenRead(path)) { } 14 | 15 | public LocMetaReader(Stream stream) : this(new BinaryReader(stream)) { } 16 | 17 | public LocMetaReader(BinaryReader reader) 18 | { 19 | if (Magic != new FGuid(reader)) 20 | { 21 | throw new IOException("LocMeta file has an invalid magic constant!"); 22 | } 23 | 24 | var VersionNumber = (Version)reader.ReadByte(); 25 | if (VersionNumber > Version.LATEST) 26 | { 27 | throw new IOException($"LocMeta file is too new to be loaded! (File Version: {(byte)VersionNumber}, Loader Version: {(byte)Version.LATEST})"); 28 | } 29 | 30 | NativeCulture = reader.ReadFString(); 31 | NativeLocRes = reader.ReadFString(); 32 | } 33 | 34 | public enum Version : byte 35 | { 36 | /** Initial format. */ 37 | INITIAL = 0, 38 | 39 | LATEST_PLUS_ONE, 40 | LATEST = LATEST_PLUS_ONE - 1 41 | } 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /LocResReader.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.IO; 4 | using PakReader.Parsers.Objects; 5 | 6 | namespace PakReader 7 | { 8 | public class LocResReader 9 | { 10 | static readonly FGuid Magic = new FGuid(0x7574140E, 0xFC034A67, 0x9D90154A, 0x1B7F37C3); 11 | 12 | readonly Dictionary> Entries = new Dictionary>(); 13 | 14 | public LocResReader(string path) : this(File.OpenRead(path)) { } 15 | 16 | public LocResReader(Stream stream) : this(new BinaryReader(stream)) { } 17 | 18 | public LocResReader(BinaryReader reader) 19 | { 20 | var MagicNumber = new FGuid(reader); 21 | 22 | var VersionNumber = Version.LEGACY; 23 | if (MagicNumber == Magic) 24 | { 25 | VersionNumber = (Version)reader.ReadByte(); 26 | } 27 | else 28 | { 29 | // Legacy LocRes files lack the magic number, assume that's what we're dealing with, and seek back to the start of the file 30 | reader.BaseStream.Seek(0, SeekOrigin.Begin); 31 | } 32 | 33 | if (VersionNumber > Version.LATEST) 34 | { 35 | throw new IOException($"LocRes file is too new to be loaded! (File Version: {(byte)VersionNumber}, Loader Version: {(byte)LocMetaReader.Version.LATEST})"); 36 | } 37 | 38 | // Read the localized string array 39 | FTextLocalizationResourceString[] LocalizedStringArray = Array.Empty(); 40 | if (VersionNumber >= Version.COMPACT) 41 | { 42 | long LocalizedStringArrayOffset = -1; // INDEX_NONE 43 | LocalizedStringArrayOffset = reader.ReadInt64(); 44 | 45 | if (LocalizedStringArrayOffset != -1) 46 | { 47 | if (VersionNumber >= Version.OPTIMIZED) 48 | { 49 | long CurrentFileOffset = reader.BaseStream.Position; 50 | reader.BaseStream.Seek(LocalizedStringArrayOffset, SeekOrigin.Begin); 51 | LocalizedStringArray = reader.ReadTArray(() => new FTextLocalizationResourceString(reader)); 52 | reader.BaseStream.Seek(CurrentFileOffset, SeekOrigin.Begin); 53 | } 54 | else 55 | { 56 | string[] TmpLocalizedStringArray; 57 | 58 | long CurrentFileOffset = reader.BaseStream.Position; 59 | reader.BaseStream.Seek(LocalizedStringArrayOffset, SeekOrigin.Begin); 60 | TmpLocalizedStringArray = reader.ReadTArray(() => reader.ReadFString()); 61 | reader.BaseStream.Seek(CurrentFileOffset, SeekOrigin.Begin); 62 | 63 | LocalizedStringArray = new FTextLocalizationResourceString[TmpLocalizedStringArray.Length]; 64 | for (int i = 0; i < TmpLocalizedStringArray.Length; i++) 65 | { 66 | LocalizedStringArray[i] = new FTextLocalizationResourceString(TmpLocalizedStringArray[i], -1); 67 | } 68 | } 69 | } 70 | } 71 | 72 | // Read entries count 73 | if (VersionNumber >= Version.OPTIMIZED) 74 | { 75 | uint EntriesCount = reader.ReadUInt32(); 76 | // No need for initializer 77 | // Link: https://github.com/EpicGames/UnrealEngine/blob/7d9919ac7bfd80b7483012eab342cb427d60e8c9/Engine/Source/Runtime/Core/Private/Internationalization/TextLocalizationResource.cpp#L266 78 | } 79 | 80 | // Read namespace count 81 | uint NamespaceCount = reader.ReadUInt32(); 82 | 83 | for (uint i = 0; i < NamespaceCount; i++) 84 | { 85 | // Read namespace 86 | if (VersionNumber >= Version.OPTIMIZED) 87 | { 88 | reader.ReadUInt32(); // StrHash 89 | } 90 | var Namespace = reader.ReadFString(); 91 | 92 | var Entries = new Dictionary(); 93 | 94 | // Read key count 95 | uint KeyCount = reader.ReadUInt32(); 96 | 97 | for (uint j = 0; j < KeyCount; j++) 98 | { 99 | // Read key 100 | if (VersionNumber >= Version.OPTIMIZED) 101 | { 102 | reader.ReadUInt32(); // StrHash 103 | } 104 | var Key = reader.ReadFString(); 105 | 106 | reader.ReadUInt32(); // SourceStringHash 107 | 108 | string EntryLocalizedString; 109 | if (VersionNumber >= Version.COMPACT) 110 | { 111 | int LocalizedStringIndex = reader.ReadInt32(); 112 | 113 | if (LocalizedStringArray.Length > LocalizedStringIndex) 114 | { 115 | // Steal the string if possible 116 | ref var LocalizedString = ref LocalizedStringArray[LocalizedStringIndex]; 117 | if (LocalizedString.RefCount == 1) 118 | { 119 | EntryLocalizedString = LocalizedString.String; 120 | LocalizedString.RefCount--; 121 | } 122 | else 123 | { 124 | EntryLocalizedString = LocalizedString.String; 125 | if (LocalizedString.RefCount != -1) 126 | { 127 | LocalizedString.RefCount--; 128 | } 129 | } 130 | } 131 | else 132 | { 133 | throw new IOException($"LocRes has an invalid localized string index for namespace '{Namespace}' and key '{Key}'. This entry will have no translation."); 134 | } 135 | } 136 | else 137 | { 138 | EntryLocalizedString = reader.ReadFString(); 139 | } 140 | Entries.Add(Key, EntryLocalizedString); 141 | } 142 | this.Entries.Add(Namespace, Entries); 143 | } 144 | } 145 | 146 | public string this[string ns, string key] => Entries[ns][key]; 147 | public bool TryGetValue(string ns, string key, out string value) 148 | { 149 | value = null; 150 | return Entries.TryGetValue(ns, out var nsret) && nsret.TryGetValue(key, out value); 151 | } 152 | 153 | public enum Version : byte 154 | { 155 | /** Legacy format file - will be missing the magic number. */ 156 | LEGACY = 0, 157 | /** Compact format file - strings are stored in a LUT to avoid duplication. */ 158 | COMPACT, 159 | /** Optimized format file - namespaces/keys are pre-hashed, we know the number of elements up-front, and the number of references for each string in the LUT (to allow stealing). */ 160 | OPTIMIZED, 161 | 162 | LATEST_PLUS_ONE, 163 | LATEST = LATEST_PLUS_ONE - 1 164 | } 165 | 166 | struct FTextLocalizationResourceString 167 | { 168 | public readonly string String; 169 | public int RefCount; 170 | 171 | internal FTextLocalizationResourceString(BinaryReader reader) 172 | { 173 | String = reader.ReadFString(); 174 | RefCount = reader.ReadInt32(); 175 | } 176 | 177 | internal FTextLocalizationResourceString(string str, int refCount) 178 | { 179 | String = str; 180 | RefCount = refCount; 181 | } 182 | } 183 | } 184 | } 185 | -------------------------------------------------------------------------------- /Pak/DefaultPakFilter.cs: -------------------------------------------------------------------------------- 1 | namespace PakReader.Pak 2 | { 3 | class DefaultPakFilter : IPakFilter 4 | { 5 | public bool CheckFilter(string path, bool caseSensitive) => true; 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /Pak/IPakFilter.cs: -------------------------------------------------------------------------------- 1 | namespace PakReader.Pak 2 | { 3 | public interface IPakFilter 4 | { 5 | bool CheckFilter(string path, bool caseSensitive); 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /Pak/PakFileReader.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections; 3 | using System.Collections.Generic; 4 | using System.IO; 5 | using System.Linq; 6 | using System.Text; 7 | using PakReader.Parsers.Objects; 8 | 9 | namespace PakReader.Pak 10 | { 11 | public sealed class PakFileReader : IReadOnlyDictionary 12 | { 13 | public FPakInfo Info { get; } 14 | public Stream Stream { get; } 15 | public bool CaseSensitive { get; } 16 | public byte[] Key { get; private set; } 17 | public string MountPoint { get; private set; } 18 | public bool Initialized { get; private set; } 19 | 20 | readonly BinaryReader Reader; 21 | Dictionary Entries; 22 | 23 | // Buffered streams increase performance dramatically 24 | public PakFileReader(string file, bool caseSensitive = true) : this(new BufferedStream(File.OpenRead(file)), caseSensitive) { } 25 | 26 | public PakFileReader(Stream stream, bool caseSensitive = true) 27 | { 28 | Stream = stream; 29 | CaseSensitive = caseSensitive; 30 | Reader = new BinaryReader(stream, Encoding.Default, true); 31 | stream.Seek(-FPakInfo.SERIALIZED_SIZE, SeekOrigin.End); 32 | Info = new FPakInfo(Reader); 33 | } 34 | 35 | public bool TryReadIndex(byte[] key, PakFilter filter = null) 36 | { 37 | ReadIndexInternal(key, filter, out var exc); 38 | return exc == null; 39 | } 40 | 41 | public void ReadIndex(byte[] key, PakFilter filter = null) 42 | { 43 | ReadIndexInternal(key, filter, out var exc); 44 | if (exc != null) 45 | throw exc; 46 | } 47 | 48 | void ReadIndexInternal(byte[] key, PakFilter filter, out Exception exc) 49 | { 50 | if (Initialized) 51 | { 52 | exc = new InvalidOperationException("Index is already initialized"); 53 | return; 54 | } 55 | 56 | if (Info.bEncryptedIndex && key == null) 57 | { 58 | exc = new ArgumentException("Index is encrypted but no key was provided", nameof(key)); 59 | return; 60 | } 61 | 62 | Stream.Position = Info.IndexOffset; 63 | 64 | BinaryReader IndexReader; 65 | if (Info.bEncryptedIndex) 66 | { 67 | IndexReader = new BinaryReader(new MemoryStream(AESDecryptor.DecryptAES(Reader.ReadBytes((int)Info.IndexSize), key))); 68 | int stringLen = IndexReader.ReadInt32(); 69 | if (stringLen > 512 || stringLen < -512) 70 | { 71 | exc = new ArgumentException("The provided key is invalid", nameof(key)); 72 | return; 73 | } 74 | if (stringLen < 0) 75 | { 76 | IndexReader.BaseStream.Position += (stringLen - 1) * 2; 77 | if (IndexReader.ReadUInt16() != 0) 78 | { 79 | exc = new ArgumentException("The provided key is invalid", nameof(key)); 80 | return; 81 | } 82 | } 83 | else 84 | { 85 | IndexReader.BaseStream.Position += stringLen - 1; 86 | if (IndexReader.ReadByte() != 0) 87 | { 88 | exc = new ArgumentException("The provided key is invalid", nameof(key)); 89 | return; 90 | } 91 | } 92 | IndexReader.BaseStream.Position = 0; 93 | } 94 | else 95 | { 96 | IndexReader = Reader; 97 | } 98 | 99 | if (Info.Version >= EPakVersion.PATH_HASH_INDEX) 100 | { 101 | ReadIndexUpdated(IndexReader, key, Stream.Length, filter); 102 | } 103 | else 104 | { 105 | 106 | // https://github.com/EpicGames/UnrealEngine/blob/bf95c2cbc703123e08ab54e3ceccdd47e48d224a/Engine/Source/Runtime/PakFile/Private/IPlatformFilePak.cpp#L4509 107 | 108 | MountPoint = IndexReader.ReadFString(); 109 | if (MountPoint.StartsWith("../../..")) 110 | { 111 | MountPoint = MountPoint.Substring(8); 112 | } 113 | else 114 | { 115 | // Weird mount point location... 116 | MountPoint = "/"; 117 | } 118 | if (!CaseSensitive) 119 | { 120 | MountPoint = MountPoint.ToLowerInvariant(); 121 | } 122 | 123 | var NumEntries = IndexReader.ReadInt32(); 124 | Entries = new Dictionary(NumEntries); 125 | for (int i = 0; i < NumEntries; i++) 126 | { 127 | var filename = CaseSensitive ? IndexReader.ReadFString() : IndexReader.ReadFString().ToLowerInvariant(); 128 | var entry = new FPakEntry(IndexReader, Info.Version); 129 | // if there is no filter OR the filter passes 130 | if (filter == null || filter.CheckFilter(MountPoint + filename, CaseSensitive)) 131 | { 132 | // Filename is without the MountPoint concatenated to save memory 133 | Entries[filename] = entry; 134 | } 135 | } 136 | } 137 | 138 | if (Info.bEncryptedIndex) 139 | { 140 | // underlying stream is a MemoryStream of the decrypted index, might improve performance with a crypto stream of some sort 141 | IndexReader.Dispose(); 142 | } 143 | Reader.Dispose(); 144 | Key = key; 145 | Initialized = true; 146 | exc = null; 147 | } 148 | 149 | void ReadIndexUpdated(BinaryReader reader, byte[] aesKey, long totalSize, PakFilter filter) 150 | { 151 | MountPoint = reader.ReadFString(); 152 | if (MountPoint.StartsWith("../../..")) 153 | { 154 | MountPoint = MountPoint.Substring(8); 155 | } 156 | else 157 | { 158 | // Weird mount point location... 159 | MountPoint = "/"; 160 | } 161 | if (!CaseSensitive) 162 | { 163 | MountPoint = MountPoint.ToLowerInvariant(); 164 | } 165 | var NumEntries = reader.ReadInt32(); 166 | var PathHashSeed = reader.ReadUInt64(); 167 | 168 | bool bReaderHasPathHashIndex = false; 169 | long PathHashIndexOffset = -1; // INDEX_NONE 170 | long PathHashIndexSize = 0; 171 | FSHAHash PathHashIndexHash = default; 172 | bReaderHasPathHashIndex = reader.ReadInt32() != 0; 173 | if (bReaderHasPathHashIndex) 174 | { 175 | PathHashIndexOffset = reader.ReadInt64(); 176 | PathHashIndexSize = reader.ReadInt64(); 177 | PathHashIndexHash = new FSHAHash(reader); 178 | bReaderHasPathHashIndex = bReaderHasPathHashIndex && PathHashIndexOffset != -1; 179 | } 180 | 181 | bool bReaderHasFullDirectoryIndex = false; 182 | long FullDirectoryIndexOffset = -1; // INDEX_NONE 183 | long FullDirectoryIndexSize = 0; 184 | FSHAHash FullDirectoryIndexHash = default; 185 | bReaderHasFullDirectoryIndex = reader.ReadInt32() != 0; 186 | if (bReaderHasFullDirectoryIndex) 187 | { 188 | FullDirectoryIndexOffset = reader.ReadInt64(); 189 | FullDirectoryIndexSize = reader.ReadInt64(); 190 | FullDirectoryIndexHash = new FSHAHash(reader); 191 | bReaderHasFullDirectoryIndex = bReaderHasFullDirectoryIndex && FullDirectoryIndexOffset != -1; 192 | } 193 | 194 | byte[] EncodedPakEntries = reader.ReadTArray(() => reader.ReadByte()); 195 | File.WriteAllBytes("pakentryencoded", EncodedPakEntries); 196 | 197 | int FilesNum = reader.ReadInt32(); 198 | if (FilesNum < 0) 199 | { 200 | // Should not be possible for any values in the PrimaryIndex to be invalid, since we verified the index hash 201 | throw new FileLoadException("Corrupt pak PrimaryIndex detected!"); 202 | } 203 | FPakEntry[] Files = new FPakEntry[FilesNum]; // from what i can see, there aren't any??? 204 | if (FilesNum > 0) 205 | { 206 | for (int FileIndex = 0; FileIndex < FilesNum; ++FileIndex) 207 | { 208 | Files[FileIndex] = new FPakEntry(reader, Info.Version); 209 | } 210 | } 211 | 212 | // Decide which SecondaryIndex(es) to load 213 | bool bWillUseFullDirectoryIndex; 214 | bool bWillUsePathHashIndex; 215 | bool bReadFullDirectoryIndex; 216 | if (bReaderHasPathHashIndex && bReaderHasFullDirectoryIndex) 217 | { 218 | bWillUseFullDirectoryIndex = false; // https://github.com/EpicGames/UnrealEngine/blob/79a64829237ae339118bb50b61d84e4599c14e8a/Engine/Source/Runtime/PakFile/Private/IPlatformFilePak.cpp#L5628 219 | bWillUsePathHashIndex = !bWillUseFullDirectoryIndex; 220 | bool bWantToReadFullDirectoryIndex = false; 221 | bReadFullDirectoryIndex = bReaderHasFullDirectoryIndex && bWantToReadFullDirectoryIndex; 222 | } 223 | else if (bReaderHasPathHashIndex) 224 | { 225 | bWillUsePathHashIndex = true; 226 | bWillUseFullDirectoryIndex = false; 227 | bReadFullDirectoryIndex = false; 228 | } 229 | else if (bReaderHasFullDirectoryIndex) 230 | { 231 | // We don't support creating the PathHash Index at runtime; we want to move to having only the PathHashIndex, so supporting not having it at all is not useful enough to write 232 | bWillUsePathHashIndex = false; 233 | bWillUseFullDirectoryIndex = true; 234 | bReadFullDirectoryIndex = true; 235 | } 236 | else 237 | { 238 | // It should not be possible for PrimaryIndexes to be built without a PathHashIndex AND without a FullDirectoryIndex; CreatePakFile in UnrealPak.exe has a check statement for it. 239 | throw new FileLoadException("Corrupt pak PrimaryIndex detected!"); 240 | } 241 | 242 | // Load the Secondary Index(es) 243 | byte[] PathHashIndexData; 244 | Dictionary PathHashIndex; 245 | BinaryReader PathHashIndexReader = default; 246 | bool bHasPathHashIndex; 247 | if (bWillUsePathHashIndex) 248 | { 249 | if (PathHashIndexOffset < 0 || totalSize < (PathHashIndexOffset + PathHashIndexSize)) 250 | { 251 | // Should not be possible for these values (which came from the PrimaryIndex) to be invalid, since we verified the index hash of the PrimaryIndex 252 | throw new FileLoadException("Corrupt pak PrimaryIndex detected!"); 253 | //UE_LOG(LogPakFile, Log, TEXT(" Filename: %s"), *PakFilename); 254 | //UE_LOG(LogPakFile, Log, TEXT(" Total Size: %d"), Reader->TotalSize()); 255 | //UE_LOG(LogPakFile, Log, TEXT(" PathHashIndexOffset : %d"), PathHashIndexOffset); 256 | //UE_LOG(LogPakFile, Log, TEXT(" PathHashIndexSize: %d"), PathHashIndexSize); 257 | } 258 | Reader.BaseStream.Position = PathHashIndexOffset; 259 | PathHashIndexData = Reader.ReadBytes((int)PathHashIndexSize); 260 | File.WriteAllBytes("indexdata.daa", PathHashIndexData); 261 | 262 | { 263 | if (!DecryptAndValidateIndex(Reader, ref PathHashIndexData, aesKey, PathHashIndexHash, out var ComputedHash)) 264 | { 265 | throw new FileLoadException("Corrupt pak PrimaryIndex detected!"); 266 | //UE_LOG(LogPakFile, Log, TEXT(" Filename: %s"), *PakFilename); 267 | //UE_LOG(LogPakFile, Log, TEXT(" Encrypted: %d"), Info.bEncryptedIndex); 268 | //UE_LOG(LogPakFile, Log, TEXT(" Total Size: %d"), Reader->TotalSize()); 269 | //UE_LOG(LogPakFile, Log, TEXT(" Index Offset: %d"), FullDirectoryIndexOffset); 270 | //UE_LOG(LogPakFile, Log, TEXT(" Index Size: %d"), FullDirectoryIndexSize); 271 | //UE_LOG(LogPakFile, Log, TEXT(" Stored Index Hash: %s"), *PathHashIndexHash.ToString()); 272 | //UE_LOG(LogPakFile, Log, TEXT(" Computed Index Hash: %s"), *ComputedHash.ToString()); 273 | } 274 | } 275 | 276 | PathHashIndexReader = new BinaryReader(new MemoryStream(PathHashIndexData)); 277 | PathHashIndex = ReadPathHashIndex(PathHashIndexReader); 278 | bHasPathHashIndex = true; 279 | } 280 | 281 | var DirectoryIndex = new Dictionary>(); 282 | bool bHasFullDirectoryIndex; 283 | if (!bReadFullDirectoryIndex) 284 | { 285 | DirectoryIndex = ReadDirectoryIndex(PathHashIndexReader); 286 | bHasFullDirectoryIndex = false; 287 | } 288 | if (DirectoryIndex.Count == 0) 289 | { 290 | if (totalSize < (FullDirectoryIndexOffset + FullDirectoryIndexSize) || 291 | FullDirectoryIndexOffset < 0) 292 | { 293 | // Should not be possible for these values (which came from the PrimaryIndex) to be invalid, since we verified the index hash of the PrimaryIndex 294 | throw new FileLoadException("Corrupt pak PrimaryIndex detected!"); 295 | //UE_LOG(LogPakFile, Log, TEXT(" Filename: %s"), *PakFilename); 296 | //UE_LOG(LogPakFile, Log, TEXT(" Total Size: %d"), Reader->TotalSize()); 297 | //UE_LOG(LogPakFile, Log, TEXT(" FullDirectoryIndexOffset : %d"), FullDirectoryIndexOffset); 298 | //UE_LOG(LogPakFile, Log, TEXT(" FullDirectoryIndexSize: %d"), FullDirectoryIndexSize); 299 | } 300 | Reader.BaseStream.Position = FullDirectoryIndexOffset; 301 | byte[] FullDirectoryIndexData = Reader.ReadBytes((int)FullDirectoryIndexSize); 302 | 303 | { 304 | if (!DecryptAndValidateIndex(Reader, ref FullDirectoryIndexData, aesKey, FullDirectoryIndexHash, out var ComputedHash)) 305 | { 306 | throw new FileLoadException("Corrupt pak PrimaryIndex detected!"); 307 | //UE_LOG(LogPakFile, Log, TEXT(" Filename: %s"), *PakFilename); 308 | //UE_LOG(LogPakFile, Log, TEXT(" Encrypted: %d"), Info.bEncryptedIndex); 309 | //UE_LOG(LogPakFile, Log, TEXT(" Total Size: %d"), Reader->TotalSize()); 310 | //UE_LOG(LogPakFile, Log, TEXT(" Index Offset: %d"), FullDirectoryIndexOffset); 311 | //UE_LOG(LogPakFile, Log, TEXT(" Index Size: %d"), FullDirectoryIndexSize); 312 | //UE_LOG(LogPakFile, Log, TEXT(" Stored Index Hash: %s"), *FullDirectoryIndexHash.ToString()); 313 | //UE_LOG(LogPakFile, Log, TEXT(" Computed Index Hash: %s"), *ComputedHash.ToString()); 314 | } 315 | } 316 | 317 | var SecondaryIndexReader = new BinaryReader(new MemoryStream(FullDirectoryIndexData)); 318 | DirectoryIndex = ReadDirectoryIndex(SecondaryIndexReader); 319 | bHasFullDirectoryIndex = true; 320 | } 321 | 322 | Entries = new Dictionary(NumEntries); 323 | foreach (var (dirname, dir) in DirectoryIndex) 324 | { 325 | foreach(var (filename, pakLocation) in dir) 326 | { 327 | var path = dirname + filename; 328 | if (!CaseSensitive) 329 | { 330 | path = path.ToLowerInvariant(); 331 | } 332 | // if there is no filter OR the filter passes 333 | if (filter == null || filter.CheckFilter(MountPoint + filename, CaseSensitive)) 334 | { 335 | // Filename is without the MountPoint concatenated to save memory 336 | Entries[path] = GetEntry(pakLocation, EncodedPakEntries); 337 | } 338 | } 339 | } 340 | } 341 | 342 | Dictionary ReadPathHashIndex(BinaryReader reader) 343 | { 344 | var ret = new Dictionary(); 345 | var keys = reader.ReadTArray(() => (reader.ReadUInt64(), reader.ReadInt32())); 346 | foreach (var (k, v) in keys) 347 | { 348 | ret[k] = v; 349 | } 350 | return ret; 351 | } 352 | 353 | Dictionary> ReadDirectoryIndex(BinaryReader reader) 354 | { 355 | var ret = new Dictionary>(); 356 | var keys = reader.ReadTArray(() => (reader.ReadFString(), ReadFPakDirectory(reader))); 357 | foreach(var (k,v) in keys) 358 | { 359 | ret[k] = v; 360 | } 361 | return ret; 362 | } 363 | 364 | Dictionary ReadFPakDirectory(BinaryReader reader) 365 | { 366 | var ret = new Dictionary(); 367 | var keys = reader.ReadTArray(() => (reader.ReadFString(), reader.ReadInt32())); 368 | foreach (var (k, v) in keys) 369 | { 370 | ret[k] = v; 371 | } 372 | return ret; 373 | } 374 | 375 | bool DecryptAndValidateIndex(BinaryReader reader, ref byte[] IndexData, byte[] aesKey, FSHAHash ExpectedHash, out FSHAHash OutHash) 376 | { 377 | if (Info.bEncryptedIndex) 378 | { 379 | IndexData = AESDecryptor.DecryptAES(IndexData, aesKey); 380 | } 381 | OutHash = ExpectedHash; // too lazy to actually check against the hash 382 | // https://github.com/EpicGames/UnrealEngine/blob/79a64829237ae339118bb50b61d84e4599c14e8a/Engine/Source/Runtime/PakFile/Private/IPlatformFilePak.cpp#L5371 383 | return true; 384 | } 385 | 386 | FPakEntry GetEntry(int pakLocation, byte[] encodedPakEntries) 387 | { 388 | if (pakLocation >= 0) 389 | { 390 | // Grab the big bitfield value: 391 | // Bit 31 = Offset 32-bit safe? 392 | // Bit 30 = Uncompressed size 32-bit safe? 393 | // Bit 29 = Size 32-bit safe? 394 | // Bits 28-23 = Compression method 395 | // Bit 22 = Encrypted 396 | // Bits 21-6 = Compression blocks count 397 | // Bits 5-0 = Compression block size 398 | 399 | // Filter out the CompressionMethod. 400 | 401 | long Offset, UncompressedSize, Size; 402 | uint CompressionMethodIndex, CompressionBlockSize; 403 | bool Encrypted, Deleted; 404 | 405 | uint Value = BitConverter.ToUInt32(encodedPakEntries, pakLocation); 406 | pakLocation += sizeof(uint); 407 | 408 | CompressionMethodIndex = ((Value >> 23) & 0x3f); 409 | 410 | // Test for 32-bit safe values. Grab it, or memcpy the 64-bit value 411 | // to avoid alignment exceptions on platforms requiring 64-bit alignment 412 | // for 64-bit variables. 413 | // 414 | // Read the Offset. 415 | bool bIsOffset32BitSafe = (Value & (1 << 31)) != 0; 416 | if (bIsOffset32BitSafe) 417 | { 418 | Offset = BitConverter.ToUInt32(encodedPakEntries, pakLocation); 419 | pakLocation += sizeof(uint); 420 | } 421 | else 422 | { 423 | Offset = BitConverter.ToInt64(encodedPakEntries, pakLocation); 424 | pakLocation += sizeof(long); 425 | } 426 | 427 | // Read the UncompressedSize. 428 | bool bIsUncompressedSize32BitSafe = (Value & (1 << 30)) != 0; 429 | if (bIsUncompressedSize32BitSafe) 430 | { 431 | UncompressedSize = BitConverter.ToUInt32(encodedPakEntries, pakLocation); 432 | pakLocation += sizeof(uint); 433 | } 434 | else 435 | { 436 | UncompressedSize = BitConverter.ToInt64(encodedPakEntries, pakLocation); 437 | pakLocation += sizeof(long); 438 | } 439 | 440 | // Fill in the Size. 441 | if (CompressionMethodIndex != 0) 442 | { 443 | // Size is only present if compression is applied. 444 | bool bIsSize32BitSafe = (Value & (1 << 29)) != 0; 445 | if (bIsSize32BitSafe) 446 | { 447 | Size = BitConverter.ToUInt32(encodedPakEntries, pakLocation); 448 | pakLocation += sizeof(uint); 449 | } 450 | else 451 | { 452 | Size = BitConverter.ToInt64(encodedPakEntries, pakLocation); 453 | pakLocation += sizeof(long); 454 | } 455 | } 456 | else 457 | { 458 | // The Size is the same thing as the UncompressedSize when 459 | // CompressionMethod == COMPRESS_None. 460 | Size = UncompressedSize; 461 | } 462 | 463 | // Filter the encrypted flag. 464 | Encrypted = (Value & (1 << 22)) != 0; 465 | 466 | // This should clear out any excess CompressionBlocks that may be valid in the user's 467 | // passed in entry. 468 | var CompressionBlocksCount = (Value >> 6) & 0xffff; 469 | FPakCompressedBlock[] CompressionBlocks = new FPakCompressedBlock[CompressionBlocksCount]; 470 | 471 | // Filter the compression block size or use the UncompressedSize if less that 64k. 472 | CompressionBlockSize = 0; 473 | if (CompressionBlocksCount > 0) 474 | { 475 | CompressionBlockSize = UncompressedSize < 65536 ? (uint)UncompressedSize : ((Value & 0x3f) << 11); 476 | } 477 | 478 | // Set bDeleteRecord to false, because it obviously isn't deleted if we are here. 479 | Deleted = false; 480 | 481 | // Base offset to the compressed data 482 | long BaseOffset = true ? 0 : Offset; // HasRelativeCompressedChunkOffsets -> Version >= PakFile_Version_RelativeChunkOffsets 483 | 484 | // Handle building of the CompressionBlocks array. 485 | if (CompressionBlocks.Length == 1 && !Encrypted) 486 | { 487 | // If the number of CompressionBlocks is 1, we didn't store any extra information. 488 | // Derive what we can from the entry's file offset and size. 489 | var start = BaseOffset + FPakEntry.GetSize(EPakVersion.LATEST, CompressionMethodIndex, CompressionBlocksCount); 490 | CompressionBlocks[0] = new FPakCompressedBlock(start, start + Size); 491 | } 492 | else if (CompressionBlocks.Length > 0) 493 | { 494 | // Get the right pointer to start copying the CompressionBlocks information from. 495 | 496 | // Alignment of the compressed blocks 497 | var CompressedBlockAlignment = Encrypted ? AESDecryptor.BLOCK_SIZE : 1; 498 | 499 | // CompressedBlockOffset is the starting offset. Everything else can be derived from there. 500 | long CompressedBlockOffset = BaseOffset + FPakEntry.GetSize(EPakVersion.LATEST, CompressionMethodIndex, CompressionBlocksCount); 501 | for (int CompressionBlockIndex = 0; CompressionBlockIndex < CompressionBlocks.Length; ++CompressionBlockIndex) 502 | { 503 | CompressionBlocks[CompressionBlockIndex] = new FPakCompressedBlock(CompressedBlockOffset, CompressedBlockOffset + BitConverter.ToUInt32(encodedPakEntries, pakLocation)); 504 | pakLocation += sizeof(uint); 505 | { 506 | var toAlign = CompressionBlocks[CompressionBlockIndex].CompressedEnd - CompressionBlocks[CompressionBlockIndex].CompressedStart; 507 | CompressedBlockOffset += toAlign + CompressedBlockAlignment - (toAlign % CompressedBlockAlignment); 508 | } 509 | } 510 | } 511 | return new FPakEntry(Offset, Size, UncompressedSize, new byte[20], CompressionBlocks, CompressionBlockSize, CompressionMethodIndex, (byte)((Encrypted ? 0x01 : 0x00) | (Deleted ? 0x02 : 0x00))); 512 | } 513 | else 514 | { 515 | pakLocation = -(pakLocation + 1); 516 | throw new FileLoadException("list indexes aren't supported"); 517 | } 518 | } 519 | 520 | // path is without the mountpoint (not even if it's "/") 521 | public bool TryGetFile(string path, out ArraySegment ret) 522 | { 523 | if (Entries.TryGetValue(CaseSensitive ? path : path.ToLowerInvariant(), out var entry)) 524 | { 525 | ret = entry.GetData(Stream, Key); 526 | return true; 527 | } 528 | ret = null; 529 | return false; 530 | } 531 | public ReadOnlyMemory GetFile(string path) => Entries[CaseSensitive ? path : path.ToLowerInvariant()].GetData(Stream, Key); 532 | 533 | // IReadOnlyDictionary implementation (to prevent writing to the Entries dictionary 534 | 535 | // TODO: Make these methods respect CaseSensitive property 536 | FPakEntry IReadOnlyDictionary.this[string key] => Entries[key]; 537 | IEnumerable IReadOnlyDictionary.Keys => Entries.Keys; 538 | IEnumerable IReadOnlyDictionary.Values => Entries.Values; 539 | int IReadOnlyCollection>.Count => Entries.Count; 540 | 541 | bool IReadOnlyDictionary.ContainsKey(string key) => Entries.ContainsKey(key); 542 | IEnumerator> IEnumerable>.GetEnumerator() => Entries.GetEnumerator(); 543 | IEnumerator IEnumerable.GetEnumerator() => Entries.GetEnumerator(); 544 | bool IReadOnlyDictionary.TryGetValue(string key, out FPakEntry value) => Entries.TryGetValue(key, out value); 545 | } 546 | } 547 | -------------------------------------------------------------------------------- /Pak/PakFilter.cs: -------------------------------------------------------------------------------- 1 | using System.Collections; 2 | using System.Collections.Generic; 3 | 4 | namespace PakReader.Pak 5 | { 6 | // Currently only supports strings that start with a value 7 | // I've just implemented this myself to save tons of memory so you don't have to 8 | // allocate FPakEntries on Fortnite emoji textures you're not going to use 9 | public class PakFilter : IPakFilter, IEnumerable 10 | { 11 | public bool CaseSensitive { get; } 12 | readonly List Filter; 13 | 14 | // only 1 instance 15 | public static readonly IPakFilter Default = new DefaultPakFilter(); 16 | 17 | public PakFilter(IEnumerable filter, bool caseSensitive = true) 18 | { 19 | Filter = new List(filter); 20 | if (!caseSensitive) 21 | { 22 | for (int i = 0; i < Filter.Count; i++) 23 | { 24 | Filter[i] = Filter[i].ToLowerInvariant(); 25 | } 26 | } 27 | CaseSensitive = caseSensitive; 28 | } 29 | 30 | public bool AddFilter(string filter) 31 | { 32 | if (!CaseSensitive) filter = filter.ToLowerInvariant(); 33 | if (!Filter.Contains(filter)) 34 | { 35 | Filter.Add(filter); 36 | return true; 37 | } 38 | return false; 39 | } 40 | 41 | public int AddFilters(IEnumerable filter) 42 | { 43 | var i = 0; 44 | foreach (var s in filter) 45 | if (AddFilter(s)) i++; 46 | return i; 47 | } 48 | 49 | public bool CheckFilter(string path, bool caseSensitive) 50 | { 51 | // path is case sensitive but the filter isn't 52 | if (caseSensitive && !CaseSensitive) 53 | path = path.ToLowerInvariant(); 54 | foreach (var filter in Filter) 55 | if (path.StartsWith(filter)) 56 | return true; 57 | return false; 58 | } 59 | 60 | public IEnumerator GetEnumerator() => Filter.GetEnumerator(); 61 | IEnumerator IEnumerable.GetEnumerator() => Filter.GetEnumerator(); 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /Pak/PakIndex.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections; 3 | using System.Collections.Generic; 4 | using System.IO; 5 | using PakReader.Parsers.Objects; 6 | 7 | namespace PakReader.Pak 8 | { 9 | public class PakIndex : IEnumerable 10 | { 11 | public bool CacheFiles { get; } 12 | public bool CaseSensitive { get; } 13 | public PakFilter Filter { get; } 14 | readonly List PakFiles = new List(); 15 | 16 | public PakIndex(bool cacheFiles, bool caseSensitive = true) : this(cacheFiles, caseSensitive, null) { } 17 | public PakIndex(bool cacheFiles, bool caseSensitive = true, IEnumerable filter = null) : this(cacheFiles, caseSensitive, new PakFilter(filter, caseSensitive)) { } 18 | public PakIndex(bool cacheFiles, bool caseSensitive = true, PakFilter filter = null) 19 | { 20 | CacheFiles = cacheFiles; 21 | CaseSensitive = caseSensitive; 22 | Filter = filter; 23 | if (CacheFiles) 24 | CachedFiles = new Dictionary>(); 25 | } 26 | 27 | public PakIndex(string path, bool cacheFiles, bool caseSensitive = true) : this(path, cacheFiles, caseSensitive, null) { } 28 | public PakIndex(string path, bool cacheFiles, bool caseSensitive = true, IEnumerable filter = null) : this(path, cacheFiles, caseSensitive, new PakFilter(filter, caseSensitive)) { } 29 | public PakIndex(string path, bool cacheFiles, bool caseSensitive = true, PakFilter filter = null) : this(Directory.EnumerateFiles(path, "*.pak"), cacheFiles, caseSensitive, filter) { } 30 | 31 | public PakIndex(IEnumerable files, bool cacheFiles, bool caseSensitive = true) : this(files, cacheFiles, caseSensitive, null) { } 32 | public PakIndex(IEnumerable files, bool cacheFiles, bool caseSensitive = true, IEnumerable filter = null) : this(files, cacheFiles, caseSensitive, new PakFilter(filter, caseSensitive)) { } 33 | public PakIndex(IEnumerable files, bool cacheFiles, bool caseSensitive = true, PakFilter filter = null) 34 | { 35 | CacheFiles = cacheFiles; 36 | CaseSensitive = caseSensitive; 37 | Filter = filter; 38 | if (CacheFiles) 39 | CachedFiles = new Dictionary>(); 40 | foreach (var file in files) 41 | { 42 | PakFiles.Add(new PakFileReader(file, caseSensitive)); 43 | } 44 | } 45 | 46 | public PakIndex(IEnumerable streams, bool cacheFiles, bool caseSensitive = true) : this(streams, cacheFiles, caseSensitive, null) { } 47 | public PakIndex(IEnumerable streams, bool cacheFiles, bool caseSensitive = true, IEnumerable filter = null) : this(streams, cacheFiles, caseSensitive, new PakFilter(filter, caseSensitive)) { } 48 | public PakIndex(IEnumerable streams, bool cacheFiles, bool caseSensitive = true, PakFilter filter = null) 49 | { 50 | CacheFiles = cacheFiles; 51 | CaseSensitive = caseSensitive; 52 | Filter = filter; 53 | if (CacheFiles) 54 | CachedFiles = new Dictionary>(); 55 | foreach (var stream in streams) 56 | { 57 | PakFiles.Add(new PakFileReader(stream, caseSensitive)); 58 | } 59 | } 60 | 61 | public void AddPak(string path, byte[] key = null) 62 | { 63 | var reader = new PakFileReader(path, CaseSensitive); 64 | if (key != null) 65 | reader.ReadIndex(key, Filter); 66 | PakFiles.Add(reader); 67 | } 68 | public void AddPak(Stream stream, byte[] key = null) 69 | { 70 | var reader = new PakFileReader(stream, CaseSensitive); 71 | if (key != null) 72 | reader.ReadIndex(key, Filter); 73 | PakFiles.Add(reader); 74 | } 75 | 76 | public int UseKey(byte[] key) 77 | { 78 | int n = 0; 79 | foreach (var pak in PakFiles) 80 | { 81 | if (!pak.Initialized) 82 | { 83 | if (pak.TryReadIndex(key, Filter)) 84 | n++; 85 | } 86 | } 87 | return n; 88 | } 89 | 90 | public int UseKeys(IEnumerable keys) 91 | { 92 | int n = 0; 93 | foreach(var key in keys) 94 | n += UseKey(key); 95 | return n; 96 | } 97 | 98 | public int UseKey(FGuid EncryptionGuid, byte[] key) 99 | { 100 | int n = 0; 101 | foreach (var pak in PakFiles) 102 | { 103 | if (!pak.Initialized && EncryptionGuid == pak.Info.EncryptionKeyGuid) 104 | { 105 | if (pak.TryReadIndex(key, Filter)) 106 | n++; 107 | } 108 | } 109 | return n; 110 | } 111 | 112 | readonly Dictionary> CachedFiles; 113 | public ArraySegment GetFile(string path) 114 | { 115 | TryGetFile(path, out var ret); 116 | return ret; 117 | } 118 | public bool TryGetFile(string path, out ArraySegment ret) 119 | { 120 | if (!CaseSensitive) 121 | path = path.ToLowerInvariant(); 122 | if (CacheFiles && CachedFiles.TryGetValue(path, out ret)) 123 | return true; 124 | foreach (var pak in PakFiles) 125 | { 126 | if (!pak.Initialized) continue; 127 | if (path.IndexOf(pak.MountPoint, 0) == 0) // same as StartsWith but more performant 128 | { 129 | if (pak.TryGetFile(path.Substring(pak.MountPoint.Length), out ret)) 130 | { 131 | if (CacheFiles) 132 | CachedFiles[path] = ret; 133 | return true; 134 | } 135 | } 136 | } 137 | ret = null; 138 | return false; 139 | } 140 | 141 | Dictionary Packages; 142 | public PakPackage GetPackage(string path) 143 | { 144 | TryGetPackage(path, out var ret); 145 | return ret; 146 | } 147 | public bool TryGetPackage(string path, out PakPackage package) 148 | { 149 | if (Packages == null) 150 | Packages = new Dictionary(); 151 | if (!Packages.TryGetValue(path, out package)) 152 | { 153 | var uasset = GetFile(path + ".uasset"); 154 | var uexp = GetFile(path + ".uexp"); 155 | var ubulk = GetFile(path + ".ubulk"); 156 | if (uasset == null || uexp == null) return false; // Can't have a package without uassets or uexps 157 | Packages[path] = package = new PakPackage(uasset, uexp, ubulk); 158 | } 159 | return true; 160 | } 161 | 162 | public IEnumerator GetEnumerator() 163 | { 164 | foreach(var pak in PakFiles) 165 | { 166 | if (!pak.Initialized) 167 | continue; 168 | foreach(var file in pak) 169 | { 170 | yield return pak.MountPoint + file.Key; 171 | } 172 | } 173 | } 174 | IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); 175 | } 176 | } 177 | -------------------------------------------------------------------------------- /Pak/PakPackage.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.IO; 3 | using PakReader.Parsers; 4 | 5 | namespace PakReader.Pak 6 | { 7 | public readonly struct PakPackage 8 | { 9 | // might optimize this if I add more extensions like umaps or uptnls 10 | readonly ArraySegment UAsset; 11 | readonly ArraySegment UExp; 12 | readonly ArraySegment UBulk; 13 | 14 | public UObject[] Exports 15 | { 16 | get 17 | { 18 | if (exports.Exports == null) 19 | { 20 | using var asset = new MemoryStream(UAsset.Array, UAsset.Offset, UAsset.Count); 21 | using var exp = new MemoryStream(UExp.Array, UExp.Offset, UExp.Count); 22 | using var bulk = UBulk != null ? new MemoryStream(UBulk.Array, UBulk.Offset, UBulk.Count) : null; 23 | asset.Position = 0; 24 | exp.Position = 0; 25 | if (bulk != null) 26 | bulk.Position = 0; 27 | return exports.Exports = new PackageReader(asset, exp, bulk).Exports; 28 | } 29 | return exports.Exports; 30 | } 31 | } 32 | readonly ExportList exports; 33 | 34 | internal PakPackage(ArraySegment asset, ArraySegment exp, ArraySegment bulk) 35 | { 36 | UAsset = asset; 37 | UExp = exp; 38 | UBulk = bulk; 39 | exports = new ExportList(); 40 | } 41 | 42 | public T GetExport() where T : UObject 43 | { 44 | var exports = Exports; 45 | for (int i = 0; i < exports.Length; i++) 46 | { 47 | if (exports[i] is T) 48 | return (T)exports[i]; 49 | } 50 | return null; 51 | } 52 | 53 | // hacky way to get the package to be a readonly struct, essentially a double pointer i guess 54 | sealed class ExportList 55 | { 56 | public UObject[] Exports; 57 | } 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /PakReader.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | netstandard2.1 5 | latest 6 | AnyCPU;x64;x86 7 | 8 | 9 | true 10 | lib\ 11 | 12 | 13 | true 14 | lib\ 15 | 16 | 17 | true 18 | lib\ 19 | 20 | 21 | true 22 | lib\ 23 | 24 | 25 | true 26 | lib\ 27 | 28 | 29 | true 30 | lib\ 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | Library 39 | 40 | true 41 | Copyright © WorkingRobot 2020 42 | https://github.com/WorkingRobot/PakReader 43 | https://github.com/WorkingRobot/PakReader.git 44 | git 45 | unreal engine pak parsing uasset uexp fortnite 46 | 2.1.0.0 47 | 2.1.0.0 48 | Read UE pak files and its' included assets. 49 | 50 | 2.1.1 51 | MIT 52 | Now supports pak v10. 53 | 54 | -------------------------------------------------------------------------------- /Parsers/Objects/EBulkDataFlags.cs: -------------------------------------------------------------------------------- 1 | namespace PakReader.Parsers.Objects 2 | { 3 | /** 4 | * Flags serialized with the bulk data. 5 | */ 6 | public enum EBulkDataFlags : uint 7 | { 8 | /** Empty flag set. */ 9 | BULKDATA_None = 0, 10 | /** If set, payload is stored at the end of the file and not inline */ 11 | BULKDATA_PayloadAtEndOfFile = 1 << 0, 12 | /** If set, payload should be [un]compressed using ZLIB during serialization. */ 13 | BULKDATA_SerializeCompressedZLIB = 1 << 1, 14 | /** Force usage of SerializeElement over bulk serialization. */ 15 | BULKDATA_ForceSingleElementSerialization = 1 << 2, 16 | /** Bulk data is only used once at runtime in the game. */ 17 | BULKDATA_SingleUse = 1 << 3, 18 | /** Bulk data won't be used and doesn't need to be loaded */ 19 | BULKDATA_Unused = 1 << 5, 20 | /** Forces the payload to be saved inline, regardless of its size */ 21 | BULKDATA_ForceInlinePayload = 1 << 6, 22 | /** Flag to check if either compression mode is specified */ 23 | BULKDATA_SerializeCompressed = (BULKDATA_SerializeCompressedZLIB), 24 | /** Forces the payload to be always streamed, regardless of its size */ 25 | BULKDATA_ForceStreamPayload = 1 << 7, 26 | /** If set, payload is stored in a .upack file alongside the uasset */ 27 | BULKDATA_PayloadInSeperateFile = 1 << 8, 28 | /** DEPRECATED: If set, payload is compressed using platform specific bit window */ 29 | BULKDATA_SerializeCompressedBitWindow = 1 << 9, 30 | /** There is a new default to inline unless you opt out */ 31 | BULKDATA_Force_NOT_InlinePayload = 1 << 10, 32 | /** This payload is optional and may not be on device */ 33 | BULKDATA_OptionalPayload = 1 << 11, 34 | /** This payload will be memory mapped, this requires alignment, no compression etc. */ 35 | BULKDATA_MemoryMappedPayload = 1 << 12, 36 | /** Bulk data size is 64 bits long */ 37 | BULKDATA_Size64Bit = 1 << 13, 38 | /** Duplicate non-optional payload in optional bulk data. */ 39 | BULKDATA_DuplicateNonOptionalPayload = 1 << 14 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /Parsers/Objects/ECompressionFlags.cs: -------------------------------------------------------------------------------- 1 | namespace PakReader.Parsers.Objects 2 | { 3 | /** 4 | * Flags controlling [de]compression 5 | * Make sure to update VerifyCompressionFlagsValid after changing these values. 6 | */ 7 | public enum ECompressionFlags : uint 8 | { 9 | /** No compression */ 10 | COMPRESS_None = 0x00, 11 | /** Compress with ZLIB - DEPRECATED, USE FNAME */ 12 | COMPRESS_ZLIB = 0x01, 13 | /** Compress with GZIP - DEPRECATED, USE FNAME */ 14 | COMPRESS_GZIP = 0x02, 15 | /** Compress with user defined callbacks - DEPRECATED, USE FNAME */ 16 | COMPRESS_Custom = 0x04, 17 | /** Joint of the previous ones to determine if old flags are being used */ 18 | COMPRESS_DeprecatedFormatFlagsMask = 0xF, 19 | 20 | 21 | /** No flags specified / */ 22 | COMPRESS_NoFlags = 0x00, 23 | /** Prefer compression that compresses smaller (ONLY VALID FOR COMPRESSION) */ 24 | COMPRESS_BiasMemory = 0x10, 25 | /** Prefer compression that compresses faster (ONLY VALID FOR COMPRESSION) */ 26 | COMPRESS_BiasSpeed = 0x20, 27 | /** Is the source buffer padded out (ONLY VALID FOR UNCOMPRESS) */ 28 | COMPRESS_SourceIsPadded = 0x80, 29 | 30 | /** Set of flags that are options are still allowed */ 31 | COMPRESS_OptionsFlagsMask = 0xF0, 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /Parsers/Objects/EDateTimeStyle.cs: -------------------------------------------------------------------------------- 1 | namespace PakReader.Parsers.Objects 2 | { 3 | public enum EDateTimeStyle : byte 4 | { 5 | Default, 6 | Short, 7 | Medium, 8 | Long, 9 | Full 10 | // Add new enum types at the end only! They are serialized by index. 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /Parsers/Objects/EObjectFlags.cs: -------------------------------------------------------------------------------- 1 | namespace PakReader.Parsers.Objects 2 | { 3 | /** 4 | * Flags describing an object instance 5 | */ 6 | public enum EObjectFlags : uint 7 | { 8 | // Do not add new flags unless they truly belong here. There are alternatives. 9 | // if you change any the bit of any of the RF_Load flags, then you will need legacy serialization 10 | RF_NoFlags = 0x00000000, ///< No flags, used to avoid a cast 11 | 12 | // This first group of flags mostly has to do with what kind of object it is. Other than transient, these are the persistent object flags. 13 | // The garbage collector also tends to look at these. 14 | RF_Public = 0x00000001, ///< Object is visible outside its package. 15 | RF_Standalone = 0x00000002, ///< Keep object around for editing even if unreferenced. 16 | RF_MarkAsNative = 0x00000004, ///< Object (UField) will be marked as native on construction (DO NOT USE THIS FLAG in HasAnyFlags() etc) 17 | RF_Transactional = 0x00000008, ///< Object is transactional. 18 | RF_ClassDefaultObject = 0x00000010, ///< This object is its class's default object 19 | RF_ArchetypeObject = 0x00000020, ///< This object is a template for another object - treat like a class default object 20 | RF_Transient = 0x00000040, ///< Don't save object. 21 | 22 | // This group of flags is primarily concerned with garbage collection. 23 | RF_MarkAsRootSet = 0x00000080, ///< Object will be marked as root set on construction and not be garbage collected, even if unreferenced (DO NOT USE THIS FLAG in HasAnyFlags() etc) 24 | RF_TagGarbageTemp = 0x00000100, ///< This is a temp user flag for various utilities that need to use the garbage collector. The garbage collector itself does not interpret it. 25 | 26 | // The group of flags tracks the stages of the lifetime of a uobject 27 | RF_NeedInitialization = 0x00000200, ///< This object has not completed its initialization process. Cleared when ~FObjectInitializer completes 28 | RF_NeedLoad = 0x00000400, ///< During load, indicates object needs loading. 29 | RF_KeepForCooker = 0x00000800, ///< Keep this object during garbage collection because it's still being used by the cooker 30 | RF_NeedPostLoad = 0x00001000, ///< Object needs to be postloaded. 31 | RF_NeedPostLoadSubobjects = 0x00002000, ///< During load, indicates that the object still needs to instance subobjects and fixup serialized component references 32 | RF_NewerVersionExists = 0x00004000, ///< Object has been consigned to oblivion due to its owner package being reloaded, and a newer version currently exists 33 | RF_BeginDestroyed = 0x00008000, ///< BeginDestroy has been called on the object. 34 | RF_FinishDestroyed = 0x00010000, ///< FinishDestroy has been called on the object. 35 | 36 | // Misc. Flags 37 | RF_BeingRegenerated = 0x00020000, ///< Flagged on UObjects that are used to create UClasses (e.g. Blueprints) while they are regenerating their UClass on load (See FLinkerLoad::CreateExport()) 38 | RF_DefaultSubObject = 0x00040000, ///< Flagged on subobjects that are defaults 39 | RF_WasLoaded = 0x00080000, ///< Flagged on UObjects that were loaded 40 | RF_TextExportTransient = 0x00100000, ///< Do not export object to text form (e.g. copy/paste). Generally used for sub-objects that can be regenerated from data in their parent object. 41 | RF_LoadCompleted = 0x00200000, ///< Object has been completely serialized by linkerload at least once. DO NOT USE THIS FLAG, It should be replaced with RF_WasLoaded. 42 | RF_InheritableComponentTemplate = 0x00400000, ///< Archetype of the object can be in its super class 43 | RF_DuplicateTransient = 0x00800000, ///< Object should not be included in any type of duplication (copy/paste, binary duplication, etc.) 44 | RF_StrongRefOnFrame = 0x01000000, ///< References to this object from persistent function frame are handled as strong ones. 45 | RF_NonPIEDuplicateTransient = 0x02000000, ///< Object should not be included for duplication unless it's being duplicated for a PIE session 46 | RF_Dynamic = 0x04000000, ///< Field Only. Dynamic field - doesn't get constructed during static initialization, can be constructed multiple times 47 | RF_WillBeLoaded = 0x08000000, ///< This object was constructed during load and will be loaded shortly 48 | 49 | // Extra defines 50 | RF_Load = RF_Public | RF_Standalone | RF_Transactional | RF_ClassDefaultObject | RF_ArchetypeObject | RF_DefaultSubObject | RF_TextExportTransient | RF_InheritableComponentTemplate | RF_DuplicateTransient | RF_NonPIEDuplicateTransient, 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /Parsers/Objects/EPackageFlags.cs: -------------------------------------------------------------------------------- 1 | namespace PakReader.Parsers.Objects 2 | { 3 | public enum EPackageFlags : uint 4 | { 5 | PKG_None = 0x00000000, ///< No flags 6 | PKG_NewlyCreated = 0x00000001, ///< Newly created package, not saved yet. In editor only. 7 | PKG_ClientOptional = 0x00000002, ///< Purely optional for clients. 8 | PKG_ServerSideOnly = 0x00000004, ///< Only needed on the server side. 9 | PKG_CompiledIn = 0x00000010, ///< This package is from "compiled in" classes. 10 | PKG_ForDiffing = 0x00000020, ///< This package was loaded just for the purposes of diffing 11 | PKG_EditorOnly = 0x00000040, ///< This is editor-only package (for example: editor module script package) 12 | PKG_Developer = 0x00000080, ///< Developer module 13 | // PKG_Unused = 0x00000100, 14 | // PKG_Unused = 0x00000200, 15 | // PKG_Unused = 0x00000400, 16 | // PKG_Unused = 0x00000800, 17 | // PKG_Unused = 0x00001000, 18 | // PKG_Unused = 0x00002000, 19 | PKG_ContainsMapData = 0x00004000, ///< Contains map data (UObjects only referenced by a single ULevel) but is stored in a different package 20 | PKG_Need = 0x00008000, ///< Client needs to download this package. 21 | PKG_Compiling = 0x00010000, ///< package is currently being compiled 22 | PKG_ContainsMap = 0x00020000, ///< Set if the package contains a ULevel/ UWorld object 23 | PKG_RequiresLocalizationGather = 0x00040000, ///< Set if the package contains any data to be gathered by localization 24 | PKG_DisallowLazyLoading = 0x00080000, ///< Set if the archive serializing this package cannot use lazy loading 25 | PKG_PlayInEditor = 0x00100000, ///< Set if the package was created for the purpose of PIE 26 | PKG_ContainsScript = 0x00200000, ///< Package is allowed to contain UClass objects 27 | PKG_DisallowExport = 0x00400000, ///< Editor should not export asset in this package 28 | // PKG_Unused = 0x00800000, 29 | // PKG_Unused = 0x01000000, 30 | // PKG_Unused = 0x02000000, 31 | // PKG_Unused = 0x04000000, 32 | // PKG_Unused = 0x08000000, 33 | // PKG_Unused = 0x10000000, 34 | // PKG_Unused = 0x20000000, 35 | PKG_ReloadingForCooker = 0x40000000, ///< This package is reloading in the cooker, try to avoid getting data we will never need. We won't save this package. 36 | PKG_FilterEditorOnly = 0x80000000, ///< Package has editor-only data filtered out 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /Parsers/Objects/EPakVersion.cs: -------------------------------------------------------------------------------- 1 | namespace PakReader.Parsers.Objects 2 | { 3 | // NOTE: THIS IS NOT AN ACTUAL ENUM IN UE4. 4 | // LINK: https://github.com/EpicGames/UnrealEngine/blob/8b6414ae4bca5f93b878afadcc41ab518b09984f/Engine/Source/Runtime/PakFile/Public/IPlatformFilePak.h#L85 5 | public enum EPakVersion 6 | { 7 | INITIAL = 1, 8 | NO_TIMESTAMPS = 2, 9 | COMPRESSION_ENCRYPTION = 3, // UE4.13+ 10 | INDEX_ENCRYPTION = 4, // UE4.17+ - encrypts only pak file index data leaving file content as is 11 | RELATIVE_CHUNK_OFFSETS = 5, // UE4.20+ 12 | DELETE_RECORDS = 6, // UE4.21+ - this constant is not used in UE4 code 13 | ENCRYPTION_KEY_GUID = 7, // ... allows to use multiple encryption keys over the single project 14 | FNAME_BASED_COMPRESSION_METHOD = 8, // UE4.22+ - use string instead of enum for compression method 15 | FROZEN_INDEX = 9, 16 | PATH_HASH_INDEX = 10, 17 | 18 | 19 | LAST, 20 | INVALID, 21 | LATEST = LAST - 1 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /Parsers/Objects/EPixelFormat.cs: -------------------------------------------------------------------------------- 1 | namespace PakReader.Parsers.Objects 2 | { 3 | public enum EPixelFormat 4 | { 5 | PF_Unknown = 0, 6 | PF_A32B32G32R32F = 1, 7 | PF_B8G8R8A8 = 2, 8 | PF_G8 = 3, 9 | PF_G16 = 4, 10 | PF_DXT1 = 5, 11 | PF_DXT3 = 6, 12 | PF_DXT5 = 7, 13 | PF_UYVY = 8, 14 | PF_FloatRGB = 9, 15 | PF_FloatRGBA = 10, 16 | PF_DepthStencil = 11, 17 | PF_ShadowDepth = 12, 18 | PF_R32_FLOAT = 13, 19 | PF_G16R16 = 14, 20 | PF_G16R16F = 15, 21 | PF_G16R16F_FILTER = 16, 22 | PF_G32R32F = 17, 23 | PF_A2B10G10R10 = 18, 24 | PF_A16B16G16R16 = 19, 25 | PF_D24 = 20, 26 | PF_R16F = 21, 27 | PF_R16F_FILTER = 22, 28 | PF_BC5 = 23, 29 | PF_V8U8 = 24, 30 | PF_A1 = 25, 31 | PF_FloatR11G11B10 = 26, 32 | PF_A8 = 27, 33 | PF_R32_UINT = 28, 34 | PF_R32_SINT = 29, 35 | PF_PVRTC2 = 30, 36 | PF_PVRTC4 = 31, 37 | PF_R16_UINT = 32, 38 | PF_R16_SINT = 33, 39 | PF_R16G16B16A16_UINT = 34, 40 | PF_R16G16B16A16_SINT = 35, 41 | PF_R5G6B5_UNORM = 36, 42 | PF_R8G8B8A8 = 37, 43 | PF_A8R8G8B8 = 38, // Only used for legacy loading; do NOT use! 44 | PF_BC4 = 39, 45 | PF_R8G8 = 40, 46 | PF_ATC_RGB = 41, 47 | PF_ATC_RGBA_E = 42, 48 | PF_ATC_RGBA_I = 43, 49 | PF_X24_G8 = 44, // Used for creating SRVs to alias a DepthStencil buffer to read Stencil. Don't use for creating textures. 50 | PF_ETC1 = 45, 51 | PF_ETC2_RGB = 46, 52 | PF_ETC2_RGBA = 47, 53 | PF_R32G32B32A32_UINT = 48, 54 | PF_R16G16_UINT = 49, 55 | PF_ASTC_4x4 = 50, // 8.00 bpp 56 | PF_ASTC_6x6 = 51, // 3.56 bpp 57 | PF_ASTC_8x8 = 52, // 2.00 bpp 58 | PF_ASTC_10x10 = 53, // 1.28 bpp 59 | PF_ASTC_12x12 = 54, // 0.89 bpp 60 | PF_BC6H = 55, 61 | PF_BC7 = 56, 62 | PF_R8_UINT = 57, 63 | PF_L8 = 58, 64 | PF_XGXR8 = 59, 65 | PF_R8G8B8A8_UINT = 60, 66 | PF_R8G8B8A8_SNORM = 61, 67 | PF_R16G16B16A16_UNORM = 62, 68 | PF_R16G16B16A16_SNORM = 63, 69 | PF_PLATFORM_HDR_0 = 64, 70 | PF_PLATFORM_HDR_1 = 65, // Reserved. 71 | PF_PLATFORM_HDR_2 = 66, // Reserved. 72 | PF_NV12 = 67, 73 | PF_R32G32_UINT = 68, 74 | PF_MAX = 69, 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /Parsers/Objects/ETextFlag.cs: -------------------------------------------------------------------------------- 1 | namespace PakReader.Parsers.Objects 2 | { 3 | public enum ETextFlag : uint 4 | { 5 | Transient = 1 << 0, 6 | CultureInvariant = 1 << 1, 7 | ConvertedProperty = 1 << 2, 8 | Immutable = 1 << 3, 9 | InitializedFromString = 1 << 4, // this ftext was initialized using FromString 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /Parsers/Objects/ETextHistoryType.cs: -------------------------------------------------------------------------------- 1 | namespace PakReader.Parsers.Objects 2 | { 3 | public enum ETextHistoryType : sbyte 4 | { 5 | None = -1, 6 | Base = 0, 7 | NamedFormat, 8 | OrderedFormat, 9 | ArgumentFormat, 10 | AsNumber, 11 | AsPercent, 12 | AsCurrency, 13 | AsDate, 14 | AsTime, 15 | AsDateTime, 16 | Transform, 17 | StringTableEntry, 18 | TextGenerator, 19 | 20 | // Add new enum types at the end only! They are serialized by index. 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /Parsers/Objects/FByteBulkData.cs: -------------------------------------------------------------------------------- 1 | using System.IO; 2 | 3 | namespace PakReader.Parsers.Objects 4 | { 5 | public readonly struct FByteBulkData 6 | { 7 | // Memory saving, we don't need this 8 | //uint BulkDataFlags; 9 | //long ElementCount; 10 | //long BulkDataOffsetInFile; 11 | //long BulkDataSizeOnDisk; 12 | 13 | public readonly byte[] Data; 14 | 15 | internal FByteBulkData(BinaryReader reader, Stream ubulk, int bulkOffset) 16 | { 17 | var BulkDataFlags = reader.ReadUInt32(); 18 | 19 | bool LongBits = (BulkDataFlags & (uint)EBulkDataFlags.BULKDATA_Size64Bit) != 0; 20 | 21 | var ElementCount = LongBits ? reader.ReadInt64() : reader.ReadInt32(); 22 | var BulkDataSizeOnDisk = LongBits ? reader.ReadInt64() : reader.ReadInt32(); 23 | var BulkDataOffsetInFile = reader.ReadInt64(); 24 | 25 | Data = null; 26 | if ((BulkDataFlags & (uint)EBulkDataFlags.BULKDATA_ForceInlinePayload) != 0) 27 | { 28 | Data = reader.ReadBytes((int)ElementCount); 29 | } 30 | 31 | if ((BulkDataFlags & (uint)EBulkDataFlags.BULKDATA_PayloadInSeperateFile) != 0) 32 | { 33 | ubulk.Position = BulkDataOffsetInFile + bulkOffset; 34 | Data = new byte[ElementCount]; 35 | ubulk.Read(Data, 0, (int)ElementCount); 36 | } 37 | } 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /Parsers/Objects/FColor.cs: -------------------------------------------------------------------------------- 1 | using System.IO; 2 | 3 | namespace PakReader.Parsers.Objects 4 | { 5 | public readonly struct FColor : IUStruct 6 | { 7 | public readonly byte R; 8 | public readonly byte G; 9 | public readonly byte B; 10 | public readonly byte A; 11 | 12 | internal FColor(BinaryReader reader) 13 | { 14 | R = reader.ReadByte(); 15 | G = reader.ReadByte(); 16 | B = reader.ReadByte(); 17 | A = reader.ReadByte(); 18 | } 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /Parsers/Objects/FCompressedChunk.cs: -------------------------------------------------------------------------------- 1 | using System.IO; 2 | 3 | namespace PakReader.Parsers.Objects 4 | { 5 | public readonly struct FCompressedChunk 6 | { 7 | public readonly int UncompressedOffset; 8 | public readonly int UncompressedSize; 9 | public readonly int CompressedOffset; 10 | public readonly int CompressedSize; 11 | 12 | internal FCompressedChunk(BinaryReader reader) 13 | { 14 | UncompressedOffset = reader.ReadInt32(); 15 | UncompressedSize = reader.ReadInt32(); 16 | CompressedOffset = reader.ReadInt32(); 17 | CompressedSize = reader.ReadInt32(); 18 | } 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /Parsers/Objects/FCustomVersion.cs: -------------------------------------------------------------------------------- 1 | using System.IO; 2 | 3 | namespace PakReader.Parsers.Objects 4 | { 5 | public readonly struct FCustomVersion 6 | { 7 | public readonly FGuid Key; 8 | public readonly int Version; 9 | //public readonly int ReferenceCount; unused in serialization 10 | 11 | internal FCustomVersion(BinaryReader reader) 12 | { 13 | Key = new FGuid(reader); 14 | Version = reader.ReadInt32(); 15 | } 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /Parsers/Objects/FCustomVersionContainer.cs: -------------------------------------------------------------------------------- 1 | using System.IO; 2 | 3 | namespace PakReader.Parsers.Objects 4 | { 5 | public readonly struct FCustomVersionContainer 6 | { 7 | public readonly FCustomVersion[] Versions; // actually FCustomVersionArray, but typedeffed to TArray 8 | 9 | internal FCustomVersionContainer(BinaryReader reader) 10 | { 11 | Versions = reader.ReadTArray(() => new FCustomVersion(reader)); 12 | } 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /Parsers/Objects/FDateTime.cs: -------------------------------------------------------------------------------- 1 | using System.IO; 2 | 3 | namespace PakReader.Parsers.Objects 4 | { 5 | public readonly struct FDateTime 6 | { 7 | // might add more helper methods here 8 | 9 | /** Holds the ticks in 100 nanoseconds resolution since January 1, 0001 A.D. */ 10 | public readonly long Ticks; 11 | 12 | internal FDateTime(BinaryReader reader) 13 | { 14 | Ticks = reader.ReadInt64(); 15 | } 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /Parsers/Objects/FEngineVersion.cs: -------------------------------------------------------------------------------- 1 | using System.IO; 2 | 3 | namespace PakReader.Parsers.Objects 4 | { 5 | public readonly struct FEngineVersion 6 | { 7 | // FEngineVersionBase 8 | public readonly ushort Major; 9 | public readonly ushort Minor; 10 | public readonly ushort Patch; 11 | public readonly uint Changelist; 12 | 13 | // FEngineVersion 14 | public readonly string Branch; 15 | 16 | internal FEngineVersion(BinaryReader reader) 17 | { 18 | Major = reader.ReadUInt16(); 19 | Minor = reader.ReadUInt16(); 20 | Patch = reader.ReadUInt16(); 21 | Changelist = reader.ReadUInt32(); 22 | Branch = reader.ReadFString(); 23 | } 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /Parsers/Objects/FGameplayTagContainer.cs: -------------------------------------------------------------------------------- 1 | namespace PakReader.Parsers.Objects 2 | { 3 | public readonly struct FGameplayTagContainer : IUStruct 4 | { 5 | // It's technically a TArray but FGameplayTag is just a fancy wrapper around an FName 6 | public readonly FName[] GameplayTags; 7 | 8 | internal FGameplayTagContainer(PackageReader reader) 9 | { 10 | GameplayTags = reader.ReadTArray(() => reader.ReadFName()); 11 | } 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /Parsers/Objects/FGenerationInfo.cs: -------------------------------------------------------------------------------- 1 | using System.IO; 2 | 3 | namespace PakReader.Parsers.Objects 4 | { 5 | public readonly struct FGenerationInfo 6 | { 7 | public readonly int ExportCount; 8 | public readonly int NameCount; 9 | 10 | internal FGenerationInfo(BinaryReader reader) 11 | { 12 | ExportCount = reader.ReadInt32(); 13 | NameCount = reader.ReadInt32(); 14 | } 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /Parsers/Objects/FGuid.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Globalization; 3 | using System.IO; 4 | using Newtonsoft.Json; 5 | 6 | namespace PakReader.Parsers.Objects 7 | { 8 | public readonly struct FGuid : IUStruct, IEquatable 9 | { 10 | [JsonIgnore] 11 | public readonly uint A; 12 | [JsonIgnore] 13 | public readonly uint B; 14 | [JsonIgnore] 15 | public readonly uint C; 16 | [JsonIgnore] 17 | public readonly uint D; 18 | 19 | public string Hex => ToString(); 20 | 21 | private static readonly FGuid zero = new FGuid(0, 0, 0, 0); 22 | public static ref readonly FGuid Zero => ref zero; 23 | 24 | public FGuid(uint a, uint b, uint c, uint d) 25 | { 26 | A = a; 27 | B = b; 28 | C = c; 29 | D = d; 30 | } 31 | 32 | public FGuid(string guid) 33 | { 34 | A = uint.Parse(guid[0 .. 8], NumberStyles.HexNumber); 35 | B = uint.Parse(guid[8 ..16], NumberStyles.HexNumber); 36 | C = uint.Parse(guid[16..24], NumberStyles.HexNumber); 37 | D = uint.Parse(guid[24..32], NumberStyles.HexNumber); 38 | } 39 | 40 | internal FGuid(BinaryReader reader) 41 | { 42 | A = reader.ReadUInt32(); 43 | B = reader.ReadUInt32(); 44 | C = reader.ReadUInt32(); 45 | D = reader.ReadUInt32(); 46 | } 47 | 48 | public bool IsValid() => (A | B | C | D) != 0; 49 | 50 | public bool Equals(FGuid b) => A == b.A && B == b.B && C == b.C && D == b.D; 51 | 52 | public override bool Equals(object obj) => obj is FGuid ? Equals((FGuid)obj) : false; 53 | 54 | public override int GetHashCode() => (int)(A ^ B ^ C ^ D); 55 | 56 | public static bool operator ==(FGuid left, FGuid right) => left.Equals(right); 57 | 58 | public static bool operator !=(FGuid left, FGuid right) => !left.Equals(right); 59 | 60 | // TODO: maybe make this more performant? 61 | public override string ToString() => 62 | A.ToString("x8") + B.ToString("x8") + C.ToString("x8") + D.ToString("x8"); 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /Parsers/Objects/FIntPoint.cs: -------------------------------------------------------------------------------- 1 | using System.IO; 2 | 3 | namespace PakReader.Parsers.Objects 4 | { 5 | public readonly struct FIntPoint : IUStruct 6 | { 7 | public readonly int X; 8 | public readonly int Y; 9 | 10 | internal FIntPoint(BinaryReader reader) 11 | { 12 | X = reader.ReadInt32(); 13 | Y = reader.ReadInt32(); 14 | } 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /Parsers/Objects/FLinearColor.cs: -------------------------------------------------------------------------------- 1 | using System.IO; 2 | 3 | namespace PakReader.Parsers.Objects 4 | { 5 | public readonly struct FLinearColor : IUStruct 6 | { 7 | public readonly float R; 8 | public readonly float G; 9 | public readonly float B; 10 | public readonly float A; 11 | 12 | internal FLinearColor(BinaryReader reader) 13 | { 14 | R = reader.ReadSingle(); 15 | G = reader.ReadSingle(); 16 | B = reader.ReadSingle(); 17 | A = reader.ReadSingle(); 18 | } 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /Parsers/Objects/FName.cs: -------------------------------------------------------------------------------- 1 | using Newtonsoft.Json; 2 | 3 | namespace PakReader.Parsers.Objects 4 | { 5 | public readonly struct FName 6 | { 7 | readonly FNameEntrySerialized Name; 8 | [JsonIgnore] 9 | public readonly int Index; 10 | [JsonIgnore] 11 | public readonly int Number; 12 | 13 | public string String => Name.Name; 14 | 15 | [JsonIgnore] 16 | public bool IsNone => String == "None"; 17 | 18 | internal FName(FNameEntrySerialized name, int index, int number) 19 | { 20 | Name = name; 21 | Index = index; 22 | Number = number; 23 | } 24 | 25 | public override string ToString() => Name.Name; 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /Parsers/Objects/FNameEntrySerialized.cs: -------------------------------------------------------------------------------- 1 | using System.IO; 2 | 3 | namespace PakReader.Parsers.Objects 4 | { 5 | // The only values this contains from the original FNameEntrySerialized is the isWide (unused here since C# strings are always 16 bit anyway) and the Index (some typedef of an int which was unused anyway) 6 | // FNames are passed into a pool, but I don't think this has any impact or difference on the resolving of these values. I could make a Dictionary or Lookup for values having the same hash or something..? 7 | 8 | // FNameEntrySerialized is a class due to the value typing that C# has for structs. This is for memory performance to reduce duplicate strings in memory. Refrain from saving the FNameEntrySerialized's value (Name) and opt for a class instead 9 | internal readonly struct FNameEntrySerialized 10 | { 11 | public readonly string Name; 12 | 13 | // The parser is basically the same as FString. Let me know if there are any breaking test cases here 14 | internal FNameEntrySerialized(BinaryReader reader) 15 | { 16 | Name = reader.ReadFString(); 17 | // skip DummyHashes (case and non-case preserving hashes) 18 | reader.BaseStream.Position += 4; 19 | } 20 | 21 | public override string ToString() => Name; 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /Parsers/Objects/FObjectExport.cs: -------------------------------------------------------------------------------- 1 | namespace PakReader.Parsers.Objects 2 | { 3 | public sealed class FObjectExport : FObjectResource 4 | { 5 | public FPackageIndex ClassIndex { get; } 6 | //public FPackageIndex ThisIndex { get; } unused for serialization 7 | public FPackageIndex SuperIndex { get; } 8 | public FPackageIndex TemplateIndex { get; } 9 | public EObjectFlags ObjectFlags { get; } 10 | public long SerialSize { get; } 11 | public long SerialOffset { get; } 12 | //public long ScriptSerializationStartOffset { get; } 13 | //public long ScriptSerializationEndOffset { get; } 14 | //public UObject Object { get; } 15 | //public int HashNext { get; } 16 | public bool bForcedExport { get; } 17 | public bool bNotForClient { get; } 18 | public bool bNotForServer { get; } 19 | public bool bNotAlwaysLoadedForEditorGame { get; } 20 | public bool bIsAsset { get; } 21 | //public bool bExportLoadFailed { get; } 22 | //public EDynamicType DynamicType { get; } 23 | //public bool bWasFiltered { get; } 24 | public FGuid PackageGuid { get; } 25 | public uint PackageFlags { get; } 26 | public int FirstExportDependency { get; } 27 | public int SerializationBeforeSerializationDependencies { get; } 28 | public int CreateBeforeSerializationDependencies { get; } 29 | public int SerializationBeforeCreateDependencies { get; } 30 | public int CreateBeforeCreateDependencies { get; } 31 | 32 | internal FObjectExport(PackageReader reader) 33 | { 34 | ClassIndex = new FPackageIndex(reader); 35 | SuperIndex = new FPackageIndex(reader); 36 | 37 | // only serialize when file version is past VER_UE4_TemplateIndex_IN_COOKED_EXPORTS 38 | TemplateIndex = new FPackageIndex(reader); 39 | 40 | OuterIndex = new FPackageIndex(reader); 41 | ObjectName = reader.ReadFName(); 42 | 43 | ObjectFlags = (EObjectFlags)reader.ReadUInt32() & EObjectFlags.RF_Load; 44 | 45 | // only serialize when file version is past VER_UE4_64BIT_EXPORTMAP_SERIALSIZES 46 | SerialSize = reader.ReadInt64(); 47 | SerialOffset = reader.ReadInt64(); 48 | 49 | bForcedExport = reader.ReadInt32() != 0; 50 | bNotForClient = reader.ReadInt32() != 0; 51 | bNotForServer = reader.ReadInt32() != 0; 52 | 53 | PackageGuid = new FGuid(reader); 54 | PackageFlags = reader.ReadUInt32(); 55 | 56 | // only serialize when file version is past VER_UE4_LOAD_FOR_EDITOR_GAME 57 | bNotAlwaysLoadedForEditorGame = reader.ReadInt32() != 0; 58 | 59 | // only serialize when file version is past VER_UE4_COOKED_ASSETS_IN_EDITOR_SUPPORT 60 | bIsAsset = reader.ReadInt32() != 0; 61 | 62 | // only serialize when file version is past VER_UE4_PRELOAD_DEPENDENCIES_IN_COOKED_EXPORTS 63 | FirstExportDependency = reader.ReadInt32(); 64 | SerializationBeforeSerializationDependencies = reader.ReadInt32(); 65 | CreateBeforeSerializationDependencies = reader.ReadInt32(); 66 | SerializationBeforeCreateDependencies = reader.ReadInt32() ; 67 | CreateBeforeCreateDependencies = reader.ReadInt32(); 68 | } 69 | 70 | public enum EDynamicType : byte 71 | { 72 | NotDynamicExport, 73 | DynamicType, 74 | ClassDefaultObject, 75 | }; 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /Parsers/Objects/FObjectImport.cs: -------------------------------------------------------------------------------- 1 | namespace PakReader.Parsers.Objects 2 | { 3 | public sealed class FObjectImport : FObjectResource 4 | { 5 | public FName ClassPackage { get; } 6 | public FName ClassName { get; } 7 | //public bool bImportPackageHandled { get; } unused for serialization 8 | //public bool bImportSearchedFor { get; } 9 | //public bool bImportFailed { get; } 10 | 11 | internal FObjectImport(PackageReader reader) 12 | { 13 | ClassPackage = reader.ReadFName(); 14 | ClassName = reader.ReadFName(); 15 | OuterIndex = new FPackageIndex(reader); 16 | ObjectName = reader.ReadFName(); 17 | } 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /Parsers/Objects/FObjectResource.cs: -------------------------------------------------------------------------------- 1 | namespace PakReader.Parsers.Objects 2 | { 3 | public class FObjectResource 4 | { 5 | public FName ObjectName { get; protected set; } 6 | public FPackageIndex OuterIndex { get; protected set; } 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /Parsers/Objects/FPackageFileSummary.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.IO; 3 | 4 | namespace PakReader.Parsers.Objects 5 | { 6 | public readonly struct FPackageFileSummary 7 | { 8 | const int PACKAGE_FILE_TAG = unchecked((int)0x9E2A83C1); 9 | const int PACKAGE_FILE_TAG_SWAPPED = unchecked((int)0xC1832A9E); 10 | 11 | private readonly int FileVersionUE4; 12 | private readonly int FileVersionLicenseeUE4; 13 | private readonly FCustomVersionContainer CustomVersionContainer; 14 | 15 | public readonly int TotalHeaderSize; 16 | public readonly EPackageFlags PackageFlags; 17 | public readonly string FolderName; 18 | public readonly int NameCount; 19 | public readonly int NameOffset; 20 | //public readonly string LocalizationId; only serialized in editor 21 | public readonly int GatherableTextDataCount; 22 | public readonly int GatherableTextDataOffset; 23 | public readonly int ExportCount; 24 | public readonly int ExportOffset; 25 | public readonly int ImportCount; 26 | public readonly int ImportOffset; 27 | public readonly int DependsOffset; 28 | public readonly int SoftPackageReferencesCount; 29 | public readonly int SoftPackageReferencesOffset; 30 | public readonly int SearchableNamesOffset; 31 | public readonly int ThumbnailTableOffset; 32 | public readonly FGuid Guid; 33 | public readonly FGenerationInfo[] Generations; 34 | public readonly FEngineVersion SavedByEngineVersion; 35 | public readonly FEngineVersion CompatibleWithEngineVersion; 36 | public readonly ECompressionFlags CompressionFlags; 37 | public readonly uint PackageSource; 38 | public readonly bool bUnversioned; 39 | public readonly int AssetRegistryDataOffset; 40 | public readonly long BulkDataStartOffset; 41 | public readonly int WorldTileInfoDataOffset; 42 | public readonly int[] ChunkIDs; 43 | public readonly int PreloadDependencyCount; 44 | public readonly int PreloadDependencyOffset; 45 | 46 | internal FPackageFileSummary(BinaryReader reader) 47 | { 48 | bUnversioned = false; 49 | CustomVersionContainer = default; 50 | 51 | var Tag = reader.ReadInt32(); 52 | if (Tag != PACKAGE_FILE_TAG && Tag != PACKAGE_FILE_TAG_SWAPPED) 53 | { 54 | throw new FileLoadException("Not a UE package"); 55 | } 56 | 57 | // The package has been stored in a separate endianness than the linker expected so we need to force 58 | // endian conversion. Latent handling allows the PC version to retrieve information about cooked packages. 59 | if (Tag == PACKAGE_FILE_TAG_SWAPPED) 60 | { 61 | // Set proper tag. 62 | Tag = PACKAGE_FILE_TAG; 63 | // Toggle forced byte swapping. 64 | throw new NotImplementedException("Byte swapping for packages not implemented"); 65 | } 66 | 67 | /** 68 | * The package file version number when this package was saved. 69 | * 70 | * Lower 16 bits stores the UE3 engine version 71 | * Upper 16 bits stores the UE4/licensee version 72 | * For newer packages this is -7 73 | * -2 indicates presence of enum-based custom versions 74 | * -3 indicates guid-based custom versions 75 | * -4 indicates removal of the UE3 version. Packages saved with this ID cannot be loaded in older engine versions 76 | * -5 indicates the replacement of writing out the "UE3 version" so older versions of engine can gracefully fail to open newer packages 77 | * -6 indicates optimizations to how custom versions are being serialized 78 | * -7 indicates the texture allocation info has been removed from the summary 79 | */ 80 | var LegacyFileVersion = reader.ReadInt32(); 81 | if (LegacyFileVersion < 0) // means we have modern version numbers 82 | { 83 | if (LegacyFileVersion < -7) // CurrentLegacyFileVersion 84 | { 85 | // we can't safely load more than this because the legacy version code differs in ways we can not predict. 86 | // Make sure that the linker will fail to load with it. 87 | throw new FileLoadException("Can't load legacy UE3 file"); 88 | } 89 | 90 | if (LegacyFileVersion != -4) 91 | { 92 | reader.BaseStream.Position += 4; // LegacyUE3Version (int32) 93 | } 94 | FileVersionUE4 = reader.ReadInt32(); 95 | FileVersionLicenseeUE4 = reader.ReadInt32(); 96 | 97 | if (LegacyFileVersion <= -2) 98 | { 99 | CustomVersionContainer = new FCustomVersionContainer(reader); 100 | } 101 | 102 | if (FileVersionUE4 != 0 && FileVersionLicenseeUE4 != 0) 103 | { 104 | // this file is unversioned, remember that, then use current versions 105 | bUnversioned = true; 106 | 107 | // set the versions to latest here, etc. 108 | } 109 | } 110 | else 111 | { 112 | // This is probably an old UE3 file, make sure that the linker will fail to load with it. 113 | throw new FileLoadException("Can't load legacy UE3 file"); 114 | } 115 | 116 | TotalHeaderSize = reader.ReadInt32(); 117 | FolderName = reader.ReadFString(); 118 | PackageFlags = (EPackageFlags)reader.ReadUInt32(); 119 | NameCount = reader.ReadInt32(); 120 | NameOffset = reader.ReadInt32(); 121 | 122 | // only serialize when file version is past VER_UE4_SERIALIZE_TEXT_IN_PACKAGES 123 | GatherableTextDataCount = reader.ReadInt32(); 124 | GatherableTextDataOffset = reader.ReadInt32(); 125 | 126 | ExportCount = reader.ReadInt32(); 127 | ExportOffset = reader.ReadInt32(); 128 | ImportCount = reader.ReadInt32(); 129 | ImportOffset = reader.ReadInt32(); 130 | DependsOffset = reader.ReadInt32(); 131 | 132 | // only serialize when file version is past VER_UE4_ADD_STRING_ASSET_REFERENCES_MAP 133 | SoftPackageReferencesCount = reader.ReadInt32(); 134 | SoftPackageReferencesOffset = reader.ReadInt32(); 135 | 136 | // only serialize when file version is past VER_UE4_ADDED_SEARCHABLE_NAMES 137 | SearchableNamesOffset = reader.ReadInt32(); 138 | 139 | ThumbnailTableOffset = reader.ReadInt32(); 140 | Guid = new FGuid(reader); 141 | 142 | { 143 | var GenerationCount = reader.ReadInt32(); 144 | if (GenerationCount > 0) 145 | { 146 | Generations = new FGenerationInfo[GenerationCount]; 147 | for (int i = 0; i < Generations.Length; i++) 148 | { 149 | Generations[i] = new FGenerationInfo(reader); 150 | } 151 | } 152 | else 153 | Generations = null; 154 | } 155 | 156 | // only serialize when file version is past VER_UE4_ENGINE_VERSION_OBJECT 157 | SavedByEngineVersion = new FEngineVersion(reader); 158 | 159 | // only serialize when file version is past VER_UE4_PACKAGE_SUMMARY_HAS_COMPATIBLE_ENGINE_VERSION 160 | CompatibleWithEngineVersion = new FEngineVersion(reader); 161 | 162 | CompressionFlags = (ECompressionFlags)reader.ReadUInt32(); 163 | if (CompressionFlags != ECompressionFlags.COMPRESS_None) // No support for deprecated compression 164 | throw new FileLoadException($"Incompatible compression flags ({(uint)CompressionFlags})"); 165 | 166 | if (reader.ReadTArray(() => new FCompressedChunk(reader)).Length != 0) // "CompressedChunks" 167 | { 168 | throw new FileLoadException("Package level compression is enabled"); 169 | } 170 | 171 | PackageSource = reader.ReadUInt32(); 172 | reader.ReadTArray(() => reader.ReadFString()); // "AdditionalPackagesToCook" 173 | 174 | if (LegacyFileVersion > -7) 175 | { 176 | // We haven't used texture allocation info for ages and it's no longer supported anyway 177 | if (reader.ReadInt32() != 0) // "NumTextureAllocations" 178 | { 179 | throw new FileLoadException("Can't load legacy UE3 file"); 180 | } 181 | } 182 | 183 | AssetRegistryDataOffset = reader.ReadInt32(); 184 | BulkDataStartOffset = reader.ReadInt64(); 185 | 186 | // only serialize when file version is past VER_UE4_WORLD_LEVEL_INFO 187 | WorldTileInfoDataOffset = reader.ReadInt32(); 188 | 189 | // only serialize when file version is past VER_UE4_CHANGED_CHUNKID_TO_BE_AN_ARRAY_OF_CHUNKIDS 190 | ChunkIDs = reader.ReadTArray(() => reader.ReadInt32()); 191 | 192 | // only serialize when file version is past VER_UE4_PRELOAD_DEPENDENCIES_IN_COOKED_EXPORTS 193 | PreloadDependencyCount = reader.ReadInt32(); 194 | PreloadDependencyOffset = reader.ReadInt32(); 195 | } 196 | } 197 | } 198 | -------------------------------------------------------------------------------- /Parsers/Objects/FPackageIndex.cs: -------------------------------------------------------------------------------- 1 | using Newtonsoft.Json; 2 | 3 | namespace PakReader.Parsers.Objects 4 | { 5 | /** 6 | * Wrapper for index into a ULnker's ImportMap or ExportMap. 7 | * Values greater than zero indicate that this is an index into the ExportMap. The 8 | * actual array index will be (FPackageIndex - 1). 9 | * 10 | * Values less than zero indicate that this is an index into the ImportMap. The actual 11 | * array index will be (-FPackageIndex - 1) 12 | */ 13 | public readonly struct FPackageIndex 14 | { 15 | [JsonIgnore] 16 | public readonly int Index; 17 | public FObjectResource Resource => 18 | !IsNull ? 19 | IsImport ? 20 | Reader.ImportMap[AsImport] : 21 | (FObjectResource)Reader.ExportMap[AsExport] 22 | : null; 23 | 24 | readonly PackageReader Reader; 25 | 26 | internal FPackageIndex(PackageReader reader) 27 | { 28 | Index = reader.ReadInt32(); 29 | Reader = reader; 30 | } 31 | 32 | [JsonIgnore] 33 | public bool IsNull => Index == 0; 34 | [JsonIgnore] 35 | public bool IsImport => Index < 0; 36 | [JsonIgnore] 37 | public bool IsExport => Index > 0; 38 | 39 | // Original names were ToImport and ToExport but I prefer "As" to "To" for properties 40 | [JsonIgnore] 41 | public int AsImport => -Index - 1; 42 | [JsonIgnore] 43 | public int AsExport => Index - 1; 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /Parsers/Objects/FPakCompressedBlock.cs: -------------------------------------------------------------------------------- 1 | using System.IO; 2 | 3 | namespace PakReader.Parsers.Objects 4 | { 5 | public readonly struct FPakCompressedBlock 6 | { 7 | public readonly long CompressedStart; 8 | public readonly long CompressedEnd; 9 | 10 | internal FPakCompressedBlock(BinaryReader reader) 11 | { 12 | CompressedStart = reader.ReadInt64(); 13 | CompressedEnd = reader.ReadInt64(); 14 | } 15 | 16 | internal FPakCompressedBlock(long start, long end) 17 | { 18 | CompressedStart = start; 19 | CompressedEnd = end; 20 | } 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /Parsers/Objects/FPakEntry.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.IO; 3 | 4 | namespace PakReader.Parsers.Objects 5 | { 6 | public readonly struct FPakEntry 7 | { 8 | const byte Flag_None = 0x00; 9 | const byte Flag_Encrypted = 0x01; 10 | const byte Flag_Deleted = 0x02; 11 | 12 | public bool Encrypted => (Flags & Flag_Encrypted) != 0; 13 | public bool Deleted => (Flags & Flag_Deleted) != 0; 14 | 15 | public readonly long Offset; 16 | public readonly long Size; 17 | public readonly long UncompressedSize; 18 | public readonly byte[] Hash; // why isn't this an FShaHash? 19 | public readonly FPakCompressedBlock[] CompressionBlocks; 20 | public readonly uint CompressionBlockSize; 21 | public readonly uint CompressionMethodIndex; 22 | public readonly byte Flags; 23 | 24 | public readonly int StructSize; 25 | 26 | internal FPakEntry(BinaryReader reader, EPakVersion Version) 27 | { 28 | CompressionBlocks = null; 29 | CompressionBlockSize = 0; 30 | Flags = 0; 31 | 32 | var StartOffset = reader.BaseStream.Position; 33 | 34 | Offset = reader.ReadInt64(); 35 | Size = reader.ReadInt64(); 36 | UncompressedSize = reader.ReadInt64(); 37 | if (Version < EPakVersion.FNAME_BASED_COMPRESSION_METHOD) 38 | { 39 | var LegacyCompressionMethod = reader.ReadInt32(); 40 | if (LegacyCompressionMethod == (int)ECompressionFlags.COMPRESS_None) 41 | { 42 | CompressionMethodIndex = 0; 43 | } 44 | else if ((LegacyCompressionMethod & (int)ECompressionFlags.COMPRESS_ZLIB) != 0) 45 | { 46 | CompressionMethodIndex = 1; 47 | } 48 | else if ((LegacyCompressionMethod & (int)ECompressionFlags.COMPRESS_GZIP) != 0) 49 | { 50 | CompressionMethodIndex = 2; 51 | } 52 | else if ((LegacyCompressionMethod & (int)ECompressionFlags.COMPRESS_Custom) != 0) 53 | { 54 | CompressionMethodIndex = 3; 55 | } 56 | else 57 | { 58 | // https://github.com/EpicGames/UnrealEngine/blob/8b6414ae4bca5f93b878afadcc41ab518b09984f/Engine/Source/Runtime/PakFile/Public/IPlatformFilePak.h#L441 59 | throw new FileLoadException(@"Found an unknown compression type in pak file, will need to be supported for legacy files"); 60 | } 61 | } 62 | else 63 | { 64 | CompressionMethodIndex = reader.ReadUInt32(); 65 | } 66 | if (Version <= EPakVersion.INITIAL) 67 | { 68 | // Timestamp of type FDateTime, but the serializer only reads to the Ticks property (int64) 69 | reader.ReadInt64(); 70 | } 71 | Hash = reader.ReadBytes(20); 72 | if (Version >= EPakVersion.COMPRESSION_ENCRYPTION) 73 | { 74 | if (CompressionMethodIndex != 0) 75 | { 76 | CompressionBlocks = reader.ReadTArray(() => new FPakCompressedBlock(reader)); 77 | } 78 | Flags = reader.ReadByte(); 79 | CompressionBlockSize = reader.ReadUInt32(); 80 | } 81 | 82 | // Used to seek ahead to the file data instead of parsing the entry again 83 | StructSize = (int)(reader.BaseStream.Position - StartOffset); 84 | } 85 | 86 | internal FPakEntry(BinaryReader reader) 87 | { 88 | CompressionBlocks = null; 89 | CompressionBlockSize = 0; 90 | Flags = 0; 91 | 92 | var StartOffset = reader.BaseStream.Position; 93 | 94 | Offset = reader.ReadInt64(); 95 | Size = reader.ReadInt64(); 96 | UncompressedSize = reader.ReadInt64(); 97 | CompressionMethodIndex = reader.ReadUInt32(); 98 | Hash = reader.ReadBytes(20); 99 | if (CompressionMethodIndex != 0) 100 | { 101 | CompressionBlocks = reader.ReadTArray(() => new FPakCompressedBlock(reader)); 102 | } 103 | Flags = reader.ReadByte(); 104 | CompressionBlockSize = reader.ReadUInt32(); 105 | 106 | // Used to seek ahead to the file data instead of parsing the entry again 107 | StructSize = (int)(reader.BaseStream.Position - StartOffset); 108 | } 109 | 110 | internal FPakEntry(long offset, long size, long uncompressedSize, byte[] hash, FPakCompressedBlock[] compressionBlocks, uint compressionBlockSize, uint compressionMethodIndex, byte flags) 111 | { 112 | Offset = offset; 113 | Size = size; 114 | UncompressedSize = uncompressedSize; 115 | Hash = hash; 116 | CompressionBlocks = compressionBlocks; 117 | CompressionBlockSize = compressionBlockSize; 118 | CompressionMethodIndex = compressionMethodIndex; 119 | Flags = flags; 120 | StructSize = (int)GetSize(EPakVersion.LATEST, compressionMethodIndex, (uint)compressionBlocks.Length); 121 | } 122 | 123 | public ArraySegment GetData(Stream stream, byte[] key) 124 | { 125 | if (CompressionMethodIndex != 0) 126 | throw new NotImplementedException("Decompression not yet implemented"); 127 | lock (stream) 128 | { 129 | stream.Position = Offset + StructSize; 130 | if (Encrypted) 131 | { 132 | var data = new byte[(Size & 15) == 0 ? Size : ((Size / 16) + 1) * 16]; 133 | stream.Read(data); 134 | return new ArraySegment(AESDecryptor.DecryptAES(data, key), 0, (int)UncompressedSize); 135 | } 136 | else 137 | { 138 | var data = new byte[UncompressedSize]; 139 | stream.Read(data); 140 | return new ArraySegment(data); 141 | } 142 | } 143 | } 144 | 145 | public static long GetSize(EPakVersion version, uint CompressionMethodIndex = 0, uint CompressionBlocksCount = 0) 146 | { 147 | long SerializedSize = sizeof(long) + sizeof(long) + sizeof(long) + 20; 148 | 149 | if (version >= EPakVersion.FNAME_BASED_COMPRESSION_METHOD) 150 | { 151 | SerializedSize += sizeof(uint); 152 | } 153 | else 154 | { 155 | SerializedSize += sizeof(int); // Old CompressedMethod var from pre-fname based compression methods 156 | } 157 | 158 | if (version >= EPakVersion.COMPRESSION_ENCRYPTION) 159 | { 160 | SerializedSize += sizeof(byte) + sizeof(uint); 161 | if (CompressionMethodIndex != 0) 162 | { 163 | SerializedSize += sizeof(long) * 2 * CompressionBlocksCount + sizeof(int); 164 | } 165 | } 166 | if (version < EPakVersion.NO_TIMESTAMPS) 167 | { 168 | // Timestamp 169 | SerializedSize += sizeof(long); 170 | } 171 | return SerializedSize; 172 | } 173 | } 174 | } 175 | -------------------------------------------------------------------------------- /Parsers/Objects/FPakInfo.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.IO; 3 | using System.Text; 4 | 5 | namespace PakReader.Parsers.Objects 6 | { 7 | public readonly struct FPakInfo 8 | { 9 | const uint PAK_FILE_MAGIC = 0x5A6F12E1; 10 | const int COMPRESSION_METHOD_NAME_LEN = 32; 11 | const int MAX_NUM_COMPRESSION_METHODS = 5; 12 | 13 | 14 | // Magic // 4 bytes 15 | public readonly EPakVersion Version; // 4 bytes 16 | public readonly long IndexOffset; // 8 bytes 17 | public readonly long IndexSize; // 8 bytes 18 | public readonly FSHAHash IndexHash; // 20 bytes 19 | public readonly bool bEncryptedIndex; // 1 byte 20 | public readonly FGuid EncryptionKeyGuid; // 16 bytes 21 | public readonly string[] CompressionMethods; // 160 bytes 22 | // 221 bytes total 23 | 24 | // I calculate the size myself instead of asking for an input version 25 | // https://github.com/EpicGames/UnrealEngine/blob/8b6414ae4bca5f93b878afadcc41ab518b09984f/Engine/Source/Runtime/PakFile/Public/IPlatformFilePak.h#L138 26 | internal const int SERIALIZED_SIZE = 221; 27 | 28 | internal FPakInfo(BinaryReader reader) 29 | { 30 | // Serialize if version is at least EPakVersion.ENCRYPTION_KEY_GUID 31 | EncryptionKeyGuid = new FGuid(reader); 32 | bEncryptedIndex = reader.ReadByte() != 0; 33 | 34 | if (reader.ReadUInt32() != PAK_FILE_MAGIC) 35 | { 36 | // UE4 tries to handle old versions but I'd rather not deal with that right now 37 | throw new FileLoadException("Invalid pak magic"); 38 | } 39 | 40 | Version = (EPakVersion)reader.ReadInt32(); 41 | IndexOffset = reader.ReadInt64(); 42 | IndexSize = reader.ReadInt64(); 43 | IndexHash = new FSHAHash(reader); 44 | 45 | // I'd do some version checking here, but I'd rather not care to check that you loaded a pak file from 2003 46 | // https://github.com/EpicGames/UnrealEngine/blob/8b6414ae4bca5f93b878afadcc41ab518b09984f/Engine/Source/Runtime/PakFile/Public/IPlatformFilePak.h#L185 47 | 48 | if (Version < EPakVersion.FNAME_BASED_COMPRESSION_METHOD) 49 | { 50 | CompressionMethods = new string[] { "Zlib", "Gzip", "Oodle" }; 51 | } 52 | else 53 | { 54 | int BufferSize = COMPRESSION_METHOD_NAME_LEN * MAX_NUM_COMPRESSION_METHODS; 55 | byte[] Methods = reader.ReadBytes(BufferSize); 56 | var MethodList = new List(MAX_NUM_COMPRESSION_METHODS); 57 | for (int i = 0; i < MAX_NUM_COMPRESSION_METHODS; i++) 58 | { 59 | if (Methods[i*COMPRESSION_METHOD_NAME_LEN] != 0) 60 | { 61 | MethodList.Add(Encoding.ASCII.GetString(Methods, i * COMPRESSION_METHOD_NAME_LEN, COMPRESSION_METHOD_NAME_LEN)); 62 | } 63 | } 64 | CompressionMethods = MethodList.ToArray(); 65 | } 66 | } 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /Parsers/Objects/FPropertyTag.cs: -------------------------------------------------------------------------------- 1 | namespace PakReader.Parsers.Objects 2 | { 3 | readonly struct FPropertyTag 4 | { 5 | public readonly int ArrayIndex; 6 | public readonly byte BoolVal; 7 | public readonly FName EnumName; 8 | public readonly byte HasPropertyGuid; 9 | public readonly FName InnerType; 10 | public readonly FName Name; 11 | //public readonly UProperty* Prop; // Transient 12 | public readonly FGuid PropertyGuid; 13 | public readonly int Size; 14 | public readonly long SizeOffset; // TODO: not set, check code 15 | public readonly FGuid StructGuid; 16 | public readonly FName StructName; 17 | public readonly FName Type; // Variables 18 | public readonly FName ValueType; 19 | 20 | internal FPropertyTag(PackageReader reader) 21 | { 22 | ArrayIndex = 0; 23 | BoolVal = 0; 24 | EnumName = default; 25 | HasPropertyGuid = 0; 26 | InnerType = default; 27 | Name = default; 28 | PropertyGuid = default; 29 | Size = 0; 30 | SizeOffset = 0; 31 | StructGuid = default; 32 | StructName = default; 33 | Type = default; 34 | ValueType = default; 35 | 36 | Name = reader.ReadFName(); 37 | if (Name.IsNone) 38 | return; 39 | 40 | Type = reader.ReadFName(); 41 | Size = reader.ReadInt32(); 42 | ArrayIndex = reader.ReadInt32(); 43 | 44 | if (Type.Number == 0) 45 | { 46 | switch (Type.String) 47 | { 48 | case "StructProperty": 49 | StructName = reader.ReadFName(); 50 | // Serialize if version is past VER_UE4_STRUCT_GUID_IN_PROPERTY_TAG 51 | StructGuid = new FGuid(reader); 52 | break; 53 | case "BoolProperty": 54 | BoolVal = reader.ReadByte(); 55 | break; 56 | case "ByteProperty": 57 | case "EnumProperty": 58 | EnumName = reader.ReadFName(); 59 | break; 60 | case "ArrayProperty": 61 | // Serialize if version is past VAR_UE4_ARRAY_PROPERTY_INNER_TAGS 62 | InnerType = reader.ReadFName(); 63 | break; 64 | // Serialize the following if version is past VER_UE4_PROPERTY_TAG_SET_MAP_SUPPORT 65 | case "SetProperty": 66 | InnerType = reader.ReadFName(); 67 | break; 68 | case "MapProperty": 69 | InnerType = reader.ReadFName(); 70 | ValueType = reader.ReadFName(); 71 | break; 72 | } 73 | } 74 | 75 | // Property tags to handle renamed blueprint properties effectively. 76 | // Serialize if version is past VER_UE4_PROPERTY_GUID_IN_PROPERTY_TAG 77 | HasPropertyGuid = reader.ReadByte(); 78 | if (HasPropertyGuid != 0) 79 | { 80 | PropertyGuid = new FGuid(reader); 81 | } 82 | } 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /Parsers/Objects/FQuat.cs: -------------------------------------------------------------------------------- 1 | using System.IO; 2 | 3 | namespace PakReader.Parsers.Objects 4 | { 5 | public readonly struct FQuat : IUStruct 6 | { 7 | public readonly float X; 8 | public readonly float Y; 9 | public readonly float Z; 10 | public readonly float W; 11 | 12 | internal FQuat(BinaryReader reader) 13 | { 14 | X = reader.ReadSingle(); 15 | Y = reader.ReadSingle(); 16 | Z = reader.ReadSingle(); 17 | W = reader.ReadSingle(); 18 | } 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /Parsers/Objects/FRotator.cs: -------------------------------------------------------------------------------- 1 | using System.IO; 2 | 3 | namespace PakReader.Parsers.Objects 4 | { 5 | public readonly struct FRotator : IUStruct 6 | { 7 | public readonly float Pitch; 8 | public readonly float Yaw; 9 | public readonly float Roll; 10 | 11 | internal FRotator(BinaryReader reader) 12 | { 13 | Pitch = reader.ReadSingle(); 14 | Yaw = reader.ReadSingle(); 15 | Roll = reader.ReadSingle(); 16 | } 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /Parsers/Objects/FSHAHash.cs: -------------------------------------------------------------------------------- 1 | using System.IO; 2 | 3 | namespace PakReader.Parsers.Objects 4 | { 5 | public readonly struct FSHAHash 6 | { 7 | public readonly byte[] Hash; 8 | 9 | internal FSHAHash(BinaryReader reader) 10 | { 11 | Hash = reader.ReadBytes(20); 12 | } 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /Parsers/Objects/FSoftObjectPath.cs: -------------------------------------------------------------------------------- 1 | namespace PakReader.Parsers.Objects 2 | { 3 | public readonly struct FSoftObjectPath : IUStruct 4 | { 5 | /** Asset path, patch to a top level object in a package. This is /package/path.assetname */ 6 | public readonly FName AssetPathName; 7 | /** Optional FString for subobject within an asset. This is the sub path after the : */ 8 | public readonly string SubPathString; 9 | 10 | internal FSoftObjectPath(PackageReader reader) 11 | { 12 | AssetPathName = reader.ReadFName(); 13 | SubPathString = reader.ReadFString(); 14 | } 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /Parsers/Objects/FStripDataFlags.cs: -------------------------------------------------------------------------------- 1 | using System.IO; 2 | 3 | namespace PakReader.Parsers.Objects 4 | { 5 | public readonly struct FStripDataFlags 6 | { 7 | readonly byte GlobalStripFlags; 8 | readonly byte ClassStripFlags; 9 | 10 | public bool EditorDataStripped => (GlobalStripFlags & (byte)EStrippedData.Editor) != 0; 11 | public bool DataStrippedForServer => (GlobalStripFlags & (byte)EStrippedData.Server) != 0; 12 | 13 | public bool ClassDataStripped(byte InFlags) => (ClassStripFlags & InFlags) != 0; 14 | 15 | internal FStripDataFlags(BinaryReader reader) 16 | { 17 | GlobalStripFlags = reader.ReadByte(); 18 | ClassStripFlags = reader.ReadByte(); 19 | } 20 | 21 | enum EStrippedData : byte 22 | { 23 | None = 0, 24 | 25 | /* Editor data */ 26 | Editor = 1, 27 | /* All data not required for dedicated server to work correctly (usually includes editor data). */ 28 | Server = 2, 29 | 30 | // Add global flags here (up to 8 including the already defined ones). 31 | 32 | /** All flags */ 33 | All = 0xff 34 | } 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /Parsers/Objects/FText.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace PakReader.Parsers.Objects 4 | { 5 | public readonly struct FText 6 | { 7 | public readonly ETextFlag Flags; 8 | public readonly FTextHistory Text; 9 | 10 | bool IsBaseType => Text is FTextHistory.Base; 11 | FTextHistory.Base BaseText => Text.As(); 12 | public string Key => IsBaseType ? BaseText.Key : null; 13 | public string Namespace => IsBaseType ? BaseText.Namespace : null; 14 | public string SourceString => IsBaseType ? BaseText.SourceString : null; 15 | 16 | // https://github.com/EpicGames/UnrealEngine/blob/7d9919ac7bfd80b7483012eab342cb427d60e8c9/Engine/Source/Runtime/Core/Private/Internationalization/Text.cpp#L769 17 | internal FText(PackageReader reader) 18 | { 19 | Flags = (ETextFlag)reader.ReadUInt32(); 20 | 21 | // "Assuming" the reader/archive is persistent 22 | Flags &= ETextFlag.ConvertedProperty | ETextFlag.InitializedFromString; 23 | 24 | // Execute if UE4 version is at least VER_UE4_FTEXT_HISTORY 25 | 26 | // The type is serialized during the serialization of the history, during deserialization we need to deserialize it and create the correct history 27 | var HistoryType = (ETextHistoryType)reader.ReadSByte(); 28 | 29 | // Create the history class based on the serialized type 30 | switch (HistoryType) 31 | { 32 | case ETextHistoryType.Base: 33 | Text = new FTextHistory.Base(reader); 34 | break; 35 | case ETextHistoryType.AsDateTime: 36 | Text = new FTextHistory.DateTime(reader); 37 | break; 38 | // https://github.com/EpicGames/UnrealEngine/blob/bf95c2cbc703123e08ab54e3ceccdd47e48d224a/Engine/Source/Runtime/Core/Private/Internationalization/TextHistory.cpp 39 | // https://github.com/EpicGames/UnrealEngine/blob/bf95c2cbc703123e08ab54e3ceccdd47e48d224a/Engine/Source/Runtime/Core/Private/Internationalization/TextData.h 40 | case ETextHistoryType.NamedFormat: 41 | case ETextHistoryType.OrderedFormat: 42 | case ETextHistoryType.ArgumentFormat: 43 | case ETextHistoryType.AsNumber: 44 | case ETextHistoryType.AsPercent: 45 | case ETextHistoryType.AsCurrency: 46 | case ETextHistoryType.AsDate: 47 | case ETextHistoryType.AsTime: 48 | case ETextHistoryType.Transform: 49 | case ETextHistoryType.StringTableEntry: 50 | case ETextHistoryType.TextGenerator: 51 | // Let me know if you find a package that has an unsupported text history type. 52 | throw new NotImplementedException($"Parsing of {HistoryType} history type isn't supported yet."); 53 | default: 54 | Text = new FTextHistory.None(reader); 55 | break; 56 | } 57 | } 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /Parsers/Objects/FTextHistory.cs: -------------------------------------------------------------------------------- 1 | namespace PakReader.Parsers.Objects 2 | { 3 | public abstract partial class FTextHistory 4 | { 5 | // quick conversion so extra space isn't wasted casting this if you know what the type is 6 | public T As() where T : FTextHistory => (T)this; 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /Parsers/Objects/FTextHistoryBase.cs: -------------------------------------------------------------------------------- 1 | namespace PakReader.Parsers.Objects 2 | { 3 | public partial class FTextHistory 4 | { 5 | public sealed class Base : FTextHistory 6 | { 7 | // This is the base class for text histories 8 | public readonly string Namespace; 9 | public readonly string Key; 10 | public readonly string SourceString; 11 | 12 | internal Base(PackageReader reader) 13 | { 14 | Namespace = reader.ReadFString() ?? string.Empty; // namespaces are sometimes null 15 | Key = reader.ReadFString(); 16 | SourceString = reader.ReadFString(); 17 | } 18 | } 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /Parsers/Objects/FTextHistoryDateTime.cs: -------------------------------------------------------------------------------- 1 | namespace PakReader.Parsers.Objects 2 | { 3 | public partial class FTextHistory 4 | { 5 | public sealed class DateTime : FTextHistory 6 | { 7 | public readonly FDateTime SourceDateTime; 8 | public readonly EDateTimeStyle DateStyle; 9 | public readonly EDateTimeStyle TimeStyle; 10 | public readonly string TimeZone; 11 | // UE4 converts the string into an FCulturePtr 12 | // https://github.com/EpicGames/UnrealEngine/blob/bf95c2cbc703123e08ab54e3ceccdd47e48d224a/Engine/Source/Runtime/Core/Private/Internationalization/TextHistory.cpp#L2188 13 | public readonly string TargetCulture; 14 | 15 | internal DateTime(PackageReader reader) 16 | { 17 | SourceDateTime = new FDateTime(reader); 18 | DateStyle = (EDateTimeStyle)reader.ReadByte(); 19 | TimeStyle = (EDateTimeStyle)reader.ReadByte(); 20 | TimeZone = reader.ReadFString(); 21 | TargetCulture = reader.ReadFString(); 22 | } 23 | } 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /Parsers/Objects/FTextHistoryNone.cs: -------------------------------------------------------------------------------- 1 | namespace PakReader.Parsers.Objects 2 | { 3 | public partial class FTextHistory 4 | { 5 | public sealed class None : FTextHistory 6 | { 7 | public readonly string CultureInvariantString; 8 | 9 | // https://github.com/EpicGames/UnrealEngine/blob/5677c544747daa1efc3b5ede31642176644518a6/Engine/Source/Runtime/Core/Private/Internationalization/Text.cpp#L974 10 | internal None(PackageReader reader) 11 | { 12 | if (reader.ReadInt32() != 0) // bHasCultureInvariantString 13 | { 14 | CultureInvariantString = reader.ReadFString(); 15 | } 16 | } 17 | } 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /Parsers/Objects/FTextKey.cs: -------------------------------------------------------------------------------- 1 | using System.IO; 2 | 3 | namespace PakReader.Parsers.Objects 4 | { 5 | public readonly struct FTextKey 6 | { 7 | public readonly uint StrHash; 8 | public readonly string String; 9 | 10 | internal FTextKey(BinaryReader reader) 11 | { 12 | StrHash = reader.ReadUInt32(); 13 | String = reader.ReadFString(); 14 | } 15 | 16 | internal FTextKey(string str) 17 | { 18 | StrHash = 0; 19 | String = str; 20 | } 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /Parsers/Objects/FTexture2DMipMap.cs: -------------------------------------------------------------------------------- 1 | using System.IO; 2 | 3 | namespace PakReader.Parsers.Objects 4 | { 5 | public readonly struct FTexture2DMipMap 6 | { 7 | public readonly int SizeX; 8 | public readonly int SizeY; 9 | public readonly int SizeZ; 10 | public readonly FByteBulkData BulkData; 11 | 12 | internal FTexture2DMipMap(BinaryReader reader, Stream ubulk, int bulkOffset) 13 | { 14 | var bCooked = reader.ReadInt32() != 0; 15 | BulkData = new FByteBulkData(reader, ubulk, bulkOffset); 16 | SizeX = reader.ReadInt32(); 17 | SizeY = reader.ReadInt32(); 18 | SizeZ = reader.ReadInt32(); 19 | } 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /Parsers/Objects/FTexturePlatformData.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.IO; 3 | 4 | namespace PakReader.Parsers.Objects 5 | { 6 | public readonly struct FTexturePlatformData 7 | { 8 | public readonly int SizeX; 9 | public readonly int SizeY; 10 | public readonly int NumSlices; 11 | public readonly EPixelFormat PixelFormat; 12 | public readonly FTexture2DMipMap[] Mips; 13 | 14 | internal FTexturePlatformData(PackageReader reader, Stream ubulk, int bulkOffset) 15 | { 16 | SizeX = reader.ReadInt32(); 17 | SizeY = reader.ReadInt32(); 18 | NumSlices = reader.ReadInt32(); 19 | PixelFormat = Enum.Parse(reader.ReadFString()); 20 | 21 | var FirstMipToSerialize = reader.ReadInt32(); 22 | FirstMipToSerialize = 0; // what: https://github.com/EpicGames/UnrealEngine/blob/4.24/Engine/Source/Runtime/Engine/Private/TextureDerivedData.cpp#L1316 23 | 24 | Mips = reader.ReadTArray(() => new FTexture2DMipMap(reader, ubulk, bulkOffset)); 25 | 26 | if (reader.ReadInt32() != 0) 27 | { 28 | throw new FileLoadException("Too lazy to add virtual textures right now"); 29 | } 30 | } 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /Parsers/Objects/FVector.cs: -------------------------------------------------------------------------------- 1 | using System.IO; 2 | 3 | namespace PakReader.Parsers.Objects 4 | { 5 | public readonly struct FVector : IUStruct 6 | { 7 | public readonly float X; 8 | public readonly float Y; 9 | public readonly float Z; 10 | 11 | internal FVector(BinaryReader reader) 12 | { 13 | X = reader.ReadSingle(); 14 | Y = reader.ReadSingle(); 15 | Z = reader.ReadSingle(); 16 | } 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /Parsers/Objects/FVector2D.cs: -------------------------------------------------------------------------------- 1 | using System.IO; 2 | 3 | namespace PakReader.Parsers.Objects 4 | { 5 | public readonly struct FVector2D : IUStruct 6 | { 7 | public readonly float X; 8 | public readonly float Y; 9 | 10 | internal FVector2D(BinaryReader reader) 11 | { 12 | X = reader.ReadSingle(); 13 | Y = reader.ReadSingle(); 14 | } 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /Parsers/Objects/IUStruct.cs: -------------------------------------------------------------------------------- 1 | namespace PakReader.Parsers.Objects 2 | { 3 | // Used to signify if it is used in UScriptStruct binary serialization 4 | public interface IUStruct { } 5 | } 6 | -------------------------------------------------------------------------------- /Parsers/Objects/UScriptStruct.cs: -------------------------------------------------------------------------------- 1 | namespace PakReader.Parsers.Objects 2 | { 3 | public readonly struct UScriptStruct 4 | { 5 | public readonly IUStruct Struct; 6 | 7 | // Binary serialization, tagged property serialization otherwise 8 | // https://github.com/EpicGames/UnrealEngine/blob/7d9919ac7bfd80b7483012eab342cb427d60e8c9/Engine/Source/Runtime/CoreUObject/Private/UObject/Class.cpp#L2146 9 | internal UScriptStruct(PackageReader reader, FName structName) => 10 | Struct = structName.String switch 11 | { 12 | "GameplayTagContainer" => new FGameplayTagContainer(reader), 13 | "Quat" => new FQuat(reader), 14 | "Vector2D" => new FVector2D(reader), 15 | "Vector" => new FVector(reader), 16 | "Rotator" => new FRotator(reader), 17 | "IntPoint" => new FIntPoint(reader), 18 | "Guid" => new FGuid(reader), 19 | "SoftObjectPath" => new FSoftObjectPath(reader), 20 | "Color" => new FColor(reader), 21 | "LinearColor" => new FLinearColor(reader), 22 | _ => new UObject(reader, true), 23 | }; 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /Parsers/PackageReader.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.IO; 3 | using System.Linq; 4 | using PakReader.Parsers.Objects; 5 | 6 | namespace PakReader.Parsers 7 | { 8 | public sealed class PackageReader 9 | { 10 | BinaryReader Loader { get; } 11 | 12 | public FPackageFileSummary PackageFileSummary { get; } 13 | FNameEntrySerialized[] NameMap { get; } 14 | public FObjectImport[] ImportMap { get; } 15 | public FObjectExport[] ExportMap { get; } 16 | 17 | public UObject[] Exports { get; } 18 | 19 | public PackageReader(string path) : this(path + ".uasset", path + ".uexp", path + ".ubulk") { } 20 | public PackageReader(string uasset, string uexp, string ubulk) : this(File.OpenRead(uasset), File.OpenRead(uexp), File.Exists(ubulk) ? File.OpenRead(ubulk) : null) { } 21 | public PackageReader(Stream uasset, Stream uexp, Stream ubulk) : this(new BinaryReader(uasset), new BinaryReader(uexp), ubulk) { } 22 | 23 | PackageReader(BinaryReader uasset, BinaryReader uexp, Stream ubulk) 24 | { 25 | Loader = uasset; 26 | PackageFileSummary = new FPackageFileSummary(Loader); 27 | 28 | NameMap = SerializeNameMap(); 29 | ImportMap = SerializeImportMap(); 30 | ExportMap = SerializeExportMap(); 31 | Exports = new UObject[ExportMap.Length]; 32 | Loader = uexp; 33 | for(int i = 0; i < ExportMap.Length; i++) 34 | { 35 | var Export = ExportMap[i]; 36 | // Serialize everything, not just specifically assets 37 | // if (Export.bIsAsset) 38 | { 39 | // We need to get the class name from the import/export maps 40 | FName ObjectClassName; 41 | if (Export.ClassIndex.IsNull) 42 | ObjectClassName = ReadFName(); // check if this is true, I don't know if Fortnite ever uses this 43 | else if (Export.ClassIndex.IsExport) 44 | ObjectClassName = ExportMap[Export.ClassIndex.AsExport].ObjectName; 45 | else if (Export.ClassIndex.IsImport) 46 | ObjectClassName = ImportMap[Export.ClassIndex.AsImport].ObjectName; 47 | else 48 | throw new FileLoadException("Can't get class name"); // Shouldn't reach this unless the laws of math have bent to MagmaReef's will 49 | 50 | 51 | var pos = Position = Export.SerialOffset - PackageFileSummary.TotalHeaderSize; 52 | Exports[i] = ObjectClassName.String switch 53 | { 54 | "Texture2D" => new Texture2D(this, ubulk, (int)(ExportMap.Sum(e => e.SerialSize) + PackageFileSummary.TotalHeaderSize)), 55 | _ => new UObject(this), 56 | }; 57 | if (pos + Export.SerialSize != Position) 58 | { 59 | Console.WriteLine($"Didn't read {Export.ObjectName} ({ObjectClassName}) correctly (at {Position}, should be {pos + Export.SerialSize}, {pos + Export.SerialSize - Position} behind)"); 60 | } 61 | Exports[i].ExportInfo = Export; 62 | } 63 | } 64 | return; 65 | } 66 | 67 | FNameEntrySerialized[] SerializeNameMap() 68 | { 69 | if (PackageFileSummary.NameCount > 0) 70 | { 71 | Loader.BaseStream.Position = PackageFileSummary.NameOffset; 72 | 73 | var OutNameMap = new FNameEntrySerialized[PackageFileSummary.NameCount]; 74 | for (int NameMapIdx = 0; NameMapIdx < PackageFileSummary.NameCount; ++NameMapIdx) 75 | { 76 | // Read the name entry from the file. 77 | OutNameMap[NameMapIdx] = new FNameEntrySerialized(Loader); 78 | } 79 | return OutNameMap; 80 | } 81 | return Array.Empty(); 82 | } 83 | 84 | FObjectImport[] SerializeImportMap() 85 | { 86 | if (PackageFileSummary.ImportCount > 0) 87 | { 88 | Loader.BaseStream.Position = PackageFileSummary.ImportOffset; 89 | 90 | var OutImportMap = new FObjectImport[PackageFileSummary.ImportCount]; 91 | for (int ImportMapIdx = 0; ImportMapIdx < PackageFileSummary.ImportCount; ++ImportMapIdx) 92 | { 93 | OutImportMap[ImportMapIdx] = new FObjectImport(this); 94 | } 95 | return OutImportMap; 96 | } 97 | return Array.Empty(); 98 | } 99 | 100 | FObjectExport[] SerializeExportMap() 101 | { 102 | if (PackageFileSummary.ExportCount > 0) 103 | { 104 | Loader.BaseStream.Position = PackageFileSummary.ExportOffset; 105 | 106 | var OutExportMap = new FObjectExport[PackageFileSummary.ExportCount]; 107 | for (int ExportMapIdx = 0; ExportMapIdx < PackageFileSummary.ExportCount; ++ExportMapIdx) 108 | { 109 | OutExportMap[ExportMapIdx] = new FObjectExport(this); 110 | } 111 | return OutExportMap; 112 | } 113 | return Array.Empty(); 114 | } 115 | 116 | public FName ReadFName() 117 | { 118 | var NameIndex = Loader.ReadInt32(); 119 | var Number = Loader.ReadInt32(); 120 | 121 | // https://github.com/EpicGames/UnrealEngine/blob/bf95c2cbc703123e08ab54e3ceccdd47e48d224a/Engine/Source/Runtime/CoreUObject/Public/UObject/LinkerLoad.h#L821 122 | // Has some more complicated stuff related to name map pools etc. that seems unnecessary atm 123 | if (NameIndex >= 0 && NameIndex < NameMap.Length) 124 | { 125 | return new FName(NameMap[NameIndex], NameIndex, Number); 126 | } 127 | throw new FileLoadException($"Bad Name Index {NameIndex} - {Loader.BaseStream.Position}"); 128 | } 129 | 130 | 131 | public static implicit operator BinaryReader(PackageReader reader) => reader.Loader; 132 | 133 | public byte ReadByte() => Loader.ReadByte(); 134 | public sbyte ReadSByte() => Loader.ReadSByte(); 135 | public byte[] ReadBytes(int count) => Loader.ReadBytes(count); 136 | public string ReadFString() => Loader.ReadFString(); 137 | public T[] ReadTArray(Func Getter) => Loader.ReadTArray(Getter); 138 | 139 | public short ReadInt16() => Loader.ReadInt16(); 140 | public ushort ReadUInt16() => Loader.ReadUInt16(); 141 | public int ReadInt32() => Loader.ReadInt32(); 142 | public uint ReadUInt32() => Loader.ReadUInt32(); 143 | public long ReadInt64() => Loader.ReadInt64(); 144 | public ulong ReadUInt64() => Loader.ReadUInt64(); 145 | public float ReadFloat() => Loader.ReadSingle(); 146 | public double ReadDouble() => Loader.ReadDouble(); 147 | 148 | public long Position { get => Loader.BaseStream.Position; set => Loader.BaseStream.Position = value; } 149 | } 150 | } 151 | -------------------------------------------------------------------------------- /Parsers/PropertyAttribute.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace PakReader.Parsers 4 | { 5 | [AttributeUsage(AttributeTargets.Field | AttributeTargets.Property, Inherited = true, AllowMultiple = false)] 6 | public sealed class UPropAttribute : Attribute 7 | { 8 | public string Name { get; } 9 | 10 | public UPropAttribute(string name) => 11 | Name = name; 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /Parsers/PropertyTagData/ArrayProperty.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using PakReader.Parsers.Objects; 3 | 4 | namespace PakReader.Parsers.PropertyTagData 5 | { 6 | public sealed class ArrayProperty : BaseProperty 7 | { 8 | internal ArrayProperty(PackageReader reader, FPropertyTag tag) 9 | { 10 | int length = reader.ReadInt32(); 11 | Value = new BaseProperty[length]; 12 | 13 | FPropertyTag InnerTag = default; 14 | // Execute if UE4 version is at least VER_UE4_INNER_ARRAY_TAG_INFO 15 | if (tag.InnerType.String == "StructProperty") 16 | { 17 | // Serialize a PropertyTag for the inner property of this array, allows us to validate the inner struct to see if it has changed 18 | InnerTag = new FPropertyTag(reader); 19 | } 20 | for (int i = 0; i < length; i++) 21 | { 22 | Value[i] = ReadProperty(reader, InnerTag, tag.InnerType, ReadType.ARRAY); 23 | } 24 | } 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /Parsers/PropertyTagData/BaseProperty.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using PakReader.Parsers.Objects; 3 | 4 | namespace PakReader.Parsers.PropertyTagData 5 | { 6 | public class BaseProperty 7 | { 8 | internal static BaseProperty ReadProperty(PackageReader reader, FPropertyTag tag, FName type, ReadType readType) 9 | { 10 | BaseProperty prop = type.String switch 11 | { 12 | "ByteProperty" => new ByteProperty(reader, tag, readType), 13 | "BoolProperty" => new BoolProperty(reader, tag, readType), 14 | "IntProperty" => new IntProperty(reader, tag), 15 | "FloatProperty" => new FloatProperty(reader, tag), 16 | "ObjectProperty" => new ObjectProperty(reader, tag), 17 | "NameProperty" => new NameProperty(reader, tag), 18 | "DelegateProperty" => new DelegateProperty(reader, tag), 19 | "DoubleProperty" => new DoubleProperty(reader, tag), 20 | "ArrayProperty" => new ArrayProperty(reader, tag), 21 | "StructProperty" => new StructProperty(reader, tag), 22 | // No code in UE4 source despite these being technically serializable properties 23 | //"VectorProperty" => new VectorProperty(reader, tag), 24 | //"RotatorProperty" => new RotatorProperty(reader, tag), 25 | "StrProperty" => new StrProperty(reader, tag), 26 | "TextProperty" => new TextProperty(reader, tag), 27 | "InterfaceProperty" => new InterfaceProperty(reader, tag), 28 | "MulticastDelegateProperty" => new MulticastDelegateProperty(reader, tag), 29 | "LazyObjectProperty" => new LazyObjectProperty(reader, tag), 30 | "SoftObjectProperty" => new SoftObjectProperty(reader, tag, readType), 31 | "UInt64Property" => new UInt64Property(reader, tag), 32 | "UInt32Property" => new UInt32Property(reader, tag), 33 | "UInt16Property" => new UInt16Property(reader, tag), 34 | "Int64Property" => new Int64Property(reader, tag), 35 | "Int16Property" => new Int16Property(reader, tag), 36 | "Int8Property" => new Int8Property(reader, tag), 37 | "MapProperty" => new MapProperty(reader, tag), 38 | "SetProperty" => new SetProperty(reader, tag), 39 | "EnumProperty" => new EnumProperty(reader, tag), 40 | _ => throw new NotImplementedException($"Parsing of {tag.Type.String} types aren't supported yet."), 41 | }; 42 | return prop; 43 | } 44 | } 45 | 46 | public class BaseProperty : BaseProperty 47 | { 48 | public T Value { get; protected set; } 49 | } 50 | 51 | public enum ReadType 52 | { 53 | NORMAL, 54 | MAP, 55 | ARRAY 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /Parsers/PropertyTagData/BoolProperty.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using PakReader.Parsers.Objects; 3 | 4 | namespace PakReader.Parsers.PropertyTagData 5 | { 6 | public sealed class BoolProperty : BaseProperty 7 | { 8 | internal BoolProperty(PackageReader reader, FPropertyTag tag, ReadType readType) 9 | { 10 | Value = readType switch 11 | { 12 | ReadType.NORMAL => tag.BoolVal != 0, 13 | ReadType.MAP => reader.ReadByte() != 0, 14 | ReadType.ARRAY => reader.ReadByte() != 0, 15 | _ => throw new ArgumentOutOfRangeException(nameof(readType)), 16 | }; 17 | } 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /Parsers/PropertyTagData/ByteProperty.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using PakReader.Parsers.Objects; 3 | 4 | namespace PakReader.Parsers.PropertyTagData 5 | { 6 | public sealed class ByteProperty : BaseProperty 7 | { 8 | internal ByteProperty(PackageReader reader, FPropertyTag tag, ReadType readType) 9 | { 10 | Value = readType switch 11 | { 12 | ReadType.NORMAL => (byte)reader.ReadFName().Index, 13 | ReadType.MAP => (byte)reader.ReadUInt32(), 14 | ReadType.ARRAY => reader.ReadByte(), 15 | _ => throw new ArgumentOutOfRangeException(nameof(readType)), 16 | }; 17 | } 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /Parsers/PropertyTagData/DelegateProperty.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using PakReader.Parsers.Objects; 3 | 4 | namespace PakReader.Parsers.PropertyTagData 5 | { 6 | public sealed class DelegateProperty : BaseProperty 7 | { 8 | internal DelegateProperty(PackageReader reader, FPropertyTag tag) 9 | { 10 | // Let me know if you find a package that has a DelegateProperty 11 | throw new NotImplementedException("Parsing of DelegateProperty types aren't supported yet."); 12 | } 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /Parsers/PropertyTagData/DoubleProperty.cs: -------------------------------------------------------------------------------- 1 | using PakReader.Parsers.Objects; 2 | 3 | namespace PakReader.Parsers.PropertyTagData 4 | { 5 | public sealed class DoubleProperty : BaseProperty 6 | { 7 | internal DoubleProperty(PackageReader reader, FPropertyTag tag) 8 | { 9 | Value = reader.ReadDouble(); 10 | } 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /Parsers/PropertyTagData/EnumProperty.cs: -------------------------------------------------------------------------------- 1 | using PakReader.Parsers.Objects; 2 | 3 | namespace PakReader.Parsers.PropertyTagData 4 | { 5 | public sealed class EnumProperty : BaseProperty 6 | { 7 | internal EnumProperty(PackageReader reader, FPropertyTag tag) 8 | { 9 | Value = reader.ReadFName(); 10 | } 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /Parsers/PropertyTagData/FloatProperty.cs: -------------------------------------------------------------------------------- 1 | using PakReader.Parsers.Objects; 2 | 3 | namespace PakReader.Parsers.PropertyTagData 4 | { 5 | public sealed class FloatProperty : BaseProperty 6 | { 7 | internal FloatProperty(PackageReader reader, FPropertyTag tag) 8 | { 9 | Value = reader.ReadFloat(); 10 | } 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /Parsers/PropertyTagData/Int16Property.cs: -------------------------------------------------------------------------------- 1 | using PakReader.Parsers.Objects; 2 | 3 | namespace PakReader.Parsers.PropertyTagData 4 | { 5 | public sealed class Int16Property : BaseProperty 6 | { 7 | internal Int16Property(PackageReader reader, FPropertyTag tag) 8 | { 9 | Value = reader.ReadInt16(); 10 | } 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /Parsers/PropertyTagData/Int64Property.cs: -------------------------------------------------------------------------------- 1 | using PakReader.Parsers.Objects; 2 | 3 | namespace PakReader.Parsers.PropertyTagData 4 | { 5 | public sealed class Int64Property : BaseProperty 6 | { 7 | internal Int64Property(PackageReader reader, FPropertyTag tag) 8 | { 9 | Value = reader.ReadInt64(); 10 | } 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /Parsers/PropertyTagData/Int8Property.cs: -------------------------------------------------------------------------------- 1 | using PakReader.Parsers.Objects; 2 | 3 | namespace PakReader.Parsers.PropertyTagData 4 | { 5 | public sealed class Int8Property : BaseProperty 6 | { 7 | internal Int8Property(PackageReader reader, FPropertyTag tag) 8 | { 9 | Value = reader.ReadByte(); 10 | } 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /Parsers/PropertyTagData/IntProperty.cs: -------------------------------------------------------------------------------- 1 | using PakReader.Parsers.Objects; 2 | 3 | namespace PakReader.Parsers.PropertyTagData 4 | { 5 | public sealed class IntProperty : BaseProperty 6 | { 7 | internal IntProperty(PackageReader reader, FPropertyTag tag) 8 | { 9 | Value = reader.ReadInt32(); 10 | } 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /Parsers/PropertyTagData/InterfaceProperty.cs: -------------------------------------------------------------------------------- 1 | using PakReader.Parsers.Objects; 2 | 3 | namespace PakReader.Parsers.PropertyTagData 4 | { 5 | public sealed class InterfaceProperty : BaseProperty 6 | { 7 | // Value is ObjectRef 8 | internal InterfaceProperty(PackageReader reader, FPropertyTag tag) 9 | { 10 | Value = reader.ReadUInt32(); 11 | } 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /Parsers/PropertyTagData/LazyObjectProperty.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using PakReader.Parsers.Objects; 3 | 4 | namespace PakReader.Parsers.PropertyTagData 5 | { 6 | public sealed class LazyObjectProperty : BaseProperty 7 | { 8 | internal LazyObjectProperty(PackageReader reader, FPropertyTag tag) 9 | { 10 | // Let me know if you find a package that has a LazyObjectProperty 11 | throw new NotImplementedException("Parsing of LazyObjectProperty types aren't supported yet."); 12 | } 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /Parsers/PropertyTagData/MapProperty.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using PakReader.Parsers.Objects; 4 | 5 | namespace PakReader.Parsers.PropertyTagData 6 | { 7 | public sealed class MapProperty : BaseProperty> 8 | { 9 | // https://github.com/EpicGames/UnrealEngine/blob/7d9919ac7bfd80b7483012eab342cb427d60e8c9/Engine/Source/Runtime/CoreUObject/Private/UObject/PropertyMap.cpp#L243 10 | internal MapProperty(PackageReader reader, FPropertyTag tag) 11 | { 12 | var NumKeysToRemove = reader.ReadInt32(); 13 | if (NumKeysToRemove != 0) 14 | { 15 | // Let me know if you find a package that has a non-zero NumKeysToRemove value 16 | throw new NotImplementedException("Parsing of non-zero NumKeysToRemove maps aren't supported yet."); 17 | } 18 | 19 | var NumEntries = reader.ReadInt32(); 20 | var dict = new Dictionary(NumEntries); 21 | for (int i = 0; i < NumEntries; i++) 22 | { 23 | dict[ReadProperty(reader, tag, tag.InnerType, ReadType.MAP)] = ReadProperty(reader, tag, tag.ValueType, ReadType.MAP); 24 | } 25 | Value = dict; 26 | } 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /Parsers/PropertyTagData/MulticastDelegateProperty.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using PakReader.Parsers.Objects; 3 | 4 | namespace PakReader.Parsers.PropertyTagData 5 | { 6 | public sealed class MulticastDelegateProperty : BaseProperty 7 | { 8 | internal MulticastDelegateProperty(PackageReader reader, FPropertyTag tag) 9 | { 10 | // Let me know if you find a package that has a MutlicastDelegateProperty 11 | throw new NotImplementedException("Parsing of MulticastDelegateProperty types aren't supported yet."); 12 | } 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /Parsers/PropertyTagData/NameProperty.cs: -------------------------------------------------------------------------------- 1 | using PakReader.Parsers.Objects; 2 | 3 | namespace PakReader.Parsers.PropertyTagData 4 | { 5 | public sealed class NameProperty : BaseProperty 6 | { 7 | internal NameProperty(PackageReader reader, FPropertyTag tag) 8 | { 9 | Value = reader.ReadFName(); 10 | } 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /Parsers/PropertyTagData/ObjectProperty.cs: -------------------------------------------------------------------------------- 1 | using PakReader.Parsers.Objects; 2 | 3 | namespace PakReader.Parsers.PropertyTagData 4 | { 5 | public sealed class ObjectProperty : BaseProperty 6 | { 7 | internal ObjectProperty(PackageReader reader, FPropertyTag tag) 8 | { 9 | Value = new FPackageIndex(reader); 10 | } 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /Parsers/PropertyTagData/SetProperty.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using PakReader.Parsers.Objects; 3 | 4 | namespace PakReader.Parsers.PropertyTagData 5 | { 6 | public sealed class SetProperty : BaseProperty 7 | { 8 | // https://github.com/EpicGames/UnrealEngine/blob/bf95c2cbc703123e08ab54e3ceccdd47e48d224a/Engine/Source/Runtime/CoreUObject/Private/UObject/PropertySet.cpp#L216 9 | internal SetProperty(PackageReader reader, FPropertyTag tag) 10 | { 11 | var NumKeysToRemove = reader.ReadInt32(); 12 | if (NumKeysToRemove != 0) 13 | { 14 | // Let me know if you find a package that has a non-zero NumKeysToRemove value 15 | throw new NotImplementedException("Parsing of non-zero NumKeysToRemove sets aren't supported yet."); 16 | } 17 | 18 | var NumEntries = reader.ReadInt32(); 19 | Value = new BaseProperty[NumEntries]; 20 | for (int i = 0; i < NumEntries; i++) 21 | { 22 | Value[i] = ReadProperty(reader, tag, tag.InnerType, ReadType.ARRAY); 23 | } 24 | } 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /Parsers/PropertyTagData/SoftObjectProperty.cs: -------------------------------------------------------------------------------- 1 | using PakReader.Parsers.Objects; 2 | 3 | namespace PakReader.Parsers.PropertyTagData 4 | { 5 | public sealed class SoftObjectProperty : BaseProperty 6 | { 7 | internal SoftObjectProperty(PackageReader reader, FPropertyTag tag, ReadType readType) 8 | { 9 | Value = new FSoftObjectPath(reader); 10 | if (readType == ReadType.MAP) 11 | reader.Position += 4; // skip ahead, putting the total bytes read to 16 12 | } 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /Parsers/PropertyTagData/StrProperty.cs: -------------------------------------------------------------------------------- 1 | using PakReader.Parsers.Objects; 2 | 3 | namespace PakReader.Parsers.PropertyTagData 4 | { 5 | public sealed class StrProperty : BaseProperty 6 | { 7 | internal StrProperty(PackageReader reader, FPropertyTag tag) 8 | { 9 | Value = reader.ReadFString(); 10 | } 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /Parsers/PropertyTagData/StructProperty.cs: -------------------------------------------------------------------------------- 1 | using PakReader.Parsers.Objects; 2 | 3 | namespace PakReader.Parsers.PropertyTagData 4 | { 5 | public sealed class StructProperty : BaseProperty 6 | { 7 | internal StructProperty(PackageReader reader, FPropertyTag tag) 8 | { 9 | Value = new UScriptStruct(reader, tag.StructName).Struct; 10 | } 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /Parsers/PropertyTagData/TextProperty.cs: -------------------------------------------------------------------------------- 1 | using PakReader.Parsers.Objects; 2 | 3 | namespace PakReader.Parsers.PropertyTagData 4 | { 5 | public sealed class TextProperty : BaseProperty 6 | { 7 | internal TextProperty(PackageReader reader, FPropertyTag tag) 8 | { 9 | Value = new FText(reader); 10 | } 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /Parsers/PropertyTagData/UInt16Property.cs: -------------------------------------------------------------------------------- 1 | using PakReader.Parsers.Objects; 2 | 3 | namespace PakReader.Parsers.PropertyTagData 4 | { 5 | public sealed class UInt16Property : BaseProperty 6 | { 7 | internal UInt16Property(PackageReader reader, FPropertyTag tag) 8 | { 9 | Value = reader.ReadUInt16(); 10 | } 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /Parsers/PropertyTagData/UInt32Property.cs: -------------------------------------------------------------------------------- 1 | using PakReader.Parsers.Objects; 2 | 3 | namespace PakReader.Parsers.PropertyTagData 4 | { 5 | public sealed class UInt32Property : BaseProperty 6 | { 7 | internal UInt32Property(PackageReader reader, FPropertyTag tag) 8 | { 9 | Value = reader.ReadUInt32(); 10 | } 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /Parsers/PropertyTagData/UInt64Property.cs: -------------------------------------------------------------------------------- 1 | using PakReader.Parsers.Objects; 2 | 3 | namespace PakReader.Parsers.PropertyTagData 4 | { 5 | public sealed class UInt64Property : BaseProperty 6 | { 7 | internal UInt64Property(PackageReader reader, FPropertyTag tag) 8 | { 9 | Value = reader.ReadUInt64(); 10 | } 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /Parsers/ReflectionHelper.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Reflection; 4 | 5 | namespace PakReader.Parsers 6 | { 7 | static class ReflectionHelper 8 | { 9 | readonly static Dictionary Setter)> PropertyCache = new Dictionary)>(); 10 | 11 | static class New 12 | { 13 | public static readonly Func Instance = GetInstance(); 14 | public static readonly IReadOnlyDictionary<(string Name, Type Type), Action> ActionMap = GetActionMap(); 15 | 16 | static Func GetInstance() 17 | { 18 | var constructor = typeof(T).GetConstructor(Type.EmptyTypes); 19 | return () => (T)constructor.Invoke(Array.Empty()); 20 | } 21 | 22 | static IReadOnlyDictionary<(string Name, Type Type), Action> GetActionMap() 23 | { 24 | var dict = new Dictionary<(string Name, Type Type), Action>(); 25 | 26 | var Fields = typeof(T).GetFields(BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic); 27 | var Properties = typeof(T).GetProperties(BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic); 28 | 29 | for (int i = 0; i < Properties.Length; i++) 30 | { 31 | dict[((Properties[i].GetCustomAttribute()?.Name ?? Properties[i].Name).ToLowerInvariant(), Properties[i].PropertyType)] = Properties[i].SetValue; 32 | } 33 | for (int i = 0; i < Fields.Length; i++) 34 | { 35 | dict[((Fields[i].GetCustomAttribute()?.Name ?? Fields[i].Name).ToLowerInvariant(), Fields[i].FieldType)] = Fields[i].SetValue; 36 | } 37 | 38 | return dict; 39 | } 40 | } 41 | 42 | internal static T NewInstance() => New.Instance(); 43 | internal static IReadOnlyDictionary<(string Name, Type Type), Action> GetActionMap() => New.ActionMap; 44 | 45 | internal static (Type BaseType, Func Getter) GetPropertyInfo(Type property) 46 | { 47 | if (!PropertyCache.TryGetValue(property, out var info)) 48 | { 49 | var baseType = property.BaseType; 50 | // PropertyCache[property] = info = (baseType.GenericTypeArguments[0], baseType.GetField("Value").GetValue); 51 | // Currently don't have this because of array/map/set property schenanigans 52 | PropertyCache[property] = info = (property, (prop) => prop); 53 | } 54 | return info; 55 | } 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /Parsers/Texture2D.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.IO; 3 | using PakReader.Parsers.Objects; 4 | using SkiaSharp; 5 | 6 | namespace PakReader.Parsers 7 | { 8 | public sealed class Texture2D : UObject 9 | { 10 | public FTexturePlatformData[] PlatformDatas { get; } 11 | 12 | SKImage image; 13 | public SKImage Image { 14 | get 15 | { 16 | if (image == null) 17 | { 18 | var mip = PlatformDatas[0].Mips[0]; 19 | image = TextureDecoder.DecodeImage(mip.BulkData.Data, mip.SizeX, mip.SizeY, mip.SizeZ, PlatformDatas[0].PixelFormat); 20 | } 21 | return image; 22 | } 23 | } 24 | 25 | internal Texture2D(PackageReader reader, Stream ubulk, int bulkOffset) : base(reader) 26 | { 27 | new FStripDataFlags(reader); // and I quote, "still no idea" 28 | new FStripDataFlags(reader); // "why there are two" :) 29 | 30 | if (reader.ReadInt32() != 0) // bIsCooked 31 | { 32 | var data = new List(1); // Probably gonna be only one texture anyway 33 | var PixelFormatName = reader.ReadFName(); 34 | while (!PixelFormatName.IsNone) 35 | { 36 | long SkipOffset = reader.ReadInt64(); 37 | data.Add(new FTexturePlatformData(reader, ubulk, bulkOffset)); 38 | PixelFormatName = reader.ReadFName(); 39 | } 40 | PlatformDatas = data.ToArray(); 41 | } 42 | } 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /Parsers/UObject.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections; 3 | using System.Collections.Generic; 4 | using Newtonsoft.Json; 5 | using PakReader.Parsers.Objects; 6 | using PakReader.Parsers.PropertyTagData; 7 | 8 | namespace PakReader.Parsers 9 | { 10 | public class UObject : IUStruct, IReadOnlyDictionary 11 | { 12 | public FObjectExport ExportInfo { get; internal set; } 13 | 14 | // This isn't exposed for ease of use to the properties instead of always referencing Dict 15 | readonly Dictionary Dict; 16 | 17 | readonly FGuid GUID; 18 | 19 | // https://github.com/EpicGames/UnrealEngine/blob/bf95c2cbc703123e08ab54e3ceccdd47e48d224a/Engine/Source/Runtime/CoreUObject/Private/UObject/Class.cpp#L930 20 | public UObject(PackageReader reader) : this(reader, false) { } 21 | 22 | // Structs that don't use binary serialization 23 | // https://github.com/EpicGames/UnrealEngine/blob/7d9919ac7bfd80b7483012eab342cb427d60e8c9/Engine/Source/Runtime/CoreUObject/Private/UObject/Class.cpp#L2197 24 | internal UObject(PackageReader reader, bool structFallback) 25 | { 26 | var props = new Dictionary(); 27 | 28 | while (true) 29 | { 30 | var Tag = new FPropertyTag(reader); 31 | if (Tag.Name.IsNone) 32 | break; 33 | 34 | var pos = reader.Position; 35 | props[Tag.Name.String] = BaseProperty.ReadProperty(reader, Tag, Tag.Type, ReadType.NORMAL); 36 | if (Tag.Size + pos != reader.Position) 37 | { 38 | Console.WriteLine($"Didn't read {Tag.Type.String} correctly (at {reader.Position}, should be {Tag.Size + pos}, {Tag.Size + pos - reader.Position} behind)"); 39 | reader.Position = Tag.Size + pos; 40 | } 41 | } 42 | Dict = props; 43 | 44 | if (!structFallback && reader.ReadInt32() != 0) 45 | { 46 | GUID = new FGuid(reader); 47 | } 48 | } 49 | 50 | public BaseProperty this[string key] => Dict[key]; 51 | public IEnumerable Keys => Dict.Keys; 52 | public IEnumerable Values => Dict.Values; 53 | public int Count => Dict.Count; 54 | public bool ContainsKey(string key) => Dict.ContainsKey(key); 55 | public IEnumerator> GetEnumerator() => Dict.GetEnumerator(); 56 | IEnumerator IEnumerable.GetEnumerator() => Dict.GetEnumerator(); 57 | 58 | public bool TryGetValue(string key, out BaseProperty value) => Dict.TryGetValue(key, out value); 59 | 60 | public T Deserialize() 61 | { 62 | var ret = ReflectionHelper.NewInstance(); 63 | var map = ReflectionHelper.GetActionMap(); 64 | foreach (var kv in Dict) 65 | { 66 | (var baseType, var typeGetter) = ReflectionHelper.GetPropertyInfo(kv.Value.GetType()); 67 | if (map.TryGetValue((kv.Key.ToLowerInvariant(), baseType), out Action setter)) 68 | { 69 | setter(ret, typeGetter(kv.Value)); 70 | } 71 | } 72 | return ret; 73 | } 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # PakReader 2 | Documentation soon, maybe? :) -------------------------------------------------------------------------------- /ReaderExtensions.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.IO; 3 | using System.Text; 4 | 5 | namespace PakReader 6 | { 7 | static class ReaderExtensions 8 | { 9 | public static string ReadFString(this BinaryReader reader) 10 | { 11 | // > 0 for ANSICHAR, < 0 for UCS2CHAR serialization 12 | var SaveNum = reader.ReadInt32(); 13 | bool LoadUCS2Char = SaveNum < 0; 14 | if (LoadUCS2Char) 15 | { 16 | // If SaveNum cannot be negated due to integer overflow, Ar is corrupted. 17 | if (SaveNum == int.MinValue) 18 | { 19 | throw new FileLoadException("Archive is corrupted"); 20 | } 21 | 22 | SaveNum = -SaveNum; 23 | } 24 | 25 | if (SaveNum == 0) return null; 26 | 27 | // 1 byte is removed because of null terminator (\0) 28 | if (LoadUCS2Char) 29 | { 30 | ushort[] data = new ushort[SaveNum]; 31 | for (int i = 0; i < SaveNum; i++) 32 | { 33 | data[i] = reader.ReadUInt16(); 34 | } 35 | unsafe 36 | { 37 | fixed (ushort* dataPtr = &data[0]) 38 | return new string((char*)dataPtr, 0, data.Length - 1); 39 | } 40 | } 41 | else 42 | { 43 | return Encoding.UTF8.GetString(reader.ReadBytes(SaveNum).AsSpan(..^1)); 44 | } 45 | } 46 | 47 | public static T[] ReadTArray(this BinaryReader reader, Func Getter) 48 | { 49 | int SerializeNum = reader.ReadInt32(); 50 | T[] A = new T[SerializeNum]; 51 | for(int i = 0; i < SerializeNum; i++) 52 | { 53 | A[i] = Getter(); 54 | } 55 | return A; 56 | } 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /TextureDecoder.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.IO; 3 | using PakReader.Parsers.Objects; 4 | using SkiaSharp; 5 | 6 | namespace PakReader 7 | { 8 | static class TextureDecoder 9 | { 10 | public static SKImage DecodeImage(byte[] sequence, int width, int height, int depth, EPixelFormat format) 11 | { 12 | byte[] data; 13 | SKColorType colorType; 14 | switch (format) 15 | { 16 | case EPixelFormat.PF_DXT5: 17 | data = DXTDecoder.DecodeDXT5(sequence, width, height, depth); 18 | colorType = SKColorType.Rgba8888; 19 | break; 20 | case EPixelFormat.PF_DXT1: 21 | data = DXTDecoder.DecodeDXT1(sequence, width, height, depth); 22 | colorType = SKColorType.Rgba8888; 23 | break; 24 | case EPixelFormat.PF_B8G8R8A8: 25 | data = sequence; 26 | colorType = SKColorType.Bgra8888; 27 | break; 28 | case EPixelFormat.PF_BC5: 29 | data = BCDecoder.DecodeBC5(sequence, width, height); 30 | colorType = SKColorType.Bgra8888; 31 | break; 32 | case EPixelFormat.PF_BC4: 33 | data = BCDecoder.DecodeBC4(sequence, width, height); 34 | colorType = SKColorType.Bgra8888; 35 | break; 36 | case EPixelFormat.PF_G8: 37 | data = sequence; 38 | colorType = SKColorType.Gray8; 39 | break; 40 | case EPixelFormat.PF_FloatRGBA: 41 | data = sequence; 42 | colorType = SKColorType.RgbaF16; 43 | break; 44 | default: 45 | throw new NotImplementedException($"Cannot decode {format} format"); 46 | } 47 | 48 | using (var bitmap = new SKBitmap(new SKImageInfo(width, height, colorType, SKAlphaType.Unpremul))) 49 | { 50 | unsafe 51 | { 52 | fixed (byte* p = data) 53 | { 54 | bitmap.SetPixels(new IntPtr(p)); 55 | } 56 | } 57 | return SKImage.FromBitmap(bitmap); 58 | } 59 | } 60 | 61 | static class BCDecoder 62 | { 63 | public static byte[] DecodeBC4(byte[] inp, int width, int height) 64 | { 65 | byte[] ret = new byte[width * height * 4]; 66 | using var reader = new BinaryReader(new MemoryStream(inp)); 67 | for (int y_block = 0; y_block < height / 4; y_block++) 68 | { 69 | for (int x_block = 0; x_block < width / 4; x_block++) 70 | { 71 | var r_bytes = DecodeBC3Block(reader); 72 | for (int i = 0; i < 16; i++) 73 | { 74 | ret[GetPixelLoc(width, x_block * 4 + (i % 4), y_block * 4 + (i / 4), 4, 0)] = r_bytes[i]; 75 | } 76 | } 77 | } 78 | return ret; 79 | } 80 | 81 | public static byte[] DecodeBC5(byte[] inp, int width, int height) 82 | { 83 | byte[] ret = new byte[width * height * 4]; 84 | using var reader = new BinaryReader(new MemoryStream(inp)); 85 | for (int y_block = 0; y_block < height / 4; y_block++) 86 | { 87 | for (int x_block = 0; x_block < width / 4; x_block++) 88 | { 89 | var r_bytes = DecodeBC3Block(reader); 90 | var g_bytes = DecodeBC3Block(reader); 91 | 92 | for (int i = 0; i < 16; i++) 93 | { 94 | ret[GetPixelLoc(width, x_block * 4 + (i % 4), y_block * 4 + (i / 4), 4, 0)] = r_bytes[i]; 95 | ret[GetPixelLoc(width, x_block * 4 + (i % 4), y_block * 4 + (i / 4), 4, 1)] = g_bytes[i]; 96 | ret[GetPixelLoc(width, x_block * 4 + (i % 4), y_block * 4 + (i / 4), 4, 2)] = GetZNormal(r_bytes[i], g_bytes[i]); 97 | } 98 | } 99 | } 100 | return ret; 101 | } 102 | 103 | static int GetPixelLoc(int width, int x, int y, int bpp, int off) => (y * width + x) * bpp + off; 104 | 105 | static byte GetZNormal(byte x, byte y) 106 | { 107 | var xf = (x / 127.5f) - 1; 108 | var yf = (y / 127.5f) - 1; 109 | var zval = 1 - xf * xf - yf * yf; 110 | var zval_ = (float)Math.Sqrt(zval > 0 ? zval : 0); 111 | zval = zval_ < 1 ? zval_ : 1; 112 | return (byte)((zval * 127) + 128); 113 | } 114 | 115 | static byte[] DecodeBC3Block(BinaryReader reader) 116 | { 117 | float ref0 = reader.ReadByte(); 118 | float ref1 = reader.ReadByte(); 119 | 120 | float[] ref_sl = new float[8]; 121 | ref_sl[0] = ref0; 122 | ref_sl[1] = ref1; 123 | 124 | if (ref0 > ref1) 125 | { 126 | ref_sl[2] = (6 * ref0 + 1 * ref1) / 7; 127 | ref_sl[3] = (5 * ref0 + 2 * ref1) / 7; 128 | ref_sl[4] = (4 * ref0 + 3 * ref1) / 7; 129 | ref_sl[5] = (3 * ref0 + 4 * ref1) / 7; 130 | ref_sl[6] = (2 * ref0 + 5 * ref1) / 7; 131 | ref_sl[7] = (1 * ref0 + 6 * ref1) / 7; 132 | } 133 | else 134 | { 135 | ref_sl[2] = (4 * ref0 + 1 * ref1) / 5; 136 | ref_sl[3] = (3 * ref0 + 2 * ref1) / 5; 137 | ref_sl[4] = (2 * ref0 + 3 * ref1) / 5; 138 | ref_sl[5] = (1 * ref0 + 4 * ref1) / 5; 139 | ref_sl[6] = 0; 140 | ref_sl[7] = 255; 141 | } 142 | 143 | byte[] index_block1 = GetBC3Indices(reader.ReadBytes(3)); 144 | 145 | byte[] index_block2 = GetBC3Indices(reader.ReadBytes(3)); 146 | 147 | byte[] bytes = new byte[16]; 148 | for (int i = 0; i < 8; i++) 149 | { 150 | bytes[7 - i] = (byte)ref_sl[index_block1[i]]; 151 | } 152 | for (int i = 0; i < 8; i++) 153 | { 154 | bytes[15 - i] = (byte)ref_sl[index_block2[i]]; 155 | } 156 | 157 | return bytes; 158 | } 159 | 160 | static byte[] GetBC3Indices(byte[] buf_block) => 161 | new byte[] { 162 | (byte)((buf_block[2] & 0b1110_0000) >> 5), 163 | (byte)((buf_block[2] & 0b0001_1100) >> 2), 164 | (byte)(((buf_block[2] & 0b0000_0011) << 1) | ((buf_block[1] & 0b1 << 7) >> 7)), 165 | (byte)((buf_block[1] & 0b0111_0000) >> 4), 166 | (byte)((buf_block[1] & 0b0000_1110) >> 1), 167 | (byte)(((buf_block[1] & 0b0000_0001) << 2) | ((buf_block[0] & 0b11 << 6) >> 6)), 168 | (byte)((buf_block[0] & 0b0011_1000) >> 3), 169 | (byte)(buf_block[0] & 0b0000_0111) 170 | }; 171 | } 172 | 173 | static class DXTDecoder 174 | { 175 | struct Colour8888 176 | { 177 | public byte red; 178 | public byte green; 179 | public byte blue; 180 | public byte alpha; 181 | } 182 | 183 | public static byte[] DecodeDXT1(byte[] inp, int width, int height, int depth) 184 | { 185 | var bpp = 4; 186 | var bps = width * bpp * 1; 187 | var sizeofplane = bps * height; 188 | 189 | byte[] rawData = new byte[depth * sizeofplane + height * bps + width * bpp]; 190 | var colours = new Colour8888[4]; 191 | colours[0].alpha = 0xFF; 192 | colours[1].alpha = 0xFF; 193 | colours[2].alpha = 0xFF; 194 | 195 | unsafe 196 | { 197 | fixed (byte* bytePtr = inp) 198 | { 199 | byte* temp = bytePtr; 200 | for (int z = 0; z < depth; z++) 201 | { 202 | for (int y = 0; y < height; y += 4) 203 | { 204 | for (int x = 0; x < width; x += 4) 205 | { 206 | ushort colour0 = *((ushort*)temp); 207 | ushort colour1 = *((ushort*)(temp + 2)); 208 | DxtcReadColor(colour0, ref colours[0]); 209 | DxtcReadColor(colour1, ref colours[1]); 210 | 211 | uint bitmask = ((uint*)temp)[1]; 212 | temp += 8; 213 | 214 | if (colour0 > colour1) 215 | { 216 | // Four-color block: derive the other two colors. 217 | // 00 = color_0, 01 = color_1, 10 = color_2, 11 = color_3 218 | // These 2-bit codes correspond to the 2-bit fields 219 | // stored in the 64-bit block. 220 | colours[2].blue = (byte)((2 * colours[0].blue + colours[1].blue + 1) / 3); 221 | colours[2].green = (byte)((2 * colours[0].green + colours[1].green + 1) / 3); 222 | colours[2].red = (byte)((2 * colours[0].red + colours[1].red + 1) / 3); 223 | //colours[2].alpha = 0xFF; 224 | 225 | colours[3].blue = (byte)((colours[0].blue + 2 * colours[1].blue + 1) / 3); 226 | colours[3].green = (byte)((colours[0].green + 2 * colours[1].green + 1) / 3); 227 | colours[3].red = (byte)((colours[0].red + 2 * colours[1].red + 1) / 3); 228 | colours[3].alpha = 0xFF; 229 | } 230 | else 231 | { 232 | // Three-color block: derive the other color. 233 | // 00 = color_0, 01 = color_1, 10 = color_2, 234 | // 11 = transparent. 235 | // These 2-bit codes correspond to the 2-bit fields 236 | // stored in the 64-bit block. 237 | colours[2].blue = (byte)((colours[0].blue + colours[1].blue) / 2); 238 | colours[2].green = (byte)((colours[0].green + colours[1].green) / 2); 239 | colours[2].red = (byte)((colours[0].red + colours[1].red) / 2); 240 | //colours[2].alpha = 0xFF; 241 | 242 | colours[3].blue = (byte)((colours[0].blue + 2 * colours[1].blue + 1) / 3); 243 | colours[3].green = (byte)((colours[0].green + 2 * colours[1].green + 1) / 3); 244 | colours[3].red = (byte)((colours[0].red + 2 * colours[1].red + 1) / 3); 245 | colours[3].alpha = 0x00; 246 | } 247 | 248 | for (int j = 0, k = 0; j < 4; j++) 249 | { 250 | for (int i = 0; i < 4; i++, k++) 251 | { 252 | int select = (int)((bitmask & (0x03 << k * 2)) >> k * 2); 253 | Colour8888 col = colours[select]; 254 | if (((x + i) < width) && ((y + j) < height)) 255 | { 256 | uint offset = (uint)(z * sizeofplane + (y + j) * bps + (x + i) * bpp); 257 | rawData[offset + 0] = col.red; 258 | rawData[offset + 1] = col.green; 259 | rawData[offset + 2] = col.blue; 260 | rawData[offset + 3] = col.alpha; 261 | } 262 | } 263 | } 264 | } 265 | } 266 | } 267 | } 268 | } 269 | 270 | return rawData; 271 | } 272 | 273 | public static byte[] DecodeDXT5(byte[] inp, int width, int height, int depth) 274 | { 275 | var bpp = 4; 276 | var bps = width * bpp * 1; 277 | var sizeofplane = bps * height; 278 | 279 | byte[] rawData = new byte[depth * sizeofplane + height * bps + width * bpp]; 280 | var colours = new Colour8888[4]; 281 | ushort[] alphas = new ushort[8]; 282 | 283 | unsafe 284 | { 285 | fixed (byte* bytePtr = inp) 286 | { 287 | byte* temp = bytePtr; 288 | for (int z = 0; z < depth; z++) 289 | { 290 | for (int y = 0; y < height; y += 4) 291 | { 292 | for (int x = 0; x < width; x += 4) 293 | { 294 | if (y >= height || x >= width) 295 | break; 296 | 297 | alphas[0] = temp[0]; 298 | alphas[1] = temp[1]; 299 | byte* alphamask = (temp + 2); 300 | temp += 8; 301 | 302 | DxtcReadColors(temp, colours); 303 | uint bitmask = ((uint*)temp)[1]; 304 | temp += 8; 305 | 306 | // Four-color block: derive the other two colors. 307 | // 00 = color_0, 01 = color_1, 10 = color_2, 11 = color_3 308 | // These 2-bit codes correspond to the 2-bit fields 309 | // stored in the 64-bit block. 310 | colours[2].blue = (byte)((2 * colours[0].blue + colours[1].blue + 1) / 3); 311 | colours[2].green = (byte)((2 * colours[0].green + colours[1].green + 1) / 3); 312 | colours[2].red = (byte)((2 * colours[0].red + colours[1].red + 1) / 3); 313 | //colours[2].alpha = 0xFF; 314 | 315 | colours[3].blue = (byte)((colours[0].blue + 2 * colours[1].blue + 1) / 3); 316 | colours[3].green = (byte)((colours[0].green + 2 * colours[1].green + 1) / 3); 317 | colours[3].red = (byte)((colours[0].red + 2 * colours[1].red + 1) / 3); 318 | //colours[3].alpha = 0xFF; 319 | 320 | int k = 0; 321 | for (int j = 0; j < 4; j++) 322 | { 323 | for (int i = 0; i < 4; k++, i++) 324 | { 325 | int select = (int)((bitmask & (0x03 << k * 2)) >> k * 2); 326 | Colour8888 col = colours[select]; 327 | // only put pixels out < width or height 328 | if (((x + i) < width) && ((y + j) < height)) 329 | { 330 | uint offset = (uint)(z * sizeofplane + (y + j) * bps + (x + i) * bpp); 331 | rawData[offset] = col.red; 332 | rawData[offset + 1] = col.green; 333 | rawData[offset + 2] = col.blue; 334 | } 335 | } 336 | } 337 | 338 | // 8-alpha or 6-alpha block? 339 | if (alphas[0] > alphas[1]) 340 | { 341 | // 8-alpha block: derive the other six alphas. 342 | // Bit code 000 = alpha_0, 001 = alpha_1, others are interpolated. 343 | alphas[2] = (ushort)((6 * alphas[0] + 1 * alphas[1] + 3) / 7); // bit code 010 344 | alphas[3] = (ushort)((5 * alphas[0] + 2 * alphas[1] + 3) / 7); // bit code 011 345 | alphas[4] = (ushort)((4 * alphas[0] + 3 * alphas[1] + 3) / 7); // bit code 100 346 | alphas[5] = (ushort)((3 * alphas[0] + 4 * alphas[1] + 3) / 7); // bit code 101 347 | alphas[6] = (ushort)((2 * alphas[0] + 5 * alphas[1] + 3) / 7); // bit code 110 348 | alphas[7] = (ushort)((1 * alphas[0] + 6 * alphas[1] + 3) / 7); // bit code 111 349 | } 350 | else 351 | { 352 | // 6-alpha block. 353 | // Bit code 000 = alpha_0, 001 = alpha_1, others are interpolated. 354 | alphas[2] = (ushort)((4 * alphas[0] + 1 * alphas[1] + 2) / 5); // Bit code 010 355 | alphas[3] = (ushort)((3 * alphas[0] + 2 * alphas[1] + 2) / 5); // Bit code 011 356 | alphas[4] = (ushort)((2 * alphas[0] + 3 * alphas[1] + 2) / 5); // Bit code 100 357 | alphas[5] = (ushort)((1 * alphas[0] + 4 * alphas[1] + 2) / 5); // Bit code 101 358 | alphas[6] = 0x00; // Bit code 110 359 | alphas[7] = 0xFF; // Bit code 111 360 | } 361 | 362 | // Note: Have to separate the next two loops, 363 | // it operates on a 6-byte system. 364 | 365 | // First three bytes 366 | //uint bits = (uint)(alphamask[0]); 367 | uint bits = (uint)((alphamask[0]) | (alphamask[1] << 8) | (alphamask[2] << 16)); 368 | for (int j = 0; j < 2; j++) 369 | { 370 | for (int i = 0; i < 4; i++) 371 | { 372 | // only put pixels out < width or height 373 | if (((x + i) < width) && ((y + j) < height)) 374 | { 375 | uint offset = (uint)(z * sizeofplane + (y + j) * bps + (x + i) * bpp + 3); 376 | rawData[offset] = (byte)alphas[bits & 0x07]; 377 | } 378 | bits >>= 3; 379 | } 380 | } 381 | 382 | // Last three bytes 383 | //bits = (uint)(alphamask[3]); 384 | bits = (uint)((alphamask[3]) | (alphamask[4] << 8) | (alphamask[5] << 16)); 385 | for (int j = 2; j < 4; j++) 386 | { 387 | for (int i = 0; i < 4; i++) 388 | { 389 | // only put pixels out < width or height 390 | if (((x + i) < width) && ((y + j) < height)) 391 | { 392 | uint offset = (uint)(z * sizeofplane + (y + j) * bps + (x + i) * bpp + 3); 393 | rawData[offset] = (byte)alphas[bits & 0x07]; 394 | } 395 | bits >>= 3; 396 | } 397 | } 398 | } 399 | } 400 | } 401 | } 402 | return rawData; 403 | } 404 | } 405 | 406 | static unsafe void DxtcReadColors(byte* data, Colour8888[] op) 407 | { 408 | byte buf = (byte)((data[1] & 0xF8) >> 3); 409 | op[0].red = (byte)(buf << 3 | buf >> 2); 410 | buf = (byte)(((data[0] & 0xE0) >> 5) | ((data[1] & 0x7) << 3)); 411 | op[0].green = (byte)(buf << 2 | buf >> 3); 412 | buf = (byte)(data[0] & 0x1F); 413 | op[0].blue = (byte)(buf << 3 | buf >> 2); 414 | 415 | buf = (byte)((data[3] & 0xF8) >> 3); 416 | op[1].red = (byte)(buf << 3 | buf >> 2); 417 | buf = (byte)(((data[2] & 0xE0) >> 5) | ((data[3] & 0x7) << 3)); 418 | op[1].green = (byte)(buf << 2 | buf >> 3); 419 | buf = (byte)(data[2] & 0x1F); 420 | op[1].blue = (byte)(buf << 3 | buf >> 2); 421 | } 422 | 423 | static void DxtcReadColor(ushort data, ref Colour8888 op) 424 | { 425 | byte buf = (byte)((data & 0xF800) >> 11); 426 | op.red = (byte)(buf << 3 | buf >> 2); 427 | buf = (byte)((data & 0x7E0) >> 5); 428 | op.green = (byte)(buf << 2 | buf >> 3); 429 | buf = (byte)(data & 0x1f); 430 | op.blue = (byte)(buf << 3 | buf >> 2); 431 | } 432 | } 433 | } 434 | } 435 | --------------------------------------------------------------------------------