From 06deddcd8a826b0eae3f0cbfe8cb7a93c45985d7 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Wed, 1 Jun 2022 18:04:11 +0200 Subject: [PATCH] Extend the item identification a bit to count unidentifiable items and handle icons, demihumans and monsters. --- Penumbra.GameData/GamePathParser.cs | 472 +++++++------- Penumbra.GameData/ObjectIdentification.cs | 615 +++++++++--------- Penumbra.GameData/Structs/GameObjectInfo.cs | 281 ++++---- Penumbra/Collections/ModCollection.Cache.cs | 6 +- .../Classes/ModFileSystemSelector.Filters.cs | 2 +- Penumbra/UI/ConfigWindow.ChangedItemsTab.cs | 4 +- Penumbra/UI/ConfigWindow.Misc.cs | 6 + 7 files changed, 714 insertions(+), 672 deletions(-) diff --git a/Penumbra.GameData/GamePathParser.cs b/Penumbra.GameData/GamePathParser.cs index 8afa3d1b..a054ab39 100644 --- a/Penumbra.GameData/GamePathParser.cs +++ b/Penumbra.GameData/GamePathParser.cs @@ -7,34 +7,34 @@ using Penumbra.GameData.Enums; using Penumbra.GameData.Structs; using Penumbra.GameData.Util; -namespace Penumbra.GameData +namespace Penumbra.GameData; + +internal class GamePathParser : IGamePathParser { - internal class GamePathParser : IGamePathParser - { - 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"; + 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 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.Icon, new Regex[]{ new(@"ui/icon/(?'group'\d*)(/(?'lang'[a-z]{2}))?(/(?'hq'hq))?/(?'id'\d*)(?'hr'_hr1)?\.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(?'id'\d{4})/obj/body/b(?'weapon'\d{4})/texture/v(?'variant'\d{2})_w\k'id'b\k'weapon'(_[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") } } @@ -58,7 +58,7 @@ namespace Penumbra.GameData , { 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") } } } } + , { 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(?'id'\d{4})/obj/body/b(?'weapon'\d{4})/b\k'weapon'\.imc") } } , { ObjectType.Monster, new Regex[]{ new(@"chara/monster/m(?'monster'\d{4})/obj/body/b(?'id'\d{4})/b\k'id'\.imc") } } @@ -66,267 +66,267 @@ namespace Penumbra.GameData , { 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 + // @formatter:on - public ObjectType PathToObjectType( GamePath path ) + public ObjectType PathToObjectType( GamePath path ) + { + if( path.Empty ) { - if( path.Empty ) - { - 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, - }; + return ObjectType.Unknown; } - private (FileType, ObjectType, Match?) ParseGamePath( GamePath path ) + string p = path; + var folders = p.Split( '/' ); + if( folders.Length < 2 ) { - if( !Names.ExtensionToFileType.TryGetValue( Extension( path ), out var fileType ) ) + return ObjectType.Unknown; + } + + return folders[ 0 ] switch + { + CharacterFolder => folders[ 1 ] switch { - fileType = FileType.Unknown; - } - - var objectType = PathToObjectType( path ); - - if( !_regexes.TryGetValue( fileType, out var objectDict ) ) + 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 { - return ( fileType, objectType, null ); - } - - if( !objectDict.TryGetValue( objectType, out var regexes ) ) + IconFolder => ObjectType.Icon, + LoadingFolder => ObjectType.LoadingScreen, + MapFolder => ObjectType.Map, + InterfaceFolder => ObjectType.Interface, + _ => ObjectType.Unknown, + }, + CommonFolder => folders[ 1 ] switch { - return ( fileType, objectType, null ); - } - - foreach( var regex in regexes ) + FontFolder => ObjectType.Font, + _ => ObjectType.Unknown, + }, + HousingFolder => ObjectType.Housing, + WorldFolder1 => folders[ 1 ] switch { - var match = regex.Match( path ); - if( match.Success ) - { - return ( fileType, objectType, match ); - } - } + HousingFolder => ObjectType.Housing, + _ => ObjectType.World, + }, + WorldFolder2 => ObjectType.World, + VfxFolder => ObjectType.Vfx, + _ => ObjectType.Unknown, + }; + } + private (FileType, ObjectType, Match?) ParseGamePath( GamePath path ) + { + if( !Names.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 ); } - private static string Extension( string filename ) + if( !objectDict.TryGetValue( objectType, out var regexes ) ) { - var extIdx = filename.LastIndexOf( '.' ); - return extIdx < 0 ? "" : filename.Substring( extIdx ); + return ( fileType, objectType, null ); } - private static GameObjectInfo HandleEquipment( FileType fileType, GroupCollection groups ) + foreach( var regex in regexes ) { - var setId = ushort.Parse( groups[ "id" ].Value ); - if( fileType == FileType.Imc ) + var match = regex.Match( path ); + if( match.Success ) { - return GameObjectInfo.Equipment( fileType, setId ); + return ( fileType, objectType, match ); } - - var gr = Names.GenderRaceFromCode( groups[ "race" ].Value ); - var slot = Names.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 ); } - private static GameObjectInfo HandleWeapon( FileType fileType, GroupCollection groups ) - { - 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 ); - } + return ( fileType, objectType, null ); + } - var variant = byte.Parse( groups[ "variant" ].Value ); - return GameObjectInfo.Weapon( fileType, setId, weaponId, variant ); + private static string Extension( string filename ) + { + var extIdx = filename.LastIndexOf( '.' ); + return extIdx < 0 ? "" : filename.Substring( extIdx ); + } + + private static GameObjectInfo HandleEquipment( FileType fileType, GroupCollection groups ) + { + var setId = ushort.Parse( groups[ "id" ].Value ); + if( fileType == FileType.Imc ) + { + return GameObjectInfo.Equipment( fileType, setId ); } - private static GameObjectInfo HandleMonster( FileType fileType, GroupCollection groups ) + var gr = Names.GenderRaceFromCode( groups[ "race" ].Value ); + var slot = Names.SuffixToEquipSlot[ groups[ "slot" ].Value ]; + if( fileType == FileType.Model ) { - 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 ); + return GameObjectInfo.Equipment( fileType, setId, gr, slot ); } - private static GameObjectInfo HandleDemiHuman( FileType fileType, GroupCollection groups ) + var variant = byte.Parse( groups[ "variant" ].Value ); + return GameObjectInfo.Equipment( fileType, setId, gr, slot, variant ); + } + + private static GameObjectInfo HandleWeapon( FileType fileType, GroupCollection groups ) + { + var weaponId = ushort.Parse( groups[ "weapon" ].Value ); + var setId = ushort.Parse( groups[ "id" ].Value ); + if( fileType == FileType.Imc || fileType == FileType.Model ) { - var demiHumanId = ushort.Parse( groups[ "id" ].Value ); - var equipId = ushort.Parse( groups[ "equip" ].Value ); - if( fileType == FileType.Imc ) - { - return GameObjectInfo.DemiHuman( fileType, demiHumanId, equipId ); - } - - var slot = Names.SuffixToEquipSlot[ groups[ "slot" ].Value ]; - if( fileType == FileType.Model ) - { - return GameObjectInfo.DemiHuman( fileType, demiHumanId, equipId, slot ); - } - - var variant = byte.Parse( groups[ "variant" ].Value ); - return GameObjectInfo.DemiHuman( fileType, demiHumanId, equipId, slot, variant ); + return GameObjectInfo.Weapon( fileType, setId, weaponId ); } - private static GameObjectInfo HandleCustomization( FileType fileType, GroupCollection groups ) + var variant = byte.Parse( groups[ "variant" ].Value ); + return GameObjectInfo.Weapon( fileType, setId, weaponId, variant ); + } + + private static GameObjectInfo HandleMonster( FileType fileType, GroupCollection groups ) + { + var monsterId = ushort.Parse( groups[ "monster" ].Value ); + var bodyId = ushort.Parse( groups[ "id" ].Value ); + if( fileType == FileType.Imc || fileType == FileType.Model ) { - 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 = Names.GenderRaceFromCode( groups[ "race" ].Value ); - var bodySlot = Names.StringToBodySlot[ groups[ "type" ].Value ]; - var type = groups[ "slot" ].Success - ? Names.SuffixToCustomizationType[ groups[ "slot" ].Value ] - : CustomizationType.Skin; - 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 ); + return GameObjectInfo.Monster( fileType, monsterId, bodyId ); } - private static GameObjectInfo HandleIcon( FileType fileType, GroupCollection groups ) - { - var hq = groups[ "hq" ].Success; - var id = uint.Parse( groups[ "id" ].Value ); - if( !groups[ "lang" ].Success ) - { - return GameObjectInfo.Icon( fileType, id, hq ); - } + var variant = byte.Parse( groups[ "variant" ].Value ); + return GameObjectInfo.Monster( fileType, monsterId, bodyId, variant ); + } - 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 ); + private static GameObjectInfo HandleDemiHuman( FileType fileType, GroupCollection groups ) + { + var demiHumanId = ushort.Parse( groups[ "id" ].Value ); + var equipId = ushort.Parse( groups[ "equip" ].Value ); + if( fileType == FileType.Imc ) + { + return GameObjectInfo.DemiHuman( fileType, demiHumanId, equipId ); } - private static GameObjectInfo HandleMap( FileType fileType, GroupCollection groups ) + var slot = Names.SuffixToEquipSlot[ groups[ "slot" ].Value ]; + if( fileType == FileType.Model ) { - 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 ); + return GameObjectInfo.DemiHuman( fileType, demiHumanId, equipId, slot ); } - public GameObjectInfo GetFileInfo( GamePath path ) + var variant = byte.Parse( groups[ "variant" ].Value ); + return GameObjectInfo.DemiHuman( fileType, demiHumanId, equipId, slot, variant ); + } + + private static GameObjectInfo HandleCustomization( FileType fileType, GroupCollection groups ) + { + if( groups[ "skin" ].Success ) { - var (fileType, objectType, match) = ParseGamePath( path ); - if( match == null || !match.Success ) - { - return new GameObjectInfo { FileType = fileType, ObjectType = objectType }; - } + return GameObjectInfo.Customization( fileType, CustomizationType.Skin ); + } - try - { - var groups = match.Groups; - switch( objectType ) - { - case ObjectType.Accessory: return HandleEquipment( fileType, groups ); - case ObjectType.Equipment: return HandleEquipment( fileType, groups ); - case ObjectType.Weapon: return HandleWeapon( fileType, groups ); - case ObjectType.Map: return HandleMap( fileType, groups ); - case ObjectType.Monster: return HandleMonster( fileType, groups ); - case ObjectType.DemiHuman: return HandleDemiHuman( fileType, groups ); - case ObjectType.Character: return HandleCustomization( fileType, groups ); - case ObjectType.Icon: return HandleIcon( fileType, groups ); - } - } - catch( Exception e ) - { - PluginLog.Error( $"Could not parse {path}:\n{e}" ); - } + 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 = Names.GenderRaceFromCode( groups[ "race" ].Value ); + var bodySlot = Names.StringToBodySlot[ groups[ "type" ].Value ]; + var type = groups[ "slot" ].Success + ? Names.SuffixToCustomizationType[ groups[ "slot" ].Value ] + : CustomizationType.Skin; + if( fileType == FileType.Material ) + { + var variant = groups[ "variant" ].Success ? byte.Parse( groups[ "variant" ].Value ) : ( byte )0; + return GameObjectInfo.Customization( fileType, type, id, gr, bodySlot, variant ); + } + + return GameObjectInfo.Customization( fileType, type, id, gr, bodySlot ); + } + + private static GameObjectInfo HandleIcon( FileType fileType, GroupCollection groups ) + { + var hq = groups[ "hq" ].Success; + var hr = groups[ "hr" ].Success; + var id = uint.Parse( groups[ "id" ].Value ); + if( !groups[ "lang" ].Success ) + { + return GameObjectInfo.Icon( fileType, id, hq, hr ); + } + + 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, hr, language ); + } + + private static GameObjectInfo HandleMap( FileType fileType, GroupCollection groups ) + { + 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 ); + } + + public GameObjectInfo GetFileInfo( GamePath path ) + { + var (fileType, objectType, match) = ParseGamePath( path ); + if( match == null || !match.Success ) + { return new GameObjectInfo { FileType = fileType, ObjectType = objectType }; } - private readonly Regex _vfxRegexTmb = new( @"chara/action/(?'key'[^\s]+?)\.tmb" ); - private readonly Regex _vfxRegexPap = new( @"chara/human/c0101/animation/a0001/[^\s]+?/(?'key'[^\s]+?)\.pap" ); - - public string VfxToKey( GamePath path ) + try { - var match = _vfxRegexTmb.Match( path ); - if( match.Success ) + var groups = match.Groups; + switch( objectType ) { - return match.Groups[ "key" ].Value.ToLowerInvariant(); + case ObjectType.Accessory: return HandleEquipment( fileType, groups ); + case ObjectType.Equipment: return HandleEquipment( fileType, groups ); + case ObjectType.Weapon: return HandleWeapon( fileType, groups ); + case ObjectType.Map: return HandleMap( fileType, groups ); + case ObjectType.Monster: return HandleMonster( fileType, groups ); + case ObjectType.DemiHuman: return HandleDemiHuman( fileType, groups ); + case ObjectType.Character: return HandleCustomization( fileType, groups ); + case ObjectType.Icon: return HandleIcon( fileType, groups ); } - - match = _vfxRegexPap.Match( path ); - return match.Success ? match.Groups[ "key" ].Value.ToLowerInvariant() : string.Empty; } + catch( Exception e ) + { + PluginLog.Error( $"Could not parse {path}:\n{e}" ); + } + + return new GameObjectInfo { FileType = fileType, ObjectType = objectType }; + } + + private readonly Regex _vfxRegexTmb = new(@"chara/action/(?'key'[^\s]+?)\.tmb"); + private readonly Regex _vfxRegexPap = new(@"chara/human/c0101/animation/a0001/[^\s]+?/(?'key'[^\s]+?)\.pap"); + + public string VfxToKey( GamePath path ) + { + var match = _vfxRegexTmb.Match( path ); + if( match.Success ) + { + return match.Groups[ "key" ].Value.ToLowerInvariant(); + } + + match = _vfxRegexPap.Match( path ); + return match.Success ? match.Groups[ "key" ].Value.ToLowerInvariant() : string.Empty; } } \ No newline at end of file diff --git a/Penumbra.GameData/ObjectIdentification.cs b/Penumbra.GameData/ObjectIdentification.cs index 93e69b1d..e25e1ca2 100644 --- a/Penumbra.GameData/ObjectIdentification.cs +++ b/Penumbra.GameData/ObjectIdentification.cs @@ -1,324 +1,355 @@ -using System.Collections.Generic; -using System.ComponentModel; -using System.Linq; using Dalamud; using Dalamud.Data; using Lumina.Excel.GeneratedSheets; using Penumbra.GameData.Enums; using Penumbra.GameData.Structs; using Penumbra.GameData.Util; +using System.Collections.Generic; +using System.ComponentModel; +using System.Linq; using Action = Lumina.Excel.GeneratedSheets.Action; -namespace Penumbra.GameData +namespace Penumbra.GameData; + +internal class ObjectIdentification : IObjectIdentifier { - internal class ObjectIdentification : IObjectIdentifier + public static DataManager? DataManager = null!; + private readonly List< (ulong, HashSet< Item >) > _weapons; + private readonly List< (ulong, HashSet< Item >) > _equipment; + private readonly Dictionary< string, HashSet< Action > > _actions; + + private static bool Add( IDictionary< ulong, HashSet< Item > > dict, ulong key, Item item ) { - public static DataManager? DataManager = null!; - private readonly List< (ulong, HashSet< Item >) > _weapons; - private readonly List< (ulong, HashSet< Item >) > _equipment; - private readonly Dictionary< string, HashSet< Action > > _actions; - - private static bool Add( IDictionary< ulong, HashSet< Item > > dict, ulong key, Item item ) + if( dict.TryGetValue( key, out var list ) ) { - if( dict.TryGetValue( key, out var list ) ) - { - return list.Add( item ); - } - - dict[ key ] = new HashSet< Item > { item }; - return true; + return list.Add( item ); } - private static ulong EquipmentKey( Item i ) + dict[ key ] = new HashSet< Item > { item }; + return true; + } + + private static ulong EquipmentKey( Item i ) + { + var model = ( ulong )( ( Lumina.Data.Parsing.Quad )i.ModelMain ).A; + var variant = ( ulong )( ( Lumina.Data.Parsing.Quad )i.ModelMain ).B; + var slot = ( ulong )( ( EquipSlot )i.EquipSlotCategory.Row ).ToSlot(); + return ( model << 32 ) | ( slot << 16 ) | variant; + } + + private static ulong WeaponKey( Item i, bool offhand ) + { + var quad = offhand ? ( Lumina.Data.Parsing.Quad )i.ModelSub : ( Lumina.Data.Parsing.Quad )i.ModelMain; + var model = ( ulong )quad.A; + var type = ( ulong )quad.B; + var variant = ( ulong )quad.C; + + return ( model << 32 ) | ( type << 16 ) | variant; + } + + private void AddAction( string key, Action action ) + { + if( key.Length == 0 ) { - var model = ( ulong )( ( Lumina.Data.Parsing.Quad )i.ModelMain ).A; - var variant = ( ulong )( ( Lumina.Data.Parsing.Quad )i.ModelMain ).B; - var slot = ( ulong )( ( EquipSlot )i.EquipSlotCategory.Row ).ToSlot(); - return ( model << 32 ) | ( slot << 16 ) | variant; + return; } - private static ulong WeaponKey( Item i, bool offhand ) + key = key.ToLowerInvariant(); + if( _actions.TryGetValue( key, out var actions ) ) { - var quad = offhand ? ( Lumina.Data.Parsing.Quad )i.ModelSub : ( Lumina.Data.Parsing.Quad )i.ModelMain; - var model = ( ulong )quad.A; - var type = ( ulong )quad.B; - var variant = ( ulong )quad.C; - - return ( model << 32 ) | ( type << 16 ) | variant; + actions.Add( action ); } - - private void AddAction( string key, Action action ) + else { - if( key.Length == 0 ) - { - return; - } - - key = key.ToLowerInvariant(); - if( _actions.TryGetValue( key, out var actions ) ) - { - actions.Add( action ); - } - else - { - _actions[ key ] = new HashSet< Action > { action }; - } + _actions[ key ] = new HashSet< Action > { action }; } + } - public ObjectIdentification( DataManager dataManager, ClientLanguage clientLanguage ) + public ObjectIdentification( DataManager dataManager, ClientLanguage clientLanguage ) + { + DataManager = dataManager; + var items = dataManager.GetExcelSheet< Item >( clientLanguage )!; + SortedList< ulong, HashSet< Item > > weapons = new(); + SortedList< ulong, HashSet< Item > > equipment = new(); + foreach( var item in items ) { - DataManager = dataManager; - var items = dataManager.GetExcelSheet< Item >( clientLanguage )!; - SortedList< ulong, HashSet< Item > > weapons = new(); - SortedList< ulong, HashSet< Item > > equipment = new(); - foreach( var item in items ) - { - switch( ( EquipSlot )item.EquipSlotCategory.Row ) - { - case EquipSlot.MainHand: - case EquipSlot.OffHand: - case EquipSlot.BothHand: - if( item.ModelMain != 0 ) - { - Add( weapons, WeaponKey( item, false ), item ); - } - - if( item.ModelSub != 0 ) - { - Add( weapons, WeaponKey( item, true ), item ); - } - - break; - // Accessories - case EquipSlot.RFinger: - case EquipSlot.Wrists: - case EquipSlot.Ears: - case EquipSlot.Neck: - Add( equipment, EquipmentKey( item ), item ); - break; - // Equipment - case EquipSlot.Head: - case EquipSlot.Body: - case EquipSlot.Hands: - case EquipSlot.Legs: - case EquipSlot.Feet: - case EquipSlot.BodyHands: - case EquipSlot.BodyHandsLegsFeet: - case EquipSlot.BodyLegsFeet: - case EquipSlot.FullBody: - case EquipSlot.HeadBody: - case EquipSlot.LegsFeet: - Add( equipment, EquipmentKey( item ), item ); - break; - default: continue; - } - } - - _actions = new Dictionary< string, HashSet< Action > >(); - foreach( var action in dataManager.GetExcelSheet< Action >( clientLanguage )! - .Where( a => a.Name.ToString().Any() ) ) - { - var startKey = action.AnimationStart?.Value?.Name?.Value?.Key.ToString() ?? string.Empty; - var endKey = action.AnimationEnd?.Value?.Key.ToString() ?? string.Empty; - var hitKey = action.ActionTimelineHit?.Value?.Key.ToString() ?? string.Empty; - AddAction( startKey, action ); - AddAction( endKey, action ); - AddAction( hitKey, action ); - } - - _weapons = weapons.Select( kvp => ( kvp.Key, kvp.Value ) ).ToList(); - _equipment = equipment.Select( kvp => ( kvp.Key, kvp.Value ) ).ToList(); - } - - private class Comparer : IComparer< (ulong, HashSet< Item >) > - { - public int Compare( (ulong, HashSet< Item >) x, (ulong, HashSet< Item >) y ) - => x.Item1.CompareTo( y.Item1 ); - } - - private static (int, int) FindIndexRange( List< (ulong, HashSet< Item >) > list, ulong key, ulong mask ) - { - var maskedKey = key & mask; - var idx = list.BinarySearch( 0, list.Count, ( key, null! ), new Comparer() ); - if( idx < 0 ) - { - if( ~idx == list.Count || maskedKey != ( list[ ~idx ].Item1 & mask ) ) - { - return ( -1, -1 ); - } - - idx = ~idx; - } - - var endIdx = idx + 1; - while( endIdx < list.Count && maskedKey == ( list[ endIdx ].Item1 & mask ) ) - { - ++endIdx; - } - - return ( idx, endIdx ); - } - - private void FindEquipment( IDictionary< string, object? > set, GameObjectInfo info ) - { - var key = ( ulong )info.PrimaryId << 32; - var mask = 0xFFFF00000000ul; - if( info.EquipSlot != EquipSlot.Unknown ) - { - key |= ( ulong )info.EquipSlot.ToSlot() << 16; - mask |= 0xFFFF0000; - } - - if( info.Variant != 0 ) - { - key |= info.Variant; - mask |= 0xFFFF; - } - - var (start, end) = FindIndexRange( _equipment, key, mask ); - if( start == -1 ) - { - return; - } - - for( ; start < end; ++start ) - { - foreach( var item in _equipment[ start ].Item2 ) - { - set[ item.Name.ToString() ] = item; - } - } - } - - private void FindWeapon( IDictionary< string, object? > set, GameObjectInfo info ) - { - var key = ( ulong )info.PrimaryId << 32; - var mask = 0xFFFF00000000ul; - if( info.SecondaryId != 0 ) - { - key |= ( ulong )info.SecondaryId << 16; - mask |= 0xFFFF0000; - } - - if( info.Variant != 0 ) - { - key |= info.Variant; - mask |= 0xFFFF; - } - - var (start, end) = FindIndexRange( _weapons, key, mask ); - if( start == -1 ) - { - return; - } - - for( ; start < end; ++start ) - { - foreach( var item in _weapons[ start ].Item2 ) - { - set[ item.Name.ToString() ] = item; - } - } - } - - - private void IdentifyParsed( IDictionary< string, object? > set, GameObjectInfo info ) - { - switch( info.ObjectType ) - { - case ObjectType.Unknown: - case ObjectType.LoadingScreen: - case ObjectType.Map: - case ObjectType.Interface: - case ObjectType.Vfx: - case ObjectType.World: - case ObjectType.Housing: - case ObjectType.DemiHuman: - case ObjectType.Monster: - case ObjectType.Icon: - case ObjectType.Font: - // Don't do anything for these cases. - break; - case ObjectType.Accessory: - case ObjectType.Equipment: - FindEquipment( set, info ); - break; - case ObjectType.Weapon: - FindWeapon( set, info ); - break; - case ObjectType.Character: - var (gender, race) = info.GenderRace.Split(); - var raceString = race != ModelRace.Unknown ? race.ToName() + " " : ""; - var genderString = gender != Gender.Unknown ? gender.ToName() + " " : "Player "; - if( info.CustomizationType == CustomizationType.Skin ) - { - set[ $"Customization: {raceString}{genderString}Skin Textures" ] = null; - } - else - { - var customizationString = - $"Customization: {race} {gender} {info.BodySlot} ({info.CustomizationType}) {info.PrimaryId}"; - set[ customizationString ] = null; - } - - break; - - default: throw new InvalidEnumArgumentException(); - } - } - - private void IdentifyVfx( IDictionary< string, object? > set, GamePath path ) - { - var key = GameData.GamePathParser.VfxToKey( path ); - if( key.Length == 0 || !_actions.TryGetValue( key, out var actions ) ) - { - return; - } - - foreach( var action in actions ) - { - set[ $"Action: {action.Name}" ] = action; - } - } - - public void Identify( IDictionary< string, object? > set, GamePath path ) - { - if( ( ( string )path ).EndsWith( ".pap" ) || ( ( string )path ).EndsWith( ".tmb" ) ) - { - IdentifyVfx( set, path ); - } - else - { - var info = GameData.GamePathParser.GetFileInfo( path ); - IdentifyParsed( set, info ); - } - } - - public Dictionary< string, object? > Identify( GamePath path ) - { - Dictionary< string, object? > ret = new(); - Identify( ret, path ); - return ret; - } - - public Item? Identify( SetId setId, WeaponType weaponType, ushort variant, EquipSlot slot ) - { - switch( slot ) + switch( ( EquipSlot )item.EquipSlotCategory.Row ) { case EquipSlot.MainHand: case EquipSlot.OffHand: + case EquipSlot.BothHand: + if( item.ModelMain != 0 ) + { + Add( weapons, WeaponKey( item, false ), item ); + } + + if( item.ModelSub != 0 ) + { + Add( weapons, WeaponKey( item, true ), item ); + } + + break; + // Accessories + case EquipSlot.RFinger: + case EquipSlot.Wrists: + case EquipSlot.Ears: + case EquipSlot.Neck: + Add( equipment, EquipmentKey( item ), item ); + break; + // Equipment + case EquipSlot.Head: + case EquipSlot.Body: + case EquipSlot.Hands: + case EquipSlot.Legs: + case EquipSlot.Feet: + case EquipSlot.BodyHands: + case EquipSlot.BodyHandsLegsFeet: + case EquipSlot.BodyLegsFeet: + case EquipSlot.FullBody: + case EquipSlot.HeadBody: + case EquipSlot.LegsFeet: + Add( equipment, EquipmentKey( item ), item ); + break; + default: continue; + } + } + + _actions = new Dictionary< string, HashSet< Action > >(); + foreach( var action in dataManager.GetExcelSheet< Action >( clientLanguage )! + .Where( a => a.Name.ToString().Any() ) ) + { + var startKey = action.AnimationStart?.Value?.Name?.Value?.Key.ToString() ?? string.Empty; + var endKey = action.AnimationEnd?.Value?.Key.ToString() ?? string.Empty; + var hitKey = action.ActionTimelineHit?.Value?.Key.ToString() ?? string.Empty; + AddAction( startKey, action ); + AddAction( endKey, action ); + AddAction( hitKey, action ); + } + + _weapons = weapons.Select( kvp => ( kvp.Key, kvp.Value ) ).ToList(); + _equipment = equipment.Select( kvp => ( kvp.Key, kvp.Value ) ).ToList(); + } + + private class Comparer : IComparer< (ulong, HashSet< Item >) > + { + public int Compare( (ulong, HashSet< Item >) x, (ulong, HashSet< Item >) y ) + => x.Item1.CompareTo( y.Item1 ); + } + + private static (int, int) FindIndexRange( List< (ulong, HashSet< Item >) > list, ulong key, ulong mask ) + { + var maskedKey = key & mask; + var idx = list.BinarySearch( 0, list.Count, ( key, null! ), new Comparer() ); + if( idx < 0 ) + { + if( ~idx == list.Count || maskedKey != ( list[ ~idx ].Item1 & mask ) ) + { + return ( -1, -1 ); + } + + idx = ~idx; + } + + var endIdx = idx + 1; + while( endIdx < list.Count && maskedKey == ( list[ endIdx ].Item1 & mask ) ) + { + ++endIdx; + } + + return ( idx, endIdx ); + } + + private void FindEquipment( IDictionary< string, object? > set, GameObjectInfo info ) + { + var key = ( ulong )info.PrimaryId << 32; + var mask = 0xFFFF00000000ul; + if( info.EquipSlot != EquipSlot.Unknown ) + { + key |= ( ulong )info.EquipSlot.ToSlot() << 16; + mask |= 0xFFFF0000; + } + + if( info.Variant != 0 ) + { + key |= info.Variant; + mask |= 0xFFFF; + } + + var (start, end) = FindIndexRange( _equipment, key, mask ); + if( start == -1 ) + { + return; + } + + for( ; start < end; ++start ) + { + foreach( var item in _equipment[ start ].Item2 ) + { + set[ item.Name.ToString() ] = item; + } + } + } + + private void FindWeapon( IDictionary< string, object? > set, GameObjectInfo info ) + { + var key = ( ulong )info.PrimaryId << 32; + var mask = 0xFFFF00000000ul; + if( info.SecondaryId != 0 ) + { + key |= ( ulong )info.SecondaryId << 16; + mask |= 0xFFFF0000; + } + + if( info.Variant != 0 ) + { + key |= info.Variant; + mask |= 0xFFFF; + } + + var (start, end) = FindIndexRange( _weapons, key, mask ); + if( start == -1 ) + { + return; + } + + for( ; start < end; ++start ) + { + foreach( var item in _weapons[ start ].Item2 ) + { + set[ item.Name.ToString() ] = item; + } + } + } + + private static void AddCounterString( IDictionary< string, object? > set, string data ) + { + if( set.TryGetValue( data, out var obj ) && obj is int counter ) + { + set[ data ] = counter + 1; + } + else + { + set[ data ] = 1; + } + } + + private void IdentifyParsed( IDictionary< string, object? > set, GameObjectInfo info ) + { + switch( info.ObjectType ) + { + case ObjectType.Unknown: + switch( info.FileType ) { - var (begin, _) = FindIndexRange( _weapons, ( ( ulong )setId << 32 ) | ( ( ulong )weaponType << 16 ) | variant, - 0xFFFFFFFFFFFF ); - return begin >= 0 ? _weapons[ begin ].Item2.FirstOrDefault() : null; + case FileType.Sound: + AddCounterString( set, FileType.Sound.ToString() ); + break; + case FileType.Animation: + case FileType.Pap: + AddCounterString( set, FileType.Animation.ToString() ); + break; + case FileType.Shader: + AddCounterString( set, FileType.Shader.ToString() ); + break; } - default: + + break; + case ObjectType.LoadingScreen: + case ObjectType.Map: + case ObjectType.Interface: + case ObjectType.Vfx: + case ObjectType.World: + case ObjectType.Housing: + case ObjectType.Font: + AddCounterString( set, info.ObjectType.ToString() ); + break; + case ObjectType.DemiHuman: + set[ $"Demi Human: {info.PrimaryId}" ] = null; + break; + case ObjectType.Monster: + set[ $"Monster: {info.PrimaryId}" ] = null; + break; + case ObjectType.Icon: + set[ $"Icon: {info.IconId}" ] = null; + break; + case ObjectType.Accessory: + case ObjectType.Equipment: + FindEquipment( set, info ); + break; + case ObjectType.Weapon: + FindWeapon( set, info ); + break; + case ObjectType.Character: + var (gender, race) = info.GenderRace.Split(); + var raceString = race != ModelRace.Unknown ? race.ToName() + " " : ""; + var genderString = gender != Gender.Unknown ? gender.ToName() + " " : "Player "; + if( info.CustomizationType == CustomizationType.Skin ) { - var (begin, _) = FindIndexRange( _equipment, - ( ( ulong )setId << 32 ) | ( ( ulong )slot.ToSlot() << 16 ) | variant, - 0xFFFFFFFFFFFF ); - return begin >= 0 ? _equipment[ begin ].Item2.FirstOrDefault() : null; + set[ $"Customization: {raceString}{genderString}Skin Textures" ] = null; } + else + { + var customizationString = + $"Customization: {race} {gender} {info.BodySlot} ({info.CustomizationType}) {info.PrimaryId}"; + set[ customizationString ] = null; + } + + break; + + default: throw new InvalidEnumArgumentException(); + } + } + + private void IdentifyVfx( IDictionary< string, object? > set, GamePath path ) + { + var key = GameData.GamePathParser.VfxToKey( path ); + if( key.Length == 0 || !_actions.TryGetValue( key, out var actions ) ) + { + return; + } + + foreach( var action in actions ) + { + set[ $"Action: {action.Name}" ] = action; + } + } + + public void Identify( IDictionary< string, object? > set, GamePath path ) + { + if( ( ( string )path ).EndsWith( ".pap" ) || ( ( string )path ).EndsWith( ".tmb" ) ) + { + IdentifyVfx( set, path ); + } + else + { + var info = GameData.GamePathParser.GetFileInfo( path ); + IdentifyParsed( set, info ); + } + } + + public Dictionary< string, object? > Identify( GamePath path ) + { + Dictionary< string, object? > ret = new(); + Identify( ret, path ); + return ret; + } + + public Item? Identify( SetId setId, WeaponType weaponType, ushort variant, EquipSlot slot ) + { + switch( slot ) + { + case EquipSlot.MainHand: + case EquipSlot.OffHand: + { + var (begin, _) = FindIndexRange( _weapons, ( ( ulong )setId << 32 ) | ( ( ulong )weaponType << 16 ) | variant, + 0xFFFFFFFFFFFF ); + return begin >= 0 ? _weapons[ begin ].Item2.FirstOrDefault() : null; + } + default: + { + var (begin, _) = FindIndexRange( _equipment, + ( ( ulong )setId << 32 ) | ( ( ulong )slot.ToSlot() << 16 ) | variant, + 0xFFFFFFFFFFFF ); + return begin >= 0 ? _equipment[ begin ].Item2.FirstOrDefault() : null; } } } diff --git a/Penumbra.GameData/Structs/GameObjectInfo.cs b/Penumbra.GameData/Structs/GameObjectInfo.cs index 80b6c792..fae17494 100644 --- a/Penumbra.GameData/Structs/GameObjectInfo.cs +++ b/Penumbra.GameData/Structs/GameObjectInfo.cs @@ -3,158 +3,157 @@ using System.Runtime.InteropServices; using Dalamud; using Penumbra.GameData.Enums; -namespace Penumbra.GameData.Structs +namespace Penumbra.GameData.Structs; + +[StructLayout( LayoutKind.Explicit )] +public struct GameObjectInfo : IComparable { - [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, EquipSlot slot = EquipSlot.Unknown, - byte variant = 0 - ) - => 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 + public static GameObjectInfo Equipment( FileType type, ushort setId, GenderRace gr = GenderRace.Unknown + , EquipSlot slot = EquipSlot.Unknown, byte variant = 0 ) + => new() { - get => Names.GenderRaceFromByte( _genderRaceByte ); - set => _genderRaceByte = value.ToByte(); - } + FileType = type, + ObjectType = slot.IsAccessory() ? ObjectType.Accessory : ObjectType.Equipment, + PrimaryId = setId, + GenderRace = gr, + Variant = variant, + EquipSlot = slot, + }; - [FieldOffset( 5 )] - public BodySlot BodySlot; // Customization + 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, + }; - [FieldOffset( 5 )] - public byte MapC4; // Map + 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, + }; - [FieldOffset( 6 )] - public byte Variant; // Equipment, Weapon, Customization, Map, Monster, Demihuman + 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, + }; - [FieldOffset( 6 )] - public bool IconHq; // Icon + public static GameObjectInfo DemiHuman( FileType type, ushort demiHumanId, ushort bodyId, EquipSlot slot = EquipSlot.Unknown, + byte variant = 0 + ) + => new() + { + FileType = type, + ObjectType = ObjectType.DemiHuman, + PrimaryId = demiHumanId, + SecondaryId = bodyId, + Variant = variant, + EquipSlot = slot, + }; - [FieldOffset( 7 )] - public EquipSlot EquipSlot; // Equipment, Demihuman + 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, + }; - [FieldOffset( 7 )] - public CustomizationType CustomizationType; // Customization + public static GameObjectInfo Icon( FileType type, uint iconId, bool hq, bool hr, ClientLanguage lang = ClientLanguage.English ) + => new() + { + FileType = type, + ObjectType = ObjectType.Icon, + IconId = iconId, + IconHqHr = ( byte )( hq ? hr ? 3 : 1 : hr ? 2 : 0 ), + Language = lang, + }; - [FieldOffset( 7 )] - public ClientLanguage Language; // Icon - [FieldOffset( 7 )] - public byte MapSuffix; + [FieldOffset( 0 )] + public readonly ulong Identifier; - public override int GetHashCode() - => Identifier.GetHashCode(); + [FieldOffset( 0 )] + public FileType FileType; - public int CompareTo( object? r ) - => Identifier.CompareTo( r ); + [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 => Names.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 byte IconHqHr; // 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/Collections/ModCollection.Cache.cs b/Penumbra/Collections/ModCollection.Cache.cs index 44df1ae2..31cd55a3 100644 --- a/Penumbra/Collections/ModCollection.Cache.cs +++ b/Penumbra/Collections/ModCollection.Cache.cs @@ -437,7 +437,11 @@ public partial class ModCollection } else if( !data.Item1.Contains( modPath.Mod ) ) { - _changedItems[ name ] = ( data.Item1.Append( modPath.Mod ), obj ); + _changedItems[ name ] = ( data.Item1.Append( modPath.Mod ), obj is int x && data.Item2 is int y ? x + y : obj); + } + else if (obj is int x && data.Item2 is int y) + { + _changedItems[name] = (data.Item1, x + y); } } } diff --git a/Penumbra/UI/Classes/ModFileSystemSelector.Filters.cs b/Penumbra/UI/Classes/ModFileSystemSelector.Filters.cs index cc10eb12..25da4f4f 100644 --- a/Penumbra/UI/Classes/ModFileSystemSelector.Filters.cs +++ b/Penumbra/UI/Classes/ModFileSystemSelector.Filters.cs @@ -96,7 +96,7 @@ public partial class ModFileSystemSelector 0 => !( leaf.FullName().Contains( _modFilter.Lower, IgnoreCase ) || mod.Name.Contains( _modFilter ) ), 1 => !mod.Name.Contains( _modFilter ), 2 => !mod.Author.Contains( _modFilter ), - 3 => !mod.LowerChangedItemsString.Contains( _modFilter ), + 3 => !mod.LowerChangedItemsString.Contains( _modFilter.Lower, IgnoreCase ), _ => false, // Should never happen }; } diff --git a/Penumbra/UI/ConfigWindow.ChangedItemsTab.cs b/Penumbra/UI/ConfigWindow.ChangedItemsTab.cs index 96253526..596a48f0 100644 --- a/Penumbra/UI/ConfigWindow.ChangedItemsTab.cs +++ b/Penumbra/UI/ConfigWindow.ChangedItemsTab.cs @@ -24,7 +24,9 @@ public partial class ConfigWindow { // Functions in here for less pollution. bool FilterChangedItem( KeyValuePair< string, (SingleArray< Mod >, object?) > item ) - => ( _changedItemFilter.IsEmpty || item.Key.Contains( _changedItemFilter.Lower, StringComparison.InvariantCultureIgnoreCase ) ) + => ( _changedItemFilter.IsEmpty + || ChangedItemName( item.Key, item.Value.Item2 ) + .Contains( _changedItemFilter.Lower, StringComparison.InvariantCultureIgnoreCase ) ) && ( _changedItemModFilter.IsEmpty || item.Value.Item1.Any( m => m.Name.Contains( _changedItemModFilter ) ) ); void DrawChangedItemColumn( KeyValuePair< string, (SingleArray< Mod >, object?) > item ) diff --git a/Penumbra/UI/ConfigWindow.Misc.cs b/Penumbra/UI/ConfigWindow.Misc.cs index 995ad308..d9ab33e9 100644 --- a/Penumbra/UI/ConfigWindow.Misc.cs +++ b/Penumbra/UI/ConfigWindow.Misc.cs @@ -28,10 +28,16 @@ public partial class ConfigWindow private static unsafe void Text( ResourceHandle* resource ) => Text( resource->FileName(), resource->FileNameLength ); + + // Apply Changed Item Counters to the Name if necessary. + private static string ChangedItemName( string name, object? data ) + => data is int counter ? $"{counter} Files Manipulating {name}s" : name; + // Draw a changed item, invoking the Api-Events for clicks and tooltips. // Also draw the item Id in grey if requested private void DrawChangedItem( string name, object? data, bool drawId ) { + name = ChangedItemName( name, data ); var ret = ImGui.Selectable( name ) ? MouseButton.Left : MouseButton.None; ret = ImGui.IsItemClicked( ImGuiMouseButton.Right ) ? MouseButton.Right : ret; ret = ImGui.IsItemClicked( ImGuiMouseButton.Middle ) ? MouseButton.Middle : ret;