From 8c25ef4b47486df7b79c63d66c78fcf7710f2112 Mon Sep 17 00:00:00 2001 From: Exter-N Date: Sat, 30 Aug 2025 16:53:12 +0200 Subject: [PATCH 1/6] Make the save button ResourceTreeViewer baseline --- .../ModEditWindow.QuickImport.cs | 62 +---------- Penumbra/UI/AdvancedWindow/ModEditWindow.cs | 2 +- .../UI/AdvancedWindow/ResourceTreeViewer.cs | 104 ++++++++++++++---- .../ResourceTreeViewerFactory.cs | 11 +- 4 files changed, 95 insertions(+), 84 deletions(-) diff --git a/Penumbra/UI/AdvancedWindow/ModEditWindow.QuickImport.cs b/Penumbra/UI/AdvancedWindow/ModEditWindow.QuickImport.cs index 72350857..f55ae576 100644 --- a/Penumbra/UI/AdvancedWindow/ModEditWindow.QuickImport.cs +++ b/Penumbra/UI/AdvancedWindow/ModEditWindow.QuickImport.cs @@ -17,7 +17,6 @@ public partial class ModEditWindow private readonly FileDialogService _fileDialog; private readonly ResourceTreeFactory _resourceTreeFactory; private readonly ResourceTreeViewer _quickImportViewer; - private readonly Dictionary _quickImportWritables = new(); private readonly Dictionary<(Utf8GamePath, IWritable?), QuickImportAction> _quickImportActions = new(); private HashSet GetPlayerResourcesOfType(ResourceType type) @@ -56,52 +55,11 @@ public partial class ModEditWindow private void OnQuickImportRefresh() { - _quickImportWritables.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(); 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 const string FallbackOptionName = "the current option"; diff --git a/Penumbra/UI/AdvancedWindow/ModEditWindow.cs b/Penumbra/UI/AdvancedWindow/ModEditWindow.cs index 952d8489..5a0fb849 100644 --- a/Penumbra/UI/AdvancedWindow/ModEditWindow.cs +++ b/Penumbra/UI/AdvancedWindow/ModEditWindow.cs @@ -667,7 +667,7 @@ public partial class ModEditWindow : Window, IDisposable, IUiService _center = new CombinedTexture(_left, _right); _textureSelectCombo = new TextureDrawer.PathSelectCombo(textures, editor, () => GetPlayerResourcesOfType(ResourceType.Tex)); _resourceTreeFactory = resourceTreeFactory; - _quickImportViewer = resourceTreeViewerFactory.Create(2, OnQuickImportRefresh, DrawQuickImportActions); + _quickImportViewer = resourceTreeViewerFactory.Create(1, OnQuickImportRefresh, DrawQuickImportActions); _communicator.ModPathChanged.Subscribe(OnModPathChange, ModPathChanged.Priority.ModEditWindow); IsOpen = _config is { OpenWindowAtStart: true, Ephemeral.AdvancedEditingOpen: true }; if (IsOpen && selection.Mod != null) diff --git a/Penumbra/UI/AdvancedWindow/ResourceTreeViewer.cs b/Penumbra/UI/AdvancedWindow/ResourceTreeViewer.cs index 617ba30f..00003451 100644 --- a/Penumbra/UI/AdvancedWindow/ResourceTreeViewer.cs +++ b/Penumbra/UI/AdvancedWindow/ResourceTreeViewer.cs @@ -4,16 +4,20 @@ using Dalamud.Interface.Colors; using Dalamud.Interface.ImGuiNotification; using Dalamud.Interface.Utility; using Dalamud.Plugin.Services; +using Lumina.Data; using OtterGui; using OtterGui.Classes; +using OtterGui.Compression; using OtterGui.Extensions; using OtterGui.Raii; using OtterGui.Text; using Penumbra.Api.Enums; +using Penumbra.GameData.Files; using Penumbra.GameData.Structs; using Penumbra.Interop.ResourceTree; using Penumbra.Services; using Penumbra.String; +using Penumbra.String.Classes; using Penumbra.UI.Classes; namespace Penumbra.UI.AdvancedWindow; @@ -25,17 +29,20 @@ public class ResourceTreeViewer( IncognitoService incognito, int actionCapacity, Action onRefresh, - Action drawActions, + Action drawActions, CommunicatorService communicator, PcpService pcpService, - IDataManager gameData) + IDataManager gameData, + FileDialogService fileDialog, + FileCompactor compactor) { private const ResourceTreeFactory.Flags ResourceTreeFactoryFlags = ResourceTreeFactory.Flags.RedactExternalPaths | ResourceTreeFactory.Flags.WithUiData | ResourceTreeFactory.Flags.WithOwnership; private readonly HashSet _unfolded = []; - private readonly Dictionary _filterCache = []; + private readonly Dictionary _filterCache = []; + private readonly Dictionary _writableCache = []; private TreeCategory _categoryFilter = AllCategories; private ChangedItemIconFlag _typeFilter = ChangedItemFlagExtensions.AllFlags; @@ -115,7 +122,7 @@ public class ResourceTreeViewer( 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); if (!table) continue; @@ -123,9 +130,8 @@ public class ResourceTreeViewer( 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.TableSetupColumn(string.Empty, ImGuiTableColumnFlags.WidthFixed, + actionCapacity * 3 * ImGuiHelpers.GlobalScale + (actionCapacity + 1) * ImGui.GetFrameHeight()); ImGui.TableHeadersRow(); DrawNodes(tree.Nodes, 0, unchecked(tree.DrawObjectAddress * 31), 0); @@ -211,6 +217,7 @@ public class ResourceTreeViewer( finally { _filterCache.Clear(); + _writableCache.Clear(); _unfolded.Clear(); onRefresh(); } @@ -221,7 +228,6 @@ public class ResourceTreeViewer( { var debugMode = config.DebugMode; var frameHeight = ImGui.GetFrameHeight(); - var cellHeight = actionCapacity > 0 ? frameHeight : 0.0f; foreach (var (resourceNode, index) in resourceNodes.WithIndex()) { @@ -291,7 +297,7 @@ public class ResourceTreeViewer( 0 => "(none)", 1 => resourceNode.GamePath.ToString(), _ => "(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) { var allPaths = string.Join('\n', resourceNode.PossibleGamePaths); @@ -312,7 +318,7 @@ public class ResourceTreeViewer( using (var color = ImRaii.PushColor(ImGuiCol.Text, (hasMod ? ColorId.NewMod : ColorId.DisabledMod).Value())) { ImUtf8.Selectable(modName, false, ImGuiSelectableFlags.AllowItemOverlap, - new Vector2(ImGui.GetContentRegionAvail().X, cellHeight)); + new Vector2(ImGui.GetContentRegionAvail().X, frameHeight)); } ImGui.SameLine(); @@ -322,7 +328,7 @@ public class ResourceTreeViewer( else { ImGui.Selectable(resourceNode.FullPath.ToPath(), false, ImGuiSelectableFlags.AllowItemOverlap, - new Vector2(ImGui.GetContentRegionAvail().X, cellHeight)); + new Vector2(ImGui.GetContentRegionAvail().X, frameHeight)); } if (ImGui.IsItemClicked()) @@ -336,20 +342,17 @@ public class ResourceTreeViewer( else { ImUtf8.Selectable(GetPathStatusLabel(resourceNode.FullPathStatus), false, ImGuiSelectableFlags.Disabled, - new Vector2(ImGui.GetContentRegionAvail().X, cellHeight)); + new Vector2(ImGui.GetContentRegionAvail().X, frameHeight)); ImGuiUtil.HoverTooltip( $"{GetPathStatusDescription(resourceNode.FullPathStatus)}{GetAdditionalDataSuffix(resourceNode.AdditionalData)}"); } mutedColor.Dispose(); - 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)); - } + 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, unchecked(nodePathHash * 31), filterIcon); @@ -402,6 +405,51 @@ public class ResourceTreeViewer( || node.FullPath.InternalName.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 GetPathStatusLabel(ResourceNode.PathStatus status) @@ -465,4 +513,22 @@ public class ResourceTreeViewer( Visible = 1, 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; + } } diff --git a/Penumbra/UI/AdvancedWindow/ResourceTreeViewerFactory.cs b/Penumbra/UI/AdvancedWindow/ResourceTreeViewerFactory.cs index 43b60716..6518ae67 100644 --- a/Penumbra/UI/AdvancedWindow/ResourceTreeViewerFactory.cs +++ b/Penumbra/UI/AdvancedWindow/ResourceTreeViewerFactory.cs @@ -1,5 +1,7 @@ using Dalamud.Plugin.Services; +using OtterGui.Compression; using OtterGui.Services; +using Penumbra.GameData.Files; using Penumbra.Interop.ResourceTree; using Penumbra.Services; @@ -12,8 +14,11 @@ public class ResourceTreeViewerFactory( IncognitoService incognito, CommunicatorService communicator, PcpService pcpService, - IDataManager gameData) : IService + IDataManager gameData, + FileDialogService fileDialog, + FileCompactor compactor) : IService { - public ResourceTreeViewer Create(int actionCapacity, Action onRefresh, Action drawActions) - => new(config, treeFactory, changedItemDrawer, incognito, actionCapacity, onRefresh, drawActions, communicator, pcpService, gameData); + public ResourceTreeViewer Create(int actionCapacity, Action onRefresh, Action drawActions) + => new(config, treeFactory, changedItemDrawer, incognito, actionCapacity, onRefresh, drawActions, communicator, pcpService, gameData, + fileDialog, compactor); } From b3379a97105d37f685dd0686d89d0bf27c1c0807 Mon Sep 17 00:00:00 2001 From: Exter-N Date: Sat, 30 Aug 2025 16:55:20 +0200 Subject: [PATCH 2/6] Stop redacting external paths --- Penumbra/UI/AdvancedWindow/ResourceTreeViewer.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Penumbra/UI/AdvancedWindow/ResourceTreeViewer.cs b/Penumbra/UI/AdvancedWindow/ResourceTreeViewer.cs index 00003451..cb765fcf 100644 --- a/Penumbra/UI/AdvancedWindow/ResourceTreeViewer.cs +++ b/Penumbra/UI/AdvancedWindow/ResourceTreeViewer.cs @@ -37,7 +37,7 @@ public class ResourceTreeViewer( FileCompactor compactor) { private const ResourceTreeFactory.Flags ResourceTreeFactoryFlags = - ResourceTreeFactory.Flags.RedactExternalPaths | ResourceTreeFactory.Flags.WithUiData | ResourceTreeFactory.Flags.WithOwnership; + ResourceTreeFactory.Flags.WithUiData | ResourceTreeFactory.Flags.WithOwnership; private readonly HashSet _unfolded = []; From f3ec4b2e081a4cb477f7c85189ac1525586f97c7 Mon Sep 17 00:00:00 2001 From: Exter-N Date: Sat, 30 Aug 2025 19:19:07 +0200 Subject: [PATCH 3/6] Only display the file name and last dir for externals --- Penumbra/UI/AdvancedWindow/ResourceTreeViewer.cs | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/Penumbra/UI/AdvancedWindow/ResourceTreeViewer.cs b/Penumbra/UI/AdvancedWindow/ResourceTreeViewer.cs index cb765fcf..ae450bec 100644 --- a/Penumbra/UI/AdvancedWindow/ResourceTreeViewer.cs +++ b/Penumbra/UI/AdvancedWindow/ResourceTreeViewer.cs @@ -325,6 +325,18 @@ public class ResourceTreeViewer( ImGui.SetCursorPosX(textPos); 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 { ImGui.Selectable(resourceNode.FullPath.ToPath(), false, ImGuiSelectableFlags.AllowItemOverlap, From 5503bb32e059ed1438ebb139c5da6306e870f3b2 Mon Sep 17 00:00:00 2001 From: Exter-N Date: Sun, 31 Aug 2025 04:13:56 +0200 Subject: [PATCH 4/6] CloudApi testing in Debug tab --- Penumbra/Interop/CloudApi.cs | 29 ++++++++++++++++++++++ Penumbra/UI/Tabs/Debug/DebugTab.cs | 39 ++++++++++++++++++++++++++++++ 2 files changed, 68 insertions(+) create mode 100644 Penumbra/Interop/CloudApi.cs diff --git a/Penumbra/Interop/CloudApi.cs b/Penumbra/Interop/CloudApi.cs new file mode 100644 index 00000000..9ec29fa5 --- /dev/null +++ b/Penumbra/Interop/CloudApi.cs @@ -0,0 +1,29 @@ +namespace Penumbra.Interop; + +public static unsafe partial class CloudApi +{ + private const int CfSyncRootInfoBasic = 0; + + public static bool IsCloudSynced(string path) + { + var buffer = stackalloc long[1]; + var hr = CfGetSyncRootInfoByPath(path, CfSyncRootInfoBasic, buffer, sizeof(long), out var length); + Penumbra.Log.Information($"{nameof(CfGetSyncRootInfoByPath)} returned HRESULT {hr}"); + if (hr < 0) + return false; + + if (length != sizeof(long)) + { + Penumbra.Log.Warning($"Expected {nameof(CfGetSyncRootInfoByPath)} to return {sizeof(long)} bytes, got {length} bytes"); + return false; + } + + Penumbra.Log.Information($"{nameof(CfGetSyncRootInfoByPath)} returned {{ SyncRootFileId = 0x{*buffer:X16} }}"); + + return true; + } + + [LibraryImport("cldapi.dll", StringMarshalling = StringMarshalling.Utf16)] + private static partial int CfGetSyncRootInfoByPath(string filePath, int infoClass, void* infoBuffer, uint infoBufferLength, + out uint returnedLength); +} diff --git a/Penumbra/UI/Tabs/Debug/DebugTab.cs b/Penumbra/UI/Tabs/Debug/DebugTab.cs index d41dd25a..05f77e29 100644 --- a/Penumbra/UI/Tabs/Debug/DebugTab.cs +++ b/Penumbra/UI/Tabs/Debug/DebugTab.cs @@ -9,6 +9,7 @@ using FFXIVClientStructs.FFXIV.Client.Game.Object; using FFXIVClientStructs.FFXIV.Client.System.Resource; using FFXIVClientStructs.FFXIV.Client.UI.Agent; using Dalamud.Bindings.ImGui; +using Dalamud.Interface.Colors; using Microsoft.Extensions.DependencyInjection; using OtterGui; using OtterGui.Classes; @@ -41,6 +42,7 @@ using Penumbra.GameData.Data; using Penumbra.Interop.Hooks.PostProcessing; using Penumbra.Interop.Hooks.ResourceLoading; using Penumbra.GameData.Files.StainMapStructs; +using Penumbra.Interop; using Penumbra.String.Classes; using Penumbra.UI.AdvancedWindow.Materials; @@ -206,6 +208,7 @@ public class DebugTab : Window, ITab, IUiService _hookOverrides.Draw(); DrawPlayerModelInfo(); _globalVariablesDrawer.Draw(); + DrawCloudApi(); DrawDebugTabIpc(); } @@ -1199,6 +1202,42 @@ public class DebugTab : Window, ITab, IUiService } + private string _cloudTesterPath = string.Empty; + private bool? _cloudTesterReturn; + private Exception? _cloudTesterError; + + private void DrawCloudApi() + { + if (!ImUtf8.CollapsingHeader("Cloud API"u8)) + return; + + using var id = ImRaii.PushId("CloudApiTester"u8); + + if (ImUtf8.InputText("Path"u8, ref _cloudTesterPath, flags: ImGuiInputTextFlags.EnterReturnsTrue)) + { + try + { + _cloudTesterReturn = CloudApi.IsCloudSynced(_cloudTesterPath); + _cloudTesterError = null; + } + catch (Exception e) + { + _cloudTesterReturn = null; + _cloudTesterError = e; + } + } + + if (_cloudTesterReturn.HasValue) + ImUtf8.Text($"Is Cloud Synced? {_cloudTesterReturn}"); + + if (_cloudTesterError is not null) + { + using var color = ImRaii.PushColor(ImGuiCol.Text, ImGuiColors.DalamudRed); + ImUtf8.Text($"{_cloudTesterError}"); + } + } + + /// Draw information about IPC options and availability. private void DrawDebugTabIpc() { From d59be1e660e26adce11664ffdbef5631e2511aeb Mon Sep 17 00:00:00 2001 From: Exter-N Date: Sun, 31 Aug 2025 05:25:37 +0200 Subject: [PATCH 5/6] Refine IsCloudSynced --- Penumbra/Interop/CloudApi.cs | 28 +++++++++++++++++++++++----- 1 file changed, 23 insertions(+), 5 deletions(-) diff --git a/Penumbra/Interop/CloudApi.cs b/Penumbra/Interop/CloudApi.cs index 9ec29fa5..603d4c9f 100644 --- a/Penumbra/Interop/CloudApi.cs +++ b/Penumbra/Interop/CloudApi.cs @@ -4,21 +4,39 @@ public static unsafe partial class CloudApi { private const int CfSyncRootInfoBasic = 0; + /// Determines whether a file or directory is cloud-synced using OneDrive or other providers that use the Cloud API. + /// Can be expensive. Callers should cache the result when relevant. public static bool IsCloudSynced(string path) { - var buffer = stackalloc long[1]; - var hr = CfGetSyncRootInfoByPath(path, CfSyncRootInfoBasic, buffer, sizeof(long), out var length); - Penumbra.Log.Information($"{nameof(CfGetSyncRootInfoByPath)} returned HRESULT {hr}"); + var buffer = stackalloc long[1]; + int hr; + uint length; + try + { + hr = CfGetSyncRootInfoByPath(path, CfSyncRootInfoBasic, buffer, sizeof(long), out length); + } + catch (DllNotFoundException) + { + Penumbra.Log.Debug($"{nameof(CfGetSyncRootInfoByPath)} threw DllNotFoundException"); + return false; + } + catch (EntryPointNotFoundException) + { + Penumbra.Log.Debug($"{nameof(CfGetSyncRootInfoByPath)} threw EntryPointNotFoundException"); + return false; + } + + Penumbra.Log.Debug($"{nameof(CfGetSyncRootInfoByPath)} returned HRESULT 0x{hr:X8}"); if (hr < 0) return false; if (length != sizeof(long)) { - Penumbra.Log.Warning($"Expected {nameof(CfGetSyncRootInfoByPath)} to return {sizeof(long)} bytes, got {length} bytes"); + Penumbra.Log.Debug($"Expected {nameof(CfGetSyncRootInfoByPath)} to return {sizeof(long)} bytes, got {length} bytes"); return false; } - Penumbra.Log.Information($"{nameof(CfGetSyncRootInfoByPath)} returned {{ SyncRootFileId = 0x{*buffer:X16} }}"); + Penumbra.Log.Debug($"{nameof(CfGetSyncRootInfoByPath)} returned {{ SyncRootFileId = 0x{*buffer:X16} }}"); return true; } From 2cf60b78cd73f01b6207325a2359663b39745079 Mon Sep 17 00:00:00 2001 From: Exter-N Date: Sun, 31 Aug 2025 06:42:45 +0200 Subject: [PATCH 6/6] Reject and warn about cloud-synced base directories --- Penumbra/Mods/Manager/ModManager.cs | 4 ++++ Penumbra/Penumbra.cs | 13 ++++++++----- Penumbra/UI/Tabs/SettingsTab.cs | 13 +++++++++++++ 3 files changed, 25 insertions(+), 5 deletions(-) diff --git a/Penumbra/Mods/Manager/ModManager.cs b/Penumbra/Mods/Manager/ModManager.cs index 32dac049..77385bbd 100644 --- a/Penumbra/Mods/Manager/ModManager.cs +++ b/Penumbra/Mods/Manager/ModManager.cs @@ -1,5 +1,6 @@ using OtterGui.Services; using Penumbra.Communication; +using Penumbra.Interop; using Penumbra.Mods.Editor; using Penumbra.Mods.Manager.OptionEditor; using Penumbra.Services; @@ -303,6 +304,9 @@ public sealed class ModManager : ModStorage, IDisposable, IService if (!firstTime && _config.ModDirectory != BasePath.FullName) TriggerModDirectoryChange(BasePath.FullName, Valid); } + + if (CloudApi.IsCloudSynced(BasePath.FullName)) + Penumbra.Log.Warning($"Mod base directory {BasePath.FullName} is cloud-synced. This may cause issues."); } private void TriggerModDirectoryChange(string newPath, bool valid) diff --git a/Penumbra/Penumbra.cs b/Penumbra/Penumbra.cs index b22d049d..f036adc7 100644 --- a/Penumbra/Penumbra.cs +++ b/Penumbra/Penumbra.cs @@ -23,6 +23,7 @@ using Dalamud.Plugin.Services; using Lumina.Excel.Sheets; using Penumbra.GameData; using Penumbra.GameData.Data; +using Penumbra.Interop; using Penumbra.Interop.Hooks; using Penumbra.Interop.Hooks.PostProcessing; using Penumbra.Interop.Hooks.ResourceLoading; @@ -211,10 +212,11 @@ public class Penumbra : IDalamudPlugin public string GatherSupportInformation() { - var sb = new StringBuilder(10240); - var exists = _config.ModDirectory.Length > 0 && Directory.Exists(_config.ModDirectory); - var hdrEnabler = _services.GetService(); - var drive = exists ? new DriveInfo(new DirectoryInfo(_config.ModDirectory).Root.FullName) : null; + var sb = new StringBuilder(10240); + var exists = _config.ModDirectory.Length > 0 && Directory.Exists(_config.ModDirectory); + var cloudSynced = exists && CloudApi.IsCloudSynced(_config.ModDirectory); + var hdrEnabler = _services.GetService(); + var drive = exists ? new DriveInfo(new DirectoryInfo(_config.ModDirectory).Root.FullName) : null; sb.AppendLine("**Settings**"); sb.Append($"> **`Plugin Version: `** {_validityChecker.Version}\n"); sb.Append($"> **`Commit Hash: `** {_validityChecker.CommitHash}\n"); @@ -223,7 +225,8 @@ public class Penumbra : IDalamudPlugin sb.Append($"> **`Operating System: `** {(Dalamud.Utility.Util.IsWine() ? "Mac/Linux (Wine)" : "Windows")}\n"); if (Dalamud.Utility.Util.IsWine()) sb.Append($"> **`Locale Environment Variables:`** {CollectLocaleEnvironmentVariables()}\n"); - sb.Append($"> **`Root Directory: `** `{_config.ModDirectory}`, {(exists ? "Exists" : "Not Existing")}\n"); + sb.Append( + $"> **`Root Directory: `** `{_config.ModDirectory}`, {(exists ? "Exists" : "Not Existing")}{(cloudSynced ? ", Cloud-Synced" : "")}\n"); sb.Append( $"> **`Free Drive Space: `** {(drive != null ? Functions.HumanReadableSize(drive.AvailableFreeSpace) : "Unknown")}\n"); sb.Append($"> **`Game Data Files: `** {(_gameData.HasModifiedGameDataFiles ? "Modified" : "Pristine")}\n"); diff --git a/Penumbra/UI/Tabs/SettingsTab.cs b/Penumbra/UI/Tabs/SettingsTab.cs index ded56bb1..308cc471 100644 --- a/Penumbra/UI/Tabs/SettingsTab.cs +++ b/Penumbra/UI/Tabs/SettingsTab.cs @@ -14,6 +14,7 @@ using OtterGui.Text; using OtterGui.Widgets; using Penumbra.Api; using Penumbra.Collections; +using Penumbra.Interop; using Penumbra.Interop.Hooks.PostProcessing; using Penumbra.Interop.Services; using Penumbra.Mods.Manager; @@ -59,6 +60,9 @@ public class SettingsTab : ITab, IUiService private readonly TagButtons _sharedTags = new(); + private string _lastCloudSyncTestedPath = string.Empty; + private bool _lastCloudSyncTestResult = false; + public SettingsTab(IDalamudPluginInterface pluginInterface, Configuration config, FontReloader fontReloader, TutorialService tutorial, Penumbra penumbra, FileDialogService fileDialog, ModManager modManager, ModFileSystemSelector selector, CharacterUtility characterUtility, ResidentResourceManager residentResources, ModExportManager modExportManager, HttpApi httpApi, @@ -208,6 +212,15 @@ public class SettingsTab : ITab, IUiService if (IsSubPathOf(gameDir, newName)) return ("Path is not allowed to be inside your game folder.", false); + if (_lastCloudSyncTestedPath != newName) + { + _lastCloudSyncTestResult = CloudApi.IsCloudSynced(newName); + _lastCloudSyncTestedPath = newName; + } + + if (_lastCloudSyncTestResult) + return ("Path is not allowed to be cloud-synced.", false); + return selected ? ($"Press Enter or Click Here to Save (Current Directory: {old})", true) : ($"Click Here to Save (Current Directory: {old})", true);