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