├── 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 |
--------------------------------------------------------------------------------