Skip to content

Commit

Permalink
Update to version 2.0.0
Browse files Browse the repository at this point in the history
This is a complete rewrite in C#.

Godotdec now supports bigger archives and also fixes several extraction failures.
Additionally, a new command line parameter allows to automatically convert certain engine-specific file types to standard formats, making the output more useful.
  • Loading branch information
Bioruebe committed Oct 10, 2019
1 parent b262f29 commit 9c224e2
Show file tree
Hide file tree
Showing 14 changed files with 435 additions and 3 deletions.
5 changes: 4 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
@@ -1 +1,4 @@
/bin
/godotdec/bin
/legacy/bin
/godotdec/obj
/.vs
11 changes: 9 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,16 @@
A simple unpacker for Godot Engine package files (.pck)

### Usage
`godotdec <input_file> [<output_dir>]`
`godotdec [<options>] <input_file> [<output_dir>]`

###### 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 |
Expand All @@ -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

Expand Down
25 changes: 25 additions & 0 deletions godotdec.sln
Original file line number Diff line number Diff line change
@@ -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
2 changes: 2 additions & 0 deletions godotdec.sln.DotSettings.user
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
<wpf:ResourceDictionary xml:space="preserve" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:s="clr-namespace:System;assembly=mscorlib" xmlns:ss="urn:shemas-jetbrains-com:settings-storage-xaml" xmlns:wpf="http://schemas.microsoft.com/winfx/2006/xaml/presentation">
<s:Int64 x:Key="/Default/CodeStyle/Naming/CSharpAutoNaming/AutoNamingCompletedVersion/@EntryValue">2</s:Int64></wpf:ResourceDictionary>
6 changes: 6 additions & 0 deletions godotdec/App.config
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<configuration>
<startup>
<supportedRuntime version="v4.0" sku=".NETFramework,Version=v4.6"/>
</startup>
</configuration>
138 changes: 138 additions & 0 deletions godotdec/Bio.cs
Original file line number Diff line number Diff line change
@@ -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<string, PROMPT_SETTING> promptSettings = new Dictionary<string, PROMPT_SETTING>();

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
}
}
}
151 changes: 151 additions & 0 deletions godotdec/Program.cs
Original file line number Diff line number Diff line change
@@ -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)",
"[<options>] <input_file> [<output_directory>]\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<FileEntry>();
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}";
}
}
36 changes: 36 additions & 0 deletions godotdec/Properties/AssemblyInfo.cs
Original file line number Diff line number Diff line change
@@ -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")]
Loading

0 comments on commit 9c224e2

Please sign in to comment.