diff --git a/.gitignore b/.gitignore index ffdc51e..15d51f9 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,7 @@ test/ -env/ \ No newline at end of file +env/ +PakTool/.vs/ +PakTool/bin/ +PakTool/obj/ +PakTool/PAKTool.pdb +*ref.hx \ No newline at end of file diff --git a/PAKTool/AssemblyInfo.cs b/PAKTool/AssemblyInfo.cs new file mode 100644 index 0000000..2cdd198 --- /dev/null +++ b/PAKTool/AssemblyInfo.cs @@ -0,0 +1,14 @@ +using System.Reflection; +using System.Runtime.InteropServices; + +[assembly: AssemblyTitle("PAKTool")] +[assembly: AssemblyDescription("")] +[assembly: AssemblyConfiguration("")] +[assembly: AssemblyCompany("")] +[assembly: AssemblyProduct("PAKTool")] +[assembly: AssemblyCopyright("Copyright © 2018")] +[assembly: AssemblyTrademark("")] +[assembly: ComVisible(false)] +[assembly: Guid("72731be3-1523-4543-9287-f1205e754f6d")] +[assembly: AssemblyFileVersion("1.0.0.0")] +[assembly: AssemblyVersion("1.0.0.0")] diff --git a/PAKTool/DirectoryData.cs b/PAKTool/DirectoryData.cs new file mode 100644 index 0000000..433ac8c --- /dev/null +++ b/PAKTool/DirectoryData.cs @@ -0,0 +1,33 @@ +// Decompiled with JetBrains decompiler +// Type: PAKTool.DirectoryData +// Assembly: PAKTool, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null +// MVID: ECC9A4AB-62C6-4F7C-95CE-FBC3CF0F40A2 +// Assembly location: D:\SteamLibrary\steamapps\common\Dead Cells\ModTools\PAKTool.exe + +using System.Collections.Generic; + +#nullable disable +namespace PAKTool +{ + internal class DirectoryData : EntryData + { + public override bool isDirectory => true; + + public DirectoryData(DirectoryData _parent, string _name) + : base(_parent, _name) + { + } + + public void AddEntry(EntryData _entry) + { + if (_entry.isDirectory) + this.directories.Add((DirectoryData) _entry); + else + this.files.Add((FileData) _entry); + } + + public List files { get; private set; } = new List(); + + public List directories { get; private set; } = new List(); + } +} diff --git a/PAKTool/EntryData.cs b/PAKTool/EntryData.cs new file mode 100644 index 0000000..5b2518b --- /dev/null +++ b/PAKTool/EntryData.cs @@ -0,0 +1,35 @@ +// Decompiled with JetBrains decompiler +// Type: PAKTool.EntryData +// Assembly: PAKTool, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null +// MVID: ECC9A4AB-62C6-4F7C-95CE-FBC3CF0F40A2 +// Assembly location: D:\SteamLibrary\steamapps\common\Dead Cells\ModTools\PAKTool.exe + +using System.IO; + +#nullable disable +namespace PAKTool +{ + internal abstract class EntryData + { + public abstract bool isDirectory { get; } + + public string name { get; private set; } + + public string fullName + { + get + { + DirectoryData parent = this.parent; + return parent != null ? Path.Combine(parent.fullName, this.name) : this.name; + } + } + + public DirectoryData parent { get; private set; } + + public EntryData(DirectoryData _parent, string _name) + { + this.name = _name; + this.parent = _parent; + } + } +} diff --git a/PAKTool/FileData.cs b/PAKTool/FileData.cs new file mode 100644 index 0000000..184e564 --- /dev/null +++ b/PAKTool/FileData.cs @@ -0,0 +1,28 @@ +// Decompiled with JetBrains decompiler +// Type: PAKTool.FileData +// Assembly: PAKTool, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null +// MVID: ECC9A4AB-62C6-4F7C-95CE-FBC3CF0F40A2 +// Assembly location: D:\SteamLibrary\steamapps\common\Dead Cells\ModTools\PAKTool.exe + +#nullable disable +namespace PAKTool +{ + internal class FileData : EntryData + { + public int position { get; private set; } + + public int size { get; private set; } + + public int checksum { get; private set; } + + public override bool isDirectory => false; + + public FileData(DirectoryData _parent, string _name, int _position, int _size, int _crc) + : base(_parent, _name) + { + this.position = _position; + this.size = _size; + this.checksum = _crc; + } + } +} diff --git a/PAKTool/PAKTool.cs b/PAKTool/PAKTool.cs new file mode 100644 index 0000000..a5e192f --- /dev/null +++ b/PAKTool/PAKTool.cs @@ -0,0 +1,292 @@ +// Decompiled with JetBrains decompiler +// Type: PAKTool.PAKTool +// Assembly: PAKTool, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null +// MVID: ECC9A4AB-62C6-4F7C-95CE-FBC3CF0F40A2 +// Assembly location: D:\SteamLibrary\steamapps\common\Dead Cells\ModTools\PAKTool.exe + +using ModTools; +using System; +using System.Collections.Generic; +using System.IO; + +#nullable disable +namespace PAKTool +{ + internal class PAKTool + { + private Dictionary headerData = new Dictionary(); + + public byte version { get; private set; } + + public DirectoryData root { get; private set; } + + public void ExpandPAK(string _pakPath, string _destination) + { + if (!File.Exists(_pakPath)) + throw new FileNotFoundException("File not found: " + _pakPath, _pakPath); + DirectoryInfo directory = Directory.CreateDirectory(_destination); + BinaryReader _reader = this.ReadPAKHeader(_pakPath); + this.CreateTree(_reader, directory, this.root); + _reader.Close(); + } + + public void BuildPAK(string _expandedPakPath, string _destination) + { + DirectoryInfo directoryInfo = new DirectoryInfo(_expandedPakPath); + if (!directoryInfo.Exists) + throw new DirectoryNotFoundException("Directory " + _expandedPakPath + " not found"); + FileInfo fileInfo = new FileInfo(_destination); + fileInfo.Directory.Create(); + this.root = new DirectoryData((DirectoryData)null, ""); + this.dataSize = 0; + this.headerSize = 0; + this.version = (byte)0; + this.CreatePAKDirectory(this.root, directoryInfo); + this.headerSize += 16; + BinaryWriter _writer = new BinaryWriter((Stream)fileInfo.Create()); + _writer.Write(new char[3] { 'P', 'A', 'K' }); + _writer.Write(this.version); + _writer.Write(this.headerSize); + _writer.Write(this.dataSize); + this.WritePAKEntry(_writer, (EntryData)this.root); + _writer.Write(new char[4] { 'D', 'A', 'T', 'A' }); + this.WritePAKContent(_writer, directoryInfo); + _writer.Close(); + } + + public void BuildPAKStamped(string _expandedPakPath, string _destination, string stamp) + { + if (stamp.Length != 64) + { + throw new FormatException("Stamp should be 64 bytes long."); + } + + DirectoryInfo directoryInfo = new DirectoryInfo(_expandedPakPath); + if (!directoryInfo.Exists) + throw new DirectoryNotFoundException("Directory " + _expandedPakPath + " not found"); + + FileInfo fileInfo = new FileInfo(_destination); + fileInfo.Directory.Create(); + + this.root = new DirectoryData((DirectoryData)null, ""); + this.dataSize = 0; + this.headerSize = 0; + this.version = (byte)1; + this.CreatePAKDirectory(this.root, directoryInfo); + + this.headerSize += 16 + 64; // extra 64 for the stamp + + using (BinaryWriter _writer = new BinaryWriter(fileInfo.Create())) + { + _writer.Write(new char[3] { 'P', 'A', 'K' }); + _writer.Write(this.version); + _writer.Write(this.headerSize); + _writer.Write(this.dataSize); + + // HACK: writing without conversion breaks things, somehow + byte[] stampBytes = System.Text.Encoding.ASCII.GetBytes(stamp); + _writer.Write(stampBytes); + + this.WritePAKEntry(_writer, (EntryData)this.root); + _writer.Write(new char[4] { 'D', 'A', 'T', 'A' }); + this.WritePAKContent(_writer, directoryInfo); + } + } + + public void BuildDiffPAK(string _referencePAK, string _inputDir, string _diffPAKPath) + { + DirectoryInfo directoryInfo1 = new DirectoryInfo(_inputDir); + if (!directoryInfo1.Exists) + throw new DirectoryNotFoundException("Directory " + _inputDir + " not found."); + BinaryReader binaryReader = this.ReadPAKHeader(_referencePAK); + Dictionary dictionary1 = new Dictionary(); + List directoryDataList = new List(); + directoryDataList.Add(this.root); + for (int index = 0; index < directoryDataList.Count; ++index) + { + foreach (FileData file in directoryDataList[index].files) + dictionary1.Add(file.fullName, file.checksum); + foreach (DirectoryData directory in directoryDataList[index].directories) + directoryDataList.Add(directory); + } + Dictionary dictionary2 = new Dictionary(); + Adler32 adler32 = new Adler32(); + if (_inputDir.Length > 0 && _inputDir[_inputDir.Length - 1] != '\\') + _inputDir += "\\"; + foreach (FileInfo file in directoryInfo1.GetFiles("*.*", SearchOption.AllDirectories)) + dictionary2.Add(file.FullName.Replace(_inputDir, ""), adler32.Make((Stream)File.OpenRead(file.FullName))); + List stringList = new List(); + DirectoryInfo directoryInfo2 = new DirectoryInfo(Path.Combine(Path.GetTempPath(), Path.GetRandomFileName())); + directoryInfo2.Create(); + bool flag = false; + foreach (KeyValuePair keyValuePair in dictionary2) + { + int num; + if (dictionary1.TryGetValue(keyValuePair.Key, out num)) + { + if (num != keyValuePair.Value) + { + if (keyValuePair.Key.Substring(keyValuePair.Key.Length - 4, 4).ToUpper() == ".CDB") + { + CDBTool.CDBTool cdbTool = new CDBTool.CDBTool(); + DirectoryInfo directoryInfo3 = new DirectoryInfo(Path.Combine(Path.GetTempPath(), Path.GetRandomFileName())); + directoryInfo3.Create(); + string str = Path.Combine(directoryInfo3.FullName, keyValuePair.Key); + EntryData entryData; + if (!this.headerData.TryGetValue(keyValuePair.Key, out entryData)) + throw new Exception("original CDB called " + keyValuePair.Key + " not found."); + FileData fileData = (FileData)entryData; + FileStream fileStream = new FileStream(str, FileMode.OpenOrCreate); + binaryReader.BaseStream.Seek((long)fileData.position, SeekOrigin.Begin); + fileStream.Write(binaryReader.ReadBytes(fileData.size), 0, fileData.size); + fileStream.Close(); + DirectoryInfo directoryInfo4 = new DirectoryInfo(Path.Combine(Path.GetTempPath(), Path.GetRandomFileName())); + cdbTool.Expand(Path.Combine(_inputDir, keyValuePair.Key), directoryInfo4.FullName); + DirectoryInfo directoryInfo5 = new DirectoryInfo(Path.Combine(directoryInfo2.FullName, keyValuePair.Key + "_")); + directoryInfo5.Create(); + cdbTool.BuildDiffCDB(str, directoryInfo4.FullName, directoryInfo5.FullName); + flag = true; + } + else + stringList.Add(Path.Combine(directoryInfo2.FullName, keyValuePair.Key)); + } + } + else + stringList.Add(Path.Combine(directoryInfo2.FullName, keyValuePair.Key)); + } + binaryReader.Close(); + if (stringList.Count == 0 && !flag) + throw new Exception("No diff pak created, no changed or added files found."); + foreach (string fileName in stringList) + { + FileInfo fileInfo = new FileInfo(fileName); + fileInfo.Directory.Create(); + File.Copy(Path.Combine(_inputDir, fileName.Replace(directoryInfo2.FullName, "").Substring(1)), fileInfo.FullName); + } + this.BuildPAK(directoryInfo2.FullName, _diffPAKPath); + directoryInfo2.Delete(true); + } + + private BinaryReader ReadPAKHeader(string _pakPath) + { + this.headerData.Clear(); + BinaryReader _reader = new BinaryReader((Stream)File.OpenRead(_pakPath)); + string str = new string(_reader.ReadChars(3)); + this.version = _reader.ReadByte(); + this.headerSize = _reader.ReadInt32(); + this.dataSize = _reader.ReadInt32(); + if (this.version >= (byte)1) + _reader.ReadChars(64); + this.root = (DirectoryData)this.ReadPAKEntry(_reader, (DirectoryData)null); + return _reader; + } + + private void CreatePAKDirectory(DirectoryData _pakDirectory, DirectoryInfo _physicalDirectory) + { + ++this.headerSize; + this.headerSize += _pakDirectory.parent == null ? 0 : _physicalDirectory.Name.Length; + ++this.headerSize; + this.headerSize += 4; + foreach (DirectoryInfo directory in _physicalDirectory.GetDirectories()) + { + DirectoryData directoryData = new DirectoryData(_pakDirectory, directory.Name); + _pakDirectory.AddEntry((EntryData)directoryData); + this.CreatePAKDirectory(directoryData, directory); + } + Adler32 adler32 = new Adler32(); + foreach (FileInfo file in _physicalDirectory.GetFiles()) + { + ++this.headerSize; + this.headerSize += file.Name.Length; + ++this.headerSize; + this.headerSize += 12; + Stream _stream = (Stream)file.OpenRead(); + FileData _entry = new FileData(_pakDirectory, file.Name, this.dataSize, (int)file.Length, adler32.Make(_stream)); + _pakDirectory.AddEntry((EntryData)_entry); + this.dataSize += (int)file.Length; + _stream.Close(); + } + } + + private void CreateTree( + BinaryReader _reader, + DirectoryInfo _rootDir, + DirectoryData _currentDir) + { + if (_currentDir.name != "") + _rootDir.CreateSubdirectory(_currentDir.fullName); + foreach (DirectoryData directory in _currentDir.directories) + this.CreateTree(_reader, _rootDir, directory); + foreach (FileData file in _currentDir.files) + { + FileStream fileStream = new FileStream(Path.Combine(_rootDir.FullName, file.fullName), FileMode.Create); + _reader.BaseStream.Seek((long)file.position, SeekOrigin.Begin); + fileStream.Write(_reader.ReadBytes(file.size), 0, file.size); + fileStream.Close(); + } + } + + private EntryData ReadPAKEntry(BinaryReader _reader, DirectoryData _parent) + { + string _name = new string(_reader.ReadChars((int)_reader.ReadByte())); + EntryData entryData; + if (((int)_reader.ReadByte() & 1) != 0) + { + DirectoryData _parent1 = new DirectoryData(_parent, _name); + int num = _reader.ReadInt32(); + for (int index = 0; index < num; ++index) + { + EntryData _entry = this.ReadPAKEntry(_reader, _parent1); + _parent1.AddEntry(_entry); + this.headerData.Add(_entry.fullName, _entry); + } + entryData = (EntryData)_parent1; + } + else + entryData = (EntryData)new FileData(_parent, _name, this.headerSize + _reader.ReadInt32(), _reader.ReadInt32(), _reader.ReadInt32()); + return entryData; + } + + private void WritePAKEntry(BinaryWriter _writer, EntryData _entry) + { + byte length = (byte)_entry.name.Length; + _writer.Write(length); + if (length > (byte)0) + _writer.Write(_entry.name.ToCharArray()); + if (_entry.isDirectory) + { + DirectoryData directoryData = (DirectoryData)_entry; + _writer.Write((byte)1); + _writer.Write(directoryData.directories.Count + directoryData.files.Count); + foreach (DirectoryData directory in directoryData.directories) + this.WritePAKEntry(_writer, (EntryData)directory); + foreach (FileData file in directoryData.files) + this.WritePAKEntry(_writer, (EntryData)file); + } + else + { + FileData fileData = (FileData)_entry; + _writer.Write((byte)0); + _writer.Write(fileData.position); + _writer.Write(fileData.size); + _writer.Write(fileData.checksum); + } + } + + private void WritePAKContent(BinaryWriter _writer, DirectoryInfo _dir) + { + foreach (DirectoryInfo directory in _dir.GetDirectories()) + this.WritePAKContent(_writer, directory); + foreach (FileInfo file in _dir.GetFiles()) + { + BinaryReader binaryReader = new BinaryReader((Stream)file.OpenRead()); + _writer.Write(binaryReader.ReadBytes((int)file.Length)); + binaryReader.Close(); + } + } + + private int headerSize { get; set; } + + private int dataSize { get; set; } + } +} diff --git a/PAKTool/PAKTool.csproj b/PAKTool/PAKTool.csproj new file mode 100644 index 0000000..b9c1997 --- /dev/null +++ b/PAKTool/PAKTool.csproj @@ -0,0 +1,54 @@ + + + + + 8.0 + + + Debug + AnyCPU + {4384B575-BF54-4912-8AB9-2B13C5D8AC89} + Exe + PAKTool + v4.5.2 + 1.0.0.0 + 512 + PAKTool + + + AnyCPU + true + full + false + bin\Debug\ + DEBUG;TRACE + prompt + 4 + + + AnyCPU + pdbonly + true + bin\Release\ + TRACE + prompt + 4 + + + + lib\CDBTool.exe + + + lib\Common.dll + + + + + + + + + + + + \ No newline at end of file diff --git a/PAKTool/PAKTool.sln b/PAKTool/PAKTool.sln new file mode 100644 index 0000000..241a729 --- /dev/null +++ b/PAKTool/PAKTool.sln @@ -0,0 +1,20 @@ + +Microsoft Visual Studio Solution File, Format Version 11.00 +# Visual Studio 2010 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "PAKTool", "PAKTool.csproj", "{4384B575-BF54-4912-8AB9-2B13C5D8AC89}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {4384B575-BF54-4912-8AB9-2B13C5D8AC89}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {4384B575-BF54-4912-8AB9-2B13C5D8AC89}.Debug|Any CPU.Build.0 = Debug|Any CPU + {4384B575-BF54-4912-8AB9-2B13C5D8AC89}.Release|Any CPU.ActiveCfg = Release|Any CPU + {4384B575-BF54-4912-8AB9-2B13C5D8AC89}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection +EndGlobal diff --git a/PAKTool/Program.cs b/PAKTool/Program.cs new file mode 100644 index 0000000..35f2620 --- /dev/null +++ b/PAKTool/Program.cs @@ -0,0 +1,147 @@ +using ModTools; +using System; +using System.Globalization; +using System.Threading; + +namespace PAKTool +{ + internal class Program + { + static readonly string DEFAULT_STAMP = "f5601c10de243a6dcc424121ee4bcc2b9b36323a15a0dd00312f61b3e704c71f"; // for game commit f8349ab + + private static void Main(string[] args) + { + Console.WriteLine($"----- N3rdL0rd's PakTool v0.1a (based on ModTools v{Versionning.currentVersion}) -----"); + Thread.CurrentThread.CurrentUICulture = CultureInfo.InvariantCulture; + + if (args.Length == 0 || args[0] == "--help" || args[0] == "-h") + { + DisplayHelp(); + Environment.Exit(0); + } + + string action = args[0].ToLowerInvariant(); + string inputDir = string.Empty; + string outputDir = string.Empty; + string refPak = string.Empty; + string outputPak = string.Empty; + string stamp = Program.DEFAULT_STAMP; + bool showErrors = false; + + try + { + switch (action) + { + case "expand": + if (args.Length < 3) + throw new ArgumentException("Usage: expand "); + outputDir = args[1]; + refPak = args[2]; + break; + + case "collapse": + if (args.Length < 3) + throw new ArgumentException("Usage: collapse "); + inputDir = args[1]; + outputPak = args[2]; + break; + + case "collapsev1": + if (args.Length < 3) + throw new ArgumentException("Usage: collapsev1 "); + inputDir = args[1]; + outputPak = args[2]; + ParseOptions(args, ref showErrors, ref stamp); + break; + + case "creatediffpak": + if (args.Length < 4) + throw new ArgumentException("Usage: creatediffpak "); + refPak = args[1]; + inputDir = args[2]; + outputPak = args[3]; + break; + + default: + Console.WriteLine($"The action \"{action}\" is not recognized. Please refer to the documentation."); + DisplayHelp(); + Environment.Exit(1); + break; + } + + ExecuteAction(action, inputDir, outputDir, refPak, outputPak, stamp, showErrors); + } + catch (Exception ex) + { + Error.Show(ex, showErrors); + } + } + + private static void ParseOptions(string[] args, ref bool showErrors, ref string stamp) + { + for (int i = 3; i < args.Length; i++) + { + string arg = args[i]; + + switch (arg.ToLowerInvariant()) + { + case "--popup-errors": + case "-p": + showErrors = true; + break; + case "--stamp": + case "-s": + if (i + 1 < args.Length) + { + stamp = args[++i]; + } + else + { + throw new ArgumentException("The --stamp/-s option requires a value."); + } + break; + } + } + } + + private static void DisplayHelp() + { + Console.WriteLine("Usage:"); + Console.WriteLine(" expand Expands a PAK of any version into a directory"); + Console.WriteLine(" collapse Creates a v0 PAK from a directory, but is broken past v35"); + Console.WriteLine(" collapsev1 Creates a v1 PAK from a directory, bypassing the stamp verification"); + Console.WriteLine(" creatediffpak Creates a v0 diff PAK, completely unmodified from the base tool"); + Console.WriteLine("Options:"); + Console.WriteLine(" -p, --popup-errors Pop up a message box upon errors occuring, similarly to how the original PakTool did"); + Console.WriteLine(" -s, --stamp Specify a custom stamp to use to bypass PAK authenticity verification past v35"); + Console.WriteLine(" -h, --help Display this help message"); + } + + private static void ExecuteAction(string action, string inputDir, string outputDir, string refPak, string outputPak, string stamp, bool showErrors) + { + PAKTool pakTool = new PAKTool(); + switch (action) + { + case "expand": + Console.WriteLine("Expanding PAK..."); + pakTool.ExpandPAK(refPak, outputDir); + break; + case "collapse": + Console.WriteLine("Collapsing PAK..."); + pakTool.BuildPAK(inputDir, outputPak); + break; + case "collapsev1": + Console.WriteLine($"Repacking PAK... (with stamp {stamp})"); + pakTool.BuildPAKStamped(inputDir, outputPak, stamp); + break; + case "creatediffpak": + Console.WriteLine("Creating Diff PAK..."); + pakTool.BuildDiffPAK(refPak, inputDir, outputPak); + break; + default: + Console.WriteLine($"The action \"{action}\" is not recognized. Please refer to the documentation."); + break; + } + } + } +} diff --git a/PAKTool/lib/CDBTool.exe b/PAKTool/lib/CDBTool.exe new file mode 100644 index 0000000..a3e53c6 Binary files /dev/null and b/PAKTool/lib/CDBTool.exe differ diff --git a/PAKTool/lib/Common.dll b/PAKTool/lib/Common.dll new file mode 100644 index 0000000..cc625dc Binary files /dev/null and b/PAKTool/lib/Common.dll differ diff --git a/README.md b/README.md index 147d786..9de1bd2 100644 --- a/README.md +++ b/README.md @@ -62,6 +62,8 @@ options: --in-game-dir Save the Hashlink bytecode in the Dead Cells game directory as well as the current directory. ``` +Other tools are provided as standalone executables, such as the patched PAKTool, which you can either clone and build yourself, or get from the [Releases](https://github.com/N3rdL0rd/alivecells/releases). + ## Known Issues - When running in a seperate directory than the original Steam installation, the game will fail to sync Steam Cloud saves. This can be fixed by symlinking the `save` directory in the game directory to the original Steam installation directory, but this is only a temporary fix - a proper solution is being worked on.