mirror of
https://github.com/xivdev/Penumbra.git
synced 2025-12-12 18:27:24 +01:00
Move Mod.Manager and ModCollection.Manager to outer scope and required changes.
This commit is contained in:
parent
ccdafcf85d
commit
1253079968
59 changed files with 2562 additions and 2615 deletions
|
|
@ -38,7 +38,7 @@ public class IpcTester : IDisposable
|
|||
private readonly ModSettings _modSettings;
|
||||
private readonly Temporary _temporary;
|
||||
|
||||
public IpcTester(DalamudPluginInterface pi, PenumbraIpcProviders ipcProviders, Mod.Manager modManager)
|
||||
public IpcTester(DalamudPluginInterface pi, PenumbraIpcProviders ipcProviders, ModManager modManager)
|
||||
{
|
||||
_ipcProviders = ipcProviders;
|
||||
_pluginState = new PluginState(pi);
|
||||
|
|
@ -1139,9 +1139,9 @@ public class IpcTester : IDisposable
|
|||
private class Temporary
|
||||
{
|
||||
private readonly DalamudPluginInterface _pi;
|
||||
private readonly Mod.Manager _modManager;
|
||||
private readonly ModManager _modManager;
|
||||
|
||||
public Temporary(DalamudPluginInterface pi, Mod.Manager modManager)
|
||||
public Temporary(DalamudPluginInterface pi, ModManager modManager)
|
||||
{
|
||||
_pi = pi;
|
||||
_modManager = modManager;
|
||||
|
|
|
|||
|
|
@ -93,10 +93,10 @@ public class PenumbraApi : IDisposable, IPenumbraApi
|
|||
private Penumbra _penumbra;
|
||||
private Lumina.GameData? _lumina;
|
||||
|
||||
private Mod.Manager _modManager;
|
||||
private ModManager _modManager;
|
||||
private ResourceLoader _resourceLoader;
|
||||
private Configuration _config;
|
||||
private ModCollection.Manager _collectionManager;
|
||||
private CollectionManager _collectionManager;
|
||||
private DalamudServices _dalamud;
|
||||
private TempCollectionManager _tempCollections;
|
||||
private TempModManager _tempMods;
|
||||
|
|
@ -104,8 +104,8 @@ public class PenumbraApi : IDisposable, IPenumbraApi
|
|||
private CollectionResolver _collectionResolver;
|
||||
private CutsceneService _cutsceneService;
|
||||
|
||||
public unsafe PenumbraApi(CommunicatorService communicator, Penumbra penumbra, Mod.Manager modManager, ResourceLoader resourceLoader,
|
||||
Configuration config, ModCollection.Manager collectionManager, DalamudServices dalamud, TempCollectionManager tempCollections,
|
||||
public unsafe PenumbraApi(CommunicatorService communicator, Penumbra penumbra, ModManager modManager, ResourceLoader resourceLoader,
|
||||
Configuration config, CollectionManager collectionManager, DalamudServices dalamud, TempCollectionManager tempCollections,
|
||||
TempModManager tempMods, ActorService actors, CollectionResolver collectionResolver, CutsceneService cutsceneService)
|
||||
{
|
||||
_communicator = communicator;
|
||||
|
|
@ -1021,7 +1021,7 @@ public class PenumbraApi : IDisposable, IPenumbraApi
|
|||
|
||||
// Resolve a path given by string for a specific collection.
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)]
|
||||
private string ResolvePath(string path, Mod.Manager _, ModCollection collection)
|
||||
private string ResolvePath(string path, ModManager _, ModCollection collection)
|
||||
{
|
||||
if (!_config.EnableMods)
|
||||
return path;
|
||||
|
|
|
|||
|
|
@ -112,7 +112,7 @@ public class PenumbraIpcProviders : IDisposable
|
|||
internal readonly FuncProvider< string, int, PenumbraApiEc > RemoveTemporaryModAll;
|
||||
internal readonly FuncProvider< string, string, int, PenumbraApiEc > RemoveTemporaryMod;
|
||||
|
||||
public PenumbraIpcProviders( DalamudPluginInterface pi, IPenumbraApi api, Mod.Manager modManager )
|
||||
public PenumbraIpcProviders( DalamudPluginInterface pi, IPenumbraApi api, ModManager modManager )
|
||||
{
|
||||
Api = api;
|
||||
|
||||
|
|
|
|||
|
|
@ -1,422 +1,419 @@
|
|||
using Newtonsoft.Json;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using OtterGui;
|
||||
using Penumbra.Mods;
|
||||
using Penumbra.UI;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using Dalamud.Interface.Internal.Notifications;
|
||||
using Penumbra.GameData.Actors;
|
||||
using Penumbra.Services;
|
||||
using Penumbra.Util;
|
||||
|
||||
namespace Penumbra.Collections;
|
||||
|
||||
public partial class ModCollection
|
||||
{
|
||||
public sealed partial class Manager : ISavable
|
||||
{
|
||||
public const int Version = 1;
|
||||
|
||||
// The collection currently selected for changing settings.
|
||||
public ModCollection Current { get; private set; } = Empty;
|
||||
|
||||
// The collection currently selected is in use either as an active collection or through inheritance.
|
||||
public bool CurrentCollectionInUse { get; private set; }
|
||||
|
||||
// The collection used for general file redirections and all characters not specifically named.
|
||||
public ModCollection Default { get; private set; } = Empty;
|
||||
|
||||
// The collection used for all files categorized as UI files.
|
||||
public ModCollection Interface { get; private set; } = Empty;
|
||||
|
||||
// A single collection that can not be deleted as a fallback for the current collection.
|
||||
private ModCollection DefaultName { get; set; } = Empty;
|
||||
|
||||
// The list of character collections.
|
||||
public readonly IndividualCollections Individuals;
|
||||
|
||||
public ModCollection Individual(ActorIdentifier identifier)
|
||||
=> Individuals.TryGetCollection(identifier, out var c) ? c : Default;
|
||||
|
||||
// Special Collections
|
||||
private readonly ModCollection?[] _specialCollections = new ModCollection?[Enum.GetValues<Api.Enums.ApiCollectionType>().Length - 3];
|
||||
|
||||
// Return the configured collection for the given type or null.
|
||||
// Does not handle Inactive, use ByName instead.
|
||||
public ModCollection? ByType(CollectionType type)
|
||||
=> ByType(type, ActorIdentifier.Invalid);
|
||||
|
||||
public ModCollection? ByType(CollectionType type, ActorIdentifier identifier)
|
||||
{
|
||||
if (type.IsSpecial())
|
||||
return _specialCollections[(int)type];
|
||||
|
||||
return type switch
|
||||
{
|
||||
CollectionType.Default => Default,
|
||||
CollectionType.Interface => Interface,
|
||||
CollectionType.Current => Current,
|
||||
CollectionType.Individual => identifier.IsValid && Individuals.Individuals.TryGetValue(identifier, out var c) ? c : null,
|
||||
_ => null,
|
||||
};
|
||||
}
|
||||
|
||||
// Set a active collection, can be used to set Default, Current, Interface, Special, or Individual collections.
|
||||
private void SetCollection(int newIdx, CollectionType collectionType, int individualIndex = -1)
|
||||
{
|
||||
var oldCollectionIdx = collectionType switch
|
||||
{
|
||||
CollectionType.Default => Default.Index,
|
||||
CollectionType.Interface => Interface.Index,
|
||||
CollectionType.Current => Current.Index,
|
||||
CollectionType.Individual => individualIndex < 0 || individualIndex >= Individuals.Count
|
||||
? -1
|
||||
: Individuals[individualIndex].Collection.Index,
|
||||
_ when collectionType.IsSpecial() => _specialCollections[(int)collectionType]?.Index ?? Default.Index,
|
||||
_ => -1,
|
||||
};
|
||||
|
||||
if (oldCollectionIdx == -1 || newIdx == oldCollectionIdx)
|
||||
return;
|
||||
|
||||
var newCollection = this[newIdx];
|
||||
if (newIdx > Empty.Index)
|
||||
newCollection.CreateCache(collectionType is CollectionType.Default);
|
||||
|
||||
switch (collectionType)
|
||||
{
|
||||
case CollectionType.Default:
|
||||
Default = newCollection;
|
||||
break;
|
||||
case CollectionType.Interface:
|
||||
Interface = newCollection;
|
||||
break;
|
||||
case CollectionType.Current:
|
||||
Current = newCollection;
|
||||
break;
|
||||
case CollectionType.Individual:
|
||||
if (!Individuals.ChangeCollection(individualIndex, newCollection))
|
||||
{
|
||||
RemoveCache(newIdx);
|
||||
return;
|
||||
}
|
||||
|
||||
break;
|
||||
default:
|
||||
_specialCollections[(int)collectionType] = newCollection;
|
||||
break;
|
||||
}
|
||||
|
||||
RemoveCache(oldCollectionIdx);
|
||||
|
||||
UpdateCurrentCollectionInUse();
|
||||
_communicator.CollectionChange.Invoke(collectionType, this[oldCollectionIdx], newCollection,
|
||||
collectionType == CollectionType.Individual ? Individuals[individualIndex].DisplayName : string.Empty);
|
||||
}
|
||||
|
||||
private void UpdateCurrentCollectionInUse()
|
||||
=> CurrentCollectionInUse = _specialCollections
|
||||
.OfType<ModCollection>()
|
||||
.Prepend(Interface)
|
||||
.Prepend(Default)
|
||||
.Concat(Individuals.Assignments.Select(kvp => kvp.Collection))
|
||||
.SelectMany(c => c.GetFlattenedInheritance()).Contains(Current);
|
||||
|
||||
public void SetCollection(ModCollection collection, CollectionType collectionType, int individualIndex = -1)
|
||||
=> SetCollection(collection.Index, collectionType, individualIndex);
|
||||
|
||||
// Create a special collection if it does not exist and set it to Empty.
|
||||
public bool CreateSpecialCollection(CollectionType collectionType)
|
||||
{
|
||||
if (!collectionType.IsSpecial() || _specialCollections[(int)collectionType] != null)
|
||||
return false;
|
||||
|
||||
_specialCollections[(int)collectionType] = Default;
|
||||
_communicator.CollectionChange.Invoke(collectionType, null, Default, string.Empty);
|
||||
return true;
|
||||
}
|
||||
|
||||
// Remove a special collection if it exists
|
||||
public void RemoveSpecialCollection(CollectionType collectionType)
|
||||
{
|
||||
if (!collectionType.IsSpecial())
|
||||
return;
|
||||
|
||||
var old = _specialCollections[(int)collectionType];
|
||||
if (old != null)
|
||||
{
|
||||
_specialCollections[(int)collectionType] = null;
|
||||
_communicator.CollectionChange.Invoke(collectionType, old, null, string.Empty);
|
||||
}
|
||||
}
|
||||
|
||||
// Wrappers around Individual Collection handling.
|
||||
public void CreateIndividualCollection(params ActorIdentifier[] identifiers)
|
||||
{
|
||||
if (Individuals.Add(identifiers, Default))
|
||||
_communicator.CollectionChange.Invoke(CollectionType.Individual, null, Default, Individuals.Last().DisplayName);
|
||||
}
|
||||
|
||||
public void RemoveIndividualCollection(int individualIndex)
|
||||
{
|
||||
if (individualIndex < 0 || individualIndex >= Individuals.Count)
|
||||
return;
|
||||
|
||||
var (name, old) = Individuals[individualIndex];
|
||||
if (Individuals.Delete(individualIndex))
|
||||
_communicator.CollectionChange.Invoke(CollectionType.Individual, old, null, name);
|
||||
}
|
||||
|
||||
public void MoveIndividualCollection(int from, int to)
|
||||
{
|
||||
if (Individuals.Move(from, to))
|
||||
Penumbra.SaveService.QueueSave(this);
|
||||
}
|
||||
|
||||
// Obtain the index of a collection by name.
|
||||
private int GetIndexForCollectionName(string name)
|
||||
=> name.Length == 0 ? Empty.Index : _collections.IndexOf(c => c.Name == name);
|
||||
|
||||
// Load default, current, special, and character collections from config.
|
||||
// Then create caches. If a collection does not exist anymore, reset it to an appropriate default.
|
||||
private void LoadCollections(FilenameService files)
|
||||
{
|
||||
var configChanged = !ReadActiveCollections(files, out var jObject);
|
||||
|
||||
// Load the default collection.
|
||||
var defaultName = jObject[nameof(Default)]?.ToObject<string>() ?? (configChanged ? DefaultCollection : Empty.Name);
|
||||
var defaultIdx = GetIndexForCollectionName(defaultName);
|
||||
if (defaultIdx < 0)
|
||||
{
|
||||
Penumbra.ChatService.NotificationMessage(
|
||||
$"Last choice of {TutorialService.DefaultCollection} {defaultName} is not available, reset to {Empty.Name}.", "Load Failure",
|
||||
NotificationType.Warning);
|
||||
Default = Empty;
|
||||
configChanged = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
Default = this[defaultIdx];
|
||||
}
|
||||
|
||||
// Load the interface collection.
|
||||
var interfaceName = jObject[nameof(Interface)]?.ToObject<string>() ?? Default.Name;
|
||||
var interfaceIdx = GetIndexForCollectionName(interfaceName);
|
||||
if (interfaceIdx < 0)
|
||||
{
|
||||
Penumbra.ChatService.NotificationMessage(
|
||||
$"Last choice of {TutorialService.InterfaceCollection} {interfaceName} is not available, reset to {Empty.Name}.",
|
||||
"Load Failure", NotificationType.Warning);
|
||||
Interface = Empty;
|
||||
configChanged = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
Interface = this[interfaceIdx];
|
||||
}
|
||||
|
||||
// Load the current collection.
|
||||
var currentName = jObject[nameof(Current)]?.ToObject<string>() ?? DefaultCollection;
|
||||
var currentIdx = GetIndexForCollectionName(currentName);
|
||||
if (currentIdx < 0)
|
||||
{
|
||||
Penumbra.ChatService.NotificationMessage(
|
||||
$"Last choice of {TutorialService.SelectedCollection} {currentName} is not available, reset to {DefaultCollection}.",
|
||||
"Load Failure", NotificationType.Warning);
|
||||
Current = DefaultName;
|
||||
configChanged = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
Current = this[currentIdx];
|
||||
}
|
||||
|
||||
// Load special collections.
|
||||
foreach (var (type, name, _) in CollectionTypeExtensions.Special)
|
||||
{
|
||||
var typeName = jObject[type.ToString()]?.ToObject<string>();
|
||||
if (typeName != null)
|
||||
{
|
||||
var idx = GetIndexForCollectionName(typeName);
|
||||
if (idx < 0)
|
||||
{
|
||||
Penumbra.ChatService.NotificationMessage($"Last choice of {name} Collection {typeName} is not available, removed.",
|
||||
"Load Failure",
|
||||
NotificationType.Warning);
|
||||
configChanged = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
_specialCollections[(int)type] = this[idx];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
configChanged |= MigrateIndividualCollections(jObject);
|
||||
configChanged |= Individuals.ReadJObject(jObject[nameof(Individuals)] as JArray, this);
|
||||
|
||||
// Save any changes and create all required caches.
|
||||
if (configChanged)
|
||||
Penumbra.SaveService.ImmediateSave(this);
|
||||
}
|
||||
|
||||
// Migrate ungendered collections to Male and Female for 0.5.9.0.
|
||||
public static void MigrateUngenderedCollections(FilenameService fileNames)
|
||||
{
|
||||
if (!ReadActiveCollections(fileNames, out var jObject))
|
||||
return;
|
||||
|
||||
foreach (var (type, _, _) in CollectionTypeExtensions.Special.Where(t => t.Item2.StartsWith("Male ")))
|
||||
{
|
||||
var oldName = type.ToString()[4..];
|
||||
var value = jObject[oldName];
|
||||
if (value == null)
|
||||
continue;
|
||||
|
||||
jObject.Remove(oldName);
|
||||
jObject.Add("Male" + oldName, value);
|
||||
jObject.Add("Female" + oldName, value);
|
||||
}
|
||||
|
||||
using var stream = File.Open(fileNames.ActiveCollectionsFile, FileMode.Truncate);
|
||||
using var writer = new StreamWriter(stream);
|
||||
using var j = new JsonTextWriter(writer);
|
||||
j.Formatting = Formatting.Indented;
|
||||
jObject.WriteTo(j);
|
||||
}
|
||||
|
||||
// Migrate individual collections to Identifiers for 0.6.0.
|
||||
private bool MigrateIndividualCollections(JObject jObject)
|
||||
{
|
||||
var version = jObject[nameof(Version)]?.Value<int>() ?? 0;
|
||||
if (version > 0)
|
||||
return false;
|
||||
|
||||
// Load character collections. If a player name comes up multiple times, the last one is applied.
|
||||
var characters = jObject["Characters"]?.ToObject<Dictionary<string, string>>() ?? new Dictionary<string, string>();
|
||||
var dict = new Dictionary<string, ModCollection>(characters.Count);
|
||||
foreach (var (player, collectionName) in characters)
|
||||
{
|
||||
var idx = GetIndexForCollectionName(collectionName);
|
||||
if (idx < 0)
|
||||
{
|
||||
Penumbra.ChatService.NotificationMessage(
|
||||
$"Last choice of <{player}>'s Collection {collectionName} is not available, reset to {Empty.Name}.", "Load Failure",
|
||||
NotificationType.Warning);
|
||||
dict.Add(player, Empty);
|
||||
}
|
||||
else
|
||||
{
|
||||
dict.Add(player, this[idx]);
|
||||
}
|
||||
}
|
||||
|
||||
Individuals.Migrate0To1(dict);
|
||||
return true;
|
||||
}
|
||||
|
||||
// Read the active collection file into a jObject.
|
||||
// Returns true if this is successful, false if the file does not exist or it is unsuccessful.
|
||||
private static bool ReadActiveCollections(FilenameService files, out JObject ret)
|
||||
{
|
||||
var file = files.ActiveCollectionsFile;
|
||||
if (File.Exists(file))
|
||||
try
|
||||
{
|
||||
ret = JObject.Parse(File.ReadAllText(file));
|
||||
return true;
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Penumbra.Log.Error($"Could not read active collections from file {file}:\n{e}");
|
||||
}
|
||||
|
||||
ret = new JObject();
|
||||
return false;
|
||||
}
|
||||
|
||||
// Save if any of the active collections is changed.
|
||||
private void SaveOnChange(CollectionType collectionType, ModCollection? _1, ModCollection? _2, string _3)
|
||||
{
|
||||
if (collectionType is not CollectionType.Inactive and not CollectionType.Temporary)
|
||||
Penumbra.SaveService.QueueSave(this);
|
||||
}
|
||||
|
||||
// Cache handling. Usually recreate caches on the next framework tick,
|
||||
// but at launch create all of them at once.
|
||||
public void CreateNecessaryCaches()
|
||||
{
|
||||
var tasks = _specialCollections.OfType<ModCollection>()
|
||||
.Concat(Individuals.Select(p => p.Collection))
|
||||
.Prepend(Current)
|
||||
.Prepend(Default)
|
||||
.Prepend(Interface)
|
||||
.Distinct()
|
||||
.Select(c => Task.Run(() => c.CalculateEffectiveFileListInternal(c == Default)))
|
||||
.ToArray();
|
||||
|
||||
Task.WaitAll(tasks);
|
||||
}
|
||||
|
||||
private void RemoveCache(int idx)
|
||||
{
|
||||
if (idx != Empty.Index
|
||||
&& idx != Default.Index
|
||||
&& idx != Interface.Index
|
||||
&& idx != Current.Index
|
||||
&& _specialCollections.All(c => c == null || c.Index != idx)
|
||||
&& Individuals.Select(p => p.Collection).All(c => c.Index != idx))
|
||||
_collections[idx].ClearCache();
|
||||
}
|
||||
|
||||
// Recalculate effective files for active collections on events.
|
||||
private void OnModAddedActive(Mod mod)
|
||||
{
|
||||
foreach (var collection in this.Where(c => c.HasCache && c[mod.Index].Settings?.Enabled == true))
|
||||
collection._cache!.AddMod(mod, true);
|
||||
}
|
||||
|
||||
private void OnModRemovedActive(Mod mod)
|
||||
{
|
||||
foreach (var collection in this.Where(c => c.HasCache && c[mod.Index].Settings?.Enabled == true))
|
||||
collection._cache!.RemoveMod(mod, true);
|
||||
}
|
||||
|
||||
private void OnModMovedActive(Mod mod)
|
||||
{
|
||||
foreach (var collection in this.Where(c => c.HasCache && c[mod.Index].Settings?.Enabled == true))
|
||||
collection._cache!.ReloadMod(mod, true);
|
||||
}
|
||||
|
||||
public string ToFilename(FilenameService fileNames)
|
||||
=> fileNames.ActiveCollectionsFile;
|
||||
|
||||
public string TypeName
|
||||
=> "Active Collections";
|
||||
|
||||
public string LogName(string _)
|
||||
=> "to file";
|
||||
|
||||
public void Save(StreamWriter writer)
|
||||
{
|
||||
var jObj = new JObject
|
||||
{
|
||||
{ nameof(Version), Version },
|
||||
{ nameof(Default), Default.Name },
|
||||
{ nameof(Interface), Interface.Name },
|
||||
{ nameof(Current), Current.Name },
|
||||
};
|
||||
foreach (var (type, collection) in _specialCollections.WithIndex().Where(p => p.Value != null)
|
||||
.Select(p => ((CollectionType)p.Index, p.Value!)))
|
||||
jObj.Add(type.ToString(), collection.Name);
|
||||
|
||||
jObj.Add(nameof(Individuals), Individuals.ToJObject());
|
||||
using var j = new JsonTextWriter(writer) { Formatting = Formatting.Indented };
|
||||
jObj.WriteTo(j);
|
||||
}
|
||||
}
|
||||
}
|
||||
using Newtonsoft.Json;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using OtterGui;
|
||||
using Penumbra.Mods;
|
||||
using Penumbra.UI;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using Dalamud.Interface.Internal.Notifications;
|
||||
using Penumbra.GameData.Actors;
|
||||
using Penumbra.Services;
|
||||
using Penumbra.Util;
|
||||
|
||||
namespace Penumbra.Collections;
|
||||
|
||||
public sealed partial class CollectionManager : ISavable
|
||||
{
|
||||
public const int Version = 1;
|
||||
|
||||
// The collection currently selected for changing settings.
|
||||
public ModCollection Current { get; private set; } = ModCollection.Empty;
|
||||
|
||||
// The collection currently selected is in use either as an active collection or through inheritance.
|
||||
public bool CurrentCollectionInUse { get; private set; }
|
||||
|
||||
// The collection used for general file redirections and all characters not specifically named.
|
||||
public ModCollection Default { get; private set; } = ModCollection.Empty;
|
||||
|
||||
// The collection used for all files categorized as UI files.
|
||||
public ModCollection Interface { get; private set; } = ModCollection.Empty;
|
||||
|
||||
// A single collection that can not be deleted as a fallback for the current collection.
|
||||
private ModCollection DefaultName { get; set; } = ModCollection.Empty;
|
||||
|
||||
// The list of character collections.
|
||||
public readonly IndividualCollections Individuals;
|
||||
|
||||
public ModCollection Individual(ActorIdentifier identifier)
|
||||
=> Individuals.TryGetCollection(identifier, out var c) ? c : Default;
|
||||
|
||||
// Special Collections
|
||||
private readonly ModCollection?[] _specialCollections = new ModCollection?[Enum.GetValues<Api.Enums.ApiCollectionType>().Length - 3];
|
||||
|
||||
// Return the configured collection for the given type or null.
|
||||
// Does not handle Inactive, use ByName instead.
|
||||
public ModCollection? ByType(CollectionType type)
|
||||
=> ByType(type, ActorIdentifier.Invalid);
|
||||
|
||||
public ModCollection? ByType(CollectionType type, ActorIdentifier identifier)
|
||||
{
|
||||
if (type.IsSpecial())
|
||||
return _specialCollections[(int)type];
|
||||
|
||||
return type switch
|
||||
{
|
||||
CollectionType.Default => Default,
|
||||
CollectionType.Interface => Interface,
|
||||
CollectionType.Current => Current,
|
||||
CollectionType.Individual => identifier.IsValid && Individuals.Individuals.TryGetValue(identifier, out var c) ? c : null,
|
||||
_ => null,
|
||||
};
|
||||
}
|
||||
|
||||
// Set a active collection, can be used to set Default, Current, Interface, Special, or Individual collections.
|
||||
private void SetCollection(int newIdx, CollectionType collectionType, int individualIndex = -1)
|
||||
{
|
||||
var oldCollectionIdx = collectionType switch
|
||||
{
|
||||
CollectionType.Default => Default.Index,
|
||||
CollectionType.Interface => Interface.Index,
|
||||
CollectionType.Current => Current.Index,
|
||||
CollectionType.Individual => individualIndex < 0 || individualIndex >= Individuals.Count
|
||||
? -1
|
||||
: Individuals[individualIndex].Collection.Index,
|
||||
_ when collectionType.IsSpecial() => _specialCollections[(int)collectionType]?.Index ?? Default.Index,
|
||||
_ => -1,
|
||||
};
|
||||
|
||||
if (oldCollectionIdx == -1 || newIdx == oldCollectionIdx)
|
||||
return;
|
||||
|
||||
var newCollection = this[newIdx];
|
||||
if (newIdx > ModCollection.Empty.Index)
|
||||
newCollection.CreateCache(collectionType is CollectionType.Default);
|
||||
|
||||
switch (collectionType)
|
||||
{
|
||||
case CollectionType.Default:
|
||||
Default = newCollection;
|
||||
break;
|
||||
case CollectionType.Interface:
|
||||
Interface = newCollection;
|
||||
break;
|
||||
case CollectionType.Current:
|
||||
Current = newCollection;
|
||||
break;
|
||||
case CollectionType.Individual:
|
||||
if (!Individuals.ChangeCollection(individualIndex, newCollection))
|
||||
{
|
||||
RemoveCache(newIdx);
|
||||
return;
|
||||
}
|
||||
|
||||
break;
|
||||
default:
|
||||
_specialCollections[(int)collectionType] = newCollection;
|
||||
break;
|
||||
}
|
||||
|
||||
RemoveCache(oldCollectionIdx);
|
||||
|
||||
UpdateCurrentCollectionInUse();
|
||||
_communicator.CollectionChange.Invoke(collectionType, this[oldCollectionIdx], newCollection,
|
||||
collectionType == CollectionType.Individual ? Individuals[individualIndex].DisplayName : string.Empty);
|
||||
}
|
||||
|
||||
private void UpdateCurrentCollectionInUse()
|
||||
=> CurrentCollectionInUse = _specialCollections
|
||||
.OfType<ModCollection>()
|
||||
.Prepend(Interface)
|
||||
.Prepend(Default)
|
||||
.Concat(Individuals.Assignments.Select(kvp => kvp.Collection))
|
||||
.SelectMany(c => c.GetFlattenedInheritance()).Contains(Current);
|
||||
|
||||
public void SetCollection(ModCollection collection, CollectionType collectionType, int individualIndex = -1)
|
||||
=> SetCollection(collection.Index, collectionType, individualIndex);
|
||||
|
||||
// Create a special collection if it does not exist and set it to Empty.
|
||||
public bool CreateSpecialCollection(CollectionType collectionType)
|
||||
{
|
||||
if (!collectionType.IsSpecial() || _specialCollections[(int)collectionType] != null)
|
||||
return false;
|
||||
|
||||
_specialCollections[(int)collectionType] = Default;
|
||||
_communicator.CollectionChange.Invoke(collectionType, null, Default, string.Empty);
|
||||
return true;
|
||||
}
|
||||
|
||||
// Remove a special collection if it exists
|
||||
public void RemoveSpecialCollection(CollectionType collectionType)
|
||||
{
|
||||
if (!collectionType.IsSpecial())
|
||||
return;
|
||||
|
||||
var old = _specialCollections[(int)collectionType];
|
||||
if (old != null)
|
||||
{
|
||||
_specialCollections[(int)collectionType] = null;
|
||||
_communicator.CollectionChange.Invoke(collectionType, old, null, string.Empty);
|
||||
}
|
||||
}
|
||||
|
||||
// Wrappers around Individual Collection handling.
|
||||
public void CreateIndividualCollection(params ActorIdentifier[] identifiers)
|
||||
{
|
||||
if (Individuals.Add(identifiers, Default))
|
||||
_communicator.CollectionChange.Invoke(CollectionType.Individual, null, Default, Individuals.Last().DisplayName);
|
||||
}
|
||||
|
||||
public void RemoveIndividualCollection(int individualIndex)
|
||||
{
|
||||
if (individualIndex < 0 || individualIndex >= Individuals.Count)
|
||||
return;
|
||||
|
||||
var (name, old) = Individuals[individualIndex];
|
||||
if (Individuals.Delete(individualIndex))
|
||||
_communicator.CollectionChange.Invoke(CollectionType.Individual, old, null, name);
|
||||
}
|
||||
|
||||
public void MoveIndividualCollection(int from, int to)
|
||||
{
|
||||
if (Individuals.Move(from, to))
|
||||
Penumbra.SaveService.QueueSave(this);
|
||||
}
|
||||
|
||||
// Obtain the index of a collection by name.
|
||||
private int GetIndexForCollectionName(string name)
|
||||
=> name.Length == 0 ? ModCollection.Empty.Index : _collections.IndexOf(c => c.Name == name);
|
||||
|
||||
// Load default, current, special, and character collections from config.
|
||||
// Then create caches. If a collection does not exist anymore, reset it to an appropriate default.
|
||||
private void LoadCollections(FilenameService files)
|
||||
{
|
||||
var configChanged = !ReadActiveCollections(files, out var jObject);
|
||||
|
||||
// Load the default collection.
|
||||
var defaultName = jObject[nameof(Default)]?.ToObject<string>() ?? (configChanged ? ModCollection.DefaultCollection : ModCollection.Empty.Name);
|
||||
var defaultIdx = GetIndexForCollectionName(defaultName);
|
||||
if (defaultIdx < 0)
|
||||
{
|
||||
Penumbra.ChatService.NotificationMessage(
|
||||
$"Last choice of {TutorialService.DefaultCollection} {defaultName} is not available, reset to {ModCollection.Empty.Name}.", "Load Failure",
|
||||
NotificationType.Warning);
|
||||
Default = ModCollection.Empty;
|
||||
configChanged = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
Default = this[defaultIdx];
|
||||
}
|
||||
|
||||
// Load the interface collection.
|
||||
var interfaceName = jObject[nameof(Interface)]?.ToObject<string>() ?? Default.Name;
|
||||
var interfaceIdx = GetIndexForCollectionName(interfaceName);
|
||||
if (interfaceIdx < 0)
|
||||
{
|
||||
Penumbra.ChatService.NotificationMessage(
|
||||
$"Last choice of {TutorialService.InterfaceCollection} {interfaceName} is not available, reset to {ModCollection.Empty.Name}.",
|
||||
"Load Failure", NotificationType.Warning);
|
||||
Interface = ModCollection.Empty;
|
||||
configChanged = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
Interface = this[interfaceIdx];
|
||||
}
|
||||
|
||||
// Load the current collection.
|
||||
var currentName = jObject[nameof(Current)]?.ToObject<string>() ?? ModCollection.DefaultCollection;
|
||||
var currentIdx = GetIndexForCollectionName(currentName);
|
||||
if (currentIdx < 0)
|
||||
{
|
||||
Penumbra.ChatService.NotificationMessage(
|
||||
$"Last choice of {TutorialService.SelectedCollection} {currentName} is not available, reset to {ModCollection.DefaultCollection}.",
|
||||
"Load Failure", NotificationType.Warning);
|
||||
Current = DefaultName;
|
||||
configChanged = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
Current = this[currentIdx];
|
||||
}
|
||||
|
||||
// Load special collections.
|
||||
foreach (var (type, name, _) in CollectionTypeExtensions.Special)
|
||||
{
|
||||
var typeName = jObject[type.ToString()]?.ToObject<string>();
|
||||
if (typeName != null)
|
||||
{
|
||||
var idx = GetIndexForCollectionName(typeName);
|
||||
if (idx < 0)
|
||||
{
|
||||
Penumbra.ChatService.NotificationMessage($"Last choice of {name} Collection {typeName} is not available, removed.",
|
||||
"Load Failure",
|
||||
NotificationType.Warning);
|
||||
configChanged = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
_specialCollections[(int)type] = this[idx];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
configChanged |= MigrateIndividualCollections(jObject);
|
||||
configChanged |= Individuals.ReadJObject(jObject[nameof(Individuals)] as JArray, this);
|
||||
|
||||
// Save any changes and create all required caches.
|
||||
if (configChanged)
|
||||
Penumbra.SaveService.ImmediateSave(this);
|
||||
}
|
||||
|
||||
// Migrate ungendered collections to Male and Female for 0.5.9.0.
|
||||
public static void MigrateUngenderedCollections(FilenameService fileNames)
|
||||
{
|
||||
if (!ReadActiveCollections(fileNames, out var jObject))
|
||||
return;
|
||||
|
||||
foreach (var (type, _, _) in CollectionTypeExtensions.Special.Where(t => t.Item2.StartsWith("Male ")))
|
||||
{
|
||||
var oldName = type.ToString()[4..];
|
||||
var value = jObject[oldName];
|
||||
if (value == null)
|
||||
continue;
|
||||
|
||||
jObject.Remove(oldName);
|
||||
jObject.Add("Male" + oldName, value);
|
||||
jObject.Add("Female" + oldName, value);
|
||||
}
|
||||
|
||||
using var stream = File.Open(fileNames.ActiveCollectionsFile, FileMode.Truncate);
|
||||
using var writer = new StreamWriter(stream);
|
||||
using var j = new JsonTextWriter(writer);
|
||||
j.Formatting = Formatting.Indented;
|
||||
jObject.WriteTo(j);
|
||||
}
|
||||
|
||||
// Migrate individual collections to Identifiers for 0.6.0.
|
||||
private bool MigrateIndividualCollections(JObject jObject)
|
||||
{
|
||||
var version = jObject[nameof(Version)]?.Value<int>() ?? 0;
|
||||
if (version > 0)
|
||||
return false;
|
||||
|
||||
// Load character collections. If a player name comes up multiple times, the last one is applied.
|
||||
var characters = jObject["Characters"]?.ToObject<Dictionary<string, string>>() ?? new Dictionary<string, string>();
|
||||
var dict = new Dictionary<string, ModCollection>(characters.Count);
|
||||
foreach (var (player, collectionName) in characters)
|
||||
{
|
||||
var idx = GetIndexForCollectionName(collectionName);
|
||||
if (idx < 0)
|
||||
{
|
||||
Penumbra.ChatService.NotificationMessage(
|
||||
$"Last choice of <{player}>'s Collection {collectionName} is not available, reset to {ModCollection.Empty.Name}.", "Load Failure",
|
||||
NotificationType.Warning);
|
||||
dict.Add(player, ModCollection.Empty);
|
||||
}
|
||||
else
|
||||
{
|
||||
dict.Add(player, this[idx]);
|
||||
}
|
||||
}
|
||||
|
||||
Individuals.Migrate0To1(dict);
|
||||
return true;
|
||||
}
|
||||
|
||||
// Read the active collection file into a jObject.
|
||||
// Returns true if this is successful, false if the file does not exist or it is unsuccessful.
|
||||
private static bool ReadActiveCollections(FilenameService files, out JObject ret)
|
||||
{
|
||||
var file = files.ActiveCollectionsFile;
|
||||
if (File.Exists(file))
|
||||
try
|
||||
{
|
||||
ret = JObject.Parse(File.ReadAllText(file));
|
||||
return true;
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Penumbra.Log.Error($"Could not read active collections from file {file}:\n{e}");
|
||||
}
|
||||
|
||||
ret = new JObject();
|
||||
return false;
|
||||
}
|
||||
|
||||
// Save if any of the active collections is changed.
|
||||
private void SaveOnChange(CollectionType collectionType, ModCollection? _1, ModCollection? _2, string _3)
|
||||
{
|
||||
if (collectionType is not CollectionType.Inactive and not CollectionType.Temporary)
|
||||
Penumbra.SaveService.QueueSave(this);
|
||||
}
|
||||
|
||||
// Cache handling. Usually recreate caches on the next framework tick,
|
||||
// but at launch create all of them at once.
|
||||
public void CreateNecessaryCaches()
|
||||
{
|
||||
var tasks = _specialCollections.OfType<ModCollection>()
|
||||
.Concat(Individuals.Select(p => p.Collection))
|
||||
.Prepend(Current)
|
||||
.Prepend(Default)
|
||||
.Prepend(Interface)
|
||||
.Distinct()
|
||||
.Select(c => Task.Run(() => c.CalculateEffectiveFileListInternal(c == Default)))
|
||||
.ToArray();
|
||||
|
||||
Task.WaitAll(tasks);
|
||||
}
|
||||
|
||||
private void RemoveCache(int idx)
|
||||
{
|
||||
if (idx != ModCollection.Empty.Index
|
||||
&& idx != Default.Index
|
||||
&& idx != Interface.Index
|
||||
&& idx != Current.Index
|
||||
&& _specialCollections.All(c => c == null || c.Index != idx)
|
||||
&& Individuals.Select(p => p.Collection).All(c => c.Index != idx))
|
||||
_collections[idx].ClearCache();
|
||||
}
|
||||
|
||||
// Recalculate effective files for active collections on events.
|
||||
private void OnModAddedActive(Mod mod)
|
||||
{
|
||||
foreach (var collection in this.Where(c => c.HasCache && c[mod.Index].Settings?.Enabled == true))
|
||||
collection._cache!.AddMod(mod, true);
|
||||
}
|
||||
|
||||
private void OnModRemovedActive(Mod mod)
|
||||
{
|
||||
foreach (var collection in this.Where(c => c.HasCache && c[mod.Index].Settings?.Enabled == true))
|
||||
collection._cache!.RemoveMod(mod, true);
|
||||
}
|
||||
|
||||
private void OnModMovedActive(Mod mod)
|
||||
{
|
||||
foreach (var collection in this.Where(c => c.HasCache && c[mod.Index].Settings?.Enabled == true))
|
||||
collection._cache!.ReloadMod(mod, true);
|
||||
}
|
||||
|
||||
public string ToFilename(FilenameService fileNames)
|
||||
=> fileNames.ActiveCollectionsFile;
|
||||
|
||||
public string TypeName
|
||||
=> "Active Collections";
|
||||
|
||||
public string LogName(string _)
|
||||
=> "to file";
|
||||
|
||||
public void Save(StreamWriter writer)
|
||||
{
|
||||
var jObj = new JObject
|
||||
{
|
||||
{ nameof(Version), Version },
|
||||
{ nameof(Default), Default.Name },
|
||||
{ nameof(Interface), Interface.Name },
|
||||
{ nameof(Current), Current.Name },
|
||||
};
|
||||
foreach (var (type, collection) in _specialCollections.WithIndex().Where(p => p.Value != null)
|
||||
.Select(p => ((CollectionType)p.Index, p.Value!)))
|
||||
jObj.Add(type.ToString(), collection.Name);
|
||||
|
||||
jObj.Add(nameof(Individuals), Individuals.ToJObject());
|
||||
using var j = new JsonTextWriter(writer) { Formatting = Formatting.Indented };
|
||||
jObj.WriteTo(j);
|
||||
}
|
||||
}
|
||||
|
|
@ -17,462 +17,459 @@ using CharacterUtility = Penumbra.Interop.Services.CharacterUtility;
|
|||
|
||||
namespace Penumbra.Collections;
|
||||
|
||||
public partial class ModCollection
|
||||
public sealed partial class CollectionManager : IDisposable, IEnumerable<ModCollection>
|
||||
{
|
||||
public sealed partial class Manager : IDisposable, IEnumerable<ModCollection>
|
||||
private readonly Mods.ModManager _modManager;
|
||||
private readonly CommunicatorService _communicator;
|
||||
private readonly CharacterUtility _characterUtility;
|
||||
private readonly ResidentResourceManager _residentResources;
|
||||
private readonly Configuration _config;
|
||||
|
||||
|
||||
// The empty collection is always available and always has index 0.
|
||||
// It can not be deleted or moved.
|
||||
private readonly List<ModCollection> _collections = new()
|
||||
{
|
||||
private readonly Mod.Manager _modManager;
|
||||
private readonly CommunicatorService _communicator;
|
||||
private readonly CharacterUtility _characterUtility;
|
||||
private readonly ResidentResourceManager _residentResources;
|
||||
private readonly Configuration _config;
|
||||
ModCollection.Empty,
|
||||
};
|
||||
|
||||
public ModCollection this[Index idx]
|
||||
=> _collections[idx];
|
||||
|
||||
// The empty collection is always available and always has index 0.
|
||||
// It can not be deleted or moved.
|
||||
private readonly List<ModCollection> _collections = new()
|
||||
public ModCollection? this[string name]
|
||||
=> ByName(name, out var c) ? c : null;
|
||||
|
||||
public int Count
|
||||
=> _collections.Count;
|
||||
|
||||
// Obtain a collection case-independently by name.
|
||||
public bool ByName(string name, [NotNullWhen(true)] out ModCollection? collection)
|
||||
=> _collections.FindFirst(c => string.Equals(c.Name, name, StringComparison.OrdinalIgnoreCase), out collection);
|
||||
|
||||
// Default enumeration skips the empty collection.
|
||||
public IEnumerator<ModCollection> GetEnumerator()
|
||||
=> _collections.Skip(1).GetEnumerator();
|
||||
|
||||
IEnumerator IEnumerable.GetEnumerator()
|
||||
=> GetEnumerator();
|
||||
|
||||
public IEnumerable<ModCollection> GetEnumeratorWithEmpty()
|
||||
=> _collections;
|
||||
|
||||
public CollectionManager(StartTracker timer, CommunicatorService communicator, FilenameService files, CharacterUtility characterUtility,
|
||||
ResidentResourceManager residentResources, Configuration config, Mods.ModManager modManager, IndividualCollections individuals)
|
||||
{
|
||||
using var time = timer.Measure(StartTimeType.Collections);
|
||||
_communicator = communicator;
|
||||
_characterUtility = characterUtility;
|
||||
_residentResources = residentResources;
|
||||
_config = config;
|
||||
_modManager = modManager;
|
||||
Individuals = individuals;
|
||||
|
||||
// The collection manager reacts to changes in mods by itself.
|
||||
_modManager.ModDiscoveryStarted += OnModDiscoveryStarted;
|
||||
_modManager.ModDiscoveryFinished += OnModDiscoveryFinished;
|
||||
_modManager.ModOptionChanged += OnModOptionsChanged;
|
||||
_modManager.ModPathChanged += OnModPathChange;
|
||||
_communicator.CollectionChange.Event += SaveOnChange;
|
||||
_communicator.TemporaryGlobalModChange.Event += OnGlobalModChange;
|
||||
ReadCollections(files);
|
||||
LoadCollections(files);
|
||||
UpdateCurrentCollectionInUse();
|
||||
CreateNecessaryCaches();
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
_communicator.CollectionChange.Event -= SaveOnChange;
|
||||
_communicator.TemporaryGlobalModChange.Event -= OnGlobalModChange;
|
||||
_modManager.ModDiscoveryStarted -= OnModDiscoveryStarted;
|
||||
_modManager.ModDiscoveryFinished -= OnModDiscoveryFinished;
|
||||
_modManager.ModOptionChanged -= OnModOptionsChanged;
|
||||
_modManager.ModPathChanged -= OnModPathChange;
|
||||
}
|
||||
|
||||
private void OnGlobalModChange(TemporaryMod mod, bool created, bool removed)
|
||||
=> TempModManager.OnGlobalModChange(_collections, mod, created, removed);
|
||||
|
||||
// Returns true if the name is not empty, it is not the name of the empty collection
|
||||
// and no existing collection results in the same filename as name.
|
||||
public bool CanAddCollection(string name, out string fixedName)
|
||||
{
|
||||
if (!ModCollection.IsValidName(name))
|
||||
{
|
||||
Empty,
|
||||
};
|
||||
|
||||
public ModCollection this[Index idx]
|
||||
=> _collections[idx];
|
||||
|
||||
public ModCollection? this[string name]
|
||||
=> ByName(name, out var c) ? c : null;
|
||||
|
||||
public int Count
|
||||
=> _collections.Count;
|
||||
|
||||
// Obtain a collection case-independently by name.
|
||||
public bool ByName(string name, [NotNullWhen(true)] out ModCollection? collection)
|
||||
=> _collections.FindFirst(c => string.Equals(c.Name, name, StringComparison.OrdinalIgnoreCase), out collection);
|
||||
|
||||
// Default enumeration skips the empty collection.
|
||||
public IEnumerator<ModCollection> GetEnumerator()
|
||||
=> _collections.Skip(1).GetEnumerator();
|
||||
|
||||
IEnumerator IEnumerable.GetEnumerator()
|
||||
=> GetEnumerator();
|
||||
|
||||
public IEnumerable<ModCollection> GetEnumeratorWithEmpty()
|
||||
=> _collections;
|
||||
|
||||
public Manager(StartTracker timer, CommunicatorService communicator, FilenameService files, CharacterUtility characterUtility,
|
||||
ResidentResourceManager residentResources, Configuration config, Mod.Manager manager, IndividualCollections individuals)
|
||||
{
|
||||
using var time = timer.Measure(StartTimeType.Collections);
|
||||
_communicator = communicator;
|
||||
_characterUtility = characterUtility;
|
||||
_residentResources = residentResources;
|
||||
_config = config;
|
||||
_modManager = manager;
|
||||
Individuals = individuals;
|
||||
|
||||
// The collection manager reacts to changes in mods by itself.
|
||||
_modManager.ModDiscoveryStarted += OnModDiscoveryStarted;
|
||||
_modManager.ModDiscoveryFinished += OnModDiscoveryFinished;
|
||||
_modManager.ModOptionChanged += OnModOptionsChanged;
|
||||
_modManager.ModPathChanged += OnModPathChange;
|
||||
_communicator.CollectionChange.Event += SaveOnChange;
|
||||
_communicator.TemporaryGlobalModChange.Event += OnGlobalModChange;
|
||||
ReadCollections(files);
|
||||
LoadCollections(files);
|
||||
UpdateCurrentCollectionInUse();
|
||||
CreateNecessaryCaches();
|
||||
fixedName = string.Empty;
|
||||
return false;
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
name = name.RemoveInvalidPathSymbols().ToLowerInvariant();
|
||||
if (name.Length == 0
|
||||
|| name == ModCollection.Empty.Name.ToLowerInvariant()
|
||||
|| _collections.Any(c => c.Name.RemoveInvalidPathSymbols().ToLowerInvariant() == name))
|
||||
{
|
||||
_communicator.CollectionChange.Event -= SaveOnChange;
|
||||
_communicator.TemporaryGlobalModChange.Event -= OnGlobalModChange;
|
||||
_modManager.ModDiscoveryStarted -= OnModDiscoveryStarted;
|
||||
_modManager.ModDiscoveryFinished -= OnModDiscoveryFinished;
|
||||
_modManager.ModOptionChanged -= OnModOptionsChanged;
|
||||
_modManager.ModPathChanged -= OnModPathChange;
|
||||
fixedName = string.Empty;
|
||||
return false;
|
||||
}
|
||||
|
||||
private void OnGlobalModChange(TemporaryMod mod, bool created, bool removed)
|
||||
=> TempModManager.OnGlobalModChange(_collections, mod, created, removed);
|
||||
fixedName = name;
|
||||
return true;
|
||||
}
|
||||
|
||||
// Returns true if the name is not empty, it is not the name of the empty collection
|
||||
// and no existing collection results in the same filename as name.
|
||||
public bool CanAddCollection(string name, out string fixedName)
|
||||
// Add a new collection of the given name.
|
||||
// If duplicate is not-null, the new collection will be a duplicate of it.
|
||||
// If the name of the collection would result in an already existing filename, skip it.
|
||||
// Returns true if the collection was successfully created and fires a Inactive event.
|
||||
// Also sets the current collection to the new collection afterwards.
|
||||
public bool AddCollection(string name, ModCollection? duplicate)
|
||||
{
|
||||
if (!CanAddCollection(name, out var fixedName))
|
||||
{
|
||||
if (!IsValidName(name))
|
||||
{
|
||||
fixedName = string.Empty;
|
||||
return false;
|
||||
}
|
||||
|
||||
name = name.RemoveInvalidPathSymbols().ToLowerInvariant();
|
||||
if (name.Length == 0
|
||||
|| name == Empty.Name.ToLowerInvariant()
|
||||
|| _collections.Any(c => c.Name.RemoveInvalidPathSymbols().ToLowerInvariant() == name))
|
||||
{
|
||||
fixedName = string.Empty;
|
||||
return false;
|
||||
}
|
||||
|
||||
fixedName = name;
|
||||
return true;
|
||||
Penumbra.Log.Warning($"The new collection {name} would lead to the same path {fixedName} as one that already exists.");
|
||||
return false;
|
||||
}
|
||||
|
||||
// Add a new collection of the given name.
|
||||
// If duplicate is not-null, the new collection will be a duplicate of it.
|
||||
// If the name of the collection would result in an already existing filename, skip it.
|
||||
// Returns true if the collection was successfully created and fires a Inactive event.
|
||||
// Also sets the current collection to the new collection afterwards.
|
||||
public bool AddCollection(string name, ModCollection? duplicate)
|
||||
var newCollection = duplicate?.Duplicate(name) ?? ModCollection.CreateNewEmpty(name);
|
||||
newCollection.Index = _collections.Count;
|
||||
_collections.Add(newCollection);
|
||||
|
||||
Penumbra.SaveService.ImmediateSave(newCollection);
|
||||
Penumbra.Log.Debug($"Added collection {newCollection.AnonymizedName}.");
|
||||
_communicator.CollectionChange.Invoke(CollectionType.Inactive, null, newCollection, string.Empty);
|
||||
SetCollection(newCollection.Index, CollectionType.Current);
|
||||
return true;
|
||||
}
|
||||
|
||||
// Remove the given collection if it exists and is neither the empty nor the default-named collection.
|
||||
// If the removed collection was active, it also sets the corresponding collection to the appropriate default.
|
||||
// Also removes the collection from inheritances of all other collections.
|
||||
public bool RemoveCollection(int idx)
|
||||
{
|
||||
if (idx <= ModCollection.Empty.Index || idx >= _collections.Count)
|
||||
{
|
||||
if (!CanAddCollection(name, out var fixedName))
|
||||
{
|
||||
Penumbra.Log.Warning($"The new collection {name} would lead to the same path {fixedName} as one that already exists.");
|
||||
return false;
|
||||
}
|
||||
|
||||
var newCollection = duplicate?.Duplicate(name) ?? CreateNewEmpty(name);
|
||||
newCollection.Index = _collections.Count;
|
||||
_collections.Add(newCollection);
|
||||
|
||||
Penumbra.SaveService.ImmediateSave(newCollection);
|
||||
Penumbra.Log.Debug($"Added collection {newCollection.AnonymizedName}.");
|
||||
_communicator.CollectionChange.Invoke(CollectionType.Inactive, null, newCollection, string.Empty);
|
||||
SetCollection(newCollection.Index, CollectionType.Current);
|
||||
return true;
|
||||
Penumbra.Log.Error("Can not remove the empty collection.");
|
||||
return false;
|
||||
}
|
||||
|
||||
// Remove the given collection if it exists and is neither the empty nor the default-named collection.
|
||||
// If the removed collection was active, it also sets the corresponding collection to the appropriate default.
|
||||
// Also removes the collection from inheritances of all other collections.
|
||||
public bool RemoveCollection(int idx)
|
||||
if (idx == DefaultName.Index)
|
||||
{
|
||||
if (idx <= Empty.Index || idx >= _collections.Count)
|
||||
{
|
||||
Penumbra.Log.Error("Can not remove the empty collection.");
|
||||
return false;
|
||||
}
|
||||
|
||||
if (idx == DefaultName.Index)
|
||||
{
|
||||
Penumbra.Log.Error("Can not remove the default collection.");
|
||||
return false;
|
||||
}
|
||||
|
||||
if (idx == Current.Index)
|
||||
SetCollection(DefaultName.Index, CollectionType.Current);
|
||||
|
||||
if (idx == Default.Index)
|
||||
SetCollection(Empty.Index, CollectionType.Default);
|
||||
|
||||
for (var i = 0; i < _specialCollections.Length; ++i)
|
||||
{
|
||||
if (idx == _specialCollections[i]?.Index)
|
||||
SetCollection(Empty, (CollectionType)i);
|
||||
}
|
||||
|
||||
for (var i = 0; i < Individuals.Count; ++i)
|
||||
{
|
||||
if (Individuals[i].Collection.Index == idx)
|
||||
SetCollection(Empty, CollectionType.Individual, i);
|
||||
}
|
||||
|
||||
var collection = _collections[idx];
|
||||
|
||||
// Clear own inheritances.
|
||||
foreach (var inheritance in collection.Inheritance)
|
||||
collection.ClearSubscriptions(inheritance);
|
||||
|
||||
Penumbra.SaveService.ImmediateDelete(collection);
|
||||
_collections.RemoveAt(idx);
|
||||
|
||||
// Clear external inheritances.
|
||||
foreach (var c in _collections)
|
||||
{
|
||||
var inheritedIdx = c._inheritance.IndexOf(collection);
|
||||
if (inheritedIdx >= 0)
|
||||
c.RemoveInheritance(inheritedIdx);
|
||||
|
||||
if (c.Index > idx)
|
||||
--c.Index;
|
||||
}
|
||||
|
||||
Penumbra.Log.Debug($"Removed collection {collection.AnonymizedName}.");
|
||||
_communicator.CollectionChange.Invoke(CollectionType.Inactive, collection, null, string.Empty);
|
||||
return true;
|
||||
Penumbra.Log.Error("Can not remove the default collection.");
|
||||
return false;
|
||||
}
|
||||
|
||||
public bool RemoveCollection(ModCollection collection)
|
||||
=> RemoveCollection(collection.Index);
|
||||
if (idx == Current.Index)
|
||||
SetCollection(DefaultName.Index, CollectionType.Current);
|
||||
|
||||
private void OnModDiscoveryStarted()
|
||||
if (idx == Default.Index)
|
||||
SetCollection(ModCollection.Empty.Index, CollectionType.Default);
|
||||
|
||||
for (var i = 0; i < _specialCollections.Length; ++i)
|
||||
{
|
||||
foreach (var collection in this)
|
||||
collection.PrepareModDiscovery();
|
||||
if (idx == _specialCollections[i]?.Index)
|
||||
SetCollection(ModCollection.Empty, (CollectionType)i);
|
||||
}
|
||||
|
||||
private void OnModDiscoveryFinished()
|
||||
for (var i = 0; i < Individuals.Count; ++i)
|
||||
{
|
||||
// First, re-apply all mod settings.
|
||||
foreach (var collection in this)
|
||||
collection.ApplyModSettings();
|
||||
|
||||
// Afterwards, we update the caches. This can not happen in the same loop due to inheritance.
|
||||
foreach (var collection in this.Where(c => c.HasCache))
|
||||
collection.ForceCacheUpdate();
|
||||
if (Individuals[i].Collection.Index == idx)
|
||||
SetCollection(ModCollection.Empty, CollectionType.Individual, i);
|
||||
}
|
||||
|
||||
var collection = _collections[idx];
|
||||
|
||||
// A changed mod path forces changes for all collections, active and inactive.
|
||||
private void OnModPathChange(ModPathChangeType type, Mod mod, DirectoryInfo? oldDirectory,
|
||||
DirectoryInfo? newDirectory)
|
||||
// Clear own inheritances.
|
||||
foreach (var inheritance in collection.Inheritance)
|
||||
collection.ClearSubscriptions(inheritance);
|
||||
|
||||
Penumbra.SaveService.ImmediateDelete(collection);
|
||||
_collections.RemoveAt(idx);
|
||||
|
||||
// Clear external inheritances.
|
||||
foreach (var c in _collections)
|
||||
{
|
||||
switch (type)
|
||||
{
|
||||
case ModPathChangeType.Added:
|
||||
foreach (var collection in this)
|
||||
collection.AddMod(mod);
|
||||
var inheritedIdx = c._inheritance.IndexOf(collection);
|
||||
if (inheritedIdx >= 0)
|
||||
c.RemoveInheritance(inheritedIdx);
|
||||
|
||||
OnModAddedActive(mod);
|
||||
break;
|
||||
case ModPathChangeType.Deleted:
|
||||
OnModRemovedActive(mod);
|
||||
foreach (var collection in this)
|
||||
collection.RemoveMod(mod, mod.Index);
|
||||
|
||||
break;
|
||||
case ModPathChangeType.Moved:
|
||||
OnModMovedActive(mod);
|
||||
foreach (var collection in this.Where(collection => collection.Settings[mod.Index] != null))
|
||||
Penumbra.SaveService.QueueSave(collection);
|
||||
|
||||
break;
|
||||
case ModPathChangeType.StartingReload:
|
||||
OnModRemovedActive(mod);
|
||||
break;
|
||||
case ModPathChangeType.Reloaded:
|
||||
OnModAddedActive(mod);
|
||||
break;
|
||||
default: throw new ArgumentOutOfRangeException(nameof(type), type, null);
|
||||
}
|
||||
if (c.Index > idx)
|
||||
--c.Index;
|
||||
}
|
||||
|
||||
// Automatically update all relevant collections when a mod is changed.
|
||||
// This means saving if options change in a way where the settings may change and the collection has settings for this mod.
|
||||
// And also updating effective file and meta manipulation lists if necessary.
|
||||
private void OnModOptionsChanged(ModOptionChangeType type, Mod mod, int groupIdx, int optionIdx, int movedToIdx)
|
||||
Penumbra.Log.Debug($"Removed collection {collection.AnonymizedName}.");
|
||||
_communicator.CollectionChange.Invoke(CollectionType.Inactive, collection, null, string.Empty);
|
||||
return true;
|
||||
}
|
||||
|
||||
public bool RemoveCollection(ModCollection collection)
|
||||
=> RemoveCollection(collection.Index);
|
||||
|
||||
private void OnModDiscoveryStarted()
|
||||
{
|
||||
foreach (var collection in this)
|
||||
collection.PrepareModDiscovery();
|
||||
}
|
||||
|
||||
private void OnModDiscoveryFinished()
|
||||
{
|
||||
// First, re-apply all mod settings.
|
||||
foreach (var collection in this)
|
||||
collection.ApplyModSettings();
|
||||
|
||||
// Afterwards, we update the caches. This can not happen in the same loop due to inheritance.
|
||||
foreach (var collection in this.Where(c => c.HasCache))
|
||||
collection.ForceCacheUpdate();
|
||||
}
|
||||
|
||||
|
||||
// A changed mod path forces changes for all collections, active and inactive.
|
||||
private void OnModPathChange(ModPathChangeType type, Mod mod, DirectoryInfo? oldDirectory,
|
||||
DirectoryInfo? newDirectory)
|
||||
{
|
||||
switch (type)
|
||||
{
|
||||
// Handle changes that break revertability.
|
||||
if (type == ModOptionChangeType.PrepareChange)
|
||||
{
|
||||
foreach (var collection in this.Where(c => c.HasCache))
|
||||
{
|
||||
if (collection[mod.Index].Settings is { Enabled: true })
|
||||
collection._cache!.RemoveMod(mod, false);
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
type.HandlingInfo(out var requiresSaving, out var recomputeList, out var reload);
|
||||
|
||||
// Handle changes that require overwriting the collection.
|
||||
if (requiresSaving)
|
||||
case ModPathChangeType.Added:
|
||||
foreach (var collection in this)
|
||||
{
|
||||
if (collection._settings[mod.Index]?.HandleChanges(type, mod, groupIdx, optionIdx, movedToIdx) ?? false)
|
||||
Penumbra.SaveService.QueueSave(collection);
|
||||
}
|
||||
collection.AddMod(mod);
|
||||
|
||||
// Handle changes that reload the mod if the changes did not need to be prepared,
|
||||
// or re-add the mod if they were prepared.
|
||||
if (recomputeList)
|
||||
foreach (var collection in this.Where(c => c.HasCache))
|
||||
{
|
||||
if (collection[mod.Index].Settings is { Enabled: true })
|
||||
{
|
||||
if (reload)
|
||||
collection._cache!.ReloadMod(mod, true);
|
||||
else
|
||||
collection._cache!.AddMod(mod, true);
|
||||
}
|
||||
}
|
||||
}
|
||||
OnModAddedActive(mod);
|
||||
break;
|
||||
case ModPathChangeType.Deleted:
|
||||
OnModRemovedActive(mod);
|
||||
foreach (var collection in this)
|
||||
collection.RemoveMod(mod, mod.Index);
|
||||
|
||||
// Add the collection with the default name if it does not exist.
|
||||
// It should always be ensured that it exists, otherwise it will be created.
|
||||
// This can also not be deleted, so there are always at least the empty and a collection with default name.
|
||||
private void AddDefaultCollection()
|
||||
{
|
||||
var idx = GetIndexForCollectionName(DefaultCollection);
|
||||
if (idx >= 0)
|
||||
{
|
||||
DefaultName = this[idx];
|
||||
return;
|
||||
}
|
||||
break;
|
||||
case ModPathChangeType.Moved:
|
||||
OnModMovedActive(mod);
|
||||
foreach (var collection in this.Where(collection => collection.Settings[mod.Index] != null))
|
||||
Penumbra.SaveService.QueueSave(collection);
|
||||
|
||||
var defaultCollection = CreateNewEmpty(DefaultCollection);
|
||||
Penumbra.SaveService.ImmediateSave(defaultCollection);
|
||||
defaultCollection.Index = _collections.Count;
|
||||
_collections.Add(defaultCollection);
|
||||
}
|
||||
|
||||
// Inheritances can not be setup before all collections are read,
|
||||
// so this happens after reading the collections.
|
||||
private void ApplyInheritances(IEnumerable<IReadOnlyList<string>> inheritances)
|
||||
{
|
||||
foreach (var (collection, inheritance) in this.Zip(inheritances))
|
||||
{
|
||||
var changes = false;
|
||||
foreach (var subCollectionName in inheritance)
|
||||
{
|
||||
if (!ByName(subCollectionName, out var subCollection))
|
||||
{
|
||||
changes = true;
|
||||
Penumbra.Log.Warning($"Inherited collection {subCollectionName} for {collection.Name} does not exist, removed.");
|
||||
}
|
||||
else if (!collection.AddInheritance(subCollection, false))
|
||||
{
|
||||
changes = true;
|
||||
Penumbra.Log.Warning($"{collection.Name} can not inherit from {subCollectionName}, removed.");
|
||||
}
|
||||
}
|
||||
|
||||
if (changes)
|
||||
Penumbra.SaveService.ImmediateSave(collection);
|
||||
}
|
||||
}
|
||||
|
||||
// Read all collection files in the Collection Directory.
|
||||
// Ensure that the default named collection exists, and apply inheritances afterwards.
|
||||
// Duplicate collection files are not deleted, just not added here.
|
||||
private void ReadCollections(FilenameService files)
|
||||
{
|
||||
var inheritances = new List<IReadOnlyList<string>>();
|
||||
foreach (var file in files.CollectionFiles)
|
||||
{
|
||||
var collection = LoadFromFile(file, out var inheritance);
|
||||
if (collection == null || collection.Name.Length == 0)
|
||||
continue;
|
||||
|
||||
if (file.Name != $"{collection.Name.RemoveInvalidPathSymbols()}.json")
|
||||
Penumbra.Log.Warning($"Collection {file.Name} does not correspond to {collection.Name}.");
|
||||
|
||||
if (this[collection.Name] != null)
|
||||
{
|
||||
Penumbra.Log.Warning($"Duplicate collection found: {collection.Name} already exists.");
|
||||
}
|
||||
else
|
||||
{
|
||||
inheritances.Add(inheritance);
|
||||
collection.Index = _collections.Count;
|
||||
_collections.Add(collection);
|
||||
}
|
||||
}
|
||||
|
||||
AddDefaultCollection();
|
||||
ApplyInheritances(inheritances);
|
||||
}
|
||||
|
||||
public string RedundancyCheck(CollectionType type, ActorIdentifier id)
|
||||
{
|
||||
var checkAssignment = ByType(type, id);
|
||||
if (checkAssignment == null)
|
||||
return string.Empty;
|
||||
|
||||
switch (type)
|
||||
{
|
||||
// Check individual assignments. We can only be sure of redundancy for world-overlap or ownership overlap.
|
||||
case CollectionType.Individual:
|
||||
switch (id.Type)
|
||||
{
|
||||
case IdentifierType.Player when id.HomeWorld != ushort.MaxValue:
|
||||
{
|
||||
var global = ByType(CollectionType.Individual, Penumbra.Actors.CreatePlayer(id.PlayerName, ushort.MaxValue));
|
||||
return global?.Index == checkAssignment.Index
|
||||
? "Assignment is redundant due to an identical Any-World assignment existing.\nYou can remove it."
|
||||
: string.Empty;
|
||||
}
|
||||
case IdentifierType.Owned:
|
||||
if (id.HomeWorld != ushort.MaxValue)
|
||||
{
|
||||
var global = ByType(CollectionType.Individual,
|
||||
Penumbra.Actors.CreateOwned(id.PlayerName, ushort.MaxValue, id.Kind, id.DataId));
|
||||
if (global?.Index == checkAssignment.Index)
|
||||
return "Assignment is redundant due to an identical Any-World assignment existing.\nYou can remove it.";
|
||||
}
|
||||
|
||||
var unowned = ByType(CollectionType.Individual, Penumbra.Actors.CreateNpc(id.Kind, id.DataId));
|
||||
return unowned?.Index == checkAssignment.Index
|
||||
? "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:
|
||||
case CollectionType.MaleNonPlayerCharacter:
|
||||
case CollectionType.FemalePlayerCharacter:
|
||||
case CollectionType.FemaleNonPlayerCharacter:
|
||||
var first = ByType(CollectionType.MalePlayerCharacter) ?? Default;
|
||||
var second = ByType(CollectionType.MaleNonPlayerCharacter) ?? Default;
|
||||
var third = ByType(CollectionType.FemalePlayerCharacter) ?? Default;
|
||||
var fourth = ByType(CollectionType.FemaleNonPlayerCharacter) ?? Default;
|
||||
if (first.Index == second.Index
|
||||
&& first.Index == third.Index
|
||||
&& first.Index == fourth.Index
|
||||
&& first.Index == Default.Index)
|
||||
return
|
||||
"Assignment is currently redundant due to the group [Male, Female, Player, NPC] Characters being unassigned or identical to each other and Default.\n"
|
||||
+ "You can keep just the Default Assignment.";
|
||||
|
||||
break;
|
||||
// Children and Elderly are redundant if they are identical to both Male NPCs and Female NPCs, or if they are unassigned to Default.
|
||||
case CollectionType.NonPlayerChild:
|
||||
case CollectionType.NonPlayerElderly:
|
||||
var maleNpc = ByType(CollectionType.MaleNonPlayerCharacter);
|
||||
var femaleNpc = ByType(CollectionType.FemaleNonPlayerCharacter);
|
||||
var collection1 = CollectionType.MaleNonPlayerCharacter;
|
||||
var collection2 = CollectionType.FemaleNonPlayerCharacter;
|
||||
if (maleNpc == null)
|
||||
{
|
||||
maleNpc = Default;
|
||||
if (maleNpc.Index != checkAssignment.Index)
|
||||
return string.Empty;
|
||||
|
||||
collection1 = CollectionType.Default;
|
||||
}
|
||||
|
||||
if (femaleNpc == null)
|
||||
{
|
||||
femaleNpc = Default;
|
||||
if (femaleNpc.Index != checkAssignment.Index)
|
||||
return string.Empty;
|
||||
|
||||
collection2 = CollectionType.Default;
|
||||
}
|
||||
|
||||
return collection1 == collection2
|
||||
? $"Assignment is currently redundant due to overwriting {collection1.ToName()} with an identical collection.\nYou can remove them."
|
||||
: $"Assignment is currently redundant due to overwriting {collection1.ToName()} and {collection2.ToName()} with an identical collection.\nYou can remove them.";
|
||||
|
||||
// For other assignments, check the inheritance order, unassigned means fall-through,
|
||||
// assigned needs identical assignments to be redundant.
|
||||
default:
|
||||
var group = type.InheritanceOrder();
|
||||
foreach (var parentType in group)
|
||||
{
|
||||
var assignment = ByType(parentType);
|
||||
if (assignment == null)
|
||||
continue;
|
||||
|
||||
if (assignment.Index == checkAssignment.Index)
|
||||
return $"Assignment is currently redundant due to overwriting {parentType.ToName()} with an identical collection.\nYou can remove it.";
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
return string.Empty;
|
||||
break;
|
||||
case ModPathChangeType.StartingReload:
|
||||
OnModRemovedActive(mod);
|
||||
break;
|
||||
case ModPathChangeType.Reloaded:
|
||||
OnModAddedActive(mod);
|
||||
break;
|
||||
default: throw new ArgumentOutOfRangeException(nameof(type), type, null);
|
||||
}
|
||||
}
|
||||
|
||||
// Automatically update all relevant collections when a mod is changed.
|
||||
// This means saving if options change in a way where the settings may change and the collection has settings for this mod.
|
||||
// And also updating effective file and meta manipulation lists if necessary.
|
||||
private void OnModOptionsChanged(ModOptionChangeType type, Mod mod, int groupIdx, int optionIdx, int movedToIdx)
|
||||
{
|
||||
// Handle changes that break revertability.
|
||||
if (type == ModOptionChangeType.PrepareChange)
|
||||
{
|
||||
foreach (var collection in this.Where(c => c.HasCache))
|
||||
{
|
||||
if (collection[mod.Index].Settings is { Enabled: true })
|
||||
collection._cache!.RemoveMod(mod, false);
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
type.HandlingInfo(out var requiresSaving, out var recomputeList, out var reload);
|
||||
|
||||
// Handle changes that require overwriting the collection.
|
||||
if (requiresSaving)
|
||||
foreach (var collection in this)
|
||||
{
|
||||
if (collection._settings[mod.Index]?.HandleChanges(type, mod, groupIdx, optionIdx, movedToIdx) ?? false)
|
||||
Penumbra.SaveService.QueueSave(collection);
|
||||
}
|
||||
|
||||
// Handle changes that reload the mod if the changes did not need to be prepared,
|
||||
// or re-add the mod if they were prepared.
|
||||
if (recomputeList)
|
||||
foreach (var collection in this.Where(c => c.HasCache))
|
||||
{
|
||||
if (collection[mod.Index].Settings is { Enabled: true })
|
||||
{
|
||||
if (reload)
|
||||
collection._cache!.ReloadMod(mod, true);
|
||||
else
|
||||
collection._cache!.AddMod(mod, true);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Add the collection with the default name if it does not exist.
|
||||
// It should always be ensured that it exists, otherwise it will be created.
|
||||
// This can also not be deleted, so there are always at least the empty and a collection with default name.
|
||||
private void AddDefaultCollection()
|
||||
{
|
||||
var idx = GetIndexForCollectionName(ModCollection.DefaultCollection);
|
||||
if (idx >= 0)
|
||||
{
|
||||
DefaultName = this[idx];
|
||||
return;
|
||||
}
|
||||
|
||||
var defaultCollection = ModCollection.CreateNewEmpty((string)ModCollection.DefaultCollection);
|
||||
Penumbra.SaveService.ImmediateSave(defaultCollection);
|
||||
defaultCollection.Index = _collections.Count;
|
||||
_collections.Add(defaultCollection);
|
||||
}
|
||||
|
||||
// Inheritances can not be setup before all collections are read,
|
||||
// so this happens after reading the collections.
|
||||
private void ApplyInheritances(IEnumerable<IReadOnlyList<string>> inheritances)
|
||||
{
|
||||
foreach (var (collection, inheritance) in this.Zip(inheritances))
|
||||
{
|
||||
var changes = false;
|
||||
foreach (var subCollectionName in inheritance)
|
||||
{
|
||||
if (!ByName(subCollectionName, out var subCollection))
|
||||
{
|
||||
changes = true;
|
||||
Penumbra.Log.Warning($"Inherited collection {subCollectionName} for {collection.Name} does not exist, removed.");
|
||||
}
|
||||
else if (!collection.AddInheritance(subCollection, false))
|
||||
{
|
||||
changes = true;
|
||||
Penumbra.Log.Warning($"{collection.Name} can not inherit from {subCollectionName}, removed.");
|
||||
}
|
||||
}
|
||||
|
||||
if (changes)
|
||||
Penumbra.SaveService.ImmediateSave(collection);
|
||||
}
|
||||
}
|
||||
|
||||
// Read all collection files in the Collection Directory.
|
||||
// Ensure that the default named collection exists, and apply inheritances afterwards.
|
||||
// Duplicate collection files are not deleted, just not added here.
|
||||
private void ReadCollections(FilenameService files)
|
||||
{
|
||||
var inheritances = new List<IReadOnlyList<string>>();
|
||||
foreach (var file in files.CollectionFiles)
|
||||
{
|
||||
var collection = ModCollection.LoadFromFile(file, out var inheritance);
|
||||
if (collection == null || collection.Name.Length == 0)
|
||||
continue;
|
||||
|
||||
if (file.Name != $"{collection.Name.RemoveInvalidPathSymbols()}.json")
|
||||
Penumbra.Log.Warning($"Collection {file.Name} does not correspond to {collection.Name}.");
|
||||
|
||||
if (this[collection.Name] != null)
|
||||
{
|
||||
Penumbra.Log.Warning($"Duplicate collection found: {collection.Name} already exists.");
|
||||
}
|
||||
else
|
||||
{
|
||||
inheritances.Add(inheritance);
|
||||
collection.Index = _collections.Count;
|
||||
_collections.Add(collection);
|
||||
}
|
||||
}
|
||||
|
||||
AddDefaultCollection();
|
||||
ApplyInheritances(inheritances);
|
||||
}
|
||||
|
||||
public string RedundancyCheck(CollectionType type, ActorIdentifier id)
|
||||
{
|
||||
var checkAssignment = ByType(type, id);
|
||||
if (checkAssignment == null)
|
||||
return string.Empty;
|
||||
|
||||
switch (type)
|
||||
{
|
||||
// Check individual assignments. We can only be sure of redundancy for world-overlap or ownership overlap.
|
||||
case CollectionType.Individual:
|
||||
switch (id.Type)
|
||||
{
|
||||
case IdentifierType.Player when id.HomeWorld != ushort.MaxValue:
|
||||
{
|
||||
var global = ByType(CollectionType.Individual, Penumbra.Actors.CreatePlayer(id.PlayerName, ushort.MaxValue));
|
||||
return global?.Index == checkAssignment.Index
|
||||
? "Assignment is redundant due to an identical Any-World assignment existing.\nYou can remove it."
|
||||
: string.Empty;
|
||||
}
|
||||
case IdentifierType.Owned:
|
||||
if (id.HomeWorld != ushort.MaxValue)
|
||||
{
|
||||
var global = ByType(CollectionType.Individual,
|
||||
Penumbra.Actors.CreateOwned(id.PlayerName, ushort.MaxValue, id.Kind, id.DataId));
|
||||
if (global?.Index == checkAssignment.Index)
|
||||
return "Assignment is redundant due to an identical Any-World assignment existing.\nYou can remove it.";
|
||||
}
|
||||
|
||||
var unowned = ByType(CollectionType.Individual, Penumbra.Actors.CreateNpc(id.Kind, id.DataId));
|
||||
return unowned?.Index == checkAssignment.Index
|
||||
? "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:
|
||||
case CollectionType.MaleNonPlayerCharacter:
|
||||
case CollectionType.FemalePlayerCharacter:
|
||||
case CollectionType.FemaleNonPlayerCharacter:
|
||||
var first = ByType(CollectionType.MalePlayerCharacter) ?? Default;
|
||||
var second = ByType(CollectionType.MaleNonPlayerCharacter) ?? Default;
|
||||
var third = ByType(CollectionType.FemalePlayerCharacter) ?? Default;
|
||||
var fourth = ByType(CollectionType.FemaleNonPlayerCharacter) ?? Default;
|
||||
if (first.Index == second.Index
|
||||
&& first.Index == third.Index
|
||||
&& first.Index == fourth.Index
|
||||
&& first.Index == Default.Index)
|
||||
return
|
||||
"Assignment is currently redundant due to the group [Male, Female, Player, NPC] Characters being unassigned or identical to each other and Default.\n"
|
||||
+ "You can keep just the Default Assignment.";
|
||||
|
||||
break;
|
||||
// Children and Elderly are redundant if they are identical to both Male NPCs and Female NPCs, or if they are unassigned to Default.
|
||||
case CollectionType.NonPlayerChild:
|
||||
case CollectionType.NonPlayerElderly:
|
||||
var maleNpc = ByType(CollectionType.MaleNonPlayerCharacter);
|
||||
var femaleNpc = ByType(CollectionType.FemaleNonPlayerCharacter);
|
||||
var collection1 = CollectionType.MaleNonPlayerCharacter;
|
||||
var collection2 = CollectionType.FemaleNonPlayerCharacter;
|
||||
if (maleNpc == null)
|
||||
{
|
||||
maleNpc = Default;
|
||||
if (maleNpc.Index != checkAssignment.Index)
|
||||
return string.Empty;
|
||||
|
||||
collection1 = CollectionType.Default;
|
||||
}
|
||||
|
||||
if (femaleNpc == null)
|
||||
{
|
||||
femaleNpc = Default;
|
||||
if (femaleNpc.Index != checkAssignment.Index)
|
||||
return string.Empty;
|
||||
|
||||
collection2 = CollectionType.Default;
|
||||
}
|
||||
|
||||
return collection1 == collection2
|
||||
? $"Assignment is currently redundant due to overwriting {collection1.ToName()} with an identical collection.\nYou can remove them."
|
||||
: $"Assignment is currently redundant due to overwriting {collection1.ToName()} and {collection2.ToName()} with an identical collection.\nYou can remove them.";
|
||||
|
||||
// For other assignments, check the inheritance order, unassigned means fall-through,
|
||||
// assigned needs identical assignments to be redundant.
|
||||
default:
|
||||
var group = type.InheritanceOrder();
|
||||
foreach (var parentType in group)
|
||||
{
|
||||
var assignment = ByType(parentType);
|
||||
if (assignment == null)
|
||||
continue;
|
||||
|
||||
if (assignment.Index == checkAssignment.Index)
|
||||
return $"Assignment is currently redundant due to overwriting {parentType.ToName()} with an identical collection.\nYou can remove it.";
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
return string.Empty;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -26,7 +26,7 @@ public partial class IndividualCollections
|
|||
return ret;
|
||||
}
|
||||
|
||||
public bool ReadJObject( JArray? obj, ModCollection.Manager manager )
|
||||
public bool ReadJObject( JArray? obj, CollectionManager manager )
|
||||
{
|
||||
if( obj == null )
|
||||
{
|
||||
|
|
|
|||
|
|
@ -8,7 +8,6 @@ using System.Diagnostics.CodeAnalysis;
|
|||
using System.Linq;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Threading;
|
||||
using Penumbra.Interop;
|
||||
using Penumbra.Interop.Structs;
|
||||
using Penumbra.Meta.Files;
|
||||
using Penumbra.Meta.Manipulations;
|
||||
|
|
@ -20,27 +19,27 @@ namespace Penumbra.Collections;
|
|||
public partial class ModCollection
|
||||
{
|
||||
// Only active collections need to have a cache.
|
||||
private Cache? _cache;
|
||||
internal ModCollectionCache? _cache;
|
||||
|
||||
public bool HasCache
|
||||
=> _cache != null;
|
||||
|
||||
// Count the number of changes of the effective file list.
|
||||
// This is used for material and imc changes.
|
||||
public int ChangeCounter { get; private set; }
|
||||
public int ChangeCounter { get; internal set; }
|
||||
|
||||
// Only create, do not update.
|
||||
private void CreateCache(bool isDefault)
|
||||
internal void CreateCache(bool isDefault)
|
||||
{
|
||||
if (_cache == null)
|
||||
{
|
||||
CalculateEffectiveFileList(isDefault);
|
||||
Penumbra.Log.Verbose($"Created new cache for collection {Name}.");
|
||||
}
|
||||
if (_cache != null)
|
||||
return;
|
||||
|
||||
CalculateEffectiveFileList(isDefault);
|
||||
Penumbra.Log.Verbose($"Created new cache for collection {Name}.");
|
||||
}
|
||||
|
||||
// Force an update with metadata for this cache.
|
||||
private void ForceCacheUpdate()
|
||||
internal void ForceCacheUpdate()
|
||||
=> CalculateEffectiveFileList(this == Penumbra.CollectionManager.Default);
|
||||
|
||||
// Handle temporary mods for this collection.
|
||||
|
|
@ -83,7 +82,7 @@ public partial class ModCollection
|
|||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
private static bool CheckFullPath(Utf8GamePath path, FullPath fullPath)
|
||||
internal static bool CheckFullPath(Utf8GamePath path, FullPath fullPath)
|
||||
{
|
||||
if (fullPath.InternalName.Length < Utf8GamePath.MaxGamePathLength)
|
||||
return true;
|
||||
|
|
@ -127,14 +126,14 @@ public partial class ModCollection
|
|||
=> Penumbra.Framework.RegisterImportant(nameof(CalculateEffectiveFileList) + Name, () =>
|
||||
CalculateEffectiveFileListInternal(isDefault));
|
||||
|
||||
private void CalculateEffectiveFileListInternal(bool isDefault)
|
||||
internal void CalculateEffectiveFileListInternal(bool isDefault)
|
||||
{
|
||||
// Skip the empty collection.
|
||||
if (Index == 0)
|
||||
return;
|
||||
|
||||
Penumbra.Log.Debug($"[{Thread.CurrentThread.ManagedThreadId}] Recalculating effective file list for {AnonymizedName}");
|
||||
_cache ??= new Cache(this);
|
||||
_cache ??= new ModCollectionCache(this);
|
||||
_cache.FullRecalculation(isDefault);
|
||||
|
||||
Penumbra.Log.Debug($"[{Thread.CurrentThread.ManagedThreadId}] Recalculation of effective file list for {AnonymizedName} finished.");
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
|
|
@ -15,7 +15,7 @@ public partial class ModCollection : ISavable
|
|||
{
|
||||
// Since inheritances depend on other collections existing,
|
||||
// we return them as a list to be applied after reading all collections.
|
||||
private static ModCollection? LoadFromFile(FileInfo file, out IReadOnlyList<string> inheritance)
|
||||
internal static ModCollection? LoadFromFile(FileInfo file, out IReadOnlyList<string> inheritance)
|
||||
{
|
||||
inheritance = Array.Empty<string>();
|
||||
if (!file.Exists)
|
||||
|
|
|
|||
|
|
@ -16,7 +16,7 @@ public partial class ModCollection
|
|||
// The bool signifies whether the change was in an already inherited collection.
|
||||
public event Action< bool > InheritanceChanged;
|
||||
|
||||
private readonly List< ModCollection > _inheritance = new();
|
||||
internal readonly List< ModCollection > _inheritance = new();
|
||||
|
||||
public IReadOnlyList< ModCollection > Inheritance
|
||||
=> _inheritance;
|
||||
|
|
@ -98,7 +98,7 @@ public partial class ModCollection
|
|||
Penumbra.Log.Debug( $"Removed {inheritance.AnonymizedName} from {AnonymizedName} inheritances." );
|
||||
}
|
||||
|
||||
private void ClearSubscriptions( ModCollection other )
|
||||
internal void ClearSubscriptions( ModCollection other )
|
||||
{
|
||||
other.ModSettingChanged -= OnInheritedModSettingChange;
|
||||
other.InheritanceChanged -= OnInheritedInheritanceChange;
|
||||
|
|
|
|||
|
|
@ -23,18 +23,18 @@ public partial class ModCollection
|
|||
|
||||
// The collection name can contain invalid path characters,
|
||||
// but after removing those and going to lower case it has to be unique.
|
||||
public string Name { get; private init; }
|
||||
public string Name { get; internal init; }
|
||||
|
||||
// Get the first two letters of a collection name and its Index (or None if it is the empty collection).
|
||||
public string AnonymizedName
|
||||
=> this == Empty ? Empty.Name : Name.Length > 2 ? $"{Name[..2]}... ({Index})" : $"{Name} ({Index})";
|
||||
|
||||
public int Version { get; private set; }
|
||||
public int Index { get; private set; } = -1;
|
||||
public int Version { get; internal set; }
|
||||
public int Index { get; internal set; } = -1;
|
||||
|
||||
// If a ModSetting is null, it can be inherited from other collections.
|
||||
// If no collection provides a setting for the mod, it is just disabled.
|
||||
private readonly List<ModSettings?> _settings;
|
||||
internal readonly List<ModSettings?> _settings;
|
||||
|
||||
public IReadOnlyList<ModSettings?> Settings
|
||||
=> _settings;
|
||||
|
|
@ -115,7 +115,7 @@ public partial class ModCollection
|
|||
}
|
||||
|
||||
// Add settings for a new appended mod, by checking if the mod had settings from a previous deletion.
|
||||
private bool AddMod(Mod mod)
|
||||
internal bool AddMod(Mod mod)
|
||||
{
|
||||
if (_unusedSettings.TryGetValue(mod.ModPath.Name, out var save))
|
||||
{
|
||||
|
|
@ -130,7 +130,7 @@ public partial class ModCollection
|
|||
}
|
||||
|
||||
// Move settings from the current mod list to the unused mod settings.
|
||||
private void RemoveMod(Mod mod, int idx)
|
||||
internal void RemoveMod(Mod mod, int idx)
|
||||
{
|
||||
var settings = _settings[idx];
|
||||
if (settings != null)
|
||||
|
|
@ -150,7 +150,7 @@ public partial class ModCollection
|
|||
}
|
||||
|
||||
// Move all settings to unused settings for rediscovery.
|
||||
private void PrepareModDiscovery()
|
||||
internal void PrepareModDiscovery()
|
||||
{
|
||||
foreach (var (mod, setting) in Penumbra.ModManager.Zip(_settings).Where(s => s.Second != null))
|
||||
_unusedSettings[mod.ModPath.Name] = new ModSettings.SavedSettings(setting!, mod);
|
||||
|
|
@ -160,7 +160,7 @@ public partial class ModCollection
|
|||
|
||||
// Apply all mod settings from unused settings to the current set of mods.
|
||||
// Also fixes invalid settings.
|
||||
private void ApplyModSettings()
|
||||
internal void ApplyModSettings()
|
||||
{
|
||||
_settings.Capacity = Math.Max(_settings.Capacity, Penumbra.ModManager.Count);
|
||||
if (Penumbra.ModManager.Aggregate(false, (current, mod) => current | AddMod(mod)))
|
||||
|
|
|
|||
|
|
@ -27,12 +27,12 @@ public class CommandHandler : IDisposable
|
|||
private readonly Configuration _config;
|
||||
private readonly ConfigWindow _configWindow;
|
||||
private readonly ActorManager _actors;
|
||||
private readonly Mod.Manager _modManager;
|
||||
private readonly ModCollection.Manager _collectionManager;
|
||||
private readonly ModManager _modManager;
|
||||
private readonly CollectionManager _collectionManager;
|
||||
private readonly Penumbra _penumbra;
|
||||
|
||||
public CommandHandler(Framework framework, CommandManager commandManager, ChatGui chat, RedrawService redrawService, Configuration config,
|
||||
ConfigWindow configWindow, Mod.Manager modManager, ModCollection.Manager collectionManager, ActorService actors, Penumbra penumbra)
|
||||
ConfigWindow configWindow, ModManager modManager, CollectionManager collectionManager, ActorService actors, Penumbra penumbra)
|
||||
{
|
||||
_commandManager = commandManager;
|
||||
_redrawService = redrawService;
|
||||
|
|
|
|||
|
|
@ -36,10 +36,10 @@ public partial class TexToolsImporter : IDisposable
|
|||
|
||||
private readonly Configuration _config;
|
||||
private readonly ModEditor _editor;
|
||||
private readonly Mod.Manager _modManager;
|
||||
private readonly ModManager _modManager;
|
||||
|
||||
public TexToolsImporter( DirectoryInfo baseDirectory, int count, IEnumerable< FileInfo > modPackFiles,
|
||||
Action< FileInfo, DirectoryInfo?, Exception? > handler, Configuration config, ModEditor editor, Mod.Manager modManager)
|
||||
Action< FileInfo, DirectoryInfo?, Exception? > handler, Configuration config, ModEditor editor, ModManager modManager)
|
||||
{
|
||||
_baseDirectory = baseDirectory;
|
||||
_tmpFile = Path.Combine( _baseDirectory.FullName, TempFileName );
|
||||
|
|
|
|||
|
|
@ -31,12 +31,12 @@ public unsafe class CollectionResolver
|
|||
private readonly CutsceneService _cutscenes;
|
||||
|
||||
private readonly Configuration _config;
|
||||
private readonly ModCollection.Manager _collectionManager;
|
||||
private readonly CollectionManager _collectionManager;
|
||||
private readonly TempCollectionManager _tempCollections;
|
||||
private readonly DrawObjectState _drawObjectState;
|
||||
|
||||
public CollectionResolver(PerformanceTracker performance, IdentifiedCollectionCache cache, ClientState clientState, GameGui gameGui,
|
||||
DataManager gameData, ActorService actors, CutsceneService cutscenes, Configuration config, ModCollection.Manager collectionManager,
|
||||
DataManager gameData, ActorService actors, CutsceneService cutscenes, Configuration config, CollectionManager collectionManager,
|
||||
TempCollectionManager tempCollections, DrawObjectState drawObjectState)
|
||||
{
|
||||
_performance = performance;
|
||||
|
|
|
|||
|
|
@ -16,7 +16,7 @@ public class PathResolver : IDisposable
|
|||
{
|
||||
private readonly PerformanceTracker _performance;
|
||||
private readonly Configuration _config;
|
||||
private readonly ModCollection.Manager _collectionManager;
|
||||
private readonly CollectionManager _collectionManager;
|
||||
private readonly TempCollectionManager _tempCollections;
|
||||
private readonly ResourceLoader _loader;
|
||||
|
||||
|
|
@ -25,7 +25,7 @@ public class PathResolver : IDisposable
|
|||
private readonly PathState _pathState;
|
||||
private readonly MetaState _metaState;
|
||||
|
||||
public unsafe PathResolver(PerformanceTracker performance, Configuration config, ModCollection.Manager collectionManager,
|
||||
public unsafe PathResolver(PerformanceTracker performance, Configuration config, CollectionManager collectionManager,
|
||||
TempCollectionManager tempCollections, ResourceLoader loader, AnimationHookService animationHookService, SubfileHelper subfileHelper,
|
||||
PathState pathState, MetaState metaState)
|
||||
{
|
||||
|
|
|
|||
|
|
@ -11,12 +11,12 @@ namespace Penumbra.Mods;
|
|||
|
||||
public class DuplicateManager
|
||||
{
|
||||
private readonly Mod.Manager _modManager;
|
||||
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, Mod.Manager modManager)
|
||||
public DuplicateManager(ModFileCollection files, ModManager modManager)
|
||||
{
|
||||
_files = files;
|
||||
_modManager = modManager;
|
||||
|
|
@ -80,7 +80,7 @@ public class DuplicateManager
|
|||
}
|
||||
else
|
||||
{
|
||||
var sub = (Mod.SubMod)subMod;
|
||||
var sub = (SubMod)subMod;
|
||||
sub.FileData = dict;
|
||||
if (groupIdx == -1)
|
||||
mod.SaveDefaultMod();
|
||||
|
|
|
|||
|
|
@ -10,12 +10,12 @@ public class ModBackup
|
|||
{
|
||||
public static bool CreatingBackup { get; private set; }
|
||||
|
||||
private readonly Mod.Manager _modManager;
|
||||
private readonly ModManager _modManager;
|
||||
private readonly Mod _mod;
|
||||
public readonly string Name;
|
||||
public readonly bool Exists;
|
||||
|
||||
public ModBackup(Mod.Manager modManager, Mod mod)
|
||||
public ModBackup(ModManager modManager, Mod mod)
|
||||
{
|
||||
_modManager = modManager;
|
||||
_mod = mod;
|
||||
|
|
@ -24,9 +24,9 @@ public class ModBackup
|
|||
}
|
||||
|
||||
/// <summary> Migrate file extensions. </summary>
|
||||
public static void MigrateZipToPmp(Mod.Manager manager)
|
||||
public static void MigrateZipToPmp(ModManager modManager)
|
||||
{
|
||||
foreach (var mod in manager)
|
||||
foreach (var mod in modManager)
|
||||
{
|
||||
var pmpName = mod.ModPath + ".pmp";
|
||||
var zipName = mod.ModPath + ".zip";
|
||||
|
|
|
|||
|
|
@ -9,11 +9,11 @@ namespace Penumbra.Mods;
|
|||
public class ModFileEditor
|
||||
{
|
||||
private readonly ModFileCollection _files;
|
||||
private readonly Mod.Manager _modManager;
|
||||
private readonly ModManager _modManager;
|
||||
|
||||
public bool Changes { get; private set; }
|
||||
|
||||
public ModFileEditor(ModFileCollection files, Mod.Manager modManager)
|
||||
public ModFileEditor(ModFileCollection files, ModManager modManager)
|
||||
{
|
||||
_files = files;
|
||||
_modManager = modManager;
|
||||
|
|
@ -24,7 +24,7 @@ public class ModFileEditor
|
|||
Changes = false;
|
||||
}
|
||||
|
||||
public int Apply(Mod mod, Mod.SubMod option)
|
||||
public int Apply(Mod mod, SubMod option)
|
||||
{
|
||||
var dict = new Dictionary<Utf8GamePath, FullPath>();
|
||||
var num = 0;
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ namespace Penumbra.Mods;
|
|||
|
||||
public class ModMetaEditor
|
||||
{
|
||||
private readonly Mod.Manager _modManager;
|
||||
private readonly ModManager _modManager;
|
||||
|
||||
private readonly HashSet<ImcManipulation> _imc = new();
|
||||
private readonly HashSet<EqpManipulation> _eqp = new();
|
||||
|
|
@ -15,7 +15,7 @@ public class ModMetaEditor
|
|||
private readonly HashSet<EstManipulation> _est = new();
|
||||
private readonly HashSet<RspManipulation> _rsp = new();
|
||||
|
||||
public ModMetaEditor(Mod.Manager modManager)
|
||||
public ModMetaEditor(ModManager modManager)
|
||||
=> _modManager = modManager;
|
||||
|
||||
public bool Changes { get; private set; } = false;
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@ namespace Penumbra.Mods;
|
|||
|
||||
public class ModNormalizer
|
||||
{
|
||||
private readonly Mod.Manager _modManager;
|
||||
private readonly ModManager _modManager;
|
||||
private readonly List<List<Dictionary<Utf8GamePath, FullPath>>> _redirections = new();
|
||||
|
||||
public Mod Mod { get; private set; } = null!;
|
||||
|
|
@ -24,7 +24,7 @@ public class ModNormalizer
|
|||
public bool Running
|
||||
=> Step < TotalSteps;
|
||||
|
||||
public ModNormalizer(Mod.Manager modManager)
|
||||
public ModNormalizer(ModManager modManager)
|
||||
=> _modManager = modManager;
|
||||
|
||||
public void Normalize(Mod mod)
|
||||
|
|
@ -177,7 +177,7 @@ public class ModNormalizer
|
|||
_redirections[groupIdx + 1].Add(new Dictionary<Utf8GamePath, FullPath>());
|
||||
|
||||
var groupDir = Mod.Creator.CreateModFolder(directory, group.Name);
|
||||
foreach (var option in group.OfType<Mod.SubMod>())
|
||||
foreach (var option in group.OfType<SubMod>())
|
||||
{
|
||||
var optionDir = Mod.Creator.CreateModFolder(groupDir, option.Name);
|
||||
|
||||
|
|
@ -279,7 +279,7 @@ public class ModNormalizer
|
|||
|
||||
private void ApplyRedirections()
|
||||
{
|
||||
foreach (var option in Mod.AllSubMods.OfType<Mod.SubMod>())
|
||||
foreach (var option in Mod.AllSubMods.OfType<SubMod>())
|
||||
{
|
||||
_modManager.OptionSetFiles(Mod, option.GroupIdx, option.OptionIdx, _redirections[option.GroupIdx + 1][option.OptionIdx]);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,13 +5,13 @@ using Penumbra.Util;
|
|||
|
||||
public class ModSwapEditor
|
||||
{
|
||||
private readonly Mod.Manager _modManager;
|
||||
private readonly ModManager _modManager;
|
||||
private readonly Dictionary<Utf8GamePath, FullPath> _swaps = new();
|
||||
|
||||
public IReadOnlyDictionary<Utf8GamePath, FullPath> Swaps
|
||||
=> _swaps;
|
||||
|
||||
public ModSwapEditor(Mod.Manager modManager)
|
||||
public ModSwapEditor(ModManager modManager)
|
||||
=> _modManager = modManager;
|
||||
|
||||
public void Revert(ISubMod option)
|
||||
|
|
|
|||
|
|
@ -4,202 +4,199 @@ using System.Linq;
|
|||
|
||||
namespace Penumbra.Mods;
|
||||
|
||||
public partial class Mod
|
||||
public partial class ModManager
|
||||
{
|
||||
public partial class Manager
|
||||
public delegate void ModPathChangeDelegate(ModPathChangeType type, Mod mod, DirectoryInfo? oldDirectory,
|
||||
DirectoryInfo? newDirectory);
|
||||
|
||||
public event ModPathChangeDelegate ModPathChanged;
|
||||
|
||||
// Rename/Move a mod directory.
|
||||
// Updates all collection settings and sort order settings.
|
||||
public void MoveModDirectory(int idx, string newName)
|
||||
{
|
||||
public delegate void ModPathChangeDelegate(ModPathChangeType type, Mod mod, DirectoryInfo? oldDirectory,
|
||||
DirectoryInfo? newDirectory);
|
||||
var mod = this[idx];
|
||||
var oldName = mod.Name;
|
||||
var oldDirectory = mod.ModPath;
|
||||
|
||||
public event ModPathChangeDelegate ModPathChanged;
|
||||
|
||||
// Rename/Move a mod directory.
|
||||
// Updates all collection settings and sort order settings.
|
||||
public void MoveModDirectory(int idx, string newName)
|
||||
switch (NewDirectoryValid(oldDirectory.Name, newName, out var dir))
|
||||
{
|
||||
var mod = this[idx];
|
||||
var oldName = mod.Name;
|
||||
var oldDirectory = mod.ModPath;
|
||||
|
||||
switch (NewDirectoryValid(oldDirectory.Name, newName, out var dir))
|
||||
{
|
||||
case NewDirectoryState.NonExisting:
|
||||
// Nothing to do
|
||||
break;
|
||||
case NewDirectoryState.ExistsEmpty:
|
||||
try
|
||||
{
|
||||
Directory.Delete(dir!.FullName);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Penumbra.Log.Error($"Could not delete empty directory {dir!.FullName} to move {mod.Name} to it:\n{e}");
|
||||
return;
|
||||
}
|
||||
|
||||
break;
|
||||
// Should be caught beforehand.
|
||||
case NewDirectoryState.ExistsNonEmpty:
|
||||
case NewDirectoryState.ExistsAsFile:
|
||||
case NewDirectoryState.ContainsInvalidSymbols:
|
||||
// Nothing to do at all.
|
||||
case NewDirectoryState.Identical:
|
||||
default:
|
||||
return;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
Directory.Move(oldDirectory.FullName, dir!.FullName);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Penumbra.Log.Error($"Could not move {mod.Name} from {oldDirectory.Name} to {dir!.Name}:\n{e}");
|
||||
return;
|
||||
}
|
||||
|
||||
DataEditor.MoveDataFile(oldDirectory, dir);
|
||||
new ModBackup(this, mod).Move(null, dir.Name);
|
||||
|
||||
dir.Refresh();
|
||||
mod.ModPath = dir;
|
||||
if (!mod.Reload(this, false, out var metaChange))
|
||||
{
|
||||
Penumbra.Log.Error($"Error reloading moved mod {mod.Name}.");
|
||||
return;
|
||||
}
|
||||
|
||||
ModPathChanged.Invoke(ModPathChangeType.Moved, mod, oldDirectory, dir);
|
||||
if (metaChange != ModDataChangeType.None)
|
||||
_communicator.ModDataChanged.Invoke(metaChange, mod, oldName);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Reload a mod without changing its base directory.
|
||||
/// If the base directory does not exist anymore, the mod will be deleted.
|
||||
/// </summary>
|
||||
public void ReloadMod(int idx)
|
||||
{
|
||||
var mod = this[idx];
|
||||
var oldName = mod.Name;
|
||||
|
||||
ModPathChanged.Invoke(ModPathChangeType.StartingReload, mod, mod.ModPath, mod.ModPath);
|
||||
if (!mod.Reload(this, true, out var metaChange))
|
||||
{
|
||||
Penumbra.Log.Warning(mod.Name.Length == 0
|
||||
? $"Reloading mod {oldName} has failed, new name is empty. Deleting instead."
|
||||
: $"Reloading mod {oldName} failed, {mod.ModPath.FullName} does not exist anymore or it ha. Deleting instead.");
|
||||
|
||||
DeleteMod(idx);
|
||||
return;
|
||||
}
|
||||
|
||||
ModPathChanged.Invoke(ModPathChangeType.Reloaded, mod, mod.ModPath, mod.ModPath);
|
||||
if (metaChange != ModDataChangeType.None)
|
||||
_communicator.ModDataChanged.Invoke(metaChange, mod, oldName);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Delete a mod by its index. The event is invoked before the mod is removed from the list.
|
||||
/// Deletes from filesystem as well as from internal data.
|
||||
/// Updates indices of later mods.
|
||||
/// </summary>
|
||||
public void DeleteMod(int idx)
|
||||
{
|
||||
var mod = this[idx];
|
||||
if (Directory.Exists(mod.ModPath.FullName))
|
||||
case NewDirectoryState.NonExisting:
|
||||
// Nothing to do
|
||||
break;
|
||||
case NewDirectoryState.ExistsEmpty:
|
||||
try
|
||||
{
|
||||
Directory.Delete(mod.ModPath.FullName, true);
|
||||
Penumbra.Log.Debug($"Deleted directory {mod.ModPath.FullName} for {mod.Name}.");
|
||||
Directory.Delete(dir!.FullName);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Penumbra.Log.Error($"Could not delete the mod {mod.ModPath.Name}:\n{e}");
|
||||
Penumbra.Log.Error($"Could not delete empty directory {dir!.FullName} to move {mod.Name} to it:\n{e}");
|
||||
return;
|
||||
}
|
||||
|
||||
ModPathChanged.Invoke(ModPathChangeType.Deleted, mod, mod.ModPath, null);
|
||||
_mods.RemoveAt(idx);
|
||||
foreach (var remainingMod in _mods.Skip(idx))
|
||||
--remainingMod.Index;
|
||||
|
||||
Penumbra.Log.Debug($"Deleted mod {mod.Name}.");
|
||||
}
|
||||
|
||||
/// <summary> Load a new mod and add it to the manager if successful. </summary>
|
||||
public void AddMod(DirectoryInfo modFolder)
|
||||
{
|
||||
if (_mods.Any(m => m.ModPath.Name == modFolder.Name))
|
||||
break;
|
||||
// Should be caught beforehand.
|
||||
case NewDirectoryState.ExistsNonEmpty:
|
||||
case NewDirectoryState.ExistsAsFile:
|
||||
case NewDirectoryState.ContainsInvalidSymbols:
|
||||
// Nothing to do at all.
|
||||
case NewDirectoryState.Identical:
|
||||
default:
|
||||
return;
|
||||
|
||||
Creator.SplitMultiGroups(modFolder);
|
||||
var mod = LoadMod(this, modFolder, true);
|
||||
if (mod == null)
|
||||
return;
|
||||
|
||||
mod.Index = _mods.Count;
|
||||
_mods.Add(mod);
|
||||
ModPathChanged.Invoke(ModPathChangeType.Added, mod, null, mod.ModPath);
|
||||
Penumbra.Log.Debug($"Added new mod {mod.Name} from {modFolder.FullName}.");
|
||||
}
|
||||
|
||||
public enum NewDirectoryState
|
||||
try
|
||||
{
|
||||
NonExisting,
|
||||
ExistsEmpty,
|
||||
ExistsNonEmpty,
|
||||
ExistsAsFile,
|
||||
ContainsInvalidSymbols,
|
||||
Identical,
|
||||
Empty,
|
||||
Directory.Move(oldDirectory.FullName, dir!.FullName);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Penumbra.Log.Error($"Could not move {mod.Name} from {oldDirectory.Name} to {dir!.Name}:\n{e}");
|
||||
return;
|
||||
}
|
||||
|
||||
/// <summary> Return the state of the new potential name of a directory. </summary>
|
||||
public NewDirectoryState NewDirectoryValid(string oldName, string newName, out DirectoryInfo? directory)
|
||||
DataEditor.MoveDataFile(oldDirectory, dir);
|
||||
new ModBackup(this, mod).Move(null, dir.Name);
|
||||
|
||||
dir.Refresh();
|
||||
mod.ModPath = dir;
|
||||
if (!mod.Reload(this, false, out var metaChange))
|
||||
{
|
||||
directory = null;
|
||||
if (newName.Length == 0)
|
||||
return NewDirectoryState.Empty;
|
||||
|
||||
if (oldName == newName)
|
||||
return NewDirectoryState.Identical;
|
||||
|
||||
var fixedNewName = Creator.ReplaceBadXivSymbols(newName);
|
||||
if (fixedNewName != newName)
|
||||
return NewDirectoryState.ContainsInvalidSymbols;
|
||||
|
||||
directory = new DirectoryInfo(Path.Combine(BasePath.FullName, fixedNewName));
|
||||
if (File.Exists(directory.FullName))
|
||||
return NewDirectoryState.ExistsAsFile;
|
||||
|
||||
if (!Directory.Exists(directory.FullName))
|
||||
return NewDirectoryState.NonExisting;
|
||||
|
||||
if (directory.EnumerateFileSystemInfos().Any())
|
||||
return NewDirectoryState.ExistsNonEmpty;
|
||||
|
||||
return NewDirectoryState.ExistsEmpty;
|
||||
Penumbra.Log.Error($"Error reloading moved mod {mod.Name}.");
|
||||
return;
|
||||
}
|
||||
|
||||
ModPathChanged.Invoke(ModPathChangeType.Moved, mod, oldDirectory, dir);
|
||||
if (metaChange != ModDataChangeType.None)
|
||||
_communicator.ModDataChanged.Invoke(metaChange, mod, oldName);
|
||||
}
|
||||
|
||||
/// <summary> Add new mods to NewMods and remove deleted mods from NewMods. </summary>
|
||||
private void OnModPathChange(ModPathChangeType type, Mod mod, DirectoryInfo? oldDirectory,
|
||||
DirectoryInfo? newDirectory)
|
||||
/// <summary>
|
||||
/// Reload a mod without changing its base directory.
|
||||
/// If the base directory does not exist anymore, the mod will be deleted.
|
||||
/// </summary>
|
||||
public void ReloadMod(int idx)
|
||||
{
|
||||
var mod = this[idx];
|
||||
var oldName = mod.Name;
|
||||
|
||||
ModPathChanged.Invoke(ModPathChangeType.StartingReload, mod, mod.ModPath, mod.ModPath);
|
||||
if (!mod.Reload(this, true, out var metaChange))
|
||||
{
|
||||
switch (type)
|
||||
Penumbra.Log.Warning(mod.Name.Length == 0
|
||||
? $"Reloading mod {oldName} has failed, new name is empty. Deleting instead."
|
||||
: $"Reloading mod {oldName} failed, {mod.ModPath.FullName} does not exist anymore or it ha. Deleting instead.");
|
||||
|
||||
DeleteMod(idx);
|
||||
return;
|
||||
}
|
||||
|
||||
ModPathChanged.Invoke(ModPathChangeType.Reloaded, mod, mod.ModPath, mod.ModPath);
|
||||
if (metaChange != ModDataChangeType.None)
|
||||
_communicator.ModDataChanged.Invoke(metaChange, mod, oldName);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Delete a mod by its index. The event is invoked before the mod is removed from the list.
|
||||
/// Deletes from filesystem as well as from internal data.
|
||||
/// Updates indices of later mods.
|
||||
/// </summary>
|
||||
public void DeleteMod(int idx)
|
||||
{
|
||||
var mod = this[idx];
|
||||
if (Directory.Exists(mod.ModPath.FullName))
|
||||
try
|
||||
{
|
||||
case ModPathChangeType.Added:
|
||||
NewMods.Add(mod);
|
||||
break;
|
||||
case ModPathChangeType.Deleted:
|
||||
NewMods.Remove(mod);
|
||||
break;
|
||||
case ModPathChangeType.Moved:
|
||||
if (oldDirectory != null && newDirectory != null)
|
||||
DataEditor.MoveDataFile(oldDirectory, newDirectory);
|
||||
|
||||
break;
|
||||
Directory.Delete(mod.ModPath.FullName, true);
|
||||
Penumbra.Log.Debug($"Deleted directory {mod.ModPath.FullName} for {mod.Name}.");
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Penumbra.Log.Error($"Could not delete the mod {mod.ModPath.Name}:\n{e}");
|
||||
}
|
||||
|
||||
ModPathChanged.Invoke(ModPathChangeType.Deleted, mod, mod.ModPath, null);
|
||||
_mods.RemoveAt(idx);
|
||||
foreach (var remainingMod in _mods.Skip(idx))
|
||||
--remainingMod.Index;
|
||||
|
||||
Penumbra.Log.Debug($"Deleted mod {mod.Name}.");
|
||||
}
|
||||
|
||||
/// <summary> Load a new mod and add it to the manager if successful. </summary>
|
||||
public void AddMod(DirectoryInfo modFolder)
|
||||
{
|
||||
if (_mods.Any(m => m.ModPath.Name == modFolder.Name))
|
||||
return;
|
||||
|
||||
Mod.Creator.SplitMultiGroups(modFolder);
|
||||
var mod = Mod.LoadMod(this, modFolder, true);
|
||||
if (mod == null)
|
||||
return;
|
||||
|
||||
mod.Index = _mods.Count;
|
||||
_mods.Add(mod);
|
||||
ModPathChanged.Invoke(ModPathChangeType.Added, mod, null, mod.ModPath);
|
||||
Penumbra.Log.Debug($"Added new mod {mod.Name} from {modFolder.FullName}.");
|
||||
}
|
||||
|
||||
public enum NewDirectoryState
|
||||
{
|
||||
NonExisting,
|
||||
ExistsEmpty,
|
||||
ExistsNonEmpty,
|
||||
ExistsAsFile,
|
||||
ContainsInvalidSymbols,
|
||||
Identical,
|
||||
Empty,
|
||||
}
|
||||
|
||||
/// <summary> Return the state of the new potential name of a directory. </summary>
|
||||
public NewDirectoryState NewDirectoryValid(string oldName, string newName, out DirectoryInfo? directory)
|
||||
{
|
||||
directory = null;
|
||||
if (newName.Length == 0)
|
||||
return NewDirectoryState.Empty;
|
||||
|
||||
if (oldName == newName)
|
||||
return NewDirectoryState.Identical;
|
||||
|
||||
var fixedNewName = Mod.Creator.ReplaceBadXivSymbols(newName);
|
||||
if (fixedNewName != newName)
|
||||
return NewDirectoryState.ContainsInvalidSymbols;
|
||||
|
||||
directory = new DirectoryInfo(Path.Combine(BasePath.FullName, fixedNewName));
|
||||
if (File.Exists(directory.FullName))
|
||||
return NewDirectoryState.ExistsAsFile;
|
||||
|
||||
if (!Directory.Exists(directory.FullName))
|
||||
return NewDirectoryState.NonExisting;
|
||||
|
||||
if (directory.EnumerateFileSystemInfos().Any())
|
||||
return NewDirectoryState.ExistsNonEmpty;
|
||||
|
||||
return NewDirectoryState.ExistsEmpty;
|
||||
}
|
||||
|
||||
|
||||
/// <summary> Add new mods to NewMods and remove deleted mods from NewMods. </summary>
|
||||
private void OnModPathChange(ModPathChangeType type, Mod mod, DirectoryInfo? oldDirectory,
|
||||
DirectoryInfo? newDirectory)
|
||||
{
|
||||
switch (type)
|
||||
{
|
||||
case ModPathChangeType.Added:
|
||||
NewMods.Add(mod);
|
||||
break;
|
||||
case ModPathChangeType.Deleted:
|
||||
NewMods.Remove(mod);
|
||||
break;
|
||||
case ModPathChangeType.Moved:
|
||||
if (oldDirectory != null && newDirectory != null)
|
||||
DataEditor.MoveDataFile(oldDirectory, newDirectory);
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,12 +0,0 @@
|
|||
using System;
|
||||
using System.Linq;
|
||||
|
||||
namespace Penumbra.Mods;
|
||||
|
||||
public sealed partial class Mod
|
||||
{
|
||||
public partial class Manager
|
||||
{
|
||||
|
||||
}
|
||||
}
|
||||
|
|
@ -11,371 +11,367 @@ using Penumbra.Util;
|
|||
|
||||
namespace Penumbra.Mods;
|
||||
|
||||
public sealed partial class Mod
|
||||
public sealed partial class ModManager
|
||||
{
|
||||
public sealed partial class Manager
|
||||
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)
|
||||
{
|
||||
public delegate void ModOptionChangeDelegate(ModOptionChangeType type, Mod mod, int groupIdx, int optionIdx, int movedToIdx);
|
||||
public event ModOptionChangeDelegate ModOptionChanged;
|
||||
var group = mod._groups[groupIdx];
|
||||
if (group.Type == type)
|
||||
return;
|
||||
|
||||
public void ChangeModGroupType(Mod mod, int groupIdx, GroupType type)
|
||||
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
|
||||
{
|
||||
var group = mod._groups[groupIdx];
|
||||
if (group.Type == type)
|
||||
return;
|
||||
SingleModGroup s => s.Name = newName,
|
||||
MultiModGroup m => m.Name = newName,
|
||||
_ => newName,
|
||||
};
|
||||
|
||||
mod._groups[groupIdx] = group.Convert(type);
|
||||
ModOptionChanged.Invoke(ModOptionChangeType.GroupTypeChanged, mod, groupIdx, -1, -1);
|
||||
}
|
||||
ModOptionChanged.Invoke(ModOptionChangeType.GroupRenamed, mod, groupIdx, -1, -1);
|
||||
}
|
||||
|
||||
public void ChangeModGroupDefaultOption(Mod mod, int groupIdx, uint defaultOption)
|
||||
{
|
||||
var group = mod._groups[groupIdx];
|
||||
if (group.DefaultSettings == defaultOption)
|
||||
return;
|
||||
public void AddModGroup(Mod mod, GroupType type, string newName)
|
||||
{
|
||||
if (!VerifyFileName(mod, null, newName, true))
|
||||
return;
|
||||
|
||||
group.DefaultSettings = defaultOption;
|
||||
ModOptionChanged.Invoke(ModOptionChangeType.DefaultOptionChanged, mod, groupIdx, -1, -1);
|
||||
}
|
||||
var maxPriority = mod._groups.Count == 0 ? 0 : mod._groups.Max(o => o.Priority) + 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
|
||||
mod._groups.Add(type == GroupType.Multi
|
||||
? new MultiModGroup
|
||||
{
|
||||
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);
|
||||
Name = newName,
|
||||
Priority = maxPriority,
|
||||
}
|
||||
}
|
||||
|
||||
private static void UpdateSubModPositions(Mod mod, int fromGroup)
|
||||
{
|
||||
foreach (var (group, groupIdx) in mod._groups.WithIndex().Skip(fromGroup))
|
||||
: new SingleModGroup
|
||||
{
|
||||
foreach (var (o, optionIdx) in group.OfType<SubMod>().WithIndex())
|
||||
o.SetPosition(groupIdx, optionIdx);
|
||||
}
|
||||
}
|
||||
Name = newName,
|
||||
Priority = maxPriority,
|
||||
});
|
||||
ModOptionChanged.Invoke(ModOptionChangeType.GroupAdded, mod, mod._groups.Count - 1, -1, -1);
|
||||
}
|
||||
|
||||
public void ChangeGroupDescription(Mod mod, int groupIdx, string newDescription)
|
||||
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))
|
||||
{
|
||||
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,
|
||||
};
|
||||
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,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -6,169 +6,144 @@ using System.Threading.Tasks;
|
|||
|
||||
namespace Penumbra.Mods;
|
||||
|
||||
public sealed partial class Mod
|
||||
public sealed partial class ModManager
|
||||
{
|
||||
public sealed partial class Manager
|
||||
public DirectoryInfo BasePath { get; private set; } = null!;
|
||||
private DirectoryInfo? _exportDirectory;
|
||||
|
||||
public DirectoryInfo ExportDirectory
|
||||
=> _exportDirectory ?? BasePath;
|
||||
|
||||
public bool Valid { get; private set; }
|
||||
|
||||
public event Action? ModDiscoveryStarted;
|
||||
public event Action? ModDiscoveryFinished;
|
||||
public event Action<string, bool> ModDirectoryChanged;
|
||||
|
||||
// Change the mod base directory and discover available mods.
|
||||
public void DiscoverMods(string newDir)
|
||||
{
|
||||
public DirectoryInfo BasePath { get; private set; } = null!;
|
||||
private DirectoryInfo? _exportDirectory;
|
||||
SetBaseDirectory(newDir, false);
|
||||
DiscoverMods();
|
||||
}
|
||||
|
||||
public DirectoryInfo ExportDirectory
|
||||
=> _exportDirectory ?? BasePath;
|
||||
// Set the mod base directory.
|
||||
// If its not the first time, check if it is the same directory as before.
|
||||
// Also checks if the directory is available and tries to create it if it is not.
|
||||
private void SetBaseDirectory(string newPath, bool firstTime)
|
||||
{
|
||||
if (!firstTime && string.Equals(newPath, Penumbra.Config.ModDirectory, StringComparison.OrdinalIgnoreCase))
|
||||
return;
|
||||
|
||||
public bool Valid { get; private set; }
|
||||
|
||||
public event Action? ModDiscoveryStarted;
|
||||
public event Action? ModDiscoveryFinished;
|
||||
public event Action< string, bool > ModDirectoryChanged;
|
||||
|
||||
// Change the mod base directory and discover available mods.
|
||||
public void DiscoverMods( string newDir )
|
||||
if (newPath.Length == 0)
|
||||
{
|
||||
SetBaseDirectory( newDir, false );
|
||||
DiscoverMods();
|
||||
Valid = false;
|
||||
BasePath = new DirectoryInfo(".");
|
||||
if (Penumbra.Config.ModDirectory != BasePath.FullName)
|
||||
ModDirectoryChanged.Invoke(string.Empty, false);
|
||||
}
|
||||
|
||||
// Set the mod base directory.
|
||||
// If its not the first time, check if it is the same directory as before.
|
||||
// Also checks if the directory is available and tries to create it if it is not.
|
||||
private void SetBaseDirectory( string newPath, bool firstTime )
|
||||
else
|
||||
{
|
||||
if( !firstTime && string.Equals( newPath, Penumbra.Config.ModDirectory, StringComparison.OrdinalIgnoreCase ) )
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if( newPath.Length == 0 )
|
||||
{
|
||||
Valid = false;
|
||||
BasePath = new DirectoryInfo( "." );
|
||||
if( Penumbra.Config.ModDirectory != BasePath.FullName )
|
||||
{
|
||||
ModDirectoryChanged.Invoke( string.Empty, false );
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
var newDir = new DirectoryInfo( newPath );
|
||||
if( !newDir.Exists )
|
||||
{
|
||||
try
|
||||
{
|
||||
Directory.CreateDirectory( newDir.FullName );
|
||||
newDir.Refresh();
|
||||
}
|
||||
catch( Exception e )
|
||||
{
|
||||
Penumbra.Log.Error( $"Could not create specified mod directory {newDir.FullName}:\n{e}" );
|
||||
}
|
||||
}
|
||||
|
||||
BasePath = newDir;
|
||||
Valid = Directory.Exists( newDir.FullName );
|
||||
if( Penumbra.Config.ModDirectory != BasePath.FullName )
|
||||
{
|
||||
ModDirectoryChanged.Invoke( BasePath.FullName, Valid );
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static void OnModDirectoryChange( string newPath, bool _ )
|
||||
{
|
||||
Penumbra.Log.Information( $"Set new mod base directory from {Penumbra.Config.ModDirectory} to {newPath}." );
|
||||
Penumbra.Config.ModDirectory = newPath;
|
||||
Penumbra.Config.Save();
|
||||
}
|
||||
|
||||
// Discover new mods.
|
||||
public void DiscoverMods()
|
||||
{
|
||||
NewMods.Clear();
|
||||
ModDiscoveryStarted?.Invoke();
|
||||
_mods.Clear();
|
||||
BasePath.Refresh();
|
||||
|
||||
if( Valid && BasePath.Exists )
|
||||
{
|
||||
var options = new ParallelOptions()
|
||||
{
|
||||
MaxDegreeOfParallelism = Environment.ProcessorCount / 2,
|
||||
};
|
||||
var queue = new ConcurrentQueue< Mod >();
|
||||
Parallel.ForEach( BasePath.EnumerateDirectories(), options, dir =>
|
||||
{
|
||||
var mod = LoadMod( this, dir, false );
|
||||
if( mod != null )
|
||||
{
|
||||
queue.Enqueue( mod );
|
||||
}
|
||||
} );
|
||||
|
||||
foreach( var mod in queue )
|
||||
{
|
||||
mod.Index = _mods.Count;
|
||||
_mods.Add( mod );
|
||||
}
|
||||
}
|
||||
|
||||
ModDiscoveryFinished?.Invoke();
|
||||
Penumbra.Log.Information( "Rediscovered mods." );
|
||||
|
||||
if( MigrateModBackups )
|
||||
{
|
||||
ModBackup.MigrateZipToPmp( this );
|
||||
}
|
||||
}
|
||||
|
||||
public void UpdateExportDirectory( string newDirectory, bool change )
|
||||
{
|
||||
if( newDirectory.Length == 0 )
|
||||
{
|
||||
if( _exportDirectory == null )
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_exportDirectory = null;
|
||||
_config.ExportDirectory = string.Empty;
|
||||
_config.Save();
|
||||
return;
|
||||
}
|
||||
|
||||
var dir = new DirectoryInfo( newDirectory );
|
||||
if( dir.FullName.Equals( _exportDirectory?.FullName, StringComparison.OrdinalIgnoreCase ) )
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if( !dir.Exists )
|
||||
{
|
||||
var newDir = new DirectoryInfo(newPath);
|
||||
if (!newDir.Exists)
|
||||
try
|
||||
{
|
||||
Directory.CreateDirectory( dir.FullName );
|
||||
Directory.CreateDirectory(newDir.FullName);
|
||||
newDir.Refresh();
|
||||
}
|
||||
catch( Exception e )
|
||||
catch (Exception e)
|
||||
{
|
||||
Penumbra.Log.Error( $"Could not create Export Directory:\n{e}" );
|
||||
return;
|
||||
Penumbra.Log.Error($"Could not create specified mod directory {newDir.FullName}:\n{e}");
|
||||
}
|
||||
}
|
||||
|
||||
if( change )
|
||||
{
|
||||
foreach( var mod in _mods )
|
||||
{
|
||||
new ModBackup( this, mod ).Move( dir.FullName );
|
||||
}
|
||||
}
|
||||
|
||||
_exportDirectory = dir;
|
||||
|
||||
if( change )
|
||||
{
|
||||
_config.ExportDirectory = dir.FullName;
|
||||
_config.Save();
|
||||
}
|
||||
BasePath = newDir;
|
||||
Valid = Directory.Exists(newDir.FullName);
|
||||
if (Penumbra.Config.ModDirectory != BasePath.FullName)
|
||||
ModDirectoryChanged.Invoke(BasePath.FullName, Valid);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static void OnModDirectoryChange(string newPath, bool _)
|
||||
{
|
||||
Penumbra.Log.Information($"Set new mod base directory from {Penumbra.Config.ModDirectory} to {newPath}.");
|
||||
Penumbra.Config.ModDirectory = newPath;
|
||||
Penumbra.Config.Save();
|
||||
}
|
||||
|
||||
// Discover new mods.
|
||||
public void DiscoverMods()
|
||||
{
|
||||
NewMods.Clear();
|
||||
ModDiscoveryStarted?.Invoke();
|
||||
_mods.Clear();
|
||||
BasePath.Refresh();
|
||||
|
||||
if (Valid && BasePath.Exists)
|
||||
{
|
||||
var options = new ParallelOptions()
|
||||
{
|
||||
MaxDegreeOfParallelism = Environment.ProcessorCount / 2,
|
||||
};
|
||||
var queue = new ConcurrentQueue<Mod>();
|
||||
Parallel.ForEach(BasePath.EnumerateDirectories(), options, dir =>
|
||||
{
|
||||
var mod = Mod.LoadMod(this, dir, false);
|
||||
if (mod != null)
|
||||
queue.Enqueue(mod);
|
||||
});
|
||||
|
||||
foreach (var mod in queue)
|
||||
{
|
||||
mod.Index = _mods.Count;
|
||||
_mods.Add(mod);
|
||||
}
|
||||
}
|
||||
|
||||
ModDiscoveryFinished?.Invoke();
|
||||
Penumbra.Log.Information("Rediscovered mods.");
|
||||
|
||||
if (MigrateModBackups)
|
||||
ModBackup.MigrateZipToPmp(this);
|
||||
}
|
||||
|
||||
public void UpdateExportDirectory(string newDirectory, bool change)
|
||||
{
|
||||
if (newDirectory.Length == 0)
|
||||
{
|
||||
if (_exportDirectory == null)
|
||||
return;
|
||||
|
||||
_exportDirectory = null;
|
||||
_config.ExportDirectory = string.Empty;
|
||||
_config.Save();
|
||||
return;
|
||||
}
|
||||
|
||||
var dir = new DirectoryInfo(newDirectory);
|
||||
if (dir.FullName.Equals(_exportDirectory?.FullName, StringComparison.OrdinalIgnoreCase))
|
||||
return;
|
||||
|
||||
if (!dir.Exists)
|
||||
try
|
||||
{
|
||||
Directory.CreateDirectory(dir.FullName);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Penumbra.Log.Error($"Could not create Export Directory:\n{e}");
|
||||
return;
|
||||
}
|
||||
|
||||
if (change)
|
||||
foreach (var mod in _mods)
|
||||
new ModBackup(this, mod).Move(dir.FullName);
|
||||
|
||||
_exportDirectory = dir;
|
||||
|
||||
if (change)
|
||||
{
|
||||
_config.ExportDirectory = dir.FullName;
|
||||
_config.Save();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -7,73 +7,70 @@ using Penumbra.Util;
|
|||
|
||||
namespace Penumbra.Mods;
|
||||
|
||||
public sealed partial class Mod
|
||||
public sealed partial class ModManager : IReadOnlyList<Mod>
|
||||
{
|
||||
public sealed partial class Manager : IReadOnlyList<Mod>
|
||||
// Set when reading Config and migrating from v4 to v5.
|
||||
public static bool MigrateModBackups = false;
|
||||
|
||||
// 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.
|
||||
public readonly HashSet<Mod> NewMods = new();
|
||||
|
||||
private readonly List<Mod> _mods = 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();
|
||||
|
||||
private readonly Configuration _config;
|
||||
private readonly CommunicatorService _communicator;
|
||||
public readonly ModDataEditor DataEditor;
|
||||
|
||||
public ModManager(StartTracker time, Configuration config, CommunicatorService communicator, ModDataEditor dataEditor)
|
||||
{
|
||||
// Set when reading Config and migrating from v4 to v5.
|
||||
public static bool MigrateModBackups = false;
|
||||
using var timer = time.Measure(StartTimeType.Mods);
|
||||
_config = config;
|
||||
_communicator = communicator;
|
||||
DataEditor = dataEditor;
|
||||
ModDirectoryChanged += OnModDirectoryChange;
|
||||
SetBaseDirectory(config.ModDirectory, true);
|
||||
UpdateExportDirectory(_config.ExportDirectory, false);
|
||||
ModOptionChanged += OnModOptionChange;
|
||||
ModPathChanged += OnModPathChange;
|
||||
DiscoverMods();
|
||||
}
|
||||
|
||||
// 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.
|
||||
public readonly HashSet<Mod> NewMods = new();
|
||||
|
||||
private readonly List<Mod> _mods = 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();
|
||||
|
||||
private readonly Configuration _config;
|
||||
private readonly CommunicatorService _communicator;
|
||||
public readonly ModDataEditor DataEditor;
|
||||
|
||||
public Manager(StartTracker time, Configuration config, CommunicatorService communicator, ModDataEditor dataEditor)
|
||||
// Try to obtain a mod by its directory name (unique identifier, preferred),
|
||||
// or the first mod of the given name if no directory fits.
|
||||
public bool TryGetMod(string modDirectory, string modName, [NotNullWhen(true)] out Mod? mod)
|
||||
{
|
||||
mod = null;
|
||||
foreach (var m in _mods)
|
||||
{
|
||||
using var timer = time.Measure(StartTimeType.Mods);
|
||||
_config = config;
|
||||
_communicator = communicator;
|
||||
DataEditor = dataEditor;
|
||||
ModDirectoryChanged += OnModDirectoryChange;
|
||||
SetBaseDirectory(config.ModDirectory, true);
|
||||
UpdateExportDirectory(_config.ExportDirectory, false);
|
||||
ModOptionChanged += OnModOptionChange;
|
||||
ModPathChanged += OnModPathChange;
|
||||
DiscoverMods();
|
||||
}
|
||||
|
||||
|
||||
// Try to obtain a mod by its directory name (unique identifier, preferred),
|
||||
// or the first mod of the given name if no directory fits.
|
||||
public bool TryGetMod(string modDirectory, string modName, [NotNullWhen(true)] out Mod? mod)
|
||||
{
|
||||
mod = null;
|
||||
foreach (var m in _mods)
|
||||
if (string.Equals(m.ModPath.Name, modDirectory, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
if (string.Equals(m.ModPath.Name, modDirectory, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
mod = m;
|
||||
return true;
|
||||
}
|
||||
|
||||
if (m.Name == modName)
|
||||
mod ??= m;
|
||||
mod = m;
|
||||
return true;
|
||||
}
|
||||
|
||||
return mod != null;
|
||||
if (m.Name == modName)
|
||||
mod ??= m;
|
||||
}
|
||||
|
||||
return mod != null;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,7 +2,6 @@ using System;
|
|||
using System.IO;
|
||||
using System.Linq;
|
||||
using Dalamud.Utility;
|
||||
using Newtonsoft.Json;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using OtterGui.Classes;
|
||||
using Penumbra.Services;
|
||||
|
|
|
|||
|
|
@ -15,10 +15,10 @@ public enum ModPathChangeType
|
|||
|
||||
public partial class Mod
|
||||
{
|
||||
public DirectoryInfo ModPath { get; private set; }
|
||||
public DirectoryInfo ModPath { get; internal set; }
|
||||
public string Identifier
|
||||
=> Index >= 0 ? ModPath.Name : Name;
|
||||
public int Index { get; private set; } = -1;
|
||||
public int Index { get; internal set; } = -1;
|
||||
|
||||
public bool IsTemporary
|
||||
=> Index < 0;
|
||||
|
|
@ -33,7 +33,7 @@ public partial class Mod
|
|||
_default = new SubMod( this );
|
||||
}
|
||||
|
||||
private static Mod? LoadMod( Manager modManager, DirectoryInfo modPath, bool incorporateMetaChanges )
|
||||
public static Mod? LoadMod( ModManager modManager, DirectoryInfo modPath, bool incorporateMetaChanges )
|
||||
{
|
||||
modPath.Refresh();
|
||||
if( !modPath.Exists )
|
||||
|
|
@ -52,7 +52,7 @@ public partial class Mod
|
|||
|
||||
}
|
||||
|
||||
internal bool Reload(Manager modManager, bool incorporateMetaChanges, out ModDataChangeType modDataChange )
|
||||
internal bool Reload(ModManager modManager, bool incorporateMetaChanges, out ModDataChangeType modDataChange )
|
||||
{
|
||||
modDataChange = ModDataChangeType.Deletion;
|
||||
ModPath.Refresh();
|
||||
|
|
|
|||
|
|
@ -12,7 +12,7 @@ public sealed partial class Mod
|
|||
public SortedList< string, object? > ChangedItems { get; } = new();
|
||||
public string LowerChangedItemsString { get; private set; } = string.Empty;
|
||||
|
||||
private void ComputeChangedItems()
|
||||
internal void ComputeChangedItems()
|
||||
{
|
||||
ChangedItems.Clear();
|
||||
foreach( var gamePath in AllRedirects )
|
||||
|
|
|
|||
|
|
@ -18,15 +18,15 @@ public partial class Mod
|
|||
public IReadOnlyList< IModGroup > Groups
|
||||
=> _groups;
|
||||
|
||||
private readonly SubMod _default;
|
||||
private readonly List< IModGroup > _groups = new();
|
||||
internal readonly SubMod _default;
|
||||
internal readonly List< IModGroup > _groups = new();
|
||||
|
||||
public int TotalFileCount { get; private set; }
|
||||
public int TotalSwapCount { get; private set; }
|
||||
public int TotalManipulations { get; private set; }
|
||||
public bool HasOptions { get; private set; }
|
||||
public int TotalFileCount { get; internal set; }
|
||||
public int TotalSwapCount { get; internal set; }
|
||||
public int TotalManipulations { get; internal set; }
|
||||
public bool HasOptions { get; internal set; }
|
||||
|
||||
private bool SetCounts()
|
||||
internal bool SetCounts()
|
||||
{
|
||||
TotalFileCount = 0;
|
||||
TotalSwapCount = 0;
|
||||
|
|
@ -120,7 +120,7 @@ public partial class Mod
|
|||
|
||||
// Delete all existing group files and save them anew.
|
||||
// Used when indices change in complex ways.
|
||||
private void SaveAllGroups()
|
||||
internal void SaveAllGroups()
|
||||
{
|
||||
foreach( var file in GroupFiles )
|
||||
{
|
||||
|
|
|
|||
|
|
@ -12,12 +12,12 @@ namespace Penumbra.Mods;
|
|||
|
||||
public sealed class ModFileSystem : FileSystem<Mod>, IDisposable, ISavable
|
||||
{
|
||||
private readonly Mod.Manager _modManager;
|
||||
private readonly ModManager _modManager;
|
||||
private readonly CommunicatorService _communicator;
|
||||
private readonly FilenameService _files;
|
||||
|
||||
// Create a new ModFileSystem from the currently loaded mods and the current sort order file.
|
||||
public ModFileSystem(Mod.Manager modManager, CommunicatorService communicator, FilenameService files)
|
||||
public ModFileSystem(ModManager modManager, CommunicatorService communicator, FilenameService files)
|
||||
{
|
||||
_modManager = modManager;
|
||||
_communicator = communicator;
|
||||
|
|
|
|||
|
|
@ -15,103 +15,105 @@ namespace Penumbra.Mods;
|
|||
|
||||
public partial class Mod
|
||||
{
|
||||
// Groups that allow all available options to be selected at once.
|
||||
private sealed class MultiModGroup : IModGroup
|
||||
|
||||
}
|
||||
|
||||
/// <summary> Groups that allow all available options to be selected at once. </summary>
|
||||
public sealed class MultiModGroup : IModGroup
|
||||
{
|
||||
public GroupType Type
|
||||
=> GroupType.Multi;
|
||||
|
||||
public string Name { get; set; } = "Group";
|
||||
public string Description { get; set; } = "A non-exclusive group of settings.";
|
||||
public int Priority { get; set; }
|
||||
public uint DefaultSettings { get; set; }
|
||||
|
||||
public int OptionPriority(Index idx)
|
||||
=> PrioritizedOptions[idx].Priority;
|
||||
|
||||
public ISubMod this[Index idx]
|
||||
=> PrioritizedOptions[idx].Mod;
|
||||
|
||||
[JsonIgnore]
|
||||
public int Count
|
||||
=> PrioritizedOptions.Count;
|
||||
|
||||
public readonly List<(SubMod Mod, int Priority)> PrioritizedOptions = new();
|
||||
|
||||
public IEnumerator<ISubMod> GetEnumerator()
|
||||
=> PrioritizedOptions.Select(o => o.Mod).GetEnumerator();
|
||||
|
||||
IEnumerator IEnumerable.GetEnumerator()
|
||||
=> GetEnumerator();
|
||||
|
||||
public static MultiModGroup? Load(Mod mod, JObject json, int groupIdx)
|
||||
{
|
||||
public GroupType Type
|
||||
=> GroupType.Multi;
|
||||
|
||||
public string Name { get; set; } = "Group";
|
||||
public string Description { get; set; } = "A non-exclusive group of settings.";
|
||||
public int Priority { get; set; }
|
||||
public uint DefaultSettings { get; set; }
|
||||
|
||||
public int OptionPriority(Index idx)
|
||||
=> PrioritizedOptions[idx].Priority;
|
||||
|
||||
public ISubMod this[Index idx]
|
||||
=> PrioritizedOptions[idx].Mod;
|
||||
|
||||
[JsonIgnore]
|
||||
public int Count
|
||||
=> PrioritizedOptions.Count;
|
||||
|
||||
public readonly List<(SubMod Mod, int Priority)> PrioritizedOptions = new();
|
||||
|
||||
public IEnumerator<ISubMod> GetEnumerator()
|
||||
=> PrioritizedOptions.Select(o => o.Mod).GetEnumerator();
|
||||
|
||||
IEnumerator IEnumerable.GetEnumerator()
|
||||
=> GetEnumerator();
|
||||
|
||||
public static MultiModGroup? Load(Mod mod, JObject json, int groupIdx)
|
||||
var ret = new MultiModGroup()
|
||||
{
|
||||
var ret = new MultiModGroup()
|
||||
Name = json[nameof(Name)]?.ToObject<string>() ?? string.Empty,
|
||||
Description = json[nameof(Description)]?.ToObject<string>() ?? string.Empty,
|
||||
Priority = json[nameof(Priority)]?.ToObject<int>() ?? 0,
|
||||
DefaultSettings = json[nameof(DefaultSettings)]?.ToObject<uint>() ?? 0,
|
||||
};
|
||||
if (ret.Name.Length == 0)
|
||||
return null;
|
||||
|
||||
var options = json["Options"];
|
||||
if (options != null)
|
||||
foreach (var child in options.Children())
|
||||
{
|
||||
Name = json[nameof(Name)]?.ToObject<string>() ?? string.Empty,
|
||||
Description = json[nameof(Description)]?.ToObject<string>() ?? string.Empty,
|
||||
Priority = json[nameof(Priority)]?.ToObject<int>() ?? 0,
|
||||
DefaultSettings = json[nameof(DefaultSettings)]?.ToObject<uint>() ?? 0,
|
||||
};
|
||||
if (ret.Name.Length == 0)
|
||||
return null;
|
||||
|
||||
var options = json["Options"];
|
||||
if (options != null)
|
||||
foreach (var child in options.Children())
|
||||
if (ret.PrioritizedOptions.Count == IModGroup.MaxMultiOptions)
|
||||
{
|
||||
if (ret.PrioritizedOptions.Count == IModGroup.MaxMultiOptions)
|
||||
{
|
||||
Penumbra.ChatService.NotificationMessage(
|
||||
$"Multi Group {ret.Name} has more than {IModGroup.MaxMultiOptions} options, ignoring excessive options.", "Warning",
|
||||
NotificationType.Warning);
|
||||
break;
|
||||
}
|
||||
|
||||
var subMod = new SubMod(mod);
|
||||
subMod.SetPosition(groupIdx, ret.PrioritizedOptions.Count);
|
||||
subMod.Load(mod.ModPath, child, out var priority);
|
||||
ret.PrioritizedOptions.Add((subMod, priority));
|
||||
Penumbra.ChatService.NotificationMessage(
|
||||
$"Multi Group {ret.Name} has more than {IModGroup.MaxMultiOptions} options, ignoring excessive options.", "Warning",
|
||||
NotificationType.Warning);
|
||||
break;
|
||||
}
|
||||
|
||||
ret.DefaultSettings = (uint)(ret.DefaultSettings & ((1ul << ret.Count) - 1));
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
public IModGroup Convert(GroupType type)
|
||||
{
|
||||
switch (type)
|
||||
{
|
||||
case GroupType.Multi: return this;
|
||||
case GroupType.Single:
|
||||
var multi = new SingleModGroup()
|
||||
{
|
||||
Name = Name,
|
||||
Description = Description,
|
||||
Priority = Priority,
|
||||
DefaultSettings = (uint)Math.Max(Math.Min(Count - 1, BitOperations.TrailingZeroCount(DefaultSettings)), 0),
|
||||
};
|
||||
multi.OptionData.AddRange(PrioritizedOptions.Select(p => p.Mod));
|
||||
return multi;
|
||||
default: throw new ArgumentOutOfRangeException(nameof(type), type, null);
|
||||
var subMod = new SubMod(mod);
|
||||
subMod.SetPosition(groupIdx, ret.PrioritizedOptions.Count);
|
||||
subMod.Load(mod.ModPath, child, out var priority);
|
||||
ret.PrioritizedOptions.Add((subMod, priority));
|
||||
}
|
||||
}
|
||||
|
||||
public bool MoveOption(int optionIdxFrom, int optionIdxTo)
|
||||
ret.DefaultSettings = (uint)(ret.DefaultSettings & ((1ul << ret.Count) - 1));
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
public IModGroup Convert(GroupType type)
|
||||
{
|
||||
switch (type)
|
||||
{
|
||||
if (!PrioritizedOptions.Move(optionIdxFrom, optionIdxTo))
|
||||
return false;
|
||||
|
||||
DefaultSettings = Functions.MoveBit(DefaultSettings, optionIdxFrom, optionIdxTo);
|
||||
UpdatePositions(Math.Min(optionIdxFrom, optionIdxTo));
|
||||
return true;
|
||||
}
|
||||
|
||||
public void UpdatePositions(int from = 0)
|
||||
{
|
||||
foreach (var ((o, _), i) in PrioritizedOptions.WithIndex().Skip(from))
|
||||
o.SetPosition(o.GroupIdx, i);
|
||||
case GroupType.Multi: return this;
|
||||
case GroupType.Single:
|
||||
var multi = new SingleModGroup()
|
||||
{
|
||||
Name = Name,
|
||||
Description = Description,
|
||||
Priority = Priority,
|
||||
DefaultSettings = (uint)Math.Max(Math.Min(Count - 1, BitOperations.TrailingZeroCount(DefaultSettings)), 0),
|
||||
};
|
||||
multi.OptionData.AddRange(PrioritizedOptions.Select(p => p.Mod));
|
||||
return multi;
|
||||
default: throw new ArgumentOutOfRangeException(nameof(type), type, null);
|
||||
}
|
||||
}
|
||||
|
||||
public bool MoveOption(int optionIdxFrom, int optionIdxTo)
|
||||
{
|
||||
if (!PrioritizedOptions.Move(optionIdxFrom, optionIdxTo))
|
||||
return false;
|
||||
|
||||
DefaultSettings = Functions.MoveBit(DefaultSettings, optionIdxFrom, optionIdxTo);
|
||||
UpdatePositions(Math.Min(optionIdxFrom, optionIdxTo));
|
||||
return true;
|
||||
}
|
||||
|
||||
public void UpdatePositions(int from = 0)
|
||||
{
|
||||
foreach (var ((o, _), i) in PrioritizedOptions.WithIndex().Skip(from))
|
||||
o.SetPosition(o.GroupIdx, i);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -10,122 +10,119 @@ using Penumbra.Api.Enums;
|
|||
|
||||
namespace Penumbra.Mods;
|
||||
|
||||
public partial class Mod
|
||||
/// <summary> Groups that allow only one of their available options to be selected. </summary>
|
||||
public sealed class SingleModGroup : IModGroup
|
||||
{
|
||||
// Groups that allow only one of their available options to be selected.
|
||||
private sealed class SingleModGroup : IModGroup
|
||||
public GroupType Type
|
||||
=> GroupType.Single;
|
||||
|
||||
public string Name { get; set; } = "Option";
|
||||
public string Description { get; set; } = "A mutually exclusive group of settings.";
|
||||
public int Priority { get; set; }
|
||||
public uint DefaultSettings { get; set; }
|
||||
|
||||
public readonly List< SubMod > OptionData = new();
|
||||
|
||||
public int OptionPriority( Index _ )
|
||||
=> Priority;
|
||||
|
||||
public ISubMod this[ Index idx ]
|
||||
=> OptionData[ idx ];
|
||||
|
||||
[JsonIgnore]
|
||||
public int Count
|
||||
=> OptionData.Count;
|
||||
|
||||
public IEnumerator< ISubMod > GetEnumerator()
|
||||
=> OptionData.GetEnumerator();
|
||||
|
||||
IEnumerator IEnumerable.GetEnumerator()
|
||||
=> GetEnumerator();
|
||||
|
||||
public static SingleModGroup? Load( Mod mod, JObject json, int groupIdx )
|
||||
{
|
||||
public GroupType Type
|
||||
=> GroupType.Single;
|
||||
|
||||
public string Name { get; set; } = "Option";
|
||||
public string Description { get; set; } = "A mutually exclusive group of settings.";
|
||||
public int Priority { get; set; }
|
||||
public uint DefaultSettings { get; set; }
|
||||
|
||||
public readonly List< SubMod > OptionData = new();
|
||||
|
||||
public int OptionPriority( Index _ )
|
||||
=> Priority;
|
||||
|
||||
public ISubMod this[ Index idx ]
|
||||
=> OptionData[ idx ];
|
||||
|
||||
[JsonIgnore]
|
||||
public int Count
|
||||
=> OptionData.Count;
|
||||
|
||||
public IEnumerator< ISubMod > GetEnumerator()
|
||||
=> OptionData.GetEnumerator();
|
||||
|
||||
IEnumerator IEnumerable.GetEnumerator()
|
||||
=> GetEnumerator();
|
||||
|
||||
public static SingleModGroup? Load( Mod mod, JObject json, int groupIdx )
|
||||
var options = json[ "Options" ];
|
||||
var ret = new SingleModGroup
|
||||
{
|
||||
var options = json[ "Options" ];
|
||||
var ret = new SingleModGroup
|
||||
{
|
||||
Name = json[ nameof( Name ) ]?.ToObject< string >() ?? string.Empty,
|
||||
Description = json[ nameof( Description ) ]?.ToObject< string >() ?? string.Empty,
|
||||
Priority = json[ nameof( Priority ) ]?.ToObject< int >() ?? 0,
|
||||
DefaultSettings = json[ nameof( DefaultSettings ) ]?.ToObject< uint >() ?? 0u,
|
||||
};
|
||||
if( ret.Name.Length == 0 )
|
||||
{
|
||||
return null;
|
||||
}
|
||||
Name = json[ nameof( Name ) ]?.ToObject< string >() ?? string.Empty,
|
||||
Description = json[ nameof( Description ) ]?.ToObject< string >() ?? string.Empty,
|
||||
Priority = json[ nameof( Priority ) ]?.ToObject< int >() ?? 0,
|
||||
DefaultSettings = json[ nameof( DefaultSettings ) ]?.ToObject< uint >() ?? 0u,
|
||||
};
|
||||
if( ret.Name.Length == 0 )
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
if( options != null )
|
||||
if( options != null )
|
||||
{
|
||||
foreach( var child in options.Children() )
|
||||
{
|
||||
foreach( var child in options.Children() )
|
||||
var subMod = new SubMod( mod );
|
||||
subMod.SetPosition( groupIdx, ret.OptionData.Count );
|
||||
subMod.Load( mod.ModPath, child, out _ );
|
||||
ret.OptionData.Add( subMod );
|
||||
}
|
||||
}
|
||||
|
||||
if( ( int )ret.DefaultSettings >= ret.Count )
|
||||
ret.DefaultSettings = 0;
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
public IModGroup Convert( GroupType type )
|
||||
{
|
||||
switch( type )
|
||||
{
|
||||
case GroupType.Single: return this;
|
||||
case GroupType.Multi:
|
||||
var multi = new MultiModGroup()
|
||||
{
|
||||
var subMod = new SubMod( mod );
|
||||
subMod.SetPosition( groupIdx, ret.OptionData.Count );
|
||||
subMod.Load( mod.ModPath, child, out _ );
|
||||
ret.OptionData.Add( subMod );
|
||||
}
|
||||
}
|
||||
Name = Name,
|
||||
Description = Description,
|
||||
Priority = Priority,
|
||||
DefaultSettings = 1u << ( int )DefaultSettings,
|
||||
};
|
||||
multi.PrioritizedOptions.AddRange( OptionData.Select( ( o, i ) => ( o, i ) ) );
|
||||
return multi;
|
||||
default: throw new ArgumentOutOfRangeException( nameof( type ), type, null );
|
||||
}
|
||||
}
|
||||
|
||||
if( ( int )ret.DefaultSettings >= ret.Count )
|
||||
ret.DefaultSettings = 0;
|
||||
|
||||
return ret;
|
||||
public bool MoveOption( int optionIdxFrom, int optionIdxTo )
|
||||
{
|
||||
if( !OptionData.Move( optionIdxFrom, optionIdxTo ) )
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
public IModGroup Convert( GroupType type )
|
||||
// Update default settings with the move.
|
||||
if( DefaultSettings == optionIdxFrom )
|
||||
{
|
||||
switch( type )
|
||||
DefaultSettings = ( uint )optionIdxTo;
|
||||
}
|
||||
else if( optionIdxFrom < optionIdxTo )
|
||||
{
|
||||
if( DefaultSettings > optionIdxFrom && DefaultSettings <= optionIdxTo )
|
||||
{
|
||||
case GroupType.Single: return this;
|
||||
case GroupType.Multi:
|
||||
var multi = new MultiModGroup()
|
||||
{
|
||||
Name = Name,
|
||||
Description = Description,
|
||||
Priority = Priority,
|
||||
DefaultSettings = 1u << ( int )DefaultSettings,
|
||||
};
|
||||
multi.PrioritizedOptions.AddRange( OptionData.Select( ( o, i ) => ( o, i ) ) );
|
||||
return multi;
|
||||
default: throw new ArgumentOutOfRangeException( nameof( type ), type, null );
|
||||
--DefaultSettings;
|
||||
}
|
||||
}
|
||||
|
||||
public bool MoveOption( int optionIdxFrom, int optionIdxTo )
|
||||
else if( DefaultSettings < optionIdxFrom && DefaultSettings >= optionIdxTo )
|
||||
{
|
||||
if( !OptionData.Move( optionIdxFrom, optionIdxTo ) )
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
// Update default settings with the move.
|
||||
if( DefaultSettings == optionIdxFrom )
|
||||
{
|
||||
DefaultSettings = ( uint )optionIdxTo;
|
||||
}
|
||||
else if( optionIdxFrom < optionIdxTo )
|
||||
{
|
||||
if( DefaultSettings > optionIdxFrom && DefaultSettings <= optionIdxTo )
|
||||
{
|
||||
--DefaultSettings;
|
||||
}
|
||||
}
|
||||
else if( DefaultSettings < optionIdxFrom && DefaultSettings >= optionIdxTo )
|
||||
{
|
||||
++DefaultSettings;
|
||||
}
|
||||
|
||||
UpdatePositions( Math.Min( optionIdxFrom, optionIdxTo ) );
|
||||
return true;
|
||||
++DefaultSettings;
|
||||
}
|
||||
|
||||
public void UpdatePositions( int from = 0 )
|
||||
UpdatePositions( Math.Min( optionIdxFrom, optionIdxTo ) );
|
||||
return true;
|
||||
}
|
||||
|
||||
public void UpdatePositions( int from = 0 )
|
||||
{
|
||||
foreach( var (o, i) in OptionData.WithIndex().Skip( from ) )
|
||||
{
|
||||
foreach( var (o, i) in OptionData.WithIndex().Skip( from ) )
|
||||
{
|
||||
o.SetPosition( o.GroupIdx, i );
|
||||
}
|
||||
o.SetPosition( o.GroupIdx, i );
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -36,7 +36,7 @@ public partial class Mod
|
|||
ISubMod.WriteSubMod( j, serializer, _default, ModPath, 0 );
|
||||
}
|
||||
|
||||
private void SaveDefaultModDelayed()
|
||||
internal void SaveDefaultModDelayed()
|
||||
=> Penumbra.Framework.RegisterDelayed( nameof( SaveDefaultMod ) + ModPath.Name, SaveDefaultMod );
|
||||
|
||||
private void LoadDefaultOption()
|
||||
|
|
@ -92,233 +92,237 @@ public partial class Mod
|
|||
}
|
||||
|
||||
|
||||
// A sub mod is a collection of
|
||||
// - file replacements
|
||||
// - file swaps
|
||||
// - meta manipulations
|
||||
// that can be used either as an option or as the default data for a mod.
|
||||
// It can be loaded and reloaded from Json.
|
||||
// Nothing is checked for existence or validity when loading.
|
||||
// Objects are also not checked for uniqueness, the first appearance of a game path or meta path decides.
|
||||
public sealed class SubMod : ISubMod
|
||||
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// A sub mod is a collection of
|
||||
/// - file replacements
|
||||
/// - file swaps
|
||||
/// - meta manipulations
|
||||
/// that can be used either as an option or as the default data for a mod.
|
||||
/// It can be loaded and reloaded from Json.
|
||||
/// Nothing is checked for existence or validity when loading.
|
||||
/// Objects are also not checked for uniqueness, the first appearance of a game path or meta path decides.
|
||||
/// </summary>
|
||||
public sealed class SubMod : ISubMod
|
||||
{
|
||||
public string Name { get; set; } = "Default";
|
||||
|
||||
public string FullName
|
||||
=> GroupIdx < 0 ? "Default Option" : $"{ParentMod.Groups[ GroupIdx ].Name}: {Name}";
|
||||
|
||||
public string Description { get; set; } = string.Empty;
|
||||
|
||||
internal IMod ParentMod { get; private init; }
|
||||
internal int GroupIdx { get; private set; }
|
||||
internal int OptionIdx { get; private set; }
|
||||
|
||||
public bool IsDefault
|
||||
=> GroupIdx < 0;
|
||||
|
||||
public Dictionary< Utf8GamePath, FullPath > FileData = new();
|
||||
public Dictionary< Utf8GamePath, FullPath > FileSwapData = new();
|
||||
public HashSet< MetaManipulation > ManipulationData = new();
|
||||
|
||||
public SubMod( IMod parentMod )
|
||||
=> ParentMod = parentMod;
|
||||
|
||||
public IReadOnlyDictionary< Utf8GamePath, FullPath > Files
|
||||
=> FileData;
|
||||
|
||||
public IReadOnlyDictionary< Utf8GamePath, FullPath > FileSwaps
|
||||
=> FileSwapData;
|
||||
|
||||
public IReadOnlySet< MetaManipulation > Manipulations
|
||||
=> ManipulationData;
|
||||
|
||||
public void SetPosition( int groupIdx, int optionIdx )
|
||||
{
|
||||
public string Name { get; set; } = "Default";
|
||||
GroupIdx = groupIdx;
|
||||
OptionIdx = optionIdx;
|
||||
}
|
||||
|
||||
public string FullName
|
||||
=> GroupIdx < 0 ? "Default Option" : $"{ParentMod.Groups[ GroupIdx ].Name}: {Name}";
|
||||
public void Load( DirectoryInfo basePath, JToken json, out int priority )
|
||||
{
|
||||
FileData.Clear();
|
||||
FileSwapData.Clear();
|
||||
ManipulationData.Clear();
|
||||
|
||||
public string Description { get; set; } = string.Empty;
|
||||
// Every option has a name, but priorities are only relevant for multi group options.
|
||||
Name = json[ nameof( ISubMod.Name ) ]?.ToObject< string >() ?? string.Empty;
|
||||
Description = json[ nameof( ISubMod.Description ) ]?.ToObject< string >() ?? string.Empty;
|
||||
priority = json[ nameof( IModGroup.Priority ) ]?.ToObject< int >() ?? 0;
|
||||
|
||||
internal IMod ParentMod { get; private init; }
|
||||
internal int GroupIdx { get; private set; }
|
||||
internal int OptionIdx { get; private set; }
|
||||
|
||||
public bool IsDefault
|
||||
=> GroupIdx < 0;
|
||||
|
||||
public Dictionary< Utf8GamePath, FullPath > FileData = new();
|
||||
public Dictionary< Utf8GamePath, FullPath > FileSwapData = new();
|
||||
public HashSet< MetaManipulation > ManipulationData = new();
|
||||
|
||||
public SubMod( IMod parentMod )
|
||||
=> ParentMod = parentMod;
|
||||
|
||||
public IReadOnlyDictionary< Utf8GamePath, FullPath > Files
|
||||
=> FileData;
|
||||
|
||||
public IReadOnlyDictionary< Utf8GamePath, FullPath > FileSwaps
|
||||
=> FileSwapData;
|
||||
|
||||
public IReadOnlySet< MetaManipulation > Manipulations
|
||||
=> ManipulationData;
|
||||
|
||||
public void SetPosition( int groupIdx, int optionIdx )
|
||||
var files = ( JObject? )json[ nameof( Files ) ];
|
||||
if( files != null )
|
||||
{
|
||||
GroupIdx = groupIdx;
|
||||
OptionIdx = optionIdx;
|
||||
}
|
||||
|
||||
public void Load( DirectoryInfo basePath, JToken json, out int priority )
|
||||
{
|
||||
FileData.Clear();
|
||||
FileSwapData.Clear();
|
||||
ManipulationData.Clear();
|
||||
|
||||
// Every option has a name, but priorities are only relevant for multi group options.
|
||||
Name = json[ nameof( ISubMod.Name ) ]?.ToObject< string >() ?? string.Empty;
|
||||
Description = json[ nameof( ISubMod.Description ) ]?.ToObject< string >() ?? string.Empty;
|
||||
priority = json[ nameof( IModGroup.Priority ) ]?.ToObject< int >() ?? 0;
|
||||
|
||||
var files = ( JObject? )json[ nameof( Files ) ];
|
||||
if( files != null )
|
||||
foreach( var property in files.Properties() )
|
||||
{
|
||||
foreach( var property in files.Properties() )
|
||||
if( Utf8GamePath.FromString( property.Name, out var p, true ) )
|
||||
{
|
||||
if( Utf8GamePath.FromString( property.Name, out var p, true ) )
|
||||
{
|
||||
FileData.TryAdd( p, new FullPath( basePath, property.Value.ToObject< Utf8RelPath >() ) );
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var swaps = ( JObject? )json[ nameof( FileSwaps ) ];
|
||||
if( swaps != null )
|
||||
{
|
||||
foreach( var property in swaps.Properties() )
|
||||
{
|
||||
if( Utf8GamePath.FromString( property.Name, out var p, true ) )
|
||||
{
|
||||
FileSwapData.TryAdd( p, new FullPath( property.Value.ToObject< string >()! ) );
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var manips = json[ nameof( Manipulations ) ];
|
||||
if( manips != null )
|
||||
{
|
||||
foreach( var s in manips.Children().Select( c => c.ToObject< MetaManipulation >() ).Where( m => m.ManipulationType != MetaManipulation.Type.Unknown ) )
|
||||
{
|
||||
ManipulationData.Add( s );
|
||||
FileData.TryAdd( p, new FullPath( basePath, property.Value.ToObject< Utf8RelPath >() ) );
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// If .meta or .rgsp files are encountered, parse them and incorporate their meta changes into the mod.
|
||||
// If delete is true, the files are deleted afterwards.
|
||||
public (bool Changes, List< string > DeleteList) IncorporateMetaChanges( DirectoryInfo basePath, bool delete )
|
||||
var swaps = ( JObject? )json[ nameof( FileSwaps ) ];
|
||||
if( swaps != null )
|
||||
{
|
||||
var deleteList = new List< string >();
|
||||
var oldSize = ManipulationData.Count;
|
||||
var deleteString = delete ? "with deletion." : "without deletion.";
|
||||
foreach( var (key, file) in Files.ToList() )
|
||||
foreach( var property in swaps.Properties() )
|
||||
{
|
||||
var ext1 = key.Extension().AsciiToLower().ToString();
|
||||
var ext2 = file.Extension.ToLowerInvariant();
|
||||
try
|
||||
if( Utf8GamePath.FromString( property.Name, out var p, true ) )
|
||||
{
|
||||
if( ext1 == ".meta" || ext2 == ".meta" )
|
||||
{
|
||||
FileData.Remove( key );
|
||||
if( !file.Exists )
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var meta = new TexToolsMeta( Penumbra.GamePathParser, File.ReadAllBytes( file.FullName ), Penumbra.Config.KeepDefaultMetaChanges );
|
||||
Penumbra.Log.Verbose( $"Incorporating {file} as Metadata file of {meta.MetaManipulations.Count} manipulations {deleteString}" );
|
||||
deleteList.Add( file.FullName );
|
||||
ManipulationData.UnionWith( meta.MetaManipulations );
|
||||
}
|
||||
else if( ext1 == ".rgsp" || ext2 == ".rgsp" )
|
||||
{
|
||||
FileData.Remove( key );
|
||||
if( !file.Exists )
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var rgsp = TexToolsMeta.FromRgspFile( file.FullName, File.ReadAllBytes( file.FullName ), Penumbra.Config.KeepDefaultMetaChanges );
|
||||
Penumbra.Log.Verbose( $"Incorporating {file} as racial scaling file of {rgsp.MetaManipulations.Count} manipulations {deleteString}" );
|
||||
deleteList.Add( file.FullName );
|
||||
|
||||
ManipulationData.UnionWith( rgsp.MetaManipulations );
|
||||
}
|
||||
}
|
||||
catch( Exception e )
|
||||
{
|
||||
Penumbra.Log.Error( $"Could not incorporate meta changes in mod {basePath} from file {file.FullName}:\n{e}" );
|
||||
}
|
||||
}
|
||||
|
||||
DeleteDeleteList( deleteList, delete );
|
||||
return ( oldSize < ManipulationData.Count, deleteList );
|
||||
}
|
||||
|
||||
internal static void DeleteDeleteList( IEnumerable< string > deleteList, bool delete )
|
||||
{
|
||||
if( !delete )
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
foreach( var file in deleteList )
|
||||
{
|
||||
try
|
||||
{
|
||||
File.Delete( file );
|
||||
}
|
||||
catch( Exception e )
|
||||
{
|
||||
Penumbra.Log.Error( $"Could not delete incorporated meta file {file}:\n{e}" );
|
||||
FileSwapData.TryAdd( p, new FullPath( property.Value.ToObject< string >()! ) );
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void WriteTexToolsMeta( DirectoryInfo basePath, bool test = false )
|
||||
var manips = json[ nameof( Manipulations ) ];
|
||||
if( manips != null )
|
||||
{
|
||||
var files = TexToolsMeta.ConvertToTexTools( Manipulations );
|
||||
|
||||
foreach( var (file, data) in files )
|
||||
foreach( var s in manips.Children().Select( c => c.ToObject< MetaManipulation >() ).Where( m => m.ManipulationType != MetaManipulation.Type.Unknown ) )
|
||||
{
|
||||
var path = Path.Combine( basePath.FullName, file );
|
||||
try
|
||||
{
|
||||
Directory.CreateDirectory( Path.GetDirectoryName( path )! );
|
||||
File.WriteAllBytes( path, data );
|
||||
}
|
||||
catch( Exception e )
|
||||
{
|
||||
Penumbra.Log.Error( $"Could not write meta file {path}:\n{e}" );
|
||||
}
|
||||
}
|
||||
|
||||
if( test )
|
||||
{
|
||||
TestMetaWriting( files );
|
||||
}
|
||||
}
|
||||
|
||||
[Conditional( "DEBUG" )]
|
||||
private void TestMetaWriting( Dictionary< string, byte[] > files )
|
||||
{
|
||||
var meta = new HashSet< MetaManipulation >( Manipulations.Count );
|
||||
foreach( var (file, data) in files )
|
||||
{
|
||||
try
|
||||
{
|
||||
var x = file.EndsWith( "rgsp" )
|
||||
? TexToolsMeta.FromRgspFile( file, data, Penumbra.Config.KeepDefaultMetaChanges )
|
||||
: new TexToolsMeta( Penumbra.GamePathParser, data, Penumbra.Config.KeepDefaultMetaChanges );
|
||||
meta.UnionWith( x.MetaManipulations );
|
||||
}
|
||||
catch
|
||||
{
|
||||
// ignored
|
||||
}
|
||||
}
|
||||
|
||||
if( !Manipulations.SetEquals( meta ) )
|
||||
{
|
||||
Penumbra.Log.Information( "Meta Sets do not equal." );
|
||||
foreach( var (m1, m2) in Manipulations.Zip( meta ) )
|
||||
{
|
||||
Penumbra.Log.Information( $"{m1} {m1.EntryToString()} | {m2} {m2.EntryToString()}" );
|
||||
}
|
||||
|
||||
foreach( var m in Manipulations.Skip( meta.Count ) )
|
||||
{
|
||||
Penumbra.Log.Information( $"{m} {m.EntryToString()} " );
|
||||
}
|
||||
|
||||
foreach( var m in meta.Skip( Manipulations.Count ) )
|
||||
{
|
||||
Penumbra.Log.Information( $"{m} {m.EntryToString()} " );
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
Penumbra.Log.Information( "Meta Sets are equal." );
|
||||
ManipulationData.Add( s );
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// If .meta or .rgsp files are encountered, parse them and incorporate their meta changes into the mod.
|
||||
// If delete is true, the files are deleted afterwards.
|
||||
public (bool Changes, List< string > DeleteList) IncorporateMetaChanges( DirectoryInfo basePath, bool delete )
|
||||
{
|
||||
var deleteList = new List< string >();
|
||||
var oldSize = ManipulationData.Count;
|
||||
var deleteString = delete ? "with deletion." : "without deletion.";
|
||||
foreach( var (key, file) in Files.ToList() )
|
||||
{
|
||||
var ext1 = key.Extension().AsciiToLower().ToString();
|
||||
var ext2 = file.Extension.ToLowerInvariant();
|
||||
try
|
||||
{
|
||||
if( ext1 == ".meta" || ext2 == ".meta" )
|
||||
{
|
||||
FileData.Remove( key );
|
||||
if( !file.Exists )
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var meta = new TexToolsMeta( Penumbra.GamePathParser, File.ReadAllBytes( file.FullName ), Penumbra.Config.KeepDefaultMetaChanges );
|
||||
Penumbra.Log.Verbose( $"Incorporating {file} as Metadata file of {meta.MetaManipulations.Count} manipulations {deleteString}" );
|
||||
deleteList.Add( file.FullName );
|
||||
ManipulationData.UnionWith( meta.MetaManipulations );
|
||||
}
|
||||
else if( ext1 == ".rgsp" || ext2 == ".rgsp" )
|
||||
{
|
||||
FileData.Remove( key );
|
||||
if( !file.Exists )
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var rgsp = TexToolsMeta.FromRgspFile( file.FullName, File.ReadAllBytes( file.FullName ), Penumbra.Config.KeepDefaultMetaChanges );
|
||||
Penumbra.Log.Verbose( $"Incorporating {file} as racial scaling file of {rgsp.MetaManipulations.Count} manipulations {deleteString}" );
|
||||
deleteList.Add( file.FullName );
|
||||
|
||||
ManipulationData.UnionWith( rgsp.MetaManipulations );
|
||||
}
|
||||
}
|
||||
catch( Exception e )
|
||||
{
|
||||
Penumbra.Log.Error( $"Could not incorporate meta changes in mod {basePath} from file {file.FullName}:\n{e}" );
|
||||
}
|
||||
}
|
||||
|
||||
DeleteDeleteList( deleteList, delete );
|
||||
return ( oldSize < ManipulationData.Count, deleteList );
|
||||
}
|
||||
|
||||
internal static void DeleteDeleteList( IEnumerable< string > deleteList, bool delete )
|
||||
{
|
||||
if( !delete )
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
foreach( var file in deleteList )
|
||||
{
|
||||
try
|
||||
{
|
||||
File.Delete( file );
|
||||
}
|
||||
catch( Exception e )
|
||||
{
|
||||
Penumbra.Log.Error( $"Could not delete incorporated meta file {file}:\n{e}" );
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void WriteTexToolsMeta( DirectoryInfo basePath, bool test = false )
|
||||
{
|
||||
var files = TexToolsMeta.ConvertToTexTools( Manipulations );
|
||||
|
||||
foreach( var (file, data) in files )
|
||||
{
|
||||
var path = Path.Combine( basePath.FullName, file );
|
||||
try
|
||||
{
|
||||
Directory.CreateDirectory( Path.GetDirectoryName( path )! );
|
||||
File.WriteAllBytes( path, data );
|
||||
}
|
||||
catch( Exception e )
|
||||
{
|
||||
Penumbra.Log.Error( $"Could not write meta file {path}:\n{e}" );
|
||||
}
|
||||
}
|
||||
|
||||
if( test )
|
||||
{
|
||||
TestMetaWriting( files );
|
||||
}
|
||||
}
|
||||
|
||||
[Conditional("DEBUG" )]
|
||||
private void TestMetaWriting( Dictionary< string, byte[] > files )
|
||||
{
|
||||
var meta = new HashSet< MetaManipulation >( Manipulations.Count );
|
||||
foreach( var (file, data) in files )
|
||||
{
|
||||
try
|
||||
{
|
||||
var x = file.EndsWith( "rgsp" )
|
||||
? TexToolsMeta.FromRgspFile( file, data, Penumbra.Config.KeepDefaultMetaChanges )
|
||||
: new TexToolsMeta( Penumbra.GamePathParser, data, Penumbra.Config.KeepDefaultMetaChanges );
|
||||
meta.UnionWith( x.MetaManipulations );
|
||||
}
|
||||
catch
|
||||
{
|
||||
// ignored
|
||||
}
|
||||
}
|
||||
|
||||
if( !Manipulations.SetEquals( meta ) )
|
||||
{
|
||||
Penumbra.Log.Information( "Meta Sets do not equal." );
|
||||
foreach( var (m1, m2) in Manipulations.Zip( meta ) )
|
||||
{
|
||||
Penumbra.Log.Information( $"{m1} {m1.EntryToString()} | {m2} {m2.EntryToString()}" );
|
||||
}
|
||||
|
||||
foreach( var m in Manipulations.Skip( meta.Count ) )
|
||||
{
|
||||
Penumbra.Log.Information( $"{m} {m.EntryToString()} " );
|
||||
}
|
||||
|
||||
foreach( var m in meta.Skip( Manipulations.Count ) )
|
||||
{
|
||||
Penumbra.Log.Information( $"{m} {m.EntryToString()} " );
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
Penumbra.Log.Information( "Meta Sets are equal." );
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -10,7 +10,7 @@ using Penumbra.String.Classes;
|
|||
|
||||
namespace Penumbra.Mods;
|
||||
|
||||
// Contains the settings for a given mod.
|
||||
/// <summary> Contains the settings for a given mod. </summary>
|
||||
public class ModSettings
|
||||
{
|
||||
public static readonly ModSettings Empty = new();
|
||||
|
|
|
|||
|
|
@ -27,10 +27,10 @@ public class TemporaryMod : IMod
|
|||
public IEnumerable< ISubMod > AllSubMods
|
||||
=> new[] { Default };
|
||||
|
||||
private readonly Mod.SubMod _default;
|
||||
private readonly SubMod _default;
|
||||
|
||||
public TemporaryMod()
|
||||
=> _default = new Mod.SubMod( this );
|
||||
=> _default = new SubMod( this );
|
||||
|
||||
public void SetFile( Utf8GamePath gamePath, FullPath fullPath )
|
||||
=> _default.FileData[ gamePath ] = fullPath;
|
||||
|
|
@ -44,7 +44,7 @@ public class TemporaryMod : IMod
|
|||
_default.ManipulationData = manips;
|
||||
}
|
||||
|
||||
public static void SaveTempCollection( Mod.Manager modManager, ModCollection collection, string? character = null )
|
||||
public static void SaveTempCollection( ModManager modManager, ModCollection collection, string? character = null )
|
||||
{
|
||||
DirectoryInfo? dir = null;
|
||||
try
|
||||
|
|
@ -54,7 +54,7 @@ public class TemporaryMod : IMod
|
|||
modManager.DataEditor.CreateMeta( dir, collection.Name, character ?? Penumbra.Config.DefaultModAuthor,
|
||||
$"Mod generated from temporary collection {collection.Name} for {character ?? "Unknown Character"}.", null, null );
|
||||
var mod = new Mod( dir );
|
||||
var defaultMod = (Mod.SubMod) mod.Default;
|
||||
var defaultMod = (SubMod) mod.Default;
|
||||
foreach( var (gamePath, fullPath) in collection.ResolvedFiles )
|
||||
{
|
||||
if( gamePath.Path.EndsWith( ".imc"u8 ) )
|
||||
|
|
|
|||
|
|
@ -47,8 +47,8 @@ public class Penumbra : IDalamudPlugin
|
|||
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 Mod.Manager ModManager { get; private set; } = null!;
|
||||
public static ModCollection.Manager 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!;
|
||||
|
|
@ -96,8 +96,8 @@ public class Penumbra : IDalamudPlugin
|
|||
TempMods = _tmp.Services.GetRequiredService<TempModManager>();
|
||||
ResidentResources = _tmp.Services.GetRequiredService<ResidentResourceManager>();
|
||||
_tmp.Services.GetRequiredService<ResourceManagerService>();
|
||||
ModManager = _tmp.Services.GetRequiredService<Mod.Manager>();
|
||||
CollectionManager = _tmp.Services.GetRequiredService<ModCollection.Manager>();
|
||||
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>();
|
||||
|
|
|
|||
|
|
@ -88,12 +88,12 @@ public class PenumbraNew
|
|||
// Add Collection Services
|
||||
services.AddTransient<IndividualCollections>()
|
||||
.AddSingleton<TempCollectionManager>()
|
||||
.AddSingleton<ModCollection.Manager>();
|
||||
.AddSingleton<CollectionManager>();
|
||||
|
||||
// Add Mod Services
|
||||
services.AddSingleton<TempModManager>()
|
||||
.AddSingleton<ModDataEditor>()
|
||||
.AddSingleton<Mod.Manager>()
|
||||
.AddSingleton<ModManager>()
|
||||
.AddSingleton<ModFileSystem>();
|
||||
|
||||
// Add Resource services
|
||||
|
|
|
|||
|
|
@ -87,7 +87,7 @@ public class ConfigMigrationService
|
|||
if (_config.Version != 6)
|
||||
return;
|
||||
|
||||
ModCollection.Manager.MigrateUngenderedCollections(_fileNames);
|
||||
CollectionManager.MigrateUngenderedCollections(_fileNames);
|
||||
_config.Version = 7;
|
||||
}
|
||||
|
||||
|
|
@ -113,7 +113,7 @@ public class ConfigMigrationService
|
|||
if (_config.Version != 4)
|
||||
return;
|
||||
|
||||
Mod.Manager.MigrateModBackups = true;
|
||||
ModManager.MigrateModBackups = true;
|
||||
_config.Version = 5;
|
||||
}
|
||||
|
||||
|
|
@ -257,11 +257,11 @@ public class ConfigMigrationService
|
|||
using var j = new JsonTextWriter(writer);
|
||||
j.Formatting = Formatting.Indented;
|
||||
j.WriteStartObject();
|
||||
j.WritePropertyName(nameof(ModCollection.Manager.Default));
|
||||
j.WritePropertyName(nameof(CollectionManager.Default));
|
||||
j.WriteValue(def);
|
||||
j.WritePropertyName(nameof(ModCollection.Manager.Interface));
|
||||
j.WritePropertyName(nameof(CollectionManager.Interface));
|
||||
j.WriteValue(ui);
|
||||
j.WritePropertyName(nameof(ModCollection.Manager.Current));
|
||||
j.WritePropertyName(nameof(CollectionManager.Current));
|
||||
j.WriteValue(current);
|
||||
foreach (var (type, collection) in special)
|
||||
{
|
||||
|
|
|
|||
|
|
@ -25,12 +25,12 @@ public class ItemSwapTab : IDisposable, ITab
|
|||
{
|
||||
private readonly CommunicatorService _communicator;
|
||||
private readonly ItemService _itemService;
|
||||
private readonly ModCollection.Manager _collectionManager;
|
||||
private readonly Mod.Manager _modManager;
|
||||
private readonly CollectionManager _collectionManager;
|
||||
private readonly ModManager _modManager;
|
||||
private readonly Configuration _config;
|
||||
|
||||
public ItemSwapTab(CommunicatorService communicator, ItemService itemService, ModCollection.Manager collectionManager,
|
||||
Mod.Manager modManager, Configuration config)
|
||||
public ItemSwapTab(CommunicatorService communicator, ItemService itemService, CollectionManager collectionManager,
|
||||
ModManager modManager, Configuration config)
|
||||
{
|
||||
_communicator = communicator;
|
||||
_itemService = itemService;
|
||||
|
|
|
|||
|
|
@ -297,7 +297,7 @@ public partial class ModEditWindow
|
|||
var tt = changes ? "Apply the current file setup to the currently selected option." : "No changes made.";
|
||||
if (ImGuiUtil.DrawDisabledButton("Apply Changes", Vector2.Zero, tt, !changes))
|
||||
{
|
||||
var failedFiles = _editor.FileEditor.Apply(_editor.Mod!, (Mod.SubMod)_editor.Option!);
|
||||
var failedFiles = _editor.FileEditor.Apply(_editor.Mod!, (SubMod)_editor.Option!);
|
||||
if (failedFiles > 0)
|
||||
Penumbra.Log.Information($"Failed to apply {failedFiles} file redirections to {_editor.Option!.FullName}.");
|
||||
}
|
||||
|
|
|
|||
|
|
@ -194,7 +194,7 @@ public partial class ModEditWindow
|
|||
_editor.FileEditor.Revert(_editor.Mod!, _editor.Option!);
|
||||
var fileRegistry = _editor.Files.Available.First(file => file.File.FullName == _targetPath);
|
||||
_editor.FileEditor.AddPathsToSelected(_editor.Option!, new []{ fileRegistry }, _subDirs);
|
||||
_editor.FileEditor.Apply(_editor.Mod!, (Mod.SubMod) _editor.Option!);
|
||||
_editor.FileEditor.Apply(_editor.Mod!, (SubMod) _editor.Option!);
|
||||
|
||||
return fileRegistry;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -9,9 +9,9 @@ namespace Penumbra.UI.CollectionTab;
|
|||
|
||||
public sealed class CollectionSelector : FilterComboCache<ModCollection>
|
||||
{
|
||||
private readonly ModCollection.Manager _collectionManager;
|
||||
private readonly CollectionManager _collectionManager;
|
||||
|
||||
public CollectionSelector(ModCollection.Manager manager, Func<IReadOnlyList<ModCollection>> items)
|
||||
public CollectionSelector(CollectionManager manager, Func<IReadOnlyList<ModCollection>> items)
|
||||
: base(items)
|
||||
=> _collectionManager = manager;
|
||||
|
||||
|
|
|
|||
|
|
@ -16,10 +16,10 @@ namespace Penumbra.UI.CollectionTab;
|
|||
public class IndividualCollectionUi
|
||||
{
|
||||
private readonly ActorService _actorService;
|
||||
private readonly ModCollection.Manager _collectionManager;
|
||||
private readonly CollectionManager _collectionManager;
|
||||
private readonly CollectionSelector _withEmpty;
|
||||
|
||||
public IndividualCollectionUi(ActorService actors, ModCollection.Manager collectionManager, CollectionSelector withEmpty)
|
||||
public IndividualCollectionUi(ActorService actors, CollectionManager collectionManager, CollectionSelector withEmpty)
|
||||
{
|
||||
_actorService = actors;
|
||||
_collectionManager = collectionManager;
|
||||
|
|
|
|||
|
|
@ -16,9 +16,9 @@ public class InheritanceUi
|
|||
private const int InheritedCollectionHeight = 9;
|
||||
private const string InheritanceDragDropLabel = "##InheritanceMove";
|
||||
|
||||
private readonly ModCollection.Manager _collectionManager;
|
||||
private readonly CollectionManager _collectionManager;
|
||||
|
||||
public InheritanceUi(ModCollection.Manager collectionManager)
|
||||
public InheritanceUi(CollectionManager collectionManager)
|
||||
=> _collectionManager = collectionManager;
|
||||
|
||||
/// <summary> Draw the whole inheritance block. </summary>
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@ namespace Penumbra.UI.CollectionTab;
|
|||
|
||||
public sealed class SpecialCombo : FilterComboBase<(CollectionType, string, string)>
|
||||
{
|
||||
private readonly ModCollection.Manager _collectionManager;
|
||||
private readonly CollectionManager _collectionManager;
|
||||
|
||||
public (CollectionType, string, string)? CurrentType
|
||||
=> CollectionTypeExtensions.Special[CurrentIdx];
|
||||
|
|
@ -16,7 +16,7 @@ public sealed class SpecialCombo : FilterComboBase<(CollectionType, string, stri
|
|||
private readonly float _unscaledWidth;
|
||||
private readonly string _label;
|
||||
|
||||
public SpecialCombo(ModCollection.Manager collectionManager, string label, float unscaledWidth)
|
||||
public SpecialCombo(CollectionManager collectionManager, string label, float unscaledWidth)
|
||||
: base(CollectionTypeExtensions.Special, false)
|
||||
{
|
||||
_collectionManager = collectionManager;
|
||||
|
|
|
|||
|
|
@ -14,12 +14,12 @@ namespace Penumbra.UI;
|
|||
|
||||
public class FileDialogService : IDisposable
|
||||
{
|
||||
private readonly Mod.Manager _mods;
|
||||
private readonly ModManager _mods;
|
||||
private readonly FileDialogManager _manager;
|
||||
private readonly ConcurrentDictionary<string, string> _startPaths = new();
|
||||
private bool _isOpen;
|
||||
|
||||
public FileDialogService(Mod.Manager mods, Configuration config)
|
||||
public FileDialogService(ModManager mods, Configuration config)
|
||||
{
|
||||
_mods = mods;
|
||||
_manager = SetupFileManager(config.ModDirectory);
|
||||
|
|
|
|||
|
|
@ -29,8 +29,8 @@ public sealed partial class ModFileSystemSelector : FileSystemSelector<Mod, ModF
|
|||
private readonly ChatService _chat;
|
||||
private readonly Configuration _config;
|
||||
private readonly FileDialogService _fileDialog;
|
||||
private readonly Mod.Manager _modManager;
|
||||
private readonly ModCollection.Manager _collectionManager;
|
||||
private readonly ModManager _modManager;
|
||||
private readonly CollectionManager _collectionManager;
|
||||
private readonly TutorialService _tutorial;
|
||||
private readonly ModEditor _modEditor;
|
||||
|
||||
|
|
@ -38,8 +38,8 @@ public sealed partial class ModFileSystemSelector : FileSystemSelector<Mod, ModF
|
|||
public ModSettings SelectedSettings { get; private set; } = ModSettings.Empty;
|
||||
public ModCollection SelectedSettingCollection { get; private set; } = ModCollection.Empty;
|
||||
|
||||
public ModFileSystemSelector(CommunicatorService communicator, ModFileSystem fileSystem, Mod.Manager modManager,
|
||||
ModCollection.Manager collectionManager, Configuration config, TutorialService tutorial, FileDialogService fileDialog, ChatService chat,
|
||||
public ModFileSystemSelector(CommunicatorService communicator, ModFileSystem fileSystem, ModManager modManager,
|
||||
CollectionManager collectionManager, Configuration config, TutorialService tutorial, FileDialogService fileDialog, ChatService chat,
|
||||
ModEditor modEditor)
|
||||
: base(fileSystem, DalamudServices.KeyState, HandleException)
|
||||
{
|
||||
|
|
|
|||
|
|
@ -14,9 +14,9 @@ namespace Penumbra.UI.ModsTab;
|
|||
public class ModPanelConflictsTab : ITab
|
||||
{
|
||||
private readonly ModFileSystemSelector _selector;
|
||||
private readonly ModCollection.Manager _collectionManager;
|
||||
private readonly CollectionManager _collectionManager;
|
||||
|
||||
public ModPanelConflictsTab(ModCollection.Manager collectionManager, ModFileSystemSelector selector)
|
||||
public ModPanelConflictsTab(CollectionManager collectionManager, ModFileSystemSelector selector)
|
||||
{
|
||||
_collectionManager = collectionManager;
|
||||
_selector = selector;
|
||||
|
|
|
|||
|
|
@ -13,11 +13,11 @@ public class ModPanelDescriptionTab : ITab
|
|||
{
|
||||
private readonly ModFileSystemSelector _selector;
|
||||
private readonly TutorialService _tutorial;
|
||||
private readonly Mod.Manager _modManager;
|
||||
private readonly ModManager _modManager;
|
||||
private readonly TagButtons _localTags = new();
|
||||
private readonly TagButtons _modTags = new();
|
||||
|
||||
public ModPanelDescriptionTab(ModFileSystemSelector selector, TutorialService tutorial, Mod.Manager modManager)
|
||||
public ModPanelDescriptionTab(ModFileSystemSelector selector, TutorialService tutorial, ModManager modManager)
|
||||
{
|
||||
_selector = selector;
|
||||
_tutorial = tutorial;
|
||||
|
|
|
|||
|
|
@ -20,7 +20,7 @@ namespace Penumbra.UI.ModsTab;
|
|||
public class ModPanelEditTab : ITab
|
||||
{
|
||||
private readonly ChatService _chat;
|
||||
private readonly Mod.Manager _modManager;
|
||||
private readonly ModManager _modManager;
|
||||
private readonly ModFileSystem _fileSystem;
|
||||
private readonly ModFileSystemSelector _selector;
|
||||
private readonly ModEditWindow _editWindow;
|
||||
|
|
@ -33,7 +33,7 @@ public class ModPanelEditTab : ITab
|
|||
private ModFileSystem.Leaf _leaf = null!;
|
||||
private Mod _mod = null!;
|
||||
|
||||
public ModPanelEditTab(Mod.Manager modManager, ModFileSystemSelector selector, ModFileSystem fileSystem, ChatService chat,
|
||||
public ModPanelEditTab(ModManager modManager, ModFileSystemSelector selector, ModFileSystem fileSystem, ChatService chat,
|
||||
ModEditWindow editWindow, ModEditor editor)
|
||||
{
|
||||
_modManager = modManager;
|
||||
|
|
@ -219,7 +219,7 @@ public class ModPanelEditTab : ITab
|
|||
public static void Reset()
|
||||
=> _newGroupName = string.Empty;
|
||||
|
||||
public static void Draw(Mod.Manager modManager, Mod mod)
|
||||
public static void Draw(ModManager modManager, Mod mod)
|
||||
{
|
||||
using var spacing = ImRaii.PushStyle(ImGuiStyleVar.ItemSpacing, new Vector2(UiHelpers.ScaleX3));
|
||||
ImGui.SetNextItemWidth(UiHelpers.InputTextMinusButton3);
|
||||
|
|
@ -250,15 +250,15 @@ public class ModPanelEditTab : ITab
|
|||
private static class MoveDirectory
|
||||
{
|
||||
private static string? _currentModDirectory;
|
||||
private static Mod.Manager.NewDirectoryState _state = Mod.Manager.NewDirectoryState.Identical;
|
||||
private static ModManager.NewDirectoryState _state = ModManager.NewDirectoryState.Identical;
|
||||
|
||||
public static void Reset()
|
||||
{
|
||||
_currentModDirectory = null;
|
||||
_state = Mod.Manager.NewDirectoryState.Identical;
|
||||
_state = ModManager.NewDirectoryState.Identical;
|
||||
}
|
||||
|
||||
public static void Draw(Mod.Manager modManager, Mod mod, Vector2 buttonSize)
|
||||
public static void Draw(ModManager modManager, Mod mod, Vector2 buttonSize)
|
||||
{
|
||||
ImGui.SetNextItemWidth(buttonSize.X * 2 + ImGui.GetStyle().ItemSpacing.X);
|
||||
var tmp = _currentModDirectory ?? mod.ModPath.Name;
|
||||
|
|
@ -270,13 +270,13 @@ public class ModPanelEditTab : ITab
|
|||
|
||||
var (disabled, tt) = _state switch
|
||||
{
|
||||
Mod.Manager.NewDirectoryState.Identical => (true, "Current directory name is identical to new one."),
|
||||
Mod.Manager.NewDirectoryState.Empty => (true, "Please enter a new directory name first."),
|
||||
Mod.Manager.NewDirectoryState.NonExisting => (false, $"Move mod from {mod.ModPath.Name} to {_currentModDirectory}."),
|
||||
Mod.Manager.NewDirectoryState.ExistsEmpty => (false, $"Move mod from {mod.ModPath.Name} to {_currentModDirectory}."),
|
||||
Mod.Manager.NewDirectoryState.ExistsNonEmpty => (true, $"{_currentModDirectory} already exists and is not empty."),
|
||||
Mod.Manager.NewDirectoryState.ExistsAsFile => (true, $"{_currentModDirectory} exists as a file."),
|
||||
Mod.Manager.NewDirectoryState.ContainsInvalidSymbols => (true,
|
||||
ModManager.NewDirectoryState.Identical => (true, "Current directory name is identical to new one."),
|
||||
ModManager.NewDirectoryState.Empty => (true, "Please enter a new directory name first."),
|
||||
ModManager.NewDirectoryState.NonExisting => (false, $"Move mod from {mod.ModPath.Name} to {_currentModDirectory}."),
|
||||
ModManager.NewDirectoryState.ExistsEmpty => (false, $"Move mod from {mod.ModPath.Name} to {_currentModDirectory}."),
|
||||
ModManager.NewDirectoryState.ExistsNonEmpty => (true, $"{_currentModDirectory} already exists and is not empty."),
|
||||
ModManager.NewDirectoryState.ExistsAsFile => (true, $"{_currentModDirectory} exists as a file."),
|
||||
ModManager.NewDirectoryState.ContainsInvalidSymbols => (true,
|
||||
$"{_currentModDirectory} contains invalid symbols for FFXIV."),
|
||||
_ => (true, "Unknown error."),
|
||||
};
|
||||
|
|
@ -317,7 +317,7 @@ public class ModPanelEditTab : ITab
|
|||
ImGui.OpenPopup(PopupName);
|
||||
}
|
||||
|
||||
public static void DrawPopup(Mod.Manager modManager)
|
||||
public static void DrawPopup(ModManager modManager)
|
||||
{
|
||||
if (_mod == null)
|
||||
return;
|
||||
|
|
|
|||
|
|
@ -18,11 +18,11 @@ namespace Penumbra.UI.ModsTab;
|
|||
public class ModPanelSettingsTab : ITab
|
||||
{
|
||||
private readonly Configuration _config;
|
||||
private readonly ModCollection.Manager _collectionManager;
|
||||
private readonly CollectionManager _collectionManager;
|
||||
private readonly ModFileSystemSelector _selector;
|
||||
private readonly TutorialService _tutorial;
|
||||
private readonly PenumbraApi _api;
|
||||
private readonly Mod.Manager _modManager;
|
||||
private readonly ModManager _modManager;
|
||||
|
||||
private bool _inherited;
|
||||
private ModSettings _settings = null!;
|
||||
|
|
@ -30,7 +30,7 @@ public class ModPanelSettingsTab : ITab
|
|||
private bool _empty;
|
||||
private int? _currentPriority = null;
|
||||
|
||||
public ModPanelSettingsTab(ModCollection.Manager collectionManager, Mod.Manager modManager, ModFileSystemSelector selector,
|
||||
public ModPanelSettingsTab(CollectionManager collectionManager, ModManager modManager, ModFileSystemSelector selector,
|
||||
TutorialService tutorial, PenumbraApi api, Configuration config)
|
||||
{
|
||||
_collectionManager = collectionManager;
|
||||
|
|
|
|||
|
|
@ -27,7 +27,7 @@ public class ModPanelTabBar
|
|||
public readonly ModPanelChangedItemsTab ChangedItems;
|
||||
public readonly ModPanelEditTab Edit;
|
||||
private readonly ModEditWindow _modEditWindow;
|
||||
private readonly Mod.Manager _modManager;
|
||||
private readonly ModManager _modManager;
|
||||
private readonly TutorialService _tutorial;
|
||||
|
||||
public readonly ITab[] Tabs;
|
||||
|
|
@ -35,7 +35,7 @@ public class ModPanelTabBar
|
|||
private Mod? _lastMod = null;
|
||||
|
||||
public ModPanelTabBar(ModEditWindow modEditWindow, ModPanelSettingsTab settings, ModPanelDescriptionTab description,
|
||||
ModPanelConflictsTab conflicts, ModPanelChangedItemsTab changedItems, ModPanelEditTab edit, Mod.Manager modManager,
|
||||
ModPanelConflictsTab conflicts, ModPanelChangedItemsTab changedItems, ModPanelEditTab edit, ModManager modManager,
|
||||
TutorialService tutorial)
|
||||
{
|
||||
_modEditWindow = modEditWindow;
|
||||
|
|
@ -107,7 +107,7 @@ public class ModPanelTabBar
|
|||
if (ImGui.TabItemButton("Advanced Editing", ImGuiTabItemFlags.Trailing | ImGuiTabItemFlags.NoTooltip))
|
||||
{
|
||||
_modEditWindow.ChangeMod(mod);
|
||||
_modEditWindow.ChangeOption((Mod.SubMod) mod.Default);
|
||||
_modEditWindow.ChangeOption((SubMod) mod.Default);
|
||||
_modEditWindow.IsOpen = true;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -16,10 +16,10 @@ namespace Penumbra.UI.Tabs;
|
|||
|
||||
public class ChangedItemsTab : ITab
|
||||
{
|
||||
private readonly ModCollection.Manager _collectionManager;
|
||||
private readonly CollectionManager _collectionManager;
|
||||
private readonly PenumbraApi _api;
|
||||
|
||||
public ChangedItemsTab(ModCollection.Manager collectionManager, PenumbraApi api)
|
||||
public ChangedItemsTab(CollectionManager collectionManager, PenumbraApi api)
|
||||
{
|
||||
_collectionManager = collectionManager;
|
||||
_api = api;
|
||||
|
|
|
|||
|
|
@ -17,7 +17,7 @@ public class CollectionsTab : IDisposable, ITab
|
|||
{
|
||||
private readonly CommunicatorService _communicator;
|
||||
private readonly Configuration _config;
|
||||
private readonly ModCollection.Manager _collectionManager;
|
||||
private readonly CollectionManager _collectionManager;
|
||||
private readonly TutorialService _tutorial;
|
||||
private readonly SpecialCombo _specialCollectionCombo;
|
||||
|
||||
|
|
@ -26,7 +26,7 @@ public class CollectionsTab : IDisposable, ITab
|
|||
private readonly InheritanceUi _inheritance;
|
||||
private readonly IndividualCollectionUi _individualCollections;
|
||||
|
||||
public CollectionsTab(ActorService actorService, CommunicatorService communicator, ModCollection.Manager collectionManager,
|
||||
public CollectionsTab(ActorService actorService, CommunicatorService communicator, CollectionManager collectionManager,
|
||||
TutorialService tutorial, Configuration config)
|
||||
{
|
||||
_communicator = communicator;
|
||||
|
|
|
|||
|
|
@ -34,8 +34,8 @@ public class DebugTab : ITab
|
|||
private readonly StartTracker _timer;
|
||||
private readonly PerformanceTracker _performance;
|
||||
private readonly Configuration _config;
|
||||
private readonly ModCollection.Manager _collectionManager;
|
||||
private readonly Mod.Manager _modManager;
|
||||
private readonly CollectionManager _collectionManager;
|
||||
private readonly ModManager _modManager;
|
||||
private readonly ValidityChecker _validityChecker;
|
||||
private readonly HttpApi _httpApi;
|
||||
private readonly ActorService _actorService;
|
||||
|
|
@ -52,8 +52,8 @@ public class DebugTab : ITab
|
|||
private readonly IdentifiedCollectionCache _identifiedCollectionCache;
|
||||
private readonly CutsceneService _cutsceneService;
|
||||
|
||||
public DebugTab(StartTracker timer, PerformanceTracker performance, Configuration config, ModCollection.Manager collectionManager,
|
||||
ValidityChecker validityChecker, Mod.Manager modManager, HttpApi httpApi, ActorService actorService,
|
||||
public DebugTab(StartTracker timer, PerformanceTracker performance, Configuration config, CollectionManager collectionManager,
|
||||
ValidityChecker validityChecker, ModManager modManager, HttpApi httpApi, ActorService actorService,
|
||||
DalamudServices dalamud, StainService stains, CharacterUtility characterUtility, ResidentResourceManager residentResources,
|
||||
ResourceManagerService resourceManager, PenumbraIpcProviders ipc, CollectionResolver collectionResolver,
|
||||
DrawObjectState drawObjectState, PathState pathState, SubfileHelper subfileHelper, IdentifiedCollectionCache identifiedCollectionCache,
|
||||
|
|
|
|||
|
|
@ -17,9 +17,9 @@ namespace Penumbra.UI.Tabs;
|
|||
|
||||
public class EffectiveTab : ITab
|
||||
{
|
||||
private readonly ModCollection.Manager _collectionManager;
|
||||
private readonly CollectionManager _collectionManager;
|
||||
|
||||
public EffectiveTab(ModCollection.Manager collectionManager)
|
||||
public EffectiveTab(CollectionManager collectionManager)
|
||||
=> _collectionManager = collectionManager;
|
||||
|
||||
public ReadOnlySpan<byte> Label
|
||||
|
|
|
|||
|
|
@ -23,13 +23,13 @@ public class ModsTab : ITab
|
|||
private readonly ModFileSystemSelector _selector;
|
||||
private readonly ModPanel _panel;
|
||||
private readonly TutorialService _tutorial;
|
||||
private readonly Mod.Manager _modManager;
|
||||
private readonly ModCollection.Manager _collectionManager;
|
||||
private readonly ModManager _modManager;
|
||||
private readonly CollectionManager _collectionManager;
|
||||
private readonly RedrawService _redrawService;
|
||||
private readonly Configuration _config;
|
||||
private readonly CollectionsTab _collectionsTab;
|
||||
|
||||
public ModsTab(Mod.Manager modManager, ModCollection.Manager collectionManager, ModFileSystemSelector selector, ModPanel panel,
|
||||
public ModsTab(ModManager modManager, CollectionManager collectionManager, ModFileSystemSelector selector, ModPanel panel,
|
||||
TutorialService tutorial, RedrawService redrawService, Configuration config, CollectionsTab collectionsTab)
|
||||
{
|
||||
_modManager = modManager;
|
||||
|
|
|
|||
|
|
@ -31,14 +31,14 @@ public class SettingsTab : ITab
|
|||
private readonly TutorialService _tutorial;
|
||||
private readonly Penumbra _penumbra;
|
||||
private readonly FileDialogService _fileDialog;
|
||||
private readonly Mod.Manager _modManager;
|
||||
private readonly ModManager _modManager;
|
||||
private readonly ModFileSystemSelector _selector;
|
||||
private readonly CharacterUtility _characterUtility;
|
||||
private readonly ResidentResourceManager _residentResources;
|
||||
private readonly DalamudServices _dalamud;
|
||||
|
||||
public SettingsTab(Configuration config, FontReloader fontReloader, TutorialService tutorial, Penumbra penumbra,
|
||||
FileDialogService fileDialog, Mod.Manager modManager, ModFileSystemSelector selector, CharacterUtility characterUtility,
|
||||
FileDialogService fileDialog, ModManager modManager, ModFileSystemSelector selector, CharacterUtility characterUtility,
|
||||
ResidentResourceManager residentResources, DalamudServices dalamud)
|
||||
{
|
||||
_config = config;
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue