mirror of
https://github.com/xivdev/Penumbra.git
synced 2025-12-12 18:27:24 +01:00
This is going rather well.
This commit is contained in:
parent
73e2793da6
commit
bdaff7b781
48 changed files with 2944 additions and 2952 deletions
2
OtterGui
2
OtterGui
|
|
@ -1 +1 @@
|
|||
Subproject commit d7867dfa6579d4e69876753e9cde72e13d3372ce
|
||||
Subproject commit 3d346700e8800c045aa19d70d516d8a4fda2f2ee
|
||||
|
|
@ -1404,10 +1404,10 @@ public class IpcTester : IDisposable
|
|||
return;
|
||||
}
|
||||
|
||||
foreach( var collection in Penumbra.TempMods.CustomCollections.Values )
|
||||
foreach( var collection in Penumbra.TempCollections.Values )
|
||||
{
|
||||
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}" ) )
|
||||
{
|
||||
Mod.TemporaryMod.SaveTempCollection( collection, character );
|
||||
|
|
@ -1416,7 +1416,7 @@ public class IpcTester : IDisposable
|
|||
ImGuiUtil.DrawTableColumn( collection.Name );
|
||||
ImGuiUtil.DrawTableColumn( collection.ResolvedFiles.Count.ToString() );
|
||||
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 ) ) );
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
119
Penumbra/Api/TempCollectionManager.cs
Normal file
119
Penumbra/Api/TempCollectionManager.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
|
|
@ -3,10 +3,7 @@ using Penumbra.Collections;
|
|||
using Penumbra.Meta.Manipulations;
|
||||
using Penumbra.Mods;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Linq;
|
||||
using Penumbra.GameData.Actors;
|
||||
using Penumbra.String;
|
||||
using Penumbra.Services;
|
||||
using Penumbra.String.Classes;
|
||||
|
||||
namespace Penumbra.Api;
|
||||
|
|
@ -19,319 +16,120 @@ public enum RedirectResult
|
|||
FilteredGamePath = 3,
|
||||
}
|
||||
|
||||
public class TempModManager
|
||||
public class TempModManager : IDisposable
|
||||
{
|
||||
public int GlobalChangeCounter { get; private set; } = 0;
|
||||
private readonly Dictionary< ModCollection, List< Mod.TemporaryMod > > _mods = new();
|
||||
private readonly List< Mod.TemporaryMod > _modsForAllCollections = new();
|
||||
private readonly Dictionary< string, ModCollection > _customCollections = new();
|
||||
public readonly IndividualCollections Collections = new(Penumbra.Actors);
|
||||
private readonly CommunicatorService _communicator;
|
||||
|
||||
public event ModCollection.Manager.CollectionChangeDelegate? CollectionChanged;
|
||||
private readonly Dictionary<ModCollection, List<Mod.TemporaryMod>> _mods = new();
|
||||
private readonly List<Mod.TemporaryMod> _modsForAllCollections = new();
|
||||
|
||||
public IReadOnlyDictionary< ModCollection, List< Mod.TemporaryMod > > Mods
|
||||
public TempModManager(CommunicatorService communicator)
|
||||
{
|
||||
_communicator = communicator;
|
||||
_communicator.CollectionChange.Event += OnCollectionChange;
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
_communicator.CollectionChange.Event -= OnCollectionChange;
|
||||
}
|
||||
|
||||
public IReadOnlyDictionary<ModCollection, List<Mod.TemporaryMod>> Mods
|
||||
=> _mods;
|
||||
|
||||
public IReadOnlyList< Mod.TemporaryMod > ModsForAllCollections
|
||||
public IReadOnlyList<Mod.TemporaryMod> 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,
|
||||
HashSet< MetaManipulation > manips, int priority )
|
||||
public RedirectResult Register(string tag, ModCollection? collection, Dictionary<Utf8GamePath, FullPath> dict,
|
||||
HashSet<MetaManipulation> manips, int priority)
|
||||
{
|
||||
var mod = GetOrCreateMod( tag, collection, priority, out var created );
|
||||
mod.SetAll( dict, manips );
|
||||
ApplyModChange( mod, collection, created, false );
|
||||
var mod = GetOrCreateMod(tag, collection, priority, out var created);
|
||||
mod.SetAll(dict, manips);
|
||||
ApplyModChange(mod, collection, created, false);
|
||||
return RedirectResult.Success;
|
||||
}
|
||||
|
||||
public RedirectResult Unregister( string tag, ModCollection? collection, int? priority )
|
||||
public RedirectResult Unregister(string tag, ModCollection? collection, int? priority)
|
||||
{
|
||||
var list = collection == null ? _modsForAllCollections : _mods.TryGetValue( collection, out var l ) ? l : null;
|
||||
if( list == null )
|
||||
{
|
||||
var list = collection == null ? _modsForAllCollections : _mods.TryGetValue(collection, out var l) ? l : null;
|
||||
if (list == null)
|
||||
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;
|
||||
}
|
||||
|
||||
ApplyModChange( m, collection, false, true );
|
||||
ApplyModChange(m, collection, false, true);
|
||||
return true;
|
||||
} );
|
||||
});
|
||||
|
||||
if( removed == 0 )
|
||||
{
|
||||
if (removed == 0)
|
||||
return RedirectResult.NotRegistered;
|
||||
}
|
||||
|
||||
if( list.Count == 0 && collection != null )
|
||||
{
|
||||
_mods.Remove( collection );
|
||||
}
|
||||
if (list.Count == 0 && collection != null)
|
||||
_mods.Remove(collection);
|
||||
|
||||
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.
|
||||
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 )
|
||||
{
|
||||
foreach( var c in Penumbra.CollectionManager )
|
||||
{
|
||||
c.Remove( mod );
|
||||
}
|
||||
}
|
||||
if (removed)
|
||||
collection.Remove(mod);
|
||||
else
|
||||
{
|
||||
foreach( var c in Penumbra.CollectionManager )
|
||||
{
|
||||
c.Apply( mod, created );
|
||||
}
|
||||
}
|
||||
collection.Apply(mod, created);
|
||||
}
|
||||
else
|
||||
{
|
||||
if( removed )
|
||||
{
|
||||
collection.Remove( mod );
|
||||
}
|
||||
else
|
||||
{
|
||||
collection.Apply( mod, created );
|
||||
}
|
||||
_communicator.TemporaryGlobalModChange.Invoke(mod, created, removed);
|
||||
}
|
||||
}
|
||||
|
||||
// Only find already existing mods, currently unused.
|
||||
//private Mod.TemporaryMod? GetExistingMod( string tag, ModCollection? collection, int? priority )
|
||||
//{
|
||||
// var list = collection == null ? _modsForAllCollections : _mods.TryGetValue( collection, out var l ) ? l : null;
|
||||
// if( list == null )
|
||||
// {
|
||||
// return null;
|
||||
// }
|
||||
//
|
||||
// if( priority != null )
|
||||
// {
|
||||
// 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;
|
||||
//}
|
||||
|
||||
/// <summary>
|
||||
/// Apply a mod change to a set of collections.
|
||||
/// </summary>
|
||||
public static void OnGlobalModChange(IEnumerable<ModCollection> collections, Mod.TemporaryMod mod, bool created, bool removed)
|
||||
{
|
||||
if (removed)
|
||||
foreach (var c in collections)
|
||||
c.Remove(mod);
|
||||
else
|
||||
foreach (var c in collections)
|
||||
c.Apply(mod, created);
|
||||
}
|
||||
|
||||
// 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.
|
||||
private Mod.TemporaryMod GetOrCreateMod( string tag, ModCollection? collection, int priority, out bool created )
|
||||
private Mod.TemporaryMod GetOrCreateMod(string tag, ModCollection? collection, int priority, out bool created)
|
||||
{
|
||||
List< Mod.TemporaryMod > list;
|
||||
if( collection == null )
|
||||
List<Mod.TemporaryMod> list;
|
||||
if (collection == null)
|
||||
{
|
||||
list = _modsForAllCollections;
|
||||
}
|
||||
else if( _mods.TryGetValue( collection, out var l ) )
|
||||
else if (_mods.TryGetValue(collection, out var l))
|
||||
{
|
||||
list = l;
|
||||
}
|
||||
else
|
||||
{
|
||||
list = new List< Mod.TemporaryMod >();
|
||||
_mods.Add( collection, list );
|
||||
list = new List<Mod.TemporaryMod>();
|
||||
_mods.Add(collection, list);
|
||||
}
|
||||
|
||||
var mod = list.Find( m => m.Priority == priority && m.Name == tag );
|
||||
if( mod == null )
|
||||
var mod = list.Find(m => m.Priority == priority && m.Name == tag);
|
||||
if (mod == null)
|
||||
{
|
||||
mod = new Mod.TemporaryMod()
|
||||
{
|
||||
Name = tag,
|
||||
Priority = priority,
|
||||
};
|
||||
list.Add( mod );
|
||||
list.Add(mod);
|
||||
created = true;
|
||||
}
|
||||
else
|
||||
|
|
@ -341,4 +139,11 @@ public class TempModManager
|
|||
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -9,10 +9,11 @@ using System.IO;
|
|||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using Dalamud.Interface.Internal.Notifications;
|
||||
using Dalamud.Plugin;
|
||||
using Penumbra.GameData.Actors;
|
||||
using Penumbra.Util;
|
||||
using Penumbra.Services;
|
||||
|
||||
using Penumbra.Services;
|
||||
|
||||
namespace Penumbra.Collections;
|
||||
|
||||
public partial class ModCollection
|
||||
|
|
@ -21,9 +22,6 @@ public partial class ModCollection
|
|||
{
|
||||
public const int Version = 1;
|
||||
|
||||
// Is invoked after the collections actually changed.
|
||||
public event CollectionChangeDelegate CollectionChanged;
|
||||
|
||||
// The collection currently selected for changing settings.
|
||||
public ModCollection Current { get; private set; } = Empty;
|
||||
|
||||
|
|
@ -40,65 +38,62 @@ public partial class ModCollection
|
|||
private ModCollection DefaultName { get; set; } = Empty;
|
||||
|
||||
// The list of character collections.
|
||||
// TODO
|
||||
public readonly IndividualCollections Individuals = new(Penumbra.Actors);
|
||||
|
||||
public ModCollection Individual( ActorIdentifier identifier )
|
||||
=> Individuals.TryGetCollection( identifier, out var c ) ? c : Default;
|
||||
public ModCollection Individual(ActorIdentifier identifier)
|
||||
=> Individuals.TryGetCollection(identifier, out var c) ? c : Default;
|
||||
|
||||
// Special Collections
|
||||
private readonly ModCollection?[] _specialCollections = new ModCollection?[Enum.GetValues< Api.Enums.ApiCollectionType >().Length - 3];
|
||||
private readonly ModCollection?[] _specialCollections = new ModCollection?[Enum.GetValues<Api.Enums.ApiCollectionType>().Length - 3];
|
||||
|
||||
// Return the configured collection for the given type or null.
|
||||
// Does not handle Inactive, use ByName instead.
|
||||
public ModCollection? ByType( CollectionType type )
|
||||
=> ByType( type, ActorIdentifier.Invalid );
|
||||
public ModCollection? ByType(CollectionType type)
|
||||
=> ByType(type, ActorIdentifier.Invalid);
|
||||
|
||||
public ModCollection? ByType( CollectionType type, ActorIdentifier identifier )
|
||||
public ModCollection? ByType(CollectionType type, ActorIdentifier identifier)
|
||||
{
|
||||
if( type.IsSpecial() )
|
||||
{
|
||||
return _specialCollections[ ( int )type ];
|
||||
}
|
||||
if (type.IsSpecial())
|
||||
return _specialCollections[(int)type];
|
||||
|
||||
return type switch
|
||||
{
|
||||
CollectionType.Default => Default,
|
||||
CollectionType.Interface => Interface,
|
||||
CollectionType.Current => Current,
|
||||
CollectionType.Individual => identifier.IsValid && Individuals.Individuals.TryGetValue( identifier, out var c ) ? c : null,
|
||||
CollectionType.Individual => identifier.IsValid && Individuals.Individuals.TryGetValue(identifier, out var c) ? c : null,
|
||||
_ => null,
|
||||
};
|
||||
}
|
||||
|
||||
// Set a active collection, can be used to set Default, Current, Interface, Special, or Individual collections.
|
||||
private void SetCollection( int newIdx, CollectionType collectionType, int individualIndex = -1 )
|
||||
private void SetCollection(int newIdx, CollectionType collectionType, int individualIndex = -1)
|
||||
{
|
||||
var oldCollectionIdx = collectionType switch
|
||||
{
|
||||
CollectionType.Default => Default.Index,
|
||||
CollectionType.Interface => Interface.Index,
|
||||
CollectionType.Current => Current.Index,
|
||||
CollectionType.Individual => individualIndex < 0 || individualIndex >= Individuals.Count ? -1 : Individuals[ individualIndex ].Collection.Index,
|
||||
_ when collectionType.IsSpecial() => _specialCollections[ ( int )collectionType ]?.Index ?? Default.Index,
|
||||
CollectionType.Default => Default.Index,
|
||||
CollectionType.Interface => Interface.Index,
|
||||
CollectionType.Current => Current.Index,
|
||||
CollectionType.Individual => individualIndex < 0 || individualIndex >= Individuals.Count
|
||||
? -1
|
||||
: Individuals[individualIndex].Collection.Index,
|
||||
_ when collectionType.IsSpecial() => _specialCollections[(int)collectionType]?.Index ?? Default.Index,
|
||||
_ => -1,
|
||||
};
|
||||
|
||||
if( oldCollectionIdx == -1 || newIdx == oldCollectionIdx )
|
||||
{
|
||||
if (oldCollectionIdx == -1 || newIdx == oldCollectionIdx)
|
||||
return;
|
||||
}
|
||||
|
||||
var newCollection = this[ newIdx ];
|
||||
if( newIdx > Empty.Index )
|
||||
{
|
||||
var newCollection = this[newIdx];
|
||||
if (newIdx > Empty.Index)
|
||||
newCollection.CreateCache();
|
||||
}
|
||||
|
||||
switch( collectionType )
|
||||
switch (collectionType)
|
||||
{
|
||||
case CollectionType.Default:
|
||||
Default = newCollection;
|
||||
if( Penumbra.CharacterUtility.Ready && Penumbra.Config.EnableMods )
|
||||
if (Penumbra.CharacterUtility.Ready && Penumbra.Config.EnableMods)
|
||||
{
|
||||
Penumbra.ResidentResources.Reload();
|
||||
Default.SetFiles();
|
||||
|
|
@ -112,362 +107,336 @@ public partial class ModCollection
|
|||
Current = newCollection;
|
||||
break;
|
||||
case CollectionType.Individual:
|
||||
if( !Individuals.ChangeCollection( individualIndex, newCollection ) )
|
||||
if (!Individuals.ChangeCollection(individualIndex, newCollection))
|
||||
{
|
||||
RemoveCache( newIdx );
|
||||
RemoveCache(newIdx);
|
||||
return;
|
||||
}
|
||||
|
||||
break;
|
||||
default:
|
||||
_specialCollections[ ( int )collectionType ] = newCollection;
|
||||
_specialCollections[(int)collectionType] = newCollection;
|
||||
break;
|
||||
}
|
||||
|
||||
RemoveCache( oldCollectionIdx );
|
||||
RemoveCache(oldCollectionIdx);
|
||||
|
||||
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()
|
||||
=> CurrentCollectionInUse = _specialCollections
|
||||
.OfType< ModCollection >()
|
||||
.Prepend( Interface )
|
||||
.Prepend( Default )
|
||||
.Concat( Individuals.Assignments.Select( kvp => kvp.Collection ) )
|
||||
.SelectMany( c => c.GetFlattenedInheritance() ).Contains( Current );
|
||||
.OfType<ModCollection>()
|
||||
.Prepend(Interface)
|
||||
.Prepend(Default)
|
||||
.Concat(Individuals.Assignments.Select(kvp => kvp.Collection))
|
||||
.SelectMany(c => c.GetFlattenedInheritance()).Contains(Current);
|
||||
|
||||
public void SetCollection( ModCollection collection, CollectionType collectionType, int individualIndex = -1 )
|
||||
=> SetCollection( collection.Index, collectionType, individualIndex );
|
||||
public void SetCollection(ModCollection collection, CollectionType collectionType, int individualIndex = -1)
|
||||
=> SetCollection(collection.Index, collectionType, individualIndex);
|
||||
|
||||
// Create a special collection if it does not exist and set it to Empty.
|
||||
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;
|
||||
}
|
||||
|
||||
_specialCollections[ ( int )collectionType ] = Default;
|
||||
CollectionChanged.Invoke( collectionType, null, Default );
|
||||
_specialCollections[(int)collectionType] = Default;
|
||||
_communicator.CollectionChange.Invoke(collectionType, null, Default, string.Empty);
|
||||
return true;
|
||||
}
|
||||
|
||||
// Remove a special collection if it exists
|
||||
public void RemoveSpecialCollection( CollectionType collectionType )
|
||||
public void RemoveSpecialCollection(CollectionType collectionType)
|
||||
{
|
||||
if( !collectionType.IsSpecial() )
|
||||
{
|
||||
if (!collectionType.IsSpecial())
|
||||
return;
|
||||
}
|
||||
|
||||
var old = _specialCollections[ ( int )collectionType ];
|
||||
if( old != null )
|
||||
var old = _specialCollections[(int)collectionType];
|
||||
if (old != null)
|
||||
{
|
||||
_specialCollections[ ( int )collectionType ] = null;
|
||||
CollectionChanged.Invoke( collectionType, old, null );
|
||||
_specialCollections[(int)collectionType] = null;
|
||||
_communicator.CollectionChange.Invoke(collectionType, old, null, string.Empty);
|
||||
}
|
||||
}
|
||||
|
||||
// Wrappers around Individual Collection handling.
|
||||
public void CreateIndividualCollection( params ActorIdentifier[] identifiers )
|
||||
public void CreateIndividualCollection(params ActorIdentifier[] identifiers)
|
||||
{
|
||||
if( Individuals.Add( identifiers, Default ) )
|
||||
{
|
||||
CollectionChanged.Invoke( CollectionType.Individual, null, Default, Individuals.Last().DisplayName );
|
||||
}
|
||||
if (Individuals.Add(identifiers, Default))
|
||||
_communicator.CollectionChange.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;
|
||||
}
|
||||
|
||||
var (name, old) = Individuals[ individualIndex ];
|
||||
if( Individuals.Delete( individualIndex ) )
|
||||
{
|
||||
CollectionChanged.Invoke( CollectionType.Individual, old, null, name );
|
||||
}
|
||||
var (name, old) = Individuals[individualIndex];
|
||||
if (Individuals.Delete(individualIndex))
|
||||
_communicator.CollectionChange.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();
|
||||
}
|
||||
}
|
||||
|
||||
// Obtain the index of a collection by name.
|
||||
private int GetIndexForCollectionName( string name )
|
||||
=> name.Length == 0 ? Empty.Index : _collections.IndexOf( c => c.Name == name );
|
||||
private int GetIndexForCollectionName(string name)
|
||||
=> name.Length == 0 ? Empty.Index : _collections.IndexOf(c => c.Name == name);
|
||||
|
||||
public static string ActiveCollectionFile
|
||||
=> Path.Combine( DalamudServices.PluginInterface.ConfigDirectory.FullName, "active_collections.json" );
|
||||
public static string ActiveCollectionFile(DalamudPluginInterface pi)
|
||||
=> Path.Combine(pi.ConfigDirectory.FullName, "active_collections.json");
|
||||
|
||||
// Load default, current, special, and character collections from config.
|
||||
// Then create caches. If a collection does not exist anymore, reset it to an appropriate default.
|
||||
private void LoadCollections()
|
||||
{
|
||||
var configChanged = !ReadActiveCollections( out var jObject );
|
||||
var configChanged = !ReadActiveCollections(out var jObject);
|
||||
|
||||
// Load the default collection.
|
||||
var defaultName = jObject[ nameof( Default ) ]?.ToObject< string >() ?? ( configChanged ? DefaultCollection : Empty.Name );
|
||||
var defaultIdx = GetIndexForCollectionName( defaultName );
|
||||
if( defaultIdx < 0 )
|
||||
var defaultName = jObject[nameof(Default)]?.ToObject<string>() ?? (configChanged ? DefaultCollection : Empty.Name);
|
||||
var defaultIdx = GetIndexForCollectionName(defaultName);
|
||||
if (defaultIdx < 0)
|
||||
{
|
||||
ChatUtil.NotificationMessage( $"Last choice of {ConfigWindow.DefaultCollection} {defaultName} is not available, reset to {Empty.Name}.", "Load Failure",
|
||||
NotificationType.Warning );
|
||||
ChatUtil.NotificationMessage(
|
||||
$"Last choice of {ConfigWindow.DefaultCollection} {defaultName} is not available, reset to {Empty.Name}.", "Load Failure",
|
||||
NotificationType.Warning);
|
||||
Default = Empty;
|
||||
configChanged = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
Default = this[ defaultIdx ];
|
||||
Default = this[defaultIdx];
|
||||
}
|
||||
|
||||
// Load the interface collection.
|
||||
var interfaceName = jObject[ nameof( Interface ) ]?.ToObject< string >() ?? Default.Name;
|
||||
var interfaceIdx = GetIndexForCollectionName( interfaceName );
|
||||
if( interfaceIdx < 0 )
|
||||
var interfaceName = jObject[nameof(Interface)]?.ToObject<string>() ?? Default.Name;
|
||||
var interfaceIdx = GetIndexForCollectionName(interfaceName);
|
||||
if (interfaceIdx < 0)
|
||||
{
|
||||
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;
|
||||
configChanged = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
Interface = this[ interfaceIdx ];
|
||||
Interface = this[interfaceIdx];
|
||||
}
|
||||
|
||||
// Load the current collection.
|
||||
var currentName = jObject[ nameof( Current ) ]?.ToObject< string >() ?? DefaultCollection;
|
||||
var currentIdx = GetIndexForCollectionName( currentName );
|
||||
if( currentIdx < 0 )
|
||||
var currentName = jObject[nameof(Current)]?.ToObject<string>() ?? DefaultCollection;
|
||||
var currentIdx = GetIndexForCollectionName(currentName);
|
||||
if (currentIdx < 0)
|
||||
{
|
||||
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;
|
||||
configChanged = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
Current = this[ currentIdx ];
|
||||
Current = this[currentIdx];
|
||||
}
|
||||
|
||||
// Load special collections.
|
||||
foreach( var (type, name, _) in CollectionTypeExtensions.Special )
|
||||
foreach (var (type, name, _) in CollectionTypeExtensions.Special)
|
||||
{
|
||||
var typeName = jObject[ type.ToString() ]?.ToObject< string >();
|
||||
if( typeName != null )
|
||||
var typeName = jObject[type.ToString()]?.ToObject<string>();
|
||||
if (typeName != null)
|
||||
{
|
||||
var idx = GetIndexForCollectionName( typeName );
|
||||
if( idx < 0 )
|
||||
var idx = GetIndexForCollectionName(typeName);
|
||||
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;
|
||||
}
|
||||
else
|
||||
{
|
||||
_specialCollections[ ( int )type ] = this[ idx ];
|
||||
_specialCollections[(int)type] = this[idx];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
configChanged |= MigrateIndividualCollections( jObject );
|
||||
configChanged |= Individuals.ReadJObject( jObject[ nameof( Individuals ) ] as JArray, this );
|
||||
configChanged |= MigrateIndividualCollections(jObject);
|
||||
configChanged |= Individuals.ReadJObject(jObject[nameof(Individuals)] as JArray, this);
|
||||
|
||||
// Save any changes and create all required caches.
|
||||
if( configChanged )
|
||||
{
|
||||
if (configChanged)
|
||||
SaveActiveCollections();
|
||||
}
|
||||
}
|
||||
|
||||
// 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;
|
||||
}
|
||||
|
||||
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 value = jObject[ oldName ];
|
||||
if( value == null )
|
||||
{
|
||||
var oldName = type.ToString()[4..];
|
||||
var value = jObject[oldName];
|
||||
if (value == null)
|
||||
continue;
|
||||
}
|
||||
|
||||
jObject.Remove( oldName );
|
||||
jObject.Add( "Male" + oldName, value );
|
||||
jObject.Add( "Female" + oldName, value );
|
||||
jObject.Remove(oldName);
|
||||
jObject.Add("Male" + oldName, value);
|
||||
jObject.Add("Female" + oldName, value);
|
||||
}
|
||||
|
||||
using var stream = File.Open( ActiveCollectionFile, FileMode.Truncate );
|
||||
using var writer = new StreamWriter( stream );
|
||||
using var j = new JsonTextWriter( writer );
|
||||
using var stream = File.Open(fileNames.ActiveCollectionsFile, FileMode.Truncate);
|
||||
using var writer = new StreamWriter(stream);
|
||||
using var j = new JsonTextWriter(writer);
|
||||
j.Formatting = Formatting.Indented;
|
||||
jObject.WriteTo( j );
|
||||
jObject.WriteTo(j);
|
||||
}
|
||||
|
||||
// Migrate individual collections to Identifiers for 0.6.0.
|
||||
private bool MigrateIndividualCollections( JObject jObject )
|
||||
private bool MigrateIndividualCollections(JObject jObject)
|
||||
{
|
||||
var version = jObject[ nameof( Version ) ]?.Value< int >() ?? 0;
|
||||
if( version > 0 )
|
||||
{
|
||||
var version = jObject[nameof(Version)]?.Value<int>() ?? 0;
|
||||
if (version > 0)
|
||||
return false;
|
||||
}
|
||||
|
||||
// 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 dict = new Dictionary< string, ModCollection >( characters.Count );
|
||||
foreach( var (player, collectionName) in characters )
|
||||
var characters = jObject["Characters"]?.ToObject<Dictionary<string, string>>() ?? new Dictionary<string, string>();
|
||||
var dict = new Dictionary<string, ModCollection>(characters.Count);
|
||||
foreach (var (player, collectionName) in characters)
|
||||
{
|
||||
var idx = GetIndexForCollectionName( collectionName );
|
||||
if( idx < 0 )
|
||||
var idx = GetIndexForCollectionName(collectionName);
|
||||
if (idx < 0)
|
||||
{
|
||||
ChatUtil.NotificationMessage( $"Last choice of <{player}>'s Collection {collectionName} is not available, reset to {Empty.Name}.", "Load Failure",
|
||||
NotificationType.Warning );
|
||||
dict.Add( player, Empty );
|
||||
ChatUtil.NotificationMessage(
|
||||
$"Last choice of <{player}>'s Collection {collectionName} is not available, reset to {Empty.Name}.", "Load Failure",
|
||||
NotificationType.Warning);
|
||||
dict.Add(player, Empty);
|
||||
}
|
||||
else
|
||||
{
|
||||
dict.Add( player, this[ idx ] );
|
||||
dict.Add(player, this[idx]);
|
||||
}
|
||||
}
|
||||
|
||||
Individuals.Migrate0To1( dict );
|
||||
Individuals.Migrate0To1(dict);
|
||||
return true;
|
||||
}
|
||||
|
||||
public void SaveActiveCollections()
|
||||
{
|
||||
Penumbra.Framework.RegisterDelayed( nameof( SaveActiveCollections ),
|
||||
SaveActiveCollectionsInternal );
|
||||
Penumbra.Framework.RegisterDelayed(nameof(SaveActiveCollections),
|
||||
SaveActiveCollectionsInternal);
|
||||
}
|
||||
|
||||
internal void SaveActiveCollectionsInternal()
|
||||
{
|
||||
var file = ActiveCollectionFile;
|
||||
// TODO
|
||||
var file = ActiveCollectionFile(DalamudServices.PluginInterface);
|
||||
try
|
||||
{
|
||||
var jObj = new JObject
|
||||
{
|
||||
{ nameof( Version ), Version },
|
||||
{ nameof( Default ), Default.Name },
|
||||
{ nameof( Interface ), Interface.Name },
|
||||
{ nameof( Current ), Current.Name },
|
||||
{ nameof(Version), Version },
|
||||
{ nameof(Default), Default.Name },
|
||||
{ nameof(Interface), Interface.Name },
|
||||
{ nameof(Current), Current.Name },
|
||||
};
|
||||
foreach( var (type, collection) in _specialCollections.WithIndex().Where( p => p.Value != null ).Select( p => ( ( CollectionType )p.Index, p.Value! ) ) )
|
||||
{
|
||||
jObj.Add( type.ToString(), collection.Name );
|
||||
}
|
||||
foreach (var (type, collection) in _specialCollections.WithIndex().Where(p => p.Value != null)
|
||||
.Select(p => ((CollectionType)p.Index, p.Value!)))
|
||||
jObj.Add(type.ToString(), collection.Name);
|
||||
|
||||
jObj.Add( nameof( Individuals ), Individuals.ToJObject() );
|
||||
using var stream = File.Open( file, File.Exists( file ) ? FileMode.Truncate : FileMode.CreateNew );
|
||||
using var writer = new StreamWriter( stream );
|
||||
using var j = new JsonTextWriter( writer )
|
||||
{ Formatting = Formatting.Indented };
|
||||
jObj.WriteTo( j );
|
||||
Penumbra.Log.Verbose( "Active Collections saved." );
|
||||
jObj.Add(nameof(Individuals), Individuals.ToJObject());
|
||||
using var stream = File.Open(file, File.Exists(file) ? FileMode.Truncate : FileMode.CreateNew);
|
||||
using var writer = new StreamWriter(stream);
|
||||
using var j = new JsonTextWriter(writer) { Formatting = Formatting.Indented };
|
||||
jObj.WriteTo(j);
|
||||
Penumbra.Log.Verbose("Active Collections saved.");
|
||||
}
|
||||
catch( Exception e )
|
||||
catch (Exception e)
|
||||
{
|
||||
Penumbra.Log.Error( $"Could not save active collections to file {file}:\n{e}" );
|
||||
Penumbra.Log.Error($"Could not save active collections to file {file}:\n{e}");
|
||||
}
|
||||
}
|
||||
|
||||
// Read the active collection file into a jObject.
|
||||
// Returns true if this is successful, false if the file does not exist or it is unsuccessful.
|
||||
private static bool ReadActiveCollections( out JObject ret )
|
||||
private static bool ReadActiveCollections(out JObject ret)
|
||||
{
|
||||
var file = ActiveCollectionFile;
|
||||
if( File.Exists( file ) )
|
||||
{
|
||||
// TODO
|
||||
var file = ActiveCollectionFile(DalamudServices.PluginInterface);
|
||||
if (File.Exists(file))
|
||||
try
|
||||
{
|
||||
ret = JObject.Parse( File.ReadAllText( file ) );
|
||||
ret = JObject.Parse(File.ReadAllText(file));
|
||||
return true;
|
||||
}
|
||||
catch( Exception e )
|
||||
catch (Exception e)
|
||||
{
|
||||
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();
|
||||
return false;
|
||||
}
|
||||
|
||||
// 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();
|
||||
}
|
||||
}
|
||||
|
||||
// Cache handling. Usually recreate caches on the next framework tick,
|
||||
// but at launch create all of them at once.
|
||||
public void CreateNecessaryCaches()
|
||||
{
|
||||
var tasks = _specialCollections.OfType< ModCollection >()
|
||||
.Concat( Individuals.Select( p => p.Collection ) )
|
||||
.Prepend( Current )
|
||||
.Prepend( Default )
|
||||
.Prepend( Interface )
|
||||
.Distinct()
|
||||
.Select( c => Task.Run( c.CalculateEffectiveFileListInternal ) )
|
||||
.ToArray();
|
||||
var tasks = _specialCollections.OfType<ModCollection>()
|
||||
.Concat(Individuals.Select(p => p.Collection))
|
||||
.Prepend(Current)
|
||||
.Prepend(Default)
|
||||
.Prepend(Interface)
|
||||
.Distinct()
|
||||
.Select(c => Task.Run(c.CalculateEffectiveFileListInternal))
|
||||
.ToArray();
|
||||
|
||||
Task.WaitAll( tasks );
|
||||
Task.WaitAll(tasks);
|
||||
}
|
||||
|
||||
private void RemoveCache( int idx )
|
||||
private void RemoveCache(int idx)
|
||||
{
|
||||
if( idx != Empty.Index
|
||||
&& idx != Default.Index
|
||||
&& idx != Interface.Index
|
||||
&& idx != Current.Index
|
||||
&& _specialCollections.All( c => c == null || c.Index != idx )
|
||||
&& Individuals.Select( p => p.Collection ).All( c => c.Index != idx ) )
|
||||
{
|
||||
_collections[ idx ].ClearCache();
|
||||
}
|
||||
if (idx != Empty.Index
|
||||
&& idx != Default.Index
|
||||
&& idx != Interface.Index
|
||||
&& idx != Current.Index
|
||||
&& _specialCollections.All(c => c == null || c.Index != idx)
|
||||
&& Individuals.Select(p => p.Collection).All(c => c.Index != idx))
|
||||
_collections[idx].ClearCache();
|
||||
}
|
||||
|
||||
// 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 ) )
|
||||
{
|
||||
collection._cache!.AddMod( mod, true );
|
||||
}
|
||||
foreach (var collection in this.Where(c => c.HasCache && c[mod.Index].Settings?.Enabled == 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 ) )
|
||||
{
|
||||
collection._cache!.RemoveMod( mod, true );
|
||||
}
|
||||
foreach (var collection in this.Where(c => c.HasCache && c[mod.Index].Settings?.Enabled == 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 ) )
|
||||
{
|
||||
collection._cache!.ReloadMod( mod, true );
|
||||
}
|
||||
foreach (var collection in this.Where(c => c.HasCache && c[mod.Index].Settings?.Enabled == true))
|
||||
collection._cache!.ReloadMod(mod, true);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -7,60 +7,60 @@ using System.Collections.Generic;
|
|||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using Penumbra.Api;
|
||||
using Penumbra.Services;
|
||||
|
||||
namespace Penumbra.Collections;
|
||||
|
||||
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.
|
||||
// It can not be deleted or moved.
|
||||
private readonly List< ModCollection > _collections = new()
|
||||
private readonly List<ModCollection> _collections = new()
|
||||
{
|
||||
Empty,
|
||||
};
|
||||
|
||||
public ModCollection this[ Index idx ]
|
||||
=> _collections[ idx ];
|
||||
public ModCollection this[Index idx]
|
||||
=> _collections[idx];
|
||||
|
||||
public ModCollection? this[ string name ]
|
||||
=> ByName( name, out var c ) ? c : null;
|
||||
public ModCollection? this[string name]
|
||||
=> ByName(name, out var c) ? c : null;
|
||||
|
||||
public int Count
|
||||
=> _collections.Count;
|
||||
|
||||
// Obtain a collection case-independently by name.
|
||||
public bool ByName( string name, [NotNullWhen( true )] out ModCollection? collection )
|
||||
=> _collections.FindFirst( c => string.Equals( c.Name, name, StringComparison.OrdinalIgnoreCase ), out collection );
|
||||
public bool ByName(string name, [NotNullWhen(true)] out ModCollection? collection)
|
||||
=> _collections.FindFirst(c => string.Equals(c.Name, name, StringComparison.OrdinalIgnoreCase), out collection);
|
||||
|
||||
// Default enumeration skips the empty collection.
|
||||
public IEnumerator< ModCollection > GetEnumerator()
|
||||
=> _collections.Skip( 1 ).GetEnumerator();
|
||||
public IEnumerator<ModCollection> GetEnumerator()
|
||||
=> _collections.Skip(1).GetEnumerator();
|
||||
|
||||
IEnumerator IEnumerable.GetEnumerator()
|
||||
=> GetEnumerator();
|
||||
|
||||
public IEnumerable< ModCollection > GetEnumeratorWithEmpty()
|
||||
public IEnumerable<ModCollection> GetEnumeratorWithEmpty()
|
||||
=> _collections;
|
||||
|
||||
public Manager( Mod.Manager manager )
|
||||
public Manager(CommunicatorService communicator, Mod.Manager manager)
|
||||
{
|
||||
_modManager = manager;
|
||||
_communicator = communicator;
|
||||
_modManager = manager;
|
||||
|
||||
// The collection manager reacts to changes in mods by itself.
|
||||
_modManager.ModDiscoveryStarted += OnModDiscoveryStarted;
|
||||
_modManager.ModDiscoveryFinished += OnModDiscoveryFinished;
|
||||
_modManager.ModOptionChanged += OnModOptionsChanged;
|
||||
_modManager.ModPathChanged += OnModPathChange;
|
||||
CollectionChanged += SaveOnChange;
|
||||
_modManager.ModDiscoveryStarted += OnModDiscoveryStarted;
|
||||
_modManager.ModDiscoveryFinished += OnModDiscoveryFinished;
|
||||
_modManager.ModOptionChanged += OnModOptionsChanged;
|
||||
_modManager.ModPathChanged += OnModPathChange;
|
||||
_communicator.CollectionChange.Event += SaveOnChange;
|
||||
_communicator.TemporaryGlobalModChange.Event += OnGlobalModChange;
|
||||
ReadCollections();
|
||||
LoadCollections();
|
||||
UpdateCurrentCollectionInUse();
|
||||
|
|
@ -68,26 +68,31 @@ public partial class ModCollection
|
|||
|
||||
public void Dispose()
|
||||
{
|
||||
_modManager.ModDiscoveryStarted -= OnModDiscoveryStarted;
|
||||
_modManager.ModDiscoveryFinished -= OnModDiscoveryFinished;
|
||||
_modManager.ModOptionChanged -= OnModOptionsChanged;
|
||||
_modManager.ModPathChanged -= OnModPathChange;
|
||||
_communicator.CollectionChange.Event -= SaveOnChange;
|
||||
_communicator.TemporaryGlobalModChange.Event -= OnGlobalModChange;
|
||||
_modManager.ModDiscoveryStarted -= OnModDiscoveryStarted;
|
||||
_modManager.ModDiscoveryFinished -= OnModDiscoveryFinished;
|
||||
_modManager.ModOptionChanged -= OnModOptionsChanged;
|
||||
_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
|
||||
// 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)
|
||||
{
|
||||
if( !IsValidName( name ) )
|
||||
if (!IsValidName(name))
|
||||
{
|
||||
fixedName = string.Empty;
|
||||
return false;
|
||||
}
|
||||
|
||||
name = name.RemoveInvalidPathSymbols().ToLowerInvariant();
|
||||
if( name.Length == 0
|
||||
|| name == Empty.Name.ToLowerInvariant()
|
||||
|| _collections.Any( c => c.Name.RemoveInvalidPathSymbols().ToLowerInvariant() == name ) )
|
||||
if (name.Length == 0
|
||||
|| name == Empty.Name.ToLowerInvariant()
|
||||
|| _collections.Any(c => c.Name.RemoveInvalidPathSymbols().ToLowerInvariant() == name))
|
||||
{
|
||||
fixedName = string.Empty;
|
||||
return false;
|
||||
|
|
@ -102,217 +107,179 @@ public partial class ModCollection
|
|||
// If the name of the collection would result in an already existing filename, skip it.
|
||||
// Returns true if the collection was successfully created and fires a Inactive event.
|
||||
// Also sets the current collection to the new collection afterwards.
|
||||
public bool AddCollection( string name, ModCollection? duplicate )
|
||||
public bool AddCollection(string name, ModCollection? duplicate)
|
||||
{
|
||||
if( !CanAddCollection( name, out var fixedName ) )
|
||||
if (!CanAddCollection(name, out var fixedName))
|
||||
{
|
||||
Penumbra.Log.Warning( $"The new collection {name} would lead to the same path {fixedName} as one that already exists." );
|
||||
Penumbra.Log.Warning($"The new collection {name} would lead to the same path {fixedName} as one that already exists.");
|
||||
return false;
|
||||
}
|
||||
|
||||
var newCollection = duplicate?.Duplicate( name ) ?? CreateNewEmpty( name );
|
||||
var newCollection = duplicate?.Duplicate(name) ?? CreateNewEmpty(name);
|
||||
newCollection.Index = _collections.Count;
|
||||
_collections.Add( newCollection );
|
||||
_collections.Add(newCollection);
|
||||
newCollection.Save();
|
||||
Penumbra.Log.Debug( $"Added collection {newCollection.AnonymizedName}." );
|
||||
CollectionChanged.Invoke( CollectionType.Inactive, null, newCollection );
|
||||
SetCollection( newCollection.Index, CollectionType.Current );
|
||||
Penumbra.Log.Debug($"Added collection {newCollection.AnonymizedName}.");
|
||||
_communicator.CollectionChange.Invoke(CollectionType.Inactive, null, newCollection, string.Empty);
|
||||
SetCollection(newCollection.Index, CollectionType.Current);
|
||||
return true;
|
||||
}
|
||||
|
||||
// Remove the given collection if it exists and is neither the empty nor the default-named collection.
|
||||
// If the removed collection was active, it also sets the corresponding collection to the appropriate default.
|
||||
// Also removes the collection from inheritances of all other collections.
|
||||
public bool RemoveCollection( int idx )
|
||||
public bool RemoveCollection(int idx)
|
||||
{
|
||||
if( idx <= Empty.Index || idx >= _collections.Count )
|
||||
if (idx <= Empty.Index || idx >= _collections.Count)
|
||||
{
|
||||
Penumbra.Log.Error( "Can not remove the empty collection." );
|
||||
Penumbra.Log.Error("Can not remove the empty collection.");
|
||||
return false;
|
||||
}
|
||||
|
||||
if( idx == DefaultName.Index )
|
||||
if (idx == DefaultName.Index)
|
||||
{
|
||||
Penumbra.Log.Error( "Can not remove the default collection." );
|
||||
Penumbra.Log.Error("Can not remove the default collection.");
|
||||
return false;
|
||||
}
|
||||
|
||||
if( idx == Current.Index )
|
||||
if (idx == Current.Index)
|
||||
SetCollection(DefaultName.Index, CollectionType.Current);
|
||||
|
||||
if (idx == Default.Index)
|
||||
SetCollection(Empty.Index, CollectionType.Default);
|
||||
|
||||
for (var i = 0; i < _specialCollections.Length; ++i)
|
||||
{
|
||||
SetCollection( DefaultName.Index, CollectionType.Current );
|
||||
if (idx == _specialCollections[i]?.Index)
|
||||
SetCollection(Empty, (CollectionType)i);
|
||||
}
|
||||
|
||||
if( idx == Default.Index )
|
||||
for (var i = 0; i < Individuals.Count; ++i)
|
||||
{
|
||||
SetCollection( Empty.Index, CollectionType.Default );
|
||||
if (Individuals[i].Collection.Index == idx)
|
||||
SetCollection(Empty, CollectionType.Individual, i);
|
||||
}
|
||||
|
||||
for( var i = 0; i < _specialCollections.Length; ++i )
|
||||
{
|
||||
if( idx == _specialCollections[ i ]?.Index )
|
||||
{
|
||||
SetCollection( Empty, ( CollectionType )i );
|
||||
}
|
||||
}
|
||||
|
||||
for( var i = 0; i < Individuals.Count; ++i )
|
||||
{
|
||||
if( Individuals[ i ].Collection.Index == idx )
|
||||
{
|
||||
SetCollection( Empty, CollectionType.Individual, i );
|
||||
}
|
||||
}
|
||||
|
||||
var collection = _collections[ idx ];
|
||||
var collection = _collections[idx];
|
||||
|
||||
// Clear own inheritances.
|
||||
foreach( var inheritance in collection.Inheritance )
|
||||
{
|
||||
collection.ClearSubscriptions( inheritance );
|
||||
}
|
||||
foreach (var inheritance in collection.Inheritance)
|
||||
collection.ClearSubscriptions(inheritance);
|
||||
|
||||
collection.Delete();
|
||||
_collections.RemoveAt( idx );
|
||||
_collections.RemoveAt(idx);
|
||||
|
||||
// Clear external inheritances.
|
||||
foreach( var c in _collections )
|
||||
foreach (var c in _collections)
|
||||
{
|
||||
var inheritedIdx = c._inheritance.IndexOf( collection );
|
||||
if( inheritedIdx >= 0 )
|
||||
{
|
||||
c.RemoveInheritance( inheritedIdx );
|
||||
}
|
||||
var inheritedIdx = c._inheritance.IndexOf(collection);
|
||||
if (inheritedIdx >= 0)
|
||||
c.RemoveInheritance(inheritedIdx);
|
||||
|
||||
if( c.Index > idx )
|
||||
{
|
||||
if (c.Index > idx)
|
||||
--c.Index;
|
||||
}
|
||||
}
|
||||
|
||||
Penumbra.Log.Debug( $"Removed collection {collection.AnonymizedName}." );
|
||||
CollectionChanged.Invoke( CollectionType.Inactive, collection, null );
|
||||
Penumbra.Log.Debug($"Removed collection {collection.AnonymizedName}.");
|
||||
_communicator.CollectionChange.Invoke(CollectionType.Inactive, collection, null, string.Empty);
|
||||
return true;
|
||||
}
|
||||
|
||||
public bool RemoveCollection( ModCollection collection )
|
||||
=> RemoveCollection( collection.Index );
|
||||
public bool RemoveCollection(ModCollection collection)
|
||||
=> RemoveCollection(collection.Index);
|
||||
|
||||
private void OnModDiscoveryStarted()
|
||||
{
|
||||
foreach( var collection in this )
|
||||
{
|
||||
foreach (var collection in this)
|
||||
collection.PrepareModDiscovery();
|
||||
}
|
||||
}
|
||||
|
||||
private void OnModDiscoveryFinished()
|
||||
{
|
||||
// First, re-apply all mod settings.
|
||||
foreach( var collection in this )
|
||||
{
|
||||
foreach (var collection in this)
|
||||
collection.ApplyModSettings();
|
||||
}
|
||||
|
||||
// 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();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// A changed mod path forces changes for all collections, active and inactive.
|
||||
private void OnModPathChange( ModPathChangeType type, Mod mod, DirectoryInfo? oldDirectory,
|
||||
DirectoryInfo? newDirectory )
|
||||
private void OnModPathChange(ModPathChangeType type, Mod mod, DirectoryInfo? oldDirectory,
|
||||
DirectoryInfo? newDirectory)
|
||||
{
|
||||
switch( type )
|
||||
switch (type)
|
||||
{
|
||||
case ModPathChangeType.Added:
|
||||
foreach( var collection in this )
|
||||
{
|
||||
collection.AddMod( mod );
|
||||
}
|
||||
foreach (var collection in this)
|
||||
collection.AddMod(mod);
|
||||
|
||||
OnModAddedActive( mod );
|
||||
OnModAddedActive(mod);
|
||||
break;
|
||||
case ModPathChangeType.Deleted:
|
||||
OnModRemovedActive( mod );
|
||||
foreach( var collection in this )
|
||||
{
|
||||
collection.RemoveMod( mod, mod.Index );
|
||||
}
|
||||
OnModRemovedActive(mod);
|
||||
foreach (var collection in this)
|
||||
collection.RemoveMod(mod, mod.Index);
|
||||
|
||||
break;
|
||||
case ModPathChangeType.Moved:
|
||||
OnModMovedActive( mod );
|
||||
foreach( var collection in this.Where( collection => collection.Settings[ mod.Index ] != null ) )
|
||||
{
|
||||
OnModMovedActive(mod);
|
||||
foreach (var collection in this.Where(collection => collection.Settings[mod.Index] != null))
|
||||
collection.Save();
|
||||
}
|
||||
|
||||
break;
|
||||
case ModPathChangeType.StartingReload:
|
||||
OnModRemovedActive( mod );
|
||||
OnModRemovedActive(mod);
|
||||
break;
|
||||
case ModPathChangeType.Reloaded:
|
||||
OnModAddedActive( mod );
|
||||
OnModAddedActive(mod);
|
||||
break;
|
||||
default: throw new ArgumentOutOfRangeException( nameof( type ), type, null );
|
||||
default: throw new ArgumentOutOfRangeException(nameof(type), type, null);
|
||||
}
|
||||
}
|
||||
|
||||
// Automatically update all relevant collections when a mod is changed.
|
||||
// This means saving if options change in a way where the settings may change and the collection has settings for this mod.
|
||||
// And also updating effective file and meta manipulation lists if necessary.
|
||||
private void OnModOptionsChanged( ModOptionChangeType type, Mod mod, int groupIdx, int optionIdx, int movedToIdx )
|
||||
private void OnModOptionsChanged(ModOptionChangeType type, Mod mod, int groupIdx, int optionIdx, int movedToIdx)
|
||||
{
|
||||
// Handle changes that break revertability.
|
||||
if( type == ModOptionChangeType.PrepareChange )
|
||||
if (type == ModOptionChangeType.PrepareChange)
|
||||
{
|
||||
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 } )
|
||||
{
|
||||
collection._cache!.RemoveMod( mod, false );
|
||||
}
|
||||
if (collection[mod.Index].Settings is { Enabled: true })
|
||||
collection._cache!.RemoveMod(mod, false);
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
type.HandlingInfo( out var requiresSaving, out var recomputeList, out var reload );
|
||||
type.HandlingInfo(out var requiresSaving, out var recomputeList, out var reload);
|
||||
|
||||
// Handle changes that require overwriting the collection.
|
||||
if( requiresSaving )
|
||||
{
|
||||
foreach( var collection in this )
|
||||
if (requiresSaving)
|
||||
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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Handle changes that reload the mod if the changes did not need to be prepared,
|
||||
// or re-add the mod if they were prepared.
|
||||
if( recomputeList )
|
||||
{
|
||||
foreach( var collection in this.Where( c => c.HasCache ) )
|
||||
if (recomputeList)
|
||||
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 )
|
||||
{
|
||||
collection._cache!.ReloadMod( mod, true );
|
||||
}
|
||||
if (reload)
|
||||
collection._cache!.ReloadMod(mod, true);
|
||||
else
|
||||
{
|
||||
collection._cache!.AddMod( mod, true );
|
||||
}
|
||||
collection._cache!.AddMod(mod, true);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Add the collection with the default name if it does not exist.
|
||||
|
|
@ -320,44 +287,42 @@ public partial class ModCollection
|
|||
// This can also not be deleted, so there are always at least the empty and a collection with default name.
|
||||
private void AddDefaultCollection()
|
||||
{
|
||||
var idx = GetIndexForCollectionName( DefaultCollection );
|
||||
if( idx >= 0 )
|
||||
var idx = GetIndexForCollectionName(DefaultCollection);
|
||||
if (idx >= 0)
|
||||
{
|
||||
DefaultName = this[ idx ];
|
||||
DefaultName = this[idx];
|
||||
return;
|
||||
}
|
||||
|
||||
var defaultCollection = CreateNewEmpty( DefaultCollection );
|
||||
var defaultCollection = CreateNewEmpty(DefaultCollection);
|
||||
defaultCollection.Save();
|
||||
defaultCollection.Index = _collections.Count;
|
||||
_collections.Add( defaultCollection );
|
||||
_collections.Add(defaultCollection);
|
||||
}
|
||||
|
||||
// Inheritances can not be setup before all collections are read,
|
||||
// so this happens after reading the collections.
|
||||
private void ApplyInheritances( IEnumerable< IReadOnlyList< string > > inheritances )
|
||||
private void ApplyInheritances(IEnumerable<IReadOnlyList<string>> inheritances)
|
||||
{
|
||||
foreach( var (collection, inheritance) in this.Zip( inheritances ) )
|
||||
foreach (var (collection, inheritance) in this.Zip(inheritances))
|
||||
{
|
||||
var changes = false;
|
||||
foreach( var subCollectionName in inheritance )
|
||||
foreach (var subCollectionName in inheritance)
|
||||
{
|
||||
if( !ByName( subCollectionName, out var subCollection ) )
|
||||
if (!ByName(subCollectionName, out var subCollection))
|
||||
{
|
||||
changes = true;
|
||||
Penumbra.Log.Warning( $"Inherited collection {subCollectionName} for {collection.Name} does not exist, removed." );
|
||||
Penumbra.Log.Warning($"Inherited collection {subCollectionName} for {collection.Name} does not exist, removed.");
|
||||
}
|
||||
else if( !collection.AddInheritance( subCollection, false ) )
|
||||
else if (!collection.AddInheritance(subCollection, false))
|
||||
{
|
||||
changes = true;
|
||||
Penumbra.Log.Warning( $"{collection.Name} can not inherit from {subCollectionName}, removed." );
|
||||
Penumbra.Log.Warning($"{collection.Name} can not inherit from {subCollectionName}, removed.");
|
||||
}
|
||||
}
|
||||
|
||||
if( changes )
|
||||
{
|
||||
if (changes)
|
||||
collection.Save();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -366,38 +331,33 @@ public partial class ModCollection
|
|||
// Duplicate collection files are not deleted, just not added here.
|
||||
private void ReadCollections()
|
||||
{
|
||||
var collectionDir = new DirectoryInfo( CollectionDirectory );
|
||||
var inheritances = new List< IReadOnlyList< string > >();
|
||||
if( collectionDir.Exists )
|
||||
{
|
||||
foreach( var file in collectionDir.EnumerateFiles( "*.json" ) )
|
||||
// TODO
|
||||
var collectionDir = new DirectoryInfo(CollectionDirectory(DalamudServices.PluginInterface));
|
||||
var inheritances = new List<IReadOnlyList<string>>();
|
||||
if (collectionDir.Exists)
|
||||
foreach (var file in collectionDir.EnumerateFiles("*.json"))
|
||||
{
|
||||
var collection = LoadFromFile( file, out var inheritance );
|
||||
if( collection == null || collection.Name.Length == 0 )
|
||||
{
|
||||
var collection = LoadFromFile(file, out var inheritance);
|
||||
if (collection == null || collection.Name.Length == 0)
|
||||
continue;
|
||||
}
|
||||
|
||||
if( file.Name != $"{collection.Name.RemoveInvalidPathSymbols()}.json" )
|
||||
{
|
||||
Penumbra.Log.Warning( $"Collection {file.Name} does not correspond to {collection.Name}." );
|
||||
}
|
||||
if (file.Name != $"{collection.Name.RemoveInvalidPathSymbols()}.json")
|
||||
Penumbra.Log.Warning($"Collection {file.Name} does not correspond to {collection.Name}.");
|
||||
|
||||
if( this[ collection.Name ] != null )
|
||||
if (this[collection.Name] != null)
|
||||
{
|
||||
Penumbra.Log.Warning( $"Duplicate collection found: {collection.Name} already exists." );
|
||||
Penumbra.Log.Warning($"Duplicate collection found: {collection.Name} already exists.");
|
||||
}
|
||||
else
|
||||
{
|
||||
inheritances.Add( inheritance );
|
||||
inheritances.Add(inheritance);
|
||||
collection.Index = _collections.Count;
|
||||
_collections.Add( collection );
|
||||
_collections.Add(collection);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
AddDefaultCollection();
|
||||
ApplyInheritances( inheritances );
|
||||
ApplyInheritances(inheritances);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ using System.Linq;
|
|||
using Dalamud.Game.ClientState.Objects.Enums;
|
||||
using OtterGui.Filesystem;
|
||||
using Penumbra.GameData.Actors;
|
||||
using Penumbra.Services;
|
||||
using Penumbra.String;
|
||||
|
||||
namespace Penumbra.Collections;
|
||||
|
|
@ -19,8 +20,12 @@ public sealed partial class IndividualCollections
|
|||
|
||||
public IReadOnlyDictionary< ActorIdentifier, ModCollection > Individuals
|
||||
=> _individuals;
|
||||
|
||||
// TODO
|
||||
public IndividualCollections( ActorService actorManager )
|
||||
=> _actorManager = actorManager.AwaitedService;
|
||||
|
||||
public IndividualCollections( ActorManager actorManager )
|
||||
public IndividualCollections(ActorManager actorManager)
|
||||
=> _actorManager = actorManager;
|
||||
|
||||
public enum AddResult
|
||||
|
|
|
|||
|
|
@ -2,24 +2,26 @@ using Newtonsoft.Json;
|
|||
using Newtonsoft.Json.Linq;
|
||||
using OtterGui.Filesystem;
|
||||
using Penumbra.Mods;
|
||||
using Penumbra.Services;
|
||||
using Penumbra.Services;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using Dalamud.Plugin;
|
||||
|
||||
namespace Penumbra.Collections;
|
||||
|
||||
// File operations like saving, loading and deleting for a collection.
|
||||
public partial class ModCollection
|
||||
{
|
||||
public static string CollectionDirectory
|
||||
=> Path.Combine( DalamudServices.PluginInterface.GetPluginConfigDirectory(), "collections" );
|
||||
public static string CollectionDirectory(DalamudPluginInterface pi)
|
||||
=> Path.Combine( pi.GetPluginConfigDirectory(), "collections" );
|
||||
|
||||
// We need to remove all invalid path symbols from the collection name to be able to save it to file.
|
||||
// We need to remove all invalid path symbols from the collection name to be able to save it to file.
|
||||
// TODO
|
||||
public FileInfo FileName
|
||||
=> new(Path.Combine( CollectionDirectory, $"{Name.RemoveInvalidPathSymbols()}.json" ));
|
||||
=> new(Path.Combine( CollectionDirectory(DalamudServices.PluginInterface), $"{Name.RemoveInvalidPathSymbols()}.json" ));
|
||||
|
||||
// Custom serialization due to shared mod information across managers.
|
||||
private void SaveCollection()
|
||||
|
|
|
|||
|
|
@ -90,7 +90,7 @@ public partial class ModCollection
|
|||
var collection = new ModCollection( name, Empty );
|
||||
collection.ModSettingChanged -= collection.SaveOnChange;
|
||||
collection.InheritanceChanged -= collection.SaveOnChange;
|
||||
collection.Index = ~Penumbra.TempMods.Collections.Count;
|
||||
collection.Index = ~Penumbra.TempCollections.Count;
|
||||
collection.ChangeCounter = changeCounter;
|
||||
collection.CreateCache();
|
||||
return collection;
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -19,8 +19,14 @@ using ErrorEventArgs = Newtonsoft.Json.Serialization.ErrorEventArgs;
|
|||
namespace Penumbra;
|
||||
|
||||
[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 LastSeenVersion { get; set; } = ConfigWindow.LastChangelogVersion;
|
||||
|
|
@ -86,47 +92,44 @@ public partial class Configuration : IPluginConfiguration
|
|||
public Dictionary< ColorId, uint > Colors { get; set; }
|
||||
= Enum.GetValues< ColorId >().ToDictionary( c => c, c => c.Data().DefaultColor );
|
||||
|
||||
// Load the current configuration.
|
||||
// Includes adding new colors and migrating from old versions.
|
||||
public static Configuration Load()
|
||||
/// <summary>
|
||||
/// Load the current configuration.
|
||||
/// 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(
|
||||
$"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;
|
||||
}
|
||||
|
||||
Configuration? configuration = null;
|
||||
if( File.Exists( DalamudServices.PluginInterface.ConfigFile.FullName ) )
|
||||
if (File.Exists(_fileName))
|
||||
{
|
||||
var text = File.ReadAllText( DalamudServices.PluginInterface.ConfigFile.FullName );
|
||||
configuration = JsonConvert.DeserializeObject< Configuration >( text, new JsonSerializerSettings
|
||||
var text = File.ReadAllText(_fileName);
|
||||
JsonConvert.PopulateObject(text, this, new JsonSerializerSettings
|
||||
{
|
||||
Error = HandleDeserializationError,
|
||||
} );
|
||||
});
|
||||
}
|
||||
|
||||
configuration ??= new Configuration();
|
||||
if( configuration.Version == Constants.CurrentVersion )
|
||||
{
|
||||
configuration.AddColors( false );
|
||||
return configuration;
|
||||
}
|
||||
|
||||
Migration.Migrate( configuration );
|
||||
configuration.AddColors( true );
|
||||
|
||||
return configuration;
|
||||
migrator.Migrate(this);
|
||||
}
|
||||
|
||||
// Save the current configuration.
|
||||
/// <summary> Save the current configuration. </summary>
|
||||
private void SaveConfiguration()
|
||||
{
|
||||
try
|
||||
{
|
||||
var text = JsonConvert.SerializeObject( this, Formatting.Indented );
|
||||
File.WriteAllText( DalamudServices.PluginInterface.ConfigFile.FullName, text );
|
||||
File.WriteAllText( _fileName, text );
|
||||
}
|
||||
catch( Exception e )
|
||||
{
|
||||
|
|
@ -135,24 +138,9 @@ public partial class Configuration : IPluginConfiguration
|
|||
}
|
||||
|
||||
public void Save()
|
||||
=> Penumbra.Framework.RegisterDelayed( nameof( SaveConfiguration ), SaveConfiguration );
|
||||
=> _framework.RegisterDelayed( nameof( SaveConfiguration ), SaveConfiguration );
|
||||
|
||||
// Add missing colors to the dictionary if necessary.
|
||||
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.
|
||||
/// <summary> Contains some default values or boundaries for config values. </summary>
|
||||
public static class Constants
|
||||
{
|
||||
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 > >
|
||||
{
|
||||
public override void WriteJson( JsonWriter writer, ISortMode< Mod >? value, JsonSerializer serializer )
|
||||
|
|
|
|||
|
|
@ -1,9 +1,9 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using Dalamud.Game;
|
||||
using Dalamud.Utility.Signatures;
|
||||
using Penumbra.GameData;
|
||||
using Penumbra.Services;
|
||||
|
||||
namespace Penumbra.Interop;
|
||||
|
||||
|
|
@ -52,14 +52,17 @@ public unsafe partial class CharacterUtility : IDisposable
|
|||
public (IntPtr Address, int Size) DefaultResource( InternalIndex idx )
|
||||
=> _lists[ idx.Value ].DefaultResource;
|
||||
|
||||
public CharacterUtility()
|
||||
private readonly Framework _framework;
|
||||
|
||||
public CharacterUtility(Framework framework)
|
||||
{
|
||||
SignatureHelper.Initialise( this );
|
||||
_framework = framework;
|
||||
LoadingFinished += () => Penumbra.Log.Debug( "Loading of CharacterUtility finished." );
|
||||
LoadDefaultResources( null! );
|
||||
if( !Ready )
|
||||
{
|
||||
DalamudServices.Framework.Update += LoadDefaultResources;
|
||||
_framework.Update += LoadDefaultResources;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -99,7 +102,7 @@ public unsafe partial class CharacterUtility : IDisposable
|
|||
if( !anyMissing )
|
||||
{
|
||||
Ready = true;
|
||||
DalamudServices.Framework.Update -= LoadDefaultResources;
|
||||
_framework.Update -= LoadDefaultResources;
|
||||
LoadingFinished.Invoke();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -10,7 +10,6 @@ using FFXIVClientStructs.STD;
|
|||
using Penumbra.Collections;
|
||||
using Penumbra.GameData;
|
||||
using Penumbra.GameData.Enums;
|
||||
using Penumbra.String;
|
||||
using Penumbra.String.Classes;
|
||||
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." );
|
||||
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}." );
|
||||
}
|
||||
|
|
@ -18,39 +18,6 @@ public unsafe partial class ResourceLoader : IDisposable
|
|||
// Hooks are required for everything, even events firing.
|
||||
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()
|
||||
{
|
||||
if( DoReplacements )
|
||||
|
|
@ -150,7 +117,6 @@ public unsafe partial class ResourceLoader : IDisposable
|
|||
|
||||
public void Dispose()
|
||||
{
|
||||
DisableFullLogging();
|
||||
DisposeHooks();
|
||||
DisposeTexMdlTreatment();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
@ -2,9 +2,9 @@ using System;
|
|||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.Linq;
|
||||
using Dalamud.Game.ClientState.Objects;
|
||||
using FFXIVClientStructs.FFXIV.Client.Game.Character;
|
||||
using Penumbra.Services;
|
||||
|
||||
|
||||
namespace Penumbra.Interop.Resolver;
|
||||
|
||||
public class CutsceneCharacters : IDisposable
|
||||
|
|
@ -14,39 +14,39 @@ public class CutsceneCharacters : IDisposable
|
|||
public const int CutsceneEndIdx = CutsceneStartIdx + CutsceneSlots;
|
||||
|
||||
private readonly GameEventManager _events;
|
||||
private readonly short[] _copiedCharacters = Enumerable.Repeat( ( short )-1, CutsceneSlots ).ToArray();
|
||||
private readonly ObjectTable _objects;
|
||||
private readonly short[] _copiedCharacters = Enumerable.Repeat((short)-1, CutsceneSlots).ToArray();
|
||||
|
||||
public IEnumerable< KeyValuePair< int, global::Dalamud.Game.ClientState.Objects.Types.GameObject > > Actors
|
||||
=> Enumerable.Range( CutsceneStartIdx, CutsceneSlots )
|
||||
.Where( i => DalamudServices.Objects[ i ] != null )
|
||||
.Select( i => KeyValuePair.Create( i, this[ i ] ?? DalamudServices.Objects[ i ]! ) );
|
||||
public IEnumerable<KeyValuePair<int, Dalamud.Game.ClientState.Objects.Types.GameObject>> Actors
|
||||
=> Enumerable.Range(CutsceneStartIdx, CutsceneSlots)
|
||||
.Where(i => _objects[i] != null)
|
||||
.Select(i => KeyValuePair.Create(i, this[i] ?? _objects[i]!));
|
||||
|
||||
public CutsceneCharacters(GameEventManager events)
|
||||
public CutsceneCharacters(ObjectTable objects, GameEventManager events)
|
||||
{
|
||||
_events = events;
|
||||
_objects = objects;
|
||||
_events = events;
|
||||
Enable();
|
||||
}
|
||||
|
||||
// Get the related actor to a cutscene actor.
|
||||
// Does not check for valid input index.
|
||||
// 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
|
||||
{
|
||||
Debug.Assert( idx is >= CutsceneStartIdx and < CutsceneEndIdx );
|
||||
idx = _copiedCharacters[ idx - CutsceneStartIdx ];
|
||||
return idx < 0 ? null : DalamudServices.Objects[ idx ];
|
||||
Debug.Assert(idx is >= CutsceneStartIdx and < CutsceneEndIdx);
|
||||
idx = _copiedCharacters[idx - CutsceneStartIdx];
|
||||
return idx < 0 ? null : _objects[idx];
|
||||
}
|
||||
}
|
||||
|
||||
// Return the currently set index of a parent or -1 if none is set or the index is invalid.
|
||||
public int GetParentIndex( int idx )
|
||||
public int GetParentIndex(int idx)
|
||||
{
|
||||
if( idx is >= CutsceneStartIdx and < CutsceneEndIdx )
|
||||
{
|
||||
return _copiedCharacters[ idx - CutsceneStartIdx ];
|
||||
}
|
||||
if (idx is >= CutsceneStartIdx and < CutsceneEndIdx)
|
||||
return _copiedCharacters[idx - CutsceneStartIdx];
|
||||
|
||||
return -1;
|
||||
}
|
||||
|
|
@ -66,21 +66,21 @@ public class CutsceneCharacters : IDisposable
|
|||
public void Dispose()
|
||||
=> Disable();
|
||||
|
||||
private unsafe void OnCharacterDestructor( Character* character )
|
||||
private unsafe void OnCharacterDestructor(Character* character)
|
||||
{
|
||||
if( character->GameObject.ObjectIndex is >= CutsceneStartIdx and < CutsceneEndIdx )
|
||||
if (character->GameObject.ObjectIndex is >= CutsceneStartIdx and < CutsceneEndIdx)
|
||||
{
|
||||
var idx = character->GameObject.ObjectIndex - CutsceneStartIdx;
|
||||
_copiedCharacters[ idx ] = -1;
|
||||
_copiedCharacters[idx] = -1;
|
||||
}
|
||||
}
|
||||
|
||||
private unsafe void OnCharacterCopy( Character* target, Character* source )
|
||||
private unsafe void OnCharacterCopy(Character* target, Character* source)
|
||||
{
|
||||
if( target != null && target->GameObject.ObjectIndex is >= CutsceneStartIdx and < CutsceneEndIdx )
|
||||
if (target != null && target->GameObject.ObjectIndex is >= CutsceneStartIdx and < CutsceneEndIdx)
|
||||
{
|
||||
var idx = target->GameObject.ObjectIndex - CutsceneStartIdx;
|
||||
_copiedCharacters[idx] = (short) (source != null ? source->GameObject.ObjectIndex : -1);
|
||||
_copiedCharacters[idx] = (short)(source != null ? source->GameObject.ObjectIndex : -1);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,72 +5,68 @@ using FFXIVClientStructs.FFXIV.Client.Game.Character;
|
|||
using FFXIVClientStructs.FFXIV.Client.Game.Object;
|
||||
using Penumbra.Collections;
|
||||
using Penumbra.GameData.Actors;
|
||||
using Penumbra.Services;
|
||||
|
||||
using Penumbra.Services;
|
||||
|
||||
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 GameEventManager _events;
|
||||
private readonly Dictionary< IntPtr, (ActorIdentifier, ModCollection) > _cache = new(317);
|
||||
private bool _dirty = false;
|
||||
private bool _enabled = false;
|
||||
private readonly CommunicatorService _communicator;
|
||||
private readonly GameEventManager _events;
|
||||
private readonly Dictionary<IntPtr, (ActorIdentifier, ModCollection)> _cache = new(317);
|
||||
private bool _dirty = false;
|
||||
private bool _enabled = false;
|
||||
|
||||
public IdentifiedCollectionCache(GameEventManager events)
|
||||
public IdentifiedCollectionCache(CommunicatorService communicator, GameEventManager events)
|
||||
{
|
||||
_events = events;
|
||||
_communicator = communicator;
|
||||
_events = events;
|
||||
}
|
||||
|
||||
public void Enable()
|
||||
{
|
||||
if( _enabled )
|
||||
{
|
||||
if (_enabled)
|
||||
return;
|
||||
}
|
||||
|
||||
Penumbra.CollectionManager.CollectionChanged += CollectionChangeClear;
|
||||
Penumbra.TempMods.CollectionChanged += CollectionChangeClear;
|
||||
DalamudServices.ClientState.TerritoryChanged += TerritoryClear;
|
||||
_communicator.CollectionChange.Event += CollectionChangeClear;
|
||||
DalamudServices.ClientState.TerritoryChanged += TerritoryClear;
|
||||
_events.CharacterDestructor += OnCharacterDestruct;
|
||||
_enabled = true;
|
||||
}
|
||||
|
||||
public void Disable()
|
||||
{
|
||||
if( !_enabled )
|
||||
{
|
||||
if (!_enabled)
|
||||
return;
|
||||
}
|
||||
|
||||
Penumbra.CollectionManager.CollectionChanged -= CollectionChangeClear;
|
||||
Penumbra.TempMods.CollectionChanged -= CollectionChangeClear;
|
||||
DalamudServices.ClientState.TerritoryChanged -= TerritoryClear;
|
||||
_communicator.CollectionChange.Event -= CollectionChangeClear;
|
||||
DalamudServices.ClientState.TerritoryChanged -= TerritoryClear;
|
||||
_events.CharacterDestructor -= OnCharacterDestruct;
|
||||
_enabled = false;
|
||||
}
|
||||
|
||||
public ResolveData Set( ModCollection collection, ActorIdentifier identifier, GameObject* data )
|
||||
public ResolveData Set(ModCollection collection, ActorIdentifier identifier, GameObject* data)
|
||||
{
|
||||
if( _dirty )
|
||||
if (_dirty)
|
||||
{
|
||||
_dirty = false;
|
||||
_cache.Clear();
|
||||
}
|
||||
|
||||
_cache[ ( IntPtr )data ] = ( identifier, collection );
|
||||
return collection.ToResolveData( data );
|
||||
_cache[(IntPtr)data] = (identifier, collection);
|
||||
return collection.ToResolveData(data);
|
||||
}
|
||||
|
||||
public bool TryGetValue( GameObject* gameObject, out ResolveData resolve )
|
||||
public bool TryGetValue(GameObject* gameObject, out ResolveData resolve)
|
||||
{
|
||||
if( _dirty )
|
||||
if (_dirty)
|
||||
{
|
||||
_dirty = false;
|
||||
_cache.Clear();
|
||||
}
|
||||
else if( _cache.TryGetValue( ( IntPtr )gameObject, out var p ) )
|
||||
else if (_cache.TryGetValue((IntPtr)gameObject, out var p))
|
||||
{
|
||||
resolve = p.Item2.ToResolveData( gameObject );
|
||||
resolve = p.Item2.ToResolveData(gameObject);
|
||||
return true;
|
||||
}
|
||||
|
||||
|
|
@ -81,19 +77,17 @@ public unsafe class IdentifiedCollectionCache : IDisposable, IEnumerable< (IntPt
|
|||
public void Dispose()
|
||||
{
|
||||
Disable();
|
||||
GC.SuppressFinalize( this );
|
||||
GC.SuppressFinalize(this);
|
||||
}
|
||||
|
||||
public IEnumerator< (IntPtr Address, ActorIdentifier Identifier, ModCollection Collection) > GetEnumerator()
|
||||
public IEnumerator<(IntPtr Address, ActorIdentifier Identifier, ModCollection Collection)> GetEnumerator()
|
||||
{
|
||||
foreach( var (address, (identifier, collection)) in _cache )
|
||||
foreach (var (address, (identifier, collection)) in _cache)
|
||||
{
|
||||
if( _dirty )
|
||||
{
|
||||
if (_dirty)
|
||||
yield break;
|
||||
}
|
||||
|
||||
yield return ( address, identifier, collection );
|
||||
yield return (address, identifier, collection);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -103,17 +97,15 @@ public unsafe class IdentifiedCollectionCache : IDisposable, IEnumerable< (IntPt
|
|||
IEnumerator IEnumerable.GetEnumerator()
|
||||
=> GetEnumerator();
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
private void TerritoryClear( object? _1, ushort _2 )
|
||||
private void TerritoryClear(object? _1, ushort _2)
|
||||
=> _dirty = _cache.Count > 0;
|
||||
|
||||
private void OnCharacterDestruct( Character* character )
|
||||
=> _cache.Remove( ( IntPtr )character );
|
||||
}
|
||||
private void OnCharacterDestruct(Character* character)
|
||||
=> _cache.Remove((IntPtr)character);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -12,43 +12,42 @@ using Penumbra.GameData;
|
|||
using Penumbra.GameData.Enums;
|
||||
using Penumbra.String.Classes;
|
||||
using Penumbra.Util;
|
||||
using Penumbra.Services;
|
||||
|
||||
using Penumbra.Services;
|
||||
|
||||
namespace Penumbra.Interop.Resolver;
|
||||
|
||||
public unsafe partial class PathResolver
|
||||
{
|
||||
public class DrawObjectState
|
||||
{
|
||||
private readonly CommunicatorService _communicator;
|
||||
public static event CreatingCharacterBaseDelegate? CreatingCharacterBase;
|
||||
public static event CreatedCharacterBaseDelegate? CreatedCharacterBase;
|
||||
public static event CreatedCharacterBaseDelegate? CreatedCharacterBase;
|
||||
|
||||
public IEnumerable< KeyValuePair< IntPtr, (ResolveData, int) > > DrawObjects
|
||||
public IEnumerable<KeyValuePair<IntPtr, (ResolveData, int)>> DrawObjects
|
||||
=> _drawObjectToObject;
|
||||
|
||||
public int Count
|
||||
=> _drawObjectToObject.Count;
|
||||
|
||||
public bool TryGetValue( IntPtr drawObject, out (ResolveData, int) value, out GameObject* gameObject )
|
||||
public bool TryGetValue(IntPtr drawObject, out (ResolveData, int) value, out GameObject* gameObject)
|
||||
{
|
||||
gameObject = null;
|
||||
if( !_drawObjectToObject.TryGetValue( drawObject, out value ) )
|
||||
{
|
||||
if (!_drawObjectToObject.TryGetValue(drawObject, out value))
|
||||
return false;
|
||||
}
|
||||
|
||||
var gameObjectIdx = value.Item2;
|
||||
return VerifyEntry( drawObject, gameObjectIdx, out gameObject );
|
||||
return VerifyEntry(drawObject, gameObjectIdx, out gameObject);
|
||||
}
|
||||
|
||||
|
||||
// Set and update a parent object if it exists and a last game object is set.
|
||||
public ResolveData CheckParentDrawObject( IntPtr drawObject, IntPtr parentObject )
|
||||
public ResolveData CheckParentDrawObject(IntPtr drawObject, IntPtr parentObject)
|
||||
{
|
||||
if( parentObject == IntPtr.Zero && LastGameObject != null )
|
||||
if (parentObject == IntPtr.Zero && LastGameObject != null)
|
||||
{
|
||||
var collection = IdentifyCollection( LastGameObject, true );
|
||||
_drawObjectToObject[ drawObject ] = ( collection, LastGameObject->ObjectIndex );
|
||||
var collection = IdentifyCollection(LastGameObject, true);
|
||||
_drawObjectToObject[drawObject] = (collection, LastGameObject->ObjectIndex);
|
||||
return collection;
|
||||
}
|
||||
|
||||
|
|
@ -56,11 +55,11 @@ public unsafe partial class PathResolver
|
|||
}
|
||||
|
||||
|
||||
public bool HandleDecalFile( ResourceType type, Utf8GamePath gamePath, out ResolveData resolveData )
|
||||
public bool HandleDecalFile(ResourceType type, Utf8GamePath gamePath, out ResolveData resolveData)
|
||||
{
|
||||
if( type == ResourceType.Tex
|
||||
&& LastCreatedCollection.Valid
|
||||
&& gamePath.Path.Substring( "chara/common/texture/".Length ).StartsWith( "decal"u8 ) )
|
||||
if (type == ResourceType.Tex
|
||||
&& LastCreatedCollection.Valid
|
||||
&& gamePath.Path.Substring("chara/common/texture/".Length).StartsWith("decal"u8))
|
||||
{
|
||||
resolveData = LastCreatedCollection;
|
||||
return true;
|
||||
|
|
@ -76,9 +75,10 @@ public unsafe partial class PathResolver
|
|||
|
||||
public GameObject* LastGameObject { get; private set; }
|
||||
|
||||
public DrawObjectState()
|
||||
public DrawObjectState(CommunicatorService communicator)
|
||||
{
|
||||
SignatureHelper.Initialise( this );
|
||||
SignatureHelper.Initialise(this);
|
||||
_communicator = communicator;
|
||||
}
|
||||
|
||||
public void Enable()
|
||||
|
|
@ -88,8 +88,7 @@ public unsafe partial class PathResolver
|
|||
_enableDrawHook.Enable();
|
||||
_weaponReloadHook.Enable();
|
||||
InitializeDrawObjects();
|
||||
Penumbra.CollectionManager.CollectionChanged += CheckCollections;
|
||||
Penumbra.TempMods.CollectionChanged += CheckCollections;
|
||||
_communicator.CollectionChange.Event += CheckCollections;
|
||||
}
|
||||
|
||||
public void Disable()
|
||||
|
|
@ -98,8 +97,7 @@ public unsafe partial class PathResolver
|
|||
_characterBaseDestructorHook.Disable();
|
||||
_enableDrawHook.Disable();
|
||||
_weaponReloadHook.Disable();
|
||||
Penumbra.CollectionManager.CollectionChanged -= CheckCollections;
|
||||
Penumbra.TempMods.CollectionChanged -= CheckCollections;
|
||||
_communicator.CollectionChange.Event -= CheckCollections;
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
|
|
@ -112,63 +110,61 @@ public unsafe partial class PathResolver
|
|||
}
|
||||
|
||||
// Check that a linked DrawObject still corresponds to the correct actor and that it still exists, otherwise remove it.
|
||||
private bool VerifyEntry( IntPtr drawObject, int gameObjectIdx, out GameObject* gameObject )
|
||||
private bool VerifyEntry(IntPtr drawObject, int gameObjectIdx, out GameObject* gameObject)
|
||||
{
|
||||
gameObject = ( GameObject* )DalamudServices.Objects.GetObjectAddress( gameObjectIdx );
|
||||
var draw = ( DrawObject* )drawObject;
|
||||
if( gameObject != null
|
||||
&& ( gameObject->DrawObject == draw || draw != null && gameObject->DrawObject == draw->Object.ParentObject ) )
|
||||
{
|
||||
gameObject = (GameObject*)DalamudServices.Objects.GetObjectAddress(gameObjectIdx);
|
||||
var draw = (DrawObject*)drawObject;
|
||||
if (gameObject != null
|
||||
&& (gameObject->DrawObject == draw || draw != null && gameObject->DrawObject == draw->Object.ParentObject))
|
||||
return true;
|
||||
}
|
||||
|
||||
gameObject = null;
|
||||
_drawObjectToObject.Remove( drawObject );
|
||||
_drawObjectToObject.Remove(drawObject);
|
||||
return false;
|
||||
}
|
||||
|
||||
// This map links DrawObjects directly to Actors (by ObjectTable index) and their collections.
|
||||
// It contains any DrawObjects that correspond to a human actor, even those without specific collections.
|
||||
private readonly Dictionary< IntPtr, (ResolveData, int) > _drawObjectToObject = new();
|
||||
private ResolveData _lastCreatedCollection = ResolveData.Invalid;
|
||||
private readonly Dictionary<IntPtr, (ResolveData, int)> _drawObjectToObject = new();
|
||||
private ResolveData _lastCreatedCollection = ResolveData.Invalid;
|
||||
|
||||
// Keep track of created DrawObjects that are CharacterBase,
|
||||
// and use the last game object that called EnableDraw to link them.
|
||||
private delegate IntPtr CharacterBaseCreateDelegate( uint a, IntPtr b, IntPtr c, byte d );
|
||||
private delegate IntPtr CharacterBaseCreateDelegate(uint a, IntPtr b, IntPtr c, byte d);
|
||||
|
||||
[Signature( Sigs.CharacterBaseCreate, DetourName = nameof( CharacterBaseCreateDetour ) )]
|
||||
private readonly Hook< CharacterBaseCreateDelegate > _characterBaseCreateHook = null!;
|
||||
[Signature(Sigs.CharacterBaseCreate, DetourName = nameof(CharacterBaseCreateDetour))]
|
||||
private readonly Hook<CharacterBaseCreateDelegate> _characterBaseCreateHook = null!;
|
||||
|
||||
private IntPtr CharacterBaseCreateDetour( uint a, IntPtr b, IntPtr c, byte d )
|
||||
private IntPtr CharacterBaseCreateDetour(uint a, IntPtr b, IntPtr c, byte d)
|
||||
{
|
||||
using var performance = Penumbra.Performance.Measure( PerformanceType.CharacterBaseCreate );
|
||||
using var performance = Penumbra.Performance.Measure(PerformanceType.CharacterBaseCreate);
|
||||
|
||||
var meta = DisposableContainer.Empty;
|
||||
if( LastGameObject != null )
|
||||
if (LastGameObject != null)
|
||||
{
|
||||
_lastCreatedCollection = IdentifyCollection( LastGameObject, false );
|
||||
_lastCreatedCollection = IdentifyCollection(LastGameObject, false);
|
||||
// Change the transparent or 1.0 Decal if necessary.
|
||||
var decal = new CharacterUtility.DecalReverter( _lastCreatedCollection.ModCollection, UsesDecal( a, c ) );
|
||||
var decal = new CharacterUtility.DecalReverter(_lastCreatedCollection.ModCollection, UsesDecal(a, c));
|
||||
// Change the rsp parameters.
|
||||
meta = new DisposableContainer( _lastCreatedCollection.ModCollection.TemporarilySetCmpFile(), decal );
|
||||
meta = new DisposableContainer(_lastCreatedCollection.ModCollection.TemporarilySetCmpFile(), decal);
|
||||
try
|
||||
{
|
||||
var modelPtr = &a;
|
||||
CreatingCharacterBase?.Invoke( ( IntPtr )LastGameObject, _lastCreatedCollection!.ModCollection.Name, ( IntPtr )modelPtr, b, c );
|
||||
CreatingCharacterBase?.Invoke((IntPtr)LastGameObject, _lastCreatedCollection!.ModCollection.Name, (IntPtr)modelPtr, b, c);
|
||||
}
|
||||
catch( Exception e )
|
||||
catch (Exception e)
|
||||
{
|
||||
Penumbra.Log.Error( $"Unknown Error during CreatingCharacterBase:\n{e}" );
|
||||
Penumbra.Log.Error($"Unknown Error during CreatingCharacterBase:\n{e}");
|
||||
}
|
||||
}
|
||||
|
||||
var ret = _characterBaseCreateHook.Original( a, b, c, d );
|
||||
var ret = _characterBaseCreateHook.Original(a, b, c, d);
|
||||
try
|
||||
{
|
||||
if( LastGameObject != null && ret != IntPtr.Zero )
|
||||
if (LastGameObject != null && ret != IntPtr.Zero)
|
||||
{
|
||||
_drawObjectToObject[ ret ] = ( _lastCreatedCollection!, LastGameObject->ObjectIndex );
|
||||
CreatedCharacterBase?.Invoke( ( IntPtr )LastGameObject, _lastCreatedCollection!.ModCollection.Name, ret );
|
||||
_drawObjectToObject[ret] = (_lastCreatedCollection!, LastGameObject->ObjectIndex);
|
||||
CreatedCharacterBase?.Invoke((IntPtr)LastGameObject, _lastCreatedCollection!.ModCollection.Name, ret);
|
||||
}
|
||||
}
|
||||
finally
|
||||
|
|
@ -181,70 +177,66 @@ public unsafe partial class PathResolver
|
|||
|
||||
// Check the customize array for the FaceCustomization byte and the last bit of that.
|
||||
// Also check for humans.
|
||||
public static bool UsesDecal( uint modelId, IntPtr customizeData )
|
||||
=> modelId == 0 && ( ( byte* )customizeData )[ 12 ] > 0x7F;
|
||||
public static bool UsesDecal(uint modelId, IntPtr customizeData)
|
||||
=> modelId == 0 && ((byte*)customizeData)[12] > 0x7F;
|
||||
|
||||
|
||||
// Remove DrawObjects from the list when they are destroyed.
|
||||
private delegate void CharacterBaseDestructorDelegate( IntPtr drawBase );
|
||||
private delegate void CharacterBaseDestructorDelegate(IntPtr drawBase);
|
||||
|
||||
[Signature( Sigs.CharacterBaseDestructor, DetourName = nameof( CharacterBaseDestructorDetour ) )]
|
||||
private readonly Hook< CharacterBaseDestructorDelegate > _characterBaseDestructorHook = null!;
|
||||
[Signature(Sigs.CharacterBaseDestructor, DetourName = nameof(CharacterBaseDestructorDetour))]
|
||||
private readonly Hook<CharacterBaseDestructorDelegate> _characterBaseDestructorHook = null!;
|
||||
|
||||
private void CharacterBaseDestructorDetour( IntPtr drawBase )
|
||||
private void CharacterBaseDestructorDetour(IntPtr drawBase)
|
||||
{
|
||||
_drawObjectToObject.Remove( drawBase );
|
||||
_characterBaseDestructorHook!.Original.Invoke( drawBase );
|
||||
_drawObjectToObject.Remove(drawBase);
|
||||
_characterBaseDestructorHook!.Original.Invoke(drawBase);
|
||||
}
|
||||
|
||||
|
||||
// EnableDraw is what creates DrawObjects for gameObjects,
|
||||
// so we always keep track of the current GameObject to be able to link it to the DrawObject.
|
||||
private delegate void EnableDrawDelegate( IntPtr gameObject, IntPtr b, IntPtr c, IntPtr d );
|
||||
private delegate void EnableDrawDelegate(IntPtr gameObject, IntPtr b, IntPtr c, IntPtr d);
|
||||
|
||||
[Signature( Sigs.EnableDraw, DetourName = nameof( EnableDrawDetour ) )]
|
||||
private readonly Hook< EnableDrawDelegate > _enableDrawHook = null!;
|
||||
[Signature(Sigs.EnableDraw, DetourName = nameof(EnableDrawDetour))]
|
||||
private readonly Hook<EnableDrawDelegate> _enableDrawHook = null!;
|
||||
|
||||
private void EnableDrawDetour( IntPtr gameObject, IntPtr b, IntPtr c, IntPtr d )
|
||||
private void EnableDrawDetour(IntPtr gameObject, IntPtr b, IntPtr c, IntPtr d)
|
||||
{
|
||||
var oldObject = LastGameObject;
|
||||
LastGameObject = ( GameObject* )gameObject;
|
||||
_enableDrawHook!.Original.Invoke( gameObject, b, c, d );
|
||||
LastGameObject = (GameObject*)gameObject;
|
||||
_enableDrawHook!.Original.Invoke(gameObject, b, c, d);
|
||||
LastGameObject = oldObject;
|
||||
}
|
||||
|
||||
// Not fully understood. The game object the weapon is loaded for is seemingly found at a1 + 8,
|
||||
// so we use that.
|
||||
private delegate void WeaponReloadFunc( IntPtr a1, uint a2, IntPtr a3, byte a4, byte a5, byte a6, byte a7 );
|
||||
private delegate void WeaponReloadFunc(IntPtr a1, uint a2, IntPtr a3, byte a4, byte a5, byte a6, byte a7);
|
||||
|
||||
[Signature( Sigs.WeaponReload, DetourName = nameof( WeaponReloadDetour ) )]
|
||||
private readonly Hook< WeaponReloadFunc > _weaponReloadHook = null!;
|
||||
[Signature(Sigs.WeaponReload, DetourName = nameof(WeaponReloadDetour))]
|
||||
private readonly Hook<WeaponReloadFunc> _weaponReloadHook = null!;
|
||||
|
||||
public void WeaponReloadDetour( IntPtr a1, uint a2, IntPtr a3, byte a4, byte a5, byte a6, byte a7 )
|
||||
public void WeaponReloadDetour(IntPtr a1, uint a2, IntPtr a3, byte a4, byte a5, byte a6, byte a7)
|
||||
{
|
||||
var oldGame = LastGameObject;
|
||||
LastGameObject = *( GameObject** )( a1 + 8 );
|
||||
_weaponReloadHook!.Original( a1, a2, a3, a4, a5, a6, a7 );
|
||||
LastGameObject = *(GameObject**)(a1 + 8);
|
||||
_weaponReloadHook!.Original(a1, a2, a3, a4, a5, a6, a7);
|
||||
LastGameObject = oldGame;
|
||||
}
|
||||
|
||||
// Update collections linked to Game/DrawObjects due to a change in collection configuration.
|
||||
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;
|
||||
}
|
||||
|
||||
foreach( var (key, (_, idx)) in _drawObjectToObject.ToArray() )
|
||||
foreach (var (key, (_, idx)) in _drawObjectToObject.ToArray())
|
||||
{
|
||||
if( !VerifyEntry( key, idx, out var obj ) )
|
||||
{
|
||||
_drawObjectToObject.Remove( key );
|
||||
}
|
||||
if (!VerifyEntry(key, idx, out var obj))
|
||||
_drawObjectToObject.Remove(key);
|
||||
|
||||
var newCollection = IdentifyCollection( obj, false );
|
||||
_drawObjectToObject[ key ] = ( newCollection, idx );
|
||||
var newCollection = IdentifyCollection(obj, false);
|
||||
_drawObjectToObject[key] = (newCollection, idx);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -252,14 +244,12 @@ public unsafe partial class PathResolver
|
|||
// We do not iterate the Dalamud table because it does not work when not logged in.
|
||||
private void InitializeDrawObjects()
|
||||
{
|
||||
for( var i = 0; i < DalamudServices.Objects.Length; ++i )
|
||||
for (var i = 0; i < DalamudServices.Objects.Length; ++i)
|
||||
{
|
||||
var ptr = ( GameObject* )DalamudServices.Objects.GetObjectAddress( i );
|
||||
if( ptr != null && ptr->IsCharacter() && ptr->DrawObject != null )
|
||||
{
|
||||
_drawObjectToObject[ ( IntPtr )ptr->DrawObject ] = ( IdentifyCollection( ptr, false ), ptr->ObjectIndex );
|
||||
}
|
||||
var ptr = (GameObject*)DalamudServices.Objects.GetObjectAddress(i);
|
||||
if (ptr != null && ptr->IsCharacter() && ptr->DrawObject != null)
|
||||
_drawObjectToObject[(IntPtr)ptr->DrawObject] = (IdentifyCollection(ptr, false), ptr->ObjectIndex);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -103,7 +103,7 @@ public unsafe partial class PathResolver
|
|||
|
||||
// Check both temporary and permanent character collections. Temporary first.
|
||||
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 )
|
||||
? collection
|
||||
: null;
|
||||
|
|
|
|||
|
|
@ -265,7 +265,7 @@ public partial class PathResolver
|
|||
}
|
||||
|
||||
var parentObject = ( IntPtr )( ( DrawObject* )drawObject )->Object.ParentObject;
|
||||
var parentCollection = DrawObjects.CheckParentDrawObject( drawObject, parentObject );
|
||||
var parentCollection = _drawObjects.CheckParentDrawObject( drawObject, parentObject );
|
||||
if( parentCollection.Valid )
|
||||
{
|
||||
return _parent._paths.ResolvePath( ( IntPtr )FindParent( parentObject, out _ ), parentCollection.ModCollection, path );
|
||||
|
|
|
|||
|
|
@ -5,10 +5,11 @@ using Dalamud.Utility.Signatures;
|
|||
using FFXIVClientStructs.FFXIV.Client.Game.Object;
|
||||
using FFXIVClientStructs.FFXIV.Client.Graphics.Scene;
|
||||
using FFXIVClientStructs.FFXIV.Client.System.Resource;
|
||||
using OtterGui.Classes;
|
||||
using Penumbra.Collections;
|
||||
using Penumbra.GameData.Enums;
|
||||
using Penumbra.Interop.Loader;
|
||||
using Penumbra.Services;
|
||||
using Penumbra.Services;
|
||||
using Penumbra.String;
|
||||
using Penumbra.String.Classes;
|
||||
using Penumbra.Util;
|
||||
|
|
@ -24,70 +25,70 @@ public partial class PathResolver : IDisposable
|
|||
{
|
||||
public bool Enabled { get; private set; }
|
||||
|
||||
private readonly ResourceLoader _loader;
|
||||
private static readonly CutsceneCharacters Cutscenes = new(Penumbra.GameEvents);
|
||||
private static readonly DrawObjectState DrawObjects = new();
|
||||
private static readonly BitArray ValidHumanModels;
|
||||
internal static readonly IdentifiedCollectionCache IdentifiedCache = new(Penumbra.GameEvents);
|
||||
private readonly AnimationState _animations;
|
||||
private readonly PathState _paths;
|
||||
private readonly MetaState _meta;
|
||||
private readonly SubfileHelper _subFiles;
|
||||
private readonly CommunicatorService _communicator;
|
||||
private readonly ResourceLoader _loader;
|
||||
private static readonly CutsceneCharacters Cutscenes = new(DalamudServices.Objects, Penumbra.GameEvents); // TODO
|
||||
private static DrawObjectState _drawObjects = null!; // TODO
|
||||
private static readonly BitArray ValidHumanModels;
|
||||
internal static IdentifiedCollectionCache IdentifiedCache = null!; // TODO
|
||||
private readonly AnimationState _animations;
|
||||
private readonly PathState _paths;
|
||||
private readonly MetaState _meta;
|
||||
private readonly SubfileHelper _subFiles;
|
||||
|
||||
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 );
|
||||
SignatureHelper.Initialise( this );
|
||||
using var tApi = timer.Measure(StartTimeType.PathResolver);
|
||||
_communicator = communicator;
|
||||
IdentifiedCache = new IdentifiedCollectionCache(communicator, events);
|
||||
SignatureHelper.Initialise(this);
|
||||
_drawObjects = new DrawObjectState(_communicator);
|
||||
_loader = loader;
|
||||
_animations = new AnimationState( DrawObjects );
|
||||
_paths = new PathState( this );
|
||||
_meta = new MetaState( _paths.HumanVTable );
|
||||
_subFiles = new SubfileHelper( _loader, Penumbra.GameEvents );
|
||||
_animations = new AnimationState(_drawObjects);
|
||||
_paths = new PathState(this);
|
||||
_meta = new MetaState(_paths.HumanVTable);
|
||||
_subFiles = new SubfileHelper(_loader, Penumbra.GameEvents);
|
||||
}
|
||||
|
||||
// The modified resolver that handles game path resolving.
|
||||
private bool CharacterResolver( Utf8GamePath gamePath, ResourceCategory _1, ResourceType type, int _2, out (FullPath?, ResolveData) data )
|
||||
private bool CharacterResolver(Utf8GamePath gamePath, ResourceCategory _1, ResourceType type, int _2, out (FullPath?, ResolveData) data)
|
||||
{
|
||||
using var performance = Penumbra.Performance.Measure( PerformanceType.CharacterResolver );
|
||||
using var performance = Penumbra.Performance.Measure(PerformanceType.CharacterResolver);
|
||||
// Check if the path was marked for a specific collection,
|
||||
// or if it is a file loaded by a material, and if we are currently in a material load,
|
||||
// or if it is a face decal path and the current mod collection is set.
|
||||
// If not use the default collection.
|
||||
// We can remove paths after they have actually been loaded.
|
||||
// A potential next request will add the path anew.
|
||||
var nonDefault = _subFiles.HandleSubFiles( type, out var resolveData )
|
||||
|| _paths.Consume( gamePath.Path, out resolveData )
|
||||
|| _animations.HandleFiles( type, gamePath, out resolveData )
|
||||
|| DrawObjects.HandleDecalFile( type, gamePath, out resolveData );
|
||||
if( !nonDefault || !resolveData.Valid )
|
||||
{
|
||||
var nonDefault = _subFiles.HandleSubFiles(type, out var resolveData)
|
||||
|| _paths.Consume(gamePath.Path, out resolveData)
|
||||
|| _animations.HandleFiles(type, gamePath, out resolveData)
|
||||
|| _drawObjects.HandleDecalFile(type, gamePath, out resolveData);
|
||||
if (!nonDefault || !resolveData.Valid)
|
||||
resolveData = Penumbra.CollectionManager.Default.ToResolveData();
|
||||
}
|
||||
|
||||
// Resolve using character/default collection first, otherwise forced, as usual.
|
||||
var resolved = resolveData.ModCollection.ResolvePath( gamePath );
|
||||
var resolved = resolveData.ModCollection.ResolvePath(gamePath);
|
||||
|
||||
// Since mtrl files load their files separately, we need to add the new, resolved path
|
||||
// so that the functions loading tex and shpk can find that path and use its collection.
|
||||
// We also need to handle defaulted materials against a non-default collection.
|
||||
var path = resolved == null ? gamePath.Path : resolved.Value.InternalName;
|
||||
SubfileHelper.HandleCollection( resolveData, path, nonDefault, type, resolved, out data );
|
||||
SubfileHelper.HandleCollection(resolveData, path, nonDefault, type, resolved, out data);
|
||||
return true;
|
||||
}
|
||||
|
||||
public void Enable()
|
||||
{
|
||||
if( Enabled )
|
||||
{
|
||||
if (Enabled)
|
||||
return;
|
||||
}
|
||||
|
||||
Enabled = true;
|
||||
Cutscenes.Enable();
|
||||
DrawObjects.Enable();
|
||||
_drawObjects.Enable();
|
||||
IdentifiedCache.Enable();
|
||||
_animations.Enable();
|
||||
_paths.Enable();
|
||||
|
|
@ -95,19 +96,17 @@ public partial class PathResolver : IDisposable
|
|||
_subFiles.Enable();
|
||||
|
||||
_loader.ResolvePathCustomization += CharacterResolver;
|
||||
Penumbra.Log.Debug( "Character Path Resolver enabled." );
|
||||
Penumbra.Log.Debug("Character Path Resolver enabled.");
|
||||
}
|
||||
|
||||
public void Disable()
|
||||
{
|
||||
if( !Enabled )
|
||||
{
|
||||
if (!Enabled)
|
||||
return;
|
||||
}
|
||||
|
||||
Enabled = false;
|
||||
_animations.Disable();
|
||||
DrawObjects.Disable();
|
||||
_drawObjects.Disable();
|
||||
Cutscenes.Disable();
|
||||
IdentifiedCache.Disable();
|
||||
_paths.Disable();
|
||||
|
|
@ -115,7 +114,7 @@ public partial class PathResolver : IDisposable
|
|||
_subFiles.Disable();
|
||||
|
||||
_loader.ResolvePathCustomization -= CharacterResolver;
|
||||
Penumbra.Log.Debug( "Character Path Resolver disabled." );
|
||||
Penumbra.Log.Debug("Character Path Resolver disabled.");
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
|
|
@ -123,58 +122,58 @@ public partial class PathResolver : IDisposable
|
|||
Disable();
|
||||
_paths.Dispose();
|
||||
_animations.Dispose();
|
||||
DrawObjects.Dispose();
|
||||
_drawObjects.Dispose();
|
||||
Cutscenes.Dispose();
|
||||
IdentifiedCache.Dispose();
|
||||
_meta.Dispose();
|
||||
_subFiles.Dispose();
|
||||
}
|
||||
|
||||
public static unsafe (IntPtr, ResolveData) IdentifyDrawObject( IntPtr drawObject )
|
||||
public static unsafe (IntPtr, ResolveData) IdentifyDrawObject(IntPtr drawObject)
|
||||
{
|
||||
var parent = FindParent( drawObject, out var resolveData );
|
||||
return ( ( IntPtr )parent, resolveData );
|
||||
var parent = FindParent(drawObject, out var resolveData);
|
||||
return ((IntPtr)parent, resolveData);
|
||||
}
|
||||
|
||||
public int CutsceneActor( int idx )
|
||||
=> Cutscenes.GetParentIndex( idx );
|
||||
public int CutsceneActor(int idx)
|
||||
=> Cutscenes.GetParentIndex(idx);
|
||||
|
||||
// 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;
|
||||
return gameObject;
|
||||
}
|
||||
|
||||
if( DrawObjects.LastGameObject != null
|
||||
&& ( DrawObjects.LastGameObject->DrawObject == null || DrawObjects.LastGameObject->DrawObject == ( DrawObject* )drawObject ) )
|
||||
if (_drawObjects.LastGameObject != null
|
||||
&& (_drawObjects.LastGameObject->DrawObject == null || _drawObjects.LastGameObject->DrawObject == (DrawObject*)drawObject))
|
||||
{
|
||||
resolveData = IdentifyCollection( DrawObjects.LastGameObject, true );
|
||||
return DrawObjects.LastGameObject;
|
||||
resolveData = IdentifyCollection(_drawObjects.LastGameObject, true);
|
||||
return _drawObjects.LastGameObject;
|
||||
}
|
||||
|
||||
resolveData = IdentifyCollection( null, true );
|
||||
resolveData = IdentifyCollection(null, true);
|
||||
return null;
|
||||
}
|
||||
|
||||
private static unsafe ResolveData GetResolveData( IntPtr drawObject )
|
||||
private static unsafe ResolveData GetResolveData(IntPtr drawObject)
|
||||
{
|
||||
var _ = FindParent( drawObject, out var resolveData );
|
||||
var _ = FindParent(drawObject, out var resolveData);
|
||||
return resolveData;
|
||||
}
|
||||
|
||||
internal IEnumerable< KeyValuePair< ByteString, ResolveData > > PathCollections
|
||||
internal IEnumerable<KeyValuePair<ByteString, ResolveData>> PathCollections
|
||||
=> _paths.Paths;
|
||||
|
||||
internal IEnumerable< KeyValuePair< IntPtr, (ResolveData, int) > > DrawObjectMap
|
||||
=> DrawObjects.DrawObjects;
|
||||
internal IEnumerable<KeyValuePair<IntPtr, (ResolveData, int)>> DrawObjectMap
|
||||
=> _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;
|
||||
|
||||
internal IEnumerable< KeyValuePair< IntPtr, ResolveData > > ResourceCollections
|
||||
internal IEnumerable<KeyValuePair<IntPtr, ResolveData>> ResourceCollections
|
||||
=> _subFiles;
|
||||
|
||||
internal int SubfileCount
|
||||
|
|
@ -187,8 +186,8 @@ public partial class PathResolver : IDisposable
|
|||
=> _subFiles.AvfxData;
|
||||
|
||||
internal ResolveData LastGameObjectData
|
||||
=> DrawObjects.LastCreatedCollection;
|
||||
=> _drawObjects.LastCreatedCollection;
|
||||
|
||||
internal unsafe nint LastGameObject
|
||||
=> (nint) DrawObjects.LastGameObject;
|
||||
}
|
||||
=> (nint)_drawObjects.LastGameObject;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -169,7 +169,7 @@ public partial class MetaManager
|
|||
|
||||
var lastUnderscore = split.LastIndexOf( ( byte )'_' );
|
||||
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 ) )
|
||||
&& collection.HasCache
|
||||
&& collection.MetaCache!._imcFiles.TryGetValue( Utf8GamePath.FromSpan( path.Span, out var p ) ? p : Utf8GamePath.Empty, out var file ) )
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ using System;
|
|||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using Dalamud.Plugin;
|
||||
using Newtonsoft.Json;
|
||||
using Penumbra.Services;
|
||||
|
||||
|
|
@ -10,8 +11,8 @@ namespace Penumbra.Mods;
|
|||
|
||||
public sealed partial class Mod
|
||||
{
|
||||
public static DirectoryInfo LocalDataDirectory
|
||||
=> new(Path.Combine( DalamudServices.PluginInterface.ConfigDirectory.FullName, "mod_data" ));
|
||||
public static DirectoryInfo LocalDataDirectory(DalamudPluginInterface pi)
|
||||
=> new(Path.Combine( pi.ConfigDirectory.FullName, "mod_data" ));
|
||||
|
||||
public long ImportDate { get; private set; } = DateTimeOffset.UnixEpoch.ToUnixTimeMilliseconds();
|
||||
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ using System.Diagnostics.CodeAnalysis;
|
|||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Text.RegularExpressions;
|
||||
using Dalamud.Plugin;
|
||||
using OtterGui.Filesystem;
|
||||
using Penumbra.Services;
|
||||
|
||||
|
|
@ -11,15 +12,16 @@ namespace Penumbra.Mods;
|
|||
|
||||
public sealed class ModFileSystem : FileSystem< Mod >, IDisposable
|
||||
{
|
||||
public static string ModFileSystemFile
|
||||
=> Path.Combine( DalamudServices.PluginInterface.GetPluginConfigDirectory(), "sort_order.json" );
|
||||
public static string ModFileSystemFile(DalamudPluginInterface pi)
|
||||
=> Path.Combine( pi.GetPluginConfigDirectory(), "sort_order.json" );
|
||||
|
||||
// Save the current sort order.
|
||||
// Does not save or copy the backup in the current mod directory,
|
||||
// as this is done on mod directory changes only.
|
||||
// as this is done on mod directory changes only.
|
||||
// TODO
|
||||
private void SaveFilesystem()
|
||||
{
|
||||
SaveToFile( new FileInfo( ModFileSystemFile ), SaveMod, true );
|
||||
SaveToFile( new FileInfo( ModFileSystemFile(DalamudServices.PluginInterface) ), SaveMod, true );
|
||||
Penumbra.Log.Verbose( "Saved mod filesystem." );
|
||||
}
|
||||
|
||||
|
|
@ -74,8 +76,9 @@ public sealed class ModFileSystem : FileSystem< Mod >, IDisposable
|
|||
// Reload the whole filesystem from currently loaded mods and the current sort order file.
|
||||
// Used on construction and on mod rediscoveries.
|
||||
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();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,4 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Reflection;
|
||||
|
|
@ -29,45 +28,10 @@ using Penumbra.Mods;
|
|||
using CharacterUtility = Penumbra.Interop.CharacterUtility;
|
||||
using DalamudUtil = Dalamud.Utility.Util;
|
||||
using ResidentResourceManager = Penumbra.Interop.ResidentResourceManager;
|
||||
using Penumbra.Services;
|
||||
|
||||
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 string Name
|
||||
|
|
@ -76,135 +40,123 @@ public class Penumbra : IDalamudPlugin
|
|||
public static readonly string Version = Assembly.GetExecutingAssembly().GetName().Version?.ToString() ?? string.Empty;
|
||||
|
||||
public static readonly string CommitHash =
|
||||
Assembly.GetExecutingAssembly().GetCustomAttribute< AssemblyInformationalVersionAttribute >()?.InformationalVersion ?? "Unknown";
|
||||
Assembly.GetExecutingAssembly().GetCustomAttribute<AssemblyInformationalVersionAttribute>()?.InformationalVersion ?? "Unknown";
|
||||
|
||||
public static Logger Log { get; private set; } = null!;
|
||||
public static Logger Log { get; private set; } = null!;
|
||||
public static Configuration Config { get; private set; } = null!;
|
||||
|
||||
public static ResidentResourceManager ResidentResources { get; private set; } = null!;
|
||||
public static CharacterUtility CharacterUtility { get; private set; } = null!;
|
||||
public static GameEventManager GameEvents { get; private set; } = null!;
|
||||
public static MetaFileManager MetaFileManager { get; private set; } = null!;
|
||||
public static Mod.Manager ModManager { get; private set; } = null!;
|
||||
public static ModCollection.Manager CollectionManager { get; private set; } = null!;
|
||||
public static TempModManager TempMods { get; private set; } = null!;
|
||||
public static ResourceLoader ResourceLoader { get; private set; } = null!;
|
||||
public static FrameworkManager Framework { get; private set; } = null!;
|
||||
public static ActorManager Actors { get; private set; } = null!;
|
||||
public static IObjectIdentifier Identifier { get; private set; } = null!;
|
||||
public static IGamePathParser GamePathParser { get; private set; } = null!;
|
||||
public static StainManager StainManager { get; private set; } = null!;
|
||||
public static CharacterUtility CharacterUtility { get; private set; } = null!;
|
||||
public static GameEventManager GameEvents { get; private set; } = null!;
|
||||
public static MetaFileManager MetaFileManager { get; private set; } = null!;
|
||||
public static Mod.Manager ModManager { 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 ResourceLoader ResourceLoader { get; private set; } = null!;
|
||||
public static FrameworkManager Framework { get; private set; } = null!;
|
||||
public static ActorManager Actors { get; private set; } = null!;
|
||||
public static IObjectIdentifier Identifier { get; private set; } = null!;
|
||||
public static IGamePathParser GamePathParser { get; private set; } = null!;
|
||||
public static StainService StainService { get; private set; } = null!;
|
||||
|
||||
// TODO
|
||||
public static DalamudServices Dalamud { get; private set; } = null!;
|
||||
|
||||
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 PathResolver PathResolver;
|
||||
public readonly ObjectReloader ObjectReloader;
|
||||
public readonly ModFileSystem ModFileSystem;
|
||||
public readonly PenumbraApi Api;
|
||||
public readonly HttpApi HttpApi;
|
||||
public readonly PenumbraIpcProviders IpcProviders;
|
||||
internal ConfigWindow? ConfigWindow { get; private set; }
|
||||
private LaunchButton? _launchButton;
|
||||
private WindowSystem? _windowSystem;
|
||||
private Changelog? _changelog;
|
||||
private CommandHandler? _commandHandler;
|
||||
private readonly ResourceWatcher _resourceWatcher;
|
||||
private bool _disposed;
|
||||
|
||||
public readonly ResourceLogger ResourceLogger;
|
||||
public readonly PathResolver PathResolver;
|
||||
public readonly ObjectReloader ObjectReloader;
|
||||
public readonly ModFileSystem ModFileSystem;
|
||||
public readonly PenumbraApi Api;
|
||||
public readonly HttpApi HttpApi;
|
||||
public readonly PenumbraIpcProviders IpcProviders;
|
||||
internal ConfigWindow? ConfigWindow { get; private set; }
|
||||
private LaunchButton? _launchButton;
|
||||
private WindowSystem? _windowSystem;
|
||||
private Changelog? _changelog;
|
||||
private CommandHandler? _commandHandler;
|
||||
private readonly ResourceWatcher _resourceWatcher;
|
||||
private bool _disposed;
|
||||
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
|
||||
{
|
||||
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();
|
||||
_resourceWatcher = new ResourceWatcher( ResourceLoader );
|
||||
ResourceLogger = new ResourceLogger( ResourceLoader );
|
||||
_resourceWatcher = new ResourceWatcher(ResourceLoader);
|
||||
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();
|
||||
} );
|
||||
});
|
||||
|
||||
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();
|
||||
} );
|
||||
});
|
||||
|
||||
|
||||
TempCollections = _tmp.Services.GetRequiredService<TempCollectionManager>();
|
||||
|
||||
ModFileSystem = ModFileSystem.Load();
|
||||
ObjectReloader = new ObjectReloader();
|
||||
PathResolver = new PathResolver( ResourceLoader );
|
||||
PathResolver = new PathResolver(_tmp.Services.GetRequiredService<StartTracker>(), _tmp.Services.GetRequiredService<CommunicatorService>(), _tmp.Services.GetRequiredService<GameEventManager>(), ResourceLoader);
|
||||
|
||||
SetupInterface();
|
||||
|
||||
if( Config.EnableMods )
|
||||
if (Config.EnableMods)
|
||||
{
|
||||
ResourceLoader.EnableReplacements();
|
||||
PathResolver.Enable();
|
||||
}
|
||||
|
||||
if( Config.DebugMode )
|
||||
{
|
||||
if (Config.DebugMode)
|
||||
ResourceLoader.EnableDebug();
|
||||
}
|
||||
|
||||
using( var tApi = StartTimer.Measure( StartTimeType.Api ) )
|
||||
using (var tApi = _tmp.Services.GetRequiredService<StartTracker>().Measure(StartTimeType.Api))
|
||||
{
|
||||
Api = new PenumbraApi( this );
|
||||
IpcProviders = new PenumbraIpcProviders( DalamudServices.PluginInterface, Api );
|
||||
HttpApi = new HttpApi( Api );
|
||||
if( Config.EnableHttpApi )
|
||||
{
|
||||
Api = new PenumbraApi(_tmp.Services.GetRequiredService<CommunicatorService>(), this);
|
||||
IpcProviders = new PenumbraIpcProviders(DalamudServices.PluginInterface, Api);
|
||||
HttpApi = new HttpApi(Api);
|
||||
if (Config.EnableHttpApi)
|
||||
HttpApi.CreateWebServer();
|
||||
}
|
||||
|
||||
SubscribeItemLinks();
|
||||
}
|
||||
|
||||
ValidityChecker.LogExceptions();
|
||||
Log.Information( $"Penumbra Version {Version}, Commit #{CommitHash} successfully Loaded from {pluginInterface.SourceRepository}." );
|
||||
OtterTex.NativeDll.Initialize( DalamudServices.PluginInterface.AssemblyLocation.DirectoryName );
|
||||
Log.Information( $"Loading native OtterTex assembly from {OtterTex.NativeDll.Directory}." );
|
||||
Log.Information($"Penumbra Version {Version}, Commit #{CommitHash} successfully Loaded from {pluginInterface.SourceRepository}.");
|
||||
OtterTex.NativeDll.Initialize(DalamudServices.PluginInterface.AssemblyLocation.DirectoryName);
|
||||
Log.Information($"Loading native OtterTex assembly from {OtterTex.NativeDll.Directory}.");
|
||||
|
||||
if( CharacterUtility.Ready )
|
||||
{
|
||||
if (CharacterUtility.Ready)
|
||||
ResidentResources.Reload();
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
|
|
@ -215,21 +167,22 @@ public class Penumbra : IDalamudPlugin
|
|||
|
||||
private void SetupInterface()
|
||||
{
|
||||
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 cfg = new ConfigWindow( this, _resourceWatcher )
|
||||
var cfg = new ConfigWindow(_tmp.Services.GetRequiredService<CommunicatorService>(), _tmp.Services.GetRequiredService<StartTracker>(), this, _resourceWatcher)
|
||||
{
|
||||
IsOpen = Config.DebugMode,
|
||||
};
|
||||
var btn = new LaunchButton( cfg );
|
||||
var system = new WindowSystem( Name );
|
||||
var cmd = new CommandHandler( DalamudServices.Commands, ObjectReloader, Config, this, cfg, ModManager, CollectionManager, Actors );
|
||||
system.AddWindow( cfg );
|
||||
system.AddWindow( cfg.ModEditPopup );
|
||||
system.AddWindow( changelog );
|
||||
if( !_disposed )
|
||||
var btn = new LaunchButton(cfg);
|
||||
var system = new WindowSystem(Name);
|
||||
var cmd = new CommandHandler(DalamudServices.Commands, ObjectReloader, Config, this, cfg, ModManager, CollectionManager,
|
||||
Actors);
|
||||
system.AddWindow(cfg);
|
||||
system.AddWindow(cfg.ModEditPopup);
|
||||
system.AddWindow(changelog);
|
||||
if (!_disposed)
|
||||
{
|
||||
_changelog = changelog;
|
||||
ConfigWindow = cfg;
|
||||
|
|
@ -251,100 +204,87 @@ public class Penumbra : IDalamudPlugin
|
|||
|
||||
private void DisposeInterface()
|
||||
{
|
||||
if( _windowSystem != null )
|
||||
{
|
||||
if (_windowSystem != null)
|
||||
DalamudServices.PluginInterface.UiBuilder.Draw -= _windowSystem.Draw;
|
||||
}
|
||||
|
||||
_launchButton?.Dispose();
|
||||
if( ConfigWindow != null )
|
||||
if (ConfigWindow != null)
|
||||
{
|
||||
DalamudServices.PluginInterface.UiBuilder.OpenConfigUi -= ConfigWindow.Toggle;
|
||||
ConfigWindow.Dispose();
|
||||
}
|
||||
}
|
||||
|
||||
public event Action< bool >? EnabledChange;
|
||||
public event Action<bool>? EnabledChange;
|
||||
|
||||
public bool SetEnabled( bool enabled )
|
||||
public bool SetEnabled(bool enabled)
|
||||
{
|
||||
if( enabled == Config.EnableMods )
|
||||
{
|
||||
if (enabled == Config.EnableMods)
|
||||
return false;
|
||||
}
|
||||
|
||||
Config.EnableMods = enabled;
|
||||
if( enabled )
|
||||
if (enabled)
|
||||
{
|
||||
ResourceLoader.EnableReplacements();
|
||||
PathResolver.Enable();
|
||||
if( CharacterUtility.Ready )
|
||||
if (CharacterUtility.Ready)
|
||||
{
|
||||
CollectionManager.Default.SetFiles();
|
||||
ResidentResources.Reload();
|
||||
ObjectReloader.RedrawAll( RedrawType.Redraw );
|
||||
ObjectReloader.RedrawAll(RedrawType.Redraw);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
ResourceLoader.DisableReplacements();
|
||||
PathResolver.Disable();
|
||||
if( CharacterUtility.Ready )
|
||||
if (CharacterUtility.Ready)
|
||||
{
|
||||
CharacterUtility.ResetAll();
|
||||
ResidentResources.Reload();
|
||||
ObjectReloader.RedrawAll( RedrawType.Redraw );
|
||||
ObjectReloader.RedrawAll(RedrawType.Redraw);
|
||||
}
|
||||
}
|
||||
|
||||
Config.Save();
|
||||
EnabledChange?.Invoke( enabled );
|
||||
EnabledChange?.Invoke(enabled);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public void ForceChangelogOpen()
|
||||
{
|
||||
if( _changelog != null )
|
||||
{
|
||||
if (_changelog != null)
|
||||
_changelog.ForceOpen = true;
|
||||
}
|
||||
}
|
||||
|
||||
private void SubscribeItemLinks()
|
||||
{
|
||||
Api.ChangedItemTooltip += it =>
|
||||
{
|
||||
if( it is Item )
|
||||
{
|
||||
ImGui.TextUnformatted( "Left Click to create an item link in chat." );
|
||||
}
|
||||
if (it is Item)
|
||||
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 )
|
||||
{
|
||||
ChatUtil.LinkItem( item );
|
||||
}
|
||||
if (button == MouseButton.Left && it is Item item)
|
||||
ChatUtil.LinkItem(item);
|
||||
};
|
||||
}
|
||||
|
||||
private short ResolveCutscene( ushort index )
|
||||
=> ( short )PathResolver.CutsceneActor( index );
|
||||
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
if( _disposed )
|
||||
{
|
||||
if (_disposed)
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
// TODO
|
||||
_tmp?.Dispose();
|
||||
_disposed = true;
|
||||
HttpApi?.Dispose();
|
||||
IpcProviders?.Dispose();
|
||||
Api?.Dispose();
|
||||
_commandHandler?.Dispose();
|
||||
StainManager?.Dispose();
|
||||
StainService?.Dispose();
|
||||
ItemData?.Dispose();
|
||||
Actors?.Dispose();
|
||||
Identifier?.Dispose();
|
||||
|
|
@ -354,99 +294,85 @@ public class Penumbra : IDalamudPlugin
|
|||
ModFileSystem?.Dispose();
|
||||
CollectionManager?.Dispose();
|
||||
PathResolver?.Dispose();
|
||||
ResourceLogger?.Dispose();
|
||||
_resourceWatcher?.Dispose();
|
||||
ResourceLoader?.Dispose();
|
||||
GameEvents?.Dispose();
|
||||
CharacterUtility?.Dispose();
|
||||
Performance?.Dispose();
|
||||
Performance?.Dispose();
|
||||
}
|
||||
|
||||
// Collect all relevant files for penumbra configuration.
|
||||
private static IReadOnlyList< FileInfo > PenumbraBackupFiles()
|
||||
public string GatherSupportInformation()
|
||||
{
|
||||
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 exists = Config.ModDirectory.Length > 0 && Directory.Exists( Config.ModDirectory );
|
||||
var drive = exists ? new DriveInfo( new DirectoryInfo( Config.ModDirectory ).Root.FullName ) : null;
|
||||
sb.AppendLine( "**Settings**" );
|
||||
sb.Append( $"> **`Plugin Version: `** {Version}\n" );
|
||||
sb.Append( $"> **`Commit Hash: `** {CommitHash}\n" );
|
||||
sb.Append( $"> **`Enable Mods: `** {Config.EnableMods}\n" );
|
||||
sb.Append( $"> **`Enable HTTP API: `** {Config.EnableHttpApi}\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( $"> **`Free Drive Space: `** {( drive != null ? Functions.HumanReadableSize( drive.AvailableFreeSpace ) : "Unknown" )}\n" );
|
||||
sb.Append( $"> **`Auto-Deduplication: `** {Config.AutoDeduplicateOnImport}\n" );
|
||||
sb.Append( $"> **`Debug Mode: `** {Config.DebugMode}\n" );
|
||||
var sb = new StringBuilder(10240);
|
||||
var exists = Config.ModDirectory.Length > 0 && Directory.Exists(Config.ModDirectory);
|
||||
var drive = exists ? new DriveInfo(new DirectoryInfo(Config.ModDirectory).Root.FullName) : null;
|
||||
sb.AppendLine("**Settings**");
|
||||
sb.Append($"> **`Plugin Version: `** {Version}\n");
|
||||
sb.Append($"> **`Commit Hash: `** {CommitHash}\n");
|
||||
sb.Append($"> **`Enable Mods: `** {Config.EnableMods}\n");
|
||||
sb.Append($"> **`Enable HTTP API: `** {Config.EnableHttpApi}\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(
|
||||
$"> **`Synchronous Load (Dalamud): `** {( 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( $"> **`Use Ownership: `** {Config.UseOwnerNameForCharacterCollection}\n" );
|
||||
sb.AppendLine( "**Mods**" );
|
||||
sb.Append( $"> **`Installed Mods: `** {ModManager.Count}\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( $"> **`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( $"> **`#Temp Mods: `** {TempMods.Mods.Sum( kvp => kvp.Value.Count ) + TempMods.ModsForAllCollections.Count}\n" );
|
||||
$"> **`Free Drive Space: `** {(drive != null ? Functions.HumanReadableSize(drive.AvailableFreeSpace) : "Unknown")}\n");
|
||||
sb.Append($"> **`Auto-Deduplication: `** {Config.AutoDeduplicateOnImport}\n");
|
||||
sb.Append($"> **`Debug Mode: `** {Config.DebugMode}\n");
|
||||
sb.Append(
|
||||
$"> **`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($"> **`Use Ownership: `** {Config.UseOwnerNameForCharacterCollection}\n");
|
||||
sb.AppendLine("**Mods**");
|
||||
sb.Append($"> **`Installed Mods: `** {ModManager.Count}\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(
|
||||
$"> **`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(
|
||||
$"> **`#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 );
|
||||
return string.Join( " ", parts.Length != 3 ? parts.Select( n => $"{n[ 0 ]}." ) : parts[ ..2 ].Select( n => $"{n[ 0 ]}." ).Append( parts[ 2 ] ) );
|
||||
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 name + ':';
|
||||
}
|
||||
|
||||
void PrintCollection( ModCollection c )
|
||||
=> sb.Append( $"**Collection {c.AnonymizedName}**\n"
|
||||
void PrintCollection(ModCollection c)
|
||||
=> sb.Append($"**Collection {c.AnonymizedName}**\n"
|
||||
+ $"> **`Inheritances: `** {c.Inheritance.Count}\n"
|
||||
+ $"> **`Enabled Mods: `** {c.ActualSettings.Count( s => s is { Enabled: true } )}\n"
|
||||
+ $"> **`Conflicts (Solved/Total): `** {c.AllConflicts.SelectMany( x => x ).Sum( x => x.HasPriority ? 0 : x.Conflicts.Count )}/{c.AllConflicts.SelectMany( x => x ).Sum( x => x.HasPriority || !x.Solved ? 0 : x.Conflicts.Count )}\n" );
|
||||
+ $"> **`Enabled Mods: `** {c.ActualSettings.Count(s => s is { Enabled: true })}\n"
|
||||
+ $"> **`Conflicts (Solved/Total): `** {c.AllConflicts.SelectMany(x => x).Sum(x => x.HasPriority ? 0 : x.Conflicts.Count)}/{c.AllConflicts.SelectMany(x => x).Sum(x => x.HasPriority || !x.Solved ? 0 : x.Conflicts.Count)}\n");
|
||||
|
||||
sb.AppendLine( "**Collections**" );
|
||||
sb.Append( $"> **`#Collections: `** {CollectionManager.Count - 1}\n" );
|
||||
sb.Append( $"> **`#Temp Collections: `** {TempMods.CustomCollections.Count}\n" );
|
||||
sb.Append( $"> **`Active Collections: `** {CollectionManager.Count( c => c.HasCache )}\n" );
|
||||
sb.Append( $"> **`Base Collection: `** {CollectionManager.Default.AnonymizedName}\n" );
|
||||
sb.Append( $"> **`Interface Collection: `** {CollectionManager.Interface.AnonymizedName}\n" );
|
||||
sb.Append( $"> **`Selected Collection: `** {CollectionManager.Current.AnonymizedName}\n" );
|
||||
foreach( var (type, name, _) in CollectionTypeExtensions.Special )
|
||||
sb.AppendLine("**Collections**");
|
||||
sb.Append($"> **`#Collections: `** {CollectionManager.Count - 1}\n");
|
||||
sb.Append($"> **`#Temp Collections: `** {TempCollections.Count}\n");
|
||||
sb.Append($"> **`Active Collections: `** {CollectionManager.Count(c => c.HasCache)}\n");
|
||||
sb.Append($"> **`Base Collection: `** {CollectionManager.Default.AnonymizedName}\n");
|
||||
sb.Append($"> **`Interface Collection: `** {CollectionManager.Interface.AnonymizedName}\n");
|
||||
sb.Append($"> **`Selected Collection: `** {CollectionManager.Current.AnonymizedName}\n");
|
||||
foreach (var (type, name, _) in CollectionTypeExtensions.Special)
|
||||
{
|
||||
var collection = CollectionManager.ByType( type );
|
||||
if( collection != null )
|
||||
{
|
||||
sb.Append( $"> **`{name,-30}`** {collection.AnonymizedName}\n" );
|
||||
}
|
||||
var collection = CollectionManager.ByType(type);
|
||||
if (collection != null)
|
||||
sb.Append($"> **`{name,-30}`** {collection.AnonymizedName}\n");
|
||||
}
|
||||
|
||||
foreach( var (name, id, collection) in CollectionManager.Individuals.Assignments )
|
||||
{
|
||||
sb.Append( $"> **`{CharacterName( id[ 0 ], name ),-30}`** {collection.AnonymizedName}\n" );
|
||||
}
|
||||
foreach (var (name, id, collection) in CollectionManager.Individuals.Assignments)
|
||||
sb.Append($"> **`{CharacterName(id[0], name),-30}`** {collection.AnonymizedName}\n");
|
||||
|
||||
foreach( var collection in CollectionManager.Where( c => c.HasCache ) )
|
||||
{
|
||||
PrintCollection( collection );
|
||||
}
|
||||
foreach (var collection in CollectionManager.Where(c => c.HasCache))
|
||||
PrintCollection(collection);
|
||||
|
||||
return sb.ToString();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,11 +1,14 @@
|
|||
using System.IO;
|
||||
using System;
|
||||
using Dalamud.Plugin;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using OtterGui.Classes;
|
||||
using OtterGui.Log;
|
||||
using Penumbra.Api;
|
||||
using Penumbra.Collections;
|
||||
using Penumbra.GameData;
|
||||
using Penumbra.GameData.Data;
|
||||
using Penumbra.Interop;
|
||||
using Penumbra.Interop.Resolver;
|
||||
using Penumbra.Services;
|
||||
using Penumbra.Util;
|
||||
|
||||
|
|
@ -16,35 +19,61 @@ 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 static readonly Logger Log = new();
|
||||
public readonly ServiceProvider Services;
|
||||
|
||||
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.
|
||||
Services.AddSingleton(Log);
|
||||
Services.AddSingleton(StartTimer);
|
||||
Services.AddSingleton<ValidityChecker>();
|
||||
Services.AddSingleton<PerformanceTracker<PerformanceType>>();
|
||||
services.AddSingleton(Log)
|
||||
.AddSingleton(startTimer)
|
||||
.AddSingleton<ValidityChecker>()
|
||||
.AddSingleton<PerformanceTracker>()
|
||||
.AddSingleton<FilenameService>()
|
||||
.AddSingleton<BackupService>()
|
||||
.AddSingleton<CommunicatorService>();
|
||||
|
||||
// Add Dalamud services
|
||||
var dalamud = new DalamudServices(pi);
|
||||
dalamud.AddServices(Services);
|
||||
dalamud.AddServices(services);
|
||||
|
||||
// Add Game Data
|
||||
Services.AddSingleton<GameEventManager>();
|
||||
Services.AddSingleton<IGamePathParser, GamePathParser>();
|
||||
Services.AddSingleton<IObjectIdentifier, ObjectIdentifier>();
|
||||
services.AddSingleton<IGamePathParser, GamePathParser>()
|
||||
.AddSingleton<IdentifierService>()
|
||||
.AddSingleton<StainService>()
|
||||
.AddSingleton<ItemService>()
|
||||
.AddSingleton<ActorService>();
|
||||
|
||||
// Add Game Services
|
||||
services.AddSingleton<GameEventManager>()
|
||||
.AddSingleton<FrameworkManager>()
|
||||
.AddSingleton<MetaFileManager>()
|
||||
.AddSingleton<CutsceneCharacters>()
|
||||
.AddSingleton<CharacterUtility>();
|
||||
|
||||
|
||||
// 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()
|
||||
{ }
|
||||
}
|
||||
{
|
||||
Services.Dispose();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
29
Penumbra/Services/BackupService.cs
Normal file
29
Penumbra/Services/BackupService.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
30
Penumbra/Services/CommunicatorService.cs
Normal file
30
Penumbra/Services/CommunicatorService.cs
Normal 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();
|
||||
}
|
||||
}
|
||||
377
Penumbra/Services/ConfigMigrationService.cs
Normal file
377
Penumbra/Services/ConfigMigrationService.cs
Normal 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,
|
||||
}
|
||||
}
|
||||
|
|
@ -78,21 +78,22 @@ public class DalamudServices
|
|||
services.AddSingleton(SigScanner);
|
||||
services.AddSingleton(this);
|
||||
}
|
||||
|
||||
|
||||
// TODO remove static
|
||||
// @formatter:off
|
||||
[PluginService][RequiredVersion("1.0")] public DalamudPluginInterface PluginInterface { get; private set; } = null!;
|
||||
[PluginService][RequiredVersion("1.0")] public CommandManager Commands { get; private set; } = null!;
|
||||
[PluginService][RequiredVersion("1.0")] public DataManager GameData { get; private set; } = null!;
|
||||
[PluginService][RequiredVersion("1.0")] public ClientState ClientState { get; private set; } = null!;
|
||||
[PluginService][RequiredVersion("1.0")] public ChatGui Chat { get; private set; } = null!;
|
||||
[PluginService][RequiredVersion("1.0")] public Framework Framework { get; private set; } = null!;
|
||||
[PluginService][RequiredVersion("1.0")] public Condition Conditions { get; private set; } = null!;
|
||||
[PluginService][RequiredVersion("1.0")] public TargetManager Targets { get; private set; } = null!;
|
||||
[PluginService][RequiredVersion("1.0")] public ObjectTable Objects { get; private set; } = null!;
|
||||
[PluginService][RequiredVersion("1.0")] public TitleScreenMenu TitleScreenMenu { get; private set; } = null!;
|
||||
[PluginService][RequiredVersion("1.0")] public GameGui GameGui { get; private set; } = null!;
|
||||
[PluginService][RequiredVersion("1.0")] public KeyState KeyState { get; private set; } = null!;
|
||||
[PluginService][RequiredVersion("1.0")] public SigScanner SigScanner { get; private set; } = null!;
|
||||
[PluginService][RequiredVersion("1.0")] public static DalamudPluginInterface PluginInterface { get; private set; } = null!;
|
||||
[PluginService][RequiredVersion("1.0")] public static CommandManager Commands { get; private set; } = null!;
|
||||
[PluginService][RequiredVersion("1.0")] public static DataManager GameData { get; private set; } = null!;
|
||||
[PluginService][RequiredVersion("1.0")] public static ClientState ClientState { get; private set; } = null!;
|
||||
[PluginService][RequiredVersion("1.0")] public static ChatGui Chat { get; private set; } = null!;
|
||||
[PluginService][RequiredVersion("1.0")] public static Framework Framework { get; private set; } = null!;
|
||||
[PluginService][RequiredVersion("1.0")] public static Condition Conditions { get; private set; } = null!;
|
||||
[PluginService][RequiredVersion("1.0")] public static TargetManager Targets { get; private set; } = null!;
|
||||
[PluginService][RequiredVersion("1.0")] public static ObjectTable Objects { get; private set; } = null!;
|
||||
[PluginService][RequiredVersion("1.0")] public static TitleScreenMenu TitleScreenMenu { get; private set; } = null!;
|
||||
[PluginService][RequiredVersion("1.0")] public static GameGui GameGui { get; private set; } = null!;
|
||||
[PluginService][RequiredVersion("1.0")] public static KeyState KeyState { get; private set; } = null!;
|
||||
[PluginService][RequiredVersion("1.0")] public static SigScanner SigScanner { get; private set; } = null!;
|
||||
// @formatter:on
|
||||
|
||||
public const string WaitingForPluginsOption = "IsResumeGameAfterPluginLoad";
|
||||
|
|
|
|||
51
Penumbra/Services/FilenameService.cs
Normal file
51
Penumbra/Services/FilenameService.cs
Normal 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>();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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>();
|
||||
}
|
||||
117
Penumbra/Services/ServiceWrapper.cs
Normal file
117
Penumbra/Services/ServiceWrapper.cs
Normal 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.");
|
||||
}
|
||||
}
|
||||
43
Penumbra/Services/StainService.cs
Normal file
43
Penumbra/Services/StainService.cs
Normal 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.");
|
||||
}
|
||||
}
|
||||
37
Penumbra/Services/Wrappers.cs
Normal file
37
Penumbra/Services/Wrappers.cs
Normal 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)))
|
||||
{ }
|
||||
}
|
||||
|
|
@ -17,6 +17,7 @@ using Penumbra.GameData.Enums;
|
|||
using Penumbra.GameData.Structs;
|
||||
using Penumbra.Mods;
|
||||
using Penumbra.Mods.ItemSwap;
|
||||
using Penumbra.Services;
|
||||
using Penumbra.Util;
|
||||
|
||||
namespace Penumbra.UI.Classes;
|
||||
|
|
@ -42,49 +43,61 @@ public class ItemSwapWindow : IDisposable
|
|||
Weapon,
|
||||
}
|
||||
|
||||
private class ItemSelector : FilterComboCache< (string, Item) >
|
||||
private class ItemSelector : FilterComboCache<(string, Item)>
|
||||
{
|
||||
public ItemSelector( FullEquipType type )
|
||||
: base( () => Penumbra.ItemData[ type ].Select( i => ( i.Name.ToDalamudString().TextValue, i ) ).ToArray() )
|
||||
public ItemSelector(FullEquipType type)
|
||||
: base(() => Penumbra.ItemData[type].Select(i => (i.Name.ToDalamudString().TextValue, i)).ToArray())
|
||||
{ }
|
||||
|
||||
protected override string ToString( (string, Item) obj )
|
||||
protected override string ToString((string, Item) obj)
|
||||
=> obj.Item1;
|
||||
}
|
||||
|
||||
private class WeaponSelector : FilterComboCache< FullEquipType >
|
||||
private class WeaponSelector : FilterComboCache<FullEquipType>
|
||||
{
|
||||
public WeaponSelector()
|
||||
: base( FullEquipTypeExtensions.WeaponTypes.Concat( FullEquipTypeExtensions.ToolTypes ) )
|
||||
: base(FullEquipTypeExtensions.WeaponTypes.Concat(FullEquipTypeExtensions.ToolTypes))
|
||||
{ }
|
||||
|
||||
protected override string ToString( FullEquipType type )
|
||||
protected override string ToString(FullEquipType type)
|
||||
=> 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;
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
Penumbra.CollectionManager.CollectionChanged += OnCollectionChange;
|
||||
_communicator.CollectionChange.Event -= OnCollectionChange;
|
||||
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.Top ] = ( new ItemSelector( FullEquipType.Body ), new ItemSelector( FullEquipType.Body ), "Take this Top", "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.Pants ] = ( new ItemSelector( FullEquipType.Legs ), new ItemSelector( FullEquipType.Legs ), "Take these Pants", "and put them on these" ),
|
||||
[ 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" ),
|
||||
[SwapType.Hat] =
|
||||
(new ItemSelector(FullEquipType.Head), new ItemSelector(FullEquipType.Head), "Take this Hat", "and put it on this one"),
|
||||
[SwapType.Top] =
|
||||
(new ItemSelector(FullEquipType.Body), new ItemSelector(FullEquipType.Body), "Take this Top", "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.Pants] =
|
||||
(new ItemSelector(FullEquipType.Legs), new ItemSelector(FullEquipType.Legs), "Take these Pants", "and put them on these"),
|
||||
[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;
|
||||
|
|
@ -117,39 +130,33 @@ public class ItemSwapWindow : IDisposable
|
|||
|
||||
private Item[]? _affectedItems;
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
var oldDefaultName = $"{_mod?.Name.Text ?? "Unknown"} (Swapped)";
|
||||
if( _newModName.Length == 0 || oldDefaultName == _newModName )
|
||||
{
|
||||
if (_newModName.Length == 0 || oldDefaultName == _newModName)
|
||||
_newModName = $"{mod.Name.Text} (Swapped)";
|
||||
}
|
||||
|
||||
_mod = mod;
|
||||
_modSettings = settings;
|
||||
_swapData.LoadMod( _mod, _modSettings );
|
||||
_swapData.LoadMod(_mod, _modSettings);
|
||||
UpdateOption();
|
||||
_dirty = true;
|
||||
}
|
||||
|
||||
private void UpdateState()
|
||||
{
|
||||
if( !_dirty )
|
||||
{
|
||||
if (!_dirty)
|
||||
return;
|
||||
}
|
||||
|
||||
_swapData.Clear();
|
||||
_loadException = null;
|
||||
_affectedItems = null;
|
||||
try
|
||||
{
|
||||
switch( _lastTab )
|
||||
switch (_lastTab)
|
||||
{
|
||||
case SwapType.Hat:
|
||||
case SwapType.Top:
|
||||
|
|
@ -178,27 +185,31 @@ public class ItemSwapWindow : IDisposable
|
|||
}
|
||||
break;
|
||||
case SwapType.Hair when _targetId > 0 && _sourceId > 0:
|
||||
_swapData.LoadCustomization( BodySlot.Hair, Names.CombinedRace( _currentGender, _currentRace ), ( SetId )_sourceId, ( SetId )_targetId,
|
||||
_useCurrentCollection ? Penumbra.CollectionManager.Current : null );
|
||||
_swapData.LoadCustomization(BodySlot.Hair, Names.CombinedRace(_currentGender, _currentRace), (SetId)_sourceId,
|
||||
(SetId)_targetId,
|
||||
_useCurrentCollection ? Penumbra.CollectionManager.Current : null);
|
||||
break;
|
||||
case SwapType.Face when _targetId > 0 && _sourceId > 0:
|
||||
_swapData.LoadCustomization( BodySlot.Face, Names.CombinedRace( _currentGender, _currentRace ), ( SetId )_sourceId, ( SetId )_targetId,
|
||||
_useCurrentCollection ? Penumbra.CollectionManager.Current : null );
|
||||
_swapData.LoadCustomization(BodySlot.Face, Names.CombinedRace(_currentGender, _currentRace), (SetId)_sourceId,
|
||||
(SetId)_targetId,
|
||||
_useCurrentCollection ? Penumbra.CollectionManager.Current : null);
|
||||
break;
|
||||
case SwapType.Ears when _targetId > 0 && _sourceId > 0:
|
||||
_swapData.LoadCustomization( BodySlot.Zear, Names.CombinedRace( _currentGender, ModelRace.Viera ), ( SetId )_sourceId, ( SetId )_targetId,
|
||||
_useCurrentCollection ? Penumbra.CollectionManager.Current : null );
|
||||
_swapData.LoadCustomization(BodySlot.Zear, Names.CombinedRace(_currentGender, ModelRace.Viera), (SetId)_sourceId,
|
||||
(SetId)_targetId,
|
||||
_useCurrentCollection ? Penumbra.CollectionManager.Current : null);
|
||||
break;
|
||||
case SwapType.Tail when _targetId > 0 && _sourceId > 0:
|
||||
_swapData.LoadCustomization( BodySlot.Tail, Names.CombinedRace( _currentGender, _currentRace ), ( SetId )_sourceId, ( SetId )_targetId,
|
||||
_useCurrentCollection ? Penumbra.CollectionManager.Current : null );
|
||||
_swapData.LoadCustomization(BodySlot.Tail, Names.CombinedRace(_currentGender, _currentRace), (SetId)_sourceId,
|
||||
(SetId)_targetId,
|
||||
_useCurrentCollection ? Penumbra.CollectionManager.Current : null);
|
||||
break;
|
||||
case SwapType.Weapon: break;
|
||||
}
|
||||
}
|
||||
catch( Exception e )
|
||||
catch (Exception e)
|
||||
{
|
||||
Penumbra.Log.Error( $"Could not get Customization Data container for {_lastTab}:\n{e}" );
|
||||
Penumbra.Log.Error($"Could not get Customization Data container for {_lastTab}:\n{e}");
|
||||
_loadException = e;
|
||||
_affectedItems = null;
|
||||
_swapData.Clear();
|
||||
|
|
@ -207,13 +218,14 @@ public class ItemSwapWindow : IDisposable
|
|||
_dirty = false;
|
||||
}
|
||||
|
||||
private static string SwapToString( Swap swap )
|
||||
private static string SwapToString(Swap swap)
|
||||
{
|
||||
return swap switch
|
||||
{
|
||||
MetaSwap meta => $"{meta.SwapFrom}: {meta.SwapFrom.EntryToString()} -> {meta.SwapApplied.EntryToString()}",
|
||||
FileSwap file => $"{file.Type}: {file.SwapFromRequestPath} -> {file.SwapToModded.FullName}{( file.DataWasChanged ? " (EDITED)" : string.Empty )}",
|
||||
_ => string.Empty,
|
||||
FileSwap file =>
|
||||
$"{file.Type}: {file.SwapFromRequestPath} -> {file.SwapToModded.FullName}{(file.DataWasChanged ? " (EDITED)" : string.Empty)}",
|
||||
_ => string.Empty,
|
||||
};
|
||||
}
|
||||
|
||||
|
|
@ -222,28 +234,28 @@ public class ItemSwapWindow : IDisposable
|
|||
|
||||
private void UpdateOption()
|
||||
{
|
||||
_selectedGroup = _mod?.Groups.FirstOrDefault( g => g.Name == _newGroupName );
|
||||
_subModValid = _mod != null && _newGroupName.Length > 0 && _newOptionName.Length > 0 && ( _selectedGroup?.All( o => o.Name != _newOptionName ) ?? true );
|
||||
_selectedGroup = _mod?.Groups.FirstOrDefault(g => g.Name == _newGroupName);
|
||||
_subModValid = _mod != null
|
||||
&& _newGroupName.Length > 0
|
||||
&& _newOptionName.Length > 0
|
||||
&& (_selectedGroup?.All(o => o.Name != _newOptionName) ?? true);
|
||||
}
|
||||
|
||||
private void CreateMod()
|
||||
{
|
||||
var newDir = Mod.Creator.CreateModFolder( Penumbra.ModManager.BasePath, _newModName );
|
||||
Mod.Creator.CreateMeta( newDir, _newModName, Penumbra.Config.DefaultModAuthor, CreateDescription(), "1.0", string.Empty );
|
||||
Mod.Creator.CreateDefaultFiles( newDir );
|
||||
Penumbra.ModManager.AddMod( newDir );
|
||||
if( !_swapData.WriteMod( Penumbra.ModManager.Last(), _useFileSwaps ? ItemSwapContainer.WriteType.UseSwaps : ItemSwapContainer.WriteType.NoSwaps ) )
|
||||
{
|
||||
Penumbra.ModManager.DeleteMod( Penumbra.ModManager.Count - 1 );
|
||||
}
|
||||
var newDir = Mod.Creator.CreateModFolder(Penumbra.ModManager.BasePath, _newModName);
|
||||
Mod.Creator.CreateMeta(newDir, _newModName, Penumbra.Config.DefaultModAuthor, CreateDescription(), "1.0", string.Empty);
|
||||
Mod.Creator.CreateDefaultFiles(newDir);
|
||||
Penumbra.ModManager.AddMod(newDir);
|
||||
if (!_swapData.WriteMod(Penumbra.ModManager.Last(),
|
||||
_useFileSwaps ? ItemSwapContainer.WriteType.UseSwaps : ItemSwapContainer.WriteType.NoSwaps))
|
||||
Penumbra.ModManager.DeleteMod(Penumbra.ModManager.Count - 1);
|
||||
}
|
||||
|
||||
private void CreateOption()
|
||||
{
|
||||
if( _mod == null || !_subModValid )
|
||||
{
|
||||
if (_mod == null || !_subModValid)
|
||||
return;
|
||||
}
|
||||
|
||||
var groupCreated = false;
|
||||
var dirCreated = false;
|
||||
|
|
@ -251,52 +263,47 @@ public class ItemSwapWindow : IDisposable
|
|||
DirectoryInfo? optionFolderName = null;
|
||||
try
|
||||
{
|
||||
optionFolderName = Mod.Creator.NewSubFolderName( new DirectoryInfo( Path.Combine( _mod.ModPath.FullName, _selectedGroup?.Name ?? _newGroupName ) ), _newOptionName );
|
||||
if( optionFolderName?.Exists == true )
|
||||
{
|
||||
throw new Exception( $"The folder {optionFolderName.FullName} for the option already exists." );
|
||||
}
|
||||
optionFolderName =
|
||||
Mod.Creator.NewSubFolderName(new DirectoryInfo(Path.Combine(_mod.ModPath.FullName, _selectedGroup?.Name ?? _newGroupName)),
|
||||
_newOptionName);
|
||||
if (optionFolderName?.Exists == true)
|
||||
throw new Exception($"The folder {optionFolderName.FullName} for the option already exists.");
|
||||
|
||||
if( optionFolderName != null )
|
||||
if (optionFolderName != null)
|
||||
{
|
||||
if( _selectedGroup == null )
|
||||
if (_selectedGroup == null)
|
||||
{
|
||||
Penumbra.ModManager.AddModGroup( _mod, GroupType.Multi, _newGroupName );
|
||||
Penumbra.ModManager.AddModGroup(_mod, GroupType.Multi, _newGroupName);
|
||||
_selectedGroup = _mod.Groups.Last();
|
||||
groupCreated = true;
|
||||
}
|
||||
|
||||
Penumbra.ModManager.AddOption( _mod, _mod.Groups.IndexOf( _selectedGroup ), _newOptionName );
|
||||
Penumbra.ModManager.AddOption(_mod, _mod.Groups.IndexOf(_selectedGroup), _newOptionName);
|
||||
optionCreated = true;
|
||||
optionFolderName = Directory.CreateDirectory( optionFolderName.FullName );
|
||||
optionFolderName = Directory.CreateDirectory(optionFolderName.FullName);
|
||||
dirCreated = true;
|
||||
if( !_swapData.WriteMod( _mod, _useFileSwaps ? ItemSwapContainer.WriteType.UseSwaps : ItemSwapContainer.WriteType.NoSwaps, optionFolderName,
|
||||
_mod.Groups.IndexOf( _selectedGroup ), _selectedGroup.Count - 1 ) )
|
||||
{
|
||||
throw new Exception( "Failure writing files for mod swap." );
|
||||
}
|
||||
if (!_swapData.WriteMod(_mod, _useFileSwaps ? ItemSwapContainer.WriteType.UseSwaps : ItemSwapContainer.WriteType.NoSwaps,
|
||||
optionFolderName,
|
||||
_mod.Groups.IndexOf(_selectedGroup), _selectedGroup.Count - 1))
|
||||
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
|
||||
{
|
||||
if( optionCreated && _selectedGroup != null )
|
||||
{
|
||||
Penumbra.ModManager.DeleteOption( _mod, _mod.Groups.IndexOf( _selectedGroup ), _selectedGroup.Count - 1 );
|
||||
}
|
||||
if (optionCreated && _selectedGroup != null)
|
||||
Penumbra.ModManager.DeleteOption(_mod, _mod.Groups.IndexOf(_selectedGroup), _selectedGroup.Count - 1);
|
||||
|
||||
if( groupCreated )
|
||||
if (groupCreated)
|
||||
{
|
||||
Penumbra.ModManager.DeleteModGroup( _mod, _mod.Groups.IndexOf( _selectedGroup! ) );
|
||||
Penumbra.ModManager.DeleteModGroup(_mod, _mod.Groups.IndexOf(_selectedGroup!));
|
||||
_selectedGroup = null;
|
||||
}
|
||||
|
||||
if( dirCreated && optionFolderName != null )
|
||||
{
|
||||
Directory.Delete( optionFolderName.FullName, true );
|
||||
}
|
||||
if (dirCreated && optionFolderName != null)
|
||||
Directory.Delete(optionFolderName.FullName, true);
|
||||
}
|
||||
catch
|
||||
{
|
||||
|
|
@ -307,12 +314,12 @@ public class ItemSwapWindow : IDisposable
|
|||
UpdateOption();
|
||||
}
|
||||
|
||||
private void DrawHeaderLine( float width )
|
||||
private void DrawHeaderLine(float width)
|
||||
{
|
||||
var newModAvailable = _loadException == null && _swapData.Loaded;
|
||||
|
||||
ImGui.SetNextItemWidth( width );
|
||||
if( ImGui.InputTextWithHint( "##newModName", "New Mod Name...", ref _newModName, 64 ) )
|
||||
ImGui.SetNextItemWidth(width);
|
||||
if (ImGui.InputTextWithHint("##newModName", "New Mod Name...", ref _newModName, 64))
|
||||
{ }
|
||||
|
||||
ImGui.SameLine();
|
||||
|
|
@ -321,29 +328,23 @@ public class ItemSwapWindow : IDisposable
|
|||
: _newModName.Length == 0
|
||||
? "Please enter a name for your mod."
|
||||
: "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();
|
||||
}
|
||||
|
||||
ImGui.SameLine();
|
||||
ImGui.SetCursorPosX( ImGui.GetCursorPosX() + 20 * ImGuiHelpers.GlobalScale );
|
||||
ImGui.Checkbox( "Use File Swaps", ref _useFileSwaps );
|
||||
ImGuiUtil.HoverTooltip( "Instead of writing every single non-default file to the newly created mod or option,\n"
|
||||
+ "even those available from game files, use File Swaps to default game files where possible." );
|
||||
ImGui.SetCursorPosX(ImGui.GetCursorPosX() + 20 * ImGuiHelpers.GlobalScale);
|
||||
ImGui.Checkbox("Use File Swaps", ref _useFileSwaps);
|
||||
ImGuiUtil.HoverTooltip("Instead of writing every single non-default file to the newly created mod or option,\n"
|
||||
+ "even those available from game files, use File Swaps to default game files where possible.");
|
||||
|
||||
ImGui.SetNextItemWidth( ( width - ImGui.GetStyle().ItemSpacing.X ) / 2 );
|
||||
if( ImGui.InputTextWithHint( "##groupName", "Group Name...", ref _newGroupName, 32 ) )
|
||||
{
|
||||
ImGui.SetNextItemWidth((width - ImGui.GetStyle().ItemSpacing.X) / 2);
|
||||
if (ImGui.InputTextWithHint("##groupName", "Group Name...", ref _newGroupName, 32))
|
||||
UpdateOption();
|
||||
}
|
||||
|
||||
ImGui.SameLine();
|
||||
ImGui.SetNextItemWidth( ( width - ImGui.GetStyle().ItemSpacing.X ) / 2 );
|
||||
if( ImGui.InputTextWithHint( "##optionName", "New Option Name...", ref _newOptionName, 32 ) )
|
||||
{
|
||||
ImGui.SetNextItemWidth((width - ImGui.GetStyle().ItemSpacing.X) / 2);
|
||||
if (ImGui.InputTextWithHint("##optionName", "New Option Name...", ref _newOptionName, 32))
|
||||
UpdateOption();
|
||||
}
|
||||
|
||||
ImGui.SameLine();
|
||||
tt = !_subModValid
|
||||
|
|
@ -351,16 +352,15 @@ public class ItemSwapWindow : IDisposable
|
|||
: !newModAvailable
|
||||
? "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.";
|
||||
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();
|
||||
}
|
||||
|
||||
ImGui.SameLine();
|
||||
ImGui.SetCursorPosX( ImGui.GetCursorPosX() + 20 * ImGuiHelpers.GlobalScale );
|
||||
_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"
|
||||
+ "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." );
|
||||
ImGui.SetCursorPosX(ImGui.GetCursorPosX() + 20 * ImGuiHelpers.GlobalScale);
|
||||
_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"
|
||||
+ "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.");
|
||||
}
|
||||
|
||||
private void DrawSwapBar()
|
||||
|
|
@ -491,61 +491,58 @@ public class ItemSwapWindow : IDisposable
|
|||
return (article1, article2, source ? tuple.Source : tuple.Target);
|
||||
}
|
||||
|
||||
private void DrawEquipmentSwap( SwapType type )
|
||||
private void DrawEquipmentSwap(SwapType type)
|
||||
{
|
||||
using var tab = DrawTab( type );
|
||||
if( !tab )
|
||||
{
|
||||
using var tab = DrawTab(type);
|
||||
if (!tab)
|
||||
return;
|
||||
}
|
||||
|
||||
var (sourceSelector, targetSelector, text1, text2) = _selectors[ type ];
|
||||
using var table = ImRaii.Table( "##settings", 2, ImGuiTableFlags.SizingFixedFit );
|
||||
var (sourceSelector, targetSelector, text1, text2) = _selectors[type];
|
||||
using var table = ImRaii.Table("##settings", 2, ImGuiTableFlags.SizingFixedFit);
|
||||
ImGui.TableNextColumn();
|
||||
ImGui.AlignTextToFramePadding();
|
||||
ImGui.TextUnformatted( text1 );
|
||||
ImGui.TextUnformatted(text1);
|
||||
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)
|
||||
{
|
||||
ImGui.SameLine();
|
||||
_dirty |= ImGui.Checkbox( "Swap Right Ring", ref _useRightRing );
|
||||
_dirty |= ImGui.Checkbox("Swap Right Ring", ref _useRightRing);
|
||||
}
|
||||
|
||||
ImGui.TableNextColumn();
|
||||
ImGui.AlignTextToFramePadding();
|
||||
ImGui.TextUnformatted( text2 );
|
||||
ImGui.TextUnformatted(text2);
|
||||
ImGui.TableNextColumn();
|
||||
_dirty |= targetSelector.Draw( "##itemTarget", targetSelector.CurrentSelection.Item1 ?? string.Empty, string.Empty, InputWidth * 2, ImGui.GetTextLineHeightWithSpacing() );
|
||||
if( type == SwapType.Ring )
|
||||
_dirty |= targetSelector.Draw("##itemTarget", targetSelector.CurrentSelection.Item1 ?? string.Empty, string.Empty, InputWidth * 2,
|
||||
ImGui.GetTextLineHeightWithSpacing());
|
||||
if (type == SwapType.Ring)
|
||||
{
|
||||
ImGui.SameLine();
|
||||
_dirty |= ImGui.Checkbox( "Swap Left Ring", ref _useLeftRing );
|
||||
_dirty |= ImGui.Checkbox("Swap Left Ring", ref _useLeftRing);
|
||||
}
|
||||
|
||||
if( _affectedItems is { Length: > 1 } )
|
||||
if (_affectedItems is { Length: > 1 })
|
||||
{
|
||||
ImGui.SameLine();
|
||||
ImGuiUtil.DrawTextButton( $"which will also affect {_affectedItems.Length - 1} other Items.", Vector2.Zero, Colors.PressEnterWarningBg );
|
||||
if( ImGui.IsItemHovered() )
|
||||
{
|
||||
ImGui.SetTooltip( string.Join( '\n', _affectedItems.Where( i => !ReferenceEquals( i, targetSelector.CurrentSelection.Item2 ) )
|
||||
.Select( i => i.Name.ToDalamudString().TextValue ) ) );
|
||||
}
|
||||
ImGuiUtil.DrawTextButton($"which will also affect {_affectedItems.Length - 1} other Items.", Vector2.Zero,
|
||||
Colors.PressEnterWarningBg);
|
||||
if (ImGui.IsItemHovered())
|
||||
ImGui.SetTooltip(string.Join('\n', _affectedItems.Where(i => !ReferenceEquals(i, targetSelector.CurrentSelection.Item2))
|
||||
.Select(i => i.Name.ToDalamudString().TextValue)));
|
||||
}
|
||||
}
|
||||
|
||||
private void DrawHairSwap()
|
||||
{
|
||||
using var tab = DrawTab( SwapType.Hair );
|
||||
if( !tab )
|
||||
{
|
||||
using var tab = DrawTab(SwapType.Hair);
|
||||
if (!tab)
|
||||
return;
|
||||
}
|
||||
|
||||
using var table = ImRaii.Table( "##settings", 2, ImGuiTableFlags.SizingFixedFit );
|
||||
DrawTargetIdInput( "Take this Hairstyle" );
|
||||
using var table = ImRaii.Table("##settings", 2, ImGuiTableFlags.SizingFixedFit);
|
||||
DrawTargetIdInput("Take this Hairstyle");
|
||||
DrawSourceIdInput();
|
||||
DrawGenderInput();
|
||||
}
|
||||
|
|
@ -553,145 +550,139 @@ public class ItemSwapWindow : IDisposable
|
|||
private void DrawFaceSwap()
|
||||
{
|
||||
using var disabled = ImRaii.Disabled();
|
||||
using var tab = DrawTab( SwapType.Face );
|
||||
if( !tab )
|
||||
{
|
||||
using var tab = DrawTab(SwapType.Face);
|
||||
if (!tab)
|
||||
return;
|
||||
}
|
||||
|
||||
using var table = ImRaii.Table( "##settings", 2, ImGuiTableFlags.SizingFixedFit );
|
||||
DrawTargetIdInput( "Take this Face Type" );
|
||||
using var table = ImRaii.Table("##settings", 2, ImGuiTableFlags.SizingFixedFit);
|
||||
DrawTargetIdInput("Take this Face Type");
|
||||
DrawSourceIdInput();
|
||||
DrawGenderInput();
|
||||
}
|
||||
|
||||
private void DrawTailSwap()
|
||||
{
|
||||
using var tab = DrawTab( SwapType.Tail );
|
||||
if( !tab )
|
||||
{
|
||||
using var tab = DrawTab(SwapType.Tail);
|
||||
if (!tab)
|
||||
return;
|
||||
}
|
||||
|
||||
using var table = ImRaii.Table( "##settings", 2, ImGuiTableFlags.SizingFixedFit );
|
||||
DrawTargetIdInput( "Take this Tail Type" );
|
||||
using var table = ImRaii.Table("##settings", 2, ImGuiTableFlags.SizingFixedFit);
|
||||
DrawTargetIdInput("Take this Tail Type");
|
||||
DrawSourceIdInput();
|
||||
DrawGenderInput( "for all", 2 );
|
||||
DrawGenderInput("for all", 2);
|
||||
}
|
||||
|
||||
|
||||
private void DrawEarSwap()
|
||||
{
|
||||
using var tab = DrawTab( SwapType.Ears );
|
||||
if( !tab )
|
||||
{
|
||||
using var tab = DrawTab(SwapType.Ears);
|
||||
if (!tab)
|
||||
return;
|
||||
}
|
||||
|
||||
using var table = ImRaii.Table( "##settings", 2, ImGuiTableFlags.SizingFixedFit );
|
||||
DrawTargetIdInput( "Take this Ear Type" );
|
||||
using var table = ImRaii.Table("##settings", 2, ImGuiTableFlags.SizingFixedFit);
|
||||
DrawTargetIdInput("Take this Ear Type");
|
||||
DrawSourceIdInput();
|
||||
DrawGenderInput( "for all Viera", 0 );
|
||||
DrawGenderInput("for all Viera", 0);
|
||||
}
|
||||
|
||||
|
||||
private void DrawWeaponSwap()
|
||||
{
|
||||
using var disabled = ImRaii.Disabled();
|
||||
using var tab = DrawTab( SwapType.Weapon );
|
||||
if( !tab )
|
||||
{
|
||||
using var tab = DrawTab(SwapType.Weapon);
|
||||
if (!tab)
|
||||
return;
|
||||
}
|
||||
|
||||
using var table = ImRaii.Table( "##settings", 2, ImGuiTableFlags.SizingFixedFit );
|
||||
using var table = ImRaii.Table("##settings", 2, ImGuiTableFlags.SizingFixedFit);
|
||||
ImGui.TableNextColumn();
|
||||
ImGui.AlignTextToFramePadding();
|
||||
ImGui.TextUnformatted( "Select the weapon or tool you want" );
|
||||
ImGui.TextUnformatted("Select the weapon or tool you want");
|
||||
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;
|
||||
_weaponSource = new ItemSelector( _slotSelector.CurrentSelection );
|
||||
_weaponTarget = new ItemSelector( _slotSelector.CurrentSelection );
|
||||
_weaponSource = new ItemSelector(_slotSelector.CurrentSelection);
|
||||
_weaponTarget = new ItemSelector(_slotSelector.CurrentSelection);
|
||||
}
|
||||
else
|
||||
{
|
||||
_dirty = _weaponSource == null || _weaponTarget == null;
|
||||
_weaponSource ??= new ItemSelector( _slotSelector.CurrentSelection );
|
||||
_weaponTarget ??= new ItemSelector( _slotSelector.CurrentSelection );
|
||||
_weaponSource ??= new ItemSelector(_slotSelector.CurrentSelection);
|
||||
_weaponTarget ??= new ItemSelector(_slotSelector.CurrentSelection);
|
||||
}
|
||||
|
||||
ImGui.TableNextColumn();
|
||||
ImGui.AlignTextToFramePadding();
|
||||
ImGui.TextUnformatted( "and put this variant of it" );
|
||||
ImGui.TextUnformatted("and put this variant of it");
|
||||
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.AlignTextToFramePadding();
|
||||
ImGui.TextUnformatted( "onto this one" );
|
||||
ImGui.TextUnformatted("onto this one");
|
||||
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 void DrawTargetIdInput( string text = "Take this ID" )
|
||||
private void DrawTargetIdInput(string text = "Take this ID")
|
||||
{
|
||||
ImGui.TableNextColumn();
|
||||
ImGui.AlignTextToFramePadding();
|
||||
ImGui.TextUnformatted( text );
|
||||
ImGui.TextUnformatted(text);
|
||||
|
||||
ImGui.TableNextColumn();
|
||||
ImGui.SetNextItemWidth( InputWidth * ImGuiHelpers.GlobalScale );
|
||||
if( ImGui.InputInt( "##targetId", ref _targetId, 0, 0 ) )
|
||||
{
|
||||
_targetId = Math.Clamp( _targetId, 0, byte.MaxValue );
|
||||
}
|
||||
ImGui.SetNextItemWidth(InputWidth * ImGuiHelpers.GlobalScale);
|
||||
if (ImGui.InputInt("##targetId", ref _targetId, 0, 0))
|
||||
_targetId = Math.Clamp(_targetId, 0, byte.MaxValue);
|
||||
|
||||
_dirty |= ImGui.IsItemDeactivatedAfterEdit();
|
||||
}
|
||||
|
||||
private void DrawSourceIdInput( string text = "and put it on this one" )
|
||||
private void DrawSourceIdInput(string text = "and put it on this one")
|
||||
{
|
||||
ImGui.TableNextColumn();
|
||||
ImGui.AlignTextToFramePadding();
|
||||
ImGui.TextUnformatted( text );
|
||||
ImGui.TextUnformatted(text);
|
||||
|
||||
ImGui.TableNextColumn();
|
||||
ImGui.SetNextItemWidth( InputWidth * ImGuiHelpers.GlobalScale );
|
||||
if( ImGui.InputInt( "##sourceId", ref _sourceId, 0, 0 ) )
|
||||
{
|
||||
_sourceId = Math.Clamp( _sourceId, 0, byte.MaxValue );
|
||||
}
|
||||
ImGui.SetNextItemWidth(InputWidth * ImGuiHelpers.GlobalScale);
|
||||
if (ImGui.InputInt("##sourceId", ref _sourceId, 0, 0))
|
||||
_sourceId = Math.Clamp(_sourceId, 0, byte.MaxValue);
|
||||
|
||||
_dirty |= ImGui.IsItemDeactivatedAfterEdit();
|
||||
}
|
||||
|
||||
private void DrawGenderInput( string text = "for all", int drawRace = 1 )
|
||||
private void DrawGenderInput(string text = "for all", int drawRace = 1)
|
||||
{
|
||||
ImGui.TableNextColumn();
|
||||
ImGui.AlignTextToFramePadding();
|
||||
ImGui.TextUnformatted( text );
|
||||
ImGui.TextUnformatted(text);
|
||||
|
||||
ImGui.TableNextColumn();
|
||||
_dirty |= Combos.Gender( "##Gender", InputWidth, _currentGender, out _currentGender );
|
||||
if( drawRace == 1 )
|
||||
_dirty |= Combos.Gender("##Gender", InputWidth, _currentGender, out _currentGender);
|
||||
if (drawRace == 1)
|
||||
{
|
||||
ImGui.SameLine();
|
||||
_dirty |= Combos.Race( "##Race", InputWidth, _currentRace, out _currentRace );
|
||||
_dirty |= Combos.Race("##Race", InputWidth, _currentRace, out _currentRace);
|
||||
}
|
||||
else if( drawRace == 2 )
|
||||
else if (drawRace == 2)
|
||||
{
|
||||
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;
|
||||
}
|
||||
|
||||
_dirty |= ImGuiUtil.GenericEnumCombo( "##Race", InputWidth, _currentRace, out _currentRace, new[] { ModelRace.Miqote, ModelRace.AuRa, ModelRace.Hrothgar },
|
||||
RaceEnumExtensions.ToName );
|
||||
_dirty |= ImGuiUtil.GenericEnumCombo("##Race", InputWidth, _currentRace, out _currentRace, new[]
|
||||
{
|
||||
ModelRace.Miqote,
|
||||
ModelRace.AuRa,
|
||||
ModelRace.Hrothgar,
|
||||
},
|
||||
RaceEnumExtensions.ToName);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -718,72 +709,54 @@ public class ItemSwapWindow : IDisposable
|
|||
|
||||
public void DrawItemSwapPanel()
|
||||
{
|
||||
using var tab = ImRaii.TabItem( "Item Swap (WIP)" );
|
||||
if( !tab )
|
||||
{
|
||||
using var tab = ImRaii.TabItem("Item Swap (WIP)");
|
||||
if (!tab)
|
||||
return;
|
||||
}
|
||||
|
||||
ImGui.NewLine();
|
||||
DrawHeaderLine( 300 * ImGuiHelpers.GlobalScale );
|
||||
DrawHeaderLine(300 * ImGuiHelpers.GlobalScale);
|
||||
ImGui.NewLine();
|
||||
|
||||
DrawSwapBar();
|
||||
|
||||
using var table = ImRaii.ListBox( "##swaps", -Vector2.One );
|
||||
if( _loadException != null )
|
||||
{
|
||||
ImGuiUtil.TextWrapped( $"Could not load Customization Swap:\n{_loadException}" );
|
||||
}
|
||||
else if( _swapData.Loaded )
|
||||
{
|
||||
foreach( var swap in _swapData.Swaps )
|
||||
{
|
||||
DrawSwap( swap );
|
||||
}
|
||||
}
|
||||
using var table = ImRaii.ListBox("##swaps", -Vector2.One);
|
||||
if (_loadException != null)
|
||||
ImGuiUtil.TextWrapped($"Could not load Customization Swap:\n{_loadException}");
|
||||
else if (_swapData.Loaded)
|
||||
foreach (var swap in _swapData.Swaps)
|
||||
DrawSwap(swap);
|
||||
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;
|
||||
using var tree = ImRaii.TreeNode( SwapToString( swap ), flags );
|
||||
if( !tree )
|
||||
{
|
||||
using var tree = ImRaii.TreeNode(SwapToString(swap), flags);
|
||||
if (!tree)
|
||||
return;
|
||||
}
|
||||
|
||||
foreach( var child in swap.ChildSwaps )
|
||||
{
|
||||
DrawSwap( child );
|
||||
}
|
||||
foreach (var child in swap.ChildSwaps)
|
||||
DrawSwap(child);
|
||||
}
|
||||
|
||||
private void OnCollectionChange( CollectionType collectionType, ModCollection? oldCollection,
|
||||
ModCollection? newCollection, string _ )
|
||||
private void OnCollectionChange(CollectionType collectionType, ModCollection? oldCollection,
|
||||
ModCollection? newCollection, string _)
|
||||
{
|
||||
if( collectionType != CollectionType.Current || _mod == null || newCollection == null )
|
||||
{
|
||||
if (collectionType != CollectionType.Current || _mod == null || newCollection == null)
|
||||
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;
|
||||
if( oldCollection != null )
|
||||
{
|
||||
if (oldCollection != null)
|
||||
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)
|
||||
{
|
||||
if( modIdx == _mod?.Index )
|
||||
if (modIdx == _mod?.Index)
|
||||
{
|
||||
_swapData.LoadMod( _mod, _modSettings );
|
||||
_swapData.LoadMod(_mod, _modSettings);
|
||||
_dirty = true;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -97,7 +97,7 @@ public partial class ModEditWindow
|
|||
|
||||
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.";
|
||||
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 )
|
||||
{
|
||||
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();
|
||||
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;
|
||||
}
|
||||
|
||||
|
|
@ -355,10 +355,10 @@ public partial class ModEditWindow
|
|||
ImGui.TableNextColumn();
|
||||
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 ) )
|
||||
{
|
||||
file.ColorDyeSets[ colorSetIdx ].Rows[ rowIdx ].Template = Penumbra.StainManager.TemplateCombo.CurrentSelection;
|
||||
file.ColorDyeSets[ colorSetIdx ].Rows[ rowIdx ].Template = Penumbra.StainService.TemplateCombo.CurrentSelection;
|
||||
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 )
|
||||
{
|
||||
var stain = Penumbra.StainManager.StainCombo.CurrentSelection.Key;
|
||||
if( stain == 0 || !Penumbra.StainManager.StmFile.Entries.TryGetValue( dye.Template, out var entry ) )
|
||||
var stain = Penumbra.StainService.StainCombo.CurrentSelection.Key;
|
||||
if( stain == 0 || !Penumbra.StainService.StmFile.Entries.TryGetValue( dye.Template, out var entry ) )
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
|
@ -390,7 +390,7 @@ public partial class ModEditWindow
|
|||
var ret = ImGuiUtil.DrawDisabledButton( FontAwesomeIcon.PaintBrush.ToIconString(), new Vector2( ImGui.GetFrameHeight() ),
|
||||
"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();
|
||||
ColorPicker( "##diffusePreview", string.Empty, values.Diffuse, _ => { }, "D" );
|
||||
|
|
|
|||
|
|
@ -13,6 +13,7 @@ using Penumbra.GameData.Enums;
|
|||
using Penumbra.GameData.Files;
|
||||
using Penumbra.Import.Textures;
|
||||
using Penumbra.Mods;
|
||||
using Penumbra.Services;
|
||||
using Penumbra.String.Classes;
|
||||
using Penumbra.Util;
|
||||
using static Penumbra.Mods.Mod;
|
||||
|
|
@ -22,7 +23,7 @@ namespace Penumbra.UI.Classes;
|
|||
public partial class ModEditWindow : Window, IDisposable
|
||||
{
|
||||
private const string WindowBaseLabel = "###SubModEdit";
|
||||
internal readonly ItemSwapWindow _swapWindow = new();
|
||||
internal readonly ItemSwapWindow _swapWindow;
|
||||
|
||||
private Editor? _editor;
|
||||
private Mod? _mod;
|
||||
|
|
@ -567,9 +568,10 @@ public partial class ModEditWindow : Window, IDisposable
|
|||
return new FullPath( path );
|
||||
}
|
||||
|
||||
public ModEditWindow()
|
||||
public ModEditWindow(CommunicatorService communicator)
|
||||
: base( WindowBaseLabel )
|
||||
{
|
||||
{
|
||||
_swapWindow = new ItemSwapWindow( communicator );
|
||||
_materialTab = new FileEditor< MtrlTab >( "Materials", ".mtrl",
|
||||
() => _editor?.MtrlFiles ?? Array.Empty< Editor.FileRegistry >(),
|
||||
DrawMaterialPanel,
|
||||
|
|
|
|||
|
|
@ -14,41 +14,43 @@ using System.IO;
|
|||
using System.Linq;
|
||||
using System.Numerics;
|
||||
using Penumbra.Api.Enums;
|
||||
using Penumbra.Services;
|
||||
|
||||
using Penumbra.Services;
|
||||
|
||||
namespace Penumbra.UI.Classes;
|
||||
|
||||
public sealed partial class ModFileSystemSelector : FileSystemSelector< Mod, ModFileSystemSelector.ModState >
|
||||
public sealed partial class ModFileSystemSelector : FileSystemSelector<Mod, ModFileSystemSelector.ModState>
|
||||
{
|
||||
private readonly FileDialogManager _fileManager = ConfigWindow.SetupFileManager();
|
||||
private TexToolsImporter? _import;
|
||||
public ModSettings SelectedSettings { get; private set; } = ModSettings.Empty;
|
||||
public ModCollection SelectedSettingCollection { get; private set; } = ModCollection.Empty;
|
||||
private readonly CommunicatorService _communicator;
|
||||
private readonly FileDialogManager _fileManager = ConfigWindow.SetupFileManager();
|
||||
private TexToolsImporter? _import;
|
||||
public ModSettings SelectedSettings { get; private set; } = ModSettings.Empty;
|
||||
public ModCollection SelectedSettingCollection { get; private set; } = ModCollection.Empty;
|
||||
|
||||
public ModFileSystemSelector( ModFileSystem fileSystem )
|
||||
: base( fileSystem, DalamudServices.KeyState )
|
||||
public ModFileSystemSelector(CommunicatorService communicator, ModFileSystem fileSystem)
|
||||
: base(fileSystem, DalamudServices.KeyState)
|
||||
{
|
||||
SubscribeRightClickFolder( EnableDescendants, 10 );
|
||||
SubscribeRightClickFolder( DisableDescendants, 10 );
|
||||
SubscribeRightClickFolder( InheritDescendants, 15 );
|
||||
SubscribeRightClickFolder( OwnDescendants, 15 );
|
||||
SubscribeRightClickFolder( SetDefaultImportFolder, 100 );
|
||||
SubscribeRightClickLeaf( ToggleLeafFavorite, 0 );
|
||||
SubscribeRightClickMain( ClearDefaultImportFolder, 100 );
|
||||
AddButton( AddNewModButton, 0 );
|
||||
AddButton( AddImportModButton, 1 );
|
||||
AddButton( AddHelpButton, 2 );
|
||||
AddButton( DeleteModButton, 1000 );
|
||||
_communicator = communicator;
|
||||
SubscribeRightClickFolder(EnableDescendants, 10);
|
||||
SubscribeRightClickFolder(DisableDescendants, 10);
|
||||
SubscribeRightClickFolder(InheritDescendants, 15);
|
||||
SubscribeRightClickFolder(OwnDescendants, 15);
|
||||
SubscribeRightClickFolder(SetDefaultImportFolder, 100);
|
||||
SubscribeRightClickLeaf(ToggleLeafFavorite, 0);
|
||||
SubscribeRightClickMain(ClearDefaultImportFolder, 100);
|
||||
AddButton(AddNewModButton, 0);
|
||||
AddButton(AddImportModButton, 1);
|
||||
AddButton(AddHelpButton, 2);
|
||||
AddButton(DeleteModButton, 1000);
|
||||
SetFilterTooltip();
|
||||
|
||||
SelectionChanged += OnSelectionChange;
|
||||
Penumbra.CollectionManager.CollectionChanged += OnCollectionChange;
|
||||
_communicator.CollectionChange.Event += OnCollectionChange;
|
||||
Penumbra.CollectionManager.Current.ModSettingChanged += OnSettingChange;
|
||||
Penumbra.CollectionManager.Current.InheritanceChanged += OnInheritanceChange;
|
||||
Penumbra.ModManager.ModDataChanged += OnModDataChange;
|
||||
Penumbra.ModManager.ModDiscoveryStarted += StoreCurrentSelection;
|
||||
Penumbra.ModManager.ModDiscoveryFinished += RestoreLastSelection;
|
||||
OnCollectionChange( CollectionType.Current, null, Penumbra.CollectionManager.Current, "" );
|
||||
OnCollectionChange(CollectionType.Current, null, Penumbra.CollectionManager.Current, "");
|
||||
}
|
||||
|
||||
public override void Dispose()
|
||||
|
|
@ -59,7 +61,7 @@ public sealed partial class ModFileSystemSelector : FileSystemSelector< Mod, Mod
|
|||
Penumbra.ModManager.ModDataChanged -= OnModDataChange;
|
||||
Penumbra.CollectionManager.Current.ModSettingChanged -= OnSettingChange;
|
||||
Penumbra.CollectionManager.Current.InheritanceChanged -= OnInheritanceChange;
|
||||
Penumbra.CollectionManager.CollectionChanged -= OnCollectionChange;
|
||||
_communicator.CollectionChange.Event -= OnCollectionChange;
|
||||
_import?.Dispose();
|
||||
_import = null;
|
||||
}
|
||||
|
|
@ -68,7 +70,7 @@ public sealed partial class ModFileSystemSelector : FileSystemSelector< Mod, Mod
|
|||
=> base.SelectedLeaf;
|
||||
|
||||
// Customization points.
|
||||
public override ISortMode< Mod > SortMode
|
||||
public override ISortMode<Mod> SortMode
|
||||
=> Penumbra.Config.SortMode;
|
||||
|
||||
protected override uint ExpandedFolderColor
|
||||
|
|
@ -89,91 +91,79 @@ public sealed partial class ModFileSystemSelector : FileSystemSelector< Mod, Mod
|
|||
DrawHelpPopup();
|
||||
DrawInfoPopup();
|
||||
|
||||
if( ImGuiUtil.OpenNameField( "Create New Mod", ref _newModName ) )
|
||||
{
|
||||
if (ImGuiUtil.OpenNameField("Create New Mod", ref _newModName))
|
||||
try
|
||||
{
|
||||
var newDir = Mod.Creator.CreateModFolder( Penumbra.ModManager.BasePath, _newModName );
|
||||
Mod.Creator.CreateMeta( newDir, _newModName, Penumbra.Config.DefaultModAuthor, string.Empty, "1.0", string.Empty );
|
||||
Mod.Creator.CreateDefaultFiles( newDir );
|
||||
Penumbra.ModManager.AddMod( newDir );
|
||||
var newDir = Mod.Creator.CreateModFolder(Penumbra.ModManager.BasePath, _newModName);
|
||||
Mod.Creator.CreateMeta(newDir, _newModName, Penumbra.Config.DefaultModAuthor, string.Empty, "1.0", string.Empty);
|
||||
Mod.Creator.CreateDefaultFiles(newDir);
|
||||
Penumbra.ModManager.AddMod(newDir);
|
||||
_newModName = string.Empty;
|
||||
}
|
||||
catch( Exception e )
|
||||
catch (Exception e)
|
||||
{
|
||||
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))
|
||||
{
|
||||
Penumbra.ModManager.AddMod( dir );
|
||||
Penumbra.ModManager.AddMod(dir);
|
||||
var mod = Penumbra.ModManager.LastOrDefault();
|
||||
if( mod != null )
|
||||
if (mod != null)
|
||||
{
|
||||
MoveModToDefaultDirectory( mod );
|
||||
SelectByValue( mod );
|
||||
MoveModToDefaultDirectory(mod);
|
||||
SelectByValue(mod);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected override void DrawLeafName( FileSystem< Mod >.Leaf leaf, in ModState state, bool selected )
|
||||
protected override void DrawLeafName(FileSystem<Mod>.Leaf leaf, in ModState state, bool selected)
|
||||
{
|
||||
var flags = selected ? ImGuiTreeNodeFlags.Selected | LeafFlags : LeafFlags;
|
||||
using var c = ImRaii.PushColor( ImGuiCol.Text, state.Color.Value() )
|
||||
.Push( ImGuiCol.HeaderHovered, 0x4000FFFF, leaf.Value.Favorite );
|
||||
using var id = ImRaii.PushId( leaf.Value.Index );
|
||||
ImRaii.TreeNode( leaf.Value.Name, flags ).Dispose();
|
||||
using var c = ImRaii.PushColor(ImGuiCol.Text, state.Color.Value())
|
||||
.Push(ImGuiCol.HeaderHovered, 0x4000FFFF, leaf.Value.Favorite);
|
||||
using var id = ImRaii.PushId(leaf.Value.Index);
|
||||
ImRaii.TreeNode(leaf.Value.Name, flags).Dispose();
|
||||
}
|
||||
|
||||
|
||||
// Add custom context menu items.
|
||||
private static void EnableDescendants( ModFileSystem.Folder folder )
|
||||
private static void EnableDescendants(ModFileSystem.Folder folder)
|
||||
{
|
||||
if( ImGui.MenuItem( "Enable Descendants" ) )
|
||||
{
|
||||
SetDescendants( folder, true );
|
||||
}
|
||||
if (ImGui.MenuItem("Enable Descendants"))
|
||||
SetDescendants(folder, true);
|
||||
}
|
||||
|
||||
private static void DisableDescendants( ModFileSystem.Folder folder )
|
||||
private static void DisableDescendants(ModFileSystem.Folder folder)
|
||||
{
|
||||
if( ImGui.MenuItem( "Disable Descendants" ) )
|
||||
{
|
||||
SetDescendants( folder, false );
|
||||
}
|
||||
if (ImGui.MenuItem("Disable Descendants"))
|
||||
SetDescendants(folder, false);
|
||||
}
|
||||
|
||||
private static void InheritDescendants( ModFileSystem.Folder folder )
|
||||
private static void InheritDescendants(ModFileSystem.Folder folder)
|
||||
{
|
||||
if( ImGui.MenuItem( "Inherit Descendants" ) )
|
||||
{
|
||||
SetDescendants( folder, true, true );
|
||||
}
|
||||
if (ImGui.MenuItem("Inherit Descendants"))
|
||||
SetDescendants(folder, true, true);
|
||||
}
|
||||
|
||||
private static void OwnDescendants( ModFileSystem.Folder folder )
|
||||
private static void OwnDescendants(ModFileSystem.Folder folder)
|
||||
{
|
||||
if( ImGui.MenuItem( "Stop Inheriting Descendants" ) )
|
||||
{
|
||||
SetDescendants( folder, false, true );
|
||||
}
|
||||
if (ImGui.MenuItem("Stop Inheriting Descendants"))
|
||||
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" ) )
|
||||
{
|
||||
Penumbra.ModManager.ChangeModFavorite( mod.Value.Index, !mod.Value.Favorite );
|
||||
}
|
||||
if (ImGui.MenuItem(mod.Value.Favorite ? "Remove Favorite" : "Mark as Favorite"))
|
||||
Penumbra.ModManager.ChangeModFavorite(mod.Value.Index, !mod.Value.Favorite);
|
||||
}
|
||||
|
||||
private static void SetDefaultImportFolder( ModFileSystem.Folder folder )
|
||||
private static void SetDefaultImportFolder(ModFileSystem.Folder folder)
|
||||
{
|
||||
if( ImGui.MenuItem( "Set As Default Import Folder" ) )
|
||||
if (ImGui.MenuItem("Set As Default Import Folder"))
|
||||
{
|
||||
var newName = folder.FullName();
|
||||
if( newName != Penumbra.Config.DefaultImportFolder )
|
||||
if (newName != Penumbra.Config.DefaultImportFolder)
|
||||
{
|
||||
Penumbra.Config.DefaultImportFolder = newName;
|
||||
Penumbra.Config.Save();
|
||||
|
|
@ -183,7 +173,7 @@ public sealed partial class ModFileSystemSelector : FileSystemSelector< Mod, Mod
|
|||
|
||||
private static void ClearDefaultImportFolder()
|
||||
{
|
||||
if( ImGui.MenuItem( "Clear Default Import Folder" ) && Penumbra.Config.DefaultImportFolder.Length > 0 )
|
||||
if (ImGui.MenuItem("Clear Default Import Folder") && Penumbra.Config.DefaultImportFolder.Length > 0)
|
||||
{
|
||||
Penumbra.Config.DefaultImportFolder = string.Empty;
|
||||
Penumbra.Config.Save();
|
||||
|
|
@ -194,71 +184,63 @@ public sealed partial class ModFileSystemSelector : FileSystemSelector< Mod, Mod
|
|||
// Add custom buttons.
|
||||
private string _newModName = string.Empty;
|
||||
|
||||
private static void AddNewModButton( Vector2 size )
|
||||
private static void AddNewModButton(Vector2 size)
|
||||
{
|
||||
if( ImGuiUtil.DrawDisabledButton( FontAwesomeIcon.Plus.ToIconString(), size, "Create a new, empty mod of a given name.",
|
||||
!Penumbra.ModManager.Valid, true ) )
|
||||
{
|
||||
ImGui.OpenPopup( "Create New Mod" );
|
||||
}
|
||||
if (ImGuiUtil.DrawDisabledButton(FontAwesomeIcon.Plus.ToIconString(), size, "Create a new, empty mod of a given name.",
|
||||
!Penumbra.ModManager.Valid, true))
|
||||
ImGui.OpenPopup("Create New Mod");
|
||||
}
|
||||
|
||||
// Add an import mods button that opens a file selector.
|
||||
// Only set the initial directory once.
|
||||
private bool _hasSetFolder;
|
||||
|
||||
private void AddImportModButton( Vector2 size )
|
||||
private void AddImportModButton(Vector2 size)
|
||||
{
|
||||
var button = ImGuiUtil.DrawDisabledButton( FontAwesomeIcon.FileImport.ToIconString(), size,
|
||||
"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 );
|
||||
if( !button )
|
||||
{
|
||||
var button = ImGuiUtil.DrawDisabledButton(FontAwesomeIcon.FileImport.ToIconString(), size,
|
||||
"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);
|
||||
if (!button)
|
||||
return;
|
||||
}
|
||||
|
||||
var modPath = _hasSetFolder && !Penumbra.Config.AlwaysOpenDefaultImport ? null
|
||||
: Penumbra.Config.DefaultModImportPath.Length > 0 ? Penumbra.Config.DefaultModImportPath
|
||||
: Penumbra.Config.ModDirectory.Length > 0 ? Penumbra.Config.ModDirectory : null;
|
||||
: Penumbra.Config.ModDirectory.Length > 0 ? Penumbra.Config.ModDirectory : null;
|
||||
_hasSetFolder = true;
|
||||
|
||||
_fileManager.OpenFileDialog( "Import Mod Pack",
|
||||
"Mod Packs{.ttmp,.ttmp2,.pmp},TexTools Mod Packs{.ttmp,.ttmp2},Penumbra Mod Packs{.pmp},Archives{.zip,.7z,.rar}", ( s, f ) =>
|
||||
_fileManager.OpenFileDialog("Import Mod Pack",
|
||||
"Mod Packs{.ttmp,.ttmp2,.pmp},TexTools Mod Packs{.ttmp,.ttmp2},Penumbra Mod Packs{.pmp},Archives{.zip,.7z,.rar}", (s, f) =>
|
||||
{
|
||||
if( s )
|
||||
if (s)
|
||||
{
|
||||
_import = new TexToolsImporter( Penumbra.ModManager.BasePath, f.Count, f.Select( file => new FileInfo( file ) ),
|
||||
AddNewMod );
|
||||
ImGui.OpenPopup( "Import Status" );
|
||||
_import = new TexToolsImporter(Penumbra.ModManager.BasePath, f.Count, f.Select(file => new FileInfo(file)),
|
||||
AddNewMod);
|
||||
ImGui.OpenPopup("Import Status");
|
||||
}
|
||||
}, 0, modPath );
|
||||
}, 0, modPath);
|
||||
}
|
||||
|
||||
// Draw the progress information for import.
|
||||
private void DrawInfoPopup()
|
||||
{
|
||||
var display = ImGui.GetIO().DisplaySize;
|
||||
var height = Math.Max( display.Y / 4, 15 * ImGui.GetFrameHeightWithSpacing() );
|
||||
var height = Math.Max(display.Y / 4, 15 * ImGui.GetFrameHeightWithSpacing());
|
||||
var width = display.X / 8;
|
||||
var size = new Vector2( width * 2, height );
|
||||
ImGui.SetNextWindowPos( ImGui.GetMainViewport().GetCenter(), ImGuiCond.Always, Vector2.One / 2 );
|
||||
ImGui.SetNextWindowSize( size );
|
||||
using var popup = ImRaii.Popup( "Import Status", ImGuiWindowFlags.Modal );
|
||||
if( _import == null || !popup.Success )
|
||||
{
|
||||
var size = new Vector2(width * 2, height);
|
||||
ImGui.SetNextWindowPos(ImGui.GetMainViewport().GetCenter(), ImGuiCond.Always, Vector2.One / 2);
|
||||
ImGui.SetNextWindowSize(size);
|
||||
using var popup = ImRaii.Popup("Import Status", ImGuiWindowFlags.Modal);
|
||||
if (_import == null || !popup.Success)
|
||||
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 )
|
||||
{
|
||||
_import.DrawProgressInfo( new Vector2( -1, ImGui.GetFrameHeight() ) );
|
||||
}
|
||||
if (child)
|
||||
_import.DrawProgressInfo(new Vector2(-1, ImGui.GetFrameHeight()));
|
||||
}
|
||||
|
||||
if( _import.State == ImporterState.Done && ImGui.Button( "Close", -Vector2.UnitX )
|
||||
|| _import.State != ImporterState.Done && _import.DrawCancelButton( -Vector2.UnitX ) )
|
||||
if (_import.State == ImporterState.Done && ImGui.Button("Close", -Vector2.UnitX)
|
||||
|| _import.State != ImporterState.Done && _import.DrawCancelButton(-Vector2.UnitX))
|
||||
{
|
||||
_import?.Dispose();
|
||||
_import = null;
|
||||
|
|
@ -267,100 +249,84 @@ public sealed partial class ModFileSystemSelector : FileSystemSelector< Mod, Mod
|
|||
}
|
||||
|
||||
// Mods need to be added thread-safely outside of iteration.
|
||||
private readonly ConcurrentQueue< DirectoryInfo > _modsToAdd = new();
|
||||
private readonly ConcurrentQueue<DirectoryInfo> _modsToAdd = new();
|
||||
|
||||
// Clean up invalid directory if necessary.
|
||||
// Add successfully extracted mods.
|
||||
private void AddNewMod( FileInfo file, DirectoryInfo? dir, Exception? error )
|
||||
private void AddNewMod(FileInfo file, DirectoryInfo? dir, Exception? error)
|
||||
{
|
||||
if( error != null )
|
||||
if (error != null)
|
||||
{
|
||||
if( dir != null && Directory.Exists( dir.FullName ) )
|
||||
{
|
||||
if (dir != null && Directory.Exists(dir.FullName))
|
||||
try
|
||||
{
|
||||
Directory.Delete( dir.FullName, true );
|
||||
Directory.Delete(dir.FullName, true);
|
||||
}
|
||||
catch( Exception e )
|
||||
catch (Exception e)
|
||||
{
|
||||
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 )
|
||||
{
|
||||
Penumbra.Log.Error( $"Error extracting {file.FullName}, mod skipped:\n{error}" );
|
||||
}
|
||||
if (error is not OperationCanceledException)
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
private void DeleteModButton( Vector2 size )
|
||||
private void DeleteModButton(Vector2 size)
|
||||
{
|
||||
var keys = Penumbra.Config.DeleteModModifier.IsActive();
|
||||
var tt = SelectedLeaf == null
|
||||
? "No mod selected."
|
||||
: "Delete the currently selected mod entirely from your drive.\n"
|
||||
+ "This can not be undone.";
|
||||
if( !keys )
|
||||
{
|
||||
if (!keys)
|
||||
tt += $"\nHold {Penumbra.Config.DeleteModModifier} while clicking to delete the mod.";
|
||||
}
|
||||
|
||||
if( ImGuiUtil.DrawDisabledButton( FontAwesomeIcon.Trash.ToIconString(), size, tt, SelectedLeaf == null || !keys, true )
|
||||
&& Selected != null )
|
||||
{
|
||||
Penumbra.ModManager.DeleteMod( Selected.Index );
|
||||
}
|
||||
if (ImGuiUtil.DrawDisabledButton(FontAwesomeIcon.Trash.ToIconString(), size, tt, SelectedLeaf == null || !keys, true)
|
||||
&& Selected != null)
|
||||
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 ) )
|
||||
{
|
||||
ImGui.OpenPopup( "ExtendedHelp" );
|
||||
}
|
||||
if (ImGuiUtil.DrawDisabledButton(FontAwesomeIcon.QuestionCircle.ToIconString(), size, "Open extended help.", false, true))
|
||||
ImGui.OpenPopup("ExtendedHelp");
|
||||
|
||||
ConfigWindow.OpenTutorial( ConfigWindow.BasicTutorialSteps.AdvancedHelp );
|
||||
ConfigWindow.OpenTutorial(ConfigWindow.BasicTutorialSteps.AdvancedHelp);
|
||||
}
|
||||
|
||||
// Helpers.
|
||||
private static void SetDescendants( ModFileSystem.Folder folder, bool enabled, bool inherit = false )
|
||||
private static void SetDescendants(ModFileSystem.Folder folder, bool enabled, bool inherit = false)
|
||||
{
|
||||
var mods = folder.GetAllDescendants( ISortMode< Mod >.Lexicographical ).OfType< ModFileSystem.Leaf >().Select( l =>
|
||||
var mods = folder.GetAllDescendants(ISortMode<Mod>.Lexicographical).OfType<ModFileSystem.Leaf>().Select(l =>
|
||||
{
|
||||
// Any mod handled here should not stay new.
|
||||
Penumbra.ModManager.NewMods.Remove( l.Value );
|
||||
Penumbra.ModManager.NewMods.Remove(l.Value);
|
||||
return l.Value;
|
||||
} );
|
||||
});
|
||||
|
||||
if( inherit )
|
||||
{
|
||||
Penumbra.CollectionManager.Current.SetMultipleModInheritances( mods, enabled );
|
||||
}
|
||||
if (inherit)
|
||||
Penumbra.CollectionManager.Current.SetMultipleModInheritances(mods, enabled);
|
||||
else
|
||||
{
|
||||
Penumbra.CollectionManager.Current.SetMultipleModStates( mods, enabled );
|
||||
}
|
||||
Penumbra.CollectionManager.Current.SetMultipleModStates(mods, enabled);
|
||||
}
|
||||
|
||||
// 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)
|
||||
{
|
||||
// TODO: maybe make more efficient
|
||||
SetFilterDirty();
|
||||
if( modIdx == Selected?.Index )
|
||||
{
|
||||
OnSelectionChange( Selected, Selected, default );
|
||||
}
|
||||
if (modIdx == Selected?.Index)
|
||||
OnSelectionChange(Selected, Selected, default);
|
||||
}
|
||||
|
||||
private void OnModDataChange( ModDataChangeType type, Mod mod, string? oldName )
|
||||
private void OnModDataChange(ModDataChangeType type, Mod mod, string? oldName)
|
||||
{
|
||||
switch( type )
|
||||
switch (type)
|
||||
{
|
||||
case ModDataChangeType.Name:
|
||||
case ModDataChangeType.Author:
|
||||
|
|
@ -372,46 +338,44 @@ public sealed partial class ModFileSystemSelector : FileSystemSelector< Mod, Mod
|
|||
}
|
||||
}
|
||||
|
||||
private void OnInheritanceChange( bool _ )
|
||||
private void OnInheritanceChange(bool _)
|
||||
{
|
||||
SetFilterDirty();
|
||||
OnSelectionChange( Selected, Selected, default );
|
||||
OnSelectionChange(Selected, Selected, default);
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
if( oldCollection != null )
|
||||
if (oldCollection != null)
|
||||
{
|
||||
oldCollection.ModSettingChanged -= OnSettingChange;
|
||||
oldCollection.InheritanceChanged -= OnInheritanceChange;
|
||||
}
|
||||
|
||||
if( newCollection != null )
|
||||
if (newCollection != null)
|
||||
{
|
||||
newCollection.ModSettingChanged += OnSettingChange;
|
||||
newCollection.InheritanceChanged += OnInheritanceChange;
|
||||
}
|
||||
|
||||
SetFilterDirty();
|
||||
OnSelectionChange( Selected, Selected, default );
|
||||
OnSelectionChange(Selected, Selected, default);
|
||||
}
|
||||
|
||||
private void OnSelectionChange( Mod? _1, Mod? newSelection, in ModState _2 )
|
||||
private void OnSelectionChange(Mod? _1, Mod? newSelection, in ModState _2)
|
||||
{
|
||||
if( newSelection == null )
|
||||
if (newSelection == null)
|
||||
{
|
||||
SelectedSettings = ModSettings.Empty;
|
||||
SelectedSettingCollection = ModCollection.Empty;
|
||||
}
|
||||
else
|
||||
{
|
||||
( var settings, SelectedSettingCollection ) = Penumbra.CollectionManager.Current[ newSelection.Index ];
|
||||
SelectedSettings = settings ?? ModSettings.Empty;
|
||||
(var settings, SelectedSettingCollection) = Penumbra.CollectionManager.Current[newSelection.Index];
|
||||
SelectedSettings = settings ?? ModSettings.Empty;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -426,92 +390,89 @@ public sealed partial class ModFileSystemSelector : FileSystemSelector< Mod, Mod
|
|||
|
||||
private void RestoreLastSelection()
|
||||
{
|
||||
if( _lastSelectedDirectory.Length > 0 )
|
||||
if (_lastSelectedDirectory.Length > 0)
|
||||
{
|
||||
var leaf = ( ModFileSystem.Leaf? )FileSystem.Root.GetAllDescendants( ISortMode< Mod >.Lexicographical )
|
||||
.FirstOrDefault( l => l is ModFileSystem.Leaf m && m.Value.ModPath.FullName == _lastSelectedDirectory );
|
||||
Select( leaf );
|
||||
var leaf = (ModFileSystem.Leaf?)FileSystem.Root.GetAllDescendants(ISortMode<Mod>.Lexicographical)
|
||||
.FirstOrDefault(l => l is ModFileSystem.Leaf m && m.Value.ModPath.FullName == _lastSelectedDirectory);
|
||||
Select(leaf);
|
||||
_lastSelectedDirectory = string.Empty;
|
||||
}
|
||||
}
|
||||
|
||||
// If a default import folder is setup, try to move the given mod in there.
|
||||
// If the folder does not exist, create it if possible.
|
||||
private void MoveModToDefaultDirectory( Mod mod )
|
||||
private void MoveModToDefaultDirectory(Mod mod)
|
||||
{
|
||||
if( Penumbra.Config.DefaultImportFolder.Length == 0 )
|
||||
{
|
||||
if (Penumbra.Config.DefaultImportFolder.Length == 0)
|
||||
return;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var leaf = FileSystem.Root.GetChildren( ISortMode< Mod >.Lexicographical )
|
||||
.FirstOrDefault( f => f is FileSystem< Mod >.Leaf l && l.Value == mod );
|
||||
if( leaf == null )
|
||||
{
|
||||
throw new Exception( "Mod was not found at root." );
|
||||
}
|
||||
var leaf = FileSystem.Root.GetChildren(ISortMode<Mod>.Lexicographical)
|
||||
.FirstOrDefault(f => f is FileSystem<Mod>.Leaf l && l.Value == mod);
|
||||
if (leaf == null)
|
||||
throw new Exception("Mod was not found at root.");
|
||||
|
||||
var folder = FileSystem.FindOrCreateAllFolders( Penumbra.Config.DefaultImportFolder );
|
||||
FileSystem.Move( leaf, folder );
|
||||
var folder = FileSystem.FindOrCreateAllFolders(Penumbra.Config.DefaultImportFolder);
|
||||
FileSystem.Move(leaf, folder);
|
||||
}
|
||||
catch( Exception e )
|
||||
catch (Exception e)
|
||||
{
|
||||
Penumbra.Log.Warning(
|
||||
$"Could not move newly imported mod {mod.Name} to default import folder {Penumbra.Config.DefaultImportFolder}:\n{e}" );
|
||||
$"Could not move newly imported mod {mod.Name} to default import folder {Penumbra.Config.DefaultImportFolder}:\n{e}");
|
||||
}
|
||||
}
|
||||
|
||||
private static void DrawHelpPopup()
|
||||
{
|
||||
ImGuiUtil.HelpPopup( "ExtendedHelp", new Vector2( 1000 * ImGuiHelpers.GlobalScale, 34.5f * ImGui.GetTextLineHeightWithSpacing() ), () =>
|
||||
ImGuiUtil.HelpPopup("ExtendedHelp", new Vector2(1000 * ImGuiHelpers.GlobalScale, 34.5f * ImGui.GetTextLineHeightWithSpacing()), () =>
|
||||
{
|
||||
ImGui.Dummy( Vector2.UnitY * ImGui.GetTextLineHeight() );
|
||||
ImGui.TextUnformatted( "Mod Management" );
|
||||
ImGui.BulletText( "You can create empty mods or import mods with the buttons in this row." );
|
||||
ImGui.Dummy(Vector2.UnitY * ImGui.GetTextLineHeight());
|
||||
ImGui.TextUnformatted("Mod Management");
|
||||
ImGui.BulletText("You can create empty mods or import mods with the buttons in this row.");
|
||||
using var indent = ImRaii.PushIndent();
|
||||
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." );
|
||||
indent.Pop( 1 );
|
||||
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.Dummy( Vector2.UnitY * ImGui.GetTextLineHeight() );
|
||||
ImGui.TextUnformatted( "Mod Selector" );
|
||||
ImGui.BulletText( "Select a mod to obtain more information or change settings." );
|
||||
ImGui.BulletText( "Names are colored according to your config and their current state in the collection:" );
|
||||
indent.Push();
|
||||
ImGuiUtil.BulletTextColored( ColorId.EnabledMod.Value(), "enabled in the current collection." );
|
||||
ImGuiUtil.BulletTextColored( ColorId.DisabledMod.Value(), "disabled in the current collection." );
|
||||
ImGuiUtil.BulletTextColored( ColorId.InheritedMod.Value(), "enabled due to inheritance from another collection." );
|
||||
ImGuiUtil.BulletTextColored( ColorId.InheritedDisabledMod.Value(), "disabled due to inheritance from another collection." );
|
||||
ImGuiUtil.BulletTextColored( ColorId.UndefinedMod.Value(), "unconfigured in all inherited collections." );
|
||||
ImGuiUtil.BulletTextColored( ColorId.NewMod.Value(),
|
||||
"newly imported during this session. Will go away when first enabling a mod or when Penumbra is reloaded." );
|
||||
ImGuiUtil.BulletTextColored( ColorId.HandledConflictMod.Value(),
|
||||
"enabled and conflicting with another enabled Mod, but on different priorities (i.e. the conflict is solved)." );
|
||||
ImGuiUtil.BulletTextColored( ColorId.ConflictingMod.Value(),
|
||||
"enabled and conflicting with another enabled Mod on the same priority." );
|
||||
ImGuiUtil.BulletTextColored( ColorId.FolderExpanded.Value(), "expanded mod folder." );
|
||||
ImGuiUtil.BulletTextColored( ColorId.FolderCollapsed.Value(), "collapsed mod folder" );
|
||||
indent.Pop( 1 );
|
||||
ImGui.BulletText( "Right-click a mod to enter its sort order, which is its name by default, possibly with a duplicate number." );
|
||||
indent.Push();
|
||||
ImGui.BulletText( "A sort order differing from the mods name will not be displayed, it will just be used for ordering." );
|
||||
ImGui.BulletText("Supported formats for import are: .ttmp, .ttmp2, .pmp.");
|
||||
ImGui.BulletText(
|
||||
"If the sort order string contains Forward-Slashes ('/'), the preceding substring will be turned into folders automatically." );
|
||||
indent.Pop( 1 );
|
||||
ImGui.BulletText(
|
||||
"You can drag and drop mods and subfolders into existing folders. Dropping them onto mods is the same as dropping them onto the parent of the mod." );
|
||||
ImGui.BulletText( "Right-clicking a folder opens a context menu." );
|
||||
ImGui.BulletText( "Right-clicking empty space allows you to expand or collapse all folders at once." );
|
||||
ImGui.BulletText( "Use the Filter Mods... input at the top to filter the list for mods whose name or path contain the text." );
|
||||
"You can also support .zip, .7z or .rar archives, but only if they already contain Penumbra-styled mods with appropriate metadata.");
|
||||
indent.Pop(1);
|
||||
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.Dummy(Vector2.UnitY * ImGui.GetTextLineHeight());
|
||||
ImGui.TextUnformatted("Mod Selector");
|
||||
ImGui.BulletText("Select a mod to obtain more information or change settings.");
|
||||
ImGui.BulletText("Names are colored according to your config and their current state in the collection:");
|
||||
indent.Push();
|
||||
ImGui.BulletText( "You can enter n:[string] to filter only for names, without path." );
|
||||
ImGui.BulletText( "You can enter c:[string] to filter for Changed Items instead." );
|
||||
ImGui.BulletText( "You can enter a:[string] to filter for Mod Authors instead." );
|
||||
indent.Pop( 1 );
|
||||
ImGui.BulletText( "Use the expandable menu beside the input to filter for mods fulfilling specific criteria." );
|
||||
} );
|
||||
ImGuiUtil.BulletTextColored(ColorId.EnabledMod.Value(), "enabled in the current collection.");
|
||||
ImGuiUtil.BulletTextColored(ColorId.DisabledMod.Value(), "disabled in the current collection.");
|
||||
ImGuiUtil.BulletTextColored(ColorId.InheritedMod.Value(), "enabled due to inheritance from another collection.");
|
||||
ImGuiUtil.BulletTextColored(ColorId.InheritedDisabledMod.Value(), "disabled due to inheritance from another collection.");
|
||||
ImGuiUtil.BulletTextColored(ColorId.UndefinedMod.Value(), "unconfigured in all inherited collections.");
|
||||
ImGuiUtil.BulletTextColored(ColorId.NewMod.Value(),
|
||||
"newly imported during this session. Will go away when first enabling a mod or when Penumbra is reloaded.");
|
||||
ImGuiUtil.BulletTextColored(ColorId.HandledConflictMod.Value(),
|
||||
"enabled and conflicting with another enabled Mod, but on different priorities (i.e. the conflict is solved).");
|
||||
ImGuiUtil.BulletTextColored(ColorId.ConflictingMod.Value(),
|
||||
"enabled and conflicting with another enabled Mod on the same priority.");
|
||||
ImGuiUtil.BulletTextColored(ColorId.FolderExpanded.Value(), "expanded mod folder.");
|
||||
ImGuiUtil.BulletTextColored(ColorId.FolderCollapsed.Value(), "collapsed mod folder");
|
||||
indent.Pop(1);
|
||||
ImGui.BulletText("Right-click a mod to enter its sort order, which is its name by default, possibly with a duplicate number.");
|
||||
indent.Push();
|
||||
ImGui.BulletText("A sort order differing from the mods name will not be displayed, it will just be used for ordering.");
|
||||
ImGui.BulletText(
|
||||
"If the sort order string contains Forward-Slashes ('/'), the preceding substring will be turned into folders automatically.");
|
||||
indent.Pop(1);
|
||||
ImGui.BulletText(
|
||||
"You can drag and drop mods and subfolders into existing folders. Dropping them onto mods is the same as dropping them onto the parent of the mod.");
|
||||
ImGui.BulletText("Right-clicking a folder opens a context menu.");
|
||||
ImGui.BulletText("Right-clicking empty space allows you to expand or collapse all folders at once.");
|
||||
ImGui.BulletText("Use the Filter Mods... input at the top to filter the list for mods whose name or path contain the text.");
|
||||
indent.Push();
|
||||
ImGui.BulletText("You can enter n:[string] to filter only for names, without path.");
|
||||
ImGui.BulletText("You can enter c:[string] to filter for Changed Items instead.");
|
||||
ImGui.BulletText("You can enter a:[string] to filter for Mod Authors instead.");
|
||||
indent.Pop(1);
|
||||
ImGui.BulletText("Use the expandable menu beside the input to filter for mods fulfilling specific criteria.");
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ using OtterGui.Classes;
|
|||
using OtterGui.Raii;
|
||||
using OtterGui.Widgets;
|
||||
using Penumbra.Collections;
|
||||
using Penumbra.Services;
|
||||
|
||||
namespace Penumbra.UI;
|
||||
|
||||
|
|
@ -16,20 +17,22 @@ public partial class ConfigWindow
|
|||
// Encapsulate for less pollution.
|
||||
private partial class CollectionsTab : IDisposable, ITab
|
||||
{
|
||||
private readonly ConfigWindow _window;
|
||||
private readonly CommunicatorService _communicator;
|
||||
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
|
||||
=> "Collections"u8;
|
||||
|
||||
public void Dispose()
|
||||
=> Penumbra.CollectionManager.CollectionChanged -= UpdateIdentifiers;
|
||||
=> _communicator.CollectionChange.Event -= UpdateIdentifiers;
|
||||
|
||||
public void DrawHeader()
|
||||
=> OpenTutorial( BasicTutorialSteps.Collections );
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ using FFXIVClientStructs.FFXIV.Client.Game.Object;
|
|||
using FFXIVClientStructs.FFXIV.Client.System.Resource;
|
||||
using ImGuiNET;
|
||||
using OtterGui;
|
||||
using OtterGui.Classes;
|
||||
using OtterGui.Widgets;
|
||||
using Penumbra.GameData.Actors;
|
||||
using Penumbra.GameData.Files;
|
||||
|
|
@ -28,10 +29,14 @@ public partial class ConfigWindow
|
|||
{
|
||||
private class DebugTab : ITab
|
||||
{
|
||||
private readonly StartTracker _timer;
|
||||
private readonly ConfigWindow _window;
|
||||
|
||||
public DebugTab( ConfigWindow window )
|
||||
=> _window = window;
|
||||
public DebugTab( ConfigWindow window, StartTracker timer)
|
||||
{
|
||||
_window = window;
|
||||
_timer = timer;
|
||||
}
|
||||
|
||||
public ReadOnlySpan<byte> Label
|
||||
=> "Debug"u8;
|
||||
|
|
@ -109,7 +114,7 @@ public partial class ConfigWindow
|
|||
PrintValue( "Web Server Enabled", _window._penumbra.HttpApi.Enabled.ToString() );
|
||||
}
|
||||
|
||||
private static void DrawPerformanceTab()
|
||||
private void DrawPerformanceTab()
|
||||
{
|
||||
ImGui.NewLine();
|
||||
if( ImGui.CollapsingHeader( "Performance" ) )
|
||||
|
|
@ -121,7 +126,7 @@ public partial class ConfigWindow
|
|||
{
|
||||
if( start )
|
||||
{
|
||||
Penumbra.StartTimer.Draw( "##startTimer", TimingExtensions.ToName );
|
||||
_timer.Draw( "##startTimer", TimingExtensions.ToName );
|
||||
ImGui.NewLine();
|
||||
}
|
||||
}
|
||||
|
|
@ -397,7 +402,7 @@ public partial class ConfigWindow
|
|||
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}" );
|
||||
if( !tree )
|
||||
|
|
|
|||
|
|
@ -105,7 +105,7 @@ public partial class ConfigWindow
|
|||
|
||||
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();
|
||||
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
|
||||
{
|
||||
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" ) );
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -299,11 +299,11 @@ public partial class ConfigWindow
|
|||
|
||||
private const string SupportInfoButtonText = "Copy Support Info to Clipboard";
|
||||
|
||||
public static void DrawSupportButton()
|
||||
public static void DrawSupportButton(Penumbra penumbra)
|
||||
{
|
||||
if( ImGui.Button( SupportInfoButtonText ) )
|
||||
{
|
||||
var text = Penumbra.GatherSupportInformation();
|
||||
var text = penumbra.GatherSupportInformation();
|
||||
ImGui.SetClipboardText( text );
|
||||
}
|
||||
}
|
||||
|
|
@ -345,7 +345,7 @@ public partial class ConfigWindow
|
|||
}
|
||||
|
||||
ImGui.SetCursorPos( new Vector2( xPos, ImGui.GetFrameHeightWithSpacing() ) );
|
||||
DrawSupportButton();
|
||||
DrawSupportButton(_window._penumbra);
|
||||
|
||||
ImGui.SetCursorPos( new Vector2( xPos, 0 ) );
|
||||
DrawDiscordButton( width );
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@ using OtterGui.Raii;
|
|||
using OtterGui.Widgets;
|
||||
using Penumbra.Api.Enums;
|
||||
using Penumbra.Mods;
|
||||
using Penumbra.Services;
|
||||
using Penumbra.Services;
|
||||
using Penumbra.UI.Classes;
|
||||
using Penumbra.Util;
|
||||
|
||||
|
|
@ -19,7 +19,7 @@ public sealed partial class ConfigWindow : Window, IDisposable
|
|||
private readonly Penumbra _penumbra;
|
||||
private readonly ModFileSystemSelector _selector;
|
||||
private readonly ModPanel _modPanel;
|
||||
public readonly ModEditWindow ModEditPopup = new();
|
||||
public readonly ModEditWindow ModEditPopup;
|
||||
|
||||
private readonly SettingsTab _settingsTab;
|
||||
private readonly CollectionsTab _collectionsTab;
|
||||
|
|
@ -31,43 +31,43 @@ public sealed partial class ConfigWindow : Window, IDisposable
|
|||
private readonly ResourceWatcher _resourceWatcher;
|
||||
|
||||
public TabType SelectTab = TabType.None;
|
||||
public void SelectMod( Mod mod )
|
||||
=> _selector.SelectByValue( mod );
|
||||
|
||||
public ConfigWindow( Penumbra penumbra, ResourceWatcher watcher )
|
||||
: base( GetLabel() )
|
||||
public void SelectMod(Mod mod)
|
||||
=> _selector.SelectByValue(mod);
|
||||
|
||||
public ConfigWindow(CommunicatorService communicator, StartTracker timer, Penumbra penumbra, ResourceWatcher watcher)
|
||||
: base(GetLabel())
|
||||
{
|
||||
_penumbra = penumbra;
|
||||
_resourceWatcher = watcher;
|
||||
|
||||
_settingsTab = new SettingsTab( this );
|
||||
_selector = new ModFileSystemSelector( _penumbra.ModFileSystem );
|
||||
_modPanel = new ModPanel( this );
|
||||
_modsTab = new ModsTab( _selector, _modPanel, _penumbra );
|
||||
ModEditPopup = new ModEditWindow(communicator);
|
||||
_settingsTab = new SettingsTab(this);
|
||||
_selector = new ModFileSystemSelector(communicator, _penumbra.ModFileSystem);
|
||||
_modPanel = new ModPanel(this);
|
||||
_modsTab = new ModsTab(_selector, _modPanel, _penumbra);
|
||||
_selector.SelectionChanged += _modPanel.OnSelectionChange;
|
||||
_collectionsTab = new CollectionsTab( this );
|
||||
_changedItemsTab = new ChangedItemsTab( this );
|
||||
_collectionsTab = new CollectionsTab(communicator, this);
|
||||
_changedItemsTab = new ChangedItemsTab(this);
|
||||
_effectiveTab = new EffectiveTab();
|
||||
_debugTab = new DebugTab( this );
|
||||
_debugTab = new DebugTab(this, timer);
|
||||
_resourceTab = new ResourceTab();
|
||||
if( Penumbra.Config.FixMainWindow )
|
||||
{
|
||||
if (Penumbra.Config.FixMainWindow)
|
||||
Flags |= ImGuiWindowFlags.NoResize | ImGuiWindowFlags.NoMove;
|
||||
}
|
||||
|
||||
DalamudServices.PluginInterface.UiBuilder.DisableGposeUiHide = !Penumbra.Config.HideUiInGPose;
|
||||
DalamudServices.PluginInterface.UiBuilder.DisableCutsceneUiHide = !Penumbra.Config.HideUiInCutscenes;
|
||||
DalamudServices.PluginInterface.UiBuilder.DisableUserUiHide = !Penumbra.Config.HideUiWhenUiHidden;
|
||||
RespectCloseHotkey = true;
|
||||
RespectCloseHotkey = true;
|
||||
SizeConstraints = new WindowSizeConstraints()
|
||||
{
|
||||
MinimumSize = new Vector2( 800, 600 ),
|
||||
MaximumSize = new Vector2( 4096, 2160 ),
|
||||
MinimumSize = new Vector2(800, 600),
|
||||
MaximumSize = new Vector2(4096, 2160),
|
||||
};
|
||||
UpdateTutorialStep();
|
||||
}
|
||||
|
||||
private ReadOnlySpan< byte > ToLabel( TabType type )
|
||||
private ReadOnlySpan<byte> ToLabel(TabType type)
|
||||
=> type switch
|
||||
{
|
||||
TabType.Settings => _settingsTab.Label,
|
||||
|
|
@ -78,85 +78,85 @@ public sealed partial class ConfigWindow : Window, IDisposable
|
|||
TabType.ResourceWatcher => _resourceWatcher.Label,
|
||||
TabType.Debug => _debugTab.Label,
|
||||
TabType.ResourceManager => _resourceTab.Label,
|
||||
_ => ReadOnlySpan< byte >.Empty,
|
||||
_ => ReadOnlySpan<byte>.Empty,
|
||||
};
|
||||
|
||||
public override void Draw()
|
||||
{
|
||||
using var performance = Penumbra.Performance.Measure( PerformanceType.UiMainWindow );
|
||||
using var performance = Penumbra.Performance.Measure(PerformanceType.UiMainWindow);
|
||||
|
||||
try
|
||||
{
|
||||
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"
|
||||
+ "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"
|
||||
+ $"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 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 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.DevPenumbraExists )
|
||||
else if (Penumbra.ValidityChecker.DevPenumbraExists)
|
||||
{
|
||||
DrawProblemWindow(
|
||||
DrawProblemWindow(_penumbra,
|
||||
$"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"
|
||||
+ "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
|
||||
{
|
||||
SetupSizes();
|
||||
if( TabBar.Draw( string.Empty, ImGuiTabBarFlags.NoTooltip, ToLabel( SelectTab ), _settingsTab, _modsTab, _collectionsTab,
|
||||
_changedItemsTab, _effectiveTab, _resourceWatcher, _debugTab, _resourceTab ) )
|
||||
{
|
||||
if (TabBar.Draw(string.Empty, ImGuiTabBarFlags.NoTooltip, ToLabel(SelectTab), _settingsTab, _modsTab, _collectionsTab,
|
||||
_changedItemsTab, _effectiveTab, _resourceWatcher, _debugTab, _resourceTab))
|
||||
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();
|
||||
ImGuiUtil.TextWrapped( text );
|
||||
ImGuiUtil.TextWrapped(text);
|
||||
color.Pop();
|
||||
|
||||
ImGui.NewLine();
|
||||
ImGui.NewLine();
|
||||
SettingsTab.DrawDiscordButton( 0 );
|
||||
SettingsTab.DrawDiscordButton(0);
|
||||
ImGui.SameLine();
|
||||
SettingsTab.DrawSupportButton();
|
||||
SettingsTab.DrawSupportButton(penumbra);
|
||||
ImGui.NewLine();
|
||||
ImGui.NewLine();
|
||||
|
||||
if( withExceptions )
|
||||
if (withExceptions)
|
||||
{
|
||||
ImGui.TextUnformatted( "Exceptions" );
|
||||
ImGui.TextUnformatted("Exceptions");
|
||||
ImGui.Separator();
|
||||
using var box = ImRaii.ListBox( "##Exceptions", new Vector2( -1, -1 ) );
|
||||
foreach( var exception in Penumbra.ValidityChecker.ImcExceptions )
|
||||
using var box = ImRaii.ListBox("##Exceptions", new Vector2(-1, -1));
|
||||
foreach (var exception in Penumbra.ValidityChecker.ImcExceptions)
|
||||
{
|
||||
ImGuiUtil.TextWrapped( exception.ToString() );
|
||||
ImGuiUtil.TextWrapped(exception.ToString());
|
||||
ImGui.Separator();
|
||||
ImGui.NewLine();
|
||||
}
|
||||
|
|
@ -182,8 +182,8 @@ public sealed partial class ConfigWindow : Window, IDisposable
|
|||
|
||||
private void SetupSizes()
|
||||
{
|
||||
_defaultSpace = new Vector2( 0, 10 * ImGuiHelpers.GlobalScale );
|
||||
_inputTextWidth = new Vector2( 350f * ImGuiHelpers.GlobalScale, 0 );
|
||||
_iconButtonSize = new Vector2( ImGui.GetFrameHeight() );
|
||||
_defaultSpace = new Vector2(0, 10 * ImGuiHelpers.GlobalScale);
|
||||
_inputTextWidth = new Vector2(350f * ImGuiHelpers.GlobalScale, 0);
|
||||
_iconButtonSize = new Vector2(ImGui.GetFrameHeight());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
329
Penumbra/Util/EventWrapper.cs
Normal file
329
Penumbra/Util/EventWrapper.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
|
||||
|
|
@ -47,24 +48,24 @@ public enum PerformanceType
|
|||
|
||||
public static class TimingExtensions
|
||||
{
|
||||
public static string ToName( this StartTimeType type )
|
||||
public static string ToName(this StartTimeType type)
|
||||
=> type switch
|
||||
{
|
||||
StartTimeType.Total => "Total Construction",
|
||||
StartTimeType.Identifier => "Identification Data",
|
||||
StartTimeType.Stains => "Stain Data",
|
||||
StartTimeType.Items => "Item Data",
|
||||
StartTimeType.Actors => "Actor Data",
|
||||
StartTimeType.Backup => "Checking Backups",
|
||||
StartTimeType.Mods => "Loading Mods",
|
||||
StartTimeType.Collections => "Loading Collections",
|
||||
StartTimeType.Api => "Setting Up API",
|
||||
StartTimeType.Interface => "Setting Up Interface",
|
||||
StartTimeType.PathResolver => "Setting Up Path Resolver",
|
||||
_ => $"Unknown {(int) type}",
|
||||
StartTimeType.Total => "Total Construction",
|
||||
StartTimeType.Identifier => "Identification Data",
|
||||
StartTimeType.Stains => "Stain Data",
|
||||
StartTimeType.Items => "Item Data",
|
||||
StartTimeType.Actors => "Actor Data",
|
||||
StartTimeType.Backup => "Checking Backups",
|
||||
StartTimeType.Mods => "Loading Mods",
|
||||
StartTimeType.Collections => "Loading Collections",
|
||||
StartTimeType.Api => "Setting Up API",
|
||||
StartTimeType.Interface => "Setting Up Interface",
|
||||
StartTimeType.PathResolver => "Setting Up Path Resolver",
|
||||
_ => $"Unknown {(int)type}",
|
||||
};
|
||||
|
||||
public static string ToName( this PerformanceType type )
|
||||
public static string ToName(this PerformanceType type)
|
||||
=> type switch
|
||||
{
|
||||
PerformanceType.UiMainWindow => "Main Interface Drawing",
|
||||
|
|
@ -91,6 +92,6 @@ public static class TimingExtensions
|
|||
PerformanceType.LoadPap => "LoadPap Hook",
|
||||
PerformanceType.LoadAction => "LoadAction Hook",
|
||||
PerformanceType.DebugTimes => "Debug Tracking",
|
||||
_ => $"Unknown {( int )type}",
|
||||
_ => $"Unknown {(int)type}",
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue