This is going rather well.

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

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

View file

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

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.Meta.Manipulations;
using Penumbra.Mods; using Penumbra.Mods;
using System.Collections.Generic; using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis; using Penumbra.Services;
using System.Linq;
using Penumbra.GameData.Actors;
using Penumbra.String;
using Penumbra.String.Classes; using Penumbra.String.Classes;
namespace Penumbra.Api; namespace Penumbra.Api;
@ -19,319 +16,120 @@ public enum RedirectResult
FilteredGamePath = 3, FilteredGamePath = 3,
} }
public class TempModManager public class TempModManager : IDisposable
{ {
public int GlobalChangeCounter { get; private set; } = 0; private readonly CommunicatorService _communicator;
private readonly Dictionary< ModCollection, List< Mod.TemporaryMod > > _mods = new();
private readonly List< Mod.TemporaryMod > _modsForAllCollections = new();
private readonly Dictionary< string, ModCollection > _customCollections = new();
public readonly IndividualCollections Collections = new(Penumbra.Actors);
public event ModCollection.Manager.CollectionChangeDelegate? CollectionChanged; 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; => _mods;
public IReadOnlyList< Mod.TemporaryMod > ModsForAllCollections public IReadOnlyList<Mod.TemporaryMod> ModsForAllCollections
=> _modsForAllCollections; => _modsForAllCollections;
public IReadOnlyDictionary< string, ModCollection > CustomCollections public RedirectResult Register(string tag, ModCollection? collection, Dictionary<Utf8GamePath, FullPath> dict,
=> _customCollections; HashSet<MetaManipulation> manips, int priority)
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 )
{ {
var mod = GetOrCreateMod( tag, collection, priority, out var created ); var mod = GetOrCreateMod(tag, collection, priority, out var created);
mod.SetAll( dict, manips ); mod.SetAll(dict, manips);
ApplyModChange( mod, collection, created, false ); ApplyModChange(mod, collection, created, false);
return RedirectResult.Success; 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; var list = collection == null ? _modsForAllCollections : _mods.TryGetValue(collection, out var l) ? l : null;
if( list == null ) if (list == null)
{
return RedirectResult.NotRegistered; return RedirectResult.NotRegistered;
}
var removed = list.RemoveAll( m => var removed = list.RemoveAll(m =>
{ {
if( m.Name != tag || priority != null && m.Priority != priority.Value ) if (m.Name != tag || priority != null && m.Priority != priority.Value)
{
return false; return false;
}
ApplyModChange( m, collection, false, true ); ApplyModChange(m, collection, false, true);
return true; return true;
} ); });
if( removed == 0 ) if (removed == 0)
{
return RedirectResult.NotRegistered; return RedirectResult.NotRegistered;
}
if( list.Count == 0 && collection != null ) if (list.Count == 0 && collection != null)
{ _mods.Remove(collection);
_mods.Remove( collection );
}
return RedirectResult.Success; return RedirectResult.Success;
} }
public string CreateTemporaryCollection( string name )
{
if( Penumbra.CollectionManager.ByName( name, out _ ) )
{
return string.Empty;
}
if( GlobalChangeCounter == int.MaxValue )
GlobalChangeCounter = 0;
var collection = ModCollection.CreateNewTemporary( name, GlobalChangeCounter++ );
if( _customCollections.TryAdd( collection.Name.ToLowerInvariant(), collection ) )
{
return collection.Name;
}
collection.ClearCache();
return string.Empty;
}
public bool RemoveTemporaryCollection( string collectionName )
{
if( !_customCollections.Remove( collectionName.ToLowerInvariant(), out var collection ) )
{
return false;
}
GlobalChangeCounter += Math.Max(collection.ChangeCounter + 1 - GlobalChangeCounter, 0);
_mods.Remove( collection );
collection.ClearCache();
for( var i = 0; i < Collections.Count; ++i )
{
if( Collections[ i ].Collection == collection )
{
CollectionChanged?.Invoke( CollectionType.Temporary, collection, null, Collections[ i ].DisplayName );
Collections.Delete( i );
}
}
return true;
}
public bool AddIdentifier( ModCollection collection, params ActorIdentifier[] identifiers )
{
if( Collections.Add( identifiers, collection ) )
{
CollectionChanged?.Invoke( CollectionType.Temporary, null, collection, Collections.Last().DisplayName );
return true;
}
return false;
}
public bool AddIdentifier( string collectionName, params ActorIdentifier[] identifiers )
{
if( !_customCollections.TryGetValue( collectionName.ToLowerInvariant(), out var collection ) )
{
return false;
}
return AddIdentifier( collection, identifiers );
}
public bool AddIdentifier( string collectionName, string characterName, ushort worldId = ushort.MaxValue )
{
if( !ByteString.FromString( characterName, out var byteString, false ) )
{
return false;
}
var identifier = Penumbra.Actors.CreatePlayer( byteString, worldId );
if( !identifier.IsValid )
{
return false;
}
return AddIdentifier( collectionName, identifier );
}
internal bool RemoveByCharacterName( string characterName, ushort worldId = ushort.MaxValue )
{
if( !ByteString.FromString( characterName, out var byteString, false ) )
{
return false;
}
var identifier = Penumbra.Actors.CreatePlayer( byteString, worldId );
return Collections.Individuals.TryGetValue( identifier, out var collection ) && RemoveTemporaryCollection( collection.Name );
}
// Apply any new changes to the temporary mod. // Apply any new changes to the temporary mod.
private static void ApplyModChange( Mod.TemporaryMod mod, ModCollection? collection, bool created, bool removed ) private void ApplyModChange(Mod.TemporaryMod mod, ModCollection? collection, bool created, bool removed)
{ {
if( collection == null ) if (collection != null)
{ {
if( removed ) if (removed)
{ collection.Remove(mod);
foreach( var c in Penumbra.CollectionManager )
{
c.Remove( mod );
}
}
else else
{ collection.Apply(mod, created);
foreach( var c in Penumbra.CollectionManager )
{
c.Apply( mod, created );
}
}
} }
else else
{ {
if( removed ) _communicator.TemporaryGlobalModChange.Invoke(mod, created, removed);
{
collection.Remove( mod );
}
else
{
collection.Apply( mod, created );
}
} }
} }
// Only find already existing mods, currently unused. /// <summary>
//private Mod.TemporaryMod? GetExistingMod( string tag, ModCollection? collection, int? priority ) /// Apply a mod change to a set of collections.
//{ /// </summary>
// var list = collection == null ? _modsForAllCollections : _mods.TryGetValue( collection, out var l ) ? l : null; public static void OnGlobalModChange(IEnumerable<ModCollection> collections, Mod.TemporaryMod mod, bool created, bool removed)
// if( list == null ) {
// { if (removed)
// return null; foreach (var c in collections)
// } c.Remove(mod);
// else
// if( priority != null ) foreach (var c in collections)
// { c.Apply(mod, created);
// return list.Find( m => m.Priority == priority.Value && m.Name == tag ); }
// }
//
// Mod.TemporaryMod? highestMod = null;
// var highestPriority = int.MinValue;
// foreach( var m in list )
// {
// if( highestPriority < m.Priority && m.Name == tag )
// {
// highestPriority = m.Priority;
// highestMod = m;
// }
// }
//
// return highestMod;
//}
// Find or create a mod with the given tag as name and the given priority, for the given collection (or all collections). // Find or create a mod with the given tag as name and the given priority, for the given collection (or all collections).
// Returns the found or created mod and whether it was newly created. // Returns the found or created mod and whether it was newly created.
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; List<Mod.TemporaryMod> list;
if( collection == null ) if (collection == null)
{ {
list = _modsForAllCollections; list = _modsForAllCollections;
} }
else if( _mods.TryGetValue( collection, out var l ) ) else if (_mods.TryGetValue(collection, out var l))
{ {
list = l; list = l;
} }
else else
{ {
list = new List< Mod.TemporaryMod >(); list = new List<Mod.TemporaryMod>();
_mods.Add( collection, list ); _mods.Add(collection, list);
} }
var mod = list.Find( m => m.Priority == priority && m.Name == tag ); var mod = list.Find(m => m.Priority == priority && m.Name == tag);
if( mod == null ) if (mod == null)
{ {
mod = new Mod.TemporaryMod() mod = new Mod.TemporaryMod()
{ {
Name = tag, Name = tag,
Priority = priority, Priority = priority,
}; };
list.Add( mod ); list.Add(mod);
created = true; created = true;
} }
else else
@ -341,4 +139,11 @@ public class TempModManager
return mod; return mod;
} }
private void OnCollectionChange(CollectionType collectionType, ModCollection? oldCollection, ModCollection? newCollection,
string _)
{
if (collectionType is CollectionType.Temporary or CollectionType.Inactive && newCollection == null && oldCollection != null)
_mods.Remove(oldCollection);
}
} }

View file

@ -9,6 +9,7 @@ using System.IO;
using System.Linq; using System.Linq;
using System.Threading.Tasks; using System.Threading.Tasks;
using Dalamud.Interface.Internal.Notifications; using Dalamud.Interface.Internal.Notifications;
using Dalamud.Plugin;
using Penumbra.GameData.Actors; using Penumbra.GameData.Actors;
using Penumbra.Util; using Penumbra.Util;
using Penumbra.Services; using Penumbra.Services;
@ -21,9 +22,6 @@ public partial class ModCollection
{ {
public const int Version = 1; public const int Version = 1;
// Is invoked after the collections actually changed.
public event CollectionChangeDelegate CollectionChanged;
// The collection currently selected for changing settings. // The collection currently selected for changing settings.
public ModCollection Current { get; private set; } = Empty; public ModCollection Current { get; private set; } = Empty;
@ -40,65 +38,62 @@ public partial class ModCollection
private ModCollection DefaultName { get; set; } = Empty; private ModCollection DefaultName { get; set; } = Empty;
// The list of character collections. // The list of character collections.
// TODO
public readonly IndividualCollections Individuals = new(Penumbra.Actors); public readonly IndividualCollections Individuals = new(Penumbra.Actors);
public ModCollection Individual( ActorIdentifier identifier ) public ModCollection Individual(ActorIdentifier identifier)
=> Individuals.TryGetCollection( identifier, out var c ) ? c : Default; => Individuals.TryGetCollection(identifier, out var c) ? c : Default;
// Special Collections // 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. // Return the configured collection for the given type or null.
// Does not handle Inactive, use ByName instead. // Does not handle Inactive, use ByName instead.
public ModCollection? ByType( CollectionType type ) public ModCollection? ByType(CollectionType type)
=> ByType( type, ActorIdentifier.Invalid ); => ByType(type, ActorIdentifier.Invalid);
public ModCollection? ByType( CollectionType type, ActorIdentifier identifier ) public ModCollection? ByType(CollectionType type, ActorIdentifier identifier)
{ {
if( type.IsSpecial() ) if (type.IsSpecial())
{ return _specialCollections[(int)type];
return _specialCollections[ ( int )type ];
}
return type switch return type switch
{ {
CollectionType.Default => Default, CollectionType.Default => Default,
CollectionType.Interface => Interface, CollectionType.Interface => Interface,
CollectionType.Current => Current, 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, _ => null,
}; };
} }
// Set a active collection, can be used to set Default, Current, Interface, Special, or Individual collections. // 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 var oldCollectionIdx = collectionType switch
{ {
CollectionType.Default => Default.Index, CollectionType.Default => Default.Index,
CollectionType.Interface => Interface.Index, CollectionType.Interface => Interface.Index,
CollectionType.Current => Current.Index, CollectionType.Current => Current.Index,
CollectionType.Individual => individualIndex < 0 || individualIndex >= Individuals.Count ? -1 : Individuals[ individualIndex ].Collection.Index, CollectionType.Individual => individualIndex < 0 || individualIndex >= Individuals.Count
_ when collectionType.IsSpecial() => _specialCollections[ ( int )collectionType ]?.Index ?? Default.Index, ? -1
: Individuals[individualIndex].Collection.Index,
_ when collectionType.IsSpecial() => _specialCollections[(int)collectionType]?.Index ?? Default.Index,
_ => -1, _ => -1,
}; };
if( oldCollectionIdx == -1 || newIdx == oldCollectionIdx ) if (oldCollectionIdx == -1 || newIdx == oldCollectionIdx)
{
return; return;
}
var newCollection = this[ newIdx ]; var newCollection = this[newIdx];
if( newIdx > Empty.Index ) if (newIdx > Empty.Index)
{
newCollection.CreateCache(); newCollection.CreateCache();
}
switch( collectionType ) switch (collectionType)
{ {
case CollectionType.Default: case CollectionType.Default:
Default = newCollection; Default = newCollection;
if( Penumbra.CharacterUtility.Ready && Penumbra.Config.EnableMods ) if (Penumbra.CharacterUtility.Ready && Penumbra.Config.EnableMods)
{ {
Penumbra.ResidentResources.Reload(); Penumbra.ResidentResources.Reload();
Default.SetFiles(); Default.SetFiles();
@ -112,362 +107,336 @@ public partial class ModCollection
Current = newCollection; Current = newCollection;
break; break;
case CollectionType.Individual: case CollectionType.Individual:
if( !Individuals.ChangeCollection( individualIndex, newCollection ) ) if (!Individuals.ChangeCollection(individualIndex, newCollection))
{ {
RemoveCache( newIdx ); RemoveCache(newIdx);
return; return;
} }
break; break;
default: default:
_specialCollections[ ( int )collectionType ] = newCollection; _specialCollections[(int)collectionType] = newCollection;
break; break;
} }
RemoveCache( oldCollectionIdx ); RemoveCache(oldCollectionIdx);
UpdateCurrentCollectionInUse(); UpdateCurrentCollectionInUse();
CollectionChanged.Invoke( collectionType, this[ oldCollectionIdx ], newCollection, collectionType == CollectionType.Individual ? Individuals[ individualIndex ].DisplayName : string.Empty ); _communicator.CollectionChange.Invoke(collectionType, this[oldCollectionIdx], newCollection,
collectionType == CollectionType.Individual ? Individuals[individualIndex].DisplayName : string.Empty);
} }
private void UpdateCurrentCollectionInUse() private void UpdateCurrentCollectionInUse()
=> CurrentCollectionInUse = _specialCollections => CurrentCollectionInUse = _specialCollections
.OfType< ModCollection >() .OfType<ModCollection>()
.Prepend( Interface ) .Prepend(Interface)
.Prepend( Default ) .Prepend(Default)
.Concat( Individuals.Assignments.Select( kvp => kvp.Collection ) ) .Concat(Individuals.Assignments.Select(kvp => kvp.Collection))
.SelectMany( c => c.GetFlattenedInheritance() ).Contains( Current ); .SelectMany(c => c.GetFlattenedInheritance()).Contains(Current);
public void SetCollection( ModCollection collection, CollectionType collectionType, int individualIndex = -1 ) public void SetCollection(ModCollection collection, CollectionType collectionType, int individualIndex = -1)
=> SetCollection( collection.Index, collectionType, individualIndex ); => SetCollection(collection.Index, collectionType, individualIndex);
// Create a special collection if it does not exist and set it to Empty. // 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; return false;
}
_specialCollections[ ( int )collectionType ] = Default; _specialCollections[(int)collectionType] = Default;
CollectionChanged.Invoke( collectionType, null, Default ); _communicator.CollectionChange.Invoke(collectionType, null, Default, string.Empty);
return true; return true;
} }
// Remove a special collection if it exists // Remove a special collection if it exists
public void RemoveSpecialCollection( CollectionType collectionType ) public void RemoveSpecialCollection(CollectionType collectionType)
{ {
if( !collectionType.IsSpecial() ) if (!collectionType.IsSpecial())
{
return; return;
}
var old = _specialCollections[ ( int )collectionType ]; var old = _specialCollections[(int)collectionType];
if( old != null ) if (old != null)
{ {
_specialCollections[ ( int )collectionType ] = null; _specialCollections[(int)collectionType] = null;
CollectionChanged.Invoke( collectionType, old, null ); _communicator.CollectionChange.Invoke(collectionType, old, null, string.Empty);
} }
} }
// Wrappers around Individual Collection handling. // Wrappers around Individual Collection handling.
public void CreateIndividualCollection( params ActorIdentifier[] identifiers ) public void CreateIndividualCollection(params ActorIdentifier[] identifiers)
{ {
if( Individuals.Add( identifiers, Default ) ) if (Individuals.Add(identifiers, Default))
{ _communicator.CollectionChange.Invoke(CollectionType.Individual, null, Default, Individuals.Last().DisplayName);
CollectionChanged.Invoke( CollectionType.Individual, null, Default, Individuals.Last().DisplayName );
}
} }
public void RemoveIndividualCollection( int individualIndex ) public void RemoveIndividualCollection(int individualIndex)
{ {
if( individualIndex < 0 || individualIndex >= Individuals.Count ) if (individualIndex < 0 || individualIndex >= Individuals.Count)
{
return; return;
}
var (name, old) = Individuals[ individualIndex ]; var (name, old) = Individuals[individualIndex];
if( Individuals.Delete( individualIndex ) ) if (Individuals.Delete(individualIndex))
{ _communicator.CollectionChange.Invoke(CollectionType.Individual, old, null, name);
CollectionChanged.Invoke( CollectionType.Individual, old, null, name );
}
} }
public void MoveIndividualCollection( int from, int to ) public void MoveIndividualCollection(int from, int to)
{ {
if( Individuals.Move( from, to ) ) if (Individuals.Move(from, to))
{
SaveActiveCollections(); SaveActiveCollections();
}
} }
// Obtain the index of a collection by name. // Obtain the index of a collection by name.
private int GetIndexForCollectionName( string name ) private int GetIndexForCollectionName(string name)
=> name.Length == 0 ? Empty.Index : _collections.IndexOf( c => c.Name == name ); => name.Length == 0 ? Empty.Index : _collections.IndexOf(c => c.Name == name);
public static string ActiveCollectionFile public static string ActiveCollectionFile(DalamudPluginInterface pi)
=> Path.Combine( DalamudServices.PluginInterface.ConfigDirectory.FullName, "active_collections.json" ); => Path.Combine(pi.ConfigDirectory.FullName, "active_collections.json");
// Load default, current, special, and character collections from config. // Load default, current, special, and character collections from config.
// Then create caches. If a collection does not exist anymore, reset it to an appropriate default. // Then create caches. If a collection does not exist anymore, reset it to an appropriate default.
private void LoadCollections() private void LoadCollections()
{ {
var configChanged = !ReadActiveCollections( out var jObject ); var configChanged = !ReadActiveCollections(out var jObject);
// Load the default collection. // Load the default collection.
var defaultName = jObject[ nameof( Default ) ]?.ToObject< string >() ?? ( configChanged ? DefaultCollection : Empty.Name ); var defaultName = jObject[nameof(Default)]?.ToObject<string>() ?? (configChanged ? DefaultCollection : Empty.Name);
var defaultIdx = GetIndexForCollectionName( defaultName ); var defaultIdx = GetIndexForCollectionName(defaultName);
if( defaultIdx < 0 ) if (defaultIdx < 0)
{ {
ChatUtil.NotificationMessage( $"Last choice of {ConfigWindow.DefaultCollection} {defaultName} is not available, reset to {Empty.Name}.", "Load Failure", ChatUtil.NotificationMessage(
NotificationType.Warning ); $"Last choice of {ConfigWindow.DefaultCollection} {defaultName} is not available, reset to {Empty.Name}.", "Load Failure",
NotificationType.Warning);
Default = Empty; Default = Empty;
configChanged = true; configChanged = true;
} }
else else
{ {
Default = this[ defaultIdx ]; Default = this[defaultIdx];
} }
// Load the interface collection. // Load the interface collection.
var interfaceName = jObject[ nameof( Interface ) ]?.ToObject< string >() ?? Default.Name; var interfaceName = jObject[nameof(Interface)]?.ToObject<string>() ?? Default.Name;
var interfaceIdx = GetIndexForCollectionName( interfaceName ); var interfaceIdx = GetIndexForCollectionName(interfaceName);
if( interfaceIdx < 0 ) if (interfaceIdx < 0)
{ {
ChatUtil.NotificationMessage( ChatUtil.NotificationMessage(
$"Last choice of {ConfigWindow.InterfaceCollection} {interfaceName} is not available, reset to {Empty.Name}.", "Load Failure", NotificationType.Warning ); $"Last choice of {ConfigWindow.InterfaceCollection} {interfaceName} is not available, reset to {Empty.Name}.",
"Load Failure", NotificationType.Warning);
Interface = Empty; Interface = Empty;
configChanged = true; configChanged = true;
} }
else else
{ {
Interface = this[ interfaceIdx ]; Interface = this[interfaceIdx];
} }
// Load the current collection. // Load the current collection.
var currentName = jObject[ nameof( Current ) ]?.ToObject< string >() ?? DefaultCollection; var currentName = jObject[nameof(Current)]?.ToObject<string>() ?? DefaultCollection;
var currentIdx = GetIndexForCollectionName( currentName ); var currentIdx = GetIndexForCollectionName(currentName);
if( currentIdx < 0 ) if (currentIdx < 0)
{ {
ChatUtil.NotificationMessage( ChatUtil.NotificationMessage(
$"Last choice of {ConfigWindow.SelectedCollection} {currentName} is not available, reset to {DefaultCollection}.", "Load Failure", NotificationType.Warning ); $"Last choice of {ConfigWindow.SelectedCollection} {currentName} is not available, reset to {DefaultCollection}.",
"Load Failure", NotificationType.Warning);
Current = DefaultName; Current = DefaultName;
configChanged = true; configChanged = true;
} }
else else
{ {
Current = this[ currentIdx ]; Current = this[currentIdx];
} }
// Load special collections. // Load special collections.
foreach( var (type, name, _) in CollectionTypeExtensions.Special ) foreach (var (type, name, _) in CollectionTypeExtensions.Special)
{ {
var typeName = jObject[ type.ToString() ]?.ToObject< string >(); var typeName = jObject[type.ToString()]?.ToObject<string>();
if( typeName != null ) if (typeName != null)
{ {
var idx = GetIndexForCollectionName( typeName ); var idx = GetIndexForCollectionName(typeName);
if( idx < 0 ) if (idx < 0)
{ {
ChatUtil.NotificationMessage( $"Last choice of {name} Collection {typeName} is not available, removed.", "Load Failure", NotificationType.Warning ); ChatUtil.NotificationMessage($"Last choice of {name} Collection {typeName} is not available, removed.", "Load Failure",
NotificationType.Warning);
configChanged = true; configChanged = true;
} }
else else
{ {
_specialCollections[ ( int )type ] = this[ idx ]; _specialCollections[(int)type] = this[idx];
} }
} }
} }
configChanged |= MigrateIndividualCollections( jObject ); configChanged |= MigrateIndividualCollections(jObject);
configChanged |= Individuals.ReadJObject( jObject[ nameof( Individuals ) ] as JArray, this ); configChanged |= Individuals.ReadJObject(jObject[nameof(Individuals)] as JArray, this);
// Save any changes and create all required caches. // Save any changes and create all required caches.
if( configChanged ) if (configChanged)
{
SaveActiveCollections(); SaveActiveCollections();
}
} }
// Migrate ungendered collections to Male and Female for 0.5.9.0. // Migrate ungendered collections to Male and Female for 0.5.9.0.
public static void MigrateUngenderedCollections() public static void MigrateUngenderedCollections(FilenameService fileNames)
{ {
if( !ReadActiveCollections( out var jObject ) ) if (!ReadActiveCollections(out var jObject))
{
return; return;
}
foreach( var (type, _, _) in CollectionTypeExtensions.Special.Where( t => t.Item2.StartsWith( "Male " ) ) ) foreach (var (type, _, _) in CollectionTypeExtensions.Special.Where(t => t.Item2.StartsWith("Male ")))
{ {
var oldName = type.ToString()[ 4.. ]; var oldName = type.ToString()[4..];
var value = jObject[ oldName ]; var value = jObject[oldName];
if( value == null ) if (value == null)
{
continue; continue;
}
jObject.Remove( oldName ); jObject.Remove(oldName);
jObject.Add( "Male" + oldName, value ); jObject.Add("Male" + oldName, value);
jObject.Add( "Female" + oldName, value ); jObject.Add("Female" + oldName, value);
} }
using var stream = File.Open( ActiveCollectionFile, FileMode.Truncate ); using var stream = File.Open(fileNames.ActiveCollectionsFile, FileMode.Truncate);
using var writer = new StreamWriter( stream ); using var writer = new StreamWriter(stream);
using var j = new JsonTextWriter( writer ); using var j = new JsonTextWriter(writer);
j.Formatting = Formatting.Indented; j.Formatting = Formatting.Indented;
jObject.WriteTo( j ); jObject.WriteTo(j);
} }
// Migrate individual collections to Identifiers for 0.6.0. // 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; var version = jObject[nameof(Version)]?.Value<int>() ?? 0;
if( version > 0 ) if (version > 0)
{
return false; return false;
}
// Load character collections. If a player name comes up multiple times, the last one is applied. // Load character collections. If a player name comes up multiple times, the last one is applied.
var characters = jObject[ "Characters" ]?.ToObject< Dictionary< string, string > >() ?? new Dictionary< string, string >(); var characters = jObject["Characters"]?.ToObject<Dictionary<string, string>>() ?? new Dictionary<string, string>();
var dict = new Dictionary< string, ModCollection >( characters.Count ); var dict = new Dictionary<string, ModCollection>(characters.Count);
foreach( var (player, collectionName) in characters ) foreach (var (player, collectionName) in characters)
{ {
var idx = GetIndexForCollectionName( collectionName ); var idx = GetIndexForCollectionName(collectionName);
if( idx < 0 ) if (idx < 0)
{ {
ChatUtil.NotificationMessage( $"Last choice of <{player}>'s Collection {collectionName} is not available, reset to {Empty.Name}.", "Load Failure", ChatUtil.NotificationMessage(
NotificationType.Warning ); $"Last choice of <{player}>'s Collection {collectionName} is not available, reset to {Empty.Name}.", "Load Failure",
dict.Add( player, Empty ); NotificationType.Warning);
dict.Add(player, Empty);
} }
else else
{ {
dict.Add( player, this[ idx ] ); dict.Add(player, this[idx]);
} }
} }
Individuals.Migrate0To1( dict ); Individuals.Migrate0To1(dict);
return true; return true;
} }
public void SaveActiveCollections() public void SaveActiveCollections()
{ {
Penumbra.Framework.RegisterDelayed( nameof( SaveActiveCollections ), Penumbra.Framework.RegisterDelayed(nameof(SaveActiveCollections),
SaveActiveCollectionsInternal ); SaveActiveCollectionsInternal);
} }
internal void SaveActiveCollectionsInternal() internal void SaveActiveCollectionsInternal()
{ {
var file = ActiveCollectionFile; // TODO
var file = ActiveCollectionFile(DalamudServices.PluginInterface);
try try
{ {
var jObj = new JObject var jObj = new JObject
{ {
{ nameof( Version ), Version }, { nameof(Version), Version },
{ nameof( Default ), Default.Name }, { nameof(Default), Default.Name },
{ nameof( Interface ), Interface.Name }, { nameof(Interface), Interface.Name },
{ nameof( Current ), Current.Name }, { nameof(Current), Current.Name },
}; };
foreach( var (type, collection) in _specialCollections.WithIndex().Where( p => p.Value != null ).Select( p => ( ( CollectionType )p.Index, p.Value! ) ) ) foreach (var (type, collection) in _specialCollections.WithIndex().Where(p => p.Value != null)
{ .Select(p => ((CollectionType)p.Index, p.Value!)))
jObj.Add( type.ToString(), collection.Name ); jObj.Add(type.ToString(), collection.Name);
}
jObj.Add( nameof( Individuals ), Individuals.ToJObject() ); jObj.Add(nameof(Individuals), Individuals.ToJObject());
using var stream = File.Open( file, File.Exists( file ) ? FileMode.Truncate : FileMode.CreateNew ); using var stream = File.Open(file, File.Exists(file) ? FileMode.Truncate : FileMode.CreateNew);
using var writer = new StreamWriter( stream ); using var writer = new StreamWriter(stream);
using var j = new JsonTextWriter( writer ) using var j = new JsonTextWriter(writer) { Formatting = Formatting.Indented };
{ Formatting = Formatting.Indented }; jObj.WriteTo(j);
jObj.WriteTo( j ); Penumbra.Log.Verbose("Active Collections saved.");
Penumbra.Log.Verbose( "Active Collections saved." );
} }
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. // 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. // Returns true if this is successful, false if the file does not exist or it is unsuccessful.
private static bool ReadActiveCollections( out JObject ret ) private static bool ReadActiveCollections(out JObject ret)
{ {
var file = ActiveCollectionFile; // TODO
if( File.Exists( file ) ) var file = ActiveCollectionFile(DalamudServices.PluginInterface);
{ if (File.Exists(file))
try try
{ {
ret = JObject.Parse( File.ReadAllText( file ) ); ret = JObject.Parse(File.ReadAllText(file));
return true; 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(); ret = new JObject();
return false; return false;
} }
// Save if any of the active collections is changed. // Save if any of the active collections is changed.
private void SaveOnChange( CollectionType collectionType, ModCollection? _1, ModCollection? _2, string _3 ) private void SaveOnChange(CollectionType collectionType, ModCollection? _1, ModCollection? _2, string _3)
{ {
if( collectionType != CollectionType.Inactive ) if (collectionType is not CollectionType.Inactive and not CollectionType.Temporary)
{
SaveActiveCollections(); SaveActiveCollections();
}
} }
// Cache handling. Usually recreate caches on the next framework tick, // Cache handling. Usually recreate caches on the next framework tick,
// but at launch create all of them at once. // but at launch create all of them at once.
public void CreateNecessaryCaches() public void CreateNecessaryCaches()
{ {
var tasks = _specialCollections.OfType< ModCollection >() var tasks = _specialCollections.OfType<ModCollection>()
.Concat( Individuals.Select( p => p.Collection ) ) .Concat(Individuals.Select(p => p.Collection))
.Prepend( Current ) .Prepend(Current)
.Prepend( Default ) .Prepend(Default)
.Prepend( Interface ) .Prepend(Interface)
.Distinct() .Distinct()
.Select( c => Task.Run( c.CalculateEffectiveFileListInternal ) ) .Select(c => Task.Run(c.CalculateEffectiveFileListInternal))
.ToArray(); .ToArray();
Task.WaitAll( tasks ); Task.WaitAll(tasks);
} }
private void RemoveCache( int idx ) private void RemoveCache(int idx)
{ {
if( idx != Empty.Index if (idx != Empty.Index
&& idx != Default.Index && idx != Default.Index
&& idx != Interface.Index && idx != Interface.Index
&& idx != Current.Index && idx != Current.Index
&& _specialCollections.All( c => c == null || c.Index != idx ) && _specialCollections.All(c => c == null || c.Index != idx)
&& Individuals.Select( p => p.Collection ).All( c => c.Index != idx ) ) && Individuals.Select(p => p.Collection).All(c => c.Index != idx))
{ _collections[idx].ClearCache();
_collections[ idx ].ClearCache();
}
} }
// Recalculate effective files for active collections on events. // Recalculate effective files for active collections on events.
private void OnModAddedActive( Mod mod ) private void OnModAddedActive(Mod mod)
{ {
foreach( var collection in this.Where( c => c.HasCache && c[ mod.Index ].Settings?.Enabled == true ) ) foreach (var collection in this.Where(c => c.HasCache && c[mod.Index].Settings?.Enabled == true))
{ collection._cache!.AddMod(mod, true);
collection._cache!.AddMod( mod, true );
}
} }
private void OnModRemovedActive( Mod mod ) private void OnModRemovedActive(Mod mod)
{ {
foreach( var collection in this.Where( c => c.HasCache && c[ mod.Index ].Settings?.Enabled == true ) ) foreach (var collection in this.Where(c => c.HasCache && c[mod.Index].Settings?.Enabled == true))
{ collection._cache!.RemoveMod(mod, true);
collection._cache!.RemoveMod( mod, true );
}
} }
private void OnModMovedActive( Mod mod ) private void OnModMovedActive(Mod mod)
{ {
foreach( var collection in this.Where( c => c.HasCache && c[ mod.Index ].Settings?.Enabled == true ) ) foreach (var collection in this.Where(c => c.HasCache && c[mod.Index].Settings?.Enabled == true))
{ collection._cache!.ReloadMod(mod, true);
collection._cache!.ReloadMod( mod, true );
}
} }
} }
} }

View file

@ -7,60 +7,60 @@ using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis; using System.Diagnostics.CodeAnalysis;
using System.IO; using System.IO;
using System.Linq; using System.Linq;
using Penumbra.Api;
using Penumbra.Services;
namespace Penumbra.Collections; namespace Penumbra.Collections;
public partial class ModCollection 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. private readonly Mod.Manager _modManager;
// displayName is only set for type == Individual. private readonly CommunicatorService _communicator;
public delegate void CollectionChangeDelegate( CollectionType collectionType, ModCollection? oldCollection,
ModCollection? newCollection, string displayName = "" );
private readonly Mod.Manager _modManager;
// The empty collection is always available and always has index 0. // The empty collection is always available and always has index 0.
// It can not be deleted or moved. // It can not be deleted or moved.
private readonly List< ModCollection > _collections = new() private readonly List<ModCollection> _collections = new()
{ {
Empty, Empty,
}; };
public ModCollection this[ Index idx ] public ModCollection this[Index idx]
=> _collections[ idx ]; => _collections[idx];
public ModCollection? this[ string name ] public ModCollection? this[string name]
=> ByName( name, out var c ) ? c : null; => ByName(name, out var c) ? c : null;
public int Count public int Count
=> _collections.Count; => _collections.Count;
// Obtain a collection case-independently by name. // Obtain a collection case-independently by name.
public bool ByName( string name, [NotNullWhen( true )] out ModCollection? collection ) public bool ByName(string name, [NotNullWhen(true)] out ModCollection? collection)
=> _collections.FindFirst( c => string.Equals( c.Name, name, StringComparison.OrdinalIgnoreCase ), out collection ); => _collections.FindFirst(c => string.Equals(c.Name, name, StringComparison.OrdinalIgnoreCase), out collection);
// Default enumeration skips the empty collection. // Default enumeration skips the empty collection.
public IEnumerator< ModCollection > GetEnumerator() public IEnumerator<ModCollection> GetEnumerator()
=> _collections.Skip( 1 ).GetEnumerator(); => _collections.Skip(1).GetEnumerator();
IEnumerator IEnumerable.GetEnumerator() IEnumerator IEnumerable.GetEnumerator()
=> GetEnumerator(); => GetEnumerator();
public IEnumerable< ModCollection > GetEnumeratorWithEmpty() public IEnumerable<ModCollection> GetEnumeratorWithEmpty()
=> _collections; => _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. // The collection manager reacts to changes in mods by itself.
_modManager.ModDiscoveryStarted += OnModDiscoveryStarted; _modManager.ModDiscoveryStarted += OnModDiscoveryStarted;
_modManager.ModDiscoveryFinished += OnModDiscoveryFinished; _modManager.ModDiscoveryFinished += OnModDiscoveryFinished;
_modManager.ModOptionChanged += OnModOptionsChanged; _modManager.ModOptionChanged += OnModOptionsChanged;
_modManager.ModPathChanged += OnModPathChange; _modManager.ModPathChanged += OnModPathChange;
CollectionChanged += SaveOnChange; _communicator.CollectionChange.Event += SaveOnChange;
_communicator.TemporaryGlobalModChange.Event += OnGlobalModChange;
ReadCollections(); ReadCollections();
LoadCollections(); LoadCollections();
UpdateCurrentCollectionInUse(); UpdateCurrentCollectionInUse();
@ -68,26 +68,31 @@ public partial class ModCollection
public void Dispose() public void Dispose()
{ {
_modManager.ModDiscoveryStarted -= OnModDiscoveryStarted; _communicator.CollectionChange.Event -= SaveOnChange;
_modManager.ModDiscoveryFinished -= OnModDiscoveryFinished; _communicator.TemporaryGlobalModChange.Event -= OnGlobalModChange;
_modManager.ModOptionChanged -= OnModOptionsChanged; _modManager.ModDiscoveryStarted -= OnModDiscoveryStarted;
_modManager.ModPathChanged -= OnModPathChange; _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 // Returns true if the name is not empty, it is not the name of the empty collection
// and no existing collection results in the same filename as name. // and no existing collection results in the same filename as name.
public bool CanAddCollection( string name, out string fixedName ) public bool CanAddCollection(string name, out string fixedName)
{ {
if( !IsValidName( name ) ) if (!IsValidName(name))
{ {
fixedName = string.Empty; fixedName = string.Empty;
return false; return false;
} }
name = name.RemoveInvalidPathSymbols().ToLowerInvariant(); name = name.RemoveInvalidPathSymbols().ToLowerInvariant();
if( name.Length == 0 if (name.Length == 0
|| name == Empty.Name.ToLowerInvariant() || name == Empty.Name.ToLowerInvariant()
|| _collections.Any( c => c.Name.RemoveInvalidPathSymbols().ToLowerInvariant() == name ) ) || _collections.Any(c => c.Name.RemoveInvalidPathSymbols().ToLowerInvariant() == name))
{ {
fixedName = string.Empty; fixedName = string.Empty;
return false; 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. // 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. // Returns true if the collection was successfully created and fires a Inactive event.
// Also sets the current collection to the new collection afterwards. // 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; return false;
} }
var newCollection = duplicate?.Duplicate( name ) ?? CreateNewEmpty( name ); var newCollection = duplicate?.Duplicate(name) ?? CreateNewEmpty(name);
newCollection.Index = _collections.Count; newCollection.Index = _collections.Count;
_collections.Add( newCollection ); _collections.Add(newCollection);
newCollection.Save(); newCollection.Save();
Penumbra.Log.Debug( $"Added collection {newCollection.AnonymizedName}." ); Penumbra.Log.Debug($"Added collection {newCollection.AnonymizedName}.");
CollectionChanged.Invoke( CollectionType.Inactive, null, newCollection ); _communicator.CollectionChange.Invoke(CollectionType.Inactive, null, newCollection, string.Empty);
SetCollection( newCollection.Index, CollectionType.Current ); SetCollection(newCollection.Index, CollectionType.Current);
return true; return true;
} }
// Remove the given collection if it exists and is neither the empty nor the default-named collection. // 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. // 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. // 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; 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; 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 ) var collection = _collections[idx];
{
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 ];
// Clear own inheritances. // Clear own inheritances.
foreach( var inheritance in collection.Inheritance ) foreach (var inheritance in collection.Inheritance)
{ collection.ClearSubscriptions(inheritance);
collection.ClearSubscriptions( inheritance );
}
collection.Delete(); collection.Delete();
_collections.RemoveAt( idx ); _collections.RemoveAt(idx);
// Clear external inheritances. // Clear external inheritances.
foreach( var c in _collections ) foreach (var c in _collections)
{ {
var inheritedIdx = c._inheritance.IndexOf( collection ); var inheritedIdx = c._inheritance.IndexOf(collection);
if( inheritedIdx >= 0 ) if (inheritedIdx >= 0)
{ c.RemoveInheritance(inheritedIdx);
c.RemoveInheritance( inheritedIdx );
}
if( c.Index > idx ) if (c.Index > idx)
{
--c.Index; --c.Index;
}
} }
Penumbra.Log.Debug( $"Removed collection {collection.AnonymizedName}." ); Penumbra.Log.Debug($"Removed collection {collection.AnonymizedName}.");
CollectionChanged.Invoke( CollectionType.Inactive, collection, null ); _communicator.CollectionChange.Invoke(CollectionType.Inactive, collection, null, string.Empty);
return true; return true;
} }
public bool RemoveCollection( ModCollection collection ) public bool RemoveCollection(ModCollection collection)
=> RemoveCollection( collection.Index ); => RemoveCollection(collection.Index);
private void OnModDiscoveryStarted() private void OnModDiscoveryStarted()
{ {
foreach( var collection in this ) foreach (var collection in this)
{
collection.PrepareModDiscovery(); collection.PrepareModDiscovery();
}
} }
private void OnModDiscoveryFinished() private void OnModDiscoveryFinished()
{ {
// First, re-apply all mod settings. // First, re-apply all mod settings.
foreach( var collection in this ) foreach (var collection in this)
{
collection.ApplyModSettings(); collection.ApplyModSettings();
}
// Afterwards, we update the caches. This can not happen in the same loop due to inheritance. // Afterwards, we update the caches. This can not happen in the same loop due to inheritance.
foreach( var collection in this.Where( c => c.HasCache ) ) foreach (var collection in this.Where(c => c.HasCache))
{
collection.ForceCacheUpdate(); collection.ForceCacheUpdate();
}
} }
// A changed mod path forces changes for all collections, active and inactive. // A changed mod path forces changes for all collections, active and inactive.
private void OnModPathChange( ModPathChangeType type, Mod mod, DirectoryInfo? oldDirectory, private void OnModPathChange(ModPathChangeType type, Mod mod, DirectoryInfo? oldDirectory,
DirectoryInfo? newDirectory ) DirectoryInfo? newDirectory)
{ {
switch( type ) switch (type)
{ {
case ModPathChangeType.Added: case ModPathChangeType.Added:
foreach( var collection in this ) foreach (var collection in this)
{ collection.AddMod(mod);
collection.AddMod( mod );
}
OnModAddedActive( mod ); OnModAddedActive(mod);
break; break;
case ModPathChangeType.Deleted: case ModPathChangeType.Deleted:
OnModRemovedActive( mod ); OnModRemovedActive(mod);
foreach( var collection in this ) foreach (var collection in this)
{ collection.RemoveMod(mod, mod.Index);
collection.RemoveMod( mod, mod.Index );
}
break; break;
case ModPathChangeType.Moved: case ModPathChangeType.Moved:
OnModMovedActive( mod ); OnModMovedActive(mod);
foreach( var collection in this.Where( collection => collection.Settings[ mod.Index ] != null ) ) foreach (var collection in this.Where(collection => collection.Settings[mod.Index] != null))
{
collection.Save(); collection.Save();
}
break; break;
case ModPathChangeType.StartingReload: case ModPathChangeType.StartingReload:
OnModRemovedActive( mod ); OnModRemovedActive(mod);
break; break;
case ModPathChangeType.Reloaded: case ModPathChangeType.Reloaded:
OnModAddedActive( mod ); OnModAddedActive(mod);
break; 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. // 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. // 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. // 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. // 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 } ) if (collection[mod.Index].Settings is { Enabled: true })
{ collection._cache!.RemoveMod(mod, false);
collection._cache!.RemoveMod( mod, false );
}
} }
return; 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. // Handle changes that require overwriting the collection.
if( requiresSaving ) if (requiresSaving)
{ foreach (var collection in this)
foreach( var collection in this )
{ {
if( collection._settings[ mod.Index ]?.HandleChanges( type, mod, groupIdx, optionIdx, movedToIdx ) ?? false ) if (collection._settings[mod.Index]?.HandleChanges(type, mod, groupIdx, optionIdx, movedToIdx) ?? false)
{
collection.Save(); collection.Save();
}
} }
}
// Handle changes that reload the mod if the changes did not need to be prepared, // Handle changes that reload the mod if the changes did not need to be prepared,
// or re-add the mod if they were prepared. // or re-add the mod if they were prepared.
if( recomputeList ) if (recomputeList)
{ foreach (var collection in this.Where(c => c.HasCache))
foreach( var collection in this.Where( c => c.HasCache ) )
{ {
if( collection[ mod.Index ].Settings is { Enabled: true } ) if (collection[mod.Index].Settings is { Enabled: true })
{ {
if( reload ) if (reload)
{ collection._cache!.ReloadMod(mod, true);
collection._cache!.ReloadMod( mod, true );
}
else else
{ collection._cache!.AddMod(mod, true);
collection._cache!.AddMod( mod, true );
}
} }
} }
}
} }
// Add the collection with the default name if it does not exist. // Add the collection with the default name if it does not exist.
@ -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. // This can also not be deleted, so there are always at least the empty and a collection with default name.
private void AddDefaultCollection() private void AddDefaultCollection()
{ {
var idx = GetIndexForCollectionName( DefaultCollection ); var idx = GetIndexForCollectionName(DefaultCollection);
if( idx >= 0 ) if (idx >= 0)
{ {
DefaultName = this[ idx ]; DefaultName = this[idx];
return; return;
} }
var defaultCollection = CreateNewEmpty( DefaultCollection ); var defaultCollection = CreateNewEmpty(DefaultCollection);
defaultCollection.Save(); defaultCollection.Save();
defaultCollection.Index = _collections.Count; defaultCollection.Index = _collections.Count;
_collections.Add( defaultCollection ); _collections.Add(defaultCollection);
} }
// Inheritances can not be setup before all collections are read, // Inheritances can not be setup before all collections are read,
// so this happens after reading the collections. // 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; 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; 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; 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(); collection.Save();
}
} }
} }
@ -366,38 +331,33 @@ public partial class ModCollection
// Duplicate collection files are not deleted, just not added here. // Duplicate collection files are not deleted, just not added here.
private void ReadCollections() private void ReadCollections()
{ {
var collectionDir = new DirectoryInfo( CollectionDirectory ); // TODO
var inheritances = new List< IReadOnlyList< string > >(); var collectionDir = new DirectoryInfo(CollectionDirectory(DalamudServices.PluginInterface));
if( collectionDir.Exists ) var inheritances = new List<IReadOnlyList<string>>();
{ if (collectionDir.Exists)
foreach( var file in collectionDir.EnumerateFiles( "*.json" ) ) foreach (var file in collectionDir.EnumerateFiles("*.json"))
{ {
var collection = LoadFromFile( file, out var inheritance ); var collection = LoadFromFile(file, out var inheritance);
if( collection == null || collection.Name.Length == 0 ) if (collection == null || collection.Name.Length == 0)
{
continue; continue;
}
if( file.Name != $"{collection.Name.RemoveInvalidPathSymbols()}.json" ) if (file.Name != $"{collection.Name.RemoveInvalidPathSymbols()}.json")
{ Penumbra.Log.Warning($"Collection {file.Name} does not correspond to {collection.Name}.");
Penumbra.Log.Warning( $"Collection {file.Name} does not correspond to {collection.Name}." );
}
if( this[ collection.Name ] != null ) if (this[collection.Name] != null)
{ {
Penumbra.Log.Warning( $"Duplicate collection found: {collection.Name} already exists." ); Penumbra.Log.Warning($"Duplicate collection found: {collection.Name} already exists.");
} }
else else
{ {
inheritances.Add( inheritance ); inheritances.Add(inheritance);
collection.Index = _collections.Count; collection.Index = _collections.Count;
_collections.Add( collection ); _collections.Add(collection);
} }
} }
}
AddDefaultCollection(); AddDefaultCollection();
ApplyInheritances( inheritances ); ApplyInheritances(inheritances);
} }
} }
} }

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -2,8 +2,8 @@ using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Diagnostics; using System.Diagnostics;
using System.Linq; using System.Linq;
using Dalamud.Game.ClientState.Objects;
using FFXIVClientStructs.FFXIV.Client.Game.Character; using FFXIVClientStructs.FFXIV.Client.Game.Character;
using Penumbra.Services;
namespace Penumbra.Interop.Resolver; namespace Penumbra.Interop.Resolver;
@ -14,39 +14,39 @@ public class CutsceneCharacters : IDisposable
public const int CutsceneEndIdx = CutsceneStartIdx + CutsceneSlots; public const int CutsceneEndIdx = CutsceneStartIdx + CutsceneSlots;
private readonly GameEventManager _events; private readonly GameEventManager _events;
private readonly 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 public IEnumerable<KeyValuePair<int, Dalamud.Game.ClientState.Objects.Types.GameObject>> Actors
=> Enumerable.Range( CutsceneStartIdx, CutsceneSlots ) => Enumerable.Range(CutsceneStartIdx, CutsceneSlots)
.Where( i => DalamudServices.Objects[ i ] != null ) .Where(i => _objects[i] != null)
.Select( i => KeyValuePair.Create( i, this[ i ] ?? DalamudServices.Objects[ i ]! ) ); .Select(i => KeyValuePair.Create(i, this[i] ?? _objects[i]!));
public CutsceneCharacters(GameEventManager events) public CutsceneCharacters(ObjectTable objects, GameEventManager events)
{ {
_events = events; _objects = objects;
_events = events;
Enable(); Enable();
} }
// Get the related actor to a cutscene actor. // Get the related actor to a cutscene actor.
// Does not check for valid input index. // Does not check for valid input index.
// Returns null if no connected actor is set or the actor does not exist anymore. // Returns null if no connected actor is set or the actor does not exist anymore.
public global::Dalamud.Game.ClientState.Objects.Types.GameObject? this[ int idx ] public Dalamud.Game.ClientState.Objects.Types.GameObject? this[int idx]
{ {
get get
{ {
Debug.Assert( idx is >= CutsceneStartIdx and < CutsceneEndIdx ); Debug.Assert(idx is >= CutsceneStartIdx and < CutsceneEndIdx);
idx = _copiedCharacters[ idx - CutsceneStartIdx ]; idx = _copiedCharacters[idx - CutsceneStartIdx];
return idx < 0 ? null : DalamudServices.Objects[ idx ]; return idx < 0 ? null : _objects[idx];
} }
} }
// Return the currently set index of a parent or -1 if none is set or the index is invalid. // 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 ) if (idx is >= CutsceneStartIdx and < CutsceneEndIdx)
{ return _copiedCharacters[idx - CutsceneStartIdx];
return _copiedCharacters[ idx - CutsceneStartIdx ];
}
return -1; return -1;
} }
@ -66,21 +66,21 @@ public class CutsceneCharacters : IDisposable
public void Dispose() public void Dispose()
=> Disable(); => 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; 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; 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

@ -9,68 +9,64 @@ using Penumbra.Services;
namespace Penumbra.Interop.Resolver; 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 CommunicatorService _communicator;
private readonly Dictionary< IntPtr, (ActorIdentifier, ModCollection) > _cache = new(317); private readonly GameEventManager _events;
private bool _dirty = false; private readonly Dictionary<IntPtr, (ActorIdentifier, ModCollection)> _cache = new(317);
private bool _enabled = false; 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() public void Enable()
{ {
if( _enabled ) if (_enabled)
{
return; return;
}
Penumbra.CollectionManager.CollectionChanged += CollectionChangeClear; _communicator.CollectionChange.Event += CollectionChangeClear;
Penumbra.TempMods.CollectionChanged += CollectionChangeClear; DalamudServices.ClientState.TerritoryChanged += TerritoryClear;
DalamudServices.ClientState.TerritoryChanged += TerritoryClear;
_events.CharacterDestructor += OnCharacterDestruct; _events.CharacterDestructor += OnCharacterDestruct;
_enabled = true; _enabled = true;
} }
public void Disable() public void Disable()
{ {
if( !_enabled ) if (!_enabled)
{
return; return;
}
Penumbra.CollectionManager.CollectionChanged -= CollectionChangeClear; _communicator.CollectionChange.Event -= CollectionChangeClear;
Penumbra.TempMods.CollectionChanged -= CollectionChangeClear; DalamudServices.ClientState.TerritoryChanged -= TerritoryClear;
DalamudServices.ClientState.TerritoryChanged -= TerritoryClear;
_events.CharacterDestructor -= OnCharacterDestruct; _events.CharacterDestructor -= OnCharacterDestruct;
_enabled = false; _enabled = false;
} }
public ResolveData Set( ModCollection collection, ActorIdentifier identifier, GameObject* data ) public ResolveData Set(ModCollection collection, ActorIdentifier identifier, GameObject* data)
{ {
if( _dirty ) if (_dirty)
{ {
_dirty = false; _dirty = false;
_cache.Clear(); _cache.Clear();
} }
_cache[ ( IntPtr )data ] = ( identifier, collection ); _cache[(IntPtr)data] = (identifier, collection);
return collection.ToResolveData( data ); 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; _dirty = false;
_cache.Clear(); _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; return true;
} }
@ -81,19 +77,17 @@ public unsafe class IdentifiedCollectionCache : IDisposable, IEnumerable< (IntPt
public void Dispose() public void Dispose()
{ {
Disable(); 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 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() IEnumerator IEnumerable.GetEnumerator()
=> 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; _dirty = _cache.Count > 0;
}
} }
private void TerritoryClear( object? _1, ushort _2 ) private void TerritoryClear(object? _1, ushort _2)
=> _dirty = _cache.Count > 0; => _dirty = _cache.Count > 0;
private void OnCharacterDestruct( Character* character ) private void OnCharacterDestruct(Character* character)
=> _cache.Remove( ( IntPtr )character ); => _cache.Remove((IntPtr)character);
} }

View file

@ -20,35 +20,34 @@ public unsafe partial class PathResolver
{ {
public class DrawObjectState public class DrawObjectState
{ {
private readonly CommunicatorService _communicator;
public static event CreatingCharacterBaseDelegate? CreatingCharacterBase; public static event CreatingCharacterBaseDelegate? CreatingCharacterBase;
public static event CreatedCharacterBaseDelegate? CreatedCharacterBase; public static event CreatedCharacterBaseDelegate? CreatedCharacterBase;
public IEnumerable< KeyValuePair< IntPtr, (ResolveData, int) > > DrawObjects public IEnumerable<KeyValuePair<IntPtr, (ResolveData, int)>> DrawObjects
=> _drawObjectToObject; => _drawObjectToObject;
public int Count public int Count
=> _drawObjectToObject.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; gameObject = null;
if( !_drawObjectToObject.TryGetValue( drawObject, out value ) ) if (!_drawObjectToObject.TryGetValue(drawObject, out value))
{
return false; return false;
}
var gameObjectIdx = value.Item2; var gameObjectIdx = value.Item2;
return VerifyEntry( drawObject, gameObjectIdx, out gameObject ); return VerifyEntry(drawObject, gameObjectIdx, out gameObject);
} }
// Set and update a parent object if it exists and a last game object is set. // 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 ); var collection = IdentifyCollection(LastGameObject, true);
_drawObjectToObject[ drawObject ] = ( collection, LastGameObject->ObjectIndex ); _drawObjectToObject[drawObject] = (collection, LastGameObject->ObjectIndex);
return collection; 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 if (type == ResourceType.Tex
&& LastCreatedCollection.Valid && LastCreatedCollection.Valid
&& gamePath.Path.Substring( "chara/common/texture/".Length ).StartsWith( "decal"u8 ) ) && gamePath.Path.Substring("chara/common/texture/".Length).StartsWith("decal"u8))
{ {
resolveData = LastCreatedCollection; resolveData = LastCreatedCollection;
return true; return true;
@ -76,9 +75,10 @@ public unsafe partial class PathResolver
public GameObject* LastGameObject { get; private set; } public GameObject* LastGameObject { get; private set; }
public DrawObjectState() public DrawObjectState(CommunicatorService communicator)
{ {
SignatureHelper.Initialise( this ); SignatureHelper.Initialise(this);
_communicator = communicator;
} }
public void Enable() public void Enable()
@ -88,8 +88,7 @@ public unsafe partial class PathResolver
_enableDrawHook.Enable(); _enableDrawHook.Enable();
_weaponReloadHook.Enable(); _weaponReloadHook.Enable();
InitializeDrawObjects(); InitializeDrawObjects();
Penumbra.CollectionManager.CollectionChanged += CheckCollections; _communicator.CollectionChange.Event += CheckCollections;
Penumbra.TempMods.CollectionChanged += CheckCollections;
} }
public void Disable() public void Disable()
@ -98,8 +97,7 @@ public unsafe partial class PathResolver
_characterBaseDestructorHook.Disable(); _characterBaseDestructorHook.Disable();
_enableDrawHook.Disable(); _enableDrawHook.Disable();
_weaponReloadHook.Disable(); _weaponReloadHook.Disable();
Penumbra.CollectionManager.CollectionChanged -= CheckCollections; _communicator.CollectionChange.Event -= CheckCollections;
Penumbra.TempMods.CollectionChanged -= CheckCollections;
} }
public void Dispose() public void Dispose()
@ -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. // 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 ); gameObject = (GameObject*)DalamudServices.Objects.GetObjectAddress(gameObjectIdx);
var draw = ( DrawObject* )drawObject; var draw = (DrawObject*)drawObject;
if( gameObject != null if (gameObject != null
&& ( gameObject->DrawObject == draw || draw != null && gameObject->DrawObject == draw->Object.ParentObject ) ) && (gameObject->DrawObject == draw || draw != null && gameObject->DrawObject == draw->Object.ParentObject))
{
return true; return true;
}
gameObject = null; gameObject = null;
_drawObjectToObject.Remove( drawObject ); _drawObjectToObject.Remove(drawObject);
return false; return false;
} }
// This map links DrawObjects directly to Actors (by ObjectTable index) and their collections. // 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. // It contains any DrawObjects that correspond to a human actor, even those without specific collections.
private readonly Dictionary< IntPtr, (ResolveData, int) > _drawObjectToObject = new(); private readonly Dictionary<IntPtr, (ResolveData, int)> _drawObjectToObject = new();
private ResolveData _lastCreatedCollection = ResolveData.Invalid; private ResolveData _lastCreatedCollection = ResolveData.Invalid;
// Keep track of created DrawObjects that are CharacterBase, // Keep track of created DrawObjects that are CharacterBase,
// and use the last game object that called EnableDraw to link them. // 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 ) )] [Signature(Sigs.CharacterBaseCreate, DetourName = nameof(CharacterBaseCreateDetour))]
private readonly Hook< CharacterBaseCreateDelegate > _characterBaseCreateHook = null!; 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; 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. // 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. // Change the rsp parameters.
meta = new DisposableContainer( _lastCreatedCollection.ModCollection.TemporarilySetCmpFile(), decal ); meta = new DisposableContainer(_lastCreatedCollection.ModCollection.TemporarilySetCmpFile(), decal);
try try
{ {
var modelPtr = &a; 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 try
{ {
if( LastGameObject != null && ret != IntPtr.Zero ) if (LastGameObject != null && ret != IntPtr.Zero)
{ {
_drawObjectToObject[ ret ] = ( _lastCreatedCollection!, LastGameObject->ObjectIndex ); _drawObjectToObject[ret] = (_lastCreatedCollection!, LastGameObject->ObjectIndex);
CreatedCharacterBase?.Invoke( ( IntPtr )LastGameObject, _lastCreatedCollection!.ModCollection.Name, ret ); CreatedCharacterBase?.Invoke((IntPtr)LastGameObject, _lastCreatedCollection!.ModCollection.Name, ret);
} }
} }
finally finally
@ -181,70 +177,66 @@ public unsafe partial class PathResolver
// Check the customize array for the FaceCustomization byte and the last bit of that. // Check the customize array for the FaceCustomization byte and the last bit of that.
// Also check for humans. // Also check for humans.
public static bool UsesDecal( uint modelId, IntPtr customizeData ) public static bool UsesDecal(uint modelId, IntPtr customizeData)
=> modelId == 0 && ( ( byte* )customizeData )[ 12 ] > 0x7F; => modelId == 0 && ((byte*)customizeData)[12] > 0x7F;
// Remove DrawObjects from the list when they are destroyed. // 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 ) )] [Signature(Sigs.CharacterBaseDestructor, DetourName = nameof(CharacterBaseDestructorDetour))]
private readonly Hook< CharacterBaseDestructorDelegate > _characterBaseDestructorHook = null!; private readonly Hook<CharacterBaseDestructorDelegate> _characterBaseDestructorHook = null!;
private void CharacterBaseDestructorDetour( IntPtr drawBase ) private void CharacterBaseDestructorDetour(IntPtr drawBase)
{ {
_drawObjectToObject.Remove( drawBase ); _drawObjectToObject.Remove(drawBase);
_characterBaseDestructorHook!.Original.Invoke( drawBase ); _characterBaseDestructorHook!.Original.Invoke(drawBase);
} }
// EnableDraw is what creates DrawObjects for gameObjects, // 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. // 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 ) )] [Signature(Sigs.EnableDraw, DetourName = nameof(EnableDrawDetour))]
private readonly Hook< EnableDrawDelegate > _enableDrawHook = null!; 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; var oldObject = LastGameObject;
LastGameObject = ( GameObject* )gameObject; LastGameObject = (GameObject*)gameObject;
_enableDrawHook!.Original.Invoke( gameObject, b, c, d ); _enableDrawHook!.Original.Invoke(gameObject, b, c, d);
LastGameObject = oldObject; LastGameObject = oldObject;
} }
// Not fully understood. The game object the weapon is loaded for is seemingly found at a1 + 8, // Not fully understood. The game object the weapon is loaded for is seemingly found at a1 + 8,
// so we use that. // 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 ) )] [Signature(Sigs.WeaponReload, DetourName = nameof(WeaponReloadDetour))]
private readonly Hook< WeaponReloadFunc > _weaponReloadHook = null!; 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; var oldGame = LastGameObject;
LastGameObject = *( GameObject** )( a1 + 8 ); LastGameObject = *(GameObject**)(a1 + 8);
_weaponReloadHook!.Original( a1, a2, a3, a4, a5, a6, a7 ); _weaponReloadHook!.Original(a1, a2, a3, a4, a5, a6, a7);
LastGameObject = oldGame; LastGameObject = oldGame;
} }
// Update collections linked to Game/DrawObjects due to a change in collection configuration. // 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; return;
}
foreach( var (key, (_, idx)) in _drawObjectToObject.ToArray() ) foreach (var (key, (_, idx)) in _drawObjectToObject.ToArray())
{ {
if( !VerifyEntry( key, idx, out var obj ) ) if (!VerifyEntry(key, idx, out var obj))
{ _drawObjectToObject.Remove(key);
_drawObjectToObject.Remove( key );
}
var newCollection = IdentifyCollection( obj, false ); var newCollection = IdentifyCollection(obj, false);
_drawObjectToObject[ key ] = ( newCollection, idx ); _drawObjectToObject[key] = (newCollection, idx);
} }
} }
@ -252,13 +244,11 @@ public unsafe partial class PathResolver
// We do not iterate the Dalamud table because it does not work when not logged in. // We do not iterate the Dalamud table because it does not work when not logged in.
private void InitializeDrawObjects() 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 ); var ptr = (GameObject*)DalamudServices.Objects.GetObjectAddress(i);
if( ptr != null && ptr->IsCharacter() && ptr->DrawObject != null ) if (ptr != null && ptr->IsCharacter() && ptr->DrawObject != null)
{ _drawObjectToObject[(IntPtr)ptr->DrawObject] = (IdentifyCollection(ptr, false), ptr->ObjectIndex);
_drawObjectToObject[ ( IntPtr )ptr->DrawObject ] = ( IdentifyCollection( ptr, false ), ptr->ObjectIndex );
}
} }
} }
} }

View file

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

View file

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

View file

@ -5,6 +5,7 @@ using Dalamud.Utility.Signatures;
using FFXIVClientStructs.FFXIV.Client.Game.Object; using FFXIVClientStructs.FFXIV.Client.Game.Object;
using FFXIVClientStructs.FFXIV.Client.Graphics.Scene; using FFXIVClientStructs.FFXIV.Client.Graphics.Scene;
using FFXIVClientStructs.FFXIV.Client.System.Resource; using FFXIVClientStructs.FFXIV.Client.System.Resource;
using OtterGui.Classes;
using Penumbra.Collections; using Penumbra.Collections;
using Penumbra.GameData.Enums; using Penumbra.GameData.Enums;
using Penumbra.Interop.Loader; using Penumbra.Interop.Loader;
@ -24,70 +25,70 @@ public partial class PathResolver : IDisposable
{ {
public bool Enabled { get; private set; } public bool Enabled { get; private set; }
private readonly ResourceLoader _loader; private readonly CommunicatorService _communicator;
private static readonly CutsceneCharacters Cutscenes = new(Penumbra.GameEvents); private readonly ResourceLoader _loader;
private static readonly DrawObjectState DrawObjects = new(); private static readonly CutsceneCharacters Cutscenes = new(DalamudServices.Objects, Penumbra.GameEvents); // TODO
private static readonly BitArray ValidHumanModels; private static DrawObjectState _drawObjects = null!; // TODO
internal static readonly IdentifiedCollectionCache IdentifiedCache = new(Penumbra.GameEvents); private static readonly BitArray ValidHumanModels;
private readonly AnimationState _animations; internal static IdentifiedCollectionCache IdentifiedCache = null!; // TODO
private readonly PathState _paths; private readonly AnimationState _animations;
private readonly MetaState _meta; private readonly PathState _paths;
private readonly SubfileHelper _subFiles; private readonly MetaState _meta;
private readonly SubfileHelper _subFiles;
static PathResolver() static PathResolver()
=> ValidHumanModels = GetValidHumanModels( DalamudServices.GameData ); => ValidHumanModels = GetValidHumanModels(DalamudServices.GameData);
public unsafe PathResolver( ResourceLoader loader ) public unsafe PathResolver(StartTracker timer, CommunicatorService communicator, GameEventManager events, ResourceLoader loader)
{ {
using var tApi = Penumbra.StartTimer.Measure( StartTimeType.PathResolver ); using var tApi = timer.Measure(StartTimeType.PathResolver);
SignatureHelper.Initialise( this ); _communicator = communicator;
IdentifiedCache = new IdentifiedCollectionCache(communicator, events);
SignatureHelper.Initialise(this);
_drawObjects = new DrawObjectState(_communicator);
_loader = loader; _loader = loader;
_animations = new AnimationState( DrawObjects ); _animations = new AnimationState(_drawObjects);
_paths = new PathState( this ); _paths = new PathState(this);
_meta = new MetaState( _paths.HumanVTable ); _meta = new MetaState(_paths.HumanVTable);
_subFiles = new SubfileHelper( _loader, Penumbra.GameEvents ); _subFiles = new SubfileHelper(_loader, Penumbra.GameEvents);
} }
// The modified resolver that handles game path resolving. // 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, // 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 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. // or if it is a face decal path and the current mod collection is set.
// If not use the default collection. // If not use the default collection.
// We can remove paths after they have actually been loaded. // We can remove paths after they have actually been loaded.
// A potential next request will add the path anew. // A potential next request will add the path anew.
var nonDefault = _subFiles.HandleSubFiles( type, out var resolveData ) var nonDefault = _subFiles.HandleSubFiles(type, out var resolveData)
|| _paths.Consume( gamePath.Path, out resolveData ) || _paths.Consume(gamePath.Path, out resolveData)
|| _animations.HandleFiles( type, gamePath, out resolveData ) || _animations.HandleFiles(type, gamePath, out resolveData)
|| DrawObjects.HandleDecalFile( type, gamePath, out resolveData ); || _drawObjects.HandleDecalFile(type, gamePath, out resolveData);
if( !nonDefault || !resolveData.Valid ) if (!nonDefault || !resolveData.Valid)
{
resolveData = Penumbra.CollectionManager.Default.ToResolveData(); resolveData = Penumbra.CollectionManager.Default.ToResolveData();
}
// Resolve using character/default collection first, otherwise forced, as usual. // Resolve using character/default collection first, otherwise forced, as usual.
var resolved = resolveData.ModCollection.ResolvePath( gamePath ); var resolved = resolveData.ModCollection.ResolvePath(gamePath);
// Since mtrl files load their files separately, we need to add the new, resolved path // 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. // 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. // We also need to handle defaulted materials against a non-default collection.
var path = resolved == null ? gamePath.Path : resolved.Value.InternalName; 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; return true;
} }
public void Enable() public void Enable()
{ {
if( Enabled ) if (Enabled)
{
return; return;
}
Enabled = true; Enabled = true;
Cutscenes.Enable(); Cutscenes.Enable();
DrawObjects.Enable(); _drawObjects.Enable();
IdentifiedCache.Enable(); IdentifiedCache.Enable();
_animations.Enable(); _animations.Enable();
_paths.Enable(); _paths.Enable();
@ -95,19 +96,17 @@ public partial class PathResolver : IDisposable
_subFiles.Enable(); _subFiles.Enable();
_loader.ResolvePathCustomization += CharacterResolver; _loader.ResolvePathCustomization += CharacterResolver;
Penumbra.Log.Debug( "Character Path Resolver enabled." ); Penumbra.Log.Debug("Character Path Resolver enabled.");
} }
public void Disable() public void Disable()
{ {
if( !Enabled ) if (!Enabled)
{
return; return;
}
Enabled = false; Enabled = false;
_animations.Disable(); _animations.Disable();
DrawObjects.Disable(); _drawObjects.Disable();
Cutscenes.Disable(); Cutscenes.Disable();
IdentifiedCache.Disable(); IdentifiedCache.Disable();
_paths.Disable(); _paths.Disable();
@ -115,7 +114,7 @@ public partial class PathResolver : IDisposable
_subFiles.Disable(); _subFiles.Disable();
_loader.ResolvePathCustomization -= CharacterResolver; _loader.ResolvePathCustomization -= CharacterResolver;
Penumbra.Log.Debug( "Character Path Resolver disabled." ); Penumbra.Log.Debug("Character Path Resolver disabled.");
} }
public void Dispose() public void Dispose()
@ -123,58 +122,58 @@ public partial class PathResolver : IDisposable
Disable(); Disable();
_paths.Dispose(); _paths.Dispose();
_animations.Dispose(); _animations.Dispose();
DrawObjects.Dispose(); _drawObjects.Dispose();
Cutscenes.Dispose(); Cutscenes.Dispose();
IdentifiedCache.Dispose(); IdentifiedCache.Dispose();
_meta.Dispose(); _meta.Dispose();
_subFiles.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 ); var parent = FindParent(drawObject, out var resolveData);
return ( ( IntPtr )parent, resolveData ); return ((IntPtr)parent, resolveData);
} }
public int CutsceneActor( int idx ) public int CutsceneActor(int idx)
=> Cutscenes.GetParentIndex( idx ); => Cutscenes.GetParentIndex(idx);
// Use the stored information to find the GameObject and Collection linked to a DrawObject. // Use the stored information to find the GameObject and Collection linked to a DrawObject.
public static unsafe GameObject* FindParent( IntPtr drawObject, out ResolveData resolveData ) public static unsafe GameObject* FindParent(IntPtr drawObject, out ResolveData resolveData)
{ {
if( DrawObjects.TryGetValue( drawObject, out var data, out var gameObject ) ) if (_drawObjects.TryGetValue(drawObject, out var data, out var gameObject))
{ {
resolveData = data.Item1; resolveData = data.Item1;
return gameObject; return gameObject;
} }
if( DrawObjects.LastGameObject != null if (_drawObjects.LastGameObject != null
&& ( DrawObjects.LastGameObject->DrawObject == null || DrawObjects.LastGameObject->DrawObject == ( DrawObject* )drawObject ) ) && (_drawObjects.LastGameObject->DrawObject == null || _drawObjects.LastGameObject->DrawObject == (DrawObject*)drawObject))
{ {
resolveData = IdentifyCollection( DrawObjects.LastGameObject, true ); resolveData = IdentifyCollection(_drawObjects.LastGameObject, true);
return DrawObjects.LastGameObject; return _drawObjects.LastGameObject;
} }
resolveData = IdentifyCollection( null, true ); resolveData = IdentifyCollection(null, true);
return null; 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; return resolveData;
} }
internal IEnumerable< KeyValuePair< ByteString, ResolveData > > PathCollections internal IEnumerable<KeyValuePair<ByteString, ResolveData>> PathCollections
=> _paths.Paths; => _paths.Paths;
internal IEnumerable< KeyValuePair< IntPtr, (ResolveData, int) > > DrawObjectMap internal IEnumerable<KeyValuePair<IntPtr, (ResolveData, int)>> DrawObjectMap
=> DrawObjects.DrawObjects; => _drawObjects.DrawObjects;
internal IEnumerable< KeyValuePair< int, global::Dalamud.Game.ClientState.Objects.Types.GameObject > > CutsceneActors internal IEnumerable<KeyValuePair<int, global::Dalamud.Game.ClientState.Objects.Types.GameObject>> CutsceneActors
=> Cutscenes.Actors; => Cutscenes.Actors;
internal IEnumerable< KeyValuePair< IntPtr, ResolveData > > ResourceCollections internal IEnumerable<KeyValuePair<IntPtr, ResolveData>> ResourceCollections
=> _subFiles; => _subFiles;
internal int SubfileCount internal int SubfileCount
@ -187,8 +186,8 @@ public partial class PathResolver : IDisposable
=> _subFiles.AvfxData; => _subFiles.AvfxData;
internal ResolveData LastGameObjectData internal ResolveData LastGameObjectData
=> DrawObjects.LastCreatedCollection; => _drawObjects.LastCreatedCollection;
internal unsafe nint LastGameObject internal unsafe nint LastGameObject
=> (nint) DrawObjects.LastGameObject; => (nint)_drawObjects.LastGameObject;
} }

View file

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

View file

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

View file

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

View file

@ -1,5 +1,4 @@
using System; using System;
using System.Collections.Generic;
using System.IO; using System.IO;
using System.Linq; using System.Linq;
using System.Reflection; using System.Reflection;
@ -29,45 +28,10 @@ using Penumbra.Mods;
using CharacterUtility = Penumbra.Interop.CharacterUtility; using CharacterUtility = Penumbra.Interop.CharacterUtility;
using DalamudUtil = Dalamud.Utility.Util; using DalamudUtil = Dalamud.Utility.Util;
using ResidentResourceManager = Penumbra.Interop.ResidentResourceManager; using ResidentResourceManager = Penumbra.Interop.ResidentResourceManager;
using Penumbra.Services;
namespace Penumbra; namespace Penumbra;
public class PenumbraNew
{
public string Name
=> "Penumbra";
public static readonly Logger Log = new();
public readonly StartTimeTracker< StartTimeType > StartTimer = new();
public readonly IServiceCollection Services = new ServiceCollection();
public PenumbraNew( DalamudPluginInterface pi )
{
using var time = StartTimer.Measure( StartTimeType.Total );
// Add meta services.
Services.AddSingleton( Log );
Services.AddSingleton( StartTimer );
Services.AddSingleton< ValidityChecker >();
Services.AddSingleton< PerformanceTracker< PerformanceType > >();
// Add Dalamud services
var dalamud = new DalamudServices( pi );
dalamud.AddServices( Services );
// Add Game Data
// Add Configuration
Services.AddSingleton< Configuration >();
}
public void Dispose()
{ }
}
public class Penumbra : IDalamudPlugin public class Penumbra : IDalamudPlugin
{ {
public string Name public string Name
@ -76,135 +40,123 @@ public class Penumbra : IDalamudPlugin
public static readonly string Version = Assembly.GetExecutingAssembly().GetName().Version?.ToString() ?? string.Empty; public static readonly string Version = Assembly.GetExecutingAssembly().GetName().Version?.ToString() ?? string.Empty;
public static readonly string CommitHash = 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 Configuration Config { get; private set; } = null!;
public static ResidentResourceManager ResidentResources { get; private set; } = null!; public static ResidentResourceManager ResidentResources { get; private set; } = null!;
public static CharacterUtility CharacterUtility { get; private set; } = null!; public static CharacterUtility CharacterUtility { get; private set; } = null!;
public static GameEventManager GameEvents { get; private set; } = null!; public static GameEventManager GameEvents { get; private set; } = null!;
public static MetaFileManager MetaFileManager { get; private set; } = null!; public static MetaFileManager MetaFileManager { get; private set; } = null!;
public static Mod.Manager ModManager { get; private set; } = null!; public static Mod.Manager ModManager { get; private set; } = null!;
public static ModCollection.Manager CollectionManager { get; private set; } = null!; public static ModCollection.Manager CollectionManager { get; private set; } = null!;
public static TempModManager TempMods { get; private set; } = null!; public static TempCollectionManager TempCollections { get; private set; } = null!;
public static ResourceLoader ResourceLoader { get; private set; } = null!; public static TempModManager TempMods { get; private set; } = null!;
public static FrameworkManager Framework { get; private set; } = null!; public static ResourceLoader ResourceLoader { get; private set; } = null!;
public static ActorManager Actors { get; private set; } = null!; public static FrameworkManager Framework { get; private set; } = null!;
public static IObjectIdentifier Identifier { get; private set; } = null!; public static ActorManager Actors { get; private set; } = null!;
public static IGamePathParser GamePathParser { get; private set; } = null!; public static IObjectIdentifier Identifier { get; private set; } = null!;
public static StainManager StainManager { 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 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; private readonly PenumbraNew _tmp;
public readonly PathResolver PathResolver; public static ItemData ItemData { get; private set; } = null!;
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 static ItemData ItemData { get; private set; } = null!; public Penumbra(DalamudPluginInterface pluginInterface)
public Penumbra( DalamudPluginInterface pluginInterface )
{ {
using var time = StartTimer.Measure( StartTimeType.Total ); Log = PenumbraNew.Log;
_tmp = new PenumbraNew(pluginInterface);
Performance = _tmp.Services.GetRequiredService<PerformanceTracker>();
ValidityChecker = _tmp.Services.GetRequiredService<ValidityChecker>();
_tmp.Services.GetRequiredService<BackupService>();
Config = _tmp.Services.GetRequiredService<Configuration>();
CharacterUtility = _tmp.Services.GetRequiredService<CharacterUtility>();
GameEvents = _tmp.Services.GetRequiredService<GameEventManager>();
MetaFileManager = _tmp.Services.GetRequiredService<MetaFileManager>();
Framework = _tmp.Services.GetRequiredService<FrameworkManager>();
Actors = _tmp.Services.GetRequiredService<ActorService>().AwaitedService;
Identifier = _tmp.Services.GetRequiredService<IdentifierService>().AwaitedService;
GamePathParser = _tmp.Services.GetRequiredService<IGamePathParser>();
StainService = _tmp.Services.GetRequiredService<StainService>();
ItemData = _tmp.Services.GetRequiredService<ItemService>().AwaitedService;
Dalamud = _tmp.Services.GetRequiredService<DalamudServices>();
TempMods = _tmp.Services.GetRequiredService<TempModManager>();
try try
{ {
DalamudServices.Initialize( pluginInterface ); ResourceLoader = new ResourceLoader(this);
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.EnableHooks(); ResourceLoader.EnableHooks();
_resourceWatcher = new ResourceWatcher( ResourceLoader ); _resourceWatcher = new ResourceWatcher(ResourceLoader);
ResourceLogger = new ResourceLogger( ResourceLoader );
ResidentResources = new ResidentResourceManager(); ResidentResources = new ResidentResourceManager();
StartTimer.Measure( StartTimeType.Mods, () => _tmp.Services.GetRequiredService<StartTracker>().Measure(StartTimeType.Mods, () =>
{ {
ModManager = new Mod.Manager( Config.ModDirectory ); ModManager = new Mod.Manager(Config.ModDirectory);
ModManager.DiscoverMods(); ModManager.DiscoverMods();
} ); });
StartTimer.Measure( StartTimeType.Collections, () => _tmp.Services.GetRequiredService<StartTracker>().Measure(StartTimeType.Collections, () =>
{ {
CollectionManager = new ModCollection.Manager( ModManager ); CollectionManager = new ModCollection.Manager(_tmp.Services.GetRequiredService<CommunicatorService>(), ModManager);
CollectionManager.CreateNecessaryCaches(); CollectionManager.CreateNecessaryCaches();
} ); });
TempCollections = _tmp.Services.GetRequiredService<TempCollectionManager>();
ModFileSystem = ModFileSystem.Load(); ModFileSystem = ModFileSystem.Load();
ObjectReloader = new ObjectReloader(); ObjectReloader = new ObjectReloader();
PathResolver = new PathResolver( ResourceLoader ); PathResolver = new PathResolver(_tmp.Services.GetRequiredService<StartTracker>(), _tmp.Services.GetRequiredService<CommunicatorService>(), _tmp.Services.GetRequiredService<GameEventManager>(), ResourceLoader);
SetupInterface(); SetupInterface();
if( Config.EnableMods ) if (Config.EnableMods)
{ {
ResourceLoader.EnableReplacements(); ResourceLoader.EnableReplacements();
PathResolver.Enable(); PathResolver.Enable();
} }
if( Config.DebugMode ) if (Config.DebugMode)
{
ResourceLoader.EnableDebug(); ResourceLoader.EnableDebug();
}
using( var tApi = StartTimer.Measure( StartTimeType.Api ) ) using (var tApi = _tmp.Services.GetRequiredService<StartTracker>().Measure(StartTimeType.Api))
{ {
Api = new PenumbraApi( this ); Api = new PenumbraApi(_tmp.Services.GetRequiredService<CommunicatorService>(), this);
IpcProviders = new PenumbraIpcProviders( DalamudServices.PluginInterface, Api ); IpcProviders = new PenumbraIpcProviders(DalamudServices.PluginInterface, Api);
HttpApi = new HttpApi( Api ); HttpApi = new HttpApi(Api);
if( Config.EnableHttpApi ) if (Config.EnableHttpApi)
{
HttpApi.CreateWebServer(); HttpApi.CreateWebServer();
}
SubscribeItemLinks(); SubscribeItemLinks();
} }
ValidityChecker.LogExceptions(); ValidityChecker.LogExceptions();
Log.Information( $"Penumbra Version {Version}, Commit #{CommitHash} successfully Loaded from {pluginInterface.SourceRepository}." ); Log.Information($"Penumbra Version {Version}, Commit #{CommitHash} successfully Loaded from {pluginInterface.SourceRepository}.");
OtterTex.NativeDll.Initialize( DalamudServices.PluginInterface.AssemblyLocation.DirectoryName ); OtterTex.NativeDll.Initialize(DalamudServices.PluginInterface.AssemblyLocation.DirectoryName);
Log.Information( $"Loading native OtterTex assembly from {OtterTex.NativeDll.Directory}." ); Log.Information($"Loading native OtterTex assembly from {OtterTex.NativeDll.Directory}.");
if( CharacterUtility.Ready ) if (CharacterUtility.Ready)
{
ResidentResources.Reload(); ResidentResources.Reload();
}
} }
catch catch
{ {
@ -215,21 +167,22 @@ public class Penumbra : IDalamudPlugin
private void SetupInterface() 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 changelog = ConfigWindow.CreateChangelog();
var cfg = new ConfigWindow( this, _resourceWatcher ) var cfg = new ConfigWindow(_tmp.Services.GetRequiredService<CommunicatorService>(), _tmp.Services.GetRequiredService<StartTracker>(), this, _resourceWatcher)
{ {
IsOpen = Config.DebugMode, IsOpen = Config.DebugMode,
}; };
var btn = new LaunchButton( cfg ); var btn = new LaunchButton(cfg);
var system = new WindowSystem( Name ); var system = new WindowSystem(Name);
var cmd = new CommandHandler( DalamudServices.Commands, ObjectReloader, Config, this, cfg, ModManager, CollectionManager, Actors ); var cmd = new CommandHandler(DalamudServices.Commands, ObjectReloader, Config, this, cfg, ModManager, CollectionManager,
system.AddWindow( cfg ); Actors);
system.AddWindow( cfg.ModEditPopup ); system.AddWindow(cfg);
system.AddWindow( changelog ); system.AddWindow(cfg.ModEditPopup);
if( !_disposed ) system.AddWindow(changelog);
if (!_disposed)
{ {
_changelog = changelog; _changelog = changelog;
ConfigWindow = cfg; ConfigWindow = cfg;
@ -251,100 +204,87 @@ public class Penumbra : IDalamudPlugin
private void DisposeInterface() private void DisposeInterface()
{ {
if( _windowSystem != null ) if (_windowSystem != null)
{
DalamudServices.PluginInterface.UiBuilder.Draw -= _windowSystem.Draw; DalamudServices.PluginInterface.UiBuilder.Draw -= _windowSystem.Draw;
}
_launchButton?.Dispose(); _launchButton?.Dispose();
if( ConfigWindow != null ) if (ConfigWindow != null)
{ {
DalamudServices.PluginInterface.UiBuilder.OpenConfigUi -= ConfigWindow.Toggle; DalamudServices.PluginInterface.UiBuilder.OpenConfigUi -= ConfigWindow.Toggle;
ConfigWindow.Dispose(); 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; return false;
}
Config.EnableMods = enabled; Config.EnableMods = enabled;
if( enabled ) if (enabled)
{ {
ResourceLoader.EnableReplacements(); ResourceLoader.EnableReplacements();
PathResolver.Enable(); PathResolver.Enable();
if( CharacterUtility.Ready ) if (CharacterUtility.Ready)
{ {
CollectionManager.Default.SetFiles(); CollectionManager.Default.SetFiles();
ResidentResources.Reload(); ResidentResources.Reload();
ObjectReloader.RedrawAll( RedrawType.Redraw ); ObjectReloader.RedrawAll(RedrawType.Redraw);
} }
} }
else else
{ {
ResourceLoader.DisableReplacements(); ResourceLoader.DisableReplacements();
PathResolver.Disable(); PathResolver.Disable();
if( CharacterUtility.Ready ) if (CharacterUtility.Ready)
{ {
CharacterUtility.ResetAll(); CharacterUtility.ResetAll();
ResidentResources.Reload(); ResidentResources.Reload();
ObjectReloader.RedrawAll( RedrawType.Redraw ); ObjectReloader.RedrawAll(RedrawType.Redraw);
} }
} }
Config.Save(); Config.Save();
EnabledChange?.Invoke( enabled ); EnabledChange?.Invoke(enabled);
return true; return true;
} }
public void ForceChangelogOpen() public void ForceChangelogOpen()
{ {
if( _changelog != null ) if (_changelog != null)
{
_changelog.ForceOpen = true; _changelog.ForceOpen = true;
}
} }
private void SubscribeItemLinks() private void SubscribeItemLinks()
{ {
Api.ChangedItemTooltip += it => Api.ChangedItemTooltip += it =>
{ {
if( it is Item ) if (it is Item)
{ ImGui.TextUnformatted("Left Click to create an item link in chat.");
ImGui.TextUnformatted( "Left Click to create an item link in chat." );
}
}; };
Api.ChangedItemClicked += ( button, it ) => Api.ChangedItemClicked += (button, it) =>
{ {
if( button == MouseButton.Left && it is Item item ) if (button == MouseButton.Left && it is Item item)
{ ChatUtil.LinkItem(item);
ChatUtil.LinkItem( item );
}
}; };
} }
private short ResolveCutscene( ushort index )
=> ( short )PathResolver.CutsceneActor( index );
public void Dispose() public void Dispose()
{ {
if( _disposed ) if (_disposed)
{
return; return;
}
// TODO
_tmp?.Dispose();
_disposed = true; _disposed = true;
HttpApi?.Dispose(); HttpApi?.Dispose();
IpcProviders?.Dispose(); IpcProviders?.Dispose();
Api?.Dispose(); Api?.Dispose();
_commandHandler?.Dispose(); _commandHandler?.Dispose();
StainManager?.Dispose(); StainService?.Dispose();
ItemData?.Dispose(); ItemData?.Dispose();
Actors?.Dispose(); Actors?.Dispose();
Identifier?.Dispose(); Identifier?.Dispose();
@ -354,7 +294,6 @@ public class Penumbra : IDalamudPlugin
ModFileSystem?.Dispose(); ModFileSystem?.Dispose();
CollectionManager?.Dispose(); CollectionManager?.Dispose();
PathResolver?.Dispose(); PathResolver?.Dispose();
ResourceLogger?.Dispose();
_resourceWatcher?.Dispose(); _resourceWatcher?.Dispose();
ResourceLoader?.Dispose(); ResourceLoader?.Dispose();
GameEvents?.Dispose(); GameEvents?.Dispose();
@ -362,90 +301,77 @@ public class Penumbra : IDalamudPlugin
Performance?.Dispose(); Performance?.Dispose();
} }
// Collect all relevant files for penumbra configuration. public string GatherSupportInformation()
private static IReadOnlyList< FileInfo > PenumbraBackupFiles()
{ {
var collectionDir = ModCollection.CollectionDirectory; var sb = new StringBuilder(10240);
var list = Directory.Exists( collectionDir ) var exists = Config.ModDirectory.Length > 0 && Directory.Exists(Config.ModDirectory);
? new DirectoryInfo( collectionDir ).EnumerateFiles( "*.json" ).ToList() var drive = exists ? new DriveInfo(new DirectoryInfo(Config.ModDirectory).Root.FullName) : null;
: new List< FileInfo >(); sb.AppendLine("**Settings**");
list.AddRange( Mod.LocalDataDirectory.Exists ? Mod.LocalDataDirectory.EnumerateFiles( "*.json" ) : Enumerable.Empty< FileInfo >() ); sb.Append($"> **`Plugin Version: `** {Version}\n");
list.Add( DalamudServices.PluginInterface.ConfigFile ); sb.Append($"> **`Commit Hash: `** {CommitHash}\n");
list.Add( new FileInfo( ModFileSystem.ModFileSystemFile ) ); sb.Append($"> **`Enable Mods: `** {Config.EnableMods}\n");
list.Add( new FileInfo( ModCollection.Manager.ActiveCollectionFile ) ); sb.Append($"> **`Enable HTTP API: `** {Config.EnableHttpApi}\n");
return list; sb.Append($"> **`Operating System: `** {(DalamudUtil.IsLinux() ? "Mac/Linux (Wine)" : "Windows")}\n");
} sb.Append($"> **`Root Directory: `** `{Config.ModDirectory}`, {(exists ? "Exists" : "Not Existing")}\n");
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" );
sb.Append( sb.Append(
$"> **`Synchronous Load (Dalamud): `** {( DalamudServices.GetDalamudConfig( DalamudServices.WaitingForPluginsOption, out bool v ) ? v.ToString() : "Unknown" )}\n" ); $"> **`Free Drive Space: `** {(drive != null ? Functions.HumanReadableSize(drive.AvailableFreeSpace) : "Unknown")}\n");
sb.Append( $"> **`Logging: `** Log: {Config.EnableResourceLogging}, Watcher: {Config.EnableResourceWatcher} ({Config.MaxResourceWatcherRecords})\n" ); sb.Append($"> **`Auto-Deduplication: `** {Config.AutoDeduplicateOnImport}\n");
sb.Append( $"> **`Use Ownership: `** {Config.UseOwnerNameForCharacterCollection}\n" ); sb.Append($"> **`Debug Mode: `** {Config.DebugMode}\n");
sb.AppendLine( "**Mods**" ); sb.Append(
sb.Append( $"> **`Installed Mods: `** {ModManager.Count}\n" ); $"> **`Synchronous Load (Dalamud): `** {(_tmp.Services.GetRequiredService<DalamudServices>().GetDalamudConfig(DalamudServices.WaitingForPluginsOption, out bool v) ? v.ToString() : "Unknown")}\n");
sb.Append( $"> **`Mods with Config: `** {ModManager.Count( m => m.HasOptions )}\n" ); sb.Append(
sb.Append( $"> **`Mods with File Redirections: `** {ModManager.Count( m => m.TotalFileCount > 0 )}, Total: {ModManager.Sum( m => m.TotalFileCount )}\n" ); $"> **`Logging: `** Log: {Config.EnableResourceLogging}, Watcher: {Config.EnableResourceWatcher} ({Config.MaxResourceWatcherRecords})\n");
sb.Append( $"> **`Mods with FileSwaps: `** {ModManager.Count( m => m.TotalSwapCount > 0 )}, Total: {ModManager.Sum( m => m.TotalSwapCount )}\n" ); sb.Append($"> **`Use Ownership: `** {Config.UseOwnerNameForCharacterCollection}\n");
sb.Append( $"> **`Mods with Meta Manipulations:`** {ModManager.Count( m => m.TotalManipulations > 0 )}, Total {ModManager.Sum( m => m.TotalManipulations )}\n" ); sb.AppendLine("**Mods**");
sb.Append( $"> **`IMC Exceptions Thrown: `** {ValidityChecker.ImcExceptions.Count}\n" ); sb.Append($"> **`Installed Mods: `** {ModManager.Count}\n");
sb.Append( $"> **`#Temp Mods: `** {TempMods.Mods.Sum( kvp => kvp.Value.Count ) + TempMods.ModsForAllCollections.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 ); var parts = name.Split(' ', 3);
return string.Join( " ", parts.Length != 3 ? parts.Select( n => $"{n[ 0 ]}." ) : parts[ ..2 ].Select( n => $"{n[ 0 ]}." ).Append( parts[ 2 ] ) ); return string.Join(" ",
parts.Length != 3 ? parts.Select(n => $"{n[0]}.") : parts[..2].Select(n => $"{n[0]}.").Append(parts[2]));
} }
return name + ':'; return name + ':';
} }
void PrintCollection( ModCollection c ) void PrintCollection(ModCollection c)
=> sb.Append( $"**Collection {c.AnonymizedName}**\n" => sb.Append($"**Collection {c.AnonymizedName}**\n"
+ $"> **`Inheritances: `** {c.Inheritance.Count}\n" + $"> **`Inheritances: `** {c.Inheritance.Count}\n"
+ $"> **`Enabled Mods: `** {c.ActualSettings.Count( s => s is { Enabled: true } )}\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" ); + $"> **`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.AppendLine("**Collections**");
sb.Append( $"> **`#Collections: `** {CollectionManager.Count - 1}\n" ); sb.Append($"> **`#Collections: `** {CollectionManager.Count - 1}\n");
sb.Append( $"> **`#Temp Collections: `** {TempMods.CustomCollections.Count}\n" ); sb.Append($"> **`#Temp Collections: `** {TempCollections.Count}\n");
sb.Append( $"> **`Active Collections: `** {CollectionManager.Count( c => c.HasCache )}\n" ); sb.Append($"> **`Active Collections: `** {CollectionManager.Count(c => c.HasCache)}\n");
sb.Append( $"> **`Base Collection: `** {CollectionManager.Default.AnonymizedName}\n" ); sb.Append($"> **`Base Collection: `** {CollectionManager.Default.AnonymizedName}\n");
sb.Append( $"> **`Interface Collection: `** {CollectionManager.Interface.AnonymizedName}\n" ); sb.Append($"> **`Interface Collection: `** {CollectionManager.Interface.AnonymizedName}\n");
sb.Append( $"> **`Selected Collection: `** {CollectionManager.Current.AnonymizedName}\n" ); sb.Append($"> **`Selected Collection: `** {CollectionManager.Current.AnonymizedName}\n");
foreach( var (type, name, _) in CollectionTypeExtensions.Special ) foreach (var (type, name, _) in CollectionTypeExtensions.Special)
{ {
var collection = CollectionManager.ByType( type ); var collection = CollectionManager.ByType(type);
if( collection != null ) if (collection != null)
{ sb.Append($"> **`{name,-30}`** {collection.AnonymizedName}\n");
sb.Append( $"> **`{name,-30}`** {collection.AnonymizedName}\n" );
}
} }
foreach( var (name, id, collection) in CollectionManager.Individuals.Assignments ) foreach (var (name, id, collection) in CollectionManager.Individuals.Assignments)
{ sb.Append($"> **`{CharacterName(id[0], name),-30}`** {collection.AnonymizedName}\n");
sb.Append( $"> **`{CharacterName( id[ 0 ], name ),-30}`** {collection.AnonymizedName}\n" );
}
foreach( var collection in CollectionManager.Where( c => c.HasCache ) ) foreach (var collection in CollectionManager.Where(c => c.HasCache))
{ PrintCollection(collection);
PrintCollection( collection );
}
return sb.ToString(); return sb.ToString();
} }

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -17,6 +17,7 @@ using Penumbra.GameData.Enums;
using Penumbra.GameData.Structs; using Penumbra.GameData.Structs;
using Penumbra.Mods; using Penumbra.Mods;
using Penumbra.Mods.ItemSwap; using Penumbra.Mods.ItemSwap;
using Penumbra.Services;
using Penumbra.Util; using Penumbra.Util;
namespace Penumbra.UI.Classes; namespace Penumbra.UI.Classes;
@ -42,49 +43,61 @@ public class ItemSwapWindow : IDisposable
Weapon, Weapon,
} }
private class ItemSelector : FilterComboCache< (string, Item) > private class ItemSelector : FilterComboCache<(string, Item)>
{ {
public ItemSelector( FullEquipType type ) public ItemSelector(FullEquipType type)
: base( () => Penumbra.ItemData[ type ].Select( i => ( i.Name.ToDalamudString().TextValue, i ) ).ToArray() ) : 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; => obj.Item1;
} }
private class WeaponSelector : FilterComboCache< FullEquipType > private class WeaponSelector : FilterComboCache<FullEquipType>
{ {
public WeaponSelector() 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(); => type.ToName();
} }
public ItemSwapWindow() private readonly CommunicatorService _communicator;
public ItemSwapWindow(CommunicatorService communicator)
{ {
Penumbra.CollectionManager.CollectionChanged += OnCollectionChange; _communicator = communicator;
_communicator.CollectionChange.Event += OnCollectionChange;
Penumbra.CollectionManager.Current.ModSettingChanged += OnSettingChange; Penumbra.CollectionManager.Current.ModSettingChanged += OnSettingChange;
} }
public void Dispose() public void Dispose()
{ {
Penumbra.CollectionManager.CollectionChanged += OnCollectionChange; _communicator.CollectionChange.Event -= OnCollectionChange;
Penumbra.CollectionManager.Current.ModSettingChanged -= OnSettingChange; Penumbra.CollectionManager.Current.ModSettingChanged -= OnSettingChange;
} }
private readonly Dictionary< SwapType, (ItemSelector Source, ItemSelector Target, string TextFrom, string TextTo) > _selectors = new() private readonly Dictionary<SwapType, (ItemSelector Source, ItemSelector Target, string TextFrom, string TextTo)> _selectors = new()
{ {
[ SwapType.Hat ] = ( new ItemSelector( FullEquipType.Head ), new ItemSelector( FullEquipType.Head ), "Take this Hat", "and put it on this one" ), [SwapType.Hat] =
[ SwapType.Top ] = ( new ItemSelector( FullEquipType.Body ), new ItemSelector( FullEquipType.Body ), "Take this Top", "and put it on this one" ), (new ItemSelector(FullEquipType.Head), new ItemSelector(FullEquipType.Head), "Take this Hat", "and put it on this one"),
[ SwapType.Gloves ] = ( new ItemSelector( FullEquipType.Hands ), new ItemSelector( FullEquipType.Hands ), "Take these Gloves", "and put them on these" ), [SwapType.Top] =
[ SwapType.Pants ] = ( new ItemSelector( FullEquipType.Legs ), new ItemSelector( FullEquipType.Legs ), "Take these Pants", "and put them on these" ), (new ItemSelector(FullEquipType.Body), new ItemSelector(FullEquipType.Body), "Take this Top", "and put it on this one"),
[ SwapType.Shoes ] = ( new ItemSelector( FullEquipType.Feet ), new ItemSelector( FullEquipType.Feet ), "Take these Shoes", "and put them on these" ), [SwapType.Gloves] =
[ SwapType.Earrings ] = ( new ItemSelector( FullEquipType.Ears ), new ItemSelector( FullEquipType.Ears ), "Take these Earrings", "and put them on these" ), (new ItemSelector(FullEquipType.Hands), new ItemSelector(FullEquipType.Hands), "Take these Gloves", "and put them on these"),
[ SwapType.Necklace ] = ( new ItemSelector( FullEquipType.Neck ), new ItemSelector( FullEquipType.Neck ), "Take this Necklace", "and put it on this one" ), [SwapType.Pants] =
[ SwapType.Bracelet ] = ( new ItemSelector( FullEquipType.Wrists ), new ItemSelector( FullEquipType.Wrists ), "Take these Bracelets", "and put them on these" ), (new ItemSelector(FullEquipType.Legs), new ItemSelector(FullEquipType.Legs), "Take these Pants", "and put them on these"),
[ SwapType.Ring ] = ( new ItemSelector( FullEquipType.Finger ), new ItemSelector( FullEquipType.Finger ), "Take this Ring", "and put it on this one" ), [SwapType.Shoes] =
(new ItemSelector(FullEquipType.Feet), new ItemSelector(FullEquipType.Feet), "Take these Shoes", "and put them on these"),
[SwapType.Earrings] =
(new ItemSelector(FullEquipType.Ears), new ItemSelector(FullEquipType.Ears), "Take these Earrings", "and put them on these"),
[SwapType.Necklace] =
(new ItemSelector(FullEquipType.Neck), new ItemSelector(FullEquipType.Neck), "Take this Necklace", "and put it on this one"),
[SwapType.Bracelet] =
(new ItemSelector(FullEquipType.Wrists), new ItemSelector(FullEquipType.Wrists), "Take these Bracelets", "and put them on these"),
[SwapType.Ring] = (new ItemSelector(FullEquipType.Finger), new ItemSelector(FullEquipType.Finger), "Take this Ring",
"and put it on this one"),
}; };
private ItemSelector? _weaponSource = null; private ItemSelector? _weaponSource = null;
@ -117,39 +130,33 @@ public class ItemSwapWindow : IDisposable
private Item[]? _affectedItems; 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; return;
}
var oldDefaultName = $"{_mod?.Name.Text ?? "Unknown"} (Swapped)"; var oldDefaultName = $"{_mod?.Name.Text ?? "Unknown"} (Swapped)";
if( _newModName.Length == 0 || oldDefaultName == _newModName ) if (_newModName.Length == 0 || oldDefaultName == _newModName)
{
_newModName = $"{mod.Name.Text} (Swapped)"; _newModName = $"{mod.Name.Text} (Swapped)";
}
_mod = mod; _mod = mod;
_modSettings = settings; _modSettings = settings;
_swapData.LoadMod( _mod, _modSettings ); _swapData.LoadMod(_mod, _modSettings);
UpdateOption(); UpdateOption();
_dirty = true; _dirty = true;
} }
private void UpdateState() private void UpdateState()
{ {
if( !_dirty ) if (!_dirty)
{
return; return;
}
_swapData.Clear(); _swapData.Clear();
_loadException = null; _loadException = null;
_affectedItems = null; _affectedItems = null;
try try
{ {
switch( _lastTab ) switch (_lastTab)
{ {
case SwapType.Hat: case SwapType.Hat:
case SwapType.Top: case SwapType.Top:
@ -178,27 +185,31 @@ public class ItemSwapWindow : IDisposable
} }
break; break;
case SwapType.Hair when _targetId > 0 && _sourceId > 0: case SwapType.Hair when _targetId > 0 && _sourceId > 0:
_swapData.LoadCustomization( BodySlot.Hair, Names.CombinedRace( _currentGender, _currentRace ), ( SetId )_sourceId, ( SetId )_targetId, _swapData.LoadCustomization(BodySlot.Hair, Names.CombinedRace(_currentGender, _currentRace), (SetId)_sourceId,
_useCurrentCollection ? Penumbra.CollectionManager.Current : null ); (SetId)_targetId,
_useCurrentCollection ? Penumbra.CollectionManager.Current : null);
break; break;
case SwapType.Face when _targetId > 0 && _sourceId > 0: case SwapType.Face when _targetId > 0 && _sourceId > 0:
_swapData.LoadCustomization( BodySlot.Face, Names.CombinedRace( _currentGender, _currentRace ), ( SetId )_sourceId, ( SetId )_targetId, _swapData.LoadCustomization(BodySlot.Face, Names.CombinedRace(_currentGender, _currentRace), (SetId)_sourceId,
_useCurrentCollection ? Penumbra.CollectionManager.Current : null ); (SetId)_targetId,
_useCurrentCollection ? Penumbra.CollectionManager.Current : null);
break; break;
case SwapType.Ears when _targetId > 0 && _sourceId > 0: case SwapType.Ears when _targetId > 0 && _sourceId > 0:
_swapData.LoadCustomization( BodySlot.Zear, Names.CombinedRace( _currentGender, ModelRace.Viera ), ( SetId )_sourceId, ( SetId )_targetId, _swapData.LoadCustomization(BodySlot.Zear, Names.CombinedRace(_currentGender, ModelRace.Viera), (SetId)_sourceId,
_useCurrentCollection ? Penumbra.CollectionManager.Current : null ); (SetId)_targetId,
_useCurrentCollection ? Penumbra.CollectionManager.Current : null);
break; break;
case SwapType.Tail when _targetId > 0 && _sourceId > 0: case SwapType.Tail when _targetId > 0 && _sourceId > 0:
_swapData.LoadCustomization( BodySlot.Tail, Names.CombinedRace( _currentGender, _currentRace ), ( SetId )_sourceId, ( SetId )_targetId, _swapData.LoadCustomization(BodySlot.Tail, Names.CombinedRace(_currentGender, _currentRace), (SetId)_sourceId,
_useCurrentCollection ? Penumbra.CollectionManager.Current : null ); (SetId)_targetId,
_useCurrentCollection ? Penumbra.CollectionManager.Current : null);
break; break;
case SwapType.Weapon: 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; _loadException = e;
_affectedItems = null; _affectedItems = null;
_swapData.Clear(); _swapData.Clear();
@ -207,13 +218,14 @@ public class ItemSwapWindow : IDisposable
_dirty = false; _dirty = false;
} }
private static string SwapToString( Swap swap ) private static string SwapToString(Swap swap)
{ {
return swap switch return swap switch
{ {
MetaSwap meta => $"{meta.SwapFrom}: {meta.SwapFrom.EntryToString()} -> {meta.SwapApplied.EntryToString()}", MetaSwap meta => $"{meta.SwapFrom}: {meta.SwapFrom.EntryToString()} -> {meta.SwapApplied.EntryToString()}",
FileSwap file => $"{file.Type}: {file.SwapFromRequestPath} -> {file.SwapToModded.FullName}{( file.DataWasChanged ? " (EDITED)" : string.Empty )}", FileSwap file =>
_ => string.Empty, $"{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() private void UpdateOption()
{ {
_selectedGroup = _mod?.Groups.FirstOrDefault( g => g.Name == _newGroupName ); _selectedGroup = _mod?.Groups.FirstOrDefault(g => g.Name == _newGroupName);
_subModValid = _mod != null && _newGroupName.Length > 0 && _newOptionName.Length > 0 && ( _selectedGroup?.All( o => o.Name != _newOptionName ) ?? true ); _subModValid = _mod != null
&& _newGroupName.Length > 0
&& _newOptionName.Length > 0
&& (_selectedGroup?.All(o => o.Name != _newOptionName) ?? true);
} }
private void CreateMod() private void CreateMod()
{ {
var newDir = Mod.Creator.CreateModFolder( Penumbra.ModManager.BasePath, _newModName ); var newDir = Mod.Creator.CreateModFolder(Penumbra.ModManager.BasePath, _newModName);
Mod.Creator.CreateMeta( newDir, _newModName, Penumbra.Config.DefaultModAuthor, CreateDescription(), "1.0", string.Empty ); Mod.Creator.CreateMeta(newDir, _newModName, Penumbra.Config.DefaultModAuthor, CreateDescription(), "1.0", string.Empty);
Mod.Creator.CreateDefaultFiles( newDir ); Mod.Creator.CreateDefaultFiles(newDir);
Penumbra.ModManager.AddMod( newDir ); Penumbra.ModManager.AddMod(newDir);
if( !_swapData.WriteMod( Penumbra.ModManager.Last(), _useFileSwaps ? ItemSwapContainer.WriteType.UseSwaps : ItemSwapContainer.WriteType.NoSwaps ) ) if (!_swapData.WriteMod(Penumbra.ModManager.Last(),
{ _useFileSwaps ? ItemSwapContainer.WriteType.UseSwaps : ItemSwapContainer.WriteType.NoSwaps))
Penumbra.ModManager.DeleteMod( Penumbra.ModManager.Count - 1 ); Penumbra.ModManager.DeleteMod(Penumbra.ModManager.Count - 1);
}
} }
private void CreateOption() private void CreateOption()
{ {
if( _mod == null || !_subModValid ) if (_mod == null || !_subModValid)
{
return; return;
}
var groupCreated = false; var groupCreated = false;
var dirCreated = false; var dirCreated = false;
@ -251,52 +263,47 @@ public class ItemSwapWindow : IDisposable
DirectoryInfo? optionFolderName = null; DirectoryInfo? optionFolderName = null;
try try
{ {
optionFolderName = Mod.Creator.NewSubFolderName( new DirectoryInfo( Path.Combine( _mod.ModPath.FullName, _selectedGroup?.Name ?? _newGroupName ) ), _newOptionName ); optionFolderName =
if( optionFolderName?.Exists == true ) Mod.Creator.NewSubFolderName(new DirectoryInfo(Path.Combine(_mod.ModPath.FullName, _selectedGroup?.Name ?? _newGroupName)),
{ _newOptionName);
throw new Exception( $"The folder {optionFolderName.FullName} for the option already exists." ); 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(); _selectedGroup = _mod.Groups.Last();
groupCreated = true; groupCreated = true;
} }
Penumbra.ModManager.AddOption( _mod, _mod.Groups.IndexOf( _selectedGroup ), _newOptionName ); Penumbra.ModManager.AddOption(_mod, _mod.Groups.IndexOf(_selectedGroup), _newOptionName);
optionCreated = true; optionCreated = true;
optionFolderName = Directory.CreateDirectory( optionFolderName.FullName ); optionFolderName = Directory.CreateDirectory(optionFolderName.FullName);
dirCreated = true; dirCreated = true;
if( !_swapData.WriteMod( _mod, _useFileSwaps ? ItemSwapContainer.WriteType.UseSwaps : ItemSwapContainer.WriteType.NoSwaps, optionFolderName, if (!_swapData.WriteMod(_mod, _useFileSwaps ? ItemSwapContainer.WriteType.UseSwaps : ItemSwapContainer.WriteType.NoSwaps,
_mod.Groups.IndexOf( _selectedGroup ), _selectedGroup.Count - 1 ) ) optionFolderName,
{ _mod.Groups.IndexOf(_selectedGroup), _selectedGroup.Count - 1))
throw new Exception( "Failure writing files for mod swap." ); throw new Exception("Failure writing files for mod swap.");
}
} }
} }
catch( Exception e ) catch (Exception e)
{ {
ChatUtil.NotificationMessage( $"Could not create new Swap Option:\n{e}", "Error", NotificationType.Error ); ChatUtil.NotificationMessage($"Could not create new Swap Option:\n{e}", "Error", NotificationType.Error);
try try
{ {
if( optionCreated && _selectedGroup != null ) if (optionCreated && _selectedGroup != null)
{ Penumbra.ModManager.DeleteOption(_mod, _mod.Groups.IndexOf(_selectedGroup), _selectedGroup.Count - 1);
Penumbra.ModManager.DeleteOption( _mod, _mod.Groups.IndexOf( _selectedGroup ), _selectedGroup.Count - 1 );
}
if( groupCreated ) if (groupCreated)
{ {
Penumbra.ModManager.DeleteModGroup( _mod, _mod.Groups.IndexOf( _selectedGroup! ) ); Penumbra.ModManager.DeleteModGroup(_mod, _mod.Groups.IndexOf(_selectedGroup!));
_selectedGroup = null; _selectedGroup = null;
} }
if( dirCreated && optionFolderName != null ) if (dirCreated && optionFolderName != null)
{ Directory.Delete(optionFolderName.FullName, true);
Directory.Delete( optionFolderName.FullName, true );
}
} }
catch catch
{ {
@ -307,12 +314,12 @@ public class ItemSwapWindow : IDisposable
UpdateOption(); UpdateOption();
} }
private void DrawHeaderLine( float width ) private void DrawHeaderLine(float width)
{ {
var newModAvailable = _loadException == null && _swapData.Loaded; var newModAvailable = _loadException == null && _swapData.Loaded;
ImGui.SetNextItemWidth( width ); ImGui.SetNextItemWidth(width);
if( ImGui.InputTextWithHint( "##newModName", "New Mod Name...", ref _newModName, 64 ) ) if (ImGui.InputTextWithHint("##newModName", "New Mod Name...", ref _newModName, 64))
{ } { }
ImGui.SameLine(); ImGui.SameLine();
@ -321,29 +328,23 @@ public class ItemSwapWindow : IDisposable
: _newModName.Length == 0 : _newModName.Length == 0
? "Please enter a name for your mod." ? "Please enter a name for your mod."
: "Create a new mod of the given name containing only the swap."; : "Create a new mod of the given name containing only the swap.";
if( ImGuiUtil.DrawDisabledButton( "Create New Mod", new Vector2( width / 2, 0 ), tt, !newModAvailable || _newModName.Length == 0 ) ) if (ImGuiUtil.DrawDisabledButton("Create New Mod", new Vector2(width / 2, 0), tt, !newModAvailable || _newModName.Length == 0))
{
CreateMod(); CreateMod();
}
ImGui.SameLine(); ImGui.SameLine();
ImGui.SetCursorPosX( ImGui.GetCursorPosX() + 20 * ImGuiHelpers.GlobalScale ); ImGui.SetCursorPosX(ImGui.GetCursorPosX() + 20 * ImGuiHelpers.GlobalScale);
ImGui.Checkbox( "Use File Swaps", ref _useFileSwaps ); 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" 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." ); + "even those available from game files, use File Swaps to default game files where possible.");
ImGui.SetNextItemWidth( ( width - ImGui.GetStyle().ItemSpacing.X ) / 2 ); ImGui.SetNextItemWidth((width - ImGui.GetStyle().ItemSpacing.X) / 2);
if( ImGui.InputTextWithHint( "##groupName", "Group Name...", ref _newGroupName, 32 ) ) if (ImGui.InputTextWithHint("##groupName", "Group Name...", ref _newGroupName, 32))
{
UpdateOption(); UpdateOption();
}
ImGui.SameLine(); ImGui.SameLine();
ImGui.SetNextItemWidth( ( width - ImGui.GetStyle().ItemSpacing.X ) / 2 ); ImGui.SetNextItemWidth((width - ImGui.GetStyle().ItemSpacing.X) / 2);
if( ImGui.InputTextWithHint( "##optionName", "New Option Name...", ref _newOptionName, 32 ) ) if (ImGui.InputTextWithHint("##optionName", "New Option Name...", ref _newOptionName, 32))
{
UpdateOption(); UpdateOption();
}
ImGui.SameLine(); ImGui.SameLine();
tt = !_subModValid tt = !_subModValid
@ -351,16 +352,15 @@ public class ItemSwapWindow : IDisposable
: !newModAvailable : !newModAvailable
? "Create a new option inside this mod containing only the swap." ? "Create a new option inside this mod containing only the swap."
: "Create a new option (and possibly Multi-Group) inside the currently selected mod containing the swap."; : "Create a new option (and possibly Multi-Group) inside the currently selected mod containing the swap.";
if( ImGuiUtil.DrawDisabledButton( "Create New Option", new Vector2( width / 2, 0 ), tt, !newModAvailable || !_subModValid ) ) if (ImGuiUtil.DrawDisabledButton("Create New Option", new Vector2(width / 2, 0), tt, !newModAvailable || !_subModValid))
{
CreateOption(); CreateOption();
}
ImGui.SameLine(); ImGui.SameLine();
ImGui.SetCursorPosX( ImGui.GetCursorPosX() + 20 * ImGuiHelpers.GlobalScale ); ImGui.SetCursorPosX(ImGui.GetCursorPosX() + 20 * ImGuiHelpers.GlobalScale);
_dirty |= ImGui.Checkbox( "Use Entire Collection", ref _useCurrentCollection ); _dirty |= ImGui.Checkbox("Use Entire Collection", ref _useCurrentCollection);
ImGuiUtil.HoverTooltip( "Use all applied mods from the Selected Collection with their current settings and respecting the enabled state of mods and inheritance,\n" ImGuiUtil.HoverTooltip(
+ "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." ); "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() private void DrawSwapBar()
@ -491,61 +491,58 @@ public class ItemSwapWindow : IDisposable
return (article1, article2, source ? tuple.Source : tuple.Target); return (article1, article2, source ? tuple.Source : tuple.Target);
} }
private void DrawEquipmentSwap( SwapType type ) private void DrawEquipmentSwap(SwapType type)
{ {
using var tab = DrawTab( type ); using var tab = DrawTab(type);
if( !tab ) if (!tab)
{
return; return;
}
var (sourceSelector, targetSelector, text1, text2) = _selectors[ type ]; var (sourceSelector, targetSelector, text1, text2) = _selectors[type];
using var table = ImRaii.Table( "##settings", 2, ImGuiTableFlags.SizingFixedFit ); using var table = ImRaii.Table("##settings", 2, ImGuiTableFlags.SizingFixedFit);
ImGui.TableNextColumn(); ImGui.TableNextColumn();
ImGui.AlignTextToFramePadding(); ImGui.AlignTextToFramePadding();
ImGui.TextUnformatted( text1 ); ImGui.TextUnformatted(text1);
ImGui.TableNextColumn(); ImGui.TableNextColumn();
_dirty |= sourceSelector.Draw( "##itemSource", sourceSelector.CurrentSelection.Item1 ?? string.Empty, string.Empty, InputWidth * 2, ImGui.GetTextLineHeightWithSpacing() ); _dirty |= sourceSelector.Draw("##itemSource", sourceSelector.CurrentSelection.Item1 ?? string.Empty, string.Empty, InputWidth * 2,
ImGui.GetTextLineHeightWithSpacing());
if( type == SwapType.Ring ) if (type == SwapType.Ring)
{ {
ImGui.SameLine(); ImGui.SameLine();
_dirty |= ImGui.Checkbox( "Swap Right Ring", ref _useRightRing ); _dirty |= ImGui.Checkbox("Swap Right Ring", ref _useRightRing);
} }
ImGui.TableNextColumn(); ImGui.TableNextColumn();
ImGui.AlignTextToFramePadding(); ImGui.AlignTextToFramePadding();
ImGui.TextUnformatted( text2 ); ImGui.TextUnformatted(text2);
ImGui.TableNextColumn(); ImGui.TableNextColumn();
_dirty |= targetSelector.Draw( "##itemTarget", targetSelector.CurrentSelection.Item1 ?? string.Empty, string.Empty, InputWidth * 2, ImGui.GetTextLineHeightWithSpacing() ); _dirty |= targetSelector.Draw("##itemTarget", targetSelector.CurrentSelection.Item1 ?? string.Empty, string.Empty, InputWidth * 2,
if( type == SwapType.Ring ) ImGui.GetTextLineHeightWithSpacing());
if (type == SwapType.Ring)
{ {
ImGui.SameLine(); 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(); ImGui.SameLine();
ImGuiUtil.DrawTextButton( $"which will also affect {_affectedItems.Length - 1} other Items.", Vector2.Zero, Colors.PressEnterWarningBg ); ImGuiUtil.DrawTextButton($"which will also affect {_affectedItems.Length - 1} other Items.", Vector2.Zero,
if( ImGui.IsItemHovered() ) Colors.PressEnterWarningBg);
{ if (ImGui.IsItemHovered())
ImGui.SetTooltip( string.Join( '\n', _affectedItems.Where( i => !ReferenceEquals( i, targetSelector.CurrentSelection.Item2 ) ) ImGui.SetTooltip(string.Join('\n', _affectedItems.Where(i => !ReferenceEquals(i, targetSelector.CurrentSelection.Item2))
.Select( i => i.Name.ToDalamudString().TextValue ) ) ); .Select(i => i.Name.ToDalamudString().TextValue)));
}
} }
} }
private void DrawHairSwap() private void DrawHairSwap()
{ {
using var tab = DrawTab( SwapType.Hair ); using var tab = DrawTab(SwapType.Hair);
if( !tab ) if (!tab)
{
return; return;
}
using var table = ImRaii.Table( "##settings", 2, ImGuiTableFlags.SizingFixedFit ); using var table = ImRaii.Table("##settings", 2, ImGuiTableFlags.SizingFixedFit);
DrawTargetIdInput( "Take this Hairstyle" ); DrawTargetIdInput("Take this Hairstyle");
DrawSourceIdInput(); DrawSourceIdInput();
DrawGenderInput(); DrawGenderInput();
} }
@ -553,145 +550,139 @@ public class ItemSwapWindow : IDisposable
private void DrawFaceSwap() private void DrawFaceSwap()
{ {
using var disabled = ImRaii.Disabled(); using var disabled = ImRaii.Disabled();
using var tab = DrawTab( SwapType.Face ); using var tab = DrawTab(SwapType.Face);
if( !tab ) if (!tab)
{
return; return;
}
using var table = ImRaii.Table( "##settings", 2, ImGuiTableFlags.SizingFixedFit ); using var table = ImRaii.Table("##settings", 2, ImGuiTableFlags.SizingFixedFit);
DrawTargetIdInput( "Take this Face Type" ); DrawTargetIdInput("Take this Face Type");
DrawSourceIdInput(); DrawSourceIdInput();
DrawGenderInput(); DrawGenderInput();
} }
private void DrawTailSwap() private void DrawTailSwap()
{ {
using var tab = DrawTab( SwapType.Tail ); using var tab = DrawTab(SwapType.Tail);
if( !tab ) if (!tab)
{
return; return;
}
using var table = ImRaii.Table( "##settings", 2, ImGuiTableFlags.SizingFixedFit ); using var table = ImRaii.Table("##settings", 2, ImGuiTableFlags.SizingFixedFit);
DrawTargetIdInput( "Take this Tail Type" ); DrawTargetIdInput("Take this Tail Type");
DrawSourceIdInput(); DrawSourceIdInput();
DrawGenderInput( "for all", 2 ); DrawGenderInput("for all", 2);
} }
private void DrawEarSwap() private void DrawEarSwap()
{ {
using var tab = DrawTab( SwapType.Ears ); using var tab = DrawTab(SwapType.Ears);
if( !tab ) if (!tab)
{
return; return;
}
using var table = ImRaii.Table( "##settings", 2, ImGuiTableFlags.SizingFixedFit ); using var table = ImRaii.Table("##settings", 2, ImGuiTableFlags.SizingFixedFit);
DrawTargetIdInput( "Take this Ear Type" ); DrawTargetIdInput("Take this Ear Type");
DrawSourceIdInput(); DrawSourceIdInput();
DrawGenderInput( "for all Viera", 0 ); DrawGenderInput("for all Viera", 0);
} }
private void DrawWeaponSwap() private void DrawWeaponSwap()
{ {
using var disabled = ImRaii.Disabled(); using var disabled = ImRaii.Disabled();
using var tab = DrawTab( SwapType.Weapon ); using var tab = DrawTab(SwapType.Weapon);
if( !tab ) if (!tab)
{
return; return;
}
using var table = ImRaii.Table( "##settings", 2, ImGuiTableFlags.SizingFixedFit ); using var table = ImRaii.Table("##settings", 2, ImGuiTableFlags.SizingFixedFit);
ImGui.TableNextColumn(); ImGui.TableNextColumn();
ImGui.AlignTextToFramePadding(); ImGui.AlignTextToFramePadding();
ImGui.TextUnformatted( "Select the weapon or tool you want" ); ImGui.TextUnformatted("Select the weapon or tool you want");
ImGui.TableNextColumn(); ImGui.TableNextColumn();
if( _slotSelector.Draw( "##weaponSlot", _slotSelector.CurrentSelection.ToName(), string.Empty, InputWidth * 2, ImGui.GetTextLineHeightWithSpacing() ) ) if (_slotSelector.Draw("##weaponSlot", _slotSelector.CurrentSelection.ToName(), string.Empty, InputWidth * 2,
ImGui.GetTextLineHeightWithSpacing()))
{ {
_dirty = true; _dirty = true;
_weaponSource = new ItemSelector( _slotSelector.CurrentSelection ); _weaponSource = new ItemSelector(_slotSelector.CurrentSelection);
_weaponTarget = new ItemSelector( _slotSelector.CurrentSelection ); _weaponTarget = new ItemSelector(_slotSelector.CurrentSelection);
} }
else else
{ {
_dirty = _weaponSource == null || _weaponTarget == null; _dirty = _weaponSource == null || _weaponTarget == null;
_weaponSource ??= new ItemSelector( _slotSelector.CurrentSelection ); _weaponSource ??= new ItemSelector(_slotSelector.CurrentSelection);
_weaponTarget ??= new ItemSelector( _slotSelector.CurrentSelection ); _weaponTarget ??= new ItemSelector(_slotSelector.CurrentSelection);
} }
ImGui.TableNextColumn(); ImGui.TableNextColumn();
ImGui.AlignTextToFramePadding(); ImGui.AlignTextToFramePadding();
ImGui.TextUnformatted( "and put this variant of it" ); ImGui.TextUnformatted("and put this variant of it");
ImGui.TableNextColumn(); ImGui.TableNextColumn();
_dirty |= _weaponSource.Draw( "##weaponSource", _weaponSource.CurrentSelection.Item1 ?? string.Empty, string.Empty, InputWidth * 2, ImGui.GetTextLineHeightWithSpacing() ); _dirty |= _weaponSource.Draw("##weaponSource", _weaponSource.CurrentSelection.Item1 ?? string.Empty, string.Empty, InputWidth * 2,
ImGui.GetTextLineHeightWithSpacing());
ImGui.TableNextColumn(); ImGui.TableNextColumn();
ImGui.AlignTextToFramePadding(); ImGui.AlignTextToFramePadding();
ImGui.TextUnformatted( "onto this one" ); ImGui.TextUnformatted("onto this one");
ImGui.TableNextColumn(); ImGui.TableNextColumn();
_dirty |= _weaponTarget.Draw( "##weaponTarget", _weaponTarget.CurrentSelection.Item1 ?? string.Empty, string.Empty, InputWidth * 2, ImGui.GetTextLineHeightWithSpacing() ); _dirty |= _weaponTarget.Draw("##weaponTarget", _weaponTarget.CurrentSelection.Item1 ?? string.Empty, string.Empty, InputWidth * 2,
ImGui.GetTextLineHeightWithSpacing());
} }
private const float InputWidth = 120; private const float InputWidth = 120;
private void DrawTargetIdInput( string text = "Take this ID" ) private void DrawTargetIdInput(string text = "Take this ID")
{ {
ImGui.TableNextColumn(); ImGui.TableNextColumn();
ImGui.AlignTextToFramePadding(); ImGui.AlignTextToFramePadding();
ImGui.TextUnformatted( text ); ImGui.TextUnformatted(text);
ImGui.TableNextColumn(); ImGui.TableNextColumn();
ImGui.SetNextItemWidth( InputWidth * ImGuiHelpers.GlobalScale ); ImGui.SetNextItemWidth(InputWidth * ImGuiHelpers.GlobalScale);
if( ImGui.InputInt( "##targetId", ref _targetId, 0, 0 ) ) if (ImGui.InputInt("##targetId", ref _targetId, 0, 0))
{ _targetId = Math.Clamp(_targetId, 0, byte.MaxValue);
_targetId = Math.Clamp( _targetId, 0, byte.MaxValue );
}
_dirty |= ImGui.IsItemDeactivatedAfterEdit(); _dirty |= ImGui.IsItemDeactivatedAfterEdit();
} }
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.TableNextColumn();
ImGui.AlignTextToFramePadding(); ImGui.AlignTextToFramePadding();
ImGui.TextUnformatted( text ); ImGui.TextUnformatted(text);
ImGui.TableNextColumn(); ImGui.TableNextColumn();
ImGui.SetNextItemWidth( InputWidth * ImGuiHelpers.GlobalScale ); ImGui.SetNextItemWidth(InputWidth * ImGuiHelpers.GlobalScale);
if( ImGui.InputInt( "##sourceId", ref _sourceId, 0, 0 ) ) if (ImGui.InputInt("##sourceId", ref _sourceId, 0, 0))
{ _sourceId = Math.Clamp(_sourceId, 0, byte.MaxValue);
_sourceId = Math.Clamp( _sourceId, 0, byte.MaxValue );
}
_dirty |= ImGui.IsItemDeactivatedAfterEdit(); _dirty |= ImGui.IsItemDeactivatedAfterEdit();
} }
private void DrawGenderInput( string text = "for all", int drawRace = 1 ) private void DrawGenderInput(string text = "for all", int drawRace = 1)
{ {
ImGui.TableNextColumn(); ImGui.TableNextColumn();
ImGui.AlignTextToFramePadding(); ImGui.AlignTextToFramePadding();
ImGui.TextUnformatted( text ); ImGui.TextUnformatted(text);
ImGui.TableNextColumn(); ImGui.TableNextColumn();
_dirty |= Combos.Gender( "##Gender", InputWidth, _currentGender, out _currentGender ); _dirty |= Combos.Gender("##Gender", InputWidth, _currentGender, out _currentGender);
if( drawRace == 1 ) if (drawRace == 1)
{ {
ImGui.SameLine(); 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(); ImGui.SameLine();
if( _currentRace is not ModelRace.Miqote and not ModelRace.AuRa and not ModelRace.Hrothgar ) if (_currentRace is not ModelRace.Miqote and not ModelRace.AuRa and not ModelRace.Hrothgar)
{
_currentRace = ModelRace.Miqote; _currentRace = ModelRace.Miqote;
}
_dirty |= ImGuiUtil.GenericEnumCombo( "##Race", InputWidth, _currentRace, out _currentRace, new[] { ModelRace.Miqote, ModelRace.AuRa, ModelRace.Hrothgar }, _dirty |= ImGuiUtil.GenericEnumCombo("##Race", InputWidth, _currentRace, out _currentRace, new[]
RaceEnumExtensions.ToName ); {
ModelRace.Miqote,
ModelRace.AuRa,
ModelRace.Hrothgar,
},
RaceEnumExtensions.ToName);
} }
} }
@ -718,72 +709,54 @@ public class ItemSwapWindow : IDisposable
public void DrawItemSwapPanel() public void DrawItemSwapPanel()
{ {
using var tab = ImRaii.TabItem( "Item Swap (WIP)" ); using var tab = ImRaii.TabItem("Item Swap (WIP)");
if( !tab ) if (!tab)
{
return; return;
}
ImGui.NewLine(); ImGui.NewLine();
DrawHeaderLine( 300 * ImGuiHelpers.GlobalScale ); DrawHeaderLine(300 * ImGuiHelpers.GlobalScale);
ImGui.NewLine(); ImGui.NewLine();
DrawSwapBar(); DrawSwapBar();
using var table = ImRaii.ListBox( "##swaps", -Vector2.One ); using var table = ImRaii.ListBox("##swaps", -Vector2.One);
if( _loadException != null ) if (_loadException != null)
{ ImGuiUtil.TextWrapped($"Could not load Customization Swap:\n{_loadException}");
ImGuiUtil.TextWrapped( $"Could not load Customization Swap:\n{_loadException}" ); else if (_swapData.Loaded)
} foreach (var swap in _swapData.Swaps)
else if( _swapData.Loaded ) DrawSwap(swap);
{
foreach( var swap in _swapData.Swaps )
{
DrawSwap( swap );
}
}
else else
{ ImGui.TextUnformatted(NonExistentText());
ImGui.TextUnformatted( NonExistentText() );
}
} }
private static void DrawSwap( Swap swap ) private static void DrawSwap(Swap swap)
{ {
var flags = swap.ChildSwaps.Count == 0 ? ImGuiTreeNodeFlags.Bullet | ImGuiTreeNodeFlags.Leaf : ImGuiTreeNodeFlags.DefaultOpen; var flags = swap.ChildSwaps.Count == 0 ? ImGuiTreeNodeFlags.Bullet | ImGuiTreeNodeFlags.Leaf : ImGuiTreeNodeFlags.DefaultOpen;
using var tree = ImRaii.TreeNode( SwapToString( swap ), flags ); using var tree = ImRaii.TreeNode(SwapToString(swap), flags);
if( !tree ) if (!tree)
{
return; return;
}
foreach( var child in swap.ChildSwaps ) foreach (var child in swap.ChildSwaps)
{ DrawSwap(child);
DrawSwap( child );
}
} }
private void OnCollectionChange( CollectionType collectionType, ModCollection? oldCollection, private void OnCollectionChange(CollectionType collectionType, ModCollection? oldCollection,
ModCollection? newCollection, string _ ) ModCollection? newCollection, string _)
{ {
if( collectionType != CollectionType.Current || _mod == null || newCollection == null ) if (collectionType != CollectionType.Current || _mod == null || newCollection == null)
{
return; return;
}
UpdateMod( _mod, _mod.Index < newCollection.Settings.Count ? newCollection.Settings[ _mod.Index ] : null ); UpdateMod(_mod, _mod.Index < newCollection.Settings.Count ? newCollection.Settings[_mod.Index] : null);
newCollection.ModSettingChanged += OnSettingChange; newCollection.ModSettingChanged += OnSettingChange;
if( oldCollection != null ) if (oldCollection != null)
{
oldCollection.ModSettingChanged -= OnSettingChange; oldCollection.ModSettingChanged -= OnSettingChange;
}
} }
private void OnSettingChange( ModSettingChange type, int modIdx, int oldValue, int groupIdx, bool inherited ) private void OnSettingChange(ModSettingChange type, int modIdx, int oldValue, int groupIdx, bool inherited)
{ {
if( modIdx == _mod?.Index ) if (modIdx == _mod?.Index)
{ {
_swapData.LoadMod( _mod, _modSettings ); _swapData.LoadMod(_mod, _modSettings);
_dirty = true; _dirty = true;
} }
} }

View file

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

View file

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

View file

@ -18,37 +18,39 @@ using Penumbra.Services;
namespace Penumbra.UI.Classes; 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 readonly CommunicatorService _communicator;
private TexToolsImporter? _import; private readonly FileDialogManager _fileManager = ConfigWindow.SetupFileManager();
public ModSettings SelectedSettings { get; private set; } = ModSettings.Empty; private TexToolsImporter? _import;
public ModCollection SelectedSettingCollection { get; private set; } = ModCollection.Empty; public ModSettings SelectedSettings { get; private set; } = ModSettings.Empty;
public ModCollection SelectedSettingCollection { get; private set; } = ModCollection.Empty;
public ModFileSystemSelector( ModFileSystem fileSystem ) public ModFileSystemSelector(CommunicatorService communicator, ModFileSystem fileSystem)
: base( fileSystem, DalamudServices.KeyState ) : base(fileSystem, DalamudServices.KeyState)
{ {
SubscribeRightClickFolder( EnableDescendants, 10 ); _communicator = communicator;
SubscribeRightClickFolder( DisableDescendants, 10 ); SubscribeRightClickFolder(EnableDescendants, 10);
SubscribeRightClickFolder( InheritDescendants, 15 ); SubscribeRightClickFolder(DisableDescendants, 10);
SubscribeRightClickFolder( OwnDescendants, 15 ); SubscribeRightClickFolder(InheritDescendants, 15);
SubscribeRightClickFolder( SetDefaultImportFolder, 100 ); SubscribeRightClickFolder(OwnDescendants, 15);
SubscribeRightClickLeaf( ToggleLeafFavorite, 0 ); SubscribeRightClickFolder(SetDefaultImportFolder, 100);
SubscribeRightClickMain( ClearDefaultImportFolder, 100 ); SubscribeRightClickLeaf(ToggleLeafFavorite, 0);
AddButton( AddNewModButton, 0 ); SubscribeRightClickMain(ClearDefaultImportFolder, 100);
AddButton( AddImportModButton, 1 ); AddButton(AddNewModButton, 0);
AddButton( AddHelpButton, 2 ); AddButton(AddImportModButton, 1);
AddButton( DeleteModButton, 1000 ); AddButton(AddHelpButton, 2);
AddButton(DeleteModButton, 1000);
SetFilterTooltip(); SetFilterTooltip();
SelectionChanged += OnSelectionChange; SelectionChanged += OnSelectionChange;
Penumbra.CollectionManager.CollectionChanged += OnCollectionChange; _communicator.CollectionChange.Event += OnCollectionChange;
Penumbra.CollectionManager.Current.ModSettingChanged += OnSettingChange; Penumbra.CollectionManager.Current.ModSettingChanged += OnSettingChange;
Penumbra.CollectionManager.Current.InheritanceChanged += OnInheritanceChange; Penumbra.CollectionManager.Current.InheritanceChanged += OnInheritanceChange;
Penumbra.ModManager.ModDataChanged += OnModDataChange; Penumbra.ModManager.ModDataChanged += OnModDataChange;
Penumbra.ModManager.ModDiscoveryStarted += StoreCurrentSelection; Penumbra.ModManager.ModDiscoveryStarted += StoreCurrentSelection;
Penumbra.ModManager.ModDiscoveryFinished += RestoreLastSelection; Penumbra.ModManager.ModDiscoveryFinished += RestoreLastSelection;
OnCollectionChange( CollectionType.Current, null, Penumbra.CollectionManager.Current, "" ); OnCollectionChange(CollectionType.Current, null, Penumbra.CollectionManager.Current, "");
} }
public override void Dispose() public override void Dispose()
@ -59,7 +61,7 @@ public sealed partial class ModFileSystemSelector : FileSystemSelector< Mod, Mod
Penumbra.ModManager.ModDataChanged -= OnModDataChange; Penumbra.ModManager.ModDataChanged -= OnModDataChange;
Penumbra.CollectionManager.Current.ModSettingChanged -= OnSettingChange; Penumbra.CollectionManager.Current.ModSettingChanged -= OnSettingChange;
Penumbra.CollectionManager.Current.InheritanceChanged -= OnInheritanceChange; Penumbra.CollectionManager.Current.InheritanceChanged -= OnInheritanceChange;
Penumbra.CollectionManager.CollectionChanged -= OnCollectionChange; _communicator.CollectionChange.Event -= OnCollectionChange;
_import?.Dispose(); _import?.Dispose();
_import = null; _import = null;
} }
@ -68,7 +70,7 @@ public sealed partial class ModFileSystemSelector : FileSystemSelector< Mod, Mod
=> base.SelectedLeaf; => base.SelectedLeaf;
// Customization points. // Customization points.
public override ISortMode< Mod > SortMode public override ISortMode<Mod> SortMode
=> Penumbra.Config.SortMode; => Penumbra.Config.SortMode;
protected override uint ExpandedFolderColor protected override uint ExpandedFolderColor
@ -89,91 +91,79 @@ public sealed partial class ModFileSystemSelector : FileSystemSelector< Mod, Mod
DrawHelpPopup(); DrawHelpPopup();
DrawInfoPopup(); DrawInfoPopup();
if( ImGuiUtil.OpenNameField( "Create New Mod", ref _newModName ) ) if (ImGuiUtil.OpenNameField("Create New Mod", ref _newModName))
{
try try
{ {
var newDir = Mod.Creator.CreateModFolder( Penumbra.ModManager.BasePath, _newModName ); var newDir = Mod.Creator.CreateModFolder(Penumbra.ModManager.BasePath, _newModName);
Mod.Creator.CreateMeta( newDir, _newModName, Penumbra.Config.DefaultModAuthor, string.Empty, "1.0", string.Empty ); Mod.Creator.CreateMeta(newDir, _newModName, Penumbra.Config.DefaultModAuthor, string.Empty, "1.0", string.Empty);
Mod.Creator.CreateDefaultFiles( newDir ); Mod.Creator.CreateDefaultFiles(newDir);
Penumbra.ModManager.AddMod( newDir ); Penumbra.ModManager.AddMod(newDir);
_newModName = string.Empty; _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(); var mod = Penumbra.ModManager.LastOrDefault();
if( mod != null ) if (mod != null)
{ {
MoveModToDefaultDirectory( mod ); MoveModToDefaultDirectory(mod);
SelectByValue( 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; var flags = selected ? ImGuiTreeNodeFlags.Selected | LeafFlags : LeafFlags;
using var c = ImRaii.PushColor( ImGuiCol.Text, state.Color.Value() ) using var c = ImRaii.PushColor(ImGuiCol.Text, state.Color.Value())
.Push( ImGuiCol.HeaderHovered, 0x4000FFFF, leaf.Value.Favorite ); .Push(ImGuiCol.HeaderHovered, 0x4000FFFF, leaf.Value.Favorite);
using var id = ImRaii.PushId( leaf.Value.Index ); using var id = ImRaii.PushId(leaf.Value.Index);
ImRaii.TreeNode( leaf.Value.Name, flags ).Dispose(); ImRaii.TreeNode(leaf.Value.Name, flags).Dispose();
} }
// Add custom context menu items. // Add custom context menu items.
private static void EnableDescendants( ModFileSystem.Folder folder ) private static void EnableDescendants(ModFileSystem.Folder folder)
{ {
if( ImGui.MenuItem( "Enable Descendants" ) ) if (ImGui.MenuItem("Enable Descendants"))
{ SetDescendants(folder, true);
SetDescendants( folder, true );
}
} }
private static void DisableDescendants( ModFileSystem.Folder folder ) private static void DisableDescendants(ModFileSystem.Folder folder)
{ {
if( ImGui.MenuItem( "Disable Descendants" ) ) if (ImGui.MenuItem("Disable Descendants"))
{ SetDescendants(folder, false);
SetDescendants( folder, false );
}
} }
private static void InheritDescendants( ModFileSystem.Folder folder ) private static void InheritDescendants(ModFileSystem.Folder folder)
{ {
if( ImGui.MenuItem( "Inherit Descendants" ) ) if (ImGui.MenuItem("Inherit Descendants"))
{ SetDescendants(folder, true, true);
SetDescendants( folder, true, true );
}
} }
private static void OwnDescendants( ModFileSystem.Folder folder ) private static void OwnDescendants(ModFileSystem.Folder folder)
{ {
if( ImGui.MenuItem( "Stop Inheriting Descendants" ) ) if (ImGui.MenuItem("Stop Inheriting Descendants"))
{ SetDescendants(folder, false, true);
SetDescendants( folder, false, true );
}
} }
private static void ToggleLeafFavorite( FileSystem< Mod >.Leaf mod ) private static void ToggleLeafFavorite(FileSystem<Mod>.Leaf mod)
{ {
if( ImGui.MenuItem( mod.Value.Favorite ? "Remove Favorite" : "Mark as Favorite" ) ) if (ImGui.MenuItem(mod.Value.Favorite ? "Remove Favorite" : "Mark as Favorite"))
{ Penumbra.ModManager.ChangeModFavorite(mod.Value.Index, !mod.Value.Favorite);
Penumbra.ModManager.ChangeModFavorite( mod.Value.Index, !mod.Value.Favorite );
}
} }
private static void SetDefaultImportFolder( ModFileSystem.Folder folder ) private static void SetDefaultImportFolder(ModFileSystem.Folder folder)
{ {
if( ImGui.MenuItem( "Set As Default Import Folder" ) ) if (ImGui.MenuItem("Set As Default Import Folder"))
{ {
var newName = folder.FullName(); var newName = folder.FullName();
if( newName != Penumbra.Config.DefaultImportFolder ) if (newName != Penumbra.Config.DefaultImportFolder)
{ {
Penumbra.Config.DefaultImportFolder = newName; Penumbra.Config.DefaultImportFolder = newName;
Penumbra.Config.Save(); Penumbra.Config.Save();
@ -183,7 +173,7 @@ public sealed partial class ModFileSystemSelector : FileSystemSelector< Mod, Mod
private static void ClearDefaultImportFolder() 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.DefaultImportFolder = string.Empty;
Penumbra.Config.Save(); Penumbra.Config.Save();
@ -194,71 +184,63 @@ public sealed partial class ModFileSystemSelector : FileSystemSelector< Mod, Mod
// Add custom buttons. // Add custom buttons.
private string _newModName = string.Empty; 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.", if (ImGuiUtil.DrawDisabledButton(FontAwesomeIcon.Plus.ToIconString(), size, "Create a new, empty mod of a given name.",
!Penumbra.ModManager.Valid, true ) ) !Penumbra.ModManager.Valid, true))
{ ImGui.OpenPopup("Create New Mod");
ImGui.OpenPopup( "Create New Mod" );
}
} }
// Add an import mods button that opens a file selector. // Add an import mods button that opens a file selector.
// Only set the initial directory once. // Only set the initial directory once.
private bool _hasSetFolder; private bool _hasSetFolder;
private void AddImportModButton( Vector2 size ) private void AddImportModButton(Vector2 size)
{ {
var button = ImGuiUtil.DrawDisabledButton( FontAwesomeIcon.FileImport.ToIconString(), 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 ); "Import one or multiple mods from Tex Tools Mod Pack Files or Penumbra Mod Pack Files.", !Penumbra.ModManager.Valid, true);
ConfigWindow.OpenTutorial( ConfigWindow.BasicTutorialSteps.ModImport ); ConfigWindow.OpenTutorial(ConfigWindow.BasicTutorialSteps.ModImport);
if( !button ) if (!button)
{
return; return;
}
var modPath = _hasSetFolder && !Penumbra.Config.AlwaysOpenDefaultImport ? null var modPath = _hasSetFolder && !Penumbra.Config.AlwaysOpenDefaultImport ? null
: Penumbra.Config.DefaultModImportPath.Length > 0 ? Penumbra.Config.DefaultModImportPath : Penumbra.Config.DefaultModImportPath.Length > 0 ? Penumbra.Config.DefaultModImportPath
: Penumbra.Config.ModDirectory.Length > 0 ? Penumbra.Config.ModDirectory : null; : Penumbra.Config.ModDirectory.Length > 0 ? Penumbra.Config.ModDirectory : null;
_hasSetFolder = true; _hasSetFolder = true;
_fileManager.OpenFileDialog( "Import Mod Pack", _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 ) => "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 ) ), _import = new TexToolsImporter(Penumbra.ModManager.BasePath, f.Count, f.Select(file => new FileInfo(file)),
AddNewMod ); AddNewMod);
ImGui.OpenPopup( "Import Status" ); ImGui.OpenPopup("Import Status");
} }
}, 0, modPath ); }, 0, modPath);
} }
// Draw the progress information for import. // Draw the progress information for import.
private void DrawInfoPopup() private void DrawInfoPopup()
{ {
var display = ImGui.GetIO().DisplaySize; 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 width = display.X / 8;
var size = new Vector2( width * 2, height ); var size = new Vector2(width * 2, height);
ImGui.SetNextWindowPos( ImGui.GetMainViewport().GetCenter(), ImGuiCond.Always, Vector2.One / 2 ); ImGui.SetNextWindowPos(ImGui.GetMainViewport().GetCenter(), ImGuiCond.Always, Vector2.One / 2);
ImGui.SetNextWindowSize( size ); ImGui.SetNextWindowSize(size);
using var popup = ImRaii.Popup( "Import Status", ImGuiWindowFlags.Modal ); using var popup = ImRaii.Popup("Import Status", ImGuiWindowFlags.Modal);
if( _import == null || !popup.Success ) if (_import == null || !popup.Success)
{
return; return;
}
using( var child = ImRaii.Child( "##import", new Vector2( -1, size.Y - ImGui.GetFrameHeight() * 2 ) ) ) using (var child = ImRaii.Child("##import", new Vector2(-1, size.Y - ImGui.GetFrameHeight() * 2)))
{ {
if( child ) if (child)
{ _import.DrawProgressInfo(new Vector2(-1, ImGui.GetFrameHeight()));
_import.DrawProgressInfo( new Vector2( -1, ImGui.GetFrameHeight() ) );
}
} }
if( _import.State == ImporterState.Done && ImGui.Button( "Close", -Vector2.UnitX ) if (_import.State == ImporterState.Done && ImGui.Button("Close", -Vector2.UnitX)
|| _import.State != ImporterState.Done && _import.DrawCancelButton( -Vector2.UnitX ) ) || _import.State != ImporterState.Done && _import.DrawCancelButton(-Vector2.UnitX))
{ {
_import?.Dispose(); _import?.Dispose();
_import = null; _import = null;
@ -267,100 +249,84 @@ public sealed partial class ModFileSystemSelector : FileSystemSelector< Mod, Mod
} }
// Mods need to be added thread-safely outside of iteration. // 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. // Clean up invalid directory if necessary.
// Add successfully extracted mods. // 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 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 ) if (error is not OperationCanceledException)
{ Penumbra.Log.Error($"Error extracting {file.FullName}, mod skipped:\n{error}");
Penumbra.Log.Error( $"Error extracting {file.FullName}, mod skipped:\n{error}" );
}
} }
else if( dir != null ) else if (dir != null)
{ {
_modsToAdd.Enqueue( dir ); _modsToAdd.Enqueue(dir);
} }
} }
private void DeleteModButton( Vector2 size ) private void DeleteModButton(Vector2 size)
{ {
var keys = Penumbra.Config.DeleteModModifier.IsActive(); var keys = Penumbra.Config.DeleteModModifier.IsActive();
var tt = SelectedLeaf == null var tt = SelectedLeaf == null
? "No mod selected." ? "No mod selected."
: "Delete the currently selected mod entirely from your drive.\n" : "Delete the currently selected mod entirely from your drive.\n"
+ "This can not be undone."; + "This can not be undone.";
if( !keys ) if (!keys)
{
tt += $"\nHold {Penumbra.Config.DeleteModModifier} while clicking to delete the mod."; tt += $"\nHold {Penumbra.Config.DeleteModModifier} while clicking to delete the mod.";
}
if( ImGuiUtil.DrawDisabledButton( FontAwesomeIcon.Trash.ToIconString(), size, tt, SelectedLeaf == null || !keys, true ) if (ImGuiUtil.DrawDisabledButton(FontAwesomeIcon.Trash.ToIconString(), size, tt, SelectedLeaf == null || !keys, true)
&& Selected != null ) && Selected != null)
{ Penumbra.ModManager.DeleteMod(Selected.Index);
Penumbra.ModManager.DeleteMod( Selected.Index );
}
} }
private static void AddHelpButton( Vector2 size ) private static void AddHelpButton(Vector2 size)
{ {
if( ImGuiUtil.DrawDisabledButton( FontAwesomeIcon.QuestionCircle.ToIconString(), size, "Open extended help.", false, true ) ) if (ImGuiUtil.DrawDisabledButton(FontAwesomeIcon.QuestionCircle.ToIconString(), size, "Open extended help.", false, true))
{ ImGui.OpenPopup("ExtendedHelp");
ImGui.OpenPopup( "ExtendedHelp" );
}
ConfigWindow.OpenTutorial( ConfigWindow.BasicTutorialSteps.AdvancedHelp ); ConfigWindow.OpenTutorial(ConfigWindow.BasicTutorialSteps.AdvancedHelp);
} }
// Helpers. // 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. // Any mod handled here should not stay new.
Penumbra.ModManager.NewMods.Remove( l.Value ); Penumbra.ModManager.NewMods.Remove(l.Value);
return l.Value; return l.Value;
} ); });
if( inherit ) if (inherit)
{ Penumbra.CollectionManager.Current.SetMultipleModInheritances(mods, enabled);
Penumbra.CollectionManager.Current.SetMultipleModInheritances( mods, enabled );
}
else else
{ Penumbra.CollectionManager.Current.SetMultipleModStates(mods, enabled);
Penumbra.CollectionManager.Current.SetMultipleModStates( mods, enabled );
}
} }
// Automatic cache update functions. // Automatic cache update functions.
private void OnSettingChange( ModSettingChange type, int modIdx, int oldValue, int groupIdx, bool inherited ) private void OnSettingChange(ModSettingChange type, int modIdx, int oldValue, int groupIdx, bool inherited)
{ {
// TODO: maybe make more efficient // TODO: maybe make more efficient
SetFilterDirty(); SetFilterDirty();
if( modIdx == Selected?.Index ) if (modIdx == Selected?.Index)
{ OnSelectionChange(Selected, Selected, default);
OnSelectionChange( Selected, Selected, default );
}
} }
private void OnModDataChange( ModDataChangeType type, Mod mod, string? oldName ) private void OnModDataChange(ModDataChangeType type, Mod mod, string? oldName)
{ {
switch( type ) switch (type)
{ {
case ModDataChangeType.Name: case ModDataChangeType.Name:
case ModDataChangeType.Author: case ModDataChangeType.Author:
@ -372,46 +338,44 @@ public sealed partial class ModFileSystemSelector : FileSystemSelector< Mod, Mod
} }
} }
private void OnInheritanceChange( bool _ ) private void OnInheritanceChange(bool _)
{ {
SetFilterDirty(); 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; return;
}
if( oldCollection != null ) if (oldCollection != null)
{ {
oldCollection.ModSettingChanged -= OnSettingChange; oldCollection.ModSettingChanged -= OnSettingChange;
oldCollection.InheritanceChanged -= OnInheritanceChange; oldCollection.InheritanceChanged -= OnInheritanceChange;
} }
if( newCollection != null ) if (newCollection != null)
{ {
newCollection.ModSettingChanged += OnSettingChange; newCollection.ModSettingChanged += OnSettingChange;
newCollection.InheritanceChanged += OnInheritanceChange; newCollection.InheritanceChanged += OnInheritanceChange;
} }
SetFilterDirty(); 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; SelectedSettings = ModSettings.Empty;
SelectedSettingCollection = ModCollection.Empty; SelectedSettingCollection = ModCollection.Empty;
} }
else else
{ {
( var settings, SelectedSettingCollection ) = Penumbra.CollectionManager.Current[ newSelection.Index ]; (var settings, SelectedSettingCollection) = Penumbra.CollectionManager.Current[newSelection.Index];
SelectedSettings = settings ?? ModSettings.Empty; SelectedSettings = settings ?? ModSettings.Empty;
} }
} }
@ -426,92 +390,89 @@ public sealed partial class ModFileSystemSelector : FileSystemSelector< Mod, Mod
private void RestoreLastSelection() private void RestoreLastSelection()
{ {
if( _lastSelectedDirectory.Length > 0 ) if (_lastSelectedDirectory.Length > 0)
{ {
var leaf = ( ModFileSystem.Leaf? )FileSystem.Root.GetAllDescendants( ISortMode< Mod >.Lexicographical ) var leaf = (ModFileSystem.Leaf?)FileSystem.Root.GetAllDescendants(ISortMode<Mod>.Lexicographical)
.FirstOrDefault( l => l is ModFileSystem.Leaf m && m.Value.ModPath.FullName == _lastSelectedDirectory ); .FirstOrDefault(l => l is ModFileSystem.Leaf m && m.Value.ModPath.FullName == _lastSelectedDirectory);
Select( leaf ); Select(leaf);
_lastSelectedDirectory = string.Empty; _lastSelectedDirectory = string.Empty;
} }
} }
// If a default import folder is setup, try to move the given mod in there. // 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. // 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; return;
}
try try
{ {
var leaf = FileSystem.Root.GetChildren( ISortMode< Mod >.Lexicographical ) var leaf = FileSystem.Root.GetChildren(ISortMode<Mod>.Lexicographical)
.FirstOrDefault( f => f is FileSystem< Mod >.Leaf l && l.Value == mod ); .FirstOrDefault(f => f is FileSystem<Mod>.Leaf l && l.Value == mod);
if( leaf == null ) if (leaf == null)
{ throw new Exception("Mod was not found at root.");
throw new Exception( "Mod was not found at root." );
}
var folder = FileSystem.FindOrCreateAllFolders( Penumbra.Config.DefaultImportFolder ); var folder = FileSystem.FindOrCreateAllFolders(Penumbra.Config.DefaultImportFolder);
FileSystem.Move( leaf, folder ); FileSystem.Move(leaf, folder);
} }
catch( Exception e ) catch (Exception e)
{ {
Penumbra.Log.Warning( 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() 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.Dummy(Vector2.UnitY * ImGui.GetTextLineHeight());
ImGui.TextUnformatted( "Mod Management" ); ImGui.TextUnformatted("Mod Management");
ImGui.BulletText( "You can create empty mods or import mods with the buttons in this row." ); ImGui.BulletText("You can create empty mods or import mods with the buttons in this row.");
using var indent = ImRaii.PushIndent(); using var indent = ImRaii.PushIndent();
ImGui.BulletText( "Supported formats for import are: .ttmp, .ttmp2, .pmp." ); ImGui.BulletText("Supported formats for import are: .ttmp, .ttmp2, .pmp.");
ImGui.BulletText( "You can also support .zip, .7z or .rar archives, but only if they already contain Penumbra-styled mods with appropriate metadata." );
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( ImGui.BulletText(
"If the sort order string contains Forward-Slashes ('/'), the preceding substring will be turned into folders automatically." ); "You can also support .zip, .7z or .rar archives, but only if they already contain Penumbra-styled mods with appropriate metadata.");
indent.Pop( 1 ); indent.Pop(1);
ImGui.BulletText( ImGui.BulletText("You can also create empty mod folders and delete mods.");
"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("For further editing of mods, select them and use the Edit Mod tab in the panel or the Advanced Editing popup.");
ImGui.BulletText( "Right-clicking a folder opens a context menu." ); ImGui.Dummy(Vector2.UnitY * ImGui.GetTextLineHeight());
ImGui.BulletText( "Right-clicking empty space allows you to expand or collapse all folders at once." ); ImGui.TextUnformatted("Mod Selector");
ImGui.BulletText( "Use the Filter Mods... input at the top to filter the list for mods whose name or path contain the text." ); 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(); indent.Push();
ImGui.BulletText( "You can enter n:[string] to filter only for names, without path." ); ImGuiUtil.BulletTextColored(ColorId.EnabledMod.Value(), "enabled in the current collection.");
ImGui.BulletText( "You can enter c:[string] to filter for Changed Items instead." ); ImGuiUtil.BulletTextColored(ColorId.DisabledMod.Value(), "disabled in the current collection.");
ImGui.BulletText( "You can enter a:[string] to filter for Mod Authors instead." ); ImGuiUtil.BulletTextColored(ColorId.InheritedMod.Value(), "enabled due to inheritance from another collection.");
indent.Pop( 1 ); ImGuiUtil.BulletTextColored(ColorId.InheritedDisabledMod.Value(), "disabled due to inheritance from another collection.");
ImGui.BulletText( "Use the expandable menu beside the input to filter for mods fulfilling specific criteria." ); 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.Raii;
using OtterGui.Widgets; using OtterGui.Widgets;
using Penumbra.Collections; using Penumbra.Collections;
using Penumbra.Services;
namespace Penumbra.UI; namespace Penumbra.UI;
@ -16,20 +17,22 @@ public partial class ConfigWindow
// Encapsulate for less pollution. // Encapsulate for less pollution.
private partial class CollectionsTab : IDisposable, ITab private partial class CollectionsTab : IDisposable, ITab
{ {
private readonly 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 public ReadOnlySpan<byte> Label
=> "Collections"u8; => "Collections"u8;
public void Dispose() public void Dispose()
=> Penumbra.CollectionManager.CollectionChanged -= UpdateIdentifiers; => _communicator.CollectionChange.Event -= UpdateIdentifiers;
public void DrawHeader() public void DrawHeader()
=> OpenTutorial( BasicTutorialSteps.Collections ); => OpenTutorial( BasicTutorialSteps.Collections );

View file

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

View file

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

View file

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

View file

@ -19,7 +19,7 @@ public sealed partial class ConfigWindow : Window, IDisposable
private readonly Penumbra _penumbra; private readonly Penumbra _penumbra;
private readonly ModFileSystemSelector _selector; private readonly ModFileSystemSelector _selector;
private readonly ModPanel _modPanel; private readonly ModPanel _modPanel;
public readonly ModEditWindow ModEditPopup = new(); public readonly ModEditWindow ModEditPopup;
private readonly SettingsTab _settingsTab; private readonly SettingsTab _settingsTab;
private readonly CollectionsTab _collectionsTab; private readonly CollectionsTab _collectionsTab;
@ -31,43 +31,43 @@ public sealed partial class ConfigWindow : Window, IDisposable
private readonly ResourceWatcher _resourceWatcher; private readonly ResourceWatcher _resourceWatcher;
public TabType SelectTab = TabType.None; public TabType SelectTab = TabType.None;
public void SelectMod( Mod mod )
=> _selector.SelectByValue( mod );
public ConfigWindow( Penumbra penumbra, ResourceWatcher watcher ) public void SelectMod(Mod mod)
: base( GetLabel() ) => _selector.SelectByValue(mod);
public ConfigWindow(CommunicatorService communicator, StartTracker timer, Penumbra penumbra, ResourceWatcher watcher)
: base(GetLabel())
{ {
_penumbra = penumbra; _penumbra = penumbra;
_resourceWatcher = watcher; _resourceWatcher = watcher;
_settingsTab = new SettingsTab( this ); ModEditPopup = new ModEditWindow(communicator);
_selector = new ModFileSystemSelector( _penumbra.ModFileSystem ); _settingsTab = new SettingsTab(this);
_modPanel = new ModPanel( this ); _selector = new ModFileSystemSelector(communicator, _penumbra.ModFileSystem);
_modsTab = new ModsTab( _selector, _modPanel, _penumbra ); _modPanel = new ModPanel(this);
_modsTab = new ModsTab(_selector, _modPanel, _penumbra);
_selector.SelectionChanged += _modPanel.OnSelectionChange; _selector.SelectionChanged += _modPanel.OnSelectionChange;
_collectionsTab = new CollectionsTab( this ); _collectionsTab = new CollectionsTab(communicator, this);
_changedItemsTab = new ChangedItemsTab( this ); _changedItemsTab = new ChangedItemsTab(this);
_effectiveTab = new EffectiveTab(); _effectiveTab = new EffectiveTab();
_debugTab = new DebugTab( this ); _debugTab = new DebugTab(this, timer);
_resourceTab = new ResourceTab(); _resourceTab = new ResourceTab();
if( Penumbra.Config.FixMainWindow ) if (Penumbra.Config.FixMainWindow)
{
Flags |= ImGuiWindowFlags.NoResize | ImGuiWindowFlags.NoMove; Flags |= ImGuiWindowFlags.NoResize | ImGuiWindowFlags.NoMove;
}
DalamudServices.PluginInterface.UiBuilder.DisableGposeUiHide = !Penumbra.Config.HideUiInGPose; DalamudServices.PluginInterface.UiBuilder.DisableGposeUiHide = !Penumbra.Config.HideUiInGPose;
DalamudServices.PluginInterface.UiBuilder.DisableCutsceneUiHide = !Penumbra.Config.HideUiInCutscenes; DalamudServices.PluginInterface.UiBuilder.DisableCutsceneUiHide = !Penumbra.Config.HideUiInCutscenes;
DalamudServices.PluginInterface.UiBuilder.DisableUserUiHide = !Penumbra.Config.HideUiWhenUiHidden; DalamudServices.PluginInterface.UiBuilder.DisableUserUiHide = !Penumbra.Config.HideUiWhenUiHidden;
RespectCloseHotkey = true; RespectCloseHotkey = true;
SizeConstraints = new WindowSizeConstraints() SizeConstraints = new WindowSizeConstraints()
{ {
MinimumSize = new Vector2( 800, 600 ), MinimumSize = new Vector2(800, 600),
MaximumSize = new Vector2( 4096, 2160 ), MaximumSize = new Vector2(4096, 2160),
}; };
UpdateTutorialStep(); UpdateTutorialStep();
} }
private ReadOnlySpan< byte > ToLabel( TabType type ) private ReadOnlySpan<byte> ToLabel(TabType type)
=> type switch => type switch
{ {
TabType.Settings => _settingsTab.Label, TabType.Settings => _settingsTab.Label,
@ -78,85 +78,85 @@ public sealed partial class ConfigWindow : Window, IDisposable
TabType.ResourceWatcher => _resourceWatcher.Label, TabType.ResourceWatcher => _resourceWatcher.Label,
TabType.Debug => _debugTab.Label, TabType.Debug => _debugTab.Label,
TabType.ResourceManager => _resourceTab.Label, TabType.ResourceManager => _resourceTab.Label,
_ => ReadOnlySpan< byte >.Empty, _ => ReadOnlySpan<byte>.Empty,
}; };
public override void Draw() public override void Draw()
{ {
using var performance = Penumbra.Performance.Measure( PerformanceType.UiMainWindow ); using var performance = Penumbra.Performance.Measure(PerformanceType.UiMainWindow);
try 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" + "This usually means that your game installation was corrupted by updating the game while having TexTools mods still active.\n"
+ "It is recommended to not use TexTools and Penumbra (or other Lumina-based tools) at the same time.\n\n" + "It is recommended to not use TexTools and Penumbra (or other Lumina-based tools) at the same time.\n\n"
+ "Please use the Launcher's Repair Game Files function to repair your client installation.", true ); + "Please use the Launcher's Repair Game Files function to repair your client installation.", true);
} }
else if( !Penumbra.ValidityChecker.IsValidSourceRepo ) else if (!Penumbra.ValidityChecker.IsValidSourceRepo)
{ {
DrawProblemWindow( DrawProblemWindow(_penumbra,
$"You are loading a release version of Penumbra from the repository \"{DalamudServices.PluginInterface.SourceRepository}\" instead of the official repository.\n" $"You are loading a release version of Penumbra from the repository \"{DalamudServices.PluginInterface.SourceRepository}\" instead of the official repository.\n"
+ $"Please use the official repository at {ValidityChecker.Repository}.\n\n" + $"Please use the official repository at {ValidityChecker.Repository}.\n\n"
+ "If you are developing for Penumbra and see this, you should compile your version in debug mode to avoid it.", false ); + "If you are developing for Penumbra and see this, you should compile your version in debug mode to avoid it.", false);
} }
else if( Penumbra.ValidityChecker.IsNotInstalledPenumbra ) else if (Penumbra.ValidityChecker.IsNotInstalledPenumbra)
{ {
DrawProblemWindow( DrawProblemWindow(_penumbra,
$"You are loading a release version of Penumbra from \"{DalamudServices.PluginInterface.AssemblyLocation.Directory?.FullName ?? "Unknown"}\" instead of the installedPlugins directory.\n\n" $"You are loading a release version of Penumbra from \"{DalamudServices.PluginInterface.AssemblyLocation.Directory?.FullName ?? "Unknown"}\" instead of the installedPlugins directory.\n\n"
+ "You should not install Penumbra manually, but rather add the plugin repository under settings and then install it via the plugin installer.\n\n" + "You should not install Penumbra manually, but rather add the plugin repository under settings and then install it via the plugin installer.\n\n"
+ "If you do not know how to do this, please take a look at the readme in Penumbras github repository or join us in discord.\n" + "If you do not know how to do this, please take a look at the readme in Penumbras github repository or join us in discord.\n"
+ "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"}\", " $"You are loading a installed version of Penumbra from \"{DalamudServices.PluginInterface.AssemblyLocation.Directory?.FullName ?? "Unknown"}\", "
+ "but also still have some remnants of a custom install of Penumbra in your devPlugins folder.\n\n" + "but also still have some remnants of a custom install of Penumbra in your devPlugins folder.\n\n"
+ "This can cause some issues, so please go to your \"%%appdata%%\\XIVLauncher\\devPlugins\" folder and delete the Penumbra folder from there.\n\n" + "This can cause some issues, so please go to your \"%%appdata%%\\XIVLauncher\\devPlugins\" folder and delete the Penumbra folder from there.\n\n"
+ "If you are developing for Penumbra, try to avoid mixing versions. This warning will not appear if compiled in Debug mode.", false ); + "If you are developing for Penumbra, try to avoid mixing versions. This warning will not appear if compiled in Debug mode.",
false);
} }
else else
{ {
SetupSizes(); SetupSizes();
if( TabBar.Draw( string.Empty, ImGuiTabBarFlags.NoTooltip, ToLabel( SelectTab ), _settingsTab, _modsTab, _collectionsTab, if (TabBar.Draw(string.Empty, ImGuiTabBarFlags.NoTooltip, ToLabel(SelectTab), _settingsTab, _modsTab, _collectionsTab,
_changedItemsTab, _effectiveTab, _resourceWatcher, _debugTab, _resourceTab ) ) _changedItemsTab, _effectiveTab, _resourceWatcher, _debugTab, _resourceTab))
{
SelectTab = TabType.None; SelectTab = TabType.None;
}
} }
} }
catch( Exception e ) catch (Exception e)
{ {
Penumbra.Log.Error( $"Exception thrown during UI Render:\n{e}" ); Penumbra.Log.Error($"Exception thrown during UI Render:\n{e}");
} }
} }
private static void DrawProblemWindow( string text, bool withExceptions ) private static void DrawProblemWindow(Penumbra penumbra, string text, bool withExceptions)
{ {
using var color = ImRaii.PushColor( ImGuiCol.Text, Colors.RegexWarningBorder ); using var color = ImRaii.PushColor(ImGuiCol.Text, Colors.RegexWarningBorder);
ImGui.NewLine(); ImGui.NewLine();
ImGui.NewLine(); ImGui.NewLine();
ImGuiUtil.TextWrapped( text ); ImGuiUtil.TextWrapped(text);
color.Pop(); color.Pop();
ImGui.NewLine(); ImGui.NewLine();
ImGui.NewLine(); ImGui.NewLine();
SettingsTab.DrawDiscordButton( 0 ); SettingsTab.DrawDiscordButton(0);
ImGui.SameLine(); ImGui.SameLine();
SettingsTab.DrawSupportButton(); SettingsTab.DrawSupportButton(penumbra);
ImGui.NewLine(); ImGui.NewLine();
ImGui.NewLine(); ImGui.NewLine();
if( withExceptions ) if (withExceptions)
{ {
ImGui.TextUnformatted( "Exceptions" ); ImGui.TextUnformatted("Exceptions");
ImGui.Separator(); ImGui.Separator();
using var box = ImRaii.ListBox( "##Exceptions", new Vector2( -1, -1 ) ); using var box = ImRaii.ListBox("##Exceptions", new Vector2(-1, -1));
foreach( var exception in Penumbra.ValidityChecker.ImcExceptions ) foreach (var exception in Penumbra.ValidityChecker.ImcExceptions)
{ {
ImGuiUtil.TextWrapped( exception.ToString() ); ImGuiUtil.TextWrapped(exception.ToString());
ImGui.Separator(); ImGui.Separator();
ImGui.NewLine(); ImGui.NewLine();
} }
@ -182,8 +182,8 @@ public sealed partial class ConfigWindow : Window, IDisposable
private void SetupSizes() private void SetupSizes()
{ {
_defaultSpace = new Vector2( 0, 10 * ImGuiHelpers.GlobalScale ); _defaultSpace = new Vector2(0, 10 * ImGuiHelpers.GlobalScale);
_inputTextWidth = new Vector2( 350f * ImGuiHelpers.GlobalScale, 0 ); _inputTextWidth = new Vector2(350f * ImGuiHelpers.GlobalScale, 0);
_iconButtonSize = new Vector2( ImGui.GetFrameHeight() ); _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; namespace Penumbra.Util;
@ -47,24 +48,24 @@ public enum PerformanceType
public static class TimingExtensions public static class TimingExtensions
{ {
public static string ToName( this StartTimeType type ) public static string ToName(this StartTimeType type)
=> type switch => type switch
{ {
StartTimeType.Total => "Total Construction", StartTimeType.Total => "Total Construction",
StartTimeType.Identifier => "Identification Data", StartTimeType.Identifier => "Identification Data",
StartTimeType.Stains => "Stain Data", StartTimeType.Stains => "Stain Data",
StartTimeType.Items => "Item Data", StartTimeType.Items => "Item Data",
StartTimeType.Actors => "Actor Data", StartTimeType.Actors => "Actor Data",
StartTimeType.Backup => "Checking Backups", StartTimeType.Backup => "Checking Backups",
StartTimeType.Mods => "Loading Mods", StartTimeType.Mods => "Loading Mods",
StartTimeType.Collections => "Loading Collections", StartTimeType.Collections => "Loading Collections",
StartTimeType.Api => "Setting Up API", StartTimeType.Api => "Setting Up API",
StartTimeType.Interface => "Setting Up Interface", StartTimeType.Interface => "Setting Up Interface",
StartTimeType.PathResolver => "Setting Up Path Resolver", StartTimeType.PathResolver => "Setting Up Path Resolver",
_ => $"Unknown {(int) type}", _ => $"Unknown {(int)type}",
}; };
public static string ToName( this PerformanceType type ) public static string ToName(this PerformanceType type)
=> type switch => type switch
{ {
PerformanceType.UiMainWindow => "Main Interface Drawing", PerformanceType.UiMainWindow => "Main Interface Drawing",
@ -91,6 +92,6 @@ public static class TimingExtensions
PerformanceType.LoadPap => "LoadPap Hook", PerformanceType.LoadPap => "LoadPap Hook",
PerformanceType.LoadAction => "LoadAction Hook", PerformanceType.LoadAction => "LoadAction Hook",
PerformanceType.DebugTimes => "Debug Tracking", 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();
}