This is going rather well.

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

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

View file

@ -1404,10 +1404,10 @@ public class IpcTester : IDisposable
return;
}
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

View file

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

View file

@ -3,10 +3,7 @@ using Penumbra.Collections;
using Penumbra.Meta.Manipulations;
using Penumbra.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);
}
}

View file

@ -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);
}
}
}
}

View file

@ -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);
}
}
}
}

View file

@ -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

View file

@ -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()

View file

@ -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;

View file

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

View file

@ -19,8 +19,14 @@ using ErrorEventArgs = Newtonsoft.Json.Serialization.ErrorEventArgs;
namespace Penumbra;
[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 )

View file

@ -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();
}
}

View file

@ -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}." );
}

View file

@ -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();
}

View file

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

View file

@ -2,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);
}
}
}
}

View file

@ -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);
}

View file

@ -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);
}
}
}
}
}

View file

@ -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;

View file

@ -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 );

View file

@ -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;
}

View file

@ -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 ) )

View 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();

View file

@ -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();
}

View file

@ -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();
}
}
}

View file

@ -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();
}
}

View file

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

View file

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

View file

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

View file

@ -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";

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -17,6 +17,7 @@ using Penumbra.GameData.Enums;
using Penumbra.GameData.Structs;
using Penumbra.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;
}
}

View file

@ -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" );

View file

@ -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,

View file

@ -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.");
});
}
}
}

View file

@ -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 );

View file

@ -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 )

View file

@ -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" ) );
}
}
}

View file

@ -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 );

View file

@ -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());
}
}
}

View file

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

View file

@ -1,4 +1,5 @@
using System;
global using StartTracker = OtterGui.Classes.StartTimeTracker<Penumbra.Util.StartTimeType>;
global using PerformanceTracker = OtterGui.Classes.PerformanceTracker<Penumbra.Util.PerformanceType>;
namespace Penumbra.Util;
@ -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}",
};
}
}

View file

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