diff --git a/Luna b/Luna index c764db88..78216203 160000 --- a/Luna +++ b/Luna @@ -1 +1 @@ -Subproject commit c764db88097c88cd49f2bed4f60268d617f97fdb +Subproject commit 78216203f4570a6194fce9422204d8abb536c828 diff --git a/OtterGui b/OtterGui index a63f6735..9af1e5fc 160000 --- a/OtterGui +++ b/OtterGui @@ -1 +1 @@ -Subproject commit a63f6735cf4bed4f7502a022a10378607082b770 +Subproject commit 9af1e5fce4c13ef98842807d4f593dec8ae80c87 diff --git a/Penumbra.Api b/Penumbra.Api index c23ee05c..97fe622e 160000 --- a/Penumbra.Api +++ b/Penumbra.Api @@ -1 +1 @@ -Subproject commit c23ee05c1e9fa103eaa52e6aa7e855ef568ee669 +Subproject commit 97fe622e4ec0a5469a26aba8a8c3933fa8ef7fd6 diff --git a/Penumbra.GameData b/Penumbra.GameData index cf3d868e..182cca56 160000 --- a/Penumbra.GameData +++ b/Penumbra.GameData @@ -1 +1 @@ -Subproject commit cf3d868eeeb4ea3ea728ae15a8d09ec127ce80e9 +Subproject commit 182cca56a49411430233d73d7a8a6bb3d983f8f0 diff --git a/Penumbra/Configuration.cs b/Penumbra/Configuration.cs index e3ca27a7..454963fb 100644 --- a/Penumbra/Configuration.cs +++ b/Penumbra/Configuration.cs @@ -51,7 +51,6 @@ public class Configuration : IPluginConfiguration, ISavable, IService public string ModDirectory { get; set; } = string.Empty; public string ExportDirectory { get; set; } = string.Empty; - public string WatchDirectory { get; set; } = string.Empty; public bool? UseCrashHandler { get; set; } = null; public bool OpenWindowAtStart { get; set; } = false; @@ -75,8 +74,6 @@ public class Configuration : IPluginConfiguration, ISavable, IService public bool HideRedrawBar { get; set; } = false; public bool HideMachinistOffhandFromChangedItems { get; set; } = true; public bool DefaultTemporaryMode { get; set; } = false; - public bool EnableDirectoryWatch { get; set; } = false; - public bool EnableAutomaticModImport { get; set; } = false; public bool EnableCustomShapes { get; set; } = true; public PcpSettings PcpSettings = new(); public RenameField ShowRename { get; set; } = RenameField.BothDataPrio; diff --git a/Penumbra/EphemeralConfig.cs b/Penumbra/EphemeralConfig.cs index caf34027..7a5091be 100644 --- a/Penumbra/EphemeralConfig.cs +++ b/Penumbra/EphemeralConfig.cs @@ -41,7 +41,7 @@ public class EphemeralConfig : ISavable, IDisposable, IService public ChangedItemIconFlag ChangedItemFilter { get; set; } = ChangedItemFlagExtensions.DefaultFlags; public bool FixMainWindow { get; set; } = false; public string LastModPath { get; set; } = string.Empty; - public HashSet AdvancedEditingOpenForModPaths { get; set; } = []; + public bool AdvancedEditingOpen { get; set; } = false; public bool ForceRedrawOnFileChange { get; set; } = false; public bool IncognitoMode { get; set; } = false; diff --git a/Penumbra/Interop/PathResolving/CutsceneService.cs b/Penumbra/Interop/PathResolving/CutsceneService.cs index 892688b6..23d1b9bf 100644 --- a/Penumbra/Interop/PathResolving/CutsceneService.cs +++ b/Penumbra/Interop/PathResolving/CutsceneService.cs @@ -73,7 +73,6 @@ public sealed class CutsceneService : Luna.IRequiredService, IDisposable return false; _copiedCharacters[copyIdx - CutsceneStartIdx] = (short)parentIdx; - _objects.InvokeRequiredUpdates(); return true; } diff --git a/Penumbra/Mods/Manager/ModStorage.cs b/Penumbra/Mods/Manager/ModStorage.cs index 15d86866..ea82582d 100644 --- a/Penumbra/Mods/Manager/ModStorage.cs +++ b/Penumbra/Mods/Manager/ModStorage.cs @@ -1,88 +1,76 @@ -using OtterGui.Classes; -using OtterGui.Widgets; - -namespace Penumbra.Mods.Manager; - -public class ModCombo(Func> generator) : FilterComboCache(generator, MouseWheelType.None, Penumbra.Log) -{ - protected override bool IsVisible(int globalIndex, LowerString filter) - => Items[globalIndex].Name.Contains(filter); - - protected override string ToString(Mod obj) - => obj.Name; -} - -public class ModStorage : IReadOnlyList -{ - /// The actual list of mods. - protected readonly List Mods = []; - - public int Count - => Mods.Count; - - public Mod this[int idx] - => Mods[idx]; - - public Mod this[Index idx] - => Mods[idx]; - - public IEnumerator GetEnumerator() - => Mods.GetEnumerator(); - - IEnumerator IEnumerable.GetEnumerator() - => GetEnumerator(); - - /// - /// Try to obtain a mod by its directory name (unique identifier). - /// - public bool TryGetMod(string identifier, [NotNullWhen(true)] out Mod? mod) - { - mod = this.FirstOrDefault(m => string.Equals(m.Identifier, identifier, StringComparison.OrdinalIgnoreCase)); - return mod is not null; - } - - /// - /// Try to obtain a mod by its directory name (unique identifier, preferred), - /// or the first mod of the given name if no directory fits. - /// - public bool TryGetMod(string identifier, string modName, [NotNullWhen(true)] out Mod? mod) - { - if (modName.Length is 0) - return TryGetMod(identifier, out mod); - - mod = null; - foreach (var m in Mods) - { - if (string.Equals(m.Identifier, identifier, StringComparison.OrdinalIgnoreCase)) - { - mod = m; - return true; - } - - if (m.Name == modName) - mod ??= m; - } - - return mod != null; - } - - /// - /// An easily accessible set of new mods. - /// Mods are added when they are created or imported. - /// Mods are removed when they are deleted or when they are toggled in any collection. - /// Also gets cleared on mod rediscovery. - /// - private readonly HashSet _newMods = []; - - public bool IsNew(Mod mod) - => _newMods.Contains(mod); - - public void SetNew(Mod mod) - => _newMods.Add(mod); - - public void SetKnown(Mod mod) - => _newMods.Remove(mod); - - public void ClearNewMods() - => _newMods.Clear(); -} +using OtterGui.Classes; +using OtterGui.Widgets; + +namespace Penumbra.Mods.Manager; + +public class ModCombo(Func> generator) : FilterComboCache(generator, MouseWheelType.None, Penumbra.Log) +{ + protected override bool IsVisible(int globalIndex, LowerString filter) + => Items[globalIndex].Name.Contains(filter); + + protected override string ToString(Mod obj) + => obj.Name; +} + +public class ModStorage : IReadOnlyList +{ + /// The actual list of mods. + protected readonly List Mods = []; + + public int Count + => Mods.Count; + + public Mod this[int idx] + => Mods[idx]; + + public Mod this[Index idx] + => Mods[idx]; + + public IEnumerator GetEnumerator() + => Mods.GetEnumerator(); + + IEnumerator IEnumerable.GetEnumerator() + => GetEnumerator(); + + /// + /// Try to obtain a mod by its directory name (unique identifier, preferred), + /// or the first mod of the given name if no directory fits. + /// + public bool TryGetMod(string identifier, string modName, [NotNullWhen(true)] out Mod? mod) + { + mod = null; + foreach (var m in Mods) + { + if (string.Equals(m.Identifier, identifier, StringComparison.OrdinalIgnoreCase)) + { + mod = m; + return true; + } + + if (m.Name == modName) + mod ??= m; + } + + return mod != null; + } + + /// + /// An easily accessible set of new mods. + /// Mods are added when they are created or imported. + /// Mods are removed when they are deleted or when they are toggled in any collection. + /// Also gets cleared on mod rediscovery. + /// + private readonly HashSet _newMods = []; + + public bool IsNew(Mod mod) + => _newMods.Contains(mod); + + public void SetNew(Mod mod) + => _newMods.Add(mod); + + public void SetKnown(Mod mod) + => _newMods.Remove(mod); + + public void ClearNewMods() + => _newMods.Clear(); +} diff --git a/Penumbra/Mods/ModSelection.cs b/Penumbra/Mods/ModSelection.cs index 294d8244..986f074b 100644 --- a/Penumbra/Mods/ModSelection.cs +++ b/Penumbra/Mods/ModSelection.cs @@ -27,8 +27,8 @@ public class ModSelection : EventBase 0 && mods.TryGetMod(config.LastModPath, out var mod)) - SelectMod(mod); + if (_config.LastModPath.Length > 0) + SelectMod(mods.FirstOrDefault(m => string.Equals(m.Identifier, config.LastModPath, StringComparison.OrdinalIgnoreCase))); _communicator.CollectionChange.Subscribe(OnCollectionChange, CollectionChange.Priority.ModSelection); _communicator.CollectionInheritanceChanged.Subscribe(OnInheritanceChange, CollectionInheritanceChanged.Priority.ModSelection); diff --git a/Penumbra/Penumbra.cs b/Penumbra/Penumbra.cs index 24066e6e..770f809a 100644 --- a/Penumbra/Penumbra.cs +++ b/Penumbra/Penumbra.cs @@ -141,14 +141,8 @@ public class Penumbra : IDalamudPlugin if (!_disposed) { _windowSystem = system; - if (_config is { OpenWindowAtStart: true, Ephemeral.AdvancedEditingOpenForModPaths.Count: > 0 }) - { - var mods = _services.GetService(); - var editWindowFactory = _services.GetService(); - foreach (var identifier in _config.Ephemeral.AdvancedEditingOpenForModPaths) - if (mods.TryGetMod(identifier, out var mod)) - editWindowFactory.OpenForMod(mod); - } + if (_config is { OpenWindowAtStart: true, Ephemeral.AdvancedEditingOpen: true } && _services.GetService().Mod is {} mod) + _services.GetService().OpenForMod(mod); } else system.Dispose(); diff --git a/Penumbra/Services/FileWatcher.cs b/Penumbra/Services/FileWatcher.cs deleted file mode 100644 index 92f5bebe..00000000 --- a/Penumbra/Services/FileWatcher.cs +++ /dev/null @@ -1,209 +0,0 @@ -using Luna; -using Penumbra.Mods.Manager; - -namespace Penumbra.Services; - -public class FileWatcher : IDisposable, IService -{ - private readonly ConcurrentSet _pending = new(StringComparer.OrdinalIgnoreCase); - private readonly ModImportManager _modImportManager; - private readonly MessageService _messageService; - private readonly Configuration _config; - - private bool _pausedConsumer; - private FileSystemWatcher? _fsw; - private CancellationTokenSource? _cts = new(); - private Task? _consumer; - - public FileWatcher(ModImportManager modImportManager, MessageService messageService, Configuration config) - { - _modImportManager = modImportManager; - _messageService = messageService; - _config = config; - - if (_config.EnableDirectoryWatch) - { - SetupFileWatcher(_config.WatchDirectory); - SetupConsumerTask(); - } - } - - public void Toggle(bool value) - { - if (_config.EnableDirectoryWatch == value) - return; - - _config.EnableDirectoryWatch = value; - _config.Save(); - if (value) - { - SetupFileWatcher(_config.WatchDirectory); - SetupConsumerTask(); - } - else - { - EndFileWatcher(); - EndConsumerTask(); - } - } - - internal void PauseConsumer(bool pause) - => _pausedConsumer = pause; - - private void EndFileWatcher() - { - if (_fsw is null) - return; - - _fsw.Dispose(); - _fsw = null; - } - - private void SetupFileWatcher(string directory) - { - EndFileWatcher(); - _fsw = new FileSystemWatcher - { - IncludeSubdirectories = false, - NotifyFilter = NotifyFilters.FileName | NotifyFilters.CreationTime, - InternalBufferSize = 32 * 1024, - }; - - // Only wake us for the exact patterns we care about - _fsw.Filters.Add("*.pmp"); - _fsw.Filters.Add("*.pcp"); - _fsw.Filters.Add("*.ttmp"); - _fsw.Filters.Add("*.ttmp2"); - - _fsw.Created += OnPath; - _fsw.Renamed += OnPath; - UpdateDirectory(directory); - } - - - private void EndConsumerTask() - { - if (_cts is not null) - { - _cts.Cancel(); - _cts = null; - } - - _consumer = null; - } - - private void SetupConsumerTask() - { - EndConsumerTask(); - _cts = new CancellationTokenSource(); - _consumer = Task.Factory.StartNew( - () => ConsumerLoopAsync(_cts.Token), - _cts.Token, TaskCreationOptions.LongRunning, TaskScheduler.Default).Unwrap(); - } - - public void UpdateDirectory(string newPath) - { - if (_config.WatchDirectory != newPath) - { - _config.WatchDirectory = newPath; - _config.Save(); - } - - if (_fsw is null) - return; - - _fsw.EnableRaisingEvents = false; - if (!Directory.Exists(newPath) || newPath.Length is 0) - { - _fsw.Path = string.Empty; - } - else - { - _fsw.Path = newPath; - _fsw.EnableRaisingEvents = true; - } - } - - private void OnPath(object? sender, FileSystemEventArgs e) - => _pending.TryAdd(e.FullPath); - - private async Task ConsumerLoopAsync(CancellationToken token) - { - while (true) - { - var path = _pending.FirstOrDefault(); - if (path is null || _pausedConsumer) - { - await Task.Delay(500, token).ConfigureAwait(false); - continue; - } - - try - { - await ProcessOneAsync(path, token).ConfigureAwait(false); - } - catch (OperationCanceledException) - { - Penumbra.Log.Debug("[FileWatcher] Canceled via Token."); - } - catch (Exception ex) - { - Penumbra.Log.Warning($"[FileWatcher] Error during Processing: {ex}"); - } - finally - { - _pending.TryRemove(path); - } - } - } - - private async Task ProcessOneAsync(string path, CancellationToken token) - { - // Downloads often finish via rename; file may be locked briefly. - // Wait until it exists and is readable; also require two stable size checks. - const int maxTries = 40; - long lastLen = -1; - - for (var i = 0; i < maxTries && !token.IsCancellationRequested; i++) - { - if (!File.Exists(path)) - { - await Task.Delay(100, token); - continue; - } - - try - { - var fi = new FileInfo(path); - var len = fi.Length; - if (len > 0 && len == lastLen) - { - if (_config.EnableAutomaticModImport) - _modImportManager.AddUnpack(path); - else - _messageService.AddMessage(new InstallNotification(_modImportManager, path), false); - return; - } - - lastLen = len; - } - catch (IOException) - { - Penumbra.Log.Debug($"[FileWatcher] File is still being written to."); - } - catch (UnauthorizedAccessException) - { - Penumbra.Log.Debug($"[FileWatcher] File is locked."); - } - - await Task.Delay(150, token); - } - } - - - public void Dispose() - { - EndConsumerTask(); - EndFileWatcher(); - } -} diff --git a/Penumbra/Services/InstallNotification.cs b/Penumbra/Services/InstallNotification.cs deleted file mode 100644 index 7b0da414..00000000 --- a/Penumbra/Services/InstallNotification.cs +++ /dev/null @@ -1,49 +0,0 @@ -using Dalamud.Bindings.ImGui; -using Dalamud.Game.Text.SeStringHandling; -using Dalamud.Interface.ImGuiNotification; -using Dalamud.Interface.ImGuiNotification.EventArgs; -using ImSharp; -using Penumbra.Mods.Manager; - -namespace Penumbra.Services; - -public class InstallNotification(ModImportManager modImportManager, string filePath) : Luna.IMessage -{ - public NotificationType NotificationType - => NotificationType.Info; - - public string NotificationMessage - => "A new mod has been found!"; - - public TimeSpan NotificationDuration - => TimeSpan.MaxValue; - - public string NotificationTitle { get; } = Path.GetFileNameWithoutExtension(filePath); - - public string LogMessage - => $"A new mod has been found: {Path.GetFileName(filePath)}"; - - public SeString ChatMessage - => SeString.Empty; - - public StringU8 StoredMessage - => StringU8.Empty; - - public StringU8 StoredTooltip - => StringU8.Empty; - - public void OnNotificationActions(INotificationDrawArgs args) - { - var region = Im.ContentRegion.Available; - var buttonSize = new Vector2((region.X - Im.Style.ItemSpacing.X) / 2, 0); - if (Im.Button("Install"u8, buttonSize)) - { - modImportManager.AddUnpack(filePath); - args.Notification.DismissNow(); - } - - ImGui.SameLine(); - if (Im.Button("Ignore"u8, buttonSize)) - args.Notification.DismissNow(); - } -} diff --git a/Penumbra/UI/AdvancedWindow/ModEditWindow.cs b/Penumbra/UI/AdvancedWindow/ModEditWindow.cs index 73ee867b..d6657420 100644 --- a/Penumbra/UI/AdvancedWindow/ModEditWindow.cs +++ b/Penumbra/UI/AdvancedWindow/ModEditWindow.cs @@ -180,8 +180,8 @@ public partial class ModEditWindow : IndexedWindow, IDisposable public override void OnClose() { base.OnClose(); - if (Mod is not null && _config.Ephemeral.AdvancedEditingOpenForModPaths.Remove(Mod.Identifier)) - _config.Ephemeral.Save(); + _config.Ephemeral.AdvancedEditingOpen = false; + _config.Ephemeral.Save(); AppendTask(() => { _left.Dispose(); @@ -194,8 +194,11 @@ public partial class ModEditWindow : IndexedWindow, IDisposable public override void Draw() { - if (Mod is not null && _config.Ephemeral.AdvancedEditingOpenForModPaths.Add(Mod.Identifier)) + if (!_config.Ephemeral.AdvancedEditingOpen) + { + _config.Ephemeral.AdvancedEditingOpen = true; _config.Ephemeral.Save(); + } if (IsLoading) { diff --git a/Penumbra/UI/Tabs/SettingsTab.cs b/Penumbra/UI/Tabs/SettingsTab.cs index 6980117d..08a3b472 100644 --- a/Penumbra/UI/Tabs/SettingsTab.cs +++ b/Penumbra/UI/Tabs/SettingsTab.cs @@ -36,7 +36,6 @@ public class SettingsTab : ITab, IUiService private readonly Penumbra _penumbra; private readonly FileDialogService _fileDialog; private readonly ModManager _modManager; - private readonly FileWatcher _fileWatcher; private readonly ModExportManager _modExportManager; private readonly ModFileSystemSelector _selector; private readonly CharacterUtility _characterUtility; @@ -65,8 +64,7 @@ public class SettingsTab : ITab, IUiService public SettingsTab(IDalamudPluginInterface pluginInterface, Configuration config, FontReloader fontReloader, TutorialService tutorial, Penumbra penumbra, FileDialogService fileDialog, ModManager modManager, ModFileSystemSelector selector, - CharacterUtility characterUtility, ResidentResourceManager residentResources, ModExportManager modExportManager, - FileWatcher fileWatcher, HttpApi httpApi, + CharacterUtility characterUtility, ResidentResourceManager residentResources, ModExportManager modExportManager, HttpApi httpApi, DalamudSubstitutionProvider dalamudSubstitutionProvider, FileCompactor compactor, DalamudConfigService dalamudConfig, IDataManager gameData, PredefinedTagManager predefinedTagConfig, CrashHandlerService crashService, MigrationSectionDrawer migrationDrawer, CollectionAutoSelector autoSelector, CleanupService cleanupService, @@ -83,7 +81,6 @@ public class SettingsTab : ITab, IUiService _characterUtility = characterUtility; _residentResources = residentResources; _modExportManager = modExportManager; - _fileWatcher = fileWatcher; _httpApi = httpApi; _dalamudSubstitutionProvider = dalamudSubstitutionProvider; _compactor = compactor; @@ -725,13 +722,6 @@ public class SettingsTab : ITab, IUiService DrawPcpFolder(); DrawPcpExtension(); DrawDefaultModExportPath(); - Checkbox("Enable Directory Watcher", - "Enables a File Watcher that automatically listens for Mod files that enter a specified directory, causing Penumbra to open a popup to import these mods.", - _config.EnableDirectoryWatch, _fileWatcher.Toggle); - Checkbox("Enable Fully Automatic Import", - "Uses the File Watcher in order to skip the query popup and automatically import any new mods.", - _config.EnableAutomaticModImport, v => _config.EnableAutomaticModImport = v); - DrawFileWatcherPath(); } @@ -811,46 +801,6 @@ public class SettingsTab : ITab, IUiService + "Keep this empty to use the root directory."); } - private string? _tempWatchDirectory; - - /// Draw input for the Automatic Mod import path. - private void DrawFileWatcherPath() - { - var tmp = _tempWatchDirectory ?? _config.WatchDirectory; - var spacing = new Vector2(UiHelpers.ScaleX3); - using var style = ImRaii.PushStyle(ImGuiStyleVar.ItemSpacing, spacing); - ImGui.SetNextItemWidth(UiHelpers.InputTextMinusButton3); - if (ImGui.InputText("##fileWatchPath", ref tmp, 256)) - _tempWatchDirectory = tmp; - - if (ImGui.IsItemDeactivated() && _tempWatchDirectory is not null) - { - if (ImGui.IsItemDeactivatedAfterEdit()) - _fileWatcher.UpdateDirectory(_tempWatchDirectory); - _tempWatchDirectory = null; - } - - ImGui.SameLine(); - if (ImGuiUtil.DrawDisabledButton($"{FontAwesomeIcon.Folder.ToIconString()}##fileWatch", UiHelpers.IconButtonSize, - "Select a directory via dialog.", false, true)) - { - var startDir = _config.WatchDirectory.Length > 0 && Directory.Exists(_config.WatchDirectory) - ? _config.WatchDirectory - : Directory.Exists(_config.ModDirectory) - ? _config.ModDirectory - : null; - _fileDialog.OpenFolderPicker("Choose Automatic Import Directory", (b, s) => - { - if (b) - _fileWatcher.UpdateDirectory(s); - }, startDir, false); - } - - style.Pop(); - ImGuiUtil.LabeledHelpMarker("Automatic Import Director", - "Choose the Directory the File Watcher listens to."); - } - /// Draw input for the default name to input as author into newly generated mods. private void DrawDefaultModAuthor() { diff --git a/repo.json b/repo.json index 34405eb6..2a31b75e 100644 --- a/repo.json +++ b/repo.json @@ -6,7 +6,7 @@ "Description": "Runtime mod loader and manager.", "InternalName": "Penumbra", "AssemblyVersion": "1.5.1.6", - "TestingAssemblyVersion": "1.5.1.7", + "TestingAssemblyVersion": "1.5.1.6", "RepoUrl": "https://github.com/xivdev/Penumbra", "ApplicableVersion": "any", "DalamudApiLevel": 13, @@ -19,7 +19,7 @@ "LoadRequiredState": 2, "LoadSync": true, "DownloadLinkInstall": "https://github.com/xivdev/Penumbra/releases/download/1.5.1.6/Penumbra.zip", - "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/testing_1.5.1.7/Penumbra.zip", + "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/1.5.1.6/Penumbra.zip", "DownloadLinkUpdate": "https://github.com/xivdev/Penumbra/releases/download/1.5.1.6/Penumbra.zip", "IconUrl": "https://raw.githubusercontent.com/xivdev/Penumbra/master/images/icon.png" }