347 lines
12 KiB
C#
347 lines
12 KiB
C#
using CUE4Parse.Compression;
|
|
using CUE4Parse.Encryption.Aes;
|
|
using CUE4Parse.FileProvider;
|
|
using CUE4Parse.MappingsProvider.Usmap;
|
|
using CUE4Parse.UE4.Assets;
|
|
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;
|
|
|
|
struct CharacterInfo
|
|
{
|
|
public string name;
|
|
public int idx;
|
|
public string iconFilePath;
|
|
}
|
|
|
|
struct ItemInfo
|
|
{
|
|
public string id;
|
|
public string name;
|
|
public string iconFilePath;
|
|
}
|
|
|
|
class Dumper
|
|
{
|
|
private DefaultFileProvider? _provider;
|
|
private IAesVfsReader? _dataPak;
|
|
private Logger _log;
|
|
|
|
private string _outDir;
|
|
|
|
private readonly Dictionary<int, CharacterInfo> _characterMap = new();
|
|
private readonly Dictionary<string, ItemInfo> _itemMap = new();
|
|
|
|
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
|
|
*/
|
|
string? gamePath = Utils.GetGamePath();
|
|
if (gamePath == null) return false;
|
|
|
|
string pakDir = Path.Combine(gamePath, "DeadByDaylight", "Content", "Paks");
|
|
if (!Directory.Exists(pakDir))
|
|
{
|
|
Console.WriteLine("PAK dir does not exist. (Invalid install?)");
|
|
return false;
|
|
}
|
|
|
|
/*
|
|
* file provider
|
|
*/
|
|
VersionContainer? 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("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<string> charDbPaths = _dataPak.Files.Keys.Where(x => x.Contains("/CharacterDescriptionDB.uasset", StringComparison.OrdinalIgnoreCase)).ToList();
|
|
|
|
foreach (string path in charDbPaths)
|
|
{
|
|
string cleanPath = path.Contains('.') ? path[..path.LastIndexOf('.')] : path;
|
|
|
|
if (_provider.TryLoadPackageObject<UDataTable>(cleanPath, out UDataTable? dataTable))
|
|
{
|
|
foreach (KeyValuePair<FName, FStructFallback> row in dataTable.RowMap)
|
|
{
|
|
List<FPropertyTag> props = row.Value.Properties;
|
|
|
|
if (!TryGetProp<int>(props, "CharacterIndex", out int charIndex))
|
|
throw new KeyNotFoundException("CharacterIndex was not found.");
|
|
if (charIndex == -1) continue;
|
|
|
|
if (!TryGetStringProp<FText>(props, "DisplayName", out string charName) || !TryGetStringProp<FName>(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 (_provider == null)
|
|
throw new InvalidOperationException("Attempted to call dump function without dumper initialization");
|
|
|
|
_log.Info("Dumping character icons");
|
|
|
|
foreach (CharacterInfo info in _characterMap.Values)
|
|
ExportIcon(info.iconFilePath, "/character-icons/");
|
|
|
|
_log.Info("Dumped all character icons");
|
|
|
|
return;
|
|
}
|
|
|
|
public void DumpItems()
|
|
{
|
|
if (_dataPak == null || _provider == null)
|
|
throw new InvalidOperationException("Attempted to call dump function without dumper initialization");
|
|
|
|
_log.Info("Dumping character icons");
|
|
|
|
List<string> itemDBPaths = _dataPak.Files.Keys.Where(x => x.Contains("/ItemDB.uasset", StringComparison.OrdinalIgnoreCase)).ToList();
|
|
|
|
foreach (string path in itemDBPaths)
|
|
{
|
|
string cleanPath = path.Contains('.') ? path[..path.LastIndexOf('.')] : path;
|
|
|
|
if (_provider.TryLoadPackageObject<UDataTable>(cleanPath, out UDataTable? dataTable))
|
|
{
|
|
foreach (KeyValuePair<FName, FStructFallback> row in dataTable.RowMap)
|
|
{
|
|
List<FPropertyTag> props = row.Value.Properties;
|
|
|
|
string itemId = row.Key.Text;
|
|
|
|
if (!TryGetProp(props, "Type", out EInventoryItemType itemType)
|
|
|| !TryGetProp(props, "UIData", out FStructFallback uiDataFb)
|
|
|| !TryGetProp(props, "Role", out EPlayerRole role)
|
|
|| !TryGetProp(props, "Inventory", out bool isInventory))
|
|
throw new KeyNotFoundException("Type, UIData or Role was not found.");
|
|
|
|
UIDataStruct uiData = uiDataFb.MapToStruct<UIDataStruct>();
|
|
|
|
// ignore powers, slasher and challenge related items
|
|
if (!isInventory || itemType != EInventoryItemType.Item || role != EPlayerRole.VE_Camper)
|
|
{
|
|
_log.Verbose("Ignoring invalid item ({0})", itemId);
|
|
continue;
|
|
}
|
|
|
|
if (uiData.IconAssetList.Length == 0)
|
|
throw new InvalidDataException("Item's UIData had no icons");
|
|
|
|
_itemMap[itemId] = new ItemInfo
|
|
{
|
|
id = itemId,
|
|
name = uiData.DisplayName.ToString(),
|
|
iconFilePath = uiData.IconAssetList[0].ToString()
|
|
};
|
|
}
|
|
}
|
|
}
|
|
|
|
WriteJson("items", _itemMap.Values);
|
|
_log.Info("Dumped all items");
|
|
}
|
|
|
|
public void DumpItemIcons()
|
|
{
|
|
if (_provider == null)
|
|
throw new InvalidOperationException("Attempted to call dump function without dumper initialization");
|
|
|
|
_log.Info("Dumping item icons");
|
|
|
|
foreach (ItemInfo info in _itemMap.Values)
|
|
ExportIcon(info.iconFilePath, "/item-icons/");
|
|
|
|
_log.Info("Dumped all item icons");
|
|
|
|
return;
|
|
}
|
|
|
|
/*
|
|
* internal helper functions
|
|
*/
|
|
private bool TryGetProp<T>(IEnumerable<FPropertyTag> properties, string propName, out T value)
|
|
{
|
|
FPropertyTag? prop = properties.FirstOrDefault(p => p.Name.Text == propName);
|
|
if (prop != null && prop.Tag != null)
|
|
{
|
|
T? val = prop.Tag.GetValue<T>();
|
|
if (val != null)
|
|
{
|
|
value = val;
|
|
return true;
|
|
}
|
|
}
|
|
_log.Error("Character missing or invalid property: {0}", propName);
|
|
value = default!;
|
|
return false;
|
|
}
|
|
|
|
private bool TryGetStringProp<T>(IEnumerable<FPropertyTag> properties, string propName, out string value)
|
|
{
|
|
if (TryGetProp<T>(properties, propName, out T? 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)
|
|
{
|
|
if (_provider == null) throw new InvalidOperationException("Attempted to export icon when dumper was not initialized/invalid");
|
|
|
|
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);
|
|
|
|
if (cleanPath.StartsWith("UI/Icons/"))
|
|
cleanPath = "/Game/UI/UMGAssets/" + cleanPath["UI/".Length..];
|
|
|
|
if (File.Exists(fullPath)) return;
|
|
|
|
/*
|
|
* shoutout to whichever dev kept fucking up the casing
|
|
* its rly surprising how many times this happened
|
|
*/
|
|
if (!_provider.TryLoadPackage(cleanPath, out IPackage? package))
|
|
{
|
|
string searchSuffix = cleanPath;
|
|
|
|
if (searchSuffix.StartsWith("/Game/"))
|
|
searchSuffix = searchSuffix["/Game/".Length..];
|
|
|
|
if (!searchSuffix.EndsWith(".uasset"))
|
|
searchSuffix += ".uasset";
|
|
|
|
var actualFile = _provider.Files.FirstOrDefault(kvp => kvp.Key.EndsWith(searchSuffix, StringComparison.OrdinalIgnoreCase));
|
|
if (actualFile.Value != null)
|
|
_provider.TryLoadPackage(actualFile.Value, out package);
|
|
}
|
|
|
|
|
|
if (package != null)
|
|
{
|
|
UTexture2D? texture = package.GetExports().OfType<UTexture2D>().FirstOrDefault();
|
|
|
|
if (texture != null)
|
|
{
|
|
CTexture? bitmap = texture.Decode(ETexturePlatform.DesktopMobile);
|
|
|
|
if (bitmap != null)
|
|
{
|
|
byte[] bytes = bitmap.Encode(ETextureFormat.Png, false, out _);
|
|
|
|
string? parentFolder = Path.GetDirectoryName(fullPath);
|
|
if (!string.IsNullOrEmpty(parentFolder))
|
|
Directory.CreateDirectory(parentFolder);
|
|
|
|
File.WriteAllBytes(fullPath, bytes);
|
|
_log.Verbose("Exported icon: {0}", fileName);
|
|
}
|
|
else
|
|
throw new InvalidDataException("Bitmap was invalid");
|
|
}
|
|
else
|
|
throw new FileNotFoundException("Failed to find texture");
|
|
}
|
|
else
|
|
throw new FileNotFoundException("Failed to find texture package");
|
|
}
|
|
}
|