Complete refactoring of most code, indiscriminate application of .editorconfig and general cleanup.

This commit is contained in:
Ottermandias 2021-06-19 11:53:54 +02:00
parent 5332119a63
commit a19ec226c5
84 changed files with 3168 additions and 1709 deletions

View file

@ -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
return modManager.CurrentCollection.Cache?.AvailableMods.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 )
} );
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,
return modManager.CurrentCollection.Cache?.ResolvedFiles.ToDictionary(
o => ( string )o.Key,
o => o.Value.FullName
);
)
?? new Dictionary< string, string >();
}
}
}

View file

@ -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() );
}
}

View file

@ -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 },
};
}
}

View file

@ -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 },
};
}
}

View file

@ -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 },
};
}
}

View file

@ -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 },
};
}
}

View file

@ -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,
}
}

View file

@ -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,
};
}
}

View file

@ -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,
};
}
}

View file

@ -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,
};
}
}

View file

@ -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 )]

View file

@ -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,
};
}
@ -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 );
}

View file

@ -1,5 +1,5 @@
using System.IO;
using Penumbra.Mods;
using Penumbra.Meta;
namespace Penumbra.Game
{

View file

@ -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 );
}

View file

@ -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 ] );
}

View file

@ -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;
}
}

View file

@ -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; }
@ -47,6 +48,7 @@ namespace Penumbra.Hooks
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,
@ -141,24 +145,29 @@ namespace Penumbra.Hooks
bool isUnknown
)
{
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 :(

View file

@ -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;
}
}
}

View file

@ -5,6 +5,6 @@ namespace Penumbra.Importer
None,
WritingPackToDisk,
ExtractingModFiles,
Done
Done,
}
}

View file

@ -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()
{

View file

@ -1,5 +1,5 @@
using System.Collections.Generic;
using Penumbra.Models;
using Penumbra.Structs;
namespace Penumbra.Importer.Models
{

View file

@ -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 ) )

View file

@ -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
{

View file

@ -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 )
{

View file

@ -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 );

View file

@ -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]

View file

@ -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 );

View file

@ -2,12 +2,10 @@ 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
{
@ -18,6 +16,19 @@ namespace Penumbra.MetaData
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 );

View file

@ -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(),
};
}
}

View file

@ -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
{

149
Penumbra/Meta/Identifier.cs Normal file
View file

@ -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}",
};
}
}
}

View file

@ -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;
}
}
}
}

View file

@ -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();
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();
_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
{

View file

@ -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
public override void WriteJson( JsonWriter writer, MetaManipulation manip, JsonSerializer serializer )
{
[FieldOffset( 0 )]
public ulong Value;
var s = Convert.ToBase64String( manip.ToBytes() );
writer.WriteValue( s );
}
[FieldOffset( 0 )]
public MetaType Type;
public override MetaManipulation ReadJson( JsonReader reader, Type objectType, MetaManipulation existingValue, bool hasExistingValue,
JsonSerializer serializer )
[FieldOffset( 1 )]
public EquipSlot Slot;
{
if( reader.TokenType != JsonToken.String )
{
throw new JsonReaderException();
}
[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
{
get => ( ObjectType )( _objectAndBody & 0b00011111 );
set => _objectAndBody = ( byte )( ( _objectAndBody & 0b11100000 ) | ( byte )value );
}
public BodySlot BodySlot
{
get => ( BodySlot )( _objectAndBody & 0b11100000 );
set => _objectAndBody = ( byte )( ( _objectAndBody & 0b00011111 ) | ( byte )value );
}
[FieldOffset( 2 )]
public ushort PrimaryId;
[FieldOffset( 4 )]
public ushort Variant;
[FieldOffset( 6 )]
public ushort SecondaryId;
[FieldOffset( 6 )]
public EquipSlot EquipSlot;
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 );
}
}
[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(),
};
}
}
}

View file

@ -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;
}
}
}
}

32
Penumbra/Mod/Mod.cs Normal file
View file

@ -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;
}
}

57
Penumbra/Mod/ModCache.cs Normal file
View file

@ -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;
} );
}
}
}

View file

@ -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 ) );
@ -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 );
}
}
}

59
Penumbra/Mod/ModData.cs Normal file
View file

@ -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 );
}
}

View file

@ -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;
}
}
}

103
Penumbra/Mod/ModMeta.cs Normal file
View file

@ -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}" );
}
}
}
}

View file

@ -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;
}
}
}

View file

@ -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 ) );
}
}
}

View file

@ -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 )
{

View file

@ -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;
}
}

View file

@ -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 );
}
}

View file

@ -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 );
}
}
}

View file

@ -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 ) );
}
}
}

View file

@ -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();
Name = DefaultCollection;
Settings = new Dictionary< string, ModSettings >();
}
catch( Exception e )
public ModCollection( string name, Dictionary< string, ModSettings > settings )
{
PluginLog.Error( $"failed to read log collection information, failed path: {collectionPath}, err: {e.Message}" );
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 );
}
}
#if DEBUG
if( ModSettings != null )
foreach( var s in removeList )
{
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;
Settings.Remove( s );
}
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;
return removeList.Count > 0;
}
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 )
public void CreateCache( DirectoryInfo modDirectory, Dictionary< string, ModData > data, bool cleanUnavailable = false )
{
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();
}
public void Save()
Cache = new ModCollectionCache( Name, modDirectory );
var changedSettings = false;
foreach( var modKvp in data )
{
var collectionPath = Path.Combine( _basePath.FullName, "collection.json" );
try
if( Settings.TryGetValue( modKvp.Key, out var settings ) )
{
var data = JsonConvert.SerializeObject( ModSettings.OrderBy( x => x.Priority ).ToList() );
File.WriteAllText( collectionPath, data );
}
catch( Exception e )
{
PluginLog.Error( $"failed to write log collection information, failed path: {collectionPath}, err: {e.Message}" );
}
}
private int CleanPriority( int priority )
=> priority < 0 ? 0 : priority >= ModSettings!.Count ? ModSettings.Count - 1 : priority;
public void ReorderMod( ModInfo info, int newPriority )
{
if( ModSettings == null )
{
return;
}
var oldPriority = info.Priority;
newPriority = CleanPriority( newPriority );
if( oldPriority == newPriority )
{
return;
}
info.Priority = newPriority;
if( newPriority < oldPriority )
{
for( var i = oldPriority - 1; i >= newPriority; --i )
{
++ModSettings![ i ].Priority;
ModSettings.Swap( i, i + 1 );
}
Cache.AvailableMods.Add( new Mod.Mod( settings, modKvp.Value ) );
}
else
{
for( var i = oldPriority + 1; i <= newPriority; ++i )
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 )
{
--ModSettings![ i ].Priority;
ModSettings.Swap( i - 1, i );
}
changedSettings |= CleanUnavailableSettings( data );
}
EnabledMods = GetOrderedAndEnabledModList().ToArray();
Save();
}
public void ReorderMod( ModInfo info, bool up )
=> ReorderMod( info, info.Priority + ( up ? 1 : -1 ) );
public ModInfo? FindModSettings( string name )
if( changedSettings )
{
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;
Save( Service< DalamudPluginInterface >.Get() );
}
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;
Cache.SortMods();
CalculateEffectiveFileList( modDirectory, true );
}
public ModInfo FindOrCreateModSettings( ResourceMod mod )
public void UpdateSetting( ModData mod )
{
var settings = FindModSettings( mod.ModBasePath.Name );
if( settings == null )
if( !Settings.TryGetValue( mod.BasePath.Name, out var settings ) )
{
return AddModSettings( mod );
return;
}
settings.Mod = mod;
settings.FixInvalidSettings();
return settings;
}
public IEnumerable< ModInfo > GetOrderedAndEnabledModSettings( bool invertOrder = false )
if( settings.FixInvalidSettings( mod.Meta ) )
{
var query = ModSettings?
.Where( x => x.Enabled )
?? Enumerable.Empty< ModInfo >();
Save( Service< DalamudPluginInterface >.Get() );
}
}
if( !invertOrder )
public void UpdateSettings()
{
return query.OrderBy( x => x.Priority );
}
return query.OrderByDescending( x => x.Priority );
}
public IEnumerable< ResourceMod > GetOrderedAndEnabledModList( bool invertOrder = false )
if( Cache == null )
{
return GetOrderedAndEnabledModSettings( invertOrder )
.Select( x => x.Mod );
return;
}
public IEnumerable< (ResourceMod, ModInfo) > GetOrderedAndEnabledModListWithSettings( bool invertOrder = false )
var changes = false;
foreach( var mod in Cache.AvailableMods )
{
return GetOrderedAndEnabledModSettings( invertOrder )
.Select( x => ( x.Mod, x ) );
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 collection = JsonConvert.DeserializeObject< ModCollection >( File.ReadAllText( file.FullName ) );
return collection;
}
catch( Exception e )
{
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}" );
}
}
public static DirectoryInfo CollectionDir( DalamudPluginInterface pi )
=> new( Path.Combine( pi.GetPluginConfigDirectory(), "collections" ) );
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 )
{
try
{
var dir = CollectionDir( pi );
dir.Create();
var file = FileName( dir, Name );
SaveToFile( file );
}
catch( Exception e )
{
PluginLog.Error( $"Could not save collection {Name}:\n{e}" );
}
}
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 )
{
try
{
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
{
Cache.AddMod( ModSettings.DefaultSettings( data.Meta ), data );
}
}
public string? ResolveSwappedOrReplacementPath( GamePath gameResourcePath )
=> Cache?.ResolveSwappedOrReplacementPath( gameResourcePath );
}
}

View file

@ -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;
}
}

View file

@ -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;
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()
if( SetCurrentCollection( ModCollection.DefaultCollection ) )
{
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 ) )
{
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 );
}
return true;
}
return false;
}
public void ReadCollections()
{
var collectionDir = ModCollection.CollectionDir( _plugin.PluginInterface! );
if( collectionDir.Exists )
{
foreach( var file in collectionDir.EnumerateFiles( "*.json" ) )
{
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
{
AddFiles( gamePaths, file, registeredFiles, mod );
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( !ResolvedFiles.ContainsKey( gamePath ) )
{
ResolvedFiles[ gamePath ] = file;
registeredFiles[ gamePath ] = mod.Meta.Name;
}
else if( registeredFiles.TryGetValue( gamePath, out var modName ) )
{
mod.AddConflict( modName, gamePath );
}
}
}
private void AddManipulations( FileInfo file, ResourceMod mod )
if( !Collections.ContainsKey( ModCollection.DefaultCollection ) )
{
if( !mod.MetaManipulations.TryGetValue( file, out var meta ) )
{
PluginLog.Error( $"{file.FullName} is a TexTools Meta File without meta information." );
return;
}
foreach( var manipulation in meta.Manipulations )
{
MetaManipulations!.ApplyMod( manipulation );
var defaultCollection = new ModCollection();
SaveCollection( defaultCollection );
Collections.Add( defaultCollection.Name, defaultCollection );
}
}
public void ChangeModPriority( ModInfo info, int newPriority )
public void SaveCollection( ModCollection collection )
=> collection.Save( _plugin.PluginInterface! );
public bool AddCollection( string name, Dictionary< string, ModSettings > settings )
{
Mods!.ReorderMod( info, newPriority );
CalculateEffectiveFileList();
var nameFixed = name.RemoveInvalidPathSymbols().ToLowerInvariant();
if( Collections.Values.Any( c => c.Name.RemoveInvalidPathSymbols().ToLowerInvariant() == nameFixed ) )
{
PluginLog.Warning( $"The new collection {name} would lead to the same path as one that already exists." );
return false;
}
public void ChangeModPriority( ModInfo info, bool up = false )
{
Mods!.ReorderMod( info, up );
CalculateEffectiveFileList();
var newCollection = new ModCollection( name, settings );
Collections.Add( name, newCollection );
SaveCollection( newCollection );
CurrentCollection = newCollection;
return true;
}
public void DeleteMod( ResourceMod? mod )
public bool RemoveCollection( string name )
{
if( mod?.ModBasePath.Exists ?? false )
if( name == ModCollection.DefaultCollection )
{
try
{
Directory.Delete( mod.ModBasePath.FullName, true );
}
catch( Exception e )
{
PluginLog.Error( $"Could not delete the mod {mod.ModBasePath.Name}:\n{e}" );
}
PluginLog.Error( "Can not remove the default collection." );
return false;
}
if( Collections.TryGetValue( name, out var collection ) )
{
if( CurrentCollection == collection )
{
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;
}
return candidate;
Mods.Add( modFolder.Name, mod );
}
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()
foreach( var collection in Collections.Values.Where( c => c.Cache != null ) )
{
MetaManipulations?.Dispose();
// _fileSystemWatcher?.Dispose();
collection.CreateCache( BasePath, Mods );
}
}
public void DeleteMod( DirectoryInfo modFolder )
{
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 )

View file

@ -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;
}
}
}

View file

@ -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 };
}
}
}

View file

@ -6,9 +6,10 @@ 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
{
@ -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();
@ -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();
}
}
}

View file

@ -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,
}
}

View file

@ -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." ),
};
}
}
}

View file

@ -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 )
{

View file

@ -1,6 +1,6 @@
using ImGuiNET;
namespace Penumbra.UI
namespace Penumbra.UI.Custom
{
public static partial class ImGuiCustom
{

View file

@ -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 )
{

View file

@ -1,7 +1,7 @@
using System.Windows.Forms;
using ImGuiNET;
namespace Penumbra.UI
namespace Penumbra.UI.Custom
{
public static partial class ImGuiCustom
{

View file

@ -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()
{

View file

@ -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();
}
}
}
}

View file

@ -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();
}

View file

@ -6,6 +6,7 @@ using System.Windows.Forms;
using Dalamud.Plugin;
using ImGuiNET;
using Penumbra.Importer;
using Penumbra.Util;
namespace Penumbra.UI
{
@ -25,26 +26,30 @@ 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 () =>
{
try
{
var picker = new OpenFileDialog
{
Multiselect = true,
Filter = FileTypeFilter,
CheckFileExists = true,
Title = LabelFileDialog
Title = LabelFileDialog,
};
var result = await picker.ShowDialogAsync();
@ -59,7 +64,7 @@ namespace Penumbra.UI
try
{
_texToolsImport = new TexToolsImport( new DirectoryInfo( _base._plugin!.Configuration!.CurrentCollection ) );
_texToolsImport = new TexToolsImport( new DirectoryInfo( _base._plugin!.Configuration!.ModDirectory ) );
_texToolsImport.ImportModPack( new FileInfo( fileName ) );
PluginLog.Log( $"-> {fileName} OK!" );
@ -74,6 +79,11 @@ namespace Penumbra.UI
_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();
}
}

View file

@ -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;
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();

View file

@ -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();
}
}

View file

@ -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;
_modManager.ChangeModGroup( group.GroupName, groupName, Mod.Data );
}
return true;
}
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,134 +167,74 @@ namespace Penumbra.UI
DrawMultiSelectorEditAdd( group, nameBoxStart );
if( modChanged )
{
_selector.SaveCurrentMod();
Save();
Custom.ImGuiCustom.EndFramedGroup();
}
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 );
_modManager.ChangeModGroup( group.GroupName, groupName, Mod.Data );
}
if( groupName.Length > 0 )
{
Meta.Groups.Add( groupName, new OptionGroup()
{
GroupName = groupName,
Options = group.Options,
SelectionType = SelectType.Single,
} );
Mod.Settings[ groupName ] = oldConf;
}
return true;
}
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 );
}
if( code != oldSetting )
{
Save();
}
ImGui.SameLine();
var labelEditPos = ImGui.GetCursorPosX();
modChanged |= DrawSingleSelectorEditGroup( group, ref selectionChanged );
if( modChanged )
{
_selector.SaveCurrentMod();
}
if( selectionChanged )
{
Save();
}
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 = "";
ImGui.SetCursorPosX( labelEditPos );
if( labelEditPos == CheckMarkSize )
@ -323,9 +242,10 @@ namespace Penumbra.UI
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();
}

View file

@ -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();

View file

@ -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
{
[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 ModFilter UnfilteredStateMods = ( ModFilter )( ( 1 << 13 ) - 1 );
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 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:";
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( 60, 0 );
private static readonly string ArrowUpString = FontAwesomeIcon.ArrowUp.ToIconString();
private static readonly string ArrowDownString = FontAwesomeIcon.ArrowDown.ToIconString();
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;
_modNamesLower = Array.Empty< string >();
_modManager = Service<ModManager>.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();
}
}
}

View file

@ -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";
@ -33,10 +36,10 @@ 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;
_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;
@ -91,10 +83,22 @@ 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 )
{
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,7 +131,7 @@ namespace Penumbra.UI
}
}
private void DrawReloadResourceButton()
private static void DrawReloadResourceButton()
{
if( ImGui.Button( LabelReloadResource ) )
{
@ -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 )

View file

@ -1,6 +1,7 @@
using System.IO;
using System.Numerics;
using Penumbra.Mods;
using Penumbra.Util;
namespace Penumbra.UI
{
@ -25,7 +26,8 @@ namespace Penumbra.UI
_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();
}
}

View file

@ -16,8 +16,9 @@ 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 )
{
@ -26,7 +27,8 @@ namespace Penumbra.UI
_importTab = new TabImport( _base );
_browserTab = new TabBrowser();
InstalledTab = new TabInstalled( _base );
EffectiveTab = new TabEffective();
_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();
}
}

View file

@ -1,7 +1,7 @@
using System;
using System.Collections.Generic;
namespace Penumbra
namespace Penumbra.Util
{
public static class ArrayExtensions
{

View file

@ -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,7 +36,7 @@ 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 );
@ -57,7 +55,7 @@ namespace Penumbra.Util
// im a pirate arr
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 );
@ -76,9 +74,7 @@ namespace Penumbra.Util
/// <param name="offset">The offset to read a string starting from.</param>
/// <returns></returns>
public static string ReadStringOffsetData( this BinaryReader br, long offset )
{
return Encoding.UTF8.GetString( ReadRawOffsetData( br, offset ) );
}
=> Encoding.UTF8.GetString( ReadRawOffsetData( br, offset ) );
/// <summary>
/// Moves the BinaryReader position to offset, reads raw bytes until a null byte, then
@ -108,7 +104,8 @@ namespace Penumbra.Util
/// <summary>
/// Seeks this BinaryReader's position to the given offset. Syntactic sugar.
/// </summary>
public static void Seek( this BinaryReader br, long offset ) {
public static void Seek( this BinaryReader br, long offset )
{
br.BaseStream.Position = offset;
}

View file

@ -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 );
}
}
}

View file

@ -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

View file

@ -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
{

View file

@ -1,6 +1,6 @@
using System;
namespace Penumbra
namespace Penumbra.Util
{
/// <summary>
/// Basic service locator
@ -11,8 +11,7 @@ namespace Penumbra
private static T? _object;
static Service()
{
}
{ }
public static void Set( T obj )
{

View file

@ -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 )
{
@ -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();
}
}

View file

@ -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 = "_" )
{