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>
{
private readonly Mods.ModManager _modManager;
private readonly ModManager _modManager;
private readonly CommunicatorService _communicator;
private readonly SaveService _saveService;
private readonly CharacterUtility _characterUtility;
private readonly ResidentResourceManager _residentResources;
private readonly Configuration _config;
@ -57,7 +58,8 @@ public sealed partial class CollectionManager : IDisposable, IEnumerable<ModColl
=> _collections;
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);
_communicator = communicator;
@ -65,12 +67,13 @@ public sealed partial class CollectionManager : IDisposable, IEnumerable<ModColl
_residentResources = residentResources;
_config = config;
_modManager = modManager;
_saveService = saveService;
Individuals = individuals;
// The collection manager reacts to changes in mods by itself.
_modManager.ModDiscoveryStarted += OnModDiscoveryStarted;
_modManager.ModDiscoveryFinished += OnModDiscoveryFinished;
_modManager.ModOptionChanged += OnModOptionsChanged;
_communicator.ModOptionChanged.Event += OnModOptionsChanged;
_modManager.ModPathChanged += OnModPathChange;
_communicator.CollectionChange.Event += SaveOnChange;
_communicator.TemporaryGlobalModChange.Event += OnGlobalModChange;
@ -86,7 +89,7 @@ public sealed partial class CollectionManager : IDisposable, IEnumerable<ModColl
_communicator.TemporaryGlobalModChange.Event -= OnGlobalModChange;
_modManager.ModDiscoveryStarted -= OnModDiscoveryStarted;
_modManager.ModDiscoveryFinished -= OnModDiscoveryFinished;
_modManager.ModOptionChanged -= OnModOptionsChanged;
_communicator.ModOptionChanged.Event -= OnModOptionsChanged;
_modManager.ModPathChanged -= OnModPathChange;
}
@ -279,7 +282,7 @@ public sealed partial class CollectionManager : IDisposable, IEnumerable<ModColl
foreach (var collection in this)
{
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,
@ -310,7 +313,7 @@ public sealed partial class CollectionManager : IDisposable, IEnumerable<ModColl
}
var defaultCollection = ModCollection.CreateNewEmpty((string)ModCollection.DefaultCollection);
Penumbra.SaveService.ImmediateSave(defaultCollection);
_saveService.ImmediateSave(defaultCollection);
defaultCollection.Index = _collections.Count;
_collections.Add(defaultCollection);
}
@ -337,7 +340,7 @@ public sealed partial class CollectionManager : IDisposable, IEnumerable<ModColl
}
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."
: string.Empty;
}
break;
// The group of all Characters is redundant if they are all equal to Default or unassigned.
case CollectionType.MalePlayerCharacter:
@ -464,7 +468,8 @@ public sealed partial class CollectionManager : IDisposable, IEnumerable<ModColl
continue;
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;

View file

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

View file

@ -34,7 +34,7 @@ public class ModFileEditor
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);
return num;
@ -54,7 +54,7 @@ public class ModFileEditor
var newDict = subMod.Files.Where(kvp => CheckAgainstMissing(mod, subMod, kvp.Value, kvp.Key, subMod == option))
.ToDictionary(kvp => kvp.Key, kvp => kvp.Value);
if (newDict.Count != subMod.Files.Count)
_modManager.OptionSetFiles(mod, groupIdx, optionIdx, newDict);
_modManager.OptionEditor.OptionSetFiles(mod, groupIdx, optionIdx, newDict);
}
ModEditor.ApplyToAllOptions(mod, HandleSubMod);

View file

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

View file

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

View file

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

View file

@ -38,7 +38,7 @@ public class ItemSwapContainer
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 convertedFiles = new Dictionary< Utf8GamePath, FullPath >( Swaps.Count );
@ -82,9 +82,9 @@ public class ItemSwapContainer
}
}
Penumbra.ModManager.OptionSetFiles( mod, groupIndex, optionIndex, convertedFiles );
Penumbra.ModManager.OptionSetFileSwaps( mod, groupIndex, optionIndex, convertedSwaps );
Penumbra.ModManager.OptionSetManipulations( mod, groupIndex, optionIndex, convertedManips );
manager.OptionEditor.OptionSetFiles( mod, groupIndex, optionIndex, convertedFiles );
manager.OptionEditor.OptionSetFileSwaps( mod, groupIndex, optionIndex, convertedSwaps );
manager.OptionEditor.OptionSetManipulations( mod, groupIndex, optionIndex, convertedManips );
return true;
}
catch( Exception e )

View file

@ -2,12 +2,76 @@ using System;
using System.Collections;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using Penumbra.Services;
using Penumbra.Util;
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.
public static bool MigrateModBackups = false;
@ -38,21 +102,29 @@ public sealed partial class ModManager : IReadOnlyList<Mod>
private readonly Configuration _config;
private readonly CommunicatorService _communicator;
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);
_config = config;
_communicator = communicator;
DataEditor = dataEditor;
OptionEditor = optionEditor;
ModDirectoryChanged += OnModDirectoryChange;
SetBaseDirectory(config.ModDirectory, true);
UpdateExportDirectory(_config.ExportDirectory, false);
ModOptionChanged += OnModOptionChange;
ModPathChanged += OnModPathChange;
_communicator.ModOptionChanged.Event += OnModOptionChange;
ModPathChanged += OnModPathChange;
DiscoverMods();
}
public void Dispose()
{
_communicator.ModOptionChanged.Event -= OnModOptionChange;
}
// Try to obtain a mod by its directory name (unique identifier, preferred),
// 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;
}
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.Collections.Generic;
using System.Linq;
using Dalamud.Interface.Internal.Notifications;
using OtterGui;
using OtterGui.Filesystem;
using Penumbra.Api.Enums;
using Penumbra.Meta.Manipulations;
using Penumbra.String.Classes;
using Penumbra.Util;
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 void ChangeModGroupType(Mod mod, int groupIdx, GroupType type)
{
var group = mod._groups[groupIdx];
if (group.Type == type)
return;
mod._groups[groupIdx] = group.Convert(type);
ModOptionChanged.Invoke(ModOptionChangeType.GroupTypeChanged, mod, groupIdx, -1, -1);
}
public void ChangeModGroupDefaultOption(Mod mod, int groupIdx, uint defaultOption)
{
var group = mod._groups[groupIdx];
if (group.DefaultSettings == defaultOption)
return;
group.DefaultSettings = defaultOption;
ModOptionChanged.Invoke(ModOptionChangeType.DefaultOptionChanged, mod, groupIdx, -1, -1);
}
public void RenameModGroup(Mod mod, int groupIdx, string newName)
{
var group = mod._groups[groupIdx];
var oldName = group.Name;
if (oldName == newName || !VerifyFileName(mod, group, newName, true))
return;
group.DeleteFile(mod.ModPath, groupIdx);
var _ = group switch
{
SingleModGroup s => s.Name = newName,
MultiModGroup m => m.Name = newName,
_ => newName,
};
ModOptionChanged.Invoke(ModOptionChangeType.GroupRenamed, mod, groupIdx, -1, -1);
}
public void AddModGroup(Mod mod, GroupType type, string newName)
{
if (!VerifyFileName(mod, null, newName, true))
return;
var maxPriority = mod._groups.Count == 0 ? 0 : mod._groups.Max(o => o.Priority) + 1;
mod._groups.Add(type == GroupType.Multi
? new MultiModGroup
{
Name = newName,
Priority = maxPriority,
}
: new SingleModGroup
{
Name = newName,
Priority = maxPriority,
});
ModOptionChanged.Invoke(ModOptionChangeType.GroupAdded, mod, mod._groups.Count - 1, -1, -1);
}
public void DeleteModGroup(Mod mod, int groupIdx)
{
var group = mod._groups[groupIdx];
ModOptionChanged.Invoke(ModOptionChangeType.PrepareChange, mod, groupIdx, -1, -1);
mod._groups.RemoveAt(groupIdx);
UpdateSubModPositions(mod, groupIdx);
group.DeleteFile(mod.ModPath, groupIdx);
ModOptionChanged.Invoke(ModOptionChangeType.GroupDeleted, mod, groupIdx, -1, -1);
}
public void MoveModGroup(Mod mod, int groupIdxFrom, int groupIdxTo)
{
if (mod._groups.Move(groupIdxFrom, groupIdxTo))
{
UpdateSubModPositions(mod, Math.Min(groupIdxFrom, groupIdxTo));
ModOptionChanged.Invoke(ModOptionChangeType.GroupMoved, mod, groupIdxFrom, -1, groupIdxTo);
}
}
private static void UpdateSubModPositions(Mod mod, int fromGroup)
{
foreach (var (group, groupIdx) in mod._groups.WithIndex().Skip(fromGroup))
{
foreach (var (o, optionIdx) in group.OfType<SubMod>().WithIndex())
o.SetPosition(groupIdx, optionIdx);
}
}
public void ChangeGroupDescription(Mod mod, int groupIdx, string newDescription)
{
var group = mod._groups[groupIdx];
if (group.Description == newDescription)
return;
var _ = group switch
{
SingleModGroup s => s.Description = newDescription,
MultiModGroup m => m.Description = newDescription,
_ => newDescription,
};
ModOptionChanged.Invoke(ModOptionChangeType.DisplayChange, mod, groupIdx, -1, -1);
}
public void ChangeOptionDescription(Mod mod, int groupIdx, int optionIdx, string newDescription)
{
var group = mod._groups[groupIdx];
var option = group[optionIdx];
if (option.Description == newDescription || option is not SubMod s)
return;
s.Description = newDescription;
ModOptionChanged.Invoke(ModOptionChangeType.DisplayChange, mod, groupIdx, optionIdx, -1);
}
public void ChangeGroupPriority(Mod mod, int groupIdx, int newPriority)
{
var group = mod._groups[groupIdx];
if (group.Priority == newPriority)
return;
var _ = group switch
{
SingleModGroup s => s.Priority = newPriority,
MultiModGroup m => m.Priority = newPriority,
_ => newPriority,
};
ModOptionChanged.Invoke(ModOptionChangeType.PriorityChanged, mod, groupIdx, -1, -1);
}
public void ChangeOptionPriority(Mod mod, int groupIdx, int optionIdx, int newPriority)
{
switch (mod._groups[groupIdx])
{
case SingleModGroup:
ChangeGroupPriority(mod, groupIdx, newPriority);
break;
case MultiModGroup m:
if (m.PrioritizedOptions[optionIdx].Priority == newPriority)
return;
m.PrioritizedOptions[optionIdx] = (m.PrioritizedOptions[optionIdx].Mod, newPriority);
ModOptionChanged.Invoke(ModOptionChangeType.PriorityChanged, mod, groupIdx, optionIdx, -1);
return;
}
}
public void RenameOption(Mod mod, int groupIdx, int optionIdx, string newName)
{
switch (mod._groups[groupIdx])
{
case SingleModGroup s:
if (s.OptionData[optionIdx].Name == newName)
return;
s.OptionData[optionIdx].Name = newName;
break;
case MultiModGroup m:
var option = m.PrioritizedOptions[optionIdx].Mod;
if (option.Name == newName)
return;
option.Name = newName;
break;
}
ModOptionChanged.Invoke(ModOptionChangeType.DisplayChange, mod, groupIdx, optionIdx, -1);
}
public void AddOption(Mod mod, int groupIdx, string newName)
{
var group = mod._groups[groupIdx];
var subMod = new SubMod(mod) { Name = newName };
subMod.SetPosition(groupIdx, group.Count);
switch (group)
{
case SingleModGroup s:
s.OptionData.Add(subMod);
break;
case MultiModGroup m:
m.PrioritizedOptions.Add((subMod, 0));
break;
}
ModOptionChanged.Invoke(ModOptionChangeType.OptionAdded, mod, groupIdx, group.Count - 1, -1);
}
public void AddOption(Mod mod, int groupIdx, ISubMod option, int priority = 0)
{
if (option is not SubMod o)
return;
var group = mod._groups[groupIdx];
if (group.Count > 63)
{
Penumbra.Log.Error(
$"Could not add option {option.Name} to {group.Name} for mod {mod.Name}, "
+ "since only up to 64 options are supported in one group.");
return;
}
o.SetPosition(groupIdx, group.Count);
switch (group)
{
case SingleModGroup s:
s.OptionData.Add(o);
break;
case MultiModGroup m:
m.PrioritizedOptions.Add((o, priority));
break;
}
ModOptionChanged.Invoke(ModOptionChangeType.OptionAdded, mod, groupIdx, group.Count - 1, -1);
}
public void DeleteOption(Mod mod, int groupIdx, int optionIdx)
{
var group = mod._groups[groupIdx];
ModOptionChanged.Invoke(ModOptionChangeType.PrepareChange, mod, groupIdx, optionIdx, -1);
switch (group)
{
case SingleModGroup s:
s.OptionData.RemoveAt(optionIdx);
break;
case MultiModGroup m:
m.PrioritizedOptions.RemoveAt(optionIdx);
break;
}
group.UpdatePositions(optionIdx);
ModOptionChanged.Invoke(ModOptionChangeType.OptionDeleted, mod, groupIdx, optionIdx, -1);
}
public void MoveOption(Mod mod, int groupIdx, int optionIdxFrom, int optionIdxTo)
{
var group = mod._groups[groupIdx];
if (group.MoveOption(optionIdxFrom, optionIdxTo))
ModOptionChanged.Invoke(ModOptionChangeType.OptionMoved, mod, groupIdx, optionIdxFrom, optionIdxTo);
}
public void OptionSetManipulations(Mod mod, int groupIdx, int optionIdx, HashSet<MetaManipulation> manipulations)
{
var subMod = GetSubMod(mod, groupIdx, optionIdx);
if (subMod.Manipulations.Count == manipulations.Count
&& subMod.Manipulations.All(m => manipulations.TryGetValue(m, out var old) && old.EntryEquals(m)))
return;
ModOptionChanged.Invoke(ModOptionChangeType.PrepareChange, mod, groupIdx, optionIdx, -1);
subMod.ManipulationData = manipulations;
ModOptionChanged.Invoke(ModOptionChangeType.OptionMetaChanged, mod, groupIdx, optionIdx, -1);
}
public void OptionSetFiles(Mod mod, int groupIdx, int optionIdx, Dictionary<Utf8GamePath, FullPath> replacements)
{
var subMod = GetSubMod(mod, groupIdx, optionIdx);
if (subMod.FileData.SetEquals(replacements))
return;
ModOptionChanged.Invoke(ModOptionChangeType.PrepareChange, mod, groupIdx, optionIdx, -1);
subMod.FileData = replacements;
ModOptionChanged.Invoke(ModOptionChangeType.OptionFilesChanged, mod, groupIdx, optionIdx, -1);
}
public void OptionAddFiles(Mod mod, int groupIdx, int optionIdx, Dictionary<Utf8GamePath, FullPath> additions)
{
var subMod = GetSubMod(mod, groupIdx, optionIdx);
var oldCount = subMod.FileData.Count;
subMod.FileData.AddFrom(additions);
if (oldCount != subMod.FileData.Count)
ModOptionChanged.Invoke(ModOptionChangeType.OptionFilesAdded, mod, groupIdx, optionIdx, -1);
}
public void OptionSetFileSwaps(Mod mod, int groupIdx, int optionIdx, Dictionary<Utf8GamePath, FullPath> swaps)
{
var subMod = GetSubMod(mod, groupIdx, optionIdx);
if (subMod.FileSwapData.SetEquals(swaps))
return;
ModOptionChanged.Invoke(ModOptionChangeType.PrepareChange, mod, groupIdx, optionIdx, -1);
subMod.FileSwapData = swaps;
ModOptionChanged.Invoke(ModOptionChangeType.OptionSwapsChanged, mod, groupIdx, optionIdx, -1);
}
public bool VerifyFileName(Mod mod, IModGroup? group, string newName, bool message)
{
var path = newName.RemoveInvalidPathSymbols();
if (path.Length != 0
&& !mod.Groups.Any(o => !ReferenceEquals(o, group)
&& string.Equals(o.Name.RemoveInvalidPathSymbols(), path, StringComparison.OrdinalIgnoreCase)))
return true;
if (message)
Penumbra.ChatService.NotificationMessage(
$"Could not name option {newName} because option with same filename {path} already exists.",
"Warning", NotificationType.Warning);
return 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(),
};
}
private static void OnModOptionChange(ModOptionChangeType type, Mod mod, int groupIdx, int _, int _2)
{
if (type == ModOptionChangeType.PrepareChange)
return;
// File deletion is handled in the actual function.
if (type is ModOptionChangeType.GroupDeleted or ModOptionChangeType.GroupMoved)
{
mod.SaveAllGroups();
}
else
{
if (groupIdx == -1)
mod.SaveDefaultModDelayed();
else
IModGroup.SaveDelayed(mod._groups[groupIdx], mod.ModPath, groupIdx);
}
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,
};
}
}
using System;
using System.Collections.Generic;
using System.Linq;
using Dalamud.Interface.Internal.Notifications;
using OtterGui;
using OtterGui.Filesystem;
using Penumbra.Api.Enums;
using Penumbra.Meta.Manipulations;
using Penumbra.Services;
using Penumbra.String.Classes;
using Penumbra.Util;
namespace Penumbra.Mods;
public class ModOptionEditor
{
private readonly CommunicatorService _communicator;
private readonly FilenameService _filenames;
private readonly SaveService _saveService;
public ModOptionEditor(CommunicatorService communicator, SaveService saveService, FilenameService filenames)
{
_communicator = communicator;
_saveService = saveService;
_filenames = filenames;
}
/// <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];
if (group.Type == type)
return;
mod._groups[groupIdx] = group.Convert(type);
_saveService.QueueSave(new ModSaveGroup(mod, groupIdx));
_communicator.ModOptionChanged.Invoke(ModOptionChangeType.GroupTypeChanged, mod, groupIdx, -1, -1);
}
/// <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];
if (group.DefaultSettings == defaultOption)
return;
group.DefaultSettings = defaultOption;
_saveService.QueueSave(new ModSaveGroup(mod, groupIdx));
_communicator.ModOptionChanged.Invoke(ModOptionChangeType.DefaultOptionChanged, mod, groupIdx, -1, -1);
}
/// <summary> Rename an option group if possible. </summary>
public void RenameModGroup(Mod mod, int groupIdx, string newName)
{
var group = mod._groups[groupIdx];
var oldName = group.Name;
if (oldName == newName || !VerifyFileName(mod, group, newName, true))
return;
_saveService.ImmediateDelete(new ModSaveGroup(mod, groupIdx));
var _ = group switch
{
SingleModGroup s => s.Name = newName,
MultiModGroup m => m.Name = newName,
_ => newName,
};
_saveService.ImmediateSave(new ModSaveGroup(mod, groupIdx));
_communicator.ModOptionChanged.Invoke(ModOptionChangeType.GroupRenamed, mod, groupIdx, -1, -1);
}
/// <summary> Add a new mod, empty option group of the given type and name. </summary>
public void AddModGroup(Mod mod, GroupType type, string newName)
{
if (!VerifyFileName(mod, null, newName, true))
return;
var maxPriority = mod._groups.Count == 0 ? 0 : mod._groups.Max(o => o.Priority) + 1;
mod._groups.Add(type == GroupType.Multi
? new MultiModGroup
{
Name = newName,
Priority = maxPriority,
}
: new SingleModGroup
{
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);
}
/// <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];
_communicator.ModOptionChanged.Invoke(ModOptionChangeType.PrepareChange, mod, groupIdx, -1, -1);
mod._groups.RemoveAt(groupIdx);
UpdateSubModPositions(mod, groupIdx);
_saveService.SaveAllOptionGroups(mod);
_communicator.ModOptionChanged.Invoke(ModOptionChangeType.GroupDeleted, mod, groupIdx, -1, -1);
}
/// <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))
return;
UpdateSubModPositions(mod, Math.Min(groupIdxFrom, groupIdxTo));
_saveService.SaveAllOptionGroups(mod);
_communicator.ModOptionChanged.Invoke(ModOptionChangeType.GroupMoved, mod, groupIdxFrom, -1, groupIdxTo);
}
/// <summary> Change the description of the given option group. </summary>
public void ChangeGroupDescription(Mod mod, int groupIdx, string newDescription)
{
var group = mod._groups[groupIdx];
if (group.Description == newDescription)
return;
var _ = group switch
{
SingleModGroup s => s.Description = newDescription,
MultiModGroup m => m.Description = newDescription,
_ => newDescription,
};
_saveService.QueueSave(new ModSaveGroup(mod, groupIdx));
_communicator.ModOptionChanged.Invoke(ModOptionChangeType.DisplayChange, mod, groupIdx, -1, -1);
}
/// <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 option = group[optionIdx];
if (option.Description == newDescription || option is not SubMod s)
return;
s.Description = newDescription;
_saveService.QueueSave(new ModSaveGroup(mod, groupIdx));
_communicator.ModOptionChanged.Invoke(ModOptionChangeType.DisplayChange, mod, groupIdx, optionIdx, -1);
}
/// <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];
if (group.Priority == newPriority)
return;
var _ = group switch
{
SingleModGroup s => s.Priority = newPriority,
MultiModGroup m => m.Priority = newPriority,
_ => newPriority,
};
_saveService.QueueSave(new ModSaveGroup(mod, groupIdx));
_communicator.ModOptionChanged.Invoke(ModOptionChangeType.PriorityChanged, mod, groupIdx, -1, -1);
}
/// <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])
{
case SingleModGroup:
ChangeGroupPriority(mod, groupIdx, newPriority);
break;
case MultiModGroup m:
if (m.PrioritizedOptions[optionIdx].Priority == newPriority)
return;
m.PrioritizedOptions[optionIdx] = (m.PrioritizedOptions[optionIdx].Mod, newPriority);
_saveService.QueueSave(new ModSaveGroup(mod, groupIdx));
_communicator.ModOptionChanged.Invoke(ModOptionChangeType.PriorityChanged, mod, groupIdx, optionIdx, -1);
return;
}
}
/// <summary> Rename the given option. </summary>
public void RenameOption(Mod mod, int groupIdx, int optionIdx, string newName)
{
switch (mod._groups[groupIdx])
{
case SingleModGroup s:
if (s.OptionData[optionIdx].Name == newName)
return;
s.OptionData[optionIdx].Name = newName;
break;
case MultiModGroup m:
var option = m.PrioritizedOptions[optionIdx].Mod;
if (option.Name == newName)
return;
option.Name = newName;
break;
}
_saveService.QueueSave(new ModSaveGroup(mod, groupIdx));
_communicator.ModOptionChanged.Invoke(ModOptionChangeType.DisplayChange, mod, groupIdx, optionIdx, -1);
}
/// <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 subMod = new SubMod(mod) { Name = newName };
subMod.SetPosition(groupIdx, group.Count);
switch (group)
{
case SingleModGroup s:
s.OptionData.Add(subMod);
break;
case MultiModGroup m:
m.PrioritizedOptions.Add((subMod, 0));
break;
}
_saveService.QueueSave(new ModSaveGroup(mod, groupIdx));
_communicator.ModOptionChanged.Invoke(ModOptionChangeType.OptionAdded, mod, groupIdx, group.Count - 1, -1);
}
/// <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)
{
if (option is not SubMod o)
return;
var group = mod._groups[groupIdx];
if (group.Type is GroupType.Multi && group.Count >= IModGroup.MaxMultiOptions)
{
Penumbra.Log.Error(
$"Could not add option {option.Name} to {group.Name} for mod {mod.Name}, "
+ $"since only up to {IModGroup.MaxMultiOptions} options are supported in one group.");
return;
}
o.SetPosition(groupIdx, group.Count);
switch (group)
{
case SingleModGroup s:
s.OptionData.Add(o);
break;
case MultiModGroup m:
m.PrioritizedOptions.Add((o, priority));
break;
}
_saveService.QueueSave(new ModSaveGroup(mod, groupIdx));
_communicator.ModOptionChanged.Invoke(ModOptionChangeType.OptionAdded, mod, groupIdx, group.Count - 1, -1);
}
/// <summary> Delete the given option from the given group. </summary>
public void DeleteOption(Mod mod, int groupIdx, int optionIdx)
{
var group = mod._groups[groupIdx];
_communicator.ModOptionChanged.Invoke(ModOptionChangeType.PrepareChange, mod, groupIdx, optionIdx, -1);
switch (group)
{
case SingleModGroup s:
s.OptionData.RemoveAt(optionIdx);
break;
case MultiModGroup m:
m.PrioritizedOptions.RemoveAt(optionIdx);
break;
}
group.UpdatePositions(optionIdx);
_saveService.QueueSave(new ModSaveGroup(mod, groupIdx));
_communicator.ModOptionChanged.Invoke(ModOptionChangeType.OptionDeleted, 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)
{
var group = mod._groups[groupIdx];
if (!group.MoveOption(optionIdxFrom, optionIdxTo))
return;
_saveService.QueueSave(new ModSaveGroup(mod, groupIdx));
_communicator.ModOptionChanged.Invoke(ModOptionChangeType.OptionMoved, mod, groupIdx, optionIdxFrom, optionIdxTo);
}
/// <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.Manipulations.Count == manipulations.Count
&& subMod.Manipulations.All(m => manipulations.TryGetValue(m, out var old) && old.EntryEquals(m)))
return;
_communicator.ModOptionChanged.Invoke(ModOptionChangeType.PrepareChange, mod, groupIdx, optionIdx, -1);
subMod.ManipulationData = manipulations;
_saveService.QueueSave(new ModSaveGroup(mod, groupIdx));
_communicator.ModOptionChanged.Invoke(ModOptionChangeType.OptionMetaChanged, mod, groupIdx, optionIdx, -1);
}
/// <summary> Set the file redirections for a given option. Replaces existing redirections. </summary>
public void OptionSetFiles(Mod mod, int groupIdx, int optionIdx, Dictionary<Utf8GamePath, FullPath> replacements)
{
var subMod = GetSubMod(mod, groupIdx, optionIdx);
if (subMod.FileData.SetEquals(replacements))
return;
_communicator.ModOptionChanged.Invoke(ModOptionChangeType.PrepareChange, mod, groupIdx, optionIdx, -1);
subMod.FileData = replacements;
_saveService.QueueSave(new ModSaveGroup(mod, groupIdx));
_communicator.ModOptionChanged.Invoke(ModOptionChangeType.OptionFilesChanged, mod, groupIdx, optionIdx, -1);
}
/// <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)
{
var subMod = GetSubMod(mod, groupIdx, optionIdx);
var oldCount = subMod.FileData.Count;
subMod.FileData.AddFrom(additions);
if (oldCount != subMod.FileData.Count)
{
_saveService.QueueSave(new ModSaveGroup(mod, groupIdx));
_communicator.ModOptionChanged.Invoke(ModOptionChangeType.OptionFilesAdded, mod, groupIdx, optionIdx, -1);
}
}
/// <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)
{
var subMod = GetSubMod(mod, groupIdx, optionIdx);
if (subMod.FileSwapData.SetEquals(swaps))
return;
_communicator.ModOptionChanged.Invoke(ModOptionChangeType.PrepareChange, mod, groupIdx, optionIdx, -1);
subMod.FileSwapData = swaps;
_saveService.QueueSave(new ModSaveGroup(mod, groupIdx));
_communicator.ModOptionChanged.Invoke(ModOptionChangeType.OptionSwapsChanged, mod, groupIdx, optionIdx, -1);
}
/// <summary> Verify that a new option group name is unique in this mod. </summary>
public static bool VerifyFileName(Mod mod, IModGroup? group, string newName, bool message)
{
var path = newName.RemoveInvalidPathSymbols();
if (path.Length != 0
&& !mod.Groups.Any(o => !ReferenceEquals(o, group)
&& string.Equals(o.Name.RemoveInvalidPathSymbols(), path, StringComparison.OrdinalIgnoreCase)))
return true;
if (message)
Penumbra.ChatService.NotificationMessage(
$"Could not name option {newName} because option with same filename {path} already exists.",
"Warning", NotificationType.Warning);
return false;
}
/// <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 (o, optionIdx) in group.OfType<SubMod>().WithIndex())
o.SetPosition(groupIdx, optionIdx);
}
}
/// <summary> Get the correct option for the given group and option index. </summary>
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 )
{
SaveAllGroups();
Penumbra.SaveService.SaveAllOptionGroups(this);
}
}
}

View file

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

View file

@ -103,7 +103,7 @@ public partial class Mod
var group = LoadModGroup( this, file, _groups.Count );
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 );
}
else
@ -114,32 +114,7 @@ public partial class Mod
if( changes )
{
SaveAllGroups();
}
}
// 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 );
Penumbra.SaveService.SaveAllOptionGroups(this);
}
}
}

View file

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

View file

@ -2,24 +2,25 @@ using System;
using System.Collections.Generic;
using System.IO;
using Newtonsoft.Json;
using OtterGui.Filesystem;
using Penumbra.Api.Enums;
using Penumbra.Services;
using Penumbra.Util;
namespace Penumbra.Mods;
public interface IModGroup : IEnumerable< ISubMod >
public interface IModGroup : IEnumerable<ISubMod>
{
public const int MaxMultiOptions = 32;
public string Name { get; }
public string Description { get; }
public GroupType Type { get; }
public int Priority { get; }
public uint DefaultSettings { get; set; }
public string Name { get; }
public string Description { get; }
public GroupType Type { get; }
public int Priority { get; }
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; }
@ -28,72 +29,76 @@ public interface IModGroup : IEnumerable< ISubMod >
{
GroupType.Single => Count > 1,
GroupType.Multi => Count > 0,
_ => false,
_ => false,
};
public string FileName( DirectoryInfo basePath, int groupIdx )
=> Path.Combine( basePath.FullName, $"group_{groupIdx + 1:D3}_{Name.RemoveInvalidPathSymbols().ToLowerInvariant()}.json" );
public IModGroup Convert(GroupType type);
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 );
if( !File.Exists( file ) )
{
return;
}
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;
}
_basePath = mod.ModPath;
if (_groupIdx < 0)
_defaultMod = mod.Default;
else
_group = mod.Groups[groupIdx];
_groupIdx = groupIdx;
}
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}",
() => SaveModGroup( group, basePath, groupIdx ) );
_basePath = basePath;
_group = group;
_groupIdx = groupIdx;
}
public static void Save( IModGroup group, DirectoryInfo basePath, int groupIdx )
=> SaveModGroup( group, basePath, groupIdx );
private static void SaveModGroup( IModGroup group, DirectoryInfo basePath, int groupIdx )
public ModSaveGroup(DirectoryInfo basePath, ISubMod @default)
{
var file = group.FileName( basePath, groupIdx );
using var s = File.Exists( file ) ? File.Open( file, FileMode.Truncate ) : File.Open( file, FileMode.CreateNew );
using var writer = new StreamWriter( s );
using var j = new JsonTextWriter( writer ) { Formatting = Formatting.Indented };
var serializer = new JsonSerializer { Formatting = Formatting.Indented };
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 )
_basePath = basePath;
_groupIdx = -1;
_defaultMod = @default;
}
public string ToFilename(FilenameService fileNames)
=> fileNames.OptionGroupFile(_basePath.FullName, _groupIdx, _group?.Name ?? string.Empty);
public void Save(StreamWriter writer)
{
using var j = new JsonTextWriter(writer) { Formatting = Formatting.Indented };
var serializer = new JsonSerializer { Formatting = Formatting.Indented };
if (_groupIdx >= 0)
{
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
=> "Penumbra";
public static Logger Log { get; private set; } = null!;
public static ChatService ChatService { get; private set; } = null!;
public static SaveService SaveService { get; private set; } = null!;
public static Configuration Config { get; private set; } = null!;
public static Logger Log { get; private set; } = null!;
public static ChatService ChatService { get; private set; } = null!;
public static FilenameService Filenames { 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 CharacterUtility CharacterUtility { get; private set; } = null!;
public static GameEventManager GameEvents { get; private set; } = null!;
public static MetaFileManager MetaFileManager { get; private set; } = null!;
public static ModManager ModManager { get; private set; } = null!;
public static CollectionManager CollectionManager { get; private set; } = null!;
public static ModManager ModManager { get; private set; } = null!;
public static CollectionManager CollectionManager { get; private set; } = null!;
public static TempCollectionManager TempCollections { get; private set; } = null!;
public static TempModManager TempMods { 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 readonly PathResolver PathResolver;
public readonly RedrawService RedrawService;
public readonly ModFileSystem ModFileSystem;
public HttpApi HttpApi = null!;
internal ConfigWindow? ConfigWindow { get; private set; }
private PenumbraWindowSystem? _windowSystem;
private bool _disposed;
public readonly PathResolver PathResolver;
public readonly RedrawService RedrawService;
public readonly ModFileSystem ModFileSystem;
public HttpApi HttpApi = null!;
internal ConfigWindow? ConfigWindow { get; private set; }
private PenumbraWindowSystem? _windowSystem;
private bool _disposed;
private readonly PenumbraNew _tmp;
@ -80,29 +81,30 @@ public class Penumbra : IDalamudPlugin
{
_tmp = new PenumbraNew(this, pluginInterface);
ChatService = _tmp.Services.GetRequiredService<ChatService>();
Filenames = _tmp.Services.GetRequiredService<FilenameService>();
SaveService = _tmp.Services.GetRequiredService<SaveService>();
Performance = _tmp.Services.GetRequiredService<PerformanceTracker>();
ValidityChecker = _tmp.Services.GetRequiredService<ValidityChecker>();
_tmp.Services.GetRequiredService<BackupService>();
Config = _tmp.Services.GetRequiredService<Configuration>();
CharacterUtility = _tmp.Services.GetRequiredService<CharacterUtility>();
GameEvents = _tmp.Services.GetRequiredService<GameEventManager>();
MetaFileManager = _tmp.Services.GetRequiredService<MetaFileManager>();
Framework = _tmp.Services.GetRequiredService<FrameworkManager>();
Actors = _tmp.Services.GetRequiredService<ActorService>().AwaitedService;
Identifier = _tmp.Services.GetRequiredService<IdentifierService>().AwaitedService;
GamePathParser = _tmp.Services.GetRequiredService<IGamePathParser>();
StainService = _tmp.Services.GetRequiredService<StainService>();
TempMods = _tmp.Services.GetRequiredService<TempModManager>();
ResidentResources = _tmp.Services.GetRequiredService<ResidentResourceManager>();
Config = _tmp.Services.GetRequiredService<Configuration>();
CharacterUtility = _tmp.Services.GetRequiredService<CharacterUtility>();
GameEvents = _tmp.Services.GetRequiredService<GameEventManager>();
MetaFileManager = _tmp.Services.GetRequiredService<MetaFileManager>();
Framework = _tmp.Services.GetRequiredService<FrameworkManager>();
Actors = _tmp.Services.GetRequiredService<ActorService>().AwaitedService;
Identifier = _tmp.Services.GetRequiredService<IdentifierService>().AwaitedService;
GamePathParser = _tmp.Services.GetRequiredService<IGamePathParser>();
StainService = _tmp.Services.GetRequiredService<StainService>();
TempMods = _tmp.Services.GetRequiredService<TempModManager>();
ResidentResources = _tmp.Services.GetRequiredService<ResidentResourceManager>();
_tmp.Services.GetRequiredService<ResourceManagerService>();
ModManager = _tmp.Services.GetRequiredService<ModManager>();
CollectionManager = _tmp.Services.GetRequiredService<CollectionManager>();
TempCollections = _tmp.Services.GetRequiredService<TempCollectionManager>();
ModFileSystem = _tmp.Services.GetRequiredService<ModFileSystem>();
RedrawService = _tmp.Services.GetRequiredService<RedrawService>();
ModManager = _tmp.Services.GetRequiredService<ModManager>();
CollectionManager = _tmp.Services.GetRequiredService<CollectionManager>();
TempCollections = _tmp.Services.GetRequiredService<TempCollectionManager>();
ModFileSystem = _tmp.Services.GetRequiredService<ModFileSystem>();
RedrawService = _tmp.Services.GetRequiredService<RedrawService>();
_tmp.Services.GetRequiredService<ResourceService>();
ResourceLoader = _tmp.Services.GetRequiredService<ResourceLoader>();
ResourceLoader = _tmp.Services.GetRequiredService<ResourceLoader>();
using (var t = _tmp.Services.GetRequiredService<StartTracker>().Measure(StartTimeType.PathResolver))
{
PathResolver = _tmp.Services.GetRequiredService<PathResolver>();
@ -112,7 +114,8 @@ public class Penumbra : IDalamudPlugin
SetupApi();
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);
Log.Information($"Loading native OtterTex assembly from {OtterTex.NativeDll.Directory}.");
@ -129,8 +132,8 @@ public class Penumbra : IDalamudPlugin
private void SetupApi()
{
using var timer = _tmp.Services.GetRequiredService<StartTracker>().Measure(StartTimeType.Api);
var api = _tmp.Services.GetRequiredService<IPenumbraApi>();
HttpApi = _tmp.Services.GetRequiredService<HttpApi>();
var api = _tmp.Services.GetRequiredService<IPenumbraApi>();
HttpApi = _tmp.Services.GetRequiredService<HttpApi>();
_tmp.Services.GetRequiredService<PenumbraIpcProviders>();
if (Config.EnableHttpApi)
HttpApi.CreateWebServer();

View file

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

View file

@ -45,13 +45,22 @@ public class CommunicatorService : IDisposable
/// </list> </summary>
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 changed mod. </item>
/// <item>Parameter is the old name of the mod in case of a name change, and null otherwise. </item>
/// </list> </summary>
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()
{
CollectionChange.Dispose();
@ -60,5 +69,6 @@ public class CommunicatorService : IDisposable
CreatingCharacterBase.Dispose();
CreatedCharacterBase.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>
public string ModMetaPath(string modDirectory)
=> 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);
Mod.Creator.CreateDefaultFiles(newDir);
_modManager.AddMod(newDir);
if (!_swapData.WriteMod(_modManager.Last(),
if (!_swapData.WriteMod(_modManager, _modManager.Last(),
_useFileSwaps ? ItemSwapContainer.WriteType.UseSwaps : ItemSwapContainer.WriteType.NoSwaps))
_modManager.DeleteMod(_modManager.Count - 1);
}
@ -296,16 +296,16 @@ public class ItemSwapTab : IDisposable, ITab
{
if (_selectedGroup == null)
{
_modManager.AddModGroup(_mod, GroupType.Multi, _newGroupName);
_modManager.OptionEditor.AddModGroup(_mod, GroupType.Multi, _newGroupName);
_selectedGroup = _mod.Groups.Last();
groupCreated = true;
}
_modManager.AddOption(_mod, _mod.Groups.IndexOf(_selectedGroup), _newOptionName);
_modManager.OptionEditor.AddOption(_mod, _mod.Groups.IndexOf(_selectedGroup), _newOptionName);
optionCreated = true;
optionFolderName = Directory.CreateDirectory(optionFolderName.FullName);
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,
_mod.Groups.IndexOf(_selectedGroup), _selectedGroup.Count - 1))
throw new Exception("Failure writing files for mod swap.");
@ -317,11 +317,11 @@ public class ItemSwapTab : IDisposable, ITab
try
{
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)
{
_modManager.DeleteModGroup(_mod, _mod.Groups.IndexOf(_selectedGroup!));
_modManager.OptionEditor.DeleteModGroup(_mod, _mod.Groups.IndexOf(_selectedGroup!));
_selectedGroup = null;
}

View file

@ -12,6 +12,7 @@ using OtterGui.Raii;
using OtterGui.Widgets;
using Penumbra.Api.Enums;
using Penumbra.Mods;
using Penumbra.Services;
using Penumbra.UI.AdvancedWindow;
using Penumbra.Util;
@ -20,7 +21,8 @@ namespace Penumbra.UI.ModsTab;
public class ModPanelEditTab : ITab
{
private readonly ChatService _chat;
private readonly ModManager _modManager;
private readonly FilenameService _filenames;
private readonly ModManager _modManager;
private readonly ModFileSystem _fileSystem;
private readonly ModFileSystemSelector _selector;
private readonly ModEditWindow _editWindow;
@ -34,14 +36,15 @@ public class ModPanelEditTab : ITab
private Mod _mod = null!;
public ModPanelEditTab(ModManager modManager, ModFileSystemSelector selector, ModFileSystem fileSystem, ChatService chat,
ModEditWindow editWindow, ModEditor editor)
ModEditWindow editWindow, ModEditor editor, FilenameService filenames)
{
_modManager = modManager;
_selector = selector;
_fileSystem = fileSystem;
_chat = chat;
_editWindow = editWindow;
_editor = editor;
_modManager = modManager;
_selector = selector;
_fileSystem = fileSystem;
_chat = chat;
_editWindow = editWindow;
_editor = editor;
_filenames = filenames;
}
public ReadOnlySpan<byte> Label
@ -129,7 +132,7 @@ public class ModPanelEditTab : ITab
if (ImGui.Button("Update Bibo Material", buttonSize))
{
_editor.LoadMod(_mod);
_editor.MdlMaterialEditor.ReplaceAllMaterials("bibo", "b");
_editor.MdlMaterialEditor.ReplaceAllMaterials("bibo", "b");
_editor.MdlMaterialEditor.ReplaceAllMaterials("bibopube", "c");
_editor.MdlMaterialEditor.SaveAllModels();
_editWindow.UpdateModels();
@ -189,7 +192,7 @@ public class ModPanelEditTab : ITab
var reducedSize = new Vector2(UiHelpers.InputTextMinusButton3, 0);
if (ImGui.Button("Edit Description", reducedSize))
_delayedActions.Enqueue(() => DescriptionEdit.OpenPopup(_mod, Input.Description));
_delayedActions.Enqueue(() => DescriptionEdit.OpenPopup(_filenames, _mod, Input.Description));
ImGui.SameLine();
var fileExists = File.Exists(_modManager.DataEditor.MetaFile(_mod));
@ -235,13 +238,13 @@ public class ModPanelEditTab : ITab
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.";
if (!ImGuiUtil.DrawDisabledButton(FontAwesomeIcon.Plus.ToIconString(), UiHelpers.IconButtonSize,
tt, !nameValid, true))
return;
modManager.AddModGroup(mod, GroupType.Single, _newGroupName);
modManager.OptionEditor.AddModGroup(mod, GroupType.Single, _newGroupName);
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>
private static class MoveDirectory
{
private static string? _currentModDirectory;
private static string? _currentModDirectory;
private static ModManager.NewDirectoryState _state = ModManager.NewDirectoryState.Identical;
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>
private static class DescriptionEdit
{
private const string PopupName = "Edit Description";
private static string _newDescription = string.Empty;
private static int _newDescriptionIdx = -1;
private static int _newDescriptionOptionIdx = -1;
private static Mod? _mod;
private const string PopupName = "Edit Description";
private static string _newDescription = string.Empty;
private static int _newDescriptionIdx = -1;
private static int _newDescriptionOptionIdx = -1;
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;
_newDescriptionOptionIdx = optionIdx;
_newDescription = groupIdx < 0
@ -353,9 +358,10 @@ public class ModPanelEditTab : ITab
break;
case >= 0:
if (_newDescriptionOptionIdx < 0)
modManager.ChangeGroupDescription(_mod, _newDescriptionIdx, _newDescription);
modManager.OptionEditor.ChangeGroupDescription(_mod, _newDescriptionIdx, _newDescription);
else
modManager.ChangeOptionDescription(_mod, _newDescriptionIdx, _newDescriptionOptionIdx, _newDescription);
modManager.OptionEditor.ChangeOptionDescription(_mod, _newDescriptionIdx, _newDescriptionOptionIdx,
_newDescription);
break;
}
@ -384,18 +390,18 @@ public class ModPanelEditTab : ITab
.Push(ImGuiStyleVar.ItemSpacing, _itemSpacing);
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");
ImGui.SameLine();
if (ImGuiUtil.DrawDisabledButton(FontAwesomeIcon.Trash.ToIconString(), UiHelpers.IconButtonSize,
"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();
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");
@ -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}.";
if (ImGuiUtil.DrawDisabledButton(FontAwesomeIcon.ArrowUp.ToIconString(), UiHelpers.IconButtonSize,
tt, groupIdx == 0, true))
_delayedActions.Enqueue(() => _modManager.MoveModGroup(_mod, groupIdx, groupIdx - 1));
_delayedActions.Enqueue(() => _modManager.OptionEditor.MoveModGroup(_mod, groupIdx, groupIdx - 1));
ImGui.SameLine();
tt = groupIdx == _mod.Groups.Count - 1
@ -413,16 +419,16 @@ public class ModPanelEditTab : ITab
: $"Move this group down to group {groupIdx + 2}.";
if (ImGuiUtil.DrawDisabledButton(FontAwesomeIcon.ArrowDown.ToIconString(), UiHelpers.IconButtonSize,
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();
if (ImGuiUtil.DrawDisabledButton(FontAwesomeIcon.Edit.ToIconString(), UiHelpers.IconButtonSize,
"Edit group description.", false, true))
_delayedActions.Enqueue(() => DescriptionEdit.OpenPopup(_mod, groupIdx));
_delayedActions.Enqueue(() => DescriptionEdit.OpenPopup(_filenames, _mod, groupIdx));
ImGui.SameLine();
var fileName = group.FileName(_mod.ModPath, groupIdx);
var fileName = _filenames.OptionGroupFile(_mod, groupIdx);
var fileExists = File.Exists(fileName);
tt = fileExists
? $"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 (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.");
}
@ -499,7 +505,7 @@ public class ModPanelEditTab : ITab
{
var isDefaultOption = ((group.DefaultSettings >> optionIdx) & 1) != 0;
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));
@ -508,17 +514,17 @@ public class ModPanelEditTab : ITab
ImGui.TableNextColumn();
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();
if (ImGuiUtil.DrawDisabledButton(FontAwesomeIcon.Edit.ToIconString(), UiHelpers.IconButtonSize, "Edit option description.",
false, true))
panel._delayedActions.Enqueue(() => DescriptionEdit.OpenPopup(panel._mod, groupIdx, optionIdx));
panel._delayedActions.Enqueue(() => DescriptionEdit.OpenPopup(panel._filenames, panel._mod, groupIdx, optionIdx));
ImGui.TableNextColumn();
if (ImGuiUtil.DrawDisabledButton(FontAwesomeIcon.Trash.ToIconString(), UiHelpers.IconButtonSize,
"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();
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,
50 * UiHelpers.Scale))
panel._modManager.ChangeOptionPriority(panel._mod, groupIdx, optionIdx, priority);
panel._modManager.OptionEditor.ChangeOptionPriority(panel._mod, groupIdx, optionIdx, priority);
ImGuiUtil.HoverTooltip("Option priority.");
}
@ -560,7 +566,7 @@ public class ModPanelEditTab : ITab
tt, !(canAddGroup && validName), true))
return;
panel._modManager.AddOption(mod, groupIdx, _newOptionName);
panel._modManager.OptionEditor.AddOption(mod, groupIdx, _newOptionName);
_newOptionName = string.Empty;
}
@ -591,7 +597,7 @@ public class ModPanelEditTab : ITab
if (_dragDropGroupIdx == groupIdx)
{
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
{
@ -604,9 +610,9 @@ public class ModPanelEditTab : ITab
var priority = sourceGroup.OptionPriority(_dragDropOptionIdx);
panel._delayedActions.Enqueue(() =>
{
panel._modManager.DeleteOption(panel._mod, sourceGroupIdx, sourceOption);
panel._modManager.AddOption(panel._mod, groupIdx, option, priority);
panel._modManager.MoveOption(panel._mod, groupIdx, currentCount, optionIdx);
panel._modManager.OptionEditor.DeleteOption(panel._mod, sourceGroupIdx, sourceOption);
panel._modManager.OptionEditor.AddOption(panel._mod, groupIdx, option, priority);
panel._modManager.OptionEditor.MoveOption(panel._mod, groupIdx, currentCount, optionIdx);
});
}
}
@ -633,12 +639,12 @@ public class ModPanelEditTab : ITab
return;
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;
using var style = ImRaii.PushStyle(ImGuiStyleVar.Alpha, 0.5f, !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();
if (!canSwitchToMulti)

View file

@ -3,6 +3,7 @@ using System.IO;
using System.Text;
using OtterGui.Classes;
using OtterGui.Log;
using Penumbra.Mods;
using Penumbra.Services;
namespace Penumbra.Util;
@ -94,4 +95,24 @@ public class SaveService
_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));
}
}