Merge branch 'master' into Exter-N/cldapi

This commit is contained in:
Ottermandias 2025-09-01 15:58:22 +02:00
commit e68e821b2a
4 changed files with 108 additions and 85 deletions

View file

@ -17,7 +17,6 @@ public partial class ModEditWindow
private readonly FileDialogService _fileDialog; private readonly FileDialogService _fileDialog;
private readonly ResourceTreeFactory _resourceTreeFactory; private readonly ResourceTreeFactory _resourceTreeFactory;
private readonly ResourceTreeViewer _quickImportViewer; private readonly ResourceTreeViewer _quickImportViewer;
private readonly Dictionary<FullPath, IWritable?> _quickImportWritables = new();
private readonly Dictionary<(Utf8GamePath, IWritable?), QuickImportAction> _quickImportActions = new(); private readonly Dictionary<(Utf8GamePath, IWritable?), QuickImportAction> _quickImportActions = new();
private HashSet<string> GetPlayerResourcesOfType(ResourceType type) private HashSet<string> GetPlayerResourcesOfType(ResourceType type)
@ -56,52 +55,11 @@ public partial class ModEditWindow
private void OnQuickImportRefresh() private void OnQuickImportRefresh()
{ {
_quickImportWritables.Clear();
_quickImportActions.Clear(); _quickImportActions.Clear();
} }
private void DrawQuickImportActions(ResourceNode resourceNode, Vector2 buttonSize) private void DrawQuickImportActions(ResourceNode resourceNode, IWritable? writable, 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 is null ? null : new RawGameFileWritable(file);
}
_quickImportWritables.Add(resourceNode.FullPath, writable);
}
if (ImUtf8.IconButton(FontAwesomeIcon.Save, "Export this file."u8, buttonSize,
resourceNode.FullPath.FullName.Length is 0 || writable is null))
{
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
{
_editor.Compactor.WriteAllBytes(name, writable!.Write());
}
catch (Exception e)
{
Penumbra.Log.Error($"Could not export {fullPathStr}:\n{e}");
}
}, null, false);
}
ImGui.SameLine(); ImGui.SameLine();
if (!_quickImportActions!.TryGetValue((resourceNode.GamePath, writable), out var quickImport)) if (!_quickImportActions!.TryGetValue((resourceNode.GamePath, writable), out var quickImport))
{ {
@ -121,24 +79,6 @@ public partial class ModEditWindow
} }
} }
private record RawFileWritable(string Path) : IWritable
{
public bool Valid
=> true;
public byte[] Write()
=> File.ReadAllBytes(Path);
}
private record RawGameFileWritable(FileResource FileResource) : IWritable
{
public bool Valid
=> true;
public byte[] Write()
=> FileResource.Data;
}
public class QuickImportAction public class QuickImportAction
{ {
public const string FallbackOptionName = "the current option"; public const string FallbackOptionName = "the current option";

View file

@ -667,7 +667,7 @@ public partial class ModEditWindow : Window, IDisposable, IUiService
_center = new CombinedTexture(_left, _right); _center = new CombinedTexture(_left, _right);
_textureSelectCombo = new TextureDrawer.PathSelectCombo(textures, editor, () => GetPlayerResourcesOfType(ResourceType.Tex)); _textureSelectCombo = new TextureDrawer.PathSelectCombo(textures, editor, () => GetPlayerResourcesOfType(ResourceType.Tex));
_resourceTreeFactory = resourceTreeFactory; _resourceTreeFactory = resourceTreeFactory;
_quickImportViewer = resourceTreeViewerFactory.Create(2, OnQuickImportRefresh, DrawQuickImportActions); _quickImportViewer = resourceTreeViewerFactory.Create(1, OnQuickImportRefresh, DrawQuickImportActions);
_communicator.ModPathChanged.Subscribe(OnModPathChange, ModPathChanged.Priority.ModEditWindow); _communicator.ModPathChanged.Subscribe(OnModPathChange, ModPathChanged.Priority.ModEditWindow);
IsOpen = _config is { OpenWindowAtStart: true, Ephemeral.AdvancedEditingOpen: true }; IsOpen = _config is { OpenWindowAtStart: true, Ephemeral.AdvancedEditingOpen: true };
if (IsOpen && selection.Mod != null) if (IsOpen && selection.Mod != null)

View file

@ -4,16 +4,20 @@ using Dalamud.Interface.Colors;
using Dalamud.Interface.ImGuiNotification; using Dalamud.Interface.ImGuiNotification;
using Dalamud.Interface.Utility; using Dalamud.Interface.Utility;
using Dalamud.Plugin.Services; using Dalamud.Plugin.Services;
using Lumina.Data;
using OtterGui; using OtterGui;
using OtterGui.Classes; using OtterGui.Classes;
using OtterGui.Compression;
using OtterGui.Extensions; using OtterGui.Extensions;
using OtterGui.Raii; using OtterGui.Raii;
using OtterGui.Text; using OtterGui.Text;
using Penumbra.Api.Enums; using Penumbra.Api.Enums;
using Penumbra.GameData.Files;
using Penumbra.GameData.Structs; using Penumbra.GameData.Structs;
using Penumbra.Interop.ResourceTree; using Penumbra.Interop.ResourceTree;
using Penumbra.Services; using Penumbra.Services;
using Penumbra.String; using Penumbra.String;
using Penumbra.String.Classes;
using Penumbra.UI.Classes; using Penumbra.UI.Classes;
namespace Penumbra.UI.AdvancedWindow; namespace Penumbra.UI.AdvancedWindow;
@ -25,17 +29,20 @@ public class ResourceTreeViewer(
IncognitoService incognito, IncognitoService incognito,
int actionCapacity, int actionCapacity,
Action onRefresh, Action onRefresh,
Action<ResourceNode, Vector2> drawActions, Action<ResourceNode, IWritable?, Vector2> drawActions,
CommunicatorService communicator, CommunicatorService communicator,
PcpService pcpService, PcpService pcpService,
IDataManager gameData) IDataManager gameData,
FileDialogService fileDialog,
FileCompactor compactor)
{ {
private const ResourceTreeFactory.Flags ResourceTreeFactoryFlags = private const ResourceTreeFactory.Flags ResourceTreeFactoryFlags =
ResourceTreeFactory.Flags.RedactExternalPaths | ResourceTreeFactory.Flags.WithUiData | ResourceTreeFactory.Flags.WithOwnership; ResourceTreeFactory.Flags.WithUiData | ResourceTreeFactory.Flags.WithOwnership;
private readonly HashSet<nint> _unfolded = []; private readonly HashSet<nint> _unfolded = [];
private readonly Dictionary<nint, NodeVisibility> _filterCache = []; private readonly Dictionary<nint, NodeVisibility> _filterCache = [];
private readonly Dictionary<FullPath, IWritable?> _writableCache = [];
private TreeCategory _categoryFilter = AllCategories; private TreeCategory _categoryFilter = AllCategories;
private ChangedItemIconFlag _typeFilter = ChangedItemFlagExtensions.AllFlags; private ChangedItemIconFlag _typeFilter = ChangedItemFlagExtensions.AllFlags;
@ -115,7 +122,7 @@ public class ResourceTreeViewer(
ImUtf8.InputText("##note"u8, ref _note, "Export note..."u8); ImUtf8.InputText("##note"u8, ref _note, "Export note..."u8);
using var table = ImRaii.Table("##ResourceTree", actionCapacity > 0 ? 4 : 3, using var table = ImRaii.Table("##ResourceTree", 4,
ImGuiTableFlags.SizingFixedFit | ImGuiTableFlags.RowBg); ImGuiTableFlags.SizingFixedFit | ImGuiTableFlags.RowBg);
if (!table) if (!table)
continue; continue;
@ -123,9 +130,8 @@ public class ResourceTreeViewer(
ImGui.TableSetupColumn(string.Empty, ImGuiTableColumnFlags.WidthStretch, 0.2f); ImGui.TableSetupColumn(string.Empty, ImGuiTableColumnFlags.WidthStretch, 0.2f);
ImGui.TableSetupColumn("Game Path", ImGuiTableColumnFlags.WidthStretch, 0.3f); ImGui.TableSetupColumn("Game Path", ImGuiTableColumnFlags.WidthStretch, 0.3f);
ImGui.TableSetupColumn("Actual Path", ImGuiTableColumnFlags.WidthStretch, 0.5f); ImGui.TableSetupColumn("Actual Path", ImGuiTableColumnFlags.WidthStretch, 0.5f);
if (actionCapacity > 0)
ImGui.TableSetupColumn(string.Empty, ImGuiTableColumnFlags.WidthFixed, ImGui.TableSetupColumn(string.Empty, ImGuiTableColumnFlags.WidthFixed,
(actionCapacity - 1) * 3 * ImGuiHelpers.GlobalScale + actionCapacity * ImGui.GetFrameHeight()); actionCapacity * 3 * ImGuiHelpers.GlobalScale + (actionCapacity + 1) * ImGui.GetFrameHeight());
ImGui.TableHeadersRow(); ImGui.TableHeadersRow();
DrawNodes(tree.Nodes, 0, unchecked(tree.DrawObjectAddress * 31), 0); DrawNodes(tree.Nodes, 0, unchecked(tree.DrawObjectAddress * 31), 0);
@ -211,6 +217,7 @@ public class ResourceTreeViewer(
finally finally
{ {
_filterCache.Clear(); _filterCache.Clear();
_writableCache.Clear();
_unfolded.Clear(); _unfolded.Clear();
onRefresh(); onRefresh();
} }
@ -221,7 +228,6 @@ public class ResourceTreeViewer(
{ {
var debugMode = config.DebugMode; var debugMode = config.DebugMode;
var frameHeight = ImGui.GetFrameHeight(); var frameHeight = ImGui.GetFrameHeight();
var cellHeight = actionCapacity > 0 ? frameHeight : 0.0f;
foreach (var (resourceNode, index) in resourceNodes.WithIndex()) foreach (var (resourceNode, index) in resourceNodes.WithIndex())
{ {
@ -291,7 +297,7 @@ public class ResourceTreeViewer(
0 => "(none)", 0 => "(none)",
1 => resourceNode.GamePath.ToString(), 1 => resourceNode.GamePath.ToString(),
_ => "(multiple)", _ => "(multiple)",
}, false, hasGamePaths ? 0 : ImGuiSelectableFlags.Disabled, new Vector2(ImGui.GetContentRegionAvail().X, cellHeight)); }, false, hasGamePaths ? 0 : ImGuiSelectableFlags.Disabled, new Vector2(ImGui.GetContentRegionAvail().X, frameHeight));
if (hasGamePaths) if (hasGamePaths)
{ {
var allPaths = string.Join('\n', resourceNode.PossibleGamePaths); var allPaths = string.Join('\n', resourceNode.PossibleGamePaths);
@ -312,17 +318,29 @@ public class ResourceTreeViewer(
using (var color = ImRaii.PushColor(ImGuiCol.Text, (hasMod ? ColorId.NewMod : ColorId.DisabledMod).Value())) using (var color = ImRaii.PushColor(ImGuiCol.Text, (hasMod ? ColorId.NewMod : ColorId.DisabledMod).Value()))
{ {
ImUtf8.Selectable(modName, false, ImGuiSelectableFlags.AllowItemOverlap, ImUtf8.Selectable(modName, false, ImGuiSelectableFlags.AllowItemOverlap,
new Vector2(ImGui.GetContentRegionAvail().X, cellHeight)); new Vector2(ImGui.GetContentRegionAvail().X, frameHeight));
} }
ImGui.SameLine(); ImGui.SameLine();
ImGui.SetCursorPosX(textPos); ImGui.SetCursorPosX(textPos);
ImUtf8.Text(resourceNode.ModRelativePath); ImUtf8.Text(resourceNode.ModRelativePath);
} }
else if (resourceNode.FullPath.IsRooted)
{
var path = resourceNode.FullPath.FullName;
var lastDirectorySeparator = path.LastIndexOf('\\');
var secondLastDirectorySeparator = lastDirectorySeparator > 0
? path.LastIndexOf('\\', lastDirectorySeparator - 1)
: -1;
if (secondLastDirectorySeparator >= 0)
path = $"…{path.AsSpan(secondLastDirectorySeparator)}";
ImGui.Selectable(path.AsSpan(), false, ImGuiSelectableFlags.AllowItemOverlap,
new Vector2(ImGui.GetContentRegionAvail().X, frameHeight));
}
else else
{ {
ImGui.Selectable(resourceNode.FullPath.ToPath(), false, ImGuiSelectableFlags.AllowItemOverlap, ImGui.Selectable(resourceNode.FullPath.ToPath(), false, ImGuiSelectableFlags.AllowItemOverlap,
new Vector2(ImGui.GetContentRegionAvail().X, cellHeight)); new Vector2(ImGui.GetContentRegionAvail().X, frameHeight));
} }
if (ImGui.IsItemClicked()) if (ImGui.IsItemClicked())
@ -336,20 +354,17 @@ public class ResourceTreeViewer(
else else
{ {
ImUtf8.Selectable(GetPathStatusLabel(resourceNode.FullPathStatus), false, ImGuiSelectableFlags.Disabled, ImUtf8.Selectable(GetPathStatusLabel(resourceNode.FullPathStatus), false, ImGuiSelectableFlags.Disabled,
new Vector2(ImGui.GetContentRegionAvail().X, cellHeight)); new Vector2(ImGui.GetContentRegionAvail().X, frameHeight));
ImGuiUtil.HoverTooltip( ImGuiUtil.HoverTooltip(
$"{GetPathStatusDescription(resourceNode.FullPathStatus)}{GetAdditionalDataSuffix(resourceNode.AdditionalData)}"); $"{GetPathStatusDescription(resourceNode.FullPathStatus)}{GetAdditionalDataSuffix(resourceNode.AdditionalData)}");
} }
mutedColor.Dispose(); mutedColor.Dispose();
if (actionCapacity > 0)
{
ImGui.TableNextColumn(); ImGui.TableNextColumn();
using var spacing = ImRaii.PushStyle(ImGuiStyleVar.ItemSpacing, using var spacing = ImRaii.PushStyle(ImGuiStyleVar.ItemSpacing,
ImGui.GetStyle().ItemSpacing with { X = 3 * ImGuiHelpers.GlobalScale }); ImGui.GetStyle().ItemSpacing with { X = 3 * ImGuiHelpers.GlobalScale });
drawActions(resourceNode, new Vector2(frameHeight)); DrawActions(resourceNode, new Vector2(frameHeight));
}
if (unfolded) if (unfolded)
DrawNodes(resourceNode.Children, level + 1, unchecked(nodePathHash * 31), filterIcon); DrawNodes(resourceNode.Children, level + 1, unchecked(nodePathHash * 31), filterIcon);
@ -402,6 +417,51 @@ public class ResourceTreeViewer(
|| node.FullPath.InternalName.ToString().Contains(_nodeFilter, StringComparison.OrdinalIgnoreCase) || node.FullPath.InternalName.ToString().Contains(_nodeFilter, StringComparison.OrdinalIgnoreCase)
|| Array.Exists(node.PossibleGamePaths, path => path.Path.ToString().Contains(_nodeFilter, StringComparison.OrdinalIgnoreCase)); || Array.Exists(node.PossibleGamePaths, path => path.Path.ToString().Contains(_nodeFilter, StringComparison.OrdinalIgnoreCase));
} }
void DrawActions(ResourceNode resourceNode, Vector2 buttonSize)
{
if (!_writableCache!.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 is null ? null : new RawGameFileWritable(file);
}
_writableCache.Add(resourceNode.FullPath, writable);
}
if (ImUtf8.IconButton(FontAwesomeIcon.Save, "Export this file."u8, buttonSize,
resourceNode.FullPath.FullName.Length is 0 || writable is null))
{
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
{
compactor.WriteAllBytes(name, writable!.Write());
}
catch (Exception e)
{
Penumbra.Log.Error($"Could not export {fullPathStr}:\n{e}");
}
}, null, false);
}
drawActions(resourceNode, writable, new Vector2(frameHeight));
}
} }
private static ReadOnlySpan<byte> GetPathStatusLabel(ResourceNode.PathStatus status) private static ReadOnlySpan<byte> GetPathStatusLabel(ResourceNode.PathStatus status)
@ -465,4 +525,22 @@ public class ResourceTreeViewer(
Visible = 1, Visible = 1,
DescendentsOnly = 2, DescendentsOnly = 2,
} }
private record RawFileWritable(string Path) : IWritable
{
public bool Valid
=> true;
public byte[] Write()
=> File.ReadAllBytes(Path);
}
private record RawGameFileWritable(FileResource FileResource) : IWritable
{
public bool Valid
=> true;
public byte[] Write()
=> FileResource.Data;
}
} }

View file

@ -1,5 +1,7 @@
using Dalamud.Plugin.Services; using Dalamud.Plugin.Services;
using OtterGui.Compression;
using OtterGui.Services; using OtterGui.Services;
using Penumbra.GameData.Files;
using Penumbra.Interop.ResourceTree; using Penumbra.Interop.ResourceTree;
using Penumbra.Services; using Penumbra.Services;
@ -12,8 +14,11 @@ public class ResourceTreeViewerFactory(
IncognitoService incognito, IncognitoService incognito,
CommunicatorService communicator, CommunicatorService communicator,
PcpService pcpService, PcpService pcpService,
IDataManager gameData) : IService IDataManager gameData,
FileDialogService fileDialog,
FileCompactor compactor) : IService
{ {
public ResourceTreeViewer Create(int actionCapacity, Action onRefresh, Action<ResourceNode, Vector2> drawActions) public ResourceTreeViewer Create(int actionCapacity, Action onRefresh, Action<ResourceNode, IWritable?, Vector2> drawActions)
=> new(config, treeFactory, changedItemDrawer, incognito, actionCapacity, onRefresh, drawActions, communicator, pcpService, gameData); => new(config, treeFactory, changedItemDrawer, incognito, actionCapacity, onRefresh, drawActions, communicator, pcpService, gameData,
fileDialog, compactor);
} }