Current State.
Some checks failed
.NET Build / build (push) Has been cancelled

This commit is contained in:
Ottermandias 2025-12-17 18:21:11 +01:00
parent 7b451c5097
commit bb957c1119
50 changed files with 2016 additions and 1247 deletions

View file

@ -1,169 +1,172 @@
using Penumbra.Communication;
using Penumbra.GameData.Data;
using Penumbra.Mods.Manager.OptionEditor;
using Penumbra.Services;
using Penumbra.Util;
namespace Penumbra.Mods.Manager;
public class ModCacheManager : IDisposable, Luna.IRequiredService
{
private readonly Configuration _config;
private readonly CommunicatorService _communicator;
private readonly ObjectIdentification _identifier;
private readonly ModStorage _modManager;
private bool _updatingItems;
public ModCacheManager(CommunicatorService communicator, ObjectIdentification identifier, ModStorage modStorage, Configuration config)
{
_communicator = communicator;
_identifier = identifier;
_modManager = modStorage;
_config = config;
_communicator.ModOptionChanged.Subscribe(OnModOptionChange, ModOptionChanged.Priority.ModCacheManager);
_communicator.ModPathChanged.Subscribe(OnModPathChange, ModPathChanged.Priority.ModCacheManager);
_communicator.ModDataChanged.Subscribe(OnModDataChange, ModDataChanged.Priority.ModCacheManager);
_communicator.ModDiscoveryFinished.Subscribe(OnModDiscoveryFinished, ModDiscoveryFinished.Priority.ModCacheManager);
identifier.Awaiter.ContinueWith(_ => OnIdentifierCreation(), TaskScheduler.Default);
OnModDiscoveryFinished();
}
public void Dispose()
{
_communicator.ModOptionChanged.Unsubscribe(OnModOptionChange);
_communicator.ModPathChanged.Unsubscribe(OnModPathChange);
_communicator.ModDataChanged.Unsubscribe(OnModDataChange);
_communicator.ModDiscoveryFinished.Unsubscribe(OnModDiscoveryFinished);
}
private void OnModOptionChange(in ModOptionChanged.Arguments arguments)
{
switch (arguments.Type)
{
case ModOptionChangeType.GroupAdded:
case ModOptionChangeType.GroupDeleted:
case ModOptionChangeType.OptionAdded:
case ModOptionChangeType.OptionDeleted:
UpdateChangedItems(arguments.Mod);
UpdateCounts(arguments.Mod);
break;
case ModOptionChangeType.GroupTypeChanged:
UpdateHasOptions(arguments.Mod);
break;
case ModOptionChangeType.OptionFilesChanged:
case ModOptionChangeType.OptionFilesAdded:
UpdateChangedItems(arguments.Mod);
UpdateFileCount(arguments.Mod);
break;
case ModOptionChangeType.OptionSwapsChanged:
UpdateChangedItems(arguments.Mod);
UpdateSwapCount(arguments.Mod);
break;
case ModOptionChangeType.OptionMetaChanged:
UpdateChangedItems(arguments.Mod);
UpdateMetaCount(arguments.Mod);
break;
}
}
private void OnModPathChange(in ModPathChanged.Arguments arguments)
{
switch (arguments.Type)
{
case ModPathChangeType.Added:
case ModPathChangeType.Reloaded:
RefreshWithChangedItems(arguments.Mod);
break;
}
}
private static void OnModDataChange(in ModDataChanged.Arguments arguments)
{
if ((arguments.Type & (ModDataChangeType.LocalTags | ModDataChangeType.ModTags)) is not 0)
UpdateTags(arguments.Mod);
}
private void OnModDiscoveryFinished()
{
if (!_identifier.Awaiter.IsCompletedSuccessfully || _updatingItems)
{
Parallel.ForEach(_modManager, RefreshWithoutChangedItems);
}
else
{
_updatingItems = true;
Parallel.ForEach(_modManager, RefreshWithChangedItems);
_updatingItems = false;
}
}
private void OnIdentifierCreation()
{
if (_updatingItems)
return;
_updatingItems = true;
Parallel.ForEach(_modManager, UpdateChangedItems);
_updatingItems = false;
}
private static void UpdateFileCount(Mod mod)
=> mod.TotalFileCount = mod.AllDataContainers.Sum(s => s.Files.Count);
private static void UpdateSwapCount(Mod mod)
=> mod.TotalSwapCount = mod.AllDataContainers.Sum(s => s.FileSwaps.Count);
private static void UpdateMetaCount(Mod mod)
=> mod.TotalManipulations = mod.AllDataContainers.Sum(s => s.Manipulations.Count);
private static void UpdateHasOptions(Mod mod)
=> mod.HasOptions = mod.Groups.Any(o => o.IsOption);
private static void UpdateTags(Mod mod)
=> mod.AllTagsLower = string.Join('\0', mod.ModTags.Concat(mod.LocalTags).Select(s => s.ToLowerInvariant()));
private void UpdateChangedItems(Mod mod)
{
mod.ChangedItems.Clear();
_identifier.AddChangedItems(mod.Default, mod.ChangedItems);
foreach (var group in mod.Groups)
group.AddChangedItems(_identifier, mod.ChangedItems);
if (_config.HideMachinistOffhandFromChangedItems)
mod.ChangedItems.RemoveMachinistOffhands();
mod.LowerChangedItemsString = string.Join("\0", mod.ChangedItems.Keys.Select(k => k.ToLowerInvariant()));
++mod.LastChangedItemsUpdate;
}
private static void UpdateCounts(Mod mod)
{
mod.TotalFileCount = mod.Default.Files.Count;
mod.TotalSwapCount = mod.Default.FileSwaps.Count;
mod.TotalManipulations = mod.Default.Manipulations.Count;
mod.HasOptions = false;
foreach (var group in mod.Groups)
{
mod.HasOptions |= group.IsOption;
var (files, swaps, manips) = group.GetCounts();
mod.TotalFileCount += files;
mod.TotalSwapCount += swaps;
mod.TotalManipulations += manips;
}
}
private void RefreshWithChangedItems(Mod mod)
{
UpdateTags(mod);
UpdateCounts(mod);
UpdateChangedItems(mod);
}
private void RefreshWithoutChangedItems(Mod mod)
{
UpdateTags(mod);
UpdateCounts(mod);
}
}
using Luna;
using Penumbra.Communication;
using Penumbra.GameData.Data;
using Penumbra.Mods.Manager.OptionEditor;
using Penumbra.Services;
using Penumbra.Util;
namespace Penumbra.Mods.Manager;
public class ModCacheManager : IDisposable, IRequiredService
{
private readonly Configuration _config;
private readonly CommunicatorService _communicator;
private readonly ObjectIdentification _identifier;
private readonly ModStorage _modManager;
private readonly SaveService _saveService;
private bool _updatingItems;
public ModCacheManager(CommunicatorService communicator, ObjectIdentification identifier, ModStorage modStorage, Configuration config,
SaveService saveService)
{
_communicator = communicator;
_identifier = identifier;
_modManager = modStorage;
_config = config;
_saveService = saveService;
_communicator.ModOptionChanged.Subscribe(OnModOptionChange, ModOptionChanged.Priority.ModCacheManager);
_communicator.ModPathChanged.Subscribe(OnModPathChange, ModPathChanged.Priority.ModCacheManager);
_communicator.ModDataChanged.Subscribe(OnModDataChange, ModDataChanged.Priority.ModCacheManager);
_communicator.ModDiscoveryFinished.Subscribe(OnModDiscoveryFinished, ModDiscoveryFinished.Priority.ModCacheManager);
identifier.Awaiter.ContinueWith(_ => OnIdentifierCreation(), TaskScheduler.Default);
OnModDiscoveryFinished();
}
public void Dispose()
{
_communicator.ModOptionChanged.Unsubscribe(OnModOptionChange);
_communicator.ModPathChanged.Unsubscribe(OnModPathChange);
_communicator.ModDataChanged.Unsubscribe(OnModDataChange);
_communicator.ModDiscoveryFinished.Unsubscribe(OnModDiscoveryFinished);
}
private void OnModOptionChange(in ModOptionChanged.Arguments arguments)
{
switch (arguments.Type)
{
case ModOptionChangeType.GroupAdded:
case ModOptionChangeType.GroupDeleted:
case ModOptionChangeType.OptionAdded:
case ModOptionChangeType.OptionDeleted:
UpdateChangedItems(arguments.Mod);
UpdateCounts(arguments.Mod);
break;
case ModOptionChangeType.GroupTypeChanged: UpdateHasOptions(arguments.Mod); break;
case ModOptionChangeType.OptionFilesChanged:
case ModOptionChangeType.OptionFilesAdded:
UpdateChangedItems(arguments.Mod);
UpdateFileCount(arguments.Mod);
break;
case ModOptionChangeType.OptionSwapsChanged:
UpdateChangedItems(arguments.Mod);
UpdateSwapCount(arguments.Mod);
break;
case ModOptionChangeType.OptionMetaChanged:
UpdateChangedItems(arguments.Mod);
UpdateMetaCount(arguments.Mod);
break;
}
}
private void OnModPathChange(in ModPathChanged.Arguments arguments)
{
switch (arguments.Type)
{
case ModPathChangeType.Added:
case ModPathChangeType.Reloaded:
RefreshWithChangedItems(arguments.Mod);
break;
}
}
private static void OnModDataChange(in ModDataChanged.Arguments arguments)
{
if ((arguments.Type & (ModDataChangeType.LocalTags | ModDataChangeType.ModTags)) is not 0)
UpdateTags(arguments.Mod);
}
private void OnModDiscoveryFinished()
{
if (!_identifier.Awaiter.IsCompletedSuccessfully || _updatingItems)
{
Parallel.ForEach(_modManager, RefreshWithoutChangedItems);
}
else
{
_updatingItems = true;
Parallel.ForEach(_modManager, RefreshWithChangedItems);
_updatingItems = false;
}
}
private void OnIdentifierCreation()
{
if (_updatingItems)
return;
_updatingItems = true;
Parallel.ForEach(_modManager, UpdateChangedItems);
_updatingItems = false;
}
private static void UpdateFileCount(Mod mod)
=> mod.TotalFileCount = mod.AllDataContainers.Sum(s => s.Files.Count);
private static void UpdateSwapCount(Mod mod)
=> mod.TotalSwapCount = mod.AllDataContainers.Sum(s => s.FileSwaps.Count);
private static void UpdateMetaCount(Mod mod)
=> mod.TotalManipulations = mod.AllDataContainers.Sum(s => s.Manipulations.Count);
private static void UpdateHasOptions(Mod mod)
=> mod.HasOptions = mod.Groups.Any(o => o.IsOption);
private static void UpdateTags(Mod mod)
=> mod.AllTagsLower = string.Join('\0', mod.ModTags.Concat(mod.LocalTags).Select(s => s.ToLowerInvariant()));
private void UpdateChangedItems(Mod mod)
{
mod.ChangedItems.Clear();
_identifier.AddChangedItems(mod.Default, mod.ChangedItems);
foreach (var group in mod.Groups)
group.AddChangedItems(_identifier, mod.ChangedItems);
if (_config.HideMachinistOffhandFromChangedItems)
mod.ChangedItems.RemoveMachinistOffhands();
mod.LowerChangedItemsString = string.Join("\0", mod.ChangedItems.Keys.Select(k => k.ToLowerInvariant()));
++mod.LastChangedItemsUpdate;
}
private static void UpdateCounts(Mod mod)
{
mod.TotalFileCount = mod.Default.Files.Count;
mod.TotalSwapCount = mod.Default.FileSwaps.Count;
mod.TotalManipulations = mod.Default.Manipulations.Count;
mod.HasOptions = false;
foreach (var group in mod.Groups)
{
mod.HasOptions |= group.IsOption;
var (files, swaps, manips) = group.GetCounts();
mod.TotalFileCount += files;
mod.TotalSwapCount += swaps;
mod.TotalManipulations += manips;
}
}
private void RefreshWithChangedItems(Mod mod)
{
UpdateTags(mod);
UpdateCounts(mod);
UpdateChangedItems(mod);
}
private void RefreshWithoutChangedItems(Mod mod)
{
UpdateTags(mod);
UpdateCounts(mod);
}
}

View file

@ -0,0 +1,74 @@
using Luna;
using Penumbra.Api.Enums;
using Penumbra.Collections.Manager;
using Penumbra.Communication;
using Penumbra.Services;
namespace Penumbra.Mods.Manager;
public class ModConfigUpdater : IDisposable, IRequiredService
{
private readonly CommunicatorService _communicator;
private readonly SaveService _saveService;
private readonly ModStorage _mods;
private readonly CollectionStorage _collections;
public ModConfigUpdater(CommunicatorService communicator, SaveService saveService, ModStorage mods, CollectionStorage collections)
{
_communicator = communicator;
_saveService = saveService;
_mods = mods;
_collections = collections;
_communicator.ModSettingChanged.Subscribe(OnModSettingChanged, ModSettingChanged.Priority.ModConfigUpdater);
}
public IEnumerable<Mod> ListUnusedMods(TimeSpan age)
{
var cutoff = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds() - (int)age.TotalMilliseconds;
foreach (var mod in _mods)
{
// Skip actively ignored mods.
if (mod.IgnoreLastConfig)
continue;
// Skip mods that had settings changed since the given maximum age.
if (mod.LastConfigEdit >= cutoff)
continue;
// Skip mods that are currently permanently enabled or have any temporary settings.
if (_collections.Any(c => c.GetOwnSettings(mod.Index)?.Enabled is true || c.GetTempSettings(mod.Index) is not null))
continue;
yield return mod;
}
}
private void OnModSettingChanged(in ModSettingChanged.Arguments arguments)
{
if (arguments.Inherited)
return;
switch (arguments.Type)
{
case ModSettingChange.Inheritance:
case ModSettingChange.MultiInheritance:
case ModSettingChange.MultiEnableState:
case ModSettingChange.TemporaryMod:
case ModSettingChange.Edited:
return;
}
if (arguments.Mod is { } mod)
{
mod.LastConfigEdit = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds();
_communicator.ModDataChanged.Invoke(new ModDataChanged.Arguments(ModDataChangeType.LastConfigEdit, mod, null));
_saveService.Save(SaveType.Delay, new ModLocalData(mod));
}
}
public void Dispose()
{
_communicator.ModSettingChanged.Unsubscribe(OnModSettingChanged);
}
}

View file

@ -27,7 +27,8 @@ public enum ModDataChangeType : uint
PreferredChangedItems = 0x004000,
RequiredFeatures = 0x008000,
FileSystemFolder = 0x010000,
FileSystemSortOrder = 0x020000,
FileSystemSortOrder = 0x020000,
LastConfigEdit = 0x040000,
}
public class ModDataEditor(SaveService saveService, CommunicatorService communicatorService, ItemData itemData) : Luna.IService

View file

@ -17,13 +17,14 @@ public sealed class ModTab : TwoPanelLayout, ITab<TabType>
public override ReadOnlySpan<byte> Label
=> "Mods2"u8;
public ModTab(ModFileSystemDrawer drawer, ModPanel panel, CollectionSelectHeader collectionHeader)
public ModTab(ModFileSystemDrawer drawer, ModPanel panel, CollectionSelectHeader collectionHeader, RedrawFooter redrawFooter)
{
LeftHeader = drawer.Header;
LeftFooter = drawer.Footer;
LeftPanel = drawer;
RightPanel = panel;
RightHeader = collectionHeader;
RightFooter = redrawFooter;
}
public void DrawContent()

View file

@ -71,6 +71,8 @@ public sealed class Mod : IMod, IFileSystemValue<Mod>
// Local Data
public DataPath Path { get; } = new();
public long ImportDate { get; internal set; } = DateTimeOffset.UnixEpoch.ToUnixTimeMilliseconds();
public long LastConfigEdit { get; internal set; } = DateTimeOffset.UnixEpoch.ToUnixTimeMilliseconds();
public bool IgnoreLastConfig { get; internal set; } = false;
public IReadOnlyList<string> LocalTags { get; internal set; } = [];
public string Note { get; internal set; } = string.Empty;
public HashSet<CustomItemId> PreferredChangedItems { get; internal set; } = [];

View file

@ -24,6 +24,7 @@ public readonly struct ModLocalData(Mod mod) : ISavable
{ nameof(Mod.Note), JToken.FromObject(mod.Note) },
{ nameof(Mod.Favorite), JToken.FromObject(mod.Favorite) },
{ nameof(Mod.PreferredChangedItems), JToken.FromObject(mod.PreferredChangedItems) },
{ nameof(Mod.LastConfigEdit), JToken.FromObject(mod.LastConfigEdit) },
};
if (mod.Path.Folder.Length > 0)
@ -40,12 +41,14 @@ public readonly struct ModLocalData(Mod mod) : ISavable
{
var dataFile = editor.SaveService.FileNames.LocalDataFile(mod);
var importDate = 0L;
var now = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds();
var importDate = now;
var localTags = Enumerable.Empty<string>();
var favorite = false;
var note = string.Empty;
var fileSystemFolder = string.Empty;
string? sortOrderName = null;
var lastConfigEdit = now;
HashSet<CustomItemId> preferredChangedItems = [];
@ -56,10 +59,11 @@ public readonly struct ModLocalData(Mod mod) : ISavable
var text = File.ReadAllText(dataFile);
var json = JObject.Parse(text);
importDate = json[nameof(Mod.ImportDate)]?.Value<long>() ?? importDate;
favorite = json[nameof(Mod.Favorite)]?.Value<bool>() ?? favorite;
note = json[nameof(Mod.Note)]?.Value<string>() ?? note;
localTags = (json[nameof(Mod.LocalTags)] as JArray)?.Values<string>().OfType<string>() ?? localTags;
importDate = json[nameof(Mod.ImportDate)]?.Value<long>() ?? importDate;
lastConfigEdit = json[nameof(Mod.LastConfigEdit)]?.Value<long>() ?? lastConfigEdit;
favorite = json[nameof(Mod.Favorite)]?.Value<bool>() ?? favorite;
note = json[nameof(Mod.Note)]?.Value<string>() ?? note;
localTags = (json[nameof(Mod.LocalTags)] as JArray)?.Values<string>().OfType<string>() ?? localTags;
preferredChangedItems =
(json[nameof(Mod.PreferredChangedItems)] as JArray)?.Values<ulong>().Select(i => (CustomItemId)i).ToHashSet()
?? mod.DefaultPreferredItems;
@ -84,6 +88,12 @@ public readonly struct ModLocalData(Mod mod) : ISavable
changes |= ModDataChangeType.ImportDate;
}
if (mod.LastConfigEdit != lastConfigEdit)
{
mod.LastConfigEdit = lastConfigEdit;
changes |= ModDataChangeType.LastConfigEdit;
}
changes |= UpdateTags(mod, null, localTags);
if (mod.Favorite != favorite)
@ -124,11 +134,11 @@ public readonly struct ModLocalData(Mod mod) : ISavable
internal static ModDataChangeType UpdateTags(Mod mod, IEnumerable<string>? newModTags, IEnumerable<string>? newLocalTags)
{
if (newModTags == null && newLocalTags == null)
if (newModTags is null && newLocalTags is null)
return 0;
ModDataChangeType type = 0;
if (newModTags != null)
if (newModTags is not null)
{
var modTags = newModTags.Where(t => t.Length > 0).Distinct().ToArray();
if (!modTags.SequenceEqual(mod.ModTags))
@ -139,7 +149,7 @@ public readonly struct ModLocalData(Mod mod) : ISavable
}
}
if (newLocalTags != null)
if (newLocalTags is not null)
{
var localTags = newLocalTags!.Where(t => t.Length > 0 && !mod.ModTags.Contains(t)).Distinct().ToArray();
if (!localTags.SequenceEqual(mod.LocalTags))