Move Object Identification and Path Parsing to GameData, create initializable static Identifier in GameData.

This commit is contained in:
Ottermandias 2021-07-25 02:41:36 +02:00
parent b93c5376de
commit 702f8e3967
13 changed files with 88 additions and 35 deletions

View file

@ -0,0 +1,330 @@
using System;
using System.Collections.Generic;
using System.Text;
using System.Text.RegularExpressions;
using Dalamud.Plugin;
using Penumbra.GameData.Enums;
using Penumbra.GameData.Structs;
using Penumbra.GameData.Util;
namespace Penumbra.GameData
{
public static class GamePathParser
{
private const string CharacterFolder = "chara";
private const string EquipmentFolder = "equipment";
private const string PlayerFolder = "human";
private const string WeaponFolder = "weapon";
private const string AccessoryFolder = "accessory";
private const string DemiHumanFolder = "demihuman";
private const string MonsterFolder = "monster";
private const string CommonFolder = "common";
private const string UiFolder = "ui";
private const string IconFolder = "icon";
private const string LoadingFolder = "loadingimage";
private const string MapFolder = "map";
private const string InterfaceFolder = "uld";
private const string FontFolder = "font";
private const string HousingFolder = "hou";
private const string VfxFolder = "vfx";
private const string WorldFolder1 = "bgcommon";
private const string WorldFolder2 = "bg";
// @formatter:off
private static readonly Dictionary<FileType, Dictionary<ObjectType, Regex[]>> Regexes = new()
{ { FileType.Font, new Dictionary< ObjectType, Regex[] >(){ { ObjectType.Font, new Regex[]{ new(@"common/font/(?'fontname'.*)_(?'id'\d\d)(_lobby)?\.fdt") } } } }
, { FileType.Texture, new Dictionary< ObjectType, Regex[] >()
{ { ObjectType.Icon, new Regex[]{ new(@"ui/icon/(?'group'\d*)(/(?'lang'[a-z]{2}))?(/(?'hq'hq))?/(?'id'\d*)\.tex") } }
, { ObjectType.Map, new Regex[]{ new(@"ui/map/(?'id'[a-z0-9]{4})/(?'variant'\d{2})/\k'id'\k'variant'(?'suffix'[a-z])?(_[a-z])?\.tex") } }
, { ObjectType.Weapon, new Regex[]{ new(@"chara/weapon/w(?'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'(--)?)(v(?'variant'\d{2})_)?c\k'race'\k'typeabr'\k'id'(_(?'slot'[a-z]{3}))?(_[a-z])?_[a-z]\.tex")
, new(@"chara/human/c(?'race'\d{4})/obj/(?'type'[a-z]+)/(?'typeabr'[a-z])(?'id'\d{4})/texture")
, 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(?'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(?'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(?'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") } }
, { ObjectType.Accessory, new Regex[]{ new(@"chara/accessory/a(?'id'\d{4})/a\k'id'\.imc") } } } },
};
// @formatter:on
public static ObjectType PathToObjectType( GamePath path )
{
if( path.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,
};
}
private static (FileType, ObjectType, Match?) ParseGamePath( GamePath path )
{
if( !GameData.Enums.GameData.ExtensionToFileType.TryGetValue( Extension( path ), out var fileType ) )
{
fileType = FileType.Unknown;
}
var objectType = PathToObjectType( path );
if( !Regexes.TryGetValue( fileType, out var objectDict ) )
{
return ( fileType, objectType, null );
}
if( !objectDict.TryGetValue( objectType, out var regexes ) )
{
return ( fileType, objectType, null );
}
foreach( var regex in regexes )
{
var match = regex.Match( path );
if( match.Success )
{
return ( fileType, objectType, match );
}
}
return ( fileType, objectType, null );
}
private static string Extension( string filename )
{
var extIdx = filename.LastIndexOf( '.' );
return extIdx < 0 ? "" : filename.Substring( extIdx );
}
private static GameObjectInfo HandleEquipment( FileType fileType, ObjectType objectType, GroupCollection groups )
{
var setId = ushort.Parse( groups[ "id" ].Value );
if( fileType == FileType.Imc )
{
return GameObjectInfo.Equipment( fileType, setId );
}
var gr = GameData.Enums.GameData.GenderRaceFromCode( groups[ "race" ].Value );
var slot = GameData.Enums.GameData.SuffixToEquipSlot[ groups[ "slot" ].Value ];
if( fileType == FileType.Model )
{
return GameObjectInfo.Equipment( fileType, setId, gr, slot );
}
var variant = byte.Parse( groups[ "variant" ].Value );
return GameObjectInfo.Equipment( fileType, setId, gr, slot, variant );
}
private static GameObjectInfo HandleWeapon( FileType fileType, ObjectType objectType, 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 );
}
var variant = byte.Parse( groups[ "variant" ].Value );
return GameObjectInfo.Weapon( fileType, setId, weaponId, variant );
}
private static GameObjectInfo HandleMonster( FileType fileType, ObjectType objectType, GroupCollection groups )
{
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 );
}
private static GameObjectInfo HandleDemiHuman( FileType fileType, ObjectType objectType, 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 );
}
var slot = GameData.Enums.GameData.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 );
}
private static GameObjectInfo HandleCustomization( FileType fileType, ObjectType objectType, GroupCollection groups )
{
if( groups[ "skin" ].Success )
{
return GameObjectInfo.Customization( fileType, CustomizationType.Skin );
}
var id = ushort.Parse( groups[ "id" ].Value );
if( groups[ "location" ].Success )
{
var tmpType = groups[ "location" ].Value == "face" ? CustomizationType.DecalFace
: groups[ "location" ].Value == "equip" ? CustomizationType.DecalEquip : CustomizationType.Unknown;
return GameObjectInfo.Customization( fileType, tmpType, id );
}
var gr = GameData.Enums.GameData.GenderRaceFromCode( groups[ "race" ].Value );
var bodySlot = GameData.Enums.GameData.StringToBodySlot[ groups[ "type" ].Value ];
var type = groups[ "slot" ].Success ? GameData.Enums.GameData.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 );
}
private static GameObjectInfo HandleIcon( FileType fileType, ObjectType objectType, 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 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 HandleMap( FileType fileType, ObjectType objectType, 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 static GameObjectInfo GetFileInfo( GamePath path )
{
var (fileType, objectType, match) = ParseGamePath( path );
if( match == null || !match.Success )
{
return new GameObjectInfo { FileType = fileType, ObjectType = objectType };
}
try
{
var groups = match.Groups;
switch( objectType )
{
case ObjectType.Accessory: return HandleEquipment( fileType, objectType, groups );
case ObjectType.Equipment: return HandleEquipment( fileType, objectType, groups );
case ObjectType.Weapon: return HandleWeapon( fileType, objectType, groups );
case ObjectType.Map: return HandleMap( fileType, objectType, groups );
case ObjectType.Monster: return HandleMonster( fileType, objectType, groups );
case ObjectType.DemiHuman: return HandleDemiHuman( fileType, objectType, groups );
case ObjectType.Character: return HandleCustomization( fileType, objectType, groups );
case ObjectType.Icon: return HandleIcon( fileType, objectType, groups );
}
}
catch( Exception e )
{
PluginLog.Error( $"Could not parse {path}:\n{e}" );
}
return new GameObjectInfo { FileType = fileType, ObjectType = objectType };
}
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 )
{
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;
}
}
}

View file

@ -0,0 +1,316 @@
using System.Collections.Generic;
using System.ComponentModel;
using System.Linq;
using Dalamud.Plugin;
using Lumina.Excel.GeneratedSheets;
using Penumbra.GameData.Enums;
using Penumbra.GameData.Structs;
using Penumbra.GameData.Util;
using Action = Lumina.Excel.GeneratedSheets.Action;
using Race = Penumbra.GameData.Enums.Race;
namespace Penumbra.GameData
{
internal 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 )
.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 != Race.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 = 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 );
}
}
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 ) | ( ulong )weaponType,
0xFFFFFFFFFFFF );
return begin >= 0 ? _equipment[ begin ].Item2.FirstOrDefault() : null;
}
}
}
}
}

View file

@ -0,0 +1,62 @@
using System;
using System.Collections.Generic;
using Dalamud.Plugin;
using Lumina.Excel.GeneratedSheets;
using Penumbra.GameData.Enums;
using Penumbra.GameData.Structs;
using Penumbra.GameData.Util;
namespace Penumbra.GameData
{
public static class ObjectIdentifier
{
private static ObjectIdentification? _identification = null;
public static bool Initialize( DalamudPluginInterface pi )
{
if( _identification != null )
{
return true;
}
try
{
_identification = new ObjectIdentification( pi );
return true;
}
catch( Exception e )
{
_identification = null;
PluginLog.Error( $"Failure while initializing Object Identifier:\n{e}" );
return false;
}
}
private static void Verify()
{
if( _identification == null )
{
throw new Exception( "Object Identifier not initialized." );
}
}
public static void Identify( IDictionary< string, object? > set, GamePath path )
{
Verify();
_identification!.Identify( set, path );
}
public static Dictionary< string, object? > Identify( GamePath path )
{
Dictionary< string, object? > ret = new();
Identify( ret, path );
return ret;
}
public static Item? Identify( SetId setId, WeaponType weaponType, ushort variant, EquipSlot slot )
{
Verify();
return _identification!.Identify( setId, weaponType, variant, slot );
}
}
}

View file

@ -0,0 +1,160 @@
using System;
using System.Runtime.InteropServices;
using Dalamud;
using Penumbra.GameData.Enums;
namespace Penumbra.GameData.Structs
{
[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
{
get => GameData.Enums.GameData.GenderRaceFromByte( _genderRaceByte );
set => _genderRaceByte = value.ToByte();
}
[FieldOffset( 5 )]
public BodySlot BodySlot; // Customization
[FieldOffset( 5 )]
public byte MapC4; // Map
[FieldOffset( 6 )]
public byte Variant; // Equipment, Weapon, Customization, Map, Monster, Demihuman
[FieldOffset( 6 )]
public bool IconHq; // Icon
[FieldOffset( 7 )]
public EquipSlot EquipSlot; // Equipment, Demihuman
[FieldOffset( 7 )]
public CustomizationType CustomizationType; // Customization
[FieldOffset( 7 )]
public ClientLanguage Language; // Icon
[FieldOffset( 7 )]
public byte MapSuffix;
public override int GetHashCode()
=> Identifier.GetHashCode();
public int CompareTo( object? r )
=> Identifier.CompareTo( r );
}
}