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-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-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-29 19:29:29 -05:00
|
|
|
using CUE4Parse.UE4.Objects.Core.Math;
|
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-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";
|
2025-03-21 16:51:03 -05:00
|
|
|
static readonly Dictionary<string, string> PenetrationGroupNameMap = new()
|
2025-03-20 00:18:00 -05:00
|
|
|
{
|
|
|
|
|
{ $"{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-04-01 21:03:39 -05:00
|
|
|
TanksFull tanksFull = null!;
|
2025-01-27 22:17:13 -06:00
|
|
|
|
2025-04-01 21:03:39 -05:00
|
|
|
public async Task Initialize()
|
2025-04-01 15:32:37 -05:00
|
|
|
{
|
2025-04-01 21:03:39 -05:00
|
|
|
await FetchStrings();
|
|
|
|
|
tanksFull = TanksFull();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public TanksFull TanksFull()
|
|
|
|
|
{
|
|
|
|
|
if (tanksFull != null)
|
|
|
|
|
return tanksFull;
|
|
|
|
|
|
2025-04-01 15:32:37 -05:00
|
|
|
var dirs = provider.RootDirectory.GetDirectory("Blitz/Content/Tanks").Directories;
|
2025-04-01 21:03:39 -05:00
|
|
|
tanksFull = new();
|
2025-04-01 15:32:37 -05:00
|
|
|
|
|
|
|
|
foreach (var dir in dirs)
|
|
|
|
|
{
|
|
|
|
|
if (dir.Key == "TankStub")
|
|
|
|
|
continue;
|
|
|
|
|
|
|
|
|
|
foreach (var tankDir in dir.Value.Directories)
|
|
|
|
|
{
|
2025-04-01 21:03:39 -05:00
|
|
|
var tank = Tank(tankDir.Value);
|
2025-04-01 15:32:37 -05:00
|
|
|
tanksFull.Tanks.Add(tank);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
DeduplicateSlugs(tanksFull);
|
|
|
|
|
|
|
|
|
|
return tanksFull;
|
|
|
|
|
}
|
|
|
|
|
|
2025-04-01 21:03:39 -05:00
|
|
|
public Tanks Tanks()
|
|
|
|
|
{
|
|
|
|
|
var tanksFull = TanksFull();
|
|
|
|
|
Tanks tanks = new();
|
|
|
|
|
|
|
|
|
|
foreach (var tank in tanksFull.Tanks)
|
|
|
|
|
{
|
|
|
|
|
tanks.Tanks_.Add(tank.Id);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return tanks;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public Tank Tank(string id)
|
|
|
|
|
{
|
|
|
|
|
return tanksFull.Tanks.First(tank => tank.Id == id);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
Tank Tank(VFS tankDir)
|
2025-04-01 15:32:37 -05:00
|
|
|
{
|
|
|
|
|
var pdaName = $"PDA_{tankDir.Name}";
|
|
|
|
|
var pda = provider.LoadObject($"{tankDir.Path}/{pdaName}.{pdaName}");
|
|
|
|
|
|
|
|
|
|
var id = pda.Get<FName>("TankId").Text;
|
|
|
|
|
var name = GetString($"TankEntity__{id.ToLowerInvariant()}__Short_Name");
|
|
|
|
|
var slug = Diacritics.Remove(name.Locales["en"]).ToLower();
|
|
|
|
|
slug = NonAlphanumericRegex().Replace(slug, "-");
|
|
|
|
|
slug = MultipleDashesRegex().Replace(slug, "-");
|
|
|
|
|
slug = TrailingDashRegex().Replace(slug, "");
|
|
|
|
|
|
|
|
|
|
Tank tank = new()
|
|
|
|
|
{
|
|
|
|
|
Id = id,
|
|
|
|
|
Name = name,
|
|
|
|
|
Slug = slug,
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
return tank;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
///////////////////////////////////////////////////////////////////////////
|
|
|
|
|
|
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-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-20 11:59:24 -05:00
|
|
|
async Task MangleArmor(UObject pda)
|
2025-03-20 00:18:00 -05:00
|
|
|
{
|
2025-03-29 19:29:29 -05:00
|
|
|
var id = pda.Get<FName>("TankId").Text;
|
2025-03-20 11:59:24 -05:00
|
|
|
TankArmor tankArmor = new();
|
2025-03-29 19:29:29 -05:00
|
|
|
var attributes = pda.Get<UObject>("DA_Attributes");
|
2025-03-20 00:18:00 -05:00
|
|
|
|
2025-03-29 19:29:29 -05:00
|
|
|
if (attributes.TryGet<UScriptMap>("PenetrationGroups", out var penetrationGroups))
|
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];
|
2025-03-29 19:29:29 -05:00
|
|
|
var armors = group
|
|
|
|
|
.Value!.GetValue<FStructFallback>()!
|
|
|
|
|
.Get<UScriptMap>("PenetrationGroups");
|
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>()!;
|
2025-03-29 19:29:29 -05:00
|
|
|
var thickness = armorProperties.Get<float>("Armor");
|
|
|
|
|
var commonData = armorProperties.Get<FStructFallback>("CommonData");
|
|
|
|
|
var rowName = commonData.Get<FName>("RowName");
|
2025-03-20 11:59:24 -05:00
|
|
|
|
|
|
|
|
penetrationGroup.Armors[armorName] = new()
|
|
|
|
|
{
|
|
|
|
|
Thickness = thickness,
|
|
|
|
|
Type = rowName.Text,
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
tankArmor.Groups[name] = penetrationGroup;
|
|
|
|
|
}
|
2025-03-20 00:18:00 -05:00
|
|
|
}
|
2025-03-29 19:29:29 -05:00
|
|
|
else
|
|
|
|
|
{
|
|
|
|
|
PrettyLog.Warn($"Penetration groups missing for {id}; creating empty list");
|
|
|
|
|
}
|
2025-03-20 00:18:00 -05:00
|
|
|
|
2025-04-01 15:32:37 -05:00
|
|
|
// await assetUploader.Add($"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-29 19:29:29 -05:00
|
|
|
var id = pda.Get<FName>("TankId").Text;
|
2025-03-06 19:41:55 -06:00
|
|
|
|
2025-03-29 19:29:29 -05:00
|
|
|
if (pda.TryGet<UTexture2D>("SmallIcon", out var smallIconTexture))
|
|
|
|
|
{
|
|
|
|
|
var smallIcon = BlitzKitExporter.Texture2D(smallIconTexture);
|
2025-04-01 15:32:37 -05:00
|
|
|
// await assetUploader.Add($"tanks/{id}/icons/small.png", smallIcon);
|
2025-03-29 19:29:29 -05:00
|
|
|
}
|
|
|
|
|
else
|
2025-03-06 21:49:17 -06:00
|
|
|
{
|
|
|
|
|
PrettyLog.Warn($"Small icon missing for {id}");
|
|
|
|
|
}
|
|
|
|
|
|
2025-03-29 19:29:29 -05:00
|
|
|
if (pda.TryGet<UTexture2D>("BigIcon", out var bigIconTexture))
|
|
|
|
|
{
|
|
|
|
|
var bigIcon = BlitzKitExporter.Texture2D(bigIconTexture);
|
2025-04-01 15:32:37 -05:00
|
|
|
// await assetUploader.Add($"tanks/{id}/icons/big.png", bigIcon);
|
2025-03-29 19:29:29 -05:00
|
|
|
}
|
|
|
|
|
else
|
2025-03-06 21:49:17 -06:00
|
|
|
{
|
|
|
|
|
PrettyLog.Warn($"Big icon missing for {id}");
|
|
|
|
|
}
|
2025-03-06 19:41:55 -06:00
|
|
|
}
|
|
|
|
|
|
2025-03-29 19:29:29 -05:00
|
|
|
async Task MangleModules(Tank tank, UObject pda)
|
2025-03-06 19:41:55 -06:00
|
|
|
{
|
2025-03-29 19:29:29 -05:00
|
|
|
TankModel tankModel = new();
|
|
|
|
|
|
|
|
|
|
foreach (var moduleType in CollisionDTs)
|
2025-03-21 16:15:56 -05:00
|
|
|
{
|
2025-03-29 19:29:29 -05:00
|
|
|
var dt = pda.Get<UDataTable>(moduleType);
|
2025-03-21 15:49:58 -05:00
|
|
|
|
2025-03-29 19:29:29 -05:00
|
|
|
foreach (var row in dt.RowMap)
|
2025-03-21 16:15:56 -05:00
|
|
|
{
|
2025-03-29 19:29:29 -05:00
|
|
|
var moduleName = row.Key.Text;
|
|
|
|
|
ModuleModel moduleModel = new();
|
|
|
|
|
var visualData = row.Value.Get<UObject>("VisualData");
|
|
|
|
|
var meshSettings = visualData.Get<FStructFallback>("MeshSettings");
|
|
|
|
|
var boneTransform = meshSettings.Get<FStructFallback>("BoneTransform");
|
2025-03-21 15:49:58 -05:00
|
|
|
|
2025-03-29 19:29:29 -05:00
|
|
|
switch (moduleType)
|
|
|
|
|
{
|
|
|
|
|
case "DT_Turrets":
|
|
|
|
|
{
|
2025-03-29 21:15:55 -05:00
|
|
|
Turret turret = new() { Id = moduleName };
|
2025-03-29 19:29:29 -05:00
|
|
|
tank.Turrets[moduleName] = turret;
|
|
|
|
|
break;
|
|
|
|
|
}
|
2025-03-21 15:49:58 -05:00
|
|
|
|
2025-03-29 19:29:29 -05:00
|
|
|
case "DT_Guns":
|
|
|
|
|
{
|
2025-03-29 21:15:55 -05:00
|
|
|
Gun gun = new() { Id = moduleName };
|
2025-03-29 19:29:29 -05:00
|
|
|
tank.Guns[moduleName] = gun;
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (boneTransform.TryGet<FVector>("Translation", out var translation))
|
|
|
|
|
{
|
|
|
|
|
moduleModel.Translation = BlitzKitExporter.Vector3(translation);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
tankModel.Modules[moduleName] = moduleModel;
|
|
|
|
|
|
|
|
|
|
if (meshSettings.TryGet<UStaticMesh>("CollisionMesh", out var collision))
|
|
|
|
|
{
|
|
|
|
|
MonoGltf gltf = new(collision);
|
|
|
|
|
|
2025-04-01 15:32:37 -05:00
|
|
|
// await assetUploader.Add(
|
|
|
|
|
// new($"tanks/{tank.Id}/collision/{moduleName}.glb", gltf.Write())
|
|
|
|
|
// );
|
2025-03-29 19:29:29 -05:00
|
|
|
}
|
|
|
|
|
else
|
|
|
|
|
{
|
|
|
|
|
PrettyLog.Warn($"No collision mesh found for {tank.Id} {moduleName}; skipping...");
|
|
|
|
|
}
|
|
|
|
|
}
|
2025-03-21 16:15:56 -05:00
|
|
|
}
|
2025-03-29 19:29:29 -05:00
|
|
|
|
2025-04-01 15:32:37 -05:00
|
|
|
// await assetUploader.Add($"tanks/{tank.Id}/model.pb", tankModel.ToByteArray());
|
2025-01-30 11:31:55 -06:00
|
|
|
}
|
2025-01-26 21:17:25 -06:00
|
|
|
}
|
|
|
|
|
}
|