From 8d11e1075dbb48949a34c7902023522a13b9b8f6 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Wed, 9 Nov 2022 13:53:52 +0100 Subject: [PATCH] Add some refactoring of data, Stains and STM files. --- OtterGui | 2 +- Penumbra.GameData/Data/DataSharer.cs | 56 +++++ .../{ => Data}/GamePathParser.cs | 44 ++-- .../{ => Data}/ObjectIdentification.cs | 183 +++++++--------- Penumbra.GameData/Data/StainData.cs | 68 ++++++ Penumbra.GameData/Files/MdlFile.cs | 166 +++++++-------- Penumbra.GameData/Files/StmFile.cs | 197 ++++++++++++++++++ Penumbra.GameData/GameData.cs | 11 +- Penumbra.GameData/Structs/Stain.cs | 52 +++++ Penumbra/Penumbra.cs | 3 + Penumbra/Util/StainManager.cs | 25 +++ 11 files changed, 580 insertions(+), 227 deletions(-) create mode 100644 Penumbra.GameData/Data/DataSharer.cs rename Penumbra.GameData/{ => Data}/GamePathParser.cs (83%) rename Penumbra.GameData/{ => Data}/ObjectIdentification.cs (69%) create mode 100644 Penumbra.GameData/Data/StainData.cs create mode 100644 Penumbra.GameData/Files/StmFile.cs create mode 100644 Penumbra.GameData/Structs/Stain.cs create mode 100644 Penumbra/Util/StainManager.cs diff --git a/OtterGui b/OtterGui index 87debfd2..77ecf97a 160000 --- a/OtterGui +++ b/OtterGui @@ -1 +1 @@ -Subproject commit 87debfd2eceaee8a93fecc0565c454372bcef1f3 +Subproject commit 77ecf97a620e20a1bd65d2e76c784f6f569f4643 diff --git a/Penumbra.GameData/Data/DataSharer.cs b/Penumbra.GameData/Data/DataSharer.cs new file mode 100644 index 00000000..140006c7 --- /dev/null +++ b/Penumbra.GameData/Data/DataSharer.cs @@ -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(string tag, Func 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(); + } + } +} diff --git a/Penumbra.GameData/GamePathParser.cs b/Penumbra.GameData/Data/GamePathParser.cs similarity index 83% rename from Penumbra.GameData/GamePathParser.cs rename to Penumbra.GameData/Data/GamePathParser.cs index d1475a10..affe8704 100644 --- a/Penumbra.GameData/GamePathParser.cs +++ b/Penumbra.GameData/Data/GamePathParser.cs @@ -8,7 +8,7 @@ using Dalamud.Logging; using Penumbra.GameData.Enums; using Penumbra.GameData.Structs; -namespace Penumbra.GameData; +namespace Penumbra.GameData.Data; internal class GamePathParser : IGamePathParser { @@ -17,7 +17,7 @@ internal class GamePathParser : IGamePathParser path = path.ToLowerInvariant().Replace('\\', '/'); var (fileType, objectType, match) = ParseGamePath(path); - if (match == null || !match.Success) + if (match is not { Success: true }) return new GameObjectInfo { FileType = fileType, @@ -84,20 +84,20 @@ internal class GamePathParser : IGamePathParser // language=regex private readonly IReadOnlyDictionary>> _regexes = new Dictionary>>() { - [FileType.Font] = new Dictionary> + [FileType.Font] = new Dictionary> { - [ObjectType.Font] = CreateRegexes( @"common/font/(?'fontname'.*)_(?'id'\d\d)(_lobby)?\.fdt"), + [ObjectType.Font] = CreateRegexes(@"common/font/(?'fontname'.*)_(?'id'\d\d)(_lobby)?\.fdt"), }, [FileType.Texture] = new Dictionary> { - [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" + [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" @@ -105,8 +105,8 @@ internal class GamePathParser : IGamePathParser }, [FileType.Model] = new Dictionary> { - [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.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"), @@ -114,8 +114,8 @@ internal class GamePathParser : IGamePathParser }, [FileType.Material] = new Dictionary> { - [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.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"), @@ -123,8 +123,8 @@ internal class GamePathParser : IGamePathParser }, [FileType.Imc] = new Dictionary> { - [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.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"), @@ -225,7 +225,7 @@ internal class GamePathParser : IGamePathParser { var weaponId = ushort.Parse(groups["weapon"].Value); var setId = ushort.Parse(groups["id"].Value); - if (fileType == FileType.Imc || fileType == FileType.Model) + if (fileType is FileType.Imc or FileType.Model) return GameObjectInfo.Weapon(fileType, setId, weaponId); var variant = byte.Parse(groups["variant"].Value); @@ -236,7 +236,7 @@ internal class GamePathParser : IGamePathParser { var monsterId = ushort.Parse(groups["monster"].Value); var bodyId = ushort.Parse(groups["id"].Value); - if (fileType == FileType.Imc || fileType == FileType.Model) + if (fileType is FileType.Imc or FileType.Model) return GameObjectInfo.Monster(fileType, monsterId, bodyId); var variant = byte.Parse(groups["variant"].Value); @@ -322,5 +322,7 @@ internal class GamePathParser : IGamePathParser 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); + + private readonly Regex _vfxRegexPap = new(@"chara[\/]human[\/]c0101[\/]animation[\/]a0001[\/][^\s]+?[\/](?'key'[^\s]+?)\.pap", + RegexOptions.Compiled | RegexOptions.IgnoreCase); } diff --git a/Penumbra.GameData/ObjectIdentification.cs b/Penumbra.GameData/Data/ObjectIdentification.cs similarity index 69% rename from Penumbra.GameData/ObjectIdentification.cs rename to Penumbra.GameData/Data/ObjectIdentification.cs index ddffe9e2..e0a1ec40 100644 --- a/Penumbra.GameData/ObjectIdentification.cs +++ b/Penumbra.GameData/Data/ObjectIdentification.cs @@ -8,14 +8,13 @@ using System.Collections.Generic; using System.ComponentModel; using System.Linq; using Dalamud.Game.ClientState.Objects.Enums; -using Dalamud.Logging; using Dalamud.Plugin; using Dalamud.Utility; using Action = Lumina.Excel.GeneratedSheets.Action; + +namespace Penumbra.GameData.Data; -namespace Penumbra.GameData; - -internal class ObjectIdentification : IObjectIdentifier +internal sealed class ObjectIdentification : DataSharer, IObjectIdentifier { public IGamePathParser GamePathParser { get; } = new GamePathParser(); @@ -44,79 +43,45 @@ internal class ObjectIdentification : IObjectIdentifier switch (slot) { case EquipSlot.MainHand: - case EquipSlot.OffHand: - { - var (begin, _) = FindIndexRange((List<(ulong, IReadOnlyList)>)_weapons, - ((ulong)setId << 32) | ((ulong)weaponType << 16) | variant, - 0xFFFFFFFFFFFF); - return begin >= 0 ? _weapons[begin].Item2 : Array.Empty(); - } - default: - { - var (begin, _) = FindIndexRange((List<(ulong, IReadOnlyList)>)_equipment, - ((ulong)setId << 32) | ((ulong)slot.ToSlot() << 16) | variant, - 0xFFFFFFFFFFFF); - return begin >= 0 ? _equipment[begin].Item2 : Array.Empty(); - } + case EquipSlot.OffHand: + { + var (begin, _) = FindIndexRange((List<(ulong, IReadOnlyList)>)_weapons, + (ulong)setId << 32 | (ulong)weaponType << 16 | variant, + 0xFFFFFFFFFFFF); + return begin >= 0 ? _weapons[begin].Item2 : Array.Empty(); + } + default: + { + var (begin, _) = FindIndexRange((List<(ulong, IReadOnlyList)>)_equipment, + (ulong)setId << 32 | (ulong)slot.ToSlot() << 16 | variant, + 0xFFFFFFFFFFFF); + return begin >= 0 ? _equipment[begin].Item2 : Array.Empty(); + } } } - private const int Version = 1; - - private readonly DataManager _dataManager; - private readonly DalamudPluginInterface _pluginInterface; - private readonly ClientLanguage _language; - - private readonly IReadOnlyList<(ulong Key, IReadOnlyList Values)> _weapons; - private readonly IReadOnlyList<(ulong Key, IReadOnlyList Values)> _equipment; + private readonly IReadOnlyList<(ulong Key, IReadOnlyList Values)> _weapons; + private readonly IReadOnlyList<(ulong Key, IReadOnlyList Values)> _equipment; private readonly IReadOnlyList<(ulong Key, IReadOnlyList<(ObjectKind Kind, uint Id)>)> _models; - private readonly IReadOnlyDictionary> _actions; - private bool _disposed = false; + private readonly IReadOnlyDictionary> _actions; public ObjectIdentification(DalamudPluginInterface pluginInterface, DataManager dataManager, ClientLanguage language) + : base(pluginInterface, language, 1) { - _pluginInterface = pluginInterface; - _dataManager = dataManager; - _language = language; - - _weapons = TryCatchData("Weapons", CreateWeaponList); - _equipment = TryCatchData("Equipment", CreateEquipmentList); - _actions = TryCatchData("Actions", CreateActionList); - _models = TryCatchData("Models", CreateModelList); + _weapons = TryCatchData("Weapons", () => CreateWeaponList(dataManager)); + _equipment = TryCatchData("Equipment", () => CreateEquipmentList(dataManager)); + _actions = TryCatchData("Actions", () => CreateActionList(dataManager)); + _models = TryCatchData("Models", () => CreateModelList(dataManager)); } - public void Dispose() + protected override void DisposeInternal() { - if (_disposed) - return; - - GC.SuppressFinalize(this); - _pluginInterface.RelinquishData(GetVersionedTag("Weapons")); - _pluginInterface.RelinquishData(GetVersionedTag("Equipment")); - _pluginInterface.RelinquishData(GetVersionedTag("Actions")); - _pluginInterface.RelinquishData(GetVersionedTag("Models")); - _disposed = true; + DisposeTag("Weapons"); + DisposeTag("Equipment"); + DisposeTag("Actions"); + DisposeTag("Models"); } - ~ObjectIdentification() - => Dispose(); - - private string GetVersionedTag(string tag) - => $"Penumbra.Identification.{tag}.{_language}.V{Version}"; - - private T TryCatchData(string tag, Func func) where T : class - { - try - { - return _pluginInterface.GetOrCreateData(GetVersionedTag(tag), func); - } - catch (Exception ex) - { - PluginLog.Error($"Error creating shared identification data for {tag}:\n{ex}"); - return func(); - } - } - private static bool Add(IDictionary> dict, ulong key, Item item) { if (dict.TryGetValue(key, out var list)) @@ -128,25 +93,25 @@ internal class ObjectIdentification : IObjectIdentifier private static ulong EquipmentKey(Item i) { - var model = (ulong)((Lumina.Data.Parsing.Quad)i.ModelMain).A; + 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; + 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 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; + return model << 32 | type << 16 | variant; } - private IReadOnlyList<(ulong Key, IReadOnlyList Values)> CreateWeaponList() + private IReadOnlyList<(ulong Key, IReadOnlyList Values)> CreateWeaponList(DataManager gameData) { - var items = _dataManager.GetExcelSheet(_language)!; + var items = gameData.GetExcelSheet(Language)!; var storage = new SortedList>(); foreach (var item in items.Where(i => (EquipSlot)i.EquipSlotCategory.Row is EquipSlot.MainHand or EquipSlot.OffHand or EquipSlot.BothHand)) @@ -161,9 +126,9 @@ internal class ObjectIdentification : IObjectIdentifier return storage.Select(kvp => (kvp.Key, (IReadOnlyList)kvp.Value.ToArray())).ToList(); } - private IReadOnlyList<(ulong Key, IReadOnlyList Values)> CreateEquipmentList() + private IReadOnlyList<(ulong Key, IReadOnlyList Values)> CreateEquipmentList(DataManager gameData) { - var items = _dataManager.GetExcelSheet(_language)!; + var items = gameData.GetExcelSheet(Language)!; var storage = new SortedList>(); foreach (var item in items) { @@ -195,9 +160,9 @@ internal class ObjectIdentification : IObjectIdentifier return storage.Select(kvp => (kvp.Key, (IReadOnlyList)kvp.Value.ToArray())).ToList(); } - private IReadOnlyDictionary> CreateActionList() + private IReadOnlyDictionary> CreateActionList(DataManager gameData) { - var sheet = _dataManager.GetExcelSheet(_language)!; + var sheet = gameData.GetExcelSheet(Language)!; var storage = new Dictionary>((int)sheet.RowCount); void AddAction(string? key, Action action) @@ -215,29 +180,29 @@ internal class ObjectIdentification : IObjectIdentifier 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(); + 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); + AddAction(endKey, action); + AddAction(hitKey, action); } return storage.ToDictionary(kvp => kvp.Key, kvp => (IReadOnlyList)kvp.Value.ToArray()); } private static ulong ModelValue(ModelChara row) - => row.Type | ((ulong) row.Model << 8) | ((ulong) row.Base << 24) | ((ulong) row.Variant << 32); + => 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); + => Enumerable.Repeat((model, ObjectKind.BattleNpc, bNpc), 1); - private IReadOnlyList<(ulong Key, IReadOnlyList<(ObjectKind Kind, uint Id)>)> CreateModelList() + private IReadOnlyList<(ulong Key, IReadOnlyList<(ObjectKind Kind, uint Id)>)> CreateModelList(DataManager gameData) { - var sheetBNpc = _dataManager.GetExcelSheet(_language)!; - var sheetENpc = _dataManager.GetExcelSheet(_language)!; - var sheetCompanion = _dataManager.GetExcelSheet(_language)!; - var sheetMount = _dataManager.GetExcelSheet(_language)!; - var sheetModel = _dataManager.GetExcelSheet(_language)!; + var sheetBNpc = gameData.GetExcelSheet(Language)!; + var sheetENpc = gameData.GetExcelSheet(Language)!; + var sheetCompanion = gameData.GetExcelSheet(Language)!; + var sheetMount = gameData.GetExcelSheet(Language)!; + var sheetModel = gameData.GetExcelSheet(Language)!; var modelCharaToModel = sheetModel.ToDictionary(m => m.RowId, ModelValue); @@ -248,7 +213,7 @@ internal class ObjectIdentification : IObjectIdentifier .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)> { @@ -259,7 +224,7 @@ internal class ObjectIdentification : IObjectIdentifier private static (int, int) FindIndexRange(List<(ulong, IReadOnlyList)> list, ulong key, ulong mask) { var maskedKey = key & mask; - var idx = list.BinarySearch(0, list.Count, (key, null!), new Comparer()); + var idx = list.BinarySearch(0, list.Count, (key, null!), new Comparer()); if (idx < 0) { if (~idx == list.Count || maskedKey != (list[~idx].Item1 & mask)) @@ -277,17 +242,17 @@ internal class ObjectIdentification : IObjectIdentifier private void FindEquipment(IDictionary set, GameObjectInfo info) { - var key = (ulong)info.PrimaryId << 32; + var key = (ulong)info.PrimaryId << 32; var mask = 0xFFFF00000000ul; if (info.EquipSlot != EquipSlot.Unknown) { - key |= (ulong)info.EquipSlot.ToSlot() << 16; + key |= (ulong)info.EquipSlot.ToSlot() << 16; mask |= 0xFFFF0000; } if (info.Variant != 0) { - key |= info.Variant; + key |= info.Variant; mask |= 0xFFFF; } @@ -304,17 +269,17 @@ internal class ObjectIdentification : IObjectIdentifier private void FindWeapon(IDictionary set, GameObjectInfo info) { - var key = (ulong)info.PrimaryId << 32; + var key = (ulong)info.PrimaryId << 32; var mask = 0xFFFF00000000ul; if (info.SecondaryId != 0) { - key |= (ulong)info.SecondaryId << 16; + key |= (ulong)info.SecondaryId << 16; mask |= 0xFFFF0000; } if (info.Variant != 0) { - key |= info.Variant; + key |= info.Variant; mask |= 0xFFFF; } @@ -384,7 +349,7 @@ internal class ObjectIdentification : IObjectIdentifier break; case ObjectType.Character: var (gender, race) = info.GenderRace.Split(); - var raceString = race != ModelRace.Unknown ? race.ToName() + " " : ""; + var raceString = race != ModelRace.Unknown ? race.ToName() + " " : ""; var genderString = gender != Gender.Unknown ? gender.ToName() + " " : "Player "; switch (info.CustomizationType) { @@ -400,16 +365,16 @@ internal class ObjectIdentification : IObjectIdentifier 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; - } + 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; diff --git a/Penumbra.GameData/Data/StainData.cs b/Penumbra.GameData/Data/StainData.cs new file mode 100644 index 00000000..f4c4080e --- /dev/null +++ b/Penumbra.GameData/Data/StainData.cs @@ -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 +{ + public readonly IReadOnlyDictionary 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 CreateStainData(DataManager dataManager) + { + var stainSheet = dataManager.GetExcelSheet(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> GetEnumerator() + => Data.Select(kvp => new KeyValuePair(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 Keys + => Data.Keys.Select(k => new StainId(k)); + + public IEnumerable Values + => Data.Select(kvp => new Stain(kvp.Value.Name, kvp.Value.Dye, kvp.Key, kvp.Value.Gloss)); +} diff --git a/Penumbra.GameData/Files/MdlFile.cs b/Penumbra.GameData/Files/MdlFile.cs index e39f612c..09efb624 100644 --- a/Penumbra.GameData/Files/MdlFile.cs +++ b/Penumbra.GameData/Files/MdlFile.cs @@ -20,10 +20,10 @@ public partial class MdlFile : IWritable public ushort[] ShapeMeshStartIndex; public ushort[] ShapeMeshCount; - public Shape( MdlStructs.ShapeStruct data, uint[] offsets, string[] strings ) + public Shape(MdlStructs.ShapeStruct data, uint[] offsets, string[] strings) { - var idx = offsets.AsSpan().IndexOf( data.StringOffset ); - ShapeName = idx >= 0 ? strings[ idx ] : string.Empty; + var idx = offsets.AsSpan().IndexOf(data.StringOffset); + ShapeName = idx >= 0 ? strings[idx] : string.Empty; ShapeMeshStartIndex = data.ShapeMeshStartIndex; ShapeMeshCount = data.ShapeMeshCount; } @@ -85,144 +85,128 @@ public partial class MdlFile : IWritable // Raw, unparsed data. public byte[] RemainingData; - public MdlFile( byte[] data ) + public MdlFile(byte[] data) { - using var stream = new MemoryStream( data ); - using var r = new LuminaBinaryReader( stream ); + using var stream = new MemoryStream(data); + using var r = new LuminaBinaryReader(stream); - var header = LoadModelFileHeader( r ); + var header = LoadModelFileHeader(r); LodCount = header.LodCount; VertexBufferSize = header.VertexBufferSize; IndexBufferSize = header.IndexBufferSize; VertexOffset = header.VertexOffset; IndexOffset = header.IndexOffset; - for( var i = 0; i < 3; ++i ) + for (var i = 0; i < 3; ++i) { - if( VertexOffset[ i ] > 0 ) - { - VertexOffset[ i ] -= header.RuntimeSize; - } + if (VertexOffset[i] > 0) + VertexOffset[i] -= header.RuntimeSize; - if( IndexOffset[ i ] > 0 ) - { - IndexOffset[ i ] -= header.RuntimeSize; - } + if (IndexOffset[i] > 0) + IndexOffset[i] -= header.RuntimeSize; } VertexDeclarations = new MdlStructs.VertexDeclarationStruct[header.VertexDeclarationCount]; - for( var i = 0; i < header.VertexDeclarationCount; ++i ) - { - VertexDeclarations[ i ] = MdlStructs.VertexDeclarationStruct.Read( r ); - } + for (var i = 0; i < header.VertexDeclarationCount; ++i) + VertexDeclarations[i] = MdlStructs.VertexDeclarationStruct.Read(r); - var (offsets, strings) = LoadStrings( r ); + var (offsets, strings) = LoadStrings(r); - var modelHeader = LoadModelHeader( r ); + var modelHeader = LoadModelHeader(r); ElementIds = new MdlStructs.ElementIdStruct[modelHeader.ElementIdCount]; - for( var i = 0; i < modelHeader.ElementIdCount; i++ ) - { - ElementIds[ i ] = MdlStructs.ElementIdStruct.Read( r ); - } + for (var i = 0; i < modelHeader.ElementIdCount; i++) + ElementIds[i] = MdlStructs.ElementIdStruct.Read(r); - Lods = r.ReadStructuresAsArray< MdlStructs.LodStruct >( 3 ); + Lods = r.ReadStructuresAsArray(3); ExtraLods = modelHeader.ExtraLodEnabled - ? r.ReadStructuresAsArray< MdlStructs.ExtraLodStruct >( 3 ) - : Array.Empty< MdlStructs.ExtraLodStruct >(); + ? r.ReadStructuresAsArray(3) + : Array.Empty(); Meshes = new MdlStructs.MeshStruct[modelHeader.MeshCount]; - for( var i = 0; i < modelHeader.MeshCount; i++ ) - { - Meshes[ i ] = MdlStructs.MeshStruct.Read( r ); - } + for (var i = 0; i < modelHeader.MeshCount; i++) + Meshes[i] = MdlStructs.MeshStruct.Read(r); Attributes = new string[modelHeader.AttributeCount]; - for( var i = 0; i < modelHeader.AttributeCount; ++i ) + for (var i = 0; i < modelHeader.AttributeCount; ++i) { var offset = r.ReadUInt32(); - var stringIdx = offsets.AsSpan().IndexOf( offset ); - Attributes[ i ] = stringIdx >= 0 ? strings[ stringIdx ] : string.Empty; + var stringIdx = offsets.AsSpan().IndexOf(offset); + Attributes[i] = stringIdx >= 0 ? strings[stringIdx] : string.Empty; } - TerrainShadowMeshes = r.ReadStructuresAsArray< MdlStructs.TerrainShadowMeshStruct >( modelHeader.TerrainShadowMeshCount ); - SubMeshes = r.ReadStructuresAsArray< MdlStructs.SubmeshStruct >( modelHeader.SubmeshCount ); - TerrainShadowSubMeshes = r.ReadStructuresAsArray< MdlStructs.TerrainShadowSubmeshStruct >( modelHeader.TerrainShadowSubmeshCount ); + TerrainShadowMeshes = r.ReadStructuresAsArray(modelHeader.TerrainShadowMeshCount); + SubMeshes = r.ReadStructuresAsArray(modelHeader.SubmeshCount); + TerrainShadowSubMeshes = r.ReadStructuresAsArray(modelHeader.TerrainShadowSubmeshCount); Materials = new string[modelHeader.MaterialCount]; - for( var i = 0; i < modelHeader.MaterialCount; ++i ) + for (var i = 0; i < modelHeader.MaterialCount; ++i) { var offset = r.ReadUInt32(); - var stringIdx = offsets.AsSpan().IndexOf( offset ); - Materials[ i ] = stringIdx >= 0 ? strings[ stringIdx ] : string.Empty; + var stringIdx = offsets.AsSpan().IndexOf(offset); + Materials[i] = stringIdx >= 0 ? strings[stringIdx] : string.Empty; } Bones = new string[modelHeader.BoneCount]; - for( var i = 0; i < modelHeader.BoneCount; ++i ) + for (var i = 0; i < modelHeader.BoneCount; ++i) { var offset = r.ReadUInt32(); - var stringIdx = offsets.AsSpan().IndexOf( offset ); - Bones[ i ] = stringIdx >= 0 ? strings[ stringIdx ] : string.Empty; + var stringIdx = offsets.AsSpan().IndexOf(offset); + Bones[i] = stringIdx >= 0 ? strings[stringIdx] : string.Empty; } BoneTables = new MdlStructs.BoneTableStruct[modelHeader.BoneTableCount]; - for( var i = 0; i < modelHeader.BoneTableCount; i++ ) - { - BoneTables[ i ] = MdlStructs.BoneTableStruct.Read( r ); - } + for (var i = 0; i < modelHeader.BoneTableCount; i++) + BoneTables[i] = MdlStructs.BoneTableStruct.Read(r); Shapes = new Shape[modelHeader.ShapeCount]; - for( var i = 0; i < modelHeader.ShapeCount; i++ ) - { - Shapes[ i ] = new Shape( MdlStructs.ShapeStruct.Read( r ), offsets, strings ); - } + for (var i = 0; i < modelHeader.ShapeCount; i++) + Shapes[i] = new Shape(MdlStructs.ShapeStruct.Read(r), offsets, strings); - ShapeMeshes = r.ReadStructuresAsArray< MdlStructs.ShapeMeshStruct >( modelHeader.ShapeMeshCount ); - ShapeValues = r.ReadStructuresAsArray< MdlStructs.ShapeValueStruct >( modelHeader.ShapeValueCount ); + ShapeMeshes = r.ReadStructuresAsArray(modelHeader.ShapeMeshCount); + ShapeValues = r.ReadStructuresAsArray(modelHeader.ShapeValueCount); var submeshBoneMapSize = r.ReadUInt32(); - SubMeshBoneMap = r.ReadStructures< ushort >( ( int )submeshBoneMapSize / 2 ).ToArray(); + SubMeshBoneMap = r.ReadStructures((int)submeshBoneMapSize / 2).ToArray(); var paddingAmount = r.ReadByte(); - r.Seek( r.BaseStream.Position + paddingAmount ); + r.Seek(r.BaseStream.Position + paddingAmount); // Dunno what this first one is for? - BoundingBoxes = MdlStructs.BoundingBoxStruct.Read( r ); - ModelBoundingBoxes = MdlStructs.BoundingBoxStruct.Read( r ); - WaterBoundingBoxes = MdlStructs.BoundingBoxStruct.Read( r ); - VerticalFogBoundingBoxes = MdlStructs.BoundingBoxStruct.Read( r ); + BoundingBoxes = MdlStructs.BoundingBoxStruct.Read(r); + ModelBoundingBoxes = MdlStructs.BoundingBoxStruct.Read(r); + WaterBoundingBoxes = MdlStructs.BoundingBoxStruct.Read(r); + VerticalFogBoundingBoxes = MdlStructs.BoundingBoxStruct.Read(r); BoneBoundingBoxes = new MdlStructs.BoundingBoxStruct[modelHeader.BoneCount]; - for( var i = 0; i < modelHeader.BoneCount; i++ ) - { - BoneBoundingBoxes[ i ] = MdlStructs.BoundingBoxStruct.Read( r ); - } + for (var i = 0; i < modelHeader.BoneCount; i++) + BoneBoundingBoxes[i] = MdlStructs.BoundingBoxStruct.Read(r); - RemainingData = r.ReadBytes( ( int )( r.BaseStream.Length - r.BaseStream.Position ) ); + RemainingData = r.ReadBytes((int)(r.BaseStream.Length - r.BaseStream.Position)); } - private MdlStructs.ModelFileHeader LoadModelFileHeader( LuminaBinaryReader r ) + private MdlStructs.ModelFileHeader LoadModelFileHeader(LuminaBinaryReader r) { - var header = MdlStructs.ModelFileHeader.Read( r ); + var header = MdlStructs.ModelFileHeader.Read(r); Version = header.Version; EnableIndexBufferStreaming = header.EnableIndexBufferStreaming; EnableEdgeGeometry = header.EnableEdgeGeometry; return header; } - private MdlStructs.ModelHeader LoadModelHeader( BinaryReader r ) + private MdlStructs.ModelHeader LoadModelHeader(BinaryReader r) { - var modelHeader = r.ReadStructure< MdlStructs.ModelHeader >(); + var modelHeader = r.ReadStructure(); Radius = modelHeader.Radius; - Flags1 = ( MdlStructs.ModelFlags1 )( modelHeader.GetType() - .GetField( "Flags1", BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.Public )?.GetValue( modelHeader ) - ?? 0 ); - Flags2 = ( MdlStructs.ModelFlags2 )( modelHeader.GetType() - .GetField( "Flags2", BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.Public )?.GetValue( modelHeader ) - ?? 0 ); + Flags1 = (MdlStructs.ModelFlags1)(modelHeader.GetType() + .GetField("Flags1", BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.Public)?.GetValue(modelHeader) + ?? 0); + Flags2 = (MdlStructs.ModelFlags2)(modelHeader.GetType() + .GetField("Flags2", BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.Public)?.GetValue(modelHeader) + ?? 0); ModelClipOutDistance = modelHeader.ModelClipOutDistance; ShadowClipOutDistance = modelHeader.ShadowClipOutDistance; Unknown4 = modelHeader.Unknown4; - Unknown5 = ( byte )( modelHeader.GetType() - .GetField( "Unknown5", BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.Public )?.GetValue( modelHeader ) - ?? 0 ); + Unknown5 = (byte)(modelHeader.GetType() + .GetField("Unknown5", BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.Public)?.GetValue(modelHeader) + ?? 0); Unknown6 = modelHeader.Unknown6; Unknown7 = modelHeader.Unknown7; Unknown8 = modelHeader.Unknown8; @@ -233,27 +217,27 @@ public partial class MdlFile : IWritable return modelHeader; } - private static (uint[], string[]) LoadStrings( BinaryReader r ) + private static (uint[], string[]) LoadStrings(BinaryReader r) { var stringCount = r.ReadUInt16(); r.ReadUInt16(); - var stringSize = ( int )r.ReadUInt32(); - var stringData = r.ReadBytes( stringSize ); + var stringSize = (int)r.ReadUInt32(); + var stringData = r.ReadBytes(stringSize); var start = 0; var strings = new string[stringCount]; var offsets = new uint[stringCount]; - for( var i = 0; i < stringCount; ++i ) + for (var i = 0; i < stringCount; ++i) { - var span = stringData.AsSpan( start ); - var idx = span.IndexOf( ( byte )'\0' ); - strings[ i ] = Encoding.UTF8.GetString( span[ ..idx ] ); - offsets[ i ] = ( uint )start; - start = start + idx + 1; + var span = stringData.AsSpan(start); + var idx = span.IndexOf((byte)'\0'); + strings[i] = Encoding.UTF8.GetString(span[..idx]); + offsets[i] = (uint)start; + start = start + idx + 1; } - return ( offsets, strings ); + return (offsets, strings); } public unsafe uint StackSize - => ( uint )( VertexDeclarations.Length * NumVertices * sizeof( MdlStructs.VertexElement ) ); -} \ No newline at end of file + => (uint)(VertexDeclarations.Length * NumVertices * sizeof(MdlStructs.VertexElement)); +} diff --git a/Penumbra.GameData/Files/StmFile.cs b/Penumbra.GameData/Files/StmFile.cs new file mode 100644 index 00000000..38f0bc47 --- /dev/null +++ b/Penumbra.GameData/Files/StmFile.cs @@ -0,0 +1,197 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.IO; +using Dalamud.Data; +using Lumina.Extensions; +using Penumbra.GameData.Structs; + +namespace Penumbra.GameData.Files; + +public partial class StmFile +{ + public const string Path = "chara/base_material/stainingtemplate.stm"; + + public record struct DyePack + { + public uint Diffuse; + public uint Specular; + public uint Emissive; + public float SpecularPower; + public float Gloss; + } + + public readonly struct StainingTemplateEntry + { + public const int NumElements = 128; + + public readonly IReadOnlyList<(Half R, Half G, Half B)> DiffuseEntries; + public readonly IReadOnlyList<(Half R, Half G, Half B)> SpecularEntries; + public readonly IReadOnlyList<(Half R, Half G, Half B)> EmissiveEntries; + public readonly IReadOnlyList SpecularPowerEntries; + public readonly IReadOnlyList GlossEntries; + + private static uint HalfToByte(Half value) + => (byte)((float)value * byte.MaxValue + 0.5f); + + public DyePack this[StainId idx] + => this[(int)idx.Value]; + + public DyePack this[int idx] + { + get + { + var (dr, dg, db) = DiffuseEntries[idx]; + var (sr, sg, sb) = SpecularEntries[idx]; + var (er, eg, eb) = EmissiveEntries[idx]; + var sp = SpecularPowerEntries[idx]; + var g = GlossEntries[idx]; + return new DyePack() + { + Diffuse = 0xFF000000u | HalfToByte(dr) | (HalfToByte(dg) << 8) | (HalfToByte(db) << 16), + Emissive = 0xFF000000u | HalfToByte(sr) | (HalfToByte(sg) << 8) | (HalfToByte(sb) << 16), + Specular = 0xFF000000u | HalfToByte(er) | (HalfToByte(eg) << 8) | (HalfToByte(eb) << 16), + SpecularPower = (float)sp, + Gloss = (float)g, + }; + } + } + + private class RepeatingList : IReadOnlyList + { + private readonly T _value; + public int Count { get; } + + public RepeatingList(T value, int size) + { + _value = value; + Count = size; + } + + public IEnumerator GetEnumerator() + { + for (var i = 0; i < Count; ++i) + yield return _value; + } + + IEnumerator IEnumerable.GetEnumerator() + => GetEnumerator(); + + public T this[int index] + => index >= 0 && index < Count ? _value : throw new IndexOutOfRangeException(); + } + + private class IndexedList : IReadOnlyList + { + private readonly T[] _values; + private readonly byte[] _indices; + + public IndexedList(BinaryReader br, int count, int indexCount, Func read, int entrySize) + { + _values = new T[count + 1]; + _indices = new byte[indexCount]; + _values[0] = default!; + for (var i = 1; i <= count; ++i) + _values[i] = read(br); + for (var i = 0; i < indexCount; ++i) + { + _indices[i] = br.ReadByte(); + if (_indices[i] > count) + _indices[i] = 0; + } + } + + public IEnumerator GetEnumerator() + { + for (var i = 0; i < NumElements; ++i) + yield return _values[_indices[i]]; + } + + IEnumerator IEnumerable.GetEnumerator() + => GetEnumerator(); + + public int Count + => _indices.Length; + + public T this[int index] + => index >= 0 && index < Count ? _values[_indices[index]] : throw new IndexOutOfRangeException(); + } + + private static IReadOnlyList ReadArray(BinaryReader br, int offset, int size, Func read, int entrySize) + { + br.Seek(offset); + var arraySize = size / entrySize; + switch (arraySize) + { + case 0: return new RepeatingList(default!, NumElements); + case 1: return new RepeatingList(read(br), NumElements); + case NumElements: + var ret = new T[NumElements]; + for (var i = 0; i < NumElements; ++i) + ret[i] = read(br); + return ret; + case < NumElements: return new IndexedList(br, arraySize - NumElements / entrySize / 2, NumElements, read, entrySize); + case > NumElements: throw new InvalidDataException($"Stain Template can not have more than {NumElements} elements."); + } + } + + private static (Half, Half, Half) ReadTriple(BinaryReader br) + => (br.ReadHalf(), br.ReadHalf(), br.ReadHalf()); + + private static Half ReadSingle(BinaryReader br) + => br.ReadHalf(); + + public unsafe StainingTemplateEntry(BinaryReader br, int offset) + { + br.Seek(offset); + Span ends = stackalloc ushort[5]; + for (var i = 0; i < ends.Length; ++i) + ends[i] = br.ReadUInt16(); + + offset += ends.Length * 2; + DiffuseEntries = ReadArray(br, offset, ends[0], ReadTriple, 3); + SpecularEntries = ReadArray(br, offset + ends[0], ends[1] - ends[0], ReadTriple, 3); + EmissiveEntries = ReadArray(br, offset + ends[1], ends[2] - ends[1], ReadTriple, 3); + SpecularPowerEntries = ReadArray(br, offset + ends[2], ends[3] - ends[2], ReadSingle, 1); + GlossEntries = ReadArray(br, offset + ends[3], ends[4] - ends[3], ReadSingle, 1); + } + } + + public readonly IReadOnlyDictionary Entries; + + public DyePack this[ushort template, int idx] + => Entries.TryGetValue(template, out var entry) ? entry[idx] : default; + + public DyePack this[ushort template, StainId idx] + => this[template, (int)idx.Value]; + + public StmFile(byte[] data) + { + using var stream = new MemoryStream(data); + using var br = new BinaryReader(stream); + br.ReadUInt32(); + var numEntries = br.ReadInt32(); + + var keys = new ushort[numEntries]; + var offsets = new ushort[numEntries]; + + for (var i = 0; i < numEntries; ++i) + keys[i] = br.ReadUInt16(); + + for (var i = 0; i < numEntries; ++i) + offsets[i] = br.ReadUInt16(); + + var entries = new Dictionary(numEntries); + Entries = entries; + + for (var i = 0; i < numEntries; ++i) + { + var offset = offsets[i] * 2 + 8 + 4 * numEntries; + entries.Add(keys[i], new StainingTemplateEntry(br, offset)); + } + } + + public StmFile(DataManager gameData) + : this(gameData.GetFile(Path)?.Data ?? Array.Empty()) + { } +} diff --git a/Penumbra.GameData/GameData.cs b/Penumbra.GameData/GameData.cs index 639d13d1..e29499a6 100644 --- a/Penumbra.GameData/GameData.cs +++ b/Penumbra.GameData/GameData.cs @@ -4,6 +4,7 @@ using Dalamud; using Dalamud.Data; using Dalamud.Plugin; using Lumina.Excel.GeneratedSheets; +using Penumbra.GameData.Data; using Penumbra.GameData.Enums; using Penumbra.GameData.Structs; @@ -31,11 +32,11 @@ public static class GameData } public interface IObjectIdentifier : IDisposable -{ - /// - /// An accessible parser for game paths. - /// - public IGamePathParser GamePathParser { get; } +{ + /// + /// An accessible parser for game paths. + /// + public IGamePathParser GamePathParser { get; } /// /// Add all known game objects using the given game path to the dictionary. diff --git a/Penumbra.GameData/Structs/Stain.cs b/Penumbra.GameData/Structs/Stain.cs new file mode 100644 index 00000000..c8b47b42 --- /dev/null +++ b/Penumbra.GameData/Structs/Stain.cs @@ -0,0 +1,52 @@ +using Dalamud.Utility; + +namespace Penumbra.GameData.Structs; + +// A wrapper for the clothing dyes the game provides with their RGBA color value, game ID, unmodified color value and name. +public readonly struct Stain +{ + // An empty stain with transparent color. + public static readonly Stain None = new("None"); + + public readonly string Name; + public readonly uint RgbaColor; + public readonly byte RowIndex; + public readonly bool Gloss; + + public byte R + => (byte)(RgbaColor & 0xFF); + + public byte G + => (byte)((RgbaColor >> 8) & 0xFF); + + public byte B + => (byte)((RgbaColor >> 16) & 0xFF); + + public byte Intensity + => (byte)((1 + R + G + B) / 3); + + // R and B need to be shuffled and Alpha set to max. + private static uint SeColorToRgba(uint color) + => ((color & 0xFF) << 16) | ((color >> 16) & 0xFF) | (color & 0xFF00) | 0xFF000000; + + public Stain(Lumina.Excel.GeneratedSheets.Stain stain) + : this(stain.Name.ToDalamudString().ToString(), SeColorToRgba(stain.Color), (byte)stain.RowId, stain.Unknown4) + { } + + internal Stain(string name, uint dye, byte index, bool gloss) + { + Name = name; + RowIndex = index; + Gloss = gloss; + RgbaColor = dye; + } + + // Only used by None. + private Stain(string name) + { + Name = name; + RowIndex = 0; + RgbaColor = 0; + Gloss = false; + } +} diff --git a/Penumbra/Penumbra.cs b/Penumbra/Penumbra.cs index f54b189f..9335c32d 100644 --- a/Penumbra/Penumbra.cs +++ b/Penumbra/Penumbra.cs @@ -61,6 +61,7 @@ public class Penumbra : IDalamudPlugin public static ActorManager Actors { get; private set; } = null!; public static IObjectIdentifier Identifier { get; private set; } = null!; public static IGamePathParser GamePathParser { get; private set; } = null!; + public static StainManager StainManager { get; private set; } = null!; public static readonly List< Exception > ImcExceptions = new(); @@ -85,6 +86,7 @@ public class Penumbra : IDalamudPlugin Log = new Logger(); Identifier = GameData.GameData.GetIdentifier( Dalamud.PluginInterface, Dalamud.GameData ); GamePathParser = GameData.GameData.GetGamePathParser(); + StainManager = new StainManager( Dalamud.PluginInterface, Dalamud.GameData ); DevPenumbraExists = CheckDevPluginPenumbra(); IsNotInstalledPenumbra = CheckIsNotInstalled(); @@ -292,6 +294,7 @@ public class Penumbra : IDalamudPlugin public void Dispose() { + StainManager?.Dispose(); Actors?.Dispose(); Identifier?.Dispose(); Framework?.Dispose(); diff --git a/Penumbra/Util/StainManager.cs b/Penumbra/Util/StainManager.cs new file mode 100644 index 00000000..fc42e78e --- /dev/null +++ b/Penumbra/Util/StainManager.cs @@ -0,0 +1,25 @@ +using System; +using Dalamud.Data; +using Dalamud.Plugin; +using OtterGui.Widgets; +using Penumbra.GameData.Data; +using Penumbra.GameData.Files; + +namespace Penumbra.Util; + +public class StainManager : IDisposable +{ + public readonly StainData StainData; + public readonly FilterComboColors Combo; + public readonly StmFile StmFile; + + public StainManager(DalamudPluginInterface pluginInterface, DataManager dataManager) + { + StainData = new StainData( pluginInterface, dataManager, dataManager.Language ); + Combo = new FilterComboColors( 140, StainData.Data ); + StmFile = new StmFile( dataManager ); + } + + public void Dispose() + => StainData.Dispose(); +} \ No newline at end of file