Finish work on dye previews.

This commit is contained in:
Ottermandias 2022-11-14 17:15:41 +01:00
parent 4df8f720f5
commit 17a8e06c1d
7 changed files with 722 additions and 547 deletions

View file

@ -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;
@ -123,10 +124,8 @@ public partial class MtrlFile : IWritable
public IEnumerator<Row> GetEnumerator()
{
for (var i = 0; i < NumRows; ++i)
{
yield return this[i];
}
}
IEnumerator IEnumerable.GetEnumerator()
=> GetEnumerator();
@ -208,10 +207,8 @@ public partial class MtrlFile : IWritable
public IEnumerator<Row> GetEnumerator()
{
for (var i = 0; i < NumRows; ++i)
{
yield return this[i];
}
}
IEnumerator IEnumerable.GetEnumerator()
=> GetEnumerator();
@ -262,6 +259,49 @@ public partial class MtrlFile : IWritable
public ShaderPackageData ShaderPackage;
public byte[] AdditionalData;
public bool ApplyDyeTemplate(StmFile stm, int colorSetIdx, int rowIdx, StainId stainId)
{
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);
@ -283,22 +323,20 @@ public partial class MtrlFile : IWritable
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 < 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()
? ColorSets.Select(c => new ColorDyeSet
{
Index = c.Index,
Name = c.Name,
}).ToArray()
: Array.Empty<ColorDyeSet>();
ShaderPackage.Name = UseOffset(strings, shaderPackageNameOffset);
@ -318,9 +356,7 @@ public partial class MtrlFile : IWritable
}
for (var i = 0; i < ColorDyeSets.Length; ++i)
{
ColorDyeSets[i].Rows = r.ReadStructure<ColorDyeSet.RowArray>();
}
var shaderValueListSize = r.ReadUInt16();
var shaderKeyCount = r.ReadUInt16();

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

View file

@ -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";
/// <summary>
/// All dye-able color set information for a row.
/// </summary>
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<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);
}
}
/// <summary>
/// All currently available dyeing templates with their IDs.
/// </summary>
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]
=> Entries.TryGetValue(template, out var entry) ? entry[idx] : default;
/// <inheritdoc cref="this[ushort, StainId]"/>
public DyePack this[ushort template, StainId idx]
=> 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)
{
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)
: this(gameData.GetFile(Path)?.Data ?? Array.Empty<byte>())
{ }

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

View file

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

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

View file

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