mirror of
https://github.com/xivdev/Penumbra.git
synced 2025-12-20 07:34:25 +01:00
1127 lines
40 KiB
C#
1127 lines
40 KiB
C#
using Dalamud.Game.ClientState.Objects.Types;
|
|
using Lumina.Data;
|
|
using Newtonsoft.Json;
|
|
using OtterGui;
|
|
using Penumbra.Collections;
|
|
using Penumbra.Interop.Resolver;
|
|
using Penumbra.Interop.Structs;
|
|
using Penumbra.Meta.Manipulations;
|
|
using Penumbra.Mods;
|
|
using System;
|
|
using System.Collections.Generic;
|
|
using System.Diagnostics.CodeAnalysis;
|
|
using System.IO;
|
|
using System.Linq;
|
|
using System.Reflection;
|
|
using System.Runtime.CompilerServices;
|
|
using Penumbra.Api.Enums;
|
|
using Penumbra.GameData.Actors;
|
|
using Penumbra.Interop.Loader;
|
|
using Penumbra.String;
|
|
using Penumbra.String.Classes;
|
|
using Penumbra.Services;
|
|
|
|
namespace Penumbra.Api;
|
|
|
|
public class PenumbraApi : IDisposable, IPenumbraApi
|
|
{
|
|
public (int, int) ApiVersion
|
|
=> (4, 19);
|
|
|
|
private readonly Dictionary<ModCollection, ModCollection.ModSettingChangeDelegate> _delegates = new();
|
|
|
|
public event Action<string>? PreSettingsPanelDraw;
|
|
public event Action<string>? PostSettingsPanelDraw;
|
|
|
|
public event GameObjectRedrawnDelegate? GameObjectRedrawn
|
|
{
|
|
add
|
|
{
|
|
CheckInitialized();
|
|
_penumbra!.RedrawService.GameObjectRedrawn += value;
|
|
}
|
|
remove
|
|
{
|
|
CheckInitialized();
|
|
_penumbra!.RedrawService.GameObjectRedrawn -= value;
|
|
}
|
|
}
|
|
|
|
public event ModSettingChangedDelegate? ModSettingChanged;
|
|
|
|
public event CreatingCharacterBaseDelegate? CreatingCharacterBase
|
|
{
|
|
add
|
|
{
|
|
CheckInitialized();
|
|
PathResolver.DrawObjectState.CreatingCharacterBase += value;
|
|
}
|
|
remove
|
|
{
|
|
CheckInitialized();
|
|
PathResolver.DrawObjectState.CreatingCharacterBase -= value;
|
|
}
|
|
}
|
|
|
|
public event CreatedCharacterBaseDelegate? CreatedCharacterBase
|
|
{
|
|
add
|
|
{
|
|
CheckInitialized();
|
|
PathResolver.DrawObjectState.CreatedCharacterBase += value;
|
|
}
|
|
remove
|
|
{
|
|
CheckInitialized();
|
|
PathResolver.DrawObjectState.CreatedCharacterBase -= value;
|
|
}
|
|
}
|
|
|
|
public bool Valid
|
|
=> _penumbra != null;
|
|
|
|
private CommunicatorService _communicator;
|
|
private Penumbra _penumbra;
|
|
private Lumina.GameData? _lumina;
|
|
|
|
private Mod.Manager _modManager;
|
|
private ResourceLoader _resourceLoader;
|
|
private Configuration _config;
|
|
private ModCollection.Manager _collectionManager;
|
|
private DalamudServices _dalamud;
|
|
private TempCollectionManager _tempCollections;
|
|
private TempModManager _tempMods;
|
|
private ActorService _actors;
|
|
|
|
public unsafe PenumbraApi(CommunicatorService communicator, Penumbra penumbra, Mod.Manager modManager, ResourceLoader resourceLoader,
|
|
Configuration config, ModCollection.Manager collectionManager, DalamudServices dalamud, TempCollectionManager tempCollections,
|
|
TempModManager tempMods, ActorService actors)
|
|
{
|
|
_communicator = communicator;
|
|
_penumbra = penumbra;
|
|
_modManager = modManager;
|
|
_resourceLoader = resourceLoader;
|
|
_config = config;
|
|
_collectionManager = collectionManager;
|
|
_dalamud = dalamud;
|
|
_tempCollections = tempCollections;
|
|
_tempMods = tempMods;
|
|
_actors = actors;
|
|
|
|
_lumina = (Lumina.GameData?)_dalamud.GameData.GetType()
|
|
.GetField("gameData", BindingFlags.Instance | BindingFlags.NonPublic)
|
|
?.GetValue(_dalamud.GameData);
|
|
foreach (var collection in _collectionManager)
|
|
SubscribeToCollection(collection);
|
|
|
|
_communicator.CollectionChange.Event += SubscribeToNewCollections;
|
|
_resourceLoader.ResourceLoaded += OnResourceLoaded;
|
|
_modManager.ModPathChanged += ModPathChangeSubscriber;
|
|
}
|
|
|
|
public unsafe void Dispose()
|
|
{
|
|
if (!Valid)
|
|
return;
|
|
|
|
foreach (var collection in _collectionManager)
|
|
{
|
|
if (_delegates.TryGetValue(collection, out var del))
|
|
collection.ModSettingChanged -= del;
|
|
}
|
|
|
|
_resourceLoader.ResourceLoaded -= OnResourceLoaded;
|
|
_communicator.CollectionChange.Event -= SubscribeToNewCollections;
|
|
_modManager.ModPathChanged -= ModPathChangeSubscriber;
|
|
_lumina = null;
|
|
_communicator = null!;
|
|
_penumbra = null!;
|
|
_modManager = null!;
|
|
_resourceLoader = null!;
|
|
_config = null!;
|
|
_collectionManager = null!;
|
|
_dalamud = null!;
|
|
_tempCollections = null!;
|
|
_tempMods = null!;
|
|
_actors = null!;
|
|
}
|
|
|
|
public event ChangedItemClick? ChangedItemClicked;
|
|
|
|
public string GetModDirectory()
|
|
{
|
|
CheckInitialized();
|
|
return _config.ModDirectory;
|
|
}
|
|
|
|
private unsafe void OnResourceLoaded(ResourceHandle* _, Utf8GamePath originalPath, FullPath? manipulatedPath,
|
|
ResolveData resolveData)
|
|
{
|
|
if (resolveData.AssociatedGameObject != IntPtr.Zero)
|
|
GameObjectResourceResolved?.Invoke(resolveData.AssociatedGameObject, originalPath.ToString(),
|
|
manipulatedPath?.ToString() ?? originalPath.ToString());
|
|
}
|
|
|
|
public event Action<string, bool>? ModDirectoryChanged
|
|
{
|
|
add
|
|
{
|
|
CheckInitialized();
|
|
_modManager.ModDirectoryChanged += value;
|
|
}
|
|
remove
|
|
{
|
|
CheckInitialized();
|
|
_modManager.ModDirectoryChanged -= value;
|
|
}
|
|
}
|
|
|
|
public bool GetEnabledState()
|
|
=> _config.EnableMods;
|
|
|
|
public event Action<bool>? EnabledChange
|
|
{
|
|
add
|
|
{
|
|
CheckInitialized();
|
|
_penumbra!.EnabledChange += value;
|
|
}
|
|
remove
|
|
{
|
|
CheckInitialized();
|
|
_penumbra!.EnabledChange -= value;
|
|
}
|
|
}
|
|
|
|
public string GetConfiguration()
|
|
{
|
|
CheckInitialized();
|
|
return JsonConvert.SerializeObject(_config, Formatting.Indented);
|
|
}
|
|
|
|
public event ChangedItemHover? ChangedItemTooltip;
|
|
public event GameObjectResourceResolvedDelegate? GameObjectResourceResolved;
|
|
|
|
public PenumbraApiEc OpenMainWindow(TabType tab, string modDirectory, string modName)
|
|
{
|
|
CheckInitialized();
|
|
if (_penumbra!.ConfigWindow == null)
|
|
return PenumbraApiEc.SystemDisposed;
|
|
|
|
_penumbra!.ConfigWindow.IsOpen = true;
|
|
|
|
if (!Enum.IsDefined(tab))
|
|
return PenumbraApiEc.InvalidArgument;
|
|
|
|
if (tab != TabType.None)
|
|
_penumbra!.ConfigWindow.SelectTab(tab);
|
|
|
|
if (tab == TabType.Mods && (modDirectory.Length > 0 || modName.Length > 0))
|
|
{
|
|
if (_modManager.TryGetMod(modDirectory, modName, out var mod))
|
|
_penumbra!.ConfigWindow.SelectMod(mod);
|
|
else
|
|
return PenumbraApiEc.ModMissing;
|
|
}
|
|
|
|
return PenumbraApiEc.Success;
|
|
}
|
|
|
|
public void CloseMainWindow()
|
|
{
|
|
CheckInitialized();
|
|
if (_penumbra!.ConfigWindow == null)
|
|
return;
|
|
|
|
_penumbra!.ConfigWindow.IsOpen = false;
|
|
}
|
|
|
|
public void RedrawObject(int tableIndex, RedrawType setting)
|
|
{
|
|
CheckInitialized();
|
|
_penumbra!.RedrawService.RedrawObject(tableIndex, setting);
|
|
}
|
|
|
|
public void RedrawObject(string name, RedrawType setting)
|
|
{
|
|
CheckInitialized();
|
|
_penumbra!.RedrawService.RedrawObject(name, setting);
|
|
}
|
|
|
|
public void RedrawObject(GameObject? gameObject, RedrawType setting)
|
|
{
|
|
CheckInitialized();
|
|
_penumbra!.RedrawService.RedrawObject(gameObject, setting);
|
|
}
|
|
|
|
public void RedrawAll(RedrawType setting)
|
|
{
|
|
CheckInitialized();
|
|
_penumbra!.RedrawService.RedrawAll(setting);
|
|
}
|
|
|
|
public string ResolveDefaultPath(string path)
|
|
{
|
|
CheckInitialized();
|
|
return ResolvePath(path, _modManager, _collectionManager.Default);
|
|
}
|
|
|
|
public string ResolveInterfacePath(string path)
|
|
{
|
|
CheckInitialized();
|
|
return ResolvePath(path, _modManager, _collectionManager.Interface);
|
|
}
|
|
|
|
public string ResolvePlayerPath(string path)
|
|
{
|
|
CheckInitialized();
|
|
return ResolvePath(path, _modManager, PathResolver.PlayerCollection());
|
|
}
|
|
|
|
// TODO: cleanup when incrementing API level
|
|
public string ResolvePath(string path, string characterName)
|
|
=> ResolvePath(path, characterName, ushort.MaxValue);
|
|
|
|
public string ResolveGameObjectPath(string path, int gameObjectIdx)
|
|
{
|
|
CheckInitialized();
|
|
AssociatedCollection(gameObjectIdx, out var collection);
|
|
return ResolvePath(path, _modManager, collection);
|
|
}
|
|
|
|
public string ResolvePath(string path, string characterName, ushort worldId)
|
|
{
|
|
CheckInitialized();
|
|
return ResolvePath(path, _modManager,
|
|
_collectionManager.Individual(NameToIdentifier(characterName, worldId)));
|
|
}
|
|
|
|
// TODO: cleanup when incrementing API level
|
|
public string[] ReverseResolvePath(string path, string characterName)
|
|
=> ReverseResolvePath(path, characterName, ushort.MaxValue);
|
|
|
|
public string[] ReverseResolvePath(string path, string characterName, ushort worldId)
|
|
{
|
|
CheckInitialized();
|
|
if (!_config.EnableMods)
|
|
return new[]
|
|
{
|
|
path,
|
|
};
|
|
|
|
var ret = _collectionManager.Individual(NameToIdentifier(characterName, worldId)).ReverseResolvePath(new FullPath(path));
|
|
return ret.Select(r => r.ToString()).ToArray();
|
|
}
|
|
|
|
public string[] ReverseResolveGameObjectPath(string path, int gameObjectIdx)
|
|
{
|
|
CheckInitialized();
|
|
if (!_config.EnableMods)
|
|
return new[]
|
|
{
|
|
path,
|
|
};
|
|
|
|
AssociatedCollection(gameObjectIdx, out var collection);
|
|
var ret = collection.ReverseResolvePath(new FullPath(path));
|
|
return ret.Select(r => r.ToString()).ToArray();
|
|
}
|
|
|
|
public string[] ReverseResolvePlayerPath(string path)
|
|
{
|
|
CheckInitialized();
|
|
if (!_config.EnableMods)
|
|
return new[]
|
|
{
|
|
path,
|
|
};
|
|
|
|
var ret = PathResolver.PlayerCollection().ReverseResolvePath(new FullPath(path));
|
|
return ret.Select(r => r.ToString()).ToArray();
|
|
}
|
|
|
|
public (string[], string[][]) ResolvePlayerPaths(string[] forward, string[] reverse)
|
|
{
|
|
CheckInitialized();
|
|
if (!_config.EnableMods)
|
|
return (forward, reverse.Select(p => new[]
|
|
{
|
|
p,
|
|
}).ToArray());
|
|
|
|
var playerCollection = PathResolver.PlayerCollection();
|
|
var resolved = forward.Select(p => ResolvePath(p, _modManager, playerCollection)).ToArray();
|
|
var reverseResolved = playerCollection.ReverseResolvePaths(reverse);
|
|
return (resolved, reverseResolved.Select(a => a.Select(p => p.ToString()).ToArray()).ToArray());
|
|
}
|
|
|
|
public T? GetFile<T>(string gamePath) where T : FileResource
|
|
=> GetFileIntern<T>(ResolveDefaultPath(gamePath));
|
|
|
|
public T? GetFile<T>(string gamePath, string characterName) where T : FileResource
|
|
=> GetFileIntern<T>(ResolvePath(gamePath, characterName));
|
|
|
|
public IReadOnlyDictionary<string, object?> GetChangedItemsForCollection(string collectionName)
|
|
{
|
|
CheckInitialized();
|
|
try
|
|
{
|
|
if (!_collectionManager.ByName(collectionName, out var collection))
|
|
collection = ModCollection.Empty;
|
|
|
|
if (collection.HasCache)
|
|
return collection.ChangedItems.ToDictionary(kvp => kvp.Key, kvp => kvp.Value.Item2);
|
|
|
|
Penumbra.Log.Warning($"Collection {collectionName} does not exist or is not loaded.");
|
|
return new Dictionary<string, object?>();
|
|
}
|
|
catch (Exception e)
|
|
{
|
|
Penumbra.Log.Error($"Could not obtain Changed Items for {collectionName}:\n{e}");
|
|
throw;
|
|
}
|
|
}
|
|
|
|
public string GetCollectionForType(ApiCollectionType type)
|
|
{
|
|
CheckInitialized();
|
|
if (!Enum.IsDefined(type))
|
|
return string.Empty;
|
|
|
|
var collection = _collectionManager.ByType((CollectionType)type);
|
|
return collection?.Name ?? string.Empty;
|
|
}
|
|
|
|
public (PenumbraApiEc, string OldCollection) SetCollectionForType(ApiCollectionType type, string collectionName, bool allowCreateNew,
|
|
bool allowDelete)
|
|
{
|
|
CheckInitialized();
|
|
if (!Enum.IsDefined(type))
|
|
return (PenumbraApiEc.InvalidArgument, string.Empty);
|
|
|
|
var oldCollection = _collectionManager.ByType((CollectionType)type)?.Name ?? string.Empty;
|
|
|
|
if (collectionName.Length == 0)
|
|
{
|
|
if (oldCollection.Length == 0)
|
|
return (PenumbraApiEc.NothingChanged, oldCollection);
|
|
|
|
if (!allowDelete || type is ApiCollectionType.Current or ApiCollectionType.Default or ApiCollectionType.Interface)
|
|
return (PenumbraApiEc.AssignmentDeletionDisallowed, oldCollection);
|
|
|
|
_collectionManager.RemoveSpecialCollection((CollectionType)type);
|
|
return (PenumbraApiEc.Success, oldCollection);
|
|
}
|
|
|
|
if (!_collectionManager.ByName(collectionName, out var collection))
|
|
return (PenumbraApiEc.CollectionMissing, oldCollection);
|
|
|
|
if (oldCollection.Length == 0)
|
|
{
|
|
if (!allowCreateNew)
|
|
return (PenumbraApiEc.AssignmentCreationDisallowed, oldCollection);
|
|
|
|
_collectionManager.CreateSpecialCollection((CollectionType)type);
|
|
}
|
|
else if (oldCollection == collection.Name)
|
|
{
|
|
return (PenumbraApiEc.NothingChanged, oldCollection);
|
|
}
|
|
|
|
_collectionManager.SetCollection(collection, (CollectionType)type);
|
|
return (PenumbraApiEc.Success, oldCollection);
|
|
}
|
|
|
|
public (bool ObjectValid, bool IndividualSet, string EffectiveCollection) GetCollectionForObject(int gameObjectIdx)
|
|
{
|
|
CheckInitialized();
|
|
var id = AssociatedIdentifier(gameObjectIdx);
|
|
if (!id.IsValid)
|
|
return (false, false, _collectionManager.Default.Name);
|
|
|
|
if (_collectionManager.Individuals.Individuals.TryGetValue(id, out var collection))
|
|
return (true, true, collection.Name);
|
|
|
|
AssociatedCollection(gameObjectIdx, out collection);
|
|
return (true, false, collection.Name);
|
|
}
|
|
|
|
public (PenumbraApiEc, string OldCollection) SetCollectionForObject(int gameObjectIdx, string collectionName, bool allowCreateNew,
|
|
bool allowDelete)
|
|
{
|
|
CheckInitialized();
|
|
var id = AssociatedIdentifier(gameObjectIdx);
|
|
if (!id.IsValid)
|
|
return (PenumbraApiEc.InvalidIdentifier, _collectionManager.Default.Name);
|
|
|
|
var oldCollection = _collectionManager.Individuals.Individuals.TryGetValue(id, out var c) ? c.Name : string.Empty;
|
|
|
|
if (collectionName.Length == 0)
|
|
{
|
|
if (oldCollection.Length == 0)
|
|
return (PenumbraApiEc.NothingChanged, oldCollection);
|
|
|
|
if (!allowDelete)
|
|
return (PenumbraApiEc.AssignmentDeletionDisallowed, oldCollection);
|
|
|
|
var idx = _collectionManager.Individuals.Index(id);
|
|
_collectionManager.RemoveIndividualCollection(idx);
|
|
return (PenumbraApiEc.Success, oldCollection);
|
|
}
|
|
|
|
if (!_collectionManager.ByName(collectionName, out var collection))
|
|
return (PenumbraApiEc.CollectionMissing, oldCollection);
|
|
|
|
if (oldCollection.Length == 0)
|
|
{
|
|
if (!allowCreateNew)
|
|
return (PenumbraApiEc.AssignmentCreationDisallowed, oldCollection);
|
|
|
|
var ids = _collectionManager.Individuals.GetGroup(id);
|
|
_collectionManager.CreateIndividualCollection(ids);
|
|
}
|
|
else if (oldCollection == collection.Name)
|
|
{
|
|
return (PenumbraApiEc.NothingChanged, oldCollection);
|
|
}
|
|
|
|
_collectionManager.SetCollection(collection, CollectionType.Individual, _collectionManager.Individuals.Index(id));
|
|
return (PenumbraApiEc.Success, oldCollection);
|
|
}
|
|
|
|
public IList<string> GetCollections()
|
|
{
|
|
CheckInitialized();
|
|
return _collectionManager.Select(c => c.Name).ToArray();
|
|
}
|
|
|
|
public string GetCurrentCollection()
|
|
{
|
|
CheckInitialized();
|
|
return _collectionManager.Current.Name;
|
|
}
|
|
|
|
public string GetDefaultCollection()
|
|
{
|
|
CheckInitialized();
|
|
return _collectionManager.Default.Name;
|
|
}
|
|
|
|
public string GetInterfaceCollection()
|
|
{
|
|
CheckInitialized();
|
|
return _collectionManager.Interface.Name;
|
|
}
|
|
|
|
// TODO: cleanup when incrementing API level
|
|
public (string, bool) GetCharacterCollection(string characterName)
|
|
=> GetCharacterCollection(characterName, ushort.MaxValue);
|
|
|
|
public (string, bool) GetCharacterCollection(string characterName, ushort worldId)
|
|
{
|
|
CheckInitialized();
|
|
return _collectionManager.Individuals.TryGetCollection(NameToIdentifier(characterName, worldId), out var collection)
|
|
? (collection.Name, true)
|
|
: (_collectionManager.Default.Name, false);
|
|
}
|
|
|
|
public (IntPtr, string) GetDrawObjectInfo(IntPtr drawObject)
|
|
{
|
|
CheckInitialized();
|
|
var (obj, collection) = PathResolver.IdentifyDrawObject(drawObject);
|
|
return (obj, collection.ModCollection.Name);
|
|
}
|
|
|
|
public int GetCutsceneParentIndex(int actorIdx)
|
|
{
|
|
CheckInitialized();
|
|
return _penumbra!.PathResolver.CutsceneActor(actorIdx);
|
|
}
|
|
|
|
public IList<(string, string)> GetModList()
|
|
{
|
|
CheckInitialized();
|
|
return _modManager.Select(m => (m.ModPath.Name, m.Name.Text)).ToArray();
|
|
}
|
|
|
|
public IDictionary<string, (IList<string>, GroupType)>? GetAvailableModSettings(string modDirectory, string modName)
|
|
{
|
|
CheckInitialized();
|
|
return _modManager.TryGetMod(modDirectory, modName, out var mod)
|
|
? mod.Groups.ToDictionary(g => g.Name, g => ((IList<string>)g.Select(o => o.Name).ToList(), g.Type))
|
|
: null;
|
|
}
|
|
|
|
public (PenumbraApiEc, (bool, int, IDictionary<string, IList<string>>, bool)?) GetCurrentModSettings(string collectionName,
|
|
string modDirectory, string modName, bool allowInheritance)
|
|
{
|
|
CheckInitialized();
|
|
if (!_collectionManager.ByName(collectionName, out var collection))
|
|
return (PenumbraApiEc.CollectionMissing, null);
|
|
|
|
if (!_modManager.TryGetMod(modDirectory, modName, out var mod))
|
|
return (PenumbraApiEc.ModMissing, null);
|
|
|
|
var settings = allowInheritance ? collection.Settings[mod.Index] : collection[mod.Index].Settings;
|
|
if (settings == null)
|
|
return (PenumbraApiEc.Success, null);
|
|
|
|
var shareSettings = settings.ConvertToShareable(mod);
|
|
return (PenumbraApiEc.Success,
|
|
(shareSettings.Enabled, shareSettings.Priority, shareSettings.Settings, collection.Settings[mod.Index] != null));
|
|
}
|
|
|
|
public PenumbraApiEc ReloadMod(string modDirectory, string modName)
|
|
{
|
|
CheckInitialized();
|
|
if (!_modManager.TryGetMod(modDirectory, modName, out var mod))
|
|
return PenumbraApiEc.ModMissing;
|
|
|
|
_modManager.ReloadMod(mod.Index);
|
|
return PenumbraApiEc.Success;
|
|
}
|
|
|
|
public PenumbraApiEc AddMod(string modDirectory)
|
|
{
|
|
CheckInitialized();
|
|
var dir = new DirectoryInfo(Path.Join(_modManager.BasePath.FullName, Path.GetFileName(modDirectory)));
|
|
if (!dir.Exists)
|
|
return PenumbraApiEc.FileMissing;
|
|
|
|
_modManager.AddMod(dir);
|
|
return PenumbraApiEc.Success;
|
|
}
|
|
|
|
public PenumbraApiEc DeleteMod(string modDirectory, string modName)
|
|
{
|
|
CheckInitialized();
|
|
if (!_modManager.TryGetMod(modDirectory, modName, out var mod))
|
|
return PenumbraApiEc.NothingChanged;
|
|
|
|
_modManager.DeleteMod(mod.Index);
|
|
return PenumbraApiEc.Success;
|
|
}
|
|
|
|
public event Action<string>? ModDeleted;
|
|
public event Action<string>? ModAdded;
|
|
public event Action<string, string>? ModMoved;
|
|
|
|
private void ModPathChangeSubscriber(ModPathChangeType type, Mod mod, DirectoryInfo? oldDirectory,
|
|
DirectoryInfo? newDirectory)
|
|
{
|
|
switch (type)
|
|
{
|
|
case ModPathChangeType.Deleted when oldDirectory != null:
|
|
ModDeleted?.Invoke(oldDirectory.Name);
|
|
break;
|
|
case ModPathChangeType.Added when newDirectory != null:
|
|
ModAdded?.Invoke(newDirectory.Name);
|
|
break;
|
|
case ModPathChangeType.Moved when newDirectory != null && oldDirectory != null:
|
|
ModMoved?.Invoke(oldDirectory.Name, newDirectory.Name);
|
|
break;
|
|
}
|
|
}
|
|
|
|
public (PenumbraApiEc, string, bool) GetModPath(string modDirectory, string modName)
|
|
{
|
|
CheckInitialized();
|
|
if (!_modManager.TryGetMod(modDirectory, modName, out var mod)
|
|
|| !_penumbra!.ModFileSystem.FindLeaf(mod, out var leaf))
|
|
return (PenumbraApiEc.ModMissing, string.Empty, false);
|
|
|
|
var fullPath = leaf.FullName();
|
|
|
|
return (PenumbraApiEc.Success, fullPath, !ModFileSystem.ModHasDefaultPath(mod, fullPath));
|
|
}
|
|
|
|
public PenumbraApiEc SetModPath(string modDirectory, string modName, string newPath)
|
|
{
|
|
CheckInitialized();
|
|
if (newPath.Length == 0)
|
|
return PenumbraApiEc.InvalidArgument;
|
|
|
|
if (!_modManager.TryGetMod(modDirectory, modName, out var mod)
|
|
|| !_penumbra!.ModFileSystem.FindLeaf(mod, out var leaf))
|
|
return PenumbraApiEc.ModMissing;
|
|
|
|
try
|
|
{
|
|
_penumbra.ModFileSystem.RenameAndMove(leaf, newPath);
|
|
return PenumbraApiEc.Success;
|
|
}
|
|
catch
|
|
{
|
|
return PenumbraApiEc.PathRenameFailed;
|
|
}
|
|
}
|
|
|
|
public PenumbraApiEc TryInheritMod(string collectionName, string modDirectory, string modName, bool inherit)
|
|
{
|
|
CheckInitialized();
|
|
if (!_collectionManager.ByName(collectionName, out var collection))
|
|
return PenumbraApiEc.CollectionMissing;
|
|
|
|
if (!_modManager.TryGetMod(modDirectory, modName, out var mod))
|
|
return PenumbraApiEc.ModMissing;
|
|
|
|
|
|
return collection.SetModInheritance(mod.Index, inherit) ? PenumbraApiEc.Success : PenumbraApiEc.NothingChanged;
|
|
}
|
|
|
|
public PenumbraApiEc TrySetMod(string collectionName, string modDirectory, string modName, bool enabled)
|
|
{
|
|
CheckInitialized();
|
|
if (!_collectionManager.ByName(collectionName, out var collection))
|
|
return PenumbraApiEc.CollectionMissing;
|
|
|
|
if (!_modManager.TryGetMod(modDirectory, modName, out var mod))
|
|
return PenumbraApiEc.ModMissing;
|
|
|
|
return collection.SetModState(mod.Index, enabled) ? PenumbraApiEc.Success : PenumbraApiEc.NothingChanged;
|
|
}
|
|
|
|
public PenumbraApiEc TrySetModPriority(string collectionName, string modDirectory, string modName, int priority)
|
|
{
|
|
CheckInitialized();
|
|
if (!_collectionManager.ByName(collectionName, out var collection))
|
|
return PenumbraApiEc.CollectionMissing;
|
|
|
|
if (!_modManager.TryGetMod(modDirectory, modName, out var mod))
|
|
return PenumbraApiEc.ModMissing;
|
|
|
|
return collection.SetModPriority(mod.Index, priority) ? PenumbraApiEc.Success : PenumbraApiEc.NothingChanged;
|
|
}
|
|
|
|
public PenumbraApiEc TrySetModSetting(string collectionName, string modDirectory, string modName, string optionGroupName,
|
|
string optionName)
|
|
{
|
|
CheckInitialized();
|
|
if (!_collectionManager.ByName(collectionName, out var collection))
|
|
return PenumbraApiEc.CollectionMissing;
|
|
|
|
if (!_modManager.TryGetMod(modDirectory, modName, out var mod))
|
|
return PenumbraApiEc.ModMissing;
|
|
|
|
var groupIdx = mod.Groups.IndexOf(g => g.Name == optionGroupName);
|
|
if (groupIdx < 0)
|
|
return PenumbraApiEc.OptionGroupMissing;
|
|
|
|
var optionIdx = mod.Groups[groupIdx].IndexOf(o => o.Name == optionName);
|
|
if (optionIdx < 0)
|
|
return PenumbraApiEc.OptionMissing;
|
|
|
|
var setting = mod.Groups[groupIdx].Type == GroupType.Multi ? 1u << optionIdx : (uint)optionIdx;
|
|
|
|
return collection.SetModSetting(mod.Index, groupIdx, setting) ? PenumbraApiEc.Success : PenumbraApiEc.NothingChanged;
|
|
}
|
|
|
|
public PenumbraApiEc TrySetModSettings(string collectionName, string modDirectory, string modName, string optionGroupName,
|
|
IReadOnlyList<string> optionNames)
|
|
{
|
|
CheckInitialized();
|
|
if (!_collectionManager.ByName(collectionName, out var collection))
|
|
return PenumbraApiEc.CollectionMissing;
|
|
|
|
if (!_modManager.TryGetMod(modDirectory, modName, out var mod))
|
|
return PenumbraApiEc.ModMissing;
|
|
|
|
var groupIdx = mod.Groups.IndexOf(g => g.Name == optionGroupName);
|
|
if (groupIdx < 0)
|
|
return PenumbraApiEc.OptionGroupMissing;
|
|
|
|
var group = mod.Groups[groupIdx];
|
|
|
|
uint setting = 0;
|
|
if (group.Type == GroupType.Single)
|
|
{
|
|
var optionIdx = optionNames.Count == 0 ? -1 : group.IndexOf(o => o.Name == optionNames[^1]);
|
|
if (optionIdx < 0)
|
|
return PenumbraApiEc.OptionMissing;
|
|
|
|
setting = (uint)optionIdx;
|
|
}
|
|
else
|
|
{
|
|
foreach (var name in optionNames)
|
|
{
|
|
var optionIdx = group.IndexOf(o => o.Name == name);
|
|
if (optionIdx < 0)
|
|
return PenumbraApiEc.OptionMissing;
|
|
|
|
setting |= 1u << optionIdx;
|
|
}
|
|
}
|
|
|
|
return collection.SetModSetting(mod.Index, groupIdx, setting) ? PenumbraApiEc.Success : PenumbraApiEc.NothingChanged;
|
|
}
|
|
|
|
|
|
public PenumbraApiEc CopyModSettings(string? collectionName, string modDirectoryFrom, string modDirectoryTo)
|
|
{
|
|
CheckInitialized();
|
|
|
|
var sourceModIdx = _modManager
|
|
.FirstOrDefault(m => string.Equals(m.ModPath.Name, modDirectoryFrom, StringComparison.OrdinalIgnoreCase))?.Index
|
|
?? -1;
|
|
var targetModIdx = _modManager
|
|
.FirstOrDefault(m => string.Equals(m.ModPath.Name, modDirectoryTo, StringComparison.OrdinalIgnoreCase))?.Index
|
|
?? -1;
|
|
if (string.IsNullOrEmpty(collectionName))
|
|
foreach (var collection in _collectionManager)
|
|
collection.CopyModSettings(sourceModIdx, modDirectoryFrom, targetModIdx, modDirectoryTo);
|
|
else if (_collectionManager.ByName(collectionName, out var collection))
|
|
collection.CopyModSettings(sourceModIdx, modDirectoryFrom, targetModIdx, modDirectoryTo);
|
|
else
|
|
return PenumbraApiEc.CollectionMissing;
|
|
|
|
return PenumbraApiEc.Success;
|
|
}
|
|
|
|
public (PenumbraApiEc, string) CreateTemporaryCollection(string tag, string character, bool forceOverwriteCharacter)
|
|
{
|
|
CheckInitialized();
|
|
|
|
if (!ActorManager.VerifyPlayerName(character.AsSpan()) || tag.Length == 0)
|
|
return (PenumbraApiEc.InvalidArgument, string.Empty);
|
|
|
|
var identifier = NameToIdentifier(character, ushort.MaxValue);
|
|
if (!identifier.IsValid)
|
|
return (PenumbraApiEc.InvalidArgument, string.Empty);
|
|
|
|
if (!forceOverwriteCharacter && _collectionManager.Individuals.Individuals.ContainsKey(identifier)
|
|
|| _tempCollections.Collections.Individuals.ContainsKey(identifier))
|
|
return (PenumbraApiEc.CharacterCollectionExists, string.Empty);
|
|
|
|
var name = $"{tag}_{character}";
|
|
var ret = CreateNamedTemporaryCollection(name);
|
|
if (ret != PenumbraApiEc.Success)
|
|
return (ret, name);
|
|
|
|
if (_tempCollections.AddIdentifier(name, identifier))
|
|
return (PenumbraApiEc.Success, name);
|
|
|
|
_tempCollections.RemoveTemporaryCollection(name);
|
|
return (PenumbraApiEc.UnknownError, string.Empty);
|
|
}
|
|
|
|
public PenumbraApiEc CreateNamedTemporaryCollection(string name)
|
|
{
|
|
CheckInitialized();
|
|
if (name.Length == 0 || Mod.Creator.ReplaceBadXivSymbols(name) != name)
|
|
return PenumbraApiEc.InvalidArgument;
|
|
|
|
return _tempCollections.CreateTemporaryCollection(name).Length > 0
|
|
? PenumbraApiEc.Success
|
|
: PenumbraApiEc.CollectionExists;
|
|
}
|
|
|
|
public PenumbraApiEc AssignTemporaryCollection(string collectionName, int actorIndex, bool forceAssignment)
|
|
{
|
|
CheckInitialized();
|
|
|
|
if (!_actors.Valid)
|
|
return PenumbraApiEc.SystemDisposed;
|
|
|
|
if (actorIndex < 0 || actorIndex >= DalamudServices.SObjects.Length)
|
|
return PenumbraApiEc.InvalidArgument;
|
|
|
|
var identifier = _actors.AwaitedService.FromObject(DalamudServices.SObjects[actorIndex], false, false, true);
|
|
if (!identifier.IsValid)
|
|
return PenumbraApiEc.InvalidArgument;
|
|
|
|
if (!_tempCollections.CollectionByName(collectionName, out var collection))
|
|
return PenumbraApiEc.CollectionMissing;
|
|
|
|
if (!forceAssignment
|
|
&& (_tempCollections.Collections.Individuals.ContainsKey(identifier)
|
|
|| _collectionManager.Individuals.Individuals.ContainsKey(identifier)))
|
|
return PenumbraApiEc.CharacterCollectionExists;
|
|
|
|
var group = _tempCollections.Collections.GetGroup(identifier);
|
|
return _tempCollections.AddIdentifier(collection, group)
|
|
? PenumbraApiEc.Success
|
|
: PenumbraApiEc.UnknownError;
|
|
}
|
|
|
|
public PenumbraApiEc RemoveTemporaryCollection(string character)
|
|
{
|
|
CheckInitialized();
|
|
return _tempCollections.RemoveByCharacterName(character)
|
|
? PenumbraApiEc.Success
|
|
: PenumbraApiEc.NothingChanged;
|
|
}
|
|
|
|
public PenumbraApiEc RemoveTemporaryCollectionByName(string name)
|
|
{
|
|
CheckInitialized();
|
|
return _tempCollections.RemoveTemporaryCollection(name)
|
|
? PenumbraApiEc.Success
|
|
: PenumbraApiEc.NothingChanged;
|
|
}
|
|
|
|
public PenumbraApiEc AddTemporaryModAll(string tag, Dictionary<string, string> paths, string manipString, int priority)
|
|
{
|
|
CheckInitialized();
|
|
if (!ConvertPaths(paths, out var p))
|
|
return PenumbraApiEc.InvalidGamePath;
|
|
|
|
if (!ConvertManips(manipString, out var m))
|
|
return PenumbraApiEc.InvalidManipulation;
|
|
|
|
return _tempMods.Register(tag, null, p, m, priority) switch
|
|
{
|
|
RedirectResult.Success => PenumbraApiEc.Success,
|
|
_ => PenumbraApiEc.UnknownError,
|
|
};
|
|
}
|
|
|
|
public PenumbraApiEc AddTemporaryMod(string tag, string collectionName, Dictionary<string, string> paths, string manipString,
|
|
int priority)
|
|
{
|
|
CheckInitialized();
|
|
if (!_tempCollections.CollectionByName(collectionName, out var collection)
|
|
&& !_collectionManager.ByName(collectionName, out collection))
|
|
return PenumbraApiEc.CollectionMissing;
|
|
|
|
if (!ConvertPaths(paths, out var p))
|
|
return PenumbraApiEc.InvalidGamePath;
|
|
|
|
if (!ConvertManips(manipString, out var m))
|
|
return PenumbraApiEc.InvalidManipulation;
|
|
|
|
return _tempMods.Register(tag, collection, p, m, priority) switch
|
|
{
|
|
RedirectResult.Success => PenumbraApiEc.Success,
|
|
_ => PenumbraApiEc.UnknownError,
|
|
};
|
|
}
|
|
|
|
public PenumbraApiEc RemoveTemporaryModAll(string tag, int priority)
|
|
{
|
|
CheckInitialized();
|
|
return _tempMods.Unregister(tag, null, priority) switch
|
|
{
|
|
RedirectResult.Success => PenumbraApiEc.Success,
|
|
RedirectResult.NotRegistered => PenumbraApiEc.NothingChanged,
|
|
_ => PenumbraApiEc.UnknownError,
|
|
};
|
|
}
|
|
|
|
public PenumbraApiEc RemoveTemporaryMod(string tag, string collectionName, int priority)
|
|
{
|
|
CheckInitialized();
|
|
if (!_tempCollections.CollectionByName(collectionName, out var collection)
|
|
&& !_collectionManager.ByName(collectionName, out collection))
|
|
return PenumbraApiEc.CollectionMissing;
|
|
|
|
return _tempMods.Unregister(tag, collection, priority) switch
|
|
{
|
|
RedirectResult.Success => PenumbraApiEc.Success,
|
|
RedirectResult.NotRegistered => PenumbraApiEc.NothingChanged,
|
|
_ => PenumbraApiEc.UnknownError,
|
|
};
|
|
}
|
|
|
|
public string GetPlayerMetaManipulations()
|
|
{
|
|
CheckInitialized();
|
|
var collection = PathResolver.PlayerCollection();
|
|
var set = collection.MetaCache?.Manipulations.ToArray() ?? Array.Empty<MetaManipulation>();
|
|
return Functions.ToCompressedBase64(set, MetaManipulation.CurrentVersion);
|
|
}
|
|
|
|
// TODO: cleanup when incrementing API
|
|
public string GetMetaManipulations(string characterName)
|
|
=> GetMetaManipulations(characterName, ushort.MaxValue);
|
|
|
|
public string GetMetaManipulations(string characterName, ushort worldId)
|
|
{
|
|
CheckInitialized();
|
|
var identifier = NameToIdentifier(characterName, worldId);
|
|
var collection = _tempCollections.Collections.TryGetCollection(identifier, out var c)
|
|
? c
|
|
: _collectionManager.Individual(identifier);
|
|
var set = collection.MetaCache?.Manipulations.ToArray() ?? Array.Empty<MetaManipulation>();
|
|
return Functions.ToCompressedBase64(set, MetaManipulation.CurrentVersion);
|
|
}
|
|
|
|
public string GetGameObjectMetaManipulations(int gameObjectIdx)
|
|
{
|
|
CheckInitialized();
|
|
AssociatedCollection(gameObjectIdx, out var collection);
|
|
var set = collection.MetaCache?.Manipulations.ToArray() ?? Array.Empty<MetaManipulation>();
|
|
return Functions.ToCompressedBase64(set, MetaManipulation.CurrentVersion);
|
|
}
|
|
|
|
internal bool HasTooltip
|
|
=> ChangedItemTooltip != null;
|
|
|
|
internal void InvokeTooltip(object? it)
|
|
=> ChangedItemTooltip?.Invoke(it);
|
|
|
|
internal void InvokeClick(MouseButton button, object? it)
|
|
=> ChangedItemClicked?.Invoke(button, it);
|
|
|
|
|
|
[MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)]
|
|
private void CheckInitialized()
|
|
{
|
|
if (!Valid)
|
|
throw new Exception("PluginShare is not initialized.");
|
|
}
|
|
|
|
// Return the collection associated to a current game object. If it does not exist, return the default collection.
|
|
// If the index is invalid, returns false and the default collection.
|
|
[MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)]
|
|
private unsafe bool AssociatedCollection(int gameObjectIdx, out ModCollection collection)
|
|
{
|
|
collection = _collectionManager.Default;
|
|
if (gameObjectIdx < 0 || gameObjectIdx >= DalamudServices.SObjects.Length)
|
|
return false;
|
|
|
|
var ptr = (FFXIVClientStructs.FFXIV.Client.Game.Object.GameObject*)DalamudServices.SObjects.GetObjectAddress(gameObjectIdx);
|
|
var data = PathResolver.IdentifyCollection(ptr, false);
|
|
if (data.Valid)
|
|
collection = data.ModCollection;
|
|
|
|
return true;
|
|
}
|
|
|
|
[MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)]
|
|
private unsafe ActorIdentifier AssociatedIdentifier(int gameObjectIdx)
|
|
{
|
|
if (gameObjectIdx < 0 || gameObjectIdx >= _dalamud.Objects.Length || !_actors.Valid)
|
|
return ActorIdentifier.Invalid;
|
|
|
|
var ptr = (FFXIVClientStructs.FFXIV.Client.Game.Object.GameObject*)DalamudServices.SObjects.GetObjectAddress(gameObjectIdx);
|
|
return _actors.AwaitedService.FromObject(ptr, out _, false, true, true);
|
|
}
|
|
|
|
// Resolve a path given by string for a specific collection.
|
|
[MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)]
|
|
private string ResolvePath(string path, Mod.Manager _, ModCollection collection)
|
|
{
|
|
if (!_config.EnableMods)
|
|
return path;
|
|
|
|
var gamePath = Utf8GamePath.FromString(path, out var p, true) ? p : Utf8GamePath.Empty;
|
|
var ret = collection.ResolvePath(gamePath);
|
|
return ret?.ToString() ?? path;
|
|
}
|
|
|
|
// Get a file for a resolved path.
|
|
private T? GetFileIntern<T>(string resolvedPath) where T : FileResource
|
|
{
|
|
CheckInitialized();
|
|
try
|
|
{
|
|
if (Path.IsPathRooted(resolvedPath))
|
|
return _lumina?.GetFileFromDisk<T>(resolvedPath);
|
|
|
|
return _dalamud.GameData.GetFile<T>(resolvedPath);
|
|
}
|
|
catch (Exception e)
|
|
{
|
|
Penumbra.Log.Warning($"Could not load file {resolvedPath}:\n{e}");
|
|
return null;
|
|
}
|
|
}
|
|
|
|
|
|
// Convert a dictionary of strings to a dictionary of gamepaths to full paths.
|
|
// Only returns true if all paths can successfully be converted and added.
|
|
private static bool ConvertPaths(IReadOnlyDictionary<string, string> redirections,
|
|
[NotNullWhen(true)] out Dictionary<Utf8GamePath, FullPath>? paths)
|
|
{
|
|
paths = new Dictionary<Utf8GamePath, FullPath>(redirections.Count);
|
|
foreach (var (gString, fString) in redirections)
|
|
{
|
|
if (!Utf8GamePath.FromString(gString, out var path, false))
|
|
{
|
|
paths = null;
|
|
return false;
|
|
}
|
|
|
|
var fullPath = new FullPath(fString);
|
|
if (!paths.TryAdd(path, fullPath))
|
|
{
|
|
paths = null;
|
|
return false;
|
|
}
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
// Convert manipulations from a transmitted base64 string to actual manipulations.
|
|
// The empty string is treated as an empty set.
|
|
// Only returns true if all conversions are successful and distinct.
|
|
private static bool ConvertManips(string manipString,
|
|
[NotNullWhen(true)] out HashSet<MetaManipulation>? manips)
|
|
{
|
|
if (manipString.Length == 0)
|
|
{
|
|
manips = new HashSet<MetaManipulation>();
|
|
return true;
|
|
}
|
|
|
|
if (Functions.FromCompressedBase64<MetaManipulation[]>(manipString, out var manipArray) != MetaManipulation.CurrentVersion)
|
|
{
|
|
manips = null;
|
|
return false;
|
|
}
|
|
|
|
manips = new HashSet<MetaManipulation>(manipArray!.Length);
|
|
foreach (var manip in manipArray.Where(m => m.ManipulationType != MetaManipulation.Type.Unknown))
|
|
{
|
|
if (!manips.Add(manip))
|
|
{
|
|
manips = null;
|
|
return false;
|
|
}
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
private void SubscribeToCollection(ModCollection c)
|
|
{
|
|
var name = c.Name;
|
|
|
|
void Del(ModSettingChange type, int idx, int _, int _2, bool inherited)
|
|
=> ModSettingChanged?.Invoke(type, name, idx >= 0 ? _modManager[idx].ModPath.Name : string.Empty, inherited);
|
|
|
|
_delegates[c] = Del;
|
|
c.ModSettingChanged += Del;
|
|
}
|
|
|
|
private void SubscribeToNewCollections(CollectionType type, ModCollection? oldCollection, ModCollection? newCollection, string _)
|
|
{
|
|
if (type != CollectionType.Inactive)
|
|
return;
|
|
|
|
if (oldCollection != null && _delegates.TryGetValue(oldCollection, out var del))
|
|
oldCollection.ModSettingChanged -= del;
|
|
|
|
if (newCollection != null)
|
|
SubscribeToCollection(newCollection);
|
|
}
|
|
|
|
public void InvokePreSettingsPanel(string modDirectory)
|
|
=> PreSettingsPanelDraw?.Invoke(modDirectory);
|
|
|
|
public void InvokePostSettingsPanel(string modDirectory)
|
|
=> PostSettingsPanelDraw?.Invoke(modDirectory);
|
|
|
|
// TODO: replace all usages with ActorIdentifier stuff when incrementing API
|
|
private ActorIdentifier NameToIdentifier(string name, ushort worldId)
|
|
{
|
|
if (!_actors.Valid)
|
|
return ActorIdentifier.Invalid;
|
|
|
|
// Verified to be valid name beforehand.
|
|
var b = ByteString.FromStringUnsafe(name, false);
|
|
return _actors.AwaitedService.CreatePlayer(b, worldId);
|
|
}
|
|
}
|