mirror of
https://github.com/xivdev/Penumbra.git
synced 2025-12-13 20:24:17 +01:00
Some mod movement.
This commit is contained in:
parent
c12dbf3f8a
commit
577669b21f
26 changed files with 726 additions and 732 deletions
|
|
@ -828,7 +828,7 @@ public class PenumbraApi : IDisposable, IPenumbraApi
|
||||||
public PenumbraApiEc CreateNamedTemporaryCollection(string name)
|
public PenumbraApiEc CreateNamedTemporaryCollection(string name)
|
||||||
{
|
{
|
||||||
CheckInitialized();
|
CheckInitialized();
|
||||||
if (name.Length == 0 || Mod.Creator.ReplaceBadXivSymbols(name) != name)
|
if (name.Length == 0 || ModCreator.ReplaceBadXivSymbols(name) != name)
|
||||||
return PenumbraApiEc.InvalidArgument;
|
return PenumbraApiEc.InvalidArgument;
|
||||||
|
|
||||||
return _tempCollections.CreateTemporaryCollection(name).Length > 0
|
return _tempCollections.CreateTemporaryCollection(name).Length > 0
|
||||||
|
|
|
||||||
|
|
@ -11,6 +11,7 @@ using OtterGui.Widgets;
|
||||||
using Penumbra.GameData.Enums;
|
using Penumbra.GameData.Enums;
|
||||||
using Penumbra.Import.Structs;
|
using Penumbra.Import.Structs;
|
||||||
using Penumbra.Mods;
|
using Penumbra.Mods;
|
||||||
|
using Penumbra.Mods.Manager;
|
||||||
using Penumbra.Services;
|
using Penumbra.Services;
|
||||||
using Penumbra.UI;
|
using Penumbra.UI;
|
||||||
using Penumbra.UI.Classes;
|
using Penumbra.UI.Classes;
|
||||||
|
|
|
||||||
|
|
@ -45,7 +45,7 @@ public partial class TexToolsImporter
|
||||||
};
|
};
|
||||||
Penumbra.Log.Information( $" -> Importing {archive.Type} Archive." );
|
Penumbra.Log.Information( $" -> Importing {archive.Type} Archive." );
|
||||||
|
|
||||||
_currentModDirectory = Mod.Creator.CreateModFolder( _baseDirectory, Path.GetRandomFileName() );
|
_currentModDirectory = ModCreator.CreateModFolder( _baseDirectory, Path.GetRandomFileName() );
|
||||||
var options = new ExtractionOptions()
|
var options = new ExtractionOptions()
|
||||||
{
|
{
|
||||||
ExtractFullPath = true,
|
ExtractFullPath = true,
|
||||||
|
|
@ -100,18 +100,18 @@ public partial class TexToolsImporter
|
||||||
// Use either the top-level directory as the mods base name, or the (fixed for path) name in the json.
|
// Use either the top-level directory as the mods base name, or the (fixed for path) name in the json.
|
||||||
if( leadDir )
|
if( leadDir )
|
||||||
{
|
{
|
||||||
_currentModDirectory = Mod.Creator.CreateModFolder( _baseDirectory, baseName, false );
|
_currentModDirectory = ModCreator.CreateModFolder( _baseDirectory, baseName, false );
|
||||||
Directory.Move( Path.Combine( oldName, baseName ), _currentModDirectory.FullName );
|
Directory.Move( Path.Combine( oldName, baseName ), _currentModDirectory.FullName );
|
||||||
Directory.Delete( oldName );
|
Directory.Delete( oldName );
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
_currentModDirectory = Mod.Creator.CreateModFolder( _baseDirectory, name, false );
|
_currentModDirectory = ModCreator.CreateModFolder( _baseDirectory, name, false );
|
||||||
Directory.Move( oldName, _currentModDirectory.FullName );
|
Directory.Move( oldName, _currentModDirectory.FullName );
|
||||||
}
|
}
|
||||||
|
|
||||||
_currentModDirectory.Refresh();
|
_currentModDirectory.Refresh();
|
||||||
Mod.Creator.SplitMultiGroups( _currentModDirectory );
|
ModCreator.SplitMultiGroups( _currentModDirectory );
|
||||||
|
|
||||||
return _currentModDirectory;
|
return _currentModDirectory;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -33,14 +33,14 @@ public partial class TexToolsImporter
|
||||||
|
|
||||||
var modList = modListRaw.Select( m => JsonConvert.DeserializeObject< SimpleMod >( m, JsonSettings )! ).ToList();
|
var modList = modListRaw.Select( m => JsonConvert.DeserializeObject< SimpleMod >( m, JsonSettings )! ).ToList();
|
||||||
|
|
||||||
_currentModDirectory = Mod.Creator.CreateModFolder( _baseDirectory, Path.GetFileNameWithoutExtension( modPackFile.Name ) );
|
_currentModDirectory = ModCreator.CreateModFolder( _baseDirectory, Path.GetFileNameWithoutExtension( modPackFile.Name ) );
|
||||||
// Create a new ModMeta from the TTMP mod list info
|
// Create a new ModMeta from the TTMP mod list info
|
||||||
_modManager.DataEditor.CreateMeta( _currentModDirectory, _currentModName, DefaultTexToolsData.Author, DefaultTexToolsData.Description, null, null );
|
_modManager.DataEditor.CreateMeta( _currentModDirectory, _currentModName, DefaultTexToolsData.Author, DefaultTexToolsData.Description, null, null );
|
||||||
|
|
||||||
// Open the mod data file from the mod pack as a SqPackStream
|
// Open the mod data file from the mod pack as a SqPackStream
|
||||||
_streamDisposer = GetSqPackStreamStream( extractedModPack, "TTMPD.mpd" );
|
_streamDisposer = GetSqPackStreamStream( extractedModPack, "TTMPD.mpd" );
|
||||||
ExtractSimpleModList( _currentModDirectory, modList );
|
ExtractSimpleModList( _currentModDirectory, modList );
|
||||||
Mod.Creator.CreateDefaultFiles( _currentModDirectory );
|
ModCreator.CreateDefaultFiles( _currentModDirectory );
|
||||||
ResetStreamDisposer();
|
ResetStreamDisposer();
|
||||||
return _currentModDirectory;
|
return _currentModDirectory;
|
||||||
}
|
}
|
||||||
|
|
@ -89,7 +89,7 @@ public partial class TexToolsImporter
|
||||||
_currentOptionName = DefaultTexToolsData.DefaultOption;
|
_currentOptionName = DefaultTexToolsData.DefaultOption;
|
||||||
Penumbra.Log.Information( " -> Importing Simple V2 ModPack" );
|
Penumbra.Log.Information( " -> Importing Simple V2 ModPack" );
|
||||||
|
|
||||||
_currentModDirectory = Mod.Creator.CreateModFolder( _baseDirectory, _currentModName );
|
_currentModDirectory = ModCreator.CreateModFolder( _baseDirectory, _currentModName );
|
||||||
_modManager.DataEditor.CreateMeta( _currentModDirectory, _currentModName, modList.Author, string.IsNullOrEmpty( modList.Description )
|
_modManager.DataEditor.CreateMeta( _currentModDirectory, _currentModName, modList.Author, string.IsNullOrEmpty( modList.Description )
|
||||||
? "Mod imported from TexTools mod pack"
|
? "Mod imported from TexTools mod pack"
|
||||||
: modList.Description, modList.Version, modList.Url );
|
: modList.Description, modList.Version, modList.Url );
|
||||||
|
|
@ -97,7 +97,7 @@ public partial class TexToolsImporter
|
||||||
// Open the mod data file from the mod pack as a SqPackStream
|
// Open the mod data file from the mod pack as a SqPackStream
|
||||||
_streamDisposer = GetSqPackStreamStream( extractedModPack, "TTMPD.mpd" );
|
_streamDisposer = GetSqPackStreamStream( extractedModPack, "TTMPD.mpd" );
|
||||||
ExtractSimpleModList( _currentModDirectory, modList.SimpleModsList );
|
ExtractSimpleModList( _currentModDirectory, modList.SimpleModsList );
|
||||||
Mod.Creator.CreateDefaultFiles( _currentModDirectory );
|
ModCreator.CreateDefaultFiles( _currentModDirectory );
|
||||||
ResetStreamDisposer();
|
ResetStreamDisposer();
|
||||||
return _currentModDirectory;
|
return _currentModDirectory;
|
||||||
}
|
}
|
||||||
|
|
@ -134,7 +134,7 @@ public partial class TexToolsImporter
|
||||||
_currentNumOptions = GetOptionCount( modList );
|
_currentNumOptions = GetOptionCount( modList );
|
||||||
_currentModName = modList.Name;
|
_currentModName = modList.Name;
|
||||||
|
|
||||||
_currentModDirectory = Mod.Creator.CreateModFolder( _baseDirectory, _currentModName );
|
_currentModDirectory = ModCreator.CreateModFolder( _baseDirectory, _currentModName );
|
||||||
_modManager.DataEditor.CreateMeta( _currentModDirectory, _currentModName, modList.Author, modList.Description, modList.Version, modList.Url );
|
_modManager.DataEditor.CreateMeta( _currentModDirectory, _currentModName, modList.Author, modList.Description, modList.Version, modList.Url );
|
||||||
|
|
||||||
if( _currentNumOptions == 0 )
|
if( _currentNumOptions == 0 )
|
||||||
|
|
@ -172,7 +172,7 @@ public partial class TexToolsImporter
|
||||||
{
|
{
|
||||||
var name = numGroups == 1 ? _currentGroupName : $"{_currentGroupName}, Part {groupId + 1}";
|
var name = numGroups == 1 ? _currentGroupName : $"{_currentGroupName}, Part {groupId + 1}";
|
||||||
options.Clear();
|
options.Clear();
|
||||||
var groupFolder = Mod.Creator.NewSubFolderName( _currentModDirectory, name )
|
var groupFolder = ModCreator.NewSubFolderName( _currentModDirectory, name )
|
||||||
?? new DirectoryInfo( Path.Combine( _currentModDirectory.FullName,
|
?? new DirectoryInfo( Path.Combine( _currentModDirectory.FullName,
|
||||||
numGroups == 1 ? $"Group {groupPriority + 1}" : $"Group {groupPriority + 1}, Part {groupId + 1}" ) );
|
numGroups == 1 ? $"Group {groupPriority + 1}" : $"Group {groupPriority + 1}, Part {groupId + 1}" ) );
|
||||||
|
|
||||||
|
|
@ -182,10 +182,10 @@ public partial class TexToolsImporter
|
||||||
var option = allOptions[ i + optionIdx ];
|
var option = allOptions[ i + optionIdx ];
|
||||||
_token.ThrowIfCancellationRequested();
|
_token.ThrowIfCancellationRequested();
|
||||||
_currentOptionName = option.Name;
|
_currentOptionName = option.Name;
|
||||||
var optionFolder = Mod.Creator.NewSubFolderName( groupFolder, option.Name )
|
var optionFolder = ModCreator.NewSubFolderName( groupFolder, option.Name )
|
||||||
?? new DirectoryInfo( Path.Combine( groupFolder.FullName, $"Option {i + optionIdx + 1}" ) );
|
?? new DirectoryInfo( Path.Combine( groupFolder.FullName, $"Option {i + optionIdx + 1}" ) );
|
||||||
ExtractSimpleModList( optionFolder, option.ModsJsons );
|
ExtractSimpleModList( optionFolder, option.ModsJsons );
|
||||||
options.Add( Mod.Creator.CreateSubMod( _currentModDirectory, optionFolder, option ) );
|
options.Add( ModCreator.CreateSubMod( _currentModDirectory, optionFolder, option ) );
|
||||||
if( option.IsChecked )
|
if( option.IsChecked )
|
||||||
{
|
{
|
||||||
defaultSettings = group.SelectionType == GroupType.Multi
|
defaultSettings = group.SelectionType == GroupType.Multi
|
||||||
|
|
@ -206,12 +206,12 @@ public partial class TexToolsImporter
|
||||||
if( empty != null )
|
if( empty != null )
|
||||||
{
|
{
|
||||||
_currentOptionName = empty.Name;
|
_currentOptionName = empty.Name;
|
||||||
options.Insert( 0, Mod.Creator.CreateEmptySubMod( empty.Name ) );
|
options.Insert( 0, ModCreator.CreateEmptySubMod( empty.Name ) );
|
||||||
defaultSettings = defaultSettings == null ? 0 : defaultSettings.Value + 1;
|
defaultSettings = defaultSettings == null ? 0 : defaultSettings.Value + 1;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Mod.Creator.CreateOptionGroup( _currentModDirectory, group.SelectionType, name, groupPriority, groupPriority,
|
ModCreator.CreateOptionGroup( _currentModDirectory, group.SelectionType, name, groupPriority, groupPriority,
|
||||||
defaultSettings ?? 0, group.Description, options );
|
defaultSettings ?? 0, group.Description, options );
|
||||||
++groupPriority;
|
++groupPriority;
|
||||||
}
|
}
|
||||||
|
|
@ -219,7 +219,7 @@ public partial class TexToolsImporter
|
||||||
}
|
}
|
||||||
|
|
||||||
ResetStreamDisposer();
|
ResetStreamDisposer();
|
||||||
Mod.Creator.CreateDefaultFiles( _currentModDirectory );
|
ModCreator.CreateDefaultFiles( _currentModDirectory );
|
||||||
return _currentModDirectory;
|
return _currentModDirectory;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@ using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
|
using Penumbra.Mods.Manager;
|
||||||
using Penumbra.String.Classes;
|
using Penumbra.String.Classes;
|
||||||
|
|
||||||
namespace Penumbra.Mods;
|
namespace Penumbra.Mods;
|
||||||
|
|
|
||||||
|
|
@ -181,10 +181,10 @@ public class ModNormalizer
|
||||||
for (var i = _redirections[groupIdx + 1].Count; i < group.Count; ++i)
|
for (var i = _redirections[groupIdx + 1].Count; i < group.Count; ++i)
|
||||||
_redirections[groupIdx + 1].Add(new Dictionary<Utf8GamePath, FullPath>());
|
_redirections[groupIdx + 1].Add(new Dictionary<Utf8GamePath, FullPath>());
|
||||||
|
|
||||||
var groupDir = Mod.Creator.CreateModFolder(directory, group.Name);
|
var groupDir = ModCreator.CreateModFolder(directory, group.Name);
|
||||||
foreach (var option in group.OfType<SubMod>())
|
foreach (var option in group.OfType<SubMod>())
|
||||||
{
|
{
|
||||||
var optionDir = Mod.Creator.CreateModFolder(groupDir, option.Name);
|
var optionDir = ModCreator.CreateModFolder(groupDir, option.Name);
|
||||||
|
|
||||||
newDict = _redirections[groupIdx + 1][option.OptionIdx];
|
newDict = _redirections[groupIdx + 1][option.OptionIdx];
|
||||||
newDict.Clear();
|
newDict.Clear();
|
||||||
|
|
|
||||||
|
|
@ -10,25 +10,25 @@ using Penumbra.GameData.Enums;
|
||||||
using Penumbra.Meta.Manipulations;
|
using Penumbra.Meta.Manipulations;
|
||||||
using Penumbra.Services;
|
using Penumbra.Services;
|
||||||
|
|
||||||
namespace Penumbra.Mods;
|
namespace Penumbra.Mods.Manager;
|
||||||
|
|
||||||
public class ModCacheManager : IDisposable, IReadOnlyList<ModCache>
|
public class ModCacheManager : IDisposable, IReadOnlyList<ModCache>
|
||||||
{
|
{
|
||||||
private readonly CommunicatorService _communicator;
|
private readonly CommunicatorService _communicator;
|
||||||
private readonly IdentifierService _identifier;
|
private readonly IdentifierService _identifier;
|
||||||
private readonly IReadOnlyList<Mod> _modManager;
|
private readonly IReadOnlyList<Mod> _modManager;
|
||||||
|
|
||||||
private readonly List<ModCache> _cache = new();
|
private readonly List<ModCache> _cache = new();
|
||||||
|
|
||||||
public ModCacheManager(CommunicatorService communicator, IdentifierService identifier, ModManager modManager)
|
public ModCacheManager(CommunicatorService communicator, IdentifierService identifier, ModManager modManager)
|
||||||
{
|
{
|
||||||
_communicator = communicator;
|
_communicator = communicator;
|
||||||
_identifier = identifier;
|
_identifier = identifier;
|
||||||
_modManager = modManager;
|
_modManager = modManager;
|
||||||
|
|
||||||
_communicator.ModOptionChanged.Event += OnModOptionChange;
|
_communicator.ModOptionChanged.Event += OnModOptionChange;
|
||||||
_communicator.ModPathChanged.Event += OnModPathChange;
|
_communicator.ModPathChanged.Event += OnModPathChange;
|
||||||
_communicator.ModDataChanged.Event += OnModDataChange;
|
_communicator.ModDataChanged.Event += OnModDataChange;
|
||||||
_communicator.ModDiscoveryFinished.Event += OnModDiscoveryFinished;
|
_communicator.ModDiscoveryFinished.Event += OnModDiscoveryFinished;
|
||||||
if (!identifier.Valid)
|
if (!identifier.Valid)
|
||||||
identifier.FinishedCreation += OnIdentifierCreation;
|
identifier.FinishedCreation += OnIdentifierCreation;
|
||||||
|
|
@ -51,9 +51,9 @@ public class ModCacheManager : IDisposable, IReadOnlyList<ModCache>
|
||||||
|
|
||||||
public void Dispose()
|
public void Dispose()
|
||||||
{
|
{
|
||||||
_communicator.ModOptionChanged.Event -= OnModOptionChange;
|
_communicator.ModOptionChanged.Event -= OnModOptionChange;
|
||||||
_communicator.ModPathChanged.Event -= OnModPathChange;
|
_communicator.ModPathChanged.Event -= OnModPathChange;
|
||||||
_communicator.ModDataChanged.Event -= OnModDataChange;
|
_communicator.ModDataChanged.Event -= OnModDataChange;
|
||||||
_communicator.ModDiscoveryFinished.Event -= OnModDiscoveryFinished;
|
_communicator.ModDiscoveryFinished.Event -= OnModDiscoveryFinished;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -232,17 +232,17 @@ public class ModCacheManager : IDisposable, IReadOnlyList<ModCache>
|
||||||
|
|
||||||
private static void UpdateCounts(ModCache cache, Mod mod)
|
private static void UpdateCounts(ModCache cache, Mod mod)
|
||||||
{
|
{
|
||||||
cache.TotalFileCount = mod.Default.Files.Count;
|
cache.TotalFileCount = mod.Default.Files.Count;
|
||||||
cache.TotalSwapCount = mod.Default.FileSwaps.Count;
|
cache.TotalSwapCount = mod.Default.FileSwaps.Count;
|
||||||
cache.TotalManipulations = mod.Default.Manipulations.Count;
|
cache.TotalManipulations = mod.Default.Manipulations.Count;
|
||||||
cache.HasOptions = false;
|
cache.HasOptions = false;
|
||||||
foreach (var group in mod.Groups)
|
foreach (var group in mod.Groups)
|
||||||
{
|
{
|
||||||
cache.HasOptions |= group.IsOption;
|
cache.HasOptions |= group.IsOption;
|
||||||
foreach (var s in group)
|
foreach (var s in group)
|
||||||
{
|
{
|
||||||
cache.TotalFileCount += s.Files.Count;
|
cache.TotalFileCount += s.Files.Count;
|
||||||
cache.TotalSwapCount += s.FileSwaps.Count;
|
cache.TotalSwapCount += s.FileSwaps.Count;
|
||||||
cache.TotalManipulations += s.Manipulations.Count;
|
cache.TotalManipulations += s.Manipulations.Count;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,7 @@ using OtterGui.Classes;
|
||||||
using Penumbra.Services;
|
using Penumbra.Services;
|
||||||
using Penumbra.Util;
|
using Penumbra.Util;
|
||||||
|
|
||||||
namespace Penumbra.Mods;
|
namespace Penumbra.Mods.Manager;
|
||||||
|
|
||||||
[Flags]
|
[Flags]
|
||||||
public enum ModDataChangeType : ushort
|
public enum ModDataChangeType : ushort
|
||||||
|
|
@ -29,22 +29,20 @@ public enum ModDataChangeType : ushort
|
||||||
|
|
||||||
public class ModDataEditor
|
public class ModDataEditor
|
||||||
{
|
{
|
||||||
private readonly FilenameService _filenameService;
|
|
||||||
private readonly SaveService _saveService;
|
private readonly SaveService _saveService;
|
||||||
private readonly CommunicatorService _communicatorService;
|
private readonly CommunicatorService _communicatorService;
|
||||||
|
|
||||||
public ModDataEditor(FilenameService filenameService, SaveService saveService, CommunicatorService communicatorService)
|
public ModDataEditor(SaveService saveService, CommunicatorService communicatorService)
|
||||||
{
|
{
|
||||||
_filenameService = filenameService;
|
|
||||||
_saveService = saveService;
|
_saveService = saveService;
|
||||||
_communicatorService = communicatorService;
|
_communicatorService = communicatorService;
|
||||||
}
|
}
|
||||||
|
|
||||||
public string MetaFile(Mod mod)
|
public string MetaFile(Mod mod)
|
||||||
=> _filenameService.ModMetaPath(mod);
|
=> _saveService.FileNames.ModMetaPath(mod);
|
||||||
|
|
||||||
public string DataFile(Mod mod)
|
public string DataFile(Mod mod)
|
||||||
=> _filenameService.LocalDataFile(mod);
|
=> _saveService.FileNames.LocalDataFile(mod);
|
||||||
|
|
||||||
/// <summary> Create the file containing the meta information about a mod from scratch. </summary>
|
/// <summary> Create the file containing the meta information about a mod from scratch. </summary>
|
||||||
public void CreateMeta(DirectoryInfo directory, string? name, string? author, string? description, string? version,
|
public void CreateMeta(DirectoryInfo directory, string? name, string? author, string? description, string? version,
|
||||||
|
|
@ -56,12 +54,12 @@ public class ModDataEditor
|
||||||
mod.Description = description ?? mod.Description;
|
mod.Description = description ?? mod.Description;
|
||||||
mod.Version = version ?? mod.Version;
|
mod.Version = version ?? mod.Version;
|
||||||
mod.Website = website ?? mod.Website;
|
mod.Website = website ?? mod.Website;
|
||||||
_saveService.ImmediateSave(new Mod.ModMeta(mod));
|
_saveService.ImmediateSave(new ModMeta(mod));
|
||||||
}
|
}
|
||||||
|
|
||||||
public ModDataChangeType LoadLocalData(Mod mod)
|
public ModDataChangeType LoadLocalData(Mod mod)
|
||||||
{
|
{
|
||||||
var dataFile = _filenameService.LocalDataFile(mod);
|
var dataFile = _saveService.FileNames.LocalDataFile(mod);
|
||||||
|
|
||||||
var importDate = 0L;
|
var importDate = 0L;
|
||||||
var localTags = Enumerable.Empty<string>();
|
var localTags = Enumerable.Empty<string>();
|
||||||
|
|
@ -98,7 +96,7 @@ public class ModDataEditor
|
||||||
changes |= ModDataChangeType.ImportDate;
|
changes |= ModDataChangeType.ImportDate;
|
||||||
}
|
}
|
||||||
|
|
||||||
changes |= mod.UpdateTags(null, localTags);
|
changes |= ModLocalData.UpdateTags(mod, null, localTags);
|
||||||
|
|
||||||
if (mod.Favorite != favorite)
|
if (mod.Favorite != favorite)
|
||||||
{
|
{
|
||||||
|
|
@ -113,14 +111,14 @@ public class ModDataEditor
|
||||||
}
|
}
|
||||||
|
|
||||||
if (save)
|
if (save)
|
||||||
_saveService.QueueSave(new Mod.ModData(mod));
|
_saveService.QueueSave(new ModLocalData(mod));
|
||||||
|
|
||||||
return changes;
|
return changes;
|
||||||
}
|
}
|
||||||
|
|
||||||
public ModDataChangeType LoadMeta(Mod mod)
|
public ModDataChangeType LoadMeta(Mod mod)
|
||||||
{
|
{
|
||||||
var metaFile = _filenameService.ModMetaPath(mod);
|
var metaFile = _saveService.FileNames.ModMetaPath(mod);
|
||||||
if (!File.Exists(metaFile))
|
if (!File.Exists(metaFile))
|
||||||
{
|
{
|
||||||
Penumbra.Log.Debug($"No mod meta found for {mod.ModPath.Name}.");
|
Penumbra.Log.Debug($"No mod meta found for {mod.ModPath.Name}.");
|
||||||
|
|
@ -137,7 +135,7 @@ public class ModDataEditor
|
||||||
var newDescription = json[nameof(Mod.Description)]?.Value<string>() ?? string.Empty;
|
var newDescription = json[nameof(Mod.Description)]?.Value<string>() ?? string.Empty;
|
||||||
var newVersion = json[nameof(Mod.Version)]?.Value<string>() ?? string.Empty;
|
var newVersion = json[nameof(Mod.Version)]?.Value<string>() ?? string.Empty;
|
||||||
var newWebsite = json[nameof(Mod.Website)]?.Value<string>() ?? string.Empty;
|
var newWebsite = json[nameof(Mod.Website)]?.Value<string>() ?? string.Empty;
|
||||||
var newFileVersion = json[nameof(Mod.ModMeta.FileVersion)]?.Value<uint>() ?? 0;
|
var newFileVersion = json[nameof(ModMeta.FileVersion)]?.Value<uint>() ?? 0;
|
||||||
var importDate = json[nameof(Mod.ImportDate)]?.Value<long>();
|
var importDate = json[nameof(Mod.ImportDate)]?.Value<long>();
|
||||||
var modTags = json[nameof(Mod.ModTags)]?.Values<string>().OfType<string>();
|
var modTags = json[nameof(Mod.ModTags)]?.Values<string>().OfType<string>();
|
||||||
|
|
||||||
|
|
@ -172,14 +170,12 @@ public class ModDataEditor
|
||||||
mod.Website = newWebsite;
|
mod.Website = newWebsite;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (newFileVersion != Mod.ModMeta.FileVersion)
|
if (newFileVersion != ModMeta.FileVersion)
|
||||||
{
|
if (ModMigration.Migrate(_saveService, mod, json, ref newFileVersion))
|
||||||
if (Mod.Migration.Migrate(mod, json, ref newFileVersion))
|
|
||||||
{
|
{
|
||||||
changes |= ModDataChangeType.Migration;
|
changes |= ModDataChangeType.Migration;
|
||||||
_saveService.ImmediateSave(new Mod.ModMeta(mod));
|
_saveService.ImmediateSave(new ModMeta(mod));
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
if (importDate != null && mod.ImportDate != importDate.Value)
|
if (importDate != null && mod.ImportDate != importDate.Value)
|
||||||
{
|
{
|
||||||
|
|
@ -187,7 +183,7 @@ public class ModDataEditor
|
||||||
changes |= ModDataChangeType.ImportDate;
|
changes |= ModDataChangeType.ImportDate;
|
||||||
}
|
}
|
||||||
|
|
||||||
changes |= mod.UpdateTags(modTags, null);
|
changes |= ModLocalData.UpdateTags(mod, modTags, null);
|
||||||
|
|
||||||
return changes;
|
return changes;
|
||||||
}
|
}
|
||||||
|
|
@ -205,7 +201,7 @@ public class ModDataEditor
|
||||||
|
|
||||||
var oldName = mod.Name;
|
var oldName = mod.Name;
|
||||||
mod.Name = newName;
|
mod.Name = newName;
|
||||||
_saveService.QueueSave(new Mod.ModMeta(mod));
|
_saveService.QueueSave(new ModMeta(mod));
|
||||||
_communicatorService.ModDataChanged.Invoke(ModDataChangeType.Name, mod, oldName.Text);
|
_communicatorService.ModDataChanged.Invoke(ModDataChangeType.Name, mod, oldName.Text);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -215,7 +211,7 @@ public class ModDataEditor
|
||||||
return;
|
return;
|
||||||
|
|
||||||
mod.Author = newAuthor;
|
mod.Author = newAuthor;
|
||||||
_saveService.QueueSave(new Mod.ModMeta(mod));
|
_saveService.QueueSave(new ModMeta(mod));
|
||||||
_communicatorService.ModDataChanged.Invoke(ModDataChangeType.Author, mod, null);
|
_communicatorService.ModDataChanged.Invoke(ModDataChangeType.Author, mod, null);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -225,7 +221,7 @@ public class ModDataEditor
|
||||||
return;
|
return;
|
||||||
|
|
||||||
mod.Description = newDescription;
|
mod.Description = newDescription;
|
||||||
_saveService.QueueSave(new Mod.ModMeta(mod));
|
_saveService.QueueSave(new ModMeta(mod));
|
||||||
_communicatorService.ModDataChanged.Invoke(ModDataChangeType.Description, mod, null);
|
_communicatorService.ModDataChanged.Invoke(ModDataChangeType.Description, mod, null);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -235,7 +231,7 @@ public class ModDataEditor
|
||||||
return;
|
return;
|
||||||
|
|
||||||
mod.Version = newVersion;
|
mod.Version = newVersion;
|
||||||
_saveService.QueueSave(new Mod.ModMeta(mod));
|
_saveService.QueueSave(new ModMeta(mod));
|
||||||
_communicatorService.ModDataChanged.Invoke(ModDataChangeType.Version, mod, null);
|
_communicatorService.ModDataChanged.Invoke(ModDataChangeType.Version, mod, null);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -245,7 +241,7 @@ public class ModDataEditor
|
||||||
return;
|
return;
|
||||||
|
|
||||||
mod.Website = newWebsite;
|
mod.Website = newWebsite;
|
||||||
_saveService.QueueSave(new Mod.ModMeta(mod));
|
_saveService.QueueSave(new ModMeta(mod));
|
||||||
_communicatorService.ModDataChanged.Invoke(ModDataChangeType.Website, mod, null);
|
_communicatorService.ModDataChanged.Invoke(ModDataChangeType.Website, mod, null);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -261,7 +257,7 @@ public class ModDataEditor
|
||||||
return;
|
return;
|
||||||
|
|
||||||
mod.Favorite = state;
|
mod.Favorite = state;
|
||||||
_saveService.QueueSave(new Mod.ModData(mod));
|
_saveService.QueueSave(new ModLocalData(mod));
|
||||||
;
|
;
|
||||||
_communicatorService.ModDataChanged.Invoke(ModDataChangeType.Favorite, mod, null);
|
_communicatorService.ModDataChanged.Invoke(ModDataChangeType.Favorite, mod, null);
|
||||||
}
|
}
|
||||||
|
|
@ -272,7 +268,7 @@ public class ModDataEditor
|
||||||
return;
|
return;
|
||||||
|
|
||||||
mod.Note = newNote;
|
mod.Note = newNote;
|
||||||
_saveService.QueueSave(new Mod.ModData(mod));
|
_saveService.QueueSave(new ModLocalData(mod));
|
||||||
;
|
;
|
||||||
_communicatorService.ModDataChanged.Invoke(ModDataChangeType.Favorite, mod, null);
|
_communicatorService.ModDataChanged.Invoke(ModDataChangeType.Favorite, mod, null);
|
||||||
}
|
}
|
||||||
|
|
@ -287,20 +283,20 @@ public class ModDataEditor
|
||||||
ModDataChangeType flags = 0;
|
ModDataChangeType flags = 0;
|
||||||
if (tagIdx == which.Count)
|
if (tagIdx == which.Count)
|
||||||
{
|
{
|
||||||
flags = mod.UpdateTags(local ? null : which.Append(newTag), local ? which.Append(newTag) : null);
|
flags = ModLocalData.UpdateTags(mod, local ? null : which.Append(newTag), local ? which.Append(newTag) : null);
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
var tmp = which.ToArray();
|
var tmp = which.ToArray();
|
||||||
tmp[tagIdx] = newTag;
|
tmp[tagIdx] = newTag;
|
||||||
flags = mod.UpdateTags(local ? null : tmp, local ? tmp : null);
|
flags = ModLocalData.UpdateTags(mod, local ? null : tmp, local ? tmp : null);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (flags.HasFlag(ModDataChangeType.ModTags))
|
if (flags.HasFlag(ModDataChangeType.ModTags))
|
||||||
_saveService.QueueSave(new Mod.ModMeta(mod));
|
_saveService.QueueSave(new ModMeta(mod));
|
||||||
|
|
||||||
if (flags.HasFlag(ModDataChangeType.LocalTags))
|
if (flags.HasFlag(ModDataChangeType.LocalTags))
|
||||||
_saveService.QueueSave(new Mod.ModData(mod));
|
_saveService.QueueSave(new ModLocalData(mod));
|
||||||
|
|
||||||
if (flags != 0)
|
if (flags != 0)
|
||||||
_communicatorService.ModDataChanged.Invoke(flags, mod, null);
|
_communicatorService.ModDataChanged.Invoke(flags, mod, null);
|
||||||
|
|
@ -308,8 +304,8 @@ public class ModDataEditor
|
||||||
|
|
||||||
public void MoveDataFile(DirectoryInfo oldMod, DirectoryInfo newMod)
|
public void MoveDataFile(DirectoryInfo oldMod, DirectoryInfo newMod)
|
||||||
{
|
{
|
||||||
var oldFile = _filenameService.LocalDataFile(oldMod.Name);
|
var oldFile = _saveService.FileNames.LocalDataFile(oldMod.Name);
|
||||||
var newFile = _filenameService.LocalDataFile(newMod.Name);
|
var newFile = _saveService.FileNames.LocalDataFile(newMod.Name);
|
||||||
if (!File.Exists(oldFile))
|
if (!File.Exists(oldFile))
|
||||||
return;
|
return;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -5,11 +5,10 @@ using System.IO;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Text.RegularExpressions;
|
using System.Text.RegularExpressions;
|
||||||
using OtterGui.Filesystem;
|
using OtterGui.Filesystem;
|
||||||
using Penumbra.Mods.Manager;
|
|
||||||
using Penumbra.Services;
|
using Penumbra.Services;
|
||||||
using Penumbra.Util;
|
using Penumbra.Util;
|
||||||
|
|
||||||
namespace Penumbra.Mods;
|
namespace Penumbra.Mods.Manager;
|
||||||
|
|
||||||
public sealed class ModFileSystem : FileSystem<Mod>, IDisposable, ISavable
|
public sealed class ModFileSystem : FileSystem<Mod>, IDisposable, ISavable
|
||||||
{
|
{
|
||||||
|
|
@ -5,7 +5,7 @@ using System.Linq;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using Penumbra.Services;
|
using Penumbra.Services;
|
||||||
|
|
||||||
namespace Penumbra.Mods;
|
namespace Penumbra.Mods.Manager;
|
||||||
|
|
||||||
/// <summary> Describes the state of a potential move-target for a mod. </summary>
|
/// <summary> Describes the state of a potential move-target for a mod. </summary>
|
||||||
public enum NewDirectoryState
|
public enum NewDirectoryState
|
||||||
|
|
@ -73,7 +73,7 @@ public sealed class ModManager : ModStorage
|
||||||
if (this.Any(m => m.ModPath.Name == modFolder.Name))
|
if (this.Any(m => m.ModPath.Name == modFolder.Name))
|
||||||
return;
|
return;
|
||||||
|
|
||||||
Mod.Creator.SplitMultiGroups(modFolder);
|
ModCreator.SplitMultiGroups(modFolder);
|
||||||
var mod = Mod.LoadMod(this, modFolder, true);
|
var mod = Mod.LoadMod(this, modFolder, true);
|
||||||
if (mod == null)
|
if (mod == null)
|
||||||
return;
|
return;
|
||||||
|
|
@ -206,7 +206,7 @@ public sealed class ModManager : ModStorage
|
||||||
if (oldName == newName)
|
if (oldName == newName)
|
||||||
return NewDirectoryState.Identical;
|
return NewDirectoryState.Identical;
|
||||||
|
|
||||||
var fixedNewName = Mod.Creator.ReplaceBadXivSymbols(newName);
|
var fixedNewName = ModCreator.ReplaceBadXivSymbols(newName);
|
||||||
if (fixedNewName != newName)
|
if (fixedNewName != newName)
|
||||||
return NewDirectoryState.ContainsInvalidSymbols;
|
return NewDirectoryState.ContainsInvalidSymbols;
|
||||||
|
|
||||||
|
|
|
||||||
244
Penumbra/Mods/Manager/ModMigration.cs
Normal file
244
Penumbra/Mods/Manager/ModMigration.cs
Normal file
|
|
@ -0,0 +1,244 @@
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.IO;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Text.RegularExpressions;
|
||||||
|
using Newtonsoft.Json;
|
||||||
|
using Newtonsoft.Json.Linq;
|
||||||
|
using OtterGui;
|
||||||
|
using Penumbra.Api.Enums;
|
||||||
|
using Penumbra.String.Classes;
|
||||||
|
using Penumbra.Util;
|
||||||
|
|
||||||
|
namespace Penumbra.Mods.Manager;
|
||||||
|
|
||||||
|
public static partial class ModMigration
|
||||||
|
{
|
||||||
|
[GeneratedRegex(@"group_\d{3}_", RegexOptions.Compiled | RegexOptions.NonBacktracking | RegexOptions.ExplicitCapture)]
|
||||||
|
private static partial Regex GroupRegex();
|
||||||
|
|
||||||
|
[GeneratedRegex("^group_", RegexOptions.Compiled)]
|
||||||
|
private static partial Regex GroupStartRegex();
|
||||||
|
|
||||||
|
public static bool Migrate(SaveService saveService, Mod mod, JObject json, ref uint fileVersion)
|
||||||
|
=> MigrateV0ToV1(saveService, mod, json, ref fileVersion) || MigrateV1ToV2(mod, ref fileVersion) || MigrateV2ToV3(mod, ref fileVersion);
|
||||||
|
|
||||||
|
private static bool MigrateV2ToV3(Mod _, ref uint fileVersion)
|
||||||
|
{
|
||||||
|
if (fileVersion > 2)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
// Remove import time.
|
||||||
|
fileVersion = 3;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool MigrateV1ToV2(Mod mod, ref uint fileVersion)
|
||||||
|
{
|
||||||
|
if (fileVersion > 1)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
if (!mod.GroupFiles.All(g => GroupRegex().IsMatch(g.Name)))
|
||||||
|
foreach (var (group, index) in mod.GroupFiles.WithIndex().ToArray())
|
||||||
|
{
|
||||||
|
var newName = GroupStartRegex().Replace(group.Name, $"group_{index + 1:D3}_");
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (newName != group.Name)
|
||||||
|
group.MoveTo(Path.Combine(group.DirectoryName ?? string.Empty, newName), false);
|
||||||
|
}
|
||||||
|
catch (Exception e)
|
||||||
|
{
|
||||||
|
Penumbra.Log.Error($"Could not rename group file {group.Name} to {newName} during migration:\n{e}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fileVersion = 2;
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool MigrateV0ToV1(SaveService saveService, Mod mod, JObject json, ref uint fileVersion)
|
||||||
|
{
|
||||||
|
if (fileVersion > 0)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
var swaps = json["FileSwaps"]?.ToObject<Dictionary<Utf8GamePath, FullPath>>()
|
||||||
|
?? new Dictionary<Utf8GamePath, FullPath>();
|
||||||
|
var groups = json["Groups"]?.ToObject<Dictionary<string, OptionGroupV0>>() ?? new Dictionary<string, OptionGroupV0>();
|
||||||
|
var priority = 1;
|
||||||
|
var seenMetaFiles = new HashSet<FullPath>();
|
||||||
|
foreach (var group in groups.Values)
|
||||||
|
ConvertGroup(mod, group, ref priority, seenMetaFiles);
|
||||||
|
|
||||||
|
foreach (var unusedFile in mod.FindUnusedFiles().Where(f => !seenMetaFiles.Contains(f)))
|
||||||
|
{
|
||||||
|
if (unusedFile.ToGamePath(mod.ModPath, out var gamePath)
|
||||||
|
&& !mod._default.FileData.TryAdd(gamePath, unusedFile))
|
||||||
|
Penumbra.Log.Error($"Could not add {gamePath} because it already points to {mod._default.FileData[gamePath]}.");
|
||||||
|
}
|
||||||
|
|
||||||
|
mod._default.FileSwapData.Clear();
|
||||||
|
mod._default.FileSwapData.EnsureCapacity(swaps.Count);
|
||||||
|
foreach (var (gamePath, swapPath) in swaps)
|
||||||
|
mod._default.FileSwapData.Add(gamePath, swapPath);
|
||||||
|
|
||||||
|
mod._default.IncorporateMetaChanges(mod.ModPath, true);
|
||||||
|
foreach (var (_, index) in mod.Groups.WithIndex())
|
||||||
|
saveService.ImmediateSave(new ModSaveGroup(mod, index));
|
||||||
|
|
||||||
|
// Delete meta files.
|
||||||
|
foreach (var file in seenMetaFiles.Where(f => f.Exists))
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
File.Delete(file.FullName);
|
||||||
|
}
|
||||||
|
catch (Exception e)
|
||||||
|
{
|
||||||
|
Penumbra.Log.Warning($"Could not delete meta file {file.FullName} during migration:\n{e}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Delete old meta files.
|
||||||
|
var oldMetaFile = Path.Combine(mod.ModPath.FullName, "metadata_manipulations.json");
|
||||||
|
if (File.Exists(oldMetaFile))
|
||||||
|
try
|
||||||
|
{
|
||||||
|
File.Delete(oldMetaFile);
|
||||||
|
}
|
||||||
|
catch (Exception e)
|
||||||
|
{
|
||||||
|
Penumbra.Log.Warning($"Could not delete old meta file {oldMetaFile} during migration:\n{e}");
|
||||||
|
}
|
||||||
|
|
||||||
|
fileVersion = 1;
|
||||||
|
saveService.ImmediateSave(new ModSaveGroup(mod, -1));
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void ConvertGroup(Mod mod, OptionGroupV0 group, ref int priority, HashSet<FullPath> seenMetaFiles)
|
||||||
|
{
|
||||||
|
if (group.Options.Count == 0)
|
||||||
|
return;
|
||||||
|
|
||||||
|
switch (group.SelectionType)
|
||||||
|
{
|
||||||
|
case GroupType.Multi:
|
||||||
|
|
||||||
|
var optionPriority = 0;
|
||||||
|
var newMultiGroup = new MultiModGroup()
|
||||||
|
{
|
||||||
|
Name = group.GroupName,
|
||||||
|
Priority = priority++,
|
||||||
|
Description = string.Empty,
|
||||||
|
};
|
||||||
|
mod._groups.Add(newMultiGroup);
|
||||||
|
foreach (var option in group.Options)
|
||||||
|
newMultiGroup.PrioritizedOptions.Add((SubModFromOption(mod, option, seenMetaFiles), optionPriority++));
|
||||||
|
|
||||||
|
break;
|
||||||
|
case GroupType.Single:
|
||||||
|
if (group.Options.Count == 1)
|
||||||
|
{
|
||||||
|
AddFilesToSubMod(mod._default, mod.ModPath, group.Options[0], seenMetaFiles);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var newSingleGroup = new SingleModGroup()
|
||||||
|
{
|
||||||
|
Name = group.GroupName,
|
||||||
|
Priority = priority++,
|
||||||
|
Description = string.Empty,
|
||||||
|
};
|
||||||
|
mod._groups.Add(newSingleGroup);
|
||||||
|
foreach (var option in group.Options)
|
||||||
|
newSingleGroup.OptionData.Add(SubModFromOption(mod, option, seenMetaFiles));
|
||||||
|
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void AddFilesToSubMod(SubMod mod, DirectoryInfo basePath, OptionV0 option, HashSet<FullPath> seenMetaFiles)
|
||||||
|
{
|
||||||
|
foreach (var (relPath, gamePaths) in option.OptionFiles)
|
||||||
|
{
|
||||||
|
var fullPath = new FullPath(basePath, relPath);
|
||||||
|
foreach (var gamePath in gamePaths)
|
||||||
|
mod.FileData.TryAdd(gamePath, fullPath);
|
||||||
|
|
||||||
|
if (fullPath.Extension is ".meta" or ".rgsp")
|
||||||
|
seenMetaFiles.Add(fullPath);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static SubMod SubModFromOption(Mod mod, OptionV0 option, HashSet<FullPath> seenMetaFiles)
|
||||||
|
{
|
||||||
|
var subMod = new SubMod(mod) { Name = option.OptionName };
|
||||||
|
AddFilesToSubMod(subMod, mod.ModPath, option, seenMetaFiles);
|
||||||
|
subMod.IncorporateMetaChanges(mod.ModPath, false);
|
||||||
|
return subMod;
|
||||||
|
}
|
||||||
|
|
||||||
|
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 GroupType SelectionType = GroupType.Single;
|
||||||
|
|
||||||
|
public List<OptionV0> Options = new();
|
||||||
|
|
||||||
|
public OptionGroupV0()
|
||||||
|
{ }
|
||||||
|
}
|
||||||
|
|
||||||
|
// Not used anymore, but required for migration.
|
||||||
|
private class SingleOrArrayConverter<T> : JsonConverter
|
||||||
|
{
|
||||||
|
public override bool CanConvert(Type objectType)
|
||||||
|
=> objectType == typeof(HashSet<T>);
|
||||||
|
|
||||||
|
public override object ReadJson(JsonReader reader, Type objectType, object? existingValue, JsonSerializer serializer)
|
||||||
|
{
|
||||||
|
var token = JToken.Load(reader);
|
||||||
|
|
||||||
|
if (token.Type == JTokenType.Array)
|
||||||
|
return token.ToObject<HashSet<T>>() ?? new HashSet<T>();
|
||||||
|
|
||||||
|
var tmp = token.ToObject<T>();
|
||||||
|
return tmp != null
|
||||||
|
? new HashSet<T> { tmp }
|
||||||
|
: new HashSet<T>();
|
||||||
|
}
|
||||||
|
|
||||||
|
public override bool CanWrite
|
||||||
|
=> true;
|
||||||
|
|
||||||
|
public override void WriteJson(JsonWriter writer, object? value, JsonSerializer serializer)
|
||||||
|
{
|
||||||
|
writer.WriteStartArray();
|
||||||
|
if (value != null)
|
||||||
|
{
|
||||||
|
var v = (HashSet<T>)value;
|
||||||
|
foreach (var val in v)
|
||||||
|
serializer.Serialize(writer, val?.ToString());
|
||||||
|
}
|
||||||
|
|
||||||
|
writer.WriteEndArray();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -13,273 +13,270 @@ using Penumbra.String.Classes;
|
||||||
|
|
||||||
namespace Penumbra.Mods;
|
namespace Penumbra.Mods;
|
||||||
|
|
||||||
public partial class Mod
|
internal static partial class ModCreator
|
||||||
{
|
{
|
||||||
internal static partial class Creator
|
/// <summary>
|
||||||
|
/// Create and return a new directory based on the given directory and name, that is <br/>
|
||||||
|
/// - Not Empty.<br/>
|
||||||
|
/// - Unique, by appending (digit) for duplicates.<br/>
|
||||||
|
/// - Containing no symbols invalid for FFXIV or windows paths.<br/>
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="outDirectory"></param>
|
||||||
|
/// <param name="modListName"></param>
|
||||||
|
/// <param name="create"></param>
|
||||||
|
/// <returns></returns>
|
||||||
|
/// <exception cref="IOException"></exception>
|
||||||
|
public static DirectoryInfo CreateModFolder( DirectoryInfo outDirectory, string modListName, bool create = true )
|
||||||
{
|
{
|
||||||
/// <summary>
|
var name = modListName;
|
||||||
/// Create and return a new directory based on the given directory and name, that is <br/>
|
if( name.Length == 0 )
|
||||||
/// - Not Empty.<br/>
|
|
||||||
/// - Unique, by appending (digit) for duplicates.<br/>
|
|
||||||
/// - Containing no symbols invalid for FFXIV or windows paths.<br/>
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="outDirectory"></param>
|
|
||||||
/// <param name="modListName"></param>
|
|
||||||
/// <param name="create"></param>
|
|
||||||
/// <returns></returns>
|
|
||||||
/// <exception cref="IOException"></exception>
|
|
||||||
public static DirectoryInfo CreateModFolder( DirectoryInfo outDirectory, string modListName, bool create = true )
|
|
||||||
{
|
{
|
||||||
var name = modListName;
|
name = "_";
|
||||||
if( name.Length == 0 )
|
|
||||||
{
|
|
||||||
name = "_";
|
|
||||||
}
|
|
||||||
|
|
||||||
var newModFolderBase = NewOptionDirectory( outDirectory, name );
|
|
||||||
var newModFolder = newModFolderBase.FullName.ObtainUniqueFile();
|
|
||||||
if( newModFolder.Length == 0 )
|
|
||||||
{
|
|
||||||
throw new IOException( "Could not create mod folder: too many folders of the same name exist." );
|
|
||||||
}
|
|
||||||
|
|
||||||
if( create )
|
|
||||||
{
|
|
||||||
Directory.CreateDirectory( newModFolder );
|
|
||||||
}
|
|
||||||
|
|
||||||
return new DirectoryInfo( newModFolder );
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
var newModFolderBase = NewOptionDirectory( outDirectory, name );
|
||||||
/// Create the name for a group or option subfolder based on its parent folder and given name.
|
var newModFolder = newModFolderBase.FullName.ObtainUniqueFile();
|
||||||
/// subFolderName should never be empty, and the result is unique and contains no invalid symbols.
|
if( newModFolder.Length == 0 )
|
||||||
/// </summary>
|
|
||||||
public static DirectoryInfo? NewSubFolderName( DirectoryInfo parentFolder, string subFolderName )
|
|
||||||
{
|
{
|
||||||
var newModFolderBase = NewOptionDirectory( parentFolder, subFolderName );
|
throw new IOException( "Could not create mod folder: too many folders of the same name exist." );
|
||||||
var newModFolder = newModFolderBase.FullName.ObtainUniqueFile();
|
|
||||||
return newModFolder.Length == 0 ? null : new DirectoryInfo( newModFolder );
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary> Create a file for an option group from given data. </summary>
|
if( create )
|
||||||
public static void CreateOptionGroup( DirectoryInfo baseFolder, GroupType type, string name,
|
|
||||||
int priority, int index, uint defaultSettings, string desc, IEnumerable< ISubMod > subMods )
|
|
||||||
{
|
{
|
||||||
switch( type )
|
Directory.CreateDirectory( newModFolder );
|
||||||
{
|
|
||||||
case GroupType.Multi:
|
|
||||||
{
|
|
||||||
var group = new MultiModGroup()
|
|
||||||
{
|
|
||||||
Name = name,
|
|
||||||
Description = desc,
|
|
||||||
Priority = priority,
|
|
||||||
DefaultSettings = defaultSettings,
|
|
||||||
};
|
|
||||||
group.PrioritizedOptions.AddRange( subMods.OfType< SubMod >().Select( ( s, idx ) => ( s, idx ) ) );
|
|
||||||
Penumbra.SaveService.ImmediateSave(new ModSaveGroup(baseFolder, group, index));
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
case GroupType.Single:
|
|
||||||
{
|
|
||||||
var group = new SingleModGroup()
|
|
||||||
{
|
|
||||||
Name = name,
|
|
||||||
Description = desc,
|
|
||||||
Priority = priority,
|
|
||||||
DefaultSettings = defaultSettings,
|
|
||||||
};
|
|
||||||
group.OptionData.AddRange( subMods.OfType< SubMod >() );
|
|
||||||
Penumbra.SaveService.ImmediateSave(new ModSaveGroup(baseFolder, group, index));
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary> Create the data for a given sub mod from its data and the folder it is based on. </summary>
|
return new DirectoryInfo( newModFolder );
|
||||||
public static ISubMod CreateSubMod( DirectoryInfo baseFolder, DirectoryInfo optionFolder, OptionList option )
|
|
||||||
{
|
|
||||||
var list = optionFolder.EnumerateFiles( "*.*", SearchOption.AllDirectories )
|
|
||||||
.Select( f => ( Utf8GamePath.FromFile( f, optionFolder, out var gamePath, true ), gamePath, new FullPath( f ) ) )
|
|
||||||
.Where( t => t.Item1 );
|
|
||||||
|
|
||||||
var mod = new SubMod( null! ) // Mod is irrelevant here, only used for saving.
|
|
||||||
{
|
|
||||||
Name = option.Name,
|
|
||||||
Description = option.Description,
|
|
||||||
};
|
|
||||||
foreach( var (_, gamePath, file) in list )
|
|
||||||
{
|
|
||||||
mod.FileData.TryAdd( gamePath, file );
|
|
||||||
}
|
|
||||||
|
|
||||||
mod.IncorporateMetaChanges( baseFolder, true );
|
|
||||||
return mod;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary> Create an empty sub mod for single groups with None options. </summary>
|
|
||||||
internal static ISubMod CreateEmptySubMod( string name )
|
|
||||||
=> new SubMod( null! ) // Mod is irrelevant here, only used for saving.
|
|
||||||
{
|
|
||||||
Name = name,
|
|
||||||
};
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Create the default data file from all unused files that were not handled before
|
|
||||||
/// and are used in sub mods.
|
|
||||||
/// </summary>
|
|
||||||
internal static void CreateDefaultFiles( DirectoryInfo directory )
|
|
||||||
{
|
|
||||||
var mod = new Mod( directory );
|
|
||||||
mod.Reload( Penumbra.ModManager, false, out _ );
|
|
||||||
foreach( var file in mod.FindUnusedFiles() )
|
|
||||||
{
|
|
||||||
if( Utf8GamePath.FromFile( new FileInfo( file.FullName ), directory, out var gamePath, true ) )
|
|
||||||
mod._default.FileData.TryAdd( gamePath, file );
|
|
||||||
}
|
|
||||||
|
|
||||||
mod._default.IncorporateMetaChanges( directory, true );
|
|
||||||
Penumbra.SaveService.ImmediateSave(new ModSaveGroup(mod, -1));
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary> Return the name of a new valid directory based on the base directory and the given name. </summary>
|
|
||||||
public static DirectoryInfo NewOptionDirectory( DirectoryInfo baseDir, string optionName )
|
|
||||||
=> new(Path.Combine( baseDir.FullName, ReplaceBadXivSymbols( optionName ) ));
|
|
||||||
|
|
||||||
/// <summary> Normalize for nicer names, and remove invalid symbols or invalid paths. </summary>
|
|
||||||
public static string ReplaceBadXivSymbols( string s, string replacement = "_" )
|
|
||||||
{
|
|
||||||
switch( s )
|
|
||||||
{
|
|
||||||
case ".": return replacement;
|
|
||||||
case "..": return replacement + replacement;
|
|
||||||
}
|
|
||||||
|
|
||||||
StringBuilder sb = new(s.Length);
|
|
||||||
foreach( var c in s.Normalize( NormalizationForm.FormKC ) )
|
|
||||||
{
|
|
||||||
if( c.IsInvalidInPath() )
|
|
||||||
{
|
|
||||||
sb.Append( replacement );
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
sb.Append( c );
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return sb.ToString();
|
|
||||||
}
|
|
||||||
|
|
||||||
public static void SplitMultiGroups( DirectoryInfo baseDir )
|
|
||||||
{
|
|
||||||
var mod = new Mod( baseDir );
|
|
||||||
|
|
||||||
var files = mod.GroupFiles.ToList();
|
|
||||||
var idx = 0;
|
|
||||||
var reorder = false;
|
|
||||||
foreach( var groupFile in files )
|
|
||||||
{
|
|
||||||
++idx;
|
|
||||||
try
|
|
||||||
{
|
|
||||||
if( reorder )
|
|
||||||
{
|
|
||||||
var newName = $"{baseDir.FullName}\\group_{idx:D3}{groupFile.Name[ 9.. ]}";
|
|
||||||
Penumbra.Log.Debug( $"Moving {groupFile.Name} to {Path.GetFileName( newName )} due to reordering after multi group split." );
|
|
||||||
groupFile.MoveTo( newName, false );
|
|
||||||
}
|
|
||||||
}
|
|
||||||
catch( Exception ex )
|
|
||||||
{
|
|
||||||
throw new Exception( "Could not reorder group file after splitting multi group on .pmp import.", ex );
|
|
||||||
}
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
var json = JObject.Parse( File.ReadAllText( groupFile.FullName ) );
|
|
||||||
if( json[ nameof( IModGroup.Type ) ]?.ToObject< GroupType >() is not GroupType.Multi )
|
|
||||||
{
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
var name = json[ nameof( IModGroup.Name ) ]?.ToObject< string >() ?? string.Empty;
|
|
||||||
if( name.Length == 0 )
|
|
||||||
{
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
var options = json[ "Options" ]?.Children().ToList();
|
|
||||||
if( options == null )
|
|
||||||
{
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
if( options.Count <= IModGroup.MaxMultiOptions )
|
|
||||||
{
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
Penumbra.Log.Information( $"Splitting multi group {name} in {mod.Name} due to {options.Count} being too many options." );
|
|
||||||
var clone = json.DeepClone();
|
|
||||||
reorder = true;
|
|
||||||
foreach( var o in options.Skip( IModGroup.MaxMultiOptions ) )
|
|
||||||
{
|
|
||||||
o.Remove();
|
|
||||||
}
|
|
||||||
|
|
||||||
var newOptions = clone[ "Options" ]!.Children().ToList();
|
|
||||||
foreach( var o in newOptions.Take( IModGroup.MaxMultiOptions ) )
|
|
||||||
{
|
|
||||||
o.Remove();
|
|
||||||
}
|
|
||||||
|
|
||||||
var match = DuplicateNumber().Match( name );
|
|
||||||
var startNumber = match.Success ? int.Parse( match.Groups[ 0 ].Value ) : 1;
|
|
||||||
name = match.Success ? name[ ..4 ] : name;
|
|
||||||
var oldName = $"{name}, Part {startNumber}";
|
|
||||||
var oldPath = $"{baseDir.FullName}\\group_{idx:D3}_{oldName.RemoveInvalidPathSymbols().ToLowerInvariant()}.json";
|
|
||||||
var newName = $"{name}, Part {startNumber + 1}";
|
|
||||||
var newPath = $"{baseDir.FullName}\\group_{++idx:D3}_{newName.RemoveInvalidPathSymbols().ToLowerInvariant()}.json";
|
|
||||||
json[ nameof( IModGroup.Name ) ] = oldName;
|
|
||||||
clone[ nameof( IModGroup.Name ) ] = newName;
|
|
||||||
|
|
||||||
clone[ nameof( IModGroup.DefaultSettings ) ] = 0u;
|
|
||||||
|
|
||||||
Penumbra.Log.Debug( $"Writing the first {IModGroup.MaxMultiOptions} options to {Path.GetFileName( oldPath )} after split." );
|
|
||||||
using( var oldFile = File.CreateText( oldPath ) )
|
|
||||||
{
|
|
||||||
using var j = new JsonTextWriter( oldFile )
|
|
||||||
{
|
|
||||||
Formatting = Formatting.Indented,
|
|
||||||
};
|
|
||||||
json.WriteTo( j );
|
|
||||||
}
|
|
||||||
|
|
||||||
Penumbra.Log.Debug( $"Writing the remaining {options.Count - IModGroup.MaxMultiOptions} options to {Path.GetFileName( newPath )} after split." );
|
|
||||||
using( var newFile = File.CreateText( newPath ) )
|
|
||||||
{
|
|
||||||
using var j = new JsonTextWriter( newFile )
|
|
||||||
{
|
|
||||||
Formatting = Formatting.Indented,
|
|
||||||
};
|
|
||||||
clone.WriteTo( j );
|
|
||||||
}
|
|
||||||
|
|
||||||
Penumbra.Log.Debug(
|
|
||||||
$"Deleting the old group file at {groupFile.Name} after splitting it into {Path.GetFileName( oldPath )} and {Path.GetFileName( newPath )}." );
|
|
||||||
groupFile.Delete();
|
|
||||||
}
|
|
||||||
catch( Exception ex )
|
|
||||||
{
|
|
||||||
throw new Exception( $"Could not split multi group file {groupFile.Name} on .pmp import.", ex );
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
[GeneratedRegex( @", Part (\d+)$", RegexOptions.NonBacktracking )]
|
|
||||||
private static partial Regex DuplicateNumber();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Create the name for a group or option subfolder based on its parent folder and given name.
|
||||||
|
/// subFolderName should never be empty, and the result is unique and contains no invalid symbols.
|
||||||
|
/// </summary>
|
||||||
|
public static DirectoryInfo? NewSubFolderName( DirectoryInfo parentFolder, string subFolderName )
|
||||||
|
{
|
||||||
|
var newModFolderBase = NewOptionDirectory( parentFolder, subFolderName );
|
||||||
|
var newModFolder = newModFolderBase.FullName.ObtainUniqueFile();
|
||||||
|
return newModFolder.Length == 0 ? null : new DirectoryInfo( newModFolder );
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary> Create a file for an option group from given data. </summary>
|
||||||
|
public static void CreateOptionGroup( DirectoryInfo baseFolder, GroupType type, string name,
|
||||||
|
int priority, int index, uint defaultSettings, string desc, IEnumerable< ISubMod > subMods )
|
||||||
|
{
|
||||||
|
switch( type )
|
||||||
|
{
|
||||||
|
case GroupType.Multi:
|
||||||
|
{
|
||||||
|
var group = new MultiModGroup()
|
||||||
|
{
|
||||||
|
Name = name,
|
||||||
|
Description = desc,
|
||||||
|
Priority = priority,
|
||||||
|
DefaultSettings = defaultSettings,
|
||||||
|
};
|
||||||
|
group.PrioritizedOptions.AddRange( subMods.OfType< SubMod >().Select( ( s, idx ) => ( s, idx ) ) );
|
||||||
|
Penumbra.SaveService.ImmediateSave(new ModSaveGroup(baseFolder, group, index));
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case GroupType.Single:
|
||||||
|
{
|
||||||
|
var group = new SingleModGroup()
|
||||||
|
{
|
||||||
|
Name = name,
|
||||||
|
Description = desc,
|
||||||
|
Priority = priority,
|
||||||
|
DefaultSettings = defaultSettings,
|
||||||
|
};
|
||||||
|
group.OptionData.AddRange( subMods.OfType< SubMod >() );
|
||||||
|
Penumbra.SaveService.ImmediateSave(new ModSaveGroup(baseFolder, group, index));
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary> Create the data for a given sub mod from its data and the folder it is based on. </summary>
|
||||||
|
public static ISubMod CreateSubMod( DirectoryInfo baseFolder, DirectoryInfo optionFolder, OptionList option )
|
||||||
|
{
|
||||||
|
var list = optionFolder.EnumerateFiles( "*.*", SearchOption.AllDirectories )
|
||||||
|
.Select( f => ( Utf8GamePath.FromFile( f, optionFolder, out var gamePath, true ), gamePath, new FullPath( f ) ) )
|
||||||
|
.Where( t => t.Item1 );
|
||||||
|
|
||||||
|
var mod = new SubMod( null! ) // Mod is irrelevant here, only used for saving.
|
||||||
|
{
|
||||||
|
Name = option.Name,
|
||||||
|
Description = option.Description,
|
||||||
|
};
|
||||||
|
foreach( var (_, gamePath, file) in list )
|
||||||
|
{
|
||||||
|
mod.FileData.TryAdd( gamePath, file );
|
||||||
|
}
|
||||||
|
|
||||||
|
mod.IncorporateMetaChanges( baseFolder, true );
|
||||||
|
return mod;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary> Create an empty sub mod for single groups with None options. </summary>
|
||||||
|
internal static ISubMod CreateEmptySubMod( string name )
|
||||||
|
=> new SubMod( null! ) // Mod is irrelevant here, only used for saving.
|
||||||
|
{
|
||||||
|
Name = name,
|
||||||
|
};
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Create the default data file from all unused files that were not handled before
|
||||||
|
/// and are used in sub mods.
|
||||||
|
/// </summary>
|
||||||
|
internal static void CreateDefaultFiles( DirectoryInfo directory )
|
||||||
|
{
|
||||||
|
var mod = new Mod( directory );
|
||||||
|
mod.Reload( Penumbra.ModManager, false, out _ );
|
||||||
|
foreach( var file in mod.FindUnusedFiles() )
|
||||||
|
{
|
||||||
|
if( Utf8GamePath.FromFile( new FileInfo( file.FullName ), directory, out var gamePath, true ) )
|
||||||
|
mod._default.FileData.TryAdd( gamePath, file );
|
||||||
|
}
|
||||||
|
|
||||||
|
mod._default.IncorporateMetaChanges( directory, true );
|
||||||
|
Penumbra.SaveService.ImmediateSave(new ModSaveGroup(mod, -1));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary> Return the name of a new valid directory based on the base directory and the given name. </summary>
|
||||||
|
public static DirectoryInfo NewOptionDirectory( DirectoryInfo baseDir, string optionName )
|
||||||
|
=> new(Path.Combine( baseDir.FullName, ReplaceBadXivSymbols( optionName ) ));
|
||||||
|
|
||||||
|
/// <summary> Normalize for nicer names, and remove invalid symbols or invalid paths. </summary>
|
||||||
|
public static string ReplaceBadXivSymbols( string s, string replacement = "_" )
|
||||||
|
{
|
||||||
|
switch( s )
|
||||||
|
{
|
||||||
|
case ".": return replacement;
|
||||||
|
case "..": return replacement + replacement;
|
||||||
|
}
|
||||||
|
|
||||||
|
StringBuilder sb = new(s.Length);
|
||||||
|
foreach( var c in s.Normalize( NormalizationForm.FormKC ) )
|
||||||
|
{
|
||||||
|
if( c.IsInvalidInPath() )
|
||||||
|
{
|
||||||
|
sb.Append( replacement );
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
sb.Append( c );
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return sb.ToString();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void SplitMultiGroups( DirectoryInfo baseDir )
|
||||||
|
{
|
||||||
|
var mod = new Mod( baseDir );
|
||||||
|
|
||||||
|
var files = mod.GroupFiles.ToList();
|
||||||
|
var idx = 0;
|
||||||
|
var reorder = false;
|
||||||
|
foreach( var groupFile in files )
|
||||||
|
{
|
||||||
|
++idx;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if( reorder )
|
||||||
|
{
|
||||||
|
var newName = $"{baseDir.FullName}\\group_{idx:D3}{groupFile.Name[ 9.. ]}";
|
||||||
|
Penumbra.Log.Debug( $"Moving {groupFile.Name} to {Path.GetFileName( newName )} due to reordering after multi group split." );
|
||||||
|
groupFile.MoveTo( newName, false );
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch( Exception ex )
|
||||||
|
{
|
||||||
|
throw new Exception( "Could not reorder group file after splitting multi group on .pmp import.", ex );
|
||||||
|
}
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var json = JObject.Parse( File.ReadAllText( groupFile.FullName ) );
|
||||||
|
if( json[ nameof( IModGroup.Type ) ]?.ToObject< GroupType >() is not GroupType.Multi )
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
var name = json[ nameof( IModGroup.Name ) ]?.ToObject< string >() ?? string.Empty;
|
||||||
|
if( name.Length == 0 )
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
var options = json[ "Options" ]?.Children().ToList();
|
||||||
|
if( options == null )
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if( options.Count <= IModGroup.MaxMultiOptions )
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
Penumbra.Log.Information( $"Splitting multi group {name} in {mod.Name} due to {options.Count} being too many options." );
|
||||||
|
var clone = json.DeepClone();
|
||||||
|
reorder = true;
|
||||||
|
foreach( var o in options.Skip( IModGroup.MaxMultiOptions ) )
|
||||||
|
{
|
||||||
|
o.Remove();
|
||||||
|
}
|
||||||
|
|
||||||
|
var newOptions = clone[ "Options" ]!.Children().ToList();
|
||||||
|
foreach( var o in newOptions.Take( IModGroup.MaxMultiOptions ) )
|
||||||
|
{
|
||||||
|
o.Remove();
|
||||||
|
}
|
||||||
|
|
||||||
|
var match = DuplicateNumber().Match( name );
|
||||||
|
var startNumber = match.Success ? int.Parse( match.Groups[ 0 ].Value ) : 1;
|
||||||
|
name = match.Success ? name[ ..4 ] : name;
|
||||||
|
var oldName = $"{name}, Part {startNumber}";
|
||||||
|
var oldPath = $"{baseDir.FullName}\\group_{idx:D3}_{oldName.RemoveInvalidPathSymbols().ToLowerInvariant()}.json";
|
||||||
|
var newName = $"{name}, Part {startNumber + 1}";
|
||||||
|
var newPath = $"{baseDir.FullName}\\group_{++idx:D3}_{newName.RemoveInvalidPathSymbols().ToLowerInvariant()}.json";
|
||||||
|
json[ nameof( IModGroup.Name ) ] = oldName;
|
||||||
|
clone[ nameof( IModGroup.Name ) ] = newName;
|
||||||
|
|
||||||
|
clone[ nameof( IModGroup.DefaultSettings ) ] = 0u;
|
||||||
|
|
||||||
|
Penumbra.Log.Debug( $"Writing the first {IModGroup.MaxMultiOptions} options to {Path.GetFileName( oldPath )} after split." );
|
||||||
|
using( var oldFile = File.CreateText( oldPath ) )
|
||||||
|
{
|
||||||
|
using var j = new JsonTextWriter( oldFile )
|
||||||
|
{
|
||||||
|
Formatting = Formatting.Indented,
|
||||||
|
};
|
||||||
|
json.WriteTo( j );
|
||||||
|
}
|
||||||
|
|
||||||
|
Penumbra.Log.Debug( $"Writing the remaining {options.Count - IModGroup.MaxMultiOptions} options to {Path.GetFileName( newPath )} after split." );
|
||||||
|
using( var newFile = File.CreateText( newPath ) )
|
||||||
|
{
|
||||||
|
using var j = new JsonTextWriter( newFile )
|
||||||
|
{
|
||||||
|
Formatting = Formatting.Indented,
|
||||||
|
};
|
||||||
|
clone.WriteTo( j );
|
||||||
|
}
|
||||||
|
|
||||||
|
Penumbra.Log.Debug(
|
||||||
|
$"Deleting the old group file at {groupFile.Name} after splitting it into {Path.GetFileName( oldPath )} and {Path.GetFileName( newPath )}." );
|
||||||
|
groupFile.Delete();
|
||||||
|
}
|
||||||
|
catch( Exception ex )
|
||||||
|
{
|
||||||
|
throw new Exception( $"Could not split multi group file {groupFile.Name} on .pmp import.", ex );
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[GeneratedRegex(@", Part (\d+)$", RegexOptions.NonBacktracking )]
|
||||||
|
private static partial Regex DuplicateNumber();
|
||||||
}
|
}
|
||||||
|
|
@ -114,13 +114,13 @@ public partial class Mod
|
||||||
_default.WriteTexToolsMeta(ModPath);
|
_default.WriteTexToolsMeta(ModPath);
|
||||||
foreach (var group in Groups)
|
foreach (var group in Groups)
|
||||||
{
|
{
|
||||||
var dir = Creator.NewOptionDirectory(ModPath, group.Name);
|
var dir = ModCreator.NewOptionDirectory(ModPath, group.Name);
|
||||||
if (!dir.Exists)
|
if (!dir.Exists)
|
||||||
dir.Create();
|
dir.Create();
|
||||||
|
|
||||||
foreach (var option in group.OfType<SubMod>())
|
foreach (var option in group.OfType<SubMod>())
|
||||||
{
|
{
|
||||||
var optionDir = Creator.NewOptionDirectory(dir, option.Name);
|
var optionDir = ModCreator.NewOptionDirectory(dir, option.Name);
|
||||||
if (!optionDir.Exists)
|
if (!optionDir.Exists)
|
||||||
optionDir.Create();
|
optionDir.Create();
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,77 +0,0 @@
|
||||||
using Newtonsoft.Json.Linq;
|
|
||||||
using System;
|
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.IO;
|
|
||||||
using System.Linq;
|
|
||||||
using Newtonsoft.Json;
|
|
||||||
using Penumbra.Services;
|
|
||||||
using Penumbra.Util;
|
|
||||||
|
|
||||||
namespace Penumbra.Mods;
|
|
||||||
|
|
||||||
public sealed partial class Mod
|
|
||||||
{
|
|
||||||
public long ImportDate { get; internal set; } = DateTimeOffset.UnixEpoch.ToUnixTimeMilliseconds();
|
|
||||||
|
|
||||||
public IReadOnlyList<string> LocalTags { get; private set; } = Array.Empty<string>();
|
|
||||||
|
|
||||||
public string Note { get; internal set; } = string.Empty;
|
|
||||||
public bool Favorite { get; internal set; } = false;
|
|
||||||
|
|
||||||
internal ModDataChangeType UpdateTags(IEnumerable<string>? newModTags, IEnumerable<string>? newLocalTags)
|
|
||||||
{
|
|
||||||
if (newModTags == null && newLocalTags == null)
|
|
||||||
return 0;
|
|
||||||
|
|
||||||
ModDataChangeType type = 0;
|
|
||||||
if (newModTags != null)
|
|
||||||
{
|
|
||||||
var modTags = newModTags.Where(t => t.Length > 0).Distinct().ToArray();
|
|
||||||
if (!modTags.SequenceEqual(ModTags))
|
|
||||||
{
|
|
||||||
newLocalTags ??= LocalTags;
|
|
||||||
ModTags = modTags;
|
|
||||||
type |= ModDataChangeType.ModTags;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (newLocalTags != null)
|
|
||||||
{
|
|
||||||
var localTags = newLocalTags!.Where(t => t.Length > 0 && !ModTags.Contains(t)).Distinct().ToArray();
|
|
||||||
if (!localTags.SequenceEqual(LocalTags))
|
|
||||||
{
|
|
||||||
LocalTags = localTags;
|
|
||||||
type |= ModDataChangeType.LocalTags;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return type;
|
|
||||||
}
|
|
||||||
|
|
||||||
internal readonly struct ModData : ISavable
|
|
||||||
{
|
|
||||||
public const int FileVersion = 3;
|
|
||||||
|
|
||||||
private readonly Mod _mod;
|
|
||||||
|
|
||||||
public ModData(Mod mod)
|
|
||||||
=> _mod = mod;
|
|
||||||
|
|
||||||
public string ToFilename(FilenameService fileNames)
|
|
||||||
=> fileNames.LocalDataFile(_mod);
|
|
||||||
|
|
||||||
public void Save(StreamWriter writer)
|
|
||||||
{
|
|
||||||
var jObject = new JObject
|
|
||||||
{
|
|
||||||
{ nameof(FileVersion), JToken.FromObject(FileVersion) },
|
|
||||||
{ nameof(ImportDate), JToken.FromObject(_mod.ImportDate) },
|
|
||||||
{ nameof(LocalTags), JToken.FromObject(_mod.LocalTags) },
|
|
||||||
{ nameof(Note), JToken.FromObject(_mod.Note) },
|
|
||||||
{ nameof(Favorite), JToken.FromObject(_mod.Favorite) },
|
|
||||||
};
|
|
||||||
using var jWriter = new JsonTextWriter(writer) { Formatting = Formatting.Indented };
|
|
||||||
jObject.WriteTo(jWriter);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,246 +0,0 @@
|
||||||
using System;
|
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.IO;
|
|
||||||
using System.Linq;
|
|
||||||
using System.Text.RegularExpressions;
|
|
||||||
using Newtonsoft.Json;
|
|
||||||
using Newtonsoft.Json.Linq;
|
|
||||||
using OtterGui;
|
|
||||||
using Penumbra.Api.Enums;
|
|
||||||
using Penumbra.String.Classes;
|
|
||||||
|
|
||||||
namespace Penumbra.Mods;
|
|
||||||
|
|
||||||
public sealed partial class Mod
|
|
||||||
{
|
|
||||||
public static partial class Migration
|
|
||||||
{
|
|
||||||
[GeneratedRegex(@"group_\d{3}_", RegexOptions.Compiled | RegexOptions.NonBacktracking | RegexOptions.ExplicitCapture)]
|
|
||||||
private static partial Regex GroupRegex();
|
|
||||||
|
|
||||||
[GeneratedRegex("^group_", RegexOptions.Compiled)]
|
|
||||||
private static partial Regex GroupStartRegex();
|
|
||||||
|
|
||||||
public static bool Migrate(Mod mod, JObject json, ref uint fileVersion)
|
|
||||||
=> MigrateV0ToV1(mod, json, ref fileVersion) || MigrateV1ToV2(mod, ref fileVersion) || MigrateV2ToV3(mod, ref fileVersion);
|
|
||||||
|
|
||||||
private static bool MigrateV2ToV3(Mod _, ref uint fileVersion)
|
|
||||||
{
|
|
||||||
if (fileVersion > 2)
|
|
||||||
return false;
|
|
||||||
|
|
||||||
// Remove import time.
|
|
||||||
fileVersion = 3;
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static bool MigrateV1ToV2(Mod mod, ref uint fileVersion)
|
|
||||||
{
|
|
||||||
if (fileVersion > 1)
|
|
||||||
return false;
|
|
||||||
|
|
||||||
if (!mod.GroupFiles.All(g => GroupRegex().IsMatch(g.Name)))
|
|
||||||
foreach (var (group, index) in mod.GroupFiles.WithIndex().ToArray())
|
|
||||||
{
|
|
||||||
var newName = GroupStartRegex().Replace(group.Name, $"group_{index + 1:D3}_");
|
|
||||||
try
|
|
||||||
{
|
|
||||||
if (newName != group.Name)
|
|
||||||
group.MoveTo(Path.Combine(group.DirectoryName ?? string.Empty, newName), false);
|
|
||||||
}
|
|
||||||
catch (Exception e)
|
|
||||||
{
|
|
||||||
Penumbra.Log.Error($"Could not rename group file {group.Name} to {newName} during migration:\n{e}");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fileVersion = 2;
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static bool MigrateV0ToV1(Mod mod, JObject json, ref uint fileVersion)
|
|
||||||
{
|
|
||||||
if (fileVersion > 0)
|
|
||||||
return false;
|
|
||||||
|
|
||||||
var swaps = json["FileSwaps"]?.ToObject<Dictionary<Utf8GamePath, FullPath>>()
|
|
||||||
?? new Dictionary<Utf8GamePath, FullPath>();
|
|
||||||
var groups = json["Groups"]?.ToObject<Dictionary<string, OptionGroupV0>>() ?? new Dictionary<string, OptionGroupV0>();
|
|
||||||
var priority = 1;
|
|
||||||
var seenMetaFiles = new HashSet<FullPath>();
|
|
||||||
foreach (var group in groups.Values)
|
|
||||||
ConvertGroup(mod, group, ref priority, seenMetaFiles);
|
|
||||||
|
|
||||||
foreach (var unusedFile in mod.FindUnusedFiles().Where(f => !seenMetaFiles.Contains(f)))
|
|
||||||
{
|
|
||||||
if (unusedFile.ToGamePath(mod.ModPath, out var gamePath)
|
|
||||||
&& !mod._default.FileData.TryAdd(gamePath, unusedFile))
|
|
||||||
Penumbra.Log.Error($"Could not add {gamePath} because it already points to {mod._default.FileData[gamePath]}.");
|
|
||||||
}
|
|
||||||
|
|
||||||
mod._default.FileSwapData.Clear();
|
|
||||||
mod._default.FileSwapData.EnsureCapacity(swaps.Count);
|
|
||||||
foreach (var (gamePath, swapPath) in swaps)
|
|
||||||
mod._default.FileSwapData.Add(gamePath, swapPath);
|
|
||||||
|
|
||||||
mod._default.IncorporateMetaChanges(mod.ModPath, true);
|
|
||||||
foreach (var (_, index) in mod.Groups.WithIndex())
|
|
||||||
Penumbra.SaveService.ImmediateSave(new ModSaveGroup(mod, index));
|
|
||||||
|
|
||||||
// Delete meta files.
|
|
||||||
foreach (var file in seenMetaFiles.Where(f => f.Exists))
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
File.Delete(file.FullName);
|
|
||||||
}
|
|
||||||
catch (Exception e)
|
|
||||||
{
|
|
||||||
Penumbra.Log.Warning($"Could not delete meta file {file.FullName} during migration:\n{e}");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Delete old meta files.
|
|
||||||
var oldMetaFile = Path.Combine(mod.ModPath.FullName, "metadata_manipulations.json");
|
|
||||||
if (File.Exists(oldMetaFile))
|
|
||||||
try
|
|
||||||
{
|
|
||||||
File.Delete(oldMetaFile);
|
|
||||||
}
|
|
||||||
catch (Exception e)
|
|
||||||
{
|
|
||||||
Penumbra.Log.Warning($"Could not delete old meta file {oldMetaFile} during migration:\n{e}");
|
|
||||||
}
|
|
||||||
|
|
||||||
fileVersion = 1;
|
|
||||||
Penumbra.SaveService.ImmediateSave(new ModSaveGroup(mod, -1));
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static void ConvertGroup(Mod mod, OptionGroupV0 group, ref int priority, HashSet<FullPath> seenMetaFiles)
|
|
||||||
{
|
|
||||||
if (group.Options.Count == 0)
|
|
||||||
return;
|
|
||||||
|
|
||||||
switch (group.SelectionType)
|
|
||||||
{
|
|
||||||
case GroupType.Multi:
|
|
||||||
|
|
||||||
var optionPriority = 0;
|
|
||||||
var newMultiGroup = new MultiModGroup()
|
|
||||||
{
|
|
||||||
Name = group.GroupName,
|
|
||||||
Priority = priority++,
|
|
||||||
Description = string.Empty,
|
|
||||||
};
|
|
||||||
mod._groups.Add(newMultiGroup);
|
|
||||||
foreach (var option in group.Options)
|
|
||||||
newMultiGroup.PrioritizedOptions.Add((SubModFromOption(mod, option, seenMetaFiles), optionPriority++));
|
|
||||||
|
|
||||||
break;
|
|
||||||
case GroupType.Single:
|
|
||||||
if (group.Options.Count == 1)
|
|
||||||
{
|
|
||||||
AddFilesToSubMod(mod._default, mod.ModPath, group.Options[0], seenMetaFiles);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
var newSingleGroup = new SingleModGroup()
|
|
||||||
{
|
|
||||||
Name = group.GroupName,
|
|
||||||
Priority = priority++,
|
|
||||||
Description = string.Empty,
|
|
||||||
};
|
|
||||||
mod._groups.Add(newSingleGroup);
|
|
||||||
foreach (var option in group.Options)
|
|
||||||
newSingleGroup.OptionData.Add(SubModFromOption(mod, option, seenMetaFiles));
|
|
||||||
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private static void AddFilesToSubMod(SubMod mod, DirectoryInfo basePath, OptionV0 option, HashSet<FullPath> seenMetaFiles)
|
|
||||||
{
|
|
||||||
foreach (var (relPath, gamePaths) in option.OptionFiles)
|
|
||||||
{
|
|
||||||
var fullPath = new FullPath(basePath, relPath);
|
|
||||||
foreach (var gamePath in gamePaths)
|
|
||||||
mod.FileData.TryAdd(gamePath, fullPath);
|
|
||||||
|
|
||||||
if (fullPath.Extension is ".meta" or ".rgsp")
|
|
||||||
seenMetaFiles.Add(fullPath);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private static SubMod SubModFromOption(Mod mod, OptionV0 option, HashSet<FullPath> seenMetaFiles)
|
|
||||||
{
|
|
||||||
var subMod = new SubMod(mod) { Name = option.OptionName };
|
|
||||||
AddFilesToSubMod(subMod, mod.ModPath, option, seenMetaFiles);
|
|
||||||
subMod.IncorporateMetaChanges(mod.ModPath, false);
|
|
||||||
return subMod;
|
|
||||||
}
|
|
||||||
|
|
||||||
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 GroupType SelectionType = GroupType.Single;
|
|
||||||
|
|
||||||
public List<OptionV0> Options = new();
|
|
||||||
|
|
||||||
public OptionGroupV0()
|
|
||||||
{ }
|
|
||||||
}
|
|
||||||
|
|
||||||
// Not used anymore, but required for migration.
|
|
||||||
private class SingleOrArrayConverter<T> : JsonConverter
|
|
||||||
{
|
|
||||||
public override bool CanConvert(Type objectType)
|
|
||||||
=> objectType == typeof(HashSet<T>);
|
|
||||||
|
|
||||||
public override object ReadJson(JsonReader reader, Type objectType, object? existingValue, JsonSerializer serializer)
|
|
||||||
{
|
|
||||||
var token = JToken.Load(reader);
|
|
||||||
|
|
||||||
if (token.Type == JTokenType.Array)
|
|
||||||
return token.ToObject<HashSet<T>>() ?? new HashSet<T>();
|
|
||||||
|
|
||||||
var tmp = token.ToObject<T>();
|
|
||||||
return tmp != null
|
|
||||||
? new HashSet<T> { tmp }
|
|
||||||
: new HashSet<T>();
|
|
||||||
}
|
|
||||||
|
|
||||||
public override bool CanWrite
|
|
||||||
=> true;
|
|
||||||
|
|
||||||
public override void WriteJson(JsonWriter writer, object? value, JsonSerializer serializer)
|
|
||||||
{
|
|
||||||
writer.WriteStartArray();
|
|
||||||
if (value != null)
|
|
||||||
{
|
|
||||||
var v = (HashSet<T>)value;
|
|
||||||
foreach (var val in v)
|
|
||||||
serializer.Serialize(writer, val?.ToString());
|
|
||||||
}
|
|
||||||
|
|
||||||
writer.WriteEndArray();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,59 +0,0 @@
|
||||||
using System;
|
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.IO;
|
|
||||||
using Newtonsoft.Json;
|
|
||||||
using Newtonsoft.Json.Linq;
|
|
||||||
using OtterGui.Classes;
|
|
||||||
using Penumbra.Services;
|
|
||||||
using Penumbra.Util;
|
|
||||||
|
|
||||||
namespace Penumbra.Mods;
|
|
||||||
|
|
||||||
public sealed partial class Mod : IMod
|
|
||||||
{
|
|
||||||
public static readonly TemporaryMod ForcedFiles = new()
|
|
||||||
{
|
|
||||||
Name = "Forced Files",
|
|
||||||
Index = -1,
|
|
||||||
Priority = int.MaxValue,
|
|
||||||
};
|
|
||||||
|
|
||||||
public LowerString Name { get; internal set; } = "New Mod";
|
|
||||||
public LowerString Author { get; internal set; } = LowerString.Empty;
|
|
||||||
public string Description { get; internal set; } = string.Empty;
|
|
||||||
public string Version { get; internal set; } = string.Empty;
|
|
||||||
public string Website { get; internal set; } = string.Empty;
|
|
||||||
public IReadOnlyList<string> ModTags { get; internal set; } = Array.Empty<string>();
|
|
||||||
|
|
||||||
public override string ToString()
|
|
||||||
=> Name.Text;
|
|
||||||
|
|
||||||
internal readonly struct ModMeta : ISavable
|
|
||||||
{
|
|
||||||
public const uint FileVersion = 3;
|
|
||||||
|
|
||||||
private readonly Mod _mod;
|
|
||||||
|
|
||||||
public ModMeta(Mod mod)
|
|
||||||
=> _mod = mod;
|
|
||||||
|
|
||||||
public string ToFilename(FilenameService fileNames)
|
|
||||||
=> fileNames.ModMetaPath(_mod);
|
|
||||||
|
|
||||||
public void Save(StreamWriter writer)
|
|
||||||
{
|
|
||||||
var jObject = new JObject
|
|
||||||
{
|
|
||||||
{ nameof(FileVersion), JToken.FromObject(FileVersion) },
|
|
||||||
{ nameof(Name), JToken.FromObject(_mod.Name) },
|
|
||||||
{ nameof(Author), JToken.FromObject(_mod.Author) },
|
|
||||||
{ nameof(Description), JToken.FromObject(_mod.Description) },
|
|
||||||
{ nameof(Version), JToken.FromObject(_mod.Version) },
|
|
||||||
{ nameof(Website), JToken.FromObject(_mod.Website) },
|
|
||||||
{ nameof(ModTags), JToken.FromObject(_mod.ModTags) },
|
|
||||||
};
|
|
||||||
using var jWriter = new JsonTextWriter(writer) { Formatting = Formatting.Indented };
|
|
||||||
jObject.WriteTo(jWriter);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
35
Penumbra/Mods/Mod.cs
Normal file
35
Penumbra/Mods/Mod.cs
Normal file
|
|
@ -0,0 +1,35 @@
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using OtterGui.Classes;
|
||||||
|
|
||||||
|
namespace Penumbra.Mods;
|
||||||
|
|
||||||
|
public sealed partial class Mod : IMod
|
||||||
|
{
|
||||||
|
public static readonly TemporaryMod ForcedFiles = new()
|
||||||
|
{
|
||||||
|
Name = "Forced Files",
|
||||||
|
Index = -1,
|
||||||
|
Priority = int.MaxValue,
|
||||||
|
};
|
||||||
|
|
||||||
|
// Meta Data
|
||||||
|
public LowerString Name { get; internal set; } = "New Mod";
|
||||||
|
public LowerString Author { get; internal set; } = LowerString.Empty;
|
||||||
|
public string Description { get; internal set; } = string.Empty;
|
||||||
|
public string Version { get; internal set; } = string.Empty;
|
||||||
|
public string Website { get; internal set; } = string.Empty;
|
||||||
|
public IReadOnlyList<string> ModTags { get; internal set; } = Array.Empty<string>();
|
||||||
|
|
||||||
|
|
||||||
|
// Local Data
|
||||||
|
public long ImportDate { get; internal set; } = DateTimeOffset.UnixEpoch.ToUnixTimeMilliseconds();
|
||||||
|
public IReadOnlyList<string> LocalTags { get; internal set; } = Array.Empty<string>();
|
||||||
|
public string Note { get; internal set; } = string.Empty;
|
||||||
|
public bool Favorite { get; internal set; } = false;
|
||||||
|
|
||||||
|
|
||||||
|
// Access
|
||||||
|
public override string ToString()
|
||||||
|
=> Name.Text;
|
||||||
|
}
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
|
|
||||||
namespace Penumbra.Mods.Manager;
|
namespace Penumbra.Mods;
|
||||||
|
|
||||||
public class ModCache
|
public class ModCache
|
||||||
{
|
{
|
||||||
|
|
|
||||||
67
Penumbra/Mods/ModLocalData.cs
Normal file
67
Penumbra/Mods/ModLocalData.cs
Normal file
|
|
@ -0,0 +1,67 @@
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.IO;
|
||||||
|
using System.Linq;
|
||||||
|
using Newtonsoft.Json;
|
||||||
|
using Newtonsoft.Json.Linq;
|
||||||
|
using Penumbra.Mods.Manager;
|
||||||
|
using Penumbra.Services;
|
||||||
|
using Penumbra.Util;
|
||||||
|
|
||||||
|
namespace Penumbra.Mods;
|
||||||
|
|
||||||
|
public readonly struct ModLocalData : ISavable
|
||||||
|
{
|
||||||
|
public const int FileVersion = 3;
|
||||||
|
|
||||||
|
private readonly Mod _mod;
|
||||||
|
|
||||||
|
public ModLocalData(Mod mod)
|
||||||
|
=> _mod = mod;
|
||||||
|
|
||||||
|
public string ToFilename(FilenameService fileNames)
|
||||||
|
=> fileNames.LocalDataFile(_mod);
|
||||||
|
|
||||||
|
public void Save(StreamWriter writer)
|
||||||
|
{
|
||||||
|
var jObject = new JObject
|
||||||
|
{
|
||||||
|
{ nameof(FileVersion), JToken.FromObject(FileVersion) },
|
||||||
|
{ nameof(Mod.ImportDate), JToken.FromObject(_mod.ImportDate) },
|
||||||
|
{ nameof(Mod.LocalTags), JToken.FromObject(_mod.LocalTags) },
|
||||||
|
{ nameof(Mod.Note), JToken.FromObject(_mod.Note) },
|
||||||
|
{ nameof(Mod.Favorite), JToken.FromObject(_mod.Favorite) },
|
||||||
|
};
|
||||||
|
using var jWriter = new JsonTextWriter(writer) { Formatting = Formatting.Indented };
|
||||||
|
jObject.WriteTo(jWriter);
|
||||||
|
}
|
||||||
|
|
||||||
|
internal static ModDataChangeType UpdateTags(Mod mod, IEnumerable<string>? newModTags, IEnumerable<string>? newLocalTags)
|
||||||
|
{
|
||||||
|
if (newModTags == null && newLocalTags == null)
|
||||||
|
return 0;
|
||||||
|
|
||||||
|
ModDataChangeType type = 0;
|
||||||
|
if (newModTags != null)
|
||||||
|
{
|
||||||
|
var modTags = newModTags.Where(t => t.Length > 0).Distinct().ToArray();
|
||||||
|
if (!modTags.SequenceEqual(mod.ModTags))
|
||||||
|
{
|
||||||
|
newLocalTags ??= mod.LocalTags;
|
||||||
|
mod.ModTags = modTags;
|
||||||
|
type |= ModDataChangeType.ModTags;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (newLocalTags != null)
|
||||||
|
{
|
||||||
|
var localTags = newLocalTags!.Where(t => t.Length > 0 && !mod.ModTags.Contains(t)).Distinct().ToArray();
|
||||||
|
if (!localTags.SequenceEqual(mod.LocalTags))
|
||||||
|
{
|
||||||
|
mod.LocalTags = localTags;
|
||||||
|
type |= ModDataChangeType.LocalTags;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return type;
|
||||||
|
}
|
||||||
|
}
|
||||||
36
Penumbra/Mods/ModMeta.cs
Normal file
36
Penumbra/Mods/ModMeta.cs
Normal file
|
|
@ -0,0 +1,36 @@
|
||||||
|
using System.IO;
|
||||||
|
using Newtonsoft.Json;
|
||||||
|
using Newtonsoft.Json.Linq;
|
||||||
|
using Penumbra.Services;
|
||||||
|
using Penumbra.Util;
|
||||||
|
|
||||||
|
namespace Penumbra.Mods;
|
||||||
|
|
||||||
|
public readonly struct ModMeta : ISavable
|
||||||
|
{
|
||||||
|
public const uint FileVersion = 3;
|
||||||
|
|
||||||
|
private readonly Mod _mod;
|
||||||
|
|
||||||
|
public ModMeta(Mod mod)
|
||||||
|
=> _mod = mod;
|
||||||
|
|
||||||
|
public string ToFilename(FilenameService fileNames)
|
||||||
|
=> fileNames.ModMetaPath(_mod);
|
||||||
|
|
||||||
|
public void Save(StreamWriter writer)
|
||||||
|
{
|
||||||
|
var jObject = new JObject
|
||||||
|
{
|
||||||
|
{ nameof(FileVersion), JToken.FromObject(FileVersion) },
|
||||||
|
{ nameof(Mod.Name), JToken.FromObject(_mod.Name) },
|
||||||
|
{ nameof(Mod.Author), JToken.FromObject(_mod.Author) },
|
||||||
|
{ nameof(Mod.Description), JToken.FromObject(_mod.Description) },
|
||||||
|
{ nameof(Mod.Version), JToken.FromObject(_mod.Version) },
|
||||||
|
{ nameof(Mod.Website), JToken.FromObject(_mod.Website) },
|
||||||
|
{ nameof(Mod.ModTags), JToken.FromObject(_mod.ModTags) },
|
||||||
|
};
|
||||||
|
using var jWriter = new JsonTextWriter(writer) { Formatting = Formatting.Indented };
|
||||||
|
jObject.WriteTo(jWriter);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -50,7 +50,7 @@ public class TemporaryMod : IMod
|
||||||
DirectoryInfo? dir = null;
|
DirectoryInfo? dir = null;
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
dir = Mod.Creator.CreateModFolder( Penumbra.ModManager.BasePath, collection.Name );
|
dir = ModCreator.CreateModFolder( Penumbra.ModManager.BasePath, collection.Name );
|
||||||
var fileDir = Directory.CreateDirectory( Path.Combine( dir.FullName, "files" ) );
|
var fileDir = Directory.CreateDirectory( Path.Combine( dir.FullName, "files" ) );
|
||||||
modManager.DataEditor.CreateMeta( dir, collection.Name, character ?? Penumbra.Config.DefaultModAuthor,
|
modManager.DataEditor.CreateMeta( dir, collection.Name, character ?? Penumbra.Config.DefaultModAuthor,
|
||||||
$"Mod generated from temporary collection {collection.Name} for {character ?? "Unknown Character"}.", null, null );
|
$"Mod generated from temporary collection {collection.Name} for {character ?? "Unknown Character"}.", null, null );
|
||||||
|
|
|
||||||
|
|
@ -24,7 +24,6 @@ using Penumbra.GameData.Actors;
|
||||||
using Penumbra.GameData.Data;
|
using Penumbra.GameData.Data;
|
||||||
using Penumbra.Interop.ResourceLoading;
|
using Penumbra.Interop.ResourceLoading;
|
||||||
using Penumbra.Interop.PathResolving;
|
using Penumbra.Interop.PathResolving;
|
||||||
using Penumbra.Mods;
|
|
||||||
using CharacterUtility = Penumbra.Interop.Services.CharacterUtility;
|
using CharacterUtility = Penumbra.Interop.Services.CharacterUtility;
|
||||||
using DalamudUtil = Dalamud.Utility.Util;
|
using DalamudUtil = Dalamud.Utility.Util;
|
||||||
using ResidentResourceManager = Penumbra.Interop.Services.ResidentResourceManager;
|
using ResidentResourceManager = Penumbra.Interop.Services.ResidentResourceManager;
|
||||||
|
|
|
||||||
|
|
@ -269,9 +269,9 @@ public class ItemSwapTab : IDisposable, ITab
|
||||||
|
|
||||||
private void CreateMod()
|
private void CreateMod()
|
||||||
{
|
{
|
||||||
var newDir = Mod.Creator.CreateModFolder(_modManager.BasePath, _newModName);
|
var newDir = ModCreator.CreateModFolder(_modManager.BasePath, _newModName);
|
||||||
_modManager.DataEditor.CreateMeta(newDir, _newModName, _config.DefaultModAuthor, CreateDescription(), "1.0", string.Empty);
|
_modManager.DataEditor.CreateMeta(newDir, _newModName, _config.DefaultModAuthor, CreateDescription(), "1.0", string.Empty);
|
||||||
Mod.Creator.CreateDefaultFiles(newDir);
|
ModCreator.CreateDefaultFiles(newDir);
|
||||||
_modManager.AddMod(newDir);
|
_modManager.AddMod(newDir);
|
||||||
if (!_swapData.WriteMod(_modManager, _modManager[^1],
|
if (!_swapData.WriteMod(_modManager, _modManager[^1],
|
||||||
_useFileSwaps ? ItemSwapContainer.WriteType.UseSwaps : ItemSwapContainer.WriteType.NoSwaps))
|
_useFileSwaps ? ItemSwapContainer.WriteType.UseSwaps : ItemSwapContainer.WriteType.NoSwaps))
|
||||||
|
|
@ -290,7 +290,7 @@ public class ItemSwapTab : IDisposable, ITab
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
optionFolderName =
|
optionFolderName =
|
||||||
Mod.Creator.NewSubFolderName(new DirectoryInfo(Path.Combine(_mod.ModPath.FullName, _selectedGroup?.Name ?? _newGroupName)),
|
ModCreator.NewSubFolderName(new DirectoryInfo(Path.Combine(_mod.ModPath.FullName, _selectedGroup?.Name ?? _newGroupName)),
|
||||||
_newOptionName);
|
_newOptionName);
|
||||||
if (optionFolderName?.Exists == true)
|
if (optionFolderName?.Exists == true)
|
||||||
throw new Exception($"The folder {optionFolderName.FullName} for the option already exists.");
|
throw new Exception($"The folder {optionFolderName.FullName} for the option already exists.");
|
||||||
|
|
|
||||||
|
|
@ -217,13 +217,13 @@ public partial class ModEditWindow
|
||||||
var fullName = subMod.FullName;
|
var fullName = subMod.FullName;
|
||||||
if (fullName.EndsWith(": " + name))
|
if (fullName.EndsWith(": " + name))
|
||||||
{
|
{
|
||||||
path = Mod.Creator.NewOptionDirectory(path, fullName[..^(name.Length + 2)]);
|
path = ModCreator.NewOptionDirectory(path, fullName[..^(name.Length + 2)]);
|
||||||
path = Mod.Creator.NewOptionDirectory(path, name);
|
path = ModCreator.NewOptionDirectory(path, name);
|
||||||
subDirs = 2;
|
subDirs = 2;
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
path = Mod.Creator.NewOptionDirectory(path, fullName);
|
path = ModCreator.NewOptionDirectory(path, fullName);
|
||||||
subDirs = 1;
|
subDirs = 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -131,9 +131,9 @@ public sealed partial class ModFileSystemSelector : FileSystemSelector<Mod, ModF
|
||||||
if (ImGuiUtil.OpenNameField("Create New Mod", ref _newModName))
|
if (ImGuiUtil.OpenNameField("Create New Mod", ref _newModName))
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
var newDir = Mod.Creator.CreateModFolder(Penumbra.ModManager.BasePath, _newModName);
|
var newDir = ModCreator.CreateModFolder(Penumbra.ModManager.BasePath, _newModName);
|
||||||
_modManager.DataEditor.CreateMeta(newDir, _newModName, Penumbra.Config.DefaultModAuthor, string.Empty, "1.0", string.Empty);
|
_modManager.DataEditor.CreateMeta(newDir, _newModName, Penumbra.Config.DefaultModAuthor, string.Empty, "1.0", string.Empty);
|
||||||
Mod.Creator.CreateDefaultFiles(newDir);
|
ModCreator.CreateDefaultFiles(newDir);
|
||||||
_modManager.AddMod(newDir);
|
_modManager.AddMod(newDir);
|
||||||
_newModName = string.Empty;
|
_newModName = string.Empty;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -30,20 +30,21 @@ public interface ISavable
|
||||||
public class SaveService
|
public class SaveService
|
||||||
{
|
{
|
||||||
private readonly Logger _log;
|
private readonly Logger _log;
|
||||||
private readonly FilenameService _fileNames;
|
|
||||||
private readonly FrameworkManager _framework;
|
private readonly FrameworkManager _framework;
|
||||||
|
|
||||||
public SaveService(Logger log, FilenameService fileNames, FrameworkManager framework)
|
public readonly FilenameService FileNames;
|
||||||
|
|
||||||
|
public SaveService(Logger log, FrameworkManager framework, FilenameService fileNames)
|
||||||
{
|
{
|
||||||
_log = log;
|
_log = log;
|
||||||
_fileNames = fileNames;
|
|
||||||
_framework = framework;
|
_framework = framework;
|
||||||
|
FileNames = fileNames;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary> Queue a save for the next framework tick. </summary>
|
/// <summary> Queue a save for the next framework tick. </summary>
|
||||||
public void QueueSave(ISavable value)
|
public void QueueSave(ISavable value)
|
||||||
{
|
{
|
||||||
var file = value.ToFilename(_fileNames);
|
var file = value.ToFilename(FileNames);
|
||||||
_framework.RegisterDelayed(value.GetType().Name + file, () =>
|
_framework.RegisterDelayed(value.GetType().Name + file, () =>
|
||||||
{
|
{
|
||||||
ImmediateSave(value);
|
ImmediateSave(value);
|
||||||
|
|
@ -53,7 +54,7 @@ public class SaveService
|
||||||
/// <summary> Immediately trigger a save. </summary>
|
/// <summary> Immediately trigger a save. </summary>
|
||||||
public void ImmediateSave(ISavable value)
|
public void ImmediateSave(ISavable value)
|
||||||
{
|
{
|
||||||
var name = value.ToFilename(_fileNames);
|
var name = value.ToFilename(FileNames);
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
if (name.Length == 0)
|
if (name.Length == 0)
|
||||||
|
|
@ -76,7 +77,7 @@ public class SaveService
|
||||||
|
|
||||||
public void ImmediateDelete(ISavable value)
|
public void ImmediateDelete(ISavable value)
|
||||||
{
|
{
|
||||||
var name = value.ToFilename(_fileNames);
|
var name = value.ToFilename(FileNames);
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
if (name.Length == 0)
|
if (name.Length == 0)
|
||||||
|
|
@ -99,7 +100,7 @@ public class SaveService
|
||||||
/// <summary> Immediately delete all existing option group files for a mod and save them anew. </summary>
|
/// <summary> Immediately delete all existing option group files for a mod and save them anew. </summary>
|
||||||
public void SaveAllOptionGroups(Mod mod)
|
public void SaveAllOptionGroups(Mod mod)
|
||||||
{
|
{
|
||||||
foreach (var file in _fileNames.GetOptionGroupFiles(mod))
|
foreach (var file in FileNames.GetOptionGroupFiles(mod))
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue