diff --git a/Penumbra.GameData/Structs/GmpEntry.cs b/Penumbra.GameData/Structs/GmpEntry.cs index c6af3fba..8ad571ed 100644 --- a/Penumbra.GameData/Structs/GmpEntry.cs +++ b/Penumbra.GameData/Structs/GmpEntry.cs @@ -1,94 +1,103 @@ +using System; using System.IO; -namespace Penumbra.GameData.Structs +namespace Penumbra.GameData.Structs; + +public struct GmpEntry : IEquatable< GmpEntry > { - public struct GmpEntry + public static readonly GmpEntry Default = new(); + + public bool Enabled { - public static readonly GmpEntry Default = new (); - - public bool Enabled + get => ( Value & 1 ) == 1; + set { - get => ( Value & 1 ) == 1; - set + if( value ) { - if( value ) - { - Value |= 1ul; - } - else - { - Value &= ~1ul; - } + Value |= 1ul; + } + else + { + Value &= ~1ul; } } - - public bool Animated - { - get => ( Value & 2 ) == 2; - set - { - if( value ) - { - Value |= 2ul; - } - else - { - Value &= ~2ul; - } - } - } - - public ushort RotationA - { - get => ( ushort )( ( Value >> 2 ) & 0x3FF ); - set => Value = ( Value & ~0xFFCul ) | ( ( value & 0x3FFul ) << 2 ); - } - - public ushort RotationB - { - get => ( ushort )( ( Value >> 12 ) & 0x3FF ); - set => Value = ( Value & ~0x3FF000ul ) | ( ( value & 0x3FFul ) << 12 ); - } - - public ushort RotationC - { - get => ( ushort )( ( Value >> 22 ) & 0x3FF ); - set => Value = ( Value & ~0xFFC00000ul ) | ( ( value & 0x3FFul ) << 22 ); - } - - public byte UnknownA - { - get => ( byte )( ( Value >> 32 ) & 0x0F ); - set => Value = ( Value & ~0x0F00000000ul ) | ( ( value & 0x0Ful ) << 32 ); - } - - public byte UnknownB - { - get => ( byte )( ( Value >> 36 ) & 0x0F ); - set => Value = ( Value & ~0xF000000000ul ) | ( ( value & 0x0Ful ) << 36 ); - } - - public byte UnknownTotal - { - get => ( byte )( ( Value >> 32 ) & 0xFF ); - set => Value = ( Value & ~0xFF00000000ul ) | ( ( value & 0xFFul ) << 32 ); - } - - public ulong Value { get; set; } - - public static GmpEntry FromTexToolsMeta( byte[] data ) - { - GmpEntry ret = new(); - using var reader = new BinaryReader( new MemoryStream( data ) ); - ret.Value = reader.ReadUInt32(); - ret.UnknownTotal = data[ 4 ]; - return ret; - } - - public static implicit operator ulong( GmpEntry entry ) - => entry.Value; - - public static explicit operator GmpEntry( ulong entry ) - => new() { Value = entry }; } + + public bool Animated + { + get => ( Value & 2 ) == 2; + set + { + if( value ) + { + Value |= 2ul; + } + else + { + Value &= ~2ul; + } + } + } + + public ushort RotationA + { + get => ( ushort )( ( Value >> 2 ) & 0x3FF ); + set => Value = ( Value & ~0xFFCul ) | ( ( value & 0x3FFul ) << 2 ); + } + + public ushort RotationB + { + get => ( ushort )( ( Value >> 12 ) & 0x3FF ); + set => Value = ( Value & ~0x3FF000ul ) | ( ( value & 0x3FFul ) << 12 ); + } + + public ushort RotationC + { + get => ( ushort )( ( Value >> 22 ) & 0x3FF ); + set => Value = ( Value & ~0xFFC00000ul ) | ( ( value & 0x3FFul ) << 22 ); + } + + public byte UnknownA + { + get => ( byte )( ( Value >> 32 ) & 0x0F ); + set => Value = ( Value & ~0x0F00000000ul ) | ( ( value & 0x0Ful ) << 32 ); + } + + public byte UnknownB + { + get => ( byte )( ( Value >> 36 ) & 0x0F ); + set => Value = ( Value & ~0xF000000000ul ) | ( ( value & 0x0Ful ) << 36 ); + } + + public byte UnknownTotal + { + get => ( byte )( ( Value >> 32 ) & 0xFF ); + set => Value = ( Value & ~0xFF00000000ul ) | ( ( value & 0xFFul ) << 32 ); + } + + public ulong Value { get; set; } + + public static GmpEntry FromTexToolsMeta( byte[] data ) + { + GmpEntry ret = new(); + using var reader = new BinaryReader( new MemoryStream( data ) ); + ret.Value = reader.ReadUInt32(); + ret.UnknownTotal = data[ 4 ]; + return ret; + } + + public static implicit operator ulong( GmpEntry entry ) + => entry.Value; + + public static explicit operator GmpEntry( ulong entry ) + => new() { Value = entry }; + + public bool Equals( GmpEntry other ) + => Value == other.Value; + + public override bool Equals( object? obj ) + => obj is GmpEntry other && Equals( other ); + + public override int GetHashCode() + => Value.GetHashCode(); } \ No newline at end of file diff --git a/Penumbra/Collections/CollectionManager.Active.cs b/Penumbra/Collections/CollectionManager.Active.cs index 30b9298d..5e264981 100644 --- a/Penumbra/Collections/CollectionManager.Active.cs +++ b/Penumbra/Collections/CollectionManager.Active.cs @@ -206,14 +206,6 @@ public partial class ModCollection } } - private void ForceCacheUpdates() - { - foreach( var collection in this ) - { - collection.ForceCacheUpdate( collection == Default ); - } - } - // Recalculate effective files for active collections on events. private void OnModAddedActive( bool meta ) { diff --git a/Penumbra/Collections/CollectionManager.cs b/Penumbra/Collections/CollectionManager.cs index 76961f81..d54ffa05 100644 --- a/Penumbra/Collections/CollectionManager.cs +++ b/Penumbra/Collections/CollectionManager.cs @@ -56,21 +56,23 @@ public partial class ModCollection IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); - public Manager( Mods.Mod.Manager manager ) + public Manager( Mod.Manager manager ) { _modManager = manager; // The collection manager reacts to changes in mods by itself. - _modManager.ModsRediscovered += OnModsRediscovered; - _modManager.ModChange += OnModChanged; + _modManager.ModDiscoveryStarted += OnModDiscoveryStarted; + _modManager.ModDiscoveryFinished += OnModDiscoveryFinished; + _modManager.ModChange += OnModChanged; ReadCollections(); LoadCollections(); } public void Dispose() { - _modManager.ModsRediscovered -= OnModsRediscovered; - _modManager.ModChange -= OnModChanged; + _modManager.ModDiscoveryStarted -= OnModDiscoveryStarted; + _modManager.ModDiscoveryFinished -= OnModDiscoveryFinished; + _modManager.ModChange -= OnModChanged; } // Add a new collection of the given name. @@ -144,12 +146,27 @@ public partial class ModCollection public bool RemoveCollection( ModCollection collection ) => RemoveCollection( collection.Index ); - - private void OnModsRediscovered() + private void OnModDiscoveryStarted() { - // When mods are rediscovered, force all cache updates and set the files of the default collection. - ForceCacheUpdates(); - Default.SetFiles(); + foreach( var collection in this ) + { + collection.PrepareModDiscovery(); + } + } + + private void OnModDiscoveryFinished() + { + // First, re-apply all mod settings. + foreach( var collection in this ) + { + collection.ApplyModSettings(); + } + + // Afterwards, we update the caches. This can not happen in the same loop due to inheritance. + foreach( var collection in this ) + { + collection.ForceCacheUpdate( collection == Default ); + } } @@ -209,8 +226,7 @@ public partial class ModCollection // Inheritances can not be setup before all collections are read, // so this happens after reading the collections. - // During this iteration, we can also fix all settings that are not valid for the given mod anymore. - private void ApplyInheritancesAndFixSettings( IEnumerable< IReadOnlyList< string > > inheritances ) + private void ApplyInheritances( IEnumerable< IReadOnlyList< string > > inheritances ) { foreach( var (collection, inheritance) in this.Zip( inheritances ) ) { @@ -229,11 +245,6 @@ public partial class ModCollection } } - foreach( var (setting, mod) in collection.Settings.Zip( _modManager.Mods ).Where( s => s.First != null ) ) - { - changes |= setting!.FixInvalidSettings( mod.Meta ); - } - if( changes ) { collection.Save(); @@ -277,7 +288,7 @@ public partial class ModCollection } AddDefaultCollection(); - ApplyInheritancesAndFixSettings( inheritances ); + ApplyInheritances( inheritances ); } } } \ No newline at end of file diff --git a/Penumbra/Collections/ModCollection.Cache.cs b/Penumbra/Collections/ModCollection.Cache.cs index 33726689..226f7b05 100644 --- a/Penumbra/Collections/ModCollection.Cache.cs +++ b/Penumbra/Collections/ModCollection.Cache.cs @@ -74,7 +74,7 @@ public partial class ModCollection // Update the effective file list for the given cache. // Creates a cache if necessary. - public void CalculateEffectiveFileList( bool withMetaManipulations, bool reloadResident ) + public void CalculateEffectiveFileList( bool withMetaManipulations, bool reloadDefault ) { // Skip the empty collection. if( Index == 0 ) @@ -82,15 +82,20 @@ public partial class ModCollection return; } - PluginLog.Debug( "Recalculating effective file list for {CollectionName} [{WithMetaManipulations}]", Name, withMetaManipulations ); + PluginLog.Debug( "Recalculating effective file list for {CollectionName} [{WithMetaManipulations}] [{ReloadDefault}]", Name, + withMetaManipulations, reloadDefault ); _cache ??= new Cache( this ); _cache.CalculateEffectiveFileList(); if( withMetaManipulations ) { _cache.UpdateMetaManipulations(); + if( reloadDefault ) + { + SetFiles(); + } } - if( reloadResident ) + if( reloadDefault ) { Penumbra.ResidentResources.Reload(); } diff --git a/Penumbra/Collections/ModCollection.cs b/Penumbra/Collections/ModCollection.cs index 9e4f7c60..6e82617f 100644 --- a/Penumbra/Collections/ModCollection.cs +++ b/Penumbra/Collections/ModCollection.cs @@ -1,22 +1,10 @@ +using System; using System.Collections.Generic; using System.Linq; using Penumbra.Mods; namespace Penumbra.Collections; -public partial class ModCollection -{ - // Create the always available Empty Collection that will always sit at index 0, - // can not be deleted and does never create a cache. - private static ModCollection CreateEmpty() - { - var collection = CreateNewEmpty( EmptyCollection ); - collection.Index = 0; - collection._settings.Clear(); - return collection; - } -} - // A ModCollection is a named set of ModSettings to all of the users' installed mods. // Settings to mods that are not installed anymore are kept as long as no call to CleanUnavailableSettings is made. // Invariants: @@ -51,7 +39,6 @@ public partial class ModCollection // Settings for deleted mods will be kept via directory name. private readonly Dictionary< string, ModSettings > _unusedSettings; - // Constructor for duplication. private ModCollection( string name, ModCollection duplicate ) { @@ -70,16 +57,9 @@ public partial class ModCollection Name = name; Version = version; _unusedSettings = allSettings; - _settings = Enumerable.Repeat( ( ModSettings? )null, Penumbra.ModManager.Count ).ToList(); - for( var i = 0; i < Penumbra.ModManager.Count; ++i ) - { - var modName = Penumbra.ModManager[ i ].BasePath.Name; - if( _unusedSettings.TryGetValue( Penumbra.ModManager[ i ].BasePath.Name, out var settings ) ) - { - _unusedSettings.Remove( modName ); - _settings[ i ] = settings; - } - } + + _settings = new List< ModSettings? >(); + ApplyModSettings(); Migration.Migrate( this ); ModSettingChanged += SaveOnChange; @@ -106,7 +86,7 @@ public partial class ModCollection } // Add settings for a new appended mod, by checking if the mod had settings from a previous deletion. - private void AddMod( Mods.Mod mod ) + private void AddMod( Mod mod ) { if( _unusedSettings.TryGetValue( mod.BasePath.Name, out var settings ) ) { @@ -120,7 +100,7 @@ public partial class ModCollection } // Move settings from the current mod list to the unused mod settings. - private void RemoveMod( Mods.Mod mod, int idx ) + private void RemoveMod( Mod mod, int idx ) { var settings = _settings[ idx ]; if( settings != null ) @@ -130,4 +110,51 @@ public partial class ModCollection _settings.RemoveAt( idx ); } + + // Create the always available Empty Collection that will always sit at index 0, + // can not be deleted and does never create a cache. + private static ModCollection CreateEmpty() + { + var collection = CreateNewEmpty( EmptyCollection ); + collection.Index = 0; + collection._settings.Clear(); + return collection; + } + + // Move all settings to unused settings for rediscovery. + private void PrepareModDiscovery() + { + foreach( var (mod, setting) in Penumbra.ModManager.Zip( _settings ).Where( s => s.Second != null ) ) + { + _unusedSettings[ mod.BasePath.Name ] = setting!; + } + + _settings.Clear(); + } + + // Apply all mod settings from unused settings to the current set of mods. + // Also fixes invalid settings. + private void ApplyModSettings() + { + _settings.Capacity = Math.Max( _settings.Capacity, Penumbra.ModManager.Count ); + var changes = false; + foreach( var mod in Penumbra.ModManager ) + { + if( _unusedSettings.TryGetValue( mod.BasePath.Name, out var s ) ) + { + changes |= s.FixInvalidSettings( mod.Meta ); + _settings.Add( s ); + _unusedSettings.Remove( mod.BasePath.Name ); + } + else + { + _settings.Add( null ); + } + } + + if( changes ) + { + Save(); + } + } } \ No newline at end of file diff --git a/Penumbra/Meta/Manipulations/EstManipulation.cs b/Penumbra/Meta/Manipulations/EstManipulation.cs index d25893f1..b607e5e5 100644 --- a/Penumbra/Meta/Manipulations/EstManipulation.cs +++ b/Penumbra/Meta/Manipulations/EstManipulation.cs @@ -19,7 +19,7 @@ public readonly struct EstManipulation : IMetaManipulation< EstManipulation > Head = CharacterUtility.HeadEstIdx, } - public readonly ushort SkeletonIdx; + public readonly ushort Entry; // SkeletonIdx. [JsonConverter( typeof( StringEnumConverter ) )] public readonly Gender Gender; @@ -33,13 +33,13 @@ public readonly struct EstManipulation : IMetaManipulation< EstManipulation > public readonly EstType Slot; [JsonConstructor] - public EstManipulation( Gender gender, ModelRace race, EstType slot, ushort setId, ushort skeletonIdx ) + public EstManipulation( Gender gender, ModelRace race, EstType slot, ushort setId, ushort entry ) { - SkeletonIdx = skeletonIdx; - Gender = gender; - Race = race; - SetId = setId; - Slot = slot; + Entry = entry; + Gender = gender; + Race = race; + SetId = setId; + Slot = slot; } @@ -81,7 +81,7 @@ public readonly struct EstManipulation : IMetaManipulation< EstManipulation > public bool Apply( EstFile file ) { - return file.SetEntry( Names.CombinedRace( Gender, Race ), SetId, SkeletonIdx ) switch + return file.SetEntry( Names.CombinedRace( Gender, Race ), SetId, Entry ) switch { EstFile.EstEntryChange.Unchanged => false, EstFile.EstEntryChange.Changed => true, diff --git a/Penumbra/Meta/Manipulations/MetaManipulation.cs b/Penumbra/Meta/Manipulations/MetaManipulation.cs index 92caaa23..421642a0 100644 --- a/Penumbra/Meta/Manipulations/MetaManipulation.cs +++ b/Penumbra/Meta/Manipulations/MetaManipulation.cs @@ -137,7 +137,7 @@ public readonly struct MetaManipulation : IEquatable< MetaManipulation >, ICompa [FieldOffset( 15 )] [JsonConverter( typeof( StringEnumConverter ) )] - [JsonProperty("Type")] + [JsonProperty( "Type" )] public readonly Type ManipulationType; public object? Manipulation @@ -239,6 +239,25 @@ public readonly struct MetaManipulation : IEquatable< MetaManipulation >, ICompa public static implicit operator MetaManipulation( ImcManipulation imc ) => new(imc); + public bool EntryEquals( MetaManipulation other ) + { + if( ManipulationType != other.ManipulationType ) + { + return false; + } + + return ManipulationType switch + { + Type.Eqp => Eqp.Entry.Equals( other.Eqp.Entry ), + Type.Gmp => Gmp.Entry.Equals( other.Gmp.Entry ), + Type.Eqdp => Eqdp.Entry.Equals( other.Eqdp.Entry ), + Type.Est => Est.Entry.Equals( other.Est.Entry ), + Type.Rsp => Rsp.Entry.Equals( other.Rsp.Entry ), + Type.Imc => Imc.Entry.Equals( other.Imc.Entry ), + _ => throw new ArgumentOutOfRangeException(), + }; + } + public bool Equals( MetaManipulation other ) { if( ManipulationType != other.ManipulationType ) diff --git a/Penumbra/Mods/IModGroup.cs b/Penumbra/Mods/IModGroup.cs new file mode 100644 index 00000000..498d17d2 --- /dev/null +++ b/Penumbra/Mods/IModGroup.cs @@ -0,0 +1,53 @@ +using System; +using System.Collections.Generic; +using System.IO; +using Dalamud.Logging; +using Penumbra.Util; + +namespace Penumbra.Mods; + +public interface IModGroup : IEnumerable< ISubMod > +{ + public string Name { get; } + public string Description { get; } + public SelectType Type { get; } + public int Priority { get; } + + public int OptionPriority( Index optionIdx ); + + public ISubMod this[ Index idx ] { get; } + + public int Count { get; } + + public bool IsOption + => Type switch + { + SelectType.Single => Count > 1, + SelectType.Multi => Count > 0, + _ => false, + }; + + public void Save( DirectoryInfo basePath ); + + public string FileName( DirectoryInfo basePath ) + => Path.Combine( basePath.FullName, Name.RemoveInvalidPathSymbols() + ".json" ); + + public void DeleteFile( DirectoryInfo basePath ) + { + var file = FileName( basePath ); + if( !File.Exists( file ) ) + { + return; + } + + try + { + File.Delete( file ); + } + catch( Exception e ) + { + PluginLog.Error( $"Could not delete file {file}:\n{e}" ); + throw; + } + } +} \ No newline at end of file diff --git a/Penumbra/Mods/ISubMod.cs b/Penumbra/Mods/ISubMod.cs new file mode 100644 index 00000000..ee05e876 --- /dev/null +++ b/Penumbra/Mods/ISubMod.cs @@ -0,0 +1,14 @@ +using System.Collections.Generic; +using Penumbra.GameData.ByteString; +using Penumbra.Meta.Manipulations; + +namespace Penumbra.Mods; + +public interface ISubMod +{ + public string Name { get; } + + public IReadOnlyDictionary< Utf8GamePath, FullPath > Files { get; } + public IReadOnlyDictionary< Utf8GamePath, FullPath > FileSwaps { get; } + public IReadOnlyList< MetaManipulation > Manipulations { get; } +} \ No newline at end of file diff --git a/Penumbra/Mods/Mod.SortOrder.cs b/Penumbra/Mods/Mod.SortOrder.cs index caaac4f9..5cf55c5e 100644 --- a/Penumbra/Mods/Mod.SortOrder.cs +++ b/Penumbra/Mods/Mod.SortOrder.cs @@ -28,7 +28,6 @@ public partial class Mod } } - public SortOrder( ModFolder parentFolder, string name ) { ParentFolder = parentFolder; diff --git a/Penumbra/Mods/Mod.cs b/Penumbra/Mods/Mod.cs index 427d13ba..9ca93f85 100644 --- a/Penumbra/Mods/Mod.cs +++ b/Penumbra/Mods/Mod.cs @@ -9,7 +9,7 @@ namespace Penumbra.Mods; // Mod contains all permanent information about a mod, // and is independent of collections or settings. // It only changes when the user actively changes the mod or their filesystem. -public partial class Mod +public sealed partial class Mod { public DirectoryInfo BasePath; public ModMeta Meta; diff --git a/Penumbra/Mods/Mod2.BasePath.cs b/Penumbra/Mods/Mod2.BasePath.cs new file mode 100644 index 00000000..2fe9dfe3 --- /dev/null +++ b/Penumbra/Mods/Mod2.BasePath.cs @@ -0,0 +1,56 @@ +using System.IO; +using Dalamud.Logging; + +namespace Penumbra.Mods; + +public enum ModPathChangeType +{ + Added, + Deleted, + Moved, +} + +public partial class Mod2 +{ + public DirectoryInfo BasePath { get; private set; } + public int Index { get; private set; } = -1; + + private FileInfo MetaFile + => new(Path.Combine( BasePath.FullName, "meta.json" )); + + private Mod2( ModFolder parentFolder, DirectoryInfo basePath ) + { + BasePath = basePath; + Order = new Mod.SortOrder( parentFolder, Name ); + //Order.ParentFolder.AddMod( this ); // TODO + ComputeChangedItems(); + } + + public static Mod2? LoadMod( ModFolder parentFolder, DirectoryInfo basePath ) + { + basePath.Refresh(); + if( !basePath.Exists ) + { + PluginLog.Error( $"Supplied mod directory {basePath} does not exist." ); + return null; + } + + var mod = new Mod2( parentFolder, basePath ); + + var metaFile = mod.MetaFile; + if( !metaFile.Exists ) + { + PluginLog.Debug( "No mod meta found for {ModLocation}.", basePath.Name ); + return null; + } + + mod.LoadMetaFromFile( metaFile ); + if( mod.Name.Length == 0 ) + { + PluginLog.Error( $"Mod at {basePath} without name is not supported." ); + } + + mod.ReloadFiles(); + return mod; + } +} \ No newline at end of file diff --git a/Penumbra/Mods/Mod2.ChangedItems.cs b/Penumbra/Mods/Mod2.ChangedItems.cs new file mode 100644 index 00000000..c6007321 --- /dev/null +++ b/Penumbra/Mods/Mod2.ChangedItems.cs @@ -0,0 +1,23 @@ +using System.Collections.Generic; +using System.Linq; + +namespace Penumbra.Mods; + +public sealed partial class Mod2 +{ + public SortedList ChangedItems { get; } = new(); + public string LowerChangedItemsString { get; private set; } = string.Empty; + + public void ComputeChangedItems() + { + var identifier = GameData.GameData.GetIdentifier(); + ChangedItems.Clear(); + foreach( var (file, _) in AllFiles ) + { + identifier.Identify( ChangedItems, file.ToGamePath() ); + } + + // TODO: manipulations + LowerChangedItemsString = string.Join( "\0", ChangedItems.Keys.Select( k => k.ToLowerInvariant() ) ); + } +} \ No newline at end of file diff --git a/Penumbra/Mods/Mod2.Files.MultiModGroup.cs b/Penumbra/Mods/Mod2.Files.MultiModGroup.cs new file mode 100644 index 00000000..4ce3fd48 --- /dev/null +++ b/Penumbra/Mods/Mod2.Files.MultiModGroup.cs @@ -0,0 +1,53 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using Dalamud.Logging; +using Newtonsoft.Json; + +namespace Penumbra.Mods; + +public partial class Mod2 +{ + private sealed class MultiModGroup : IModGroup + { + public SelectType Type + => SelectType.Multi; + + public string Name { get; set; } = "Group"; + public string Description { get; set; } = "A non-exclusive group of settings."; + public int Priority { get; set; } = 0; + + public int OptionPriority( Index idx ) + => PrioritizedOptions[ idx ].Priority; + + public ISubMod this[ Index idx ] + => PrioritizedOptions[ idx ].Mod; + + public int Count + => PrioritizedOptions.Count; + + public readonly List< (SubMod Mod, int Priority) > PrioritizedOptions = new(); + + public IEnumerator< ISubMod > GetEnumerator() + => PrioritizedOptions.Select( o => o.Mod ).GetEnumerator(); + + IEnumerator IEnumerable.GetEnumerator() + => GetEnumerator(); + + public void Save( DirectoryInfo basePath ) + { + var path = ( ( IModGroup )this ).FileName( basePath ); + try + { + var text = JsonConvert.SerializeObject( this, Formatting.Indented ); + File.WriteAllText( path, text ); + } + catch( Exception e ) + { + PluginLog.Error( $"Could not save option group {Name} to {path}:\n{e}" ); + } + } + } +} \ No newline at end of file diff --git a/Penumbra/Mods/Mod2.Files.SingleModGroup.cs b/Penumbra/Mods/Mod2.Files.SingleModGroup.cs new file mode 100644 index 00000000..92212ad7 --- /dev/null +++ b/Penumbra/Mods/Mod2.Files.SingleModGroup.cs @@ -0,0 +1,52 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.IO; +using Dalamud.Logging; +using Newtonsoft.Json; + +namespace Penumbra.Mods; + +public partial class Mod2 +{ + private sealed class SingleModGroup : IModGroup + { + public SelectType Type + => SelectType.Single; + + public string Name { get; set; } = "Option"; + public string Description { get; set; } = "A mutually exclusive group of settings."; + public int Priority { get; set; } = 0; + + public readonly List< SubMod > OptionData = new(); + + public int OptionPriority( Index _ ) + => Priority; + + public ISubMod this[ Index idx ] + => OptionData[ idx ]; + + public int Count + => OptionData.Count; + + public IEnumerator< ISubMod > GetEnumerator() + => OptionData.GetEnumerator(); + + IEnumerator IEnumerable.GetEnumerator() + => GetEnumerator(); + + public void Save( DirectoryInfo basePath ) + { + var path = ( ( IModGroup )this ).FileName( basePath ); + try + { + var text = JsonConvert.SerializeObject( this, Formatting.Indented ); + File.WriteAllText( path, text ); + } + catch( Exception e ) + { + PluginLog.Error( $"Could not save option group {Name} to {path}:\n{e}" ); + } + } + } +} \ No newline at end of file diff --git a/Penumbra/Mods/Mod2.Files.SubMod.cs b/Penumbra/Mods/Mod2.Files.SubMod.cs new file mode 100644 index 00000000..245de4f8 --- /dev/null +++ b/Penumbra/Mods/Mod2.Files.SubMod.cs @@ -0,0 +1,31 @@ +using System.Collections.Generic; +using Newtonsoft.Json; +using Penumbra.GameData.ByteString; +using Penumbra.Meta.Manipulations; + +namespace Penumbra.Mods; + +public partial class Mod2 +{ + private sealed class SubMod : ISubMod + { + public string Name { get; set; } = "Default"; + + [JsonProperty( ItemConverterType = typeof( FullPath.FullPathConverter ) )] + public readonly Dictionary< Utf8GamePath, FullPath > FileData = new(); + + [JsonProperty( ItemConverterType = typeof( FullPath.FullPathConverter ) )] + public readonly Dictionary< Utf8GamePath, FullPath > FileSwapData = new(); + + public readonly List< MetaManipulation > ManipulationData = new(); + + public IReadOnlyDictionary< Utf8GamePath, FullPath > Files + => FileData; + + public IReadOnlyDictionary< Utf8GamePath, FullPath > FileSwaps + => FileSwapData; + + public IReadOnlyList< MetaManipulation > Manipulations + => ManipulationData; + } +} \ No newline at end of file diff --git a/Penumbra/Mods/Mod2.Files.cs b/Penumbra/Mods/Mod2.Files.cs new file mode 100644 index 00000000..67cd3ad4 --- /dev/null +++ b/Penumbra/Mods/Mod2.Files.cs @@ -0,0 +1,43 @@ +using System.Collections.Generic; +using System.IO; +using System.Linq; +using Penumbra.GameData.ByteString; +using Penumbra.Meta.Manipulations; + +namespace Penumbra.Mods; + +public partial class Mod2 +{ + public IReadOnlyDictionary< Utf8GamePath, FullPath > RemainingFiles + => _remainingFiles; + + public IReadOnlyList< IModGroup > Options + => _options; + + public bool HasOptions { get; private set; } = false; + + private void SetHasOptions() + { + HasOptions = _options.Any( o + => o is MultiModGroup m && m.PrioritizedOptions.Count > 0 || o is SingleModGroup s && s.OptionData.Count > 1 ); + } + + private readonly Dictionary< Utf8GamePath, FullPath > _remainingFiles = new(); + private readonly List< IModGroup > _options = new(); + + public IEnumerable< (Utf8GamePath, FullPath) > AllFiles + => _remainingFiles.Concat( _options.SelectMany( o => o ).SelectMany( o => o.Files.Concat( o.FileSwaps ) ) ) + .Select( kvp => ( kvp.Key, kvp.Value ) ); + + public IEnumerable< MetaManipulation > AllManipulations + => _options.SelectMany( o => o ).SelectMany( o => o.Manipulations ); + + private void ReloadFiles() + { + // _remainingFiles.Clear(); + // _options.Clear(); + // HasOptions = false; + // if( !Directory.Exists( BasePath.FullName ) ) + // return; + } +} \ No newline at end of file diff --git a/Penumbra/Mods/Mod2.Manager.BasePath.cs b/Penumbra/Mods/Mod2.Manager.BasePath.cs new file mode 100644 index 00000000..a0c9b8ca --- /dev/null +++ b/Penumbra/Mods/Mod2.Manager.BasePath.cs @@ -0,0 +1,77 @@ +using System; +using System.IO; +using Dalamud.Logging; + +namespace Penumbra.Mods; + +public partial class Mod2 +{ + public partial class Manager + { + public delegate void ModPathChangeDelegate( ModPathChangeType type, Mod2 mod, DirectoryInfo? oldDirectory, + DirectoryInfo? newDirectory ); + + public event ModPathChangeDelegate? ModPathChanged; + + public void MoveModDirectory( Index idx, DirectoryInfo newDirectory ) + { + var mod = this[ idx ]; + // TODO + } + + public void DeleteMod( Index idx ) + { + var mod = this[ idx ]; + if( Directory.Exists( mod.BasePath.FullName ) ) + { + try + { + Directory.Delete( mod.BasePath.FullName, true ); + } + catch( Exception e ) + { + PluginLog.Error( $"Could not delete the mod {mod.BasePath.Name}:\n{e}" ); + } + } + + // TODO + // mod.Order.ParentFolder.RemoveMod( mod ); + // _mods.RemoveAt( idx ); + //for( var i = idx; i < _mods.Count; ++i ) + //{ + // --_mods[i].Index; + //} + + ModPathChanged?.Invoke( ModPathChangeType.Deleted, mod, mod.BasePath, null ); + } + + public Mod2 AddMod( DirectoryInfo modFolder ) + { + // TODO + + //var mod = LoadMod( StructuredMods, modFolder ); + //if( mod == null ) + //{ + // return -1; + //} + // + //if( Config.ModSortOrder.TryGetValue( mod.BasePath.Name, out var sortOrder ) ) + //{ + // if( SetSortOrderPath( mod, sortOrder ) ) + // { + // Config.Save(); + // } + //} + // + //if( _mods.Any( m => m.BasePath.Name == modFolder.Name ) ) + //{ + // return -1; + //} + // + //_mods.Add( mod ); + //ModChange?.Invoke( ChangeType.Added, _mods.Count - 1, mod ); + // + return this[^1]; + } + } +} \ No newline at end of file diff --git a/Penumbra/Mods/Mod2.Manager.Meta.cs b/Penumbra/Mods/Mod2.Manager.Meta.cs new file mode 100644 index 00000000..5fa8c6f8 --- /dev/null +++ b/Penumbra/Mods/Mod2.Manager.Meta.cs @@ -0,0 +1,67 @@ +using System; + +namespace Penumbra.Mods; + +public sealed partial class Mod2 +{ + public partial class Manager + { + public delegate void ModMetaChangeDelegate( MetaChangeType type, Mod2 mod ); + public event ModMetaChangeDelegate? ModMetaChanged; + + public void ChangeModName( Index idx, string newName ) + { + var mod = this[ idx ]; + if( mod.Name != newName ) + { + mod.Name = newName; + mod.SaveMeta(); + ModMetaChanged?.Invoke( MetaChangeType.Name, mod ); + } + } + + public void ChangeModAuthor( Index idx, string newAuthor ) + { + var mod = this[ idx ]; + if( mod.Author != newAuthor ) + { + mod.Author = newAuthor; + mod.SaveMeta(); + ModMetaChanged?.Invoke( MetaChangeType.Author, mod ); + } + } + + public void ChangeModDescription( Index idx, string newDescription ) + { + var mod = this[ idx ]; + if( mod.Description != newDescription ) + { + mod.Description = newDescription; + mod.SaveMeta(); + ModMetaChanged?.Invoke( MetaChangeType.Description, mod ); + } + } + + public void ChangeModVersion( Index idx, string newVersion ) + { + var mod = this[ idx ]; + if( mod.Version != newVersion ) + { + mod.Version = newVersion; + mod.SaveMeta(); + ModMetaChanged?.Invoke( MetaChangeType.Version, mod ); + } + } + + public void ChangeModWebsite( Index idx, string newWebsite ) + { + var mod = this[ idx ]; + if( mod.Website != newWebsite ) + { + mod.Website = newWebsite; + mod.SaveMeta(); + ModMetaChanged?.Invoke( MetaChangeType.Website, mod ); + } + } + } +} \ No newline at end of file diff --git a/Penumbra/Mods/Mod2.Manager.Options.cs b/Penumbra/Mods/Mod2.Manager.Options.cs new file mode 100644 index 00000000..3c26d6be --- /dev/null +++ b/Penumbra/Mods/Mod2.Manager.Options.cs @@ -0,0 +1,301 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using Dalamud.Logging; +using Penumbra.GameData.ByteString; +using Penumbra.Meta.Manipulations; +using Penumbra.Util; + +namespace Penumbra.Mods; + +public enum ModOptionChangeType +{ + GroupRenamed, + GroupAdded, + GroupDeleted, + PriorityChanged, + OptionAdded, + OptionDeleted, + OptionChanged, + DisplayChange, +} + +public sealed partial class Mod2 +{ + public sealed partial class Manager + { + public delegate void ModOptionChangeDelegate( ModOptionChangeType type, Mod2 mod, int groupIdx, int optionIdx ); + public event ModOptionChangeDelegate ModOptionChanged; + + public void RenameModGroup( Mod2 mod, int groupIdx, string newName ) + { + var group = mod._options[ groupIdx ]; + var oldName = group.Name; + if( oldName == newName || !VerifyFileName( mod, group, newName ) ) + { + return; + } + + var _ = group switch + { + SingleModGroup s => s.Name = newName, + MultiModGroup m => m.Name = newName, + _ => newName, + }; + + ModOptionChanged.Invoke( ModOptionChangeType.GroupRenamed, mod, groupIdx, 0 ); + } + + public void AddModGroup( Mod2 mod, SelectType type, string newName ) + { + if( !VerifyFileName( mod, null, newName ) ) + { + return; + } + + var maxPriority = mod._options.Max( o => o.Priority ) + 1; + + mod._options.Add( type == SelectType.Multi + ? new MultiModGroup { Name = newName, Priority = maxPriority } + : new SingleModGroup { Name = newName, Priority = maxPriority } ); + ModOptionChanged.Invoke( ModOptionChangeType.GroupAdded, mod, mod._options.Count - 1, 0 ); + } + + public void DeleteModGroup( Mod2 mod, int groupIdx ) + { + var group = mod._options[ groupIdx ]; + mod._options.RemoveAt( groupIdx ); + group.DeleteFile( BasePath ); + ModOptionChanged.Invoke( ModOptionChangeType.GroupDeleted, mod, groupIdx, 0 ); + } + + public void ChangeGroupDescription( Mod2 mod, int groupIdx, string newDescription ) + { + var group = mod._options[ groupIdx ]; + if( group.Description == newDescription ) + { + return; + } + + var _ = group switch + { + SingleModGroup s => s.Description = newDescription, + MultiModGroup m => m.Description = newDescription, + _ => newDescription, + }; + ModOptionChanged.Invoke( ModOptionChangeType.DisplayChange, mod, groupIdx, 0 ); + } + + public void ChangeGroupPriority( Mod2 mod, int groupIdx, int newPriority ) + { + var group = mod._options[ groupIdx ]; + if( group.Priority == newPriority ) + { + return; + } + + var _ = group switch + { + SingleModGroup s => s.Priority = newPriority, + MultiModGroup m => m.Priority = newPriority, + _ => newPriority, + }; + ModOptionChanged.Invoke( ModOptionChangeType.PriorityChanged, mod, groupIdx, -1 ); + } + + public void ChangeOptionPriority( Mod2 mod, int groupIdx, int optionIdx, int newPriority ) + { + switch( mod._options[ groupIdx ] ) + { + case SingleModGroup s: + ChangeGroupPriority( mod, groupIdx, newPriority ); + break; + case MultiModGroup m: + if( m.PrioritizedOptions[ optionIdx ].Priority == newPriority ) + { + return; + } + + m.PrioritizedOptions[ optionIdx ] = ( m.PrioritizedOptions[ optionIdx ].Mod, newPriority ); + ModOptionChanged.Invoke( ModOptionChangeType.PriorityChanged, mod, groupIdx, optionIdx ); + return; + } + } + + public void RenameOption( Mod2 mod, int groupIdx, int optionIdx, string newName ) + { + switch( mod._options[ groupIdx ] ) + { + case SingleModGroup s: + if( s.OptionData[ optionIdx ].Name == newName ) + { + return; + } + + s.OptionData[ optionIdx ].Name = newName; + break; + case MultiModGroup m: + var option = m.PrioritizedOptions[ optionIdx ].Mod; + if( option.Name == newName ) + { + return; + } + + option.Name = newName; + return; + } + + ModOptionChanged.Invoke( ModOptionChangeType.DisplayChange, mod, groupIdx, optionIdx ); + } + + public void AddOption( Mod2 mod, int groupIdx, string newName ) + { + switch( mod._options[ groupIdx ] ) + { + case SingleModGroup s: + s.OptionData.Add( new SubMod { Name = newName } ); + break; + case MultiModGroup m: + m.PrioritizedOptions.Add( ( new SubMod { Name = newName }, 0 ) ); + break; + } + + ModOptionChanged.Invoke( ModOptionChangeType.OptionAdded, mod, groupIdx, mod._options[ groupIdx ].Count - 1 ); + } + + public void DeleteOption( Mod2 mod, int groupIdx, int optionIdx ) + { + switch( mod._options[ groupIdx ] ) + { + case SingleModGroup s: + s.OptionData.RemoveAt( optionIdx ); + break; + case MultiModGroup m: + m.PrioritizedOptions.RemoveAt( optionIdx ); + break; + } + + ModOptionChanged.Invoke( ModOptionChangeType.OptionDeleted, mod, groupIdx, optionIdx ); + } + + public void OptionSetManipulation( Mod2 mod, int groupIdx, int optionIdx, MetaManipulation manip, bool delete = false ) + { + var subMod = GetSubMod( mod, groupIdx, optionIdx ); + var idx = subMod.ManipulationData.FindIndex( m => m.Equals( manip ) ); + if( delete ) + { + if( idx < 0 ) + { + return; + } + + subMod.ManipulationData.RemoveAt( idx ); + } + else + { + if( idx >= 0 ) + { + if( manip.EntryEquals( subMod.ManipulationData[ idx ] ) ) + { + return; + } + + subMod.ManipulationData[ idx ] = manip; + } + else + { + subMod.ManipulationData.Add( manip ); + } + } + + ModOptionChanged.Invoke( ModOptionChangeType.OptionChanged, mod, groupIdx, optionIdx ); + } + + public void OptionSetFile( Mod2 mod, int groupIdx, int optionIdx, Utf8GamePath gamePath, FullPath? newPath ) + { + var subMod = GetSubMod( mod, groupIdx, optionIdx ); + if( OptionSetFile( subMod.FileData, gamePath, newPath ) ) + { + ModOptionChanged.Invoke( ModOptionChangeType.OptionChanged, mod, groupIdx, optionIdx ); + } + } + + public void OptionSetFileSwap( Mod2 mod, int groupIdx, int optionIdx, Utf8GamePath gamePath, FullPath? newPath ) + { + var subMod = GetSubMod( mod, groupIdx, optionIdx ); + if( OptionSetFile( subMod.FileSwapData, gamePath, newPath ) ) + { + ModOptionChanged.Invoke( ModOptionChangeType.OptionChanged, mod, groupIdx, optionIdx ); + } + } + + private bool VerifyFileName( Mod2 mod, IModGroup? group, string newName ) + { + var path = newName.RemoveInvalidPathSymbols(); + if( mod.Options.Any( o => !ReferenceEquals( o, group ) + && string.Equals( o.Name.RemoveInvalidPathSymbols(), path, StringComparison.InvariantCultureIgnoreCase ) ) ) + { + PluginLog.Warning( $"Could not name option {newName} because option with same filename {path} already exists." ); + return false; + } + + return true; + } + + private static SubMod GetSubMod( Mod2 mod, int groupIdx, int optionIdx ) + { + return mod._options[ groupIdx ] switch + { + SingleModGroup s => s.OptionData[ optionIdx ], + MultiModGroup m => m.PrioritizedOptions[ optionIdx ].Mod, + _ => throw new InvalidOperationException(), + }; + } + + private static bool OptionSetFile( IDictionary< Utf8GamePath, FullPath > dict, Utf8GamePath gamePath, FullPath? newPath ) + { + if( dict.TryGetValue( gamePath, out var oldPath ) ) + { + if( newPath == null ) + { + dict.Remove( gamePath ); + return true; + } + + if( newPath.Value.Equals( oldPath ) ) + { + return false; + } + + dict[ gamePath ] = newPath.Value; + return true; + } + + if( newPath == null ) + { + return false; + } + + dict.Add( gamePath, newPath.Value ); + return true; + } + + private static void OnModOptionChange( ModOptionChangeType type, Mod2 mod, int groupIdx, int _ ) + { + // File deletion is handled in the actual function. + if( type != ModOptionChangeType.GroupDeleted ) + { + mod._options[groupIdx].Save( mod.BasePath ); + } + + // State can not change on adding groups, as they have no immediate options. + mod.HasOptions = type switch + { + ModOptionChangeType.GroupDeleted => mod.HasOptions = mod.Options.Any( o => o.IsOption ), + ModOptionChangeType.OptionAdded => mod.HasOptions |= mod._options[groupIdx].IsOption, + ModOptionChangeType.OptionDeleted => mod.HasOptions = mod.Options.Any( o => o.IsOption ), + _ => mod.HasOptions, + }; + } + } +} \ No newline at end of file diff --git a/Penumbra/Mods/Mod2.Manager.Root.cs b/Penumbra/Mods/Mod2.Manager.Root.cs new file mode 100644 index 00000000..85216b66 --- /dev/null +++ b/Penumbra/Mods/Mod2.Manager.Root.cs @@ -0,0 +1,91 @@ +using System; +using System.IO; +using Dalamud.Logging; + +namespace Penumbra.Mods; + +public sealed partial class Mod2 +{ + public sealed partial class Manager + { + public DirectoryInfo BasePath { get; private set; } = null!; + public bool Valid { get; private set; } + + + public event Action? ModDiscoveryStarted; + public event Action? ModDiscoveryFinished; + + public void DiscoverMods( string newDir ) + { + SetBaseDirectory( newDir, false ); + DiscoverMods(); + } + + private void SetBaseDirectory( string newPath, bool firstTime ) + { + if( !firstTime && string.Equals( newPath, Penumbra.Config.ModDirectory, StringComparison.InvariantCultureIgnoreCase ) ) + { + return; + } + + if( newPath.Length == 0 ) + { + Valid = false; + BasePath = new DirectoryInfo( "." ); + } + else + { + var newDir = new DirectoryInfo( newPath ); + if( !newDir.Exists ) + { + try + { + Directory.CreateDirectory( newDir.FullName ); + newDir.Refresh(); + } + catch( Exception e ) + { + PluginLog.Error( $"Could not create specified mod directory {newDir.FullName}:\n{e}" ); + } + } + + BasePath = newDir; + Valid = true; + if( Penumbra.Config.ModDirectory != BasePath.FullName ) + { + Penumbra.Config.ModDirectory = BasePath.FullName; + Penumbra.Config.Save(); + } + } + } + + public void DiscoverMods() + { + ModDiscoveryStarted?.Invoke(); + _mods.Clear(); + BasePath.Refresh(); + + // TODO + //StructuredMods.SubFolders.Clear(); + //StructuredMods.Mods.Clear(); + if( Valid && BasePath.Exists ) + { + foreach( var modFolder in BasePath.EnumerateDirectories() ) + { + //var mod = LoadMod( StructuredMods, modFolder ); + //if( mod == null ) + //{ + // continue; + //} + // + //mod.Index = _mods.Count; + //_mods.Add( mod ); + } + + //SetModStructure(); + } + + ModDiscoveryFinished?.Invoke(); + } + } +} \ No newline at end of file diff --git a/Penumbra/Mods/Mod2.Manager.cs b/Penumbra/Mods/Mod2.Manager.cs new file mode 100644 index 00000000..2ba5fe27 --- /dev/null +++ b/Penumbra/Mods/Mod2.Manager.cs @@ -0,0 +1,35 @@ +using System; +using System.Collections; +using System.Collections.Generic; + +namespace Penumbra.Mods; + +public sealed partial class Mod2 +{ + public sealed partial class Manager : IEnumerable< Mod2 > + { + private readonly List< Mod2 > _mods = new(); + + public Mod2 this[ Index idx ] + => _mods[ idx ]; + + public IReadOnlyList< Mod2 > Mods + => _mods; + + public int Count + => _mods.Count; + + public IEnumerator< Mod2 > GetEnumerator() + => _mods.GetEnumerator(); + + IEnumerator IEnumerable.GetEnumerator() + => GetEnumerator(); + + + public Manager( string modDirectory ) + { + SetBaseDirectory( modDirectory, true ); + ModOptionChanged += OnModOptionChange; + } + } +} \ No newline at end of file diff --git a/Penumbra/Mods/Mod2.Meta.Migration.cs b/Penumbra/Mods/Mod2.Meta.Migration.cs new file mode 100644 index 00000000..a0975396 --- /dev/null +++ b/Penumbra/Mods/Mod2.Meta.Migration.cs @@ -0,0 +1,62 @@ +using System.Collections.Generic; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; +using Penumbra.GameData.ByteString; +using Penumbra.Util; + +namespace Penumbra.Mods; + +public sealed partial class Mod2 +{ + private static class Migration + { + public static void Migrate( Mod2 mod, string text ) + { + MigrateV0ToV1( mod, text ); + } + + private static void MigrateV0ToV1( Mod2 mod, string text ) + { + if( mod.FileVersion > 0 ) + { + return; + } + + var data = JObject.Parse( text ); + var swaps = data[ "FileSwaps" ]?.ToObject< Dictionary< Utf8GamePath, FullPath > >() + ?? new Dictionary< Utf8GamePath, FullPath >(); + var groups = data[ "Groups" ]?.ToObject< Dictionary< string, OptionGroupV0 > >() ?? new Dictionary< string, OptionGroupV0 >(); + foreach( var group in groups.Values ) + { } + + foreach( var swap in swaps ) + { } + } + + + private struct OptionV0 + { + public string OptionName = string.Empty; + public string OptionDesc = string.Empty; + + [JsonProperty( ItemConverterType = typeof( SingleOrArrayConverter< Utf8GamePath > ) )] + public Dictionary< Utf8RelPath, HashSet< Utf8GamePath > > OptionFiles = new(); + + public OptionV0() + { } + } + + private struct OptionGroupV0 + { + public string GroupName = string.Empty; + + [JsonConverter( typeof( Newtonsoft.Json.Converters.StringEnumConverter ) )] + public SelectType SelectionType = SelectType.Single; + + public List< OptionV0 > Options = new(); + + public OptionGroupV0() + { } + } + } +} \ No newline at end of file diff --git a/Penumbra/Mods/Mod2.Meta.cs b/Penumbra/Mods/Mod2.Meta.cs new file mode 100644 index 00000000..0bc6f72d --- /dev/null +++ b/Penumbra/Mods/Mod2.Meta.cs @@ -0,0 +1,121 @@ +using System; +using System.IO; +using Dalamud.Logging; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; +using Penumbra.Util; + +namespace Penumbra.Mods; + +[Flags] +public enum MetaChangeType : byte +{ + None = 0x00, + Name = 0x01, + Author = 0x02, + Description = 0x04, + Version = 0x08, + Website = 0x10, + Deletion = 0x20, +} + +public sealed partial class Mod2 +{ + public const uint CurrentFileVersion = 1; + public uint FileVersion { get; private set; } = CurrentFileVersion; + public LowerString Name { get; private set; } = "Mod"; + public LowerString Author { get; private set; } = LowerString.Empty; + public string Description { get; private set; } = string.Empty; + public string Version { get; private set; } = string.Empty; + public string Website { get; private set; } = string.Empty; + + private void SaveMeta() + => SaveToFile( MetaFile ); + + private MetaChangeType LoadMetaFromFile( FileInfo filePath ) + { + if( !File.Exists( filePath.FullName ) ) + { + return MetaChangeType.Deletion; + } + + try + { + var text = File.ReadAllText( filePath.FullName ); + var json = JObject.Parse( text ); + + var newName = json[ nameof( Name ) ]?.Value< string >() ?? string.Empty; + var newAuthor = json[ nameof( Author ) ]?.Value< string >() ?? string.Empty; + var newDescription = json[ nameof( Description ) ]?.Value< string >() ?? string.Empty; + var newVersion = json[ nameof( Version ) ]?.Value< string >() ?? string.Empty; + var newWebsite = json[ nameof( Website ) ]?.Value< string >() ?? string.Empty; + var newFileVersion = json[ nameof( FileVersion ) ]?.Value< uint >() ?? 0; + + MetaChangeType changes = 0; + if( newFileVersion < CurrentFileVersion ) + { + Migration.Migrate( this, text ); + FileVersion = newFileVersion; + } + + if( Name != newName ) + { + changes |= MetaChangeType.Name; + Name = newName; + } + + if( Author != newAuthor ) + { + changes |= MetaChangeType.Author; + Author = newAuthor; + } + + if( Description != newDescription ) + { + changes |= MetaChangeType.Description; + Description = newDescription; + } + + if( Version != newVersion ) + { + changes |= MetaChangeType.Version; + Version = newVersion; + } + + if( Website != newWebsite ) + { + changes |= MetaChangeType.Website; + Website = newWebsite; + } + + + return changes; + } + catch( Exception e ) + { + PluginLog.Error( $"Could not load mod meta:\n{e}" ); + return MetaChangeType.Deletion; + } + } + + private void SaveToFile( FileInfo filePath ) + { + try + { + var jObject = new JObject + { + { nameof( FileVersion ), JToken.FromObject( FileVersion ) }, + { nameof( Name ), JToken.FromObject( Name ) }, + { nameof( Author ), JToken.FromObject( Author ) }, + { nameof( Description ), JToken.FromObject( Description ) }, + { nameof( Version ), JToken.FromObject( Version ) }, + { nameof( Website ), JToken.FromObject( Website ) }, + }; + File.WriteAllText( filePath.FullName, jObject.ToString( Formatting.Indented ) ); + } + catch( Exception e ) + { + PluginLog.Error( $"Could not write meta file for mod {Name} to {filePath.FullName}:\n{e}" ); + } + } +} \ No newline at end of file diff --git a/Penumbra/Mods/Mod2.SortOrder.cs b/Penumbra/Mods/Mod2.SortOrder.cs new file mode 100644 index 00000000..e3f1cc37 --- /dev/null +++ b/Penumbra/Mods/Mod2.SortOrder.cs @@ -0,0 +1,8 @@ +namespace Penumbra.Mods; + +public sealed partial class Mod2 +{ + public Mod.SortOrder Order; + public override string ToString() + => Order.FullPath; +} \ No newline at end of file diff --git a/Penumbra/Mods/ModManager.Directory.cs b/Penumbra/Mods/ModManager.Directory.cs deleted file mode 100644 index 56673d20..00000000 --- a/Penumbra/Mods/ModManager.Directory.cs +++ /dev/null @@ -1,109 +0,0 @@ -using System; -using System.Collections.Generic; -using System.IO; -using Dalamud.Logging; - -namespace Penumbra.Mods; - -public partial class ModManagerNew -{ - private readonly List< Mod > _mods = new(); - - public IReadOnlyList< Mod > Mods - => _mods; - - public void DiscoverMods() - { - //_mods.Clear(); - // - //if( CheckValidity() ) - //{ - // foreach( var modFolder in BasePath.EnumerateDirectories() ) - // { - // var mod = ModData.LoadMod( StructuredMods, modFolder ); - // if( mod == null ) - // { - // continue; - // } - // - // Mods.Add( modFolder.Name, mod ); - // } - // - // SetModStructure(); - //} - // - //Collections.RecreateCaches(); - } -} - -public partial class ModManagerNew -{ - public DirectoryInfo BasePath { get; private set; } = null!; - public bool Valid { get; private set; } - - public event Action< DirectoryInfo >? BasePathChanged; - - public ModManagerNew() - { - InitBaseDirectory( Penumbra.Config.ModDirectory ); - } - - public bool CheckValidity() - { - if( Valid ) - { - Valid = Directory.Exists( BasePath.FullName ); - } - - return Valid; - } - - private static (DirectoryInfo, bool) CreateDirectory( string path ) - { - var newDir = new DirectoryInfo( path ); - if( !newDir.Exists ) - { - try - { - Directory.CreateDirectory( newDir.FullName ); - newDir.Refresh(); - } - catch( Exception e ) - { - PluginLog.Error( $"Could not create specified mod directory {newDir.FullName}:\n{e}" ); - return ( newDir, false ); - } - } - - return ( newDir, true ); - } - - private void InitBaseDirectory( string path ) - { - if( path.Length == 0 ) - { - Valid = false; - BasePath = new DirectoryInfo( "." ); - return; - } - - ( BasePath, Valid ) = CreateDirectory( path ); - - if( Penumbra.Config.ModDirectory != BasePath.FullName ) - { - Penumbra.Config.ModDirectory = BasePath.FullName; - Penumbra.Config.Save(); - } - } - - private void ChangeBaseDirectory( string path ) - { - if( string.Equals( path, Penumbra.Config.ModDirectory, StringComparison.InvariantCultureIgnoreCase ) ) - { - return; - } - - InitBaseDirectory( path ); - BasePathChanged?.Invoke( BasePath ); - } -} \ No newline at end of file diff --git a/Penumbra/Mods/ModManager.cs b/Penumbra/Mods/ModManager.cs index 8c1f90df..aeae873b 100644 --- a/Penumbra/Mods/ModManager.cs +++ b/Penumbra/Mods/ModManager.cs @@ -45,7 +45,8 @@ public partial class Mod public delegate void ModChangeDelegate( ChangeType type, int modIndex, Mod mod ); public event ModChangeDelegate? ModChange; - public event Action? ModsRediscovered; + public event Action? ModDiscoveryStarted; + public event Action? ModDiscoveryFinished; public bool Valid { get; private set; } @@ -97,8 +98,6 @@ public partial class Mod Config.Save(); } } - - ModsRediscovered?.Invoke(); } public Manager() @@ -150,6 +149,7 @@ public partial class Mod public void DiscoverMods() { + ModDiscoveryStarted?.Invoke(); _mods.Clear(); BasePath.Refresh(); @@ -172,7 +172,7 @@ public partial class Mod SetModStructure(); } - ModsRediscovered?.Invoke(); + ModDiscoveryFinished?.Invoke(); } public void DeleteMod( DirectoryInfo modFolder ) @@ -235,7 +235,7 @@ public partial class Mod { var mod = Mods[ idx ]; var oldName = mod.Meta.Name; - var metaChanges = mod.Meta.RefreshFromFile( mod.MetaFile ) || force; + var metaChanges = mod.Meta.RefreshFromFile( mod.MetaFile ) != 0 || force; var fileChanges = mod.Resources.RefreshModFiles( mod.BasePath ); if( !recomputeMeta && !reloadMeta && !metaChanges && fileChanges == 0 ) diff --git a/Penumbra/Mods/ModMeta.cs b/Penumbra/Mods/ModMeta.cs index 228f3bb2..68469515 100644 --- a/Penumbra/Mods/ModMeta.cs +++ b/Penumbra/Mods/ModMeta.cs @@ -4,6 +4,7 @@ using System.IO; using System.Linq; using Dalamud.Logging; using Newtonsoft.Json; +using Newtonsoft.Json.Linq; using Penumbra.GameData.ByteString; using Penumbra.Util; @@ -12,51 +13,83 @@ namespace Penumbra.Mods; // Contains descriptive data about the mod as well as possible settings and fileswaps. public class ModMeta { - public uint FileVersion { get; set; } + public const uint CurrentFileVersion = 1; + [Flags] + public enum ChangeType : byte + { + Name = 0x01, + Author = 0x02, + Description = 0x04, + Version = 0x08, + Website = 0x10, + Deletion = 0x20, + } + + public uint FileVersion { get; set; } = CurrentFileVersion; public LowerString Name { get; set; } = "Mod"; public LowerString Author { get; set; } = LowerString.Empty; public string Description { get; set; } = string.Empty; public string Version { get; set; } = string.Empty; public string Website { get; set; } = string.Empty; - [JsonProperty( ItemConverterType = typeof( FullPath.FullPathConverter ) )] - public Dictionary< Utf8GamePath, FullPath > FileSwaps { get; set; } = new(); + public bool HasGroupsWithConfig = false; - public Dictionary< string, OptionGroup > Groups { get; set; } = new(); + public bool RefreshHasGroupsWithConfig() + { + var oldValue = HasGroupsWithConfig; + HasGroupsWithConfig = Groups.Values.Any( g => g.Options.Count > 1 || g.SelectionType == SelectType.Multi && g.Options.Count == 1 ); + return oldValue != HasGroupsWithConfig; + } - [JsonIgnore] - private int FileHash { get; set; } - - [JsonIgnore] - public bool HasGroupsWithConfig { get; private set; } - - public bool RefreshFromFile( FileInfo filePath ) + public ChangeType RefreshFromFile( FileInfo filePath ) { var newMeta = LoadFromFile( filePath ); if( newMeta == null ) { - return true; + return ChangeType.Deletion; } - if( newMeta.FileHash == FileHash ) + ChangeType changes = 0; + + if( Name != newMeta.Name ) { - return false; + changes |= ChangeType.Name; + Name = newMeta.Name; } - FileVersion = newMeta.FileVersion; - Name = newMeta.Name; - Author = newMeta.Author; - Description = newMeta.Description; - Version = newMeta.Version; - Website = newMeta.Website; - FileSwaps = newMeta.FileSwaps; - Groups = newMeta.Groups; - FileHash = newMeta.FileHash; - HasGroupsWithConfig = newMeta.HasGroupsWithConfig; - return true; + if( Author != newMeta.Author ) + { + changes |= ChangeType.Author; + Author = newMeta.Author; + } + + if( Description != newMeta.Description ) + { + changes |= ChangeType.Description; + Description = newMeta.Description; + } + + if( Version != newMeta.Version ) + { + changes |= ChangeType.Version; + Version = newMeta.Version; + } + + if( Website != newMeta.Website ) + { + changes |= ChangeType.Website; + Website = newMeta.Website; + } + + return changes; } + [JsonProperty( ItemConverterType = typeof( FullPath.FullPathConverter ) )] + public Dictionary< Utf8GamePath, FullPath > FileSwaps { get; set; } = new(); + + public Dictionary< string, OptionGroup > Groups { get; set; } = new(); + public static ModMeta? LoadFromFile( FileInfo filePath ) { try @@ -67,8 +100,8 @@ public class ModMeta new JsonSerializerSettings { NullValueHandling = NullValueHandling.Ignore } ); if( meta != null ) { - meta.FileHash = text.GetHashCode(); meta.RefreshHasGroupsWithConfig(); + Migration.Migrate( meta, text ); } return meta; @@ -80,29 +113,71 @@ public class ModMeta } } - public bool RefreshHasGroupsWithConfig() - { - var oldValue = HasGroupsWithConfig; - HasGroupsWithConfig = Groups.Values.Any( g => g.Options.Count > 1 || g.SelectionType == SelectType.Multi && g.Options.Count == 1 ); - return oldValue != HasGroupsWithConfig; - } - public void SaveToFile( FileInfo filePath ) { try { - var text = JsonConvert.SerializeObject( this, Formatting.Indented ); - var newHash = text.GetHashCode(); - if( newHash != FileHash ) - { - File.WriteAllText( filePath.FullName, text ); - FileHash = newHash; - } + var text = JsonConvert.SerializeObject( this, Formatting.Indented ); + File.WriteAllText( filePath.FullName, text ); } catch( Exception e ) { PluginLog.Error( $"Could not write meta file for mod {Name} to {filePath.FullName}:\n{e}" ); } } + + private static class Migration + { + public static void Migrate( ModMeta meta, string text ) + { + MigrateV0ToV1( meta, text ); + } + + private static void MigrateV0ToV1( ModMeta meta, string text ) + { + if( meta.FileVersion > 0 ) + { + return; + } + + var data = JObject.Parse( text ); + var swaps = data[ "FileSwaps" ]?.ToObject< Dictionary< Utf8GamePath, FullPath > >() + ?? new Dictionary< Utf8GamePath, FullPath >(); + var groups = data[ "Groups" ]?.ToObject< Dictionary< string, OptionGroupV0 > >() ?? new Dictionary< string, OptionGroupV0 >(); + foreach( var group in groups.Values ) + { } + + foreach( var swap in swaps ) + { } + + //var meta = + } + + + private struct OptionV0 + { + public string OptionName = string.Empty; + public string OptionDesc = string.Empty; + + [JsonProperty( ItemConverterType = typeof( SingleOrArrayConverter< Utf8GamePath > ) )] + public Dictionary< Utf8RelPath, HashSet< Utf8GamePath > > OptionFiles = new(); + + public OptionV0() + { } + } + + private struct OptionGroupV0 + { + public string GroupName = string.Empty; + + [JsonConverter( typeof( Newtonsoft.Json.Converters.StringEnumConverter ) )] + public SelectType SelectionType = SelectType.Single; + + public List< OptionV0 > Options = new(); + + public OptionGroupV0() + { } + } + } } \ No newline at end of file diff --git a/Penumbra/UI/MenuTabs/TabInstalled/TabInstalledDetailsManipulations.cs b/Penumbra/UI/MenuTabs/TabInstalled/TabInstalledDetailsManipulations.cs index a61f6e90..8331ceb8 100644 --- a/Penumbra/UI/MenuTabs/TabInstalled/TabInstalledDetailsManipulations.cs +++ b/Penumbra/UI/MenuTabs/TabInstalled/TabInstalledDetailsManipulations.cs @@ -393,7 +393,7 @@ public partial class SettingsInterface { var ret = false; var id = list[ manipIdx ].Est; - var val = id.SkeletonIdx; + var val = id.Entry; if( ImGui.BeginPopup( $"##MetaPopup{manipIdx}" ) ) { @@ -570,7 +570,7 @@ public partial class SettingsInterface case MetaManipulation.Type.Est: changes = DrawEstRow( manipIdx, list ); ImGui.TableSetColumnIndex( 9 ); - if( ImGui.Selectable( $"{list[ manipIdx ].Est.SkeletonIdx}##{manipIdx}" ) ) + if( ImGui.Selectable( $"{list[ manipIdx ].Est.Entry}##{manipIdx}" ) ) { ImGui.OpenPopup( $"##MetaPopup{manipIdx}" ); } diff --git a/Penumbra/Util/Functions.cs b/Penumbra/Util/Functions.cs new file mode 100644 index 00000000..a3c50e4a --- /dev/null +++ b/Penumbra/Util/Functions.cs @@ -0,0 +1,19 @@ +using System; +using System.Runtime.CompilerServices; + +namespace Penumbra.Util; + +public static class Functions +{ + [MethodImpl( MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization )] + public static bool SetDifferent< T >( T oldValue, T newValue, Action< T > set ) where T : IEquatable< T > + { + if( oldValue.Equals( newValue ) ) + { + return false; + } + + set( newValue ); + return true; + } +} \ No newline at end of file