mirror of
https://github.com/xivdev/Penumbra.git
synced 2025-12-12 18:27:24 +01:00
Add some refactoring of data, Stains and STM files.
This commit is contained in:
parent
7e167cf0cf
commit
8d11e1075d
11 changed files with 580 additions and 227 deletions
56
Penumbra.GameData/Data/DataSharer.cs
Normal file
56
Penumbra.GameData/Data/DataSharer.cs
Normal file
|
|
@ -0,0 +1,56 @@
|
|||
using System;
|
||||
using Dalamud;
|
||||
using Dalamud.Logging;
|
||||
using Dalamud.Plugin;
|
||||
|
||||
namespace Penumbra.GameData.Data;
|
||||
|
||||
public abstract class DataSharer : IDisposable
|
||||
{
|
||||
private readonly DalamudPluginInterface _pluginInterface;
|
||||
private readonly int _version;
|
||||
protected readonly ClientLanguage Language;
|
||||
private bool _disposed;
|
||||
|
||||
protected DataSharer(DalamudPluginInterface pluginInterface, ClientLanguage language, int version)
|
||||
{
|
||||
_pluginInterface = pluginInterface;
|
||||
Language = language;
|
||||
_version = version;
|
||||
}
|
||||
|
||||
protected virtual void DisposeInternal()
|
||||
{ }
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
if (_disposed)
|
||||
return;
|
||||
|
||||
DisposeInternal();
|
||||
GC.SuppressFinalize(this);
|
||||
_disposed = true;
|
||||
}
|
||||
|
||||
~DataSharer()
|
||||
=> Dispose();
|
||||
|
||||
protected void DisposeTag(string tag)
|
||||
=> _pluginInterface.RelinquishData(GetVersionedTag(tag));
|
||||
|
||||
private string GetVersionedTag(string tag)
|
||||
=> $"Penumbra.GameData.{tag}.{Language}.V{_version}";
|
||||
|
||||
protected T TryCatchData<T>(string tag, Func<T> func) where T : class
|
||||
{
|
||||
try
|
||||
{
|
||||
return _pluginInterface.GetOrCreateData(GetVersionedTag(tag), func);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
PluginLog.Error($"Error creating shared actor data for {tag}:\n{ex}");
|
||||
return func();
|
||||
}
|
||||
}
|
||||
}
|
||||
328
Penumbra.GameData/Data/GamePathParser.cs
Normal file
328
Penumbra.GameData/Data/GamePathParser.cs
Normal file
|
|
@ -0,0 +1,328 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Text.RegularExpressions;
|
||||
using Dalamud.Logging;
|
||||
using Penumbra.GameData.Enums;
|
||||
using Penumbra.GameData.Structs;
|
||||
|
||||
namespace Penumbra.GameData.Data;
|
||||
|
||||
internal class GamePathParser : IGamePathParser
|
||||
{
|
||||
public GameObjectInfo GetFileInfo(string path)
|
||||
{
|
||||
path = path.ToLowerInvariant().Replace('\\', '/');
|
||||
|
||||
var (fileType, objectType, match) = ParseGamePath(path);
|
||||
if (match is not { Success: true })
|
||||
return new GameObjectInfo
|
||||
{
|
||||
FileType = fileType,
|
||||
ObjectType = objectType,
|
||||
};
|
||||
|
||||
try
|
||||
{
|
||||
var groups = match.Groups;
|
||||
switch (objectType)
|
||||
{
|
||||
case ObjectType.Accessory: return HandleEquipment(fileType, groups);
|
||||
case ObjectType.Equipment: return HandleEquipment(fileType, groups);
|
||||
case ObjectType.Weapon: return HandleWeapon(fileType, groups);
|
||||
case ObjectType.Map: return HandleMap(fileType, groups);
|
||||
case ObjectType.Monster: return HandleMonster(fileType, groups);
|
||||
case ObjectType.DemiHuman: return HandleDemiHuman(fileType, groups);
|
||||
case ObjectType.Character: return HandleCustomization(fileType, groups);
|
||||
case ObjectType.Icon: return HandleIcon(fileType, groups);
|
||||
}
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
PluginLog.Error($"Could not parse {path}:\n{e}");
|
||||
}
|
||||
|
||||
return new GameObjectInfo
|
||||
{
|
||||
FileType = fileType,
|
||||
ObjectType = objectType,
|
||||
};
|
||||
}
|
||||
|
||||
public string VfxToKey(string 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;
|
||||
}
|
||||
|
||||
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
|
||||
// language=regex
|
||||
private readonly IReadOnlyDictionary<FileType, IReadOnlyDictionary<ObjectType, IReadOnlyList<Regex>>> _regexes = new Dictionary<FileType, IReadOnlyDictionary<ObjectType, IReadOnlyList<Regex>>>()
|
||||
{
|
||||
[FileType.Font] = new Dictionary<ObjectType, IReadOnlyList<Regex>>
|
||||
{
|
||||
[ObjectType.Font] = CreateRegexes(@"common/font/(?'fontname'.*)_(?'id'\d\d)(_lobby)?\.fdt"),
|
||||
},
|
||||
[FileType.Texture] = new Dictionary<ObjectType, IReadOnlyList<Regex>>
|
||||
{
|
||||
[ObjectType.Icon] = CreateRegexes(@"ui/icon/(?'group'\d*)(/(?'lang'[a-z]{2}))?(/(?'hq'hq))?/(?'id'\d*)(?'hr'_hr1)?\.tex"),
|
||||
[ObjectType.Map] = CreateRegexes(@"ui/map/(?'id'[a-z0-9]{4})/(?'variant'\d{2})/\k'id'\k'variant'(?'suffix'[a-z])?(_[a-z])?\.tex"),
|
||||
[ObjectType.Weapon] = CreateRegexes(@"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] = CreateRegexes(@"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] = CreateRegexes(@"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] = CreateRegexes(@"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] = CreateRegexes(@"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] = CreateRegexes(@"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"
|
||||
, @"chara/human/c(?'race'\d{4})/obj/(?'type'[a-z]+)/(?'typeabr'[a-z])(?'id'\d{4})/texture"
|
||||
, @"chara/common/texture/skin(?'skin'.*)\.tex"
|
||||
, @"chara/common/texture/(?'catchlight'catchlight)(.*)\.tex"
|
||||
, @"chara/common/texture/decal_(?'location'[a-z]+)/[-_]?decal_(?'id'\d+).tex"),
|
||||
},
|
||||
[FileType.Model] = new Dictionary<ObjectType, IReadOnlyList<Regex>>
|
||||
{
|
||||
[ObjectType.Weapon] = CreateRegexes(@"chara/weapon/w(?'id'\d{4})/obj/body/b(?'weapon'\d{4})/model/w\k'id'b\k'weapon'\.mdl"),
|
||||
[ObjectType.Monster] = CreateRegexes(@"chara/monster/m(?'monster'\d{4})/obj/body/b(?'id'\d{4})/model/m\k'monster'b\k'id'\.mdl"),
|
||||
[ObjectType.Equipment] = CreateRegexes(@"chara/equipment/e(?'id'\d{4})/model/c(?'race'\d{4})e\k'id'_(?'slot'[a-z]{3})\.mdl"),
|
||||
[ObjectType.DemiHuman] = CreateRegexes(@"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] = CreateRegexes(@"chara/accessory/a(?'id'\d{4})/model/c(?'race'\d{4})a\k'id'_(?'slot'[a-z]{3})\.mdl"),
|
||||
[ObjectType.Character] = CreateRegexes(@"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, IReadOnlyList<Regex>>
|
||||
{
|
||||
[ObjectType.Weapon] = CreateRegexes(@"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] = CreateRegexes(@"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] = CreateRegexes(@"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] = CreateRegexes(@"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] = CreateRegexes(@"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] = CreateRegexes(@"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, IReadOnlyList<Regex>>
|
||||
{
|
||||
[ObjectType.Weapon] = CreateRegexes(@"chara/weapon/w(?'id'\d{4})/obj/body/b(?'weapon'\d{4})/b\k'weapon'\.imc"),
|
||||
[ObjectType.Monster] = CreateRegexes(@"chara/monster/m(?'monster'\d{4})/obj/body/b(?'id'\d{4})/b\k'id'\.imc"),
|
||||
[ObjectType.Equipment] = CreateRegexes(@"chara/equipment/e(?'id'\d{4})/e\k'id'\.imc"),
|
||||
[ObjectType.DemiHuman] = CreateRegexes(@"chara/demihuman/d(?'id'\d{4})/obj/equipment/e(?'equip'\d{4})/e\k'equip'\.imc"),
|
||||
[ObjectType.Accessory] = CreateRegexes(@"chara/accessory/a(?'id'\d{4})/a\k'id'\.imc"),
|
||||
},
|
||||
};
|
||||
// @formatter:on
|
||||
|
||||
|
||||
private static IReadOnlyList<Regex> CreateRegexes(params string[] regexes)
|
||||
=> regexes.Select(s => new Regex(s, RegexOptions.Compiled)).ToArray();
|
||||
|
||||
public ObjectType PathToObjectType(string path)
|
||||
{
|
||||
if (path.Length == 0)
|
||||
return ObjectType.Unknown;
|
||||
|
||||
var folders = path.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 (FileType, ObjectType, Match?) ParseGamePath(string path)
|
||||
{
|
||||
if (!Names.ExtensionToFileType.TryGetValue(Path.GetExtension(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 GameObjectInfo HandleEquipment(FileType fileType, GroupCollection groups)
|
||||
{
|
||||
var setId = ushort.Parse(groups["id"].Value);
|
||||
if (fileType == FileType.Imc)
|
||||
return GameObjectInfo.Equipment(fileType, setId);
|
||||
|
||||
var gr = Names.GenderRaceFromCode(groups["race"].Value);
|
||||
var slot = Names.SuffixToEquipSlot[groups["slot"].Value];
|
||||
if (fileType == FileType.Model)
|
||||
return GameObjectInfo.Equipment(fileType, setId, gr, slot);
|
||||
|
||||
var variant = byte.Parse(groups["variant"].Value);
|
||||
return GameObjectInfo.Equipment(fileType, setId, gr, slot, variant);
|
||||
}
|
||||
|
||||
private static GameObjectInfo HandleWeapon(FileType fileType, GroupCollection groups)
|
||||
{
|
||||
var weaponId = ushort.Parse(groups["weapon"].Value);
|
||||
var setId = ushort.Parse(groups["id"].Value);
|
||||
if (fileType is FileType.Imc or 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, GroupCollection groups)
|
||||
{
|
||||
var monsterId = ushort.Parse(groups["monster"].Value);
|
||||
var bodyId = ushort.Parse(groups["id"].Value);
|
||||
if (fileType is FileType.Imc or 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, 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 = Names.SuffixToEquipSlot[groups["slot"].Value];
|
||||
if (fileType == FileType.Model)
|
||||
return GameObjectInfo.DemiHuman(fileType, demiHumanId, equipId, slot);
|
||||
|
||||
var variant = byte.Parse(groups["variant"].Value);
|
||||
return GameObjectInfo.DemiHuman(fileType, demiHumanId, equipId, slot, variant);
|
||||
}
|
||||
|
||||
private static GameObjectInfo HandleCustomization(FileType fileType, GroupCollection groups)
|
||||
{
|
||||
if (groups["catchlight"].Success)
|
||||
return GameObjectInfo.Customization(fileType, CustomizationType.Iris);
|
||||
|
||||
if (groups["skin"].Success)
|
||||
return GameObjectInfo.Customization(fileType, CustomizationType.Skin);
|
||||
|
||||
var id = ushort.Parse(groups["id"].Value);
|
||||
if (groups["location"].Success)
|
||||
{
|
||||
var tmpType = groups["location"].Value == "face" ? CustomizationType.DecalFace
|
||||
: groups["location"].Value == "equip" ? CustomizationType.DecalEquip : CustomizationType.Unknown;
|
||||
return GameObjectInfo.Customization(fileType, tmpType, id);
|
||||
}
|
||||
|
||||
var gr = Names.GenderRaceFromCode(groups["race"].Value);
|
||||
var bodySlot = Names.StringToBodySlot[groups["type"].Value];
|
||||
var type = groups["slot"].Success
|
||||
? Names.SuffixToCustomizationType[groups["slot"].Value]
|
||||
: CustomizationType.Skin;
|
||||
if (fileType == FileType.Material)
|
||||
{
|
||||
var variant = groups["variant"].Success ? byte.Parse(groups["variant"].Value) : (byte)0;
|
||||
return GameObjectInfo.Customization(fileType, type, id, gr, bodySlot, variant);
|
||||
}
|
||||
|
||||
return GameObjectInfo.Customization(fileType, type, id, gr, bodySlot);
|
||||
}
|
||||
|
||||
private static GameObjectInfo HandleIcon(FileType fileType, GroupCollection groups)
|
||||
{
|
||||
var hq = groups["hq"].Success;
|
||||
var hr = groups["hr"].Success;
|
||||
var id = uint.Parse(groups["id"].Value);
|
||||
if (!groups["lang"].Success)
|
||||
return GameObjectInfo.Icon(fileType, id, hq, hr);
|
||||
|
||||
var language = groups["lang"].Value switch
|
||||
{
|
||||
"en" => Dalamud.ClientLanguage.English,
|
||||
"ja" => Dalamud.ClientLanguage.Japanese,
|
||||
"de" => Dalamud.ClientLanguage.German,
|
||||
"fr" => Dalamud.ClientLanguage.French,
|
||||
_ => Dalamud.ClientLanguage.English,
|
||||
};
|
||||
return GameObjectInfo.Icon(fileType, id, hq, hr, language);
|
||||
}
|
||||
|
||||
private static GameObjectInfo HandleMap(FileType fileType, GroupCollection groups)
|
||||
{
|
||||
var map = Encoding.ASCII.GetBytes(groups["id"].Value);
|
||||
var variant = byte.Parse(groups["variant"].Value);
|
||||
if (groups["suffix"].Success)
|
||||
{
|
||||
var suffix = Encoding.ASCII.GetBytes(groups["suffix"].Value)[0];
|
||||
return GameObjectInfo.Map(fileType, map[0], map[1], map[2], map[3], variant, suffix);
|
||||
}
|
||||
|
||||
return GameObjectInfo.Map(fileType, map[0], map[1], map[2], map[3], variant);
|
||||
}
|
||||
|
||||
|
||||
private readonly Regex _vfxRegexTmb = new(@"chara[\/]action[\/](?'key'[^\s]+?)\.tmb", RegexOptions.Compiled | RegexOptions.IgnoreCase);
|
||||
|
||||
private readonly Regex _vfxRegexPap = new(@"chara[\/]human[\/]c0101[\/]animation[\/]a0001[\/][^\s]+?[\/](?'key'[^\s]+?)\.pap",
|
||||
RegexOptions.Compiled | RegexOptions.IgnoreCase);
|
||||
}
|
||||
395
Penumbra.GameData/Data/ObjectIdentification.cs
Normal file
395
Penumbra.GameData/Data/ObjectIdentification.cs
Normal file
|
|
@ -0,0 +1,395 @@
|
|||
using System;
|
||||
using Dalamud;
|
||||
using Dalamud.Data;
|
||||
using Lumina.Excel.GeneratedSheets;
|
||||
using Penumbra.GameData.Enums;
|
||||
using Penumbra.GameData.Structs;
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel;
|
||||
using System.Linq;
|
||||
using Dalamud.Game.ClientState.Objects.Enums;
|
||||
using Dalamud.Plugin;
|
||||
using Dalamud.Utility;
|
||||
using Action = Lumina.Excel.GeneratedSheets.Action;
|
||||
|
||||
namespace Penumbra.GameData.Data;
|
||||
|
||||
internal sealed class ObjectIdentification : DataSharer, IObjectIdentifier
|
||||
{
|
||||
public IGamePathParser GamePathParser { get; } = new GamePathParser();
|
||||
|
||||
public void Identify(IDictionary<string, object?> set, string path)
|
||||
{
|
||||
if (path.EndsWith(".pap", StringComparison.OrdinalIgnoreCase) || path.EndsWith(".tmb", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
IdentifyVfx(set, path);
|
||||
}
|
||||
else
|
||||
{
|
||||
var info = GamePathParser.GetFileInfo(path);
|
||||
IdentifyParsed(set, info);
|
||||
}
|
||||
}
|
||||
|
||||
public Dictionary<string, object?> Identify(string path)
|
||||
{
|
||||
Dictionary<string, object?> ret = new();
|
||||
Identify(ret, path);
|
||||
return ret;
|
||||
}
|
||||
|
||||
public IReadOnlyList<Item> Identify(SetId setId, WeaponType weaponType, ushort variant, EquipSlot slot)
|
||||
{
|
||||
switch (slot)
|
||||
{
|
||||
case EquipSlot.MainHand:
|
||||
case EquipSlot.OffHand:
|
||||
{
|
||||
var (begin, _) = FindIndexRange((List<(ulong, IReadOnlyList<Item>)>)_weapons,
|
||||
(ulong)setId << 32 | (ulong)weaponType << 16 | variant,
|
||||
0xFFFFFFFFFFFF);
|
||||
return begin >= 0 ? _weapons[begin].Item2 : Array.Empty<Item>();
|
||||
}
|
||||
default:
|
||||
{
|
||||
var (begin, _) = FindIndexRange((List<(ulong, IReadOnlyList<Item>)>)_equipment,
|
||||
(ulong)setId << 32 | (ulong)slot.ToSlot() << 16 | variant,
|
||||
0xFFFFFFFFFFFF);
|
||||
return begin >= 0 ? _equipment[begin].Item2 : Array.Empty<Item>();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private readonly IReadOnlyList<(ulong Key, IReadOnlyList<Item> Values)> _weapons;
|
||||
private readonly IReadOnlyList<(ulong Key, IReadOnlyList<Item> Values)> _equipment;
|
||||
private readonly IReadOnlyList<(ulong Key, IReadOnlyList<(ObjectKind Kind, uint Id)>)> _models;
|
||||
private readonly IReadOnlyDictionary<string, IReadOnlyList<Action>> _actions;
|
||||
|
||||
public ObjectIdentification(DalamudPluginInterface pluginInterface, DataManager dataManager, ClientLanguage language)
|
||||
: base(pluginInterface, language, 1)
|
||||
{
|
||||
_weapons = TryCatchData("Weapons", () => CreateWeaponList(dataManager));
|
||||
_equipment = TryCatchData("Equipment", () => CreateEquipmentList(dataManager));
|
||||
_actions = TryCatchData("Actions", () => CreateActionList(dataManager));
|
||||
_models = TryCatchData("Models", () => CreateModelList(dataManager));
|
||||
}
|
||||
|
||||
protected override void DisposeInternal()
|
||||
{
|
||||
DisposeTag("Weapons");
|
||||
DisposeTag("Equipment");
|
||||
DisposeTag("Actions");
|
||||
DisposeTag("Models");
|
||||
}
|
||||
|
||||
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 IReadOnlyList<(ulong Key, IReadOnlyList<Item> Values)> CreateWeaponList(DataManager gameData)
|
||||
{
|
||||
var items = gameData.GetExcelSheet<Item>(Language)!;
|
||||
var storage = new SortedList<ulong, HashSet<Item>>();
|
||||
foreach (var item in items.Where(i
|
||||
=> (EquipSlot)i.EquipSlotCategory.Row is EquipSlot.MainHand or EquipSlot.OffHand or EquipSlot.BothHand))
|
||||
{
|
||||
if (item.ModelMain != 0)
|
||||
Add(storage, WeaponKey(item, false), item);
|
||||
|
||||
if (item.ModelSub != 0)
|
||||
Add(storage, WeaponKey(item, true), item);
|
||||
}
|
||||
|
||||
return storage.Select(kvp => (kvp.Key, (IReadOnlyList<Item>)kvp.Value.ToArray())).ToList();
|
||||
}
|
||||
|
||||
private IReadOnlyList<(ulong Key, IReadOnlyList<Item> Values)> CreateEquipmentList(DataManager gameData)
|
||||
{
|
||||
var items = gameData.GetExcelSheet<Item>(Language)!;
|
||||
var storage = new SortedList<ulong, HashSet<Item>>();
|
||||
foreach (var item in items)
|
||||
{
|
||||
switch ((EquipSlot)item.EquipSlotCategory.Row)
|
||||
{
|
||||
// Accessories
|
||||
case EquipSlot.RFinger:
|
||||
case EquipSlot.Wrists:
|
||||
case EquipSlot.Ears:
|
||||
case EquipSlot.Neck:
|
||||
// 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:
|
||||
case EquipSlot.ChestHands:
|
||||
Add(storage, EquipmentKey(item), item);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return storage.Select(kvp => (kvp.Key, (IReadOnlyList<Item>)kvp.Value.ToArray())).ToList();
|
||||
}
|
||||
|
||||
private IReadOnlyDictionary<string, IReadOnlyList<Action>> CreateActionList(DataManager gameData)
|
||||
{
|
||||
var sheet = gameData.GetExcelSheet<Action>(Language)!;
|
||||
var storage = new Dictionary<string, HashSet<Action>>((int)sheet.RowCount);
|
||||
|
||||
void AddAction(string? key, Action action)
|
||||
{
|
||||
if (key.IsNullOrEmpty())
|
||||
return;
|
||||
|
||||
key = key.ToLowerInvariant();
|
||||
if (storage.TryGetValue(key, out var actions))
|
||||
actions.Add(action);
|
||||
else
|
||||
storage[key] = new HashSet<Action> { action };
|
||||
}
|
||||
|
||||
foreach (var action in sheet.Where(a => !a.Name.RawData.IsEmpty))
|
||||
{
|
||||
var startKey = action.AnimationStart?.Value?.Name?.Value?.Key.ToDalamudString().ToString();
|
||||
var endKey = action.AnimationEnd?.Value?.Key.ToDalamudString().ToString();
|
||||
var hitKey = action.ActionTimelineHit?.Value?.Key.ToDalamudString().ToString();
|
||||
AddAction(startKey, action);
|
||||
AddAction(endKey, action);
|
||||
AddAction(hitKey, action);
|
||||
}
|
||||
|
||||
return storage.ToDictionary(kvp => kvp.Key, kvp => (IReadOnlyList<Action>)kvp.Value.ToArray());
|
||||
}
|
||||
|
||||
private static ulong ModelValue(ModelChara row)
|
||||
=> row.Type | (ulong)row.Model << 8 | (ulong)row.Base << 24 | (ulong)row.Variant << 32;
|
||||
|
||||
private static IEnumerable<(ulong, ObjectKind, uint)> BattleNpcToName(ulong model, uint bNpc)
|
||||
=> Enumerable.Repeat((model, ObjectKind.BattleNpc, bNpc), 1);
|
||||
|
||||
private IReadOnlyList<(ulong Key, IReadOnlyList<(ObjectKind Kind, uint Id)>)> CreateModelList(DataManager gameData)
|
||||
{
|
||||
var sheetBNpc = gameData.GetExcelSheet<BNpcBase>(Language)!;
|
||||
var sheetENpc = gameData.GetExcelSheet<ENpcBase>(Language)!;
|
||||
var sheetCompanion = gameData.GetExcelSheet<Companion>(Language)!;
|
||||
var sheetMount = gameData.GetExcelSheet<Mount>(Language)!;
|
||||
var sheetModel = gameData.GetExcelSheet<ModelChara>(Language)!;
|
||||
|
||||
var modelCharaToModel = sheetModel.ToDictionary(m => m.RowId, ModelValue);
|
||||
|
||||
return sheetENpc.Select(e => (modelCharaToModel[e.ModelChara.Row], ObjectKind.EventNpc, e.RowId))
|
||||
.Concat(sheetCompanion.Select(c => (modelCharaToModel[c.Model.Row], ObjectKind.Companion, c.RowId)))
|
||||
.Concat(sheetMount.Select(c => (modelCharaToModel[c.ModelChara.Row], ObjectKind.MountType, c.RowId)))
|
||||
.Concat(sheetBNpc.SelectMany(c => BattleNpcToName(modelCharaToModel[c.ModelChara.Row], c.RowId)))
|
||||
.GroupBy(t => t.Item1)
|
||||
.Select(g => (g.Key, (IReadOnlyList<(ObjectKind, uint)>)g.Select(p => (p.Item2, p.Item3)).ToArray()))
|
||||
.ToArray();
|
||||
}
|
||||
|
||||
private class Comparer : IComparer<(ulong, IReadOnlyList<Item>)>
|
||||
{
|
||||
public int Compare((ulong, IReadOnlyList<Item>) x, (ulong, IReadOnlyList<Item>) y)
|
||||
=> x.Item1.CompareTo(y.Item1);
|
||||
}
|
||||
|
||||
private static (int, int) FindIndexRange(List<(ulong, IReadOnlyList<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((List<(ulong, IReadOnlyList<Item>)>)_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((List<(ulong, IReadOnlyList<Item>)>)_weapons, key, mask);
|
||||
if (start == -1)
|
||||
return;
|
||||
|
||||
for (; start < end; ++start)
|
||||
{
|
||||
foreach (var item in _weapons[start].Item2)
|
||||
set[item.Name.ToString()] = item;
|
||||
}
|
||||
}
|
||||
|
||||
private static void AddCounterString(IDictionary<string, object?> set, string data)
|
||||
{
|
||||
if (set.TryGetValue(data, out var obj) && obj is int counter)
|
||||
set[data] = counter + 1;
|
||||
else
|
||||
set[data] = 1;
|
||||
}
|
||||
|
||||
private void IdentifyParsed(IDictionary<string, object?> set, GameObjectInfo info)
|
||||
{
|
||||
switch (info.ObjectType)
|
||||
{
|
||||
case ObjectType.Unknown:
|
||||
switch (info.FileType)
|
||||
{
|
||||
case FileType.Sound:
|
||||
AddCounterString(set, FileType.Sound.ToString());
|
||||
break;
|
||||
case FileType.Animation:
|
||||
case FileType.Pap:
|
||||
AddCounterString(set, FileType.Animation.ToString());
|
||||
break;
|
||||
case FileType.Shader:
|
||||
AddCounterString(set, FileType.Shader.ToString());
|
||||
break;
|
||||
}
|
||||
|
||||
break;
|
||||
case ObjectType.LoadingScreen:
|
||||
case ObjectType.Map:
|
||||
case ObjectType.Interface:
|
||||
case ObjectType.Vfx:
|
||||
case ObjectType.World:
|
||||
case ObjectType.Housing:
|
||||
case ObjectType.Font:
|
||||
AddCounterString(set, info.ObjectType.ToString());
|
||||
break;
|
||||
case ObjectType.DemiHuman:
|
||||
set[$"Demi Human: {info.PrimaryId}"] = null;
|
||||
break;
|
||||
case ObjectType.Monster:
|
||||
set[$"Monster: {info.PrimaryId}"] = null;
|
||||
break;
|
||||
case ObjectType.Icon:
|
||||
set[$"Icon: {info.IconId}"] = null;
|
||||
break;
|
||||
case ObjectType.Accessory:
|
||||
case ObjectType.Equipment:
|
||||
FindEquipment(set, info);
|
||||
break;
|
||||
case ObjectType.Weapon:
|
||||
FindWeapon(set, info);
|
||||
break;
|
||||
case ObjectType.Character:
|
||||
var (gender, race) = info.GenderRace.Split();
|
||||
var raceString = race != ModelRace.Unknown ? race.ToName() + " " : "";
|
||||
var genderString = gender != Gender.Unknown ? gender.ToName() + " " : "Player ";
|
||||
switch (info.CustomizationType)
|
||||
{
|
||||
case CustomizationType.Skin:
|
||||
set[$"Customization: {raceString}{genderString}Skin Textures"] = null;
|
||||
break;
|
||||
case CustomizationType.DecalFace:
|
||||
set[$"Customization: Face Decal {info.PrimaryId}"] = null;
|
||||
break;
|
||||
case CustomizationType.Iris when race == ModelRace.Unknown:
|
||||
set[$"Customization: All Eyes (Catchlight)"] = null;
|
||||
break;
|
||||
case CustomizationType.DecalEquip:
|
||||
set[$"Equipment Decal {info.PrimaryId}"] = null;
|
||||
break;
|
||||
default:
|
||||
{
|
||||
var customizationString = race == ModelRace.Unknown
|
||||
|| info.BodySlot == BodySlot.Unknown
|
||||
|| info.CustomizationType == CustomizationType.Unknown
|
||||
? "Customization: Unknown"
|
||||
: $"Customization: {race} {gender} {info.BodySlot} ({info.CustomizationType}) {info.PrimaryId}";
|
||||
set[customizationString] = null;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
break;
|
||||
|
||||
default: throw new InvalidEnumArgumentException();
|
||||
}
|
||||
}
|
||||
|
||||
private void IdentifyVfx(IDictionary<string, object?> set, string 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;
|
||||
}
|
||||
}
|
||||
68
Penumbra.GameData/Data/StainData.cs
Normal file
68
Penumbra.GameData/Data/StainData.cs
Normal file
|
|
@ -0,0 +1,68 @@
|
|||
using System;
|
||||
using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using Dalamud;
|
||||
using Dalamud.Data;
|
||||
using Dalamud.Plugin;
|
||||
using Penumbra.GameData.Structs;
|
||||
|
||||
namespace Penumbra.GameData.Data;
|
||||
|
||||
public sealed class StainData : DataSharer, IReadOnlyDictionary<StainId, Stain>
|
||||
{
|
||||
public readonly IReadOnlyDictionary<byte, (string Name, uint Dye, bool Gloss)> Data;
|
||||
|
||||
public StainData(DalamudPluginInterface pluginInterface, DataManager dataManager, ClientLanguage language)
|
||||
: base(pluginInterface, language, 1)
|
||||
{
|
||||
Data = TryCatchData("Stains", () => CreateStainData(dataManager));
|
||||
}
|
||||
|
||||
protected override void DisposeInternal()
|
||||
=> DisposeTag("Stains");
|
||||
|
||||
private IReadOnlyDictionary<byte, (string Name, uint Dye, bool Gloss)> CreateStainData(DataManager dataManager)
|
||||
{
|
||||
var stainSheet = dataManager.GetExcelSheet<Lumina.Excel.GeneratedSheets.Stain>(Language)!;
|
||||
return stainSheet.Where(s => s.Color != 0 && s.Name.RawData.Length > 0)
|
||||
.ToDictionary(s => (byte)s.RowId, s =>
|
||||
{
|
||||
var stain = new Stain(s);
|
||||
return (stain.Name, stain.RgbaColor, stain.Gloss);
|
||||
});
|
||||
}
|
||||
|
||||
public IEnumerator<KeyValuePair<StainId, Stain>> GetEnumerator()
|
||||
=> Data.Select(kvp => new KeyValuePair<StainId, Stain>(new StainId(kvp.Key), new Stain())).GetEnumerator();
|
||||
|
||||
IEnumerator IEnumerable.GetEnumerator()
|
||||
=> GetEnumerator();
|
||||
|
||||
public int Count
|
||||
=> Data.Count;
|
||||
|
||||
public bool ContainsKey(StainId key)
|
||||
=> Data.ContainsKey(key.Value);
|
||||
|
||||
public bool TryGetValue(StainId key, out Stain value)
|
||||
{
|
||||
if (!Data.TryGetValue(key.Value, out var data))
|
||||
{
|
||||
value = default;
|
||||
return false;
|
||||
}
|
||||
|
||||
value = new Stain(data.Name, data.Dye, key.Value, data.Gloss);
|
||||
return true;
|
||||
}
|
||||
|
||||
public Stain this[StainId key]
|
||||
=> TryGetValue(key, out var data) ? data : throw new ArgumentOutOfRangeException(nameof(key));
|
||||
|
||||
public IEnumerable<StainId> Keys
|
||||
=> Data.Keys.Select(k => new StainId(k));
|
||||
|
||||
public IEnumerable<Stain> Values
|
||||
=> Data.Select(kvp => new Stain(kvp.Value.Name, kvp.Value.Dye, kvp.Key, kvp.Value.Gloss));
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue