diff --git a/Penumbra/API/ModsController.cs b/Penumbra/API/ModsController.cs index f466b476..3535380c 100644 --- a/Penumbra/API/ModsController.cs +++ b/Penumbra/API/ModsController.cs @@ -1,8 +1,10 @@ +using System.Collections.Generic; using System.Linq; using EmbedIO; using EmbedIO.Routing; using EmbedIO.WebApi; using Penumbra.Mods; +using Penumbra.Util; namespace Penumbra.API { @@ -10,37 +12,38 @@ namespace Penumbra.API { private readonly Plugin _plugin; - public ModsController( Plugin plugin ) => _plugin = plugin; + public ModsController( Plugin plugin ) + => _plugin = plugin; [Route( HttpVerbs.Get, "/mods" )] public object? GetMods() { var modManager = Service< ModManager >.Get(); - return modManager.Mods?.ModSettings.Select( x => new - { - x.Enabled, - x.Priority, - x.FolderName, - x.Mod.Meta, - BasePath = x.Mod.ModBasePath.FullName, - Files = x.Mod.ModFiles.Select( fi => fi.FullName ) - } ); + return modManager.CurrentCollection.Cache?.AvailableMods.Select( x => new + { + x.Settings.Enabled, + x.Settings.Priority, + x.Data.BasePath.Name, + x.Data.Meta, + BasePath = x.Data.BasePath.FullName, + Files = x.Data.Resources.ModFiles.Select( fi => fi.FullName ), + } ) + ?? null; } [Route( HttpVerbs.Post, "/mods" )] public object CreateMod() - { - return new { }; - } + => new { }; [Route( HttpVerbs.Get, "/files" )] public object GetFiles() { var modManager = Service< ModManager >.Get(); - return modManager.ResolvedFiles.ToDictionary( - o => o.Key, - o => o.Value.FullName - ); + return modManager.CurrentCollection.Cache?.ResolvedFiles.ToDictionary( + o => ( string )o.Key, + o => o.Value.FullName + ) + ?? new Dictionary< string, string >(); } } } \ No newline at end of file diff --git a/Penumbra/Configuration.cs b/Penumbra/Configuration.cs index e1084499..4760f258 100644 --- a/Penumbra/Configuration.cs +++ b/Penumbra/Configuration.cs @@ -1,14 +1,16 @@ using System; -using System.Collections.Generic; using Dalamud.Configuration; using Dalamud.Plugin; +using Penumbra.Util; namespace Penumbra { [Serializable] public class Configuration : IPluginConfiguration { - public int Version { get; set; } = 0; + private const int CurrentVersion = 1; + + public int Version { get; set; } = CurrentVersion; public bool IsEnabled { get; set; } = true; @@ -18,25 +20,39 @@ namespace Penumbra public bool EnableHttpApi { get; set; } - public string CurrentCollection { get; set; } = @"D:/ffxiv/fs_mods/"; + public string ModDirectory { get; set; } = @"D:/ffxiv/fs_mods/"; - public List< string > ModCollections { get; set; } = new(); + public string CurrentCollection { get; set; } = "Default"; - public bool InvertModListOrder { get; set; } + public bool InvertModListOrder { internal get; set; } - // the below exist just to make saving less cumbersome - - [NonSerialized] - private DalamudPluginInterface? _pluginInterface; - - public void Initialize( DalamudPluginInterface pluginInterface ) + public static Configuration Load( DalamudPluginInterface pi ) { - _pluginInterface = pluginInterface; + var configuration = pi.GetPluginConfig() as Configuration ?? new Configuration(); + if( configuration.Version == CurrentVersion ) + { + return configuration; + } + + MigrateConfiguration.Version0To1( configuration ); + configuration.Save( pi ); + + return configuration; + } + + public void Save( DalamudPluginInterface pi ) + { + try + { + pi.SavePluginConfig( this ); + } + catch( Exception e ) + { + PluginLog.Error( $"Could not save plugin configuration:\n{e}" ); + } } public void Save() - { - _pluginInterface?.SavePluginConfig( this ); - } + => Save( Service< DalamudPluginInterface >.Get() ); } } \ No newline at end of file diff --git a/Penumbra/Game/Enums/BodySlot.cs b/Penumbra/Game/Enums/BodySlot.cs index f8474e5e..296070a6 100644 --- a/Penumbra/Game/Enums/BodySlot.cs +++ b/Penumbra/Game/Enums/BodySlot.cs @@ -1,7 +1,7 @@ using System.Collections.Generic; using System.ComponentModel; -namespace Penumbra.Game +namespace Penumbra.Game.Enums { public enum BodySlot : byte { @@ -10,7 +10,7 @@ namespace Penumbra.Game Face, Tail, Body, - Zear + Zear, } public static class BodySlotEnumExtension @@ -24,7 +24,7 @@ namespace Penumbra.Game BodySlot.Hair => "hair", BodySlot.Body => "body", BodySlot.Tail => "tail", - _ => throw new InvalidEnumArgumentException() + _ => throw new InvalidEnumArgumentException(), }; } } @@ -37,7 +37,7 @@ namespace Penumbra.Game { BodySlot.Face.ToSuffix(), BodySlot.Face }, { BodySlot.Hair.ToSuffix(), BodySlot.Hair }, { BodySlot.Body.ToSuffix(), BodySlot.Body }, - { BodySlot.Tail.ToSuffix(), BodySlot.Tail } + { BodySlot.Tail.ToSuffix(), BodySlot.Tail }, }; } } \ No newline at end of file diff --git a/Penumbra/Game/Enums/CustomizationType.cs b/Penumbra/Game/Enums/CustomizationType.cs index ac82da2c..4f380537 100644 --- a/Penumbra/Game/Enums/CustomizationType.cs +++ b/Penumbra/Game/Enums/CustomizationType.cs @@ -1,7 +1,7 @@ using System.Collections.Generic; using System.ComponentModel; -namespace Penumbra.Game +namespace Penumbra.Game.Enums { public enum CustomizationType : byte { @@ -15,7 +15,7 @@ namespace Penumbra.Game DecalFace, DecalEquip, Skin, - Etc + Etc, } public static class CustomizationTypeEnumExtension @@ -30,7 +30,7 @@ namespace Penumbra.Game CustomizationType.Hair => "hir", CustomizationType.Tail => "til", CustomizationType.Etc => "etc", - _ => throw new InvalidEnumArgumentException() + _ => throw new InvalidEnumArgumentException(), }; } } @@ -44,7 +44,7 @@ namespace Penumbra.Game { CustomizationType.Accessory.ToSuffix(), CustomizationType.Accessory }, { CustomizationType.Hair.ToSuffix(), CustomizationType.Hair }, { CustomizationType.Tail.ToSuffix(), CustomizationType.Tail }, - { CustomizationType.Etc.ToSuffix(), CustomizationType.Etc } + { CustomizationType.Etc.ToSuffix(), CustomizationType.Etc }, }; } } \ No newline at end of file diff --git a/Penumbra/Game/Enums/EquipSlot.cs b/Penumbra/Game/Enums/EquipSlot.cs index 10834112..ba4654bb 100644 --- a/Penumbra/Game/Enums/EquipSlot.cs +++ b/Penumbra/Game/Enums/EquipSlot.cs @@ -1,7 +1,7 @@ using System.Collections.Generic; using System.ComponentModel; -namespace Penumbra.Game +namespace Penumbra.Game.Enums { public enum EquipSlot : byte { @@ -27,7 +27,7 @@ namespace Penumbra.Game FullBody = 19, BodyHands = 20, BodyLegsFeet = 21, - All = 22 + All = 22, } public static class EquipSlotEnumExtension @@ -46,7 +46,7 @@ namespace Penumbra.Game EquipSlot.RingR => "rir", EquipSlot.RingL => "ril", EquipSlot.Wrists => "wrs", - _ => throw new InvalidEnumArgumentException() + _ => throw new InvalidEnumArgumentException(), }; } @@ -59,7 +59,7 @@ namespace Penumbra.Game EquipSlot.Legs => true, EquipSlot.Feet => true, EquipSlot.Body => true, - _ => false + _ => false, }; } @@ -72,7 +72,7 @@ namespace Penumbra.Game EquipSlot.RingR => true, EquipSlot.RingL => true, EquipSlot.Wrists => true, - _ => false + _ => false, }; } } @@ -90,7 +90,7 @@ namespace Penumbra.Game { EquipSlot.Neck.ToSuffix(), EquipSlot.Neck }, { EquipSlot.RingR.ToSuffix(), EquipSlot.RingR }, { EquipSlot.RingL.ToSuffix(), EquipSlot.RingL }, - { EquipSlot.Wrists.ToSuffix(), EquipSlot.Wrists } + { EquipSlot.Wrists.ToSuffix(), EquipSlot.Wrists }, }; } } \ No newline at end of file diff --git a/Penumbra/Game/Enums/FileType.cs b/Penumbra/Game/Enums/FileType.cs index 6f8d92c4..a6db0cf2 100644 --- a/Penumbra/Game/Enums/FileType.cs +++ b/Penumbra/Game/Enums/FileType.cs @@ -1,6 +1,6 @@ using System.Collections.Generic; -namespace Penumbra.Game +namespace Penumbra.Game.Enums { public enum FileType : byte { @@ -16,7 +16,7 @@ namespace Penumbra.Game Model, Shader, Font, - Environment + Environment, } public static partial class GameData @@ -39,7 +39,7 @@ namespace Penumbra.Game { ".shpk", FileType.Shader }, { ".shcd", FileType.Shader }, { ".fdt", FileType.Font }, - { ".envb", FileType.Environment } + { ".envb", FileType.Environment }, }; } } \ No newline at end of file diff --git a/Penumbra/Game/Enums/ObjectType.cs b/Penumbra/Game/Enums/ObjectType.cs index 5c7b7383..b2884512 100644 --- a/Penumbra/Game/Enums/ObjectType.cs +++ b/Penumbra/Game/Enums/ObjectType.cs @@ -1,4 +1,4 @@ -namespace Penumbra.Game +namespace Penumbra.Game.Enums { public enum ObjectType : byte { @@ -16,6 +16,6 @@ namespace Penumbra.Game Equipment, Character, Weapon, - Font + Font, } } \ No newline at end of file diff --git a/Penumbra/Game/Enums/Race.cs b/Penumbra/Game/Enums/Race.cs index c772b25f..aea6fcf6 100644 --- a/Penumbra/Game/Enums/Race.cs +++ b/Penumbra/Game/Enums/Race.cs @@ -2,7 +2,7 @@ using System; using System.Collections.Generic; using System.ComponentModel; -namespace Penumbra.Game +namespace Penumbra.Game.Enums { public enum Gender : byte { @@ -10,7 +10,7 @@ namespace Penumbra.Game Male, Female, MaleNpc, - FemaleNpc + FemaleNpc, } public enum Race : byte @@ -24,7 +24,7 @@ namespace Penumbra.Game Roegadyn, AuRa, Hrothgar, - Viera + Viera, } public enum GenderRace : ushort @@ -63,7 +63,7 @@ namespace Penumbra.Game VieraFemale = 1801, VieraFemaleNpc = 1804, UnknownMaleNpc = 9104, - UnknownFemaleNpc = 9204 + UnknownFemaleNpc = 9204, } public static class RaceEnumExtensions @@ -118,7 +118,7 @@ namespace Penumbra.Game GenderRace.VieraFemaleNpc => ( Gender.FemaleNpc, Race.Viera ), GenderRace.UnknownMaleNpc => ( Gender.MaleNpc, Race.Unknown ), GenderRace.UnknownFemaleNpc => ( Gender.FemaleNpc, Race.Unknown ), - _ => throw new InvalidEnumArgumentException() + _ => throw new InvalidEnumArgumentException(), }; } @@ -163,7 +163,7 @@ namespace Penumbra.Game GenderRace.VieraFemaleNpc => "1804", GenderRace.UnknownMaleNpc => "9104", GenderRace.UnknownFemaleNpc => "9204", - _ => throw new InvalidEnumArgumentException() + _ => throw new InvalidEnumArgumentException(), }; } } @@ -208,7 +208,7 @@ namespace Penumbra.Game "1804" => GenderRace.VieraFemaleNpc, "9104" => GenderRace.UnknownMaleNpc, "9204" => GenderRace.UnknownFemaleNpc, - _ => throw new KeyNotFoundException() + _ => throw new KeyNotFoundException(), }; } @@ -233,7 +233,7 @@ namespace Penumbra.Game Race.Roegadyn => GenderRace.RoegadynMale, Race.AuRa => GenderRace.AuRaMale, Race.Hrothgar => GenderRace.HrothgarMale, - _ => GenderRace.Unknown + _ => GenderRace.Unknown, }, Gender.MaleNpc => race switch { @@ -245,7 +245,7 @@ namespace Penumbra.Game Race.Roegadyn => GenderRace.RoegadynMaleNpc, Race.AuRa => GenderRace.AuRaMaleNpc, Race.Hrothgar => GenderRace.HrothgarMaleNpc, - _ => GenderRace.Unknown + _ => GenderRace.Unknown, }, Gender.Female => race switch { @@ -257,7 +257,7 @@ namespace Penumbra.Game Race.Roegadyn => GenderRace.RoegadynFemale, Race.AuRa => GenderRace.AuRaFemale, Race.Viera => GenderRace.VieraFemale, - _ => GenderRace.Unknown + _ => GenderRace.Unknown, }, Gender.FemaleNpc => race switch { @@ -269,9 +269,9 @@ namespace Penumbra.Game Race.Roegadyn => GenderRace.RoegadynFemaleNpc, Race.AuRa => GenderRace.AuRaFemaleNpc, Race.Viera => GenderRace.VieraFemaleNpc, - _ => GenderRace.Unknown + _ => GenderRace.Unknown, }, - _ => GenderRace.Unknown + _ => GenderRace.Unknown, }; } } diff --git a/Penumbra/Game/EqdpEntry.cs b/Penumbra/Game/EqdpEntry.cs index 18cc4c90..91b7a76f 100644 --- a/Penumbra/Game/EqdpEntry.cs +++ b/Penumbra/Game/EqdpEntry.cs @@ -1,6 +1,7 @@ using System; using System.ComponentModel; -using Penumbra.Mods; +using Penumbra.Game.Enums; +using Penumbra.Meta; namespace Penumbra.Game { @@ -46,7 +47,7 @@ namespace Penumbra.Game RingL1 = 0b0100000000, RingL2 = 0b1000000000, - RingLMask = 0b1100000000 + RingLMask = 0b1100000000, } public static class Eqdp @@ -65,7 +66,7 @@ namespace Penumbra.Game EquipSlot.Wrists => 4, EquipSlot.RingR => 6, EquipSlot.RingL => 8, - _ => throw new InvalidEnumArgumentException() + _ => throw new InvalidEnumArgumentException(), }; } @@ -100,7 +101,7 @@ namespace Penumbra.Game EquipSlot.Wrists => EqdpEntry.WristsMask, EquipSlot.RingR => EqdpEntry.RingRMask, EquipSlot.RingL => EqdpEntry.RingLMask, - _ => 0 + _ => 0, }; } } diff --git a/Penumbra/Game/EqpEntry.cs b/Penumbra/Game/EqpEntry.cs index f32e992c..2bc9ac75 100644 --- a/Penumbra/Game/EqpEntry.cs +++ b/Penumbra/Game/EqpEntry.cs @@ -1,6 +1,7 @@ using System; using System.ComponentModel; -using Penumbra.Mods; +using Penumbra.Game.Enums; +using Penumbra.Meta; namespace Penumbra.Game { @@ -79,7 +80,7 @@ namespace Penumbra.Game _61 = 0x20_00_00ul << 40, _62 = 0x40_00_00ul << 40, _63 = 0x80_00_00ul << 40, - HeadMask = 0xFF_FF_FFul << 40 + HeadMask = 0xFF_FF_FFul << 40, } public static class Eqp @@ -93,7 +94,7 @@ namespace Penumbra.Game EquipSlot.Hands => ( 1, 3 ), EquipSlot.Feet => ( 1, 4 ), EquipSlot.Head => ( 3, 5 ), - _ => throw new InvalidEnumArgumentException() + _ => throw new InvalidEnumArgumentException(), }; } @@ -123,7 +124,7 @@ namespace Penumbra.Game EquipSlot.Legs => EqpEntry.LegsMask, EquipSlot.Feet => EqpEntry.FeetMask, EquipSlot.Hands => EqpEntry.HandsMask, - _ => 0 + _ => 0, }; } } diff --git a/Penumbra/Game/GameObjectInfo.cs b/Penumbra/Game/GameObjectInfo.cs index 3443cdf0..927944b5 100644 --- a/Penumbra/Game/GameObjectInfo.cs +++ b/Penumbra/Game/GameObjectInfo.cs @@ -1,6 +1,7 @@ using System; using System.Runtime.InteropServices; using Dalamud; +using Penumbra.Game.Enums; namespace Penumbra.Game { @@ -16,7 +17,7 @@ namespace Penumbra.Game PrimaryId = setId, GenderRace = gr, Variant = variant, - EquipSlot = slot + EquipSlot = slot, }; public static GameObjectInfo Weapon( FileType type, ushort setId, ushort weaponId, byte variant = 0 ) @@ -26,7 +27,7 @@ namespace Penumbra.Game ObjectType = ObjectType.Weapon, PrimaryId = setId, SecondaryId = weaponId, - Variant = variant + Variant = variant, }; public static GameObjectInfo Customization( FileType type, CustomizationType customizationType, ushort id = 0 @@ -39,7 +40,7 @@ namespace Penumbra.Game GenderRace = gr, BodySlot = bodySlot, Variant = variant, - CustomizationType = customizationType + CustomizationType = customizationType, }; public static GameObjectInfo Monster( FileType type, ushort monsterId, ushort bodyId, byte variant = 0 ) @@ -49,7 +50,7 @@ namespace Penumbra.Game ObjectType = ObjectType.Monster, PrimaryId = monsterId, SecondaryId = bodyId, - Variant = variant + Variant = variant, }; public static GameObjectInfo DemiHuman( FileType type, ushort demiHumanId, ushort bodyId, byte variant = 0, @@ -61,7 +62,7 @@ namespace Penumbra.Game PrimaryId = demiHumanId, SecondaryId = bodyId, Variant = variant, - EquipSlot = slot + EquipSlot = slot, }; public static GameObjectInfo Map( FileType type, byte c1, byte c2, byte c3, byte c4, byte variant, byte suffix = 0 ) @@ -74,7 +75,7 @@ namespace Penumbra.Game MapC3 = c3, MapC4 = c4, MapSuffix = suffix, - Variant = variant + Variant = variant, }; public static GameObjectInfo Icon( FileType type, uint iconId, bool hq, ClientLanguage lang = ClientLanguage.English ) @@ -84,7 +85,7 @@ namespace Penumbra.Game ObjectType = ObjectType.Map, IconId = iconId, IconHq = hq, - Language = lang + Language = lang, }; [FieldOffset( 0 )] diff --git a/Penumbra/Game/GamePathParser.cs b/Penumbra/Game/GamePathParser.cs index bdd7d935..0ecbf54b 100644 --- a/Penumbra/Game/GamePathParser.cs +++ b/Penumbra/Game/GamePathParser.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using System.Text; using System.Text.RegularExpressions; using Dalamud.Plugin; +using Penumbra.Game.Enums; using Penumbra.Util; namespace Penumbra.Game @@ -61,7 +62,7 @@ namespace Penumbra.Game , { ObjectType.Monster, new Regex[]{ new(@"chara/monster/m(?'monster'\d{4})/obj/body/b(?'id'\d{4})/b\k'id'\.imc") } } , { ObjectType.Equipment, new Regex[]{ new(@"chara/equipment/e(?'id'\d{4})/e\k'id'\.imc") } } , { ObjectType.DemiHuman, new Regex[]{ new(@"chara/demihuman/d(?'id'\d{4})/obj/equipment/e(?'equip'\d{4})/e\k'equip'\.imc") } } - , { ObjectType.Accessory, new Regex[]{ new(@"chara/accessory/a(?'id'\d{4})/a\k'id'\.imc") } } } } + , { ObjectType.Accessory, new Regex[]{ new(@"chara/accessory/a(?'id'\d{4})/a\k'id'\.imc") } } } }, }; // @formatter:on @@ -90,7 +91,7 @@ namespace Penumbra.Game DemiHumanFolder => ObjectType.DemiHuman, MonsterFolder => ObjectType.Monster, CommonFolder => ObjectType.Character, - _ => ObjectType.Unknown + _ => ObjectType.Unknown, }, UiFolder => folders[ 1 ] switch { @@ -98,22 +99,22 @@ namespace Penumbra.Game LoadingFolder => ObjectType.LoadingScreen, MapFolder => ObjectType.Map, InterfaceFolder => ObjectType.Interface, - _ => ObjectType.Unknown + _ => ObjectType.Unknown, }, CommonFolder => folders[ 1 ] switch { FontFolder => ObjectType.Font, - _ => ObjectType.Unknown + _ => ObjectType.Unknown, }, HousingFolder => ObjectType.Housing, WorldFolder1 => folders[ 1 ] switch { HousingFolder => ObjectType.Housing, - _ => ObjectType.World + _ => ObjectType.World, }, WorldFolder2 => ObjectType.World, VfxFolder => ObjectType.Vfx, - _ => ObjectType.Unknown + _ => ObjectType.Unknown, }; } @@ -257,8 +258,8 @@ namespace Penumbra.Game var id = ushort.Parse( groups[ "id" ].Value ); if( groups[ "location" ].Success ) { - var tmpType = groups[ "location" ].Value == "face" ? CustomizationType.DecalFace - : groups[ "location" ].Value == "equip" ? CustomizationType.DecalEquip : CustomizationType.Unknown; + var tmpType = groups[ "location" ].Value == "face" ? CustomizationType.DecalFace + : groups[ "location" ].Value == "equip" ? CustomizationType.DecalEquip : CustomizationType.Unknown; return GameObjectInfo.Customization( fileType, tmpType, id ); } @@ -297,7 +298,7 @@ namespace Penumbra.Game "ja" => Dalamud.ClientLanguage.Japanese, "de" => Dalamud.ClientLanguage.German, "fr" => Dalamud.ClientLanguage.French, - _ => Dalamud.ClientLanguage.English + _ => Dalamud.ClientLanguage.English, }; return GameObjectInfo.Icon( fileType, id, hq, language ); } diff --git a/Penumbra/Game/GmpEntry.cs b/Penumbra/Game/GmpEntry.cs index 5a78cd6b..000b2c13 100644 --- a/Penumbra/Game/GmpEntry.cs +++ b/Penumbra/Game/GmpEntry.cs @@ -1,5 +1,5 @@ using System.IO; -using Penumbra.Mods; +using Penumbra.Meta; namespace Penumbra.Game { diff --git a/Penumbra/Game/RefreshActors.cs b/Penumbra/Game/RefreshActors.cs index 573f26db..d3e70e95 100644 --- a/Penumbra/Game/RefreshActors.cs +++ b/Penumbra/Game/RefreshActors.cs @@ -44,7 +44,7 @@ namespace Penumbra.Game RedrawAll( actors ); } - foreach( var actor in actors.Where( A => A.Name == name ) ) + foreach( var actor in actors.Where( a => a.Name == name ) ) { Redraw( actor ); } diff --git a/Penumbra/Hooks/GameResourceManagement.cs b/Penumbra/Hooks/GameResourceManagement.cs index 7b63b30e..80038be1 100644 --- a/Penumbra/Hooks/GameResourceManagement.cs +++ b/Penumbra/Hooks/GameResourceManagement.cs @@ -30,11 +30,14 @@ namespace Penumbra.Hooks // Object addresses private readonly IntPtr _playerResourceManagerAddress; - public IntPtr PlayerResourceManagerPtr => Marshal.ReadIntPtr( _playerResourceManagerAddress ); + + public IntPtr PlayerResourceManagerPtr + => Marshal.ReadIntPtr( _playerResourceManagerAddress ); + private readonly IntPtr _characterResourceManagerAddress; - public unsafe CharacterResourceManager* CharacterResourceManagerPtr => - ( CharacterResourceManager* )Marshal.ReadIntPtr( _characterResourceManagerAddress ).ToPointer(); + public unsafe CharacterResourceManager* CharacterResourceManagerPtr + => ( CharacterResourceManager* )Marshal.ReadIntPtr( _characterResourceManagerAddress ).ToPointer(); public GameResourceManagement( DalamudPluginInterface pluginInterface ) { @@ -70,7 +73,7 @@ namespace Penumbra.Hooks public unsafe string ResourceToPath( byte* resource ) => Marshal.PtrToStringAnsi( new IntPtr( *( char** )( resource + 9 * 8 ) ) )!; - public unsafe void ReloadCharacterResources() + private unsafe void ReloadCharacterResources() { var oldResources = new IntPtr[NumResources]; var resources = new IntPtr( &CharacterResourceManagerPtr->Resources ); @@ -88,9 +91,9 @@ namespace Penumbra.Hooks continue; } - PluginLog.Debug( "Freeing " + - $"{ResourceToPath( ( byte* )oldResources[ i ].ToPointer() )}, replaced with " + - $"{ResourceToPath( ( byte* )pResources[ i ] )}" ); + PluginLog.Debug( "Freeing " + + $"{ResourceToPath( ( byte* )oldResources[ i ].ToPointer() )}, replaced with " + + $"{ResourceToPath( ( byte* )pResources[ i ] )}" ); UnloadCharacterResource( oldResources[ i ] ); } diff --git a/Penumbra/Hooks/MusicManager.cs b/Penumbra/Hooks/MusicManager.cs new file mode 100644 index 00000000..82fc08a1 --- /dev/null +++ b/Penumbra/Hooks/MusicManager.cs @@ -0,0 +1,45 @@ +using System; +using Dalamud.Plugin; + +namespace Penumbra.Hooks +{ + public unsafe class MusicManager + { + private readonly IntPtr _musicManager; + + public MusicManager( Plugin plugin ) + { + var scanner = plugin!.PluginInterface!.TargetModuleScanner; + var framework = plugin.PluginInterface.Framework.Address.BaseAddress; + + // the wildcard is basically the framework offset we want (lol) + // .text:000000000009051A 48 8B 8E 18 2A 00 00 mov rcx, [rsi+2A18h] + // .text:0000000000090521 39 78 20 cmp [rax+20h], edi + // .text:0000000000090524 0F 94 C2 setz dl + // .text:0000000000090527 45 33 C0 xor r8d, r8d + // .text:000000000009052A E8 41 1C 15 00 call musicInit + var musicInitCallLocation = scanner.ScanText( "48 8B 8E ?? ?? ?? ?? 39 78 20 0F 94 C2 45 33 C0" ); + var musicManagerOffset = *( int* )( musicInitCallLocation + 3 ); + PluginLog.Debug( "Found MusicInitCall location at 0x{Location:X16}. Framework offset for MusicManager is 0x{Offset:X8}", + musicInitCallLocation.ToInt64(), musicManagerOffset ); + _musicManager = *( IntPtr* )( framework + musicManagerOffset ); + PluginLog.Debug( "MusicManager found at 0x{Location:X16}", _musicManager ); + } + + public bool StreamingEnabled + { + get => *( bool* )( _musicManager + 50 ); + private set + { + PluginLog.Debug( value ? "Music streaming enabled." : "Music streaming disabled." ); + *( bool* )( _musicManager + 50 ) = value; + } + } + + public void EnableStreaming() + => StreamingEnabled = true; + + public void DisableStreaming() + => StreamingEnabled = false; + } +} \ No newline at end of file diff --git a/Penumbra/Hooks/ResourceLoader.cs b/Penumbra/Hooks/ResourceLoader.cs index b2e16ca8..199038be 100644 --- a/Penumbra/Hooks/ResourceLoader.cs +++ b/Penumbra/Hooks/ResourceLoader.cs @@ -2,6 +2,7 @@ using System; using System.IO; using System.Runtime.InteropServices; using System.Text; +using System.Text.RegularExpressions; using Dalamud.Plugin; using Penumbra.Mods; using Penumbra.Structs; @@ -30,12 +31,12 @@ namespace Penumbra.Hooks public unsafe delegate byte ReadSqpackPrototype( IntPtr pFileHandler, SeFileDescriptor* pFileDesc, int priority, bool isSync ); [Function( CallingConventions.Microsoft )] - public unsafe delegate void* GetResourceSyncPrototype( IntPtr pFileManager, uint* pCategoryId, char* pResourceType, - uint* pResourceHash, char* pPath, void* pUnknown ); + public unsafe delegate void* GetResourceSyncPrototype( IntPtr pFileManager, uint* pCategoryId, char* pResourceType + , uint* pResourceHash, char* pPath, void* pUnknown ); [Function( CallingConventions.Microsoft )] - public unsafe delegate void* GetResourceAsyncPrototype( IntPtr pFileManager, uint* pCategoryId, char* pResourceType, - uint* pResourceHash, char* pPath, void* pUnknown, bool isUnknown ); + public unsafe delegate void* GetResourceAsyncPrototype( IntPtr pFileManager, uint* pCategoryId, char* pResourceType + , uint* pResourceHash, char* pPath, void* pUnknown, bool isUnknown ); // Hooks public IHook< GetResourceSyncPrototype >? GetResourceSyncHook { get; private set; } @@ -46,7 +47,8 @@ namespace Penumbra.Hooks public ReadFilePrototype? ReadFile { get; private set; } - public bool LogAllFiles = false; + public bool LogAllFiles = false; + public Regex? LogFileFilter = null; public ResourceLoader( Plugin plugin ) @@ -87,7 +89,8 @@ namespace Penumbra.Hooks uint* pResourceHash, char* pPath, void* pUnknown - ) => GetResourceHandler( true, pFileManager, pCategoryId, pResourceType, pResourceHash, pPath, pUnknown, false ); + ) + => GetResourceHandler( true, pFileManager, pCategoryId, pResourceType, pResourceHash, pPath, pUnknown, false ); private unsafe void* GetResourceAsyncHandler( IntPtr pFileManager, @@ -97,7 +100,8 @@ namespace Penumbra.Hooks char* pPath, void* pUnknown, bool isUnknown - ) => GetResourceHandler( false, pFileManager, pCategoryId, pResourceType, pResourceHash, pPath, pUnknown, isUnknown ); + ) + => GetResourceHandler( false, pFileManager, pCategoryId, pResourceType, pResourceHash, pPath, pUnknown, isUnknown ); private unsafe void* CallOriginalHandler( bool isSync, @@ -131,34 +135,39 @@ namespace Penumbra.Hooks } private unsafe void* GetResourceHandler( - bool isSync, + bool isSync, IntPtr pFileManager, - uint* pCategoryId, - char* pResourceType, - uint* pResourceHash, - char* pPath, - void* pUnknown, - bool isUnknown + uint* pCategoryId, + char* pResourceType, + uint* pResourceHash, + char* pPath, + void* pUnknown, + bool isUnknown ) { - var modManager = Service< ModManager >.Get(); + string file; + var modManager = Service< ModManager >.Get(); if( !Plugin!.Configuration!.IsEnabled || modManager == null ) { if( LogAllFiles ) { - PluginLog.Log( "[GetResourceHandler] {0}", - GamePath.GenerateUncheckedLower( Marshal.PtrToStringAnsi( new IntPtr( pPath ) )! ) ); + file = Marshal.PtrToStringAnsi( new IntPtr( pPath ) )!; + if( LogFileFilter == null || LogFileFilter.IsMatch( file ) ) + { + PluginLog.Log( "[GetResourceHandler] {0}", file ); + } } return CallOriginalHandler( isSync, pFileManager, pCategoryId, pResourceType, pResourceHash, pPath, pUnknown, isUnknown ); } - var gameFsPath = GamePath.GenerateUncheckedLower( Marshal.PtrToStringAnsi( new IntPtr( pPath ) )! ); - var replacementPath = modManager.ResolveSwappedOrReplacementFilePath( gameFsPath ); - if( LogAllFiles ) + file = Marshal.PtrToStringAnsi( new IntPtr( pPath ) )!; + var gameFsPath = GamePath.GenerateUncheckedLower( file ); + var replacementPath = modManager.CurrentCollection.ResolveSwappedOrReplacementPath( gameFsPath ); + if( LogAllFiles && ( LogFileFilter == null || LogFileFilter.IsMatch( file ) ) ) { - PluginLog.Log( "[GetResourceHandler] {0}", gameFsPath ); + PluginLog.Log( "[GetResourceHandler] {0}", file ); } // path must be < 260 because statically defined array length :( diff --git a/Penumbra/Hooks/SoundShit.cs b/Penumbra/Hooks/SoundShit.cs deleted file mode 100644 index de50eafd..00000000 --- a/Penumbra/Hooks/SoundShit.cs +++ /dev/null @@ -1,40 +0,0 @@ -using System; -using System.Runtime.InteropServices; -using Dalamud.Plugin; -using Reloaded.Hooks; -using Reloaded.Hooks.Definitions; -using Reloaded.Hooks.Definitions.X64; - -namespace Penumbra.Hooks -{ - public unsafe class SoundShit - { - private readonly IntPtr _musicManager; - - public SoundShit( Plugin plugin ) - { - var scanner = plugin!.PluginInterface!.TargetModuleScanner; - - var fw = plugin.PluginInterface.Framework.Address.BaseAddress; - - // the wildcard is basically the framework offset we want (lol) - // .text:000000000009051A 48 8B 8E 18 2A 00 00 mov rcx, [rsi+2A18h] - // .text:0000000000090521 39 78 20 cmp [rax+20h], edi - // .text:0000000000090524 0F 94 C2 setz dl - // .text:0000000000090527 45 33 C0 xor r8d, r8d - // .text:000000000009052A E8 41 1C 15 00 call musicInit - var shit = scanner.ScanText( "48 8B 8E ?? ?? ?? ?? 39 78 20 0F 94 C2 45 33 C0" ); - var fuckkk = *( int* )( shit + 3 ); - _musicManager = *( IntPtr* )( fw + fuckkk ); - StreamingEnabled = false; - - // PluginLog.Information("disabled streaming: {addr}", _musicManager); - } - - public bool StreamingEnabled - { - get => *( bool* )( _musicManager + 50 ); - set => *( bool* )( _musicManager + 50 ) = value; - } - } -} \ No newline at end of file diff --git a/Penumbra/Importer/ImporterState.cs b/Penumbra/Importer/ImporterState.cs index c8615d90..608976fc 100644 --- a/Penumbra/Importer/ImporterState.cs +++ b/Penumbra/Importer/ImporterState.cs @@ -5,6 +5,6 @@ namespace Penumbra.Importer None, WritingPackToDisk, ExtractingModFiles, - Done + Done, } } \ No newline at end of file diff --git a/Penumbra/Importer/MagicTempFileStreamManagerAndDeleterFuckery.cs b/Penumbra/Importer/MagicTempFileStreamManagerAndDeleter.cs similarity index 59% rename from Penumbra/Importer/MagicTempFileStreamManagerAndDeleterFuckery.cs rename to Penumbra/Importer/MagicTempFileStreamManagerAndDeleter.cs index 639edd6c..10be9f15 100644 --- a/Penumbra/Importer/MagicTempFileStreamManagerAndDeleterFuckery.cs +++ b/Penumbra/Importer/MagicTempFileStreamManagerAndDeleter.cs @@ -1,15 +1,16 @@ using System; using System.IO; -using Lumina.Data; using Penumbra.Util; namespace Penumbra.Importer { - public class MagicTempFileStreamManagerAndDeleterFuckery : PenumbraSqPackStream, IDisposable + public class MagicTempFileStreamManagerAndDeleter : PenumbraSqPackStream, IDisposable { private readonly FileStream _fileStream; - public MagicTempFileStreamManagerAndDeleterFuckery( FileStream stream ) : base( stream ) => _fileStream = stream; + public MagicTempFileStreamManagerAndDeleter( FileStream stream ) + : base( stream ) + => _fileStream = stream; public new void Dispose() { diff --git a/Penumbra/Importer/Models/ExtendedModPack.cs b/Penumbra/Importer/Models/ExtendedModPack.cs index 1696012b..91bb5d01 100644 --- a/Penumbra/Importer/Models/ExtendedModPack.cs +++ b/Penumbra/Importer/Models/ExtendedModPack.cs @@ -1,5 +1,5 @@ using System.Collections.Generic; -using Penumbra.Models; +using Penumbra.Structs; namespace Penumbra.Importer.Models { diff --git a/Penumbra/Importer/TexToolsImport.cs b/Penumbra/Importer/TexToolsImport.cs index 2bfaad50..6588148a 100644 --- a/Penumbra/Importer/TexToolsImport.cs +++ b/Penumbra/Importer/TexToolsImport.cs @@ -7,8 +7,10 @@ using Dalamud.Plugin; using ICSharpCode.SharpZipLib.Zip; using Newtonsoft.Json; using Penumbra.Importer.Models; -using Penumbra.Models; +using Penumbra.Mod; +using Penumbra.Structs; using Penumbra.Util; +using FileMode = System.IO.FileMode; namespace Penumbra.Importer { @@ -46,6 +48,12 @@ namespace Penumbra.Importer _resolvedTempFilePath = Path.Combine( _outDirectory.FullName, TempFileName ); } + private static string ReplaceBadXivSymbols( string source ) + => source.ReplaceInvalidPathSymbols().RemoveNonAsciiSymbols(); + + private static DirectoryInfo NewOptionDirectory( DirectoryInfo baseDir, string optionName ) + => new( Path.Combine( baseDir.FullName, ReplaceBadXivSymbols( optionName ) ) ); + public void ImportModPack( FileInfo modPackFile ) { CurrentModPack = modPackFile.Name; @@ -94,7 +102,7 @@ namespace Penumbra.Importer WriteZipEntryToTempFile( s ); var fs = new FileStream( _resolvedTempFilePath, FileMode.Open ); - return new MagicTempFileStreamManagerAndDeleterFuckery( fs ); + return new MagicTempFileStreamManagerAndDeleter( fs ); } private void VerifyVersionAndImport( FileInfo modPackFile ) @@ -187,13 +195,11 @@ namespace Penumbra.Importer public static DirectoryInfo CreateModFolder( DirectoryInfo outDirectory, string modListName ) { - var correctedPath = Path.Combine( outDirectory.FullName, - Path.GetFileName( modListName ).RemoveInvalidPathSymbols().RemoveNonAsciiSymbols() ); - var newModFolder = new DirectoryInfo( correctedPath ); + var newModFolder = NewOptionDirectory( outDirectory, Path.GetFileName( modListName ) ); var i = 2; while( newModFolder.Exists && i < 12 ) { - newModFolder = new DirectoryInfo( correctedPath + $" ({i++})" ); + newModFolder = new DirectoryInfo( newModFolder.FullName + $" ({i++})" ); } if( newModFolder.Exists ) @@ -272,7 +278,7 @@ namespace Penumbra.Importer foreach( var group in page.ModGroups.Where( group => group.GroupName != null && group.OptionList != null ) ) { - var groupFolder = new DirectoryInfo( Path.Combine( newModFolder.FullName, group.GroupName!.ReplaceInvalidPathSymbols().RemoveNonAsciiSymbols( ) ) ); + var groupFolder = NewOptionDirectory( newModFolder, group.GroupName! ); if( groupFolder.Exists ) { groupFolder = new DirectoryInfo( groupFolder.FullName + $" ({page.PageIndex})" ); @@ -281,7 +287,7 @@ namespace Penumbra.Importer foreach( var option in group.OptionList!.Where( option => option.Name != null && option.ModsJsons != null ) ) { - var optionFolder = new DirectoryInfo( Path.Combine( groupFolder.FullName, option.Name!.ReplaceInvalidPathSymbols().RemoveNonAsciiSymbols() ) ); + var optionFolder = NewOptionDirectory( groupFolder, option.Name! ); ExtractSimpleModList( optionFolder, option.ModsJsons!, modData ); } @@ -311,7 +317,7 @@ namespace Penumbra.Importer OptionDesc = string.IsNullOrEmpty( opt.Description ) ? "" : opt.Description!, OptionFiles = new Dictionary< RelPath, HashSet< GamePath > >(), }; - var optDir = new DirectoryInfo( Path.Combine( groupFolder.FullName, opt.Name!.ReplaceInvalidPathSymbols().RemoveNonAsciiSymbols() ) ); + var optDir = NewOptionDirectory( groupFolder, opt.Name! ); if( optDir.Exists ) { foreach( var file in optDir.EnumerateFiles( "*.*", SearchOption.AllDirectories ) ) diff --git a/Penumbra/Importer/TexToolsMeta.cs b/Penumbra/Importer/TexToolsMeta.cs index 72a68b6b..8508e90a 100644 --- a/Penumbra/Importer/TexToolsMeta.cs +++ b/Penumbra/Importer/TexToolsMeta.cs @@ -5,9 +5,11 @@ using System.Text.RegularExpressions; using Dalamud.Plugin; using Lumina.Data.Files; using Penumbra.Game; -using Penumbra.MetaData; -using Penumbra.Mods; +using Penumbra.Game.Enums; +using Penumbra.Meta; +using Penumbra.Meta.Files; using Penumbra.Util; +using GameData = Penumbra.Game.Enums.GameData; namespace Penumbra.Importer { diff --git a/Penumbra/MetaData/EqdpFile.cs b/Penumbra/Meta/Files/EqdpFile.cs similarity index 94% rename from Penumbra/MetaData/EqdpFile.cs rename to Penumbra/Meta/Files/EqdpFile.cs index 2cb37f1a..4618011b 100644 --- a/Penumbra/MetaData/EqdpFile.cs +++ b/Penumbra/Meta/Files/EqdpFile.cs @@ -4,7 +4,7 @@ using System.Linq; using Lumina.Data; using Penumbra.Game; -namespace Penumbra.MetaData +namespace Penumbra.Meta.Files { // EQDP file structure: // [Identifier][BlockSize:ushort][BlockCount:ushort] @@ -37,7 +37,8 @@ namespace Penumbra.MetaData } } - public ref EqdpEntry this[ ushort setId ] => ref GetTrueEntry( setId ); + public ref EqdpEntry this[ ushort setId ] + => ref GetTrueEntry( setId ); public EqdpFile Clone() @@ -49,8 +50,11 @@ namespace Penumbra.MetaData private ushort ExpandedBlockCount { get; set; } private EqdpEntry[]?[] Blocks { get; } - private int BlockIdx( ushort id ) => ( ushort )( id / BlockSize ); - private int SubIdx( ushort id ) => ( ushort )( id % BlockSize ); + private int BlockIdx( ushort id ) + => ( ushort )( id / BlockSize ); + + private int SubIdx( ushort id ) + => ( ushort )( id % BlockSize ); private bool ExpandBlock( int idx ) { @@ -156,7 +160,7 @@ namespace Penumbra.MetaData private void WriteBlocks( BinaryWriter bw ) { foreach( var entry in Blocks.Where( block => block != null ) - .SelectMany( block => block ) ) + .SelectMany( block => block ) ) { bw.Write( ( ushort )entry ); } diff --git a/Penumbra/MetaData/EqpFile.cs b/Penumbra/Meta/Files/EqpFile.cs similarity index 97% rename from Penumbra/MetaData/EqpFile.cs rename to Penumbra/Meta/Files/EqpFile.cs index fa006982..2be4275d 100644 --- a/Penumbra/MetaData/EqpFile.cs +++ b/Penumbra/Meta/Files/EqpFile.cs @@ -4,7 +4,7 @@ using System.Linq; using Lumina.Data; using Penumbra.Game; -namespace Penumbra.MetaData +namespace Penumbra.Meta.Files { // EQP Structure: // 64 x [Block collapsed or not bit] @@ -28,7 +28,7 @@ namespace Penumbra.MetaData } public byte[] WriteBytes() - => WriteBytes( _entries, E => ( ulong )E ); + => WriteBytes( _entries, e => ( ulong )e ); public EqpFile Clone() => new( this ); @@ -40,7 +40,7 @@ namespace Penumbra.MetaData => GetEntry( _entries, setId, ( EqpEntry )0 ); public bool SetEntry( ushort setId, EqpEntry entry ) - => SetEntry( _entries, setId, entry, E => E == 0, ( E1, E2 ) => E1 == E2 ); + => SetEntry( _entries, setId, entry, e => e == 0, ( e1, e2 ) => e1 == e2 ); public ref EqpEntry this[ ushort setId ] => ref GetTrueEntry( _entries, setId ); diff --git a/Penumbra/MetaData/EstFile.cs b/Penumbra/Meta/Files/EstFile.cs similarity index 98% rename from Penumbra/MetaData/EstFile.cs rename to Penumbra/Meta/Files/EstFile.cs index 10cabba6..8a421a5e 100644 --- a/Penumbra/MetaData/EstFile.cs +++ b/Penumbra/Meta/Files/EstFile.cs @@ -2,9 +2,9 @@ using System.Collections.Generic; using System.IO; using System.Linq; using Lumina.Data; -using Penumbra.Game; +using Penumbra.Game.Enums; -namespace Penumbra.MetaData +namespace Penumbra.Meta.Files { // EST Structure: // 1x [NumEntries : UInt32] @@ -101,7 +101,7 @@ namespace Penumbra.MetaData return 0; } - return !setDict.TryGetValue( setId, out var entry ) ? (ushort) 0 : entry; + return !setDict.TryGetValue( setId, out var entry ) ? ( ushort )0 : entry; } public byte[] WriteBytes() diff --git a/Penumbra/MetaData/GmpFile.cs b/Penumbra/Meta/Files/GmpFile.cs similarity index 81% rename from Penumbra/MetaData/GmpFile.cs rename to Penumbra/Meta/Files/GmpFile.cs index 67112940..02322d20 100644 --- a/Penumbra/MetaData/GmpFile.cs +++ b/Penumbra/Meta/Files/GmpFile.cs @@ -1,7 +1,7 @@ using Lumina.Data; using Penumbra.Game; -namespace Penumbra.MetaData +namespace Penumbra.Meta.Files { // GmpFiles use the same structure as Eqp Files. // Entries are also one ulong. @@ -22,19 +22,19 @@ namespace Penumbra.MetaData } public byte[] WriteBytes() - => WriteBytes( _entries, E => ( ulong )E ); + => WriteBytes( _entries, e => ( ulong )e ); public GmpFile Clone() => new( this ); public GmpFile( FileResource file ) - => ReadFile( _entries, file, I => ( GmpEntry )I ); + => ReadFile( _entries, file, i => ( GmpEntry )i ); public GmpEntry GetEntry( ushort setId ) => GetEntry( _entries, setId, ( GmpEntry )0 ); public bool SetEntry( ushort setId, GmpEntry entry ) - => SetEntry( _entries, setId, entry, E => E == 0, ( E1, E2 ) => E1 == E2 ); + => SetEntry( _entries, setId, entry, e => e == 0, ( e1, e2 ) => e1 == e2 ); public ref GmpEntry this[ ushort setId ] => ref GetTrueEntry( _entries, setId ); diff --git a/Penumbra/MetaData/ImcExtensions.cs b/Penumbra/Meta/Files/ImcExtensions.cs similarity index 80% rename from Penumbra/MetaData/ImcExtensions.cs rename to Penumbra/Meta/Files/ImcExtensions.cs index e73406e6..f9d64090 100644 --- a/Penumbra/MetaData/ImcExtensions.cs +++ b/Penumbra/Meta/Files/ImcExtensions.cs @@ -2,22 +2,33 @@ using System; using System.ComponentModel; using System.IO; using System.Linq; -using Dalamud.Plugin; using Lumina.Data.Files; -using Penumbra.Game; -using Penumbra.Mods; +using Penumbra.Game.Enums; -namespace Penumbra.MetaData +namespace Penumbra.Meta.Files { public class InvalidImcVariantException : ArgumentOutOfRangeException { public InvalidImcVariantException() - : base("Trying to manipulate invalid variant.") + : base( "Trying to manipulate invalid variant." ) { } } public static class ImcExtensions { + public static ulong ToInteger( this ImcFile.ImageChangeData imc ) + { + ulong ret = imc.MaterialId; + ret |= ( ulong )imc.DecalId << 8; + ret |= ( ulong )imc.AttributeMask << 16; + ret |= ( ulong )imc.SoundId << 16; + ret |= ( ulong )imc.VfxId << 32; + var tmp = imc.GetType().GetField( "_MaterialAnimationIdMask", + System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance ); + ret |= ( ulong )( byte )tmp!.GetValue( imc ) << 40; + return ret; + } + public static bool Equal( this ImcFile.ImageChangeData lhs, ImcFile.ImageChangeData rhs ) => lhs.MaterialId == rhs.MaterialId && lhs.DecalId == rhs.DecalId @@ -35,7 +46,6 @@ namespace Penumbra.MetaData bw.Write( variant.MaterialAnimationId ); } - public static byte[] WriteBytes( this ImcFile file ) { var parts = file.PartMask == 31 ? 5 : 1; @@ -104,10 +114,10 @@ namespace Penumbra.MetaData Count = file.Count, PartMask = file.PartMask, }; - var parts = file.GetParts().Select( P => new ImcFile.ImageChangeParts() + var parts = file.GetParts().Select( p => new ImcFile.ImageChangeParts() { - DefaultVariant = P.DefaultVariant, - Variants = ( ImcFile.ImageChangeData[] )P.Variants.Clone(), + DefaultVariant = p.DefaultVariant, + Variants = ( ImcFile.ImageChangeData[] )p.Variants.Clone(), } ).ToArray(); var prop = ret.GetType().GetField( "Parts", System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance ); prop!.SetValue( ret, parts ); diff --git a/Penumbra/MetaData/MetaDefaults.cs b/Penumbra/Meta/Files/MetaDefaults.cs similarity index 89% rename from Penumbra/MetaData/MetaDefaults.cs rename to Penumbra/Meta/Files/MetaDefaults.cs index 53c8a024..4d484de2 100644 --- a/Penumbra/MetaData/MetaDefaults.cs +++ b/Penumbra/Meta/Files/MetaDefaults.cs @@ -4,10 +4,10 @@ using Dalamud.Plugin; using Lumina.Data; using Lumina.Data.Files; using Penumbra.Game; -using Penumbra.Mods; +using Penumbra.Game.Enums; using Penumbra.Util; -namespace Penumbra.MetaData +namespace Penumbra.Meta.Files { public class MetaDefaults { @@ -112,14 +112,20 @@ namespace Penumbra.MetaData return m.Type switch { MetaType.Imc => GetDefaultImcFile( m.ImcIdentifier.ObjectType, m.ImcIdentifier.PrimaryId, m.ImcIdentifier.SecondaryId ) - ?.GetValue( m ).Equal( m.ImcValue ) ?? false, - MetaType.Gmp => GetDefaultGmpFile()?.GetEntry( m.GmpIdentifier.SetId ) == m.GmpValue, - MetaType.Eqp => GetDefaultEqpFile()?.GetEntry( m.EqpIdentifier.SetId ).Reduce( m.EqpIdentifier.Slot ) == m.EqpValue, + ?.GetValue( m ).Equal( m.ImcValue ) + ?? true, + MetaType.Gmp => GetDefaultGmpFile()?.GetEntry( m.GmpIdentifier.SetId ) + == m.GmpValue, + MetaType.Eqp => GetDefaultEqpFile()?.GetEntry( m.EqpIdentifier.SetId ) + .Reduce( m.EqpIdentifier.Slot ) + == m.EqpValue, MetaType.Eqdp => GetDefaultEqdpFile( m.EqdpIdentifier.Slot, m.EqdpIdentifier.GenderRace )?.GetEntry( m.EqdpIdentifier.SetId ) - .Reduce( m.EqdpIdentifier.Slot ) == m.EqdpValue, + .Reduce( m.EqdpIdentifier.Slot ) + == m.EqdpValue, MetaType.Est => GetDefaultEstFile( m.EstIdentifier.ObjectType, m.EstIdentifier.EquipSlot, m.EstIdentifier.BodySlot ) - ?.GetEntry( m.EstIdentifier.GenderRace, m.EstIdentifier.PrimaryId ) == m.EstValue, - _ => throw new NotImplementedException() + ?.GetEntry( m.EstIdentifier.GenderRace, m.EstIdentifier.PrimaryId ) + == m.EstValue, + _ => throw new NotImplementedException(), }; } @@ -132,7 +138,7 @@ namespace Penumbra.MetaData MetaType.Eqp => GetNewEqpFile(), MetaType.Eqdp => GetNewEqdpFile( m.EqdpIdentifier.Slot, m.EqdpIdentifier.GenderRace ), MetaType.Est => GetNewEstFile( m.EstIdentifier.ObjectType, m.EstIdentifier.EquipSlot, m.EstIdentifier.BodySlot ), - _ => throw new NotImplementedException() + _ => throw new NotImplementedException(), }; } } diff --git a/Penumbra/MetaData/MetaFilenames.cs b/Penumbra/Meta/Files/MetaFilenames.cs similarity index 98% rename from Penumbra/MetaData/MetaFilenames.cs rename to Penumbra/Meta/Files/MetaFilenames.cs index 008d8ac8..1b6272dc 100644 --- a/Penumbra/MetaData/MetaFilenames.cs +++ b/Penumbra/Meta/Files/MetaFilenames.cs @@ -1,9 +1,8 @@ using System; -using Penumbra.Game; -using Penumbra.Mods; +using Penumbra.Game.Enums; using Penumbra.Util; -namespace Penumbra.MetaData +namespace Penumbra.Meta.Files { public static class MetaFileNames { diff --git a/Penumbra/Meta/Identifier.cs b/Penumbra/Meta/Identifier.cs new file mode 100644 index 00000000..041b5d03 --- /dev/null +++ b/Penumbra/Meta/Identifier.cs @@ -0,0 +1,149 @@ +using System.Runtime.InteropServices; +using Penumbra.Game.Enums; + +namespace Penumbra.Meta +{ + public enum MetaType : byte + { + Unknown = 0, + Imc = 1, + Eqdp = 2, + Eqp = 3, + Est = 4, + Gmp = 5, + }; + + [StructLayout( LayoutKind.Explicit )] + public struct EqpIdentifier + { + [FieldOffset( 0 )] + public ulong Value; + + [FieldOffset( 0 )] + public MetaType Type; + + [FieldOffset( 1 )] + public EquipSlot Slot; + + [FieldOffset( 2 )] + public ushort SetId; + + public override string ToString() + => $"Eqp - {SetId} - {Slot}"; + } + + [StructLayout( LayoutKind.Explicit )] + public struct EqdpIdentifier + { + [FieldOffset( 0 )] + public ulong Value; + + [FieldOffset( 0 )] + public MetaType Type; + + [FieldOffset( 1 )] + public EquipSlot Slot; + + [FieldOffset( 2 )] + public GenderRace GenderRace; + + [FieldOffset( 4 )] + public ushort SetId; + + public override string ToString() + => $"Eqdp - {SetId} - {Slot} - {GenderRace.Split().Item2} {GenderRace.Split().Item1}"; + } + + [StructLayout( LayoutKind.Explicit )] + public struct GmpIdentifier + { + [FieldOffset( 0 )] + public ulong Value; + + [FieldOffset( 0 )] + public MetaType Type; + + [FieldOffset( 1 )] + public ushort SetId; + + public override string ToString() + => $"Gmp - {SetId}"; + } + + [StructLayout( LayoutKind.Explicit )] + public struct EstIdentifier + { + [FieldOffset( 0 )] + public ulong Value; + + [FieldOffset( 0 )] + public MetaType Type; + + [FieldOffset( 1 )] + public ObjectType ObjectType; + + [FieldOffset( 2 )] + public EquipSlot EquipSlot; + + [FieldOffset( 3 )] + public BodySlot BodySlot; + + [FieldOffset( 4 )] + public GenderRace GenderRace; + + [FieldOffset( 6 )] + public ushort PrimaryId; + + public override string ToString() + => ObjectType == ObjectType.Equipment + ? $"Est - {PrimaryId} - {EquipSlot} - {GenderRace.Split().Item2} {GenderRace.Split().Item1}" + : $"Est - {PrimaryId} - {BodySlot} - {GenderRace.Split().Item2} {GenderRace.Split().Item1}"; + } + + [StructLayout( LayoutKind.Explicit )] + public struct ImcIdentifier + { + [FieldOffset( 0 )] + public ulong Value; + + [FieldOffset( 0 )] + public MetaType Type; + + [FieldOffset( 1 )] + public byte _objectAndBody; + + public ObjectType ObjectType + { + get => ( ObjectType )( _objectAndBody & 0b00011111 ); + set => _objectAndBody = ( byte )( ( _objectAndBody & 0b11100000 ) | ( byte )value ); + } + + public BodySlot BodySlot + { + get => ( BodySlot )( _objectAndBody >> 5 ); + set => _objectAndBody = ( byte )( ( _objectAndBody & 0b00011111 ) | ( ( byte )value << 5 ) ); + } + + [FieldOffset( 2 )] + public ushort PrimaryId; + + [FieldOffset( 4 )] + public ushort Variant; + + [FieldOffset( 6 )] + public ushort SecondaryId; + + [FieldOffset( 6 )] + public EquipSlot EquipSlot; + + public override string ToString() + { + return ObjectType switch + { + ObjectType.Accessory => $"Imc - {PrimaryId} - {EquipSlot} - {Variant}", + ObjectType.Equipment => $"Imc - {PrimaryId} - {EquipSlot} - {Variant}", + _ => $"Imc - {PrimaryId} - {ObjectType} - {SecondaryId} - {BodySlot} - {Variant}", + }; + } + } +} \ No newline at end of file diff --git a/Penumbra/Meta/MetaCollection.cs b/Penumbra/Meta/MetaCollection.cs new file mode 100644 index 00000000..012f85e9 --- /dev/null +++ b/Penumbra/Meta/MetaCollection.cs @@ -0,0 +1,221 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using Dalamud.Plugin; +using Newtonsoft.Json; +using Penumbra.Importer; +using Penumbra.Meta.Files; +using Penumbra.Mod; +using Penumbra.Structs; +using Penumbra.Util; + +namespace Penumbra.Meta +{ + // Corresponds meta manipulations of any kind with the settings for a mod. + // DefaultData contains all manipulations that are active regardless of option groups. + // GroupData contains a mapping of Group -> { Options -> {Manipulations} }. + public class MetaCollection + { + public List< MetaManipulation > DefaultData = new(); + public Dictionary< string, Dictionary< string, List< MetaManipulation > > > GroupData = new(); + + + // Store total number of manipulations for some ease of access. + [JsonProperty] + public int Count { get; private set; } = 0; + + + // Return an enumeration of all active meta manipulations for a given mod with given settings. + public IEnumerable< MetaManipulation > GetManipulationsForConfig( ModSettings settings, ModMeta modMeta ) + { + if( Count == DefaultData.Count ) + { + return DefaultData; + } + + IEnumerable< MetaManipulation > ret = DefaultData; + + foreach( var group in modMeta.Groups ) + { + if( !GroupData.TryGetValue( group.Key, out var metas ) || !settings.Settings.TryGetValue( group.Key, out var setting ) ) + { + continue; + } + + if( group.Value.SelectionType == SelectType.Single ) + { + var settingName = group.Value.Options[ setting ].OptionName; + if( metas.TryGetValue( settingName, out var meta ) ) + { + ret = ret.Concat( meta ); + } + } + else + { + for( var i = 0; i < group.Value.Options.Count; ++i ) + { + var flag = 1 << i; + if( ( setting & flag ) == 0 ) + { + continue; + } + + var settingName = group.Value.Options[ i ].OptionName; + if( metas.TryGetValue( settingName, out var meta ) ) + { + ret = ret.Concat( meta ); + } + } + } + } + + return ret; + } + + // Check that the collection is still basically valid, + // i.e. keep it sorted, and verify that the options stored by name are all still part of the mod, + // and that the contained manipulations are still valid and non-default manipulations. + public bool Validate( ModMeta modMeta ) + { + var defaultFiles = Service< MetaDefaults >.Get(); + SortLists(); + foreach( var group in GroupData ) + { + if( !modMeta.Groups.TryGetValue( group.Key, out var options ) ) + { + return false; + } + + foreach( var option in group.Value ) + { + if( options.Options.All( o => o.OptionName != option.Key ) ) + { + return false; + } + + if( option.Value.Any( manip => defaultFiles.CheckAgainstDefault( manip ) ) ) + { + return false; + } + } + } + + return DefaultData.All( manip => !defaultFiles.CheckAgainstDefault( manip ) ); + } + + // Re-sort all manipulations. + private void SortLists() + { + DefaultData.Sort(); + foreach( var list in GroupData.Values.SelectMany( g => g.Values ) ) + { + list.Sort(); + } + } + + // Add a parsed TexTools .meta file to a given option group and option. If group is the empty string, add it to default. + // Creates the option group and the option if necessary. + private void AddMeta( string group, string option, TexToolsMeta meta ) + { + if( meta.Manipulations.Count == 0 ) + { + return; + } + + if( group.Length == 0 ) + { + DefaultData.AddRange( meta.Manipulations ); + } + else if( option.Length == 0 ) + { } + else if( !GroupData.TryGetValue( group, out var options ) ) + { + GroupData.Add( group, new Dictionary< string, List< MetaManipulation > >() { { option, meta.Manipulations.ToList() } } ); + } + else if( !options.TryGetValue( option, out var list ) ) + { + options.Add( option, meta.Manipulations.ToList() ); + } + else + { + list.AddRange( meta.Manipulations ); + } + + Count += meta.Manipulations.Count; + } + + // Update the whole meta collection by reading all TexTools .meta files in a mod directory anew, + // combining them with the given ModMeta. + public void Update( IEnumerable< FileInfo > files, DirectoryInfo basePath, ModMeta modMeta ) + { + DefaultData.Clear(); + GroupData.Clear(); + foreach( var file in files.Where( f => f.Extension == ".meta" ) ) + { + var metaData = new TexToolsMeta( File.ReadAllBytes( file.FullName ) ); + if( metaData.FilePath == string.Empty || metaData.Manipulations.Count == 0 ) + { + continue; + } + + var path = new RelPath( file, basePath ); + var foundAny = false; + foreach( var group in modMeta.Groups ) + { + foreach( var option in group.Value.Options.Where( o => o.OptionFiles.ContainsKey( path ) ) ) + { + foundAny = true; + AddMeta( group.Key, option.OptionName, metaData ); + } + } + + if( !foundAny ) + { + AddMeta( string.Empty, string.Empty, metaData ); + } + } + + SortLists(); + } + + public static FileInfo FileName( DirectoryInfo basePath ) + => new( Path.Combine( basePath.FullName, "metadata_manipulations.json" ) ); + + public void SaveToFile( FileInfo file ) + { + try + { + var text = JsonConvert.SerializeObject( this, Formatting.Indented ); + File.WriteAllText( file.FullName, text ); + } + catch( Exception e ) + { + PluginLog.Error( $"Could not write metadata manipulations file to {file.FullName}:\n{e}" ); + } + } + + public static MetaCollection? LoadFromFile( FileInfo file ) + { + if( !file.Exists ) + { + return null; + } + + try + { + var text = File.ReadAllText( file.FullName ); + + var collection = JsonConvert.DeserializeObject< MetaCollection >( text, + new JsonSerializerSettings { NullValueHandling = NullValueHandling.Ignore } ); + + return collection; + } + catch( Exception e ) + { + PluginLog.Error( $"Could not load mod metadata manipulations from {file.FullName}:\n{e}" ); + return null; + } + } + } +} \ No newline at end of file diff --git a/Penumbra/Mods/MetaManager.cs b/Penumbra/Meta/MetaManager.cs similarity index 78% rename from Penumbra/Mods/MetaManager.cs rename to Penumbra/Meta/MetaManager.cs index 9bbff0f4..3fc80de3 100644 --- a/Penumbra/Mods/MetaManager.cs +++ b/Penumbra/Meta/MetaManager.cs @@ -5,10 +5,10 @@ using System.Linq; using Dalamud.Plugin; using Lumina.Data.Files; using Penumbra.Hooks; +using Penumbra.Meta.Files; using Penumbra.Util; -using Penumbra.MetaData; -namespace Penumbra.Mods +namespace Penumbra.Meta { public class MetaManager : IDisposable { @@ -45,9 +45,15 @@ namespace Penumbra.Mods private readonly GameResourceManagement _resourceManagement; private readonly Dictionary< GamePath, FileInfo > _resolvedFiles; - private readonly HashSet< MetaManipulation > _currentManipulations = new(); + private readonly Dictionary< MetaManipulation, Mod.Mod > _currentManipulations = new(); private readonly Dictionary< GamePath, FileInformation > _currentFiles = new(); + public IEnumerable< (MetaManipulation, Mod.Mod) > Manipulations + => _currentManipulations.Select( kvp => ( kvp.Key, kvp.Value ) ); + + public bool TryGetValue( MetaManipulation manip, out Mod.Mod mod ) + => _currentManipulations.TryGetValue( manip, out mod ); + private static void DisposeFile( FileInfo? file ) { if( !( file?.Exists ?? false ) ) @@ -65,7 +71,7 @@ namespace Penumbra.Mods } } - public void Dispose() + private void Reset( bool reload ) { foreach( var file in _currentFiles ) { @@ -76,11 +82,26 @@ namespace Penumbra.Mods _currentManipulations.Clear(); _currentFiles.Clear(); ClearDirectory(); - _resourceManagement.ReloadPlayerResources(); + if( reload ) + { + _resourceManagement.ReloadPlayerResources(); + } + } + + public void Reset() + => Reset( true ); + + public void Dispose() + => Reset(); + + ~MetaManager() + { + Reset( false ); } private void ClearDirectory() { + _dir.Refresh(); if( _dir.Exists ) { try @@ -94,18 +115,18 @@ namespace Penumbra.Mods } } - public MetaManager( Dictionary< GamePath, FileInfo > resolvedFiles, DirectoryInfo modDir ) + public MetaManager( string name, Dictionary< GamePath, FileInfo > resolvedFiles, DirectoryInfo modDir ) { - _resolvedFiles = resolvedFiles; - _default = Service< MetaDefaults >.Get(); + _resolvedFiles = resolvedFiles; + _default = Service< MetaDefaults >.Get(); _resourceManagement = Service< GameResourceManagement >.Get(); - _dir = new DirectoryInfo( Path.Combine( modDir.FullName, TmpDirectory ) ); + _dir = new DirectoryInfo( Path.Combine( modDir.FullName, TmpDirectory, name.ReplaceInvalidPathSymbols().RemoveNonAsciiSymbols() ) ); ClearDirectory(); - Directory.CreateDirectory( _dir.FullName ); } public void WriteNewFiles() { + Directory.CreateDirectory( _dir.FullName ); foreach( var kvp in _currentFiles.Where( kvp => kvp.Value.Changed ) ) { kvp.Value.Write( _dir ); @@ -115,13 +136,14 @@ namespace Penumbra.Mods _resourceManagement.ReloadPlayerResources(); } - public bool ApplyMod( MetaManipulation m ) + public bool ApplyMod( MetaManipulation m, Mod.Mod mod ) { - if( !_currentManipulations.Add( m ) ) + if( _currentManipulations.ContainsKey( m ) ) { return false; } + _currentManipulations.Add( m, mod ); var gamePath = m.CorrespondingFilename(); try { diff --git a/Penumbra/Mods/MetaManipulation.cs b/Penumbra/Meta/MetaManipulation.cs similarity index 63% rename from Penumbra/Mods/MetaManipulation.cs rename to Penumbra/Meta/MetaManipulation.cs index 681d85ff..01b25b66 100644 --- a/Penumbra/Mods/MetaManipulation.cs +++ b/Penumbra/Meta/MetaManipulation.cs @@ -1,134 +1,45 @@ using System; using System.ComponentModel; +using System.IO; using System.Runtime.InteropServices; +using Newtonsoft.Json; using Penumbra.Game; -using Penumbra.MetaData; +using Penumbra.Game.Enums; +using Penumbra.Meta.Files; using Penumbra.Util; +using Swan; using ImcFile = Lumina.Data.Files.ImcFile; -namespace Penumbra.Mods +namespace Penumbra.Meta { - public enum MetaType : byte + public class MetaManipulationConverter : JsonConverter< MetaManipulation > { - Unknown = 0, - Imc = 1, - Eqdp = 2, - Eqp = 3, - Est = 4, - Gmp = 5 - }; - - [StructLayout( LayoutKind.Explicit )] - public struct EqpIdentifier - { - [FieldOffset( 0 )] - public ulong Value; - - [FieldOffset( 0 )] - public MetaType Type; - - [FieldOffset( 1 )] - public EquipSlot Slot; - - [FieldOffset( 2 )] - public ushort SetId; - } - - [StructLayout( LayoutKind.Explicit )] - public struct EqdpIdentifier - { - [FieldOffset( 0 )] - public ulong Value; - - [FieldOffset( 0 )] - public MetaType Type; - - [FieldOffset( 1 )] - public EquipSlot Slot; - - [FieldOffset( 2 )] - public GenderRace GenderRace; - - [FieldOffset( 4 )] - public ushort SetId; - } - - [StructLayout( LayoutKind.Explicit )] - public struct GmpIdentifier - { - [FieldOffset( 0 )] - public ulong Value; - - [FieldOffset( 0 )] - public MetaType Type; - - [FieldOffset( 1 )] - public ushort SetId; - } - - [StructLayout( LayoutKind.Explicit )] - public struct EstIdentifier - { - [FieldOffset( 0 )] - public ulong Value; - - [FieldOffset( 0 )] - public MetaType Type; - - [FieldOffset( 1 )] - public ObjectType ObjectType; - - [FieldOffset( 2 )] - public EquipSlot EquipSlot; - - [FieldOffset( 3 )] - public BodySlot BodySlot; - - [FieldOffset( 4 )] - public GenderRace GenderRace; - - [FieldOffset( 6 )] - public ushort PrimaryId; - } - - [StructLayout( LayoutKind.Explicit )] - public struct ImcIdentifier - { - [FieldOffset( 0 )] - public ulong Value; - - [FieldOffset( 0 )] - public MetaType Type; - - [FieldOffset( 1 )] - public byte _objectAndBody; - - public ObjectType ObjectType + public override void WriteJson( JsonWriter writer, MetaManipulation manip, JsonSerializer serializer ) { - get => ( ObjectType )( _objectAndBody & 0b00011111 ); - set => _objectAndBody = ( byte )( ( _objectAndBody & 0b11100000 ) | ( byte )value ); + var s = Convert.ToBase64String( manip.ToBytes() ); + writer.WriteValue( s ); } - public BodySlot BodySlot + public override MetaManipulation ReadJson( JsonReader reader, Type objectType, MetaManipulation existingValue, bool hasExistingValue, + JsonSerializer serializer ) + { - get => ( BodySlot )( _objectAndBody & 0b11100000 ); - set => _objectAndBody = ( byte )( ( _objectAndBody & 0b00011111 ) | ( byte )value ); + if( reader.TokenType != JsonToken.String ) + { + throw new JsonReaderException(); + } + + var bytes = Convert.FromBase64String( ( string )reader.Value! ); + using MemoryStream m = new( bytes ); + using BinaryReader br = new( m ); + var i = br.ReadUInt64(); + var v = br.ReadUInt64(); + return new MetaManipulation( i, v ); } - - [FieldOffset( 2 )] - public ushort PrimaryId; - - [FieldOffset( 4 )] - public ushort Variant; - - [FieldOffset( 6 )] - public ushort SecondaryId; - - [FieldOffset( 6 )] - public EquipSlot EquipSlot; } [StructLayout( LayoutKind.Explicit )] + [JsonConverter( typeof( MetaManipulationConverter ) )] public struct MetaManipulation : IComparable { public static MetaManipulation Eqp( EquipSlot equipSlot, ushort setId, EqpEntry value ) @@ -138,9 +49,9 @@ namespace Penumbra.Mods { Type = MetaType.Eqp, Slot = equipSlot, - SetId = setId + SetId = setId, }, - EqpValue = value + EqpValue = value, }; public static MetaManipulation Eqdp( EquipSlot equipSlot, GenderRace gr, ushort setId, EqdpEntry value ) @@ -151,9 +62,9 @@ namespace Penumbra.Mods Type = MetaType.Eqdp, Slot = equipSlot, GenderRace = gr, - SetId = setId + SetId = setId, }, - EqdpValue = value + EqdpValue = value, }; public static MetaManipulation Gmp( ushort setId, GmpEntry value ) @@ -162,9 +73,9 @@ namespace Penumbra.Mods GmpIdentifier = new GmpIdentifier() { Type = MetaType.Gmp, - SetId = setId + SetId = setId, }, - GmpValue = value + GmpValue = value, }; public static MetaManipulation Est( ObjectType type, EquipSlot equipSlot, GenderRace gr, BodySlot bodySlot, ushort setId, @@ -178,9 +89,9 @@ namespace Penumbra.Mods GenderRace = gr, EquipSlot = equipSlot, BodySlot = bodySlot, - PrimaryId = setId + PrimaryId = setId, }, - EstValue = value + EstValue = value, }; public static MetaManipulation Imc( ObjectType type, BodySlot secondaryType, ushort primaryId, ushort secondaryId @@ -194,9 +105,9 @@ namespace Penumbra.Mods BodySlot = secondaryType, PrimaryId = primaryId, SecondaryId = secondaryId, - Variant = idx + Variant = idx, }, - ImcValue = value + ImcValue = value, }; public static MetaManipulation Imc( EquipSlot slot, ushort primaryId, ushort idx, ImcFile.ImageChangeData value ) @@ -208,11 +119,18 @@ namespace Penumbra.Mods ObjectType = slot.IsAccessory() ? ObjectType.Accessory : ObjectType.Equipment, EquipSlot = slot, PrimaryId = primaryId, - Variant = idx + Variant = idx, }, - ImcValue = value + ImcValue = value, }; + internal MetaManipulation( ulong identifier, ulong value ) + : this() + { + Identifier = identifier; + Value = value; + } + [FieldOffset( 0 )] public readonly ulong Identifier; @@ -257,7 +175,7 @@ namespace Penumbra.Mods => Identifier.GetHashCode(); public int CompareTo( object? rhs ) - => Identifier.CompareTo( rhs ); + => Identifier.CompareTo( rhs is MetaManipulation m ? m.Identifier : null ); public GamePath CorrespondingFilename() { @@ -268,7 +186,7 @@ namespace Penumbra.Mods MetaType.Est => MetaFileNames.Est( EstIdentifier.ObjectType, EstIdentifier.EquipSlot, EstIdentifier.BodySlot ), MetaType.Gmp => MetaFileNames.Gmp(), MetaType.Imc => MetaFileNames.Imc( ImcIdentifier.ObjectType, ImcIdentifier.PrimaryId, ImcIdentifier.SecondaryId ), - _ => throw new InvalidEnumArgumentException() + _ => throw new InvalidEnumArgumentException(), }; } @@ -296,5 +214,18 @@ namespace Penumbra.Mods value = ImcValue; return true; } + + public string IdentifierString() + { + return Type switch + { + MetaType.Eqp => $"EQP - {EqpIdentifier}", + MetaType.Eqdp => $"EQDP - {EqdpIdentifier}", + MetaType.Est => $"EST - {EstIdentifier}", + MetaType.Gmp => $"GMP - {GmpIdentifier}", + MetaType.Imc => $"IMC - {ImcIdentifier}", + _ => throw new InvalidEnumArgumentException(), + }; + } } } \ No newline at end of file diff --git a/Penumbra/MigrateConfiguration.cs b/Penumbra/MigrateConfiguration.cs new file mode 100644 index 00000000..1d5b14c4 --- /dev/null +++ b/Penumbra/MigrateConfiguration.cs @@ -0,0 +1,84 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using Dalamud.Plugin; +using Newtonsoft.Json.Linq; +using Penumbra.Mod; +using Penumbra.Mods; +using Penumbra.Util; + +namespace Penumbra +{ + public static class MigrateConfiguration + { + public static void Version0To1( Configuration config ) + { + if( config.Version != 0 ) + { + return; + } + + config.ModDirectory = config.CurrentCollection; + config.CurrentCollection = "Default"; + config.Version = 1; + ResettleCollectionJson( config ); + } + + private static void ResettleCollectionJson( Configuration config ) + { + var collectionJson = new FileInfo( Path.Combine( config.ModDirectory, "collection.json" ) ); + if( !collectionJson.Exists ) + { + return; + } + + var defaultCollection = new ModCollection(); + var defaultCollectionFile = defaultCollection.FileName(); + if( defaultCollectionFile.Exists ) + { + return; + } + + try + { + var text = File.ReadAllText( collectionJson.FullName ); + var data = JArray.Parse( text ); + + var maxPriority = 0; + foreach( var setting in data.Cast< JObject >() ) + { + var modName = ( string )setting[ "FolderName" ]!; + var enabled = ( bool )setting[ "Enabled" ]!; + var priority = ( int )setting[ "Priority" ]!; + var settings = setting[ "Settings" ]!.ToObject< Dictionary< string, int > >() + ?? setting[ "Conf" ]!.ToObject< Dictionary< string, int > >(); + + var save = new ModSettings() + { + Enabled = enabled, + Priority = priority, + Settings = settings!, + }; + defaultCollection.Settings.Add( modName, save ); + maxPriority = Math.Max( maxPriority, priority ); + } + + if( config.InvertModListOrder ) + { + foreach( var setting in defaultCollection.Settings.Values ) + { + setting.Priority = maxPriority - setting.Priority; + } + } + + defaultCollection.Save( Service< DalamudPluginInterface >.Get() ); + } + catch( Exception e ) + { + PluginLog.Error( $"Could not migrate the old collection file to new collection files:\n{e}" ); + throw; + } + } + } +} \ No newline at end of file diff --git a/Penumbra/Mod/Mod.cs b/Penumbra/Mod/Mod.cs new file mode 100644 index 00000000..75db53b7 --- /dev/null +++ b/Penumbra/Mod/Mod.cs @@ -0,0 +1,32 @@ +using System.Collections.Generic; +using System.IO; +using Penumbra.Util; + +namespace Penumbra.Mod +{ + public class Mod + { + public ModSettings Settings { get; } + public ModData Data { get; } + public ModCache Cache { get; } + + public Mod( ModSettings settings, ModData data ) + { + Settings = settings; + Data = data; + Cache = new ModCache(); + } + + public bool FixSettings() + => Settings.FixInvalidSettings( Data.Meta ); + + public HashSet< GamePath > GetFiles( FileInfo file ) + { + var relPath = new RelPath( file, Data.BasePath ); + return ModFunctions.GetFilesForConfig( relPath, Settings, Data.Meta ); + } + + public override string ToString() + => Data.Meta.Name; + } +} \ No newline at end of file diff --git a/Penumbra/Mod/ModCache.cs b/Penumbra/Mod/ModCache.cs new file mode 100644 index 00000000..ca6649e7 --- /dev/null +++ b/Penumbra/Mod/ModCache.cs @@ -0,0 +1,57 @@ +using System.Collections.Generic; +using System.Linq; +using Penumbra.Meta; +using Penumbra.Util; + +namespace Penumbra.Mod +{ + public class ModCache + { + public Dictionary< Mod, (List< GamePath > Files, List< MetaManipulation > Manipulations) > Conflicts { get; private set; } = new(); + + public void AddConflict( Mod precedingMod, GamePath gamePath ) + { + if( Conflicts.TryGetValue( precedingMod, out var conflicts ) && !conflicts.Files.Contains( gamePath ) ) + { + conflicts.Files.Add( gamePath ); + } + else + { + Conflicts[ precedingMod ] = ( new List< GamePath > { gamePath }, new List< MetaManipulation >() ); + } + } + + public void AddConflict( Mod precedingMod, MetaManipulation manipulation ) + { + if( Conflicts.TryGetValue( precedingMod, out var conflicts ) && !conflicts.Manipulations.Contains( manipulation ) ) + { + conflicts.Manipulations.Add( manipulation ); + } + else + { + Conflicts[ precedingMod ] = ( new List< GamePath >(), new List< MetaManipulation > { manipulation } ); + } + } + + public void ClearConflicts() + => Conflicts.Clear(); + + public void ClearFileConflicts() + { + Conflicts = Conflicts.Where( kvp => kvp.Value.Manipulations.Count > 0 ).ToDictionary( kvp => kvp.Key, kvp => + { + kvp.Value.Files.Clear(); + return kvp.Value; + } ); + } + + public void ClearMetaConflicts() + { + Conflicts = Conflicts.Where( kvp => kvp.Value.Files.Count > 0 ).ToDictionary( kvp => kvp.Key, kvp => + { + kvp.Value.Manipulations.Clear(); + return kvp.Value; + } ); + } + } +} \ No newline at end of file diff --git a/Penumbra/Models/ModCleanup.cs b/Penumbra/Mod/ModCleanup.cs similarity index 97% rename from Penumbra/Models/ModCleanup.cs rename to Penumbra/Mod/ModCleanup.cs index 6c08e8ee..3be18f13 100644 --- a/Penumbra/Models/ModCleanup.cs +++ b/Penumbra/Mod/ModCleanup.cs @@ -6,9 +6,10 @@ using System.IO; using System.Linq; using System.Security.Cryptography; using Dalamud.Plugin; +using Penumbra.Structs; using Penumbra.Util; -namespace Penumbra.Models +namespace Penumbra.Mod { public class ModCleanup { @@ -202,7 +203,7 @@ namespace Penumbra.Models private static bool FileIsInAnyGroup( ModMeta meta, RelPath relPath, bool exceptDuplicates = false ) { var groupEnumerator = exceptDuplicates - ? meta.Groups.Values.Where( G => G.GroupName != Duplicates ) + ? meta.Groups.Values.Where( g => g.GroupName != Duplicates ) : meta.Groups.Values; return groupEnumerator.SelectMany( group => group.Options ) .Any( option => option.OptionFiles.ContainsKey( relPath ) ); @@ -252,7 +253,7 @@ namespace Penumbra.Models }; private static void RemoveFromGroups( ModMeta meta, RelPath relPath, GamePath gamePath, GroupType type = GroupType.Both, - bool skipDuplicates = true ) + bool skipDuplicates = true ) { if( meta.Groups.Count == 0 ) { @@ -315,7 +316,8 @@ namespace Penumbra.Models private static void RemoveUselessGroups( ModMeta meta ) { - meta.Groups = meta.Groups.Where( kvp => kvp.Value.Options.Any( o => o.OptionFiles.Count > 0 ) ).ToDictionary( kvp => kvp.Key, kvp => kvp.Value ); + meta.Groups = meta.Groups.Where( kvp => kvp.Value.Options.Any( o => o.OptionFiles.Count > 0 ) ) + .ToDictionary( kvp => kvp.Key, kvp => kvp.Value ); } // Goes through all Single-Select options and checks if file links are in each of them. @@ -356,7 +358,7 @@ namespace Penumbra.Models var usedRelPath = new RelPath( usedGamePath ); required.AddFile( usedRelPath, gamePath ); required.AddFile( usedRelPath, usedGamePath ); - RemoveFromGroups( meta, relPath, gamePath, GroupType.Single, true ); + RemoveFromGroups( meta, relPath, gamePath, GroupType.Single ); } else if( MoveFile( meta, baseDir.FullName, path, relPath ) ) { @@ -366,7 +368,7 @@ namespace Penumbra.Models FindOrCreateDuplicates( meta ).AddFile( relPath, gamePath ); } - RemoveFromGroups( meta, relPath, gamePath, GroupType.Single, true ); + RemoveFromGroups( meta, relPath, gamePath, GroupType.Single ); } } } diff --git a/Penumbra/Mod/ModData.cs b/Penumbra/Mod/ModData.cs new file mode 100644 index 00000000..a5fb3f46 --- /dev/null +++ b/Penumbra/Mod/ModData.cs @@ -0,0 +1,59 @@ +using System.IO; +using Dalamud.Plugin; + +namespace Penumbra.Mod +{ + public class ModData + { + public DirectoryInfo BasePath; + public ModMeta Meta; + public ModResources Resources; + + public FileInfo MetaFile { get; set; } + + private ModData( DirectoryInfo basePath, ModMeta meta, ModResources resources ) + { + BasePath = basePath; + Meta = meta; + Resources = resources; + MetaFile = MetaFileInfo( basePath ); + } + + public static FileInfo MetaFileInfo( DirectoryInfo basePath ) + => new( Path.Combine( basePath.FullName, "meta.json" ) ); + + public static ModData? LoadMod( DirectoryInfo basePath ) + { + basePath.Refresh(); + if( !basePath.Exists ) + { + PluginLog.Error( $"Supplied mod directory {basePath} does not exist." ); + return null; + } + + var metaFile = MetaFileInfo( basePath ); + if( !metaFile.Exists ) + { + PluginLog.Debug( "No mod meta found for {ModLocation}.", basePath.Name ); + return null; + } + + var meta = ModMeta.LoadFromFile( metaFile ); + if( meta == null ) + { + return null; + } + + var data = new ModResources(); + if( data.RefreshModFiles( basePath ).HasFlag( ResourceChange.Meta ) ) + { + data.SetManipulations( meta, basePath ); + } + + return new ModData( basePath, meta, data ); + } + + public void SaveMeta() + => Meta.SaveToFile( MetaFile ); + } +} \ No newline at end of file diff --git a/Penumbra/Mod/ModFunctions.cs b/Penumbra/Mod/ModFunctions.cs new file mode 100644 index 00000000..74a6d4b4 --- /dev/null +++ b/Penumbra/Mod/ModFunctions.cs @@ -0,0 +1,82 @@ +using System.Collections.Generic; +using System.IO; +using System.Linq; +using Penumbra.Structs; +using Penumbra.Util; + +namespace Penumbra.Mod +{ + public static class ModFunctions + { + public static bool CleanUpCollection( Dictionary< string, ModSettings > settings, IEnumerable< DirectoryInfo > modPaths ) + { + var hashes = modPaths.Select( p => p.Name ).ToHashSet(); + var missingMods = settings.Keys.Where( k => !hashes.Contains( k ) ).ToArray(); + var anyChanges = false; + foreach( var toRemove in missingMods ) + { + anyChanges |= settings.Remove( toRemove ); + } + + return anyChanges; + } + + public static HashSet< GamePath > GetFilesForConfig( RelPath relPath, ModSettings settings, ModMeta meta ) + { + var doNotAdd = false; + var files = new HashSet< GamePath >(); + foreach( var group in meta.Groups.Values.Where( g => g.Options.Count > 0 ) ) + { + doNotAdd |= group.ApplyGroupFiles( relPath, settings.Settings[ group.GroupName ], files ); + } + + if( !doNotAdd ) + { + files.Add( new GamePath( relPath ) ); + } + + return files; + } + + public static ModSettings ConvertNamedSettings( NamedModSettings namedSettings, ModMeta meta ) + { + ModSettings ret = new() + { + Priority = namedSettings.Priority, + Settings = namedSettings.Settings.Keys.ToDictionary( k => k, _ => 0 ), + }; + + foreach( var kvp in namedSettings.Settings ) + { + if( !meta.Groups.TryGetValue( kvp.Key, out var info ) ) + { + continue; + } + + if( info.SelectionType == SelectType.Single ) + { + if( namedSettings.Settings[ kvp.Key ].Count == 0 ) + { + ret.Settings[ kvp.Key ] = 0; + } + else + { + var idx = info.Options.FindIndex( o => o.OptionName == namedSettings.Settings[ kvp.Key ].Last() ); + ret.Settings[ kvp.Key ] = idx < 0 ? 0 : idx; + } + } + else + { + foreach( var idx in namedSettings.Settings[ kvp.Key ] + .Select( option => info.Options.FindIndex( o => o.OptionName == option ) ) + .Where( idx => idx >= 0 ) ) + { + ret.Settings[ kvp.Key ] |= 1 << idx; + } + } + } + + return ret; + } + } +} \ No newline at end of file diff --git a/Penumbra/Mod/ModMeta.cs b/Penumbra/Mod/ModMeta.cs new file mode 100644 index 00000000..49ae549c --- /dev/null +++ b/Penumbra/Mod/ModMeta.cs @@ -0,0 +1,103 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using Dalamud.Plugin; +using Newtonsoft.Json; +using Penumbra.Structs; +using Penumbra.Util; + +namespace Penumbra.Mod +{ + // Contains descriptive data about the mod as well as possible settings and fileswaps. + public class ModMeta + { + public uint FileVersion { get; set; } + public string Name { get; set; } = "Mod"; + public string Author { get; set; } = ""; + public string Description { get; set; } = ""; + public string Version { get; set; } = ""; + public string Website { get; set; } = ""; + public List< string > ChangedItems { get; set; } = new(); + + [JsonProperty( ItemConverterType = typeof( GamePathConverter ) )] + public Dictionary< GamePath, GamePath > FileSwaps { get; set; } = new(); + + public Dictionary< string, OptionGroup > Groups { get; set; } = new(); + + [JsonIgnore] + private int FileHash { get; set; } + + [JsonIgnore] + public bool HasGroupsWithConfig { get; private set; } + + public bool RefreshFromFile( FileInfo filePath ) + { + var newMeta = LoadFromFile( filePath ); + if( newMeta == null ) + { + return true; + } + + if( newMeta.FileHash == FileHash ) + { + return false; + } + + FileVersion = newMeta.FileVersion; + Name = newMeta.Name; + Author = newMeta.Author; + Description = newMeta.Description; + Version = newMeta.Version; + Website = newMeta.Website; + ChangedItems = newMeta.ChangedItems; + FileSwaps = newMeta.FileSwaps; + Groups = newMeta.Groups; + FileHash = newMeta.FileHash; + HasGroupsWithConfig = newMeta.HasGroupsWithConfig; + return true; + } + + + public static ModMeta? LoadFromFile( FileInfo filePath ) + { + try + { + var text = File.ReadAllText( filePath.FullName ); + + var meta = JsonConvert.DeserializeObject< ModMeta >( text, + new JsonSerializerSettings { NullValueHandling = NullValueHandling.Ignore } ); + if( meta != null ) + { + meta.FileHash = text.GetHashCode(); + meta.HasGroupsWithConfig = meta.Groups.Values.Any( g => g.SelectionType == SelectType.Multi || g.Options.Count > 1 ); + } + + return meta; + } + catch( Exception e ) + { + PluginLog.Error( $"Could not load mod meta:\n{e}" ); + return null; + } + } + + 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; + } + } + 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/Mod/ModResources.cs b/Penumbra/Mod/ModResources.cs new file mode 100644 index 00000000..7f0f1292 --- /dev/null +++ b/Penumbra/Mod/ModResources.cs @@ -0,0 +1,86 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using Penumbra.Meta; + +namespace Penumbra.Mod +{ + [Flags] + public enum ResourceChange + { + Files = 1, + Meta = 2, + } + + // Contains static mod data that should only change on filesystem changes. + public class ModResources + { + public List< FileInfo > ModFiles { get; private set; } = new(); + public List< FileInfo > MetaFiles { get; private set; } = new(); + + public MetaCollection MetaManipulations { get; private set; } = new(); + + + private void ForceManipulationsUpdate( ModMeta meta, DirectoryInfo basePath ) + { + MetaManipulations.Update( MetaFiles, basePath, meta ); + MetaManipulations.SaveToFile( MetaCollection.FileName( basePath ) ); + } + + public void SetManipulations( ModMeta meta, DirectoryInfo basePath ) + { + var newManipulations = MetaCollection.LoadFromFile( MetaCollection.FileName( basePath ) ); + if( newManipulations == null ) + { + ForceManipulationsUpdate( meta, basePath ); + } + else + { + MetaManipulations = newManipulations; + if( !MetaManipulations.Validate( meta ) ) + { + ForceManipulationsUpdate( meta, basePath ); + } + } + } + + + // Update the current set of files used by the mod, + // returns true if anything changed. + public ResourceChange RefreshModFiles( DirectoryInfo basePath ) + { + List< FileInfo > tmpFiles = new( ModFiles.Count ); + List< FileInfo > tmpMetas = new( MetaFiles.Count ); + // we don't care about any _files_ in the root dir, but any folders should be a game folder/file combo + foreach( var file in basePath.EnumerateDirectories() + .SelectMany( dir => dir.EnumerateFiles( "*.*", SearchOption.AllDirectories ) ) + .OrderBy( f => f.FullName ) ) + { + if( file.Extension != ".meta" ) + { + tmpFiles.Add( file ); + } + else + { + tmpMetas.Add( file ); + } + } + + ResourceChange changes = 0; + if( !tmpFiles.SequenceEqual( ModFiles ) ) + { + ModFiles = tmpFiles; + changes |= ResourceChange.Files; + } + + if( !tmpMetas.SequenceEqual( MetaFiles ) ) + { + MetaFiles = tmpMetas; + changes |= ResourceChange.Meta; + } + + return changes; + } + } +} \ No newline at end of file diff --git a/Penumbra/Mod/ModSettings.cs b/Penumbra/Mod/ModSettings.cs new file mode 100644 index 00000000..e40448ca --- /dev/null +++ b/Penumbra/Mod/ModSettings.cs @@ -0,0 +1,74 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using Penumbra.Structs; + +namespace Penumbra.Mod +{ + public class ModSettings + { + public bool Enabled { get; set; } + public int Priority { get; set; } + public Dictionary< string, int > Settings { get; set; } = new(); + + // For backwards compatibility + private Dictionary< string, int > Conf + { + set => Settings = value; + } + + public ModSettings DeepCopy() + { + var settings = new ModSettings + { + Enabled = Enabled, + Priority = Priority, + Settings = Settings.ToDictionary( kvp => kvp.Key, kvp => kvp.Value ), + }; + return settings; + } + + public static ModSettings DefaultSettings( ModMeta meta ) + { + return new() + { + Enabled = false, + Priority = 0, + Settings = meta.Groups.ToDictionary( kvp => kvp.Key, _ => 0 ), + }; + } + + public bool FixSpecificSetting( string name, ModMeta meta ) + { + if( !meta.Groups.TryGetValue( name, out var group ) ) + { + return Settings.Remove( name ); + } + + if( Settings.TryGetValue( name, out var oldSetting ) ) + { + Settings[ name ] = group.SelectionType switch + { + SelectType.Single => Math.Min( Math.Max( oldSetting, 0 ), group.Options.Count - 1 ), + SelectType.Multi => Math.Min( Math.Max( oldSetting, 0 ), ( 1 << group.Options.Count ) - 1 ), + _ => Settings[ group.GroupName ], + }; + return oldSetting != Settings[ group.GroupName ]; + } + + Settings[ name ] = 0; + return true; + } + + public bool FixInvalidSettings( ModMeta meta ) + { + if( meta.Groups.Count == 0 ) + { + return false; + } + + return Settings.Keys.ToArray().Union( meta.Groups.Keys ) + .Aggregate( false, ( current, name ) => current | FixSpecificSetting( name, meta ) ); + } + } +} \ No newline at end of file diff --git a/Penumbra/Models/NamedModSettings.cs b/Penumbra/Mod/NamedModSettings.cs similarity index 92% rename from Penumbra/Models/NamedModSettings.cs rename to Penumbra/Mod/NamedModSettings.cs index 5b70e029..34e3f883 100644 --- a/Penumbra/Models/NamedModSettings.cs +++ b/Penumbra/Mod/NamedModSettings.cs @@ -1,7 +1,8 @@ using System.Collections.Generic; using System.Linq; +using Penumbra.Structs; -namespace Penumbra.Models +namespace Penumbra.Mod { public class NamedModSettings { @@ -11,7 +12,7 @@ namespace Penumbra.Models public void AddFromModSetting( ModSettings s, ModMeta meta ) { Priority = s.Priority; - Settings = s.Settings.Keys.ToDictionary( K => K, K => new HashSet< string >() ); + Settings = s.Settings.Keys.ToDictionary( k => k, _ => new HashSet< string >() ); foreach( var kvp in Settings ) { diff --git a/Penumbra/Models/GroupInformation.cs b/Penumbra/Models/GroupInformation.cs deleted file mode 100644 index 9982f52d..00000000 --- a/Penumbra/Models/GroupInformation.cs +++ /dev/null @@ -1,42 +0,0 @@ -using System.Collections.Generic; -using Newtonsoft.Json; -using Penumbra.Util; - -namespace Penumbra.Models -{ - public enum SelectType - { - Single, - Multi - } - - public struct Option - { - public string OptionName; - public string OptionDesc; - - [JsonProperty( ItemConverterType = typeof( SingleOrArrayConverter< GamePath > ) )] - public Dictionary< RelPath, HashSet< GamePath > > OptionFiles; - - public bool AddFile( RelPath filePath, GamePath gamePath ) - { - if( OptionFiles.TryGetValue( filePath, out var set ) ) - { - return set.Add( gamePath ); - } - - OptionFiles[ filePath ] = new HashSet< GamePath >() { gamePath }; - return true; - } - } - - public struct OptionGroup - { - public string GroupName; - - [JsonConverter( typeof( Newtonsoft.Json.Converters.StringEnumConverter ) )] - public SelectType SelectionType; - - public List< Option > Options; - } -} \ No newline at end of file diff --git a/Penumbra/Models/ModInfo.cs b/Penumbra/Models/ModInfo.cs deleted file mode 100644 index 2fa0e5a7..00000000 --- a/Penumbra/Models/ModInfo.cs +++ /dev/null @@ -1,23 +0,0 @@ -using Newtonsoft.Json; -using Penumbra.Mods; - -namespace Penumbra.Models -{ - public class ModInfo : ModSettings - { - public ModInfo( ResourceMod mod ) - => Mod = mod; - - public string FolderName { get; set; } = ""; - public bool Enabled { get; set; } - - [JsonIgnore] - public ResourceMod Mod { get; set; } - - public bool FixSpecificSetting( string name ) - => FixSpecificSetting( Mod.Meta, name ); - - public bool FixInvalidSettings() - => FixInvalidSettings( Mod.Meta ); - } -} \ No newline at end of file diff --git a/Penumbra/Models/ModMeta.cs b/Penumbra/Models/ModMeta.cs deleted file mode 100644 index 4932f50d..00000000 --- a/Penumbra/Models/ModMeta.cs +++ /dev/null @@ -1,134 +0,0 @@ -using System; -using System.Collections.Generic; -using System.IO; -using System.Linq; -using Newtonsoft.Json; -using Penumbra.Util; - -namespace Penumbra.Models -{ - public class ModMeta - { - public uint FileVersion { get; set; } - public string Name { get; set; } = "Mod"; - public string Author { get; set; } = ""; - public string Description { get; set; } = ""; - - public string Version { get; set; } = ""; - - public string Website { get; set; } = ""; - - public List< string > ChangedItems { get; set; } = new(); - - - [JsonProperty( ItemConverterType = typeof( GamePathConverter ))] - public Dictionary< GamePath, GamePath > FileSwaps { get; } = new(); - - public Dictionary< string, OptionGroup > Groups { get; set; } = new(); - - [JsonIgnore] - public bool HasGroupWithConfig { get; set; } = false; - - private static readonly JsonSerializerSettings JsonSettings - = new() { NullValueHandling = NullValueHandling.Ignore }; - - public static ModMeta? LoadFromFile( string filePath ) - { - try - { - var meta = JsonConvert.DeserializeObject< ModMeta >( File.ReadAllText( filePath ), JsonSettings ); - if( meta != null ) - { - meta.HasGroupWithConfig = meta.Groups.Count > 0 - && meta.Groups.Values.Any( G => G.SelectionType == SelectType.Multi || G.Options.Count > 1 ); - } - - return meta; - } - catch( Exception ) - { - return null; - // todo: handle broken mods properly - } - } - - private static bool ApplySingleGroupFiles( OptionGroup group, RelPath relPath, int selection, HashSet< GamePath > paths ) - { - if( group.Options[ selection ].OptionFiles.TryGetValue( relPath, out var groupPaths ) ) - { - paths.UnionWith( groupPaths ); - return true; - } - - for( var i = 0; i < group.Options.Count; ++i ) - { - if( i == selection ) - { - continue; - } - - if( group.Options[ i ].OptionFiles.ContainsKey( relPath ) ) - { - return true; - } - } - - return false; - } - - private static bool ApplyMultiGroupFiles( OptionGroup group, RelPath relPath, int selection, HashSet< GamePath > paths ) - { - var doNotAdd = false; - for( var i = 0; i < group.Options.Count; ++i ) - { - if( ( selection & ( 1 << i ) ) != 0 ) - { - if( group.Options[ i ].OptionFiles.TryGetValue( relPath, out var groupPaths ) ) - { - paths.UnionWith( groupPaths ); - } - } - else if( group.Options[ i ].OptionFiles.ContainsKey( relPath ) ) - { - doNotAdd = true; - } - } - - return doNotAdd; - } - - public (bool configChanged, HashSet< GamePath > paths) GetFilesForConfig( RelPath relPath, ModSettings settings ) - { - var doNotAdd = false; - var configChanged = false; - - HashSet< GamePath > paths = new(); - foreach( var group in Groups.Values ) - { - configChanged |= settings.FixSpecificSetting( this, group.GroupName ); - - if( group.Options.Count == 0 ) - { - continue; - } - - switch( group.SelectionType ) - { - case SelectType.Single: - doNotAdd |= ApplySingleGroupFiles( group, relPath, settings.Settings[ group.GroupName ], paths ); - break; - case SelectType.Multi: - doNotAdd |= ApplyMultiGroupFiles( group, relPath, settings.Settings[ group.GroupName ], paths ); - break; - } - } - - if( !doNotAdd ) - { - paths.Add( new GamePath( relPath ) ); - } - - return ( configChanged, paths ); - } - } -} \ No newline at end of file diff --git a/Penumbra/Models/ModSettings.cs b/Penumbra/Models/ModSettings.cs deleted file mode 100644 index b7a1662e..00000000 --- a/Penumbra/Models/ModSettings.cs +++ /dev/null @@ -1,92 +0,0 @@ -using System.Collections.Generic; -using System.Linq; -using System; - -namespace Penumbra.Models -{ - public class ModSettings - { - public int Priority { get; set; } - public Dictionary< string, int > Settings { get; set; } = new(); - - // For backwards compatibility - private Dictionary< string, int > Conf - { - set => Settings = value; - } - - public static ModSettings CreateFrom( NamedModSettings n, ModMeta meta ) - { - ModSettings ret = new() - { - Priority = n.Priority, - Settings = n.Settings.Keys.ToDictionary( K => K, K => 0 ) - }; - - foreach( var kvp in n.Settings ) - { - if( !meta.Groups.TryGetValue( kvp.Key, out var info ) ) - { - continue; - } - - if( info.SelectionType == SelectType.Single ) - { - if( n.Settings[ kvp.Key ].Count == 0 ) - { - ret.Settings[ kvp.Key ] = 0; - } - else - { - var idx = info.Options.FindIndex( O => O.OptionName == n.Settings[ kvp.Key ].Last() ); - ret.Settings[ kvp.Key ] = idx < 0 ? 0 : idx; - } - } - else - { - foreach( var idx in n.Settings[ kvp.Key ] - .Select( option => info.Options.FindIndex( O => O.OptionName == option ) ) - .Where( idx => idx >= 0 ) ) - { - ret.Settings[ kvp.Key ] |= 1 << idx; - } - } - } - - return ret; - } - - public bool FixSpecificSetting( ModMeta meta, string name ) - { - if( !meta.Groups.TryGetValue( name, out var group ) ) - { - return Settings.Remove( name ); - } - - if( Settings.TryGetValue( name, out var oldSetting ) ) - { - Settings[ name ] = group.SelectionType switch - { - SelectType.Single => Math.Min( Math.Max( oldSetting, 0 ), group.Options.Count - 1 ), - SelectType.Multi => Math.Min( Math.Max( oldSetting, 0 ), ( 1 << group.Options.Count ) - 1 ), - _ => Settings[ group.GroupName ] - }; - return oldSetting != Settings[ group.GroupName ]; - } - - Settings[ name ] = 0; - return true; - } - - public bool FixInvalidSettings( ModMeta meta ) - { - if( meta.Groups.Count == 0 ) - { - return false; - } - - return Settings.Keys.ToArray().Union( meta.Groups.Keys ) - .Aggregate( false, ( current, name ) => current | FixSpecificSetting( meta, name ) ); - } - } -} \ No newline at end of file diff --git a/Penumbra/Mods/ModCollection.cs b/Penumbra/Mods/ModCollection.cs index a9a4c2f8..1aa0e390 100644 --- a/Penumbra/Mods/ModCollection.cs +++ b/Penumbra/Mods/ModCollection.cs @@ -1,229 +1,236 @@ +using Dalamud.Plugin; +using Newtonsoft.Json; using System; using System.Collections.Generic; using System.IO; using System.Linq; -using Dalamud.Plugin; -using Newtonsoft.Json; -using Penumbra.Models; +using Penumbra.Mod; +using Penumbra.Util; namespace Penumbra.Mods { public class ModCollection { - private readonly DirectoryInfo _basePath; + public const string DefaultCollection = "Default"; - public List< ModInfo >? ModSettings { get; set; } - public ResourceMod[]? EnabledMods { get; set; } + public string Name { get; set; } + public Dictionary< string, ModSettings > Settings { get; } - public ModCollection( DirectoryInfo basePath ) - => _basePath = basePath; - - public void Load( bool invertOrder = false ) + public ModCollection() { - // find the collection json - var collectionPath = Path.Combine( _basePath.FullName, "collection.json" ); - if( File.Exists( collectionPath ) ) - { - try - { - ModSettings = JsonConvert.DeserializeObject< List< ModInfo > >( File.ReadAllText( collectionPath ) ); - ModSettings = ModSettings.OrderBy( x => x.Priority ).ToList(); - } - catch( Exception e ) - { - PluginLog.Error( $"failed to read log collection information, failed path: {collectionPath}, err: {e.Message}" ); - } - } - -#if DEBUG - if( ModSettings != null ) - { - foreach( var ms in ModSettings ) - { - PluginLog.Debug( - "mod: {ModName} Enabled: {Enabled} Priority: {Priority}", - ms.FolderName, ms.Enabled, ms.Priority - ); - } - } -#endif - - ModSettings ??= new List< ModInfo >(); - var foundMods = new List< string >(); - - foreach( var modDir in _basePath.EnumerateDirectories() ) - { - if( modDir.Name.ToLowerInvariant() == MetaManager.TmpDirectory ) - { - continue; - } - - var metaFile = modDir.EnumerateFiles().FirstOrDefault( f => f.Name == "meta.json" ); - - if( metaFile == null ) - { -#if DEBUG - PluginLog.Error( "mod meta is missing for resource mod: {ResourceModLocation}", modDir ); -#else - PluginLog.Debug( "mod meta is missing for resource mod: {ResourceModLocation}", modDir ); -#endif - continue; - } - - var meta = ModMeta.LoadFromFile( metaFile.FullName ) ?? new ModMeta(); - - var mod = new ResourceMod( meta, modDir ); - FindOrCreateModSettings( mod ); - foundMods.Add( modDir.Name ); - mod.RefreshModFiles(); - } - - // remove any mods from the collection we didn't find - ModSettings = ModSettings.Where( - x => - foundMods.Any( - fm => string.Equals( x.FolderName, fm, StringComparison.InvariantCultureIgnoreCase ) - ) - ).ToList(); - - // if anything gets removed above, the priority ordering gets fucked, so we need to resort and reindex them otherwise BAD THINGS HAPPEN - ModSettings = ModSettings.OrderBy( x => x.Priority ).ToList(); - var p = 0; - foreach( var modSetting in ModSettings ) - { - modSetting.Priority = p++; - } - - // reorder the resourcemods list so we can just directly iterate - EnabledMods = GetOrderedAndEnabledModList( invertOrder ).ToArray(); - - // write the collection metadata back to disk - Save(); + Name = DefaultCollection; + Settings = new Dictionary< string, ModSettings >(); } - public void Save() + public ModCollection( string name, Dictionary< string, ModSettings > settings ) { - var collectionPath = Path.Combine( _basePath.FullName, "collection.json" ); + Name = name; + Settings = settings.ToDictionary( kvp => kvp.Key, kvp => kvp.Value.DeepCopy() ); + } + + private bool CleanUnavailableSettings( Dictionary< string, ModData > data ) + { + if( Settings.Count <= data.Count ) + { + return false; + } + + List< string > removeList = new(); + foreach( var settingKvp in Settings ) + { + if( !data.ContainsKey( settingKvp.Key ) ) + { + removeList.Add( settingKvp.Key ); + } + } + + foreach( var s in removeList ) + { + Settings.Remove( s ); + } + + return removeList.Count > 0; + } + + public void CreateCache( DirectoryInfo modDirectory, Dictionary< string, ModData > data, bool cleanUnavailable = false ) + { + Cache = new ModCollectionCache( Name, modDirectory ); + var changedSettings = false; + foreach( var modKvp in data ) + { + if( Settings.TryGetValue( modKvp.Key, out var settings ) ) + { + Cache.AvailableMods.Add( new Mod.Mod( settings, modKvp.Value ) ); + } + else + { + changedSettings = true; + var newSettings = ModSettings.DefaultSettings( modKvp.Value.Meta ); + Settings.Add( modKvp.Key, newSettings ); + Cache.AvailableMods.Add( new Mod.Mod( newSettings, modKvp.Value ) ); + } + } + + if( cleanUnavailable ) + { + changedSettings |= CleanUnavailableSettings( data ); + } + + if( changedSettings ) + { + Save( Service< DalamudPluginInterface >.Get() ); + } + + Cache.SortMods(); + CalculateEffectiveFileList( modDirectory, true ); + } + + public void UpdateSetting( ModData mod ) + { + if( !Settings.TryGetValue( mod.BasePath.Name, out var settings ) ) + { + return; + } + + if( settings.FixInvalidSettings( mod.Meta ) ) + { + Save( Service< DalamudPluginInterface >.Get() ); + } + } + + public void UpdateSettings() + { + if( Cache == null ) + { + return; + } + + var changes = false; + foreach( var mod in Cache.AvailableMods ) + { + changes |= mod.FixSettings(); + } + + if( changes ) + { + Save( Service< DalamudPluginInterface >.Get() ); + } + } + + public void CalculateEffectiveFileList( DirectoryInfo modDir, bool withMetaManipulations ) + { + Cache ??= new ModCollectionCache( Name, modDir ); + UpdateSettings(); + Cache.CalculateEffectiveFileList(); + if( withMetaManipulations ) + { + Cache.UpdateMetaManipulations(); + } + } + + + [JsonIgnore] + public ModCollectionCache? Cache { get; private set; } + + public static ModCollection? LoadFromFile( FileInfo file ) + { + if( !file.Exists ) + { + PluginLog.Error( $"Could not read collection because {file.FullName} does not exist." ); + return null; + } try { - var data = JsonConvert.SerializeObject( ModSettings.OrderBy( x => x.Priority ).ToList() ); - File.WriteAllText( collectionPath, data ); + var collection = JsonConvert.DeserializeObject< ModCollection >( File.ReadAllText( file.FullName ) ); + return collection; } catch( Exception e ) { - PluginLog.Error( $"failed to write log collection information, failed path: {collectionPath}, err: {e.Message}" ); + PluginLog.Error( $"Could not read collection information from {file.FullName}:\n{e}" ); + } + + return null; + } + + private void SaveToFile( FileInfo file ) + { + try + { + File.WriteAllText( file.FullName, JsonConvert.SerializeObject( this, Formatting.Indented ) ); + } + catch( Exception e ) + { + PluginLog.Error( $"Could not write collection {Name} to {file.FullName}:\n{e}" ); } } - private int CleanPriority( int priority ) - => priority < 0 ? 0 : priority >= ModSettings!.Count ? ModSettings.Count - 1 : priority; + public static DirectoryInfo CollectionDir( DalamudPluginInterface pi ) + => new( Path.Combine( pi.GetPluginConfigDirectory(), "collections" ) ); - public void ReorderMod( ModInfo info, int newPriority ) + private static FileInfo FileName( DirectoryInfo collectionDir, string name ) + => new( Path.Combine( collectionDir.FullName, $"{name.RemoveInvalidPathSymbols()}.json" ) ); + + public FileInfo FileName() + => new( Path.Combine( Service< DalamudPluginInterface >.Get().GetPluginConfigDirectory(), + $"{Name.RemoveInvalidPathSymbols()}.json" ) ); + + public void Save( DalamudPluginInterface pi ) { - if( ModSettings == null ) + try { - return; + var dir = CollectionDir( pi ); + dir.Create(); + var file = FileName( dir, Name ); + SaveToFile( file ); } - - var oldPriority = info.Priority; - newPriority = CleanPriority( newPriority ); - if( oldPriority == newPriority ) + catch( Exception e ) { - return; + PluginLog.Error( $"Could not save collection {Name}:\n{e}" ); } + } - info.Priority = newPriority; - if( newPriority < oldPriority ) + public static ModCollection? Load( string name, DalamudPluginInterface pi ) + { + var file = FileName( CollectionDir( pi ), name ); + return file.Exists ? LoadFromFile( file ) : null; + } + + public void Delete( DalamudPluginInterface pi ) + { + var file = FileName( CollectionDir( pi ), Name ); + if( file.Exists ) { - for( var i = oldPriority - 1; i >= newPriority; --i ) + try { - ++ModSettings![ i ].Priority; - ModSettings.Swap( i, i + 1 ); + file.Delete(); } + catch( Exception e ) + { + PluginLog.Error( $"Could not delete collection file {file} for {Name}:\n{e}" ); + } + } + } + + public void AddMod( ModData data ) + { + if( Cache == null ) + { + return; + } + + if( Settings.TryGetValue( data.BasePath.Name, out var settings ) ) + { + Cache.AddMod( settings, data ); } else { - for( var i = oldPriority + 1; i <= newPriority; ++i ) - { - --ModSettings![ i ].Priority; - ModSettings.Swap( i - 1, i ); - } + Cache.AddMod( ModSettings.DefaultSettings( data.Meta ), data ); } - - EnabledMods = GetOrderedAndEnabledModList().ToArray(); - Save(); } - public void ReorderMod( ModInfo info, bool up ) - => ReorderMod( info, info.Priority + ( up ? 1 : -1 ) ); - - public ModInfo? FindModSettings( string name ) - { - var settings = ModSettings?.FirstOrDefault( - x => string.Equals( x.FolderName, name, StringComparison.InvariantCultureIgnoreCase ) - ); -#if DEBUG - PluginLog.Information( "finding mod {ModName} - found: {ModSettingsExist}", name, settings != null ); -#endif - return settings; - } - - public ModInfo AddModSettings( ResourceMod mod ) - { - var entry = new ModInfo( mod ) - { - Priority = ModSettings?.Count ?? 0, - FolderName = mod.ModBasePath.Name, - Enabled = true, - }; - entry.FixInvalidSettings(); -#if DEBUG - PluginLog.Information( "creating mod settings {ModName}", entry.FolderName ); -#endif - ModSettings ??= new List< ModInfo >(); - ModSettings.Add( entry ); - return entry; - } - - public ModInfo FindOrCreateModSettings( ResourceMod mod ) - { - var settings = FindModSettings( mod.ModBasePath.Name ); - if( settings == null ) - { - return AddModSettings( mod ); - } - - settings.Mod = mod; - settings.FixInvalidSettings(); - return settings; - } - - public IEnumerable< ModInfo > GetOrderedAndEnabledModSettings( bool invertOrder = false ) - { - var query = ModSettings? - .Where( x => x.Enabled ) - ?? Enumerable.Empty< ModInfo >(); - - if( !invertOrder ) - { - return query.OrderBy( x => x.Priority ); - } - - return query.OrderByDescending( x => x.Priority ); - } - - public IEnumerable< ResourceMod > GetOrderedAndEnabledModList( bool invertOrder = false ) - { - return GetOrderedAndEnabledModSettings( invertOrder ) - .Select( x => x.Mod ); - } - - public IEnumerable< (ResourceMod, ModInfo) > GetOrderedAndEnabledModListWithSettings( bool invertOrder = false ) - { - return GetOrderedAndEnabledModSettings( invertOrder ) - .Select( x => ( x.Mod, x ) ); - } + public string? ResolveSwappedOrReplacementPath( GamePath gameResourcePath ) + => Cache?.ResolveSwappedOrReplacementPath( gameResourcePath ); } } \ No newline at end of file diff --git a/Penumbra/Mods/ModCollectionCache.cs b/Penumbra/Mods/ModCollectionCache.cs new file mode 100644 index 00000000..217cf950 --- /dev/null +++ b/Penumbra/Mods/ModCollectionCache.cs @@ -0,0 +1,167 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using Penumbra.Meta; +using Penumbra.Mod; +using Penumbra.Util; + +namespace Penumbra.Mods +{ + public class ModCollectionCache + { + public readonly List< Mod.Mod > AvailableMods = new(); + + public readonly Dictionary< GamePath, FileInfo > ResolvedFiles = new(); + public readonly Dictionary< GamePath, GamePath > SwappedFiles = new(); + public readonly MetaManager MetaManipulations; + + public ModCollectionCache( string collectionName, DirectoryInfo modDir ) + => MetaManipulations = new MetaManager( collectionName, ResolvedFiles, modDir ); + + public void SortMods() + { + AvailableMods.Sort( ( m1, m2 ) => string.Compare( m1.Data.Meta.Name, m2.Data.Meta.Name, StringComparison.InvariantCulture ) ); + } + + private void AddFiles( Dictionary< GamePath, Mod.Mod > registeredFiles, Mod.Mod mod ) + { + foreach( var file in mod.Data.Resources.ModFiles ) + { + var gamePaths = mod.GetFiles( file ); + foreach( var gamePath in gamePaths ) + { + if( !registeredFiles.TryGetValue( gamePath, out var oldMod ) ) + { + registeredFiles.Add( gamePath, mod ); + ResolvedFiles[ gamePath ] = file; + } + else + { + mod.Cache.AddConflict( oldMod, gamePath ); + } + } + } + } + + private void AddSwaps( Dictionary< GamePath, Mod.Mod > registeredFiles, Mod.Mod mod ) + { + foreach( var swap in mod.Data.Meta.FileSwaps ) + { + if( !registeredFiles.TryGetValue( swap.Key, out var oldMod ) ) + { + registeredFiles.Add( swap.Key, mod ); + SwappedFiles.Add( swap.Key, swap.Value ); + } + else + { + mod.Cache.AddConflict( oldMod, swap.Key ); + } + } + } + + private void AddManipulations( Mod.Mod mod ) + { + foreach( var manip in mod.Data.Resources.MetaManipulations.GetManipulationsForConfig( mod.Settings, mod.Data.Meta ) ) + { + if( MetaManipulations.TryGetValue( manip, out var precedingMod ) ) + { + mod.Cache.AddConflict( precedingMod, manip ); + } + else + { + MetaManipulations.ApplyMod( manip, mod ); + } + } + } + + public void UpdateMetaManipulations() + { + MetaManipulations.Reset(); + + foreach( var mod in AvailableMods.Where( m => m.Settings.Enabled && m.Data.Resources.MetaManipulations.Count > 0 ) + .OrderByDescending( m => m.Settings.Priority ) ) + { + mod.Cache.ClearMetaConflicts(); + AddManipulations( mod ); + } + + MetaManipulations.WriteNewFiles(); + } + + public void CalculateEffectiveFileList() + { + ResolvedFiles.Clear(); + SwappedFiles.Clear(); + + var registeredFiles = new Dictionary< GamePath, Mod.Mod >(); + foreach( var mod in AvailableMods.Where( m => m.Settings.Enabled ).OrderByDescending( m => m.Settings.Priority ) ) + { + mod.Cache.ClearFileConflicts(); + AddFiles( registeredFiles, mod ); + AddSwaps( registeredFiles, mod ); + } + } + + public void RemoveMod( DirectoryInfo basePath ) + { + var hadMeta = false; + var wasEnabled = false; + AvailableMods.RemoveAll( m => + { + if( m.Settings.Enabled ) + { + wasEnabled = true; + hadMeta |= m.Data.Resources.MetaManipulations.Count > 0; + } + + return m.Data.BasePath.Name == basePath.Name; + } ); + + if( wasEnabled ) + { + CalculateEffectiveFileList(); + if( hadMeta ) + { + UpdateMetaManipulations(); + } + } + } + + public void AddMod( ModSettings settings, ModData data ) + { + AvailableMods.Add( new Mod.Mod( settings, data ) ); + SortMods(); + if( settings.Enabled ) + { + CalculateEffectiveFileList(); + if( data.Resources.MetaManipulations.Count > 0 ) + { + UpdateMetaManipulations(); + } + } + } + + public FileInfo? GetCandidateForGameFile( GamePath gameResourcePath ) + { + if( !ResolvedFiles.TryGetValue( gameResourcePath, out var candidate ) ) + { + return null; + } + + candidate.Refresh(); + if( candidate.FullName.Length >= 260 || !candidate.Exists ) + { + return null; + } + + return candidate; + } + + public GamePath? GetSwappedFilePath( GamePath gameResourcePath ) + => SwappedFiles.TryGetValue( gameResourcePath, out var swappedPath ) ? swappedPath : null; + + public string? ResolveSwappedOrReplacementPath( GamePath gameResourcePath ) + => GetCandidateForGameFile( gameResourcePath )?.FullName.Replace( '\\', '/' ) ?? GetSwappedFilePath( gameResourcePath ) ?? null; + } +} \ No newline at end of file diff --git a/Penumbra/Mods/ModManager.cs b/Penumbra/Mods/ModManager.cs index e8085e34..58da8881 100644 --- a/Penumbra/Mods/ModManager.cs +++ b/Penumbra/Mods/ModManager.cs @@ -1,207 +1,267 @@ using System; using System.Collections.Generic; using System.IO; +using System.Linq; using Dalamud.Plugin; -using Penumbra.Hooks; -using Penumbra.Models; +using Penumbra.Meta; +using Penumbra.Mod; using Penumbra.Util; namespace Penumbra.Mods { - public class ModManager : IDisposable + public class ModManager { - private readonly Plugin _plugin; - public readonly Dictionary< GamePath, FileInfo > ResolvedFiles = new(); - public readonly Dictionary< GamePath, GamePath > SwappedFiles = new(); - public MetaManager? MetaManipulations; + private readonly Plugin _plugin; + public DirectoryInfo BasePath { get; private set; } - public ModCollection? Mods { get; set; } - private DirectoryInfo? _basePath; + public Dictionary< string, ModData > Mods { get; } = new(); + public Dictionary< string, ModCollection > Collections { get; } = new(); + + public ModCollection CurrentCollection { get; private set; } public ModManager( Plugin plugin ) - => _plugin = plugin; - - public void DiscoverMods() - => DiscoverMods( _basePath ); - - public void DiscoverMods( string? basePath ) - => DiscoverMods( basePath == null ? null : new DirectoryInfo( basePath ) ); - - public void DiscoverMods( DirectoryInfo? basePath ) { - _basePath = basePath; - if( basePath == null || !basePath.Exists ) + _plugin = plugin; + BasePath = new DirectoryInfo( plugin.Configuration!.ModDirectory ); + ReadCollections(); + CurrentCollection = Collections.Values.First(); + if( !SetCurrentCollection( plugin.Configuration!.CurrentCollection ) ) { - Mods = null; - return; - } + PluginLog.Debug( "Last choice of collection {Name} is not available, reset to Default.", + plugin.Configuration!.CurrentCollection ); - // FileSystemPasta(); - - Mods = new ModCollection( basePath ); - Mods.Load(); - - CalculateEffectiveFileList(); - } - - public void CalculateEffectiveFileList() - { - ResolvedFiles.Clear(); - SwappedFiles.Clear(); - MetaManipulations?.Dispose(); - - if( Mods == null ) - { - return; - } - - MetaManipulations = new MetaManager( ResolvedFiles, _basePath! ); - - var changedSettings = false; - var registeredFiles = new Dictionary< GamePath, string >(); - foreach( var (mod, settings) in Mods.GetOrderedAndEnabledModListWithSettings( _plugin!.Configuration!.InvertModListOrder ) ) - { - mod.FileConflicts.Clear(); - - changedSettings |= ProcessModFiles( registeredFiles, mod, settings ); - ProcessSwappedFiles( registeredFiles, mod, settings ); - } - - if( changedSettings ) - { - Mods.Save(); - } - - MetaManipulations.WriteNewFiles(); - - Service< GameResourceManagement >.Get().ReloadPlayerResources(); - } - - private void ProcessSwappedFiles( Dictionary< GamePath, string > registeredFiles, ResourceMod mod, ModInfo settings ) - { - foreach( var swap in mod.Meta.FileSwaps ) - { - // just assume people put not fucked paths in here lol - if( !SwappedFiles.ContainsKey( swap.Value ) ) + if( SetCurrentCollection( ModCollection.DefaultCollection ) ) { - SwappedFiles[ swap.Key ] = swap.Value; - registeredFiles[ swap.Key ] = mod.Meta.Name; - } - else if( registeredFiles.TryGetValue( swap.Key, out var modName ) ) - { - mod.AddConflict( modName, swap.Key ); + PluginLog.Error( "Could not load any collection. Default collection unavailable." ); + CurrentCollection = new ModCollection(); } } } - private bool ProcessModFiles( Dictionary< GamePath, string > registeredFiles, ResourceMod mod, ModInfo settings ) + public bool SetCurrentCollection( string name ) { - var changedConfig = settings.FixInvalidSettings(); - foreach( var file in mod.ModFiles ) + if( Collections.TryGetValue( name, out var collection ) ) { - RelPath relativeFilePath = new( file, mod.ModBasePath ); - var (configChanged, gamePaths) = mod.Meta.GetFilesForConfig( relativeFilePath, settings ); - changedConfig |= configChanged; - if( file.Extension == ".meta" && gamePaths.Count > 0 ) + CurrentCollection = collection; + if( CurrentCollection.Cache == null ) { - AddManipulations( file, mod ); + CurrentCollection.CreateCache( BasePath, Mods ); } - else + + return true; + } + + return false; + } + + public void ReadCollections() + { + var collectionDir = ModCollection.CollectionDir( _plugin.PluginInterface! ); + if( collectionDir.Exists ) + { + foreach( var file in collectionDir.EnumerateFiles( "*.json" ) ) { - AddFiles( gamePaths, file, registeredFiles, mod ); + var collection = ModCollection.LoadFromFile( file ); + if( collection != null ) + { + if( file.Name != $"{collection.Name.RemoveInvalidPathSymbols()}.json" ) + { + PluginLog.Warning( $"Collection {file.Name} does not correspond to {collection.Name}." ); + } + + if( Collections.ContainsKey( collection.Name ) ) + { + PluginLog.Warning( $"Duplicate collection found: {collection.Name} already exists." ); + } + else + { + Collections.Add( collection.Name, collection ); + } + } } } - return changedConfig; - } - - private void AddFiles( IEnumerable< GamePath > gamePaths, FileInfo file, Dictionary< GamePath, string > registeredFiles, - ResourceMod mod ) - { - foreach( var gamePath in gamePaths ) + if( !Collections.ContainsKey( ModCollection.DefaultCollection ) ) { - if( !ResolvedFiles.ContainsKey( gamePath ) ) - { - ResolvedFiles[ gamePath ] = file; - registeredFiles[ gamePath ] = mod.Meta.Name; - } - else if( registeredFiles.TryGetValue( gamePath, out var modName ) ) - { - mod.AddConflict( modName, gamePath ); - } + var defaultCollection = new ModCollection(); + SaveCollection( defaultCollection ); + Collections.Add( defaultCollection.Name, defaultCollection ); } } - private void AddManipulations( FileInfo file, ResourceMod mod ) + public void SaveCollection( ModCollection collection ) + => collection.Save( _plugin.PluginInterface! ); + + + public bool AddCollection( string name, Dictionary< string, ModSettings > settings ) { - if( !mod.MetaManipulations.TryGetValue( file, out var meta ) ) + var nameFixed = name.RemoveInvalidPathSymbols().ToLowerInvariant(); + if( Collections.Values.Any( c => c.Name.RemoveInvalidPathSymbols().ToLowerInvariant() == nameFixed ) ) { - PluginLog.Error( $"{file.FullName} is a TexTools Meta File without meta information." ); - return; + PluginLog.Warning( $"The new collection {name} would lead to the same path as one that already exists." ); + return false; } - foreach( var manipulation in meta.Manipulations ) + var newCollection = new ModCollection( name, settings ); + Collections.Add( name, newCollection ); + SaveCollection( newCollection ); + CurrentCollection = newCollection; + return true; + } + + public bool RemoveCollection( string name ) + { + if( name == ModCollection.DefaultCollection ) { - MetaManipulations!.ApplyMod( manipulation ); + PluginLog.Error( "Can not remove the default collection." ); + return false; } - } - public void ChangeModPriority( ModInfo info, int newPriority ) - { - Mods!.ReorderMod( info, newPriority ); - CalculateEffectiveFileList(); - } - - public void ChangeModPriority( ModInfo info, bool up = false ) - { - Mods!.ReorderMod( info, up ); - CalculateEffectiveFileList(); - } - - public void DeleteMod( ResourceMod? mod ) - { - if( mod?.ModBasePath.Exists ?? false ) + if( Collections.TryGetValue( name, out var collection ) ) { - try + if( CurrentCollection == collection ) { - Directory.Delete( mod.ModBasePath.FullName, true ); - } - catch( Exception e ) - { - PluginLog.Error( $"Could not delete the mod {mod.ModBasePath.Name}:\n{e}" ); + SetCurrentCollection( ModCollection.DefaultCollection ); } + + collection.Delete( _plugin.PluginInterface! ); + Collections.Remove( name ); + return true; } + return false; + } + + public void DiscoverMods( DirectoryInfo basePath ) + { + BasePath = basePath; DiscoverMods(); } - public FileInfo? GetCandidateForGameFile( GamePath gameResourcePath ) + public void DiscoverMods() { - var val = ResolvedFiles.TryGetValue( gameResourcePath, out var candidate ); - if( !val ) + Mods.Clear(); + if( !BasePath.Exists ) { - return null; + PluginLog.Debug( "The mod directory {Directory} does not exist.", BasePath.FullName ); + try + { + Directory.CreateDirectory( BasePath.FullName ); + } + catch( Exception e ) + { + PluginLog.Error( $"The mod directory {BasePath.FullName} does not exist and could not be created:\n{e}" ); + return; + } } - if( candidate.FullName.Length >= 260 || !candidate.Exists ) + foreach( var modFolder in BasePath.EnumerateDirectories() ) { - return null; + var mod = ModData.LoadMod( modFolder ); + if( mod == null ) + { + continue; + } + + Mods.Add( modFolder.Name, mod ); } - return candidate; + foreach( var collection in Collections.Values.Where( c => c.Cache != null ) ) + { + collection.CreateCache( BasePath, Mods ); + } } - public GamePath? GetSwappedFilePath( GamePath gameResourcePath ) - => SwappedFiles.TryGetValue( gameResourcePath, out var swappedPath ) ? swappedPath : null; - - public string? ResolveSwappedOrReplacementFilePath( GamePath gameResourcePath ) - => GetCandidateForGameFile( gameResourcePath )?.FullName.Replace( '\\', '/' ) ?? GetSwappedFilePath( gameResourcePath ) ?? null; - - - public void Dispose() + public void DeleteMod( DirectoryInfo modFolder ) { - MetaManipulations?.Dispose(); - // _fileSystemWatcher?.Dispose(); + modFolder.Refresh(); + if( modFolder.Exists ) + { + try + { + Directory.Delete( modFolder.FullName, true ); + } + catch( Exception e ) + { + PluginLog.Error( $"Could not delete the mod {modFolder.Name}:\n{e}" ); + } + + Mods.Remove( modFolder.Name ); + foreach( var collection in Collections.Values.Where( c => c.Cache != null ) ) + { + collection.Cache!.RemoveMod( modFolder ); + } + } + } + + public bool AddMod( DirectoryInfo modFolder ) + { + var mod = ModData.LoadMod( modFolder ); + if( mod == null ) + { + return false; + } + + if( Mods.ContainsKey( modFolder.Name ) ) + { + return false; + } + + Mods.Add( modFolder.Name, mod ); + foreach( var collection in Collections.Values ) + { + collection.AddMod( mod ); + } + + return true; + } + + + public bool UpdateMod( ModData mod, bool recomputeMeta = false ) + { + var oldName = mod.Meta.Name; + var metaChanges = mod.Meta.RefreshFromFile( mod.MetaFile ); + var fileChanges = mod.Resources.RefreshModFiles( mod.BasePath ); + + if( !( recomputeMeta || metaChanges || fileChanges == 0 ) ) + { + return false; + } + + var nameChange = !string.Equals( oldName, mod.Meta.Name, StringComparison.InvariantCulture ); + + recomputeMeta |= fileChanges.HasFlag( ResourceChange.Meta ); + if( recomputeMeta ) + { + mod.Resources.MetaManipulations.Update( mod.Resources.MetaFiles, mod.BasePath, mod.Meta ); + mod.Resources.MetaManipulations.SaveToFile( MetaCollection.FileName( mod.BasePath ) ); + } + + foreach( var collection in Collections.Values ) + { + if( metaChanges ) + { + collection.UpdateSetting( mod ); + if( nameChange ) + { + collection.Cache?.SortMods(); + } + } + + if( fileChanges.HasFlag( ResourceChange.Files ) + && collection.Settings.TryGetValue( mod.BasePath.Name, out var settings ) + && settings.Enabled ) + { + collection.Cache?.CalculateEffectiveFileList(); + } + + if( recomputeMeta ) + { + collection.Cache?.UpdateMetaManipulations(); + } + } + + return true; } // private void FileSystemWatcherOnChanged( object sender, FileSystemEventArgs e ) diff --git a/Penumbra/Mods/ModManagerEditExtensions.cs b/Penumbra/Mods/ModManagerEditExtensions.cs new file mode 100644 index 00000000..8ec686c8 --- /dev/null +++ b/Penumbra/Mods/ModManagerEditExtensions.cs @@ -0,0 +1,188 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel; +using System.IO; +using System.Linq; +using Dalamud.Plugin; +using Penumbra.Mod; +using Penumbra.Structs; + +namespace Penumbra.Mods +{ + public static class ModManagerEditExtensions + { + public static bool RenameMod( this ModManager manager, string newName, ModData mod ) + { + if( newName.Length == 0 || string.Equals( newName, mod.Meta.Name, StringComparison.InvariantCulture ) ) + { + return false; + } + + mod.Meta.Name = newName; + mod.SaveMeta(); + foreach( var collection in manager.Collections.Values.Where( c => c.Cache != null ) ) + { + collection.Cache!.SortMods(); + } + + return true; + } + + public static bool RenameModFolder( this ModManager manager, ModData mod, DirectoryInfo newDir, bool move = true ) + { + if( move ) + { + newDir.Refresh(); + if( newDir.Exists ) + { + return false; + } + + var oldDir = new DirectoryInfo( mod.BasePath.FullName ); + try + { + oldDir.MoveTo( newDir.FullName ); + } + catch( Exception e ) + { + PluginLog.Error( $"Error while renaming directory {oldDir.FullName} to {newDir.FullName}:\n{e}" ); + return false; + } + } + + manager.Mods.Remove( mod.BasePath.Name ); + manager.Mods[ newDir.Name ] = mod; + + var oldBasePath = mod.BasePath; + mod.BasePath = newDir; + mod.MetaFile = ModData.MetaFileInfo( newDir ); + manager.UpdateMod( mod ); + + foreach( var collection in manager.Collections.Values ) + { + if( collection.Settings.TryGetValue( oldBasePath.Name, out var settings ) ) + { + collection.Settings[ newDir.Name ] = settings; + collection.Settings.Remove( oldBasePath.Name ); + manager.SaveCollection( collection ); + } + + if( collection.Cache != null ) + { + collection.Cache.RemoveMod( newDir ); + collection.AddMod( mod ); + } + } + + return true; + } + + public static bool ChangeModGroup( this ModManager manager, string oldGroupName, string newGroupName, ModData mod, + SelectType type = SelectType.Single ) + { + if( newGroupName == oldGroupName || mod.Meta.Groups.ContainsKey( newGroupName ) ) + { + return false; + } + + if( mod.Meta.Groups.TryGetValue( oldGroupName, out var oldGroup ) ) + { + if( newGroupName.Length > 0 ) + { + mod.Meta.Groups[ newGroupName ] = new OptionGroup() + { + GroupName = newGroupName, + SelectionType = oldGroup.SelectionType, + Options = oldGroup.Options, + }; + } + + mod.Meta.Groups.Remove( oldGroupName ); + } + else + { + if( newGroupName.Length == 0 ) + { + return false; + } + + mod.Meta.Groups[ newGroupName ] = new OptionGroup() + { + GroupName = newGroupName, + SelectionType = type, + Options = new List< Option >(), + }; + } + + mod.SaveMeta(); + + foreach( var collection in manager.Collections.Values ) + { + if( !collection.Settings.TryGetValue( mod.BasePath.Name, out var settings ) ) + { + continue; + } + + if( newGroupName.Length > 0 ) + { + settings.Settings[ newGroupName ] = settings.Settings.TryGetValue( oldGroupName, out var value ) ? value : 0; + } + + settings.Settings.Remove( oldGroupName ); + manager.SaveCollection( collection ); + } + + return true; + } + + public static bool RemoveModOption( this ModManager manager, int optionIdx, OptionGroup group, ModData mod ) + { + if( optionIdx < 0 || optionIdx >= group.Options.Count ) + { + return false; + } + + group.Options.RemoveAt( optionIdx ); + mod.SaveMeta(); + + static int MoveMultiSetting( int oldSetting, int idx ) + { + var bitmaskFront = ( 1 << idx ) - 1; + var bitmaskBack = ~( bitmaskFront | ( 1 << idx ) ); + return ( oldSetting & bitmaskFront ) | ( ( oldSetting & bitmaskBack ) >> 1 ); + } + + foreach( var collection in manager.Collections.Values ) + { + if( !collection.Settings.TryGetValue( mod.BasePath.Name, out var settings ) ) + { + continue; + } + + if( !settings.Settings.TryGetValue( group.GroupName, out var setting ) ) + { + setting = 0; + } + + var newSetting = group.SelectionType switch + { + SelectType.Single => setting >= optionIdx ? setting - 1 : setting, + SelectType.Multi => MoveMultiSetting( setting, optionIdx ), + _ => throw new InvalidEnumArgumentException(), + }; + + if( newSetting != setting ) + { + settings.Settings[ group.GroupName ] = newSetting; + manager.SaveCollection( collection ); + if( collection.Cache != null && settings.Enabled ) + { + collection.CalculateEffectiveFileList( manager.BasePath, mod.Resources.MetaManipulations.Count > 0 ); + } + } + } + + return true; + } + } +} \ No newline at end of file diff --git a/Penumbra/Mods/ResourceMod.cs b/Penumbra/Mods/ResourceMod.cs deleted file mode 100644 index 3ee800c8..00000000 --- a/Penumbra/Mods/ResourceMod.cs +++ /dev/null @@ -1,70 +0,0 @@ -using System; -using System.Collections.Generic; -using System.IO; -using System.Linq; -using Dalamud.Plugin; -using Penumbra.Importer; -using Penumbra.Models; -using Penumbra.Util; - -namespace Penumbra.Mods -{ - public class ResourceMod - { - public ResourceMod( ModMeta meta, DirectoryInfo dir ) - { - Meta = meta; - ModBasePath = dir; - } - - public ModMeta Meta { get; set; } - - public DirectoryInfo ModBasePath { get; set; } - - public List< FileInfo > ModFiles { get; } = new(); - public Dictionary< FileInfo, TexToolsMeta > MetaManipulations { get; } = new(); - - public Dictionary< string, List< GamePath > > FileConflicts { get; } = new(); - - - public void RefreshModFiles() - { - FileConflicts.Clear(); - ModFiles.Clear(); - MetaManipulations.Clear(); - // we don't care about any _files_ in the root dir, but any folders should be a game folder/file combo - foreach( var file in ModBasePath.EnumerateDirectories() - .SelectMany( dir => dir.EnumerateFiles( "*.*", SearchOption.AllDirectories ) ) ) - { - if( file.Extension == ".meta" ) - { - try - { - MetaManipulations[ file ] = new TexToolsMeta( File.ReadAllBytes( file.FullName ) ); - } - catch( Exception e ) - { - PluginLog.Error( $"Could not parse meta file {file.FullName}:\n{e}" ); - } - } - - ModFiles.Add( file ); - } - } - - public void AddConflict( string modName, GamePath path ) - { - if( FileConflicts.TryGetValue( modName, out var arr ) ) - { - if( !arr.Contains( path ) ) - { - arr.Add( path ); - } - - return; - } - - FileConflicts[ modName ] = new List< GamePath > { path }; - } - } -} \ No newline at end of file diff --git a/Penumbra/Plugin.cs b/Penumbra/Plugin.cs index ce3ed811..072cbf0d 100644 --- a/Penumbra/Plugin.cs +++ b/Penumbra/Plugin.cs @@ -6,16 +6,17 @@ using EmbedIO.WebApi; using Penumbra.API; using Penumbra.Game; using Penumbra.Hooks; -using Penumbra.MetaData; +using Penumbra.Meta.Files; using Penumbra.Mods; using Penumbra.UI; +using Penumbra.Util; namespace Penumbra { public class Plugin : IDalamudPlugin { public string Name { get; } - public string PluginDebugTitleStr { get; } + public string PluginDebugTitleStr { get; } public Plugin() { @@ -25,34 +26,35 @@ namespace Penumbra private const string CommandName = "/penumbra"; - public DalamudPluginInterface? PluginInterface { get; set; } - public Configuration? Configuration { get; set; } - public ResourceLoader? ResourceLoader { get; set; } - public SettingsInterface? SettingsInterface { get; set; } - public SoundShit? SoundShit { get; set; } + public DalamudPluginInterface PluginInterface { get; set; } = null!; + public Configuration Configuration { get; set; } = null!; + public ResourceLoader ResourceLoader { get; set; } = null!; + public SettingsInterface SettingsInterface { get; set; } = null!; + public MusicManager SoundShit { get; set; } = null!; private WebServer? _webServer; public void Initialize( DalamudPluginInterface pluginInterface ) { PluginInterface = pluginInterface; + Service< DalamudPluginInterface >.Set( PluginInterface ); - Configuration = PluginInterface.GetPluginConfig() as Configuration ?? new Configuration(); - Configuration.Initialize( PluginInterface ); - - SoundShit = new SoundShit( this ); + Configuration = Configuration.Load( PluginInterface ); + SoundShit = new MusicManager( this ); + SoundShit.DisableStreaming(); var gameUtils = Service< GameResourceManagement >.Set( PluginInterface ); - var modManager = Service< ModManager >.Set( this ); Service< MetaDefaults >.Set( PluginInterface ); - modManager.DiscoverMods( Configuration.CurrentCollection ); + var modManager = Service< ModManager >.Set( this ); + + modManager.DiscoverMods(); ResourceLoader = new ResourceLoader( this ); PluginInterface.CommandManager.AddHandler( CommandName, new CommandInfo( OnCommand ) { - HelpMessage = "/penumbra - toggle ui\n/penumbra reload - reload mod file lists & discover any new mods" + HelpMessage = "/penumbra - toggle ui\n/penumbra reload - reload mod file lists & discover any new mods", } ); ResourceLoader.Init(); @@ -77,11 +79,11 @@ namespace Penumbra ShutdownWebServer(); _webServer = new WebServer( o => o - .WithUrlPrefix( prefix ) - .WithMode( HttpListenerMode.EmbedIO ) ) - .WithCors( prefix ) - .WithWebApi( "/api", m => m - .WithController( () => new ModsController( this ) ) ); + .WithUrlPrefix( prefix ) + .WithMode( HttpListenerMode.EmbedIO ) ) + .WithCors( prefix ) + .WithWebApi( "/api", m => m + .WithController( () => new ModsController( this ) ) ); _webServer.StateChanged += ( s, e ) => PluginLog.Information( $"WebServer New State - {e.NewState}" ); @@ -96,14 +98,12 @@ namespace Penumbra public void Dispose() { - // ModManager?.Dispose(); - - PluginInterface!.UiBuilder.OnBuildUi -= SettingsInterface!.Draw; + PluginInterface.UiBuilder.OnBuildUi -= SettingsInterface.Draw; PluginInterface.CommandManager.RemoveHandler( CommandName ); PluginInterface.Dispose(); - ResourceLoader?.Dispose(); + ResourceLoader.Dispose(); ShutdownWebServer(); } @@ -118,8 +118,8 @@ namespace Penumbra case "reload": { Service< ModManager >.Get().DiscoverMods(); - PluginInterface!.Framework.Gui.Chat.Print( - $"Reloaded Penumbra mods. You have {Service< ModManager >.Get()?.Mods?.ModSettings?.Count ?? 0} mods, {Service< ModManager >.Get()?.Mods?.EnabledMods?.Length ?? 0} of which are enabled." + PluginInterface.Framework.Gui.Chat.Print( + $"Reloaded Penumbra mods. You have {Service< ModManager >.Get()?.Mods.Count} mods." ); break; } @@ -127,11 +127,11 @@ namespace Penumbra { if( args.Length > 1 ) { - RefreshActors.RedrawSpecific( PluginInterface!.ClientState.Actors, string.Join( " ", args.Skip( 1 ) ) ); + RefreshActors.RedrawSpecific( PluginInterface.ClientState.Actors, string.Join( " ", args.Skip( 1 ) ) ); } else { - RefreshActors.RedrawAll( PluginInterface!.ClientState.Actors ); + RefreshActors.RedrawAll( PluginInterface.ClientState.Actors ); } break; @@ -141,7 +141,7 @@ namespace Penumbra return; } - SettingsInterface!.FlipVisibility(); + SettingsInterface.FlipVisibility(); } } } \ No newline at end of file diff --git a/Penumbra/Structs/FileMode.cs b/Penumbra/Structs/FileMode.cs index a28d969c..13235521 100644 --- a/Penumbra/Structs/FileMode.cs +++ b/Penumbra/Structs/FileMode.cs @@ -7,6 +7,6 @@ namespace Penumbra.Structs // some shit here, the game does some jump if its < 0xA for other files for some reason but there's no impl, probs debug? LoadIndexResource = 0xA, // load index/index2 - LoadSqPackResource = 0xB + LoadSqPackResource = 0xB, } } \ No newline at end of file diff --git a/Penumbra/Structs/GroupInformation.cs b/Penumbra/Structs/GroupInformation.cs new file mode 100644 index 00000000..0e574d1d --- /dev/null +++ b/Penumbra/Structs/GroupInformation.cs @@ -0,0 +1,99 @@ +using System.Collections.Generic; +using System.ComponentModel; +using Newtonsoft.Json; +using Penumbra.Util; + +namespace Penumbra.Structs +{ + public enum SelectType + { + Single, + Multi, + } + + public struct Option + { + public string OptionName; + public string OptionDesc; + + [JsonProperty( ItemConverterType = typeof( SingleOrArrayConverter< GamePath > ) )] + public Dictionary< RelPath, HashSet< GamePath > > OptionFiles; + + public bool AddFile( RelPath filePath, GamePath gamePath ) + { + if( OptionFiles.TryGetValue( filePath, out var set ) ) + { + return set.Add( gamePath ); + } + + OptionFiles[ filePath ] = new HashSet< GamePath >() { gamePath }; + return true; + } + } + + public struct OptionGroup + { + public string GroupName; + + [JsonConverter( typeof( Newtonsoft.Json.Converters.StringEnumConverter ) )] + public SelectType SelectionType; + + public List< Option > Options; + + private bool ApplySingleGroupFiles( RelPath relPath, int selection, HashSet< GamePath > paths ) + { + if( Options[ selection ].OptionFiles.TryGetValue( relPath, out var groupPaths ) ) + { + paths.UnionWith( groupPaths ); + return true; + } + + for( var i = 0; i < Options.Count; ++i ) + { + if( i == selection ) + { + continue; + } + + if( Options[ i ].OptionFiles.ContainsKey( relPath ) ) + { + return true; + } + } + + return false; + } + + private bool ApplyMultiGroupFiles( RelPath relPath, int selection, HashSet< GamePath > paths ) + { + var doNotAdd = false; + for( var i = 0; i < Options.Count; ++i ) + { + if( ( selection & ( 1 << i ) ) != 0 ) + { + if( Options[ i ].OptionFiles.TryGetValue( relPath, out var groupPaths ) ) + { + paths.UnionWith( groupPaths ); + } + } + else if( Options[ i ].OptionFiles.ContainsKey( relPath ) ) + { + doNotAdd = true; + } + } + + return doNotAdd; + } + + // Adds all game paths from the given option that correspond to the given RelPath to paths, if any exist. + internal bool ApplyGroupFiles( RelPath relPath, int selection, HashSet< GamePath > paths ) + { + return SelectionType switch + { + SelectType.Single => ApplySingleGroupFiles( relPath, selection, paths ), + SelectType.Multi => ApplyMultiGroupFiles( relPath, selection, paths ), + _ => throw new InvalidEnumArgumentException( "Invalid option group type." ), + }; + } + } +} \ No newline at end of file diff --git a/Penumbra/UI/Custom/ImGuiFramedGroup.cs b/Penumbra/UI/Custom/ImGuiFramedGroup.cs index a8b0ee2d..6f0f903f 100644 --- a/Penumbra/UI/Custom/ImGuiFramedGroup.cs +++ b/Penumbra/UI/Custom/ImGuiFramedGroup.cs @@ -3,15 +3,21 @@ using System.Collections.Generic; using System.Numerics; using ImGuiNET; -namespace Penumbra.UI +namespace Penumbra.UI.Custom { public static partial class ImGuiCustom { - public static void BeginFramedGroup( string label ) => BeginFramedGroupInternal( ref label, ZeroVector, false ); - public static void BeginFramedGroup( string label, Vector2 minSize ) => BeginFramedGroupInternal( ref label, minSize, false ); + public static void BeginFramedGroup( string label ) + => BeginFramedGroupInternal( ref label, ZeroVector, false ); - public static bool BeginFramedGroupEdit( ref string label ) => BeginFramedGroupInternal( ref label, ZeroVector, true ); - public static bool BeginFramedGroupEdit( ref string label, Vector2 minSize ) => BeginFramedGroupInternal( ref label, minSize, true ); + public static void BeginFramedGroup( string label, Vector2 minSize ) + => BeginFramedGroupInternal( ref label, minSize, false ); + + public static bool BeginFramedGroupEdit( ref string label ) + => BeginFramedGroupInternal( ref label, ZeroVector, true ); + + public static bool BeginFramedGroupEdit( ref string label, Vector2 minSize ) + => BeginFramedGroupInternal( ref label, minSize, true ); private static bool BeginFramedGroupInternal( ref string label, Vector2 minSize, bool edit ) { diff --git a/Penumbra/UI/Custom/ImGuiRenameableCombo.cs b/Penumbra/UI/Custom/ImGuiRenameableCombo.cs index c26c8a1a..2e81f125 100644 --- a/Penumbra/UI/Custom/ImGuiRenameableCombo.cs +++ b/Penumbra/UI/Custom/ImGuiRenameableCombo.cs @@ -1,6 +1,6 @@ using ImGuiNET; -namespace Penumbra.UI +namespace Penumbra.UI.Custom { public static partial class ImGuiCustom { diff --git a/Penumbra/UI/Custom/ImGuiResizingTextInput.cs b/Penumbra/UI/Custom/ImGuiResizingTextInput.cs index aaabdf5c..f7ef2c53 100644 --- a/Penumbra/UI/Custom/ImGuiResizingTextInput.cs +++ b/Penumbra/UI/Custom/ImGuiResizingTextInput.cs @@ -1,7 +1,7 @@ using System.Collections.Generic; using ImGuiNET; -namespace Penumbra.UI +namespace Penumbra.UI.Custom { public static partial class ImGuiCustom { @@ -16,8 +16,8 @@ namespace Penumbra.UI return false; } - public static bool ResizingTextInput( string label, ref string input, uint maxLength ) => - ResizingTextInputIntern( label, ref input, maxLength ).Item1; + public static bool ResizingTextInput( string label, ref string input, uint maxLength ) + => ResizingTextInputIntern( label, ref input, maxLength ).Item1; public static bool ResizingTextInput( ref string input, uint maxLength ) { diff --git a/Penumbra/UI/Custom/ImGuiUtil.cs b/Penumbra/UI/Custom/ImGuiUtil.cs index 0403dd64..4020199c 100644 --- a/Penumbra/UI/Custom/ImGuiUtil.cs +++ b/Penumbra/UI/Custom/ImGuiUtil.cs @@ -1,7 +1,7 @@ using System.Windows.Forms; using ImGuiNET; -namespace Penumbra.UI +namespace Penumbra.UI.Custom { public static partial class ImGuiCustom { diff --git a/Penumbra/UI/LaunchButton.cs b/Penumbra/UI/LaunchButton.cs index 55883cce..12a40e35 100644 --- a/Penumbra/UI/LaunchButton.cs +++ b/Penumbra/UI/LaunchButton.cs @@ -18,13 +18,13 @@ namespace Penumbra.UI private static readonly Vector2 WindowPosOffset = new( Padding + Width, Padding + Height ); private const ImGuiWindowFlags ButtonFlags = - ImGuiWindowFlags.AlwaysAutoResize - | ImGuiWindowFlags.NoBackground - | ImGuiWindowFlags.NoDecoration - | ImGuiWindowFlags.NoMove - | ImGuiWindowFlags.NoScrollbar - | ImGuiWindowFlags.NoResize - | ImGuiWindowFlags.NoSavedSettings; + ImGuiWindowFlags.AlwaysAutoResize + | ImGuiWindowFlags.NoBackground + | ImGuiWindowFlags.NoDecoration + | ImGuiWindowFlags.NoMove + | ImGuiWindowFlags.NoScrollbar + | ImGuiWindowFlags.NoResize + | ImGuiWindowFlags.NoSavedSettings; private readonly SettingsInterface _base; private readonly Dalamud.Game.ClientState.Condition _condition; @@ -43,8 +43,8 @@ namespace Penumbra.UI } var ss = ImGui.GetMainViewport().Size + ImGui.GetMainViewport().Pos; - ImGui.SetNextWindowViewport(ImGui.GetMainViewport().ID); - + ImGui.SetNextWindowViewport( ImGui.GetMainViewport().ID ); + ImGui.SetNextWindowPos( ss - WindowPosOffset, ImGuiCond.Always ); if( !ImGui.Begin( MenuButtonsName, ButtonFlags ) ) @@ -61,4 +61,4 @@ namespace Penumbra.UI } } } -} +} \ No newline at end of file diff --git a/Penumbra/UI/MenuBar.cs b/Penumbra/UI/MenuBar.cs index 3f616c74..4fdb464f 100644 --- a/Penumbra/UI/MenuBar.cs +++ b/Penumbra/UI/MenuBar.cs @@ -19,7 +19,9 @@ namespace Penumbra.UI #endif private readonly SettingsInterface _base; - public MenuBar( SettingsInterface ui ) => _base = ui; + + public MenuBar( SettingsInterface ui ) + => _base = ui; public void Draw() { diff --git a/Penumbra/UI/MenuTabs/TabCollections.cs b/Penumbra/UI/MenuTabs/TabCollections.cs new file mode 100644 index 00000000..7a124ddc --- /dev/null +++ b/Penumbra/UI/MenuTabs/TabCollections.cs @@ -0,0 +1,120 @@ +using System.Collections.Generic; +using System.Linq; +using System.Numerics; +using Dalamud.Plugin; +using ImGuiNET; +using Penumbra.Hooks; +using Penumbra.Mod; +using Penumbra.Mods; +using Penumbra.Util; + +namespace Penumbra.UI +{ + public partial class SettingsInterface + { + private class TabCollections + { + private readonly Selector _selector; + private readonly ModManager _manager; + private string[] _collectionNames = null!; + private int _currentIndex = 0; + private string _newCollectionName = string.Empty; + + private void UpdateNames() + => _collectionNames = _manager.Collections.Values.Select( c => c.Name ).ToArray(); + + private void UpdateIndex() + { + _currentIndex = _collectionNames.IndexOf( c => c == _manager.CurrentCollection.Name ); + if( _currentIndex < 0 ) + { + PluginLog.Error( $"Current Collection {_manager.CurrentCollection.Name} is not found in collections." ); + _currentIndex = 0; + } + } + + public TabCollections( Selector selector ) + { + _selector = selector; + _manager = Service< ModManager >.Get(); + UpdateNames(); + UpdateIndex(); + } + + + private void CreateNewCollection( Dictionary< string, ModSettings > settings ) + { + _manager.AddCollection( _newCollectionName, settings ); + _manager.SetCurrentCollection( _newCollectionName ); + _newCollectionName = string.Empty; + UpdateNames(); + UpdateIndex(); + } + + private void DrawNewCollectionInput() + { + ImGui.InputTextWithHint( "##New Collection", "New Collection", ref _newCollectionName, 64 ); + + var changedStyle = false; + if( _newCollectionName.Length == 0 ) + { + changedStyle = true; + ImGui.PushStyleVar( ImGuiStyleVar.Alpha, 0.5f ); + } + + + if( ImGui.Button( "Create New Empty Collection" ) && _newCollectionName.Length > 0 ) + { + CreateNewCollection( new Dictionary< string, ModSettings >() ); + } + + ImGui.SameLine(); + if( ImGui.Button( "Duplicate Current Collection" ) && _newCollectionName.Length > 0 ) + { + CreateNewCollection( _manager.CurrentCollection.Settings ); + } + + if( changedStyle ) + { + ImGui.PopStyleVar(); + } + + if( _manager.Collections.Count > 1 ) + { + ImGui.SameLine(); + if( ImGui.Button( "Delete Current Collection" ) ) + { + _manager.RemoveCollection( _manager.CurrentCollection.Name ); + UpdateNames(); + UpdateIndex(); + } + } + } + + public void Draw() + { + if( !ImGui.BeginTabItem( "Collections" ) ) + { + return; + } + + var index = _currentIndex; + if( ImGui.Combo( "Current Collection", ref index, _collectionNames, _collectionNames.Length ) ) + { + if( index != _currentIndex && _manager.SetCurrentCollection( _collectionNames[ index ] ) ) + { + _currentIndex = index; + _selector.ReloadSelection(); + var resourceManager = Service< GameResourceManagement >.Get(); + resourceManager.ReloadPlayerResources(); + } + } + + ImGui.Dummy( new Vector2( 0, 5 ) ); + DrawNewCollectionInput(); + + ImGui.EndTabItem(); + } + } + } +} \ No newline at end of file diff --git a/Penumbra/UI/MenuTabs/TabEffective.cs b/Penumbra/UI/MenuTabs/TabEffective.cs index 8df660f5..fdfe0424 100644 --- a/Penumbra/UI/MenuTabs/TabEffective.cs +++ b/Penumbra/UI/MenuTabs/TabEffective.cs @@ -1,6 +1,7 @@ using System.IO; using Dalamud.Interface; using ImGuiNET; +using Penumbra.Meta; using Penumbra.Mods; using Penumbra.Util; @@ -10,24 +11,40 @@ namespace Penumbra.UI { private class TabEffective { - private const string LabelTab = "Effective File List"; - private const float TextSizePadding = 5f; + private const string LabelTab = "Effective Changes"; + private static readonly string LongArrowLeft = $"{( char )FontAwesomeIcon.LongArrowAltLeft}"; + private readonly ModManager _modManager; + + public TabEffective() + => _modManager = Service< ModManager >.Get(); - private ModManager _mods - => Service< ModManager >.Get(); private static void DrawFileLine( FileInfo file, GamePath path ) { ImGui.TableNextColumn(); - ImGuiCustom.CopyOnClickSelectable( path ); + Custom.ImGuiCustom.CopyOnClickSelectable( path ); ImGui.TableNextColumn(); ImGui.PushFont( UiBuilder.IconFont ); - ImGui.TextUnformatted( $"{( char )FontAwesomeIcon.LongArrowAltLeft}" ); + ImGui.TextUnformatted( LongArrowLeft ); ImGui.PopFont(); ImGui.TableNextColumn(); - ImGuiCustom.CopyOnClickSelectable( file.FullName ); + Custom.ImGuiCustom.CopyOnClickSelectable( file.FullName ); + } + + private static void DrawManipulationLine( MetaManipulation manip, Mod.Mod mod ) + { + ImGui.TableNextColumn(); + ImGui.Selectable( manip.IdentifierString() ); + + ImGui.TableNextColumn(); + ImGui.PushFont( UiBuilder.IconFont ); + ImGui.TextUnformatted( LongArrowLeft ); + ImGui.PopFont(); + + ImGui.TableNextColumn(); + ImGui.Selectable( mod.Data.Meta.Name ); } public void Draw() @@ -40,14 +57,21 @@ namespace Penumbra.UI const ImGuiTableFlags flags = ImGuiTableFlags.SizingFixedFit | ImGuiTableFlags.RowBg | ImGuiTableFlags.ScrollX; - if( ImGui.BeginTable( "##effective_files", 3, flags, AutoFillSize ) ) + if( ImGui.BeginTable( "##effective_changes", 3, flags, AutoFillSize ) ) { - foreach ( var file in _mods.ResolvedFiles ) + var currentCollection = _modManager.CurrentCollection.Cache!; + foreach( var file in currentCollection.ResolvedFiles ) { DrawFileLine( file.Value, file.Key ); ImGui.TableNextRow(); } + foreach( var (manip, mod) in currentCollection.MetaManipulations.Manipulations ) + { + DrawManipulationLine( manip, mod ); + ImGui.TableNextRow(); + } + ImGui.EndTable(); } diff --git a/Penumbra/UI/MenuTabs/TabImport.cs b/Penumbra/UI/MenuTabs/TabImport.cs index 0600431f..79259731 100644 --- a/Penumbra/UI/MenuTabs/TabImport.cs +++ b/Penumbra/UI/MenuTabs/TabImport.cs @@ -6,6 +6,7 @@ using System.Windows.Forms; using Dalamud.Plugin; using ImGuiNET; using Penumbra.Importer; +using Penumbra.Util; namespace Penumbra.UI { @@ -25,54 +26,63 @@ namespace Penumbra.UI private static readonly Vector2 ImportBarSize = new( -1, 0 ); - private bool _isImportRunning = false; - private bool _hasError = false; + private bool _isImportRunning; + private bool _hasError; private TexToolsImport? _texToolsImport; private readonly SettingsInterface _base; - public TabImport( SettingsInterface ui ) => _base = ui; + public TabImport( SettingsInterface ui ) + => _base = ui; - public bool IsImporting() => _isImportRunning; + public bool IsImporting() + => _isImportRunning; private void RunImportTask() { _isImportRunning = true; Task.Run( async () => { - var picker = new OpenFileDialog + try { - Multiselect = true, - Filter = FileTypeFilter, - CheckFileExists = true, - Title = LabelFileDialog - }; - - var result = await picker.ShowDialogAsync(); - - if( result == DialogResult.OK ) - { - _hasError = false; - - foreach( var fileName in picker.FileNames ) + var picker = new OpenFileDialog { - PluginLog.Log( $"-> {fileName} START" ); + Multiselect = true, + Filter = FileTypeFilter, + CheckFileExists = true, + Title = LabelFileDialog, + }; - try - { - _texToolsImport = new TexToolsImport( new DirectoryInfo( _base._plugin!.Configuration!.CurrentCollection ) ); - _texToolsImport.ImportModPack( new FileInfo( fileName ) ); + var result = await picker.ShowDialogAsync(); - PluginLog.Log( $"-> {fileName} OK!" ); - } - catch( Exception ex ) + if( result == DialogResult.OK ) + { + _hasError = false; + + foreach( var fileName in picker.FileNames ) { - PluginLog.LogError( ex, "Failed to import modpack at {0}", fileName ); - _hasError = true; + PluginLog.Log( $"-> {fileName} START" ); + + try + { + _texToolsImport = new TexToolsImport( new DirectoryInfo( _base._plugin!.Configuration!.ModDirectory ) ); + _texToolsImport.ImportModPack( new FileInfo( fileName ) ); + + PluginLog.Log( $"-> {fileName} OK!" ); + } + catch( Exception ex ) + { + PluginLog.LogError( ex, "Failed to import modpack at {0}", fileName ); + _hasError = true; + } } + + _texToolsImport = null; + _base.ReloadMods(); } - - _texToolsImport = null; - _base.ReloadMods(); + } + catch( Exception e ) + { + PluginLog.Error( $"Error opening file picker dialogue:\n{e}" ); } _isImportRunning = false; @@ -98,8 +108,7 @@ namespace Penumbra.UI switch( _texToolsImport.State ) { - case ImporterState.None: - break; + case ImporterState.None: break; case ImporterState.WritingPackToDisk: ImGui.Text( TooltipModpack1 ); break; @@ -111,10 +120,8 @@ namespace Penumbra.UI ImGui.ProgressBar( _texToolsImport.Progress, ImportBarSize, str ); break; } - case ImporterState.Done: - break; - default: - throw new ArgumentOutOfRangeException(); + case ImporterState.Done: break; + default: throw new ArgumentOutOfRangeException(); } } diff --git a/Penumbra/UI/MenuTabs/TabInstalled/TabInstalled.cs b/Penumbra/UI/MenuTabs/TabInstalled/TabInstalled.cs index 09ac8d10..dbc7e895 100644 --- a/Penumbra/UI/MenuTabs/TabInstalled/TabInstalled.cs +++ b/Penumbra/UI/MenuTabs/TabInstalled/TabInstalled.cs @@ -1,5 +1,6 @@ using ImGuiNET; using Penumbra.Mods; +using Penumbra.Util; namespace Penumbra.UI { @@ -9,21 +10,21 @@ namespace Penumbra.UI { private const string LabelTab = "Installed Mods"; - private readonly SettingsInterface _base; - public readonly Selector Selector; - public readonly ModPanel ModPanel; + private readonly ModManager _modManager; + public readonly Selector Selector; + public readonly ModPanel ModPanel; public TabInstalled( SettingsInterface ui ) { - _base = ui; - Selector = new Selector( _base ); - ModPanel = new ModPanel( _base, Selector ); + Selector = new Selector( ui ); + ModPanel = new ModPanel( ui, Selector ); + _modManager = Service< ModManager >.Get(); } private static void DrawNoModsAvailable() { ImGui.Text( "You don't have any mods :(" ); - ImGuiCustom.VerticalDistance( 20f ); + Custom.ImGuiCustom.VerticalDistance( 20f ); ImGui.Text( "You'll need to install them first by creating a folder close to the root of your drive (preferably an SSD)." ); ImGui.Text( "For example: D:/ffxiv/mods/" ); ImGui.Text( "And pasting that path into the settings tab and clicking the 'Rediscover Mods' button." ); @@ -38,7 +39,7 @@ namespace Penumbra.UI return; } - if( Service< ModManager >.Get().Mods != null ) + if( _modManager.Mods.Count > 0 ) { Selector.Draw(); ImGui.SameLine(); diff --git a/Penumbra/UI/MenuTabs/TabInstalled/TabInstalledDetails.cs b/Penumbra/UI/MenuTabs/TabInstalled/TabInstalledDetails.cs index 6c1aa1a3..57630e07 100644 --- a/Penumbra/UI/MenuTabs/TabInstalled/TabInstalledDetails.cs +++ b/Penumbra/UI/MenuTabs/TabInstalled/TabInstalledDetails.cs @@ -1,10 +1,13 @@ using System.Collections.Generic; -using System.Linq; using System.IO; +using System.Linq; using Dalamud.Interface; using ImGuiNET; -using Penumbra.Models; +using Penumbra.Game.Enums; +using Penumbra.Meta; +using Penumbra.Mod; using Penumbra.Mods; +using Penumbra.Structs; using Penumbra.Util; namespace Penumbra.UI @@ -35,7 +38,7 @@ namespace Penumbra.UI private const string LabelChangedItemsHeader = "##changedItems"; private const string LabelChangedItemIdx = "##citem_"; private const string LabelChangedItemNew = "##citem_new"; - private const string LabelConflictsTab = "File Conflicts"; + private const string LabelConflictsTab = "Mod Conflicts"; private const string LabelConflictsHeader = "##conflicts"; private const string LabelFileSwapTab = "File Swaps"; private const string LabelFileSwapHeader = "##fileSwaps"; @@ -49,7 +52,6 @@ namespace Penumbra.UI "Green files replace their standard game path counterpart (not in any option) or are in all options of a Single-Select option.\n" + "Yellow files are restricted to some options."; - private const float TextSizePadding = 5f; private const float OptionSelectionWidth = 140f; private const float CheckMarkSize = 50f; private const uint ColorGreen = 0xFF00C800; @@ -68,11 +70,12 @@ namespace Penumbra.UI private readonly Selector _selector; private readonly SettingsInterface _base; + private readonly ModManager _modManager; private void SelectGroup( int idx ) { // Not using the properties here because we need it to be not null forgiving in this case. - var numGroups = _selector.Mod?.Mod.Meta.Groups.Count ?? 0; + var numGroups = _selector.Mod?.Data.Meta.Groups.Count ?? 0; _selectedGroupIndex = idx; if( _selectedGroupIndex >= numGroups ) { @@ -126,20 +129,19 @@ namespace Penumbra.UI _base = ui; _selector = s; ResetState(); + _modManager = Service< ModManager >.Get(); } // This is only drawn when we have a mod selected, so we can forgive nulls. - private ModInfo Mod + private Mod.Mod Mod => _selector.Mod!; private ModMeta Meta - => Mod.Mod.Meta; + => Mod.Data.Meta; private void Save() { - var modManager = Service< ModManager >.Get(); - modManager.Mods?.Save(); - modManager.CalculateEffectiveFileList(); + _modManager.CurrentCollection.Save( _base._plugin.PluginInterface! ); } private void DrawAboutTab() @@ -161,7 +163,8 @@ namespace Penumbra.UI if( _editMode ) { - if( ImGui.InputTextMultiline( LabelDescEdit, ref desc, 1 << 16, AutoFillSize, flags ) ) + if( ImGui.InputTextMultiline( LabelDescEdit, ref desc, 1 << 16, + AutoFillSize, flags ) ) { Meta.Description = desc; _selector.SaveCurrentMod(); @@ -194,6 +197,7 @@ namespace Penumbra.UI if( ImGui.BeginTabItem( LabelChangedItemsTab ) ) { ImGui.SetNextItemWidth( -1 ); + var changedItems = false; if( ImGui.BeginListBox( LabelChangedItemsHeader, AutoFillSize ) ) { _changedItemsList ??= Meta.ChangedItems @@ -205,6 +209,7 @@ namespace Penumbra.UI if( ImGui.InputText( _changedItemsList[ i ].label, ref _changedItemsList[ i ].name, 128, flags ) ) { Meta.ChangedItems.RemoveOrChange( _changedItemsList[ i ].name, i ); + changedItems = true; _selector.SaveCurrentMod(); } } @@ -217,10 +222,16 @@ namespace Penumbra.UI && newItem.Length > 0 ) { Meta.ChangedItems.Add( newItem ); + changedItems = true; _selector.SaveCurrentMod(); } } + if( changedItems ) + { + _changedItemsList = null; + } + ImGui.EndListBox(); } @@ -234,7 +245,7 @@ namespace Penumbra.UI private void DrawConflictTab() { - if( !Mod.Mod.FileConflicts.Any() || !ImGui.BeginTabItem( LabelConflictsTab ) ) + if( !Mod.Cache.Conflicts.Any() || !ImGui.BeginTabItem( LabelConflictsTab ) ) { return; } @@ -242,20 +253,28 @@ namespace Penumbra.UI ImGui.SetNextItemWidth( -1 ); if( ImGui.BeginListBox( LabelConflictsHeader, AutoFillSize ) ) { - foreach( var kv in Mod.Mod.FileConflicts ) + foreach( var kv in Mod.Cache.Conflicts ) { var mod = kv.Key; - if( ImGui.Selectable( mod ) ) + if( ImGui.Selectable( mod.Data.Meta.Name ) ) { - _selector.SelectModByName( mod ); + _selector.SelectModByName( mod.Data.Meta.Name ); } + ImGui.SameLine(); + ImGui.Text( $"(Priority {mod.Settings.Priority})" ); + ImGui.Indent( 15 ); - foreach( var file in kv.Value ) + foreach( var file in kv.Value.Files ) { ImGui.Selectable( file ); } + foreach( var manip in kv.Value.Manipulations ) + { + ImGui.Text( manip.IdentifierString() ); + } + ImGui.Unindent( 15 ); } @@ -286,7 +305,7 @@ namespace Penumbra.UI foreach( var file in Meta.FileSwaps ) { ImGui.TableNextColumn(); - ImGuiCustom.CopyOnClickSelectable( file.Key ); + Custom.ImGuiCustom.CopyOnClickSelectable( file.Key ); ImGui.TableNextColumn(); ImGui.PushFont( UiBuilder.IconFont ); @@ -294,7 +313,7 @@ namespace Penumbra.UI ImGui.PopFont(); ImGui.TableNextColumn(); - ImGuiCustom.CopyOnClickSelectable( file.Value ); + Custom.ImGuiCustom.CopyOnClickSelectable( file.Value ); ImGui.TableNextRow(); } @@ -312,16 +331,15 @@ namespace Penumbra.UI return; } - var len = Mod.Mod.ModBasePath.FullName.Length; - _fullFilenameList = Mod.Mod.ModFiles - .Select( F => ( F, false, ColorGreen, new RelPath( F, Mod.Mod.ModBasePath ) ) ).ToArray(); + _fullFilenameList = Mod.Data.Resources.ModFiles + .Select( f => ( f, false, ColorGreen, new RelPath( f, Mod.Data.BasePath ) ) ).ToArray(); if( Meta.Groups.Count == 0 ) { return; } - for( var i = 0; i < Mod.Mod.ModFiles.Count; ++i ) + for( var i = 0; i < Mod.Data.Resources.ModFiles.Count; ++i ) { foreach( var group in Meta.Groups.Values ) { @@ -362,10 +380,10 @@ namespace Penumbra.UI if( ImGui.BeginListBox( LabelFileListHeader, AutoFillSize ) ) { UpdateFilenameList(); - foreach( var file in _fullFilenameList! ) + foreach( var (name, _, color, _) in _fullFilenameList! ) { - ImGui.PushStyleColor( ImGuiCol.Text, file.color ); - ImGui.Selectable( file.name.FullName ); + ImGui.PushStyleColor( ImGuiCol.Text, color ); + ImGui.Selectable( name.FullName ); ImGui.PopStyleColor(); } @@ -379,10 +397,11 @@ namespace Penumbra.UI ImGui.EndTabItem(); } - private int HandleDefaultString( GamePath[] gamePaths, out int removeFolders ) + private static int HandleDefaultString( GamePath[] gamePaths, out int removeFolders ) { removeFolders = 0; - var defaultIndex = gamePaths.IndexOf( p => ( ( string )p ).StartsWith( TextDefaultGamePath ) ); + var defaultIndex = + gamePaths.IndexOf( p => ( ( string )p ).StartsWith( TextDefaultGamePath ) ); if( defaultIndex < 0 ) { return defaultIndex; @@ -412,7 +431,7 @@ namespace Penumbra.UI var option = ( Option )_selectedOption; - var gamePaths = _currentGamePaths.Split( ';' ).Select( P => new GamePath( P ) ).ToArray(); + var gamePaths = _currentGamePaths.Split( ';' ).Select( p => new GamePath( p ) ).ToArray(); if( gamePaths.Length == 0 || ( ( string )gamePaths[ 0 ] ).Length == 0 ) { return; @@ -420,7 +439,7 @@ namespace Penumbra.UI var defaultIndex = HandleDefaultString( gamePaths, out var removeFolders ); var changed = false; - for( var i = 0; i < Mod.Mod.ModFiles.Count; ++i ) + for( var i = 0; i < Mod.Data.Resources.ModFiles.Count; ++i ) { if( !_fullFilenameList![ i ].selected ) { @@ -435,7 +454,7 @@ namespace Penumbra.UI if( remove && option.OptionFiles.TryGetValue( relName, out var setPaths ) ) { - if( setPaths.RemoveWhere( P => gamePaths.Contains( P ) ) > 0 ) + if( setPaths.RemoveWhere( p => gamePaths.Contains( p ) ) > 0 ) { changed = true; } @@ -477,7 +496,8 @@ namespace Penumbra.UI private void DrawGamePathInput() { ImGui.SetNextItemWidth( -1 ); - ImGui.InputTextWithHint( LabelGamePathsEditBox, "Hover for help...", ref _currentGamePaths, 128 ); + ImGui.InputTextWithHint( LabelGamePathsEditBox, "Hover for help...", ref _currentGamePaths, + 128 ); if( ImGui.IsItemHovered() ) { ImGui.SetTooltip( TooltipGamePathsEdit ); @@ -580,7 +600,7 @@ namespace Penumbra.UI var oldEnabled = enabled; if( ImGui.Checkbox( label, ref enabled ) && oldEnabled != enabled ) { - Mod.Settings[ group.GroupName ] ^= 1 << idx; + Mod.Settings.Settings[ group.GroupName ] ^= 1 << idx; Save(); } } @@ -592,14 +612,14 @@ namespace Penumbra.UI return; } - ImGuiCustom.BeginFramedGroup( group.GroupName ); + Custom.ImGuiCustom.BeginFramedGroup( group.GroupName ); for( var i = 0; i < group.Options.Count; ++i ) { - DrawMultiSelectorCheckBox( group, i, Mod.Settings[ group.GroupName ], + DrawMultiSelectorCheckBox( group, i, Mod.Settings.Settings[ group.GroupName ], $"{group.Options[ i ].OptionName}##{group.GroupName}" ); } - ImGuiCustom.EndFramedGroup(); + Custom.ImGuiCustom.EndFramedGroup(); } private void DrawSingleSelector( OptionGroup group ) @@ -609,11 +629,12 @@ namespace Penumbra.UI return; } - var code = Mod.Settings[ group.GroupName ]; + var code = Mod.Settings.Settings[ group.GroupName ]; if( ImGui.Combo( group.GroupName, ref code - , group.Options.Select( x => x.OptionName ).ToArray(), group.Options.Count ) ) + , group.Options.Select( x => x.OptionName ).ToArray(), group.Options.Count ) + && code != Mod.Settings.Settings[ group.GroupName ] ) { - Mod.Settings[ group.GroupName ] = code; + Mod.Settings.Settings[ group.GroupName ] = code; Save(); } } @@ -633,7 +654,7 @@ namespace Penumbra.UI private void DrawConfigurationTab() { - if( !_editMode && !Meta.HasGroupWithConfig ) + if( !_editMode && !Meta.HasGroupsWithConfig ) { return; } @@ -653,6 +674,178 @@ namespace Penumbra.UI } } + private static void DrawManipulationRow( MetaManipulation manip ) + { + ImGui.TableNextColumn(); + ImGui.Text( manip.Type.ToString() ); + ImGui.TableNextColumn(); + + switch( manip.Type ) + { + case MetaType.Eqp: + { + ImGui.Text( manip.EqpIdentifier.Slot.IsAccessory() + ? ObjectType.Accessory.ToString() + : ObjectType.Equipment.ToString() ); + ImGui.TableNextColumn(); + ImGui.Text( manip.EqpIdentifier.SetId.ToString() ); + ImGui.TableNextColumn(); + ImGui.Text( manip.EqpIdentifier.Slot.ToString() ); + break; + } + case MetaType.Gmp: + { + ImGui.Text( ObjectType.Equipment.ToString() ); + ImGui.TableNextColumn(); + ImGui.Text( manip.GmpIdentifier.SetId.ToString() ); + ImGui.TableNextColumn(); + ImGui.Text( EquipSlot.Head.ToString() ); + break; + } + case MetaType.Eqdp: + { + ImGui.Text( manip.EqpIdentifier.Slot.IsAccessory() + ? ObjectType.Accessory.ToString() + : ObjectType.Equipment.ToString() ); + ImGui.TableNextColumn(); + ImGui.Text( manip.EqdpIdentifier.SetId.ToString() ); + ImGui.TableNextColumn(); + ImGui.Text( manip.EqpIdentifier.Slot.ToString() ); + ImGui.TableNextColumn(); + var (gender, race) = manip.EqdpIdentifier.GenderRace.Split(); + ImGui.Text( race.ToString() ); + ImGui.TableNextColumn(); + ImGui.Text( gender.ToString() ); + break; + } + case MetaType.Est: + { + ImGui.Text( manip.EstIdentifier.ObjectType.ToString() ); + ImGui.TableNextColumn(); + ImGui.Text( manip.EstIdentifier.PrimaryId.ToString() ); + ImGui.TableNextColumn(); + ImGui.Text( manip.EstIdentifier.ObjectType == ObjectType.Equipment + ? manip.EstIdentifier.EquipSlot.ToString() + : manip.EstIdentifier.BodySlot.ToString() ); + ImGui.TableNextColumn(); + var (gender, race) = manip.EstIdentifier.GenderRace.Split(); + ImGui.Text( race.ToString() ); + ImGui.TableNextColumn(); + ImGui.Text( gender.ToString() ); + break; + } + case MetaType.Imc: + { + ImGui.Text( manip.ImcIdentifier.ObjectType.ToString() ); + ImGui.TableNextColumn(); + ImGui.Text( manip.ImcIdentifier.PrimaryId.ToString() ); + ImGui.TableNextColumn(); + if( manip.ImcIdentifier.ObjectType == ObjectType.Accessory + || manip.ImcIdentifier.ObjectType == ObjectType.Equipment ) + { + ImGui.Text( manip.ImcIdentifier.ObjectType == ObjectType.Equipment + || manip.ImcIdentifier.ObjectType == ObjectType.Accessory + ? manip.ImcIdentifier.EquipSlot.ToString() + : manip.ImcIdentifier.BodySlot.ToString() ); + } + + ImGui.TableNextColumn(); + ImGui.TableNextColumn(); + ImGui.TableNextColumn(); + if( manip.ImcIdentifier.ObjectType != ObjectType.Equipment + && manip.ImcIdentifier.ObjectType != ObjectType.Accessory ) + { + ImGui.Text( manip.ImcIdentifier.SecondaryId.ToString() ); + } + + ImGui.TableNextColumn(); + ImGui.Text( manip.ImcIdentifier.Variant.ToString() ); + break; + } + } + + ImGui.TableSetColumnIndex( 9 ); + ImGui.Text( manip.Value.ToString() ); + ImGui.TableNextRow(); + } + + private static void DrawMetaManipulationsTable( string label, List< MetaManipulation > list ) + { + if( list.Count == 0 + || !ImGui.BeginTable( label, 10, + ImGuiTableFlags.BordersInner | ImGuiTableFlags.RowBg | ImGuiTableFlags.SizingFixedFit ) ) + { + return; + } + + ImGui.TableNextColumn(); + ImGui.TableHeader( $"Type##{label}" ); + ImGui.TableNextColumn(); + ImGui.TableHeader( $"Object Type##{label}" ); + ImGui.TableNextColumn(); + ImGui.TableHeader( $"Set##{label}" ); + ImGui.TableNextColumn(); + ImGui.TableHeader( $"Slot##{label}" ); + ImGui.TableNextColumn(); + ImGui.TableHeader( $"Race##{label}" ); + ImGui.TableNextColumn(); + ImGui.TableHeader( $"Gender##{label}" ); + ImGui.TableNextColumn(); + ImGui.TableHeader( $"Secondary ID##{label}" ); + ImGui.TableNextColumn(); + ImGui.TableHeader( $"Variant##{label}" ); + ImGui.TableNextColumn(); + ImGui.TableNextColumn(); + ImGui.TableHeader( $"Value##{label}" ); + ImGui.TableNextRow(); + foreach( var manip in list ) + { + DrawManipulationRow( manip ); + } + + ImGui.EndTable(); + } + + private void DrawMetaManipulationsTab() + { + if( Mod.Data.Resources.MetaManipulations.Count == 0 ) + { + return; + } + + if( !ImGui.BeginTabItem( "Meta Manipulations" ) ) + { + return; + } + + if( ImGui.BeginListBox( "##MetaManipulations", AutoFillSize ) ) + { + var manips = Mod.Data.Resources.MetaManipulations; + if( manips.DefaultData.Count > 0 ) + { + if( ImGui.CollapsingHeader( "Default" ) ) + { + DrawMetaManipulationsTable( "##DefaultManips", manips.DefaultData ); + } + } + + foreach( var group in manips.GroupData ) + { + foreach( var option in @group.Value ) + { + if( ImGui.CollapsingHeader( $"{@group.Key} - {option.Key}" ) ) + { + DrawMetaManipulationsTable( $"##{@group.Key}{option.Key}manips", option.Value ); + } + } + } + + ImGui.EndListBox(); + } + + ImGui.EndTabItem(); + } + public void Draw( bool editMode ) { _editMode = editMode; @@ -660,6 +853,7 @@ namespace Penumbra.UI DrawAboutTab(); DrawChangedItemsTab(); + DrawConfigurationTab(); if( _editMode ) { @@ -671,8 +865,8 @@ namespace Penumbra.UI } DrawFileSwapTab(); + DrawMetaManipulationsTab(); DrawConflictTab(); - ImGui.EndTabBar(); } } diff --git a/Penumbra/UI/MenuTabs/TabInstalled/TabInstalledDetailsEdit.cs b/Penumbra/UI/MenuTabs/TabInstalled/TabInstalledDetailsEdit.cs index a0f0f84f..18e25c10 100644 --- a/Penumbra/UI/MenuTabs/TabInstalled/TabInstalledDetailsEdit.cs +++ b/Penumbra/UI/MenuTabs/TabInstalled/TabInstalledDetailsEdit.cs @@ -3,7 +3,8 @@ using System.Linq; using System.Numerics; using Dalamud.Interface; using ImGuiNET; -using Penumbra.Models; +using Penumbra.Mods; +using Penumbra.Structs; using Penumbra.Util; namespace Penumbra.UI @@ -44,7 +45,7 @@ namespace Penumbra.UI } if( ImGui.Combo( LabelGroupSelect, ref _selectedGroupIndex - , Meta.Groups.Values.Select( G => G.GroupName ).ToArray() + , Meta.Groups.Values.Select( g => g.GroupName ).ToArray() , Meta.Groups.Count ) ) { SelectGroup(); @@ -65,7 +66,7 @@ namespace Penumbra.UI } var group = ( OptionGroup )_selectedGroup!; - if( ImGui.Combo( LabelOptionSelect, ref _selectedOptionIndex, group.Options.Select( O => O.OptionName ).ToArray(), + if( ImGui.Combo( LabelOptionSelect, ref _selectedOptionIndex, group.Options.Select( o => o.OptionName ).ToArray(), group.Options.Count ) ) { SelectOption(); @@ -87,7 +88,7 @@ namespace Penumbra.UI ImGui.SetNextItemWidth( -1 ); if( ImGui.BeginListBox( LabelFileListHeader, AutoFillSize - new Vector2( 0, 1.5f * ImGui.GetTextLineHeight() ) ) ) { - for( var i = 0; i < Mod!.Mod.ModFiles.Count; ++i ) + for( var i = 0; i < Mod!.Data.Resources.ModFiles.Count; ++i ) { DrawFileAndGamePaths( i ); } @@ -104,31 +105,13 @@ namespace Penumbra.UI } } - private bool DrawMultiSelectorEditBegin( OptionGroup group ) + private void DrawMultiSelectorEditBegin( OptionGroup group ) { var groupName = group.GroupName; - if( ImGuiCustom.BeginFramedGroupEdit( ref groupName ) - && groupName != group.GroupName - && !Meta!.Groups.ContainsKey( groupName ) ) + if( Custom.ImGuiCustom.BeginFramedGroupEdit( ref groupName ) ) { - var oldConf = Mod!.Settings[ group.GroupName ]; - Meta.Groups.Remove( group.GroupName ); - Mod.FixSpecificSetting( group.GroupName ); - if( groupName.Length > 0 ) - { - Meta.Groups[ groupName ] = new OptionGroup() - { - GroupName = groupName, - SelectionType = SelectType.Multi, - Options = group.Options, - }; - Mod.Settings[ groupName ] = oldConf; - } - - return true; + _modManager.ChangeModGroup( group.GroupName, groupName, Mod.Data ); } - - return false; } private void DrawMultiSelectorEditAdd( OptionGroup group, float nameBoxStart ) @@ -149,9 +132,9 @@ namespace Penumbra.UI private void DrawMultiSelectorEdit( OptionGroup group ) { var nameBoxStart = CheckMarkSize; - var flag = Mod!.Settings[ group.GroupName ]; - var modChanged = DrawMultiSelectorEditBegin( group ); + var flag = Mod!.Settings.Settings[ group.GroupName ]; + DrawMultiSelectorEditBegin( group ); for( var i = 0; i < group.Options.Count; ++i ) { var opt = group.Options[ i ]; @@ -171,11 +154,7 @@ namespace Penumbra.UI { if( newName.Length == 0 ) { - group.Options.RemoveAt( i ); - var bitmaskFront = ( 1 << i ) - 1; - var bitmaskBack = ~( bitmaskFront | ( 1 << i ) ); - Mod.Settings[ group.GroupName ] = ( flag & bitmaskFront ) | ( ( flag & bitmaskBack ) >> 1 ); - modChanged = true; + _modManager.RemoveModOption( i, group, Mod.Data ); } else if( newName != opt.OptionName ) { @@ -188,144 +167,85 @@ namespace Penumbra.UI DrawMultiSelectorEditAdd( group, nameBoxStart ); - if( modChanged ) - { - _selector.SaveCurrentMod(); - Save(); - } - - ImGuiCustom.EndFramedGroup(); + Custom.ImGuiCustom.EndFramedGroup(); } - private bool DrawSingleSelectorEditGroup( OptionGroup group, ref bool selectionChanged ) + private void DrawSingleSelectorEditGroup( OptionGroup group ) { var groupName = group.GroupName; - if( ImGui.InputText( $"##{groupName}_add", ref groupName, 64, ImGuiInputTextFlags.EnterReturnsTrue ) - && !Meta!.Groups.ContainsKey( groupName ) ) + if( ImGui.InputText( $"##{groupName}_add", ref groupName, 64, ImGuiInputTextFlags.EnterReturnsTrue ) ) { - var oldConf = Mod!.Settings[ group.GroupName ]; - if( groupName != group.GroupName ) - { - Meta.Groups.Remove( group.GroupName ); - selectionChanged |= Mod.FixSpecificSetting( group.GroupName ); - } - - if( groupName.Length > 0 ) - { - Meta.Groups.Add( groupName, new OptionGroup() - { - GroupName = groupName, - Options = group.Options, - SelectionType = SelectType.Single, - } ); - Mod.Settings[ groupName ] = oldConf; - } - - return true; + _modManager.ChangeModGroup( group.GroupName, groupName, Mod.Data ); } - - return false; } private float DrawSingleSelectorEdit( OptionGroup group ) { - var code = Mod!.Settings[ group.GroupName ]; - var selectionChanged = false; - var modChanged = false; - if( ImGuiCustom.RenameableCombo( $"##{group.GroupName}", ref code, out var newName, + var oldSetting = Mod!.Settings.Settings[ group.GroupName ]; + var code = oldSetting; + if( Custom.ImGuiCustom.RenameableCombo( $"##{group.GroupName}", ref code, out var newName, group.Options.Select( x => x.OptionName ).ToArray(), group.Options.Count ) ) { if( code == group.Options.Count ) { if( newName.Length > 0 ) { - selectionChanged = true; - modChanged = true; - Mod.Settings[ group.GroupName ] = code; + Mod.Settings.Settings[ group.GroupName ] = code; group.Options.Add( new Option() { OptionName = newName, OptionDesc = "", OptionFiles = new Dictionary< RelPath, HashSet< GamePath > >(), } ); + _selector.SaveCurrentMod(); } } else { if( newName.Length == 0 ) { - modChanged = true; - group.Options.RemoveAt( code ); + _modManager.RemoveModOption( code, group, Mod.Data ); } else { if( newName != group.Options[ code ].OptionName ) { - modChanged = true; group.Options[ code ] = new Option() { OptionName = newName, OptionDesc = group.Options[ code ].OptionDesc, OptionFiles = group.Options[ code ].OptionFiles, }; + _selector.SaveCurrentMod(); } - - selectionChanged |= Mod.Settings[ group.GroupName ] != code; - Mod.Settings[ group.GroupName ] = code; } - - selectionChanged |= Mod.FixSpecificSetting( group.GroupName ); } } - ImGui.SameLine(); - var labelEditPos = ImGui.GetCursorPosX(); - modChanged |= DrawSingleSelectorEditGroup( group, ref selectionChanged ); - - if( modChanged ) - { - _selector.SaveCurrentMod(); - } - - if( selectionChanged ) + if( code != oldSetting ) { Save(); } + ImGui.SameLine(); + var labelEditPos = ImGui.GetCursorPosX(); + DrawSingleSelectorEditGroup( group ); + return labelEditPos; } - private void AddNewGroup( string newGroup, SelectType selectType ) - { - if( Meta!.Groups.ContainsKey( newGroup ) || newGroup.Length <= 0 ) - { - return; - } - - Meta.Groups[ newGroup ] = new OptionGroup() - { - GroupName = newGroup, - SelectionType = selectType, - Options = new List< Option >(), - }; - - Mod.Settings[ newGroup ] = 0; - _selector.SaveCurrentMod(); - Save(); - } - private void DrawAddSingleGroupField( float labelEditPos ) { - const string hint = "Add new Single Group..."; - var newGroup = ""; + var newGroup = ""; ImGui.SetCursorPosX( labelEditPos ); if( labelEditPos == CheckMarkSize ) { ImGui.SetNextItemWidth( MultiEditBoxWidth ); } - if( ImGui.InputTextWithHint( LabelNewSingleGroupEdit, hint, ref newGroup, 64, ImGuiInputTextFlags.EnterReturnsTrue ) ) + if( ImGui.InputTextWithHint( LabelNewSingleGroupEdit, "Add new Single Group...", ref newGroup, 64, + ImGuiInputTextFlags.EnterReturnsTrue ) ) { - AddNewGroup( newGroup, SelectType.Single ); + _modManager.ChangeModGroup( "", newGroup, Mod.Data, SelectType.Single ); } } @@ -337,21 +257,22 @@ namespace Penumbra.UI if( ImGui.InputTextWithHint( LabelNewMultiGroup, "Add new Multi Group...", ref newGroup, 64, ImGuiInputTextFlags.EnterReturnsTrue ) ) { - AddNewGroup( newGroup, SelectType.Multi ); + _modManager.ChangeModGroup( "", newGroup, Mod.Data, SelectType.Multi ); } } private void DrawGroupSelectorsEdit() { var labelEditPos = CheckMarkSize; - foreach( var g in Meta.Groups.Values.Where( g => g.SelectionType == SelectType.Single ) ) + var groups = Meta.Groups.Values.ToArray(); + foreach( var g in groups.Where( g => g.SelectionType == SelectType.Single ) ) { labelEditPos = DrawSingleSelectorEdit( g ); } DrawAddSingleGroupField( labelEditPos ); - foreach( var g in Meta.Groups.Values.Where( g => g.SelectionType == SelectType.Multi ) ) + foreach( var g in groups.Where( g => g.SelectionType == SelectType.Multi ) ) { DrawMultiSelectorEdit( g ); } @@ -403,7 +324,7 @@ namespace Penumbra.UI } _selector.SaveCurrentMod(); - if( Mod.Enabled ) + if( Mod.Settings.Enabled ) { _selector.ReloadCurrentMod(); } @@ -428,7 +349,7 @@ namespace Penumbra.UI { Meta.FileSwaps[ key ] = newValue; _selector.SaveCurrentMod(); - if( Mod.Enabled ) + if( Mod.Settings.Enabled ) { _selector.ReloadCurrentMod(); } diff --git a/Penumbra/UI/MenuTabs/TabInstalled/TabInstalledModPanel.cs b/Penumbra/UI/MenuTabs/TabInstalled/TabInstalledModPanel.cs index 58c7fa3c..2bf53ff8 100644 --- a/Penumbra/UI/MenuTabs/TabInstalled/TabInstalledModPanel.cs +++ b/Penumbra/UI/MenuTabs/TabInstalled/TabInstalledModPanel.cs @@ -4,8 +4,9 @@ using System.IO; using System.Numerics; using Dalamud.Plugin; using ImGuiNET; -using Penumbra.Models; +using Penumbra.Mod; using Penumbra.Mods; +using Penumbra.Util; namespace Penumbra.UI { @@ -20,6 +21,7 @@ namespace Penumbra.UI private const string LabelEditWebsite = "##editWebsite"; private const string LabelModEnabled = "Enabled"; private const string LabelEditingEnabled = "Enable Editing"; + private const string LabelOverWriteDir = "OverwriteDir"; private const string ButtonOpenWebsite = "Open Website"; private const string ButtonOpenModFolder = "Open Mod Folder"; private const string ButtonRenameModFolder = "Rename Mod Folder"; @@ -45,6 +47,7 @@ namespace Penumbra.UI private readonly SettingsInterface _base; private readonly Selector _selector; + private readonly ModManager _modManager; public readonly PluginDetails Details; private bool _editMode; @@ -57,23 +60,22 @@ namespace Penumbra.UI _selector = s; Details = new PluginDetails( _base, _selector ); _currentWebsite = Meta?.Website ?? ""; + _modManager = Service< ModManager >.Get(); } - private ModInfo? Mod + private Mod.Mod? Mod => _selector.Mod; private ModMeta? Meta - => Mod?.Mod.Meta; + => Mod?.Data.Meta; private void DrawName() { var name = Meta!.Name; - if( ImGuiCustom.InputOrText( _editMode, LabelEditName, ref name, 64 ) - && name.Length > 0 - && name != Meta.Name ) + if( Custom.ImGuiCustom.InputOrText( _editMode, LabelEditName, ref name, 64 ) && _modManager.RenameMod( name, Mod!.Data ) ) { - Meta.Name = name; - _selector.SaveCurrentMod(); + _selector.RenameCurrentModLower( name ); + _selector.SelectModByDir( Mod.Data.BasePath.Name ); } } @@ -87,7 +89,7 @@ namespace Penumbra.UI ImGui.PushStyleVar( ImGuiStyleVar.ItemSpacing, ZeroVector ); ImGui.SameLine(); var version = Meta!.Version; - if( ImGuiCustom.ResizingTextInput( LabelEditVersion, ref version, 16 ) + if( Custom.ImGuiCustom.ResizingTextInput( LabelEditVersion, ref version, 16 ) && version != Meta.Version ) { Meta.Version = version; @@ -112,7 +114,7 @@ namespace Penumbra.UI ImGui.SameLine(); var author = Meta!.Author; - if( ImGuiCustom.InputOrText( _editMode, LabelEditAuthor, ref author, 64 ) + if( Custom.ImGuiCustom.InputOrText( _editMode, LabelEditAuthor, ref author, 64 ) && author != Meta.Author ) { Meta.Author = author; @@ -130,7 +132,7 @@ namespace Penumbra.UI ImGui.TextColored( GreyColor, "from" ); ImGui.SameLine(); var website = Meta!.Website; - if( ImGuiCustom.ResizingTextInput( LabelEditWebsite, ref website, 512 ) + if( Custom.ImGuiCustom.ResizingTextInput( LabelEditWebsite, ref website, 512 ) && website != Meta.Website ) { Meta.Website = website; @@ -191,15 +193,34 @@ namespace Penumbra.UI DrawWebsite(); } + private void DrawPriority() + { + var priority = Mod!.Settings.Priority; + ImGui.SetNextItemWidth( 50 ); + if( ImGui.InputInt( "Priority", ref priority, 0 ) && priority != Mod!.Settings.Priority ) + { + Mod.Settings.Priority = priority; + var collection = _modManager.CurrentCollection; + collection.Save( _base._plugin.PluginInterface! ); + collection.CalculateEffectiveFileList( _modManager.BasePath, Mod.Data.Resources.MetaManipulations.Count > 0 ); + } + + if( ImGui.IsItemHovered() ) + { + ImGui.SetTooltip( "Higher priority mods take precedence over other mods in the case of file conflicts.\n" + + "In case of identical priority, the alphabetically first mod takes precedence." ); + } + } + private void DrawEnabledMark() { - var enabled = Mod!.Enabled; + var enabled = Mod!.Settings.Enabled; if( ImGui.Checkbox( LabelModEnabled, ref enabled ) ) { - Mod.Enabled = enabled; - var modManager = Service< ModManager >.Get(); - modManager.Mods!.Save(); - modManager.CalculateEffectiveFileList(); + Mod.Settings.Enabled = enabled; + var collection = _modManager.CurrentCollection; + collection.Save( _base._plugin.PluginInterface! ); + collection.CalculateEffectiveFileList( _modManager.BasePath, Mod.Data.Resources.MetaManipulations.Count > 0 ); } } @@ -212,7 +233,7 @@ namespace Penumbra.UI { if( ImGui.Button( ButtonOpenModFolder ) ) { - Process.Start( Mod!.Mod.ModBasePath.FullName ); + Process.Start( Mod!.Data.BasePath.FullName ); } if( ImGui.IsItemHovered() ) @@ -224,7 +245,98 @@ namespace Penumbra.UI private string _newName = ""; private bool _keyboardFocus = true; - private void DrawRenameModFolderButton() + private void RenameModFolder( string newName ) + { + _newName = newName.RemoveNonAsciiSymbols().RemoveInvalidPathSymbols(); + if( _newName.Length == 0 ) + { + PluginLog.Debug( "New Directory name {NewName} was empty after removing invalid symbols.", newName ); + ImGui.CloseCurrentPopup(); + } + else if( !string.Equals( _newName, Mod!.Data.BasePath.Name, StringComparison.InvariantCultureIgnoreCase ) ) + { + DirectoryInfo dir = Mod!.Data.BasePath; + DirectoryInfo newDir = new( Path.Combine( dir.Parent!.FullName, _newName ) ); + + if( newDir.Exists ) + { + ImGui.OpenPopup( LabelOverWriteDir ); + } + else if( Service< ModManager >.Get()!.RenameModFolder( Mod.Data, newDir ) ) + { + _selector.ReloadCurrentMod(); + ImGui.CloseCurrentPopup(); + } + } + } + + private static bool MergeFolderInto( DirectoryInfo source, DirectoryInfo target ) + { + try + { + foreach( var file in source.EnumerateFiles( "*", SearchOption.AllDirectories ) ) + { + var targetFile = new FileInfo( Path.Combine( target.FullName, file.FullName.Substring( source.FullName.Length + 1 ) ) ); + if( targetFile.Exists ) + { + targetFile.Delete(); + } + + targetFile.Directory?.Create(); + file.MoveTo( targetFile.FullName ); + } + + source.Delete( true ); + return true; + } + catch( Exception e ) + { + PluginLog.Error( $"Could not merge directory {source.FullName} into {target.FullName}:\n{e}" ); + } + + return false; + } + + private bool OverwriteDirPopup() + { + var closeParent = false; + var _ = true; + if( ImGui.BeginPopupModal( LabelOverWriteDir, ref _, ImGuiWindowFlags.AlwaysAutoResize ) ) + { + DirectoryInfo dir = Mod!.Data.BasePath; + DirectoryInfo newDir = new( Path.Combine( dir.Parent!.FullName, _newName ) ); + ImGui.Text( + $"The mod directory {newDir} already exists.\nDo you want to merge / overwrite both mods?\nThis may corrupt the resulting mod in irrecoverable ways." ); + var buttonSize = new Vector2( 120, 0 ); + if( ImGui.Button( "Yes", buttonSize ) ) + { + if( MergeFolderInto( dir, newDir ) ) + { + Service< ModManager >.Get()!.RenameModFolder( Mod.Data, newDir, false ); + + _selector.ResetModNamesLower(); + _selector.SelectModByDir( _newName ); + + closeParent = true; + ImGui.CloseCurrentPopup(); + } + } + + ImGui.SameLine(); + + if( ImGui.Button( "Cancel", buttonSize ) ) + { + _keyboardFocus = true; + ImGui.CloseCurrentPopup(); + } + + ImGui.EndPopup(); + } + + return closeParent; + } + + private void DrawRenameModFolderPopup() { var _ = true; _keyboardFocus |= !ImGui.IsPopupOpen( PopupRenameFolder ); @@ -237,121 +349,38 @@ namespace Penumbra.UI ImGui.CloseCurrentPopup(); } - var newName = Mod!.FolderName; + var newName = Mod!.Data.BasePath.Name; if( _keyboardFocus ) { - PluginLog.Log( "Fuck you" ); ImGui.SetKeyboardFocusHere(); _keyboardFocus = false; } if( ImGui.InputText( "New Folder Name##RenameFolderInput", ref newName, 64, ImGuiInputTextFlags.EnterReturnsTrue ) ) { - _newName = newName.RemoveNonAsciiSymbols().RemoveInvalidPathSymbols(); - if( _newName.Length == 0 ) - { - ImGui.CloseCurrentPopup(); - } - else if( !string.Equals( _newName, Mod!.FolderName, StringComparison.InvariantCultureIgnoreCase ) ) - { - DirectoryInfo dir = Mod!.Mod.ModBasePath; - DirectoryInfo newDir = new( Path.Combine( dir.Parent!.FullName, _newName ) ); - if( newDir.Exists ) - { - PluginLog.Error( "GOTT" ); - ImGui.OpenPopup( "OverwriteDir" ); - } - else - { - try - { - dir.MoveTo( newDir.FullName ); - } - catch( Exception e ) - { - PluginLog.Error( $"Error while renaming directory {dir.FullName} to {newDir.FullName}:\n{e}" ); - } - - Mod!.FolderName = _newName; - Mod!.Mod.ModBasePath = newDir; - _selector.ReloadCurrentMod(); - Service< ModManager >.Get()!.Mods!.Save(); - ImGui.CloseCurrentPopup(); - } - } + RenameModFolder( newName ); } ImGui.TextColored( GreyColor, "Please restrict yourself to ascii symbols that are valid in a windows path,\nother symbols will be replaced by underscores." ); - var closeParent = false; - _ = true; ImGui.SetNextWindowPos( ImGui.GetMainViewport().GetCenter(), ImGuiCond.Appearing, Vector2.One / 2 ); - if( ImGui.BeginPopupModal( "OverwriteDir", ref _, ImGuiWindowFlags.AlwaysAutoResize ) ) - { - DirectoryInfo dir = Mod!.Mod.ModBasePath; - DirectoryInfo newDir = new( Path.Combine( dir.Parent!.FullName, _newName ) ); - ImGui.Text( - $"The mod directory {newDir} already exists.\nDo you want to merge / overwrite both mods?\nThis may corrupt the resulting mod in irrecoverable ways." ); - var buttonSize = new Vector2( 120, 0 ); - if( ImGui.Button( "Yes", buttonSize ) ) - { - try - { - foreach( var file in dir.EnumerateFiles( "*", SearchOption.AllDirectories ) ) - { - var target = new FileInfo( Path.Combine( newDir.FullName, - file.FullName.Substring( dir.FullName.Length ) ) ); - if( target.Exists ) - { - target.Delete(); - } - target.Directory?.Create(); - file.MoveTo( target.FullName ); - } - dir.Delete( true ); - - var mod = Service< ModManager >.Get()!.Mods!.ModSettings! - .RemoveAll( m => m.FolderName == _newName ); - - Mod!.FolderName = _newName; - Mod!.Mod.ModBasePath = newDir; - Service< ModManager >.Get()!.Mods!.Save(); - _base.ReloadMods(); - _selector.SelectModByDir( _newName ); - } - catch( Exception e ) - { - PluginLog.Error( $"Error while renaming directory {dir.FullName} to {newDir.FullName}:\n{e}" ); - } - - closeParent = true; - ImGui.CloseCurrentPopup(); - } - - ImGui.SameLine(); - - if( ImGui.Button( "Cancel", buttonSize ) ) - { - PluginLog.Error( "FUCKFUCK" ); - _keyboardFocus = true; - ImGui.CloseCurrentPopup(); - } - - ImGui.EndPopup(); - } - - if( closeParent ) + if( OverwriteDirPopup() ) { ImGui.CloseCurrentPopup(); } ImGui.EndPopup(); } + } + + private void DrawRenameModFolderButton() + { + DrawRenameModFolderPopup(); if( ImGui.Button( ButtonRenameModFolder ) ) { ImGui.OpenPopup( PopupRenameFolder ); @@ -367,7 +396,8 @@ namespace Penumbra.UI { if( ImGui.Button( ButtonEditJson ) ) { - Process.Start( _selector.SaveCurrentMod() ); + _selector.SaveCurrentMod(); + Process.Start( Mod!.Data.MetaFile.FullName ); } if( ImGui.IsItemHovered() ) @@ -389,14 +419,27 @@ namespace Penumbra.UI } } + private void DrawResetMetaButton() + { + if( ImGui.Button( "Recompute Metadata" ) ) + { + _selector.ReloadCurrentMod( true ); + } + + if( ImGui.IsItemHovered() ) + { + ImGui.SetTooltip( + "Force a recomputation of the metadata_manipulations.json file from all .meta files in the folder.\nAlso reloads the mod." ); + } + } + private void DrawDeduplicateButton() { if( ImGui.Button( ButtonDeduplicate ) ) { - ModCleanup.Deduplicate( Mod!.Mod.ModBasePath, Meta! ); + ModCleanup.Deduplicate( Mod!.Data.BasePath, Meta! ); _selector.SaveCurrentMod(); - Mod.Mod.RefreshModFiles(); - Service< ModManager >.Get().CalculateEffectiveFileList(); + _selector.ReloadCurrentMod(); } if( ImGui.IsItemHovered() ) @@ -409,10 +452,9 @@ namespace Penumbra.UI { if( ImGui.Button( ButtonNormalize ) ) { - ModCleanup.Normalize( Mod!.Mod.ModBasePath, Meta! ); + ModCleanup.Normalize( Mod!.Data.BasePath, Meta! ); _selector.SaveCurrentMod(); - Mod.Mod.RefreshModFiles(); - Service< ModManager >.Get().CalculateEffectiveFileList(); + _selector.ReloadCurrentMod(); } if( ImGui.IsItemHovered() ) @@ -430,6 +472,8 @@ namespace Penumbra.UI DrawEditJsonButton(); ImGui.SameLine(); DrawReloadJsonButton(); + + DrawResetMetaButton(); ImGui.SameLine(); DrawDeduplicateButton(); ImGui.SameLine(); @@ -454,9 +498,11 @@ namespace Penumbra.UI DrawHeaderLine(); // Next line with fixed distance. - ImGuiCustom.VerticalDistance( HeaderLineDistance ); + Custom.ImGuiCustom.VerticalDistance( HeaderLineDistance ); DrawEnabledMark(); + ImGui.SameLine(); + DrawPriority(); if( _base._plugin!.Configuration!.ShowAdvanced ) { ImGui.SameLine(); diff --git a/Penumbra/UI/MenuTabs/TabInstalled/TabInstalledSelector.cs b/Penumbra/UI/MenuTabs/TabInstalled/TabInstalledSelector.cs index 620605ee..82c40a66 100644 --- a/Penumbra/UI/MenuTabs/TabInstalled/TabInstalledSelector.cs +++ b/Penumbra/UI/MenuTabs/TabInstalled/TabInstalledSelector.cs @@ -1,14 +1,15 @@ using System; +using System.Collections.Generic; using System.IO; using System.Linq; using System.Numerics; using Dalamud.Interface; using Dalamud.Plugin; using ImGuiNET; -using Newtonsoft.Json; using Penumbra.Importer; -using Penumbra.Models; +using Penumbra.Mod; using Penumbra.Mods; +using Penumbra.Util; namespace Penumbra.UI { @@ -16,80 +17,92 @@ namespace Penumbra.UI { private class Selector { - private const string LabelSelectorList = "##availableModList"; - private const string LabelModFilter = "##ModFilter"; - private const string LabelPriorityPopup = "Priority"; - private const string LabelAddModPopup = "AddMod"; - private const string TooltipModFilter = "Filter mods for those containing the given substring."; - private const string TooltipMoveDown = "Move the selected mod down in priority"; - private const string TooltipMoveUp = "Move the selected mod up in priority"; - private const string TooltipDelete = "Delete the selected mod"; - private const string TooltipAdd = "Add an empty mod"; - private const string DialogDeleteMod = "PenumbraDeleteMod"; - private const string ButtonYesDelete = "Yes, delete it"; - private const string ButtonNoDelete = "No, keep it"; - private const string DescPriorityPopup = "New Priority:"; + [Flags] + private enum ModFilter + { + Enabled = 1 << 0, + Disabled = 1 << 1, + NoConflict = 1 << 2, + SolvedConflict = 1 << 3, + UnsolvedConflict = 1 << 4, + HasNoMetaManipulations = 1 << 5, + HasMetaManipulations = 1 << 6, + HasNoFileSwaps = 1 << 7, + HasFileSwaps = 1 << 8, + HasConfig = 1 << 9, + HasNoConfig = 1 << 10, + HasNoFiles = 1 << 11, + HasFiles = 1 << 12, + }; - private const float SelectorPanelWidth = 240f; - private const uint DisabledModColor = 0xFF666666; - private const uint ConflictingModColor = 0xFFAAAAFF; + private const ModFilter UnfilteredStateMods = ( ModFilter )( ( 1 << 13 ) - 1 ); - private static readonly Vector2 SelectorButtonSizes = new( 60, 0 ); - private static readonly string ArrowUpString = FontAwesomeIcon.ArrowUp.ToIconString(); - private static readonly string ArrowDownString = FontAwesomeIcon.ArrowDown.ToIconString(); + private static readonly Dictionary< ModFilter, string > ModFilterNames = new() + { + { ModFilter.Enabled, "Enabled" }, + { ModFilter.Disabled, "Disabled" }, + { ModFilter.NoConflict, "No Conflicts" }, + { ModFilter.SolvedConflict, "Solved Conflicts" }, + { ModFilter.UnsolvedConflict, "Unsolved Conflicts" }, + { ModFilter.HasNoMetaManipulations, "No Meta Manipulations" }, + { ModFilter.HasMetaManipulations, "Meta Manipulations" }, + { ModFilter.HasNoFileSwaps, "No File Swaps" }, + { ModFilter.HasFileSwaps, "File Swaps" }, + { ModFilter.HasNoConfig, "No Configuration" }, + { ModFilter.HasConfig, "Configuration" }, + { ModFilter.HasNoFiles, "No Files" }, + { ModFilter.HasFiles, "Files" }, + }; + + private const string LabelSelectorList = "##availableModList"; + private const string LabelModFilter = "##ModFilter"; + private const string LabelAddModPopup = "AddMod"; + private const string TooltipModFilter = "Filter mods for those containing the given substring."; + private const string TooltipDelete = "Delete the selected mod"; + private const string TooltipAdd = "Add an empty mod"; + private const string DialogDeleteMod = "PenumbraDeleteMod"; + private const string ButtonYesDelete = "Yes, delete it"; + private const string ButtonNoDelete = "No, keep it"; + + private const float SelectorPanelWidth = 240f; + private const uint DisabledModColor = 0xFF666666; + private const uint ConflictingModColor = 0xFFAAAAFF; + private const uint HandledConflictModColor = 0xFF88DDDD; + + private static readonly Vector2 SelectorButtonSizes = new( 120, 0 ); private readonly SettingsInterface _base; + private readonly ModManager _modManager; - private static ModCollection? Mods - => Service< ModManager >.Get().Mods; + private List< Mod.Mod >? Mods + => _modManager.CurrentCollection.Cache?.AvailableMods; - public ModInfo? Mod { get; private set; } + public Mod.Mod? Mod { get; private set; } private int _index; private int? _deleteIndex; private string _modFilter = ""; - private string[]? _modNamesLower; - + private string[] _modNamesLower; + private ModFilter _stateFilter = UnfilteredStateMods; public Selector( SettingsInterface ui ) { - _base = ui; + _base = ui; + _modNamesLower = Array.Empty< string >(); + _modManager = Service.Get(); ResetModNamesLower(); } public void ResetModNamesLower() { - _modNamesLower = Mods?.ModSettings?.Where( I => I.Mod != null ) - .Select( I => I.Mod!.Meta.Name.ToLowerInvariant() ).ToArray() - ?? new string[] { }; + _modNamesLower = Mods?.Select( m => m.Data.Meta.Name.ToLowerInvariant() ).ToArray() + ?? Array.Empty< string >(); } - private void DrawPriorityChangeButton( string iconString, bool up, int unavailableWhen ) + public void RenameCurrentModLower( string newName ) { - ImGui.PushFont( UiBuilder.IconFont ); - if( _index != unavailableWhen ) + if( _index >= 0 ) { - if( ImGui.Button( iconString, SelectorButtonSizes ) ) - { - SetSelection( _index ); - Service< ModManager >.Get().ChangeModPriority( Mod!, up ); - _modNamesLower!.Swap( _index, _index + ( up ? 1 : -1 ) ); - _index += up ? 1 : -1; - } - } - else - { - ImGui.PushStyleVar( ImGuiStyleVar.Alpha, 0.5f ); - ImGui.Button( iconString, SelectorButtonSizes ); - ImGui.PopStyleVar(); - } - - ImGui.PopFont(); - - if( ImGui.IsItemHovered() ) - { - ImGui.SetTooltip( - _base._plugin!.Configuration!.InvertModListOrder ^ up ? TooltipMoveDown : TooltipMoveUp - ); + _modNamesLower[ _index ] = newName.ToLowerInvariant(); } } @@ -127,7 +140,7 @@ namespace Penumbra.UI { try { - var newDir = TexToolsImport.CreateModFolder( new DirectoryInfo( _base._plugin.Configuration!.CurrentCollection ), + var newDir = TexToolsImport.CreateModFolder( new DirectoryInfo( _base._plugin.Configuration!.ModDirectory ), newName ); var modMeta = new ModMeta { @@ -135,9 +148,10 @@ namespace Penumbra.UI Name = newName, Description = string.Empty, }; - var metaPath = Path.Combine( newDir.FullName, "meta.json" ); - File.WriteAllText( metaPath, JsonConvert.SerializeObject( modMeta, Formatting.Indented ) ); - _base.ReloadMods(); + + var metaFile = new FileInfo( Path.Combine( newDir.FullName, "meta.json" ) ); + modMeta.SaveToFile( metaFile ); + _modManager.AddMod( newDir ); SelectModByDir( newDir.Name ); } catch( Exception e ) @@ -174,7 +188,7 @@ namespace Penumbra.UI private void DrawModsSelectorFilter() { - ImGui.SetNextItemWidth( SelectorButtonSizes.X * 4 ); + ImGui.SetNextItemWidth( SelectorButtonSizes.X * 2 - 22 ); var tmp = _modFilter; if( ImGui.InputTextWithHint( LabelModFilter, "Filter Mods...", ref tmp, 256 ) ) { @@ -185,6 +199,26 @@ namespace Penumbra.UI { ImGui.SetTooltip( TooltipModFilter ); } + + ImGui.SameLine(); + if( ImGui.BeginCombo( "##ModStateFilter", "", + ImGuiComboFlags.NoPreview | ImGuiComboFlags.PopupAlignLeft | ImGuiComboFlags.HeightLargest ) ) + { + var flags = ( int )_stateFilter; + foreach( ModFilter flag in Enum.GetValues( typeof( ModFilter ) ) ) + { + ImGui.CheckboxFlags( ModFilterNames[ flag ], ref flags, ( int )flag ); + } + + _stateFilter = ( ModFilter )flags; + + ImGui.EndCombo(); + } + + if( ImGui.IsItemHovered() ) + { + ImGui.SetTooltip( "Filter mods for their activation status." ); + } } private void DrawModsSelectorButtons() @@ -193,10 +227,6 @@ namespace Penumbra.UI ImGui.PushStyleVar( ImGuiStyleVar.WindowPadding, ZeroVector ); ImGui.PushStyleVar( ImGuiStyleVar.FrameRounding, 0 ); - DrawPriorityChangeButton( ArrowUpString, false, 0 ); - ImGui.SameLine(); - DrawPriorityChangeButton( ArrowDownString, true, Mods?.ModSettings?.Count - 1 ?? 0 ); - ImGui.SameLine(); DrawModTrashButton(); ImGui.SameLine(); DrawModAddButton(); @@ -221,7 +251,7 @@ namespace Penumbra.UI return; } - if( Mod?.Mod == null ) + if( Mod == null ) { ImGui.CloseCurrentPopup(); ImGui.EndPopup(); @@ -231,16 +261,15 @@ namespace Penumbra.UI ImGui.Text( "Are you sure you want to delete the following mod:" ); // todo: why the fuck does this become null?????? ImGui.Dummy( new Vector2( ImGui.GetTextLineHeight() / 2 ) ); - ImGui.TextColored( new Vector4( 0.7f, 0.1f, 0.1f, 1 ), Mod?.Mod?.Meta?.Name ?? "Unknown" ); + ImGui.TextColored( new Vector4( 0.7f, 0.1f, 0.1f, 1 ), Mod.Data.Meta.Name ?? "Unknown" ); ImGui.Dummy( new Vector2( ImGui.GetTextLineHeight() ) / 2 ); var buttonSize = new Vector2( 120, 0 ); if( ImGui.Button( ButtonYesDelete, buttonSize ) ) { ImGui.CloseCurrentPopup(); - Service< ModManager >.Get().DeleteMod( Mod?.Mod ); + _modManager.DeleteMod( Mod.Data.BasePath ); ClearSelection(); - _base.ReloadMods(); } ImGui.SameLine(); @@ -254,7 +283,93 @@ namespace Penumbra.UI ImGui.EndPopup(); } - private int _priorityPopupIdx = 0; + private bool CheckFlags( int count, ModFilter hasNoFlag, ModFilter hasFlag ) + { + if( count == 0 ) + { + if( _stateFilter.HasFlag( hasNoFlag ) ) + { + return false; + } + } + else if( _stateFilter.HasFlag( hasFlag ) ) + { + return false; + } + + return true; + } + + public void DrawMod( Mod.Mod mod, int modIndex ) + { + if( _modFilter.Length > 0 && !_modNamesLower[ modIndex ].Contains( _modFilter ) + || CheckFlags( mod.Data.Resources.ModFiles.Count, ModFilter.HasNoFiles, ModFilter.HasFiles ) + || CheckFlags( mod.Data.Meta.FileSwaps.Count, ModFilter.HasNoFileSwaps, ModFilter.HasFileSwaps ) + || CheckFlags( mod.Data.Resources.MetaManipulations.Count, ModFilter.HasNoMetaManipulations, ModFilter.HasMetaManipulations ) + || CheckFlags( mod.Data.Meta.HasGroupsWithConfig ? 1 : 0, ModFilter.HasNoConfig, ModFilter.HasConfig ) ) + { + return; + } + + var changedColour = false; + if( !mod.Settings.Enabled ) + { + if( !_stateFilter.HasFlag( ModFilter.Disabled ) || !_stateFilter.HasFlag( ModFilter.NoConflict ) ) + { + return; + } + + ImGui.PushStyleColor( ImGuiCol.Text, DisabledModColor ); + changedColour = true; + } + else + { + if( !_stateFilter.HasFlag( ModFilter.Enabled ) ) + { + return; + } + + if( mod.Cache.Conflicts.Any() ) + { + if( mod.Cache.Conflicts.Keys.Any( m => m.Settings.Priority == mod.Settings.Priority ) ) + { + if( !_stateFilter.HasFlag( ModFilter.UnsolvedConflict ) ) + { + return; + } + + ImGui.PushStyleColor( ImGuiCol.Text, ConflictingModColor ); + } + else + { + if( !_stateFilter.HasFlag( ModFilter.SolvedConflict ) ) + { + return; + } + + ImGui.PushStyleColor( ImGuiCol.Text, HandledConflictModColor ); + } + + changedColour = true; + } + else if( !_stateFilter.HasFlag( ModFilter.NoConflict ) ) + { + return; + } + } + + var selected = ImGui.Selectable( $"{mod.Data.Meta.Name}##{modIndex}", modIndex == _index ); + + if( changedColour ) + { + ImGui.PopStyleColor(); + } + + if( selected ) + { + SetSelection( modIndex, mod ); + } + } public void Draw() { @@ -271,61 +386,9 @@ namespace Penumbra.UI // Inlay selector list ImGui.BeginChild( LabelSelectorList, new Vector2( SelectorPanelWidth, -ImGui.GetFrameHeightWithSpacing() ), true ); - if( Mods.ModSettings != null ) + for( var modIndex = 0; modIndex < Mods.Count; modIndex++ ) { - for( var modIndex = 0; modIndex < Mods.ModSettings.Count; modIndex++ ) - { - var settings = Mods.ModSettings[ modIndex ]; - var modName = settings.Mod.Meta.Name; - if( _modFilter.Length > 0 && !_modNamesLower![ modIndex ].Contains( _modFilter ) ) - { - continue; - } - - var changedColour = false; - if( !settings.Enabled ) - { - ImGui.PushStyleColor( ImGuiCol.Text, DisabledModColor ); - changedColour = true; - } - else if( settings.Mod.FileConflicts.Any() ) - { - ImGui.PushStyleColor( ImGuiCol.Text, ConflictingModColor ); - changedColour = true; - } - -#if DEBUG - var selected = ImGui.Selectable( - $"id={modIndex} {modName}", - modIndex == _index - ); -#else - var selected = ImGui.Selectable( modName, modIndex == _index ); -#endif - if( ImGui.IsItemClicked( ImGuiMouseButton.Right ) ) - { - if( ImGui.IsPopupOpen( LabelPriorityPopup ) ) - { - ImGui.CloseCurrentPopup(); - } - - _priorityPopupIdx = modIndex; - _keyboardFocus = true; - ImGui.OpenPopup( LabelPriorityPopup ); - } - - ImGui.OpenPopupOnItemClick( LabelPriorityPopup, ImGuiPopupFlags.MouseButtonRight ); - - if( changedColour ) - { - ImGui.PopStyleColor(); - } - - if( selected ) - { - SetSelection( modIndex, settings ); - } - } + DrawMod( Mods[ modIndex ], modIndex ); } ImGui.EndChild(); @@ -334,52 +397,9 @@ namespace Penumbra.UI ImGui.EndGroup(); DrawDeleteModal(); - DrawPriorityPopup(); } - private void DrawPriorityPopup() - { - if( !ImGui.BeginPopupContextItem( LabelPriorityPopup ) ) - { - return; - } - - var size = ImGui.CalcTextSize( DescPriorityPopup ).X; - //ImGui.Text( DescPriorityPopup ); - var newPriority = _priorityPopupIdx; - - if( _keyboardFocus ) - { - ImGui.SetKeyboardFocusHere( -1 ); - _keyboardFocus = false; - } - - ImGui.SetNextItemWidth( size ); - if( ImGui.InputInt( "New Priority", ref newPriority, 0, 0, - ImGuiInputTextFlags.EnterReturnsTrue ) - && newPriority != _priorityPopupIdx ) - { - Service< ModManager >.Get().ChangeModPriority( Mods!.ModSettings![ _priorityPopupIdx ], newPriority ); - ResetModNamesLower(); - if( _priorityPopupIdx == _index ) - { - _index = newPriority; - SetSelection( _index ); - } - - ImGui.CloseCurrentPopup(); - } - - if( ImGui.IsKeyPressed( ImGui.GetKeyIndex( ImGuiKey.Escape ) ) ) - { - ImGui.CloseCurrentPopup(); - } - - ImGui.EndPopup(); - } - - - private void SetSelection( int idx, ModInfo? info ) + private void SetSelection( int idx, Mod.Mod? info ) { Mod = info; if( idx != _index ) @@ -393,7 +413,7 @@ namespace Penumbra.UI private void SetSelection( int idx ) { - if( idx >= ( Mods?.ModSettings?.Count ?? 0 ) ) + if( idx >= ( Mods?.Count ?? 0 ) ) { idx = -1; } @@ -404,58 +424,45 @@ namespace Penumbra.UI } else { - SetSelection( idx, Mods!.ModSettings![ idx ] ); + SetSelection( idx, Mods![ idx ] ); } } + public void ReloadSelection() + => SetSelection( _index, Mods![ _index ] ); + public void ClearSelection() => SetSelection( -1 ); public void SelectModByName( string name ) { - var idx = Mods?.ModSettings?.FindIndex( mod => mod.Mod.Meta.Name == name ) ?? -1; + var idx = Mods?.FindIndex( mod => mod.Data.Meta.Name == name ) ?? -1; SetSelection( idx ); } public void SelectModByDir( string name ) { - var idx = Mods?.ModSettings?.FindIndex( mod => mod.FolderName == name ) ?? -1; + var idx = Mods?.FindIndex( mod => mod.Data.BasePath.Name == name ) ?? -1; SetSelection( idx ); } - private string GetCurrentModMetaFile() - => Mod == null ? "" : Path.Combine( Mod.Mod.ModBasePath.FullName, "meta.json" ); - - public void ReloadCurrentMod() - { - var metaPath = GetCurrentModMetaFile(); - if( metaPath.Length > 0 && File.Exists( metaPath ) ) - { - Mod!.Mod.Meta = ModMeta.LoadFromFile( metaPath ) ?? Mod.Mod.Meta; - _base._menu.InstalledTab.ModPanel.Details.ResetState(); - } - - Mod!.Mod.RefreshModFiles(); - Service< ModManager >.Get().CalculateEffectiveFileList(); - ResetModNamesLower(); - } - - public string SaveCurrentMod() + public void ReloadCurrentMod( bool recomputeMeta = false ) { if( Mod == null ) { - return ""; + return; } - var metaPath = GetCurrentModMetaFile(); - if( metaPath.Length > 0 ) + if( _index >= 0 && _modManager.UpdateMod( Mod.Data, recomputeMeta ) ) { - File.WriteAllText( metaPath, JsonConvert.SerializeObject( Mod.Mod.Meta, Formatting.Indented ) ); + ResetModNamesLower(); + SelectModByDir( Mod.Data.BasePath.Name ); + _base._menu.InstalledTab.ModPanel.Details.ResetState(); } - - _base._menu.InstalledTab.ModPanel.Details.ResetState(); - return metaPath; } + + public void SaveCurrentMod() + => Mod?.Data.SaveMeta(); } } } \ No newline at end of file diff --git a/Penumbra/UI/MenuTabs/TabSettings.cs b/Penumbra/UI/MenuTabs/TabSettings.cs index ccea3a30..f778617c 100644 --- a/Penumbra/UI/MenuTabs/TabSettings.cs +++ b/Penumbra/UI/MenuTabs/TabSettings.cs @@ -1,6 +1,10 @@ +using System; using System.Diagnostics; +using System.Text.RegularExpressions; +using Dalamud.Plugin; using ImGuiNET; using Penumbra.Hooks; +using Penumbra.Util; namespace Penumbra.UI { @@ -13,7 +17,6 @@ namespace Penumbra.UI private const string LabelRediscoverButton = "Rediscover Mods"; private const string LabelOpenFolder = "Open Mods Folder"; private const string LabelEnabled = "Enable Mods"; - private const string LabelInvertModOrder = "Invert mod load order (mods are loaded bottom up)"; private const string LabelShowAdvanced = "Show Advanced Settings"; private const string LabelLogLoadedFiles = "Log all loaded files"; private const string LabelDisableNotifications = "Disable filesystem change notifications"; @@ -21,7 +24,7 @@ namespace Penumbra.UI private const string LabelReloadResource = "Reload Player Resource"; private readonly SettingsInterface _base; - private readonly Configuration _config; + private readonly Configuration _config; private bool _configChanged; public TabSettings( SettingsInterface ui ) @@ -33,11 +36,11 @@ namespace Penumbra.UI private void DrawRootFolder() { - var basePath = _config.CurrentCollection; - if( ImGui.InputText( LabelRootFolder, ref basePath, 255 ) && _config.CurrentCollection != basePath ) + var basePath = _config.ModDirectory; + if( ImGui.InputText( LabelRootFolder, ref basePath, 255 ) && _config.ModDirectory != basePath ) { - _config.CurrentCollection = basePath; - _configChanged = true; + _config.ModDirectory = basePath; + _configChanged = true; } } @@ -54,7 +57,7 @@ namespace Penumbra.UI { if( ImGui.Button( LabelOpenFolder ) ) { - Process.Start( _config.CurrentCollection ); + Process.Start( _config.ModDirectory ); } } @@ -69,17 +72,6 @@ namespace Penumbra.UI } } - private void DrawInvertModOrderBox() - { - var invertOrder = _config.InvertModListOrder; - if( ImGui.Checkbox( LabelInvertModOrder, ref invertOrder ) ) - { - _config.InvertModListOrder = invertOrder; - _base.ReloadMods(); - _configChanged = true; - } - } - private void DrawShowAdvancedBox() { var showAdvanced = _config.ShowAdvanced; @@ -92,9 +84,21 @@ namespace Penumbra.UI private void DrawLogLoadedFilesBox() { - if( _base._plugin.ResourceLoader != null ) + ImGui.Checkbox( LabelLogLoadedFiles, ref _base._plugin.ResourceLoader.LogAllFiles ); + ImGui.SameLine(); + var regex = _base._plugin.ResourceLoader.LogFileFilter?.ToString() ?? string.Empty; + var tmp = regex; + if( ImGui.InputTextWithHint( "##LogFilter", "Matching this Regex...", ref tmp, 64 ) && tmp != regex ) { - ImGui.Checkbox( LabelLogLoadedFiles, ref _base._plugin.ResourceLoader.LogAllFiles ); + try + { + var newRegex = tmp.Length > 0 ? new Regex( tmp, RegexOptions.Compiled ) : null; + _base._plugin.ResourceLoader.LogFileFilter = newRegex; + } + catch( Exception e ) + { + PluginLog.Debug( "Could not create regex:\n{Exception}", e ); + } } } @@ -127,11 +131,11 @@ namespace Penumbra.UI } } - private void DrawReloadResourceButton() + private static void DrawReloadResourceButton() { if( ImGui.Button( LabelReloadResource ) ) { - Service.Get().ReloadPlayerResources(); + Service< GameResourceManagement >.Get().ReloadPlayerResources(); } } @@ -157,13 +161,10 @@ namespace Penumbra.UI ImGui.SameLine(); DrawOpenModsButton(); - ImGuiCustom.VerticalDistance( DefaultVerticalSpace ); + Custom.ImGuiCustom.VerticalDistance( DefaultVerticalSpace ); DrawEnabledBox(); - ImGuiCustom.VerticalDistance( DefaultVerticalSpace ); - DrawInvertModOrderBox(); - - ImGuiCustom.VerticalDistance( DefaultVerticalSpace ); + Custom.ImGuiCustom.VerticalDistance( DefaultVerticalSpace ); DrawShowAdvancedBox(); if( _config.ShowAdvanced ) diff --git a/Penumbra/UI/SettingsInterface.cs b/Penumbra/UI/SettingsInterface.cs index 6a17bd9b..aed3c1f7 100644 --- a/Penumbra/UI/SettingsInterface.cs +++ b/Penumbra/UI/SettingsInterface.cs @@ -1,6 +1,7 @@ using System.IO; using System.Numerics; using Penumbra.Mods; +using Penumbra.Util; namespace Penumbra.UI { @@ -14,18 +15,19 @@ namespace Penumbra.UI private readonly Plugin _plugin; private readonly ManageModsButton _manageModsButton; - private readonly MenuBar _menuBar; - private readonly SettingsMenu _menu; + private readonly MenuBar _menuBar; + private readonly SettingsMenu _menu; public SettingsInterface( Plugin plugin ) { - _plugin = plugin; + _plugin = plugin; _manageModsButton = new ManageModsButton( this ); - _menuBar = new MenuBar( this ); - _menu = new SettingsMenu( this ); + _menuBar = new MenuBar( this ); + _menu = new SettingsMenu( this ); } - public void FlipVisibility() => _menu.Visible = !_menu.Visible; + public void FlipVisibility() + => _menu.Visible = !_menu.Visible; public void Draw() { @@ -39,10 +41,10 @@ namespace Penumbra.UI _menu.InstalledTab.Selector.ResetModNamesLower(); _menu.InstalledTab.Selector.ClearSelection(); // create the directory if it doesn't exist - Directory.CreateDirectory( _plugin!.Configuration!.CurrentCollection ); + Directory.CreateDirectory( _plugin!.Configuration!.ModDirectory ); var modManager = Service< ModManager >.Get(); - modManager.DiscoverMods( _plugin.Configuration.CurrentCollection ); + modManager.DiscoverMods( new DirectoryInfo( _plugin.Configuration.ModDirectory ) ); _menu.InstalledTab.Selector.ResetModNamesLower(); } } diff --git a/Penumbra/UI/SettingsMenu.cs b/Penumbra/UI/SettingsMenu.cs index b01b684f..21b7c747 100644 --- a/Penumbra/UI/SettingsMenu.cs +++ b/Penumbra/UI/SettingsMenu.cs @@ -16,17 +16,19 @@ namespace Penumbra.UI private readonly TabSettings _settingsTab; private readonly TabImport _importTab; private readonly TabBrowser _browserTab; + private readonly TabCollections _collectionsTab; public readonly TabInstalled InstalledTab; - public readonly TabEffective EffectiveTab; + private readonly TabEffective _effectiveTab; public SettingsMenu( SettingsInterface ui ) { - _base = ui; - _settingsTab = new TabSettings( _base ); - _importTab = new TabImport( _base ); - _browserTab = new TabBrowser(); - InstalledTab = new TabInstalled( _base ); - EffectiveTab = new TabEffective(); + _base = ui; + _settingsTab = new TabSettings( _base ); + _importTab = new TabImport( _base ); + _browserTab = new TabBrowser(); + InstalledTab = new TabInstalled( _base ); + _collectionsTab = new TabCollections( InstalledTab.Selector ); + _effectiveTab = new TabEffective(); } #if DEBUG @@ -57,6 +59,7 @@ namespace Penumbra.UI ImGui.BeginTabBar( PenumbraSettingsLabel ); _settingsTab.Draw(); + _collectionsTab.Draw(); _importTab.Draw(); if( !_importTab.IsImporting() ) @@ -66,7 +69,7 @@ namespace Penumbra.UI if( _base._plugin!.Configuration!.ShowAdvanced ) { - EffectiveTab.Draw(); + _effectiveTab.Draw(); } } diff --git a/Penumbra/Util/ArrayExtensions.cs b/Penumbra/Util/ArrayExtensions.cs index f7dd5324..0429f446 100644 --- a/Penumbra/Util/ArrayExtensions.cs +++ b/Penumbra/Util/ArrayExtensions.cs @@ -1,7 +1,7 @@ using System; using System.Collections.Generic; -namespace Penumbra +namespace Penumbra.Util { public static class ArrayExtensions { diff --git a/Penumbra/Util/BinaryReaderExtensions.cs b/Penumbra/Util/BinaryReaderExtensions.cs index ca6138b3..19be89ca 100644 --- a/Penumbra/Util/BinaryReaderExtensions.cs +++ b/Penumbra/Util/BinaryReaderExtensions.cs @@ -1,11 +1,9 @@ using System; using System.Collections.Generic; using System.IO; -using System.Linq; using System.Runtime.CompilerServices; using System.Runtime.InteropServices; using System.Text; -using System.Threading.Tasks; namespace Penumbra.Util { @@ -38,10 +36,10 @@ namespace Penumbra.Util var list = new List< T >( count ); - for( int i = 0; i < count; i++ ) + for( var i = 0; i < count; i++ ) { var offset = size * i; - var span = new ReadOnlySpan< byte >( data, offset, size ); + var span = new ReadOnlySpan< byte >( data, offset, size ); list.Add( MemoryMarshal.Read< T >( span ) ); } @@ -55,12 +53,12 @@ namespace Penumbra.Util var data = br.ReadBytes( size * count ); // im a pirate arr - var arr = new T[ count ]; + var arr = new T[count]; - for( int i = 0; i < count; i++ ) + for( var i = 0; i < count; i++ ) { var offset = size * i; - var span = new ReadOnlySpan< byte >( data, offset, size ); + var span = new ReadOnlySpan< byte >( data, offset, size ); arr[ i ] = MemoryMarshal.Read< T >( span ); } @@ -76,9 +74,7 @@ namespace Penumbra.Util /// The offset to read a string starting from. /// public static string ReadStringOffsetData( this BinaryReader br, long offset ) - { - return Encoding.UTF8.GetString( ReadRawOffsetData( br, offset ) ); - } + => Encoding.UTF8.GetString( ReadRawOffsetData( br, offset ) ); /// /// Moves the BinaryReader position to offset, reads raw bytes until a null byte, then @@ -91,9 +87,9 @@ namespace Penumbra.Util { var originalPosition = br.BaseStream.Position; br.BaseStream.Position = offset; - + var chars = new List< byte >(); - + byte current; while( ( current = br.ReadByte() ) != 0 ) { @@ -108,7 +104,8 @@ namespace Penumbra.Util /// /// Seeks this BinaryReader's position to the given offset. Syntactic sugar. /// - public static void Seek( this BinaryReader br, long offset ) { + public static void Seek( this BinaryReader br, long offset ) + { br.BaseStream.Position = offset; } @@ -137,4 +134,4 @@ namespace Penumbra.Util return data; } } -} +} \ No newline at end of file diff --git a/Penumbra/Util/Crc32.cs b/Penumbra/Util/Crc32.cs index 818c2244..655d18e2 100644 --- a/Penumbra/Util/Crc32.cs +++ b/Penumbra/Util/Crc32.cs @@ -22,7 +22,8 @@ namespace Penumbra.Util return k; } ).ToArray(); - public uint Checksum => ~_crc32; + public uint Checksum + => ~_crc32; private uint _crc32 = 0xFFFFFFFF; @@ -50,8 +51,7 @@ namespace Penumbra.Util [MethodImpl( MethodImplOptions.AggressiveInlining )] public void Update( byte b ) { - _crc32 = CrcArray[ ( _crc32 ^ b ) & 0xFF ] ^ - ( ( _crc32 >> 8 ) & 0x00FFFFFF ); + _crc32 = CrcArray[ ( _crc32 ^ b ) & 0xFF ] ^ ( ( _crc32 >> 8 ) & 0x00FFFFFF ); } } } \ No newline at end of file diff --git a/Penumbra/DialogExtensions.cs b/Penumbra/Util/DialogExtensions.cs similarity index 95% rename from Penumbra/DialogExtensions.cs rename to Penumbra/Util/DialogExtensions.cs index 31bc3c80..8ac4ee3c 100644 --- a/Penumbra/DialogExtensions.cs +++ b/Penumbra/Util/DialogExtensions.cs @@ -5,7 +5,7 @@ using System.Threading; using System.Threading.Tasks; using System.Windows.Forms; -namespace Penumbra +namespace Penumbra.Util { public static class DialogExtensions { @@ -38,7 +38,8 @@ namespace Penumbra { public IntPtr Handle { get; set; } - public DialogHandle( IntPtr handle ) => Handle = handle; + public DialogHandle( IntPtr handle ) + => Handle = handle; } public class HiddenForm : Form diff --git a/Penumbra/Util/MemoryStreamExtensions.cs b/Penumbra/Util/MemoryStreamExtensions.cs index 60bd8f41..5d4c6235 100644 --- a/Penumbra/Util/MemoryStreamExtensions.cs +++ b/Penumbra/Util/MemoryStreamExtensions.cs @@ -1,9 +1,4 @@ -using System; -using System.Collections.Generic; using System.IO; -using System.Linq; -using System.Text; -using System.Threading.Tasks; namespace Penumbra.Util { @@ -14,4 +9,4 @@ namespace Penumbra.Util stream.Write( data, 0, data.Length ); } } -} +} \ No newline at end of file diff --git a/Penumbra/Util/PenumbraPath.cs b/Penumbra/Util/PenumbraPath.cs index 86717e12..72f66119 100644 --- a/Penumbra/Util/PenumbraPath.cs +++ b/Penumbra/Util/PenumbraPath.cs @@ -171,9 +171,9 @@ namespace Penumbra.Util { if( value != null ) { - var v = ( GamePath) value; + var v = ( GamePath )value; serializer.Serialize( writer, v.ToString() ); } } } -} +} \ No newline at end of file diff --git a/Penumbra/Util/PenumbraSqPackStream.cs b/Penumbra/Util/PenumbraSqPackStream.cs index 4c461f8f..e27a24e4 100644 --- a/Penumbra/Util/PenumbraSqPackStream.cs +++ b/Penumbra/Util/PenumbraSqPackStream.cs @@ -153,8 +153,8 @@ namespace Penumbra.Util totalBlocks += mdlBlock.IndexBufferBlockNum[ i ]; } - var compressedBlockSizes = Reader.ReadStructures< ushort >( totalBlocks ); - var currentBlock = 0; + var compressedBlockSizes = Reader.ReadStructures< ushort >( totalBlocks ); + var currentBlock = 0; var vertexDataOffsets = new int[3]; var indexDataOffsets = new int[3]; var vertexBufferSizes = new int[3]; diff --git a/Penumbra/Service.cs b/Penumbra/Util/Service.cs similarity index 97% rename from Penumbra/Service.cs rename to Penumbra/Util/Service.cs index 6f198f0c..3412fc14 100644 --- a/Penumbra/Service.cs +++ b/Penumbra/Util/Service.cs @@ -1,6 +1,6 @@ using System; -namespace Penumbra +namespace Penumbra.Util { /// /// Basic service locator @@ -11,8 +11,7 @@ namespace Penumbra private static T? _object; static Service() - { - } + { } public static void Set( T obj ) { diff --git a/Penumbra/Util/SingleOrArrayConverter.cs b/Penumbra/Util/SingleOrArrayConverter.cs index bb6799ba..62840df0 100644 --- a/Penumbra/Util/SingleOrArrayConverter.cs +++ b/Penumbra/Util/SingleOrArrayConverter.cs @@ -7,7 +7,8 @@ namespace Penumbra.Util { public class SingleOrArrayConverter< T > : JsonConverter { - public override bool CanConvert( Type objectType ) => objectType == typeof( HashSet< T > ); + public override bool CanConvert( Type objectType ) + => objectType == typeof( HashSet< T > ); public override object ReadJson( JsonReader reader, Type objectType, object? existingValue, JsonSerializer serializer ) { @@ -15,7 +16,7 @@ namespace Penumbra.Util if( token.Type == JTokenType.Array ) { - return token.ToObject< HashSet< T > >() ?? new HashSet(); + return token.ToObject< HashSet< T > >() ?? new HashSet< T >(); } var tmp = token.ToObject< T >(); @@ -24,7 +25,8 @@ namespace Penumbra.Util : new HashSet< T >(); } - public override bool CanWrite => true; + public override bool CanWrite + => true; public override void WriteJson( JsonWriter writer, object? value, JsonSerializer serializer ) { @@ -37,6 +39,7 @@ namespace Penumbra.Util serializer.Serialize( writer, val?.ToString() ); } } + writer.WriteEndArray(); } } diff --git a/Penumbra/Util/StringPathExtensions.cs b/Penumbra/Util/StringPathExtensions.cs index b97e9061..9c9f4f5a 100644 --- a/Penumbra/Util/StringPathExtensions.cs +++ b/Penumbra/Util/StringPathExtensions.cs @@ -1,18 +1,17 @@ using System.IO; -using System.Linq; using System.Text; -namespace Penumbra +namespace Penumbra.Util { public static class StringPathExtensions { - private static readonly char[] _invalid = Path.GetInvalidFileNameChars(); + private static readonly char[] Invalid = Path.GetInvalidFileNameChars(); public static string ReplaceInvalidPathSymbols( this string s, string replacement = "_" ) - => string.Join( replacement, s.Split( _invalid ) ); + => string.Join( replacement, s.Split( Invalid ) ); public static string RemoveInvalidPathSymbols( this string s ) - => string.Concat( s.Split( _invalid ) ); + => string.Concat( s.Split( Invalid ) ); public static string RemoveNonAsciiSymbols( this string s, string replacement = "_" ) { diff --git a/Penumbra/Util/TempFile.cs b/Penumbra/Util/TempFile.cs index 0c4cb2d2..0b6b8e79 100644 --- a/Penumbra/Util/TempFile.cs +++ b/Penumbra/Util/TempFile.cs @@ -28,4 +28,4 @@ namespace Penumbra.Util return fileName; } } -} +} \ No newline at end of file