Add local data, favorites and tags.

This commit is contained in:
Ottermandias 2022-10-19 01:01:40 +02:00
parent b9662e39a9
commit ccfc05f2b2
19 changed files with 481 additions and 94 deletions

@ -1 +1 @@
Subproject commit 1e0d04b90043faad979c3e7316a733870eb16108 Subproject commit 0d2284a82504aac0bff797fa3355f750a3e68834

View file

@ -58,6 +58,7 @@ public partial class Mod
return; return;
} }
MoveDataFile( oldDirectory, BasePath );
new ModBackup( mod ).Move( null, dir.Name ); new ModBackup( mod ).Move( null, dir.Name );
dir.Refresh(); dir.Refresh();
@ -69,9 +70,9 @@ public partial class Mod
} }
ModPathChanged.Invoke( ModPathChangeType.Moved, mod, oldDirectory, BasePath ); ModPathChanged.Invoke( ModPathChangeType.Moved, mod, oldDirectory, BasePath );
if( metaChange != MetaChangeType.None ) if( metaChange != ModDataChangeType.None )
{ {
ModMetaChanged?.Invoke( metaChange, mod, oldName ); ModDataChanged?.Invoke( metaChange, mod, oldName );
} }
} }
@ -94,9 +95,9 @@ public partial class Mod
} }
ModPathChanged.Invoke( ModPathChangeType.Reloaded, mod, mod.ModPath, mod.ModPath ); ModPathChanged.Invoke( ModPathChangeType.Reloaded, mod, mod.ModPath, mod.ModPath );
if( metaChange != MetaChangeType.None ) if( metaChange != ModDataChangeType.None )
{ {
ModMetaChanged?.Invoke( metaChange, mod, oldName ); ModDataChanged?.Invoke( metaChange, mod, oldName );
} }
} }
@ -211,6 +212,13 @@ public partial class Mod
break; break;
case ModPathChangeType.Deleted: case ModPathChangeType.Deleted:
NewMods.Remove( mod ); NewMods.Remove( mod );
break;
case ModPathChangeType.Moved:
if( oldDirectory != null && newDirectory != null )
{
MoveDataFile( oldDirectory, newDirectory );
}
break; break;
} }
} }

View file

@ -0,0 +1,75 @@
using System;
using System.Collections.Generic;
using System.Linq;
using ImGuiScene;
namespace Penumbra.Mods;
public sealed partial class Mod
{
public partial class Manager
{
public void ChangeModFavorite( Index idx, bool state )
{
var mod = this[ idx ];
if( mod.Favorite != state )
{
mod.Favorite = state;
mod.SaveLocalData();
ModDataChanged?.Invoke( ModDataChangeType.Favorite, mod, null );
}
}
public void ChangeModNote( Index idx, string newNote )
{
var mod = this[ idx ];
if( mod.Note != newNote )
{
mod.Note = newNote;
mod.SaveLocalData();
ModDataChanged?.Invoke( ModDataChangeType.Favorite, mod, null );
}
}
private void ChangeTag( Index idx, int tagIdx, string newTag, bool local )
{
var mod = this[ idx ];
var which = local ? mod.LocalTags : mod.ModTags;
if( tagIdx < 0 || tagIdx > which.Count )
{
return;
}
ModDataChangeType flags = 0;
if( tagIdx == which.Count )
{
flags = mod.UpdateTags( local ? null : which.Append( newTag ), local ? which.Append( newTag ) : null );
}
else
{
var tmp = which.ToArray();
tmp[ tagIdx ] = newTag;
flags = mod.UpdateTags( local ? null : tmp, local ? tmp : null );
}
if( flags.HasFlag( ModDataChangeType.ModTags ) )
{
mod.SaveMeta();
}
if( flags.HasFlag( ModDataChangeType.LocalTags ) )
{
mod.SaveLocalData();
}
if( flags != 0 )
{
ModDataChanged?.Invoke( flags, mod, null );
}
}
public void ChangeLocalTag( Index idx, int tagIdx, string newTag )
=> ChangeTag( idx, tagIdx, newTag, true );
}
}

View file

@ -6,8 +6,8 @@ public sealed partial class Mod
{ {
public partial class Manager public partial class Manager
{ {
public delegate void ModMetaChangeDelegate( MetaChangeType type, Mod mod, string? oldName ); public delegate void ModDataChangeDelegate( ModDataChangeType type, Mod mod, string? oldName );
public event ModMetaChangeDelegate? ModMetaChanged; public event ModDataChangeDelegate? ModDataChanged;
public void ChangeModName( Index idx, string newName ) public void ChangeModName( Index idx, string newName )
{ {
@ -17,7 +17,7 @@ public sealed partial class Mod
var oldName = mod.Name; var oldName = mod.Name;
mod.Name = newName; mod.Name = newName;
mod.SaveMeta(); mod.SaveMeta();
ModMetaChanged?.Invoke( MetaChangeType.Name, mod, oldName.Text ); ModDataChanged?.Invoke( ModDataChangeType.Name, mod, oldName.Text );
} }
} }
@ -28,7 +28,7 @@ public sealed partial class Mod
{ {
mod.Author = newAuthor; mod.Author = newAuthor;
mod.SaveMeta(); mod.SaveMeta();
ModMetaChanged?.Invoke( MetaChangeType.Author, mod, null ); ModDataChanged?.Invoke( ModDataChangeType.Author, mod, null );
} }
} }
@ -39,7 +39,7 @@ public sealed partial class Mod
{ {
mod.Description = newDescription; mod.Description = newDescription;
mod.SaveMeta(); mod.SaveMeta();
ModMetaChanged?.Invoke( MetaChangeType.Description, mod, null ); ModDataChanged?.Invoke( ModDataChangeType.Description, mod, null );
} }
} }
@ -50,7 +50,7 @@ public sealed partial class Mod
{ {
mod.Version = newVersion; mod.Version = newVersion;
mod.SaveMeta(); mod.SaveMeta();
ModMetaChanged?.Invoke( MetaChangeType.Version, mod, null ); ModDataChanged?.Invoke( ModDataChangeType.Version, mod, null );
} }
} }
@ -61,8 +61,11 @@ public sealed partial class Mod
{ {
mod.Website = newWebsite; mod.Website = newWebsite;
mod.SaveMeta(); mod.SaveMeta();
ModMetaChanged?.Invoke( MetaChangeType.Website, mod, null ); ModDataChanged?.Invoke( ModDataChangeType.Website, mod, null );
} }
} }
public void ChangeModTag( Index idx, int tagIdx, string newTag )
=> ChangeTag( idx, tagIdx, newTag, false );
} }
} }

View file

@ -47,21 +47,23 @@ public partial class Mod
return mod; return mod;
} }
private bool Reload( bool incorporateMetaChanges, out MetaChangeType metaChange ) private bool Reload( bool incorporateMetaChanges, out ModDataChangeType modDataChange )
{ {
metaChange = MetaChangeType.Deletion; modDataChange = ModDataChangeType.Deletion;
ModPath.Refresh(); ModPath.Refresh();
if( !ModPath.Exists ) if( !ModPath.Exists )
{ {
return false; return false;
} }
metaChange = LoadMeta(); modDataChange = LoadMeta();
if( metaChange.HasFlag( MetaChangeType.Deletion ) || Name.Length == 0 ) if( modDataChange.HasFlag( ModDataChangeType.Deletion ) || Name.Length == 0 )
{ {
return false; return false;
} }
LoadLocalData();
LoadDefaultOption(); LoadDefaultOption();
LoadAllGroups(); LoadAllGroups();
if( incorporateMetaChanges ) if( incorporateMetaChanges )

View file

@ -0,0 +1,167 @@
using Newtonsoft.Json.Linq;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using Newtonsoft.Json;
namespace Penumbra.Mods;
public sealed partial class Mod
{
public static DirectoryInfo LocalDataDirectory
=> new(Path.Combine( Dalamud.PluginInterface.ConfigDirectory.FullName, "mod_data" ));
public long ImportDate { get; private set; } = DateTimeOffset.UnixEpoch.ToUnixTimeMilliseconds();
public IReadOnlyList< string > LocalTags { get; private set; } = Array.Empty< string >();
public string AllTagsLower { get; private set; } = string.Empty;
public string Note { get; private set; } = string.Empty;
public bool Favorite { get; private set; } = false;
private FileInfo LocalDataFile
=> new(Path.Combine( Dalamud.PluginInterface.ConfigDirectory.FullName, "mod_data", $"{ModPath.Name}.json" ));
private ModDataChangeType LoadLocalData()
{
var dataFile = LocalDataFile;
var importDate = 0L;
var localTags = Enumerable.Empty< string >();
var favorite = false;
var note = string.Empty;
var save = true;
if( File.Exists( dataFile.FullName ) )
{
save = false;
try
{
var text = File.ReadAllText( dataFile.FullName );
var json = JObject.Parse( text );
importDate = json[ nameof( ImportDate ) ]?.Value< long >() ?? importDate;
favorite = json[ nameof( Favorite ) ]?.Value< bool >() ?? favorite;
note = json[ nameof( Note ) ]?.Value< string >() ?? note;
localTags = json[ nameof( LocalTags ) ]?.Values< string >().OfType< string >() ?? localTags;
}
catch( Exception e )
{
Penumbra.Log.Error( $"Could not load local mod data:\n{e}" );
}
}
if( importDate == 0 )
{
importDate = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds();
}
ModDataChangeType changes = 0;
if( ImportDate != importDate )
{
ImportDate = importDate;
changes |= ModDataChangeType.ImportDate;
}
changes |= UpdateTags( null, localTags );
if( Favorite != favorite )
{
Favorite = favorite;
changes |= ModDataChangeType.Favorite;
}
if( Note != note )
{
Note = note;
changes |= ModDataChangeType.Note;
}
if( save )
{
SaveLocalDataFile();
}
return changes;
}
private void SaveLocalData()
=> Penumbra.Framework.RegisterDelayed( nameof( SaveLocalData ) + ModPath.Name, SaveLocalDataFile );
private void SaveLocalDataFile()
{
var dataFile = LocalDataFile;
try
{
var jObject = new JObject
{
{ nameof( FileVersion ), JToken.FromObject( FileVersion ) },
{ nameof( ImportDate ), JToken.FromObject( ImportDate ) },
{ nameof( LocalTags ), JToken.FromObject( LocalTags ) },
{ nameof( Note ), JToken.FromObject( Note ) },
{ nameof( Favorite ), JToken.FromObject( Favorite ) },
};
dataFile.Directory!.Create();
File.WriteAllText( dataFile.FullName, jObject.ToString( Formatting.Indented ) );
}
catch( Exception e )
{
Penumbra.Log.Error( $"Could not write local data file for mod {Name} to {dataFile.FullName}:\n{e}" );
}
}
private static void MoveDataFile( DirectoryInfo oldMod, DirectoryInfo newMod )
{
var oldFile = Path.Combine( Dalamud.PluginInterface.ConfigDirectory.FullName, "mod_data", $"{oldMod.Name}.json" );
var newFile = Path.Combine( Dalamud.PluginInterface.ConfigDirectory.FullName, "mod_data", $"{newMod.Name}.json" );
if( File.Exists( oldFile ) )
{
try
{
File.Move( oldFile, newFile, true );
}
catch( Exception e )
{
Penumbra.Log.Error( $"Could not move local data file {oldFile} to {newFile}:\n{e}" );
}
}
}
private ModDataChangeType UpdateTags( IEnumerable< string >? newModTags, IEnumerable< string >? newLocalTags )
{
if( newModTags == null && newLocalTags == null )
{
return 0;
}
ModDataChangeType type = 0;
if( newModTags != null )
{
var modTags = newModTags.Where( t => t.Length > 0 ).Distinct().ToArray();
if( !modTags.SequenceEqual( ModTags ) )
{
newLocalTags ??= LocalTags;
ModTags = modTags;
type |= ModDataChangeType.ModTags;
}
}
if( newLocalTags != null )
{
var localTags = newLocalTags!.Where( t => t.Length > 0 && !ModTags.Contains( t ) ).Distinct().ToArray();
if( !localTags.SequenceEqual( LocalTags ) )
{
LocalTags = localTags;
type |= ModDataChangeType.LocalTags;
}
}
if( type != 0 )
{
AllTagsLower = string.Join( '\0', ModTags.Concat( LocalTags ).Select( s => s.ToLowerInvariant() ) );
}
return type;
}
}

View file

@ -16,7 +16,20 @@ public sealed partial class Mod
private static class Migration private static class Migration
{ {
public static bool Migrate( Mod mod, JObject json ) public static bool Migrate( Mod mod, JObject json )
=> MigrateV0ToV1( mod, json ) || MigrateV1ToV2( mod ); => MigrateV0ToV1( mod, json ) || MigrateV1ToV2( mod ) || MigrateV2ToV3( mod );
private static bool MigrateV2ToV3( Mod mod )
{
if( mod.FileVersion > 2 )
{
return false;
}
// Remove import time.
mod.FileVersion = 3;
mod.SaveMeta();
return true;
}
private static bool MigrateV1ToV2( Mod mod ) private static bool MigrateV1ToV2( Mod mod )
{ {
@ -56,8 +69,8 @@ public sealed partial class Mod
var swaps = json[ "FileSwaps" ]?.ToObject< Dictionary< Utf8GamePath, FullPath > >() var swaps = json[ "FileSwaps" ]?.ToObject< Dictionary< Utf8GamePath, FullPath > >()
?? new Dictionary< Utf8GamePath, FullPath >(); ?? new Dictionary< Utf8GamePath, FullPath >();
var groups = json[ "Groups" ]?.ToObject< Dictionary< string, OptionGroupV0 > >() ?? new Dictionary< string, OptionGroupV0 >(); var groups = json[ "Groups" ]?.ToObject< Dictionary< string, OptionGroupV0 > >() ?? new Dictionary< string, OptionGroupV0 >();
var priority = 1; var priority = 1;
var seenMetaFiles = new HashSet< FullPath >(); var seenMetaFiles = new HashSet< FullPath >();
foreach( var group in groups.Values ) foreach( var group in groups.Values )
{ {
@ -187,7 +200,7 @@ public sealed partial class Mod
private static SubMod SubModFromOption( Mod mod, OptionV0 option, HashSet< FullPath > seenMetaFiles ) private static SubMod SubModFromOption( Mod mod, OptionV0 option, HashSet< FullPath > seenMetaFiles )
{ {
var subMod = new SubMod(mod) { Name = option.OptionName }; var subMod = new SubMod( mod ) { Name = option.OptionName };
AddFilesToSubMod( subMod, mod.ModPath, option, seenMetaFiles ); AddFilesToSubMod( subMod, mod.ModPath, option, seenMetaFiles );
subMod.IncorporateMetaChanges( mod.ModPath, false ); subMod.IncorporateMetaChanges( mod.ModPath, false );
return subMod; return subMod;

View file

@ -1,5 +1,7 @@
using System; using System;
using System.Collections.Generic;
using System.IO; using System.IO;
using System.Linq;
using Newtonsoft.Json; using Newtonsoft.Json;
using Newtonsoft.Json.Linq; using Newtonsoft.Json.Linq;
using OtterGui.Classes; using OtterGui.Classes;
@ -7,17 +9,21 @@ using OtterGui.Classes;
namespace Penumbra.Mods; namespace Penumbra.Mods;
[Flags] [Flags]
public enum MetaChangeType : ushort public enum ModDataChangeType : ushort
{ {
None = 0x00, None = 0x0000,
Name = 0x01, Name = 0x0001,
Author = 0x02, Author = 0x0002,
Description = 0x04, Description = 0x0004,
Version = 0x08, Version = 0x0008,
Website = 0x10, Website = 0x0010,
Deletion = 0x20, Deletion = 0x0020,
Migration = 0x40, Migration = 0x0040,
ImportDate = 0x80, ModTags = 0x0080,
ImportDate = 0x0100,
Favorite = 0x0200,
LocalTags = 0x0400,
Note = 0x0800,
} }
public sealed partial class Mod public sealed partial class Mod
@ -29,25 +35,25 @@ public sealed partial class Mod
Priority = int.MaxValue, Priority = int.MaxValue,
}; };
public const uint CurrentFileVersion = 1; public const uint CurrentFileVersion = 3;
public uint FileVersion { get; private set; } = CurrentFileVersion; public uint FileVersion { get; private set; } = CurrentFileVersion;
public LowerString Name { get; private set; } = "New Mod"; public LowerString Name { get; private set; } = "New Mod";
public LowerString Author { get; private set; } = LowerString.Empty; public LowerString Author { get; private set; } = LowerString.Empty;
public string Description { get; private set; } = string.Empty; public string Description { get; private set; } = string.Empty;
public string Version { get; private set; } = string.Empty; public string Version { get; private set; } = string.Empty;
public string Website { get; private set; } = string.Empty; public string Website { get; private set; } = string.Empty;
public long ImportDate { get; private set; } = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds(); public IReadOnlyList< string > ModTags { get; private set; } = Array.Empty< string >();
internal FileInfo MetaFile internal FileInfo MetaFile
=> new(Path.Combine( ModPath.FullName, "meta.json" )); => new(Path.Combine( ModPath.FullName, "meta.json" ));
private MetaChangeType LoadMeta() private ModDataChangeType LoadMeta()
{ {
var metaFile = MetaFile; var metaFile = MetaFile;
if( !File.Exists( metaFile.FullName ) ) if( !File.Exists( metaFile.FullName ) )
{ {
Penumbra.Log.Debug( $"No mod meta found for {ModPath.Name}." ); Penumbra.Log.Debug( $"No mod meta found for {ModPath.Name}." );
return MetaChangeType.Deletion; return ModDataChangeType.Deletion;
} }
try try
@ -61,36 +67,37 @@ public sealed partial class Mod
var newVersion = json[ nameof( Version ) ]?.Value< string >() ?? string.Empty; var newVersion = json[ nameof( Version ) ]?.Value< string >() ?? string.Empty;
var newWebsite = json[ nameof( Website ) ]?.Value< string >() ?? string.Empty; var newWebsite = json[ nameof( Website ) ]?.Value< string >() ?? string.Empty;
var newFileVersion = json[ nameof( FileVersion ) ]?.Value< uint >() ?? 0; var newFileVersion = json[ nameof( FileVersion ) ]?.Value< uint >() ?? 0;
var importDate = json[ nameof( ImportDate ) ]?.Value< long >() ?? DateTimeOffset.UtcNow.ToUnixTimeMilliseconds(); var importDate = json[ nameof( ImportDate ) ]?.Value< long >();
var modTags = json[ nameof( ModTags ) ]?.Values< string >().OfType< string >();
MetaChangeType changes = 0; ModDataChangeType changes = 0;
if( Name != newName ) if( Name != newName )
{ {
changes |= MetaChangeType.Name; changes |= ModDataChangeType.Name;
Name = newName; Name = newName;
} }
if( Author != newAuthor ) if( Author != newAuthor )
{ {
changes |= MetaChangeType.Author; changes |= ModDataChangeType.Author;
Author = newAuthor; Author = newAuthor;
} }
if( Description != newDescription ) if( Description != newDescription )
{ {
changes |= MetaChangeType.Description; changes |= ModDataChangeType.Description;
Description = newDescription; Description = newDescription;
} }
if( Version != newVersion ) if( Version != newVersion )
{ {
changes |= MetaChangeType.Version; changes |= ModDataChangeType.Version;
Version = newVersion; Version = newVersion;
} }
if( Website != newWebsite ) if( Website != newWebsite )
{ {
changes |= MetaChangeType.Website; changes |= ModDataChangeType.Website;
Website = newWebsite; Website = newWebsite;
} }
@ -99,22 +106,24 @@ public sealed partial class Mod
FileVersion = newFileVersion; FileVersion = newFileVersion;
if( Migration.Migrate( this, json ) ) if( Migration.Migrate( this, json ) )
{ {
changes |= MetaChangeType.Migration; changes |= ModDataChangeType.Migration;
} }
} }
if( ImportDate != importDate ) if( importDate != null && ImportDate != importDate.Value )
{ {
ImportDate = importDate; ImportDate = importDate.Value;
changes |= MetaChangeType.ImportDate; changes |= ModDataChangeType.ImportDate;
} }
changes |= UpdateTags( modTags, null );
return changes; return changes;
} }
catch( Exception e ) catch( Exception e )
{ {
Penumbra.Log.Error( $"Could not load mod meta:\n{e}" ); Penumbra.Log.Error( $"Could not load mod meta:\n{e}" );
return MetaChangeType.Deletion; return ModDataChangeType.Deletion;
} }
} }
@ -134,7 +143,7 @@ public sealed partial class Mod
{ nameof( Description ), JToken.FromObject( Description ) }, { nameof( Description ), JToken.FromObject( Description ) },
{ nameof( Version ), JToken.FromObject( Version ) }, { nameof( Version ), JToken.FromObject( Version ) },
{ nameof( Website ), JToken.FromObject( Website ) }, { nameof( Website ), JToken.FromObject( Website ) },
{ nameof( ImportDate ), JToken.FromObject( ImportDate ) }, { nameof( ModTags ), JToken.FromObject( ModTags ) },
}; };
File.WriteAllText( metaFile.FullName, jObject.ToString( Formatting.Indented ) ); File.WriteAllText( metaFile.FullName, jObject.ToString( Formatting.Indented ) );
} }

View file

@ -33,7 +33,7 @@ public sealed class ModFileSystem : FileSystem< Mod >, IDisposable
ret.Changed += ret.OnChange; ret.Changed += ret.OnChange;
Penumbra.ModManager.ModDiscoveryFinished += ret.Reload; Penumbra.ModManager.ModDiscoveryFinished += ret.Reload;
Penumbra.ModManager.ModMetaChanged += ret.OnMetaChange; Penumbra.ModManager.ModDataChanged += ret.OnDataChange;
Penumbra.ModManager.ModPathChanged += ret.OnModPathChange; Penumbra.ModManager.ModPathChanged += ret.OnModPathChange;
return ret; return ret;
@ -43,7 +43,7 @@ public sealed class ModFileSystem : FileSystem< Mod >, IDisposable
{ {
Penumbra.ModManager.ModPathChanged -= OnModPathChange; Penumbra.ModManager.ModPathChanged -= OnModPathChange;
Penumbra.ModManager.ModDiscoveryFinished -= Reload; Penumbra.ModManager.ModDiscoveryFinished -= Reload;
Penumbra.ModManager.ModMetaChanged -= OnMetaChange; Penumbra.ModManager.ModDataChanged -= OnDataChange;
} }
public struct ImportDate : ISortMode< Mod > public struct ImportDate : ISortMode< Mod >
@ -92,9 +92,9 @@ public sealed class ModFileSystem : FileSystem< Mod >, IDisposable
} }
// Update sort order when defaulted mod names change. // Update sort order when defaulted mod names change.
private void OnMetaChange( MetaChangeType type, Mod mod, string? oldName ) private void OnDataChange( ModDataChangeType type, Mod mod, string? oldName )
{ {
if( type.HasFlag( MetaChangeType.Name ) && oldName != null ) if( type.HasFlag( ModDataChangeType.Name ) && oldName != null )
{ {
var old = oldName.FixName(); var old = oldName.FixName();
if( Find( old, out var child ) && child is not Folder ) if( Find( old, out var child ) && child is not Folder )

View file

@ -454,6 +454,7 @@ public class Penumbra : IDalamudPlugin
var list = Directory.Exists( collectionDir ) var list = Directory.Exists( collectionDir )
? new DirectoryInfo( collectionDir ).EnumerateFiles( "*.json" ).ToList() ? new DirectoryInfo( collectionDir ).EnumerateFiles( "*.json" ).ToList()
: new List< FileInfo >(); : new List< FileInfo >();
list.AddRange( Mod.LocalDataDirectory.Exists ? Mod.LocalDataDirectory.EnumerateFiles( "*.json" ) : Enumerable.Empty< FileInfo >() );
list.Add( Dalamud.PluginInterface.ConfigFile ); list.Add( Dalamud.PluginInterface.ConfigFile );
list.Add( new FileInfo( ModFileSystem.ModFileSystemFile ) ); list.Add( new FileInfo( ModFileSystem.ModFileSystemFile ) );
list.Add( new FileInfo( ModCollection.Manager.ActiveCollectionFile ) ); list.Add( new FileInfo( ModCollection.Manager.ActiveCollectionFile ) );

View file

@ -29,8 +29,9 @@ public partial class ModFileSystemSelector
private void SetFilterTooltip() private void SetFilterTooltip()
{ {
FilterTooltip = "Filter mods for those where their full paths or names contain the given substring.\n" FilterTooltip = "Filter mods for those where their full paths or names contain the given substring.\n"
+ "Enter n:[string] to filter only for mod names and no paths.\n"
+ "Enter c:[string] to filter for mods changing specific items.\n" + "Enter c:[string] to filter for mods changing specific items.\n"
+ "Enter t:[string] to filter for mods set to specific tags.\n"
+ "Enter n:[string] to filter only for mod names and no paths.\n"
+ "Enter a:[string] to filter for mods by specific authors."; + "Enter a:[string] to filter for mods by specific authors.";
} }
@ -49,6 +50,8 @@ public partial class ModFileSystemSelector
'A' => filterValue.Length == 2 ? ( LowerString.Empty, -1 ) : ( new LowerString( filterValue[ 2.. ] ), 2 ), 'A' => filterValue.Length == 2 ? ( LowerString.Empty, -1 ) : ( new LowerString( filterValue[ 2.. ] ), 2 ),
'c' => filterValue.Length == 2 ? ( LowerString.Empty, -1 ) : ( new LowerString( filterValue[ 2.. ] ), 3 ), 'c' => filterValue.Length == 2 ? ( LowerString.Empty, -1 ) : ( new LowerString( filterValue[ 2.. ] ), 3 ),
'C' => filterValue.Length == 2 ? ( LowerString.Empty, -1 ) : ( new LowerString( filterValue[ 2.. ] ), 3 ), 'C' => filterValue.Length == 2 ? ( LowerString.Empty, -1 ) : ( new LowerString( filterValue[ 2.. ] ), 3 ),
't' => filterValue.Length == 2 ? ( LowerString.Empty, -1 ) : ( new LowerString( filterValue[ 2.. ] ), 4 ),
'T' => filterValue.Length == 2 ? ( LowerString.Empty, -1 ) : ( new LowerString( filterValue[ 2.. ] ), 4 ),
_ => ( new LowerString( filterValue ), 0 ), _ => ( new LowerString( filterValue ), 0 ),
}, },
_ => ( new LowerString( filterValue ), 0 ), _ => ( new LowerString( filterValue ), 0 ),
@ -96,7 +99,8 @@ public partial class ModFileSystemSelector
0 => !( leaf.FullName().Contains( _modFilter.Lower, IgnoreCase ) || mod.Name.Contains( _modFilter ) ), 0 => !( leaf.FullName().Contains( _modFilter.Lower, IgnoreCase ) || mod.Name.Contains( _modFilter ) ),
1 => !mod.Name.Contains( _modFilter ), 1 => !mod.Name.Contains( _modFilter ),
2 => !mod.Author.Contains( _modFilter ), 2 => !mod.Author.Contains( _modFilter ),
3 => !mod.LowerChangedItemsString.Contains( _modFilter.Lower, IgnoreCase ), 3 => !mod.LowerChangedItemsString.Contains( _modFilter.Lower ),
4 => !mod.AllTagsLower.Contains( _modFilter.Lower ),
_ => false, // Should never happen _ => false, // Should never happen
}; };
} }
@ -143,6 +147,13 @@ public partial class ModFileSystemSelector
return true; return true;
} }
// Handle Favoritism
if( !_stateFilter.HasFlag( ModFilter.Favorite ) && mod.Favorite
|| !_stateFilter.HasFlag( ModFilter.NotFavorite ) && !mod.Favorite )
{
return true;
}
// Handle Inheritance // Handle Inheritance
if( collection == Penumbra.CollectionManager.Current ) if( collection == Penumbra.CollectionManager.Current )
{ {

View file

@ -43,7 +43,7 @@ public sealed partial class ModFileSystemSelector : FileSystemSelector< Mod, Mod
Penumbra.CollectionManager.CollectionChanged += OnCollectionChange; Penumbra.CollectionManager.CollectionChanged += OnCollectionChange;
Penumbra.CollectionManager.Current.ModSettingChanged += OnSettingChange; Penumbra.CollectionManager.Current.ModSettingChanged += OnSettingChange;
Penumbra.CollectionManager.Current.InheritanceChanged += OnInheritanceChange; Penumbra.CollectionManager.Current.InheritanceChanged += OnInheritanceChange;
Penumbra.ModManager.ModMetaChanged += OnModMetaChange; Penumbra.ModManager.ModDataChanged += OnModDataChange;
Penumbra.ModManager.ModDiscoveryStarted += StoreCurrentSelection; Penumbra.ModManager.ModDiscoveryStarted += StoreCurrentSelection;
Penumbra.ModManager.ModDiscoveryFinished += RestoreLastSelection; Penumbra.ModManager.ModDiscoveryFinished += RestoreLastSelection;
OnCollectionChange( CollectionType.Current, null, Penumbra.CollectionManager.Current, null ); OnCollectionChange( CollectionType.Current, null, Penumbra.CollectionManager.Current, null );
@ -54,7 +54,7 @@ public sealed partial class ModFileSystemSelector : FileSystemSelector< Mod, Mod
base.Dispose(); base.Dispose();
Penumbra.ModManager.ModDiscoveryStarted -= StoreCurrentSelection; Penumbra.ModManager.ModDiscoveryStarted -= StoreCurrentSelection;
Penumbra.ModManager.ModDiscoveryFinished -= RestoreLastSelection; Penumbra.ModManager.ModDiscoveryFinished -= RestoreLastSelection;
Penumbra.ModManager.ModMetaChanged -= OnModMetaChange; Penumbra.ModManager.ModDataChanged -= OnModDataChange;
Penumbra.CollectionManager.Current.ModSettingChanged -= OnSettingChange; Penumbra.CollectionManager.Current.ModSettingChanged -= OnSettingChange;
Penumbra.CollectionManager.Current.InheritanceChanged -= OnInheritanceChange; Penumbra.CollectionManager.Current.InheritanceChanged -= OnInheritanceChange;
Penumbra.CollectionManager.CollectionChanged -= OnCollectionChange; Penumbra.CollectionManager.CollectionChanged -= OnCollectionChange;
@ -120,7 +120,7 @@ public sealed partial class ModFileSystemSelector : FileSystemSelector< Mod, Mod
var flags = selected ? ImGuiTreeNodeFlags.Selected | LeafFlags : LeafFlags; var flags = selected ? ImGuiTreeNodeFlags.Selected | LeafFlags : LeafFlags;
using var c = ImRaii.PushColor( ImGuiCol.Text, state.Color.Value() ); using var c = ImRaii.PushColor( ImGuiCol.Text, state.Color.Value() );
using var id = ImRaii.PushId( leaf.Value.Index ); using var id = ImRaii.PushId( leaf.Value.Index );
using var _ = ImRaii.TreeNode( leaf.Value.Name, flags ); ImRaii.TreeNode( leaf.Value.Name, flags ).Dispose();
} }
@ -347,12 +347,15 @@ public sealed partial class ModFileSystemSelector : FileSystemSelector< Mod, Mod
} }
} }
private void OnModMetaChange( MetaChangeType type, Mod mod, string? oldName ) private void OnModDataChange( ModDataChangeType type, Mod mod, string? oldName )
{ {
switch( type ) switch( type )
{ {
case MetaChangeType.Name: case ModDataChangeType.Name:
case MetaChangeType.Author: case ModDataChangeType.Author:
case ModDataChangeType.ModTags:
case ModDataChangeType.LocalTags:
case ModDataChangeType.Favorite:
SetFilterDirty(); SetFilterDirty();
break; break;
} }

View file

@ -7,33 +7,37 @@ public enum ModFilter
{ {
Enabled = 1 << 0, Enabled = 1 << 0,
Disabled = 1 << 1, Disabled = 1 << 1,
NoConflict = 1 << 2, Favorite = 1 << 2,
SolvedConflict = 1 << 3, NotFavorite = 1 << 3,
UnsolvedConflict = 1 << 4, NoConflict = 1 << 4,
HasNoMetaManipulations = 1 << 5, SolvedConflict = 1 << 5,
HasMetaManipulations = 1 << 6, UnsolvedConflict = 1 << 6,
HasNoFileSwaps = 1 << 7, HasNoMetaManipulations = 1 << 7,
HasFileSwaps = 1 << 8, HasMetaManipulations = 1 << 8,
HasConfig = 1 << 9, HasNoFileSwaps = 1 << 9,
HasNoConfig = 1 << 10, HasFileSwaps = 1 << 10,
HasNoFiles = 1 << 11, HasConfig = 1 << 11,
HasFiles = 1 << 12, HasNoConfig = 1 << 12,
IsNew = 1 << 13, HasNoFiles = 1 << 13,
NotNew = 1 << 14, HasFiles = 1 << 14,
Inherited = 1 << 15, IsNew = 1 << 15,
Uninherited = 1 << 16, NotNew = 1 << 16,
Undefined = 1 << 17, Inherited = 1 << 17,
Uninherited = 1 << 18,
Undefined = 1 << 19,
}; };
public static class ModFilterExtensions public static class ModFilterExtensions
{ {
public const ModFilter UnfilteredStateMods = ( ModFilter )( ( 1 << 18 ) - 1 ); public const ModFilter UnfilteredStateMods = ( ModFilter )( ( 1 << 20 ) - 1 );
public static string ToName( this ModFilter filter ) public static string ToName( this ModFilter filter )
=> filter switch => filter switch
{ {
ModFilter.Enabled => "Enabled", ModFilter.Enabled => "Enabled",
ModFilter.Disabled => "Disabled", ModFilter.Disabled => "Disabled",
ModFilter.Favorite => "Favorite",
ModFilter.NotFavorite => "No Favorite",
ModFilter.NoConflict => "No Conflicts", ModFilter.NoConflict => "No Conflicts",
ModFilter.SolvedConflict => "Solved Conflicts", ModFilter.SolvedConflict => "Solved Conflicts",
ModFilter.UnsolvedConflict => "Unsolved Conflicts", ModFilter.UnsolvedConflict => "Unsolved Conflicts",

View file

@ -22,12 +22,19 @@ public partial class ConfigWindow
Add5_8_7( ret ); Add5_8_7( ret );
Add5_9_0( ret ); Add5_9_0( ret );
Add5_10_0( ret ); Add5_10_0( ret );
Add5_11_0( ret );
return ret; return ret;
} }
private static void Add5_11_0( Changelog log ) private static void Add5_11_0( Changelog log )
=> log.NextVersion( "Version 0.5.11.0" ) => log.NextVersion( "Version 0.5.11.0" )
.RegisterEntry(
"Added local data storage for mods in the plugin config folder. This information is not exported together with your mod, but not dependent on collections." )
.RegisterEntry( "Moved the import date from mod metadata to local data.", 1 )
.RegisterEntry( "Added Favorites. You can declare mods as favorites and filter for them.", 1 )
.RegisterEntry( "Added Local Tags. You can apply custom Tags to mods and filter for them.", 1 )
.RegisterEntry( "Added Mod Tags. Mod Creators (and the Edit Mod tab) can set tags that are stored in the mod meta data and are thus exported." )
.RegisterEntry( "Add backface and transparency toggles to .mtrl editing, as well as a info section." ) .RegisterEntry( "Add backface and transparency toggles to .mtrl editing, as well as a info section." )
.RegisterEntry( "Meta Manipulation editing now highlights if the selected ID is 0 or 1." ) .RegisterEntry( "Meta Manipulation editing now highlights if the selected ID is 0 or 1." )
.RegisterEntry( "Fixed a bug when manually adding EQP or EQDP entries to Mods." ) .RegisterEntry( "Fixed a bug when manually adding EQP or EQDP entries to Mods." )

View file

@ -55,6 +55,13 @@ public partial class ConfigWindow
} }
} }
ImGui.Dummy( _window._defaultSpace );
var tagIdx = _modTags.Draw( "Mod Tags: ", "Edit tags by clicking them, or add new tags. Empty tags are removed.", _mod.ModTags, out var editedTag );
if( tagIdx >= 0 )
{
Penumbra.ModManager.ChangeModTag( _mod.Index, tagIdx, editedTag );
}
ImGui.Dummy( _window._defaultSpace ); ImGui.Dummy( _window._defaultSpace );
AddOptionGroup.Draw( _window, _mod ); AddOptionGroup.Draw( _window, _mod );
ImGui.Dummy( _window._defaultSpace ); ImGui.Dummy( _window._defaultSpace );
@ -566,7 +573,7 @@ public partial class ConfigWindow
ImGui.TableNextColumn(); ImGui.TableNextColumn();
var canAddGroup = mod.Groups[ groupIdx ].Type != GroupType.Multi || mod.Groups[ groupIdx ].Count < IModGroup.MaxMultiOptions; var canAddGroup = mod.Groups[ groupIdx ].Type != GroupType.Multi || mod.Groups[ groupIdx ].Count < IModGroup.MaxMultiOptions;
var validName = _newOptionName.Length > 0 && _newOptionNameIdx == groupIdx; var validName = _newOptionName.Length > 0 && _newOptionNameIdx == groupIdx;
var tt = canAddGroup var tt = canAddGroup
? validName ? "Add a new option to this group." : "Please enter a name for the new option." ? validName ? "Add a new option to this group." : "Please enter a name for the new option."
: $"Can not add more than {IModGroup.MaxMultiOptions} options to a multi group."; : $"Can not add more than {IModGroup.MaxMultiOptions} options to a multi group.";
@ -642,7 +649,7 @@ public partial class ConfigWindow
{ {
GroupType.Single => "Single Group", GroupType.Single => "Single Group",
GroupType.Multi => "Multi Group", GroupType.Multi => "Multi Group",
_ => "Unknown", _ => "Unknown",
}; };
ImGui.SetNextItemWidth( _window._inputTextWidth.X - 3 * _window._iconButtonSize.X - 12 * ImGuiHelpers.GlobalScale ); ImGui.SetNextItemWidth( _window._inputTextWidth.X - 3 * _window._iconButtonSize.X - 12 * ImGuiHelpers.GlobalScale );

View file

@ -58,16 +58,20 @@ public partial class ConfigWindow
DrawPriorityInput(); DrawPriorityInput();
OpenTutorial( BasicTutorialSteps.Priority ); OpenTutorial( BasicTutorialSteps.Priority );
DrawRemoveSettings(); DrawRemoveSettings();
ImGui.Dummy( _window._defaultSpace );
for( var idx = 0; idx < _mod.Groups.Count; ++idx )
{
DrawSingleGroup( _mod.Groups[ idx ], idx );
}
ImGui.Dummy( _window._defaultSpace ); if( _mod.Groups.Count > 0 )
for( var idx = 0; idx < _mod.Groups.Count; ++idx )
{ {
DrawMultiGroup( _mod.Groups[ idx ], idx ); ImGui.Dummy( _window._defaultSpace );
for( var idx = 0; idx < _mod.Groups.Count; ++idx )
{
DrawSingleGroup( _mod.Groups[ idx ], idx );
}
ImGui.Dummy( _window._defaultSpace );
for( var idx = 0; idx < _mod.Groups.Count; ++idx )
{
DrawMultiGroup( _mod.Groups[ idx ], idx );
}
} }
_window._penumbra.Api.InvokePostSettingsPanel( _mod.ModPath.Name ); _window._penumbra.Api.InvokePostSettingsPanel( _mod.ModPath.Name );

View file

@ -1,9 +1,11 @@
using System; using System;
using System.Numerics; using System.Numerics;
using Dalamud.Interface;
using ImGuiNET; using ImGuiNET;
using OtterGui; using OtterGui;
using OtterGui.Classes; using OtterGui.Classes;
using OtterGui.Raii; using OtterGui.Raii;
using OtterGui.Widgets;
using Penumbra.GameData.ByteString; using Penumbra.GameData.ByteString;
using Penumbra.Meta.Manipulations; using Penumbra.Meta.Manipulations;
using Penumbra.Mods; using Penumbra.Mods;
@ -36,10 +38,12 @@ public partial class ConfigWindow
private static readonly Utf8String ChangedItemsTabHeader = Utf8String.FromStringUnsafe( "Changed Items", false ); private static readonly Utf8String ChangedItemsTabHeader = Utf8String.FromStringUnsafe( "Changed Items", false );
private static readonly Utf8String EditModTabHeader = Utf8String.FromStringUnsafe( "Edit Mod", false ); private static readonly Utf8String EditModTabHeader = Utf8String.FromStringUnsafe( "Edit Mod", false );
private readonly TagButtons _modTags = new();
private void DrawTabBar() private void DrawTabBar()
{ {
ImGui.Dummy( _window._defaultSpace ); var tabBarHeight = ImGui.GetCursorPosY();
using var tabBar = ImRaii.TabBar( "##ModTabs" ); using var tabBar = ImRaii.TabBar( "##ModTabs" );
if( !tabBar ) if( !tabBar )
{ {
return; return;
@ -47,8 +51,8 @@ public partial class ConfigWindow
_availableTabs = Tabs.Settings _availableTabs = Tabs.Settings
| ( _mod.ChangedItems.Count > 0 ? Tabs.ChangedItems : 0 ) | ( _mod.ChangedItems.Count > 0 ? Tabs.ChangedItems : 0 )
| ( _mod.Description.Length > 0 ? Tabs.Description : 0 ) | Tabs.Description
| ( _conflicts.Count > 0 ? Tabs.Conflicts : 0 ) | ( _conflicts.Count > 0 ? Tabs.Conflicts : 0 )
| Tabs.Edit; | Tabs.Edit;
DrawSettingsTab(); DrawSettingsTab();
@ -56,6 +60,12 @@ public partial class ConfigWindow
DrawChangedItemsTab(); DrawChangedItemsTab();
DrawConflictsTab(); DrawConflictsTab();
DrawEditModTab(); DrawEditModTab();
DrawAdvancedEditingButton();
DrawFavoriteButton( tabBarHeight );
}
private void DrawAdvancedEditingButton()
{
if( ImGui.TabItemButton( "Advanced Editing", ImGuiTabItemFlags.Trailing | ImGuiTabItemFlags.NoTooltip ) ) if( ImGui.TabItemButton( "Advanced Editing", ImGuiTabItemFlags.Trailing | ImGuiTabItemFlags.NoTooltip ) )
{ {
_window.ModEditPopup.ChangeMod( _mod ); _window.ModEditPopup.ChangeMod( _mod );
@ -73,6 +83,44 @@ public partial class ConfigWindow
+ "\t\t- textures" ); + "\t\t- textures" );
} }
private void DrawFavoriteButton( float height )
{
var oldPos = ImGui.GetCursorPos();
using( var font = ImRaii.PushFont( UiBuilder.IconFont ) )
{
var size = ImGui.CalcTextSize( FontAwesomeIcon.Star.ToIconString() ) + ImGui.GetStyle().FramePadding * 2;
var newPos = new Vector2( ImGui.GetWindowWidth() - size.X - ImGui.GetStyle().ItemSpacing.X, height );
if( ImGui.GetScrollMaxX() > 0 )
{
newPos.X += ImGui.GetScrollX();
}
var rectUpper = ImGui.GetWindowPos() + newPos;
var color = ImGui.IsMouseHoveringRect( rectUpper, rectUpper + size ) ? ImGui.GetColorU32( ImGuiCol.Text ) :
_mod.Favorite ? 0xFF00FFFF : ImGui.GetColorU32( ImGuiCol.TextDisabled );
using var c = ImRaii.PushColor( ImGuiCol.Text, color )
.Push( ImGuiCol.Button, 0 )
.Push( ImGuiCol.ButtonHovered, 0 )
.Push( ImGuiCol.ButtonActive, 0 );
ImGui.SetCursorPos( newPos );
if( ImGui.Button( FontAwesomeIcon.Star.ToIconString() ) )
{
Penumbra.ModManager.ChangeModFavorite( _mod.Index, !_mod.Favorite );
}
}
var hovered = ImGui.IsItemHovered();
OpenTutorial( BasicTutorialSteps.Favorites );
if( hovered )
{
ImGui.SetTooltip( "Favorite" );
}
}
// Just a simple text box with the wrapped description, if it exists. // Just a simple text box with the wrapped description, if it exists.
private void DrawDescriptionTab() private void DrawDescriptionTab()
{ {
@ -88,6 +136,26 @@ public partial class ConfigWindow
return; return;
} }
ImGui.Dummy( ImGuiHelpers.ScaledVector2( 2 ) );
ImGui.Dummy( ImGuiHelpers.ScaledVector2( 2 ) );
var tagIdx = _localTags.Draw( "Local Tags: ", "Custom tags you can set personally that will not be exported to the mod data but only set for you.\n"
+ "If the mod already contains a local tag in its own tags, the local tag will be ignored.", _mod.LocalTags,
out var editedTag );
OpenTutorial( BasicTutorialSteps.Tags );
if( tagIdx >= 0 )
{
Penumbra.ModManager.ChangeLocalTag( _mod.Index, tagIdx, editedTag );
}
if( _mod.ModTags.Count > 0 )
{
_modTags.Draw( "Mod Tags: ", "Tags assigned by the mod creator and saved with the mod data. To edit these, look at Edit Mod.", _mod.ModTags, out var _, false,
ImGui.CalcTextSize( "Local " ).X - ImGui.CalcTextSize( "Mod " ).X );
}
ImGui.Dummy( ImGuiHelpers.ScaledVector2( 2 ) );
ImGui.Separator();
ImGuiUtil.TextWrapped( _mod.Description ); ImGuiUtil.TextWrapped( _mod.Description );
} }

View file

@ -8,8 +8,8 @@ using System;
using System.Linq; using System.Linq;
using System.Numerics; using System.Numerics;
using Dalamud.Interface; using Dalamud.Interface;
using OtterGui.Widgets;
using Penumbra.Api.Enums; using Penumbra.Api.Enums;
using Penumbra.GameData.Enums;
namespace Penumbra.UI; namespace Penumbra.UI;
@ -198,9 +198,10 @@ public partial class ConfigWindow
{ {
private readonly ConfigWindow _window; private readonly ConfigWindow _window;
private bool _valid; private bool _valid;
private ModFileSystem.Leaf _leaf = null!; private ModFileSystem.Leaf _leaf = null!;
private Mod _mod = null!; private Mod _mod = null!;
private readonly TagButtons _localTags = new();
public ModPanel( ConfigWindow window ) public ModPanel( ConfigWindow window )
=> _window = window; => _window = window;

View file

@ -81,6 +81,8 @@ public partial class ConfigWindow
Faq1, Faq1,
Faq2, Faq2,
Faq3, Faq3,
Favorites,
Tags,
} }
public static readonly Tutorial Tutorial = new Tutorial() public static readonly Tutorial Tutorial = new Tutorial()
@ -159,5 +161,7 @@ public partial class ConfigWindow
.Register( "FAQ 2", .Register( "FAQ 2",
"It is advised to not use TexTools and Penumbra at the same time. Penumbra may refuse to work if TexTools broke your game indices." ) "It is advised to not use TexTools and Penumbra at the same time. Penumbra may refuse to work if TexTools broke your game indices." )
.Register( "FAQ 3", "Penumbra can change the skin material a mod uses. This is under advanced editing." ) .Register( "FAQ 3", "Penumbra can change the skin material a mod uses. This is under advanced editing." )
.Register( "Favorites", "You can now toggle mods as favorites using this button. You can filter for favorited mods in the mod selector. Favorites are stored locally, not within the mod, but independently of collections." )
.Register( "Tags", "Mods can now have two types of tags:\n\n- Local Tags are those that you can set for yourself. They are stored locally and are not saved in any way in the mod directory itself.\n- Mod Tags are stored in the mod metadata, are set by the mod creator and are exported together with the mod, they can only be edited in the Edit Mod tab.\n\nIf a mod has a tag in its Mod Tags, this overwrites any identical Local Tags.\n\nYou can filter for tags in the mod selector via 't:text'." )
.EnsureSize( Enum.GetValues< BasicTutorialSteps >().Length ); .EnsureSize( Enum.GetValues< BasicTutorialSteps >().Length );
} }