mirror of
https://github.com/xivdev/Penumbra.git
synced 2025-12-12 10:17:22 +01:00
323 lines
No EOL
12 KiB
C#
323 lines
No EOL
12 KiB
C#
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 : IObjectIdentifier
|
|
{
|
|
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 = GameData.GamePathParser.VfxToKey( path );
|
|
if( key.Length == 0 || !_actions.TryGetValue( key, out var actions ) )
|
|
{
|
|
return;
|
|
}
|
|
|
|
foreach( var action in actions )
|
|
{
|
|
set[ $"Action: {action.Name}" ] = action;
|
|
}
|
|
}
|
|
|
|
public void Identify( IDictionary< string, object? > set, GamePath path )
|
|
{
|
|
if( ( ( string )path ).EndsWith( ".pap" ) || ( ( string )path ).EndsWith( ".tmb" ) )
|
|
{
|
|
IdentifyVfx( set, path );
|
|
}
|
|
else
|
|
{
|
|
var info = GameData.GamePathParser.GetFileInfo( path );
|
|
IdentifyParsed( set, info );
|
|
}
|
|
}
|
|
|
|
public Dictionary< string, object? > Identify( GamePath path )
|
|
{
|
|
Dictionary< string, object? > ret = new();
|
|
Identify( ret, path );
|
|
return ret;
|
|
}
|
|
|
|
public Item? Identify( SetId setId, WeaponType weaponType, ushort variant, EquipSlot slot )
|
|
{
|
|
switch( slot )
|
|
{
|
|
case EquipSlot.MainHand:
|
|
case EquipSlot.Offhand:
|
|
{
|
|
var (begin, _) = FindIndexRange( _weapons, ( ( ulong )setId << 32 ) | ( ( ulong )weaponType << 16 ) | variant,
|
|
0xFFFFFFFFFFFF );
|
|
return begin >= 0 ? _weapons[ begin ].Item2.FirstOrDefault() : null;
|
|
}
|
|
default:
|
|
{
|
|
var (begin, _) = FindIndexRange( _equipment,
|
|
( ( ulong )setId << 32 ) | ( ( ulong )slot.ToSlot() << 16 ) | ( ulong )weaponType,
|
|
0xFFFFFFFFFFFF );
|
|
return begin >= 0 ? _equipment[ begin ].Item2.FirstOrDefault() : null;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
} |