diff --git a/Penumbra.GameData/Files/MtrlFile.cs b/Penumbra.GameData/Files/MtrlFile.cs index cbefed8d..b9f46c1f 100644 --- a/Penumbra.GameData/Files/MtrlFile.cs +++ b/Penumbra.GameData/Files/MtrlFile.cs @@ -7,6 +7,7 @@ using System.Numerics; using System.Text; using Lumina.Data.Parsing; using Lumina.Extensions; +using Penumbra.GameData.Structs; namespace Penumbra.GameData.Files; @@ -28,114 +29,112 @@ public partial class MtrlFile : IWritable public Vector3 Diffuse { - get => new(ToFloat( 0 ), ToFloat( 1 ), ToFloat( 2 )); + get => new(ToFloat(0), ToFloat(1), ToFloat(2)); set { - _data[ 0 ] = FromFloat( value.X ); - _data[ 1 ] = FromFloat( value.Y ); - _data[ 2 ] = FromFloat( value.Z ); + _data[0] = FromFloat(value.X); + _data[1] = FromFloat(value.Y); + _data[2] = FromFloat(value.Z); } } public Vector3 Specular { - get => new(ToFloat( 4 ), ToFloat( 5 ), ToFloat( 6 )); + get => new(ToFloat(4), ToFloat(5), ToFloat(6)); set { - _data[ 4 ] = FromFloat( value.X ); - _data[ 5 ] = FromFloat( value.Y ); - _data[ 6 ] = FromFloat( value.Z ); + _data[4] = FromFloat(value.X); + _data[5] = FromFloat(value.Y); + _data[6] = FromFloat(value.Z); } } public Vector3 Emissive { - get => new(ToFloat( 8 ), ToFloat( 9 ), ToFloat( 10 )); + get => new(ToFloat(8), ToFloat(9), ToFloat(10)); set { - _data[ 8 ] = FromFloat( value.X ); - _data[ 9 ] = FromFloat( value.Y ); - _data[ 10 ] = FromFloat( value.Z ); + _data[8] = FromFloat(value.X); + _data[9] = FromFloat(value.Y); + _data[10] = FromFloat(value.Z); } } public Vector2 MaterialRepeat { - get => new(ToFloat( 12 ), ToFloat( 15 )); + get => new(ToFloat(12), ToFloat(15)); set { - _data[ 12 ] = FromFloat( value.X ); - _data[ 15 ] = FromFloat( value.Y ); + _data[12] = FromFloat(value.X); + _data[15] = FromFloat(value.Y); } } public Vector2 MaterialSkew { - get => new(ToFloat( 13 ), ToFloat( 14 )); + get => new(ToFloat(13), ToFloat(14)); set { - _data[ 13 ] = FromFloat( value.X ); - _data[ 14 ] = FromFloat( value.Y ); + _data[13] = FromFloat(value.X); + _data[14] = FromFloat(value.Y); } } public float SpecularStrength { - get => ToFloat( 3 ); - set => _data[ 3 ] = FromFloat( value ); + get => ToFloat(3); + set => _data[3] = FromFloat(value); } public float GlossStrength { - get => ToFloat( 7 ); - set => _data[ 7 ] = FromFloat( value ); + get => ToFloat(7); + set => _data[7] = FromFloat(value); } public ushort TileSet { - get => (ushort) (ToFloat(11) * 64f); - set => _data[ 11 ] = FromFloat(value / 64f); + get => (ushort)(ToFloat(11) * 64f); + set => _data[11] = FromFloat(value / 64f); } - private float ToFloat( int idx ) - => ( float )BitConverter.UInt16BitsToHalf( _data[ idx ] ); + private float ToFloat(int idx) + => (float)BitConverter.UInt16BitsToHalf(_data[idx]); - private static ushort FromFloat( float x ) - => BitConverter.HalfToUInt16Bits( ( Half )x ); + private static ushort FromFloat(float x) + => BitConverter.HalfToUInt16Bits((Half)x); } - public struct RowArray : IEnumerable< Row > + public struct RowArray : IEnumerable { public const int NumRows = 16; private fixed byte _rowData[NumRows * Row.Size]; - public ref Row this[ int i ] + public ref Row this[int i] { get { - fixed( byte* ptr = _rowData ) + fixed (byte* ptr = _rowData) { - return ref ( ( Row* )ptr )[ i ]; + return ref ((Row*)ptr)[i]; } } } - public IEnumerator< Row > GetEnumerator() + public IEnumerator GetEnumerator() { - for( var i = 0; i < NumRows; ++i ) - { - yield return this[ i ]; - } + for (var i = 0; i < NumRows; ++i) + yield return this[i]; } IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); - public ReadOnlySpan< byte > AsBytes() + public ReadOnlySpan AsBytes() { - fixed( byte* ptr = _rowData ) + fixed (byte* ptr = _rowData) { - return new ReadOnlySpan< byte >( ptr, NumRows * Row.Size ); + return new ReadOnlySpan(ptr, NumRows * Row.Size); } } } @@ -154,73 +153,71 @@ public partial class MtrlFile : IWritable public ushort Template { - get => ( ushort )( _data >> 5 ); - set => _data = ( ushort )( ( _data & 0x1F ) | ( value << 5 ) ); + get => (ushort)(_data >> 5); + set => _data = (ushort)((_data & 0x1F) | (value << 5)); } public bool Diffuse { - get => ( _data & 0x01 ) != 0; - set => _data = ( ushort )( value ? _data | 0x01 : _data & 0xFFFE ); + get => (_data & 0x01) != 0; + set => _data = (ushort)(value ? _data | 0x01 : _data & 0xFFFE); } public bool Specular { - get => ( _data & 0x02 ) != 0; - set => _data = ( ushort )( value ? _data | 0x02 : _data & 0xFFFD ); + get => (_data & 0x02) != 0; + set => _data = (ushort)(value ? _data | 0x02 : _data & 0xFFFD); } public bool Emissive { - get => ( _data & 0x04 ) != 0; - set => _data = ( ushort )( value ? _data | 0x04 : _data & 0xFFFB ); + get => (_data & 0x04) != 0; + set => _data = (ushort)(value ? _data | 0x04 : _data & 0xFFFB); } public bool Gloss { - get => ( _data & 0x08 ) != 0; - set => _data = ( ushort )( value ? _data | 0x08 : _data & 0xFFF7 ); + get => (_data & 0x08) != 0; + set => _data = (ushort)(value ? _data | 0x08 : _data & 0xFFF7); } public bool SpecularStrength { - get => ( _data & 0x10 ) != 0; - set => _data = ( ushort )( value ? _data | 0x10 : _data & 0xFFEF ); + get => (_data & 0x10) != 0; + set => _data = (ushort)(value ? _data | 0x10 : _data & 0xFFEF); } } - public struct RowArray : IEnumerable< Row > + public struct RowArray : IEnumerable { public const int NumRows = 16; private fixed ushort _rowData[NumRows]; - public ref Row this[ int i ] + public ref Row this[int i] { get { - fixed( ushort* ptr = _rowData ) + fixed (ushort* ptr = _rowData) { - return ref ( ( Row* )ptr )[ i ]; + return ref ((Row*)ptr)[i]; } } } - public IEnumerator< Row > GetEnumerator() + public IEnumerator GetEnumerator() { - for( var i = 0; i < NumRows; ++i ) - { - yield return this[ i ]; - } + for (var i = 0; i < NumRows; ++i) + yield return this[i]; } IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); - public ReadOnlySpan< byte > AsBytes() + public ReadOnlySpan AsBytes() { - fixed( ushort* ptr = _rowData ) + fixed (ushort* ptr = _rowData) { - return new ReadOnlySpan< byte >( ptr, NumRows * sizeof( ushort ) ); + return new ReadOnlySpan(ptr, NumRows * sizeof(ushort)); } } } @@ -262,10 +259,53 @@ public partial class MtrlFile : IWritable public ShaderPackageData ShaderPackage; public byte[] AdditionalData; - public MtrlFile( byte[] data ) + public bool ApplyDyeTemplate(StmFile stm, int colorSetIdx, int rowIdx, StainId stainId) { - using var stream = new MemoryStream( data ); - using var r = new BinaryReader( stream ); + if (colorSetIdx < 0 || colorSetIdx >= ColorDyeSets.Length || rowIdx is < 0 or >= ColorSet.RowArray.NumRows) + return false; + + var dyeSet = ColorDyeSets[colorSetIdx].Rows[rowIdx]; + if (!stm.TryGetValue(dyeSet.Template, stainId, out var dyes)) + return false; + + var ret = false; + if (dyeSet.Diffuse && ColorSets[colorSetIdx].Rows[rowIdx].Diffuse != dyes.Diffuse) + { + ColorSets[colorSetIdx].Rows[rowIdx].Diffuse = dyes.Diffuse; + ret = true; + } + + if (dyeSet.Specular && ColorSets[colorSetIdx].Rows[rowIdx].Specular != dyes.Specular) + { + ColorSets[colorSetIdx].Rows[rowIdx].Specular = dyes.Specular; + ret = true; + } + + if (dyeSet.SpecularStrength && ColorSets[colorSetIdx].Rows[rowIdx].SpecularStrength != dyes.SpecularPower) + { + ColorSets[colorSetIdx].Rows[rowIdx].SpecularStrength = dyes.SpecularPower; + ret = true; + } + + if (dyeSet.Emissive && ColorSets[colorSetIdx].Rows[rowIdx].Emissive != dyes.Emissive) + { + ColorSets[colorSetIdx].Rows[rowIdx].Emissive = dyes.Emissive; + ret = true; + } + + if (dyeSet.Gloss && ColorSets[colorSetIdx].Rows[rowIdx].GlossStrength != dyes.Gloss) + { + ColorSets[colorSetIdx].Rows[rowIdx].GlossStrength = dyes.Gloss; + ret = true; + } + + return ret; + } + + public MtrlFile(byte[] data) + { + using var stream = new MemoryStream(data); + using var r = new BinaryReader(stream); Version = r.ReadUInt32(); r.ReadUInt16(); // file size @@ -277,39 +317,37 @@ public partial class MtrlFile : IWritable var colorSetCount = r.ReadByte(); var additionalDataSize = r.ReadByte(); - Textures = ReadTextureOffsets( r, textureCount, out var textureOffsets ); - UvSets = ReadUvSetOffsets( r, uvSetCount, out var uvOffsets ); - ColorSets = ReadColorSetOffsets( r, colorSetCount, out var colorOffsets ); + Textures = ReadTextureOffsets(r, textureCount, out var textureOffsets); + UvSets = ReadUvSetOffsets(r, uvSetCount, out var uvOffsets); + ColorSets = ReadColorSetOffsets(r, colorSetCount, out var colorOffsets); - var strings = r.ReadBytes( stringTableSize ); - for( var i = 0; i < textureCount; ++i ) - { - Textures[ i ].Path = UseOffset( strings, textureOffsets[ i ] ); - } + var strings = r.ReadBytes(stringTableSize); + for (var i = 0; i < textureCount; ++i) + Textures[i].Path = UseOffset(strings, textureOffsets[i]); - for( var i = 0; i < uvSetCount; ++i ) - { - UvSets[ i ].Name = UseOffset( strings, uvOffsets[ i ] ); - } + for (var i = 0; i < uvSetCount; ++i) + UvSets[i].Name = UseOffset(strings, uvOffsets[i]); - for( var i = 0; i < colorSetCount; ++i ) - { - ColorSets[ i ].Name = UseOffset( strings, colorOffsets[ i ] ); - } + for (var i = 0; i < colorSetCount; ++i) + ColorSets[i].Name = UseOffset(strings, colorOffsets[i]); ColorDyeSets = ColorSets.Length * ColorSet.RowArray.NumRows * ColorSet.Row.Size < dataSetSize - ? ColorSets.Select( c => new ColorDyeSet { Index = c.Index, Name = c.Name } ).ToArray() - : Array.Empty< ColorDyeSet >(); - - ShaderPackage.Name = UseOffset( strings, shaderPackageNameOffset ); - - AdditionalData = r.ReadBytes( additionalDataSize ); - for( var i = 0; i < ColorSets.Length; ++i ) - { - if( stream.Position + ColorSet.RowArray.NumRows * ColorSet.Row.Size <= stream.Length ) + ? ColorSets.Select(c => new ColorDyeSet { - ColorSets[ i ].Rows = r.ReadStructure< ColorSet.RowArray >(); - ColorSets[ i ].HasRows = true; + Index = c.Index, + Name = c.Name, + }).ToArray() + : Array.Empty(); + + ShaderPackage.Name = UseOffset(strings, shaderPackageNameOffset); + + AdditionalData = r.ReadBytes(additionalDataSize); + for (var i = 0; i < ColorSets.Length; ++i) + { + if (stream.Position + ColorSet.RowArray.NumRows * ColorSet.Row.Size <= stream.Length) + { + ColorSets[i].Rows = r.ReadStructure(); + ColorSets[i].HasRows = true; } else { @@ -317,10 +355,8 @@ public partial class MtrlFile : IWritable } } - for( var i = 0; i < ColorDyeSets.Length; ++i ) - { - ColorDyeSets[ i ].Rows = r.ReadStructure< ColorDyeSet.RowArray >(); - } + for (var i = 0; i < ColorDyeSets.Length; ++i) + ColorDyeSets[i].Rows = r.ReadStructure(); var shaderValueListSize = r.ReadUInt16(); var shaderKeyCount = r.ReadUInt16(); @@ -328,55 +364,55 @@ public partial class MtrlFile : IWritable var samplerCount = r.ReadUInt16(); ShaderPackage.Flags = r.ReadUInt32(); - ShaderPackage.ShaderKeys = r.ReadStructuresAsArray< ShaderKey >( shaderKeyCount ); - ShaderPackage.Constants = r.ReadStructuresAsArray< Constant >( constantCount ); - ShaderPackage.Samplers = r.ReadStructuresAsArray< Sampler >( samplerCount ); - ShaderPackage.ShaderValues = r.ReadStructuresAsArray< float >( shaderValueListSize / 4 ); + ShaderPackage.ShaderKeys = r.ReadStructuresAsArray(shaderKeyCount); + ShaderPackage.Constants = r.ReadStructuresAsArray(constantCount); + ShaderPackage.Samplers = r.ReadStructuresAsArray(samplerCount); + ShaderPackage.ShaderValues = r.ReadStructuresAsArray(shaderValueListSize / 4); } - private static Texture[] ReadTextureOffsets( BinaryReader r, int count, out ushort[] offsets ) + private static Texture[] ReadTextureOffsets(BinaryReader r, int count, out ushort[] offsets) { var ret = new Texture[count]; offsets = new ushort[count]; - for( var i = 0; i < count; ++i ) + for (var i = 0; i < count; ++i) { - offsets[ i ] = r.ReadUInt16(); - ret[ i ].Flags = r.ReadUInt16(); + offsets[i] = r.ReadUInt16(); + ret[i].Flags = r.ReadUInt16(); } return ret; } - private static UvSet[] ReadUvSetOffsets( BinaryReader r, int count, out ushort[] offsets ) + private static UvSet[] ReadUvSetOffsets(BinaryReader r, int count, out ushort[] offsets) { var ret = new UvSet[count]; offsets = new ushort[count]; - for( var i = 0; i < count; ++i ) + for (var i = 0; i < count; ++i) { - offsets[ i ] = r.ReadUInt16(); - ret[ i ].Index = r.ReadUInt16(); + offsets[i] = r.ReadUInt16(); + ret[i].Index = r.ReadUInt16(); } return ret; } - private static ColorSet[] ReadColorSetOffsets( BinaryReader r, int count, out ushort[] offsets ) + private static ColorSet[] ReadColorSetOffsets(BinaryReader r, int count, out ushort[] offsets) { var ret = new ColorSet[count]; offsets = new ushort[count]; - for( var i = 0; i < count; ++i ) + for (var i = 0; i < count; ++i) { - offsets[ i ] = r.ReadUInt16(); - ret[ i ].Index = r.ReadUInt16(); + offsets[i] = r.ReadUInt16(); + ret[i].Index = r.ReadUInt16(); } return ret; } - private static string UseOffset( ReadOnlySpan< byte > strings, ushort offset ) + private static string UseOffset(ReadOnlySpan strings, ushort offset) { - strings = strings[ offset.. ]; - var end = strings.IndexOf( ( byte )'\0' ); - return Encoding.UTF8.GetString( strings[ ..end ] ); + strings = strings[offset..]; + var end = strings.IndexOf((byte)'\0'); + return Encoding.UTF8.GetString(strings[..end]); } -} \ No newline at end of file +} diff --git a/Penumbra.GameData/Files/StmFile.StainingTemplateEntry.cs b/Penumbra.GameData/Files/StmFile.StainingTemplateEntry.cs new file mode 100644 index 00000000..6da0ab2e --- /dev/null +++ b/Penumbra.GameData/Files/StmFile.StainingTemplateEntry.cs @@ -0,0 +1,174 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.IO; +using System.Numerics; +using Lumina.Extensions; +using Penumbra.GameData.Structs; + +namespace Penumbra.GameData.Files; + +public partial class StmFile +{ + public readonly struct StainingTemplateEntry + { + /// + /// The number of stains is capped at 128 at the moment + /// + public const int NumElements = 128; + + // ColorSet row information for each stain. + 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 GlossEntries; + public readonly IReadOnlyList SpecularPowerEntries; + + public DyePack this[StainId idx] + => this[(int)idx.Value]; + + public DyePack this[int idx] + { + get + { + // The 0th index is skipped. + if (idx is <= 0 or > NumElements) + return default; + + --idx; + var (dr, dg, db) = DiffuseEntries[idx]; + var (sr, sg, sb) = SpecularEntries[idx]; + var (er, eg, eb) = EmissiveEntries[idx]; + var g = GlossEntries[idx]; + var sp = SpecularPowerEntries[idx]; + // Convert to DyePack using floats. + return new DyePack + { + Diffuse = new Vector3((float)dr, (float)dg, (float)db), + Specular = new Vector3((float)sr, (float)sg, (float)sb), + Emissive = new Vector3((float)er, (float)eg, (float)eb), + Gloss = (float)g, + SpecularPower = (float)sp, + }; + } + } + + private static IReadOnlyList ReadArray(BinaryReader br, int offset, int size, Func read, int entrySize) + { + br.Seek(offset); + var arraySize = size / entrySize; + // The actual amount of entries informs which type of list we use. + switch (arraySize) + { + case 0: return new RepeatingList(default!, NumElements); // All default + case 1: return new RepeatingList(read(br), NumElements); // All single entry + case NumElements: // 1-to-1 entries + var ret = new T[NumElements]; + for (var i = 0; i < NumElements; ++i) + ret[i] = read(br); + return ret; + // Indexed access. + case < NumElements: return new IndexedList(br, arraySize - NumElements / entrySize, NumElements, read); + // Should not happen. + case > NumElements: throw new InvalidDataException($"Stain Template can not have more than {NumElements} elements."); + } + } + + // Read functions + private static (Half, Half, Half) ReadTriple(BinaryReader br) + => (br.ReadHalf(), br.ReadHalf(), br.ReadHalf()); + + private static Half ReadSingle(BinaryReader br) + => br.ReadHalf(); + + // Actually parse an entry. + public unsafe StainingTemplateEntry(BinaryReader br, int offset) + { + br.Seek(offset); + // 5 different lists of values. + Span ends = stackalloc ushort[5]; + for (var i = 0; i < ends.Length; ++i) + ends[i] = (ushort)(br.ReadUInt16() * 2); // because the ends are in terms of ushort. + offset += ends.Length * 2; + + DiffuseEntries = ReadArray(br, offset, ends[0], ReadTriple, 6); + SpecularEntries = ReadArray(br, offset + ends[0], ends[1] - ends[0], ReadTriple, 6); + EmissiveEntries = ReadArray(br, offset + ends[1], ends[2] - ends[1], ReadTriple, 6); + GlossEntries = ReadArray(br, offset + ends[2], ends[3] - ends[2], ReadSingle, 2); + SpecularPowerEntries = ReadArray(br, offset + ends[3], ends[4] - ends[3], ReadSingle, 2); + } + + /// + /// Used if a single value is used for all entries of a list. + /// + private sealed 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(); + } + + /// + /// Used if there is a small set of values for a bigger list, accessed via index information. + /// + private sealed class IndexedList : IReadOnlyList + { + private readonly T[] _values; + private readonly byte[] _indices; + + /// + /// Reads values from via , then reads byte indices. + /// + public IndexedList(BinaryReader br, int count, int indexCount, Func read) + { + _values = new T[count + 1]; + _indices = new byte[indexCount]; + _values[0] = default!; + for (var i = 1; i < count + 1; ++i) + _values[i] = read(br); + + // Seems to be an unused 0xFF byte marker. + // Necessary for correct offsets. + br.ReadByte(); + 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]] : default!; + } + } +} diff --git a/Penumbra.GameData/Files/StmFile.cs b/Penumbra.GameData/Files/StmFile.cs index 288c8600..7ee4f0d3 100644 --- a/Penumbra.GameData/Files/StmFile.cs +++ b/Penumbra.GameData/Files/StmFile.cs @@ -1,10 +1,8 @@ using System; -using System.Collections; using System.Collections.Generic; using System.IO; using System.Numerics; using Dalamud.Data; -using Lumina.Extensions; using Penumbra.GameData.Structs; namespace Penumbra.GameData.Files; @@ -13,160 +11,58 @@ public partial class StmFile { public const string Path = "chara/base_material/stainingtemplate.stm"; + /// + /// All dye-able color set information for a row. + /// public record struct DyePack { public Vector3 Diffuse; public Vector3 Specular; public Vector3 Emissive; - public float SpecularPower; public float Gloss; + public float SpecularPower; } - 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; - - public DyePack this[StainId idx] - => this[(int)idx.Value]; - - public DyePack this[int idx] - { - get - { - if (idx is <= 0 or > NumElements) - return default; - - --idx; - 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 = new Vector3((float)dr, (float)dg, (float)db), - Emissive = new Vector3((float)sr, (float)sg, (float)sb), - Specular = new Vector3((float)er, (float)eg, (float)eb), - 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); - } - } - + /// + /// All currently available dyeing templates with their IDs. + /// public readonly IReadOnlyDictionary Entries; + /// + /// Access a specific dye pack. + /// + /// The ID of the accessed template. + /// The ID of the Stain. + /// The corresponding color set information or a defaulted DyePack of 0-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]; + /// + /// Try to access a specific dye pack. + /// + /// The ID of the accessed template. + /// The ID of the Stain. + /// On success, the corresponding color set information, otherwise a defaulted DyePack. + /// True on success, false otherwise. + public bool TryGetValue(ushort template, StainId idx, out DyePack dyes) + { + if (idx.Value is > 0 and <= StainingTemplateEntry.NumElements && Entries.TryGetValue(template, out var entry)) + { + dyes = entry[idx]; + return true; + } + + dyes = default; + return false; + } + + /// + /// Create a STM file from the given data array. + /// public StmFile(byte[] data) { using var stream = new MemoryStream(data); @@ -193,6 +89,9 @@ public partial class StmFile } } + /// + /// Try to read and parse the default STM file given by Lumina. + /// public StmFile(DataManager gameData) : this(gameData.GetFile(Path)?.Data ?? Array.Empty()) { } diff --git a/Penumbra/UI/Classes/ModEditWindow.FileEditor.cs b/Penumbra/UI/Classes/ModEditWindow.FileEditor.cs new file mode 100644 index 00000000..a4042065 --- /dev/null +++ b/Penumbra/UI/Classes/ModEditWindow.FileEditor.cs @@ -0,0 +1,252 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Numerics; +using Dalamud.Interface; +using Dalamud.Interface.ImGuiFileDialog; +using ImGuiNET; +using OtterGui; +using OtterGui.Raii; +using Penumbra.GameData.Files; +using Penumbra.Mods; +using Penumbra.String.Classes; + +namespace Penumbra.UI.Classes; + +public partial class ModEditWindow +{ + private class FileEditor< T > where T : class, IWritable + { + private readonly string _tabName; + private readonly string _fileType; + private readonly Func< IReadOnlyList< Mod.Editor.FileRegistry > > _getFiles; + private readonly Func< T, bool, bool > _drawEdit; + private readonly Func< string > _getInitialPath; + + private Mod.Editor.FileRegistry? _currentPath; + private T? _currentFile; + private Exception? _currentException; + private bool _changed; + + private string _defaultPath = string.Empty; + private bool _inInput; + private T? _defaultFile; + private Exception? _defaultException; + + private IReadOnlyList< Mod.Editor.FileRegistry > _list = null!; + + private readonly FileDialogManager _fileDialog = ConfigWindow.SetupFileManager(); + + public FileEditor( string tabName, string fileType, Func< IReadOnlyList< Mod.Editor.FileRegistry > > getFiles, + Func< T, bool, bool > drawEdit, Func< string > getInitialPath ) + { + _tabName = tabName; + _fileType = fileType; + _getFiles = getFiles; + _drawEdit = drawEdit; + _getInitialPath = getInitialPath; + } + + public void Draw() + { + _list = _getFiles(); + if( _list.Count == 0 ) + { + return; + } + + using var tab = ImRaii.TabItem( _tabName ); + if( !tab ) + { + return; + } + + ImGui.NewLine(); + DrawFileSelectCombo(); + SaveButton(); + ImGui.SameLine(); + ResetButton(); + ImGui.SameLine(); + DefaultInput(); + ImGui.Dummy( new Vector2( ImGui.GetTextLineHeight() / 2 ) ); + + DrawFilePanel(); + } + + private void DefaultInput() + { + using var spacing = ImRaii.PushStyle( ImGuiStyleVar.ItemSpacing, ImGui.GetStyle().ItemSpacing with { X = 3 * ImGuiHelpers.GlobalScale } ); + ImGui.SetNextItemWidth( ImGui.GetContentRegionAvail().X - 3 * ImGuiHelpers.GlobalScale - ImGui.GetFrameHeight() ); + ImGui.InputTextWithHint( "##defaultInput", "Input game path to compare...", ref _defaultPath, Utf8GamePath.MaxGamePathLength ); + _inInput = ImGui.IsItemActive(); + if( ImGui.IsItemDeactivatedAfterEdit() && _defaultPath.Length > 0 ) + { + _fileDialog.Reset(); + try + { + var file = Dalamud.GameData.GetFile( _defaultPath ); + if( file != null ) + { + _defaultException = null; + _defaultFile = Activator.CreateInstance( typeof( T ), file.Data ) as T; + } + else + { + _defaultFile = null; + _defaultException = new Exception( "File does not exist." ); + } + } + catch( Exception e ) + { + _defaultFile = null; + _defaultException = e; + } + } + + ImGui.SameLine(); + if( ImGuiUtil.DrawDisabledButton( FontAwesomeIcon.Save.ToIconString(), new Vector2( ImGui.GetFrameHeight() ), "Export this file.", _defaultFile == null, true ) ) + { + _fileDialog.SaveFileDialog( $"Export {_defaultPath} to...", _fileType, Path.GetFileNameWithoutExtension( _defaultPath ), _fileType, ( success, name ) => + { + if( !success ) + { + return; + } + + try + { + File.WriteAllBytes( name, _defaultFile?.Write() ?? throw new Exception( "File invalid." ) ); + } + catch( Exception e ) + { + Penumbra.Log.Error( $"Could not export {_defaultPath}:\n{e}" ); + } + }, _getInitialPath() ); + } + + _fileDialog.Draw(); + } + + public void Reset() + { + _currentException = null; + _currentPath = null; + _currentFile = null; + _changed = false; + } + + private void DrawFileSelectCombo() + { + ImGui.SetNextItemWidth( ImGui.GetContentRegionAvail().X ); + using var combo = ImRaii.Combo( "##fileSelect", _currentPath?.RelPath.ToString() ?? $"Select {_fileType} File..." ); + if( !combo ) + { + return; + } + + foreach( var file in _list ) + { + if( ImGui.Selectable( file.RelPath.ToString(), ReferenceEquals( file, _currentPath ) ) ) + { + UpdateCurrentFile( file ); + } + } + } + + private void UpdateCurrentFile( Mod.Editor.FileRegistry path ) + { + if( ReferenceEquals( _currentPath, path ) ) + { + return; + } + + _changed = false; + _currentPath = path; + _currentException = null; + try + { + var bytes = File.ReadAllBytes( _currentPath.File.FullName ); + _currentFile = Activator.CreateInstance( typeof( T ), bytes ) as T; + } + catch( Exception e ) + { + _currentFile = null; + _currentException = e; + } + } + + private void SaveButton() + { + if( ImGuiUtil.DrawDisabledButton( "Save to File", Vector2.Zero, + $"Save the selected {_fileType} file with all changes applied. This is not revertible.", !_changed ) ) + { + File.WriteAllBytes( _currentPath!.File.FullName, _currentFile!.Write() ); + _changed = false; + } + } + + private void ResetButton() + { + if( ImGuiUtil.DrawDisabledButton( "Reset Changes", Vector2.Zero, + $"Reset all changes made to the {_fileType} file.", !_changed ) ) + { + var tmp = _currentPath; + _currentPath = null; + UpdateCurrentFile( tmp! ); + } + } + + private void DrawFilePanel() + { + using var child = ImRaii.Child( "##filePanel", -Vector2.One, true ); + if( !child ) + { + return; + } + + if( _currentPath != null ) + { + if( _currentFile == null ) + { + ImGui.TextUnformatted( $"Could not parse selected {_fileType} file." ); + if( _currentException != null ) + { + using var tab = ImRaii.PushIndent(); + ImGuiUtil.TextWrapped( _currentException.ToString() ); + } + } + else + { + using var id = ImRaii.PushId( 0 ); + _changed |= _drawEdit( _currentFile, false ); + } + } + + if( !_inInput && _defaultPath.Length > 0 ) + { + if( _currentPath != null ) + { + ImGui.NewLine(); + ImGui.NewLine(); + ImGui.TextUnformatted( $"Preview of {_defaultPath}:" ); + ImGui.Separator(); + } + + if( _defaultFile == null ) + { + ImGui.TextUnformatted( $"Could not parse provided {_fileType} game file:\n" ); + if( _defaultException != null ) + { + using var tab = ImRaii.PushIndent(); + ImGuiUtil.TextWrapped( _defaultException.ToString() ); + } + } + else + { + using var id = ImRaii.PushId( 1 ); + _drawEdit( _defaultFile, true ); + } + } + } + } +} \ No newline at end of file diff --git a/Penumbra/UI/Classes/ModEditWindow.FileEdit.cs b/Penumbra/UI/Classes/ModEditWindow.Materials.cs similarity index 68% rename from Penumbra/UI/Classes/ModEditWindow.FileEdit.cs rename to Penumbra/UI/Classes/ModEditWindow.Materials.cs index 3f6ba35b..c59f3f6d 100644 --- a/Penumbra/UI/Classes/ModEditWindow.FileEdit.cs +++ b/Penumbra/UI/Classes/ModEditWindow.Materials.cs @@ -1,20 +1,13 @@ using System; -using System.Collections.Generic; using System.Globalization; -using System.IO; using System.Linq; using System.Numerics; using System.Runtime.InteropServices; using Dalamud.Interface; -using Dalamud.Interface.ImGuiFileDialog; using ImGuiNET; -using Lumina.Data.Parsing.Layer; using OtterGui; using OtterGui.Raii; -using OtterGui.Widgets; using Penumbra.GameData.Files; -using Penumbra.GameData.Structs; -using Penumbra.Mods; using Penumbra.String.Classes; using Penumbra.String.Functions; @@ -23,263 +16,6 @@ namespace Penumbra.UI.Classes; public partial class ModEditWindow { private readonly FileEditor< MtrlFile > _materialTab; - private readonly FileEditor< MdlFile > _modelTab; - - private class FileEditor< T > where T : class, IWritable - { - private readonly string _tabName; - private readonly string _fileType; - private readonly Func< IReadOnlyList< Mod.Editor.FileRegistry > > _getFiles; - private readonly Func< T, bool, bool > _drawEdit; - private readonly Func< string > _getInitialPath; - - private Mod.Editor.FileRegistry? _currentPath; - private T? _currentFile; - private Exception? _currentException; - private bool _changed; - - private string _defaultPath = string.Empty; - private bool _inInput; - private T? _defaultFile; - private Exception? _defaultException; - - private IReadOnlyList< Mod.Editor.FileRegistry > _list = null!; - - private readonly FileDialogManager _fileDialog = ConfigWindow.SetupFileManager(); - - public FileEditor( string tabName, string fileType, Func< IReadOnlyList< Mod.Editor.FileRegistry > > getFiles, - Func< T, bool, bool > drawEdit, Func< string > getInitialPath ) - { - _tabName = tabName; - _fileType = fileType; - _getFiles = getFiles; - _drawEdit = drawEdit; - _getInitialPath = getInitialPath; - } - - public void Draw() - { - _list = _getFiles(); - if( _list.Count == 0 ) - { - return; - } - - using var tab = ImRaii.TabItem( _tabName ); - if( !tab ) - { - return; - } - - ImGui.NewLine(); - DrawFileSelectCombo(); - SaveButton(); - ImGui.SameLine(); - ResetButton(); - ImGui.SameLine(); - DefaultInput(); - ImGui.Dummy( new Vector2( ImGui.GetTextLineHeight() / 2 ) ); - - DrawFilePanel(); - } - - private void DefaultInput() - { - using var spacing = ImRaii.PushStyle( ImGuiStyleVar.ItemSpacing, ImGui.GetStyle().ItemSpacing with { X = 3 * ImGuiHelpers.GlobalScale } ); - ImGui.SetNextItemWidth( ImGui.GetContentRegionAvail().X - 3 * ImGuiHelpers.GlobalScale - ImGui.GetFrameHeight() ); - ImGui.InputTextWithHint( "##defaultInput", "Input game path to compare...", ref _defaultPath, Utf8GamePath.MaxGamePathLength ); - _inInput = ImGui.IsItemActive(); - if( ImGui.IsItemDeactivatedAfterEdit() && _defaultPath.Length > 0 ) - { - _fileDialog.Reset(); - try - { - var file = Dalamud.GameData.GetFile( _defaultPath ); - if( file != null ) - { - _defaultException = null; - _defaultFile = Activator.CreateInstance( typeof( T ), file.Data ) as T; - } - else - { - _defaultFile = null; - _defaultException = new Exception( "File does not exist." ); - } - } - catch( Exception e ) - { - _defaultFile = null; - _defaultException = e; - } - } - - ImGui.SameLine(); - if( ImGuiUtil.DrawDisabledButton( FontAwesomeIcon.Save.ToIconString(), new Vector2( ImGui.GetFrameHeight() ), "Export this file.", _defaultFile == null, true ) ) - { - _fileDialog.SaveFileDialog( $"Export {_defaultPath} to...", _fileType, Path.GetFileNameWithoutExtension( _defaultPath ), _fileType, ( success, name ) => - { - if( !success ) - { - return; - } - - try - { - File.WriteAllBytes( name, _defaultFile?.Write() ?? throw new Exception( "File invalid." ) ); - } - catch( Exception e ) - { - Penumbra.Log.Error( $"Could not export {_defaultPath}:\n{e}" ); - } - }, _getInitialPath() ); - } - - _fileDialog.Draw(); - } - - public void Reset() - { - _currentException = null; - _currentPath = null; - _currentFile = null; - _changed = false; - } - - private void DrawFileSelectCombo() - { - ImGui.SetNextItemWidth( ImGui.GetContentRegionAvail().X ); - using var combo = ImRaii.Combo( "##fileSelect", _currentPath?.RelPath.ToString() ?? $"Select {_fileType} File..." ); - if( !combo ) - { - return; - } - - foreach( var file in _list ) - { - if( ImGui.Selectable( file.RelPath.ToString(), ReferenceEquals( file, _currentPath ) ) ) - { - UpdateCurrentFile( file ); - } - } - } - - private void UpdateCurrentFile( Mod.Editor.FileRegistry path ) - { - if( ReferenceEquals( _currentPath, path ) ) - { - return; - } - - _changed = false; - _currentPath = path; - _currentException = null; - try - { - var bytes = File.ReadAllBytes( _currentPath.File.FullName ); - _currentFile = Activator.CreateInstance( typeof( T ), bytes ) as T; - } - catch( Exception e ) - { - _currentFile = null; - _currentException = e; - } - } - - private void SaveButton() - { - if( ImGuiUtil.DrawDisabledButton( "Save to File", Vector2.Zero, - $"Save the selected {_fileType} file with all changes applied. This is not revertible.", !_changed ) ) - { - File.WriteAllBytes( _currentPath!.File.FullName, _currentFile!.Write() ); - _changed = false; - } - } - - private void ResetButton() - { - if( ImGuiUtil.DrawDisabledButton( "Reset Changes", Vector2.Zero, - $"Reset all changes made to the {_fileType} file.", !_changed ) ) - { - var tmp = _currentPath; - _currentPath = null; - UpdateCurrentFile( tmp! ); - } - } - - private void DrawFilePanel() - { - using var child = ImRaii.Child( "##filePanel", -Vector2.One, true ); - if( !child ) - { - return; - } - - if( _currentPath != null ) - { - if( _currentFile == null ) - { - ImGui.TextUnformatted( $"Could not parse selected {_fileType} file." ); - if( _currentException != null ) - { - using var tab = ImRaii.PushIndent(); - ImGuiUtil.TextWrapped( _currentException.ToString() ); - } - } - else - { - using var id = ImRaii.PushId( 0 ); - _changed |= _drawEdit( _currentFile, false ); - } - } - - if( !_inInput && _defaultPath.Length > 0 ) - { - if( _currentPath != null ) - { - ImGui.NewLine(); - ImGui.NewLine(); - ImGui.TextUnformatted( $"Preview of {_defaultPath}:" ); - ImGui.Separator(); - } - - if( _defaultFile == null ) - { - ImGui.TextUnformatted( $"Could not parse provided {_fileType} game file:\n" ); - if( _defaultException != null ) - { - using var tab = ImRaii.PushIndent(); - ImGuiUtil.TextWrapped( _defaultException.ToString() ); - } - } - else - { - using var id = ImRaii.PushId( 1 ); - _drawEdit( _defaultFile, true ); - } - } - } - } - - private static bool DrawModelPanel( MdlFile file, bool disabled ) - { - var ret = false; - for( var i = 0; i < file.Materials.Length; ++i ) - { - using var id = ImRaii.PushId( i ); - var tmp = file.Materials[ i ]; - if( ImGui.InputText( string.Empty, ref tmp, Utf8GamePath.MaxGamePathLength, - disabled ? ImGuiInputTextFlags.ReadOnly : ImGuiInputTextFlags.None ) - && tmp.Length > 0 - && tmp != file.Materials[ i ] ) - { - file.Materials[ i ] = tmp; - ret = true; - } - } - - return !disabled && ret; - } - private static bool DrawMaterialPanel( MtrlFile file, bool disabled ) { @@ -330,11 +66,9 @@ public partial class ModEditWindow ImGui.SameLine(); var ret = ColorSetPasteAllClipboardButton( file, 0 ); ImGui.SameLine(); - ImGui.Dummy( ImGuiHelpers.ScaledVector2( 10, 0 ) ); + ImGui.Dummy( ImGuiHelpers.ScaledVector2( 20, 0 ) ); ImGui.SameLine(); - Penumbra.StainManager.StainCombo.Draw( "Preview Dye", Penumbra.StainManager.StainCombo.CurrentSelection.Value.Color, true ); - ImGui.SameLine(); - ImGui.Button( "Apply Preview Dyes." ); + ret |= DrawPreviewDye( file, disabled ); using var table = ImRaii.Table( "##ColorSets", 11, ImGuiTableFlags.SizingFixedFit | ImGuiTableFlags.RowBg | ImGuiTableFlags.BordersInnerV ); @@ -503,6 +237,30 @@ public partial class ModEditWindow } } + private static bool DrawPreviewDye( MtrlFile file, bool disabled ) + { + var (dyeId, (name, dyeColor, _)) = Penumbra.StainManager.StainCombo.CurrentSelection; + var tt = dyeId == 0 ? "Select a preview dye first." : "Apply all preview values corresponding to the dye template and chosen dye where dyeing is enabled."; + if( ImGuiUtil.DrawDisabledButton( "Apply Preview Dye", Vector2.Zero, tt, disabled || dyeId == 0 ) ) + { + var ret = false; + for( var j = 0; j < file.ColorDyeSets.Length; ++j ) + { + for( var i = 0; i < MtrlFile.ColorSet.RowArray.NumRows; ++i ) + { + ret |= file.ApplyDyeTemplate( Penumbra.StainManager.StmFile, j, i, dyeId ); + } + } + + return ret; + } + + ImGui.SameLine(); + var label = dyeId == 0 ? "Preview Dye###previewDye" : $"{name} (Preview)###previewDye"; + Penumbra.StainManager.StainCombo.Draw( label, dyeColor, true ); + return false; + } + private static unsafe bool ColorSetPasteAllClipboardButton( MtrlFile file, int colorSetIdx ) { if( !ImGui.Button( "Import All Rows from Clipboard", ImGuiHelpers.ScaledVector2( 200, 0 ) ) || file.ColorSets.Length <= colorSetIdx ) @@ -744,7 +502,7 @@ public partial class ModEditWindow if( hasDye ) { if( Penumbra.StainManager.TemplateCombo.Draw( "##dyeTemplate", dye.Template.ToString(), intSize - + ImGui.GetStyle().ScrollbarSize / 2, ImGui.GetTextLineHeightWithSpacing(), ImGuiComboFlags.NoArrowButton ) ) + + ImGui.GetStyle().ScrollbarSize / 2, ImGui.GetTextLineHeightWithSpacing(), ImGuiComboFlags.NoArrowButton ) ) { file.ColorDyeSets[ colorSetIdx ].Rows[ rowIdx ].Template = Penumbra.StainManager.TemplateCombo.CurrentSelection; ret = true; @@ -753,23 +511,7 @@ public partial class ModEditWindow ImGuiUtil.HoverTooltip( "Dye Template", ImGuiHoveredFlags.AllowWhenDisabled ); ImGui.TableNextColumn(); - var stain = Penumbra.StainManager.StainCombo.CurrentSelection.Key; - if( stain != 0 && Penumbra.StainManager.StmFile.Entries.TryGetValue( dye.Template, out var entry ) ) - { - var values = entry[ ( int )stain ]; - using var _ = ImRaii.Disabled(); - ColorPicker( "##diffusePreview", string.Empty, values.Diffuse, c => { } ); - ImGui.SameLine(); - ColorPicker( "##specularPreview", string.Empty, values.Specular, c => { } ); - ImGui.SameLine(); - ColorPicker( "##emissivePreview", string.Empty, values.Emissive, c => { } ); - ImGui.SameLine(); - ImGui.SetNextItemWidth( floatSize ); - ImGui.DragFloat( "##specularStrength", ref values.SpecularPower ); - ImGui.SameLine(); - ImGui.SetNextItemWidth( floatSize ); - ImGui.DragFloat( "##gloss", ref values.Gloss ); - } + ret |= DrawDyePreview( file, colorSetIdx, rowIdx, disabled, dye, floatSize ); } else { @@ -780,7 +522,40 @@ public partial class ModEditWindow return ret; } - private static bool ColorPicker( string label, string tooltip, Vector3 input, Action< Vector3 > setter ) + private static bool DrawDyePreview( MtrlFile file, int colorSetIdx, int rowIdx, bool disabled, MtrlFile.ColorDyeSet.Row dye, float floatSize ) + { + var stain = Penumbra.StainManager.StainCombo.CurrentSelection.Key; + if( stain == 0 || !Penumbra.StainManager.StmFile.Entries.TryGetValue( dye.Template, out var entry ) ) + { + return false; + } + + var values = entry[ ( int )stain ]; + using var style = ImRaii.PushStyle( ImGuiStyleVar.ItemSpacing, ImGui.GetStyle().ItemSpacing / 2 ); + + var ret = ImGuiUtil.DrawDisabledButton( FontAwesomeIcon.PaintBrush.ToIconString(), new Vector2( ImGui.GetFrameHeight() ), + "Apply the selected dye to this row.", disabled, true ); + + ret = ret && file.ApplyDyeTemplate( Penumbra.StainManager.StmFile, colorSetIdx, rowIdx, stain ); + + ImGui.SameLine(); + ColorPicker( "##diffusePreview", string.Empty, values.Diffuse, _ => { }, "D" ); + ImGui.SameLine(); + ColorPicker( "##specularPreview", string.Empty, values.Specular, _ => { }, "S" ); + ImGui.SameLine(); + ColorPicker( "##emissivePreview", string.Empty, values.Emissive, _ => { }, "E" ); + ImGui.SameLine(); + using var dis = ImRaii.Disabled(); + ImGui.SetNextItemWidth( floatSize ); + ImGui.DragFloat( "##gloss", ref values.Gloss, 0, 0, 0, "%.2f G" ); + ImGui.SameLine(); + ImGui.SetNextItemWidth( floatSize ); + ImGui.DragFloat( "##specularStrength", ref values.SpecularPower, 0, 0, 0, "%.2f S" ); + + return ret; + } + + private static bool ColorPicker( string label, string tooltip, Vector3 input, Action< Vector3 > setter, string letter = "" ) { var ret = false; var tmp = input; @@ -792,6 +567,14 @@ public partial class ModEditWindow ret = true; } + if( letter.Length > 0 && ImGui.IsItemVisible() ) + { + var textSize = ImGui.CalcTextSize( letter ); + var center = ImGui.GetItemRectMin() + ( ImGui.GetItemRectSize() - textSize ) / 2; + var textColor = input.LengthSquared() < 0.25f ? 0x80FFFFFFu : 0x80000000u; + ImGui.GetWindowDrawList().AddText( center, textColor, letter ); + } + ImGuiUtil.HoverTooltip( tooltip, ImGuiHoveredFlags.AllowWhenDisabled ); return ret; diff --git a/Penumbra/UI/Classes/ModEditWindow.Models.cs b/Penumbra/UI/Classes/ModEditWindow.Models.cs new file mode 100644 index 00000000..5b6ad685 --- /dev/null +++ b/Penumbra/UI/Classes/ModEditWindow.Models.cs @@ -0,0 +1,31 @@ +using ImGuiNET; +using OtterGui.Raii; +using Penumbra.GameData.Files; +using Penumbra.String.Classes; + +namespace Penumbra.UI.Classes; + +public partial class ModEditWindow +{ + private readonly FileEditor< MdlFile > _modelTab; + + private static bool DrawModelPanel( MdlFile file, bool disabled ) + { + var ret = false; + for( var i = 0; i < file.Materials.Length; ++i ) + { + using var id = ImRaii.PushId( i ); + var tmp = file.Materials[ i ]; + if( ImGui.InputText( string.Empty, ref tmp, Utf8GamePath.MaxGamePathLength, + disabled ? ImGuiInputTextFlags.ReadOnly : ImGuiInputTextFlags.None ) + && tmp.Length > 0 + && tmp != file.Materials[ i ] ) + { + file.Materials[ i ] = tmp; + ret = true; + } + } + + return !disabled && ret; + } +} \ No newline at end of file diff --git a/Penumbra/UI/Classes/ModEditWindow.cs b/Penumbra/UI/Classes/ModEditWindow.cs index 83d58be4..bbe21025 100644 --- a/Penumbra/UI/Classes/ModEditWindow.cs +++ b/Penumbra/UI/Classes/ModEditWindow.cs @@ -38,7 +38,7 @@ public partial class ModEditWindow : Window, IDisposable SizeConstraints = new WindowSizeConstraints { - MinimumSize = ImGuiHelpers.ScaledVector2( 1000, 600 ), + MinimumSize = new Vector2( 1240, 600 ), MaximumSize = 4000 * Vector2.One, }; _selectedFiles.Clear();