mirror of
https://github.com/xivdev/Penumbra.git
synced 2026-02-18 05:47:52 +01:00
On-Screen resource tree & quick import
This commit is contained in:
parent
3c564add0e
commit
045c84512f
11 changed files with 1194 additions and 18 deletions
|
|
@ -1,6 +1,7 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Numerics;
|
||||
using System.Reflection;
|
||||
using Dalamud.Interface;
|
||||
|
|
@ -18,6 +19,7 @@ public partial class ModEditWindow
|
|||
{
|
||||
private class FileEditor< T > where T : class, IWritable
|
||||
{
|
||||
private readonly ModEditWindow _owner;
|
||||
private readonly string _tabName;
|
||||
private readonly string _fileType;
|
||||
private readonly Func< IReadOnlyList< Mod.Editor.FileRegistry > > _getFiles;
|
||||
|
|
@ -30,18 +32,23 @@ public partial class ModEditWindow
|
|||
private Exception? _currentException;
|
||||
private bool _changed;
|
||||
|
||||
private string _defaultPath = string.Empty;
|
||||
private bool _inInput;
|
||||
private T? _defaultFile;
|
||||
private Exception? _defaultException;
|
||||
private string _defaultPath = string.Empty;
|
||||
private bool _inInput;
|
||||
private Utf8GamePath _defaultPathUtf8;
|
||||
private bool _isDefaultPathUtf8Valid;
|
||||
private T? _defaultFile;
|
||||
private Exception? _defaultException;
|
||||
|
||||
private QuickImportAction? _quickImport;
|
||||
|
||||
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,
|
||||
public FileEditor( ModEditWindow owner, string tabName, string fileType, Func< IReadOnlyList< Mod.Editor.FileRegistry > > getFiles,
|
||||
Func< T, bool, bool > drawEdit, Func< string > getInitialPath, Func< byte[], T? >? parseFile )
|
||||
{
|
||||
_owner = owner;
|
||||
_tabName = tabName;
|
||||
_fileType = fileType;
|
||||
_getFiles = getFiles;
|
||||
|
|
@ -56,6 +63,7 @@ public partial class ModEditWindow
|
|||
using var tab = ImRaii.TabItem( _tabName );
|
||||
if( !tab )
|
||||
{
|
||||
_quickImport = null;
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
@ -74,11 +82,13 @@ public partial class ModEditWindow
|
|||
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.SetNextItemWidth( ImGui.GetContentRegionAvail().X - 2 * (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 )
|
||||
{
|
||||
_isDefaultPathUtf8Valid = Utf8GamePath.FromString( _defaultPath, out _defaultPathUtf8, true );
|
||||
_quickImport = null;
|
||||
_fileDialog.Reset();
|
||||
try
|
||||
{
|
||||
|
|
@ -122,6 +132,22 @@ public partial class ModEditWindow
|
|||
}, _getInitialPath() );
|
||||
}
|
||||
|
||||
_quickImport ??= QuickImportAction.Prepare( _owner, _isDefaultPathUtf8Valid ? _defaultPathUtf8 : Utf8GamePath.Empty, _defaultFile );
|
||||
|
||||
ImGui.SameLine();
|
||||
if( ImGuiUtil.DrawDisabledButton( FontAwesomeIcon.FileImport.ToIconString(), new Vector2( ImGui.GetFrameHeight() ), $"Add a copy of this file to {_quickImport.OptionName}.", !_quickImport.CanExecute, true ) )
|
||||
{
|
||||
try
|
||||
{
|
||||
UpdateCurrentFile( _quickImport.Execute() );
|
||||
}
|
||||
catch( Exception e )
|
||||
{
|
||||
Penumbra.Log.Error( $"Could not add a copy of {_quickImport.GamePath} to {_quickImport.OptionName}:\n{e}" );
|
||||
}
|
||||
_quickImport = null;
|
||||
}
|
||||
|
||||
_fileDialog.Draw();
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Numerics;
|
||||
using Dalamud.Interface;
|
||||
|
|
|
|||
352
Penumbra/UI/Classes/ModEditWindow.QuickImport.cs
Normal file
352
Penumbra/UI/Classes/ModEditWindow.QuickImport.cs
Normal file
|
|
@ -0,0 +1,352 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Numerics;
|
||||
using Dalamud.Interface;
|
||||
using Dalamud.Interface.ImGuiFileDialog;
|
||||
using ImGuiNET;
|
||||
using Lumina.Data;
|
||||
using OtterGui;
|
||||
using OtterGui.Raii;
|
||||
using Penumbra.GameData.Files;
|
||||
using Penumbra.Interop;
|
||||
using Penumbra.Mods;
|
||||
using Penumbra.String.Classes;
|
||||
|
||||
namespace Penumbra.UI.Classes;
|
||||
|
||||
public partial class ModEditWindow
|
||||
{
|
||||
private ResourceTree[]? _quickImportTrees;
|
||||
private HashSet<ResourceTree.Node>? _quickImportUnfolded;
|
||||
private Dictionary<FullPath, IWritable?>? _quickImportWritables;
|
||||
private Dictionary<(Utf8GamePath, IWritable?), QuickImportAction>? _quickImportActions;
|
||||
|
||||
private readonly FileDialogManager _quickImportFileDialog = ConfigWindow.SetupFileManager();
|
||||
|
||||
private void DrawQuickImportTab()
|
||||
{
|
||||
using var tab = ImRaii.TabItem( "Import from Screen" );
|
||||
if( !tab )
|
||||
{
|
||||
_quickImportActions = null;
|
||||
return;
|
||||
}
|
||||
|
||||
_quickImportUnfolded ??= new();
|
||||
_quickImportWritables ??= new();
|
||||
_quickImportActions ??= new();
|
||||
|
||||
if( ImGui.Button( "Refresh Character List" ) )
|
||||
{
|
||||
try
|
||||
{
|
||||
_quickImportTrees = ResourceTree.FromObjectTable();
|
||||
}
|
||||
catch( Exception e )
|
||||
{
|
||||
Penumbra.Log.Error( $"Could not get character list for Import from Screen tab:\n{e}" );
|
||||
_quickImportTrees = Array.Empty<ResourceTree>();
|
||||
}
|
||||
_quickImportUnfolded.Clear();
|
||||
_quickImportWritables.Clear();
|
||||
_quickImportActions.Clear();
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
_quickImportTrees ??= ResourceTree.FromObjectTable();
|
||||
}
|
||||
catch( Exception e )
|
||||
{
|
||||
Penumbra.Log.Error( $"Could not get character list for Import from Screen tab:\n{e}" );
|
||||
_quickImportTrees ??= Array.Empty<ResourceTree>();
|
||||
}
|
||||
|
||||
foreach( var (tree, index) in _quickImportTrees.WithIndex() )
|
||||
{
|
||||
if( !ImGui.CollapsingHeader( $"{tree.Name}##{index}", ( index == 0 ) ? ImGuiTreeNodeFlags.DefaultOpen : 0 ) )
|
||||
{
|
||||
continue;
|
||||
}
|
||||
using var id = ImRaii.PushId( index );
|
||||
|
||||
ImGui.Text( $"Collection: {tree.CollectionName}" );
|
||||
|
||||
using var table = ImRaii.Table( "##ResourceTree", 4,
|
||||
ImGuiTableFlags.SizingFixedFit | ImGuiTableFlags.RowBg );
|
||||
if( !table )
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
ImGui.TableSetupColumn( string.Empty , ImGuiTableColumnFlags.WidthStretch, 0.2f );
|
||||
ImGui.TableSetupColumn( "Game Path" , ImGuiTableColumnFlags.WidthStretch, 0.3f );
|
||||
ImGui.TableSetupColumn( "Actual Path", ImGuiTableColumnFlags.WidthStretch, 0.5f );
|
||||
ImGui.TableSetupColumn( string.Empty , ImGuiTableColumnFlags.WidthFixed, 3 * ImGuiHelpers.GlobalScale + 2 * ImGui.GetFrameHeight() );
|
||||
ImGui.TableHeadersRow();
|
||||
|
||||
DrawQuickImportNodes( tree.Nodes, 0 );
|
||||
}
|
||||
|
||||
_quickImportFileDialog.Draw();
|
||||
}
|
||||
|
||||
private void DrawQuickImportNodes( IEnumerable<ResourceTree.Node> resourceNodes, int level )
|
||||
{
|
||||
var debugMode = Penumbra.Config.DebugMode;
|
||||
var frameHeight = ImGui.GetFrameHeight();
|
||||
foreach( var (resourceNode, index) in resourceNodes.WithIndex() )
|
||||
{
|
||||
if( resourceNode.Internal && !debugMode )
|
||||
{
|
||||
continue;
|
||||
}
|
||||
using var id = ImRaii.PushId( index );
|
||||
ImGui.TableNextColumn();
|
||||
var unfolded = _quickImportUnfolded!.Contains( resourceNode );
|
||||
using( var indent = ImRaii.PushIndent( level ) )
|
||||
{
|
||||
ImGui.TableHeader( ( ( resourceNode.Children.Count > 0 ) ? ( unfolded ? "[-] " : "[+] " ) : string.Empty ) + resourceNode.Name );
|
||||
if( ImGui.IsItemClicked() && resourceNode.Children.Count > 0 )
|
||||
{
|
||||
if( unfolded )
|
||||
{
|
||||
_quickImportUnfolded.Remove( resourceNode );
|
||||
}
|
||||
else
|
||||
{
|
||||
_quickImportUnfolded.Add( resourceNode );
|
||||
}
|
||||
unfolded = !unfolded;
|
||||
}
|
||||
if( debugMode )
|
||||
{
|
||||
ImGuiUtil.HoverTooltip( $"Resource Type: {resourceNode.Type}\nSource Address: 0x{resourceNode.SourceAddress.ToString("X" + nint.Size * 2)}" );
|
||||
}
|
||||
}
|
||||
ImGui.TableNextColumn();
|
||||
var hasGamePaths = resourceNode.PossibleGamePaths.Length > 0;
|
||||
ImGui.Selectable( resourceNode.PossibleGamePaths.Length switch
|
||||
{
|
||||
0 => "(none)",
|
||||
1 => resourceNode.GamePath.ToString(),
|
||||
_ => "(multiple)",
|
||||
}, false, hasGamePaths ? 0 : ImGuiSelectableFlags.Disabled, new Vector2( ImGui.GetContentRegionAvail().X, frameHeight ) );
|
||||
if( hasGamePaths )
|
||||
{
|
||||
var allPaths = string.Join( '\n', resourceNode.PossibleGamePaths );
|
||||
if( ImGui.IsItemClicked() )
|
||||
{
|
||||
ImGui.SetClipboardText( allPaths );
|
||||
}
|
||||
ImGuiUtil.HoverTooltip( $"{allPaths}\n\nClick to copy to clipboard." );
|
||||
}
|
||||
ImGui.TableNextColumn();
|
||||
var hasFullPath = resourceNode.FullPath.FullName.Length > 0;
|
||||
if( hasFullPath )
|
||||
{
|
||||
ImGui.Selectable( resourceNode.FullPath.ToString(), false, 0, new Vector2( ImGui.GetContentRegionAvail().X, frameHeight ) );
|
||||
if( ImGui.IsItemClicked() )
|
||||
{
|
||||
ImGui.SetClipboardText( resourceNode.FullPath.ToString() );
|
||||
}
|
||||
ImGuiUtil.HoverTooltip( $"{resourceNode.FullPath}\n\nClick to copy to clipboard." );
|
||||
}
|
||||
else
|
||||
{
|
||||
ImGui.Selectable( "(unavailable)", false, ImGuiSelectableFlags.Disabled, new Vector2( ImGui.GetContentRegionAvail().X, frameHeight ) );
|
||||
ImGuiUtil.HoverTooltip( "The actual path to this file is unavailable.\nIt may be managed by another plug-in." );
|
||||
}
|
||||
ImGui.TableNextColumn();
|
||||
using( var spacing = ImRaii.PushStyle( ImGuiStyleVar.ItemSpacing, ImGui.GetStyle().ItemSpacing with { X = 3 * ImGuiHelpers.GlobalScale } ) )
|
||||
{
|
||||
if( !_quickImportWritables!.TryGetValue( resourceNode.FullPath, out var writable ) )
|
||||
{
|
||||
var path = resourceNode.FullPath.ToPath();
|
||||
if( resourceNode.FullPath.IsRooted )
|
||||
{
|
||||
writable = new RawFileWritable( path );
|
||||
}
|
||||
else
|
||||
{
|
||||
var file = Dalamud.GameData.GetFile( path );
|
||||
writable = ( file == null ) ? null : new RawGameFileWritable( file );
|
||||
}
|
||||
_quickImportWritables.Add( resourceNode.FullPath, writable );
|
||||
}
|
||||
if( ImGuiUtil.DrawDisabledButton( FontAwesomeIcon.Save.ToIconString(), new Vector2( frameHeight ), "Export this file.", !hasFullPath || writable == null, true ) )
|
||||
{
|
||||
var fullPathStr = resourceNode.FullPath.FullName;
|
||||
var ext = ( resourceNode.PossibleGamePaths.Length == 1 ) ? Path.GetExtension( resourceNode.GamePath.ToString() ) : Path.GetExtension( fullPathStr );
|
||||
_quickImportFileDialog.SaveFileDialog( $"Export {Path.GetFileName( fullPathStr )} to...", ext, Path.GetFileNameWithoutExtension( fullPathStr ), ext, ( success, name ) =>
|
||||
{
|
||||
if( !success )
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
File.WriteAllBytes( name, writable!.Write() );
|
||||
}
|
||||
catch( Exception e )
|
||||
{
|
||||
Penumbra.Log.Error( $"Could not export {fullPathStr}:\n{e}" );
|
||||
}
|
||||
} );
|
||||
}
|
||||
ImGui.SameLine();
|
||||
if( !_quickImportActions!.TryGetValue( (resourceNode.GamePath, writable), out var quickImport ) )
|
||||
{
|
||||
quickImport = QuickImportAction.Prepare( this, resourceNode.GamePath, writable );
|
||||
_quickImportActions.Add( (resourceNode.GamePath, writable), quickImport );
|
||||
}
|
||||
if( ImGuiUtil.DrawDisabledButton( FontAwesomeIcon.FileImport.ToIconString(), new Vector2( frameHeight ), $"Add a copy of this file to {quickImport.OptionName}.", !quickImport.CanExecute, true ) )
|
||||
{
|
||||
quickImport.Execute();
|
||||
_quickImportActions.Remove( (resourceNode.GamePath, writable) );
|
||||
}
|
||||
}
|
||||
if( unfolded )
|
||||
{
|
||||
DrawQuickImportNodes( resourceNode.Children, level + 1 );
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private record class RawFileWritable( string Path ) : IWritable
|
||||
{
|
||||
public bool Valid => true;
|
||||
|
||||
public byte[] Write()
|
||||
=> File.ReadAllBytes( Path );
|
||||
}
|
||||
|
||||
private record class RawGameFileWritable( FileResource FileResource ) : IWritable
|
||||
{
|
||||
public bool Valid => true;
|
||||
|
||||
public byte[] Write()
|
||||
=> FileResource.Data;
|
||||
}
|
||||
|
||||
private class QuickImportAction
|
||||
{
|
||||
public const string FallbackOptionName = "the current option";
|
||||
|
||||
private readonly string _optionName;
|
||||
private readonly Utf8GamePath _gamePath;
|
||||
private readonly Mod.Editor? _editor;
|
||||
private readonly IWritable? _file;
|
||||
private readonly string? _targetPath;
|
||||
private readonly int _subDirs;
|
||||
|
||||
public string OptionName => _optionName;
|
||||
public Utf8GamePath GamePath => _gamePath;
|
||||
public bool CanExecute => !_gamePath.IsEmpty && _editor != null && _file != null && _targetPath != null;
|
||||
|
||||
/// <summary>
|
||||
/// Creates a non-executable QuickImportAction.
|
||||
/// </summary>
|
||||
private QuickImportAction( string optionName, Utf8GamePath gamePath )
|
||||
{
|
||||
_optionName = optionName;
|
||||
_gamePath = gamePath;
|
||||
_editor = null;
|
||||
_file = null;
|
||||
_targetPath = null;
|
||||
_subDirs = 0;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates an executable QuickImportAction.
|
||||
/// </summary>
|
||||
private QuickImportAction( string optionName, Utf8GamePath gamePath, Mod.Editor editor, IWritable file, string targetPath, int subDirs )
|
||||
{
|
||||
_optionName = optionName;
|
||||
_gamePath = gamePath;
|
||||
_editor = editor;
|
||||
_file = file;
|
||||
_targetPath = targetPath;
|
||||
_subDirs = subDirs;
|
||||
}
|
||||
|
||||
public static QuickImportAction Prepare( ModEditWindow owner, Utf8GamePath gamePath, IWritable? file )
|
||||
{
|
||||
var editor = owner._editor;
|
||||
if( editor == null )
|
||||
{
|
||||
return new QuickImportAction( FallbackOptionName, gamePath );
|
||||
}
|
||||
var subMod = editor.CurrentOption;
|
||||
var optionName = subMod.FullName;
|
||||
if( gamePath.IsEmpty || file == null || editor.FileChanges )
|
||||
{
|
||||
return new QuickImportAction( optionName, gamePath );
|
||||
}
|
||||
if( subMod.Files.ContainsKey( gamePath ) || subMod.FileSwaps.ContainsKey( gamePath ) )
|
||||
{
|
||||
return new QuickImportAction( optionName, gamePath );
|
||||
}
|
||||
var mod = owner._mod;
|
||||
if( mod == null )
|
||||
{
|
||||
return new QuickImportAction( optionName, gamePath );
|
||||
}
|
||||
var ( preferredPath, subDirs ) = GetPreferredPath( mod, subMod );
|
||||
var targetPath = new FullPath( Path.Combine( preferredPath.FullName, gamePath.ToString() ) ).FullName;
|
||||
if( File.Exists( targetPath ) )
|
||||
{
|
||||
return new QuickImportAction( optionName, gamePath );
|
||||
}
|
||||
|
||||
return new QuickImportAction( optionName, gamePath, editor, file, targetPath, subDirs );
|
||||
}
|
||||
|
||||
public Mod.Editor.FileRegistry Execute()
|
||||
{
|
||||
if( !CanExecute )
|
||||
{
|
||||
throw new InvalidOperationException();
|
||||
}
|
||||
var directory = Path.GetDirectoryName( _targetPath );
|
||||
if( directory != null )
|
||||
{
|
||||
Directory.CreateDirectory( directory );
|
||||
}
|
||||
File.WriteAllBytes( _targetPath!, _file!.Write() );
|
||||
_editor!.RevertFiles();
|
||||
var fileRegistry = _editor.AvailableFiles.First( file => file.File.FullName == _targetPath );
|
||||
_editor.AddPathsToSelected( new Mod.Editor.FileRegistry[] { fileRegistry }, _subDirs );
|
||||
_editor.ApplyFiles();
|
||||
|
||||
return fileRegistry;
|
||||
}
|
||||
|
||||
private static (DirectoryInfo, int) GetPreferredPath( Mod mod, ISubMod subMod )
|
||||
{
|
||||
var path = mod.ModPath;
|
||||
var subDirs = 0;
|
||||
if( subMod != mod.Default )
|
||||
{
|
||||
var name = subMod.Name;
|
||||
var fullName = subMod.FullName;
|
||||
if( fullName.EndsWith( ": " + name ) )
|
||||
{
|
||||
path = Mod.Creator.NewOptionDirectory( path, fullName[..^( name.Length + 2 )] );
|
||||
path = Mod.Creator.NewOptionDirectory( path, name );
|
||||
subDirs = 2;
|
||||
}
|
||||
else
|
||||
{
|
||||
path = Mod.Creator.NewOptionDirectory( path, fullName );
|
||||
subDirs = 1;
|
||||
}
|
||||
}
|
||||
|
||||
return (path, subDirs);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -153,6 +153,7 @@ public partial class ModEditWindow : Window, IDisposable
|
|||
DrawSwapTab();
|
||||
DrawMissingFilesTab();
|
||||
DrawDuplicatesTab();
|
||||
DrawQuickImportTab();
|
||||
DrawMaterialReassignmentTab();
|
||||
_modelTab.Draw();
|
||||
_materialTab.Draw();
|
||||
|
|
@ -570,17 +571,17 @@ public partial class ModEditWindow : Window, IDisposable
|
|||
public ModEditWindow()
|
||||
: base( WindowBaseLabel )
|
||||
{
|
||||
_materialTab = new FileEditor< MtrlTab >( "Materials", ".mtrl",
|
||||
_materialTab = new FileEditor< MtrlTab >( this, "Materials", ".mtrl",
|
||||
() => _editor?.MtrlFiles ?? Array.Empty< Editor.FileRegistry >(),
|
||||
DrawMaterialPanel,
|
||||
() => _mod?.ModPath.FullName ?? string.Empty,
|
||||
bytes => new MtrlTab( this, new MtrlFile( bytes ) ) );
|
||||
_modelTab = new FileEditor< MdlFile >( "Models", ".mdl",
|
||||
_modelTab = new FileEditor< MdlFile >( this, "Models", ".mdl",
|
||||
() => _editor?.MdlFiles ?? Array.Empty< Editor.FileRegistry >(),
|
||||
DrawModelPanel,
|
||||
() => _mod?.ModPath.FullName ?? string.Empty,
|
||||
null );
|
||||
_shaderPackageTab = new FileEditor< ShpkTab >( "Shader Packages", ".shpk",
|
||||
_shaderPackageTab = new FileEditor< ShpkTab >( this, "Shader Packages", ".shpk",
|
||||
() => _editor?.ShpkFiles ?? Array.Empty< Editor.FileRegistry >(),
|
||||
DrawShaderPackagePanel,
|
||||
() => _mod?.ModPath.FullName ?? string.Empty,
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue