mirror of
https://github.com/xivdev/Penumbra.git
synced 2025-12-12 18:27:24 +01:00
Compare commits
No commits in common. "5bf901d0c45f7c0384480387cab03eb626d25899" and "7ed81a982365fa99164a2ab5d8cdb6801987c0d7" have entirely different histories.
5bf901d0c4
...
7ed81a9823
8 changed files with 4 additions and 274 deletions
2
OtterGui
2
OtterGui
|
|
@ -1 +1 @@
|
|||
Subproject commit a63f6735cf4bed4f7502a022a10378607082b770
|
||||
Subproject commit 9af1e5fce4c13ef98842807d4f593dec8ae80c87
|
||||
|
|
@ -1 +1 @@
|
|||
Subproject commit c23ee05c1e9fa103eaa52e6aa7e855ef568ee669
|
||||
Subproject commit dd14131793e5ae47cc8e9232f46469216017b5aa
|
||||
|
|
@ -1 +1 @@
|
|||
Subproject commit 283d51f6f6c7721a810548d95ba83eef2484e17e
|
||||
Subproject commit 3baace73c828271dcb71a8156e3e7b91e1dd12ae
|
||||
|
|
@ -53,7 +53,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;
|
||||
|
|
@ -77,8 +76,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;
|
||||
|
|
|
|||
|
|
@ -75,7 +75,6 @@ public sealed class CutsceneService : IRequiredService, IDisposable
|
|||
return false;
|
||||
|
||||
_copiedCharacters[copyIdx - CutsceneStartIdx] = (short)parentIdx;
|
||||
_objects.InvokeRequiredUpdates();
|
||||
return true;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,187 +0,0 @@
|
|||
using System.Threading.Channels;
|
||||
using OtterGui.Services;
|
||||
using Penumbra.Mods.Manager;
|
||||
|
||||
namespace Penumbra.Services;
|
||||
|
||||
public class FileWatcher : IDisposable, IService
|
||||
{
|
||||
private readonly FileSystemWatcher _fsw;
|
||||
private readonly Channel<string> _queue;
|
||||
private readonly CancellationTokenSource _cts = new();
|
||||
private readonly Task _consumer;
|
||||
private readonly ConcurrentDictionary<string, byte> _pending = new(StringComparer.OrdinalIgnoreCase);
|
||||
private readonly ModImportManager _modImportManager;
|
||||
private readonly MessageService _messageService;
|
||||
private readonly Configuration _config;
|
||||
|
||||
public FileWatcher(ModImportManager modImportManager, MessageService messageService, Configuration config)
|
||||
{
|
||||
_modImportManager = modImportManager;
|
||||
_messageService = messageService;
|
||||
_config = config;
|
||||
|
||||
if (!_config.EnableDirectoryWatch)
|
||||
return;
|
||||
|
||||
_queue = Channel.CreateBounded<string>(new BoundedChannelOptions(256)
|
||||
{
|
||||
SingleReader = true,
|
||||
SingleWriter = false,
|
||||
FullMode = BoundedChannelFullMode.DropOldest,
|
||||
});
|
||||
|
||||
_fsw = new FileSystemWatcher(_config.WatchDirectory)
|
||||
{
|
||||
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;
|
||||
|
||||
_consumer = Task.Factory.StartNew(
|
||||
() => ConsumerLoopAsync(_cts.Token),
|
||||
_cts.Token, TaskCreationOptions.LongRunning, TaskScheduler.Default).Unwrap();
|
||||
|
||||
_fsw.EnableRaisingEvents = true;
|
||||
}
|
||||
|
||||
private void OnPath(object? sender, FileSystemEventArgs e)
|
||||
{
|
||||
// Cheap de-dupe: only queue once per filename until processed
|
||||
if (!_config.EnableDirectoryWatch || !_pending.TryAdd(e.FullPath, 0))
|
||||
return;
|
||||
|
||||
_ = _queue.Writer.TryWrite(e.FullPath);
|
||||
}
|
||||
|
||||
private async Task ConsumerLoopAsync(CancellationToken token)
|
||||
{
|
||||
if (!_config.EnableDirectoryWatch)
|
||||
return;
|
||||
|
||||
var reader = _queue.Reader;
|
||||
while (await reader.WaitToReadAsync(token).ConfigureAwait(false))
|
||||
{
|
||||
while (reader.TryRead(out var path))
|
||||
{
|
||||
try
|
||||
{
|
||||
await ProcessOneAsync(path, token).ConfigureAwait(false);
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
Penumbra.Log.Debug($"[FileWatcher] Canceled via Token.");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Penumbra.Log.Debug($"[FileWatcher] Error during Processing: {ex}");
|
||||
}
|
||||
finally
|
||||
{
|
||||
_pending.TryRemove(path, out _);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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);
|
||||
return;
|
||||
}
|
||||
else
|
||||
{
|
||||
var invoked = false;
|
||||
Action<bool> installRequest = args =>
|
||||
{
|
||||
if (invoked)
|
||||
return;
|
||||
|
||||
invoked = true;
|
||||
_modImportManager.AddUnpack(path);
|
||||
};
|
||||
|
||||
_messageService.PrintModFoundInfo(
|
||||
Path.GetFileNameWithoutExtension(path),
|
||||
installRequest);
|
||||
|
||||
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 UpdateDirectory(string newPath)
|
||||
{
|
||||
if (!_config.EnableDirectoryWatch || _fsw is null || !Directory.Exists(newPath) || string.IsNullOrWhiteSpace(newPath))
|
||||
return;
|
||||
|
||||
_fsw.EnableRaisingEvents = false;
|
||||
_fsw.Path = newPath;
|
||||
_fsw.EnableRaisingEvents = true;
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
if (!_config.EnableDirectoryWatch)
|
||||
return;
|
||||
|
||||
_fsw.EnableRaisingEvents = false;
|
||||
_cts.Cancel();
|
||||
_fsw.Dispose();
|
||||
_queue.Writer.TryComplete();
|
||||
try
|
||||
{
|
||||
_consumer.Wait(TimeSpan.FromSeconds(5));
|
||||
}
|
||||
catch
|
||||
{
|
||||
/* swallow */
|
||||
}
|
||||
|
||||
_cts.Dispose();
|
||||
}
|
||||
}
|
||||
|
|
@ -1,43 +1,19 @@
|
|||
using Dalamud.Bindings.ImGui;
|
||||
using Dalamud.Game.Text;
|
||||
using Dalamud.Game.Text.SeStringHandling;
|
||||
using Dalamud.Game.Text.SeStringHandling.Payloads;
|
||||
using Dalamud.Interface;
|
||||
using Dalamud.Interface.ImGuiNotification;
|
||||
using Dalamud.Interface.ImGuiNotification.EventArgs;
|
||||
using Dalamud.Plugin.Services;
|
||||
using Lumina.Excel.Sheets;
|
||||
using OtterGui.Log;
|
||||
using OtterGui.Services;
|
||||
using OtterGui.Text;
|
||||
using Penumbra.GameData.Data;
|
||||
using Penumbra.Mods.Manager;
|
||||
using Penumbra.String.Classes;
|
||||
using static OtterGui.Classes.MessageService;
|
||||
using Notification = OtterGui.Classes.Notification;
|
||||
|
||||
namespace Penumbra.Services;
|
||||
|
||||
public class InstallNotification(string message, Action<bool> installRequest) : IMessage
|
||||
{
|
||||
private bool _invoked = false;
|
||||
|
||||
public string Message { get; } = message;
|
||||
|
||||
public NotificationType NotificationType => NotificationType.Info;
|
||||
|
||||
public uint NotificationDuration => 10000;
|
||||
|
||||
public void OnNotificationActions(INotificationDrawArgs args)
|
||||
{
|
||||
if (ImUtf8.ButtonEx("Install"u8, "Install this mod."u8, disabled: _invoked))
|
||||
{
|
||||
installRequest(true);
|
||||
_invoked = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public class MessageService(Logger log, IUiBuilder builder, IChatGui chat, INotificationManager notificationManager)
|
||||
: OtterGui.Classes.MessageService(log, builder, chat, notificationManager), IService
|
||||
{
|
||||
|
|
@ -79,11 +55,4 @@ public class MessageService(Logger log, IUiBuilder builder, IChatGui chat, INoti
|
|||
$"Cowardly refusing to load replacement for {originalGamePath.Filename().ToString().ToLowerInvariant()} by {mod.Name}{(messageComplement.Length > 0 ? ":\n" : ".")}{messageComplement}",
|
||||
NotificationType.Warning, 10000));
|
||||
}
|
||||
|
||||
public void PrintModFoundInfo(string fileName, Action<bool> installRequest)
|
||||
{
|
||||
AddMessage(
|
||||
new InstallNotification($"A new mod has been found: {fileName}", installRequest)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -37,7 +37,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;
|
||||
|
|
@ -66,7 +65,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 +82,6 @@ public class SettingsTab : ITab, IUiService
|
|||
_characterUtility = characterUtility;
|
||||
_residentResources = residentResources;
|
||||
_modExportManager = modExportManager;
|
||||
_fileWatcher = fileWatcher;
|
||||
_httpApi = httpApi;
|
||||
_dalamudSubstitutionProvider = dalamudSubstitutionProvider;
|
||||
_compactor = compactor;
|
||||
|
|
@ -649,13 +647,6 @@ public class SettingsTab : ITab, IUiService
|
|||
DrawDefaultModImportFolder();
|
||||
DrawPcpFolder();
|
||||
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, v => _config.EnableDirectoryWatch = v);
|
||||
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();
|
||||
}
|
||||
|
||||
|
||||
|
|
@ -735,45 +726,6 @@ public class SettingsTab : ITab, IUiService
|
|||
+ "Keep this empty to use the root directory.");
|
||||
}
|
||||
|
||||
private string _tempWatchDirectory = string.Empty;
|
||||
/// <summary> Draw input for the Automatic Mod import path. </summary>
|
||||
private void DrawFileWatcherPath()
|
||||
{
|
||||
var tmp = _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.IsItemDeactivatedAfterEdit())
|
||||
_fileWatcher.UpdateDirectory(_tempWatchDirectory);
|
||||
|
||||
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);
|
||||
_config.WatchDirectory = s;
|
||||
_config.Save();
|
||||
}
|
||||
}, 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>
|
||||
private void DrawDefaultModAuthor()
|
||||
{
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue