This is going rather well.

This commit is contained in:
Ottermandias 2023-03-11 17:50:32 +01:00
parent 73e2793da6
commit bdaff7b781
48 changed files with 2944 additions and 2952 deletions

@ -1 +1 @@
Subproject commit d7867dfa6579d4e69876753e9cde72e13d3372ce Subproject commit 3d346700e8800c045aa19d70d516d8a4fda2f2ee

View file

@ -1404,10 +1404,10 @@ public class IpcTester : IDisposable
return; return;
} }
foreach( var collection in Penumbra.TempMods.CustomCollections.Values ) foreach( var collection in Penumbra.TempCollections.Values )
{ {
ImGui.TableNextColumn(); ImGui.TableNextColumn();
var character = Penumbra.TempMods.Collections.Where( p => p.Collection == collection ).Select( p => p.DisplayName ).FirstOrDefault() ?? "Unknown"; var character = Penumbra.TempCollections.Collections.Where( p => p.Collection == collection ).Select( p => p.DisplayName ).FirstOrDefault() ?? "Unknown";
if( ImGui.Button( $"Save##{collection.Name}" ) ) if( ImGui.Button( $"Save##{collection.Name}" ) )
{ {
Mod.TemporaryMod.SaveTempCollection( collection, character ); Mod.TemporaryMod.SaveTempCollection( collection, character );
@ -1416,7 +1416,7 @@ public class IpcTester : IDisposable
ImGuiUtil.DrawTableColumn( collection.Name ); ImGuiUtil.DrawTableColumn( collection.Name );
ImGuiUtil.DrawTableColumn( collection.ResolvedFiles.Count.ToString() ); ImGuiUtil.DrawTableColumn( collection.ResolvedFiles.Count.ToString() );
ImGuiUtil.DrawTableColumn( collection.MetaCache?.Count.ToString() ?? "0" ); ImGuiUtil.DrawTableColumn( collection.MetaCache?.Count.ToString() ?? "0" );
ImGuiUtil.DrawTableColumn( string.Join( ", ", Penumbra.TempMods.Collections.Where( p => p.Collection == collection ).Select( c => c.DisplayName ) ) ); ImGuiUtil.DrawTableColumn( string.Join( ", ", Penumbra.TempCollections.Collections.Where( p => p.Collection == collection ).Select( c => c.DisplayName ) ) );
} }
} }

View file

@ -27,6 +27,7 @@ public class PenumbraApi : IDisposable, IPenumbraApi
public (int, int) ApiVersion public (int, int) ApiVersion
=> (4, 19); => (4, 19);
private CommunicatorService? _communicator;
private Penumbra? _penumbra; private Penumbra? _penumbra;
private Lumina.GameData? _lumina; private Lumina.GameData? _lumina;
@ -82,18 +83,17 @@ public class PenumbraApi : IDisposable, IPenumbraApi
public bool Valid public bool Valid
=> _penumbra != null; => _penumbra != null;
public unsafe PenumbraApi( Penumbra penumbra ) public unsafe PenumbraApi(CommunicatorService communicator, Penumbra penumbra)
{ {
_communicator = communicator;
_penumbra = penumbra; _penumbra = penumbra;
_lumina = (Lumina.GameData?)DalamudServices.GameData.GetType() _lumina = (Lumina.GameData?)DalamudServices.GameData.GetType()
.GetField("gameData", BindingFlags.Instance | BindingFlags.NonPublic) .GetField("gameData", BindingFlags.Instance | BindingFlags.NonPublic)
?.GetValue(DalamudServices.GameData); ?.GetValue(DalamudServices.GameData);
foreach (var collection in Penumbra.CollectionManager) foreach (var collection in Penumbra.CollectionManager)
{
SubscribeToCollection(collection); SubscribeToCollection(collection);
}
Penumbra.CollectionManager.CollectionChanged += SubscribeToNewCollections; _communicator.CollectionChange.Event += SubscribeToNewCollections;
Penumbra.ResourceLoader.ResourceLoaded += OnResourceLoaded; Penumbra.ResourceLoader.ResourceLoaded += OnResourceLoaded;
Penumbra.ModManager.ModPathChanged += ModPathChangeSubscriber; Penumbra.ModManager.ModPathChanged += ModPathChangeSubscriber;
} }
@ -101,18 +101,17 @@ public class PenumbraApi : IDisposable, IPenumbraApi
public unsafe void Dispose() public unsafe void Dispose()
{ {
Penumbra.ResourceLoader.ResourceLoaded -= OnResourceLoaded; Penumbra.ResourceLoader.ResourceLoaded -= OnResourceLoaded;
Penumbra.CollectionManager.CollectionChanged -= SubscribeToNewCollections; _communicator!.CollectionChange.Event -= SubscribeToNewCollections;
Penumbra.ModManager.ModPathChanged -= ModPathChangeSubscriber; Penumbra.ModManager.ModPathChanged -= ModPathChangeSubscriber;
_penumbra = null; _penumbra = null;
_lumina = null; _lumina = null;
_communicator = null;
foreach (var collection in Penumbra.CollectionManager) foreach (var collection in Penumbra.CollectionManager)
{ {
if (_delegates.TryGetValue(collection, out var del)) if (_delegates.TryGetValue(collection, out var del))
{
collection.ModSettingChanged -= del; collection.ModSettingChanged -= del;
} }
} }
}
public event ChangedItemClick? ChangedItemClicked; public event ChangedItemClick? ChangedItemClicked;
@ -126,11 +125,9 @@ public class PenumbraApi : IDisposable, IPenumbraApi
ResolveData resolveData) ResolveData resolveData)
{ {
if (resolveData.AssociatedGameObject != IntPtr.Zero) if (resolveData.AssociatedGameObject != IntPtr.Zero)
{
GameObjectResourceResolved?.Invoke(resolveData.AssociatedGameObject, originalPath.ToString(), GameObjectResourceResolved?.Invoke(resolveData.AssociatedGameObject, originalPath.ToString(),
manipulatedPath?.ToString() ?? originalPath.ToString()); manipulatedPath?.ToString() ?? originalPath.ToString());
} }
}
public event Action<string, bool>? ModDirectoryChanged public event Action<string, bool>? ModDirectoryChanged
{ {
@ -176,33 +173,23 @@ public class PenumbraApi : IDisposable, IPenumbraApi
{ {
CheckInitialized(); CheckInitialized();
if (_penumbra!.ConfigWindow == null) if (_penumbra!.ConfigWindow == null)
{
return PenumbraApiEc.SystemDisposed; return PenumbraApiEc.SystemDisposed;
}
_penumbra!.ConfigWindow.IsOpen = true; _penumbra!.ConfigWindow.IsOpen = true;
if (!Enum.IsDefined(tab)) if (!Enum.IsDefined(tab))
{
return PenumbraApiEc.InvalidArgument; return PenumbraApiEc.InvalidArgument;
}
if (tab != TabType.None) if (tab != TabType.None)
{
_penumbra!.ConfigWindow.SelectTab = tab; _penumbra!.ConfigWindow.SelectTab = tab;
}
if (tab == TabType.Mods && (modDirectory.Length > 0 || modName.Length > 0)) if (tab == TabType.Mods && (modDirectory.Length > 0 || modName.Length > 0))
{ {
if (Penumbra.ModManager.TryGetMod(modDirectory, modName, out var mod)) if (Penumbra.ModManager.TryGetMod(modDirectory, modName, out var mod))
{
_penumbra!.ConfigWindow.SelectMod(mod); _penumbra!.ConfigWindow.SelectMod(mod);
}
else else
{
return PenumbraApiEc.ModMissing; return PenumbraApiEc.ModMissing;
} }
}
return PenumbraApiEc.Success; return PenumbraApiEc.Success;
} }
@ -211,9 +198,7 @@ public class PenumbraApi : IDisposable, IPenumbraApi
{ {
CheckInitialized(); CheckInitialized();
if (_penumbra!.ConfigWindow == null) if (_penumbra!.ConfigWindow == null)
{
return; return;
}
_penumbra!.ConfigWindow.IsOpen = false; _penumbra!.ConfigWindow.IsOpen = false;
} }
@ -286,9 +271,10 @@ public class PenumbraApi : IDisposable, IPenumbraApi
{ {
CheckInitialized(); CheckInitialized();
if (!Penumbra.Config.EnableMods) if (!Penumbra.Config.EnableMods)
return new[]
{ {
return new[] { path }; path,
} };
var ret = Penumbra.CollectionManager.Individual(NameToIdentifier(characterName, worldId)).ReverseResolvePath(new FullPath(path)); var ret = Penumbra.CollectionManager.Individual(NameToIdentifier(characterName, worldId)).ReverseResolvePath(new FullPath(path));
return ret.Select(r => r.ToString()).ToArray(); return ret.Select(r => r.ToString()).ToArray();
@ -298,9 +284,10 @@ public class PenumbraApi : IDisposable, IPenumbraApi
{ {
CheckInitialized(); CheckInitialized();
if (!Penumbra.Config.EnableMods) if (!Penumbra.Config.EnableMods)
return new[]
{ {
return new[] { path }; path,
} };
AssociatedCollection(gameObjectIdx, out var collection); AssociatedCollection(gameObjectIdx, out var collection);
var ret = collection.ReverseResolvePath(new FullPath(path)); var ret = collection.ReverseResolvePath(new FullPath(path));
@ -311,9 +298,10 @@ public class PenumbraApi : IDisposable, IPenumbraApi
{ {
CheckInitialized(); CheckInitialized();
if (!Penumbra.Config.EnableMods) if (!Penumbra.Config.EnableMods)
return new[]
{ {
return new[] { path }; path,
} };
var ret = PathResolver.PlayerCollection().ReverseResolvePath(new FullPath(path)); var ret = PathResolver.PlayerCollection().ReverseResolvePath(new FullPath(path));
return ret.Select(r => r.ToString()).ToArray(); return ret.Select(r => r.ToString()).ToArray();
@ -323,9 +311,10 @@ public class PenumbraApi : IDisposable, IPenumbraApi
{ {
CheckInitialized(); CheckInitialized();
if (!Penumbra.Config.EnableMods) if (!Penumbra.Config.EnableMods)
return (forward, reverse.Select(p => new[]
{ {
return ( forward, reverse.Select( p => new[] { p } ).ToArray() ); p,
} }).ToArray());
var playerCollection = PathResolver.PlayerCollection(); var playerCollection = PathResolver.PlayerCollection();
var resolved = forward.Select(p => ResolvePath(p, Penumbra.ModManager, playerCollection)).ToArray(); var resolved = forward.Select(p => ResolvePath(p, Penumbra.ModManager, playerCollection)).ToArray();
@ -345,14 +334,10 @@ public class PenumbraApi : IDisposable, IPenumbraApi
try try
{ {
if (!Penumbra.CollectionManager.ByName(collectionName, out var collection)) if (!Penumbra.CollectionManager.ByName(collectionName, out var collection))
{
collection = ModCollection.Empty; collection = ModCollection.Empty;
}
if (collection.HasCache) if (collection.HasCache)
{
return collection.ChangedItems.ToDictionary(kvp => kvp.Key, kvp => kvp.Value.Item2); return collection.ChangedItems.ToDictionary(kvp => kvp.Key, kvp => kvp.Value.Item2);
}
Penumbra.Log.Warning($"Collection {collectionName} does not exist or is not loaded."); Penumbra.Log.Warning($"Collection {collectionName} does not exist or is not loaded.");
return new Dictionary<string, object?>(); return new Dictionary<string, object?>();
@ -368,51 +353,40 @@ public class PenumbraApi : IDisposable, IPenumbraApi
{ {
CheckInitialized(); CheckInitialized();
if (!Enum.IsDefined(type)) if (!Enum.IsDefined(type))
{
return string.Empty; return string.Empty;
}
var collection = Penumbra.CollectionManager.ByType((CollectionType)type); var collection = Penumbra.CollectionManager.ByType((CollectionType)type);
return collection?.Name ?? string.Empty; return collection?.Name ?? string.Empty;
} }
public (PenumbraApiEc, string OldCollection) SetCollectionForType( ApiCollectionType type, string collectionName, bool allowCreateNew, bool allowDelete ) public (PenumbraApiEc, string OldCollection) SetCollectionForType(ApiCollectionType type, string collectionName, bool allowCreateNew,
bool allowDelete)
{ {
CheckInitialized(); CheckInitialized();
if (!Enum.IsDefined(type)) if (!Enum.IsDefined(type))
{
return (PenumbraApiEc.InvalidArgument, string.Empty); return (PenumbraApiEc.InvalidArgument, string.Empty);
}
var oldCollection = Penumbra.CollectionManager.ByType((CollectionType)type)?.Name ?? string.Empty; var oldCollection = Penumbra.CollectionManager.ByType((CollectionType)type)?.Name ?? string.Empty;
if (collectionName.Length == 0) if (collectionName.Length == 0)
{ {
if (oldCollection.Length == 0) if (oldCollection.Length == 0)
{
return (PenumbraApiEc.NothingChanged, oldCollection); return (PenumbraApiEc.NothingChanged, oldCollection);
}
if (!allowDelete || type is ApiCollectionType.Current or ApiCollectionType.Default or ApiCollectionType.Interface) if (!allowDelete || type is ApiCollectionType.Current or ApiCollectionType.Default or ApiCollectionType.Interface)
{
return (PenumbraApiEc.AssignmentDeletionDisallowed, oldCollection); return (PenumbraApiEc.AssignmentDeletionDisallowed, oldCollection);
}
Penumbra.CollectionManager.RemoveSpecialCollection((CollectionType)type); Penumbra.CollectionManager.RemoveSpecialCollection((CollectionType)type);
return (PenumbraApiEc.Success, oldCollection); return (PenumbraApiEc.Success, oldCollection);
} }
if (!Penumbra.CollectionManager.ByName(collectionName, out var collection)) if (!Penumbra.CollectionManager.ByName(collectionName, out var collection))
{
return (PenumbraApiEc.CollectionMissing, oldCollection); return (PenumbraApiEc.CollectionMissing, oldCollection);
}
if (oldCollection.Length == 0) if (oldCollection.Length == 0)
{ {
if (!allowCreateNew) if (!allowCreateNew)
{
return (PenumbraApiEc.AssignmentCreationDisallowed, oldCollection); return (PenumbraApiEc.AssignmentCreationDisallowed, oldCollection);
}
Penumbra.CollectionManager.CreateSpecialCollection((CollectionType)type); Penumbra.CollectionManager.CreateSpecialCollection((CollectionType)type);
} }
@ -430,41 +404,32 @@ public class PenumbraApi : IDisposable, IPenumbraApi
CheckInitialized(); CheckInitialized();
var id = AssociatedIdentifier(gameObjectIdx); var id = AssociatedIdentifier(gameObjectIdx);
if (!id.IsValid) if (!id.IsValid)
{
return (false, false, Penumbra.CollectionManager.Default.Name); return (false, false, Penumbra.CollectionManager.Default.Name);
}
if (Penumbra.CollectionManager.Individuals.Individuals.TryGetValue(id, out var collection)) if (Penumbra.CollectionManager.Individuals.Individuals.TryGetValue(id, out var collection))
{
return (true, true, collection.Name); return (true, true, collection.Name);
}
AssociatedCollection(gameObjectIdx, out collection); AssociatedCollection(gameObjectIdx, out collection);
return (true, false, collection.Name); return (true, false, collection.Name);
} }
public (PenumbraApiEc, string OldCollection) SetCollectionForObject( int gameObjectIdx, string collectionName, bool allowCreateNew, bool allowDelete ) public (PenumbraApiEc, string OldCollection) SetCollectionForObject(int gameObjectIdx, string collectionName, bool allowCreateNew,
bool allowDelete)
{ {
CheckInitialized(); CheckInitialized();
var id = AssociatedIdentifier(gameObjectIdx); var id = AssociatedIdentifier(gameObjectIdx);
if (!id.IsValid) if (!id.IsValid)
{
return (PenumbraApiEc.InvalidIdentifier, Penumbra.CollectionManager.Default.Name); return (PenumbraApiEc.InvalidIdentifier, Penumbra.CollectionManager.Default.Name);
}
var oldCollection = Penumbra.CollectionManager.Individuals.Individuals.TryGetValue(id, out var c) ? c.Name : string.Empty; var oldCollection = Penumbra.CollectionManager.Individuals.Individuals.TryGetValue(id, out var c) ? c.Name : string.Empty;
if (collectionName.Length == 0) if (collectionName.Length == 0)
{ {
if (oldCollection.Length == 0) if (oldCollection.Length == 0)
{
return (PenumbraApiEc.NothingChanged, oldCollection); return (PenumbraApiEc.NothingChanged, oldCollection);
}
if (!allowDelete) if (!allowDelete)
{
return (PenumbraApiEc.AssignmentDeletionDisallowed, oldCollection); return (PenumbraApiEc.AssignmentDeletionDisallowed, oldCollection);
}
var idx = Penumbra.CollectionManager.Individuals.Index(id); var idx = Penumbra.CollectionManager.Individuals.Index(id);
Penumbra.CollectionManager.RemoveIndividualCollection(idx); Penumbra.CollectionManager.RemoveIndividualCollection(idx);
@ -472,16 +437,12 @@ public class PenumbraApi : IDisposable, IPenumbraApi
} }
if (!Penumbra.CollectionManager.ByName(collectionName, out var collection)) if (!Penumbra.CollectionManager.ByName(collectionName, out var collection))
{
return (PenumbraApiEc.CollectionMissing, oldCollection); return (PenumbraApiEc.CollectionMissing, oldCollection);
}
if (oldCollection.Length == 0) if (oldCollection.Length == 0)
{ {
if (!allowCreateNew) if (!allowCreateNew)
{
return (PenumbraApiEc.AssignmentCreationDisallowed, oldCollection); return (PenumbraApiEc.AssignmentCreationDisallowed, oldCollection);
}
var ids = Penumbra.CollectionManager.Individuals.GetGroup(id); var ids = Penumbra.CollectionManager.Individuals.GetGroup(id);
Penumbra.CollectionManager.CreateIndividualCollection(ids); Penumbra.CollectionManager.CreateIndividualCollection(ids);
@ -563,20 +524,14 @@ public class PenumbraApi : IDisposable, IPenumbraApi
{ {
CheckInitialized(); CheckInitialized();
if (!Penumbra.CollectionManager.ByName(collectionName, out var collection)) if (!Penumbra.CollectionManager.ByName(collectionName, out var collection))
{
return (PenumbraApiEc.CollectionMissing, null); return (PenumbraApiEc.CollectionMissing, null);
}
if (!Penumbra.ModManager.TryGetMod(modDirectory, modName, out var mod)) if (!Penumbra.ModManager.TryGetMod(modDirectory, modName, out var mod))
{
return (PenumbraApiEc.ModMissing, null); return (PenumbraApiEc.ModMissing, null);
}
var settings = allowInheritance ? collection.Settings[mod.Index] : collection[mod.Index].Settings; var settings = allowInheritance ? collection.Settings[mod.Index] : collection[mod.Index].Settings;
if (settings == null) if (settings == null)
{
return (PenumbraApiEc.Success, null); return (PenumbraApiEc.Success, null);
}
var shareSettings = settings.ConvertToShareable(mod); var shareSettings = settings.ConvertToShareable(mod);
return (PenumbraApiEc.Success, return (PenumbraApiEc.Success,
@ -587,9 +542,7 @@ public class PenumbraApi : IDisposable, IPenumbraApi
{ {
CheckInitialized(); CheckInitialized();
if (!Penumbra.ModManager.TryGetMod(modDirectory, modName, out var mod)) if (!Penumbra.ModManager.TryGetMod(modDirectory, modName, out var mod))
{
return PenumbraApiEc.ModMissing; return PenumbraApiEc.ModMissing;
}
Penumbra.ModManager.ReloadMod(mod.Index); Penumbra.ModManager.ReloadMod(mod.Index);
return PenumbraApiEc.Success; return PenumbraApiEc.Success;
@ -600,9 +553,7 @@ public class PenumbraApi : IDisposable, IPenumbraApi
CheckInitialized(); CheckInitialized();
var dir = new DirectoryInfo(Path.Join(Penumbra.ModManager.BasePath.FullName, Path.GetFileName(modDirectory))); var dir = new DirectoryInfo(Path.Join(Penumbra.ModManager.BasePath.FullName, Path.GetFileName(modDirectory)));
if (!dir.Exists) if (!dir.Exists)
{
return PenumbraApiEc.FileMissing; return PenumbraApiEc.FileMissing;
}
Penumbra.ModManager.AddMod(dir); Penumbra.ModManager.AddMod(dir);
return PenumbraApiEc.Success; return PenumbraApiEc.Success;
@ -612,9 +563,7 @@ public class PenumbraApi : IDisposable, IPenumbraApi
{ {
CheckInitialized(); CheckInitialized();
if (!Penumbra.ModManager.TryGetMod(modDirectory, modName, out var mod)) if (!Penumbra.ModManager.TryGetMod(modDirectory, modName, out var mod))
{
return PenumbraApiEc.NothingChanged; return PenumbraApiEc.NothingChanged;
}
Penumbra.ModManager.DeleteMod(mod.Index); Penumbra.ModManager.DeleteMod(mod.Index);
return PenumbraApiEc.Success; return PenumbraApiEc.Success;
@ -646,9 +595,7 @@ public class PenumbraApi : IDisposable, IPenumbraApi
CheckInitialized(); CheckInitialized();
if (!Penumbra.ModManager.TryGetMod(modDirectory, modName, out var mod) if (!Penumbra.ModManager.TryGetMod(modDirectory, modName, out var mod)
|| !_penumbra!.ModFileSystem.FindLeaf(mod, out var leaf)) || !_penumbra!.ModFileSystem.FindLeaf(mod, out var leaf))
{
return (PenumbraApiEc.ModMissing, string.Empty, false); return (PenumbraApiEc.ModMissing, string.Empty, false);
}
var fullPath = leaf.FullName(); var fullPath = leaf.FullName();
@ -659,15 +606,11 @@ public class PenumbraApi : IDisposable, IPenumbraApi
{ {
CheckInitialized(); CheckInitialized();
if (newPath.Length == 0) if (newPath.Length == 0)
{
return PenumbraApiEc.InvalidArgument; return PenumbraApiEc.InvalidArgument;
}
if (!Penumbra.ModManager.TryGetMod(modDirectory, modName, out var mod) if (!Penumbra.ModManager.TryGetMod(modDirectory, modName, out var mod)
|| !_penumbra!.ModFileSystem.FindLeaf(mod, out var leaf)) || !_penumbra!.ModFileSystem.FindLeaf(mod, out var leaf))
{
return PenumbraApiEc.ModMissing; return PenumbraApiEc.ModMissing;
}
try try
{ {
@ -684,14 +627,10 @@ public class PenumbraApi : IDisposable, IPenumbraApi
{ {
CheckInitialized(); CheckInitialized();
if (!Penumbra.CollectionManager.ByName(collectionName, out var collection)) if (!Penumbra.CollectionManager.ByName(collectionName, out var collection))
{
return PenumbraApiEc.CollectionMissing; return PenumbraApiEc.CollectionMissing;
}
if (!Penumbra.ModManager.TryGetMod(modDirectory, modName, out var mod)) if (!Penumbra.ModManager.TryGetMod(modDirectory, modName, out var mod))
{
return PenumbraApiEc.ModMissing; return PenumbraApiEc.ModMissing;
}
return collection.SetModInheritance(mod.Index, inherit) ? PenumbraApiEc.Success : PenumbraApiEc.NothingChanged; return collection.SetModInheritance(mod.Index, inherit) ? PenumbraApiEc.Success : PenumbraApiEc.NothingChanged;
@ -701,14 +640,10 @@ public class PenumbraApi : IDisposable, IPenumbraApi
{ {
CheckInitialized(); CheckInitialized();
if (!Penumbra.CollectionManager.ByName(collectionName, out var collection)) if (!Penumbra.CollectionManager.ByName(collectionName, out var collection))
{
return PenumbraApiEc.CollectionMissing; return PenumbraApiEc.CollectionMissing;
}
if (!Penumbra.ModManager.TryGetMod(modDirectory, modName, out var mod)) if (!Penumbra.ModManager.TryGetMod(modDirectory, modName, out var mod))
{
return PenumbraApiEc.ModMissing; return PenumbraApiEc.ModMissing;
}
return collection.SetModState(mod.Index, enabled) ? PenumbraApiEc.Success : PenumbraApiEc.NothingChanged; return collection.SetModState(mod.Index, enabled) ? PenumbraApiEc.Success : PenumbraApiEc.NothingChanged;
} }
@ -717,14 +652,10 @@ public class PenumbraApi : IDisposable, IPenumbraApi
{ {
CheckInitialized(); CheckInitialized();
if (!Penumbra.CollectionManager.ByName(collectionName, out var collection)) if (!Penumbra.CollectionManager.ByName(collectionName, out var collection))
{
return PenumbraApiEc.CollectionMissing; return PenumbraApiEc.CollectionMissing;
}
if (!Penumbra.ModManager.TryGetMod(modDirectory, modName, out var mod)) if (!Penumbra.ModManager.TryGetMod(modDirectory, modName, out var mod))
{
return PenumbraApiEc.ModMissing; return PenumbraApiEc.ModMissing;
}
return collection.SetModPriority(mod.Index, priority) ? PenumbraApiEc.Success : PenumbraApiEc.NothingChanged; return collection.SetModPriority(mod.Index, priority) ? PenumbraApiEc.Success : PenumbraApiEc.NothingChanged;
} }
@ -734,26 +665,18 @@ public class PenumbraApi : IDisposable, IPenumbraApi
{ {
CheckInitialized(); CheckInitialized();
if (!Penumbra.CollectionManager.ByName(collectionName, out var collection)) if (!Penumbra.CollectionManager.ByName(collectionName, out var collection))
{
return PenumbraApiEc.CollectionMissing; return PenumbraApiEc.CollectionMissing;
}
if (!Penumbra.ModManager.TryGetMod(modDirectory, modName, out var mod)) if (!Penumbra.ModManager.TryGetMod(modDirectory, modName, out var mod))
{
return PenumbraApiEc.ModMissing; return PenumbraApiEc.ModMissing;
}
var groupIdx = mod.Groups.IndexOf(g => g.Name == optionGroupName); var groupIdx = mod.Groups.IndexOf(g => g.Name == optionGroupName);
if (groupIdx < 0) if (groupIdx < 0)
{
return PenumbraApiEc.OptionGroupMissing; return PenumbraApiEc.OptionGroupMissing;
}
var optionIdx = mod.Groups[groupIdx].IndexOf(o => o.Name == optionName); var optionIdx = mod.Groups[groupIdx].IndexOf(o => o.Name == optionName);
if (optionIdx < 0) if (optionIdx < 0)
{
return PenumbraApiEc.OptionMissing; return PenumbraApiEc.OptionMissing;
}
var setting = mod.Groups[groupIdx].Type == GroupType.Multi ? 1u << optionIdx : (uint)optionIdx; var setting = mod.Groups[groupIdx].Type == GroupType.Multi ? 1u << optionIdx : (uint)optionIdx;
@ -765,20 +688,14 @@ public class PenumbraApi : IDisposable, IPenumbraApi
{ {
CheckInitialized(); CheckInitialized();
if (!Penumbra.CollectionManager.ByName(collectionName, out var collection)) if (!Penumbra.CollectionManager.ByName(collectionName, out var collection))
{
return PenumbraApiEc.CollectionMissing; return PenumbraApiEc.CollectionMissing;
}
if (!Penumbra.ModManager.TryGetMod(modDirectory, modName, out var mod)) if (!Penumbra.ModManager.TryGetMod(modDirectory, modName, out var mod))
{
return PenumbraApiEc.ModMissing; return PenumbraApiEc.ModMissing;
}
var groupIdx = mod.Groups.IndexOf(g => g.Name == optionGroupName); var groupIdx = mod.Groups.IndexOf(g => g.Name == optionGroupName);
if (groupIdx < 0) if (groupIdx < 0)
{
return PenumbraApiEc.OptionGroupMissing; return PenumbraApiEc.OptionGroupMissing;
}
var group = mod.Groups[groupIdx]; var group = mod.Groups[groupIdx];
@ -787,9 +704,7 @@ public class PenumbraApi : IDisposable, IPenumbraApi
{ {
var optionIdx = optionNames.Count == 0 ? -1 : group.IndexOf(o => o.Name == optionNames[^1]); var optionIdx = optionNames.Count == 0 ? -1 : group.IndexOf(o => o.Name == optionNames[^1]);
if (optionIdx < 0) if (optionIdx < 0)
{
return PenumbraApiEc.OptionMissing; return PenumbraApiEc.OptionMissing;
}
setting = (uint)optionIdx; setting = (uint)optionIdx;
} }
@ -799,9 +714,7 @@ public class PenumbraApi : IDisposable, IPenumbraApi
{ {
var optionIdx = group.IndexOf(o => o.Name == name); var optionIdx = group.IndexOf(o => o.Name == name);
if (optionIdx < 0) if (optionIdx < 0)
{
return PenumbraApiEc.OptionMissing; return PenumbraApiEc.OptionMissing;
}
setting |= 1u << optionIdx; setting |= 1u << optionIdx;
} }
@ -815,23 +728,19 @@ public class PenumbraApi : IDisposable, IPenumbraApi
{ {
CheckInitialized(); CheckInitialized();
var sourceModIdx = Penumbra.ModManager.FirstOrDefault( m => string.Equals( m.ModPath.Name, modDirectoryFrom, StringComparison.OrdinalIgnoreCase ) )?.Index ?? -1; var sourceModIdx = Penumbra.ModManager
var targetModIdx = Penumbra.ModManager.FirstOrDefault( m => string.Equals( m.ModPath.Name, modDirectoryTo, StringComparison.OrdinalIgnoreCase ) )?.Index ?? -1; .FirstOrDefault(m => string.Equals(m.ModPath.Name, modDirectoryFrom, StringComparison.OrdinalIgnoreCase))?.Index
?? -1;
var targetModIdx = Penumbra.ModManager
.FirstOrDefault(m => string.Equals(m.ModPath.Name, modDirectoryTo, StringComparison.OrdinalIgnoreCase))?.Index
?? -1;
if (string.IsNullOrEmpty(collectionName)) if (string.IsNullOrEmpty(collectionName))
{
foreach (var collection in Penumbra.CollectionManager) foreach (var collection in Penumbra.CollectionManager)
{
collection.CopyModSettings(sourceModIdx, modDirectoryFrom, targetModIdx, modDirectoryTo); collection.CopyModSettings(sourceModIdx, modDirectoryFrom, targetModIdx, modDirectoryTo);
}
}
else if (Penumbra.CollectionManager.ByName(collectionName, out var collection)) else if (Penumbra.CollectionManager.ByName(collectionName, out var collection))
{
collection.CopyModSettings(sourceModIdx, modDirectoryFrom, targetModIdx, modDirectoryTo); collection.CopyModSettings(sourceModIdx, modDirectoryFrom, targetModIdx, modDirectoryTo);
}
else else
{
return PenumbraApiEc.CollectionMissing; return PenumbraApiEc.CollectionMissing;
}
return PenumbraApiEc.Success; return PenumbraApiEc.Success;
} }
@ -841,35 +750,25 @@ public class PenumbraApi : IDisposable, IPenumbraApi
CheckInitialized(); CheckInitialized();
if (!ActorManager.VerifyPlayerName(character.AsSpan()) || tag.Length == 0) if (!ActorManager.VerifyPlayerName(character.AsSpan()) || tag.Length == 0)
{
return (PenumbraApiEc.InvalidArgument, string.Empty); return (PenumbraApiEc.InvalidArgument, string.Empty);
}
var identifier = NameToIdentifier(character, ushort.MaxValue); var identifier = NameToIdentifier(character, ushort.MaxValue);
if (!identifier.IsValid) if (!identifier.IsValid)
{
return (PenumbraApiEc.InvalidArgument, string.Empty); return (PenumbraApiEc.InvalidArgument, string.Empty);
}
if (!forceOverwriteCharacter && Penumbra.CollectionManager.Individuals.Individuals.ContainsKey(identifier) if (!forceOverwriteCharacter && Penumbra.CollectionManager.Individuals.Individuals.ContainsKey(identifier)
|| Penumbra.TempMods.Collections.Individuals.ContainsKey( identifier ) ) || Penumbra.TempCollections.Collections.Individuals.ContainsKey(identifier))
{
return (PenumbraApiEc.CharacterCollectionExists, string.Empty); return (PenumbraApiEc.CharacterCollectionExists, string.Empty);
}
var name = $"{tag}_{character}"; var name = $"{tag}_{character}";
var ret = CreateNamedTemporaryCollection(name); var ret = CreateNamedTemporaryCollection(name);
if (ret != PenumbraApiEc.Success) if (ret != PenumbraApiEc.Success)
{
return (ret, name); return (ret, name);
}
if( Penumbra.TempMods.AddIdentifier( name, identifier ) ) if (Penumbra.TempCollections.AddIdentifier(name, identifier))
{
return (PenumbraApiEc.Success, name); return (PenumbraApiEc.Success, name);
}
Penumbra.TempMods.RemoveTemporaryCollection( name ); Penumbra.TempCollections.RemoveTemporaryCollection(name);
return (PenumbraApiEc.UnknownError, string.Empty); return (PenumbraApiEc.UnknownError, string.Empty);
} }
@ -877,11 +776,9 @@ public class PenumbraApi : IDisposable, IPenumbraApi
{ {
CheckInitialized(); CheckInitialized();
if (name.Length == 0 || Mod.Creator.ReplaceBadXivSymbols(name) != name) if (name.Length == 0 || Mod.Creator.ReplaceBadXivSymbols(name) != name)
{
return PenumbraApiEc.InvalidArgument; return PenumbraApiEc.InvalidArgument;
}
return Penumbra.TempMods.CreateTemporaryCollection( name ).Length > 0 return Penumbra.TempCollections.CreateTemporaryCollection(name).Length > 0
? PenumbraApiEc.Success ? PenumbraApiEc.Success
: PenumbraApiEc.CollectionExists; : PenumbraApiEc.CollectionExists;
} }
@ -891,29 +788,22 @@ public class PenumbraApi : IDisposable, IPenumbraApi
CheckInitialized(); CheckInitialized();
if (actorIndex < 0 || actorIndex >= DalamudServices.Objects.Length) if (actorIndex < 0 || actorIndex >= DalamudServices.Objects.Length)
{
return PenumbraApiEc.InvalidArgument; return PenumbraApiEc.InvalidArgument;
}
var identifier = Penumbra.Actors.FromObject(DalamudServices.Objects[actorIndex], false, false, true); var identifier = Penumbra.Actors.FromObject(DalamudServices.Objects[actorIndex], false, false, true);
if (!identifier.IsValid) if (!identifier.IsValid)
{
return PenumbraApiEc.InvalidArgument; return PenumbraApiEc.InvalidArgument;
}
if( !Penumbra.TempMods.CollectionByName( collectionName, out var collection ) ) if (!Penumbra.TempCollections.CollectionByName(collectionName, out var collection))
{
return PenumbraApiEc.CollectionMissing; return PenumbraApiEc.CollectionMissing;
}
if (!forceAssignment if (!forceAssignment
&& ( Penumbra.TempMods.Collections.Individuals.ContainsKey( identifier ) || Penumbra.CollectionManager.Individuals.Individuals.ContainsKey( identifier ) ) ) && (Penumbra.TempCollections.Collections.Individuals.ContainsKey(identifier)
{ || Penumbra.CollectionManager.Individuals.Individuals.ContainsKey(identifier)))
return PenumbraApiEc.CharacterCollectionExists; return PenumbraApiEc.CharacterCollectionExists;
}
var group = Penumbra.TempMods.Collections.GetGroup( identifier ); var group = Penumbra.TempCollections.Collections.GetGroup(identifier);
return Penumbra.TempMods.AddIdentifier( collection, group ) return Penumbra.TempCollections.AddIdentifier(collection, group)
? PenumbraApiEc.Success ? PenumbraApiEc.Success
: PenumbraApiEc.UnknownError; : PenumbraApiEc.UnknownError;
} }
@ -921,7 +811,7 @@ public class PenumbraApi : IDisposable, IPenumbraApi
public PenumbraApiEc RemoveTemporaryCollection(string character) public PenumbraApiEc RemoveTemporaryCollection(string character)
{ {
CheckInitialized(); CheckInitialized();
return Penumbra.TempMods.RemoveByCharacterName( character ) return Penumbra.TempCollections.RemoveByCharacterName(character)
? PenumbraApiEc.Success ? PenumbraApiEc.Success
: PenumbraApiEc.NothingChanged; : PenumbraApiEc.NothingChanged;
} }
@ -929,7 +819,7 @@ public class PenumbraApi : IDisposable, IPenumbraApi
public PenumbraApiEc RemoveTemporaryCollectionByName(string name) public PenumbraApiEc RemoveTemporaryCollectionByName(string name)
{ {
CheckInitialized(); CheckInitialized();
return Penumbra.TempMods.RemoveTemporaryCollection( name ) return Penumbra.TempCollections.RemoveTemporaryCollection(name)
? PenumbraApiEc.Success ? PenumbraApiEc.Success
: PenumbraApiEc.NothingChanged; : PenumbraApiEc.NothingChanged;
} }
@ -938,14 +828,10 @@ public class PenumbraApi : IDisposable, IPenumbraApi
{ {
CheckInitialized(); CheckInitialized();
if (!ConvertPaths(paths, out var p)) if (!ConvertPaths(paths, out var p))
{
return PenumbraApiEc.InvalidGamePath; return PenumbraApiEc.InvalidGamePath;
}
if (!ConvertManips(manipString, out var m)) if (!ConvertManips(manipString, out var m))
{
return PenumbraApiEc.InvalidManipulation; return PenumbraApiEc.InvalidManipulation;
}
return Penumbra.TempMods.Register(tag, null, p, m, priority) switch return Penumbra.TempMods.Register(tag, null, p, m, priority) switch
{ {
@ -958,21 +844,15 @@ public class PenumbraApi : IDisposable, IPenumbraApi
int priority) int priority)
{ {
CheckInitialized(); CheckInitialized();
if( !Penumbra.TempMods.CollectionByName( collectionName, out var collection ) if (!Penumbra.TempCollections.CollectionByName(collectionName, out var collection)
&& !Penumbra.CollectionManager.ByName(collectionName, out collection)) && !Penumbra.CollectionManager.ByName(collectionName, out collection))
{
return PenumbraApiEc.CollectionMissing; return PenumbraApiEc.CollectionMissing;
}
if (!ConvertPaths(paths, out var p)) if (!ConvertPaths(paths, out var p))
{
return PenumbraApiEc.InvalidGamePath; return PenumbraApiEc.InvalidGamePath;
}
if (!ConvertManips(manipString, out var m)) if (!ConvertManips(manipString, out var m))
{
return PenumbraApiEc.InvalidManipulation; return PenumbraApiEc.InvalidManipulation;
}
return Penumbra.TempMods.Register(tag, collection, p, m, priority) switch return Penumbra.TempMods.Register(tag, collection, p, m, priority) switch
{ {
@ -995,11 +875,9 @@ public class PenumbraApi : IDisposable, IPenumbraApi
public PenumbraApiEc RemoveTemporaryMod(string tag, string collectionName, int priority) public PenumbraApiEc RemoveTemporaryMod(string tag, string collectionName, int priority)
{ {
CheckInitialized(); CheckInitialized();
if( !Penumbra.TempMods.CollectionByName( collectionName, out var collection ) if (!Penumbra.TempCollections.CollectionByName(collectionName, out var collection)
&& !Penumbra.CollectionManager.ByName(collectionName, out collection)) && !Penumbra.CollectionManager.ByName(collectionName, out collection))
{
return PenumbraApiEc.CollectionMissing; return PenumbraApiEc.CollectionMissing;
}
return Penumbra.TempMods.Unregister(tag, collection, priority) switch return Penumbra.TempMods.Unregister(tag, collection, priority) switch
{ {
@ -1025,7 +903,7 @@ public class PenumbraApi : IDisposable, IPenumbraApi
{ {
CheckInitialized(); CheckInitialized();
var identifier = NameToIdentifier(characterName, worldId); var identifier = NameToIdentifier(characterName, worldId);
var collection = Penumbra.TempMods.Collections.TryGetCollection( identifier, out var c ) var collection = Penumbra.TempCollections.Collections.TryGetCollection(identifier, out var c)
? c ? c
: Penumbra.CollectionManager.Individual(identifier); : Penumbra.CollectionManager.Individual(identifier);
var set = collection.MetaCache?.Manipulations.ToArray() ?? Array.Empty<MetaManipulation>(); var set = collection.MetaCache?.Manipulations.ToArray() ?? Array.Empty<MetaManipulation>();
@ -1054,10 +932,8 @@ public class PenumbraApi : IDisposable, IPenumbraApi
private void CheckInitialized() private void CheckInitialized()
{ {
if (!Valid) if (!Valid)
{
throw new Exception("PluginShare is not initialized."); throw new Exception("PluginShare is not initialized.");
} }
}
// Return the collection associated to a current game object. If it does not exist, return the default collection. // Return the collection associated to a current game object. If it does not exist, return the default collection.
// If the index is invalid, returns false and the default collection. // If the index is invalid, returns false and the default collection.
@ -1066,16 +942,12 @@ public class PenumbraApi : IDisposable, IPenumbraApi
{ {
collection = Penumbra.CollectionManager.Default; collection = Penumbra.CollectionManager.Default;
if (gameObjectIdx < 0 || gameObjectIdx >= DalamudServices.Objects.Length) if (gameObjectIdx < 0 || gameObjectIdx >= DalamudServices.Objects.Length)
{
return false; return false;
}
var ptr = (FFXIVClientStructs.FFXIV.Client.Game.Object.GameObject*)DalamudServices.Objects.GetObjectAddress(gameObjectIdx); var ptr = (FFXIVClientStructs.FFXIV.Client.Game.Object.GameObject*)DalamudServices.Objects.GetObjectAddress(gameObjectIdx);
var data = PathResolver.IdentifyCollection(ptr, false); var data = PathResolver.IdentifyCollection(ptr, false);
if (data.Valid) if (data.Valid)
{
collection = data.ModCollection; collection = data.ModCollection;
}
return true; return true;
} }
@ -1084,9 +956,7 @@ public class PenumbraApi : IDisposable, IPenumbraApi
private static unsafe ActorIdentifier AssociatedIdentifier(int gameObjectIdx) private static unsafe ActorIdentifier AssociatedIdentifier(int gameObjectIdx)
{ {
if (gameObjectIdx < 0 || gameObjectIdx >= DalamudServices.Objects.Length) if (gameObjectIdx < 0 || gameObjectIdx >= DalamudServices.Objects.Length)
{
return ActorIdentifier.Invalid; return ActorIdentifier.Invalid;
}
var ptr = (FFXIVClientStructs.FFXIV.Client.Game.Object.GameObject*)DalamudServices.Objects.GetObjectAddress(gameObjectIdx); var ptr = (FFXIVClientStructs.FFXIV.Client.Game.Object.GameObject*)DalamudServices.Objects.GetObjectAddress(gameObjectIdx);
return Penumbra.Actors.FromObject(ptr, out _, false, true, true); return Penumbra.Actors.FromObject(ptr, out _, false, true, true);
@ -1097,9 +967,7 @@ public class PenumbraApi : IDisposable, IPenumbraApi
private static string ResolvePath(string path, Mod.Manager _, ModCollection collection) private static string ResolvePath(string path, Mod.Manager _, ModCollection collection)
{ {
if (!Penumbra.Config.EnableMods) if (!Penumbra.Config.EnableMods)
{
return path; return path;
}
var gamePath = Utf8GamePath.FromString(path, out var p, true) ? p : Utf8GamePath.Empty; var gamePath = Utf8GamePath.FromString(path, out var p, true) ? p : Utf8GamePath.Empty;
var ret = collection.ResolvePath(gamePath); var ret = collection.ResolvePath(gamePath);
@ -1113,9 +981,7 @@ public class PenumbraApi : IDisposable, IPenumbraApi
try try
{ {
if (Path.IsPathRooted(resolvedPath)) if (Path.IsPathRooted(resolvedPath))
{
return _lumina?.GetFileFromDisk<T>(resolvedPath); return _lumina?.GetFileFromDisk<T>(resolvedPath);
}
return DalamudServices.GameData.GetFile<T>(resolvedPath); return DalamudServices.GameData.GetFile<T>(resolvedPath);
} }
@ -1197,20 +1063,14 @@ public class PenumbraApi : IDisposable, IPenumbraApi
private void SubscribeToNewCollections(CollectionType type, ModCollection? oldCollection, ModCollection? newCollection, string _) private void SubscribeToNewCollections(CollectionType type, ModCollection? oldCollection, ModCollection? newCollection, string _)
{ {
if (type != CollectionType.Inactive) if (type != CollectionType.Inactive)
{
return; return;
}
if (oldCollection != null && _delegates.TryGetValue(oldCollection, out var del)) if (oldCollection != null && _delegates.TryGetValue(oldCollection, out var del))
{
oldCollection.ModSettingChanged -= del; oldCollection.ModSettingChanged -= del;
}
if (newCollection != null) if (newCollection != null)
{
SubscribeToCollection(newCollection); SubscribeToCollection(newCollection);
} }
}
public void InvokePreSettingsPanel(string modDirectory) public void InvokePreSettingsPanel(string modDirectory)
=> PreSettingsPanelDraw?.Invoke(modDirectory); => PreSettingsPanelDraw?.Invoke(modDirectory);

View file

@ -0,0 +1,119 @@
using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using Penumbra.Collections;
using Penumbra.GameData.Actors;
using Penumbra.Mods;
using Penumbra.Services;
using Penumbra.String;
namespace Penumbra.Api;
public class TempCollectionManager : IDisposable
{
public int GlobalChangeCounter { get; private set; } = 0;
public readonly IndividualCollections Collections;
private readonly CommunicatorService _communicator;
private readonly Dictionary<string, ModCollection> _customCollections = new();
public TempCollectionManager(CommunicatorService communicator, IndividualCollections collections)
{
_communicator = communicator;
Collections = collections;
_communicator.TemporaryGlobalModChange.Event += OnGlobalModChange;
}
public void Dispose()
{
_communicator.TemporaryGlobalModChange.Event -= OnGlobalModChange;
}
private void OnGlobalModChange(Mod.TemporaryMod mod, bool created, bool removed)
=> TempModManager.OnGlobalModChange(_customCollections.Values, mod, created, removed);
public int Count
=> _customCollections.Count;
public IEnumerable<ModCollection> Values
=> _customCollections.Values;
public bool CollectionByName(string name, [NotNullWhen(true)] out ModCollection? collection)
=> _customCollections.TryGetValue(name.ToLowerInvariant(), out collection);
public string CreateTemporaryCollection(string name)
{
if (Penumbra.CollectionManager.ByName(name, out _))
return string.Empty;
if (GlobalChangeCounter == int.MaxValue)
GlobalChangeCounter = 0;
var collection = ModCollection.CreateNewTemporary(name, GlobalChangeCounter++);
if (_customCollections.TryAdd(collection.Name.ToLowerInvariant(), collection))
return collection.Name;
collection.ClearCache();
return string.Empty;
}
public bool RemoveTemporaryCollection(string collectionName)
{
if (!_customCollections.Remove(collectionName.ToLowerInvariant(), out var collection))
return false;
GlobalChangeCounter += Math.Max(collection.ChangeCounter + 1 - GlobalChangeCounter, 0);
collection.ClearCache();
for (var i = 0; i < Collections.Count; ++i)
{
if (Collections[i].Collection == collection)
{
_communicator.CollectionChange.Invoke(CollectionType.Temporary, collection, null, Collections[i].DisplayName);
Collections.Delete(i);
}
}
return true;
}
public bool AddIdentifier(ModCollection collection, params ActorIdentifier[] identifiers)
{
if (Collections.Add(identifiers, collection))
{
_communicator.CollectionChange.Invoke(CollectionType.Temporary, null, collection, Collections.Last().DisplayName);
return true;
}
return false;
}
public bool AddIdentifier(string collectionName, params ActorIdentifier[] identifiers)
{
if (!_customCollections.TryGetValue(collectionName.ToLowerInvariant(), out var collection))
return false;
return AddIdentifier(collection, identifiers);
}
public bool AddIdentifier(string collectionName, string characterName, ushort worldId = ushort.MaxValue)
{
if (!ByteString.FromString(characterName, out var byteString, false))
return false;
var identifier = Penumbra.Actors.CreatePlayer(byteString, worldId);
if (!identifier.IsValid)
return false;
return AddIdentifier(collectionName, identifier);
}
internal bool RemoveByCharacterName(string characterName, ushort worldId = ushort.MaxValue)
{
if (!ByteString.FromString(characterName, out var byteString, false))
return false;
var identifier = Penumbra.Actors.CreatePlayer(byteString, worldId);
return Collections.Individuals.TryGetValue(identifier, out var collection) && RemoveTemporaryCollection(collection.Name);
}
}

View file

@ -3,10 +3,7 @@ using Penumbra.Collections;
using Penumbra.Meta.Manipulations; using Penumbra.Meta.Manipulations;
using Penumbra.Mods; using Penumbra.Mods;
using System.Collections.Generic; using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis; using Penumbra.Services;
using System.Linq;
using Penumbra.GameData.Actors;
using Penumbra.String;
using Penumbra.String.Classes; using Penumbra.String.Classes;
namespace Penumbra.Api; namespace Penumbra.Api;
@ -19,15 +16,23 @@ public enum RedirectResult
FilteredGamePath = 3, FilteredGamePath = 3,
} }
public class TempModManager public class TempModManager : IDisposable
{ {
public int GlobalChangeCounter { get; private set; } = 0; private readonly CommunicatorService _communicator;
private readonly Dictionary<ModCollection, List<Mod.TemporaryMod>> _mods = new(); private readonly Dictionary<ModCollection, List<Mod.TemporaryMod>> _mods = new();
private readonly List<Mod.TemporaryMod> _modsForAllCollections = new(); private readonly List<Mod.TemporaryMod> _modsForAllCollections = new();
private readonly Dictionary< string, ModCollection > _customCollections = new();
public readonly IndividualCollections Collections = new(Penumbra.Actors);
public event ModCollection.Manager.CollectionChangeDelegate? CollectionChanged; public TempModManager(CommunicatorService communicator)
{
_communicator = communicator;
_communicator.CollectionChange.Event += OnCollectionChange;
}
public void Dispose()
{
_communicator.CollectionChange.Event -= OnCollectionChange;
}
public IReadOnlyDictionary<ModCollection, List<Mod.TemporaryMod>> Mods public IReadOnlyDictionary<ModCollection, List<Mod.TemporaryMod>> Mods
=> _mods; => _mods;
@ -35,83 +40,6 @@ public class TempModManager
public IReadOnlyList<Mod.TemporaryMod> ModsForAllCollections public IReadOnlyList<Mod.TemporaryMod> ModsForAllCollections
=> _modsForAllCollections; => _modsForAllCollections;
public IReadOnlyDictionary< string, ModCollection > CustomCollections
=> _customCollections;
public bool CollectionByName( string name, [NotNullWhen( true )] out ModCollection? collection )
=> _customCollections.TryGetValue( name.ToLowerInvariant(), out collection );
// These functions to check specific redirections or meta manipulations for existence are currently unused.
//public bool IsRegistered( string tag, ModCollection? collection, Utf8GamePath gamePath, out FullPath? fullPath, out int priority )
//{
// var mod = GetExistingMod( tag, collection, null );
// if( mod == null )
// {
// priority = 0;
// fullPath = null;
// return false;
// }
//
// priority = mod.Priority;
// if( mod.Default.Files.TryGetValue( gamePath, out var f ) )
// {
// fullPath = f;
// return true;
// }
//
// fullPath = null;
// return false;
//}
//
//public bool IsRegistered( string tag, ModCollection? collection, MetaManipulation meta, out MetaManipulation? manipulation,
// out int priority )
//{
// var mod = GetExistingMod( tag, collection, null );
// if( mod == null )
// {
// priority = 0;
// manipulation = null;
// return false;
// }
//
// priority = mod.Priority;
// // IReadOnlySet has no TryGetValue for some reason.
// if( ( ( HashSet< MetaManipulation > )mod.Default.Manipulations ).TryGetValue( meta, out var manip ) )
// {
// manipulation = manip;
// return true;
// }
//
// manipulation = null;
// return false;
//}
// These functions for setting single redirections or manips are currently unused.
//public RedirectResult Register( string tag, ModCollection? collection, Utf8GamePath path, FullPath file, int priority )
//{
// if( Mod.FilterFile( path ) )
// {
// return RedirectResult.FilteredGamePath;
// }
//
// var mod = GetOrCreateMod( tag, collection, priority, out var created );
//
// var changes = !mod.Default.Files.TryGetValue( path, out var oldFile ) || !oldFile.Equals( file );
// mod.SetFile( path, file );
// ApplyModChange( mod, collection, created, false );
// return changes ? RedirectResult.IdenticalFileRegistered : RedirectResult.Success;
//}
//
//public RedirectResult Register( string tag, ModCollection? collection, MetaManipulation meta, int priority )
//{
// var mod = GetOrCreateMod( tag, collection, priority, out var created );
// var changes = !( ( HashSet< MetaManipulation > )mod.Default.Manipulations ).TryGetValue( meta, out var oldMeta )
// || !oldMeta.Equals( meta );
// mod.SetManipulation( meta );
// ApplyModChange( mod, collection, created, false );
// return changes ? RedirectResult.IdenticalFileRegistered : RedirectResult.Success;
//}
public RedirectResult Register(string tag, ModCollection? collection, Dictionary<Utf8GamePath, FullPath> dict, public RedirectResult Register(string tag, ModCollection? collection, Dictionary<Utf8GamePath, FullPath> dict,
HashSet<MetaManipulation> manips, int priority) HashSet<MetaManipulation> manips, int priority)
{ {
@ -125,184 +53,54 @@ public class TempModManager
{ {
var list = collection == null ? _modsForAllCollections : _mods.TryGetValue(collection, out var l) ? l : null; var list = collection == null ? _modsForAllCollections : _mods.TryGetValue(collection, out var l) ? l : null;
if (list == null) if (list == null)
{
return RedirectResult.NotRegistered; return RedirectResult.NotRegistered;
}
var removed = list.RemoveAll(m => var removed = list.RemoveAll(m =>
{ {
if (m.Name != tag || priority != null && m.Priority != priority.Value) if (m.Name != tag || priority != null && m.Priority != priority.Value)
{
return false; return false;
}
ApplyModChange(m, collection, false, true); ApplyModChange(m, collection, false, true);
return true; return true;
}); });
if (removed == 0) if (removed == 0)
{
return RedirectResult.NotRegistered; return RedirectResult.NotRegistered;
}
if (list.Count == 0 && collection != null) if (list.Count == 0 && collection != null)
{
_mods.Remove(collection); _mods.Remove(collection);
}
return RedirectResult.Success; return RedirectResult.Success;
} }
public string CreateTemporaryCollection( string name )
{
if( Penumbra.CollectionManager.ByName( name, out _ ) )
{
return string.Empty;
}
if( GlobalChangeCounter == int.MaxValue )
GlobalChangeCounter = 0;
var collection = ModCollection.CreateNewTemporary( name, GlobalChangeCounter++ );
if( _customCollections.TryAdd( collection.Name.ToLowerInvariant(), collection ) )
{
return collection.Name;
}
collection.ClearCache();
return string.Empty;
}
public bool RemoveTemporaryCollection( string collectionName )
{
if( !_customCollections.Remove( collectionName.ToLowerInvariant(), out var collection ) )
{
return false;
}
GlobalChangeCounter += Math.Max(collection.ChangeCounter + 1 - GlobalChangeCounter, 0);
_mods.Remove( collection );
collection.ClearCache();
for( var i = 0; i < Collections.Count; ++i )
{
if( Collections[ i ].Collection == collection )
{
CollectionChanged?.Invoke( CollectionType.Temporary, collection, null, Collections[ i ].DisplayName );
Collections.Delete( i );
}
}
return true;
}
public bool AddIdentifier( ModCollection collection, params ActorIdentifier[] identifiers )
{
if( Collections.Add( identifiers, collection ) )
{
CollectionChanged?.Invoke( CollectionType.Temporary, null, collection, Collections.Last().DisplayName );
return true;
}
return false;
}
public bool AddIdentifier( string collectionName, params ActorIdentifier[] identifiers )
{
if( !_customCollections.TryGetValue( collectionName.ToLowerInvariant(), out var collection ) )
{
return false;
}
return AddIdentifier( collection, identifiers );
}
public bool AddIdentifier( string collectionName, string characterName, ushort worldId = ushort.MaxValue )
{
if( !ByteString.FromString( characterName, out var byteString, false ) )
{
return false;
}
var identifier = Penumbra.Actors.CreatePlayer( byteString, worldId );
if( !identifier.IsValid )
{
return false;
}
return AddIdentifier( collectionName, identifier );
}
internal bool RemoveByCharacterName( string characterName, ushort worldId = ushort.MaxValue )
{
if( !ByteString.FromString( characterName, out var byteString, false ) )
{
return false;
}
var identifier = Penumbra.Actors.CreatePlayer( byteString, worldId );
return Collections.Individuals.TryGetValue( identifier, out var collection ) && RemoveTemporaryCollection( collection.Name );
}
// Apply any new changes to the temporary mod. // Apply any new changes to the temporary mod.
private static void ApplyModChange( Mod.TemporaryMod mod, ModCollection? collection, bool created, bool removed ) private void ApplyModChange(Mod.TemporaryMod mod, ModCollection? collection, bool created, bool removed)
{ {
if( collection == null ) if (collection != null)
{ {
if (removed) if (removed)
{
foreach( var c in Penumbra.CollectionManager )
{
c.Remove( mod );
}
}
else
{
foreach( var c in Penumbra.CollectionManager )
{
c.Apply( mod, created );
}
}
}
else
{
if( removed )
{
collection.Remove(mod); collection.Remove(mod);
}
else else
{
collection.Apply(mod, created); collection.Apply(mod, created);
} }
else
{
_communicator.TemporaryGlobalModChange.Invoke(mod, created, removed);
} }
} }
// Only find already existing mods, currently unused. /// <summary>
//private Mod.TemporaryMod? GetExistingMod( string tag, ModCollection? collection, int? priority ) /// Apply a mod change to a set of collections.
//{ /// </summary>
// var list = collection == null ? _modsForAllCollections : _mods.TryGetValue( collection, out var l ) ? l : null; public static void OnGlobalModChange(IEnumerable<ModCollection> collections, Mod.TemporaryMod mod, bool created, bool removed)
// if( list == null ) {
// { if (removed)
// return null; foreach (var c in collections)
// } c.Remove(mod);
// else
// if( priority != null ) foreach (var c in collections)
// { c.Apply(mod, created);
// return list.Find( m => m.Priority == priority.Value && m.Name == tag ); }
// }
//
// Mod.TemporaryMod? highestMod = null;
// var highestPriority = int.MinValue;
// foreach( var m in list )
// {
// if( highestPriority < m.Priority && m.Name == tag )
// {
// highestPriority = m.Priority;
// highestMod = m;
// }
// }
//
// return highestMod;
//}
// Find or create a mod with the given tag as name and the given priority, for the given collection (or all collections). // Find or create a mod with the given tag as name and the given priority, for the given collection (or all collections).
// Returns the found or created mod and whether it was newly created. // Returns the found or created mod and whether it was newly created.
@ -341,4 +139,11 @@ public class TempModManager
return mod; return mod;
} }
private void OnCollectionChange(CollectionType collectionType, ModCollection? oldCollection, ModCollection? newCollection,
string _)
{
if (collectionType is CollectionType.Temporary or CollectionType.Inactive && newCollection == null && oldCollection != null)
_mods.Remove(oldCollection);
}
} }

View file

@ -9,6 +9,7 @@ using System.IO;
using System.Linq; using System.Linq;
using System.Threading.Tasks; using System.Threading.Tasks;
using Dalamud.Interface.Internal.Notifications; using Dalamud.Interface.Internal.Notifications;
using Dalamud.Plugin;
using Penumbra.GameData.Actors; using Penumbra.GameData.Actors;
using Penumbra.Util; using Penumbra.Util;
using Penumbra.Services; using Penumbra.Services;
@ -21,9 +22,6 @@ public partial class ModCollection
{ {
public const int Version = 1; public const int Version = 1;
// Is invoked after the collections actually changed.
public event CollectionChangeDelegate CollectionChanged;
// The collection currently selected for changing settings. // The collection currently selected for changing settings.
public ModCollection Current { get; private set; } = Empty; public ModCollection Current { get; private set; } = Empty;
@ -40,6 +38,7 @@ public partial class ModCollection
private ModCollection DefaultName { get; set; } = Empty; private ModCollection DefaultName { get; set; } = Empty;
// The list of character collections. // The list of character collections.
// TODO
public readonly IndividualCollections Individuals = new(Penumbra.Actors); public readonly IndividualCollections Individuals = new(Penumbra.Actors);
public ModCollection Individual(ActorIdentifier identifier) public ModCollection Individual(ActorIdentifier identifier)
@ -56,9 +55,7 @@ public partial class ModCollection
public ModCollection? ByType(CollectionType type, ActorIdentifier identifier) public ModCollection? ByType(CollectionType type, ActorIdentifier identifier)
{ {
if (type.IsSpecial()) if (type.IsSpecial())
{
return _specialCollections[(int)type]; return _specialCollections[(int)type];
}
return type switch return type switch
{ {
@ -78,21 +75,19 @@ public partial class ModCollection
CollectionType.Default => Default.Index, CollectionType.Default => Default.Index,
CollectionType.Interface => Interface.Index, CollectionType.Interface => Interface.Index,
CollectionType.Current => Current.Index, CollectionType.Current => Current.Index,
CollectionType.Individual => individualIndex < 0 || individualIndex >= Individuals.Count ? -1 : Individuals[ individualIndex ].Collection.Index, CollectionType.Individual => individualIndex < 0 || individualIndex >= Individuals.Count
? -1
: Individuals[individualIndex].Collection.Index,
_ when collectionType.IsSpecial() => _specialCollections[(int)collectionType]?.Index ?? Default.Index, _ when collectionType.IsSpecial() => _specialCollections[(int)collectionType]?.Index ?? Default.Index,
_ => -1, _ => -1,
}; };
if (oldCollectionIdx == -1 || newIdx == oldCollectionIdx) if (oldCollectionIdx == -1 || newIdx == oldCollectionIdx)
{
return; return;
}
var newCollection = this[newIdx]; var newCollection = this[newIdx];
if (newIdx > Empty.Index) if (newIdx > Empty.Index)
{
newCollection.CreateCache(); newCollection.CreateCache();
}
switch (collectionType) switch (collectionType)
{ {
@ -127,7 +122,8 @@ public partial class ModCollection
RemoveCache(oldCollectionIdx); RemoveCache(oldCollectionIdx);
UpdateCurrentCollectionInUse(); UpdateCurrentCollectionInUse();
CollectionChanged.Invoke( collectionType, this[ oldCollectionIdx ], newCollection, collectionType == CollectionType.Individual ? Individuals[ individualIndex ].DisplayName : string.Empty ); _communicator.CollectionChange.Invoke(collectionType, this[oldCollectionIdx], newCollection,
collectionType == CollectionType.Individual ? Individuals[individualIndex].DisplayName : string.Empty);
} }
private void UpdateCurrentCollectionInUse() private void UpdateCurrentCollectionInUse()
@ -145,12 +141,10 @@ public partial class ModCollection
public bool CreateSpecialCollection(CollectionType collectionType) public bool CreateSpecialCollection(CollectionType collectionType)
{ {
if (!collectionType.IsSpecial() || _specialCollections[(int)collectionType] != null) if (!collectionType.IsSpecial() || _specialCollections[(int)collectionType] != null)
{
return false; return false;
}
_specialCollections[(int)collectionType] = Default; _specialCollections[(int)collectionType] = Default;
CollectionChanged.Invoke( collectionType, null, Default ); _communicator.CollectionChange.Invoke(collectionType, null, Default, string.Empty);
return true; return true;
} }
@ -158,15 +152,13 @@ public partial class ModCollection
public void RemoveSpecialCollection(CollectionType collectionType) public void RemoveSpecialCollection(CollectionType collectionType)
{ {
if (!collectionType.IsSpecial()) if (!collectionType.IsSpecial())
{
return; return;
}
var old = _specialCollections[(int)collectionType]; var old = _specialCollections[(int)collectionType];
if (old != null) if (old != null)
{ {
_specialCollections[(int)collectionType] = null; _specialCollections[(int)collectionType] = null;
CollectionChanged.Invoke( collectionType, old, null ); _communicator.CollectionChange.Invoke(collectionType, old, null, string.Empty);
} }
} }
@ -174,39 +166,31 @@ public partial class ModCollection
public void CreateIndividualCollection(params ActorIdentifier[] identifiers) public void CreateIndividualCollection(params ActorIdentifier[] identifiers)
{ {
if (Individuals.Add(identifiers, Default)) if (Individuals.Add(identifiers, Default))
{ _communicator.CollectionChange.Invoke(CollectionType.Individual, null, Default, Individuals.Last().DisplayName);
CollectionChanged.Invoke( CollectionType.Individual, null, Default, Individuals.Last().DisplayName );
}
} }
public void RemoveIndividualCollection(int individualIndex) public void RemoveIndividualCollection(int individualIndex)
{ {
if (individualIndex < 0 || individualIndex >= Individuals.Count) if (individualIndex < 0 || individualIndex >= Individuals.Count)
{
return; return;
}
var (name, old) = Individuals[individualIndex]; var (name, old) = Individuals[individualIndex];
if (Individuals.Delete(individualIndex)) if (Individuals.Delete(individualIndex))
{ _communicator.CollectionChange.Invoke(CollectionType.Individual, old, null, name);
CollectionChanged.Invoke( CollectionType.Individual, old, null, name );
}
} }
public void MoveIndividualCollection(int from, int to) public void MoveIndividualCollection(int from, int to)
{ {
if (Individuals.Move(from, to)) if (Individuals.Move(from, to))
{
SaveActiveCollections(); SaveActiveCollections();
} }
}
// Obtain the index of a collection by name. // Obtain the index of a collection by name.
private int GetIndexForCollectionName(string name) private int GetIndexForCollectionName(string name)
=> name.Length == 0 ? Empty.Index : _collections.IndexOf(c => c.Name == name); => name.Length == 0 ? Empty.Index : _collections.IndexOf(c => c.Name == name);
public static string ActiveCollectionFile public static string ActiveCollectionFile(DalamudPluginInterface pi)
=> Path.Combine( DalamudServices.PluginInterface.ConfigDirectory.FullName, "active_collections.json" ); => Path.Combine(pi.ConfigDirectory.FullName, "active_collections.json");
// Load default, current, special, and character collections from config. // 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. // Then create caches. If a collection does not exist anymore, reset it to an appropriate default.
@ -219,7 +203,8 @@ public partial class ModCollection
var defaultIdx = GetIndexForCollectionName(defaultName); var defaultIdx = GetIndexForCollectionName(defaultName);
if (defaultIdx < 0) if (defaultIdx < 0)
{ {
ChatUtil.NotificationMessage( $"Last choice of {ConfigWindow.DefaultCollection} {defaultName} is not available, reset to {Empty.Name}.", "Load Failure", ChatUtil.NotificationMessage(
$"Last choice of {ConfigWindow.DefaultCollection} {defaultName} is not available, reset to {Empty.Name}.", "Load Failure",
NotificationType.Warning); NotificationType.Warning);
Default = Empty; Default = Empty;
configChanged = true; configChanged = true;
@ -235,7 +220,8 @@ public partial class ModCollection
if (interfaceIdx < 0) if (interfaceIdx < 0)
{ {
ChatUtil.NotificationMessage( ChatUtil.NotificationMessage(
$"Last choice of {ConfigWindow.InterfaceCollection} {interfaceName} is not available, reset to {Empty.Name}.", "Load Failure", NotificationType.Warning ); $"Last choice of {ConfigWindow.InterfaceCollection} {interfaceName} is not available, reset to {Empty.Name}.",
"Load Failure", NotificationType.Warning);
Interface = Empty; Interface = Empty;
configChanged = true; configChanged = true;
} }
@ -250,7 +236,8 @@ public partial class ModCollection
if (currentIdx < 0) if (currentIdx < 0)
{ {
ChatUtil.NotificationMessage( ChatUtil.NotificationMessage(
$"Last choice of {ConfigWindow.SelectedCollection} {currentName} is not available, reset to {DefaultCollection}.", "Load Failure", NotificationType.Warning ); $"Last choice of {ConfigWindow.SelectedCollection} {currentName} is not available, reset to {DefaultCollection}.",
"Load Failure", NotificationType.Warning);
Current = DefaultName; Current = DefaultName;
configChanged = true; configChanged = true;
} }
@ -268,7 +255,8 @@ public partial class ModCollection
var idx = GetIndexForCollectionName(typeName); var idx = GetIndexForCollectionName(typeName);
if (idx < 0) if (idx < 0)
{ {
ChatUtil.NotificationMessage( $"Last choice of {name} Collection {typeName} is not available, removed.", "Load Failure", NotificationType.Warning ); ChatUtil.NotificationMessage($"Last choice of {name} Collection {typeName} is not available, removed.", "Load Failure",
NotificationType.Warning);
configChanged = true; configChanged = true;
} }
else else
@ -283,34 +271,28 @@ public partial class ModCollection
// Save any changes and create all required caches. // Save any changes and create all required caches.
if (configChanged) if (configChanged)
{
SaveActiveCollections(); SaveActiveCollections();
} }
}
// Migrate ungendered collections to Male and Female for 0.5.9.0. // Migrate ungendered collections to Male and Female for 0.5.9.0.
public static void MigrateUngenderedCollections() public static void MigrateUngenderedCollections(FilenameService fileNames)
{ {
if (!ReadActiveCollections(out var jObject)) if (!ReadActiveCollections(out var jObject))
{
return; return;
}
foreach (var (type, _, _) in CollectionTypeExtensions.Special.Where(t => t.Item2.StartsWith("Male "))) foreach (var (type, _, _) in CollectionTypeExtensions.Special.Where(t => t.Item2.StartsWith("Male ")))
{ {
var oldName = type.ToString()[4..]; var oldName = type.ToString()[4..];
var value = jObject[oldName]; var value = jObject[oldName];
if (value == null) if (value == null)
{
continue; continue;
}
jObject.Remove(oldName); jObject.Remove(oldName);
jObject.Add("Male" + oldName, value); jObject.Add("Male" + oldName, value);
jObject.Add("Female" + oldName, value); jObject.Add("Female" + oldName, value);
} }
using var stream = File.Open( ActiveCollectionFile, FileMode.Truncate ); using var stream = File.Open(fileNames.ActiveCollectionsFile, FileMode.Truncate);
using var writer = new StreamWriter(stream); using var writer = new StreamWriter(stream);
using var j = new JsonTextWriter(writer); using var j = new JsonTextWriter(writer);
j.Formatting = Formatting.Indented; j.Formatting = Formatting.Indented;
@ -322,9 +304,7 @@ public partial class ModCollection
{ {
var version = jObject[nameof(Version)]?.Value<int>() ?? 0; var version = jObject[nameof(Version)]?.Value<int>() ?? 0;
if (version > 0) if (version > 0)
{
return false; return false;
}
// Load character collections. If a player name comes up multiple times, the last one is applied. // Load character collections. If a player name comes up multiple times, the last one is applied.
var characters = jObject["Characters"]?.ToObject<Dictionary<string, string>>() ?? new Dictionary<string, string>(); var characters = jObject["Characters"]?.ToObject<Dictionary<string, string>>() ?? new Dictionary<string, string>();
@ -334,7 +314,8 @@ public partial class ModCollection
var idx = GetIndexForCollectionName(collectionName); var idx = GetIndexForCollectionName(collectionName);
if (idx < 0) if (idx < 0)
{ {
ChatUtil.NotificationMessage( $"Last choice of <{player}>'s Collection {collectionName} is not available, reset to {Empty.Name}.", "Load Failure", ChatUtil.NotificationMessage(
$"Last choice of <{player}>'s Collection {collectionName} is not available, reset to {Empty.Name}.", "Load Failure",
NotificationType.Warning); NotificationType.Warning);
dict.Add(player, Empty); dict.Add(player, Empty);
} }
@ -356,7 +337,8 @@ public partial class ModCollection
internal void SaveActiveCollectionsInternal() internal void SaveActiveCollectionsInternal()
{ {
var file = ActiveCollectionFile; // TODO
var file = ActiveCollectionFile(DalamudServices.PluginInterface);
try try
{ {
var jObj = new JObject var jObj = new JObject
@ -366,16 +348,14 @@ public partial class ModCollection
{ nameof(Interface), Interface.Name }, { nameof(Interface), Interface.Name },
{ nameof(Current), Current.Name }, { nameof(Current), Current.Name },
}; };
foreach( var (type, collection) in _specialCollections.WithIndex().Where( p => p.Value != null ).Select( p => ( ( CollectionType )p.Index, p.Value! ) ) ) 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(type.ToString(), collection.Name);
}
jObj.Add(nameof(Individuals), Individuals.ToJObject()); jObj.Add(nameof(Individuals), Individuals.ToJObject());
using var stream = File.Open(file, File.Exists(file) ? FileMode.Truncate : FileMode.CreateNew); using var stream = File.Open(file, File.Exists(file) ? FileMode.Truncate : FileMode.CreateNew);
using var writer = new StreamWriter(stream); using var writer = new StreamWriter(stream);
using var j = new JsonTextWriter( writer ) using var j = new JsonTextWriter(writer) { Formatting = Formatting.Indented };
{ Formatting = Formatting.Indented };
jObj.WriteTo(j); jObj.WriteTo(j);
Penumbra.Log.Verbose("Active Collections saved."); Penumbra.Log.Verbose("Active Collections saved.");
} }
@ -389,9 +369,9 @@ public partial class ModCollection
// Returns true if this is successful, false if the file does not exist or it is unsuccessful. // 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(out JObject ret)
{ {
var file = ActiveCollectionFile; // TODO
var file = ActiveCollectionFile(DalamudServices.PluginInterface);
if (File.Exists(file)) if (File.Exists(file))
{
try try
{ {
ret = JObject.Parse(File.ReadAllText(file)); ret = JObject.Parse(File.ReadAllText(file));
@ -401,7 +381,6 @@ public partial class ModCollection
{ {
Penumbra.Log.Error($"Could not read active collections from file {file}:\n{e}"); Penumbra.Log.Error($"Could not read active collections from file {file}:\n{e}");
} }
}
ret = new JObject(); ret = new JObject();
return false; return false;
@ -410,11 +389,9 @@ public partial class ModCollection
// Save if any of the active collections is changed. // Save if any of the active collections is changed.
private void SaveOnChange(CollectionType collectionType, ModCollection? _1, ModCollection? _2, string _3) private void SaveOnChange(CollectionType collectionType, ModCollection? _1, ModCollection? _2, string _3)
{ {
if( collectionType != CollectionType.Inactive ) if (collectionType is not CollectionType.Inactive and not CollectionType.Temporary)
{
SaveActiveCollections(); SaveActiveCollections();
} }
}
// Cache handling. Usually recreate caches on the next framework tick, // Cache handling. Usually recreate caches on the next framework tick,
// but at launch create all of them at once. // but at launch create all of them at once.
@ -440,34 +417,26 @@ public partial class ModCollection
&& idx != Current.Index && idx != Current.Index
&& _specialCollections.All(c => c == null || c.Index != idx) && _specialCollections.All(c => c == null || c.Index != idx)
&& Individuals.Select(p => p.Collection).All(c => c.Index != idx)) && Individuals.Select(p => p.Collection).All(c => c.Index != idx))
{
_collections[idx].ClearCache(); _collections[idx].ClearCache();
} }
}
// Recalculate effective files for active collections on events. // Recalculate effective files for active collections on events.
private void OnModAddedActive(Mod mod) private void OnModAddedActive(Mod mod)
{ {
foreach (var collection in this.Where(c => c.HasCache && c[mod.Index].Settings?.Enabled == true)) foreach (var collection in this.Where(c => c.HasCache && c[mod.Index].Settings?.Enabled == true))
{
collection._cache!.AddMod(mod, true); collection._cache!.AddMod(mod, true);
} }
}
private void OnModRemovedActive(Mod mod) private void OnModRemovedActive(Mod mod)
{ {
foreach (var collection in this.Where(c => c.HasCache && c[mod.Index].Settings?.Enabled == true)) foreach (var collection in this.Where(c => c.HasCache && c[mod.Index].Settings?.Enabled == true))
{
collection._cache!.RemoveMod(mod, true); collection._cache!.RemoveMod(mod, true);
} }
}
private void OnModMovedActive(Mod mod) private void OnModMovedActive(Mod mod)
{ {
foreach (var collection in this.Where(c => c.HasCache && c[mod.Index].Settings?.Enabled == true)) foreach (var collection in this.Where(c => c.HasCache && c[mod.Index].Settings?.Enabled == true))
{
collection._cache!.ReloadMod(mod, true); collection._cache!.ReloadMod(mod, true);
} }
} }
} }
}

View file

@ -7,6 +7,8 @@ using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis; using System.Diagnostics.CodeAnalysis;
using System.IO; using System.IO;
using System.Linq; using System.Linq;
using Penumbra.Api;
using Penumbra.Services;
namespace Penumbra.Collections; namespace Penumbra.Collections;
@ -14,12 +16,8 @@ public partial class ModCollection
{ {
public sealed partial class Manager : IDisposable, IEnumerable<ModCollection> public sealed partial class Manager : IDisposable, IEnumerable<ModCollection>
{ {
// On addition, oldCollection is null. On deletion, newCollection is null.
// displayName is only set for type == Individual.
public delegate void CollectionChangeDelegate( CollectionType collectionType, ModCollection? oldCollection,
ModCollection? newCollection, string displayName = "" );
private readonly Mod.Manager _modManager; private readonly Mod.Manager _modManager;
private readonly CommunicatorService _communicator;
// The empty collection is always available and always has index 0. // The empty collection is always available and always has index 0.
// It can not be deleted or moved. // It can not be deleted or moved.
@ -51,8 +49,9 @@ public partial class ModCollection
public IEnumerable<ModCollection> GetEnumeratorWithEmpty() public IEnumerable<ModCollection> GetEnumeratorWithEmpty()
=> _collections; => _collections;
public Manager( Mod.Manager manager ) public Manager(CommunicatorService communicator, Mod.Manager manager)
{ {
_communicator = communicator;
_modManager = manager; _modManager = manager;
// The collection manager reacts to changes in mods by itself. // The collection manager reacts to changes in mods by itself.
@ -60,7 +59,8 @@ public partial class ModCollection
_modManager.ModDiscoveryFinished += OnModDiscoveryFinished; _modManager.ModDiscoveryFinished += OnModDiscoveryFinished;
_modManager.ModOptionChanged += OnModOptionsChanged; _modManager.ModOptionChanged += OnModOptionsChanged;
_modManager.ModPathChanged += OnModPathChange; _modManager.ModPathChanged += OnModPathChange;
CollectionChanged += SaveOnChange; _communicator.CollectionChange.Event += SaveOnChange;
_communicator.TemporaryGlobalModChange.Event += OnGlobalModChange;
ReadCollections(); ReadCollections();
LoadCollections(); LoadCollections();
UpdateCurrentCollectionInUse(); UpdateCurrentCollectionInUse();
@ -68,12 +68,17 @@ public partial class ModCollection
public void Dispose() public void Dispose()
{ {
_communicator.CollectionChange.Event -= SaveOnChange;
_communicator.TemporaryGlobalModChange.Event -= OnGlobalModChange;
_modManager.ModDiscoveryStarted -= OnModDiscoveryStarted; _modManager.ModDiscoveryStarted -= OnModDiscoveryStarted;
_modManager.ModDiscoveryFinished -= OnModDiscoveryFinished; _modManager.ModDiscoveryFinished -= OnModDiscoveryFinished;
_modManager.ModOptionChanged -= OnModOptionsChanged; _modManager.ModOptionChanged -= OnModOptionsChanged;
_modManager.ModPathChanged -= OnModPathChange; _modManager.ModPathChanged -= OnModPathChange;
} }
private void OnGlobalModChange(Mod.TemporaryMod mod, bool created, bool removed)
=> TempModManager.OnGlobalModChange(_collections, mod, created, removed);
// Returns true if the name is not empty, it is not the name of the empty collection // 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. // and no existing collection results in the same filename as name.
public bool CanAddCollection(string name, out string fixedName) public bool CanAddCollection(string name, out string fixedName)
@ -115,7 +120,7 @@ public partial class ModCollection
_collections.Add(newCollection); _collections.Add(newCollection);
newCollection.Save(); newCollection.Save();
Penumbra.Log.Debug($"Added collection {newCollection.AnonymizedName}."); Penumbra.Log.Debug($"Added collection {newCollection.AnonymizedName}.");
CollectionChanged.Invoke( CollectionType.Inactive, null, newCollection ); _communicator.CollectionChange.Invoke(CollectionType.Inactive, null, newCollection, string.Empty);
SetCollection(newCollection.Index, CollectionType.Current); SetCollection(newCollection.Index, CollectionType.Current);
return true; return true;
} }
@ -138,38 +143,28 @@ public partial class ModCollection
} }
if (idx == Current.Index) if (idx == Current.Index)
{
SetCollection(DefaultName.Index, CollectionType.Current); SetCollection(DefaultName.Index, CollectionType.Current);
}
if (idx == Default.Index) if (idx == Default.Index)
{
SetCollection(Empty.Index, CollectionType.Default); SetCollection(Empty.Index, CollectionType.Default);
}
for (var i = 0; i < _specialCollections.Length; ++i) for (var i = 0; i < _specialCollections.Length; ++i)
{ {
if (idx == _specialCollections[i]?.Index) if (idx == _specialCollections[i]?.Index)
{
SetCollection(Empty, (CollectionType)i); SetCollection(Empty, (CollectionType)i);
} }
}
for (var i = 0; i < Individuals.Count; ++i) for (var i = 0; i < Individuals.Count; ++i)
{ {
if (Individuals[i].Collection.Index == idx) if (Individuals[i].Collection.Index == idx)
{
SetCollection(Empty, CollectionType.Individual, i); SetCollection(Empty, CollectionType.Individual, i);
} }
}
var collection = _collections[idx]; var collection = _collections[idx];
// Clear own inheritances. // Clear own inheritances.
foreach (var inheritance in collection.Inheritance) foreach (var inheritance in collection.Inheritance)
{
collection.ClearSubscriptions(inheritance); collection.ClearSubscriptions(inheritance);
}
collection.Delete(); collection.Delete();
_collections.RemoveAt(idx); _collections.RemoveAt(idx);
@ -179,18 +174,14 @@ public partial class ModCollection
{ {
var inheritedIdx = c._inheritance.IndexOf(collection); var inheritedIdx = c._inheritance.IndexOf(collection);
if (inheritedIdx >= 0) if (inheritedIdx >= 0)
{
c.RemoveInheritance(inheritedIdx); c.RemoveInheritance(inheritedIdx);
}
if (c.Index > idx) if (c.Index > idx)
{
--c.Index; --c.Index;
} }
}
Penumbra.Log.Debug($"Removed collection {collection.AnonymizedName}."); Penumbra.Log.Debug($"Removed collection {collection.AnonymizedName}.");
CollectionChanged.Invoke( CollectionType.Inactive, collection, null ); _communicator.CollectionChange.Invoke(CollectionType.Inactive, collection, null, string.Empty);
return true; return true;
} }
@ -200,25 +191,19 @@ public partial class ModCollection
private void OnModDiscoveryStarted() private void OnModDiscoveryStarted()
{ {
foreach (var collection in this) foreach (var collection in this)
{
collection.PrepareModDiscovery(); collection.PrepareModDiscovery();
} }
}
private void OnModDiscoveryFinished() private void OnModDiscoveryFinished()
{ {
// First, re-apply all mod settings. // First, re-apply all mod settings.
foreach (var collection in this) foreach (var collection in this)
{
collection.ApplyModSettings(); collection.ApplyModSettings();
}
// Afterwards, we update the caches. This can not happen in the same loop due to inheritance. // Afterwards, we update the caches. This can not happen in the same loop due to inheritance.
foreach (var collection in this.Where(c => c.HasCache)) foreach (var collection in this.Where(c => c.HasCache))
{
collection.ForceCacheUpdate(); collection.ForceCacheUpdate();
} }
}
// A changed mod path forces changes for all collections, active and inactive. // A changed mod path forces changes for all collections, active and inactive.
@ -229,26 +214,20 @@ public partial class ModCollection
{ {
case ModPathChangeType.Added: case ModPathChangeType.Added:
foreach (var collection in this) foreach (var collection in this)
{
collection.AddMod(mod); collection.AddMod(mod);
}
OnModAddedActive(mod); OnModAddedActive(mod);
break; break;
case ModPathChangeType.Deleted: case ModPathChangeType.Deleted:
OnModRemovedActive(mod); OnModRemovedActive(mod);
foreach (var collection in this) foreach (var collection in this)
{
collection.RemoveMod(mod, mod.Index); collection.RemoveMod(mod, mod.Index);
}
break; break;
case ModPathChangeType.Moved: case ModPathChangeType.Moved:
OnModMovedActive(mod); OnModMovedActive(mod);
foreach (var collection in this.Where(collection => collection.Settings[mod.Index] != null)) foreach (var collection in this.Where(collection => collection.Settings[mod.Index] != null))
{
collection.Save(); collection.Save();
}
break; break;
case ModPathChangeType.StartingReload: case ModPathChangeType.StartingReload:
@ -272,10 +251,8 @@ public partial class ModCollection
foreach (var collection in this.Where(c => c.HasCache)) foreach (var collection in this.Where(c => c.HasCache))
{ {
if (collection[mod.Index].Settings is { Enabled: true }) if (collection[mod.Index].Settings is { Enabled: true })
{
collection._cache!.RemoveMod(mod, false); collection._cache!.RemoveMod(mod, false);
} }
}
return; return;
} }
@ -284,36 +261,26 @@ public partial class ModCollection
// Handle changes that require overwriting the collection. // Handle changes that require overwriting the collection.
if (requiresSaving) if (requiresSaving)
{
foreach (var collection in this) foreach (var collection in this)
{ {
if (collection._settings[mod.Index]?.HandleChanges(type, mod, groupIdx, optionIdx, movedToIdx) ?? false) if (collection._settings[mod.Index]?.HandleChanges(type, mod, groupIdx, optionIdx, movedToIdx) ?? false)
{
collection.Save(); collection.Save();
} }
}
}
// Handle changes that reload the mod if the changes did not need to be prepared, // Handle changes that reload the mod if the changes did not need to be prepared,
// or re-add the mod if they were prepared. // or re-add the mod if they were prepared.
if (recomputeList) if (recomputeList)
{
foreach (var collection in this.Where(c => c.HasCache)) foreach (var collection in this.Where(c => c.HasCache))
{ {
if (collection[mod.Index].Settings is { Enabled: true }) if (collection[mod.Index].Settings is { Enabled: true })
{ {
if (reload) if (reload)
{
collection._cache!.ReloadMod(mod, true); collection._cache!.ReloadMod(mod, true);
}
else else
{
collection._cache!.AddMod(mod, true); collection._cache!.AddMod(mod, true);
} }
} }
} }
}
}
// Add the collection with the default name if it does not exist. // Add the collection with the default name if it does not exist.
// It should always be ensured that it exists, otherwise it will be created. // It should always be ensured that it exists, otherwise it will be created.
@ -355,33 +322,27 @@ public partial class ModCollection
} }
if (changes) if (changes)
{
collection.Save(); collection.Save();
} }
} }
}
// Read all collection files in the Collection Directory. // Read all collection files in the Collection Directory.
// Ensure that the default named collection exists, and apply inheritances afterwards. // Ensure that the default named collection exists, and apply inheritances afterwards.
// Duplicate collection files are not deleted, just not added here. // Duplicate collection files are not deleted, just not added here.
private void ReadCollections() private void ReadCollections()
{ {
var collectionDir = new DirectoryInfo( CollectionDirectory ); // TODO
var collectionDir = new DirectoryInfo(CollectionDirectory(DalamudServices.PluginInterface));
var inheritances = new List<IReadOnlyList<string>>(); var inheritances = new List<IReadOnlyList<string>>();
if (collectionDir.Exists) if (collectionDir.Exists)
{
foreach (var file in collectionDir.EnumerateFiles("*.json")) foreach (var file in collectionDir.EnumerateFiles("*.json"))
{ {
var collection = LoadFromFile(file, out var inheritance); var collection = LoadFromFile(file, out var inheritance);
if (collection == null || collection.Name.Length == 0) if (collection == null || collection.Name.Length == 0)
{
continue; continue;
}
if (file.Name != $"{collection.Name.RemoveInvalidPathSymbols()}.json") if (file.Name != $"{collection.Name.RemoveInvalidPathSymbols()}.json")
{
Penumbra.Log.Warning($"Collection {file.Name} does not correspond to {collection.Name}."); Penumbra.Log.Warning($"Collection {file.Name} does not correspond to {collection.Name}.");
}
if (this[collection.Name] != null) if (this[collection.Name] != null)
{ {
@ -394,7 +355,6 @@ public partial class ModCollection
_collections.Add(collection); _collections.Add(collection);
} }
} }
}
AddDefaultCollection(); AddDefaultCollection();
ApplyInheritances(inheritances); ApplyInheritances(inheritances);

View file

@ -4,6 +4,7 @@ using System.Linq;
using Dalamud.Game.ClientState.Objects.Enums; using Dalamud.Game.ClientState.Objects.Enums;
using OtterGui.Filesystem; using OtterGui.Filesystem;
using Penumbra.GameData.Actors; using Penumbra.GameData.Actors;
using Penumbra.Services;
using Penumbra.String; using Penumbra.String;
namespace Penumbra.Collections; namespace Penumbra.Collections;
@ -20,6 +21,10 @@ public sealed partial class IndividualCollections
public IReadOnlyDictionary< ActorIdentifier, ModCollection > Individuals public IReadOnlyDictionary< ActorIdentifier, ModCollection > Individuals
=> _individuals; => _individuals;
// TODO
public IndividualCollections( ActorService actorManager )
=> _actorManager = actorManager.AwaitedService;
public IndividualCollections(ActorManager actorManager) public IndividualCollections(ActorManager actorManager)
=> _actorManager = actorManager; => _actorManager = actorManager;

View file

@ -8,18 +8,20 @@ using System.Collections.Generic;
using System.IO; using System.IO;
using System.Linq; using System.Linq;
using System.Text; using System.Text;
using Dalamud.Plugin;
namespace Penumbra.Collections; namespace Penumbra.Collections;
// File operations like saving, loading and deleting for a collection. // File operations like saving, loading and deleting for a collection.
public partial class ModCollection public partial class ModCollection
{ {
public static string CollectionDirectory public static string CollectionDirectory(DalamudPluginInterface pi)
=> Path.Combine( DalamudServices.PluginInterface.GetPluginConfigDirectory(), "collections" ); => 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. // We need to remove all invalid path symbols from the collection name to be able to save it to file.
// TODO
public FileInfo FileName public FileInfo FileName
=> new(Path.Combine( CollectionDirectory, $"{Name.RemoveInvalidPathSymbols()}.json" )); => new(Path.Combine( CollectionDirectory(DalamudServices.PluginInterface), $"{Name.RemoveInvalidPathSymbols()}.json" ));
// Custom serialization due to shared mod information across managers. // Custom serialization due to shared mod information across managers.
private void SaveCollection() private void SaveCollection()

View file

@ -90,7 +90,7 @@ public partial class ModCollection
var collection = new ModCollection( name, Empty ); var collection = new ModCollection( name, Empty );
collection.ModSettingChanged -= collection.SaveOnChange; collection.ModSettingChanged -= collection.SaveOnChange;
collection.InheritanceChanged -= collection.SaveOnChange; collection.InheritanceChanged -= collection.SaveOnChange;
collection.Index = ~Penumbra.TempMods.Collections.Count; collection.Index = ~Penumbra.TempCollections.Count;
collection.ChangeCounter = changeCounter; collection.ChangeCounter = changeCounter;
collection.CreateCache(); collection.CreateCache();
return collection; return collection;

View file

@ -1,370 +0,0 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using OtterGui.Filesystem;
using Penumbra.Collections;
using Penumbra.Mods;
using Penumbra.Services;
namespace Penumbra;
public partial class Configuration
{
// Contains everything to migrate from older versions of the config to the current,
// including deprecated fields.
private class Migration
{
private Configuration _config = null!;
private JObject _data = null!;
public string CurrentCollection = ModCollection.DefaultCollection;
public string DefaultCollection = ModCollection.DefaultCollection;
public string ForcedCollection = string.Empty;
public Dictionary< string, string > CharacterCollections = new();
public Dictionary< string, string > ModSortOrder = new();
public bool InvertModListOrder;
public bool SortFoldersFirst;
public SortModeV3 SortMode = SortModeV3.FoldersFirst;
public static void Migrate( Configuration config )
{
if( !File.Exists( DalamudServices.PluginInterface.ConfigFile.FullName ) )
{
return;
}
var m = new Migration
{
_config = config,
_data = JObject.Parse( File.ReadAllText( DalamudServices.PluginInterface.ConfigFile.FullName ) ),
};
CreateBackup();
m.Version0To1();
m.Version1To2();
m.Version2To3();
m.Version3To4();
m.Version4To5();
m.Version5To6();
m.Version6To7();
}
// Gendered special collections were added.
private void Version6To7()
{
if( _config.Version != 6 )
return;
ModCollection.Manager.MigrateUngenderedCollections();
_config.Version = 7;
}
// A new tutorial step was inserted in the middle.
// The UI collection and a new tutorial for it was added.
// The migration for the UI collection itself happens in the ActiveCollections file.
private void Version5To6()
{
if( _config.Version != 5 )
{
return;
}
if( _config.TutorialStep == 25 )
{
_config.TutorialStep = 27;
}
_config.Version = 6;
}
// Mod backup extension was changed from .zip to .pmp.
// Actual migration takes place in ModManager.
private void Version4To5()
{
if( _config.Version != 4 )
{
return;
}
Mod.Manager.MigrateModBackups = true;
_config.Version = 5;
}
// SortMode was changed from an enum to a type.
private void Version3To4()
{
if( _config.Version != 3 )
{
return;
}
SortMode = _data[ nameof( SortMode ) ]?.ToObject< SortModeV3 >() ?? SortMode;
_config.SortMode = SortMode switch
{
SortModeV3.FoldersFirst => ISortMode< Mod >.FoldersFirst,
SortModeV3.Lexicographical => ISortMode< Mod >.Lexicographical,
SortModeV3.InverseFoldersFirst => ISortMode< Mod >.InverseFoldersFirst,
SortModeV3.InverseLexicographical => ISortMode< Mod >.InverseLexicographical,
SortModeV3.FoldersLast => ISortMode< Mod >.FoldersLast,
SortModeV3.InverseFoldersLast => ISortMode< Mod >.InverseFoldersLast,
SortModeV3.InternalOrder => ISortMode< Mod >.InternalOrder,
SortModeV3.InternalOrderInverse => ISortMode< Mod >.InverseInternalOrder,
_ => ISortMode< Mod >.FoldersFirst,
};
_config.Version = 4;
}
// SortFoldersFirst was changed from a bool to the enum SortMode.
private void Version2To3()
{
if( _config.Version != 2 )
{
return;
}
SortFoldersFirst = _data[ nameof( SortFoldersFirst ) ]?.ToObject< bool >() ?? false;
SortMode = SortFoldersFirst ? SortModeV3.FoldersFirst : SortModeV3.Lexicographical;
_config.Version = 3;
}
// The forced collection was removed due to general inheritance.
// Sort Order was moved to a separate file and may contain empty folders.
// Active collections in general were moved to their own file.
// Delete the penumbrametatmp folder if it exists.
private void Version1To2()
{
if( _config.Version != 1 )
{
return;
}
// Ensure the right meta files are loaded.
DeleteMetaTmp();
Penumbra.CharacterUtility.LoadCharacterResources();
ResettleSortOrder();
ResettleCollectionSettings();
ResettleForcedCollection();
_config.Version = 2;
}
private void DeleteMetaTmp()
{
var path = Path.Combine( _config.ModDirectory, "penumbrametatmp" );
if( Directory.Exists( path ) )
{
try
{
Directory.Delete( path, true );
}
catch( Exception e )
{
Penumbra.Log.Error( $"Could not delete the outdated penumbrametatmp folder:\n{e}" );
}
}
}
private void ResettleForcedCollection()
{
ForcedCollection = _data[ nameof( ForcedCollection ) ]?.ToObject< string >() ?? ForcedCollection;
if( ForcedCollection.Length <= 0 )
{
return;
}
// Add the previous forced collection to all current collections except itself as an inheritance.
foreach( var collection in Directory.EnumerateFiles( ModCollection.CollectionDirectory, "*.json" ) )
{
try
{
var jObject = JObject.Parse( File.ReadAllText( collection ) );
if( jObject[ nameof( ModCollection.Name ) ]?.ToObject< string >() != ForcedCollection )
{
jObject[ nameof( ModCollection.Inheritance ) ] = JToken.FromObject( new List< string >() { ForcedCollection } );
File.WriteAllText( collection, jObject.ToString() );
}
}
catch( Exception e )
{
Penumbra.Log.Error(
$"Could not transfer forced collection {ForcedCollection} to inheritance of collection {collection}:\n{e}" );
}
}
}
// Move the current sort order to its own file.
private void ResettleSortOrder()
{
ModSortOrder = _data[ nameof( ModSortOrder ) ]?.ToObject< Dictionary< string, string > >() ?? ModSortOrder;
var file = ModFileSystem.ModFileSystemFile;
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 );
j.Formatting = Formatting.Indented;
j.WriteStartObject();
j.WritePropertyName( "Data" );
j.WriteStartObject();
foreach( var (mod, path) in ModSortOrder.Where( kvp => Directory.Exists( Path.Combine( _config.ModDirectory, kvp.Key ) ) ) )
{
j.WritePropertyName( mod, true );
j.WriteValue( path );
}
j.WriteEndObject();
j.WritePropertyName( "EmptyFolders" );
j.WriteStartArray();
j.WriteEndArray();
j.WriteEndObject();
}
// Move the active collections to their own file.
private void ResettleCollectionSettings()
{
CurrentCollection = _data[ nameof( CurrentCollection ) ]?.ToObject< string >() ?? CurrentCollection;
DefaultCollection = _data[ nameof( DefaultCollection ) ]?.ToObject< string >() ?? DefaultCollection;
CharacterCollections = _data[ nameof( CharacterCollections ) ]?.ToObject< Dictionary< string, string > >() ?? CharacterCollections;
SaveActiveCollectionsV0( DefaultCollection, CurrentCollection, DefaultCollection,
CharacterCollections.Select( kvp => ( kvp.Key, kvp.Value ) ), Array.Empty< (CollectionType, string) >() );
}
// Outdated saving using the Characters list.
private static void SaveActiveCollectionsV0( string def, string ui, string current, IEnumerable<(string, string)> characters,
IEnumerable<(CollectionType, string)> special )
{
var file = ModCollection.Manager.ActiveCollectionFile;
try
{
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 );
j.Formatting = Formatting.Indented;
j.WriteStartObject();
j.WritePropertyName( nameof( ModCollection.Manager.Default ) );
j.WriteValue( def );
j.WritePropertyName( nameof( ModCollection.Manager.Interface ) );
j.WriteValue( ui );
j.WritePropertyName( nameof( ModCollection.Manager.Current ) );
j.WriteValue( current );
foreach( var (type, collection) in special )
{
j.WritePropertyName( type.ToString() );
j.WriteValue( collection );
}
j.WritePropertyName( "Characters" );
j.WriteStartObject();
foreach( var (character, collection) in characters )
{
j.WritePropertyName( character, true );
j.WriteValue( collection );
}
j.WriteEndObject();
j.WriteEndObject();
Penumbra.Log.Verbose( "Active Collections saved." );
}
catch( Exception e )
{
Penumbra.Log.Error( $"Could not save active collections to file {file}:\n{e}" );
}
}
// Collections were introduced and the previous CurrentCollection got put into ModDirectory.
private void Version0To1()
{
if( _config.Version != 0 )
{
return;
}
_config.ModDirectory = _data[ nameof( CurrentCollection ) ]?.ToObject< string >() ?? string.Empty;
_config.Version = 1;
ResettleCollectionJson();
}
// Move the previous mod configurations to a new default collection file.
private void ResettleCollectionJson()
{
var collectionJson = new FileInfo( Path.Combine( _config.ModDirectory, "collection.json" ) );
if( !collectionJson.Exists )
{
return;
}
var defaultCollection = ModCollection.CreateNewEmpty( ModCollection.DefaultCollection );
var defaultCollectionFile = defaultCollection.FileName;
if( defaultCollectionFile.Exists )
{
return;
}
try
{
var text = File.ReadAllText( collectionJson.FullName );
var data = JArray.Parse( text );
var maxPriority = 0;
var dict = new Dictionary< string, ModSettings.SavedSettings >();
foreach( var setting in data.Cast< JObject >() )
{
var modName = ( string )setting[ "FolderName" ]!;
var enabled = ( bool )setting[ "Enabled" ]!;
var priority = ( int )setting[ "Priority" ]!;
var settings = setting[ "Settings" ]!.ToObject< Dictionary< string, long > >()
?? setting[ "Conf" ]!.ToObject< Dictionary< string, long > >();
dict[ modName ] = new ModSettings.SavedSettings()
{
Enabled = enabled,
Priority = priority,
Settings = settings!,
};
maxPriority = Math.Max( maxPriority, priority );
}
InvertModListOrder = _data[ nameof( InvertModListOrder ) ]?.ToObject< bool >() ?? InvertModListOrder;
if( !InvertModListOrder )
{
dict = dict.ToDictionary( kvp => kvp.Key, kvp => kvp.Value with { Priority = maxPriority - kvp.Value.Priority } );
}
defaultCollection = ModCollection.MigrateFromV0( ModCollection.DefaultCollection, dict );
defaultCollection.Save();
}
catch( Exception e )
{
Penumbra.Log.Error( $"Could not migrate the old collection file to new collection files:\n{e}" );
throw;
}
}
// Create a backup of the configuration file specifically.
private static void CreateBackup()
{
var name = DalamudServices.PluginInterface.ConfigFile.FullName;
var bakName = name + ".bak";
try
{
File.Copy( name, bakName, true );
}
catch( Exception e )
{
Penumbra.Log.Error( $"Could not create backup copy of config at {bakName}:\n{e}" );
}
}
public enum SortModeV3 : byte
{
FoldersFirst = 0x00,
Lexicographical = 0x01,
InverseFoldersFirst = 0x02,
InverseLexicographical = 0x03,
FoldersLast = 0x04,
InverseFoldersLast = 0x05,
InternalOrder = 0x06,
InternalOrderInverse = 0x07,
}
}
}

View file

@ -19,8 +19,14 @@ using ErrorEventArgs = Newtonsoft.Json.Serialization.ErrorEventArgs;
namespace Penumbra; namespace Penumbra;
[Serializable] [Serializable]
public partial class Configuration : IPluginConfiguration public class Configuration : IPluginConfiguration
{ {
[JsonIgnore]
private readonly string _fileName;
[JsonIgnore]
private readonly FrameworkManager _framework;
public int Version { get; set; } = Constants.CurrentVersion; public int Version { get; set; } = Constants.CurrentVersion;
public int LastSeenVersion { get; set; } = ConfigWindow.LastChangelogVersion; public int LastSeenVersion { get; set; } = ConfigWindow.LastChangelogVersion;
@ -86,47 +92,44 @@ public partial class Configuration : IPluginConfiguration
public Dictionary< ColorId, uint > Colors { get; set; } public Dictionary< ColorId, uint > Colors { get; set; }
= Enum.GetValues< ColorId >().ToDictionary( c => c, c => c.Data().DefaultColor ); = Enum.GetValues< ColorId >().ToDictionary( c => c, c => c.Data().DefaultColor );
// Load the current configuration. /// <summary>
// Includes adding new colors and migrating from old versions. /// Load the current configuration.
public static Configuration Load() /// Includes adding new colors and migrating from old versions.
/// </summary>
public Configuration(FilenameService fileNames, ConfigMigrationService migrator, FrameworkManager framework)
{ {
void HandleDeserializationError( object? sender, ErrorEventArgs errorArgs ) _fileName = fileNames.ConfigFile;
_framework = framework;
Load(migrator);
}
public void Load(ConfigMigrationService migrator)
{
static void HandleDeserializationError(object? sender, ErrorEventArgs errorArgs)
{ {
Penumbra.Log.Error( Penumbra.Log.Error(
$"Error parsing Configuration at {errorArgs.ErrorContext.Path}, using default or migrating:\n{errorArgs.ErrorContext.Error}"); $"Error parsing Configuration at {errorArgs.ErrorContext.Path}, using default or migrating:\n{errorArgs.ErrorContext.Error}");
errorArgs.ErrorContext.Handled = true; errorArgs.ErrorContext.Handled = true;
} }
Configuration? configuration = null; if (File.Exists(_fileName))
if( File.Exists( DalamudServices.PluginInterface.ConfigFile.FullName ) )
{ {
var text = File.ReadAllText( DalamudServices.PluginInterface.ConfigFile.FullName ); var text = File.ReadAllText(_fileName);
configuration = JsonConvert.DeserializeObject< Configuration >( text, new JsonSerializerSettings JsonConvert.PopulateObject(text, this, new JsonSerializerSettings
{ {
Error = HandleDeserializationError, Error = HandleDeserializationError,
}); });
} }
migrator.Migrate(this);
configuration ??= new Configuration();
if( configuration.Version == Constants.CurrentVersion )
{
configuration.AddColors( false );
return configuration;
} }
Migration.Migrate( configuration ); /// <summary> Save the current configuration. </summary>
configuration.AddColors( true );
return configuration;
}
// Save the current configuration.
private void SaveConfiguration() private void SaveConfiguration()
{ {
try try
{ {
var text = JsonConvert.SerializeObject( this, Formatting.Indented ); var text = JsonConvert.SerializeObject( this, Formatting.Indented );
File.WriteAllText( DalamudServices.PluginInterface.ConfigFile.FullName, text ); File.WriteAllText( _fileName, text );
} }
catch( Exception e ) catch( Exception e )
{ {
@ -135,24 +138,9 @@ public partial class Configuration : IPluginConfiguration
} }
public void Save() public void Save()
=> Penumbra.Framework.RegisterDelayed( nameof( SaveConfiguration ), SaveConfiguration ); => _framework.RegisterDelayed( nameof( SaveConfiguration ), SaveConfiguration );
// Add missing colors to the dictionary if necessary. /// <summary> Contains some default values or boundaries for config values. </summary>
private void AddColors( bool forceSave )
{
var save = false;
foreach( var color in Enum.GetValues< ColorId >() )
{
save |= Colors.TryAdd( color, color.Data().DefaultColor );
}
if( save || forceSave )
{
Save();
}
}
// Contains some default values or boundaries for config values.
public static class Constants public static class Constants
{ {
public const int CurrentVersion = 7; public const int CurrentVersion = 7;
@ -178,6 +166,7 @@ public partial class Configuration : IPluginConfiguration
}; };
} }
/// <summary> Convert SortMode Types to their name. </summary>
private class SortModeConverter : JsonConverter< ISortMode< Mod > > private class SortModeConverter : JsonConverter< ISortMode< Mod > >
{ {
public override void WriteJson( JsonWriter writer, ISortMode< Mod >? value, JsonSerializer serializer ) public override void WriteJson( JsonWriter writer, ISortMode< Mod >? value, JsonSerializer serializer )

View file

@ -1,9 +1,9 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using Dalamud.Game;
using Dalamud.Utility.Signatures; using Dalamud.Utility.Signatures;
using Penumbra.GameData; using Penumbra.GameData;
using Penumbra.Services;
namespace Penumbra.Interop; namespace Penumbra.Interop;
@ -52,14 +52,17 @@ public unsafe partial class CharacterUtility : IDisposable
public (IntPtr Address, int Size) DefaultResource( InternalIndex idx ) public (IntPtr Address, int Size) DefaultResource( InternalIndex idx )
=> _lists[ idx.Value ].DefaultResource; => _lists[ idx.Value ].DefaultResource;
public CharacterUtility() private readonly Framework _framework;
public CharacterUtility(Framework framework)
{ {
SignatureHelper.Initialise( this ); SignatureHelper.Initialise( this );
_framework = framework;
LoadingFinished += () => Penumbra.Log.Debug( "Loading of CharacterUtility finished." ); LoadingFinished += () => Penumbra.Log.Debug( "Loading of CharacterUtility finished." );
LoadDefaultResources( null! ); LoadDefaultResources( null! );
if( !Ready ) if( !Ready )
{ {
DalamudServices.Framework.Update += LoadDefaultResources; _framework.Update += LoadDefaultResources;
} }
} }
@ -99,7 +102,7 @@ public unsafe partial class CharacterUtility : IDisposable
if( !anyMissing ) if( !anyMissing )
{ {
Ready = true; Ready = true;
DalamudServices.Framework.Update -= LoadDefaultResources; _framework.Update -= LoadDefaultResources;
LoadingFinished.Invoke(); LoadingFinished.Invoke();
} }
} }

View file

@ -10,7 +10,6 @@ using FFXIVClientStructs.STD;
using Penumbra.Collections; using Penumbra.Collections;
using Penumbra.GameData; using Penumbra.GameData;
using Penumbra.GameData.Enums; using Penumbra.GameData.Enums;
using Penumbra.String;
using Penumbra.String.Classes; using Penumbra.String.Classes;
using Penumbra.Util; using Penumbra.Util;
@ -249,18 +248,4 @@ public unsafe partial class ResourceLoader
Penumbra.Log.Error( $"Caught decrease of Reference Counter for {handle->FileName} at 0x{( ulong )handle:X} below 0." ); Penumbra.Log.Error( $"Caught decrease of Reference Counter for {handle->FileName} at 0x{( ulong )handle:X} below 0." );
return 1; return 1;
} }
// Logging functions for EnableFullLogging.
private static void LogPath( Utf8GamePath path, bool synchronous )
=> Penumbra.Log.Information( $"[ResourceLoader] Requested {path} {( synchronous ? "synchronously." : "asynchronously." )}" );
private static void LogResource( Structs.ResourceHandle* handle, Utf8GamePath path, FullPath? manipulatedPath, ResolveData data )
{
var pathString = manipulatedPath != null ? $"custom file {manipulatedPath} instead of {path}" : path.ToString();
Penumbra.Log.Information(
$"[ResourceLoader] [{handle->FileType}] Loaded {pathString} to 0x{( ulong )handle:X} using collection {data.ModCollection.AnonymizedName} for {data.AssociatedName()} (Refcount {handle->RefCount}) " );
}
private static void LogLoadedFile( Structs.ResourceHandle* resource, ByteString path, bool success, bool custom )
=> Penumbra.Log.Information( $"[ResourceLoader] Loading {path} from {( custom ? "local files" : "SqPack" )} into 0x{( ulong )resource:X} returned {success}." );
} }

View file

@ -18,39 +18,6 @@ public unsafe partial class ResourceLoader : IDisposable
// Hooks are required for everything, even events firing. // Hooks are required for everything, even events firing.
public bool HooksEnabled { get; private set; } public bool HooksEnabled { get; private set; }
// This Logging just logs all file requests, returns and loads to the Dalamud log.
// Events can be used to make smarter logging.
public bool IsLoggingEnabled { get; private set; }
public void EnableFullLogging()
{
if( IsLoggingEnabled )
{
return;
}
IsLoggingEnabled = true;
ResourceRequested += LogPath;
ResourceLoaded += LogResource;
FileLoaded += LogLoadedFile;
ResourceHandleDestructorHook?.Enable();
EnableHooks();
}
public void DisableFullLogging()
{
if( !IsLoggingEnabled )
{
return;
}
IsLoggingEnabled = false;
ResourceRequested -= LogPath;
ResourceLoaded -= LogResource;
FileLoaded -= LogLoadedFile;
ResourceHandleDestructorHook?.Disable();
}
public void EnableReplacements() public void EnableReplacements()
{ {
if( DoReplacements ) if( DoReplacements )
@ -150,7 +117,6 @@ public unsafe partial class ResourceLoader : IDisposable
public void Dispose() public void Dispose()
{ {
DisableFullLogging();
DisposeHooks(); DisposeHooks();
DisposeTexMdlTreatment(); DisposeTexMdlTreatment();
} }

View file

@ -1,98 +0,0 @@
using System;
using System.Text.RegularExpressions;
using Penumbra.String;
using Penumbra.String.Classes;
namespace Penumbra.Interop.Loader;
// A logger class that contains the relevant data to log requested files via regex.
// Filters are case-insensitive.
public class ResourceLogger : IDisposable
{
// Enable or disable the logging of resources subject to the current filter.
public void SetState( bool value )
{
if( value == Penumbra.Config.EnableResourceLogging )
{
return;
}
Penumbra.Config.EnableResourceLogging = value;
Penumbra.Config.Save();
if( value )
{
_resourceLoader.ResourceRequested += OnResourceRequested;
}
else
{
_resourceLoader.ResourceRequested -= OnResourceRequested;
}
}
// Set the current filter to a new string, doing all other necessary work.
public void SetFilter( string newFilter )
{
if( newFilter == Filter )
{
return;
}
Penumbra.Config.ResourceLoggingFilter = newFilter;
Penumbra.Config.Save();
SetupRegex();
}
// Returns whether the current filter is a valid regular expression.
public bool ValidRegex
=> _filterRegex != null;
private readonly ResourceLoader _resourceLoader;
private Regex? _filterRegex;
private static string Filter
=> Penumbra.Config.ResourceLoggingFilter;
private void SetupRegex()
{
try
{
_filterRegex = new Regex( Filter, RegexOptions.Compiled | RegexOptions.IgnoreCase | RegexOptions.CultureInvariant );
}
catch
{
_filterRegex = null;
}
}
public ResourceLogger( ResourceLoader loader )
{
_resourceLoader = loader;
SetupRegex();
if( Penumbra.Config.EnableResourceLogging )
{
_resourceLoader.ResourceRequested += OnResourceRequested;
}
}
private void OnResourceRequested( Utf8GamePath data, bool synchronous )
{
var path = Match( data.Path );
if( path != null )
{
Penumbra.Log.Information( $"{path} was requested {( synchronous ? "synchronously." : "asynchronously." )}" );
}
}
// Returns the converted string if the filter matches, and null otherwise.
// The filter matches if it is empty, if it is a valid and matching regex or if the given string contains it.
private string? Match( ByteString data )
{
var s = data.ToString();
return Filter.Length == 0 || ( _filterRegex?.IsMatch( s ) ?? s.Contains( Filter, StringComparison.OrdinalIgnoreCase ) )
? s
: null;
}
public void Dispose()
=> _resourceLoader.ResourceRequested -= OnResourceRequested;
}

View file

@ -2,8 +2,8 @@ using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Diagnostics; using System.Diagnostics;
using System.Linq; using System.Linq;
using Dalamud.Game.ClientState.Objects;
using FFXIVClientStructs.FFXIV.Client.Game.Character; using FFXIVClientStructs.FFXIV.Client.Game.Character;
using Penumbra.Services;
namespace Penumbra.Interop.Resolver; namespace Penumbra.Interop.Resolver;
@ -14,15 +14,17 @@ public class CutsceneCharacters : IDisposable
public const int CutsceneEndIdx = CutsceneStartIdx + CutsceneSlots; public const int CutsceneEndIdx = CutsceneStartIdx + CutsceneSlots;
private readonly GameEventManager _events; private readonly GameEventManager _events;
private readonly ObjectTable _objects;
private readonly short[] _copiedCharacters = Enumerable.Repeat((short)-1, CutsceneSlots).ToArray(); private readonly short[] _copiedCharacters = Enumerable.Repeat((short)-1, CutsceneSlots).ToArray();
public IEnumerable< KeyValuePair< int, global::Dalamud.Game.ClientState.Objects.Types.GameObject > > Actors public IEnumerable<KeyValuePair<int, Dalamud.Game.ClientState.Objects.Types.GameObject>> Actors
=> Enumerable.Range(CutsceneStartIdx, CutsceneSlots) => Enumerable.Range(CutsceneStartIdx, CutsceneSlots)
.Where( i => DalamudServices.Objects[ i ] != null ) .Where(i => _objects[i] != null)
.Select( i => KeyValuePair.Create( i, this[ i ] ?? DalamudServices.Objects[ i ]! ) ); .Select(i => KeyValuePair.Create(i, this[i] ?? _objects[i]!));
public CutsceneCharacters(GameEventManager events) public CutsceneCharacters(ObjectTable objects, GameEventManager events)
{ {
_objects = objects;
_events = events; _events = events;
Enable(); Enable();
} }
@ -30,13 +32,13 @@ public class CutsceneCharacters : IDisposable
// Get the related actor to a cutscene actor. // Get the related actor to a cutscene actor.
// Does not check for valid input index. // Does not check for valid input index.
// Returns null if no connected actor is set or the actor does not exist anymore. // Returns null if no connected actor is set or the actor does not exist anymore.
public global::Dalamud.Game.ClientState.Objects.Types.GameObject? this[ int idx ] public Dalamud.Game.ClientState.Objects.Types.GameObject? this[int idx]
{ {
get get
{ {
Debug.Assert(idx is >= CutsceneStartIdx and < CutsceneEndIdx); Debug.Assert(idx is >= CutsceneStartIdx and < CutsceneEndIdx);
idx = _copiedCharacters[idx - CutsceneStartIdx]; idx = _copiedCharacters[idx - CutsceneStartIdx];
return idx < 0 ? null : DalamudServices.Objects[ idx ]; return idx < 0 ? null : _objects[idx];
} }
} }
@ -44,9 +46,7 @@ public class CutsceneCharacters : IDisposable
public int GetParentIndex(int idx) public int GetParentIndex(int idx)
{ {
if (idx is >= CutsceneStartIdx and < CutsceneEndIdx) if (idx is >= CutsceneStartIdx and < CutsceneEndIdx)
{
return _copiedCharacters[idx - CutsceneStartIdx]; return _copiedCharacters[idx - CutsceneStartIdx];
}
return -1; return -1;
} }

View file

@ -11,25 +11,24 @@ namespace Penumbra.Interop.Resolver;
public unsafe class IdentifiedCollectionCache : IDisposable, IEnumerable<(IntPtr Address, ActorIdentifier Identifier, ModCollection Collection)> public unsafe class IdentifiedCollectionCache : IDisposable, IEnumerable<(IntPtr Address, ActorIdentifier Identifier, ModCollection Collection)>
{ {
private readonly CommunicatorService _communicator;
private readonly GameEventManager _events; private readonly GameEventManager _events;
private readonly Dictionary<IntPtr, (ActorIdentifier, ModCollection)> _cache = new(317); private readonly Dictionary<IntPtr, (ActorIdentifier, ModCollection)> _cache = new(317);
private bool _dirty = false; private bool _dirty = false;
private bool _enabled = false; private bool _enabled = false;
public IdentifiedCollectionCache(GameEventManager events) public IdentifiedCollectionCache(CommunicatorService communicator, GameEventManager events)
{ {
_communicator = communicator;
_events = events; _events = events;
} }
public void Enable() public void Enable()
{ {
if (_enabled) if (_enabled)
{
return; return;
}
Penumbra.CollectionManager.CollectionChanged += CollectionChangeClear; _communicator.CollectionChange.Event += CollectionChangeClear;
Penumbra.TempMods.CollectionChanged += CollectionChangeClear;
DalamudServices.ClientState.TerritoryChanged += TerritoryClear; DalamudServices.ClientState.TerritoryChanged += TerritoryClear;
_events.CharacterDestructor += OnCharacterDestruct; _events.CharacterDestructor += OnCharacterDestruct;
_enabled = true; _enabled = true;
@ -38,12 +37,9 @@ public unsafe class IdentifiedCollectionCache : IDisposable, IEnumerable< (IntPt
public void Disable() public void Disable()
{ {
if (!_enabled) if (!_enabled)
{
return; return;
}
Penumbra.CollectionManager.CollectionChanged -= CollectionChangeClear; _communicator.CollectionChange.Event -= CollectionChangeClear;
Penumbra.TempMods.CollectionChanged -= CollectionChangeClear;
DalamudServices.ClientState.TerritoryChanged -= TerritoryClear; DalamudServices.ClientState.TerritoryChanged -= TerritoryClear;
_events.CharacterDestructor -= OnCharacterDestruct; _events.CharacterDestructor -= OnCharacterDestruct;
_enabled = false; _enabled = false;
@ -89,9 +85,7 @@ public unsafe class IdentifiedCollectionCache : IDisposable, IEnumerable< (IntPt
foreach (var (address, (identifier, collection)) in _cache) foreach (var (address, (identifier, collection)) in _cache)
{ {
if (_dirty) if (_dirty)
{
yield break; yield break;
}
yield return (address, identifier, collection); yield return (address, identifier, collection);
} }
@ -106,10 +100,8 @@ public unsafe class IdentifiedCollectionCache : IDisposable, IEnumerable< (IntPt
private void CollectionChangeClear(CollectionType type, ModCollection? _1, ModCollection? _2, string _3) private void CollectionChangeClear(CollectionType type, ModCollection? _1, ModCollection? _2, string _3)
{ {
if (type is not (CollectionType.Current or CollectionType.Interface or CollectionType.Inactive)) if (type is not (CollectionType.Current or CollectionType.Interface or CollectionType.Inactive))
{
_dirty = _cache.Count > 0; _dirty = _cache.Count > 0;
} }
}
private void TerritoryClear(object? _1, ushort _2) private void TerritoryClear(object? _1, ushort _2)
=> _dirty = _cache.Count > 0; => _dirty = _cache.Count > 0;

View file

@ -20,6 +20,7 @@ public unsafe partial class PathResolver
{ {
public class DrawObjectState public class DrawObjectState
{ {
private readonly CommunicatorService _communicator;
public static event CreatingCharacterBaseDelegate? CreatingCharacterBase; public static event CreatingCharacterBaseDelegate? CreatingCharacterBase;
public static event CreatedCharacterBaseDelegate? CreatedCharacterBase; public static event CreatedCharacterBaseDelegate? CreatedCharacterBase;
@ -33,9 +34,7 @@ public unsafe partial class PathResolver
{ {
gameObject = null; gameObject = null;
if (!_drawObjectToObject.TryGetValue(drawObject, out value)) if (!_drawObjectToObject.TryGetValue(drawObject, out value))
{
return false; return false;
}
var gameObjectIdx = value.Item2; var gameObjectIdx = value.Item2;
return VerifyEntry(drawObject, gameObjectIdx, out gameObject); return VerifyEntry(drawObject, gameObjectIdx, out gameObject);
@ -76,9 +75,10 @@ public unsafe partial class PathResolver
public GameObject* LastGameObject { get; private set; } public GameObject* LastGameObject { get; private set; }
public DrawObjectState() public DrawObjectState(CommunicatorService communicator)
{ {
SignatureHelper.Initialise(this); SignatureHelper.Initialise(this);
_communicator = communicator;
} }
public void Enable() public void Enable()
@ -88,8 +88,7 @@ public unsafe partial class PathResolver
_enableDrawHook.Enable(); _enableDrawHook.Enable();
_weaponReloadHook.Enable(); _weaponReloadHook.Enable();
InitializeDrawObjects(); InitializeDrawObjects();
Penumbra.CollectionManager.CollectionChanged += CheckCollections; _communicator.CollectionChange.Event += CheckCollections;
Penumbra.TempMods.CollectionChanged += CheckCollections;
} }
public void Disable() public void Disable()
@ -98,8 +97,7 @@ public unsafe partial class PathResolver
_characterBaseDestructorHook.Disable(); _characterBaseDestructorHook.Disable();
_enableDrawHook.Disable(); _enableDrawHook.Disable();
_weaponReloadHook.Disable(); _weaponReloadHook.Disable();
Penumbra.CollectionManager.CollectionChanged -= CheckCollections; _communicator.CollectionChange.Event -= CheckCollections;
Penumbra.TempMods.CollectionChanged -= CheckCollections;
} }
public void Dispose() public void Dispose()
@ -118,9 +116,7 @@ public unsafe partial class PathResolver
var draw = (DrawObject*)drawObject; var draw = (DrawObject*)drawObject;
if (gameObject != null if (gameObject != null
&& (gameObject->DrawObject == draw || draw != null && gameObject->DrawObject == draw->Object.ParentObject)) && (gameObject->DrawObject == draw || draw != null && gameObject->DrawObject == draw->Object.ParentObject))
{
return true; return true;
}
gameObject = null; gameObject = null;
_drawObjectToObject.Remove(drawObject); _drawObjectToObject.Remove(drawObject);
@ -232,16 +228,12 @@ public unsafe partial class PathResolver
private void CheckCollections(CollectionType type, ModCollection? _1, ModCollection? _2, string _3) private void CheckCollections(CollectionType type, ModCollection? _1, ModCollection? _2, string _3)
{ {
if (type is CollectionType.Inactive or CollectionType.Current or CollectionType.Interface) if (type is CollectionType.Inactive or CollectionType.Current or CollectionType.Interface)
{
return; return;
}
foreach (var (key, (_, idx)) in _drawObjectToObject.ToArray()) foreach (var (key, (_, idx)) in _drawObjectToObject.ToArray())
{ {
if (!VerifyEntry(key, idx, out var obj)) if (!VerifyEntry(key, idx, out var obj))
{
_drawObjectToObject.Remove(key); _drawObjectToObject.Remove(key);
}
var newCollection = IdentifyCollection(obj, false); var newCollection = IdentifyCollection(obj, false);
_drawObjectToObject[key] = (newCollection, idx); _drawObjectToObject[key] = (newCollection, idx);
@ -256,10 +248,8 @@ public unsafe partial class PathResolver
{ {
var ptr = (GameObject*)DalamudServices.Objects.GetObjectAddress(i); var ptr = (GameObject*)DalamudServices.Objects.GetObjectAddress(i);
if (ptr != null && ptr->IsCharacter() && ptr->DrawObject != null) if (ptr != null && ptr->IsCharacter() && ptr->DrawObject != null)
{
_drawObjectToObject[(IntPtr)ptr->DrawObject] = (IdentifyCollection(ptr, false), ptr->ObjectIndex); _drawObjectToObject[(IntPtr)ptr->DrawObject] = (IdentifyCollection(ptr, false), ptr->ObjectIndex);
} }
} }
} }
} }
}

View file

@ -103,7 +103,7 @@ public unsafe partial class PathResolver
// Check both temporary and permanent character collections. Temporary first. // Check both temporary and permanent character collections. Temporary first.
private static ModCollection? CollectionByIdentifier( ActorIdentifier identifier ) private static ModCollection? CollectionByIdentifier( ActorIdentifier identifier )
=> Penumbra.TempMods.Collections.TryGetCollection( identifier, out var collection ) => Penumbra.TempCollections.Collections.TryGetCollection( identifier, out var collection )
|| Penumbra.CollectionManager.Individuals.TryGetCollection( identifier, out collection ) || Penumbra.CollectionManager.Individuals.TryGetCollection( identifier, out collection )
? collection ? collection
: null; : null;

View file

@ -265,7 +265,7 @@ public partial class PathResolver
} }
var parentObject = ( IntPtr )( ( DrawObject* )drawObject )->Object.ParentObject; var parentObject = ( IntPtr )( ( DrawObject* )drawObject )->Object.ParentObject;
var parentCollection = DrawObjects.CheckParentDrawObject( drawObject, parentObject ); var parentCollection = _drawObjects.CheckParentDrawObject( drawObject, parentObject );
if( parentCollection.Valid ) if( parentCollection.Valid )
{ {
return _parent._paths.ResolvePath( ( IntPtr )FindParent( parentObject, out _ ), parentCollection.ModCollection, path ); return _parent._paths.ResolvePath( ( IntPtr )FindParent( parentObject, out _ ), parentCollection.ModCollection, path );

View file

@ -5,6 +5,7 @@ using Dalamud.Utility.Signatures;
using FFXIVClientStructs.FFXIV.Client.Game.Object; using FFXIVClientStructs.FFXIV.Client.Game.Object;
using FFXIVClientStructs.FFXIV.Client.Graphics.Scene; using FFXIVClientStructs.FFXIV.Client.Graphics.Scene;
using FFXIVClientStructs.FFXIV.Client.System.Resource; using FFXIVClientStructs.FFXIV.Client.System.Resource;
using OtterGui.Classes;
using Penumbra.Collections; using Penumbra.Collections;
using Penumbra.GameData.Enums; using Penumbra.GameData.Enums;
using Penumbra.Interop.Loader; using Penumbra.Interop.Loader;
@ -24,11 +25,12 @@ public partial class PathResolver : IDisposable
{ {
public bool Enabled { get; private set; } public bool Enabled { get; private set; }
private readonly CommunicatorService _communicator;
private readonly ResourceLoader _loader; private readonly ResourceLoader _loader;
private static readonly CutsceneCharacters Cutscenes = new(Penumbra.GameEvents); private static readonly CutsceneCharacters Cutscenes = new(DalamudServices.Objects, Penumbra.GameEvents); // TODO
private static readonly DrawObjectState DrawObjects = new(); private static DrawObjectState _drawObjects = null!; // TODO
private static readonly BitArray ValidHumanModels; private static readonly BitArray ValidHumanModels;
internal static readonly IdentifiedCollectionCache IdentifiedCache = new(Penumbra.GameEvents); internal static IdentifiedCollectionCache IdentifiedCache = null!; // TODO
private readonly AnimationState _animations; private readonly AnimationState _animations;
private readonly PathState _paths; private readonly PathState _paths;
private readonly MetaState _meta; private readonly MetaState _meta;
@ -37,12 +39,15 @@ public partial class PathResolver : IDisposable
static PathResolver() static PathResolver()
=> ValidHumanModels = GetValidHumanModels(DalamudServices.GameData); => ValidHumanModels = GetValidHumanModels(DalamudServices.GameData);
public unsafe PathResolver( ResourceLoader loader ) public unsafe PathResolver(StartTracker timer, CommunicatorService communicator, GameEventManager events, ResourceLoader loader)
{ {
using var tApi = Penumbra.StartTimer.Measure( StartTimeType.PathResolver ); using var tApi = timer.Measure(StartTimeType.PathResolver);
_communicator = communicator;
IdentifiedCache = new IdentifiedCollectionCache(communicator, events);
SignatureHelper.Initialise(this); SignatureHelper.Initialise(this);
_drawObjects = new DrawObjectState(_communicator);
_loader = loader; _loader = loader;
_animations = new AnimationState( DrawObjects ); _animations = new AnimationState(_drawObjects);
_paths = new PathState(this); _paths = new PathState(this);
_meta = new MetaState(_paths.HumanVTable); _meta = new MetaState(_paths.HumanVTable);
_subFiles = new SubfileHelper(_loader, Penumbra.GameEvents); _subFiles = new SubfileHelper(_loader, Penumbra.GameEvents);
@ -61,11 +66,9 @@ public partial class PathResolver : IDisposable
var nonDefault = _subFiles.HandleSubFiles(type, out var resolveData) var nonDefault = _subFiles.HandleSubFiles(type, out var resolveData)
|| _paths.Consume(gamePath.Path, out resolveData) || _paths.Consume(gamePath.Path, out resolveData)
|| _animations.HandleFiles(type, gamePath, out resolveData) || _animations.HandleFiles(type, gamePath, out resolveData)
|| DrawObjects.HandleDecalFile( type, gamePath, out resolveData ); || _drawObjects.HandleDecalFile(type, gamePath, out resolveData);
if (!nonDefault || !resolveData.Valid) if (!nonDefault || !resolveData.Valid)
{
resolveData = Penumbra.CollectionManager.Default.ToResolveData(); resolveData = Penumbra.CollectionManager.Default.ToResolveData();
}
// Resolve using character/default collection first, otherwise forced, as usual. // Resolve using character/default collection first, otherwise forced, as usual.
var resolved = resolveData.ModCollection.ResolvePath(gamePath); var resolved = resolveData.ModCollection.ResolvePath(gamePath);
@ -81,13 +84,11 @@ public partial class PathResolver : IDisposable
public void Enable() public void Enable()
{ {
if (Enabled) if (Enabled)
{
return; return;
}
Enabled = true; Enabled = true;
Cutscenes.Enable(); Cutscenes.Enable();
DrawObjects.Enable(); _drawObjects.Enable();
IdentifiedCache.Enable(); IdentifiedCache.Enable();
_animations.Enable(); _animations.Enable();
_paths.Enable(); _paths.Enable();
@ -101,13 +102,11 @@ public partial class PathResolver : IDisposable
public void Disable() public void Disable()
{ {
if (!Enabled) if (!Enabled)
{
return; return;
}
Enabled = false; Enabled = false;
_animations.Disable(); _animations.Disable();
DrawObjects.Disable(); _drawObjects.Disable();
Cutscenes.Disable(); Cutscenes.Disable();
IdentifiedCache.Disable(); IdentifiedCache.Disable();
_paths.Disable(); _paths.Disable();
@ -123,7 +122,7 @@ public partial class PathResolver : IDisposable
Disable(); Disable();
_paths.Dispose(); _paths.Dispose();
_animations.Dispose(); _animations.Dispose();
DrawObjects.Dispose(); _drawObjects.Dispose();
Cutscenes.Dispose(); Cutscenes.Dispose();
IdentifiedCache.Dispose(); IdentifiedCache.Dispose();
_meta.Dispose(); _meta.Dispose();
@ -142,17 +141,17 @@ public partial class PathResolver : IDisposable
// Use the stored information to find the GameObject and Collection linked to a DrawObject. // Use the stored information to find the GameObject and Collection linked to a DrawObject.
public static unsafe GameObject* FindParent(IntPtr drawObject, out ResolveData resolveData) public static unsafe GameObject* FindParent(IntPtr drawObject, out ResolveData resolveData)
{ {
if( DrawObjects.TryGetValue( drawObject, out var data, out var gameObject ) ) if (_drawObjects.TryGetValue(drawObject, out var data, out var gameObject))
{ {
resolveData = data.Item1; resolveData = data.Item1;
return gameObject; return gameObject;
} }
if( DrawObjects.LastGameObject != null if (_drawObjects.LastGameObject != null
&& ( DrawObjects.LastGameObject->DrawObject == null || DrawObjects.LastGameObject->DrawObject == ( DrawObject* )drawObject ) ) && (_drawObjects.LastGameObject->DrawObject == null || _drawObjects.LastGameObject->DrawObject == (DrawObject*)drawObject))
{ {
resolveData = IdentifyCollection( DrawObjects.LastGameObject, true ); resolveData = IdentifyCollection(_drawObjects.LastGameObject, true);
return DrawObjects.LastGameObject; return _drawObjects.LastGameObject;
} }
resolveData = IdentifyCollection(null, true); resolveData = IdentifyCollection(null, true);
@ -169,7 +168,7 @@ public partial class PathResolver : IDisposable
=> _paths.Paths; => _paths.Paths;
internal IEnumerable<KeyValuePair<IntPtr, (ResolveData, int)>> DrawObjectMap internal IEnumerable<KeyValuePair<IntPtr, (ResolveData, int)>> DrawObjectMap
=> DrawObjects.DrawObjects; => _drawObjects.DrawObjects;
internal IEnumerable<KeyValuePair<int, global::Dalamud.Game.ClientState.Objects.Types.GameObject>> CutsceneActors internal IEnumerable<KeyValuePair<int, global::Dalamud.Game.ClientState.Objects.Types.GameObject>> CutsceneActors
=> Cutscenes.Actors; => Cutscenes.Actors;
@ -187,8 +186,8 @@ public partial class PathResolver : IDisposable
=> _subFiles.AvfxData; => _subFiles.AvfxData;
internal ResolveData LastGameObjectData internal ResolveData LastGameObjectData
=> DrawObjects.LastCreatedCollection; => _drawObjects.LastCreatedCollection;
internal unsafe nint LastGameObject internal unsafe nint LastGameObject
=> (nint) DrawObjects.LastGameObject; => (nint)_drawObjects.LastGameObject;
} }

View file

@ -169,7 +169,7 @@ public partial class MetaManager
var lastUnderscore = split.LastIndexOf( ( byte )'_' ); var lastUnderscore = split.LastIndexOf( ( byte )'_' );
var name = lastUnderscore == -1 ? split.ToString() : split.Substring( 0, lastUnderscore ).ToString(); var name = lastUnderscore == -1 ? split.ToString() : split.Substring( 0, lastUnderscore ).ToString();
if( ( Penumbra.TempMods.CollectionByName( name, out var collection ) if( ( Penumbra.TempCollections.CollectionByName( name, out var collection )
|| Penumbra.CollectionManager.ByName( name, out collection ) ) || Penumbra.CollectionManager.ByName( name, out collection ) )
&& collection.HasCache && collection.HasCache
&& collection.MetaCache!._imcFiles.TryGetValue( Utf8GamePath.FromSpan( path.Span, out var p ) ? p : Utf8GamePath.Empty, out var file ) ) && collection.MetaCache!._imcFiles.TryGetValue( Utf8GamePath.FromSpan( path.Span, out var p ) ? p : Utf8GamePath.Empty, out var file ) )

View file

@ -3,6 +3,7 @@ using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.IO; using System.IO;
using System.Linq; using System.Linq;
using Dalamud.Plugin;
using Newtonsoft.Json; using Newtonsoft.Json;
using Penumbra.Services; using Penumbra.Services;
@ -10,8 +11,8 @@ namespace Penumbra.Mods;
public sealed partial class Mod public sealed partial class Mod
{ {
public static DirectoryInfo LocalDataDirectory public static DirectoryInfo LocalDataDirectory(DalamudPluginInterface pi)
=> new(Path.Combine( DalamudServices.PluginInterface.ConfigDirectory.FullName, "mod_data" )); => new(Path.Combine( pi.ConfigDirectory.FullName, "mod_data" ));
public long ImportDate { get; private set; } = DateTimeOffset.UnixEpoch.ToUnixTimeMilliseconds(); public long ImportDate { get; private set; } = DateTimeOffset.UnixEpoch.ToUnixTimeMilliseconds();

View file

@ -4,6 +4,7 @@ using System.Diagnostics.CodeAnalysis;
using System.IO; using System.IO;
using System.Linq; using System.Linq;
using System.Text.RegularExpressions; using System.Text.RegularExpressions;
using Dalamud.Plugin;
using OtterGui.Filesystem; using OtterGui.Filesystem;
using Penumbra.Services; using Penumbra.Services;
@ -11,15 +12,16 @@ namespace Penumbra.Mods;
public sealed class ModFileSystem : FileSystem< Mod >, IDisposable public sealed class ModFileSystem : FileSystem< Mod >, IDisposable
{ {
public static string ModFileSystemFile public static string ModFileSystemFile(DalamudPluginInterface pi)
=> Path.Combine( DalamudServices.PluginInterface.GetPluginConfigDirectory(), "sort_order.json" ); => Path.Combine( pi.GetPluginConfigDirectory(), "sort_order.json" );
// Save the current sort order. // Save the current sort order.
// Does not save or copy the backup in the current mod directory, // Does not save or copy the backup in the current mod directory,
// as this is done on mod directory changes only. // as this is done on mod directory changes only.
// TODO
private void SaveFilesystem() private void SaveFilesystem()
{ {
SaveToFile( new FileInfo( ModFileSystemFile ), SaveMod, true ); SaveToFile( new FileInfo( ModFileSystemFile(DalamudServices.PluginInterface) ), SaveMod, true );
Penumbra.Log.Verbose( "Saved mod filesystem." ); Penumbra.Log.Verbose( "Saved mod filesystem." );
} }
@ -75,7 +77,8 @@ public sealed class ModFileSystem : FileSystem< Mod >, IDisposable
// Used on construction and on mod rediscoveries. // Used on construction and on mod rediscoveries.
private void Reload() private void Reload()
{ {
if( Load( new FileInfo( ModFileSystemFile ), Penumbra.ModManager, ModToIdentifier, ModToName ) ) // TODO
if( Load( new FileInfo( ModFileSystemFile(DalamudServices.PluginInterface) ), Penumbra.ModManager, ModToIdentifier, ModToName ) )
{ {
Save(); Save();
} }

View file

@ -1,5 +1,4 @@
using System; using System;
using System.Collections.Generic;
using System.IO; using System.IO;
using System.Linq; using System.Linq;
using System.Reflection; using System.Reflection;
@ -29,45 +28,10 @@ using Penumbra.Mods;
using CharacterUtility = Penumbra.Interop.CharacterUtility; using CharacterUtility = Penumbra.Interop.CharacterUtility;
using DalamudUtil = Dalamud.Utility.Util; using DalamudUtil = Dalamud.Utility.Util;
using ResidentResourceManager = Penumbra.Interop.ResidentResourceManager; using ResidentResourceManager = Penumbra.Interop.ResidentResourceManager;
using Penumbra.Services;
namespace Penumbra; namespace Penumbra;
public class PenumbraNew
{
public string Name
=> "Penumbra";
public static readonly Logger Log = new();
public readonly StartTimeTracker< StartTimeType > StartTimer = new();
public readonly IServiceCollection Services = new ServiceCollection();
public PenumbraNew( DalamudPluginInterface pi )
{
using var time = StartTimer.Measure( StartTimeType.Total );
// Add meta services.
Services.AddSingleton( Log );
Services.AddSingleton( StartTimer );
Services.AddSingleton< ValidityChecker >();
Services.AddSingleton< PerformanceTracker< PerformanceType > >();
// Add Dalamud services
var dalamud = new DalamudServices( pi );
dalamud.AddServices( Services );
// Add Game Data
// Add Configuration
Services.AddSingleton< Configuration >();
}
public void Dispose()
{ }
}
public class Penumbra : IDalamudPlugin public class Penumbra : IDalamudPlugin
{ {
public string Name public string Name
@ -87,21 +51,22 @@ public class Penumbra : IDalamudPlugin
public static MetaFileManager MetaFileManager { get; private set; } = null!; public static MetaFileManager MetaFileManager { get; private set; } = null!;
public static Mod.Manager ModManager { get; private set; } = null!; public static Mod.Manager ModManager { get; private set; } = null!;
public static ModCollection.Manager CollectionManager { get; private set; } = null!; public static ModCollection.Manager CollectionManager { get; private set; } = null!;
public static TempCollectionManager TempCollections { get; private set; } = null!;
public static TempModManager TempMods { get; private set; } = null!; public static TempModManager TempMods { get; private set; } = null!;
public static ResourceLoader ResourceLoader { get; private set; } = null!; public static ResourceLoader ResourceLoader { get; private set; } = null!;
public static FrameworkManager Framework { get; private set; } = null!; public static FrameworkManager Framework { get; private set; } = null!;
public static ActorManager Actors { get; private set; } = null!; public static ActorManager Actors { get; private set; } = null!;
public static IObjectIdentifier Identifier { get; private set; } = null!; public static IObjectIdentifier Identifier { get; private set; } = null!;
public static IGamePathParser GamePathParser { get; private set; } = null!; public static IGamePathParser GamePathParser { get; private set; } = null!;
public static StainManager StainManager { get; private set; } = null!; public static StainService StainService { get; private set; } = null!;
// TODO
public static DalamudServices Dalamud { get; private set; } = null!;
public static ValidityChecker ValidityChecker { get; private set; } = null!; public static ValidityChecker ValidityChecker { get; private set; } = null!;
public static PerformanceTracker< PerformanceType > Performance { get; private set; } = null!; public static PerformanceTracker Performance { get; private set; } = null!;
public static readonly StartTimeTracker< StartTimeType > StartTimer = new();
public readonly ResourceLogger ResourceLogger;
public readonly PathResolver PathResolver; public readonly PathResolver PathResolver;
public readonly ObjectReloader ObjectReloader; public readonly ObjectReloader ObjectReloader;
public readonly ModFileSystem ModFileSystem; public readonly ModFileSystem ModFileSystem;
@ -116,59 +81,52 @@ public class Penumbra : IDalamudPlugin
private readonly ResourceWatcher _resourceWatcher; private readonly ResourceWatcher _resourceWatcher;
private bool _disposed; private bool _disposed;
private readonly PenumbraNew _tmp;
public static ItemData ItemData { get; private set; } = null!; public static ItemData ItemData { get; private set; } = null!;
public Penumbra(DalamudPluginInterface pluginInterface) public Penumbra(DalamudPluginInterface pluginInterface)
{ {
using var time = StartTimer.Measure( StartTimeType.Total ); Log = PenumbraNew.Log;
_tmp = new PenumbraNew(pluginInterface);
Performance = _tmp.Services.GetRequiredService<PerformanceTracker>();
ValidityChecker = _tmp.Services.GetRequiredService<ValidityChecker>();
_tmp.Services.GetRequiredService<BackupService>();
Config = _tmp.Services.GetRequiredService<Configuration>();
CharacterUtility = _tmp.Services.GetRequiredService<CharacterUtility>();
GameEvents = _tmp.Services.GetRequiredService<GameEventManager>();
MetaFileManager = _tmp.Services.GetRequiredService<MetaFileManager>();
Framework = _tmp.Services.GetRequiredService<FrameworkManager>();
Actors = _tmp.Services.GetRequiredService<ActorService>().AwaitedService;
Identifier = _tmp.Services.GetRequiredService<IdentifierService>().AwaitedService;
GamePathParser = _tmp.Services.GetRequiredService<IGamePathParser>();
StainService = _tmp.Services.GetRequiredService<StainService>();
ItemData = _tmp.Services.GetRequiredService<ItemService>().AwaitedService;
Dalamud = _tmp.Services.GetRequiredService<DalamudServices>();
TempMods = _tmp.Services.GetRequiredService<TempModManager>();
try try
{ {
DalamudServices.Initialize( pluginInterface );
Performance = new PerformanceTracker< PerformanceType >( DalamudServices.Framework );
Log = new Logger();
ValidityChecker = new ValidityChecker( DalamudServices.PluginInterface );
GameEvents = new GameEventManager();
StartTimer.Measure( StartTimeType.Identifier, () => Identifier = GameData.GameData.GetIdentifier( DalamudServices.PluginInterface, DalamudServices.GameData ) );
StartTimer.Measure( StartTimeType.GamePathParser, () => GamePathParser = GameData.GameData.GetGamePathParser() );
StartTimer.Measure( StartTimeType.Stains, () => StainManager = new StainManager( DalamudServices.PluginInterface, DalamudServices.GameData ) );
ItemData = StartTimer.Measure( StartTimeType.Items,
() => new ItemData( DalamudServices.PluginInterface, DalamudServices.GameData, DalamudServices.GameData.Language ) );
StartTimer.Measure( StartTimeType.Actors,
() => Actors = new ActorManager( DalamudServices.PluginInterface, DalamudServices.Objects, DalamudServices.ClientState, DalamudServices.Framework,
DalamudServices.GameData, DalamudServices.GameGui,
ResolveCutscene ) );
Framework = new FrameworkManager( DalamudServices.Framework, Log );
CharacterUtility = new CharacterUtility();
StartTimer.Measure( StartTimeType.Backup, () => Backup.CreateBackup( pluginInterface.ConfigDirectory, PenumbraBackupFiles() ) );
Config = Configuration.Load();
TempMods = new TempModManager();
MetaFileManager = new MetaFileManager();
ResourceLoader = new ResourceLoader(this); ResourceLoader = new ResourceLoader(this);
ResourceLoader.EnableHooks(); ResourceLoader.EnableHooks();
_resourceWatcher = new ResourceWatcher(ResourceLoader); _resourceWatcher = new ResourceWatcher(ResourceLoader);
ResourceLogger = new ResourceLogger( ResourceLoader );
ResidentResources = new ResidentResourceManager(); ResidentResources = new ResidentResourceManager();
StartTimer.Measure( StartTimeType.Mods, () => _tmp.Services.GetRequiredService<StartTracker>().Measure(StartTimeType.Mods, () =>
{ {
ModManager = new Mod.Manager(Config.ModDirectory); ModManager = new Mod.Manager(Config.ModDirectory);
ModManager.DiscoverMods(); ModManager.DiscoverMods();
}); });
StartTimer.Measure( StartTimeType.Collections, () => _tmp.Services.GetRequiredService<StartTracker>().Measure(StartTimeType.Collections, () =>
{ {
CollectionManager = new ModCollection.Manager( ModManager ); CollectionManager = new ModCollection.Manager(_tmp.Services.GetRequiredService<CommunicatorService>(), ModManager);
CollectionManager.CreateNecessaryCaches(); CollectionManager.CreateNecessaryCaches();
}); });
TempCollections = _tmp.Services.GetRequiredService<TempCollectionManager>();
ModFileSystem = ModFileSystem.Load(); ModFileSystem = ModFileSystem.Load();
ObjectReloader = new ObjectReloader(); ObjectReloader = new ObjectReloader();
PathResolver = new PathResolver( ResourceLoader ); PathResolver = new PathResolver(_tmp.Services.GetRequiredService<StartTracker>(), _tmp.Services.GetRequiredService<CommunicatorService>(), _tmp.Services.GetRequiredService<GameEventManager>(), ResourceLoader);
SetupInterface(); SetupInterface();
@ -179,19 +137,15 @@ public class Penumbra : IDalamudPlugin
} }
if (Config.DebugMode) if (Config.DebugMode)
{
ResourceLoader.EnableDebug(); ResourceLoader.EnableDebug();
}
using( var tApi = StartTimer.Measure( StartTimeType.Api ) ) using (var tApi = _tmp.Services.GetRequiredService<StartTracker>().Measure(StartTimeType.Api))
{ {
Api = new PenumbraApi( this ); Api = new PenumbraApi(_tmp.Services.GetRequiredService<CommunicatorService>(), this);
IpcProviders = new PenumbraIpcProviders(DalamudServices.PluginInterface, Api); IpcProviders = new PenumbraIpcProviders(DalamudServices.PluginInterface, Api);
HttpApi = new HttpApi(Api); HttpApi = new HttpApi(Api);
if (Config.EnableHttpApi) if (Config.EnableHttpApi)
{
HttpApi.CreateWebServer(); HttpApi.CreateWebServer();
}
SubscribeItemLinks(); SubscribeItemLinks();
} }
@ -202,10 +156,8 @@ public class Penumbra : IDalamudPlugin
Log.Information($"Loading native OtterTex assembly from {OtterTex.NativeDll.Directory}."); Log.Information($"Loading native OtterTex assembly from {OtterTex.NativeDll.Directory}.");
if (CharacterUtility.Ready) if (CharacterUtility.Ready)
{
ResidentResources.Reload(); ResidentResources.Reload();
} }
}
catch catch
{ {
Dispose(); Dispose();
@ -217,15 +169,16 @@ public class Penumbra : IDalamudPlugin
{ {
Task.Run(() => Task.Run(() =>
{ {
using var tInterface = StartTimer.Measure( StartTimeType.Interface ); using var tInterface = _tmp.Services.GetRequiredService<StartTracker>().Measure(StartTimeType.Interface);
var changelog = ConfigWindow.CreateChangelog(); var changelog = ConfigWindow.CreateChangelog();
var cfg = new ConfigWindow( this, _resourceWatcher ) var cfg = new ConfigWindow(_tmp.Services.GetRequiredService<CommunicatorService>(), _tmp.Services.GetRequiredService<StartTracker>(), this, _resourceWatcher)
{ {
IsOpen = Config.DebugMode, IsOpen = Config.DebugMode,
}; };
var btn = new LaunchButton(cfg); var btn = new LaunchButton(cfg);
var system = new WindowSystem(Name); var system = new WindowSystem(Name);
var cmd = new CommandHandler( DalamudServices.Commands, ObjectReloader, Config, this, cfg, ModManager, CollectionManager, Actors ); var cmd = new CommandHandler(DalamudServices.Commands, ObjectReloader, Config, this, cfg, ModManager, CollectionManager,
Actors);
system.AddWindow(cfg); system.AddWindow(cfg);
system.AddWindow(cfg.ModEditPopup); system.AddWindow(cfg.ModEditPopup);
system.AddWindow(changelog); system.AddWindow(changelog);
@ -252,9 +205,7 @@ public class Penumbra : IDalamudPlugin
private void DisposeInterface() private void DisposeInterface()
{ {
if (_windowSystem != null) if (_windowSystem != null)
{
DalamudServices.PluginInterface.UiBuilder.Draw -= _windowSystem.Draw; DalamudServices.PluginInterface.UiBuilder.Draw -= _windowSystem.Draw;
}
_launchButton?.Dispose(); _launchButton?.Dispose();
if (ConfigWindow != null) if (ConfigWindow != null)
@ -269,9 +220,7 @@ public class Penumbra : IDalamudPlugin
public bool SetEnabled(bool enabled) public bool SetEnabled(bool enabled)
{ {
if (enabled == Config.EnableMods) if (enabled == Config.EnableMods)
{
return false; return false;
}
Config.EnableMods = enabled; Config.EnableMods = enabled;
if (enabled) if (enabled)
@ -306,45 +255,36 @@ public class Penumbra : IDalamudPlugin
public void ForceChangelogOpen() public void ForceChangelogOpen()
{ {
if (_changelog != null) if (_changelog != null)
{
_changelog.ForceOpen = true; _changelog.ForceOpen = true;
} }
}
private void SubscribeItemLinks() private void SubscribeItemLinks()
{ {
Api.ChangedItemTooltip += it => Api.ChangedItemTooltip += it =>
{ {
if (it is Item) if (it is Item)
{
ImGui.TextUnformatted("Left Click to create an item link in chat."); ImGui.TextUnformatted("Left Click to create an item link in chat.");
}
}; };
Api.ChangedItemClicked += (button, it) => Api.ChangedItemClicked += (button, it) =>
{ {
if (button == MouseButton.Left && it is Item item) if (button == MouseButton.Left && it is Item item)
{
ChatUtil.LinkItem(item); ChatUtil.LinkItem(item);
}
}; };
} }
private short ResolveCutscene( ushort index )
=> ( short )PathResolver.CutsceneActor( index );
public void Dispose() public void Dispose()
{ {
if (_disposed) if (_disposed)
{
return; return;
}
// TODO
_tmp?.Dispose();
_disposed = true; _disposed = true;
HttpApi?.Dispose(); HttpApi?.Dispose();
IpcProviders?.Dispose(); IpcProviders?.Dispose();
Api?.Dispose(); Api?.Dispose();
_commandHandler?.Dispose(); _commandHandler?.Dispose();
StainManager?.Dispose(); StainService?.Dispose();
ItemData?.Dispose(); ItemData?.Dispose();
Actors?.Dispose(); Actors?.Dispose();
Identifier?.Dispose(); Identifier?.Dispose();
@ -354,7 +294,6 @@ public class Penumbra : IDalamudPlugin
ModFileSystem?.Dispose(); ModFileSystem?.Dispose();
CollectionManager?.Dispose(); CollectionManager?.Dispose();
PathResolver?.Dispose(); PathResolver?.Dispose();
ResourceLogger?.Dispose();
_resourceWatcher?.Dispose(); _resourceWatcher?.Dispose();
ResourceLoader?.Dispose(); ResourceLoader?.Dispose();
GameEvents?.Dispose(); GameEvents?.Dispose();
@ -362,21 +301,7 @@ public class Penumbra : IDalamudPlugin
Performance?.Dispose(); Performance?.Dispose();
} }
// Collect all relevant files for penumbra configuration. public string GatherSupportInformation()
private static IReadOnlyList< FileInfo > PenumbraBackupFiles()
{
var collectionDir = ModCollection.CollectionDirectory;
var list = Directory.Exists( collectionDir )
? new DirectoryInfo( collectionDir ).EnumerateFiles( "*.json" ).ToList()
: new List< FileInfo >();
list.AddRange( Mod.LocalDataDirectory.Exists ? Mod.LocalDataDirectory.EnumerateFiles( "*.json" ) : Enumerable.Empty< FileInfo >() );
list.Add( DalamudServices.PluginInterface.ConfigFile );
list.Add( new FileInfo( ModFileSystem.ModFileSystemFile ) );
list.Add( new FileInfo( ModCollection.Manager.ActiveCollectionFile ) );
return list;
}
public static string GatherSupportInformation()
{ {
var sb = new StringBuilder(10240); var sb = new StringBuilder(10240);
var exists = Config.ModDirectory.Length > 0 && Directory.Exists(Config.ModDirectory); var exists = Config.ModDirectory.Length > 0 && Directory.Exists(Config.ModDirectory);
@ -388,28 +313,35 @@ public class Penumbra : IDalamudPlugin
sb.Append($"> **`Enable HTTP API: `** {Config.EnableHttpApi}\n"); sb.Append($"> **`Enable HTTP API: `** {Config.EnableHttpApi}\n");
sb.Append($"> **`Operating System: `** {(DalamudUtil.IsLinux() ? "Mac/Linux (Wine)" : "Windows")}\n"); sb.Append($"> **`Operating System: `** {(DalamudUtil.IsLinux() ? "Mac/Linux (Wine)" : "Windows")}\n");
sb.Append($"> **`Root Directory: `** `{Config.ModDirectory}`, {(exists ? "Exists" : "Not Existing")}\n"); sb.Append($"> **`Root Directory: `** `{Config.ModDirectory}`, {(exists ? "Exists" : "Not Existing")}\n");
sb.Append( $"> **`Free Drive Space: `** {( drive != null ? Functions.HumanReadableSize( drive.AvailableFreeSpace ) : "Unknown" )}\n" ); sb.Append(
$"> **`Free Drive Space: `** {(drive != null ? Functions.HumanReadableSize(drive.AvailableFreeSpace) : "Unknown")}\n");
sb.Append($"> **`Auto-Deduplication: `** {Config.AutoDeduplicateOnImport}\n"); sb.Append($"> **`Auto-Deduplication: `** {Config.AutoDeduplicateOnImport}\n");
sb.Append($"> **`Debug Mode: `** {Config.DebugMode}\n"); sb.Append($"> **`Debug Mode: `** {Config.DebugMode}\n");
sb.Append( sb.Append(
$"> **`Synchronous Load (Dalamud): `** {( DalamudServices.GetDalamudConfig( DalamudServices.WaitingForPluginsOption, out bool v ) ? v.ToString() : "Unknown" )}\n" ); $"> **`Synchronous Load (Dalamud): `** {(_tmp.Services.GetRequiredService<DalamudServices>().GetDalamudConfig(DalamudServices.WaitingForPluginsOption, out bool v) ? v.ToString() : "Unknown")}\n");
sb.Append( $"> **`Logging: `** Log: {Config.EnableResourceLogging}, Watcher: {Config.EnableResourceWatcher} ({Config.MaxResourceWatcherRecords})\n" ); sb.Append(
$"> **`Logging: `** Log: {Config.EnableResourceLogging}, Watcher: {Config.EnableResourceWatcher} ({Config.MaxResourceWatcherRecords})\n");
sb.Append($"> **`Use Ownership: `** {Config.UseOwnerNameForCharacterCollection}\n"); sb.Append($"> **`Use Ownership: `** {Config.UseOwnerNameForCharacterCollection}\n");
sb.AppendLine("**Mods**"); sb.AppendLine("**Mods**");
sb.Append($"> **`Installed Mods: `** {ModManager.Count}\n"); sb.Append($"> **`Installed Mods: `** {ModManager.Count}\n");
sb.Append($"> **`Mods with Config: `** {ModManager.Count(m => m.HasOptions)}\n"); sb.Append($"> **`Mods with Config: `** {ModManager.Count(m => m.HasOptions)}\n");
sb.Append( $"> **`Mods with File Redirections: `** {ModManager.Count( m => m.TotalFileCount > 0 )}, Total: {ModManager.Sum( m => m.TotalFileCount )}\n" ); sb.Append(
sb.Append( $"> **`Mods with FileSwaps: `** {ModManager.Count( m => m.TotalSwapCount > 0 )}, Total: {ModManager.Sum( m => m.TotalSwapCount )}\n" ); $"> **`Mods with File Redirections: `** {ModManager.Count(m => m.TotalFileCount > 0)}, Total: {ModManager.Sum(m => m.TotalFileCount)}\n");
sb.Append( $"> **`Mods with Meta Manipulations:`** {ModManager.Count( m => m.TotalManipulations > 0 )}, Total {ModManager.Sum( m => m.TotalManipulations )}\n" ); sb.Append(
$"> **`Mods with FileSwaps: `** {ModManager.Count(m => m.TotalSwapCount > 0)}, Total: {ModManager.Sum(m => m.TotalSwapCount)}\n");
sb.Append(
$"> **`Mods with Meta Manipulations:`** {ModManager.Count(m => m.TotalManipulations > 0)}, Total {ModManager.Sum(m => m.TotalManipulations)}\n");
sb.Append($"> **`IMC Exceptions Thrown: `** {ValidityChecker.ImcExceptions.Count}\n"); sb.Append($"> **`IMC Exceptions Thrown: `** {ValidityChecker.ImcExceptions.Count}\n");
sb.Append( $"> **`#Temp Mods: `** {TempMods.Mods.Sum( kvp => kvp.Value.Count ) + TempMods.ModsForAllCollections.Count}\n" ); sb.Append(
$"> **`#Temp Mods: `** {TempMods.Mods.Sum(kvp => kvp.Value.Count) + TempMods.ModsForAllCollections.Count}\n");
string CharacterName(ActorIdentifier id, string name) string CharacterName(ActorIdentifier id, string name)
{ {
if (id.Type is IdentifierType.Player or IdentifierType.Owned) if (id.Type is IdentifierType.Player or IdentifierType.Owned)
{ {
var parts = name.Split(' ', 3); var parts = name.Split(' ', 3);
return string.Join( " ", parts.Length != 3 ? parts.Select( n => $"{n[ 0 ]}." ) : parts[ ..2 ].Select( n => $"{n[ 0 ]}." ).Append( parts[ 2 ] ) ); return string.Join(" ",
parts.Length != 3 ? parts.Select(n => $"{n[0]}.") : parts[..2].Select(n => $"{n[0]}.").Append(parts[2]));
} }
return name + ':'; return name + ':';
@ -423,7 +355,7 @@ public class Penumbra : IDalamudPlugin
sb.AppendLine("**Collections**"); sb.AppendLine("**Collections**");
sb.Append($"> **`#Collections: `** {CollectionManager.Count - 1}\n"); sb.Append($"> **`#Collections: `** {CollectionManager.Count - 1}\n");
sb.Append( $"> **`#Temp Collections: `** {TempMods.CustomCollections.Count}\n" ); sb.Append($"> **`#Temp Collections: `** {TempCollections.Count}\n");
sb.Append($"> **`Active Collections: `** {CollectionManager.Count(c => c.HasCache)}\n"); sb.Append($"> **`Active Collections: `** {CollectionManager.Count(c => c.HasCache)}\n");
sb.Append($"> **`Base Collection: `** {CollectionManager.Default.AnonymizedName}\n"); sb.Append($"> **`Base Collection: `** {CollectionManager.Default.AnonymizedName}\n");
sb.Append($"> **`Interface Collection: `** {CollectionManager.Interface.AnonymizedName}\n"); sb.Append($"> **`Interface Collection: `** {CollectionManager.Interface.AnonymizedName}\n");
@ -432,20 +364,14 @@ public class Penumbra : IDalamudPlugin
{ {
var collection = CollectionManager.ByType(type); var collection = CollectionManager.ByType(type);
if (collection != null) if (collection != null)
{
sb.Append($"> **`{name,-30}`** {collection.AnonymizedName}\n"); sb.Append($"> **`{name,-30}`** {collection.AnonymizedName}\n");
} }
}
foreach (var (name, id, collection) in CollectionManager.Individuals.Assignments) foreach (var (name, id, collection) in CollectionManager.Individuals.Assignments)
{
sb.Append($"> **`{CharacterName(id[0], name),-30}`** {collection.AnonymizedName}\n"); sb.Append($"> **`{CharacterName(id[0], name),-30}`** {collection.AnonymizedName}\n");
}
foreach (var collection in CollectionManager.Where(c => c.HasCache)) foreach (var collection in CollectionManager.Where(c => c.HasCache))
{
PrintCollection(collection); PrintCollection(collection);
}
return sb.ToString(); return sb.ToString();
} }

View file

@ -1,11 +1,14 @@
using System.IO; using System;
using Dalamud.Plugin; using Dalamud.Plugin;
using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection;
using OtterGui.Classes; using OtterGui.Classes;
using OtterGui.Log; using OtterGui.Log;
using Penumbra.Api;
using Penumbra.Collections;
using Penumbra.GameData; using Penumbra.GameData;
using Penumbra.GameData.Data; using Penumbra.GameData.Data;
using Penumbra.Interop; using Penumbra.Interop;
using Penumbra.Interop.Resolver;
using Penumbra.Services; using Penumbra.Services;
using Penumbra.Util; using Penumbra.Util;
@ -17,34 +20,60 @@ public class PenumbraNew
=> "Penumbra"; => "Penumbra";
public static readonly Logger Log = new(); public static readonly Logger Log = new();
public readonly StartTimeTracker<StartTimeType> StartTimer = new(); public readonly ServiceProvider Services;
public readonly IServiceCollection Services = new ServiceCollection();
public PenumbraNew(DalamudPluginInterface pi) public PenumbraNew(DalamudPluginInterface pi)
{ {
using var time = StartTimer.Measure(StartTimeType.Total); var startTimer = new StartTracker();
using var time = startTimer.Measure(StartTimeType.Total);
var services = new ServiceCollection();
// Add meta services. // Add meta services.
Services.AddSingleton(Log); services.AddSingleton(Log)
Services.AddSingleton(StartTimer); .AddSingleton(startTimer)
Services.AddSingleton<ValidityChecker>(); .AddSingleton<ValidityChecker>()
Services.AddSingleton<PerformanceTracker<PerformanceType>>(); .AddSingleton<PerformanceTracker>()
.AddSingleton<FilenameService>()
.AddSingleton<BackupService>()
.AddSingleton<CommunicatorService>();
// Add Dalamud services // Add Dalamud services
var dalamud = new DalamudServices(pi); var dalamud = new DalamudServices(pi);
dalamud.AddServices(Services); dalamud.AddServices(services);
// Add Game Data // Add Game Data
Services.AddSingleton<GameEventManager>(); services.AddSingleton<IGamePathParser, GamePathParser>()
Services.AddSingleton<IGamePathParser, GamePathParser>(); .AddSingleton<IdentifierService>()
Services.AddSingleton<IObjectIdentifier, ObjectIdentifier>(); .AddSingleton<StainService>()
.AddSingleton<ItemService>()
.AddSingleton<ActorService>();
// Add Game Services
services.AddSingleton<GameEventManager>()
.AddSingleton<FrameworkManager>()
.AddSingleton<MetaFileManager>()
.AddSingleton<CutsceneCharacters>()
.AddSingleton<CharacterUtility>();
// Add Configuration // Add Configuration
Services.AddSingleton<Configuration>(); services.AddTransient<ConfigMigrationService>()
.AddSingleton<Configuration>();
// Add Collection Services
services.AddTransient<IndividualCollections>()
.AddSingleton<TempCollectionManager>();
// Add Mod Services
// TODO
services.AddSingleton<TempModManager>();
// Add Interface
Services = services.BuildServiceProvider(new ServiceProviderOptions { ValidateOnBuild = true });
} }
public void Dispose() public void Dispose()
{ } {
Services.Dispose();
}
} }

View file

@ -0,0 +1,29 @@
using System.Collections.Generic;
using System.IO;
using System.Linq;
using OtterGui.Classes;
using OtterGui.Log;
using Penumbra.Util;
namespace Penumbra.Services;
public class BackupService
{
public BackupService(Logger logger, StartTracker timer, FilenameService fileNames)
{
using var t = timer.Measure(StartTimeType.Backup);
var files = PenumbraFiles(fileNames);
Backup.CreateBackup(logger, new DirectoryInfo(fileNames.ConfigDirectory), files);
}
// Collect all relevant files for penumbra configuration.
private static IReadOnlyList<FileInfo> PenumbraFiles(FilenameService fileNames)
{
var list = fileNames.CollectionFiles.ToList();
list.AddRange(fileNames.LocalDataFiles);
list.Add(new FileInfo(fileNames.ConfigFile));
list.Add(new FileInfo(fileNames.FilesystemFile));
list.Add(new FileInfo(fileNames.ActiveCollectionsFile));
return list;
}
}

View file

@ -0,0 +1,30 @@
using System;
using Penumbra.Collections;
using Penumbra.Mods;
using Penumbra.Util;
namespace Penumbra.Services;
public class CommunicatorService : IDisposable
{
/// <summary> <list type="number">
/// <item>Parameter is the type of the changed collection. (Inactive or Temporary for additions or deletions)</item>
/// <item>Parameter is the old collection, or null on additions.</item>
/// <item>Parameter is the new collection, or null on deletions.</item>
/// <item>Parameter is the display name for Individual collections or an empty string otherwise.</item>
/// </list> </summary>
public readonly EventWrapper<CollectionType, ModCollection?, ModCollection?, string> CollectionChange = new(nameof(CollectionChange));
/// <summary> <list type="number">
/// <item>Parameter added, deleted or edited temporary mod.</item>
/// <item>Parameter is whether the mod was newly created.</item>
/// <item>Parameter is whether the mod was deleted.</item>
/// </list> </summary>
public readonly EventWrapper<Mod.TemporaryMod, bool, bool> TemporaryGlobalModChange = new(nameof(TemporaryGlobalModChange));
public void Dispose()
{
CollectionChange.Dispose();
TemporaryGlobalModChange.Dispose();
}
}

View file

@ -0,0 +1,377 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using Dalamud.Plugin;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using OtterGui.Filesystem;
using Penumbra.Collections;
using Penumbra.Mods;
using Penumbra.UI.Classes;
using SixLabors.ImageSharp;
namespace Penumbra.Services;
/// <summary>
/// Contains everything to migrate from older versions of the config to the current,
/// including deprecated fields.
/// </summary>
public class ConfigMigrationService
{
private readonly FilenameService _fileNames;
private readonly DalamudPluginInterface _pluginInterface;
private Configuration _config = null!;
private JObject _data = null!;
public string CurrentCollection = ModCollection.DefaultCollection;
public string DefaultCollection = ModCollection.DefaultCollection;
public string ForcedCollection = string.Empty;
public Dictionary<string, string> CharacterCollections = new();
public Dictionary<string, string> ModSortOrder = new();
public bool InvertModListOrder;
public bool SortFoldersFirst;
public SortModeV3 SortMode = SortModeV3.FoldersFirst;
public ConfigMigrationService(FilenameService fileNames, DalamudPluginInterface pi)
{
_fileNames = fileNames;
_pluginInterface = pi;
}
/// <summary> Add missing colors to the dictionary if necessary. </summary>
private static void AddColors(Configuration config, bool forceSave)
{
var save = false;
foreach (var color in Enum.GetValues<ColorId>())
{
save |= config.Colors.TryAdd(color, color.Data().DefaultColor);
}
if (save || forceSave)
{
config.Save();
}
}
public void Migrate(Configuration config)
{
_config = config;
// Do this on every migration from now on for a while
// because it stayed alive for a bunch of people for some reason.
DeleteMetaTmp();
if (config.Version >= Configuration.Constants.CurrentVersion || !File.Exists(_fileNames.ConfigFile))
{
AddColors(config, false);
return;
}
_data = JObject.Parse(File.ReadAllText(_fileNames.ConfigFile));
CreateBackup();
Version0To1();
Version1To2();
Version2To3();
Version3To4();
Version4To5();
Version5To6();
Version6To7();
AddColors(config, true);
}
// Gendered special collections were added.
private void Version6To7()
{
if (_config.Version != 6)
return;
ModCollection.Manager.MigrateUngenderedCollections(_fileNames);
_config.Version = 7;
}
// A new tutorial step was inserted in the middle.
// The UI collection and a new tutorial for it was added.
// The migration for the UI collection itself happens in the ActiveCollections file.
private void Version5To6()
{
if (_config.Version != 5)
return;
if (_config.TutorialStep == 25)
_config.TutorialStep = 27;
_config.Version = 6;
}
// Mod backup extension was changed from .zip to .pmp.
// Actual migration takes place in ModManager.
private void Version4To5()
{
if (_config.Version != 4)
return;
Mod.Manager.MigrateModBackups = true;
_config.Version = 5;
}
// SortMode was changed from an enum to a type.
private void Version3To4()
{
if (_config.Version != 3)
return;
SortMode = _data[nameof(SortMode)]?.ToObject<SortModeV3>() ?? SortMode;
_config.SortMode = SortMode switch
{
SortModeV3.FoldersFirst => ISortMode<Mod>.FoldersFirst,
SortModeV3.Lexicographical => ISortMode<Mod>.Lexicographical,
SortModeV3.InverseFoldersFirst => ISortMode<Mod>.InverseFoldersFirst,
SortModeV3.InverseLexicographical => ISortMode<Mod>.InverseLexicographical,
SortModeV3.FoldersLast => ISortMode<Mod>.FoldersLast,
SortModeV3.InverseFoldersLast => ISortMode<Mod>.InverseFoldersLast,
SortModeV3.InternalOrder => ISortMode<Mod>.InternalOrder,
SortModeV3.InternalOrderInverse => ISortMode<Mod>.InverseInternalOrder,
_ => ISortMode<Mod>.FoldersFirst,
};
_config.Version = 4;
}
// SortFoldersFirst was changed from a bool to the enum SortMode.
private void Version2To3()
{
if (_config.Version != 2)
return;
SortFoldersFirst = _data[nameof(SortFoldersFirst)]?.ToObject<bool>() ?? false;
SortMode = SortFoldersFirst ? SortModeV3.FoldersFirst : SortModeV3.Lexicographical;
_config.Version = 3;
}
// The forced collection was removed due to general inheritance.
// Sort Order was moved to a separate file and may contain empty folders.
// Active collections in general were moved to their own file.
// Delete the penumbrametatmp folder if it exists.
private void Version1To2()
{
if (_config.Version != 1)
return;
// Ensure the right meta files are loaded.
DeleteMetaTmp();
Penumbra.CharacterUtility.LoadCharacterResources();
ResettleSortOrder();
ResettleCollectionSettings();
ResettleForcedCollection();
_config.Version = 2;
}
private void DeleteMetaTmp()
{
var path = Path.Combine(_config.ModDirectory, "penumbrametatmp");
if (!Directory.Exists(path))
return;
try
{
Directory.Delete(path, true);
}
catch (Exception e)
{
Penumbra.Log.Error($"Could not delete the outdated penumbrametatmp folder:\n{e}");
}
}
private void ResettleForcedCollection()
{
ForcedCollection = _data[nameof(ForcedCollection)]?.ToObject<string>() ?? ForcedCollection;
if (ForcedCollection.Length <= 0)
return;
// Add the previous forced collection to all current collections except itself as an inheritance.
foreach (var collection in _fileNames.CollectionFiles)
{
try
{
var jObject = JObject.Parse(File.ReadAllText(collection.FullName));
if (jObject[nameof(ModCollection.Name)]?.ToObject<string>() == ForcedCollection)
continue;
jObject[nameof(ModCollection.Inheritance)] = JToken.FromObject(new List<string> { ForcedCollection });
File.WriteAllText(collection.FullName, jObject.ToString());
}
catch (Exception e)
{
Penumbra.Log.Error(
$"Could not transfer forced collection {ForcedCollection} to inheritance of collection {collection}:\n{e}");
}
}
}
// Move the current sort order to its own file.
private void ResettleSortOrder()
{
ModSortOrder = _data[nameof(ModSortOrder)]?.ToObject<Dictionary<string, string>>() ?? ModSortOrder;
var file = _fileNames.FilesystemFile;
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);
j.Formatting = Formatting.Indented;
j.WriteStartObject();
j.WritePropertyName("Data");
j.WriteStartObject();
foreach (var (mod, path) in ModSortOrder.Where(kvp => Directory.Exists(Path.Combine(_config.ModDirectory, kvp.Key))))
{
j.WritePropertyName(mod, true);
j.WriteValue(path);
}
j.WriteEndObject();
j.WritePropertyName("EmptyFolders");
j.WriteStartArray();
j.WriteEndArray();
j.WriteEndObject();
}
// Move the active collections to their own file.
private void ResettleCollectionSettings()
{
CurrentCollection = _data[nameof(CurrentCollection)]?.ToObject<string>() ?? CurrentCollection;
DefaultCollection = _data[nameof(DefaultCollection)]?.ToObject<string>() ?? DefaultCollection;
CharacterCollections = _data[nameof(CharacterCollections)]?.ToObject<Dictionary<string, string>>() ?? CharacterCollections;
SaveActiveCollectionsV0(DefaultCollection, CurrentCollection, DefaultCollection,
CharacterCollections.Select(kvp => (kvp.Key, kvp.Value)), Array.Empty<(CollectionType, string)>());
}
// Outdated saving using the Characters list.
private void SaveActiveCollectionsV0(string def, string ui, string current, IEnumerable<(string, string)> characters,
IEnumerable<(CollectionType, string)> special)
{
var file = _fileNames.ActiveCollectionsFile;
try
{
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);
j.Formatting = Formatting.Indented;
j.WriteStartObject();
j.WritePropertyName(nameof(ModCollection.Manager.Default));
j.WriteValue(def);
j.WritePropertyName(nameof(ModCollection.Manager.Interface));
j.WriteValue(ui);
j.WritePropertyName(nameof(ModCollection.Manager.Current));
j.WriteValue(current);
foreach (var (type, collection) in special)
{
j.WritePropertyName(type.ToString());
j.WriteValue(collection);
}
j.WritePropertyName("Characters");
j.WriteStartObject();
foreach (var (character, collection) in characters)
{
j.WritePropertyName(character, true);
j.WriteValue(collection);
}
j.WriteEndObject();
j.WriteEndObject();
Penumbra.Log.Verbose("Active Collections saved.");
}
catch (Exception e)
{
Penumbra.Log.Error($"Could not save active collections to file {file}:\n{e}");
}
}
// Collections were introduced and the previous CurrentCollection got put into ModDirectory.
private void Version0To1()
{
if (_config.Version != 0)
return;
_config.ModDirectory = _data[nameof(CurrentCollection)]?.ToObject<string>() ?? string.Empty;
_config.Version = 1;
ResettleCollectionJson();
}
// Move the previous mod configurations to a new default collection file.
private void ResettleCollectionJson()
{
var collectionJson = new FileInfo(Path.Combine(_config.ModDirectory, "collection.json"));
if (!collectionJson.Exists)
return;
var defaultCollection = ModCollection.CreateNewEmpty(ModCollection.DefaultCollection);
var defaultCollectionFile = defaultCollection.FileName;
if (defaultCollectionFile.Exists)
return;
try
{
var text = File.ReadAllText(collectionJson.FullName);
var data = JArray.Parse(text);
var maxPriority = 0;
var dict = new Dictionary<string, ModSettings.SavedSettings>();
foreach (var setting in data.Cast<JObject>())
{
var modName = (string)setting["FolderName"]!;
var enabled = (bool)setting["Enabled"]!;
var priority = (int)setting["Priority"]!;
var settings = setting["Settings"]!.ToObject<Dictionary<string, long>>()
?? setting["Conf"]!.ToObject<Dictionary<string, long>>();
dict[modName] = new ModSettings.SavedSettings()
{
Enabled = enabled,
Priority = priority,
Settings = settings!,
};
maxPriority = Math.Max(maxPriority, priority);
}
InvertModListOrder = _data[nameof(InvertModListOrder)]?.ToObject<bool>() ?? InvertModListOrder;
if (!InvertModListOrder)
dict = dict.ToDictionary(kvp => kvp.Key, kvp => kvp.Value with { Priority = maxPriority - kvp.Value.Priority });
defaultCollection = ModCollection.MigrateFromV0(ModCollection.DefaultCollection, dict);
defaultCollection.Save();
}
catch (Exception e)
{
Penumbra.Log.Error($"Could not migrate the old collection file to new collection files:\n{e}");
throw;
}
}
// Create a backup of the configuration file specifically.
private void CreateBackup()
{
var name = _fileNames.ConfigFile;
var bakName = name + ".bak";
try
{
File.Copy(name, bakName, true);
}
catch (Exception e)
{
Penumbra.Log.Error($"Could not create backup copy of config at {bakName}:\n{e}");
}
}
public enum SortModeV3 : byte
{
FoldersFirst = 0x00,
Lexicographical = 0x01,
InverseFoldersFirst = 0x02,
InverseLexicographical = 0x03,
FoldersLast = 0x04,
InverseFoldersLast = 0x05,
InternalOrder = 0x06,
InternalOrderInverse = 0x07,
}
}

View file

@ -79,20 +79,21 @@ public class DalamudServices
services.AddSingleton(this); services.AddSingleton(this);
} }
// TODO remove static
// @formatter:off // @formatter:off
[PluginService][RequiredVersion("1.0")] public DalamudPluginInterface PluginInterface { get; private set; } = null!; [PluginService][RequiredVersion("1.0")] public static DalamudPluginInterface PluginInterface { get; private set; } = null!;
[PluginService][RequiredVersion("1.0")] public CommandManager Commands { get; private set; } = null!; [PluginService][RequiredVersion("1.0")] public static CommandManager Commands { get; private set; } = null!;
[PluginService][RequiredVersion("1.0")] public DataManager GameData { get; private set; } = null!; [PluginService][RequiredVersion("1.0")] public static DataManager GameData { get; private set; } = null!;
[PluginService][RequiredVersion("1.0")] public ClientState ClientState { get; private set; } = null!; [PluginService][RequiredVersion("1.0")] public static ClientState ClientState { get; private set; } = null!;
[PluginService][RequiredVersion("1.0")] public ChatGui Chat { get; private set; } = null!; [PluginService][RequiredVersion("1.0")] public static ChatGui Chat { get; private set; } = null!;
[PluginService][RequiredVersion("1.0")] public Framework Framework { get; private set; } = null!; [PluginService][RequiredVersion("1.0")] public static Framework Framework { get; private set; } = null!;
[PluginService][RequiredVersion("1.0")] public Condition Conditions { get; private set; } = null!; [PluginService][RequiredVersion("1.0")] public static Condition Conditions { get; private set; } = null!;
[PluginService][RequiredVersion("1.0")] public TargetManager Targets { get; private set; } = null!; [PluginService][RequiredVersion("1.0")] public static TargetManager Targets { get; private set; } = null!;
[PluginService][RequiredVersion("1.0")] public ObjectTable Objects { get; private set; } = null!; [PluginService][RequiredVersion("1.0")] public static ObjectTable Objects { get; private set; } = null!;
[PluginService][RequiredVersion("1.0")] public TitleScreenMenu TitleScreenMenu { get; private set; } = null!; [PluginService][RequiredVersion("1.0")] public static TitleScreenMenu TitleScreenMenu { get; private set; } = null!;
[PluginService][RequiredVersion("1.0")] public GameGui GameGui { get; private set; } = null!; [PluginService][RequiredVersion("1.0")] public static GameGui GameGui { get; private set; } = null!;
[PluginService][RequiredVersion("1.0")] public KeyState KeyState { get; private set; } = null!; [PluginService][RequiredVersion("1.0")] public static KeyState KeyState { get; private set; } = null!;
[PluginService][RequiredVersion("1.0")] public SigScanner SigScanner { get; private set; } = null!; [PluginService][RequiredVersion("1.0")] public static SigScanner SigScanner { get; private set; } = null!;
// @formatter:on // @formatter:on
public const string WaitingForPluginsOption = "IsResumeGameAfterPluginLoad"; public const string WaitingForPluginsOption = "IsResumeGameAfterPluginLoad";

View file

@ -0,0 +1,51 @@
using System;
using System.Collections.Generic;
using System.IO;
using Dalamud.Plugin;
using OtterGui.Filesystem;
namespace Penumbra.Services;
public class FilenameService
{
public readonly string ConfigDirectory;
public readonly string CollectionDirectory;
public readonly string LocalDataDirectory;
public readonly string ConfigFile;
public readonly string FilesystemFile;
public readonly string ActiveCollectionsFile;
public FilenameService(DalamudPluginInterface pi)
{
ConfigDirectory = pi.ConfigDirectory.FullName;
CollectionDirectory = Path.Combine(pi.GetPluginConfigDirectory(), "collections");
LocalDataDirectory = Path.Combine(pi.ConfigDirectory.FullName, "mod_data");
ConfigFile = pi.ConfigFile.FullName;
FilesystemFile = Path.Combine(pi.GetPluginConfigDirectory(), "sort_order.json");
ActiveCollectionsFile = Path.Combine(pi.ConfigDirectory.FullName, "active_collections.json");
}
public string CollectionFile(string collectionName)
=> Path.Combine(CollectionDirectory, $"{collectionName.RemoveInvalidPathSymbols()}.json");
public string LocalDataFile(string modPath)
=> Path.Combine(LocalDataDirectory, $"{modPath}.json");
public IEnumerable<FileInfo> CollectionFiles
{
get
{
var directory = new DirectoryInfo(CollectionDirectory);
return directory.Exists ? directory.EnumerateFiles("*.json") : Array.Empty<FileInfo>();
}
}
public IEnumerable<FileInfo> LocalDataFiles
{
get
{
var directory = new DirectoryInfo(LocalDataDirectory);
return directory.Exists ? directory.EnumerateFiles("*.json") : Array.Empty<FileInfo>();
}
}
}

View file

@ -1,66 +0,0 @@
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using Dalamud.Data;
using Dalamud.Plugin;
using Lumina.Excel.GeneratedSheets;
using OtterGui.Classes;
using Penumbra.GameData;
using Penumbra.GameData.Enums;
using Penumbra.GameData.Structs;
using Penumbra.Util;
using Action = System.Action;
namespace Penumbra.Services;
public sealed class ObjectIdentifier : IObjectIdentifier
{
private const string Prefix = $"[{nameof(ObjectIdentifier)}]";
public IObjectIdentifier? Identifier { get; private set; }
public bool IsDisposed { get; private set; }
public bool Ready
=> Identifier != null && !IsDisposed;
public event Action? FinishedCreation;
public ObjectIdentifier(StartTimeTracker<StartTimeType> tracker, DalamudPluginInterface pi, DataManager data)
{
Task.Run(() =>
{
using var timer = tracker.Measure(StartTimeType.Identifier);
var identifier = GameData.GameData.GetIdentifier(pi, data);
if (IsDisposed)
{
identifier.Dispose();
}
else
{
Identifier = identifier;
Penumbra.Log.Verbose($"{Prefix} Created.");
FinishedCreation?.Invoke();
}
});
}
public void Dispose()
{
Identifier?.Dispose();
IsDisposed = true;
Penumbra.Log.Verbose($"{Prefix} Disposed.");
}
public IGamePathParser GamePathParser
=> Identifier?.GamePathParser ?? throw new Exception($"{Prefix} Not yet ready.");
public void Identify(IDictionary<string, object?> set, string path)
=> Identifier?.Identify(set, path);
public Dictionary<string, object?> Identify(string path)
=> Identifier?.Identify(path) ?? new Dictionary<string, object?>();
public IEnumerable<Item> Identify(SetId setId, WeaponType weaponType, ushort variant, EquipSlot slot)
=> Identifier?.Identify(setId, weaponType, variant, slot) ?? Array.Empty<Item>();
}

View file

@ -0,0 +1,117 @@
using System;
using System.Threading.Tasks;
using OtterGui.Classes;
using Penumbra.Util;
namespace Penumbra.Services;
public interface IServiceWrapper<out T> : IDisposable
{
public string Name { get; }
public T? Service { get; }
public bool Valid { get; }
}
public abstract class SyncServiceWrapper<T> : IServiceWrapper<T>
{
public string Name { get; }
public T Service { get; }
private bool _isDisposed;
public bool Valid
=> !_isDisposed;
protected SyncServiceWrapper(string name, StartTracker tracker, StartTimeType type, Func<T> factory)
{
Name = name;
using var timer = tracker.Measure(type);
Service = factory();
Penumbra.Log.Verbose($"[{Name}] Created.");
}
public void Dispose()
{
if (_isDisposed)
return;
_isDisposed = true;
if (Service is IDisposable d)
d.Dispose();
Penumbra.Log.Verbose($"[{Name}] Disposed.");
}
}
public abstract class AsyncServiceWrapper<T> : IServiceWrapper<T>
{
public string Name { get; }
public T? Service { get; private set; }
public T AwaitedService
{
get
{
_task.Wait();
return Service!;
}
}
public bool Valid
=> Service != null && !_isDisposed;
public event Action? FinishedCreation;
private readonly Task _task;
private bool _isDisposed;
protected AsyncServiceWrapper(string name, StartTracker tracker, StartTimeType type, Func<T> factory)
{
Name = name;
_task = Task.Run(() =>
{
using var timer = tracker.Measure(type);
var service = factory();
if (_isDisposed)
{
if (service is IDisposable d)
d.Dispose();
}
else
{
Service = service;
Penumbra.Log.Verbose($"[{Name}] Created.");
FinishedCreation?.Invoke();
}
});
}
protected AsyncServiceWrapper(string name, Func<T> factory)
{
Name = name;
_task = Task.Run(() =>
{
var service = factory();
if (_isDisposed)
{
if (service is IDisposable d)
d.Dispose();
}
else
{
Service = service;
Penumbra.Log.Verbose($"[{Name}] Created.");
FinishedCreation?.Invoke();
}
});
}
public void Dispose()
{
if (_isDisposed)
return;
_isDisposed = true;
if (Service is IDisposable d)
d.Dispose();
Penumbra.Log.Verbose($"[{Name}] Disposed.");
}
}

View file

@ -0,0 +1,43 @@
using System;
using System.Collections.Generic;
using System.Linq;
using Dalamud.Data;
using Dalamud.Plugin;
using OtterGui.Classes;
using OtterGui.Widgets;
using Penumbra.GameData.Data;
using Penumbra.GameData.Files;
using Penumbra.Util;
namespace Penumbra.Services;
public class StainService : IDisposable
{
public sealed class StainTemplateCombo : FilterComboCache<ushort>
{
public StainTemplateCombo(IEnumerable<ushort> items)
: base(items)
{ }
}
public readonly StainData StainData;
public readonly FilterComboColors StainCombo;
public readonly StmFile StmFile;
public readonly StainTemplateCombo TemplateCombo;
public StainService(StartTracker timer, DalamudPluginInterface pluginInterface, DataManager dataManager)
{
using var t = timer.Measure(StartTimeType.Stains);
StainData = new StainData(pluginInterface, dataManager, dataManager.Language);
StainCombo = new FilterComboColors(140, StainData.Data.Prepend(new KeyValuePair<byte, (string Name, uint Dye, bool Gloss)>(0, ("None", 0, false))));
StmFile = new StmFile(dataManager);
TemplateCombo = new StainTemplateCombo(StmFile.Entries.Keys.Prepend((ushort)0));
Penumbra.Log.Verbose($"[{nameof(StainService)}] Created.");
}
public void Dispose()
{
StainData.Dispose();
Penumbra.Log.Verbose($"[{nameof(StainService)}] Disposed.");
}
}

View file

@ -0,0 +1,37 @@
using Dalamud.Data;
using Dalamud.Game;
using Dalamud.Game.ClientState;
using Dalamud.Game.ClientState.Objects;
using Dalamud.Game.Gui;
using Dalamud.Plugin;
using OtterGui.Classes;
using Penumbra.GameData;
using Penumbra.GameData.Actors;
using Penumbra.GameData.Data;
using Penumbra.Interop.Resolver;
using Penumbra.Util;
namespace Penumbra.Services;
public sealed class IdentifierService : AsyncServiceWrapper<IObjectIdentifier>
{
public IdentifierService(StartTracker tracker, DalamudPluginInterface pi, DataManager data)
: base(nameof(IdentifierService), tracker, StartTimeType.Identifier, () => GameData.GameData.GetIdentifier(pi, data))
{ }
}
public sealed class ItemService : AsyncServiceWrapper<ItemData>
{
public ItemService(StartTracker tracker, DalamudPluginInterface pi, DataManager gameData)
: base(nameof(ItemService), tracker, StartTimeType.Items, () => new ItemData(pi, gameData, gameData.Language))
{ }
}
public sealed class ActorService : AsyncServiceWrapper<ActorManager>
{
public ActorService(StartTracker tracker, DalamudPluginInterface pi, ObjectTable objects, ClientState clientState,
Framework framework, DataManager gameData, GameGui gui, CutsceneCharacters cutscene)
: base(nameof(ActorService), tracker, StartTimeType.Actors,
() => new ActorManager(pi, objects, clientState, framework, gameData, gui, idx => (short)cutscene.GetParentIndex(idx)))
{ }
}

View file

@ -17,6 +17,7 @@ using Penumbra.GameData.Enums;
using Penumbra.GameData.Structs; using Penumbra.GameData.Structs;
using Penumbra.Mods; using Penumbra.Mods;
using Penumbra.Mods.ItemSwap; using Penumbra.Mods.ItemSwap;
using Penumbra.Services;
using Penumbra.Util; using Penumbra.Util;
namespace Penumbra.UI.Classes; namespace Penumbra.UI.Classes;
@ -62,29 +63,41 @@ public class ItemSwapWindow : IDisposable
=> type.ToName(); => type.ToName();
} }
public ItemSwapWindow() private readonly CommunicatorService _communicator;
public ItemSwapWindow(CommunicatorService communicator)
{ {
Penumbra.CollectionManager.CollectionChanged += OnCollectionChange; _communicator = communicator;
_communicator.CollectionChange.Event += OnCollectionChange;
Penumbra.CollectionManager.Current.ModSettingChanged += OnSettingChange; Penumbra.CollectionManager.Current.ModSettingChanged += OnSettingChange;
} }
public void Dispose() public void Dispose()
{ {
Penumbra.CollectionManager.CollectionChanged += OnCollectionChange; _communicator.CollectionChange.Event -= OnCollectionChange;
Penumbra.CollectionManager.Current.ModSettingChanged -= OnSettingChange; Penumbra.CollectionManager.Current.ModSettingChanged -= OnSettingChange;
} }
private readonly Dictionary<SwapType, (ItemSelector Source, ItemSelector Target, string TextFrom, string TextTo)> _selectors = new() private readonly Dictionary<SwapType, (ItemSelector Source, ItemSelector Target, string TextFrom, string TextTo)> _selectors = new()
{ {
[ SwapType.Hat ] = ( new ItemSelector( FullEquipType.Head ), new ItemSelector( FullEquipType.Head ), "Take this Hat", "and put it on this one" ), [SwapType.Hat] =
[ SwapType.Top ] = ( new ItemSelector( FullEquipType.Body ), new ItemSelector( FullEquipType.Body ), "Take this Top", "and put it on this one" ), (new ItemSelector(FullEquipType.Head), new ItemSelector(FullEquipType.Head), "Take this Hat", "and put it on this one"),
[ SwapType.Gloves ] = ( new ItemSelector( FullEquipType.Hands ), new ItemSelector( FullEquipType.Hands ), "Take these Gloves", "and put them on these" ), [SwapType.Top] =
[ SwapType.Pants ] = ( new ItemSelector( FullEquipType.Legs ), new ItemSelector( FullEquipType.Legs ), "Take these Pants", "and put them on these" ), (new ItemSelector(FullEquipType.Body), new ItemSelector(FullEquipType.Body), "Take this Top", "and put it on this one"),
[ SwapType.Shoes ] = ( new ItemSelector( FullEquipType.Feet ), new ItemSelector( FullEquipType.Feet ), "Take these Shoes", "and put them on these" ), [SwapType.Gloves] =
[ SwapType.Earrings ] = ( new ItemSelector( FullEquipType.Ears ), new ItemSelector( FullEquipType.Ears ), "Take these Earrings", "and put them on these" ), (new ItemSelector(FullEquipType.Hands), new ItemSelector(FullEquipType.Hands), "Take these Gloves", "and put them on these"),
[ SwapType.Necklace ] = ( new ItemSelector( FullEquipType.Neck ), new ItemSelector( FullEquipType.Neck ), "Take this Necklace", "and put it on this one" ), [SwapType.Pants] =
[ SwapType.Bracelet ] = ( new ItemSelector( FullEquipType.Wrists ), new ItemSelector( FullEquipType.Wrists ), "Take these Bracelets", "and put them on these" ), (new ItemSelector(FullEquipType.Legs), new ItemSelector(FullEquipType.Legs), "Take these Pants", "and put them on these"),
[ SwapType.Ring ] = ( new ItemSelector( FullEquipType.Finger ), new ItemSelector( FullEquipType.Finger ), "Take this Ring", "and put it on this one" ), [SwapType.Shoes] =
(new ItemSelector(FullEquipType.Feet), new ItemSelector(FullEquipType.Feet), "Take these Shoes", "and put them on these"),
[SwapType.Earrings] =
(new ItemSelector(FullEquipType.Ears), new ItemSelector(FullEquipType.Ears), "Take these Earrings", "and put them on these"),
[SwapType.Necklace] =
(new ItemSelector(FullEquipType.Neck), new ItemSelector(FullEquipType.Neck), "Take this Necklace", "and put it on this one"),
[SwapType.Bracelet] =
(new ItemSelector(FullEquipType.Wrists), new ItemSelector(FullEquipType.Wrists), "Take these Bracelets", "and put them on these"),
[SwapType.Ring] = (new ItemSelector(FullEquipType.Finger), new ItemSelector(FullEquipType.Finger), "Take this Ring",
"and put it on this one"),
}; };
private ItemSelector? _weaponSource = null; private ItemSelector? _weaponSource = null;
@ -120,15 +133,11 @@ public class ItemSwapWindow : IDisposable
public void UpdateMod(Mod mod, ModSettings? settings) public void UpdateMod(Mod mod, ModSettings? settings)
{ {
if (mod == _mod && settings == _modSettings) if (mod == _mod && settings == _modSettings)
{
return; return;
}
var oldDefaultName = $"{_mod?.Name.Text ?? "Unknown"} (Swapped)"; var oldDefaultName = $"{_mod?.Name.Text ?? "Unknown"} (Swapped)";
if (_newModName.Length == 0 || oldDefaultName == _newModName) if (_newModName.Length == 0 || oldDefaultName == _newModName)
{
_newModName = $"{mod.Name.Text} (Swapped)"; _newModName = $"{mod.Name.Text} (Swapped)";
}
_mod = mod; _mod = mod;
_modSettings = settings; _modSettings = settings;
@ -140,9 +149,7 @@ public class ItemSwapWindow : IDisposable
private void UpdateState() private void UpdateState()
{ {
if (!_dirty) if (!_dirty)
{
return; return;
}
_swapData.Clear(); _swapData.Clear();
_loadException = null; _loadException = null;
@ -178,19 +185,23 @@ public class ItemSwapWindow : IDisposable
} }
break; break;
case SwapType.Hair when _targetId > 0 && _sourceId > 0: case SwapType.Hair when _targetId > 0 && _sourceId > 0:
_swapData.LoadCustomization( BodySlot.Hair, Names.CombinedRace( _currentGender, _currentRace ), ( SetId )_sourceId, ( SetId )_targetId, _swapData.LoadCustomization(BodySlot.Hair, Names.CombinedRace(_currentGender, _currentRace), (SetId)_sourceId,
(SetId)_targetId,
_useCurrentCollection ? Penumbra.CollectionManager.Current : null); _useCurrentCollection ? Penumbra.CollectionManager.Current : null);
break; break;
case SwapType.Face when _targetId > 0 && _sourceId > 0: case SwapType.Face when _targetId > 0 && _sourceId > 0:
_swapData.LoadCustomization( BodySlot.Face, Names.CombinedRace( _currentGender, _currentRace ), ( SetId )_sourceId, ( SetId )_targetId, _swapData.LoadCustomization(BodySlot.Face, Names.CombinedRace(_currentGender, _currentRace), (SetId)_sourceId,
(SetId)_targetId,
_useCurrentCollection ? Penumbra.CollectionManager.Current : null); _useCurrentCollection ? Penumbra.CollectionManager.Current : null);
break; break;
case SwapType.Ears when _targetId > 0 && _sourceId > 0: case SwapType.Ears when _targetId > 0 && _sourceId > 0:
_swapData.LoadCustomization( BodySlot.Zear, Names.CombinedRace( _currentGender, ModelRace.Viera ), ( SetId )_sourceId, ( SetId )_targetId, _swapData.LoadCustomization(BodySlot.Zear, Names.CombinedRace(_currentGender, ModelRace.Viera), (SetId)_sourceId,
(SetId)_targetId,
_useCurrentCollection ? Penumbra.CollectionManager.Current : null); _useCurrentCollection ? Penumbra.CollectionManager.Current : null);
break; break;
case SwapType.Tail when _targetId > 0 && _sourceId > 0: case SwapType.Tail when _targetId > 0 && _sourceId > 0:
_swapData.LoadCustomization( BodySlot.Tail, Names.CombinedRace( _currentGender, _currentRace ), ( SetId )_sourceId, ( SetId )_targetId, _swapData.LoadCustomization(BodySlot.Tail, Names.CombinedRace(_currentGender, _currentRace), (SetId)_sourceId,
(SetId)_targetId,
_useCurrentCollection ? Penumbra.CollectionManager.Current : null); _useCurrentCollection ? Penumbra.CollectionManager.Current : null);
break; break;
case SwapType.Weapon: break; case SwapType.Weapon: break;
@ -212,7 +223,8 @@ public class ItemSwapWindow : IDisposable
return swap switch return swap switch
{ {
MetaSwap meta => $"{meta.SwapFrom}: {meta.SwapFrom.EntryToString()} -> {meta.SwapApplied.EntryToString()}", MetaSwap meta => $"{meta.SwapFrom}: {meta.SwapFrom.EntryToString()} -> {meta.SwapApplied.EntryToString()}",
FileSwap file => $"{file.Type}: {file.SwapFromRequestPath} -> {file.SwapToModded.FullName}{( file.DataWasChanged ? " (EDITED)" : string.Empty )}", FileSwap file =>
$"{file.Type}: {file.SwapFromRequestPath} -> {file.SwapToModded.FullName}{(file.DataWasChanged ? " (EDITED)" : string.Empty)}",
_ => string.Empty, _ => string.Empty,
}; };
} }
@ -223,7 +235,10 @@ public class ItemSwapWindow : IDisposable
private void UpdateOption() private void UpdateOption()
{ {
_selectedGroup = _mod?.Groups.FirstOrDefault(g => g.Name == _newGroupName); _selectedGroup = _mod?.Groups.FirstOrDefault(g => g.Name == _newGroupName);
_subModValid = _mod != null && _newGroupName.Length > 0 && _newOptionName.Length > 0 && ( _selectedGroup?.All( o => o.Name != _newOptionName ) ?? true ); _subModValid = _mod != null
&& _newGroupName.Length > 0
&& _newOptionName.Length > 0
&& (_selectedGroup?.All(o => o.Name != _newOptionName) ?? true);
} }
private void CreateMod() private void CreateMod()
@ -232,18 +247,15 @@ public class ItemSwapWindow : IDisposable
Mod.Creator.CreateMeta(newDir, _newModName, Penumbra.Config.DefaultModAuthor, CreateDescription(), "1.0", string.Empty); Mod.Creator.CreateMeta(newDir, _newModName, Penumbra.Config.DefaultModAuthor, CreateDescription(), "1.0", string.Empty);
Mod.Creator.CreateDefaultFiles(newDir); Mod.Creator.CreateDefaultFiles(newDir);
Penumbra.ModManager.AddMod(newDir); Penumbra.ModManager.AddMod(newDir);
if( !_swapData.WriteMod( Penumbra.ModManager.Last(), _useFileSwaps ? ItemSwapContainer.WriteType.UseSwaps : ItemSwapContainer.WriteType.NoSwaps ) ) if (!_swapData.WriteMod(Penumbra.ModManager.Last(),
{ _useFileSwaps ? ItemSwapContainer.WriteType.UseSwaps : ItemSwapContainer.WriteType.NoSwaps))
Penumbra.ModManager.DeleteMod(Penumbra.ModManager.Count - 1); Penumbra.ModManager.DeleteMod(Penumbra.ModManager.Count - 1);
} }
}
private void CreateOption() private void CreateOption()
{ {
if (_mod == null || !_subModValid) if (_mod == null || !_subModValid)
{
return; return;
}
var groupCreated = false; var groupCreated = false;
var dirCreated = false; var dirCreated = false;
@ -251,11 +263,11 @@ public class ItemSwapWindow : IDisposable
DirectoryInfo? optionFolderName = null; DirectoryInfo? optionFolderName = null;
try try
{ {
optionFolderName = Mod.Creator.NewSubFolderName( new DirectoryInfo( Path.Combine( _mod.ModPath.FullName, _selectedGroup?.Name ?? _newGroupName ) ), _newOptionName ); optionFolderName =
Mod.Creator.NewSubFolderName(new DirectoryInfo(Path.Combine(_mod.ModPath.FullName, _selectedGroup?.Name ?? _newGroupName)),
_newOptionName);
if (optionFolderName?.Exists == true) if (optionFolderName?.Exists == true)
{
throw new Exception($"The folder {optionFolderName.FullName} for the option already exists."); throw new Exception($"The folder {optionFolderName.FullName} for the option already exists.");
}
if (optionFolderName != null) if (optionFolderName != null)
{ {
@ -270,22 +282,19 @@ public class ItemSwapWindow : IDisposable
optionCreated = true; optionCreated = true;
optionFolderName = Directory.CreateDirectory(optionFolderName.FullName); optionFolderName = Directory.CreateDirectory(optionFolderName.FullName);
dirCreated = true; dirCreated = true;
if( !_swapData.WriteMod( _mod, _useFileSwaps ? ItemSwapContainer.WriteType.UseSwaps : ItemSwapContainer.WriteType.NoSwaps, optionFolderName, if (!_swapData.WriteMod(_mod, _useFileSwaps ? ItemSwapContainer.WriteType.UseSwaps : ItemSwapContainer.WriteType.NoSwaps,
optionFolderName,
_mod.Groups.IndexOf(_selectedGroup), _selectedGroup.Count - 1)) _mod.Groups.IndexOf(_selectedGroup), _selectedGroup.Count - 1))
{
throw new Exception("Failure writing files for mod swap."); throw new Exception("Failure writing files for mod swap.");
} }
} }
}
catch (Exception e) catch (Exception e)
{ {
ChatUtil.NotificationMessage($"Could not create new Swap Option:\n{e}", "Error", NotificationType.Error); ChatUtil.NotificationMessage($"Could not create new Swap Option:\n{e}", "Error", NotificationType.Error);
try try
{ {
if (optionCreated && _selectedGroup != null) if (optionCreated && _selectedGroup != null)
{
Penumbra.ModManager.DeleteOption(_mod, _mod.Groups.IndexOf(_selectedGroup), _selectedGroup.Count - 1); Penumbra.ModManager.DeleteOption(_mod, _mod.Groups.IndexOf(_selectedGroup), _selectedGroup.Count - 1);
}
if (groupCreated) if (groupCreated)
{ {
@ -294,10 +303,8 @@ public class ItemSwapWindow : IDisposable
} }
if (dirCreated && optionFolderName != null) if (dirCreated && optionFolderName != null)
{
Directory.Delete(optionFolderName.FullName, true); Directory.Delete(optionFolderName.FullName, true);
} }
}
catch catch
{ {
// ignored // ignored
@ -322,9 +329,7 @@ public class ItemSwapWindow : IDisposable
? "Please enter a name for your mod." ? "Please enter a name for your mod."
: "Create a new mod of the given name containing only the swap."; : "Create a new mod of the given name containing only the swap.";
if (ImGuiUtil.DrawDisabledButton("Create New Mod", new Vector2(width / 2, 0), tt, !newModAvailable || _newModName.Length == 0)) if (ImGuiUtil.DrawDisabledButton("Create New Mod", new Vector2(width / 2, 0), tt, !newModAvailable || _newModName.Length == 0))
{
CreateMod(); CreateMod();
}
ImGui.SameLine(); ImGui.SameLine();
ImGui.SetCursorPosX(ImGui.GetCursorPosX() + 20 * ImGuiHelpers.GlobalScale); ImGui.SetCursorPosX(ImGui.GetCursorPosX() + 20 * ImGuiHelpers.GlobalScale);
@ -334,16 +339,12 @@ public class ItemSwapWindow : IDisposable
ImGui.SetNextItemWidth((width - ImGui.GetStyle().ItemSpacing.X) / 2); ImGui.SetNextItemWidth((width - ImGui.GetStyle().ItemSpacing.X) / 2);
if (ImGui.InputTextWithHint("##groupName", "Group Name...", ref _newGroupName, 32)) if (ImGui.InputTextWithHint("##groupName", "Group Name...", ref _newGroupName, 32))
{
UpdateOption(); UpdateOption();
}
ImGui.SameLine(); ImGui.SameLine();
ImGui.SetNextItemWidth((width - ImGui.GetStyle().ItemSpacing.X) / 2); ImGui.SetNextItemWidth((width - ImGui.GetStyle().ItemSpacing.X) / 2);
if (ImGui.InputTextWithHint("##optionName", "New Option Name...", ref _newOptionName, 32)) if (ImGui.InputTextWithHint("##optionName", "New Option Name...", ref _newOptionName, 32))
{
UpdateOption(); UpdateOption();
}
ImGui.SameLine(); ImGui.SameLine();
tt = !_subModValid tt = !_subModValid
@ -352,14 +353,13 @@ public class ItemSwapWindow : IDisposable
? "Create a new option inside this mod containing only the swap." ? "Create a new option inside this mod containing only the swap."
: "Create a new option (and possibly Multi-Group) inside the currently selected mod containing the swap."; : "Create a new option (and possibly Multi-Group) inside the currently selected mod containing the swap.";
if (ImGuiUtil.DrawDisabledButton("Create New Option", new Vector2(width / 2, 0), tt, !newModAvailable || !_subModValid)) if (ImGuiUtil.DrawDisabledButton("Create New Option", new Vector2(width / 2, 0), tt, !newModAvailable || !_subModValid))
{
CreateOption(); CreateOption();
}
ImGui.SameLine(); ImGui.SameLine();
ImGui.SetCursorPosX(ImGui.GetCursorPosX() + 20 * ImGuiHelpers.GlobalScale); ImGui.SetCursorPosX(ImGui.GetCursorPosX() + 20 * ImGuiHelpers.GlobalScale);
_dirty |= ImGui.Checkbox("Use Entire Collection", ref _useCurrentCollection); _dirty |= ImGui.Checkbox("Use Entire Collection", ref _useCurrentCollection);
ImGuiUtil.HoverTooltip( "Use all applied mods from the Selected Collection with their current settings and respecting the enabled state of mods and inheritance,\n" ImGuiUtil.HoverTooltip(
"Use all applied mods from the Selected Collection with their current settings and respecting the enabled state of mods and inheritance,\n"
+ "instead of using only the selected mod with its current settings in the Selected collection or the default settings, ignoring the enabled state and inheritance."); + "instead of using only the selected mod with its current settings in the Selected collection or the default settings, ignoring the enabled state and inheritance.");
} }
@ -495,9 +495,7 @@ public class ItemSwapWindow : IDisposable
{ {
using var tab = DrawTab(type); using var tab = DrawTab(type);
if (!tab) if (!tab)
{
return; return;
}
var (sourceSelector, targetSelector, text1, text2) = _selectors[type]; var (sourceSelector, targetSelector, text1, text2) = _selectors[type];
using var table = ImRaii.Table("##settings", 2, ImGuiTableFlags.SizingFixedFit); using var table = ImRaii.Table("##settings", 2, ImGuiTableFlags.SizingFixedFit);
@ -505,7 +503,8 @@ public class ItemSwapWindow : IDisposable
ImGui.AlignTextToFramePadding(); ImGui.AlignTextToFramePadding();
ImGui.TextUnformatted(text1); ImGui.TextUnformatted(text1);
ImGui.TableNextColumn(); ImGui.TableNextColumn();
_dirty |= sourceSelector.Draw( "##itemSource", sourceSelector.CurrentSelection.Item1 ?? string.Empty, string.Empty, InputWidth * 2, ImGui.GetTextLineHeightWithSpacing() ); _dirty |= sourceSelector.Draw("##itemSource", sourceSelector.CurrentSelection.Item1 ?? string.Empty, string.Empty, InputWidth * 2,
ImGui.GetTextLineHeightWithSpacing());
if (type == SwapType.Ring) if (type == SwapType.Ring)
{ {
@ -517,7 +516,8 @@ public class ItemSwapWindow : IDisposable
ImGui.AlignTextToFramePadding(); ImGui.AlignTextToFramePadding();
ImGui.TextUnformatted(text2); ImGui.TextUnformatted(text2);
ImGui.TableNextColumn(); ImGui.TableNextColumn();
_dirty |= targetSelector.Draw( "##itemTarget", targetSelector.CurrentSelection.Item1 ?? string.Empty, string.Empty, InputWidth * 2, ImGui.GetTextLineHeightWithSpacing() ); _dirty |= targetSelector.Draw("##itemTarget", targetSelector.CurrentSelection.Item1 ?? string.Empty, string.Empty, InputWidth * 2,
ImGui.GetTextLineHeightWithSpacing());
if (type == SwapType.Ring) if (type == SwapType.Ring)
{ {
ImGui.SameLine(); ImGui.SameLine();
@ -527,22 +527,19 @@ public class ItemSwapWindow : IDisposable
if (_affectedItems is { Length: > 1 }) if (_affectedItems is { Length: > 1 })
{ {
ImGui.SameLine(); ImGui.SameLine();
ImGuiUtil.DrawTextButton( $"which will also affect {_affectedItems.Length - 1} other Items.", Vector2.Zero, Colors.PressEnterWarningBg ); ImGuiUtil.DrawTextButton($"which will also affect {_affectedItems.Length - 1} other Items.", Vector2.Zero,
Colors.PressEnterWarningBg);
if (ImGui.IsItemHovered()) if (ImGui.IsItemHovered())
{
ImGui.SetTooltip(string.Join('\n', _affectedItems.Where(i => !ReferenceEquals(i, targetSelector.CurrentSelection.Item2)) ImGui.SetTooltip(string.Join('\n', _affectedItems.Where(i => !ReferenceEquals(i, targetSelector.CurrentSelection.Item2))
.Select(i => i.Name.ToDalamudString().TextValue))); .Select(i => i.Name.ToDalamudString().TextValue)));
} }
} }
}
private void DrawHairSwap() private void DrawHairSwap()
{ {
using var tab = DrawTab(SwapType.Hair); using var tab = DrawTab(SwapType.Hair);
if (!tab) if (!tab)
{
return; return;
}
using var table = ImRaii.Table("##settings", 2, ImGuiTableFlags.SizingFixedFit); using var table = ImRaii.Table("##settings", 2, ImGuiTableFlags.SizingFixedFit);
DrawTargetIdInput("Take this Hairstyle"); DrawTargetIdInput("Take this Hairstyle");
@ -555,9 +552,7 @@ public class ItemSwapWindow : IDisposable
using var disabled = ImRaii.Disabled(); using var disabled = ImRaii.Disabled();
using var tab = DrawTab(SwapType.Face); using var tab = DrawTab(SwapType.Face);
if (!tab) if (!tab)
{
return; return;
}
using var table = ImRaii.Table("##settings", 2, ImGuiTableFlags.SizingFixedFit); using var table = ImRaii.Table("##settings", 2, ImGuiTableFlags.SizingFixedFit);
DrawTargetIdInput("Take this Face Type"); DrawTargetIdInput("Take this Face Type");
@ -569,9 +564,7 @@ public class ItemSwapWindow : IDisposable
{ {
using var tab = DrawTab(SwapType.Tail); using var tab = DrawTab(SwapType.Tail);
if (!tab) if (!tab)
{
return; return;
}
using var table = ImRaii.Table("##settings", 2, ImGuiTableFlags.SizingFixedFit); using var table = ImRaii.Table("##settings", 2, ImGuiTableFlags.SizingFixedFit);
DrawTargetIdInput("Take this Tail Type"); DrawTargetIdInput("Take this Tail Type");
@ -584,9 +577,7 @@ public class ItemSwapWindow : IDisposable
{ {
using var tab = DrawTab(SwapType.Ears); using var tab = DrawTab(SwapType.Ears);
if (!tab) if (!tab)
{
return; return;
}
using var table = ImRaii.Table("##settings", 2, ImGuiTableFlags.SizingFixedFit); using var table = ImRaii.Table("##settings", 2, ImGuiTableFlags.SizingFixedFit);
DrawTargetIdInput("Take this Ear Type"); DrawTargetIdInput("Take this Ear Type");
@ -600,16 +591,15 @@ public class ItemSwapWindow : IDisposable
using var disabled = ImRaii.Disabled(); using var disabled = ImRaii.Disabled();
using var tab = DrawTab(SwapType.Weapon); using var tab = DrawTab(SwapType.Weapon);
if (!tab) if (!tab)
{
return; return;
}
using var table = ImRaii.Table("##settings", 2, ImGuiTableFlags.SizingFixedFit); using var table = ImRaii.Table("##settings", 2, ImGuiTableFlags.SizingFixedFit);
ImGui.TableNextColumn(); ImGui.TableNextColumn();
ImGui.AlignTextToFramePadding(); ImGui.AlignTextToFramePadding();
ImGui.TextUnformatted("Select the weapon or tool you want"); ImGui.TextUnformatted("Select the weapon or tool you want");
ImGui.TableNextColumn(); ImGui.TableNextColumn();
if( _slotSelector.Draw( "##weaponSlot", _slotSelector.CurrentSelection.ToName(), string.Empty, InputWidth * 2, ImGui.GetTextLineHeightWithSpacing() ) ) if (_slotSelector.Draw("##weaponSlot", _slotSelector.CurrentSelection.ToName(), string.Empty, InputWidth * 2,
ImGui.GetTextLineHeightWithSpacing()))
{ {
_dirty = true; _dirty = true;
_weaponSource = new ItemSelector(_slotSelector.CurrentSelection); _weaponSource = new ItemSelector(_slotSelector.CurrentSelection);
@ -626,13 +616,15 @@ public class ItemSwapWindow : IDisposable
ImGui.AlignTextToFramePadding(); ImGui.AlignTextToFramePadding();
ImGui.TextUnformatted("and put this variant of it"); ImGui.TextUnformatted("and put this variant of it");
ImGui.TableNextColumn(); ImGui.TableNextColumn();
_dirty |= _weaponSource.Draw( "##weaponSource", _weaponSource.CurrentSelection.Item1 ?? string.Empty, string.Empty, InputWidth * 2, ImGui.GetTextLineHeightWithSpacing() ); _dirty |= _weaponSource.Draw("##weaponSource", _weaponSource.CurrentSelection.Item1 ?? string.Empty, string.Empty, InputWidth * 2,
ImGui.GetTextLineHeightWithSpacing());
ImGui.TableNextColumn(); ImGui.TableNextColumn();
ImGui.AlignTextToFramePadding(); ImGui.AlignTextToFramePadding();
ImGui.TextUnformatted("onto this one"); ImGui.TextUnformatted("onto this one");
ImGui.TableNextColumn(); ImGui.TableNextColumn();
_dirty |= _weaponTarget.Draw( "##weaponTarget", _weaponTarget.CurrentSelection.Item1 ?? string.Empty, string.Empty, InputWidth * 2, ImGui.GetTextLineHeightWithSpacing() ); _dirty |= _weaponTarget.Draw("##weaponTarget", _weaponTarget.CurrentSelection.Item1 ?? string.Empty, string.Empty, InputWidth * 2,
ImGui.GetTextLineHeightWithSpacing());
} }
private const float InputWidth = 120; private const float InputWidth = 120;
@ -646,9 +638,7 @@ public class ItemSwapWindow : IDisposable
ImGui.TableNextColumn(); ImGui.TableNextColumn();
ImGui.SetNextItemWidth(InputWidth * ImGuiHelpers.GlobalScale); ImGui.SetNextItemWidth(InputWidth * ImGuiHelpers.GlobalScale);
if (ImGui.InputInt("##targetId", ref _targetId, 0, 0)) if (ImGui.InputInt("##targetId", ref _targetId, 0, 0))
{
_targetId = Math.Clamp(_targetId, 0, byte.MaxValue); _targetId = Math.Clamp(_targetId, 0, byte.MaxValue);
}
_dirty |= ImGui.IsItemDeactivatedAfterEdit(); _dirty |= ImGui.IsItemDeactivatedAfterEdit();
} }
@ -662,9 +652,7 @@ public class ItemSwapWindow : IDisposable
ImGui.TableNextColumn(); ImGui.TableNextColumn();
ImGui.SetNextItemWidth(InputWidth * ImGuiHelpers.GlobalScale); ImGui.SetNextItemWidth(InputWidth * ImGuiHelpers.GlobalScale);
if (ImGui.InputInt("##sourceId", ref _sourceId, 0, 0)) if (ImGui.InputInt("##sourceId", ref _sourceId, 0, 0))
{
_sourceId = Math.Clamp(_sourceId, 0, byte.MaxValue); _sourceId = Math.Clamp(_sourceId, 0, byte.MaxValue);
}
_dirty |= ImGui.IsItemDeactivatedAfterEdit(); _dirty |= ImGui.IsItemDeactivatedAfterEdit();
} }
@ -686,11 +674,14 @@ public class ItemSwapWindow : IDisposable
{ {
ImGui.SameLine(); ImGui.SameLine();
if (_currentRace is not ModelRace.Miqote and not ModelRace.AuRa and not ModelRace.Hrothgar) if (_currentRace is not ModelRace.Miqote and not ModelRace.AuRa and not ModelRace.Hrothgar)
{
_currentRace = ModelRace.Miqote; _currentRace = ModelRace.Miqote;
}
_dirty |= ImGuiUtil.GenericEnumCombo( "##Race", InputWidth, _currentRace, out _currentRace, new[] { ModelRace.Miqote, ModelRace.AuRa, ModelRace.Hrothgar }, _dirty |= ImGuiUtil.GenericEnumCombo("##Race", InputWidth, _currentRace, out _currentRace, new[]
{
ModelRace.Miqote,
ModelRace.AuRa,
ModelRace.Hrothgar,
},
RaceEnumExtensions.ToName); RaceEnumExtensions.ToName);
} }
} }
@ -720,9 +711,7 @@ public class ItemSwapWindow : IDisposable
{ {
using var tab = ImRaii.TabItem("Item Swap (WIP)"); using var tab = ImRaii.TabItem("Item Swap (WIP)");
if (!tab) if (!tab)
{
return; return;
}
ImGui.NewLine(); ImGui.NewLine();
DrawHeaderLine(300 * ImGuiHelpers.GlobalScale); DrawHeaderLine(300 * ImGuiHelpers.GlobalScale);
@ -732,52 +721,36 @@ public class ItemSwapWindow : IDisposable
using var table = ImRaii.ListBox("##swaps", -Vector2.One); using var table = ImRaii.ListBox("##swaps", -Vector2.One);
if (_loadException != null) if (_loadException != null)
{
ImGuiUtil.TextWrapped($"Could not load Customization Swap:\n{_loadException}"); ImGuiUtil.TextWrapped($"Could not load Customization Swap:\n{_loadException}");
}
else if (_swapData.Loaded) else if (_swapData.Loaded)
{
foreach (var swap in _swapData.Swaps) foreach (var swap in _swapData.Swaps)
{
DrawSwap(swap); DrawSwap(swap);
}
}
else else
{
ImGui.TextUnformatted(NonExistentText()); ImGui.TextUnformatted(NonExistentText());
} }
}
private static void DrawSwap(Swap swap) private static void DrawSwap(Swap swap)
{ {
var flags = swap.ChildSwaps.Count == 0 ? ImGuiTreeNodeFlags.Bullet | ImGuiTreeNodeFlags.Leaf : ImGuiTreeNodeFlags.DefaultOpen; var flags = swap.ChildSwaps.Count == 0 ? ImGuiTreeNodeFlags.Bullet | ImGuiTreeNodeFlags.Leaf : ImGuiTreeNodeFlags.DefaultOpen;
using var tree = ImRaii.TreeNode(SwapToString(swap), flags); using var tree = ImRaii.TreeNode(SwapToString(swap), flags);
if (!tree) if (!tree)
{
return; return;
}
foreach (var child in swap.ChildSwaps) foreach (var child in swap.ChildSwaps)
{
DrawSwap(child); DrawSwap(child);
} }
}
private void OnCollectionChange(CollectionType collectionType, ModCollection? oldCollection, private void OnCollectionChange(CollectionType collectionType, ModCollection? oldCollection,
ModCollection? newCollection, string _) ModCollection? newCollection, string _)
{ {
if (collectionType != CollectionType.Current || _mod == null || newCollection == null) if (collectionType != CollectionType.Current || _mod == null || newCollection == null)
{
return; return;
}
UpdateMod(_mod, _mod.Index < newCollection.Settings.Count ? newCollection.Settings[_mod.Index] : null); UpdateMod(_mod, _mod.Index < newCollection.Settings.Count ? newCollection.Settings[_mod.Index] : null);
newCollection.ModSettingChanged += OnSettingChange; newCollection.ModSettingChanged += OnSettingChange;
if (oldCollection != null) if (oldCollection != null)
{
oldCollection.ModSettingChanged -= OnSettingChange; oldCollection.ModSettingChanged -= OnSettingChange;
} }
}
private void OnSettingChange(ModSettingChange type, int modIdx, int oldValue, int groupIdx, bool inherited) private void OnSettingChange(ModSettingChange type, int modIdx, int oldValue, int groupIdx, bool inherited)
{ {

View file

@ -97,7 +97,7 @@ public partial class ModEditWindow
private static bool DrawPreviewDye( MtrlFile file, bool disabled ) private static bool DrawPreviewDye( MtrlFile file, bool disabled )
{ {
var (dyeId, (name, dyeColor, _)) = Penumbra.StainManager.StainCombo.CurrentSelection; var (dyeId, (name, dyeColor, _)) = Penumbra.StainService.StainCombo.CurrentSelection;
var tt = dyeId == 0 ? "Select a preview dye first." : "Apply all preview values corresponding to the dye template and chosen dye where dyeing is enabled."; var tt = dyeId == 0 ? "Select a preview dye first." : "Apply all preview values corresponding to the dye template and chosen dye where dyeing is enabled.";
if( ImGuiUtil.DrawDisabledButton( "Apply Preview Dye", Vector2.Zero, tt, disabled || dyeId == 0 ) ) if( ImGuiUtil.DrawDisabledButton( "Apply Preview Dye", Vector2.Zero, tt, disabled || dyeId == 0 ) )
{ {
@ -106,7 +106,7 @@ public partial class ModEditWindow
{ {
for( var i = 0; i < MtrlFile.ColorSet.RowArray.NumRows; ++i ) for( var i = 0; i < MtrlFile.ColorSet.RowArray.NumRows; ++i )
{ {
ret |= file.ApplyDyeTemplate( Penumbra.StainManager.StmFile, j, i, dyeId ); ret |= file.ApplyDyeTemplate( Penumbra.StainService.StmFile, j, i, dyeId );
} }
} }
@ -115,7 +115,7 @@ public partial class ModEditWindow
ImGui.SameLine(); ImGui.SameLine();
var label = dyeId == 0 ? "Preview Dye###previewDye" : $"{name} (Preview)###previewDye"; var label = dyeId == 0 ? "Preview Dye###previewDye" : $"{name} (Preview)###previewDye";
Penumbra.StainManager.StainCombo.Draw( label, dyeColor, string.Empty, true ); Penumbra.StainService.StainCombo.Draw( label, dyeColor, string.Empty, true );
return false; return false;
} }
@ -355,10 +355,10 @@ public partial class ModEditWindow
ImGui.TableNextColumn(); ImGui.TableNextColumn();
if( hasDye ) if( hasDye )
{ {
if( Penumbra.StainManager.TemplateCombo.Draw( "##dyeTemplate", dye.Template.ToString(), string.Empty, intSize if( Penumbra.StainService.TemplateCombo.Draw( "##dyeTemplate", dye.Template.ToString(), string.Empty, intSize
+ ImGui.GetStyle().ScrollbarSize / 2, ImGui.GetTextLineHeightWithSpacing(), ImGuiComboFlags.NoArrowButton ) ) + ImGui.GetStyle().ScrollbarSize / 2, ImGui.GetTextLineHeightWithSpacing(), ImGuiComboFlags.NoArrowButton ) )
{ {
file.ColorDyeSets[ colorSetIdx ].Rows[ rowIdx ].Template = Penumbra.StainManager.TemplateCombo.CurrentSelection; file.ColorDyeSets[ colorSetIdx ].Rows[ rowIdx ].Template = Penumbra.StainService.TemplateCombo.CurrentSelection;
ret = true; ret = true;
} }
@ -378,8 +378,8 @@ public partial class ModEditWindow
private static bool DrawDyePreview( MtrlFile file, int colorSetIdx, int rowIdx, bool disabled, MtrlFile.ColorDyeSet.Row dye, float floatSize ) private static bool DrawDyePreview( MtrlFile file, int colorSetIdx, int rowIdx, bool disabled, MtrlFile.ColorDyeSet.Row dye, float floatSize )
{ {
var stain = Penumbra.StainManager.StainCombo.CurrentSelection.Key; var stain = Penumbra.StainService.StainCombo.CurrentSelection.Key;
if( stain == 0 || !Penumbra.StainManager.StmFile.Entries.TryGetValue( dye.Template, out var entry ) ) if( stain == 0 || !Penumbra.StainService.StmFile.Entries.TryGetValue( dye.Template, out var entry ) )
{ {
return false; return false;
} }
@ -390,7 +390,7 @@ public partial class ModEditWindow
var ret = ImGuiUtil.DrawDisabledButton( FontAwesomeIcon.PaintBrush.ToIconString(), new Vector2( ImGui.GetFrameHeight() ), var ret = ImGuiUtil.DrawDisabledButton( FontAwesomeIcon.PaintBrush.ToIconString(), new Vector2( ImGui.GetFrameHeight() ),
"Apply the selected dye to this row.", disabled, true ); "Apply the selected dye to this row.", disabled, true );
ret = ret && file.ApplyDyeTemplate( Penumbra.StainManager.StmFile, colorSetIdx, rowIdx, stain ); ret = ret && file.ApplyDyeTemplate( Penumbra.StainService.StmFile, colorSetIdx, rowIdx, stain );
ImGui.SameLine(); ImGui.SameLine();
ColorPicker( "##diffusePreview", string.Empty, values.Diffuse, _ => { }, "D" ); ColorPicker( "##diffusePreview", string.Empty, values.Diffuse, _ => { }, "D" );

View file

@ -13,6 +13,7 @@ using Penumbra.GameData.Enums;
using Penumbra.GameData.Files; using Penumbra.GameData.Files;
using Penumbra.Import.Textures; using Penumbra.Import.Textures;
using Penumbra.Mods; using Penumbra.Mods;
using Penumbra.Services;
using Penumbra.String.Classes; using Penumbra.String.Classes;
using Penumbra.Util; using Penumbra.Util;
using static Penumbra.Mods.Mod; using static Penumbra.Mods.Mod;
@ -22,7 +23,7 @@ namespace Penumbra.UI.Classes;
public partial class ModEditWindow : Window, IDisposable public partial class ModEditWindow : Window, IDisposable
{ {
private const string WindowBaseLabel = "###SubModEdit"; private const string WindowBaseLabel = "###SubModEdit";
internal readonly ItemSwapWindow _swapWindow = new(); internal readonly ItemSwapWindow _swapWindow;
private Editor? _editor; private Editor? _editor;
private Mod? _mod; private Mod? _mod;
@ -567,9 +568,10 @@ public partial class ModEditWindow : Window, IDisposable
return new FullPath( path ); return new FullPath( path );
} }
public ModEditWindow() public ModEditWindow(CommunicatorService communicator)
: base( WindowBaseLabel ) : base( WindowBaseLabel )
{ {
_swapWindow = new ItemSwapWindow( communicator );
_materialTab = new FileEditor< MtrlTab >( "Materials", ".mtrl", _materialTab = new FileEditor< MtrlTab >( "Materials", ".mtrl",
() => _editor?.MtrlFiles ?? Array.Empty< Editor.FileRegistry >(), () => _editor?.MtrlFiles ?? Array.Empty< Editor.FileRegistry >(),
DrawMaterialPanel, DrawMaterialPanel,

View file

@ -20,14 +20,16 @@ namespace Penumbra.UI.Classes;
public sealed partial class ModFileSystemSelector : FileSystemSelector<Mod, ModFileSystemSelector.ModState> public sealed partial class ModFileSystemSelector : FileSystemSelector<Mod, ModFileSystemSelector.ModState>
{ {
private readonly CommunicatorService _communicator;
private readonly FileDialogManager _fileManager = ConfigWindow.SetupFileManager(); private readonly FileDialogManager _fileManager = ConfigWindow.SetupFileManager();
private TexToolsImporter? _import; private TexToolsImporter? _import;
public ModSettings SelectedSettings { get; private set; } = ModSettings.Empty; public ModSettings SelectedSettings { get; private set; } = ModSettings.Empty;
public ModCollection SelectedSettingCollection { get; private set; } = ModCollection.Empty; public ModCollection SelectedSettingCollection { get; private set; } = ModCollection.Empty;
public ModFileSystemSelector( ModFileSystem fileSystem ) public ModFileSystemSelector(CommunicatorService communicator, ModFileSystem fileSystem)
: base(fileSystem, DalamudServices.KeyState) : base(fileSystem, DalamudServices.KeyState)
{ {
_communicator = communicator;
SubscribeRightClickFolder(EnableDescendants, 10); SubscribeRightClickFolder(EnableDescendants, 10);
SubscribeRightClickFolder(DisableDescendants, 10); SubscribeRightClickFolder(DisableDescendants, 10);
SubscribeRightClickFolder(InheritDescendants, 15); SubscribeRightClickFolder(InheritDescendants, 15);
@ -42,7 +44,7 @@ public sealed partial class ModFileSystemSelector : FileSystemSelector< Mod, Mod
SetFilterTooltip(); SetFilterTooltip();
SelectionChanged += OnSelectionChange; SelectionChanged += OnSelectionChange;
Penumbra.CollectionManager.CollectionChanged += OnCollectionChange; _communicator.CollectionChange.Event += OnCollectionChange;
Penumbra.CollectionManager.Current.ModSettingChanged += OnSettingChange; Penumbra.CollectionManager.Current.ModSettingChanged += OnSettingChange;
Penumbra.CollectionManager.Current.InheritanceChanged += OnInheritanceChange; Penumbra.CollectionManager.Current.InheritanceChanged += OnInheritanceChange;
Penumbra.ModManager.ModDataChanged += OnModDataChange; Penumbra.ModManager.ModDataChanged += OnModDataChange;
@ -59,7 +61,7 @@ public sealed partial class ModFileSystemSelector : FileSystemSelector< Mod, Mod
Penumbra.ModManager.ModDataChanged -= OnModDataChange; Penumbra.ModManager.ModDataChanged -= OnModDataChange;
Penumbra.CollectionManager.Current.ModSettingChanged -= OnSettingChange; Penumbra.CollectionManager.Current.ModSettingChanged -= OnSettingChange;
Penumbra.CollectionManager.Current.InheritanceChanged -= OnInheritanceChange; Penumbra.CollectionManager.Current.InheritanceChanged -= OnInheritanceChange;
Penumbra.CollectionManager.CollectionChanged -= OnCollectionChange; _communicator.CollectionChange.Event -= OnCollectionChange;
_import?.Dispose(); _import?.Dispose();
_import = null; _import = null;
} }
@ -90,7 +92,6 @@ public sealed partial class ModFileSystemSelector : FileSystemSelector< Mod, Mod
DrawInfoPopup(); DrawInfoPopup();
if (ImGuiUtil.OpenNameField("Create New Mod", ref _newModName)) if (ImGuiUtil.OpenNameField("Create New Mod", ref _newModName))
{
try try
{ {
var newDir = Mod.Creator.CreateModFolder(Penumbra.ModManager.BasePath, _newModName); var newDir = Mod.Creator.CreateModFolder(Penumbra.ModManager.BasePath, _newModName);
@ -103,7 +104,6 @@ public sealed partial class ModFileSystemSelector : FileSystemSelector< Mod, Mod
{ {
Penumbra.Log.Error($"Could not create directory for new Mod {_newModName}:\n{e}"); Penumbra.Log.Error($"Could not create directory for new Mod {_newModName}:\n{e}");
} }
}
while (_modsToAdd.TryDequeue(out var dir)) while (_modsToAdd.TryDequeue(out var dir))
{ {
@ -131,42 +131,32 @@ public sealed partial class ModFileSystemSelector : FileSystemSelector< Mod, Mod
private static void EnableDescendants(ModFileSystem.Folder folder) private static void EnableDescendants(ModFileSystem.Folder folder)
{ {
if (ImGui.MenuItem("Enable Descendants")) if (ImGui.MenuItem("Enable Descendants"))
{
SetDescendants(folder, true); SetDescendants(folder, true);
} }
}
private static void DisableDescendants(ModFileSystem.Folder folder) private static void DisableDescendants(ModFileSystem.Folder folder)
{ {
if (ImGui.MenuItem("Disable Descendants")) if (ImGui.MenuItem("Disable Descendants"))
{
SetDescendants(folder, false); SetDescendants(folder, false);
} }
}
private static void InheritDescendants(ModFileSystem.Folder folder) private static void InheritDescendants(ModFileSystem.Folder folder)
{ {
if (ImGui.MenuItem("Inherit Descendants")) if (ImGui.MenuItem("Inherit Descendants"))
{
SetDescendants(folder, true, true); SetDescendants(folder, true, true);
} }
}
private static void OwnDescendants(ModFileSystem.Folder folder) private static void OwnDescendants(ModFileSystem.Folder folder)
{ {
if (ImGui.MenuItem("Stop Inheriting Descendants")) if (ImGui.MenuItem("Stop Inheriting Descendants"))
{
SetDescendants(folder, false, true); SetDescendants(folder, false, true);
} }
}
private static void ToggleLeafFavorite(FileSystem<Mod>.Leaf mod) private static void ToggleLeafFavorite(FileSystem<Mod>.Leaf mod)
{ {
if (ImGui.MenuItem(mod.Value.Favorite ? "Remove Favorite" : "Mark as Favorite")) if (ImGui.MenuItem(mod.Value.Favorite ? "Remove Favorite" : "Mark as Favorite"))
{
Penumbra.ModManager.ChangeModFavorite(mod.Value.Index, !mod.Value.Favorite); Penumbra.ModManager.ChangeModFavorite(mod.Value.Index, !mod.Value.Favorite);
} }
}
private static void SetDefaultImportFolder(ModFileSystem.Folder folder) private static void SetDefaultImportFolder(ModFileSystem.Folder folder)
{ {
@ -198,10 +188,8 @@ public sealed partial class ModFileSystemSelector : FileSystemSelector< Mod, Mod
{ {
if (ImGuiUtil.DrawDisabledButton(FontAwesomeIcon.Plus.ToIconString(), size, "Create a new, empty mod of a given name.", if (ImGuiUtil.DrawDisabledButton(FontAwesomeIcon.Plus.ToIconString(), size, "Create a new, empty mod of a given name.",
!Penumbra.ModManager.Valid, true)) !Penumbra.ModManager.Valid, true))
{
ImGui.OpenPopup("Create New Mod"); ImGui.OpenPopup("Create New Mod");
} }
}
// Add an import mods button that opens a file selector. // Add an import mods button that opens a file selector.
// Only set the initial directory once. // Only set the initial directory once.
@ -213,9 +201,7 @@ public sealed partial class ModFileSystemSelector : FileSystemSelector< Mod, Mod
"Import one or multiple mods from Tex Tools Mod Pack Files or Penumbra Mod Pack Files.", !Penumbra.ModManager.Valid, true); "Import one or multiple mods from Tex Tools Mod Pack Files or Penumbra Mod Pack Files.", !Penumbra.ModManager.Valid, true);
ConfigWindow.OpenTutorial(ConfigWindow.BasicTutorialSteps.ModImport); ConfigWindow.OpenTutorial(ConfigWindow.BasicTutorialSteps.ModImport);
if (!button) if (!button)
{
return; return;
}
var modPath = _hasSetFolder && !Penumbra.Config.AlwaysOpenDefaultImport ? null var modPath = _hasSetFolder && !Penumbra.Config.AlwaysOpenDefaultImport ? null
: Penumbra.Config.DefaultModImportPath.Length > 0 ? Penumbra.Config.DefaultModImportPath : Penumbra.Config.DefaultModImportPath.Length > 0 ? Penumbra.Config.DefaultModImportPath
@ -245,17 +231,13 @@ public sealed partial class ModFileSystemSelector : FileSystemSelector< Mod, Mod
ImGui.SetNextWindowSize(size); ImGui.SetNextWindowSize(size);
using var popup = ImRaii.Popup("Import Status", ImGuiWindowFlags.Modal); using var popup = ImRaii.Popup("Import Status", ImGuiWindowFlags.Modal);
if (_import == null || !popup.Success) if (_import == null || !popup.Success)
{
return; return;
}
using (var child = ImRaii.Child("##import", new Vector2(-1, size.Y - ImGui.GetFrameHeight() * 2))) using (var child = ImRaii.Child("##import", new Vector2(-1, size.Y - ImGui.GetFrameHeight() * 2)))
{ {
if (child) if (child)
{
_import.DrawProgressInfo(new Vector2(-1, ImGui.GetFrameHeight())); _import.DrawProgressInfo(new Vector2(-1, ImGui.GetFrameHeight()));
} }
}
if (_import.State == ImporterState.Done && ImGui.Button("Close", -Vector2.UnitX) if (_import.State == ImporterState.Done && ImGui.Button("Close", -Vector2.UnitX)
|| _import.State != ImporterState.Done && _import.DrawCancelButton(-Vector2.UnitX)) || _import.State != ImporterState.Done && _import.DrawCancelButton(-Vector2.UnitX))
@ -276,7 +258,6 @@ public sealed partial class ModFileSystemSelector : FileSystemSelector< Mod, Mod
if (error != null) if (error != null)
{ {
if (dir != null && Directory.Exists(dir.FullName)) if (dir != null && Directory.Exists(dir.FullName))
{
try try
{ {
Directory.Delete(dir.FullName, true); Directory.Delete(dir.FullName, true);
@ -285,13 +266,10 @@ public sealed partial class ModFileSystemSelector : FileSystemSelector< Mod, Mod
{ {
Penumbra.Log.Error($"Error cleaning up failed mod extraction of {file.FullName} to {dir.FullName}:\n{e}"); Penumbra.Log.Error($"Error cleaning up failed mod extraction of {file.FullName} to {dir.FullName}:\n{e}");
} }
}
if (error is not OperationCanceledException) if (error is not OperationCanceledException)
{
Penumbra.Log.Error($"Error extracting {file.FullName}, mod skipped:\n{error}"); Penumbra.Log.Error($"Error extracting {file.FullName}, mod skipped:\n{error}");
} }
}
else if (dir != null) else if (dir != null)
{ {
_modsToAdd.Enqueue(dir); _modsToAdd.Enqueue(dir);
@ -306,23 +284,17 @@ public sealed partial class ModFileSystemSelector : FileSystemSelector< Mod, Mod
: "Delete the currently selected mod entirely from your drive.\n" : "Delete the currently selected mod entirely from your drive.\n"
+ "This can not be undone."; + "This can not be undone.";
if (!keys) if (!keys)
{
tt += $"\nHold {Penumbra.Config.DeleteModModifier} while clicking to delete the mod."; tt += $"\nHold {Penumbra.Config.DeleteModModifier} while clicking to delete the mod.";
}
if (ImGuiUtil.DrawDisabledButton(FontAwesomeIcon.Trash.ToIconString(), size, tt, SelectedLeaf == null || !keys, true) if (ImGuiUtil.DrawDisabledButton(FontAwesomeIcon.Trash.ToIconString(), size, tt, SelectedLeaf == null || !keys, true)
&& Selected != null) && Selected != null)
{
Penumbra.ModManager.DeleteMod(Selected.Index); Penumbra.ModManager.DeleteMod(Selected.Index);
} }
}
private static void AddHelpButton(Vector2 size) private static void AddHelpButton(Vector2 size)
{ {
if (ImGuiUtil.DrawDisabledButton(FontAwesomeIcon.QuestionCircle.ToIconString(), size, "Open extended help.", false, true)) if (ImGuiUtil.DrawDisabledButton(FontAwesomeIcon.QuestionCircle.ToIconString(), size, "Open extended help.", false, true))
{
ImGui.OpenPopup("ExtendedHelp"); ImGui.OpenPopup("ExtendedHelp");
}
ConfigWindow.OpenTutorial(ConfigWindow.BasicTutorialSteps.AdvancedHelp); ConfigWindow.OpenTutorial(ConfigWindow.BasicTutorialSteps.AdvancedHelp);
} }
@ -338,14 +310,10 @@ public sealed partial class ModFileSystemSelector : FileSystemSelector< Mod, Mod
}); });
if (inherit) if (inherit)
{
Penumbra.CollectionManager.Current.SetMultipleModInheritances(mods, enabled); Penumbra.CollectionManager.Current.SetMultipleModInheritances(mods, enabled);
}
else else
{
Penumbra.CollectionManager.Current.SetMultipleModStates(mods, enabled); Penumbra.CollectionManager.Current.SetMultipleModStates(mods, enabled);
} }
}
// Automatic cache update functions. // Automatic cache update functions.
private void OnSettingChange(ModSettingChange type, int modIdx, int oldValue, int groupIdx, bool inherited) private void OnSettingChange(ModSettingChange type, int modIdx, int oldValue, int groupIdx, bool inherited)
@ -353,10 +321,8 @@ public sealed partial class ModFileSystemSelector : FileSystemSelector< Mod, Mod
// TODO: maybe make more efficient // TODO: maybe make more efficient
SetFilterDirty(); SetFilterDirty();
if (modIdx == Selected?.Index) if (modIdx == Selected?.Index)
{
OnSelectionChange(Selected, Selected, default); OnSelectionChange(Selected, Selected, default);
} }
}
private void OnModDataChange(ModDataChangeType type, Mod mod, string? oldName) private void OnModDataChange(ModDataChangeType type, Mod mod, string? oldName)
{ {
@ -381,9 +347,7 @@ public sealed partial class ModFileSystemSelector : FileSystemSelector< Mod, Mod
private void OnCollectionChange(CollectionType collectionType, ModCollection? oldCollection, ModCollection? newCollection, string _) private void OnCollectionChange(CollectionType collectionType, ModCollection? oldCollection, ModCollection? newCollection, string _)
{ {
if (collectionType != CollectionType.Current || oldCollection == newCollection) if (collectionType != CollectionType.Current || oldCollection == newCollection)
{
return; return;
}
if (oldCollection != null) if (oldCollection != null)
{ {
@ -440,18 +404,14 @@ public sealed partial class ModFileSystemSelector : FileSystemSelector< Mod, Mod
private void MoveModToDefaultDirectory(Mod mod) private void MoveModToDefaultDirectory(Mod mod)
{ {
if (Penumbra.Config.DefaultImportFolder.Length == 0) if (Penumbra.Config.DefaultImportFolder.Length == 0)
{
return; return;
}
try try
{ {
var leaf = FileSystem.Root.GetChildren(ISortMode<Mod>.Lexicographical) var leaf = FileSystem.Root.GetChildren(ISortMode<Mod>.Lexicographical)
.FirstOrDefault(f => f is FileSystem<Mod>.Leaf l && l.Value == mod); .FirstOrDefault(f => f is FileSystem<Mod>.Leaf l && l.Value == mod);
if (leaf == null) if (leaf == null)
{
throw new Exception("Mod was not found at root."); throw new Exception("Mod was not found at root.");
}
var folder = FileSystem.FindOrCreateAllFolders(Penumbra.Config.DefaultImportFolder); var folder = FileSystem.FindOrCreateAllFolders(Penumbra.Config.DefaultImportFolder);
FileSystem.Move(leaf, folder); FileSystem.Move(leaf, folder);
@ -472,7 +432,8 @@ public sealed partial class ModFileSystemSelector : FileSystemSelector< Mod, Mod
ImGui.BulletText("You can create empty mods or import mods with the buttons in this row."); ImGui.BulletText("You can create empty mods or import mods with the buttons in this row.");
using var indent = ImRaii.PushIndent(); using var indent = ImRaii.PushIndent();
ImGui.BulletText("Supported formats for import are: .ttmp, .ttmp2, .pmp."); ImGui.BulletText("Supported formats for import are: .ttmp, .ttmp2, .pmp.");
ImGui.BulletText( "You can also support .zip, .7z or .rar archives, but only if they already contain Penumbra-styled mods with appropriate metadata." ); ImGui.BulletText(
"You can also support .zip, .7z or .rar archives, but only if they already contain Penumbra-styled mods with appropriate metadata.");
indent.Pop(1); indent.Pop(1);
ImGui.BulletText("You can also create empty mod folders and delete mods."); ImGui.BulletText("You can also create empty mod folders and delete mods.");
ImGui.BulletText("For further editing of mods, select them and use the Edit Mod tab in the panel or the Advanced Editing popup."); ImGui.BulletText("For further editing of mods, select them and use the Edit Mod tab in the panel or the Advanced Editing popup.");

View file

@ -8,6 +8,7 @@ using OtterGui.Classes;
using OtterGui.Raii; using OtterGui.Raii;
using OtterGui.Widgets; using OtterGui.Widgets;
using Penumbra.Collections; using Penumbra.Collections;
using Penumbra.Services;
namespace Penumbra.UI; namespace Penumbra.UI;
@ -16,20 +17,22 @@ public partial class ConfigWindow
// Encapsulate for less pollution. // Encapsulate for less pollution.
private partial class CollectionsTab : IDisposable, ITab private partial class CollectionsTab : IDisposable, ITab
{ {
private readonly CommunicatorService _communicator;
private readonly ConfigWindow _window; private readonly ConfigWindow _window;
public CollectionsTab( ConfigWindow window ) public CollectionsTab( CommunicatorService communicator, ConfigWindow window )
{ {
_window = window; _window = window;
_communicator = communicator;
Penumbra.CollectionManager.CollectionChanged += UpdateIdentifiers; _communicator.CollectionChange.Event += UpdateIdentifiers;
} }
public ReadOnlySpan<byte> Label public ReadOnlySpan<byte> Label
=> "Collections"u8; => "Collections"u8;
public void Dispose() public void Dispose()
=> Penumbra.CollectionManager.CollectionChanged -= UpdateIdentifiers; => _communicator.CollectionChange.Event -= UpdateIdentifiers;
public void DrawHeader() public void DrawHeader()
=> OpenTutorial( BasicTutorialSteps.Collections ); => OpenTutorial( BasicTutorialSteps.Collections );

View file

@ -8,6 +8,7 @@ using FFXIVClientStructs.FFXIV.Client.Game.Object;
using FFXIVClientStructs.FFXIV.Client.System.Resource; using FFXIVClientStructs.FFXIV.Client.System.Resource;
using ImGuiNET; using ImGuiNET;
using OtterGui; using OtterGui;
using OtterGui.Classes;
using OtterGui.Widgets; using OtterGui.Widgets;
using Penumbra.GameData.Actors; using Penumbra.GameData.Actors;
using Penumbra.GameData.Files; using Penumbra.GameData.Files;
@ -28,10 +29,14 @@ public partial class ConfigWindow
{ {
private class DebugTab : ITab private class DebugTab : ITab
{ {
private readonly StartTracker _timer;
private readonly ConfigWindow _window; private readonly ConfigWindow _window;
public DebugTab( ConfigWindow window ) public DebugTab( ConfigWindow window, StartTracker timer)
=> _window = window; {
_window = window;
_timer = timer;
}
public ReadOnlySpan<byte> Label public ReadOnlySpan<byte> Label
=> "Debug"u8; => "Debug"u8;
@ -109,7 +114,7 @@ public partial class ConfigWindow
PrintValue( "Web Server Enabled", _window._penumbra.HttpApi.Enabled.ToString() ); PrintValue( "Web Server Enabled", _window._penumbra.HttpApi.Enabled.ToString() );
} }
private static void DrawPerformanceTab() private void DrawPerformanceTab()
{ {
ImGui.NewLine(); ImGui.NewLine();
if( ImGui.CollapsingHeader( "Performance" ) ) if( ImGui.CollapsingHeader( "Performance" ) )
@ -121,7 +126,7 @@ public partial class ConfigWindow
{ {
if( start ) if( start )
{ {
Penumbra.StartTimer.Draw( "##startTimer", TimingExtensions.ToName ); _timer.Draw( "##startTimer", TimingExtensions.ToName );
ImGui.NewLine(); ImGui.NewLine();
} }
} }
@ -397,7 +402,7 @@ public partial class ConfigWindow
return; return;
} }
foreach( var (key, data) in Penumbra.StainManager.StmFile.Entries ) foreach( var (key, data) in Penumbra.StainService.StmFile.Entries )
{ {
using var tree = TreeNode( $"Template {key}" ); using var tree = TreeNode( $"Template {key}" );
if( !tree ) if( !tree )

View file

@ -105,7 +105,7 @@ public partial class ConfigWindow
private static void DrawWaitForPluginsReflection() private static void DrawWaitForPluginsReflection()
{ {
if( !DalamudServices.GetDalamudConfig( DalamudServices.WaitingForPluginsOption, out bool value ) ) if( !Penumbra.Dalamud.GetDalamudConfig( DalamudServices.WaitingForPluginsOption, out bool value ) )
{ {
using var disabled = ImRaii.Disabled(); using var disabled = ImRaii.Disabled();
Checkbox( "Wait for Plugins on Startup (Disabled, can not access Dalamud Configuration)", string.Empty, false, v => { } ); Checkbox( "Wait for Plugins on Startup (Disabled, can not access Dalamud Configuration)", string.Empty, false, v => { } );
@ -113,7 +113,7 @@ public partial class ConfigWindow
else else
{ {
Checkbox( "Wait for Plugins on Startup", "This changes a setting in the Dalamud Configuration found at /xlsettings -> General.", value, Checkbox( "Wait for Plugins on Startup", "This changes a setting in the Dalamud Configuration found at /xlsettings -> General.", value,
v => DalamudServices.SetDalamudConfig( DalamudServices.WaitingForPluginsOption, v, "doWaitForPluginsOnStartup" ) ); v => Penumbra.Dalamud.SetDalamudConfig( DalamudServices.WaitingForPluginsOption, v, "doWaitForPluginsOnStartup" ) );
} }
} }
} }

View file

@ -299,11 +299,11 @@ public partial class ConfigWindow
private const string SupportInfoButtonText = "Copy Support Info to Clipboard"; private const string SupportInfoButtonText = "Copy Support Info to Clipboard";
public static void DrawSupportButton() public static void DrawSupportButton(Penumbra penumbra)
{ {
if( ImGui.Button( SupportInfoButtonText ) ) if( ImGui.Button( SupportInfoButtonText ) )
{ {
var text = Penumbra.GatherSupportInformation(); var text = penumbra.GatherSupportInformation();
ImGui.SetClipboardText( text ); ImGui.SetClipboardText( text );
} }
} }
@ -345,7 +345,7 @@ public partial class ConfigWindow
} }
ImGui.SetCursorPos( new Vector2( xPos, ImGui.GetFrameHeightWithSpacing() ) ); ImGui.SetCursorPos( new Vector2( xPos, ImGui.GetFrameHeightWithSpacing() ) );
DrawSupportButton(); DrawSupportButton(_window._penumbra);
ImGui.SetCursorPos( new Vector2( xPos, 0 ) ); ImGui.SetCursorPos( new Vector2( xPos, 0 ) );
DrawDiscordButton( width ); DrawDiscordButton( width );

View file

@ -19,7 +19,7 @@ public sealed partial class ConfigWindow : Window, IDisposable
private readonly Penumbra _penumbra; private readonly Penumbra _penumbra;
private readonly ModFileSystemSelector _selector; private readonly ModFileSystemSelector _selector;
private readonly ModPanel _modPanel; private readonly ModPanel _modPanel;
public readonly ModEditWindow ModEditPopup = new(); public readonly ModEditWindow ModEditPopup;
private readonly SettingsTab _settingsTab; private readonly SettingsTab _settingsTab;
private readonly CollectionsTab _collectionsTab; private readonly CollectionsTab _collectionsTab;
@ -31,29 +31,29 @@ public sealed partial class ConfigWindow : Window, IDisposable
private readonly ResourceWatcher _resourceWatcher; private readonly ResourceWatcher _resourceWatcher;
public TabType SelectTab = TabType.None; public TabType SelectTab = TabType.None;
public void SelectMod(Mod mod) public void SelectMod(Mod mod)
=> _selector.SelectByValue(mod); => _selector.SelectByValue(mod);
public ConfigWindow( Penumbra penumbra, ResourceWatcher watcher ) public ConfigWindow(CommunicatorService communicator, StartTracker timer, Penumbra penumbra, ResourceWatcher watcher)
: base(GetLabel()) : base(GetLabel())
{ {
_penumbra = penumbra; _penumbra = penumbra;
_resourceWatcher = watcher; _resourceWatcher = watcher;
ModEditPopup = new ModEditWindow(communicator);
_settingsTab = new SettingsTab(this); _settingsTab = new SettingsTab(this);
_selector = new ModFileSystemSelector( _penumbra.ModFileSystem ); _selector = new ModFileSystemSelector(communicator, _penumbra.ModFileSystem);
_modPanel = new ModPanel(this); _modPanel = new ModPanel(this);
_modsTab = new ModsTab(_selector, _modPanel, _penumbra); _modsTab = new ModsTab(_selector, _modPanel, _penumbra);
_selector.SelectionChanged += _modPanel.OnSelectionChange; _selector.SelectionChanged += _modPanel.OnSelectionChange;
_collectionsTab = new CollectionsTab( this ); _collectionsTab = new CollectionsTab(communicator, this);
_changedItemsTab = new ChangedItemsTab(this); _changedItemsTab = new ChangedItemsTab(this);
_effectiveTab = new EffectiveTab(); _effectiveTab = new EffectiveTab();
_debugTab = new DebugTab( this ); _debugTab = new DebugTab(this, timer);
_resourceTab = new ResourceTab(); _resourceTab = new ResourceTab();
if (Penumbra.Config.FixMainWindow) if (Penumbra.Config.FixMainWindow)
{
Flags |= ImGuiWindowFlags.NoResize | ImGuiWindowFlags.NoMove; Flags |= ImGuiWindowFlags.NoResize | ImGuiWindowFlags.NoMove;
}
DalamudServices.PluginInterface.UiBuilder.DisableGposeUiHide = !Penumbra.Config.HideUiInGPose; DalamudServices.PluginInterface.UiBuilder.DisableGposeUiHide = !Penumbra.Config.HideUiInGPose;
DalamudServices.PluginInterface.UiBuilder.DisableCutsceneUiHide = !Penumbra.Config.HideUiInCutscenes; DalamudServices.PluginInterface.UiBuilder.DisableCutsceneUiHide = !Penumbra.Config.HideUiInCutscenes;
@ -89,21 +89,22 @@ public sealed partial class ConfigWindow : Window, IDisposable
{ {
if (Penumbra.ValidityChecker.ImcExceptions.Count > 0) if (Penumbra.ValidityChecker.ImcExceptions.Count > 0)
{ {
DrawProblemWindow( $"There were {Penumbra.ValidityChecker.ImcExceptions.Count} errors while trying to load IMC files from the game data.\n" DrawProblemWindow(_penumbra,
$"There were {Penumbra.ValidityChecker.ImcExceptions.Count} errors while trying to load IMC files from the game data.\n"
+ "This usually means that your game installation was corrupted by updating the game while having TexTools mods still active.\n" + "This usually means that your game installation was corrupted by updating the game while having TexTools mods still active.\n"
+ "It is recommended to not use TexTools and Penumbra (or other Lumina-based tools) at the same time.\n\n" + "It is recommended to not use TexTools and Penumbra (or other Lumina-based tools) at the same time.\n\n"
+ "Please use the Launcher's Repair Game Files function to repair your client installation.", true); + "Please use the Launcher's Repair Game Files function to repair your client installation.", true);
} }
else if (!Penumbra.ValidityChecker.IsValidSourceRepo) else if (!Penumbra.ValidityChecker.IsValidSourceRepo)
{ {
DrawProblemWindow( DrawProblemWindow(_penumbra,
$"You are loading a release version of Penumbra from the repository \"{DalamudServices.PluginInterface.SourceRepository}\" instead of the official repository.\n" $"You are loading a release version of Penumbra from the repository \"{DalamudServices.PluginInterface.SourceRepository}\" instead of the official repository.\n"
+ $"Please use the official repository at {ValidityChecker.Repository}.\n\n" + $"Please use the official repository at {ValidityChecker.Repository}.\n\n"
+ "If you are developing for Penumbra and see this, you should compile your version in debug mode to avoid it.", false); + "If you are developing for Penumbra and see this, you should compile your version in debug mode to avoid it.", false);
} }
else if (Penumbra.ValidityChecker.IsNotInstalledPenumbra) else if (Penumbra.ValidityChecker.IsNotInstalledPenumbra)
{ {
DrawProblemWindow( DrawProblemWindow(_penumbra,
$"You are loading a release version of Penumbra from \"{DalamudServices.PluginInterface.AssemblyLocation.Directory?.FullName ?? "Unknown"}\" instead of the installedPlugins directory.\n\n" $"You are loading a release version of Penumbra from \"{DalamudServices.PluginInterface.AssemblyLocation.Directory?.FullName ?? "Unknown"}\" instead of the installedPlugins directory.\n\n"
+ "You should not install Penumbra manually, but rather add the plugin repository under settings and then install it via the plugin installer.\n\n" + "You should not install Penumbra manually, but rather add the plugin repository under settings and then install it via the plugin installer.\n\n"
+ "If you do not know how to do this, please take a look at the readme in Penumbras github repository or join us in discord.\n" + "If you do not know how to do this, please take a look at the readme in Penumbras github repository or join us in discord.\n"
@ -111,29 +112,28 @@ public sealed partial class ConfigWindow : Window, IDisposable
} }
else if (Penumbra.ValidityChecker.DevPenumbraExists) else if (Penumbra.ValidityChecker.DevPenumbraExists)
{ {
DrawProblemWindow( DrawProblemWindow(_penumbra,
$"You are loading a installed version of Penumbra from \"{DalamudServices.PluginInterface.AssemblyLocation.Directory?.FullName ?? "Unknown"}\", " $"You are loading a installed version of Penumbra from \"{DalamudServices.PluginInterface.AssemblyLocation.Directory?.FullName ?? "Unknown"}\", "
+ "but also still have some remnants of a custom install of Penumbra in your devPlugins folder.\n\n" + "but also still have some remnants of a custom install of Penumbra in your devPlugins folder.\n\n"
+ "This can cause some issues, so please go to your \"%%appdata%%\\XIVLauncher\\devPlugins\" folder and delete the Penumbra folder from there.\n\n" + "This can cause some issues, so please go to your \"%%appdata%%\\XIVLauncher\\devPlugins\" folder and delete the Penumbra folder from there.\n\n"
+ "If you are developing for Penumbra, try to avoid mixing versions. This warning will not appear if compiled in Debug mode.", false ); + "If you are developing for Penumbra, try to avoid mixing versions. This warning will not appear if compiled in Debug mode.",
false);
} }
else else
{ {
SetupSizes(); SetupSizes();
if (TabBar.Draw(string.Empty, ImGuiTabBarFlags.NoTooltip, ToLabel(SelectTab), _settingsTab, _modsTab, _collectionsTab, if (TabBar.Draw(string.Empty, ImGuiTabBarFlags.NoTooltip, ToLabel(SelectTab), _settingsTab, _modsTab, _collectionsTab,
_changedItemsTab, _effectiveTab, _resourceWatcher, _debugTab, _resourceTab)) _changedItemsTab, _effectiveTab, _resourceWatcher, _debugTab, _resourceTab))
{
SelectTab = TabType.None; SelectTab = TabType.None;
} }
} }
}
catch (Exception e) catch (Exception e)
{ {
Penumbra.Log.Error($"Exception thrown during UI Render:\n{e}"); Penumbra.Log.Error($"Exception thrown during UI Render:\n{e}");
} }
} }
private static void DrawProblemWindow( string text, bool withExceptions ) private static void DrawProblemWindow(Penumbra penumbra, string text, bool withExceptions)
{ {
using var color = ImRaii.PushColor(ImGuiCol.Text, Colors.RegexWarningBorder); using var color = ImRaii.PushColor(ImGuiCol.Text, Colors.RegexWarningBorder);
ImGui.NewLine(); ImGui.NewLine();
@ -145,7 +145,7 @@ public sealed partial class ConfigWindow : Window, IDisposable
ImGui.NewLine(); ImGui.NewLine();
SettingsTab.DrawDiscordButton(0); SettingsTab.DrawDiscordButton(0);
ImGui.SameLine(); ImGui.SameLine();
SettingsTab.DrawSupportButton(); SettingsTab.DrawSupportButton(penumbra);
ImGui.NewLine(); ImGui.NewLine();
ImGui.NewLine(); ImGui.NewLine();

View file

@ -0,0 +1,329 @@
using System;
using System.Collections.Generic;
using System.Linq;
namespace Penumbra.Util;
public readonly struct EventWrapper : IDisposable
{
private readonly string _name;
private readonly List<Action> _event = new();
public EventWrapper(string name)
=> _name = name;
public void Invoke()
{
lock (_event)
{
foreach (var action in _event)
{
try
{
action.Invoke();
}
catch (Exception ex)
{
Penumbra.Log.Error($"[{_name}] Exception thrown during invocation:\n{ex}");
}
}
}
}
public void Dispose()
{
lock (_event)
{
_event.Clear();
}
}
public event Action Event
{
add
{
lock (_event)
{
if (_event.All(a => a != value))
_event.Add(value);
}
}
remove
{
lock (_event)
{
_event.Remove(value);
}
}
}
}
public readonly struct EventWrapper<T1, T2> : IDisposable
{
private readonly string _name;
private readonly List<Action<T1, T2>> _event = new();
public EventWrapper(string name)
=> _name = name;
public void Invoke(T1 arg1, T2 arg2)
{
lock (_event)
{
foreach (var action in _event)
{
try
{
action.Invoke(arg1, arg2);
}
catch (Exception ex)
{
Penumbra.Log.Error($"[{_name}] Exception thrown during invocation:\n{ex}");
}
}
}
}
public void Dispose()
{
lock (_event)
{
_event.Clear();
}
}
public event Action<T1, T2> Event
{
add
{
lock (_event)
{
if (_event.All(a => a != value))
_event.Add(value);
}
}
remove
{
lock (_event)
{
_event.Remove(value);
}
}
}
}
public readonly struct EventWrapper<T1, T2, T3> : IDisposable
{
private readonly string _name;
private readonly List<Action<T1, T2, T3>> _event = new();
public EventWrapper(string name)
=> _name = name;
public void Invoke(T1 arg1, T2 arg2, T3 arg3)
{
lock (_event)
{
foreach (var action in _event)
{
try
{
action.Invoke(arg1, arg2, arg3);
}
catch (Exception ex)
{
Penumbra.Log.Error($"[{_name}] Exception thrown during invocation:\n{ex}");
}
}
}
}
public void Dispose()
{
lock (_event)
{
_event.Clear();
}
}
public event Action<T1, T2, T3> Event
{
add
{
lock (_event)
{
if (_event.All(a => a != value))
_event.Add(value);
}
}
remove
{
lock (_event)
{
_event.Remove(value);
}
}
}
}
public readonly struct EventWrapper<T1, T2, T3, T4> : IDisposable
{
private readonly string _name;
private readonly List<Action<T1, T2, T3, T4>> _event = new();
public EventWrapper(string name)
=> _name = name;
public void Invoke(T1 arg1, T2 arg2, T3 arg3, T4 arg4)
{
lock (_event)
{
foreach (var action in _event)
{
try
{
action.Invoke(arg1, arg2, arg3, arg4);
}
catch (Exception ex)
{
Penumbra.Log.Error($"[{_name}] Exception thrown during invocation:\n{ex}");
}
}
}
}
public void Dispose()
{
lock (_event)
{
_event.Clear();
}
}
public event Action<T1, T2, T3, T4> Event
{
add
{
lock (_event)
{
if (_event.All(a => a != value))
_event.Add(value);
}
}
remove
{
lock (_event)
{
_event.Remove(value);
}
}
}
}
public readonly struct EventWrapper<T1, T2, T3, T4, T5> : IDisposable
{
private readonly string _name;
private readonly List<Action<T1, T2, T3, T4, T5>> _event = new();
public EventWrapper(string name)
=> _name = name;
public void Invoke(T1 arg1, T2 arg2, T3 arg3, T4 arg4, T5 arg5)
{
lock (_event)
{
foreach (var action in _event)
{
try
{
action.Invoke(arg1, arg2, arg3, arg4, arg5);
}
catch (Exception ex)
{
Penumbra.Log.Error($"[{_name}] Exception thrown during invocation:\n{ex}");
}
}
}
}
public void Dispose()
{
lock (_event)
{
_event.Clear();
}
}
public event Action<T1, T2, T3, T4, T5> Event
{
add
{
lock (_event)
{
if (_event.All(a => a != value))
_event.Add(value);
}
}
remove
{
lock (_event)
{
_event.Remove(value);
}
}
}
}
public readonly struct EventWrapper<T1, T2, T3, T4, T5, T6> : IDisposable
{
private readonly string _name;
private readonly List<Action<T1, T2, T3, T4, T5, T6>> _event = new();
public EventWrapper(string name)
=> _name = name;
public void Invoke(T1 arg1, T2 arg2, T3 arg3, T4 arg4, T5 arg5, T6 arg6)
{
lock (_event)
{
foreach (var action in _event)
{
try
{
action.Invoke(arg1, arg2, arg3, arg4, arg5, arg6);
}
catch (Exception ex)
{
Penumbra.Log.Error($"[{_name}] Exception thrown during invocation:\n{ex}");
}
}
}
}
public void Dispose()
{
lock (_event)
{
_event.Clear();
}
}
public event Action<T1, T2, T3, T4, T5, T6> Event
{
add
{
lock (_event)
{
if (_event.All(a => a != value))
_event.Add(value);
}
}
remove
{
lock (_event)
{
_event.Remove(value);
}
}
}
}

View file

@ -1,4 +1,5 @@
using System; global using StartTracker = OtterGui.Classes.StartTimeTracker<Penumbra.Util.StartTimeType>;
global using PerformanceTracker = OtterGui.Classes.PerformanceTracker<Penumbra.Util.PerformanceType>;
namespace Penumbra.Util; namespace Penumbra.Util;

View file

@ -1,36 +0,0 @@
using System;
using System.Collections.Generic;
using System.Linq;
using Dalamud.Data;
using Dalamud.Plugin;
using OtterGui.Widgets;
using Penumbra.GameData.Data;
using Penumbra.GameData.Files;
namespace Penumbra.Util;
public class StainManager : IDisposable
{
public sealed class StainTemplateCombo : FilterComboCache< ushort >
{
public StainTemplateCombo( IEnumerable< ushort > items )
: base( items )
{ }
}
public readonly StainData StainData;
public readonly FilterComboColors StainCombo;
public readonly StmFile StmFile;
public readonly StainTemplateCombo TemplateCombo;
public StainManager( DalamudPluginInterface pluginInterface, DataManager dataManager )
{
StainData = new StainData( pluginInterface, dataManager, dataManager.Language );
StainCombo = new FilterComboColors( 140, StainData.Data.Prepend( new KeyValuePair< byte, (string Name, uint Dye, bool Gloss) >( 0, ( "None", 0, false ) ) ) );
StmFile = new StmFile( dataManager );
TemplateCombo = new StainTemplateCombo( StmFile.Entries.Keys.Prepend( ( ushort )0 ) );
}
public void Dispose()
=> StainData.Dispose();
}