Hopefully merge the rest of the changes correctly.

This commit is contained in:
Ottermandias 2023-03-23 18:54:16 +01:00
parent e6b17d536b
commit 49f1e2dcde
20 changed files with 606 additions and 536 deletions

@ -1 +1 @@
Subproject commit f66e49bde2878542de17edf428de61f6c8a42efc
Subproject commit abdc732be8b36061dc35bb72e25f1dc4876d5286

View file

@ -13,13 +13,13 @@ using Penumbra.String.Classes;
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)
=> 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)
{
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)
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);
if (shpkNode != null)
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;
for (var i = 0; i < resource->NumTex; i++)

View file

@ -13,13 +13,15 @@ public class ResourceTree
{
public readonly string Name;
public readonly nint SourceAddress;
public readonly bool PlayerRelated;
public readonly string CollectionName;
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;
SourceAddress = sourceAddress;
PlayerRelated = playerRelated;
CollectionName = collectionName;
Nodes = new List<ResourceNode>();
}
@ -27,7 +29,7 @@ public class ResourceTree
internal unsafe void LoadResources(GlobalResolveContext globalContext)
{
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 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);
}
if (character->GameObject.GetObjectKind() == (byte) ObjectKind.Pc)
if (character->GameObject.GetObjectKind() == (byte)ObjectKind.Pc)
AddHumanResources(globalContext, (HumanExt*)model);
}
}
private unsafe void AddHumanResources(GlobalResolveContext globalContext, HumanExt* human)
{
var prependIndex = 0;
var firstWeapon = (WeaponExt*)human->Human.CharacterBase.DrawObject.Object.ChildObject;
if (firstWeapon != null)
var firstSubObject = (CharacterBase*)human->Human.CharacterBase.DrawObject.Object.ChildObject;
if (firstSubObject != null)
{
var weapon = firstWeapon;
var weaponIndex = 0;
var subObjectNodes = new List<ResourceNode>();
var subObject = firstSubObject;
var subObjectIndex = 0;
do
{
var weaponContext = globalContext.CreateContext(
slot: EquipSlot.MainHand,
equipment: new CharacterArmor(weapon->Weapon.ModelSetId, (byte)weapon->Weapon.Variant, (byte)weapon->Weapon.ModelUnknown)
var weapon = subObject->GetModelType() == CharacterBase.ModelType.Weapon ? (Weapon*)subObject : null;
var subObjectNamePrefix = weapon != null ? "Weapon" : "Fashion Acc.";
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);
if (weaponMdlNode != null)
Nodes.Insert(prependIndex++,
globalContext.WithNames ? weaponMdlNode.WithName(weaponMdlNode.Name ?? $"Weapon Model #{weaponIndex}") : weaponMdlNode);
for (var i = 0; i < subObject->SlotCount; ++i)
{
var imc = (ResourceHandle*)subObject->IMCArray[i];
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;
++weaponIndex;
} while (weapon != null && weapon != firstWeapon);
var mdl = (RenderModel*)subObject->ModelArray[i];
var mdlNode = subObjectContext.CreateNodeFromRenderModel(mdl);
if (mdlNode != null)
subObjectNodes.Add(globalContext.WithNames
? mdlNode.WithName(mdlNode.Name ?? $"{subObjectNamePrefix} #{subObjectIndex}, Model #{i}")
: mdlNode);
}
subObject = (CharacterBase*)subObject->DrawObject.Object.NextSiblingObject;
++subObjectIndex;
} while (subObject != null && subObject != firstSubObject);
Nodes.InsertRange(0, subObjectNodes);
}
var context = globalContext.CreateContext(
EquipSlot.Unknown,
default
);
var context = globalContext.CreateContext(EquipSlot.Unknown, default);
var skeletonNode = context.CreateHumanSkeletonNode((GenderRace) human->Human.RaceSexId);
var skeletonNode = context.CreateHumanSkeletonNode((GenderRace)human->Human.RaceSexId);
if (skeletonNode != null)
Nodes.Add(globalContext.WithNames ? skeletonNode.WithName(skeletonNode.Name ?? "Skeleton") : skeletonNode);

View file

@ -5,6 +5,7 @@ using Dalamud.Game.ClientState.Objects;
using FFXIVClientStructs.FFXIV.Client.Game.Character;
using FFXIVClientStructs.FFXIV.Client.Game.Object;
using Penumbra.GameData;
using Penumbra.GameData.Actors;
using Penumbra.Interop.Resolver;
using Penumbra.Services;
@ -17,23 +18,24 @@ public class ResourceTreeFactory
private readonly CollectionResolver _collectionResolver;
private readonly IdentifierService _identifier;
private readonly Configuration _config;
private readonly ActorService _actors;
public ResourceTreeFactory(DataManager gameData, ObjectTable objects, CollectionResolver resolver, IdentifierService identifier,
Configuration config)
Configuration config, ActorService actors)
{
_gameData = gameData;
_objects = objects;
_collectionResolver = resolver;
_identifier = identifier;
_config = config;
_actors = actors;
}
public ResourceTree[] FromObjectTable(bool withNames = true)
{
var cache = new FileCache(_gameData);
var cache = new TreeBuildCache(_objects, _gameData);
return _objects
.OfType<Dalamud.Game.ClientState.Objects.Types.Character>()
return cache.Characters
.Select(c => FromCharacter(c, cache, withNames))
.OfType<ResourceTree>()
.ToArray();
@ -43,7 +45,7 @@ public class ResourceTreeFactory
IEnumerable<Dalamud.Game.ClientState.Objects.Types.Character> characters,
bool withNames = true)
{
var cache = new FileCache(_gameData);
var cache = new TreeBuildCache(_objects, _gameData);
foreach (var character in characters)
{
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)
=> 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)
{
if (!character.IsValid())
return null;
var gameObjStruct = (GameObject*)character.Address;
if (gameObjStruct->GetDrawObject() == null)
return null;
@ -66,11 +71,37 @@ public class ResourceTreeFactory
if (!collectionResolveData.Valid)
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,
((Character*)gameObjStruct)->ModelCharaId,
withNames);
((Character*)gameObjStruct)->ModelCharaId, withNames);
tree.LoadResources(globalContext);
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);
}
}

View file

@ -1,20 +1,32 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using Dalamud.Data;
using Dalamud.Game.ClientState.Objects;
using Dalamud.Game.ClientState.Objects.Types;
using Penumbra.GameData.Files;
using Penumbra.String.Classes;
namespace Penumbra.Interop.ResourceTree;
internal class FileCache
internal class TreeBuildCache
{
private readonly DataManager _dataManager;
private readonly Dictionary<FullPath, MtrlFile?> _materials = new();
private readonly Dictionary<FullPath, ShpkFile?> _shaderPackages = new();
public readonly List<Character> Characters;
public readonly Dictionary<uint, Character> CharactersById;
public FileCache(DataManager dataManager)
=> _dataManager = dataManager;
public TreeBuildCache(ObjectTable objects, 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>
public MtrlFile? ReadMaterial(FullPath path)

View file

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

View file

@ -40,7 +40,7 @@ public class ModFileEditor
return num;
}
public void RevertFiles(Mod mod, ISubMod option)
public void Revert(Mod mod, ISubMod option)
{
_files.UpdatePaths(mod, option);
Changes = false;

View file

@ -133,6 +133,7 @@ public class PenumbraNew
.AddSingleton<CollectionsTab>()
.AddSingleton<ChangedItemsTab>()
.AddSingleton<EffectiveTab>()
.AddSingleton<OnScreenTab>()
.AddSingleton<DebugTab>()
.AddSingleton<ResourceTab>()
.AddSingleton<ConfigTabBar>()

View file

@ -20,11 +20,13 @@ public class FileEditor<T> where T : class, IWritable
private readonly Configuration _config;
private readonly FileDialogService _fileDialog;
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<byte[], T?>? parseFile)
{
_owner = owner;
_gameData = gameData;
_config = config;
_fileDialog = fileDialog;
@ -41,7 +43,10 @@ public class FileEditor<T> where T : class, IWritable
_list = _getFiles();
using var tab = ImRaii.TabItem(_tabName);
if (!tab)
{
_quickImport = null;
return;
}
ImGui.NewLine();
DrawFileSelectCombo();
@ -67,21 +72,27 @@ public class FileEditor<T> where T : class, IWritable
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 IReadOnlyList<FileRegistry> _list = null!;
private ModEditWindow.QuickImportAction? _quickImport;
private void DefaultInput()
{
using var spacing = ImRaii.PushStyle(ImGuiStyleVar.ItemSpacing, ImGui.GetStyle().ItemSpacing with { X = 3 * UiHelpers.Scale });
ImGui.SetNextItemWidth(ImGui.GetContentRegionAvail().X - 3 * UiHelpers.Scale - ImGui.GetFrameHeight());
using var spacing = ImRaii.PushStyle(ImGuiStyleVar.ItemSpacing, ImGui.GetStyle().ItemSpacing with { X = UiHelpers.ScaleX3 });
ImGui.SetNextItemWidth(ImGui.GetContentRegionAvail().X - 2 * (UiHelpers.ScaleX3 + 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
{
@ -123,6 +134,21 @@ public class FileEditor<T> where T : class, IWritable
}
}, _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();
}

View file

@ -307,7 +307,7 @@ public partial class ModEditWindow
var label = changes ? "Revert Changes" : "Reload Files";
var length = new Vector2(ImGui.CalcTextSize("Revert Changes").X, 0);
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.");

View file

@ -6,7 +6,6 @@ using OtterGui;
using OtterGui.Raii;
using Penumbra.GameData.Files;
using Penumbra.String.Classes;
using Penumbra.UI.AdvancedWindow;
namespace Penumbra.UI.AdvancedWindow;

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

View file

@ -12,6 +12,7 @@ using OtterGui.Raii;
using Penumbra.GameData.Enums;
using Penumbra.GameData.Files;
using Penumbra.Import.Textures;
using Penumbra.Interop.ResourceTree;
using Penumbra.Mods;
using Penumbra.String.Classes;
using Penumbra.UI.Classes;
@ -24,12 +25,14 @@ public partial class ModEditWindow : Window, IDisposable
{
private const string WindowBaseLabel = "###SubModEdit";
private readonly ModEditor _editor;
private readonly Configuration _config;
private readonly ItemSwapTab _itemSwapTab;
private readonly ModEditor _editor;
private readonly Configuration _config;
private readonly ItemSwapTab _itemSwapTab;
private readonly ResourceTreeFactory _resourceTreeFactory;
private readonly DataManager _gameData;
private Mod? _mod;
private Vector2 _iconSize = Vector2.Zero;
private Vector2 _iconSize = Vector2.Zero;
private bool _allowReduplicate;
public void ChangeMod(Mod mod)
@ -136,13 +139,14 @@ public partial class ModEditWindow : Window, IDisposable
DrawSwapTab();
DrawMissingFilesTab();
DrawDuplicatesTab();
DrawQuickImportTab();
DrawMaterialReassignmentTab();
_modelTab.Draw();
_materialTab.Draw();
DrawTextureTab();
_shaderPackageTab.Draw();
using var tab = ImRaii.TabItem("Item Swap (WIP)");
if (tab)
using var tab = ImRaii.TabItem("Item Swap (WIP)");
if (tab)
_itemSwapTab.DrawContent();
}
@ -488,19 +492,21 @@ public partial class ModEditWindow : Window, IDisposable
}
public ModEditWindow(FileDialogService fileDialog, ItemSwapTab itemSwapTab, DataManager gameData,
Configuration config, ModEditor editor)
Configuration config, ModEditor editor, ResourceTreeFactory resourceTreeFactory)
: base(WindowBaseLabel)
{
_itemSwapTab = itemSwapTab;
_config = config;
_editor = editor;
_fileDialog = fileDialog;
_materialTab = new FileEditor<MtrlTab>(gameData, config, _fileDialog, "Materials", ".mtrl",
_itemSwapTab = itemSwapTab;
_config = config;
_editor = editor;
_gameData = gameData;
_resourceTreeFactory = resourceTreeFactory;
_fileDialog = fileDialog;
_materialTab = new FileEditor<MtrlTab>(this, gameData, config, _fileDialog, "Materials", ".mtrl",
() => _editor.Files.Mtrl, DrawMaterialPanel, () => _mod?.ModPath.FullName ?? string.Empty,
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);
_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);
_center = new CombinedTexture(_left, _right);
}

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

View file

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

View file

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

View file

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

View file

@ -7,14 +7,15 @@ namespace Penumbra.UI.Tabs;
public class ConfigTabBar
{
public readonly SettingsTab Settings;
public readonly ModsTab Mods;
public readonly CollectionsTab Collections;
public readonly SettingsTab Settings;
public readonly ModsTab Mods;
public readonly CollectionsTab Collections;
public readonly ChangedItemsTab ChangedItems;
public readonly EffectiveTab Effective;
public readonly DebugTab Debug;
public readonly ResourceTab Resource;
public readonly EffectiveTab Effective;
public readonly DebugTab Debug;
public readonly ResourceTab Resource;
public readonly ResourceWatcher Watcher;
public readonly OnScreenTab OnScreenTab;
public readonly ITab[] Tabs;
@ -22,16 +23,17 @@ public class ConfigTabBar
public TabType SelectTab = TabType.None;
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;
Mods = mods;
Collections = collections;
Settings = settings;
Mods = mods;
Collections = collections;
ChangedItems = changedItems;
Effective = effective;
Debug = debug;
Resource = resource;
Watcher = watcher;
Effective = effective;
Debug = debug;
Resource = resource;
Watcher = watcher;
OnScreenTab = onScreenTab;
Tabs = new ITab[]
{
Settings,
@ -39,6 +41,7 @@ public class ConfigTabBar
Collections,
ChangedItems,
Effective,
OnScreenTab,
Debug,
Resource,
Watcher,
@ -54,14 +57,15 @@ public class ConfigTabBar
private ReadOnlySpan<byte> ToLabel(TabType type)
=> type switch
{
TabType.Settings => Settings.Label,
TabType.Mods => Mods.Label,
TabType.Collections => Collections.Label,
TabType.ChangedItems => ChangedItems.Label,
TabType.Settings => Settings.Label,
TabType.Mods => Mods.Label,
TabType.Collections => Collections.Label,
TabType.ChangedItems => ChangedItems.Label,
TabType.EffectiveChanges => Effective.Label,
TabType.ResourceWatcher => Watcher.Label,
TabType.Debug => Debug.Label,
TabType.ResourceManager => Resource.Label,
_ => ReadOnlySpan<byte>.Empty,
TabType.OnScreen => OnScreenTab.Label,
TabType.ResourceWatcher => Watcher.Label,
TabType.Debug => Debug.Label,
TabType.ResourceManager => Resource.Label,
_ => ReadOnlySpan<byte>.Empty,
};
}

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

View file

@ -3,7 +3,6 @@ using Dalamud.Interface;
using Dalamud.Interface.Windowing;
using Dalamud.Plugin;
using Penumbra.UI;
using Penumbra.UI.Classes;
using Penumbra.UI.AdvancedWindow;
namespace Penumbra;