diff --git a/Penumbra/Game/Enums/CustomizationType.cs b/Penumbra/Game/Enums/CustomizationType.cs index 4f380537..88fc63a5 100644 --- a/Penumbra/Game/Enums/CustomizationType.cs +++ b/Penumbra/Game/Enums/CustomizationType.cs @@ -24,6 +24,7 @@ namespace Penumbra.Game.Enums { return value switch { + CustomizationType.Body => "top", CustomizationType.Face => "fac", CustomizationType.Iris => "iri", CustomizationType.Accessory => "acc", @@ -39,6 +40,7 @@ namespace Penumbra.Game.Enums { public static readonly Dictionary< string, CustomizationType > SuffixToCustomizationType = new() { + { CustomizationType.Body.ToSuffix(), CustomizationType.Body }, { CustomizationType.Face.ToSuffix(), CustomizationType.Face }, { CustomizationType.Iris.ToSuffix(), CustomizationType.Iris }, { CustomizationType.Accessory.ToSuffix(), CustomizationType.Accessory }, diff --git a/Penumbra/Game/Enums/EquipSlot.cs b/Penumbra/Game/Enums/EquipSlot.cs index ba4654bb..3c942c94 100644 --- a/Penumbra/Game/Enums/EquipSlot.cs +++ b/Penumbra/Game/Enums/EquipSlot.cs @@ -16,10 +16,10 @@ namespace Penumbra.Game.Enums Feet = 8, Ears = 9, Neck = 10, - RingR = 12, - RingL = 14, Wrists = 11, + RingR = 12, BothHand = 13, + RingL = 14, // Not officially existing, means "weapon could be equipped in either hand" for the game. HeadBody = 15, BodyHandsLegsFeet = 16, SoulCrystal = 17, @@ -27,7 +27,7 @@ namespace Penumbra.Game.Enums FullBody = 19, BodyHands = 20, BodyLegsFeet = 21, - All = 22, + All = 22, // Not officially existing } public static class EquipSlotEnumExtension @@ -50,6 +50,35 @@ namespace Penumbra.Game.Enums }; } + public static EquipSlot ToSlot( this EquipSlot value ) + { + return value switch + { + EquipSlot.MainHand => EquipSlot.MainHand, + EquipSlot.Offhand => EquipSlot.Offhand, + EquipSlot.Head => EquipSlot.Head, + EquipSlot.Body => EquipSlot.Body, + EquipSlot.Hands => EquipSlot.Hands, + EquipSlot.Belt => EquipSlot.Belt, + EquipSlot.Legs => EquipSlot.Legs, + EquipSlot.Feet => EquipSlot.Feet, + EquipSlot.Ears => EquipSlot.Ears, + EquipSlot.Neck => EquipSlot.Neck, + EquipSlot.Wrists => EquipSlot.Wrists, + EquipSlot.RingR => EquipSlot.RingR, + EquipSlot.BothHand => EquipSlot.MainHand, + EquipSlot.RingL => EquipSlot.RingR, + EquipSlot.HeadBody => EquipSlot.Body, + EquipSlot.BodyHandsLegsFeet => EquipSlot.Body, + EquipSlot.SoulCrystal => EquipSlot.SoulCrystal, + EquipSlot.LegsFeet => EquipSlot.Legs, + EquipSlot.FullBody => EquipSlot.Body, + EquipSlot.BodyHands => EquipSlot.Body, + EquipSlot.BodyLegsFeet => EquipSlot.Body, + _ => throw new InvalidEnumArgumentException(), + }; + } + public static bool IsEquipment( this EquipSlot value ) { return value switch diff --git a/Penumbra/Game/Enums/Race.cs b/Penumbra/Game/Enums/Race.cs index cb8b694d..3957494b 100644 --- a/Penumbra/Game/Enums/Race.cs +++ b/Penumbra/Game/Enums/Race.cs @@ -64,18 +64,18 @@ namespace Penumbra.Game.Enums 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, + MiqoteMale = 0701, + MiqoteMaleNpc = 0704, + MiqoteFemale = 0801, + MiqoteFemaleNpc = 0804, + RoegadynMale = 0901, + RoegadynMaleNpc = 0904, + RoegadynFemale = 1001, + RoegadynFemaleNpc = 1004, + LalafellMale = 1101, + LalafellMaleNpc = 1104, + LalafellFemale = 1201, + LalafellFemaleNpc = 1204, AuRaMale = 1301, AuRaMaleNpc = 1304, AuRaFemale = 1401, @@ -158,6 +158,7 @@ namespace Penumbra.Game.Enums { return value switch { + GenderRace.Unknown => ( Gender.Unknown, Race.Unknown ), GenderRace.MidlanderMale => ( Gender.Male, Race.Midlander ), GenderRace.MidlanderMaleNpc => ( Gender.MaleNpc, Race.Midlander ), GenderRace.MidlanderFemale => ( Gender.Female, Race.Midlander ), @@ -215,18 +216,18 @@ namespace Penumbra.Game.Enums 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.MiqoteMale => "0701", + GenderRace.MiqoteMaleNpc => "0704", + GenderRace.MiqoteFemale => "0801", + GenderRace.MiqoteFemaleNpc => "0804", + GenderRace.RoegadynMale => "0901", + GenderRace.RoegadynMaleNpc => "0904", + GenderRace.RoegadynFemale => "1001", + GenderRace.RoegadynFemaleNpc => "1004", + GenderRace.LalafellMale => "1101", + GenderRace.LalafellMaleNpc => "1104", + GenderRace.LalafellFemale => "1201", + GenderRace.LalafellFemaleNpc => "1204", GenderRace.AuRaMale => "1301", GenderRace.AuRaMaleNpc => "1304", GenderRace.AuRaFemale => "1401", @@ -260,18 +261,18 @@ namespace Penumbra.Game.Enums "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, + "0701" => GenderRace.MiqoteMale, + "0704" => GenderRace.MiqoteMaleNpc, + "0801" => GenderRace.MiqoteFemale, + "0804" => GenderRace.MiqoteFemaleNpc, + "0901" => GenderRace.RoegadynMale, + "0904" => GenderRace.RoegadynMaleNpc, + "1001" => GenderRace.RoegadynFemale, + "1004" => GenderRace.RoegadynFemaleNpc, + "1101" => GenderRace.LalafellMale, + "1104" => GenderRace.LalafellMaleNpc, + "1201" => GenderRace.LalafellFemale, + "1204" => GenderRace.LalafellFemaleNpc, "1301" => GenderRace.AuRaMale, "1304" => GenderRace.AuRaMaleNpc, "1401" => GenderRace.AuRaFemale, diff --git a/Penumbra/Game/GameObjectInfo.cs b/Penumbra/Game/GameObjectInfo.cs index 927944b5..ddecf837 100644 --- a/Penumbra/Game/GameObjectInfo.cs +++ b/Penumbra/Game/GameObjectInfo.cs @@ -53,8 +53,9 @@ namespace Penumbra.Game Variant = variant, }; - public static GameObjectInfo DemiHuman( FileType type, ushort demiHumanId, ushort bodyId, byte variant = 0, - EquipSlot slot = EquipSlot.Unknown ) + public static GameObjectInfo DemiHuman( FileType type, ushort demiHumanId, ushort bodyId, EquipSlot slot = EquipSlot.Unknown, + byte variant = 0 + ) => new() { FileType = type, @@ -88,6 +89,7 @@ namespace Penumbra.Game Language = lang, }; + [FieldOffset( 0 )] public readonly ulong Identifier; diff --git a/Penumbra/Game/GamePathParser.cs b/Penumbra/Game/GamePathParser.cs index 0ecbf54b..f4837a3c 100644 --- a/Penumbra/Game/GamePathParser.cs +++ b/Penumbra/Game/GamePathParser.cs @@ -35,30 +35,30 @@ namespace Penumbra.Game , { 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.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") } } , { 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") + , { ObjectType.Character, new Regex[]{ new(@"chara/human/c(?'race'\d{4})/obj/(?'type'[a-z]+)/(?'typeabr'[a-z])(?'id'\d{4})/texture/(?'minus'(--)?)(v(?'variant'\d{2})_)?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.Weapon, new Regex[]{ new(@"chara/weapon/w(?'id'\d{4})/obj/body/b(?'weapon'\d{4})/model/w\k'id'b\k'weapon'\.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.Weapon, new Regex[]{ new(@"chara/weapon/w(?'id'\d{4})/obj/body/b(?'weapon'\d{4})/material/v(?'variant'\d{4})/mt_w\k'id'b\k'weapon'_[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.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") } } , { 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") } } @@ -228,16 +228,21 @@ namespace Penumbra.Game { try { - var demiHumanId = ushort.Parse( groups[ "monster" ].Value ); - var bodyId = ushort.Parse( groups[ "id" ].Value ); + var demiHumanId = ushort.Parse( groups[ "id" ].Value ); + var equipId = ushort.Parse( groups[ "equip" ].Value ); if( fileType == FileType.Imc ) { - return GameObjectInfo.DemiHuman( fileType, demiHumanId, bodyId ); + return GameObjectInfo.DemiHuman( fileType, demiHumanId, equipId ); + } + + var slot = GameData.SuffixToEquipSlot[ groups[ "slot" ].Value ]; + if( fileType == FileType.Model ) + { + return GameObjectInfo.DemiHuman( fileType, demiHumanId, equipId, slot ); } - var slot = GameData.SuffixToEquipSlot[ groups[ "slot" ].Value ]; var variant = byte.Parse( groups[ "variant" ].Value ); - return GameObjectInfo.DemiHuman( fileType, demiHumanId, bodyId, variant, slot ); + return GameObjectInfo.DemiHuman( fileType, demiHumanId, equipId, slot, variant ); } catch( Exception e ) { @@ -265,7 +270,7 @@ namespace Penumbra.Game var gr = GameData.GenderRaceFromCode( groups[ "race" ].Value ); var bodySlot = GameData.StringToBodySlot[ groups[ "type" ].Value ]; - var type = GameData.SuffixToCustomizationType[ groups[ "slot" ].Value ]; + var type = groups[ "slot" ].Success ? GameData.SuffixToCustomizationType[ groups[ "slot" ].Value ] : CustomizationType.Skin; if( fileType == FileType.Material ) { var variant = byte.Parse( groups[ "variant" ].Value ); @@ -361,24 +366,19 @@ namespace Penumbra.Game return new GameObjectInfo { FileType = fileType, ObjectType = objectType }; } - public static bool IsTailTexture( GameObjectInfo info ) + private static readonly Regex VfxRegexTmb = new( @"chara/action/(?'key'[^\s]+?)\.tmb" ); + private static readonly Regex VfxRegexPap = new( @"chara/human/c0101/animation/a0001/[^\s]+?/(?'key'[^\s]+?)\.pap" ); + + public static string VfxToKey( GamePath path ) { - if( info.ObjectType != ObjectType.Character ) + var match = VfxRegexTmb.Match( path ); + if( match.Success ) { - return false; + return match.Groups[ "key" ].Value.ToLowerInvariant(); } - 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; + match = VfxRegexPap.Match( path ); + return match.Success ? match.Groups[ "key" ].Value.ToLowerInvariant() : string.Empty; } } } \ No newline at end of file diff --git a/Penumbra/Game/ObjectIdentification.cs b/Penumbra/Game/ObjectIdentification.cs new file mode 100644 index 00000000..c84917c6 --- /dev/null +++ b/Penumbra/Game/ObjectIdentification.cs @@ -0,0 +1,290 @@ +using System.Collections.Generic; +using System.ComponentModel; +using System.Linq; +using Dalamud.Plugin; +using Lumina.Excel.GeneratedSheets; +using Penumbra.Game.Enums; +using Penumbra.Util; +using Action = Lumina.Excel.GeneratedSheets.Action; + +namespace Penumbra.Game +{ + public class ObjectIdentification + { + 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 ) ) + { + return list.Add( item ); + } + + 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 ) + { + return; + } + + key = key.ToLowerInvariant(); + if( _actions.TryGetValue( key, out var actions ) ) + { + actions.Add( action ); + } + else + { + _actions[ key ] = new HashSet< Action > { action }; + } + } + + public ObjectIdentification( DalamudPluginInterface plugin ) + { + var items = plugin.Data.GetExcelSheet< Item >( plugin.ClientState.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.RingR: + 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 plugin.Data.GetExcelSheet< Action >( plugin.ClientState.ClientLanguage ) ) + { + 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( 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: + if( info.CustomizationType == CustomizationType.Skin ) + { + set[ "Customization: Player Skin" ] = null; + } + else + { + var (gender, race) = info.GenderRace.Split(); + var customizationString = + $"Customization: {race} {gender}s {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 = 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 = GamePathParser.GetFileInfo( path ); + IdentifyParsed( set, info ); + } + } + } +} \ No newline at end of file diff --git a/Penumbra/Game/RspEntry.cs b/Penumbra/Game/RspEntry.cs index 0555809b..64faea47 100644 --- a/Penumbra/Game/RspEntry.cs +++ b/Penumbra/Game/RspEntry.cs @@ -13,6 +13,9 @@ namespace Penumbra.Game private readonly float[] Attributes; + public RspEntry( RspEntry copy ) + => Attributes = ( float[] )copy.Attributes.Clone(); + public RspEntry( byte[] bytes, int offset ) { if( offset < 0 || offset + ByteSize > bytes.Length ) diff --git a/Penumbra/Importer/TexToolsImport.cs b/Penumbra/Importer/TexToolsImport.cs index 0662c367..ae69bd95 100644 --- a/Penumbra/Importer/TexToolsImport.cs +++ b/Penumbra/Importer/TexToolsImport.cs @@ -197,11 +197,12 @@ namespace Penumbra.Importer public static DirectoryInfo CreateModFolder( DirectoryInfo outDirectory, string modListName ) { - var newModFolder = NewOptionDirectory( outDirectory, Path.GetFileName( modListName ) ); - var i = 2; + var newModFolderBase = NewOptionDirectory( outDirectory, Path.GetFileName( modListName ) ); + var newModFolder = newModFolderBase; + var i = 2; while( newModFolder.Exists && i < 12 ) { - newModFolder = new DirectoryInfo( newModFolder.FullName + $" ({i++})" ); + newModFolder = new DirectoryInfo( newModFolderBase.FullName + $" ({i++})" ); } if( newModFolder.Exists ) @@ -258,7 +259,7 @@ namespace Penumbra.Importer // Open the mod data file from the modpack as a SqPackStream using var modData = GetMagicSqPackDeleterStream( extractedModPack, "TTMPD.mpd" ); - ExtractedDirectory = CreateModFolder( _outDirectory, modList.Name ?? "New Mod" ); + ExtractedDirectory = CreateModFolder( _outDirectory, modList.Name ?? "New Mod" ); if( modList.SimpleModsList != null ) { @@ -349,7 +350,7 @@ namespace Penumbra.Importer TotalProgress += wtf.LongCount(); // Extract each SimpleMod into the new mod folder - foreach( var simpleMod in wtf.Where( M => M != null ) ) + foreach( var simpleMod in wtf.Where( m => m != null ) ) { ExtractMod( outDirectory, simpleMod, dataStream ); CurrentProgress++; diff --git a/Penumbra/Meta/Files/CmpFile.cs b/Penumbra/Meta/Files/CmpFile.cs index e8ed5132..85026b32 100644 --- a/Penumbra/Meta/Files/CmpFile.cs +++ b/Penumbra/Meta/Files/CmpFile.cs @@ -1,6 +1,5 @@ using System; using System.Collections.Generic; -using System.ComponentModel; using System.IO; using System.Linq; using Penumbra.Game; @@ -12,8 +11,8 @@ namespace Penumbra.Meta.Files { private const int RacialScalingStart = 0x2A800; - private readonly byte[] _byteData = new byte[RacialScalingStart]; - private readonly List< RspEntry > _rspEntries; + private readonly byte[] _byteData = new byte[RacialScalingStart]; + private readonly RspEntry[] _rspEntries; public CmpFile( byte[] bytes ) { @@ -24,11 +23,13 @@ namespace Penumbra.Meta.Files Array.Copy( bytes, _byteData, RacialScalingStart ); var rspEntryNum = ( bytes.Length - RacialScalingStart ) / RspEntry.ByteSize; - _rspEntries = new List< RspEntry >( rspEntryNum ); + var tmp = new List< RspEntry >( rspEntryNum ); for( var i = 0; i < rspEntryNum; ++i ) { - _rspEntries.Add( new RspEntry( bytes, RacialScalingStart + i * RspEntry.ByteSize ) ); + tmp.Add( new RspEntry( bytes, RacialScalingStart + i * RspEntry.ByteSize ) ); } + + _rspEntries = tmp.ToArray(); } public RspEntry this[ SubRace subRace ] @@ -49,7 +50,7 @@ namespace Penumbra.Meta.Files public byte[] WriteBytes() { - using var s = new MemoryStream( RacialScalingStart + _rspEntries.Count * RspEntry.ByteSize ); + using var s = new MemoryStream( RacialScalingStart + _rspEntries.Length * RspEntry.ByteSize ); s.Write( _byteData, 0, _byteData.Length ); foreach( var entry in _rspEntries ) { @@ -60,10 +61,10 @@ namespace Penumbra.Meta.Files return s.ToArray(); } - private CmpFile( byte[] data, List< RspEntry > entries ) + private CmpFile( byte[] data, RspEntry[] entries ) { _byteData = data.ToArray(); - _rspEntries = entries.ToList(); + _rspEntries = entries.Select( e => new RspEntry( e ) ).ToArray(); } public CmpFile Clone() diff --git a/Penumbra/Meta/MetaManager.cs b/Penumbra/Meta/MetaManager.cs index 1b412726..dee114b2 100644 --- a/Penumbra/Meta/MetaManager.cs +++ b/Penumbra/Meta/MetaManager.cs @@ -73,7 +73,7 @@ namespace Penumbra.Meta } } - private void Reset( bool reload ) + public void Reset( bool reload = true ) { foreach( var file in _currentFiles ) { @@ -90,9 +90,6 @@ namespace Penumbra.Meta } } - public void Reset() - => Reset( true ); - public void Dispose() => Reset(); diff --git a/Penumbra/Mod/ModData.cs b/Penumbra/Mod/ModData.cs index 0ed16f5c..8efe3f9d 100644 --- a/Penumbra/Mod/ModData.cs +++ b/Penumbra/Mod/ModData.cs @@ -1,5 +1,9 @@ +using System.Collections.Generic; using System.IO; +using System.Linq; using Dalamud.Plugin; +using Penumbra.Game; +using Penumbra.Util; namespace Penumbra.Mod { @@ -12,7 +16,7 @@ namespace Penumbra.Mod public ModMeta Meta; public ModResources Resources; public string SortOrder; - + public SortedList< string, object? > ChangedItems { get; } = new(); public FileInfo MetaFile { get; set; } private ModData( DirectoryInfo basePath, ModMeta meta, ModResources resources ) @@ -22,6 +26,26 @@ namespace Penumbra.Mod Resources = resources; MetaFile = MetaFileInfo( basePath ); SortOrder = meta.Name; + ComputeChangedItems(); + } + + public void ComputeChangedItems() + { + var ident = Service< ObjectIdentification >.Get(); + + ChangedItems.Clear(); + foreach( var file in Resources.ModFiles.Select( f => new RelPath( f, BasePath ) ) ) + { + foreach( var path in ModFunctions.GetAllFiles( file, Meta ) ) + { + ident.Identify( ChangedItems, path ); + } + } + + foreach( var path in Meta.FileSwaps.Keys ) + { + ident.Identify( ChangedItems, path ); + } } public static FileInfo MetaFileInfo( DirectoryInfo basePath ) diff --git a/Penumbra/Mod/ModFunctions.cs b/Penumbra/Mod/ModFunctions.cs index bb022e47..675f1dfc 100644 --- a/Penumbra/Mod/ModFunctions.cs +++ b/Penumbra/Mod/ModFunctions.cs @@ -39,6 +39,25 @@ namespace Penumbra.Mod return files; } + public static HashSet< GamePath > GetAllFiles( RelPath relPath, ModMeta meta ) + { + var ret = new HashSet< GamePath >(); + foreach( var option in meta.Groups.Values.SelectMany( g => g.Options ) ) + { + if( option.OptionFiles.TryGetValue( relPath, out var files ) ) + { + ret.UnionWith( files ); + } + } + + if( ret.Count == 0 ) + { + ret.Add( new GamePath( relPath, 0 ) ); + } + + return ret; + } + public static ModSettings ConvertNamedSettings( NamedModSettings namedSettings, ModMeta meta ) { ModSettings ret = new() diff --git a/Penumbra/Mod/ModMeta.cs b/Penumbra/Mod/ModMeta.cs index 117cd1a8..e7d41912 100644 --- a/Penumbra/Mod/ModMeta.cs +++ b/Penumbra/Mod/ModMeta.cs @@ -18,7 +18,6 @@ namespace Penumbra.Mod public string Description { get; set; } = ""; public string Version { get; set; } = ""; public string Website { get; set; } = ""; - public List< string > ChangedItems { get; set; } = new(); [JsonProperty( ItemConverterType = typeof( GamePathConverter ) )] public Dictionary< GamePath, GamePath > FileSwaps { get; set; } = new(); @@ -50,7 +49,6 @@ namespace Penumbra.Mod Description = newMeta.Description; Version = newMeta.Version; Website = newMeta.Website; - ChangedItems = newMeta.ChangedItems; FileSwaps = newMeta.FileSwaps; Groups = newMeta.Groups; FileHash = newMeta.FileHash; diff --git a/Penumbra/Mod/ModResources.cs b/Penumbra/Mod/ModResources.cs index 69cf94ec..0cfe7154 100644 --- a/Penumbra/Mod/ModResources.cs +++ b/Penumbra/Mod/ModResources.cs @@ -45,7 +45,6 @@ namespace Penumbra.Mod } } - // Update the current set of files used by the mod, // returns true if anything changed. public ResourceChange RefreshModFiles( DirectoryInfo basePath ) @@ -70,13 +69,13 @@ namespace Penumbra.Mod } ResourceChange changes = 0; - if( !tmpFiles.SequenceEqual( ModFiles ) ) + if( !tmpFiles.Select( f => f.FullName ).SequenceEqual( ModFiles.Select( f => f.FullName ) ) ) { ModFiles = tmpFiles; changes |= ResourceChange.Files; } - if( !tmpMetas.SequenceEqual( MetaFiles ) ) + if( !tmpMetas.Select( f => f.FullName ).SequenceEqual( MetaFiles.Select( f => f.FullName ) ) ) { MetaFiles = tmpMetas; changes |= ResourceChange.Meta; diff --git a/Penumbra/Mods/ModCollectionCache.cs b/Penumbra/Mods/ModCollectionCache.cs index d00239cf..101369ab 100644 --- a/Penumbra/Mods/ModCollectionCache.cs +++ b/Penumbra/Mods/ModCollectionCache.cs @@ -23,7 +23,8 @@ namespace Penumbra.Mods public void SortMods() { - AvailableMods.Sort( ( m1, m2 ) => string.Compare( m1.Data.SortOrder, m2.Data.SortOrder, StringComparison.InvariantCultureIgnoreCase ) ); + AvailableMods.Sort( ( m1, m2 ) + => string.Compare( m1.Data.SortOrder, m2.Data.SortOrder, StringComparison.InvariantCultureIgnoreCase ) ); } private void AddFiles( Dictionary< GamePath, Mod.Mod > registeredFiles, Mod.Mod mod ) @@ -79,7 +80,7 @@ namespace Penumbra.Mods public void UpdateMetaManipulations() { - MetaManipulations.Reset(); + MetaManipulations.Reset( false ); foreach( var mod in AvailableMods.Where( m => m.Settings.Enabled && m.Data.Resources.MetaManipulations.Count > 0 ) .OrderByDescending( m => m.Settings.Priority ) ) diff --git a/Penumbra/Mods/ModManager.cs b/Penumbra/Mods/ModManager.cs index 0f2d9c74..b3294087 100644 --- a/Penumbra/Mods/ModManager.cs +++ b/Penumbra/Mods/ModManager.cs @@ -141,6 +141,11 @@ namespace Penumbra.Mods return false; } + if( metaChanges || fileChanges.HasFlag( ResourceChange.Files ) ) + { + mod.ComputeChangedItems(); + } + var nameChange = !string.Equals( oldName, mod.Meta.Name, StringComparison.InvariantCulture ); recomputeMeta |= fileChanges.HasFlag( ResourceChange.Meta ); diff --git a/Penumbra/Penumbra.csproj b/Penumbra/Penumbra.csproj index d0622987..395f73c2 100644 --- a/Penumbra/Penumbra.csproj +++ b/Penumbra/Penumbra.csproj @@ -51,6 +51,12 @@ $(AppData)\XIVLauncher\addon\Hooks\dev\Lumina.dll False + + $(DALAMUD_ROOT)\Lumina.Excel.dll + ..\libs\Lumina.Excel.dll + $(AppData)\XIVLauncher\addon\Hooks\dev\Lumina.Excel.dll + False + diff --git a/Penumbra/Plugin.cs b/Penumbra/Plugin.cs index 8d9a7a4b..750fb445 100644 --- a/Penumbra/Plugin.cs +++ b/Penumbra/Plugin.cs @@ -1,5 +1,3 @@ -using System.Linq; -using Dalamud.Game.ClientState.Actors.Types; using Dalamud.Game.Command; using Dalamud.Plugin; using EmbedIO; @@ -41,6 +39,7 @@ namespace Penumbra { PluginInterface = pluginInterface; Service< DalamudPluginInterface >.Set( PluginInterface ); + Service< ObjectIdentification >.Set( PluginInterface ); Configuration = Configuration.Load( PluginInterface ); diff --git a/Penumbra/UI/MenuTabs/TabInstalled/TabInstalledDetails.cs b/Penumbra/UI/MenuTabs/TabInstalled/TabInstalledDetails.cs index 38e3525a..4306d73a 100644 --- a/Penumbra/UI/MenuTabs/TabInstalled/TabInstalledDetails.cs +++ b/Penumbra/UI/MenuTabs/TabInstalled/TabInstalledDetails.cs @@ -4,6 +4,7 @@ using System.IO; using System.Linq; using Dalamud.Interface; using ImGuiNET; +using Lumina.Excel.GeneratedSheets; using Penumbra.Game.Enums; using Penumbra.Meta; using Penumbra.Mod; @@ -37,8 +38,6 @@ namespace Penumbra.UI private const string LabelAboutTab = "About"; private const string LabelChangedItemsTab = "Changed Items"; private const string LabelChangedItemsHeader = "##changedItems"; - private const string LabelChangedItemIdx = "##citem_"; - private const string LabelChangedItemNew = "##citem_new"; private const string LabelConflictsTab = "Mod Conflicts"; private const string LabelConflictsHeader = "##conflicts"; private const string LabelFileSwapTab = "File Swaps"; @@ -59,13 +58,12 @@ namespace Penumbra.UI private const uint ColorYellow = 0xFF00C8C8; private const uint ColorRed = 0xFF0000C8; - private bool _editMode; - private int _selectedGroupIndex; - private OptionGroup? _selectedGroup; - private int _selectedOptionIndex; - private Option? _selectedOption; - private (string label, string name)[]? _changedItemsList; - private string _currentGamePaths = ""; + private bool _editMode; + private int _selectedGroupIndex; + private OptionGroup? _selectedGroup; + private int _selectedOptionIndex; + private Option? _selectedOption; + private string _currentGamePaths = ""; private (FileInfo name, bool selected, uint color, RelPath relName)[]? _fullFilenameList; @@ -119,7 +117,6 @@ namespace Penumbra.UI public void ResetState() { - _changedItemsList = null; _fullFilenameList = null; SelectGroup(); SelectOption(); @@ -186,62 +183,25 @@ namespace Penumbra.UI private void DrawChangedItemsTab() { - if( !_editMode && Meta.ChangedItems.Count == 0 ) + if( Mod.Data.ChangedItems.Count == 0 || !ImGui.BeginTabItem( LabelChangedItemsTab ) ) { return; } - var flags = _editMode - ? ImGuiInputTextFlags.EnterReturnsTrue - : ImGuiInputTextFlags.ReadOnly; - - if( ImGui.BeginTabItem( LabelChangedItemsTab ) ) + if( ImGui.BeginListBox( LabelChangedItemsHeader, AutoFillSize ) ) { - ImGui.SetNextItemWidth( -1 ); - var changedItems = false; - if( ImGui.BeginListBox( LabelChangedItemsHeader, AutoFillSize ) ) + foreach( var item in Mod.Data.ChangedItems ) { - _changedItemsList ??= Meta.ChangedItems - .Select( ( I, index ) => ( $"{LabelChangedItemIdx}{index}", I ) ).ToArray(); - - for( var i = 0; i < Meta.ChangedItems.Count; ++i ) + if( ImGui.Selectable( item.Key ) && item.Value is Item it ) { - ImGui.SetNextItemWidth( -1 ); - if( ImGui.InputText( _changedItemsList[ i ].label, ref _changedItemsList[ i ].name, 128, flags ) ) - { - Meta.ChangedItems.RemoveOrChange( _changedItemsList[ i ].name, i ); - changedItems = true; - _selector.SaveCurrentMod(); - } + ChatUtil.LinkItem( it ); } - - var newItem = ""; - if( _editMode ) - { - ImGui.SetNextItemWidth( -1 ); - if( ImGui.InputTextWithHint( LabelChangedItemNew, "Enter new changed item...", ref newItem, 128, flags ) - && newItem.Length > 0 ) - { - Meta.ChangedItems.Add( newItem ); - changedItems = true; - _selector.SaveCurrentMod(); - } - } - - if( changedItems ) - { - _changedItemsList = null; - } - - ImGui.EndListBox(); } - ImGui.EndTabItem(); - } - else - { - _changedItemsList = null; + ImGui.EndListBox(); } + + ImGui.EndTabItem(); } private void DrawConflictTab() diff --git a/Penumbra/UI/MenuTabs/TabInstalled/TabInstalledSelector.cs b/Penumbra/UI/MenuTabs/TabInstalled/TabInstalledSelector.cs index 926e41dc..1025addf 100644 --- a/Penumbra/UI/MenuTabs/TabInstalled/TabInstalledSelector.cs +++ b/Penumbra/UI/MenuTabs/TabInstalled/TabInstalledSelector.cs @@ -6,7 +6,6 @@ using System.Numerics; using Dalamud.Interface; using Dalamud.Plugin; using ImGuiNET; -using ImGuiScene; using Penumbra.Importer; using Penumbra.Mod; using Penumbra.Mods; @@ -58,12 +57,15 @@ namespace Penumbra.UI private const string LabelSelectorList = "##availableModList"; private const string LabelModFilter = "##ModFilter"; private const string LabelAddModPopup = "AddMod"; - private const string TooltipModFilter = "Filter mods for those containing the given substring."; - private const string TooltipDelete = "Delete the selected mod"; - private const string TooltipAdd = "Add an empty mod"; - private const string DialogDeleteMod = "PenumbraDeleteMod"; - private const string ButtonYesDelete = "Yes, delete it"; - private const string ButtonNoDelete = "No, keep it"; + + private const string TooltipModFilter = + "Filter mods for those containing the given substring.\nEnter c:[string] to filter for mods changing specific items.\n:Enter a:[string] to filter for mods by specific authors."; + + private const string TooltipDelete = "Delete the selected mod"; + private const string TooltipAdd = "Add an empty mod"; + private const string DialogDeleteMod = "PenumbraDeleteMod"; + private const string ButtonYesDelete = "Yes, delete it"; + private const string ButtonNoDelete = "No, keep it"; private const float SelectorPanelWidth = 240f; private const uint DisabledModColor = 0xFF666666; @@ -82,7 +84,10 @@ namespace Penumbra.UI public Mod.Mod? Mod { get; private set; } private int _index; private int? _deleteIndex; - private string _modFilter = ""; + private string _modFilterInput = ""; + private string _modFilter = ""; + private string _modFilterChanges = ""; + private string _modFilterAuthor = ""; private string[] _modNamesLower; private ModFilter _stateFilter = UnfilteredStateMods; @@ -191,10 +196,27 @@ namespace Penumbra.UI private void DrawModsSelectorFilter() { ImGui.SetNextItemWidth( SelectorButtonSizes.X * 2 - 22 ); - var tmp = _modFilter; - if( ImGui.InputTextWithHint( LabelModFilter, "Filter Mods...", ref tmp, 256 ) ) + if( ImGui.InputTextWithHint( LabelModFilter, "Filter Mods...", ref _modFilterInput, 256 ) ) { - _modFilter = tmp.ToLowerInvariant(); + var lower = _modFilterInput.ToLowerInvariant(); + if( lower.StartsWith( "c:" ) ) + { + _modFilterChanges = lower.Substring( 2 ); + _modFilter = string.Empty; + _modFilterAuthor = string.Empty; + } + else if( lower.StartsWith( "a:" ) ) + { + _modFilterAuthor = lower.Substring( 2 ); + _modFilter = string.Empty; + _modFilterChanges = string.Empty; + } + else + { + _modFilter = lower; + _modFilterAuthor = string.Empty; + _modFilterChanges = string.Empty; + } } if( ImGui.IsItemHovered() ) @@ -304,7 +326,10 @@ namespace Penumbra.UI } private bool CheckFilters( Mod.Mod mod, int modIndex ) - => ( _modFilter.Length <= 0 || _modNamesLower[ modIndex ].Contains( _modFilter ) ) + => ( _modFilter.Length == 0 || _modNamesLower[ modIndex ].Contains( _modFilter ) ) + && ( _modFilterAuthor.Length == 0 || mod.Data.Meta.Author.ToLowerInvariant().Contains( _modFilterAuthor ) ) + && ( _modFilterChanges.Length == 0 + || mod.Data.ChangedItems.Any( s => s.Key.ToLowerInvariant().Contains( _modFilterChanges ) ) ) && !CheckFlags( mod.Data.Resources.ModFiles.Count, ModFilter.HasNoFiles, ModFilter.HasFiles ) && !CheckFlags( mod.Data.Meta.FileSwaps.Count, ModFilter.HasNoFileSwaps, ModFilter.HasFileSwaps ) && !CheckFlags( mod.Data.Resources.MetaManipulations.Count, ModFilter.HasNoMetaManipulations, ModFilter.HasMetaManipulations ) diff --git a/Penumbra/Util/ChatUtil.cs b/Penumbra/Util/ChatUtil.cs new file mode 100644 index 00000000..a030d088 --- /dev/null +++ b/Penumbra/Util/ChatUtil.cs @@ -0,0 +1,41 @@ +using System.Collections.Generic; +using Dalamud.Game.Text; +using Dalamud.Game.Text.SeStringHandling; +using Dalamud.Game.Text.SeStringHandling.Payloads; +using Dalamud.Plugin; +using Lumina.Excel.GeneratedSheets; + +namespace Penumbra.Util +{ + public static class ChatUtil + { + private static DalamudPluginInterface? _pi; + + public static void LinkItem( Item item ) + { + _pi ??= Service< DalamudPluginInterface >.Get(); + + var payloadList = new List< Payload > + { + new UIForegroundPayload( _pi.Data, ( ushort )( 0x223 + item.Rarity * 2 ) ), + new UIGlowPayload( _pi.Data, ( ushort )( 0x224 + item.Rarity * 2 ) ), + new ItemPayload( _pi.Data, item.RowId, false ), + new UIForegroundPayload( _pi.Data, 500 ), + new UIGlowPayload( _pi.Data, 501 ), + new TextPayload( $"{( char )SeIconChar.LinkMarker}" ), + new UIForegroundPayload( _pi.Data, 0 ), + new UIGlowPayload( _pi.Data, 0 ), + new TextPayload( item.Name ), + new RawPayload( new byte[] { 0x02, 0x27, 0x07, 0xCF, 0x01, 0x01, 0x01, 0xFF, 0x01, 0x03 } ), + new RawPayload( new byte[] { 0x02, 0x13, 0x02, 0xEC, 0x03 } ), + }; + + var payload = new SeString( payloadList ); + + _pi.Framework.Gui.Chat.PrintChat( new XivChatEntry + { + MessageBytes = payload.Encode(), + } ); + } + } +} \ No newline at end of file