Files
HexUnlocked/src/dumper/dumper.cs
T
2026-06-19 04:30:59 -03:00

598 lines
21 KiB
C#

using CUE4Parse.Compression;
using CUE4Parse.Encryption.Aes;
using CUE4Parse.FileProvider;
using CUE4Parse.FileProvider.Objects;
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;
}
struct OfferingInfo
{
public string id;
public string name;
public string iconFilePath;
public EPlayerRole role;
}
struct PerkInfo
{
public string id;
public string name;
public string iconFilePath;
public EPlayerRole role;
}
struct DLCInfo
{
public string id;
public string? name;
public Dictionary<string, string> dlcIds;
}
struct CustomizationItemInfo
{
public string id;
public string name;
public string iconFilePath;
public int associatedCharacter;
public ECustomizationCategory category;
}
struct AddonInfo
{
public string id { get; set; }
public string name { get; set; }
public string iconFilePath { get; set; }
public EPlayerRole role { get; set; }
}
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();
private readonly Dictionary<string, AddonInfo> _addonMap = new();
private readonly Dictionary<string, PerkInfo> _perkMap = new();
private readonly Dictionary<string, OfferingInfo> _offeringMap = new();
private readonly Dictionary<string, CustomizationItemInfo> _customizationItemMap = 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()
{
ProcessDataTables("/CharacterDescriptionDB.uasset", "characters", (rowKey, props) =>
{
if (!TryGetProp<int>(props, "CharacterIndex", out int charIndex))
throw new KeyNotFoundException("CharacterIndex was not found.");
if (charIndex == -1) return; // there is a -1 character (template, placeholder, spectator or smth)
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);
}
public void DumpItems()
{
ProcessDataTables("/ItemDB.uasset", "items", (rowKey, props) =>
{
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)
|| !TryGetProp(props, "IsFakeItem", out bool isFakeItem))
throw new KeyNotFoundException($"Required properties missing for Item: {rowKey}");
UIDataStruct uiData = uiDataFb.MapToStruct<UIDataStruct>();
if (isFakeItem || !isInventory || itemType != EInventoryItemType.Item || role != EPlayerRole.VE_Camper)
return;
if (uiData.IconAssetList.Length == 0)
throw new InvalidDataException("Item's UIData had no icons");
_itemMap[rowKey] = new ItemInfo
{
id = rowKey,
name = uiData.DisplayName.ToString(),
iconFilePath = uiData.IconAssetList[0].ToString()
};
});
_log.Info("Dumped {0} items", _itemMap.Count);
WriteJson("items", _itemMap.Values);
}
public void DumpAddons()
{
ProcessDataTables("/ItemAddonDB.uasset", "addons", (rowKey, props) =>
{
if (!TryGetProp(props, "Role", out EPlayerRole role)
|| !TryGetProp(props, "UIData", out FStructFallback uiDataFb)
|| !TryGetProp(props, "Inventory", out bool isInventory)
|| !TryGetProp(props, "IsFakeItem", out bool isFakeItem))
throw new KeyNotFoundException($"Required properties missing for Addon: {rowKey}");
UIDataStruct uiData = uiDataFb.MapToStruct<UIDataStruct>();
if (!isInventory || isFakeItem)
return;
if (uiData.IconAssetList.Length == 0)
throw new InvalidDataException("Addon's UIData had no icons");
_addonMap[rowKey] = new AddonInfo
{
id = rowKey,
name = uiData.DisplayName.ToString(),
iconFilePath = uiData.IconAssetList[0].ToString(),
role = role
};
});
_log.Info("Dumped {0} addons", _addonMap.Count);
WriteJson("addons", _addonMap.Values);
}
public void DumpPerks()
{
ProcessDataTables("/PerkDB.uasset", "perks", (rowKey, props) =>
{
if (!TryGetProp(props, "Role", out EPlayerRole role)
|| !TryGetProp(props, "UIData", out FStructFallback uiDataFb)
|| !TryGetProp(props, "Inventory", out bool isInventory)
|| !TryGetProp(props, "IsFakeItem", out bool isFakeItem))
throw new KeyNotFoundException($"Required properties missing for Perk: {rowKey}");
UIDataStruct uiData = uiDataFb.MapToStruct<UIDataStruct>();
if (!isInventory || isFakeItem)
return;
if (uiData.IconAssetList.Length == 0)
throw new InvalidDataException("Perk's UIData had no icons");
_perkMap[rowKey] = new PerkInfo
{
id = rowKey,
name = uiData.DisplayName.ToString(),
iconFilePath = uiData.IconAssetList[0].ToString(),
role = role
};
});
_log.Info("Dumped {0} perks", _perkMap.Count);
WriteJson("perks", _perkMap.Values);
}
public void DumpOfferings()
{
ProcessDataTables("/OfferingDB.uasset", "offerings", (rowKey, props) =>
{
if (!TryGetProp(props, "Role", out EPlayerRole role)
|| !TryGetProp(props, "UIData", out FStructFallback uiDataFb)
|| !TryGetProp(props, "Inventory", out bool isInventory)
|| !TryGetProp(props, "IsFakeItem", out bool isFakeItem))
throw new KeyNotFoundException($"Required properties missing for offering: {rowKey}");
UIDataStruct uiData = uiDataFb.MapToStruct<UIDataStruct>();
if (!isInventory || isFakeItem)
return;
if (uiData.IconAssetList.Length == 0)
throw new InvalidDataException("Offerings's UIData had no icons");
_offeringMap[rowKey] = new OfferingInfo
{
id = rowKey,
name = uiData.DisplayName.ToString(),
iconFilePath = uiData.IconAssetList[0].ToString(),
role = role
};
});
_log.Info("Dumped {0} offerings", _offeringMap.Count);
WriteJson("offerings", _offeringMap.Values);
}
public void DumpDLCs()
{
List<DLCInfo> dlcList = new List<DLCInfo>();
ProcessDataTables("/DlcDB.uasset", "dlcs", (rowKey, props) =>
{
if (!TryGetProp(props, "DlcIdSteam", out string steamId)
|| !TryGetProp(props, "DlcIdEpic", out string epicId)
|| !TryGetProp(props, "DlcIdGRDK", out string grdkId)
)
throw new KeyNotFoundException($"Required properties missing for DLC: {rowKey}");
string? displayName = null;
if (TryGetStringProp<FText>(props, "DisplayName", out string foundName) && !string.IsNullOrWhiteSpace(foundName))
displayName = foundName;
DLCInfo info = new DLCInfo
{
id = rowKey,
name = displayName,
dlcIds = new Dictionary<string, string> { { "steam", steamId }, { "epic", epicId }, { "grdk", grdkId } }
};
dlcList.Add(info);
});
_log.Info("Dumped {0} dlcs", dlcList.Count);
WriteJson("dlcs", dlcList.ToArray());
}
public void DumpCustomizations()
{
ProcessDataTables("/CustomizationItemDB.uasset", "customization items", (rowKey, props) =>
{
if (!TryGetProp(props, "Category", out ECustomizationCategory category)
|| !TryGetProp(props, "Availability", out FStructFallback availabilityFb)
|| !TryGetProp(props, "UIData", out FStructFallback uiDataFb)
|| !TryGetProp(props, "AssociatedCharacter", out int associatedCharacter))
throw new KeyNotFoundException($"Required properties missing for CustomizationItem: {rowKey}");
AvailabilityStruct availability = availabilityFb.MapToStruct<AvailabilityStruct>();
UIDataStruct uiData = uiDataFb.MapToStruct<UIDataStruct>();
if (availability.DLCId == "development") return; // skip development dlc items just in case
if (uiData.IconAssetList.Length == 0)
throw new InvalidDataException("CustomizationItem's UIData had no icons");
string iconPathStr = uiData.IconAssetList[0].ToString();
_customizationItemMap[rowKey] = new CustomizationItemInfo
{
id = rowKey,
name = uiData.DisplayName.ToString(),
associatedCharacter = associatedCharacter,
category = category,
iconFilePath = iconPathStr,
};
});
_log.Info("Dumped {0} customization items", _customizationItemMap.Count);
WriteJson("customization_items", _customizationItemMap.Values);
}
public void DumpCharacterIcons() => ExportIcons(_characterMap.Values.Select(x => x.iconFilePath), "character", "/character-icons/");
public void DumpItemIcons() => ExportIcons(_itemMap.Values.Select(x => x.iconFilePath), "item", "/item-icons/");
public void DumpAddonIcons() => ExportIcons(_addonMap.Values.Select(x => x.iconFilePath), "addon", "/addon-icons/");
public void DumpPerkIcons() => ExportIcons(_perkMap.Values.Select(x => x.iconFilePath), "perk", "/perk-icons/");
public void DumpOfferingIcons() => ExportIcons(_offeringMap.Values.Select(x => x.iconFilePath), "offering", "/offering-icons/");
public void DumpCustomizationIcons()
{
_log.Info("Dumping customization item icons");
foreach (var item in _customizationItemMap.Values)
{
string outPath = "customization/";
if (item.category == ECustomizationCategory.Charm ||
item.category == ECustomizationCategory.Badge ||
item.category == ECustomizationCategory.Banner ||
item.category == ECustomizationCategory.PortraitBackground)
{
switch (item.category)
{
case ECustomizationCategory.Charm:
outPath += "charms/";
break;
case ECustomizationCategory.Badge:
outPath += "badges/";
break;
case ECustomizationCategory.Banner:
outPath += "banners/";
break;
case ECustomizationCategory.PortraitBackground:
outPath += "portrait-backgrounds/";
break;
}
}
else if (item.associatedCharacter > -1)
{
string charFolderName = item.associatedCharacter.ToString();
if (_characterMap.TryGetValue(item.associatedCharacter, out CharacterInfo charInfo) && !string.IsNullOrWhiteSpace(charInfo.name))
charFolderName = string.Join("_", charInfo.name.Split(Path.GetInvalidFileNameChars()));
outPath += $"characters/{charFolderName}/";
switch (item.category)
{
case ECustomizationCategory.SurvivorHead:
case ECustomizationCategory.KillerHead:
outPath += "heads/";
break;
case ECustomizationCategory.SurvivorTorso:
outPath += "torsos/";
break;
case ECustomizationCategory.SurvivorLegs:
outPath += "legs/";
break;
case ECustomizationCategory.KillerBody:
outPath += "bodys/";
break;
case ECustomizationCategory.KillerWeapon:
outPath += "weapons/";
break;
case ECustomizationCategory.Outfits:
outPath += "outfits/";
break;
}
}
try
{
ExportIcon(item.iconFilePath, outPath);
}
catch (Exception ex)
{
_log.Error($"Failed to export icon for {item.id}: {ex.Message}");
}
}
_log.Info("Dumped all customization item icons");
}
/*
* bulk functions for dumping
*/
private void ProcessDataTables(string pathFilter, string logName, Action<string, List<FPropertyTag>> rowProcessor)
{
if (_dataPak == null || _provider == null)
throw new InvalidOperationException("Attempted to call dump function without dumper initialization/state");
_log.Info($"Dumping {logName}");
var dbPaths = _dataPak.Files.Keys.Where(x => x.Contains(pathFilter, StringComparison.OrdinalIgnoreCase));
foreach (string path in dbPaths)
{
string cleanPath = path.Contains('.') ? path[..path.LastIndexOf('.')] : path;
if (_provider.TryLoadPackageObject<UDataTable>(cleanPath, out UDataTable? dataTable))
{
foreach (KeyValuePair<FName, FStructFallback> row in dataTable.RowMap)
rowProcessor(row.Key.Text, row.Value.Properties);
}
}
}
private void ExportIcons(IEnumerable<string> iconPaths, string logName, string outFolder)
{
if (_provider == null)
throw new InvalidOperationException("Attempted to call dump function without dumper initialization/state");
_log.Info($"Dumping {logName} icons");
foreach (string path in iconPaths)
ExportIcon(path, outFolder);
_log.Info($"Dumped all {logName} icons");
}
/*
* 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 call dump function without dumper initialization/state");
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[] pathSegments = cleanPath.Split('/', StringSplitOptions.RemoveEmptyEntries);
if (pathSegments.Length > 1)
{
string innerPath = string.Join('/', pathSegments.Skip(1)) + ".uasset";
KeyValuePair<string, GameFile> actualFile = _provider.Files.FirstOrDefault(
kvp => kvp.Key.EndsWith(innerPath, 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);
}
else
throw new InvalidDataException("Bitmap was invalid");
}
else
throw new FileNotFoundException("Failed to find texture");
}
else
throw new FileNotFoundException("Failed to find texture package");
}
}