diff --git a/.gitignore b/.gitignore index 5e56e04..d38bd2c 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,4 @@ -/bin +/godotdec/bin +/legacy/bin +/godotdec/obj +/.vs diff --git a/README.md b/README.md index 9631007..48a039d 100644 --- a/README.md +++ b/README.md @@ -3,9 +3,16 @@ A simple unpacker for Godot Engine package files (.pck) ### Usage -`godotdec []` +`godotdec [] []` + +###### Options: + +| Flag (short) | Flag (long) | Description | +| ------------ | ----------- | ------------------------------------------------------------ | +| -c | --convert | Convert certain engine-specific file types (textures, some audio streams) to standard formats. | ### Technical details + Godot Engine's package format is specified as: | Value/Type | Description | @@ -27,8 +34,8 @@ The source code of the .pck packer can be found [here](https://github.com/godote ### Limitations - Modified engine versions may use a custom package format, which godotdec does not support -- Very big files might not be unpacked correctly, but that's untested for now - MD5 checksum is not used to verify extracted files +- Format conversion is only supported for .png, .ogg ### Remarks diff --git a/godotdec.sln b/godotdec.sln new file mode 100644 index 0000000..80f2f30 --- /dev/null +++ b/godotdec.sln @@ -0,0 +1,25 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 16 +VisualStudioVersion = 16.0.29123.88 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "godotdec", "godotdec\godotdec.csproj", "{3C36505F-9F80-4B13-B212-446B463689A7}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {3C36505F-9F80-4B13-B212-446B463689A7}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {3C36505F-9F80-4B13-B212-446B463689A7}.Debug|Any CPU.Build.0 = Debug|Any CPU + {3C36505F-9F80-4B13-B212-446B463689A7}.Release|Any CPU.ActiveCfg = Release|Any CPU + {3C36505F-9F80-4B13-B212-446B463689A7}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {E6A83782-A391-42A0-85A4-5287504E8E22} + EndGlobalSection +EndGlobal diff --git a/godotdec.sln.DotSettings.user b/godotdec.sln.DotSettings.user new file mode 100644 index 0000000..44a54de --- /dev/null +++ b/godotdec.sln.DotSettings.user @@ -0,0 +1,2 @@ + + 2 \ No newline at end of file diff --git a/godotdec/App.config b/godotdec/App.config new file mode 100644 index 0000000..2d2a12d --- /dev/null +++ b/godotdec/App.config @@ -0,0 +1,6 @@ + + + + + + diff --git a/godotdec/Bio.cs b/godotdec/Bio.cs new file mode 100644 index 0000000..e45b35c --- /dev/null +++ b/godotdec/Bio.cs @@ -0,0 +1,138 @@ +using System; +using System.Collections.Generic; +using System.IO; + +namespace cicdec { + public static class Bio { + public const string SEPERATOR = "\n---------------------------------------------------------------------"; + private static readonly Dictionary promptSettings = new Dictionary(); + + public static void CopyStream(Stream input, Stream output, int bytes = -1, bool keepPosition = true, int bufferSize = 1024) { + var buffer = new byte[bufferSize]; + long initialPosition = 0; + if (keepPosition) initialPosition = input.Position; + int read; + if (bytes < 1) bytes = (int) (input.Length - input.Position); + + while (bytes > 0 && (read = input.Read(buffer, 0, Math.Min(bufferSize, bytes))) > 0) { + output.Write(buffer, 0, read); + bytes -= read; + } + + if (keepPosition) input.Seek(initialPosition, SeekOrigin.Begin); + } + + public static string FileReplaceInvalidChars(string filename, string by = "_") { + return string.Join(by, filename.Split(Path.GetInvalidPathChars())); + } + + public static void Header(string name, string version, string year, string description, string usage = "") { + var header = string.Format("{0} by Bioruebe (https://bioruebe.com), {1}, Version {2}, Released under a BSD 3-Clause style license\n\n{3}{4}\n{5}", name, year, version, description, (usage == null ? "" : "\n\nUsage: " + GetProgramName() + " " + usage), SEPERATOR); + + Console.WriteLine(header); + } + + public static void Seperator() { + Console.WriteLine(SEPERATOR + "\n"); + } + + public static string GetProgramName() { + return System.Diagnostics.Process.GetCurrentProcess().ProcessName; + } + + public static bool Prompt(string msg, string id = "", string choices = + "[Y]es | [N]o | [A]lways | n[E]ver", string chars = "ynae") { + // Check setting from previous function calls + promptSettings.TryGetValue(id, out var setting); + if (setting == PROMPT_SETTING.ALWAYS) return true; + if (setting == PROMPT_SETTING.NEVER) return false; + + var aChars = chars.ToCharArray(); + int input; + + while (true) { + Console.WriteLine(msg + $" {choices}"); + input = Console.ReadKey().KeyChar; + Console.WriteLine(); + + if (input == aChars[0]) return true; + if (input == aChars[1]) return false; + if (input == aChars[2]) { + promptSettings[id] = PROMPT_SETTING.ALWAYS; + return true; + } + + if (input == aChars[3]) { + promptSettings[id] = PROMPT_SETTING.NEVER; + return false; + } + } + + } + + public static void Cout(string msg, LOG_SEVERITY logSeverity = LOG_SEVERITY.MESSAGE) { +#if !DEBUG + if (logSeverity == LOG_SEVERITY.DEBUG) return; +#endif + if (msg.StartsWith("\n")) { + Console.WriteLine(); + msg = msg.Substring(1); + } + + if (logSeverity != LOG_SEVERITY.MESSAGE) msg = string.Format("[{0}] {1}", logSeverity, msg); + + switch (logSeverity) { + case LOG_SEVERITY.ERROR: + case LOG_SEVERITY.CRITICAL: + Console.Error.WriteLine(); + Console.WriteLine(msg); + break; + default: + Console.WriteLine(msg); + break; + } + } + + public static void Cout(object msg, LOG_SEVERITY logSeverity = LOG_SEVERITY.MESSAGE) { + Cout(msg.ToString(), logSeverity); + } + + public static void Debug(object msg) { + Cout(msg, LOG_SEVERITY.DEBUG); + } + + public static void Warn(object msg) { + Cout(msg, LOG_SEVERITY.WARNING); + } + + public static void Error(object msg, int exitCode = -1) { + Cout(msg, LOG_SEVERITY.ERROR); +#if DEBUG + Console.ReadKey(false); +#endif + if (exitCode > -1) Environment.Exit(exitCode); + } + + public static void Pause() { +#if DEBUG + Console.ReadKey(); +#endif + } + + public enum LOG_SEVERITY { + DEBUG, + INFO, + WARNING, + ERROR, + CRITICAL, + MESSAGE, + UNITTEST + } + + public enum PROMPT_SETTING { + NONE, + ALWAYS, + NEVER + } + } +} \ No newline at end of file diff --git a/godotdec/Program.cs b/godotdec/Program.cs new file mode 100644 index 0000000..ae49572 --- /dev/null +++ b/godotdec/Program.cs @@ -0,0 +1,151 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using cicdec; + +namespace godotdec { + class Program { + static void Main(string[] aArgs) { + Bio.Header("godotdec", "2.0.0", "2018-2019", "A simple unpacker for Godot Engine package files (.pck|.exe)", + "[] []\n\nOptions:\n-c\t--convert\tConvert textures and audio files"); + + var args = aArgs.ToList(); + + if (args.Contains("-h") || args.Contains("--help") || args.Contains("/?") || args.Contains("-?") || + args.Contains("/h")) return; + + var convert = args.Remove("--convert") || args.Remove("-c"); + if (args.Count < 1) Bio.Error("Please specify the path to a Godot .pck or .exe file.", 1); + + var inputFile = args[0]; + if (!File.Exists(inputFile)) Bio.Error("The input file " + inputFile + " does not exist.", 1); + var outdir = args.Count > 1? args[1]: Path.Combine(Path.GetDirectoryName(inputFile), Path.GetFileNameWithoutExtension(inputFile)); + Bio.Debug("Input file: " + inputFile); + Bio.Debug("Output directory: " + outdir); + + var failed = 0; + using (var inputStream = new BinaryReader(File.Open(inputFile, FileMode.Open))) { + if (inputStream.ReadInt32() != 0x43504447) { + inputStream.BaseStream.Seek(-4, SeekOrigin.End); + + CheckMagic(inputStream.ReadInt32()); + + inputStream.BaseStream.Seek(-12, SeekOrigin.Current); + var offset = inputStream.ReadInt64(); + inputStream.BaseStream.Seek(-offset - 8, SeekOrigin.Current); + + CheckMagic(inputStream.ReadInt32()); + } + + Bio.Cout($"Godot Engine version: {inputStream.ReadInt32()}.{inputStream.ReadInt32()}.{inputStream.ReadInt32()}.{inputStream.ReadInt32()}"); + + // Skip reserved bytes (16x Int32) + inputStream.BaseStream.Seek(16 * 4, SeekOrigin.Current); + + var fileCount = inputStream.ReadInt32(); + Bio.Cout($"Found {fileCount} files in package"); + Bio.Cout("Reading file index"); + + var fileIndex = new List(); + for (var i = 0; i < fileCount; i++) { + var pathLength = inputStream.ReadInt32(); + var path = Encoding.UTF8.GetString(inputStream.ReadBytes(pathLength)); + var fileEntry = new FileEntry(path.ToString(), inputStream.ReadInt64(), inputStream.ReadInt64()); + fileIndex.Add(fileEntry); + Bio.Debug(fileEntry); + inputStream.BaseStream.Seek(16, SeekOrigin.Current); + //break; + } + + if (fileIndex.Count < 1) Bio.Error("No files were found inside the archive", 2); + fileIndex.Sort((a, b) => (int) (a.offset - b.offset)); + + var fileIndexEnd = inputStream.BaseStream.Position; + for (var i = 0; i < fileIndex.Count; i++) { + var fileEntry = fileIndex[i]; + Bio.Cout($"{i+1}/{fileIndex.Count}\t{fileEntry.path}"); + //break; + if (fileEntry.offset < fileIndexEnd) { + Bio.Warn("Invalid file offset: " + fileEntry.offset); + continue; + } + + // TODO: Only PNG compression is supported + if (convert) { + // https://github.com/godotengine/godot/blob/master/editor/import/resource_importer_texture.cpp#L222 + if (fileEntry.path.EndsWith(".stex") && fileEntry.path.Contains(".png")) { + fileEntry.Resize(32); + fileEntry.ChangeExtension(".stex", ".png"); + Bio.Debug(fileEntry); + } + // https://github.com/godotengine/godot/blob/master/core/io/resource_format_binary.cpp#L836 + else if (fileEntry.path.EndsWith(".oggstr")) { + fileEntry.Resize(279, 4); + fileEntry.ChangeExtension(".oggstr", ".ogg"); + } + // https://github.com/godotengine/godot/blob/master/scene/resources/audio_stream_sample.cpp#L518 + else if (fileEntry.path.EndsWith(".sample")) { + // TODO + } + } + + var destination = Path.Combine(outdir, Bio.FileReplaceInvalidChars(fileEntry.path)); + inputStream.BaseStream.Seek(fileEntry.offset, SeekOrigin.Begin); + + try { + var fileMode = FileMode.CreateNew; + Directory.CreateDirectory(Path.GetDirectoryName(destination)); + if (File.Exists(destination)) { + if (!Bio.Prompt($"The file {fileEntry.path} already exists. Overwrite?", "godotdec_overwrite")) continue; + fileMode = FileMode.Create; + } + using (var outputStream = new FileStream(destination, fileMode)) { + Bio.CopyStream(inputStream.BaseStream, outputStream, (int) fileEntry.size, false); + } + } + catch (Exception e) { + Bio.Error(e); + failed++; + } + } + } + + Bio.Cout(failed < 1? "All OK": failed + " files failed to extract"); + Bio.Pause(); + } + + static void CheckMagic(int magic) { + if (magic == 0x43504447) return; + Bio.Error("The input file is not a valid Godot package file.", 2); + } + } +} + +class FileEntry { + public string path; + public long offset; + public long size; + //public var md5:String; + + public FileEntry (string path, long offset, long size) { + this.path = path.Substring(6).TrimEnd('\0'); + this.offset = offset; + this.size = size; + } + + public void Resize(int by, int stripAtEnd = 0) { + offset += by; + size -= by + stripAtEnd; + } + + public void ChangeExtension(string from, string to) { + path = path.Replace(from, to); + } + + public override string ToString() { + return $"{offset:000000} {path}, {size}"; + } +} diff --git a/godotdec/Properties/AssemblyInfo.cs b/godotdec/Properties/AssemblyInfo.cs new file mode 100644 index 0000000..a6ae9ef --- /dev/null +++ b/godotdec/Properties/AssemblyInfo.cs @@ -0,0 +1,36 @@ +using System.Reflection; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; + +// Allgemeine Informationen über eine Assembly werden über die folgenden +// Attribute gesteuert. Ändern Sie diese Attributwerte, um die Informationen zu ändern, +// die einer Assembly zugeordnet sind. +[assembly: AssemblyTitle("godotdec")] +[assembly: AssemblyDescription("")] +[assembly: AssemblyConfiguration("")] +[assembly: AssemblyCompany("")] +[assembly: AssemblyProduct("godotdec")] +[assembly: AssemblyCopyright("Copyright © 2019")] +[assembly: AssemblyTrademark("")] +[assembly: AssemblyCulture("")] + +// Durch Festlegen von ComVisible auf FALSE werden die Typen in dieser Assembly +// für COM-Komponenten unsichtbar. Wenn Sie auf einen Typ in dieser Assembly von +// COM aus zugreifen müssen, sollten Sie das ComVisible-Attribut für diesen Typ auf "True" festlegen. +[assembly: ComVisible(false)] + +// Die folgende GUID bestimmt die ID der Typbibliothek, wenn dieses Projekt für COM verfügbar gemacht wird +[assembly: Guid("3c36505f-9f80-4b13-b212-446b463689a7")] + +// Versionsinformationen für eine Assembly bestehen aus den folgenden vier Werten: +// +// Hauptversion +// Nebenversion +// Buildnummer +// Revision +// +// Sie können alle Werte angeben oder Standardwerte für die Build- und Revisionsnummern verwenden, +// indem Sie "*" wie unten gezeigt eingeben: +// [assembly: AssemblyVersion("1.0.*")] +[assembly: AssemblyVersion("1.0.0.0")] +[assembly: AssemblyFileVersion("1.0.0.0")] diff --git a/godotdec/godotdec.csproj b/godotdec/godotdec.csproj new file mode 100644 index 0000000..ddd3002 --- /dev/null +++ b/godotdec/godotdec.csproj @@ -0,0 +1,55 @@ + + + + + Debug + AnyCPU + {3C36505F-9F80-4B13-B212-446B463689A7} + Exe + godotdec + godotdec + v4.6 + 512 + true + true + + + + AnyCPU + true + full + false + bin\Debug\ + DEBUG;TRACE + prompt + 4 + + + AnyCPU + pdbonly + true + bin\Release\ + TRACE + prompt + 4 + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/Package.bat b/legacy/Package.bat similarity index 100% rename from Package.bat rename to legacy/Package.bat diff --git a/Run.bat b/legacy/Run.bat similarity index 100% rename from Run.bat rename to legacy/Run.bat diff --git a/godotdec.hxproj b/legacy/godotdec.hxproj similarity index 100% rename from godotdec.hxproj rename to legacy/godotdec.hxproj diff --git a/src/FileMeta.hx b/legacy/src/FileMeta.hx similarity index 100% rename from src/FileMeta.hx rename to legacy/src/FileMeta.hx diff --git a/src/Main.hx b/legacy/src/Main.hx similarity index 91% rename from src/Main.hx rename to legacy/src/Main.hx index 101adfe..d2fbb95 100644 --- a/src/Main.hx +++ b/legacy/src/Main.hx @@ -24,6 +24,7 @@ class Main { Bio.Seperator(); var args = Sys.args(); + args.push("Z:\\Documents\\Software\\JDownloader 2\\Downloads\\polyle\\2019z06225\\sluttyravegirl.pck"); if (args.length < 1) Bio.Error("Please specify the path to a .pck file.", 1); var pck = args[0]; @@ -73,9 +74,16 @@ class Main { index.add(f); } catch (eof:Eof){ + Bio.Debug("End of file"); break; } + catch (e: Dynamic) { + Bio.Error(e); + continue; + } } + + Bio.PrintList(index); } static function readInt64() { @@ -85,6 +93,7 @@ class Main { static function findFileEntry():FileMeta { var pos = file.tell(); + Bio.Debug(pos); return index.find(function (el) { return el.offset.compare(pos) == 0; });