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