mirror of
https://github.com/xivdev/Penumbra.git
synced 2025-12-13 12:14: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)
|
||||
{
|
||||
CheckInitialized();
|
||||
if (name.Length == 0 || Mod.Creator.ReplaceBadXivSymbols(name) != name)
|
||||
if (name.Length == 0 || ModCreator.ReplaceBadXivSymbols(name) != name)
|
||||
return PenumbraApiEc.InvalidArgument;
|
||||
|
||||
return _tempCollections.CreateTemporaryCollection(name).Length > 0
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@ using OtterGui.Widgets;
|
|||
using Penumbra.GameData.Enums;
|
||||
using Penumbra.Import.Structs;
|
||||
using Penumbra.Mods;
|
||||
using Penumbra.Mods.Manager;
|
||||
using Penumbra.Services;
|
||||
using Penumbra.UI;
|
||||
using Penumbra.UI.Classes;
|
||||
|
|
|
|||
|
|
@ -45,7 +45,7 @@ public partial class TexToolsImporter
|
|||
};
|
||||
Penumbra.Log.Information( $" -> Importing {archive.Type} Archive." );
|
||||
|
||||
_currentModDirectory = Mod.Creator.CreateModFolder( _baseDirectory, Path.GetRandomFileName() );
|
||||
_currentModDirectory = ModCreator.CreateModFolder( _baseDirectory, Path.GetRandomFileName() );
|
||||
var options = new ExtractionOptions()
|
||||
{
|
||||
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.
|
||||
if( leadDir )
|
||||
{
|
||||
_currentModDirectory = Mod.Creator.CreateModFolder( _baseDirectory, baseName, false );
|
||||
_currentModDirectory = ModCreator.CreateModFolder( _baseDirectory, baseName, false );
|
||||
Directory.Move( Path.Combine( oldName, baseName ), _currentModDirectory.FullName );
|
||||
Directory.Delete( oldName );
|
||||
}
|
||||
else
|
||||
{
|
||||
_currentModDirectory = Mod.Creator.CreateModFolder( _baseDirectory, name, false );
|
||||
_currentModDirectory = ModCreator.CreateModFolder( _baseDirectory, name, false );
|
||||
Directory.Move( oldName, _currentModDirectory.FullName );
|
||||
}
|
||||
|
||||
_currentModDirectory.Refresh();
|
||||
Mod.Creator.SplitMultiGroups( _currentModDirectory );
|
||||
ModCreator.SplitMultiGroups( _currentModDirectory );
|
||||
|
||||
return _currentModDirectory;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -33,14 +33,14 @@ public partial class TexToolsImporter
|
|||
|
||||
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
|
||||
_modManager.DataEditor.CreateMeta( _currentModDirectory, _currentModName, DefaultTexToolsData.Author, DefaultTexToolsData.Description, null, null );
|
||||
|
||||
// Open the mod data file from the mod pack as a SqPackStream
|
||||
_streamDisposer = GetSqPackStreamStream( extractedModPack, "TTMPD.mpd" );
|
||||
ExtractSimpleModList( _currentModDirectory, modList );
|
||||
Mod.Creator.CreateDefaultFiles( _currentModDirectory );
|
||||
ModCreator.CreateDefaultFiles( _currentModDirectory );
|
||||
ResetStreamDisposer();
|
||||
return _currentModDirectory;
|
||||
}
|
||||
|
|
@ -89,7 +89,7 @@ public partial class TexToolsImporter
|
|||
_currentOptionName = DefaultTexToolsData.DefaultOption;
|
||||
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 )
|
||||
? "Mod imported from TexTools mod pack"
|
||||
: 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
|
||||
_streamDisposer = GetSqPackStreamStream( extractedModPack, "TTMPD.mpd" );
|
||||
ExtractSimpleModList( _currentModDirectory, modList.SimpleModsList );
|
||||
Mod.Creator.CreateDefaultFiles( _currentModDirectory );
|
||||
ModCreator.CreateDefaultFiles( _currentModDirectory );
|
||||
ResetStreamDisposer();
|
||||
return _currentModDirectory;
|
||||
}
|
||||
|
|
@ -134,7 +134,7 @@ public partial class TexToolsImporter
|
|||
_currentNumOptions = GetOptionCount( modList );
|
||||
_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 );
|
||||
|
||||
if( _currentNumOptions == 0 )
|
||||
|
|
@ -172,7 +172,7 @@ public partial class TexToolsImporter
|
|||
{
|
||||
var name = numGroups == 1 ? _currentGroupName : $"{_currentGroupName}, Part {groupId + 1}";
|
||||
options.Clear();
|
||||
var groupFolder = Mod.Creator.NewSubFolderName( _currentModDirectory, name )
|
||||
var groupFolder = ModCreator.NewSubFolderName( _currentModDirectory, name )
|
||||
?? new DirectoryInfo( Path.Combine( _currentModDirectory.FullName,
|
||||
numGroups == 1 ? $"Group {groupPriority + 1}" : $"Group {groupPriority + 1}, Part {groupId + 1}" ) );
|
||||
|
||||
|
|
@ -182,10 +182,10 @@ public partial class TexToolsImporter
|
|||
var option = allOptions[ i + optionIdx ];
|
||||
_token.ThrowIfCancellationRequested();
|
||||
_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}" ) );
|
||||
ExtractSimpleModList( optionFolder, option.ModsJsons );
|
||||
options.Add( Mod.Creator.CreateSubMod( _currentModDirectory, optionFolder, option ) );
|
||||
options.Add( ModCreator.CreateSubMod( _currentModDirectory, optionFolder, option ) );
|
||||
if( option.IsChecked )
|
||||
{
|
||||
defaultSettings = group.SelectionType == GroupType.Multi
|
||||
|
|
@ -206,12 +206,12 @@ public partial class TexToolsImporter
|
|||
if( empty != null )
|
||||
{
|
||||
_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;
|
||||
}
|
||||
}
|
||||
|
||||
Mod.Creator.CreateOptionGroup( _currentModDirectory, group.SelectionType, name, groupPriority, groupPriority,
|
||||
ModCreator.CreateOptionGroup( _currentModDirectory, group.SelectionType, name, groupPriority, groupPriority,
|
||||
defaultSettings ?? 0, group.Description, options );
|
||||
++groupPriority;
|
||||
}
|
||||
|
|
@ -219,7 +219,7 @@ public partial class TexToolsImporter
|
|||
}
|
||||
|
||||
ResetStreamDisposer();
|
||||
Mod.Creator.CreateDefaultFiles( _currentModDirectory );
|
||||
ModCreator.CreateDefaultFiles( _currentModDirectory );
|
||||
return _currentModDirectory;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ using System;
|
|||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using Penumbra.Mods.Manager;
|
||||
using Penumbra.String.Classes;
|
||||
|
||||
namespace Penumbra.Mods;
|
||||
|
|
|
|||
|
|
@ -181,10 +181,10 @@ public class ModNormalizer
|
|||
for (var i = _redirections[groupIdx + 1].Count; i < group.Count; ++i)
|
||||
_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>())
|
||||
{
|
||||
var optionDir = Mod.Creator.CreateModFolder(groupDir, option.Name);
|
||||
var optionDir = ModCreator.CreateModFolder(groupDir, option.Name);
|
||||
|
||||
newDict = _redirections[groupIdx + 1][option.OptionIdx];
|
||||
newDict.Clear();
|
||||
|
|
|
|||
|
|
@ -10,25 +10,25 @@ using Penumbra.GameData.Enums;
|
|||
using Penumbra.Meta.Manipulations;
|
||||
using Penumbra.Services;
|
||||
|
||||
namespace Penumbra.Mods;
|
||||
namespace Penumbra.Mods.Manager;
|
||||
|
||||
public class ModCacheManager : IDisposable, IReadOnlyList<ModCache>
|
||||
{
|
||||
private readonly CommunicatorService _communicator;
|
||||
private readonly IdentifierService _identifier;
|
||||
private readonly IReadOnlyList<Mod> _modManager;
|
||||
private readonly IdentifierService _identifier;
|
||||
private readonly IReadOnlyList<Mod> _modManager;
|
||||
|
||||
private readonly List<ModCache> _cache = new();
|
||||
|
||||
public ModCacheManager(CommunicatorService communicator, IdentifierService identifier, ModManager modManager)
|
||||
{
|
||||
_communicator = communicator;
|
||||
_identifier = identifier;
|
||||
_modManager = modManager;
|
||||
_identifier = identifier;
|
||||
_modManager = modManager;
|
||||
|
||||
_communicator.ModOptionChanged.Event += OnModOptionChange;
|
||||
_communicator.ModPathChanged.Event += OnModPathChange;
|
||||
_communicator.ModDataChanged.Event += OnModDataChange;
|
||||
_communicator.ModOptionChanged.Event += OnModOptionChange;
|
||||
_communicator.ModPathChanged.Event += OnModPathChange;
|
||||
_communicator.ModDataChanged.Event += OnModDataChange;
|
||||
_communicator.ModDiscoveryFinished.Event += OnModDiscoveryFinished;
|
||||
if (!identifier.Valid)
|
||||
identifier.FinishedCreation += OnIdentifierCreation;
|
||||
|
|
@ -51,9 +51,9 @@ public class ModCacheManager : IDisposable, IReadOnlyList<ModCache>
|
|||
|
||||
public void Dispose()
|
||||
{
|
||||
_communicator.ModOptionChanged.Event -= OnModOptionChange;
|
||||
_communicator.ModPathChanged.Event -= OnModPathChange;
|
||||
_communicator.ModDataChanged.Event -= OnModDataChange;
|
||||
_communicator.ModOptionChanged.Event -= OnModOptionChange;
|
||||
_communicator.ModPathChanged.Event -= OnModPathChange;
|
||||
_communicator.ModDataChanged.Event -= OnModDataChange;
|
||||
_communicator.ModDiscoveryFinished.Event -= OnModDiscoveryFinished;
|
||||
}
|
||||
|
||||
|
|
@ -232,17 +232,17 @@ public class ModCacheManager : IDisposable, IReadOnlyList<ModCache>
|
|||
|
||||
private static void UpdateCounts(ModCache cache, Mod mod)
|
||||
{
|
||||
cache.TotalFileCount = mod.Default.Files.Count;
|
||||
cache.TotalSwapCount = mod.Default.FileSwaps.Count;
|
||||
cache.TotalFileCount = mod.Default.Files.Count;
|
||||
cache.TotalSwapCount = mod.Default.FileSwaps.Count;
|
||||
cache.TotalManipulations = mod.Default.Manipulations.Count;
|
||||
cache.HasOptions = false;
|
||||
cache.HasOptions = false;
|
||||
foreach (var group in mod.Groups)
|
||||
{
|
||||
cache.HasOptions |= group.IsOption;
|
||||
foreach (var s in group)
|
||||
{
|
||||
cache.TotalFileCount += s.Files.Count;
|
||||
cache.TotalSwapCount += s.FileSwaps.Count;
|
||||
cache.TotalFileCount += s.Files.Count;
|
||||
cache.TotalSwapCount += s.FileSwaps.Count;
|
||||
cache.TotalManipulations += s.Manipulations.Count;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@ using OtterGui.Classes;
|
|||
using Penumbra.Services;
|
||||
using Penumbra.Util;
|
||||
|
||||
namespace Penumbra.Mods;
|
||||
namespace Penumbra.Mods.Manager;
|
||||
|
||||
[Flags]
|
||||
public enum ModDataChangeType : ushort
|
||||
|
|
@ -29,22 +29,20 @@ public enum ModDataChangeType : ushort
|
|||
|
||||
public class ModDataEditor
|
||||
{
|
||||
private readonly FilenameService _filenameService;
|
||||
private readonly SaveService _saveService;
|
||||
private readonly CommunicatorService _communicatorService;
|
||||
|
||||
public ModDataEditor(FilenameService filenameService, SaveService saveService, CommunicatorService communicatorService)
|
||||
public ModDataEditor(SaveService saveService, CommunicatorService communicatorService)
|
||||
{
|
||||
_filenameService = filenameService;
|
||||
_saveService = saveService;
|
||||
_communicatorService = communicatorService;
|
||||
}
|
||||
|
||||
public string MetaFile(Mod mod)
|
||||
=> _filenameService.ModMetaPath(mod);
|
||||
=> _saveService.FileNames.ModMetaPath(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>
|
||||
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.Version = version ?? mod.Version;
|
||||
mod.Website = website ?? mod.Website;
|
||||
_saveService.ImmediateSave(new Mod.ModMeta(mod));
|
||||
_saveService.ImmediateSave(new ModMeta(mod));
|
||||
}
|
||||
|
||||
public ModDataChangeType LoadLocalData(Mod mod)
|
||||
{
|
||||
var dataFile = _filenameService.LocalDataFile(mod);
|
||||
var dataFile = _saveService.FileNames.LocalDataFile(mod);
|
||||
|
||||
var importDate = 0L;
|
||||
var localTags = Enumerable.Empty<string>();
|
||||
|
|
@ -98,7 +96,7 @@ public class ModDataEditor
|
|||
changes |= ModDataChangeType.ImportDate;
|
||||
}
|
||||
|
||||
changes |= mod.UpdateTags(null, localTags);
|
||||
changes |= ModLocalData.UpdateTags(mod, null, localTags);
|
||||
|
||||
if (mod.Favorite != favorite)
|
||||
{
|
||||
|
|
@ -113,14 +111,14 @@ public class ModDataEditor
|
|||
}
|
||||
|
||||
if (save)
|
||||
_saveService.QueueSave(new Mod.ModData(mod));
|
||||
_saveService.QueueSave(new ModLocalData(mod));
|
||||
|
||||
return changes;
|
||||
}
|
||||
|
||||
public ModDataChangeType LoadMeta(Mod mod)
|
||||
{
|
||||
var metaFile = _filenameService.ModMetaPath(mod);
|
||||
var metaFile = _saveService.FileNames.ModMetaPath(mod);
|
||||
if (!File.Exists(metaFile))
|
||||
{
|
||||
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 newVersion = json[nameof(Mod.Version)]?.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 modTags = json[nameof(Mod.ModTags)]?.Values<string>().OfType<string>();
|
||||
|
||||
|
|
@ -172,14 +170,12 @@ public class ModDataEditor
|
|||
mod.Website = newWebsite;
|
||||
}
|
||||
|
||||
if (newFileVersion != Mod.ModMeta.FileVersion)
|
||||
{
|
||||
if (Mod.Migration.Migrate(mod, json, ref newFileVersion))
|
||||
if (newFileVersion != ModMeta.FileVersion)
|
||||
if (ModMigration.Migrate(_saveService, mod, json, ref newFileVersion))
|
||||
{
|
||||
changes |= ModDataChangeType.Migration;
|
||||
_saveService.ImmediateSave(new Mod.ModMeta(mod));
|
||||
_saveService.ImmediateSave(new ModMeta(mod));
|
||||
}
|
||||
}
|
||||
|
||||
if (importDate != null && mod.ImportDate != importDate.Value)
|
||||
{
|
||||
|
|
@ -187,7 +183,7 @@ public class ModDataEditor
|
|||
changes |= ModDataChangeType.ImportDate;
|
||||
}
|
||||
|
||||
changes |= mod.UpdateTags(modTags, null);
|
||||
changes |= ModLocalData.UpdateTags(mod, modTags, null);
|
||||
|
||||
return changes;
|
||||
}
|
||||
|
|
@ -205,7 +201,7 @@ public class ModDataEditor
|
|||
|
||||
var oldName = mod.Name;
|
||||
mod.Name = newName;
|
||||
_saveService.QueueSave(new Mod.ModMeta(mod));
|
||||
_saveService.QueueSave(new ModMeta(mod));
|
||||
_communicatorService.ModDataChanged.Invoke(ModDataChangeType.Name, mod, oldName.Text);
|
||||
}
|
||||
|
||||
|
|
@ -215,7 +211,7 @@ public class ModDataEditor
|
|||
return;
|
||||
|
||||
mod.Author = newAuthor;
|
||||
_saveService.QueueSave(new Mod.ModMeta(mod));
|
||||
_saveService.QueueSave(new ModMeta(mod));
|
||||
_communicatorService.ModDataChanged.Invoke(ModDataChangeType.Author, mod, null);
|
||||
}
|
||||
|
||||
|
|
@ -225,7 +221,7 @@ public class ModDataEditor
|
|||
return;
|
||||
|
||||
mod.Description = newDescription;
|
||||
_saveService.QueueSave(new Mod.ModMeta(mod));
|
||||
_saveService.QueueSave(new ModMeta(mod));
|
||||
_communicatorService.ModDataChanged.Invoke(ModDataChangeType.Description, mod, null);
|
||||
}
|
||||
|
||||
|
|
@ -235,7 +231,7 @@ public class ModDataEditor
|
|||
return;
|
||||
|
||||
mod.Version = newVersion;
|
||||
_saveService.QueueSave(new Mod.ModMeta(mod));
|
||||
_saveService.QueueSave(new ModMeta(mod));
|
||||
_communicatorService.ModDataChanged.Invoke(ModDataChangeType.Version, mod, null);
|
||||
}
|
||||
|
||||
|
|
@ -245,7 +241,7 @@ public class ModDataEditor
|
|||
return;
|
||||
|
||||
mod.Website = newWebsite;
|
||||
_saveService.QueueSave(new Mod.ModMeta(mod));
|
||||
_saveService.QueueSave(new ModMeta(mod));
|
||||
_communicatorService.ModDataChanged.Invoke(ModDataChangeType.Website, mod, null);
|
||||
}
|
||||
|
||||
|
|
@ -261,7 +257,7 @@ public class ModDataEditor
|
|||
return;
|
||||
|
||||
mod.Favorite = state;
|
||||
_saveService.QueueSave(new Mod.ModData(mod));
|
||||
_saveService.QueueSave(new ModLocalData(mod));
|
||||
;
|
||||
_communicatorService.ModDataChanged.Invoke(ModDataChangeType.Favorite, mod, null);
|
||||
}
|
||||
|
|
@ -272,7 +268,7 @@ public class ModDataEditor
|
|||
return;
|
||||
|
||||
mod.Note = newNote;
|
||||
_saveService.QueueSave(new Mod.ModData(mod));
|
||||
_saveService.QueueSave(new ModLocalData(mod));
|
||||
;
|
||||
_communicatorService.ModDataChanged.Invoke(ModDataChangeType.Favorite, mod, null);
|
||||
}
|
||||
|
|
@ -287,20 +283,20 @@ public class ModDataEditor
|
|||
ModDataChangeType flags = 0;
|
||||
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
|
||||
{
|
||||
var tmp = which.ToArray();
|
||||
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))
|
||||
_saveService.QueueSave(new Mod.ModMeta(mod));
|
||||
_saveService.QueueSave(new ModMeta(mod));
|
||||
|
||||
if (flags.HasFlag(ModDataChangeType.LocalTags))
|
||||
_saveService.QueueSave(new Mod.ModData(mod));
|
||||
_saveService.QueueSave(new ModLocalData(mod));
|
||||
|
||||
if (flags != 0)
|
||||
_communicatorService.ModDataChanged.Invoke(flags, mod, null);
|
||||
|
|
@ -308,8 +304,8 @@ public class ModDataEditor
|
|||
|
||||
public void MoveDataFile(DirectoryInfo oldMod, DirectoryInfo newMod)
|
||||
{
|
||||
var oldFile = _filenameService.LocalDataFile(oldMod.Name);
|
||||
var newFile = _filenameService.LocalDataFile(newMod.Name);
|
||||
var oldFile = _saveService.FileNames.LocalDataFile(oldMod.Name);
|
||||
var newFile = _saveService.FileNames.LocalDataFile(newMod.Name);
|
||||
if (!File.Exists(oldFile))
|
||||
return;
|
||||
|
||||
|
|
|
|||
|
|
@ -5,11 +5,10 @@ using System.IO;
|
|||
using System.Linq;
|
||||
using System.Text.RegularExpressions;
|
||||
using OtterGui.Filesystem;
|
||||
using Penumbra.Mods.Manager;
|
||||
using Penumbra.Services;
|
||||
using Penumbra.Util;
|
||||
|
||||
namespace Penumbra.Mods;
|
||||
namespace Penumbra.Mods.Manager;
|
||||
|
||||
public sealed class ModFileSystem : FileSystem<Mod>, IDisposable, ISavable
|
||||
{
|
||||
|
|
@ -5,7 +5,7 @@ using System.Linq;
|
|||
using System.Threading.Tasks;
|
||||
using Penumbra.Services;
|
||||
|
||||
namespace Penumbra.Mods;
|
||||
namespace Penumbra.Mods.Manager;
|
||||
|
||||
/// <summary> Describes the state of a potential move-target for a mod. </summary>
|
||||
public enum NewDirectoryState
|
||||
|
|
@ -73,7 +73,7 @@ public sealed class ModManager : ModStorage
|
|||
if (this.Any(m => m.ModPath.Name == modFolder.Name))
|
||||
return;
|
||||
|
||||
Mod.Creator.SplitMultiGroups(modFolder);
|
||||
ModCreator.SplitMultiGroups(modFolder);
|
||||
var mod = Mod.LoadMod(this, modFolder, true);
|
||||
if (mod == null)
|
||||
return;
|
||||
|
|
@ -206,7 +206,7 @@ public sealed class ModManager : ModStorage
|
|||
if (oldName == newName)
|
||||
return NewDirectoryState.Identical;
|
||||
|
||||
var fixedNewName = Mod.Creator.ReplaceBadXivSymbols(newName);
|
||||
var fixedNewName = ModCreator.ReplaceBadXivSymbols(newName);
|
||||
if (fixedNewName != newName)
|
||||
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;
|
||||
|
||||
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>
|
||||
/// 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 )
|
||||
var name = modListName;
|
||||
if( name.Length == 0 )
|
||||
{
|
||||
var name = modListName;
|
||||
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 );
|
||||
name = "_";
|
||||
}
|
||||
|
||||
/// <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( outDirectory, name );
|
||||
var newModFolder = newModFolderBase.FullName.ObtainUniqueFile();
|
||||
if( newModFolder.Length == 0 )
|
||||
{
|
||||
var newModFolderBase = NewOptionDirectory( parentFolder, subFolderName );
|
||||
var newModFolder = newModFolderBase.FullName.ObtainUniqueFile();
|
||||
return newModFolder.Length == 0 ? null : new DirectoryInfo( newModFolder );
|
||||
throw new IOException( "Could not create mod folder: too many folders of the same name exist." );
|
||||
}
|
||||
|
||||
/// <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 )
|
||||
if( create )
|
||||
{
|
||||
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;
|
||||
}
|
||||
}
|
||||
Directory.CreateDirectory( newModFolder );
|
||||
}
|
||||
|
||||
/// <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();
|
||||
return new DirectoryInfo( newModFolder );
|
||||
}
|
||||
|
||||
/// <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);
|
||||
foreach (var group in Groups)
|
||||
{
|
||||
var dir = Creator.NewOptionDirectory(ModPath, group.Name);
|
||||
var dir = ModCreator.NewOptionDirectory(ModPath, group.Name);
|
||||
if (!dir.Exists)
|
||||
dir.Create();
|
||||
|
||||
foreach (var option in group.OfType<SubMod>())
|
||||
{
|
||||
var optionDir = Creator.NewOptionDirectory(dir, option.Name);
|
||||
var optionDir = ModCreator.NewOptionDirectory(dir, option.Name);
|
||||
if (!optionDir.Exists)
|
||||
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;
|
||||
|
||||
namespace Penumbra.Mods.Manager;
|
||||
namespace Penumbra.Mods;
|
||||
|
||||
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;
|
||||
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" ) );
|
||||
modManager.DataEditor.CreateMeta( dir, collection.Name, character ?? Penumbra.Config.DefaultModAuthor,
|
||||
$"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.Interop.ResourceLoading;
|
||||
using Penumbra.Interop.PathResolving;
|
||||
using Penumbra.Mods;
|
||||
using CharacterUtility = Penumbra.Interop.Services.CharacterUtility;
|
||||
using DalamudUtil = Dalamud.Utility.Util;
|
||||
using ResidentResourceManager = Penumbra.Interop.Services.ResidentResourceManager;
|
||||
|
|
|
|||
|
|
@ -269,9 +269,9 @@ public class ItemSwapTab : IDisposable, ITab
|
|||
|
||||
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);
|
||||
Mod.Creator.CreateDefaultFiles(newDir);
|
||||
ModCreator.CreateDefaultFiles(newDir);
|
||||
_modManager.AddMod(newDir);
|
||||
if (!_swapData.WriteMod(_modManager, _modManager[^1],
|
||||
_useFileSwaps ? ItemSwapContainer.WriteType.UseSwaps : ItemSwapContainer.WriteType.NoSwaps))
|
||||
|
|
@ -290,7 +290,7 @@ public class ItemSwapTab : IDisposable, ITab
|
|||
try
|
||||
{
|
||||
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);
|
||||
if (optionFolderName?.Exists == true)
|
||||
throw new Exception($"The folder {optionFolderName.FullName} for the option already exists.");
|
||||
|
|
|
|||
|
|
@ -217,13 +217,13 @@ public partial class ModEditWindow
|
|||
var fullName = subMod.FullName;
|
||||
if (fullName.EndsWith(": " + name))
|
||||
{
|
||||
path = Mod.Creator.NewOptionDirectory(path, fullName[..^(name.Length + 2)]);
|
||||
path = Mod.Creator.NewOptionDirectory(path, name);
|
||||
path = ModCreator.NewOptionDirectory(path, fullName[..^(name.Length + 2)]);
|
||||
path = ModCreator.NewOptionDirectory(path, name);
|
||||
subDirs = 2;
|
||||
}
|
||||
else
|
||||
{
|
||||
path = Mod.Creator.NewOptionDirectory(path, fullName);
|
||||
path = ModCreator.NewOptionDirectory(path, fullName);
|
||||
subDirs = 1;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -131,9 +131,9 @@ public sealed partial class ModFileSystemSelector : FileSystemSelector<Mod, ModF
|
|||
if (ImGuiUtil.OpenNameField("Create New Mod", ref _newModName))
|
||||
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);
|
||||
Mod.Creator.CreateDefaultFiles(newDir);
|
||||
ModCreator.CreateDefaultFiles(newDir);
|
||||
_modManager.AddMod(newDir);
|
||||
_newModName = string.Empty;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -30,20 +30,21 @@ public interface ISavable
|
|||
public class SaveService
|
||||
{
|
||||
private readonly Logger _log;
|
||||
private readonly FilenameService _fileNames;
|
||||
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;
|
||||
_fileNames = fileNames;
|
||||
_framework = framework;
|
||||
FileNames = fileNames;
|
||||
}
|
||||
|
||||
/// <summary> Queue a save for the next framework tick. </summary>
|
||||
public void QueueSave(ISavable value)
|
||||
{
|
||||
var file = value.ToFilename(_fileNames);
|
||||
var file = value.ToFilename(FileNames);
|
||||
_framework.RegisterDelayed(value.GetType().Name + file, () =>
|
||||
{
|
||||
ImmediateSave(value);
|
||||
|
|
@ -53,7 +54,7 @@ public class SaveService
|
|||
/// <summary> Immediately trigger a save. </summary>
|
||||
public void ImmediateSave(ISavable value)
|
||||
{
|
||||
var name = value.ToFilename(_fileNames);
|
||||
var name = value.ToFilename(FileNames);
|
||||
try
|
||||
{
|
||||
if (name.Length == 0)
|
||||
|
|
@ -76,7 +77,7 @@ public class SaveService
|
|||
|
||||
public void ImmediateDelete(ISavable value)
|
||||
{
|
||||
var name = value.ToFilename(_fileNames);
|
||||
var name = value.ToFilename(FileNames);
|
||||
try
|
||||
{
|
||||
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>
|
||||
public void SaveAllOptionGroups(Mod mod)
|
||||
{
|
||||
foreach (var file in _fileNames.GetOptionGroupFiles(mod))
|
||||
foreach (var file in FileNames.GetOptionGroupFiles(mod))
|
||||
{
|
||||
try
|
||||
{
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue