Update everything except for IPC and temp collections to new system.

This commit is contained in:
Ottermandias 2022-11-17 18:17:23 +01:00
parent 6a6eac1c3b
commit 4309ae8ce2
16 changed files with 400 additions and 249 deletions

View file

@ -37,7 +37,8 @@ public readonly struct ActorIdentifier : IEquatable<ActorIdentifier>
IdentifierType.Player => HomeWorld == other.HomeWorld && PlayerName.EqualsCi(other.PlayerName), IdentifierType.Player => HomeWorld == other.HomeWorld && PlayerName.EqualsCi(other.PlayerName),
IdentifierType.Owned => HomeWorld == other.HomeWorld && PlayerName.EqualsCi(other.PlayerName) && Manager.DataIdEquals(this, other), IdentifierType.Owned => HomeWorld == other.HomeWorld && PlayerName.EqualsCi(other.PlayerName) && Manager.DataIdEquals(this, other),
IdentifierType.Special => Special == other.Special, IdentifierType.Special => Special == other.Special,
IdentifierType.Npc => Manager.DataIdEquals(this, other) && (Index == other.Index || Index == ushort.MaxValue || other.Index == ushort.MaxValue), IdentifierType.Npc => Manager.DataIdEquals(this, other)
&& (Index == other.Index || Index == ushort.MaxValue || other.Index == ushort.MaxValue),
_ => false, _ => false,
}; };
} }
@ -107,8 +108,9 @@ public readonly struct ActorIdentifier : IEquatable<ActorIdentifier>
ret.Add(nameof(Special), Special.ToString()); ret.Add(nameof(Special), Special.ToString());
return ret; return ret;
case IdentifierType.Npc: case IdentifierType.Npc:
ret.Add(nameof(Kind), Kind.ToString()); ret.Add(nameof(Kind), Kind.ToString());
ret.Add(nameof(Index), Index); if (Index != ushort.MaxValue)
ret.Add(nameof(Index), Index);
ret.Add(nameof(DataId), DataId); ret.Add(nameof(DataId), DataId);
return ret; return ret;
} }

View file

@ -14,40 +14,41 @@ public partial class ActorManager
/// </summary> /// </summary>
/// <param name="data">A parsed JObject</param> /// <param name="data">A parsed JObject</param>
/// <returns>ActorIdentifier.Invalid if the JObject can not be converted, a valid ActorIdentifier otherwise.</returns> /// <returns>ActorIdentifier.Invalid if the JObject can not be converted, a valid ActorIdentifier otherwise.</returns>
public ActorIdentifier FromJson(JObject data) public ActorIdentifier FromJson(JObject? data)
{ {
var type = data[nameof(ActorIdentifier.Type)]?.Value<IdentifierType>() ?? IdentifierType.Invalid; if (data == null)
return ActorIdentifier.Invalid;
var type = data[nameof(ActorIdentifier.Type)]?.ToObject<IdentifierType>() ?? IdentifierType.Invalid;
switch (type) switch (type)
{ {
case IdentifierType.Player: case IdentifierType.Player:
{ {
var name = ByteString.FromStringUnsafe(data[nameof(ActorIdentifier.PlayerName)]?.Value<string>(), false); var name = ByteString.FromStringUnsafe(data[nameof(ActorIdentifier.PlayerName)]?.ToObject<string>(), false);
var homeWorld = data[nameof(ActorIdentifier.HomeWorld)]?.Value<ushort>() ?? 0; var homeWorld = data[nameof(ActorIdentifier.HomeWorld)]?.ToObject<ushort>() ?? 0;
return CreatePlayer(name, homeWorld); return CreatePlayer(name, homeWorld);
} }
case IdentifierType.Owned: case IdentifierType.Owned:
{ {
var name = ByteString.FromStringUnsafe(data[nameof(ActorIdentifier.PlayerName)]?.Value<string>(), false); var name = ByteString.FromStringUnsafe(data[nameof(ActorIdentifier.PlayerName)]?.ToObject<string>(), false);
var homeWorld = data[nameof(ActorIdentifier.HomeWorld)]?.Value<ushort>() ?? 0; var homeWorld = data[nameof(ActorIdentifier.HomeWorld)]?.ToObject<ushort>() ?? 0;
var kind = data[nameof(ActorIdentifier.Kind)]?.Value<ObjectKind>() ?? ObjectKind.CardStand; var kind = data[nameof(ActorIdentifier.Kind)]?.ToObject<ObjectKind>() ?? ObjectKind.CardStand;
var dataId = data[nameof(ActorIdentifier.DataId)]?.Value<uint>() ?? 0; var dataId = data[nameof(ActorIdentifier.DataId)]?.ToObject<uint>() ?? 0;
return CreateOwned(name, homeWorld, kind, dataId); return CreateOwned(name, homeWorld, kind, dataId);
} }
case IdentifierType.Special: case IdentifierType.Special:
{ {
var special = data[nameof(ActorIdentifier.Special)]?.Value<SpecialActor>() ?? 0; var special = data[nameof(ActorIdentifier.Special)]?.ToObject<SpecialActor>() ?? 0;
return CreateSpecial(special); return CreateSpecial(special);
} }
case IdentifierType.Npc: case IdentifierType.Npc:
{ {
var index = data[nameof(ActorIdentifier.Index)]?.Value<ushort>() ?? ushort.MaxValue; var index = data[nameof(ActorIdentifier.Index)]?.ToObject<ushort>() ?? ushort.MaxValue;
var kind = data[nameof(ActorIdentifier.Kind)]?.Value<ObjectKind>() ?? ObjectKind.CardStand; var kind = data[nameof(ActorIdentifier.Kind)]?.ToObject<ObjectKind>() ?? ObjectKind.CardStand;
var dataId = data[nameof(ActorIdentifier.DataId)]?.Value<uint>() ?? 0; var dataId = data[nameof(ActorIdentifier.DataId)]?.ToObject<uint>() ?? 0;
return CreateNpc(kind, dataId, index); return CreateNpc(kind, dataId, index);
} }
case IdentifierType.Invalid: default: return ActorIdentifier.Invalid;
default:
return ActorIdentifier.Invalid;
} }
} }

View file

@ -14,6 +14,8 @@ using System.IO;
using System.Linq; using System.Linq;
using System.Reflection; using System.Reflection;
using Penumbra.Api.Enums; using Penumbra.Api.Enums;
using Penumbra.GameData.Actors;
using Penumbra.String;
using Penumbra.String.Classes; using Penumbra.String.Classes;
namespace Penumbra.Api; namespace Penumbra.Api;
@ -212,7 +214,7 @@ public class PenumbraApi : IDisposable, IPenumbraApi
{ {
CheckInitialized(); CheckInitialized();
return ResolvePath( path, Penumbra.ModManager, return ResolvePath( path, Penumbra.ModManager,
Penumbra.CollectionManager.Character( characterName ) ); Penumbra.CollectionManager.Individual( NameToIdentifier( characterName ) ) );
} }
public string[] ReverseResolvePath( string path, string characterName ) public string[] ReverseResolvePath( string path, string characterName )
@ -223,7 +225,7 @@ public class PenumbraApi : IDisposable, IPenumbraApi
return new[] { path }; return new[] { path };
} }
var ret = Penumbra.CollectionManager.Character( characterName ).ReverseResolvePath( new FullPath( path ) ); var ret = Penumbra.CollectionManager.Individual( NameToIdentifier( characterName ) ).ReverseResolvePath( new FullPath( path ) );
return ret.Select( r => r.ToString() ).ToArray(); return ret.Select( r => r.ToString() ).ToArray();
} }
@ -297,7 +299,7 @@ public class PenumbraApi : IDisposable, IPenumbraApi
public (string, bool) GetCharacterCollection( string characterName ) public (string, bool) GetCharacterCollection( string characterName )
{ {
CheckInitialized(); CheckInitialized();
return Penumbra.CollectionManager.Characters.TryGetValue( characterName, out var collection ) return Penumbra.CollectionManager.Individuals.TryGetCollection( NameToIdentifier( characterName ), out var collection )
? ( collection.Name, true ) ? ( collection.Name, true )
: ( Penumbra.CollectionManager.Default.Name, false ); : ( Penumbra.CollectionManager.Default.Name, false );
} }
@ -570,7 +572,7 @@ public class PenumbraApi : IDisposable, IPenumbraApi
return ( PenumbraApiEc.InvalidArgument, string.Empty ); return ( PenumbraApiEc.InvalidArgument, string.Empty );
} }
if( !forceOverwriteCharacter && Penumbra.CollectionManager.Characters.ContainsKey( character ) if( !forceOverwriteCharacter && Penumbra.CollectionManager.Individuals.Individuals.ContainsKey( NameToIdentifier(character) )
|| Penumbra.TempMods.Collections.ContainsKey( character ) ) || Penumbra.TempMods.Collections.ContainsKey( character ) )
{ {
return ( PenumbraApiEc.CharacterCollectionExists, string.Empty ); return ( PenumbraApiEc.CharacterCollectionExists, string.Empty );
@ -680,7 +682,7 @@ public class PenumbraApi : IDisposable, IPenumbraApi
CheckInitialized(); CheckInitialized();
var collection = Penumbra.TempMods.Collections.TryGetValue( characterName, out var c ) var collection = Penumbra.TempMods.Collections.TryGetValue( characterName, out var c )
? c ? c
: Penumbra.CollectionManager.Character( characterName ); : Penumbra.CollectionManager.Individual( NameToIdentifier(characterName) );
var set = collection.MetaCache?.Manipulations.ToArray() ?? Array.Empty< MetaManipulation >(); var set = collection.MetaCache?.Manipulations.ToArray() ?? Array.Empty< MetaManipulation >();
return Functions.ToCompressedBase64( set, MetaManipulation.CurrentVersion ); return Functions.ToCompressedBase64( set, MetaManipulation.CurrentVersion );
} }
@ -804,7 +806,7 @@ public class PenumbraApi : IDisposable, IPenumbraApi
c.ModSettingChanged += Del; c.ModSettingChanged += Del;
} }
private void SubscribeToNewCollections( CollectionType type, ModCollection? oldCollection, ModCollection? newCollection, string? _ ) private void SubscribeToNewCollections( CollectionType type, ModCollection? oldCollection, ModCollection? newCollection, string _ )
{ {
if( type != CollectionType.Inactive ) if( type != CollectionType.Inactive )
{ {
@ -827,4 +829,11 @@ public class PenumbraApi : IDisposable, IPenumbraApi
public void InvokePostSettingsPanel( string modDirectory ) public void InvokePostSettingsPanel( string modDirectory )
=> PostSettingsPanelDraw?.Invoke( modDirectory ); => PostSettingsPanelDraw?.Invoke( modDirectory );
// TODO
private static ActorIdentifier NameToIdentifier( string name )
{
var b = ByteString.FromStringUnsafe( name, false );
return Penumbra.Actors.CreatePlayer( b, ( ushort )( Dalamud.ClientState.LocalPlayer?.HomeWorld.Id ?? ushort.MaxValue ) );
}
} }

View file

@ -8,6 +8,9 @@ using System.Collections.Generic;
using System.IO; using System.IO;
using System.Linq; using System.Linq;
using System.Threading.Tasks; using System.Threading.Tasks;
using Dalamud.Interface.Internal.Notifications;
using Penumbra.GameData.Actors;
using Penumbra.Util;
namespace Penumbra.Collections; namespace Penumbra.Collections;
@ -36,21 +39,20 @@ 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.
private readonly Dictionary< string, ModCollection > _characters = new(); public readonly IndividualCollections Individuals = new(Penumbra.Actors);
public readonly IndividualCollections Individuals = new(Penumbra.Actors);
public IReadOnlyDictionary< string, ModCollection > Characters public ModCollection Individual( ActorIdentifier identifier )
=> _characters; => Individuals.TryGetCollection( identifier, out var c ) ? c : Default;
// If a name does not correspond to a character, return the default collection instead.
public ModCollection Character( string name )
=> _characters.TryGetValue( name, out var c ) ? c : Default;
// Special Collections // Special Collections
private readonly ModCollection?[] _specialCollections = new ModCollection?[Enum.GetValues< CollectionType >().Length - 4]; private readonly ModCollection?[] _specialCollections = new ModCollection?[Enum.GetValues< CollectionType >().Length - 4];
// Return the configured collection for the given type or null. // Return the configured collection for the given type or null.
public ModCollection? ByType( CollectionType type, string? name = null ) // Does not handle Inactive, use ByName instead.
public ModCollection? ByType( CollectionType type )
=> ByType( type, ActorIdentifier.Invalid );
public ModCollection? ByType( CollectionType type, ActorIdentifier identifier )
{ {
if( type.IsSpecial() ) if( type.IsSpecial() )
{ {
@ -59,28 +61,23 @@ public partial class ModCollection
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 => name != null ? _characters.TryGetValue( name, out var c ) ? c : null : null, CollectionType.Individual => identifier.IsValid ? Individuals.TryGetCollection( identifier, out var c ) ? c : null : null,
CollectionType.Inactive => name != null ? ByName( name, out var c ) ? c : null : null, _ => null,
_ => null,
}; };
} }
// Set a active collection, can be used to set Default, Current or Character collections. // Set a active collection, can be used to set Default, Current, Interface, Special, or Individual collections.
private void SetCollection( int newIdx, CollectionType collectionType, string? characterName = null ) 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 => characterName?.Length > 0 CollectionType.Individual => individualIndex < 0 || individualIndex >= Individuals.Count ? -1 : Individuals[ individualIndex ].Collection.Index,
? _characters.TryGetValue( characterName, out var c )
? c.Index
: Default.Index
: -1,
_ when collectionType.IsSpecial() => _specialCollections[ ( int )collectionType ]?.Index ?? Default.Index, _ when collectionType.IsSpecial() => _specialCollections[ ( int )collectionType ]?.Index ?? Default.Index,
_ => -1, _ => -1,
}; };
@ -114,7 +111,12 @@ public partial class ModCollection
Current = newCollection; Current = newCollection;
break; break;
case CollectionType.Individual: case CollectionType.Individual:
_characters[ characterName! ] = newCollection; if( !Individuals.ChangeCollection( individualIndex, newCollection ) )
{
RemoveCache( newIdx );
return;
}
break; break;
default: default:
_specialCollections[ ( int )collectionType ] = newCollection; _specialCollections[ ( int )collectionType ] = newCollection;
@ -124,7 +126,7 @@ public partial class ModCollection
RemoveCache( oldCollectionIdx ); RemoveCache( oldCollectionIdx );
UpdateCurrentCollectionInUse(); UpdateCurrentCollectionInUse();
CollectionChanged.Invoke( collectionType, this[ oldCollectionIdx ], newCollection, characterName ); CollectionChanged.Invoke( collectionType, this[ oldCollectionIdx ], newCollection, Individuals[ individualIndex ].DisplayName );
} }
private void UpdateCurrentCollectionInUse() private void UpdateCurrentCollectionInUse()
@ -132,11 +134,11 @@ public partial class ModCollection
.OfType< ModCollection >() .OfType< ModCollection >()
.Prepend( Interface ) .Prepend( Interface )
.Prepend( Default ) .Prepend( Default )
.Concat( Characters.Values ) .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, string? characterName = null ) public void SetCollection( ModCollection collection, CollectionType collectionType, int individualIndex = -1 )
=> SetCollection( collection.Index, collectionType, characterName ); => 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 )
@ -147,7 +149,7 @@ public partial class ModCollection
} }
_specialCollections[ ( int )collectionType ] = Default; _specialCollections[ ( int )collectionType ] = Default;
CollectionChanged.Invoke( collectionType, null, Default, null ); CollectionChanged.Invoke( collectionType, null, Default );
return true; return true;
} }
@ -163,31 +165,38 @@ public partial class ModCollection
if( old != null ) if( old != null )
{ {
_specialCollections[ ( int )collectionType ] = null; _specialCollections[ ( int )collectionType ] = null;
CollectionChanged.Invoke( collectionType, old, null, null ); CollectionChanged.Invoke( collectionType, old, null );
} }
} }
// Create a new character collection. Returns false if the character name already has a collection. // Wrappers around Individual Collection handling.
public bool CreateCharacterCollection( string characterName ) public void CreateIndividualCollection( ActorIdentifier[] identifiers )
{ {
if( _characters.ContainsKey( characterName ) ) if( Individuals.Add( identifiers, Default ) )
{ {
return false; CollectionChanged.Invoke( CollectionType.Individual, null, Default, Individuals.Last().DisplayName );
} }
_characters[ characterName ] = Default;
CollectionChanged.Invoke( CollectionType.Individual, null, Default, characterName );
return true;
} }
// Remove a character collection if it exists. public void RemoveIndividualCollection( int individualIndex )
public void RemoveCharacterCollection( string characterName )
{ {
if( _characters.TryGetValue( characterName, out var collection ) ) if( individualIndex < 0 || individualIndex >= Individuals.Count )
{ {
RemoveCache( collection.Index ); return;
_characters.Remove( characterName ); }
CollectionChanged.Invoke( CollectionType.Individual, collection, null, characterName );
var (name, old) = Individuals[ individualIndex ];
if( Individuals.Delete( individualIndex ) )
{
CollectionChanged.Invoke( CollectionType.Individual, old, null, name );
}
}
public void MoveIndividualCollection( int from, int to )
{
if( Individuals.Move( from, to ) )
{
SaveActiveCollections();
} }
} }
@ -209,7 +218,8 @@ public partial class ModCollection
var defaultIdx = GetIndexForCollectionName( defaultName ); var defaultIdx = GetIndexForCollectionName( defaultName );
if( defaultIdx < 0 ) if( defaultIdx < 0 )
{ {
Penumbra.Log.Error( $"Last choice of {ConfigWindow.DefaultCollection} {defaultName} is not available, reset to {Empty.Name}." ); ChatUtil.NotificationMessage( $"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;
} }
@ -223,8 +233,8 @@ public partial class ModCollection
var interfaceIdx = GetIndexForCollectionName( interfaceName ); var interfaceIdx = GetIndexForCollectionName( interfaceName );
if( interfaceIdx < 0 ) if( interfaceIdx < 0 )
{ {
Penumbra.Log.Error( ChatUtil.NotificationMessage(
$"Last choice of {ConfigWindow.InterfaceCollection} {interfaceName} is not available, reset to {Empty.Name}." ); $"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;
} }
@ -238,8 +248,8 @@ public partial class ModCollection
var currentIdx = GetIndexForCollectionName( currentName ); var currentIdx = GetIndexForCollectionName( currentName );
if( currentIdx < 0 ) if( currentIdx < 0 )
{ {
Penumbra.Log.Error( ChatUtil.NotificationMessage(
$"Last choice of {ConfigWindow.SelectedCollection} {currentName} is not available, reset to {DefaultCollection}." ); $"Last choice of {ConfigWindow.SelectedCollection} {currentName} is not available, reset to {DefaultCollection}.", "Load Failure", NotificationType.Warning );
Current = DefaultName; Current = DefaultName;
configChanged = true; configChanged = true;
} }
@ -257,7 +267,7 @@ public partial class ModCollection
var idx = GetIndexForCollectionName( typeName ); var idx = GetIndexForCollectionName( typeName );
if( idx < 0 ) if( idx < 0 )
{ {
Penumbra.Log.Error( $"Last choice of {name} Collection {typeName} is not available, removed." ); ChatUtil.NotificationMessage( $"Last choice of {name} Collection {typeName} is not available, removed.", "Load Failure", NotificationType.Warning );
configChanged = true; configChanged = true;
} }
else else
@ -267,30 +277,14 @@ public partial class ModCollection
} }
} }
// Load character collections. If a player name comes up multiple times, the last one is applied. configChanged |= MigrateIndividualCollections( jObject );
var characters = jObject[ nameof( Characters ) ]?.ToObject< Dictionary< string, string > >() ?? new Dictionary< string, string >(); configChanged |= Individuals.ReadJObject( jObject[ nameof( Individuals ) ] as JArray, this );
foreach( var (player, collectionName) in characters )
{
var idx = GetIndexForCollectionName( collectionName );
if( idx < 0 )
{
Penumbra.Log.Error( $"Last choice of <{player}>'s Collection {collectionName} is not available, reset to {Empty.Name}." );
_characters.Add( player, Empty );
configChanged = true;
}
else
{
_characters.Add( player, this[ idx ] );
}
}
// Save any changes and create all required caches. // Save any changes and create all required caches.
if( configChanged ) if( configChanged )
{ {
SaveActiveCollections(); SaveActiveCollections();
} }
MigrateIndividualCollections( jObject );
} }
// Migrate ungendered collections to Male and Female for 0.5.9.0. // Migrate ungendered collections to Male and Female for 0.5.9.0.
@ -323,21 +317,24 @@ public partial class ModCollection
} }
// 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[nameof( 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 )
{ {
Penumbra.Log.Error( $"Last choice of <{player}>'s Collection {collectionName} is not available, reset to {Empty.Name}." ); ChatUtil.NotificationMessage( $"Last choice of <{player}>'s Collection {collectionName} is not available, reset to {Empty.Name}.", "Load Failure",
NotificationType.Warning );
dict.Add( player, Empty ); dict.Add( player, Empty );
} }
else else
@ -345,7 +342,7 @@ public partial class ModCollection
dict.Add( player, this[ idx ] ); dict.Add( player, this[ idx ] );
} }
} }
Individuals.Migrate0To1( dict ); Individuals.Migrate0To1( dict );
return true; return true;
} }
@ -353,46 +350,32 @@ public partial class ModCollection
public void SaveActiveCollections() public void SaveActiveCollections()
{ {
Penumbra.Framework.RegisterDelayed( nameof( SaveActiveCollections ), Penumbra.Framework.RegisterDelayed( nameof( SaveActiveCollections ),
() => SaveActiveCollections( Default.Name, Interface.Name, Current.Name, SaveActiveCollectionsInternal );
Characters.Select( kvp => ( kvp.Key, kvp.Value.Name ) ),
_specialCollections.WithIndex()
.Where( c => c.Item1 != null )
.Select( c => ( ( CollectionType )c.Item2, c.Item1!.Name ) ) ) );
} }
internal static void SaveActiveCollections( string def, string ui, string current, IEnumerable< (string, string) > characters, internal void SaveActiveCollectionsInternal()
IEnumerable< (CollectionType, string) > special )
{ {
var file = ActiveCollectionFile; var file = ActiveCollectionFile;
try try
{ {
var jObj = new JObject
{
{ nameof( Version ), Version },
{ nameof( Default ), Default.Name },
{ nameof( Interface ), Interface.Name },
{ nameof( Current ), Current.Name },
};
foreach( var (type, collection) in _specialCollections.WithIndex().Where( p => p.Value != null ).Select( p => ( ( CollectionType )p.Index, p.Value! ) ) )
{
jObj.Add( type.ToString(), collection.Name );
}
jObj.Add( nameof( Individuals ), Individuals.ToJObject() );
using var stream = File.Open( file, File.Exists( file ) ? FileMode.Truncate : FileMode.CreateNew ); using var 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 )
j.Formatting = Formatting.Indented; { Formatting = Formatting.Indented };
j.WriteStartObject(); jObj.WriteTo( j );
j.WritePropertyName( nameof( Default ) );
j.WriteValue( def );
j.WritePropertyName( nameof( Interface ) );
j.WriteValue( ui );
j.WritePropertyName( nameof( Current ) );
j.WriteValue( current );
foreach( var (type, collection) in special )
{
j.WritePropertyName( type.ToString() );
j.WriteValue( collection );
}
j.WritePropertyName( nameof( 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." ); Penumbra.Log.Verbose( "Active Collections saved." );
} }
catch( Exception e ) catch( Exception e )
@ -424,7 +407,7 @@ public partial class ModCollection
} }
// Save if any of the active collections is changed. // Save if any of the active collections is changed.
private void SaveOnChange( CollectionType collectionType, ModCollection? _1, ModCollection? _2, string? _3 ) private void SaveOnChange( CollectionType collectionType, ModCollection? _1, ModCollection? _2, string _3 )
{ {
if( collectionType != CollectionType.Inactive ) if( collectionType != CollectionType.Inactive )
{ {
@ -437,7 +420,7 @@ public partial class ModCollection
public void CreateNecessaryCaches() public void CreateNecessaryCaches()
{ {
var tasks = _specialCollections.OfType< ModCollection >() var tasks = _specialCollections.OfType< ModCollection >()
.Concat( _characters.Values ) .Concat( Individuals.Select( p => p.Collection ) )
.Prepend( Current ) .Prepend( Current )
.Prepend( Default ) .Prepend( Default )
.Prepend( Interface ) .Prepend( Interface )
@ -455,7 +438,7 @@ public partial class ModCollection
&& 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 )
&& _characters.Values.All( c => c.Index != idx ) ) && Individuals.Select( p => p.Collection ).All( c => c.Index != idx ) )
{ {
_collections[ idx ].ClearCache(); _collections[ idx ].ClearCache();
} }

View file

@ -7,6 +7,7 @@ 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.GameData.Actors;
namespace Penumbra.Collections; namespace Penumbra.Collections;
@ -15,9 +16,9 @@ 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. // On addition, oldCollection is null. On deletion, newCollection is null.
// CharacterName is only set for type == Character. // displayName is only set for type == Individual.
public delegate void CollectionChangeDelegate( CollectionType collectionType, ModCollection? oldCollection, public delegate void CollectionChangeDelegate( CollectionType collectionType, ModCollection? oldCollection,
ModCollection? newCollection, string? characterName = null ); ModCollection? newCollection, string displayName = "" );
private readonly Mod.Manager _modManager; private readonly Mod.Manager _modManager;
@ -139,19 +140,21 @@ public partial class ModCollection
if( idx == Current.Index ) if( idx == Current.Index )
{ {
SetCollection( DefaultName, CollectionType.Current ); SetCollection( DefaultName.Index, CollectionType.Current );
} }
if( idx == Default.Index ) if( idx == Default.Index )
{ {
SetCollection( Empty, CollectionType.Default ); SetCollection( Empty.Index, CollectionType.Default );
} }
foreach( var (characterName, _) in _characters.Where( c => c.Value.Index == idx ).ToList() ) for( var i = 0; i < Individuals.Count; ++i )
{ {
SetCollection( Empty, CollectionType.Individual, characterName ); if( Individuals[ i ].Collection.Index == idx )
{
Individuals.ChangeCollection( i, Empty );
}
} }
var collection = _collections[ idx ]; var collection = _collections[ idx ];
// Clear own inheritances. // Clear own inheritances.

View file

@ -12,7 +12,7 @@ namespace Penumbra.Collections;
public sealed partial class IndividualCollections : IReadOnlyList< (string DisplayName, ModCollection Collection) > public sealed partial class IndividualCollections : IReadOnlyList< (string DisplayName, ModCollection Collection) >
{ {
public IEnumerator< (string DisplayName, ModCollection Collection) > GetEnumerator() public IEnumerator< (string DisplayName, ModCollection Collection) > GetEnumerator()
=> _assignments.Select( kvp => ( kvp.Key, kvp.Value.Collection ) ).GetEnumerator(); => _assignments.Select( t => ( t.DisplayName, t.Collection ) ).GetEnumerator();
IEnumerator IEnumerable.GetEnumerator() IEnumerator IEnumerable.GetEnumerator()
=> GetEnumerator(); => GetEnumerator();
@ -21,7 +21,7 @@ public sealed partial class IndividualCollections : IReadOnlyList< (string Displ
=> _assignments.Count; => _assignments.Count;
public (string DisplayName, ModCollection Collection) this[ int index ] public (string DisplayName, ModCollection Collection) this[ int index ]
=> ( _assignments.Keys[ index ], _assignments.Values[ index ].Collection ); => ( _assignments[ index ].DisplayName, _assignments[ index ].Collection );
public bool TryGetCollection( ActorIdentifier identifier, [NotNullWhen( true )] out ModCollection? collection ) public bool TryGetCollection( ActorIdentifier identifier, [NotNullWhen( true )] out ModCollection? collection )
{ {
@ -36,7 +36,7 @@ public sealed partial class IndividualCollections : IReadOnlyList< (string Displ
} }
// Handle generic NPC // Handle generic NPC
var npcIdentifier = _manager.CreateIndividualUnchecked( IdentifierType.Npc, ByteString.Empty, ushort.MaxValue, identifier.Kind, identifier.DataId ); var npcIdentifier = _actorManager.CreateIndividualUnchecked( IdentifierType.Npc, ByteString.Empty, ushort.MaxValue, identifier.Kind, identifier.DataId );
if( npcIdentifier.IsValid && _individuals.TryGetValue( npcIdentifier, out collection ) ) if( npcIdentifier.IsValid && _individuals.TryGetValue( npcIdentifier, out collection ) )
{ {
return true; return true;
@ -45,7 +45,7 @@ public sealed partial class IndividualCollections : IReadOnlyList< (string Displ
// Handle Ownership. // Handle Ownership.
if( Penumbra.Config.UseOwnerNameForCharacterCollection ) if( Penumbra.Config.UseOwnerNameForCharacterCollection )
{ {
identifier = _manager.CreateIndividualUnchecked( IdentifierType.Player, identifier.PlayerName, identifier.HomeWorld, ObjectKind.None, uint.MaxValue ); identifier = _actorManager.CreateIndividualUnchecked( IdentifierType.Player, identifier.PlayerName, identifier.HomeWorld, ObjectKind.None, uint.MaxValue );
return CheckWorlds( identifier, out collection ); return CheckWorlds( identifier, out collection );
} }
@ -59,12 +59,12 @@ public sealed partial class IndividualCollections : IReadOnlyList< (string Displ
case SpecialActor.FittingRoom when Penumbra.Config.UseCharacterCollectionInTryOn: case SpecialActor.FittingRoom when Penumbra.Config.UseCharacterCollectionInTryOn:
case SpecialActor.DyePreview when Penumbra.Config.UseCharacterCollectionInTryOn: case SpecialActor.DyePreview when Penumbra.Config.UseCharacterCollectionInTryOn:
case SpecialActor.Portrait when Penumbra.Config.UseCharacterCollectionsInCards: case SpecialActor.Portrait when Penumbra.Config.UseCharacterCollectionsInCards:
return CheckWorlds( _manager.GetCurrentPlayer(), out collection ); return CheckWorlds( _actorManager.GetCurrentPlayer(), out collection );
case SpecialActor.ExamineScreen: case SpecialActor.ExamineScreen:
{ {
return CheckWorlds( _manager.GetInspectPlayer(), out collection! ) return CheckWorlds( _actorManager.GetInspectPlayer(), out collection! )
|| CheckWorlds( _manager.GetCardPlayer(), out collection! ) || CheckWorlds( _actorManager.GetCardPlayer(), out collection! )
|| CheckWorlds( _manager.GetGlamourPlayer(), out collection! ); || CheckWorlds( _actorManager.GetGlamourPlayer(), out collection! );
} }
} }
@ -76,10 +76,10 @@ public sealed partial class IndividualCollections : IReadOnlyList< (string Displ
} }
public bool TryGetCollection( GameObject? gameObject, out ModCollection? collection ) public bool TryGetCollection( GameObject? gameObject, out ModCollection? collection )
=> TryGetCollection( _manager.FromObject( gameObject ), out collection ); => TryGetCollection( _actorManager.FromObject( gameObject ), out collection );
public unsafe bool TryGetCollection( FFXIVClientStructs.FFXIV.Client.Game.Object.GameObject* gameObject, out ModCollection? collection ) public unsafe bool TryGetCollection( FFXIVClientStructs.FFXIV.Client.Game.Object.GameObject* gameObject, out ModCollection? collection )
=> TryGetCollection( _manager.FromObject( gameObject ), out collection ); => TryGetCollection( _actorManager.FromObject( gameObject ), out collection );
private bool CheckWorlds( ActorIdentifier identifier, out ModCollection? collection ) private bool CheckWorlds( ActorIdentifier identifier, out ModCollection? collection )
{ {
@ -94,7 +94,7 @@ public sealed partial class IndividualCollections : IReadOnlyList< (string Displ
return true; return true;
} }
identifier = _manager.CreateIndividualUnchecked( identifier.Type, identifier.PlayerName, ushort.MaxValue, identifier.Kind, identifier.DataId ); identifier = _actorManager.CreateIndividualUnchecked( identifier.Type, identifier.PlayerName, ushort.MaxValue, identifier.Kind, identifier.DataId );
if( identifier.IsValid && _individuals.TryGetValue( identifier, out collection ) ) if( identifier.IsValid && _individuals.TryGetValue( identifier, out collection ) )
{ {
return true; return true;

View file

@ -3,6 +3,7 @@ using System.Collections.Generic;
using System.Linq; using System.Linq;
using Dalamud.Game.ClientState.Objects.Enums; using Dalamud.Game.ClientState.Objects.Enums;
using Dalamud.Interface.Internal.Notifications; using Dalamud.Interface.Internal.Notifications;
using Newtonsoft.Json.Linq;
using Penumbra.GameData.Actors; using Penumbra.GameData.Actors;
using Penumbra.String; using Penumbra.String;
using Penumbra.Util; using Penumbra.Util;
@ -11,7 +12,66 @@ namespace Penumbra.Collections;
public partial class IndividualCollections public partial class IndividualCollections
{ {
public const int Version = 1; public JArray ToJObject()
{
var ret = new JArray();
foreach( var (name, identifiers, collection) in Assignments )
{
var tmp = identifiers[0].ToJson();
tmp.Add( "Collection", collection.Name );
tmp.Add( "Display", name );
ret.Add( tmp );
}
return ret;
}
public bool ReadJObject( JArray? obj, ModCollection.Manager manager )
{
if( obj == null )
{
return true;
}
var changes = false;
foreach( var data in obj )
{
try
{
var identifier = Penumbra.Actors.FromJson( data as JObject );
var group = GetGroup( identifier );
if( group.Length == 0 || group.Any( i => !i.IsValid ) )
{
changes = true;
ChatUtil.NotificationMessage( "Could not load an unknown individual collection, removed.", "Load Failure", NotificationType.Warning );
continue;
}
var collectionName = data[ "Collection" ]?.ToObject< string >() ?? string.Empty;
if( collectionName.Length == 0 || !manager.ByName( collectionName, out var collection ) )
{
changes = true;
ChatUtil.NotificationMessage( $"Could not load the collection \"{collectionName}\" as individual collection for {identifier}, set to None.", "Load Failure",
NotificationType.Warning );
continue;
}
if( !Add( group, collection ) )
{
changes = true;
ChatUtil.NotificationMessage( $"Could not add an individual collection for {identifier}, removed.", "Load Failure",
NotificationType.Warning );
}
}
catch( Exception e )
{
changes = true;
ChatUtil.NotificationMessage( $"Could not load an unknown individual collection, removed:\n{e}", "Load Failure", NotificationType.Error );
}
}
return changes;
}
internal void Migrate0To1( Dictionary< string, ModCollection > old ) internal void Migrate0To1( Dictionary< string, ModCollection > old )
{ {
@ -28,30 +88,30 @@ public partial class IndividualCollections
var kind = ObjectKind.None; var kind = ObjectKind.None;
var lowerName = name.ToLowerInvariant(); var lowerName = name.ToLowerInvariant();
// Prefer matching NPC names, fewer false positives than preferring players. // Prefer matching NPC names, fewer false positives than preferring players.
if( FindDataId( lowerName, _manager.Companions, out var dataId ) ) if( FindDataId( lowerName, _actorManager.Companions, out var dataId ) )
{ {
kind = ObjectKind.Companion; kind = ObjectKind.Companion;
} }
else if( FindDataId( lowerName, _manager.Mounts, out dataId ) ) else if( FindDataId( lowerName, _actorManager.Mounts, out dataId ) )
{ {
kind = ObjectKind.MountType; kind = ObjectKind.MountType;
} }
else if( FindDataId( lowerName, _manager.BNpcs, out dataId ) ) else if( FindDataId( lowerName, _actorManager.BNpcs, out dataId ) )
{ {
kind = ObjectKind.BattleNpc; kind = ObjectKind.BattleNpc;
} }
else if( FindDataId( lowerName, _manager.ENpcs, out dataId ) ) else if( FindDataId( lowerName, _actorManager.ENpcs, out dataId ) )
{ {
kind = ObjectKind.EventNpc; kind = ObjectKind.EventNpc;
} }
var identifier = _manager.CreateNpc( kind, dataId ); var identifier = _actorManager.CreateNpc( kind, dataId );
if( identifier.IsValid ) if( identifier.IsValid )
{ {
// If the name corresponds to a valid npc, add it as a group. If this fails, notify users. // If the name corresponds to a valid npc, add it as a group. If this fails, notify users.
var group = GetGroup( identifier ); var group = GetGroup( identifier );
var ids = string.Join( ", ", group.Select( i => i.DataId.ToString() ) ); var ids = string.Join( ", ", group.Select( i => i.DataId.ToString() ) );
if( Add( $"{_manager.ToName( kind, dataId )} ({kind.ToName()})", group, collection ) ) if( Add( $"{_actorManager.ToName( kind, dataId )} ({kind.ToName()})", group, collection ) )
{ {
Penumbra.Log.Information( $"Migrated {name} ({kind.ToName()}) to NPC Identifiers [{ids}]." ); Penumbra.Log.Information( $"Migrated {name} ({kind.ToName()}) to NPC Identifiers [{ids}]." );
} }
@ -65,10 +125,10 @@ public partial class IndividualCollections
// If it is not a valid NPC name, check if it can be a player name. // If it is not a valid NPC name, check if it can be a player name.
else if( ActorManager.VerifyPlayerName( name ) ) else if( ActorManager.VerifyPlayerName( name ) )
{ {
identifier = _manager.CreatePlayer( ByteString.FromStringUnsafe( name, false ), ushort.MaxValue ); identifier = _actorManager.CreatePlayer( ByteString.FromStringUnsafe( name, false ), ushort.MaxValue );
var shortName = string.Join( " ", name.Split().Select( n => $"{n[ 0 ]}." ) ); var shortName = string.Join( " ", name.Split().Select( n => $"{n[ 0 ]}." ) );
// Try to migrate the player name without logging full names. // Try to migrate the player name without logging full names.
if( Add( $"{name} ({_manager.ToWorldName( identifier.HomeWorld )})", new[] { identifier }, collection ) ) if( Add( $"{name} ({_actorManager.ToWorldName( identifier.HomeWorld )})", new[] { identifier }, collection ) )
{ {
Penumbra.Log.Information( $"Migrated {shortName} ({collection.AnonymizedName}) to Player Identifier." ); Penumbra.Log.Information( $"Migrated {shortName} ({collection.AnonymizedName}) to Player Identifier." );
} }

View file

@ -2,6 +2,7 @@ using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using Dalamud.Game.ClientState.Objects.Enums; using Dalamud.Game.ClientState.Objects.Enums;
using OtterGui.Filesystem;
using Penumbra.GameData.Actors; using Penumbra.GameData.Actors;
using Penumbra.String; using Penumbra.String;
@ -9,18 +10,18 @@ namespace Penumbra.Collections;
public sealed partial class IndividualCollections public sealed partial class IndividualCollections
{ {
private readonly ActorManager _manager; private readonly ActorManager _actorManager;
private readonly SortedList< string, (IReadOnlyList< ActorIdentifier > Identifiers, ModCollection Collection) > _assignments = new(); private readonly List< (string DisplayName, IReadOnlyList< ActorIdentifier > Identifiers, ModCollection Collection) > _assignments = new();
private readonly Dictionary< ActorIdentifier, ModCollection > _individuals = new(); private readonly Dictionary< ActorIdentifier, ModCollection > _individuals = new();
public IReadOnlyDictionary< string, (IReadOnlyList< ActorIdentifier > Identifiers, ModCollection Collection) > Assignments public IReadOnlyList< (string DisplayName, IReadOnlyList< ActorIdentifier > Identifiers, ModCollection Collection) > Assignments
=> _assignments; => _assignments;
public IReadOnlyDictionary< ActorIdentifier, ModCollection > Individuals public IReadOnlyDictionary< ActorIdentifier, ModCollection > Individuals
=> _individuals; => _individuals;
public IndividualCollections( ActorManager manager ) public IndividualCollections( ActorManager actorManager )
=> _manager = manager; => _actorManager = actorManager;
public enum AddResult public enum AddResult
{ {
@ -61,7 +62,7 @@ public sealed partial class IndividualCollections
return AddResult.Invalid; return AddResult.Invalid;
} }
var identifier = _manager.CreatePlayer( playerName, homeWorld ); var identifier = _actorManager.CreatePlayer( playerName, homeWorld );
identifiers = new[] { identifier }; identifiers = new[] { identifier };
break; break;
case IdentifierType.Owned: case IdentifierType.Owned:
@ -70,10 +71,10 @@ public sealed partial class IndividualCollections
return AddResult.Invalid; return AddResult.Invalid;
} }
identifiers = dataIds.Select( id => _manager.CreateOwned( ownerName, homeWorld, kind, id ) ).ToArray(); identifiers = dataIds.Select( id => _actorManager.CreateOwned( ownerName, homeWorld, kind, id ) ).ToArray();
break; break;
case IdentifierType.Npc: case IdentifierType.Npc:
identifiers = dataIds.Select( id => _manager.CreateIndividual( IdentifierType.Npc, ByteString.Empty, ushort.MaxValue, kind, id ) ).ToArray(); identifiers = dataIds.Select( id => _actorManager.CreateIndividual( IdentifierType.Npc, ByteString.Empty, ushort.MaxValue, kind, id ) ).ToArray();
break; break;
default: default:
identifiers = Array.Empty< ActorIdentifier >(); identifiers = Array.Empty< ActorIdentifier >();
@ -109,38 +110,33 @@ public sealed partial class IndividualCollections
{ {
IdentifierType.Player => new[] { identifier.CreatePermanent() }, IdentifierType.Player => new[] { identifier.CreatePermanent() },
IdentifierType.Special => new[] { identifier }, IdentifierType.Special => new[] { identifier },
IdentifierType.Owned => CreateNpcs( _manager, identifier.CreatePermanent() ), IdentifierType.Owned => CreateNpcs( _actorManager, identifier.CreatePermanent() ),
IdentifierType.Npc => CreateNpcs( _manager, identifier ), IdentifierType.Npc => CreateNpcs( _actorManager, identifier ),
_ => Array.Empty< ActorIdentifier >(), _ => Array.Empty< ActorIdentifier >(),
}; };
} }
public bool Add( ActorIdentifier[] identifiers, ModCollection collection ) internal bool Add( ActorIdentifier[] identifiers, ModCollection collection )
{ {
if( identifiers.Length == 0 || !identifiers[ 0 ].IsValid ) if( identifiers.Length == 0 || !identifiers[ 0 ].IsValid )
{ {
return false; return false;
} }
var name = identifiers[ 0 ].Type switch var name = DisplayString( identifiers[ 0 ] );
{
IdentifierType.Player => $"{identifiers[ 0 ].PlayerName} ({_manager.ToWorldName( identifiers[ 0 ].HomeWorld )})",
IdentifierType.Owned =>
$"{identifiers[ 0 ].PlayerName} ({_manager.ToWorldName( identifiers[ 0 ].HomeWorld )})'s {_manager.ToName( identifiers[ 0 ].Kind, identifiers[ 0 ].DataId )}",
IdentifierType.Npc => $"{_manager.ToName( identifiers[ 0 ].Kind, identifiers[ 0 ].DataId )} ({identifiers[ 0 ].Kind})",
_ => string.Empty,
};
return Add( name, identifiers, collection ); return Add( name, identifiers, collection );
} }
private bool Add( string displayName, ActorIdentifier[] identifiers, ModCollection collection ) private bool Add( string displayName, ActorIdentifier[] identifiers, ModCollection collection )
{ {
if( CanAdd( identifiers ) != AddResult.Valid || displayName.Length == 0 || _assignments.ContainsKey( displayName ) ) if( CanAdd( identifiers ) != AddResult.Valid
|| displayName.Length == 0
|| _assignments.Any( a => a.DisplayName.Equals( displayName, StringComparison.OrdinalIgnoreCase ) ) )
{ {
return false; return false;
} }
_assignments.Add( displayName, ( identifiers, collection ) ); _assignments.Add( ( displayName, identifiers, collection ) );
foreach( var identifier in identifiers ) foreach( var identifier in identifiers )
{ {
_individuals.Add( identifier, collection ); _individuals.Add( identifier, collection );
@ -149,21 +145,21 @@ public sealed partial class IndividualCollections
return true; return true;
} }
public bool ChangeCollection( string displayName, ModCollection newCollection ) internal bool ChangeCollection( ActorIdentifier identifier, ModCollection newCollection )
{ => ChangeCollection( DisplayString( identifier ), newCollection );
var displayIndex = _assignments.IndexOfKey( displayName );
return ChangeCollection( displayIndex, newCollection );
}
public bool ChangeCollection( int displayIndex, ModCollection newCollection ) internal bool ChangeCollection( string displayName, ModCollection newCollection )
=> ChangeCollection( _assignments.FindIndex( t => t.DisplayName.Equals( displayName, StringComparison.OrdinalIgnoreCase ) ), newCollection );
internal bool ChangeCollection( int displayIndex, ModCollection newCollection )
{ {
if( displayIndex < 0 || displayIndex >= _assignments.Count || _assignments.Values[ displayIndex ].Collection == newCollection ) if( displayIndex < 0 || displayIndex >= _assignments.Count || _assignments[ displayIndex ].Collection == newCollection )
{ {
return false; return false;
} }
_assignments.Values[ displayIndex ] = _assignments.Values[ displayIndex ] with { Collection = newCollection }; _assignments[ displayIndex ] = _assignments[ displayIndex ] with { Collection = newCollection };
foreach( var identifier in _assignments.Values[ displayIndex ].Identifiers ) foreach( var identifier in _assignments[ displayIndex ].Identifiers )
{ {
_individuals[ identifier ] = newCollection; _individuals[ identifier ] = newCollection;
} }
@ -171,20 +167,20 @@ public sealed partial class IndividualCollections
return true; return true;
} }
public bool Delete( string displayName ) internal bool Delete( ActorIdentifier identifier )
{ => Delete( DisplayString( identifier ) );
var displayIndex = _assignments.IndexOfKey( displayName );
return Delete( displayIndex );
}
public bool Delete( int displayIndex ) internal bool Delete( string displayName )
=> Delete( _assignments.FindIndex( t => t.DisplayName.Equals( displayName, StringComparison.OrdinalIgnoreCase ) ) );
internal bool Delete( int displayIndex )
{ {
if( displayIndex < 0 || displayIndex >= _assignments.Count ) if( displayIndex < 0 || displayIndex >= _assignments.Count )
{ {
return false; return false;
} }
var (identifiers, _) = _assignments.Values[ displayIndex ]; var (name, identifiers, _) = _assignments[ displayIndex ];
_assignments.RemoveAt( displayIndex ); _assignments.RemoveAt( displayIndex );
foreach( var identifier in identifiers ) foreach( var identifier in identifiers )
{ {
@ -193,4 +189,19 @@ public sealed partial class IndividualCollections
return true; return true;
} }
internal bool Move( int from, int to )
=> _assignments.Move( from, to );
private string DisplayString( ActorIdentifier identifier )
{
return identifier.Type switch
{
IdentifierType.Player => $"{identifier.PlayerName} ({_actorManager.ToWorldName( identifier.HomeWorld )})",
IdentifierType.Owned =>
$"{identifier.PlayerName} ({_actorManager.ToWorldName( identifier.HomeWorld )})'s {_actorManager.ToName( identifier.Kind, identifier.DataId )}",
IdentifierType.Npc => $"{_actorManager.ToName( identifier.Kind, identifier.DataId )} ({identifier.Kind.ToName()})",
_ => string.Empty,
};
}
} }

View file

@ -224,10 +224,52 @@ public partial class Configuration
CurrentCollection = _data[ nameof( CurrentCollection ) ]?.ToObject< string >() ?? CurrentCollection; CurrentCollection = _data[ nameof( CurrentCollection ) ]?.ToObject< string >() ?? CurrentCollection;
DefaultCollection = _data[ nameof( DefaultCollection ) ]?.ToObject< string >() ?? DefaultCollection; DefaultCollection = _data[ nameof( DefaultCollection ) ]?.ToObject< string >() ?? DefaultCollection;
CharacterCollections = _data[ nameof( CharacterCollections ) ]?.ToObject< Dictionary< string, string > >() ?? CharacterCollections; CharacterCollections = _data[ nameof( CharacterCollections ) ]?.ToObject< Dictionary< string, string > >() ?? CharacterCollections;
ModCollection.Manager.SaveActiveCollections( DefaultCollection, CurrentCollection, DefaultCollection, SaveActiveCollectionsV0( DefaultCollection, CurrentCollection, DefaultCollection,
CharacterCollections.Select( kvp => ( kvp.Key, kvp.Value ) ), Array.Empty< (CollectionType, string) >() ); 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. // Collections were introduced and the previous CurrentCollection got put into ModDirectory.
private void Version0To1() private void Version0To1()
{ {

View file

@ -8,6 +8,7 @@ using FFXIVClientStructs.FFXIV.Client.Game.Object;
using Penumbra.Api; using Penumbra.Api;
using FFXIVClientStructs.FFXIV.Client.Graphics.Scene; using FFXIVClientStructs.FFXIV.Client.Graphics.Scene;
using OtterGui.Classes; using OtterGui.Classes;
using Penumbra.GameData.Actors;
using Penumbra.GameData.Enums; using Penumbra.GameData.Enums;
using Penumbra.String.Classes; using Penumbra.String.Classes;
@ -221,9 +222,9 @@ public unsafe partial class PathResolver
} }
// 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? name ) private void CheckCollections( CollectionType type, ModCollection? _1, ModCollection? _2, string _3 )
{ {
if( type is CollectionType.Inactive or CollectionType.Current ) if( type is CollectionType.Inactive or CollectionType.Current or CollectionType.Interface )
{ {
return; return;
} }

View file

@ -355,26 +355,27 @@ public class Penumbra : IDalamudPlugin
return false; return false;
} }
var oldCollection = CollectionManager.ByType( type, characterName ); // TODO
if( collection == oldCollection ) //var oldCollection = CollectionManager.ByType( type, characterName );
{ //if( collection == oldCollection )
Dalamud.Chat.Print( $"{collection.Name} already is the {type.ToName()} Collection." ); //{
return false; // Dalamud.Chat.Print( $"{collection.Name} already is the {type.ToName()} Collection." );
} // return false;
//}
if( oldCollection == null ) //
{ //if( oldCollection == null )
if( type.IsSpecial() ) //{
{ // if( type.IsSpecial() )
CollectionManager.CreateSpecialCollection( type ); // {
} // CollectionManager.CreateSpecialCollection( type );
else if( type is CollectionType.Individual ) // }
{ // else if( type is CollectionType.Individual )
CollectionManager.CreateCharacterCollection( characterName! ); // {
} // CollectionManager.CreateIndividualCollection( characterName! );
} // }
//}
CollectionManager.SetCollection( collection, type, characterName ); //
//CollectionManager.SetCollection( collection, type, characterName );
Dalamud.Chat.Print( $"Set {collection.Name} as {type.ToName()} Collection{( characterName != null ? $" for {characterName}." : "." )}" ); Dalamud.Chat.Print( $"Set {collection.Name} as {type.ToName()} Collection{( characterName != null ? $" for {characterName}." : "." )}" );
return true; return true;
} }
@ -507,8 +508,15 @@ public class Penumbra : IDalamudPlugin
ModManager.Sum( m => m.TotalManipulations ) ); ModManager.Sum( m => m.TotalManipulations ) );
sb.AppendFormat( "> **`IMC Exceptions Thrown: `** {0}\n", ImcExceptions ); sb.AppendFormat( "> **`IMC Exceptions Thrown: `** {0}\n", ImcExceptions );
string CharacterName( string name ) string CharacterName( ActorIdentifier id, string name )
=> string.Join( " ", name.Split().Select( n => $"{n[ 0 ]}." ) ) + ':'; {
if( id.Type is IdentifierType.Player or IdentifierType.Owned )
{
return string.Join( " ", name.Split( ' ', 3 ).Select( n => $"{n[ 0 ]}." ) ) + ':';
}
return name + ':';
}
void PrintCollection( ModCollection c ) void PrintCollection( ModCollection c )
=> sb.AppendFormat( "**Collection {0}**\n" => sb.AppendFormat( "**Collection {0}**\n"
@ -535,9 +543,9 @@ public class Penumbra : IDalamudPlugin
} }
} }
foreach( var (name, collection) in CollectionManager.Characters ) foreach( var (name, id, collection) in CollectionManager.Individuals.Assignments )
{ {
sb.AppendFormat( "> **`{1,-29}`** {0}\n", collection.AnonymizedName, CharacterName( name ) ); sb.AppendFormat( "> **`{1,-29}`** {0}\n", collection.AnonymizedName, CharacterName( id[ 0 ], name ) );
} }
foreach( var collection in CollectionManager.Where( c => c.HasCache ) ) foreach( var collection in CollectionManager.Where( c => c.HasCache ) )

View file

@ -14,6 +14,7 @@ using System.IO;
using System.Linq; using System.Linq;
using System.Numerics; using System.Numerics;
using Penumbra.Api.Enums; using Penumbra.Api.Enums;
using Penumbra.GameData.Actors;
namespace Penumbra.UI.Classes; namespace Penumbra.UI.Classes;
@ -47,7 +48,7 @@ public sealed partial class ModFileSystemSelector : FileSystemSelector< Mod, Mod
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, null ); OnCollectionChange( CollectionType.Current, null, Penumbra.CollectionManager.Current, "" );
} }
public override void Dispose() public override void Dispose()
@ -377,7 +378,7 @@ public sealed partial class ModFileSystemSelector : FileSystemSelector< Mod, Mod
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 )
{ {

View file

@ -11,7 +11,6 @@ using Dalamud.Game.ClientState.Objects.Enums;
using Dalamud.Interface.Components; using Dalamud.Interface.Components;
using OtterGui.Widgets; using OtterGui.Widgets;
using Penumbra.GameData.Actors; using Penumbra.GameData.Actors;
using Lumina.Data.Parsing;
namespace Penumbra.UI; namespace Penumbra.UI;
@ -110,6 +109,7 @@ public partial class ConfigWindow
return ret; return ret;
} }
private int _individualDragDropIdx = -1;
private void DrawIndividualAssignments() private void DrawIndividualAssignments()
{ {
@ -124,19 +124,42 @@ public partial class ConfigWindow
ImGui.Separator(); ImGui.Separator();
for( var i = 0; i < Penumbra.CollectionManager.Individuals.Count; ++i ) for( var i = 0; i < Penumbra.CollectionManager.Individuals.Count; ++i )
{ {
var (name, collection) = Penumbra.CollectionManager.Individuals[ i ]; var (name, _) = Penumbra.CollectionManager.Individuals[ i ];
using var id = ImRaii.PushId( i ); using var id = ImRaii.PushId( i );
DrawCollectionSelector( string.Empty, _window._inputTextWidth.X, CollectionType.Individual, true, name ); CollectionsWithEmpty.Draw( string.Empty, _window._inputTextWidth.X, i );
ImGui.SameLine(); ImGui.SameLine();
if( ImGuiUtil.DrawDisabledButton( FontAwesomeIcon.Trash.ToIconString(), _window._iconButtonSize, string.Empty, if( ImGuiUtil.DrawDisabledButton( FontAwesomeIcon.Trash.ToIconString(), _window._iconButtonSize, string.Empty,
false, true ) ) false, true ) )
{ {
Penumbra.CollectionManager.Individuals.Delete( i ); Penumbra.CollectionManager.RemoveIndividualCollection( i );
} }
ImGui.SameLine(); ImGui.SameLine();
ImGui.AlignTextToFramePadding(); ImGui.AlignTextToFramePadding();
ImGui.TextUnformatted( name ); ImGui.Selectable( name );
using( var source = ImRaii.DragDropSource() )
{
if( source )
{
ImGui.SetDragDropPayload( "Individual", IntPtr.Zero, 0 );
_individualDragDropIdx = i;
}
}
using( var target = ImRaii.DragDropTarget() )
{
if( !target.Success || !ImGuiUtil.IsDropping( "Individual" ) )
{
continue;
}
if( _individualDragDropIdx >= 0 )
{
Penumbra.CollectionManager.MoveIndividualCollection( _individualDragDropIdx, i );
}
_individualDragDropIdx = -1;
}
} }
ImGui.Dummy( Vector2.Zero ); ImGui.Dummy( Vector2.Zero );

View file

@ -1,5 +1,3 @@
using System;
using System.Linq;
using System.Numerics; using System.Numerics;
using Dalamud.Interface; using Dalamud.Interface;
using Dalamud.Interface.Components; using Dalamud.Interface.Components;
@ -41,7 +39,7 @@ public partial class ConfigWindow
// Input text fields. // Input text fields.
private string _newCollectionName = string.Empty; private string _newCollectionName = string.Empty;
private bool _canAddCollection = false; private bool _canAddCollection;
// Create a new collection that is either empty or a duplicate of the current collection. // Create a new collection that is either empty or a duplicate of the current collection.
// Resets the new collection name. // Resets the new collection name.
@ -103,7 +101,7 @@ public partial class ConfigWindow
private void DrawCurrentCollectionSelector( Vector2 width ) private void DrawCurrentCollectionSelector( Vector2 width )
{ {
using var group = ImRaii.Group(); using var group = ImRaii.Group();
DrawCollectionSelector( "##current", _window._inputTextWidth.X, CollectionType.Current, false, null ); DrawCollectionSelector( "##current", _window._inputTextWidth.X, CollectionType.Current, false );
ImGui.SameLine(); ImGui.SameLine();
ImGuiUtil.LabeledHelpMarker( SelectedCollection, ImGuiUtil.LabeledHelpMarker( SelectedCollection,
"This collection will be modified when using the Installed Mods tab and making changes.\nIt is not automatically assigned to anything." ); "This collection will be modified when using the Installed Mods tab and making changes.\nIt is not automatically assigned to anything." );
@ -126,7 +124,7 @@ public partial class ConfigWindow
private void DrawDefaultCollectionSelector() private void DrawDefaultCollectionSelector()
{ {
using var group = ImRaii.Group(); using var group = ImRaii.Group();
DrawCollectionSelector( "##default", _window._inputTextWidth.X, CollectionType.Default, true, null ); DrawCollectionSelector( "##default", _window._inputTextWidth.X, CollectionType.Default, true );
ImGui.SameLine(); ImGui.SameLine();
ImGuiUtil.LabeledHelpMarker( DefaultCollection, ImGuiUtil.LabeledHelpMarker( DefaultCollection,
$"Mods in the {DefaultCollection} are loaded for anything that is not associated with the user interface or a character in the game," $"Mods in the {DefaultCollection} are loaded for anything that is not associated with the user interface or a character in the game,"
@ -136,7 +134,7 @@ public partial class ConfigWindow
private void DrawInterfaceCollectionSelector() private void DrawInterfaceCollectionSelector()
{ {
using var group = ImRaii.Group(); using var group = ImRaii.Group();
DrawCollectionSelector( "##interface", _window._inputTextWidth.X, CollectionType.Interface, true, null ); DrawCollectionSelector( "##interface", _window._inputTextWidth.X, CollectionType.Interface, true );
ImGui.SameLine(); ImGui.SameLine();
ImGuiUtil.LabeledHelpMarker( InterfaceCollection, ImGuiUtil.LabeledHelpMarker( InterfaceCollection,
$"Mods in the {InterfaceCollection} are loaded for any file that the game categorizes as an UI file. This is mostly icons as well as the tiles that generate the user interface windows themselves." ); $"Mods in the {InterfaceCollection} are loaded for any file that the game categorizes as an UI file. This is mostly icons as well as the tiles that generate the user interface windows themselves." );
@ -147,7 +145,7 @@ public partial class ConfigWindow
public (CollectionType, string, string)? CurrentType public (CollectionType, string, string)? CurrentType
=> CollectionTypeExtensions.Special[ CurrentIdx ]; => CollectionTypeExtensions.Special[ CurrentIdx ];
public int CurrentIdx = 0; public int CurrentIdx;
private readonly float _unscaledWidth; private readonly float _unscaledWidth;
private readonly string _label; private readonly string _label;
@ -219,7 +217,7 @@ public partial class ConfigWindow
if( collection != null ) if( collection != null )
{ {
using var id = ImRaii.PushId( ( int )type ); using var id = ImRaii.PushId( ( int )type );
DrawCollectionSelector( string.Empty, _window._inputTextWidth.X, type, true, null ); DrawCollectionSelector( string.Empty, _window._inputTextWidth.X, type, true );
ImGui.SameLine(); ImGui.SameLine();
if( ImGuiUtil.DrawDisabledButton( FontAwesomeIcon.Trash.ToIconString(), _window._iconButtonSize, string.Empty, if( ImGuiUtil.DrawDisabledButton( FontAwesomeIcon.Trash.ToIconString(), _window._iconButtonSize, string.Empty,
false, true ) ) false, true ) )

View file

@ -11,6 +11,7 @@ using OtterGui.Raii;
using OtterGui.Widgets; using OtterGui.Widgets;
using Penumbra.Api.Enums; using Penumbra.Api.Enums;
using Penumbra.Collections; using Penumbra.Collections;
using Penumbra.GameData.Actors;
using Penumbra.Interop.Structs; using Penumbra.Interop.Structs;
using Penumbra.String; using Penumbra.String;
using Penumbra.UI.Classes; using Penumbra.UI.Classes;
@ -98,12 +99,21 @@ public partial class ConfigWindow
: base( items ) : base( items )
{ } { }
public void Draw( string label, float width, CollectionType type, string? characterName ) public void Draw( string label, float width, int individualIdx )
{ {
var current = Penumbra.CollectionManager.ByType( type, characterName ); var (_, collection) = Penumbra.CollectionManager.Individuals[ individualIdx ];
if( Draw( label, collection.Name, width, ImGui.GetTextLineHeightWithSpacing() ) && CurrentSelection != null )
{
Penumbra.CollectionManager.SetCollection( CurrentSelection, CollectionType.Individual, individualIdx );
}
}
public void Draw( string label, float width, CollectionType type )
{
var current = Penumbra.CollectionManager.ByType( type, ActorIdentifier.Invalid );
if( Draw( label, current?.Name ?? string.Empty, width, ImGui.GetTextLineHeightWithSpacing() ) && CurrentSelection != null ) if( Draw( label, current?.Name ?? string.Empty, width, ImGui.GetTextLineHeightWithSpacing() ) && CurrentSelection != null )
{ {
Penumbra.CollectionManager.SetCollection( CurrentSelection, type, characterName ); Penumbra.CollectionManager.SetCollection( CurrentSelection, type );
} }
} }
@ -115,9 +125,8 @@ public partial class ConfigWindow
private static readonly CollectionSelector Collections = new(Penumbra.CollectionManager.OrderBy( c => c.Name )); private static readonly CollectionSelector Collections = new(Penumbra.CollectionManager.OrderBy( c => c.Name ));
// Draw a collection selector of a certain width for a certain type. // Draw a collection selector of a certain width for a certain type.
private static void DrawCollectionSelector( string label, float width, CollectionType collectionType, bool withEmpty, private static void DrawCollectionSelector( string label, float width, CollectionType collectionType, bool withEmpty )
string? characterName ) => ( withEmpty ? CollectionsWithEmpty : Collections ).Draw( label, width, collectionType );
=> ( withEmpty ? CollectionsWithEmpty : Collections ).Draw( label, width, collectionType, characterName );
// Set up the file selector with the right flags and custom side bar items. // Set up the file selector with the right flags and custom side bar items.
public static FileDialogManager SetupFileManager() public static FileDialogManager SetupFileManager()

View file

@ -136,7 +136,7 @@ public partial class ConfigWindow
ImGui.SameLine(); ImGui.SameLine();
DrawInheritedCollectionButton( 3 * buttonSize ); DrawInheritedCollectionButton( 3 * buttonSize );
ImGui.SameLine(); ImGui.SameLine();
DrawCollectionSelector( "##collectionSelector", 2 * buttonSize.X, CollectionType.Current, false, null ); DrawCollectionSelector( "##collectionSelector", 2 * buttonSize.X, CollectionType.Current, false );
} }
OpenTutorial( BasicTutorialSteps.CollectionSelectors ); OpenTutorial( BasicTutorialSteps.CollectionSelectors );