· last year · Jan 11, 2024, 08:46 AM
1/* 2>nul || @title DOTA MOD BUILDER by AveYo v10f - No click sound QOL request
2
3@set "OVERRIDE_STEAM_PATH_IF_NEEDED="
4@set "OVERRIDE_DOTA2_PATH_IF_NEEDED="
5@set "OVERRIDE_OUTPUT_VPK_IF_NEEDED="
6@goto :init
7
8:vpkmod_source_replacement_pairs:[
9sounds/ui/ui_general_deny.vsnd_c sounds/ui/select_action.vsnd_c
10sounds/ui/deny_cooldown.vsnd_c sounds/ui/select_action.vsnd_c
11sounds/ui/deny_mana.vsnd_c sounds/ui/select_action.vsnd_c
12sounds/ui/select_action.vsnd_c sounds/null.vsnd_c
13
14:vpkmod_source_replacement_pairs:]
15
16:: gameinfo_branchspecific.gi is a static alternative to using launch option: -language mods
17:gameinfo:[
18"GameInfo"
19{
20 //
21 // Branch-varying info, such as the game/title and app IDs, is in gameinfo_branchspecific.gi.
22 // gameinfo.gi is the non-branch-varying content and can be integrated between branches.
23 //
24
25 game "Dota 2"
26 title "Dota 2"
27
28 FileSystem
29 {
30 SteamAppId 570
31 BreakpadAppId 373300
32 BreakpadAppId_Tools 375360
33
34 // gameinfo_branchspecific.gi alternative to -language option for running mods is quite popular in China
35 // and Valve have been less enthusiastic about hindering client-side modding in non-western world, wonder why
36 SearchPaths
37 {
38 // AveYo: here is a cleaner way to add mods, without messing the cfg/ path and other auto SearchPaths
39 Game_NonTools dota_mods
40
41 // These are optional language paths. They must be mounted first, which is why there are first in the list.
42 // *LANGUAGE* will be replaced with the actual language name. Not mounted if not running a specific language.
43 Game_Language dota_*LANGUAGE*
44
45 // These are optional low-violence paths. They will only get mounted if you are in a low-violence mode.
46 Game_LowViolence dota_lv
47
48 Game dota
49 Game core
50
51 Mod dota
52
53 Write dota
54
55 // These are optional language paths. They must be mounted first, which is why there are first in the list.
56 // *LANGUAGE* will be replaced with the actual language name. Not mounted if not running a specific language.
57 AddonRoot_Language dota_*LANGUAGE*_addons
58
59 AddonRoot dota_addons
60
61 // Note: addon content is included in publiccontent by default.
62 PublicContent dota_core
63 PublicContent core
64 }
65 AddonsChangeDefaultWritePath 0
66 // restore original file: github.com/SteamDatabase/GameTracking-Dota2/blob/master/game/dota/gameinfo_branchspecific.gi
67 }
68}
69
70:gameinfo:]
71
72:init
73@echo off & chcp 1252 >nul & cls
74::# detect STEAM path
75for /f "tokens=2*" %%R in ('reg query HKCU\SOFTWARE\Valve\Steam /v SteamPath 2^>nul') do set "steam_reg=%%S" & set "libfs="
76if not exist "%STEAM%\steamapps\libraryfolders.vdf" for %%S in ("%steam_reg%") do set "STEAM=%%~fS"
77if defined OVERRIDE_STEAM_PATH_IF_NEEDED set "STEAM=%OVERRIDE_STEAM_PATH_IF_NEEDED%"
78::# detect DOTA2 path
79for /f usebackq^ delims^=^"^ tokens^=4 %%s in (`findstr /c:":\\" "%STEAM%\SteamApps\libraryfolders.vdf"`) do (
80 if exist "%%s\steamapps\appmanifest_570.acf" if exist "%%s\steamapps\common\dota 2 beta\game\core\pak01_dir.vpk" set "libfs=%%s")
81set "STEAMAPPS=%STEAM%\steamapps"& if defined libfs set "STEAMAPPS=%libfs:\\=\%\steamapps"
82set "DOTA2=%STEAMAPPS%\common\dota 2 beta"
83if defined OVERRIDE_DOTA2_PATH_IF_NEEDED set "DOTA2=%OVERRIDE_DOTA2_PATH_IF_NEEDED%"
84::# lean xp+ color macros by AveYo: %<%:af " hello "%>>% & %<%:cf " w\"or\"ld "%>% for single \ / " use .%|%\ .%|%/ \"%|%\"
85for /f "delims=:" %%s in ('echo;prompt $h$s$h:^|cmd /d') do set "|=%%s"&set ">>=\..\c nul&set /p s=%%s%%s%%s%%s%%s%%s%%s<nul&popd"
86set "<=pushd "%appdata%"&2>nul findstr /c:\ /a" &set ">=%>>%&echo;" &set "|=%|:~0,1%" &set /p s=\<nul>"%appdata%\c"
87::# is dota running?
88tasklist /fi "imagename eq dota2.exe" |findstr /i dota2.exe >nul 2>nul && (
89 %<%:cf " ERROR "%>>% & %<%:70 " DOTA2 is currently running! Try again after closing it "%>% & timeout -1 >nul & exit
90)
91::# check required paths
92if not exist "%STEAM%\steamapps\libraryfolders.vdf" (
93 %<%:cf " ERROR "%>>% & %<%:70 " STEAM not found! Set it manually in the script "%>% & timeout -1 >nul & exit
94)
95if not exist "%DOTA2%\game\core\pak01_dir.vpk" (
96 %<%:cf " ERROR "%>>% & %<%:70 " DOTA2 not found! Set it manually in the script "%>% & timeout -1 >nul & exit
97)
98set resourcecompiler="%DOTA2%\game\bin\win64\resourcecompiler.exe"
99set vpkmod="%~dp0vpkmod.exe"
100if not exist %vpkmod% call :csc_compile_vpkmod_tool & if not exist %vpkmod% (
101 %<%:cf " ERROR "%>>% & %<%:70 " compiling VPKMOD C# code! Needs .net framework 4.0 or VS2010+ "%>% & timeout -1 >nul & exit
102)
103%<%:4f " DOTA "%>>% & %<%:2f " MOD "%>>% & %<%:9f " BUILDER "%>%
104
105:process
106set "DIR=%DOTA2%\game\dota_mods" & set "FILE=pak01_dir"
107if defined OVERRIDE_OUTPUT_VPK_IF_NEEDED set "FILE=%OVERRIDE_OUTPUT_VPK_IF_NEEDED%"
108echo DIR = %DIR%
109echo FILE = %FILE%.vpk
110echo Preparing quick file replacement mod with nothing but unaltered Valve authored files
111(mkdir "%DIR%" & rmdir /s/q "%DIR%\working" & mkdir "%DIR%\working") >nul 2>nul
112echo Exporting Mod.ini source replacement pairs for VPKMOD tool exclusive feature -m
113call :export vpkmod_source_replacement_pairs > "%DIR%\working\Mod.ini"
114%vpkmod% -i "%DOTA2%\game\dota\pak01_dir.vpk" -o "%DIR%\%FILE%.vpk" -m "%DIR%\working\Mod.ini"
115echo Exporting gameinfo static alternative to launch option: -language mods
116call :export gameinfo > "%DOTA2%\game\dota\gameinfo_branchspecific.gi"
117echo Cleanup
118rmdir /s/q "%DIR%\working" >nul 2>nul
119%<%:2f " DONE "%>%
120timeout /t -1
121exit /b
122
123:export usage: call :export NAME || Prints all text between lines starting with :NAME:[ and :NAME:] - pure batch snippet by AveYo
124setlocal enabledelayedexpansion || can expand variables by using [/] instead of % - example: [/]systemroot[/]
125set [=&for /f "delims=:" %%s in ('findstr /nbrc:":%~1:\[" /c:":%~1:\]" "%~f0"')do if defined [ (set /a ]=%%s-3)else set /a [=%%s-1
126<"%~fs0" ((for /l %%i in (0 1 %[%) do set /p =)&for /l %%i in (%[% 1 %]%) do (set txt=&set /p txt=&set var=!txt:[/]=%%!&set %%=[/]
127if "!var!" neq "!txt!" (if "!txt!" equ "" (echo() else call echo(!var!) else echo(!txt!)) &endlocal &exit /b
128
129:csc_compile_vpkmod_tool used to create vpk archive
130for /f "tokens=* delims=" %%v in ('dir /b /s /a:-d /o:-n "%SystemRoot%\Microsoft.NET\Framework\*csc.exe"') do set "csc=%%v"
131pushd %~dp0 & "%csc%" /out:vpkmod.exe /target:exe /platform:anycpu /optimize /nologo "%~f0"
132exit /b VPKMOD C# source */
133
134using System; using System.IO; using System.Collections.Generic; using System.Linq; using System.Text; using System.Net;
135using System.Diagnostics; using System.Reflection; using System.Security.Cryptography; using System.Runtime.CompilerServices;
136using SteamDB.ValvePak; [assembly:AssemblyDescriptionAttribute("VPKMOD 2.3")] [assembly: AssemblyTitle("AveYo")]
137[assembly:AssemblyVersionAttribute("2023.01.12")]
138
139class Program
140{
141 // VPKMOD v2.3 retains the useful v1.x legacy code based on Decompiler by SteamDB
142 // so it continues to be a generic tool to list, extract, create, filter and in-memory mod VPKs
143 // The main focus however is on supporting No-Bling DOTA mod builder functionality [stripped]
144
145 private static Options Options;
146 private static readonly object ConsoleWriterLock = new object();
147 private static Dictionary<string,uint> OldPakManifest = new Dictionary<string,uint>();
148 private static Dictionary<string,Dictionary<string,bool>> ModSrc = new Dictionary<string,Dictionary<string,bool>>();
149 private static Dictionary<string,string> SrcMod = new Dictionary<string,string>();
150 private static List<string> FileFilter = new List<string>();
151 private static List<string> ExtFilter = new List<string>();
152 private static bool ExportFilter = false;
153
154 public static void Main(string[] args)
155 {
156 Options = new Options(args);
157
158 // Legacy VPKMOD v1 functions:
159 if (String.IsNullOrEmpty(Options.Input))
160 {
161 Echo("Missing -i input parameter!", ConsoleColor.Red);
162 return;
163 }
164 Options.Input = SlashPath(Path.GetFullPath(Options.Input));
165
166 if (!String.IsNullOrEmpty(Options.Output)) Options.Output = SlashPath(Path.GetFullPath(Options.Output));
167
168 if (!String.IsNullOrEmpty(Options.ModList))
169 {
170 Options.ModList = SlashPath(Path.GetFullPath(Options.ModList));
171 if (File.Exists(Options.ModList))
172 {
173 var file = new StreamReader(Options.ModList);
174 string line, ext, mod, src;
175 Dictionary<string,bool> m = new Dictionary<string,bool>();
176 while ((line = file.ReadLine()) != null)
177 {
178 //var split = line.Split(new string[] { " ? " }, 2, 0);
179 var split = line.IndexOf(" ");
180 if (split > 0)
181 {
182 mod = SlashPath(line.Substring(0, split).Trim());
183 src = SlashPath(line.Substring(split + 2).Trim());
184 FileFilter.Add(src);
185 ext = Path.GetExtension(src);
186 if (ext.Length > 1) ExtFilter.Add(ext.Substring(1));
187 SrcMod[src] = mod;
188 if (!ModSrc.ContainsKey(src)) ModSrc.Add(src, new Dictionary<string,bool> { { mod, false } });
189 else ModSrc[src].Add(mod, false);
190 }
191 }
192 file.Close();
193 }
194 }
195 else if (!String.IsNullOrEmpty(Options.FilterList))
196 {
197 Options.FilterList = SlashPath(Path.GetFullPath(Options.FilterList));
198 if (File.Exists(Options.FilterList))
199 {
200 var file = new StreamReader(Options.FilterList);
201 string line, ext;
202 while ((line = file.ReadLine()) != null)
203 {
204 FileFilter.Add(SlashPath(line));
205 ext = Path.GetExtension(line);
206 if (ext.Length > 1) ExtFilter.Add(ext.Substring(1));
207 }
208 file.Close();
209 }
210
211 if (Options.PathFilter.Count > 0 || Options.ExtFilter.Count > 0) ExportFilter = true;
212 }
213
214 if (Options.PathFilter.Count > 0) Options.PathFilter = Options.PathFilter.ConvertAll(SlashPath);
215
216 var paths = new List<string>();
217
218 if (Directory.Exists(Options.Input))
219 {
220 if (Path.GetExtension(Options.Output).ToLower() != ".vpk")
221 {
222 Echo(String.Format("Input \"{0}\" is a directory while Output \"{1}\" is not a VPK.",
223 Options.Input, Options.Output), ConsoleColor.Red);
224 return;
225 }
226 paths.AddRange(Directory.GetFiles(Options.Input, "*.*",
227 Options.Recursive ? SearchOption.AllDirectories : SearchOption.TopDirectoryOnly));
228 if (paths.Count == 0)
229 {
230 Echo(String.Format("No such file \"{0}\" or dir is empty. Did you mean to include -r (recursive) parameter?",
231 Options.Input), ConsoleColor.Red);
232 return;
233 }
234 LegacyWriteVPK(paths, false); // pak directory into output.vpk
235 }
236 else if (File.Exists(Options.Input))
237 {
238 if (Path.GetExtension(Options.Input).ToLower() != ".vpk")
239 {
240 Echo(String.Format("Input \"{0}\" is not a VPK.", Options.Input), ConsoleColor.Red);
241 return;
242 }
243 paths.Add(Options.Input);
244
245 if (Path.GetExtension(Options.Output).ToLower() != ".vpk")
246 LegacyReadVPK(Options.Input); // unpak input.vpk into output dir
247 else
248 LegacyWriteVPK(paths, true); // mod input.vpk into output.vpk
249 }
250 }
251
252 private static void LegacyReadVPK(string path)
253 {
254 Echo(String.Format("--- Listing files in package \"{0}\"", path), ConsoleColor.Green);
255 var sw = Stopwatch.StartNew();
256 var package = new Package();
257 try
258 {
259 package.Read(path);
260 }
261 catch (Exception e)
262 {
263 Echo(e.ToString(), ConsoleColor.Yellow);
264 }
265
266 if (Options.VerifyVPKChecksums && package.Version == 2)
267 {
268 try
269 {
270 package.VerifyHashes();
271 Console.WriteLine("VPK verification succeeded");
272 }
273 catch (Exception)
274 {
275 Echo("Failed to verify checksums and signature of given VPK:", ConsoleColor.Red);
276 }
277 return;
278 }
279
280 if (!String.IsNullOrEmpty(Options.Output) && !Options.OutputVPKDir)
281 {
282 //Console.WriteLine("--- Reading VPK files...");
283 var manifestPath = String.Concat(path, ".manifest.txt");
284 if (Options.CachedManifest && File.Exists(manifestPath))
285 {
286 var file = new StreamReader(manifestPath);
287 string line;
288 while ((line = file.ReadLine()) != null)
289 {
290 var split = line.Split(new char[1] { ' ' }, 2);
291 if (split.Length == 2) OldPakManifest.Add(split[1], uint.Parse(split[0]));
292 }
293 file.Close();
294 }
295
296 foreach (var etype in package.Entries)
297 {
298 if (ExtFilter.Count > 0 && !ExtFilter.Contains(etype.Key)) continue;
299 else if (Options.ExtFilter.Count > 0 && !Options.ExtFilter.Contains(etype.Key)) continue;
300
301 LegacyDumpVPK(package, etype.Key);
302 }
303
304 if (Options.CachedManifest)
305 {
306 using (var file = new StreamWriter(manifestPath))
307 {
308 foreach (var hash in OldPakManifest)
309 {
310 if (package.FindEntry(hash.Key) == null) Console.WriteLine("\t{0} no longer exists in VPK", hash.Key);
311 file.WriteLine("{0} {1}", hash.Value, hash.Key);
312 }
313 }
314 }
315 }
316
317 if (Options.OutputVPKDir)
318 {
319 foreach (var etype in package.Entries)
320 {
321 foreach (var entry in etype.Value)
322 {
323 Console.WriteLine(entry);
324 }
325 }
326 }
327
328 if (ExportFilter)
329 {
330 using (var filter = new StreamWriter(Options.FilterList))
331 {
332 foreach (var etype in package.Entries)
333 {
334 if (Options.ExtFilter.Count > 0 && !Options.ExtFilter.Contains(etype.Key)) continue;
335
336 foreach (var entry in etype.Value)
337 {
338 var ListPath = SlashPath(entry.GetFullPath());
339 if (Options.PathFilter.Count > 0)
340 {
341 var found = false;
342 foreach (string pathfilter in Options.PathFilter)
343 {
344 if (ListPath.StartsWith(pathfilter, StringComparison.OrdinalIgnoreCase)) found = true;
345 }
346 if (!found) continue;
347 }
348 filter.WriteLine(ListPath);
349 if (!Options.Silent) Console.WriteLine(ListPath);
350 }
351 }
352 }
353 }
354
355 sw.Stop();
356
357 Echo(String.Format("--- Processed in {0}s", sw.Elapsed.TotalSeconds), ConsoleColor.Cyan);
358 }
359
360 private static int LegacyWriteVPK(List<string> paths, bool modding)
361 {
362 if (paths.Count == 0) return 0;
363 var inputdir = Options.Input;
364 var sw = Stopwatch.StartNew();
365 var package = new Package();
366 var pak01_dir = new Package();
367
368 Echo(modding ? "--- Modding... " : "--- Paking... ", ConsoleColor.Green, 0);
369 Echo(paths.Count);
370
371 if (modding)
372 {
373 try { package.Read(Options.Input); } catch (Exception e) { Echo(e.ToString(), ConsoleColor.Yellow); }
374
375 foreach (var etype in package.Entries)
376 {
377 if (ExtFilter.Count > 0 && !ExtFilter.Contains(etype.Key)) continue;
378 else if (Options.ExtFilter.Count > 0 && !Options.ExtFilter.Contains(etype.Key)) continue;
379
380 var entries = package.Entries[etype.Key];
381
382 foreach (var entry in entries)
383 {
384 var filePath = String.Format("{0}.{1}", entry.FileName, entry.TypeName);
385 if (entry.DirectoryName.Length > 0) filePath = Path.Combine(entry.DirectoryName, filePath);
386 filePath = SlashPath(filePath);
387
388 bool found = false;
389 if (FileFilter.Count > 0)
390 {
391 foreach (string filter in FileFilter)
392 {
393 if (filePath == filter) found = true; // StartsWith
394 }
395 if (!found) continue;
396 }
397 else if (Options.PathFilter.Count > 0)
398 {
399 foreach (string filter in Options.PathFilter)
400 {
401 if (filePath.StartsWith(filter, StringComparison.OrdinalIgnoreCase)) found = true;
402 }
403 if (!found) continue;
404 }
405
406 var ext = entry.TypeName;
407 if (ext == "")
408 {
409 ext = " ";
410 if (!Options.Silent) Echo(" missing extension!", ConsoleColor.Red);
411 //continue;
412 }
413 var file = entry.FileName; //Path.GetFileNameWithoutExtension(root);
414 if (file == "")
415 {
416 file = " ";
417 if (!Options.Silent) Echo(" missing name!", ConsoleColor.Red);
418 //continue;
419 }
420 var dir = entry.DirectoryName; //Path.GetDirectoryName(root).Replace('\\', '/');
421
422 byte[] output;
423 lock (package)
424 {
425 package.ReadEntry(entry, out output, false);
426 }
427
428 if (ModSrc.ContainsKey(filePath))
429 {
430 if (!Options.Silent) Console.WriteLine("--- Replacing with {0}", filePath);
431 foreach (var m in ModSrc[filePath])
432 {
433 if (!Options.Silent) Console.WriteLine(" {0}", m);
434 filePath = m.Key;
435 ext = Path.GetExtension(m.Key).TrimStart('.');
436 file = Path.GetFileNameWithoutExtension(m.Key);
437 dir = Path.GetDirectoryName(m.Key).Replace('\\', '/');
438 if (dir == "") dir = " ";
439 pak01_dir.AddEntry(dir, file, ext, output);
440 }
441 }
442 else
443 {
444 if (dir == "") dir = " ";
445 pak01_dir.AddEntry(dir, file, ext, output);
446 }
447 }
448 }
449
450 // mod size optimization: replace res with zero-byte file if mod src pair has src="00"
451 string nix = "00";
452 if (ModSrc.ContainsKey(nix))
453 {
454 if (!Options.Silent) Console.WriteLine("--- Replacing with \"00\" [ 0-byte data ]");
455 foreach (var m in ModSrc[nix])
456 {
457 if (!Options.Silent) Console.WriteLine(" {0}", m);
458 var ext = Path.GetExtension(m.Key).TrimStart('.');
459 var file = Path.GetFileNameWithoutExtension(m.Key);
460 var dir = Path.GetDirectoryName(m.Key).Replace('\\', '/');
461 if (dir == "") dir = " ";
462 pak01_dir.AddEntry(dir, file, ext, new byte[0]);
463 }
464 }
465 }
466
467 // include pak01_dir subfolder (if it exists) for manual overrides when modding
468 if (Directory.Exists("pak01_dir") && modding)
469 {
470 if (!Options.Silent) Console.WriteLine("--- Including files in \"pak01_dir\" folder");
471 pak01_dir.AddFolder("pak01_dir");
472 }
473
474 if (!modding)
475 {
476 pak01_dir.AddFolder(inputdir);
477 }
478
479 pak01_dir.SaveToFile(Options.Output);
480 sw.Stop();
481 var files = pak01_dir.Entries.Values.Sum(_ => _.Count);
482 Echo(String.Format("--- Processed {0} files in {1}s", files, sw.Elapsed.TotalSeconds), ConsoleColor.Cyan);
483 return files;
484 }
485
486 private static void LegacyDumpVPK(Package package, string ext)
487 {
488 var entries = package.Entries[ext];
489
490 foreach (var entry in entries)
491 {
492 var filePath = String.Format("{0}.{1}", entry.FileName, entry.TypeName);
493 if (!String.IsNullOrEmpty(entry.DirectoryName)) filePath = Path.Combine(entry.DirectoryName, filePath);
494 filePath = SlashPath(filePath);
495
496 bool found = false;
497 if (FileFilter.Count > 0)
498 {
499 foreach (string filter in FileFilter)
500 {
501 if (filePath.StartsWith(filter, StringComparison.OrdinalIgnoreCase)) found = true;
502 }
503 if (!found) continue;
504 }
505 else if (Options.PathFilter.Count > 0)
506 {
507 foreach (string filter in Options.PathFilter)
508 {
509 if (filePath.StartsWith(filter, StringComparison.OrdinalIgnoreCase)) found = true;
510 }
511 if (!found) continue;
512 }
513
514 if (!String.IsNullOrEmpty(Options.Output))
515 {
516 uint oldCrc32;
517 if (Options.CachedManifest && OldPakManifest.TryGetValue(filePath, out oldCrc32) && oldCrc32 == entry.CRC32)
518 continue;
519 OldPakManifest[filePath] = entry.CRC32;
520 }
521
522 byte[] output;
523 lock (package)
524 {
525 package.ReadEntry(entry, out output, false);
526 }
527
528 if (!String.IsNullOrEmpty(Options.Output)) LegacyDumpFile(filePath, output);
529 }
530 }
531
532 private static void LegacyDumpFile(string path, byte[] data)
533 {
534 var outputFile = SlashPath(Path.Combine(Options.Output, path));
535 Directory.CreateDirectory(Path.GetDirectoryName(outputFile));
536 File.WriteAllBytes(outputFile, data);
537 if (!Options.Silent) Console.WriteLine("--- Written \"{0}\"", outputFile);
538 }
539
540 public static string SlashPath(string path)
541 {
542 if (path == null) return null;
543 else if (path.Length == 1) return path;
544 else return String.Join("/", path.Split(new char[2] {'/', '\\'}, StringSplitOptions.None)).TrimEnd('/');
545 }
546
547 public static void Log(params object[] msg)
548 {
549 using (TextWriter errorWriter = Console.Error)
550 {
551 errorWriter.WriteLine(String.Join(" ", msg));
552 }
553 }
554
555 public static void Echo(object msg, ConsoleColor clr = ConsoleColor.Gray, int newline = 1)
556 {
557 lock (ConsoleWriterLock)
558 {
559 Console.ForegroundColor = clr;
560 Console.Write(newline == 1 ? "{0}\n" : "{0}", msg);
561 Console.ResetColor();
562 }
563 }
564}
565
566public class Options
567{
568 public string Input { get; set; }
569 public bool Recursive { get; set; }
570 public string Output { get; set; }
571 public bool OutputVPKDir { get; set; }
572 public bool CachedManifest { get; set; }
573 public bool VerifyVPKChecksums { get; set; }
574 public List<string> ExtFilter { get; set; }
575 public List<string> PathFilter { get; set; }
576 public string FilterList { get; set; }
577 public string ModList { get; set; }
578 public bool Silent { get; set; }
579 public bool Help { get; set; }
580 public bool Dialog { get { return (Environment.GetEnvironmentVariable("NO_CHOICES_DIALOG") == null); } }
581 public bool MONO { get { int p = (int)Environment.OSVersion.Platform; return ((p == 4) || (p == 6) || (p == 128)); } }
582 internal IDictionary<string,List<string>> Parsed { get; private set; }
583
584 public Options(string[] cmd)
585 {
586 Parsed = new Dictionary<string,List<string>>(); Parse(cmd);
587 }
588
589 internal bool Find(string key, bool novalue = false)
590 {
591 if (Parsed.ContainsKey(key))
592 {
593 if (novalue || Parsed[key].Count != 0) return true;
594 Console.WriteLine("-" + key + " requires a value!");
595 }
596 return false;
597 }
598
599 internal void Parse(string[] cmd)
600 {
601 var key = ""; List<string> values = new List<string>();
602 foreach (string item in cmd)
603 {
604 if (item[0] == '-') { if (key != "") Parsed[key] = values; key = item.Substring(1); values = new List<string>(); }
605 else if (key == "") { Parsed[item] = new List<string>(); }
606 else { values = new List<string>(item.Split(',')); }
607 }
608 if (key != "") Parsed[key] = values;
609
610 Input = Find("i") ? Parsed["i"][0] : "";
611 Recursive = Find("r", true);
612 Output = Find("o") ? Parsed["o"][0] : "";
613 CachedManifest = Find("c", true);
614 OutputVPKDir = Find("d", true);
615 VerifyVPKChecksums = Find("v", true);
616 ExtFilter = Find("e") ? Parsed["e"] : new List<string>();
617 PathFilter = Find("p") ? Parsed["p"] : new List<string>();
618 FilterList = Find("l") ? Parsed["l"][0] : "";
619 ModList = Find("m") ? Parsed["m"][0] : "";
620 Silent = Find("s", true);
621 Help = Find("h", true) || cmd.Length == 0;
622
623 if (Silent == false)
624 {
625 Console.ForegroundColor = ConsoleColor.Black; Console.BackgroundColor = ConsoleColor.Cyan;
626 Console.Write(" VPKMOD v2.3 "); Console.ResetColor(); Console.WriteLine(" AveYo / SteamDB");
627 }
628 if (Help)
629 {
630 Console.WriteLine(" -i input Directory to create new VPK from, or File to extract VPK from");
631 Console.WriteLine(" -o output File to create VPK to, or Directory to extract VPK to");
632 Console.WriteLine(" -r Recursively include all files in Directory");
633 Console.WriteLine(" -c Cached VPK manifest: only changed files get extracted to disk");
634 Console.WriteLine(" -d Write VPK directory of files and their CRC to console");
635 Console.WriteLine(" -v Verify checksums and signatures: only for VPK version 2");
636 Console.WriteLine(" -e txt,vjs_c Extension(s) filter: only include these file extensions");
637 Console.WriteLine(" -p cfg/,dev/ Path(s) filter: only include files from these paths");
638 Console.WriteLine(" -l list.txt List file to import fullpath filters from");
639 Console.WriteLine(" | if -e or -p are also used, export current filters instead");
640 Console.WriteLine(" | vpkmod -i pak01_dir.vpk -e vmdl_c -p models/heroes/mars -l mars.txt");
641 Console.WriteLine(" -m mod.txt Mod = Src pairs file for in-memory unpak-replace-pak quick modding");
642 Console.WriteLine(" | sounds/misc/soundboard/all_dead.vsnd_c?sounds/null.vsnd_c");
643 Console.WriteLine(" | if Src is \"00\" set Mod file content to 0-byte");
644 Console.WriteLine(" | automatically imports files from a pak01_dir subfolder");
645 Console.WriteLine(" -s Silent");
646 Console.WriteLine(" -h This help screen");
647 Console.ReadKey(); Environment.Exit(0);
648 }
649 }
650}
651
652namespace SteamDB.ValvePak
653{
654 /*
655 MIT License
656
657 Copyright (c) 2008 Rick (rick 'at' gibbed 'dot' us)
658 Copyright (c) 2016 SteamDB
659 Copyright (c) 2019 AveYo
660
661 Permission is hereby granted, free of charge, to any person obtaining a copy
662 of this software and associated documentation files (the "Software"), to deal
663 in the Software without restriction, including without limitation the rights
664 to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
665 copies of the Software, and to permit persons to whom the Software is
666 furnished to do so, subject to the following conditions:
667
668 The above copyright notice and this permission notice shall be included in all
669 copies or substantial portions of the Software.
670
671 THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
672 IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
673 FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
674 AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
675 LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
676 OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
677 SOFTWARE.
678 */
679
680 public class Package : IDisposable
681 {
682 public const int MAGIC = 0x55AA1234;
683 public const char DirectorySeparatorChar = '/';
684 private BinaryReader Reader;
685 public string FileName { get; private set; }
686 public bool IsDirVPK { get; private set; }
687 public uint Version { get; private set; }
688 public uint HeaderSize { get; private set; }
689 public uint TreeSize { get; private set; }
690 public uint FileDataSectionSize { get; private set; }
691 public uint ArchiveMD5SectionSize { get; private set; }
692 public uint OtherMD5SectionSize { get; private set; }
693 public uint SignatureSectionSize { get; private set; }
694 public byte[] TreeChecksum { get; private set; }
695 public byte[] ArchiveMD5EntriesChecksum { get; private set; }
696 public byte[] WholeFileChecksum { get; private set; }
697 public byte[] PublicKey { get; private set; }
698 public byte[] Signature { get; private set; }
699 public Dictionary<string,List<PackageEntry>> Entries { get; private set; }
700 public List<ArchiveMD5SectionEntry> ArchiveMD5Entries { get; private set; }
701
702 public void Dispose()
703 {
704 Dispose(true);
705 GC.SuppressFinalize(this);
706 }
707
708 protected virtual void Dispose(bool disposing)
709 {
710 if (disposing && Reader != null)
711 {
712 Reader.Dispose();
713 Reader = null;
714 }
715 }
716
717 public void SetFileName(string fileName)
718 {
719 if (fileName == null)
720 {
721 throw new ArgumentNullException("vpk fileName is null");
722 }
723
724 if (fileName.EndsWith(".vpk", StringComparison.OrdinalIgnoreCase))
725 {
726 fileName = fileName.Substring(0, fileName.Length - 4);
727 }
728
729 if (fileName.EndsWith("_dir", StringComparison.OrdinalIgnoreCase))
730 {
731 IsDirVPK = true;
732 fileName = fileName.Substring(0, fileName.Length - 4);
733 }
734
735 FileName = fileName;
736 }
737
738 public void Read(string filename)
739 {
740 SetFileName(filename);
741
742 var fs = new FileStream(filename, FileMode.Open, FileAccess.Read, FileShare.ReadWrite);
743 //String.Format("{0}{1}.vpk", FileName, IsDirVPK ? "_dir" : "")
744
745 Read(fs);
746 }
747
748 public void Read(Stream input)
749 {
750 if (input == null)
751 {
752 throw new ArgumentNullException("VPK stream input is null");
753 }
754
755 if (FileName == null)
756 throw new InvalidOperationException("You must call SetFileName() before calling Read() directly with a stream.");
757
758 Reader = new BinaryReader(input);
759
760 if (Reader.ReadUInt32() != MAGIC)
761 throw new InvalidDataException("Given file is not a VPK.");
762
763 Version = Reader.ReadUInt32();
764 TreeSize = Reader.ReadUInt32();
765
766 if (Version == 1)
767 {
768 // Nothing else
769 }
770 else if (Version == 2)
771 {
772 FileDataSectionSize = Reader.ReadUInt32();
773 ArchiveMD5SectionSize = Reader.ReadUInt32();
774 OtherMD5SectionSize = Reader.ReadUInt32();
775 SignatureSectionSize = Reader.ReadUInt32();
776 }
777 else if (Version == 0x00030002) // Apex Legends, Titanfall
778 {
779 throw new NotSupportedException("Respawn uses customized vpk format which this library does not support.");
780 }
781 else
782 {
783 throw new InvalidDataException(String.Format("Bad VPK version. ({0})", Version));
784 }
785
786 HeaderSize = (uint)input.Position;
787
788 ReadEntries();
789
790 if (Version == 2)
791 {
792 // Skip over file data, if any
793 input.Position += FileDataSectionSize;
794
795 ReadArchiveMD5Section();
796 ReadOtherMD5Section();
797 ReadSignatureSection();
798 }
799 }
800
801 public PackageEntry FindEntry(string filePath)
802 {
803 if (filePath == null)
804 throw new ArgumentNullException("filePath");
805
806 filePath = filePath.Replace('\\', DirectorySeparatorChar);
807
808 var lastSeparator = filePath.LastIndexOf(DirectorySeparatorChar);
809 var directory = lastSeparator > -1 ? filePath.Substring(0, lastSeparator) : string.Empty;
810 var fileName = filePath.Substring(lastSeparator + 1);
811
812 return FindEntry(directory, fileName);
813 }
814
815 public PackageEntry FindEntry(string directory, string fileName)
816 {
817 if (directory == null)
818 throw new ArgumentNullException("directory");
819
820 if (fileName == null)
821 throw new ArgumentNullException("fileName");
822
823 var dot = fileName.LastIndexOf('.');
824 string extension;
825
826 if (dot > -1)
827 {
828 extension = fileName.Substring(dot + 1);
829 fileName = fileName.Substring(0, dot);
830 }
831 else
832 {
833 // Valve uses a space for missing extensions
834 extension = " ";
835 }
836
837 return FindEntry(directory, fileName, extension);
838 }
839
840 public PackageEntry FindEntry(string directory, string fileName, string extension)
841 {
842 if (directory == null)
843 throw new ArgumentNullException("directory");
844
845 if (fileName == null)
846 throw new ArgumentNullException("fileName");
847
848 if (extension == null)
849 throw new ArgumentNullException("extension");
850
851 if (!Entries.ContainsKey(extension))
852 return null;
853
854 // We normalize path separators when reading the file list
855 // And remove the trailing slash
856 directory = directory.Replace('\\', DirectorySeparatorChar).Trim(DirectorySeparatorChar);
857
858 // If the directory is empty after trimming, set it to a space to match Valve's behaviour
859 if (directory.Length == 0)
860 directory = " ";
861
862 return Entries[extension].Find(_ => _.DirectoryName == directory && _.FileName == fileName);
863 }
864
865 public void ReadEntry(PackageEntry entry, out byte[] output, bool validateCrc = true)
866 {
867 output = new byte[entry.SmallData.Length + entry.Length];
868
869 if (entry.SmallData.Length > 0)
870 entry.SmallData.CopyTo(output, 0);
871
872 if (entry.Length > 0)
873 {
874 Stream fs = null;
875
876 try
877 {
878 var offset = entry.Offset;
879
880 if (entry.ArchiveIndex != 0x7FFF)
881 {
882 if (!IsDirVPK)
883 throw new InvalidOperationException("Given VPK is not _dir, but entry references external archive.");
884
885 var fileName = String.Format("{0}_{1:d3}.vpk", FileName, entry.ArchiveIndex);
886 fs = new FileStream(fileName, FileMode.Open, FileAccess.Read);
887 }
888 else
889 {
890 fs = Reader.BaseStream;
891 offset += HeaderSize + TreeSize;
892 }
893
894 fs.Seek(offset, SeekOrigin.Begin);
895
896 int length = (int)entry.Length;
897 int readOffset = entry.SmallData.Length;
898 int bytesRead;
899 int totalRead = 0;
900 while ((bytesRead = fs.Read(output, readOffset + totalRead, length - totalRead)) != 0)
901 {
902 totalRead += bytesRead;
903 }
904 }
905 finally
906 {
907 if (entry.ArchiveIndex != 0x7FFF && fs != null)
908 fs.Close();
909 }
910 }
911
912 if (validateCrc && entry.CRC32 != Crc32.Compute(output))
913 throw new InvalidDataException("CRC32 mismatch for read data.");
914 }
915
916 private void ReadEntries()
917 {
918 var typeEntries = new Dictionary<string, List<PackageEntry>>();
919 using (MemoryStream ms = new MemoryStream())
920
921 // Types
922 while (true)
923 {
924 var typeName = ReadNullTermUtf8String(ms);
925
926 if (string.IsNullOrEmpty(typeName))
927 {
928 break;
929 }
930
931 var entries = new List<PackageEntry>();
932
933 // Directories
934 while (true)
935 {
936 var directoryName = ReadNullTermUtf8String(ms);
937
938 if (string.IsNullOrEmpty(directoryName))
939 {
940 break;
941 }
942
943 // Files
944 while (true)
945 {
946 var fileName = ReadNullTermUtf8String(ms);
947
948 if (string.IsNullOrEmpty(fileName))
949 {
950 break;
951 }
952
953 var entry = new PackageEntry
954 {
955 FileName = fileName,
956 DirectoryName = directoryName,
957 TypeName = typeName,
958 };
959
960 entry.CRC32 = Reader.ReadUInt32();
961 var smallDataSize = Reader.ReadUInt16();
962 entry.ArchiveIndex = Reader.ReadUInt16();
963 entry.Offset = Reader.ReadUInt32();
964 entry.Length = Reader.ReadUInt32();
965
966 var terminator = Reader.ReadUInt16();
967
968 if (terminator != 0xFFFF)
969 throw new FormatException("VPK entry with invalid terminator.");
970
971 if (smallDataSize > 0)
972 {
973 entry.SmallData = new byte[smallDataSize];
974
975 int bytesRead;
976 int totalRead = 0;
977 while ((bytesRead = Reader.Read(entry.SmallData, totalRead, entry.SmallData.Length - totalRead)) != 0)
978 {
979 totalRead += bytesRead;
980 }
981 }
982 else
983 {
984 entry.SmallData = Array.Empty<byte>();
985 }
986
987 entries.Add(entry);
988 }
989 }
990
991 typeEntries.Add(typeName, entries);
992 }
993
994 Entries = typeEntries;
995 }
996
997 public void VerifyHashes()
998 {
999 if (Version != 2)
1000 throw new InvalidDataException("Only version 2 is supported.");
1001
1002 using (var md5 = MD5.Create())
1003 {
1004 Reader.BaseStream.Position = 0;
1005
1006 var hash = md5.ComputeHash(
1007 Reader.ReadBytes((int)(HeaderSize + TreeSize + FileDataSectionSize + ArchiveMD5SectionSize + 32)));
1008
1009 if (!hash.SequenceEqual(WholeFileChecksum))
1010 throw new InvalidDataException(String.Format("Package checksum mismatch ({0} != expected {1})",
1011 BitConverter.ToString(hash), BitConverter.ToString(WholeFileChecksum)));
1012
1013 Reader.BaseStream.Position = HeaderSize;
1014
1015 hash = md5.ComputeHash(Reader.ReadBytes((int)TreeSize));
1016
1017 if (!hash.SequenceEqual(TreeChecksum))
1018 throw new InvalidDataException(String.Format("File tree checksum mismatch ({0} != expected {1})",
1019 BitConverter.ToString(hash), BitConverter.ToString(TreeChecksum)));
1020
1021 Reader.BaseStream.Position = HeaderSize + TreeSize + FileDataSectionSize;
1022
1023 hash = md5.ComputeHash(Reader.ReadBytes((int)ArchiveMD5SectionSize));
1024
1025 if (!hash.SequenceEqual(ArchiveMD5EntriesChecksum))
1026 throw new InvalidDataException(String.Format("Archive MD5 entries checksum mismatch ({0} != expected {1})",
1027 BitConverter.ToString(hash), BitConverter.ToString(ArchiveMD5EntriesChecksum)));
1028
1029 // TODO: verify archive checksums
1030 }
1031
1032 if (PublicKey == null || Signature == null)
1033 return;
1034
1035 if (!IsSignatureValid())
1036 throw new InvalidDataException("VPK signature is not valid.");
1037 }
1038
1039 public bool IsSignatureValid()
1040 {
1041 // AveYo : just return true since RSA and AsnKeyParser are not used in VPKMOD
1042 return true;
1043 }
1044
1045 private void ReadArchiveMD5Section()
1046 {
1047 ArchiveMD5Entries = new List<ArchiveMD5SectionEntry>();
1048
1049 if (ArchiveMD5SectionSize == 0)
1050 {
1051 return;
1052 }
1053
1054 var entries = ArchiveMD5SectionSize / 28; // 28 is sizeof(VPK_MD5SectionEntry), which is int + int + int + 16 chars
1055
1056 for (var i = 0; i < entries; i++)
1057 {
1058 ArchiveMD5Entries.Add(new ArchiveMD5SectionEntry
1059 {
1060 ArchiveIndex = Reader.ReadUInt32(),
1061 Offset = Reader.ReadUInt32(),
1062 Length = Reader.ReadUInt32(),
1063 Checksum = Reader.ReadBytes(16)
1064 });
1065 }
1066 }
1067
1068 private void ReadOtherMD5Section()
1069 {
1070 if (OtherMD5SectionSize != 48)
1071 throw new InvalidDataException(String.Format("Encountered OtherMD5Section with size of {0} (should be 48)",
1072 OtherMD5SectionSize));
1073
1074 TreeChecksum = Reader.ReadBytes(16);
1075 ArchiveMD5EntriesChecksum = Reader.ReadBytes(16);
1076 WholeFileChecksum = Reader.ReadBytes(16);
1077 }
1078
1079 private void ReadSignatureSection()
1080 {
1081 if (SignatureSectionSize == 0)
1082 {
1083 return;
1084 }
1085
1086 var publicKeySize = Reader.ReadInt32();
1087
1088 if (SignatureSectionSize == 20 && publicKeySize == MAGIC)
1089 {
1090 // CS2 has this
1091 return;
1092 }
1093
1094 PublicKey = Reader.ReadBytes(publicKeySize);
1095
1096 var signatureSize = Reader.ReadInt32();
1097 Signature = Reader.ReadBytes(signatureSize);
1098 }
1099
1100 [MethodImpl(MethodImplOptions.AggressiveInlining)]
1101 private string ReadNullTermUtf8String(MemoryStream ms)
1102 {
1103 while (true)
1104 {
1105 var b = Reader.ReadByte();
1106
1107 if (b == 0x00)
1108 {
1109 break;
1110 }
1111
1112 ms.WriteByte(b);
1113 }
1114
1115 ArraySegment<byte> buffer;
1116
1117 ms.TryGetBuffer(out buffer);
1118
1119 var str = Encoding.UTF8.GetString(buffer.ToArray());
1120
1121 ms.SetLength(0);
1122
1123 return str;
1124 }
1125
1126 // AveYo: enhanced rework of my previous stand-alone vpk writer to fit in the Package class
1127 public void SaveToFile(string fn)
1128 {
1129 // VPKMOD23 just counts offsets for duplicate files, so the exported .vpk is generally smaller for repetitive content!
1130 var sw = Stopwatch.StartNew();
1131 if (Entries == null) { File.Delete(fn); return; }
1132 Directory.CreateDirectory(Path.GetDirectoryName(fn));
1133 using (var md5 = MD5.Create())
1134 using (var sha1 = SHA1.Create())
1135 using (FileStream fs = new FileStream(fn, FileMode.Create, FileAccess.Write))
1136 using (MemoryStream mtree = new MemoryStream(), mdata = new MemoryStream())
1137 using (BinaryWriter tree = new BinaryWriter(mtree), data = new BinaryWriter(mdata))
1138 using (BinaryReader buff = new BinaryReader(mtree))
1139 {
1140 uint version = 2, tree_size = 0, data_size = 0, data_offset = 16, lookup_offset = 0;
1141 short preload_bytes = 0, archive_index = 0x7fff, terminator = -1;
1142 var seen = new Dictionary<string, uint>();
1143 bool unique = true;
1144 byte nul1 = 0;
1145
1146 data.Write(0x4d4b5056); data.Write(0x3332444f); data.Write(0x41204020); data.Write(0x4f594556); // id
1147
1148 tree.Write(MAGIC); // Signature 4
1149 tree.Write(version); // Version 4
1150 tree.Write(tree_size); // TreeSize (TBD) 4
1151
1152 tree.Write(0x00000000); // FileDataSectionSize 4
1153 tree.Write(0x00000000); // ArchiveMD5SectionSize 4
1154 tree.Write(0x00000030); // OtherMD5SectionSize 4
1155 tree.Write(0x00000000); // SignatureSectionSize 4
1156
1157 foreach (string etype in Entries.Keys)
1158 {
1159 tree.Write(Encoding.UTF8.GetBytes(etype)); // TypeName ?
1160 tree.Write(nul1); // 00 1
1161 tree_size += (uint)etype.Length + 1;
1162
1163 var directories = Entries[etype].Select(_ => _.DirectoryName).Distinct();
1164 foreach (string dirname in directories)
1165 {
1166 tree.Write(Encoding.UTF8.GetBytes(dirname)); // DirectoryName ?
1167 tree.Write(nul1); // 00 1
1168 tree_size += (uint)dirname.Length + 1;
1169
1170 var files = Entries[etype].Where(_ => _.DirectoryName == dirname).Select(_ => _.FileName);
1171 foreach (string filename in files)
1172 {
1173 byte[] data_bytes;
1174 var found = Entries[etype].Find(_ => _.DirectoryName == dirname && _.FileName == filename);
1175
1176 if (found != null)
1177 ReadEntry(found, out data_bytes, false); // it should always be found
1178 else
1179 data_bytes = new byte[0];
1180
1181 uint data_length = (uint)data_bytes.Length;
1182 uint crc = Crc32.Compute(data_bytes);
1183 string hash = String.Join("", sha1.ComputeHash(data_bytes));
1184 if (seen.ContainsKey(hash))
1185 {
1186 unique = false; lookup_offset = seen[hash];
1187 }
1188 else
1189 {
1190 unique = true; lookup_offset = data_offset; seen.Add(hash, data_offset);
1191 }
1192 tree.Write(Encoding.UTF8.GetBytes(filename)); // FileName ?
1193 tree.Write(nul1); // 00 1
1194 tree.Write(crc); // CheckSum 4
1195 tree.Write(preload_bytes); // PreloadBytes 2
1196 tree.Write(archive_index); // ArchiveIndex 2
1197 tree.Write(lookup_offset); // EntryOffset 4
1198 tree.Write(data_length); // EntryLength 4
1199 tree.Write(terminator); // Terminator 2
1200 if (unique)
1201 {
1202 data.Write(data_bytes); // DataBytes written
1203 data_offset += data_length; // to secondary stream
1204 }
1205 tree_size += (uint)filename.Length + 19;
1206 }
1207 tree.Write(nul1); // 00 Next Directory 1
1208 tree_size += 1;
1209 }
1210 tree.Write(nul1); // 00 Next Type 1
1211 tree_size += 1;
1212 }
1213
1214 tree.Write(nul1); // 00 Tree End 1
1215 tree_size += 1;
1216
1217 mdata.Position = 0;
1218 mdata.CopyTo(mtree); // Data write ?
1219 data_size = (uint)mdata.Length;
1220
1221 mtree.Position = 8;
1222 tree.Write(tree_size); // TreeSize update
1223 tree.Write(data_size); // FileDataSectionSize update
1224
1225 mtree.Position = 28;
1226 var tree_checksum = md5.ComputeHash(buff.ReadBytes((int)tree_size));
1227 var archive000_checksum = md5.ComputeHash(new byte[0]);
1228 mtree.Position = 28 + tree_size + data_size;
1229 tree.Write(tree_checksum); // TreeChecksum 16
1230 tree.Write(archive000_checksum); // Archive000Checksum 16
1231
1232 mtree.Position = 0;
1233 var wholefile_checksum = md5.ComputeHash(buff.ReadBytes((int)(28 + tree_size + data_size + 32)));
1234 mtree.Position = 28 + tree_size + data_size + 32;
1235 tree.Write(wholefile_checksum); // WholeFileChecksum 16
1236
1237 mtree.Position = 0;
1238 mtree.CopyTo(fs); // File write tree + data
1239 }
1240 sw.Stop();
1241 Console.WriteLine(String.Format("--- Written {0} in {1}s", fn, sw.Elapsed.TotalSeconds));
1242 }
1243
1244 public void AddEntry(string dir, string name, string ext, byte[] data)
1245 {
1246 if (Entries == null)
1247 Entries = new Dictionary<string,List<PackageEntry>>();
1248
1249 if (!Entries.Keys.Contains(ext))
1250 Entries.Add(ext, new List<PackageEntry>());
1251
1252 var found = Entries[ext].Find(_ => _.DirectoryName == dir && _.FileName == name);
1253
1254 if (found == null)
1255 {
1256 Entries[ext].Add(new PackageEntry {
1257 FileName = name, DirectoryName = dir, TypeName = ext,
1258 CRC32 = 0, SmallData = data, ArchiveIndex = 0, Offset = 0, Length = 0
1259 });
1260 }
1261 else
1262 {
1263 found.Length = 0;
1264 found.SmallData = data;
1265 }
1266 }
1267
1268 public void AddEntry(string path, byte[] data)
1269 {
1270 var s = path.LastIndexOf("/");
1271 var dir = (s == -1) ? " " : path.Substring(0, s);
1272 var file = path.Substring(s + 1);
1273 s = file.LastIndexOf('.');
1274 var ext = (s == -1) ? " " : file.Substring(s + 1);
1275 var name = (s == -1) ? file : file.Substring(0, s);
1276
1277 if (Entries == null)
1278 Entries = new Dictionary<string,List<PackageEntry>>();
1279
1280 if (!Entries.Keys.Contains(ext))
1281 Entries.Add(ext, new List<PackageEntry>());
1282
1283 var found = Entries[ext].Find(_ => _.DirectoryName == dir && _.FileName == name);
1284
1285 if (found == null)
1286 {
1287 Entries[ext].Add(new PackageEntry {
1288 FileName = name, DirectoryName = dir, TypeName = ext,
1289 CRC32 = 0, SmallData = data, ArchiveIndex = 0, Offset = 0, Length = 0
1290 });
1291 }
1292 else
1293 {
1294 found.Length = 0;
1295 found.SmallData = data;
1296 }
1297 }
1298
1299 public void AddFolder(string inputdir)
1300 {
1301 // include pak01_dir subfolder (if it exists) for manual overrides when modding
1302 if (!Directory.Exists(inputdir))
1303 return;
1304
1305 var paths = new List<string>();
1306 paths.AddRange(Directory.GetFiles(inputdir, "*.*", SearchOption.AllDirectories));
1307
1308 if (paths.Count == 0)
1309 return;
1310
1311 Console.WriteLine("--- Adding files in \"{0}\"", inputdir);
1312
1313 var excluded = new List<string>() { "zip", "reg", "rar", "msi", "exe", "dll", "com", "cmd", "bat", "vbs" };
1314 var iso = Encoding.GetEncoding("ISO-8859-1");
1315 var utf = Encoding.UTF8;
1316
1317 foreach (var path in paths)
1318 {
1319 byte[] latin = Encoding.Convert(utf, iso, utf.GetBytes(path.Substring(inputdir.Length + 1)));
1320 string root = iso.GetString(latin).ToLower();
1321
1322 var ext = Path.GetExtension(root).TrimStart('.');
1323
1324 if (excluded.Contains(ext))
1325 continue; // ERROR illegal extension!
1326
1327 if (ext == "")
1328 ext = " "; // WARNING missing extension
1329
1330 var name = Path.GetFileNameWithoutExtension(root);
1331
1332 if (name == "")
1333 name = " "; // WARNING missing filename
1334
1335 var dir = Path.GetDirectoryName(root).Replace('\\', '/');
1336
1337 if (dir == "")
1338 dir = " "; // WARNING missing directoryname
1339
1340 AddEntry(dir, name, ext, File.ReadAllBytes(path));
1341 }
1342 }
1343
1344 public void Filter(string types, string paths = null, string names = null)
1345 {
1346 var fTypes = String.IsNullOrEmpty(types) ? new List<string>() : types.Split(',').Select(_ => _.Trim()).ToList();
1347 var fPaths = String.IsNullOrEmpty(paths) ? new List<string>() : paths.Split(',').Select(_ => _.Trim()).ToList();
1348 var fNames = String.IsNullOrEmpty(names) ? new List<string>() : names.Split(',').Select(_ => _.Trim()).ToList();
1349
1350 foreach (string etype in Entries.Keys.ToList())
1351 {
1352 if (fTypes.Count > 0)
1353 {
1354 if (!fTypes.Contains(etype))
1355 {
1356 Entries.Remove(etype);
1357 continue;
1358 }
1359 }
1360
1361 if (fPaths.Count > 0)
1362 Entries[etype].RemoveAll(_ => !fPaths.Exists(_.DirectoryName.StartsWith));
1363
1364 if (fNames.Count > 0)
1365 Entries[etype].RemoveAll(_ => !fNames.Exists(_.FileName.Equals));
1366 }
1367 }
1368 }
1369
1370 public class PackageEntry
1371 {
1372 public string FileName { get; set; }
1373 public string DirectoryName { get; set; }
1374 public string TypeName { get; set; }
1375 public uint CRC32 { get; set; }
1376 public uint Length { get; set; }
1377 public uint Offset { get; set; }
1378 public ushort ArchiveIndex { get; set; }
1379 public uint TotalLength { get { return SmallData == null ? Length : Length + (uint)SmallData.Length; } }
1380 public byte[] SmallData { get; set; }
1381
1382 public string GetFileName()
1383 {
1384 return TypeName == " " ? FileName : FileName + "." + TypeName;
1385 }
1386
1387 public string GetFullPath()
1388 {
1389 return DirectoryName == " " ? GetFileName() : DirectoryName + Package.DirectorySeparatorChar + GetFileName();
1390 }
1391
1392 public override string ToString()
1393 {
1394 return String.Format("{0} crc=0x{1:x2} metadatasz={2} fnumber={3} ofs=0x{4:x2} sz={5}",
1395 GetFullPath(), CRC32, SmallData.Length, ArchiveIndex, Offset, Length);
1396 }
1397 }
1398
1399 public class ArchiveMD5SectionEntry
1400 {
1401 public uint ArchiveIndex { get; set; }
1402 public uint Offset { get; set; }
1403 public uint Length { get; set; }
1404 public byte[] Checksum { get; set; }
1405 }
1406
1407 public static class Crc32
1408 {
1409 // CRC polynomial 0xEDB88320.
1410 private static readonly uint[] Table =
1411 {
1412 0x00000000, 0x77073096, 0xEE0E612C, 0x990951BA, 0x076DC419, 0x706AF48F, 0xE963A535, 0x9E6495A3, 0x0EDB8832, 0x79DCB8A4,
1413 0xE0D5E91E, 0x97D2D988, 0x09B64C2B, 0x7EB17CBD, 0xE7B82D07, 0x90BF1D91, 0x1DB71064, 0x6AB020F2, 0xF3B97148, 0x84BE41DE,
1414 0x1ADAD47D, 0x6DDDE4EB, 0xF4D4B551, 0x83D385C7, 0x136C9856, 0x646BA8C0, 0xFD62F97A, 0x8A65C9EC, 0x14015C4F, 0x63066CD9,
1415 0xFA0F3D63, 0x8D080DF5, 0x3B6E20C8, 0x4C69105E, 0xD56041E4, 0xA2677172, 0x3C03E4D1, 0x4B04D447, 0xD20D85FD, 0xA50AB56B,
1416 0x35B5A8FA, 0x42B2986C, 0xDBBBC9D6, 0xACBCF940, 0x32D86CE3, 0x45DF5C75, 0xDCD60DCF, 0xABD13D59, 0x26D930AC, 0x51DE003A,
1417 0xC8D75180, 0xBFD06116, 0x21B4F4B5, 0x56B3C423, 0xCFBA9599, 0xB8BDA50F, 0x2802B89E, 0x5F058808, 0xC60CD9B2, 0xB10BE924,
1418 0x2F6F7C87, 0x58684C11, 0xC1611DAB, 0xB6662D3D, 0x76DC4190, 0x01DB7106, 0x98D220BC, 0xEFD5102A, 0x71B18589, 0x06B6B51F,
1419 0x9FBFE4A5, 0xE8B8D433, 0x7807C9A2, 0x0F00F934, 0x9609A88E, 0xE10E9818, 0x7F6A0DBB, 0x086D3D2D, 0x91646C97, 0xE6635C01,
1420 0x6B6B51F4, 0x1C6C6162, 0x856530D8, 0xF262004E, 0x6C0695ED, 0x1B01A57B, 0x8208F4C1, 0xF50FC457, 0x65B0D9C6, 0x12B7E950,
1421 0x8BBEB8EA, 0xFCB9887C, 0x62DD1DDF, 0x15DA2D49, 0x8CD37CF3, 0xFBD44C65, 0x4DB26158, 0x3AB551CE, 0xA3BC0074, 0xD4BB30E2,
1422 0x4ADFA541, 0x3DD895D7, 0xA4D1C46D, 0xD3D6F4FB, 0x4369E96A, 0x346ED9FC, 0xAD678846, 0xDA60B8D0, 0x44042D73, 0x33031DE5,
1423 0xAA0A4C5F, 0xDD0D7CC9, 0x5005713C, 0x270241AA, 0xBE0B1010, 0xC90C2086, 0x5768B525, 0x206F85B3, 0xB966D409, 0xCE61E49F,
1424 0x5EDEF90E, 0x29D9C998, 0xB0D09822, 0xC7D7A8B4, 0x59B33D17, 0x2EB40D81, 0xB7BD5C3B, 0xC0BA6CAD, 0xEDB88320, 0x9ABFB3B6,
1425 0x03B6E20C, 0x74B1D29A, 0xEAD54739, 0x9DD277AF, 0x04DB2615, 0x73DC1683, 0xE3630B12, 0x94643B84, 0x0D6D6A3E, 0x7A6A5AA8,
1426 0xE40ECF0B, 0x9309FF9D, 0x0A00AE27, 0x7D079EB1, 0xF00F9344, 0x8708A3D2, 0x1E01F268, 0x6906C2FE, 0xF762575D, 0x806567CB,
1427 0x196C3671, 0x6E6B06E7, 0xFED41B76, 0x89D32BE0, 0x10DA7A5A, 0x67DD4ACC, 0xF9B9DF6F, 0x8EBEEFF9, 0x17B7BE43, 0x60B08ED5,
1428 0xD6D6A3E8, 0xA1D1937E, 0x38D8C2C4, 0x4FDFF252, 0xD1BB67F1, 0xA6BC5767, 0x3FB506DD, 0x48B2364B, 0xD80D2BDA, 0xAF0A1B4C,
1429 0x36034AF6, 0x41047A60, 0xDF60EFC3, 0xA867DF55, 0x316E8EEF, 0x4669BE79, 0xCB61B38C, 0xBC66831A, 0x256FD2A0, 0x5268E236,
1430 0xCC0C7795, 0xBB0B4703, 0x220216B9, 0x5505262F, 0xC5BA3BBE, 0xB2BD0B28, 0x2BB45A92, 0x5CB36A04, 0xC2D7FFA7, 0xB5D0CF31,
1431 0x2CD99E8B, 0x5BDEAE1D, 0x9B64C2B0, 0xEC63F226, 0x756AA39C, 0x026D930A, 0x9C0906A9, 0xEB0E363F, 0x72076785, 0x05005713,
1432 0x95BF4A82, 0xE2B87A14, 0x7BB12BAE, 0x0CB61B38, 0x92D28E9B, 0xE5D5BE0D, 0x7CDCEFB7, 0x0BDBDF21, 0x86D3D2D4, 0xF1D4E242,
1433 0x68DDB3F8, 0x1FDA836E, 0x81BE16CD, 0xF6B9265B, 0x6FB077E1, 0x18B74777, 0x88085AE6, 0xFF0F6A70, 0x66063BCA, 0x11010B5C,
1434 0x8F659EFF, 0xF862AE69, 0x616BFFD3, 0x166CCF45, 0xA00AE278, 0xD70DD2EE, 0x4E048354, 0x3903B3C2, 0xA7672661, 0xD06016F7,
1435 0x4969474D, 0x3E6E77DB, 0xAED16A4A, 0xD9D65ADC, 0x40DF0B66, 0x37D83BF0, 0xA9BCAE53, 0xDEBB9EC5, 0x47B2CF7F, 0x30B5FFE9,
1436 0xBDBDF21C, 0xCABAC28A, 0x53B39330, 0x24B4A3A6, 0xBAD03605, 0xCDD70693, 0x54DE5729, 0x23D967BF, 0xB3667A2E, 0xC4614AB8,
1437 0x5D681B02, 0x2A6F2B94, 0xB40BBE37, 0xC30C8EA1, 0x5A05DF1B, 0x2D02EF8D
1438 };
1439
1440 public static uint Compute(byte[] buffer)
1441 {
1442 return ~buffer.Aggregate(0xFFFFFFFF, (current, t) => (current >> 8) ^ Table[t ^ (current & 0xff)]);
1443 }
1444 }
1445
1446}
1447
1448