Merge branch 'xivdev:master' into master

This commit is contained in:
rootdarkarchon 2022-06-19 19:32:35 +02:00 committed by GitHub
commit 81b3a12341
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
44 changed files with 1384 additions and 381 deletions

View file

@ -75,10 +75,10 @@ jobs:
git config --global user.name "Actions User" git config --global user.name "Actions User"
git config --global user.email "actions@github.com" git config --global user.email "actions@github.com"
git fetch origin master && git fetch origin test && git branch -f test origin/master && git checkout master git fetch origin master && git fetch origin test && git checkout master
git add repo.json git add repo.json
git commit -m "[CI] Updating repo.json for ${{ github.ref }}" || true git commit -m "[CI] Updating repo.json for ${{ github.ref }}" || true
git push origin master || true git push origin master || true
git checkout test git branch -f test origin/master && git checkout test
git push origin test -f || true git push origin test -f || true

@ -1 +1 @@
Subproject commit d97a26923981db2a27d0172367c9e2841767f9b1 Subproject commit 6ce8ca816678e7a363f9f4a6f43f009f8d79c070

View file

@ -1,13 +1,9 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.ComponentModel.Design;
using Dalamud.Configuration; using Dalamud.Configuration;
using Dalamud.Game.ClientState.Objects.Types; using Dalamud.Game.ClientState.Objects.Types;
using Lumina.Data; using Lumina.Data;
using OtterGui.Classes;
using Penumbra.GameData.ByteString;
using Penumbra.GameData.Enums; using Penumbra.GameData.Enums;
using Penumbra.Meta.Manipulations;
using Penumbra.Mods; using Penumbra.Mods;
namespace Penumbra.Api; namespace Penumbra.Api;
@ -24,18 +20,20 @@ public delegate void GameObjectRedrawn( IntPtr objectPtr, int objectTableIndex )
public enum PenumbraApiEc public enum PenumbraApiEc
{ {
Okay = 0, Success = 0,
NothingChanged = 1, NothingChanged = 1,
CollectionMissing = 2, CollectionMissing = 2,
ModMissing = 3, ModMissing = 3,
OptionGroupMissing = 4, OptionGroupMissing = 4,
SettingMissing = 5, OptionMissing = 5,
CharacterCollectionExists = 6, CharacterCollectionExists = 6,
LowerPriority = 7, LowerPriority = 7,
InvalidGamePath = 8, InvalidGamePath = 8,
FileMissing = 9, FileMissing = 9,
InvalidManipulation = 10, InvalidManipulation = 10,
InvalidArgument = 11,
UnknownError = 255,
} }
public interface IPenumbraApi : IPenumbraApiBase public interface IPenumbraApi : IPenumbraApiBase
@ -75,7 +73,7 @@ public interface IPenumbraApi : IPenumbraApiBase
public string ResolvePath( string gamePath, string characterName ); public string ResolvePath( string gamePath, string characterName );
// Reverse resolves a given modded local path into its replacement in form of all applicable game path for given character // Reverse resolves a given modded local path into its replacement in form of all applicable game path for given character
public IList<string> ReverseResolvePath( string moddedPath, string characterName ); public IList< string > ReverseResolvePath( string moddedPath, string characterName );
// Try to load a given gamePath with the resolved path from Penumbra. // Try to load a given gamePath with the resolved path from Penumbra.
public T? GetFile< T >( string gamePath ) where T : FileResource; public T? GetFile< T >( string gamePath ) where T : FileResource;
@ -109,12 +107,12 @@ public interface IPenumbraApi : IPenumbraApiBase
// Obtain the potential settings of a mod specified by its directory name first or mod name second. // Obtain the potential settings of a mod specified by its directory name first or mod name second.
// Returns null if the mod could not be found. // Returns null if the mod could not be found.
public IDictionary< string, (IList<string>, SelectType) >? GetAvailableModSettings( string modDirectory, string modName ); public IDictionary< string, (IList< string >, SelectType) >? GetAvailableModSettings( string modDirectory, string modName );
// Obtain the enabled state, the priority, the settings of a mod specified by its directory name first or mod name second, // Obtain the enabled state, the priority, the settings of a mod specified by its directory name first or mod name second,
// and whether these settings are inherited, or null if the collection does not set them at all. // and whether these settings are inherited, or null if the collection does not set them at all.
// If allowInheritance is false, only the collection itself will be checked. // If allowInheritance is false, only the collection itself will be checked.
public (PenumbraApiEc, (bool, int, IDictionary< string, IList<string> >, bool)?) GetCurrentModSettings( string collectionName, public (PenumbraApiEc, (bool, int, IDictionary< string, IList< string > >, bool)?) GetCurrentModSettings( string collectionName,
string modDirectory, string modName, bool allowInheritance ); string modDirectory, string modName, bool allowInheritance );
// Try to set the inheritance state in the given collection of a mod specified by its directory name first or mod name second. // Try to set the inheritance state in the given collection of a mod specified by its directory name first or mod name second.
@ -135,32 +133,36 @@ public interface IPenumbraApi : IPenumbraApiBase
// If any setting can not be found, it will not change anything. // If any setting can not be found, it will not change anything.
public PenumbraApiEc TrySetModSetting( string collectionName, string modDirectory, string modName, string optionGroupName, string option ); public PenumbraApiEc TrySetModSetting( string collectionName, string modDirectory, string modName, string optionGroupName, string option );
public PenumbraApiEc TrySetModSetting( string collectionName, string modDirectory, string modName, string optionGroupName, public PenumbraApiEc TrySetModSettings( string collectionName, string modDirectory, string modName, string optionGroupName,
IReadOnlyList<string> options ); IReadOnlyList< string > options );
// Create a temporary collection without actual settings but with a cache. // Create a temporary collection without actual settings but with a cache.
// If character is non-zero and either no character collection for this character exists or forceOverwriteCharacter is true, // If no character collection for this character exists or forceOverwriteCharacter is true,
// associate this collection to a specific character. // associate this collection to a specific character.
// Can return Okay, CharacterCollectionExists or NothingChanged. // Can return Okay, CharacterCollectionExists or NothingChanged, as well as the name of the new temporary collection on success.
public PenumbraApiEc CreateTemporaryCollection( string collectionName, string? character, bool forceOverwriteCharacter ); public (PenumbraApiEc, string) CreateTemporaryCollection( string tag, string character, bool forceOverwriteCharacter );
// Remove a temporary collection if it exists. // Remove the temporary collection associated with characterName if it exists.
// Can return Okay or NothingChanged. // Can return Okay or NothingChanged.
public PenumbraApiEc RemoveTemporaryCollection( string collectionName ); public PenumbraApiEc RemoveTemporaryCollection( string characterName );
// Set a temporary mod with the given paths, manipulations and priority and the name tag to all collections.
// Can return Okay, InvalidGamePath, or InvalidManipulation.
public PenumbraApiEc AddTemporaryModAll( string tag, IReadOnlyDictionary< string, string > paths, IReadOnlySet< string > manipCodes,
int priority );
// Set or remove a specific file redirection or meta manipulation under the name of Tag and with a given priority // Set a temporary mod with the given paths, manipulations and priority and the name tag to the collection with the given name, which can be temporary.
// for a given collection, which may be temporary. // Can return Okay, MissingCollection InvalidGamePath, or InvalidManipulation.
// Can return Okay, CollectionMissing, InvalidPath, FileMissing, LowerPriority, or NothingChanged. public PenumbraApiEc AddTemporaryMod( string tag, string collectionName, IReadOnlyDictionary< string, string > paths,
public PenumbraApiEc SetFileRedirection( string tag, string collectionName, string gamePath, string fullPath, int priority ); IReadOnlySet< string > manipCodes,
int priority );
// Can return Okay, CollectionMissing, InvalidManipulation, LowerPriority, or NothingChanged. // Remove the temporary mod with the given tag and priority from the temporary mods applying to all collections, if it exists.
public PenumbraApiEc SetMetaManipulation( string tag, string collectionName, string manipulationBase64, int priority ); // Can return Okay or NothingDone.
public PenumbraApiEc RemoveTemporaryModAll( string tag, int priority );
// Can return Okay, CollectionMissing, InvalidPath, or NothingChanged. // Remove the temporary mod with the given tag and priority from the temporary mods applying to the collection of the given name, which can be temporary.
public PenumbraApiEc RemoveFileRedirection( string tag, string collectionName, string gamePath ); // Can return Okay or NothingDone.
public PenumbraApiEc RemoveTemporaryMod( string tag, string collectionName, int priority );
// Can return Okay, CollectionMissing, InvalidManipulation, or NothingChanged.
public PenumbraApiEc RemoveMetaManipulation( string tag, string collectionName, string manipulationBase64 );
} }

View file

@ -37,7 +37,6 @@ public class ModsController : WebApiController
return Penumbra.CollectionManager.Current.ResolvedFiles.ToDictionary( return Penumbra.CollectionManager.Current.ResolvedFiles.ToDictionary(
o => o.Key.ToString(), o => o.Key.ToString(),
o => o.Value.Path.FullName o => o.Value.Path.FullName
) );
?? new Dictionary< string, string >();
} }
} }

View file

@ -1,5 +1,6 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.IO; using System.IO;
using System.Linq; using System.Linq;
using System.Reflection; using System.Reflection;
@ -8,16 +9,21 @@ using Dalamud.Game.ClientState.Objects.Types;
using Dalamud.Logging; using Dalamud.Logging;
using Lumina.Data; using Lumina.Data;
using Newtonsoft.Json; using Newtonsoft.Json;
using OtterGui;
using Penumbra.Collections; using Penumbra.Collections;
using Penumbra.GameData.ByteString; using Penumbra.GameData.ByteString;
using Penumbra.GameData.Enums; using Penumbra.GameData.Enums;
using Penumbra.Meta.Manipulations;
using Penumbra.Mods; using Penumbra.Mods;
using Penumbra.Util;
namespace Penumbra.Api; namespace Penumbra.Api;
public class PenumbraApi : IDisposable, IPenumbraApi public class PenumbraApi : IDisposable, IPenumbraApi
{ {
public int ApiVersion { get; } = 4; public int ApiVersion
=> 4;
private Penumbra? _penumbra; private Penumbra? _penumbra;
private Lumina.GameData? _lumina; private Lumina.GameData? _lumina;
public event GameObjectRedrawn? GameObjectRedrawn; public event GameObjectRedrawn? GameObjectRedrawn;
@ -57,24 +63,6 @@ public class PenumbraApi : IDisposable, IPenumbraApi
public event ChangedItemHover? ChangedItemTooltip; public event ChangedItemHover? ChangedItemTooltip;
internal bool HasTooltip
=> ChangedItemTooltip != null;
internal void InvokeTooltip( object? it )
=> ChangedItemTooltip?.Invoke( it );
internal void InvokeClick( MouseButton button, object? it )
=> ChangedItemClicked?.Invoke( button, it );
private void CheckInitialized()
{
if( !Valid )
{
throw new Exception( "PluginShare is not initialized." );
}
}
public void RedrawObject( int tableIndex, RedrawType setting ) public void RedrawObject( int tableIndex, RedrawType setting )
{ {
CheckInitialized(); CheckInitialized();
@ -93,29 +81,12 @@ public class PenumbraApi : IDisposable, IPenumbraApi
_penumbra!.ObjectReloader.RedrawObject( gameObject, setting ); _penumbra!.ObjectReloader.RedrawObject( gameObject, setting );
} }
private void OnGameObjectRedrawn( IntPtr objectAddress, int objectTableIndex )
{
GameObjectRedrawn?.Invoke( objectAddress, objectTableIndex );
}
public void RedrawAll( RedrawType setting ) public void RedrawAll( RedrawType setting )
{ {
CheckInitialized(); CheckInitialized();
_penumbra!.ObjectReloader.RedrawAll( setting ); _penumbra!.ObjectReloader.RedrawAll( setting );
} }
private static string ResolvePath( string path, Mod.Manager _, ModCollection collection )
{
if( !Penumbra.Config.EnableMods )
{
return path;
}
var gamePath = Utf8GamePath.FromString( path, out var p, true ) ? p : Utf8GamePath.Empty;
var ret = collection.ResolvePath( gamePath );
return ret?.ToString() ?? path;
}
public string ResolvePath( string path ) public string ResolvePath( string path )
{ {
CheckInitialized(); CheckInitialized();
@ -141,25 +112,6 @@ public class PenumbraApi : IDisposable, IPenumbraApi
return ret.Select( r => r.ToString() ).ToList(); return ret.Select( r => r.ToString() ).ToList();
} }
private T? GetFileIntern< T >( string resolvedPath ) where T : FileResource
{
CheckInitialized();
try
{
if( Path.IsPathRooted( resolvedPath ) )
{
return _lumina?.GetFileFromDisk< T >( resolvedPath );
}
return Dalamud.GameData.GetFile< T >( resolvedPath );
}
catch( Exception e )
{
PluginLog.Warning( $"Could not load file {resolvedPath}:\n{e}" );
return null;
}
}
public T? GetFile< T >( string gamePath ) where T : FileResource public T? GetFile< T >( string gamePath ) where T : FileResource
=> GetFileIntern< T >( ResolvePath( gamePath ) ); => GetFileIntern< T >( ResolvePath( gamePath ) );
@ -231,44 +183,378 @@ public class PenumbraApi : IDisposable, IPenumbraApi
} }
public IDictionary< string, (IList< string >, SelectType) >? GetAvailableModSettings( string modDirectory, string modName ) public IDictionary< string, (IList< string >, SelectType) >? GetAvailableModSettings( string modDirectory, string modName )
=> throw new NotImplementedException(); {
CheckInitialized();
return Penumbra.ModManager.TryGetMod( modDirectory, modName, out var mod )
? mod.Groups.ToDictionary( g => g.Name, g => ( ( IList< string > )g.Select( o => o.Name ).ToList(), g.Type ) )
: null;
}
public (PenumbraApiEc, (bool, int, IDictionary< string, IList< string > >, bool)?) GetCurrentModSettings( string collectionName, public (PenumbraApiEc, (bool, int, IDictionary< string, IList< string > >, bool)?) GetCurrentModSettings( string collectionName,
string modDirectory, string modName, string modDirectory, string modName, bool allowInheritance )
bool allowInheritance ) {
=> throw new NotImplementedException(); CheckInitialized();
if( !Penumbra.CollectionManager.ByName( collectionName, out var collection ) )
{
return ( PenumbraApiEc.CollectionMissing, null );
}
if( !Penumbra.ModManager.TryGetMod( modDirectory, modName, out var mod ) )
{
return ( PenumbraApiEc.ModMissing, null );
}
var settings = allowInheritance ? collection.Settings[ mod.Index ] : collection[ mod.Index ].Settings;
if( settings == null )
{
return ( PenumbraApiEc.Success, null );
}
var shareSettings = settings.ConvertToShareable( mod );
return ( PenumbraApiEc.Success,
( shareSettings.Enabled, shareSettings.Priority, shareSettings.Settings, collection.Settings[ mod.Index ] != null ) );
}
public PenumbraApiEc TryInheritMod( string collectionName, string modDirectory, string modName, bool inherit ) public PenumbraApiEc TryInheritMod( string collectionName, string modDirectory, string modName, bool inherit )
=> throw new NotImplementedException(); {
CheckInitialized();
if( !Penumbra.CollectionManager.ByName( collectionName, out var collection ) )
{
return PenumbraApiEc.CollectionMissing;
}
if( !Penumbra.ModManager.TryGetMod( modDirectory, modName, out var mod ) )
{
return PenumbraApiEc.ModMissing;
}
return collection.SetModInheritance( mod.Index, inherit ) ? PenumbraApiEc.Success : PenumbraApiEc.NothingChanged;
}
public PenumbraApiEc TrySetMod( string collectionName, string modDirectory, string modName, bool enabled ) public PenumbraApiEc TrySetMod( string collectionName, string modDirectory, string modName, bool enabled )
=> throw new NotImplementedException(); {
CheckInitialized();
if( !Penumbra.CollectionManager.ByName( collectionName, out var collection ) )
{
return PenumbraApiEc.CollectionMissing;
}
if( !Penumbra.ModManager.TryGetMod( modDirectory, modName, out var mod ) )
{
return PenumbraApiEc.ModMissing;
}
return collection.SetModState( mod.Index, enabled ) ? PenumbraApiEc.Success : PenumbraApiEc.NothingChanged;
}
public PenumbraApiEc TrySetModPriority( string collectionName, string modDirectory, string modName, int priority ) public PenumbraApiEc TrySetModPriority( string collectionName, string modDirectory, string modName, int priority )
=> throw new NotImplementedException(); {
CheckInitialized();
if( !Penumbra.CollectionManager.ByName( collectionName, out var collection ) )
{
return PenumbraApiEc.CollectionMissing;
}
public PenumbraApiEc TrySetModSetting( string collectionName, string modDirectory, string modName, string optionGroupName, string option ) if( !Penumbra.ModManager.TryGetMod( modDirectory, modName, out var mod ) )
=> throw new NotImplementedException(); {
return PenumbraApiEc.ModMissing;
}
return collection.SetModPriority( mod.Index, priority ) ? PenumbraApiEc.Success : PenumbraApiEc.NothingChanged;
}
public PenumbraApiEc TrySetModSetting( string collectionName, string modDirectory, string modName, string optionGroupName, public PenumbraApiEc TrySetModSetting( string collectionName, string modDirectory, string modName, string optionGroupName,
IReadOnlyList< string > options ) string optionName )
=> throw new NotImplementedException(); {
CheckInitialized();
if( !Penumbra.CollectionManager.ByName( collectionName, out var collection ) )
{
return PenumbraApiEc.CollectionMissing;
}
public PenumbraApiEc CreateTemporaryCollection( string collectionName, string? character, bool forceOverwriteCharacter ) if( !Penumbra.ModManager.TryGetMod( modDirectory, modName, out var mod ) )
=> throw new NotImplementedException(); {
return PenumbraApiEc.ModMissing;
}
public PenumbraApiEc RemoveTemporaryCollection( string collectionName ) var groupIdx = mod.Groups.IndexOf( g => g.Name == optionGroupName );
=> throw new NotImplementedException(); if( groupIdx < 0 )
{
return PenumbraApiEc.OptionGroupMissing;
}
public PenumbraApiEc SetFileRedirection( string tag, string collectionName, string gamePath, string fullPath, int priority ) var optionIdx = mod.Groups[ groupIdx ].IndexOf( o => o.Name == optionName );
=> throw new NotImplementedException(); if( optionIdx < 0 )
{
return PenumbraApiEc.OptionMissing;
}
public PenumbraApiEc SetMetaManipulation( string tag, string collectionName, string manipulationBase64, int priority ) var setting = mod.Groups[ groupIdx ].Type == SelectType.Multi ? 1u << optionIdx : ( uint )optionIdx;
=> throw new NotImplementedException();
public PenumbraApiEc RemoveFileRedirection( string tag, string collectionName, string gamePath ) return collection.SetModSetting( mod.Index, groupIdx, setting ) ? PenumbraApiEc.Success : PenumbraApiEc.NothingChanged;
=> throw new NotImplementedException(); }
public PenumbraApiEc RemoveMetaManipulation( string tag, string collectionName, string manipulationBase64 ) public PenumbraApiEc TrySetModSettings( string collectionName, string modDirectory, string modName, string optionGroupName,
=> throw new NotImplementedException(); IReadOnlyList< string > optionNames )
{
CheckInitialized();
if( optionNames.Count == 0 )
{
return PenumbraApiEc.InvalidArgument;
}
if( !Penumbra.CollectionManager.ByName( collectionName, out var collection ) )
{
return PenumbraApiEc.CollectionMissing;
}
if( !Penumbra.ModManager.TryGetMod( modDirectory, modName, out var mod ) )
{
return PenumbraApiEc.ModMissing;
}
var groupIdx = mod.Groups.IndexOf( g => g.Name == optionGroupName );
if( groupIdx < 0 )
{
return PenumbraApiEc.OptionGroupMissing;
}
var group = mod.Groups[ groupIdx ];
uint setting = 0;
if( group.Type == SelectType.Single )
{
var name = optionNames[ ^1 ];
var optionIdx = group.IndexOf( o => o.Name == optionNames[ ^1 ] );
if( optionIdx < 0 )
{
return PenumbraApiEc.OptionMissing;
}
setting = ( uint )optionIdx;
}
else
{
foreach( var name in optionNames )
{
var optionIdx = group.IndexOf( o => o.Name == name );
if( optionIdx < 0 )
{
return PenumbraApiEc.OptionMissing;
}
setting |= 1u << optionIdx;
}
}
return collection.SetModSetting( mod.Index, groupIdx, setting ) ? PenumbraApiEc.Success : PenumbraApiEc.NothingChanged;
}
public (PenumbraApiEc, string) CreateTemporaryCollection( string tag, string character, bool forceOverwriteCharacter )
{
CheckInitialized();
if( !forceOverwriteCharacter && Penumbra.CollectionManager.Characters.ContainsKey( character )
|| Penumbra.TempMods.Collections.ContainsKey( character ) )
{
return ( PenumbraApiEc.CharacterCollectionExists, string.Empty );
}
var name = Penumbra.TempMods.SetTemporaryCollection( tag, character );
return ( PenumbraApiEc.Success, name );
}
public PenumbraApiEc RemoveTemporaryCollection( string character )
{
CheckInitialized();
if( !Penumbra.TempMods.Collections.ContainsKey( character ) )
{
return PenumbraApiEc.NothingChanged;
}
Penumbra.TempMods.RemoveTemporaryCollection( character );
return PenumbraApiEc.Success;
}
public PenumbraApiEc AddTemporaryModAll( string tag, IReadOnlyDictionary< string, string > paths, IReadOnlySet< string > manipCodes,
int priority )
{
CheckInitialized();
if( !ConvertPaths( paths, out var p ) )
{
return PenumbraApiEc.InvalidGamePath;
}
if( !ConvertManips( manipCodes, out var m ) )
{
return PenumbraApiEc.InvalidManipulation;
}
return Penumbra.TempMods.Register( tag, null, p, m, priority ) switch
{
RedirectResult.Success => PenumbraApiEc.Success,
_ => PenumbraApiEc.UnknownError,
};
}
public PenumbraApiEc AddTemporaryMod( string tag, string collectionName, IReadOnlyDictionary< string, string > paths,
IReadOnlySet< string > manipCodes, int priority )
{
CheckInitialized();
if( !Penumbra.TempMods.Collections.TryGetValue( collectionName, out var collection )
&& !Penumbra.CollectionManager.ByName( collectionName, out collection ) )
{
return PenumbraApiEc.CollectionMissing;
}
if( !ConvertPaths( paths, out var p ) )
{
return PenumbraApiEc.InvalidGamePath;
}
if( !ConvertManips( manipCodes, out var m ) )
{
return PenumbraApiEc.InvalidManipulation;
}
return Penumbra.TempMods.Register( tag, collection, p, m, priority ) switch
{
RedirectResult.Success => PenumbraApiEc.Success,
_ => PenumbraApiEc.UnknownError,
};
}
public PenumbraApiEc RemoveTemporaryModAll( string tag, int priority )
{
CheckInitialized();
return Penumbra.TempMods.Unregister( tag, null, priority ) switch
{
RedirectResult.Success => PenumbraApiEc.Success,
RedirectResult.NotRegistered => PenumbraApiEc.NothingChanged,
_ => PenumbraApiEc.UnknownError,
};
}
public PenumbraApiEc RemoveTemporaryMod( string tag, string collectionName, int priority )
{
CheckInitialized();
if( !Penumbra.TempMods.Collections.TryGetValue( collectionName, out var collection )
&& !Penumbra.CollectionManager.ByName( collectionName, out collection ) )
{
return PenumbraApiEc.CollectionMissing;
}
return Penumbra.TempMods.Unregister( tag, collection, priority ) switch
{
RedirectResult.Success => PenumbraApiEc.Success,
RedirectResult.NotRegistered => PenumbraApiEc.NothingChanged,
_ => PenumbraApiEc.UnknownError,
};
}
internal bool HasTooltip
=> ChangedItemTooltip != null;
internal void InvokeTooltip( object? it )
=> ChangedItemTooltip?.Invoke( it );
internal void InvokeClick( MouseButton button, object? it )
=> ChangedItemClicked?.Invoke( button, it );
private void CheckInitialized()
{
if( !Valid )
{
throw new Exception( "PluginShare is not initialized." );
}
}
private void OnGameObjectRedrawn( IntPtr objectAddress, int objectTableIndex )
{
GameObjectRedrawn?.Invoke( objectAddress, objectTableIndex );
}
// Resolve a path given by string for a specific collection.
private static string ResolvePath( string path, Mod.Manager _, ModCollection collection )
{
if( !Penumbra.Config.EnableMods )
{
return path;
}
var gamePath = Utf8GamePath.FromString( path, out var p, true ) ? p : Utf8GamePath.Empty;
var ret = collection.ResolvePath( gamePath );
return ret?.ToString() ?? path;
}
// Get a file for a resolved path.
private T? GetFileIntern< T >( string resolvedPath ) where T : FileResource
{
CheckInitialized();
try
{
if( Path.IsPathRooted( resolvedPath ) )
{
return _lumina?.GetFileFromDisk< T >( resolvedPath );
}
return Dalamud.GameData.GetFile< T >( resolvedPath );
}
catch( Exception e )
{
PluginLog.Warning( $"Could not load file {resolvedPath}:\n{e}" );
return null;
}
}
// Convert a dictionary of strings to a dictionary of gamepaths to full paths.
// Only returns true if all paths can successfully be converted and added.
private static bool ConvertPaths( IReadOnlyDictionary< string, string > redirections,
[NotNullWhen( true )] out Dictionary< Utf8GamePath, FullPath >? paths )
{
paths = new Dictionary< Utf8GamePath, FullPath >( redirections.Count );
foreach( var (gString, fString) in redirections )
{
if( !Utf8GamePath.FromString( gString, out var path, false ) )
{
paths = null;
return false;
}
var fullPath = new FullPath( fString );
if( !paths.TryAdd( path, fullPath ) )
{
paths = null;
return false;
}
}
return true;
}
// Convert manipulations from transmitted base64 strings to actual manipulations.
// Only returns true if all conversions are successful and distinct.
private static bool ConvertManips( IReadOnlyCollection< string > manipStrings,
[NotNullWhen( true )] out HashSet< MetaManipulation >? manips )
{
manips = new HashSet< MetaManipulation >( manipStrings.Count );
foreach( var m in manipStrings )
{
if( Functions.FromCompressedBase64< MetaManipulation >( m, out var manip ) != MetaManipulation.CurrentVersion )
{
manips = null;
return false;
}
if( !manips.Add( manip ) )
{
manips = null;
return false;
}
}
return true;
}
} }

View file

@ -22,6 +22,8 @@ public partial class PenumbraIpc : IDisposable
InitializeRedrawProviders( pi ); InitializeRedrawProviders( pi );
InitializeChangedItemProviders( pi ); InitializeChangedItemProviders( pi );
InitializeDataProviders( pi ); InitializeDataProviders( pi );
InitializeSettingProviders( pi );
InitializeTempProviders( pi );
ProviderInitialized?.SendMessage(); ProviderInitialized?.SendMessage();
} }
@ -32,6 +34,8 @@ public partial class PenumbraIpc : IDisposable
DisposeRedrawProviders(); DisposeRedrawProviders();
DisposeResolveProviders(); DisposeResolveProviders();
DisposeGeneralProviders(); DisposeGeneralProviders();
DisposeSettingProviders();
DisposeTempProviders();
ProviderDisposed?.SendMessage(); ProviderDisposed?.SendMessage();
} }
} }
@ -402,4 +406,217 @@ public partial class PenumbraIpc
ProviderDefaultCollectionName?.UnregisterFunc(); ProviderDefaultCollectionName?.UnregisterFunc();
ProviderCharacterCollectionName?.UnregisterFunc(); ProviderCharacterCollectionName?.UnregisterFunc();
} }
}
public partial class PenumbraIpc
{
public const string LabelProviderGetAvailableModSettings = "Penumbra.GetAvailableModSettings";
public const string LabelProviderGetCurrentModSettings = "Penumbra.GetCurrentModSettings";
public const string LabelProviderTryInheritMod = "Penumbra.TryInheritMod";
public const string LabelProviderTrySetMod = "Penumbra.TrySetMod";
public const string LabelProviderTrySetModPriority = "Penumbra.TrySetModPriority";
public const string LabelProviderTrySetModSetting = "Penumbra.TrySetModSetting";
public const string LabelProviderTrySetModSettings = "Penumbra.TrySetModSettings";
internal ICallGateProvider< string, string, IDictionary< string, (IList< string >, Mods.SelectType) >? >? ProviderGetAvailableModSettings;
internal ICallGateProvider< string, string, string, bool, (PenumbraApiEc, (bool, int, IDictionary< string, IList< string > >, bool)?) >?
ProviderGetCurrentModSettings;
internal ICallGateProvider< string, string, string, bool, PenumbraApiEc >? ProviderTryInheritMod;
internal ICallGateProvider< string, string, string, bool, PenumbraApiEc >? ProviderTrySetMod;
internal ICallGateProvider< string, string, string, int, PenumbraApiEc >? ProviderTrySetModPriority;
internal ICallGateProvider< string, string, string, string, string, PenumbraApiEc >? ProviderTrySetModSetting;
internal ICallGateProvider< string, string, string, string, IReadOnlyList< string >, PenumbraApiEc >? ProviderTrySetModSettings;
private void InitializeSettingProviders( DalamudPluginInterface pi )
{
try
{
ProviderGetAvailableModSettings =
pi.GetIpcProvider< string, string, IDictionary< string, (IList< string >, Mods.SelectType) >? >(
LabelProviderGetAvailableModSettings );
ProviderGetAvailableModSettings.RegisterFunc( Api.GetAvailableModSettings );
}
catch( Exception e )
{
PluginLog.Error( $"Error registering IPC provider for {LabelProviderGetAvailableModSettings}:\n{e}" );
}
try
{
ProviderGetCurrentModSettings =
pi.GetIpcProvider< string, string, string, bool, (PenumbraApiEc, (bool, int, IDictionary< string, IList< string > >, bool)?) >(
LabelProviderGetCurrentModSettings );
ProviderGetCurrentModSettings.RegisterFunc( Api.GetCurrentModSettings );
}
catch( Exception e )
{
PluginLog.Error( $"Error registering IPC provider for {LabelProviderGetCurrentModSettings}:\n{e}" );
}
try
{
ProviderTryInheritMod = pi.GetIpcProvider< string, string, string, bool, PenumbraApiEc >( LabelProviderTryInheritMod );
ProviderTryInheritMod.RegisterFunc( Api.TryInheritMod );
}
catch( Exception e )
{
PluginLog.Error( $"Error registering IPC provider for {LabelProviderTryInheritMod}:\n{e}" );
}
try
{
ProviderTrySetMod = pi.GetIpcProvider< string, string, string, bool, PenumbraApiEc >( LabelProviderTrySetMod );
ProviderTrySetMod.RegisterFunc( Api.TrySetMod );
}
catch( Exception e )
{
PluginLog.Error( $"Error registering IPC provider for {LabelProviderTrySetMod}:\n{e}" );
}
try
{
ProviderTrySetModPriority = pi.GetIpcProvider< string, string, string, int, PenumbraApiEc >( LabelProviderTrySetModPriority );
ProviderTrySetModPriority.RegisterFunc( Api.TrySetModPriority );
}
catch( Exception e )
{
PluginLog.Error( $"Error registering IPC provider for {LabelProviderTrySetModPriority}:\n{e}" );
}
try
{
ProviderTrySetModSetting =
pi.GetIpcProvider< string, string, string, string, string, PenumbraApiEc >( LabelProviderTrySetModSetting );
ProviderTrySetModSetting.RegisterFunc( Api.TrySetModSetting );
}
catch( Exception e )
{
PluginLog.Error( $"Error registering IPC provider for {LabelProviderTrySetModSetting}:\n{e}" );
}
try
{
ProviderTrySetModSettings =
pi.GetIpcProvider< string, string, string, string, IReadOnlyList< string >, PenumbraApiEc >( LabelProviderTrySetModSettings );
ProviderTrySetModSettings.RegisterFunc( Api.TrySetModSettings );
}
catch( Exception e )
{
PluginLog.Error( $"Error registering IPC provider for {LabelProviderTrySetModSettings}:\n{e}" );
}
}
private void DisposeSettingProviders()
{
ProviderGetAvailableModSettings?.UnregisterFunc();
ProviderGetCurrentModSettings?.UnregisterFunc();
ProviderTryInheritMod?.UnregisterFunc();
ProviderTrySetMod?.UnregisterFunc();
ProviderTrySetModPriority?.UnregisterFunc();
ProviderTrySetModSetting?.UnregisterFunc();
ProviderTrySetModSettings?.UnregisterFunc();
}
}
public partial class PenumbraIpc
{
public const string LabelProviderCreateTemporaryCollection = "Penumbra.CreateTemporaryCollection";
public const string LabelProviderRemoveTemporaryCollection = "Penumbra.RemoveTemporaryCollection";
public const string LabelProviderAddTemporaryModAll = "Penumbra.AddTemporaryModAll";
public const string LabelProviderAddTemporaryMod = "Penumbra.AddTemporaryMod";
public const string LabelProviderRemoveTemporaryModAll = "Penumbra.RemoveTemporaryModAll";
public const string LabelProviderRemoveTemporaryMod = "Penumbra.RemoveTemporaryMod";
internal ICallGateProvider< string, string, bool, (PenumbraApiEc, string) >? ProviderCreateTemporaryCollection;
internal ICallGateProvider< string, PenumbraApiEc >? ProviderRemoveTemporaryCollection;
internal ICallGateProvider< string, IReadOnlyDictionary< string, string >, IReadOnlySet< string >, int, PenumbraApiEc >?
ProviderAddTemporaryModAll;
internal ICallGateProvider< string, string, IReadOnlyDictionary< string, string >, IReadOnlySet< string >, int, PenumbraApiEc >?
ProviderAddTemporaryMod;
internal ICallGateProvider< string, int, PenumbraApiEc >? ProviderRemoveTemporaryModAll;
internal ICallGateProvider< string, string, int, PenumbraApiEc >? ProviderRemoveTemporaryMod;
private void InitializeTempProviders( DalamudPluginInterface pi )
{
try
{
ProviderCreateTemporaryCollection =
pi.GetIpcProvider< string, string, bool, (PenumbraApiEc, string) >( LabelProviderCreateTemporaryCollection );
ProviderCreateTemporaryCollection.RegisterFunc( Api.CreateTemporaryCollection );
}
catch( Exception e )
{
PluginLog.Error( $"Error registering IPC provider for {LabelProviderCreateTemporaryCollection}:\n{e}" );
}
try
{
ProviderRemoveTemporaryCollection =
pi.GetIpcProvider< string, PenumbraApiEc >( LabelProviderRemoveTemporaryCollection );
ProviderRemoveTemporaryCollection.RegisterFunc( Api.RemoveTemporaryCollection );
}
catch( Exception e )
{
PluginLog.Error( $"Error registering IPC provider for {LabelProviderRemoveTemporaryCollection}:\n{e}" );
}
try
{
ProviderAddTemporaryModAll =
pi.GetIpcProvider< string, IReadOnlyDictionary< string, string >, IReadOnlySet< string >, int, PenumbraApiEc >(
LabelProviderAddTemporaryModAll );
ProviderAddTemporaryModAll.RegisterFunc( Api.AddTemporaryModAll );
}
catch( Exception e )
{
PluginLog.Error( $"Error registering IPC provider for {LabelProviderAddTemporaryModAll}:\n{e}" );
}
try
{
ProviderAddTemporaryMod =
pi.GetIpcProvider< string, string, IReadOnlyDictionary< string, string >, IReadOnlySet< string >, int, PenumbraApiEc >(
LabelProviderAddTemporaryMod );
ProviderAddTemporaryMod.RegisterFunc( Api.AddTemporaryMod );
}
catch( Exception e )
{
PluginLog.Error( $"Error registering IPC provider for {LabelProviderAddTemporaryMod}:\n{e}" );
}
try
{
ProviderRemoveTemporaryModAll = pi.GetIpcProvider< string, int, PenumbraApiEc >( LabelProviderRemoveTemporaryModAll );
ProviderRemoveTemporaryModAll.RegisterFunc( Api.RemoveTemporaryModAll );
}
catch( Exception e )
{
PluginLog.Error( $"Error registering IPC provider for {LabelProviderRemoveTemporaryModAll}:\n{e}" );
}
try
{
ProviderRemoveTemporaryMod = pi.GetIpcProvider< string, string, int, PenumbraApiEc >( LabelProviderRemoveTemporaryMod );
ProviderRemoveTemporaryMod.RegisterFunc( Api.RemoveTemporaryMod );
}
catch( Exception e )
{
PluginLog.Error( $"Error registering IPC provider for {LabelProviderRemoveTemporaryMod}:\n{e}" );
}
}
private void DisposeTempProviders()
{
ProviderCreateTemporaryCollection?.UnregisterFunc();
ProviderRemoveTemporaryCollection?.UnregisterFunc();
ProviderAddTemporaryModAll?.UnregisterFunc();
ProviderAddTemporaryMod?.UnregisterFunc();
ProviderRemoveTemporaryModAll?.UnregisterFunc();
ProviderRemoveTemporaryMod?.UnregisterFunc();
}
} }

View file

@ -1,125 +0,0 @@
using System;
using System.Collections.Generic;
using Dalamud.Logging;
using Penumbra.Collections;
using Penumbra.GameData.ByteString;
using Penumbra.Mods;
namespace Penumbra.Api;
public enum RedirectResult
{
Registered = 0,
Success = 0,
IdenticalFileRegistered = 1,
InvalidGamePath = 2,
OtherOwner = 3,
NotRegistered = 4,
NoPermission = 5,
FilteredGamePath = 6,
UnknownError = 7,
}
public class SimpleRedirectManager
{
internal readonly Dictionary< Utf8GamePath, (FullPath File, string Tag) > Replacements = new();
public readonly HashSet< string > AllowedTags = new();
public void Apply( IDictionary< Utf8GamePath, ModPath > dict )
{
foreach( var (gamePath, (file, _)) in Replacements )
{
dict.TryAdd( gamePath, new ModPath(Mod.ForcedFiles, file) );
}
}
private RedirectResult? CheckPermission( string tag )
=> AllowedTags.Contains( tag ) ? null : RedirectResult.NoPermission;
public RedirectResult IsRegistered( Utf8GamePath path, string tag )
=> CheckPermission( tag )
?? ( Replacements.TryGetValue( path, out var pair )
? pair.Tag == tag ? RedirectResult.Registered : RedirectResult.OtherOwner
: RedirectResult.NotRegistered );
public RedirectResult Register( Utf8GamePath path, FullPath file, string tag )
{
if( CheckPermission( tag ) != null )
{
return RedirectResult.NoPermission;
}
if( Mod.FilterFile( path ) )
{
return RedirectResult.FilteredGamePath;
}
try
{
if( Replacements.TryGetValue( path, out var pair ) )
{
if( file.Equals( pair.File ) )
{
return RedirectResult.IdenticalFileRegistered;
}
if( tag != pair.Tag )
{
return RedirectResult.OtherOwner;
}
}
Replacements[ path ] = ( file, tag );
return RedirectResult.Success;
}
catch( Exception e )
{
PluginLog.Error( $"[{tag}] Unknown Error registering simple redirect {path} -> {file}:\n{e}" );
return RedirectResult.UnknownError;
}
}
public RedirectResult Unregister( Utf8GamePath path, string tag )
{
if( CheckPermission( tag ) != null )
{
return RedirectResult.NoPermission;
}
try
{
if( !Replacements.TryGetValue( path, out var pair ) )
{
return RedirectResult.NotRegistered;
}
if( tag != pair.Tag )
{
return RedirectResult.OtherOwner;
}
Replacements.Remove( path );
return RedirectResult.Success;
}
catch( Exception e )
{
PluginLog.Error( $"[{tag}] Unknown Error unregistering simple redirect {path}:\n{e}" );
return RedirectResult.UnknownError;
}
}
public RedirectResult Register( string path, string file, string tag )
=> Utf8GamePath.FromString( path, out var gamePath, true )
? Register( gamePath, new FullPath( file ), tag )
: RedirectResult.InvalidGamePath;
public RedirectResult Unregister( string path, string tag )
=> Utf8GamePath.FromString( path, out var gamePath, true )
? Unregister( gamePath, tag )
: RedirectResult.InvalidGamePath;
public RedirectResult IsRegistered( string path, string tag )
=> Utf8GamePath.FromString( path, out var gamePath, true )
? IsRegistered( gamePath, tag )
: RedirectResult.InvalidGamePath;
}

View file

@ -0,0 +1,261 @@
using System.Collections.Generic;
using Penumbra.Collections;
using Penumbra.GameData.ByteString;
using Penumbra.Meta.Manipulations;
using Penumbra.Mods;
namespace Penumbra.Api;
public enum RedirectResult
{
Success = 0,
IdenticalFileRegistered = 1,
NotRegistered = 2,
FilteredGamePath = 3,
}
public class TempModManager
{
private readonly Dictionary< ModCollection, List< Mod.TemporaryMod > > _mods = new();
private readonly List< Mod.TemporaryMod > _modsForAllCollections = new();
private readonly Dictionary< string, ModCollection > _collections = new();
public IReadOnlyDictionary< ModCollection, List< Mod.TemporaryMod > > Mods
=> _mods;
public IReadOnlyList< Mod.TemporaryMod > ModsForAllCollections
=> _modsForAllCollections;
public IReadOnlyDictionary< string, ModCollection > Collections
=> _collections;
// 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 );
mod.SetAll( dict, manips );
ApplyModChange( mod, collection, created, false );
return RedirectResult.Success;
}
public RedirectResult Unregister( string tag, ModCollection? collection, int? priority )
{
var list = collection == null ? _modsForAllCollections : _mods.TryGetValue( collection, out var l ) ? l : null;
if( list == null )
{
return RedirectResult.NotRegistered;
}
var removed = _modsForAllCollections.RemoveAll( m =>
{
if( m.Name != tag || priority != null && m.Priority != priority.Value )
{
return false;
}
ApplyModChange( m, collection, false, true );
return true;
} );
if( removed == 0 )
{
return RedirectResult.NotRegistered;
}
if( list.Count == 0 && collection != null )
{
_mods.Remove( collection );
}
return RedirectResult.Success;
}
public string SetTemporaryCollection( string tag, string characterName )
{
var collection = ModCollection.CreateNewTemporary( tag, characterName );
_collections[ characterName ] = collection;
return collection.Name;
}
public bool RemoveTemporaryCollection( string characterName )
{
if( _collections.Remove( characterName, out var c ) )
{
_mods.Remove( c );
return true;
}
return false;
}
// Apply any new changes to the temporary mod.
private static void ApplyModChange( Mod.TemporaryMod mod, ModCollection? collection, bool created, bool removed )
{
if( collection == null )
{
if( removed )
{
foreach( var c in Penumbra.CollectionManager )
{
c.Remove( mod );
}
}
else
{
foreach( var c in Penumbra.CollectionManager )
{
c.Apply( mod, created );
}
}
}
else
{
if( removed )
{
collection.Remove( mod );
}
else
{
collection.Apply( mod, created );
}
}
}
// Only find already existing mods, currently unused.
//private Mod.TemporaryMod? GetExistingMod( string tag, ModCollection? collection, int? priority )
//{
// var list = collection == null ? _modsForAllCollections : _mods.TryGetValue( collection, out var l ) ? l : null;
// if( list == null )
// {
// return null;
// }
//
// if( priority != null )
// {
// return list.Find( m => m.Priority == priority.Value && m.Name == tag );
// }
//
// Mod.TemporaryMod? highestMod = null;
// var highestPriority = int.MinValue;
// foreach( var m in list )
// {
// if( highestPriority < m.Priority && m.Name == tag )
// {
// highestPriority = m.Priority;
// highestMod = m;
// }
// }
//
// return highestMod;
//}
// Find or create a mod with the given tag as name and the given priority, for the given collection (or all collections).
// Returns the found or created mod and whether it was newly created.
private Mod.TemporaryMod GetOrCreateMod( string tag, ModCollection? collection, int priority, out bool created )
{
List< Mod.TemporaryMod > list;
if( collection == null )
{
list = _modsForAllCollections;
}
else if( _mods.TryGetValue( collection, out var l ) )
{
list = l;
}
else
{
list = new List< Mod.TemporaryMod >();
_mods.Add( collection, list );
}
var mod = list.Find( m => m.Priority == priority && m.Name == tag );
if( mod == null )
{
mod = new Mod.TemporaryMod()
{
Name = tag,
Priority = priority,
};
list.Add( mod );
created = true;
}
else
{
created = false;
}
return mod;
}
}

View file

@ -81,11 +81,14 @@ public partial class ModCollection
break; break;
} }
CurrentCollectionInUse = Characters.Values.Prepend( Default ).SelectMany( c => c.GetFlattenedInheritance() ).Contains( Current );
UpdateCurrentCollectionInUse();
CollectionChanged.Invoke( type, this[ oldCollectionIdx ], newCollection, characterName ); CollectionChanged.Invoke( type, this[ oldCollectionIdx ], newCollection, characterName );
} }
private void UpdateCurrentCollectionInUse()
=> CurrentCollectionInUse = Characters.Values.Prepend( Default ).SelectMany( c => c.GetFlattenedInheritance() ).Contains( Current );
public void SetCollection( ModCollection collection, Type type, string? characterName = null ) public void SetCollection( ModCollection collection, Type type, string? characterName = null )
=> SetCollection( collection.Index, type, characterName ); => SetCollection( collection.Index, type, characterName );

View file

@ -72,6 +72,7 @@ public partial class ModCollection
CollectionChanged += SaveOnChange; CollectionChanged += SaveOnChange;
ReadCollections(); ReadCollections();
LoadCollections(); LoadCollections();
UpdateCurrentCollectionInUse();
} }
public void Dispose() public void Dispose()

View file

@ -35,6 +35,24 @@ public partial class ModCollection
private void ForceCacheUpdate() private void ForceCacheUpdate()
=> CalculateEffectiveFileList(); => CalculateEffectiveFileList();
// Handle temporary mods for this collection.
public void Apply( Mod.TemporaryMod tempMod, bool created )
{
if( created )
{
_cache?.AddMod( tempMod, tempMod.TotalManipulations > 0 );
}
else
{
_cache?.ReloadMod( tempMod, tempMod.TotalManipulations > 0 );
}
}
public void Remove( Mod.TemporaryMod tempMod )
{
_cache?.RemoveMod( tempMod, tempMod.TotalManipulations > 0 );
}
// Clear the current cache. // Clear the current cache.
private void ClearCache() private void ClearCache()
@ -65,8 +83,8 @@ public partial class ModCollection
internal IReadOnlyDictionary< Utf8GamePath, ModPath > ResolvedFiles internal IReadOnlyDictionary< Utf8GamePath, ModPath > ResolvedFiles
=> _cache?.ResolvedFiles ?? new Dictionary< Utf8GamePath, ModPath >(); => _cache?.ResolvedFiles ?? new Dictionary< Utf8GamePath, ModPath >();
internal IReadOnlyDictionary< string, (SingleArray< Mod >, object?) > ChangedItems internal IReadOnlyDictionary< string, (SingleArray< IMod >, object?) > ChangedItems
=> _cache?.ChangedItems ?? new Dictionary< string, (SingleArray< Mod >, object?) >(); => _cache?.ChangedItems ?? new Dictionary< string, (SingleArray< IMod >, object?) >();
internal IEnumerable< SingleArray< ModConflicts > > AllConflicts internal IEnumerable< SingleArray< ModConflicts > > AllConflicts
=> _cache?.AllConflicts ?? Array.Empty< SingleArray< ModConflicts > >(); => _cache?.AllConflicts ?? Array.Empty< SingleArray< ModConflicts > >();

View file

@ -11,8 +11,8 @@ using Penumbra.Util;
namespace Penumbra.Collections; namespace Penumbra.Collections;
public record struct ModPath( Mod Mod, FullPath Path ); public record struct ModPath( IMod Mod, FullPath Path );
public record ModConflicts( Mod Mod2, List< object > Conflicts, bool HasPriority, bool Solved ); public record ModConflicts( IMod Mod2, List< object > Conflicts, bool HasPriority, bool Solved );
public partial class ModCollection public partial class ModCollection
{ {
@ -20,16 +20,16 @@ public partial class ModCollection
// It will only be setup if a collection gets activated in any way. // It will only be setup if a collection gets activated in any way.
private class Cache : IDisposable private class Cache : IDisposable
{ {
private readonly ModCollection _collection; private readonly ModCollection _collection;
private readonly SortedList< string, (SingleArray< Mod >, object?) > _changedItems = new(); private readonly SortedList< string, (SingleArray< IMod >, object?) > _changedItems = new();
public readonly Dictionary< Utf8GamePath, ModPath > ResolvedFiles = new(); public readonly Dictionary< Utf8GamePath, ModPath > ResolvedFiles = new();
public readonly MetaManager MetaManipulations; public readonly MetaManager MetaManipulations;
private readonly Dictionary< Mod, SingleArray< ModConflicts > > _conflicts = new(); private readonly Dictionary< IMod, SingleArray< ModConflicts > > _conflicts = new();
public IEnumerable< SingleArray< ModConflicts > > AllConflicts public IEnumerable< SingleArray< ModConflicts > > AllConflicts
=> _conflicts.Values; => _conflicts.Values;
public SingleArray< ModConflicts > Conflicts( Mod mod ) public SingleArray< ModConflicts > Conflicts( IMod mod )
=> _conflicts.TryGetValue( mod, out var c ) ? c : new SingleArray< ModConflicts >(); => _conflicts.TryGetValue( mod, out var c ) ? c : new SingleArray< ModConflicts >();
// Count the number of changes of the effective file list. // Count the number of changes of the effective file list.
@ -38,7 +38,7 @@ public partial class ModCollection
private int _changedItemsSaveCounter = -1; private int _changedItemsSaveCounter = -1;
// Obtain currently changed items. Computes them if they haven't been computed before. // Obtain currently changed items. Computes them if they haven't been computed before.
public IReadOnlyDictionary< string, (SingleArray< Mod >, object?) > ChangedItems public IReadOnlyDictionary< string, (SingleArray< IMod >, object?) > ChangedItems
{ {
get get
{ {
@ -160,7 +160,11 @@ public partial class ModCollection
_conflicts.Clear(); _conflicts.Clear();
// Add all forced redirects. // Add all forced redirects.
Penumbra.Redirects.Apply( ResolvedFiles ); foreach( var tempMod in Penumbra.TempMods.ModsForAllCollections.Concat(
Penumbra.TempMods.Mods.TryGetValue( _collection, out var list ) ? list : Array.Empty< Mod.TemporaryMod >() ) )
{
AddMod( tempMod, false );
}
foreach( var mod in Penumbra.ModManager ) foreach( var mod in Penumbra.ModManager )
{ {
@ -178,13 +182,13 @@ public partial class ModCollection
} }
} }
public void ReloadMod( Mod mod, bool addMetaChanges ) public void ReloadMod( IMod mod, bool addMetaChanges )
{ {
RemoveMod( mod, addMetaChanges ); RemoveMod( mod, addMetaChanges );
AddMod( mod, addMetaChanges ); AddMod( mod, addMetaChanges );
} }
public void RemoveMod( Mod mod, bool addMetaChanges ) public void RemoveMod( IMod mod, bool addMetaChanges )
{ {
var conflicts = Conflicts( mod ); var conflicts = Conflicts( mod );
@ -243,37 +247,40 @@ public partial class ModCollection
// Add all files and possibly manipulations of a given mod according to its settings in this collection. // Add all files and possibly manipulations of a given mod according to its settings in this collection.
public void AddMod( Mod mod, bool addMetaChanges ) public void AddMod( IMod mod, bool addMetaChanges )
{ {
var settings = _collection[ mod.Index ].Settings; if( mod.Index >= 0 )
if( settings is not { Enabled: true } )
{ {
return; var settings = _collection[ mod.Index ].Settings;
} if( settings is not { Enabled: true } )
foreach( var (group, groupIndex) in mod.Groups.WithIndex().OrderByDescending( g => g.Item1.Priority ) )
{
if( group.Count == 0 )
{ {
continue; return;
} }
var config = settings.Settings[ groupIndex ]; foreach( var (group, groupIndex) in mod.Groups.WithIndex().OrderByDescending( g => g.Item1.Priority ) )
switch( group.Type )
{ {
case SelectType.Single: if( group.Count == 0 )
AddSubMod( group[ ( int )config ], mod );
break;
case SelectType.Multi:
{ {
foreach( var (option, _) in group.WithIndex() continue;
.OrderByDescending( p => group.OptionPriority( p.Item2 ) ) }
.Where( p => ( ( 1 << p.Item2 ) & config ) != 0 ) )
{
AddSubMod( option, mod );
}
break; var config = settings.Settings[ groupIndex ];
switch( group.Type )
{
case SelectType.Single:
AddSubMod( group[ ( int )config ], mod );
break;
case SelectType.Multi:
{
foreach( var (option, _) in group.WithIndex()
.OrderByDescending( p => group.OptionPriority( p.Item2 ) )
.Where( p => ( ( 1 << p.Item2 ) & config ) != 0 ) )
{
AddSubMod( option, mod );
}
break;
}
} }
} }
} }
@ -297,7 +304,7 @@ public partial class ModCollection
} }
// Add all files and possibly manipulations of a specific submod // Add all files and possibly manipulations of a specific submod
private void AddSubMod( ISubMod subMod, Mod parentMod ) private void AddSubMod( ISubMod subMod, IMod parentMod )
{ {
foreach( var (path, file) in subMod.Files.Concat( subMod.FileSwaps ) ) foreach( var (path, file) in subMod.Files.Concat( subMod.FileSwaps ) )
{ {
@ -320,7 +327,7 @@ public partial class ModCollection
// For different mods, higher mod priority takes precedence before option group priority, // For different mods, higher mod priority takes precedence before option group priority,
// which takes precedence before option priority, which takes precedence before ordering. // which takes precedence before option priority, which takes precedence before ordering.
// Inside the same mod, conflicts are not recorded. // Inside the same mod, conflicts are not recorded.
private void AddFile( Utf8GamePath path, FullPath file, Mod mod ) private void AddFile( Utf8GamePath path, FullPath file, IMod mod )
{ {
if( ResolvedFiles.TryAdd( path, new ModPath( mod, file ) ) ) if( ResolvedFiles.TryAdd( path, new ModPath( mod, file ) ) )
{ {
@ -343,7 +350,7 @@ public partial class ModCollection
// Remove all empty conflict sets for a given mod with the given conflicts. // Remove all empty conflict sets for a given mod with the given conflicts.
// If transitive is true, also removes the corresponding version of the other mod. // If transitive is true, also removes the corresponding version of the other mod.
private void RemoveEmptyConflicts( Mod mod, SingleArray< ModConflicts > oldConflicts, bool transitive ) private void RemoveEmptyConflicts( IMod mod, SingleArray< ModConflicts > oldConflicts, bool transitive )
{ {
var changedConflicts = oldConflicts.Remove( c => var changedConflicts = oldConflicts.Remove( c =>
{ {
@ -372,10 +379,10 @@ public partial class ModCollection
// Add a new conflict between the added mod and the existing mod. // Add a new conflict between the added mod and the existing mod.
// Update all other existing conflicts between the existing mod and other mods if necessary. // Update all other existing conflicts between the existing mod and other mods if necessary.
// Returns if the added mod takes priority before the existing mod. // Returns if the added mod takes priority before the existing mod.
private bool AddConflict( object data, Mod addedMod, Mod existingMod ) private bool AddConflict( object data, IMod addedMod, IMod existingMod )
{ {
var addedPriority = addedMod.Index >= 0 ? _collection[ addedMod.Index ].Settings!.Priority : int.MaxValue; var addedPriority = addedMod.Index >= 0 ? _collection[ addedMod.Index ].Settings!.Priority : addedMod.Priority;
var existingPriority = existingMod.Index >= 0 ? _collection[ existingMod.Index ].Settings!.Priority : int.MaxValue; var existingPriority = existingMod.Index >= 0 ? _collection[ existingMod.Index ].Settings!.Priority : existingMod.Priority;
if( existingPriority < addedPriority ) if( existingPriority < addedPriority )
{ {
@ -417,7 +424,7 @@ public partial class ModCollection
// For different mods, higher mod priority takes precedence before option group priority, // For different mods, higher mod priority takes precedence before option group priority,
// which takes precedence before option priority, which takes precedence before ordering. // which takes precedence before option priority, which takes precedence before ordering.
// Inside the same mod, conflicts are not recorded. // Inside the same mod, conflicts are not recorded.
private void AddManipulation( MetaManipulation manip, Mod mod ) private void AddManipulation( MetaManipulation manip, IMod mod )
{ {
if( !MetaManipulations.TryGetValue( manip, out var existingMod ) ) if( !MetaManipulations.TryGetValue( manip, out var existingMod ) )
{ {
@ -463,7 +470,7 @@ public partial class ModCollection
{ {
if( !_changedItems.TryGetValue( name, out var data ) ) if( !_changedItems.TryGetValue( name, out var data ) )
{ {
_changedItems.Add( name, ( new SingleArray< Mod >( modPath.Mod ), obj ) ); _changedItems.Add( name, ( new SingleArray< IMod >( modPath.Mod ), obj ) );
} }
else if( !data.Item1.Contains( modPath.Mod ) ) else if( !data.Item1.Contains( modPath.Mod ) )
{ {

View file

@ -24,17 +24,20 @@ public partial class ModCollection
public event ModSettingChangeDelegate ModSettingChanged; public event ModSettingChangeDelegate ModSettingChanged;
// Enable or disable the mod inheritance of mod idx. // Enable or disable the mod inheritance of mod idx.
public void SetModInheritance( int idx, bool inherit ) public bool SetModInheritance( int idx, bool inherit )
{ {
if( FixInheritance( idx, inherit ) ) if( FixInheritance( idx, inherit ) )
{ {
ModSettingChanged.Invoke( ModSettingChange.Inheritance, idx, inherit ? 0 : 1, 0, false ); ModSettingChanged.Invoke( ModSettingChange.Inheritance, idx, inherit ? 0 : 1, 0, false );
return true;
} }
return false;
} }
// Set the enabled state mod idx to newValue if it differs from the current enabled state. // Set the enabled state mod idx to newValue if it differs from the current enabled state.
// If mod idx is currently inherited, stop the inheritance. // If mod idx is currently inherited, stop the inheritance.
public void SetModState( int idx, bool newValue ) public bool SetModState( int idx, bool newValue )
{ {
var oldValue = _settings[ idx ]?.Enabled ?? this[ idx ].Settings?.Enabled ?? false; var oldValue = _settings[ idx ]?.Enabled ?? this[ idx ].Settings?.Enabled ?? false;
if( newValue != oldValue ) if( newValue != oldValue )
@ -42,7 +45,10 @@ public partial class ModCollection
var inheritance = FixInheritance( idx, false ); var inheritance = FixInheritance( idx, false );
_settings[ idx ]!.Enabled = newValue; _settings[ idx ]!.Enabled = newValue;
ModSettingChanged.Invoke( ModSettingChange.EnableState, idx, inheritance ? -1 : newValue ? 0 : 1, 0, false ); ModSettingChanged.Invoke( ModSettingChange.EnableState, idx, inheritance ? -1 : newValue ? 0 : 1, 0, false );
return true;
} }
return false;
} }
// Enable or disable the mod inheritance of every mod in mods. // Enable or disable the mod inheritance of every mod in mods.
@ -78,7 +84,7 @@ public partial class ModCollection
// Set the priority of mod idx to newValue if it differs from the current priority. // Set the priority of mod idx to newValue if it differs from the current priority.
// If mod idx is currently inherited, stop the inheritance. // If mod idx is currently inherited, stop the inheritance.
public void SetModPriority( int idx, int newValue ) public bool SetModPriority( int idx, int newValue )
{ {
var oldValue = _settings[ idx ]?.Priority ?? this[ idx ].Settings?.Priority ?? 0; var oldValue = _settings[ idx ]?.Priority ?? this[ idx ].Settings?.Priority ?? 0;
if( newValue != oldValue ) if( newValue != oldValue )
@ -86,12 +92,15 @@ public partial class ModCollection
var inheritance = FixInheritance( idx, false ); var inheritance = FixInheritance( idx, false );
_settings[ idx ]!.Priority = newValue; _settings[ idx ]!.Priority = newValue;
ModSettingChanged.Invoke( ModSettingChange.Priority, idx, inheritance ? -1 : oldValue, 0, false ); ModSettingChanged.Invoke( ModSettingChange.Priority, idx, inheritance ? -1 : oldValue, 0, false );
return true;
} }
return false;
} }
// Set a given setting group settingName of mod idx to newValue if it differs from the current value and fix it if necessary. // Set a given setting group settingName of mod idx to newValue if it differs from the current value and fix it if necessary.
// If mod idx is currently inherited, stop the inheritance. // If mod idx is currently inherited, stop the inheritance.
public void SetModSetting( int idx, int groupIdx, uint newValue ) public bool SetModSetting( int idx, int groupIdx, uint newValue )
{ {
var settings = _settings[ idx ] != null ? _settings[ idx ]!.Settings : this[ idx ].Settings?.Settings; var settings = _settings[ idx ] != null ? _settings[ idx ]!.Settings : this[ idx ].Settings?.Settings;
var oldValue = settings?[ groupIdx ] ?? 0; var oldValue = settings?[ groupIdx ] ?? 0;
@ -100,31 +109,26 @@ public partial class ModCollection
var inheritance = FixInheritance( idx, false ); var inheritance = FixInheritance( idx, false );
_settings[ idx ]!.SetValue( Penumbra.ModManager[ idx ], groupIdx, newValue ); _settings[ idx ]!.SetValue( Penumbra.ModManager[ idx ], groupIdx, newValue );
ModSettingChanged.Invoke( ModSettingChange.Setting, idx, inheritance ? -1 : ( int )oldValue, groupIdx, false ); ModSettingChanged.Invoke( ModSettingChange.Setting, idx, inheritance ? -1 : ( int )oldValue, groupIdx, false );
return true;
} }
return false;
} }
// Change one of the available mod settings for mod idx discerned by type. // Change one of the available mod settings for mod idx discerned by type.
// If type == Setting, settingName should be a valid setting for that mod, otherwise it will be ignored. // If type == Setting, settingName should be a valid setting for that mod, otherwise it will be ignored.
// The setting will also be automatically fixed if it is invalid for that setting group. // The setting will also be automatically fixed if it is invalid for that setting group.
// For boolean parameters, newValue == 0 will be treated as false and != 0 as true. // For boolean parameters, newValue == 0 will be treated as false and != 0 as true.
public void ChangeModSetting( ModSettingChange type, int idx, int newValue, int groupIdx ) public bool ChangeModSetting( ModSettingChange type, int idx, int newValue, int groupIdx )
{ {
switch( type ) return type switch
{ {
case ModSettingChange.Inheritance: ModSettingChange.Inheritance => SetModInheritance( idx, newValue != 0 ),
SetModInheritance( idx, newValue != 0 ); ModSettingChange.EnableState => SetModState( idx, newValue != 0 ),
break; ModSettingChange.Priority => SetModPriority( idx, newValue ),
case ModSettingChange.EnableState: ModSettingChange.Setting => SetModSetting( idx, groupIdx, ( uint )newValue ),
SetModState( idx, newValue != 0 ); _ => throw new ArgumentOutOfRangeException( nameof( type ), type, null ),
break; };
case ModSettingChange.Priority:
SetModPriority( idx, newValue );
break;
case ModSettingChange.Setting:
SetModSetting( idx, groupIdx, ( uint )newValue );
break;
default: throw new ArgumentOutOfRangeException( nameof( type ), type, null );
}
} }
// Set inheritance of a mod without saving, // Set inheritance of a mod without saving,

View file

@ -74,6 +74,17 @@ public partial class ModCollection
public static ModCollection CreateNewEmpty( string name ) public static ModCollection CreateNewEmpty( string name )
=> new(name, CurrentVersion, new Dictionary< string, ModSettings.SavedSettings >()); => new(name, CurrentVersion, new Dictionary< string, ModSettings.SavedSettings >());
// Create a new temporary collection that does not save and has a negative index.
public static ModCollection CreateNewTemporary(string tag, string characterName)
{
var collection = new ModCollection($"{tag}_{characterName}_temporary", Empty);
collection.ModSettingChanged -= collection.SaveOnChange;
collection.InheritanceChanged -= collection.SaveOnChange;
collection.Index = ~Penumbra.TempMods.Collections.Count;
collection.CreateCache();
return collection;
}
// Duplicate the calling collection to a new, unique collection of a given name. // Duplicate the calling collection to a new, unique collection of a given name.
public ModCollection Duplicate( string name ) public ModCollection Duplicate( string name )
=> new(name, this); => new(name, this);

View file

@ -3,6 +3,7 @@ using System.Collections.Generic;
using System.Linq; using System.Linq;
using Dalamud.Configuration; using Dalamud.Configuration;
using Dalamud.Logging; using Dalamud.Logging;
using OtterGui.Classes;
using OtterGui.Filesystem; using OtterGui.Filesystem;
using Penumbra.Import; using Penumbra.Import;
using Penumbra.UI.Classes; using Penumbra.UI.Classes;
@ -46,6 +47,7 @@ public partial class Configuration : IPluginConfiguration
public int ModSelectorScaledSize { get; set; } = Constants.DefaultScaledSize; public int ModSelectorScaledSize { get; set; } = Constants.DefaultScaledSize;
public bool OpenFoldersByDefault { get; set; } = false; public bool OpenFoldersByDefault { get; set; } = false;
public string DefaultImportFolder { get; set; } = string.Empty; public string DefaultImportFolder { get; set; } = string.Empty;
public DoubleModifier DeleteModModifier { get; set; } = new(ModifierHotkey.Control, ModifierHotkey.Shift);
public bool FixMainWindow { get; set; } = false; public bool FixMainWindow { get; set; } = false;
public bool ShowAdvanced { get; set; } public bool ShowAdvanced { get; set; }

View file

@ -72,4 +72,38 @@ public unsafe partial class PathResolver
_animationLoadCollection = last; _animationLoadCollection = last;
return ret; return ret;
} }
// Unknown what exactly this is but it seems to load a bunch of paps.
public delegate void LoadSomePap( IntPtr a1, int a2, IntPtr a3, int a4 );
[Signature( "48 89 5C 24 ?? 48 89 6C 24 ?? 48 89 74 24 ?? 57 41 56 41 57 48 83 EC ?? 41 8B D9 89 51" )]
public Hook< LoadSomePap >? LoadSomePapHook;
private void LoadSomePapDetour( IntPtr a1, int a2, IntPtr a3, int a4 )
{
var timelinePtr = a1 + 0x50;
var last = _animationLoadCollection;
if( timelinePtr != IntPtr.Zero )
{
var actorIdx = ( int )( *( *( ulong** )timelinePtr + 1 ) >> 3 );
if( actorIdx >= 0 && actorIdx < Dalamud.Objects.Length )
{
_animationLoadCollection = IdentifyCollection( ( GameObject* )( Dalamud.Objects[ actorIdx ]?.Address ?? IntPtr.Zero ) );
}
}
LoadSomePapHook!.Original( a1, a2, a3, a4 );
_animationLoadCollection = last;
}
// Seems to load character actions when zoning or changing class, maybe.
[Signature( "E8 ?? ?? ?? ?? C6 83 ?? ?? ?? ?? ?? 8B 8E", DetourName = nameof( SomeActionLoadDetour ) )]
public Hook< CharacterBaseDestructorDelegate >? SomeActionLoadHook;
private void SomeActionLoadDetour( IntPtr gameObject )
{
var last = _animationLoadCollection;
_animationLoadCollection = IdentifyCollection( ( GameObject* )gameObject );
SomeActionLoadHook!.Original( gameObject );
_animationLoadCollection = last;
}
} }

View file

@ -93,6 +93,8 @@ public unsafe partial class PathResolver
LoadTimelineResourcesHook?.Enable(); LoadTimelineResourcesHook?.Enable();
CharacterBaseLoadAnimationHook?.Enable(); CharacterBaseLoadAnimationHook?.Enable();
LoadSomeAvfxHook?.Enable(); LoadSomeAvfxHook?.Enable();
LoadSomePapHook?.Enable();
SomeActionLoadHook?.Enable();
} }
private void DisableDataHooks() private void DisableDataHooks()
@ -105,6 +107,8 @@ public unsafe partial class PathResolver
LoadTimelineResourcesHook?.Disable(); LoadTimelineResourcesHook?.Disable();
CharacterBaseLoadAnimationHook?.Disable(); CharacterBaseLoadAnimationHook?.Disable();
LoadSomeAvfxHook?.Disable(); LoadSomeAvfxHook?.Disable();
LoadSomePapHook?.Disable();
SomeActionLoadHook?.Disable();
} }
private void DisposeDataHooks() private void DisposeDataHooks()
@ -116,6 +120,8 @@ public unsafe partial class PathResolver
LoadTimelineResourcesHook?.Dispose(); LoadTimelineResourcesHook?.Dispose();
CharacterBaseLoadAnimationHook?.Dispose(); CharacterBaseLoadAnimationHook?.Dispose();
LoadSomeAvfxHook?.Dispose(); LoadSomeAvfxHook?.Dispose();
LoadSomePapHook?.Dispose();
SomeActionLoadHook?.Dispose();
} }
// 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.
@ -272,8 +278,12 @@ public unsafe partial class PathResolver
} }
// Housing Retainers // Housing Retainers
if( Penumbra.Config.UseDefaultCollectionForRetainers && gameObject->ObjectKind == (byte) ObjectKind.EventNpc && gameObject->DataID == 1011832 ) if( Penumbra.Config.UseDefaultCollectionForRetainers
&& gameObject->ObjectKind == ( byte )ObjectKind.EventNpc
&& gameObject->DataID == 1011832 )
{
return Penumbra.CollectionManager.Default; return Penumbra.CollectionManager.Default;
}
string? actorName = null; string? actorName = null;
if( Penumbra.Config.PreferNamedCollectionsOverOwners ) if( Penumbra.Config.PreferNamedCollectionsOverOwners )
@ -299,7 +309,8 @@ public unsafe partial class PathResolver
} }
?? GetOwnerName( gameObject ) ?? actorName ?? new Utf8String( gameObject->Name ).ToString(); ?? GetOwnerName( gameObject ) ?? actorName ?? new Utf8String( gameObject->Name ).ToString();
return Penumbra.CollectionManager.Character( actualName ); // First check temporary character collections, then the own configuration.
return Penumbra.TempMods.Collections.TryGetValue(actualName, out var c) ? c : Penumbra.CollectionManager.Character( actualName );
} }
// 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.

View file

@ -84,6 +84,7 @@ public partial class PathResolver : IDisposable
case ResourceType.Pap: case ResourceType.Pap:
case ResourceType.Avfx: case ResourceType.Avfx:
case ResourceType.Atex: case ResourceType.Atex:
case ResourceType.Scd:
collection = _animationLoadCollection; collection = _animationLoadCollection;
return true; return true;
} }

View file

@ -14,7 +14,7 @@ public partial class MetaManager
public struct MetaManagerCmp : IDisposable public struct MetaManagerCmp : IDisposable
{ {
public CmpFile? File = null; public CmpFile? File = null;
public readonly Dictionary< RspManipulation, Mod > Manipulations = new(); public readonly Dictionary< RspManipulation, IMod > Manipulations = new();
public MetaManagerCmp() public MetaManagerCmp()
{ } { }
@ -39,7 +39,7 @@ public partial class MetaManager
Manipulations.Clear(); Manipulations.Clear();
} }
public bool ApplyMod( RspManipulation m, Mod mod ) public bool ApplyMod( RspManipulation m, IMod mod )
{ {
#if USE_CMP #if USE_CMP
Manipulations[ m ] = mod; Manipulations[ m ] = mod;

View file

@ -17,7 +17,7 @@ public partial class MetaManager
{ {
public readonly ExpandedEqdpFile?[] Files = new ExpandedEqdpFile?[CharacterUtility.NumEqdpFiles - 2]; // TODO: female Hrothgar public readonly ExpandedEqdpFile?[] Files = new ExpandedEqdpFile?[CharacterUtility.NumEqdpFiles - 2]; // TODO: female Hrothgar
public readonly Dictionary< EqdpManipulation, Mod > Manipulations = new(); public readonly Dictionary< EqdpManipulation, IMod > Manipulations = new();
public MetaManagerEqdp() public MetaManagerEqdp()
{ } { }
@ -51,7 +51,7 @@ public partial class MetaManager
Manipulations.Clear(); Manipulations.Clear();
} }
public bool ApplyMod( EqdpManipulation m, Mod mod ) public bool ApplyMod( EqdpManipulation m, IMod mod )
{ {
#if USE_EQDP #if USE_EQDP
Manipulations[ m ] = mod; Manipulations[ m ] = mod;

View file

@ -14,7 +14,7 @@ public partial class MetaManager
public struct MetaManagerEqp : IDisposable public struct MetaManagerEqp : IDisposable
{ {
public ExpandedEqpFile? File = null; public ExpandedEqpFile? File = null;
public readonly Dictionary< EqpManipulation, Mod > Manipulations = new(); public readonly Dictionary< EqpManipulation, IMod > Manipulations = new();
public MetaManagerEqp() public MetaManagerEqp()
{ } { }
@ -39,7 +39,7 @@ public partial class MetaManager
Manipulations.Clear(); Manipulations.Clear();
} }
public bool ApplyMod( EqpManipulation m, Mod mod ) public bool ApplyMod( EqpManipulation m, IMod mod )
{ {
#if USE_EQP #if USE_EQP
Manipulations[ m ] = mod; Manipulations[ m ] = mod;

View file

@ -18,7 +18,7 @@ public partial class MetaManager
public EstFile? BodyFile = null; public EstFile? BodyFile = null;
public EstFile? HeadFile = null; public EstFile? HeadFile = null;
public readonly Dictionary< EstManipulation, Mod > Manipulations = new(); public readonly Dictionary< EstManipulation, IMod > Manipulations = new();
public MetaManagerEst() public MetaManagerEst()
{ } { }
@ -51,7 +51,7 @@ public partial class MetaManager
Manipulations.Clear(); Manipulations.Clear();
} }
public bool ApplyMod( EstManipulation m, Mod mod ) public bool ApplyMod( EstManipulation m, IMod mod )
{ {
#if USE_EST #if USE_EST
Manipulations[ m ] = mod; Manipulations[ m ] = mod;

View file

@ -14,7 +14,7 @@ public partial class MetaManager
public struct MetaManagerGmp : IDisposable public struct MetaManagerGmp : IDisposable
{ {
public ExpandedGmpFile? File = null; public ExpandedGmpFile? File = null;
public readonly Dictionary< GmpManipulation, Mod > Manipulations = new(); public readonly Dictionary< GmpManipulation, IMod > Manipulations = new();
public MetaManagerGmp() public MetaManagerGmp()
{ } { }
@ -38,7 +38,7 @@ public partial class MetaManager
} }
} }
public bool ApplyMod( GmpManipulation m, Mod mod ) public bool ApplyMod( GmpManipulation m, IMod mod )
{ {
#if USE_GMP #if USE_GMP
Manipulations[ m ] = mod; Manipulations[ m ] = mod;

View file

@ -18,7 +18,7 @@ public partial class MetaManager
public readonly struct MetaManagerImc : IDisposable public readonly struct MetaManagerImc : IDisposable
{ {
public readonly Dictionary< Utf8GamePath, ImcFile > Files = new(); public readonly Dictionary< Utf8GamePath, ImcFile > Files = new();
public readonly Dictionary< ImcManipulation, Mod > Manipulations = new(); public readonly Dictionary< ImcManipulation, IMod > Manipulations = new();
private readonly ModCollection _collection; private readonly ModCollection _collection;
private static int _imcManagerCount; private static int _imcManagerCount;
@ -65,7 +65,7 @@ public partial class MetaManager
Manipulations.Clear(); Manipulations.Clear();
} }
public bool ApplyMod( ImcManipulation m, Mod mod ) public bool ApplyMod( ImcManipulation m, IMod mod )
{ {
#if USE_IMC #if USE_IMC
Manipulations[ m ] = mod; Manipulations[ m ] = mod;

View file

@ -30,7 +30,7 @@ public partial class MetaManager : IDisposable
} }
} }
public bool TryGetValue( MetaManipulation manip, [NotNullWhen(true)] out Mod? mod ) public bool TryGetValue( MetaManipulation manip, [NotNullWhen(true)] out IMod? mod )
{ {
mod = manip.ManipulationType switch mod = manip.ManipulationType switch
{ {
@ -86,7 +86,7 @@ public partial class MetaManager : IDisposable
Imc.Dispose(); Imc.Dispose();
} }
public bool ApplyMod( MetaManipulation m, Mod mod ) public bool ApplyMod( MetaManipulation m, IMod mod )
{ {
return m.ManipulationType switch return m.ManipulationType switch
{ {

View file

@ -19,6 +19,8 @@ public interface IMetaManipulation< T >
[StructLayout( LayoutKind.Explicit, Pack = 1, Size = 16 )] [StructLayout( LayoutKind.Explicit, Pack = 1, Size = 16 )]
public readonly struct MetaManipulation : IEquatable< MetaManipulation >, IComparable< MetaManipulation > public readonly struct MetaManipulation : IEquatable< MetaManipulation >, IComparable< MetaManipulation >
{ {
public const int CurrentVersion = 0;
public enum Type : byte public enum Type : byte
{ {
Unknown = 0, Unknown = 0,

View file

@ -0,0 +1,19 @@
using System.Collections.Generic;
using OtterGui.Classes;
namespace Penumbra.Mods;
public interface IMod
{
LowerString Name { get; }
public int Index { get; }
public int Priority { get; }
public int TotalManipulations { get; }
public ISubMod Default { get; }
public IReadOnlyList< IModGroup > Groups { get; }
public IEnumerable< ISubMod > AllSubMods { get; }
}

View file

@ -1,13 +1,10 @@
using System; using System;
using System.Collections.Generic;
using System.IO; using System.IO;
using System.Linq;
using Penumbra.GameData.ByteString;
using Penumbra.Util; using Penumbra.Util;
namespace Penumbra.Mods; namespace Penumbra.Mods;
public partial class Mod public partial class Mod : IMod
{ {
public partial class Editor : IDisposable public partial class Editor : IDisposable
{ {
@ -15,7 +12,7 @@ public partial class Mod
public Editor( Mod mod, int groupIdx, int optionIdx ) public Editor( Mod mod, int groupIdx, int optionIdx )
{ {
_mod = mod; _mod = mod;
SetSubMod( groupIdx, optionIdx ); SetSubMod( groupIdx, optionIdx );
GroupIdx = groupIdx; GroupIdx = groupIdx;
_subMod = _mod._default; _subMod = _mod._default;

View file

@ -1,6 +1,7 @@
using System; using System;
using System.Collections; using System.Collections;
using System.Collections.Generic; using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
namespace Penumbra.Mods; namespace Penumbra.Mods;
@ -37,5 +38,28 @@ public sealed partial class Mod
ModOptionChanged += OnModOptionChange; ModOptionChanged += OnModOptionChange;
ModPathChanged += OnModPathChange; ModPathChanged += OnModPathChange;
} }
// Try to obtain a mod by its directory name (unique identifier, preferred),
// or the first mod of the given name if no directory fits.
public bool TryGetMod( string modDirectory, string modName, [NotNullWhen( true )] out Mod? mod )
{
mod = null;
foreach( var m in _mods )
{
if( m.ModPath.Name == modDirectory )
{
mod = m;
return true;
}
if( m.Name == modName )
{
mod ??= m;
}
}
return mod != null;
}
} }
} }

View file

@ -17,6 +17,10 @@ public partial class Mod
public DirectoryInfo ModPath { get; private set; } public DirectoryInfo ModPath { get; private set; }
public int Index { get; private set; } = -1; public int Index { get; private set; } = -1;
// Unused if Index < 0 but used for special temporary mods.
public int Priority
=> 0;
private Mod( DirectoryInfo modPath ) private Mod( DirectoryInfo modPath )
=> ModPath = modPath; => ModPath = modPath;
@ -30,7 +34,7 @@ public partial class Mod
} }
var mod = new Mod( modPath ); var mod = new Mod( modPath );
if( !mod.Reload(out _) ) if( !mod.Reload( out _ ) )
{ {
// Can not be base path not existing because that is checked before. // Can not be base path not existing because that is checked before.
PluginLog.Error( $"Mod at {modPath} without name is not supported." ); PluginLog.Error( $"Mod at {modPath} without name is not supported." );
@ -40,15 +44,17 @@ public partial class Mod
return mod; return mod;
} }
private bool Reload(out MetaChangeType metaChange) private bool Reload( out MetaChangeType metaChange )
{ {
metaChange = MetaChangeType.Deletion; metaChange = MetaChangeType.Deletion;
ModPath.Refresh(); ModPath.Refresh();
if( !ModPath.Exists ) if( !ModPath.Exists )
{
return false; return false;
}
metaChange = LoadMeta(); metaChange = LoadMeta();
if( metaChange.HasFlag(MetaChangeType.Deletion) || Name.Length == 0 ) if( metaChange.HasFlag( MetaChangeType.Deletion ) || Name.Length == 0 )
{ {
return false; return false;
} }

View file

@ -24,10 +24,11 @@ public enum MetaChangeType : ushort
public sealed partial class Mod public sealed partial class Mod
{ {
public static readonly Mod ForcedFiles = new(new DirectoryInfo( "." )) public static readonly TemporaryMod ForcedFiles = new()
{ {
Name = "Forced Files", Name = "Forced Files",
Index = -1, Index = -1,
Priority = int.MaxValue,
}; };
public const uint CurrentFileVersion = 1; public const uint CurrentFileVersion = 1;

View file

@ -0,0 +1,43 @@
using System;
using System.Collections.Generic;
using OtterGui.Classes;
using Penumbra.GameData.ByteString;
using Penumbra.Meta.Manipulations;
namespace Penumbra.Mods;
public sealed partial class Mod
{
public class TemporaryMod : IMod
{
public LowerString Name { get; init; } = LowerString.Empty;
public int Index { get; init; } = -2;
public int Priority { get; init; } = int.MaxValue;
public int TotalManipulations
=> Default.Manipulations.Count;
public ISubMod Default
=> _default;
public IReadOnlyList< IModGroup > Groups
=> Array.Empty< IModGroup >();
public IEnumerable< ISubMod > AllSubMods
=> new[] { Default };
private readonly SubMod _default = new();
public void SetFile( Utf8GamePath gamePath, FullPath fullPath )
=> _default.FileData[ gamePath ] = fullPath;
public bool SetManipulation( MetaManipulation manip )
=> _default.ManipulationData.Remove( manip ) | _default.ManipulationData.Add( manip );
public void SetAll( Dictionary< Utf8GamePath, FullPath > dict, HashSet< MetaManipulation > manips )
{
_default.FileData = dict;
_default.ManipulationData = manips;
}
}
}

View file

@ -3,6 +3,7 @@ using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Numerics; using System.Numerics;
using OtterGui.Filesystem; using OtterGui.Filesystem;
using Penumbra.Util;
namespace Penumbra.Mods; namespace Penumbra.Mods;
@ -10,7 +11,7 @@ namespace Penumbra.Mods;
public class ModSettings public class ModSettings
{ {
public static readonly ModSettings Empty = new(); public static readonly ModSettings Empty = new();
public List< uint > Settings { get; init; } = new(); public List< uint > Settings { get; private init; } = new();
public int Priority { get; set; } public int Priority { get; set; }
public bool Enabled { get; set; } public bool Enabled { get; set; }
@ -100,7 +101,7 @@ public class ModSettings
private static uint FixSetting( IModGroup group, uint value ) private static uint FixSetting( IModGroup group, uint value )
=> group.Type switch => group.Type switch
{ {
SelectType.Single => ( uint )Math.Min( value, group.Count - 1 ), SelectType.Single => ( uint )Math.Min( value, group.Count - 1 ),
SelectType.Multi => ( uint )( value & ( ( 1ul << group.Count ) - 1 ) ), SelectType.Multi => ( uint )( value & ( ( 1ul << group.Count ) - 1 ) ),
_ => value, _ => value,
}; };
@ -208,4 +209,31 @@ public class ModSettings
return changes; return changes;
} }
} }
// Return the settings for a given mod in a shareable format, using the names of groups and options instead of indices.
// Does not repair settings but ignores settings not fitting to the given mod.
public (bool Enabled, int Priority, Dictionary< string, IList< string > > Settings) ConvertToShareable( Mod mod )
{
var dict = new Dictionary< string, IList< string > >( Settings.Count );
foreach( var (setting, idx) in Settings.WithIndex() )
{
if( idx >= mod.Groups.Count )
{
break;
}
var group = mod.Groups[ idx ];
if( group.Type == SelectType.Single && setting < group.Count )
{
dict.Add( group.Name, new[] { group[ ( int )setting ].Name } );
}
else
{
var list = group.Where( ( _, optionIdx ) => ( setting & ( 1 << optionIdx ) ) != 0 ).Select( o => o.Name ).ToList();
dict.Add( group.Name, list );
}
}
return ( Enabled, Priority, dict );
}
} }

View file

@ -29,10 +29,14 @@ public class MainClass : IDalamudPlugin
{ {
private Penumbra? _penumbra; private Penumbra? _penumbra;
private readonly CharacterUtility _characterUtility; private readonly CharacterUtility _characterUtility;
public static bool DevPenumbraExists;
public static bool IsNotInstalledPenumbra;
public MainClass( DalamudPluginInterface pluginInterface ) public MainClass( DalamudPluginInterface pluginInterface )
{ {
Dalamud.Initialize( pluginInterface ); Dalamud.Initialize( pluginInterface );
DevPenumbraExists = CheckDevPluginPenumbra();
IsNotInstalledPenumbra = CheckIsNotInstalled();
GameData.GameData.GetIdentifier( Dalamud.GameData, Dalamud.ClientState.ClientLanguage ); GameData.GameData.GetIdentifier( Dalamud.GameData, Dalamud.ClientState.ClientLanguage );
_characterUtility = new CharacterUtility(); _characterUtility = new CharacterUtility();
_characterUtility.LoadingFinished += () _characterUtility.LoadingFinished += ()
@ -47,6 +51,41 @@ public class MainClass : IDalamudPlugin
public string Name public string Name
=> Penumbra.Name; => Penumbra.Name;
// Because remnants of penumbra in devPlugins cause issues, we check for them to warn users to remove them.
private static bool CheckDevPluginPenumbra()
{
#if !DEBUG
var path = Path.Combine( Dalamud.PluginInterface.DalamudAssetDirectory.Parent?.FullName ?? "INVALIDPATH", "devPlugins", "Penumbra" );
var dir = new DirectoryInfo( path );
try
{
return dir.Exists && dir.EnumerateFiles( "*.dll", SearchOption.AllDirectories ).Any();
}
catch( Exception e )
{
PluginLog.Error( $"Could not check for dev plugin Penumbra:\n{e}" );
return true;
}
#else
return false;
#endif
}
// Check if the loaded version of penumbra itself is in devPlugins.
private static bool CheckIsNotInstalled()
{
#if !DEBUG
var checkedDirectory = Dalamud.PluginInterface.AssemblyLocation.Directory?.Parent?.Parent?.Name;
var ret = checkedDirectory?.Equals( "installedPlugins", StringComparison.InvariantCultureIgnoreCase ) ?? false;
if (!ret)
PluginLog.Error($"Penumbra is not correctly installed. Application loaded from \"{Dalamud.PluginInterface.AssemblyLocation.Directory!.FullName}\"." );
return !ret;
#else
return false;
#endif
}
} }
public class Penumbra : IDisposable public class Penumbra : IDisposable
@ -66,12 +105,11 @@ public class Penumbra : IDisposable
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 SimpleRedirectManager Redirects { get; private set; } = null!; public static TempModManager TempMods { get; private set; } = null!;
public static ResourceLoader ResourceLoader { get; private set; } = null!; public static ResourceLoader ResourceLoader { get; private set; } = null!;
public static FrameworkManager Framework { get; private set; } = null!; public static FrameworkManager Framework { get; private set; } = null!;
public static int ImcExceptions = 0; public static int ImcExceptions = 0;
public readonly ResourceLogger ResourceLogger; public readonly ResourceLogger ResourceLogger;
public readonly PathResolver PathResolver; public readonly PathResolver PathResolver;
public readonly MusicManager MusicManager; public readonly MusicManager MusicManager;
@ -100,7 +138,7 @@ public class Penumbra : IDisposable
} }
ResidentResources = new ResidentResourceManager(); ResidentResources = new ResidentResourceManager();
Redirects = new SimpleRedirectManager(); TempMods = new TempModManager();
MetaFileManager = new MetaFileManager(); MetaFileManager = new MetaFileManager();
ResourceLoader = new ResourceLoader( this ); ResourceLoader = new ResourceLoader( this );
ResourceLogger = new ResourceLogger( ResourceLoader ); ResourceLogger = new ResourceLogger( ResourceLoader );

View file

@ -856,8 +856,6 @@ public partial class ModEditWindow
return newValue != currentValue; return newValue != currentValue;
} }
private const byte CurrentManipulationVersion = 0;
private static void CopyToClipboardButton( string tooltip, Vector2 iconSize, IEnumerable< MetaManipulation > manipulations ) private static void CopyToClipboardButton( string tooltip, Vector2 iconSize, IEnumerable< MetaManipulation > manipulations )
{ {
if( !ImGuiUtil.DrawDisabledButton( FontAwesomeIcon.Clipboard.ToIconString(), iconSize, tooltip, false, true ) ) if( !ImGuiUtil.DrawDisabledButton( FontAwesomeIcon.Clipboard.ToIconString(), iconSize, tooltip, false, true ) )
@ -865,7 +863,7 @@ public partial class ModEditWindow
return; return;
} }
var text = Functions.ToCompressedBase64( manipulations, CurrentManipulationVersion ); var text = Functions.ToCompressedBase64( manipulations, MetaManipulation.CurrentVersion );
if( text.Length > 0 ) if( text.Length > 0 )
{ {
ImGui.SetClipboardText( text ); ImGui.SetClipboardText( text );
@ -878,7 +876,7 @@ public partial class ModEditWindow
{ {
var clipboard = ImGui.GetClipboardText(); var clipboard = ImGui.GetClipboardText();
var version = Functions.FromCompressedBase64< MetaManipulation[] >( clipboard, out var manips ); var version = Functions.FromCompressedBase64< MetaManipulation[] >( clipboard, out var manips );
if( version == CurrentManipulationVersion && manips != null ) if( version == MetaManipulation.CurrentVersion && manips != null )
{ {
foreach( var manip in manips ) foreach( var manip in manips )
{ {
@ -897,7 +895,7 @@ public partial class ModEditWindow
{ {
var clipboard = ImGui.GetClipboardText(); var clipboard = ImGui.GetClipboardText();
var version = Functions.FromCompressedBase64< MetaManipulation[] >( clipboard, out var manips ); var version = Functions.FromCompressedBase64< MetaManipulation[] >( clipboard, out var manips );
if( version == CurrentManipulationVersion && manips != null ) if( version == MetaManipulation.CurrentVersion && manips != null )
{ {
_editor!.Meta.Clear(); _editor!.Meta.Clear();
foreach( var manip in manips ) foreach( var manip in manips )

View file

@ -14,6 +14,7 @@ using System.Collections.Concurrent;
using System.IO; using System.IO;
using System.Linq; using System.Linq;
using System.Numerics; using System.Numerics;
using OtterGui.Classes;
namespace Penumbra.UI.Classes; namespace Penumbra.UI.Classes;
@ -286,14 +287,14 @@ public sealed partial class ModFileSystemSelector : FileSystemSelector< Mod, Mod
private void DeleteModButton( Vector2 size ) private void DeleteModButton( Vector2 size )
{ {
var keys = ImGui.GetIO().KeyCtrl && ImGui.GetIO().KeyShift; 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 Control and Shift 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 )

View file

@ -23,13 +23,13 @@ public partial class ConfigWindow
private void DrawChangedItemTab() private void DrawChangedItemTab()
{ {
// Functions in here for less pollution. // Functions in here for less pollution.
bool FilterChangedItem( KeyValuePair< string, (SingleArray< Mod >, object?) > item ) bool FilterChangedItem( KeyValuePair< string, (SingleArray< IMod >, object?) > item )
=> ( _changedItemFilter.IsEmpty => ( _changedItemFilter.IsEmpty
|| ChangedItemName( item.Key, item.Value.Item2 ) || ChangedItemName( item.Key, item.Value.Item2 )
.Contains( _changedItemFilter.Lower, StringComparison.InvariantCultureIgnoreCase ) ) .Contains( _changedItemFilter.Lower, StringComparison.InvariantCultureIgnoreCase ) )
&& ( _changedItemModFilter.IsEmpty || item.Value.Item1.Any( m => m.Name.Contains( _changedItemModFilter ) ) ); && ( _changedItemModFilter.IsEmpty || item.Value.Item1.Any( m => m.Name.Contains( _changedItemModFilter ) ) );
void DrawChangedItemColumn( KeyValuePair< string, (SingleArray< Mod >, object?) > item ) void DrawChangedItemColumn( KeyValuePair< string, (SingleArray< IMod >, object?) > item )
{ {
ImGui.TableNextColumn(); ImGui.TableNextColumn();
DrawChangedItem( item.Key, item.Value.Item2, false ); DrawChangedItem( item.Key, item.Value.Item2, false );

View file

@ -1,4 +1,5 @@
using System; using System;
using System.Collections.Generic;
using System.IO; using System.IO;
using System.Linq; using System.Linq;
using System.Numerics; using System.Numerics;
@ -10,8 +11,10 @@ using ImGuiNET;
using OtterGui; using OtterGui;
using OtterGui.Raii; using OtterGui.Raii;
using Penumbra.Api; using Penumbra.Api;
using Penumbra.Collections;
using Penumbra.Interop.Loader; using Penumbra.Interop.Loader;
using Penumbra.Interop.Structs; using Penumbra.Interop.Structs;
using Penumbra.Mods;
using CharacterUtility = Penumbra.Interop.CharacterUtility; using CharacterUtility = Penumbra.Interop.CharacterUtility;
namespace Penumbra.UI; namespace Penumbra.UI;
@ -413,11 +416,84 @@ public partial class ConfigWindow
foreach( var provider in ipc.GetType().GetFields( BindingFlags.Instance | BindingFlags.NonPublic ) ) foreach( var provider in ipc.GetType().GetFields( BindingFlags.Instance | BindingFlags.NonPublic ) )
{ {
var value = provider.GetValue( ipc ); var value = provider.GetValue( ipc );
if( value != null && dict.TryGetValue( "Label" + provider.Name, out var label )) if( value != null && dict.TryGetValue( "Label" + provider.Name, out var label ) )
{ {
ImGui.TextUnformatted( label ); ImGui.TextUnformatted( label );
} }
} }
using( var collTree = ImRaii.TreeNode( "Collections" ) )
{
if( collTree )
{
using var table = ImRaii.Table( "##collTree", 4 );
if( table )
{
foreach( var (character, collection) in Penumbra.TempMods.Collections )
{
ImGui.TableNextColumn();
ImGui.TextUnformatted( character );
ImGui.TableNextColumn();
ImGui.TextUnformatted( collection.Name );
ImGui.TableNextColumn();
ImGui.TextUnformatted( collection.ResolvedFiles.Count.ToString() );
ImGui.TableNextColumn();
ImGui.TextUnformatted( collection.MetaCache?.Count.ToString() ?? "0" );
}
}
}
}
using( var modTree = ImRaii.TreeNode( "Mods" ) )
{
if( modTree )
{
using var table = ImRaii.Table( "##modTree", 5 );
void PrintList( string collectionName, IReadOnlyList< Mod.TemporaryMod > list )
{
foreach( var mod in list )
{
ImGui.TableNextColumn();
ImGui.TextUnformatted( mod.Name );
ImGui.TableNextColumn();
ImGui.TextUnformatted( mod.Priority.ToString() );
ImGui.TableNextColumn();
ImGui.TextUnformatted( collectionName );
ImGui.TableNextColumn();
ImGui.TextUnformatted( mod.Default.Files.Count.ToString() );
if( ImGui.IsItemHovered() )
{
using var tt = ImRaii.Tooltip();
foreach( var (path, file) in mod.Default.Files )
{
ImGui.TextUnformatted( $"{path} -> {file}" );
}
}
ImGui.TableNextColumn();
ImGui.TextUnformatted( mod.TotalManipulations.ToString() );
if( ImGui.IsItemHovered() )
{
using var tt = ImRaii.Tooltip();
foreach( var manip in mod.Default.Manipulations )
{
ImGui.TextUnformatted( manip.ToString() );
}
}
}
}
if( table )
{
PrintList( "All", Penumbra.TempMods.ModsForAllCollections );
foreach( var (collection, list) in Penumbra.TempMods.Mods )
{
PrintList( collection.Name, list );
}
}
}
}
} }
// Helper to print a property and its value in a 2-column table. // Helper to print a property and its value in a 2-column table.

View file

@ -112,7 +112,7 @@ public partial class ConfigWindow
{ {
// We can treat all meta manipulations the same, // We can treat all meta manipulations the same,
// we are only really interested in their ToString function here. // we are only really interested in their ToString function here.
static (object, Mod) Convert< T >( KeyValuePair< T, Mod > kvp ) static (object, IMod) Convert< T >( KeyValuePair< T, IMod > kvp )
=> ( kvp.Key!, kvp.Value ); => ( kvp.Key!, kvp.Value );
var it = m.Cmp.Manipulations.Select( Convert ) var it = m.Cmp.Manipulations.Select( Convert )
@ -183,7 +183,7 @@ public partial class ConfigWindow
} }
// Draw a line for a unfiltered/unconverted manipulation and mod-index pair. // Draw a line for a unfiltered/unconverted manipulation and mod-index pair.
private static void DrawLine( (object, Mod) pair ) private static void DrawLine( (object, IMod) pair )
{ {
var (manipulation, mod) = pair; var (manipulation, mod) = pair;
ImGui.TableNextColumn(); ImGui.TableNextColumn();

View file

@ -104,7 +104,7 @@ public partial class ConfigWindow
_ => throw new ArgumentOutOfRangeException( nameof( type ), type, null ), _ => throw new ArgumentOutOfRangeException( nameof( type ), type, null ),
}; };
using var combo = ImRaii.Combo( label, current.Name ); using var combo = ImRaii.Combo( label, current.Name );
if( combo ) if( combo )
{ {
foreach( var collection in Penumbra.CollectionManager.GetEnumeratorWithEmpty().Skip( withEmpty ? 0 : 1 ).OrderBy( c => c.Name ) ) foreach( var collection in Penumbra.CollectionManager.GetEnumeratorWithEmpty().Skip( withEmpty ? 0 : 1 ).OrderBy( c => c.Name ) )
@ -128,20 +128,23 @@ public partial class ConfigWindow
if( Functions.GetDownloadsFolder( out var downloadsFolder ) ) if( Functions.GetDownloadsFolder( out var downloadsFolder ) )
{ {
fileManager.CustomSideBarItems.Add( ("Downloads", downloadsFolder, FontAwesomeIcon.Download, -1) ); fileManager.CustomSideBarItems.Add( ( "Downloads", downloadsFolder, FontAwesomeIcon.Download, -1 ) );
} }
if( Functions.GetQuickAccessFolders( out var folders ) ) if( Functions.GetQuickAccessFolders( out var folders ) )
{ {
foreach( var ((name, path), idx) in folders.WithIndex() ) foreach( var ((name, path), idx) in folders.WithIndex() )
{ {
fileManager.CustomSideBarItems.Add( ($"{name}##{idx}", path, FontAwesomeIcon.Folder, -1) ); fileManager.CustomSideBarItems.Add( ( $"{name}##{idx}", path, FontAwesomeIcon.Folder, -1 ) );
} }
} }
// Add Penumbra Root. This is not updated if the root changes right now.
fileManager.CustomSideBarItems.Add( ("Root Directory", Penumbra.Config.ModDirectory, FontAwesomeIcon.Gamepad, 0) );
// Remove Videos and Music. // Remove Videos and Music.
fileManager.CustomSideBarItems.Add( ("Videos", string.Empty, 0, -1) ); fileManager.CustomSideBarItems.Add( ( "Videos", string.Empty, 0, -1 ) );
fileManager.CustomSideBarItems.Add( ("Music", string.Empty, 0, -1) ); fileManager.CustomSideBarItems.Add( ( "Music", string.Empty, 0, -1 ) );
return fileManager; return fileManager;
} }

View file

@ -128,16 +128,19 @@ public partial class ConfigWindow
foreach( var conflict in Penumbra.CollectionManager.Current.Conflicts( _mod ) ) foreach( var conflict in Penumbra.CollectionManager.Current.Conflicts( _mod ) )
{ {
if( ImGui.Selectable( conflict.Mod2.Name ) ) if( ImGui.Selectable( conflict.Mod2.Name ) && conflict.Mod2 is Mod mod )
{ {
_window._selector.SelectByValue( conflict.Mod2 ); _window._selector.SelectByValue( mod );
} }
ImGui.SameLine(); ImGui.SameLine();
using( var color = ImRaii.PushColor( ImGuiCol.Text, using( var color = ImRaii.PushColor( ImGuiCol.Text,
conflict.HasPriority ? ColorId.HandledConflictMod.Value() : ColorId.ConflictingMod.Value() ) ) conflict.HasPriority ? ColorId.HandledConflictMod.Value() : ColorId.ConflictingMod.Value() ) )
{ {
ImGui.TextUnformatted( $"(Priority {Penumbra.CollectionManager.Current[ conflict.Mod2.Index ].Settings!.Priority})" ); var priority = conflict.Mod2.Index < 0
? conflict.Mod2.Priority
: Penumbra.CollectionManager.Current[conflict.Mod2.Index].Settings!.Priority;
ImGui.TextUnformatted( $"(Priority {priority})" );
} }
using var indent = ImRaii.PushIndent( 30f ); using var indent = ImRaii.PushIndent( 30f );

View file

@ -6,6 +6,7 @@ using ImGuiNET;
using OtterGui; using OtterGui;
using OtterGui.Filesystem; using OtterGui.Filesystem;
using OtterGui.Raii; using OtterGui.Raii;
using OtterGui.Widgets;
namespace Penumbra.UI; namespace Penumbra.UI;
@ -89,6 +90,15 @@ public partial class ConfigWindow
Penumbra.Config.OpenFoldersByDefault = v; Penumbra.Config.OpenFoldersByDefault = v;
_window._selector.SetFilterDirty(); _window._selector.SetFilterDirty();
} ); } );
Widget.DoubleModifierSelector( "Mod Deletion Modifier",
"A modifier you need to hold while clicking the Delete Mod button for it to take effect.", _window._inputTextWidth.X,
Penumbra.Config.DeleteModModifier,
v =>
{
Penumbra.Config.DeleteModModifier = v;
Penumbra.Config.Save();
} );
ImGui.Dummy( _window._defaultSpace ); ImGui.Dummy( _window._defaultSpace );
Checkbox( "Always Open Import at Default Directory", Checkbox( "Always Open Import at Default Directory",
"Open the import window at the location specified here every time, forgetting your previous path.", "Open the import window at the location specified here every time, forgetting your previous path.",

View file

@ -55,32 +55,39 @@ public sealed partial class ConfigWindow : Window, IDisposable
{ {
if( Penumbra.ImcExceptions > 0 ) if( Penumbra.ImcExceptions > 0 )
{ {
using var color = ImRaii.PushColor( ImGuiCol.Text, Colors.RegexWarningBorder ); DrawProblemWindow( $"There were {Penumbra.ImcExceptions} errors while trying to load IMC files from the game data.\n"
ImGui.NewLine();
ImGui.NewLine();
ImGui.TextWrapped( $"There were {Penumbra.ImcExceptions} 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." ); + "Please use the Launcher's Repair Game Files function to repair your client installation." );
color.Pop();
ImGui.NewLine();
ImGui.NewLine();
SettingsTab.DrawDiscordButton( 0 );
ImGui.SameLine();
SettingsTab.DrawSupportButton();
return;
} }
else if( MainClass.IsNotInstalledPenumbra )
using var bar = ImRaii.TabBar( string.Empty, ImGuiTabBarFlags.NoTooltip ); {
SetupSizes(); DrawProblemWindow(
_settingsTab.Draw(); $"You are loading a release version of Penumbra from \"{Dalamud.PluginInterface.AssemblyLocation.Directory?.FullName ?? "Unknown"}\" instead of the installedPlugins directory.\n\n"
DrawModsTab(); + "You should not install Penumbra manually, but rather add the plugin repository under settings and then install it via the plugin installer.\n\n"
_collectionsTab.Draw(); + "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"
DrawChangedItemTab(); + "If you are developing for Penumbra and see this, you should compile your version in debug mode to avoid it." );
_effectiveTab.Draw(); }
_debugTab.Draw(); else if( MainClass.DevPenumbraExists )
_resourceTab.Draw(); {
DrawProblemWindow(
$"You are loading a installed version of Penumbra from \"{Dalamud.PluginInterface.AssemblyLocation.Directory?.FullName ?? "Unknown"}\", "
+ "but also still have some remnants of a custom install of Penumbra in your devPlugins folder.\n\n"
+ "This can cause some issues, so please go to your \"%%appdata%%\\XIVLauncher\\devPlugins\" folder and delete the Penumbra folder from there.\n\n"
+ "If you are developing for Penumbra, try to avoid mixing versions. This warning will not appear if compiled in Debug mode." );
}
else
{
using var bar = ImRaii.TabBar( string.Empty, ImGuiTabBarFlags.NoTooltip );
SetupSizes();
_settingsTab.Draw();
DrawModsTab();
_collectionsTab.Draw();
DrawChangedItemTab();
_effectiveTab.Draw();
_debugTab.Draw();
_resourceTab.Draw();
}
} }
catch( Exception e ) catch( Exception e )
{ {
@ -88,6 +95,21 @@ public sealed partial class ConfigWindow : Window, IDisposable
} }
} }
private static void DrawProblemWindow( string text )
{
using var color = ImRaii.PushColor( ImGuiCol.Text, Colors.RegexWarningBorder );
ImGui.NewLine();
ImGui.NewLine();
ImGui.TextWrapped( text );
color.Pop();
ImGui.NewLine();
ImGui.NewLine();
SettingsTab.DrawDiscordButton( 0 );
ImGui.SameLine();
SettingsTab.DrawSupportButton();
}
public void Dispose() public void Dispose()
{ {
_selector.Dispose(); _selector.Dispose();

View file

@ -4,8 +4,8 @@
"Name": "Penumbra", "Name": "Penumbra",
"Description": "Runtime mod loader and manager.", "Description": "Runtime mod loader and manager.",
"InternalName": "Penumbra", "InternalName": "Penumbra",
"AssemblyVersion": "0.5.1.0", "AssemblyVersion": "0.5.1.2",
"TestingAssemblyVersion": "0.5.1.0", "TestingAssemblyVersion": "0.5.1.2",
"RepoUrl": "https://github.com/xivdev/Penumbra", "RepoUrl": "https://github.com/xivdev/Penumbra",
"ApplicableVersion": "any", "ApplicableVersion": "any",
"DalamudApiLevel": 6, "DalamudApiLevel": 6,
@ -14,9 +14,9 @@
"DownloadCount": 0, "DownloadCount": 0,
"LastUpdate": 0, "LastUpdate": 0,
"LoadPriority": 69420, "LoadPriority": 69420,
"DownloadLinkInstall": "https://github.com/xivdev/Penumbra/releases/download/0.5.1.0/Penumbra.zip", "DownloadLinkInstall": "https://github.com/xivdev/Penumbra/releases/download/0.5.1.2/Penumbra.zip",
"DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/0.5.1.0/Penumbra.zip", "DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/0.5.1.2/Penumbra.zip",
"DownloadLinkUpdate": "https://github.com/xivdev/Penumbra/releases/download/0.5.1.0/Penumbra.zip", "DownloadLinkUpdate": "https://github.com/xivdev/Penumbra/releases/download/0.5.1.2/Penumbra.zip",
"IconUrl": "https://raw.githubusercontent.com/xivdev/Penumbra/master/images/icon.png" "IconUrl": "https://raw.githubusercontent.com/xivdev/Penumbra/master/images/icon.png"
} }
] ]