mirror of
https://github.com/xivdev/Penumbra.git
synced 2025-12-12 18:27:24 +01:00
Finish work on dye previews.
This commit is contained in:
parent
4df8f720f5
commit
17a8e06c1d
7 changed files with 722 additions and 547 deletions
|
|
@ -7,6 +7,7 @@ using System.Numerics;
|
||||||
using System.Text;
|
using System.Text;
|
||||||
using Lumina.Data.Parsing;
|
using Lumina.Data.Parsing;
|
||||||
using Lumina.Extensions;
|
using Lumina.Extensions;
|
||||||
|
using Penumbra.GameData.Structs;
|
||||||
|
|
||||||
namespace Penumbra.GameData.Files;
|
namespace Penumbra.GameData.Files;
|
||||||
|
|
||||||
|
|
@ -28,114 +29,112 @@ public partial class MtrlFile : IWritable
|
||||||
|
|
||||||
public Vector3 Diffuse
|
public Vector3 Diffuse
|
||||||
{
|
{
|
||||||
get => new(ToFloat( 0 ), ToFloat( 1 ), ToFloat( 2 ));
|
get => new(ToFloat(0), ToFloat(1), ToFloat(2));
|
||||||
set
|
set
|
||||||
{
|
{
|
||||||
_data[ 0 ] = FromFloat( value.X );
|
_data[0] = FromFloat(value.X);
|
||||||
_data[ 1 ] = FromFloat( value.Y );
|
_data[1] = FromFloat(value.Y);
|
||||||
_data[ 2 ] = FromFloat( value.Z );
|
_data[2] = FromFloat(value.Z);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public Vector3 Specular
|
public Vector3 Specular
|
||||||
{
|
{
|
||||||
get => new(ToFloat( 4 ), ToFloat( 5 ), ToFloat( 6 ));
|
get => new(ToFloat(4), ToFloat(5), ToFloat(6));
|
||||||
set
|
set
|
||||||
{
|
{
|
||||||
_data[ 4 ] = FromFloat( value.X );
|
_data[4] = FromFloat(value.X);
|
||||||
_data[ 5 ] = FromFloat( value.Y );
|
_data[5] = FromFloat(value.Y);
|
||||||
_data[ 6 ] = FromFloat( value.Z );
|
_data[6] = FromFloat(value.Z);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public Vector3 Emissive
|
public Vector3 Emissive
|
||||||
{
|
{
|
||||||
get => new(ToFloat( 8 ), ToFloat( 9 ), ToFloat( 10 ));
|
get => new(ToFloat(8), ToFloat(9), ToFloat(10));
|
||||||
set
|
set
|
||||||
{
|
{
|
||||||
_data[ 8 ] = FromFloat( value.X );
|
_data[8] = FromFloat(value.X);
|
||||||
_data[ 9 ] = FromFloat( value.Y );
|
_data[9] = FromFloat(value.Y);
|
||||||
_data[ 10 ] = FromFloat( value.Z );
|
_data[10] = FromFloat(value.Z);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public Vector2 MaterialRepeat
|
public Vector2 MaterialRepeat
|
||||||
{
|
{
|
||||||
get => new(ToFloat( 12 ), ToFloat( 15 ));
|
get => new(ToFloat(12), ToFloat(15));
|
||||||
set
|
set
|
||||||
{
|
{
|
||||||
_data[ 12 ] = FromFloat( value.X );
|
_data[12] = FromFloat(value.X);
|
||||||
_data[ 15 ] = FromFloat( value.Y );
|
_data[15] = FromFloat(value.Y);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public Vector2 MaterialSkew
|
public Vector2 MaterialSkew
|
||||||
{
|
{
|
||||||
get => new(ToFloat( 13 ), ToFloat( 14 ));
|
get => new(ToFloat(13), ToFloat(14));
|
||||||
set
|
set
|
||||||
{
|
{
|
||||||
_data[ 13 ] = FromFloat( value.X );
|
_data[13] = FromFloat(value.X);
|
||||||
_data[ 14 ] = FromFloat( value.Y );
|
_data[14] = FromFloat(value.Y);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public float SpecularStrength
|
public float SpecularStrength
|
||||||
{
|
{
|
||||||
get => ToFloat( 3 );
|
get => ToFloat(3);
|
||||||
set => _data[ 3 ] = FromFloat( value );
|
set => _data[3] = FromFloat(value);
|
||||||
}
|
}
|
||||||
|
|
||||||
public float GlossStrength
|
public float GlossStrength
|
||||||
{
|
{
|
||||||
get => ToFloat( 7 );
|
get => ToFloat(7);
|
||||||
set => _data[ 7 ] = FromFloat( value );
|
set => _data[7] = FromFloat(value);
|
||||||
}
|
}
|
||||||
|
|
||||||
public ushort TileSet
|
public ushort TileSet
|
||||||
{
|
{
|
||||||
get => (ushort) (ToFloat(11) * 64f);
|
get => (ushort)(ToFloat(11) * 64f);
|
||||||
set => _data[ 11 ] = FromFloat(value / 64f);
|
set => _data[11] = FromFloat(value / 64f);
|
||||||
}
|
}
|
||||||
|
|
||||||
private float ToFloat( int idx )
|
private float ToFloat(int idx)
|
||||||
=> ( float )BitConverter.UInt16BitsToHalf( _data[ idx ] );
|
=> (float)BitConverter.UInt16BitsToHalf(_data[idx]);
|
||||||
|
|
||||||
private static ushort FromFloat( float x )
|
private static ushort FromFloat(float x)
|
||||||
=> BitConverter.HalfToUInt16Bits( ( Half )x );
|
=> BitConverter.HalfToUInt16Bits((Half)x);
|
||||||
}
|
}
|
||||||
|
|
||||||
public struct RowArray : IEnumerable< Row >
|
public struct RowArray : IEnumerable<Row>
|
||||||
{
|
{
|
||||||
public const int NumRows = 16;
|
public const int NumRows = 16;
|
||||||
private fixed byte _rowData[NumRows * Row.Size];
|
private fixed byte _rowData[NumRows * Row.Size];
|
||||||
|
|
||||||
public ref Row this[ int i ]
|
public ref Row this[int i]
|
||||||
{
|
{
|
||||||
get
|
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<Row> GetEnumerator()
|
||||||
{
|
{
|
||||||
for( var i = 0; i < NumRows; ++i )
|
for (var i = 0; i < NumRows; ++i)
|
||||||
{
|
yield return this[i];
|
||||||
yield return this[ i ];
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
IEnumerator IEnumerable.GetEnumerator()
|
IEnumerator IEnumerable.GetEnumerator()
|
||||||
=> GetEnumerator();
|
=> GetEnumerator();
|
||||||
|
|
||||||
public ReadOnlySpan< byte > AsBytes()
|
public ReadOnlySpan<byte> AsBytes()
|
||||||
{
|
{
|
||||||
fixed( byte* ptr = _rowData )
|
fixed (byte* ptr = _rowData)
|
||||||
{
|
{
|
||||||
return new ReadOnlySpan< byte >( ptr, NumRows * Row.Size );
|
return new ReadOnlySpan<byte>(ptr, NumRows * Row.Size);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -154,73 +153,71 @@ public partial class MtrlFile : IWritable
|
||||||
|
|
||||||
public ushort Template
|
public ushort Template
|
||||||
{
|
{
|
||||||
get => ( ushort )( _data >> 5 );
|
get => (ushort)(_data >> 5);
|
||||||
set => _data = ( ushort )( ( _data & 0x1F ) | ( value << 5 ) );
|
set => _data = (ushort)((_data & 0x1F) | (value << 5));
|
||||||
}
|
}
|
||||||
|
|
||||||
public bool Diffuse
|
public bool Diffuse
|
||||||
{
|
{
|
||||||
get => ( _data & 0x01 ) != 0;
|
get => (_data & 0x01) != 0;
|
||||||
set => _data = ( ushort )( value ? _data | 0x01 : _data & 0xFFFE );
|
set => _data = (ushort)(value ? _data | 0x01 : _data & 0xFFFE);
|
||||||
}
|
}
|
||||||
|
|
||||||
public bool Specular
|
public bool Specular
|
||||||
{
|
{
|
||||||
get => ( _data & 0x02 ) != 0;
|
get => (_data & 0x02) != 0;
|
||||||
set => _data = ( ushort )( value ? _data | 0x02 : _data & 0xFFFD );
|
set => _data = (ushort)(value ? _data | 0x02 : _data & 0xFFFD);
|
||||||
}
|
}
|
||||||
|
|
||||||
public bool Emissive
|
public bool Emissive
|
||||||
{
|
{
|
||||||
get => ( _data & 0x04 ) != 0;
|
get => (_data & 0x04) != 0;
|
||||||
set => _data = ( ushort )( value ? _data | 0x04 : _data & 0xFFFB );
|
set => _data = (ushort)(value ? _data | 0x04 : _data & 0xFFFB);
|
||||||
}
|
}
|
||||||
|
|
||||||
public bool Gloss
|
public bool Gloss
|
||||||
{
|
{
|
||||||
get => ( _data & 0x08 ) != 0;
|
get => (_data & 0x08) != 0;
|
||||||
set => _data = ( ushort )( value ? _data | 0x08 : _data & 0xFFF7 );
|
set => _data = (ushort)(value ? _data | 0x08 : _data & 0xFFF7);
|
||||||
}
|
}
|
||||||
|
|
||||||
public bool SpecularStrength
|
public bool SpecularStrength
|
||||||
{
|
{
|
||||||
get => ( _data & 0x10 ) != 0;
|
get => (_data & 0x10) != 0;
|
||||||
set => _data = ( ushort )( value ? _data | 0x10 : _data & 0xFFEF );
|
set => _data = (ushort)(value ? _data | 0x10 : _data & 0xFFEF);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public struct RowArray : IEnumerable< Row >
|
public struct RowArray : IEnumerable<Row>
|
||||||
{
|
{
|
||||||
public const int NumRows = 16;
|
public const int NumRows = 16;
|
||||||
private fixed ushort _rowData[NumRows];
|
private fixed ushort _rowData[NumRows];
|
||||||
|
|
||||||
public ref Row this[ int i ]
|
public ref Row this[int i]
|
||||||
{
|
{
|
||||||
get
|
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<Row> GetEnumerator()
|
||||||
{
|
{
|
||||||
for( var i = 0; i < NumRows; ++i )
|
for (var i = 0; i < NumRows; ++i)
|
||||||
{
|
yield return this[i];
|
||||||
yield return this[ i ];
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
IEnumerator IEnumerable.GetEnumerator()
|
IEnumerator IEnumerable.GetEnumerator()
|
||||||
=> GetEnumerator();
|
=> GetEnumerator();
|
||||||
|
|
||||||
public ReadOnlySpan< byte > AsBytes()
|
public ReadOnlySpan<byte> AsBytes()
|
||||||
{
|
{
|
||||||
fixed( ushort* ptr = _rowData )
|
fixed (ushort* ptr = _rowData)
|
||||||
{
|
{
|
||||||
return new ReadOnlySpan< byte >( ptr, NumRows * sizeof( ushort ) );
|
return new ReadOnlySpan<byte>(ptr, NumRows * sizeof(ushort));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -262,10 +259,53 @@ public partial class MtrlFile : IWritable
|
||||||
public ShaderPackageData ShaderPackage;
|
public ShaderPackageData ShaderPackage;
|
||||||
public byte[] AdditionalData;
|
public byte[] AdditionalData;
|
||||||
|
|
||||||
public MtrlFile( byte[] data )
|
public bool ApplyDyeTemplate(StmFile stm, int colorSetIdx, int rowIdx, StainId stainId)
|
||||||
{
|
{
|
||||||
using var stream = new MemoryStream( data );
|
if (colorSetIdx < 0 || colorSetIdx >= ColorDyeSets.Length || rowIdx is < 0 or >= ColorSet.RowArray.NumRows)
|
||||||
using var r = new BinaryReader( stream );
|
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();
|
Version = r.ReadUInt32();
|
||||||
r.ReadUInt16(); // file size
|
r.ReadUInt16(); // file size
|
||||||
|
|
@ -277,39 +317,37 @@ public partial class MtrlFile : IWritable
|
||||||
var colorSetCount = r.ReadByte();
|
var colorSetCount = r.ReadByte();
|
||||||
var additionalDataSize = r.ReadByte();
|
var additionalDataSize = r.ReadByte();
|
||||||
|
|
||||||
Textures = ReadTextureOffsets( r, textureCount, out var textureOffsets );
|
Textures = ReadTextureOffsets(r, textureCount, out var textureOffsets);
|
||||||
UvSets = ReadUvSetOffsets( r, uvSetCount, out var uvOffsets );
|
UvSets = ReadUvSetOffsets(r, uvSetCount, out var uvOffsets);
|
||||||
ColorSets = ReadColorSetOffsets( r, colorSetCount, out var colorOffsets );
|
ColorSets = ReadColorSetOffsets(r, colorSetCount, out var colorOffsets);
|
||||||
|
|
||||||
var strings = r.ReadBytes( stringTableSize );
|
var strings = r.ReadBytes(stringTableSize);
|
||||||
for( var i = 0; i < textureCount; ++i )
|
for (var i = 0; i < textureCount; ++i)
|
||||||
{
|
Textures[i].Path = UseOffset(strings, textureOffsets[i]);
|
||||||
Textures[ i ].Path = UseOffset( strings, textureOffsets[ i ] );
|
|
||||||
}
|
|
||||||
|
|
||||||
for( var i = 0; i < uvSetCount; ++i )
|
for (var i = 0; i < uvSetCount; ++i)
|
||||||
{
|
UvSets[i].Name = UseOffset(strings, uvOffsets[i]);
|
||||||
UvSets[ i ].Name = UseOffset( strings, uvOffsets[ i ] );
|
|
||||||
}
|
|
||||||
|
|
||||||
for( var i = 0; i < colorSetCount; ++i )
|
for (var i = 0; i < colorSetCount; ++i)
|
||||||
{
|
ColorSets[i].Name = UseOffset(strings, colorOffsets[i]);
|
||||||
ColorSets[ i ].Name = UseOffset( strings, colorOffsets[ i ] );
|
|
||||||
}
|
|
||||||
|
|
||||||
ColorDyeSets = ColorSets.Length * ColorSet.RowArray.NumRows * ColorSet.Row.Size < dataSetSize
|
ColorDyeSets = ColorSets.Length * ColorSet.RowArray.NumRows * ColorSet.Row.Size < dataSetSize
|
||||||
? ColorSets.Select( c => new ColorDyeSet { Index = c.Index, Name = c.Name } ).ToArray()
|
? ColorSets.Select(c => new ColorDyeSet
|
||||||
: 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[ i ].Rows = r.ReadStructure< ColorSet.RowArray >();
|
Index = c.Index,
|
||||||
ColorSets[ i ].HasRows = true;
|
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[i].Rows = r.ReadStructure<ColorSet.RowArray>();
|
||||||
|
ColorSets[i].HasRows = true;
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
|
|
@ -317,10 +355,8 @@ public partial class MtrlFile : IWritable
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
for( var i = 0; i < ColorDyeSets.Length; ++i )
|
for (var i = 0; i < ColorDyeSets.Length; ++i)
|
||||||
{
|
ColorDyeSets[i].Rows = r.ReadStructure<ColorDyeSet.RowArray>();
|
||||||
ColorDyeSets[ i ].Rows = r.ReadStructure< ColorDyeSet.RowArray >();
|
|
||||||
}
|
|
||||||
|
|
||||||
var shaderValueListSize = r.ReadUInt16();
|
var shaderValueListSize = r.ReadUInt16();
|
||||||
var shaderKeyCount = r.ReadUInt16();
|
var shaderKeyCount = r.ReadUInt16();
|
||||||
|
|
@ -328,55 +364,55 @@ public partial class MtrlFile : IWritable
|
||||||
var samplerCount = r.ReadUInt16();
|
var samplerCount = r.ReadUInt16();
|
||||||
ShaderPackage.Flags = r.ReadUInt32();
|
ShaderPackage.Flags = r.ReadUInt32();
|
||||||
|
|
||||||
ShaderPackage.ShaderKeys = r.ReadStructuresAsArray< ShaderKey >( shaderKeyCount );
|
ShaderPackage.ShaderKeys = r.ReadStructuresAsArray<ShaderKey>(shaderKeyCount);
|
||||||
ShaderPackage.Constants = r.ReadStructuresAsArray< Constant >( constantCount );
|
ShaderPackage.Constants = r.ReadStructuresAsArray<Constant>(constantCount);
|
||||||
ShaderPackage.Samplers = r.ReadStructuresAsArray< Sampler >( samplerCount );
|
ShaderPackage.Samplers = r.ReadStructuresAsArray<Sampler>(samplerCount);
|
||||||
ShaderPackage.ShaderValues = r.ReadStructuresAsArray< float >( shaderValueListSize / 4 );
|
ShaderPackage.ShaderValues = r.ReadStructuresAsArray<float>(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];
|
var ret = new Texture[count];
|
||||||
offsets = new ushort[count];
|
offsets = new ushort[count];
|
||||||
for( var i = 0; i < count; ++i )
|
for (var i = 0; i < count; ++i)
|
||||||
{
|
{
|
||||||
offsets[ i ] = r.ReadUInt16();
|
offsets[i] = r.ReadUInt16();
|
||||||
ret[ i ].Flags = r.ReadUInt16();
|
ret[i].Flags = r.ReadUInt16();
|
||||||
}
|
}
|
||||||
|
|
||||||
return ret;
|
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];
|
var ret = new UvSet[count];
|
||||||
offsets = new ushort[count];
|
offsets = new ushort[count];
|
||||||
for( var i = 0; i < count; ++i )
|
for (var i = 0; i < count; ++i)
|
||||||
{
|
{
|
||||||
offsets[ i ] = r.ReadUInt16();
|
offsets[i] = r.ReadUInt16();
|
||||||
ret[ i ].Index = r.ReadUInt16();
|
ret[i].Index = r.ReadUInt16();
|
||||||
}
|
}
|
||||||
|
|
||||||
return ret;
|
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];
|
var ret = new ColorSet[count];
|
||||||
offsets = new ushort[count];
|
offsets = new ushort[count];
|
||||||
for( var i = 0; i < count; ++i )
|
for (var i = 0; i < count; ++i)
|
||||||
{
|
{
|
||||||
offsets[ i ] = r.ReadUInt16();
|
offsets[i] = r.ReadUInt16();
|
||||||
ret[ i ].Index = r.ReadUInt16();
|
ret[i].Index = r.ReadUInt16();
|
||||||
}
|
}
|
||||||
|
|
||||||
return ret;
|
return ret;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static string UseOffset( ReadOnlySpan< byte > strings, ushort offset )
|
private static string UseOffset(ReadOnlySpan<byte> strings, ushort offset)
|
||||||
{
|
{
|
||||||
strings = strings[ offset.. ];
|
strings = strings[offset..];
|
||||||
var end = strings.IndexOf( ( byte )'\0' );
|
var end = strings.IndexOf((byte)'\0');
|
||||||
return Encoding.UTF8.GetString( strings[ ..end ] );
|
return Encoding.UTF8.GetString(strings[..end]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
174
Penumbra.GameData/Files/StmFile.StainingTemplateEntry.cs
Normal file
174
Penumbra.GameData/Files/StmFile.StainingTemplateEntry.cs
Normal file
|
|
@ -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
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// The number of stains is capped at 128 at the moment
|
||||||
|
/// </summary>
|
||||||
|
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<Half> GlossEntries;
|
||||||
|
public readonly IReadOnlyList<Half> 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<T> ReadArray<T>(BinaryReader br, int offset, int size, Func<BinaryReader, T> 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<T>(default!, NumElements); // All default
|
||||||
|
case 1: return new RepeatingList<T>(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<T>(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<ushort> 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);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Used if a single value is used for all entries of a list.
|
||||||
|
/// </summary>
|
||||||
|
private sealed 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();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Used if there is a small set of values for a bigger list, accessed via index information.
|
||||||
|
/// </summary>
|
||||||
|
private sealed class IndexedList<T> : IReadOnlyList<T>
|
||||||
|
{
|
||||||
|
private readonly T[] _values;
|
||||||
|
private readonly byte[] _indices;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Reads <paramref name="count"/> values from <paramref name="br"/> via <paramref name="read"/>, then reads <paramref name="indexCount"/> byte indices.
|
||||||
|
/// </summary>
|
||||||
|
public IndexedList(BinaryReader br, int count, int indexCount, Func<BinaryReader, T> 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<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]] : default!;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,10 +1,8 @@
|
||||||
using System;
|
using System;
|
||||||
using System.Collections;
|
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
using System.Numerics;
|
using System.Numerics;
|
||||||
using Dalamud.Data;
|
using Dalamud.Data;
|
||||||
using Lumina.Extensions;
|
|
||||||
using Penumbra.GameData.Structs;
|
using Penumbra.GameData.Structs;
|
||||||
|
|
||||||
namespace Penumbra.GameData.Files;
|
namespace Penumbra.GameData.Files;
|
||||||
|
|
@ -13,160 +11,58 @@ public partial class StmFile
|
||||||
{
|
{
|
||||||
public const string Path = "chara/base_material/stainingtemplate.stm";
|
public const string Path = "chara/base_material/stainingtemplate.stm";
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// All dye-able color set information for a row.
|
||||||
|
/// </summary>
|
||||||
public record struct DyePack
|
public record struct DyePack
|
||||||
{
|
{
|
||||||
public Vector3 Diffuse;
|
public Vector3 Diffuse;
|
||||||
public Vector3 Specular;
|
public Vector3 Specular;
|
||||||
public Vector3 Emissive;
|
public Vector3 Emissive;
|
||||||
public float SpecularPower;
|
|
||||||
public float Gloss;
|
public float Gloss;
|
||||||
|
public float SpecularPower;
|
||||||
}
|
}
|
||||||
|
|
||||||
public readonly struct StainingTemplateEntry
|
/// <summary>
|
||||||
{
|
/// All currently available dyeing templates with their IDs.
|
||||||
public const int NumElements = 128;
|
/// </summary>
|
||||||
|
|
||||||
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;
|
|
||||||
|
|
||||||
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<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 readonly IReadOnlyDictionary<ushort, StainingTemplateEntry> Entries;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Access a specific dye pack.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="template">The ID of the accessed template.</param>
|
||||||
|
/// <param name="idx">The ID of the Stain.</param>
|
||||||
|
/// <returns>The corresponding color set information or a defaulted DyePack of 0-entries.</returns>
|
||||||
public DyePack this[ushort template, int idx]
|
public DyePack this[ushort template, int idx]
|
||||||
=> Entries.TryGetValue(template, out var entry) ? entry[idx] : default;
|
=> Entries.TryGetValue(template, out var entry) ? entry[idx] : default;
|
||||||
|
|
||||||
|
/// <inheritdoc cref="this[ushort, StainId]"/>
|
||||||
public DyePack this[ushort template, StainId idx]
|
public DyePack this[ushort template, StainId idx]
|
||||||
=> this[template, (int)idx.Value];
|
=> this[template, (int)idx.Value];
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Try to access a specific dye pack.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="template">The ID of the accessed template.</param>
|
||||||
|
/// <param name="idx">The ID of the Stain.</param>
|
||||||
|
/// <param name="dyes">On success, the corresponding color set information, otherwise a defaulted DyePack.</param>
|
||||||
|
/// <returns>True on success, false otherwise.</returns>
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Create a STM file from the given data array.
|
||||||
|
/// </summary>
|
||||||
public StmFile(byte[] data)
|
public StmFile(byte[] data)
|
||||||
{
|
{
|
||||||
using var stream = new MemoryStream(data);
|
using var stream = new MemoryStream(data);
|
||||||
|
|
@ -193,6 +89,9 @@ public partial class StmFile
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Try to read and parse the default STM file given by Lumina.
|
||||||
|
/// </summary>
|
||||||
public StmFile(DataManager gameData)
|
public StmFile(DataManager gameData)
|
||||||
: this(gameData.GetFile(Path)?.Data ?? Array.Empty<byte>())
|
: this(gameData.GetFile(Path)?.Data ?? Array.Empty<byte>())
|
||||||
{ }
|
{ }
|
||||||
|
|
|
||||||
252
Penumbra/UI/Classes/ModEditWindow.FileEditor.cs
Normal file
252
Penumbra/UI/Classes/ModEditWindow.FileEditor.cs
Normal file
|
|
@ -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 );
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,20 +1,13 @@
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.Globalization;
|
using System.Globalization;
|
||||||
using System.IO;
|
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Numerics;
|
using System.Numerics;
|
||||||
using System.Runtime.InteropServices;
|
using System.Runtime.InteropServices;
|
||||||
using Dalamud.Interface;
|
using Dalamud.Interface;
|
||||||
using Dalamud.Interface.ImGuiFileDialog;
|
|
||||||
using ImGuiNET;
|
using ImGuiNET;
|
||||||
using Lumina.Data.Parsing.Layer;
|
|
||||||
using OtterGui;
|
using OtterGui;
|
||||||
using OtterGui.Raii;
|
using OtterGui.Raii;
|
||||||
using OtterGui.Widgets;
|
|
||||||
using Penumbra.GameData.Files;
|
using Penumbra.GameData.Files;
|
||||||
using Penumbra.GameData.Structs;
|
|
||||||
using Penumbra.Mods;
|
|
||||||
using Penumbra.String.Classes;
|
using Penumbra.String.Classes;
|
||||||
using Penumbra.String.Functions;
|
using Penumbra.String.Functions;
|
||||||
|
|
||||||
|
|
@ -23,263 +16,6 @@ namespace Penumbra.UI.Classes;
|
||||||
public partial class ModEditWindow
|
public partial class ModEditWindow
|
||||||
{
|
{
|
||||||
private readonly FileEditor< MtrlFile > _materialTab;
|
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 )
|
private static bool DrawMaterialPanel( MtrlFile file, bool disabled )
|
||||||
{
|
{
|
||||||
|
|
@ -330,11 +66,9 @@ public partial class ModEditWindow
|
||||||
ImGui.SameLine();
|
ImGui.SameLine();
|
||||||
var ret = ColorSetPasteAllClipboardButton( file, 0 );
|
var ret = ColorSetPasteAllClipboardButton( file, 0 );
|
||||||
ImGui.SameLine();
|
ImGui.SameLine();
|
||||||
ImGui.Dummy( ImGuiHelpers.ScaledVector2( 10, 0 ) );
|
ImGui.Dummy( ImGuiHelpers.ScaledVector2( 20, 0 ) );
|
||||||
ImGui.SameLine();
|
ImGui.SameLine();
|
||||||
Penumbra.StainManager.StainCombo.Draw( "Preview Dye", Penumbra.StainManager.StainCombo.CurrentSelection.Value.Color, true );
|
ret |= DrawPreviewDye( file, disabled );
|
||||||
ImGui.SameLine();
|
|
||||||
ImGui.Button( "Apply Preview Dyes." );
|
|
||||||
|
|
||||||
using var table = ImRaii.Table( "##ColorSets", 11,
|
using var table = ImRaii.Table( "##ColorSets", 11,
|
||||||
ImGuiTableFlags.SizingFixedFit | ImGuiTableFlags.RowBg | ImGuiTableFlags.BordersInnerV );
|
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 )
|
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 )
|
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( hasDye )
|
||||||
{
|
{
|
||||||
if( Penumbra.StainManager.TemplateCombo.Draw( "##dyeTemplate", dye.Template.ToString(), intSize
|
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;
|
file.ColorDyeSets[ colorSetIdx ].Rows[ rowIdx ].Template = Penumbra.StainManager.TemplateCombo.CurrentSelection;
|
||||||
ret = true;
|
ret = true;
|
||||||
|
|
@ -753,23 +511,7 @@ public partial class ModEditWindow
|
||||||
ImGuiUtil.HoverTooltip( "Dye Template", ImGuiHoveredFlags.AllowWhenDisabled );
|
ImGuiUtil.HoverTooltip( "Dye Template", ImGuiHoveredFlags.AllowWhenDisabled );
|
||||||
|
|
||||||
ImGui.TableNextColumn();
|
ImGui.TableNextColumn();
|
||||||
var stain = Penumbra.StainManager.StainCombo.CurrentSelection.Key;
|
ret |= DrawDyePreview( file, colorSetIdx, rowIdx, disabled, dye, floatSize );
|
||||||
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 );
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
|
|
@ -780,7 +522,40 @@ public partial class ModEditWindow
|
||||||
return ret;
|
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 ret = false;
|
||||||
var tmp = input;
|
var tmp = input;
|
||||||
|
|
@ -792,6 +567,14 @@ public partial class ModEditWindow
|
||||||
ret = true;
|
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 );
|
ImGuiUtil.HoverTooltip( tooltip, ImGuiHoveredFlags.AllowWhenDisabled );
|
||||||
|
|
||||||
return ret;
|
return ret;
|
||||||
31
Penumbra/UI/Classes/ModEditWindow.Models.cs
Normal file
31
Penumbra/UI/Classes/ModEditWindow.Models.cs
Normal file
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -38,7 +38,7 @@ public partial class ModEditWindow : Window, IDisposable
|
||||||
|
|
||||||
SizeConstraints = new WindowSizeConstraints
|
SizeConstraints = new WindowSizeConstraints
|
||||||
{
|
{
|
||||||
MinimumSize = ImGuiHelpers.ScaledVector2( 1000, 600 ),
|
MinimumSize = new Vector2( 1240, 600 ),
|
||||||
MaximumSize = 4000 * Vector2.One,
|
MaximumSize = 4000 * Vector2.One,
|
||||||
};
|
};
|
||||||
_selectedFiles.Clear();
|
_selectedFiles.Clear();
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue