diff --git a/Penumbra/Api/PenumbraApi.cs b/Penumbra/Api/PenumbraApi.cs index 7105ec72..1b581f0f 100644 --- a/Penumbra/Api/PenumbraApi.cs +++ b/Penumbra/Api/PenumbraApi.cs @@ -41,12 +41,12 @@ public class PenumbraApi : IDisposable, IPenumbraApi add { CheckInitialized(); - _penumbra!.ObjectReloader.GameObjectRedrawn += value; + _penumbra!.RedrawService.GameObjectRedrawn += value; } remove { CheckInitialized(); - _penumbra!.ObjectReloader.GameObjectRedrawn -= value; + _penumbra!.RedrawService.GameObjectRedrawn -= value; } } @@ -206,25 +206,25 @@ public class PenumbraApi : IDisposable, IPenumbraApi public void RedrawObject(int tableIndex, RedrawType setting) { CheckInitialized(); - _penumbra!.ObjectReloader.RedrawObject(tableIndex, setting); + _penumbra!.RedrawService.RedrawObject(tableIndex, setting); } public void RedrawObject(string name, RedrawType setting) { CheckInitialized(); - _penumbra!.ObjectReloader.RedrawObject(name, setting); + _penumbra!.RedrawService.RedrawObject(name, setting); } public void RedrawObject(GameObject? gameObject, RedrawType setting) { CheckInitialized(); - _penumbra!.ObjectReloader.RedrawObject(gameObject, setting); + _penumbra!.RedrawService.RedrawObject(gameObject, setting); } public void RedrawAll(RedrawType setting) { CheckInitialized(); - _penumbra!.ObjectReloader.RedrawAll(setting); + _penumbra!.RedrawService.RedrawAll(setting); } public string ResolveDefaultPath(string path) diff --git a/Penumbra/Collections/CollectionManager.Active.cs b/Penumbra/Collections/CollectionManager.Active.cs index fd6da3ad..5f2042dd 100644 --- a/Penumbra/Collections/CollectionManager.Active.cs +++ b/Penumbra/Collections/CollectionManager.Active.cs @@ -9,16 +9,15 @@ using System.IO; using System.Linq; using System.Threading.Tasks; using Dalamud.Interface.Internal.Notifications; -using Dalamud.Plugin; using Penumbra.GameData.Actors; -using Penumbra.Util; using Penumbra.Services; +using Penumbra.Util; namespace Penumbra.Collections; public partial class ModCollection { - public sealed partial class Manager + public sealed partial class Manager : ISaveable { public const int Version = 1; @@ -38,8 +37,7 @@ public partial class ModCollection private ModCollection DefaultName { get; set; } = Empty; // The list of character collections. - // TODO - public readonly IndividualCollections Individuals = new(Penumbra.Actors); + public readonly IndividualCollections Individuals; public ModCollection Individual(ActorIdentifier identifier) => Individuals.TryGetCollection(identifier, out var c) ? c : Default; @@ -87,18 +85,12 @@ public partial class ModCollection var newCollection = this[newIdx]; if (newIdx > Empty.Index) - newCollection.CreateCache(); + newCollection.CreateCache(collectionType is CollectionType.Default); switch (collectionType) { case CollectionType.Default: Default = newCollection; - if (Penumbra.CharacterUtility.Ready && Penumbra.Config.EnableMods) - { - Penumbra.ResidentResources.Reload(); - Default.SetFiles(); - } - break; case CollectionType.Interface: Interface = newCollection; @@ -182,28 +174,25 @@ public partial class ModCollection public void MoveIndividualCollection(int from, int to) { if (Individuals.Move(from, to)) - SaveActiveCollections(); + Penumbra.SaveService.QueueSave(this); } // Obtain the index of a collection by name. private int GetIndexForCollectionName(string name) => name.Length == 0 ? Empty.Index : _collections.IndexOf(c => c.Name == name); - public static string ActiveCollectionFile(DalamudPluginInterface pi) - => Path.Combine(pi.ConfigDirectory.FullName, "active_collections.json"); - // Load default, current, special, and character collections from config. // Then create caches. If a collection does not exist anymore, reset it to an appropriate default. - private void LoadCollections() + private void LoadCollections(FilenameService files) { - var configChanged = !ReadActiveCollections(out var jObject); + var configChanged = !ReadActiveCollections(files, out var jObject); // Load the default collection. var defaultName = jObject[nameof(Default)]?.ToObject() ?? (configChanged ? DefaultCollection : Empty.Name); var defaultIdx = GetIndexForCollectionName(defaultName); if (defaultIdx < 0) { - ChatUtil.NotificationMessage( + Penumbra.ChatService.NotificationMessage( $"Last choice of {ConfigWindow.DefaultCollection} {defaultName} is not available, reset to {Empty.Name}.", "Load Failure", NotificationType.Warning); Default = Empty; @@ -219,7 +208,7 @@ public partial class ModCollection var interfaceIdx = GetIndexForCollectionName(interfaceName); if (interfaceIdx < 0) { - ChatUtil.NotificationMessage( + Penumbra.ChatService.NotificationMessage( $"Last choice of {ConfigWindow.InterfaceCollection} {interfaceName} is not available, reset to {Empty.Name}.", "Load Failure", NotificationType.Warning); Interface = Empty; @@ -235,7 +224,7 @@ public partial class ModCollection var currentIdx = GetIndexForCollectionName(currentName); if (currentIdx < 0) { - ChatUtil.NotificationMessage( + Penumbra.ChatService.NotificationMessage( $"Last choice of {ConfigWindow.SelectedCollection} {currentName} is not available, reset to {DefaultCollection}.", "Load Failure", NotificationType.Warning); Current = DefaultName; @@ -255,7 +244,8 @@ public partial class ModCollection var idx = GetIndexForCollectionName(typeName); if (idx < 0) { - ChatUtil.NotificationMessage($"Last choice of {name} Collection {typeName} is not available, removed.", "Load Failure", + Penumbra.ChatService.NotificationMessage($"Last choice of {name} Collection {typeName} is not available, removed.", + "Load Failure", NotificationType.Warning); configChanged = true; } @@ -271,13 +261,13 @@ public partial class ModCollection // Save any changes and create all required caches. if (configChanged) - SaveActiveCollections(); + Penumbra.SaveService.ImmediateSave(this); } // Migrate ungendered collections to Male and Female for 0.5.9.0. public static void MigrateUngenderedCollections(FilenameService fileNames) { - if (!ReadActiveCollections(out var jObject)) + if (!ReadActiveCollections(fileNames, out var jObject)) return; foreach (var (type, _, _) in CollectionTypeExtensions.Special.Where(t => t.Item2.StartsWith("Male "))) @@ -314,7 +304,7 @@ public partial class ModCollection var idx = GetIndexForCollectionName(collectionName); if (idx < 0) { - ChatUtil.NotificationMessage( + Penumbra.ChatService.NotificationMessage( $"Last choice of <{player}>'s Collection {collectionName} is not available, reset to {Empty.Name}.", "Load Failure", NotificationType.Warning); dict.Add(player, Empty); @@ -329,48 +319,11 @@ public partial class ModCollection return true; } - public void SaveActiveCollections() - { - Penumbra.Framework.RegisterDelayed(nameof(SaveActiveCollections), - SaveActiveCollectionsInternal); - } - - internal void SaveActiveCollectionsInternal() - { - // TODO - var file = ActiveCollectionFile(DalamudServices.PluginInterface); - try - { - var jObj = new JObject - { - { nameof(Version), Version }, - { nameof(Default), Default.Name }, - { nameof(Interface), Interface.Name }, - { nameof(Current), Current.Name }, - }; - foreach (var (type, collection) in _specialCollections.WithIndex().Where(p => p.Value != null) - .Select(p => ((CollectionType)p.Index, p.Value!))) - jObj.Add(type.ToString(), collection.Name); - - jObj.Add(nameof(Individuals), Individuals.ToJObject()); - using var stream = File.Open(file, File.Exists(file) ? FileMode.Truncate : FileMode.CreateNew); - using var writer = new StreamWriter(stream); - using var j = new JsonTextWriter(writer) { Formatting = Formatting.Indented }; - jObj.WriteTo(j); - Penumbra.Log.Verbose("Active Collections saved."); - } - catch (Exception e) - { - Penumbra.Log.Error($"Could not save active collections to file {file}:\n{e}"); - } - } - // Read the active collection file into a jObject. // Returns true if this is successful, false if the file does not exist or it is unsuccessful. - private static bool ReadActiveCollections(out JObject ret) + private static bool ReadActiveCollections(FilenameService files, out JObject ret) { - // TODO - var file = ActiveCollectionFile(DalamudServices.PluginInterface); + var file = files.ActiveCollectionsFile; if (File.Exists(file)) try { @@ -390,7 +343,7 @@ public partial class ModCollection private void SaveOnChange(CollectionType collectionType, ModCollection? _1, ModCollection? _2, string _3) { if (collectionType is not CollectionType.Inactive and not CollectionType.Temporary) - SaveActiveCollections(); + Penumbra.SaveService.QueueSave(this); } // Cache handling. Usually recreate caches on the next framework tick, @@ -403,7 +356,7 @@ public partial class ModCollection .Prepend(Default) .Prepend(Interface) .Distinct() - .Select(c => Task.Run(c.CalculateEffectiveFileListInternal)) + .Select(c => Task.Run(() => c.CalculateEffectiveFileListInternal(c == Default))) .ToArray(); Task.WaitAll(tasks); @@ -438,5 +391,32 @@ public partial class ModCollection foreach (var collection in this.Where(c => c.HasCache && c[mod.Index].Settings?.Enabled == true)) collection._cache!.ReloadMod(mod, true); } + + public string ToFilename(FilenameService fileNames) + => fileNames.ActiveCollectionsFile; + + public string TypeName + => "Active Collections"; + + public string LogName(string _) + => "to file"; + + public void Save(StreamWriter writer) + { + var jObj = new JObject + { + { nameof(Version), Version }, + { nameof(Default), Default.Name }, + { nameof(Interface), Interface.Name }, + { nameof(Current), Current.Name }, + }; + foreach (var (type, collection) in _specialCollections.WithIndex().Where(p => p.Value != null) + .Select(p => ((CollectionType)p.Index, p.Value!))) + jObj.Add(type.ToString(), collection.Name); + + jObj.Add(nameof(Individuals), Individuals.ToJObject()); + using var j = new JsonTextWriter(writer) { Formatting = Formatting.Indented }; + jObj.WriteTo(j); + } } } diff --git a/Penumbra/Collections/CollectionManager.cs b/Penumbra/Collections/CollectionManager.cs index a07bc5af..f65be1ad 100644 --- a/Penumbra/Collections/CollectionManager.cs +++ b/Penumbra/Collections/CollectionManager.cs @@ -8,7 +8,10 @@ using System.Diagnostics.CodeAnalysis; using System.IO; using System.Linq; using Penumbra.Api; +using Penumbra.Interop; +using Penumbra.Interop.Services; using Penumbra.Services; +using Penumbra.Util; namespace Penumbra.Collections; @@ -16,8 +19,12 @@ public partial class ModCollection { public sealed partial class Manager : IDisposable, IEnumerable { - private readonly Mod.Manager _modManager; - private readonly CommunicatorService _communicator; + private readonly Mod.Manager _modManager; + private readonly CommunicatorService _communicator; + private readonly CharacterUtility _characterUtility; + private readonly ResidentResourceManager _residentResources; + private readonly Configuration _config; + // The empty collection is always available and always has index 0. // It can not be deleted or moved. @@ -49,10 +56,16 @@ public partial class ModCollection public IEnumerable GetEnumeratorWithEmpty() => _collections; - public Manager(CommunicatorService communicator, Mod.Manager manager) + public Manager(StartTracker timer, CommunicatorService communicator, FilenameService files, CharacterUtility characterUtility, + ResidentResourceManager residentResources, Configuration config, Mod.Manager manager, IndividualCollections individuals) { - _communicator = communicator; - _modManager = manager; + using var time = timer.Measure(StartTimeType.Collections); + _communicator = communicator; + _characterUtility = characterUtility; + _residentResources = residentResources; + _config = config; + _modManager = manager; + Individuals = individuals; // The collection manager reacts to changes in mods by itself. _modManager.ModDiscoveryStarted += OnModDiscoveryStarted; @@ -61,9 +74,10 @@ public partial class ModCollection _modManager.ModPathChanged += OnModPathChange; _communicator.CollectionChange.Event += SaveOnChange; _communicator.TemporaryGlobalModChange.Event += OnGlobalModChange; - ReadCollections(); - LoadCollections(); - UpdateCurrentCollectionInUse(); + ReadCollections(files); + LoadCollections(files); + UpdateCurrentCollectionInUse(); + CreateNecessaryCaches(); } public void Dispose() @@ -77,7 +91,7 @@ public partial class ModCollection } private void OnGlobalModChange(Mod.TemporaryMod mod, bool created, bool removed) - => TempModManager.OnGlobalModChange(_collections, mod, created, removed); + => TempModManager.OnGlobalModChange(_collections, mod, created, removed); // Returns true if the name is not empty, it is not the name of the empty collection // and no existing collection results in the same filename as name. @@ -117,8 +131,9 @@ public partial class ModCollection var newCollection = duplicate?.Duplicate(name) ?? CreateNewEmpty(name); newCollection.Index = _collections.Count; - _collections.Add(newCollection); - newCollection.Save(); + _collections.Add(newCollection); + + Penumbra.SaveService.ImmediateSave(newCollection); Penumbra.Log.Debug($"Added collection {newCollection.AnonymizedName}."); _communicator.CollectionChange.Invoke(CollectionType.Inactive, null, newCollection, string.Empty); SetCollection(newCollection.Index, CollectionType.Current); @@ -165,8 +180,8 @@ public partial class ModCollection // Clear own inheritances. foreach (var inheritance in collection.Inheritance) collection.ClearSubscriptions(inheritance); - - collection.Delete(); + + Penumbra.SaveService.ImmediateDelete(collection); _collections.RemoveAt(idx); // Clear external inheritances. @@ -227,7 +242,7 @@ public partial class ModCollection case ModPathChangeType.Moved: OnModMovedActive(mod); foreach (var collection in this.Where(collection => collection.Settings[mod.Index] != null)) - collection.Save(); + Penumbra.SaveService.QueueSave(collection); break; case ModPathChangeType.StartingReload: @@ -264,7 +279,7 @@ public partial class ModCollection foreach (var collection in this) { if (collection._settings[mod.Index]?.HandleChanges(type, mod, groupIdx, optionIdx, movedToIdx) ?? false) - collection.Save(); + Penumbra.SaveService.QueueSave(collection); } // Handle changes that reload the mod if the changes did not need to be prepared, @@ -295,7 +310,7 @@ public partial class ModCollection } var defaultCollection = CreateNewEmpty(DefaultCollection); - defaultCollection.Save(); + Penumbra.SaveService.ImmediateSave(defaultCollection); defaultCollection.Index = _collections.Count; _collections.Add(defaultCollection); } @@ -322,39 +337,36 @@ public partial class ModCollection } if (changes) - collection.Save(); + Penumbra.SaveService.ImmediateSave(collection); } } // Read all collection files in the Collection Directory. // Ensure that the default named collection exists, and apply inheritances afterwards. // Duplicate collection files are not deleted, just not added here. - private void ReadCollections() + private void ReadCollections(FilenameService files) { - // TODO - var collectionDir = new DirectoryInfo(CollectionDirectory(DalamudServices.PluginInterface)); var inheritances = new List>(); - if (collectionDir.Exists) - foreach (var file in collectionDir.EnumerateFiles("*.json")) + foreach (var file in files.CollectionFiles) + { + var collection = LoadFromFile(file, out var inheritance); + if (collection == null || collection.Name.Length == 0) + continue; + + if (file.Name != $"{collection.Name.RemoveInvalidPathSymbols()}.json") + Penumbra.Log.Warning($"Collection {file.Name} does not correspond to {collection.Name}."); + + if (this[collection.Name] != null) { - var collection = LoadFromFile(file, out var inheritance); - if (collection == null || collection.Name.Length == 0) - continue; - - if (file.Name != $"{collection.Name.RemoveInvalidPathSymbols()}.json") - Penumbra.Log.Warning($"Collection {file.Name} does not correspond to {collection.Name}."); - - if (this[collection.Name] != null) - { - Penumbra.Log.Warning($"Duplicate collection found: {collection.Name} already exists."); - } - else - { - inheritances.Add(inheritance); - collection.Index = _collections.Count; - _collections.Add(collection); - } + Penumbra.Log.Warning($"Duplicate collection found: {collection.Name} already exists."); } + else + { + inheritances.Add(inheritance); + collection.Index = _collections.Count; + _collections.Add(collection); + } + } AddDefaultCollection(); ApplyInheritances(inheritances); diff --git a/Penumbra/Collections/IndividualCollections.Files.cs b/Penumbra/Collections/IndividualCollections.Files.cs index 2cfbb711..2dd67e3c 100644 --- a/Penumbra/Collections/IndividualCollections.Files.cs +++ b/Penumbra/Collections/IndividualCollections.Files.cs @@ -43,7 +43,7 @@ public partial class IndividualCollections if( group.Length == 0 || group.Any( i => !i.IsValid ) ) { changes = true; - ChatUtil.NotificationMessage( "Could not load an unknown individual collection, removed.", "Load Failure", NotificationType.Warning ); + Penumbra.ChatService.NotificationMessage( "Could not load an unknown individual collection, removed.", "Load Failure", NotificationType.Warning ); continue; } @@ -51,7 +51,7 @@ public partial class IndividualCollections if( collectionName.Length == 0 || !manager.ByName( collectionName, out var collection ) ) { changes = true; - ChatUtil.NotificationMessage( $"Could not load the collection \"{collectionName}\" as individual collection for {identifier}, set to None.", "Load Failure", + Penumbra.ChatService.NotificationMessage( $"Could not load the collection \"{collectionName}\" as individual collection for {identifier}, set to None.", "Load Failure", NotificationType.Warning ); continue; } @@ -59,14 +59,14 @@ public partial class IndividualCollections if( !Add( group, collection ) ) { changes = true; - ChatUtil.NotificationMessage( $"Could not add an individual collection for {identifier}, removed.", "Load Failure", + Penumbra.ChatService.NotificationMessage( $"Could not add an individual collection for {identifier}, removed.", "Load Failure", NotificationType.Warning ); } } catch( Exception e ) { changes = true; - ChatUtil.NotificationMessage( $"Could not load an unknown individual collection, removed:\n{e}", "Load Failure", NotificationType.Error ); + Penumbra.ChatService.NotificationMessage( $"Could not load an unknown individual collection, removed:\n{e}", "Load Failure", NotificationType.Error ); } } @@ -117,7 +117,7 @@ public partial class IndividualCollections } else { - ChatUtil.NotificationMessage( + Penumbra.ChatService.NotificationMessage( $"Could not migrate {name} ({collection.AnonymizedName}) which was assumed to be a {kind.ToName()} with IDs [{ids}], please look through your individual collections.", "Migration Failure", NotificationType.Error ); } @@ -134,13 +134,13 @@ public partial class IndividualCollections } else { - ChatUtil.NotificationMessage( $"Could not migrate {shortName} ({collection.AnonymizedName}), please look through your individual collections.", + Penumbra.ChatService.NotificationMessage( $"Could not migrate {shortName} ({collection.AnonymizedName}), please look through your individual collections.", "Migration Failure", NotificationType.Error ); } } else { - ChatUtil.NotificationMessage( + Penumbra.ChatService.NotificationMessage( $"Could not migrate {name} ({collection.AnonymizedName}), which can not be a player name nor is it a known NPC name, please look through your individual collections.", "Migration Failure", NotificationType.Error ); } diff --git a/Penumbra/Collections/ModCollection.Cache.Access.cs b/Penumbra/Collections/ModCollection.Cache.Access.cs index fc72d3dc..8daa3c04 100644 --- a/Penumbra/Collections/ModCollection.Cache.Access.cs +++ b/Penumbra/Collections/ModCollection.Cache.Access.cs @@ -28,18 +28,18 @@ public partial class ModCollection public int ChangeCounter { get; private set; } // Only create, do not update. - private void CreateCache() + private void CreateCache(bool isDefault) { if (_cache == null) { - CalculateEffectiveFileList(); + CalculateEffectiveFileList(isDefault); Penumbra.Log.Verbose($"Created new cache for collection {Name}."); } } // Force an update with metadata for this cache. private void ForceCacheUpdate() - => CalculateEffectiveFileList(); + => CalculateEffectiveFileList(this == Penumbra.CollectionManager.Default); // Handle temporary mods for this collection. public void Apply(Mod.TemporaryMod tempMod, bool created) @@ -121,11 +121,11 @@ public partial class ModCollection // Update the effective file list for the given cache. // Creates a cache if necessary. - public void CalculateEffectiveFileList() - => Penumbra.Framework.RegisterImportant(nameof(CalculateEffectiveFileList) + Name, - CalculateEffectiveFileListInternal); + public void CalculateEffectiveFileList(bool isDefault) + => Penumbra.Framework.RegisterImportant(nameof(CalculateEffectiveFileList) + Name, () => + CalculateEffectiveFileListInternal(isDefault)); - private void CalculateEffectiveFileListInternal() + private void CalculateEffectiveFileListInternal(bool isDefault) { // Skip the empty collection. if (Index == 0) @@ -133,7 +133,7 @@ public partial class ModCollection Penumbra.Log.Debug($"[{Thread.CurrentThread.ManagedThreadId}] Recalculating effective file list for {AnonymizedName}"); _cache ??= new Cache(this); - _cache.FullRecalculation(); + _cache.FullRecalculation(isDefault); Penumbra.Log.Debug($"[{Thread.CurrentThread.ManagedThreadId}] Recalculation of effective file list for {AnonymizedName} finished."); } diff --git a/Penumbra/Collections/ModCollection.Cache.cs b/Penumbra/Collections/ModCollection.Cache.cs index 1b60561d..285d119a 100644 --- a/Penumbra/Collections/ModCollection.Cache.cs +++ b/Penumbra/Collections/ModCollection.Cache.cs @@ -174,7 +174,7 @@ public partial class ModCollection break; case ModSettingChange.MultiInheritance: case ModSettingChange.MultiEnableState: - FullRecalculation(); + FullRecalculation(_collection == Penumbra.CollectionManager.Default); break; } } @@ -182,9 +182,9 @@ public partial class ModCollection // Inheritance changes are too big to check for relevance, // just recompute everything. private void OnInheritanceChange( bool _ ) - => FullRecalculation(); + => FullRecalculation(_collection == Penumbra.CollectionManager.Default); - public void FullRecalculation() + public void FullRecalculation(bool isDefault) { ResolvedFiles.Clear(); MetaManipulations.Reset(); @@ -206,7 +206,7 @@ public partial class ModCollection ++_collection.ChangeCounter; - if( _collection == Penumbra.CollectionManager.Default && Penumbra.CharacterUtility.Ready && Penumbra.Config.EnableMods ) + if( isDefault && Penumbra.CharacterUtility.Ready && Penumbra.Config.EnableMods ) { Penumbra.ResidentResources.Reload(); MetaManipulations.SetFiles(); diff --git a/Penumbra/Collections/ModCollection.Changes.cs b/Penumbra/Collections/ModCollection.Changes.cs index fead3281..9afd0ef2 100644 --- a/Penumbra/Collections/ModCollection.Changes.cs +++ b/Penumbra/Collections/ModCollection.Changes.cs @@ -142,7 +142,7 @@ public partial class ModCollection { if( !inherited ) { - Save(); + Penumbra.SaveService.QueueSave(this); } } } \ No newline at end of file diff --git a/Penumbra/Collections/ModCollection.File.cs b/Penumbra/Collections/ModCollection.File.cs index 6474cbf5..6d13deb1 100644 --- a/Penumbra/Collections/ModCollection.File.cs +++ b/Penumbra/Collections/ModCollection.File.cs @@ -1,134 +1,93 @@ -using Newtonsoft.Json; using Newtonsoft.Json.Linq; -using OtterGui.Filesystem; using Penumbra.Mods; using Penumbra.Services; using System; using System.Collections.Generic; using System.IO; using System.Linq; -using System.Text; -using Dalamud.Plugin; +using Newtonsoft.Json; +using Penumbra.Util; namespace Penumbra.Collections; // File operations like saving, loading and deleting for a collection. -public partial class ModCollection +public partial class ModCollection : ISaveable { - public static string CollectionDirectory(DalamudPluginInterface pi) - => Path.Combine( pi.GetPluginConfigDirectory(), "collections" ); - - // We need to remove all invalid path symbols from the collection name to be able to save it to file. - // TODO - public FileInfo FileName - => new(Path.Combine( CollectionDirectory(DalamudServices.PluginInterface), $"{Name.RemoveInvalidPathSymbols()}.json" )); - - // Custom serialization due to shared mod information across managers. - private void SaveCollection() - { - try - { - Penumbra.Log.Debug( $"Saving collection {AnonymizedName}..." ); - var file = FileName; - file.Directory?.Create(); - using var s = file.Exists ? file.Open( FileMode.Truncate ) : file.Open( FileMode.CreateNew ); - using var w = new StreamWriter( s, Encoding.UTF8 ); - using var j = new JsonTextWriter( w ); - j.Formatting = Formatting.Indented; - var x = JsonSerializer.Create( new JsonSerializerSettings { Formatting = Formatting.Indented } ); - j.WriteStartObject(); - j.WritePropertyName( nameof( Version ) ); - j.WriteValue( Version ); - j.WritePropertyName( nameof( Name ) ); - j.WriteValue( Name ); - j.WritePropertyName( nameof( Settings ) ); - - // Write all used and unused settings by mod directory name. - j.WriteStartObject(); - for( var i = 0; i < _settings.Count; ++i ) - { - var settings = _settings[ i ]; - if( settings != null ) - { - j.WritePropertyName( Penumbra.ModManager[ i ].ModPath.Name ); - x.Serialize( j, new ModSettings.SavedSettings( settings, Penumbra.ModManager[ i ] ) ); - } - } - - foreach( var (modDir, settings) in _unusedSettings ) - { - j.WritePropertyName( modDir ); - x.Serialize( j, settings ); - } - - j.WriteEndObject(); - - // Inherit by collection name. - j.WritePropertyName( nameof( Inheritance ) ); - x.Serialize( j, Inheritance.Select( c => c.Name ) ); - j.WriteEndObject(); - } - catch( Exception e ) - { - Penumbra.Log.Error( $"Could not save collection {AnonymizedName}:\n{e}" ); - } - } - - public void Save() - => Penumbra.Framework.RegisterDelayed( nameof( SaveCollection ) + Name, SaveCollection ); - - public void Delete() - { - if( Index == 0 ) - { - return; - } - - var file = FileName; - if( !file.Exists ) - { - return; - } - - try - { - file.Delete(); - Penumbra.Log.Information( $"Deleted collection file for {AnonymizedName}." ); - } - catch( Exception e ) - { - Penumbra.Log.Error( $"Could not delete collection file for {AnonymizedName}:\n{e}" ); - } - } - // Since inheritances depend on other collections existing, // we return them as a list to be applied after reading all collections. - private static ModCollection? LoadFromFile( FileInfo file, out IReadOnlyList< string > inheritance ) + private static ModCollection? LoadFromFile(FileInfo file, out IReadOnlyList inheritance) { - inheritance = Array.Empty< string >(); - if( !file.Exists ) + inheritance = Array.Empty(); + if (!file.Exists) { - Penumbra.Log.Error( "Could not read collection because file does not exist." ); + Penumbra.Log.Error("Could not read collection because file does not exist."); return null; } try { - var obj = JObject.Parse( File.ReadAllText( file.FullName ) ); - var name = obj[ nameof( Name ) ]?.ToObject< string >() ?? string.Empty; - var version = obj[ nameof( Version ) ]?.ToObject< int >() ?? 0; + var obj = JObject.Parse(File.ReadAllText(file.FullName)); + var name = obj[nameof(Name)]?.ToObject() ?? string.Empty; + var version = obj[nameof(Version)]?.ToObject() ?? 0; // Custom deserialization that is converted with the constructor. - var settings = obj[ nameof( Settings ) ]?.ToObject< Dictionary< string, ModSettings.SavedSettings > >() - ?? new Dictionary< string, ModSettings.SavedSettings >(); - inheritance = obj[ nameof( Inheritance ) ]?.ToObject< List< string > >() ?? ( IReadOnlyList< string > )Array.Empty< string >(); + var settings = obj[nameof(Settings)]?.ToObject>() + ?? new Dictionary(); + inheritance = obj[nameof(Inheritance)]?.ToObject>() ?? (IReadOnlyList)Array.Empty(); - return new ModCollection( name, version, settings ); + return new ModCollection(name, version, settings); } - catch( Exception e ) + catch (Exception e) { - Penumbra.Log.Error( $"Could not read collection information from file:\n{e}" ); + Penumbra.Log.Error($"Could not read collection information from file:\n{e}"); } return null; } -} \ No newline at end of file + + public string ToFilename(FilenameService fileNames) + => fileNames.CollectionFile(this); + + public string LogName(string _) + => AnonymizedName; + + public string TypeName + => "Collection"; + + public void Save(StreamWriter writer) + { + using var j = new JsonTextWriter(writer); + j.Formatting = Formatting.Indented; + var x = JsonSerializer.Create(new JsonSerializerSettings { Formatting = Formatting.Indented }); + j.WriteStartObject(); + j.WritePropertyName(nameof(Version)); + j.WriteValue(Version); + j.WritePropertyName(nameof(Name)); + j.WriteValue(Name); + j.WritePropertyName(nameof(Settings)); + + // Write all used and unused settings by mod directory name. + j.WriteStartObject(); + for (var i = 0; i < _settings.Count; ++i) + { + var settings = _settings[i]; + if (settings != null) + { + j.WritePropertyName(Penumbra.ModManager[i].ModPath.Name); + x.Serialize(j, new ModSettings.SavedSettings(settings, Penumbra.ModManager[i])); + } + } + + foreach (var (modDir, settings) in _unusedSettings) + { + j.WritePropertyName(modDir); + x.Serialize(j, settings); + } + + j.WriteEndObject(); + + // Inherit by collection name. + j.WritePropertyName(nameof(Inheritance)); + x.Serialize(j, Inheritance.Select(c => c.Name)); + j.WriteEndObject(); + } +} diff --git a/Penumbra/Collections/ModCollection.Migration.cs b/Penumbra/Collections/ModCollection.Migration.cs index bc940651..ceaac70d 100644 --- a/Penumbra/Collections/ModCollection.Migration.cs +++ b/Penumbra/Collections/ModCollection.Migration.cs @@ -1,6 +1,8 @@ using Penumbra.Mods; using System.Collections.Generic; using System.Linq; +using Penumbra.Services; +using Penumbra.Util; namespace Penumbra.Collections; @@ -9,12 +11,12 @@ public sealed partial class ModCollection // Migration to convert ModCollections from older versions to newer. private static class Migration { - public static void Migrate( ModCollection collection ) + public static void Migrate(SaveService saver, ModCollection collection ) { var changes = MigrateV0ToV1( collection ); if( changes ) - { - collection.Save(); + { + saver.ImmediateSave(collection); } } diff --git a/Penumbra/Collections/ModCollection.cs b/Penumbra/Collections/ModCollection.cs index f1060880..a8a4e1f4 100644 --- a/Penumbra/Collections/ModCollection.cs +++ b/Penumbra/Collections/ModCollection.cs @@ -4,6 +4,8 @@ using System; using System.Collections.Generic; using System.Linq; using OtterGui; +using OtterGui.Classes; +using Penumbra.Services; namespace Penumbra.Collections; @@ -27,16 +29,16 @@ public partial class ModCollection // Get the first two letters of a collection name and its Index (or None if it is the empty collection). public string AnonymizedName - => this == Empty ? Empty.Name : Name.Length > 2 ? $"{Name[ ..2 ]}... ({Index})" : $"{Name} ({Index})"; + => this == Empty ? Empty.Name : Name.Length > 2 ? $"{Name[..2]}... ({Index})" : $"{Name} ({Index})"; public int Version { get; private set; } - public int Index { get; private set; } = -1; + public int Index { get; private set; } = -1; // If a ModSetting is null, it can be inherited from other collections. // If no collection provides a setting for the mod, it is just disabled. - private readonly List< ModSettings? > _settings; + private readonly List _settings; - public IReadOnlyList< ModSettings? > Settings + public IReadOnlyList Settings => _settings; // Returns whether there are settings not in use by any current mod. @@ -47,107 +49,103 @@ public partial class ModCollection => _unusedSettings.Count; // Evaluates the settings along the whole inheritance tree. - public IEnumerable< ModSettings? > ActualSettings - => Enumerable.Range( 0, _settings.Count ).Select( i => this[ i ].Settings ); + public IEnumerable ActualSettings + => Enumerable.Range(0, _settings.Count).Select(i => this[i].Settings); // Settings for deleted mods will be kept via directory name. - private readonly Dictionary< string, ModSettings.SavedSettings > _unusedSettings; + private readonly Dictionary _unusedSettings; // Constructor for duplication. - private ModCollection( string name, ModCollection duplicate ) + private ModCollection(string name, ModCollection duplicate) { Name = name; Version = duplicate.Version; - _settings = duplicate._settings.ConvertAll( s => s?.DeepCopy() ); - _unusedSettings = duplicate._unusedSettings.ToDictionary( kvp => kvp.Key, kvp => kvp.Value.DeepCopy() ); + _settings = duplicate._settings.ConvertAll(s => s?.DeepCopy()); + _unusedSettings = duplicate._unusedSettings.ToDictionary(kvp => kvp.Key, kvp => kvp.Value.DeepCopy()); _inheritance = duplicate._inheritance.ToList(); ModSettingChanged += SaveOnChange; InheritanceChanged += SaveOnChange; } // Constructor for reading from files. - private ModCollection( string name, int version, Dictionary< string, ModSettings.SavedSettings > allSettings ) + private ModCollection(string name, int version, Dictionary allSettings) { Name = name; Version = version; _unusedSettings = allSettings; - _settings = new List< ModSettings? >(); + _settings = new List(); ApplyModSettings(); - Migration.Migrate( this ); + Migration.Migrate(Penumbra.SaveService, this); ModSettingChanged += SaveOnChange; InheritanceChanged += SaveOnChange; } // Create a new, unique empty collection of a given name. - public static ModCollection CreateNewEmpty( string name ) - => new(name, CurrentVersion, new Dictionary< string, ModSettings.SavedSettings >()); + public static ModCollection CreateNewEmpty(string name) + => new(name, CurrentVersion, new Dictionary()); // Create a new temporary collection that does not save and has a negative index. - public static ModCollection CreateNewTemporary( string name, int changeCounter ) + public static ModCollection CreateNewTemporary(string name, int changeCounter) { - var collection = new ModCollection( name, Empty ); + var collection = new ModCollection(name, Empty); collection.ModSettingChanged -= collection.SaveOnChange; collection.InheritanceChanged -= collection.SaveOnChange; collection.Index = ~Penumbra.TempCollections.Count; collection.ChangeCounter = changeCounter; - collection.CreateCache(); + collection.CreateCache(false); return collection; } // Duplicate the calling collection to a new, unique collection of a given name. - public ModCollection Duplicate( string name ) + public ModCollection Duplicate(string name) => new(name, this); // Check if a name is valid to use for a collection. // Does not check for uniqueness. - public static bool IsValidName( string name ) - => name.Length > 0 && name.All( c => !c.IsInvalidAscii() && c is not '|' && !c.IsInvalidInPath() ); + public static bool IsValidName(string name) + => name.Length > 0 && name.All(c => !c.IsInvalidAscii() && c is not '|' && !c.IsInvalidInPath()); // Remove all settings for not currently-installed mods. public void CleanUnavailableSettings() { var any = _unusedSettings.Count > 0; _unusedSettings.Clear(); - if( any ) - { - Save(); - } + if (any) + Penumbra.SaveService.QueueSave(this); } // Add settings for a new appended mod, by checking if the mod had settings from a previous deletion. - private bool AddMod( Mod mod ) + private bool AddMod(Mod mod) { - if( _unusedSettings.TryGetValue( mod.ModPath.Name, out var save ) ) + if (_unusedSettings.TryGetValue(mod.ModPath.Name, out var save)) { - var ret = save.ToSettings( mod, out var settings ); - _settings.Add( settings ); - _unusedSettings.Remove( mod.ModPath.Name ); + var ret = save.ToSettings(mod, out var settings); + _settings.Add(settings); + _unusedSettings.Remove(mod.ModPath.Name); return ret; } - _settings.Add( null ); + _settings.Add(null); return false; } // Move settings from the current mod list to the unused mod settings. - private void RemoveMod( Mod mod, int idx ) + private void RemoveMod(Mod mod, int idx) { - var settings = _settings[ idx ]; - if( settings != null ) - { - _unusedSettings[ mod.ModPath.Name ] = new ModSettings.SavedSettings( settings, mod ); - } + var settings = _settings[idx]; + if (settings != null) + _unusedSettings[mod.ModPath.Name] = new ModSettings.SavedSettings(settings, mod); - _settings.RemoveAt( idx ); + _settings.RemoveAt(idx); } // Create the always available Empty Collection that will always sit at index 0, // can not be deleted and does never create a cache. private static ModCollection CreateEmpty() { - var collection = CreateNewEmpty( EmptyCollection ); + var collection = CreateNewEmpty(EmptyCollection); collection.Index = 0; collection._settings.Clear(); return collection; @@ -156,10 +154,8 @@ public partial class ModCollection // Move all settings to unused settings for rediscovery. private void PrepareModDiscovery() { - foreach( var (mod, setting) in Penumbra.ModManager.Zip( _settings ).Where( s => s.Second != null ) ) - { - _unusedSettings[ mod.ModPath.Name ] = new ModSettings.SavedSettings( setting!, mod ); - } + foreach (var (mod, setting) in Penumbra.ModManager.Zip(_settings).Where(s => s.Second != null)) + _unusedSettings[mod.ModPath.Name] = new ModSettings.SavedSettings(setting!, mod); _settings.Clear(); } @@ -168,50 +164,44 @@ public partial class ModCollection // Also fixes invalid settings. private void ApplyModSettings() { - _settings.Capacity = Math.Max( _settings.Capacity, Penumbra.ModManager.Count ); - if( Penumbra.ModManager.Aggregate( false, ( current, mod ) => current | AddMod( mod ) ) ) - { - Save(); - } + _settings.Capacity = Math.Max(_settings.Capacity, Penumbra.ModManager.Count); + if (Penumbra.ModManager.Aggregate(false, (current, mod) => current | AddMod(mod))) + Penumbra.SaveService.ImmediateSave(this); } - public bool CopyModSettings( int modIdx, string modName, int targetIdx, string targetName ) + public bool CopyModSettings(int modIdx, string modName, int targetIdx, string targetName) { - if( targetName.Length == 0 && targetIdx < 0 || modName.Length == 0 && modIdx < 0 ) - { + if (targetName.Length == 0 && targetIdx < 0 || modName.Length == 0 && modIdx < 0) return false; - } // If the source mod exists, convert its settings to saved settings or null if its inheriting. // If it does not exist, check unused settings. // If it does not exist and has no unused settings, also use null. ModSettings.SavedSettings? savedSettings = modIdx >= 0 - ? _settings[ modIdx ] != null - ? new ModSettings.SavedSettings( _settings[ modIdx ]!, Penumbra.ModManager[ modIdx ] ) + ? _settings[modIdx] != null + ? new ModSettings.SavedSettings(_settings[modIdx]!, Penumbra.ModManager[modIdx]) : null - : _unusedSettings.TryGetValue( modName, out var s ) + : _unusedSettings.TryGetValue(modName, out var s) ? s : null; - if( targetIdx >= 0 ) + if (targetIdx >= 0) { - if( savedSettings != null ) + if (savedSettings != null) { // The target mod exists and the source settings are not inheriting, convert and fix the settings and copy them. // This triggers multiple events. - savedSettings.Value.ToSettings( Penumbra.ModManager[ targetIdx ], out var settings ); - SetModState( targetIdx, settings.Enabled ); - SetModPriority( targetIdx, settings.Priority ); - foreach( var (value, index) in settings.Settings.WithIndex() ) - { - SetModSetting( targetIdx, index, value ); - } + savedSettings.Value.ToSettings(Penumbra.ModManager[targetIdx], out var settings); + SetModState(targetIdx, settings.Enabled); + SetModPriority(targetIdx, settings.Priority); + foreach (var (value, index) in settings.Settings.WithIndex()) + SetModSetting(targetIdx, index, value); } else { // The target mod exists, but the source is inheriting, set the target to inheriting. // This triggers events. - SetModInheritance( targetIdx, true ); + SetModInheritance(targetIdx, true); } } else @@ -219,14 +209,10 @@ public partial class ModCollection // The target mod does not exist. // Either copy the unused source settings directly if they are not inheriting, // or remove any unused settings for the target if they are inheriting. - if( savedSettings != null ) - { - _unusedSettings[ targetName ] = savedSettings.Value; - } + if (savedSettings != null) + _unusedSettings[targetName] = savedSettings.Value; else - { - _unusedSettings.Remove( targetName ); - } + _unusedSettings.Remove(targetName); } return true; @@ -234,4 +220,4 @@ public partial class ModCollection public override string ToString() => Name; -} \ No newline at end of file +} diff --git a/Penumbra/CommandHandler.cs b/Penumbra/CommandHandler.cs index 63922566..760a4870 100644 --- a/Penumbra/CommandHandler.cs +++ b/Penumbra/CommandHandler.cs @@ -2,6 +2,7 @@ using System; using System.Linq; using System.Runtime.CompilerServices; using Dalamud.Game.Command; +using Dalamud.Game.Gui; using Dalamud.Game.Text.SeStringHandling; using ImGuiNET; using Penumbra.Api.Enums; @@ -9,227 +10,184 @@ using Penumbra.Collections; using Penumbra.GameData.Actors; using Penumbra.Interop; using Penumbra.Mods; -using Penumbra.Services; +using Penumbra.Services; using Penumbra.UI; +using Penumbra.Util; namespace Penumbra; -public static class SeStringBuilderExtensions -{ - public const ushort Green = 504; - public const ushort Yellow = 31; - public const ushort Red = 534; - public const ushort Blue = 517; - public const ushort White = 1; - public const ushort Purple = 541; - - public static SeStringBuilder AddText( this SeStringBuilder sb, string text, int color, bool brackets = false ) - => sb.AddUiForeground( ( ushort )color ).AddText( brackets ? $"[{text}]" : text ).AddUiForegroundOff(); - - public static SeStringBuilder AddGreen( this SeStringBuilder sb, string text, bool brackets = false ) - => AddText( sb, text, Green, brackets ); - - public static SeStringBuilder AddYellow( this SeStringBuilder sb, string text, bool brackets = false ) - => AddText( sb, text, Yellow, brackets ); - - public static SeStringBuilder AddRed( this SeStringBuilder sb, string text, bool brackets = false ) - => AddText( sb, text, Red, brackets ); - - public static SeStringBuilder AddBlue( this SeStringBuilder sb, string text, bool brackets = false ) - => AddText( sb, text, Blue, brackets ); - - public static SeStringBuilder AddWhite( this SeStringBuilder sb, string text, bool brackets = false ) - => AddText( sb, text, White, brackets ); - - public static SeStringBuilder AddPurple( this SeStringBuilder sb, string text, bool brackets = false ) - => AddText( sb, text, Purple, brackets ); - - public static SeStringBuilder AddCommand( this SeStringBuilder sb, string command, string description ) - => sb.AddText( " 》 " ) - .AddBlue( command ) - .AddText( $" - {description}" ); - - public static SeStringBuilder AddInitialPurple( this SeStringBuilder sb, string word, bool withComma = true ) - => sb.AddPurple( $"[{word[ 0 ]}]" ) - .AddText( withComma ? $"{word[ 1.. ]}, " : word[ 1.. ] ); -} - public class CommandHandler : IDisposable { private const string CommandName = "/penumbra"; private readonly CommandManager _commandManager; - private readonly ObjectReloader _objectReloader; + private readonly RedrawService _redrawService; + private readonly ChatGui _chat; private readonly Configuration _config; - private readonly Penumbra _penumbra; private readonly ConfigWindow _configWindow; private readonly ActorManager _actors; private readonly Mod.Manager _modManager; private readonly ModCollection.Manager _collectionManager; + private readonly Penumbra _penumbra; - public CommandHandler( CommandManager commandManager, ObjectReloader objectReloader, Configuration config, Penumbra penumbra, ConfigWindow configWindow, Mod.Manager modManager, - ModCollection.Manager collectionManager, ActorManager actors ) + public CommandHandler(CommandManager commandManager, ChatGui chat, RedrawService redrawService, Configuration config, + ConfigWindow configWindow, Mod.Manager modManager, ModCollection.Manager collectionManager, ActorService actors, Penumbra penumbra) { _commandManager = commandManager; - _objectReloader = objectReloader; + _redrawService = redrawService; _config = config; - _penumbra = penumbra; _configWindow = configWindow; _modManager = modManager; _collectionManager = collectionManager; - _actors = actors; - _commandManager.AddHandler( CommandName, new CommandInfo( OnCommand ) + _actors = actors.AwaitedService; + _chat = chat; + _penumbra = penumbra; + _commandManager.AddHandler(CommandName, new CommandInfo(OnCommand) { HelpMessage = "Without arguments, toggles the main window. Use /penumbra help to get further command help.", ShowInHelp = true, - } ); + }); } public void Dispose() { - _commandManager.RemoveHandler( CommandName ); + _commandManager.RemoveHandler(CommandName); } - private void OnCommand( string command, string arguments ) + private void OnCommand(string command, string arguments) { - if( arguments.Length == 0 ) - { + if (arguments.Length == 0) arguments = "window"; - } - var argumentList = arguments.Split( ' ', 2 ); - arguments = argumentList.Length == 2 ? argumentList[ 1 ] : string.Empty; + var argumentList = arguments.Split(' ', 2); + arguments = argumentList.Length == 2 ? argumentList[1] : string.Empty; - var _ = argumentList[ 0 ].ToLowerInvariant() switch + var _ = argumentList[0].ToLowerInvariant() switch { - "window" => ToggleWindow( arguments ), - "enable" => SetPenumbraState( arguments, true ), - "disable" => SetPenumbraState( arguments, false ), - "toggle" => SetPenumbraState( arguments, null ), - "reload" => Reload( arguments ), - "redraw" => Redraw( arguments ), - "lockui" => SetUiLockState( arguments ), - "debug" => SetDebug( arguments ), - "collection" => SetCollection( arguments ), - "mod" => SetMod( arguments ), - "bulktag" => SetTag( arguments ), - _ => PrintHelp( argumentList[ 0 ] ), + "window" => ToggleWindow(arguments), + "enable" => SetPenumbraState(arguments, true), + "disable" => SetPenumbraState(arguments, false), + "toggle" => SetPenumbraState(arguments, null), + "reload" => Reload(arguments), + "redraw" => Redraw(arguments), + "lockui" => SetUiLockState(arguments), + "debug" => SetDebug(arguments), + "collection" => SetCollection(arguments), + "mod" => SetMod(arguments), + "bulktag" => SetTag(arguments), + _ => PrintHelp(argumentList[0]), }; } - private static bool PrintHelp( string arguments ) + private bool PrintHelp(string arguments) { - if( !string.Equals( arguments, "help", StringComparison.OrdinalIgnoreCase ) && arguments == "?" ) - { - DalamudServices.Chat.Print( new SeStringBuilder().AddText( "The given argument " ).AddRed( arguments, true ).AddText( " is not valid. Valid arguments are:" ).BuiltString ); - } + if (!string.Equals(arguments, "help", StringComparison.OrdinalIgnoreCase) && arguments == "?") + _chat.Print(new SeStringBuilder().AddText("The given argument ").AddRed(arguments, true) + .AddText(" is not valid. Valid arguments are:").BuiltString); else - { - DalamudServices.Chat.Print( "Valid arguments for /penumbra are:" ); - } + _chat.Print("Valid arguments for /penumbra are:"); - DalamudServices.Chat.Print( new SeStringBuilder().AddCommand( "window", - "Toggle the Penumbra main config window. Can be used with [on|off] to force specific state. Also used when no argument is provided." ).BuiltString ); - DalamudServices.Chat.Print( new SeStringBuilder().AddCommand( "enable", "Enable modding and force a redraw of all game objects if it was previously disabled." ).BuiltString ); - DalamudServices.Chat.Print( new SeStringBuilder().AddCommand( "disable", "Disable modding and force a redraw of all game objects if it was previously enabled." ).BuiltString ); - DalamudServices.Chat.Print( new SeStringBuilder().AddCommand( "toggle", "Toggle modding and force a redraw of all game objects." ).BuiltString ); - DalamudServices.Chat.Print( new SeStringBuilder().AddCommand( "reload", "Rediscover the mod directory and reload all mods." ).BuiltString ); - DalamudServices.Chat.Print( new SeStringBuilder().AddCommand( "redraw", "Redraw all game objects. Specify a placeholder or a name to redraw specific objects." ).BuiltString ); - DalamudServices.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 ); - DalamudServices.Chat.Print( new SeStringBuilder().AddCommand( "debug", "Toggle debug mode for Penumbra. Can be used with [on|off] to force specific state." ).BuiltString ); - DalamudServices.Chat.Print( new SeStringBuilder().AddCommand( "collection", "Change your active collection setup. Use without further parameters for more detailed help." ) - .BuiltString ); - DalamudServices.Chat.Print( new SeStringBuilder().AddCommand( "mod", "Change a specific mods settings. Use without further parameters for more detailed help." ).BuiltString ); - DalamudServices.Chat.Print( new SeStringBuilder() - .AddCommand( "bulktag", "Change multiple mods settings based on their tags. Use without further parameters for more detailed help." ) - .BuiltString ); + _chat.Print(new SeStringBuilder().AddCommand("window", + "Toggle the Penumbra main config window. Can be used with [on|off] to force specific state. Also used when no argument is provided.") + .BuiltString); + _chat.Print(new SeStringBuilder() + .AddCommand("enable", "Enable modding and force a redraw of all game objects if it was previously disabled.").BuiltString); + _chat.Print(new SeStringBuilder() + .AddCommand("disable", "Disable modding and force a redraw of all game objects if it was previously enabled.").BuiltString); + _chat.Print(new SeStringBuilder().AddCommand("toggle", "Toggle modding and force a redraw of all game objects.") + .BuiltString); + _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("debug", "Toggle debug mode for Penumbra. Can be used with [on|off] to force specific state.").BuiltString); + _chat.Print(new SeStringBuilder() + .AddCommand("collection", "Change your active collection setup. Use without further parameters for more detailed help.") + .BuiltString); + _chat.Print(new SeStringBuilder() + .AddCommand("mod", "Change a specific mods settings. Use without further parameters for more detailed help.").BuiltString); + _chat.Print(new SeStringBuilder() + .AddCommand("bulktag", "Change multiple mods settings based on their tags. Use without further parameters for more detailed help.") + .BuiltString); return true; } - private bool ToggleWindow( string arguments ) + private bool ToggleWindow(string arguments) { - var value = ParseTrueFalseToggle( arguments ) ?? !_configWindow.IsOpen; - if( value == _configWindow.IsOpen ) - { + var value = ParseTrueFalseToggle(arguments) ?? !_configWindow.IsOpen; + if (value == _configWindow.IsOpen) return false; - } _configWindow.Toggle(); return true; } - private bool Reload( string _ ) + private bool Reload(string _) { _modManager.DiscoverMods(); - Print( $"Reloaded Penumbra mods. You have {_modManager.Count} mods." ); + Print($"Reloaded Penumbra mods. You have {_modManager.Count} mods."); return true; } - private bool Redraw( string arguments ) + private bool Redraw(string arguments) { - if( arguments.Length > 0 ) - { - _objectReloader.RedrawObject( arguments, RedrawType.Redraw ); - } + if (arguments.Length > 0) + _redrawService.RedrawObject(arguments, RedrawType.Redraw); else - { - _objectReloader.RedrawAll( RedrawType.Redraw ); - } + _redrawService.RedrawAll(RedrawType.Redraw); return true; } - private bool SetDebug( string arguments ) + private bool SetDebug(string arguments) { - var value = ParseTrueFalseToggle( arguments ) ?? !_config.DebugMode; - if( value == _config.DebugMode ) - { + var value = ParseTrueFalseToggle(arguments) ?? !_config.DebugMode; + if (value == _config.DebugMode) return false; - } - Print( value ? "Debug mode enabled." : "Debug mode disabled." ); + Print(value ? "Debug mode enabled." : "Debug mode disabled."); _config.DebugMode = value; _config.Save(); return true; } - private bool SetPenumbraState( string _, bool? newValue ) + private bool SetPenumbraState(string _, bool? newValue) { var value = newValue ?? !_config.EnableMods; - if( value == _config.EnableMods ) + if (value == _config.EnableMods) { - Print( value + Print(value ? "Your mods are already enabled. To disable your mods, please run the following command instead: /penumbra disable" - : "Your mods are already disabled. To enable your mods, please run the following command instead: /penumbra enable" ); + : "Your mods are already disabled. To enable your mods, please run the following command instead: /penumbra enable"); return false; } - Print( value + Print(value ? "Your mods have been enabled." - : "Your mods have been disabled." ); - return _penumbra.SetEnabled( value ); + : "Your mods have been disabled."); + return _penumbra.SetEnabled(value); } - private bool SetUiLockState( string arguments ) + private bool SetUiLockState(string arguments) { - var value = ParseTrueFalseToggle( arguments ) ?? !_config.FixMainWindow; - if( value == _config.FixMainWindow ) - { + var value = ParseTrueFalseToggle(arguments) ?? !_config.FixMainWindow; + if (value == _config.FixMainWindow) return false; - } - if( value ) + if (value) { - Print( "Penumbra UI locked in place." ); + Print("Penumbra UI locked in place."); _configWindow.Flags |= ImGuiWindowFlags.NoMove | ImGuiWindowFlags.NoResize; } else { - Print( "Penumbra UI unlocked." ); - _configWindow.Flags &= ~( ImGuiWindowFlags.NoMove | ImGuiWindowFlags.NoResize ); + Print("Penumbra UI unlocked."); + _configWindow.Flags &= ~(ImGuiWindowFlags.NoMove | ImGuiWindowFlags.NoResize); } _config.FixMainWindow = value; @@ -237,260 +195,274 @@ public class CommandHandler : IDisposable return true; } - private bool SetCollection( string arguments ) + private bool SetCollection(string arguments) { - if( arguments.Length == 0 ) + if (arguments.Length == 0) { - DalamudServices.Chat.Print( new SeStringBuilder().AddText( "Use with /penumbra collection " ).AddBlue( "[Collection Type]" ).AddText( " | " ).AddYellow( "[Collection Name]" ) - .AddText( " | " ).AddGreen( "" ).BuiltString ); - DalamudServices.Chat.Print( new SeStringBuilder().AddText( " 》 Valid Collection Types are " ).AddBlue( "Base" ).AddText( ", " ).AddBlue( "Ui" ).AddText( ", " ) - .AddBlue( "Selected" ).AddText( ", " ) - .AddBlue( "Individual" ).AddText( ", and all those selectable in Character Groups." ).BuiltString ); - DalamudServices.Chat.Print( new SeStringBuilder().AddText( " 》 Valid Collection Names are " ).AddYellow( "None" ) - .AddText( ", all collections you have created by their full names, and " ).AddYellow( "Delete" ).AddText( " to remove assignments (not valid for all types)." ) - .BuiltString ); - DalamudServices.Chat.Print( new SeStringBuilder().AddText( " 》 If the type is " ).AddBlue( "Individual" ) - .AddText( " you need to specify an individual with an identifier of the form:" ).BuiltString ); - DalamudServices.Chat.Print( new SeStringBuilder().AddText( " 》》》 " ).AddGreen( "" ).AddText( " or " ).AddGreen( "" ).AddText( " or " ).AddGreen( "" ) - .AddText( " or " ).AddGreen( "" ).AddText( " as placeholders for your character, your target, your mouseover or your focus, if they exist." ).BuiltString ); - DalamudServices.Chat.Print( new SeStringBuilder().AddText( " 》》》 " ).AddGreen( "p" ).AddText( " | " ).AddWhite( "[Player Name]@" ) - .AddText( ", if no @ is provided, Any World is used." ).BuiltString ); - DalamudServices.Chat.Print( new SeStringBuilder().AddText( " 》》》 " ).AddGreen( "r" ).AddText( " | " ).AddWhite( "[Retainer Name]" ).BuiltString ); - DalamudServices.Chat.Print( new SeStringBuilder().AddText( " 》》》 " ).AddGreen( "n" ).AddText( " | " ).AddPurple( "[NPC Type]" ).AddText( " : " ) - .AddRed( "[NPC Name]" ).AddText( ", where NPC Type can be " ).AddInitialPurple( "Mount" ).AddInitialPurple( "Companion" ).AddInitialPurple( "Accessory" ) - .AddInitialPurple( "Event NPC" ).AddText( "or " ) - .AddInitialPurple( "Battle NPC", false ).AddText( "." ).BuiltString ); - DalamudServices.Chat.Print( new SeStringBuilder().AddText( " 》》》 " ).AddGreen( "o" ).AddText( " | " ).AddPurple( "[NPC Type]" ).AddText( " : " ) - .AddRed( "[NPC Name]" ).AddText( " | " ).AddWhite( "[Player Name]@" ).AddText( "." ).BuiltString ); + _chat.Print(new SeStringBuilder().AddText("Use with /penumbra collection ").AddBlue("[Collection Type]") + .AddText(" | ").AddYellow("[Collection Name]") + .AddText(" | ").AddGreen("").BuiltString); + _chat.Print(new SeStringBuilder().AddText(" 》 Valid Collection Types are ").AddBlue("Base").AddText(", ") + .AddBlue("Ui").AddText(", ") + .AddBlue("Selected").AddText(", ") + .AddBlue("Individual").AddText(", and all those selectable in Character Groups.").BuiltString); + _chat.Print(new SeStringBuilder().AddText(" 》 Valid Collection Names are ").AddYellow("None") + .AddText(", all collections you have created by their full names, and ").AddYellow("Delete") + .AddText(" to remove assignments (not valid for all types).") + .BuiltString); + _chat.Print(new SeStringBuilder().AddText(" 》 If the type is ").AddBlue("Individual") + .AddText(" you need to specify an individual with an identifier of the form:").BuiltString); + _chat.Print(new SeStringBuilder().AddText(" 》》》 ").AddGreen("").AddText(" or ").AddGreen("") + .AddText(" or ").AddGreen("") + .AddText(" or ").AddGreen("") + .AddText(" as placeholders for your character, your target, your mouseover or your focus, if they exist.").BuiltString); + _chat.Print(new SeStringBuilder().AddText(" 》》》 ").AddGreen("p").AddText(" | ") + .AddWhite("[Player Name]@") + .AddText(", if no @ is provided, Any World is used.").BuiltString); + _chat.Print(new SeStringBuilder().AddText(" 》》》 ").AddGreen("r").AddText(" | ").AddWhite("[Retainer Name]") + .BuiltString); + _chat.Print(new SeStringBuilder().AddText(" 》》》 ").AddGreen("n").AddText(" | ").AddPurple("[NPC Type]") + .AddText(" : ") + .AddRed("[NPC Name]").AddText(", where NPC Type can be ").AddInitialPurple("Mount").AddInitialPurple("Companion") + .AddInitialPurple("Accessory") + .AddInitialPurple("Event NPC").AddText("or ") + .AddInitialPurple("Battle NPC", false).AddText(".").BuiltString); + _chat.Print(new SeStringBuilder().AddText(" 》》》 ").AddGreen("o").AddText(" | ").AddPurple("[NPC Type]") + .AddText(" : ") + .AddRed("[NPC Name]").AddText(" | ").AddWhite("[Player Name]@").AddText(".").BuiltString); return true; } - var split = arguments.Split( '|', 3, StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries ); - var typeName = split[ 0 ]; + var split = arguments.Split('|', 3, StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries); + var typeName = split[0]; - if( !CollectionTypeExtensions.TryParse( typeName, out var type ) ) + if (!CollectionTypeExtensions.TryParse(typeName, out var type)) { - DalamudServices.Chat.Print( new SeStringBuilder().AddText( "The argument " ).AddRed( typeName, true ).AddText( " is not a valid collection type." ).BuiltString ); + _chat.Print(new SeStringBuilder().AddText("The argument ").AddRed(typeName, true) + .AddText(" is not a valid collection type.").BuiltString); return false; } - if( split.Length == 1 ) + if (split.Length == 1) { - DalamudServices.Chat.Print( "There was no collection name provided." ); + _chat.Print("There was no collection name provided."); return false; } - if( !GetModCollection( split[ 1 ], out var collection ) ) - { + if (!GetModCollection(split[1], out var collection)) return false; - } var identifier = ActorIdentifier.Invalid; - if( type is CollectionType.Individual ) + if (type is CollectionType.Individual) { - if( split.Length == 2 ) + if (split.Length == 2) { - DalamudServices.Chat.Print( "Setting an individual collection requires a collection name and an identifier, but no identifier was provided." ); + _chat.Print( + "Setting an individual collection requires a collection name and an identifier, but no identifier was provided."); return false; } try { - if( ObjectReloader.GetName( split[ 2 ].ToLowerInvariant(), out var obj ) ) + if (_redrawService.GetName(split[2].ToLowerInvariant(), out var obj)) { - identifier = _actors.FromObject( obj, false, true, true ); - if( !identifier.IsValid ) + identifier = _actors.FromObject(obj, false, true, true); + if (!identifier.IsValid) { - DalamudServices.Chat.Print( new SeStringBuilder().AddText( "The placeholder " ).AddGreen( split[ 2 ] ) - .AddText( " did not resolve to a game object with a valid identifier." ).BuiltString ); + _chat.Print(new SeStringBuilder().AddText("The placeholder ").AddGreen(split[2]) + .AddText(" did not resolve to a game object with a valid identifier.").BuiltString); return false; } } else { - identifier = _actors.FromUserString( split[ 2 ] ); + identifier = _actors.FromUserString(split[2]); } } - catch( ActorManager.IdentifierParseError e ) + catch (ActorManager.IdentifierParseError e) { - DalamudServices.Chat.Print( new SeStringBuilder().AddText( "The argument " ).AddRed( split[ 2 ], true ).AddText( $" could not be converted to an identifier. {e.Message}" ) - .BuiltString ); + _chat.Print(new SeStringBuilder().AddText("The argument ").AddRed(split[2], true) + .AddText($" could not be converted to an identifier. {e.Message}") + .BuiltString); return false; } } - var oldCollection = _collectionManager.ByType( type, identifier ); - if( collection == oldCollection ) + var oldCollection = _collectionManager.ByType(type, identifier); + if (collection == oldCollection) { - DalamudServices.Chat.Print( collection == null - ? $"The {type.ToName()} Collection{( identifier.IsValid ? $" for {identifier}" : string.Empty )} is already unassigned" - : $"{collection.Name} already is the {type.ToName()} Collection{( identifier.IsValid ? $" for {identifier}." : "." )}" ); + _chat.Print(collection == null + ? $"The {type.ToName()} Collection{(identifier.IsValid ? $" for {identifier}" : string.Empty)} is already unassigned" + : $"{collection.Name} already is the {type.ToName()} Collection{(identifier.IsValid ? $" for {identifier}." : ".")}"); return false; } - var individualIndex = _collectionManager.Individuals.Index( identifier ); + var individualIndex = _collectionManager.Individuals.Index(identifier); - if( oldCollection == null ) + if (oldCollection == null) { - if( type.IsSpecial() ) + if (type.IsSpecial()) { - _collectionManager.CreateSpecialCollection( type ); + _collectionManager.CreateSpecialCollection(type); } - else if( identifier.IsValid ) + else if (identifier.IsValid) { - var identifiers = _collectionManager.Individuals.GetGroup( identifier ); + var identifiers = _collectionManager.Individuals.GetGroup(identifier); individualIndex = _collectionManager.Individuals.Count; - _collectionManager.CreateIndividualCollection( identifiers ); + _collectionManager.CreateIndividualCollection(identifiers); } } - else if( collection == null ) + else if (collection == null) { - if( type.IsSpecial() ) + if (type.IsSpecial()) { - _collectionManager.RemoveSpecialCollection( type ); + _collectionManager.RemoveSpecialCollection(type); } - else if( individualIndex >= 0 ) + else if (individualIndex >= 0) { - _collectionManager.RemoveIndividualCollection( individualIndex ); + _collectionManager.RemoveIndividualCollection(individualIndex); } else { - DalamudServices.Chat.Print( $"Can not remove the {type.ToName()} Collection assignment {( identifier.IsValid ? $" for {identifier}." : "." )}" ); + _chat.Print( + $"Can not remove the {type.ToName()} Collection assignment {(identifier.IsValid ? $" for {identifier}." : ".")}"); return false; } - Print( $"Removed {oldCollection.Name} as {type.ToName()} Collection assignment {( identifier.IsValid ? $" for {identifier}." : "." )}" ); + Print( + $"Removed {oldCollection.Name} as {type.ToName()} Collection assignment {(identifier.IsValid ? $" for {identifier}." : ".")}"); return true; } - _collectionManager.SetCollection( collection!, type, individualIndex ); - Print( $"Assigned {collection!.Name} as {type.ToName()} Collection{( identifier.IsValid ? $" for {identifier}." : "." )}" ); + _collectionManager.SetCollection(collection!, type, individualIndex); + Print($"Assigned {collection!.Name} as {type.ToName()} Collection{(identifier.IsValid ? $" for {identifier}." : ".")}"); return true; } - private bool SetMod( string arguments ) + private bool SetMod(string arguments) { - if( arguments.Length == 0 ) + if (arguments.Length == 0) { var seString = new SeStringBuilder() - .AddText( "Use with /penumbra mod " ).AddBlue( "[enable|disable|inherit|toggle]" ).AddText( " " ).AddYellow( "[Collection Name]" ).AddText( " | " ) - .AddPurple( "[Mod Name or Mod Directory Name]" ); - DalamudServices.Chat.Print( seString.BuiltString ); + .AddText("Use with /penumbra mod ").AddBlue("[enable|disable|inherit|toggle]").AddText(" ").AddYellow("[Collection Name]") + .AddText(" | ") + .AddPurple("[Mod Name or Mod Directory Name]"); + _chat.Print(seString.BuiltString); return true; } - var split = arguments.Split( ' ', 2, StringSplitOptions.TrimEntries | StringSplitOptions.RemoveEmptyEntries ); - var nameSplit = split.Length != 2 ? Array.Empty< string >() : split[ 1 ].Split( '|', 2, StringSplitOptions.TrimEntries | StringSplitOptions.RemoveEmptyEntries ); - if( nameSplit.Length != 2 ) + var split = arguments.Split(' ', 2, StringSplitOptions.TrimEntries | StringSplitOptions.RemoveEmptyEntries); + var nameSplit = split.Length != 2 + ? Array.Empty() + : split[1].Split('|', 2, StringSplitOptions.TrimEntries | StringSplitOptions.RemoveEmptyEntries); + if (nameSplit.Length != 2) { - DalamudServices.Chat.Print( "Not enough arguments provided." ); + _chat.Print("Not enough arguments provided."); return false; } - var state = ConvertToSettingState( split[ 0 ] ); - if( state == -1 ) + var state = ConvertToSettingState(split[0]); + if (state == -1) { - DalamudServices.Chat.Print( new SeStringBuilder().AddRed( split[ 0 ], true ).AddText( " is not a valid type of setting." ).BuiltString ); + _chat.Print(new SeStringBuilder().AddRed(split[0], true).AddText(" is not a valid type of setting.").BuiltString); return false; } - if( !GetModCollection( nameSplit[ 0 ], out var collection ) || collection == ModCollection.Empty ) + if (!GetModCollection(nameSplit[0], out var collection) || collection == ModCollection.Empty) + return false; + + if (!_modManager.TryGetMod(nameSplit[1], nameSplit[1], out var mod)) { + _chat.Print(new SeStringBuilder().AddText("The mod ").AddRed(nameSplit[1], true).AddText(" does not exist.") + .BuiltString); return false; } - if( !_modManager.TryGetMod( nameSplit[ 1 ], nameSplit[ 1 ], out var mod ) ) - { - DalamudServices.Chat.Print( new SeStringBuilder().AddText( "The mod " ).AddRed( nameSplit[ 1 ], true ).AddText( " does not exist." ).BuiltString ); - return false; - } - - if( HandleModState( state, collection!, mod ) ) - { + if (HandleModState(state, collection!, mod)) return true; - } - DalamudServices.Chat.Print( new SeStringBuilder().AddText( "Mod " ).AddPurple( mod.Name, true ).AddText( "already had the desired state in collection " ) - .AddYellow( collection!.Name, true ).AddText( "." ).BuiltString ); + _chat.Print(new SeStringBuilder().AddText("Mod ").AddPurple(mod.Name, true) + .AddText("already had the desired state in collection ") + .AddYellow(collection!.Name, true).AddText(".").BuiltString); return false; } - private bool SetTag( string arguments ) + private bool SetTag(string arguments) { - if( arguments.Length == 0 ) + if (arguments.Length == 0) { var seString = new SeStringBuilder() - .AddText( "Use with /penumbra bulktag " ).AddBlue( "[enable|disable|toggle|inherit]" ).AddText( " " ).AddYellow( "[Collection Name]" ).AddText( " | " ) - .AddPurple( "[Local Tag]" ); - DalamudServices.Chat.Print( seString.BuiltString ); + .AddText("Use with /penumbra bulktag ").AddBlue("[enable|disable|toggle|inherit]").AddText(" ").AddYellow("[Collection Name]") + .AddText(" | ") + .AddPurple("[Local Tag]"); + _chat.Print(seString.BuiltString); return true; } - var split = arguments.Split( ' ', 2, StringSplitOptions.TrimEntries | StringSplitOptions.RemoveEmptyEntries ); - var nameSplit = split.Length != 2 ? Array.Empty< string >() : split[ 1 ].Split( '|', 2, StringSplitOptions.TrimEntries | StringSplitOptions.RemoveEmptyEntries ); - if( nameSplit.Length != 2 ) + var split = arguments.Split(' ', 2, StringSplitOptions.TrimEntries | StringSplitOptions.RemoveEmptyEntries); + var nameSplit = split.Length != 2 + ? Array.Empty() + : split[1].Split('|', 2, StringSplitOptions.TrimEntries | StringSplitOptions.RemoveEmptyEntries); + if (nameSplit.Length != 2) { - DalamudServices.Chat.Print( "Not enough arguments provided." ); + _chat.Print("Not enough arguments provided."); return false; } - var state = ConvertToSettingState( split[ 0 ] ); + var state = ConvertToSettingState(split[0]); - if( state == -1 ) + if (state == -1) { - DalamudServices.Chat.Print( new SeStringBuilder().AddRed( split[ 0 ], true ).AddText( " is not a valid type of setting." ).BuiltString ); + _chat.Print(new SeStringBuilder().AddRed(split[0], true).AddText(" is not a valid type of setting.").BuiltString); return false; } - if( !GetModCollection( nameSplit[ 0 ], out var collection ) || collection == ModCollection.Empty ) - { + if (!GetModCollection(nameSplit[0], out var collection) || collection == ModCollection.Empty) return false; - } - var mods = _modManager.Where( m => m.LocalTags.Contains( nameSplit[ 1 ], StringComparer.OrdinalIgnoreCase ) ).ToList(); + var mods = _modManager.Where(m => m.LocalTags.Contains(nameSplit[1], StringComparer.OrdinalIgnoreCase)).ToList(); - if( mods.Count == 0 ) + if (mods.Count == 0) { - DalamudServices.Chat.Print( new SeStringBuilder().AddText( "The tag " ).AddRed( nameSplit[ 1 ], true ).AddText( " does not match any mods." ).BuiltString ); + _chat.Print(new SeStringBuilder().AddText("The tag ").AddRed(nameSplit[1], true).AddText(" does not match any mods.") + .BuiltString); return false; } var changes = false; - foreach( var mod in mods ) - { - changes |= HandleModState( state, collection!, mod ); - } + foreach (var mod in mods) + changes |= HandleModState(state, collection!, mod); - if( !changes ) - { - Print( () => new SeStringBuilder().AddText( "No mod states were changed in collection " ).AddYellow( collection!.Name, true ).AddText( "." ).BuiltString ); - } + if (!changes) + Print(() => new SeStringBuilder().AddText("No mod states were changed in collection ").AddYellow(collection!.Name, true) + .AddText(".").BuiltString); return true; } - private bool GetModCollection( string collectionName, out ModCollection? collection ) + private bool GetModCollection(string collectionName, out ModCollection? collection) { var lowerName = collectionName.ToLowerInvariant(); - if( lowerName == "delete" ) + if (lowerName == "delete") { collection = null; return true; } - collection = string.Equals( lowerName, ModCollection.Empty.Name, StringComparison.OrdinalIgnoreCase ) + collection = string.Equals(lowerName, ModCollection.Empty.Name, StringComparison.OrdinalIgnoreCase) ? ModCollection.Empty - : _collectionManager[ lowerName ]; - if( collection == null ) + : _collectionManager[lowerName]; + if (collection == null) { - DalamudServices.Chat.Print( new SeStringBuilder().AddText( "The collection " ).AddRed( collectionName, true ).AddText( " does not exist." ).BuiltString ); + _chat.Print(new SeStringBuilder().AddText("The collection ").AddRed(collectionName, true).AddText(" does not exist.") + .BuiltString); return false; } return true; } - private static bool? ParseTrueFalseToggle( string value ) + private static bool? ParseTrueFalseToggle(string value) => value.ToLowerInvariant() switch { "0" => false, @@ -508,7 +480,7 @@ public class CommandHandler : IDisposable _ => null, }; - private static int ConvertToSettingState( string text ) + private static int ConvertToSettingState(string text) => text.ToLowerInvariant() switch { "enable" => 0, @@ -521,47 +493,49 @@ public class CommandHandler : IDisposable _ => -1, }; - private static bool HandleModState( int settingState, ModCollection collection, Mod mod ) + private bool HandleModState(int settingState, ModCollection collection, Mod mod) { - var settings = collection!.Settings[ mod.Index ]; - switch( settingState ) + var settings = collection!.Settings[mod.Index]; + switch (settingState) { case 0: - if( collection.SetModState( mod.Index, true ) ) + if (collection.SetModState(mod.Index, true)) { - Print( () => new SeStringBuilder().AddText( "Enabled mod " ).AddPurple( mod.Name, true ).AddText( " in collection " ) - .AddYellow( collection.Name, true ) - .AddText( "." ).BuiltString ); + Print(() => new SeStringBuilder().AddText("Enabled mod ").AddPurple(mod.Name, true).AddText(" in collection ") + .AddYellow(collection.Name, true) + .AddText(".").BuiltString); return true; } return false; case 1: - if( collection.SetModState( mod.Index, false ) ) + if (collection.SetModState(mod.Index, false)) { - Print( () => new SeStringBuilder().AddText( "Disabled mod " ).AddPurple( mod.Name, true ).AddText( " in collection " ) - .AddYellow( collection.Name, true ) - .AddText( "." ).BuiltString ); + Print(() => new SeStringBuilder().AddText("Disabled mod ").AddPurple(mod.Name, true).AddText(" in collection ") + .AddYellow(collection.Name, true) + .AddText(".").BuiltString); return true; } return false; case 2: - var setting = !( settings?.Enabled ?? false ); - if( collection.SetModState( mod.Index, setting ) ) + var setting = !(settings?.Enabled ?? false); + if (collection.SetModState(mod.Index, setting)) { - Print( () => new SeStringBuilder().AddText( setting ? "Enabled mod " : "Disabled mod " ).AddPurple( mod.Name, true ).AddText( " in collection " ) - .AddYellow( collection.Name, true ) - .AddText( "." ).BuiltString ); + Print(() => new SeStringBuilder().AddText(setting ? "Enabled mod " : "Disabled mod ").AddPurple(mod.Name, true) + .AddText(" in collection ") + .AddYellow(collection.Name, true) + .AddText(".").BuiltString); return true; } return false; case 3: - if( collection.SetModInheritance( mod.Index, true ) ) + if (collection.SetModInheritance(mod.Index, true)) { - Print( () => new SeStringBuilder().AddText( "Set mod " ).AddPurple( mod.Name, true ).AddText( " in collection " ).AddYellow( collection.Name, true ) - .AddText( " to inherit." ).BuiltString ); + Print(() => new SeStringBuilder().AddText("Set mod ").AddPurple(mod.Name, true).AddText(" in collection ") + .AddYellow(collection.Name, true) + .AddText(" to inherit.").BuiltString); return true; } @@ -571,27 +545,21 @@ public class CommandHandler : IDisposable return false; } - private static void Print( string text ) + private void Print(string text) { - if( Penumbra.Config.PrintSuccessfulCommandsToChat ) - { - DalamudServices.Chat.Print( text ); - } + if (Penumbra.Config.PrintSuccessfulCommandsToChat) + _chat.Print(text); } - private static void Print( DefaultInterpolatedStringHandler text ) + private void Print(DefaultInterpolatedStringHandler text) { - if( Penumbra.Config.PrintSuccessfulCommandsToChat ) - { - DalamudServices.Chat.Print( text.ToStringAndClear() ); - } + if (Penumbra.Config.PrintSuccessfulCommandsToChat) + _chat.Print(text.ToStringAndClear()); } - private static void Print( Func text ) + private void Print(Func text) { - if( Penumbra.Config.PrintSuccessfulCommandsToChat ) - { - DalamudServices.Chat.Print( text() ); - } + if (Penumbra.Config.PrintSuccessfulCommandsToChat) + _chat.Print(text()); } -} \ No newline at end of file +} diff --git a/Penumbra/Configuration.cs b/Penumbra/Configuration.cs index 55eee054..9499ae30 100644 --- a/Penumbra/Configuration.cs +++ b/Penumbra/Configuration.cs @@ -29,7 +29,7 @@ public class Configuration : IPluginConfiguration public int Version { get; set; } = Constants.CurrentVersion; - public int LastSeenVersion { get; set; } = ConfigWindow.LastChangelogVersion; + public int LastSeenVersion { get; set; } = PenumbraChangelog.LastChangelogVersion; public ChangeLogDisplayType ChangeLogDisplayType { get; set; } = ChangeLogDisplayType.New; public bool EnableMods { get; set; } = true; diff --git a/Penumbra/Interop/Loader/CharacterResolver.cs b/Penumbra/Interop/Loader/CharacterResolver.cs index 0231a1e8..68b83b41 100644 --- a/Penumbra/Interop/Loader/CharacterResolver.cs +++ b/Penumbra/Interop/Loader/CharacterResolver.cs @@ -70,12 +70,10 @@ public class CharacterResolver : IDisposable }; } - // TODO public unsafe void Dispose() { _loader.ResetResolvePath(); _loader.FileLoaded -= ImcLoadResource; - _pathResolver.Dispose(); } // Use the default method of path replacement. diff --git a/Penumbra/Interop/ObjectReloader.cs b/Penumbra/Interop/ObjectReloader.cs deleted file mode 100644 index ba24f155..00000000 --- a/Penumbra/Interop/ObjectReloader.cs +++ /dev/null @@ -1,370 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using Dalamud.Game.ClientState.Conditions; -using Dalamud.Game.ClientState.Objects.Enums; -using Dalamud.Game.ClientState.Objects.SubKinds; -using Dalamud.Game.ClientState.Objects.Types; -using Penumbra.Api; -using Penumbra.Api.Enums; -using Penumbra.GameData; -using Penumbra.GameData.Actors; -using Penumbra.Interop.Structs; -using Penumbra.Services; - -namespace Penumbra.Interop; - -public unsafe partial class ObjectReloader -{ - public const int GPosePlayerIdx = 201; - public const int GPoseSlots = 42; - public const int GPoseEndIdx = GPosePlayerIdx + GPoseSlots; - - private readonly string?[] _gPoseNames = new string?[GPoseSlots]; - private int _gPoseNameCounter = 0; - private bool _inGPose = false; - - // VFuncs that disable and enable draw, used only for GPose actors. - private static void DisableDraw( GameObject actor ) - => ( ( delegate* unmanaged< IntPtr, void >** )actor.Address )[ 0 ][ Offsets.DisableDrawVfunc ]( actor.Address ); - - private static void EnableDraw( GameObject actor ) - => ( ( delegate* unmanaged< IntPtr, void >** )actor.Address )[ 0 ][ Offsets.EnableDrawVfunc ]( actor.Address ); - - // Check whether we currently are in GPose. - // Also clear the name list. - private void SetGPose() - { - _inGPose = DalamudServices.Objects[ GPosePlayerIdx ] != null; - _gPoseNameCounter = 0; - } - - private static bool IsGPoseActor( int idx ) - => idx is >= GPosePlayerIdx and < GPoseEndIdx; - - // Return whether an object has to be replaced by a GPose object. - // If the object does not exist, is already a GPose actor - // or no actor of the same name is found in the GPose actor list, - // obj will be the object itself (or null) and false will be returned. - // If we are in GPose and a game object with the same name as the original actor is found, - // this will be in obj and true will be returned. - private bool FindCorrectActor( int idx, out GameObject? obj ) - { - obj = DalamudServices.Objects[ idx ]; - if( !_inGPose || obj == null || IsGPoseActor( idx ) ) - { - return false; - } - - var name = obj.Name.ToString(); - for( var i = 0; i < _gPoseNameCounter; ++i ) - { - var gPoseName = _gPoseNames[ i ]; - if( gPoseName == null ) - { - break; - } - - if( name == gPoseName ) - { - obj = DalamudServices.Objects[ GPosePlayerIdx + i ]; - return true; - } - } - - for( ; _gPoseNameCounter < GPoseSlots; ++_gPoseNameCounter ) - { - var gPoseName = DalamudServices.Objects[ GPosePlayerIdx + _gPoseNameCounter ]?.Name.ToString(); - _gPoseNames[ _gPoseNameCounter ] = gPoseName; - if( gPoseName == null ) - { - break; - } - - if( name == gPoseName ) - { - obj = DalamudServices.Objects[ GPosePlayerIdx + _gPoseNameCounter ]; - return true; - } - } - - return obj; - } - - // Do not ever redraw any of the five UI Window actors. - private static bool BadRedrawIndices( GameObject? actor, out int tableIndex ) - { - if( actor == null ) - { - tableIndex = -1; - return true; - } - - tableIndex = ObjectTableIndex( actor ); - return tableIndex is >= (int) ScreenActor.CharacterScreen and <= ( int) ScreenActor.Card8; - } -} - -public sealed unsafe partial class ObjectReloader : IDisposable -{ - private readonly List< int > _queue = new(100); - private readonly List< int > _afterGPoseQueue = new(GPoseSlots); - private int _target = -1; - - public event GameObjectRedrawnDelegate? GameObjectRedrawn; - - public ObjectReloader() - => DalamudServices.Framework.Update += OnUpdateEvent; - - public void Dispose() - => DalamudServices.Framework.Update -= OnUpdateEvent; - - public static DrawState* ActorDrawState( GameObject actor ) - => ( DrawState* )( &( ( FFXIVClientStructs.FFXIV.Client.Game.Object.GameObject* )actor.Address )->RenderFlags ); - - private static int ObjectTableIndex( GameObject actor ) - => ( ( FFXIVClientStructs.FFXIV.Client.Game.Object.GameObject* )actor.Address )->ObjectIndex; - - private static void WriteInvisible( GameObject? actor ) - { - if( BadRedrawIndices( actor, out var tableIndex ) ) - { - return; - } - - *ActorDrawState( actor! ) |= DrawState.Invisibility; - - var gPose = IsGPoseActor( tableIndex ); - if( gPose ) - { - DisableDraw( actor! ); - } - - if( actor is PlayerCharacter && DalamudServices.Objects[ tableIndex + 1 ] is { ObjectKind: ObjectKind.MountType } mount ) - { - *ActorDrawState( mount ) |= DrawState.Invisibility; - if( gPose ) - { - DisableDraw( mount ); - } - } - } - - private void WriteVisible( GameObject? actor ) - { - if( BadRedrawIndices( actor, out var tableIndex ) ) - { - return; - } - - *ActorDrawState( actor! ) &= ~DrawState.Invisibility; - - var gPose = IsGPoseActor( tableIndex ); - if( gPose ) - { - EnableDraw( actor! ); - } - - if( actor is PlayerCharacter && DalamudServices.Objects[ tableIndex + 1 ] is { ObjectKind: ObjectKind.MountType } mount ) - { - *ActorDrawState( mount ) &= ~DrawState.Invisibility; - if( gPose ) - { - EnableDraw( mount ); - } - } - - GameObjectRedrawn?.Invoke( actor!.Address, tableIndex ); - } - - private void ReloadActor( GameObject? actor ) - { - if( BadRedrawIndices( actor, out var tableIndex ) ) - { - return; - } - - if( actor!.Address == DalamudServices.Targets.Target?.Address ) - { - _target = tableIndex; - } - - _queue.Add( ~tableIndex ); - } - - private void ReloadActorAfterGPose( GameObject? actor ) - { - if( DalamudServices.Objects[ GPosePlayerIdx ] != null ) - { - ReloadActor( actor ); - return; - } - - if( actor != null ) - { - WriteInvisible( actor ); - _afterGPoseQueue.Add( ~ObjectTableIndex( actor ) ); - } - } - - private void HandleTarget() - { - if( _target < 0 ) - { - return; - } - - var actor = DalamudServices.Objects[ _target ]; - if( actor == null || DalamudServices.Targets.Target != null ) - { - return; - } - - DalamudServices.Targets.SetTarget( actor ); - _target = -1; - } - - private void HandleRedraw() - { - if( _queue.Count == 0 ) - { - return; - } - - var numKept = 0; - for( var i = 0; i < _queue.Count; ++i ) - { - var idx = _queue[ i ]; - if( FindCorrectActor( idx < 0 ? ~idx : idx, out var obj ) ) - { - _afterGPoseQueue.Add( idx < 0 ? idx : ~idx ); - } - - if( obj != null ) - { - if( idx < 0 ) - { - WriteInvisible( obj ); - _queue[ numKept++ ] = ObjectTableIndex( obj ); - } - else - { - WriteVisible( obj ); - } - } - } - - _queue.RemoveRange( numKept, _queue.Count - numKept ); - } - - private void HandleAfterGPose() - { - if( _afterGPoseQueue.Count == 0 || _inGPose ) - { - return; - } - - var numKept = 0; - for( var i = 0; i < _afterGPoseQueue.Count; ++i ) - { - var idx = _afterGPoseQueue[ i ]; - if( idx < 0 ) - { - var newIdx = ~idx; - WriteInvisible( DalamudServices.Objects[ newIdx ] ); - _afterGPoseQueue[ numKept++ ] = newIdx; - } - else - { - WriteVisible( DalamudServices.Objects[ idx ] ); - } - } - - _afterGPoseQueue.RemoveRange( numKept, _afterGPoseQueue.Count - numKept ); - } - - private void OnUpdateEvent( object framework ) - { - if( DalamudServices.Conditions[ ConditionFlag.BetweenAreas51 ] - || DalamudServices.Conditions[ ConditionFlag.BetweenAreas ] - || DalamudServices.Conditions[ ConditionFlag.OccupiedInCutSceneEvent ] ) - { - return; - } - - SetGPose(); - HandleRedraw(); - HandleAfterGPose(); - HandleTarget(); - } - - public void RedrawObject( GameObject? actor, RedrawType settings ) - { - switch( settings ) - { - case RedrawType.Redraw: - ReloadActor( actor ); - break; - case RedrawType.AfterGPose: - ReloadActorAfterGPose( actor ); - break; - default: throw new ArgumentOutOfRangeException( nameof( settings ), settings, null ); - } - } - - private static GameObject? GetLocalPlayer() - { - var gPosePlayer = DalamudServices.Objects[ GPosePlayerIdx ]; - return gPosePlayer ?? DalamudServices.Objects[ 0 ]; - } - - public static bool GetName( string lowerName, out GameObject? actor ) - { - ( actor, var ret ) = lowerName switch - { - "" => ( null, true ), - "" => ( GetLocalPlayer(), true ), - "self" => ( GetLocalPlayer(), true ), - "" => ( DalamudServices.Targets.Target, true ), - "target" => ( DalamudServices.Targets.Target, true ), - "" => ( DalamudServices.Targets.FocusTarget, true ), - "focus" => ( DalamudServices.Targets.FocusTarget, true ), - "" => ( DalamudServices.Targets.MouseOverTarget, true ), - "mouseover" => ( DalamudServices.Targets.MouseOverTarget, true ), - _ => ( null, false ), - }; - return ret; - } - - public void RedrawObject( int tableIndex, RedrawType settings ) - { - if( tableIndex >= 0 && tableIndex < DalamudServices.Objects.Length ) - { - RedrawObject( DalamudServices.Objects[ tableIndex ], settings ); - } - } - - public void RedrawObject( string name, RedrawType settings ) - { - var lowerName = name.ToLowerInvariant(); - if( GetName( lowerName, out var target ) ) - { - RedrawObject( target, settings ); - } - else - { - foreach( var actor in DalamudServices.Objects.Where( a => a.Name.ToString().ToLowerInvariant() == lowerName ) ) - { - RedrawObject( actor, settings ); - } - } - } - - public void RedrawAll( RedrawType settings ) - { - foreach( var actor in DalamudServices.Objects ) - { - RedrawObject( actor, settings ); - } - } -} \ No newline at end of file diff --git a/Penumbra/Interop/RedrawService.cs b/Penumbra/Interop/RedrawService.cs new file mode 100644 index 00000000..f738320c --- /dev/null +++ b/Penumbra/Interop/RedrawService.cs @@ -0,0 +1,341 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using Dalamud.Game; +using Dalamud.Game.ClientState.Conditions; +using Dalamud.Game.ClientState.Objects; +using Dalamud.Game.ClientState.Objects.Enums; +using Dalamud.Game.ClientState.Objects.SubKinds; +using Dalamud.Game.ClientState.Objects.Types; +using Penumbra.Api; +using Penumbra.Api.Enums; +using Penumbra.GameData; +using Penumbra.GameData.Actors; +using Penumbra.Interop.Structs; +using Penumbra.Services; + +namespace Penumbra.Interop; + +public unsafe partial class RedrawService +{ + public const int GPosePlayerIdx = 201; + public const int GPoseSlots = 42; + public const int GPoseEndIdx = GPosePlayerIdx + GPoseSlots; + + private readonly string?[] _gPoseNames = new string?[GPoseSlots]; + private int _gPoseNameCounter = 0; + private bool _inGPose = false; + + // VFuncs that disable and enable draw, used only for GPose actors. + private static void DisableDraw(GameObject actor) + => ((delegate* unmanaged< IntPtr, void >**)actor.Address)[0][Offsets.DisableDrawVfunc](actor.Address); + + private static void EnableDraw(GameObject actor) + => ((delegate* unmanaged< IntPtr, void >**)actor.Address)[0][Offsets.EnableDrawVfunc](actor.Address); + + // Check whether we currently are in GPose. + // Also clear the name list. + private void SetGPose() + { + _inGPose = _objects[GPosePlayerIdx] != null; + _gPoseNameCounter = 0; + } + + private static bool IsGPoseActor(int idx) + => idx is >= GPosePlayerIdx and < GPoseEndIdx; + + // Return whether an object has to be replaced by a GPose object. + // If the object does not exist, is already a GPose actor + // or no actor of the same name is found in the GPose actor list, + // obj will be the object itself (or null) and false will be returned. + // If we are in GPose and a game object with the same name as the original actor is found, + // this will be in obj and true will be returned. + private bool FindCorrectActor(int idx, out GameObject? obj) + { + obj = _objects[idx]; + if (!_inGPose || obj == null || IsGPoseActor(idx)) + return false; + + var name = obj.Name.ToString(); + for (var i = 0; i < _gPoseNameCounter; ++i) + { + var gPoseName = _gPoseNames[i]; + if (gPoseName == null) + break; + + if (name == gPoseName) + { + obj = _objects[GPosePlayerIdx + i]; + return true; + } + } + + for (; _gPoseNameCounter < GPoseSlots; ++_gPoseNameCounter) + { + var gPoseName = _objects[GPosePlayerIdx + _gPoseNameCounter]?.Name.ToString(); + _gPoseNames[_gPoseNameCounter] = gPoseName; + if (gPoseName == null) + break; + + if (name == gPoseName) + { + obj = _objects[GPosePlayerIdx + _gPoseNameCounter]; + return true; + } + } + + return obj; + } + + // Do not ever redraw any of the five UI Window actors. + private static bool BadRedrawIndices(GameObject? actor, out int tableIndex) + { + if (actor == null) + { + tableIndex = -1; + return true; + } + + tableIndex = ObjectTableIndex(actor); + return tableIndex is >= (int)ScreenActor.CharacterScreen and <= (int)ScreenActor.Card8; + } +} + +public sealed unsafe partial class RedrawService : IDisposable +{ + private readonly Framework _framework; + private readonly ObjectTable _objects; + private readonly TargetManager _targets; + private readonly Condition _conditions; + + private readonly List _queue = new(100); + private readonly List _afterGPoseQueue = new(GPoseSlots); + private int _target = -1; + + public event GameObjectRedrawnDelegate? GameObjectRedrawn; + + public RedrawService(Framework framework, ObjectTable objects, TargetManager targets, Condition conditions) + { + _framework = framework; + _objects = objects; + _targets = targets; + _conditions = conditions; + _framework.Update += OnUpdateEvent; + } + + public void Dispose() + { + _framework.Update -= OnUpdateEvent; + } + + public static DrawState* ActorDrawState(GameObject actor) + => (DrawState*)(&((FFXIVClientStructs.FFXIV.Client.Game.Object.GameObject*)actor.Address)->RenderFlags); + + private static int ObjectTableIndex(GameObject actor) + => ((FFXIVClientStructs.FFXIV.Client.Game.Object.GameObject*)actor.Address)->ObjectIndex; + + private void WriteInvisible(GameObject? actor) + { + if (BadRedrawIndices(actor, out var tableIndex)) + return; + + *ActorDrawState(actor!) |= DrawState.Invisibility; + + var gPose = IsGPoseActor(tableIndex); + if (gPose) + DisableDraw(actor!); + + if (actor is PlayerCharacter && _objects[tableIndex + 1] is { ObjectKind: ObjectKind.MountType } mount) + { + *ActorDrawState(mount) |= DrawState.Invisibility; + if (gPose) + DisableDraw(mount); + } + } + + private void WriteVisible(GameObject? actor) + { + if (BadRedrawIndices(actor, out var tableIndex)) + return; + + *ActorDrawState(actor!) &= ~DrawState.Invisibility; + + var gPose = IsGPoseActor(tableIndex); + if (gPose) + EnableDraw(actor!); + + if (actor is PlayerCharacter && _objects[tableIndex + 1] is { ObjectKind: ObjectKind.MountType } mount) + { + *ActorDrawState(mount) &= ~DrawState.Invisibility; + if (gPose) + EnableDraw(mount); + } + + GameObjectRedrawn?.Invoke(actor!.Address, tableIndex); + } + + private void ReloadActor(GameObject? actor) + { + if (BadRedrawIndices(actor, out var tableIndex)) + return; + + if (actor!.Address == _targets.Target?.Address) + _target = tableIndex; + + _queue.Add(~tableIndex); + } + + private void ReloadActorAfterGPose(GameObject? actor) + { + if (_objects[GPosePlayerIdx] != null) + { + ReloadActor(actor); + return; + } + + if (actor != null) + { + WriteInvisible(actor); + _afterGPoseQueue.Add(~ObjectTableIndex(actor)); + } + } + + private void HandleTarget() + { + if (_target < 0) + return; + + var actor = _objects[_target]; + if (actor == null || _targets.Target != null) + return; + + _targets.SetTarget(actor); + _target = -1; + } + + private void HandleRedraw() + { + if (_queue.Count == 0) + return; + + var numKept = 0; + for (var i = 0; i < _queue.Count; ++i) + { + var idx = _queue[i]; + if (FindCorrectActor(idx < 0 ? ~idx : idx, out var obj)) + _afterGPoseQueue.Add(idx < 0 ? idx : ~idx); + + if (obj != null) + { + if (idx < 0) + { + WriteInvisible(obj); + _queue[numKept++] = ObjectTableIndex(obj); + } + else + { + WriteVisible(obj); + } + } + } + + _queue.RemoveRange(numKept, _queue.Count - numKept); + } + + private void HandleAfterGPose() + { + if (_afterGPoseQueue.Count == 0 || _inGPose) + return; + + var numKept = 0; + for (var i = 0; i < _afterGPoseQueue.Count; ++i) + { + var idx = _afterGPoseQueue[i]; + if (idx < 0) + { + var newIdx = ~idx; + WriteInvisible(_objects[newIdx]); + _afterGPoseQueue[numKept++] = newIdx; + } + else + { + WriteVisible(_objects[idx]); + } + } + + _afterGPoseQueue.RemoveRange(numKept, _afterGPoseQueue.Count - numKept); + } + + private void OnUpdateEvent(object framework) + { + if (_conditions[ConditionFlag.BetweenAreas51] + || _conditions[ConditionFlag.BetweenAreas] + || _conditions[ConditionFlag.OccupiedInCutSceneEvent]) + return; + + SetGPose(); + HandleRedraw(); + HandleAfterGPose(); + HandleTarget(); + } + + public void RedrawObject(GameObject? actor, RedrawType settings) + { + switch (settings) + { + case RedrawType.Redraw: + ReloadActor(actor); + break; + case RedrawType.AfterGPose: + ReloadActorAfterGPose(actor); + break; + default: throw new ArgumentOutOfRangeException(nameof(settings), settings, null); + } + } + + private static GameObject? GetLocalPlayer() + { + var gPosePlayer = DalamudServices.Objects[GPosePlayerIdx]; + return gPosePlayer ?? DalamudServices.Objects[0]; + } + + public bool GetName(string lowerName, out GameObject? actor) + { + (actor, var ret) = lowerName switch + { + "" => (null, true), + "" => (GetLocalPlayer(), true), + "self" => (GetLocalPlayer(), true), + "" => (_targets.Target, true), + "target" => (_targets.Target, true), + "" => (_targets.FocusTarget, true), + "focus" => (_targets.FocusTarget, true), + "" => (_targets.MouseOverTarget, true), + "mouseover" => (_targets.MouseOverTarget, true), + _ => (null, false), + }; + return ret; + } + + public void RedrawObject(int tableIndex, RedrawType settings) + { + if (tableIndex >= 0 && tableIndex < _objects.Length) + RedrawObject(_objects[tableIndex], settings); + } + + public void RedrawObject(string name, RedrawType settings) + { + var lowerName = name.ToLowerInvariant(); + if (GetName(lowerName, out var target)) + RedrawObject(target, settings); + else + foreach (var actor in _objects.Where(a => a.Name.ToString().ToLowerInvariant() == lowerName)) + RedrawObject(actor, settings); + } + + public void RedrawAll(RedrawType settings) + { + foreach (var actor in _objects) + RedrawObject(actor, settings); + } +} diff --git a/Penumbra/Interop/Resolver/PathResolver.cs b/Penumbra/Interop/Resolver/PathResolver.cs index 4050d2f1..edcb76d5 100644 --- a/Penumbra/Interop/Resolver/PathResolver.cs +++ b/Penumbra/Interop/Resolver/PathResolver.cs @@ -53,6 +53,7 @@ public partial class PathResolver : IDisposable _paths = new PathState(this); _meta = new MetaState(_paths.HumanVTable); _subFiles = new SubfileHelper(_loader, Penumbra.GameEvents); + Enable(); } // The modified resolver that handles game path resolving. diff --git a/Penumbra/Mods/Editor/Mod.Normalization.cs b/Penumbra/Mods/Editor/Mod.Normalization.cs index 0696271d..0b487a8f 100644 --- a/Penumbra/Mods/Editor/Mod.Normalization.cs +++ b/Penumbra/Mods/Editor/Mod.Normalization.cs @@ -62,7 +62,7 @@ public partial class Mod } catch( Exception e ) { - ChatUtil.NotificationMessage( $"Could not normalize mod:\n{e}", "Failure", NotificationType.Error ); + Penumbra.ChatService.NotificationMessage( $"Could not normalize mod:\n{e}", "Failure", NotificationType.Error ); } finally { @@ -75,7 +75,7 @@ public partial class Mod { if( Directory.Exists( _normalizationDirName ) ) { - ChatUtil.NotificationMessage( "Could not normalize mod:\n" + Penumbra.ChatService.NotificationMessage( "Could not normalize mod:\n" + "The directory TmpNormalization may not already exist when normalizing a mod.", "Failure", NotificationType.Error ); return false; @@ -83,7 +83,7 @@ public partial class Mod if( Directory.Exists( _oldDirName ) ) { - ChatUtil.NotificationMessage( "Could not normalize mod:\n" + Penumbra.ChatService.NotificationMessage( "Could not normalize mod:\n" + "The directory TmpNormalizationOld may not already exist when normalizing a mod.", "Failure", NotificationType.Error ); return false; @@ -173,7 +173,7 @@ public partial class Mod } catch( Exception e ) { - ChatUtil.NotificationMessage( $"Could not normalize mod:\n{e}", "Failure", NotificationType.Error ); + Penumbra.ChatService.NotificationMessage( $"Could not normalize mod:\n{e}", "Failure", NotificationType.Error ); _redirections = null; } @@ -201,7 +201,7 @@ public partial class Mod } catch( Exception e ) { - ChatUtil.NotificationMessage( $"Could not move old files out of the way while normalizing mod mod:\n{e}", "Failure", NotificationType.Error ); + Penumbra.ChatService.NotificationMessage( $"Could not move old files out of the way while normalizing mod mod:\n{e}", "Failure", NotificationType.Error ); } return false; @@ -223,7 +223,7 @@ public partial class Mod } catch( Exception e ) { - ChatUtil.NotificationMessage( $"Could not move new files into the mod while normalizing mod mod:\n{e}", "Failure", NotificationType.Error ); + Penumbra.ChatService.NotificationMessage( $"Could not move new files into the mod while normalizing mod mod:\n{e}", "Failure", NotificationType.Error ); foreach( var dir in _mod.ModPath.EnumerateDirectories() ) { if( dir.FullName.Equals( _oldDirName, StringComparison.OrdinalIgnoreCase ) diff --git a/Penumbra/Mods/Manager/Mod.Manager.BasePath.cs b/Penumbra/Mods/Manager/Mod.Manager.BasePath.cs index 24d3a0e6..0c9c69cc 100644 --- a/Penumbra/Mods/Manager/Mod.Manager.BasePath.cs +++ b/Penumbra/Mods/Manager/Mod.Manager.BasePath.cs @@ -163,7 +163,7 @@ public partial class Mod } // Return the state of the new potential name of a directory. - public static NewDirectoryState NewDirectoryValid( string oldName, string newName, out DirectoryInfo? directory ) + public NewDirectoryState NewDirectoryValid( string oldName, string newName, out DirectoryInfo? directory ) { directory = null; if( newName.Length == 0 ) @@ -182,7 +182,7 @@ public partial class Mod return NewDirectoryState.ContainsInvalidSymbols; } - directory = new DirectoryInfo( Path.Combine( Penumbra.ModManager.BasePath.FullName, fixedNewName ) ); + directory = new DirectoryInfo( Path.Combine( BasePath.FullName, fixedNewName ) ); if( File.Exists( directory.FullName ) ) { return NewDirectoryState.ExistsAsFile; diff --git a/Penumbra/Mods/Manager/Mod.Manager.Local.cs b/Penumbra/Mods/Manager/Mod.Manager.Local.cs index cecf73b4..9b2fc839 100644 --- a/Penumbra/Mods/Manager/Mod.Manager.Local.cs +++ b/Penumbra/Mods/Manager/Mod.Manager.Local.cs @@ -1,7 +1,5 @@ using System; -using System.Collections.Generic; using System.Linq; -using ImGuiScene; namespace Penumbra.Mods; diff --git a/Penumbra/Mods/Manager/Mod.Manager.Options.cs b/Penumbra/Mods/Manager/Mod.Manager.Options.cs index 4e482612..fbc30e64 100644 --- a/Penumbra/Mods/Manager/Mod.Manager.Options.cs +++ b/Penumbra/Mods/Manager/Mod.Manager.Options.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.Linq; +using Dalamud.Interface.Internal.Notifications; using OtterGui; using OtterGui.Filesystem; using Penumbra.Api.Enums; @@ -14,43 +15,37 @@ public sealed partial class Mod { public sealed partial class Manager { - public delegate void ModOptionChangeDelegate( ModOptionChangeType type, Mod mod, int groupIdx, int optionIdx, int movedToIdx ); + public delegate void ModOptionChangeDelegate(ModOptionChangeType type, Mod mod, int groupIdx, int optionIdx, int movedToIdx); public event ModOptionChangeDelegate ModOptionChanged; - public void ChangeModGroupType( Mod mod, int groupIdx, GroupType type ) - { - var group = mod._groups[ groupIdx ]; - if( group.Type == type ) - { - return; - } - - mod._groups[ groupIdx ] = group.Convert( type ); - ModOptionChanged.Invoke( ModOptionChangeType.GroupTypeChanged, mod, groupIdx, -1, -1 ); - } - - public void ChangeModGroupDefaultOption( Mod mod, int groupIdx, uint defaultOption ) + public void ChangeModGroupType(Mod mod, int groupIdx, GroupType type) { var group = mod._groups[groupIdx]; - if( group.DefaultSettings == defaultOption ) - { + if (group.Type == type) return; - } - group.DefaultSettings = defaultOption; - ModOptionChanged.Invoke( ModOptionChangeType.DefaultOptionChanged, mod, groupIdx, -1, -1 ); + mod._groups[groupIdx] = group.Convert(type); + ModOptionChanged.Invoke(ModOptionChangeType.GroupTypeChanged, mod, groupIdx, -1, -1); } - public void RenameModGroup( Mod mod, int groupIdx, string newName ) + public void ChangeModGroupDefaultOption(Mod mod, int groupIdx, uint defaultOption) { - var group = mod._groups[ groupIdx ]; - var oldName = group.Name; - if( oldName == newName || !VerifyFileName( mod, group, newName, true ) ) - { + var group = mod._groups[groupIdx]; + if (group.DefaultSettings == defaultOption) return; - } - group.DeleteFile( mod.ModPath, groupIdx ); + group.DefaultSettings = defaultOption; + ModOptionChanged.Invoke(ModOptionChangeType.DefaultOptionChanged, mod, groupIdx, -1, -1); + } + + public void RenameModGroup(Mod mod, int groupIdx, string newName) + { + var group = mod._groups[groupIdx]; + var oldName = group.Name; + if (oldName == newName || !VerifyFileName(mod, group, newName, true)) + return; + + group.DeleteFile(mod.ModPath, groupIdx); var _ = group switch { @@ -59,61 +54,63 @@ public sealed partial class Mod _ => newName, }; - ModOptionChanged.Invoke( ModOptionChangeType.GroupRenamed, mod, groupIdx, -1, -1 ); + ModOptionChanged.Invoke(ModOptionChangeType.GroupRenamed, mod, groupIdx, -1, -1); } - public void AddModGroup( Mod mod, GroupType type, string newName ) + public void AddModGroup(Mod mod, GroupType type, string newName) { - if( !VerifyFileName( mod, null, newName, true ) ) - { + if (!VerifyFileName(mod, null, newName, true)) return; - } - var maxPriority = mod._groups.Count == 0 ? 0 : mod._groups.Max( o => o.Priority ) + 1; + var maxPriority = mod._groups.Count == 0 ? 0 : mod._groups.Max(o => o.Priority) + 1; - mod._groups.Add( type == GroupType.Multi - ? new MultiModGroup { Name = newName, Priority = maxPriority } - : new SingleModGroup { Name = newName, Priority = maxPriority } ); - ModOptionChanged.Invoke( ModOptionChangeType.GroupAdded, mod, mod._groups.Count - 1, -1, -1 ); - } - - public void DeleteModGroup( Mod mod, int groupIdx ) - { - var group = mod._groups[ groupIdx ]; - ModOptionChanged.Invoke( ModOptionChangeType.PrepareChange, mod, groupIdx, -1, -1 ); - mod._groups.RemoveAt( groupIdx ); - UpdateSubModPositions( mod, groupIdx ); - group.DeleteFile( mod.ModPath, groupIdx ); - ModOptionChanged.Invoke( ModOptionChangeType.GroupDeleted, mod, groupIdx, -1, -1 ); - } - - public void MoveModGroup( Mod mod, int groupIdxFrom, int groupIdxTo ) - { - if( mod._groups.Move( groupIdxFrom, groupIdxTo ) ) - { - UpdateSubModPositions( mod, Math.Min( groupIdxFrom, groupIdxTo ) ); - ModOptionChanged.Invoke( ModOptionChangeType.GroupMoved, mod, groupIdxFrom, -1, groupIdxTo ); - } - } - - private static void UpdateSubModPositions( Mod mod, int fromGroup ) - { - foreach( var (group, groupIdx) in mod._groups.WithIndex().Skip( fromGroup ) ) - { - foreach( var (o, optionIdx) in group.OfType< SubMod >().WithIndex() ) + mod._groups.Add(type == GroupType.Multi + ? new MultiModGroup { - o.SetPosition( groupIdx, optionIdx ); + Name = newName, + Priority = maxPriority, } + : new SingleModGroup + { + Name = newName, + Priority = maxPriority, + }); + ModOptionChanged.Invoke(ModOptionChangeType.GroupAdded, mod, mod._groups.Count - 1, -1, -1); + } + + public void DeleteModGroup(Mod mod, int groupIdx) + { + var group = mod._groups[groupIdx]; + ModOptionChanged.Invoke(ModOptionChangeType.PrepareChange, mod, groupIdx, -1, -1); + mod._groups.RemoveAt(groupIdx); + UpdateSubModPositions(mod, groupIdx); + group.DeleteFile(mod.ModPath, groupIdx); + ModOptionChanged.Invoke(ModOptionChangeType.GroupDeleted, mod, groupIdx, -1, -1); + } + + public void MoveModGroup(Mod mod, int groupIdxFrom, int groupIdxTo) + { + if (mod._groups.Move(groupIdxFrom, groupIdxTo)) + { + UpdateSubModPositions(mod, Math.Min(groupIdxFrom, groupIdxTo)); + ModOptionChanged.Invoke(ModOptionChangeType.GroupMoved, mod, groupIdxFrom, -1, groupIdxTo); } } - public void ChangeGroupDescription( Mod mod, int groupIdx, string newDescription ) + private static void UpdateSubModPositions(Mod mod, int fromGroup) { - var group = mod._groups[ groupIdx ]; - if( group.Description == newDescription ) + foreach (var (group, groupIdx) in mod._groups.WithIndex().Skip(fromGroup)) { - return; + foreach (var (o, optionIdx) in group.OfType().WithIndex()) + o.SetPosition(groupIdx, optionIdx); } + } + + public void ChangeGroupDescription(Mod mod, int groupIdx, string newDescription) + { + var group = mod._groups[groupIdx]; + if (group.Description == newDescription) + return; var _ = group switch { @@ -121,29 +118,25 @@ public sealed partial class Mod MultiModGroup m => m.Description = newDescription, _ => newDescription, }; - ModOptionChanged.Invoke( ModOptionChangeType.DisplayChange, mod, groupIdx, -1, -1 ); + ModOptionChanged.Invoke(ModOptionChangeType.DisplayChange, mod, groupIdx, -1, -1); } - public void ChangeOptionDescription( Mod mod, int groupIdx, int optionIdx, string newDescription ) + public void ChangeOptionDescription(Mod mod, int groupIdx, int optionIdx, string newDescription) { - var group = mod._groups[ groupIdx ]; - var option = group[ optionIdx ]; - if( option.Description == newDescription || option is not SubMod s ) - { + var group = mod._groups[groupIdx]; + var option = group[optionIdx]; + if (option.Description == newDescription || option is not SubMod s) return; - } s.Description = newDescription; - ModOptionChanged.Invoke( ModOptionChangeType.DisplayChange, mod, groupIdx, optionIdx, -1 ); + ModOptionChanged.Invoke(ModOptionChangeType.DisplayChange, mod, groupIdx, optionIdx, -1); } - public void ChangeGroupPriority( Mod mod, int groupIdx, int newPriority ) + public void ChangeGroupPriority(Mod mod, int groupIdx, int newPriority) { - var group = mod._groups[ groupIdx ]; - if( group.Priority == newPriority ) - { + var group = mod._groups[groupIdx]; + if (group.Priority == newPriority) return; - } var _ = group switch { @@ -151,193 +144,174 @@ public sealed partial class Mod MultiModGroup m => m.Priority = newPriority, _ => newPriority, }; - ModOptionChanged.Invoke( ModOptionChangeType.PriorityChanged, mod, groupIdx, -1, -1 ); + ModOptionChanged.Invoke(ModOptionChangeType.PriorityChanged, mod, groupIdx, -1, -1); } - public void ChangeOptionPriority( Mod mod, int groupIdx, int optionIdx, int newPriority ) + public void ChangeOptionPriority(Mod mod, int groupIdx, int optionIdx, int newPriority) { - switch( mod._groups[ groupIdx ] ) + switch (mod._groups[groupIdx]) { case SingleModGroup: - ChangeGroupPriority( mod, groupIdx, newPriority ); + ChangeGroupPriority(mod, groupIdx, newPriority); break; case MultiModGroup m: - if( m.PrioritizedOptions[ optionIdx ].Priority == newPriority ) - { + if (m.PrioritizedOptions[optionIdx].Priority == newPriority) return; - } - m.PrioritizedOptions[ optionIdx ] = ( m.PrioritizedOptions[ optionIdx ].Mod, newPriority ); - ModOptionChanged.Invoke( ModOptionChangeType.PriorityChanged, mod, groupIdx, optionIdx, -1 ); + m.PrioritizedOptions[optionIdx] = (m.PrioritizedOptions[optionIdx].Mod, newPriority); + ModOptionChanged.Invoke(ModOptionChangeType.PriorityChanged, mod, groupIdx, optionIdx, -1); return; } } - public void RenameOption( Mod mod, int groupIdx, int optionIdx, string newName ) + public void RenameOption(Mod mod, int groupIdx, int optionIdx, string newName) { - switch( mod._groups[ groupIdx ] ) + switch (mod._groups[groupIdx]) { case SingleModGroup s: - if( s.OptionData[ optionIdx ].Name == newName ) - { + if (s.OptionData[optionIdx].Name == newName) return; - } - s.OptionData[ optionIdx ].Name = newName; + s.OptionData[optionIdx].Name = newName; break; case MultiModGroup m: - var option = m.PrioritizedOptions[ optionIdx ].Mod; - if( option.Name == newName ) - { + var option = m.PrioritizedOptions[optionIdx].Mod; + if (option.Name == newName) return; - } option.Name = newName; break; } - ModOptionChanged.Invoke( ModOptionChangeType.DisplayChange, mod, groupIdx, optionIdx, -1 ); + ModOptionChanged.Invoke(ModOptionChangeType.DisplayChange, mod, groupIdx, optionIdx, -1); } - public void AddOption( Mod mod, int groupIdx, string newName ) + public void AddOption(Mod mod, int groupIdx, string newName) { - var group = mod._groups[ groupIdx ]; - var subMod = new SubMod( mod ) { Name = newName }; - subMod.SetPosition( groupIdx, group.Count ); - switch( group ) + var group = mod._groups[groupIdx]; + var subMod = new SubMod(mod) { Name = newName }; + subMod.SetPosition(groupIdx, group.Count); + switch (group) { case SingleModGroup s: - s.OptionData.Add( subMod ); + s.OptionData.Add(subMod); break; case MultiModGroup m: - m.PrioritizedOptions.Add( ( subMod, 0 ) ); + m.PrioritizedOptions.Add((subMod, 0)); break; } - ModOptionChanged.Invoke( ModOptionChangeType.OptionAdded, mod, groupIdx, group.Count - 1, -1 ); + ModOptionChanged.Invoke(ModOptionChangeType.OptionAdded, mod, groupIdx, group.Count - 1, -1); } - public void AddOption( Mod mod, int groupIdx, ISubMod option, int priority = 0 ) + public void AddOption(Mod mod, int groupIdx, ISubMod option, int priority = 0) { - if( option is not SubMod o ) - { + if (option is not SubMod o) return; - } - var group = mod._groups[ groupIdx ]; - if( group.Count > 63 ) + var group = mod._groups[groupIdx]; + if (group.Count > 63) { Penumbra.Log.Error( $"Could not add option {option.Name} to {group.Name} for mod {mod.Name}, " - + "since only up to 64 options are supported in one group." ); + + "since only up to 64 options are supported in one group."); return; } - o.SetPosition( groupIdx, group.Count ); + o.SetPosition(groupIdx, group.Count); - switch( group ) + switch (group) { case SingleModGroup s: - s.OptionData.Add( o ); + s.OptionData.Add(o); break; case MultiModGroup m: - m.PrioritizedOptions.Add( ( o, priority ) ); + m.PrioritizedOptions.Add((o, priority)); break; } - ModOptionChanged.Invoke( ModOptionChangeType.OptionAdded, mod, groupIdx, group.Count - 1, -1 ); + ModOptionChanged.Invoke(ModOptionChangeType.OptionAdded, mod, groupIdx, group.Count - 1, -1); } - public void DeleteOption( Mod mod, int groupIdx, int optionIdx ) + public void DeleteOption(Mod mod, int groupIdx, int optionIdx) { - var group = mod._groups[ groupIdx ]; - ModOptionChanged.Invoke( ModOptionChangeType.PrepareChange, mod, groupIdx, optionIdx, -1 ); - switch( group ) + var group = mod._groups[groupIdx]; + ModOptionChanged.Invoke(ModOptionChangeType.PrepareChange, mod, groupIdx, optionIdx, -1); + switch (group) { case SingleModGroup s: - s.OptionData.RemoveAt( optionIdx ); + s.OptionData.RemoveAt(optionIdx); break; case MultiModGroup m: - m.PrioritizedOptions.RemoveAt( optionIdx ); + m.PrioritizedOptions.RemoveAt(optionIdx); break; } - group.UpdatePositions( optionIdx ); - ModOptionChanged.Invoke( ModOptionChangeType.OptionDeleted, mod, groupIdx, optionIdx, -1 ); + group.UpdatePositions(optionIdx); + ModOptionChanged.Invoke(ModOptionChangeType.OptionDeleted, mod, groupIdx, optionIdx, -1); } - public void MoveOption( Mod mod, int groupIdx, int optionIdxFrom, int optionIdxTo ) + public void MoveOption(Mod mod, int groupIdx, int optionIdxFrom, int optionIdxTo) { - var group = mod._groups[ groupIdx ]; - if( group.MoveOption( optionIdxFrom, optionIdxTo ) ) - { - ModOptionChanged.Invoke( ModOptionChangeType.OptionMoved, mod, groupIdx, optionIdxFrom, optionIdxTo ); - } + var group = mod._groups[groupIdx]; + if (group.MoveOption(optionIdxFrom, optionIdxTo)) + ModOptionChanged.Invoke(ModOptionChangeType.OptionMoved, mod, groupIdx, optionIdxFrom, optionIdxTo); } - public void OptionSetManipulations( Mod mod, int groupIdx, int optionIdx, HashSet< MetaManipulation > manipulations ) + public void OptionSetManipulations(Mod mod, int groupIdx, int optionIdx, HashSet manipulations) { - var subMod = GetSubMod( mod, groupIdx, optionIdx ); - if( subMod.Manipulations.Count == manipulations.Count - && subMod.Manipulations.All( m => manipulations.TryGetValue( m, out var old ) && old.EntryEquals( m ) ) ) - { + var subMod = GetSubMod(mod, groupIdx, optionIdx); + if (subMod.Manipulations.Count == manipulations.Count + && subMod.Manipulations.All(m => manipulations.TryGetValue(m, out var old) && old.EntryEquals(m))) return; - } - ModOptionChanged.Invoke( ModOptionChangeType.PrepareChange, mod, groupIdx, optionIdx, -1 ); + ModOptionChanged.Invoke(ModOptionChangeType.PrepareChange, mod, groupIdx, optionIdx, -1); subMod.ManipulationData = manipulations; - ModOptionChanged.Invoke( ModOptionChangeType.OptionMetaChanged, mod, groupIdx, optionIdx, -1 ); + ModOptionChanged.Invoke(ModOptionChangeType.OptionMetaChanged, mod, groupIdx, optionIdx, -1); } - public void OptionSetFiles( Mod mod, int groupIdx, int optionIdx, Dictionary< Utf8GamePath, FullPath > replacements ) + public void OptionSetFiles(Mod mod, int groupIdx, int optionIdx, Dictionary replacements) { - var subMod = GetSubMod( mod, groupIdx, optionIdx ); - if( subMod.FileData.SetEquals( replacements ) ) - { + var subMod = GetSubMod(mod, groupIdx, optionIdx); + if (subMod.FileData.SetEquals(replacements)) return; - } - ModOptionChanged.Invoke( ModOptionChangeType.PrepareChange, mod, groupIdx, optionIdx, -1 ); + ModOptionChanged.Invoke(ModOptionChangeType.PrepareChange, mod, groupIdx, optionIdx, -1); subMod.FileData = replacements; - ModOptionChanged.Invoke( ModOptionChangeType.OptionFilesChanged, mod, groupIdx, optionIdx, -1 ); + ModOptionChanged.Invoke(ModOptionChangeType.OptionFilesChanged, mod, groupIdx, optionIdx, -1); } - public void OptionAddFiles( Mod mod, int groupIdx, int optionIdx, Dictionary< Utf8GamePath, FullPath > additions ) + public void OptionAddFiles(Mod mod, int groupIdx, int optionIdx, Dictionary additions) { - var subMod = GetSubMod( mod, groupIdx, optionIdx ); + var subMod = GetSubMod(mod, groupIdx, optionIdx); var oldCount = subMod.FileData.Count; - subMod.FileData.AddFrom( additions ); - if( oldCount != subMod.FileData.Count ) - { - ModOptionChanged.Invoke( ModOptionChangeType.OptionFilesAdded, mod, groupIdx, optionIdx, -1 ); - } + subMod.FileData.AddFrom(additions); + if (oldCount != subMod.FileData.Count) + ModOptionChanged.Invoke(ModOptionChangeType.OptionFilesAdded, mod, groupIdx, optionIdx, -1); } - public void OptionSetFileSwaps( Mod mod, int groupIdx, int optionIdx, Dictionary< Utf8GamePath, FullPath > swaps ) + public void OptionSetFileSwaps(Mod mod, int groupIdx, int optionIdx, Dictionary swaps) { - var subMod = GetSubMod( mod, groupIdx, optionIdx ); - if( subMod.FileSwapData.SetEquals( swaps ) ) - { + var subMod = GetSubMod(mod, groupIdx, optionIdx); + if (subMod.FileSwapData.SetEquals(swaps)) return; - } - ModOptionChanged.Invoke( ModOptionChangeType.PrepareChange, mod, groupIdx, optionIdx, -1 ); + ModOptionChanged.Invoke(ModOptionChangeType.PrepareChange, mod, groupIdx, optionIdx, -1); subMod.FileSwapData = swaps; - ModOptionChanged.Invoke( ModOptionChangeType.OptionSwapsChanged, mod, groupIdx, optionIdx, -1 ); + ModOptionChanged.Invoke(ModOptionChangeType.OptionSwapsChanged, mod, groupIdx, optionIdx, -1); } - public static bool VerifyFileName( Mod mod, IModGroup? group, string newName, bool message ) + public bool VerifyFileName(Mod mod, IModGroup? group, string newName, bool message) { var path = newName.RemoveInvalidPathSymbols(); - if( path.Length == 0 - || mod.Groups.Any( o => !ReferenceEquals( o, group ) - && string.Equals( o.Name.RemoveInvalidPathSymbols(), path, StringComparison.OrdinalIgnoreCase ) ) ) + if (path.Length == 0 + || mod.Groups.Any(o => !ReferenceEquals(o, group) + && string.Equals(o.Name.RemoveInvalidPathSymbols(), path, StringComparison.OrdinalIgnoreCase))) { - if( message ) - { - Penumbra.Log.Warning( $"Could not name option {newName} because option with same filename {path} already exists." ); - } + if (message) + _chat.NotificationMessage($"Could not name option {newName} because option with same filename {path} already exists.", + "Warning", NotificationType.Warning); return false; } @@ -345,43 +319,35 @@ public sealed partial class Mod return true; } - private static SubMod GetSubMod( Mod mod, int groupIdx, int optionIdx ) + private static SubMod GetSubMod(Mod mod, int groupIdx, int optionIdx) { - if( groupIdx == -1 && optionIdx == 0 ) - { + if (groupIdx == -1 && optionIdx == 0) return mod._default; - } - return mod._groups[ groupIdx ] switch + return mod._groups[groupIdx] switch { - SingleModGroup s => s.OptionData[ optionIdx ], - MultiModGroup m => m.PrioritizedOptions[ optionIdx ].Mod, + SingleModGroup s => s.OptionData[optionIdx], + MultiModGroup m => m.PrioritizedOptions[optionIdx].Mod, _ => throw new InvalidOperationException(), }; } - private static void OnModOptionChange( ModOptionChangeType type, Mod mod, int groupIdx, int _, int _2 ) + private static void OnModOptionChange(ModOptionChangeType type, Mod mod, int groupIdx, int _, int _2) { - if( type == ModOptionChangeType.PrepareChange ) - { + if (type == ModOptionChangeType.PrepareChange) return; - } // File deletion is handled in the actual function. - if( type is ModOptionChangeType.GroupDeleted or ModOptionChangeType.GroupMoved ) + if (type is ModOptionChangeType.GroupDeleted or ModOptionChangeType.GroupMoved) { mod.SaveAllGroups(); } else { - if( groupIdx == -1 ) - { + if (groupIdx == -1) mod.SaveDefaultModDelayed(); - } else - { - IModGroup.SaveDelayed( mod._groups[ groupIdx ], mod.ModPath, groupIdx ); - } + IModGroup.SaveDelayed(mod._groups[groupIdx], mod.ModPath, groupIdx); } bool ComputeChangedItems() @@ -396,20 +362,20 @@ public sealed partial class Mod ModOptionChangeType.GroupAdded => ComputeChangedItems() & mod.SetCounts(), ModOptionChangeType.GroupDeleted => ComputeChangedItems() & mod.SetCounts(), ModOptionChangeType.GroupMoved => false, - ModOptionChangeType.GroupTypeChanged => mod.HasOptions = mod.Groups.Any( o => o.IsOption ), + ModOptionChangeType.GroupTypeChanged => mod.HasOptions = mod.Groups.Any(o => o.IsOption), ModOptionChangeType.PriorityChanged => false, ModOptionChangeType.OptionAdded => ComputeChangedItems() & mod.SetCounts(), ModOptionChangeType.OptionDeleted => ComputeChangedItems() & mod.SetCounts(), ModOptionChangeType.OptionMoved => false, ModOptionChangeType.OptionFilesChanged => ComputeChangedItems() - & ( 0 < ( mod.TotalFileCount = mod.AllSubMods.Sum( s => s.Files.Count ) ) ), + & (0 < (mod.TotalFileCount = mod.AllSubMods.Sum(s => s.Files.Count))), ModOptionChangeType.OptionSwapsChanged => ComputeChangedItems() - & ( 0 < ( mod.TotalSwapCount = mod.AllSubMods.Sum( s => s.FileSwaps.Count ) ) ), + & (0 < (mod.TotalSwapCount = mod.AllSubMods.Sum(s => s.FileSwaps.Count))), ModOptionChangeType.OptionMetaChanged => ComputeChangedItems() - & ( 0 < ( mod.TotalManipulations = mod.AllSubMods.Sum( s => s.Manipulations.Count ) ) ), + & (0 < (mod.TotalManipulations = mod.AllSubMods.Sum(s => s.Manipulations.Count))), ModOptionChangeType.DisplayChange => false, _ => false, }; } } -} \ No newline at end of file +} diff --git a/Penumbra/Mods/Manager/Mod.Manager.Root.cs b/Penumbra/Mods/Manager/Mod.Manager.Root.cs index dace9f57..83927a1e 100644 --- a/Penumbra/Mods/Manager/Mod.Manager.Root.cs +++ b/Penumbra/Mods/Manager/Mod.Manager.Root.cs @@ -130,8 +130,8 @@ public sealed partial class Mod } _exportDirectory = null; - Penumbra.Config.ExportDirectory = string.Empty; - Penumbra.Config.Save(); + _config.ExportDirectory = string.Empty; + _config.Save(); return; } @@ -166,8 +166,8 @@ public sealed partial class Mod if( change ) { - Penumbra.Config.ExportDirectory = dir.FullName; - Penumbra.Config.Save(); + _config.ExportDirectory = dir.FullName; + _config.Save(); } } } diff --git a/Penumbra/Mods/Manager/Mod.Manager.cs b/Penumbra/Mods/Manager/Mod.Manager.cs index 55ef005f..95f40592 100644 --- a/Penumbra/Mods/Manager/Mod.Manager.cs +++ b/Penumbra/Mods/Manager/Mod.Manager.cs @@ -2,12 +2,13 @@ using System; using System.Collections; using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; +using Penumbra.Util; namespace Penumbra.Mods; public sealed partial class Mod { - public sealed partial class Manager : IReadOnlyList< Mod > + public sealed partial class Manager : IReadOnlyList { // Set when reading Config and migrating from v4 to v5. public static bool MigrateModBackups = false; @@ -16,55 +17,60 @@ public sealed partial class Mod // Mods are added when they are created or imported. // Mods are removed when they are deleted or when they are toggled in any collection. // Also gets cleared on mod rediscovery. - public readonly HashSet< Mod > NewMods = new(); + public readonly HashSet NewMods = new(); - private readonly List< Mod > _mods = new(); + private readonly List _mods = new(); - public Mod this[ int idx ] - => _mods[ idx ]; + public Mod this[int idx] + => _mods[idx]; - public Mod this[ Index idx ] - => _mods[ idx ]; + public Mod this[Index idx] + => _mods[idx]; public int Count => _mods.Count; - public IEnumerator< Mod > GetEnumerator() + public IEnumerator GetEnumerator() => _mods.GetEnumerator(); IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); - public Manager( string modDirectory ) + private readonly Configuration _config; + private readonly ChatService _chat; + + public Manager(StartTracker time, Configuration config, ChatService chat) { + using var timer = time.Measure(StartTimeType.Mods); + _config = config; + _chat = chat; ModDirectoryChanged += OnModDirectoryChange; - SetBaseDirectory( modDirectory, true ); - UpdateExportDirectory( Penumbra.Config.ExportDirectory, false ); + SetBaseDirectory(config.ModDirectory, true); + UpdateExportDirectory(_config.ExportDirectory, false); ModOptionChanged += OnModOptionChange; ModPathChanged += OnModPathChange; + DiscoverMods(); } // Try to obtain a mod by its directory name (unique identifier, preferred), // or the first mod of the given name if no directory fits. - public bool TryGetMod( string modDirectory, string modName, [NotNullWhen( true )] out Mod? mod ) + public bool TryGetMod(string modDirectory, string modName, [NotNullWhen(true)] out Mod? mod) { mod = null; - foreach( var m in _mods ) + foreach (var m in _mods) { - if( string.Equals(m.ModPath.Name, modDirectory, StringComparison.OrdinalIgnoreCase) ) + if (string.Equals(m.ModPath.Name, modDirectory, StringComparison.OrdinalIgnoreCase)) { mod = m; return true; } - if( m.Name == modName ) - { + if (m.Name == modName) mod ??= m; - } } return mod != null; } } -} \ No newline at end of file +} diff --git a/Penumbra/Mods/Mod.BasePath.cs b/Penumbra/Mods/Mod.BasePath.cs index 3a849a26..3f5db087 100644 --- a/Penumbra/Mods/Mod.BasePath.cs +++ b/Penumbra/Mods/Mod.BasePath.cs @@ -18,6 +18,9 @@ public partial class Mod public DirectoryInfo ModPath { get; private set; } public int Index { get; private set; } = -1; + public bool IsTemporary + => Index < 0; + // Unused if Index < 0 but used for special temporary mods. public int Priority => 0; diff --git a/Penumbra/Mods/ModFileSystem.cs b/Penumbra/Mods/ModFileSystem.cs index fa9fec1d..9ea21820 100644 --- a/Penumbra/Mods/ModFileSystem.cs +++ b/Penumbra/Mods/ModFileSystem.cs @@ -4,52 +4,37 @@ using System.Diagnostics.CodeAnalysis; using System.IO; using System.Linq; using System.Text.RegularExpressions; -using Dalamud.Plugin; using OtterGui.Filesystem; -using Penumbra.Services; - +using Penumbra.Services; +using Penumbra.Util; + namespace Penumbra.Mods; -public sealed class ModFileSystem : FileSystem< Mod >, IDisposable +public sealed class ModFileSystem : FileSystem, IDisposable, ISaveable { - public static string ModFileSystemFile(DalamudPluginInterface pi) - => Path.Combine( pi.GetPluginConfigDirectory(), "sort_order.json" ); - - // Save the current sort order. - // Does not save or copy the backup in the current mod directory, - // as this is done on mod directory changes only. - // TODO - private void SaveFilesystem() - { - SaveToFile( new FileInfo( ModFileSystemFile(DalamudServices.PluginInterface) ), SaveMod, true ); - Penumbra.Log.Verbose( "Saved mod filesystem." ); - } - - private void Save() - => Penumbra.Framework.RegisterDelayed( nameof( SaveFilesystem ), SaveFilesystem ); + private readonly Mod.Manager _modManager; + private readonly FilenameService _files; // Create a new ModFileSystem from the currently loaded mods and the current sort order file. - public static ModFileSystem Load() + public ModFileSystem(Mod.Manager modManager, FilenameService files) { - var ret = new ModFileSystem(); - ret.Reload(); - - ret.Changed += ret.OnChange; - Penumbra.ModManager.ModDiscoveryFinished += ret.Reload; - Penumbra.ModManager.ModDataChanged += ret.OnDataChange; - Penumbra.ModManager.ModPathChanged += ret.OnModPathChange; - - return ret; + _modManager = modManager; + _files = files; + Reload(); + Changed += OnChange; + _modManager.ModDiscoveryFinished += Reload; + _modManager.ModDataChanged += OnDataChange; + _modManager.ModPathChanged += OnModPathChange; } public void Dispose() { - Penumbra.ModManager.ModPathChanged -= OnModPathChange; - Penumbra.ModManager.ModDiscoveryFinished -= Reload; - Penumbra.ModManager.ModDataChanged -= OnDataChange; + _modManager.ModPathChanged -= OnModPathChange; + _modManager.ModDiscoveryFinished -= Reload; + _modManager.ModDataChanged -= OnDataChange; } - public struct ImportDate : ISortMode< Mod > + public struct ImportDate : ISortMode { public string Name => "Import Date (Older First)"; @@ -57,11 +42,11 @@ public sealed class ModFileSystem : FileSystem< Mod >, IDisposable public string Description => "In each folder, sort all subfolders lexicographically, then sort all leaves using their import date."; - public IEnumerable< IPath > GetChildren( Folder f ) - => f.GetSubFolders().Cast< IPath >().Concat( f.GetLeaves().OrderBy( l => l.Value.ImportDate ) ); + public IEnumerable GetChildren(Folder f) + => f.GetSubFolders().Cast().Concat(f.GetLeaves().OrderBy(l => l.Value.ImportDate)); } - public struct InverseImportDate : ISortMode< Mod > + public struct InverseImportDate : ISortMode { public string Name => "Import Date (Newer First)"; @@ -69,71 +54,61 @@ public sealed class ModFileSystem : FileSystem< Mod >, IDisposable public string Description => "In each folder, sort all subfolders lexicographically, then sort all leaves using their inverse import date."; - public IEnumerable< IPath > GetChildren( Folder f ) - => f.GetSubFolders().Cast< IPath >().Concat( f.GetLeaves().OrderByDescending( l => l.Value.ImportDate ) ); + public IEnumerable GetChildren(Folder f) + => f.GetSubFolders().Cast().Concat(f.GetLeaves().OrderByDescending(l => l.Value.ImportDate)); } // Reload the whole filesystem from currently loaded mods and the current sort order file. // Used on construction and on mod rediscoveries. private void Reload() - { + { // TODO - if( Load( new FileInfo( ModFileSystemFile(DalamudServices.PluginInterface) ), Penumbra.ModManager, ModToIdentifier, ModToName ) ) - { - Save(); - } + if (Load(new FileInfo(_files.FilesystemFile), _modManager, ModToIdentifier, ModToName)) + Penumbra.SaveService.ImmediateSave(this); - Penumbra.Log.Debug( "Reloaded mod filesystem." ); + Penumbra.Log.Debug("Reloaded mod filesystem."); } // Save the filesystem on every filesystem change except full reloading. - private void OnChange( FileSystemChangeType type, IPath _1, IPath? _2, IPath? _3 ) + private void OnChange(FileSystemChangeType type, IPath _1, IPath? _2, IPath? _3) { - if( type != FileSystemChangeType.Reload ) - { - Save(); - } + if (type != FileSystemChangeType.Reload) + Penumbra.SaveService.QueueSave(this); } // Update sort order when defaulted mod names change. - private void OnDataChange( ModDataChangeType type, Mod mod, string? oldName ) + private void OnDataChange(ModDataChangeType type, Mod mod, string? oldName) { - if( type.HasFlag( ModDataChangeType.Name ) && oldName != null ) + if (type.HasFlag(ModDataChangeType.Name) && oldName != null) { var old = oldName.FixName(); - if( Find( old, out var child ) && child is not Folder ) - { - Rename( child, mod.Name.Text ); - } + if (Find(old, out var child) && child is not Folder) + Rename(child, mod.Name.Text); } } // Update the filesystem if a mod has been added or removed. // Save it, if the mod directory has been moved, since this will change the save format. - private void OnModPathChange( ModPathChangeType type, Mod mod, DirectoryInfo? oldPath, DirectoryInfo? newPath ) + private void OnModPathChange(ModPathChangeType type, Mod mod, DirectoryInfo? oldPath, DirectoryInfo? newPath) { - switch( type ) + switch (type) { case ModPathChangeType.Added: var originalName = mod.Name.Text.FixName(); var name = originalName; var counter = 1; - while( Find( name, out _ ) ) - { + while (Find(name, out _)) name = $"{originalName} ({++counter})"; - } - CreateLeaf( Root, name, mod ); + CreateLeaf(Root, name, mod); break; case ModPathChangeType.Deleted: - if( FindLeaf( mod, out var leaf ) ) - { - Delete( leaf ); - } + if (FindLeaf(mod, out var leaf)) + Delete(leaf); break; case ModPathChangeType.Moved: - Save(); + Penumbra.SaveService.QueueSave(this); break; case ModPathChangeType.Reloaded: // Nothing @@ -142,32 +117,44 @@ public sealed class ModFileSystem : FileSystem< Mod >, IDisposable } // Search the entire filesystem for the leaf corresponding to a mod. - public bool FindLeaf( Mod mod, [NotNullWhen( true )] out Leaf? leaf ) + public bool FindLeaf(Mod mod, [NotNullWhen(true)] out Leaf? leaf) { - leaf = Root.GetAllDescendants( ISortMode< Mod >.Lexicographical ) - .OfType< Leaf >() - .FirstOrDefault( l => l.Value == mod ); + leaf = Root.GetAllDescendants(ISortMode.Lexicographical) + .OfType() + .FirstOrDefault(l => l.Value == mod); return leaf != null; } // Used for saving and loading. - private static string ModToIdentifier( Mod mod ) + private static string ModToIdentifier(Mod mod) => mod.ModPath.Name; - private static string ModToName( Mod mod ) + private static string ModToName(Mod mod) => mod.Name.Text.FixName(); // Return whether a mod has a custom path or is just a numbered default path. - public static bool ModHasDefaultPath( Mod mod, string fullPath ) + public static bool ModHasDefaultPath(Mod mod, string fullPath) { - var regex = new Regex( $@"^{Regex.Escape( ModToName( mod ) )}( \(\d+\))?$" ); - return regex.IsMatch( fullPath ); + var regex = new Regex($@"^{Regex.Escape(ModToName(mod))}( \(\d+\))?$"); + return regex.IsMatch(fullPath); } - private static (string, bool) SaveMod( Mod mod, string fullPath ) + private static (string, bool) SaveMod(Mod mod, string fullPath) // Only save pairs with non-default paths. - => ModHasDefaultPath( mod, fullPath ) - ? ( string.Empty, false ) - : ( ModToIdentifier( mod ), true ); -} \ No newline at end of file + => ModHasDefaultPath(mod, fullPath) + ? (string.Empty, false) + : (ModToIdentifier(mod), true); + + public string ToFilename(FilenameService fileNames) + => fileNames.FilesystemFile; + + public void Save(StreamWriter writer) + => SaveToFile(writer, SaveMod, true); + + public string TypeName + => "Mod File System"; + + public string LogName(string _) + => "to file"; +} diff --git a/Penumbra/Mods/Subclasses/Mod.Files.MultiModGroup.cs b/Penumbra/Mods/Subclasses/Mod.Files.MultiModGroup.cs index 90bc687a..70840b24 100644 --- a/Penumbra/Mods/Subclasses/Mod.Files.MultiModGroup.cs +++ b/Penumbra/Mods/Subclasses/Mod.Files.MultiModGroup.cs @@ -21,69 +21,67 @@ public partial class Mod public GroupType Type => GroupType.Multi; - public string Name { get; set; } = "Group"; - public string Description { get; set; } = "A non-exclusive group of settings."; - public int Priority { get; set; } - public uint DefaultSettings { get; set; } + public string Name { get; set; } = "Group"; + public string Description { get; set; } = "A non-exclusive group of settings."; + public int Priority { get; set; } + public uint DefaultSettings { get; set; } - public int OptionPriority( Index idx ) - => PrioritizedOptions[ idx ].Priority; + public int OptionPriority(Index idx) + => PrioritizedOptions[idx].Priority; - public ISubMod this[ Index idx ] - => PrioritizedOptions[ idx ].Mod; + public ISubMod this[Index idx] + => PrioritizedOptions[idx].Mod; [JsonIgnore] public int Count => PrioritizedOptions.Count; - public readonly List< (SubMod Mod, int Priority) > PrioritizedOptions = new(); + public readonly List<(SubMod Mod, int Priority)> PrioritizedOptions = new(); - public IEnumerator< ISubMod > GetEnumerator() - => PrioritizedOptions.Select( o => o.Mod ).GetEnumerator(); + public IEnumerator GetEnumerator() + => PrioritizedOptions.Select(o => o.Mod).GetEnumerator(); IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); - public static MultiModGroup? Load( Mod mod, JObject json, int groupIdx ) + public static MultiModGroup? Load(Mod mod, JObject json, int groupIdx) { var ret = new MultiModGroup() { - Name = json[ nameof( Name ) ]?.ToObject< string >() ?? string.Empty, - Description = json[ nameof( Description ) ]?.ToObject< string >() ?? string.Empty, - Priority = json[ nameof( Priority ) ]?.ToObject< int >() ?? 0, - DefaultSettings = json[ nameof( DefaultSettings ) ]?.ToObject< uint >() ?? 0, + Name = json[nameof(Name)]?.ToObject() ?? string.Empty, + Description = json[nameof(Description)]?.ToObject() ?? string.Empty, + Priority = json[nameof(Priority)]?.ToObject() ?? 0, + DefaultSettings = json[nameof(DefaultSettings)]?.ToObject() ?? 0, }; - if( ret.Name.Length == 0 ) - { + if (ret.Name.Length == 0) return null; - } var options = json["Options"]; - if( options != null ) - { - foreach( var child in options.Children() ) + if (options != null) + foreach (var child in options.Children()) { - if( ret.PrioritizedOptions.Count == IModGroup.MaxMultiOptions ) + if (ret.PrioritizedOptions.Count == IModGroup.MaxMultiOptions) { - ChatUtil.NotificationMessage( $"Multi Group {ret.Name} has more than {IModGroup.MaxMultiOptions} options, ignoring excessive options.", "Warning", NotificationType.Warning ); + Penumbra.ChatService.NotificationMessage( + $"Multi Group {ret.Name} has more than {IModGroup.MaxMultiOptions} options, ignoring excessive options.", "Warning", + NotificationType.Warning); break; } - var subMod = new SubMod( mod ); - subMod.SetPosition( groupIdx, ret.PrioritizedOptions.Count ); - subMod.Load( mod.ModPath, child, out var priority ); - ret.PrioritizedOptions.Add( ( subMod, priority ) ); + var subMod = new SubMod(mod); + subMod.SetPosition(groupIdx, ret.PrioritizedOptions.Count); + subMod.Load(mod.ModPath, child, out var priority); + ret.PrioritizedOptions.Add((subMod, priority)); } - } - ret.DefaultSettings = (uint) (ret.DefaultSettings & ( ( 1ul << ret.Count ) - 1 )); + ret.DefaultSettings = (uint)(ret.DefaultSettings & ((1ul << ret.Count) - 1)); return ret; } - public IModGroup Convert( GroupType type ) + public IModGroup Convert(GroupType type) { - switch( type ) + switch (type) { case GroupType.Multi: return this; case GroupType.Single: @@ -92,32 +90,28 @@ public partial class Mod Name = Name, Description = Description, Priority = Priority, - DefaultSettings = ( uint )Math.Max( Math.Min( Count - 1, BitOperations.TrailingZeroCount( DefaultSettings) ), 0 ), + DefaultSettings = (uint)Math.Max(Math.Min(Count - 1, BitOperations.TrailingZeroCount(DefaultSettings)), 0), }; - multi.OptionData.AddRange( PrioritizedOptions.Select( p => p.Mod ) ); + multi.OptionData.AddRange(PrioritizedOptions.Select(p => p.Mod)); return multi; - default: throw new ArgumentOutOfRangeException( nameof( type ), type, null ); + default: throw new ArgumentOutOfRangeException(nameof(type), type, null); } } - public bool MoveOption( int optionIdxFrom, int optionIdxTo ) + public bool MoveOption(int optionIdxFrom, int optionIdxTo) { - if( !PrioritizedOptions.Move( optionIdxFrom, optionIdxTo ) ) - { + if (!PrioritizedOptions.Move(optionIdxFrom, optionIdxTo)) return false; - } - DefaultSettings = Functions.MoveBit( DefaultSettings, optionIdxFrom, optionIdxTo ); - UpdatePositions( Math.Min( optionIdxFrom, optionIdxTo ) ); + DefaultSettings = Functions.MoveBit(DefaultSettings, optionIdxFrom, optionIdxTo); + UpdatePositions(Math.Min(optionIdxFrom, optionIdxTo)); return true; } - public void UpdatePositions( int from = 0 ) + public void UpdatePositions(int from = 0) { - foreach( var ((o, _), i) in PrioritizedOptions.WithIndex().Skip( from ) ) - { - o.SetPosition( o.GroupIdx, i ); - } + foreach (var ((o, _), i) in PrioritizedOptions.WithIndex().Skip(from)) + o.SetPosition(o.GroupIdx, i); } } -} \ No newline at end of file +} diff --git a/Penumbra/Penumbra.cs b/Penumbra/Penumbra.cs index 563e6de9..51204402 100644 --- a/Penumbra/Penumbra.cs +++ b/Penumbra/Penumbra.cs @@ -29,11 +29,11 @@ using CharacterUtility = Penumbra.Interop.CharacterUtility; using DalamudUtil = Dalamud.Utility.Util; using ResidentResourceManager = Penumbra.Interop.Services.ResidentResourceManager; using Penumbra.Services; -using Penumbra.Interop.Services; - +using Penumbra.Interop.Services; + namespace Penumbra; -public class Penumbra : IDalamudPlugin +public partial class Penumbra : IDalamudPlugin { public string Name => "Penumbra"; @@ -43,8 +43,10 @@ public class Penumbra : IDalamudPlugin public static readonly string CommitHash = Assembly.GetExecutingAssembly().GetCustomAttribute()?.InformationalVersion ?? "Unknown"; - public static Logger Log { get; private set; } = null!; - public static Configuration Config { get; private set; } = null!; + public static Logger Log { get; private set; } = null!; + public static ChatService ChatService { get; private set; } = null!; + public static SaveService SaveService { get; private set; } = null!; + public static Configuration Config { get; private set; } = null!; public static ResidentResourceManager ResidentResources { get; private set; } = null!; public static CharacterUtility CharacterUtility { get; private set; } = null!; @@ -60,7 +62,7 @@ public class Penumbra : IDalamudPlugin public static IObjectIdentifier Identifier { get; private set; } = null!; public static IGamePathParser GamePathParser { get; private set; } = null!; public static StainService StainService { get; private set; } = null!; - + // TODO public static DalamudServices Dalamud { get; private set; } = null!; @@ -68,23 +70,21 @@ public class Penumbra : IDalamudPlugin public static PerformanceTracker Performance { get; private set; } = null!; - public readonly PathResolver PathResolver; - public readonly ObjectReloader ObjectReloader; - public readonly ModFileSystem ModFileSystem; - public readonly PenumbraApi Api; - public readonly HttpApi HttpApi; - public readonly PenumbraIpcProviders IpcProviders; - internal ConfigWindow? ConfigWindow { get; private set; } - private LaunchButton? _launchButton; - private WindowSystem? _windowSystem; - private Changelog? _changelog; - private CommandHandler? _commandHandler; - private readonly ResourceWatcher _resourceWatcher; - private bool _disposed; + public readonly PathResolver PathResolver; + public readonly RedrawService RedrawService; + public readonly ModFileSystem ModFileSystem; + public PenumbraApi Api = null!; + public HttpApi HttpApi = null!; + public PenumbraIpcProviders IpcProviders = null!; + internal ConfigWindow? ConfigWindow { get; private set; } + private PenumbraWindowSystem? _windowSystem; + private CommandHandler? _commandHandler; + private readonly ResourceWatcher _resourceWatcher; + private bool _disposed; private readonly PenumbraNew _tmp; public static ItemData ItemData { get; private set; } = null!; - + // TODO public static ResourceManagerService ResourceManagerService { get; private set; } = null!; public static CharacterResolver CharacterResolver { get; private set; } = null!; @@ -95,67 +95,38 @@ public class Penumbra : IDalamudPlugin Log = PenumbraNew.Log; try { - _tmp = new PenumbraNew(pluginInterface); + _tmp = new PenumbraNew(this, pluginInterface); + ChatService = _tmp.Services.GetRequiredService(); + SaveService = _tmp.Services.GetRequiredService(); Performance = _tmp.Services.GetRequiredService(); ValidityChecker = _tmp.Services.GetRequiredService(); _tmp.Services.GetRequiredService(); - Config = _tmp.Services.GetRequiredService(); - CharacterUtility = _tmp.Services.GetRequiredService(); - GameEvents = _tmp.Services.GetRequiredService(); - MetaFileManager = _tmp.Services.GetRequiredService(); - Framework = _tmp.Services.GetRequiredService(); - Actors = _tmp.Services.GetRequiredService().AwaitedService; - Identifier = _tmp.Services.GetRequiredService().AwaitedService; - GamePathParser = _tmp.Services.GetRequiredService(); - StainService = _tmp.Services.GetRequiredService(); - ItemData = _tmp.Services.GetRequiredService().AwaitedService; - Dalamud = _tmp.Services.GetRequiredService(); - TempMods = _tmp.Services.GetRequiredService(); - ResidentResources = _tmp.Services.GetRequiredService(); - - ResourceManagerService = _tmp.Services.GetRequiredService(); - - _tmp.Services.GetRequiredService().Measure(StartTimeType.Mods, () => - { - ModManager = new Mod.Manager(Config.ModDirectory); - ModManager.DiscoverMods(); - }); - - _tmp.Services.GetRequiredService().Measure(StartTimeType.Collections, () => - { - CollectionManager = new ModCollection.Manager(_tmp.Services.GetRequiredService(), ModManager); - CollectionManager.CreateNecessaryCaches(); - }); - - - TempCollections = _tmp.Services.GetRequiredService(); - - ModFileSystem = ModFileSystem.Load(); - ObjectReloader = new ObjectReloader(); - ResourceService = _tmp.Services.GetRequiredService(); - ResourceLoader = new ResourceLoader(ResourceService, _tmp.Services.GetRequiredService(), _tmp.Services.GetRequiredService(), _tmp.Services.GetRequiredService()); - PathResolver = new PathResolver(_tmp.Services.GetRequiredService(), _tmp.Services.GetRequiredService(), _tmp.Services.GetRequiredService(), ResourceLoader); - CharacterResolver = new CharacterResolver(Config, CollectionManager, TempCollections, ResourceLoader, PathResolver); - - _resourceWatcher = new ResourceWatcher(Config, ResourceService, ResourceLoader); - + Config = _tmp.Services.GetRequiredService(); + CharacterUtility = _tmp.Services.GetRequiredService(); + GameEvents = _tmp.Services.GetRequiredService(); + MetaFileManager = _tmp.Services.GetRequiredService(); + Framework = _tmp.Services.GetRequiredService(); + Actors = _tmp.Services.GetRequiredService().AwaitedService; + Identifier = _tmp.Services.GetRequiredService().AwaitedService; + GamePathParser = _tmp.Services.GetRequiredService(); + StainService = _tmp.Services.GetRequiredService(); + ItemData = _tmp.Services.GetRequiredService().AwaitedService; + Dalamud = _tmp.Services.GetRequiredService(); + TempMods = _tmp.Services.GetRequiredService(); + ResidentResources = _tmp.Services.GetRequiredService(); + ResourceManagerService = _tmp.Services.GetRequiredService(); + ModManager = _tmp.Services.GetRequiredService(); + CollectionManager = _tmp.Services.GetRequiredService(); + TempCollections = _tmp.Services.GetRequiredService(); + ModFileSystem = _tmp.Services.GetRequiredService(); + RedrawService = _tmp.Services.GetRequiredService(); + ResourceService = _tmp.Services.GetRequiredService(); + ResourceLoader = _tmp.Services.GetRequiredService(); + PathResolver = _tmp.Services.GetRequiredService(); + CharacterResolver = _tmp.Services.GetRequiredService(); + _resourceWatcher = _tmp.Services.GetRequiredService(); SetupInterface(); - - if (Config.EnableMods) - { - PathResolver.Enable(); - } - - using (var tApi = _tmp.Services.GetRequiredService().Measure(StartTimeType.Api)) - { - Api = new PenumbraApi(_tmp.Services.GetRequiredService(), this); - IpcProviders = new PenumbraIpcProviders(DalamudServices.PluginInterface, Api); - HttpApi = new HttpApi(Api); - if (Config.EnableHttpApi) - HttpApi.CreateWebServer(); - - SubscribeItemLinks(); - } + SetupApi(); ValidityChecker.LogExceptions(); Log.Information($"Penumbra Version {Version}, Commit #{CommitHash} successfully Loaded from {pluginInterface.SourceRepository}."); @@ -172,56 +143,47 @@ public class Penumbra : IDalamudPlugin } } + private void SetupApi() + { + using var timer = _tmp.Services.GetRequiredService().Measure(StartTimeType.Api); + Api = (PenumbraApi)_tmp.Services.GetRequiredService(); + IpcProviders = _tmp.Services.GetRequiredService(); + HttpApi = _tmp.Services.GetRequiredService(); + + if (Config.EnableHttpApi) + HttpApi.CreateWebServer(); + Api.ChangedItemTooltip += it => + { + if (it is Item) + ImGui.TextUnformatted("Left Click to create an item link in chat."); + }; + Api.ChangedItemClicked += (button, it) => + { + if (button == MouseButton.Left && it is Item item) + ChatService.LinkItem(item); + }; + } + private void SetupInterface() { Task.Run(() => { using var tInterface = _tmp.Services.GetRequiredService().Measure(StartTimeType.Interface); - var changelog = ConfigWindow.CreateChangelog(); - var cfg = new ConfigWindow(_tmp.Services.GetRequiredService(), _tmp.Services.GetRequiredService(), _tmp.Services.GetRequiredService(), this, _resourceWatcher) - { - IsOpen = Config.DebugMode, - }; - var btn = new LaunchButton(cfg); - var system = new WindowSystem(Name); - var cmd = new CommandHandler(DalamudServices.Commands, ObjectReloader, Config, this, cfg, ModManager, CollectionManager, - Actors); - system.AddWindow(cfg); - system.AddWindow(cfg.ModEditPopup); - system.AddWindow(changelog); + var system = _tmp.Services.GetRequiredService(); + _commandHandler = _tmp.Services.GetRequiredService(); if (!_disposed) { - _changelog = changelog; - ConfigWindow = cfg; - _windowSystem = system; - _launchButton = btn; - _commandHandler = cmd; - DalamudServices.PluginInterface.UiBuilder.OpenConfigUi += cfg.Toggle; - DalamudServices.PluginInterface.UiBuilder.Draw += _windowSystem.Draw; + _windowSystem = system; + ConfigWindow = system.Window; } else { - cfg.Dispose(); - btn.Dispose(); - cmd.Dispose(); + system.Dispose(); } } ); } - private void DisposeInterface() - { - if (_windowSystem != null) - DalamudServices.PluginInterface.UiBuilder.Draw -= _windowSystem.Draw; - - _launchButton?.Dispose(); - if (ConfigWindow != null) - { - DalamudServices.PluginInterface.UiBuilder.OpenConfigUi -= ConfigWindow.Toggle; - ConfigWindow.Dispose(); - } - } - public event Action? EnabledChange; public bool SetEnabled(bool enabled) @@ -237,7 +199,7 @@ public class Penumbra : IDalamudPlugin { CollectionManager.Default.SetFiles(); ResidentResources.Reload(); - ObjectReloader.RedrawAll(RedrawType.Redraw); + RedrawService.RedrawAll(RedrawType.Redraw); } } else @@ -247,7 +209,7 @@ public class Penumbra : IDalamudPlugin { CharacterUtility.ResetAll(); ResidentResources.Reload(); - ObjectReloader.RedrawAll(RedrawType.Redraw); + RedrawService.RedrawAll(RedrawType.Redraw); } } @@ -258,52 +220,15 @@ public class Penumbra : IDalamudPlugin } public void ForceChangelogOpen() - { - if (_changelog != null) - _changelog.ForceOpen = true; - } + => _windowSystem?.ForceChangelogOpen(); - private void SubscribeItemLinks() - { - Api.ChangedItemTooltip += it => - { - if (it is Item) - ImGui.TextUnformatted("Left Click to create an item link in chat."); - }; - Api.ChangedItemClicked += (button, it) => - { - if (button == MouseButton.Left && it is Item item) - ChatUtil.LinkItem(item); - }; - } - public void Dispose() { if (_disposed) return; - - // TODO + _tmp?.Dispose(); _disposed = true; - HttpApi?.Dispose(); - IpcProviders?.Dispose(); - Api?.Dispose(); - _commandHandler?.Dispose(); - StainService?.Dispose(); - ItemData?.Dispose(); - Actors?.Dispose(); - Identifier?.Dispose(); - Framework?.Dispose(); - DisposeInterface(); - ObjectReloader?.Dispose(); - ModFileSystem?.Dispose(); - CollectionManager?.Dispose(); - CharacterResolver?.Dispose(); // disposes PathResolver, TODO - _resourceWatcher?.Dispose(); - ResourceLoader?.Dispose(); - GameEvents?.Dispose(); - CharacterUtility?.Dispose(); - Performance?.Dispose(); } public string GatherSupportInformation() diff --git a/Penumbra/PenumbraNew.cs b/Penumbra/PenumbraNew.cs index 429cd5ae..004fd284 100644 --- a/Penumbra/PenumbraNew.cs +++ b/Penumbra/PenumbraNew.cs @@ -1,6 +1,7 @@ using System; using Dalamud.Plugin; using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.DependencyInjection.Extensions; using OtterGui.Classes; using OtterGui.Log; using Penumbra.Api; @@ -10,8 +11,10 @@ using Penumbra.GameData.Data; using Penumbra.Interop; using Penumbra.Interop.Loader; using Penumbra.Interop.Resolver; -using Penumbra.Interop.Services; +using Penumbra.Interop.Services; +using Penumbra.Mods; using Penumbra.Services; +using Penumbra.UI; using Penumbra.UI.Classes; using Penumbra.Util; @@ -25,7 +28,7 @@ public class PenumbraNew public static readonly Logger Log = new(); public readonly ServiceProvider Services; - public PenumbraNew(DalamudPluginInterface pi) + public PenumbraNew(Penumbra pnumb, DalamudPluginInterface pi) { var startTimer = new StartTracker(); using var time = startTimer.Measure(StartTimeType.Total); @@ -38,11 +41,14 @@ public class PenumbraNew .AddSingleton() .AddSingleton() .AddSingleton() - .AddSingleton(); + .AddSingleton() + .AddSingleton() + .AddSingleton(); // Add Dalamud services var dalamud = new DalamudServices(pi); dalamud.AddServices(services); + services.AddSingleton(pnumb); // Add Game Data services.AddSingleton() @@ -63,22 +69,43 @@ public class PenumbraNew .AddSingleton() .AddSingleton() .AddSingleton() - .AddSingleton(); - + .AddSingleton() + .AddSingleton(); + // Add Configuration services.AddTransient() .AddSingleton(); // Add Collection Services services.AddTransient() - .AddSingleton(); + .AddSingleton() + .AddSingleton(); // Add Mod Services - // TODO - services.AddSingleton(); + services.AddSingleton() + .AddSingleton() + .AddSingleton(); + + // Add main services + services.AddSingleton() + .AddSingleton() + .AddSingleton() + .AddSingleton(); // Add Interface - Services = services.BuildServiceProvider(new ServiceProviderOptions { ValidateOnBuild = true }); + services.AddSingleton() + .AddSingleton() + .AddSingleton() + .AddSingleton() + .AddSingleton() + .AddSingleton(); + + // Add API + services.AddSingleton() + .AddSingleton() + .AddSingleton(); + + Services = services.BuildServiceProvider(new ServiceProviderOptions { ValidateOnBuild = true }); } public void Dispose() diff --git a/Penumbra/Services/CommunicatorService.cs b/Penumbra/Services/CommunicatorService.cs index b976bef0..9d14ae16 100644 --- a/Penumbra/Services/CommunicatorService.cs +++ b/Penumbra/Services/CommunicatorService.cs @@ -27,7 +27,7 @@ public class CommunicatorService : IDisposable /// Parameter is the affected mod. /// Parameter is either null or the old name of the mod. /// - public readonly EventWrapper ModMetaChange = new(nameof(ModMetaChange)); + public readonly EventWrapper ModMetaChange = new(nameof(ModMetaChange)); public void Dispose() { diff --git a/Penumbra/Services/ConfigMigrationService.cs b/Penumbra/Services/ConfigMigrationService.cs index 4d37f693..b187e991 100644 --- a/Penumbra/Services/ConfigMigrationService.cs +++ b/Penumbra/Services/ConfigMigrationService.cs @@ -306,7 +306,7 @@ public class ConfigMigrationService return; var defaultCollection = ModCollection.CreateNewEmpty(ModCollection.DefaultCollection); - var defaultCollectionFile = defaultCollection.FileName; + var defaultCollectionFile = new FileInfo(_fileNames.CollectionFile(defaultCollection)); if (defaultCollectionFile.Exists) return; @@ -339,7 +339,7 @@ public class ConfigMigrationService dict = dict.ToDictionary(kvp => kvp.Key, kvp => kvp.Value with { Priority = maxPriority - kvp.Value.Priority }); defaultCollection = ModCollection.MigrateFromV0(ModCollection.DefaultCollection, dict); - defaultCollection.Save(); + Penumbra.SaveService.ImmediateSave(defaultCollection); } catch (Exception e) { diff --git a/Penumbra/Services/FilenameService.cs b/Penumbra/Services/FilenameService.cs index 0e883149..d57c854e 100644 --- a/Penumbra/Services/FilenameService.cs +++ b/Penumbra/Services/FilenameService.cs @@ -37,7 +37,7 @@ public class FilenameService /// Obtain the path of the local data file given a mod directory. Returns an empty string if the mod is temporary. - public string LocalDataFile(IModReadable mod) + public string LocalDataFile(Mod mod) => mod.IsTemporary ? string.Empty : LocalDataFile(mod.ModPath.FullName); /// Obtain the path of the local data file given a mod directory. @@ -65,7 +65,7 @@ public class FilenameService } /// Obtain the path of the meta file for a given mod. Returns an empty string if the mod is temporary. - public string ModMetaPath(IModReadable mod) + public string ModMetaPath(Mod mod) => mod.IsTemporary ? string.Empty : ModMetaPath(mod.ModPath.FullName); /// Obtain the path of the meta file given a mod directory. diff --git a/Penumbra/Services/ValidityChecker.cs b/Penumbra/Services/ValidityChecker.cs index d86167bc..a5bc3f3c 100644 --- a/Penumbra/Services/ValidityChecker.cs +++ b/Penumbra/Services/ValidityChecker.cs @@ -38,7 +38,7 @@ public class ValidityChecker public void LogExceptions() { if( ImcExceptions.Count > 0 ) - ChatUtil.NotificationMessage( $"{ImcExceptions} IMC Exceptions thrown during Penumbra load. Please repair your game files.", "Warning", NotificationType.Warning ); + Penumbra.ChatService.NotificationMessage( $"{ImcExceptions} IMC Exceptions thrown during Penumbra load. Please repair your game files.", "Warning", NotificationType.Warning ); } // Because remnants of penumbra in devPlugins cause issues, we check for them to warn users to remove them. diff --git a/Penumbra/UI/Changelog.cs b/Penumbra/UI/Changelog.cs new file mode 100644 index 00000000..3119e48c --- /dev/null +++ b/Penumbra/UI/Changelog.cs @@ -0,0 +1,386 @@ +using OtterGui.Widgets; + +namespace Penumbra.UI; + +public class PenumbraChangelog +{ + public const int LastChangelogVersion = 0; + + private readonly Configuration _config; + public readonly Changelog Changelog; + + public PenumbraChangelog(Configuration config) + { + _config = config; + Changelog = new Changelog("Penumbra Changelog", ConfigData, Save); + + Add5_7_0(Changelog); + Add5_7_1(Changelog); + Add5_8_0(Changelog); + Add5_8_7(Changelog); + Add5_9_0(Changelog); + Add5_10_0(Changelog); + Add5_11_0(Changelog); + Add5_11_1(Changelog); + Add6_0_0(Changelog); + Add6_0_2(Changelog); + Add6_0_5(Changelog); + Add6_1_0(Changelog); + Add6_1_1(Changelog); + Add6_2_0(Changelog); + Add6_3_0(Changelog); + Add6_4_0(Changelog); + Add6_5_0(Changelog); + Add6_5_2(Changelog); + Add6_6_0(Changelog); + Add6_6_1(Changelog); + } + + #region Changelogs + + private static void Add6_6_1(Changelog log) + => log.NextVersion("Version 0.6.6.1") + .RegisterEntry("Added an option to make successful chat commands not print their success confirmations to chat.") + .RegisterEntry("Fixed an issue with migration of old mods not working anymore (fixes Material UI problems).") + .RegisterEntry("Fixed some issues with using the Assign Current Player and Assign Current Target buttons."); + + private static void Add6_6_0(Changelog log) + => log.NextVersion("Version 0.6.6.0") + .RegisterEntry( + "Added new Collection Assignment Groups for Children NPC and Elderly NPC. Those take precedence before any non-individual assignments for any NPC using a child- or elderly model respectively.") + .RegisterEntry( + "Added an option to display Single Selection Groups as a group of radio buttons similar to Multi Selection Groups, when the number of available options is below the specified value. Default value is 2.") + .RegisterEntry("Added a button in option groups to collapse the option list if it has more than 5 available options.") + .RegisterEntry( + "Penumbra now circumvents the games inability to read files at paths longer than 260 UTF16 characters and can also deal with generic unicode symbols in paths.") + .RegisterEntry( + "This means that Penumbra should no longer cause issues when files become too long or when there is a non-ASCII character in them.", + 1) + .RegisterEntry( + "Shorter paths are still better, so restrictions on the root directory have not been relaxed. Mod names should no longer replace non-ASCII symbols on import though.", + 1) + .RegisterEntry( + "Resource logging has been relegated to its own tab with better filtering. Please do not keep resource logging on arbitrarily or set a low record limit if you do, otherwise this eats a lot of performance and memory after a while.") + .RegisterEntry( + "Added a lot of facilities to edit the shader part of .mtrl files and .shpk files themselves in the Advanced Editing Tab (Thanks Ny and aers).") + .RegisterEntry("Added splitting of Multi Selection Groups with too many options when importing .pmp files or adding mods via IPC.") + .RegisterEntry("Discovery, Reloading and Unloading of a specified mod is now possible via HTTP API (Thanks Sebastina).") + .RegisterEntry("Cleaned up the HTTP API somewhat, removed currently useless options.") + .RegisterEntry("Fixed an issue when extracting some textures.") + .RegisterEntry("Fixed an issue with mannequins inheriting individual assignments for the current player when using ownership.") + .RegisterEntry( + "Fixed an issue with the resolving of .phyb and .sklb files for Item Swaps of head or body items with an EST entry but no unique racial model."); + + private static void Add6_5_2(Changelog log) + => log.NextVersion("Version 0.6.5.2") + .RegisterEntry("Updated for game version 6.31 Hotfix.") + .RegisterEntry( + "Added option-specific descriptions for mods, instead of having just descriptions for groups of options. (Thanks Caraxi!)") + .RegisterEntry("Those are now accurately parsed from TTMPs, too.", 1) + .RegisterEntry("Improved launch times somewhat through parallelization of some tasks.") + .RegisterEntry( + "Added some performance tracking for start-up durations and for real time data to Release builds. They can be seen and enabled in the Debug tab when Debug Mode is enabled.") + .RegisterEntry("Fixed an issue with IMC changes and Mare Synchronos interoperability.") + .RegisterEntry("Fixed an issue with housing mannequins crashing the game when resource logging was enabled.") + .RegisterEntry("Fixed an issue generating Mip Maps for texture import on Wine."); + + private static void Add6_5_0(Changelog log) + => log.NextVersion("Version 0.6.5.0") + .RegisterEntry("Fixed an issue with Item Swaps not using applied IMC changes in some cases.") + .RegisterEntry("Improved error message on texture import when failing to create mip maps (slightly).") + .RegisterEntry("Tried to fix duty party banner identification again, also for the recommendation window this time.") + .RegisterEntry("Added batched IPC to improve Mare performance."); + + private static void Add6_4_0(Changelog log) + => log.NextVersion("Version 0.6.4.0") + .RegisterEntry("Fixed an issue with the identification of actors in the duty group portrait.") + .RegisterEntry("Fixed some issues with wrongly cached actors and resources.") + .RegisterEntry("Fixed animation handling after redraws (notably for PLD idle animations with a shield equipped).") + .RegisterEntry("Fixed an issue with collection listing API skipping one collection.") + .RegisterEntry( + "Fixed an issue with BGM files being sometimes loaded from other collections than the base collection, causing crashes.") + .RegisterEntry( + "Also distinguished file resolving for different file categories (improving performance) and disabled resolving for script files entirely.", + 1) + .RegisterEntry("Some miscellaneous backend changes due to the Glamourer rework."); + + private static void Add6_3_0(Changelog log) + => log.NextVersion("Version 0.6.3.0") + .RegisterEntry("Add an Assign Current Target button for individual assignments") + .RegisterEntry("Try identifying all banner actors correctly for PvE duties, Crystalline Conflict and Mahjong.") + .RegisterEntry("Please let me know if this does not work for anything except identical twins.", 1) + .RegisterEntry("Add handling for the 3 new screen actors (now 8 total, for PvE dutie portraits).") + .RegisterEntry("Update the Battle NPC name database for 6.3.") + .RegisterEntry("Added API/IPC functions to obtain or set group or individual collections.") + .RegisterEntry("Maybe fix a problem with textures sometimes not loading from their corresponding collection.") + .RegisterEntry("Another try to fix a problem with the collection selectors breaking state.") + .RegisterEntry("Fix a problem identifying companions.") + .RegisterEntry("Fix a problem when deleting collections assigned to Groups.") + .RegisterEntry( + "Fix a problem when using the Assign Currently Played Character button and then logging onto a different character without restarting in between.") + .RegisterEntry("Some miscellaneous backend changes."); + + private static void Add6_2_0(Changelog log) + => log.NextVersion("Version 0.6.2.0") + .RegisterEntry("Update Penumbra for .net7, Dalamud API 8 and patch 6.3.") + .RegisterEntry("Add a Bulktag chat command to toggle all mods with specific tags. (by SoyaX)") + .RegisterEntry("Add placeholder options for setting individual collections via chat command.") + .RegisterEntry("Add toggles to swap left and/or right rings separately for ring item swap.") + .RegisterEntry("Add handling for looping sound effects caused by animations in non-base collections.") + .RegisterEntry("Add an option to not use any mods at all in the Inspect/Try-On window.") + .RegisterEntry("Add handling for Mahjong actors.") + .RegisterEntry("Improve hint text for File Swaps in Advanced Editing, also inverted file swap display order.") + .RegisterEntry("Fix a problem where the collection selectors could get desynchronized after adding or deleting collections.") + .RegisterEntry("Fix a problem that could cause setting state to get desynchronized.") + .RegisterEntry("Fix an oversight where some special screen actors did not actually respect the settings made for them.") + .RegisterEntry("Add collection and associated game object to Full Resource Logging.") + .RegisterEntry("Add performance tracking for DEBUG-compiled versions (i.e. testing only).") + .RegisterEntry("Add some information to .mdl display and fix not respecting padding when reading them. (0.6.1.3)") + .RegisterEntry("Fix association of some vfx game objects. (0.6.1.3)") + .RegisterEntry("Stop forcing AVFX files to load synchronously. (0.6.1.3)") + .RegisterEntry("Fix an issue when incorporating deduplicated meta files. (0.6.1.2)"); + + private static void Add6_1_1(Changelog log) + => log.NextVersion("Version 0.6.1.1") + .RegisterEntry( + "Added a toggle to use all the effective changes from the entire currently selected collection for swaps, instead of the selected mod.") + .RegisterEntry("Fix using equipment paths for accessory swaps and thus accessory swaps not working at all") + .RegisterEntry("Fix issues with swaps with gender-locked gear where the models for the other gender do not exist.") + .RegisterEntry("Fix swapping universal hairstyles for midlanders breaking them for other races.") + .RegisterEntry("Add some actual error messages on failure to create item swaps.") + .RegisterEntry("Fix warnings about more than one affected item appearing for single items."); + + private static void Add6_1_0(Changelog log) + => log.NextVersion("Version 0.6.1.0 (Happy New Year! Edition)") + .RegisterEntry("Add a prototype for Item Swapping.") + .RegisterEntry("A new tab in Advanced Editing.", 1) + .RegisterEntry("Swapping of Hair, Tail, Ears, Equipment and Accessories is supported. Weapons and Faces may be coming.", 1) + .RegisterEntry("The manipulations currently in use by the selected mod with its currents settings (ignoring enabled state)" + + " should be used when creating the swap, but you can also just swap unmodded things.", 1) + .RegisterEntry("You can write a swap to a new mod, or to a new option in the currently selected mod.", 1) + .RegisterEntry("The swaps are not heavily tested yet, and may also be not perfectly efficient. Please leave feedback.", 1) + .RegisterEntry("More detailed help or explanations will be added later.", 1) + .RegisterEntry("Heavily improve Chat Commands. Use /penumbra help for more information.") + .RegisterEntry("Penumbra now considers meta manipulations for Changed Items.") + .RegisterEntry("Penumbra now tries to associate battle voices to specific actors, so that they work in collections.") + .RegisterEntry( + "Heavily improve .atex and .avfx handling, Penumbra can now associate VFX to specific actors far better, including ground effects.") + .RegisterEntry("Improve some file handling for Mare-Interaction.") + .RegisterEntry("Add Equipment Slots to Demihuman IMC Edits.") + .RegisterEntry( + "Add a toggle to keep metadata edits that apply the default value (and thus do not really change anything) on import from TexTools .meta files.") + .RegisterEntry("Add an option to directly change the 'Wait For Plugins To Load'-Dalamud Option from Penumbra.") + .RegisterEntry("Add API to copy mod settings from one mod to another.") + .RegisterEntry("Fix a problem where creating individual collections did not trigger events.") + .RegisterEntry("Add a Hack to support Anamnesis Redrawing better. (0.6.0.6)") + .RegisterEntry("Fix another problem with the aesthetician. (0.6.0.6)") + .RegisterEntry("Fix a problem with the export directory not being respected. (0.6.0.6)"); + + private static void Add6_0_5(Changelog log) + => log.NextVersion("Version 0.6.0.5") + .RegisterEntry("Allow hyphen as last character in player and retainer names.") + .RegisterEntry("Fix various bugs with ownership and GPose.") + .RegisterEntry("Fix collection selectors not updating for new or deleted collections in some cases.") + .RegisterEntry("Fix Chocobos not being recognized correctly.") + .RegisterEntry("Fix some problems with UI actors.") + .RegisterEntry("Fix problems with aesthetician again."); + + private static void Add6_0_2(Changelog log) + => log.NextVersion("Version 0.6.0.2") + .RegisterEntry("Let Bell Retainer collections apply to retainer-named mannequins.") + .RegisterEntry("Added a few informations to a help marker for new individual assignments.") + .RegisterEntry("Fix bug with Demi Human IMC paths.") + .RegisterEntry("Fix Yourself collection not applying to UI actors.") + .RegisterEntry("Fix Yourself collection not applying during aesthetician."); + + private static void Add6_0_0(Changelog log) + => log.NextVersion("Version 0.6.0.0") + .RegisterEntry("Revamped Individual Collections:") + .RegisterEntry("You can now specify individual collections for players (by name) of specific worlds or any world.", 1) + .RegisterEntry("You can also specify NPCs (by grouped name and type of NPC), and owned NPCs (by specifying an NPC and a Player).", + 1) + .RegisterHighlight( + "Migration should move all current names that correspond to NPCs to the appropriate NPC group and all names that can be valid Player names to a Player of any world.", + 1) + .RegisterHighlight( + "Please look through your Individual Collections to verify everything migrated correctly and corresponds to the game object you want. You might also want to change the 'Player (Any World)' collections to your specific homeworld.", + 1) + .RegisterEntry("You can also manually sort your Individual Collections by drag and drop now.", 1) + .RegisterEntry("This new system is a pretty big rework, so please report any discrepancies or bugs you find.", 1) + .RegisterEntry("These changes made the specific ownership settings for Retainers and for preferring named over ownership obsolete.", + 1) + .RegisterEntry("General ownership can still be toggled and should apply in order of: Owned NPC > Owner (if enabled) > General NPC.", + 1) + .RegisterEntry( + "Added NPC Model Parsing, changes in NPC models should now display the names of the changed game objects for most NPCs.") + .RegisterEntry("Changed Items now also display variant or subtype in addition to the model set ID where applicable.") + .RegisterEntry("Collection selectors can now be filtered by name.") + .RegisterEntry("Try to use Unicode normalization before replacing invalid path symbols on import for somewhat nicer paths.") + .RegisterEntry("Improved interface for group settings (minimally).") + .RegisterEntry("New Special or Individual Assignments now default to your current Base assignment instead of None.") + .RegisterEntry("Improved Support Info somewhat.") + .RegisterEntry("Added Dye Previews for in-game dyes and dyeing templates in Material Editing.") + .RegisterEntry("Colorset Editing now allows for negative values in all cases.") + .RegisterEntry("Added Export buttons to .mdl and .mtrl previews in Advanced Editing.") + .RegisterEntry("File Selection in the .mdl and .mtrl tabs now shows one associated game path by default and all on hover.") + .RegisterEntry( + "Added the option to reduplicate and normalize a mod, restoring all duplicates and moving the files to appropriate folders. (Duplicates Tab in Advanced Editing)") + .RegisterEntry( + "Added an option to re-export metadata changes to TexTools-typed .meta and .rgsp files. (Meta-Manipulations Tab in Advanced Editing)") + .RegisterEntry("Fixed several bugs with the incorporation of meta changes when not done during TTMP import.") + .RegisterEntry("Fixed a bug with RSP changes on non-base collections not applying correctly in some cases.") + .RegisterEntry("Fixed a bug when dragging options during mod edit.") + .RegisterEntry("Fixed a bug where sometimes the valid folder check caused issues.") + .RegisterEntry("Fixed a bug where collections with inheritances were newly saved on every load.") + .RegisterEntry("Fixed a bug where the /penumbra enable/disable command displayed the wrong message (functionality unchanged).") + .RegisterEntry("Mods without names or invalid mod folders are now warnings instead of errors.") + .RegisterEntry("Added IPC events for mod deletion, addition or moves, and resolving based on game objects.") + .RegisterEntry("Prevent a bug that allowed IPC to add Mods from outside the Penumbra root folder.") + .RegisterEntry("A lot of big backend changes."); + + private static void Add5_11_1(Changelog log) + => log.NextVersion("Version 0.5.11.1") + .RegisterEntry( + "The 0.5.11.0 Update exposed an issue in Penumbras file-saving scheme that rarely could cause some, most or even all of your mods to lose their group information.") + .RegisterEntry( + "If this has happened to you, you will need to reimport affected mods, or manually restore their groups. I am very sorry for that.", + 1) + .RegisterEntry( + "I believe the problem is fixed with 0.5.11.1, but I can not be sure since it would occur only rarely. For the same reason, a testing build would not help (as it also did not with 0.5.11.0 itself).", + 1) + .RegisterHighlight( + "If you do encounter this or similar problems in 0.5.11.1, please immediately let me know in Discord so I can revert the update again.", + 1); + + private static void Add5_11_0(Changelog log) + => log.NextVersion("Version 0.5.11.0") + .RegisterEntry( + "Added local data storage for mods in the plugin config folder. This information is not exported together with your mod, but not dependent on collections.") + .RegisterEntry("Moved the import date from mod metadata to local data.", 1) + .RegisterEntry("Added Favorites. You can declare mods as favorites and filter for them.", 1) + .RegisterEntry("Added Local Tags. You can apply custom Tags to mods and filter for them.", 1) + .RegisterEntry( + "Added Mod Tags. Mod Creators (and the Edit Mod tab) can set tags that are stored in the mod meta data and are thus exported.") + .RegisterEntry("Add backface and transparency toggles to .mtrl editing, as well as a info section.") + .RegisterEntry("Meta Manipulation editing now highlights if the selected ID is 0 or 1.") + .RegisterEntry("Fixed a bug when manually adding EQP or EQDP entries to Mods.") + .RegisterEntry("Updated some tooltips and hints.") + .RegisterEntry("Improved handling of IMC exception problems.") + .RegisterEntry("Fixed a bug with misidentification of equipment decals.") + .RegisterEntry( + "Character collections can now be set via chat command, too. (/penumbra collection character | )") + .RegisterEntry("Backend changes regarding API/IPC, consumers can but do not need to use the Penumbra.Api library as a submodule.") + .RegisterEntry("Added API to delete mods and read and set their pseudo-filesystem paths.", 1) + .RegisterEntry("Added API to check Penumbras enabled state and updates to it.", 1); + + private static void Add5_10_0(Changelog log) + => log.NextVersion("Version 0.5.10.0") + .RegisterEntry("Renamed backup functionality to export functionality.") + .RegisterEntry("A default export directory can now optionally be specified.") + .RegisterEntry("If left blank, exports will still be stored in your mod directory.", 1) + .RegisterEntry("Existing exports corresponding to existing mods will be moved automatically if the export directory is changed.", + 1) + .RegisterEntry("Added buttons to export and import all color set rows at once during material editing.") + .RegisterEntry("Fixed texture import being case sensitive on the extension.") + .RegisterEntry("Fixed special collection selector increasing in size on non-default UI styling.") + .RegisterEntry("Fixed color set rows not importing the dye values during material editing.") + .RegisterEntry("Other miscellaneous small fixes."); + + private static void Add5_9_0(Changelog log) + => log.NextVersion("Version 0.5.9.0") + .RegisterEntry("Special Collections are now split between male and female.") + .RegisterEntry("Fix a bug where the Base and Interface Collection were set to None instead of Default on a fresh install.") + .RegisterEntry("Fix a bug where cutscene actors were not properly reset and could be misidentified across multiple cutscenes.") + .RegisterEntry("TexTools .meta and .rgsp files are now incorporated based on file- and game path extensions."); + + private static void Add5_8_7(Changelog log) + => log.NextVersion("Version 0.5.8.7") + .RegisterEntry("Fixed some problems with metadata reloading and reverting and IMC files. (5.8.1 to 5.8.7).") + .RegisterHighlight( + "If you encounter any issues, please try completely restarting your game after updating (not just relogging), before reporting them.", + 1); + + private static void Add5_8_0(Changelog log) + => log.NextVersion("Version 0.5.8.0") + .RegisterEntry("Added choices what Change Logs are to be displayed. It is recommended to just keep showing all.") + .RegisterEntry("Added an Interface Collection assignment.") + .RegisterEntry("All your UI mods will have to be in the interface collection.", 1) + .RegisterEntry("Files that are categorized as UI files by the game will only check for redirections in this collection.", 1) + .RegisterHighlight( + "Migration should have set your currently assigned Base Collection to the Interface Collection, please verify that.", 1) + .RegisterEntry("New API / IPC for the Interface Collection added.", 1) + .RegisterHighlight("API / IPC consumers should verify whether they need to change resolving to the new collection.", 1) + .RegisterHighlight( + "If other plugins are not using your interface collection yet, you can just keep Interface and Base the same collection for the time being.") + .RegisterEntry( + "Mods can now have default settings for each option group, that are shown while the mod is unconfigured and taken as initial values when configured.") + .RegisterEntry("Default values are set when importing .ttmps from their default values, and can be changed in the Edit Mod tab.", + 1) + .RegisterEntry("Files that the game loads super early should now be replaceable correctly via base or interface collection.") + .RegisterEntry( + "The 1.0 neck tattoo file should now be replaceable, even in character collections. You can also replace the transparent texture used instead. (This was ugly.)") + .RegisterEntry("Continued Work on the Texture Import/Export Tab:") + .RegisterEntry("Should work with lot more texture types for .dds and .tex files, most notably BC7 compression.", 1) + .RegisterEntry("Supports saving .tex and .dds files in multiple texture types and generating MipMaps for them.", 1) + .RegisterEntry("Interface reworked a bit, gives more information and the overlay side can be collapsed.", 1) + .RegisterHighlight( + "May contain bugs or missing safeguards. Generally let me know what's missing, ugly, buggy, not working or could be improved. Not really feasible for me to test it all.", + 1) + .RegisterEntry( + "Added buttons for redrawing self or all as well as a tooltip to describe redraw options and a tutorial step for it.") + .RegisterEntry("Collection Selectors now display None at the top if available.") + .RegisterEntry( + "Adding mods via API/IPC will now cause them to incorporate and then delete TexTools .meta and .rgsp files automatically.") + .RegisterEntry("Fixed an issue with Actor 201 using Your Character collections in cutscenes.") + .RegisterEntry("Fixed issues with and improved mod option editing.") + .RegisterEntry( + "Fixed some issues with and improved file redirection editing - you are now informed if you can not add a game path (because it is invalid or already in use).") + .RegisterEntry("Backend optimizations.") + .RegisterEntry("Changed metadata change system again.", 1) + .RegisterEntry("Improved logging efficiency.", 1); + + private static void Add5_7_1(Changelog log) + => log.NextVersion("Version 0.5.7.1") + .RegisterEntry("Fixed the Changelog window not considering UI Scale correctly.") + .RegisterEntry("Reworked Changelog display slightly."); + + private static void Add5_7_0(Changelog log) + => log.NextVersion("Version 0.5.7.0") + .RegisterEntry("Added a Changelog!") + .RegisterEntry("Files in the UI category will no longer be deduplicated for the moment.") + .RegisterHighlight("If you experience UI-related crashes, please re-import your UI mods.", 1) + .RegisterEntry("This is a temporary fix against those not-yet fully understood crashes and may be reworked later.", 1) + .RegisterHighlight( + "There is still a possibility of UI related mods crashing the game, we are still investigating - they behave very weirdly. If you continue to experience crashing, try disabling your UI mods.", + 1) + .RegisterEntry( + "On import, Penumbra will now show files with extensions '.ttmp', '.ttmp2' and '.pmp'. You can still select showing generic archive files.") + .RegisterEntry( + "Penumbra Mod Pack ('.pmp') files are meant to be renames of any of the archive types that could already be imported that contain the necessary Penumbra meta files.", + 1) + .RegisterHighlight( + "If you distribute any mod as an archive specifically for Penumbra, you should change its extension to '.pmp'. Supported base archive types are ZIP, 7-Zip and RAR.", + 1) + .RegisterEntry("Penumbra will now save mod backups with the file extension '.pmp'. They still are regular ZIP files.", 1) + .RegisterEntry( + "Existing backups in your current mod directory should be automatically renamed. If you manage multiple mod directories, you may need to migrate the other ones manually.", + 1) + .RegisterEntry("Fixed assigned collections not working correctly on adventurer plates.") + .RegisterEntry("Fixed a wrongly displayed folder line in some circumstances.") + .RegisterEntry("Fixed crash after deleting mod options.") + .RegisterEntry("Fixed Inspect Window collections not working correctly.") + .RegisterEntry("Made identically named options selectable in mod configuration. Do not name your options identically.") + .RegisterEntry("Added some additional functionality for Mare Synchronos."); + + #endregion + + private (int, ChangeLogDisplayType) ConfigData() + => (_config.LastSeenVersion, _config.ChangeLogDisplayType); + + private void Save(int version, ChangeLogDisplayType type) + { + _config.LastSeenVersion = version; + _config.ChangeLogDisplayType = type; + _config.Save(); + } +} diff --git a/Penumbra/UI/Classes/ItemSwapWindow.cs b/Penumbra/UI/Classes/ItemSwapWindow.cs index c79f46d9..9abeb3f5 100644 --- a/Penumbra/UI/Classes/ItemSwapWindow.cs +++ b/Penumbra/UI/Classes/ItemSwapWindow.cs @@ -290,7 +290,7 @@ public class ItemSwapWindow : IDisposable } catch (Exception e) { - ChatUtil.NotificationMessage($"Could not create new Swap Option:\n{e}", "Error", NotificationType.Error); + Penumbra.ChatService.NotificationMessage($"Could not create new Swap Option:\n{e}", "Error", NotificationType.Error); try { if (optionCreated && _selectedGroup != null) diff --git a/Penumbra/UI/Classes/ModEditWindow.Materials.MtrlTab.cs b/Penumbra/UI/Classes/ModEditWindow.Materials.MtrlTab.cs index 5bb82fb3..24887288 100644 --- a/Penumbra/UI/Classes/ModEditWindow.Materials.MtrlTab.cs +++ b/Penumbra/UI/Classes/ModEditWindow.Materials.MtrlTab.cs @@ -90,7 +90,7 @@ public partial class ModEditWindow LoadedShpkPath = FullPath.Empty; LoadedShpkPathName = string.Empty; AssociatedShpk = null; - ChatUtil.NotificationMessage( $"Could not load {LoadedShpkPath.ToPath()}:\n{e}", "Penumbra Advanced Editing", NotificationType.Error ); + Penumbra.ChatService.NotificationMessage( $"Could not load {LoadedShpkPath.ToPath()}:\n{e}", "Penumbra Advanced Editing", NotificationType.Error ); } Update(); diff --git a/Penumbra/UI/Classes/ModEditWindow.ShaderPackages.cs b/Penumbra/UI/Classes/ModEditWindow.ShaderPackages.cs index 784c9e24..cef13a4c 100644 --- a/Penumbra/UI/Classes/ModEditWindow.ShaderPackages.cs +++ b/Penumbra/UI/Classes/ModEditWindow.ShaderPackages.cs @@ -80,12 +80,12 @@ public partial class ModEditWindow } catch( Exception e ) { - ChatUtil.NotificationMessage( $"Could not export {defaultName}{tab.Extension} to {name}:\n{e.Message}", "Penumbra Advanced Editing", + Penumbra.ChatService.NotificationMessage( $"Could not export {defaultName}{tab.Extension} to {name}:\n{e.Message}", "Penumbra Advanced Editing", NotificationType.Error ); return; } - ChatUtil.NotificationMessage( $"Shader Program Blob {defaultName}{tab.Extension} exported successfully to {Path.GetFileName( name )}", + Penumbra.ChatService.NotificationMessage( $"Shader Program Blob {defaultName}{tab.Extension} exported successfully to {Path.GetFileName( name )}", "Penumbra Advanced Editing", NotificationType.Success ); } ); } @@ -110,7 +110,7 @@ public partial class ModEditWindow } catch( Exception e ) { - ChatUtil.NotificationMessage( $"Could not import {name}:\n{e.Message}", "Penumbra Advanced Editing", NotificationType.Error ); + Penumbra.ChatService.NotificationMessage( $"Could not import {name}:\n{e.Message}", "Penumbra Advanced Editing", NotificationType.Error ); return; } @@ -122,7 +122,7 @@ public partial class ModEditWindow catch( Exception e ) { tab.Shpk.SetInvalid(); - ChatUtil.NotificationMessage( $"Failed to update resources after importing {name}:\n{e.Message}", "Penumbra Advanced Editing", + Penumbra.ChatService.NotificationMessage( $"Failed to update resources after importing {name}:\n{e.Message}", "Penumbra Advanced Editing", NotificationType.Error ); return; } diff --git a/Penumbra/UI/Classes/ModEditWindow.cs b/Penumbra/UI/Classes/ModEditWindow.cs index b1eea353..7c4946a0 100644 --- a/Penumbra/UI/Classes/ModEditWindow.cs +++ b/Penumbra/UI/Classes/ModEditWindow.cs @@ -1,5 +1,4 @@ using System; -using System.IO; using System.Linq; using System.Numerics; using System.Text; @@ -30,31 +29,29 @@ public partial class ModEditWindow : Window, IDisposable private Vector2 _iconSize = Vector2.Zero; private bool _allowReduplicate = false; - public void ChangeMod( Mod mod ) + public void ChangeMod(Mod mod) { - if( mod == _mod ) - { + if (mod == _mod) return; - } _editor?.Dispose(); - _editor = new Editor( mod, mod.Default ); + _editor = new Editor(mod, mod.Default); _mod = mod; SizeConstraints = new WindowSizeConstraints { - MinimumSize = new Vector2( 1240, 600 ), + MinimumSize = new Vector2(1240, 600), MaximumSize = 4000 * Vector2.One, }; _selectedFiles.Clear(); _modelTab.Reset(); _materialTab.Reset(); _shaderPackageTab.Reset(); - _swapWindow.UpdateMod( mod, Penumbra.CollectionManager.Current[ mod.Index ].Settings ); + _swapWindow.UpdateMod(mod, Penumbra.CollectionManager.Current[mod.Index].Settings); } - public void ChangeOption( ISubMod? subMod ) - => _editor?.SetSubMod( subMod ); + public void ChangeOption(ISubMod? subMod) + => _editor?.SetSubMod(subMod); public void UpdateModels() => _editor?.ScanModels(); @@ -64,71 +61,53 @@ public partial class ModEditWindow : Window, IDisposable public override void PreDraw() { - using var performance = Penumbra.Performance.Measure( PerformanceType.UiAdvancedWindow ); + using var performance = Penumbra.Performance.Measure(PerformanceType.UiAdvancedWindow); - var sb = new StringBuilder( 256 ); + var sb = new StringBuilder(256); var redirections = 0; var unused = 0; - var size = _editor!.AvailableFiles.Sum( f => + var size = _editor!.AvailableFiles.Sum(f => { - if( f.SubModUsage.Count > 0 ) - { + if (f.SubModUsage.Count > 0) redirections += f.SubModUsage.Count; - } else - { ++unused; - } return f.FileSize; - } ); + }); 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 ); - if( subMods > 1 ) - { - sb.Append( $" | {subMods} Options" ); - } + }); + sb.Append(_mod!.Name); + if (subMods > 1) + sb.Append($" | {subMods} Options"); - if( size > 0 ) - { - sb.Append( $" | {_editor.AvailableFiles.Count} Files ({Functions.HumanReadableSize( size )})" ); - } + if (size > 0) + sb.Append($" | {_editor.AvailableFiles.Count} Files ({Functions.HumanReadableSize(size)})"); - if( unused > 0 ) - { - sb.Append( $" | {unused} Unused Files" ); - } + if (unused > 0) + sb.Append($" | {unused} Unused Files"); - if( _editor.MissingFiles.Count > 0 ) - { - sb.Append( $" | {_editor.MissingFiles.Count} Missing Files" ); - } + if (_editor.MissingFiles.Count > 0) + sb.Append($" | {_editor.MissingFiles.Count} Missing Files"); - if( redirections > 0 ) - { - sb.Append( $" | {redirections} Redirections" ); - } + if (redirections > 0) + sb.Append($" | {redirections} Redirections"); - if( manipulations > 0 ) - { - sb.Append( $" | {manipulations} Manipulations" ); - } + if (manipulations > 0) + sb.Append($" | {manipulations} Manipulations"); - if( swaps > 0 ) - { - sb.Append( $" | {swaps} Swaps" ); - } + if (swaps > 0) + sb.Append($" | {swaps} Swaps"); _allowReduplicate = redirections != _editor.AvailableFiles.Count || _editor.MissingFiles.Count > 0; - sb.Append( WindowBaseLabel ); + sb.Append(WindowBaseLabel); WindowName = sb.ToString(); } @@ -140,15 +119,13 @@ public partial class ModEditWindow : Window, IDisposable public override void Draw() { - using var performance = Penumbra.Performance.Measure( PerformanceType.UiAdvancedWindow ); + using var performance = Penumbra.Performance.Measure(PerformanceType.UiAdvancedWindow); - using var tabBar = ImRaii.TabBar( "##tabs" ); - if( !tabBar ) - { + using var tabBar = ImRaii.TabBar("##tabs"); + if (!tabBar) return; - } - _iconSize = new Vector2( ImGui.GetFrameHeight() ); + _iconSize = new Vector2(ImGui.GetFrameHeight()); DrawFileTab(); DrawMetaTab(); DrawSwapTab(); @@ -169,46 +146,40 @@ public partial class ModEditWindow : Window, IDisposable private static string _materialSuffixTo = string.Empty; private static GenderRace _raceCode = GenderRace.Unknown; - private static string RaceCodeName( GenderRace raceCode ) + private static string RaceCodeName(GenderRace raceCode) { - if( raceCode == GenderRace.Unknown ) - { + if (raceCode == GenderRace.Unknown) return "All Races and Genders"; - } var (gender, race) = raceCode.Split(); return $"({raceCode.ToRaceCode()}) {race.ToName()} {gender.ToName()} "; } - private static void DrawRaceCodeCombo( Vector2 buttonSize ) + private static void DrawRaceCodeCombo(Vector2 buttonSize) { - ImGui.SetNextItemWidth( buttonSize.X ); - using var combo = ImRaii.Combo( "##RaceCode", RaceCodeName( _raceCode ) ); - if( !combo ) - { + ImGui.SetNextItemWidth(buttonSize.X); + using var combo = ImRaii.Combo("##RaceCode", RaceCodeName(_raceCode)); + if (!combo) return; - } - foreach( var raceCode in Enum.GetValues< GenderRace >() ) + foreach (var raceCode in Enum.GetValues()) { - if( ImGui.Selectable( RaceCodeName( raceCode ), _raceCode == raceCode ) ) - { + if (ImGui.Selectable(RaceCodeName(raceCode), _raceCode == raceCode)) _raceCode = raceCode; - } } } - public static void Draw( Editor editor, Vector2 buttonSize ) + public static void Draw(Editor editor, Vector2 buttonSize) { - DrawRaceCodeCombo( buttonSize ); + DrawRaceCodeCombo(buttonSize); ImGui.SameLine(); - ImGui.SetNextItemWidth( buttonSize.X ); - ImGui.InputTextWithHint( "##suffixFrom", "From...", ref _materialSuffixFrom, 32 ); + ImGui.SetNextItemWidth(buttonSize.X); + ImGui.InputTextWithHint("##suffixFrom", "From...", ref _materialSuffixFrom, 32); ImGui.SameLine(); - ImGui.SetNextItemWidth( buttonSize.X ); - ImGui.InputTextWithHint( "##suffixTo", "To...", ref _materialSuffixTo, 32 ); + ImGui.SetNextItemWidth(buttonSize.X); + ImGui.InputTextWithHint("##suffixTo", "To...", ref _materialSuffixTo, 32); ImGui.SameLine(); - var disabled = !Editor.ValidString( _materialSuffixTo ); + var disabled = !Editor.ValidString(_materialSuffixTo); var tt = _materialSuffixTo.Length == 0 ? "Please enter a target suffix." : _materialSuffixFrom == _materialSuffixTo @@ -222,181 +193,149 @@ public partial class ModEditWindow : Window, IDisposable : _raceCode == GenderRace.Unknown ? $"Convert all skin material suffices that are currently '{_materialSuffixFrom}' to '{_materialSuffixTo}'." : $"Convert all skin material suffices for the given race code that are currently '{_materialSuffixFrom}' to '{_materialSuffixTo}'."; - if( ImGuiUtil.DrawDisabledButton( "Change Material Suffix", buttonSize, tt, disabled ) ) - { - editor.ReplaceAllMaterials( _materialSuffixTo, _materialSuffixFrom, _raceCode ); - } + if (ImGuiUtil.DrawDisabledButton("Change Material Suffix", buttonSize, tt, disabled)) + editor.ReplaceAllMaterials(_materialSuffixTo, _materialSuffixFrom, _raceCode); - var anyChanges = editor.ModelFiles.Any( m => m.Changed ); - if( ImGuiUtil.DrawDisabledButton( "Save All Changes", buttonSize, - anyChanges ? "Irreversibly rewrites all currently applied changes to model files." : "No changes made yet.", !anyChanges ) ) - { + var anyChanges = editor.ModelFiles.Any(m => m.Changed); + if (ImGuiUtil.DrawDisabledButton("Save All Changes", buttonSize, + anyChanges ? "Irreversibly rewrites all currently applied changes to model files." : "No changes made yet.", !anyChanges)) editor.SaveAllModels(); - } ImGui.SameLine(); - if( ImGuiUtil.DrawDisabledButton( "Revert All Changes", buttonSize, - anyChanges ? "Revert all currently made and unsaved changes." : "No changes made yet.", !anyChanges ) ) - { + if (ImGuiUtil.DrawDisabledButton("Revert All Changes", buttonSize, + anyChanges ? "Revert all currently made and unsaved changes." : "No changes made yet.", !anyChanges)) editor.RestoreAllModels(); - } ImGui.SameLine(); ImGuiComponents.HelpMarker( "Model files refer to the skin material they should use. This skin material is always the same, but modders have started using different suffices to differentiate between body types.\n" + "This option allows you to switch the suffix of all model files to another. This changes the files, so you do this on your own risk.\n" - + "If you do not know what the currently used suffix of this mod is, you can leave 'From' blank and it will replace all suffices with 'To', instead of only the matching ones." ); + + "If you do not know what the currently used suffix of this mod is, you can leave 'From' blank and it will replace all suffices with 'To', instead of only the matching ones."); } } private void DrawMissingFilesTab() { - if( _editor!.MissingFiles.Count == 0 ) - { + if (_editor!.MissingFiles.Count == 0) return; - } - using var tab = ImRaii.TabItem( "Missing Files" ); - if( !tab ) - { + using var tab = ImRaii.TabItem("Missing Files"); + if (!tab) return; - } ImGui.NewLine(); - if( ImGui.Button( "Remove Missing Files from Mod" ) ) - { + if (ImGui.Button("Remove Missing Files from Mod")) _editor.RemoveMissingPaths(); - } - using var child = ImRaii.Child( "##unusedFiles", -Vector2.One, true ); - if( !child ) - { + using var child = ImRaii.Child("##unusedFiles", -Vector2.One, true); + if (!child) return; - } - using var table = ImRaii.Table( "##missingFiles", 1, ImGuiTableFlags.RowBg, -Vector2.One ); - if( !table ) - { + using var table = ImRaii.Table("##missingFiles", 1, ImGuiTableFlags.RowBg, -Vector2.One); + if (!table) return; - } - foreach( var path in _editor.MissingFiles ) + foreach (var path in _editor.MissingFiles) { ImGui.TableNextColumn(); - ImGui.TextUnformatted( path.FullName ); + ImGui.TextUnformatted(path.FullName); } } private void DrawDuplicatesTab() { - using var tab = ImRaii.TabItem( "Duplicates" ); - if( !tab ) - { + using var tab = ImRaii.TabItem("Duplicates"); + if (!tab) return; - } var buttonText = _editor!.DuplicatesFinished ? "Scan for Duplicates###ScanButton" : "Scanning for Duplicates...###ScanButton"; - if( ImGuiUtil.DrawDisabledButton( buttonText, Vector2.Zero, "Search for identical files in this mod. This may take a while.", - !_editor.DuplicatesFinished ) ) - { + if (ImGuiUtil.DrawDisabledButton(buttonText, Vector2.Zero, "Search for identical files in this mod. This may take a while.", + !_editor.DuplicatesFinished)) _editor.StartDuplicateCheck(); - } - const string desc = "Tries to create a unique copy of a file for every game path manipulated and put them in [Groupname]/[Optionname]/[GamePath] order.\n" + const string desc = + "Tries to create a unique copy of a file for every game path manipulated and put them in [Groupname]/[Optionname]/[GamePath] order.\n" + "This will also delete all unused files and directories if it succeeds.\n" + "Care was taken that a failure should not destroy the mod but revert to its original state, but you use this at your own risk anyway."; var modifier = Penumbra.Config.DeleteModModifier.IsActive(); - var tt = _allowReduplicate ? desc : modifier ? desc : desc + $"\n\nNo duplicates detected! Hold {Penumbra.Config.DeleteModModifier} to force normalization anyway."; + var tt = _allowReduplicate ? desc : + modifier ? desc : desc + $"\n\nNo duplicates detected! Hold {Penumbra.Config.DeleteModModifier} to force normalization anyway."; - if( ImGuiUtil.DrawDisabledButton( "Re-Duplicate and Normalize Mod", Vector2.Zero, tt, !_allowReduplicate && !modifier ) ) + if (ImGuiUtil.DrawDisabledButton("Re-Duplicate and Normalize Mod", Vector2.Zero, tt, !_allowReduplicate && !modifier)) { - _mod!.Normalize( Penumbra.ModManager ); + _mod!.Normalize(Penumbra.ModManager); _editor.RevertFiles(); } - if( !_editor.DuplicatesFinished ) + if (!_editor.DuplicatesFinished) { ImGui.SameLine(); - if( ImGui.Button( "Cancel" ) ) - { + if (ImGui.Button("Cancel")) _editor.Cancel(); - } return; } - if( _editor.Duplicates.Count == 0 ) + if (_editor.Duplicates.Count == 0) { ImGui.NewLine(); - ImGui.TextUnformatted( "No duplicates found." ); + ImGui.TextUnformatted("No duplicates found."); return; } - if( ImGui.Button( "Delete and Redirect Duplicates" ) ) - { + if (ImGui.Button("Delete and Redirect Duplicates")) _editor.DeleteDuplicates(); - } - if( _editor.SavedSpace > 0 ) + if (_editor.SavedSpace > 0) { ImGui.SameLine(); - ImGui.TextUnformatted( $"Frees up {Functions.HumanReadableSize( _editor.SavedSpace )} from your hard drive." ); + ImGui.TextUnformatted($"Frees up {Functions.HumanReadableSize(_editor.SavedSpace)} from your hard drive."); } - using var child = ImRaii.Child( "##duptable", -Vector2.One, true ); - if( !child ) - { + using var child = ImRaii.Child("##duptable", -Vector2.One, true); + if (!child) return; - } - using var table = ImRaii.Table( "##duplicates", 3, ImGuiTableFlags.RowBg | ImGuiTableFlags.SizingFixedFit, -Vector2.One ); - if( !table ) - { + using var table = ImRaii.Table("##duplicates", 3, ImGuiTableFlags.RowBg | ImGuiTableFlags.SizingFixedFit, -Vector2.One); + if (!table) return; - } - var width = ImGui.CalcTextSize( "NNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNN " ).X; - ImGui.TableSetupColumn( "file", ImGuiTableColumnFlags.WidthStretch ); - ImGui.TableSetupColumn( "size", ImGuiTableColumnFlags.WidthFixed, ImGui.CalcTextSize( "NNN.NNN " ).X ); - ImGui.TableSetupColumn( "hash", ImGuiTableColumnFlags.WidthFixed, - ImGui.GetWindowWidth() > 2 * width ? width : ImGui.CalcTextSize( "NNNNNNNN... " ).X ); - foreach( var (set, size, hash) in _editor.Duplicates.Where( s => s.Paths.Length > 1 ) ) + var width = ImGui.CalcTextSize("NNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNN ").X; + ImGui.TableSetupColumn("file", ImGuiTableColumnFlags.WidthStretch); + ImGui.TableSetupColumn("size", ImGuiTableColumnFlags.WidthFixed, ImGui.CalcTextSize("NNN.NNN ").X); + ImGui.TableSetupColumn("hash", ImGuiTableColumnFlags.WidthFixed, + ImGui.GetWindowWidth() > 2 * width ? width : ImGui.CalcTextSize("NNNNNNNN... ").X); + foreach (var (set, size, hash) in _editor.Duplicates.Where(s => s.Paths.Length > 1)) { ImGui.TableNextColumn(); - using var tree = ImRaii.TreeNode( set[ 0 ].FullName[ ( _mod!.ModPath.FullName.Length + 1 ).. ], - ImGuiTreeNodeFlags.NoTreePushOnOpen ); + using var tree = ImRaii.TreeNode(set[0].FullName[(_mod!.ModPath.FullName.Length + 1)..], + ImGuiTreeNodeFlags.NoTreePushOnOpen); ImGui.TableNextColumn(); - ImGuiUtil.RightAlign( Functions.HumanReadableSize( size ) ); + ImGuiUtil.RightAlign(Functions.HumanReadableSize(size)); ImGui.TableNextColumn(); - using( var _ = ImRaii.PushFont( UiBuilder.MonoFont ) ) + using (var _ = ImRaii.PushFont(UiBuilder.MonoFont)) { - if( ImGui.GetWindowWidth() > 2 * width ) - { - ImGuiUtil.RightAlign( string.Concat( hash.Select( b => b.ToString( "X2" ) ) ) ); - } + if (ImGui.GetWindowWidth() > 2 * width) + ImGuiUtil.RightAlign(string.Concat(hash.Select(b => b.ToString("X2")))); else - { - ImGuiUtil.RightAlign( string.Concat( hash.Take( 4 ).Select( b => b.ToString( "X2" ) ) ) + "..." ); - } + ImGuiUtil.RightAlign(string.Concat(hash.Take(4).Select(b => b.ToString("X2"))) + "..."); } - if( !tree ) - { + if (!tree) continue; - } using var indent = ImRaii.PushIndent(); - foreach( var duplicate in set.Skip( 1 ) ) + foreach (var duplicate in set.Skip(1)) { ImGui.TableNextColumn(); - ImGui.TableSetBgColor( ImGuiTableBgTarget.CellBg, Colors.RedTableBgTint ); - using var node = ImRaii.TreeNode( duplicate.FullName[ ( _mod!.ModPath.FullName.Length + 1 ).. ], ImGuiTreeNodeFlags.Leaf ); + ImGui.TableSetBgColor(ImGuiTableBgTarget.CellBg, Colors.RedTableBgTint); + using var node = ImRaii.TreeNode(duplicate.FullName[(_mod!.ModPath.FullName.Length + 1)..], ImGuiTreeNodeFlags.Leaf); ImGui.TableNextColumn(); - ImGui.TableSetBgColor( ImGuiTableBgTarget.CellBg, Colors.RedTableBgTint ); + ImGui.TableSetBgColor(ImGuiTableBgTarget.CellBg, Colors.RedTableBgTint); ImGui.TableNextColumn(); - ImGui.TableSetBgColor( ImGuiTableBgTarget.CellBg, Colors.RedTableBgTint ); + ImGui.TableSetBgColor(ImGuiTableBgTarget.CellBg, Colors.RedTableBgTint); } } } @@ -404,34 +343,26 @@ public partial class ModEditWindow : Window, IDisposable private void DrawOptionSelectHeader() { const string defaultOption = "Default Option"; - using var style = ImRaii.PushStyle( ImGuiStyleVar.ItemSpacing, Vector2.Zero ).Push( ImGuiStyleVar.FrameRounding, 0 ); - var width = new Vector2( ImGui.GetWindowWidth() / 3, 0 ); - if( ImGuiUtil.DrawDisabledButton( defaultOption, width, "Switch to the default option for the mod.\nThis resets unsaved changes.", - _editor!.CurrentOption.IsDefault ) ) - { - _editor.SetSubMod( _mod!.Default ); - } + using var style = ImRaii.PushStyle(ImGuiStyleVar.ItemSpacing, Vector2.Zero).Push(ImGuiStyleVar.FrameRounding, 0); + var width = new Vector2(ImGui.GetWindowWidth() / 3, 0); + if (ImGuiUtil.DrawDisabledButton(defaultOption, width, "Switch to the default option for the mod.\nThis resets unsaved changes.", + _editor!.CurrentOption.IsDefault)) + _editor.SetSubMod(_mod!.Default); ImGui.SameLine(); - if( ImGuiUtil.DrawDisabledButton( "Refresh Data", width, "Refresh data for the current option.\nThis resets unsaved changes.", false ) ) - { - _editor.SetSubMod( _editor.CurrentOption ); - } + if (ImGuiUtil.DrawDisabledButton("Refresh Data", width, "Refresh data for the current option.\nThis resets unsaved changes.", false)) + _editor.SetSubMod(_editor.CurrentOption); ImGui.SameLine(); - using var combo = ImRaii.Combo( "##optionSelector", _editor.CurrentOption.FullName, ImGuiComboFlags.NoArrowButton ); - if( !combo ) - { + using var combo = ImRaii.Combo("##optionSelector", _editor.CurrentOption.FullName, ImGuiComboFlags.NoArrowButton); + if (!combo) return; - } - foreach( var option in _mod!.AllSubMods ) + foreach (var option in _mod!.AllSubMods) { - if( ImGui.Selectable( option.FullName, option == _editor.CurrentOption ) ) - { - _editor.SetSubMod( option ); - } + if (ImGui.Selectable(option.FullName, option == _editor.CurrentOption)) + _editor.SetSubMod(option); } } @@ -440,100 +371,84 @@ public partial class ModEditWindow : Window, IDisposable private void DrawSwapTab() { - using var tab = ImRaii.TabItem( "File Swaps" ); - if( !tab ) - { + using var tab = ImRaii.TabItem("File Swaps"); + if (!tab) return; - } DrawOptionSelectHeader(); - var setsEqual = _editor!.CurrentSwaps.SetEquals( _editor.CurrentOption.FileSwaps ); + var setsEqual = _editor!.CurrentSwaps.SetEquals(_editor.CurrentOption.FileSwaps); var tt = setsEqual ? "No changes staged." : "Apply the currently staged changes to the option."; ImGui.NewLine(); - if( ImGuiUtil.DrawDisabledButton( "Apply Changes", Vector2.Zero, tt, setsEqual ) ) - { + if (ImGuiUtil.DrawDisabledButton("Apply Changes", Vector2.Zero, tt, setsEqual)) _editor.ApplySwaps(); - } ImGui.SameLine(); tt = setsEqual ? "No changes staged." : "Revert all currently staged changes."; - if( ImGuiUtil.DrawDisabledButton( "Revert Changes", Vector2.Zero, tt, setsEqual ) ) - { + if (ImGuiUtil.DrawDisabledButton("Revert Changes", Vector2.Zero, tt, setsEqual)) _editor.RevertSwaps(); - } - using var child = ImRaii.Child( "##swaps", -Vector2.One, true ); - if( !child ) - { + using var child = ImRaii.Child("##swaps", -Vector2.One, true); + if (!child) return; - } - using var list = ImRaii.Table( "##table", 3, ImGuiTableFlags.RowBg, -Vector2.One ); - if( !list ) - { + using var list = ImRaii.Table("##table", 3, ImGuiTableFlags.RowBg, -Vector2.One); + if (!list) return; - } var idx = 0; var iconSize = ImGui.GetFrameHeight() * Vector2.One; var pathSize = ImGui.GetContentRegionAvail().X / 2 - iconSize.X; - ImGui.TableSetupColumn( "button", ImGuiTableColumnFlags.WidthFixed, iconSize.X ); - ImGui.TableSetupColumn( "source", ImGuiTableColumnFlags.WidthFixed, pathSize ); - ImGui.TableSetupColumn( "value", ImGuiTableColumnFlags.WidthFixed, pathSize ); + ImGui.TableSetupColumn("button", ImGuiTableColumnFlags.WidthFixed, iconSize.X); + ImGui.TableSetupColumn("source", ImGuiTableColumnFlags.WidthFixed, pathSize); + ImGui.TableSetupColumn("value", ImGuiTableColumnFlags.WidthFixed, pathSize); - foreach( var (gamePath, file) in _editor!.CurrentSwaps.ToList() ) + foreach (var (gamePath, file) in _editor!.CurrentSwaps.ToList()) { - using var id = ImRaii.PushId( idx++ ); + using var id = ImRaii.PushId(idx++); ImGui.TableNextColumn(); - if( ImGuiUtil.DrawDisabledButton( FontAwesomeIcon.Trash.ToIconString(), iconSize, "Delete this swap.", false, true ) ) - { - _editor.CurrentSwaps.Remove( gamePath ); - } + if (ImGuiUtil.DrawDisabledButton(FontAwesomeIcon.Trash.ToIconString(), iconSize, "Delete this swap.", false, true)) + _editor.CurrentSwaps.Remove(gamePath); ImGui.TableNextColumn(); var tmp = gamePath.Path.ToString(); - ImGui.SetNextItemWidth( -1 ); - if( ImGui.InputText( "##key", ref tmp, Utf8GamePath.MaxGamePathLength ) - && Utf8GamePath.FromString( tmp, out var path ) - && !_editor.CurrentSwaps.ContainsKey( path ) ) + ImGui.SetNextItemWidth(-1); + if (ImGui.InputText("##key", ref tmp, Utf8GamePath.MaxGamePathLength) + && Utf8GamePath.FromString(tmp, out var path) + && !_editor.CurrentSwaps.ContainsKey(path)) { - _editor.CurrentSwaps.Remove( gamePath ); - if( path.Length > 0 ) - { - _editor.CurrentSwaps[ path ] = file; - } + _editor.CurrentSwaps.Remove(gamePath); + if (path.Length > 0) + _editor.CurrentSwaps[path] = file; } ImGui.TableNextColumn(); tmp = file.FullName; - ImGui.SetNextItemWidth( -1 ); - if( ImGui.InputText( "##value", ref tmp, Utf8GamePath.MaxGamePathLength ) && tmp.Length > 0 ) - { - _editor.CurrentSwaps[ gamePath ] = new FullPath( tmp ); - } + ImGui.SetNextItemWidth(-1); + if (ImGui.InputText("##value", ref tmp, Utf8GamePath.MaxGamePathLength) && tmp.Length > 0) + _editor.CurrentSwaps[gamePath] = new FullPath(tmp); } ImGui.TableNextColumn(); - var addable = Utf8GamePath.FromString( _newSwapKey, out var newPath ) - && newPath.Length > 0 + var addable = Utf8GamePath.FromString(_newSwapKey, out var newPath) + && newPath.Length > 0 && _newSwapValue.Length > 0 - && _newSwapValue != _newSwapKey - && !_editor.CurrentSwaps.ContainsKey( newPath ); - if( ImGuiUtil.DrawDisabledButton( FontAwesomeIcon.Plus.ToIconString(), iconSize, "Add a new file swap to this option.", !addable, - true ) ) + && _newSwapValue != _newSwapKey + && !_editor.CurrentSwaps.ContainsKey(newPath); + if (ImGuiUtil.DrawDisabledButton(FontAwesomeIcon.Plus.ToIconString(), iconSize, "Add a new file swap to this option.", !addable, + true)) { - _editor.CurrentSwaps[ newPath ] = new FullPath( _newSwapValue ); - _newSwapKey = string.Empty; - _newSwapValue = string.Empty; + _editor.CurrentSwaps[newPath] = new FullPath(_newSwapValue); + _newSwapKey = string.Empty; + _newSwapValue = string.Empty; } ImGui.TableNextColumn(); - ImGui.SetNextItemWidth( -1 ); - ImGui.InputTextWithHint( "##swapKey", "Load this file...", ref _newSwapValue, Utf8GamePath.MaxGamePathLength ); + ImGui.SetNextItemWidth(-1); + ImGui.InputTextWithHint("##swapKey", "Load this file...", ref _newSwapValue, Utf8GamePath.MaxGamePathLength); ImGui.TableNextColumn(); - ImGui.SetNextItemWidth( -1 ); - ImGui.InputTextWithHint( "##swapValue", "... instead of this file.", ref _newSwapKey, Utf8GamePath.MaxGamePathLength ); + ImGui.SetNextItemWidth(-1); + ImGui.InputTextWithHint("##swapValue", "... instead of this file.", ref _newSwapKey, Utf8GamePath.MaxGamePathLength); } /// @@ -544,50 +459,44 @@ public partial class ModEditWindow : Window, IDisposable /// If none exists, goes through all options in the currently selected mod (if any) in order of priority and resolves in them. /// If no redirection is found in either of those options, returns the original path. /// - private FullPath FindBestMatch( Utf8GamePath path ) + private FullPath FindBestMatch(Utf8GamePath path) { - var currentFile = Penumbra.CollectionManager.Current.ResolvePath( path ); - if( currentFile != null ) - { + var currentFile = Penumbra.CollectionManager.Current.ResolvePath(path); + if (currentFile != null) return currentFile.Value; - } - 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 ) ) + 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)) { - if( option.Files.TryGetValue( path, out var value ) || option.FileSwaps.TryGetValue( path, out value ) ) - { + if (option.Files.TryGetValue(path, out var value) || option.FileSwaps.TryGetValue(path, out value)) return value; - } } - } - return new FullPath( path ); + return new FullPath(path); } public ModEditWindow(CommunicatorService communicator) - : base( WindowBaseLabel ) - { - _swapWindow = new ItemSwapWindow( communicator ); - _materialTab = new FileEditor< MtrlTab >( "Materials", ".mtrl", - () => _editor?.MtrlFiles ?? Array.Empty< Editor.FileRegistry >(), + : base(WindowBaseLabel) + { + _swapWindow = new ItemSwapWindow(communicator); + _materialTab = new FileEditor("Materials", ".mtrl", + () => _editor?.MtrlFiles ?? Array.Empty(), DrawMaterialPanel, () => _mod?.ModPath.FullName ?? string.Empty, - bytes => new MtrlTab( this, new MtrlFile( bytes ) ) ); - _modelTab = new FileEditor< MdlFile >( "Models", ".mdl", - () => _editor?.MdlFiles ?? Array.Empty< Editor.FileRegistry >(), + bytes => new MtrlTab(this, new MtrlFile(bytes))); + _modelTab = new FileEditor("Models", ".mdl", + () => _editor?.MdlFiles ?? Array.Empty(), DrawModelPanel, () => _mod?.ModPath.FullName ?? string.Empty, - null ); - _shaderPackageTab = new FileEditor< ShpkTab >( "Shader Packages", ".shpk", - () => _editor?.ShpkFiles ?? Array.Empty< Editor.FileRegistry >(), + null); + _shaderPackageTab = new FileEditor("Shader Packages", ".shpk", + () => _editor?.ShpkFiles ?? Array.Empty(), DrawShaderPackagePanel, () => _mod?.ModPath.FullName ?? string.Empty, - null ); - _center = new CombinedTexture( _left, _right ); + null); + _center = new CombinedTexture(_left, _right); } public void Dispose() @@ -598,4 +507,4 @@ public partial class ModEditWindow : Window, IDisposable _center.Dispose(); _swapWindow.Dispose(); } -} \ No newline at end of file +} diff --git a/Penumbra/UI/ConfigWindow.Changelog.cs b/Penumbra/UI/ConfigWindow.Changelog.cs deleted file mode 100644 index bbe0e9ad..00000000 --- a/Penumbra/UI/ConfigWindow.Changelog.cs +++ /dev/null @@ -1,350 +0,0 @@ -using System.Runtime.CompilerServices; -using OtterGui.Widgets; - -namespace Penumbra.UI; - -public partial class ConfigWindow -{ - public const int LastChangelogVersion = 0; - - public static Changelog CreateChangelog() - { - var ret = new Changelog( "Penumbra Changelog", () => ( Penumbra.Config.LastSeenVersion, Penumbra.Config.ChangeLogDisplayType ), - ( version, type ) => - { - Penumbra.Config.LastSeenVersion = version; - Penumbra.Config.ChangeLogDisplayType = type; - Penumbra.Config.Save(); - } ); - - Add5_7_0( ret ); - Add5_7_1( ret ); - Add5_8_0( ret ); - Add5_8_7( ret ); - Add5_9_0( ret ); - Add5_10_0( ret ); - Add5_11_0( ret ); - Add5_11_1( ret ); - Add6_0_0( ret ); - Add6_0_2( ret ); - Add6_0_5( ret ); - Add6_1_0( ret ); - Add6_1_1( ret ); - Add6_2_0( ret ); - Add6_3_0( ret ); - Add6_4_0( ret ); - Add6_5_0( ret ); - Add6_5_2( ret ); - Add6_6_0( ret ); - Add6_6_1( ret ); - return ret; - } - - private static void Add6_6_1( Changelog log ) - => log.NextVersion( "Version 0.6.6.1" ) - .RegisterEntry( "Added an option to make successful chat commands not print their success confirmations to chat." ) - .RegisterEntry( "Fixed an issue with migration of old mods not working anymore (fixes Material UI problems)." ) - .RegisterEntry( "Fixed some issues with using the Assign Current Player and Assign Current Target buttons." ); - - private static void Add6_6_0( Changelog log ) - => log.NextVersion( "Version 0.6.6.0" ) - .RegisterEntry( "Added new Collection Assignment Groups for Children NPC and Elderly NPC. Those take precedence before any non-individual assignments for any NPC using a child- or elderly model respectively." ) - .RegisterEntry( "Added an option to display Single Selection Groups as a group of radio buttons similar to Multi Selection Groups, when the number of available options is below the specified value. Default value is 2." ) - .RegisterEntry( "Added a button in option groups to collapse the option list if it has more than 5 available options." ) - .RegisterEntry( - "Penumbra now circumvents the games inability to read files at paths longer than 260 UTF16 characters and can also deal with generic unicode symbols in paths." ) - .RegisterEntry( "This means that Penumbra should no longer cause issues when files become too long or when there is a non-ASCII character in them.", 1 ) - .RegisterEntry( - "Shorter paths are still better, so restrictions on the root directory have not been relaxed. Mod names should no longer replace non-ASCII symbols on import though.", 1 ) - .RegisterEntry( - "Resource logging has been relegated to its own tab with better filtering. Please do not keep resource logging on arbitrarily or set a low record limit if you do, otherwise this eats a lot of performance and memory after a while." ) - .RegisterEntry( "Added a lot of facilities to edit the shader part of .mtrl files and .shpk files themselves in the Advanced Editing Tab (Thanks Ny and aers)." ) - .RegisterEntry( "Added splitting of Multi Selection Groups with too many options when importing .pmp files or adding mods via IPC." ) - .RegisterEntry( "Discovery, Reloading and Unloading of a specified mod is now possible via HTTP API (Thanks Sebastina)." ) - .RegisterEntry( "Cleaned up the HTTP API somewhat, removed currently useless options." ) - .RegisterEntry( "Fixed an issue when extracting some textures." ) - .RegisterEntry( "Fixed an issue with mannequins inheriting individual assignments for the current player when using ownership." ) - .RegisterEntry( "Fixed an issue with the resolving of .phyb and .sklb files for Item Swaps of head or body items with an EST entry but no unique racial model." ); - - private static void Add6_5_2( Changelog log ) - => log.NextVersion( "Version 0.6.5.2" ) - .RegisterEntry( "Updated for game version 6.31 Hotfix." ) - .RegisterEntry( "Added option-specific descriptions for mods, instead of having just descriptions for groups of options. (Thanks Caraxi!)" ) - .RegisterEntry( "Those are now accurately parsed from TTMPs, too.", 1 ) - .RegisterEntry( "Improved launch times somewhat through parallelization of some tasks." ) - .RegisterEntry( - "Added some performance tracking for start-up durations and for real time data to Release builds. They can be seen and enabled in the Debug tab when Debug Mode is enabled." ) - .RegisterEntry( "Fixed an issue with IMC changes and Mare Synchronos interoperability." ) - .RegisterEntry( "Fixed an issue with housing mannequins crashing the game when resource logging was enabled." ) - .RegisterEntry( "Fixed an issue generating Mip Maps for texture import on Wine." ); - - private static void Add6_5_0( Changelog log ) - => log.NextVersion( "Version 0.6.5.0" ) - .RegisterEntry( "Fixed an issue with Item Swaps not using applied IMC changes in some cases." ) - .RegisterEntry( "Improved error message on texture import when failing to create mip maps (slightly)." ) - .RegisterEntry( "Tried to fix duty party banner identification again, also for the recommendation window this time." ) - .RegisterEntry( "Added batched IPC to improve Mare performance." ); - - private static void Add6_4_0( Changelog log ) - => log.NextVersion( "Version 0.6.4.0" ) - .RegisterEntry( "Fixed an issue with the identification of actors in the duty group portrait." ) - .RegisterEntry( "Fixed some issues with wrongly cached actors and resources." ) - .RegisterEntry( "Fixed animation handling after redraws (notably for PLD idle animations with a shield equipped)." ) - .RegisterEntry( "Fixed an issue with collection listing API skipping one collection." ) - .RegisterEntry( "Fixed an issue with BGM files being sometimes loaded from other collections than the base collection, causing crashes." ) - .RegisterEntry( "Also distinguished file resolving for different file categories (improving performance) and disabled resolving for script files entirely.", 1 ) - .RegisterEntry( "Some miscellaneous backend changes due to the Glamourer rework." ); - - private static void Add6_3_0( Changelog log ) - => log.NextVersion( "Version 0.6.3.0" ) - .RegisterEntry( "Add an Assign Current Target button for individual assignments" ) - .RegisterEntry( "Try identifying all banner actors correctly for PvE duties, Crystalline Conflict and Mahjong." ) - .RegisterEntry( "Please let me know if this does not work for anything except identical twins.", 1 ) - .RegisterEntry( "Add handling for the 3 new screen actors (now 8 total, for PvE dutie portraits)." ) - .RegisterEntry( "Update the Battle NPC name database for 6.3." ) - .RegisterEntry( "Added API/IPC functions to obtain or set group or individual collections." ) - .RegisterEntry( "Maybe fix a problem with textures sometimes not loading from their corresponding collection." ) - .RegisterEntry( "Another try to fix a problem with the collection selectors breaking state." ) - .RegisterEntry( "Fix a problem identifying companions." ) - .RegisterEntry( "Fix a problem when deleting collections assigned to Groups." ) - .RegisterEntry( "Fix a problem when using the Assign Currently Played Character button and then logging onto a different character without restarting in between." ) - .RegisterEntry( "Some miscellaneous backend changes." ); - - private static void Add6_2_0( Changelog log ) - => log.NextVersion( "Version 0.6.2.0" ) - .RegisterEntry( "Update Penumbra for .net7, Dalamud API 8 and patch 6.3." ) - .RegisterEntry( "Add a Bulktag chat command to toggle all mods with specific tags. (by SoyaX)" ) - .RegisterEntry( "Add placeholder options for setting individual collections via chat command." ) - .RegisterEntry( "Add toggles to swap left and/or right rings separately for ring item swap." ) - .RegisterEntry( "Add handling for looping sound effects caused by animations in non-base collections." ) - .RegisterEntry( "Add an option to not use any mods at all in the Inspect/Try-On window." ) - .RegisterEntry( "Add handling for Mahjong actors." ) - .RegisterEntry( "Improve hint text for File Swaps in Advanced Editing, also inverted file swap display order." ) - .RegisterEntry( "Fix a problem where the collection selectors could get desynchronized after adding or deleting collections." ) - .RegisterEntry( "Fix a problem that could cause setting state to get desynchronized." ) - .RegisterEntry( "Fix an oversight where some special screen actors did not actually respect the settings made for them." ) - .RegisterEntry( "Add collection and associated game object to Full Resource Logging." ) - .RegisterEntry( "Add performance tracking for DEBUG-compiled versions (i.e. testing only)." ) - .RegisterEntry( "Add some information to .mdl display and fix not respecting padding when reading them. (0.6.1.3)" ) - .RegisterEntry( "Fix association of some vfx game objects. (0.6.1.3)" ) - .RegisterEntry( "Stop forcing AVFX files to load synchronously. (0.6.1.3)" ) - .RegisterEntry( "Fix an issue when incorporating deduplicated meta files. (0.6.1.2)" ); - - private static void Add6_1_1( Changelog log ) - => log.NextVersion( "Version 0.6.1.1" ) - .RegisterEntry( "Added a toggle to use all the effective changes from the entire currently selected collection for swaps, instead of the selected mod." ) - .RegisterEntry( "Fix using equipment paths for accessory swaps and thus accessory swaps not working at all" ) - .RegisterEntry( "Fix issues with swaps with gender-locked gear where the models for the other gender do not exist." ) - .RegisterEntry( "Fix swapping universal hairstyles for midlanders breaking them for other races." ) - .RegisterEntry( "Add some actual error messages on failure to create item swaps." ) - .RegisterEntry( "Fix warnings about more than one affected item appearing for single items." ); - - private static void Add6_1_0( Changelog log ) - => log.NextVersion( "Version 0.6.1.0 (Happy New Year! Edition)" ) - .RegisterEntry( "Add a prototype for Item Swapping." ) - .RegisterEntry( "A new tab in Advanced Editing.", 1 ) - .RegisterEntry( "Swapping of Hair, Tail, Ears, Equipment and Accessories is supported. Weapons and Faces may be coming.", 1 ) - .RegisterEntry( "The manipulations currently in use by the selected mod with its currents settings (ignoring enabled state)" - + " should be used when creating the swap, but you can also just swap unmodded things.", 1 ) - .RegisterEntry( "You can write a swap to a new mod, or to a new option in the currently selected mod.", 1 ) - .RegisterEntry( "The swaps are not heavily tested yet, and may also be not perfectly efficient. Please leave feedback.", 1 ) - .RegisterEntry( "More detailed help or explanations will be added later.", 1 ) - .RegisterEntry( "Heavily improve Chat Commands. Use /penumbra help for more information." ) - .RegisterEntry( "Penumbra now considers meta manipulations for Changed Items." ) - .RegisterEntry( "Penumbra now tries to associate battle voices to specific actors, so that they work in collections." ) - .RegisterEntry( "Heavily improve .atex and .avfx handling, Penumbra can now associate VFX to specific actors far better, including ground effects." ) - .RegisterEntry( "Improve some file handling for Mare-Interaction." ) - .RegisterEntry( "Add Equipment Slots to Demihuman IMC Edits." ) - .RegisterEntry( "Add a toggle to keep metadata edits that apply the default value (and thus do not really change anything) on import from TexTools .meta files." ) - .RegisterEntry( "Add an option to directly change the 'Wait For Plugins To Load'-Dalamud Option from Penumbra." ) - .RegisterEntry( "Add API to copy mod settings from one mod to another." ) - .RegisterEntry( "Fix a problem where creating individual collections did not trigger events." ) - .RegisterEntry( "Add a Hack to support Anamnesis Redrawing better. (0.6.0.6)" ) - .RegisterEntry( "Fix another problem with the aesthetician. (0.6.0.6)" ) - .RegisterEntry( "Fix a problem with the export directory not being respected. (0.6.0.6)" ); - - private static void Add6_0_5( Changelog log ) - => log.NextVersion( "Version 0.6.0.5" ) - .RegisterEntry( "Allow hyphen as last character in player and retainer names." ) - .RegisterEntry( "Fix various bugs with ownership and GPose." ) - .RegisterEntry( "Fix collection selectors not updating for new or deleted collections in some cases." ) - .RegisterEntry( "Fix Chocobos not being recognized correctly." ) - .RegisterEntry( "Fix some problems with UI actors." ) - .RegisterEntry( "Fix problems with aesthetician again." ); - - private static void Add6_0_2( Changelog log ) - => log.NextVersion( "Version 0.6.0.2" ) - .RegisterEntry( "Let Bell Retainer collections apply to retainer-named mannequins." ) - .RegisterEntry( "Added a few informations to a help marker for new individual assignments." ) - .RegisterEntry( "Fix bug with Demi Human IMC paths." ) - .RegisterEntry( "Fix Yourself collection not applying to UI actors." ) - .RegisterEntry( "Fix Yourself collection not applying during aesthetician." ); - - private static void Add6_0_0( Changelog log ) - => log.NextVersion( "Version 0.6.0.0" ) - .RegisterEntry( "Revamped Individual Collections:" ) - .RegisterEntry( "You can now specify individual collections for players (by name) of specific worlds or any world.", 1 ) - .RegisterEntry( "You can also specify NPCs (by grouped name and type of NPC), and owned NPCs (by specifying an NPC and a Player).", 1 ) - .RegisterHighlight( - "Migration should move all current names that correspond to NPCs to the appropriate NPC group and all names that can be valid Player names to a Player of any world.", - 1 ) - .RegisterHighlight( - "Please look through your Individual Collections to verify everything migrated correctly and corresponds to the game object you want. You might also want to change the 'Player (Any World)' collections to your specific homeworld.", - 1 ) - .RegisterEntry( "You can also manually sort your Individual Collections by drag and drop now.", 1 ) - .RegisterEntry( "This new system is a pretty big rework, so please report any discrepancies or bugs you find.", 1 ) - .RegisterEntry( "These changes made the specific ownership settings for Retainers and for preferring named over ownership obsolete.", 1 ) - .RegisterEntry( "General ownership can still be toggled and should apply in order of: Owned NPC > Owner (if enabled) > General NPC.", 1 ) - .RegisterEntry( "Added NPC Model Parsing, changes in NPC models should now display the names of the changed game objects for most NPCs." ) - .RegisterEntry( "Changed Items now also display variant or subtype in addition to the model set ID where applicable." ) - .RegisterEntry( "Collection selectors can now be filtered by name." ) - .RegisterEntry( "Try to use Unicode normalization before replacing invalid path symbols on import for somewhat nicer paths." ) - .RegisterEntry( "Improved interface for group settings (minimally)." ) - .RegisterEntry( "New Special or Individual Assignments now default to your current Base assignment instead of None." ) - .RegisterEntry( "Improved Support Info somewhat." ) - .RegisterEntry( "Added Dye Previews for in-game dyes and dyeing templates in Material Editing." ) - .RegisterEntry( "Colorset Editing now allows for negative values in all cases." ) - .RegisterEntry( "Added Export buttons to .mdl and .mtrl previews in Advanced Editing." ) - .RegisterEntry( "File Selection in the .mdl and .mtrl tabs now shows one associated game path by default and all on hover." ) - .RegisterEntry( - "Added the option to reduplicate and normalize a mod, restoring all duplicates and moving the files to appropriate folders. (Duplicates Tab in Advanced Editing)" ) - .RegisterEntry( "Added an option to re-export metadata changes to TexTools-typed .meta and .rgsp files. (Meta-Manipulations Tab in Advanced Editing)" ) - .RegisterEntry( "Fixed several bugs with the incorporation of meta changes when not done during TTMP import." ) - .RegisterEntry( "Fixed a bug with RSP changes on non-base collections not applying correctly in some cases." ) - .RegisterEntry( "Fixed a bug when dragging options during mod edit." ) - .RegisterEntry( "Fixed a bug where sometimes the valid folder check caused issues." ) - .RegisterEntry( "Fixed a bug where collections with inheritances were newly saved on every load." ) - .RegisterEntry( "Fixed a bug where the /penumbra enable/disable command displayed the wrong message (functionality unchanged)." ) - .RegisterEntry( "Mods without names or invalid mod folders are now warnings instead of errors." ) - .RegisterEntry( "Added IPC events for mod deletion, addition or moves, and resolving based on game objects." ) - .RegisterEntry( "Prevent a bug that allowed IPC to add Mods from outside the Penumbra root folder." ) - .RegisterEntry( "A lot of big backend changes." ); - - private static void Add5_11_1( Changelog log ) - => log.NextVersion( "Version 0.5.11.1" ) - .RegisterEntry( - "The 0.5.11.0 Update exposed an issue in Penumbras file-saving scheme that rarely could cause some, most or even all of your mods to lose their group information." ) - .RegisterEntry( "If this has happened to you, you will need to reimport affected mods, or manually restore their groups. I am very sorry for that.", 1 ) - .RegisterEntry( - "I believe the problem is fixed with 0.5.11.1, but I can not be sure since it would occur only rarely. For the same reason, a testing build would not help (as it also did not with 0.5.11.0 itself).", - 1 ) - .RegisterHighlight( "If you do encounter this or similar problems in 0.5.11.1, please immediately let me know in Discord so I can revert the update again.", 1 ); - - private static void Add5_11_0( Changelog log ) - => log.NextVersion( "Version 0.5.11.0" ) - .RegisterEntry( - "Added local data storage for mods in the plugin config folder. This information is not exported together with your mod, but not dependent on collections." ) - .RegisterEntry( "Moved the import date from mod metadata to local data.", 1 ) - .RegisterEntry( "Added Favorites. You can declare mods as favorites and filter for them.", 1 ) - .RegisterEntry( "Added Local Tags. You can apply custom Tags to mods and filter for them.", 1 ) - .RegisterEntry( "Added Mod Tags. Mod Creators (and the Edit Mod tab) can set tags that are stored in the mod meta data and are thus exported." ) - .RegisterEntry( "Add backface and transparency toggles to .mtrl editing, as well as a info section." ) - .RegisterEntry( "Meta Manipulation editing now highlights if the selected ID is 0 or 1." ) - .RegisterEntry( "Fixed a bug when manually adding EQP or EQDP entries to Mods." ) - .RegisterEntry( "Updated some tooltips and hints." ) - .RegisterEntry( "Improved handling of IMC exception problems." ) - .RegisterEntry( "Fixed a bug with misidentification of equipment decals." ) - .RegisterEntry( "Character collections can now be set via chat command, too. (/penumbra collection character | )" ) - .RegisterEntry( "Backend changes regarding API/IPC, consumers can but do not need to use the Penumbra.Api library as a submodule." ) - .RegisterEntry( "Added API to delete mods and read and set their pseudo-filesystem paths.", 1 ) - .RegisterEntry( "Added API to check Penumbras enabled state and updates to it.", 1 ); - - private static void Add5_10_0( Changelog log ) - => log.NextVersion( "Version 0.5.10.0" ) - .RegisterEntry( "Renamed backup functionality to export functionality." ) - .RegisterEntry( "A default export directory can now optionally be specified." ) - .RegisterEntry( "If left blank, exports will still be stored in your mod directory.", 1 ) - .RegisterEntry( "Existing exports corresponding to existing mods will be moved automatically if the export directory is changed.", - 1 ) - .RegisterEntry( "Added buttons to export and import all color set rows at once during material editing." ) - .RegisterEntry( "Fixed texture import being case sensitive on the extension." ) - .RegisterEntry( "Fixed special collection selector increasing in size on non-default UI styling." ) - .RegisterEntry( "Fixed color set rows not importing the dye values during material editing." ) - .RegisterEntry( "Other miscellaneous small fixes." ); - - private static void Add5_9_0( Changelog log ) - => log.NextVersion( "Version 0.5.9.0" ) - .RegisterEntry( "Special Collections are now split between male and female." ) - .RegisterEntry( "Fix a bug where the Base and Interface Collection were set to None instead of Default on a fresh install." ) - .RegisterEntry( "Fix a bug where cutscene actors were not properly reset and could be misidentified across multiple cutscenes." ) - .RegisterEntry( "TexTools .meta and .rgsp files are now incorporated based on file- and game path extensions." ); - - private static void Add5_8_7( Changelog log ) - => log.NextVersion( "Version 0.5.8.7" ) - .RegisterEntry( "Fixed some problems with metadata reloading and reverting and IMC files. (5.8.1 to 5.8.7)." ) - .RegisterHighlight( - "If you encounter any issues, please try completely restarting your game after updating (not just relogging), before reporting them.", - 1 ); - - private static void Add5_8_0( Changelog log ) - => log.NextVersion( "Version 0.5.8.0" ) - .RegisterEntry( "Added choices what Change Logs are to be displayed. It is recommended to just keep showing all." ) - .RegisterEntry( "Added an Interface Collection assignment." ) - .RegisterEntry( "All your UI mods will have to be in the interface collection.", 1 ) - .RegisterEntry( "Files that are categorized as UI files by the game will only check for redirections in this collection.", 1 ) - .RegisterHighlight( - "Migration should have set your currently assigned Base Collection to the Interface Collection, please verify that.", 1 ) - .RegisterEntry( "New API / IPC for the Interface Collection added.", 1 ) - .RegisterHighlight( "API / IPC consumers should verify whether they need to change resolving to the new collection.", 1 ) - .RegisterHighlight( - "If other plugins are not using your interface collection yet, you can just keep Interface and Base the same collection for the time being." ) - .RegisterEntry( - "Mods can now have default settings for each option group, that are shown while the mod is unconfigured and taken as initial values when configured." ) - .RegisterEntry( "Default values are set when importing .ttmps from their default values, and can be changed in the Edit Mod tab.", - 1 ) - .RegisterEntry( "Files that the game loads super early should now be replaceable correctly via base or interface collection." ) - .RegisterEntry( - "The 1.0 neck tattoo file should now be replaceable, even in character collections. You can also replace the transparent texture used instead. (This was ugly.)" ) - .RegisterEntry( "Continued Work on the Texture Import/Export Tab:" ) - .RegisterEntry( "Should work with lot more texture types for .dds and .tex files, most notably BC7 compression.", 1 ) - .RegisterEntry( "Supports saving .tex and .dds files in multiple texture types and generating MipMaps for them.", 1 ) - .RegisterEntry( "Interface reworked a bit, gives more information and the overlay side can be collapsed.", 1 ) - .RegisterHighlight( - "May contain bugs or missing safeguards. Generally let me know what's missing, ugly, buggy, not working or could be improved. Not really feasible for me to test it all.", - 1 ) - .RegisterEntry( - "Added buttons for redrawing self or all as well as a tooltip to describe redraw options and a tutorial step for it." ) - .RegisterEntry( "Collection Selectors now display None at the top if available." ) - .RegisterEntry( - "Adding mods via API/IPC will now cause them to incorporate and then delete TexTools .meta and .rgsp files automatically." ) - .RegisterEntry( "Fixed an issue with Actor 201 using Your Character collections in cutscenes." ) - .RegisterEntry( "Fixed issues with and improved mod option editing." ) - .RegisterEntry( - "Fixed some issues with and improved file redirection editing - you are now informed if you can not add a game path (because it is invalid or already in use)." ) - .RegisterEntry( "Backend optimizations." ) - .RegisterEntry( "Changed metadata change system again.", 1 ) - .RegisterEntry( "Improved logging efficiency.", 1 ); - - private static void Add5_7_1( Changelog log ) - => log.NextVersion( "Version 0.5.7.1" ) - .RegisterEntry( "Fixed the Changelog window not considering UI Scale correctly." ) - .RegisterEntry( "Reworked Changelog display slightly." ); - - private static void Add5_7_0( Changelog log ) - => log.NextVersion( "Version 0.5.7.0" ) - .RegisterEntry( "Added a Changelog!" ) - .RegisterEntry( "Files in the UI category will no longer be deduplicated for the moment." ) - .RegisterHighlight( "If you experience UI-related crashes, please re-import your UI mods.", 1 ) - .RegisterEntry( "This is a temporary fix against those not-yet fully understood crashes and may be reworked later.", 1 ) - .RegisterHighlight( - "There is still a possibility of UI related mods crashing the game, we are still investigating - they behave very weirdly. If you continue to experience crashing, try disabling your UI mods.", - 1 ) - .RegisterEntry( - "On import, Penumbra will now show files with extensions '.ttmp', '.ttmp2' and '.pmp'. You can still select showing generic archive files." ) - .RegisterEntry( - "Penumbra Mod Pack ('.pmp') files are meant to be renames of any of the archive types that could already be imported that contain the necessary Penumbra meta files.", - 1 ) - .RegisterHighlight( - "If you distribute any mod as an archive specifically for Penumbra, you should change its extension to '.pmp'. Supported base archive types are ZIP, 7-Zip and RAR.", - 1 ) - .RegisterEntry( "Penumbra will now save mod backups with the file extension '.pmp'. They still are regular ZIP files.", 1 ) - .RegisterEntry( - "Existing backups in your current mod directory should be automatically renamed. If you manage multiple mod directories, you may need to migrate the other ones manually.", - 1 ) - .RegisterEntry( "Fixed assigned collections not working correctly on adventurer plates." ) - .RegisterEntry( "Fixed a wrongly displayed folder line in some circumstances." ) - .RegisterEntry( "Fixed crash after deleting mod options." ) - .RegisterEntry( "Fixed Inspect Window collections not working correctly." ) - .RegisterEntry( "Made identically named options selectable in mod configuration. Do not name your options identically." ) - .RegisterEntry( "Added some additional functionality for Mare Synchronos." ); -} \ No newline at end of file diff --git a/Penumbra/UI/ConfigWindow.ModPanel.Edit.cs b/Penumbra/UI/ConfigWindow.ModPanel.Edit.cs index 45f8f25c..ca5de44b 100644 --- a/Penumbra/UI/ConfigWindow.ModPanel.Edit.cs +++ b/Penumbra/UI/ConfigWindow.ModPanel.Edit.cs @@ -239,7 +239,7 @@ public partial class ConfigWindow ImGui.SameLine(); - var nameValid = Mod.Manager.VerifyFileName( mod, null, _newGroupName, false ); + var nameValid = Penumbra.ModManager.VerifyFileName( mod, null, _newGroupName, false ); tt = nameValid ? "Add new option group to the mod." : "Can not add a group of this name."; if( ImGuiUtil.DrawDisabledButton( FontAwesomeIcon.Plus.ToIconString(), window._iconButtonSize, tt, !nameValid, true ) ) @@ -255,7 +255,7 @@ public partial class ConfigWindow { private static string? _currentModDirectory; private static Mod.Manager.NewDirectoryState _state = Mod.Manager.NewDirectoryState.Identical; - + public static void Reset() { _currentModDirectory = null; @@ -269,7 +269,7 @@ public partial class ConfigWindow if( ImGui.InputText( "##newModMove", ref tmp, 64 ) ) { _currentModDirectory = tmp; - _state = Mod.Manager.NewDirectoryValid( mod.ModPath.Name, _currentModDirectory, out _ ); + _state = Penumbra.ModManager.NewDirectoryValid( mod.ModPath.Name, _currentModDirectory, out _ ); } var (disabled, tt) = _state switch diff --git a/Penumbra/UI/ConfigWindow.ModsTab.cs b/Penumbra/UI/ConfigWindow.ModsTab.cs index d08a50c6..5be9c8e0 100644 --- a/Penumbra/UI/ConfigWindow.ModsTab.cs +++ b/Penumbra/UI/ConfigWindow.ModsTab.cs @@ -111,11 +111,11 @@ public partial class ConfigWindow { if( lower.Length > 0 ) { - _penumbra.ObjectReloader.RedrawObject( lower, RedrawType.Redraw ); + _penumbra.RedrawService.RedrawObject( lower, RedrawType.Redraw ); } else { - _penumbra.ObjectReloader.RedrawAll( RedrawType.Redraw ); + _penumbra.RedrawService.RedrawAll( RedrawType.Redraw ); } } diff --git a/Penumbra/UI/ConfigWindow.cs b/Penumbra/UI/ConfigWindow.cs index f212feab..ec149ce2 100644 --- a/Penumbra/UI/ConfigWindow.cs +++ b/Penumbra/UI/ConfigWindow.cs @@ -7,7 +7,7 @@ using OtterGui; using OtterGui.Raii; using OtterGui.Widgets; using Penumbra.Api.Enums; -using Penumbra.Interop.Services; +using Penumbra.Interop.Services; using Penumbra.Mods; using Penumbra.Services; using Penumbra.UI.Classes; @@ -21,6 +21,7 @@ public sealed partial class ConfigWindow : Window, IDisposable private readonly ModFileSystemSelector _selector; private readonly ModPanel _modPanel; public readonly ModEditWindow ModEditPopup; + private readonly Configuration _config; private readonly SettingsTab _settingsTab; private readonly CollectionsTab _collectionsTab; @@ -35,11 +36,13 @@ public sealed partial class ConfigWindow : Window, IDisposable public void SelectMod(Mod mod) => _selector.SelectByValue(mod); - - public ConfigWindow(CommunicatorService communicator, StartTracker timer, FontReloader fontReloader, Penumbra penumbra, ResourceWatcher watcher) + + public ConfigWindow(Configuration config, CommunicatorService communicator, StartTracker timer, FontReloader fontReloader, + Penumbra penumbra, ResourceWatcher watcher) : base(GetLabel()) { _penumbra = penumbra; + _config = config; _resourceWatcher = watcher; ModEditPopup = new ModEditWindow(communicator); @@ -66,6 +69,7 @@ public sealed partial class ConfigWindow : Window, IDisposable MaximumSize = new Vector2(4096, 2160), }; UpdateTutorialStep(); + IsOpen = _config.DebugMode; } private ReadOnlySpan ToLabel(TabType type) diff --git a/Penumbra/UI/LaunchButton.cs b/Penumbra/UI/LaunchButton.cs index 39f9cdd1..3aa470f5 100644 --- a/Penumbra/UI/LaunchButton.cs +++ b/Penumbra/UI/LaunchButton.cs @@ -1,49 +1,66 @@ using System; using System.IO; using Dalamud.Interface; +using Dalamud.Plugin; using ImGuiScene; -using Penumbra.Services; - + namespace Penumbra.UI; -// A Launch Button used in the title screen of the game, -// using the Dalamud-provided collapsible submenu. +/// +/// A Launch Button used in the title screen of the game, +/// using the Dalamud-provided collapsible submenu. +/// public class LaunchButton : IDisposable { - private readonly ConfigWindow _configWindow; - private TextureWrap? _icon; - private TitleScreenMenu.TitleScreenMenuEntry? _entry; + private readonly ConfigWindow _configWindow; + private readonly UiBuilder _uiBuilder; + private readonly TitleScreenMenu _title; + private readonly string _fileName; - public LaunchButton( ConfigWindow ui ) + private TextureWrap? _icon; + private TitleScreenMenu.TitleScreenMenuEntry? _entry; + + /// + /// Register the launch button to be created on the next draw event. + /// + public LaunchButton(DalamudPluginInterface pi, TitleScreenMenu title, ConfigWindow ui) { + _uiBuilder = pi.UiBuilder; _configWindow = ui; + _title = title; _icon = null; _entry = null; - void CreateEntry() - { - _icon = DalamudServices.PluginInterface.UiBuilder.LoadImage( Path.Combine( DalamudServices.PluginInterface.AssemblyLocation.DirectoryName!, - "tsmLogo.png" ) ); - if( _icon != null ) - { - _entry = DalamudServices.TitleScreenMenu.AddEntry( "Manage Penumbra", _icon, OnTriggered ); - } - - DalamudServices.PluginInterface.UiBuilder.Draw -= CreateEntry; - } - - DalamudServices.PluginInterface.UiBuilder.Draw += CreateEntry; + _fileName = Path.Combine(pi.AssemblyLocation.DirectoryName!, "tsmLogo.png"); + _uiBuilder.Draw += CreateEntry; } - private void OnTriggered() - => _configWindow.Toggle(); - public void Dispose() { _icon?.Dispose(); - if( _entry != null ) + if (_entry != null) + _title.RemoveEntry(_entry); + } + + /// + /// One-Time event to load the image and create the entry on the first drawn frame, but not before. + /// + private void CreateEntry() + { + try { - DalamudServices.TitleScreenMenu.RemoveEntry( _entry ); + _icon = _uiBuilder.LoadImage(_fileName); + if (_icon != null) + _entry = _title.AddEntry("Manage Penumbra", _icon, OnTriggered); + + _uiBuilder.Draw -= CreateEntry; + } + catch (Exception ex) + { + Penumbra.Log.Error($"Could not register title screen menu entry:\n{ex}"); } } -} \ No newline at end of file + + private void OnTriggered() + => _configWindow.Toggle(); +} diff --git a/Penumbra/UI/WindowSystem.cs b/Penumbra/UI/WindowSystem.cs new file mode 100644 index 00000000..630033b9 --- /dev/null +++ b/Penumbra/UI/WindowSystem.cs @@ -0,0 +1,40 @@ +using System; +using Dalamud.Interface; +using Dalamud.Interface.Windowing; +using Dalamud.Plugin; +using Penumbra.UI; +using Penumbra.UI.Classes; + +namespace Penumbra; + +public class PenumbraWindowSystem : IDisposable +{ + private readonly UiBuilder _uiBuilder; + private readonly WindowSystem _windowSystem; + public readonly ConfigWindow Window; + public readonly PenumbraChangelog Changelog; + + public PenumbraWindowSystem(DalamudPluginInterface pi, PenumbraChangelog changelog, ConfigWindow window, LaunchButton _, + ModEditWindow editWindow) + { + _uiBuilder = pi.UiBuilder; + Changelog = changelog; + Window = window; + _windowSystem = new WindowSystem("Penumbra"); + _windowSystem.AddWindow(changelog.Changelog); + _windowSystem.AddWindow(window); + _windowSystem.AddWindow(editWindow); + + _uiBuilder.OpenConfigUi += Window.Toggle; + _uiBuilder.Draw += _windowSystem.Draw; + } + + public void ForceChangelogOpen() + => Changelog.Changelog.ForceOpen = true; + + public void Dispose() + { + _uiBuilder.OpenConfigUi -= Window.Toggle; + _uiBuilder.Draw -= _windowSystem.Draw; + } +} diff --git a/Penumbra/Util/ChatService.cs b/Penumbra/Util/ChatService.cs new file mode 100644 index 00000000..92cf0560 --- /dev/null +++ b/Penumbra/Util/ChatService.cs @@ -0,0 +1,109 @@ +using System.Collections.Generic; +using Dalamud.Game.Gui; +using Dalamud.Game.Text; +using Dalamud.Game.Text.SeStringHandling; +using Dalamud.Game.Text.SeStringHandling.Payloads; +using Dalamud.Interface; +using Dalamud.Interface.Internal.Notifications; +using Dalamud.Plugin; +using Dalamud.Utility; +using Lumina.Excel.GeneratedSheets; +using OtterGui.Log; + +namespace Penumbra.Util; + +public class ChatService +{ + private readonly Logger _log; + private readonly UiBuilder _uiBuilder; + private readonly ChatGui _chat; + + public ChatService(Logger log, DalamudPluginInterface pi, ChatGui chat) + { + _log = log; + _uiBuilder = pi.UiBuilder; + _chat = chat; + } + + public void LinkItem(Item item) + { + // @formatter:off + var payloadList = new List + { + new UIForegroundPayload((ushort)(0x223 + item.Rarity * 2)), + new UIGlowPayload((ushort)(0x224 + item.Rarity * 2)), + new ItemPayload(item.RowId, false), + new UIForegroundPayload(500), + new UIGlowPayload(501), + new TextPayload($"{(char)SeIconChar.LinkMarker}"), + new UIForegroundPayload(0), + new UIGlowPayload(0), + new TextPayload(item.Name), + new RawPayload(new byte[] { 0x02, 0x27, 0x07, 0xCF, 0x01, 0x01, 0x01, 0xFF, 0x01, 0x03 }), + new RawPayload(new byte[] { 0x02, 0x13, 0x02, 0xEC, 0x03 }), + }; + // @formatter:on + + var payload = new SeString(payloadList); + + _chat.PrintChat(new XivChatEntry + { + Message = payload, + }); + } + + public void NotificationMessage(string content, string? title = null, NotificationType type = NotificationType.None) + { + var logLevel = type switch + { + NotificationType.None => Logger.LogLevel.Information, + NotificationType.Success => Logger.LogLevel.Information, + NotificationType.Warning => Logger.LogLevel.Warning, + NotificationType.Error => Logger.LogLevel.Error, + NotificationType.Info => Logger.LogLevel.Information, + _ => Logger.LogLevel.Debug, + }; + _uiBuilder.AddNotification(content, title, type); + _log.Message(logLevel, title.IsNullOrEmpty() ? content : $"[{title}] {content}"); + } +} + +public static class SeStringBuilderExtensions +{ + public const ushort Green = 504; + public const ushort Yellow = 31; + public const ushort Red = 534; + public const ushort Blue = 517; + public const ushort White = 1; + public const ushort Purple = 541; + + public static SeStringBuilder AddText(this SeStringBuilder sb, string text, int color, bool brackets = false) + => sb.AddUiForeground((ushort)color).AddText(brackets ? $"[{text}]" : text).AddUiForegroundOff(); + + public static SeStringBuilder AddGreen(this SeStringBuilder sb, string text, bool brackets = false) + => AddText(sb, text, Green, brackets); + + public static SeStringBuilder AddYellow(this SeStringBuilder sb, string text, bool brackets = false) + => AddText(sb, text, Yellow, brackets); + + public static SeStringBuilder AddRed(this SeStringBuilder sb, string text, bool brackets = false) + => AddText(sb, text, Red, brackets); + + public static SeStringBuilder AddBlue(this SeStringBuilder sb, string text, bool brackets = false) + => AddText(sb, text, Blue, brackets); + + public static SeStringBuilder AddWhite(this SeStringBuilder sb, string text, bool brackets = false) + => AddText(sb, text, White, brackets); + + public static SeStringBuilder AddPurple(this SeStringBuilder sb, string text, bool brackets = false) + => AddText(sb, text, Purple, brackets); + + public static SeStringBuilder AddCommand(this SeStringBuilder sb, string command, string description) + => sb.AddText(" 》 ") + .AddBlue(command) + .AddText($" - {description}"); + + public static SeStringBuilder AddInitialPurple(this SeStringBuilder sb, string word, bool withComma = true) + => sb.AddPurple($"[{word[0]}]") + .AddText(withComma ? $"{word[1..]}, " : word[1..]); +} diff --git a/Penumbra/Util/ChatUtil.cs b/Penumbra/Util/ChatUtil.cs deleted file mode 100644 index aff2e67b..00000000 --- a/Penumbra/Util/ChatUtil.cs +++ /dev/null @@ -1,54 +0,0 @@ -using System.Collections.Generic; -using Dalamud.Game.Text; -using Dalamud.Game.Text.SeStringHandling; -using Dalamud.Game.Text.SeStringHandling.Payloads; -using Dalamud.Interface.Internal.Notifications; -using Dalamud.Utility; -using Lumina.Excel.GeneratedSheets; -using OtterGui.Log; -using Penumbra.Services; - -namespace Penumbra.Util; - -public static class ChatUtil -{ - public static void LinkItem( Item item ) - { - var payloadList = new List< Payload > - { - new UIForegroundPayload( ( ushort )( 0x223 + item.Rarity * 2 ) ), - new UIGlowPayload( ( ushort )( 0x224 + item.Rarity * 2 ) ), - new ItemPayload( item.RowId, false ), - new UIForegroundPayload( 500 ), - new UIGlowPayload( 501 ), - new TextPayload( $"{( char )SeIconChar.LinkMarker}" ), - new UIForegroundPayload( 0 ), - new UIGlowPayload( 0 ), - new TextPayload( item.Name ), - new RawPayload( new byte[] { 0x02, 0x27, 0x07, 0xCF, 0x01, 0x01, 0x01, 0xFF, 0x01, 0x03 } ), - new RawPayload( new byte[] { 0x02, 0x13, 0x02, 0xEC, 0x03 } ), - }; - - var payload = new SeString( payloadList ); - - DalamudServices.Chat.PrintChat( new XivChatEntry - { - Message = payload, - } ); - } - - public static void NotificationMessage( string content, string? title = null, NotificationType type = NotificationType.None ) - { - var logLevel = type switch - { - NotificationType.None => Logger.LogLevel.Information, - NotificationType.Success => Logger.LogLevel.Information, - NotificationType.Warning => Logger.LogLevel.Warning, - NotificationType.Error => Logger.LogLevel.Error, - NotificationType.Info => Logger.LogLevel.Information, - _ => Logger.LogLevel.Debug, - }; - DalamudServices.PluginInterface.UiBuilder.AddNotification( content, title, type ); - Penumbra.Log.Message( logLevel, title.IsNullOrEmpty() ? content : $"[{title}] {content}" ); - } -} \ No newline at end of file diff --git a/Penumbra/Util/SaveService.cs b/Penumbra/Util/SaveService.cs new file mode 100644 index 00000000..71c8b6b4 --- /dev/null +++ b/Penumbra/Util/SaveService.cs @@ -0,0 +1,99 @@ +using System; +using System.IO; +using System.Runtime.CompilerServices; +using System.Text; +using OtterGui.Classes; +using OtterGui.Log; +using Penumbra.Api; +using Penumbra.Services; + +namespace Penumbra.Util; + +/// +/// Any file type that we want to save via SaveService. +/// +public interface ISaveable +{ + /// The full file name of a given object. + public string ToFilename(FilenameService fileNames); + + /// Write the objects data to the given stream writer. + public void Save(StreamWriter writer); + + /// An arbitrary message printed to Debug before saving. + public string LogName(string fileName) + => fileName; + + public string TypeName + => GetType().Name; +} + +public class SaveService +{ + private readonly Logger _log; + private readonly FilenameService _fileNames; + private readonly FrameworkManager _framework; + + public SaveService(Logger log, FilenameService fileNames, FrameworkManager framework) + { + _log = log; + _fileNames = fileNames; + _framework = framework; + } + + /// Queue a save for the next framework tick. + public void QueueSave(ISaveable value) + { + var file = value.ToFilename(_fileNames); + _framework.RegisterDelayed(value.GetType().Name + file, () => + { + ImmediateSave(value); + }); + } + + /// Immediately trigger a save. + public void ImmediateSave(ISaveable value) + { + var name = value.ToFilename(_fileNames); + try + { + if (name.Length == 0) + { + throw new Exception("Invalid object returned empty filename."); + } + + _log.Debug($"Saving {value.TypeName} {value.LogName(name)}..."); + var file = new FileInfo(name); + file.Directory?.Create(); + using var s = file.Exists ? file.Open(FileMode.Truncate) : file.Open(FileMode.CreateNew); + using var w = new StreamWriter(s, Encoding.UTF8); + value.Save(w); + } + catch (Exception ex) + { + _log.Error($"Could not save {value.GetType().Name} {value.LogName(name)}:\n{ex}"); + } + } + + public void ImmediateDelete(ISaveable value) + { + var name = value.ToFilename(_fileNames); + try + { + if (name.Length == 0) + { + throw new Exception("Invalid object returned empty filename."); + } + + if (!File.Exists(name)) + return; + + _log.Information($"Deleting {value.GetType().Name} {value.LogName(name)}..."); + File.Delete(name); + } + catch (Exception ex) + { + _log.Error($"Could not delete {value.GetType().Name} {value.LogName(name)}:\n{ex}"); + } + } +}