Bunch of work on Option Editor.

This commit is contained in:
Ottermandias 2023-03-27 17:09:19 +02:00
parent 1253079968
commit fbe2ed1a71
21 changed files with 749 additions and 595 deletions

View file

@ -19,8 +19,9 @@ namespace Penumbra.Collections;
public sealed partial class CollectionManager : IDisposable, IEnumerable<ModCollection> public sealed partial class CollectionManager : IDisposable, IEnumerable<ModCollection>
{ {
private readonly Mods.ModManager _modManager; private readonly ModManager _modManager;
private readonly CommunicatorService _communicator; private readonly CommunicatorService _communicator;
private readonly SaveService _saveService;
private readonly CharacterUtility _characterUtility; private readonly CharacterUtility _characterUtility;
private readonly ResidentResourceManager _residentResources; private readonly ResidentResourceManager _residentResources;
private readonly Configuration _config; private readonly Configuration _config;
@ -57,7 +58,8 @@ public sealed partial class CollectionManager : IDisposable, IEnumerable<ModColl
=> _collections; => _collections;
public CollectionManager(StartTracker timer, CommunicatorService communicator, FilenameService files, CharacterUtility characterUtility, public CollectionManager(StartTracker timer, CommunicatorService communicator, FilenameService files, CharacterUtility characterUtility,
ResidentResourceManager residentResources, Configuration config, Mods.ModManager modManager, IndividualCollections individuals) ResidentResourceManager residentResources, Configuration config, ModManager modManager, IndividualCollections individuals,
SaveService saveService)
{ {
using var time = timer.Measure(StartTimeType.Collections); using var time = timer.Measure(StartTimeType.Collections);
_communicator = communicator; _communicator = communicator;
@ -65,12 +67,13 @@ public sealed partial class CollectionManager : IDisposable, IEnumerable<ModColl
_residentResources = residentResources; _residentResources = residentResources;
_config = config; _config = config;
_modManager = modManager; _modManager = modManager;
_saveService = saveService;
Individuals = individuals; Individuals = individuals;
// The collection manager reacts to changes in mods by itself. // The collection manager reacts to changes in mods by itself.
_modManager.ModDiscoveryStarted += OnModDiscoveryStarted; _modManager.ModDiscoveryStarted += OnModDiscoveryStarted;
_modManager.ModDiscoveryFinished += OnModDiscoveryFinished; _modManager.ModDiscoveryFinished += OnModDiscoveryFinished;
_modManager.ModOptionChanged += OnModOptionsChanged; _communicator.ModOptionChanged.Event += OnModOptionsChanged;
_modManager.ModPathChanged += OnModPathChange; _modManager.ModPathChanged += OnModPathChange;
_communicator.CollectionChange.Event += SaveOnChange; _communicator.CollectionChange.Event += SaveOnChange;
_communicator.TemporaryGlobalModChange.Event += OnGlobalModChange; _communicator.TemporaryGlobalModChange.Event += OnGlobalModChange;
@ -86,7 +89,7 @@ public sealed partial class CollectionManager : IDisposable, IEnumerable<ModColl
_communicator.TemporaryGlobalModChange.Event -= OnGlobalModChange; _communicator.TemporaryGlobalModChange.Event -= OnGlobalModChange;
_modManager.ModDiscoveryStarted -= OnModDiscoveryStarted; _modManager.ModDiscoveryStarted -= OnModDiscoveryStarted;
_modManager.ModDiscoveryFinished -= OnModDiscoveryFinished; _modManager.ModDiscoveryFinished -= OnModDiscoveryFinished;
_modManager.ModOptionChanged -= OnModOptionsChanged; _communicator.ModOptionChanged.Event -= OnModOptionsChanged;
_modManager.ModPathChanged -= OnModPathChange; _modManager.ModPathChanged -= OnModPathChange;
} }
@ -279,7 +282,7 @@ public sealed partial class CollectionManager : IDisposable, IEnumerable<ModColl
foreach (var collection in this) foreach (var collection in this)
{ {
if (collection._settings[mod.Index]?.HandleChanges(type, mod, groupIdx, optionIdx, movedToIdx) ?? false) if (collection._settings[mod.Index]?.HandleChanges(type, mod, groupIdx, optionIdx, movedToIdx) ?? false)
Penumbra.SaveService.QueueSave(collection); _saveService.QueueSave(collection);
} }
// Handle changes that reload the mod if the changes did not need to be prepared, // Handle changes that reload the mod if the changes did not need to be prepared,
@ -310,7 +313,7 @@ public sealed partial class CollectionManager : IDisposable, IEnumerable<ModColl
} }
var defaultCollection = ModCollection.CreateNewEmpty((string)ModCollection.DefaultCollection); var defaultCollection = ModCollection.CreateNewEmpty((string)ModCollection.DefaultCollection);
Penumbra.SaveService.ImmediateSave(defaultCollection); _saveService.ImmediateSave(defaultCollection);
defaultCollection.Index = _collections.Count; defaultCollection.Index = _collections.Count;
_collections.Add(defaultCollection); _collections.Add(defaultCollection);
} }
@ -337,7 +340,7 @@ public sealed partial class CollectionManager : IDisposable, IEnumerable<ModColl
} }
if (changes) if (changes)
Penumbra.SaveService.ImmediateSave(collection); _saveService.ImmediateSave(collection);
} }
} }
@ -405,6 +408,7 @@ public sealed partial class CollectionManager : IDisposable, IEnumerable<ModColl
? "Assignment is redundant due to an identical unowned NPC assignment existing.\nYou can remove it." ? "Assignment is redundant due to an identical unowned NPC assignment existing.\nYou can remove it."
: string.Empty; : string.Empty;
} }
break; break;
// The group of all Characters is redundant if they are all equal to Default or unassigned. // The group of all Characters is redundant if they are all equal to Default or unassigned.
case CollectionType.MalePlayerCharacter: case CollectionType.MalePlayerCharacter:
@ -464,7 +468,8 @@ public sealed partial class CollectionManager : IDisposable, IEnumerable<ModColl
continue; continue;
if (assignment.Index == checkAssignment.Index) if (assignment.Index == checkAssignment.Index)
return $"Assignment is currently redundant due to overwriting {parentType.ToName()} with an identical collection.\nYou can remove it."; return
$"Assignment is currently redundant due to overwriting {parentType.ToName()} with an identical collection.\nYou can remove it.";
} }
break; break;

View file

@ -6,20 +6,23 @@ using System.Linq;
using System.Security.Cryptography; using System.Security.Cryptography;
using System.Threading.Tasks; using System.Threading.Tasks;
using Penumbra.String.Classes; using Penumbra.String.Classes;
using Penumbra.Util;
namespace Penumbra.Mods; namespace Penumbra.Mods;
public class DuplicateManager public class DuplicateManager
{ {
private readonly ModManager _modManager; private readonly SaveService _saveService;
private readonly ModManager _modManager;
private readonly SHA256 _hasher = SHA256.Create(); private readonly SHA256 _hasher = SHA256.Create();
private readonly ModFileCollection _files; private readonly ModFileCollection _files;
private readonly List<(FullPath[] Paths, long Size, byte[] Hash)> _duplicates = new(); private readonly List<(FullPath[] Paths, long Size, byte[] Hash)> _duplicates = new();
public DuplicateManager(ModFileCollection files, ModManager modManager) public DuplicateManager(ModFileCollection files, ModManager modManager, SaveService saveService)
{ {
_files = files; _files = files;
_modManager = modManager; _modManager = modManager;
_saveService = saveService;
} }
public IReadOnlyList<(FullPath[] Paths, long Size, byte[] Hash)> Duplicates public IReadOnlyList<(FullPath[] Paths, long Size, byte[] Hash)> Duplicates
@ -76,16 +79,13 @@ public class DuplicateManager
if (useModManager) if (useModManager)
{ {
_modManager.OptionSetFiles(mod, groupIdx, optionIdx, dict); _modManager.OptionEditor.OptionSetFiles(mod, groupIdx, optionIdx, dict);
} }
else else
{ {
var sub = (SubMod)subMod; var sub = (SubMod)subMod;
sub.FileData = dict; sub.FileData = dict;
if (groupIdx == -1) _saveService.ImmediateSave(new ModSaveGroup(mod, groupIdx));
mod.SaveDefaultMod();
else
IModGroup.Save(mod.Groups[groupIdx], mod.ModPath, groupIdx);
} }
} }

View file

@ -34,7 +34,7 @@ public class ModFileEditor
num += dict.TryAdd(path.Item2, file.File) ? 0 : 1; num += dict.TryAdd(path.Item2, file.File) ? 0 : 1;
} }
Penumbra.ModManager.OptionSetFiles(mod, option.GroupIdx, option.OptionIdx, dict); _modManager.OptionEditor.OptionSetFiles(mod, option.GroupIdx, option.OptionIdx, dict);
_files.UpdatePaths(mod, option); _files.UpdatePaths(mod, option);
return num; return num;
@ -54,7 +54,7 @@ public class ModFileEditor
var newDict = subMod.Files.Where(kvp => CheckAgainstMissing(mod, subMod, kvp.Value, kvp.Key, subMod == option)) var newDict = subMod.Files.Where(kvp => CheckAgainstMissing(mod, subMod, kvp.Value, kvp.Key, subMod == option))
.ToDictionary(kvp => kvp.Key, kvp => kvp.Value); .ToDictionary(kvp => kvp.Key, kvp => kvp.Value);
if (newDict.Count != subMod.Files.Count) if (newDict.Count != subMod.Files.Count)
_modManager.OptionSetFiles(mod, groupIdx, optionIdx, newDict); _modManager.OptionEditor.OptionSetFiles(mod, groupIdx, optionIdx, newDict);
} }
ModEditor.ApplyToAllOptions(mod, HandleSubMod); ModEditor.ApplyToAllOptions(mod, HandleSubMod);

View file

@ -109,7 +109,7 @@ public class ModMetaEditor
if (!Changes) if (!Changes)
return; return;
_modManager.OptionSetManipulations(mod, groupIdx, optionIdx, Recombine().ToHashSet()); _modManager.OptionEditor.OptionSetManipulations(mod, groupIdx, optionIdx, Recombine().ToHashSet());
Changes = false; Changes = false;
} }

View file

@ -11,7 +11,7 @@ namespace Penumbra.Mods;
public class ModNormalizer public class ModNormalizer
{ {
private readonly ModManager _modManager; private readonly ModManager _modManager;
private readonly List<List<Dictionary<Utf8GamePath, FullPath>>> _redirections = new(); private readonly List<List<Dictionary<Utf8GamePath, FullPath>>> _redirections = new();
public Mod Mod { get; private set; } = null!; public Mod Mod { get; private set; } = null!;
@ -280,9 +280,8 @@ public class ModNormalizer
private void ApplyRedirections() private void ApplyRedirections()
{ {
foreach (var option in Mod.AllSubMods.OfType<SubMod>()) foreach (var option in Mod.AllSubMods.OfType<SubMod>())
{ _modManager.OptionEditor.OptionSetFiles(Mod, option.GroupIdx, option.OptionIdx,
_modManager.OptionSetFiles(Mod, option.GroupIdx, option.OptionIdx, _redirections[option.GroupIdx + 1][option.OptionIdx]); _redirections[option.GroupIdx + 1][option.OptionIdx]);
}
++Step; ++Step;
} }

View file

@ -22,11 +22,11 @@ public class ModSwapEditor
public void Apply(Mod mod, int groupIdx, int optionIdx) public void Apply(Mod mod, int groupIdx, int optionIdx)
{ {
if (Changes) if (!Changes)
{ return;
_modManager.OptionSetFileSwaps(mod, groupIdx, optionIdx, _swaps);
Changes = false; _modManager.OptionEditor.OptionSetFileSwaps(mod, groupIdx, optionIdx, _swaps);
} Changes = false;
} }
public bool Changes { get; private set; } public bool Changes { get; private set; }

View file

@ -38,7 +38,7 @@ public class ItemSwapContainer
NoSwaps, NoSwaps,
} }
public bool WriteMod( Mod mod, WriteType writeType = WriteType.NoSwaps, DirectoryInfo? directory = null, int groupIndex = -1, int optionIndex = 0 ) public bool WriteMod( ModManager manager, Mod mod, WriteType writeType = WriteType.NoSwaps, DirectoryInfo? directory = null, int groupIndex = -1, int optionIndex = 0 )
{ {
var convertedManips = new HashSet< MetaManipulation >( Swaps.Count ); var convertedManips = new HashSet< MetaManipulation >( Swaps.Count );
var convertedFiles = new Dictionary< Utf8GamePath, FullPath >( Swaps.Count ); var convertedFiles = new Dictionary< Utf8GamePath, FullPath >( Swaps.Count );
@ -82,9 +82,9 @@ public class ItemSwapContainer
} }
} }
Penumbra.ModManager.OptionSetFiles( mod, groupIndex, optionIndex, convertedFiles ); manager.OptionEditor.OptionSetFiles( mod, groupIndex, optionIndex, convertedFiles );
Penumbra.ModManager.OptionSetFileSwaps( mod, groupIndex, optionIndex, convertedSwaps ); manager.OptionEditor.OptionSetFileSwaps( mod, groupIndex, optionIndex, convertedSwaps );
Penumbra.ModManager.OptionSetManipulations( mod, groupIndex, optionIndex, convertedManips ); manager.OptionEditor.OptionSetManipulations( mod, groupIndex, optionIndex, convertedManips );
return true; return true;
} }
catch( Exception e ) catch( Exception e )

View file

@ -2,12 +2,76 @@ using System;
using System.Collections; using System.Collections;
using System.Collections.Generic; using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis; using System.Diagnostics.CodeAnalysis;
using System.Linq;
using Penumbra.Services; using Penumbra.Services;
using Penumbra.Util; using Penumbra.Util;
namespace Penumbra.Mods; namespace Penumbra.Mods;
public sealed partial class ModManager : IReadOnlyList<Mod> public sealed class ModManager2 : IReadOnlyList<Mod>, IDisposable
{
public readonly ModDataEditor DataEditor;
public readonly ModOptionEditor OptionEditor;
/// <summary>
/// An easily accessible set of new mods.
/// Mods are added when they are created or imported.
/// Mods are removed when they are deleted or when they are toggled in any collection.
/// Also gets cleared on mod rediscovery.
/// </summary>
public readonly HashSet<Mod> NewMods = new();
public Mod this[int idx]
=> _mods[idx];
public Mod this[Index idx]
=> _mods[idx];
public int Count
=> _mods.Count;
public IEnumerator<Mod> GetEnumerator()
=> _mods.GetEnumerator();
IEnumerator IEnumerable.GetEnumerator()
=> GetEnumerator();
/// <summary>
/// Try to obtain a mod by its directory name (unique identifier, preferred),
/// or the first mod of the given name if no directory fits.
/// </summary>
public bool TryGetMod(string identifier, string modName, [NotNullWhen(true)] out Mod? mod)
{
mod = null;
foreach (var m in _mods)
{
if (string.Equals(m.Identifier, identifier, StringComparison.OrdinalIgnoreCase))
{
mod = m;
return true;
}
if (m.Name == modName)
mod ??= m;
}
return mod != null;
}
/// <summary> The actual list of mods. </summary>
private readonly List<Mod> _mods = new();
public ModManager2(ModDataEditor dataEditor, ModOptionEditor optionEditor)
{
DataEditor = dataEditor;
OptionEditor = optionEditor;
}
public void Dispose()
{ }
}
public sealed partial class ModManager : IReadOnlyList<Mod>, IDisposable
{ {
// Set when reading Config and migrating from v4 to v5. // Set when reading Config and migrating from v4 to v5.
public static bool MigrateModBackups = false; public static bool MigrateModBackups = false;
@ -38,21 +102,29 @@ public sealed partial class ModManager : IReadOnlyList<Mod>
private readonly Configuration _config; private readonly Configuration _config;
private readonly CommunicatorService _communicator; private readonly CommunicatorService _communicator;
public readonly ModDataEditor DataEditor; public readonly ModDataEditor DataEditor;
public readonly ModOptionEditor OptionEditor;
public ModManager(StartTracker time, Configuration config, CommunicatorService communicator, ModDataEditor dataEditor) public ModManager(StartTracker time, Configuration config, CommunicatorService communicator, ModDataEditor dataEditor,
ModOptionEditor optionEditor)
{ {
using var timer = time.Measure(StartTimeType.Mods); using var timer = time.Measure(StartTimeType.Mods);
_config = config; _config = config;
_communicator = communicator; _communicator = communicator;
DataEditor = dataEditor; DataEditor = dataEditor;
OptionEditor = optionEditor;
ModDirectoryChanged += OnModDirectoryChange; ModDirectoryChanged += OnModDirectoryChange;
SetBaseDirectory(config.ModDirectory, true); SetBaseDirectory(config.ModDirectory, true);
UpdateExportDirectory(_config.ExportDirectory, false); UpdateExportDirectory(_config.ExportDirectory, false);
ModOptionChanged += OnModOptionChange; _communicator.ModOptionChanged.Event += OnModOptionChange;
ModPathChanged += OnModPathChange; ModPathChanged += OnModPathChange;
DiscoverMods(); DiscoverMods();
} }
public void Dispose()
{
_communicator.ModOptionChanged.Event -= OnModOptionChange;
}
// Try to obtain a mod by its directory name (unique identifier, preferred), // Try to obtain a mod by its directory name (unique identifier, preferred),
// or the first mod of the given name if no directory fits. // or the first mod of the given name if no directory fits.
@ -73,4 +145,37 @@ public sealed partial class ModManager : IReadOnlyList<Mod>
return mod != null; return mod != null;
} }
private static void OnModOptionChange(ModOptionChangeType type, Mod mod, int groupIdx, int _, int _2)
{
if (type == ModOptionChangeType.PrepareChange)
return;
bool ComputeChangedItems()
{
mod.ComputeChangedItems();
return true;
}
// State can not change on adding groups, as they have no immediate options.
var unused = type switch
{
ModOptionChangeType.GroupAdded => ComputeChangedItems() & mod.SetCounts(),
ModOptionChangeType.GroupDeleted => ComputeChangedItems() & mod.SetCounts(),
ModOptionChangeType.GroupMoved => false,
ModOptionChangeType.GroupTypeChanged => mod.HasOptions = mod.Groups.Any(o => o.IsOption),
ModOptionChangeType.PriorityChanged => false,
ModOptionChangeType.OptionAdded => ComputeChangedItems() & mod.SetCounts(),
ModOptionChangeType.OptionDeleted => ComputeChangedItems() & mod.SetCounts(),
ModOptionChangeType.OptionMoved => false,
ModOptionChangeType.OptionFilesChanged => ComputeChangedItems()
& (0 < (mod.TotalFileCount = mod.AllSubMods.Sum(s => s.Files.Count))),
ModOptionChangeType.OptionSwapsChanged => ComputeChangedItems()
& (0 < (mod.TotalSwapCount = mod.AllSubMods.Sum(s => s.FileSwaps.Count))),
ModOptionChangeType.OptionMetaChanged => ComputeChangedItems()
& (0 < (mod.TotalManipulations = mod.AllSubMods.Sum(s => s.Manipulations.Count))),
ModOptionChangeType.DisplayChange => false,
_ => false,
};
}
} }

View file

@ -1,377 +1,386 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using Dalamud.Interface.Internal.Notifications; using Dalamud.Interface.Internal.Notifications;
using OtterGui; using OtterGui;
using OtterGui.Filesystem; using OtterGui.Filesystem;
using Penumbra.Api.Enums; using Penumbra.Api.Enums;
using Penumbra.Meta.Manipulations; using Penumbra.Meta.Manipulations;
using Penumbra.String.Classes; using Penumbra.Services;
using Penumbra.Util; using Penumbra.String.Classes;
using Penumbra.Util;
namespace Penumbra.Mods;
namespace Penumbra.Mods;
public sealed partial class ModManager
{
public delegate void ModOptionChangeDelegate(ModOptionChangeType type, Mod mod, int groupIdx, int optionIdx, int movedToIdx);
public event ModOptionChangeDelegate ModOptionChanged; public class ModOptionEditor
{
public void ChangeModGroupType(Mod mod, int groupIdx, GroupType type) private readonly CommunicatorService _communicator;
{ private readonly FilenameService _filenames;
var group = mod._groups[groupIdx]; private readonly SaveService _saveService;
if (group.Type == type)
return; public ModOptionEditor(CommunicatorService communicator, SaveService saveService, FilenameService filenames)
{
mod._groups[groupIdx] = group.Convert(type); _communicator = communicator;
ModOptionChanged.Invoke(ModOptionChangeType.GroupTypeChanged, mod, groupIdx, -1, -1); _saveService = saveService;
} _filenames = filenames;
}
public void ChangeModGroupDefaultOption(Mod mod, int groupIdx, uint defaultOption)
{ /// <summary> Change the type of a group given by mod and index to type, if possible. </summary>
var group = mod._groups[groupIdx]; public void ChangeModGroupType(Mod mod, int groupIdx, GroupType type)
if (group.DefaultSettings == defaultOption) {
return; var group = mod._groups[groupIdx];
if (group.Type == type)
group.DefaultSettings = defaultOption; return;
ModOptionChanged.Invoke(ModOptionChangeType.DefaultOptionChanged, mod, groupIdx, -1, -1);
} mod._groups[groupIdx] = group.Convert(type);
_saveService.QueueSave(new ModSaveGroup(mod, groupIdx));
public void RenameModGroup(Mod mod, int groupIdx, string newName) _communicator.ModOptionChanged.Invoke(ModOptionChangeType.GroupTypeChanged, mod, groupIdx, -1, -1);
{ }
var group = mod._groups[groupIdx];
var oldName = group.Name; /// <summary> Change the settings stored as default options in a mod.</summary>
if (oldName == newName || !VerifyFileName(mod, group, newName, true)) public void ChangeModGroupDefaultOption(Mod mod, int groupIdx, uint defaultOption)
return; {
var group = mod._groups[groupIdx];
group.DeleteFile(mod.ModPath, groupIdx); if (group.DefaultSettings == defaultOption)
return;
var _ = group switch
{ group.DefaultSettings = defaultOption;
SingleModGroup s => s.Name = newName, _saveService.QueueSave(new ModSaveGroup(mod, groupIdx));
MultiModGroup m => m.Name = newName, _communicator.ModOptionChanged.Invoke(ModOptionChangeType.DefaultOptionChanged, mod, groupIdx, -1, -1);
_ => newName, }
};
/// <summary> Rename an option group if possible. </summary>
ModOptionChanged.Invoke(ModOptionChangeType.GroupRenamed, mod, groupIdx, -1, -1); public void RenameModGroup(Mod mod, int groupIdx, string newName)
} {
var group = mod._groups[groupIdx];
public void AddModGroup(Mod mod, GroupType type, string newName) var oldName = group.Name;
{ if (oldName == newName || !VerifyFileName(mod, group, newName, true))
if (!VerifyFileName(mod, null, newName, true)) return;
return;
_saveService.ImmediateDelete(new ModSaveGroup(mod, groupIdx));
var maxPriority = mod._groups.Count == 0 ? 0 : mod._groups.Max(o => o.Priority) + 1; var _ = group switch
{
mod._groups.Add(type == GroupType.Multi SingleModGroup s => s.Name = newName,
? new MultiModGroup MultiModGroup m => m.Name = newName,
{ _ => newName,
Name = newName, };
Priority = maxPriority,
} _saveService.ImmediateSave(new ModSaveGroup(mod, groupIdx));
: new SingleModGroup _communicator.ModOptionChanged.Invoke(ModOptionChangeType.GroupRenamed, mod, groupIdx, -1, -1);
{ }
Name = newName,
Priority = maxPriority, /// <summary> Add a new mod, empty option group of the given type and name. </summary>
}); public void AddModGroup(Mod mod, GroupType type, string newName)
ModOptionChanged.Invoke(ModOptionChangeType.GroupAdded, mod, mod._groups.Count - 1, -1, -1); {
} if (!VerifyFileName(mod, null, newName, true))
return;
public void DeleteModGroup(Mod mod, int groupIdx)
{ var maxPriority = mod._groups.Count == 0 ? 0 : mod._groups.Max(o => o.Priority) + 1;
var group = mod._groups[groupIdx];
ModOptionChanged.Invoke(ModOptionChangeType.PrepareChange, mod, groupIdx, -1, -1); mod._groups.Add(type == GroupType.Multi
mod._groups.RemoveAt(groupIdx); ? new MultiModGroup
UpdateSubModPositions(mod, groupIdx); {
group.DeleteFile(mod.ModPath, groupIdx); Name = newName,
ModOptionChanged.Invoke(ModOptionChangeType.GroupDeleted, mod, groupIdx, -1, -1); Priority = maxPriority,
} }
: new SingleModGroup
public void MoveModGroup(Mod mod, int groupIdxFrom, int groupIdxTo) {
{ Name = newName,
if (mod._groups.Move(groupIdxFrom, groupIdxTo)) Priority = maxPriority,
{ });
UpdateSubModPositions(mod, Math.Min(groupIdxFrom, groupIdxTo)); _saveService.ImmediateSave(new ModSaveGroup(mod, mod._groups.Count - 1));
ModOptionChanged.Invoke(ModOptionChangeType.GroupMoved, mod, groupIdxFrom, -1, groupIdxTo); _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>
private static void UpdateSubModPositions(Mod mod, int fromGroup) public void DeleteModGroup(Mod mod, int groupIdx)
{ {
foreach (var (group, groupIdx) in mod._groups.WithIndex().Skip(fromGroup)) var group = mod._groups[groupIdx];
{ _communicator.ModOptionChanged.Invoke(ModOptionChangeType.PrepareChange, mod, groupIdx, -1, -1);
foreach (var (o, optionIdx) in group.OfType<SubMod>().WithIndex()) mod._groups.RemoveAt(groupIdx);
o.SetPosition(groupIdx, optionIdx); UpdateSubModPositions(mod, groupIdx);
} _saveService.SaveAllOptionGroups(mod);
} _communicator.ModOptionChanged.Invoke(ModOptionChangeType.GroupDeleted, mod, groupIdx, -1, -1);
}
public void ChangeGroupDescription(Mod mod, int groupIdx, string newDescription)
{ /// <summary> Move the index of a given option group. </summary>
var group = mod._groups[groupIdx]; public void MoveModGroup(Mod mod, int groupIdxFrom, int groupIdxTo)
if (group.Description == newDescription) {
return; if (!mod._groups.Move(groupIdxFrom, groupIdxTo))
return;
var _ = group switch
{ UpdateSubModPositions(mod, Math.Min(groupIdxFrom, groupIdxTo));
SingleModGroup s => s.Description = newDescription, _saveService.SaveAllOptionGroups(mod);
MultiModGroup m => m.Description = newDescription, _communicator.ModOptionChanged.Invoke(ModOptionChangeType.GroupMoved, mod, groupIdxFrom, -1, groupIdxTo);
_ => newDescription, }
};
ModOptionChanged.Invoke(ModOptionChangeType.DisplayChange, mod, groupIdx, -1, -1); /// <summary> Change the description of the given option group. </summary>
} public void ChangeGroupDescription(Mod mod, int groupIdx, string newDescription)
{
public void ChangeOptionDescription(Mod mod, int groupIdx, int optionIdx, string newDescription) var group = mod._groups[groupIdx];
{ if (group.Description == newDescription)
var group = mod._groups[groupIdx]; return;
var option = group[optionIdx];
if (option.Description == newDescription || option is not SubMod s) var _ = group switch
return; {
SingleModGroup s => s.Description = newDescription,
s.Description = newDescription; MultiModGroup m => m.Description = newDescription,
ModOptionChanged.Invoke(ModOptionChangeType.DisplayChange, mod, groupIdx, optionIdx, -1); _ => newDescription,
} };
_saveService.QueueSave(new ModSaveGroup(mod, groupIdx));
public void ChangeGroupPriority(Mod mod, int groupIdx, int newPriority) _communicator.ModOptionChanged.Invoke(ModOptionChangeType.DisplayChange, mod, groupIdx, -1, -1);
{ }
var group = mod._groups[groupIdx];
if (group.Priority == newPriority) /// <summary> Change the description of the given option. </summary>
return; public void ChangeOptionDescription(Mod mod, int groupIdx, int optionIdx, string newDescription)
{
var _ = group switch var group = mod._groups[groupIdx];
{ var option = group[optionIdx];
SingleModGroup s => s.Priority = newPriority, if (option.Description == newDescription || option is not SubMod s)
MultiModGroup m => m.Priority = newPriority, return;
_ => newPriority,
}; s.Description = newDescription;
ModOptionChanged.Invoke(ModOptionChangeType.PriorityChanged, mod, groupIdx, -1, -1); _saveService.QueueSave(new ModSaveGroup(mod, groupIdx));
} _communicator.ModOptionChanged.Invoke(ModOptionChangeType.DisplayChange, mod, groupIdx, optionIdx, -1);
}
public void ChangeOptionPriority(Mod mod, int groupIdx, int optionIdx, int newPriority)
{ /// <summary> Change the internal priority of the given option group. </summary>
switch (mod._groups[groupIdx]) public void ChangeGroupPriority(Mod mod, int groupIdx, int newPriority)
{ {
case SingleModGroup: var group = mod._groups[groupIdx];
ChangeGroupPriority(mod, groupIdx, newPriority); if (group.Priority == newPriority)
break; return;
case MultiModGroup m:
if (m.PrioritizedOptions[optionIdx].Priority == newPriority) var _ = group switch
return; {
SingleModGroup s => s.Priority = newPriority,
m.PrioritizedOptions[optionIdx] = (m.PrioritizedOptions[optionIdx].Mod, newPriority); MultiModGroup m => m.Priority = newPriority,
ModOptionChanged.Invoke(ModOptionChangeType.PriorityChanged, mod, groupIdx, optionIdx, -1); _ => newPriority,
return; };
} _saveService.QueueSave(new ModSaveGroup(mod, groupIdx));
} _communicator.ModOptionChanged.Invoke(ModOptionChangeType.PriorityChanged, mod, groupIdx, -1, -1);
}
public void RenameOption(Mod mod, int groupIdx, int optionIdx, string newName)
{ /// <summary> Change the internal priority of the given option. </summary>
switch (mod._groups[groupIdx]) public void ChangeOptionPriority(Mod mod, int groupIdx, int optionIdx, int newPriority)
{ {
case SingleModGroup s: switch (mod._groups[groupIdx])
if (s.OptionData[optionIdx].Name == newName) {
return; case SingleModGroup:
ChangeGroupPriority(mod, groupIdx, newPriority);
s.OptionData[optionIdx].Name = newName; break;
break; case MultiModGroup m:
case MultiModGroup m: if (m.PrioritizedOptions[optionIdx].Priority == newPriority)
var option = m.PrioritizedOptions[optionIdx].Mod; return;
if (option.Name == newName)
return; m.PrioritizedOptions[optionIdx] = (m.PrioritizedOptions[optionIdx].Mod, newPriority);
_saveService.QueueSave(new ModSaveGroup(mod, groupIdx));
option.Name = newName; _communicator.ModOptionChanged.Invoke(ModOptionChangeType.PriorityChanged, mod, groupIdx, optionIdx, -1);
break; return;
} }
}
ModOptionChanged.Invoke(ModOptionChangeType.DisplayChange, mod, groupIdx, optionIdx, -1);
} /// <summary> Rename the given option. </summary>
public void RenameOption(Mod mod, int groupIdx, int optionIdx, string newName)
public void AddOption(Mod mod, int groupIdx, string newName) {
{ switch (mod._groups[groupIdx])
var group = mod._groups[groupIdx]; {
var subMod = new SubMod(mod) { Name = newName }; case SingleModGroup s:
subMod.SetPosition(groupIdx, group.Count); if (s.OptionData[optionIdx].Name == newName)
switch (group) return;
{
case SingleModGroup s: s.OptionData[optionIdx].Name = newName;
s.OptionData.Add(subMod); break;
break; case MultiModGroup m:
case MultiModGroup m: var option = m.PrioritizedOptions[optionIdx].Mod;
m.PrioritizedOptions.Add((subMod, 0)); if (option.Name == newName)
break; return;
}
option.Name = newName;
ModOptionChanged.Invoke(ModOptionChangeType.OptionAdded, mod, groupIdx, group.Count - 1, -1); break;
} }
public void AddOption(Mod mod, int groupIdx, ISubMod option, int priority = 0) _saveService.QueueSave(new ModSaveGroup(mod, groupIdx));
{ _communicator.ModOptionChanged.Invoke(ModOptionChangeType.DisplayChange, mod, groupIdx, optionIdx, -1);
if (option is not SubMod o) }
return;
/// <summary> Add a new empty option of the given name for the given group. </summary>
var group = mod._groups[groupIdx]; public void AddOption(Mod mod, int groupIdx, string newName)
if (group.Count > 63) {
{ var group = mod._groups[groupIdx];
Penumbra.Log.Error( var subMod = new SubMod(mod) { Name = newName };
$"Could not add option {option.Name} to {group.Name} for mod {mod.Name}, " subMod.SetPosition(groupIdx, group.Count);
+ "since only up to 64 options are supported in one group."); switch (group)
return; {
} case SingleModGroup s:
s.OptionData.Add(subMod);
o.SetPosition(groupIdx, group.Count); break;
case MultiModGroup m:
switch (group) m.PrioritizedOptions.Add((subMod, 0));
{ break;
case SingleModGroup s: }
s.OptionData.Add(o);
break; _saveService.QueueSave(new ModSaveGroup(mod, groupIdx));
case MultiModGroup m: _communicator.ModOptionChanged.Invoke(ModOptionChangeType.OptionAdded, mod, groupIdx, group.Count - 1, -1);
m.PrioritizedOptions.Add((o, priority)); }
break;
} /// <summary> Add an existing option to a given group with a given priority. </summary>
public void AddOption(Mod mod, int groupIdx, ISubMod option, int priority = 0)
ModOptionChanged.Invoke(ModOptionChangeType.OptionAdded, mod, groupIdx, group.Count - 1, -1); {
} if (option is not SubMod o)
return;
public void DeleteOption(Mod mod, int groupIdx, int optionIdx)
{ var group = mod._groups[groupIdx];
var group = mod._groups[groupIdx]; if (group.Type is GroupType.Multi && group.Count >= IModGroup.MaxMultiOptions)
ModOptionChanged.Invoke(ModOptionChangeType.PrepareChange, mod, groupIdx, optionIdx, -1); {
switch (group) Penumbra.Log.Error(
{ $"Could not add option {option.Name} to {group.Name} for mod {mod.Name}, "
case SingleModGroup s: + $"since only up to {IModGroup.MaxMultiOptions} options are supported in one group.");
s.OptionData.RemoveAt(optionIdx); return;
}
break;
case MultiModGroup m: o.SetPosition(groupIdx, group.Count);
m.PrioritizedOptions.RemoveAt(optionIdx);
break; switch (group)
} {
case SingleModGroup s:
group.UpdatePositions(optionIdx); s.OptionData.Add(o);
ModOptionChanged.Invoke(ModOptionChangeType.OptionDeleted, mod, groupIdx, optionIdx, -1); break;
} case MultiModGroup m:
m.PrioritizedOptions.Add((o, priority));
public void MoveOption(Mod mod, int groupIdx, int optionIdxFrom, int optionIdxTo) break;
{ }
var group = mod._groups[groupIdx];
if (group.MoveOption(optionIdxFrom, optionIdxTo)) _saveService.QueueSave(new ModSaveGroup(mod, groupIdx));
ModOptionChanged.Invoke(ModOptionChangeType.OptionMoved, mod, groupIdx, optionIdxFrom, optionIdxTo); _communicator.ModOptionChanged.Invoke(ModOptionChangeType.OptionAdded, mod, groupIdx, group.Count - 1, -1);
} }
public void OptionSetManipulations(Mod mod, int groupIdx, int optionIdx, HashSet<MetaManipulation> manipulations) /// <summary> Delete the given option from the given group. </summary>
{ public void DeleteOption(Mod mod, int groupIdx, int optionIdx)
var subMod = GetSubMod(mod, groupIdx, optionIdx); {
if (subMod.Manipulations.Count == manipulations.Count var group = mod._groups[groupIdx];
&& subMod.Manipulations.All(m => manipulations.TryGetValue(m, out var old) && old.EntryEquals(m))) _communicator.ModOptionChanged.Invoke(ModOptionChangeType.PrepareChange, mod, groupIdx, optionIdx, -1);
return; switch (group)
{
ModOptionChanged.Invoke(ModOptionChangeType.PrepareChange, mod, groupIdx, optionIdx, -1); case SingleModGroup s:
subMod.ManipulationData = manipulations; s.OptionData.RemoveAt(optionIdx);
ModOptionChanged.Invoke(ModOptionChangeType.OptionMetaChanged, mod, groupIdx, optionIdx, -1);
} break;
case MultiModGroup m:
public void OptionSetFiles(Mod mod, int groupIdx, int optionIdx, Dictionary<Utf8GamePath, FullPath> replacements) m.PrioritizedOptions.RemoveAt(optionIdx);
{ break;
var subMod = GetSubMod(mod, groupIdx, optionIdx); }
if (subMod.FileData.SetEquals(replacements))
return; group.UpdatePositions(optionIdx);
_saveService.QueueSave(new ModSaveGroup(mod, groupIdx));
ModOptionChanged.Invoke(ModOptionChangeType.PrepareChange, mod, groupIdx, optionIdx, -1); _communicator.ModOptionChanged.Invoke(ModOptionChangeType.OptionDeleted, mod, groupIdx, optionIdx, -1);
subMod.FileData = replacements; }
ModOptionChanged.Invoke(ModOptionChangeType.OptionFilesChanged, mod, groupIdx, optionIdx, -1);
} /// <summary> Move an option inside the given option group. </summary>
public void MoveOption(Mod mod, int groupIdx, int optionIdxFrom, int optionIdxTo)
public void OptionAddFiles(Mod mod, int groupIdx, int optionIdx, Dictionary<Utf8GamePath, FullPath> additions) {
{ var group = mod._groups[groupIdx];
var subMod = GetSubMod(mod, groupIdx, optionIdx); if (!group.MoveOption(optionIdxFrom, optionIdxTo))
var oldCount = subMod.FileData.Count; return;
subMod.FileData.AddFrom(additions);
if (oldCount != subMod.FileData.Count) _saveService.QueueSave(new ModSaveGroup(mod, groupIdx));
ModOptionChanged.Invoke(ModOptionChangeType.OptionFilesAdded, mod, groupIdx, optionIdx, -1); _communicator.ModOptionChanged.Invoke(ModOptionChangeType.OptionMoved, mod, groupIdx, optionIdxFrom, optionIdxTo);
} }
public void OptionSetFileSwaps(Mod mod, int groupIdx, int optionIdx, Dictionary<Utf8GamePath, FullPath> swaps) /// <summary> Set the meta manipulations for a given option. Replaces existing manipulations. </summary>
{ public void OptionSetManipulations(Mod mod, int groupIdx, int optionIdx, HashSet<MetaManipulation> manipulations)
var subMod = GetSubMod(mod, groupIdx, optionIdx); {
if (subMod.FileSwapData.SetEquals(swaps)) var subMod = GetSubMod(mod, groupIdx, optionIdx);
return; if (subMod.Manipulations.Count == manipulations.Count
&& subMod.Manipulations.All(m => manipulations.TryGetValue(m, out var old) && old.EntryEquals(m)))
ModOptionChanged.Invoke(ModOptionChangeType.PrepareChange, mod, groupIdx, optionIdx, -1); return;
subMod.FileSwapData = swaps;
ModOptionChanged.Invoke(ModOptionChangeType.OptionSwapsChanged, mod, groupIdx, optionIdx, -1); _communicator.ModOptionChanged.Invoke(ModOptionChangeType.PrepareChange, mod, groupIdx, optionIdx, -1);
} subMod.ManipulationData = manipulations;
_saveService.QueueSave(new ModSaveGroup(mod, groupIdx));
public bool VerifyFileName(Mod mod, IModGroup? group, string newName, bool message) _communicator.ModOptionChanged.Invoke(ModOptionChangeType.OptionMetaChanged, mod, groupIdx, optionIdx, -1);
{ }
var path = newName.RemoveInvalidPathSymbols();
if (path.Length != 0 /// <summary> Set the file redirections for a given option. Replaces existing redirections. </summary>
&& !mod.Groups.Any(o => !ReferenceEquals(o, group) public void OptionSetFiles(Mod mod, int groupIdx, int optionIdx, Dictionary<Utf8GamePath, FullPath> replacements)
&& string.Equals(o.Name.RemoveInvalidPathSymbols(), path, StringComparison.OrdinalIgnoreCase))) {
return true; var subMod = GetSubMod(mod, groupIdx, optionIdx);
if (subMod.FileData.SetEquals(replacements))
if (message) return;
Penumbra.ChatService.NotificationMessage(
$"Could not name option {newName} because option with same filename {path} already exists.", _communicator.ModOptionChanged.Invoke(ModOptionChangeType.PrepareChange, mod, groupIdx, optionIdx, -1);
"Warning", NotificationType.Warning); subMod.FileData = replacements;
_saveService.QueueSave(new ModSaveGroup(mod, groupIdx));
return false; _communicator.ModOptionChanged.Invoke(ModOptionChangeType.OptionFilesChanged, mod, groupIdx, optionIdx, -1);
} }
private static SubMod GetSubMod(Mod mod, int groupIdx, int optionIdx) /// <summary> Add additional file redirections to a given option, keeping already existing ones. Only fires an event if anything is actually added.</summary>
{ public void OptionAddFiles(Mod mod, int groupIdx, int optionIdx, Dictionary<Utf8GamePath, FullPath> additions)
if (groupIdx == -1 && optionIdx == 0) {
return mod._default; var subMod = GetSubMod(mod, groupIdx, optionIdx);
var oldCount = subMod.FileData.Count;
return mod._groups[groupIdx] switch subMod.FileData.AddFrom(additions);
{ if (oldCount != subMod.FileData.Count)
SingleModGroup s => s.OptionData[optionIdx], {
MultiModGroup m => m.PrioritizedOptions[optionIdx].Mod, _saveService.QueueSave(new ModSaveGroup(mod, groupIdx));
_ => throw new InvalidOperationException(), _communicator.ModOptionChanged.Invoke(ModOptionChangeType.OptionFilesAdded, mod, groupIdx, optionIdx, -1);
}; }
} }
private static void OnModOptionChange(ModOptionChangeType type, Mod mod, int groupIdx, int _, int _2) /// <summary> Set the file swaps for a given option. Replaces existing swaps. </summary>
{ public void OptionSetFileSwaps(Mod mod, int groupIdx, int optionIdx, Dictionary<Utf8GamePath, FullPath> swaps)
if (type == ModOptionChangeType.PrepareChange) {
return; var subMod = GetSubMod(mod, groupIdx, optionIdx);
if (subMod.FileSwapData.SetEquals(swaps))
// File deletion is handled in the actual function. return;
if (type is ModOptionChangeType.GroupDeleted or ModOptionChangeType.GroupMoved)
{ _communicator.ModOptionChanged.Invoke(ModOptionChangeType.PrepareChange, mod, groupIdx, optionIdx, -1);
mod.SaveAllGroups(); subMod.FileSwapData = swaps;
} _saveService.QueueSave(new ModSaveGroup(mod, groupIdx));
else _communicator.ModOptionChanged.Invoke(ModOptionChangeType.OptionSwapsChanged, mod, groupIdx, optionIdx, -1);
{ }
if (groupIdx == -1)
mod.SaveDefaultModDelayed();
else /// <summary> Verify that a new option group name is unique in this mod. </summary>
IModGroup.SaveDelayed(mod._groups[groupIdx], mod.ModPath, groupIdx); public static bool VerifyFileName(Mod mod, IModGroup? group, string newName, bool message)
} {
var path = newName.RemoveInvalidPathSymbols();
bool ComputeChangedItems() if (path.Length != 0
{ && !mod.Groups.Any(o => !ReferenceEquals(o, group)
mod.ComputeChangedItems(); && string.Equals(o.Name.RemoveInvalidPathSymbols(), path, StringComparison.OrdinalIgnoreCase)))
return true; return true;
}
if (message)
// State can not change on adding groups, as they have no immediate options. Penumbra.ChatService.NotificationMessage(
var unused = type switch $"Could not name option {newName} because option with same filename {path} already exists.",
{ "Warning", NotificationType.Warning);
ModOptionChangeType.GroupAdded => ComputeChangedItems() & mod.SetCounts(),
ModOptionChangeType.GroupDeleted => ComputeChangedItems() & mod.SetCounts(), return false;
ModOptionChangeType.GroupMoved => false, }
ModOptionChangeType.GroupTypeChanged => mod.HasOptions = mod.Groups.Any(o => o.IsOption),
ModOptionChangeType.PriorityChanged => false, /// <summary> Update the indices stored in options from a given group on. </summary>
ModOptionChangeType.OptionAdded => ComputeChangedItems() & mod.SetCounts(), private static void UpdateSubModPositions(Mod mod, int fromGroup)
ModOptionChangeType.OptionDeleted => ComputeChangedItems() & mod.SetCounts(), {
ModOptionChangeType.OptionMoved => false, foreach (var (group, groupIdx) in mod._groups.WithIndex().Skip(fromGroup))
ModOptionChangeType.OptionFilesChanged => ComputeChangedItems() {
& (0 < (mod.TotalFileCount = mod.AllSubMods.Sum(s => s.Files.Count))), foreach (var (o, optionIdx) in group.OfType<SubMod>().WithIndex())
ModOptionChangeType.OptionSwapsChanged => ComputeChangedItems() o.SetPosition(groupIdx, optionIdx);
& (0 < (mod.TotalSwapCount = mod.AllSubMods.Sum(s => s.FileSwaps.Count))), }
ModOptionChangeType.OptionMetaChanged => ComputeChangedItems() }
& (0 < (mod.TotalManipulations = mod.AllSubMods.Sum(s => s.Manipulations.Count))),
ModOptionChangeType.DisplayChange => false, /// <summary> Get the correct option for the given group and option index. </summary>
_ => false, private static SubMod GetSubMod(Mod mod, int groupIdx, int optionIdx)
}; {
} if (groupIdx == -1 && optionIdx == 0)
} return mod._default;
return mod._groups[groupIdx] switch
{
SingleModGroup s => s.OptionData[optionIdx],
MultiModGroup m => m.PrioritizedOptions[optionIdx].Mod,
_ => throw new InvalidOperationException(),
};
}
}

View file

@ -101,7 +101,7 @@ public partial class Mod
if( changes ) if( changes )
{ {
SaveAllGroups(); Penumbra.SaveService.SaveAllOptionGroups(this);
} }
} }
} }

View file

@ -4,10 +4,8 @@ using System.IO;
using System.Linq; using System.Linq;
using System.Text; using System.Text;
using System.Text.RegularExpressions; using System.Text.RegularExpressions;
using Dalamud.Utility;
using Newtonsoft.Json; using Newtonsoft.Json;
using Newtonsoft.Json.Linq; using Newtonsoft.Json.Linq;
using OtterGui.Classes;
using OtterGui.Filesystem; using OtterGui.Filesystem;
using Penumbra.Api.Enums; using Penumbra.Api.Enums;
using Penumbra.Import.Structs; using Penumbra.Import.Structs;
@ -79,8 +77,8 @@ public partial class Mod
Priority = priority, Priority = priority,
DefaultSettings = defaultSettings, DefaultSettings = defaultSettings,
}; };
group.PrioritizedOptions.AddRange( subMods.OfType< SubMod >().Select( ( s, idx ) => ( s, idx ) ) ); group.PrioritizedOptions.AddRange( subMods.OfType< SubMod >().Select( ( s, idx ) => ( s, idx ) ) );
IModGroup.Save( group, baseFolder, index ); Penumbra.SaveService.ImmediateSave(new ModSaveGroup(baseFolder, group, index));
break; break;
} }
case GroupType.Single: case GroupType.Single:
@ -93,7 +91,7 @@ public partial class Mod
DefaultSettings = defaultSettings, DefaultSettings = defaultSettings,
}; };
group.OptionData.AddRange( subMods.OfType< SubMod >() ); group.OptionData.AddRange( subMods.OfType< SubMod >() );
IModGroup.Save( group, baseFolder, index ); Penumbra.SaveService.ImmediateSave(new ModSaveGroup(baseFolder, group, index));
break; break;
} }
} }

View file

@ -103,7 +103,7 @@ public partial class Mod
var group = LoadModGroup( this, file, _groups.Count ); var group = LoadModGroup( this, file, _groups.Count );
if( group != null && _groups.All( g => g.Name != group.Name ) ) if( group != null && _groups.All( g => g.Name != group.Name ) )
{ {
changes = changes || group.FileName( ModPath, _groups.Count ) != file.FullName; changes = changes || Penumbra.Filenames.OptionGroupFile(ModPath.FullName, Groups.Count, group.Name) != file.FullName;
_groups.Add( group ); _groups.Add( group );
} }
else else
@ -114,32 +114,7 @@ public partial class Mod
if( changes ) if( changes )
{ {
SaveAllGroups(); Penumbra.SaveService.SaveAllOptionGroups(this);
}
}
// Delete all existing group files and save them anew.
// Used when indices change in complex ways.
internal void SaveAllGroups()
{
foreach( var file in GroupFiles )
{
try
{
if( file.Exists )
{
file.Delete();
}
}
catch( Exception e )
{
Penumbra.Log.Error( $"Could not delete outdated group file {file}:\n{e}" );
}
}
foreach( var (group, index) in _groups.WithIndex() )
{
IModGroup.Save( group, ModPath, index );
} }
} }
} }

View file

@ -85,8 +85,8 @@ public sealed partial class Mod
mod._default.FileSwapData.Add(gamePath, swapPath); mod._default.FileSwapData.Add(gamePath, swapPath);
mod._default.IncorporateMetaChanges(mod.ModPath, true); mod._default.IncorporateMetaChanges(mod.ModPath, true);
foreach (var (group, index) in mod.Groups.WithIndex()) foreach (var (_, index) in mod.Groups.WithIndex())
IModGroup.Save(group, mod.ModPath, index); Penumbra.SaveService.ImmediateSave(new ModSaveGroup(mod, index));
// Delete meta files. // Delete meta files.
foreach (var file in seenMetaFiles.Where(f => f.Exists)) foreach (var file in seenMetaFiles.Where(f => f.Exists))

View file

@ -2,24 +2,25 @@ using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.IO; using System.IO;
using Newtonsoft.Json; using Newtonsoft.Json;
using OtterGui.Filesystem;
using Penumbra.Api.Enums; using Penumbra.Api.Enums;
using Penumbra.Services;
using Penumbra.Util;
namespace Penumbra.Mods; namespace Penumbra.Mods;
public interface IModGroup : IEnumerable< ISubMod > public interface IModGroup : IEnumerable<ISubMod>
{ {
public const int MaxMultiOptions = 32; public const int MaxMultiOptions = 32;
public string Name { get; } public string Name { get; }
public string Description { get; } public string Description { get; }
public GroupType Type { get; } public GroupType Type { get; }
public int Priority { get; } public int Priority { get; }
public uint DefaultSettings { get; set; } public uint DefaultSettings { get; set; }
public int OptionPriority( Index optionIdx ); public int OptionPriority(Index optionIdx);
public ISubMod this[ Index idx ] { get; } public ISubMod this[Index idx] { get; }
public int Count { get; } public int Count { get; }
@ -28,72 +29,76 @@ public interface IModGroup : IEnumerable< ISubMod >
{ {
GroupType.Single => Count > 1, GroupType.Single => Count > 1,
GroupType.Multi => Count > 0, GroupType.Multi => Count > 0,
_ => false, _ => false,
}; };
public string FileName( DirectoryInfo basePath, int groupIdx ) public IModGroup Convert(GroupType type);
=> Path.Combine( basePath.FullName, $"group_{groupIdx + 1:D3}_{Name.RemoveInvalidPathSymbols().ToLowerInvariant()}.json" ); public bool MoveOption(int optionIdxFrom, int optionIdxTo);
public void UpdatePositions(int from = 0);
}
public void DeleteFile( DirectoryInfo basePath, int groupIdx ) public readonly struct ModSaveGroup : ISavable
{
private readonly DirectoryInfo _basePath;
private readonly IModGroup? _group;
private readonly int _groupIdx;
private readonly ISubMod? _defaultMod;
public ModSaveGroup(Mod mod, int groupIdx)
{ {
var file = FileName( basePath, groupIdx ); _basePath = mod.ModPath;
if( !File.Exists( file ) ) if (_groupIdx < 0)
{ _defaultMod = mod.Default;
return; else
} _group = mod.Groups[groupIdx];
_groupIdx = groupIdx;
try
{
File.Delete( file );
Penumbra.Log.Debug( $"Deleted group file {file} for group {groupIdx + 1}: {Name}." );
}
catch( Exception e )
{
Penumbra.Log.Error( $"Could not delete file {file}:\n{e}" );
throw;
}
} }
public static void SaveDelayed( IModGroup group, DirectoryInfo basePath, int groupIdx ) public ModSaveGroup(DirectoryInfo basePath, IModGroup group, int groupIdx)
{ {
Penumbra.Framework.RegisterDelayed( $"{nameof( SaveModGroup )}_{basePath.Name}_{group.Name}", _basePath = basePath;
() => SaveModGroup( group, basePath, groupIdx ) ); _group = group;
_groupIdx = groupIdx;
} }
public static void Save( IModGroup group, DirectoryInfo basePath, int groupIdx ) public ModSaveGroup(DirectoryInfo basePath, ISubMod @default)
=> SaveModGroup( group, basePath, groupIdx );
private static void SaveModGroup( IModGroup group, DirectoryInfo basePath, int groupIdx )
{ {
var file = group.FileName( basePath, groupIdx ); _basePath = basePath;
using var s = File.Exists( file ) ? File.Open( file, FileMode.Truncate ) : File.Open( file, FileMode.CreateNew ); _groupIdx = -1;
using var writer = new StreamWriter( s ); _defaultMod = @default;
using var j = new JsonTextWriter( writer ) { Formatting = Formatting.Indented }; }
var serializer = new JsonSerializer { Formatting = Formatting.Indented };
j.WriteStartObject(); public string ToFilename(FilenameService fileNames)
j.WritePropertyName( nameof( group.Name ) ); => fileNames.OptionGroupFile(_basePath.FullName, _groupIdx, _group?.Name ?? string.Empty);
j.WriteValue( group.Name );
j.WritePropertyName( nameof( group.Description ) ); public void Save(StreamWriter writer)
j.WriteValue( group.Description ); {
j.WritePropertyName( nameof( group.Priority ) ); using var j = new JsonTextWriter(writer) { Formatting = Formatting.Indented };
j.WriteValue( group.Priority ); var serializer = new JsonSerializer { Formatting = Formatting.Indented };
j.WritePropertyName( nameof( Type ) ); if (_groupIdx >= 0)
j.WriteValue( group.Type.ToString() );
j.WritePropertyName( nameof( group.DefaultSettings ) );
j.WriteValue( group.DefaultSettings );
j.WritePropertyName( "Options" );
j.WriteStartArray();
for( var idx = 0; idx < group.Count; ++idx )
{ {
ISubMod.WriteSubMod( j, serializer, group[ idx ], basePath, group.Type == GroupType.Multi ? group.OptionPriority( idx ) : null ); j.WriteStartObject();
j.WritePropertyName(nameof(_group.Name));
j.WriteValue(_group!.Name);
j.WritePropertyName(nameof(_group.Description));
j.WriteValue(_group.Description);
j.WritePropertyName(nameof(_group.Priority));
j.WriteValue(_group.Priority);
j.WritePropertyName(nameof(Type));
j.WriteValue(_group.Type.ToString());
j.WritePropertyName(nameof(_group.DefaultSettings));
j.WriteValue(_group.DefaultSettings);
j.WritePropertyName("Options");
j.WriteStartArray();
for (var idx = 0; idx < _group.Count; ++idx)
ISubMod.WriteSubMod(j, serializer, _group[idx], _basePath, _group.Type == GroupType.Multi ? _group.OptionPriority(idx) : null);
j.WriteEndArray();
j.WriteEndObject();
}
else
{
ISubMod.WriteSubMod(j, serializer, _defaultMod!, _basePath, null);
} }
j.WriteEndArray();
j.WriteEndObject();
Penumbra.Log.Debug( $"Saved group file {file} for group {groupIdx + 1}: {group.Name}." );
} }
}
public IModGroup Convert( GroupType type );
public bool MoveOption( int optionIdxFrom, int optionIdxTo );
public void UpdatePositions( int from = 0 );
}

View file

@ -38,17 +38,18 @@ public class Penumbra : IDalamudPlugin
public string Name public string Name
=> "Penumbra"; => "Penumbra";
public static Logger Log { get; private set; } = null!; public static Logger Log { get; private set; } = null!;
public static ChatService ChatService { get; private set; } = null!; public static ChatService ChatService { get; private set; } = null!;
public static SaveService SaveService { get; private set; } = null!; public static FilenameService Filenames { get; private set; } = null!;
public static Configuration Config { get; private set; } = null!; public static SaveService SaveService { get; private set; } = null!;
public static Configuration Config { get; private set; } = null!;
public static ResidentResourceManager ResidentResources { get; private set; } = null!; public static ResidentResourceManager ResidentResources { get; private set; } = null!;
public static CharacterUtility CharacterUtility { get; private set; } = null!; public static CharacterUtility CharacterUtility { get; private set; } = null!;
public static GameEventManager GameEvents { get; private set; } = null!; public static GameEventManager GameEvents { get; private set; } = null!;
public static MetaFileManager MetaFileManager { get; private set; } = null!; public static MetaFileManager MetaFileManager { get; private set; } = null!;
public static ModManager ModManager { get; private set; } = null!; public static ModManager ModManager { get; private set; } = null!;
public static CollectionManager CollectionManager { get; private set; } = null!; public static CollectionManager CollectionManager { get; private set; } = null!;
public static TempCollectionManager TempCollections { get; private set; } = null!; public static TempCollectionManager TempCollections { get; private set; } = null!;
public static TempModManager TempMods { get; private set; } = null!; public static TempModManager TempMods { get; private set; } = null!;
public static ResourceLoader ResourceLoader { get; private set; } = null!; public static ResourceLoader ResourceLoader { get; private set; } = null!;
@ -63,13 +64,13 @@ public class Penumbra : IDalamudPlugin
public static PerformanceTracker Performance { get; private set; } = null!; public static PerformanceTracker Performance { get; private set; } = null!;
public readonly PathResolver PathResolver; public readonly PathResolver PathResolver;
public readonly RedrawService RedrawService; public readonly RedrawService RedrawService;
public readonly ModFileSystem ModFileSystem; public readonly ModFileSystem ModFileSystem;
public HttpApi HttpApi = null!; public HttpApi HttpApi = null!;
internal ConfigWindow? ConfigWindow { get; private set; } internal ConfigWindow? ConfigWindow { get; private set; }
private PenumbraWindowSystem? _windowSystem; private PenumbraWindowSystem? _windowSystem;
private bool _disposed; private bool _disposed;
private readonly PenumbraNew _tmp; private readonly PenumbraNew _tmp;
@ -80,29 +81,30 @@ public class Penumbra : IDalamudPlugin
{ {
_tmp = new PenumbraNew(this, pluginInterface); _tmp = new PenumbraNew(this, pluginInterface);
ChatService = _tmp.Services.GetRequiredService<ChatService>(); ChatService = _tmp.Services.GetRequiredService<ChatService>();
Filenames = _tmp.Services.GetRequiredService<FilenameService>();
SaveService = _tmp.Services.GetRequiredService<SaveService>(); SaveService = _tmp.Services.GetRequiredService<SaveService>();
Performance = _tmp.Services.GetRequiredService<PerformanceTracker>(); Performance = _tmp.Services.GetRequiredService<PerformanceTracker>();
ValidityChecker = _tmp.Services.GetRequiredService<ValidityChecker>(); ValidityChecker = _tmp.Services.GetRequiredService<ValidityChecker>();
_tmp.Services.GetRequiredService<BackupService>(); _tmp.Services.GetRequiredService<BackupService>();
Config = _tmp.Services.GetRequiredService<Configuration>(); Config = _tmp.Services.GetRequiredService<Configuration>();
CharacterUtility = _tmp.Services.GetRequiredService<CharacterUtility>(); CharacterUtility = _tmp.Services.GetRequiredService<CharacterUtility>();
GameEvents = _tmp.Services.GetRequiredService<GameEventManager>(); GameEvents = _tmp.Services.GetRequiredService<GameEventManager>();
MetaFileManager = _tmp.Services.GetRequiredService<MetaFileManager>(); MetaFileManager = _tmp.Services.GetRequiredService<MetaFileManager>();
Framework = _tmp.Services.GetRequiredService<FrameworkManager>(); Framework = _tmp.Services.GetRequiredService<FrameworkManager>();
Actors = _tmp.Services.GetRequiredService<ActorService>().AwaitedService; Actors = _tmp.Services.GetRequiredService<ActorService>().AwaitedService;
Identifier = _tmp.Services.GetRequiredService<IdentifierService>().AwaitedService; Identifier = _tmp.Services.GetRequiredService<IdentifierService>().AwaitedService;
GamePathParser = _tmp.Services.GetRequiredService<IGamePathParser>(); GamePathParser = _tmp.Services.GetRequiredService<IGamePathParser>();
StainService = _tmp.Services.GetRequiredService<StainService>(); StainService = _tmp.Services.GetRequiredService<StainService>();
TempMods = _tmp.Services.GetRequiredService<TempModManager>(); TempMods = _tmp.Services.GetRequiredService<TempModManager>();
ResidentResources = _tmp.Services.GetRequiredService<ResidentResourceManager>(); ResidentResources = _tmp.Services.GetRequiredService<ResidentResourceManager>();
_tmp.Services.GetRequiredService<ResourceManagerService>(); _tmp.Services.GetRequiredService<ResourceManagerService>();
ModManager = _tmp.Services.GetRequiredService<ModManager>(); ModManager = _tmp.Services.GetRequiredService<ModManager>();
CollectionManager = _tmp.Services.GetRequiredService<CollectionManager>(); CollectionManager = _tmp.Services.GetRequiredService<CollectionManager>();
TempCollections = _tmp.Services.GetRequiredService<TempCollectionManager>(); TempCollections = _tmp.Services.GetRequiredService<TempCollectionManager>();
ModFileSystem = _tmp.Services.GetRequiredService<ModFileSystem>(); ModFileSystem = _tmp.Services.GetRequiredService<ModFileSystem>();
RedrawService = _tmp.Services.GetRequiredService<RedrawService>(); RedrawService = _tmp.Services.GetRequiredService<RedrawService>();
_tmp.Services.GetRequiredService<ResourceService>(); _tmp.Services.GetRequiredService<ResourceService>();
ResourceLoader = _tmp.Services.GetRequiredService<ResourceLoader>(); ResourceLoader = _tmp.Services.GetRequiredService<ResourceLoader>();
using (var t = _tmp.Services.GetRequiredService<StartTracker>().Measure(StartTimeType.PathResolver)) using (var t = _tmp.Services.GetRequiredService<StartTracker>().Measure(StartTimeType.PathResolver))
{ {
PathResolver = _tmp.Services.GetRequiredService<PathResolver>(); PathResolver = _tmp.Services.GetRequiredService<PathResolver>();
@ -112,7 +114,8 @@ public class Penumbra : IDalamudPlugin
SetupApi(); SetupApi();
ValidityChecker.LogExceptions(); ValidityChecker.LogExceptions();
Log.Information($"Penumbra Version {ValidityChecker.Version}, Commit #{ValidityChecker.CommitHash} successfully Loaded from {pluginInterface.SourceRepository}."); Log.Information(
$"Penumbra Version {ValidityChecker.Version}, Commit #{ValidityChecker.CommitHash} successfully Loaded from {pluginInterface.SourceRepository}.");
OtterTex.NativeDll.Initialize(pluginInterface.AssemblyLocation.DirectoryName); OtterTex.NativeDll.Initialize(pluginInterface.AssemblyLocation.DirectoryName);
Log.Information($"Loading native OtterTex assembly from {OtterTex.NativeDll.Directory}."); Log.Information($"Loading native OtterTex assembly from {OtterTex.NativeDll.Directory}.");
@ -129,8 +132,8 @@ public class Penumbra : IDalamudPlugin
private void SetupApi() private void SetupApi()
{ {
using var timer = _tmp.Services.GetRequiredService<StartTracker>().Measure(StartTimeType.Api); using var timer = _tmp.Services.GetRequiredService<StartTracker>().Measure(StartTimeType.Api);
var api = _tmp.Services.GetRequiredService<IPenumbraApi>(); var api = _tmp.Services.GetRequiredService<IPenumbraApi>();
HttpApi = _tmp.Services.GetRequiredService<HttpApi>(); HttpApi = _tmp.Services.GetRequiredService<HttpApi>();
_tmp.Services.GetRequiredService<PenumbraIpcProviders>(); _tmp.Services.GetRequiredService<PenumbraIpcProviders>();
if (Config.EnableHttpApi) if (Config.EnableHttpApi)
HttpApi.CreateWebServer(); HttpApi.CreateWebServer();

View file

@ -93,6 +93,7 @@ public class PenumbraNew
// Add Mod Services // Add Mod Services
services.AddSingleton<TempModManager>() services.AddSingleton<TempModManager>()
.AddSingleton<ModDataEditor>() .AddSingleton<ModDataEditor>()
.AddSingleton<ModOptionEditor>()
.AddSingleton<ModManager>() .AddSingleton<ModManager>()
.AddSingleton<ModFileSystem>(); .AddSingleton<ModFileSystem>();

View file

@ -45,13 +45,22 @@ public class CommunicatorService : IDisposable
/// </list> </summary> /// </list> </summary>
public readonly EventWrapper<nint, string, nint> CreatedCharacterBase = new(nameof(CreatedCharacterBase)); public readonly EventWrapper<nint, string, nint> CreatedCharacterBase = new(nameof(CreatedCharacterBase));
/// <summary><list type="number"> /// <summary> <list type="number">
/// <item>Parameter is the type of data change for the mod, which can be multiple flags. </item> /// <item>Parameter is the type of data change for the mod, which can be multiple flags. </item>
/// <item>Parameter is the changed mod. </item> /// <item>Parameter is the changed mod. </item>
/// <item>Parameter is the old name of the mod in case of a name change, and null otherwise. </item> /// <item>Parameter is the old name of the mod in case of a name change, and null otherwise. </item>
/// </list> </summary> /// </list> </summary>
public readonly EventWrapper<ModDataChangeType, Mod, string?> ModDataChanged = new(nameof(ModDataChanged)); public readonly EventWrapper<ModDataChangeType, Mod, string?> ModDataChanged = new(nameof(ModDataChanged));
/// <summary><list type="number">
/// <item>Parameter is the type option change. </item>
/// <item>Parameter is the changed mod. </item>
/// <item>Parameter is the index of the changed group inside the mod. </item>
/// <item>Parameter is the index of the changed option inside the group or -1 if it does not concern a specific option. </item>
/// <item>Parameter is the index of the group an option was moved to. </item>
/// </list> </summary>
public readonly EventWrapper<ModOptionChangeType, Mod, int, int, int> ModOptionChanged = new(nameof(ModOptionChanged));
public void Dispose() public void Dispose()
{ {
CollectionChange.Dispose(); CollectionChange.Dispose();
@ -60,5 +69,6 @@ public class CommunicatorService : IDisposable
CreatingCharacterBase.Dispose(); CreatingCharacterBase.Dispose();
CreatedCharacterBase.Dispose(); CreatedCharacterBase.Dispose();
ModDataChanged.Dispose(); ModDataChanged.Dispose();
ModOptionChanged.Dispose();
} }
} }

View file

@ -71,4 +71,21 @@ public class FilenameService
/// <summary> Obtain the path of the meta file given a mod directory. </summary> /// <summary> Obtain the path of the meta file given a mod directory. </summary>
public string ModMetaPath(string modDirectory) public string ModMetaPath(string modDirectory)
=> Path.Combine(modDirectory, "meta.json"); => Path.Combine(modDirectory, "meta.json");
/// <summary> Obtain the path of the file describing a given option group by its index and the mod. If the index is < 0, return the path for the default mod file. </summary>
public string OptionGroupFile(Mod mod, int index)
=> OptionGroupFile(mod.ModPath.FullName, index, index >= 0 ? mod.Groups[index].Name : string.Empty);
/// <summary> Obtain the path of the file describing a given option group by its index, name and basepath. If the index is < 0, return the path for the default mod file. </summary>
public string OptionGroupFile(string basePath, int index, string name)
{
var fileName = index >= 0
? $"group_{index + 1:D3}_{name.RemoveInvalidPathSymbols().ToLowerInvariant()}.json"
: "default_mod.json";
return Path.Combine(basePath, fileName);
}
/// <summary> Enumerate all group files for a given mod. </summary>
public IEnumerable<FileInfo> GetOptionGroupFiles(Mod mod)
=> mod.ModPath.EnumerateFiles("group_*.json");
} }

View file

@ -270,7 +270,7 @@ public class ItemSwapTab : IDisposable, ITab
_modManager.DataEditor.CreateMeta(newDir, _newModName, _config.DefaultModAuthor, CreateDescription(), "1.0", string.Empty); _modManager.DataEditor.CreateMeta(newDir, _newModName, _config.DefaultModAuthor, CreateDescription(), "1.0", string.Empty);
Mod.Creator.CreateDefaultFiles(newDir); Mod.Creator.CreateDefaultFiles(newDir);
_modManager.AddMod(newDir); _modManager.AddMod(newDir);
if (!_swapData.WriteMod(_modManager.Last(), if (!_swapData.WriteMod(_modManager, _modManager.Last(),
_useFileSwaps ? ItemSwapContainer.WriteType.UseSwaps : ItemSwapContainer.WriteType.NoSwaps)) _useFileSwaps ? ItemSwapContainer.WriteType.UseSwaps : ItemSwapContainer.WriteType.NoSwaps))
_modManager.DeleteMod(_modManager.Count - 1); _modManager.DeleteMod(_modManager.Count - 1);
} }
@ -296,16 +296,16 @@ public class ItemSwapTab : IDisposable, ITab
{ {
if (_selectedGroup == null) if (_selectedGroup == null)
{ {
_modManager.AddModGroup(_mod, GroupType.Multi, _newGroupName); _modManager.OptionEditor.AddModGroup(_mod, GroupType.Multi, _newGroupName);
_selectedGroup = _mod.Groups.Last(); _selectedGroup = _mod.Groups.Last();
groupCreated = true; groupCreated = true;
} }
_modManager.AddOption(_mod, _mod.Groups.IndexOf(_selectedGroup), _newOptionName); _modManager.OptionEditor.AddOption(_mod, _mod.Groups.IndexOf(_selectedGroup), _newOptionName);
optionCreated = true; optionCreated = true;
optionFolderName = Directory.CreateDirectory(optionFolderName.FullName); optionFolderName = Directory.CreateDirectory(optionFolderName.FullName);
dirCreated = true; dirCreated = true;
if (!_swapData.WriteMod(_mod, _useFileSwaps ? ItemSwapContainer.WriteType.UseSwaps : ItemSwapContainer.WriteType.NoSwaps, if (!_swapData.WriteMod(_modManager, _mod, _useFileSwaps ? ItemSwapContainer.WriteType.UseSwaps : ItemSwapContainer.WriteType.NoSwaps,
optionFolderName, optionFolderName,
_mod.Groups.IndexOf(_selectedGroup), _selectedGroup.Count - 1)) _mod.Groups.IndexOf(_selectedGroup), _selectedGroup.Count - 1))
throw new Exception("Failure writing files for mod swap."); throw new Exception("Failure writing files for mod swap.");
@ -317,11 +317,11 @@ public class ItemSwapTab : IDisposable, ITab
try try
{ {
if (optionCreated && _selectedGroup != null) if (optionCreated && _selectedGroup != null)
_modManager.DeleteOption(_mod, _mod.Groups.IndexOf(_selectedGroup), _selectedGroup.Count - 1); _modManager.OptionEditor.DeleteOption(_mod, _mod.Groups.IndexOf(_selectedGroup), _selectedGroup.Count - 1);
if (groupCreated) if (groupCreated)
{ {
_modManager.DeleteModGroup(_mod, _mod.Groups.IndexOf(_selectedGroup!)); _modManager.OptionEditor.DeleteModGroup(_mod, _mod.Groups.IndexOf(_selectedGroup!));
_selectedGroup = null; _selectedGroup = null;
} }

View file

@ -12,6 +12,7 @@ using OtterGui.Raii;
using OtterGui.Widgets; using OtterGui.Widgets;
using Penumbra.Api.Enums; using Penumbra.Api.Enums;
using Penumbra.Mods; using Penumbra.Mods;
using Penumbra.Services;
using Penumbra.UI.AdvancedWindow; using Penumbra.UI.AdvancedWindow;
using Penumbra.Util; using Penumbra.Util;
@ -20,7 +21,8 @@ namespace Penumbra.UI.ModsTab;
public class ModPanelEditTab : ITab public class ModPanelEditTab : ITab
{ {
private readonly ChatService _chat; private readonly ChatService _chat;
private readonly ModManager _modManager; private readonly FilenameService _filenames;
private readonly ModManager _modManager;
private readonly ModFileSystem _fileSystem; private readonly ModFileSystem _fileSystem;
private readonly ModFileSystemSelector _selector; private readonly ModFileSystemSelector _selector;
private readonly ModEditWindow _editWindow; private readonly ModEditWindow _editWindow;
@ -34,14 +36,15 @@ public class ModPanelEditTab : ITab
private Mod _mod = null!; private Mod _mod = null!;
public ModPanelEditTab(ModManager modManager, ModFileSystemSelector selector, ModFileSystem fileSystem, ChatService chat, public ModPanelEditTab(ModManager modManager, ModFileSystemSelector selector, ModFileSystem fileSystem, ChatService chat,
ModEditWindow editWindow, ModEditor editor) ModEditWindow editWindow, ModEditor editor, FilenameService filenames)
{ {
_modManager = modManager; _modManager = modManager;
_selector = selector; _selector = selector;
_fileSystem = fileSystem; _fileSystem = fileSystem;
_chat = chat; _chat = chat;
_editWindow = editWindow; _editWindow = editWindow;
_editor = editor; _editor = editor;
_filenames = filenames;
} }
public ReadOnlySpan<byte> Label public ReadOnlySpan<byte> Label
@ -129,7 +132,7 @@ public class ModPanelEditTab : ITab
if (ImGui.Button("Update Bibo Material", buttonSize)) if (ImGui.Button("Update Bibo Material", buttonSize))
{ {
_editor.LoadMod(_mod); _editor.LoadMod(_mod);
_editor.MdlMaterialEditor.ReplaceAllMaterials("bibo", "b"); _editor.MdlMaterialEditor.ReplaceAllMaterials("bibo", "b");
_editor.MdlMaterialEditor.ReplaceAllMaterials("bibopube", "c"); _editor.MdlMaterialEditor.ReplaceAllMaterials("bibopube", "c");
_editor.MdlMaterialEditor.SaveAllModels(); _editor.MdlMaterialEditor.SaveAllModels();
_editWindow.UpdateModels(); _editWindow.UpdateModels();
@ -189,7 +192,7 @@ public class ModPanelEditTab : ITab
var reducedSize = new Vector2(UiHelpers.InputTextMinusButton3, 0); var reducedSize = new Vector2(UiHelpers.InputTextMinusButton3, 0);
if (ImGui.Button("Edit Description", reducedSize)) if (ImGui.Button("Edit Description", reducedSize))
_delayedActions.Enqueue(() => DescriptionEdit.OpenPopup(_mod, Input.Description)); _delayedActions.Enqueue(() => DescriptionEdit.OpenPopup(_filenames, _mod, Input.Description));
ImGui.SameLine(); ImGui.SameLine();
var fileExists = File.Exists(_modManager.DataEditor.MetaFile(_mod)); var fileExists = File.Exists(_modManager.DataEditor.MetaFile(_mod));
@ -235,13 +238,13 @@ public class ModPanelEditTab : ITab
ImGui.SameLine(); ImGui.SameLine();
var nameValid = modManager.VerifyFileName(mod, null, _newGroupName, false); var nameValid = ModOptionEditor.VerifyFileName(mod, null, _newGroupName, false);
tt = nameValid ? "Add new option group to the mod." : "Can not add a group of this name."; tt = nameValid ? "Add new option group to the mod." : "Can not add a group of this name.";
if (!ImGuiUtil.DrawDisabledButton(FontAwesomeIcon.Plus.ToIconString(), UiHelpers.IconButtonSize, if (!ImGuiUtil.DrawDisabledButton(FontAwesomeIcon.Plus.ToIconString(), UiHelpers.IconButtonSize,
tt, !nameValid, true)) tt, !nameValid, true))
return; return;
modManager.AddModGroup(mod, GroupType.Single, _newGroupName); modManager.OptionEditor.AddModGroup(mod, GroupType.Single, _newGroupName);
Reset(); Reset();
} }
} }
@ -249,7 +252,7 @@ public class ModPanelEditTab : ITab
/// <summary> A text input for the new directory name and a button to apply the move. </summary> /// <summary> A text input for the new directory name and a button to apply the move. </summary>
private static class MoveDirectory private static class MoveDirectory
{ {
private static string? _currentModDirectory; private static string? _currentModDirectory;
private static ModManager.NewDirectoryState _state = ModManager.NewDirectoryState.Identical; private static ModManager.NewDirectoryState _state = ModManager.NewDirectoryState.Identical;
public static void Reset() public static void Reset()
@ -297,14 +300,16 @@ public class ModPanelEditTab : ITab
/// <summary> Open a popup to edit a multi-line mod or option description. </summary> /// <summary> Open a popup to edit a multi-line mod or option description. </summary>
private static class DescriptionEdit private static class DescriptionEdit
{ {
private const string PopupName = "Edit Description"; private const string PopupName = "Edit Description";
private static string _newDescription = string.Empty; private static string _newDescription = string.Empty;
private static int _newDescriptionIdx = -1; private static int _newDescriptionIdx = -1;
private static int _newDescriptionOptionIdx = -1; private static int _newDescriptionOptionIdx = -1;
private static Mod? _mod; private static Mod? _mod;
private static FilenameService? _fileNames;
public static void OpenPopup(Mod mod, int groupIdx, int optionIdx = -1) public static void OpenPopup(FilenameService filenames, Mod mod, int groupIdx, int optionIdx = -1)
{ {
_fileNames = filenames;
_newDescriptionIdx = groupIdx; _newDescriptionIdx = groupIdx;
_newDescriptionOptionIdx = optionIdx; _newDescriptionOptionIdx = optionIdx;
_newDescription = groupIdx < 0 _newDescription = groupIdx < 0
@ -353,9 +358,10 @@ public class ModPanelEditTab : ITab
break; break;
case >= 0: case >= 0:
if (_newDescriptionOptionIdx < 0) if (_newDescriptionOptionIdx < 0)
modManager.ChangeGroupDescription(_mod, _newDescriptionIdx, _newDescription); modManager.OptionEditor.ChangeGroupDescription(_mod, _newDescriptionIdx, _newDescription);
else else
modManager.ChangeOptionDescription(_mod, _newDescriptionIdx, _newDescriptionOptionIdx, _newDescription); modManager.OptionEditor.ChangeOptionDescription(_mod, _newDescriptionIdx, _newDescriptionOptionIdx,
_newDescription);
break; break;
} }
@ -384,18 +390,18 @@ public class ModPanelEditTab : ITab
.Push(ImGuiStyleVar.ItemSpacing, _itemSpacing); .Push(ImGuiStyleVar.ItemSpacing, _itemSpacing);
if (Input.Text("##Name", groupIdx, Input.None, group.Name, out var newGroupName, 256, UiHelpers.InputTextWidth.X)) if (Input.Text("##Name", groupIdx, Input.None, group.Name, out var newGroupName, 256, UiHelpers.InputTextWidth.X))
_modManager.RenameModGroup(_mod, groupIdx, newGroupName); _modManager.OptionEditor.RenameModGroup(_mod, groupIdx, newGroupName);
ImGuiUtil.HoverTooltip("Group Name"); ImGuiUtil.HoverTooltip("Group Name");
ImGui.SameLine(); ImGui.SameLine();
if (ImGuiUtil.DrawDisabledButton(FontAwesomeIcon.Trash.ToIconString(), UiHelpers.IconButtonSize, if (ImGuiUtil.DrawDisabledButton(FontAwesomeIcon.Trash.ToIconString(), UiHelpers.IconButtonSize,
"Delete this option group.\nHold Control while clicking to delete.", !ImGui.GetIO().KeyCtrl, true)) "Delete this option group.\nHold Control while clicking to delete.", !ImGui.GetIO().KeyCtrl, true))
_delayedActions.Enqueue(() => _modManager.DeleteModGroup(_mod, groupIdx)); _delayedActions.Enqueue(() => _modManager.OptionEditor.DeleteModGroup(_mod, groupIdx));
ImGui.SameLine(); ImGui.SameLine();
if (Input.Priority("##Priority", groupIdx, Input.None, group.Priority, out var priority, 50 * UiHelpers.Scale)) if (Input.Priority("##Priority", groupIdx, Input.None, group.Priority, out var priority, 50 * UiHelpers.Scale))
_modManager.ChangeGroupPriority(_mod, groupIdx, priority); _modManager.OptionEditor.ChangeGroupPriority(_mod, groupIdx, priority);
ImGuiUtil.HoverTooltip("Group Priority"); ImGuiUtil.HoverTooltip("Group Priority");
@ -405,7 +411,7 @@ public class ModPanelEditTab : ITab
var tt = groupIdx == 0 ? "Can not move this group further upwards." : $"Move this group up to group {groupIdx}."; var tt = groupIdx == 0 ? "Can not move this group further upwards." : $"Move this group up to group {groupIdx}.";
if (ImGuiUtil.DrawDisabledButton(FontAwesomeIcon.ArrowUp.ToIconString(), UiHelpers.IconButtonSize, if (ImGuiUtil.DrawDisabledButton(FontAwesomeIcon.ArrowUp.ToIconString(), UiHelpers.IconButtonSize,
tt, groupIdx == 0, true)) tt, groupIdx == 0, true))
_delayedActions.Enqueue(() => _modManager.MoveModGroup(_mod, groupIdx, groupIdx - 1)); _delayedActions.Enqueue(() => _modManager.OptionEditor.MoveModGroup(_mod, groupIdx, groupIdx - 1));
ImGui.SameLine(); ImGui.SameLine();
tt = groupIdx == _mod.Groups.Count - 1 tt = groupIdx == _mod.Groups.Count - 1
@ -413,16 +419,16 @@ public class ModPanelEditTab : ITab
: $"Move this group down to group {groupIdx + 2}."; : $"Move this group down to group {groupIdx + 2}.";
if (ImGuiUtil.DrawDisabledButton(FontAwesomeIcon.ArrowDown.ToIconString(), UiHelpers.IconButtonSize, if (ImGuiUtil.DrawDisabledButton(FontAwesomeIcon.ArrowDown.ToIconString(), UiHelpers.IconButtonSize,
tt, groupIdx == _mod.Groups.Count - 1, true)) tt, groupIdx == _mod.Groups.Count - 1, true))
_delayedActions.Enqueue(() => _modManager.MoveModGroup(_mod, groupIdx, groupIdx + 1)); _delayedActions.Enqueue(() => _modManager.OptionEditor.MoveModGroup(_mod, groupIdx, groupIdx + 1));
ImGui.SameLine(); ImGui.SameLine();
if (ImGuiUtil.DrawDisabledButton(FontAwesomeIcon.Edit.ToIconString(), UiHelpers.IconButtonSize, if (ImGuiUtil.DrawDisabledButton(FontAwesomeIcon.Edit.ToIconString(), UiHelpers.IconButtonSize,
"Edit group description.", false, true)) "Edit group description.", false, true))
_delayedActions.Enqueue(() => DescriptionEdit.OpenPopup(_mod, groupIdx)); _delayedActions.Enqueue(() => DescriptionEdit.OpenPopup(_filenames, _mod, groupIdx));
ImGui.SameLine(); ImGui.SameLine();
var fileName = group.FileName(_mod.ModPath, groupIdx); var fileName = _filenames.OptionGroupFile(_mod, groupIdx);
var fileExists = File.Exists(fileName); var fileExists = File.Exists(fileName);
tt = fileExists tt = fileExists
? $"Open the {group.Name} json file in the text editor of your choice." ? $"Open the {group.Name} json file in the text editor of your choice."
@ -491,7 +497,7 @@ public class ModPanelEditTab : ITab
if (group.Type == GroupType.Single) if (group.Type == GroupType.Single)
{ {
if (ImGui.RadioButton("##default", group.DefaultSettings == optionIdx)) if (ImGui.RadioButton("##default", group.DefaultSettings == optionIdx))
panel._modManager.ChangeModGroupDefaultOption(panel._mod, groupIdx, (uint)optionIdx); panel._modManager.OptionEditor.ChangeModGroupDefaultOption(panel._mod, groupIdx, (uint)optionIdx);
ImGuiUtil.HoverTooltip($"Set {option.Name} as the default choice for this group."); ImGuiUtil.HoverTooltip($"Set {option.Name} as the default choice for this group.");
} }
@ -499,7 +505,7 @@ public class ModPanelEditTab : ITab
{ {
var isDefaultOption = ((group.DefaultSettings >> optionIdx) & 1) != 0; var isDefaultOption = ((group.DefaultSettings >> optionIdx) & 1) != 0;
if (ImGui.Checkbox("##default", ref isDefaultOption)) if (ImGui.Checkbox("##default", ref isDefaultOption))
panel._modManager.ChangeModGroupDefaultOption(panel._mod, groupIdx, isDefaultOption panel._modManager.OptionEditor.ChangeModGroupDefaultOption(panel._mod, groupIdx, isDefaultOption
? group.DefaultSettings | (1u << optionIdx) ? group.DefaultSettings | (1u << optionIdx)
: group.DefaultSettings & ~(1u << optionIdx)); : group.DefaultSettings & ~(1u << optionIdx));
@ -508,17 +514,17 @@ public class ModPanelEditTab : ITab
ImGui.TableNextColumn(); ImGui.TableNextColumn();
if (Input.Text("##Name", groupIdx, optionIdx, option.Name, out var newOptionName, 256, -1)) if (Input.Text("##Name", groupIdx, optionIdx, option.Name, out var newOptionName, 256, -1))
panel._modManager.RenameOption(panel._mod, groupIdx, optionIdx, newOptionName); panel._modManager.OptionEditor.RenameOption(panel._mod, groupIdx, optionIdx, newOptionName);
ImGui.TableNextColumn(); ImGui.TableNextColumn();
if (ImGuiUtil.DrawDisabledButton(FontAwesomeIcon.Edit.ToIconString(), UiHelpers.IconButtonSize, "Edit option description.", if (ImGuiUtil.DrawDisabledButton(FontAwesomeIcon.Edit.ToIconString(), UiHelpers.IconButtonSize, "Edit option description.",
false, true)) false, true))
panel._delayedActions.Enqueue(() => DescriptionEdit.OpenPopup(panel._mod, groupIdx, optionIdx)); panel._delayedActions.Enqueue(() => DescriptionEdit.OpenPopup(panel._filenames, panel._mod, groupIdx, optionIdx));
ImGui.TableNextColumn(); ImGui.TableNextColumn();
if (ImGuiUtil.DrawDisabledButton(FontAwesomeIcon.Trash.ToIconString(), UiHelpers.IconButtonSize, if (ImGuiUtil.DrawDisabledButton(FontAwesomeIcon.Trash.ToIconString(), UiHelpers.IconButtonSize,
"Delete this option.\nHold Control while clicking to delete.", !ImGui.GetIO().KeyCtrl, true)) "Delete this option.\nHold Control while clicking to delete.", !ImGui.GetIO().KeyCtrl, true))
panel._delayedActions.Enqueue(() => panel._modManager.DeleteOption(panel._mod, groupIdx, optionIdx)); panel._delayedActions.Enqueue(() => panel._modManager.OptionEditor.DeleteOption(panel._mod, groupIdx, optionIdx));
ImGui.TableNextColumn(); ImGui.TableNextColumn();
if (group.Type != GroupType.Multi) if (group.Type != GroupType.Multi)
@ -526,7 +532,7 @@ public class ModPanelEditTab : ITab
if (Input.Priority("##Priority", groupIdx, optionIdx, group.OptionPriority(optionIdx), out var priority, if (Input.Priority("##Priority", groupIdx, optionIdx, group.OptionPriority(optionIdx), out var priority,
50 * UiHelpers.Scale)) 50 * UiHelpers.Scale))
panel._modManager.ChangeOptionPriority(panel._mod, groupIdx, optionIdx, priority); panel._modManager.OptionEditor.ChangeOptionPriority(panel._mod, groupIdx, optionIdx, priority);
ImGuiUtil.HoverTooltip("Option priority."); ImGuiUtil.HoverTooltip("Option priority.");
} }
@ -560,7 +566,7 @@ public class ModPanelEditTab : ITab
tt, !(canAddGroup && validName), true)) tt, !(canAddGroup && validName), true))
return; return;
panel._modManager.AddOption(mod, groupIdx, _newOptionName); panel._modManager.OptionEditor.AddOption(mod, groupIdx, _newOptionName);
_newOptionName = string.Empty; _newOptionName = string.Empty;
} }
@ -591,7 +597,7 @@ public class ModPanelEditTab : ITab
if (_dragDropGroupIdx == groupIdx) if (_dragDropGroupIdx == groupIdx)
{ {
var sourceOption = _dragDropOptionIdx; var sourceOption = _dragDropOptionIdx;
panel._delayedActions.Enqueue(() => panel._modManager.MoveOption(panel._mod, groupIdx, sourceOption, optionIdx)); panel._delayedActions.Enqueue(() => panel._modManager.OptionEditor.MoveOption(panel._mod, groupIdx, sourceOption, optionIdx));
} }
else else
{ {
@ -604,9 +610,9 @@ public class ModPanelEditTab : ITab
var priority = sourceGroup.OptionPriority(_dragDropOptionIdx); var priority = sourceGroup.OptionPriority(_dragDropOptionIdx);
panel._delayedActions.Enqueue(() => panel._delayedActions.Enqueue(() =>
{ {
panel._modManager.DeleteOption(panel._mod, sourceGroupIdx, sourceOption); panel._modManager.OptionEditor.DeleteOption(panel._mod, sourceGroupIdx, sourceOption);
panel._modManager.AddOption(panel._mod, groupIdx, option, priority); panel._modManager.OptionEditor.AddOption(panel._mod, groupIdx, option, priority);
panel._modManager.MoveOption(panel._mod, groupIdx, currentCount, optionIdx); panel._modManager.OptionEditor.MoveOption(panel._mod, groupIdx, currentCount, optionIdx);
}); });
} }
} }
@ -633,12 +639,12 @@ public class ModPanelEditTab : ITab
return; return;
if (ImGui.Selectable(GroupTypeName(GroupType.Single), group.Type == GroupType.Single)) if (ImGui.Selectable(GroupTypeName(GroupType.Single), group.Type == GroupType.Single))
_modManager.ChangeModGroupType(_mod, groupIdx, GroupType.Single); _modManager.OptionEditor.ChangeModGroupType(_mod, groupIdx, GroupType.Single);
var canSwitchToMulti = group.Count <= IModGroup.MaxMultiOptions; var canSwitchToMulti = group.Count <= IModGroup.MaxMultiOptions;
using var style = ImRaii.PushStyle(ImGuiStyleVar.Alpha, 0.5f, !canSwitchToMulti); using var style = ImRaii.PushStyle(ImGuiStyleVar.Alpha, 0.5f, !canSwitchToMulti);
if (ImGui.Selectable(GroupTypeName(GroupType.Multi), group.Type == GroupType.Multi) && canSwitchToMulti) if (ImGui.Selectable(GroupTypeName(GroupType.Multi), group.Type == GroupType.Multi) && canSwitchToMulti)
_modManager.ChangeModGroupType(_mod, groupIdx, GroupType.Multi); _modManager.OptionEditor.ChangeModGroupType(_mod, groupIdx, GroupType.Multi);
style.Pop(); style.Pop();
if (!canSwitchToMulti) if (!canSwitchToMulti)

View file

@ -3,6 +3,7 @@ using System.IO;
using System.Text; using System.Text;
using OtterGui.Classes; using OtterGui.Classes;
using OtterGui.Log; using OtterGui.Log;
using Penumbra.Mods;
using Penumbra.Services; using Penumbra.Services;
namespace Penumbra.Util; namespace Penumbra.Util;
@ -94,4 +95,24 @@ public class SaveService
_log.Error($"Could not delete {value.GetType().Name} {value.LogName(name)}:\n{ex}"); _log.Error($"Could not delete {value.GetType().Name} {value.LogName(name)}:\n{ex}");
} }
} }
/// <summary> Immediately delete all existing option group files for a mod and save them anew. </summary>
public void SaveAllOptionGroups(Mod mod)
{
foreach (var file in _fileNames.GetOptionGroupFiles(mod))
{
try
{
if (file.Exists)
file.Delete();
}
catch (Exception e)
{
Penumbra.Log.Error($"Could not delete outdated group file {file}:\n{e}");
}
}
for (var i = 0; i < mod.Groups.Count; ++i)
ImmediateSave(new ModSaveGroup(mod, i));
}
} }