mirror of
https://github.com/xivdev/Penumbra.git
synced 2026-02-21 15:27:51 +01:00
Untangling the mods.
This commit is contained in:
parent
1d82e882ed
commit
4972dd1c9f
39 changed files with 883 additions and 935 deletions
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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; }
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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()));
|
||||
|
|
|
|||
|
|
@ -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));
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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 );
|
||||
}
|
||||
}
|
||||
|
|
@ -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();
|
||||
}
|
||||
|
|
@ -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}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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
476
Penumbra/Mods/ModCreator.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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.");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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}." );
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue