Compare commits

..

No commits in common. "d79e6871621eb6781958bfd3e774803e03756b99" and "34f067f13d68eb310fce111e39d94a2bb131d5a9" have entirely different histories.

15 changed files with 94 additions and 421 deletions

2
Luna

@ -1 +1 @@
Subproject commit c764db88097c88cd49f2bed4f60268d617f97fdb Subproject commit 78216203f4570a6194fce9422204d8abb536c828

@ -1 +1 @@
Subproject commit a63f6735cf4bed4f7502a022a10378607082b770 Subproject commit 9af1e5fce4c13ef98842807d4f593dec8ae80c87

@ -1 +1 @@
Subproject commit c23ee05c1e9fa103eaa52e6aa7e855ef568ee669 Subproject commit 97fe622e4ec0a5469a26aba8a8c3933fa8ef7fd6

@ -1 +1 @@
Subproject commit cf3d868eeeb4ea3ea728ae15a8d09ec127ce80e9 Subproject commit 182cca56a49411430233d73d7a8a6bb3d983f8f0

View file

@ -51,7 +51,6 @@ public class Configuration : IPluginConfiguration, ISavable, IService
public string ModDirectory { get; set; } = string.Empty; public string ModDirectory { get; set; } = string.Empty;
public string ExportDirectory { 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? UseCrashHandler { get; set; } = null;
public bool OpenWindowAtStart { get; set; } = false; public bool OpenWindowAtStart { get; set; } = false;
@ -75,8 +74,6 @@ public class Configuration : IPluginConfiguration, ISavable, IService
public bool HideRedrawBar { get; set; } = false; public bool HideRedrawBar { get; set; } = false;
public bool HideMachinistOffhandFromChangedItems { get; set; } = true; public bool HideMachinistOffhandFromChangedItems { get; set; } = true;
public bool DefaultTemporaryMode { get; set; } = false; 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 bool EnableCustomShapes { get; set; } = true;
public PcpSettings PcpSettings = new(); public PcpSettings PcpSettings = new();
public RenameField ShowRename { get; set; } = RenameField.BothDataPrio; public RenameField ShowRename { get; set; } = RenameField.BothDataPrio;

View file

@ -41,7 +41,7 @@ public class EphemeralConfig : ISavable, IDisposable, IService
public ChangedItemIconFlag ChangedItemFilter { get; set; } = ChangedItemFlagExtensions.DefaultFlags; public ChangedItemIconFlag ChangedItemFilter { get; set; } = ChangedItemFlagExtensions.DefaultFlags;
public bool FixMainWindow { get; set; } = false; public bool FixMainWindow { get; set; } = false;
public string LastModPath { get; set; } = string.Empty; public string LastModPath { get; set; } = string.Empty;
public HashSet<string> AdvancedEditingOpenForModPaths { get; set; } = []; public bool AdvancedEditingOpen { get; set; } = false;
public bool ForceRedrawOnFileChange { get; set; } = false; public bool ForceRedrawOnFileChange { get; set; } = false;
public bool IncognitoMode { get; set; } = false; public bool IncognitoMode { get; set; } = false;

View file

@ -73,7 +73,6 @@ public sealed class CutsceneService : Luna.IRequiredService, IDisposable
return false; return false;
_copiedCharacters[copyIdx - CutsceneStartIdx] = (short)parentIdx; _copiedCharacters[copyIdx - CutsceneStartIdx] = (short)parentIdx;
_objects.InvokeRequiredUpdates();
return true; return true;
} }

View file

@ -32,24 +32,12 @@ public class ModStorage : IReadOnlyList<Mod>
IEnumerator IEnumerable.GetEnumerator() IEnumerator IEnumerable.GetEnumerator()
=> GetEnumerator(); => GetEnumerator();
/// <summary>
/// Try to obtain a mod by its directory name (unique identifier).
/// </summary>
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;
}
/// <summary> /// <summary>
/// Try to obtain a mod by its directory name (unique identifier, preferred), /// Try to obtain a mod by its directory name (unique identifier, preferred),
/// or the first mod of the given name if no directory fits. /// or the first mod of the given name if no directory fits.
/// </summary> /// </summary>
public bool TryGetMod(string identifier, string modName, [NotNullWhen(true)] out Mod? mod) public bool TryGetMod(string identifier, string modName, [NotNullWhen(true)] out Mod? mod)
{ {
if (modName.Length is 0)
return TryGetMod(identifier, out mod);
mod = null; mod = null;
foreach (var m in Mods) foreach (var m in Mods)
{ {

View file

@ -27,8 +27,8 @@ public class ModSelection : EventBase<ModSelection.Arguments, ModSelection.Prior
_communicator = communicator; _communicator = communicator;
_collections = collections; _collections = collections;
_config = config; _config = config;
if (_config.LastModPath.Length > 0 && mods.TryGetMod(config.LastModPath, out var mod)) if (_config.LastModPath.Length > 0)
SelectMod(mod); SelectMod(mods.FirstOrDefault(m => string.Equals(m.Identifier, config.LastModPath, StringComparison.OrdinalIgnoreCase)));
_communicator.CollectionChange.Subscribe(OnCollectionChange, CollectionChange.Priority.ModSelection); _communicator.CollectionChange.Subscribe(OnCollectionChange, CollectionChange.Priority.ModSelection);
_communicator.CollectionInheritanceChanged.Subscribe(OnInheritanceChange, CollectionInheritanceChanged.Priority.ModSelection); _communicator.CollectionInheritanceChanged.Subscribe(OnInheritanceChange, CollectionInheritanceChanged.Priority.ModSelection);

View file

@ -141,14 +141,8 @@ public class Penumbra : IDalamudPlugin
if (!_disposed) if (!_disposed)
{ {
_windowSystem = system; _windowSystem = system;
if (_config is { OpenWindowAtStart: true, Ephemeral.AdvancedEditingOpenForModPaths.Count: > 0 }) if (_config is { OpenWindowAtStart: true, Ephemeral.AdvancedEditingOpen: true } && _services.GetService<ModSelection>().Mod is {} mod)
{ _services.GetService<ModEditWindowFactory>().OpenForMod(mod);
var mods = _services.GetService<ModManager>();
var editWindowFactory = _services.GetService<ModEditWindowFactory>();
foreach (var identifier in _config.Ephemeral.AdvancedEditingOpenForModPaths)
if (mods.TryGetMod(identifier, out var mod))
editWindowFactory.OpenForMod(mod);
}
} }
else else
system.Dispose(); system.Dispose();

View file

@ -1,209 +0,0 @@
using Luna;
using Penumbra.Mods.Manager;
namespace Penumbra.Services;
public class FileWatcher : IDisposable, IService
{
private readonly ConcurrentSet<string> _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<string>();
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();
}
}

View file

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

View file

@ -180,8 +180,8 @@ public partial class ModEditWindow : IndexedWindow, IDisposable
public override void OnClose() public override void OnClose()
{ {
base.OnClose(); base.OnClose();
if (Mod is not null && _config.Ephemeral.AdvancedEditingOpenForModPaths.Remove(Mod.Identifier)) _config.Ephemeral.AdvancedEditingOpen = false;
_config.Ephemeral.Save(); _config.Ephemeral.Save();
AppendTask(() => AppendTask(() =>
{ {
_left.Dispose(); _left.Dispose();
@ -194,8 +194,11 @@ public partial class ModEditWindow : IndexedWindow, IDisposable
public override void Draw() 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(); _config.Ephemeral.Save();
}
if (IsLoading) if (IsLoading)
{ {

View file

@ -36,7 +36,6 @@ public class SettingsTab : ITab, IUiService
private readonly Penumbra _penumbra; private readonly Penumbra _penumbra;
private readonly FileDialogService _fileDialog; private readonly FileDialogService _fileDialog;
private readonly ModManager _modManager; private readonly ModManager _modManager;
private readonly FileWatcher _fileWatcher;
private readonly ModExportManager _modExportManager; private readonly ModExportManager _modExportManager;
private readonly ModFileSystemSelector _selector; private readonly ModFileSystemSelector _selector;
private readonly CharacterUtility _characterUtility; private readonly CharacterUtility _characterUtility;
@ -65,8 +64,7 @@ public class SettingsTab : ITab, IUiService
public SettingsTab(IDalamudPluginInterface pluginInterface, Configuration config, FontReloader fontReloader, TutorialService tutorial, public SettingsTab(IDalamudPluginInterface pluginInterface, Configuration config, FontReloader fontReloader, TutorialService tutorial,
Penumbra penumbra, FileDialogService fileDialog, ModManager modManager, ModFileSystemSelector selector, Penumbra penumbra, FileDialogService fileDialog, ModManager modManager, ModFileSystemSelector selector,
CharacterUtility characterUtility, ResidentResourceManager residentResources, ModExportManager modExportManager, CharacterUtility characterUtility, ResidentResourceManager residentResources, ModExportManager modExportManager, HttpApi httpApi,
FileWatcher fileWatcher, HttpApi httpApi,
DalamudSubstitutionProvider dalamudSubstitutionProvider, FileCompactor compactor, DalamudConfigService dalamudConfig, DalamudSubstitutionProvider dalamudSubstitutionProvider, FileCompactor compactor, DalamudConfigService dalamudConfig,
IDataManager gameData, PredefinedTagManager predefinedTagConfig, CrashHandlerService crashService, IDataManager gameData, PredefinedTagManager predefinedTagConfig, CrashHandlerService crashService,
MigrationSectionDrawer migrationDrawer, CollectionAutoSelector autoSelector, CleanupService cleanupService, MigrationSectionDrawer migrationDrawer, CollectionAutoSelector autoSelector, CleanupService cleanupService,
@ -83,7 +81,6 @@ public class SettingsTab : ITab, IUiService
_characterUtility = characterUtility; _characterUtility = characterUtility;
_residentResources = residentResources; _residentResources = residentResources;
_modExportManager = modExportManager; _modExportManager = modExportManager;
_fileWatcher = fileWatcher;
_httpApi = httpApi; _httpApi = httpApi;
_dalamudSubstitutionProvider = dalamudSubstitutionProvider; _dalamudSubstitutionProvider = dalamudSubstitutionProvider;
_compactor = compactor; _compactor = compactor;
@ -725,13 +722,6 @@ public class SettingsTab : ITab, IUiService
DrawPcpFolder(); DrawPcpFolder();
DrawPcpExtension(); DrawPcpExtension();
DrawDefaultModExportPath(); 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."); + "Keep this empty to use the root directory.");
} }
private string? _tempWatchDirectory;
/// <summary> Draw input for the Automatic Mod import path. </summary>
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.");
}
/// <summary> Draw input for the default name to input as author into newly generated mods. </summary> /// <summary> Draw input for the default name to input as author into newly generated mods. </summary>
private void DrawDefaultModAuthor() private void DrawDefaultModAuthor()
{ {

View file

@ -6,7 +6,7 @@
"Description": "Runtime mod loader and manager.", "Description": "Runtime mod loader and manager.",
"InternalName": "Penumbra", "InternalName": "Penumbra",
"AssemblyVersion": "1.5.1.6", "AssemblyVersion": "1.5.1.6",
"TestingAssemblyVersion": "1.5.1.7", "TestingAssemblyVersion": "1.5.1.6",
"RepoUrl": "https://github.com/xivdev/Penumbra", "RepoUrl": "https://github.com/xivdev/Penumbra",
"ApplicableVersion": "any", "ApplicableVersion": "any",
"DalamudApiLevel": 13, "DalamudApiLevel": 13,
@ -19,7 +19,7 @@
"LoadRequiredState": 2, "LoadRequiredState": 2,
"LoadSync": true, "LoadSync": true,
"DownloadLinkInstall": "https://github.com/xivdev/Penumbra/releases/download/1.5.1.6/Penumbra.zip", "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", "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" "IconUrl": "https://raw.githubusercontent.com/xivdev/Penumbra/master/images/icon.png"
} }