Untangling the mods.

This commit is contained in:
Ottermandias 2023-04-17 09:35:54 +02:00
parent 1d82e882ed
commit 4972dd1c9f
39 changed files with 883 additions and 935 deletions

View file

@ -244,7 +244,7 @@ public class DuplicateManager
try
{
var mod = new Mod(modDirectory);
mod.Reload(_modManager, true, out _);
_modManager.Creator.ReloadMod(mod, true, out _);
Finished = false;
_files.UpdateAll(mod, mod.Default);

View file

@ -13,5 +13,5 @@ public interface IMod
public ISubMod Default { get; }
public IReadOnlyList< IModGroup > Groups { get; }
public IEnumerable< ISubMod > AllSubMods { get; }
public IEnumerable< SubMod > AllSubMods { get; }
}

View file

@ -160,7 +160,7 @@ public class ModFileEditor
if (deletions <= 0)
return;
mod.Reload(_modManager, false, out _);
_modManager.Creator.ReloadMod(mod, false, out _);
_files.UpdateAll(mod, option);
}

View file

@ -221,10 +221,10 @@ public class ModCacheManager : IDisposable, IReadOnlyList<ModCache>
if (!_identifier.Valid)
return;
foreach (var gamePath in mod.AllRedirects)
foreach (var gamePath in mod.AllSubMods.SelectMany(m => m.Files.Keys.Concat(m.FileSwaps.Keys)))
_identifier.AwaitedService.Identify(cache.ChangedItems, gamePath.ToString());
foreach (var manip in mod.AllManipulations)
foreach (var manip in mod.AllSubMods.SelectMany(m => m.Manipulations))
ComputeChangedItems(_identifier.AwaitedService, cache.ChangedItems, manip);
cache.LowerChangedItemsString = string.Join("\0", cache.ChangedItems.Keys.Select(k => k.ToLowerInvariant()));

View file

@ -116,7 +116,7 @@ public class ModDataEditor
return changes;
}
public ModDataChangeType LoadMeta(Mod mod)
public ModDataChangeType LoadMeta(ModCreator creator, Mod mod)
{
var metaFile = _saveService.FileNames.ModMetaPath(mod);
if (!File.Exists(metaFile))
@ -171,7 +171,7 @@ public class ModDataEditor
}
if (newFileVersion != ModMeta.FileVersion)
if (ModMigration.Migrate(_saveService, mod, json, ref newFileVersion))
if (ModMigration.Migrate(creator, _saveService, mod, json, ref newFileVersion))
{
changes |= ModDataChangeType.Migration;
_saveService.ImmediateSave(new ModMeta(mod));

View file

@ -15,14 +15,14 @@ public sealed class ModFileSystem : FileSystem<Mod>, IDisposable, ISavable
{
private readonly ModManager _modManager;
private readonly CommunicatorService _communicator;
private readonly FilenameService _files;
private readonly SaveService _saveService;
// Create a new ModFileSystem from the currently loaded mods and the current sort order file.
public ModFileSystem(ModManager modManager, CommunicatorService communicator, FilenameService files)
public ModFileSystem(ModManager modManager, CommunicatorService communicator, SaveService saveService)
{
_modManager = modManager;
_communicator = communicator;
_files = files;
_saveService = saveService;
Reload();
Changed += OnChange;
_communicator.ModDiscoveryFinished.Subscribe(Reload);
@ -66,8 +66,8 @@ public sealed class ModFileSystem : FileSystem<Mod>, IDisposable, ISavable
private void Reload()
{
// TODO
if (Load(new FileInfo(_files.FilesystemFile), _modManager, ModToIdentifier, ModToName))
Penumbra.SaveService.ImmediateSave(this);
if (Load(new FileInfo(_saveService.FileNames.FilesystemFile), _modManager, ModToIdentifier, ModToName))
_saveService.ImmediateSave(this);
Penumbra.Log.Debug("Reloaded mod filesystem.");
}
@ -76,7 +76,7 @@ public sealed class ModFileSystem : FileSystem<Mod>, IDisposable, ISavable
private void OnChange(FileSystemChangeType type, IPath _1, IPath? _2, IPath? _3)
{
if (type != FileSystemChangeType.Reload)
Penumbra.SaveService.QueueSave(this);
_saveService.QueueSave(this);
}
// Update sort order when defaulted mod names change.
@ -111,7 +111,7 @@ public sealed class ModFileSystem : FileSystem<Mod>, IDisposable, ISavable
break;
case ModPathChangeType.Moved:
Penumbra.SaveService.QueueSave(this);
_saveService.QueueSave(this);
break;
case ModPathChangeType.Reloaded:
// Nothing

View file

@ -24,18 +24,21 @@ public sealed class ModManager : ModStorage
private readonly Configuration _config;
private readonly CommunicatorService _communicator;
public readonly ModCreator Creator;
public readonly ModDataEditor DataEditor;
public readonly ModOptionEditor OptionEditor;
public DirectoryInfo BasePath { get; private set; } = null!;
public bool Valid { get; private set; }
public ModManager(Configuration config, CommunicatorService communicator, ModDataEditor dataEditor, ModOptionEditor optionEditor)
public ModManager(Configuration config, CommunicatorService communicator, ModDataEditor dataEditor, ModOptionEditor optionEditor,
ModCreator creator)
{
_config = config;
_communicator = communicator;
DataEditor = dataEditor;
OptionEditor = optionEditor;
Creator = creator;
SetBaseDirectory(config.ModDirectory, true);
DiscoverMods();
}
@ -73,8 +76,8 @@ public sealed class ModManager : ModStorage
if (this.Any(m => m.ModPath.Name == modFolder.Name))
return;
ModCreator.SplitMultiGroups(modFolder);
var mod = Mod.LoadMod(this, modFolder, true);
Creator.SplitMultiGroups(modFolder);
var mod = Creator.LoadMod(modFolder, true);
if (mod == null)
return;
@ -119,7 +122,7 @@ public sealed class ModManager : ModStorage
var oldName = mod.Name;
_communicator.ModPathChanged.Invoke(ModPathChangeType.StartingReload, mod, mod.ModPath, mod.ModPath);
if (!mod.Reload(Penumbra.ModManager, true, out var metaChange))
if (!Creator.ReloadMod(mod, true, out var metaChange))
{
Penumbra.Log.Warning(mod.Name.Length == 0
? $"Reloading mod {oldName} has failed, new name is empty. Deleting instead."
@ -185,7 +188,7 @@ public sealed class ModManager : ModStorage
dir.Refresh();
mod.ModPath = dir;
if (!mod.Reload(Penumbra.ModManager, false, out var metaChange))
if (!Creator.ReloadMod(mod, false, out var metaChange))
{
Penumbra.Log.Error($"Error reloading moved mod {mod.Name}.");
return;
@ -307,7 +310,7 @@ public sealed class ModManager : ModStorage
var queue = new ConcurrentQueue<Mod>();
Parallel.ForEach(BasePath.EnumerateDirectories(), options, dir =>
{
var mod = Mod.LoadMod(this, dir, false);
var mod = Creator.LoadMod(dir, false);
if (mod != null)
queue.Enqueue(mod);
});

View file

@ -20,8 +20,8 @@ public static partial class ModMigration
[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);
public static bool Migrate(ModCreator creator, SaveService saveService, Mod mod, JObject json, ref uint fileVersion)
=> MigrateV0ToV1(creator, saveService, mod, json, ref fileVersion) || MigrateV1ToV2(saveService, mod, ref fileVersion) || MigrateV2ToV3(mod, ref fileVersion);
private static bool MigrateV2ToV3(Mod _, ref uint fileVersion)
{
@ -33,13 +33,13 @@ public static partial class ModMigration
return true;
}
private static bool MigrateV1ToV2(Mod mod, ref uint fileVersion)
private static bool MigrateV1ToV2(SaveService saveService, 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())
if (!saveService.FileNames.GetOptionGroupFiles(mod).All(g => GroupRegex().IsMatch(g.Name)))
foreach (var (group, index) in saveService.FileNames.GetOptionGroupFiles(mod).WithIndex().ToArray())
{
var newName = GroupStartRegex().Replace(group.Name, $"group_{index + 1:D3}_");
try
@ -58,7 +58,7 @@ public static partial class ModMigration
return true;
}
private static bool MigrateV0ToV1(SaveService saveService, Mod mod, JObject json, ref uint fileVersion)
private static bool MigrateV0ToV1(ModCreator creator, SaveService saveService, Mod mod, JObject json, ref uint fileVersion)
{
if (fileVersion > 0)
return false;
@ -69,21 +69,21 @@ public static partial class ModMigration
var priority = 1;
var seenMetaFiles = new HashSet<FullPath>();
foreach (var group in groups.Values)
ConvertGroup(mod, group, ref priority, seenMetaFiles);
ConvertGroup(creator, 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.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);
mod.Default.FileSwapData.Clear();
mod.Default.FileSwapData.EnsureCapacity(swaps.Count);
foreach (var (gamePath, swapPath) in swaps)
mod._default.FileSwapData.Add(gamePath, swapPath);
mod.Default.FileSwapData.Add(gamePath, swapPath);
mod._default.IncorporateMetaChanges(mod.ModPath, true);
creator.IncorporateMetaChanges(mod.Default, mod.ModPath, true);
foreach (var (_, index) in mod.Groups.WithIndex())
saveService.ImmediateSave(new ModSaveGroup(mod, index));
@ -118,7 +118,7 @@ public static partial class ModMigration
return true;
}
private static void ConvertGroup(Mod mod, OptionGroupV0 group, ref int priority, HashSet<FullPath> seenMetaFiles)
private static void ConvertGroup(ModCreator creator, Mod mod, OptionGroupV0 group, ref int priority, HashSet<FullPath> seenMetaFiles)
{
if (group.Options.Count == 0)
return;
@ -134,15 +134,15 @@ public static partial class ModMigration
Priority = priority++,
Description = string.Empty,
};
mod._groups.Add(newMultiGroup);
mod.Groups.Add(newMultiGroup);
foreach (var option in group.Options)
newMultiGroup.PrioritizedOptions.Add((SubModFromOption(mod, option, seenMetaFiles), optionPriority++));
newMultiGroup.PrioritizedOptions.Add((SubModFromOption(creator, mod, option, seenMetaFiles), optionPriority++));
break;
case GroupType.Single:
if (group.Options.Count == 1)
{
AddFilesToSubMod(mod._default, mod.ModPath, group.Options[0], seenMetaFiles);
AddFilesToSubMod(mod.Default, mod.ModPath, group.Options[0], seenMetaFiles);
return;
}
@ -152,9 +152,9 @@ public static partial class ModMigration
Priority = priority++,
Description = string.Empty,
};
mod._groups.Add(newSingleGroup);
mod.Groups.Add(newSingleGroup);
foreach (var option in group.Options)
newSingleGroup.OptionData.Add(SubModFromOption(mod, option, seenMetaFiles));
newSingleGroup.OptionData.Add(SubModFromOption(creator, mod, option, seenMetaFiles));
break;
}
@ -173,11 +173,11 @@ public static partial class ModMigration
}
}
private static SubMod SubModFromOption(Mod mod, OptionV0 option, HashSet<FullPath> seenMetaFiles)
private static SubMod SubModFromOption(ModCreator creator, 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);
creator.IncorporateMetaChanges(subMod, mod.ModPath, false);
return subMod;
}

View file

@ -46,11 +46,11 @@ public class ModOptionEditor
/// <summary> Change the type of a group given by mod and index to type, if possible. </summary>
public void ChangeModGroupType(Mod mod, int groupIdx, GroupType type)
{
var group = mod._groups[groupIdx];
var group = mod.Groups[groupIdx];
if (group.Type == type)
return;
mod._groups[groupIdx] = group.Convert(type);
mod.Groups[groupIdx] = group.Convert(type);
_saveService.QueueSave(new ModSaveGroup(mod, groupIdx));
_communicator.ModOptionChanged.Invoke(ModOptionChangeType.GroupTypeChanged, mod, groupIdx, -1, -1);
}
@ -58,7 +58,7 @@ public class ModOptionEditor
/// <summary> Change the settings stored as default options in a mod.</summary>
public void ChangeModGroupDefaultOption(Mod mod, int groupIdx, uint defaultOption)
{
var group = mod._groups[groupIdx];
var group = mod.Groups[groupIdx];
if (group.DefaultSettings == defaultOption)
return;
@ -70,7 +70,7 @@ public class ModOptionEditor
/// <summary> Rename an option group if possible. </summary>
public void RenameModGroup(Mod mod, int groupIdx, string newName)
{
var group = mod._groups[groupIdx];
var group = mod.Groups[groupIdx];
var oldName = group.Name;
if (oldName == newName || !VerifyFileName(mod, group, newName, true))
return;
@ -93,9 +93,9 @@ public class ModOptionEditor
if (!VerifyFileName(mod, null, newName, true))
return;
var maxPriority = mod._groups.Count == 0 ? 0 : mod._groups.Max(o => o.Priority) + 1;
var maxPriority = mod.Groups.Count == 0 ? 0 : mod.Groups.Max(o => o.Priority) + 1;
mod._groups.Add(type == GroupType.Multi
mod.Groups.Add(type == GroupType.Multi
? new MultiModGroup
{
Name = newName,
@ -106,16 +106,16 @@ public class ModOptionEditor
Name = newName,
Priority = maxPriority,
});
_saveService.ImmediateSave(new ModSaveGroup(mod, mod._groups.Count - 1));
_communicator.ModOptionChanged.Invoke(ModOptionChangeType.GroupAdded, mod, mod._groups.Count - 1, -1, -1);
_saveService.ImmediateSave(new ModSaveGroup(mod, mod.Groups.Count - 1));
_communicator.ModOptionChanged.Invoke(ModOptionChangeType.GroupAdded, mod, mod.Groups.Count - 1, -1, -1);
}
/// <summary> Delete a given option group. Fires an event to prepare before actually deleting. </summary>
public void DeleteModGroup(Mod mod, int groupIdx)
{
var group = mod._groups[groupIdx];
var group = mod.Groups[groupIdx];
_communicator.ModOptionChanged.Invoke(ModOptionChangeType.PrepareChange, mod, groupIdx, -1, -1);
mod._groups.RemoveAt(groupIdx);
mod.Groups.RemoveAt(groupIdx);
UpdateSubModPositions(mod, groupIdx);
_saveService.SaveAllOptionGroups(mod);
_communicator.ModOptionChanged.Invoke(ModOptionChangeType.GroupDeleted, mod, groupIdx, -1, -1);
@ -124,7 +124,7 @@ public class ModOptionEditor
/// <summary> Move the index of a given option group. </summary>
public void MoveModGroup(Mod mod, int groupIdxFrom, int groupIdxTo)
{
if (!mod._groups.Move(groupIdxFrom, groupIdxTo))
if (!mod.Groups.Move(groupIdxFrom, groupIdxTo))
return;
UpdateSubModPositions(mod, Math.Min(groupIdxFrom, groupIdxTo));
@ -135,7 +135,7 @@ public class ModOptionEditor
/// <summary> Change the description of the given option group. </summary>
public void ChangeGroupDescription(Mod mod, int groupIdx, string newDescription)
{
var group = mod._groups[groupIdx];
var group = mod.Groups[groupIdx];
if (group.Description == newDescription)
return;
@ -152,7 +152,7 @@ public class ModOptionEditor
/// <summary> Change the description of the given option. </summary>
public void ChangeOptionDescription(Mod mod, int groupIdx, int optionIdx, string newDescription)
{
var group = mod._groups[groupIdx];
var group = mod.Groups[groupIdx];
var option = group[optionIdx];
if (option.Description == newDescription || option is not SubMod s)
return;
@ -165,7 +165,7 @@ public class ModOptionEditor
/// <summary> Change the internal priority of the given option group. </summary>
public void ChangeGroupPriority(Mod mod, int groupIdx, int newPriority)
{
var group = mod._groups[groupIdx];
var group = mod.Groups[groupIdx];
if (group.Priority == newPriority)
return;
@ -182,7 +182,7 @@ public class ModOptionEditor
/// <summary> Change the internal priority of the given option. </summary>
public void ChangeOptionPriority(Mod mod, int groupIdx, int optionIdx, int newPriority)
{
switch (mod._groups[groupIdx])
switch (mod.Groups[groupIdx])
{
case SingleModGroup:
ChangeGroupPriority(mod, groupIdx, newPriority);
@ -201,7 +201,7 @@ public class ModOptionEditor
/// <summary> Rename the given option. </summary>
public void RenameOption(Mod mod, int groupIdx, int optionIdx, string newName)
{
switch (mod._groups[groupIdx])
switch (mod.Groups[groupIdx])
{
case SingleModGroup s:
if (s.OptionData[optionIdx].Name == newName)
@ -225,7 +225,7 @@ public class ModOptionEditor
/// <summary> Add a new empty option of the given name for the given group. </summary>
public void AddOption(Mod mod, int groupIdx, string newName)
{
var group = mod._groups[groupIdx];
var group = mod.Groups[groupIdx];
var subMod = new SubMod(mod) { Name = newName };
subMod.SetPosition(groupIdx, group.Count);
switch (group)
@ -248,7 +248,7 @@ public class ModOptionEditor
if (option is not SubMod o)
return;
var group = mod._groups[groupIdx];
var group = mod.Groups[groupIdx];
if (group.Type is GroupType.Multi && group.Count >= IModGroup.MaxMultiOptions)
{
Penumbra.Log.Error(
@ -276,7 +276,7 @@ public class ModOptionEditor
/// <summary> Delete the given option from the given group. </summary>
public void DeleteOption(Mod mod, int groupIdx, int optionIdx)
{
var group = mod._groups[groupIdx];
var group = mod.Groups[groupIdx];
_communicator.ModOptionChanged.Invoke(ModOptionChangeType.PrepareChange, mod, groupIdx, optionIdx, -1);
switch (group)
{
@ -297,7 +297,7 @@ public class ModOptionEditor
/// <summary> Move an option inside the given option group. </summary>
public void MoveOption(Mod mod, int groupIdx, int optionIdxFrom, int optionIdxTo)
{
var group = mod._groups[groupIdx];
var group = mod.Groups[groupIdx];
if (!group.MoveOption(optionIdxFrom, optionIdxTo))
return;
@ -379,7 +379,7 @@ public class ModOptionEditor
/// <summary> Update the indices stored in options from a given group on. </summary>
private static void UpdateSubModPositions(Mod mod, int fromGroup)
{
foreach (var (group, groupIdx) in mod._groups.WithIndex().Skip(fromGroup))
foreach (var (group, groupIdx) in mod.Groups.WithIndex().Skip(fromGroup))
{
foreach (var (o, optionIdx) in group.OfType<SubMod>().WithIndex())
o.SetPosition(groupIdx, optionIdx);
@ -390,9 +390,9 @@ public class ModOptionEditor
private static SubMod GetSubMod(Mod mod, int groupIdx, int optionIdx)
{
if (groupIdx == -1 && optionIdx == 0)
return mod._default;
return mod.Default;
return mod._groups[groupIdx] switch
return mod.Groups[groupIdx] switch
{
SingleModGroup s => s.OptionData[optionIdx],
MultiModGroup m => m.PrioritizedOptions[optionIdx].Mod,

View file

@ -31,76 +31,6 @@ public partial class Mod
internal Mod( DirectoryInfo modPath )
{
ModPath = modPath;
_default = new SubMod( this );
}
public static Mod? LoadMod( ModManager modManager, DirectoryInfo modPath, bool incorporateMetaChanges )
{
modPath.Refresh();
if( !modPath.Exists )
{
Penumbra.Log.Error( $"Supplied mod directory {modPath} does not exist." );
return null;
}
var mod = new Mod(modPath);
if (mod.Reload(modManager, incorporateMetaChanges, out _))
return mod;
// Can not be base path not existing because that is checked before.
Penumbra.Log.Warning( $"Mod at {modPath} without name is not supported." );
return null;
}
internal bool Reload(ModManager modManager, bool incorporateMetaChanges, out ModDataChangeType modDataChange )
{
modDataChange = ModDataChangeType.Deletion;
ModPath.Refresh();
if( !ModPath.Exists )
{
return false;
}
modDataChange = modManager.DataEditor.LoadMeta(this);
if( modDataChange.HasFlag( ModDataChangeType.Deletion ) || Name.Length == 0 )
{
return false;
}
modManager.DataEditor.LoadLocalData(this);
LoadDefaultOption();
LoadAllGroups();
if( incorporateMetaChanges )
{
IncorporateAllMetaChanges(true);
}
return true;
}
// Convert all .meta and .rgsp files to their respective meta changes and add them to their options.
// Deletes the source files if delete is true.
private void IncorporateAllMetaChanges( bool delete )
{
var changes = false;
List< string > deleteList = new();
foreach( var subMod in AllSubMods.OfType< SubMod >() )
{
var (localChanges, localDeleteList) = subMod.IncorporateMetaChanges( ModPath, false );
changes |= localChanges;
if( delete )
{
deleteList.AddRange( localDeleteList );
}
}
SubMod.DeleteDeleteList( deleteList, delete );
if( changes )
{
Penumbra.SaveService.SaveAllOptionGroups(this);
}
Default = new SubMod( this );
}
}

View file

@ -1,283 +0,0 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
using System.Text.RegularExpressions;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using OtterGui;
using OtterGui.Filesystem;
using Penumbra.Api.Enums;
using Penumbra.Import.Structs;
using Penumbra.String.Classes;
namespace Penumbra.Mods;
internal static partial class ModCreator
{
/// <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 )
{
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>
/// 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.EnumerateNonHiddenFiles()
.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();
}

View file

@ -1,139 +0,0 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using Newtonsoft.Json.Linq;
using OtterGui;
using Penumbra.Api.Enums;
using Penumbra.Meta;
using Penumbra.Meta.Manipulations;
using Penumbra.String.Classes;
namespace Penumbra.Mods;
public partial class Mod
{
public ISubMod Default
=> _default;
public IReadOnlyList<IModGroup> Groups
=> _groups;
internal readonly SubMod _default;
internal readonly List<IModGroup> _groups = new();
public IEnumerable<ISubMod> AllSubMods
=> _groups.SelectMany(o => o).Prepend(_default);
public IEnumerable<MetaManipulation> AllManipulations
=> AllSubMods.SelectMany(s => s.Manipulations);
public IEnumerable<Utf8GamePath> AllRedirects
=> AllSubMods.SelectMany(s => s.Files.Keys.Concat(s.FileSwaps.Keys));
public IEnumerable<FullPath> AllFiles
=> AllSubMods.SelectMany(o => o.Files)
.Select(p => p.Value);
public IEnumerable<FileInfo> GroupFiles
=> ModPath.EnumerateFiles("group_*.json");
public List<FullPath> FindUnusedFiles()
{
var modFiles = AllFiles.ToHashSet();
return ModPath.EnumerateDirectories()
.Where(d => !d.IsHidden())
.SelectMany(FileExtensions.EnumerateNonHiddenFiles)
.Select(f => new FullPath(f))
.Where(f => !modFiles.Contains(f))
.ToList();
}
private static IModGroup? LoadModGroup(Mod mod, FileInfo file, int groupIdx)
{
if (!File.Exists(file.FullName))
return null;
try
{
var json = JObject.Parse(File.ReadAllText(file.FullName));
switch (json[nameof(Type)]?.ToObject<GroupType>() ?? GroupType.Single)
{
case GroupType.Multi: return MultiModGroup.Load(mod, json, groupIdx);
case GroupType.Single: return SingleModGroup.Load(mod, json, groupIdx);
}
}
catch (Exception e)
{
Penumbra.Log.Error($"Could not read mod group from {file.FullName}:\n{e}");
}
return null;
}
private void LoadAllGroups()
{
_groups.Clear();
var changes = false;
foreach (var file in GroupFiles)
{
var group = LoadModGroup(this, file, _groups.Count);
if (group != null && _groups.All(g => g.Name != group.Name))
{
changes = changes || Penumbra.Filenames.OptionGroupFile(ModPath.FullName, Groups.Count, group.Name) != file.FullName;
_groups.Add(group);
}
else
{
changes = true;
}
}
if (changes)
Penumbra.SaveService.SaveAllOptionGroups(this);
}
private void LoadDefaultOption()
{
var defaultFile = Penumbra.Filenames.OptionGroupFile(this, -1);
_default.SetPosition(-1, 0);
try
{
if (!File.Exists(defaultFile))
_default.Load(ModPath, new JObject(), out _);
else
_default.Load(ModPath, JObject.Parse(File.ReadAllText(defaultFile)), out _);
}
catch (Exception e)
{
Penumbra.Log.Error($"Could not parse default file for {Name}:\n{e}");
}
}
public void WriteAllTexToolsMeta(MetaFileManager manager)
{
try
{
_default.WriteTexToolsMeta(manager, ModPath);
foreach (var group in Groups)
{
var dir = ModCreator.NewOptionDirectory(ModPath, group.Name);
if (!dir.Exists)
dir.Create();
foreach (var option in group.OfType<SubMod>())
{
var optionDir = ModCreator.NewOptionDirectory(dir, option.Name);
if (!optionDir.Exists)
optionDir.Create();
option.WriteTexToolsMeta(manager, optionDir);
}
}
}
catch (Exception e)
{
Penumbra.Log.Error($"Error writing TexToolsMeta:\n{e}");
}
}
}

View file

@ -1,6 +1,11 @@
using System;
using System.Collections.Generic;
using System.Linq;
using OtterGui;
using OtterGui.Classes;
using Penumbra.Import;
using Penumbra.Meta;
using Penumbra.String.Classes;
namespace Penumbra.Mods;
@ -29,7 +34,61 @@ public sealed partial class Mod : IMod
public bool Favorite { get; internal set; } = false;
// Options
public readonly SubMod Default;
public readonly List<IModGroup> Groups = new();
ISubMod IMod.Default
=> Default;
IReadOnlyList<IModGroup> IMod.Groups
=> Groups;
public IEnumerable<SubMod> AllSubMods
=> Groups.SelectMany(o => o).OfType<SubMod>().Prepend(Default);
public List<FullPath> FindUnusedFiles()
{
var modFiles = AllSubMods.SelectMany(o => o.Files)
.Select(p => p.Value)
.ToHashSet();
return ModPath.EnumerateDirectories()
.Where(d => !d.IsHidden())
.SelectMany(FileExtensions.EnumerateNonHiddenFiles)
.Select(f => new FullPath(f))
.Where(f => !modFiles.Contains(f))
.ToList();
}
// Access
public override string ToString()
=> Name.Text;
public void WriteAllTexToolsMeta(MetaFileManager manager)
{
try
{
TexToolsMeta.WriteTexToolsMeta(manager, Default.Manipulations, ModPath);
foreach (var group in Groups)
{
var dir = ModCreator.NewOptionDirectory(ModPath, group.Name);
if (!dir.Exists)
dir.Create();
foreach (var option in group.OfType<SubMod>())
{
var optionDir = ModCreator.NewOptionDirectory(dir, option.Name);
if (!optionDir.Exists)
optionDir.Create();
TexToolsMeta.WriteTexToolsMeta(manager, option.Manipulations, optionDir);
}
}
}
catch (Exception e)
{
Penumbra.Log.Error($"Error writing TexToolsMeta:\n{e}");
}
}
}

476
Penumbra/Mods/ModCreator.cs Normal file
View file

@ -0,0 +1,476 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
using System.Text.RegularExpressions;
using Dalamud.Interface.Internal.Notifications;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using OtterGui;
using OtterGui.Filesystem;
using Penumbra.Api.Enums;
using Penumbra.GameData;
using Penumbra.Import;
using Penumbra.Import.Structs;
using Penumbra.Meta;
using Penumbra.Mods.Manager;
using Penumbra.String.Classes;
using Penumbra.Util;
namespace Penumbra.Mods;
public partial class ModCreator
{
private readonly Configuration _config;
private readonly SaveService _saveService;
private readonly ModDataEditor _dataEditor;
private readonly MetaFileManager _metaFileManager;
private readonly IGamePathParser _gamePathParser;
public ModCreator(SaveService saveService, Configuration config, ModDataEditor dataEditor, MetaFileManager metaFileManager,
IGamePathParser gamePathParser)
{
_saveService = saveService;
_config = config;
_dataEditor = dataEditor;
_metaFileManager = metaFileManager;
_gamePathParser = gamePathParser;
}
/// <summary> Creates directory and files necessary for a new mod without adding it to the manager. </summary>
public DirectoryInfo? CreateEmptyMod(DirectoryInfo basePath, string newName, string description = "")
{
try
{
var newDir = CreateModFolder(basePath, newName);
_dataEditor.CreateMeta(newDir, newName, _config.DefaultModAuthor, description, "1.0", string.Empty);
CreateDefaultFiles(newDir);
return newDir;
}
catch (Exception e)
{
Penumbra.ChatService.NotificationMessage($"Could not create directory for new Mod {newName}:\n{e}", "Failure",
NotificationType.Error);
return null;
}
}
/// <summary> Load a mod by its directory. </summary>
public Mod? LoadMod(DirectoryInfo modPath, bool incorporateMetaChanges)
{
modPath.Refresh();
if (!modPath.Exists)
{
Penumbra.Log.Error($"Supplied mod directory {modPath} does not exist.");
return null;
}
var mod = new Mod(modPath);
if (ReloadMod(mod, incorporateMetaChanges, out _))
return mod;
// Can not be base path not existing because that is checked before.
Penumbra.Log.Warning($"Mod at {modPath} without name is not supported.");
return null;
}
/// <summary> Reload a mod from its mod path. </summary>
public bool ReloadMod(Mod mod, bool incorporateMetaChanges, out ModDataChangeType modDataChange)
{
modDataChange = ModDataChangeType.Deletion;
if (!Directory.Exists(mod.ModPath.FullName))
return false;
modDataChange = _dataEditor.LoadMeta(this, mod);
if (modDataChange.HasFlag(ModDataChangeType.Deletion) || mod.Name.Length == 0)
return false;
_dataEditor.LoadLocalData(mod);
LoadDefaultOption(mod);
LoadAllGroups(mod);
if (incorporateMetaChanges)
IncorporateAllMetaChanges(mod, true);
return true;
}
/// <summary> Load all option groups for a given mod. </summary>
public void LoadAllGroups(Mod mod)
{
mod.Groups.Clear();
var changes = false;
foreach (var file in _saveService.FileNames.GetOptionGroupFiles(mod))
{
var group = LoadModGroup(mod, file, mod.Groups.Count);
if (group != null && mod.Groups.All(g => g.Name != group.Name))
{
changes = changes
|| _saveService.FileNames.OptionGroupFile(mod.ModPath.FullName, mod.Groups.Count, group.Name) != file.FullName;
mod.Groups.Add(group);
}
else
{
changes = true;
}
}
if (changes)
_saveService.SaveAllOptionGroups(mod);
}
/// <summary> Load the default option for a given mod.</summary>
public void LoadDefaultOption(Mod mod)
{
var defaultFile = _saveService.FileNames.OptionGroupFile(mod, -1);
mod.Default.SetPosition(-1, 0);
try
{
if (!File.Exists(defaultFile))
mod.Default.Load(mod.ModPath, new JObject(), out _);
else
mod.Default.Load(mod.ModPath, JObject.Parse(File.ReadAllText(defaultFile)), out _);
}
catch (Exception e)
{
Penumbra.Log.Error($"Could not parse default file for {mod.Name}:\n{e}");
}
}
/// <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>
public static DirectoryInfo CreateModFolder(DirectoryInfo outDirectory, string modListName, bool create = true)
{
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);
}
/// <summary>
/// Convert all .meta and .rgsp files to their respective meta changes and add them to their options.
/// Deletes the source files if delete is true.
/// </summary>
public void IncorporateAllMetaChanges(Mod mod, bool delete)
{
var changes = false;
List<string> deleteList = new();
foreach (var subMod in mod.AllSubMods)
{
var (localChanges, localDeleteList) = IncorporateMetaChanges(subMod, mod.ModPath, false);
changes |= localChanges;
if (delete)
deleteList.AddRange(localDeleteList);
}
SubMod.DeleteDeleteList(deleteList, delete);
if (!changes)
return;
_saveService.SaveAllOptionGroups(mod);
_saveService.ImmediateSave(new ModSaveGroup(mod.ModPath, mod.Default));
}
/// <summary>
/// If .meta or .rgsp files are encountered, parse them and incorporate their meta changes into the mod.
/// If delete is true, the files are deleted afterwards.
/// </summary>
public (bool Changes, List<string> DeleteList) IncorporateMetaChanges(SubMod option, DirectoryInfo basePath, bool delete)
{
var deleteList = new List<string>();
var oldSize = option.ManipulationData.Count;
var deleteString = delete ? "with deletion." : "without deletion.";
foreach (var (key, file) in option.Files.ToList())
{
var ext1 = key.Extension().AsciiToLower().ToString();
var ext2 = file.Extension.ToLowerInvariant();
try
{
if (ext1 == ".meta" || ext2 == ".meta")
{
option.FileData.Remove(key);
if (!file.Exists)
continue;
var meta = new TexToolsMeta(_metaFileManager, _gamePathParser, File.ReadAllBytes(file.FullName),
_config.KeepDefaultMetaChanges);
Penumbra.Log.Verbose(
$"Incorporating {file} as Metadata file of {meta.MetaManipulations.Count} manipulations {deleteString}");
deleteList.Add(file.FullName);
option.ManipulationData.UnionWith(meta.MetaManipulations);
}
else if (ext1 == ".rgsp" || ext2 == ".rgsp")
{
option.FileData.Remove(key);
if (!file.Exists)
continue;
var rgsp = TexToolsMeta.FromRgspFile(Penumbra.MetaFileManager, file.FullName, File.ReadAllBytes(file.FullName),
_config.KeepDefaultMetaChanges);
Penumbra.Log.Verbose(
$"Incorporating {file} as racial scaling file of {rgsp.MetaManipulations.Count} manipulations {deleteString}");
deleteList.Add(file.FullName);
option.ManipulationData.UnionWith(rgsp.MetaManipulations);
}
}
catch (Exception e)
{
Penumbra.Log.Error($"Could not incorporate meta changes in mod {basePath} from file {file.FullName}:\n{e}");
}
}
SubMod.DeleteDeleteList(deleteList, delete);
return (oldSize < option.ManipulationData.Count, deleteList);
}
/// <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 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)));
_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>());
_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 ISubMod CreateSubMod(DirectoryInfo baseFolder, DirectoryInfo optionFolder, OptionList option)
{
var list = optionFolder.EnumerateNonHiddenFiles()
.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);
IncorporateMetaChanges(mod, 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 void CreateDefaultFiles(DirectoryInfo directory)
{
var mod = new Mod(directory);
ReloadMod(mod, 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);
}
IncorporateMetaChanges(mod.Default, directory, true);
_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 void SplitMultiGroups(DirectoryInfo baseDir)
{
var mod = new Mod(baseDir);
var files = _saveService.FileNames.GetOptionGroupFiles(mod).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 is not { 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> Load an option group for a specific mod by its file and index. </summary>
private static IModGroup? LoadModGroup(Mod mod, FileInfo file, int groupIdx)
{
if (!File.Exists(file.FullName))
return null;
try
{
var json = JObject.Parse(File.ReadAllText(file.FullName));
switch (json[nameof(Type)]?.ToObject<GroupType>() ?? GroupType.Single)
{
case GroupType.Multi: return MultiModGroup.Load(mod, json, groupIdx);
case GroupType.Single: return SingleModGroup.Load(mod, json, groupIdx);
}
}
catch (Exception e)
{
Penumbra.Log.Error($"Could not read mod group from {file.FullName}:\n{e}");
}
return null;
}
}

View file

@ -93,57 +93,6 @@ public sealed class SubMod : ISubMod
ManipulationData.Add(s);
}
// If .meta or .rgsp files are encountered, parse them and incorporate their meta changes into the mod.
// If delete is true, the files are deleted afterwards.
public (bool Changes, List<string> DeleteList) IncorporateMetaChanges(DirectoryInfo basePath, bool delete)
{
var deleteList = new List<string>();
var oldSize = ManipulationData.Count;
var deleteString = delete ? "with deletion." : "without deletion.";
foreach (var (key, file) in Files.ToList())
{
var ext1 = key.Extension().AsciiToLower().ToString();
var ext2 = file.Extension.ToLowerInvariant();
try
{
if (ext1 == ".meta" || ext2 == ".meta")
{
FileData.Remove(key);
if (!file.Exists)
continue;
var meta = new TexToolsMeta(Penumbra.MetaFileManager, Penumbra.GamePathParser, File.ReadAllBytes(file.FullName),
Penumbra.Config.KeepDefaultMetaChanges);
Penumbra.Log.Verbose(
$"Incorporating {file} as Metadata file of {meta.MetaManipulations.Count} manipulations {deleteString}");
deleteList.Add(file.FullName);
ManipulationData.UnionWith(meta.MetaManipulations);
}
else if (ext1 == ".rgsp" || ext2 == ".rgsp")
{
FileData.Remove(key);
if (!file.Exists)
continue;
var rgsp = TexToolsMeta.FromRgspFile(Penumbra.MetaFileManager, file.FullName, File.ReadAllBytes(file.FullName),
Penumbra.Config.KeepDefaultMetaChanges);
Penumbra.Log.Verbose(
$"Incorporating {file} as racial scaling file of {rgsp.MetaManipulations.Count} manipulations {deleteString}");
deleteList.Add(file.FullName);
ManipulationData.UnionWith(rgsp.MetaManipulations);
}
}
catch (Exception e)
{
Penumbra.Log.Error($"Could not incorporate meta changes in mod {basePath} from file {file.FullName}:\n{e}");
}
}
DeleteDeleteList(deleteList, delete);
return (oldSize < ManipulationData.Count, deleteList);
}
internal static void DeleteDeleteList(IEnumerable<string> deleteList, bool delete)
{
if (!delete)
@ -161,63 +110,4 @@ public sealed class SubMod : ISubMod
}
}
}
public void WriteTexToolsMeta(MetaFileManager manager, DirectoryInfo basePath, bool test = false)
{
var files = TexToolsMeta.ConvertToTexTools(manager, Manipulations);
foreach (var (file, data) in files)
{
var path = Path.Combine(basePath.FullName, file);
try
{
Directory.CreateDirectory(Path.GetDirectoryName(path)!);
File.WriteAllBytes(path, data);
}
catch (Exception e)
{
Penumbra.Log.Error($"Could not write meta file {path}:\n{e}");
}
}
if (test)
TestMetaWriting(manager, files);
}
[Conditional("DEBUG")]
private void TestMetaWriting(MetaFileManager manager, Dictionary<string, byte[]> files)
{
var meta = new HashSet<MetaManipulation>(Manipulations.Count);
foreach (var (file, data) in files)
{
try
{
var x = file.EndsWith("rgsp")
? TexToolsMeta.FromRgspFile(manager, file, data, Penumbra.Config.KeepDefaultMetaChanges)
: new TexToolsMeta(manager, Penumbra.GamePathParser, data, Penumbra.Config.KeepDefaultMetaChanges);
meta.UnionWith(x.MetaManipulations);
}
catch
{
// ignored
}
}
if (!Manipulations.SetEquals(meta))
{
Penumbra.Log.Information("Meta Sets do not equal.");
foreach (var (m1, m2) in Manipulations.Zip(meta))
Penumbra.Log.Information($"{m1} {m1.EntryToString()} | {m2} {m2.EntryToString()}");
foreach (var m in Manipulations.Skip(meta.Count))
Penumbra.Log.Information($"{m} {m.EntryToString()} ");
foreach (var m in meta.Skip(Manipulations.Count))
Penumbra.Log.Information($"{m} {m.EntryToString()} ");
}
else
{
Penumbra.Log.Information("Meta Sets are equal.");
}
}
}

View file

@ -7,6 +7,7 @@ using Penumbra.Collections;
using Penumbra.Meta.Manipulations;
using Penumbra.Mods.Manager;
using Penumbra.String.Classes;
using Penumbra.Util;
namespace Penumbra.Mods;
@ -19,33 +20,33 @@ public class TemporaryMod : IMod
public int TotalManipulations
=> Default.Manipulations.Count;
public ISubMod Default
=> _default;
public readonly SubMod Default;
ISubMod IMod.Default
=> Default;
public IReadOnlyList< IModGroup > Groups
=> Array.Empty< IModGroup >();
public IEnumerable< ISubMod > AllSubMods
public IEnumerable< SubMod > AllSubMods
=> new[] { Default };
private readonly SubMod _default;
public TemporaryMod()
=> _default = new SubMod( this );
=> Default = new SubMod( this );
public void SetFile( Utf8GamePath gamePath, FullPath fullPath )
=> _default.FileData[ gamePath ] = fullPath;
=> Default.FileData[ gamePath ] = fullPath;
public bool SetManipulation( MetaManipulation manip )
=> _default.ManipulationData.Remove( manip ) | _default.ManipulationData.Add( manip );
=> Default.ManipulationData.Remove( manip ) | Default.ManipulationData.Add( manip );
public void SetAll( Dictionary< Utf8GamePath, FullPath > dict, HashSet< MetaManipulation > manips )
{
_default.FileData = dict;
_default.ManipulationData = manips;
Default.FileData = dict;
Default.ManipulationData = manips;
}
public static void SaveTempCollection( ModManager modManager, ModCollection collection, string? character = null )
public static void SaveTempCollection( SaveService saveService, ModManager modManager, ModCollection collection, string? character = null )
{
DirectoryInfo? dir = null;
try
@ -55,7 +56,7 @@ public class TemporaryMod : IMod
modManager.DataEditor.CreateMeta( dir, collection.Name, character ?? Penumbra.Config.DefaultModAuthor,
$"Mod generated from temporary collection {collection.Name} for {character ?? "Unknown Character"}.", null, null );
var mod = new Mod( dir );
var defaultMod = (SubMod) mod.Default;
var defaultMod = mod.Default;
foreach( var (gamePath, fullPath) in collection.ResolvedFiles )
{
if( gamePath.Path.EndsWith( ".imc"u8 ) )
@ -84,7 +85,7 @@ public class TemporaryMod : IMod
foreach( var manip in collection.MetaCache?.Manipulations ?? Array.Empty< MetaManipulation >() )
defaultMod.ManipulationData.Add( manip );
Penumbra.SaveService.ImmediateSave(new ModSaveGroup(dir, defaultMod));
saveService.ImmediateSave(new ModSaveGroup(dir, defaultMod));
modManager.AddMod( dir );
Penumbra.Log.Information( $"Successfully generated mod {mod.Name} at {mod.ModPath.FullName} for collection {collection.Name}." );
}