diff --git a/Penumbra/Api/PenumbraApi.cs b/Penumbra/Api/PenumbraApi.cs index 59ef6677..aed1a963 100644 --- a/Penumbra/Api/PenumbraApi.cs +++ b/Penumbra/Api/PenumbraApi.cs @@ -24,6 +24,7 @@ using Penumbra.Interop.Services; using Penumbra.UI; using TextureType = Penumbra.Api.Enums.TextureType; using Penumbra.Interop.ResourceTree; +using Penumbra.Mods.Editor; namespace Penumbra.Api; @@ -142,6 +143,7 @@ public class PenumbraApi : IDisposable, IPenumbraApi _communicator.ModSettingChanged.Subscribe(OnModSettingChange, Communication.ModSettingChanged.Priority.Api); _communicator.CreatedCharacterBase.Subscribe(OnCreatedCharacterBase, Communication.CreatedCharacterBase.Priority.Api); _communicator.ModOptionChanged.Subscribe(OnModOptionEdited, ModOptionChanged.Priority.Api); + _communicator.ModFileChanged.Subscribe(OnModFileChanged, ModFileChanged.Priority.Api); } public unsafe void Dispose() @@ -153,6 +155,8 @@ public class PenumbraApi : IDisposable, IPenumbraApi _communicator.ModPathChanged.Unsubscribe(ModPathChangeSubscriber); _communicator.ModSettingChanged.Unsubscribe(OnModSettingChange); _communicator.CreatedCharacterBase.Unsubscribe(OnCreatedCharacterBase); + _communicator.ModOptionChanged.Unsubscribe(OnModOptionEdited); + _communicator.ModFileChanged.Unsubscribe(OnModFileChanged); _lumina = null; _communicator = null!; _modManager = null!; @@ -1277,11 +1281,19 @@ public class PenumbraApi : IDisposable, IPenumbraApi } } + private void OnModFileChanged(Mod mod, FileRegistry file) + { + if (file.CurrentUsage == 0) + return; + + TriggerSettingEdited(mod); + } + private void TriggerSettingEdited(Mod mod) { var collection = _collectionResolver.PlayerCollection(); var (settings, parent) = collection[mod.Index]; - if (settings != null) + if (settings is { Enabled: true }) ModSettingChanged?.Invoke(ModSettingChange.Edited, collection.Name, mod.Identifier, parent != collection); } } diff --git a/Penumbra/Collections/Manager/CollectionStorage.cs b/Penumbra/Collections/Manager/CollectionStorage.cs index c43c3817..a84c79e6 100644 --- a/Penumbra/Collections/Manager/CollectionStorage.cs +++ b/Penumbra/Collections/Manager/CollectionStorage.cs @@ -4,6 +4,7 @@ using OtterGui.Classes; using OtterGui.Filesystem; using Penumbra.Communication; using Penumbra.Mods; +using Penumbra.Mods.Editor; using Penumbra.Mods.Manager; using Penumbra.Mods.Subclasses; using Penumbra.Services; @@ -56,6 +57,7 @@ public class CollectionStorage : IReadOnlyList, IDisposable _communicator.ModDiscoveryFinished.Subscribe(OnModDiscoveryFinished, ModDiscoveryFinished.Priority.CollectionStorage); _communicator.ModPathChanged.Subscribe(OnModPathChange, ModPathChanged.Priority.CollectionStorage); _communicator.ModOptionChanged.Subscribe(OnModOptionChange, ModOptionChanged.Priority.CollectionStorage); + _communicator.ModFileChanged.Subscribe(OnModFileChanged, ModFileChanged.Priority.CollectionStorage); ReadCollections(out DefaultNamed); } @@ -65,6 +67,7 @@ public class CollectionStorage : IReadOnlyList, IDisposable _communicator.ModDiscoveryFinished.Unsubscribe(OnModDiscoveryFinished); _communicator.ModPathChanged.Unsubscribe(OnModPathChange); _communicator.ModOptionChanged.Unsubscribe(OnModOptionChange); + _communicator.ModFileChanged.Unsubscribe(OnModFileChanged); } /// @@ -104,7 +107,8 @@ public class CollectionStorage : IReadOnlyList, IDisposable if (!CanAddCollection(name, out var fixedName)) { Penumbra.Messager.NotificationMessage( - $"The new collection {name} would lead to the same path {fixedName} as one that already exists.", NotificationType.Warning, false); + $"The new collection {name} would lead to the same path {fixedName} as one that already exists.", NotificationType.Warning, + false); return false; } @@ -185,20 +189,23 @@ public class CollectionStorage : IReadOnlyList, IDisposable if (!IsValidName(name)) { // TODO: handle better. - Penumbra.Messager.NotificationMessage($"Collection of unsupported name found: {name} is not a valid collection name.", NotificationType.Warning); + Penumbra.Messager.NotificationMessage($"Collection of unsupported name found: {name} is not a valid collection name.", + NotificationType.Warning); continue; } if (ByName(name, out _)) { - Penumbra.Messager.NotificationMessage($"Duplicate collection found: {name} already exists. Import skipped.", NotificationType.Warning); + Penumbra.Messager.NotificationMessage($"Duplicate collection found: {name} already exists. Import skipped.", + NotificationType.Warning); continue; } var collection = ModCollection.CreateFromData(_saveService, _modStorage, name, version, Count, settings, inheritance); var correctName = _saveService.FileNames.CollectionFile(collection); if (file.FullName != correctName) - Penumbra.Messager.NotificationMessage($"Collection {file.Name} does not correspond to {collection.Name}.", NotificationType.Warning); + Penumbra.Messager.NotificationMessage($"Collection {file.Name} does not correspond to {collection.Name}.", + NotificationType.Warning); _collections.Add(collection); } @@ -220,7 +227,8 @@ public class CollectionStorage : IReadOnlyList, IDisposable return _collections[^1]; Penumbra.Messager.NotificationMessage( - $"Unknown problem creating a collection with the name {ModCollection.DefaultCollectionName}, which is required to exist.", NotificationType.Error); + $"Unknown problem creating a collection with the name {ModCollection.DefaultCollectionName}, which is required to exist.", + NotificationType.Error); return Count > 1 ? _collections[1] : _collections[0]; } @@ -273,4 +281,18 @@ public class CollectionStorage : IReadOnlyList, IDisposable _saveService.QueueSave(new ModCollectionSave(_modStorage, collection)); } } + + /// Update change counters when changing files. + private void OnModFileChanged(Mod mod, FileRegistry file) + { + if (file.CurrentUsage == 0) + return; + + foreach (var collection in this) + { + var (settings, _) = collection[mod.Index]; + if (settings is { Enabled: true }) + collection.IncrementCounter(); + } + } } diff --git a/Penumbra/Communication/ModFileChanged.cs b/Penumbra/Communication/ModFileChanged.cs new file mode 100644 index 00000000..8b4b6f5d --- /dev/null +++ b/Penumbra/Communication/ModFileChanged.cs @@ -0,0 +1,28 @@ +using OtterGui.Classes; +using Penumbra.Api; +using Penumbra.Mods; +using Penumbra.Mods.Editor; + +namespace Penumbra.Communication; + +/// +/// Triggered whenever an existing file in a mod is overwritten by Penumbra. +/// +/// Parameter is the changed mod. +/// Parameter file registry of the changed file. +/// +public sealed class ModFileChanged() + : EventWrapper(nameof(ModFileChanged)) +{ + public enum Priority + { + /// + Api = int.MinValue, + + /// + RedrawService = -50, + + /// + CollectionStorage = 0, + } +} diff --git a/Penumbra/Configuration.cs b/Penumbra/Configuration.cs index a5a615bd..188be65d 100644 --- a/Penumbra/Configuration.cs +++ b/Penumbra/Configuration.cs @@ -47,7 +47,6 @@ public class Configuration : IPluginConfiguration, ISavable public bool UseNoModsInInspect { get; set; } = false; public bool HideChangedItemFilters { get; set; } = false; public bool ReplaceNonAsciiOnImport { get; set; } = false; - public bool HidePrioritiesInSelector { get; set; } = false; public bool HideRedrawBar { get; set; } = false; public int OptionGroupCollapsibleMin { get; set; } = 5; diff --git a/Penumbra/EphemeralConfig.cs b/Penumbra/EphemeralConfig.cs index 8cf23de6..98b1a5d6 100644 --- a/Penumbra/EphemeralConfig.cs +++ b/Penumbra/EphemeralConfig.cs @@ -39,6 +39,7 @@ public class EphemeralConfig : ISavable, IDisposable public bool FixMainWindow { get; set; } = false; public string LastModPath { get; set; } = string.Empty; public bool AdvancedEditingOpen { get; set; } = false; + public bool ForceRedrawOnFileChange { get; set; } = false; /// /// Load the current configuration. diff --git a/Penumbra/Interop/Services/RedrawService.cs b/Penumbra/Interop/Services/RedrawService.cs index e2e57b1c..c1bd8573 100644 --- a/Penumbra/Interop/Services/RedrawService.cs +++ b/Penumbra/Interop/Services/RedrawService.cs @@ -8,9 +8,13 @@ using FFXIVClientStructs.FFXIV.Client.Game.Housing; using FFXIVClientStructs.Interop; using Penumbra.Api; using Penumbra.Api.Enums; +using Penumbra.Communication; using Penumbra.GameData; using Penumbra.GameData.Enums; using Penumbra.Interop.Structs; +using Penumbra.Mods; +using Penumbra.Mods.Editor; +using Penumbra.Services; using Character = FFXIVClientStructs.FFXIV.Client.Game.Character.Character; namespace Penumbra.Interop.Services; @@ -106,11 +110,13 @@ public sealed unsafe partial class RedrawService : IDisposable { private const int FurnitureIdx = 1337; - private readonly IFramework _framework; - private readonly IObjectTable _objects; - private readonly ITargetManager _targets; - private readonly ICondition _conditions; - private readonly IClientState _clientState; + private readonly IFramework _framework; + private readonly IObjectTable _objects; + private readonly ITargetManager _targets; + private readonly ICondition _conditions; + private readonly IClientState _clientState; + private readonly Configuration _config; + private readonly CommunicatorService _communicator; private readonly List _queue = new(100); private readonly List _afterGPoseQueue = new(GPoseSlots); @@ -127,19 +133,24 @@ public sealed unsafe partial class RedrawService : IDisposable public event GameObjectRedrawnDelegate? GameObjectRedrawn; - public RedrawService(IFramework framework, IObjectTable objects, ITargetManager targets, ICondition conditions, IClientState clientState) + public RedrawService(IFramework framework, IObjectTable objects, ITargetManager targets, ICondition conditions, IClientState clientState, + Configuration config, CommunicatorService communicator) { _framework = framework; _objects = objects; _targets = targets; _conditions = conditions; _clientState = clientState; + _config = config; + _communicator = communicator; _framework.Update += OnUpdateEvent; + _communicator.ModFileChanged.Subscribe(OnModFileChanged, ModFileChanged.Priority.RedrawService); } public void Dispose() { _framework.Update -= OnUpdateEvent; + _communicator.ModFileChanged.Unsubscribe(OnModFileChanged); } public static DrawState* ActorDrawState(GameObject actor) @@ -419,4 +430,12 @@ public sealed unsafe partial class RedrawService : IDisposable gameObject->DisableDraw(); } } + + private void OnModFileChanged(Mod _1, FileRegistry _2) + { + if (!_config.ForceRedrawOnFileChange) + return; + + RedrawObject(0, RedrawType.Redraw); + } } diff --git a/Penumbra/Mods/Editor/ModFileEditor.cs b/Penumbra/Mods/Editor/ModFileEditor.cs index 5328b8fe..30e97093 100644 --- a/Penumbra/Mods/Editor/ModFileEditor.cs +++ b/Penumbra/Mods/Editor/ModFileEditor.cs @@ -1,10 +1,11 @@ using Penumbra.Mods.Manager; using Penumbra.Mods.Subclasses; +using Penumbra.Services; using Penumbra.String.Classes; namespace Penumbra.Mods.Editor; -public class ModFileEditor(ModFileCollection files, ModManager modManager) +public class ModFileEditor(ModFileCollection files, ModManager modManager, CommunicatorService communicator) { public bool Changes { get; private set; } @@ -136,6 +137,7 @@ public class ModFileEditor(ModFileCollection files, ModManager modManager) try { File.Delete(file.File.FullName); + communicator.ModFileChanged.Invoke(mod, file); Penumbra.Log.Debug($"[DeleteFiles] Deleted {file.File.FullName} from {mod.Name}."); ++deletions; } diff --git a/Penumbra/Services/CommunicatorService.cs b/Penumbra/Services/CommunicatorService.cs index be94a31e..da852855 100644 --- a/Penumbra/Services/CommunicatorService.cs +++ b/Penumbra/Services/CommunicatorService.cs @@ -42,6 +42,9 @@ public class CommunicatorService : IDisposable, IService /// public readonly ModDirectoryChanged ModDirectoryChanged = new(); + /// + public readonly ModFileChanged ModFileChanged = new(); + /// public readonly ModPathChanged ModPathChanged = new(); diff --git a/Penumbra/UI/AdvancedWindow/FileEditor.cs b/Penumbra/UI/AdvancedWindow/FileEditor.cs index 89d47eb2..c891d33a 100644 --- a/Penumbra/UI/AdvancedWindow/FileEditor.cs +++ b/Penumbra/UI/AdvancedWindow/FileEditor.cs @@ -8,39 +8,32 @@ using OtterGui.Compression; using OtterGui.Raii; using OtterGui.Widgets; using Penumbra.GameData.Files; -using Penumbra.Mods; using Penumbra.Mods.Editor; +using Penumbra.Services; using Penumbra.String.Classes; using Penumbra.UI.Classes; namespace Penumbra.UI.AdvancedWindow; -public class FileEditor : IDisposable where T : class, IWritable +public class FileEditor( + ModEditWindow owner, + CommunicatorService communicator, + IDataManager gameData, + Configuration config, + FileCompactor compactor, + FileDialogService fileDialog, + string tabName, + string fileType, + Func> getFiles, + Func drawEdit, + Func getInitialPath, + Func parseFile) + : IDisposable + where T : class, IWritable { - private readonly FileDialogService _fileDialog; - private readonly IDataManager _gameData; - private readonly ModEditWindow _owner; - private readonly FileCompactor _compactor; - - public FileEditor(ModEditWindow owner, IDataManager gameData, Configuration config, FileCompactor compactor, FileDialogService fileDialog, - string tabName, string fileType, Func> getFiles, Func drawEdit, Func getInitialPath, - Func parseFile) - { - _owner = owner; - _gameData = gameData; - _fileDialog = fileDialog; - _tabName = tabName; - _fileType = fileType; - _drawEdit = drawEdit; - _getInitialPath = getInitialPath; - _parseFile = parseFile; - _compactor = compactor; - _combo = new Combo(config, getFiles); - } - public void Draw() { - using var tab = ImRaii.TabItem(_tabName); + using var tab = ImRaii.TabItem(tabName); if (!tab) { _quickImport = null; @@ -53,12 +46,26 @@ public class FileEditor : IDisposable where T : class, IWritable ImGui.SameLine(); ResetButton(); ImGui.SameLine(); + RedrawOnSaveBox(); + ImGui.SameLine(); DefaultInput(); ImGui.Dummy(new Vector2(ImGui.GetTextLineHeight() / 2)); DrawFilePanel(); } + private void RedrawOnSaveBox() + { + var redraw = config.Ephemeral.ForceRedrawOnFileChange; + if (ImGui.Checkbox("Redraw on Save", ref redraw)) + { + config.Ephemeral.ForceRedrawOnFileChange = redraw; + config.Ephemeral.Save(); + } + + ImGuiUtil.HoverTooltip("Force a redraw of your player character whenever you save a file here."); + } + public void Dispose() { (_currentFile as IDisposable)?.Dispose(); @@ -67,12 +74,6 @@ public class FileEditor : IDisposable where T : class, IWritable _defaultFile = null; } - private readonly string _tabName; - private readonly string _fileType; - private readonly Func _drawEdit; - private readonly Func _getInitialPath; - private readonly Func _parseFile; - private FileRegistry? _currentPath; private T? _currentFile; private Exception? _currentException; @@ -85,7 +86,7 @@ public class FileEditor : IDisposable where T : class, IWritable private T? _defaultFile; private Exception? _defaultException; - private readonly Combo _combo; + private readonly Combo _combo = new(config, getFiles); private ModEditWindow.QuickImportAction? _quickImport; @@ -99,16 +100,16 @@ public class FileEditor : IDisposable where T : class, IWritable { _isDefaultPathUtf8Valid = Utf8GamePath.FromString(_defaultPath, out _defaultPathUtf8, true); _quickImport = null; - _fileDialog.Reset(); + fileDialog.Reset(); try { - var file = _gameData.GetFile(_defaultPath); + var file = gameData.GetFile(_defaultPath); if (file != null) { _defaultException = null; (_defaultFile as IDisposable)?.Dispose(); _defaultFile = null; // Avoid double disposal if an exception occurs during the parsing of the new file. - _defaultFile = _parseFile(file.Data, _defaultPath, false); + _defaultFile = parseFile(file.Data, _defaultPath, false); } else { @@ -126,7 +127,7 @@ public class FileEditor : IDisposable where T : class, IWritable ImGui.SameLine(); if (ImGuiUtil.DrawDisabledButton(FontAwesomeIcon.Save.ToIconString(), new Vector2(ImGui.GetFrameHeight()), "Export this file.", _defaultFile == null, true)) - _fileDialog.OpenSavePicker($"Export {_defaultPath} to...", _fileType, Path.GetFileNameWithoutExtension(_defaultPath), _fileType, + fileDialog.OpenSavePicker($"Export {_defaultPath} to...", fileType, Path.GetFileNameWithoutExtension(_defaultPath), fileType, (success, name) => { if (!success) @@ -134,16 +135,16 @@ public class FileEditor : IDisposable where T : class, IWritable try { - _compactor.WriteAllBytes(name, _defaultFile?.Write() ?? throw new Exception("File invalid.")); + compactor.WriteAllBytes(name, _defaultFile?.Write() ?? throw new Exception("File invalid.")); } catch (Exception e) { Penumbra.Messager.NotificationMessage(e, $"Could not export {_defaultPath}.", NotificationType.Error); } - }, _getInitialPath(), false); + }, getInitialPath(), false); _quickImport ??= - ModEditWindow.QuickImportAction.Prepare(_owner, _isDefaultPathUtf8Valid ? _defaultPathUtf8 : Utf8GamePath.Empty, _defaultFile); + ModEditWindow.QuickImportAction.Prepare(owner, _isDefaultPathUtf8Valid ? _defaultPathUtf8 : Utf8GamePath.Empty, _defaultFile); ImGui.SameLine(); if (ImGuiUtil.DrawDisabledButton(FontAwesomeIcon.FileImport.ToIconString(), new Vector2(ImGui.GetFrameHeight()), $"Add a copy of this file to {_quickImport.OptionName}.", !_quickImport.CanExecute, true)) @@ -172,7 +173,7 @@ public class FileEditor : IDisposable where T : class, IWritable private void DrawFileSelectCombo() { - if (_combo.Draw("##fileSelect", _currentPath?.RelPath.ToString() ?? $"Select {_fileType} File...", string.Empty, + if (_combo.Draw("##fileSelect", _currentPath?.RelPath.ToString() ?? $"Select {fileType} File...", string.Empty, ImGui.GetContentRegionAvail().X, ImGui.GetTextLineHeight()) && _combo.CurrentSelection != null) UpdateCurrentFile(_combo.CurrentSelection); @@ -191,7 +192,7 @@ public class FileEditor : IDisposable where T : class, IWritable var bytes = File.ReadAllBytes(_currentPath.File.FullName); (_currentFile as IDisposable)?.Dispose(); _currentFile = null; // Avoid double disposal if an exception occurs during the parsing of the new file. - _currentFile = _parseFile(bytes, _currentPath.File.FullName, true); + _currentFile = parseFile(bytes, _currentPath.File.FullName, true); } catch (Exception e) { @@ -204,9 +205,11 @@ public class FileEditor : IDisposable where T : class, IWritable private void SaveButton() { if (ImGuiUtil.DrawDisabledButton("Save to File", Vector2.Zero, - $"Save the selected {_fileType} file with all changes applied. This is not revertible.", !_changed)) + $"Save the selected {fileType} file with all changes applied. This is not revertible.", !_changed)) { - _compactor.WriteAllBytes(_currentPath!.File.FullName, _currentFile!.Write()); + compactor.WriteAllBytes(_currentPath!.File.FullName, _currentFile!.Write()); + if (owner.Mod != null) + communicator.ModFileChanged.Invoke(owner.Mod, _currentPath); _changed = false; } } @@ -214,7 +217,7 @@ public class FileEditor : IDisposable where T : class, IWritable private void ResetButton() { if (ImGuiUtil.DrawDisabledButton("Reset Changes", Vector2.Zero, - $"Reset all changes made to the {_fileType} file.", !_changed)) + $"Reset all changes made to the {fileType} file.", !_changed)) { var tmp = _currentPath; _currentPath = null; @@ -232,7 +235,7 @@ public class FileEditor : IDisposable where T : class, IWritable { if (_currentFile == null) { - ImGui.TextUnformatted($"Could not parse selected {_fileType} file."); + ImGui.TextUnformatted($"Could not parse selected {fileType} file."); if (_currentException != null) { using var tab = ImRaii.PushIndent(); @@ -242,7 +245,7 @@ public class FileEditor : IDisposable where T : class, IWritable else { using var id = ImRaii.PushId(0); - _changed |= _drawEdit(_currentFile, false); + _changed |= drawEdit(_currentFile, false); } } @@ -258,7 +261,7 @@ public class FileEditor : IDisposable where T : class, IWritable if (_defaultFile == null) { - ImGui.TextUnformatted($"Could not parse provided {_fileType} game file:\n"); + ImGui.TextUnformatted($"Could not parse provided {fileType} game file:\n"); if (_defaultException != null) { using var tab = ImRaii.PushIndent(); @@ -268,7 +271,7 @@ public class FileEditor : IDisposable where T : class, IWritable else { using var id = ImRaii.PushId(1); - _drawEdit(_defaultFile, true); + drawEdit(_defaultFile, true); } } } @@ -283,7 +286,7 @@ public class FileEditor : IDisposable where T : class, IWritable protected override bool DrawSelectable(int globalIdx, bool selected) { - var file = Items[globalIdx]; + var file = Items[globalIdx]; bool ret; using (var c = ImRaii.PushColor(ImGuiCol.Text, ColorId.HandledConflictMod.Value(), file.IsOnPlayer)) { diff --git a/Penumbra/UI/AdvancedWindow/ModEditWindow.Files.cs b/Penumbra/UI/AdvancedWindow/ModEditWindow.Files.cs index bae23729..c8db7770 100644 --- a/Penumbra/UI/AdvancedWindow/ModEditWindow.Files.cs +++ b/Penumbra/UI/AdvancedWindow/ModEditWindow.Files.cs @@ -80,7 +80,7 @@ public partial class ModEditWindow return f.SubModUsage.Count == 0 ? Enumerable.Repeat((file, "Unused", string.Empty, 0x40000080u), 1) : f.SubModUsage.Select(s => (file, s.Item2.ToString(), s.Item1.FullName, - _editor.Option! == s.Item1 && _mod!.HasOptions ? 0x40008000u : 0u)); + _editor.Option! == s.Item1 && Mod!.HasOptions ? 0x40008000u : 0u)); }); void DrawLine((string, string, string, uint) data) diff --git a/Penumbra/UI/AdvancedWindow/ModEditWindow.Materials.Shpk.cs b/Penumbra/UI/AdvancedWindow/ModEditWindow.Materials.Shpk.cs index 9e9557d3..3ce10224 100644 --- a/Penumbra/UI/AdvancedWindow/ModEditWindow.Materials.Shpk.cs +++ b/Penumbra/UI/AdvancedWindow/ModEditWindow.Materials.Shpk.cs @@ -143,7 +143,7 @@ public partial class ModEditWindow { if (success) tab.LoadShpk(new FullPath(name[0])); - }, 1, _mod!.ModPath.FullName, false); + }, 1, Mod!.ModPath.FullName, false); var moddedPath = tab.FindAssociatedShpk(out var defaultPath, out var gamePath); ImGui.SameLine(); diff --git a/Penumbra/UI/AdvancedWindow/ModEditWindow.Materials.cs b/Penumbra/UI/AdvancedWindow/ModEditWindow.Materials.cs index df20d60f..fab41c7d 100644 --- a/Penumbra/UI/AdvancedWindow/ModEditWindow.Materials.cs +++ b/Penumbra/UI/AdvancedWindow/ModEditWindow.Materials.cs @@ -209,7 +209,7 @@ public partial class ModEditWindow info.Restore(); ImGui.TableNextColumn(); - ImGui.TextUnformatted(info.Path.FullName[(_mod!.ModPath.FullName.Length + 1)..]); + ImGui.TextUnformatted(info.Path.FullName[(Mod!.ModPath.FullName.Length + 1)..]); ImGui.TableNextColumn(); ImGui.SetNextItemWidth(400 * UiHelpers.Scale); var tmp = info.CurrentMaterials[0]; diff --git a/Penumbra/UI/AdvancedWindow/ModEditWindow.Meta.cs b/Penumbra/UI/AdvancedWindow/ModEditWindow.Meta.cs index 20550a15..aad70cb3 100644 --- a/Penumbra/UI/AdvancedWindow/ModEditWindow.Meta.cs +++ b/Penumbra/UI/AdvancedWindow/ModEditWindow.Meta.cs @@ -60,7 +60,7 @@ public partial class ModEditWindow CopyToClipboardButton("Copy all current manipulations to clipboard.", _iconSize, _editor.MetaEditor.Recombine()); ImGui.SameLine(); if (ImGui.Button("Write as TexTools Files")) - _metaFileManager.WriteAllTexToolsMeta(_mod!); + _metaFileManager.WriteAllTexToolsMeta(Mod!); using var child = ImRaii.Child("##meta", -Vector2.One, true); if (!child) diff --git a/Penumbra/UI/AdvancedWindow/ModEditWindow.Models.cs b/Penumbra/UI/AdvancedWindow/ModEditWindow.Models.cs index 561cbed7..67ec97f2 100644 --- a/Penumbra/UI/AdvancedWindow/ModEditWindow.Models.cs +++ b/Penumbra/UI/AdvancedWindow/ModEditWindow.Models.cs @@ -94,7 +94,7 @@ public partial class ModEditWindow { if (success && paths.Count > 0) tab.Import(paths[0]); - }, 1, _mod!.ModPath.FullName, false); + }, 1, Mod!.ModPath.FullName, false); ImGui.SameLine(); DrawDocumentationLink(MdlImportDocumentation); @@ -142,7 +142,7 @@ public partial class ModEditWindow tab.Export(path, gamePath); }, - _mod!.ModPath.FullName, + Mod!.ModPath.FullName, false ); diff --git a/Penumbra/UI/AdvancedWindow/ModEditWindow.QuickImport.cs b/Penumbra/UI/AdvancedWindow/ModEditWindow.QuickImport.cs index c9cd3d06..9a38a5d5 100644 --- a/Penumbra/UI/AdvancedWindow/ModEditWindow.QuickImport.cs +++ b/Penumbra/UI/AdvancedWindow/ModEditWindow.QuickImport.cs @@ -195,7 +195,7 @@ public partial class ModEditWindow if (subMod.Files.ContainsKey(gamePath) || subMod.FileSwaps.ContainsKey(gamePath)) return new QuickImportAction(editor, optionName, gamePath); - var mod = owner._mod; + var mod = owner.Mod; if (mod == null) return new QuickImportAction(editor, optionName, gamePath); diff --git a/Penumbra/UI/AdvancedWindow/ModEditWindow.Textures.cs b/Penumbra/UI/AdvancedWindow/ModEditWindow.Textures.cs index 34d0800c..71c64059 100644 --- a/Penumbra/UI/AdvancedWindow/ModEditWindow.Textures.cs +++ b/Penumbra/UI/AdvancedWindow/ModEditWindow.Textures.cs @@ -3,6 +3,7 @@ using OtterGui; using OtterGui.Raii; using OtterTex; using Penumbra.Import.Textures; +using Penumbra.Mods; using Penumbra.UI.Classes; namespace Penumbra.UI.AdvancedWindow; @@ -45,10 +46,10 @@ public partial class ModEditWindow using (var disabled = ImRaii.Disabled(!_center.SaveTask.IsCompleted)) { TextureDrawer.PathInputBox(_textures, tex, ref tex.TmpPath, "##input", "Import Image...", - "Can import game paths as well as your own files.", _mod!.ModPath.FullName, _fileDialog, _config.DefaultModImportPath); + "Can import game paths as well as your own files.", Mod!.ModPath.FullName, _fileDialog, _config.DefaultModImportPath); if (_textureSelectCombo.Draw("##combo", "Select the textures included in this mod on your drive or the ones they replace from the game files.", tex.Path, - _mod.ModPath.FullName.Length + 1, out var newPath) + Mod.ModPath.FullName.Length + 1, out var newPath) && newPath != tex.Path) tex.Load(_textures, newPath); @@ -84,6 +85,18 @@ public partial class ModEditWindow ImGuiUtil.SelectableHelpMarker(newDesc); } + } + + private void RedrawOnSaveBox() + { + var redraw = _config.Ephemeral.ForceRedrawOnFileChange; + if (ImGui.Checkbox("Redraw on Save", ref redraw)) + { + _config.Ephemeral.ForceRedrawOnFileChange = redraw; + _config.Ephemeral.Save(); + } + + ImGuiUtil.HoverTooltip("Force a redraw of your player character whenever you save a file here."); } private void MipMapInput() @@ -103,6 +116,8 @@ public partial class ModEditWindow if (_center.IsLoaded) { + RedrawOnSaveBox(); + ImGui.SameLine(); SaveAsCombo(); ImGui.SameLine(); MipMapInput(); @@ -118,6 +133,7 @@ public partial class ModEditWindow tt, !isActive || !canSaveInPlace || _center.IsLeftCopy && _currentSaveAs == (int)CombinedTexture.TextureSaveType.AsIs)) { _center.SaveAs(_left.Type, _textures, _left.Path, (CombinedTexture.TextureSaveType)_currentSaveAs, _addMipMaps); + InvokeChange(Mod, _left.Path); AddReloadTask(_left.Path, false); } @@ -141,6 +157,7 @@ public partial class ModEditWindow !canConvertInPlace || _left.Format is DXGIFormat.BC7Typeless or DXGIFormat.BC7UNorm or DXGIFormat.BC7UNormSRGB)) { _center.SaveAsTex(_textures, _left.Path, CombinedTexture.TextureSaveType.BC7, _left.MipMaps > 1); + InvokeChange(Mod, _left.Path); AddReloadTask(_left.Path, false); } @@ -150,6 +167,7 @@ public partial class ModEditWindow !canConvertInPlace || _left.Format is DXGIFormat.BC3Typeless or DXGIFormat.BC3UNorm or DXGIFormat.BC3UNormSRGB)) { _center.SaveAsTex(_textures, _left.Path, CombinedTexture.TextureSaveType.BC3, _left.MipMaps > 1); + InvokeChange(Mod, _left.Path); AddReloadTask(_left.Path, false); } @@ -160,6 +178,7 @@ public partial class ModEditWindow || _left.Format is DXGIFormat.B8G8R8A8UNorm or DXGIFormat.B8G8R8A8Typeless or DXGIFormat.B8G8R8A8UNormSRGB)) { _center.SaveAsTex(_textures, _left.Path, CombinedTexture.TextureSaveType.Bitmap, _left.MipMaps > 1); + InvokeChange(Mod, _left.Path); AddReloadTask(_left.Path, false); } } @@ -192,6 +211,18 @@ public partial class ModEditWindow _center.Draw(_textures, imageSize); } + private void InvokeChange(Mod? mod, string path) + { + if (mod == null) + return; + + if (!_editor.Files.Tex.FindFirst(r => string.Equals(r.File.FullName, path, StringComparison.OrdinalIgnoreCase), + out var registry)) + return; + + _communicator.ModFileChanged.Invoke(mod, registry); + } + private void OpenSaveAsDialog(string defaultExtension) { var fileName = Path.GetFileNameWithoutExtension(_left.Path.Length > 0 ? _left.Path : _right.Path); @@ -201,12 +232,13 @@ public partial class ModEditWindow if (a) { _center.SaveAs(null, _textures, b, (CombinedTexture.TextureSaveType)_currentSaveAs, _addMipMaps); + InvokeChange(Mod, b); if (b == _left.Path) AddReloadTask(_left.Path, false); else if (b == _right.Path) AddReloadTask(_right.Path, true); } - }, _mod!.ModPath.FullName, _forceTextureStartPath); + }, Mod!.ModPath.FullName, _forceTextureStartPath); _forceTextureStartPath = false; } diff --git a/Penumbra/UI/AdvancedWindow/ModEditWindow.cs b/Penumbra/UI/AdvancedWindow/ModEditWindow.cs index 38fdf482..afa846b5 100644 --- a/Penumbra/UI/AdvancedWindow/ModEditWindow.cs +++ b/Penumbra/UI/AdvancedWindow/ModEditWindow.cs @@ -49,17 +49,18 @@ public partial class ModEditWindow : Window, IDisposable private readonly IObjectTable _objects; private readonly CharacterBaseDestructor _characterBaseDestructor; - private Mod? _mod; private Vector2 _iconSize = Vector2.Zero; private bool _allowReduplicate; + public Mod? Mod { get; private set; } + public void ChangeMod(Mod mod) { - if (mod == _mod) + if (mod == Mod) return; _editor.LoadMod(mod, -1, 0); - _mod = mod; + Mod = mod; SizeConstraints = new WindowSizeConstraints { @@ -80,12 +81,12 @@ public partial class ModEditWindow : Window, IDisposable public void UpdateModels() { - if (_mod != null) - _editor.MdlMaterialEditor.ScanModels(_mod); + if (Mod != null) + _editor.MdlMaterialEditor.ScanModels(Mod); } public override bool DrawConditions() - => _mod != null; + => Mod != null; public override void PreDraw() { @@ -106,13 +107,13 @@ public partial class ModEditWindow : Window, IDisposable }); var manipulations = 0; var subMods = 0; - var swaps = _mod!.AllSubMods.Sum(m => + var swaps = Mod!.AllSubMods.Sum(m => { ++subMods; manipulations += m.Manipulations.Count; return m.FileSwaps.Count; }); - sb.Append(_mod!.Name); + sb.Append(Mod!.Name); if (subMods > 1) sb.Append($" | {subMods} Options"); @@ -271,7 +272,7 @@ public partial class ModEditWindow : Window, IDisposable ImGui.NewLine(); if (ImGui.Button("Remove Missing Files from Mod")) - _editor.FileEditor.RemoveMissingPaths(_mod!, _editor.Option!); + _editor.FileEditor.RemoveMissingPaths(Mod!, _editor.Option!); using var child = ImRaii.Child("##unusedFiles", -Vector2.One, true); if (!child) @@ -324,8 +325,8 @@ public partial class ModEditWindow : Window, IDisposable } else if (ImGuiUtil.DrawDisabledButton("Re-Duplicate and Normalize Mod", Vector2.Zero, tt, !_allowReduplicate && !modifier)) { - _editor.ModNormalizer.Normalize(_mod!); - _editor.ModNormalizer.Worker.ContinueWith(_ => _editor.LoadMod(_mod!, _editor.GroupIdx, _editor.OptionIdx)); + _editor.ModNormalizer.Normalize(Mod!); + _editor.ModNormalizer.Worker.ContinueWith(_ => _editor.LoadMod(Mod!, _editor.GroupIdx, _editor.OptionIdx)); } if (!_editor.Duplicates.Worker.IsCompleted) @@ -363,7 +364,7 @@ public partial class ModEditWindow : Window, IDisposable foreach (var (set, size, hash) in _editor.Duplicates.Duplicates.Where(s => s.Paths.Length > 1)) { ImGui.TableNextColumn(); - using var tree = ImRaii.TreeNode(set[0].FullName[(_mod!.ModPath.FullName.Length + 1)..], + using var tree = ImRaii.TreeNode(set[0].FullName[(Mod!.ModPath.FullName.Length + 1)..], ImGuiTreeNodeFlags.NoTreePushOnOpen); ImGui.TableNextColumn(); ImGuiUtil.RightAlign(Functions.HumanReadableSize(size)); @@ -384,7 +385,7 @@ public partial class ModEditWindow : Window, IDisposable { ImGui.TableNextColumn(); ImGui.TableSetBgColor(ImGuiTableBgTarget.CellBg, Colors.RedTableBgTint); - using var node = ImRaii.TreeNode(duplicate.FullName[(_mod!.ModPath.FullName.Length + 1)..], ImGuiTreeNodeFlags.Leaf); + using var node = ImRaii.TreeNode(duplicate.FullName[(Mod!.ModPath.FullName.Length + 1)..], ImGuiTreeNodeFlags.Leaf); ImGui.TableNextColumn(); ImGui.TableSetBgColor(ImGuiTableBgTarget.CellBg, Colors.RedTableBgTint); ImGui.TableNextColumn(); @@ -421,7 +422,7 @@ public partial class ModEditWindow : Window, IDisposable if (!combo) return ret; - foreach (var (option, idx) in _mod!.AllSubMods.WithIndex()) + foreach (var (option, idx) in Mod!.AllSubMods.WithIndex()) { using var id = ImRaii.PushId(idx); if (ImGui.Selectable(option.FullName, option == _editor.Option)) @@ -537,10 +538,10 @@ public partial class ModEditWindow : Window, IDisposable if (currentFile != null) return currentFile.Value; - if (_mod != null) - foreach (var option in _mod.Groups.OrderByDescending(g => g.Priority) + if (Mod != null) + foreach (var option in Mod.Groups.OrderByDescending(g => g.Priority) .SelectMany(g => g.WithIndex().OrderByDescending(o => g.OptionPriority(o.Index)).Select(g => g.Value)) - .Append(_mod.Default)) + .Append(Mod.Default)) { if (option.Files.TryGetValue(path, out var value) || option.FileSwaps.TryGetValue(path, out value)) return value; @@ -559,8 +560,8 @@ public partial class ModEditWindow : Window, IDisposable ret.Add(path); } - if (_mod != null) - foreach (var option in _mod.Groups.SelectMany(g => g).Append(_mod.Default)) + if (Mod != null) + foreach (var option in Mod.Groups.SelectMany(g => g).Append(Mod.Default)) { foreach (var path in option.Files.Keys) { @@ -596,15 +597,15 @@ public partial class ModEditWindow : Window, IDisposable _objects = objects; _framework = framework; _characterBaseDestructor = characterBaseDestructor; - _materialTab = new FileEditor(this, gameData, config, _editor.Compactor, _fileDialog, "Materials", ".mtrl", - () => PopulateIsOnPlayer(_editor.Files.Mtrl, ResourceType.Mtrl), DrawMaterialPanel, () => _mod?.ModPath.FullName ?? string.Empty, + _materialTab = new FileEditor(this, _communicator, gameData, config, _editor.Compactor, _fileDialog, "Materials", ".mtrl", + () => PopulateIsOnPlayer(_editor.Files.Mtrl, ResourceType.Mtrl), DrawMaterialPanel, () => Mod?.ModPath.FullName ?? string.Empty, (bytes, path, writable) => new MtrlTab(this, new MtrlFile(bytes), path, writable)); - _modelTab = new FileEditor(this, gameData, config, _editor.Compactor, _fileDialog, "Models", ".mdl", - () => PopulateIsOnPlayer(_editor.Files.Mdl, ResourceType.Mdl), DrawModelPanel, () => _mod?.ModPath.FullName ?? string.Empty, + _modelTab = new FileEditor(this, _communicator, gameData, config, _editor.Compactor, _fileDialog, "Models", ".mdl", + () => PopulateIsOnPlayer(_editor.Files.Mdl, ResourceType.Mdl), DrawModelPanel, () => Mod?.ModPath.FullName ?? string.Empty, (bytes, path, _) => new MdlTab(this, bytes, path)); - _shaderPackageTab = new FileEditor(this, gameData, config, _editor.Compactor, _fileDialog, "Shaders", ".shpk", + _shaderPackageTab = new FileEditor(this, _communicator, gameData, config, _editor.Compactor, _fileDialog, "Shaders", ".shpk", () => PopulateIsOnPlayer(_editor.Files.Shpk, ResourceType.Shpk), DrawShaderPackagePanel, - () => _mod?.ModPath.FullName ?? string.Empty, + () => Mod?.ModPath.FullName ?? string.Empty, (bytes, _, _) => new ShpkTab(_fileDialog, bytes)); _center = new CombinedTexture(_left, _right); _textureSelectCombo = new TextureDrawer.PathSelectCombo(textures, editor, () => GetPlayerResourcesOfType(ResourceType.Tex)); @@ -629,10 +630,10 @@ public partial class ModEditWindow : Window, IDisposable private void OnModPathChange(ModPathChangeType type, Mod mod, DirectoryInfo? _1, DirectoryInfo? _2) { - if (type is not (ModPathChangeType.Reloaded or ModPathChangeType.Moved) || mod != _mod) + if (type is not (ModPathChangeType.Reloaded or ModPathChangeType.Moved) || mod != Mod) return; - _mod = null; + Mod = null; ChangeMod(mod); } }