diff --git a/Penumbra/Interop/CloudApi.cs b/Penumbra/Interop/CloudApi.cs
new file mode 100644
index 00000000..603d4c9f
--- /dev/null
+++ b/Penumbra/Interop/CloudApi.cs
@@ -0,0 +1,47 @@
+namespace Penumbra.Interop;
+
+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];
+ 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.Debug($"Expected {nameof(CfGetSyncRootInfoByPath)} to return {sizeof(long)} bytes, got {length} bytes");
+ return false;
+ }
+
+ Penumbra.Log.Debug($"{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/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/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..ae450bec 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;
+ 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,17 +318,29 @@ 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();
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,
- new Vector2(ImGui.GetContentRegionAvail().X, cellHeight));
+ new Vector2(ImGui.GetContentRegionAvail().X, frameHeight));
}
if (ImGui.IsItemClicked())
@@ -336,20 +354,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 +417,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 +525,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);
}
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()
{
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);