diff --git a/OtterGui b/OtterGui index 09dcd012..88bf2218 160000 --- a/OtterGui +++ b/OtterGui @@ -1 +1 @@ -Subproject commit 09dcd012a3106862f20f045b9ff9e33d168047c4 +Subproject commit 88bf221852d4a1ac26f5ffbfb5e497220aef75c4 diff --git a/Penumbra.GameData/Files/IWritable.cs b/Penumbra.GameData/Files/IWritable.cs new file mode 100644 index 00000000..afad2e94 --- /dev/null +++ b/Penumbra.GameData/Files/IWritable.cs @@ -0,0 +1,6 @@ +namespace Penumbra.GameData.Files; + +public interface IWritable +{ + public byte[] Write(); +} \ No newline at end of file diff --git a/Penumbra.GameData/Files/MdlFile.cs b/Penumbra.GameData/Files/MdlFile.cs index cf829a65..5073b80c 100644 --- a/Penumbra.GameData/Files/MdlFile.cs +++ b/Penumbra.GameData/Files/MdlFile.cs @@ -7,7 +7,7 @@ using Lumina.Extensions; namespace Penumbra.GameData.Files; -public partial class MdlFile +public partial class MdlFile : IWritable { public const uint NumVertices = 17; public const uint FileHeaderSize = 0x44; diff --git a/Penumbra.GameData/Files/MtrlFile.Write.cs b/Penumbra.GameData/Files/MtrlFile.Write.cs index 910854b9..8a3df6f5 100644 --- a/Penumbra.GameData/Files/MtrlFile.Write.cs +++ b/Penumbra.GameData/Files/MtrlFile.Write.cs @@ -1,3 +1,4 @@ +using System; using System.IO; using System.Linq; using System.Text; @@ -22,24 +23,38 @@ public partial class MtrlFile cumulativeStringOffset += ( ushort )( texture.Path.Length + 1 ); } - foreach( var colorSet in UvColorSets.Concat( ColorSets ) ) + foreach( var set in UvSets ) { w.Write( cumulativeStringOffset ); - w.Write( colorSet.Index ); - cumulativeStringOffset += ( ushort )( colorSet.Name.Length + 1 ); + w.Write( set.Index ); + cumulativeStringOffset += ( ushort )( set.Name.Length + 1 ); + } + + foreach( var set in ColorSets ) + { + w.Write( cumulativeStringOffset ); + w.Write( set.Index ); + cumulativeStringOffset += ( ushort )( set.Name.Length + 1 ); } foreach( var text in Textures.Select( t => t.Path ) - .Concat( UvColorSets.Concat( ColorSets ).Select( c => c.Name ).Append( ShaderPackage.Name ) ) ) + .Concat( UvSets.Select( c => c.Name ) ) + .Concat( ColorSets.Select( c => c.Name ) ) + .Append( ShaderPackage.Name ) ) { w.Write( Encoding.UTF8.GetBytes( text ) ); w.Write( ( byte )'\0' ); } w.Write( AdditionalData ); - foreach( var color in ColorSetData ) + foreach( var row in ColorSets.Select( c => c.Rows ) ) { - w.Write( color ); + w.Write( row.AsBytes() ); + } + + foreach( var row in ColorDyeSets.Select( c => c.Rows ) ) + { + w.Write( row.AsBytes() ); } w.Write( ( ushort )( ShaderPackage.ShaderValues.Length * 4 ) ); @@ -85,11 +100,12 @@ public partial class MtrlFile w.BaseStream.Seek( 0, SeekOrigin.Begin ); w.Write( Version ); w.Write( fileSize ); - w.Write( ( ushort )( ColorSetData.Length * 2 ) ); + w.Write( ( ushort )( ColorSets.Length * ColorSet.RowArray.NumRows * ColorSet.Row.Size + + ColorDyeSets.Length * ColorDyeSet.RowArray.NumRows * 2 ) ); w.Write( ( ushort )( shaderPackageNameOffset + ShaderPackage.Name.Length + 1 ) ); w.Write( shaderPackageNameOffset ); w.Write( ( byte )Textures.Length ); - w.Write( ( byte )UvColorSets.Length ); + w.Write( ( byte )UvSets.Length ); w.Write( ( byte )ColorSets.Length ); w.Write( ( byte )AdditionalData.Length ); } diff --git a/Penumbra.GameData/Files/MtrlFile.cs b/Penumbra.GameData/Files/MtrlFile.cs index 5d6d81a0..76257c1f 100644 --- a/Penumbra.GameData/Files/MtrlFile.cs +++ b/Penumbra.GameData/Files/MtrlFile.cs @@ -1,19 +1,234 @@ using System; +using System.Collections; +using System.Collections.Generic; using System.IO; +using System.Linq; +using System.Numerics; using System.Text; using Lumina.Data.Parsing; using Lumina.Extensions; namespace Penumbra.GameData.Files; -public partial class MtrlFile +public partial class MtrlFile : IWritable { - public struct ColorSet + public struct UvSet { public string Name; public ushort Index; } + public unsafe struct ColorSet + { + public struct Row + { + public const int Size = 32; + + private fixed ushort _data[16]; + + public Vector3 Diffuse + { + get => new(ToFloat( 0 ), ToFloat( 1 ), ToFloat( 2 )); + set + { + _data[ 0 ] = FromFloat( value.X ); + _data[ 1 ] = FromFloat( value.Y ); + _data[ 2 ] = FromFloat( value.Z ); + } + } + + public Vector3 Specular + { + get => new(ToFloat( 4 ), ToFloat( 5 ), ToFloat( 6 )); + set + { + _data[ 4 ] = FromFloat( value.X ); + _data[ 5 ] = FromFloat( value.Y ); + _data[ 6 ] = FromFloat( value.Z ); + } + } + + public Vector3 Emissive + { + get => new(ToFloat( 8 ), ToFloat( 9 ), ToFloat( 10 )); + set + { + _data[ 8 ] = FromFloat( value.X ); + _data[ 9 ] = FromFloat( value.Y ); + _data[ 10 ] = FromFloat( value.Z ); + } + } + + public Vector2 MaterialRepeat + { + get => new(ToFloat( 12 ), ToFloat( 15 )); + set + { + _data[ 12 ] = FromFloat( value.X ); + _data[ 15 ] = FromFloat( value.Y ); + } + } + + public Vector2 MaterialSkew + { + get => new(ToFloat( 13 ), ToFloat( 14 )); + set + { + _data[ 13 ] = FromFloat( value.X ); + _data[ 14 ] = FromFloat( value.Y ); + } + } + + public float SpecularStrength + { + get => ToFloat( 3 ); + set => _data[ 3 ] = FromFloat( value ); + } + + public float GlossStrength + { + get => ToFloat( 7 ); + set => _data[ 7 ] = FromFloat( value ); + } + + public ushort TileSet + { + get => (ushort) (ToFloat(11) * 64f); + set => _data[ 11 ] = FromFloat(value / 64f); + } + + private float ToFloat( int idx ) + => ( float )BitConverter.UInt16BitsToHalf( _data[ idx ] ); + + private static ushort FromFloat( float x ) + => BitConverter.HalfToUInt16Bits( ( Half )x ); + } + + public struct RowArray : IEnumerable< Row > + { + public const int NumRows = 16; + private fixed byte _rowData[NumRows * Row.Size]; + + public ref Row this[ int i ] + { + get + { + fixed( byte* ptr = _rowData ) + { + return ref ( ( Row* )ptr )[ i ]; + } + } + } + + public IEnumerator< Row > GetEnumerator() + { + for( var i = 0; i < NumRows; ++i ) + { + yield return this[ i ]; + } + } + + IEnumerator IEnumerable.GetEnumerator() + => GetEnumerator(); + + public ReadOnlySpan< byte > AsBytes() + { + fixed( byte* ptr = _rowData ) + { + return new ReadOnlySpan< byte >( ptr, NumRows * Row.Size ); + } + } + } + + public RowArray Rows; + public string Name; + public ushort Index; + } + + public unsafe struct ColorDyeSet + { + public struct Row + { + private ushort _data; + + public ushort Template + { + get => ( ushort )( _data >> 5 ); + set => _data = ( ushort )( ( _data & 0x1F ) | ( value << 5 ) ); + } + + public bool Diffuse + { + get => ( _data & 0x01 ) != 0; + set => _data = ( ushort )( value ? _data | 0x01 : _data & 0xFFFE ); + } + + public bool Specular + { + get => ( _data & 0x02 ) != 0; + set => _data = ( ushort )( value ? _data | 0x02 : _data & 0xFFFD ); + } + + public bool Emissive + { + get => ( _data & 0x04 ) != 0; + set => _data = ( ushort )( value ? _data | 0x04 : _data & 0xFFFB ); + } + + public bool Gloss + { + get => ( _data & 0x08 ) != 0; + set => _data = ( ushort )( value ? _data | 0x08 : _data & 0xFFF7 ); + } + + public bool SpecularStrength + { + get => ( _data & 0x10 ) != 0; + set => _data = ( ushort )( value ? _data | 0x10 : _data & 0xFFEF ); + } + } + + public struct RowArray : IEnumerable< Row > + { + public const int NumRows = 16; + private fixed ushort _rowData[NumRows]; + + public ref Row this[ int i ] + { + get + { + fixed( ushort* ptr = _rowData ) + { + return ref ( ( Row* )ptr )[ i ]; + } + } + } + + public IEnumerator< Row > GetEnumerator() + { + for( var i = 0; i < NumRows; ++i ) + { + yield return this[ i ]; + } + } + + IEnumerator IEnumerable.GetEnumerator() + => GetEnumerator(); + + public ReadOnlySpan< byte > AsBytes() + { + fixed( ushort* ptr = _rowData ) + { + return new ReadOnlySpan< byte >( ptr, NumRows * sizeof( ushort ) ); + } + } + } + + public RowArray Rows; + public string Name; + public ushort Index; + } + public struct Texture { public string Path; @@ -37,12 +252,12 @@ public partial class MtrlFile } - public uint Version; + public readonly uint Version; public Texture[] Textures; - public ColorSet[] UvColorSets; + public UvSet[] UvSets; public ColorSet[] ColorSets; - public ushort[] ColorSetData; + public ColorDyeSet[] ColorDyeSets; public ShaderPackageData ShaderPackage; public byte[] AdditionalData; @@ -61,9 +276,9 @@ public partial class MtrlFile var colorSetCount = r.ReadByte(); var additionalDataSize = r.ReadByte(); - Textures = ReadTextureOffsets( r, textureCount, out var textureOffsets ); - UvColorSets = ReadColorSetOffsets( r, uvSetCount, out var uvOffsets ); - ColorSets = ReadColorSetOffsets( r, colorSetCount, out var colorOffsets ); + Textures = ReadTextureOffsets( r, textureCount, out var textureOffsets ); + UvSets = ReadUvSetOffsets( r, uvSetCount, out var uvOffsets ); + ColorSets = ReadColorSetOffsets( r, colorSetCount, out var colorOffsets ); var strings = r.ReadBytes( stringTableSize ); for( var i = 0; i < textureCount; ++i ) @@ -73,7 +288,7 @@ public partial class MtrlFile for( var i = 0; i < uvSetCount; ++i ) { - UvColorSets[ i ].Name = UseOffset( strings, uvOffsets[ i ] ); + UvSets[ i ].Name = UseOffset( strings, uvOffsets[ i ] ); } for( var i = 0; i < colorSetCount; ++i ) @@ -81,10 +296,22 @@ public partial class MtrlFile ColorSets[ i ].Name = UseOffset( strings, colorOffsets[ i ] ); } + ColorDyeSets = ColorSets.Length * ColorSet.RowArray.NumRows * ColorSet.Row.Size < dataSetSize + ? ColorSets.Select( c => new ColorDyeSet { Index = c.Index, Name = c.Name } ).ToArray() + : Array.Empty< ColorDyeSet >(); + ShaderPackage.Name = UseOffset( strings, shaderPackageNameOffset ); AdditionalData = r.ReadBytes( additionalDataSize ); - ColorSetData = r.ReadStructuresAsArray< ushort >( dataSetSize / 2 ); + for( var i = 0; i < ColorSets.Length; ++i ) + { + ColorSets[ i ].Rows = r.ReadStructure< ColorSet.RowArray >(); + } + + for( var i = 0; i < ColorDyeSets.Length; ++i ) + { + ColorDyeSets[ i ].Rows = r.ReadStructure< ColorDyeSet.RowArray >(); + } var shaderValueListSize = r.ReadUInt16(); var shaderKeyCount = r.ReadUInt16(); @@ -111,6 +338,19 @@ public partial class MtrlFile return ret; } + private static UvSet[] ReadUvSetOffsets( BinaryReader r, int count, out ushort[] offsets ) + { + var ret = new UvSet[count]; + offsets = new ushort[count]; + for( var i = 0; i < count; ++i ) + { + offsets[ i ] = r.ReadUInt16(); + ret[ i ].Index = r.ReadUInt16(); + } + + return ret; + } + private static ColorSet[] ReadColorSetOffsets( BinaryReader r, int count, out ushort[] offsets ) { var ret = new ColorSet[count]; diff --git a/Penumbra.GameData/GameData.cs b/Penumbra.GameData/GameData.cs index 55b53aec..421b0031 100644 --- a/Penumbra.GameData/GameData.cs +++ b/Penumbra.GameData/GameData.cs @@ -14,9 +14,9 @@ public static class GameData internal static ObjectIdentification? Identification; internal static readonly GamePathParser GamePathParser = new(); - public static IObjectIdentifier GetIdentifier( DataManager dataManager, ClientLanguage clientLanguage ) + public static IObjectIdentifier GetIdentifier( DataManager dataManager ) { - Identification ??= new ObjectIdentification( dataManager, clientLanguage ); + Identification ??= new ObjectIdentification( dataManager, dataManager.Language ); return Identification; } diff --git a/Penumbra.GameData/Structs/WeaponType.cs b/Penumbra.GameData/Structs/WeaponType.cs index 17bb3dd5..ea310bd7 100644 --- a/Penumbra.GameData/Structs/WeaponType.cs +++ b/Penumbra.GameData/Structs/WeaponType.cs @@ -26,4 +26,10 @@ public readonly struct WeaponType : IEquatable< WeaponType > public override int GetHashCode() => Value.GetHashCode(); + + public static bool operator ==( WeaponType lhs, WeaponType rhs ) + => lhs.Value == rhs.Value; + + public static bool operator !=( WeaponType lhs, WeaponType rhs ) + => lhs.Value != rhs.Value; } \ No newline at end of file diff --git a/Penumbra/Mods/Editor/Mod.Editor.Files.cs b/Penumbra/Mods/Editor/Mod.Editor.Files.cs index 53c14729..ab610f5d 100644 --- a/Penumbra/Mods/Editor/Mod.Editor.Files.cs +++ b/Penumbra/Mods/Editor/Mod.Editor.Files.cs @@ -74,6 +74,8 @@ public partial class Mod public bool FileChanges { get; private set; } private List< FileRegistry > _availableFiles = null!; + private List< FileRegistry > _mtrlFiles = null!; + private List< FileRegistry > _mdlFiles = null!; private readonly HashSet< Utf8GamePath > _usedPaths = new(); // All paths that are used in @@ -82,6 +84,12 @@ public partial class Mod public IReadOnlySet< FullPath > MissingFiles => _missingFiles; + public IReadOnlyList< FileRegistry > MtrlFiles + => _mtrlFiles; + + public IReadOnlyList< FileRegistry > MdlFiles + => _mdlFiles; + // Remove all path redirections where the pointed-to file does not exist. public void RemoveMissingPaths() { @@ -121,6 +129,8 @@ public partial class Mod .OfType< FileRegistry >() ) .ToList(); _usedPaths.Clear(); + _mtrlFiles = _availableFiles.Where( f => f.File.FullName.EndsWith( ".mtrl", StringComparison.OrdinalIgnoreCase ) ).ToList(); + _mdlFiles = _availableFiles.Where( f => f.File.FullName.EndsWith( ".mdl", StringComparison.OrdinalIgnoreCase ) ).ToList(); FileChanges = false; foreach( var subMod in _mod.AllSubMods ) { diff --git a/Penumbra/Mods/Editor/Mod.Editor.MdlMaterials.cs b/Penumbra/Mods/Editor/Mod.Editor.MdlMaterials.cs index f9ac3708..5c1f2c7e 100644 --- a/Penumbra/Mods/Editor/Mod.Editor.MdlMaterials.cs +++ b/Penumbra/Mods/Editor/Mod.Editor.MdlMaterials.cs @@ -17,8 +17,9 @@ public partial class Mod public partial class Editor { private static readonly Regex MaterialRegex = new(@"/mt_c(?'RaceCode'\d{4})b0001_(?'Suffix'.*?)\.mtrl", RegexOptions.Compiled); - private readonly List< MaterialInfo > _modelFiles = new(); - public IReadOnlyList< MaterialInfo > ModelFiles + private readonly List< ModelMaterialInfo > _modelFiles = new(); + + public IReadOnlyList< ModelMaterialInfo > ModelFiles => _modelFiles; // Non-ASCII encoding can not be used. @@ -50,7 +51,9 @@ public partial class Mod public void ReplaceAllMaterials( string toSuffix, string fromSuffix = "", GenderRace raceCode = GenderRace.Unknown ) { if( !ValidString( toSuffix ) ) + { return; + } foreach( var info in _modelFiles ) { @@ -62,7 +65,7 @@ public partial class Mod && ( raceCode == GenderRace.Unknown || raceCode.ToRaceCode() == match.Groups[ "RaceCode" ].Value ) && ( fromSuffix.Length == 0 || fromSuffix == match.Groups[ "Suffix" ].Value ) ) { - info.SetMaterial( $"/mt_c{match.Groups["RaceCode"].Value}b0001_{toSuffix}.mtrl", i ); + info.SetMaterial( $"/mt_c{match.Groups[ "RaceCode" ].Value}b0001_{toSuffix}.mtrl", i ); } } } @@ -72,7 +75,7 @@ public partial class Mod public void ScanModels() { _modelFiles.Clear(); - foreach( var file in AvailableFiles.Where( f => f.File.Extension == ".mdl" ) ) + foreach( var file in _mdlFiles.Where( f => f.File.Extension == ".mdl" ) ) { try { @@ -82,7 +85,7 @@ public partial class Mod .Select( p => p.Item2 ).ToArray(); if( materials.Length > 0 ) { - _modelFiles.Add( new MaterialInfo( file.File, mdlFile, materials ) ); + _modelFiles.Add( new ModelMaterialInfo( file.File, mdlFile, materials ) ); } } catch( Exception e ) @@ -93,22 +96,22 @@ public partial class Mod } // A class that collects information about skin materials in a model file and handle changes on them. - public class MaterialInfo + public class ModelMaterialInfo { - public readonly FullPath Path; - private readonly MdlFile _file; - private readonly string[] _currentMaterials; - private readonly IReadOnlyList _materialIndices; + public readonly FullPath Path; + public readonly MdlFile File; + private readonly string[] _currentMaterials; + private readonly IReadOnlyList< int > _materialIndices; public bool Changed { get; private set; } - public IReadOnlyList CurrentMaterials + public IReadOnlyList< string > CurrentMaterials => _currentMaterials; - private IEnumerable DefaultMaterials - => _materialIndices.Select( i => _file.Materials[i] ); + private IEnumerable< string > DefaultMaterials + => _materialIndices.Select( i => File.Materials[ i ] ); - public (string Current, string Default) this[int idx] - => (_currentMaterials[idx], _file.Materials[_materialIndices[idx]]); + public (string Current, string Default) this[ int idx ] + => ( _currentMaterials[ idx ], File.Materials[ _materialIndices[ idx ] ] ); public int Count => _materialIndices.Count; @@ -116,8 +119,8 @@ public partial class Mod // Set the skin material to a new value and flag changes appropriately. public void SetMaterial( string value, int materialIdx ) { - var mat = _file.Materials[_materialIndices[materialIdx]]; - _currentMaterials[materialIdx] = value; + var mat = File.Materials[ _materialIndices[ materialIdx ] ]; + _currentMaterials[ materialIdx ] = value; if( mat != value ) { Changed = true; @@ -138,12 +141,12 @@ public partial class Mod foreach( var (idx, i) in _materialIndices.WithIndex() ) { - _file.Materials[idx] = _currentMaterials[i]; + File.Materials[ idx ] = _currentMaterials[ i ]; } try { - File.WriteAllBytes( Path.FullName, _file.Write() ); + System.IO.File.WriteAllBytes( Path.FullName, File.Write() ); Changed = false; } catch( Exception e ) @@ -157,23 +160,25 @@ public partial class Mod public void Restore() { if( !Changed ) + { return; + } foreach( var (idx, i) in _materialIndices.WithIndex() ) { - _currentMaterials[i] = _file.Materials[idx]; + _currentMaterials[ i ] = File.Materials[ idx ]; } + Changed = false; } - public MaterialInfo( FullPath path, MdlFile file, IReadOnlyList indices ) + public ModelMaterialInfo( FullPath path, MdlFile file, IReadOnlyList< int > indices ) { - Path = path; - _file = file; - _materialIndices = indices; + Path = path; + File = file; + _materialIndices = indices; _currentMaterials = DefaultMaterials.ToArray(); } } - } } \ No newline at end of file diff --git a/Penumbra/Penumbra.cs b/Penumbra/Penumbra.cs index a5d4e5c8..77c60420 100644 --- a/Penumbra/Penumbra.cs +++ b/Penumbra/Penumbra.cs @@ -70,7 +70,7 @@ public class Penumbra : IDalamudPlugin public Penumbra( DalamudPluginInterface pluginInterface ) { Dalamud.Initialize( pluginInterface ); - GameData.GameData.GetIdentifier( Dalamud.GameData, Dalamud.ClientState.ClientLanguage ); + GameData.GameData.GetIdentifier( Dalamud.GameData ); DevPenumbraExists = CheckDevPluginPenumbra(); IsNotInstalledPenumbra = CheckIsNotInstalled(); diff --git a/Penumbra/UI/Classes/ModEditWindow.FileEdit.cs b/Penumbra/UI/Classes/ModEditWindow.FileEdit.cs new file mode 100644 index 00000000..2be39b93 --- /dev/null +++ b/Penumbra/UI/Classes/ModEditWindow.FileEdit.cs @@ -0,0 +1,607 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Numerics; +using Dalamud.Interface; +using Dalamud.Logging; +using ImGuiNET; +using OtterGui; +using OtterGui.Raii; +using Penumbra.GameData.ByteString; +using Penumbra.GameData.Files; +using Penumbra.Mods; +using Functions = Penumbra.GameData.Util.Functions; + +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 Mod.Editor.FileRegistry? _currentPath; + private T? _currentFile; + private bool _changed; + + private string _defaultPath = string.Empty; + private bool _inInput = false; + private T? _defaultFile; + + private IReadOnlyList< Mod.Editor.FileRegistry > _list = null!; + + public FileEditor( string tabName, string fileType, Func< IReadOnlyList< Mod.Editor.FileRegistry > > getFiles, + Func< T, bool, bool > drawEdit ) + { + _tabName = tabName; + _fileType = fileType; + _getFiles = getFiles; + _drawEdit = drawEdit; + } + + 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() + { + ImGui.SetNextItemWidth( ImGui.GetContentRegionAvail().X ); + ImGui.InputTextWithHint( "##defaultInput", "Input game path to compare...", ref _defaultPath, Utf8GamePath.MaxGamePathLength ); + _inInput = ImGui.IsItemActive(); + if( ImGui.IsItemDeactivatedAfterEdit() && _defaultPath.Length > 0 ) + { + try + { + var file = Dalamud.GameData.GetFile( _defaultPath ); + if( file != null ) + { + _defaultFile = Activator.CreateInstance( typeof( T ), file.Data ) as T; + } + } + catch + { + _defaultFile = null; + } + } + } + + public void Reset() + { + _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; + try + { + var bytes = File.ReadAllBytes( _currentPath.File.FullName ); + _currentFile = Activator.CreateInstance( typeof( T ), bytes ) as T; + } + catch( Exception e ) + { + PluginLog.Error( $"Could not parse {_fileType} file {_currentPath.File.FullName}:\n{e}" ); + _currentFile = null; + } + } + + 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." ); + } + 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." ); + } + 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 ) + { + var ret = DrawMaterialTextureChange( file, disabled ); + + + ImGui.NewLine(); + ret |= DrawMaterialColorSetChange( file, disabled ); + + return disabled && ret; + } + + private static bool DrawMaterialTextureChange( MtrlFile file, bool disabled ) + { + using var id = ImRaii.PushId( "Textures" ); + var ret = false; + for( var i = 0; i < file.Textures.Length; ++i ) + { + using var _ = ImRaii.PushId( i ); + var tmp = file.Textures[ i ].Path; + ImGui.SetNextItemWidth( ImGui.GetContentRegionAvail().X ); + if( ImGui.InputText( string.Empty, ref tmp, Utf8GamePath.MaxGamePathLength, + disabled ? ImGuiInputTextFlags.ReadOnly : ImGuiInputTextFlags.None ) + && tmp.Length > 0 + && tmp != file.Textures[ i ].Path ) + { + ret = true; + file.Textures[ i ].Path = tmp; + } + } + + return ret; + } + + private static bool DrawMaterialColorSetChange( MtrlFile file, bool disabled ) + { + if( file.ColorSets.Length == 0 ) + { + return false; + } + + using var table = ImRaii.Table( "##ColorSets", 10, + ImGuiTableFlags.SizingFixedFit | ImGuiTableFlags.RowBg | ImGuiTableFlags.BordersInnerV ); + if( !table ) + { + return false; + } + + ImGui.TableNextColumn(); + ImGui.TableHeader( string.Empty ); + ImGui.TableNextColumn(); + ImGui.TableHeader( "Row" ); + ImGui.TableNextColumn(); + ImGui.TableHeader( "Diffuse" ); + ImGui.TableNextColumn(); + ImGui.TableHeader( "Specular" ); + ImGui.TableNextColumn(); + ImGui.TableHeader( "Emissive" ); + ImGui.TableNextColumn(); + ImGui.TableHeader( "Gloss" ); + ImGui.TableNextColumn(); + ImGui.TableHeader( "Tile" ); + ImGui.TableNextColumn(); + ImGui.TableHeader( "Repeat" ); + ImGui.TableNextColumn(); + ImGui.TableHeader( "Skew" ); + ImGui.TableNextColumn(); + ImGui.TableHeader( "Dye" ); + + var ret = false; + for( var j = 0; j < file.ColorSets.Length; ++j ) + { + using var _ = ImRaii.PushId( j ); + for( var i = 0; i < MtrlFile.ColorSet.RowArray.NumRows; ++i ) + { + ret |= DrawColorSetRow( file, j, i, disabled ); + ImGui.TableNextRow(); + } + } + + return ret; + } + + private static unsafe void ColorSetCopyClipboardButton( MtrlFile.ColorSet.Row row, MtrlFile.ColorDyeSet.Row dye ) + { + if( ImGuiUtil.DrawDisabledButton( FontAwesomeIcon.Clipboard.ToIconString(), ImGui.GetFrameHeight() * Vector2.One, + "Export this row to your clipboard.", false, true ) ) + { + try + { + var data = new byte[MtrlFile.ColorSet.Row.Size + 2]; + fixed( byte* ptr = data ) + { + Functions.MemCpyUnchecked( ptr, &row, MtrlFile.ColorSet.Row.Size ); + Functions.MemCpyUnchecked( ptr + MtrlFile.ColorSet.Row.Size, &dye, 2 ); + } + + var text = Convert.ToBase64String( data ); + ImGui.SetClipboardText( text ); + } + catch + { + // ignored + } + } + } + + private static unsafe bool ColorSetPasteFromClipboardButton( MtrlFile file, int colorSetIdx, int rowIdx, bool disabled ) + { + if( ImGuiUtil.DrawDisabledButton( FontAwesomeIcon.Paste.ToIconString(), ImGui.GetFrameHeight() * Vector2.One, + "Import an exported row from your clipboard onto this row.", disabled, true ) ) + { + try + { + var text = ImGui.GetClipboardText(); + var data = Convert.FromBase64String( text ); + if( data.Length != MtrlFile.ColorSet.Row.Size + 2 + || file.ColorSets.Length <= colorSetIdx ) + { + return false; + } + + fixed( byte* ptr = data ) + { + file.ColorSets[ colorSetIdx ].Rows[ rowIdx ] = *( MtrlFile.ColorSet.Row* )ptr; + if( file.ColorDyeSets.Length <= colorSetIdx ) + { + file.ColorDyeSets[ colorSetIdx ].Rows[ rowIdx ] = *( MtrlFile.ColorDyeSet.Row* )( ptr + MtrlFile.ColorSet.Row.Size ); + } + } + + return true; + } + catch + { + // ignored + } + } + + return false; + } + + private static bool DrawColorSetRow( MtrlFile file, int colorSetIdx, int rowIdx, bool disabled ) + { + using var id = ImRaii.PushId( rowIdx ); + var row = file.ColorSets[ colorSetIdx ].Rows[ rowIdx ]; + var hasDye = file.ColorDyeSets.Length > colorSetIdx; + var dye = hasDye ? file.ColorDyeSets[ colorSetIdx ].Rows[ rowIdx ] : new MtrlFile.ColorDyeSet.Row(); + var floatSize = 70 * ImGuiHelpers.GlobalScale; + var intSize = 45 * ImGuiHelpers.GlobalScale; + ImGui.TableNextColumn(); + ColorSetCopyClipboardButton( row, dye ); + ImGui.SameLine(); + var ret = ColorSetPasteFromClipboardButton( file, colorSetIdx, rowIdx, disabled ); + + ImGui.TableNextColumn(); + ImGui.TextUnformatted( $"#{rowIdx + 1:D2}" ); + + ImGui.TableNextColumn(); + using var dis = ImRaii.Disabled(disabled); + ret |= ColorPicker( "##Diffuse", "Diffuse Color", row.Diffuse, c => file.ColorSets[ colorSetIdx ].Rows[ rowIdx ].Diffuse = c ); + if( hasDye ) + { + ImGui.SameLine(); + ret |= ImGuiUtil.Checkbox( "##dyeDiffuse", "Apply Diffuse Color on Dye", dye.Diffuse, + b => file.ColorDyeSets[ colorSetIdx ].Rows[ rowIdx ].Diffuse = b, ImGuiHoveredFlags.AllowWhenDisabled ); + } + + ImGui.TableNextColumn(); + ret |= ColorPicker( "##Specular", "Specular Color", row.Specular, c => file.ColorSets[ colorSetIdx ].Rows[ rowIdx ].Specular = c ); + ImGui.SameLine(); + var tmpFloat = row.SpecularStrength; + ImGui.SetNextItemWidth( floatSize ); + if( ImGui.DragFloat( "##SpecularStrength", ref tmpFloat, 0.1f, 0f ) && tmpFloat != row.SpecularStrength ) + { + file.ColorSets[ colorSetIdx ].Rows[ rowIdx ].SpecularStrength = tmpFloat; + ret = true; + } + + ImGuiUtil.HoverTooltip( "Specular Strength", ImGuiHoveredFlags.AllowWhenDisabled ); + + if( hasDye ) + { + ImGui.SameLine(); + ret |= ImGuiUtil.Checkbox( "##dyeSpecular", "Apply Specular Color on Dye", dye.Specular, + b => file.ColorDyeSets[ colorSetIdx ].Rows[ rowIdx ].Specular = b, ImGuiHoveredFlags.AllowWhenDisabled ); + ImGui.SameLine(); + ret |= ImGuiUtil.Checkbox( "##dyeSpecularStrength", "Apply Specular Strength on Dye", dye.SpecularStrength, + b => file.ColorDyeSets[ colorSetIdx ].Rows[ rowIdx ].SpecularStrength = b, ImGuiHoveredFlags.AllowWhenDisabled ); + } + + ImGui.TableNextColumn(); + ret |= ColorPicker( "##Emissive", "Emissive Color", row.Emissive, c => file.ColorSets[ colorSetIdx ].Rows[ rowIdx ].Emissive = c ); + if( hasDye ) + { + ImGui.SameLine(); + ret |= ImGuiUtil.Checkbox( "##dyeEmissive", "Apply Emissive Color on Dye", dye.Emissive, + b => file.ColorDyeSets[ colorSetIdx ].Rows[ rowIdx ].Emissive = b, ImGuiHoveredFlags.AllowWhenDisabled ); + } + + ImGui.TableNextColumn(); + tmpFloat = row.GlossStrength; + ImGui.SetNextItemWidth( floatSize ); + if( ImGui.DragFloat( "##GlossStrength", ref tmpFloat, 0.1f, 0f ) && tmpFloat != row.GlossStrength ) + { + file.ColorSets[ colorSetIdx ].Rows[ rowIdx ].GlossStrength = tmpFloat; + ret = true; + } + + ImGuiUtil.HoverTooltip( "Gloss Strength", ImGuiHoveredFlags.AllowWhenDisabled ); + if( hasDye ) + { + ImGui.SameLine(); + ret |= ImGuiUtil.Checkbox( "##dyeGloss", "Apply Gloss Strength on Dye", dye.Gloss, + b => file.ColorDyeSets[ colorSetIdx ].Rows[ rowIdx ].Gloss = b, ImGuiHoveredFlags.AllowWhenDisabled ); + } + + ImGui.TableNextColumn(); + int tmpInt = row.TileSet; + ImGui.SetNextItemWidth( intSize ); + if( ImGui.InputInt( "##TileSet", ref tmpInt, 0, 0 ) && tmpInt != row.TileSet && tmpInt is >= 0 and <= ushort.MaxValue ) + { + file.ColorSets[ colorSetIdx ].Rows[ rowIdx ].TileSet = ( ushort )tmpInt; + ret = true; + } + + ImGuiUtil.HoverTooltip( "Tile Set", ImGuiHoveredFlags.AllowWhenDisabled ); + + ImGui.TableNextColumn(); + tmpFloat = row.MaterialRepeat.X; + ImGui.SetNextItemWidth( floatSize ); + if( ImGui.DragFloat( "##RepeatX", ref tmpFloat, 0.1f, 0f ) && tmpFloat != row.MaterialRepeat.X ) + { + file.ColorSets[ colorSetIdx ].Rows[ rowIdx ].MaterialRepeat = row.MaterialRepeat with { X = tmpFloat }; + ret = true; + } + + ImGuiUtil.HoverTooltip( "Repeat X", ImGuiHoveredFlags.AllowWhenDisabled ); + ImGui.SameLine(); + tmpFloat = row.MaterialRepeat.Y; + ImGui.SetNextItemWidth( floatSize ); + if( ImGui.DragFloat( "##RepeatY", ref tmpFloat, 0.1f, 0f ) && tmpFloat != row.MaterialRepeat.Y ) + { + file.ColorSets[ colorSetIdx ].Rows[ rowIdx ].MaterialRepeat = row.MaterialRepeat with { Y = tmpFloat }; + ret = true; + } + + ImGuiUtil.HoverTooltip( "Repeat Y", ImGuiHoveredFlags.AllowWhenDisabled ); + + ImGui.TableNextColumn(); + tmpFloat = row.MaterialSkew.X; + ImGui.SetNextItemWidth( floatSize ); + if( ImGui.DragFloat( "##SkewX", ref tmpFloat, 0.1f, 0f ) && tmpFloat != row.MaterialSkew.X ) + { + file.ColorSets[ colorSetIdx ].Rows[ rowIdx ].MaterialSkew = row.MaterialSkew with { X = tmpFloat }; + ret = true; + } + + ImGuiUtil.HoverTooltip( "Skew X", ImGuiHoveredFlags.AllowWhenDisabled ); + + ImGui.SameLine(); + tmpFloat = row.MaterialSkew.Y; + ImGui.SetNextItemWidth( floatSize ); + if( ImGui.DragFloat( "##SkewY", ref tmpFloat, 0.1f, 0f ) && tmpFloat != row.MaterialSkew.Y ) + { + file.ColorSets[ colorSetIdx ].Rows[ rowIdx ].MaterialSkew = row.MaterialSkew with { Y = tmpFloat }; + ret = true; + } + + ImGuiUtil.HoverTooltip( "Skew Y", ImGuiHoveredFlags.AllowWhenDisabled ); + + ImGui.TableNextColumn(); + if( hasDye ) + { + tmpInt = dye.Template; + ImGui.SetNextItemWidth( intSize ); + if( ImGui.InputInt( "##DyeTemplate", ref tmpInt, 0, 0 ) + && tmpInt != dye.Template + && tmpInt is >= 0 and <= ushort.MaxValue ) + { + file.ColorDyeSets[ colorSetIdx ].Rows[ rowIdx ].Template = ( ushort )tmpInt; + ret = true; + } + + ImGuiUtil.HoverTooltip( "Dye Template", ImGuiHoveredFlags.AllowWhenDisabled ); + } + + + return ret; + } + + private static bool ColorPicker( string label, string tooltip, Vector3 input, Action< Vector3 > setter ) + { + var ret = false; + var tmp = input; + if( ImGui.ColorEdit3( label, ref tmp, + ImGuiColorEditFlags.NoInputs | ImGuiColorEditFlags.DisplayRGB | ImGuiColorEditFlags.InputRGB | ImGuiColorEditFlags.NoTooltip ) + && tmp != input ) + { + setter( tmp ); + ret = true; + } + + ImGuiUtil.HoverTooltip( tooltip, ImGuiHoveredFlags.AllowWhenDisabled ); + + return ret; + } + + private void DrawMaterialReassignmentTab() + { + if( _editor!.ModelFiles.Count == 0 ) + { + return; + } + + using var tab = ImRaii.TabItem( "Material Reassignment" ); + if( !tab ) + { + return; + } + + ImGui.NewLine(); + MaterialSuffix.Draw( _editor, ImGuiHelpers.ScaledVector2( 175, 0 ) ); + + ImGui.NewLine(); + using var child = ImRaii.Child( "##mdlFiles", -Vector2.One, true ); + if( !child ) + { + return; + } + + using var table = ImRaii.Table( "##files", 4, ImGuiTableFlags.RowBg | ImGuiTableFlags.SizingFixedFit, -Vector2.One ); + if( !table ) + { + return; + } + + var iconSize = ImGui.GetFrameHeight() * Vector2.One; + foreach( var (info, idx) in _editor.ModelFiles.WithIndex() ) + { + using var id = ImRaii.PushId( idx ); + ImGui.TableNextColumn(); + if( ImGuiUtil.DrawDisabledButton( FontAwesomeIcon.Save.ToIconString(), iconSize, + "Save the changed mdl file.\nUse at own risk!", !info.Changed, true ) ) + { + info.Save(); + } + + ImGui.TableNextColumn(); + if( ImGuiUtil.DrawDisabledButton( FontAwesomeIcon.Recycle.ToIconString(), iconSize, + "Restore current changes to default.", !info.Changed, true ) ) + { + info.Restore(); + } + + ImGui.TableNextColumn(); + ImGui.TextUnformatted( info.Path.FullName[ ( _mod!.ModPath.FullName.Length + 1 ).. ] ); + ImGui.TableNextColumn(); + ImGui.SetNextItemWidth( 400 * ImGuiHelpers.GlobalScale ); + var tmp = info.CurrentMaterials[ 0 ]; + if( ImGui.InputText( "##0", ref tmp, 64 ) ) + { + info.SetMaterial( tmp, 0 ); + } + + for( var i = 1; i < info.Count; ++i ) + { + ImGui.TableNextColumn(); + ImGui.TableNextColumn(); + ImGui.TableNextColumn(); + ImGui.TableNextColumn(); + ImGui.SetNextItemWidth( 400 * ImGuiHelpers.GlobalScale ); + tmp = info.CurrentMaterials[ i ]; + if( ImGui.InputText( $"##{i}", ref tmp, 64 ) ) + { + info.SetMaterial( tmp, i ); + } + } + } + } +} \ No newline at end of file diff --git a/Penumbra/UI/Classes/ModEditWindow.cs b/Penumbra/UI/Classes/ModEditWindow.cs index 0c7d8b9f..4e3693a4 100644 --- a/Penumbra/UI/Classes/ModEditWindow.cs +++ b/Penumbra/UI/Classes/ModEditWindow.cs @@ -10,6 +10,7 @@ using OtterGui; using OtterGui.Raii; using Penumbra.GameData.ByteString; using Penumbra.GameData.Enums; +using Penumbra.GameData.Files; using Penumbra.Mods; using Penumbra.Util; using static Penumbra.Mods.Mod; @@ -40,6 +41,8 @@ public partial class ModEditWindow : Window, IDisposable MaximumSize = 4000 * Vector2.One, }; _selectedFiles.Clear(); + _modelTab.Reset(); + _materialTab.Reset(); } public void ChangeOption( ISubMod? subMod ) @@ -132,7 +135,9 @@ public partial class ModEditWindow : Window, IDisposable DrawSwapTab(); DrawMissingFilesTab(); DrawDuplicatesTab(); - DrawMaterialChangeTab(); + DrawMaterialReassignmentTab(); + _modelTab.Draw(); + _materialTab.Draw(); DrawTextureTab(); } @@ -223,118 +228,41 @@ public partial class ModEditWindow : Window, IDisposable } } - private void DrawMaterialChangeTab() + private void DrawMissingFilesTab() { - using var tab = ImRaii.TabItem( "Model Materials" ); - if( !tab ) + if( _editor!.MissingFiles.Count == 0 ) { return; } - if( _editor!.ModelFiles.Count == 0 ) - { - ImGui.NewLine(); - ImGui.TextUnformatted( "No .mdl files detected." ); - } - else - { - ImGui.NewLine(); - MaterialSuffix.Draw( _editor, ImGuiHelpers.ScaledVector2( 175, 0 ) ); - ImGui.NewLine(); - using var child = ImRaii.Child( "##mdlFiles", -Vector2.One, true ); - if( !child ) - { - return; - } - - using var table = ImRaii.Table( "##files", 4, ImGuiTableFlags.RowBg | ImGuiTableFlags.SizingFixedFit, -Vector2.One ); - if( !table ) - { - return; - } - - var iconSize = ImGui.GetFrameHeight() * Vector2.One; - foreach( var (info, idx) in _editor.ModelFiles.WithIndex() ) - { - using var id = ImRaii.PushId( idx ); - ImGui.TableNextColumn(); - if( ImGuiUtil.DrawDisabledButton( FontAwesomeIcon.Save.ToIconString(), iconSize, - "Save the changed mdl file.\nUse at own risk!", !info.Changed, true ) ) - { - info.Save(); - } - - ImGui.TableNextColumn(); - if( ImGuiUtil.DrawDisabledButton( FontAwesomeIcon.Recycle.ToIconString(), iconSize, - "Restore current changes to default.", !info.Changed, true ) ) - { - info.Restore(); - } - - ImGui.TableNextColumn(); - ImGui.TextUnformatted( info.Path.FullName[ ( _mod!.ModPath.FullName.Length + 1 ).. ] ); - ImGui.TableNextColumn(); - ImGui.SetNextItemWidth( 400 * ImGuiHelpers.GlobalScale ); - var tmp = info.CurrentMaterials[ 0 ]; - if( ImGui.InputText( "##0", ref tmp, 64 ) ) - { - info.SetMaterial( tmp, 0 ); - } - - for( var i = 1; i < info.Count; ++i ) - { - ImGui.TableNextColumn(); - ImGui.TableNextColumn(); - ImGui.TableNextColumn(); - ImGui.TableNextColumn(); - ImGui.SetNextItemWidth( 400 * ImGuiHelpers.GlobalScale ); - tmp = info.CurrentMaterials[ i ]; - if( ImGui.InputText( $"##{i}", ref tmp, 64 ) ) - { - info.SetMaterial( tmp, i ); - } - } - } - } - } - - private void DrawMissingFilesTab() - { using var tab = ImRaii.TabItem( "Missing Files" ); if( !tab ) { return; } - if( _editor!.MissingFiles.Count == 0 ) + ImGui.NewLine(); + if( ImGui.Button( "Remove Missing Files from Mod" ) ) { - ImGui.NewLine(); - ImGui.TextUnformatted( "No missing files detected." ); + _editor.RemoveMissingPaths(); } - else + + using var child = ImRaii.Child( "##unusedFiles", -Vector2.One, true ); + if( !child ) { - if( ImGui.Button( "Remove Missing Files from Mod" ) ) - { - _editor.RemoveMissingPaths(); - } + return; + } - using var child = ImRaii.Child( "##unusedFiles", -Vector2.One, true ); - if( !child ) - { - return; - } + using var table = ImRaii.Table( "##missingFiles", 1, ImGuiTableFlags.RowBg, -Vector2.One ); + if( !table ) + { + return; + } - using var table = ImRaii.Table( "##missingFiles", 1, ImGuiTableFlags.RowBg, -Vector2.One ); - if( !table ) - { - return; - } - - foreach( var path in _editor.MissingFiles ) - { - ImGui.TableNextColumn(); - ImGui.TextUnformatted( path.FullName ); - } + foreach( var path in _editor.MissingFiles ) + { + ImGui.TableNextColumn(); + ImGui.TextUnformatted( path.FullName ); } } @@ -575,7 +503,12 @@ public partial class ModEditWindow : Window, IDisposable public ModEditWindow() : base( WindowBaseLabel ) - { } + { + _materialTab = new FileEditor< MtrlFile >( "Materials (WIP)", ".mtrl", () => _editor?.MtrlFiles ?? Array.Empty< Editor.FileRegistry >(), + DrawMaterialPanel ); + _modelTab = new FileEditor< MdlFile >( "Models (WIP)", ".mdl", () => _editor?.MdlFiles ?? Array.Empty< Editor.FileRegistry >(), + DrawModelPanel ); + } public void Dispose() {