mirror of
https://github.com/xivdev/Penumbra.git
synced 2025-12-12 10:17:22 +01:00
Some renaming
This commit is contained in:
parent
828cd07df0
commit
0108e51636
12 changed files with 509 additions and 506 deletions
|
|
@ -7,6 +7,7 @@ using System.Linq;
|
|||
using System.Threading.Tasks;
|
||||
using Penumbra.Api;
|
||||
using Penumbra.Api.Enums;
|
||||
using Penumbra.Collections.Cache;
|
||||
using Penumbra.Interop.Services;
|
||||
using Penumbra.Mods;
|
||||
using Penumbra.Mods.Manager;
|
||||
|
|
@ -20,6 +21,7 @@ public class CollectionCacheManager : IDisposable, IReadOnlyDictionary<ModCollec
|
|||
private readonly CommunicatorService _communicator;
|
||||
private readonly CharacterUtility _characterUtility;
|
||||
|
||||
private readonly List<(ModCollectionCache, int ChangeCounter)>
|
||||
private readonly Dictionary<ModCollection, ModCollectionCache> _cache = new();
|
||||
|
||||
public int Count
|
||||
485
Penumbra/Collections/Cache/ModCollectionCache.cs
Normal file
485
Penumbra/Collections/Cache/ModCollectionCache.cs
Normal file
|
|
@ -0,0 +1,485 @@
|
|||
using OtterGui;
|
||||
using OtterGui.Classes;
|
||||
using Penumbra.Meta.Manager;
|
||||
using Penumbra.Meta.Manipulations;
|
||||
using Penumbra.Mods;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using Penumbra.Api.Enums;
|
||||
using Penumbra.String.Classes;
|
||||
using Penumbra.Mods.Manager;
|
||||
|
||||
namespace Penumbra.Collections.Cache;
|
||||
|
||||
public record struct ModPath(IMod Mod, FullPath Path);
|
||||
public record ModConflicts(IMod Mod2, List<object> Conflicts, bool HasPriority, bool Solved);
|
||||
|
||||
/// <summary>
|
||||
/// The Cache contains all required temporary data to use a collection.
|
||||
/// It will only be setup if a collection gets activated in any way.
|
||||
/// </summary>
|
||||
public class ModCollectionCache : IDisposable
|
||||
{
|
||||
private readonly ModCollection _collection;
|
||||
private readonly SortedList<string, (SingleArray<IMod>, object?)> _changedItems = new();
|
||||
public readonly Dictionary<Utf8GamePath, ModPath> ResolvedFiles = new();
|
||||
public readonly MetaManager MetaManipulations;
|
||||
private readonly Dictionary<IMod, SingleArray<ModConflicts>> _conflicts = new();
|
||||
|
||||
public IEnumerable<SingleArray<ModConflicts>> AllConflicts
|
||||
=> _conflicts.Values;
|
||||
|
||||
public SingleArray<ModConflicts> Conflicts(IMod mod)
|
||||
=> _conflicts.TryGetValue(mod, out var c) ? c : new SingleArray<ModConflicts>();
|
||||
|
||||
private int _changedItemsSaveCounter = -1;
|
||||
|
||||
// Obtain currently changed items. Computes them if they haven't been computed before.
|
||||
public IReadOnlyDictionary<string, (SingleArray<IMod>, object?)> ChangedItems
|
||||
{
|
||||
get
|
||||
{
|
||||
SetChangedItems();
|
||||
return _changedItems;
|
||||
}
|
||||
}
|
||||
|
||||
// The cache reacts through events on its collection changing.
|
||||
public ModCollectionCache(ModCollection collection)
|
||||
{
|
||||
_collection = collection;
|
||||
MetaManipulations = new MetaManager(_collection);
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
MetaManipulations.Dispose();
|
||||
}
|
||||
|
||||
// Resolve a given game path according to this collection.
|
||||
public FullPath? ResolvePath(Utf8GamePath gameResourcePath)
|
||||
{
|
||||
if (!ResolvedFiles.TryGetValue(gameResourcePath, out var candidate))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
if (candidate.Path.InternalName.Length > Utf8GamePath.MaxGamePathLength
|
||||
|| candidate.Path.IsRooted && !candidate.Path.Exists)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
return candidate.Path;
|
||||
}
|
||||
|
||||
// For a given full path, find all game paths that currently use this file.
|
||||
public IEnumerable<Utf8GamePath> ReverseResolvePath(FullPath localFilePath)
|
||||
{
|
||||
var needle = localFilePath.FullName.ToLower();
|
||||
if (localFilePath.IsRooted)
|
||||
{
|
||||
needle = needle.Replace('/', '\\');
|
||||
}
|
||||
|
||||
var iterator = ResolvedFiles
|
||||
.Where(f => string.Equals(f.Value.Path.FullName, needle, StringComparison.OrdinalIgnoreCase))
|
||||
.Select(kvp => kvp.Key);
|
||||
|
||||
// For files that are not rooted, try to add themselves.
|
||||
if (!localFilePath.IsRooted && Utf8GamePath.FromString(localFilePath.FullName, out var utf8))
|
||||
{
|
||||
iterator = iterator.Prepend(utf8);
|
||||
}
|
||||
|
||||
return iterator;
|
||||
}
|
||||
|
||||
// Reverse resolve multiple paths at once for efficiency.
|
||||
public HashSet<Utf8GamePath>[] ReverseResolvePaths(IReadOnlyCollection<string> fullPaths)
|
||||
{
|
||||
if (fullPaths.Count == 0)
|
||||
return Array.Empty<HashSet<Utf8GamePath>>();
|
||||
|
||||
var ret = new HashSet<Utf8GamePath>[fullPaths.Count];
|
||||
var dict = new Dictionary<FullPath, int>(fullPaths.Count);
|
||||
foreach (var (path, idx) in fullPaths.WithIndex())
|
||||
{
|
||||
dict[new FullPath(path)] = idx;
|
||||
ret[idx] = !Path.IsPathRooted(path) && Utf8GamePath.FromString(path, out var utf8)
|
||||
? new HashSet<Utf8GamePath> { utf8 }
|
||||
: new HashSet<Utf8GamePath>();
|
||||
}
|
||||
|
||||
foreach (var (game, full) in ResolvedFiles)
|
||||
{
|
||||
if (dict.TryGetValue(full.Path, out var idx))
|
||||
{
|
||||
ret[idx].Add(game);
|
||||
}
|
||||
}
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
public void FullRecalculation(bool isDefault)
|
||||
{
|
||||
ResolvedFiles.Clear();
|
||||
MetaManipulations.Reset();
|
||||
_conflicts.Clear();
|
||||
|
||||
// Add all forced redirects.
|
||||
foreach (var tempMod in Penumbra.TempMods.ModsForAllCollections.Concat(
|
||||
Penumbra.TempMods.Mods.TryGetValue(_collection, out var list) ? list : Array.Empty<TemporaryMod>()))
|
||||
{
|
||||
AddMod(tempMod, false);
|
||||
}
|
||||
|
||||
foreach (var mod in Penumbra.ModManager)
|
||||
{
|
||||
AddMod(mod, false);
|
||||
}
|
||||
|
||||
AddMetaFiles();
|
||||
|
||||
++_collection.ChangeCounter;
|
||||
|
||||
if (isDefault && Penumbra.CharacterUtility.Ready && Penumbra.Config.EnableMods)
|
||||
{
|
||||
Penumbra.ResidentResources.Reload();
|
||||
MetaManipulations.SetFiles();
|
||||
}
|
||||
}
|
||||
|
||||
public void ReloadMod(IMod mod, bool addMetaChanges)
|
||||
{
|
||||
RemoveMod(mod, addMetaChanges);
|
||||
AddMod(mod, addMetaChanges);
|
||||
}
|
||||
|
||||
public void RemoveMod(IMod mod, bool addMetaChanges)
|
||||
{
|
||||
var conflicts = Conflicts(mod);
|
||||
|
||||
foreach (var (path, _) in mod.AllSubMods.SelectMany(s => s.Files.Concat(s.FileSwaps)))
|
||||
{
|
||||
if (!ResolvedFiles.TryGetValue(path, out var modPath))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (modPath.Mod == mod)
|
||||
{
|
||||
ResolvedFiles.Remove(path);
|
||||
}
|
||||
}
|
||||
|
||||
foreach (var manipulation in mod.AllSubMods.SelectMany(s => s.Manipulations))
|
||||
{
|
||||
if (MetaManipulations.TryGetValue(manipulation, out var registeredMod) && registeredMod == mod)
|
||||
{
|
||||
MetaManipulations.RevertMod(manipulation);
|
||||
}
|
||||
}
|
||||
|
||||
_conflicts.Remove(mod);
|
||||
foreach (var conflict in conflicts)
|
||||
{
|
||||
if (conflict.HasPriority)
|
||||
{
|
||||
ReloadMod(conflict.Mod2, false);
|
||||
}
|
||||
else
|
||||
{
|
||||
var newConflicts = Conflicts(conflict.Mod2).Remove(c => c.Mod2 == mod);
|
||||
if (newConflicts.Count > 0)
|
||||
{
|
||||
_conflicts[conflict.Mod2] = newConflicts;
|
||||
}
|
||||
else
|
||||
{
|
||||
_conflicts.Remove(conflict.Mod2);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (addMetaChanges)
|
||||
{
|
||||
++_collection.ChangeCounter;
|
||||
if (_collection == Penumbra.CollectionManager.Active.Default && Penumbra.CharacterUtility.Ready && Penumbra.Config.EnableMods)
|
||||
{
|
||||
Penumbra.ResidentResources.Reload();
|
||||
MetaManipulations.SetFiles();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Add all files and possibly manipulations of a given mod according to its settings in this collection.
|
||||
public void AddMod(IMod mod, bool addMetaChanges)
|
||||
{
|
||||
if (mod.Index >= 0)
|
||||
{
|
||||
var settings = _collection[mod.Index].Settings;
|
||||
if (settings is not { Enabled: true })
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
foreach (var (group, groupIndex) in mod.Groups.WithIndex().OrderByDescending(g => g.Item1.Priority))
|
||||
{
|
||||
if (group.Count == 0)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var config = settings.Settings[groupIndex];
|
||||
switch (group.Type)
|
||||
{
|
||||
case GroupType.Single:
|
||||
AddSubMod(group[(int)config], mod);
|
||||
break;
|
||||
case GroupType.Multi:
|
||||
{
|
||||
foreach (var (option, _) in group.WithIndex()
|
||||
.Where(p => (1 << p.Item2 & config) != 0)
|
||||
.OrderByDescending(p => group.OptionPriority(p.Item2)))
|
||||
{
|
||||
AddSubMod(option, mod);
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
AddSubMod(mod.Default, mod);
|
||||
|
||||
if (addMetaChanges)
|
||||
{
|
||||
++_collection.ChangeCounter;
|
||||
if (Penumbra.ModCaches[mod.Index].TotalManipulations > 0)
|
||||
{
|
||||
AddMetaFiles();
|
||||
}
|
||||
|
||||
if (_collection == Penumbra.CollectionManager.Active.Default && Penumbra.CharacterUtility.Ready && Penumbra.Config.EnableMods)
|
||||
{
|
||||
Penumbra.ResidentResources.Reload();
|
||||
MetaManipulations.SetFiles();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Add all files and possibly manipulations of a specific submod
|
||||
private void AddSubMod(ISubMod subMod, IMod parentMod)
|
||||
{
|
||||
foreach (var (path, file) in subMod.Files.Concat(subMod.FileSwaps))
|
||||
{
|
||||
AddFile(path, file, parentMod);
|
||||
}
|
||||
|
||||
foreach (var manip in subMod.Manipulations)
|
||||
{
|
||||
AddManipulation(manip, parentMod);
|
||||
}
|
||||
}
|
||||
|
||||
// Add a specific file redirection, handling potential conflicts.
|
||||
// For different mods, higher mod priority takes precedence before option group priority,
|
||||
// which takes precedence before option priority, which takes precedence before ordering.
|
||||
// Inside the same mod, conflicts are not recorded.
|
||||
private void AddFile(Utf8GamePath path, FullPath file, IMod mod)
|
||||
{
|
||||
if (!ModCollection.CheckFullPath(path, file))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (ResolvedFiles.TryAdd(path, new ModPath(mod, file)))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var modPath = ResolvedFiles[path];
|
||||
// Lower prioritized option in the same mod.
|
||||
if (mod == modPath.Mod)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (AddConflict(path, mod, modPath.Mod))
|
||||
{
|
||||
ResolvedFiles[path] = new ModPath(mod, file);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Remove all empty conflict sets for a given mod with the given conflicts.
|
||||
// If transitive is true, also removes the corresponding version of the other mod.
|
||||
private void RemoveEmptyConflicts(IMod mod, SingleArray<ModConflicts> oldConflicts, bool transitive)
|
||||
{
|
||||
var changedConflicts = oldConflicts.Remove(c =>
|
||||
{
|
||||
if (c.Conflicts.Count == 0)
|
||||
{
|
||||
if (transitive)
|
||||
{
|
||||
RemoveEmptyConflicts(c.Mod2, Conflicts(c.Mod2), false);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
});
|
||||
if (changedConflicts.Count == 0)
|
||||
{
|
||||
_conflicts.Remove(mod);
|
||||
}
|
||||
else
|
||||
{
|
||||
_conflicts[mod] = changedConflicts;
|
||||
}
|
||||
}
|
||||
|
||||
// Add a new conflict between the added mod and the existing mod.
|
||||
// Update all other existing conflicts between the existing mod and other mods if necessary.
|
||||
// Returns if the added mod takes priority before the existing mod.
|
||||
private bool AddConflict(object data, IMod addedMod, IMod existingMod)
|
||||
{
|
||||
var addedPriority = addedMod.Index >= 0 ? _collection[addedMod.Index].Settings!.Priority : addedMod.Priority;
|
||||
var existingPriority = existingMod.Index >= 0 ? _collection[existingMod.Index].Settings!.Priority : existingMod.Priority;
|
||||
|
||||
if (existingPriority < addedPriority)
|
||||
{
|
||||
var tmpConflicts = Conflicts(existingMod);
|
||||
foreach (var conflict in tmpConflicts)
|
||||
{
|
||||
if (data is Utf8GamePath path && conflict.Conflicts.RemoveAll(p => p is Utf8GamePath x && x.Equals(path)) > 0
|
||||
|| data is MetaManipulation meta && conflict.Conflicts.RemoveAll(m => m is MetaManipulation x && x.Equals(meta)) > 0)
|
||||
{
|
||||
AddConflict(data, addedMod, conflict.Mod2);
|
||||
}
|
||||
}
|
||||
|
||||
RemoveEmptyConflicts(existingMod, tmpConflicts, true);
|
||||
}
|
||||
|
||||
var addedConflicts = Conflicts(addedMod);
|
||||
var existingConflicts = Conflicts(existingMod);
|
||||
if (addedConflicts.FindFirst(c => c.Mod2 == existingMod, out var oldConflicts))
|
||||
{
|
||||
// Only need to change one list since both conflict lists refer to the same list.
|
||||
oldConflicts.Conflicts.Add(data);
|
||||
}
|
||||
else
|
||||
{
|
||||
// Add the same conflict list to both conflict directions.
|
||||
var conflictList = new List<object> { data };
|
||||
_conflicts[addedMod] = addedConflicts.Append(new ModConflicts(existingMod, conflictList, existingPriority < addedPriority,
|
||||
existingPriority != addedPriority));
|
||||
_conflicts[existingMod] = existingConflicts.Append(new ModConflicts(addedMod, conflictList,
|
||||
existingPriority >= addedPriority,
|
||||
existingPriority != addedPriority));
|
||||
}
|
||||
|
||||
return existingPriority < addedPriority;
|
||||
}
|
||||
|
||||
// Add a specific manipulation, handling potential conflicts.
|
||||
// For different mods, higher mod priority takes precedence before option group priority,
|
||||
// which takes precedence before option priority, which takes precedence before ordering.
|
||||
// Inside the same mod, conflicts are not recorded.
|
||||
private void AddManipulation(MetaManipulation manip, IMod mod)
|
||||
{
|
||||
if (!MetaManipulations.TryGetValue(manip, out var existingMod))
|
||||
{
|
||||
MetaManipulations.ApplyMod(manip, mod);
|
||||
return;
|
||||
}
|
||||
|
||||
// Lower prioritized option in the same mod.
|
||||
if (mod == existingMod)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (AddConflict(manip, mod, existingMod))
|
||||
{
|
||||
MetaManipulations.ApplyMod(manip, mod);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Add all necessary meta file redirects.
|
||||
private void AddMetaFiles()
|
||||
=> MetaManipulations.SetImcFiles();
|
||||
|
||||
// Increment the counter to ensure new files are loaded after applying meta changes.
|
||||
private void IncrementCounter()
|
||||
{
|
||||
++_collection.ChangeCounter;
|
||||
Penumbra.CharacterUtility.LoadingFinished -= IncrementCounter;
|
||||
}
|
||||
|
||||
|
||||
// Identify and record all manipulated objects for this entire collection.
|
||||
private void SetChangedItems()
|
||||
{
|
||||
if (_changedItemsSaveCounter == _collection.ChangeCounter)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
_changedItemsSaveCounter = _collection.ChangeCounter;
|
||||
_changedItems.Clear();
|
||||
// Skip IMCs because they would result in far too many false-positive items,
|
||||
// since they are per set instead of per item-slot/item/variant.
|
||||
var identifier = Penumbra.Identifier;
|
||||
var items = new SortedList<string, object?>(512);
|
||||
|
||||
void AddItems(IMod mod)
|
||||
{
|
||||
foreach (var (name, obj) in items)
|
||||
{
|
||||
if (!_changedItems.TryGetValue(name, out var data))
|
||||
{
|
||||
_changedItems.Add(name, (new SingleArray<IMod>(mod), obj));
|
||||
}
|
||||
else if (!data.Item1.Contains(mod))
|
||||
{
|
||||
_changedItems[name] = (data.Item1.Append(mod), obj is int x && data.Item2 is int y ? x + y : obj);
|
||||
}
|
||||
else if (obj is int x && data.Item2 is int y)
|
||||
{
|
||||
_changedItems[name] = (data.Item1, x + y);
|
||||
}
|
||||
}
|
||||
|
||||
items.Clear();
|
||||
}
|
||||
|
||||
foreach (var (resolved, modPath) in ResolvedFiles.Where(file => !file.Key.Path.EndsWith("imc"u8)))
|
||||
{
|
||||
identifier.Identify(items, resolved.ToString());
|
||||
AddItems(modPath.Mod);
|
||||
}
|
||||
|
||||
foreach (var (manip, mod) in MetaManipulations)
|
||||
{
|
||||
ModCacheManager.ComputeChangedItems(identifier, items, manip);
|
||||
AddItems(mod);
|
||||
}
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Penumbra.Log.Error($"Unknown Error:\n{e}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -19,6 +19,4 @@ public class CollectionManager
|
|||
Temp = temp;
|
||||
Editor = editor;
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,7 +4,6 @@ using System.Linq;
|
|||
using Dalamud.Game.ClientState.Objects.Enums;
|
||||
using Dalamud.Interface.Internal.Notifications;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using Penumbra.Collections.Manager;
|
||||
using Penumbra.GameData.Actors;
|
||||
using Penumbra.String;
|
||||
|
||||
|
|
|
|||
|
|
@ -4,20 +4,20 @@ using System.Linq;
|
|||
using Penumbra.Mods.Manager;
|
||||
using Penumbra.Util;
|
||||
|
||||
namespace Penumbra.Collections;
|
||||
namespace Penumbra.Collections.Manager;
|
||||
|
||||
/// <summary> Migration to convert ModCollections from older versions to newer. </summary>
|
||||
internal static class ModCollectionMigration
|
||||
{
|
||||
/// <summary> Migrate a mod collection to the current version. </summary>
|
||||
/// <summary> Migrate a mod collection to the current version. </summary>
|
||||
public static void Migrate(SaveService saver, ModStorage mods, int version, ModCollection collection)
|
||||
{
|
||||
{
|
||||
var changes = MigrateV0ToV1(collection, ref version);
|
||||
if (changes)
|
||||
saver.ImmediateSave(new ModCollectionSave(mods, collection));
|
||||
}
|
||||
|
||||
/// <summary> Migrate a mod collection from Version 0 to Version 1, which introduced support for inheritance. </summary>
|
||||
|
||||
/// <summary> Migrate a mod collection from Version 0 to Version 1, which introduced support for inheritance. </summary>
|
||||
private static bool MigrateV0ToV1(ModCollection collection, ref int version)
|
||||
{
|
||||
if (version > 0)
|
||||
|
|
@ -38,10 +38,10 @@ internal static class ModCollectionMigration
|
|||
return true;
|
||||
}
|
||||
|
||||
/// <summary> We treat every completely defaulted setting as inheritance-ready. </summary>
|
||||
/// <summary> We treat every completely defaulted setting as inheritance-ready. </summary>
|
||||
private static bool SettingIsDefaultV0(ModSettings.SavedSettings setting)
|
||||
=> setting is { Enabled: false, Priority: 0 } && setting.Settings.Values.All(s => s == 0);
|
||||
|
||||
=> setting is { Enabled: false, Priority: 0 } && setting.Settings.Values.All(s => s == 0);
|
||||
|
||||
/// <inheritdoc cref="SettingIsDefaultV0(ModSettings.SavedSettings)"/>
|
||||
private static bool SettingIsDefaultV0(ModSettings? setting)
|
||||
=> setting is { Enabled: false, Priority: 0 } && setting.Settings.All(s => s == 0);
|
||||
|
|
@ -13,7 +13,8 @@ using Penumbra.Meta.Files;
|
|||
using Penumbra.Meta.Manipulations;
|
||||
using Penumbra.String.Classes;
|
||||
using CharacterUtility = Penumbra.Interop.Services.CharacterUtility;
|
||||
|
||||
using Penumbra.Collections.Cache;
|
||||
|
||||
namespace Penumbra.Collections;
|
||||
|
||||
public partial class ModCollection
|
||||
|
|
@ -24,8 +25,10 @@ public partial class ModCollection
|
|||
public bool HasCache
|
||||
=> _cache != null;
|
||||
|
||||
// Count the number of changes of the effective file list.
|
||||
// This is used for material and imc changes.
|
||||
/// <summary>
|
||||
/// Count the number of changes of the effective file list.
|
||||
/// This is used for material and imc changes.
|
||||
/// </summary>
|
||||
public int ChangeCounter { get; internal set; }
|
||||
|
||||
// Only create, do not update.
|
||||
|
|
|
|||
|
|
@ -1,485 +0,0 @@
|
|||
using OtterGui;
|
||||
using OtterGui.Classes;
|
||||
using Penumbra.Meta.Manager;
|
||||
using Penumbra.Meta.Manipulations;
|
||||
using Penumbra.Mods;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using Penumbra.Api.Enums;
|
||||
using Penumbra.String.Classes;
|
||||
using Penumbra.Mods.Manager;
|
||||
|
||||
namespace Penumbra.Collections;
|
||||
|
||||
public record struct ModPath( IMod Mod, FullPath Path );
|
||||
public record ModConflicts( IMod Mod2, List< object > Conflicts, bool HasPriority, bool Solved );
|
||||
|
||||
/// <summary>
|
||||
/// The Cache contains all required temporary data to use a collection.
|
||||
/// It will only be setup if a collection gets activated in any way.
|
||||
/// </summary>
|
||||
public class ModCollectionCache : IDisposable
|
||||
{
|
||||
private readonly ModCollection _collection;
|
||||
private readonly SortedList< string, (SingleArray< IMod >, object?) > _changedItems = new();
|
||||
public readonly Dictionary< Utf8GamePath, ModPath > ResolvedFiles = new();
|
||||
public readonly MetaManager MetaManipulations;
|
||||
private readonly Dictionary< IMod, SingleArray< ModConflicts > > _conflicts = new();
|
||||
|
||||
public IEnumerable< SingleArray< ModConflicts > > AllConflicts
|
||||
=> _conflicts.Values;
|
||||
|
||||
public SingleArray< ModConflicts > Conflicts( IMod mod )
|
||||
=> _conflicts.TryGetValue( mod, out var c ) ? c : new SingleArray< ModConflicts >();
|
||||
|
||||
private int _changedItemsSaveCounter = -1;
|
||||
|
||||
// Obtain currently changed items. Computes them if they haven't been computed before.
|
||||
public IReadOnlyDictionary< string, (SingleArray< IMod >, object?) > ChangedItems
|
||||
{
|
||||
get
|
||||
{
|
||||
SetChangedItems();
|
||||
return _changedItems;
|
||||
}
|
||||
}
|
||||
|
||||
// The cache reacts through events on its collection changing.
|
||||
public ModCollectionCache( ModCollection collection )
|
||||
{
|
||||
_collection = collection;
|
||||
MetaManipulations = new MetaManager( _collection );
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
MetaManipulations.Dispose();
|
||||
}
|
||||
|
||||
// Resolve a given game path according to this collection.
|
||||
public FullPath? ResolvePath( Utf8GamePath gameResourcePath )
|
||||
{
|
||||
if( !ResolvedFiles.TryGetValue( gameResourcePath, out var candidate ) )
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
if( candidate.Path.InternalName.Length > Utf8GamePath.MaxGamePathLength
|
||||
|| candidate.Path.IsRooted && !candidate.Path.Exists )
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
return candidate.Path;
|
||||
}
|
||||
|
||||
// For a given full path, find all game paths that currently use this file.
|
||||
public IEnumerable< Utf8GamePath > ReverseResolvePath( FullPath localFilePath )
|
||||
{
|
||||
var needle = localFilePath.FullName.ToLower();
|
||||
if( localFilePath.IsRooted )
|
||||
{
|
||||
needle = needle.Replace( '/', '\\' );
|
||||
}
|
||||
|
||||
var iterator = ResolvedFiles
|
||||
.Where( f => string.Equals( f.Value.Path.FullName, needle, StringComparison.OrdinalIgnoreCase ) )
|
||||
.Select( kvp => kvp.Key );
|
||||
|
||||
// For files that are not rooted, try to add themselves.
|
||||
if( !localFilePath.IsRooted && Utf8GamePath.FromString( localFilePath.FullName, out var utf8 ) )
|
||||
{
|
||||
iterator = iterator.Prepend( utf8 );
|
||||
}
|
||||
|
||||
return iterator;
|
||||
}
|
||||
|
||||
// Reverse resolve multiple paths at once for efficiency.
|
||||
public HashSet< Utf8GamePath >[] ReverseResolvePaths( IReadOnlyCollection< string > fullPaths )
|
||||
{
|
||||
if( fullPaths.Count == 0 )
|
||||
return Array.Empty< HashSet< Utf8GamePath > >();
|
||||
|
||||
var ret = new HashSet< Utf8GamePath >[fullPaths.Count];
|
||||
var dict = new Dictionary< FullPath, int >( fullPaths.Count );
|
||||
foreach( var (path, idx) in fullPaths.WithIndex() )
|
||||
{
|
||||
dict[ new FullPath(path) ] = idx;
|
||||
ret[ idx ] = !Path.IsPathRooted( path ) && Utf8GamePath.FromString( path, out var utf8 )
|
||||
? new HashSet< Utf8GamePath > { utf8 }
|
||||
: new HashSet< Utf8GamePath >();
|
||||
}
|
||||
|
||||
foreach( var (game, full) in ResolvedFiles )
|
||||
{
|
||||
if( dict.TryGetValue( full.Path, out var idx ) )
|
||||
{
|
||||
ret[ idx ].Add( game );
|
||||
}
|
||||
}
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
public void FullRecalculation(bool isDefault)
|
||||
{
|
||||
ResolvedFiles.Clear();
|
||||
MetaManipulations.Reset();
|
||||
_conflicts.Clear();
|
||||
|
||||
// Add all forced redirects.
|
||||
foreach( var tempMod in Penumbra.TempMods.ModsForAllCollections.Concat(
|
||||
Penumbra.TempMods.Mods.TryGetValue( _collection, out var list ) ? list : Array.Empty< TemporaryMod >() ) )
|
||||
{
|
||||
AddMod( tempMod, false );
|
||||
}
|
||||
|
||||
foreach( var mod in Penumbra.ModManager )
|
||||
{
|
||||
AddMod( mod, false );
|
||||
}
|
||||
|
||||
AddMetaFiles();
|
||||
|
||||
++_collection.ChangeCounter;
|
||||
|
||||
if( isDefault && Penumbra.CharacterUtility.Ready && Penumbra.Config.EnableMods )
|
||||
{
|
||||
Penumbra.ResidentResources.Reload();
|
||||
MetaManipulations.SetFiles();
|
||||
}
|
||||
}
|
||||
|
||||
public void ReloadMod( IMod mod, bool addMetaChanges )
|
||||
{
|
||||
RemoveMod( mod, addMetaChanges );
|
||||
AddMod( mod, addMetaChanges );
|
||||
}
|
||||
|
||||
public void RemoveMod( IMod mod, bool addMetaChanges )
|
||||
{
|
||||
var conflicts = Conflicts( mod );
|
||||
|
||||
foreach( var (path, _) in mod.AllSubMods.SelectMany( s => s.Files.Concat( s.FileSwaps ) ) )
|
||||
{
|
||||
if( !ResolvedFiles.TryGetValue( path, out var modPath ) )
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if( modPath.Mod == mod )
|
||||
{
|
||||
ResolvedFiles.Remove( path );
|
||||
}
|
||||
}
|
||||
|
||||
foreach( var manipulation in mod.AllSubMods.SelectMany( s => s.Manipulations ) )
|
||||
{
|
||||
if( MetaManipulations.TryGetValue( manipulation, out var registeredMod ) && registeredMod == mod )
|
||||
{
|
||||
MetaManipulations.RevertMod( manipulation );
|
||||
}
|
||||
}
|
||||
|
||||
_conflicts.Remove( mod );
|
||||
foreach( var conflict in conflicts )
|
||||
{
|
||||
if( conflict.HasPriority )
|
||||
{
|
||||
ReloadMod( conflict.Mod2, false );
|
||||
}
|
||||
else
|
||||
{
|
||||
var newConflicts = Conflicts( conflict.Mod2 ).Remove( c => c.Mod2 == mod );
|
||||
if( newConflicts.Count > 0 )
|
||||
{
|
||||
_conflicts[ conflict.Mod2 ] = newConflicts;
|
||||
}
|
||||
else
|
||||
{
|
||||
_conflicts.Remove( conflict.Mod2 );
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if( addMetaChanges )
|
||||
{
|
||||
++_collection.ChangeCounter;
|
||||
if( _collection == Penumbra.CollectionManager.Active.Default && Penumbra.CharacterUtility.Ready && Penumbra.Config.EnableMods )
|
||||
{
|
||||
Penumbra.ResidentResources.Reload();
|
||||
MetaManipulations.SetFiles();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Add all files and possibly manipulations of a given mod according to its settings in this collection.
|
||||
public void AddMod( IMod mod, bool addMetaChanges )
|
||||
{
|
||||
if( mod.Index >= 0 )
|
||||
{
|
||||
var settings = _collection[ mod.Index ].Settings;
|
||||
if( settings is not { Enabled: true } )
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
foreach( var (group, groupIndex) in mod.Groups.WithIndex().OrderByDescending( g => g.Item1.Priority ) )
|
||||
{
|
||||
if( group.Count == 0 )
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var config = settings.Settings[ groupIndex ];
|
||||
switch( group.Type )
|
||||
{
|
||||
case GroupType.Single:
|
||||
AddSubMod( group[ ( int )config ], mod );
|
||||
break;
|
||||
case GroupType.Multi:
|
||||
{
|
||||
foreach( var (option, _) in group.WithIndex()
|
||||
.Where( p => ( ( 1 << p.Item2 ) & config ) != 0 )
|
||||
.OrderByDescending( p => group.OptionPriority( p.Item2 ) ) )
|
||||
{
|
||||
AddSubMod( option, mod );
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
AddSubMod( mod.Default, mod );
|
||||
|
||||
if( addMetaChanges )
|
||||
{
|
||||
++_collection.ChangeCounter;
|
||||
if(Penumbra.ModCaches[mod.Index].TotalManipulations > 0 )
|
||||
{
|
||||
AddMetaFiles();
|
||||
}
|
||||
|
||||
if( _collection == Penumbra.CollectionManager.Active.Default && Penumbra.CharacterUtility.Ready && Penumbra.Config.EnableMods )
|
||||
{
|
||||
Penumbra.ResidentResources.Reload();
|
||||
MetaManipulations.SetFiles();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Add all files and possibly manipulations of a specific submod
|
||||
private void AddSubMod( ISubMod subMod, IMod parentMod )
|
||||
{
|
||||
foreach( var (path, file) in subMod.Files.Concat( subMod.FileSwaps ) )
|
||||
{
|
||||
AddFile( path, file, parentMod );
|
||||
}
|
||||
|
||||
foreach( var manip in subMod.Manipulations )
|
||||
{
|
||||
AddManipulation( manip, parentMod );
|
||||
}
|
||||
}
|
||||
|
||||
// Add a specific file redirection, handling potential conflicts.
|
||||
// For different mods, higher mod priority takes precedence before option group priority,
|
||||
// which takes precedence before option priority, which takes precedence before ordering.
|
||||
// Inside the same mod, conflicts are not recorded.
|
||||
private void AddFile( Utf8GamePath path, FullPath file, IMod mod )
|
||||
{
|
||||
if( !ModCollection.CheckFullPath( path, file ) )
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if( ResolvedFiles.TryAdd( path, new ModPath( mod, file ) ) )
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var modPath = ResolvedFiles[ path ];
|
||||
// Lower prioritized option in the same mod.
|
||||
if( mod == modPath.Mod )
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if( AddConflict( path, mod, modPath.Mod ) )
|
||||
{
|
||||
ResolvedFiles[ path ] = new ModPath( mod, file );
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Remove all empty conflict sets for a given mod with the given conflicts.
|
||||
// If transitive is true, also removes the corresponding version of the other mod.
|
||||
private void RemoveEmptyConflicts( IMod mod, SingleArray< ModConflicts > oldConflicts, bool transitive )
|
||||
{
|
||||
var changedConflicts = oldConflicts.Remove( c =>
|
||||
{
|
||||
if( c.Conflicts.Count == 0 )
|
||||
{
|
||||
if( transitive )
|
||||
{
|
||||
RemoveEmptyConflicts( c.Mod2, Conflicts( c.Mod2 ), false );
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
} );
|
||||
if( changedConflicts.Count == 0 )
|
||||
{
|
||||
_conflicts.Remove( mod );
|
||||
}
|
||||
else
|
||||
{
|
||||
_conflicts[ mod ] = changedConflicts;
|
||||
}
|
||||
}
|
||||
|
||||
// Add a new conflict between the added mod and the existing mod.
|
||||
// Update all other existing conflicts between the existing mod and other mods if necessary.
|
||||
// Returns if the added mod takes priority before the existing mod.
|
||||
private bool AddConflict( object data, IMod addedMod, IMod existingMod )
|
||||
{
|
||||
var addedPriority = addedMod.Index >= 0 ? _collection[ addedMod.Index ].Settings!.Priority : addedMod.Priority;
|
||||
var existingPriority = existingMod.Index >= 0 ? _collection[ existingMod.Index ].Settings!.Priority : existingMod.Priority;
|
||||
|
||||
if( existingPriority < addedPriority )
|
||||
{
|
||||
var tmpConflicts = Conflicts( existingMod );
|
||||
foreach( var conflict in tmpConflicts )
|
||||
{
|
||||
if( data is Utf8GamePath path && conflict.Conflicts.RemoveAll( p => p is Utf8GamePath x && x.Equals( path ) ) > 0
|
||||
|| data is MetaManipulation meta && conflict.Conflicts.RemoveAll( m => m is MetaManipulation x && x.Equals( meta ) ) > 0 )
|
||||
{
|
||||
AddConflict( data, addedMod, conflict.Mod2 );
|
||||
}
|
||||
}
|
||||
|
||||
RemoveEmptyConflicts( existingMod, tmpConflicts, true );
|
||||
}
|
||||
|
||||
var addedConflicts = Conflicts( addedMod );
|
||||
var existingConflicts = Conflicts( existingMod );
|
||||
if( addedConflicts.FindFirst( c => c.Mod2 == existingMod, out var oldConflicts ) )
|
||||
{
|
||||
// Only need to change one list since both conflict lists refer to the same list.
|
||||
oldConflicts.Conflicts.Add( data );
|
||||
}
|
||||
else
|
||||
{
|
||||
// Add the same conflict list to both conflict directions.
|
||||
var conflictList = new List< object > { data };
|
||||
_conflicts[ addedMod ] = addedConflicts.Append( new ModConflicts( existingMod, conflictList, existingPriority < addedPriority,
|
||||
existingPriority != addedPriority ) );
|
||||
_conflicts[ existingMod ] = existingConflicts.Append( new ModConflicts( addedMod, conflictList,
|
||||
existingPriority >= addedPriority,
|
||||
existingPriority != addedPriority ) );
|
||||
}
|
||||
|
||||
return existingPriority < addedPriority;
|
||||
}
|
||||
|
||||
// Add a specific manipulation, handling potential conflicts.
|
||||
// For different mods, higher mod priority takes precedence before option group priority,
|
||||
// which takes precedence before option priority, which takes precedence before ordering.
|
||||
// Inside the same mod, conflicts are not recorded.
|
||||
private void AddManipulation( MetaManipulation manip, IMod mod )
|
||||
{
|
||||
if( !MetaManipulations.TryGetValue( manip, out var existingMod ) )
|
||||
{
|
||||
MetaManipulations.ApplyMod( manip, mod );
|
||||
return;
|
||||
}
|
||||
|
||||
// Lower prioritized option in the same mod.
|
||||
if( mod == existingMod )
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if( AddConflict( manip, mod, existingMod ) )
|
||||
{
|
||||
MetaManipulations.ApplyMod( manip, mod );
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Add all necessary meta file redirects.
|
||||
private void AddMetaFiles()
|
||||
=> MetaManipulations.SetImcFiles();
|
||||
|
||||
// Increment the counter to ensure new files are loaded after applying meta changes.
|
||||
private void IncrementCounter()
|
||||
{
|
||||
++_collection.ChangeCounter;
|
||||
Penumbra.CharacterUtility.LoadingFinished -= IncrementCounter;
|
||||
}
|
||||
|
||||
|
||||
// Identify and record all manipulated objects for this entire collection.
|
||||
private void SetChangedItems()
|
||||
{
|
||||
if( _changedItemsSaveCounter == _collection.ChangeCounter )
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
_changedItemsSaveCounter = _collection.ChangeCounter;
|
||||
_changedItems.Clear();
|
||||
// Skip IMCs because they would result in far too many false-positive items,
|
||||
// since they are per set instead of per item-slot/item/variant.
|
||||
var identifier = Penumbra.Identifier;
|
||||
var items = new SortedList< string, object? >( 512 );
|
||||
|
||||
void AddItems( IMod mod )
|
||||
{
|
||||
foreach( var (name, obj) in items )
|
||||
{
|
||||
if( !_changedItems.TryGetValue( name, out var data ) )
|
||||
{
|
||||
_changedItems.Add( name, ( new SingleArray< IMod >( mod ), obj ) );
|
||||
}
|
||||
else if( !data.Item1.Contains( mod ) )
|
||||
{
|
||||
_changedItems[ name ] = ( data.Item1.Append( mod ), obj is int x && data.Item2 is int y ? x + y : obj );
|
||||
}
|
||||
else if( obj is int x && data.Item2 is int y )
|
||||
{
|
||||
_changedItems[ name ] = ( data.Item1, x + y );
|
||||
}
|
||||
}
|
||||
|
||||
items.Clear();
|
||||
}
|
||||
|
||||
foreach( var (resolved, modPath) in ResolvedFiles.Where( file => !file.Key.Path.EndsWith( "imc"u8 ) ) )
|
||||
{
|
||||
identifier.Identify( items, resolved.ToString() );
|
||||
AddItems( modPath.Mod );
|
||||
}
|
||||
|
||||
foreach( var (manip, mod) in MetaManipulations )
|
||||
{
|
||||
ModCacheManager.ComputeChangedItems(identifier, items, manip );
|
||||
AddItems( mod );
|
||||
}
|
||||
}
|
||||
catch( Exception e )
|
||||
{
|
||||
Penumbra.Log.Error( $"Unknown Error:\n{e}" );
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -5,7 +5,8 @@ using System.Diagnostics;
|
|||
using System.Linq;
|
||||
using Penumbra.Mods.Manager;
|
||||
using Penumbra.Util;
|
||||
|
||||
using Penumbra.Collections.Manager;
|
||||
|
||||
namespace Penumbra.Collections;
|
||||
|
||||
/// <summary>
|
||||
|
|
|
|||
|
|
@ -16,11 +16,9 @@ public readonly struct ResolveData
|
|||
public bool Valid
|
||||
=> _modCollection != null;
|
||||
|
||||
public ResolveData()
|
||||
{
|
||||
_modCollection = null!;
|
||||
AssociatedGameObject = nint.Zero;
|
||||
}
|
||||
public ResolveData()
|
||||
: this(null!, nint.Zero)
|
||||
{ }
|
||||
|
||||
public ResolveData(ModCollection collection, nint gameObject)
|
||||
{
|
||||
|
|
|
|||
|
|
@ -22,7 +22,8 @@ using Penumbra.Util;
|
|||
using CharacterUtility = Penumbra.Interop.Services.CharacterUtility;
|
||||
using ModFileSystemSelector = Penumbra.UI.ModsTab.ModFileSystemSelector;
|
||||
using Penumbra.Mods.Manager;
|
||||
|
||||
using Penumbra.Collections.Cache;
|
||||
|
||||
namespace Penumbra;
|
||||
|
||||
public class PenumbraNew
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@ using OtterGui.Classes;
|
|||
using OtterGui.Raii;
|
||||
using OtterGui.Widgets;
|
||||
using Penumbra.Collections;
|
||||
using Penumbra.Collections.Cache;
|
||||
using Penumbra.Collections.Manager;
|
||||
using Penumbra.Meta.Manipulations;
|
||||
using Penumbra.Mods;
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue