Files
blitzkit/src/BlitzKit.CLI/Functions/Mangler.cs
T

353 lines
11 KiB
C#
Raw Normal View History

2025-03-05 00:07:25 -06:00
using System.Text.Json;
2025-03-01 23:08:22 -06:00
using System.Text.RegularExpressions;
2025-03-20 11:59:24 -05:00
using System.Threading.Tasks;
2025-01-29 19:14:46 -06:00
using Blitzkit;
2025-01-28 12:59:59 -06:00
using BlitzKit.CLI.Models;
2025-03-05 00:07:25 -06:00
using BlitzKit.CLI.Quicktype.Locales;
2025-03-01 23:08:22 -06:00
using BlitzKit.CLI.Utils;
2025-03-20 11:59:24 -05:00
using CUE4Parse.MappingsProvider;
2025-02-25 15:15:03 -06:00
using CUE4Parse.UE4.Assets.Exports;
2025-02-02 00:25:47 -06:00
using CUE4Parse.UE4.Assets.Exports.Engine;
using CUE4Parse.UE4.Assets.Exports.StaticMesh;
2025-03-06 19:41:55 -06:00
using CUE4Parse.UE4.Assets.Exports.Texture;
2025-02-25 15:15:03 -06:00
using CUE4Parse.UE4.Assets.Objects;
2025-03-20 00:18:00 -05:00
using CUE4Parse.UE4.Assets.Objects.Properties;
2025-02-25 17:23:25 -06:00
using CUE4Parse.UE4.Objects.UObject;
2025-03-05 11:52:39 -06:00
using CUE4Parse.Utils;
2025-03-01 23:08:22 -06:00
using Google.Protobuf;
2025-03-20 00:18:00 -05:00
using Google.Protobuf.Collections;
using Org.BouncyCastle.Asn1.Misc;
2025-03-05 00:07:25 -06:00
using YamlDotNet.Serialization;
2025-01-27 22:17:13 -06:00
2025-01-28 12:59:59 -06:00
namespace BlitzKit.CLI.Functions
2025-01-26 21:17:25 -06:00
{
2025-03-20 00:18:00 -05:00
public partial class Mangler(string[] args)
2025-01-26 21:17:25 -06:00
{
2025-03-20 00:18:00 -05:00
static readonly string PenetrationGroupNameMapPrefix =
"EMintSDKProtoData_BlitzStaticTankUpgradeSingleStage_PenetrationGroupUpgrade_TankPart";
static readonly Dictionary<string, string> PenetrationGroupNameMap = new Dictionary<
string,
string
>
{
{ $"{PenetrationGroupNameMapPrefix}::TANK_PART_CHASSIS", "chassis" },
{ $"{PenetrationGroupNameMapPrefix}::TANK_PART_HULL", "hull" },
{ $"{PenetrationGroupNameMapPrefix}::TANK_PART_TURRET", "turret" },
{ $"{PenetrationGroupNameMapPrefix}::TANK_PART_GUN", "gun" },
};
2025-03-21 16:15:56 -05:00
private static readonly List<string> CollisionDTs =
[
"DT_Chassis",
"DT_Hulls",
"DT_Turrets",
"DT_Guns",
];
2025-03-20 00:18:00 -05:00
[GeneratedRegex("[^a-z0-9]")]
private static partial Regex NonAlphanumericRegex();
[GeneratedRegex("--+")]
private static partial Regex MultipleDashesRegex();
[GeneratedRegex("-$")]
private static partial Regex TrailingDashRegex();
2025-02-25 17:55:05 -06:00
readonly BlitzProvider provider = new(args.Contains("--depot"));
2025-03-05 00:07:25 -06:00
Locales? locales;
readonly Dictionary<string, Dictionary<string, string>> strings = [];
2025-03-21 16:15:56 -05:00
readonly AssetUploader assetUploader = new("mangled ue assets") { disabled = false };
2025-01-27 22:17:13 -06:00
2025-02-25 17:23:25 -06:00
public async Task Mangle()
2025-02-01 21:21:12 -06:00
{
2025-03-05 00:07:25 -06:00
await FetchStrings();
2025-03-06 21:46:05 -06:00
await MangleNations();
await assetUploader.Flush();
2025-03-05 00:07:25 -06:00
}
2025-02-01 21:21:12 -06:00
2025-03-05 00:07:25 -06:00
I18n GetString(string name)
{
I18n i18n = new();
if (strings[locales!.Default].TryGetValue(name, out var defaultString))
2025-01-27 22:17:13 -06:00
{
2025-03-05 00:07:25 -06:00
foreach (var locale in locales!.Supported)
{
if (strings[locale.Locale].TryGetValue(name, out var localeString))
{
if (locale.Locale == locales.Default || localeString != defaultString)
{
i18n.Locales[locale.Locale] = localeString;
}
}
else
{
PrettyLog.Warn($"String missing from {locale.Locale} locale: {name}");
}
}
}
else
{
PrettyLog.Warn($"String missing from default locale: {name}");
i18n.Locales[locales!.Default] = name;
2025-01-27 22:17:13 -06:00
}
2025-02-25 17:23:25 -06:00
2025-03-05 00:07:25 -06:00
return i18n;
}
async Task FetchStrings()
{
using var client = new HttpClient();
var text = File.ReadAllText("../../packages/i18n/locales.json");
2025-03-06 21:46:05 -06:00
locales =
2025-03-05 00:07:25 -06:00
JsonSerializer.Deserialize<Locales>(text)
?? throw new Exception("Failed to deserialize locales");
foreach (var locale in locales.Supported)
{
var blitzLocale = locale.Blitz ?? locale.Locale;
var url =
2025-03-05 11:52:39 -06:00
$"{Unpacker.WG_DLC_DOMAIN}/dlc/{Unpacker.CONTENT_GROUP}/ue_localizations/development/stuff/{blitzLocale}.yaml";
2025-03-05 00:07:25 -06:00
var yaml = await client.GetStringAsync(url);
var deserializer = new DeserializerBuilder().Build();
var parsedYaml = deserializer.Deserialize<Dictionary<string, string>>(yaml);
strings[locale.Locale] = parsedYaml;
}
}
2025-03-06 21:46:05 -06:00
async Task MangleNations()
2025-03-05 00:07:25 -06:00
{
var dirs = provider.RootDirectory.GetDirectory("Blitz/Content/Tanks").Directories;
2025-03-18 17:26:09 -05:00
TanksFull tanksFull = new();
2025-03-05 00:07:25 -06:00
foreach (var dir in dirs)
{
if (dir.Key == "TankStub")
continue;
2025-03-05 10:03:03 -06:00
foreach (var tankDir in dir.Value.Directories)
{
2025-03-06 21:46:05 -06:00
// if (tankDir.Value.Name != "R90_IS_4")
// continue;
2025-01-29 19:14:46 -06:00
2025-03-21 16:15:56 -05:00
// if (tankDir.Value.Name != "R81_IS_8")
// continue;
2025-03-06 21:46:05 -06:00
var tank = await MangleTank(tankDir.Value);
2025-03-18 17:26:09 -05:00
tanksFull.Tanks.Add(tank);
2025-03-05 10:03:03 -06:00
}
2025-02-01 21:21:12 -06:00
}
2025-03-01 23:08:22 -06:00
2025-03-18 17:26:09 -05:00
DeduplicateSlugs(tanksFull);
2025-03-05 11:52:39 -06:00
Tanks tanks = new();
2025-03-18 17:26:09 -05:00
foreach (var tank in tanksFull.Tanks)
2025-03-05 11:52:39 -06:00
{
2025-03-18 17:26:09 -05:00
tanks.Tanks_.Add(tank.Id);
await assetUploader.Add(new($"tanks/{tank.Id}/meta.pb", tank.ToByteArray()));
2025-03-05 11:52:39 -06:00
}
2025-03-06 21:46:05 -06:00
await assetUploader.Add(new("definitions/tanks.pb", tanks.ToByteArray()));
2025-03-18 17:26:09 -05:00
await assetUploader.Add(new("definitions/tanks-full.pb", tanksFull.ToByteArray()));
2025-01-26 21:17:25 -06:00
}
2025-01-30 11:31:55 -06:00
2025-03-18 17:26:09 -05:00
void DeduplicateSlugs(TanksFull tanksFull)
2025-03-05 11:52:39 -06:00
{
List<string> checkedSlugs = [];
Dictionary<string, List<Tank>> duplicates = [];
2025-03-18 17:26:09 -05:00
foreach (var tank in tanksFull.Tanks)
2025-03-05 11:52:39 -06:00
{
if (checkedSlugs.Contains(tank.Slug))
continue;
checkedSlugs.Add(tank.Slug);
2025-03-18 17:26:09 -05:00
foreach (var otherTank in tanksFull.Tanks)
2025-03-05 11:52:39 -06:00
{
if (otherTank.Slug.Equals(tank.Slug) && !otherTank.Id.Equals(tank.Id))
{
var thisDuplicates = duplicates.GetOrAdd(tank.Slug, () => [tank]);
thisDuplicates.Add(otherTank);
}
}
}
if (duplicates.Count > 0)
{
PrettyLog.Warn($"{duplicates.Count} duplicate slugs found, attempting strategies...");
}
foreach (var duplicate in duplicates)
{
2025-03-21 16:20:39 -05:00
PrettyLog.Log($"{duplicate.Key}:");
2025-03-05 11:52:39 -06:00
foreach (var tank in duplicate.Value)
{
2025-03-21 16:20:39 -05:00
PrettyLog.Log($"\t{tank.Id}");
2025-03-05 11:52:39 -06:00
}
2025-03-18 17:26:09 -05:00
if (duplicate.Value.Count == 2 && duplicate.Value.Any(tank => tank.Id.EndsWith("TU")))
2025-03-05 11:52:39 -06:00
{
PrettyLog.Success("\tTutorial bot discrimination...");
var tutorialBot = duplicate.Value.First(tank => tank.Id.EndsWith("TU"));
tutorialBot.Slug += "-tu";
}
2025-03-18 17:26:09 -05:00
// if (duplicate.Value.All((tank)=>duplicate.Value.All((otherTank)=>otherTank.Id == tank.Id || otherTank.Nation != tank.Nation)))
2025-03-05 11:52:39 -06:00
else
{
PrettyLog.Success("\tNation discrimination...");
foreach (var tank in duplicate.Value)
{
var match = Regex.Match(tank.Id, @"([a-zA-Z]+)\d+_");
var rawNation = match.Groups[1].Value;
var slugNation = rawNation.ToLowerInvariant();
tank.Slug += $"-{slugNation}";
}
}
2025-03-18 17:26:09 -05:00
// else
// {
// throw new Exception("No applicable strategy found");
// }
2025-03-05 11:52:39 -06:00
foreach (var tank in duplicate.Value)
{
2025-03-21 16:20:39 -05:00
PrettyLog.Log($"\t\t{tank.Slug}");
2025-03-05 11:52:39 -06:00
}
2025-03-21 16:20:39 -05:00
PrettyLog.Line();
2025-03-05 11:52:39 -06:00
}
}
2025-03-06 21:46:05 -06:00
async Task<Tank> MangleTank(VFS tankDir)
2025-02-01 21:21:12 -06:00
{
var pdaName = $"PDA_{tankDir.Name}";
2025-02-25 15:15:03 -06:00
var pda = provider.LoadObject($"{tankDir.Path}/{pdaName}.{pdaName}");
2025-02-25 17:23:25 -06:00
2025-03-05 00:07:25 -06:00
var id = PropertyUtil.Get<FName>(pda, "TankId").Text;
2025-03-05 11:52:39 -06:00
var name = GetString($"TankEntity__{id.ToLowerInvariant()}__Short_Name");
2025-03-05 09:52:09 -06:00
var slug = Diacritics.Remove(name.Locales["en"]).ToLower();
2025-03-20 00:18:00 -05:00
slug = NonAlphanumericRegex().Replace(slug, "-");
slug = MultipleDashesRegex().Replace(slug, "-");
slug = TrailingDashRegex().Replace(slug, "");
2025-03-05 09:52:09 -06:00
2025-03-21 16:15:56 -05:00
await MangleCollision(id, pda);
2025-03-18 17:35:56 -05:00
// await MangleIcon(pda);
2025-03-21 15:49:58 -05:00
// await MangleArmor(pda);
2025-03-05 00:07:25 -06:00
2025-03-05 09:52:09 -06:00
Tank tank = new()
{
Id = id,
Name = name,
Slug = slug,
};
2025-03-05 00:07:25 -06:00
return tank;
}
2025-03-20 11:59:24 -05:00
async Task MangleArmor(UObject pda)
2025-03-20 00:18:00 -05:00
{
var id = PropertyUtil.Get<FName>(pda, "TankId").Text;
2025-03-20 11:59:24 -05:00
TankArmor tankArmor = new();
2025-03-20 00:18:00 -05:00
var attributes = PropertyUtil.Get<UObject>(pda, "DA_Attributes");
PropertyUtil.TryGet<UScriptMap>(attributes, "PenetrationGroups", out var penetrationGroups);
if (penetrationGroups == null)
{
2025-03-20 11:59:24 -05:00
PrettyLog.Warn($"Penetration groups missing for {id}; creating empty list");
2025-03-20 00:18:00 -05:00
}
2025-03-20 11:59:24 -05:00
else
2025-03-20 00:18:00 -05:00
{
2025-03-20 11:59:24 -05:00
foreach (var group in penetrationGroups.Properties)
2025-03-20 00:18:00 -05:00
{
2025-03-20 11:59:24 -05:00
var rawName = ((FName)group.Key.GenericValue!).Text;
var name = PenetrationGroupNameMap[rawName];
var armors = PropertyUtil.Get<UScriptMap>(
group.Value!.GetValue<FStructFallback>()!,
"PenetrationGroups"
2025-03-20 00:18:00 -05:00
);
2025-03-20 11:59:24 -05:00
PenetrationGroup penetrationGroup = new();
2025-03-20 00:18:00 -05:00
2025-03-20 11:59:24 -05:00
foreach (var armorProperty in armors.Properties)
{
var armorName = ((FName)armorProperty.Key.GenericValue!).Text;
var armorProperties = armorProperty.Value!.GetValue<FStructFallback>()!;
var thickness = PropertyUtil.Get<float>(armorProperties, "Armor");
var commonData = PropertyUtil.Get<FStructFallback>(armorProperties, "CommonData");
var rowName = PropertyUtil.Get<FName>(commonData, "RowName");
penetrationGroup.Armors[armorName] = new()
{
Thickness = thickness,
Type = rowName.Text,
};
}
tankArmor.Groups[name] = penetrationGroup;
}
2025-03-20 00:18:00 -05:00
}
2025-03-20 11:59:24 -05:00
await assetUploader.Add(new($"tanks/{id}/armor.pb", tankArmor.ToByteArray()));
2025-03-20 00:18:00 -05:00
}
2025-03-06 21:46:05 -06:00
async Task MangleIcon(UObject pda)
2025-03-05 00:07:25 -06:00
{
2025-03-06 19:41:55 -06:00
var id = PropertyUtil.Get<FName>(pda, "TankId").Text;
2025-03-06 21:49:17 -06:00
PropertyUtil.TryGet<UTexture2D>(pda, "SmallIcon", out var smallIconTexture);
PropertyUtil.TryGet<UTexture2D>(pda, "BigIcon", out var bigIconTexture);
2025-03-06 19:41:55 -06:00
2025-03-06 21:49:17 -06:00
if (smallIconTexture is null)
{
PrettyLog.Warn($"Small icon missing for {id}");
}
else
{
var smallIcon = BlitzKitExporter.Texture2D(smallIconTexture);
await assetUploader.Add(new($"tanks/{id}/icons/small.png", smallIcon));
}
if (bigIconTexture is null)
{
PrettyLog.Warn($"Big icon missing for {id}");
}
else
{
var bigIcon = BlitzKitExporter.Texture2D(bigIconTexture);
await assetUploader.Add(new($"tanks/{id}/icons/big.png", bigIcon));
}
2025-03-06 19:41:55 -06:00
}
2025-03-21 16:15:56 -05:00
async Task MangleModuleCollision(string id, UDataTable dt)
2025-03-06 19:41:55 -06:00
{
2025-03-21 16:15:56 -05:00
foreach (var row in dt.RowMap)
{
var moduleName = row.Key.Text;
var visualData = PropertyUtil.Get<UObject>(row.Value, "VisualData");
var meshSettings = PropertyUtil.Get<FStructFallback>(visualData, "MeshSettings");
2025-03-21 15:49:58 -05:00
2025-03-21 16:15:56 -05:00
if (PropertyUtil.TryGet<UStaticMesh>(meshSettings, "CollisionMesh", out var collision))
{
MonoGltf gltf = new(collision);
2025-03-21 15:49:58 -05:00
2025-03-21 16:15:56 -05:00
await assetUploader.Add(new($"tanks/{id}/collision/{moduleName}.glb", gltf.Write()));
}
else
{
PrettyLog.Warn($"No collision mesh found for {id} {moduleName}; skipping...");
}
;
}
}
2025-03-21 15:49:58 -05:00
2025-03-21 16:15:56 -05:00
async Task MangleCollision(string id, UObject pda)
{
foreach (var name in CollisionDTs)
{
await MangleModuleCollision(id, PropertyUtil.Get<UDataTable>(pda, name));
}
2025-01-30 11:31:55 -06:00
}
2025-01-26 21:17:25 -06:00
}
}