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
2
OtterGui
2
OtterGui
|
|
@ -1 +1 @@
|
||||||
Subproject commit 87debfd2eceaee8a93fecc0565c454372bcef1f3
|
Subproject commit 77ecf97a620e20a1bd65d2e76c784f6f569f4643
|
||||||
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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -8,7 +8,7 @@ using Dalamud.Logging;
|
||||||
using Penumbra.GameData.Enums;
|
using Penumbra.GameData.Enums;
|
||||||
using Penumbra.GameData.Structs;
|
using Penumbra.GameData.Structs;
|
||||||
|
|
||||||
namespace Penumbra.GameData;
|
namespace Penumbra.GameData.Data;
|
||||||
|
|
||||||
internal class GamePathParser : IGamePathParser
|
internal class GamePathParser : IGamePathParser
|
||||||
{
|
{
|
||||||
|
|
@ -17,7 +17,7 @@ internal class GamePathParser : IGamePathParser
|
||||||
path = path.ToLowerInvariant().Replace('\\', '/');
|
path = path.ToLowerInvariant().Replace('\\', '/');
|
||||||
|
|
||||||
var (fileType, objectType, match) = ParseGamePath(path);
|
var (fileType, objectType, match) = ParseGamePath(path);
|
||||||
if (match == null || !match.Success)
|
if (match is not { Success: true })
|
||||||
return new GameObjectInfo
|
return new GameObjectInfo
|
||||||
{
|
{
|
||||||
FileType = fileType,
|
FileType = fileType,
|
||||||
|
|
@ -84,20 +84,20 @@ internal class GamePathParser : IGamePathParser
|
||||||
// language=regex
|
// language=regex
|
||||||
private readonly IReadOnlyDictionary<FileType, IReadOnlyDictionary<ObjectType, IReadOnlyList<Regex>>> _regexes = new Dictionary<FileType, IReadOnlyDictionary<ObjectType, IReadOnlyList<Regex>>>()
|
private readonly IReadOnlyDictionary<FileType, IReadOnlyDictionary<ObjectType, IReadOnlyList<Regex>>> _regexes = new Dictionary<FileType, IReadOnlyDictionary<ObjectType, IReadOnlyList<Regex>>>()
|
||||||
{
|
{
|
||||||
[FileType.Font] = new Dictionary<ObjectType, IReadOnlyList<Regex>>
|
[FileType.Font] = new Dictionary<ObjectType, IReadOnlyList<Regex>>
|
||||||
{
|
{
|
||||||
[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, IReadOnlyList<Regex>>
|
[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.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.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.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.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.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.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.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.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/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/skin(?'skin'.*)\.tex"
|
||||||
, @"chara/common/texture/(?'catchlight'catchlight)(.*)\.tex"
|
, @"chara/common/texture/(?'catchlight'catchlight)(.*)\.tex"
|
||||||
|
|
@ -105,8 +105,8 @@ internal class GamePathParser : IGamePathParser
|
||||||
},
|
},
|
||||||
[FileType.Model] = new Dictionary<ObjectType, IReadOnlyList<Regex>>
|
[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.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.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.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.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.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, IReadOnlyList<Regex>>
|
[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.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.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.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.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.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, IReadOnlyList<Regex>>
|
[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.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.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.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.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"),
|
[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 weaponId = ushort.Parse(groups["weapon"].Value);
|
||||||
var setId = ushort.Parse(groups["id"].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);
|
return GameObjectInfo.Weapon(fileType, setId, weaponId);
|
||||||
|
|
||||||
var variant = byte.Parse(groups["variant"].Value);
|
var variant = byte.Parse(groups["variant"].Value);
|
||||||
|
|
@ -236,7 +236,7 @@ internal class GamePathParser : IGamePathParser
|
||||||
{
|
{
|
||||||
var monsterId = ushort.Parse(groups["monster"].Value);
|
var monsterId = ushort.Parse(groups["monster"].Value);
|
||||||
var bodyId = ushort.Parse(groups["id"].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);
|
return GameObjectInfo.Monster(fileType, monsterId, bodyId);
|
||||||
|
|
||||||
var variant = byte.Parse(groups["variant"].Value);
|
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 _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);
|
||||||
}
|
}
|
||||||
|
|
@ -8,14 +8,13 @@ using System.Collections.Generic;
|
||||||
using System.ComponentModel;
|
using System.ComponentModel;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using Dalamud.Game.ClientState.Objects.Enums;
|
using Dalamud.Game.ClientState.Objects.Enums;
|
||||||
using Dalamud.Logging;
|
|
||||||
using Dalamud.Plugin;
|
using Dalamud.Plugin;
|
||||||
using Dalamud.Utility;
|
using Dalamud.Utility;
|
||||||
using Action = Lumina.Excel.GeneratedSheets.Action;
|
using Action = Lumina.Excel.GeneratedSheets.Action;
|
||||||
|
|
||||||
|
namespace Penumbra.GameData.Data;
|
||||||
|
|
||||||
namespace Penumbra.GameData;
|
internal sealed class ObjectIdentification : DataSharer, IObjectIdentifier
|
||||||
|
|
||||||
internal class ObjectIdentification : IObjectIdentifier
|
|
||||||
{
|
{
|
||||||
public IGamePathParser GamePathParser { get; } = new GamePathParser();
|
public IGamePathParser GamePathParser { get; } = new GamePathParser();
|
||||||
|
|
||||||
|
|
@ -44,79 +43,45 @@ internal class ObjectIdentification : IObjectIdentifier
|
||||||
switch (slot)
|
switch (slot)
|
||||||
{
|
{
|
||||||
case EquipSlot.MainHand:
|
case EquipSlot.MainHand:
|
||||||
case EquipSlot.OffHand:
|
case EquipSlot.OffHand:
|
||||||
{
|
{
|
||||||
var (begin, _) = FindIndexRange((List<(ulong, IReadOnlyList<Item>)>)_weapons,
|
var (begin, _) = FindIndexRange((List<(ulong, IReadOnlyList<Item>)>)_weapons,
|
||||||
((ulong)setId << 32) | ((ulong)weaponType << 16) | variant,
|
(ulong)setId << 32 | (ulong)weaponType << 16 | variant,
|
||||||
0xFFFFFFFFFFFF);
|
0xFFFFFFFFFFFF);
|
||||||
return begin >= 0 ? _weapons[begin].Item2 : Array.Empty<Item>();
|
return begin >= 0 ? _weapons[begin].Item2 : Array.Empty<Item>();
|
||||||
}
|
}
|
||||||
default:
|
default:
|
||||||
{
|
{
|
||||||
var (begin, _) = FindIndexRange((List<(ulong, IReadOnlyList<Item>)>)_equipment,
|
var (begin, _) = FindIndexRange((List<(ulong, IReadOnlyList<Item>)>)_equipment,
|
||||||
((ulong)setId << 32) | ((ulong)slot.ToSlot() << 16) | variant,
|
(ulong)setId << 32 | (ulong)slot.ToSlot() << 16 | variant,
|
||||||
0xFFFFFFFFFFFF);
|
0xFFFFFFFFFFFF);
|
||||||
return begin >= 0 ? _equipment[begin].Item2 : Array.Empty<Item>();
|
return begin >= 0 ? _equipment[begin].Item2 : Array.Empty<Item>();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private const int Version = 1;
|
private readonly IReadOnlyList<(ulong Key, IReadOnlyList<Item> Values)> _weapons;
|
||||||
|
private readonly IReadOnlyList<(ulong Key, IReadOnlyList<Item> Values)> _equipment;
|
||||||
private readonly DataManager _dataManager;
|
|
||||||
private readonly DalamudPluginInterface _pluginInterface;
|
|
||||||
private readonly ClientLanguage _language;
|
|
||||||
|
|
||||||
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 IReadOnlyList<(ulong Key, IReadOnlyList<(ObjectKind Kind, uint Id)>)> _models;
|
||||||
private readonly IReadOnlyDictionary<string, IReadOnlyList<Action>> _actions;
|
private readonly IReadOnlyDictionary<string, IReadOnlyList<Action>> _actions;
|
||||||
private bool _disposed = false;
|
|
||||||
|
|
||||||
public ObjectIdentification(DalamudPluginInterface pluginInterface, DataManager dataManager, ClientLanguage language)
|
public ObjectIdentification(DalamudPluginInterface pluginInterface, DataManager dataManager, ClientLanguage language)
|
||||||
|
: base(pluginInterface, language, 1)
|
||||||
{
|
{
|
||||||
_pluginInterface = pluginInterface;
|
_weapons = TryCatchData("Weapons", () => CreateWeaponList(dataManager));
|
||||||
_dataManager = dataManager;
|
_equipment = TryCatchData("Equipment", () => CreateEquipmentList(dataManager));
|
||||||
_language = language;
|
_actions = TryCatchData("Actions", () => CreateActionList(dataManager));
|
||||||
|
_models = TryCatchData("Models", () => CreateModelList(dataManager));
|
||||||
_weapons = TryCatchData("Weapons", CreateWeaponList);
|
|
||||||
_equipment = TryCatchData("Equipment", CreateEquipmentList);
|
|
||||||
_actions = TryCatchData("Actions", CreateActionList);
|
|
||||||
_models = TryCatchData("Models", CreateModelList);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public void Dispose()
|
protected override void DisposeInternal()
|
||||||
{
|
{
|
||||||
if (_disposed)
|
DisposeTag("Weapons");
|
||||||
return;
|
DisposeTag("Equipment");
|
||||||
|
DisposeTag("Actions");
|
||||||
GC.SuppressFinalize(this);
|
DisposeTag("Models");
|
||||||
_pluginInterface.RelinquishData(GetVersionedTag("Weapons"));
|
|
||||||
_pluginInterface.RelinquishData(GetVersionedTag("Equipment"));
|
|
||||||
_pluginInterface.RelinquishData(GetVersionedTag("Actions"));
|
|
||||||
_pluginInterface.RelinquishData(GetVersionedTag("Models"));
|
|
||||||
_disposed = true;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
~ObjectIdentification()
|
|
||||||
=> Dispose();
|
|
||||||
|
|
||||||
private string GetVersionedTag(string tag)
|
|
||||||
=> $"Penumbra.Identification.{tag}.{_language}.V{Version}";
|
|
||||||
|
|
||||||
private 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 identification data for {tag}:\n{ex}");
|
|
||||||
return func();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private static bool Add(IDictionary<ulong, HashSet<Item>> dict, ulong key, Item item)
|
private static bool Add(IDictionary<ulong, HashSet<Item>> dict, ulong key, Item item)
|
||||||
{
|
{
|
||||||
if (dict.TryGetValue(key, out var list))
|
if (dict.TryGetValue(key, out var list))
|
||||||
|
|
@ -128,25 +93,25 @@ internal class ObjectIdentification : IObjectIdentifier
|
||||||
|
|
||||||
private static ulong EquipmentKey(Item i)
|
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 variant = (ulong)((Lumina.Data.Parsing.Quad)i.ModelMain).B;
|
||||||
var slot = (ulong)((EquipSlot)i.EquipSlotCategory.Row).ToSlot();
|
var slot = (ulong)((EquipSlot)i.EquipSlotCategory.Row).ToSlot();
|
||||||
return (model << 32) | (slot << 16) | variant;
|
return model << 32 | slot << 16 | variant;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static ulong WeaponKey(Item i, bool offhand)
|
private static ulong WeaponKey(Item i, bool offhand)
|
||||||
{
|
{
|
||||||
var quad = offhand ? (Lumina.Data.Parsing.Quad)i.ModelSub : (Lumina.Data.Parsing.Quad)i.ModelMain;
|
var quad = offhand ? (Lumina.Data.Parsing.Quad)i.ModelSub : (Lumina.Data.Parsing.Quad)i.ModelMain;
|
||||||
var model = (ulong)quad.A;
|
var model = (ulong)quad.A;
|
||||||
var type = (ulong)quad.B;
|
var type = (ulong)quad.B;
|
||||||
var variant = (ulong)quad.C;
|
var variant = (ulong)quad.C;
|
||||||
|
|
||||||
return (model << 32) | (type << 16) | variant;
|
return model << 32 | type << 16 | variant;
|
||||||
}
|
}
|
||||||
|
|
||||||
private IReadOnlyList<(ulong Key, IReadOnlyList<Item> Values)> CreateWeaponList()
|
private IReadOnlyList<(ulong Key, IReadOnlyList<Item> Values)> CreateWeaponList(DataManager gameData)
|
||||||
{
|
{
|
||||||
var items = _dataManager.GetExcelSheet<Item>(_language)!;
|
var items = gameData.GetExcelSheet<Item>(Language)!;
|
||||||
var storage = new SortedList<ulong, HashSet<Item>>();
|
var storage = new SortedList<ulong, HashSet<Item>>();
|
||||||
foreach (var item in items.Where(i
|
foreach (var item in items.Where(i
|
||||||
=> (EquipSlot)i.EquipSlotCategory.Row is EquipSlot.MainHand or EquipSlot.OffHand or EquipSlot.BothHand))
|
=> (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<Item>)kvp.Value.ToArray())).ToList();
|
return storage.Select(kvp => (kvp.Key, (IReadOnlyList<Item>)kvp.Value.ToArray())).ToList();
|
||||||
}
|
}
|
||||||
|
|
||||||
private IReadOnlyList<(ulong Key, IReadOnlyList<Item> Values)> CreateEquipmentList()
|
private IReadOnlyList<(ulong Key, IReadOnlyList<Item> Values)> CreateEquipmentList(DataManager gameData)
|
||||||
{
|
{
|
||||||
var items = _dataManager.GetExcelSheet<Item>(_language)!;
|
var items = gameData.GetExcelSheet<Item>(Language)!;
|
||||||
var storage = new SortedList<ulong, HashSet<Item>>();
|
var storage = new SortedList<ulong, HashSet<Item>>();
|
||||||
foreach (var item in items)
|
foreach (var item in items)
|
||||||
{
|
{
|
||||||
|
|
@ -195,9 +160,9 @@ internal class ObjectIdentification : IObjectIdentifier
|
||||||
return storage.Select(kvp => (kvp.Key, (IReadOnlyList<Item>)kvp.Value.ToArray())).ToList();
|
return storage.Select(kvp => (kvp.Key, (IReadOnlyList<Item>)kvp.Value.ToArray())).ToList();
|
||||||
}
|
}
|
||||||
|
|
||||||
private IReadOnlyDictionary<string, IReadOnlyList<Action>> CreateActionList()
|
private IReadOnlyDictionary<string, IReadOnlyList<Action>> CreateActionList(DataManager gameData)
|
||||||
{
|
{
|
||||||
var sheet = _dataManager.GetExcelSheet<Action>(_language)!;
|
var sheet = gameData.GetExcelSheet<Action>(Language)!;
|
||||||
var storage = new Dictionary<string, HashSet<Action>>((int)sheet.RowCount);
|
var storage = new Dictionary<string, HashSet<Action>>((int)sheet.RowCount);
|
||||||
|
|
||||||
void AddAction(string? key, Action action)
|
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))
|
foreach (var action in sheet.Where(a => !a.Name.RawData.IsEmpty))
|
||||||
{
|
{
|
||||||
var startKey = action.AnimationStart?.Value?.Name?.Value?.Key.ToDalamudString().ToString();
|
var startKey = action.AnimationStart?.Value?.Name?.Value?.Key.ToDalamudString().ToString();
|
||||||
var endKey = action.AnimationEnd?.Value?.Key.ToDalamudString().ToString();
|
var endKey = action.AnimationEnd?.Value?.Key.ToDalamudString().ToString();
|
||||||
var hitKey = action.ActionTimelineHit?.Value?.Key.ToDalamudString().ToString();
|
var hitKey = action.ActionTimelineHit?.Value?.Key.ToDalamudString().ToString();
|
||||||
AddAction(startKey, action);
|
AddAction(startKey, action);
|
||||||
AddAction(endKey, action);
|
AddAction(endKey, action);
|
||||||
AddAction(hitKey, action);
|
AddAction(hitKey, action);
|
||||||
}
|
}
|
||||||
|
|
||||||
return storage.ToDictionary(kvp => kvp.Key, kvp => (IReadOnlyList<Action>)kvp.Value.ToArray());
|
return storage.ToDictionary(kvp => kvp.Key, kvp => (IReadOnlyList<Action>)kvp.Value.ToArray());
|
||||||
}
|
}
|
||||||
|
|
||||||
private static ulong ModelValue(ModelChara row)
|
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)
|
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<BNpcBase>(_language)!;
|
var sheetBNpc = gameData.GetExcelSheet<BNpcBase>(Language)!;
|
||||||
var sheetENpc = _dataManager.GetExcelSheet<ENpcBase>(_language)!;
|
var sheetENpc = gameData.GetExcelSheet<ENpcBase>(Language)!;
|
||||||
var sheetCompanion = _dataManager.GetExcelSheet<Companion>(_language)!;
|
var sheetCompanion = gameData.GetExcelSheet<Companion>(Language)!;
|
||||||
var sheetMount = _dataManager.GetExcelSheet<Mount>(_language)!;
|
var sheetMount = gameData.GetExcelSheet<Mount>(Language)!;
|
||||||
var sheetModel = _dataManager.GetExcelSheet<ModelChara>(_language)!;
|
var sheetModel = gameData.GetExcelSheet<ModelChara>(Language)!;
|
||||||
|
|
||||||
var modelCharaToModel = sheetModel.ToDictionary(m => m.RowId, ModelValue);
|
var modelCharaToModel = sheetModel.ToDictionary(m => m.RowId, ModelValue);
|
||||||
|
|
||||||
|
|
@ -248,7 +213,7 @@ internal class ObjectIdentification : IObjectIdentifier
|
||||||
.GroupBy(t => t.Item1)
|
.GroupBy(t => t.Item1)
|
||||||
.Select(g => (g.Key, (IReadOnlyList<(ObjectKind, uint)>)g.Select(p => (p.Item2, p.Item3)).ToArray()))
|
.Select(g => (g.Key, (IReadOnlyList<(ObjectKind, uint)>)g.Select(p => (p.Item2, p.Item3)).ToArray()))
|
||||||
.ToArray();
|
.ToArray();
|
||||||
}
|
}
|
||||||
|
|
||||||
private class Comparer : IComparer<(ulong, IReadOnlyList<Item>)>
|
private class Comparer : IComparer<(ulong, IReadOnlyList<Item>)>
|
||||||
{
|
{
|
||||||
|
|
@ -259,7 +224,7 @@ internal class ObjectIdentification : IObjectIdentifier
|
||||||
private static (int, int) FindIndexRange(List<(ulong, IReadOnlyList<Item>)> list, ulong key, ulong mask)
|
private static (int, int) FindIndexRange(List<(ulong, IReadOnlyList<Item>)> list, ulong key, ulong mask)
|
||||||
{
|
{
|
||||||
var maskedKey = key & 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 < 0)
|
||||||
{
|
{
|
||||||
if (~idx == list.Count || maskedKey != (list[~idx].Item1 & mask))
|
if (~idx == list.Count || maskedKey != (list[~idx].Item1 & mask))
|
||||||
|
|
@ -277,17 +242,17 @@ internal class ObjectIdentification : IObjectIdentifier
|
||||||
|
|
||||||
private void FindEquipment(IDictionary<string, object?> set, GameObjectInfo info)
|
private void FindEquipment(IDictionary<string, object?> set, GameObjectInfo info)
|
||||||
{
|
{
|
||||||
var key = (ulong)info.PrimaryId << 32;
|
var key = (ulong)info.PrimaryId << 32;
|
||||||
var mask = 0xFFFF00000000ul;
|
var mask = 0xFFFF00000000ul;
|
||||||
if (info.EquipSlot != EquipSlot.Unknown)
|
if (info.EquipSlot != EquipSlot.Unknown)
|
||||||
{
|
{
|
||||||
key |= (ulong)info.EquipSlot.ToSlot() << 16;
|
key |= (ulong)info.EquipSlot.ToSlot() << 16;
|
||||||
mask |= 0xFFFF0000;
|
mask |= 0xFFFF0000;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (info.Variant != 0)
|
if (info.Variant != 0)
|
||||||
{
|
{
|
||||||
key |= info.Variant;
|
key |= info.Variant;
|
||||||
mask |= 0xFFFF;
|
mask |= 0xFFFF;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -304,17 +269,17 @@ internal class ObjectIdentification : IObjectIdentifier
|
||||||
|
|
||||||
private void FindWeapon(IDictionary<string, object?> set, GameObjectInfo info)
|
private void FindWeapon(IDictionary<string, object?> set, GameObjectInfo info)
|
||||||
{
|
{
|
||||||
var key = (ulong)info.PrimaryId << 32;
|
var key = (ulong)info.PrimaryId << 32;
|
||||||
var mask = 0xFFFF00000000ul;
|
var mask = 0xFFFF00000000ul;
|
||||||
if (info.SecondaryId != 0)
|
if (info.SecondaryId != 0)
|
||||||
{
|
{
|
||||||
key |= (ulong)info.SecondaryId << 16;
|
key |= (ulong)info.SecondaryId << 16;
|
||||||
mask |= 0xFFFF0000;
|
mask |= 0xFFFF0000;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (info.Variant != 0)
|
if (info.Variant != 0)
|
||||||
{
|
{
|
||||||
key |= info.Variant;
|
key |= info.Variant;
|
||||||
mask |= 0xFFFF;
|
mask |= 0xFFFF;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -384,7 +349,7 @@ internal class ObjectIdentification : IObjectIdentifier
|
||||||
break;
|
break;
|
||||||
case ObjectType.Character:
|
case ObjectType.Character:
|
||||||
var (gender, race) = info.GenderRace.Split();
|
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 ";
|
var genderString = gender != Gender.Unknown ? gender.ToName() + " " : "Player ";
|
||||||
switch (info.CustomizationType)
|
switch (info.CustomizationType)
|
||||||
{
|
{
|
||||||
|
|
@ -400,16 +365,16 @@ internal class ObjectIdentification : IObjectIdentifier
|
||||||
case CustomizationType.DecalEquip:
|
case CustomizationType.DecalEquip:
|
||||||
set[$"Equipment Decal {info.PrimaryId}"] = null;
|
set[$"Equipment Decal {info.PrimaryId}"] = null;
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
{
|
{
|
||||||
var customizationString = race == ModelRace.Unknown
|
var customizationString = race == ModelRace.Unknown
|
||||||
|| info.BodySlot == BodySlot.Unknown
|
|| info.BodySlot == BodySlot.Unknown
|
||||||
|| info.CustomizationType == CustomizationType.Unknown
|
|| info.CustomizationType == CustomizationType.Unknown
|
||||||
? "Customization: Unknown"
|
? "Customization: Unknown"
|
||||||
: $"Customization: {race} {gender} {info.BodySlot} ({info.CustomizationType}) {info.PrimaryId}";
|
: $"Customization: {race} {gender} {info.BodySlot} ({info.CustomizationType}) {info.PrimaryId}";
|
||||||
set[customizationString] = null;
|
set[customizationString] = null;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
break;
|
break;
|
||||||
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));
|
||||||
|
}
|
||||||
|
|
@ -20,10 +20,10 @@ public partial class MdlFile : IWritable
|
||||||
public ushort[] ShapeMeshStartIndex;
|
public ushort[] ShapeMeshStartIndex;
|
||||||
public ushort[] ShapeMeshCount;
|
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 );
|
var idx = offsets.AsSpan().IndexOf(data.StringOffset);
|
||||||
ShapeName = idx >= 0 ? strings[ idx ] : string.Empty;
|
ShapeName = idx >= 0 ? strings[idx] : string.Empty;
|
||||||
ShapeMeshStartIndex = data.ShapeMeshStartIndex;
|
ShapeMeshStartIndex = data.ShapeMeshStartIndex;
|
||||||
ShapeMeshCount = data.ShapeMeshCount;
|
ShapeMeshCount = data.ShapeMeshCount;
|
||||||
}
|
}
|
||||||
|
|
@ -85,144 +85,128 @@ public partial class MdlFile : IWritable
|
||||||
// Raw, unparsed data.
|
// Raw, unparsed data.
|
||||||
public byte[] RemainingData;
|
public byte[] RemainingData;
|
||||||
|
|
||||||
public MdlFile( byte[] data )
|
public MdlFile(byte[] data)
|
||||||
{
|
{
|
||||||
using var stream = new MemoryStream( data );
|
using var stream = new MemoryStream(data);
|
||||||
using var r = new LuminaBinaryReader( stream );
|
using var r = new LuminaBinaryReader(stream);
|
||||||
|
|
||||||
var header = LoadModelFileHeader( r );
|
var header = LoadModelFileHeader(r);
|
||||||
LodCount = header.LodCount;
|
LodCount = header.LodCount;
|
||||||
VertexBufferSize = header.VertexBufferSize;
|
VertexBufferSize = header.VertexBufferSize;
|
||||||
IndexBufferSize = header.IndexBufferSize;
|
IndexBufferSize = header.IndexBufferSize;
|
||||||
VertexOffset = header.VertexOffset;
|
VertexOffset = header.VertexOffset;
|
||||||
IndexOffset = header.IndexOffset;
|
IndexOffset = header.IndexOffset;
|
||||||
for( var i = 0; i < 3; ++i )
|
for (var i = 0; i < 3; ++i)
|
||||||
{
|
{
|
||||||
if( VertexOffset[ i ] > 0 )
|
if (VertexOffset[i] > 0)
|
||||||
{
|
VertexOffset[i] -= header.RuntimeSize;
|
||||||
VertexOffset[ i ] -= header.RuntimeSize;
|
|
||||||
}
|
|
||||||
|
|
||||||
if( IndexOffset[ i ] > 0 )
|
if (IndexOffset[i] > 0)
|
||||||
{
|
IndexOffset[i] -= header.RuntimeSize;
|
||||||
IndexOffset[ i ] -= header.RuntimeSize;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
VertexDeclarations = new MdlStructs.VertexDeclarationStruct[header.VertexDeclarationCount];
|
VertexDeclarations = new MdlStructs.VertexDeclarationStruct[header.VertexDeclarationCount];
|
||||||
for( var i = 0; i < header.VertexDeclarationCount; ++i )
|
for (var i = 0; i < header.VertexDeclarationCount; ++i)
|
||||||
{
|
VertexDeclarations[i] = MdlStructs.VertexDeclarationStruct.Read(r);
|
||||||
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];
|
ElementIds = new MdlStructs.ElementIdStruct[modelHeader.ElementIdCount];
|
||||||
for( var i = 0; i < modelHeader.ElementIdCount; i++ )
|
for (var i = 0; i < modelHeader.ElementIdCount; i++)
|
||||||
{
|
ElementIds[i] = MdlStructs.ElementIdStruct.Read(r);
|
||||||
ElementIds[ i ] = MdlStructs.ElementIdStruct.Read( r );
|
|
||||||
}
|
|
||||||
|
|
||||||
Lods = r.ReadStructuresAsArray< MdlStructs.LodStruct >( 3 );
|
Lods = r.ReadStructuresAsArray<MdlStructs.LodStruct>(3);
|
||||||
ExtraLods = modelHeader.ExtraLodEnabled
|
ExtraLods = modelHeader.ExtraLodEnabled
|
||||||
? r.ReadStructuresAsArray< MdlStructs.ExtraLodStruct >( 3 )
|
? r.ReadStructuresAsArray<MdlStructs.ExtraLodStruct>(3)
|
||||||
: Array.Empty< MdlStructs.ExtraLodStruct >();
|
: Array.Empty<MdlStructs.ExtraLodStruct>();
|
||||||
|
|
||||||
Meshes = new MdlStructs.MeshStruct[modelHeader.MeshCount];
|
Meshes = new MdlStructs.MeshStruct[modelHeader.MeshCount];
|
||||||
for( var i = 0; i < modelHeader.MeshCount; i++ )
|
for (var i = 0; i < modelHeader.MeshCount; i++)
|
||||||
{
|
Meshes[i] = MdlStructs.MeshStruct.Read(r);
|
||||||
Meshes[ i ] = MdlStructs.MeshStruct.Read( r );
|
|
||||||
}
|
|
||||||
|
|
||||||
Attributes = new string[modelHeader.AttributeCount];
|
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 offset = r.ReadUInt32();
|
||||||
var stringIdx = offsets.AsSpan().IndexOf( offset );
|
var stringIdx = offsets.AsSpan().IndexOf(offset);
|
||||||
Attributes[ i ] = stringIdx >= 0 ? strings[ stringIdx ] : string.Empty;
|
Attributes[i] = stringIdx >= 0 ? strings[stringIdx] : string.Empty;
|
||||||
}
|
}
|
||||||
|
|
||||||
TerrainShadowMeshes = r.ReadStructuresAsArray< MdlStructs.TerrainShadowMeshStruct >( modelHeader.TerrainShadowMeshCount );
|
TerrainShadowMeshes = r.ReadStructuresAsArray<MdlStructs.TerrainShadowMeshStruct>(modelHeader.TerrainShadowMeshCount);
|
||||||
SubMeshes = r.ReadStructuresAsArray< MdlStructs.SubmeshStruct >( modelHeader.SubmeshCount );
|
SubMeshes = r.ReadStructuresAsArray<MdlStructs.SubmeshStruct>(modelHeader.SubmeshCount);
|
||||||
TerrainShadowSubMeshes = r.ReadStructuresAsArray< MdlStructs.TerrainShadowSubmeshStruct >( modelHeader.TerrainShadowSubmeshCount );
|
TerrainShadowSubMeshes = r.ReadStructuresAsArray<MdlStructs.TerrainShadowSubmeshStruct>(modelHeader.TerrainShadowSubmeshCount);
|
||||||
|
|
||||||
Materials = new string[modelHeader.MaterialCount];
|
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 offset = r.ReadUInt32();
|
||||||
var stringIdx = offsets.AsSpan().IndexOf( offset );
|
var stringIdx = offsets.AsSpan().IndexOf(offset);
|
||||||
Materials[ i ] = stringIdx >= 0 ? strings[ stringIdx ] : string.Empty;
|
Materials[i] = stringIdx >= 0 ? strings[stringIdx] : string.Empty;
|
||||||
}
|
}
|
||||||
|
|
||||||
Bones = new string[modelHeader.BoneCount];
|
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 offset = r.ReadUInt32();
|
||||||
var stringIdx = offsets.AsSpan().IndexOf( offset );
|
var stringIdx = offsets.AsSpan().IndexOf(offset);
|
||||||
Bones[ i ] = stringIdx >= 0 ? strings[ stringIdx ] : string.Empty;
|
Bones[i] = stringIdx >= 0 ? strings[stringIdx] : string.Empty;
|
||||||
}
|
}
|
||||||
|
|
||||||
BoneTables = new MdlStructs.BoneTableStruct[modelHeader.BoneTableCount];
|
BoneTables = new MdlStructs.BoneTableStruct[modelHeader.BoneTableCount];
|
||||||
for( var i = 0; i < modelHeader.BoneTableCount; i++ )
|
for (var i = 0; i < modelHeader.BoneTableCount; i++)
|
||||||
{
|
BoneTables[i] = MdlStructs.BoneTableStruct.Read(r);
|
||||||
BoneTables[ i ] = MdlStructs.BoneTableStruct.Read( r );
|
|
||||||
}
|
|
||||||
|
|
||||||
Shapes = new Shape[modelHeader.ShapeCount];
|
Shapes = new Shape[modelHeader.ShapeCount];
|
||||||
for( var i = 0; i < modelHeader.ShapeCount; i++ )
|
for (var i = 0; i < modelHeader.ShapeCount; i++)
|
||||||
{
|
Shapes[i] = new Shape(MdlStructs.ShapeStruct.Read(r), offsets, strings);
|
||||||
Shapes[ i ] = new Shape( MdlStructs.ShapeStruct.Read( r ), offsets, strings );
|
|
||||||
}
|
|
||||||
|
|
||||||
ShapeMeshes = r.ReadStructuresAsArray< MdlStructs.ShapeMeshStruct >( modelHeader.ShapeMeshCount );
|
ShapeMeshes = r.ReadStructuresAsArray<MdlStructs.ShapeMeshStruct>(modelHeader.ShapeMeshCount);
|
||||||
ShapeValues = r.ReadStructuresAsArray< MdlStructs.ShapeValueStruct >( modelHeader.ShapeValueCount );
|
ShapeValues = r.ReadStructuresAsArray<MdlStructs.ShapeValueStruct>(modelHeader.ShapeValueCount);
|
||||||
|
|
||||||
var submeshBoneMapSize = r.ReadUInt32();
|
var submeshBoneMapSize = r.ReadUInt32();
|
||||||
SubMeshBoneMap = r.ReadStructures< ushort >( ( int )submeshBoneMapSize / 2 ).ToArray();
|
SubMeshBoneMap = r.ReadStructures<ushort>((int)submeshBoneMapSize / 2).ToArray();
|
||||||
|
|
||||||
var paddingAmount = r.ReadByte();
|
var paddingAmount = r.ReadByte();
|
||||||
r.Seek( r.BaseStream.Position + paddingAmount );
|
r.Seek(r.BaseStream.Position + paddingAmount);
|
||||||
|
|
||||||
// Dunno what this first one is for?
|
// Dunno what this first one is for?
|
||||||
BoundingBoxes = MdlStructs.BoundingBoxStruct.Read( r );
|
BoundingBoxes = MdlStructs.BoundingBoxStruct.Read(r);
|
||||||
ModelBoundingBoxes = MdlStructs.BoundingBoxStruct.Read( r );
|
ModelBoundingBoxes = MdlStructs.BoundingBoxStruct.Read(r);
|
||||||
WaterBoundingBoxes = MdlStructs.BoundingBoxStruct.Read( r );
|
WaterBoundingBoxes = MdlStructs.BoundingBoxStruct.Read(r);
|
||||||
VerticalFogBoundingBoxes = MdlStructs.BoundingBoxStruct.Read( r );
|
VerticalFogBoundingBoxes = MdlStructs.BoundingBoxStruct.Read(r);
|
||||||
BoneBoundingBoxes = new MdlStructs.BoundingBoxStruct[modelHeader.BoneCount];
|
BoneBoundingBoxes = new MdlStructs.BoundingBoxStruct[modelHeader.BoneCount];
|
||||||
for( var i = 0; i < modelHeader.BoneCount; i++ )
|
for (var i = 0; i < modelHeader.BoneCount; i++)
|
||||||
{
|
BoneBoundingBoxes[i] = MdlStructs.BoundingBoxStruct.Read(r);
|
||||||
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;
|
Version = header.Version;
|
||||||
EnableIndexBufferStreaming = header.EnableIndexBufferStreaming;
|
EnableIndexBufferStreaming = header.EnableIndexBufferStreaming;
|
||||||
EnableEdgeGeometry = header.EnableEdgeGeometry;
|
EnableEdgeGeometry = header.EnableEdgeGeometry;
|
||||||
return header;
|
return header;
|
||||||
}
|
}
|
||||||
|
|
||||||
private MdlStructs.ModelHeader LoadModelHeader( BinaryReader r )
|
private MdlStructs.ModelHeader LoadModelHeader(BinaryReader r)
|
||||||
{
|
{
|
||||||
var modelHeader = r.ReadStructure< MdlStructs.ModelHeader >();
|
var modelHeader = r.ReadStructure<MdlStructs.ModelHeader>();
|
||||||
Radius = modelHeader.Radius;
|
Radius = modelHeader.Radius;
|
||||||
Flags1 = ( MdlStructs.ModelFlags1 )( modelHeader.GetType()
|
Flags1 = (MdlStructs.ModelFlags1)(modelHeader.GetType()
|
||||||
.GetField( "Flags1", BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.Public )?.GetValue( modelHeader )
|
.GetField("Flags1", BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.Public)?.GetValue(modelHeader)
|
||||||
?? 0 );
|
?? 0);
|
||||||
Flags2 = ( MdlStructs.ModelFlags2 )( modelHeader.GetType()
|
Flags2 = (MdlStructs.ModelFlags2)(modelHeader.GetType()
|
||||||
.GetField( "Flags2", BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.Public )?.GetValue( modelHeader )
|
.GetField("Flags2", BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.Public)?.GetValue(modelHeader)
|
||||||
?? 0 );
|
?? 0);
|
||||||
ModelClipOutDistance = modelHeader.ModelClipOutDistance;
|
ModelClipOutDistance = modelHeader.ModelClipOutDistance;
|
||||||
ShadowClipOutDistance = modelHeader.ShadowClipOutDistance;
|
ShadowClipOutDistance = modelHeader.ShadowClipOutDistance;
|
||||||
Unknown4 = modelHeader.Unknown4;
|
Unknown4 = modelHeader.Unknown4;
|
||||||
Unknown5 = ( byte )( modelHeader.GetType()
|
Unknown5 = (byte)(modelHeader.GetType()
|
||||||
.GetField( "Unknown5", BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.Public )?.GetValue( modelHeader )
|
.GetField("Unknown5", BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.Public)?.GetValue(modelHeader)
|
||||||
?? 0 );
|
?? 0);
|
||||||
Unknown6 = modelHeader.Unknown6;
|
Unknown6 = modelHeader.Unknown6;
|
||||||
Unknown7 = modelHeader.Unknown7;
|
Unknown7 = modelHeader.Unknown7;
|
||||||
Unknown8 = modelHeader.Unknown8;
|
Unknown8 = modelHeader.Unknown8;
|
||||||
|
|
@ -233,27 +217,27 @@ public partial class MdlFile : IWritable
|
||||||
return modelHeader;
|
return modelHeader;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static (uint[], string[]) LoadStrings( BinaryReader r )
|
private static (uint[], string[]) LoadStrings(BinaryReader r)
|
||||||
{
|
{
|
||||||
var stringCount = r.ReadUInt16();
|
var stringCount = r.ReadUInt16();
|
||||||
r.ReadUInt16();
|
r.ReadUInt16();
|
||||||
var stringSize = ( int )r.ReadUInt32();
|
var stringSize = (int)r.ReadUInt32();
|
||||||
var stringData = r.ReadBytes( stringSize );
|
var stringData = r.ReadBytes(stringSize);
|
||||||
var start = 0;
|
var start = 0;
|
||||||
var strings = new string[stringCount];
|
var strings = new string[stringCount];
|
||||||
var offsets = new uint[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 span = stringData.AsSpan(start);
|
||||||
var idx = span.IndexOf( ( byte )'\0' );
|
var idx = span.IndexOf((byte)'\0');
|
||||||
strings[ i ] = Encoding.UTF8.GetString( span[ ..idx ] );
|
strings[i] = Encoding.UTF8.GetString(span[..idx]);
|
||||||
offsets[ i ] = ( uint )start;
|
offsets[i] = (uint)start;
|
||||||
start = start + idx + 1;
|
start = start + idx + 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
return ( offsets, strings );
|
return (offsets, strings);
|
||||||
}
|
}
|
||||||
|
|
||||||
public unsafe uint StackSize
|
public unsafe uint StackSize
|
||||||
=> ( uint )( VertexDeclarations.Length * NumVertices * sizeof( MdlStructs.VertexElement ) );
|
=> (uint)(VertexDeclarations.Length * NumVertices * sizeof(MdlStructs.VertexElement));
|
||||||
}
|
}
|
||||||
|
|
|
||||||
197
Penumbra.GameData/Files/StmFile.cs
Normal file
197
Penumbra.GameData/Files/StmFile.cs
Normal file
|
|
@ -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<Half> SpecularPowerEntries;
|
||||||
|
public readonly IReadOnlyList<Half> 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<T> : IReadOnlyList<T>
|
||||||
|
{
|
||||||
|
private readonly T _value;
|
||||||
|
public int Count { get; }
|
||||||
|
|
||||||
|
public RepeatingList(T value, int size)
|
||||||
|
{
|
||||||
|
_value = value;
|
||||||
|
Count = size;
|
||||||
|
}
|
||||||
|
|
||||||
|
public IEnumerator<T> 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<T> : IReadOnlyList<T>
|
||||||
|
{
|
||||||
|
private readonly T[] _values;
|
||||||
|
private readonly byte[] _indices;
|
||||||
|
|
||||||
|
public IndexedList(BinaryReader br, int count, int indexCount, Func<BinaryReader, T> 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<T> 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<T> ReadArray<T>(BinaryReader br, int offset, int size, Func<BinaryReader, T> read, int entrySize)
|
||||||
|
{
|
||||||
|
br.Seek(offset);
|
||||||
|
var arraySize = size / entrySize;
|
||||||
|
switch (arraySize)
|
||||||
|
{
|
||||||
|
case 0: return new RepeatingList<T>(default!, NumElements);
|
||||||
|
case 1: return new RepeatingList<T>(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<T>(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<ushort> 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<ushort, StainingTemplateEntry> 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<ushort, StainingTemplateEntry>(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<byte>())
|
||||||
|
{ }
|
||||||
|
}
|
||||||
|
|
@ -4,6 +4,7 @@ using Dalamud;
|
||||||
using Dalamud.Data;
|
using Dalamud.Data;
|
||||||
using Dalamud.Plugin;
|
using Dalamud.Plugin;
|
||||||
using Lumina.Excel.GeneratedSheets;
|
using Lumina.Excel.GeneratedSheets;
|
||||||
|
using Penumbra.GameData.Data;
|
||||||
using Penumbra.GameData.Enums;
|
using Penumbra.GameData.Enums;
|
||||||
using Penumbra.GameData.Structs;
|
using Penumbra.GameData.Structs;
|
||||||
|
|
||||||
|
|
@ -31,11 +32,11 @@ public static class GameData
|
||||||
}
|
}
|
||||||
|
|
||||||
public interface IObjectIdentifier : IDisposable
|
public interface IObjectIdentifier : IDisposable
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// An accessible parser for game paths.
|
/// An accessible parser for game paths.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public IGamePathParser GamePathParser { get; }
|
public IGamePathParser GamePathParser { get; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Add all known game objects using the given game path to the dictionary.
|
/// Add all known game objects using the given game path to the dictionary.
|
||||||
|
|
|
||||||
52
Penumbra.GameData/Structs/Stain.cs
Normal file
52
Penumbra.GameData/Structs/Stain.cs
Normal file
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -61,6 +61,7 @@ public class Penumbra : IDalamudPlugin
|
||||||
public static ActorManager Actors { get; private set; } = null!;
|
public static ActorManager Actors { get; private set; } = null!;
|
||||||
public static IObjectIdentifier Identifier { get; private set; } = null!;
|
public static IObjectIdentifier Identifier { get; private set; } = null!;
|
||||||
public static IGamePathParser GamePathParser { 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();
|
public static readonly List< Exception > ImcExceptions = new();
|
||||||
|
|
||||||
|
|
@ -85,6 +86,7 @@ public class Penumbra : IDalamudPlugin
|
||||||
Log = new Logger();
|
Log = new Logger();
|
||||||
Identifier = GameData.GameData.GetIdentifier( Dalamud.PluginInterface, Dalamud.GameData );
|
Identifier = GameData.GameData.GetIdentifier( Dalamud.PluginInterface, Dalamud.GameData );
|
||||||
GamePathParser = GameData.GameData.GetGamePathParser();
|
GamePathParser = GameData.GameData.GetGamePathParser();
|
||||||
|
StainManager = new StainManager( Dalamud.PluginInterface, Dalamud.GameData );
|
||||||
DevPenumbraExists = CheckDevPluginPenumbra();
|
DevPenumbraExists = CheckDevPluginPenumbra();
|
||||||
IsNotInstalledPenumbra = CheckIsNotInstalled();
|
IsNotInstalledPenumbra = CheckIsNotInstalled();
|
||||||
|
|
||||||
|
|
@ -292,6 +294,7 @@ public class Penumbra : IDalamudPlugin
|
||||||
|
|
||||||
public void Dispose()
|
public void Dispose()
|
||||||
{
|
{
|
||||||
|
StainManager?.Dispose();
|
||||||
Actors?.Dispose();
|
Actors?.Dispose();
|
||||||
Identifier?.Dispose();
|
Identifier?.Dispose();
|
||||||
Framework?.Dispose();
|
Framework?.Dispose();
|
||||||
|
|
|
||||||
25
Penumbra/Util/StainManager.cs
Normal file
25
Penumbra/Util/StainManager.cs
Normal file
|
|
@ -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();
|
||||||
|
}
|
||||||
Loading…
Add table
Add a link
Reference in a new issue