Untangling the mods.

This commit is contained in:
Ottermandias 2023-04-17 09:35:54 +02:00
parent 1d82e882ed
commit 4972dd1c9f
39 changed files with 883 additions and 935 deletions

View file

@ -26,10 +26,10 @@ public class HttpApi : IDisposable
private readonly IPenumbraApi _api;
private WebServer? _server;
public HttpApi(IPenumbraApi api)
public HttpApi(Configuration config, IPenumbraApi api)
{
_api = api;
if (Penumbra.Config.EnableHttpApi)
if (config.EnableHttpApi)
CreateWebServer();
}

View file

@ -19,6 +19,7 @@ using Penumbra.Mods.Manager;
using Penumbra.Services;
using Penumbra.UI;
using Penumbra.Collections.Manager;
using Penumbra.Util;
namespace Penumbra.Api;
@ -39,7 +40,8 @@ public class IpcTester : IDisposable
private readonly ModSettings _modSettings;
private readonly Temporary _temporary;
public IpcTester(DalamudPluginInterface pi, PenumbraIpcProviders ipcProviders, ModManager modManager)
public IpcTester(DalamudPluginInterface pi, PenumbraIpcProviders ipcProviders, ModManager modManager, CollectionManager collections,
TempModManager tempMods, TempCollectionManager tempCollections, SaveService saveService)
{
_ipcProviders = ipcProviders;
_pluginState = new PluginState(pi);
@ -52,7 +54,7 @@ public class IpcTester : IDisposable
_meta = new Meta(pi);
_mods = new Mods(pi);
_modSettings = new ModSettings(pi);
_temporary = new Temporary(pi, modManager);
_temporary = new Temporary(pi, modManager, collections, tempMods, tempCollections, saveService);
UnsubscribeEvents();
}
@ -1151,11 +1153,20 @@ public class IpcTester : IDisposable
{
private readonly DalamudPluginInterface _pi;
private readonly ModManager _modManager;
private readonly CollectionManager _collections;
private readonly TempModManager _tempMods;
private readonly TempCollectionManager _tempCollections;
private readonly SaveService _saveService;
public Temporary(DalamudPluginInterface pi, ModManager modManager)
public Temporary(DalamudPluginInterface pi, ModManager modManager, CollectionManager collections, TempModManager tempMods,
TempCollectionManager tempCollections, SaveService saveService)
{
_pi = pi;
_modManager = modManager;
_pi = pi;
_modManager = modManager;
_collections = collections;
_tempMods = tempMods;
_tempCollections = tempCollections;
_saveService = saveService;
}
public string LastCreatedCollectionName = string.Empty;
@ -1223,7 +1234,7 @@ public class IpcTester : IDisposable
DrawIntro(Ipc.CreateTemporaryCollection.Label, "Copy Existing Collection");
if (ImGuiUtil.DrawDisabledButton("Copy##Collection", Vector2.Zero,
"Copies the effective list from the collection named in Temporary Mod Name...",
!Penumbra.CollectionManager.Storage.ByName(_tempModName, out var copyCollection))
!_collections.Storage.ByName(_tempModName, out var copyCollection))
&& copyCollection is { HasCache: true })
{
var files = copyCollection.ResolvedFiles.ToDictionary(kvp => kvp.Key.ToString(), kvp => kvp.Value.Path.ToString());
@ -1249,7 +1260,7 @@ public class IpcTester : IDisposable
public void DrawCollections()
{
using var collTree = ImRaii.TreeNode("Collections##TempCollections");
using var collTree = ImRaii.TreeNode("Temporary Collections##TempCollections");
if (!collTree)
return;
@ -1257,26 +1268,26 @@ public class IpcTester : IDisposable
if (!table)
return;
foreach (var collection in Penumbra.TempCollections.Values)
foreach (var collection in _tempCollections.Values)
{
ImGui.TableNextColumn();
var character = Penumbra.TempCollections.Collections.Where(p => p.Collection == collection).Select(p => p.DisplayName)
var character = _tempCollections.Collections.Where(p => p.Collection == collection).Select(p => p.DisplayName)
.FirstOrDefault()
?? "Unknown";
if (ImGui.Button($"Save##{collection.Name}"))
TemporaryMod.SaveTempCollection(_modManager, collection, character);
TemporaryMod.SaveTempCollection(_saveService, _modManager, collection, character);
ImGuiUtil.DrawTableColumn(collection.Name);
ImGuiUtil.DrawTableColumn(collection.ResolvedFiles.Count.ToString());
ImGuiUtil.DrawTableColumn(collection.MetaCache?.Count.ToString() ?? "0");
ImGuiUtil.DrawTableColumn(string.Join(", ",
Penumbra.TempCollections.Collections.Where(p => p.Collection == collection).Select(c => c.DisplayName)));
_tempCollections.Collections.Where(p => p.Collection == collection).Select(c => c.DisplayName)));
}
}
public void DrawMods()
{
using var modTree = ImRaii.TreeNode("Mods##TempMods");
using var modTree = ImRaii.TreeNode("Temporary Mods##TempMods");
if (!modTree)
return;
@ -1314,8 +1325,8 @@ public class IpcTester : IDisposable
if (table)
{
PrintList("All", Penumbra.TempMods.ModsForAllCollections);
foreach (var (collection, list) in Penumbra.TempMods.Mods)
PrintList("All", _tempMods.ModsForAllCollections);
foreach (var (collection, list) in _tempMods.Mods)
PrintList(collection.Name, list);
}
}

View file

@ -5,8 +5,9 @@ using System;
using System.Collections.Generic;
using Penumbra.Api.Enums;
using Penumbra.Api.Helpers;
using Penumbra.Mods;
using Penumbra.Collections.Manager;
using Penumbra.Mods.Manager;
using Penumbra.Util;
namespace Penumbra.Api;
@ -114,7 +115,8 @@ 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, ModManager modManager)
public PenumbraIpcProviders(DalamudPluginInterface pi, IPenumbraApi api, ModManager modManager, CollectionManager collections,
TempModManager tempMods, TempCollectionManager tempCollections, SaveService saveService)
{
Api = api;
@ -226,7 +228,7 @@ public class PenumbraIpcProviders : IDisposable
RemoveTemporaryModAll = Ipc.RemoveTemporaryModAll.Provider(pi, Api.RemoveTemporaryModAll);
RemoveTemporaryMod = Ipc.RemoveTemporaryMod.Provider(pi, Api.RemoveTemporaryMod);
Tester = new IpcTester(pi, this, modManager);
Tester = new IpcTester(pi, this, modManager, collections, tempMods, tempCollections, saveService);
Initialized.Invoke();
}

View file

@ -26,7 +26,7 @@ public class CollectionCache : IDisposable
private readonly ModCollection _collection;
public readonly SortedList<string, (SingleArray<IMod>, object?)> _changedItems = new();
public readonly Dictionary<Utf8GamePath, ModPath> ResolvedFiles = new();
public readonly MetaCache MetaManipulations;
public readonly MetaCache Meta;
public readonly Dictionary<IMod, SingleArray<ModConflicts>> _conflicts = new();
public IEnumerable<SingleArray<ModConflicts>> AllConflicts
@ -50,18 +50,18 @@ public class CollectionCache : IDisposable
// The cache reacts through events on its collection changing.
public CollectionCache(CollectionCacheManager manager, ModCollection collection)
{
_manager = manager;
_collection = collection;
MetaManipulations = new MetaCache(manager.MetaFileManager, _collection);
_manager = manager;
_collection = collection;
Meta = new MetaCache(manager.MetaFileManager, _collection);
}
public void Dispose()
{
MetaManipulations.Dispose();
Meta.Dispose();
}
~CollectionCache()
=> MetaManipulations.Dispose();
=> Meta.Dispose();
// Resolve a given game path according to this collection.
public FullPath? ResolvePath(Utf8GamePath gameResourcePath)
@ -119,16 +119,16 @@ public class CollectionCache : IDisposable
return ret;
}
/// <summary> Force a file to be resolved to a specific path regardless of conflicts. </summary>
/// <summary> Force a file to be resolved to a specific path regardless of conflicts. </summary>
internal void ForceFile(Utf8GamePath path, FullPath fullPath)
{
if (CheckFullPath(path, fullPath))
ResolvedFiles[path] = new ModPath(Mod.ForcedFiles, fullPath);
}
/// <summary> Force a file resolve to be removed. </summary>
/// <summary> Force a file resolve to be removed. </summary>
internal void RemoveFile(Utf8GamePath path)
=> ResolvedFiles.Remove(path);
=> ResolvedFiles.Remove(path);
public void ReloadMod(IMod mod, bool addMetaChanges)
{
@ -151,8 +151,8 @@ public class CollectionCache : IDisposable
foreach (var manipulation in mod.AllSubMods.SelectMany(s => s.Manipulations))
{
if (MetaManipulations.TryGetValue(manipulation, out var registeredMod) && registeredMod == mod)
MetaManipulations.RevertMod(manipulation);
if (Meta.TryGetValue(manipulation, out var registeredMod) && registeredMod == mod)
Meta.RevertMod(manipulation);
}
_conflicts.Remove(mod);
@ -175,11 +175,7 @@ public class CollectionCache : IDisposable
if (addMetaChanges)
{
++_collection.ChangeCounter;
if (_collection == Penumbra.CollectionManager.Active.Default && Penumbra.CharacterUtility.Ready && Penumbra.Config.EnableMods)
{
Penumbra.ResidentResources.Reload();
MetaManipulations.SetFiles();
}
_manager.MetaFileManager.ApplyDefaultFiles(_collection);
}
}
@ -225,11 +221,7 @@ public class CollectionCache : IDisposable
if ((mod is TemporaryMod temp ? temp.TotalManipulations : Penumbra.ModCaches[mod.Index].TotalManipulations) > 0)
AddMetaFiles();
if (_collection == Penumbra.CollectionManager.Active.Default && Penumbra.CharacterUtility.Ready && Penumbra.Config.EnableMods)
{
Penumbra.ResidentResources.Reload();
MetaManipulations.SetFiles();
}
_manager.MetaFileManager.ApplyDefaultFiles(_collection);
}
}
@ -335,9 +327,9 @@ public class CollectionCache : IDisposable
// Inside the same mod, conflicts are not recorded.
private void AddManipulation(MetaManipulation manip, IMod mod)
{
if (!MetaManipulations.TryGetValue(manip, out var existingMod))
if (!Meta.TryGetValue(manip, out var existingMod))
{
MetaManipulations.ApplyMod(manip, mod);
Meta.ApplyMod(manip, mod);
return;
}
@ -346,13 +338,13 @@ public class CollectionCache : IDisposable
return;
if (AddConflict(manip, mod, existingMod))
MetaManipulations.ApplyMod(manip, mod);
Meta.ApplyMod(manip, mod);
}
// Add all necessary meta file redirects.
public void AddMetaFiles()
=> MetaManipulations.SetImcFiles();
=> Meta.SetImcFiles();
// Identify and record all manipulated objects for this entire collection.
@ -367,7 +359,7 @@ public class CollectionCache : IDisposable
_changedItems.Clear();
// Skip IMCs because they would result in far too many false-positive items,
// since they are per set instead of per item-slot/item/variant.
var identifier = Penumbra.Identifier;
var identifier = _manager.MetaFileManager.Identifier.AwaitedService;
var items = new SortedList<string, object?>(512);
void AddItems(IMod mod)
@ -391,7 +383,7 @@ public class CollectionCache : IDisposable
AddItems(modPath.Mod);
}
foreach (var (manip, mod) in MetaManipulations)
foreach (var (manip, mod) in Meta)
{
ModCacheManager.ComputeChangedItems(identifier, items, manip);
AddItems(mod);

View file

@ -112,7 +112,7 @@ public class CollectionCacheManager : IDisposable
private void FullRecalculation(ModCollection collection, CollectionCache cache)
{
cache.ResolvedFiles.Clear();
cache.MetaManipulations.Reset();
cache.Meta.Reset();
cache._conflicts.Clear();
// Add all forced redirects.

View file

@ -10,7 +10,6 @@ using Penumbra.GameData.Actors;
using Penumbra.Services;
using Penumbra.UI;
using Penumbra.Util;
using static OtterGui.Raii.ImRaii;
namespace Penumbra.Collections.Manager;
@ -22,7 +21,7 @@ public class ActiveCollections : ISavable, IDisposable
private readonly CommunicatorService _communicator;
private readonly SaveService _saveService;
public ActiveCollections(CollectionStorage storage, ActorService actors, CommunicatorService communicator, SaveService saveService)
public ActiveCollections(Configuration config, CollectionStorage storage, ActorService actors, CommunicatorService communicator, SaveService saveService)
{
_storage = storage;
_communicator = communicator;
@ -30,7 +29,7 @@ public class ActiveCollections : ISavable, IDisposable
Current = storage.DefaultNamed;
Default = storage.DefaultNamed;
Interface = storage.DefaultNamed;
Individuals = new IndividualCollections(actors.AwaitedService);
Individuals = new IndividualCollections(actors.AwaitedService, config);
_communicator.CollectionChange.Subscribe(OnCollectionChange);
LoadCollections();
UpdateCurrentCollectionInUse();

View file

@ -9,10 +9,10 @@ using Penumbra.String;
namespace Penumbra.Collections.Manager;
public sealed partial class IndividualCollections : IReadOnlyList< (string DisplayName, ModCollection Collection) >
public sealed partial class IndividualCollections : IReadOnlyList<(string DisplayName, ModCollection Collection)>
{
public IEnumerator< (string DisplayName, ModCollection Collection) > GetEnumerator()
=> _assignments.Select( t => ( t.DisplayName, t.Collection ) ).GetEnumerator();
public IEnumerator<(string DisplayName, ModCollection Collection)> GetEnumerator()
=> _assignments.Select(t => (t.DisplayName, t.Collection)).GetEnumerator();
IEnumerator IEnumerable.GetEnumerator()
=> GetEnumerator();
@ -20,59 +20,51 @@ public sealed partial class IndividualCollections : IReadOnlyList< (string Displ
public int Count
=> _assignments.Count;
public (string DisplayName, ModCollection Collection) this[ int index ]
=> ( _assignments[ index ].DisplayName, _assignments[ index ].Collection );
public (string DisplayName, ModCollection Collection) this[int index]
=> (_assignments[index].DisplayName, _assignments[index].Collection);
public bool TryGetCollection( ActorIdentifier identifier, [NotNullWhen( true )] out ModCollection? collection )
public bool TryGetCollection(ActorIdentifier identifier, [NotNullWhen(true)] out ModCollection? collection)
{
if( Count == 0 )
if (Count == 0)
{
collection = null;
return false;
}
switch( identifier.Type )
switch (identifier.Type)
{
case IdentifierType.Player: return CheckWorlds( identifier, out collection );
case IdentifierType.Player: return CheckWorlds(identifier, out collection);
case IdentifierType.Retainer:
{
if( _individuals.TryGetValue( identifier, out collection ) )
{
if (_individuals.TryGetValue(identifier, out collection))
return true;
}
if( identifier.Retainer is not ActorIdentifier.RetainerType.Mannequin && Penumbra.Config.UseOwnerNameForCharacterCollection )
{
return CheckWorlds( _actorManager.GetCurrentPlayer(), out collection );
}
if (identifier.Retainer is not ActorIdentifier.RetainerType.Mannequin && _config.UseOwnerNameForCharacterCollection)
return CheckWorlds(_actorManager.GetCurrentPlayer(), out collection);
break;
}
case IdentifierType.Owned:
{
if( CheckWorlds( identifier, out collection! ) )
{
if (CheckWorlds(identifier, out collection!))
return true;
}
// Handle generic NPC
var npcIdentifier = _actorManager.CreateIndividualUnchecked( IdentifierType.Npc, ByteString.Empty, ushort.MaxValue, identifier.Kind, identifier.DataId );
if( npcIdentifier.IsValid && _individuals.TryGetValue( npcIdentifier, out collection ) )
{
var npcIdentifier = _actorManager.CreateIndividualUnchecked(IdentifierType.Npc, ByteString.Empty, ushort.MaxValue,
identifier.Kind, identifier.DataId);
if (npcIdentifier.IsValid && _individuals.TryGetValue(npcIdentifier, out collection))
return true;
}
// Handle Ownership.
if( Penumbra.Config.UseOwnerNameForCharacterCollection )
{
identifier = _actorManager.CreateIndividualUnchecked( IdentifierType.Player, identifier.PlayerName, identifier.HomeWorld, ObjectKind.None, uint.MaxValue );
return CheckWorlds( identifier, out collection );
}
if (!_config.UseOwnerNameForCharacterCollection)
return false;
return false;
identifier = _actorManager.CreateIndividualUnchecked(IdentifierType.Player, identifier.PlayerName, identifier.HomeWorld,
ObjectKind.None, uint.MaxValue);
return CheckWorlds(identifier, out collection);
}
case IdentifierType.Npc: return _individuals.TryGetValue( identifier, out collection );
case IdentifierType.Special: return CheckWorlds( ConvertSpecialIdentifier( identifier ).Item1, out collection );
case IdentifierType.Npc: return _individuals.TryGetValue(identifier, out collection);
case IdentifierType.Special: return CheckWorlds(ConvertSpecialIdentifier(identifier).Item1, out collection);
}
collection = null;
@ -94,80 +86,71 @@ public sealed partial class IndividualCollections : IReadOnlyList< (string Displ
Invalid,
}
public (ActorIdentifier, SpecialResult) ConvertSpecialIdentifier( ActorIdentifier identifier )
public (ActorIdentifier, SpecialResult) ConvertSpecialIdentifier(ActorIdentifier identifier)
{
if( identifier.Type != IdentifierType.Special )
{
return ( identifier, SpecialResult.Invalid );
}
if (identifier.Type != IdentifierType.Special)
return (identifier, SpecialResult.Invalid);
if( _actorManager.ResolvePartyBannerPlayer( identifier.Special, out var id ) )
{
return ( id, SpecialResult.PartyBanner );
}
if (_actorManager.ResolvePartyBannerPlayer(identifier.Special, out var id))
return (id, SpecialResult.PartyBanner);
if( _actorManager.ResolvePvPBannerPlayer( identifier.Special, out id ) )
{
return ( id, SpecialResult.PvPBanner );
}
if (_actorManager.ResolvePvPBannerPlayer(identifier.Special, out id))
return (id, SpecialResult.PvPBanner);
if( _actorManager.ResolveMahjongPlayer( identifier.Special, out id ) )
{
return ( id, SpecialResult.Mahjong );
}
if (_actorManager.ResolveMahjongPlayer(identifier.Special, out id))
return (id, SpecialResult.Mahjong);
switch( identifier.Special )
switch (identifier.Special)
{
case ScreenActor.CharacterScreen when Penumbra.Config.UseCharacterCollectionInMainWindow: return ( _actorManager.GetCurrentPlayer(), SpecialResult.CharacterScreen );
case ScreenActor.FittingRoom when Penumbra.Config.UseCharacterCollectionInTryOn: return ( _actorManager.GetCurrentPlayer(), SpecialResult.FittingRoom );
case ScreenActor.DyePreview when Penumbra.Config.UseCharacterCollectionInTryOn: return ( _actorManager.GetCurrentPlayer(), SpecialResult.DyePreview );
case ScreenActor.Portrait when Penumbra.Config.UseCharacterCollectionsInCards: return ( _actorManager.GetCurrentPlayer(), SpecialResult.Portrait );
case ScreenActor.CharacterScreen when _config.UseCharacterCollectionInMainWindow:
return (_actorManager.GetCurrentPlayer(), SpecialResult.CharacterScreen);
case ScreenActor.FittingRoom when _config.UseCharacterCollectionInTryOn:
return (_actorManager.GetCurrentPlayer(), SpecialResult.FittingRoom);
case ScreenActor.DyePreview when _config.UseCharacterCollectionInTryOn:
return (_actorManager.GetCurrentPlayer(), SpecialResult.DyePreview);
case ScreenActor.Portrait when _config.UseCharacterCollectionsInCards:
return (_actorManager.GetCurrentPlayer(), SpecialResult.Portrait);
case ScreenActor.ExamineScreen:
{
identifier = _actorManager.GetInspectPlayer();
if( identifier.IsValid )
{
return ( Penumbra.Config.UseCharacterCollectionInInspect ? identifier : ActorIdentifier.Invalid, SpecialResult.Inspect );
}
if (identifier.IsValid)
return (_config.UseCharacterCollectionInInspect ? identifier : ActorIdentifier.Invalid, SpecialResult.Inspect);
identifier = _actorManager.GetCardPlayer();
if( identifier.IsValid )
{
return ( Penumbra.Config.UseCharacterCollectionInInspect ? identifier : ActorIdentifier.Invalid, SpecialResult.Card );
}
if (identifier.IsValid)
return (_config.UseCharacterCollectionInInspect ? identifier : ActorIdentifier.Invalid, SpecialResult.Card);
return Penumbra.Config.UseCharacterCollectionInTryOn ? ( _actorManager.GetGlamourPlayer(), SpecialResult.Glamour ) : ( identifier, SpecialResult.Invalid );
return _config.UseCharacterCollectionInTryOn
? (_actorManager.GetGlamourPlayer(), SpecialResult.Glamour)
: (identifier, SpecialResult.Invalid);
}
default: return ( identifier, SpecialResult.Invalid );
default: return (identifier, SpecialResult.Invalid);
}
}
public bool TryGetCollection( GameObject? gameObject, out ModCollection? collection )
=> TryGetCollection( _actorManager.FromObject( gameObject, true, false, false ), out collection );
public bool TryGetCollection(GameObject? gameObject, out ModCollection? collection)
=> TryGetCollection(_actorManager.FromObject(gameObject, true, false, false), out collection);
public unsafe bool TryGetCollection( FFXIVClientStructs.FFXIV.Client.Game.Object.GameObject* gameObject, out ModCollection? collection )
=> TryGetCollection( _actorManager.FromObject( gameObject, out _, true, false, false ), out collection );
public unsafe bool TryGetCollection(FFXIVClientStructs.FFXIV.Client.Game.Object.GameObject* gameObject, out ModCollection? collection)
=> TryGetCollection(_actorManager.FromObject(gameObject, out _, true, false, false), out collection);
private bool CheckWorlds( ActorIdentifier identifier, out ModCollection? collection )
private bool CheckWorlds(ActorIdentifier identifier, out ModCollection? collection)
{
if( !identifier.IsValid )
if (!identifier.IsValid)
{
collection = null;
return false;
}
if( _individuals.TryGetValue( identifier, out collection ) )
{
if (_individuals.TryGetValue(identifier, out collection))
return true;
}
identifier = _actorManager.CreateIndividualUnchecked( identifier.Type, identifier.PlayerName, ushort.MaxValue, identifier.Kind, identifier.DataId );
if( identifier.IsValid && _individuals.TryGetValue( identifier, out collection ) )
{
identifier = _actorManager.CreateIndividualUnchecked(identifier.Type, identifier.PlayerName, ushort.MaxValue, identifier.Kind,
identifier.DataId);
if (identifier.IsValid && _individuals.TryGetValue(identifier, out collection))
return true;
}
collection = null;
return false;
}
}
}

View file

@ -12,6 +12,7 @@ namespace Penumbra.Collections.Manager;
public sealed partial class IndividualCollections
{
private readonly Configuration _config;
private readonly ActorManager _actorManager;
private readonly List<(string DisplayName, IReadOnlyList<ActorIdentifier> Identifiers, ModCollection Collection)> _assignments = new();
private readonly Dictionary<ActorIdentifier, ModCollection> _individuals = new();
@ -20,11 +21,17 @@ public sealed partial class IndividualCollections
=> _assignments;
// TODO
public IndividualCollections(ActorService actorManager)
=> _actorManager = actorManager.AwaitedService;
public IndividualCollections(ActorService actorManager, Configuration config)
{
_config = config;
_actorManager = actorManager.AwaitedService;
}
public IndividualCollections(ActorManager actorManager)
=> _actorManager = actorManager;
public IndividualCollections(ActorManager actorManager, Configuration config)
{
_actorManager = actorManager;
_config = config;
}
public enum AddResult
{
@ -234,7 +241,7 @@ public sealed partial class IndividualCollections
=> identifier.IsValid ? Index(DisplayString(identifier)) : -1;
private string DisplayString(ActorIdentifier identifier)
{
{
return identifier.Type switch
{
IdentifierType.Player => $"{identifier.PlayerName} ({_actorManager.Data.ToWorldName(identifier.HomeWorld)})",

View file

@ -19,11 +19,11 @@ public class TempCollectionManager : IDisposable
private readonly CollectionStorage _storage;
private readonly Dictionary<string, ModCollection> _customCollections = new();
public TempCollectionManager(CommunicatorService communicator, ActorService actors, CollectionStorage storage)
public TempCollectionManager(Configuration config, CommunicatorService communicator, ActorService actors, CollectionStorage storage)
{
_communicator = communicator;
_storage = storage;
Collections = new IndividualCollections(actors);
Collections = new IndividualCollections(actors, config);
_communicator.TemporaryGlobalModChange.Subscribe(OnGlobalModChange);
}

View file

@ -49,12 +49,12 @@ public partial class ModCollection
// Obtain data from the cache.
internal MetaCache? MetaCache
=> _cache?.MetaManipulations;
=> _cache?.Meta;
public bool GetImcFile(Utf8GamePath path, [NotNullWhen(true)] out ImcFile? file)
{
if (_cache != null)
return _cache.MetaManipulations.GetImcFile(path, out file);
return _cache.Meta.GetImcFile(path, out file);
file = null;
return false;
@ -80,7 +80,7 @@ public partial class ModCollection
}
else
{
_cache.MetaManipulations.SetFiles();
_cache.Meta.SetFiles();
Penumbra.Log.Debug($"Set CharacterUtility resources for collection {Name}.");
}
}
@ -90,28 +90,28 @@ public partial class ModCollection
if (_cache == null)
Penumbra.CharacterUtility.ResetResource(idx);
else
_cache.MetaManipulations.SetFile(idx);
_cache.Meta.SetFile(idx);
}
// Used for short periods of changed files.
public CharacterUtility.MetaList.MetaReverter TemporarilySetEqdpFile(GenderRace genderRace, bool accessory)
=> _cache?.MetaManipulations.TemporarilySetEqdpFile(genderRace, accessory)
=> _cache?.Meta.TemporarilySetEqdpFile(genderRace, accessory)
?? Penumbra.CharacterUtility.TemporarilyResetResource(Interop.Structs.CharacterUtilityData.EqdpIdx(genderRace, accessory));
public CharacterUtility.MetaList.MetaReverter TemporarilySetEqpFile()
=> _cache?.MetaManipulations.TemporarilySetEqpFile()
=> _cache?.Meta.TemporarilySetEqpFile()
?? Penumbra.CharacterUtility.TemporarilyResetResource(MetaIndex.Eqp);
public CharacterUtility.MetaList.MetaReverter TemporarilySetGmpFile()
=> _cache?.MetaManipulations.TemporarilySetGmpFile()
=> _cache?.Meta.TemporarilySetGmpFile()
?? Penumbra.CharacterUtility.TemporarilyResetResource(MetaIndex.Gmp);
public CharacterUtility.MetaList.MetaReverter TemporarilySetCmpFile()
=> _cache?.MetaManipulations.TemporarilySetCmpFile()
=> _cache?.Meta.TemporarilySetCmpFile()
?? Penumbra.CharacterUtility.TemporarilyResetResource(MetaIndex.HumanCmp);
public CharacterUtility.MetaList.MetaReverter TemporarilySetEstFile(EstManipulation.EstType type)
=> _cache?.MetaManipulations.TemporarilySetEstFile(type)
=> _cache?.Meta.TemporarilySetEstFile(type)
?? Penumbra.CharacterUtility.TemporarilyResetResource((MetaIndex)type);
}

View file

@ -550,19 +550,19 @@ public class CommandHandler : IDisposable
private void Print(string text)
{
if (Penumbra.Config.PrintSuccessfulCommandsToChat)
if (_config.PrintSuccessfulCommandsToChat)
_chat.Print(text);
}
private void Print(DefaultInterpolatedStringHandler text)
{
if (Penumbra.Config.PrintSuccessfulCommandsToChat)
if (_config.PrintSuccessfulCommandsToChat)
_chat.Print(text.ToStringAndClear());
}
private void Print(Func<SeString> text)
{
if (Penumbra.Config.PrintSuccessfulCommandsToChat)
if (_config.PrintSuccessfulCommandsToChat)
_chat.Print(text());
}
}

View file

@ -12,7 +12,7 @@ internal static class DefaultTexToolsData
}
[Serializable]
internal class SimpleMod
public class SimpleMod
{
public string Name = string.Empty;
public string Category = string.Empty;
@ -24,14 +24,14 @@ internal class SimpleMod
}
[Serializable]
internal class ModPackPage
public class ModPackPage
{
public int PageIndex = 0;
public ModGroup[] ModGroups = Array.Empty<ModGroup>();
}
[Serializable]
internal class ModGroup
public class ModGroup
{
public string GroupName = string.Empty;
public GroupType SelectionType = GroupType.Single;
@ -40,7 +40,7 @@ internal class ModGroup
}
[Serializable]
internal class OptionList
public class OptionList
{
public string Name = string.Empty;
public string Description = string.Empty;
@ -52,7 +52,7 @@ internal class OptionList
}
[Serializable]
internal class ExtendedModPack
public class ExtendedModPack
{
public string PackVersion = string.Empty;
public string Name = DefaultTexToolsData.Name;
@ -65,7 +65,7 @@ internal class ExtendedModPack
}
[Serializable]
internal class SimpleModPack
public class SimpleModPack
{
public string TtmpVersion = string.Empty;
public string Name = DefaultTexToolsData.Name;

View file

@ -18,11 +18,13 @@ namespace Penumbra.Import;
public partial class TexToolsImporter
{
// Extract regular compressed archives that are folders containing penumbra-formatted mods.
// The mod has to either contain a meta.json at top level, or one folder deep.
// If the meta.json is one folder deep, all other files have to be in the same folder.
// The extracted folder gets its name either from that one top-level folder or from the mod name.
// All data is extracted without manipulation of the files or metadata.
/// <summary>
/// Extract regular compressed archives that are folders containing penumbra-formatted mods.
/// The mod has to either contain a meta.json at top level, or one folder deep.
/// If the meta.json is one folder deep, all other files have to be in the same folder.
/// The extracted folder gets its name either from that one top-level folder or from the mod name.
/// All data is extracted without manipulation of the files or metadata.
/// </summary>
private DirectoryInfo HandleRegularArchive( FileInfo modPackFile )
{
using var zfs = modPackFile.OpenRead();
@ -111,7 +113,7 @@ public partial class TexToolsImporter
}
_currentModDirectory.Refresh();
ModCreator.SplitMultiGroups( _currentModDirectory );
_modManager.Creator.SplitMultiGroups( _currentModDirectory );
return _currentModDirectory;
}

View file

@ -40,7 +40,7 @@ public partial class TexToolsImporter
// Open the mod data file from the mod pack as a SqPackStream
_streamDisposer = GetSqPackStreamStream( extractedModPack, "TTMPD.mpd" );
ExtractSimpleModList( _currentModDirectory, modList );
ModCreator.CreateDefaultFiles( _currentModDirectory );
_modManager.Creator.CreateDefaultFiles( _currentModDirectory );
ResetStreamDisposer();
return _currentModDirectory;
}
@ -97,7 +97,7 @@ public partial class TexToolsImporter
// Open the mod data file from the mod pack as a SqPackStream
_streamDisposer = GetSqPackStreamStream( extractedModPack, "TTMPD.mpd" );
ExtractSimpleModList( _currentModDirectory, modList.SimpleModsList );
ModCreator.CreateDefaultFiles( _currentModDirectory );
_modManager.Creator.CreateDefaultFiles( _currentModDirectory );
ResetStreamDisposer();
return _currentModDirectory;
}
@ -185,7 +185,7 @@ public partial class TexToolsImporter
var optionFolder = ModCreator.NewSubFolderName( groupFolder, option.Name )
?? new DirectoryInfo( Path.Combine( groupFolder.FullName, $"Option {i + optionIdx + 1}" ) );
ExtractSimpleModList( optionFolder, option.ModsJsons );
options.Add( ModCreator.CreateSubMod( _currentModDirectory, optionFolder, option ) );
options.Add( _modManager.Creator.CreateSubMod( _currentModDirectory, optionFolder, option ) );
if( option.IsChecked )
{
defaultSettings = group.SelectionType == GroupType.Multi
@ -211,7 +211,7 @@ public partial class TexToolsImporter
}
}
ModCreator.CreateOptionGroup( _currentModDirectory, group.SelectionType, name, groupPriority, groupPriority,
_modManager.Creator.CreateOptionGroup( _currentModDirectory, group.SelectionType, name, groupPriority, groupPriority,
defaultSettings ?? 0, group.Description, options );
++groupPriority;
}
@ -219,7 +219,7 @@ public partial class TexToolsImporter
}
ResetStreamDisposer();
ModCreator.CreateDefaultFiles( _currentModDirectory );
_modManager.Creator.CreateDefaultFiles( _currentModDirectory );
return _currentModDirectory;
}

View file

@ -12,7 +12,26 @@ using Penumbra.Meta.Manipulations;
namespace Penumbra.Import;
public partial class TexToolsMeta
{
{
public static void WriteTexToolsMeta(MetaFileManager manager, IEnumerable<MetaManipulation> manipulations, DirectoryInfo basePath)
{
var files = ConvertToTexTools(manager, 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}");
}
}
}
public static Dictionary< string, byte[] > ConvertToTexTools( MetaFileManager manager, IEnumerable< MetaManipulation > manips )
{
var ret = new Dictionary< string, byte[] >();

View file

@ -9,6 +9,7 @@ using Penumbra.GameData;
using Penumbra.Interop.Services;
using Penumbra.Interop.Structs;
using Penumbra.Meta.Files;
using Penumbra.Services;
using ResidentResourceManager = Penumbra.Interop.Services.ResidentResourceManager;
namespace Penumbra.Meta;
@ -21,9 +22,10 @@ public unsafe class MetaFileManager
internal readonly DataManager GameData;
internal readonly ActiveCollections ActiveCollections;
internal readonly ValidityChecker ValidityChecker;
internal readonly IdentifierService Identifier;
public MetaFileManager(CharacterUtility characterUtility, ResidentResourceManager residentResources, DataManager gameData,
ActiveCollections activeCollections, Configuration config, ValidityChecker validityChecker)
ActiveCollections activeCollections, Configuration config, ValidityChecker validityChecker, IdentifierService identifier)
{
CharacterUtility = characterUtility;
ResidentResources = residentResources;
@ -31,6 +33,7 @@ public unsafe class MetaFileManager
ActiveCollections = activeCollections;
Config = config;
ValidityChecker = validityChecker;
Identifier = identifier;
SignatureHelper.Initialise(this);
}
@ -55,7 +58,7 @@ public unsafe class MetaFileManager
return;
ResidentResources.Reload();
collection._cache?.MetaManipulations.SetFiles();
collection._cache?.Meta.SetFiles();
}
/// <summary>

View file

@ -244,7 +244,7 @@ public class DuplicateManager
try
{
var mod = new Mod(modDirectory);
mod.Reload(_modManager, true, out _);
_modManager.Creator.ReloadMod(mod, true, out _);
Finished = false;
_files.UpdateAll(mod, mod.Default);

View file

@ -13,5 +13,5 @@ public interface IMod
public ISubMod Default { get; }
public IReadOnlyList< IModGroup > Groups { get; }
public IEnumerable< ISubMod > AllSubMods { get; }
public IEnumerable< SubMod > AllSubMods { get; }
}

View file

@ -160,7 +160,7 @@ public class ModFileEditor
if (deletions <= 0)
return;
mod.Reload(_modManager, false, out _);
_modManager.Creator.ReloadMod(mod, false, out _);
_files.UpdateAll(mod, option);
}

View file

@ -221,10 +221,10 @@ public class ModCacheManager : IDisposable, IReadOnlyList<ModCache>
if (!_identifier.Valid)
return;
foreach (var gamePath in mod.AllRedirects)
foreach (var gamePath in mod.AllSubMods.SelectMany(m => m.Files.Keys.Concat(m.FileSwaps.Keys)))
_identifier.AwaitedService.Identify(cache.ChangedItems, gamePath.ToString());
foreach (var manip in mod.AllManipulations)
foreach (var manip in mod.AllSubMods.SelectMany(m => m.Manipulations))
ComputeChangedItems(_identifier.AwaitedService, cache.ChangedItems, manip);
cache.LowerChangedItemsString = string.Join("\0", cache.ChangedItems.Keys.Select(k => k.ToLowerInvariant()));

View file

@ -116,7 +116,7 @@ public class ModDataEditor
return changes;
}
public ModDataChangeType LoadMeta(Mod mod)
public ModDataChangeType LoadMeta(ModCreator creator, Mod mod)
{
var metaFile = _saveService.FileNames.ModMetaPath(mod);
if (!File.Exists(metaFile))
@ -171,7 +171,7 @@ public class ModDataEditor
}
if (newFileVersion != ModMeta.FileVersion)
if (ModMigration.Migrate(_saveService, mod, json, ref newFileVersion))
if (ModMigration.Migrate(creator, _saveService, mod, json, ref newFileVersion))
{
changes |= ModDataChangeType.Migration;
_saveService.ImmediateSave(new ModMeta(mod));

View file

@ -15,14 +15,14 @@ public sealed class ModFileSystem : FileSystem<Mod>, IDisposable, ISavable
{
private readonly ModManager _modManager;
private readonly CommunicatorService _communicator;
private readonly FilenameService _files;
private readonly SaveService _saveService;
// Create a new ModFileSystem from the currently loaded mods and the current sort order file.
public ModFileSystem(ModManager modManager, CommunicatorService communicator, FilenameService files)
public ModFileSystem(ModManager modManager, CommunicatorService communicator, SaveService saveService)
{
_modManager = modManager;
_communicator = communicator;
_files = files;
_saveService = saveService;
Reload();
Changed += OnChange;
_communicator.ModDiscoveryFinished.Subscribe(Reload);
@ -66,8 +66,8 @@ public sealed class ModFileSystem : FileSystem<Mod>, IDisposable, ISavable
private void Reload()
{
// TODO
if (Load(new FileInfo(_files.FilesystemFile), _modManager, ModToIdentifier, ModToName))
Penumbra.SaveService.ImmediateSave(this);
if (Load(new FileInfo(_saveService.FileNames.FilesystemFile), _modManager, ModToIdentifier, ModToName))
_saveService.ImmediateSave(this);
Penumbra.Log.Debug("Reloaded mod filesystem.");
}
@ -76,7 +76,7 @@ public sealed class ModFileSystem : FileSystem<Mod>, IDisposable, ISavable
private void OnChange(FileSystemChangeType type, IPath _1, IPath? _2, IPath? _3)
{
if (type != FileSystemChangeType.Reload)
Penumbra.SaveService.QueueSave(this);
_saveService.QueueSave(this);
}
// Update sort order when defaulted mod names change.
@ -111,7 +111,7 @@ public sealed class ModFileSystem : FileSystem<Mod>, IDisposable, ISavable
break;
case ModPathChangeType.Moved:
Penumbra.SaveService.QueueSave(this);
_saveService.QueueSave(this);
break;
case ModPathChangeType.Reloaded:
// Nothing

View file

@ -24,18 +24,21 @@ public sealed class ModManager : ModStorage
private readonly Configuration _config;
private readonly CommunicatorService _communicator;
public readonly ModCreator Creator;
public readonly ModDataEditor DataEditor;
public readonly ModOptionEditor OptionEditor;
public DirectoryInfo BasePath { get; private set; } = null!;
public bool Valid { get; private set; }
public ModManager(Configuration config, CommunicatorService communicator, ModDataEditor dataEditor, ModOptionEditor optionEditor)
public ModManager(Configuration config, CommunicatorService communicator, ModDataEditor dataEditor, ModOptionEditor optionEditor,
ModCreator creator)
{
_config = config;
_communicator = communicator;
DataEditor = dataEditor;
OptionEditor = optionEditor;
Creator = creator;
SetBaseDirectory(config.ModDirectory, true);
DiscoverMods();
}
@ -73,8 +76,8 @@ public sealed class ModManager : ModStorage
if (this.Any(m => m.ModPath.Name == modFolder.Name))
return;
ModCreator.SplitMultiGroups(modFolder);
var mod = Mod.LoadMod(this, modFolder, true);
Creator.SplitMultiGroups(modFolder);
var mod = Creator.LoadMod(modFolder, true);
if (mod == null)
return;
@ -119,7 +122,7 @@ public sealed class ModManager : ModStorage
var oldName = mod.Name;
_communicator.ModPathChanged.Invoke(ModPathChangeType.StartingReload, mod, mod.ModPath, mod.ModPath);
if (!mod.Reload(Penumbra.ModManager, true, out var metaChange))
if (!Creator.ReloadMod(mod, true, out var metaChange))
{
Penumbra.Log.Warning(mod.Name.Length == 0
? $"Reloading mod {oldName} has failed, new name is empty. Deleting instead."
@ -185,7 +188,7 @@ public sealed class ModManager : ModStorage
dir.Refresh();
mod.ModPath = dir;
if (!mod.Reload(Penumbra.ModManager, false, out var metaChange))
if (!Creator.ReloadMod(mod, false, out var metaChange))
{
Penumbra.Log.Error($"Error reloading moved mod {mod.Name}.");
return;
@ -307,7 +310,7 @@ public sealed class ModManager : ModStorage
var queue = new ConcurrentQueue<Mod>();
Parallel.ForEach(BasePath.EnumerateDirectories(), options, dir =>
{
var mod = Mod.LoadMod(this, dir, false);
var mod = Creator.LoadMod(dir, false);
if (mod != null)
queue.Enqueue(mod);
});

View file

@ -20,8 +20,8 @@ public static partial class ModMigration
[GeneratedRegex("^group_", RegexOptions.Compiled)]
private static partial Regex GroupStartRegex();
public static bool Migrate(SaveService saveService, Mod mod, JObject json, ref uint fileVersion)
=> MigrateV0ToV1(saveService, mod, json, ref fileVersion) || MigrateV1ToV2(mod, ref fileVersion) || MigrateV2ToV3(mod, ref fileVersion);
public static bool Migrate(ModCreator creator, SaveService saveService, Mod mod, JObject json, ref uint fileVersion)
=> MigrateV0ToV1(creator, saveService, mod, json, ref fileVersion) || MigrateV1ToV2(saveService, mod, ref fileVersion) || MigrateV2ToV3(mod, ref fileVersion);
private static bool MigrateV2ToV3(Mod _, ref uint fileVersion)
{
@ -33,13 +33,13 @@ public static partial class ModMigration
return true;
}
private static bool MigrateV1ToV2(Mod mod, ref uint fileVersion)
private static bool MigrateV1ToV2(SaveService saveService, Mod mod, ref uint fileVersion)
{
if (fileVersion > 1)
return false;
if (!mod.GroupFiles.All(g => GroupRegex().IsMatch(g.Name)))
foreach (var (group, index) in mod.GroupFiles.WithIndex().ToArray())
if (!saveService.FileNames.GetOptionGroupFiles(mod).All(g => GroupRegex().IsMatch(g.Name)))
foreach (var (group, index) in saveService.FileNames.GetOptionGroupFiles(mod).WithIndex().ToArray())
{
var newName = GroupStartRegex().Replace(group.Name, $"group_{index + 1:D3}_");
try
@ -58,7 +58,7 @@ public static partial class ModMigration
return true;
}
private static bool MigrateV0ToV1(SaveService saveService, Mod mod, JObject json, ref uint fileVersion)
private static bool MigrateV0ToV1(ModCreator creator, SaveService saveService, Mod mod, JObject json, ref uint fileVersion)
{
if (fileVersion > 0)
return false;
@ -69,21 +69,21 @@ public static partial class ModMigration
var priority = 1;
var seenMetaFiles = new HashSet<FullPath>();
foreach (var group in groups.Values)
ConvertGroup(mod, group, ref priority, seenMetaFiles);
ConvertGroup(creator, mod, group, ref priority, seenMetaFiles);
foreach (var unusedFile in mod.FindUnusedFiles().Where(f => !seenMetaFiles.Contains(f)))
{
if (unusedFile.ToGamePath(mod.ModPath, out var gamePath)
&& !mod._default.FileData.TryAdd(gamePath, unusedFile))
Penumbra.Log.Error($"Could not add {gamePath} because it already points to {mod._default.FileData[gamePath]}.");
&& !mod.Default.FileData.TryAdd(gamePath, unusedFile))
Penumbra.Log.Error($"Could not add {gamePath} because it already points to {mod.Default.FileData[gamePath]}.");
}
mod._default.FileSwapData.Clear();
mod._default.FileSwapData.EnsureCapacity(swaps.Count);
mod.Default.FileSwapData.Clear();
mod.Default.FileSwapData.EnsureCapacity(swaps.Count);
foreach (var (gamePath, swapPath) in swaps)
mod._default.FileSwapData.Add(gamePath, swapPath);
mod.Default.FileSwapData.Add(gamePath, swapPath);
mod._default.IncorporateMetaChanges(mod.ModPath, true);
creator.IncorporateMetaChanges(mod.Default, mod.ModPath, true);
foreach (var (_, index) in mod.Groups.WithIndex())
saveService.ImmediateSave(new ModSaveGroup(mod, index));
@ -118,7 +118,7 @@ public static partial class ModMigration
return true;
}
private static void ConvertGroup(Mod mod, OptionGroupV0 group, ref int priority, HashSet<FullPath> seenMetaFiles)
private static void ConvertGroup(ModCreator creator, Mod mod, OptionGroupV0 group, ref int priority, HashSet<FullPath> seenMetaFiles)
{
if (group.Options.Count == 0)
return;
@ -134,15 +134,15 @@ public static partial class ModMigration
Priority = priority++,
Description = string.Empty,
};
mod._groups.Add(newMultiGroup);
mod.Groups.Add(newMultiGroup);
foreach (var option in group.Options)
newMultiGroup.PrioritizedOptions.Add((SubModFromOption(mod, option, seenMetaFiles), optionPriority++));
newMultiGroup.PrioritizedOptions.Add((SubModFromOption(creator, mod, option, seenMetaFiles), optionPriority++));
break;
case GroupType.Single:
if (group.Options.Count == 1)
{
AddFilesToSubMod(mod._default, mod.ModPath, group.Options[0], seenMetaFiles);
AddFilesToSubMod(mod.Default, mod.ModPath, group.Options[0], seenMetaFiles);
return;
}
@ -152,9 +152,9 @@ public static partial class ModMigration
Priority = priority++,
Description = string.Empty,
};
mod._groups.Add(newSingleGroup);
mod.Groups.Add(newSingleGroup);
foreach (var option in group.Options)
newSingleGroup.OptionData.Add(SubModFromOption(mod, option, seenMetaFiles));
newSingleGroup.OptionData.Add(SubModFromOption(creator, mod, option, seenMetaFiles));
break;
}
@ -173,11 +173,11 @@ public static partial class ModMigration
}
}
private static SubMod SubModFromOption(Mod mod, OptionV0 option, HashSet<FullPath> seenMetaFiles)
private static SubMod SubModFromOption(ModCreator creator, Mod mod, OptionV0 option, HashSet<FullPath> seenMetaFiles)
{
var subMod = new SubMod(mod) { Name = option.OptionName };
AddFilesToSubMod(subMod, mod.ModPath, option, seenMetaFiles);
subMod.IncorporateMetaChanges(mod.ModPath, false);
creator.IncorporateMetaChanges(subMod, mod.ModPath, false);
return subMod;
}

View file

@ -46,11 +46,11 @@ public class ModOptionEditor
/// <summary> Change the type of a group given by mod and index to type, if possible. </summary>
public void ChangeModGroupType(Mod mod, int groupIdx, GroupType type)
{
var group = mod._groups[groupIdx];
var group = mod.Groups[groupIdx];
if (group.Type == type)
return;
mod._groups[groupIdx] = group.Convert(type);
mod.Groups[groupIdx] = group.Convert(type);
_saveService.QueueSave(new ModSaveGroup(mod, groupIdx));
_communicator.ModOptionChanged.Invoke(ModOptionChangeType.GroupTypeChanged, mod, groupIdx, -1, -1);
}
@ -58,7 +58,7 @@ public class ModOptionEditor
/// <summary> Change the settings stored as default options in a mod.</summary>
public void ChangeModGroupDefaultOption(Mod mod, int groupIdx, uint defaultOption)
{
var group = mod._groups[groupIdx];
var group = mod.Groups[groupIdx];
if (group.DefaultSettings == defaultOption)
return;
@ -70,7 +70,7 @@ public class ModOptionEditor
/// <summary> Rename an option group if possible. </summary>
public void RenameModGroup(Mod mod, int groupIdx, string newName)
{
var group = mod._groups[groupIdx];
var group = mod.Groups[groupIdx];
var oldName = group.Name;
if (oldName == newName || !VerifyFileName(mod, group, newName, true))
return;
@ -93,9 +93,9 @@ public class ModOptionEditor
if (!VerifyFileName(mod, null, newName, true))
return;
var maxPriority = mod._groups.Count == 0 ? 0 : mod._groups.Max(o => o.Priority) + 1;
var maxPriority = mod.Groups.Count == 0 ? 0 : mod.Groups.Max(o => o.Priority) + 1;
mod._groups.Add(type == GroupType.Multi
mod.Groups.Add(type == GroupType.Multi
? new MultiModGroup
{
Name = newName,
@ -106,16 +106,16 @@ public class ModOptionEditor
Name = newName,
Priority = maxPriority,
});
_saveService.ImmediateSave(new ModSaveGroup(mod, mod._groups.Count - 1));
_communicator.ModOptionChanged.Invoke(ModOptionChangeType.GroupAdded, mod, mod._groups.Count - 1, -1, -1);
_saveService.ImmediateSave(new ModSaveGroup(mod, mod.Groups.Count - 1));
_communicator.ModOptionChanged.Invoke(ModOptionChangeType.GroupAdded, mod, mod.Groups.Count - 1, -1, -1);
}
/// <summary> Delete a given option group. Fires an event to prepare before actually deleting. </summary>
public void DeleteModGroup(Mod mod, int groupIdx)
{
var group = mod._groups[groupIdx];
var group = mod.Groups[groupIdx];
_communicator.ModOptionChanged.Invoke(ModOptionChangeType.PrepareChange, mod, groupIdx, -1, -1);
mod._groups.RemoveAt(groupIdx);
mod.Groups.RemoveAt(groupIdx);
UpdateSubModPositions(mod, groupIdx);
_saveService.SaveAllOptionGroups(mod);
_communicator.ModOptionChanged.Invoke(ModOptionChangeType.GroupDeleted, mod, groupIdx, -1, -1);
@ -124,7 +124,7 @@ public class ModOptionEditor
/// <summary> Move the index of a given option group. </summary>
public void MoveModGroup(Mod mod, int groupIdxFrom, int groupIdxTo)
{
if (!mod._groups.Move(groupIdxFrom, groupIdxTo))
if (!mod.Groups.Move(groupIdxFrom, groupIdxTo))
return;
UpdateSubModPositions(mod, Math.Min(groupIdxFrom, groupIdxTo));
@ -135,7 +135,7 @@ public class ModOptionEditor
/// <summary> Change the description of the given option group. </summary>
public void ChangeGroupDescription(Mod mod, int groupIdx, string newDescription)
{
var group = mod._groups[groupIdx];
var group = mod.Groups[groupIdx];
if (group.Description == newDescription)
return;
@ -152,7 +152,7 @@ public class ModOptionEditor
/// <summary> Change the description of the given option. </summary>
public void ChangeOptionDescription(Mod mod, int groupIdx, int optionIdx, string newDescription)
{
var group = mod._groups[groupIdx];
var group = mod.Groups[groupIdx];
var option = group[optionIdx];
if (option.Description == newDescription || option is not SubMod s)
return;
@ -165,7 +165,7 @@ public class ModOptionEditor
/// <summary> Change the internal priority of the given option group. </summary>
public void ChangeGroupPriority(Mod mod, int groupIdx, int newPriority)
{
var group = mod._groups[groupIdx];
var group = mod.Groups[groupIdx];
if (group.Priority == newPriority)
return;
@ -182,7 +182,7 @@ public class ModOptionEditor
/// <summary> Change the internal priority of the given option. </summary>
public void ChangeOptionPriority(Mod mod, int groupIdx, int optionIdx, int newPriority)
{
switch (mod._groups[groupIdx])
switch (mod.Groups[groupIdx])
{
case SingleModGroup:
ChangeGroupPriority(mod, groupIdx, newPriority);
@ -201,7 +201,7 @@ public class ModOptionEditor
/// <summary> Rename the given option. </summary>
public void RenameOption(Mod mod, int groupIdx, int optionIdx, string newName)
{
switch (mod._groups[groupIdx])
switch (mod.Groups[groupIdx])
{
case SingleModGroup s:
if (s.OptionData[optionIdx].Name == newName)
@ -225,7 +225,7 @@ public class ModOptionEditor
/// <summary> Add a new empty option of the given name for the given group. </summary>
public void AddOption(Mod mod, int groupIdx, string newName)
{
var group = mod._groups[groupIdx];
var group = mod.Groups[groupIdx];
var subMod = new SubMod(mod) { Name = newName };
subMod.SetPosition(groupIdx, group.Count);
switch (group)
@ -248,7 +248,7 @@ public class ModOptionEditor
if (option is not SubMod o)
return;
var group = mod._groups[groupIdx];
var group = mod.Groups[groupIdx];
if (group.Type is GroupType.Multi && group.Count >= IModGroup.MaxMultiOptions)
{
Penumbra.Log.Error(
@ -276,7 +276,7 @@ public class ModOptionEditor
/// <summary> Delete the given option from the given group. </summary>
public void DeleteOption(Mod mod, int groupIdx, int optionIdx)
{
var group = mod._groups[groupIdx];
var group = mod.Groups[groupIdx];
_communicator.ModOptionChanged.Invoke(ModOptionChangeType.PrepareChange, mod, groupIdx, optionIdx, -1);
switch (group)
{
@ -297,7 +297,7 @@ public class ModOptionEditor
/// <summary> Move an option inside the given option group. </summary>
public void MoveOption(Mod mod, int groupIdx, int optionIdxFrom, int optionIdxTo)
{
var group = mod._groups[groupIdx];
var group = mod.Groups[groupIdx];
if (!group.MoveOption(optionIdxFrom, optionIdxTo))
return;
@ -379,7 +379,7 @@ public class ModOptionEditor
/// <summary> Update the indices stored in options from a given group on. </summary>
private static void UpdateSubModPositions(Mod mod, int fromGroup)
{
foreach (var (group, groupIdx) in mod._groups.WithIndex().Skip(fromGroup))
foreach (var (group, groupIdx) in mod.Groups.WithIndex().Skip(fromGroup))
{
foreach (var (o, optionIdx) in group.OfType<SubMod>().WithIndex())
o.SetPosition(groupIdx, optionIdx);
@ -390,9 +390,9 @@ public class ModOptionEditor
private static SubMod GetSubMod(Mod mod, int groupIdx, int optionIdx)
{
if (groupIdx == -1 && optionIdx == 0)
return mod._default;
return mod.Default;
return mod._groups[groupIdx] switch
return mod.Groups[groupIdx] switch
{
SingleModGroup s => s.OptionData[optionIdx],
MultiModGroup m => m.PrioritizedOptions[optionIdx].Mod,

View file

@ -31,76 +31,6 @@ public partial class Mod
internal Mod( DirectoryInfo modPath )
{
ModPath = modPath;
_default = new SubMod( this );
}
public static Mod? LoadMod( ModManager modManager, DirectoryInfo modPath, bool incorporateMetaChanges )
{
modPath.Refresh();
if( !modPath.Exists )
{
Penumbra.Log.Error( $"Supplied mod directory {modPath} does not exist." );
return null;
}
var mod = new Mod(modPath);
if (mod.Reload(modManager, incorporateMetaChanges, out _))
return mod;
// Can not be base path not existing because that is checked before.
Penumbra.Log.Warning( $"Mod at {modPath} without name is not supported." );
return null;
}
internal bool Reload(ModManager modManager, bool incorporateMetaChanges, out ModDataChangeType modDataChange )
{
modDataChange = ModDataChangeType.Deletion;
ModPath.Refresh();
if( !ModPath.Exists )
{
return false;
}
modDataChange = modManager.DataEditor.LoadMeta(this);
if( modDataChange.HasFlag( ModDataChangeType.Deletion ) || Name.Length == 0 )
{
return false;
}
modManager.DataEditor.LoadLocalData(this);
LoadDefaultOption();
LoadAllGroups();
if( incorporateMetaChanges )
{
IncorporateAllMetaChanges(true);
}
return true;
}
// Convert all .meta and .rgsp files to their respective meta changes and add them to their options.
// Deletes the source files if delete is true.
private void IncorporateAllMetaChanges( bool delete )
{
var changes = false;
List< string > deleteList = new();
foreach( var subMod in AllSubMods.OfType< SubMod >() )
{
var (localChanges, localDeleteList) = subMod.IncorporateMetaChanges( ModPath, false );
changes |= localChanges;
if( delete )
{
deleteList.AddRange( localDeleteList );
}
}
SubMod.DeleteDeleteList( deleteList, delete );
if( changes )
{
Penumbra.SaveService.SaveAllOptionGroups(this);
}
Default = new SubMod( this );
}
}

View file

@ -1,283 +0,0 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
using System.Text.RegularExpressions;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using OtterGui;
using OtterGui.Filesystem;
using Penumbra.Api.Enums;
using Penumbra.Import.Structs;
using Penumbra.String.Classes;
namespace Penumbra.Mods;
internal static partial class ModCreator
{
/// <summary>
/// Create and return a new directory based on the given directory and name, that is <br/>
/// - Not Empty.<br/>
/// - Unique, by appending (digit) for duplicates.<br/>
/// - Containing no symbols invalid for FFXIV or windows paths.<br/>
/// </summary>
/// <param name="outDirectory"></param>
/// <param name="modListName"></param>
/// <param name="create"></param>
/// <returns></returns>
/// <exception cref="IOException"></exception>
public static DirectoryInfo CreateModFolder( DirectoryInfo outDirectory, string modListName, bool create = true )
{
var name = modListName;
if( name.Length == 0 )
{
name = "_";
}
var newModFolderBase = NewOptionDirectory( outDirectory, name );
var newModFolder = newModFolderBase.FullName.ObtainUniqueFile();
if( newModFolder.Length == 0 )
{
throw new IOException( "Could not create mod folder: too many folders of the same name exist." );
}
if( create )
{
Directory.CreateDirectory( newModFolder );
}
return new DirectoryInfo( newModFolder );
}
/// <summary>
/// Create the name for a group or option subfolder based on its parent folder and given name.
/// subFolderName should never be empty, and the result is unique and contains no invalid symbols.
/// </summary>
public static DirectoryInfo? NewSubFolderName( DirectoryInfo parentFolder, string subFolderName )
{
var newModFolderBase = NewOptionDirectory( parentFolder, subFolderName );
var newModFolder = newModFolderBase.FullName.ObtainUniqueFile();
return newModFolder.Length == 0 ? null : new DirectoryInfo( newModFolder );
}
/// <summary> Create a file for an option group from given data. </summary>
public static void CreateOptionGroup( DirectoryInfo baseFolder, GroupType type, string name,
int priority, int index, uint defaultSettings, string desc, IEnumerable< ISubMod > subMods )
{
switch( type )
{
case GroupType.Multi:
{
var group = new MultiModGroup()
{
Name = name,
Description = desc,
Priority = priority,
DefaultSettings = defaultSettings,
};
group.PrioritizedOptions.AddRange( subMods.OfType< SubMod >().Select( ( s, idx ) => ( s, idx ) ) );
Penumbra.SaveService.ImmediateSave(new ModSaveGroup(baseFolder, group, index));
break;
}
case GroupType.Single:
{
var group = new SingleModGroup()
{
Name = name,
Description = desc,
Priority = priority,
DefaultSettings = defaultSettings,
};
group.OptionData.AddRange( subMods.OfType< SubMod >() );
Penumbra.SaveService.ImmediateSave(new ModSaveGroup(baseFolder, group, index));
break;
}
}
}
/// <summary> Create the data for a given sub mod from its data and the folder it is based on. </summary>
public static ISubMod CreateSubMod( DirectoryInfo baseFolder, DirectoryInfo optionFolder, OptionList option )
{
var list = optionFolder.EnumerateNonHiddenFiles()
.Select( f => ( Utf8GamePath.FromFile( f, optionFolder, out var gamePath, true ), gamePath, new FullPath( f ) ) )
.Where( t => t.Item1 );
var mod = new SubMod( null! ) // Mod is irrelevant here, only used for saving.
{
Name = option.Name,
Description = option.Description,
};
foreach( var (_, gamePath, file) in list )
{
mod.FileData.TryAdd( gamePath, file );
}
mod.IncorporateMetaChanges( baseFolder, true );
return mod;
}
/// <summary> Create an empty sub mod for single groups with None options. </summary>
internal static ISubMod CreateEmptySubMod( string name )
=> new SubMod( null! ) // Mod is irrelevant here, only used for saving.
{
Name = name,
};
/// <summary>
/// Create the default data file from all unused files that were not handled before
/// and are used in sub mods.
/// </summary>
internal static void CreateDefaultFiles( DirectoryInfo directory )
{
var mod = new Mod( directory );
mod.Reload( Penumbra.ModManager, false, out _ );
foreach( var file in mod.FindUnusedFiles() )
{
if( Utf8GamePath.FromFile( new FileInfo( file.FullName ), directory, out var gamePath, true ) )
mod._default.FileData.TryAdd( gamePath, file );
}
mod._default.IncorporateMetaChanges( directory, true );
Penumbra.SaveService.ImmediateSave(new ModSaveGroup(mod, -1));
}
/// <summary> Return the name of a new valid directory based on the base directory and the given name. </summary>
public static DirectoryInfo NewOptionDirectory( DirectoryInfo baseDir, string optionName )
=> new(Path.Combine( baseDir.FullName, ReplaceBadXivSymbols( optionName ) ));
/// <summary> Normalize for nicer names, and remove invalid symbols or invalid paths. </summary>
public static string ReplaceBadXivSymbols( string s, string replacement = "_" )
{
switch( s )
{
case ".": return replacement;
case "..": return replacement + replacement;
}
StringBuilder sb = new(s.Length);
foreach( var c in s.Normalize( NormalizationForm.FormKC ) )
{
if( c.IsInvalidInPath() )
{
sb.Append( replacement );
}
else
{
sb.Append( c );
}
}
return sb.ToString();
}
public static void SplitMultiGroups( DirectoryInfo baseDir )
{
var mod = new Mod( baseDir );
var files = mod.GroupFiles.ToList();
var idx = 0;
var reorder = false;
foreach( var groupFile in files )
{
++idx;
try
{
if( reorder )
{
var newName = $"{baseDir.FullName}\\group_{idx:D3}{groupFile.Name[ 9.. ]}";
Penumbra.Log.Debug( $"Moving {groupFile.Name} to {Path.GetFileName( newName )} due to reordering after multi group split." );
groupFile.MoveTo( newName, false );
}
}
catch( Exception ex )
{
throw new Exception( "Could not reorder group file after splitting multi group on .pmp import.", ex );
}
try
{
var json = JObject.Parse( File.ReadAllText( groupFile.FullName ) );
if( json[ nameof( IModGroup.Type ) ]?.ToObject< GroupType >() is not GroupType.Multi )
{
continue;
}
var name = json[ nameof( IModGroup.Name ) ]?.ToObject< string >() ?? string.Empty;
if( name.Length == 0 )
{
continue;
}
var options = json[ "Options" ]?.Children().ToList();
if( options == null )
{
continue;
}
if( options.Count <= IModGroup.MaxMultiOptions )
{
continue;
}
Penumbra.Log.Information( $"Splitting multi group {name} in {mod.Name} due to {options.Count} being too many options." );
var clone = json.DeepClone();
reorder = true;
foreach( var o in options.Skip( IModGroup.MaxMultiOptions ) )
{
o.Remove();
}
var newOptions = clone[ "Options" ]!.Children().ToList();
foreach( var o in newOptions.Take( IModGroup.MaxMultiOptions ) )
{
o.Remove();
}
var match = DuplicateNumber().Match( name );
var startNumber = match.Success ? int.Parse( match.Groups[ 0 ].Value ) : 1;
name = match.Success ? name[ ..4 ] : name;
var oldName = $"{name}, Part {startNumber}";
var oldPath = $"{baseDir.FullName}\\group_{idx:D3}_{oldName.RemoveInvalidPathSymbols().ToLowerInvariant()}.json";
var newName = $"{name}, Part {startNumber + 1}";
var newPath = $"{baseDir.FullName}\\group_{++idx:D3}_{newName.RemoveInvalidPathSymbols().ToLowerInvariant()}.json";
json[ nameof( IModGroup.Name ) ] = oldName;
clone[ nameof( IModGroup.Name ) ] = newName;
clone[ nameof( IModGroup.DefaultSettings ) ] = 0u;
Penumbra.Log.Debug( $"Writing the first {IModGroup.MaxMultiOptions} options to {Path.GetFileName( oldPath )} after split." );
using( var oldFile = File.CreateText( oldPath ) )
{
using var j = new JsonTextWriter( oldFile )
{
Formatting = Formatting.Indented,
};
json.WriteTo( j );
}
Penumbra.Log.Debug( $"Writing the remaining {options.Count - IModGroup.MaxMultiOptions} options to {Path.GetFileName( newPath )} after split." );
using( var newFile = File.CreateText( newPath ) )
{
using var j = new JsonTextWriter( newFile )
{
Formatting = Formatting.Indented,
};
clone.WriteTo( j );
}
Penumbra.Log.Debug(
$"Deleting the old group file at {groupFile.Name} after splitting it into {Path.GetFileName( oldPath )} and {Path.GetFileName( newPath )}." );
groupFile.Delete();
}
catch( Exception ex )
{
throw new Exception( $"Could not split multi group file {groupFile.Name} on .pmp import.", ex );
}
}
}
[GeneratedRegex(@", Part (\d+)$", RegexOptions.NonBacktracking )]
private static partial Regex DuplicateNumber();
}

View file

@ -1,139 +0,0 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using Newtonsoft.Json.Linq;
using OtterGui;
using Penumbra.Api.Enums;
using Penumbra.Meta;
using Penumbra.Meta.Manipulations;
using Penumbra.String.Classes;
namespace Penumbra.Mods;
public partial class Mod
{
public ISubMod Default
=> _default;
public IReadOnlyList<IModGroup> Groups
=> _groups;
internal readonly SubMod _default;
internal readonly List<IModGroup> _groups = new();
public IEnumerable<ISubMod> AllSubMods
=> _groups.SelectMany(o => o).Prepend(_default);
public IEnumerable<MetaManipulation> AllManipulations
=> AllSubMods.SelectMany(s => s.Manipulations);
public IEnumerable<Utf8GamePath> AllRedirects
=> AllSubMods.SelectMany(s => s.Files.Keys.Concat(s.FileSwaps.Keys));
public IEnumerable<FullPath> AllFiles
=> AllSubMods.SelectMany(o => o.Files)
.Select(p => p.Value);
public IEnumerable<FileInfo> GroupFiles
=> ModPath.EnumerateFiles("group_*.json");
public List<FullPath> FindUnusedFiles()
{
var modFiles = AllFiles.ToHashSet();
return ModPath.EnumerateDirectories()
.Where(d => !d.IsHidden())
.SelectMany(FileExtensions.EnumerateNonHiddenFiles)
.Select(f => new FullPath(f))
.Where(f => !modFiles.Contains(f))
.ToList();
}
private static IModGroup? LoadModGroup(Mod mod, FileInfo file, int groupIdx)
{
if (!File.Exists(file.FullName))
return null;
try
{
var json = JObject.Parse(File.ReadAllText(file.FullName));
switch (json[nameof(Type)]?.ToObject<GroupType>() ?? GroupType.Single)
{
case GroupType.Multi: return MultiModGroup.Load(mod, json, groupIdx);
case GroupType.Single: return SingleModGroup.Load(mod, json, groupIdx);
}
}
catch (Exception e)
{
Penumbra.Log.Error($"Could not read mod group from {file.FullName}:\n{e}");
}
return null;
}
private void LoadAllGroups()
{
_groups.Clear();
var changes = false;
foreach (var file in GroupFiles)
{
var group = LoadModGroup(this, file, _groups.Count);
if (group != null && _groups.All(g => g.Name != group.Name))
{
changes = changes || Penumbra.Filenames.OptionGroupFile(ModPath.FullName, Groups.Count, group.Name) != file.FullName;
_groups.Add(group);
}
else
{
changes = true;
}
}
if (changes)
Penumbra.SaveService.SaveAllOptionGroups(this);
}
private void LoadDefaultOption()
{
var defaultFile = Penumbra.Filenames.OptionGroupFile(this, -1);
_default.SetPosition(-1, 0);
try
{
if (!File.Exists(defaultFile))
_default.Load(ModPath, new JObject(), out _);
else
_default.Load(ModPath, JObject.Parse(File.ReadAllText(defaultFile)), out _);
}
catch (Exception e)
{
Penumbra.Log.Error($"Could not parse default file for {Name}:\n{e}");
}
}
public void WriteAllTexToolsMeta(MetaFileManager manager)
{
try
{
_default.WriteTexToolsMeta(manager, ModPath);
foreach (var group in Groups)
{
var dir = ModCreator.NewOptionDirectory(ModPath, group.Name);
if (!dir.Exists)
dir.Create();
foreach (var option in group.OfType<SubMod>())
{
var optionDir = ModCreator.NewOptionDirectory(dir, option.Name);
if (!optionDir.Exists)
optionDir.Create();
option.WriteTexToolsMeta(manager, optionDir);
}
}
}
catch (Exception e)
{
Penumbra.Log.Error($"Error writing TexToolsMeta:\n{e}");
}
}
}

View file

@ -1,6 +1,11 @@
using System;
using System.Collections.Generic;
using System.Linq;
using OtterGui;
using OtterGui.Classes;
using Penumbra.Import;
using Penumbra.Meta;
using Penumbra.String.Classes;
namespace Penumbra.Mods;
@ -29,7 +34,61 @@ public sealed partial class Mod : IMod
public bool Favorite { get; internal set; } = false;
// Options
public readonly SubMod Default;
public readonly List<IModGroup> Groups = new();
ISubMod IMod.Default
=> Default;
IReadOnlyList<IModGroup> IMod.Groups
=> Groups;
public IEnumerable<SubMod> AllSubMods
=> Groups.SelectMany(o => o).OfType<SubMod>().Prepend(Default);
public List<FullPath> FindUnusedFiles()
{
var modFiles = AllSubMods.SelectMany(o => o.Files)
.Select(p => p.Value)
.ToHashSet();
return ModPath.EnumerateDirectories()
.Where(d => !d.IsHidden())
.SelectMany(FileExtensions.EnumerateNonHiddenFiles)
.Select(f => new FullPath(f))
.Where(f => !modFiles.Contains(f))
.ToList();
}
// Access
public override string ToString()
=> Name.Text;
public void WriteAllTexToolsMeta(MetaFileManager manager)
{
try
{
TexToolsMeta.WriteTexToolsMeta(manager, Default.Manipulations, ModPath);
foreach (var group in Groups)
{
var dir = ModCreator.NewOptionDirectory(ModPath, group.Name);
if (!dir.Exists)
dir.Create();
foreach (var option in group.OfType<SubMod>())
{
var optionDir = ModCreator.NewOptionDirectory(dir, option.Name);
if (!optionDir.Exists)
optionDir.Create();
TexToolsMeta.WriteTexToolsMeta(manager, option.Manipulations, optionDir);
}
}
}
catch (Exception e)
{
Penumbra.Log.Error($"Error writing TexToolsMeta:\n{e}");
}
}
}

476
Penumbra/Mods/ModCreator.cs Normal file
View file

@ -0,0 +1,476 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
using System.Text.RegularExpressions;
using Dalamud.Interface.Internal.Notifications;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using OtterGui;
using OtterGui.Filesystem;
using Penumbra.Api.Enums;
using Penumbra.GameData;
using Penumbra.Import;
using Penumbra.Import.Structs;
using Penumbra.Meta;
using Penumbra.Mods.Manager;
using Penumbra.String.Classes;
using Penumbra.Util;
namespace Penumbra.Mods;
public partial class ModCreator
{
private readonly Configuration _config;
private readonly SaveService _saveService;
private readonly ModDataEditor _dataEditor;
private readonly MetaFileManager _metaFileManager;
private readonly IGamePathParser _gamePathParser;
public ModCreator(SaveService saveService, Configuration config, ModDataEditor dataEditor, MetaFileManager metaFileManager,
IGamePathParser gamePathParser)
{
_saveService = saveService;
_config = config;
_dataEditor = dataEditor;
_metaFileManager = metaFileManager;
_gamePathParser = gamePathParser;
}
/// <summary> Creates directory and files necessary for a new mod without adding it to the manager. </summary>
public DirectoryInfo? CreateEmptyMod(DirectoryInfo basePath, string newName, string description = "")
{
try
{
var newDir = CreateModFolder(basePath, newName);
_dataEditor.CreateMeta(newDir, newName, _config.DefaultModAuthor, description, "1.0", string.Empty);
CreateDefaultFiles(newDir);
return newDir;
}
catch (Exception e)
{
Penumbra.ChatService.NotificationMessage($"Could not create directory for new Mod {newName}:\n{e}", "Failure",
NotificationType.Error);
return null;
}
}
/// <summary> Load a mod by its directory. </summary>
public Mod? LoadMod(DirectoryInfo modPath, bool incorporateMetaChanges)
{
modPath.Refresh();
if (!modPath.Exists)
{
Penumbra.Log.Error($"Supplied mod directory {modPath} does not exist.");
return null;
}
var mod = new Mod(modPath);
if (ReloadMod(mod, incorporateMetaChanges, out _))
return mod;
// Can not be base path not existing because that is checked before.
Penumbra.Log.Warning($"Mod at {modPath} without name is not supported.");
return null;
}
/// <summary> Reload a mod from its mod path. </summary>
public bool ReloadMod(Mod mod, bool incorporateMetaChanges, out ModDataChangeType modDataChange)
{
modDataChange = ModDataChangeType.Deletion;
if (!Directory.Exists(mod.ModPath.FullName))
return false;
modDataChange = _dataEditor.LoadMeta(this, mod);
if (modDataChange.HasFlag(ModDataChangeType.Deletion) || mod.Name.Length == 0)
return false;
_dataEditor.LoadLocalData(mod);
LoadDefaultOption(mod);
LoadAllGroups(mod);
if (incorporateMetaChanges)
IncorporateAllMetaChanges(mod, true);
return true;
}
/// <summary> Load all option groups for a given mod. </summary>
public void LoadAllGroups(Mod mod)
{
mod.Groups.Clear();
var changes = false;
foreach (var file in _saveService.FileNames.GetOptionGroupFiles(mod))
{
var group = LoadModGroup(mod, file, mod.Groups.Count);
if (group != null && mod.Groups.All(g => g.Name != group.Name))
{
changes = changes
|| _saveService.FileNames.OptionGroupFile(mod.ModPath.FullName, mod.Groups.Count, group.Name) != file.FullName;
mod.Groups.Add(group);
}
else
{
changes = true;
}
}
if (changes)
_saveService.SaveAllOptionGroups(mod);
}
/// <summary> Load the default option for a given mod.</summary>
public void LoadDefaultOption(Mod mod)
{
var defaultFile = _saveService.FileNames.OptionGroupFile(mod, -1);
mod.Default.SetPosition(-1, 0);
try
{
if (!File.Exists(defaultFile))
mod.Default.Load(mod.ModPath, new JObject(), out _);
else
mod.Default.Load(mod.ModPath, JObject.Parse(File.ReadAllText(defaultFile)), out _);
}
catch (Exception e)
{
Penumbra.Log.Error($"Could not parse default file for {mod.Name}:\n{e}");
}
}
/// <summary>
/// Create and return a new directory based on the given directory and name, that is <br/>
/// - Not Empty.<br/>
/// - Unique, by appending (digit) for duplicates.<br/>
/// - Containing no symbols invalid for FFXIV or windows paths.<br/>
/// </summary>
public static DirectoryInfo CreateModFolder(DirectoryInfo outDirectory, string modListName, bool create = true)
{
var name = modListName;
if (name.Length == 0)
name = "_";
var newModFolderBase = NewOptionDirectory(outDirectory, name);
var newModFolder = newModFolderBase.FullName.ObtainUniqueFile();
if (newModFolder.Length == 0)
throw new IOException("Could not create mod folder: too many folders of the same name exist.");
if (create)
Directory.CreateDirectory(newModFolder);
return new DirectoryInfo(newModFolder);
}
/// <summary>
/// Convert all .meta and .rgsp files to their respective meta changes and add them to their options.
/// Deletes the source files if delete is true.
/// </summary>
public void IncorporateAllMetaChanges(Mod mod, bool delete)
{
var changes = false;
List<string> deleteList = new();
foreach (var subMod in mod.AllSubMods)
{
var (localChanges, localDeleteList) = IncorporateMetaChanges(subMod, mod.ModPath, false);
changes |= localChanges;
if (delete)
deleteList.AddRange(localDeleteList);
}
SubMod.DeleteDeleteList(deleteList, delete);
if (!changes)
return;
_saveService.SaveAllOptionGroups(mod);
_saveService.ImmediateSave(new ModSaveGroup(mod.ModPath, mod.Default));
}
/// <summary>
/// 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.
/// </summary>
public (bool Changes, List<string> DeleteList) IncorporateMetaChanges(SubMod option, DirectoryInfo basePath, bool delete)
{
var deleteList = new List<string>();
var oldSize = option.ManipulationData.Count;
var deleteString = delete ? "with deletion." : "without deletion.";
foreach (var (key, file) in option.Files.ToList())
{
var ext1 = key.Extension().AsciiToLower().ToString();
var ext2 = file.Extension.ToLowerInvariant();
try
{
if (ext1 == ".meta" || ext2 == ".meta")
{
option.FileData.Remove(key);
if (!file.Exists)
continue;
var meta = new TexToolsMeta(_metaFileManager, _gamePathParser, File.ReadAllBytes(file.FullName),
_config.KeepDefaultMetaChanges);
Penumbra.Log.Verbose(
$"Incorporating {file} as Metadata file of {meta.MetaManipulations.Count} manipulations {deleteString}");
deleteList.Add(file.FullName);
option.ManipulationData.UnionWith(meta.MetaManipulations);
}
else if (ext1 == ".rgsp" || ext2 == ".rgsp")
{
option.FileData.Remove(key);
if (!file.Exists)
continue;
var rgsp = TexToolsMeta.FromRgspFile(Penumbra.MetaFileManager, file.FullName, File.ReadAllBytes(file.FullName),
_config.KeepDefaultMetaChanges);
Penumbra.Log.Verbose(
$"Incorporating {file} as racial scaling file of {rgsp.MetaManipulations.Count} manipulations {deleteString}");
deleteList.Add(file.FullName);
option.ManipulationData.UnionWith(rgsp.MetaManipulations);
}
}
catch (Exception e)
{
Penumbra.Log.Error($"Could not incorporate meta changes in mod {basePath} from file {file.FullName}:\n{e}");
}
}
SubMod.DeleteDeleteList(deleteList, delete);
return (oldSize < option.ManipulationData.Count, deleteList);
}
/// <summary>
/// Create the name for a group or option subfolder based on its parent folder and given name.
/// subFolderName should never be empty, and the result is unique and contains no invalid symbols.
/// </summary>
public static DirectoryInfo? NewSubFolderName(DirectoryInfo parentFolder, string subFolderName)
{
var newModFolderBase = NewOptionDirectory(parentFolder, subFolderName);
var newModFolder = newModFolderBase.FullName.ObtainUniqueFile();
return newModFolder.Length == 0 ? null : new DirectoryInfo(newModFolder);
}
/// <summary> Create a file for an option group from given data. </summary>
public void CreateOptionGroup(DirectoryInfo baseFolder, GroupType type, string name,
int priority, int index, uint defaultSettings, string desc, IEnumerable<ISubMod> subMods)
{
switch (type)
{
case GroupType.Multi:
{
var group = new MultiModGroup()
{
Name = name,
Description = desc,
Priority = priority,
DefaultSettings = defaultSettings,
};
group.PrioritizedOptions.AddRange(subMods.OfType<SubMod>().Select((s, idx) => (s, idx)));
_saveService.ImmediateSave(new ModSaveGroup(baseFolder, group, index));
break;
}
case GroupType.Single:
{
var group = new SingleModGroup()
{
Name = name,
Description = desc,
Priority = priority,
DefaultSettings = defaultSettings,
};
group.OptionData.AddRange(subMods.OfType<SubMod>());
_saveService.ImmediateSave(new ModSaveGroup(baseFolder, group, index));
break;
}
}
}
/// <summary> Create the data for a given sub mod from its data and the folder it is based on. </summary>
public ISubMod CreateSubMod(DirectoryInfo baseFolder, DirectoryInfo optionFolder, OptionList option)
{
var list = optionFolder.EnumerateNonHiddenFiles()
.Select(f => (Utf8GamePath.FromFile(f, optionFolder, out var gamePath, true), gamePath, new FullPath(f)))
.Where(t => t.Item1);
var mod = new SubMod(null!) // Mod is irrelevant here, only used for saving.
{
Name = option.Name,
Description = option.Description,
};
foreach (var (_, gamePath, file) in list)
mod.FileData.TryAdd(gamePath, file);
IncorporateMetaChanges(mod, baseFolder, true);
return mod;
}
/// <summary> Create an empty sub mod for single groups with None options. </summary>
internal static ISubMod CreateEmptySubMod(string name)
=> new SubMod(null!) // Mod is irrelevant here, only used for saving.
{
Name = name,
};
/// <summary>
/// Create the default data file from all unused files that were not handled before
/// and are used in sub mods.
/// </summary>
internal void CreateDefaultFiles(DirectoryInfo directory)
{
var mod = new Mod(directory);
ReloadMod(mod, false, out _);
foreach (var file in mod.FindUnusedFiles())
{
if (Utf8GamePath.FromFile(new FileInfo(file.FullName), directory, out var gamePath, true))
mod.Default.FileData.TryAdd(gamePath, file);
}
IncorporateMetaChanges(mod.Default, directory, true);
_saveService.ImmediateSave(new ModSaveGroup(mod, -1));
}
/// <summary> Return the name of a new valid directory based on the base directory and the given name. </summary>
public static DirectoryInfo NewOptionDirectory(DirectoryInfo baseDir, string optionName)
=> new(Path.Combine(baseDir.FullName, ReplaceBadXivSymbols(optionName)));
/// <summary> Normalize for nicer names, and remove invalid symbols or invalid paths. </summary>
public static string ReplaceBadXivSymbols(string s, string replacement = "_")
{
switch (s)
{
case ".": return replacement;
case "..": return replacement + replacement;
}
StringBuilder sb = new(s.Length);
foreach (var c in s.Normalize(NormalizationForm.FormKC))
{
if (c.IsInvalidInPath())
sb.Append(replacement);
else
sb.Append(c);
}
return sb.ToString();
}
public void SplitMultiGroups(DirectoryInfo baseDir)
{
var mod = new Mod(baseDir);
var files = _saveService.FileNames.GetOptionGroupFiles(mod).ToList();
var idx = 0;
var reorder = false;
foreach (var groupFile in files)
{
++idx;
try
{
if (reorder)
{
var newName = $"{baseDir.FullName}\\group_{idx:D3}{groupFile.Name[9..]}";
Penumbra.Log.Debug($"Moving {groupFile.Name} to {Path.GetFileName(newName)} due to reordering after multi group split.");
groupFile.MoveTo(newName, false);
}
}
catch (Exception ex)
{
throw new Exception("Could not reorder group file after splitting multi group on .pmp import.", ex);
}
try
{
var json = JObject.Parse(File.ReadAllText(groupFile.FullName));
if (json[nameof(IModGroup.Type)]?.ToObject<GroupType>() is not GroupType.Multi)
continue;
var name = json[nameof(IModGroup.Name)]?.ToObject<string>() ?? string.Empty;
if (name.Length == 0)
continue;
var options = json["Options"]?.Children().ToList();
if (options is not { Count: > IModGroup.MaxMultiOptions })
continue;
Penumbra.Log.Information($"Splitting multi group {name} in {mod.Name} due to {options.Count} being too many options.");
var clone = json.DeepClone();
reorder = true;
foreach (var o in options.Skip(IModGroup.MaxMultiOptions))
o.Remove();
var newOptions = clone["Options"]!.Children().ToList();
foreach (var o in newOptions.Take(IModGroup.MaxMultiOptions))
o.Remove();
var match = DuplicateNumber().Match(name);
var startNumber = match.Success ? int.Parse(match.Groups[0].Value) : 1;
name = match.Success ? name[..4] : name;
var oldName = $"{name}, Part {startNumber}";
var oldPath = $"{baseDir.FullName}\\group_{idx:D3}_{oldName.RemoveInvalidPathSymbols().ToLowerInvariant()}.json";
var newName = $"{name}, Part {startNumber + 1}";
var newPath = $"{baseDir.FullName}\\group_{++idx:D3}_{newName.RemoveInvalidPathSymbols().ToLowerInvariant()}.json";
json[nameof(IModGroup.Name)] = oldName;
clone[nameof(IModGroup.Name)] = newName;
clone[nameof(IModGroup.DefaultSettings)] = 0u;
Penumbra.Log.Debug($"Writing the first {IModGroup.MaxMultiOptions} options to {Path.GetFileName(oldPath)} after split.");
using (var oldFile = File.CreateText(oldPath))
{
using var j = new JsonTextWriter(oldFile)
{
Formatting = Formatting.Indented,
};
json.WriteTo(j);
}
Penumbra.Log.Debug(
$"Writing the remaining {options.Count - IModGroup.MaxMultiOptions} options to {Path.GetFileName(newPath)} after split.");
using (var newFile = File.CreateText(newPath))
{
using var j = new JsonTextWriter(newFile)
{
Formatting = Formatting.Indented,
};
clone.WriteTo(j);
}
Penumbra.Log.Debug(
$"Deleting the old group file at {groupFile.Name} after splitting it into {Path.GetFileName(oldPath)} and {Path.GetFileName(newPath)}.");
groupFile.Delete();
}
catch (Exception ex)
{
throw new Exception($"Could not split multi group file {groupFile.Name} on .pmp import.", ex);
}
}
}
[GeneratedRegex(@", Part (\d+)$", RegexOptions.NonBacktracking)]
private static partial Regex DuplicateNumber();
/// <summary> Load an option group for a specific mod by its file and index. </summary>
private static IModGroup? LoadModGroup(Mod mod, FileInfo file, int groupIdx)
{
if (!File.Exists(file.FullName))
return null;
try
{
var json = JObject.Parse(File.ReadAllText(file.FullName));
switch (json[nameof(Type)]?.ToObject<GroupType>() ?? GroupType.Single)
{
case GroupType.Multi: return MultiModGroup.Load(mod, json, groupIdx);
case GroupType.Single: return SingleModGroup.Load(mod, json, groupIdx);
}
}
catch (Exception e)
{
Penumbra.Log.Error($"Could not read mod group from {file.FullName}:\n{e}");
}
return null;
}
}

View file

@ -93,57 +93,6 @@ public sealed class SubMod : ISubMod
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.MetaFileManager, 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(Penumbra.MetaFileManager, 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)
@ -161,63 +110,4 @@ public sealed class SubMod : ISubMod
}
}
}
public void WriteTexToolsMeta(MetaFileManager manager, DirectoryInfo basePath, bool test = false)
{
var files = TexToolsMeta.ConvertToTexTools(manager, 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(manager, files);
}
[Conditional("DEBUG")]
private void TestMetaWriting(MetaFileManager manager, 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(manager, file, data, Penumbra.Config.KeepDefaultMetaChanges)
: new TexToolsMeta(manager, 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.");
}
}
}

View file

@ -7,6 +7,7 @@ using Penumbra.Collections;
using Penumbra.Meta.Manipulations;
using Penumbra.Mods.Manager;
using Penumbra.String.Classes;
using Penumbra.Util;
namespace Penumbra.Mods;
@ -19,33 +20,33 @@ public class TemporaryMod : IMod
public int TotalManipulations
=> Default.Manipulations.Count;
public ISubMod Default
=> _default;
public readonly SubMod Default;
ISubMod IMod.Default
=> Default;
public IReadOnlyList< IModGroup > Groups
=> Array.Empty< IModGroup >();
public IEnumerable< ISubMod > AllSubMods
public IEnumerable< SubMod > AllSubMods
=> new[] { Default };
private readonly SubMod _default;
public TemporaryMod()
=> _default = new SubMod( this );
=> Default = new SubMod( this );
public void SetFile( Utf8GamePath gamePath, FullPath fullPath )
=> _default.FileData[ gamePath ] = fullPath;
=> Default.FileData[ gamePath ] = fullPath;
public bool SetManipulation( MetaManipulation manip )
=> _default.ManipulationData.Remove( manip ) | _default.ManipulationData.Add( manip );
=> Default.ManipulationData.Remove( manip ) | Default.ManipulationData.Add( manip );
public void SetAll( Dictionary< Utf8GamePath, FullPath > dict, HashSet< MetaManipulation > manips )
{
_default.FileData = dict;
_default.ManipulationData = manips;
Default.FileData = dict;
Default.ManipulationData = manips;
}
public static void SaveTempCollection( ModManager modManager, ModCollection collection, string? character = null )
public static void SaveTempCollection( SaveService saveService, ModManager modManager, ModCollection collection, string? character = null )
{
DirectoryInfo? dir = null;
try
@ -55,7 +56,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 = (SubMod) mod.Default;
var defaultMod = mod.Default;
foreach( var (gamePath, fullPath) in collection.ResolvedFiles )
{
if( gamePath.Path.EndsWith( ".imc"u8 ) )
@ -84,7 +85,7 @@ public class TemporaryMod : IMod
foreach( var manip in collection.MetaCache?.Manipulations ?? Array.Empty< MetaManipulation >() )
defaultMod.ManipulationData.Add( manip );
Penumbra.SaveService.ImmediateSave(new ModSaveGroup(dir, defaultMod));
saveService.ImmediateSave(new ModSaveGroup(dir, defaultMod));
modManager.AddMod( dir );
Penumbra.Log.Information( $"Successfully generated mod {mod.Name} at {mod.ModPath.FullName} for collection {collection.Name}." );
}

View file

@ -8,7 +8,6 @@ using ImGuiNET;
using Lumina.Excel.GeneratedSheets;
using Microsoft.Extensions.DependencyInjection;
using OtterGui;
using OtterGui.Classes;
using OtterGui.Log;
using Penumbra.Api;
using Penumbra.Api.Enums;
@ -28,8 +27,8 @@ using Penumbra.Interop.Services;
using Penumbra.Mods.Manager;
using Penumbra.Collections.Manager;
using Penumbra.Mods;
using Penumbra.Meta;
using Penumbra.Meta;
namespace Penumbra;
public class Penumbra : IDalamudPlugin
@ -39,32 +38,26 @@ public class Penumbra : IDalamudPlugin
public static Logger Log { get; private set; } = null!;
public static ChatService ChatService { get; private set; } = null!;
public static FilenameService Filenames { get; private set; } = null!;
public static SaveService SaveService { get; private set; } = null!;
public static Configuration Config { get; private set; } = null!;
public static ResidentResourceManager ResidentResources { get; private set; } = null!;
public static CharacterUtility CharacterUtility { get; private set; } = null!;
public static MetaFileManager MetaFileManager { get; private set; } = null!;
public static ModManager ModManager { get; private set; } = null!;
public static ModCacheManager ModCaches { 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 ActorManager Actors { get; private set; } = null!;
public static IObjectIdentifier Identifier { get; private set; } = null!;
public static IGamePathParser GamePathParser { get; private set; } = null!;
public static StainService StainService { get; private set; } = null!;
public static CharacterUtility CharacterUtility { get; private set; } = null!;
public static MetaFileManager MetaFileManager { get; private set; } = null!;
public static ModManager ModManager { get; private set; } = null!;
public static ModCacheManager ModCaches { get; private set; } = null!;
public static CollectionManager CollectionManager { get; private set; } = null!;
public static ActorManager Actors { get; private set; } = null!;
// TODO
public static ValidityChecker ValidityChecker { get; private set; } = null!;
public readonly RedrawService RedrawService;
public readonly ModFileSystem ModFileSystem;
public HttpApi HttpApi = null!;
internal ConfigWindow? ConfigWindow { get; private set; }
public readonly RedrawService RedrawService;
public readonly ModFileSystem ModFileSystem;
public HttpApi HttpApi = null!;
internal ConfigWindow? ConfigWindow { get; private set; }
private PenumbraWindowSystem? _windowSystem;
private bool _disposed;
private readonly ValidityChecker _validityChecker;
private readonly ResidentResourceManager _residentResources;
private readonly TempModManager _tempMods;
private readonly TempCollectionManager _tempCollections;
private PenumbraWindowSystem? _windowSystem;
private bool _disposed;
private readonly PenumbraNew _tmp;
@ -73,29 +66,24 @@ public class Penumbra : IDalamudPlugin
Log = PenumbraNew.Log;
try
{
_tmp = new PenumbraNew(this, pluginInterface);
ChatService = _tmp.Services.GetRequiredService<ChatService>();
Filenames = _tmp.Services.GetRequiredService<FilenameService>();
SaveService = _tmp.Services.GetRequiredService<SaveService>();
ValidityChecker = _tmp.Services.GetRequiredService<ValidityChecker>();
_tmp = new PenumbraNew(this, pluginInterface);
ChatService = _tmp.Services.GetRequiredService<ChatService>();
_validityChecker = _tmp.Services.GetRequiredService<ValidityChecker>();
_tmp.Services.GetRequiredService<BackupService>();
Config = _tmp.Services.GetRequiredService<Configuration>();
CharacterUtility = _tmp.Services.GetRequiredService<CharacterUtility>();
MetaFileManager = _tmp.Services.GetRequiredService<MetaFileManager>();
Actors = _tmp.Services.GetRequiredService<ActorService>().AwaitedService;
Identifier = _tmp.Services.GetRequiredService<IdentifierService>().AwaitedService;
GamePathParser = _tmp.Services.GetRequiredService<IGamePathParser>();
StainService = _tmp.Services.GetRequiredService<StainService>();
TempMods = _tmp.Services.GetRequiredService<TempModManager>();
ResidentResources = _tmp.Services.GetRequiredService<ResidentResourceManager>();
_tempMods = _tmp.Services.GetRequiredService<TempModManager>();
_residentResources = _tmp.Services.GetRequiredService<ResidentResourceManager>();
_tmp.Services.GetRequiredService<ResourceManagerService>();
ModManager = _tmp.Services.GetRequiredService<ModManager>();
CollectionManager = _tmp.Services.GetRequiredService<CollectionManager>();
TempCollections = _tmp.Services.GetRequiredService<TempCollectionManager>();
_tempCollections = _tmp.Services.GetRequiredService<TempCollectionManager>();
ModFileSystem = _tmp.Services.GetRequiredService<ModFileSystem>();
RedrawService = _tmp.Services.GetRequiredService<RedrawService>();
_tmp.Services.GetRequiredService<ResourceService>();
ModCaches = _tmp.Services.GetRequiredService<ModCacheManager>();
ModCaches = _tmp.Services.GetRequiredService<ModCacheManager>();
using (var t = _tmp.Services.GetRequiredService<StartTracker>().Measure(StartTimeType.PathResolver))
{
_tmp.Services.GetRequiredService<PathResolver>();
@ -104,14 +92,14 @@ public class Penumbra : IDalamudPlugin
SetupInterface();
SetupApi();
ValidityChecker.LogExceptions();
_validityChecker.LogExceptions();
Log.Information(
$"Penumbra Version {ValidityChecker.Version}, Commit #{ValidityChecker.CommitHash} successfully Loaded from {pluginInterface.SourceRepository}.");
$"Penumbra Version {_validityChecker.Version}, Commit #{_validityChecker.CommitHash} successfully Loaded from {pluginInterface.SourceRepository}.");
OtterTex.NativeDll.Initialize(pluginInterface.AssemblyLocation.DirectoryName);
Log.Information($"Loading native OtterTex assembly from {OtterTex.NativeDll.Directory}.");
if (CharacterUtility.Ready)
ResidentResources.Reload();
_residentResources.Reload();
}
catch
{
@ -173,7 +161,7 @@ public class Penumbra : IDalamudPlugin
if (CharacterUtility.Ready)
{
CollectionManager.Active.Default.SetFiles();
ResidentResources.Reload();
_residentResources.Reload();
RedrawService.RedrawAll(RedrawType.Redraw);
}
}
@ -182,7 +170,7 @@ public class Penumbra : IDalamudPlugin
if (CharacterUtility.Ready)
{
CharacterUtility.ResetAll();
ResidentResources.Reload();
_residentResources.Reload();
RedrawService.RedrawAll(RedrawType.Redraw);
}
}
@ -211,8 +199,8 @@ public class Penumbra : IDalamudPlugin
var exists = Config.ModDirectory.Length > 0 && Directory.Exists(Config.ModDirectory);
var drive = exists ? new DriveInfo(new DirectoryInfo(Config.ModDirectory).Root.FullName) : null;
sb.AppendLine("**Settings**");
sb.Append($"> **`Plugin Version: `** {ValidityChecker.Version}\n");
sb.Append($"> **`Commit Hash: `** {ValidityChecker.CommitHash}\n");
sb.Append($"> **`Plugin Version: `** {_validityChecker.Version}\n");
sb.Append($"> **`Commit Hash: `** {_validityChecker.CommitHash}\n");
sb.Append($"> **`Enable Mods: `** {Config.EnableMods}\n");
sb.Append($"> **`Enable HTTP API: `** {Config.EnableHttpApi}\n");
sb.Append($"> **`Operating System: `** {(DalamudUtil.IsLinux() ? "Mac/Linux (Wine)" : "Windows")}\n");
@ -235,9 +223,9 @@ public class Penumbra : IDalamudPlugin
$"> **`Mods with FileSwaps: `** {ModCaches.Count(m => m.TotalSwapCount > 0)}, Total: {ModCaches.Sum(m => m.TotalSwapCount)}\n");
sb.Append(
$"> **`Mods with Meta Manipulations:`** {ModCaches.Count(m => m.TotalManipulations > 0)}, Total {ModCaches.Sum(m => m.TotalManipulations)}\n");
sb.Append($"> **`IMC Exceptions Thrown: `** {ValidityChecker.ImcExceptions.Count}\n");
sb.Append($"> **`IMC Exceptions Thrown: `** {_validityChecker.ImcExceptions.Count}\n");
sb.Append(
$"> **`#Temp Mods: `** {TempMods.Mods.Sum(kvp => kvp.Value.Count) + TempMods.ModsForAllCollections.Count}\n");
$"> **`#Temp Mods: `** {_tempMods.Mods.Sum(kvp => kvp.Value.Count) + _tempMods.ModsForAllCollections.Count}\n");
string CharacterName(ActorIdentifier id, string name)
{
@ -259,8 +247,8 @@ public class Penumbra : IDalamudPlugin
sb.AppendLine("**Collections**");
sb.Append($"> **`#Collections: `** {CollectionManager.Storage.Count - 1}\n");
sb.Append($"> **`#Temp Collections: `** {TempCollections.Count}\n");
sb.Append($"> **`Active Collections: `** {CollectionManager.Caches.Count - TempCollections.Count}\n");
sb.Append($"> **`#Temp Collections: `** {_tempCollections.Count}\n");
sb.Append($"> **`Active Collections: `** {CollectionManager.Caches.Count - _tempCollections.Count}\n");
sb.Append($"> **`Base Collection: `** {CollectionManager.Active.Default.AnonymizedName}\n");
sb.Append($"> **`Interface Collection: `** {CollectionManager.Active.Interface.AnonymizedName}\n");
sb.Append($"> **`Selected Collection: `** {CollectionManager.Active.Current.AnonymizedName}\n");

View file

@ -95,6 +95,7 @@ public class PenumbraNew
services.AddSingleton<TempModManager>()
.AddSingleton<ModDataEditor>()
.AddSingleton<ModOptionEditor>()
.AddSingleton<ModCreator>()
.AddSingleton<ModManager>()
.AddSingleton<ModExportManager>()
.AddSingleton<ModImportManager>()

View file

@ -218,22 +218,26 @@ public class ItemSwapTab : IDisposable, ITab
_useCurrentCollection ? _collectionManager.Active.Current : null);
break;
case SwapType.Hair when _targetId > 0 && _sourceId > 0:
_swapData.LoadCustomization(_metaFileManager, BodySlot.Hair, Names.CombinedRace(_currentGender, _currentRace), (SetId)_sourceId,
_swapData.LoadCustomization(_metaFileManager, BodySlot.Hair, Names.CombinedRace(_currentGender, _currentRace),
(SetId)_sourceId,
(SetId)_targetId,
_useCurrentCollection ? _collectionManager.Active.Current : null);
break;
case SwapType.Face when _targetId > 0 && _sourceId > 0:
_swapData.LoadCustomization(_metaFileManager, BodySlot.Face, Names.CombinedRace(_currentGender, _currentRace), (SetId)_sourceId,
_swapData.LoadCustomization(_metaFileManager, BodySlot.Face, Names.CombinedRace(_currentGender, _currentRace),
(SetId)_sourceId,
(SetId)_targetId,
_useCurrentCollection ? _collectionManager.Active.Current : null);
break;
case SwapType.Ears when _targetId > 0 && _sourceId > 0:
_swapData.LoadCustomization(_metaFileManager, BodySlot.Zear, Names.CombinedRace(_currentGender, ModelRace.Viera), (SetId)_sourceId,
_swapData.LoadCustomization(_metaFileManager, BodySlot.Zear, Names.CombinedRace(_currentGender, ModelRace.Viera),
(SetId)_sourceId,
(SetId)_targetId,
_useCurrentCollection ? _collectionManager.Active.Current : null);
break;
case SwapType.Tail when _targetId > 0 && _sourceId > 0:
_swapData.LoadCustomization(_metaFileManager, BodySlot.Tail, Names.CombinedRace(_currentGender, _currentRace), (SetId)_sourceId,
_swapData.LoadCustomization(_metaFileManager, BodySlot.Tail, Names.CombinedRace(_currentGender, _currentRace),
(SetId)_sourceId,
(SetId)_targetId,
_useCurrentCollection ? _collectionManager.Active.Current : null);
break;
@ -276,9 +280,10 @@ public class ItemSwapTab : IDisposable, ITab
private void CreateMod()
{
var newDir = ModCreator.CreateModFolder(_modManager.BasePath, _newModName);
_modManager.DataEditor.CreateMeta(newDir, _newModName, _config.DefaultModAuthor, CreateDescription(), "1.0", string.Empty);
ModCreator.CreateDefaultFiles(newDir);
var newDir = _modManager.Creator.CreateEmptyMod(_modManager.BasePath, _newModName, CreateDescription());
if (newDir == null)
return;
_modManager.AddMod(newDir);
if (!_swapData.WriteMod(_modManager, _modManager[^1],
_useFileSwaps ? ItemSwapContainer.WriteType.UseSwaps : ItemSwapContainer.WriteType.NoSwaps))

View file

@ -13,7 +13,7 @@ namespace Penumbra.UI.AdvancedWindow;
public partial class ModEditWindow
{
private static bool DrawMaterialColorSetChange( MtrlFile file, bool disabled )
private bool DrawMaterialColorSetChange( MtrlFile file, bool disabled )
{
if( !file.ColorSets.Any( c => c.HasRows ) )
{
@ -95,9 +95,9 @@ public partial class ModEditWindow
}
}
private static bool DrawPreviewDye( MtrlFile file, bool disabled )
private bool DrawPreviewDye( MtrlFile file, bool disabled )
{
var (dyeId, (name, dyeColor, _)) = Penumbra.StainService.StainCombo.CurrentSelection;
var (dyeId, (name, dyeColor, _)) = _stainService.StainCombo.CurrentSelection;
var tt = dyeId == 0 ? "Select a preview dye first." : "Apply all preview values corresponding to the dye template and chosen dye where dyeing is enabled.";
if( ImGuiUtil.DrawDisabledButton( "Apply Preview Dye", Vector2.Zero, tt, disabled || dyeId == 0 ) )
{
@ -106,7 +106,7 @@ public partial class ModEditWindow
{
for( var i = 0; i < MtrlFile.ColorSet.RowArray.NumRows; ++i )
{
ret |= file.ApplyDyeTemplate( Penumbra.StainService.StmFile, j, i, dyeId );
ret |= file.ApplyDyeTemplate( _stainService.StmFile, j, i, dyeId );
}
}
@ -115,7 +115,7 @@ public partial class ModEditWindow
ImGui.SameLine();
var label = dyeId == 0 ? "Preview Dye###previewDye" : $"{name} (Preview)###previewDye";
Penumbra.StainService.StainCombo.Draw( label, dyeColor, string.Empty, true );
_stainService.StainCombo.Draw( label, dyeColor, string.Empty, true );
return false;
}
@ -217,7 +217,7 @@ public partial class ModEditWindow
return false;
}
private static bool DrawColorSetRow( MtrlFile file, int colorSetIdx, int rowIdx, bool disabled )
private bool DrawColorSetRow( MtrlFile file, int colorSetIdx, int rowIdx, bool disabled )
{
static bool FixFloat( ref float val, float current )
{
@ -355,10 +355,10 @@ public partial class ModEditWindow
ImGui.TableNextColumn();
if( hasDye )
{
if( Penumbra.StainService.TemplateCombo.Draw( "##dyeTemplate", dye.Template.ToString(), string.Empty, intSize
if(_stainService.TemplateCombo.Draw( "##dyeTemplate", dye.Template.ToString(), string.Empty, intSize
+ ImGui.GetStyle().ScrollbarSize / 2, ImGui.GetTextLineHeightWithSpacing(), ImGuiComboFlags.NoArrowButton ) )
{
file.ColorDyeSets[ colorSetIdx ].Rows[ rowIdx ].Template = Penumbra.StainService.TemplateCombo.CurrentSelection;
file.ColorDyeSets[ colorSetIdx ].Rows[ rowIdx ].Template = _stainService.TemplateCombo.CurrentSelection;
ret = true;
}
@ -376,10 +376,10 @@ public partial class ModEditWindow
return ret;
}
private static bool DrawDyePreview( MtrlFile file, int colorSetIdx, int rowIdx, bool disabled, MtrlFile.ColorDyeSet.Row dye, float floatSize )
private bool DrawDyePreview( MtrlFile file, int colorSetIdx, int rowIdx, bool disabled, MtrlFile.ColorDyeSet.Row dye, float floatSize )
{
var stain = Penumbra.StainService.StainCombo.CurrentSelection.Key;
if( stain == 0 || !Penumbra.StainService.StmFile.Entries.TryGetValue( dye.Template, out var entry ) )
var stain = _stainService.StainCombo.CurrentSelection.Key;
if( stain == 0 || !_stainService.StmFile.Entries.TryGetValue( dye.Template, out var entry ) )
{
return false;
}
@ -390,7 +390,7 @@ public partial class ModEditWindow
var ret = ImGuiUtil.DrawDisabledButton( FontAwesomeIcon.PaintBrush.ToIconString(), new Vector2( ImGui.GetFrameHeight() ),
"Apply the selected dye to this row.", disabled, true );
ret = ret && file.ApplyDyeTemplate( Penumbra.StainService.StmFile, colorSetIdx, rowIdx, stain );
ret = ret && file.ApplyDyeTemplate(_stainService.StmFile, colorSetIdx, rowIdx, stain );
ImGui.SameLine();
ColorPicker( "##diffusePreview", string.Empty, values.Diffuse, _ => { }, "D" );

View file

@ -15,7 +15,8 @@ using Penumbra.Import.Textures;
using Penumbra.Interop.ResourceTree;
using Penumbra.Meta;
using Penumbra.Mods;
using Penumbra.Mods.Manager;
using Penumbra.Mods.Manager;
using Penumbra.Services;
using Penumbra.String.Classes;
using Penumbra.UI.Classes;
using Penumbra.Util;
@ -33,6 +34,7 @@ public partial class ModEditWindow : Window, IDisposable
private readonly ItemSwapTab _itemSwapTab;
private readonly DataManager _gameData;
private readonly MetaFileManager _metaFileManager;
private readonly StainService _stainService;
private Mod? _mod;
private Vector2 _iconSize = Vector2.Zero;
@ -495,17 +497,18 @@ public partial class ModEditWindow : Window, IDisposable
}
public ModEditWindow(PerformanceTracker performance, FileDialogService fileDialog, ItemSwapTab itemSwapTab, DataManager gameData,
Configuration config, ModEditor editor, ResourceTreeFactory resourceTreeFactory, ModCacheManager modCaches, MetaFileManager metaFileManager)
Configuration config, ModEditor editor, ResourceTreeFactory resourceTreeFactory, ModCacheManager modCaches, MetaFileManager metaFileManager, StainService stainService)
: base(WindowBaseLabel)
{
_performance = performance;
_itemSwapTab = itemSwapTab;
_config = config;
_editor = editor;
_modCaches = modCaches;
_metaFileManager = metaFileManager;
_gameData = gameData;
_fileDialog = fileDialog;
_performance = performance;
_itemSwapTab = itemSwapTab;
_config = config;
_editor = editor;
_modCaches = modCaches;
_metaFileManager = metaFileManager;
_stainService = stainService;
_gameData = gameData;
_fileDialog = fileDialog;
_materialTab = new FileEditor<MtrlTab>(this, gameData, config, _fileDialog, "Materials", ".mtrl",
() => _editor.Files.Mtrl, DrawMaterialPanel, () => _mod?.ModPath.FullName ?? string.Empty,
bytes => new MtrlTab(this, new MtrlFile(bytes)));

View file

@ -66,7 +66,7 @@ public sealed class ConfigWindow : Window
UiHelpers.SetupCommonSizes();
try
{
if (Penumbra.ValidityChecker.ImcExceptions.Count > 0)
if (_validityChecker.ImcExceptions.Count > 0)
{
DrawProblemWindow(
$"There were {_validityChecker.ImcExceptions.Count} errors while trying to load IMC files from the game data.\n"
@ -75,14 +75,14 @@ public sealed class ConfigWindow : Window
+ "Please use the Launcher's Repair Game Files function to repair your client installation.");
DrawImcExceptions();
}
else if (!Penumbra.ValidityChecker.IsValidSourceRepo)
else if (!_validityChecker.IsValidSourceRepo)
{
DrawProblemWindow(
$"You are loading a release version of Penumbra from the repository \"{_pluginInterface.SourceRepository}\" instead of the official repository.\n"
+ $"Please use the official repository at {ValidityChecker.Repository}.\n\n"
+ "If you are developing for Penumbra and see this, you should compile your version in debug mode to avoid it.");
}
else if (Penumbra.ValidityChecker.IsNotInstalledPenumbra)
else if (_validityChecker.IsNotInstalledPenumbra)
{
DrawProblemWindow(
$"You are loading a release version of Penumbra from \"{_pluginInterface.AssemblyLocation.Directory?.FullName ?? "Unknown"}\" instead of the installedPlugins directory.\n\n"
@ -90,7 +90,7 @@ public sealed class ConfigWindow : Window
+ "If you do not know how to do this, please take a look at the readme in Penumbras github repository or join us in discord.\n"
+ "If you are developing for Penumbra and see this, you should compile your version in debug mode to avoid it.");
}
else if (Penumbra.ValidityChecker.DevPenumbraExists)
else if (_validityChecker.DevPenumbraExists)
{
DrawProblemWindow(
$"You are loading a installed version of Penumbra from \"{_pluginInterface.AssemblyLocation.Directory?.FullName ?? "Unknown"}\", "

View file

@ -119,18 +119,14 @@ public sealed class ModFileSystemSelector : FileSystemSelector<Mod, ModFileSyste
DrawHelpPopup();
if (ImGuiUtil.OpenNameField("Create New Mod", ref _newModName))
try
{
var newDir = _modManager.Creator.CreateEmptyMod(_modManager.BasePath, _newModName);
if (newDir != null)
{
var newDir = ModCreator.CreateModFolder(Penumbra.ModManager.BasePath, _newModName);
_modManager.DataEditor.CreateMeta(newDir, _newModName, Penumbra.Config.DefaultModAuthor, string.Empty, "1.0", string.Empty);
ModCreator.CreateDefaultFiles(newDir);
_modManager.AddMod(newDir);
_newModName = string.Empty;
}
catch (Exception e)
{
Penumbra.Log.Error($"Could not create directory for new Mod {_newModName}:\n{e}");
}
}
while (_modImportManager.AddUnpackedMod(out var mod))
{