From 4fa751d1adc2d012d782f4ab0188f0c00ddfc129 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Tue, 5 May 2026 18:02:36 +0200 Subject: [PATCH 1/8] Fix ModAdapters finalizer too. --- Penumbra/Api/ModAdapter.cs | 8 +------- Penumbra/UI/ModsTab/Groups/ModGroupDrawer.cs | 1 - 2 files changed, 1 insertion(+), 8 deletions(-) diff --git a/Penumbra/Api/ModAdapter.cs b/Penumbra/Api/ModAdapter.cs index 3a15df56e..3f42f60e3 100644 --- a/Penumbra/Api/ModAdapter.cs +++ b/Penumbra/Api/ModAdapter.cs @@ -55,11 +55,5 @@ private Mod Mod } public void Dispose() - { - _mod.SetTarget(null!); - GC.SuppressFinalize(this); - } - - ~ModAdapter() - => Dispose(); + => _mod.SetTarget(null!); } diff --git a/Penumbra/UI/ModsTab/Groups/ModGroupDrawer.cs b/Penumbra/UI/ModsTab/Groups/ModGroupDrawer.cs index 6cc92ed00..26dbd289e 100644 --- a/Penumbra/UI/ModsTab/Groups/ModGroupDrawer.cs +++ b/Penumbra/UI/ModsTab/Groups/ModGroupDrawer.cs @@ -205,7 +205,6 @@ private void DrawCollapseHandling(IReadOnlyList options, float minWi draw(); } - var width = Math.Max(Im.Item.Size.X, minWidth); var endPos = Im.Cursor.Position; Im.Cursor.Position = pos; From ee6fc17e4a1fdae9138ece5036d53c9de37cfd31 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Thu, 7 May 2026 00:23:38 +0200 Subject: [PATCH 2/8] Use new window things. --- Luna | 2 +- Penumbra.GameData | 2 +- Penumbra/CommandHandler.cs | 26 ------------- Penumbra/Config/EphemeralConfig.cs | 2 - Penumbra/Meta/Files/CmpFile.cs | 16 ++++---- Penumbra/Services/ConfigMigrationService.cs | 1 - Penumbra/UI/ImportPopup.cs | 3 +- Penumbra/UI/MainWindow/MainWindow.cs | 4 -- Penumbra/UI/Tabs/Debug/CmpDrawer.cs | 42 +++++++++++---------- Penumbra/UI/Tabs/SettingsTab.cs | 3 +- 10 files changed, 35 insertions(+), 66 deletions(-) diff --git a/Luna b/Luna index e9ff60616..0f83a8666 160000 --- a/Luna +++ b/Luna @@ -1 +1 @@ -Subproject commit e9ff6061690b7dfc9ff4fe7cabfa296452866588 +Subproject commit 0f83a86666a82215c7a2287501d3589035394281 diff --git a/Penumbra.GameData b/Penumbra.GameData index 678ac6d20..20614b170 160000 --- a/Penumbra.GameData +++ b/Penumbra.GameData @@ -1 +1 @@ -Subproject commit 678ac6d20576074b0b48338bfd8c282246e9a05d +Subproject commit 20614b17050b2d8cce1f67c48b7b6955c32570b1 diff --git a/Penumbra/CommandHandler.cs b/Penumbra/CommandHandler.cs index 8fb4c64c2..050db60da 100644 --- a/Penumbra/CommandHandler.cs +++ b/Penumbra/CommandHandler.cs @@ -80,7 +80,6 @@ private void OnCommand(string command, string arguments) "toggle" => SetPenumbraState(arguments, null), "reload" => Reload(arguments), "redraw" => Redraw(arguments), - "lockui" => SetUiLockState(arguments), "size" => SetUiMinimumSize(arguments), "debug" => SetDebug(arguments), "collection" => SetCollection(arguments), @@ -112,9 +111,6 @@ private bool PrintHelp(string arguments) _chat.Print(new SeStringBuilder().AddCommand("reload", "Rediscover the mod directory and reload all mods.").BuiltString); _chat.Print(new SeStringBuilder() .AddCommand("redraw", "Redraw all game objects. Specify a placeholder or a name to redraw specific objects.").BuiltString); - _chat.Print(new SeStringBuilder() - .AddCommand("lockui", "Toggle the locked state of the main Penumbra window. Can be used with [on|off] to force specific state.") - .BuiltString); _chat.Print(new SeStringBuilder().AddCommand("size", "Reset the minimum config window size to its default values.").BuiltString); _chat.Print(new SeStringBuilder() .AddCommand("debug", "Toggle debug mode for Penumbra. Can be used with [on|off] to force specific state.").BuiltString); @@ -202,28 +198,6 @@ private bool SetPenumbraState(string _, bool? newValue) return _penumbra.SetEnabled(value); } - private bool SetUiLockState(string arguments) - { - var value = ParseTrueFalseToggle(arguments) ?? !_config.Ephemeral.FixMainWindow; - if (value == _config.Ephemeral.FixMainWindow) - return false; - - if (value) - { - Print("Penumbra UI locked in place."); - _mainWindow.Flags |= WindowFlags.NoMove | WindowFlags.NoResize; - } - else - { - Print("Penumbra UI unlocked."); - _mainWindow.Flags &= ~(WindowFlags.NoMove | WindowFlags.NoResize); - } - - _config.Ephemeral.FixMainWindow = value; - _config.Ephemeral.Save(); - return true; - } - private bool SetUiMinimumSize(string _) { if (_config.MinimumSize.X == Configuration.Constants.MinimumSizeX && _config.MinimumSize.Y == Configuration.Constants.MinimumSizeY) diff --git a/Penumbra/Config/EphemeralConfig.cs b/Penumbra/Config/EphemeralConfig.cs index a5361ead8..54611dc75 100644 --- a/Penumbra/Config/EphemeralConfig.cs +++ b/Penumbra/Config/EphemeralConfig.cs @@ -3,7 +3,6 @@ using Luna.Generators; using Newtonsoft.Json; using Penumbra.Files; -using Penumbra.Services; using Penumbra.UI; using Penumbra.UI.ManagementTab; using Penumbra.UI.ModsTab; @@ -31,7 +30,6 @@ public sealed partial class EphemeralConfig : ISavable, IService [ConfigProperty] private ModPanelTab _selectedModPanelTab = ModPanelTab.Settings; - public bool FixMainWindow { get; set; } = false; public HashSet AdvancedEditingOpenForModPaths { get; set; } = []; public bool ForceRedrawOnFileChange { get; set; } = false; public bool IncognitoMode { get; set; } = false; diff --git a/Penumbra/Meta/Files/CmpFile.cs b/Penumbra/Meta/Files/CmpFile.cs index 5c085fcc8..fb50b6702 100644 --- a/Penumbra/Meta/Files/CmpFile.cs +++ b/Penumbra/Meta/Files/CmpFile.cs @@ -16,13 +16,13 @@ public sealed unsafe class CmpFile : MetaBaseFile public static readonly CharacterUtility.InternalIndex InternalIndex = CharacterUtility.ReverseIndices[(int)MetaIndex.HumanCmp]; - public new GameData.Files.CmpData* Data - => (GameData.Files.CmpData*)base.Data; + public new CmpData* Data + => (CmpData*)base.Data; public RspEntry this[SubRace subRace, RspAttribute attribute] { - get => new(Data->GetScale(subRace).Get(attribute)); - set => Data->GetScale(subRace).Get(attribute) = value.Value; + get => new(Data->GetScaleWrite(subRace).Get(attribute)); + set => Data->GetScaleWrite(subRace).Get(attribute) = value.Value; } public override void Reset() @@ -43,15 +43,15 @@ public CmpFile(MetaFileManager manager) public static RspEntry GetDefault(MetaFileManager manager, SubRace subRace, RspAttribute attribute) { - var data = (GameData.Files.CmpData*)manager.CharacterUtility.DefaultResource(InternalIndex).Address; - return new RspEntry(data->GetScale(subRace).Get(attribute)); + var data = (CmpData*)manager.CharacterUtility.DefaultResource(InternalIndex).Address; + return new RspEntry(data->GetScaleWrite(subRace).Get(attribute)); } public static RspEntry* GetDefaults(MetaFileManager manager, SubRace subRace, RspAttribute attribute) { { - var data = (GameData.Files.CmpData*)manager.CharacterUtility.DefaultResource(InternalIndex).Address; - return (RspEntry*)Unsafe.AsPointer(ref data->GetScale(subRace).Get(attribute)); + var data = (CmpData*)manager.CharacterUtility.DefaultResource(InternalIndex).Address; + return (RspEntry*)Unsafe.AsPointer(ref data->GetScaleWrite(subRace).Get(attribute)); } } } diff --git a/Penumbra/Services/ConfigMigrationService.cs b/Penumbra/Services/ConfigMigrationService.cs index 476387745..5006fb87e 100644 --- a/Penumbra/Services/ConfigMigrationService.cs +++ b/Penumbra/Services/ConfigMigrationService.cs @@ -182,7 +182,6 @@ private void Version7To8() _config.Ephemeral.SelectedTab = _data["SelectedTab"]?.ToObject() ?? _config.Ephemeral.SelectedTab; _config.Filters.ChangedItemTypeFilter = _data["ChangedItemFilter"]?.ToObject() ?? _config.Filters.ChangedItemTypeFilter; - _config.Ephemeral.FixMainWindow = _data["FixMainWindow"]?.ToObject() ?? _config.Ephemeral.FixMainWindow; _config.Ephemeral.Save(); } diff --git a/Penumbra/UI/ImportPopup.cs b/Penumbra/UI/ImportPopup.cs index afb910e2d..711a424bc 100644 --- a/Penumbra/UI/ImportPopup.cs +++ b/Penumbra/UI/ImportPopup.cs @@ -11,12 +11,11 @@ using MessageService = Penumbra.Services.MessageService; using MouseButton = ImSharp.MouseButton; using Notification = Luna.Notification; -using Window = Luna.Window; namespace Penumbra.UI; /// Draw the progress information for import. -public sealed class ImportPopup : Window, INotificationAwareMessage +public sealed class ImportPopup : OverlayWindow, INotificationAwareMessage { public const string WindowLabel = "Penumbra Import Status"; diff --git a/Penumbra/UI/MainWindow/MainWindow.cs b/Penumbra/UI/MainWindow/MainWindow.cs index f950439a7..02d9a5d76 100644 --- a/Penumbra/UI/MainWindow/MainWindow.cs +++ b/Penumbra/UI/MainWindow/MainWindow.cs @@ -55,10 +55,6 @@ public override bool DrawConditions() public override void PreDraw() { - if (_config.Ephemeral.FixMainWindow) - Flags |= WindowFlags.NoResize | WindowFlags.NoMove; - else - Flags &= ~(WindowFlags.NoResize | WindowFlags.NoMove); SizeConstraints = new WindowSizeConstraints { MinimumSize = _config.MinimumSize, diff --git a/Penumbra/UI/Tabs/Debug/CmpDrawer.cs b/Penumbra/UI/Tabs/Debug/CmpDrawer.cs index 154497eba..6f7524fe5 100644 --- a/Penumbra/UI/Tabs/Debug/CmpDrawer.cs +++ b/Penumbra/UI/Tabs/Debug/CmpDrawer.cs @@ -42,7 +42,7 @@ private void DrawScales() foreach (var (name, race) in SubRace.NamesAndValuesU8.Skip(1)) { table.DrawColumn(name); - ref var values = ref _ptr->GetScale(race); + ref var values = ref _ptr->GetScaleWrite(race); foreach (var attribute in RspAttribute.Values.SkipLast(1)) table.DrawColumn($"{values.Get(attribute):F4}"); } @@ -96,19 +96,23 @@ private void DrawColors() ImEx.TextCentered($"{name.ToShortNameU8()}"); } - ref var eyesUi = ref _ptr->Interface.Eyes; - ref var highlightsUi = ref _ptr->Interface.HairHighlights; - ref var featuresUi = ref _ptr->Interface.Features; - var lipsUi = new CmpData.ColorsPair(ref _ptr->Interface.LipsDark, ref _ptr->Interface.LipsLight); - var facePaintUi = new CmpData.ColorsPair(ref _ptr->Interface.FacePaintDark, ref _ptr->Interface.FacePaintLight); - - ref var eyes = ref _ptr->Parameters.Eyes; - ref var highlights = ref _ptr->Parameters.HairHighlights; - ref var features = ref _ptr->Parameters.Features; - var lips = new CmpData.ColorsPair(ref _ptr->Parameters.LipsDark, ref _ptr->Parameters.LipsLight); - var facePaint = new CmpData.ColorsPair(ref _ptr->Parameters.FacePaintDark, ref _ptr->Parameters.FacePaintLight); - - using var clip = new Im.ListClipper(256, Im.Style.FrameHeightWithSpacing); + ref var eyesUi = ref _ptr->Interface.Eyes; + ref var highlightsUi = ref _ptr->Interface.HairHighlights; + ref var featuresUi = ref _ptr->Interface.Features; + ref var lipsUiDark = ref _ptr->Interface.LipsDark; + ref var lipsUiLight = ref _ptr->Interface.LipsLight; + ref var facePaintUiDark = ref _ptr->Interface.FacePaintDark; + ref var facePaintUiLight = ref _ptr->Interface.FacePaintLight; + + ref var eyes = ref _ptr->Parameters.Eyes; + ref var highlights = ref _ptr->Parameters.HairHighlights; + ref var features = ref _ptr->Parameters.Features; + ref var lipsDark = ref _ptr->Parameters.LipsDark; + ref var lipsLight = ref _ptr->Parameters.LipsLight; + ref var facePaintDark = ref _ptr->Parameters.FacePaintDark; + ref var facePaintLight = ref _ptr->Parameters.FacePaintLight; + + using var clip = new Im.ListClipper(256, Im.Style.FrameHeightWithSpacing); foreach (var index in clip) { using var id = Im.Id.Push(index); @@ -130,14 +134,14 @@ private void DrawColors() Im.Color.Button("Features Color"u8, features[index]); table.NextColumn(); - Im.Color.Button("Lips UI Color"u8, lipsUi[index]); + Im.Color.Button("Lips UI Color"u8, index < 128 ? lipsUiDark[index] : lipsUiLight[index - 128]); Im.Line.SameInner(); - Im.Color.Button("Lips Color"u8, lips[index]); + Im.Color.Button("Lips Color"u8, index < 128 ? lipsDark[index] : lipsLight[index - 128]); table.NextColumn(); - Im.Color.Button("Face Paint UI Color"u8, facePaintUi[index]); + Im.Color.Button("Face Paint UI Color"u8, index < 128 ? facePaintUiDark[index] : facePaintUiLight[index - 128]); Im.Line.SameInner(); - Im.Color.Button("Face Paint Color"u8, facePaint[index]); + Im.Color.Button("Face Paint Color"u8, index < 128 ? facePaintDark[index] : facePaintLight[index - 128]); foreach (var race in SubRace.Values.Skip(1)) { @@ -147,7 +151,7 @@ private void DrawColors() Im.Line.SameInner(); Im.Color.Button($"Skin UI Color ({name} Male)", _ptr->GetSkin(race, Gender.Male, true)[index]); Im.Line.SameInner(); - Im.Color.Button($"Skin Color ({name} Male)", _ptr->GetSkin(race, Gender.Male, false)[index]); + Im.Color.Button($"Skin Color ({name} Male)", _ptr->GetSkin(race, Gender.Male, false)[index]); Im.Line.Same(); ImEx.TextFramed("♀"u8, default, Rgba32.Transparent); Im.Line.SameInner(); diff --git a/Penumbra/UI/Tabs/SettingsTab.cs b/Penumbra/UI/Tabs/SettingsTab.cs index 8cb903a4d..26a6c8fb1 100644 --- a/Penumbra/UI/Tabs/SettingsTab.cs +++ b/Penumbra/UI/Tabs/SettingsTab.cs @@ -107,8 +107,7 @@ public void DrawContent() return; DrawEnabledBox(); - EphemeralCheckbox("Lock Main Window"u8, "Prevent the main window from being resized or moved."u8, _config.Ephemeral.FixMainWindow, - v => _config.Ephemeral.FixMainWindow = v); + Im.Line.New(); Im.Line.New(); DrawRootFolder(); From 8bbce2e3f21445cd0b883968d8ecc3b6da91de93 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Thu, 7 May 2026 00:24:03 +0200 Subject: [PATCH 3/8] Make mod import skip duplicate imports that have been made within 2 seconds. --- Penumbra/Mods/Manager/ModImportManager.cs | 59 ++++++++++++++++++----- Penumbra/UI/Tabs/Debug/DebugTab.cs | 9 ++-- 2 files changed, 52 insertions(+), 16 deletions(-) diff --git a/Penumbra/Mods/Manager/ModImportManager.cs b/Penumbra/Mods/Manager/ModImportManager.cs index 43711788a..006fb473d 100644 --- a/Penumbra/Mods/Manager/ModImportManager.cs +++ b/Penumbra/Mods/Manager/ModImportManager.cs @@ -13,28 +13,31 @@ public class ModImportManager( DuplicateManager duplicates, ModNormalizer modNormalizer, MigrationManager migrationManager, - FileCompactor compactor) : IDisposable, Luna.IService + FileCompactor compactor) : IDisposable, IService { - private readonly ConcurrentQueue _modsToUnpack = new(); + private readonly Dictionary _uniqueModsToUnpack = new(StringComparer.OrdinalIgnoreCase); + internal readonly Queue> ModsToUnpack = new(); /// Mods need to be added thread-safely outside of iteration. private readonly ConcurrentQueue _modsToAdd = new(); private TexToolsImporter? _import; - - internal IEnumerable ModBatches - => _modsToUnpack; - internal IEnumerable AddableMods => _modsToAdd; - public void TryUnpacking() { - if (Importing && _import!.State is not ImporterState.Done || !_modsToUnpack.TryDequeue(out var newMods)) + if (Importing && _import!.State is not ImporterState.Done) return; + List newMods; + lock (ModsToUnpack) + { + if (!ModsToUnpack.TryDequeue(out newMods!)) + return; + } + var files = newMods.Where(s => { if (File.Exists(s)) @@ -63,12 +66,38 @@ public bool IsImporting([NotNullWhen(true)] out TexToolsImporter? importer) } public void AddUnpack(IEnumerable paths) - => AddUnpack(paths.ToArray()); + => AddUnpack(paths.ToList()); - public void AddUnpack(params string[] paths) + public void AddUnpack(params List paths) { - Penumbra.Log.Debug($"Adding mods to install: {string.Join("\n\t", paths)}"); - _modsToUnpack.Enqueue(paths); + lock (ModsToUnpack) + { + var now = DateTime.UtcNow; + var nowOffset = now.AddSeconds(-2); + for (var i = 0; i < paths.Count; ++i) + { + var path = paths[i]; + if (_uniqueModsToUnpack.TryGetValue(path, out var lastInstallTime)) + { + _uniqueModsToUnpack[path] = now; + if (lastInstallTime >= nowOffset) + { + paths.RemoveAt(i--); + Penumbra.Log.Debug($"Skipped installing mod {path} since it was last installed {(lastInstallTime - now).TotalSeconds} seconds ago."); + } + } + else + { + _uniqueModsToUnpack.Add(path, now); + } + } + + if (paths.Count > 0) + { + Penumbra.Log.Debug($"Adding mods to install: {string.Join("\n\t", paths)}"); + ModsToUnpack.Enqueue(paths); + } + } } public void ClearImport() @@ -93,7 +122,11 @@ public void Dispose() { ClearImport(); _modsToAdd.Clear(); - _modsToUnpack.Clear(); + lock (ModsToUnpack) + { + ModsToUnpack.Clear(); + _uniqueModsToUnpack.Clear(); + } } /// diff --git a/Penumbra/UI/Tabs/Debug/DebugTab.cs b/Penumbra/UI/Tabs/Debug/DebugTab.cs index 6fed10948..c7146ae6d 100644 --- a/Penumbra/UI/Tabs/Debug/DebugTab.cs +++ b/Penumbra/UI/Tabs/Debug/DebugTab.cs @@ -380,10 +380,13 @@ private void DrawDebugTabGeneral() table.DrawDataPair("Import Popup Was Drawn"u8, _importPopup.PopupWasDrawn); table.DrawColumn("Import Batches"u8); table.NextColumn(); - foreach (var (index, batch) in _modImporter.ModBatches.Index()) + lock (_modImporter.ModsToUnpack) { - foreach (var mod in batch) - table.DrawDataPair($"{index}", mod); + foreach (var (index, batch) in _modImporter.ModsToUnpack.Index()) + { + foreach (var mod in batch) + table.DrawDataPair($"{index}", mod); + } } table.DrawColumn("Addable Mods"u8); From 0a410097dcae9fe4f1f1d12673b46d9fa7c6b369 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Thu, 7 May 2026 14:15:47 +0200 Subject: [PATCH 4/8] Allow renaming capitalization only. --- Luna | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Luna b/Luna index 0f83a8666..bd0932646 160000 --- a/Luna +++ b/Luna @@ -1 +1 @@ -Subproject commit 0f83a86666a82215c7a2287501d3589035394281 +Subproject commit bd09326466a89692cb16868c240680debe35e153 From bb381d21de3b2a8ab3ce4037c423b8a511d62b51 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Fri, 8 May 2026 01:05:17 +0200 Subject: [PATCH 5/8] Add hook for base clip association. --- Luna | 2 +- Penumbra.GameData | 2 +- .../Hooks/Animation/GetClipResources.cs | 58 +++++++++++++++++++ .../Hooks/Animation/ScheduleClipUpdate.cs | 3 +- Penumbra/Interop/Hooks/HookSettings.cs | 1 + 5 files changed, 63 insertions(+), 3 deletions(-) create mode 100644 Penumbra/Interop/Hooks/Animation/GetClipResources.cs diff --git a/Luna b/Luna index bd0932646..00d83664c 160000 --- a/Luna +++ b/Luna @@ -1 +1 @@ -Subproject commit bd09326466a89692cb16868c240680debe35e153 +Subproject commit 00d83664cf2366077253316e51ea38e47b4fd75c diff --git a/Penumbra.GameData b/Penumbra.GameData index 20614b170..7b7372399 160000 --- a/Penumbra.GameData +++ b/Penumbra.GameData @@ -1 +1 @@ -Subproject commit 20614b17050b2d8cce1f67c48b7b6955c32570b1 +Subproject commit 7b7372399d1094f6aa4f1fc18237bc2228eae41a diff --git a/Penumbra/Interop/Hooks/Animation/GetClipResources.cs b/Penumbra/Interop/Hooks/Animation/GetClipResources.cs new file mode 100644 index 000000000..760d64008 --- /dev/null +++ b/Penumbra/Interop/Hooks/Animation/GetClipResources.cs @@ -0,0 +1,58 @@ +using FFXIVClientStructs.FFXIV.Client.System.Scheduler.Clip; +using Luna; +using Penumbra.CrashHandler.Buffers; +using Penumbra.GameData; +using Penumbra.GameData.Interop; +using Penumbra.Interop.PathResolving; +using Penumbra.Services; + +namespace Penumbra.Interop.Hooks.Animation; + +public sealed unsafe class GetClipResources : FastHook +{ + private readonly delegate* unmanaged _clipGameObject; + private readonly GameState _state; + private readonly CollectionResolver _collectionResolver; + private readonly CrashHandlerService _crashHandler; + + public GetClipResources(HookManager hooks, GameState state, CollectionResolver collectionResolver, ObjectManager objects, + CrashHandlerService crashHandler) + { + _state = state; + _collectionResolver = collectionResolver; + _crashHandler = crashHandler; + _clipGameObject = (delegate* unmanaged < BaseClip*, Actor >)hooks.SigScanner.ScanText(Sigs.BaseClipGetGameObject); + if (_clipGameObject is null) + throw new Exception($"Could not scan address {Sigs.BaseClipGetGameObject}."); + + Task = hooks.CreateHook("Get Clip Resources", Sigs.BaseClipGetClipResources, Detour, + !HookOverrides.Instance.Animation.GetClipResources); + } + + public delegate nint Delegate(BaseClip* clip); + + + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] + private nint Detour(BaseClip* clip) + { + if (clip is null) + return Task.Result!.Original(clip); + + var gameObject = _clipGameObject(clip); + var data = _collectionResolver.IdentifyCollection(gameObject.AsObject, true); + if (!data.Valid) + { + var ret2 = Task.Result!.Original(clip); + Penumbra.Log.Excessive($"[Get Clip Resources] Invoked on {(nint)clip:X} -> {ret2:X}."); + return ret2; + } + + var last = _state.SetSoundData(data); + _crashHandler.LogAnimation(data.AssociatedGameObject, data.ModCollection, AnimationInvocationType.ScheduleClipUpdate); + var ret = Task.Result!.Original(clip); + _state.RestoreAnimationData(last); + Penumbra.Log.Excessive( + $"[Get Clip Resources] Invoked on {(nint)clip:X} for {data.AssociatedGameObject:X} ({data.ModCollection}) -> {ret:X}."); + return ret; + } +} diff --git a/Penumbra/Interop/Hooks/Animation/ScheduleClipUpdate.cs b/Penumbra/Interop/Hooks/Animation/ScheduleClipUpdate.cs index a1a3ab464..ff48a65f1 100644 --- a/Penumbra/Interop/Hooks/Animation/ScheduleClipUpdate.cs +++ b/Penumbra/Interop/Hooks/Animation/ScheduleClipUpdate.cs @@ -23,7 +23,8 @@ public ScheduleClipUpdate(HookManager hooks, GameState state, CollectionResolver _collectionResolver = collectionResolver; _objects = objects; _crashHandler = crashHandler; - Task = hooks.CreateHook("Schedule Clip Update", Sigs.ScheduleClipUpdate, Detour, !HookOverrides.Instance.Animation.ScheduleClipUpdate); + Task = hooks.CreateHook("Schedule Clip Update", Sigs.ScheduleClipUpdate, Detour, + !HookOverrides.Instance.Animation.ScheduleClipUpdate); } public delegate void Delegate(ClipScheduler* x); diff --git a/Penumbra/Interop/Hooks/HookSettings.cs b/Penumbra/Interop/Hooks/HookSettings.cs index 49ee0309a..2f5fc0901 100644 --- a/Penumbra/Interop/Hooks/HookSettings.cs +++ b/Penumbra/Interop/Hooks/HookSettings.cs @@ -40,6 +40,7 @@ public struct AnimationHooks public bool LoadTimelineResources; public bool PlayFootstep; public bool ScheduleClipUpdate; + public bool GetClipResources; public bool SomeActionLoad; public bool SomeMountAnimation; public bool SomePapLoad; From 6431eed65d7bac503eec61f6412e1f87a52d1949 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Fri, 8 May 2026 01:12:18 +0200 Subject: [PATCH 6/8] Own type for crash handler. --- Penumbra.CrashHandler/Buffers/AnimationInvocationBuffer.cs | 4 +++- Penumbra/Interop/Hooks/Animation/GetClipResources.cs | 2 +- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/Penumbra.CrashHandler/Buffers/AnimationInvocationBuffer.cs b/Penumbra.CrashHandler/Buffers/AnimationInvocationBuffer.cs index 71570f604..4ae6ea981 100644 --- a/Penumbra.CrashHandler/Buffers/AnimationInvocationBuffer.cs +++ b/Penumbra.CrashHandler/Buffers/AnimationInvocationBuffer.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.Text.Json.Nodes; @@ -9,6 +9,7 @@ public enum AnimationInvocationType : int { PapLoad, ActionLoad, + GetClipResources, ScheduleClipUpdate, LoadTimelineResources, LoadCharacterVfx, @@ -111,6 +112,7 @@ private static string ToName(AnimationInvocationType type) { AnimationInvocationType.PapLoad => "PAP Load", AnimationInvocationType.ActionLoad => "Action Load", + AnimationInvocationType.GetClipResources => "Get Clip Resources", AnimationInvocationType.ScheduleClipUpdate => "Schedule Clip Update", AnimationInvocationType.LoadTimelineResources => "Load Timeline Resources", AnimationInvocationType.LoadCharacterVfx => "Load Character VFX", diff --git a/Penumbra/Interop/Hooks/Animation/GetClipResources.cs b/Penumbra/Interop/Hooks/Animation/GetClipResources.cs index 760d64008..83655d5a4 100644 --- a/Penumbra/Interop/Hooks/Animation/GetClipResources.cs +++ b/Penumbra/Interop/Hooks/Animation/GetClipResources.cs @@ -48,7 +48,7 @@ private nint Detour(BaseClip* clip) } var last = _state.SetSoundData(data); - _crashHandler.LogAnimation(data.AssociatedGameObject, data.ModCollection, AnimationInvocationType.ScheduleClipUpdate); + _crashHandler.LogAnimation(data.AssociatedGameObject, data.ModCollection, AnimationInvocationType.GetClipResources); var ret = Task.Result!.Original(clip); _state.RestoreAnimationData(last); Penumbra.Log.Excessive( From effd3dcbeac15045353244f41f7e0aef9724fc39 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Fri, 8 May 2026 16:24:41 +0200 Subject: [PATCH 7/8] Only use clip resource if it returns a valid game object. --- Penumbra/Interop/Hooks/Animation/GetClipResources.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Penumbra/Interop/Hooks/Animation/GetClipResources.cs b/Penumbra/Interop/Hooks/Animation/GetClipResources.cs index 83655d5a4..c565744fd 100644 --- a/Penumbra/Interop/Hooks/Animation/GetClipResources.cs +++ b/Penumbra/Interop/Hooks/Animation/GetClipResources.cs @@ -1,5 +1,6 @@ using FFXIVClientStructs.FFXIV.Client.System.Scheduler.Clip; using Luna; +using Penumbra.Collections; using Penumbra.CrashHandler.Buffers; using Penumbra.GameData; using Penumbra.GameData.Interop; @@ -39,7 +40,7 @@ private nint Detour(BaseClip* clip) return Task.Result!.Original(clip); var gameObject = _clipGameObject(clip); - var data = _collectionResolver.IdentifyCollection(gameObject.AsObject, true); + var data = gameObject.Valid ? _collectionResolver.IdentifyCollection(gameObject.AsObject, true) : ResolveData.Invalid; if (!data.Valid) { var ret2 = Task.Result!.Original(clip); From 59b25f32f895eb80163ce9653fc9c09e2a011619 Mon Sep 17 00:00:00 2001 From: Exter-N Date: Sat, 9 May 2026 08:00:03 +0200 Subject: [PATCH 8/8] Make mod imports awaitable --- Penumbra/Import/TexToolsImport.cs | 21 +++++++++-------- Penumbra/Mods/Manager/ModImportManager.cs | 28 ++++++++++++++--------- Penumbra/Mods/Manager/ModImportResult.cs | 3 +++ Penumbra/Services/FileWatcher.cs | 10 +++++--- Penumbra/Services/InstallNotification.cs | 18 +++++++++++++-- Penumbra/UI/GlobalModImporter.cs | 2 +- Penumbra/UI/Tabs/Debug/DebugTab.cs | 2 +- 7 files changed, 57 insertions(+), 27 deletions(-) create mode 100644 Penumbra/Mods/Manager/ModImportResult.cs diff --git a/Penumbra/Import/TexToolsImport.cs b/Penumbra/Import/TexToolsImport.cs index 7981b4472..8061a5224 100644 --- a/Penumbra/Import/TexToolsImport.cs +++ b/Penumbra/Import/TexToolsImport.cs @@ -26,8 +26,8 @@ public partial class TexToolsImporter : IDisposable private readonly CancellationTokenSource _cancellation = new(); private readonly CancellationToken _token; - public ImporterState State { get; private set; } - public readonly List<(FileInfo File, DirectoryInfo? Mod, Exception? Error)> ExtractedMods; + public ImporterState State { get; private set; } + public readonly List ExtractedMods; private readonly Configuration _config; private readonly DuplicateManager _duplicates; @@ -37,8 +37,9 @@ public partial class TexToolsImporter : IDisposable private readonly MigrationManager _migrationManager; public TexToolsImporter(int count, IEnumerable modPackFiles, Action handler, - Configuration config, DuplicateManager duplicates, ModNormalizer modNormalizer, ModManager modManager, FileCompactor compactor, - MigrationManager migrationManager, TexToolsImporter? previous) + TaskCompletionSource taskCompletionSource, Configuration config, DuplicateManager duplicates, + ModNormalizer modNormalizer, ModManager modManager, FileCompactor compactor, MigrationManager migrationManager, + TexToolsImporter? previous) { if (previous is not null) { @@ -58,7 +59,7 @@ public TexToolsImporter(int count, IEnumerable modPackFiles, Action(count + _previousModPackCount); + ExtractedMods = new List(count + _previousModPackCount); _token = _cancellation.Token; if (previous is not null) ExtractedMods.AddRange(previous.ExtractedMods); @@ -68,7 +69,9 @@ public TexToolsImporter(int count, IEnumerable modPackFiles, Action { taskCompletionSource.SetResult(ExtractedMods.Skip(_previousModPackCount).ToArray()); }, + TaskScheduler.Default); } private void CloseStreams() @@ -100,14 +103,14 @@ private void ImportFiles() _currentModDirectory = null; if (_token.IsCancellationRequested) { - ExtractedMods.Add((file, null, new TaskCanceledException("Task canceled by user."))); + ExtractedMods.Add(new ModImportResult(file, null, new TaskCanceledException("Task canceled by user."))); continue; } try { var directory = VerifyVersionAndImport(file); - ExtractedMods.Add((file, directory, null)); + ExtractedMods.Add(new ModImportResult(file, directory, null)); if (_config.AutoDeduplicateOnImport) { State = ImporterState.DeduplicatingFiles; @@ -116,7 +119,7 @@ private void ImportFiles() } catch (Exception e) { - ExtractedMods.Add((file, _currentModDirectory, e)); + ExtractedMods.Add(new ModImportResult(file, _currentModDirectory, e)); _currentNumOptions = 0; _currentOptionIdx = 0; _currentFileIdx = 0; diff --git a/Penumbra/Mods/Manager/ModImportManager.cs b/Penumbra/Mods/Manager/ModImportManager.cs index 006fb473d..13ad17308 100644 --- a/Penumbra/Mods/Manager/ModImportManager.cs +++ b/Penumbra/Mods/Manager/ModImportManager.cs @@ -16,7 +16,7 @@ public class ModImportManager( FileCompactor compactor) : IDisposable, IService { private readonly Dictionary _uniqueModsToUnpack = new(StringComparer.OrdinalIgnoreCase); - internal readonly Queue> ModsToUnpack = new(); + internal readonly Queue ModsToUnpack = new(); /// Mods need to be added thread-safely outside of iteration. private readonly ConcurrentQueue _modsToAdd = new(); @@ -31,14 +31,14 @@ public void TryUnpacking() if (Importing && _import!.State is not ImporterState.Done) return; - List newMods; + UnpackRequest newMods; lock (ModsToUnpack) { - if (!ModsToUnpack.TryDequeue(out newMods!)) + if (!ModsToUnpack.TryDequeue(out newMods)) return; } - var files = newMods.Where(s => + var files = newMods.Paths.Where(s => { if (File.Exists(s)) return true; @@ -50,10 +50,13 @@ public void TryUnpacking() Penumbra.Log.Debug($"Unpacking mods: {string.Join("\n\t", files.Select(f => f.FullName))}."); if (files.Length == 0) + { + newMods.TaskCompletionSource.SetResult([]); return; + } - _import = new TexToolsImporter(files.Length, files, AddNewMod, config, duplicates, modNormalizer, modManager, compactor, - migrationManager, _import); + _import = new TexToolsImporter(files.Length, files, AddNewMod, newMods.TaskCompletionSource, config, duplicates, modNormalizer, + modManager, compactor, migrationManager, _import); } public bool Importing @@ -65,10 +68,7 @@ public bool IsImporting([NotNullWhen(true)] out TexToolsImporter? importer) return _import != null; } - public void AddUnpack(IEnumerable paths) - => AddUnpack(paths.ToList()); - - public void AddUnpack(params List paths) + public Task AddUnpack(params List paths) { lock (ModsToUnpack) { @@ -95,9 +95,13 @@ public void AddUnpack(params List paths) if (paths.Count > 0) { Penumbra.Log.Debug($"Adding mods to install: {string.Join("\n\t", paths)}"); - ModsToUnpack.Enqueue(paths); + var tcs = new TaskCompletionSource(); + ModsToUnpack.Enqueue(new UnpackRequest(paths, tcs)); + return tcs.Task; } } + + return Task.FromResult(Array.Empty()); } public void ClearImport() @@ -156,4 +160,6 @@ private void AddNewMod(FileInfo file, DirectoryInfo? dir, Exception? error) _modsToAdd.Enqueue(dir); } } + + internal readonly record struct UnpackRequest(List Paths, TaskCompletionSource TaskCompletionSource); } diff --git a/Penumbra/Mods/Manager/ModImportResult.cs b/Penumbra/Mods/Manager/ModImportResult.cs new file mode 100644 index 000000000..5350544cb --- /dev/null +++ b/Penumbra/Mods/Manager/ModImportResult.cs @@ -0,0 +1,3 @@ +namespace Penumbra.Mods.Manager; + +public readonly record struct ModImportResult(FileInfo File, DirectoryInfo? Mod, Exception? Error); diff --git a/Penumbra/Services/FileWatcher.cs b/Penumbra/Services/FileWatcher.cs index 756ff6457..638996e00 100644 --- a/Penumbra/Services/FileWatcher.cs +++ b/Penumbra/Services/FileWatcher.cs @@ -306,12 +306,16 @@ private static async Task WaitForStableAsync(string path, CancellationToke return false; } - private void TriggerImport(string path) + private Task TriggerImport(string path) { if (_config.EnableAutomaticModImport) - _modImportManager.AddUnpack(path); + return _modImportManager.AddUnpack(path); else - _messageService.AddMessage(new InstallNotification(_modImportManager, path), false); + { + var tcs = new TaskCompletionSource(); + _messageService.AddMessage(new InstallNotification(_modImportManager, path, tcs), false); + return tcs.Task; + } } /// diff --git a/Penumbra/Services/InstallNotification.cs b/Penumbra/Services/InstallNotification.cs index 71c6884f1..44c665ca0 100644 --- a/Penumbra/Services/InstallNotification.cs +++ b/Penumbra/Services/InstallNotification.cs @@ -6,8 +6,11 @@ namespace Penumbra.Services; -public class InstallNotification(ModImportManager modImportManager, string filePath) : Luna.IMessage +public class InstallNotification(ModImportManager modImportManager, string filePath, TaskCompletionSource tcs) + : Luna.INotificationAwareMessage { + private bool _reportCancellationOnDismissal = true; + public NotificationType NotificationType => NotificationType.Info; @@ -37,7 +40,9 @@ public void OnNotificationActions(INotificationDrawArgs args) var buttonSize = new Vector2((region.X - Im.Style.ItemSpacing.X) / 2, 0); if (Im.Button("Install"u8, buttonSize)) { - modImportManager.AddUnpack(filePath); + _reportCancellationOnDismissal = false; + modImportManager.AddUnpack(filePath) + .ContinueWith(tcs.SetFromTask); args.Notification.DismissNow(); } @@ -45,4 +50,13 @@ public void OnNotificationActions(INotificationDrawArgs args) if (Im.Button("Ignore"u8, buttonSize)) args.Notification.DismissNow(); } + + public void OnNotificationCreated(IActiveNotification notification) + { + notification.Dismiss += _ => + { + if (_reportCancellationOnDismissal) + tcs.SetResult([]); + }; + } } diff --git a/Penumbra/UI/GlobalModImporter.cs b/Penumbra/UI/GlobalModImporter.cs index 2e63cd238..4c99ecb12 100644 --- a/Penumbra/UI/GlobalModImporter.cs +++ b/Penumbra/UI/GlobalModImporter.cs @@ -46,7 +46,7 @@ public void Dispose() } private void ImportFiles(IReadOnlyList files, IReadOnlyList _) - => _importManager.AddUnpack(files.Where(f => ValidModExtensions.Contains(Path.GetExtension(f)))); + => _importManager.AddUnpack(files.Where(f => ValidModExtensions.Contains(Path.GetExtension(f))).ToList()); private static bool ValidExtension(IDragDropManager manager) => manager.Extensions.Any(ValidModExtensions.Contains); diff --git a/Penumbra/UI/Tabs/Debug/DebugTab.cs b/Penumbra/UI/Tabs/Debug/DebugTab.cs index c7146ae6d..9706e683d 100644 --- a/Penumbra/UI/Tabs/Debug/DebugTab.cs +++ b/Penumbra/UI/Tabs/Debug/DebugTab.cs @@ -384,7 +384,7 @@ private void DrawDebugTabGeneral() { foreach (var (index, batch) in _modImporter.ModsToUnpack.Index()) { - foreach (var mod in batch) + foreach (var mod in batch.Paths) table.DrawDataPair($"{index}", mod); } }