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