From 90c74f48d591bb7d6af0e4a205f6de3f0caa7ea6 Mon Sep 17 00:00:00 2001 From: neru Date: Thu, 18 Jun 2026 16:38:50 -0300 Subject: [PATCH] feat: add dumper (only char info for now) --- src/dumper/dumper.cs | 253 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 253 insertions(+) create mode 100644 src/dumper/dumper.cs diff --git a/src/dumper/dumper.cs b/src/dumper/dumper.cs new file mode 100644 index 0000000..6c24b02 --- /dev/null +++ b/src/dumper/dumper.cs @@ -0,0 +1,253 @@ +using CUE4Parse.Compression; +using CUE4Parse.Encryption.Aes; +using CUE4Parse.FileProvider; +using CUE4Parse.MappingsProvider.Usmap; +using CUE4Parse.UE4.Assets.Exports.Engine; +using CUE4Parse.UE4.Assets.Exports.Texture; +using CUE4Parse.UE4.Assets.Objects; +using CUE4Parse.UE4.Objects.Core.i18N; +using CUE4Parse.UE4.Objects.Core.Misc; +using CUE4Parse.UE4.Objects.UObject; +using CUE4Parse.UE4.Versions; +using CUE4Parse.UE4.VirtualFileSystem; +using CUE4Parse_Conversion.Textures; +using Newtonsoft.Json; +using System.Windows.Forms; + +struct CharacterInfo +{ + public string name; + public int idx; + public string iconFilePath; +} + +class Dumper +{ + private DefaultFileProvider? _provider; + private IAesVfsReader? _dataPak; + private IAesVfsReader? _iconPak; + private readonly Dictionary _characterMap = new(); + private Logger _log; + + private string _outDir; + + public Dumper(string outDir) + { + _log = new Logger("Dumper"); + + if (string.IsNullOrWhiteSpace(outDir)) + throw new ArgumentException("Output directory cannot be null or empty.", nameof(outDir)); + + string fullPath = Path.GetFullPath(outDir); + if (!Directory.Exists(fullPath)) + Directory.CreateDirectory(fullPath); + + _outDir = fullPath; + } + + public bool Init() + { + /* + * mapping + */ + string baseDir = AppDomain.CurrentDomain.BaseDirectory; + string mappingPath = Path.GetFullPath(Path.Combine(baseDir, "mapping.usmap")); + + if (!File.Exists(mappingPath)) + return false; + + /* + * compression + */ + ZlibHelper.Initialize(); + OodleHelper.Initialize(Path.Combine(baseDir, OodleHelper.OodleFileName)); + + /* + * game path + */ + var gamePath = Utils.GetGamePath(); + if (gamePath == null) return false; + + var pakDir = Path.Combine(gamePath, "DeadByDaylight", "Content", "Paks"); + if (!Directory.Exists(pakDir)) + { + Console.WriteLine("PAK dir does not exist. (Invalid install?)"); + return false; + } + + /* + * file provider + */ + var version = new VersionContainer(EGame.GAME_DeadByDaylight, ETexturePlatform.DesktopMobile); + _provider = new DefaultFileProvider(pakDir, SearchOption.TopDirectoryOnly, version) + { + MappingsContainer = new FileUsmapTypeMappingsProvider(mappingPath) + }; + + _provider.Initialize(); + _provider.SubmitKey(new FGuid(), new FAesKey(Constants.AESKey)); + _provider.Mount(); + _provider.PostMount(); + + if (!_provider.TryGetArchive("pakchunk3-WinGDK.utoc", out _iconPak)) // icons + { + Console.WriteLine("Failed to load pakchunk3-WinGDK.utoc"); + return false; + } + + if (!_provider.TryGetArchive("pakchunk4-WinGDK.utoc", out _dataPak)) // data + { + Console.WriteLine("Failed to load pakchunk4-WinGDK.utoc"); + return false; + } + + _provider.LoadLocalization(); + + return true; + } + + /* + * dumping functions + */ + public void DumpCharacters() + { + if (_dataPak == null || _provider == null) + throw new InvalidOperationException("Attempted to call dump function without dumper initialization"); + + _log.Info("Dumping characters"); + + List charDbPaths = _dataPak.Files.Keys.Where(x => x.Contains("/CharacterDescriptionDB.uasset", StringComparison.OrdinalIgnoreCase)).ToList(); + + foreach (string path in charDbPaths) + { + var cleanPath = path.Contains('.') ? path[..path.LastIndexOf('.')] : path; + + if (_provider.TryLoadPackageObject(cleanPath, out var dataTable)) + { + foreach (var row in dataTable.RowMap) + { + var props = row.Value.Properties; + + /* + * props + */ + if (!TryGetProp(props, "CharacterIndex", out int charIndex)) + throw new KeyNotFoundException("CharacterIndex was not found."); + if (charIndex == -1) continue; + + if (!TryGetStringProp(props, "DisplayName", out string charName) || !TryGetStringProp(props, "IconFilePath", out string charIconFilePath)) + throw new KeyNotFoundException("DisplayName or IconFilePath was not found."); + + _characterMap[charIndex] = new CharacterInfo + { + name = charName, + idx = charIndex, + iconFilePath = charIconFilePath + }; + } + } + } + + _log.Info("Dumped {0} characters", _characterMap.Count); + + WriteJson("characters", _characterMap.Values); + + return; + } + + public void DumpCharacterIcons() + { + if (_dataPak == null || _provider == null) + throw new InvalidOperationException("Attempted to call dump function without dumper initialization"); + + foreach (CharacterInfo info in _characterMap.Values) + { + ExportIcon(info.iconFilePath, "/character-icons/"); + } + + return; + } + + /* + * internal helper functions + */ + bool TryGetProp(IEnumerable properties, string propName, out T value) + { + var prop = properties.FirstOrDefault(p => p.Name.Text == propName); + if (prop?.Tag != null) + { + var val = prop.Tag.GetValue(); + if (val != null) + { + value = val; + return true; + } + } + _log.Error("Character missing or invalid property: {0}", propName); + value = default!; + return false; + } + bool TryGetStringProp(IEnumerable properties, string propName, out string value) + { + if (TryGetProp(properties, propName, out var rawValue) && rawValue != null) + { + string? strVal = rawValue.ToString(); + + if (!string.IsNullOrEmpty(strVal)) + { + value = strVal; + return true; + } + } + + _log.Error($"Property '{propName}' returned an empty or null string."); + value = string.Empty; + return false; + } + + private void WriteJson(string name, object data) + { + string json = JsonConvert.SerializeObject(data, Formatting.Indented); + File.WriteAllText(_outDir + name + ".json", json); + } + + private void ExportIcon(string assetPath, string outPath) + { + string cleanPath = assetPath.Contains('.') ? assetPath[..assetPath.LastIndexOf('.')] : assetPath; + string fileName = Path.GetFileName(cleanPath) + ".png"; + string relativeOutPath = outPath.TrimStart('/', '\\'); + string fullPath = Path.Combine(_outDir, relativeOutPath, fileName); + + // consider umg path for icons (should i also do this with perks and offerings n stuff?) + if (cleanPath.StartsWith("UI/Icons/")) + cleanPath = "/Game/UI/UMGAssets/" + cleanPath["UI/".Length..]; + + if (File.Exists(fullPath)) return; + + if (_provider == null) return; + + if (_provider.TryLoadPackage(cleanPath, out var package)) + { + var texture = package.GetExports().OfType().FirstOrDefault(); + + if (texture != null) + { + var bitmap = texture.Decode(ETexturePlatform.DesktopMobile); + + if (bitmap != null) + { + var bytes = bitmap.Encode(ETextureFormat.Png, false, out _); + + string? parentFolder = Path.GetDirectoryName(fullPath); + if (!string.IsNullOrEmpty(parentFolder)) + Directory.CreateDirectory(parentFolder); + + File.WriteAllBytes(fullPath, bytes); + Console.WriteLine($"Exported icon: {fileName}"); + } + } + else + Console.WriteLine($"Failed to find a UTexture2D export in package: {cleanPath}"); + } + } +}