mirror of
https://github.com/xivdev/Penumbra.git
synced 2025-12-14 12:44:19 +01:00
Hopefully merge the rest of the changes correctly.
This commit is contained in:
parent
e6b17d536b
commit
49f1e2dcde
20 changed files with 606 additions and 536 deletions
|
|
@ -1 +1 @@
|
||||||
Subproject commit f66e49bde2878542de17edf428de61f6c8a42efc
|
Subproject commit abdc732be8b36061dc35bb72e25f1dc4876d5286
|
||||||
|
|
@ -13,13 +13,13 @@ using Penumbra.String.Classes;
|
||||||
|
|
||||||
namespace Penumbra.Interop.ResourceTree;
|
namespace Penumbra.Interop.ResourceTree;
|
||||||
|
|
||||||
internal record class GlobalResolveContext(Configuration Config, IObjectIdentifier Identifier, FileCache FileCache, ModCollection Collection, int Skeleton, bool WithNames)
|
internal record class GlobalResolveContext(Configuration Config, IObjectIdentifier Identifier, TreeBuildCache TreeBuildCache, ModCollection Collection, int Skeleton, bool WithNames)
|
||||||
{
|
{
|
||||||
public ResolveContext CreateContext(EquipSlot slot, CharacterArmor equipment)
|
public ResolveContext CreateContext(EquipSlot slot, CharacterArmor equipment)
|
||||||
=> new(Config, Identifier, FileCache, Collection, Skeleton, WithNames, slot, equipment);
|
=> new(Config, Identifier, TreeBuildCache, Collection, Skeleton, WithNames, slot, equipment);
|
||||||
}
|
}
|
||||||
|
|
||||||
internal record class ResolveContext(Configuration Config, IObjectIdentifier Identifier, FileCache FileCache, ModCollection Collection, int Skeleton, bool WithNames, EquipSlot Slot,
|
internal record class ResolveContext(Configuration Config, IObjectIdentifier Identifier, TreeBuildCache TreeBuildCache, ModCollection Collection, int Skeleton, bool WithNames, EquipSlot Slot,
|
||||||
CharacterArmor Equipment)
|
CharacterArmor Equipment)
|
||||||
{
|
{
|
||||||
private static readonly ByteString ShpkPrefix = ByteString.FromSpanUnsafe("shader/sm5/shpk"u8, true, true, true);
|
private static readonly ByteString ShpkPrefix = ByteString.FromSpanUnsafe("shader/sm5/shpk"u8, true, true, true);
|
||||||
|
|
@ -166,12 +166,12 @@ internal record class ResolveContext(Configuration Config, IObjectIdentifier Ide
|
||||||
if (node == null)
|
if (node == null)
|
||||||
return null;
|
return null;
|
||||||
|
|
||||||
var mtrlFile = WithNames ? FileCache.ReadMaterial(node.FullPath) : null;
|
var mtrlFile = WithNames ? TreeBuildCache.ReadMaterial(node.FullPath) : null;
|
||||||
|
|
||||||
var shpkNode = CreateNodeFromShpk(nint.Zero, new ByteString(resource->ShpkString), false);
|
var shpkNode = CreateNodeFromShpk(nint.Zero, new ByteString(resource->ShpkString), false);
|
||||||
if (shpkNode != null)
|
if (shpkNode != null)
|
||||||
node.Children.Add(WithNames ? shpkNode.WithName("Shader Package") : shpkNode);
|
node.Children.Add(WithNames ? shpkNode.WithName("Shader Package") : shpkNode);
|
||||||
var shpkFile = WithNames && shpkNode != null ? FileCache.ReadShaderPackage(shpkNode.FullPath) : null;
|
var shpkFile = WithNames && shpkNode != null ? TreeBuildCache.ReadShaderPackage(shpkNode.FullPath) : null;
|
||||||
var samplers = WithNames ? mtrlFile?.GetSamplersByTexture(shpkFile) : null;
|
var samplers = WithNames ? mtrlFile?.GetSamplersByTexture(shpkFile) : null;
|
||||||
|
|
||||||
for (var i = 0; i < resource->NumTex; i++)
|
for (var i = 0; i < resource->NumTex; i++)
|
||||||
|
|
|
||||||
|
|
@ -13,13 +13,15 @@ public class ResourceTree
|
||||||
{
|
{
|
||||||
public readonly string Name;
|
public readonly string Name;
|
||||||
public readonly nint SourceAddress;
|
public readonly nint SourceAddress;
|
||||||
|
public readonly bool PlayerRelated;
|
||||||
public readonly string CollectionName;
|
public readonly string CollectionName;
|
||||||
public readonly List<ResourceNode> Nodes;
|
public readonly List<ResourceNode> Nodes;
|
||||||
|
|
||||||
public ResourceTree(string name, nint sourceAddress, string collectionName)
|
public ResourceTree(string name, nint sourceAddress, bool playerRelated, string collectionName)
|
||||||
{
|
{
|
||||||
Name = name;
|
Name = name;
|
||||||
SourceAddress = sourceAddress;
|
SourceAddress = sourceAddress;
|
||||||
|
PlayerRelated = playerRelated;
|
||||||
CollectionName = collectionName;
|
CollectionName = collectionName;
|
||||||
Nodes = new List<ResourceNode>();
|
Nodes = new List<ResourceNode>();
|
||||||
}
|
}
|
||||||
|
|
@ -27,7 +29,7 @@ public class ResourceTree
|
||||||
internal unsafe void LoadResources(GlobalResolveContext globalContext)
|
internal unsafe void LoadResources(GlobalResolveContext globalContext)
|
||||||
{
|
{
|
||||||
var character = (Character*)SourceAddress;
|
var character = (Character*)SourceAddress;
|
||||||
var model = (CharacterBase*) character->GameObject.GetDrawObject();
|
var model = (CharacterBase*)character->GameObject.GetDrawObject();
|
||||||
var equipment = new ReadOnlySpan<CharacterArmor>(character->EquipSlotData, 10);
|
var equipment = new ReadOnlySpan<CharacterArmor>(character->EquipSlotData, 10);
|
||||||
// var customize = new ReadOnlySpan<byte>( character->CustomizeData, 26 );
|
// var customize = new ReadOnlySpan<byte>( character->CustomizeData, 26 );
|
||||||
|
|
||||||
|
|
@ -49,42 +51,54 @@ public class ResourceTree
|
||||||
Nodes.Add(globalContext.WithNames ? mdlNode.WithName(mdlNode.Name ?? $"Model #{i}") : mdlNode);
|
Nodes.Add(globalContext.WithNames ? mdlNode.WithName(mdlNode.Name ?? $"Model #{i}") : mdlNode);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (character->GameObject.GetObjectKind() == (byte) ObjectKind.Pc)
|
if (character->GameObject.GetObjectKind() == (byte)ObjectKind.Pc)
|
||||||
AddHumanResources(globalContext, (HumanExt*)model);
|
AddHumanResources(globalContext, (HumanExt*)model);
|
||||||
}
|
}
|
||||||
|
|
||||||
private unsafe void AddHumanResources(GlobalResolveContext globalContext, HumanExt* human)
|
private unsafe void AddHumanResources(GlobalResolveContext globalContext, HumanExt* human)
|
||||||
{
|
{
|
||||||
var prependIndex = 0;
|
var firstSubObject = (CharacterBase*)human->Human.CharacterBase.DrawObject.Object.ChildObject;
|
||||||
|
if (firstSubObject != null)
|
||||||
var firstWeapon = (WeaponExt*)human->Human.CharacterBase.DrawObject.Object.ChildObject;
|
|
||||||
if (firstWeapon != null)
|
|
||||||
{
|
{
|
||||||
var weapon = firstWeapon;
|
var subObjectNodes = new List<ResourceNode>();
|
||||||
var weaponIndex = 0;
|
var subObject = firstSubObject;
|
||||||
|
var subObjectIndex = 0;
|
||||||
do
|
do
|
||||||
{
|
{
|
||||||
var weaponContext = globalContext.CreateContext(
|
var weapon = subObject->GetModelType() == CharacterBase.ModelType.Weapon ? (Weapon*)subObject : null;
|
||||||
slot: EquipSlot.MainHand,
|
var subObjectNamePrefix = weapon != null ? "Weapon" : "Fashion Acc.";
|
||||||
equipment: new CharacterArmor(weapon->Weapon.ModelSetId, (byte)weapon->Weapon.Variant, (byte)weapon->Weapon.ModelUnknown)
|
var subObjectContext = globalContext.CreateContext(
|
||||||
|
weapon != null ? EquipSlot.MainHand : EquipSlot.Unknown,
|
||||||
|
weapon != null ? new CharacterArmor(weapon->ModelSetId, (byte)weapon->Variant, (byte)weapon->ModelUnknown) : default
|
||||||
);
|
);
|
||||||
|
|
||||||
var weaponMdlNode = weaponContext.CreateNodeFromRenderModel(*weapon->WeaponRenderModel);
|
for (var i = 0; i < subObject->SlotCount; ++i)
|
||||||
if (weaponMdlNode != null)
|
{
|
||||||
Nodes.Insert(prependIndex++,
|
var imc = (ResourceHandle*)subObject->IMCArray[i];
|
||||||
globalContext.WithNames ? weaponMdlNode.WithName(weaponMdlNode.Name ?? $"Weapon Model #{weaponIndex}") : weaponMdlNode);
|
var imcNode = subObjectContext.CreateNodeFromImc(imc);
|
||||||
|
if (imcNode != null)
|
||||||
|
subObjectNodes.Add(globalContext.WithNames
|
||||||
|
? imcNode.WithName(imcNode.Name ?? $"{subObjectNamePrefix} #{subObjectIndex}, IMC #{i}")
|
||||||
|
: imcNode);
|
||||||
|
|
||||||
weapon = (WeaponExt*)weapon->Weapon.CharacterBase.DrawObject.Object.NextSiblingObject;
|
var mdl = (RenderModel*)subObject->ModelArray[i];
|
||||||
++weaponIndex;
|
var mdlNode = subObjectContext.CreateNodeFromRenderModel(mdl);
|
||||||
} while (weapon != null && weapon != firstWeapon);
|
if (mdlNode != null)
|
||||||
|
subObjectNodes.Add(globalContext.WithNames
|
||||||
|
? mdlNode.WithName(mdlNode.Name ?? $"{subObjectNamePrefix} #{subObjectIndex}, Model #{i}")
|
||||||
|
: mdlNode);
|
||||||
}
|
}
|
||||||
|
|
||||||
var context = globalContext.CreateContext(
|
subObject = (CharacterBase*)subObject->DrawObject.Object.NextSiblingObject;
|
||||||
EquipSlot.Unknown,
|
++subObjectIndex;
|
||||||
default
|
} while (subObject != null && subObject != firstSubObject);
|
||||||
);
|
|
||||||
|
|
||||||
var skeletonNode = context.CreateHumanSkeletonNode((GenderRace) human->Human.RaceSexId);
|
Nodes.InsertRange(0, subObjectNodes);
|
||||||
|
}
|
||||||
|
|
||||||
|
var context = globalContext.CreateContext(EquipSlot.Unknown, default);
|
||||||
|
|
||||||
|
var skeletonNode = context.CreateHumanSkeletonNode((GenderRace)human->Human.RaceSexId);
|
||||||
if (skeletonNode != null)
|
if (skeletonNode != null)
|
||||||
Nodes.Add(globalContext.WithNames ? skeletonNode.WithName(skeletonNode.Name ?? "Skeleton") : skeletonNode);
|
Nodes.Add(globalContext.WithNames ? skeletonNode.WithName(skeletonNode.Name ?? "Skeleton") : skeletonNode);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,7 @@ using Dalamud.Game.ClientState.Objects;
|
||||||
using FFXIVClientStructs.FFXIV.Client.Game.Character;
|
using FFXIVClientStructs.FFXIV.Client.Game.Character;
|
||||||
using FFXIVClientStructs.FFXIV.Client.Game.Object;
|
using FFXIVClientStructs.FFXIV.Client.Game.Object;
|
||||||
using Penumbra.GameData;
|
using Penumbra.GameData;
|
||||||
|
using Penumbra.GameData.Actors;
|
||||||
using Penumbra.Interop.Resolver;
|
using Penumbra.Interop.Resolver;
|
||||||
using Penumbra.Services;
|
using Penumbra.Services;
|
||||||
|
|
||||||
|
|
@ -17,23 +18,24 @@ public class ResourceTreeFactory
|
||||||
private readonly CollectionResolver _collectionResolver;
|
private readonly CollectionResolver _collectionResolver;
|
||||||
private readonly IdentifierService _identifier;
|
private readonly IdentifierService _identifier;
|
||||||
private readonly Configuration _config;
|
private readonly Configuration _config;
|
||||||
|
private readonly ActorService _actors;
|
||||||
|
|
||||||
public ResourceTreeFactory(DataManager gameData, ObjectTable objects, CollectionResolver resolver, IdentifierService identifier,
|
public ResourceTreeFactory(DataManager gameData, ObjectTable objects, CollectionResolver resolver, IdentifierService identifier,
|
||||||
Configuration config)
|
Configuration config, ActorService actors)
|
||||||
{
|
{
|
||||||
_gameData = gameData;
|
_gameData = gameData;
|
||||||
_objects = objects;
|
_objects = objects;
|
||||||
_collectionResolver = resolver;
|
_collectionResolver = resolver;
|
||||||
_identifier = identifier;
|
_identifier = identifier;
|
||||||
_config = config;
|
_config = config;
|
||||||
|
_actors = actors;
|
||||||
}
|
}
|
||||||
|
|
||||||
public ResourceTree[] FromObjectTable(bool withNames = true)
|
public ResourceTree[] FromObjectTable(bool withNames = true)
|
||||||
{
|
{
|
||||||
var cache = new FileCache(_gameData);
|
var cache = new TreeBuildCache(_objects, _gameData);
|
||||||
|
|
||||||
return _objects
|
return cache.Characters
|
||||||
.OfType<Dalamud.Game.ClientState.Objects.Types.Character>()
|
|
||||||
.Select(c => FromCharacter(c, cache, withNames))
|
.Select(c => FromCharacter(c, cache, withNames))
|
||||||
.OfType<ResourceTree>()
|
.OfType<ResourceTree>()
|
||||||
.ToArray();
|
.ToArray();
|
||||||
|
|
@ -43,7 +45,7 @@ public class ResourceTreeFactory
|
||||||
IEnumerable<Dalamud.Game.ClientState.Objects.Types.Character> characters,
|
IEnumerable<Dalamud.Game.ClientState.Objects.Types.Character> characters,
|
||||||
bool withNames = true)
|
bool withNames = true)
|
||||||
{
|
{
|
||||||
var cache = new FileCache(_gameData);
|
var cache = new TreeBuildCache(_objects, _gameData);
|
||||||
foreach (var character in characters)
|
foreach (var character in characters)
|
||||||
{
|
{
|
||||||
var tree = FromCharacter(character, cache, withNames);
|
var tree = FromCharacter(character, cache, withNames);
|
||||||
|
|
@ -53,11 +55,14 @@ public class ResourceTreeFactory
|
||||||
}
|
}
|
||||||
|
|
||||||
public ResourceTree? FromCharacter(Dalamud.Game.ClientState.Objects.Types.Character character, bool withNames = true)
|
public ResourceTree? FromCharacter(Dalamud.Game.ClientState.Objects.Types.Character character, bool withNames = true)
|
||||||
=> FromCharacter(character, new FileCache(_gameData), withNames);
|
=> FromCharacter(character, new TreeBuildCache(_objects, _gameData), withNames);
|
||||||
|
|
||||||
private unsafe ResourceTree? FromCharacter(Dalamud.Game.ClientState.Objects.Types.Character character, FileCache cache,
|
private unsafe ResourceTree? FromCharacter(Dalamud.Game.ClientState.Objects.Types.Character character, TreeBuildCache cache,
|
||||||
bool withNames = true)
|
bool withNames = true)
|
||||||
{
|
{
|
||||||
|
if (!character.IsValid())
|
||||||
|
return null;
|
||||||
|
|
||||||
var gameObjStruct = (GameObject*)character.Address;
|
var gameObjStruct = (GameObject*)character.Address;
|
||||||
if (gameObjStruct->GetDrawObject() == null)
|
if (gameObjStruct->GetDrawObject() == null)
|
||||||
return null;
|
return null;
|
||||||
|
|
@ -66,11 +71,37 @@ public class ResourceTreeFactory
|
||||||
if (!collectionResolveData.Valid)
|
if (!collectionResolveData.Valid)
|
||||||
return null;
|
return null;
|
||||||
|
|
||||||
var tree = new ResourceTree(character.Name.ToString(), (nint)gameObjStruct, collectionResolveData.ModCollection.Name);
|
var (name, related) = GetCharacterName(character, cache);
|
||||||
|
var tree = new ResourceTree(name, (nint)gameObjStruct, related, collectionResolveData.ModCollection.Name);
|
||||||
var globalContext = new GlobalResolveContext(_config, _identifier.AwaitedService, cache, collectionResolveData.ModCollection,
|
var globalContext = new GlobalResolveContext(_config, _identifier.AwaitedService, cache, collectionResolveData.ModCollection,
|
||||||
((Character*)gameObjStruct)->ModelCharaId,
|
((Character*)gameObjStruct)->ModelCharaId, withNames);
|
||||||
withNames);
|
|
||||||
tree.LoadResources(globalContext);
|
tree.LoadResources(globalContext);
|
||||||
return tree;
|
return tree;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private unsafe (string Name, bool PlayerRelated) GetCharacterName(Dalamud.Game.ClientState.Objects.Types.Character character,
|
||||||
|
TreeBuildCache cache)
|
||||||
|
{
|
||||||
|
var identifier = _actors.AwaitedService.FromObject((GameObject*)character.Address, out var owner, true, false, false);
|
||||||
|
string name;
|
||||||
|
bool playerRelated;
|
||||||
|
switch (identifier.Type)
|
||||||
|
{
|
||||||
|
case IdentifierType.Player:
|
||||||
|
name = identifier.PlayerName.ToString();
|
||||||
|
playerRelated = true;
|
||||||
|
break;
|
||||||
|
case IdentifierType.Owned when cache.CharactersById.TryGetValue(owner->ObjectID, out var ownerChara):
|
||||||
|
var ownerName = GetCharacterName(ownerChara, cache);
|
||||||
|
name = $"[{ownerName.Name}] {character.Name} ({identifier.Kind.ToName()})";
|
||||||
|
playerRelated = ownerName.PlayerRelated;
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
name = $"{character.Name} ({identifier.Kind.ToName()})";
|
||||||
|
playerRelated = false;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (name, playerRelated);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,20 +1,32 @@
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
|
using System.Linq;
|
||||||
using Dalamud.Data;
|
using Dalamud.Data;
|
||||||
|
using Dalamud.Game.ClientState.Objects;
|
||||||
|
using Dalamud.Game.ClientState.Objects.Types;
|
||||||
using Penumbra.GameData.Files;
|
using Penumbra.GameData.Files;
|
||||||
using Penumbra.String.Classes;
|
using Penumbra.String.Classes;
|
||||||
|
|
||||||
namespace Penumbra.Interop.ResourceTree;
|
namespace Penumbra.Interop.ResourceTree;
|
||||||
|
|
||||||
internal class FileCache
|
internal class TreeBuildCache
|
||||||
{
|
{
|
||||||
private readonly DataManager _dataManager;
|
private readonly DataManager _dataManager;
|
||||||
private readonly Dictionary<FullPath, MtrlFile?> _materials = new();
|
private readonly Dictionary<FullPath, MtrlFile?> _materials = new();
|
||||||
private readonly Dictionary<FullPath, ShpkFile?> _shaderPackages = new();
|
private readonly Dictionary<FullPath, ShpkFile?> _shaderPackages = new();
|
||||||
|
public readonly List<Character> Characters;
|
||||||
|
public readonly Dictionary<uint, Character> CharactersById;
|
||||||
|
|
||||||
public FileCache(DataManager dataManager)
|
public TreeBuildCache(ObjectTable objects, DataManager dataManager)
|
||||||
=> _dataManager = dataManager;
|
{
|
||||||
|
_dataManager = dataManager;
|
||||||
|
Characters = objects.Where(c => c is Character ch && ch.IsValid()).Cast<Character>().ToList();
|
||||||
|
CharactersById = Characters
|
||||||
|
.Where(c => c.ObjectId != GameObject.InvalidGameObjectId)
|
||||||
|
.GroupBy(c => c.ObjectId)
|
||||||
|
.ToDictionary(c => c.Key, c => c.First());
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary> Try to read a material file from the given path and cache it on success. </summary>
|
/// <summary> Try to read a material file from the given path and cache it on success. </summary>
|
||||||
public MtrlFile? ReadMaterial(FullPath path)
|
public MtrlFile? ReadMaterial(FullPath path)
|
||||||
|
|
@ -1,14 +0,0 @@
|
||||||
using System.Runtime.InteropServices;
|
|
||||||
using FFXIVClientStructs.FFXIV.Client.Graphics.Scene;
|
|
||||||
|
|
||||||
namespace Penumbra.Interop.Structs;
|
|
||||||
|
|
||||||
[StructLayout( LayoutKind.Explicit )]
|
|
||||||
public unsafe struct WeaponExt
|
|
||||||
{
|
|
||||||
[FieldOffset( 0x0 )]
|
|
||||||
public Weapon Weapon;
|
|
||||||
|
|
||||||
[FieldOffset( 0xA8 )]
|
|
||||||
public RenderModel** WeaponRenderModel;
|
|
||||||
}
|
|
||||||
|
|
@ -40,7 +40,7 @@ public class ModFileEditor
|
||||||
return num;
|
return num;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void RevertFiles(Mod mod, ISubMod option)
|
public void Revert(Mod mod, ISubMod option)
|
||||||
{
|
{
|
||||||
_files.UpdatePaths(mod, option);
|
_files.UpdatePaths(mod, option);
|
||||||
Changes = false;
|
Changes = false;
|
||||||
|
|
|
||||||
|
|
@ -133,6 +133,7 @@ public class PenumbraNew
|
||||||
.AddSingleton<CollectionsTab>()
|
.AddSingleton<CollectionsTab>()
|
||||||
.AddSingleton<ChangedItemsTab>()
|
.AddSingleton<ChangedItemsTab>()
|
||||||
.AddSingleton<EffectiveTab>()
|
.AddSingleton<EffectiveTab>()
|
||||||
|
.AddSingleton<OnScreenTab>()
|
||||||
.AddSingleton<DebugTab>()
|
.AddSingleton<DebugTab>()
|
||||||
.AddSingleton<ResourceTab>()
|
.AddSingleton<ResourceTab>()
|
||||||
.AddSingleton<ConfigTabBar>()
|
.AddSingleton<ConfigTabBar>()
|
||||||
|
|
|
||||||
|
|
@ -20,11 +20,13 @@ public class FileEditor<T> where T : class, IWritable
|
||||||
private readonly Configuration _config;
|
private readonly Configuration _config;
|
||||||
private readonly FileDialogService _fileDialog;
|
private readonly FileDialogService _fileDialog;
|
||||||
private readonly DataManager _gameData;
|
private readonly DataManager _gameData;
|
||||||
|
private readonly ModEditWindow _owner;
|
||||||
|
|
||||||
public FileEditor(DataManager gameData, Configuration config, FileDialogService fileDialog, string tabName, string fileType,
|
public FileEditor(ModEditWindow owner, DataManager gameData, Configuration config, FileDialogService fileDialog, string tabName, string fileType,
|
||||||
Func<IReadOnlyList<FileRegistry>> getFiles, Func<T, bool, bool> drawEdit, Func<string> getInitialPath,
|
Func<IReadOnlyList<FileRegistry>> getFiles, Func<T, bool, bool> drawEdit, Func<string> getInitialPath,
|
||||||
Func<byte[], T?>? parseFile)
|
Func<byte[], T?>? parseFile)
|
||||||
{
|
{
|
||||||
|
_owner = owner;
|
||||||
_gameData = gameData;
|
_gameData = gameData;
|
||||||
_config = config;
|
_config = config;
|
||||||
_fileDialog = fileDialog;
|
_fileDialog = fileDialog;
|
||||||
|
|
@ -41,7 +43,10 @@ public class FileEditor<T> where T : class, IWritable
|
||||||
_list = _getFiles();
|
_list = _getFiles();
|
||||||
using var tab = ImRaii.TabItem(_tabName);
|
using var tab = ImRaii.TabItem(_tabName);
|
||||||
if (!tab)
|
if (!tab)
|
||||||
|
{
|
||||||
|
_quickImport = null;
|
||||||
return;
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
ImGui.NewLine();
|
ImGui.NewLine();
|
||||||
DrawFileSelectCombo();
|
DrawFileSelectCombo();
|
||||||
|
|
@ -69,19 +74,25 @@ public class FileEditor<T> where T : class, IWritable
|
||||||
|
|
||||||
private string _defaultPath = string.Empty;
|
private string _defaultPath = string.Empty;
|
||||||
private bool _inInput;
|
private bool _inInput;
|
||||||
|
private Utf8GamePath _defaultPathUtf8;
|
||||||
|
private bool _isDefaultPathUtf8Valid;
|
||||||
private T? _defaultFile;
|
private T? _defaultFile;
|
||||||
private Exception? _defaultException;
|
private Exception? _defaultException;
|
||||||
|
|
||||||
private IReadOnlyList<FileRegistry> _list = null!;
|
private IReadOnlyList<FileRegistry> _list = null!;
|
||||||
|
|
||||||
|
private ModEditWindow.QuickImportAction? _quickImport;
|
||||||
|
|
||||||
private void DefaultInput()
|
private void DefaultInput()
|
||||||
{
|
{
|
||||||
using var spacing = ImRaii.PushStyle(ImGuiStyleVar.ItemSpacing, ImGui.GetStyle().ItemSpacing with { X = 3 * UiHelpers.Scale });
|
using var spacing = ImRaii.PushStyle(ImGuiStyleVar.ItemSpacing, ImGui.GetStyle().ItemSpacing with { X = UiHelpers.ScaleX3 });
|
||||||
ImGui.SetNextItemWidth(ImGui.GetContentRegionAvail().X - 3 * UiHelpers.Scale - ImGui.GetFrameHeight());
|
ImGui.SetNextItemWidth(ImGui.GetContentRegionAvail().X - 2 * (UiHelpers.ScaleX3 + ImGui.GetFrameHeight()));
|
||||||
ImGui.InputTextWithHint("##defaultInput", "Input game path to compare...", ref _defaultPath, Utf8GamePath.MaxGamePathLength);
|
ImGui.InputTextWithHint("##defaultInput", "Input game path to compare...", ref _defaultPath, Utf8GamePath.MaxGamePathLength);
|
||||||
_inInput = ImGui.IsItemActive();
|
_inInput = ImGui.IsItemActive();
|
||||||
if (ImGui.IsItemDeactivatedAfterEdit() && _defaultPath.Length > 0)
|
if (ImGui.IsItemDeactivatedAfterEdit() && _defaultPath.Length > 0)
|
||||||
{
|
{
|
||||||
|
_isDefaultPathUtf8Valid = Utf8GamePath.FromString(_defaultPath, out _defaultPathUtf8, true);
|
||||||
|
_quickImport = null;
|
||||||
_fileDialog.Reset();
|
_fileDialog.Reset();
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
|
|
@ -123,6 +134,21 @@ public class FileEditor<T> where T : class, IWritable
|
||||||
}
|
}
|
||||||
}, _getInitialPath(), false);
|
}, _getInitialPath(), false);
|
||||||
|
|
||||||
|
_quickImport ??= ModEditWindow.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();
|
_fileDialog.Draw();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -307,7 +307,7 @@ public partial class ModEditWindow
|
||||||
var label = changes ? "Revert Changes" : "Reload Files";
|
var label = changes ? "Revert Changes" : "Reload Files";
|
||||||
var length = new Vector2(ImGui.CalcTextSize("Revert Changes").X, 0);
|
var length = new Vector2(ImGui.CalcTextSize("Revert Changes").X, 0);
|
||||||
if (ImGui.Button(label, length))
|
if (ImGui.Button(label, length))
|
||||||
_editor.FileEditor.RevertFiles(_editor.Mod!, _editor.Option!);
|
_editor.FileEditor.Revert(_editor.Mod!, _editor.Option!);
|
||||||
|
|
||||||
ImGuiUtil.HoverTooltip("Revert all revertible changes since the last file or option reload or data refresh.");
|
ImGuiUtil.HoverTooltip("Revert all revertible changes since the last file or option reload or data refresh.");
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,6 @@ using OtterGui;
|
||||||
using OtterGui.Raii;
|
using OtterGui.Raii;
|
||||||
using Penumbra.GameData.Files;
|
using Penumbra.GameData.Files;
|
||||||
using Penumbra.String.Classes;
|
using Penumbra.String.Classes;
|
||||||
using Penumbra.UI.AdvancedWindow;
|
|
||||||
|
|
||||||
namespace Penumbra.UI.AdvancedWindow;
|
namespace Penumbra.UI.AdvancedWindow;
|
||||||
|
|
||||||
|
|
|
||||||
226
Penumbra/UI/AdvancedWindow/ModEditWindow.QuickImport.cs
Normal file
226
Penumbra/UI/AdvancedWindow/ModEditWindow.QuickImport.cs
Normal file
|
|
@ -0,0 +1,226 @@
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.IO;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Numerics;
|
||||||
|
using Dalamud.Interface;
|
||||||
|
using ImGuiNET;
|
||||||
|
using Lumina.Data;
|
||||||
|
using OtterGui;
|
||||||
|
using OtterGui.Raii;
|
||||||
|
using Penumbra.GameData.Files;
|
||||||
|
using Penumbra.Interop.ResourceTree;
|
||||||
|
using Penumbra.Mods;
|
||||||
|
using Penumbra.String.Classes;
|
||||||
|
|
||||||
|
namespace Penumbra.UI.AdvancedWindow;
|
||||||
|
|
||||||
|
public partial class ModEditWindow
|
||||||
|
{
|
||||||
|
private ResourceTreeViewer? _quickImportViewer;
|
||||||
|
private Dictionary<FullPath, IWritable?>? _quickImportWritables;
|
||||||
|
private Dictionary<(Utf8GamePath, IWritable?), QuickImportAction>? _quickImportActions;
|
||||||
|
private void DrawQuickImportTab()
|
||||||
|
{
|
||||||
|
using var tab = ImRaii.TabItem("Import from Screen");
|
||||||
|
if (!tab)
|
||||||
|
{
|
||||||
|
_quickImportActions = null;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
_quickImportViewer ??= new ResourceTreeViewer(_config, _resourceTreeFactory, "Import from Screen tab", 2, OnQuickImportRefresh, DrawQuickImportActions);
|
||||||
|
_quickImportWritables ??= new Dictionary<FullPath, IWritable?>();
|
||||||
|
_quickImportActions ??= new Dictionary<(Utf8GamePath, IWritable?), QuickImportAction>();
|
||||||
|
|
||||||
|
_quickImportViewer.Draw();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnQuickImportRefresh()
|
||||||
|
{
|
||||||
|
_quickImportWritables?.Clear();
|
||||||
|
_quickImportActions?.Clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void DrawQuickImportActions(ResourceNode resourceNode, Vector2 buttonSize)
|
||||||
|
{
|
||||||
|
if (!_quickImportWritables!.TryGetValue(resourceNode.FullPath, out var writable))
|
||||||
|
{
|
||||||
|
var path = resourceNode.FullPath.ToPath();
|
||||||
|
if (resourceNode.FullPath.IsRooted)
|
||||||
|
{
|
||||||
|
writable = new RawFileWritable(path);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
var file = _gameData.GetFile(path);
|
||||||
|
writable = file == null ? null : new RawGameFileWritable(file);
|
||||||
|
}
|
||||||
|
_quickImportWritables.Add(resourceNode.FullPath, writable);
|
||||||
|
}
|
||||||
|
if (ImGuiUtil.DrawDisabledButton(FontAwesomeIcon.Save.ToIconString(), buttonSize, "Export this file.", resourceNode.FullPath.FullName.Length == 0 || writable == null, true))
|
||||||
|
{
|
||||||
|
var fullPathStr = resourceNode.FullPath.FullName;
|
||||||
|
var ext = resourceNode.PossibleGamePaths.Length == 1 ? Path.GetExtension(resourceNode.GamePath.ToString()) : Path.GetExtension(fullPathStr);
|
||||||
|
_fileDialog.OpenSavePicker($"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}");
|
||||||
|
}
|
||||||
|
}, null, false);
|
||||||
|
}
|
||||||
|
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(), buttonSize, $"Add a copy of this file to {quickImport.OptionName}.", !quickImport.CanExecute, true))
|
||||||
|
{
|
||||||
|
quickImport.Execute();
|
||||||
|
_quickImportActions.Remove((resourceNode.GamePath, writable));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
public class QuickImportAction
|
||||||
|
{
|
||||||
|
public const string FallbackOptionName = "the current option";
|
||||||
|
|
||||||
|
private readonly string _optionName;
|
||||||
|
private readonly Utf8GamePath _gamePath;
|
||||||
|
private readonly ModEditor _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.Mod != null && _file != null && _targetPath != null;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Creates a non-executable QuickImportAction.
|
||||||
|
/// </summary>
|
||||||
|
private QuickImportAction(ModEditor editor, string optionName, Utf8GamePath gamePath)
|
||||||
|
{
|
||||||
|
_optionName = optionName;
|
||||||
|
_gamePath = gamePath;
|
||||||
|
_editor = editor;
|
||||||
|
_file = null;
|
||||||
|
_targetPath = null;
|
||||||
|
_subDirs = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Creates an executable QuickImportAction.
|
||||||
|
/// </summary>
|
||||||
|
private QuickImportAction(string optionName, Utf8GamePath gamePath, ModEditor 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(owner._editor, FallbackOptionName, gamePath);
|
||||||
|
}
|
||||||
|
var subMod = editor.Option;
|
||||||
|
var optionName = subMod!.FullName;
|
||||||
|
if (gamePath.IsEmpty || file == null || editor.FileEditor.Changes)
|
||||||
|
{
|
||||||
|
return new QuickImportAction(editor, optionName, gamePath);
|
||||||
|
}
|
||||||
|
if (subMod.Files.ContainsKey(gamePath) || subMod.FileSwaps.ContainsKey(gamePath))
|
||||||
|
{
|
||||||
|
return new QuickImportAction(editor, optionName, gamePath);
|
||||||
|
}
|
||||||
|
var mod = owner._mod;
|
||||||
|
if (mod == null)
|
||||||
|
{
|
||||||
|
return new QuickImportAction(editor, 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(editor, optionName, gamePath);
|
||||||
|
}
|
||||||
|
|
||||||
|
return new QuickImportAction(optionName, gamePath, editor, file, targetPath, subDirs);
|
||||||
|
}
|
||||||
|
|
||||||
|
public FileRegistry Execute()
|
||||||
|
{
|
||||||
|
if (!CanExecute)
|
||||||
|
{
|
||||||
|
throw new InvalidOperationException();
|
||||||
|
}
|
||||||
|
var directory = Path.GetDirectoryName(_targetPath);
|
||||||
|
if (directory != null)
|
||||||
|
{
|
||||||
|
Directory.CreateDirectory(directory);
|
||||||
|
}
|
||||||
|
File.WriteAllBytes(_targetPath!, _file!.Write());
|
||||||
|
_editor.FileEditor.Revert(_editor.Mod!, _editor.Option!);
|
||||||
|
var fileRegistry = _editor.Files.Available.First(file => file.File.FullName == _targetPath);
|
||||||
|
_editor.FileEditor.AddPathsToSelected(_editor.Option!, new []{ fileRegistry }, _subDirs);
|
||||||
|
_editor.FileEditor.Apply(_editor.Mod!, (Mod.SubMod) _editor.Option!);
|
||||||
|
|
||||||
|
return fileRegistry;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static (DirectoryInfo, int) GetPreferredPath(Mod mod, ISubMod subMod)
|
||||||
|
{
|
||||||
|
var path = mod.ModPath;
|
||||||
|
var subDirs = 0;
|
||||||
|
if (subMod == mod.Default)
|
||||||
|
return (path, subDirs);
|
||||||
|
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -12,6 +12,7 @@ using OtterGui.Raii;
|
||||||
using Penumbra.GameData.Enums;
|
using Penumbra.GameData.Enums;
|
||||||
using Penumbra.GameData.Files;
|
using Penumbra.GameData.Files;
|
||||||
using Penumbra.Import.Textures;
|
using Penumbra.Import.Textures;
|
||||||
|
using Penumbra.Interop.ResourceTree;
|
||||||
using Penumbra.Mods;
|
using Penumbra.Mods;
|
||||||
using Penumbra.String.Classes;
|
using Penumbra.String.Classes;
|
||||||
using Penumbra.UI.Classes;
|
using Penumbra.UI.Classes;
|
||||||
|
|
@ -27,6 +28,8 @@ public partial class ModEditWindow : Window, IDisposable
|
||||||
private readonly ModEditor _editor;
|
private readonly ModEditor _editor;
|
||||||
private readonly Configuration _config;
|
private readonly Configuration _config;
|
||||||
private readonly ItemSwapTab _itemSwapTab;
|
private readonly ItemSwapTab _itemSwapTab;
|
||||||
|
private readonly ResourceTreeFactory _resourceTreeFactory;
|
||||||
|
private readonly DataManager _gameData;
|
||||||
|
|
||||||
private Mod? _mod;
|
private Mod? _mod;
|
||||||
private Vector2 _iconSize = Vector2.Zero;
|
private Vector2 _iconSize = Vector2.Zero;
|
||||||
|
|
@ -136,6 +139,7 @@ public partial class ModEditWindow : Window, IDisposable
|
||||||
DrawSwapTab();
|
DrawSwapTab();
|
||||||
DrawMissingFilesTab();
|
DrawMissingFilesTab();
|
||||||
DrawDuplicatesTab();
|
DrawDuplicatesTab();
|
||||||
|
DrawQuickImportTab();
|
||||||
DrawMaterialReassignmentTab();
|
DrawMaterialReassignmentTab();
|
||||||
_modelTab.Draw();
|
_modelTab.Draw();
|
||||||
_materialTab.Draw();
|
_materialTab.Draw();
|
||||||
|
|
@ -488,19 +492,21 @@ public partial class ModEditWindow : Window, IDisposable
|
||||||
}
|
}
|
||||||
|
|
||||||
public ModEditWindow(FileDialogService fileDialog, ItemSwapTab itemSwapTab, DataManager gameData,
|
public ModEditWindow(FileDialogService fileDialog, ItemSwapTab itemSwapTab, DataManager gameData,
|
||||||
Configuration config, ModEditor editor)
|
Configuration config, ModEditor editor, ResourceTreeFactory resourceTreeFactory)
|
||||||
: base(WindowBaseLabel)
|
: base(WindowBaseLabel)
|
||||||
{
|
{
|
||||||
_itemSwapTab = itemSwapTab;
|
_itemSwapTab = itemSwapTab;
|
||||||
_config = config;
|
_config = config;
|
||||||
_editor = editor;
|
_editor = editor;
|
||||||
|
_gameData = gameData;
|
||||||
|
_resourceTreeFactory = resourceTreeFactory;
|
||||||
_fileDialog = fileDialog;
|
_fileDialog = fileDialog;
|
||||||
_materialTab = new FileEditor<MtrlTab>(gameData, config, _fileDialog, "Materials", ".mtrl",
|
_materialTab = new FileEditor<MtrlTab>(this, gameData, config, _fileDialog, "Materials", ".mtrl",
|
||||||
() => _editor.Files.Mtrl, DrawMaterialPanel, () => _mod?.ModPath.FullName ?? string.Empty,
|
() => _editor.Files.Mtrl, DrawMaterialPanel, () => _mod?.ModPath.FullName ?? string.Empty,
|
||||||
bytes => new MtrlTab(this, new MtrlFile(bytes)));
|
bytes => new MtrlTab(this, new MtrlFile(bytes)));
|
||||||
_modelTab = new FileEditor<MdlFile>(gameData, config, _fileDialog, "Models", ".mdl",
|
_modelTab = new FileEditor<MdlFile>(this, gameData, config, _fileDialog, "Models", ".mdl",
|
||||||
() => _editor.Files.Mdl, DrawModelPanel, () => _mod?.ModPath.FullName ?? string.Empty, null);
|
() => _editor.Files.Mdl, DrawModelPanel, () => _mod?.ModPath.FullName ?? string.Empty, null);
|
||||||
_shaderPackageTab = new FileEditor<ShpkTab>(gameData, config, _fileDialog, "Shader Packages", ".shpk",
|
_shaderPackageTab = new FileEditor<ShpkTab>(this, gameData, config, _fileDialog, "Shader Packages", ".shpk",
|
||||||
() => _editor.Files.Shpk, DrawShaderPackagePanel, () => _mod?.ModPath.FullName ?? string.Empty, null);
|
() => _editor.Files.Shpk, DrawShaderPackagePanel, () => _mod?.ModPath.FullName ?? string.Empty, null);
|
||||||
_center = new CombinedTexture(_left, _right);
|
_center = new CombinedTexture(_left, _right);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
169
Penumbra/UI/AdvancedWindow/ResourceTreeViewer.cs
Normal file
169
Penumbra/UI/AdvancedWindow/ResourceTreeViewer.cs
Normal file
|
|
@ -0,0 +1,169 @@
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Numerics;
|
||||||
|
using Dalamud.Interface;
|
||||||
|
using ImGuiNET;
|
||||||
|
using OtterGui.Raii;
|
||||||
|
using OtterGui;
|
||||||
|
using Penumbra.Interop.ResourceTree;
|
||||||
|
|
||||||
|
namespace Penumbra.UI.AdvancedWindow;
|
||||||
|
|
||||||
|
public class ResourceTreeViewer
|
||||||
|
{
|
||||||
|
private readonly Configuration _config;
|
||||||
|
private readonly ResourceTreeFactory _treeFactory;
|
||||||
|
private readonly string _name;
|
||||||
|
private readonly int _actionCapacity;
|
||||||
|
private readonly Action _onRefresh;
|
||||||
|
private readonly Action<ResourceNode, Vector2> _drawActions;
|
||||||
|
private readonly HashSet<ResourceNode> _unfolded;
|
||||||
|
private ResourceTree[]? _trees;
|
||||||
|
|
||||||
|
public ResourceTreeViewer(Configuration config, ResourceTreeFactory treeFactory, string name, int actionCapacity, Action onRefresh,
|
||||||
|
Action<ResourceNode, Vector2> drawActions)
|
||||||
|
{
|
||||||
|
_config = config;
|
||||||
|
_treeFactory = treeFactory;
|
||||||
|
_name = name;
|
||||||
|
_actionCapacity = actionCapacity;
|
||||||
|
_onRefresh = onRefresh;
|
||||||
|
_drawActions = drawActions;
|
||||||
|
_unfolded = new HashSet<ResourceNode>();
|
||||||
|
_trees = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Draw()
|
||||||
|
{
|
||||||
|
if (ImGui.Button("Refresh Character List"))
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
_trees = _treeFactory.FromObjectTable();
|
||||||
|
}
|
||||||
|
catch (Exception e)
|
||||||
|
{
|
||||||
|
Penumbra.Log.Error($"Could not get character list for {_name}:\n{e}");
|
||||||
|
_trees = Array.Empty<ResourceTree>();
|
||||||
|
}
|
||||||
|
|
||||||
|
_unfolded.Clear();
|
||||||
|
_onRefresh();
|
||||||
|
}
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
_trees ??= _treeFactory.FromObjectTable();
|
||||||
|
}
|
||||||
|
catch (Exception e)
|
||||||
|
{
|
||||||
|
Penumbra.Log.Error($"Could not get character list for {_name}:\n{e}");
|
||||||
|
_trees ??= Array.Empty<ResourceTree>();
|
||||||
|
}
|
||||||
|
|
||||||
|
var textColorNonPlayer = ImGui.GetColorU32(ImGuiCol.Text);
|
||||||
|
var textColorPlayer = (textColorNonPlayer & 0xFF000000u) | ((textColorNonPlayer & 0x00FEFEFE) >> 1) | 0x8000u; // Half green
|
||||||
|
|
||||||
|
foreach (var (tree, index) in _trees.WithIndex())
|
||||||
|
{
|
||||||
|
using (var c = ImRaii.PushColor(ImGuiCol.Text, tree.PlayerRelated ? textColorPlayer : textColorNonPlayer))
|
||||||
|
{
|
||||||
|
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", _actionCapacity > 0 ? 4 : 3,
|
||||||
|
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);
|
||||||
|
if (_actionCapacity > 0)
|
||||||
|
ImGui.TableSetupColumn(string.Empty, ImGuiTableColumnFlags.WidthFixed,
|
||||||
|
(_actionCapacity - 1) * 3 * ImGuiHelpers.GlobalScale + _actionCapacity * ImGui.GetFrameHeight());
|
||||||
|
ImGui.TableHeadersRow();
|
||||||
|
|
||||||
|
DrawNodes(tree.Nodes, 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void DrawNodes(IEnumerable<ResourceNode> resourceNodes, int level)
|
||||||
|
{
|
||||||
|
var debugMode = _config.DebugMode;
|
||||||
|
var frameHeight = ImGui.GetFrameHeight();
|
||||||
|
var cellHeight = _actionCapacity > 0 ? frameHeight : 0.0f;
|
||||||
|
foreach (var (resourceNode, index) in resourceNodes.WithIndex())
|
||||||
|
{
|
||||||
|
if (resourceNode.Internal && !debugMode)
|
||||||
|
continue;
|
||||||
|
|
||||||
|
using var id = ImRaii.PushId(index);
|
||||||
|
ImGui.TableNextColumn();
|
||||||
|
var unfolded = _unfolded!.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)
|
||||||
|
_unfolded.Remove(resourceNode);
|
||||||
|
else
|
||||||
|
_unfolded.Add(resourceNode);
|
||||||
|
unfolded = !unfolded;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (debugMode)
|
||||||
|
ImGuiUtil.HoverTooltip(
|
||||||
|
$"Resource Type: {resourceNode.Type}\nSource Address: 0x{resourceNode.SourceAddress:X16}");
|
||||||
|
}
|
||||||
|
|
||||||
|
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, cellHeight));
|
||||||
|
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();
|
||||||
|
if (resourceNode.FullPath.FullName.Length > 0)
|
||||||
|
{
|
||||||
|
ImGui.Selectable(resourceNode.FullPath.ToString(), false, 0, new Vector2(ImGui.GetContentRegionAvail().X, cellHeight));
|
||||||
|
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, cellHeight));
|
||||||
|
ImGuiUtil.HoverTooltip("The actual path to this file is unavailable.\nIt may be managed by another plug-in.");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (_actionCapacity > 0)
|
||||||
|
{
|
||||||
|
ImGui.TableNextColumn();
|
||||||
|
using var spacing = ImRaii.PushStyle(ImGuiStyleVar.ItemSpacing,
|
||||||
|
ImGui.GetStyle().ItemSpacing with { X = 3 * ImGuiHelpers.GlobalScale });
|
||||||
|
_drawActions(resourceNode, new Vector2(frameHeight));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (unfolded)
|
||||||
|
DrawNodes(resourceNode.Children, level + 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,234 +0,0 @@
|
||||||
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 ResourceTreeViewer? _quickImportViewer;
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
|
|
||||||
_quickImportViewer ??= new( "Import from Screen tab", 2, OnQuickImportRefresh, DrawQuickImportActions );
|
|
||||||
_quickImportWritables ??= new();
|
|
||||||
_quickImportActions ??= new();
|
|
||||||
|
|
||||||
_quickImportViewer.Draw();
|
|
||||||
|
|
||||||
_quickImportFileDialog.Draw();
|
|
||||||
}
|
|
||||||
|
|
||||||
private void OnQuickImportRefresh()
|
|
||||||
{
|
|
||||||
_quickImportWritables?.Clear();
|
|
||||||
_quickImportActions?.Clear();
|
|
||||||
}
|
|
||||||
|
|
||||||
private void DrawQuickImportActions( ResourceTree.Node resourceNode, Vector2 buttonSize )
|
|
||||||
{
|
|
||||||
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(), buttonSize, "Export this file.", resourceNode.FullPath.FullName.Length == 0 || 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(), buttonSize, $"Add a copy of this file to {quickImport.OptionName}.", !quickImport.CanExecute, true ) )
|
|
||||||
{
|
|
||||||
quickImport.Execute();
|
|
||||||
_quickImportActions.Remove( (resourceNode.GamePath, writable) );
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,174 +0,0 @@
|
||||||
using System;
|
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.Numerics;
|
|
||||||
using Dalamud.Interface;
|
|
||||||
using ImGuiNET;
|
|
||||||
using OtterGui.Raii;
|
|
||||||
using OtterGui;
|
|
||||||
using Penumbra.Interop;
|
|
||||||
|
|
||||||
namespace Penumbra.UI.Classes;
|
|
||||||
|
|
||||||
public class ResourceTreeViewer
|
|
||||||
{
|
|
||||||
private readonly string _name;
|
|
||||||
private readonly int _actionCapacity;
|
|
||||||
private readonly Action _onRefresh;
|
|
||||||
private readonly Action<ResourceTree.Node, Vector2> _drawActions;
|
|
||||||
private readonly HashSet<ResourceTree.Node> _unfolded;
|
|
||||||
private ResourceTree[]? _trees;
|
|
||||||
|
|
||||||
public ResourceTreeViewer( string name, int actionCapacity, Action onRefresh, Action<ResourceTree.Node, Vector2> drawActions )
|
|
||||||
{
|
|
||||||
_name = name;
|
|
||||||
_actionCapacity = actionCapacity;
|
|
||||||
_onRefresh = onRefresh;
|
|
||||||
_drawActions = drawActions;
|
|
||||||
_unfolded = new();
|
|
||||||
_trees = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void Draw()
|
|
||||||
{
|
|
||||||
if( ImGui.Button( "Refresh Character List" ) )
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
_trees = ResourceTree.FromObjectTable();
|
|
||||||
}
|
|
||||||
catch( Exception e )
|
|
||||||
{
|
|
||||||
Penumbra.Log.Error( $"Could not get character list for {_name}:\n{e}" );
|
|
||||||
_trees = Array.Empty<ResourceTree>();
|
|
||||||
}
|
|
||||||
_unfolded.Clear();
|
|
||||||
_onRefresh();
|
|
||||||
}
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
_trees ??= ResourceTree.FromObjectTable();
|
|
||||||
}
|
|
||||||
catch( Exception e )
|
|
||||||
{
|
|
||||||
Penumbra.Log.Error( $"Could not get character list for {_name}:\n{e}" );
|
|
||||||
_trees ??= Array.Empty<ResourceTree>();
|
|
||||||
}
|
|
||||||
|
|
||||||
var textColorNonPlayer = ImGui.GetColorU32( ImGuiCol.Text );
|
|
||||||
var textColorPlayer = ( textColorNonPlayer & 0xFF000000u ) | ( ( textColorNonPlayer & 0x00FEFEFE ) >> 1 ) | 0x8000u; // Half green
|
|
||||||
|
|
||||||
foreach( var (tree, index) in _trees.WithIndex() )
|
|
||||||
{
|
|
||||||
using( var c = ImRaii.PushColor( ImGuiCol.Text, tree.PlayerRelated ? textColorPlayer : textColorNonPlayer ) )
|
|
||||||
{
|
|
||||||
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", ( _actionCapacity > 0 ) ? 4 : 3,
|
|
||||||
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 );
|
|
||||||
if( _actionCapacity > 0 )
|
|
||||||
{
|
|
||||||
ImGui.TableSetupColumn( string.Empty, ImGuiTableColumnFlags.WidthFixed, (_actionCapacity - 1) * 3 * ImGuiHelpers.GlobalScale + _actionCapacity * ImGui.GetFrameHeight() );
|
|
||||||
}
|
|
||||||
ImGui.TableHeadersRow();
|
|
||||||
|
|
||||||
DrawNodes( tree.Nodes, 0 );
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void DrawNodes( IEnumerable<ResourceTree.Node> resourceNodes, int level )
|
|
||||||
{
|
|
||||||
var debugMode = Penumbra.Config.DebugMode;
|
|
||||||
var frameHeight = ImGui.GetFrameHeight();
|
|
||||||
var cellHeight = ( _actionCapacity > 0 ) ? frameHeight : 0.0f;
|
|
||||||
foreach( var (resourceNode, index) in resourceNodes.WithIndex() )
|
|
||||||
{
|
|
||||||
if( resourceNode.Internal && !debugMode )
|
|
||||||
{
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
using var id = ImRaii.PushId( index );
|
|
||||||
ImGui.TableNextColumn();
|
|
||||||
var unfolded = _unfolded!.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 )
|
|
||||||
{
|
|
||||||
_unfolded.Remove( resourceNode );
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
_unfolded.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, cellHeight ) );
|
|
||||||
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();
|
|
||||||
if( resourceNode.FullPath.FullName.Length > 0 )
|
|
||||||
{
|
|
||||||
ImGui.Selectable( resourceNode.FullPath.ToString(), false, 0, new Vector2( ImGui.GetContentRegionAvail().X, cellHeight ) );
|
|
||||||
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, cellHeight ) );
|
|
||||||
ImGuiUtil.HoverTooltip( "The actual path to this file is unavailable.\nIt may be managed by another plug-in." );
|
|
||||||
}
|
|
||||||
if( _actionCapacity > 0 )
|
|
||||||
{
|
|
||||||
ImGui.TableNextColumn();
|
|
||||||
using( var spacing = ImRaii.PushStyle( ImGuiStyleVar.ItemSpacing, ImGui.GetStyle().ItemSpacing with { X = 3 * ImGuiHelpers.GlobalScale } ) )
|
|
||||||
{
|
|
||||||
_drawActions( resourceNode, new Vector2( frameHeight ) );
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if( unfolded )
|
|
||||||
{
|
|
||||||
DrawNodes( resourceNode.Children, level + 1 );
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,23 +0,0 @@
|
||||||
using System;
|
|
||||||
using OtterGui.Widgets;
|
|
||||||
using Penumbra.UI.Classes;
|
|
||||||
|
|
||||||
namespace Penumbra.UI;
|
|
||||||
|
|
||||||
public partial class ConfigWindow
|
|
||||||
{
|
|
||||||
private class OnScreenTab : ITab
|
|
||||||
{
|
|
||||||
private ResourceTreeViewer? _viewer;
|
|
||||||
|
|
||||||
public ReadOnlySpan<byte> Label
|
|
||||||
=> "On-Screen"u8;
|
|
||||||
|
|
||||||
public void DrawContent()
|
|
||||||
{
|
|
||||||
_viewer ??= new( "On-Screen tab", 0, delegate { }, delegate { } );
|
|
||||||
|
|
||||||
_viewer.Draw();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -15,6 +15,7 @@ public class ConfigTabBar
|
||||||
public readonly DebugTab Debug;
|
public readonly DebugTab Debug;
|
||||||
public readonly ResourceTab Resource;
|
public readonly ResourceTab Resource;
|
||||||
public readonly ResourceWatcher Watcher;
|
public readonly ResourceWatcher Watcher;
|
||||||
|
public readonly OnScreenTab OnScreenTab;
|
||||||
|
|
||||||
public readonly ITab[] Tabs;
|
public readonly ITab[] Tabs;
|
||||||
|
|
||||||
|
|
@ -22,7 +23,7 @@ public class ConfigTabBar
|
||||||
public TabType SelectTab = TabType.None;
|
public TabType SelectTab = TabType.None;
|
||||||
|
|
||||||
public ConfigTabBar(SettingsTab settings, ModsTab mods, CollectionsTab collections, ChangedItemsTab changedItems, EffectiveTab effective,
|
public ConfigTabBar(SettingsTab settings, ModsTab mods, CollectionsTab collections, ChangedItemsTab changedItems, EffectiveTab effective,
|
||||||
DebugTab debug, ResourceTab resource, ResourceWatcher watcher)
|
DebugTab debug, ResourceTab resource, ResourceWatcher watcher, OnScreenTab onScreenTab)
|
||||||
{
|
{
|
||||||
Settings = settings;
|
Settings = settings;
|
||||||
Mods = mods;
|
Mods = mods;
|
||||||
|
|
@ -32,6 +33,7 @@ public class ConfigTabBar
|
||||||
Debug = debug;
|
Debug = debug;
|
||||||
Resource = resource;
|
Resource = resource;
|
||||||
Watcher = watcher;
|
Watcher = watcher;
|
||||||
|
OnScreenTab = onScreenTab;
|
||||||
Tabs = new ITab[]
|
Tabs = new ITab[]
|
||||||
{
|
{
|
||||||
Settings,
|
Settings,
|
||||||
|
|
@ -39,6 +41,7 @@ public class ConfigTabBar
|
||||||
Collections,
|
Collections,
|
||||||
ChangedItems,
|
ChangedItems,
|
||||||
Effective,
|
Effective,
|
||||||
|
OnScreenTab,
|
||||||
Debug,
|
Debug,
|
||||||
Resource,
|
Resource,
|
||||||
Watcher,
|
Watcher,
|
||||||
|
|
@ -59,6 +62,7 @@ public class ConfigTabBar
|
||||||
TabType.Collections => Collections.Label,
|
TabType.Collections => Collections.Label,
|
||||||
TabType.ChangedItems => ChangedItems.Label,
|
TabType.ChangedItems => ChangedItems.Label,
|
||||||
TabType.EffectiveChanges => Effective.Label,
|
TabType.EffectiveChanges => Effective.Label,
|
||||||
|
TabType.OnScreen => OnScreenTab.Label,
|
||||||
TabType.ResourceWatcher => Watcher.Label,
|
TabType.ResourceWatcher => Watcher.Label,
|
||||||
TabType.Debug => Debug.Label,
|
TabType.Debug => Debug.Label,
|
||||||
TabType.ResourceManager => Resource.Label,
|
TabType.ResourceManager => Resource.Label,
|
||||||
|
|
|
||||||
28
Penumbra/UI/Tabs/OnScreenTab.cs
Normal file
28
Penumbra/UI/Tabs/OnScreenTab.cs
Normal file
|
|
@ -0,0 +1,28 @@
|
||||||
|
using System;
|
||||||
|
using OtterGui.Widgets;
|
||||||
|
using Penumbra.Interop.ResourceTree;
|
||||||
|
using Penumbra.UI.AdvancedWindow;
|
||||||
|
|
||||||
|
namespace Penumbra.UI.Tabs;
|
||||||
|
|
||||||
|
public class OnScreenTab : ITab
|
||||||
|
{
|
||||||
|
private readonly Configuration _config;
|
||||||
|
private readonly ResourceTreeFactory _treeFactory;
|
||||||
|
private ResourceTreeViewer? _viewer;
|
||||||
|
|
||||||
|
public OnScreenTab(Configuration config, ResourceTreeFactory treeFactory)
|
||||||
|
{
|
||||||
|
_config = config;
|
||||||
|
_treeFactory = treeFactory;
|
||||||
|
}
|
||||||
|
|
||||||
|
public ReadOnlySpan<byte> Label
|
||||||
|
=> "On-Screen"u8;
|
||||||
|
|
||||||
|
public void DrawContent()
|
||||||
|
{
|
||||||
|
_viewer ??= new ResourceTreeViewer(_config, _treeFactory, "On-Screen tab", 0, delegate { }, delegate { });
|
||||||
|
_viewer.Draw();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -3,7 +3,6 @@ using Dalamud.Interface;
|
||||||
using Dalamud.Interface.Windowing;
|
using Dalamud.Interface.Windowing;
|
||||||
using Dalamud.Plugin;
|
using Dalamud.Plugin;
|
||||||
using Penumbra.UI;
|
using Penumbra.UI;
|
||||||
using Penumbra.UI.Classes;
|
|
||||||
using Penumbra.UI.AdvancedWindow;
|
using Penumbra.UI.AdvancedWindow;
|
||||||
|
|
||||||
namespace Penumbra;
|
namespace Penumbra;
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue