├── idCrypt ├── idCrypt.exe ├── decrypt_all.bat ├── readme.txt └── idCrypt.c ├── DOOMExtract ├── packages.config ├── App.config ├── Utility.cs ├── Properties │ └── AssemblyInfo.cs ├── DOOMExtract.csproj ├── DOOMResourceEntry.cs ├── Program.cs ├── EndianIO.cs └── DOOMResourceIndex.cs ├── DOOMModLoader ├── packages.config ├── App.config ├── Properties │ └── AssemblyInfo.cs ├── DOOMModLoader.csproj └── Program.cs ├── README.md ├── DOOMExtract.sln └── .gitignore /idCrypt/idCrypt.exe: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/emoose/DOOMExtract/HEAD/idCrypt/idCrypt.exe -------------------------------------------------------------------------------- /DOOMExtract/packages.config: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /DOOMModLoader/packages.config: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /DOOMExtract/App.config: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /DOOMModLoader/App.config: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # DOOMExtract 2 | 3 | A C# command-line tool for extracting DOOM 2016 resources. 4 | 5 | Compiled releases can be found [here](https://github.com/emoose/DOOMExtract/releases). 6 | 7 | If you have any issues please make a report on the [issues page](https://github.com/emoose/DOOMExtract/issues). 8 | 9 | ## DOOMModLoader 10 | 11 | DOOMModLoader is a simple program that allows you to load up a modified version of the game, without needing to bother with patch files/repacking. 12 | 13 | It also allows you to share mods via simple zip files, which through the loader can be easily used together with other mods, on pretty much any current or future version of DOOM. 14 | 15 | Compiled versions can be found at the releases link above. 16 | -------------------------------------------------------------------------------- /DOOMExtract/Utility.cs: -------------------------------------------------------------------------------- 1 | using System.IO; 2 | 3 | namespace DOOMExtract 4 | { 5 | public static class Utility 6 | { 7 | public static long StreamCopy(Stream destStream, Stream sourceStream, int bufferSize, long length) 8 | { 9 | long read = 0; 10 | while (read < length) 11 | { 12 | int toRead = bufferSize; 13 | if (toRead > length - read) 14 | toRead = (int)(length - read); 15 | 16 | var buf = new byte[toRead]; 17 | int buf_read = sourceStream.Read(buf, 0, toRead); 18 | destStream.Write(buf, 0, buf_read); 19 | read += buf_read; 20 | if (buf_read == 0) 21 | break; // no more to be read.. 22 | } 23 | return read; 24 | } 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /idCrypt/decrypt_all.bat: -------------------------------------------------------------------------------- 1 | @echo off 2 | 3 | idCrypt default.bfile;binaryFile default.cfg 4 | idCrypt default_coop.bfile;binaryFile default_coop.cfg 5 | idCrypt default_mp.bfile;binaryFile default_mp.cfg 6 | idCrypt default_sp.bfile;binaryFile default_sp.cfg 7 | idCrypt package.bfile;binaryFile package.cfg 8 | idCrypt snapdemo.bfile;binaryFile snapdemo.cfg 9 | idCrypt snapedit.bfile;binaryFile snapedit.cfg 10 | idCrypt xb_classic.bfile;binaryFile xb_classic.cfg 11 | idCrypt xb_default.bfile;binaryFile xb_default.cfg 12 | idCrypt xb_knuckles.bfile;binaryFile xb_knuckles.cfg 13 | idCrypt xb_mirrored.bfile;binaryFile xb_mirrored.cfg 14 | idCrypt xb_strafe.bfile;binaryFile xb_strafe.cfg 15 | idCrypt xb_tactical.bfile;binaryFile xb_tactical.cfg 16 | 17 | idCrypt strings/chinese.bfile;binaryFile strings/chinese.lang 18 | idCrypt strings/english.bfile;binaryFile strings/english.lang 19 | idCrypt strings/french.bfile;binaryFile strings/french.lang 20 | idCrypt strings/german.bfile;binaryFile strings/german.lang 21 | idCrypt strings/italian.bfile;binaryFile strings/italian.lang 22 | idCrypt strings/japanese.bfile;binaryFile strings/japanese.lang 23 | idCrypt strings/latin_spanish.bfile;binaryFile strings/latin_spanish.lang 24 | idCrypt strings/polish.bfile;binaryFile strings/polish.lang 25 | idCrypt strings/portuguese.bfile;binaryFile strings/portuguese.lang 26 | idCrypt strings/russian.bfile;binaryFile strings/russian.lang 27 | idCrypt strings/spanish.bfile;binaryFile strings/spanish.lang -------------------------------------------------------------------------------- /DOOMExtract/Properties/AssemblyInfo.cs: -------------------------------------------------------------------------------- 1 | using System.Reflection; 2 | using System.Runtime.CompilerServices; 3 | using System.Runtime.InteropServices; 4 | 5 | // General Information about an assembly is controlled through the following 6 | // set of attributes. Change these attribute values to modify the information 7 | // associated with an assembly. 8 | [assembly: AssemblyTitle("DOOMExtract")] 9 | [assembly: AssemblyDescription("")] 10 | [assembly: AssemblyConfiguration("")] 11 | [assembly: AssemblyCompany("")] 12 | [assembly: AssemblyProduct("DOOMExtract")] 13 | [assembly: AssemblyCopyright("Copyright © 2016")] 14 | [assembly: AssemblyTrademark("")] 15 | [assembly: AssemblyCulture("")] 16 | 17 | // Setting ComVisible to false makes the types in this assembly not visible 18 | // to COM components. If you need to access a type in this assembly from 19 | // COM, set the ComVisible attribute to true on that type. 20 | [assembly: ComVisible(false)] 21 | 22 | // The following GUID is for the ID of the typelib if this project is exposed to COM 23 | [assembly: Guid("ccfce054-db02-47a6-b2cc-590e9a87a806")] 24 | 25 | // Version information for an assembly consists of the following four values: 26 | // 27 | // Major Version 28 | // Minor Version 29 | // Build Number 30 | // Revision 31 | // 32 | // You can specify all the values or you can default the Build and Revision Numbers 33 | // by using the '*' as shown below: 34 | // [assembly: AssemblyVersion("1.0.*")] 35 | [assembly: AssemblyVersion("1.6.1.0")] 36 | [assembly: AssemblyFileVersion("1.6.1.0")] 37 | -------------------------------------------------------------------------------- /DOOMExtract.sln: -------------------------------------------------------------------------------- 1 | 2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio 14 4 | VisualStudioVersion = 14.0.25420.1 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DOOMExtract", "DOOMExtract\DOOMExtract.csproj", "{3B92A677-1D52-445E-A1D2-E228FB3DF706}" 7 | EndProject 8 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DOOMModLoader", "DOOMModLoader\DOOMModLoader.csproj", "{CADE36CB-99D4-4CD8-BBB3-09DBDBADA4C5}" 9 | EndProject 10 | Global 11 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 12 | Debug|Any CPU = Debug|Any CPU 13 | Release|Any CPU = Release|Any CPU 14 | EndGlobalSection 15 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 16 | {3B92A677-1D52-445E-A1D2-E228FB3DF706}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 17 | {3B92A677-1D52-445E-A1D2-E228FB3DF706}.Debug|Any CPU.Build.0 = Debug|Any CPU 18 | {3B92A677-1D52-445E-A1D2-E228FB3DF706}.Release|Any CPU.ActiveCfg = Release|Any CPU 19 | {3B92A677-1D52-445E-A1D2-E228FB3DF706}.Release|Any CPU.Build.0 = Release|Any CPU 20 | {CADE36CB-99D4-4CD8-BBB3-09DBDBADA4C5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 21 | {CADE36CB-99D4-4CD8-BBB3-09DBDBADA4C5}.Debug|Any CPU.Build.0 = Debug|Any CPU 22 | {CADE36CB-99D4-4CD8-BBB3-09DBDBADA4C5}.Release|Any CPU.ActiveCfg = Release|Any CPU 23 | {CADE36CB-99D4-4CD8-BBB3-09DBDBADA4C5}.Release|Any CPU.Build.0 = Release|Any CPU 24 | EndGlobalSection 25 | GlobalSection(SolutionProperties) = preSolution 26 | HideSolutionNode = FALSE 27 | EndGlobalSection 28 | EndGlobal 29 | -------------------------------------------------------------------------------- /DOOMModLoader/Properties/AssemblyInfo.cs: -------------------------------------------------------------------------------- 1 | using System.Reflection; 2 | using System.Runtime.CompilerServices; 3 | using System.Runtime.InteropServices; 4 | 5 | // General Information about an assembly is controlled through the following 6 | // set of attributes. Change these attribute values to modify the information 7 | // associated with an assembly. 8 | [assembly: AssemblyTitle("DOOMModLoader")] 9 | [assembly: AssemblyDescription("")] 10 | [assembly: AssemblyConfiguration("")] 11 | [assembly: AssemblyCompany("")] 12 | [assembly: AssemblyProduct("DOOMModLoader")] 13 | [assembly: AssemblyCopyright("Copyright © 2017")] 14 | [assembly: AssemblyTrademark("")] 15 | [assembly: AssemblyCulture("")] 16 | 17 | // Setting ComVisible to false makes the types in this assembly not visible 18 | // to COM components. If you need to access a type in this assembly from 19 | // COM, set the ComVisible attribute to true on that type. 20 | [assembly: ComVisible(false)] 21 | 22 | // The following GUID is for the ID of the typelib if this project is exposed to COM 23 | [assembly: Guid("6c8cde70-107d-41ba-bc69-f4bd8ae7dc2e")] 24 | 25 | // Version information for an assembly consists of the following four values: 26 | // 27 | // Major Version 28 | // Minor Version 29 | // Build Number 30 | // Revision 31 | // 32 | // You can specify all the values or you can default the Build and Revision Numbers 33 | // by using the '*' as shown below: 34 | // [assembly: AssemblyVersion("1.0.*")] 35 | [assembly: AssemblyVersion("0.1.0.0")] 36 | [assembly: AssemblyFileVersion("0.1.0.0")] 37 | -------------------------------------------------------------------------------- /DOOMExtract/DOOMExtract.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Debug 6 | AnyCPU 7 | {3B92A677-1D52-445E-A1D2-E228FB3DF706} 8 | Exe 9 | Properties 10 | DOOMExtract 11 | DOOMExtract 12 | v4.5 13 | 512 14 | 15 | 16 | x86 17 | true 18 | full 19 | false 20 | bin\Debug\ 21 | DEBUG;TRACE 22 | prompt 23 | 4 24 | 25 | 26 | x86 27 | pdbonly 28 | true 29 | bin\Release\ 30 | TRACE 31 | prompt 32 | 4 33 | 34 | 35 | 36 | ..\packages\SharpZipLib.0.86.0\lib\20\ICSharpCode.SharpZipLib.dll 37 | True 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 67 | -------------------------------------------------------------------------------- /idCrypt/readme.txt: -------------------------------------------------------------------------------- 1 | idCrypt v1.0 - by emoose 2 | 3 | === 4 | Info 5 | === 6 | idCrypt is a command-line tool that allows you to decrypt those awful .bfile files into plaintext, and also encrypt plaintext back into those awful .bfiles. 7 | 8 | Source code is provided in the idCrypt.c file for anyone interested (at exactly 250 lines of code :D) 9 | 10 | Usage: 11 | - idCrypt.exe 12 | 13 | If the input file path ends in .bfile/".bfile;binaryFile" the file will be decrypted to .dec 14 | Otherwise the file will be encrypted to .bfile. 15 | 16 | Example 1, decrypt english.bfile to english.dec: 17 | - idCrypt.exe D:\english.bfile strings/english.lang 18 | 19 | Example 2, encrypt english.dec to english.bfile: 20 | - idCrypt.exe D:\english.dec strings/english.lang 21 | 22 | Note that you _must_ use the correct internal filename! If it's incorrect idCrypt will fail to decrypt the .bfile, and the game will fail to decrypt your custom .bfile! 23 | 24 | If you like this you should also check out my other project LegacyMod for DOOM, which unlocks a bunch of commands/cvars and even adds things like the noclip command: 25 | https://cs.rin.ru/forum/viewtopic.php?p=1519324 (direct link: http://bit.ly/2wKyJXM) 26 | 27 | === 28 | Technical description 29 | === 30 | A .bfile is composed of a few different parts (in order): 31 | - 0xC bytes random salt (to make sure each file is encrypted differently) 32 | - 0x10 bytes AES IV (initialization vector) 33 | - n-bytes encrypted cipher-text 34 | - 0x20 bytes HMAC signature 35 | 36 | The cipher-text is encrypted with AES128, the key for this is generated by creating a SHA256 hash and updating it with the following (in order): 37 | - 0xC byte salt from the .bfile header 38 | - static string "swapTeam\n" (where \n is a newline character) 39 | - the internal filename of the .bfile (eg. strings/english.lang) 40 | 41 | The first 0x10 bytes of the resulting hash is used as the encryption key. 42 | In other terms, enc_key = SHA256(headerSalt, "swapTeam\n", internalFilePath)[:0x10] 43 | 44 | The data gets crypted in one go using the BCRYPT_BLOCK_PADDING flag which automatically pads it to a 0x10 byte alignment. As shown above the IV used for this crypto-operation is taken from the 0x10 bytes at offset 0xC in the .bfile. 45 | 46 | The HMAC is made with HMAC-SHA256, with the secret/key being the full 0x20 byte encryption key from above. The hash is created by updating with the following (in order): 47 | - 0xC byte salt from the .bfile header 48 | - 0x10 byte IV from the .bfile header 49 | - n-bytes of the encrypted ciphertext 50 | So basically the HMAC hash is made of the whole encrypted .bfile, minus the 0x20 bytes at the end where the HMAC gets stored. The game doesn't hash it all at once though, instead the hash is updated with each part seperately (unsure if this actually makes a difference or not though) 51 | 52 | In other words, hmac_hash = HMAC-SHA256(key = enc_key, headerSalt, headerIV, encryptedData) 53 | 54 | Source code is also included in idCrypt.c, hopefully it should be readable enough to understand. 55 | 56 | === 57 | Version History 58 | === 59 | 60 | v1.0 61 | - add support for encrypting files into .bfile 62 | - also release idCrypt.c source code 63 | 64 | v0.5 65 | - initial release, supports decrypting only 66 | 67 | === 68 | Greets 69 | === 70 | The guys on cs.rin.ru for helping to test when I asked, and everyone out there that's still into DOOM modding! -------------------------------------------------------------------------------- /DOOMModLoader/DOOMModLoader.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Debug 6 | AnyCPU 7 | {CADE36CB-99D4-4CD8-BBB3-09DBDBADA4C5} 8 | Exe 9 | Properties 10 | DOOMModLoader 11 | DOOMModLoader 12 | v4.5.2 13 | 512 14 | true 15 | 16 | 17 | AnyCPU 18 | true 19 | full 20 | false 21 | bin\Debug\ 22 | DEBUG;TRACE 23 | prompt 24 | 4 25 | 26 | 27 | AnyCPU 28 | pdbonly 29 | true 30 | bin\Release\ 31 | TRACE 32 | prompt 33 | 4 34 | 35 | 36 | 37 | ..\packages\SharpZipLib.0.86.0\lib\20\ICSharpCode.SharpZipLib.dll 38 | True 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 69 | -------------------------------------------------------------------------------- /DOOMExtract/DOOMResourceEntry.cs: -------------------------------------------------------------------------------- 1 | using ICSharpCode.SharpZipLib.Zip.Compression.Streams; 2 | using System; 3 | using System.IO; 4 | 5 | namespace DOOMExtract 6 | { 7 | public class DOOMResourceEntry 8 | { 9 | private DOOMResourceIndex _index; 10 | 11 | public int ID; 12 | public string FileType; // type? 13 | public string FileName2; // ?? 14 | public string FileName3; // full path 15 | 16 | public long Offset; 17 | public int Size; 18 | public int CompressedSize; 19 | public long Zero; 20 | public byte PatchFileNumber; 21 | 22 | public bool IsCompressed { get { return Size != CompressedSize; } } 23 | public DOOMResourceEntry(DOOMResourceIndex index) 24 | { 25 | _index = index; 26 | } 27 | 28 | public override string ToString() 29 | { 30 | return GetFullName(); 31 | } 32 | 33 | public string GetFullName() 34 | { 35 | if (!String.IsNullOrEmpty(FileName3)) 36 | return FileName3.Replace("/", "\\"); // convert to windows path 37 | 38 | if (!String.IsNullOrEmpty(FileName2)) 39 | return FileName2.Replace("/", "\\"); // convert to windows path 40 | 41 | return FileType.Replace("/", "\\"); // convert to windows path 42 | 43 | } 44 | public void Read(EndianIO io) 45 | { 46 | io.BigEndian = true; 47 | ID = io.Reader.ReadInt32(); 48 | 49 | io.BigEndian = false; 50 | // fname1 51 | int size = io.Reader.ReadInt32(); 52 | FileType = io.Reader.ReadAsciiString(size); 53 | // fname2 54 | size = io.Reader.ReadInt32(); 55 | FileName2 = io.Reader.ReadAsciiString(size); 56 | // fname3 57 | size = io.Reader.ReadInt32(); 58 | FileName3 = io.Reader.ReadAsciiString(size); 59 | 60 | io.BigEndian = true; 61 | 62 | Offset = io.Reader.ReadInt64(); 63 | Size = io.Reader.ReadInt32(); 64 | CompressedSize = io.Reader.ReadInt32(); 65 | if (_index.Header_Version <= 4) 66 | Zero = io.Reader.ReadInt64(); 67 | else 68 | Zero = io.Reader.ReadInt32(); // Zero field is 4 bytes instead of 8 in version 5+ 69 | 70 | PatchFileNumber = io.Reader.ReadByte(); 71 | } 72 | 73 | public void Write(EndianIO io) 74 | { 75 | io.BigEndian = true; 76 | io.Writer.Write(ID); 77 | 78 | io.BigEndian = false; 79 | io.Writer.Write(FileType.Length); 80 | io.Writer.WriteAsciiString(FileType, FileType.Length); 81 | io.Writer.Write(FileName2.Length); 82 | io.Writer.WriteAsciiString(FileName2, FileName2.Length); 83 | io.Writer.Write(FileName3.Length); 84 | io.Writer.WriteAsciiString(FileName3, FileName3.Length); 85 | 86 | io.BigEndian = true; 87 | 88 | io.Writer.Write(Offset); 89 | io.Writer.Write(Size); 90 | io.Writer.Write(CompressedSize); 91 | if (_index.Header_Version <= 4) 92 | io.Writer.Write(Zero); 93 | else 94 | io.Writer.Write((int)Zero); // Zero field is 4 bytes instead of 8 in version 5+ 95 | io.Writer.Write(PatchFileNumber); 96 | } 97 | 98 | public Stream GetDataStream(bool decompress) 99 | { 100 | if (Size == 0 && CompressedSize == 0) 101 | return null; 102 | 103 | var io = _index.GetResourceIO(PatchFileNumber); 104 | if (io == null) 105 | return null; 106 | 107 | io.Stream.Position = Offset; 108 | Stream dataStream = io.Stream; 109 | if (IsCompressed && decompress) 110 | dataStream = new InflaterInputStream(io.Stream, new ICSharpCode.SharpZipLib.Zip.Compression.Inflater(true), 4096); 111 | 112 | return dataStream; 113 | } 114 | } 115 | } 116 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ## Ignore Visual Studio temporary files, build results, and 2 | ## files generated by popular Visual Studio add-ons. 3 | ## 4 | ## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore 5 | 6 | # User-specific files 7 | *.suo 8 | *.user 9 | *.userosscache 10 | *.sln.docstates 11 | 12 | # User-specific files (MonoDevelop/Xamarin Studio) 13 | *.userprefs 14 | 15 | # Build results 16 | [Dd]ebug/ 17 | [Dd]ebugPublic/ 18 | [Rr]elease/ 19 | [Rr]eleases/ 20 | x64/ 21 | x86/ 22 | bld/ 23 | [Bb]in/ 24 | [Oo]bj/ 25 | [Ll]og/ 26 | 27 | # Visual Studio 2015 cache/options directory 28 | .vs/ 29 | # Uncomment if you have tasks that create the project's static files in wwwroot 30 | #wwwroot/ 31 | 32 | # MSTest test Results 33 | [Tt]est[Rr]esult*/ 34 | [Bb]uild[Ll]og.* 35 | 36 | # NUNIT 37 | *.VisualState.xml 38 | TestResult.xml 39 | 40 | # Build Results of an ATL Project 41 | [Dd]ebugPS/ 42 | [Rr]eleasePS/ 43 | dlldata.c 44 | 45 | # .NET Core 46 | project.lock.json 47 | project.fragment.lock.json 48 | artifacts/ 49 | **/Properties/launchSettings.json 50 | 51 | *_i.c 52 | *_p.c 53 | *_i.h 54 | *.ilk 55 | *.meta 56 | *.obj 57 | *.pch 58 | *.pdb 59 | *.pgc 60 | *.pgd 61 | *.rsp 62 | *.sbr 63 | *.tlb 64 | *.tli 65 | *.tlh 66 | *.tmp 67 | *.tmp_proj 68 | *.log 69 | *.vspscc 70 | *.vssscc 71 | .builds 72 | *.pidb 73 | *.svclog 74 | *.scc 75 | 76 | # Chutzpah Test files 77 | _Chutzpah* 78 | 79 | # Visual C++ cache files 80 | ipch/ 81 | *.aps 82 | *.ncb 83 | *.opendb 84 | *.opensdf 85 | *.sdf 86 | *.cachefile 87 | *.VC.db 88 | *.VC.VC.opendb 89 | 90 | # Visual Studio profiler 91 | *.psess 92 | *.vsp 93 | *.vspx 94 | *.sap 95 | 96 | # TFS 2012 Local Workspace 97 | $tf/ 98 | 99 | # Guidance Automation Toolkit 100 | *.gpState 101 | 102 | # ReSharper is a .NET coding add-in 103 | _ReSharper*/ 104 | *.[Rr]e[Ss]harper 105 | *.DotSettings.user 106 | 107 | # JustCode is a .NET coding add-in 108 | .JustCode 109 | 110 | # TeamCity is a build add-in 111 | _TeamCity* 112 | 113 | # DotCover is a Code Coverage Tool 114 | *.dotCover 115 | 116 | # Visual Studio code coverage results 117 | *.coverage 118 | *.coveragexml 119 | 120 | # NCrunch 121 | _NCrunch_* 122 | .*crunch*.local.xml 123 | nCrunchTemp_* 124 | 125 | # MightyMoose 126 | *.mm.* 127 | AutoTest.Net/ 128 | 129 | # Web workbench (sass) 130 | .sass-cache/ 131 | 132 | # Installshield output folder 133 | [Ee]xpress/ 134 | 135 | # DocProject is a documentation generator add-in 136 | DocProject/buildhelp/ 137 | DocProject/Help/*.HxT 138 | DocProject/Help/*.HxC 139 | DocProject/Help/*.hhc 140 | DocProject/Help/*.hhk 141 | DocProject/Help/*.hhp 142 | DocProject/Help/Html2 143 | DocProject/Help/html 144 | 145 | # Click-Once directory 146 | publish/ 147 | 148 | # Publish Web Output 149 | *.[Pp]ublish.xml 150 | *.azurePubxml 151 | # TODO: Comment the next line if you want to checkin your web deploy settings 152 | # but database connection strings (with potential passwords) will be unencrypted 153 | *.pubxml 154 | *.publishproj 155 | 156 | # Microsoft Azure Web App publish settings. Comment the next line if you want to 157 | # checkin your Azure Web App publish settings, but sensitive information contained 158 | # in these scripts will be unencrypted 159 | PublishScripts/ 160 | 161 | # NuGet Packages 162 | *.nupkg 163 | # The packages folder can be ignored because of Package Restore 164 | **/packages/* 165 | # except build/, which is used as an MSBuild target. 166 | !**/packages/build/ 167 | # Uncomment if necessary however generally it will be regenerated when needed 168 | #!**/packages/repositories.config 169 | # NuGet v3's project.json files produces more ignoreable files 170 | *.nuget.props 171 | *.nuget.targets 172 | 173 | # Microsoft Azure Build Output 174 | csx/ 175 | *.build.csdef 176 | 177 | # Microsoft Azure Emulator 178 | ecf/ 179 | rcf/ 180 | 181 | # Windows Store app package directories and files 182 | AppPackages/ 183 | BundleArtifacts/ 184 | Package.StoreAssociation.xml 185 | _pkginfo.txt 186 | 187 | # Visual Studio cache files 188 | # files ending in .cache can be ignored 189 | *.[Cc]ache 190 | # but keep track of directories ending in .cache 191 | !*.[Cc]ache/ 192 | 193 | # Others 194 | ClientBin/ 195 | ~$* 196 | *~ 197 | *.dbmdl 198 | *.dbproj.schemaview 199 | *.jfm 200 | *.pfx 201 | *.publishsettings 202 | orleans.codegen.cs 203 | 204 | # Since there are multiple workflows, uncomment next line to ignore bower_components 205 | # (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) 206 | #bower_components/ 207 | 208 | # RIA/Silverlight projects 209 | Generated_Code/ 210 | 211 | # Backup & report files from converting an old project file 212 | # to a newer Visual Studio version. Backup files are not needed, 213 | # because we have git ;-) 214 | _UpgradeReport_Files/ 215 | Backup*/ 216 | UpgradeLog*.XML 217 | UpgradeLog*.htm 218 | 219 | # SQL Server files 220 | *.mdf 221 | *.ldf 222 | 223 | # Business Intelligence projects 224 | *.rdl.data 225 | *.bim.layout 226 | *.bim_*.settings 227 | 228 | # Microsoft Fakes 229 | FakesAssemblies/ 230 | 231 | # GhostDoc plugin setting file 232 | *.GhostDoc.xml 233 | 234 | # Node.js Tools for Visual Studio 235 | .ntvs_analysis.dat 236 | node_modules/ 237 | 238 | # Typescript v1 declaration files 239 | typings/ 240 | 241 | # Visual Studio 6 build log 242 | *.plg 243 | 244 | # Visual Studio 6 workspace options file 245 | *.opt 246 | 247 | # Visual Studio 6 auto-generated workspace file (contains which files were open etc.) 248 | *.vbw 249 | 250 | # Visual Studio LightSwitch build output 251 | **/*.HTMLClient/GeneratedArtifacts 252 | **/*.DesktopClient/GeneratedArtifacts 253 | **/*.DesktopClient/ModelManifest.xml 254 | **/*.Server/GeneratedArtifacts 255 | **/*.Server/ModelManifest.xml 256 | _Pvt_Extensions 257 | 258 | # Paket dependency manager 259 | .paket/paket.exe 260 | paket-files/ 261 | 262 | # FAKE - F# Make 263 | .fake/ 264 | 265 | # JetBrains Rider 266 | .idea/ 267 | *.sln.iml 268 | 269 | # CodeRush 270 | .cr/ 271 | 272 | # Python Tools for Visual Studio (PTVS) 273 | __pycache__/ 274 | *.pyc 275 | 276 | # Cake - Uncomment if you are using it 277 | # tools/** 278 | # !tools/packages.config 279 | -------------------------------------------------------------------------------- /idCrypt/idCrypt.c: -------------------------------------------------------------------------------- 1 | // idCrypt v1.0 by emoose 2 | // Code licensed under GPL 3.0. 3 | 4 | #include "stdafx.h" 5 | 6 | #include 7 | 8 | #include 9 | #include 10 | #include 11 | 12 | #define NT_SUCCESS(Status) (((NTSTATUS)(Status)) >= 0) 13 | 14 | // static string used during key derivation 15 | const char* keyDeriveStatic = "swapTeam\n"; 16 | 17 | NTSTATUS hash_data(void* pbBuf1, int cbBuf1, void* pbBuf2, int cbBuf2, void* pbBuf3, int cbBuf3, void* pbSecret, int cbSecret, void* output) 18 | { 19 | BCRYPT_ALG_HANDLE hashAlg; 20 | NTSTATUS res = ERROR_SUCCESS; 21 | DWORD unk = 0; 22 | 23 | if (!NT_SUCCESS(res = BCryptOpenAlgorithmProvider(&hashAlg, BCRYPT_SHA256_ALGORITHM, NULL, pbSecret ? BCRYPT_ALG_HANDLE_HMAC_FLAG : 0))) 24 | return res; 25 | 26 | DWORD hashObjectSize = 0; 27 | if (!NT_SUCCESS(res = BCryptGetProperty(hashAlg, BCRYPT_OBJECT_LENGTH, (PBYTE)&hashObjectSize, sizeof(DWORD), &unk, 0))) 28 | return res; 29 | 30 | PBYTE hashObject = (PBYTE)HeapAlloc(GetProcessHeap(), 0, hashObjectSize); 31 | if (!hashObject) 32 | return -1; 33 | 34 | DWORD hashSize = 0; 35 | if (!NT_SUCCESS(res = BCryptGetProperty(hashAlg, BCRYPT_HASH_LENGTH, (PBYTE)&hashSize, sizeof(DWORD), &unk, 0))) 36 | return res; 37 | 38 | BCRYPT_HASH_HANDLE hashHandle; 39 | if (!NT_SUCCESS(res = BCryptCreateHash(hashAlg, &hashHandle, hashObject, hashObjectSize, (PBYTE)pbSecret, cbSecret, 0))) 40 | return res; 41 | 42 | if(pbBuf1) 43 | if (!NT_SUCCESS(res = BCryptHashData(hashHandle, (PBYTE)pbBuf1, cbBuf1, 0))) 44 | return res; 45 | 46 | if (pbBuf2) 47 | if (!NT_SUCCESS(res = BCryptHashData(hashHandle, (PBYTE)pbBuf2, cbBuf2, 0))) 48 | return res; 49 | 50 | if (pbBuf3) 51 | if (!NT_SUCCESS(res = BCryptHashData(hashHandle, (PBYTE)pbBuf3, cbBuf3, 0))) 52 | return res; 53 | 54 | res = BCryptFinishHash(hashHandle, (PBYTE)output, hashSize, 0); 55 | BCryptCloseAlgorithmProvider(hashAlg, 0); 56 | 57 | HeapFree(GetProcessHeap(), 0, hashObject); 58 | 59 | return res; 60 | } 61 | 62 | NTSTATUS crypt_data(bool decrypt, void* pbInput, int cbInput, void* pbEncKey, int cbEncKey, void* pbIV, int cbIV, void* pbOutput, ULONG* cbOutput) 63 | { 64 | BCRYPT_ALG_HANDLE hashAlg; 65 | NTSTATUS res = ERROR_SUCCESS; 66 | DWORD unk = 0; 67 | 68 | if (!NT_SUCCESS(res = BCryptOpenAlgorithmProvider(&hashAlg, BCRYPT_AES_ALGORITHM, NULL, 0))) 69 | return res; 70 | 71 | DWORD blockSize = 0; 72 | if (!NT_SUCCESS(res = BCryptGetProperty(hashAlg, BCRYPT_BLOCK_LENGTH, (PBYTE)&blockSize, sizeof(DWORD), &unk, 0))) 73 | return res; 74 | 75 | BCRYPT_KEY_HANDLE hKey = NULL; 76 | DWORD keySize = 0; 77 | if (!NT_SUCCESS(res = BCryptGetProperty(hashAlg, BCRYPT_OBJECT_LENGTH, (PBYTE)&keySize, sizeof(DWORD), &unk, 0))) 78 | return res; 79 | 80 | BYTE* key = (BYTE*)malloc(keySize); 81 | 82 | if (!NT_SUCCESS(res = BCryptGenerateSymmetricKey(hashAlg, &hKey, key, keySize, (PBYTE)pbEncKey, cbEncKey, 0))) 83 | return res; 84 | 85 | if(decrypt) 86 | res = BCryptDecrypt(hKey, (PBYTE)pbInput, cbInput, 0, (PBYTE)pbIV, cbIV, (PBYTE)pbOutput, *cbOutput, cbOutput, BCRYPT_BLOCK_PADDING); 87 | else 88 | res = BCryptEncrypt(hKey, (PBYTE)pbInput, cbInput, 0, (PBYTE)pbIV, cbIV, (PBYTE)pbOutput, *cbOutput, cbOutput, BCRYPT_BLOCK_PADDING); 89 | 90 | BCryptDestroyKey(hKey); 91 | BCryptCloseAlgorithmProvider(hashAlg, 0); 92 | 93 | free(key); 94 | 95 | return res; 96 | } 97 | 98 | int main(int argc, char *argv[]) 99 | { 100 | printf("idCrypt v1.0 - by emoose\n\n"); 101 | 102 | if (argc < 3) 103 | { 104 | printf("Usage:\n\tidCrypt.exe \n\n"); 105 | printf("Example:\n\tidCrypt.exe D:\\english.bfile strings/english.lang\n\n"); 106 | printf("If a .bfile is supplied it'll be decrypted to .dec\n"); 107 | printf("Otherwise the file will be encrypted to .bfile\n\n"); 108 | printf("You _must_ use the correct internal filepath for decryption to succeed!\n"); 109 | return 1; 110 | } 111 | bool decrypt = false; 112 | 113 | char* filePath = argv[1]; 114 | char* internalPath = argv[2]; 115 | char* dot = strrchr(filePath, '.'); 116 | 117 | // copy extension and make it lowercase, if it's a .bfile we'll switch to decryption mode 118 | if (dot) 119 | { 120 | char lowerExt[256]; 121 | strcpy_s(lowerExt, 256, dot); 122 | 123 | for (int i = 0; lowerExt[i]; i++) 124 | lowerExt[i] = tolower(lowerExt[i]); 125 | 126 | decrypt = !strcmp(lowerExt, ".bfile") || !strcmp(lowerExt, ".bfile;binaryfile"); 127 | } 128 | 129 | char destPath[256]; 130 | sprintf_s(destPath, 256, "%s.%s", filePath, decrypt ? "dec" : "bfile"); 131 | 132 | FILE* file; 133 | int res = 0; 134 | if (res = fopen_s(&file, filePath, "rb") != 0) 135 | { 136 | printf("Failed to open %s for reading (error %d)\n", filePath, res); 137 | return 2; 138 | } 139 | 140 | fseek(file, 0, SEEK_END); 141 | long size = ftell(file); 142 | fseek(file, 0, SEEK_SET); 143 | 144 | BYTE* fileData = (BYTE*)malloc(size); 145 | fread(fileData, 1, size, file); 146 | fclose(file); 147 | 148 | BYTE fileSalt[0xC]; 149 | if (decrypt) 150 | memcpy(fileSalt, fileData, 0xC); 151 | else 152 | BCryptGenRandom(NULL, (PBYTE)fileSalt, 0xC, BCRYPT_USE_SYSTEM_PREFERRED_RNG); 153 | 154 | BYTE encKey[0x20]; 155 | res = hash_data((void*)fileSalt, 0xC, (void*)keyDeriveStatic, 0xA, internalPath, strlen(internalPath), NULL, 0, encKey); 156 | if (!NT_SUCCESS(res)) 157 | { 158 | printf("Failed to derive encryption key (error 0x%x)\n", res); 159 | return 3; 160 | } 161 | 162 | BYTE fileIV[0x10]; 163 | BYTE fileIV_backup[0x10]; 164 | if (decrypt) 165 | memcpy(fileIV, fileData + 0xC, 0x10); 166 | else 167 | BCryptGenRandom(NULL, (PBYTE)fileIV, 0x10, BCRYPT_USE_SYSTEM_PREFERRED_RNG); 168 | 169 | memcpy(fileIV_backup, fileIV, 0x10); // make a backup of IV because BCrypt can overwrite it (and make you waste an hour debugging in the process...) 170 | 171 | BYTE* fileText = fileData; 172 | long fileTextSize = size; 173 | 174 | BYTE hmac[0x20]; 175 | if (decrypt) // change fileText pointer + verify HMAC if we're decrypting 176 | { 177 | fileText = fileData + 0x1C; 178 | fileTextSize = size - 0x1C - 0x20; 179 | 180 | BYTE* fileHmac = fileData + (size - 0x20); 181 | 182 | res = hash_data((void*)fileSalt, 0xC, (void*)fileIV, 0x10, (void*)fileText, fileTextSize, encKey, 0x20, hmac); 183 | if (!NT_SUCCESS(res)) 184 | { 185 | printf("Failed to create HMAC hash of ciphertext (error 0x%x)\n", res); 186 | return 4; 187 | } 188 | 189 | if (memcmp(hmac, fileHmac, 0x20)) 190 | printf("Warning: HMAC hash check failed, decrypted data might not be valid!\n"); 191 | } 192 | 193 | // call crypt_data with NULL buffer to get the buffer size 194 | ULONG cryptedTextSize = 0; 195 | res = crypt_data(decrypt, fileText, fileTextSize, encKey, 0x10, fileIV, 0x10, 0, &cryptedTextSize); 196 | if (!NT_SUCCESS(res)) 197 | { 198 | printf("Failed to %s data (error 0x%x)\n", decrypt ? "decrypt" : "encrypt", res); 199 | printf("Did you use the correct internal file name?\n"); 200 | return 5; 201 | } 202 | 203 | memcpy(fileIV, fileIV_backup, 0x10); // BCryptEncrypt overwrites the IV, so restore it from backup 204 | 205 | // now allocate that buffer and call crypt_data for realsies 206 | BYTE* cryptedText = (BYTE*)malloc(cryptedTextSize); 207 | res = crypt_data(decrypt, fileText, fileTextSize, encKey, 0x10, fileIV, 0x10, cryptedText, &cryptedTextSize); 208 | if (!NT_SUCCESS(res)) 209 | { 210 | printf("Failed to %s data (error 0x%x)\n", decrypt ? "decrypt" : "encrypt", res); 211 | printf("Did you use the correct internal file name?\n"); 212 | return 5; 213 | } 214 | 215 | memcpy(fileIV, fileIV_backup, 0x10); // BCryptEncrypt overwrites the IV, so restore it from backup 216 | 217 | free(fileData); 218 | 219 | res = 0; 220 | if (res = fopen_s(&file, destPath, "wb+") != 0) 221 | { 222 | printf("Failed to open %s for writing (error %d)\n", destPath, res); 223 | return 6; 224 | } 225 | 226 | if(decrypt) 227 | fwrite(cryptedText, 1, cryptedTextSize, file); 228 | else 229 | { 230 | fwrite(fileSalt, 1, 0xC, file); 231 | fwrite(fileIV, 1, 0x10, file); 232 | fwrite(cryptedText, 1, cryptedTextSize, file); 233 | 234 | res = hash_data((void*)fileSalt, 0xC, (void*)fileIV, 0x10, (void*)cryptedText, cryptedTextSize, encKey, 0x20, hmac); 235 | if (!NT_SUCCESS(res)) 236 | { 237 | printf("Failed to create HMAC hash of ciphertext (error 0x%x)\n", res); 238 | return 7; 239 | } 240 | fwrite(hmac, 1, 0x20, file); 241 | } 242 | 243 | fclose(file); 244 | 245 | //free(cryptedText); 246 | // ^ causes an error, wtf? 247 | 248 | printf("%s succeeded! Wrote to %s\n", decrypt ? "Decryption" : "Encryption", destPath); 249 | return 0; 250 | } -------------------------------------------------------------------------------- /DOOMModLoader/Program.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.IO; 4 | using System.Diagnostics; 5 | using DOOMExtract; 6 | using ICSharpCode.SharpZipLib.Core; 7 | using ICSharpCode.SharpZipLib.Zip; 8 | 9 | namespace DOOMModLoader 10 | { 11 | class Program 12 | { 13 | static void PrintUsage() 14 | { 15 | Console.WriteLine("Usage: DOOMModLoader.exe [-mp/-snap] [-vulkan] [-moddir ]"); 16 | Console.WriteLine(); 17 | Console.WriteLine(" -mp\t\t\tLoad MP gamemode"); 18 | Console.WriteLine(" -snap\t\t\tLoad SnapMap gamemode"); 19 | Console.WriteLine(" -vulkan\t\tLoad DOOMx64vk instead of DOOMx64"); 20 | Console.WriteLine(" -moddir \tUse mods from given path"); 21 | Console.WriteLine(" -help\t\t\tDisplay this text"); 22 | } 23 | 24 | static void Main(string[] args) 25 | { 26 | string gameMode = "1"; 27 | string resourcePrefix = ""; 28 | string modDir = "mods"; 29 | string exeName = "DOOMx64.exe"; 30 | 31 | Console.WriteLine("DOOMModLoader 0.2 by infogram - https://github.com/emoose/DOOMExtract"); 32 | Console.WriteLine(); 33 | 34 | for (int i = 0; i < args.Length; i++) 35 | { 36 | var arg = args[i]; 37 | switch (arg) 38 | { 39 | case "-mp": 40 | case "/mp": 41 | gameMode = "2"; 42 | resourcePrefix = "mp_"; 43 | break; 44 | case "-snap": 45 | case "/snap": 46 | gameMode = "3"; 47 | resourcePrefix = "snap_"; 48 | break; 49 | case "-moddir": 50 | case "/moddir": 51 | if ((i + 1) < args.Length) 52 | modDir = args[i + 1]; 53 | break; 54 | case "-vulkan": 55 | case "/vulkan": 56 | exeName = "DOOMx64vk.exe"; 57 | break; 58 | case "-help": 59 | case "/help": 60 | PrintUsage(); 61 | return; 62 | } 63 | } 64 | 65 | if (!File.Exists(exeName)) 66 | { 67 | Console.WriteLine($"Error: failed to find {exeName} in current directory!"); 68 | PressKeyPrompt(); 69 | return; 70 | } 71 | 72 | if (!Directory.Exists(modDir)) 73 | Directory.CreateDirectory(modDir); 74 | 75 | bool hasMods = false; 76 | 77 | // create folder to extract mods into 78 | var extractedPath = Path.GetTempFileName(); 79 | File.Delete(extractedPath); 80 | Directory.CreateDirectory(extractedPath); 81 | 82 | // copy loose mods from modDir into extract path 83 | Console.WriteLine("Extracting/copying mods into " + extractedPath); 84 | 85 | var modFiles = Directory.GetFiles(modDir); 86 | var modDirs = Directory.GetDirectories(modDir); 87 | 88 | var zips = new List(); 89 | foreach(var file in modFiles) 90 | { 91 | hasMods = true; 92 | 93 | if (Path.GetExtension(file).ToLower() == ".zip") 94 | zips.Add(file); // don't copy zips, we'll extract them later instead 95 | else 96 | { 97 | File.Copy(file, Path.Combine(extractedPath, Path.GetFileName(file))); 98 | } 99 | } 100 | foreach (var dir in modDirs) 101 | { 102 | hasMods = true; 103 | CloneDirectory(dir, Path.Combine(extractedPath, Path.GetFileName(dir))); 104 | } 105 | 106 | // extract mod zips 107 | var modInfoPath = Path.Combine(extractedPath, "modinfo.txt"); 108 | var fileIdsPath = Path.Combine(extractedPath, "fileIds.txt"); 109 | var fileIds = ""; 110 | if (File.Exists(fileIdsPath)) 111 | { 112 | fileIds = File.ReadAllText(fileIdsPath); 113 | File.Delete(fileIdsPath); 114 | } 115 | 116 | foreach (var zipfile in zips) 117 | { 118 | var modInfo = Path.GetFileName(zipfile); 119 | Console.WriteLine("Extracting " + modInfo); 120 | ExtractZipFile(zipfile, "", extractedPath); 121 | if (File.Exists(modInfoPath)) 122 | { 123 | modInfo = File.ReadAllText(modInfoPath); 124 | if (String.IsNullOrEmpty(modInfo)) 125 | modInfo = Path.GetFileName(zipfile); 126 | 127 | File.Delete(modInfoPath); // delete so no conflicts 128 | } 129 | if (File.Exists(fileIdsPath)) 130 | { 131 | // todo: make this use a dictionary instead, so we can detect conflicts 132 | var modsFileIds = File.ReadAllText(fileIdsPath); 133 | if (!String.IsNullOrEmpty(fileIds)) 134 | fileIds += Environment.NewLine; 135 | fileIds += modsFileIds; 136 | 137 | File.Delete(fileIdsPath); 138 | } 139 | 140 | Console.WriteLine("Extracted " + modInfo); 141 | } 142 | if (!String.IsNullOrEmpty(fileIds)) 143 | File.WriteAllText(fileIdsPath, fileIds); 144 | 145 | // mod patch creation 146 | var patchFilter = $"{resourcePrefix}gameresources_*.pindex"; 147 | var patches = Directory.GetFiles("base", patchFilter); 148 | var latestPatch = String.Empty; 149 | var latestPfi = 0; 150 | foreach (var patch in patches) 151 | { 152 | if (File.Exists(patch + ".custom")) 153 | continue; // patch is one made by us 154 | 155 | var namesp = Path.GetFileNameWithoutExtension(patch).Split('_'); 156 | var pnum = int.Parse(namesp[namesp.Length - 1]); 157 | if (pnum > latestPfi) 158 | { 159 | latestPatch = patch; 160 | latestPfi = pnum; 161 | } 162 | } 163 | if (string.IsNullOrEmpty(latestPatch)) 164 | { 165 | Console.WriteLine("Failed to find latest patch file in base folder!"); 166 | Console.WriteLine($"Search filter: {patchFilter}"); 167 | PressKeyPrompt(); 168 | return; 169 | } 170 | 171 | var customPfi = latestPfi + 1; 172 | 173 | // have to find where to copy the index, easiest way is to make a DOOMResourceIndex instance (but not load it) 174 | var index = new DOOMResourceIndex(latestPatch); 175 | var resPath = index.ResourceFilePath(customPfi); 176 | var destPath = Path.ChangeExtension(resPath, ".pindex"); 177 | 178 | // delete existing custom patch 179 | if (File.Exists(destPath)) 180 | File.Delete(destPath); 181 | 182 | if (File.Exists(resPath)) 183 | File.Delete(resPath); 184 | 185 | // if we have mods, create a custom patch out of them 186 | if (hasMods) 187 | { 188 | File.Copy(latestPatch, destPath); 189 | 190 | Console.WriteLine($"Creating custom patch... (patch base: {Path.GetFileName(latestPatch)})"); 191 | 192 | index = new DOOMResourceIndex(destPath); 193 | if (!index.Load()) 194 | { 195 | Console.WriteLine("Failed to load custom patch " + destPath); 196 | PressKeyPrompt(); 197 | return; 198 | } 199 | index.PatchFileNumber = (byte)customPfi; 200 | 201 | index.Rebuild(index.ResourceFilePath(customPfi), extractedPath, true); 202 | index.Close(); 203 | 204 | File.WriteAllText(destPath + ".custom", "DOOMModLoader token file, this tells ModLoader that this is a custom patch file, please don't remove!"); 205 | 206 | Console.WriteLine($"Custom patch {Path.GetFileName(destPath)} created."); 207 | } 208 | 209 | // cleanup 210 | Directory.Delete(extractedPath, true); 211 | 212 | Console.WriteLine("Launching game!"); 213 | 214 | var proc = new Process(); 215 | proc.StartInfo.FileName = exeName; 216 | proc.StartInfo.Arguments = $"+com_gameMode {gameMode} +com_restarted 1 +devMode_enable 1"; 217 | proc.Start(); 218 | } 219 | 220 | static void PressKeyPrompt() 221 | { 222 | Console.WriteLine("Press any key to exit..."); 223 | Console.ReadKey(); 224 | } 225 | 226 | static void CloneDirectory(string src, string dest) 227 | { 228 | if (!Directory.Exists(dest)) 229 | Directory.CreateDirectory(dest); 230 | 231 | foreach (var directory in Directory.GetDirectories(src)) 232 | { 233 | string dirName = Path.GetFileName(directory); 234 | if (!Directory.Exists(Path.Combine(dest, dirName))) 235 | { 236 | Directory.CreateDirectory(Path.Combine(dest, dirName)); 237 | } 238 | CloneDirectory(directory, Path.Combine(dest, dirName)); 239 | } 240 | 241 | foreach (var file in Directory.GetFiles(src)) 242 | { 243 | File.Copy(file, Path.Combine(dest, Path.GetFileName(file))); 244 | } 245 | } 246 | 247 | static void ExtractZipFile(string archiveFilenameIn, string password, string outFolder) 248 | { 249 | ZipFile zf = null; 250 | try 251 | { 252 | FileStream fs = File.OpenRead(archiveFilenameIn); 253 | zf = new ZipFile(fs); 254 | if (!string.IsNullOrEmpty(password)) 255 | zf.Password = password; // AES encrypted entries are handled automatically 256 | 257 | foreach (ZipEntry zipEntry in zf) 258 | { 259 | if (!zipEntry.IsFile) 260 | continue; // Ignore directories 261 | 262 | // to remove the folder from the entry:- entryFileName = Path.GetFileName(entryFileName); 263 | // Optionally match entrynames against a selection list here to skip as desired. 264 | // The unpacked length is available in the zipEntry.Size property. 265 | 266 | byte[] buffer = new byte[4096]; // 4K is optimum 267 | Stream zipStream = zf.GetInputStream(zipEntry); 268 | 269 | // Manipulate the output filename here as desired. 270 | string fullZipToPath = Path.Combine(outFolder, zipEntry.Name); 271 | string directoryName = Path.GetDirectoryName(fullZipToPath); 272 | if (directoryName.Length > 0) 273 | Directory.CreateDirectory(directoryName); 274 | 275 | if (File.Exists(fullZipToPath)) 276 | File.Delete(fullZipToPath); // TODO: warn user about mod conflict! 277 | 278 | // Unzip file in buffered chunks. This is just as fast as unpacking to a buffer the full size 279 | // of the file, but does not waste memory. 280 | // The "using" will close the stream even if an exception occurs. 281 | 282 | using (FileStream streamWriter = File.Create(fullZipToPath)) 283 | { 284 | StreamUtils.Copy(zipStream, streamWriter, buffer); 285 | } 286 | } 287 | } 288 | finally 289 | { 290 | if (zf != null) 291 | { 292 | zf.IsStreamOwner = true; // Makes close also shut the underlying stream 293 | zf.Close(); // Ensure we release resources 294 | } 295 | } 296 | } 297 | } 298 | } 299 | -------------------------------------------------------------------------------- /DOOMExtract/Program.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.IO; 5 | using System.Text; 6 | 7 | namespace DOOMExtract 8 | { 9 | class Program 10 | { 11 | static void PrintUsage() 12 | { 13 | Console.WriteLine("Usage:"); 14 | Console.WriteLine("Extraction: DOOMExtract.exe [pathToIndexFile] "); 15 | Console.WriteLine(" If destFolder isn't specified a folder will be created next to the index/pindex file."); 16 | Console.WriteLine(" Files with fileType != \"file\" will have the fileType appended to the filename."); 17 | Console.WriteLine(" eg. allowoverlays.decl;renderParm for fileType \"renderParm\""); 18 | Console.WriteLine(" A list of each files ID will be written to [destFolder]\\fileIds.txt"); 19 | Console.WriteLine(); 20 | Console.WriteLine("Repacking: DOOMExtract.exe [pathToIndexFile] --repack [repackFolder]"); 21 | Console.WriteLine(" Will repack the resources with the files in the repack folder."); 22 | Console.WriteLine(" Files that don't already exist in the resources will be added."); 23 | Console.WriteLine(" To set a new files fileType append the fileType to its filename."); 24 | Console.WriteLine(" eg. allowoverlays.decl;renderParm for fileType \"renderParm\""); 25 | Console.WriteLine(" To set/change a files ID, add a line for it in [repackFolder]\\filesIds.txt"); 26 | Console.WriteLine(" of the format [full file path]=[file id]"); 27 | Console.WriteLine(" eg. \"example\\test.txt=1337\""); 28 | Console.WriteLine(" (Note that you should only rebuild the latest patch index file,"); 29 | Console.WriteLine(" as patches rely on the data in earlier files!)"); 30 | Console.WriteLine(); 31 | Console.WriteLine("Patch creation: DOOMExtract.exe [pathToLatestPatchIndex] --createPatch [patchContentsFolder]"); 32 | Console.WriteLine(" Allows you to create your own patch files."); 33 | Console.WriteLine(" Works like repacking above, but the resulting patch files will"); 34 | Console.WriteLine(" only contain the files you've added/changed."); 35 | Console.WriteLine(" Make sure to point it to the highest-numbered .pindex file!"); 36 | Console.WriteLine(" Once completed a new .patch/.pindex file pair should be created."); 37 | Console.WriteLine(); 38 | Console.WriteLine("Deleting files: DOOMExtract.exe [pathToIndexFile] --delete [file1] ..."); 39 | Console.WriteLine(" Will delete files from the resources package. Full filepaths should be specified."); 40 | Console.WriteLine(" If a file isn't found in the package a warning will be given."); 41 | Console.WriteLine(" This should only be used on the latest patch file, as modifying"); 42 | Console.WriteLine(" earlier patch files may break later ones."); 43 | } 44 | static void Main(string[] args) 45 | { 46 | Console.WriteLine("DOOMExtract 1.7 by infogram - https://github.com/emoose/DOOMExtract"); 47 | Console.WriteLine(); 48 | if (args.Length <= 0) 49 | { 50 | PrintUsage(); 51 | return; 52 | } 53 | 54 | string indexFilePath = args[0]; 55 | string destFolder = Path.GetDirectoryName(indexFilePath); 56 | destFolder = Path.Combine(destFolder, Path.GetFileNameWithoutExtension(indexFilePath)); 57 | 58 | bool isRepacking = false; 59 | bool isDeleting = false; 60 | bool isCreatingPatch = false; 61 | bool quietMode = false; 62 | 63 | foreach (var arg in args) 64 | if (arg == "--quiet") 65 | quietMode = true; 66 | 67 | if (args.Length >= 2) 68 | { 69 | if(args[1] == "--delete") // deleting 70 | { 71 | isDeleting = true; 72 | } 73 | else if(args[1] == "--repack" || args[1] == "--createPatch") // repacking 74 | { 75 | isRepacking = true; 76 | isCreatingPatch = args[1] == "--createPatch"; 77 | 78 | if (args.Length <= 2) // missing the repack folder arg 79 | { 80 | PrintUsage(); 81 | return; 82 | } 83 | 84 | destFolder = Path.GetFullPath(args[2]); // use destFolder as the folder where repack files are 85 | if(!Directory.Exists(destFolder)) 86 | { 87 | Console.WriteLine((isCreatingPatch ? "Patch" : "Repack") + $" folder \"{destFolder}\" doesn't exist!"); 88 | return; 89 | } 90 | } 91 | else 92 | destFolder = Path.GetFullPath(args[1]); 93 | } 94 | 95 | Console.WriteLine($"Loading {indexFilePath}..."); 96 | var index = new DOOMResourceIndex(indexFilePath); 97 | if(!index.Load()) 98 | { 99 | Console.WriteLine("Failed to load index file for some reason, is it a valid DOOM index file?"); 100 | return; 101 | } 102 | 103 | Console.WriteLine($"Index loaded ({index.Entries.Count} files)" + (!quietMode ? ", data file contents:" : "")); 104 | 105 | if (!quietMode) 106 | { 107 | var pfis = new Dictionary(); 108 | 109 | foreach (var entry in index.Entries) 110 | { 111 | if (!pfis.ContainsKey(entry.PatchFileNumber)) 112 | pfis.Add(entry.PatchFileNumber, 0); 113 | pfis[entry.PatchFileNumber]++; 114 | } 115 | 116 | var pfiKeys = pfis.Keys.ToList(); 117 | pfiKeys.Sort(); 118 | 119 | int total = 0; 120 | foreach (var key in pfiKeys) 121 | { 122 | var resName = Path.GetFileName(index.ResourceFilePath(key)); 123 | Console.WriteLine($" {resName}: {pfis[key]} files"); 124 | total += pfis[key]; 125 | } 126 | 127 | Console.WriteLine(); 128 | } 129 | 130 | if (isCreatingPatch) 131 | { 132 | // clone old index and increment the patch file number 133 | 134 | byte pfi = (byte)(index.PatchFileNumber + 1); 135 | var destPath = Path.ChangeExtension(index.ResourceFilePath(pfi), ".pindex"); 136 | index.Close(); 137 | 138 | if (File.Exists(destPath)) 139 | File.Delete(destPath); // !!!! 140 | 141 | File.Copy(indexFilePath, destPath); 142 | indexFilePath = destPath; 143 | 144 | index = new DOOMResourceIndex(destPath); 145 | if(!index.Load()) 146 | { 147 | Console.WriteLine("Copied patch file failed to load? (this should never happen!)"); 148 | return; 149 | } 150 | index.PatchFileNumber = pfi; 151 | } 152 | 153 | if(isRepacking) 154 | { 155 | // REPACK (and patch creation) MODE!!! 156 | 157 | var resFile = index.ResourceFilePath(index.PatchFileNumber); 158 | 159 | Console.WriteLine((isCreatingPatch ? "Creating" : "Repacking") + $" {Path.GetFileName(indexFilePath)} from folder {destFolder}..."); 160 | 161 | index.Rebuild(resFile + "_tmp", destFolder, true); 162 | index.Close(); 163 | if (!File.Exists(resFile + "_tmp")) 164 | { 165 | Console.WriteLine("Failed to create new resource data file!"); 166 | return; 167 | } 168 | 169 | if (File.Exists(resFile)) 170 | File.Delete(resFile); 171 | 172 | File.Move(resFile + "_tmp", resFile); 173 | Console.WriteLine(isCreatingPatch ? "Patch file created!" : "Repack complete!"); 174 | return; 175 | } 176 | 177 | if(isDeleting) 178 | { 179 | if(args.Length <= 2) 180 | { 181 | PrintUsage(); 182 | return; 183 | } 184 | 185 | // DELETE MODE!! 186 | int deleted = 0; 187 | for(int i = 2; i < args.Length; i++) 188 | { 189 | var path = args[i].Replace("/", "\\").ToLower(); 190 | 191 | int delIdx = -1; 192 | for(int j = 0; j < index.Entries.Count; j++) 193 | { 194 | if (index.Entries[j].GetFullName().ToLower() == path) 195 | { 196 | delIdx = j; 197 | break; 198 | } 199 | } 200 | 201 | if (delIdx == -1) 202 | Console.WriteLine($"Failed to find file {args[i]} in package."); 203 | else 204 | { 205 | index.Entries.RemoveAt(delIdx); 206 | deleted++; 207 | Console.WriteLine($"Deleted {args[i]}!"); 208 | } 209 | } 210 | 211 | 212 | if (deleted > 0) 213 | { 214 | Console.WriteLine("Repacking/rebuilding resources file..."); 215 | index.Rebuild(index.ResourceFilePath(index.PatchFileNumber) + "_tmp", String.Empty, true); 216 | index.Close(); 217 | File.Delete(index.ResourceFilePath(index.PatchFileNumber)); 218 | File.Move(index.ResourceFilePath(index.PatchFileNumber) + "_tmp", index.ResourceFilePath(index.PatchFileNumber)); 219 | } 220 | Console.WriteLine($"Deleted {deleted} files from resources."); 221 | return; 222 | } 223 | 224 | // EXTRACT MODE! 225 | 226 | if (!Directory.Exists(destFolder)) 227 | Directory.CreateDirectory(destFolder); 228 | 229 | Console.WriteLine("Extracting contents to:"); 230 | Console.WriteLine("\t" + destFolder); 231 | 232 | var fileIds = new List(); 233 | int numExtracted = 0; 234 | int numProcessed = 0; 235 | foreach(var entry in index.Entries) 236 | { 237 | numProcessed++; 238 | if(entry.Size == 0 && entry.CompressedSize == 0) // blank entry? 239 | continue; 240 | 241 | Console.WriteLine($"Extracting {entry.GetFullName()}..."); 242 | Console.WriteLine($" id: {entry.ID}, type: {entry.FileType}, size: {entry.Size} ({entry.CompressedSize} bytes compressed)"); 243 | Console.WriteLine($" source: {Path.GetFileName(index.ResourceFilePath(entry.PatchFileNumber))}"); 244 | 245 | var destFilePath = Path.Combine(destFolder, entry.GetFullName()); 246 | if (entry.FileType != "file") 247 | destFilePath += ";" + entry.FileType; 248 | 249 | var destFileFolder = Path.GetDirectoryName(destFilePath); 250 | 251 | if (!Directory.Exists(destFileFolder)) 252 | Directory.CreateDirectory(destFileFolder); 253 | 254 | using (FileStream fs = File.OpenWrite(destFilePath)) 255 | index.CopyEntryDataToStream(entry, fs); 256 | 257 | Console.WriteLine($"--------------({numProcessed}/{index.Entries.Count})--------------"); 258 | fileIds.Add(entry.GetFullName() + "=" + entry.ID); 259 | numExtracted++; 260 | } 261 | 262 | if (fileIds.Count > 0) 263 | { 264 | var idFile = Path.Combine(destFolder, "fileIds.txt"); 265 | if (File.Exists(idFile)) 266 | File.Delete(idFile); 267 | File.WriteAllLines(idFile, fileIds.ToArray()); 268 | } 269 | 270 | Console.WriteLine($"Extraction complete! Extracted {numExtracted} files."); 271 | } 272 | } 273 | } 274 | -------------------------------------------------------------------------------- /DOOMExtract/EndianIO.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.IO; 3 | 4 | namespace DOOMExtract 5 | { 6 | public class EndianWriter : BinaryWriter 7 | { 8 | private bool _bigEndian; 9 | 10 | internal EndianIO _io; 11 | 12 | public bool BigEndian 13 | { 14 | get { return _io != null ? _io.BigEndian : _bigEndian; } 15 | set 16 | { 17 | if (_io != null) { _io.BigEndian = value; } 18 | else 19 | { 20 | _bigEndian = value; 21 | } 22 | } 23 | } 24 | 25 | internal EndianWriter(EndianIO _io) 26 | : base(_io.Stream) 27 | { 28 | this._io = _io; 29 | } 30 | 31 | public EndianWriter(Stream stream) 32 | : base(stream) 33 | { 34 | 35 | } 36 | 37 | public void SeekTo(int offset) 38 | { 39 | SeekTo(offset, SeekOrigin.Begin); 40 | } 41 | 42 | public void SeekTo(long offset) 43 | { 44 | SeekTo((int)offset, SeekOrigin.Begin); 45 | } 46 | 47 | public void SeekTo(uint offset) 48 | { 49 | SeekTo((int)offset, SeekOrigin.Begin); 50 | } 51 | 52 | public void SeekTo(int offset, SeekOrigin seekOrigin) 53 | { 54 | BaseStream.Seek(offset, seekOrigin); 55 | } 56 | 57 | public override void Write(string value) 58 | { 59 | int length = value.Length; 60 | for (int i = 0; i < length; i++) 61 | { 62 | byte num3 = (byte)value[i]; 63 | Write(num3); 64 | } 65 | } 66 | 67 | public override void Write(double value) 68 | { 69 | byte[] bytes = BitConverter.GetBytes(value); 70 | if (BigEndian) 71 | { 72 | Array.Reverse(bytes); 73 | } 74 | base.Write(bytes); 75 | } 76 | 77 | public override void Write(short value) 78 | { 79 | byte[] bytes = BitConverter.GetBytes(value); 80 | if (BigEndian) 81 | { 82 | Array.Reverse(bytes); 83 | } 84 | base.Write(bytes); 85 | } 86 | 87 | public override void Write(int value) 88 | { 89 | byte[] bytes = BitConverter.GetBytes(value); 90 | if (BigEndian) 91 | { 92 | Array.Reverse(bytes); 93 | } 94 | base.Write(bytes); 95 | } 96 | 97 | public void WriteInt24(int value) 98 | { 99 | byte[] bytes = BitConverter.GetBytes(value); 100 | byte num = bytes[0]; 101 | byte num2 = bytes[1]; 102 | byte num3 = bytes[2]; 103 | byte[] array = new[] { num, num2, num3 }; 104 | if (BigEndian) 105 | { 106 | Array.Reverse(array); 107 | } 108 | Write(array); 109 | } 110 | 111 | public override void Write(long value) 112 | { 113 | byte[] bytes = BitConverter.GetBytes(value); 114 | if (BigEndian) 115 | { 116 | Array.Reverse(bytes); 117 | } 118 | base.Write(bytes); 119 | } 120 | 121 | public override void Write(float value) 122 | { 123 | byte[] bytes = BitConverter.GetBytes(value); 124 | if (BigEndian) 125 | { 126 | Array.Reverse(bytes); 127 | } 128 | base.Write(bytes); 129 | } 130 | public override void Write(ushort value) 131 | { 132 | byte[] bytes = BitConverter.GetBytes(value); 133 | if (BigEndian) 134 | { 135 | Array.Reverse(bytes); 136 | } 137 | base.Write(bytes); 138 | } 139 | 140 | public override void Write(uint value) 141 | { 142 | byte[] bytes = BitConverter.GetBytes(value); 143 | if (BigEndian) 144 | { 145 | Array.Reverse(bytes); 146 | } 147 | base.Write(bytes); 148 | } 149 | 150 | public override void Write(ulong value) 151 | { 152 | byte[] bytes = BitConverter.GetBytes(value); 153 | if (BigEndian) 154 | { 155 | Array.Reverse(bytes); 156 | } 157 | base.Write(bytes); 158 | } 159 | 160 | public void WriteAsciiString(string value, int length) 161 | { 162 | int length1 = value.Length; 163 | for (int i = 0; i < length1; i++) 164 | { 165 | if (i > length) 166 | { 167 | break; 168 | } 169 | byte num3 = (byte)value[i]; 170 | Write(num3); 171 | } 172 | int num4 = length - length1; 173 | if (num4 > 0) 174 | { 175 | Write(new byte[num4]); 176 | } 177 | } 178 | 179 | public void WriteNullTerminatedUnicodeString(string value) 180 | { 181 | int strLen = value.Length; 182 | for (int x = 0; x < strLen; x++) 183 | { 184 | ushort val = value[x]; 185 | Write(val); 186 | } 187 | Write((ushort)0); 188 | } 189 | 190 | public void WriteNullTerminatedAsciiString(string value) 191 | { 192 | Write(value.ToCharArray()); 193 | Write((byte)0); 194 | } 195 | 196 | public void WriteUnicodeString(string value, int length) 197 | { 198 | int length1 = value.Length; 199 | for (int i = 0; i < length1; i++) 200 | { 201 | if (i > length) 202 | { 203 | break; 204 | } 205 | ushort num3 = value[i]; 206 | Write(num3); 207 | } 208 | int num4 = (length - length1) * 2; 209 | if (num4 > 0) 210 | { 211 | Write(new byte[num4]); 212 | } 213 | } 214 | } 215 | 216 | public class EndianReader : BinaryReader 217 | { 218 | internal EndianIO _io; 219 | 220 | public bool BigEndian 221 | { 222 | get { return _io.BigEndian; } 223 | set { _io.BigEndian = value; } 224 | } 225 | 226 | internal EndianReader(EndianIO io) 227 | : base(io.Stream) 228 | { 229 | _io = io; 230 | } 231 | 232 | public string ReadAsciiString(int length) 233 | { 234 | string str = ""; 235 | int num = 0; 236 | for (int i = 0; i < length; i++) 237 | { 238 | char ch = ReadChar(); 239 | num++; 240 | if (ch == '\0') 241 | { 242 | break; 243 | } 244 | str = str + ch; 245 | } 246 | int num3 = length - num; 247 | BaseStream.Seek(num3, SeekOrigin.Current); 248 | return str; 249 | } 250 | 251 | public override double ReadDouble() 252 | { 253 | byte[] array = base.ReadBytes(4); 254 | if (BigEndian) 255 | { 256 | Array.Reverse(array); 257 | } 258 | return BitConverter.ToDouble(array, 0); 259 | } 260 | 261 | public override short ReadInt16() 262 | { 263 | byte[] array = base.ReadBytes(2); 264 | if (BigEndian) 265 | { 266 | Array.Reverse(array); 267 | } 268 | return BitConverter.ToInt16(array, 0); 269 | } 270 | 271 | public int ReadInt24() 272 | { 273 | byte[] sourceArray = base.ReadBytes(3); 274 | byte[] destinationArray = new byte[4]; 275 | Array.Copy(sourceArray, 0, destinationArray, 0, 3); 276 | if (BigEndian) 277 | { 278 | Array.Reverse(destinationArray); 279 | } 280 | return BitConverter.ToInt32(destinationArray, 0); 281 | } 282 | 283 | public override int ReadInt32() 284 | { 285 | byte[] array = base.ReadBytes(4); 286 | if (BigEndian) 287 | { 288 | Array.Reverse(array); 289 | } 290 | return BitConverter.ToInt32(array, 0); 291 | } 292 | 293 | public override long ReadInt64() 294 | { 295 | byte[] array = base.ReadBytes(8); 296 | if (BigEndian) 297 | { 298 | Array.Reverse(array); 299 | } 300 | return BitConverter.ToInt64(array, 0); 301 | } 302 | 303 | public string ReadNullTerminatedAsciiString() 304 | { 305 | string newString = string.Empty; 306 | while (true) 307 | { 308 | byte tempChar = ReadByte(); 309 | if (tempChar != 0) 310 | newString += (char)tempChar; 311 | else 312 | break; 313 | } 314 | return newString; 315 | } 316 | 317 | public string ReadNullTerminatedUnicodeString() 318 | { 319 | string newString = string.Empty; 320 | while (true) 321 | { 322 | ushort tempChar = ReadUInt16(); 323 | if (tempChar != 0) 324 | newString += (char)tempChar; 325 | else 326 | break; 327 | } 328 | return newString; 329 | } 330 | 331 | public override float ReadSingle() 332 | { 333 | byte[] array = base.ReadBytes(4); 334 | if (BigEndian) 335 | { 336 | Array.Reverse(array); 337 | } 338 | return BitConverter.ToSingle(array, 0); 339 | } 340 | 341 | public string ReadString(int length) 342 | { 343 | return ReadAsciiString(length); 344 | } 345 | 346 | public override ushort ReadUInt16() 347 | { 348 | byte[] array = base.ReadBytes(2); 349 | if (BigEndian) 350 | { 351 | Array.Reverse(array); 352 | } 353 | return BitConverter.ToUInt16(array, 0); 354 | } 355 | 356 | public override uint ReadUInt32() 357 | { 358 | byte[] array = base.ReadBytes(4); 359 | if (BigEndian) 360 | { 361 | Array.Reverse(array); 362 | } 363 | return BitConverter.ToUInt32(array, 0); 364 | } 365 | 366 | public override ulong ReadUInt64() 367 | { 368 | byte[] array = base.ReadBytes(8); 369 | if (BigEndian) 370 | { 371 | Array.Reverse(array); 372 | } 373 | return BitConverter.ToUInt64(array, 0); 374 | } 375 | 376 | public string ReadUnicodeString(int length) 377 | { 378 | string str = ""; 379 | int num = 0; 380 | for (int i = 0; i < length; i++) 381 | { 382 | char ch = (char)ReadUInt16(); 383 | num++; 384 | if (ch == '\0') 385 | { 386 | break; 387 | } 388 | str = str + ch; 389 | } 390 | int num3 = (length - num) * 2; 391 | BaseStream.Seek(num3, SeekOrigin.Current); 392 | return str; 393 | } 394 | 395 | public void SeekTo(int offset) 396 | { 397 | SeekTo(offset, SeekOrigin.Begin); 398 | } 399 | 400 | public void SeekTo(long offset) 401 | { 402 | SeekTo((int)offset, SeekOrigin.Begin); 403 | } 404 | 405 | public override string ReadString() 406 | { 407 | return ReadNullTerminatedAsciiString(); 408 | } 409 | 410 | public void SeekTo(uint offset) 411 | { 412 | SeekTo((int)offset, SeekOrigin.Begin); 413 | } 414 | 415 | public void SeekTo(int offset, SeekOrigin seekOrigin) 416 | { 417 | BaseStream.Seek(offset, seekOrigin); 418 | } 419 | } 420 | 421 | public class EndianIO 422 | { 423 | public EndianReader Reader; 424 | public EndianWriter Writer; 425 | 426 | public string FilePath; 427 | 428 | public Stream Stream; 429 | 430 | public bool BigEndian; 431 | 432 | public long Length 433 | { 434 | get 435 | { 436 | return Stream.Length; 437 | } 438 | } 439 | 440 | public EndianIO(string filePath, FileMode fileMode, bool bigEndian) 441 | { 442 | Stream = new FileStream(filePath, fileMode); 443 | FilePath = filePath; 444 | BigEndian = bigEndian; 445 | Open(); 446 | } 447 | 448 | public EndianIO(Stream stream, bool bigEndian) 449 | { 450 | Stream = stream; 451 | BigEndian = bigEndian; 452 | Open(); 453 | } 454 | 455 | public EndianIO(string filePath, FileMode fileMode) 456 | { 457 | FilePath = filePath; 458 | Stream = new FileStream(filePath, fileMode); 459 | Open(); 460 | } 461 | 462 | public EndianIO(Stream stream) 463 | { 464 | Stream = stream; 465 | Open(); 466 | } 467 | 468 | public EndianIO(byte[] data) 469 | { 470 | Stream = new MemoryStream(data); 471 | Open(); 472 | } 473 | public EndianIO(byte[] data, bool bigEndian) 474 | { 475 | Stream = new MemoryStream(data); 476 | BigEndian = bigEndian; 477 | Open(); 478 | } 479 | 480 | public void Open() 481 | { 482 | Reader = new EndianReader(this); 483 | Writer = new EndianWriter(this); 484 | } 485 | 486 | public void Close() 487 | { 488 | try 489 | { 490 | Stream.Close(); 491 | Stream = null; 492 | 493 | } 494 | catch 495 | { 496 | } 497 | } 498 | } 499 | } -------------------------------------------------------------------------------- /DOOMExtract/DOOMResourceIndex.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.IO; 4 | 5 | namespace DOOMExtract 6 | { 7 | public class DOOMResourceIndex 8 | { 9 | EndianIO indexIO; 10 | Dictionary resourceIOs; 11 | 12 | public byte PatchFileNumber = 0; // highest PatchFileNumber found in the entries of this index 13 | 14 | public string IndexFilePath; 15 | public string BaseIndexFilePath 16 | { 17 | get 18 | { 19 | var sepString = "resources_"; 20 | var indexPath = IndexFilePath; 21 | var numSepIdx = indexPath.IndexOf(sepString); 22 | if (numSepIdx < 0) 23 | return indexPath; // IndexFilePath is the base index? 24 | 25 | var basePath = indexPath.Substring(0, numSepIdx + sepString.Length - 1); 26 | return basePath + ".index"; 27 | } 28 | } 29 | 30 | public byte Header_Version; 31 | public int Header_IndexSize; 32 | public int Header_NumEntries; 33 | 34 | public List Entries; 35 | 36 | public DOOMResourceIndex(string indexFilePath) 37 | { 38 | IndexFilePath = indexFilePath; 39 | } 40 | 41 | public string ResourceFilePath(int patchFileNumber) 42 | { 43 | var path = Path.Combine(Path.GetDirectoryName(BaseIndexFilePath), Path.GetFileNameWithoutExtension(BaseIndexFilePath)); 44 | if (patchFileNumber == 0) 45 | return path + ".resources"; 46 | if (patchFileNumber == 1) 47 | return path + ".patch"; 48 | 49 | return $"{path}_{patchFileNumber:D3}.patch"; 50 | } 51 | 52 | public EndianIO GetResourceIO(int patchFileNumber) 53 | { 54 | var resPath = ResourceFilePath(patchFileNumber); 55 | if (!resourceIOs.ContainsKey(resPath)) 56 | { 57 | if (!File.Exists(resPath)) 58 | return null; 59 | 60 | var io = new EndianIO(resPath, FileMode.Open); 61 | io.Stream.Position = 0; 62 | var magic = io.Reader.ReadUInt32(); 63 | if ((magic & 0xFFFFFF00) != 0x52455300) 64 | { 65 | io.Close(); 66 | return null; 67 | } 68 | 69 | resourceIOs.Add(resPath, io); 70 | } 71 | 72 | return resourceIOs[resPath]; 73 | } 74 | 75 | public long CopyEntryDataToStream(DOOMResourceEntry entry, Stream destStream, bool decompress = true) 76 | { 77 | var srcStream = entry.GetDataStream(decompress); 78 | if (srcStream == null) 79 | return 0; 80 | 81 | long copyLen = entry.CompressedSize; 82 | if (entry.IsCompressed && decompress) 83 | copyLen = entry.Size; 84 | 85 | return Utility.StreamCopy(destStream, srcStream, 40960, copyLen); 86 | } 87 | 88 | /*public static byte[] CompressData(byte[] data, ZLibNet.CompressionLevel level = ZLibNet.CompressionLevel.Level9) 89 | { 90 | using (var dest = new MemoryStream()) 91 | { 92 | using (var source = new MemoryStream(data)) 93 | { 94 | using (var deflateStream = new ZLibNet.DeflateStream(dest, ZLibNet.CompressionMode.Compress, level, true)) 95 | { 96 | source.CopyTo(deflateStream); 97 | 98 | // DOOM's compressed resources all end with 00 00 FF FF 99 | dest.SetLength(dest.Length + 4); 100 | dest.Position = dest.Length - 2; 101 | dest.WriteByte(0xFF); 102 | dest.WriteByte(0xFF); 103 | 104 | /* DOOM's compressed resources all seem to have the first bit unset 105 | * tested by decompressing data and then recompressing using this Compress method, data is 1:1 except for the first bit (eg. our compressed data started with 0x7D, the games data would be 0x7C) 106 | * in one test, keeping the bit set made the games graphics screw up and trying to open multiplayer would crash 107 | * but unsetting this bit let the game work normally (using a slightly modified decl file also) 108 | * another test like this using data that was heavily modded still resulted in a glitched game, even with this bit unset 109 | * results inconclusive :( 110 | /*dest.Position = 0; 111 | byte b = (byte)ms.ReadByte(); 112 | b &= byte.MaxValue ^ (1 << 0); 113 | ms.Position = 0; 114 | ms.WriteByte((byte)b); 115 | 116 | return dest.ToArray(); 117 | } 118 | } 119 | } 120 | }*/ 121 | 122 | private void addFilesFromFolder(string folder, string baseFolder, EndianIO destResources, ref List addedFiles) 123 | { 124 | var dirs = Directory.GetDirectories(folder); 125 | var files = Directory.GetFiles(folder); 126 | foreach(var file in files) 127 | { 128 | if (addedFiles.Contains(Path.GetFullPath(file))) 129 | continue; 130 | 131 | if (folder == baseFolder && Path.GetFileName(file).ToLower() == "fileids.txt") 132 | continue; // don't want to add fileIds.txt from base 133 | 134 | var filePath = Path.GetFullPath(file).Substring(Path.GetFullPath(baseFolder).Length).Replace("\\", "/"); 135 | var fileEntry = new DOOMResourceEntry(this); 136 | 137 | fileEntry.PatchFileNumber = PatchFileNumber; 138 | fileEntry.FileType = "file"; 139 | if(filePath.Contains(";")) // fileType is specified 140 | { 141 | var idx = filePath.IndexOf(";"); 142 | fileEntry.FileType = filePath.Substring(idx + 1); 143 | filePath = filePath.Substring(0, idx); 144 | } 145 | fileEntry.FileName2 = filePath; 146 | fileEntry.FileName3 = filePath; 147 | 148 | bool needToPad = destResources.Stream.Length % 0x10 != 0; 149 | if (PatchFileNumber > 0 && destResources.Stream.Length == 4) 150 | needToPad = false; // for some reason patch files start at 0x4 instead of 0x10 151 | 152 | if (needToPad) 153 | { 154 | long numPadding = 0x10 - (destResources.Stream.Length % 0x10); 155 | destResources.Stream.SetLength(destResources.Stream.Length + numPadding); 156 | } 157 | 158 | fileEntry.Offset = destResources.Stream.Length; 159 | 160 | byte[] fileData = File.ReadAllBytes(file); 161 | fileEntry.Size = fileEntry.CompressedSize = fileData.Length; 162 | 163 | fileEntry.ID = Entries.Count; // TODO: find out wtf the ID is needed for? 164 | destResources.Stream.Position = fileEntry.Offset; 165 | destResources.Writer.Write(fileData); 166 | 167 | Entries.Add(fileEntry); 168 | addedFiles.Add(Path.GetFullPath(file)); 169 | } 170 | 171 | foreach(var dir in dirs) 172 | { 173 | addFilesFromFolder(dir, baseFolder, destResources, ref addedFiles); 174 | } 175 | } 176 | 177 | public void Rebuild(string destResourceFile, string replaceFromFolder = "", bool keepCompressed = false) 178 | { 179 | if (File.Exists(destResourceFile)) 180 | File.Delete(destResourceFile); 181 | 182 | if (!String.IsNullOrEmpty(replaceFromFolder)) 183 | { 184 | replaceFromFolder = replaceFromFolder.Replace("/", "\\"); 185 | if (!replaceFromFolder.EndsWith("\\")) 186 | replaceFromFolder += "\\"; 187 | } 188 | 189 | var destResources = new EndianIO(destResourceFile, FileMode.CreateNew); 190 | byte[] header = { Header_Version, 0x53, 0x45, 0x52, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 }; 191 | if(PatchFileNumber > 0) 192 | header = new byte[]{ Header_Version, 0x53, 0x45, 0x52 }; // patch files start at 0x4 instead 193 | 194 | destResources.Writer.Write(header); 195 | 196 | var addedFiles = new List(); 197 | foreach (var file in Entries) 198 | { 199 | var replacePath = (String.IsNullOrEmpty(replaceFromFolder) ? String.Empty : Path.Combine(replaceFromFolder, file.GetFullName())); 200 | if (File.Exists(replacePath + ";" + file.FileType)) 201 | replacePath += ";" + file.FileType; 202 | 203 | bool isReplacing = !string.IsNullOrEmpty(replaceFromFolder) && File.Exists(replacePath); 204 | 205 | if (file.PatchFileNumber != PatchFileNumber && !isReplacing) 206 | continue; // file is located in a different patch resource and we aren't replacing it, so skip it 207 | 208 | bool needToPad = destResources.Stream.Length % 0x10 != 0; 209 | if (PatchFileNumber > 0 && destResources.Stream.Length == 4) 210 | needToPad = false; // for some reason patch files start at 0x4 instead of 0x10 211 | 212 | if (file.IsCompressed && !isReplacing && PatchFileNumber > 0) // compressed files not padded in patch files? 213 | needToPad = false; 214 | 215 | if (needToPad) 216 | { 217 | long numPadding = 0x10 - (destResources.Stream.Length % 0x10); 218 | destResources.Stream.SetLength(destResources.Stream.Length + numPadding); 219 | } 220 | 221 | if (file.Size <= 0 && file.CompressedSize <= 0) 222 | { 223 | // patch indexes have offsets for 0-byte files set to 0, but in normal indexes it's the current resource file length 224 | file.Offset = PatchFileNumber > 0 ? 0 : destResources.Stream.Length; 225 | continue; 226 | } 227 | 228 | var offset = destResources.Stream.Length; 229 | destResources.Stream.Position = offset; 230 | 231 | if (isReplacing) 232 | { 233 | file.PatchFileNumber = PatchFileNumber; 234 | 235 | addedFiles.Add(replacePath); 236 | using (var fs = File.OpenRead(replacePath)) 237 | file.CompressedSize = file.Size = (int)Utility.StreamCopy(destResources.Stream, fs, 40960, fs.Length); 238 | } 239 | else 240 | file.CompressedSize = (int)CopyEntryDataToStream(file, destResources.Stream, !keepCompressed); 241 | 242 | file.Offset = offset; 243 | } 244 | 245 | // now add any files that weren't replaced 246 | if(!String.IsNullOrEmpty(replaceFromFolder)) 247 | addFilesFromFolder(replaceFromFolder, replaceFromFolder, destResources, ref addedFiles); 248 | 249 | // read the fileIds.txt file if it exists, and set the IDs 250 | var idFile = Path.Combine(replaceFromFolder, "fileIds.txt"); 251 | if (File.Exists(idFile)) 252 | { 253 | var lines = File.ReadAllLines(idFile); 254 | foreach(var line in lines) 255 | { 256 | if (String.IsNullOrEmpty(line.Trim())) 257 | continue; 258 | 259 | var sepIdx = line.LastIndexOf('='); 260 | if (sepIdx < 0) 261 | continue; // todo: warn user? 262 | var fileName = line.Substring(0, sepIdx).Trim(); 263 | var fileId = line.Substring(sepIdx + 1).Trim(); 264 | int id = -1; 265 | if (!int.TryParse(fileId, out id)) 266 | { 267 | Console.WriteLine($"Warning: file {fileName} defined in fileIds.txt but has invalid id!"); 268 | continue; 269 | } 270 | 271 | var file = Entries.Find(s => s.GetFullName() == fileName); 272 | if (file == null) 273 | file = Entries.Find(s => s.GetFullName().Replace("\\", "/") == fileName); 274 | 275 | if (file != null) 276 | file.ID = id; 277 | else 278 | Console.WriteLine($"Warning: file {fileName} defined in fileIds.txt but doesn't exist?"); 279 | } 280 | } 281 | 282 | destResources.Close(); 283 | Save(); 284 | } 285 | 286 | public void Close() 287 | { 288 | if(indexIO != null) 289 | { 290 | indexIO.Close(); 291 | indexIO = null; 292 | } 293 | if(resourceIOs != null) 294 | { 295 | foreach (var kvp in resourceIOs) 296 | kvp.Value.Close(); 297 | 298 | resourceIOs.Clear(); 299 | resourceIOs = null; 300 | } 301 | } 302 | 303 | public void Save() 304 | { 305 | indexIO.Stream.SetLength(0x20); 306 | 307 | indexIO.BigEndian = true; 308 | indexIO.Stream.Position = 0x20; 309 | indexIO.Writer.Write(Entries.Count); 310 | 311 | foreach (var file in Entries) 312 | file.Write(indexIO); 313 | 314 | indexIO.Stream.Position = 0; 315 | indexIO.BigEndian = false; 316 | byte[] header = { Header_Version, 0x53, 0x45, 0x52 }; 317 | indexIO.Writer.Write(header); 318 | indexIO.BigEndian = true; 319 | indexIO.Writer.Write((int)indexIO.Stream.Length - 0x20); // size of index file minus header size 320 | indexIO.Stream.Flush(); 321 | } 322 | 323 | public bool Load() 324 | { 325 | var indexExt = Path.GetExtension(IndexFilePath); 326 | if (!File.Exists(IndexFilePath) || (indexExt != ".index" && indexExt != ".pindex")) 327 | return false; // not an index file 328 | 329 | if (!File.Exists(ResourceFilePath(0))) 330 | return false; // base resource data file not found! 331 | 332 | resourceIOs = new Dictionary(); 333 | 334 | indexIO = new EndianIO(IndexFilePath, FileMode.Open); 335 | 336 | indexIO.Stream.Position = 0; 337 | var magic = indexIO.Reader.ReadInt32(); 338 | if ((magic & 0xFFFFFF00) != 0x52455300) 339 | { 340 | Close(); 341 | return false; // not a RES file. 342 | } 343 | Header_Version = (byte)(magic & 0xFF); 344 | Header_IndexSize = indexIO.Reader.ReadInt32(); 345 | 346 | // init the base resource data file 347 | if(GetResourceIO(0) == null) 348 | { 349 | Close(); 350 | return false; 351 | } 352 | 353 | indexIO.Stream.Position = 0x20; 354 | indexIO.BigEndian = true; 355 | Header_NumEntries = indexIO.Reader.ReadInt32(); 356 | 357 | Entries = new List(); 358 | for (var i = 0; i < Header_NumEntries; i++) 359 | { 360 | var entry = new DOOMResourceEntry(this); 361 | entry.Read(indexIO); 362 | Entries.Add(entry); 363 | if (entry.PatchFileNumber > PatchFileNumber) 364 | PatchFileNumber = entry.PatchFileNumber; // highest PatchFileNumber must be our patch file index 365 | } 366 | 367 | return true; 368 | } 369 | } 370 | } 371 | --------------------------------------------------------------------------------