diff --git a/packages/core/src/protos/tanks.proto b/packages/core/src/protos/tanks.proto index 12a9cbcaf..d343924a4 100644 --- a/packages/core/src/protos/tanks.proto +++ b/packages/core/src/protos/tanks.proto @@ -3,5 +3,10 @@ syntax = "proto2"; package blitzkit; message Tanks { - repeated string tanks = 1; + repeated TankMeta tanks = 1; +} + +message TankMeta { + required string id = 1; + required string seo_id = 2; } \ No newline at end of file diff --git a/packages/core/src/protos/tanks.ts b/packages/core/src/protos/tanks.ts index 6ba611a68..157095201 100644 --- a/packages/core/src/protos/tanks.ts +++ b/packages/core/src/protos/tanks.ts @@ -10,7 +10,12 @@ import { BinaryReader, BinaryWriter } from "@bufbuild/protobuf/wire"; export const protobufPackage = "blitzkit"; export interface Tanks { - tanks: string[]; + tanks: TankMeta[]; +} + +export interface TankMeta { + id: string; + seo_id: string; } function createBaseTanks(): Tanks { @@ -20,7 +25,7 @@ function createBaseTanks(): Tanks { export const Tanks: MessageFns = { encode(message: Tanks, writer: BinaryWriter = new BinaryWriter()): BinaryWriter { for (const v of message.tanks) { - writer.uint32(10).string(v!); + TankMeta.encode(v!, writer.uint32(10).fork()).join(); } return writer; }, @@ -37,7 +42,7 @@ export const Tanks: MessageFns = { break; } - message.tanks.push(reader.string()); + message.tanks.push(TankMeta.decode(reader, reader.uint32())); continue; } } @@ -50,13 +55,13 @@ export const Tanks: MessageFns = { }, fromJSON(object: any): Tanks { - return { tanks: globalThis.Array.isArray(object?.tanks) ? object.tanks.map((e: any) => globalThis.String(e)) : [] }; + return { tanks: globalThis.Array.isArray(object?.tanks) ? object.tanks.map((e: any) => TankMeta.fromJSON(e)) : [] }; }, toJSON(message: Tanks): unknown { const obj: any = {}; if (message.tanks?.length) { - obj.tanks = message.tanks; + obj.tanks = message.tanks.map((e) => TankMeta.toJSON(e)); } return obj; }, @@ -66,7 +71,83 @@ export const Tanks: MessageFns = { }, fromPartial, I>>(object: I): Tanks { const message = createBaseTanks(); - message.tanks = object.tanks?.map((e) => e) || []; + message.tanks = object.tanks?.map((e) => TankMeta.fromPartial(e)) || []; + return message; + }, +}; + +function createBaseTankMeta(): TankMeta { + return { id: "", seo_id: "" }; +} + +export const TankMeta: MessageFns = { + encode(message: TankMeta, writer: BinaryWriter = new BinaryWriter()): BinaryWriter { + if (message.id !== "") { + writer.uint32(10).string(message.id); + } + if (message.seo_id !== "") { + writer.uint32(18).string(message.seo_id); + } + return writer; + }, + + decode(input: BinaryReader | Uint8Array, length?: number): TankMeta { + const reader = input instanceof BinaryReader ? input : new BinaryReader(input); + let end = length === undefined ? reader.len : reader.pos + length; + const message = createBaseTankMeta(); + while (reader.pos < end) { + const tag = reader.uint32(); + switch (tag >>> 3) { + case 1: { + if (tag !== 10) { + break; + } + + message.id = reader.string(); + continue; + } + case 2: { + if (tag !== 18) { + break; + } + + message.seo_id = reader.string(); + continue; + } + } + if ((tag & 7) === 4 || tag === 0) { + break; + } + reader.skip(tag & 7); + } + return message; + }, + + fromJSON(object: any): TankMeta { + return { + id: globalThis.String(assertSet("TankMeta.id", object.id)), + seo_id: globalThis.String(assertSet("TankMeta.seo_id", object.seo_id)), + }; + }, + + toJSON(message: TankMeta): unknown { + const obj: any = {}; + if (message.id !== "") { + obj.id = message.id; + } + if (message.seo_id !== "") { + obj.seo_id = message.seo_id; + } + return obj; + }, + + create, I>>(base?: I): TankMeta { + return TankMeta.fromPartial(base ?? ({} as any)); + }, + fromPartial, I>>(object: I): TankMeta { + const message = createBaseTankMeta(); + message.id = object.id ?? ""; + message.seo_id = object.seo_id ?? ""; return message; }, }; @@ -84,6 +165,18 @@ type KeysOfUnion = T extends T ? keyof T : never; export type Exact = P extends Builtin ? P : P & { [K in keyof P]: Exact } & { [K in Exclude>]: never }; +function isSet(value: any): boolean { + return value !== null && value !== undefined; +} + +function assertSet(field: string, value: T | undefined): T { + if (!isSet(value)) { + throw new TypeError(`Required field ${field} is not set`); + } + + return value as T; +} + export interface MessageFns { encode(message: T, writer?: BinaryWriter): BinaryWriter; decode(input: BinaryReader | Uint8Array, length?: number): T; diff --git a/src/BlitzKit.CLI/Functions/Mangler.cs b/src/BlitzKit.CLI/Functions/Mangler.cs index e3a91f468..d8b740347 100644 --- a/src/BlitzKit.CLI/Functions/Mangler.cs +++ b/src/BlitzKit.CLI/Functions/Mangler.cs @@ -1,14 +1,17 @@ using System.Data; using System.Text; +using System.Text.RegularExpressions; using System.Text.Unicode; using Blitzkit; using BlitzKit.CLI.Models; +using BlitzKit.CLI.Utils; using CUE4Parse.UE4.Assets.Exports; using CUE4Parse.UE4.Assets.Exports.Engine; using CUE4Parse.UE4.Assets.Exports.StaticMesh; using CUE4Parse.UE4.Assets.Objects; using CUE4Parse.UE4.Objects.UObject; using CUE4Parse.UE4.Pak.Objects; +using Google.Protobuf; using Google.Protobuf.Collections; using Newtonsoft.Json.Linq; @@ -16,7 +19,6 @@ namespace BlitzKit.CLI.Functions { public class Mangler(string[] args) { - readonly Tanks tanks = new(); readonly BlitzProvider provider = new(args.Contains("--depot")); readonly List changes = []; @@ -37,6 +39,8 @@ namespace BlitzKit.CLI.Functions void MangleNation(VFS nation) { + Tanks tanks = new(); + foreach (var tankDir in nation.Directories) { // if (tankDir.Value.Name != "R90_IS_4") @@ -44,14 +48,28 @@ namespace BlitzKit.CLI.Functions var tank = MangleTank(tankDir.Value); - if (tank == null) - continue; + if (!tank.Name.Locales.TryGetValue("en", out var nameEn)) + { + throw new Exception($"Failed to get name for {tankDir.Value.Name}"); + } - tanks.Tanks_.Add(tank.Id); + var seoId = Diacritics.Remove(nameEn).ToLower(); + seoId = Regex.Replace(seoId, "[^a-z0-9]", "-"); + seoId = Regex.Replace(seoId, "--+", "-"); + seoId = Regex.Replace(seoId, "-$", ""); + + Console.WriteLine($"{nameEn} -> {seoId}"); + + TankMeta tankMeta = new() { Id = tank.Id, SeoId = seoId }; + + tanks.Tanks_.Add(tankMeta); } + + FileChange change = new("definitions/tanks.pb", tanks.ToByteArray()); + changes.Add(change); } - Tank? MangleTank(VFS tankDir) + Tank MangleTank(VFS tankDir) { var pdaName = $"PDA_{tankDir.Name}"; var pda = provider.LoadObject($"{tankDir.Path}/{pdaName}.{pdaName}"); @@ -68,7 +86,10 @@ namespace BlitzKit.CLI.Functions changes.Add(new($"tanks/{tankId}/collision/hull.glb", hullCollisionGltf.Write())); - return new() { Id = tankId }; + I18n Name = new(); + Name.Locales.Add("en", tankId); + + return new() { Id = tankId, Name = Name }; } Dictionary CrewTypes = new() diff --git a/src/BlitzKit.CLI/Protos/Tanks.cs b/src/BlitzKit.CLI/Protos/Tanks.cs index 70344d207..dc025f781 100644 --- a/src/BlitzKit.CLI/Protos/Tanks.cs +++ b/src/BlitzKit.CLI/Protos/Tanks.cs @@ -25,11 +25,13 @@ namespace Blitzkit { byte[] descriptorData = global::System.Convert.FromBase64String( string.Concat( "CiRwYWNrYWdlcy9jb3JlL3NyYy9wcm90b3MvdGFua3MucHJvdG8SCGJsaXR6", - "a2l0IhYKBVRhbmtzEg0KBXRhbmtzGAEgAygJ")); + "a2l0IioKBVRhbmtzEiEKBXRhbmtzGAEgAygLMhIuYmxpdHpraXQuVGFua01l", + "dGEiJgoIVGFua01ldGESCgoCaWQYASACKAkSDgoGc2VvX2lkGAIgAigJ")); descriptor = pbr::FileDescriptor.FromGeneratedCode(descriptorData, new pbr::FileDescriptor[] { }, new pbr::GeneratedClrTypeInfo(null, null, new pbr::GeneratedClrTypeInfo[] { - new pbr::GeneratedClrTypeInfo(typeof(global::Blitzkit.Tanks), global::Blitzkit.Tanks.Parser, new[]{ "Tanks_" }, null, null, null, null) + new pbr::GeneratedClrTypeInfo(typeof(global::Blitzkit.Tanks), global::Blitzkit.Tanks.Parser, new[]{ "Tanks_" }, null, null, null, null), + new pbr::GeneratedClrTypeInfo(typeof(global::Blitzkit.TankMeta), global::Blitzkit.TankMeta.Parser, new[]{ "Id", "SeoId" }, null, null, null, null) })); } #endregion @@ -83,12 +85,12 @@ namespace Blitzkit { /// Field number for the "tanks" field. public const int Tanks_FieldNumber = 1; - private static readonly pb::FieldCodec _repeated_tanks_codec - = pb::FieldCodec.ForString(10); - private readonly pbc::RepeatedField tanks_ = new pbc::RepeatedField(); + private static readonly pb::FieldCodec _repeated_tanks_codec + = pb::FieldCodec.ForMessage(10, global::Blitzkit.TankMeta.Parser); + private readonly pbc::RepeatedField tanks_ = new pbc::RepeatedField(); [global::System.Diagnostics.DebuggerNonUserCodeAttribute] [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] - public pbc::RepeatedField Tanks_ { + public pbc::RepeatedField Tanks_ { get { return tanks_; } } @@ -223,6 +225,269 @@ namespace Blitzkit { } + [global::System.Diagnostics.DebuggerDisplayAttribute("{ToString(),nq}")] + public sealed partial class TankMeta : pb::IMessage + #if !GOOGLE_PROTOBUF_REFSTRUCT_COMPATIBILITY_MODE + , pb::IBufferMessage + #endif + { + private static readonly pb::MessageParser _parser = new pb::MessageParser(() => new TankMeta()); + private pb::UnknownFieldSet _unknownFields; + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] + public static pb::MessageParser Parser { get { return _parser; } } + + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] + public static pbr::MessageDescriptor Descriptor { + get { return global::Blitzkit.TanksReflection.Descriptor.MessageTypes[1]; } + } + + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] + pbr::MessageDescriptor pb::IMessage.Descriptor { + get { return Descriptor; } + } + + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] + public TankMeta() { + OnConstruction(); + } + + partial void OnConstruction(); + + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] + public TankMeta(TankMeta other) : this() { + id_ = other.id_; + seoId_ = other.seoId_; + _unknownFields = pb::UnknownFieldSet.Clone(other._unknownFields); + } + + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] + public TankMeta Clone() { + return new TankMeta(this); + } + + /// Field number for the "id" field. + public const int IdFieldNumber = 1; + private readonly static string IdDefaultValue = ""; + + private string id_; + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] + public string Id { + get { return id_ ?? IdDefaultValue; } + set { + id_ = pb::ProtoPreconditions.CheckNotNull(value, "value"); + } + } + /// Gets whether the "id" field is set + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] + public bool HasId { + get { return id_ != null; } + } + /// Clears the value of the "id" field + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] + public void ClearId() { + id_ = null; + } + + /// Field number for the "seo_id" field. + public const int SeoIdFieldNumber = 2; + private readonly static string SeoIdDefaultValue = ""; + + private string seoId_; + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] + public string SeoId { + get { return seoId_ ?? SeoIdDefaultValue; } + set { + seoId_ = pb::ProtoPreconditions.CheckNotNull(value, "value"); + } + } + /// Gets whether the "seo_id" field is set + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] + public bool HasSeoId { + get { return seoId_ != null; } + } + /// Clears the value of the "seo_id" field + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] + public void ClearSeoId() { + seoId_ = null; + } + + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] + public override bool Equals(object other) { + return Equals(other as TankMeta); + } + + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] + public bool Equals(TankMeta other) { + if (ReferenceEquals(other, null)) { + return false; + } + if (ReferenceEquals(other, this)) { + return true; + } + if (Id != other.Id) return false; + if (SeoId != other.SeoId) return false; + return Equals(_unknownFields, other._unknownFields); + } + + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] + public override int GetHashCode() { + int hash = 1; + if (HasId) hash ^= Id.GetHashCode(); + if (HasSeoId) hash ^= SeoId.GetHashCode(); + if (_unknownFields != null) { + hash ^= _unknownFields.GetHashCode(); + } + return hash; + } + + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] + public override string ToString() { + return pb::JsonFormatter.ToDiagnosticString(this); + } + + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] + public void WriteTo(pb::CodedOutputStream output) { + #if !GOOGLE_PROTOBUF_REFSTRUCT_COMPATIBILITY_MODE + output.WriteRawMessage(this); + #else + if (HasId) { + output.WriteRawTag(10); + output.WriteString(Id); + } + if (HasSeoId) { + output.WriteRawTag(18); + output.WriteString(SeoId); + } + if (_unknownFields != null) { + _unknownFields.WriteTo(output); + } + #endif + } + + #if !GOOGLE_PROTOBUF_REFSTRUCT_COMPATIBILITY_MODE + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] + void pb::IBufferMessage.InternalWriteTo(ref pb::WriteContext output) { + if (HasId) { + output.WriteRawTag(10); + output.WriteString(Id); + } + if (HasSeoId) { + output.WriteRawTag(18); + output.WriteString(SeoId); + } + if (_unknownFields != null) { + _unknownFields.WriteTo(ref output); + } + } + #endif + + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] + public int CalculateSize() { + int size = 0; + if (HasId) { + size += 1 + pb::CodedOutputStream.ComputeStringSize(Id); + } + if (HasSeoId) { + size += 1 + pb::CodedOutputStream.ComputeStringSize(SeoId); + } + if (_unknownFields != null) { + size += _unknownFields.CalculateSize(); + } + return size; + } + + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] + public void MergeFrom(TankMeta other) { + if (other == null) { + return; + } + if (other.HasId) { + Id = other.Id; + } + if (other.HasSeoId) { + SeoId = other.SeoId; + } + _unknownFields = pb::UnknownFieldSet.MergeFrom(_unknownFields, other._unknownFields); + } + + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] + public void MergeFrom(pb::CodedInputStream input) { + #if !GOOGLE_PROTOBUF_REFSTRUCT_COMPATIBILITY_MODE + input.ReadRawMessage(this); + #else + uint tag; + while ((tag = input.ReadTag()) != 0) { + if ((tag & 7) == 4) { + // Abort on any end group tag. + return; + } + switch(tag) { + default: + _unknownFields = pb::UnknownFieldSet.MergeFieldFrom(_unknownFields, input); + break; + case 10: { + Id = input.ReadString(); + break; + } + case 18: { + SeoId = input.ReadString(); + break; + } + } + } + #endif + } + + #if !GOOGLE_PROTOBUF_REFSTRUCT_COMPATIBILITY_MODE + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] + void pb::IBufferMessage.InternalMergeFrom(ref pb::ParseContext input) { + uint tag; + while ((tag = input.ReadTag()) != 0) { + if ((tag & 7) == 4) { + // Abort on any end group tag. + return; + } + switch(tag) { + default: + _unknownFields = pb::UnknownFieldSet.MergeFieldFrom(_unknownFields, ref input); + break; + case 10: { + Id = input.ReadString(); + break; + } + case 18: { + SeoId = input.ReadString(); + break; + } + } + } + } + #endif + + } + #endregion } diff --git a/src/BlitzKit.CLI/Utils/Diacritics.cs b/src/BlitzKit.CLI/Utils/Diacritics.cs new file mode 100644 index 000000000..3467d32da --- /dev/null +++ b/src/BlitzKit.CLI/Utils/Diacritics.cs @@ -0,0 +1,26 @@ +using System.Globalization; +using System.Text; + +namespace BlitzKit.CLI.Utils +{ + public static class Diacritics + { + public static string Remove(string text) + { + var normalizedString = text.Normalize(NormalizationForm.FormD); + var stringBuilder = new StringBuilder(capacity: normalizedString.Length); + + for (int i = 0; i < normalizedString.Length; i++) + { + char c = normalizedString[i]; + var unicodeCategory = CharUnicodeInfo.GetUnicodeCategory(c); + if (unicodeCategory != UnicodeCategory.NonSpacingMark) + { + stringBuilder.Append(c); + } + } + + return stringBuilder.ToString().Normalize(NormalizationForm.FormC); + } + } +}