Add some refactoring of data, Stains and STM files.

This commit is contained in:
Ottermandias 2022-11-09 13:53:52 +01:00
parent 7e167cf0cf
commit 8d11e1075d
11 changed files with 580 additions and 227 deletions

@ -1 +1 @@
Subproject commit 87debfd2eceaee8a93fecc0565c454372bcef1f3 Subproject commit 77ecf97a620e20a1bd65d2e76c784f6f569f4643

View 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();
}
}
}

View file

@ -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,
@ -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);
} }

View file

@ -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; namespace Penumbra.GameData.Data;
internal class ObjectIdentification : IObjectIdentifier internal sealed class ObjectIdentification : DataSharer, IObjectIdentifier
{ {
public IGamePathParser GamePathParser { get; } = new GamePathParser(); public IGamePathParser GamePathParser { get; } = new GamePathParser();
@ -47,74 +46,40 @@ internal class ObjectIdentification : IObjectIdentifier
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 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)> _weapons;
private readonly IReadOnlyList<(ulong Key, IReadOnlyList<Item> Values)> _equipment; 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)
@ -131,7 +96,7 @@ internal class ObjectIdentification : IObjectIdentifier
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)
@ -141,12 +106,12 @@ internal class ObjectIdentification : IObjectIdentifier
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)
@ -226,18 +191,18 @@ internal class ObjectIdentification : IObjectIdentifier
} }
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);

View 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));
}

View file

@ -99,30 +99,22 @@ public partial class MdlFile : IWritable
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
@ -131,9 +123,7 @@ public partial class MdlFile : IWritable
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)
@ -165,15 +155,11 @@ public partial class MdlFile : IWritable
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);
@ -191,9 +177,7 @@ public partial class MdlFile : IWritable
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));
} }

View 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>())
{ }
}

View file

@ -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;

View 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;
}
}

View file

@ -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();

View 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();
}