From 88ba14e595fd741239914e94e80214a7b3f9839e Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Thu, 4 Mar 2021 17:48:37 +0100 Subject: [PATCH] Enums and support for parsing and interpreting game paths. --- Penumbra/Game/Enums/BodySlot.cs | 43 +++ Penumbra/Game/Enums/CustomizationType.cs | 50 +++ Penumbra/Game/Enums/EquipSlot.cs | 96 ++++++ Penumbra/Game/Enums/FileType.cs | 45 +++ Penumbra/Game/Enums/ObjectType.cs | 21 ++ Penumbra/Game/Enums/Race.cs | 278 ++++++++++++++++ Penumbra/Game/GameObjectInfo.cs | 157 +++++++++ Penumbra/Game/GamePathParser.cs | 384 +++++++++++++++++++++++ 8 files changed, 1074 insertions(+) create mode 100644 Penumbra/Game/Enums/BodySlot.cs create mode 100644 Penumbra/Game/Enums/CustomizationType.cs create mode 100644 Penumbra/Game/Enums/EquipSlot.cs create mode 100644 Penumbra/Game/Enums/FileType.cs create mode 100644 Penumbra/Game/Enums/ObjectType.cs create mode 100644 Penumbra/Game/Enums/Race.cs create mode 100644 Penumbra/Game/GameObjectInfo.cs create mode 100644 Penumbra/Game/GamePathParser.cs diff --git a/Penumbra/Game/Enums/BodySlot.cs b/Penumbra/Game/Enums/BodySlot.cs new file mode 100644 index 00000000..f8474e5e --- /dev/null +++ b/Penumbra/Game/Enums/BodySlot.cs @@ -0,0 +1,43 @@ +using System.Collections.Generic; +using System.ComponentModel; + +namespace Penumbra.Game +{ + public enum BodySlot : byte + { + Unknown, + Hair, + Face, + Tail, + Body, + Zear + } + + public static class BodySlotEnumExtension + { + public static string ToSuffix( this BodySlot value ) + { + return value switch + { + BodySlot.Zear => "zear", + BodySlot.Face => "face", + BodySlot.Hair => "hair", + BodySlot.Body => "body", + BodySlot.Tail => "tail", + _ => throw new InvalidEnumArgumentException() + }; + } + } + + public static partial class GameData + { + public static readonly Dictionary< string, BodySlot > StringToBodySlot = new() + { + { BodySlot.Zear.ToSuffix(), BodySlot.Zear }, + { BodySlot.Face.ToSuffix(), BodySlot.Face }, + { BodySlot.Hair.ToSuffix(), BodySlot.Hair }, + { BodySlot.Body.ToSuffix(), BodySlot.Body }, + { BodySlot.Tail.ToSuffix(), BodySlot.Tail } + }; + } +} \ No newline at end of file diff --git a/Penumbra/Game/Enums/CustomizationType.cs b/Penumbra/Game/Enums/CustomizationType.cs new file mode 100644 index 00000000..ac82da2c --- /dev/null +++ b/Penumbra/Game/Enums/CustomizationType.cs @@ -0,0 +1,50 @@ +using System.Collections.Generic; +using System.ComponentModel; + +namespace Penumbra.Game +{ + public enum CustomizationType : byte + { + Unknown, + Body, + Tail, + Face, + Iris, + Accessory, + Hair, + DecalFace, + DecalEquip, + Skin, + Etc + } + + public static class CustomizationTypeEnumExtension + { + public static string ToSuffix( this CustomizationType value ) + { + return value switch + { + CustomizationType.Face => "fac", + CustomizationType.Iris => "iri", + CustomizationType.Accessory => "acc", + CustomizationType.Hair => "hir", + CustomizationType.Tail => "til", + CustomizationType.Etc => "etc", + _ => throw new InvalidEnumArgumentException() + }; + } + } + + public static partial class GameData + { + public static readonly Dictionary< string, CustomizationType > SuffixToCustomizationType = new() + { + { CustomizationType.Face.ToSuffix(), CustomizationType.Face }, + { CustomizationType.Iris.ToSuffix(), CustomizationType.Iris }, + { CustomizationType.Accessory.ToSuffix(), CustomizationType.Accessory }, + { CustomizationType.Hair.ToSuffix(), CustomizationType.Hair }, + { CustomizationType.Tail.ToSuffix(), CustomizationType.Tail }, + { CustomizationType.Etc.ToSuffix(), CustomizationType.Etc } + }; + } +} \ No newline at end of file diff --git a/Penumbra/Game/Enums/EquipSlot.cs b/Penumbra/Game/Enums/EquipSlot.cs new file mode 100644 index 00000000..10834112 --- /dev/null +++ b/Penumbra/Game/Enums/EquipSlot.cs @@ -0,0 +1,96 @@ +using System.Collections.Generic; +using System.ComponentModel; + +namespace Penumbra.Game +{ + public enum EquipSlot : byte + { + Unknown = 0, + MainHand = 1, + Offhand = 2, + Head = 3, + Body = 4, + Hands = 5, + Belt = 6, + Legs = 7, + Feet = 8, + Ears = 9, + Neck = 10, + RingR = 12, + RingL = 14, + Wrists = 11, + BothHand = 13, + HeadBody = 15, + BodyHandsLegsFeet = 16, + SoulCrystal = 17, + LegsFeet = 18, + FullBody = 19, + BodyHands = 20, + BodyLegsFeet = 21, + All = 22 + } + + public static class EquipSlotEnumExtension + { + public static string ToSuffix( this EquipSlot value ) + { + return value switch + { + EquipSlot.Head => "met", + EquipSlot.Hands => "glv", + EquipSlot.Legs => "dwn", + EquipSlot.Feet => "sho", + EquipSlot.Body => "top", + EquipSlot.Ears => "ear", + EquipSlot.Neck => "nek", + EquipSlot.RingR => "rir", + EquipSlot.RingL => "ril", + EquipSlot.Wrists => "wrs", + _ => throw new InvalidEnumArgumentException() + }; + } + + public static bool IsEquipment( this EquipSlot value ) + { + return value switch + { + EquipSlot.Head => true, + EquipSlot.Hands => true, + EquipSlot.Legs => true, + EquipSlot.Feet => true, + EquipSlot.Body => true, + _ => false + }; + } + + public static bool IsAccessory( this EquipSlot value ) + { + return value switch + { + EquipSlot.Ears => true, + EquipSlot.Neck => true, + EquipSlot.RingR => true, + EquipSlot.RingL => true, + EquipSlot.Wrists => true, + _ => false + }; + } + } + + public static partial class GameData + { + public static readonly Dictionary< string, EquipSlot > SuffixToEquipSlot = new() + { + { EquipSlot.Head.ToSuffix(), EquipSlot.Head }, + { EquipSlot.Hands.ToSuffix(), EquipSlot.Hands }, + { EquipSlot.Legs.ToSuffix(), EquipSlot.Legs }, + { EquipSlot.Feet.ToSuffix(), EquipSlot.Feet }, + { EquipSlot.Body.ToSuffix(), EquipSlot.Body }, + { EquipSlot.Ears.ToSuffix(), EquipSlot.Ears }, + { EquipSlot.Neck.ToSuffix(), EquipSlot.Neck }, + { EquipSlot.RingR.ToSuffix(), EquipSlot.RingR }, + { EquipSlot.RingL.ToSuffix(), EquipSlot.RingL }, + { EquipSlot.Wrists.ToSuffix(), EquipSlot.Wrists } + }; + } +} \ No newline at end of file diff --git a/Penumbra/Game/Enums/FileType.cs b/Penumbra/Game/Enums/FileType.cs new file mode 100644 index 00000000..6f8d92c4 --- /dev/null +++ b/Penumbra/Game/Enums/FileType.cs @@ -0,0 +1,45 @@ +using System.Collections.Generic; + +namespace Penumbra.Game +{ + public enum FileType : byte + { + Unknown, + Sound, + Imc, + Vfx, + Animation, + Pap, + MetaInfo, + Material, + Texture, + Model, + Shader, + Font, + Environment + } + + public static partial class GameData + { + public static readonly Dictionary< string, FileType > ExtensionToFileType = new() + { + { ".mdl", FileType.Model }, + { ".tex", FileType.Texture }, + { ".mtrl", FileType.Material }, + { ".atex", FileType.Animation }, + { ".avfx", FileType.Vfx }, + { ".scd", FileType.Sound }, + { ".imc", FileType.Imc }, + { ".pap", FileType.Pap }, + { ".eqp", FileType.MetaInfo }, + { ".eqdp", FileType.MetaInfo }, + { ".est", FileType.MetaInfo }, + { ".exd", FileType.MetaInfo }, + { ".exh", FileType.MetaInfo }, + { ".shpk", FileType.Shader }, + { ".shcd", FileType.Shader }, + { ".fdt", FileType.Font }, + { ".envb", FileType.Environment } + }; + } +} \ No newline at end of file diff --git a/Penumbra/Game/Enums/ObjectType.cs b/Penumbra/Game/Enums/ObjectType.cs new file mode 100644 index 00000000..5c7b7383 --- /dev/null +++ b/Penumbra/Game/Enums/ObjectType.cs @@ -0,0 +1,21 @@ +namespace Penumbra.Game +{ + public enum ObjectType : byte + { + Unknown, + Vfx, + DemiHuman, + Accessory, + World, + Housing, + Monster, + Icon, + LoadingScreen, + Map, + Interface, + Equipment, + Character, + Weapon, + Font + } +} \ No newline at end of file diff --git a/Penumbra/Game/Enums/Race.cs b/Penumbra/Game/Enums/Race.cs new file mode 100644 index 00000000..c772b25f --- /dev/null +++ b/Penumbra/Game/Enums/Race.cs @@ -0,0 +1,278 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel; + +namespace Penumbra.Game +{ + public enum Gender : byte + { + Unknown, + Male, + Female, + MaleNpc, + FemaleNpc + } + + public enum Race : byte + { + Unknown, + Midlander, + Highlander, + Elezen, + Lalafell, + Miqote, + Roegadyn, + AuRa, + Hrothgar, + Viera + } + + public enum GenderRace : ushort + { + Unknown = 0, + MidlanderMale = 0101, + MidlanderMaleNpc = 0104, + MidlanderFemale = 0201, + MidlanderFemaleNpc = 0204, + HighlanderMale = 0301, + HighlanderMaleNpc = 0304, + HighlanderFemale = 0401, + HighlanderFemaleNpc = 0404, + ElezenMale = 0501, + ElezenMaleNpc = 0504, + ElezenFemale = 0601, + ElezenFemaleNpc = 0604, + LalafellMale = 0701, + LalafellMaleNpc = 0704, + LalafellFemale = 0801, + LalafellFemaleNpc = 0804, + MiqoteMale = 0901, + MiqoteMaleNpc = 0904, + MiqoteFemale = 1001, + MiqoteFemaleNpc = 1004, + RoegadynMale = 1101, + RoegadynMaleNpc = 1104, + RoegadynFemale = 1201, + RoegadynFemaleNpc = 1204, + AuRaMale = 1301, + AuRaMaleNpc = 1304, + AuRaFemale = 1401, + AuRaFemaleNpc = 1404, + HrothgarMale = 1501, + HrothgarMaleNpc = 1504, + VieraFemale = 1801, + VieraFemaleNpc = 1804, + UnknownMaleNpc = 9104, + UnknownFemaleNpc = 9204 + } + + public static class RaceEnumExtensions + { + public static byte ToByte( this Gender gender, Race race ) + => ( byte )( ( int )gender | ( ( int )race << 3 ) ); + + public static byte ToByte( this Race race, Gender gender ) + => gender.ToByte( race ); + + public static byte ToByte( this GenderRace value ) + { + var (gender, race) = value.Split(); + return gender.ToByte( race ); + } + + public static (Gender, Race) Split( this GenderRace value ) + { + return value switch + { + GenderRace.MidlanderMale => ( Gender.Male, Race.Midlander ), + GenderRace.MidlanderMaleNpc => ( Gender.MaleNpc, Race.Midlander ), + GenderRace.MidlanderFemale => ( Gender.Female, Race.Midlander ), + GenderRace.MidlanderFemaleNpc => ( Gender.FemaleNpc, Race.Midlander ), + GenderRace.HighlanderMale => ( Gender.Male, Race.Highlander ), + GenderRace.HighlanderMaleNpc => ( Gender.MaleNpc, Race.Highlander ), + GenderRace.HighlanderFemale => ( Gender.Female, Race.Highlander ), + GenderRace.HighlanderFemaleNpc => ( Gender.FemaleNpc, Race.Highlander ), + GenderRace.ElezenMale => ( Gender.Male, Race.Elezen ), + GenderRace.ElezenMaleNpc => ( Gender.MaleNpc, Race.Elezen ), + GenderRace.ElezenFemale => ( Gender.Female, Race.Elezen ), + GenderRace.ElezenFemaleNpc => ( Gender.FemaleNpc, Race.Elezen ), + GenderRace.LalafellMale => ( Gender.Male, Race.Lalafell ), + GenderRace.LalafellMaleNpc => ( Gender.MaleNpc, Race.Lalafell ), + GenderRace.LalafellFemale => ( Gender.Female, Race.Lalafell ), + GenderRace.LalafellFemaleNpc => ( Gender.FemaleNpc, Race.Lalafell ), + GenderRace.MiqoteMale => ( Gender.Male, Race.Miqote ), + GenderRace.MiqoteMaleNpc => ( Gender.MaleNpc, Race.Miqote ), + GenderRace.MiqoteFemale => ( Gender.Female, Race.Miqote ), + GenderRace.MiqoteFemaleNpc => ( Gender.FemaleNpc, Race.Miqote ), + GenderRace.RoegadynMale => ( Gender.Male, Race.Roegadyn ), + GenderRace.RoegadynMaleNpc => ( Gender.MaleNpc, Race.Roegadyn ), + GenderRace.RoegadynFemale => ( Gender.Female, Race.Roegadyn ), + GenderRace.RoegadynFemaleNpc => ( Gender.FemaleNpc, Race.Roegadyn ), + GenderRace.AuRaMale => ( Gender.Male, Race.AuRa ), + GenderRace.AuRaMaleNpc => ( Gender.MaleNpc, Race.AuRa ), + GenderRace.AuRaFemale => ( Gender.Female, Race.AuRa ), + GenderRace.AuRaFemaleNpc => ( Gender.FemaleNpc, Race.AuRa ), + GenderRace.HrothgarMale => ( Gender.Male, Race.Hrothgar ), + GenderRace.HrothgarMaleNpc => ( Gender.MaleNpc, Race.Hrothgar ), + GenderRace.VieraFemale => ( Gender.Female, Race.Viera ), + GenderRace.VieraFemaleNpc => ( Gender.FemaleNpc, Race.Viera ), + GenderRace.UnknownMaleNpc => ( Gender.MaleNpc, Race.Unknown ), + GenderRace.UnknownFemaleNpc => ( Gender.FemaleNpc, Race.Unknown ), + _ => throw new InvalidEnumArgumentException() + }; + } + + public static bool IsValid( this GenderRace value ) + => value != GenderRace.Unknown && Enum.IsDefined( typeof( GenderRace ), value ); + + public static string ToRaceCode( this GenderRace value ) + { + return value switch + { + GenderRace.MidlanderMale => "0101", + GenderRace.MidlanderMaleNpc => "0104", + GenderRace.MidlanderFemale => "0201", + GenderRace.MidlanderFemaleNpc => "0204", + GenderRace.HighlanderMale => "0301", + GenderRace.HighlanderMaleNpc => "0304", + GenderRace.HighlanderFemale => "0401", + GenderRace.HighlanderFemaleNpc => "0404", + GenderRace.ElezenMale => "0501", + GenderRace.ElezenMaleNpc => "0504", + GenderRace.ElezenFemale => "0601", + GenderRace.ElezenFemaleNpc => "0604", + GenderRace.LalafellMale => "0701", + GenderRace.LalafellMaleNpc => "0704", + GenderRace.LalafellFemale => "0801", + GenderRace.LalafellFemaleNpc => "0804", + GenderRace.MiqoteMale => "0901", + GenderRace.MiqoteMaleNpc => "0904", + GenderRace.MiqoteFemale => "1001", + GenderRace.MiqoteFemaleNpc => "1004", + GenderRace.RoegadynMale => "1101", + GenderRace.RoegadynMaleNpc => "1104", + GenderRace.RoegadynFemale => "1201", + GenderRace.RoegadynFemaleNpc => "1204", + GenderRace.AuRaMale => "1301", + GenderRace.AuRaMaleNpc => "1304", + GenderRace.AuRaFemale => "1401", + GenderRace.AuRaFemaleNpc => "1404", + GenderRace.HrothgarMale => "1501", + GenderRace.HrothgarMaleNpc => "1504", + GenderRace.VieraFemale => "1801", + GenderRace.VieraFemaleNpc => "1804", + GenderRace.UnknownMaleNpc => "9104", + GenderRace.UnknownFemaleNpc => "9204", + _ => throw new InvalidEnumArgumentException() + }; + } + } + + public static partial class GameData + { + public static GenderRace GenderRaceFromCode( string code ) + { + return code switch + { + "0101" => GenderRace.MidlanderMale, + "0104" => GenderRace.MidlanderMaleNpc, + "0201" => GenderRace.MidlanderFemale, + "0204" => GenderRace.MidlanderFemaleNpc, + "0301" => GenderRace.HighlanderMale, + "0304" => GenderRace.HighlanderMaleNpc, + "0401" => GenderRace.HighlanderFemale, + "0404" => GenderRace.HighlanderFemaleNpc, + "0501" => GenderRace.ElezenMale, + "0504" => GenderRace.ElezenMaleNpc, + "0601" => GenderRace.ElezenFemale, + "0604" => GenderRace.ElezenFemaleNpc, + "0701" => GenderRace.LalafellMale, + "0704" => GenderRace.LalafellMaleNpc, + "0801" => GenderRace.LalafellFemale, + "0804" => GenderRace.LalafellFemaleNpc, + "0901" => GenderRace.MiqoteMale, + "0904" => GenderRace.MiqoteMaleNpc, + "1001" => GenderRace.MiqoteFemale, + "1004" => GenderRace.MiqoteFemaleNpc, + "1101" => GenderRace.RoegadynMale, + "1104" => GenderRace.RoegadynMaleNpc, + "1201" => GenderRace.RoegadynFemale, + "1204" => GenderRace.RoegadynFemaleNpc, + "1301" => GenderRace.AuRaMale, + "1304" => GenderRace.AuRaMaleNpc, + "1401" => GenderRace.AuRaFemale, + "1404" => GenderRace.AuRaFemaleNpc, + "1501" => GenderRace.HrothgarMale, + "1504" => GenderRace.HrothgarMaleNpc, + "1801" => GenderRace.VieraFemale, + "1804" => GenderRace.VieraFemaleNpc, + "9104" => GenderRace.UnknownMaleNpc, + "9204" => GenderRace.UnknownFemaleNpc, + _ => throw new KeyNotFoundException() + }; + } + + public static GenderRace GenderRaceFromByte( byte value ) + { + var gender = ( Gender )( value & 0b111 ); + var race = ( Race )( value >> 3 ); + return CombinedRace( gender, race ); + } + + public static GenderRace CombinedRace( Gender gender, Race race ) + { + return gender switch + { + Gender.Male => race switch + { + Race.Midlander => GenderRace.MidlanderMale, + Race.Highlander => GenderRace.HighlanderMale, + Race.Elezen => GenderRace.ElezenMale, + Race.Lalafell => GenderRace.LalafellMale, + Race.Miqote => GenderRace.MiqoteMale, + Race.Roegadyn => GenderRace.RoegadynMale, + Race.AuRa => GenderRace.AuRaMale, + Race.Hrothgar => GenderRace.HrothgarMale, + _ => GenderRace.Unknown + }, + Gender.MaleNpc => race switch + { + Race.Midlander => GenderRace.MidlanderMaleNpc, + Race.Highlander => GenderRace.HighlanderMaleNpc, + Race.Elezen => GenderRace.ElezenMaleNpc, + Race.Lalafell => GenderRace.LalafellMaleNpc, + Race.Miqote => GenderRace.MiqoteMaleNpc, + Race.Roegadyn => GenderRace.RoegadynMaleNpc, + Race.AuRa => GenderRace.AuRaMaleNpc, + Race.Hrothgar => GenderRace.HrothgarMaleNpc, + _ => GenderRace.Unknown + }, + Gender.Female => race switch + { + Race.Midlander => GenderRace.MidlanderFemale, + Race.Highlander => GenderRace.HighlanderFemale, + Race.Elezen => GenderRace.ElezenFemale, + Race.Lalafell => GenderRace.LalafellFemale, + Race.Miqote => GenderRace.MiqoteFemale, + Race.Roegadyn => GenderRace.RoegadynFemale, + Race.AuRa => GenderRace.AuRaFemale, + Race.Viera => GenderRace.VieraFemale, + _ => GenderRace.Unknown + }, + Gender.FemaleNpc => race switch + { + Race.Midlander => GenderRace.MidlanderFemaleNpc, + Race.Highlander => GenderRace.HighlanderFemaleNpc, + Race.Elezen => GenderRace.ElezenFemaleNpc, + Race.Lalafell => GenderRace.LalafellFemaleNpc, + Race.Miqote => GenderRace.MiqoteFemaleNpc, + Race.Roegadyn => GenderRace.RoegadynFemaleNpc, + Race.AuRa => GenderRace.AuRaFemaleNpc, + Race.Viera => GenderRace.VieraFemaleNpc, + _ => GenderRace.Unknown + }, + _ => GenderRace.Unknown + }; + } + } +} \ No newline at end of file diff --git a/Penumbra/Game/GameObjectInfo.cs b/Penumbra/Game/GameObjectInfo.cs new file mode 100644 index 00000000..3443cdf0 --- /dev/null +++ b/Penumbra/Game/GameObjectInfo.cs @@ -0,0 +1,157 @@ +using System; +using System.Runtime.InteropServices; +using Dalamud; + +namespace Penumbra.Game +{ + [StructLayout( LayoutKind.Explicit )] + public struct GameObjectInfo : IComparable + { + public static GameObjectInfo Equipment( FileType type, ushort setId, GenderRace gr = GenderRace.Unknown + , EquipSlot slot = EquipSlot.Unknown, byte variant = 0 ) + => new() + { + FileType = type, + ObjectType = slot.IsAccessory() ? ObjectType.Accessory : ObjectType.Equipment, + PrimaryId = setId, + GenderRace = gr, + Variant = variant, + EquipSlot = slot + }; + + public static GameObjectInfo Weapon( FileType type, ushort setId, ushort weaponId, byte variant = 0 ) + => new() + { + FileType = type, + ObjectType = ObjectType.Weapon, + PrimaryId = setId, + SecondaryId = weaponId, + Variant = variant + }; + + public static GameObjectInfo Customization( FileType type, CustomizationType customizationType, ushort id = 0 + , GenderRace gr = GenderRace.Unknown, BodySlot bodySlot = BodySlot.Unknown, byte variant = 0 ) + => new() + { + FileType = type, + ObjectType = ObjectType.Character, + PrimaryId = id, + GenderRace = gr, + BodySlot = bodySlot, + Variant = variant, + CustomizationType = customizationType + }; + + public static GameObjectInfo Monster( FileType type, ushort monsterId, ushort bodyId, byte variant = 0 ) + => new() + { + FileType = type, + ObjectType = ObjectType.Monster, + PrimaryId = monsterId, + SecondaryId = bodyId, + Variant = variant + }; + + public static GameObjectInfo DemiHuman( FileType type, ushort demiHumanId, ushort bodyId, byte variant = 0, + EquipSlot slot = EquipSlot.Unknown ) + => new() + { + FileType = type, + ObjectType = ObjectType.DemiHuman, + PrimaryId = demiHumanId, + SecondaryId = bodyId, + Variant = variant, + EquipSlot = slot + }; + + public static GameObjectInfo Map( FileType type, byte c1, byte c2, byte c3, byte c4, byte variant, byte suffix = 0 ) + => new() + { + FileType = type, + ObjectType = ObjectType.Map, + MapC1 = c1, + MapC2 = c2, + MapC3 = c3, + MapC4 = c4, + MapSuffix = suffix, + Variant = variant + }; + + public static GameObjectInfo Icon( FileType type, uint iconId, bool hq, ClientLanguage lang = ClientLanguage.English ) + => new() + { + FileType = type, + ObjectType = ObjectType.Map, + IconId = iconId, + IconHq = hq, + Language = lang + }; + + [FieldOffset( 0 )] + public readonly ulong Identifier; + + [FieldOffset( 0 )] + public FileType FileType; + + [FieldOffset( 1 )] + public ObjectType ObjectType; + + + [FieldOffset( 2 )] + public ushort PrimaryId; // Equipment, Weapon, Customization, Monster, DemiHuman + + [FieldOffset( 2 )] + public uint IconId; // Icon + + [FieldOffset( 2 )] + public byte MapC1; // Map + + [FieldOffset( 3 )] + public byte MapC2; // Map + + [FieldOffset( 4 )] + public ushort SecondaryId; // Weapon, Monster, Demihuman + + [FieldOffset( 4 )] + public byte MapC3; // Map + + [FieldOffset( 4 )] + private byte _genderRaceByte; // Equipment, Customization + + public GenderRace GenderRace + { + get => GameData.GenderRaceFromByte( _genderRaceByte ); + set => _genderRaceByte = value.ToByte(); + } + + [FieldOffset( 5 )] + public BodySlot BodySlot; // Customization + + [FieldOffset( 5 )] + public byte MapC4; // Map + + [FieldOffset( 6 )] + public byte Variant; // Equipment, Weapon, Customization, Map, Monster, Demihuman + + [FieldOffset( 6 )] + public bool IconHq; // Icon + + [FieldOffset( 7 )] + public EquipSlot EquipSlot; // Equipment, Demihuman + + [FieldOffset( 7 )] + public CustomizationType CustomizationType; // Customization + + [FieldOffset( 7 )] + public ClientLanguage Language; // Icon + + [FieldOffset( 7 )] + public byte MapSuffix; + + public override int GetHashCode() + => Identifier.GetHashCode(); + + public int CompareTo( object? r ) + => Identifier.CompareTo( r ); + } +} \ No newline at end of file diff --git a/Penumbra/Game/GamePathParser.cs b/Penumbra/Game/GamePathParser.cs new file mode 100644 index 00000000..321584d5 --- /dev/null +++ b/Penumbra/Game/GamePathParser.cs @@ -0,0 +1,384 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Text.RegularExpressions; +using Dalamud.Plugin; +using Penumbra.Util; + +namespace Penumbra.Game +{ + public static class GamePathParser + { + private const string CharacterFolder = "chara"; + private const string EquipmentFolder = "equipment"; + private const string PlayerFolder = "human"; + private const string WeaponFolder = "weapon"; + private const string AccessoryFolder = "accessory"; + private const string DemiHumanFolder = "demihuman"; + private const string MonsterFolder = "monster"; + private const string CommonFolder = "common"; + private const string UiFolder = "ui"; + private const string IconFolder = "icon"; + private const string LoadingFolder = "loadingimage"; + private const string MapFolder = "map"; + private const string InterfaceFolder = "uld"; + private const string FontFolder = "font"; + private const string HousingFolder = "hou"; + private const string VfxFolder = "vfx"; + private const string WorldFolder1 = "bgcommon"; + private const string WorldFolder2 = "bg"; + + // @formatter:off + private static readonly Dictionary> Regexes = new() + { { FileType.Font, new Dictionary< ObjectType, Regex[] >(){ { ObjectType.Font, new Regex[]{ new(@"common/font/(?'fontname'.*)_(?'id'\d\d)(_lobby)?\.fdt") } } } } + , { FileType.Texture, new Dictionary< ObjectType, Regex[] >() + { { ObjectType.Icon, new Regex[]{ new(@"ui/icon/(?'group'\d*)(/(?'lang'[a-z]{2}))?(/(?'hq'hq))?/(?'id'\d*)\.tex") } } + , { ObjectType.Map, new Regex[]{ new(@"ui/map/(?'id'[a-z0-9]{4})/(?'variant'\d{2})/\k'id'\k'variant'(?'suffix'[a-z])?(_[a-z])?\.tex") } } + , { ObjectType.Weapon, new Regex[]{ new(@"chara/weapon/w(?'weapon'\d{4})/obj/body/b(?'id'\d{4})/texture/v(?'variant'\d{2})_w\k'weapon'b\k'id'(_[a-z])?_[a-z]\.tex") } } + , { ObjectType.Monster, new Regex[]{ new(@"chara/monster/m(?'monster'\d{4})/obj/body/b(?'id'\d{4})/texture/v(?'variant'\d{2})_m\k'monster'b\k'id'(_[a-z])?_[a-z]\.tex") } } + , { ObjectType.Equipment, new Regex[]{ new(@"chara/equipment/e(?'id'\d{4})/texture/v(?'variant'\d{2})_c(?'race'\d{4})e\k'id'_(?'slot'[a-z]{3})(_[a-z])?_[a-z]\.tex") } } + , { ObjectType.DemiHuman, new Regex[]{ new(@"chara/demihuman/d(?'id'\d{4})/obj/equipment/e(?'equip'\d{4})/texture/v(?'variant'\d{2})_d\k'id'e\k'equip'_(?'slot'[a-z]{3})(_[a-z])?_[a-z]\.tex") } } + , { ObjectType.Accessory, new Regex[]{ new(@"chara/accessory/a(?'id'\d{4})/texture/v(?'variant'\d{2})_c(?'race'\d{4})a\k'id'_(?'slot'[a-z]{3})_[a-z]\.tex") } } + , { ObjectType.Character, new Regex[]{ new(@"chara/human/c(?'race'\d{4})/obj/(?'type'[a-z]+)/(?'typeabr'[a-z])(?'id'\d{4})/texture/(?'minus'(--)?)c\k'race'\k'typeabr'\k'id'_(?'slot'[a-z]{3})(_[a-z])?_[a-z]\.tex") + , new(@"chara/common/texture/skin(?'skin'.*)\.tex") + , new(@"chara/common/texture/decal_(?'location'[a-z]+)/[-_]?decal_(?'id'\d+).tex") } } } } + , { FileType.Model, new Dictionary< ObjectType, Regex[] >() + { { ObjectType.Weapon, new Regex[]{ new(@"chara/weapon/w(?'weapon'\d{4})/obj/body/b(?'id'\d{4})/model/w\k'weapon'b\k'id'\.mdl") } } + , { ObjectType.Monster, new Regex[]{ new(@"chara/monster/m(?'monster'\d{4})/obj/body/b(?'id'\d{4})/model/m\k'monster'b\k'id'\.mdl") } } + , { ObjectType.Equipment, new Regex[]{ new(@"chara/equipment/e(?'id'\d{4})/model/c(?'race'\d{4})e\k'id'_(?'slot'[a-z]{3})\.mdl") } } + , { ObjectType.DemiHuman, new Regex[]{ new(@"chara/demihuman/d(?'id'\d{4})/obj/equipment/e(?'equip'\d{4})/model/d\k'id'e\k'equip'_(?'slot'[a-z]{3})\.mdl") } } + , { ObjectType.Accessory, new Regex[]{ new(@"chara/accessory/a(?'id'\d{4})/model/c(?'race'\d{4})a\k'id'_(?'slot'[a-z]{3})\.mdl") } } + , { ObjectType.Character, new Regex[]{ new(@"chara/human/c(?'race'\d{4})/obj/(?'type'[a-z]+)/(?'typeabr'[a-z])(?'id'\d{4})/model/c\k'race'\k'typeabr'\k'id'_(?'slot'[a-z]{3})\.mdl") } } } } + , { FileType.Material, new Dictionary< ObjectType, Regex[] >() + { { ObjectType.Weapon, new Regex[]{ new(@"chara/weapon/w(?'weapon'\d{4})/obj/body/b(?'id'\d{4})/material/v(?'variant'\d{4})/mt_w\k'weapon'b\k'id'_[a-z]\.mtrl") } } + , { ObjectType.Monster, new Regex[]{ new(@"chara/monster/m(?'monster'\d{4})/obj/body/b(?'id'\d{4})/material/v(?'variant'\d{4})/mt_m\k'monster'b\k'id'_[a-z]\.mtrl") } } + , { ObjectType.Equipment, new Regex[]{ new(@"chara/equipment/e(?'id'\d{4})/material/v(?'variant'\d{4})/mt_c(?'race'\d{4})e\k'id'_(?'slot'[a-z]{3})_[a-z]\.mtrl") } } + , { ObjectType.DemiHuman, new Regex[]{ new(@"chara/demihuman/d(?'id'\d{4})/obj/equipment/e(?'equip'\d{4})/material/v(?'variant'\d{4})/mt_d\k'id'e\k'equip'_(?'slot'[a-z]{3})_[a-z]\.mtrl") } } + , { ObjectType.Accessory, new Regex[]{ new(@"chara/accessory/a(?'id'\d{4})/material/v(?'variant'\d{4})/mt_c(?'race'\d{4})a\k'id'_(?'slot'[a-z]{3})_[a-z]\.mtrl") } } + , { ObjectType.Character, new Regex[]{ new(@"chara/human/c(?'race'\d{4})/obj/(?'type'[a-z]+)/(?'typeabr'[a-z])(?'id'\d{4})/material/v(?'variant'\d{4})/mt_c\k'race'\k'typeabr'\k'id'_(?'slot'[a-z]{3})_[a-z]\.mtrl") } } } } + , { FileType.Imc, new Dictionary< ObjectType, Regex[] >() + { { ObjectType.Weapon, new Regex[]{ new(@"chara/weapon/w(?'weapon'\d{4})/obj/body/b(?'id'\d{4})/b\k'id'\.imc") } } + , { ObjectType.Monster, new Regex[]{ new(@"chara/monster/m(?'monster'\d{4})/obj/body/b(?'id'\d{4})/b\k'id'\.imc") } } + , { ObjectType.Equipment, new Regex[]{ new(@"chara/equipment/e(?'id'\d{4})/e\k'id'\.imc") } } + , { ObjectType.DemiHuman, new Regex[]{ new(@"chara/demihuman/d(?'id'\d{4})/obj/equipment/e(?'equip'\d{4})/e\k'equip'\.imc") } } + , { ObjectType.Accessory, new Regex[]{ new(@"chara/accessory/a(?'id'\d{4})/a\k'id'\.imc") } } } } + }; + // @formatter:on + + public static ObjectType PathToObjectType( GamePath path ) + { + if( !path ) + { + return ObjectType.Unknown; + } + + string p = path; + var folders = p.Split( '/' ); + if( folders.Length < 2 ) + { + return ObjectType.Unknown; + } + + return folders[ 0 ] switch + { + CharacterFolder => folders[ 1 ] switch + { + EquipmentFolder => ObjectType.Equipment, + AccessoryFolder => ObjectType.Accessory, + WeaponFolder => ObjectType.Weapon, + PlayerFolder => ObjectType.Character, + DemiHumanFolder => ObjectType.DemiHuman, + MonsterFolder => ObjectType.Monster, + CommonFolder => ObjectType.Character, + _ => ObjectType.Unknown + }, + UiFolder => folders[ 1 ] switch + { + IconFolder => ObjectType.Icon, + LoadingFolder => ObjectType.LoadingScreen, + MapFolder => ObjectType.Map, + InterfaceFolder => ObjectType.Interface, + _ => ObjectType.Unknown + }, + CommonFolder => folders[ 1 ] switch + { + FontFolder => ObjectType.Font, + _ => ObjectType.Unknown + }, + HousingFolder => ObjectType.Housing, + WorldFolder1 => folders[ 1 ] switch + { + HousingFolder => ObjectType.Housing, + _ => ObjectType.World + }, + WorldFolder2 => ObjectType.World, + VfxFolder => ObjectType.Vfx, + _ => ObjectType.Unknown + }; + } + + private static (FileType, ObjectType, Match?) ParseGamePath( GamePath path ) + { + if( !GameData.ExtensionToFileType.TryGetValue( Extension( path ), out var fileType ) ) + { + fileType = FileType.Unknown; + } + + var objectType = PathToObjectType( path ); + + if( !Regexes.TryGetValue( fileType, out var objectDict ) ) + { + return ( fileType, objectType, null ); + } + + if( !objectDict.TryGetValue( objectType, out var regexes ) ) + { + return ( fileType, objectType, null ); + } + + foreach( var regex in regexes ) + { + var match = regex.Match( path ); + if( match.Success ) + { + return ( fileType, objectType, match ); + } + } + + return ( fileType, objectType, null ); + } + + private static string Extension( string filename ) + { + var extIdx = filename.LastIndexOf( '.' ); + return extIdx < 0 ? "" : filename.Substring( extIdx ); + } + + private static GameObjectInfo HandleEquipment( FileType fileType, ObjectType objectType, GroupCollection groups ) + { + try + { + var setId = ushort.Parse( groups[ "id" ].Value ); + if( fileType == FileType.Imc ) + { + return GameObjectInfo.Equipment( fileType, setId ); + } + + var gr = GameData.GenderRaceFromCode( groups[ "race" ].Value ); + var slot = GameData.SuffixToEquipSlot[ groups[ "slot" ].Value ]; + if( fileType == FileType.Model ) + { + return GameObjectInfo.Equipment( fileType, setId, gr, slot ); + } + + var variant = byte.Parse( groups[ "variant" ].Value ); + return GameObjectInfo.Equipment( fileType, setId, gr, slot, variant ); + } + catch( Exception e ) + { + PluginLog.Error( $"Parsing game path failed:\n{e}" ); + return new GameObjectInfo { FileType = fileType, ObjectType = objectType }; + } + } + + private static GameObjectInfo HandleWeapon( FileType fileType, ObjectType objectType, GroupCollection groups ) + { + try + { + var weaponId = ushort.Parse( groups[ "weapon" ].Value ); + var setId = ushort.Parse( groups[ "id" ].Value ); + if( fileType == FileType.Imc || fileType == FileType.Model ) + { + return GameObjectInfo.Weapon( fileType, setId, weaponId ); + } + + var variant = byte.Parse( groups[ "variant" ].Value ); + return GameObjectInfo.Weapon( fileType, setId, weaponId, variant ); + } + catch( Exception e ) + { + PluginLog.Error( $"Parsing game path failed:\n{e}" ); + return new GameObjectInfo { FileType = fileType, ObjectType = objectType }; + } + } + + private static GameObjectInfo HandleMonster( FileType fileType, ObjectType objectType, GroupCollection groups ) + { + try + { + var monsterId = ushort.Parse( groups[ "monster" ].Value ); + var bodyId = ushort.Parse( groups[ "id" ].Value ); + if( fileType == FileType.Imc || fileType == FileType.Model ) + { + return GameObjectInfo.Monster( fileType, monsterId, bodyId ); + } + + var variant = byte.Parse( groups[ "variant" ].Value ); + return GameObjectInfo.Monster( fileType, monsterId, bodyId, variant ); + } + catch( Exception e ) + { + PluginLog.Error( $"Parsing game path failed:\n{e}" ); + return new GameObjectInfo { FileType = fileType, ObjectType = objectType }; + } + } + + private static GameObjectInfo HandleDemiHuman( FileType fileType, ObjectType objectType, GroupCollection groups ) + { + try + { + var demiHumanId = ushort.Parse( groups[ "monster" ].Value ); + var bodyId = ushort.Parse( groups[ "id" ].Value ); + if( fileType == FileType.Imc ) + { + return GameObjectInfo.DemiHuman( fileType, demiHumanId, bodyId ); + } + + var slot = GameData.SuffixToEquipSlot[ groups[ "slot" ].Value ]; + var variant = byte.Parse( groups[ "variant" ].Value ); + return GameObjectInfo.DemiHuman( fileType, demiHumanId, bodyId, variant, slot ); + } + catch( Exception e ) + { + PluginLog.Error( $"Parsing game path failed:\n{e}" ); + return new GameObjectInfo { FileType = fileType, ObjectType = objectType }; + } + } + + private static GameObjectInfo HandleCustomization( FileType fileType, ObjectType objectType, GroupCollection groups ) + { + try + { + if( groups[ "skin" ].Success ) + { + return GameObjectInfo.Customization( fileType, CustomizationType.Skin ); + } + + var id = ushort.Parse( groups[ "id" ].Value ); + if( groups[ "location" ].Success ) + { + var tmpType = groups[ "location" ].Value == "face" ? CustomizationType.DecalFace + : groups[ "location" ].Value == "equip" ? CustomizationType.DecalEquip : CustomizationType.Unknown; + return GameObjectInfo.Customization( fileType, tmpType, id ); + } + + var gr = GameData.GenderRaceFromCode( groups[ "race" ].Value ); + var bodySlot = GameData.StringToBodySlot[ groups[ "type" ].Value ]; + var type = GameData.SuffixToCustomizationType[ groups[ "slot" ].Value ]; + if( fileType == FileType.Material ) + { + var variant = byte.Parse( groups[ "variant" ].Value ); + return GameObjectInfo.Customization( fileType, type, id, gr, bodySlot, variant ); + } + + return GameObjectInfo.Customization( fileType, type, id, gr, bodySlot ); + } + catch( Exception e ) + { + PluginLog.Error( $"Parsing game path failed:\n{e}" ); + return new GameObjectInfo { FileType = fileType, ObjectType = objectType }; + } + } + + private static GameObjectInfo HandleIcon( FileType fileType, ObjectType objectType, GroupCollection groups ) + { + try + { + var hq = groups[ "hq" ].Success; + var id = uint.Parse( groups[ "id" ].Value ); + if( !groups[ "lang" ].Success ) + { + return GameObjectInfo.Icon( fileType, id, hq ); + } + + var language = groups[ "lang" ].Value switch + { + "en" => Dalamud.ClientLanguage.English, + "ja" => Dalamud.ClientLanguage.Japanese, + "de" => Dalamud.ClientLanguage.German, + "fr" => Dalamud.ClientLanguage.French, + _ => Dalamud.ClientLanguage.English + }; + return GameObjectInfo.Icon( fileType, id, hq, language ); + } + catch( Exception e ) + { + PluginLog.Error( $"Parsing game path failed:\n{e}" ); + return new GameObjectInfo { FileType = fileType, ObjectType = objectType }; + } + } + + private static GameObjectInfo HandleMap( FileType fileType, ObjectType objectType, GroupCollection groups ) + { + try + { + var map = Encoding.ASCII.GetBytes( groups[ "id" ].Value ); + var variant = byte.Parse( groups[ "variant" ].Value ); + if( groups[ "suffix" ].Success ) + { + var suffix = Encoding.ASCII.GetBytes( groups[ "suffix" ].Value )[ 0 ]; + return GameObjectInfo.Map( fileType, map[ 0 ], map[ 1 ], map[ 2 ], map[ 3 ], variant, suffix ); + } + + return GameObjectInfo.Map( fileType, map[ 0 ], map[ 1 ], map[ 2 ], map[ 3 ], variant ); + } + catch( Exception e ) + { + PluginLog.Error( $"Parsing game path failed:\n{e}" ); + return new GameObjectInfo { FileType = fileType, ObjectType = objectType }; + } + } + + public static GameObjectInfo GetFileInfo( GamePath path ) + { + var (fileType, objectType, match) = ParseGamePath( path ); + if( match == null || !match.Success ) + { + return new GameObjectInfo { FileType = fileType, ObjectType = objectType }; + } + + try + { + var groups = match.Groups; + switch( objectType ) + { + case ObjectType.Accessory: return HandleEquipment( fileType, objectType, groups ); + case ObjectType.Equipment: return HandleEquipment( fileType, objectType, groups ); + case ObjectType.Weapon: return HandleWeapon( fileType, objectType, groups ); + case ObjectType.Map: return HandleMap( fileType, objectType, groups ); + case ObjectType.Monster: return HandleMonster( fileType, objectType, groups ); + case ObjectType.DemiHuman: return HandleDemiHuman( fileType, objectType, groups ); + case ObjectType.Character: return HandleCustomization( fileType, objectType, groups ); + case ObjectType.Icon: return HandleIcon( fileType, objectType, groups ); + } + } + catch( Exception e ) + { + PluginLog.Error( $"Could not parse {path}:\n{e}" ); + } + + return new GameObjectInfo { FileType = fileType, ObjectType = objectType }; + } + + public static bool IsTailTexture( GameObjectInfo info ) + { + if( info.ObjectType != ObjectType.Character ) + { + return false; + } + + return info.BodySlot == BodySlot.Tail && info.FileType == FileType.Texture; + } + + public static bool IsSkinTexture( GameObjectInfo info ) + { + if( info.ObjectType != ObjectType.Character ) + { + return false; + } + + return info.FileType == FileType.Texture && info.CustomizationType == CustomizationType.Skin; + } + } +} \ No newline at end of file