mirror of
https://github.com/xivdev/Penumbra.git
synced 2025-12-13 12:14:17 +01:00
Merge branch 'master' into mdl-export
# Conflicts: # Penumbra/Services/ServiceManagerA.cs # Penumbra/UI/AdvancedWindow/ModEditWindow.cs # Penumbra/packages.lock.json
This commit is contained in:
commit
1b98626a61
80 changed files with 1793 additions and 1443 deletions
2
OtterGui
2
OtterGui
|
|
@ -1 +1 @@
|
||||||
Subproject commit 197d23eee167c232000f22ef40a7a2bded913b6c
|
Subproject commit 22ae2a8993ebf3af2313072968a44905a3fcdd2a
|
||||||
|
|
@ -1 +1 @@
|
||||||
Subproject commit db421413a15c48c63eb883dbfc2ac863c579d4c6
|
Subproject commit ac3fc0981ac8f503ac91d2419bd28c54f271763e
|
||||||
|
|
@ -259,7 +259,7 @@ public class PenumbraApi : IDisposable, IPenumbraApi
|
||||||
}
|
}
|
||||||
else if (tab != TabType.None)
|
else if (tab != TabType.None)
|
||||||
{
|
{
|
||||||
_communicator.SelectTab.Invoke(tab);
|
_communicator.SelectTab.Invoke(tab, null);
|
||||||
}
|
}
|
||||||
|
|
||||||
return PenumbraApiEc.Success;
|
return PenumbraApiEc.Success;
|
||||||
|
|
|
||||||
|
|
@ -11,7 +11,7 @@ namespace Penumbra.Communication;
|
||||||
/// <item>Parameter is the clicked object data if any. </item>
|
/// <item>Parameter is the clicked object data if any. </item>
|
||||||
/// </list>
|
/// </list>
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public sealed class ChangedItemClick : EventWrapper<Action<MouseButton, object?>, ChangedItemClick.Priority>
|
public sealed class ChangedItemClick() : EventWrapper<MouseButton, object?, ChangedItemClick.Priority>(nameof(ChangedItemClick))
|
||||||
{
|
{
|
||||||
public enum Priority
|
public enum Priority
|
||||||
{
|
{
|
||||||
|
|
@ -21,11 +21,4 @@ public sealed class ChangedItemClick : EventWrapper<Action<MouseButton, object?>
|
||||||
/// <seealso cref="Penumbra.SetupApi"/>
|
/// <seealso cref="Penumbra.SetupApi"/>
|
||||||
Link = 1,
|
Link = 1,
|
||||||
}
|
}
|
||||||
|
|
||||||
public ChangedItemClick()
|
|
||||||
: base(nameof(ChangedItemClick))
|
|
||||||
{ }
|
|
||||||
|
|
||||||
public void Invoke(MouseButton button, object? data)
|
|
||||||
=> Invoke(this, button, data);
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -8,7 +8,7 @@ namespace Penumbra.Communication;
|
||||||
/// <item>Parameter is the hovered object data if any. </item>
|
/// <item>Parameter is the hovered object data if any. </item>
|
||||||
/// </list>
|
/// </list>
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public sealed class ChangedItemHover : EventWrapper<Action<object?>, ChangedItemHover.Priority>
|
public sealed class ChangedItemHover() : EventWrapper<object?, ChangedItemHover.Priority>(nameof(ChangedItemHover))
|
||||||
{
|
{
|
||||||
public enum Priority
|
public enum Priority
|
||||||
{
|
{
|
||||||
|
|
@ -19,13 +19,6 @@ public sealed class ChangedItemHover : EventWrapper<Action<object?>, ChangedItem
|
||||||
Link = 1,
|
Link = 1,
|
||||||
}
|
}
|
||||||
|
|
||||||
public ChangedItemHover()
|
|
||||||
: base(nameof(ChangedItemHover))
|
|
||||||
{ }
|
|
||||||
|
|
||||||
public void Invoke(object? data)
|
|
||||||
=> Invoke(this, data);
|
|
||||||
|
|
||||||
public bool HasTooltip
|
public bool HasTooltip
|
||||||
=> HasSubscribers;
|
=> HasSubscribers;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -12,7 +12,8 @@ namespace Penumbra.Communication;
|
||||||
/// <item>Parameter is the new collection, or null on deletions.</item>
|
/// <item>Parameter is the new collection, or null on deletions.</item>
|
||||||
/// <item>Parameter is the display name for Individual collections or an empty string otherwise.</item>
|
/// <item>Parameter is the display name for Individual collections or an empty string otherwise.</item>
|
||||||
/// </list> </summary>
|
/// </list> </summary>
|
||||||
public sealed class CollectionChange : EventWrapper<Action<CollectionType, ModCollection?, ModCollection?, string>, CollectionChange.Priority>
|
public sealed class CollectionChange()
|
||||||
|
: EventWrapper<CollectionType, ModCollection?, ModCollection?, string, CollectionChange.Priority>(nameof(CollectionChange))
|
||||||
{
|
{
|
||||||
public enum Priority
|
public enum Priority
|
||||||
{
|
{
|
||||||
|
|
@ -46,11 +47,4 @@ public sealed class CollectionChange : EventWrapper<Action<CollectionType, ModCo
|
||||||
/// <seealso cref="UI.ModsTab.ModFileSystemSelector.OnCollectionChange"/>
|
/// <seealso cref="UI.ModsTab.ModFileSystemSelector.OnCollectionChange"/>
|
||||||
ModFileSystemSelector = 0,
|
ModFileSystemSelector = 0,
|
||||||
}
|
}
|
||||||
|
|
||||||
public CollectionChange()
|
|
||||||
: base(nameof(CollectionChange))
|
|
||||||
{ }
|
|
||||||
|
|
||||||
public void Invoke(CollectionType collectionType, ModCollection? oldCollection, ModCollection? newCollection, string displayName)
|
|
||||||
=> Invoke(this, collectionType, oldCollection, newCollection, displayName);
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -10,7 +10,8 @@ namespace Penumbra.Communication;
|
||||||
/// <item>Parameter is whether the change was itself inherited, i.e. if it happened in a direct parent (false) or a more removed ancestor (true). </item>
|
/// <item>Parameter is whether the change was itself inherited, i.e. if it happened in a direct parent (false) or a more removed ancestor (true). </item>
|
||||||
/// </list>
|
/// </list>
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public sealed class CollectionInheritanceChanged : EventWrapper<Action<ModCollection, bool>, CollectionInheritanceChanged.Priority>
|
public sealed class CollectionInheritanceChanged()
|
||||||
|
: EventWrapper<ModCollection, bool, CollectionInheritanceChanged.Priority>(nameof(CollectionInheritanceChanged))
|
||||||
{
|
{
|
||||||
public enum Priority
|
public enum Priority
|
||||||
{
|
{
|
||||||
|
|
@ -23,11 +24,4 @@ public sealed class CollectionInheritanceChanged : EventWrapper<Action<ModCollec
|
||||||
/// <seealso cref="UI.ModsTab.ModFileSystemSelector.OnInheritanceChange"/>
|
/// <seealso cref="UI.ModsTab.ModFileSystemSelector.OnInheritanceChange"/>
|
||||||
ModFileSystemSelector = 0,
|
ModFileSystemSelector = 0,
|
||||||
}
|
}
|
||||||
|
|
||||||
public CollectionInheritanceChanged()
|
|
||||||
: base(nameof(CollectionInheritanceChanged))
|
|
||||||
{ }
|
|
||||||
|
|
||||||
public void Invoke(ModCollection collection, bool inherited)
|
|
||||||
=> Invoke(this, collection, inherited);
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -9,18 +9,12 @@ namespace Penumbra.Communication;
|
||||||
/// <item>Parameter is the applied collection. </item>
|
/// <item>Parameter is the applied collection. </item>
|
||||||
/// <item>Parameter is the created draw object. </item>
|
/// <item>Parameter is the created draw object. </item>
|
||||||
/// </list> </summary>
|
/// </list> </summary>
|
||||||
public sealed class CreatedCharacterBase : EventWrapper<Action<nint, ModCollection, nint>, CreatedCharacterBase.Priority>
|
public sealed class CreatedCharacterBase()
|
||||||
|
: EventWrapper<nint, ModCollection, nint, CreatedCharacterBase.Priority>(nameof(CreatedCharacterBase))
|
||||||
{
|
{
|
||||||
public enum Priority
|
public enum Priority
|
||||||
{
|
{
|
||||||
/// <seealso cref="PenumbraApi.CreatedCharacterBase"/>
|
/// <seealso cref="PenumbraApi.CreatedCharacterBase"/>
|
||||||
Api = int.MinValue,
|
Api = int.MinValue,
|
||||||
}
|
}
|
||||||
|
|
||||||
public CreatedCharacterBase()
|
|
||||||
: base(nameof(CreatedCharacterBase))
|
|
||||||
{ }
|
|
||||||
|
|
||||||
public void Invoke(nint gameObject, ModCollection appliedCollection, nint drawObject)
|
|
||||||
=> Invoke(this, gameObject, appliedCollection, drawObject);
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -12,18 +12,12 @@ namespace Penumbra.Communication;
|
||||||
/// <item>Parameter is a pointer to the customize array. </item>
|
/// <item>Parameter is a pointer to the customize array. </item>
|
||||||
/// <item>Parameter is a pointer to the equip data array. </item>
|
/// <item>Parameter is a pointer to the equip data array. </item>
|
||||||
/// </list> </summary>
|
/// </list> </summary>
|
||||||
public sealed class CreatingCharacterBase : EventWrapper<Action<nint, string, nint, nint, nint>, CreatingCharacterBase.Priority>
|
public sealed class CreatingCharacterBase()
|
||||||
|
: EventWrapper<nint, string, nint, nint, nint, CreatingCharacterBase.Priority>(nameof(CreatingCharacterBase))
|
||||||
{
|
{
|
||||||
public enum Priority
|
public enum Priority
|
||||||
{
|
{
|
||||||
/// <seealso cref="PenumbraApi.CreatingCharacterBase"/>
|
/// <seealso cref="PenumbraApi.CreatingCharacterBase"/>
|
||||||
Api = 0,
|
Api = 0,
|
||||||
}
|
}
|
||||||
|
|
||||||
public CreatingCharacterBase()
|
|
||||||
: base(nameof(CreatingCharacterBase))
|
|
||||||
{ }
|
|
||||||
|
|
||||||
public void Invoke(nint gameObject, string appliedCollectionName, nint modelIdAddress, nint customizeArrayAddress, nint equipDataAddress)
|
|
||||||
=> Invoke(this, gameObject, appliedCollectionName, modelIdAddress, customizeArrayAddress, equipDataAddress);
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -9,7 +9,7 @@ namespace Penumbra.Communication;
|
||||||
/// <item>Parameter is whether Penumbra is now Enabled (true) or Disabled (false). </item>
|
/// <item>Parameter is whether Penumbra is now Enabled (true) or Disabled (false). </item>
|
||||||
/// </list>
|
/// </list>
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public sealed class EnabledChanged : EventWrapper<Action<bool>, EnabledChanged.Priority>
|
public sealed class EnabledChanged() : EventWrapper<bool, EnabledChanged.Priority>(nameof(EnabledChanged))
|
||||||
{
|
{
|
||||||
public enum Priority
|
public enum Priority
|
||||||
{
|
{
|
||||||
|
|
@ -19,11 +19,4 @@ public sealed class EnabledChanged : EventWrapper<Action<bool>, EnabledChanged.P
|
||||||
/// <seealso cref="Api.DalamudSubstitutionProvider.OnEnabledChange"/>
|
/// <seealso cref="Api.DalamudSubstitutionProvider.OnEnabledChange"/>
|
||||||
DalamudSubstitutionProvider = 0,
|
DalamudSubstitutionProvider = 0,
|
||||||
}
|
}
|
||||||
|
|
||||||
public EnabledChanged()
|
|
||||||
: base(nameof(EnabledChanged))
|
|
||||||
{ }
|
|
||||||
|
|
||||||
public void Invoke(bool enabled)
|
|
||||||
=> Invoke(this, enabled);
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -11,7 +11,7 @@ namespace Penumbra.Communication;
|
||||||
/// <item>Parameter is the changed mod. </item>
|
/// <item>Parameter is the changed mod. </item>
|
||||||
/// <item>Parameter is the old name of the mod in case of a name change, and null otherwise. </item>
|
/// <item>Parameter is the old name of the mod in case of a name change, and null otherwise. </item>
|
||||||
/// </list> </summary>
|
/// </list> </summary>
|
||||||
public sealed class ModDataChanged : EventWrapper<Action<ModDataChangeType, Mod, string?>, ModDataChanged.Priority>
|
public sealed class ModDataChanged() : EventWrapper<ModDataChangeType, Mod, string?, ModDataChanged.Priority>(nameof(ModDataChanged))
|
||||||
{
|
{
|
||||||
public enum Priority
|
public enum Priority
|
||||||
{
|
{
|
||||||
|
|
@ -27,11 +27,4 @@ public sealed class ModDataChanged : EventWrapper<Action<ModDataChangeType, Mod,
|
||||||
/// <seealso cref="UI.ModsTab.ModPanelHeader.OnModDataChange"/>
|
/// <seealso cref="UI.ModsTab.ModPanelHeader.OnModDataChange"/>
|
||||||
ModPanelHeader = 0,
|
ModPanelHeader = 0,
|
||||||
}
|
}
|
||||||
|
|
||||||
public ModDataChanged()
|
|
||||||
: base(nameof(ModDataChanged))
|
|
||||||
{ }
|
|
||||||
|
|
||||||
public void Invoke(ModDataChangeType changeType, Mod mod, string? oldName)
|
|
||||||
=> Invoke(this, changeType, mod, oldName);
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -10,7 +10,7 @@ namespace Penumbra.Communication;
|
||||||
/// <item>Parameter is whether the new directory is valid. </item>
|
/// <item>Parameter is whether the new directory is valid. </item>
|
||||||
/// </list>
|
/// </list>
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public sealed class ModDirectoryChanged : EventWrapper<Action<string, bool>, ModDirectoryChanged.Priority>
|
public sealed class ModDirectoryChanged() : EventWrapper<string, bool, ModDirectoryChanged.Priority>(nameof(ModDirectoryChanged))
|
||||||
{
|
{
|
||||||
public enum Priority
|
public enum Priority
|
||||||
{
|
{
|
||||||
|
|
@ -20,11 +20,4 @@ public sealed class ModDirectoryChanged : EventWrapper<Action<string, bool>, Mod
|
||||||
/// <seealso cref="UI.FileDialogService.OnModDirectoryChange"/>
|
/// <seealso cref="UI.FileDialogService.OnModDirectoryChange"/>
|
||||||
FileDialogService = 0,
|
FileDialogService = 0,
|
||||||
}
|
}
|
||||||
|
|
||||||
public ModDirectoryChanged()
|
|
||||||
: base(nameof(ModDirectoryChanged))
|
|
||||||
{ }
|
|
||||||
|
|
||||||
public void Invoke(string newModDirectory, bool newDirectoryValid)
|
|
||||||
=> Invoke(this, newModDirectory, newDirectoryValid);
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,9 @@
|
||||||
using OtterGui.Classes;
|
using OtterGui.Classes;
|
||||||
using Penumbra.Mods.Manager;
|
|
||||||
|
|
||||||
namespace Penumbra.Communication;
|
namespace Penumbra.Communication;
|
||||||
|
|
||||||
/// <summary> Triggered whenever a new mod discovery has finished. </summary>
|
/// <summary> Triggered whenever a new mod discovery has finished. </summary>
|
||||||
public sealed class ModDiscoveryFinished : EventWrapper<Action, ModDiscoveryFinished.Priority>
|
public sealed class ModDiscoveryFinished() : EventWrapper<ModDiscoveryFinished.Priority>(nameof(ModDiscoveryFinished))
|
||||||
{
|
{
|
||||||
public enum Priority
|
public enum Priority
|
||||||
{
|
{
|
||||||
|
|
@ -23,11 +22,4 @@ public sealed class ModDiscoveryFinished : EventWrapper<Action, ModDiscoveryFini
|
||||||
/// <seealso cref="Mods.Manager.ModFileSystem.Reload"/>
|
/// <seealso cref="Mods.Manager.ModFileSystem.Reload"/>
|
||||||
ModFileSystem = 0,
|
ModFileSystem = 0,
|
||||||
}
|
}
|
||||||
|
|
||||||
public ModDiscoveryFinished()
|
|
||||||
: base(nameof(ModDiscoveryFinished))
|
|
||||||
{ }
|
|
||||||
|
|
||||||
public void Invoke()
|
|
||||||
=> Invoke(this);
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,7 @@ using OtterGui.Classes;
|
||||||
namespace Penumbra.Communication;
|
namespace Penumbra.Communication;
|
||||||
|
|
||||||
/// <summary> Triggered whenever mods are prepared to be rediscovered. </summary>
|
/// <summary> Triggered whenever mods are prepared to be rediscovered. </summary>
|
||||||
public sealed class ModDiscoveryStarted : EventWrapper<Action, ModDiscoveryStarted.Priority>
|
public sealed class ModDiscoveryStarted() : EventWrapper<ModDiscoveryStarted.Priority>(nameof(ModDiscoveryStarted))
|
||||||
{
|
{
|
||||||
public enum Priority
|
public enum Priority
|
||||||
{
|
{
|
||||||
|
|
@ -16,11 +16,4 @@ public sealed class ModDiscoveryStarted : EventWrapper<Action, ModDiscoveryStart
|
||||||
/// <seealso cref="UI.ModsTab.ModFileSystemSelector.StoreCurrentSelection"/>
|
/// <seealso cref="UI.ModsTab.ModFileSystemSelector.StoreCurrentSelection"/>
|
||||||
ModFileSystemSelector = 200,
|
ModFileSystemSelector = 200,
|
||||||
}
|
}
|
||||||
|
|
||||||
public ModDiscoveryStarted()
|
|
||||||
: base(nameof(ModDiscoveryStarted))
|
|
||||||
{ }
|
|
||||||
|
|
||||||
public void Invoke()
|
|
||||||
=> Invoke(this);
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -13,7 +13,8 @@ namespace Penumbra.Communication;
|
||||||
/// <item>Parameter is the index of the changed option inside the group or -1 if it does not concern a specific option. </item>
|
/// <item>Parameter is the index of the changed option inside the group or -1 if it does not concern a specific option. </item>
|
||||||
/// <item>Parameter is the index of the group an option was moved to. </item>
|
/// <item>Parameter is the index of the group an option was moved to. </item>
|
||||||
/// </list> </summary>
|
/// </list> </summary>
|
||||||
public sealed class ModOptionChanged : EventWrapper<Action<ModOptionChangeType, Mod, int, int, int>, ModOptionChanged.Priority>
|
public sealed class ModOptionChanged()
|
||||||
|
: EventWrapper<ModOptionChangeType, Mod, int, int, int, ModOptionChanged.Priority>(nameof(ModOptionChanged))
|
||||||
{
|
{
|
||||||
public enum Priority
|
public enum Priority
|
||||||
{
|
{
|
||||||
|
|
@ -29,11 +30,4 @@ public sealed class ModOptionChanged : EventWrapper<Action<ModOptionChangeType,
|
||||||
/// <seealso cref="Collections.Manager.CollectionStorage.OnModOptionChange"/>
|
/// <seealso cref="Collections.Manager.CollectionStorage.OnModOptionChange"/>
|
||||||
CollectionStorage = 100,
|
CollectionStorage = 100,
|
||||||
}
|
}
|
||||||
|
|
||||||
public ModOptionChanged()
|
|
||||||
: base(nameof(ModOptionChanged))
|
|
||||||
{ }
|
|
||||||
|
|
||||||
public void Invoke(ModOptionChangeType changeType, Mod mod, int groupIndex, int optionIndex, int moveToIndex)
|
|
||||||
=> Invoke(this, changeType, mod, groupIndex, optionIndex, moveToIndex);
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -14,7 +14,8 @@ namespace Penumbra.Communication;
|
||||||
/// <item>Parameter is the new directory on addition, move or reload and null on deletion. </item>
|
/// <item>Parameter is the new directory on addition, move or reload and null on deletion. </item>
|
||||||
/// </list>
|
/// </list>
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public sealed class ModPathChanged : EventWrapper<Action<ModPathChangeType, Mod, DirectoryInfo?, DirectoryInfo?>, ModPathChanged.Priority>
|
public sealed class ModPathChanged()
|
||||||
|
: EventWrapper<ModPathChangeType, Mod, DirectoryInfo?, DirectoryInfo?, ModPathChanged.Priority>(nameof(ModPathChanged))
|
||||||
{
|
{
|
||||||
public enum Priority
|
public enum Priority
|
||||||
{
|
{
|
||||||
|
|
@ -48,11 +49,4 @@ public sealed class ModPathChanged : EventWrapper<Action<ModPathChangeType, Mod,
|
||||||
/// <seealso cref="Collections.Cache.CollectionCacheManager.OnModChangeRemoval"/>
|
/// <seealso cref="Collections.Cache.CollectionCacheManager.OnModChangeRemoval"/>
|
||||||
CollectionCacheManagerRemoval = 100,
|
CollectionCacheManagerRemoval = 100,
|
||||||
}
|
}
|
||||||
|
|
||||||
public ModPathChanged()
|
|
||||||
: base(nameof(ModPathChanged))
|
|
||||||
{ }
|
|
||||||
|
|
||||||
public void Invoke(ModPathChangeType changeType, Mod mod, DirectoryInfo? oldModDirectory, DirectoryInfo? newModDirectory)
|
|
||||||
=> Invoke(this, changeType, mod, oldModDirectory, newModDirectory);
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -17,7 +17,8 @@ namespace Penumbra.Communication;
|
||||||
/// <item>Parameter is whether the change was inherited from another collection. </item>
|
/// <item>Parameter is whether the change was inherited from another collection. </item>
|
||||||
/// </list>
|
/// </list>
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public sealed class ModSettingChanged : EventWrapper<Action<ModCollection, ModSettingChange, Mod?, int, int, bool>, ModSettingChanged.Priority>
|
public sealed class ModSettingChanged()
|
||||||
|
: EventWrapper<ModCollection, ModSettingChange, Mod?, int, int, bool, ModSettingChanged.Priority>(nameof(ModSettingChanged))
|
||||||
{
|
{
|
||||||
public enum Priority
|
public enum Priority
|
||||||
{
|
{
|
||||||
|
|
@ -33,11 +34,4 @@ public sealed class ModSettingChanged : EventWrapper<Action<ModCollection, ModSe
|
||||||
/// <seealso cref="UI.ModsTab.ModFileSystemSelector.OnSettingChange"/>
|
/// <seealso cref="UI.ModsTab.ModFileSystemSelector.OnSettingChange"/>
|
||||||
ModFileSystemSelector = 0,
|
ModFileSystemSelector = 0,
|
||||||
}
|
}
|
||||||
|
|
||||||
public ModSettingChanged()
|
|
||||||
: base(nameof(ModSettingChanged))
|
|
||||||
{ }
|
|
||||||
|
|
||||||
public void Invoke(ModCollection collection, ModSettingChange type, Mod? mod, int oldValue, int groupIdx, bool inherited)
|
|
||||||
=> Invoke(this, collection, type, mod, oldValue, groupIdx, inherited);
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -6,18 +6,11 @@ namespace Penumbra.Communication;
|
||||||
/// <item>Parameter is the material resource handle for which the shader package has been loaded. </item>
|
/// <item>Parameter is the material resource handle for which the shader package has been loaded. </item>
|
||||||
/// <item>Parameter is the associated game object. </item>
|
/// <item>Parameter is the associated game object. </item>
|
||||||
/// </list> </summary>
|
/// </list> </summary>
|
||||||
public sealed class MtrlShpkLoaded : EventWrapper<Action<nint, nint>, MtrlShpkLoaded.Priority>
|
public sealed class MtrlShpkLoaded() : EventWrapper<nint, nint, MtrlShpkLoaded.Priority>(nameof(MtrlShpkLoaded))
|
||||||
{
|
{
|
||||||
public enum Priority
|
public enum Priority
|
||||||
{
|
{
|
||||||
/// <seealso cref="Interop.Services.SkinFixer.OnMtrlShpkLoaded"/>
|
/// <seealso cref="Interop.Services.SkinFixer.OnMtrlShpkLoaded"/>
|
||||||
SkinFixer = 0,
|
SkinFixer = 0,
|
||||||
}
|
}
|
||||||
|
|
||||||
public MtrlShpkLoaded()
|
|
||||||
: base(nameof(MtrlShpkLoaded))
|
|
||||||
{ }
|
|
||||||
|
|
||||||
public void Invoke(nint mtrlResourceHandle, nint gameObject)
|
|
||||||
=> Invoke(this, mtrlResourceHandle, gameObject);
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -8,18 +8,11 @@ namespace Penumbra.Communication;
|
||||||
/// <item>Parameter is the identifier (directory name) of the currently selected mod. </item>
|
/// <item>Parameter is the identifier (directory name) of the currently selected mod. </item>
|
||||||
/// </list>
|
/// </list>
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public sealed class PostSettingsPanelDraw : EventWrapper<Action<string>, PostSettingsPanelDraw.Priority>
|
public sealed class PostSettingsPanelDraw() : EventWrapper<string, PostSettingsPanelDraw.Priority>(nameof(PostSettingsPanelDraw))
|
||||||
{
|
{
|
||||||
public enum Priority
|
public enum Priority
|
||||||
{
|
{
|
||||||
/// <seealso cref="Api.PenumbraApi.PostSettingsPanelDraw"/>
|
/// <seealso cref="Api.PenumbraApi.PostSettingsPanelDraw"/>
|
||||||
Default = 0,
|
Default = 0,
|
||||||
}
|
}
|
||||||
|
|
||||||
public PostSettingsPanelDraw()
|
|
||||||
: base(nameof(PostSettingsPanelDraw))
|
|
||||||
{ }
|
|
||||||
|
|
||||||
public void Invoke(string modDirectory)
|
|
||||||
=> Invoke(this, modDirectory);
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -8,18 +8,11 @@ namespace Penumbra.Communication;
|
||||||
/// <item>Parameter is the identifier (directory name) of the currently selected mod. </item>
|
/// <item>Parameter is the identifier (directory name) of the currently selected mod. </item>
|
||||||
/// </list>
|
/// </list>
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public sealed class PreSettingsPanelDraw : EventWrapper<Action<string>, PreSettingsPanelDraw.Priority>
|
public sealed class PreSettingsPanelDraw() : EventWrapper<string, PreSettingsPanelDraw.Priority>(nameof(PreSettingsPanelDraw))
|
||||||
{
|
{
|
||||||
public enum Priority
|
public enum Priority
|
||||||
{
|
{
|
||||||
/// <seealso cref="Api.PenumbraApi.PreSettingsPanelDraw"/>
|
/// <seealso cref="Api.PenumbraApi.PreSettingsPanelDraw"/>
|
||||||
Default = 0,
|
Default = 0,
|
||||||
}
|
}
|
||||||
|
|
||||||
public PreSettingsPanelDraw()
|
|
||||||
: base(nameof(PreSettingsPanelDraw))
|
|
||||||
{ }
|
|
||||||
|
|
||||||
public void Invoke(string modDirectory)
|
|
||||||
=> Invoke(this, modDirectory);
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -15,8 +15,9 @@ namespace Penumbra.Communication;
|
||||||
/// <item>Parameter is the old redirection path for Replaced, or empty. </item>
|
/// <item>Parameter is the old redirection path for Replaced, or empty. </item>
|
||||||
/// <item>Parameter is the mod responsible for the new redirection if any. </item>
|
/// <item>Parameter is the mod responsible for the new redirection if any. </item>
|
||||||
/// </list> </summary>
|
/// </list> </summary>
|
||||||
public sealed class ResolvedFileChanged : EventWrapper<Action<ModCollection, ResolvedFileChanged.Type, Utf8GamePath, FullPath, FullPath, IMod?>,
|
public sealed class ResolvedFileChanged()
|
||||||
ResolvedFileChanged.Priority>
|
: EventWrapper<ModCollection, ResolvedFileChanged.Type, Utf8GamePath, FullPath, FullPath, IMod?, ResolvedFileChanged.Priority>(
|
||||||
|
nameof(ResolvedFileChanged))
|
||||||
{
|
{
|
||||||
public enum Type
|
public enum Type
|
||||||
{
|
{
|
||||||
|
|
@ -29,14 +30,7 @@ public sealed class ResolvedFileChanged : EventWrapper<Action<ModCollection, Res
|
||||||
|
|
||||||
public enum Priority
|
public enum Priority
|
||||||
{
|
{
|
||||||
/// <seealso cref="Api.DalamudSubstitutionProvider.OnResolvedFileChanged"/>
|
/// <seealso cref="Api.DalamudSubstitutionProvider.OnResolvedFileChange"/>
|
||||||
DalamudSubstitutionProvider = 0,
|
DalamudSubstitutionProvider = 0,
|
||||||
}
|
}
|
||||||
|
|
||||||
public ResolvedFileChanged()
|
|
||||||
: base(nameof(ResolvedFileChanged))
|
|
||||||
{ }
|
|
||||||
|
|
||||||
public void Invoke(ModCollection collection, Type type, Utf8GamePath key, FullPath value, FullPath old, IMod? mod)
|
|
||||||
=> Invoke(this, collection, type, key, value, old, mod);
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -11,18 +11,11 @@ namespace Penumbra.Communication;
|
||||||
/// <item>Parameter is the selected mod, if any. </item>
|
/// <item>Parameter is the selected mod, if any. </item>
|
||||||
/// </list>
|
/// </list>
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public sealed class SelectTab : EventWrapper<Action<TabType, Mod?>, SelectTab.Priority>
|
public sealed class SelectTab() : EventWrapper<TabType, Mod?, SelectTab.Priority>(nameof(SelectTab))
|
||||||
{
|
{
|
||||||
public enum Priority
|
public enum Priority
|
||||||
{
|
{
|
||||||
/// <seealso cref="UI.Tabs.ConfigTabBar.OnSelectTab"/>
|
/// <seealso cref="UI.Tabs.ConfigTabBar.OnSelectTab"/>
|
||||||
ConfigTabBar = 0,
|
ConfigTabBar = 0,
|
||||||
}
|
}
|
||||||
|
|
||||||
public SelectTab()
|
|
||||||
: base(nameof(SelectTab))
|
|
||||||
{ }
|
|
||||||
|
|
||||||
public void Invoke(TabType tab = TabType.None, Mod? mod = null)
|
|
||||||
=> Invoke(this, tab, mod);
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -10,7 +10,8 @@ namespace Penumbra.Communication;
|
||||||
/// <item>Parameter is whether the mod was newly created.</item>
|
/// <item>Parameter is whether the mod was newly created.</item>
|
||||||
/// <item>Parameter is whether the mod was deleted.</item>
|
/// <item>Parameter is whether the mod was deleted.</item>
|
||||||
/// </list> </summary>
|
/// </list> </summary>
|
||||||
public sealed class TemporaryGlobalModChange : EventWrapper<Action<TemporaryMod, bool, bool>, TemporaryGlobalModChange.Priority>
|
public sealed class TemporaryGlobalModChange()
|
||||||
|
: EventWrapper<TemporaryMod, bool, bool, TemporaryGlobalModChange.Priority>(nameof(TemporaryGlobalModChange))
|
||||||
{
|
{
|
||||||
public enum Priority
|
public enum Priority
|
||||||
{
|
{
|
||||||
|
|
@ -20,11 +21,4 @@ public sealed class TemporaryGlobalModChange : EventWrapper<Action<TemporaryMod,
|
||||||
/// <seealso cref="Collections.Manager.TempCollectionManager.OnGlobalModChange"/>
|
/// <seealso cref="Collections.Manager.TempCollectionManager.OnGlobalModChange"/>
|
||||||
TempCollectionManager = 0,
|
TempCollectionManager = 0,
|
||||||
}
|
}
|
||||||
|
|
||||||
public TemporaryGlobalModChange()
|
|
||||||
: base(nameof(TemporaryGlobalModChange))
|
|
||||||
{ }
|
|
||||||
|
|
||||||
public void Invoke(TemporaryMod temporaryMod, bool newlyCreated, bool deleted)
|
|
||||||
=> Invoke(this, temporaryMod, newlyCreated, deleted);
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
24
Penumbra/Interop/CharacterBaseVTables.cs
Normal file
24
Penumbra/Interop/CharacterBaseVTables.cs
Normal file
|
|
@ -0,0 +1,24 @@
|
||||||
|
using Dalamud.Plugin.Services;
|
||||||
|
using Dalamud.Utility.Signatures;
|
||||||
|
using OtterGui.Services;
|
||||||
|
using Penumbra.GameData;
|
||||||
|
|
||||||
|
namespace Penumbra.Interop;
|
||||||
|
|
||||||
|
public sealed unsafe class CharacterBaseVTables : IService
|
||||||
|
{
|
||||||
|
[Signature(Sigs.HumanVTable, ScanType = ScanType.StaticAddress)]
|
||||||
|
public readonly nint* HumanVTable = null!;
|
||||||
|
|
||||||
|
[Signature(Sigs.WeaponVTable, ScanType = ScanType.StaticAddress)]
|
||||||
|
public readonly nint* WeaponVTable = null!;
|
||||||
|
|
||||||
|
[Signature(Sigs.DemiHumanVTable, ScanType = ScanType.StaticAddress)]
|
||||||
|
public readonly nint* DemiHumanVTable = null!;
|
||||||
|
|
||||||
|
[Signature(Sigs.MonsterVTable, ScanType = ScanType.StaticAddress)]
|
||||||
|
public readonly nint* MonsterVTable = null!;
|
||||||
|
|
||||||
|
public CharacterBaseVTables(IGameInteropProvider interop)
|
||||||
|
=> interop.InitializeFromAttributes(this);
|
||||||
|
}
|
||||||
117
Penumbra/Interop/GameState.cs
Normal file
117
Penumbra/Interop/GameState.cs
Normal file
|
|
@ -0,0 +1,117 @@
|
||||||
|
using FFXIVClientStructs.FFXIV.Client.Game.Object;
|
||||||
|
using OtterGui.Services;
|
||||||
|
using Penumbra.Api.Enums;
|
||||||
|
using Penumbra.Collections;
|
||||||
|
using Penumbra.Interop.PathResolving;
|
||||||
|
using Penumbra.String.Classes;
|
||||||
|
|
||||||
|
namespace Penumbra.Interop;
|
||||||
|
|
||||||
|
public class GameState : IService
|
||||||
|
{
|
||||||
|
#region Last Game Object
|
||||||
|
|
||||||
|
private readonly ThreadLocal<Queue<nint>> _lastGameObject = new(() => new Queue<nint>());
|
||||||
|
|
||||||
|
public nint LastGameObject
|
||||||
|
=> _lastGameObject.IsValueCreated && _lastGameObject.Value!.Count > 0 ? _lastGameObject.Value.Peek() : nint.Zero;
|
||||||
|
|
||||||
|
[MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)]
|
||||||
|
public unsafe void QueueGameObject(GameObject* gameObject)
|
||||||
|
=> QueueGameObject((nint)gameObject);
|
||||||
|
|
||||||
|
[MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)]
|
||||||
|
public void QueueGameObject(nint gameObject)
|
||||||
|
=> _lastGameObject.Value!.Enqueue(gameObject);
|
||||||
|
|
||||||
|
[MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)]
|
||||||
|
public void DequeueGameObject()
|
||||||
|
=> _lastGameObject.Value!.TryDequeue(out _);
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#region Animation Data
|
||||||
|
|
||||||
|
private readonly ThreadLocal<ResolveData> _animationLoadData = new(() => ResolveData.Invalid, true);
|
||||||
|
|
||||||
|
public ResolveData AnimationData
|
||||||
|
=> _animationLoadData.Value;
|
||||||
|
|
||||||
|
[MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)]
|
||||||
|
public ResolveData SetAnimationData(ResolveData data)
|
||||||
|
{
|
||||||
|
var old = _animationLoadData.Value;
|
||||||
|
_animationLoadData.Value = data;
|
||||||
|
return old;
|
||||||
|
}
|
||||||
|
|
||||||
|
[MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)]
|
||||||
|
public void RestoreAnimationData(ResolveData old)
|
||||||
|
=> _animationLoadData.Value = old;
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#region Sound Data
|
||||||
|
|
||||||
|
private readonly ThreadLocal<ResolveData> _characterSoundData = new(() => ResolveData.Invalid, true);
|
||||||
|
|
||||||
|
public ResolveData SoundData
|
||||||
|
=> _animationLoadData.Value;
|
||||||
|
|
||||||
|
[MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)]
|
||||||
|
public ResolveData SetSoundData(ResolveData data)
|
||||||
|
{
|
||||||
|
var old = _characterSoundData.Value;
|
||||||
|
_characterSoundData.Value = data;
|
||||||
|
return old;
|
||||||
|
}
|
||||||
|
|
||||||
|
[MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)]
|
||||||
|
public void RestoreSoundData(ResolveData old)
|
||||||
|
=> _characterSoundData.Value = old;
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
/// <summary> Return the correct resolve data from the stored data. </summary>
|
||||||
|
public unsafe bool HandleFiles(CollectionResolver resolver, ResourceType type, Utf8GamePath _, out ResolveData resolveData)
|
||||||
|
{
|
||||||
|
switch (type)
|
||||||
|
{
|
||||||
|
case ResourceType.Scd:
|
||||||
|
if (_characterSoundData is { IsValueCreated: true, Value.Valid: true })
|
||||||
|
{
|
||||||
|
resolveData = _characterSoundData.Value;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (_animationLoadData is { IsValueCreated: true, Value.Valid: true })
|
||||||
|
{
|
||||||
|
resolveData = _animationLoadData.Value;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
break;
|
||||||
|
case ResourceType.Tmb:
|
||||||
|
case ResourceType.Pap:
|
||||||
|
case ResourceType.Avfx:
|
||||||
|
case ResourceType.Atex:
|
||||||
|
if (_animationLoadData is { IsValueCreated: true, Value.Valid: true })
|
||||||
|
{
|
||||||
|
resolveData = _animationLoadData.Value;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
var lastObj = LastGameObject;
|
||||||
|
if (lastObj != nint.Zero)
|
||||||
|
{
|
||||||
|
resolveData = resolver.IdentifyCollection((GameObject*)lastObj, true);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
resolveData = ResolveData.Invalid;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
54
Penumbra/Interop/Hooks/Animation/ApricotListenerSoundPlay.cs
Normal file
54
Penumbra/Interop/Hooks/Animation/ApricotListenerSoundPlay.cs
Normal file
|
|
@ -0,0 +1,54 @@
|
||||||
|
using FFXIVClientStructs.FFXIV.Client.Game.Object;
|
||||||
|
using FFXIVClientStructs.FFXIV.Client.Graphics.Scene;
|
||||||
|
using OtterGui.Services;
|
||||||
|
using Penumbra.Collections;
|
||||||
|
using Penumbra.GameData;
|
||||||
|
using Penumbra.Interop.PathResolving;
|
||||||
|
|
||||||
|
namespace Penumbra.Interop.Hooks.Animation;
|
||||||
|
|
||||||
|
/// <summary> Called for some sound effects caused by animations or VFX. </summary>
|
||||||
|
public sealed unsafe class ApricotListenerSoundPlay : FastHook<ApricotListenerSoundPlay.Delegate>
|
||||||
|
{
|
||||||
|
private readonly GameState _state;
|
||||||
|
private readonly CollectionResolver _collectionResolver;
|
||||||
|
|
||||||
|
public ApricotListenerSoundPlay(HookManager hooks, GameState state, CollectionResolver collectionResolver)
|
||||||
|
{
|
||||||
|
_state = state;
|
||||||
|
_collectionResolver = collectionResolver;
|
||||||
|
Task = hooks.CreateHook<Delegate>("Apricot Listener Sound Play", Sigs.ApricotListenerSoundPlay, Detour, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
public delegate nint Delegate(nint a1, nint a2, nint a3, nint a4, nint a5, nint a6);
|
||||||
|
|
||||||
|
[MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)]
|
||||||
|
private nint Detour(nint a1, nint a2, nint a3, nint a4, nint a5, nint a6)
|
||||||
|
{
|
||||||
|
Penumbra.Log.Excessive($"[Apricot Listener Sound Play] Invoked on 0x{a1:X} with {a2}, {a3}, {a4}, {a5}, {a6}.");
|
||||||
|
if (a6 == nint.Zero)
|
||||||
|
return Task.Result.Original(a1, a2, a3, a4, a5, a6);
|
||||||
|
|
||||||
|
// a6 is some instance of Apricot.IInstanceListenner, in some cases we can obtain the associated caster via vfunc 1.
|
||||||
|
var gameObject = (*(delegate* unmanaged<nint, GameObject*>**)a6)[1](a6);
|
||||||
|
var newData = ResolveData.Invalid;
|
||||||
|
if (gameObject != null)
|
||||||
|
{
|
||||||
|
newData = _collectionResolver.IdentifyCollection(gameObject, true);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// for VfxListenner we can obtain the associated draw object as its first member,
|
||||||
|
// if the object has different type, drawObject will contain other values or garbage,
|
||||||
|
// but only be used in a dictionary pointer lookup, so this does not hurt.
|
||||||
|
var drawObject = ((DrawObject**)a6)[1];
|
||||||
|
if (drawObject != null)
|
||||||
|
newData = _collectionResolver.IdentifyCollection(drawObject, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
var last = _state.SetAnimationData(newData);
|
||||||
|
var ret = Task.Result.Original(a1, a2, a3, a4, a5, a6);
|
||||||
|
_state.RestoreAnimationData(last);
|
||||||
|
return ret;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,41 @@
|
||||||
|
using FFXIVClientStructs.FFXIV.Client.Game.Object;
|
||||||
|
using FFXIVClientStructs.FFXIV.Client.Graphics.Scene;
|
||||||
|
using OtterGui.Services;
|
||||||
|
using Penumbra.GameData;
|
||||||
|
using Penumbra.Interop.PathResolving;
|
||||||
|
|
||||||
|
namespace Penumbra.Interop.Hooks.Animation;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Probably used when the base idle animation gets loaded.
|
||||||
|
/// Make it aware of the correct collection to load the correct pap files.
|
||||||
|
/// </summary>
|
||||||
|
public sealed unsafe class CharacterBaseLoadAnimation : FastHook<CharacterBaseLoadAnimation.Delegate>
|
||||||
|
{
|
||||||
|
private readonly GameState _state;
|
||||||
|
private readonly CollectionResolver _collectionResolver;
|
||||||
|
private readonly DrawObjectState _drawObjectState;
|
||||||
|
|
||||||
|
public CharacterBaseLoadAnimation(HookManager hooks, GameState state, CollectionResolver collectionResolver,
|
||||||
|
DrawObjectState drawObjectState)
|
||||||
|
{
|
||||||
|
_state = state;
|
||||||
|
_collectionResolver = collectionResolver;
|
||||||
|
_drawObjectState = drawObjectState;
|
||||||
|
Task = hooks.CreateHook<Delegate>("CharacterBase Load Animation", Sigs.CharacterBaseLoadAnimation, Detour, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
public delegate void Delegate(DrawObject* drawBase);
|
||||||
|
|
||||||
|
[MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)]
|
||||||
|
private void Detour(DrawObject* drawObject)
|
||||||
|
{
|
||||||
|
var lastObj = _state.LastGameObject;
|
||||||
|
if (lastObj == nint.Zero && _drawObjectState.TryGetValue((nint)drawObject, out var p))
|
||||||
|
lastObj = p.Item1;
|
||||||
|
var last = _state.SetAnimationData(_collectionResolver.IdentifyCollection((GameObject*)lastObj, true));
|
||||||
|
Penumbra.Log.Excessive($"[CharacterBase Load Animation] Invoked on {(nint)drawObject:X}");
|
||||||
|
Task.Result.Original(drawObject);
|
||||||
|
_state.RestoreAnimationData(last);
|
||||||
|
}
|
||||||
|
}
|
||||||
44
Penumbra/Interop/Hooks/Animation/Dismount.cs
Normal file
44
Penumbra/Interop/Hooks/Animation/Dismount.cs
Normal file
|
|
@ -0,0 +1,44 @@
|
||||||
|
using FFXIVClientStructs.FFXIV.Client.Game.Object;
|
||||||
|
using OtterGui.Services;
|
||||||
|
using Penumbra.GameData;
|
||||||
|
using Penumbra.Interop.PathResolving;
|
||||||
|
|
||||||
|
namespace Penumbra.Interop.Hooks.Animation;
|
||||||
|
|
||||||
|
/// <summary> Called for some animations when dismounting. </summary>
|
||||||
|
public sealed unsafe class Dismount : FastHook<Dismount.Delegate>
|
||||||
|
{
|
||||||
|
private readonly GameState _state;
|
||||||
|
private readonly CollectionResolver _collectionResolver;
|
||||||
|
|
||||||
|
public Dismount(HookManager hooks, GameState state, CollectionResolver collectionResolver)
|
||||||
|
{
|
||||||
|
_state = state;
|
||||||
|
_collectionResolver = collectionResolver;
|
||||||
|
Task = hooks.CreateHook<Delegate>("Dismount", Sigs.Dismount, Detour, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
public delegate void Delegate(nint a1, nint a2);
|
||||||
|
|
||||||
|
[MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)]
|
||||||
|
private void Detour(nint a1, nint a2)
|
||||||
|
{
|
||||||
|
Penumbra.Log.Excessive($"[Dismount] Invoked on {a1:X} with {a2:X}.");
|
||||||
|
if (a1 == nint.Zero)
|
||||||
|
{
|
||||||
|
Task.Result.Original(a1, a2);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var gameObject = *(GameObject**)(a1 + 8);
|
||||||
|
if (gameObject == null)
|
||||||
|
{
|
||||||
|
Task.Result.Original(a1, a2);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var last = _state.SetAnimationData(_collectionResolver.IdentifyCollection(gameObject, true));
|
||||||
|
Task.Result.Original(a1, a2);
|
||||||
|
_state.RestoreAnimationData(last);
|
||||||
|
}
|
||||||
|
}
|
||||||
38
Penumbra/Interop/Hooks/Animation/LoadAreaVfx.cs
Normal file
38
Penumbra/Interop/Hooks/Animation/LoadAreaVfx.cs
Normal file
|
|
@ -0,0 +1,38 @@
|
||||||
|
using FFXIVClientStructs.FFXIV.Client.Game.Object;
|
||||||
|
using OtterGui.Services;
|
||||||
|
using Penumbra.Collections;
|
||||||
|
using Penumbra.GameData;
|
||||||
|
using Penumbra.Interop.PathResolving;
|
||||||
|
|
||||||
|
namespace Penumbra.Interop.Hooks.Animation;
|
||||||
|
|
||||||
|
/// <summary> Load a ground-based area VFX. </summary>
|
||||||
|
public sealed unsafe class LoadAreaVfx : FastHook<LoadAreaVfx.Delegate>
|
||||||
|
{
|
||||||
|
private readonly GameState _state;
|
||||||
|
private readonly CollectionResolver _collectionResolver;
|
||||||
|
|
||||||
|
public LoadAreaVfx(HookManager hooks, GameState state, CollectionResolver collectionResolver)
|
||||||
|
{
|
||||||
|
_state = state;
|
||||||
|
_collectionResolver = collectionResolver;
|
||||||
|
Task = hooks.CreateHook<Delegate>("Load Area VFX", Sigs.LoadAreaVfx, Detour, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
public delegate nint Delegate(uint vfxId, float* pos, GameObject* caster, float unk1, float unk2, byte unk3);
|
||||||
|
|
||||||
|
[MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)]
|
||||||
|
private nint Detour(uint vfxId, float* pos, GameObject* caster, float unk1, float unk2, byte unk3)
|
||||||
|
{
|
||||||
|
var newData = caster != null
|
||||||
|
? _collectionResolver.IdentifyCollection(caster, true)
|
||||||
|
: ResolveData.Invalid;
|
||||||
|
|
||||||
|
var last = _state.SetAnimationData(newData);
|
||||||
|
var ret = Task.Result.Original(vfxId, pos, caster, unk1, unk2, unk3);
|
||||||
|
Penumbra.Log.Excessive(
|
||||||
|
$"[Load Area VFX] Invoked with {vfxId}, [{pos[0]} {pos[1]} {pos[2]}], 0x{(nint)caster:X}, {unk1}, {unk2}, {unk3} -> 0x{ret:X}.");
|
||||||
|
_state.RestoreAnimationData(last);
|
||||||
|
return ret;
|
||||||
|
}
|
||||||
|
}
|
||||||
34
Penumbra/Interop/Hooks/Animation/LoadCharacterSound.cs
Normal file
34
Penumbra/Interop/Hooks/Animation/LoadCharacterSound.cs
Normal file
|
|
@ -0,0 +1,34 @@
|
||||||
|
using FFXIVClientStructs.FFXIV.Client.Game.Object;
|
||||||
|
using OtterGui.Services;
|
||||||
|
using Penumbra.Interop.PathResolving;
|
||||||
|
|
||||||
|
namespace Penumbra.Interop.Hooks.Animation;
|
||||||
|
|
||||||
|
/// <summary> Characters load some of their voice lines or whatever with this function. </summary>
|
||||||
|
public sealed unsafe class LoadCharacterSound : FastHook<LoadCharacterSound.Delegate>
|
||||||
|
{
|
||||||
|
private readonly GameState _state;
|
||||||
|
private readonly CollectionResolver _collectionResolver;
|
||||||
|
|
||||||
|
public LoadCharacterSound(HookManager hooks, GameState state, CollectionResolver collectionResolver)
|
||||||
|
{
|
||||||
|
_state = state;
|
||||||
|
_collectionResolver = collectionResolver;
|
||||||
|
Task = hooks.CreateHook<Delegate>("Load Character Sound",
|
||||||
|
(nint)FFXIVClientStructs.FFXIV.Client.Game.Character.Character.VfxContainer.MemberFunctionPointers.LoadCharacterSound, Detour,
|
||||||
|
true);
|
||||||
|
}
|
||||||
|
|
||||||
|
public delegate nint Delegate(nint container, int unk1, int unk2, nint unk3, ulong unk4, int unk5, int unk6, ulong unk7);
|
||||||
|
|
||||||
|
[MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)]
|
||||||
|
private nint Detour(nint container, int unk1, int unk2, nint unk3, ulong unk4, int unk5, int unk6, ulong unk7)
|
||||||
|
{
|
||||||
|
var character = *(GameObject**)(container + 8);
|
||||||
|
var last = _state.SetSoundData(_collectionResolver.IdentifyCollection(character, true));
|
||||||
|
var ret = Task.Result.Original(container, unk1, unk2, unk3, unk4, unk5, unk6, unk7);
|
||||||
|
Penumbra.Log.Excessive($"[Load Character Sound] Invoked with {container:X} {unk1} {unk2} {unk3} {unk4} {unk5} {unk6} {unk7} -> {ret}.");
|
||||||
|
_state.RestoreSoundData(last);
|
||||||
|
return ret;
|
||||||
|
}
|
||||||
|
}
|
||||||
65
Penumbra/Interop/Hooks/Animation/LoadCharacterVfx.cs
Normal file
65
Penumbra/Interop/Hooks/Animation/LoadCharacterVfx.cs
Normal file
|
|
@ -0,0 +1,65 @@
|
||||||
|
using Dalamud.Plugin.Services;
|
||||||
|
using FFXIVClientStructs.FFXIV.Client.Game.Object;
|
||||||
|
using OtterGui.Services;
|
||||||
|
using Penumbra.Collections;
|
||||||
|
using Penumbra.GameData;
|
||||||
|
using Penumbra.Interop.PathResolving;
|
||||||
|
using Penumbra.Interop.Structs;
|
||||||
|
using Penumbra.String;
|
||||||
|
|
||||||
|
namespace Penumbra.Interop.Hooks.Animation;
|
||||||
|
|
||||||
|
/// <summary> Load a VFX specifically for a character. </summary>
|
||||||
|
public sealed unsafe class LoadCharacterVfx : FastHook<LoadCharacterVfx.Delegate>
|
||||||
|
{
|
||||||
|
private readonly GameState _state;
|
||||||
|
private readonly CollectionResolver _collectionResolver;
|
||||||
|
private readonly IObjectTable _objects;
|
||||||
|
|
||||||
|
public LoadCharacterVfx(HookManager hooks, GameState state, CollectionResolver collectionResolver, IObjectTable objects)
|
||||||
|
{
|
||||||
|
_state = state;
|
||||||
|
_collectionResolver = collectionResolver;
|
||||||
|
_objects = objects;
|
||||||
|
Task = hooks.CreateHook<Delegate>("Load Character VFX", Sigs.LoadCharacterVfx, Detour, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
public delegate nint Delegate(byte* vfxPath, VfxParams* vfxParams, byte unk1, byte unk2, float unk3, int unk4);
|
||||||
|
|
||||||
|
[MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)]
|
||||||
|
private nint Detour(byte* vfxPath, VfxParams* vfxParams, byte unk1, byte unk2, float unk3, int unk4)
|
||||||
|
{
|
||||||
|
var newData = ResolveData.Invalid;
|
||||||
|
if (vfxParams != null && vfxParams->GameObjectId != unchecked((uint)-1))
|
||||||
|
{
|
||||||
|
var obj = vfxParams->GameObjectType switch
|
||||||
|
{
|
||||||
|
0 => _objects.SearchById(vfxParams->GameObjectId),
|
||||||
|
2 => _objects[(int)vfxParams->GameObjectId],
|
||||||
|
4 => GetOwnedObject(vfxParams->GameObjectId),
|
||||||
|
_ => null,
|
||||||
|
};
|
||||||
|
newData = obj != null
|
||||||
|
? _collectionResolver.IdentifyCollection((GameObject*)obj.Address, true)
|
||||||
|
: ResolveData.Invalid;
|
||||||
|
}
|
||||||
|
|
||||||
|
var last = _state.SetAnimationData(newData);
|
||||||
|
var ret = Task.Result.Original(vfxPath, vfxParams, unk1, unk2, unk3, unk4);
|
||||||
|
Penumbra.Log.Excessive(
|
||||||
|
$"[Load Character VFX] Invoked with {new ByteString(vfxPath)}, 0x{vfxParams->GameObjectId:X}, {vfxParams->TargetCount}, {unk1}, {unk2}, {unk3}, {unk4} -> 0x{ret:X}.");
|
||||||
|
_state.RestoreAnimationData(last);
|
||||||
|
return ret;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary> Search an object by its id, then get its minion/mount/ornament. </summary>
|
||||||
|
private Dalamud.Game.ClientState.Objects.Types.GameObject? GetOwnedObject(uint id)
|
||||||
|
{
|
||||||
|
var owner = _objects.SearchById(id);
|
||||||
|
if (owner == null)
|
||||||
|
return null;
|
||||||
|
|
||||||
|
var idx = ((GameObject*)owner.Address)->ObjectIndex;
|
||||||
|
return _objects[idx + 1];
|
||||||
|
}
|
||||||
|
}
|
||||||
71
Penumbra/Interop/Hooks/Animation/LoadTimelineResources.cs
Normal file
71
Penumbra/Interop/Hooks/Animation/LoadTimelineResources.cs
Normal file
|
|
@ -0,0 +1,71 @@
|
||||||
|
using Dalamud.Game.ClientState.Conditions;
|
||||||
|
using Dalamud.Plugin.Services;
|
||||||
|
using FFXIVClientStructs.FFXIV.Client.Game.Object;
|
||||||
|
using OtterGui.Services;
|
||||||
|
using Penumbra.Collections;
|
||||||
|
using Penumbra.GameData;
|
||||||
|
using Penumbra.Interop.PathResolving;
|
||||||
|
|
||||||
|
namespace Penumbra.Interop.Hooks.Animation;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The timeline object loads the requested .tmb and .pap files. The .tmb files load the respective .avfx files.
|
||||||
|
/// We can obtain the associated game object from the timelines 28'th vfunc and use that to apply the correct collection.
|
||||||
|
/// </summary>
|
||||||
|
public sealed unsafe class LoadTimelineResources : FastHook<LoadTimelineResources.Delegate>
|
||||||
|
{
|
||||||
|
private readonly GameState _state;
|
||||||
|
private readonly CollectionResolver _collectionResolver;
|
||||||
|
private readonly ICondition _conditions;
|
||||||
|
private readonly IObjectTable _objects;
|
||||||
|
|
||||||
|
public LoadTimelineResources(HookManager hooks, GameState state, CollectionResolver collectionResolver, ICondition conditions,
|
||||||
|
IObjectTable objects)
|
||||||
|
{
|
||||||
|
_state = state;
|
||||||
|
_collectionResolver = collectionResolver;
|
||||||
|
_conditions = conditions;
|
||||||
|
_objects = objects;
|
||||||
|
Task = hooks.CreateHook<Delegate>("Load Timeline Resources", Sigs.LoadTimelineResources, Detour, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
public delegate ulong Delegate(nint timeline);
|
||||||
|
|
||||||
|
[MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)]
|
||||||
|
private ulong Detour(nint timeline)
|
||||||
|
{
|
||||||
|
Penumbra.Log.Excessive($"[Load Timeline Resources] Invoked on {timeline:X}.");
|
||||||
|
// Do not check timeline loading in cutscenes.
|
||||||
|
if (_conditions[ConditionFlag.OccupiedInCutSceneEvent] || _conditions[ConditionFlag.WatchingCutscene78])
|
||||||
|
return Task.Result.Original(timeline);
|
||||||
|
|
||||||
|
var last = _state.SetAnimationData(GetDataFromTimeline(_objects, _collectionResolver, timeline));
|
||||||
|
var ret = Task.Result.Original(timeline);
|
||||||
|
_state.RestoreAnimationData(last);
|
||||||
|
return ret;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary> Use timelines vfuncs to obtain the associated game object. </summary>
|
||||||
|
public static ResolveData GetDataFromTimeline(IObjectTable objects, CollectionResolver resolver, nint timeline)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (timeline != nint.Zero)
|
||||||
|
{
|
||||||
|
var getGameObjectIdx = ((delegate* unmanaged<nint, int>**)timeline)[0][Offsets.GetGameObjectIdxVfunc];
|
||||||
|
var idx = getGameObjectIdx(timeline);
|
||||||
|
if (idx >= 0 && idx < objects.Length)
|
||||||
|
{
|
||||||
|
var obj = (GameObject*)objects.GetObjectAddress(idx);
|
||||||
|
return obj != null ? resolver.IdentifyCollection(obj, true) : ResolveData.Invalid;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception e)
|
||||||
|
{
|
||||||
|
Penumbra.Log.Error($"Error getting timeline data for 0x{timeline:X}:\n{e}");
|
||||||
|
}
|
||||||
|
|
||||||
|
return ResolveData.Invalid;
|
||||||
|
}
|
||||||
|
}
|
||||||
30
Penumbra/Interop/Hooks/Animation/PlayFootstep.cs
Normal file
30
Penumbra/Interop/Hooks/Animation/PlayFootstep.cs
Normal file
|
|
@ -0,0 +1,30 @@
|
||||||
|
using FFXIVClientStructs.FFXIV.Client.Game.Object;
|
||||||
|
using OtterGui.Services;
|
||||||
|
using Penumbra.GameData;
|
||||||
|
using Penumbra.Interop.PathResolving;
|
||||||
|
|
||||||
|
namespace Penumbra.Interop.Hooks.Animation;
|
||||||
|
|
||||||
|
public sealed unsafe class PlayFootstep : FastHook<PlayFootstep.Delegate>
|
||||||
|
{
|
||||||
|
private readonly GameState _state;
|
||||||
|
private readonly CollectionResolver _collectionResolver;
|
||||||
|
|
||||||
|
public PlayFootstep(HookManager hooks, GameState state, CollectionResolver collectionResolver)
|
||||||
|
{
|
||||||
|
_state = state;
|
||||||
|
_collectionResolver = collectionResolver;
|
||||||
|
Task = hooks.CreateHook<Delegate>("Play Footstep", Sigs.FootStepSound, Detour, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
public delegate void Delegate(GameObject* gameObject, int id, int unk);
|
||||||
|
|
||||||
|
[MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)]
|
||||||
|
private void Detour(GameObject* gameObject, int id, int unk)
|
||||||
|
{
|
||||||
|
Penumbra.Log.Excessive($"[Play Footstep] Invoked on 0x{(nint)gameObject:X} with {id}, {unk}.");
|
||||||
|
var last = _state.SetAnimationData(_collectionResolver.IdentifyCollection(gameObject, true));
|
||||||
|
Task.Result.Original(gameObject, id, unk);
|
||||||
|
_state.RestoreAnimationData(last);
|
||||||
|
}
|
||||||
|
}
|
||||||
35
Penumbra/Interop/Hooks/Animation/ScheduleClipUpdate.cs
Normal file
35
Penumbra/Interop/Hooks/Animation/ScheduleClipUpdate.cs
Normal file
|
|
@ -0,0 +1,35 @@
|
||||||
|
using Dalamud.Plugin.Services;
|
||||||
|
using OtterGui.Services;
|
||||||
|
using Penumbra.GameData;
|
||||||
|
using Penumbra.Interop.PathResolving;
|
||||||
|
using Penumbra.Interop.Structs;
|
||||||
|
|
||||||
|
namespace Penumbra.Interop.Hooks.Animation;
|
||||||
|
|
||||||
|
/// <summary> Called when some action timelines update. </summary>
|
||||||
|
public sealed unsafe class ScheduleClipUpdate : FastHook<ScheduleClipUpdate.Delegate>
|
||||||
|
{
|
||||||
|
private readonly GameState _state;
|
||||||
|
private readonly CollectionResolver _collectionResolver;
|
||||||
|
private readonly IObjectTable _objects;
|
||||||
|
|
||||||
|
public ScheduleClipUpdate(HookManager hooks, GameState state, CollectionResolver collectionResolver, IObjectTable objects)
|
||||||
|
{
|
||||||
|
_state = state;
|
||||||
|
_collectionResolver = collectionResolver;
|
||||||
|
_objects = objects;
|
||||||
|
Task = hooks.CreateHook<Delegate>("Schedule Clip Update", Sigs.ScheduleClipUpdate, Detour, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
public delegate void Delegate(ClipScheduler* x);
|
||||||
|
|
||||||
|
[MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)]
|
||||||
|
private void Detour(ClipScheduler* clipScheduler)
|
||||||
|
{
|
||||||
|
Penumbra.Log.Excessive($"[Schedule Clip Update] Invoked on {(nint)clipScheduler:X}.");
|
||||||
|
var last = _state.SetAnimationData(
|
||||||
|
LoadTimelineResources.GetDataFromTimeline(_objects, _collectionResolver, clipScheduler->SchedulerTimeline));
|
||||||
|
Task.Result.Original(clipScheduler);
|
||||||
|
_state.RestoreAnimationData(last);
|
||||||
|
}
|
||||||
|
}
|
||||||
32
Penumbra/Interop/Hooks/Animation/SomeActionLoad.cs
Normal file
32
Penumbra/Interop/Hooks/Animation/SomeActionLoad.cs
Normal file
|
|
@ -0,0 +1,32 @@
|
||||||
|
using FFXIVClientStructs.FFXIV.Client.Game;
|
||||||
|
using FFXIVClientStructs.FFXIV.Client.Game.Object;
|
||||||
|
using OtterGui.Services;
|
||||||
|
using Penumbra.GameData;
|
||||||
|
using Penumbra.Interop.PathResolving;
|
||||||
|
|
||||||
|
namespace Penumbra.Interop.Hooks.Animation;
|
||||||
|
|
||||||
|
/// <summary> Seems to load character actions when zoning or changing class, maybe. </summary>
|
||||||
|
public sealed unsafe class SomeActionLoad : FastHook<SomeActionLoad.Delegate>
|
||||||
|
{
|
||||||
|
private readonly GameState _state;
|
||||||
|
private readonly CollectionResolver _collectionResolver;
|
||||||
|
|
||||||
|
public SomeActionLoad(HookManager hooks, GameState state, CollectionResolver collectionResolver)
|
||||||
|
{
|
||||||
|
_state = state;
|
||||||
|
_collectionResolver = collectionResolver;
|
||||||
|
Task = hooks.CreateHook<Delegate>("Some Action Load", Sigs.LoadSomeAction, Detour, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
public delegate void Delegate(ActionTimelineManager* timelineManager);
|
||||||
|
|
||||||
|
[MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)]
|
||||||
|
private void Detour(ActionTimelineManager* timelineManager)
|
||||||
|
{
|
||||||
|
var last = _state.SetAnimationData(_collectionResolver.IdentifyCollection((GameObject*)timelineManager->Parent, true));
|
||||||
|
Penumbra.Log.Excessive($"[Some Action Load] Invoked on 0x{(nint)timelineManager:X}.");
|
||||||
|
Task.Result.Original(timelineManager);
|
||||||
|
_state.RestoreAnimationData(last);
|
||||||
|
}
|
||||||
|
}
|
||||||
31
Penumbra/Interop/Hooks/Animation/SomeMountAnimation.cs
Normal file
31
Penumbra/Interop/Hooks/Animation/SomeMountAnimation.cs
Normal file
|
|
@ -0,0 +1,31 @@
|
||||||
|
using FFXIVClientStructs.FFXIV.Client.Graphics.Scene;
|
||||||
|
using OtterGui.Services;
|
||||||
|
using Penumbra.GameData;
|
||||||
|
using Penumbra.Interop.PathResolving;
|
||||||
|
|
||||||
|
namespace Penumbra.Interop.Hooks.Animation;
|
||||||
|
|
||||||
|
/// <summary> Called for some animations when mounted or mounting. </summary>
|
||||||
|
public sealed unsafe class SomeMountAnimation : FastHook<SomeMountAnimation.Delegate>
|
||||||
|
{
|
||||||
|
private readonly GameState _state;
|
||||||
|
private readonly CollectionResolver _collectionResolver;
|
||||||
|
|
||||||
|
public SomeMountAnimation(HookManager hooks, GameState state, CollectionResolver collectionResolver)
|
||||||
|
{
|
||||||
|
_state = state;
|
||||||
|
_collectionResolver = collectionResolver;
|
||||||
|
Task = hooks.CreateHook<Delegate>("Some Mount Animation", Sigs.UnkMountAnimation, Detour, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
public delegate void Delegate(DrawObject* drawObject, uint unk1, byte unk2, uint unk3);
|
||||||
|
|
||||||
|
[MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)]
|
||||||
|
private void Detour(DrawObject* drawObject, uint unk1, byte unk2, uint unk3)
|
||||||
|
{
|
||||||
|
Penumbra.Log.Excessive($"[Some Mount Animation] Invoked on {(nint)drawObject:X} with {unk1}, {unk2}, {unk3}.");
|
||||||
|
var last = _state.SetAnimationData(_collectionResolver.IdentifyCollection(drawObject, true));
|
||||||
|
Task.Result.Original(drawObject, unk1, unk2, unk3);
|
||||||
|
_state.RestoreAnimationData(last);
|
||||||
|
}
|
||||||
|
}
|
||||||
46
Penumbra/Interop/Hooks/Animation/SomePapLoad.cs
Normal file
46
Penumbra/Interop/Hooks/Animation/SomePapLoad.cs
Normal file
|
|
@ -0,0 +1,46 @@
|
||||||
|
using Dalamud.Plugin.Services;
|
||||||
|
using FFXIVClientStructs.FFXIV.Client.Game.Object;
|
||||||
|
using OtterGui.Services;
|
||||||
|
using Penumbra.GameData;
|
||||||
|
using Penumbra.Interop.PathResolving;
|
||||||
|
|
||||||
|
namespace Penumbra.Interop.Hooks.Animation;
|
||||||
|
|
||||||
|
/// <summary> Unknown what exactly this is, but it seems to load a bunch of paps. </summary>
|
||||||
|
public sealed unsafe class SomePapLoad : FastHook<SomePapLoad.Delegate>
|
||||||
|
{
|
||||||
|
private readonly GameState _state;
|
||||||
|
private readonly CollectionResolver _collectionResolver;
|
||||||
|
private readonly IObjectTable _objects;
|
||||||
|
|
||||||
|
public SomePapLoad(HookManager hooks, GameState state, CollectionResolver collectionResolver, IObjectTable objects)
|
||||||
|
{
|
||||||
|
_state = state;
|
||||||
|
_collectionResolver = collectionResolver;
|
||||||
|
_objects = objects;
|
||||||
|
Task = hooks.CreateHook<Delegate>("Some PAP Load", Sigs.LoadSomePap, Detour, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
public delegate void Delegate(nint a1, int a2, nint a3, int a4);
|
||||||
|
|
||||||
|
[MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)]
|
||||||
|
private void Detour(nint a1, int a2, nint a3, int a4)
|
||||||
|
{
|
||||||
|
Penumbra.Log.Excessive($"[Some PAP Load] Invoked on 0x{a1:X} with {a2}, {a3}, {a4}.");
|
||||||
|
var timelinePtr = a1 + Offsets.TimeLinePtr;
|
||||||
|
if (timelinePtr != nint.Zero)
|
||||||
|
{
|
||||||
|
var actorIdx = (int)(*(*(ulong**)timelinePtr + 1) >> 3);
|
||||||
|
if (actorIdx >= 0 && actorIdx < _objects.Length)
|
||||||
|
{
|
||||||
|
var last = _state.SetAnimationData(_collectionResolver.IdentifyCollection((GameObject*)_objects.GetObjectAddress(actorIdx),
|
||||||
|
true));
|
||||||
|
Task.Result.Original(a1, a2, a3, a4);
|
||||||
|
_state.RestoreAnimationData(last);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Task.Result.Original(a1, a2, a3, a4);
|
||||||
|
}
|
||||||
|
}
|
||||||
31
Penumbra/Interop/Hooks/Animation/SomeParasolAnimation.cs
Normal file
31
Penumbra/Interop/Hooks/Animation/SomeParasolAnimation.cs
Normal file
|
|
@ -0,0 +1,31 @@
|
||||||
|
using FFXIVClientStructs.FFXIV.Client.Graphics.Scene;
|
||||||
|
using OtterGui.Services;
|
||||||
|
using Penumbra.GameData;
|
||||||
|
using Penumbra.Interop.PathResolving;
|
||||||
|
|
||||||
|
namespace Penumbra.Interop.Hooks.Animation;
|
||||||
|
|
||||||
|
/// <summary> Called for some animations when using a Parasol. </summary>
|
||||||
|
public sealed unsafe class SomeParasolAnimation : FastHook<SomeParasolAnimation.Delegate>
|
||||||
|
{
|
||||||
|
private readonly GameState _state;
|
||||||
|
private readonly CollectionResolver _collectionResolver;
|
||||||
|
|
||||||
|
public SomeParasolAnimation(HookManager hooks, GameState state, CollectionResolver collectionResolver)
|
||||||
|
{
|
||||||
|
_state = state;
|
||||||
|
_collectionResolver = collectionResolver;
|
||||||
|
Task = hooks.CreateHook<Delegate>("Some Parasol Animation", Sigs.UnkParasolAnimation, Detour, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
public delegate void Delegate(DrawObject* drawObject, int unk1);
|
||||||
|
|
||||||
|
[MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)]
|
||||||
|
private void Detour(DrawObject* drawObject, int unk1)
|
||||||
|
{
|
||||||
|
Penumbra.Log.Excessive($"[Some Mount Animation] Invoked on {(nint)drawObject:X} with {unk1}.");
|
||||||
|
var last = _state.SetAnimationData(_collectionResolver.IdentifyCollection(drawObject, true));
|
||||||
|
Task.Result.Original(drawObject, unk1);
|
||||||
|
_state.RestoreAnimationData(last);
|
||||||
|
}
|
||||||
|
}
|
||||||
49
Penumbra/Interop/Hooks/CharacterBaseDestructor.cs
Normal file
49
Penumbra/Interop/Hooks/CharacterBaseDestructor.cs
Normal file
|
|
@ -0,0 +1,49 @@
|
||||||
|
using Dalamud.Hooking;
|
||||||
|
using FFXIVClientStructs.FFXIV.Client.Graphics.Scene;
|
||||||
|
using OtterGui.Classes;
|
||||||
|
using OtterGui.Services;
|
||||||
|
using Penumbra.UI.AdvancedWindow;
|
||||||
|
|
||||||
|
namespace Penumbra.Interop.Hooks;
|
||||||
|
|
||||||
|
public sealed unsafe class CharacterBaseDestructor : EventWrapperPtr<CharacterBase, CharacterBaseDestructor.Priority>, IHookService
|
||||||
|
{
|
||||||
|
public enum Priority
|
||||||
|
{
|
||||||
|
/// <seealso cref="PathResolving.DrawObjectState"/>
|
||||||
|
DrawObjectState = 0,
|
||||||
|
|
||||||
|
/// <seealso cref="ModEditWindow.MtrlTab"/>
|
||||||
|
MtrlTab = -1000,
|
||||||
|
}
|
||||||
|
|
||||||
|
public CharacterBaseDestructor(HookManager hooks)
|
||||||
|
: base("Destroy CharacterBase")
|
||||||
|
=> _task = hooks.CreateHook<Delegate>(Name, Address, Detour, true);
|
||||||
|
|
||||||
|
private readonly Task<Hook<Delegate>> _task;
|
||||||
|
|
||||||
|
public nint Address
|
||||||
|
=> (nint)CharacterBase.MemberFunctionPointers.Destroy;
|
||||||
|
|
||||||
|
public void Enable()
|
||||||
|
=> _task.Result.Enable();
|
||||||
|
|
||||||
|
public void Disable()
|
||||||
|
=> _task.Result.Disable();
|
||||||
|
|
||||||
|
public Task Awaiter
|
||||||
|
=> _task;
|
||||||
|
|
||||||
|
public bool Finished
|
||||||
|
=> _task.IsCompletedSuccessfully;
|
||||||
|
|
||||||
|
private delegate nint Delegate(CharacterBase* characterBase);
|
||||||
|
|
||||||
|
private nint Detour(CharacterBase* characterBase)
|
||||||
|
{
|
||||||
|
Penumbra.Log.Verbose($"[{Name}] Triggered with 0x{(nint)characterBase:X}.");
|
||||||
|
Invoke(characterBase);
|
||||||
|
return _task.Result.Original(characterBase);
|
||||||
|
}
|
||||||
|
}
|
||||||
49
Penumbra/Interop/Hooks/CharacterDestructor.cs
Normal file
49
Penumbra/Interop/Hooks/CharacterDestructor.cs
Normal file
|
|
@ -0,0 +1,49 @@
|
||||||
|
using Dalamud.Hooking;
|
||||||
|
using FFXIVClientStructs.FFXIV.Client.Game.Character;
|
||||||
|
using OtterGui.Classes;
|
||||||
|
using OtterGui.Services;
|
||||||
|
using Penumbra.GameData;
|
||||||
|
|
||||||
|
namespace Penumbra.Interop.Hooks;
|
||||||
|
|
||||||
|
public sealed unsafe class CharacterDestructor : EventWrapperPtr<Character, CharacterDestructor.Priority>, IHookService
|
||||||
|
{
|
||||||
|
public enum Priority
|
||||||
|
{
|
||||||
|
/// <seealso cref="PathResolving.CutsceneService"/>
|
||||||
|
CutsceneService = 0,
|
||||||
|
|
||||||
|
/// <seealso cref="PathResolving.IdentifiedCollectionCache"/>
|
||||||
|
IdentifiedCollectionCache = 0,
|
||||||
|
}
|
||||||
|
|
||||||
|
public CharacterDestructor(HookManager hooks)
|
||||||
|
: base("Character Destructor")
|
||||||
|
=> _task = hooks.CreateHook<Delegate>(Name, Sigs.CharacterDestructor, Detour, true);
|
||||||
|
|
||||||
|
private readonly Task<Hook<Delegate>> _task;
|
||||||
|
|
||||||
|
public nint Address
|
||||||
|
=> _task.Result.Address;
|
||||||
|
|
||||||
|
public void Enable()
|
||||||
|
=> _task.Result.Enable();
|
||||||
|
|
||||||
|
public void Disable()
|
||||||
|
=> _task.Result.Disable();
|
||||||
|
|
||||||
|
public Task Awaiter
|
||||||
|
=> _task;
|
||||||
|
|
||||||
|
public bool Finished
|
||||||
|
=> _task.IsCompletedSuccessfully;
|
||||||
|
|
||||||
|
private delegate void Delegate(Character* character);
|
||||||
|
|
||||||
|
private void Detour(Character* character)
|
||||||
|
{
|
||||||
|
Penumbra.Log.Verbose($"[{Name}] Triggered with 0x{(nint)character:X}.");
|
||||||
|
Invoke(character);
|
||||||
|
_task.Result.Original(character);
|
||||||
|
}
|
||||||
|
}
|
||||||
47
Penumbra/Interop/Hooks/CopyCharacter.cs
Normal file
47
Penumbra/Interop/Hooks/CopyCharacter.cs
Normal file
|
|
@ -0,0 +1,47 @@
|
||||||
|
using Dalamud.Hooking;
|
||||||
|
using FFXIVClientStructs.FFXIV.Client.Game.Character;
|
||||||
|
using OtterGui.Classes;
|
||||||
|
using OtterGui.Services;
|
||||||
|
|
||||||
|
namespace Penumbra.Interop.Hooks;
|
||||||
|
|
||||||
|
public sealed unsafe class CopyCharacter : EventWrapperPtr<Character, Character, CopyCharacter.Priority>, IHookService
|
||||||
|
{
|
||||||
|
public enum Priority
|
||||||
|
{
|
||||||
|
/// <seealso cref="PathResolving.CutsceneService"/>
|
||||||
|
CutsceneService = 0,
|
||||||
|
}
|
||||||
|
|
||||||
|
public CopyCharacter(HookManager hooks)
|
||||||
|
: base("Copy Character")
|
||||||
|
=> _task = hooks.CreateHook<Delegate>(Name, Address, Detour, true);
|
||||||
|
|
||||||
|
private readonly Task<Hook<Delegate>> _task;
|
||||||
|
|
||||||
|
public nint Address
|
||||||
|
=> (nint)CharacterSetup.MemberFunctionPointers.CopyFromCharacter;
|
||||||
|
|
||||||
|
public void Enable()
|
||||||
|
=> _task.Result.Enable();
|
||||||
|
|
||||||
|
public void Disable()
|
||||||
|
=> _task.Result.Disable();
|
||||||
|
|
||||||
|
public Task Awaiter
|
||||||
|
=> _task;
|
||||||
|
|
||||||
|
public bool Finished
|
||||||
|
=> _task.IsCompletedSuccessfully;
|
||||||
|
|
||||||
|
private delegate ulong Delegate(CharacterSetup* target, Character* source, uint unk);
|
||||||
|
|
||||||
|
private ulong Detour(CharacterSetup* target, Character* source, uint unk)
|
||||||
|
{
|
||||||
|
// TODO: update when CS updated.
|
||||||
|
var character = ((Character**)target)[1];
|
||||||
|
Penumbra.Log.Verbose($"[{Name}] Triggered with target: 0x{(nint)target:X}, source : 0x{(nint)source:X} unk: {unk}.");
|
||||||
|
Invoke(character, source);
|
||||||
|
return _task.Result.Original(target, source, unk);
|
||||||
|
}
|
||||||
|
}
|
||||||
74
Penumbra/Interop/Hooks/CreateCharacterBase.cs
Normal file
74
Penumbra/Interop/Hooks/CreateCharacterBase.cs
Normal file
|
|
@ -0,0 +1,74 @@
|
||||||
|
using Dalamud.Hooking;
|
||||||
|
using FFXIVClientStructs.FFXIV.Client.Graphics.Scene;
|
||||||
|
using OtterGui.Classes;
|
||||||
|
using OtterGui.Services;
|
||||||
|
using Penumbra.GameData.Structs;
|
||||||
|
|
||||||
|
namespace Penumbra.Interop.Hooks;
|
||||||
|
|
||||||
|
public sealed unsafe class CreateCharacterBase : EventWrapperPtr<ModelCharaId, CustomizeArray, CharacterArmor, CreateCharacterBase.Priority>, IHookService
|
||||||
|
{
|
||||||
|
public enum Priority
|
||||||
|
{
|
||||||
|
/// <seealso cref="PathResolving.MetaState"/>
|
||||||
|
MetaState = 0,
|
||||||
|
}
|
||||||
|
|
||||||
|
public CreateCharacterBase(HookManager hooks)
|
||||||
|
: base("Create CharacterBase")
|
||||||
|
=> _task = hooks.CreateHook<Delegate>(Name, Address, Detour, true);
|
||||||
|
|
||||||
|
private readonly Task<Hook<Delegate>> _task;
|
||||||
|
|
||||||
|
public nint Address
|
||||||
|
=> (nint)CharacterBase.MemberFunctionPointers.Create;
|
||||||
|
|
||||||
|
public void Enable()
|
||||||
|
=> _task.Result.Enable();
|
||||||
|
|
||||||
|
public void Disable()
|
||||||
|
=> _task.Result.Disable();
|
||||||
|
|
||||||
|
public Task Awaiter
|
||||||
|
=> _task;
|
||||||
|
|
||||||
|
public bool Finished
|
||||||
|
=> _task.IsCompletedSuccessfully;
|
||||||
|
|
||||||
|
private delegate CharacterBase* Delegate(ModelCharaId model, CustomizeArray* customize, CharacterArmor* equipment, byte unk);
|
||||||
|
|
||||||
|
private CharacterBase* Detour(ModelCharaId model, CustomizeArray* customize, CharacterArmor* equipment, byte unk)
|
||||||
|
{
|
||||||
|
Penumbra.Log.Verbose($"[{Name}] Triggered with model: {model.Id}, customize: 0x{(nint) customize:X}, equipment: 0x{(nint)equipment:X}, unk: {unk}.");
|
||||||
|
Invoke(&model, customize, equipment);
|
||||||
|
var ret = _task.Result.Original(model, customize, equipment, unk);
|
||||||
|
_postEvent.Invoke(model, customize, equipment, ret);
|
||||||
|
return ret;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Subscribe(ActionPtr234<ModelCharaId, CustomizeArray, CharacterArmor, CharacterBase> subscriber, PostEvent.Priority priority)
|
||||||
|
=> _postEvent.Subscribe(subscriber, priority);
|
||||||
|
|
||||||
|
public void Unsubscribe(ActionPtr234<ModelCharaId, CustomizeArray, CharacterArmor, CharacterBase> subscriber)
|
||||||
|
=> _postEvent.Unsubscribe(subscriber);
|
||||||
|
|
||||||
|
|
||||||
|
private readonly PostEvent _postEvent = new("Created CharacterBase");
|
||||||
|
|
||||||
|
protected override void Dispose(bool disposing)
|
||||||
|
{
|
||||||
|
_postEvent.Dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
public class PostEvent(string name) : EventWrapperPtr234<ModelCharaId, CustomizeArray, CharacterArmor, CharacterBase, PostEvent.Priority>(name)
|
||||||
|
{
|
||||||
|
public enum Priority
|
||||||
|
{
|
||||||
|
/// <seealso cref="PathResolving.DrawObjectState"/>
|
||||||
|
DrawObjectState = 0,
|
||||||
|
|
||||||
|
/// <seealso cref="PathResolving.MetaState"/>
|
||||||
|
MetaState = 0,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
43
Penumbra/Interop/Hooks/DebugHook.cs
Normal file
43
Penumbra/Interop/Hooks/DebugHook.cs
Normal file
|
|
@ -0,0 +1,43 @@
|
||||||
|
using Dalamud.Hooking;
|
||||||
|
using FFXIVClientStructs.FFXIV.Client.System.Resource.Handle;
|
||||||
|
using OtterGui.Services;
|
||||||
|
|
||||||
|
namespace Penumbra.Interop.Hooks;
|
||||||
|
|
||||||
|
#if DEBUG
|
||||||
|
public sealed unsafe class DebugHook : IHookService
|
||||||
|
{
|
||||||
|
public const string Signature = "";
|
||||||
|
|
||||||
|
public DebugHook(HookManager hooks)
|
||||||
|
{
|
||||||
|
if (Signature.Length > 0)
|
||||||
|
_task = hooks.CreateHook<Delegate>("Debug Hook", Signature, Detour, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
private readonly Task<Hook<Delegate>>? _task;
|
||||||
|
|
||||||
|
public nint Address
|
||||||
|
=> _task?.Result.Address ?? nint.Zero;
|
||||||
|
|
||||||
|
public void Enable()
|
||||||
|
=> _task?.Result.Enable();
|
||||||
|
|
||||||
|
public void Disable()
|
||||||
|
=> _task?.Result.Disable();
|
||||||
|
|
||||||
|
public Task Awaiter
|
||||||
|
=> _task ?? Task.CompletedTask;
|
||||||
|
|
||||||
|
public bool Finished
|
||||||
|
=> _task?.IsCompletedSuccessfully ?? true;
|
||||||
|
|
||||||
|
private delegate nint Delegate(ResourceHandle* resourceHandle);
|
||||||
|
|
||||||
|
private nint Detour(ResourceHandle* resourceHandle)
|
||||||
|
{
|
||||||
|
Penumbra.Log.Information($"[Debug Hook] Triggered with 0x{(nint)resourceHandle:X}.");
|
||||||
|
return _task!.Result.Original(resourceHandle);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#endif
|
||||||
48
Penumbra/Interop/Hooks/EnableDraw.cs
Normal file
48
Penumbra/Interop/Hooks/EnableDraw.cs
Normal file
|
|
@ -0,0 +1,48 @@
|
||||||
|
using Dalamud.Hooking;
|
||||||
|
using FFXIVClientStructs.FFXIV.Client.Game.Object;
|
||||||
|
using OtterGui.Services;
|
||||||
|
using Penumbra.GameData;
|
||||||
|
|
||||||
|
namespace Penumbra.Interop.Hooks;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// EnableDraw is what creates DrawObjects for gameObjects,
|
||||||
|
/// so we always keep track of the current GameObject to be able to link it to the DrawObject.
|
||||||
|
/// </summary>
|
||||||
|
public sealed unsafe class EnableDraw : IHookService
|
||||||
|
{
|
||||||
|
private readonly Task<Hook<Delegate>> _task;
|
||||||
|
private readonly GameState _state;
|
||||||
|
|
||||||
|
public EnableDraw(HookManager hooks, GameState state)
|
||||||
|
{
|
||||||
|
_state = state;
|
||||||
|
_task = hooks.CreateHook<Delegate>("Enable Draw", Sigs.EnableDraw, Detour, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
private delegate void Delegate(GameObject* gameObject);
|
||||||
|
|
||||||
|
[MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)]
|
||||||
|
private void Detour(GameObject* gameObject)
|
||||||
|
{
|
||||||
|
_state.QueueGameObject(gameObject);
|
||||||
|
Penumbra.Log.Excessive($"[Enable Draw] Invoked on 0x{(nint) gameObject:X}.");
|
||||||
|
_task.Result.Original.Invoke(gameObject);
|
||||||
|
_state.DequeueGameObject();
|
||||||
|
}
|
||||||
|
|
||||||
|
public Task Awaiter
|
||||||
|
=> _task;
|
||||||
|
|
||||||
|
public bool Finished
|
||||||
|
=> _task.IsCompletedSuccessfully;
|
||||||
|
|
||||||
|
public nint Address
|
||||||
|
=> _task.Result.Address;
|
||||||
|
|
||||||
|
public void Enable()
|
||||||
|
=> _task.Result.Enable();
|
||||||
|
|
||||||
|
public void Disable()
|
||||||
|
=> _task.Result.Disable();
|
||||||
|
}
|
||||||
31
Penumbra/Interop/Hooks/Meta/CalculateHeight.cs
Normal file
31
Penumbra/Interop/Hooks/Meta/CalculateHeight.cs
Normal file
|
|
@ -0,0 +1,31 @@
|
||||||
|
using FFXIVClientStructs.FFXIV.Client.Game.Object;
|
||||||
|
using OtterGui.Services;
|
||||||
|
using Penumbra.Interop.PathResolving;
|
||||||
|
using Character = FFXIVClientStructs.FFXIV.Client.Game.Character.Character;
|
||||||
|
|
||||||
|
namespace Penumbra.Interop.Hooks.Meta;
|
||||||
|
|
||||||
|
public sealed unsafe class CalculateHeight : FastHook<CalculateHeight.Delegate>
|
||||||
|
{
|
||||||
|
private readonly CollectionResolver _collectionResolver;
|
||||||
|
private readonly MetaState _metaState;
|
||||||
|
|
||||||
|
public CalculateHeight(HookManager hooks, CollectionResolver collectionResolver, MetaState metaState)
|
||||||
|
{
|
||||||
|
_collectionResolver = collectionResolver;
|
||||||
|
_metaState = metaState;
|
||||||
|
Task = hooks.CreateHook<Delegate>("Calculate Height", (nint)Character.MemberFunctionPointers.CalculateHeight, Detour, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
public delegate ulong Delegate(Character* character);
|
||||||
|
|
||||||
|
[MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)]
|
||||||
|
private ulong Detour(Character* character)
|
||||||
|
{
|
||||||
|
var collection = _collectionResolver.IdentifyCollection((GameObject*)character, true);
|
||||||
|
using var cmp = _metaState.ResolveRspData(collection.ModCollection);
|
||||||
|
var ret = Task.Result.Original.Invoke(character);
|
||||||
|
Penumbra.Log.Excessive($"[Calculate Height] Invoked on {(nint)character:X} -> {ret}.");
|
||||||
|
return ret;
|
||||||
|
}
|
||||||
|
}
|
||||||
34
Penumbra/Interop/Hooks/Meta/ChangeCustomize.cs
Normal file
34
Penumbra/Interop/Hooks/Meta/ChangeCustomize.cs
Normal file
|
|
@ -0,0 +1,34 @@
|
||||||
|
using FFXIVClientStructs.FFXIV.Client.Graphics.Scene;
|
||||||
|
using OtterGui.Services;
|
||||||
|
using Penumbra.GameData;
|
||||||
|
using Penumbra.GameData.Structs;
|
||||||
|
using Penumbra.Interop.PathResolving;
|
||||||
|
|
||||||
|
namespace Penumbra.Interop.Hooks.Meta;
|
||||||
|
|
||||||
|
public sealed unsafe class ChangeCustomize : FastHook<ChangeCustomize.Delegate>
|
||||||
|
{
|
||||||
|
private readonly CollectionResolver _collectionResolver;
|
||||||
|
private readonly MetaState _metaState;
|
||||||
|
|
||||||
|
public ChangeCustomize(HookManager hooks, CollectionResolver collectionResolver, MetaState metaState)
|
||||||
|
{
|
||||||
|
_collectionResolver = collectionResolver;
|
||||||
|
_metaState = metaState;
|
||||||
|
Task = hooks.CreateHook<Delegate>("Change Customize", Sigs.ChangeCustomize, Detour, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
public delegate bool Delegate(Human* human, CustomizeArray* data, byte skipEquipment);
|
||||||
|
|
||||||
|
[MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)]
|
||||||
|
private bool Detour(Human* human, CustomizeArray* data, byte skipEquipment)
|
||||||
|
{
|
||||||
|
_metaState.CustomizeChangeCollection = _collectionResolver.IdentifyCollection((DrawObject*)human, true);
|
||||||
|
using var cmp = _metaState.ResolveRspData(_metaState.CustomizeChangeCollection.ModCollection);
|
||||||
|
using var decal1 = _metaState.ResolveDecal(_metaState.CustomizeChangeCollection, true);
|
||||||
|
using var decal2 = _metaState.ResolveDecal(_metaState.CustomizeChangeCollection, false);
|
||||||
|
var ret = Task.Result.Original.Invoke(human, data, skipEquipment);
|
||||||
|
Penumbra.Log.Excessive($"[Change Customize] Invoked on {(nint)human:X} with {(nint)data:X}, {skipEquipment} -> {ret}.");
|
||||||
|
return ret;
|
||||||
|
}
|
||||||
|
}
|
||||||
35
Penumbra/Interop/Hooks/Meta/GetEqpIndirect.cs
Normal file
35
Penumbra/Interop/Hooks/Meta/GetEqpIndirect.cs
Normal file
|
|
@ -0,0 +1,35 @@
|
||||||
|
using FFXIVClientStructs.FFXIV.Client.Graphics.Scene;
|
||||||
|
using OtterGui.Services;
|
||||||
|
using Penumbra.GameData;
|
||||||
|
using Penumbra.Interop.PathResolving;
|
||||||
|
|
||||||
|
namespace Penumbra.Interop.Hooks.Meta;
|
||||||
|
|
||||||
|
public sealed unsafe class GetEqpIndirect : FastHook<GetEqpIndirect.Delegate>
|
||||||
|
{
|
||||||
|
private readonly CollectionResolver _collectionResolver;
|
||||||
|
private readonly MetaState _metaState;
|
||||||
|
|
||||||
|
public GetEqpIndirect(HookManager hooks, CollectionResolver collectionResolver, MetaState metaState)
|
||||||
|
{
|
||||||
|
_collectionResolver = collectionResolver;
|
||||||
|
_metaState = metaState;
|
||||||
|
Task = hooks.CreateHook<Delegate>("Get EQP Indirect", Sigs.GetEqpIndirect, Detour, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
public delegate void Delegate(DrawObject* drawObject);
|
||||||
|
|
||||||
|
[MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)]
|
||||||
|
private void Detour(DrawObject* drawObject)
|
||||||
|
{
|
||||||
|
// Shortcut because this is also called all the time.
|
||||||
|
// Same thing is checked at the beginning of the original function.
|
||||||
|
if ((*(byte*)((nint)drawObject + Offsets.GetEqpIndirectSkip1) & 1) == 0 || *(ulong*)((nint)drawObject + Offsets.GetEqpIndirectSkip2) == 0)
|
||||||
|
return;
|
||||||
|
|
||||||
|
Penumbra.Log.Excessive($"[Get EQP Indirect] Invoked on {(nint)drawObject:X}.");
|
||||||
|
var collection = _collectionResolver.IdentifyCollection(drawObject, true);
|
||||||
|
using var eqp = _metaState.ResolveEqpData(collection.ModCollection);
|
||||||
|
Task.Result.Original(drawObject);
|
||||||
|
}
|
||||||
|
}
|
||||||
30
Penumbra/Interop/Hooks/Meta/ModelLoadComplete.cs
Normal file
30
Penumbra/Interop/Hooks/Meta/ModelLoadComplete.cs
Normal file
|
|
@ -0,0 +1,30 @@
|
||||||
|
using FFXIVClientStructs.FFXIV.Client.Graphics.Scene;
|
||||||
|
using OtterGui.Services;
|
||||||
|
using Penumbra.Interop.PathResolving;
|
||||||
|
|
||||||
|
namespace Penumbra.Interop.Hooks.Meta;
|
||||||
|
|
||||||
|
public sealed unsafe class ModelLoadComplete : FastHook<ModelLoadComplete.Delegate>
|
||||||
|
{
|
||||||
|
private readonly CollectionResolver _collectionResolver;
|
||||||
|
private readonly MetaState _metaState;
|
||||||
|
|
||||||
|
public ModelLoadComplete(HookManager hooks, CollectionResolver collectionResolver, MetaState metaState, CharacterBaseVTables vtables)
|
||||||
|
{
|
||||||
|
_collectionResolver = collectionResolver;
|
||||||
|
_metaState = metaState;
|
||||||
|
Task = hooks.CreateHook<Delegate>("Model Load Complete", vtables.HumanVTable[58], Detour, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
public delegate void Delegate(DrawObject* drawObject);
|
||||||
|
|
||||||
|
[MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)]
|
||||||
|
private void Detour(DrawObject* drawObject)
|
||||||
|
{
|
||||||
|
Penumbra.Log.Excessive($"[Model Load Complete] Invoked on {(nint)drawObject:X}.");
|
||||||
|
var collection = _collectionResolver.IdentifyCollection(drawObject, true);
|
||||||
|
using var eqp = _metaState.ResolveEqpData(collection.ModCollection);
|
||||||
|
using var eqdp = _metaState.ResolveEqdpData(collection.ModCollection, MetaState.GetDrawObjectGenderRace((nint)drawObject), true, true);
|
||||||
|
Task.Result.Original(drawObject);
|
||||||
|
}
|
||||||
|
}
|
||||||
37
Penumbra/Interop/Hooks/Meta/RspSetupCharacter.cs
Normal file
37
Penumbra/Interop/Hooks/Meta/RspSetupCharacter.cs
Normal file
|
|
@ -0,0 +1,37 @@
|
||||||
|
using FFXIVClientStructs.FFXIV.Client.Graphics.Scene;
|
||||||
|
using OtterGui.Services;
|
||||||
|
using Penumbra.GameData;
|
||||||
|
using Penumbra.Interop.PathResolving;
|
||||||
|
|
||||||
|
namespace Penumbra.Interop.Hooks.Meta;
|
||||||
|
|
||||||
|
public sealed unsafe class RspSetupCharacter : FastHook<RspSetupCharacter.Delegate>
|
||||||
|
{
|
||||||
|
private readonly CollectionResolver _collectionResolver;
|
||||||
|
private readonly MetaState _metaState;
|
||||||
|
|
||||||
|
public RspSetupCharacter(HookManager hooks, CollectionResolver collectionResolver, MetaState metaState)
|
||||||
|
{
|
||||||
|
_collectionResolver = collectionResolver;
|
||||||
|
_metaState = metaState;
|
||||||
|
Task = hooks.CreateHook<Delegate>("RSP Setup Character", Sigs.RspSetupCharacter, Detour, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
public delegate void Delegate(DrawObject* drawObject, nint unk2, float unk3, nint unk4, byte unk5);
|
||||||
|
|
||||||
|
[MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)]
|
||||||
|
private void Detour(DrawObject* drawObject, nint unk2, float unk3, nint unk4, byte unk5)
|
||||||
|
{
|
||||||
|
Penumbra.Log.Excessive($"[RSP Setup Character] Invoked on {(nint)drawObject:X} with {unk2}, {unk3}, {unk4}, {unk5}.");
|
||||||
|
// Skip if we are coming from ChangeCustomize.
|
||||||
|
if (_metaState.CustomizeChangeCollection.Valid)
|
||||||
|
{
|
||||||
|
Task.Result.Original.Invoke(drawObject, unk2, unk3, unk4, unk5);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var collection = _collectionResolver.IdentifyCollection(drawObject, true);
|
||||||
|
using var cmp = _metaState.ResolveRspData(collection.ModCollection);
|
||||||
|
Task.Result.Original.Invoke(drawObject, unk2, unk3, unk4, unk5);
|
||||||
|
}
|
||||||
|
}
|
||||||
35
Penumbra/Interop/Hooks/Meta/SetupVisor.cs
Normal file
35
Penumbra/Interop/Hooks/Meta/SetupVisor.cs
Normal file
|
|
@ -0,0 +1,35 @@
|
||||||
|
using FFXIVClientStructs.FFXIV.Client.Graphics.Scene;
|
||||||
|
using OtterGui.Services;
|
||||||
|
using Penumbra.GameData;
|
||||||
|
using Penumbra.Interop.PathResolving;
|
||||||
|
|
||||||
|
namespace Penumbra.Interop.Hooks.Meta;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// GMP. This gets called every time when changing visor state, and it accesses the gmp file itself,
|
||||||
|
/// but it only applies a changed gmp file after a redraw for some reason.
|
||||||
|
/// </summary>
|
||||||
|
public sealed unsafe class SetupVisor : FastHook<SetupVisor.Delegate>
|
||||||
|
{
|
||||||
|
private readonly CollectionResolver _collectionResolver;
|
||||||
|
private readonly MetaState _metaState;
|
||||||
|
|
||||||
|
public SetupVisor(HookManager hooks, CollectionResolver collectionResolver, MetaState metaState)
|
||||||
|
{
|
||||||
|
_collectionResolver = collectionResolver;
|
||||||
|
_metaState = metaState;
|
||||||
|
Task = hooks.CreateHook<Delegate>("Setup Visor", Sigs.SetupVisor, Detour, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
public delegate byte Delegate(DrawObject* drawObject, ushort modelId, byte visorState);
|
||||||
|
|
||||||
|
[MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)]
|
||||||
|
private byte Detour(DrawObject* drawObject, ushort modelId, byte visorState)
|
||||||
|
{
|
||||||
|
var collection = _collectionResolver.IdentifyCollection(drawObject, true);
|
||||||
|
using var gmp = _metaState.ResolveGmpData(collection.ModCollection);
|
||||||
|
var ret = Task.Result.Original.Invoke(drawObject, modelId, visorState);
|
||||||
|
Penumbra.Log.Excessive($"[Setup Visor] Invoked on {(nint)drawObject:X} with {modelId}, {visorState} -> {ret}.");
|
||||||
|
return ret;
|
||||||
|
}
|
||||||
|
}
|
||||||
36
Penumbra/Interop/Hooks/Meta/UpdateModel.cs
Normal file
36
Penumbra/Interop/Hooks/Meta/UpdateModel.cs
Normal file
|
|
@ -0,0 +1,36 @@
|
||||||
|
using FFXIVClientStructs.FFXIV.Client.Graphics.Scene;
|
||||||
|
using OtterGui.Services;
|
||||||
|
using Penumbra.GameData;
|
||||||
|
using Penumbra.Interop.PathResolving;
|
||||||
|
|
||||||
|
namespace Penumbra.Interop.Hooks.Meta;
|
||||||
|
|
||||||
|
public sealed unsafe class UpdateModel : FastHook<UpdateModel.Delegate>
|
||||||
|
{
|
||||||
|
private readonly CollectionResolver _collectionResolver;
|
||||||
|
private readonly MetaState _metaState;
|
||||||
|
|
||||||
|
public UpdateModel(HookManager hooks, CollectionResolver collectionResolver, MetaState metaState)
|
||||||
|
{
|
||||||
|
_collectionResolver = collectionResolver;
|
||||||
|
_metaState = metaState;
|
||||||
|
Task = hooks.CreateHook<Delegate>("Update Model", Sigs.UpdateModel, Detour, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
public delegate void Delegate(DrawObject* drawObject);
|
||||||
|
|
||||||
|
[MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)]
|
||||||
|
private void Detour(DrawObject* drawObject)
|
||||||
|
{
|
||||||
|
// Shortcut because this is called all the time.
|
||||||
|
// Same thing is checked at the beginning of the original function.
|
||||||
|
if (*(int*)((nint)drawObject + Offsets.UpdateModelSkip) == 0)
|
||||||
|
return;
|
||||||
|
|
||||||
|
Penumbra.Log.Excessive($"[Update Model] Invoked on {(nint)drawObject:X}.");
|
||||||
|
var collection = _collectionResolver.IdentifyCollection(drawObject, true);
|
||||||
|
using var eqp = _metaState.ResolveEqpData(collection.ModCollection);
|
||||||
|
using var eqdp = _metaState.ResolveEqdpData(collection.ModCollection, MetaState.GetDrawObjectGenderRace((nint)drawObject), true, true);
|
||||||
|
Task.Result.Original.Invoke(drawObject);
|
||||||
|
}
|
||||||
|
}
|
||||||
50
Penumbra/Interop/Hooks/ResourceHandleDestructor.cs
Normal file
50
Penumbra/Interop/Hooks/ResourceHandleDestructor.cs
Normal file
|
|
@ -0,0 +1,50 @@
|
||||||
|
using Dalamud.Hooking;
|
||||||
|
using OtterGui.Classes;
|
||||||
|
using OtterGui.Services;
|
||||||
|
using Penumbra.GameData;
|
||||||
|
using Penumbra.Interop.Services;
|
||||||
|
using Penumbra.Interop.Structs;
|
||||||
|
|
||||||
|
namespace Penumbra.Interop.Hooks;
|
||||||
|
|
||||||
|
public sealed unsafe class ResourceHandleDestructor : EventWrapperPtr<ResourceHandle, ResourceHandleDestructor.Priority>, IHookService
|
||||||
|
{
|
||||||
|
public enum Priority
|
||||||
|
{
|
||||||
|
/// <seealso cref="PathResolving.SubfileHelper"/>
|
||||||
|
SubfileHelper,
|
||||||
|
|
||||||
|
/// <seealso cref="SkinFixer"/>
|
||||||
|
SkinFixer,
|
||||||
|
}
|
||||||
|
|
||||||
|
public ResourceHandleDestructor(HookManager hooks)
|
||||||
|
: base("Destroy ResourceHandle")
|
||||||
|
=> _task = hooks.CreateHook<Delegate>(Name, Sigs.ResourceHandleDestructor, Detour, true);
|
||||||
|
|
||||||
|
private readonly Task<Hook<Delegate>> _task;
|
||||||
|
|
||||||
|
public nint Address
|
||||||
|
=> _task.Result.Address;
|
||||||
|
|
||||||
|
public void Enable()
|
||||||
|
=> _task.Result.Enable();
|
||||||
|
|
||||||
|
public void Disable()
|
||||||
|
=> _task.Result.Disable();
|
||||||
|
|
||||||
|
public Task Awaiter
|
||||||
|
=> _task;
|
||||||
|
|
||||||
|
public bool Finished
|
||||||
|
=> _task.IsCompletedSuccessfully;
|
||||||
|
|
||||||
|
private delegate nint Delegate(ResourceHandle* resourceHandle);
|
||||||
|
|
||||||
|
private nint Detour(ResourceHandle* resourceHandle)
|
||||||
|
{
|
||||||
|
Penumbra.Log.Verbose($"[{Name}] Triggered with 0x{(nint)resourceHandle:X}.");
|
||||||
|
Invoke(resourceHandle);
|
||||||
|
return _task.Result.Original(resourceHandle);
|
||||||
|
}
|
||||||
|
}
|
||||||
71
Penumbra/Interop/Hooks/WeaponReload.cs
Normal file
71
Penumbra/Interop/Hooks/WeaponReload.cs
Normal file
|
|
@ -0,0 +1,71 @@
|
||||||
|
using Dalamud.Hooking;
|
||||||
|
using FFXIVClientStructs.FFXIV.Client.Game.Character;
|
||||||
|
using OtterGui.Classes;
|
||||||
|
using OtterGui.Services;
|
||||||
|
using Penumbra.GameData.Structs;
|
||||||
|
|
||||||
|
namespace Penumbra.Interop.Hooks;
|
||||||
|
|
||||||
|
public sealed unsafe class WeaponReload : EventWrapperPtr<DrawDataContainer, Character, CharacterWeapon, WeaponReload.Priority>, IHookService
|
||||||
|
{
|
||||||
|
public enum Priority
|
||||||
|
{
|
||||||
|
/// <seealso cref="PathResolving.DrawObjectState"/>
|
||||||
|
DrawObjectState = 0,
|
||||||
|
}
|
||||||
|
|
||||||
|
public WeaponReload(HookManager hooks)
|
||||||
|
: base("Reload Weapon")
|
||||||
|
=> _task = hooks.CreateHook<Delegate>(Name, Address, Detour, true);
|
||||||
|
|
||||||
|
private readonly Task<Hook<Delegate>> _task;
|
||||||
|
|
||||||
|
public nint Address
|
||||||
|
=> (nint)DrawDataContainer.MemberFunctionPointers.LoadWeapon;
|
||||||
|
|
||||||
|
public void Enable()
|
||||||
|
=> _task.Result.Enable();
|
||||||
|
|
||||||
|
public void Disable()
|
||||||
|
=> _task.Result.Disable();
|
||||||
|
|
||||||
|
public Task Awaiter
|
||||||
|
=> _task;
|
||||||
|
|
||||||
|
public bool Finished
|
||||||
|
=> _task.IsCompletedSuccessfully;
|
||||||
|
|
||||||
|
private delegate void Delegate(DrawDataContainer* drawData, uint slot, ulong weapon, byte d, byte e, byte f, byte g);
|
||||||
|
|
||||||
|
private void Detour(DrawDataContainer* drawData, uint slot, ulong weapon, byte d, byte e, byte f, byte g)
|
||||||
|
{
|
||||||
|
var gameObject = drawData->Parent;
|
||||||
|
Penumbra.Log.Verbose($"[{Name}] Triggered with drawData: 0x{(nint)drawData:X}, {slot}, {weapon}, {d}, {e}, {f}, {g}.");
|
||||||
|
Invoke(drawData, gameObject, (CharacterWeapon*)(&weapon));
|
||||||
|
_task.Result.Original(drawData, slot, weapon, d, e, f, g);
|
||||||
|
_postEvent.Invoke(drawData, gameObject);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Subscribe(ActionPtr<DrawDataContainer, Character> subscriber, PostEvent.Priority priority)
|
||||||
|
=> _postEvent.Subscribe(subscriber, priority);
|
||||||
|
|
||||||
|
public void Unsubscribe(ActionPtr<DrawDataContainer, Character> subscriber)
|
||||||
|
=> _postEvent.Unsubscribe(subscriber);
|
||||||
|
|
||||||
|
|
||||||
|
private readonly PostEvent _postEvent = new("Created CharacterBase");
|
||||||
|
|
||||||
|
protected override void Dispose(bool disposing)
|
||||||
|
{
|
||||||
|
_postEvent.Dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
public class PostEvent(string name) : EventWrapperPtr<DrawDataContainer, Character, PostEvent.Priority>(name)
|
||||||
|
{
|
||||||
|
public enum Priority
|
||||||
|
{
|
||||||
|
/// <seealso cref="PathResolving.DrawObjectState"/>
|
||||||
|
DrawObjectState = 0,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,423 +0,0 @@
|
||||||
using Dalamud.Game.ClientState.Conditions;
|
|
||||||
using Dalamud.Hooking;
|
|
||||||
using Dalamud.Plugin.Services;
|
|
||||||
using Dalamud.Utility.Signatures;
|
|
||||||
using FFXIVClientStructs.FFXIV.Client.Game;
|
|
||||||
using FFXIVClientStructs.FFXIV.Client.Graphics.Scene;
|
|
||||||
using Penumbra.Collections;
|
|
||||||
using Penumbra.Api.Enums;
|
|
||||||
using Penumbra.GameData;
|
|
||||||
using Penumbra.Interop.Structs;
|
|
||||||
using Penumbra.String;
|
|
||||||
using Penumbra.String.Classes;
|
|
||||||
using Penumbra.Util;
|
|
||||||
using GameObject = FFXIVClientStructs.FFXIV.Client.Game.Object.GameObject;
|
|
||||||
|
|
||||||
namespace Penumbra.Interop.PathResolving;
|
|
||||||
|
|
||||||
public unsafe class AnimationHookService : IDisposable
|
|
||||||
{
|
|
||||||
private readonly PerformanceTracker _performance;
|
|
||||||
private readonly IObjectTable _objects;
|
|
||||||
private readonly CollectionResolver _collectionResolver;
|
|
||||||
private readonly DrawObjectState _drawObjectState;
|
|
||||||
private readonly CollectionResolver _resolver;
|
|
||||||
private readonly ICondition _conditions;
|
|
||||||
|
|
||||||
private readonly ThreadLocal<ResolveData> _animationLoadData = new(() => ResolveData.Invalid, true);
|
|
||||||
private readonly ThreadLocal<ResolveData> _characterSoundData = new(() => ResolveData.Invalid, true);
|
|
||||||
|
|
||||||
public AnimationHookService(PerformanceTracker performance, IObjectTable objects, CollectionResolver collectionResolver,
|
|
||||||
DrawObjectState drawObjectState, CollectionResolver resolver, ICondition conditions, IGameInteropProvider interop)
|
|
||||||
{
|
|
||||||
_performance = performance;
|
|
||||||
_objects = objects;
|
|
||||||
_collectionResolver = collectionResolver;
|
|
||||||
_drawObjectState = drawObjectState;
|
|
||||||
_resolver = resolver;
|
|
||||||
_conditions = conditions;
|
|
||||||
|
|
||||||
interop.InitializeFromAttributes(this);
|
|
||||||
_loadCharacterSoundHook =
|
|
||||||
interop.HookFromAddress<LoadCharacterSound>(
|
|
||||||
(nint)FFXIVClientStructs.FFXIV.Client.Game.Character.Character.VfxContainer.MemberFunctionPointers.LoadCharacterSound,
|
|
||||||
LoadCharacterSoundDetour);
|
|
||||||
|
|
||||||
_loadCharacterSoundHook.Enable();
|
|
||||||
_loadTimelineResourcesHook.Enable();
|
|
||||||
_characterBaseLoadAnimationHook.Enable();
|
|
||||||
_loadSomePapHook.Enable();
|
|
||||||
_someActionLoadHook.Enable();
|
|
||||||
_loadCharacterVfxHook.Enable();
|
|
||||||
_loadAreaVfxHook.Enable();
|
|
||||||
_scheduleClipUpdateHook.Enable();
|
|
||||||
_unkMountAnimationHook.Enable();
|
|
||||||
_unkParasolAnimationHook.Enable();
|
|
||||||
_dismountHook.Enable();
|
|
||||||
_apricotListenerSoundPlayHook.Enable();
|
|
||||||
_footStepHook.Enable();
|
|
||||||
}
|
|
||||||
|
|
||||||
public bool HandleFiles(ResourceType type, Utf8GamePath _, out ResolveData resolveData)
|
|
||||||
{
|
|
||||||
switch (type)
|
|
||||||
{
|
|
||||||
case ResourceType.Scd:
|
|
||||||
if (_characterSoundData is { IsValueCreated: true, Value.Valid: true })
|
|
||||||
{
|
|
||||||
resolveData = _characterSoundData.Value;
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (_animationLoadData is { IsValueCreated: true, Value.Valid: true })
|
|
||||||
{
|
|
||||||
resolveData = _animationLoadData.Value;
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
break;
|
|
||||||
case ResourceType.Tmb:
|
|
||||||
case ResourceType.Pap:
|
|
||||||
case ResourceType.Avfx:
|
|
||||||
case ResourceType.Atex:
|
|
||||||
if (_animationLoadData is { IsValueCreated: true, Value.Valid: true })
|
|
||||||
{
|
|
||||||
resolveData = _animationLoadData.Value;
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
var lastObj = _drawObjectState.LastGameObject;
|
|
||||||
if (lastObj != nint.Zero)
|
|
||||||
{
|
|
||||||
resolveData = _resolver.IdentifyCollection((GameObject*)lastObj, true);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
resolveData = ResolveData.Invalid;
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void Dispose()
|
|
||||||
{
|
|
||||||
_loadCharacterSoundHook.Dispose();
|
|
||||||
_loadTimelineResourcesHook.Dispose();
|
|
||||||
_characterBaseLoadAnimationHook.Dispose();
|
|
||||||
_loadSomePapHook.Dispose();
|
|
||||||
_someActionLoadHook.Dispose();
|
|
||||||
_loadCharacterVfxHook.Dispose();
|
|
||||||
_loadAreaVfxHook.Dispose();
|
|
||||||
_scheduleClipUpdateHook.Dispose();
|
|
||||||
_unkMountAnimationHook.Dispose();
|
|
||||||
_unkParasolAnimationHook.Dispose();
|
|
||||||
_dismountHook.Dispose();
|
|
||||||
_apricotListenerSoundPlayHook.Dispose();
|
|
||||||
_footStepHook.Dispose();
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary> Characters load some of their voice lines or whatever with this function. </summary>
|
|
||||||
private delegate nint LoadCharacterSound(nint character, int unk1, int unk2, nint unk3, ulong unk4, int unk5, int unk6, ulong unk7);
|
|
||||||
|
|
||||||
private readonly Hook<LoadCharacterSound> _loadCharacterSoundHook;
|
|
||||||
|
|
||||||
private nint LoadCharacterSoundDetour(nint container, int unk1, int unk2, nint unk3, ulong unk4, int unk5, int unk6, ulong unk7)
|
|
||||||
{
|
|
||||||
using var performance = _performance.Measure(PerformanceType.LoadSound);
|
|
||||||
var last = _characterSoundData.Value;
|
|
||||||
var character = *(GameObject**)(container + 8);
|
|
||||||
_characterSoundData.Value = _collectionResolver.IdentifyCollection(character, true);
|
|
||||||
var ret = _loadCharacterSoundHook.Original(container, unk1, unk2, unk3, unk4, unk5, unk6, unk7);
|
|
||||||
_characterSoundData.Value = last;
|
|
||||||
return ret;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// The timeline object loads the requested .tmb and .pap files. The .tmb files load the respective .avfx files.
|
|
||||||
/// We can obtain the associated game object from the timelines 28'th vfunc and use that to apply the correct collection.
|
|
||||||
/// </summary>
|
|
||||||
private delegate ulong LoadTimelineResourcesDelegate(nint timeline);
|
|
||||||
|
|
||||||
[Signature(Sigs.LoadTimelineResources, DetourName = nameof(LoadTimelineResourcesDetour))]
|
|
||||||
private readonly Hook<LoadTimelineResourcesDelegate> _loadTimelineResourcesHook = null!;
|
|
||||||
|
|
||||||
private ulong LoadTimelineResourcesDetour(nint timeline)
|
|
||||||
{
|
|
||||||
using var performance = _performance.Measure(PerformanceType.TimelineResources);
|
|
||||||
// Do not check timeline loading in cutscenes.
|
|
||||||
if (_conditions[ConditionFlag.OccupiedInCutSceneEvent] || _conditions[ConditionFlag.WatchingCutscene78])
|
|
||||||
return _loadTimelineResourcesHook.Original(timeline);
|
|
||||||
|
|
||||||
var last = _animationLoadData.Value;
|
|
||||||
_animationLoadData.Value = GetDataFromTimeline(timeline);
|
|
||||||
var ret = _loadTimelineResourcesHook.Original(timeline);
|
|
||||||
_animationLoadData.Value = last;
|
|
||||||
return ret;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Probably used when the base idle animation gets loaded.
|
|
||||||
/// Make it aware of the correct collection to load the correct pap files.
|
|
||||||
/// </summary>
|
|
||||||
private delegate void CharacterBaseNoArgumentDelegate(nint drawBase);
|
|
||||||
|
|
||||||
[Signature(Sigs.CharacterBaseLoadAnimation, DetourName = nameof(CharacterBaseLoadAnimationDetour))]
|
|
||||||
private readonly Hook<CharacterBaseNoArgumentDelegate> _characterBaseLoadAnimationHook = null!;
|
|
||||||
|
|
||||||
private void CharacterBaseLoadAnimationDetour(nint drawObject)
|
|
||||||
{
|
|
||||||
using var performance = _performance.Measure(PerformanceType.LoadCharacterBaseAnimation);
|
|
||||||
var last = _animationLoadData.Value;
|
|
||||||
var lastObj = _drawObjectState.LastGameObject;
|
|
||||||
if (lastObj == nint.Zero && _drawObjectState.TryGetValue(drawObject, out var p))
|
|
||||||
lastObj = p.Item1;
|
|
||||||
_animationLoadData.Value = _collectionResolver.IdentifyCollection((GameObject*)lastObj, true);
|
|
||||||
_characterBaseLoadAnimationHook.Original(drawObject);
|
|
||||||
_animationLoadData.Value = last;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary> Unknown what exactly this is but it seems to load a bunch of paps. </summary>
|
|
||||||
private delegate void LoadSomePap(nint a1, int a2, nint a3, int a4);
|
|
||||||
|
|
||||||
[Signature(Sigs.LoadSomePap, DetourName = nameof(LoadSomePapDetour))]
|
|
||||||
private readonly Hook<LoadSomePap> _loadSomePapHook = null!;
|
|
||||||
|
|
||||||
private void LoadSomePapDetour(nint a1, int a2, nint a3, int a4)
|
|
||||||
{
|
|
||||||
using var performance = _performance.Measure(PerformanceType.LoadPap);
|
|
||||||
var timelinePtr = a1 + Offsets.TimeLinePtr;
|
|
||||||
var last = _animationLoadData.Value;
|
|
||||||
if (timelinePtr != nint.Zero)
|
|
||||||
{
|
|
||||||
var actorIdx = (int)(*(*(ulong**)timelinePtr + 1) >> 3);
|
|
||||||
if (actorIdx >= 0 && actorIdx < _objects.Length)
|
|
||||||
_animationLoadData.Value = _collectionResolver.IdentifyCollection((GameObject*)_objects.GetObjectAddress(actorIdx), true);
|
|
||||||
}
|
|
||||||
|
|
||||||
_loadSomePapHook.Original(a1, a2, a3, a4);
|
|
||||||
_animationLoadData.Value = last;
|
|
||||||
}
|
|
||||||
|
|
||||||
private delegate void SomeActionLoadDelegate(ActionTimelineManager* timelineManager);
|
|
||||||
|
|
||||||
/// <summary> Seems to load character actions when zoning or changing class, maybe. </summary>
|
|
||||||
[Signature(Sigs.LoadSomeAction, DetourName = nameof(SomeActionLoadDetour))]
|
|
||||||
private readonly Hook<SomeActionLoadDelegate> _someActionLoadHook = null!;
|
|
||||||
|
|
||||||
private void SomeActionLoadDetour(ActionTimelineManager* timelineManager)
|
|
||||||
{
|
|
||||||
using var performance = _performance.Measure(PerformanceType.LoadAction);
|
|
||||||
var last = _animationLoadData.Value;
|
|
||||||
_animationLoadData.Value = _collectionResolver.IdentifyCollection((GameObject*)timelineManager->Parent, true);
|
|
||||||
_someActionLoadHook.Original(timelineManager);
|
|
||||||
_animationLoadData.Value = last;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary> Load a VFX specifically for a character. </summary>
|
|
||||||
private delegate nint LoadCharacterVfxDelegate(byte* vfxPath, VfxParams* vfxParams, byte unk1, byte unk2, float unk3, int unk4);
|
|
||||||
|
|
||||||
[Signature(Sigs.LoadCharacterVfx, DetourName = nameof(LoadCharacterVfxDetour))]
|
|
||||||
private readonly Hook<LoadCharacterVfxDelegate> _loadCharacterVfxHook = null!;
|
|
||||||
|
|
||||||
private nint LoadCharacterVfxDetour(byte* vfxPath, VfxParams* vfxParams, byte unk1, byte unk2, float unk3, int unk4)
|
|
||||||
{
|
|
||||||
using var performance = _performance.Measure(PerformanceType.LoadCharacterVfx);
|
|
||||||
var last = _animationLoadData.Value;
|
|
||||||
if (vfxParams != null && vfxParams->GameObjectId != unchecked((uint)-1))
|
|
||||||
{
|
|
||||||
var obj = vfxParams->GameObjectType switch
|
|
||||||
{
|
|
||||||
0 => _objects.SearchById(vfxParams->GameObjectId),
|
|
||||||
2 => _objects[(int)vfxParams->GameObjectId],
|
|
||||||
4 => GetOwnedObject(vfxParams->GameObjectId),
|
|
||||||
_ => null,
|
|
||||||
};
|
|
||||||
_animationLoadData.Value = obj != null
|
|
||||||
? _collectionResolver.IdentifyCollection((GameObject*)obj.Address, true)
|
|
||||||
: ResolveData.Invalid;
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
_animationLoadData.Value = ResolveData.Invalid;
|
|
||||||
}
|
|
||||||
|
|
||||||
var ret = _loadCharacterVfxHook.Original(vfxPath, vfxParams, unk1, unk2, unk3, unk4);
|
|
||||||
Penumbra.Log.Excessive(
|
|
||||||
$"Load Character VFX: {new ByteString(vfxPath)} 0x{vfxParams->GameObjectId:X} {vfxParams->TargetCount} {unk1} {unk2} {unk3} {unk4} -> "
|
|
||||||
+ $"0x{ret:X} {_animationLoadData.Value.ModCollection.Name} {_animationLoadData.Value.AssociatedGameObject} {last.ModCollection.Name} {last.AssociatedGameObject}");
|
|
||||||
_animationLoadData.Value = last;
|
|
||||||
return ret;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary> Load a ground-based area VFX. </summary>
|
|
||||||
private delegate nint LoadAreaVfxDelegate(uint vfxId, float* pos, GameObject* caster, float unk1, float unk2, byte unk3);
|
|
||||||
|
|
||||||
[Signature(Sigs.LoadAreaVfx, DetourName = nameof(LoadAreaVfxDetour))]
|
|
||||||
private readonly Hook<LoadAreaVfxDelegate> _loadAreaVfxHook = null!;
|
|
||||||
|
|
||||||
private nint LoadAreaVfxDetour(uint vfxId, float* pos, GameObject* caster, float unk1, float unk2, byte unk3)
|
|
||||||
{
|
|
||||||
using var performance = _performance.Measure(PerformanceType.LoadAreaVfx);
|
|
||||||
var last = _animationLoadData.Value;
|
|
||||||
_animationLoadData.Value = caster != null
|
|
||||||
? _collectionResolver.IdentifyCollection(caster, true)
|
|
||||||
: ResolveData.Invalid;
|
|
||||||
|
|
||||||
var ret = _loadAreaVfxHook.Original(vfxId, pos, caster, unk1, unk2, unk3);
|
|
||||||
Penumbra.Log.Excessive(
|
|
||||||
$"Load Area VFX: {vfxId}, {pos[0]} {pos[1]} {pos[2]} {(caster != null ? new ByteString(caster->GetName()).ToString() : "Unknown")} {unk1} {unk2} {unk3}"
|
|
||||||
+ $" -> {ret:X} {_animationLoadData.Value.ModCollection.Name} {_animationLoadData.Value.AssociatedGameObject} {last.ModCollection.Name} {last.AssociatedGameObject}");
|
|
||||||
_animationLoadData.Value = last;
|
|
||||||
return ret;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
/// <summary> Called when some action timelines update. </summary>
|
|
||||||
private delegate void ScheduleClipUpdate(ClipScheduler* x);
|
|
||||||
|
|
||||||
[Signature(Sigs.ScheduleClipUpdate, DetourName = nameof(ScheduleClipUpdateDetour))]
|
|
||||||
private readonly Hook<ScheduleClipUpdate> _scheduleClipUpdateHook = null!;
|
|
||||||
|
|
||||||
private void ScheduleClipUpdateDetour(ClipScheduler* x)
|
|
||||||
{
|
|
||||||
using var performance = _performance.Measure(PerformanceType.ScheduleClipUpdate);
|
|
||||||
var last = _animationLoadData.Value;
|
|
||||||
var timeline = x->SchedulerTimeline;
|
|
||||||
_animationLoadData.Value = GetDataFromTimeline(timeline);
|
|
||||||
_scheduleClipUpdateHook.Original(x);
|
|
||||||
_animationLoadData.Value = last;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary> Search an object by its id, then get its minion/mount/ornament. </summary>
|
|
||||||
private Dalamud.Game.ClientState.Objects.Types.GameObject? GetOwnedObject(uint id)
|
|
||||||
{
|
|
||||||
var owner = _objects.SearchById(id);
|
|
||||||
if (owner == null)
|
|
||||||
return null;
|
|
||||||
|
|
||||||
var idx = ((GameObject*)owner.Address)->ObjectIndex;
|
|
||||||
return _objects[idx + 1];
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary> Use timelines vfuncs to obtain the associated game object. </summary>
|
|
||||||
private ResolveData GetDataFromTimeline(nint timeline)
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
if (timeline != nint.Zero)
|
|
||||||
{
|
|
||||||
var getGameObjectIdx = ((delegate* unmanaged<nint, int>**)timeline)[0][Offsets.GetGameObjectIdxVfunc];
|
|
||||||
var idx = getGameObjectIdx(timeline);
|
|
||||||
if (idx >= 0 && idx < _objects.Length)
|
|
||||||
{
|
|
||||||
var obj = (GameObject*)_objects.GetObjectAddress(idx);
|
|
||||||
return obj != null ? _collectionResolver.IdentifyCollection(obj, true) : ResolveData.Invalid;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
catch (Exception e)
|
|
||||||
{
|
|
||||||
Penumbra.Log.Error($"Error getting timeline data for 0x{timeline:X}:\n{e}");
|
|
||||||
}
|
|
||||||
|
|
||||||
return ResolveData.Invalid;
|
|
||||||
}
|
|
||||||
|
|
||||||
private delegate void UnkMountAnimationDelegate(DrawObject* drawObject, uint unk1, byte unk2, uint unk3);
|
|
||||||
|
|
||||||
[Signature(Sigs.UnkMountAnimation, DetourName = nameof(UnkMountAnimationDetour))]
|
|
||||||
private readonly Hook<UnkMountAnimationDelegate> _unkMountAnimationHook = null!;
|
|
||||||
|
|
||||||
private void UnkMountAnimationDetour(DrawObject* drawObject, uint unk1, byte unk2, uint unk3)
|
|
||||||
{
|
|
||||||
var last = _animationLoadData.Value;
|
|
||||||
_animationLoadData.Value = _collectionResolver.IdentifyCollection(drawObject, true);
|
|
||||||
_unkMountAnimationHook.Original(drawObject, unk1, unk2, unk3);
|
|
||||||
_animationLoadData.Value = last;
|
|
||||||
}
|
|
||||||
|
|
||||||
private delegate void UnkParasolAnimationDelegate(DrawObject* drawObject, int unk1);
|
|
||||||
|
|
||||||
[Signature(Sigs.UnkParasolAnimation, DetourName = nameof(UnkParasolAnimationDetour))]
|
|
||||||
private readonly Hook<UnkParasolAnimationDelegate> _unkParasolAnimationHook = null!;
|
|
||||||
|
|
||||||
private void UnkParasolAnimationDetour(DrawObject* drawObject, int unk1)
|
|
||||||
{
|
|
||||||
var last = _animationLoadData.Value;
|
|
||||||
_animationLoadData.Value = _collectionResolver.IdentifyCollection(drawObject, true);
|
|
||||||
_unkParasolAnimationHook.Original(drawObject, unk1);
|
|
||||||
_animationLoadData.Value = last;
|
|
||||||
}
|
|
||||||
|
|
||||||
[Signature(Sigs.Dismount, DetourName = nameof(DismountDetour))]
|
|
||||||
private readonly Hook<DismountDelegate> _dismountHook = null!;
|
|
||||||
|
|
||||||
private delegate void DismountDelegate(nint a1, nint a2);
|
|
||||||
|
|
||||||
private void DismountDetour(nint a1, nint a2)
|
|
||||||
{
|
|
||||||
if (a1 == nint.Zero)
|
|
||||||
{
|
|
||||||
_dismountHook.Original(a1, a2);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
var gameObject = *(GameObject**)(a1 + 8);
|
|
||||||
if (gameObject == null)
|
|
||||||
{
|
|
||||||
_dismountHook.Original(a1, a2);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
var last = _animationLoadData.Value;
|
|
||||||
_animationLoadData.Value = _collectionResolver.IdentifyCollection(gameObject, true);
|
|
||||||
_dismountHook.Original(a1, a2);
|
|
||||||
_animationLoadData.Value = last;
|
|
||||||
}
|
|
||||||
|
|
||||||
[Signature(Sigs.ApricotListenerSoundPlay, DetourName = nameof(ApricotListenerSoundPlayDetour))]
|
|
||||||
private readonly Hook<ApricotListenerSoundPlayDelegate> _apricotListenerSoundPlayHook = null!;
|
|
||||||
|
|
||||||
private delegate nint ApricotListenerSoundPlayDelegate(nint a1, nint a2, nint a3, nint a4, nint a5, nint a6);
|
|
||||||
|
|
||||||
private nint ApricotListenerSoundPlayDetour(nint a1, nint a2, nint a3, nint a4, nint a5, nint a6)
|
|
||||||
{
|
|
||||||
if (a6 == nint.Zero)
|
|
||||||
return _apricotListenerSoundPlayHook!.Original(a1, a2, a3, a4, a5, a6);
|
|
||||||
|
|
||||||
var last = _animationLoadData.Value;
|
|
||||||
// a6 is some instance of Apricot.IInstanceListenner, in some cases we can obtain the associated caster via vfunc 1.
|
|
||||||
var gameObject = (*(delegate* unmanaged<nint, GameObject*>**)a6)[1](a6);
|
|
||||||
if (gameObject != null)
|
|
||||||
{
|
|
||||||
_animationLoadData.Value = _collectionResolver.IdentifyCollection(gameObject, true);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
// for VfxListenner we can obtain the associated draw object as its first member,
|
|
||||||
// if the object has different type, drawObject will contain other values or garbage,
|
|
||||||
// but only be used in a dictionary pointer lookup, so this does not hurt.
|
|
||||||
var drawObject = ((DrawObject**)a6)[1];
|
|
||||||
if (drawObject != null)
|
|
||||||
_animationLoadData.Value = _collectionResolver.IdentifyCollection(drawObject, true);
|
|
||||||
}
|
|
||||||
|
|
||||||
var ret = _apricotListenerSoundPlayHook!.Original(a1, a2, a3, a4, a5, a6);
|
|
||||||
_animationLoadData.Value = last;
|
|
||||||
return ret;
|
|
||||||
}
|
|
||||||
|
|
||||||
private delegate void FootStepDelegate(GameObject* gameObject, int id, int unk);
|
|
||||||
|
|
||||||
[Signature(Sigs.FootStepSound, DetourName = nameof(FootStepDetour))]
|
|
||||||
private readonly Hook<FootStepDelegate> _footStepHook = null!;
|
|
||||||
|
|
||||||
private void FootStepDetour(GameObject* gameObject, int id, int unk)
|
|
||||||
{
|
|
||||||
var last = _animationLoadData.Value;
|
|
||||||
_animationLoadData.Value = _collectionResolver.IdentifyCollection(gameObject, true);
|
|
||||||
_footStepHook.Original(gameObject, id, unk);
|
|
||||||
_animationLoadData.Value = last;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,11 +1,11 @@
|
||||||
using Dalamud.Plugin.Services;
|
using Dalamud.Plugin.Services;
|
||||||
using FFXIVClientStructs.FFXIV.Client.Graphics.Scene;
|
using FFXIVClientStructs.FFXIV.Client.Graphics.Scene;
|
||||||
|
using OtterGui.Services;
|
||||||
using Penumbra.Collections;
|
using Penumbra.Collections;
|
||||||
using Penumbra.Collections.Manager;
|
using Penumbra.Collections.Manager;
|
||||||
using Penumbra.GameData.Actors;
|
using Penumbra.GameData.Actors;
|
||||||
using Penumbra.GameData.DataContainers;
|
using Penumbra.GameData.DataContainers;
|
||||||
using Penumbra.GameData.Enums;
|
using Penumbra.GameData.Enums;
|
||||||
using Penumbra.Services;
|
|
||||||
using Penumbra.Util;
|
using Penumbra.Util;
|
||||||
using Character = FFXIVClientStructs.FFXIV.Client.Game.Character.Character;
|
using Character = FFXIVClientStructs.FFXIV.Client.Game.Character.Character;
|
||||||
using GameObject = FFXIVClientStructs.FFXIV.Client.Game.Object.GameObject;
|
using GameObject = FFXIVClientStructs.FFXIV.Client.Game.Object.GameObject;
|
||||||
|
|
@ -13,70 +13,51 @@ using ObjectKind = Dalamud.Game.ClientState.Objects.Enums.ObjectKind;
|
||||||
|
|
||||||
namespace Penumbra.Interop.PathResolving;
|
namespace Penumbra.Interop.PathResolving;
|
||||||
|
|
||||||
public unsafe class CollectionResolver
|
public sealed unsafe class CollectionResolver(
|
||||||
|
PerformanceTracker performance,
|
||||||
|
IdentifiedCollectionCache cache,
|
||||||
|
IClientState clientState,
|
||||||
|
IGameGui gameGui,
|
||||||
|
ActorManager actors,
|
||||||
|
CutsceneService cutscenes,
|
||||||
|
Configuration config,
|
||||||
|
CollectionManager collectionManager,
|
||||||
|
TempCollectionManager tempCollections,
|
||||||
|
DrawObjectState drawObjectState,
|
||||||
|
HumanModelList humanModels)
|
||||||
|
: IService
|
||||||
{
|
{
|
||||||
private readonly PerformanceTracker _performance;
|
|
||||||
private readonly IdentifiedCollectionCache _cache;
|
|
||||||
private readonly HumanModelList _humanModels;
|
|
||||||
|
|
||||||
private readonly IClientState _clientState;
|
|
||||||
private readonly IGameGui _gameGui;
|
|
||||||
private readonly ActorManager _actors;
|
|
||||||
private readonly CutsceneService _cutscenes;
|
|
||||||
|
|
||||||
private readonly Configuration _config;
|
|
||||||
private readonly CollectionManager _collectionManager;
|
|
||||||
private readonly TempCollectionManager _tempCollections;
|
|
||||||
private readonly DrawObjectState _drawObjectState;
|
|
||||||
|
|
||||||
public CollectionResolver(PerformanceTracker performance, IdentifiedCollectionCache cache, IClientState clientState, IGameGui gameGui,
|
|
||||||
ActorManager actors, CutsceneService cutscenes, Configuration config, CollectionManager collectionManager,
|
|
||||||
TempCollectionManager tempCollections, DrawObjectState drawObjectState, HumanModelList humanModels)
|
|
||||||
{
|
|
||||||
_performance = performance;
|
|
||||||
_cache = cache;
|
|
||||||
_clientState = clientState;
|
|
||||||
_gameGui = gameGui;
|
|
||||||
_actors = actors;
|
|
||||||
_cutscenes = cutscenes;
|
|
||||||
_config = config;
|
|
||||||
_collectionManager = collectionManager;
|
|
||||||
_tempCollections = tempCollections;
|
|
||||||
_drawObjectState = drawObjectState;
|
|
||||||
_humanModels = humanModels;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Get the collection applying to the current player character
|
/// Get the collection applying to the current player character
|
||||||
/// or the Yourself or Default collection if no player exists.
|
/// or the 'Yourself' or 'Default' collection if no player exists.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public ModCollection PlayerCollection()
|
public ModCollection PlayerCollection()
|
||||||
{
|
{
|
||||||
using var performance = _performance.Measure(PerformanceType.IdentifyCollection);
|
using var performance1 = performance.Measure(PerformanceType.IdentifyCollection);
|
||||||
var gameObject = (GameObject*)(_clientState.LocalPlayer?.Address ?? nint.Zero);
|
var gameObject = (GameObject*)(clientState.LocalPlayer?.Address ?? nint.Zero);
|
||||||
if (gameObject == null)
|
if (gameObject == null)
|
||||||
return _collectionManager.Active.ByType(CollectionType.Yourself)
|
return collectionManager.Active.ByType(CollectionType.Yourself)
|
||||||
?? _collectionManager.Active.Default;
|
?? collectionManager.Active.Default;
|
||||||
|
|
||||||
var player = _actors.GetCurrentPlayer();
|
var player = actors.GetCurrentPlayer();
|
||||||
var _ = false;
|
var _ = false;
|
||||||
return CollectionByIdentifier(player)
|
return CollectionByIdentifier(player)
|
||||||
?? CheckYourself(player, gameObject)
|
?? CheckYourself(player, gameObject)
|
||||||
?? CollectionByAttributes(gameObject, ref _)
|
?? CollectionByAttributes(gameObject, ref _)
|
||||||
?? _collectionManager.Active.Default;
|
?? collectionManager.Active.Default;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary> Identify the correct collection for a game object. </summary>
|
/// <summary> Identify the correct collection for a game object. </summary>
|
||||||
public ResolveData IdentifyCollection(GameObject* gameObject, bool useCache)
|
public ResolveData IdentifyCollection(GameObject* gameObject, bool useCache)
|
||||||
{
|
{
|
||||||
using var t = _performance.Measure(PerformanceType.IdentifyCollection);
|
using var t = performance.Measure(PerformanceType.IdentifyCollection);
|
||||||
|
|
||||||
if (gameObject == null)
|
if (gameObject == null)
|
||||||
return _collectionManager.Active.Default.ToResolveData();
|
return collectionManager.Active.Default.ToResolveData();
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
if (useCache && _cache.TryGetValue(gameObject, out var data))
|
if (useCache && cache.TryGetValue(gameObject, out var data))
|
||||||
return data;
|
return data;
|
||||||
|
|
||||||
if (LoginScreen(gameObject, out data))
|
if (LoginScreen(gameObject, out data))
|
||||||
|
|
@ -90,26 +71,26 @@ public unsafe class CollectionResolver
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
Penumbra.Log.Error($"Error identifying collection:\n{ex}");
|
Penumbra.Log.Error($"Error identifying collection:\n{ex}");
|
||||||
return _collectionManager.Active.Default.ToResolveData(gameObject);
|
return collectionManager.Active.Default.ToResolveData(gameObject);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary> Identify the correct collection for the last created game object. </summary>
|
/// <summary> Identify the correct collection for the last created game object. </summary>
|
||||||
public ResolveData IdentifyLastGameObjectCollection(bool useCache)
|
public ResolveData IdentifyLastGameObjectCollection(bool useCache)
|
||||||
=> IdentifyCollection((GameObject*)_drawObjectState.LastGameObject, useCache);
|
=> IdentifyCollection((GameObject*)drawObjectState.LastGameObject, useCache);
|
||||||
|
|
||||||
/// <summary> Identify the correct collection for a draw object. </summary>
|
/// <summary> Identify the correct collection for a draw object. </summary>
|
||||||
public ResolveData IdentifyCollection(DrawObject* drawObject, bool useCache)
|
public ResolveData IdentifyCollection(DrawObject* drawObject, bool useCache)
|
||||||
{
|
{
|
||||||
var obj = (GameObject*)(_drawObjectState.TryGetValue((nint)drawObject, out var gameObject)
|
var obj = (GameObject*)(drawObjectState.TryGetValue((nint)drawObject, out var gameObject)
|
||||||
? gameObject.Item1
|
? gameObject.Item1
|
||||||
: _drawObjectState.LastGameObject);
|
: drawObjectState.LastGameObject);
|
||||||
return IdentifyCollection(obj, useCache);
|
return IdentifyCollection(obj, useCache);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary> Return whether the given ModelChara id refers to a human-type model. </summary>
|
/// <summary> Return whether the given ModelChara id refers to a human-type model. </summary>
|
||||||
public bool IsModelHuman(uint modelCharaId)
|
public bool IsModelHuman(uint modelCharaId)
|
||||||
=> _humanModels.IsHuman(modelCharaId);
|
=> humanModels.IsHuman(modelCharaId);
|
||||||
|
|
||||||
/// <summary> Return whether the given character has a human model. </summary>
|
/// <summary> Return whether the given character has a human model. </summary>
|
||||||
public bool IsModelHuman(Character* character)
|
public bool IsModelHuman(Character* character)
|
||||||
|
|
@ -124,36 +105,36 @@ public unsafe class CollectionResolver
|
||||||
{
|
{
|
||||||
// Also check for empty names because sometimes named other characters
|
// Also check for empty names because sometimes named other characters
|
||||||
// might be loaded before being officially logged in.
|
// might be loaded before being officially logged in.
|
||||||
if (_clientState.IsLoggedIn || gameObject->Name[0] != '\0')
|
if (clientState.IsLoggedIn || gameObject->Name[0] != '\0')
|
||||||
{
|
{
|
||||||
ret = ResolveData.Invalid;
|
ret = ResolveData.Invalid;
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
var notYetReady = false;
|
var notYetReady = false;
|
||||||
var collection = _collectionManager.Active.ByType(CollectionType.Yourself)
|
var collection = collectionManager.Active.ByType(CollectionType.Yourself)
|
||||||
?? CollectionByAttributes(gameObject, ref notYetReady)
|
?? CollectionByAttributes(gameObject, ref notYetReady)
|
||||||
?? _collectionManager.Active.Default;
|
?? collectionManager.Active.Default;
|
||||||
ret = notYetReady ? collection.ToResolveData(gameObject) : _cache.Set(collection, ActorIdentifier.Invalid, gameObject);
|
ret = notYetReady ? collection.ToResolveData(gameObject) : cache.Set(collection, ActorIdentifier.Invalid, gameObject);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary> Used if at the aesthetician. The relevant actor is yourself, so use player collection when possible. </summary>
|
/// <summary> Used if at the aesthetician. The relevant actor is yourself, so use player collection when possible. </summary>
|
||||||
private bool Aesthetician(GameObject* gameObject, out ResolveData ret)
|
private bool Aesthetician(GameObject* gameObject, out ResolveData ret)
|
||||||
{
|
{
|
||||||
if (_gameGui.GetAddonByName("ScreenLog") != IntPtr.Zero)
|
if (gameGui.GetAddonByName("ScreenLog") != IntPtr.Zero)
|
||||||
{
|
{
|
||||||
ret = ResolveData.Invalid;
|
ret = ResolveData.Invalid;
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
var player = _actors.GetCurrentPlayer();
|
var player = actors.GetCurrentPlayer();
|
||||||
var notYetReady = false;
|
var notYetReady = false;
|
||||||
var collection = (player.IsValid ? CollectionByIdentifier(player) : null)
|
var collection = (player.IsValid ? CollectionByIdentifier(player) : null)
|
||||||
?? _collectionManager.Active.ByType(CollectionType.Yourself)
|
?? collectionManager.Active.ByType(CollectionType.Yourself)
|
||||||
?? CollectionByAttributes(gameObject, ref notYetReady)
|
?? CollectionByAttributes(gameObject, ref notYetReady)
|
||||||
?? _collectionManager.Active.Default;
|
?? collectionManager.Active.Default;
|
||||||
ret = notYetReady ? collection.ToResolveData(gameObject) : _cache.Set(collection, ActorIdentifier.Invalid, gameObject);
|
ret = notYetReady ? collection.ToResolveData(gameObject) : cache.Set(collection, ActorIdentifier.Invalid, gameObject);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -163,12 +144,12 @@ public unsafe class CollectionResolver
|
||||||
/// </summary>
|
/// </summary>
|
||||||
private ResolveData DefaultState(GameObject* gameObject)
|
private ResolveData DefaultState(GameObject* gameObject)
|
||||||
{
|
{
|
||||||
var identifier = _actors.FromObject(gameObject, out var owner, true, false, false);
|
var identifier = actors.FromObject(gameObject, out var owner, true, false, false);
|
||||||
if (identifier.Type is IdentifierType.Special)
|
if (identifier.Type is IdentifierType.Special)
|
||||||
{
|
{
|
||||||
(identifier, var type) = _collectionManager.Active.Individuals.ConvertSpecialIdentifier(identifier);
|
(identifier, var type) = collectionManager.Active.Individuals.ConvertSpecialIdentifier(identifier);
|
||||||
if (_config.UseNoModsInInspect && type == IndividualCollections.SpecialResult.Inspect)
|
if (config.UseNoModsInInspect && type == IndividualCollections.SpecialResult.Inspect)
|
||||||
return _cache.Set(ModCollection.Empty, identifier, gameObject);
|
return cache.Set(ModCollection.Empty, identifier, gameObject);
|
||||||
}
|
}
|
||||||
|
|
||||||
var notYetReady = false;
|
var notYetReady = false;
|
||||||
|
|
@ -176,15 +157,15 @@ public unsafe class CollectionResolver
|
||||||
?? CheckYourself(identifier, gameObject)
|
?? CheckYourself(identifier, gameObject)
|
||||||
?? CollectionByAttributes(gameObject, ref notYetReady)
|
?? CollectionByAttributes(gameObject, ref notYetReady)
|
||||||
?? CheckOwnedCollection(identifier, owner, ref notYetReady)
|
?? CheckOwnedCollection(identifier, owner, ref notYetReady)
|
||||||
?? _collectionManager.Active.Default;
|
?? collectionManager.Active.Default;
|
||||||
|
|
||||||
return notYetReady ? collection.ToResolveData(gameObject) : _cache.Set(collection, identifier, gameObject);
|
return notYetReady ? collection.ToResolveData(gameObject) : cache.Set(collection, identifier, gameObject);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary> Check both temporary and permanent character collections. Temporary first. </summary>
|
/// <summary> Check both temporary and permanent character collections. Temporary first. </summary>
|
||||||
private ModCollection? CollectionByIdentifier(ActorIdentifier identifier)
|
private ModCollection? CollectionByIdentifier(ActorIdentifier identifier)
|
||||||
=> _tempCollections.Collections.TryGetCollection(identifier, out var collection)
|
=> tempCollections.Collections.TryGetCollection(identifier, out var collection)
|
||||||
|| _collectionManager.Active.Individuals.TryGetCollection(identifier, out collection)
|
|| collectionManager.Active.Individuals.TryGetCollection(identifier, out collection)
|
||||||
? collection
|
? collection
|
||||||
: null;
|
: null;
|
||||||
|
|
||||||
|
|
@ -192,9 +173,9 @@ public unsafe class CollectionResolver
|
||||||
private ModCollection? CheckYourself(ActorIdentifier identifier, GameObject* actor)
|
private ModCollection? CheckYourself(ActorIdentifier identifier, GameObject* actor)
|
||||||
{
|
{
|
||||||
if (actor->ObjectIndex == 0
|
if (actor->ObjectIndex == 0
|
||||||
|| _cutscenes.GetParentIndex(actor->ObjectIndex) == 0
|
|| cutscenes.GetParentIndex(actor->ObjectIndex) == 0
|
||||||
|| identifier.Equals(_actors.GetCurrentPlayer()))
|
|| identifier.Equals(actors.GetCurrentPlayer()))
|
||||||
return _collectionManager.Active.ByType(CollectionType.Yourself);
|
return collectionManager.Active.ByType(CollectionType.Yourself);
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
@ -219,8 +200,8 @@ public unsafe class CollectionResolver
|
||||||
var bodyType = character->DrawData.CustomizeData[2];
|
var bodyType = character->DrawData.CustomizeData[2];
|
||||||
var collection = bodyType switch
|
var collection = bodyType switch
|
||||||
{
|
{
|
||||||
3 => _collectionManager.Active.ByType(CollectionType.NonPlayerElderly),
|
3 => collectionManager.Active.ByType(CollectionType.NonPlayerElderly),
|
||||||
4 => _collectionManager.Active.ByType(CollectionType.NonPlayerChild),
|
4 => collectionManager.Active.ByType(CollectionType.NonPlayerChild),
|
||||||
_ => null,
|
_ => null,
|
||||||
};
|
};
|
||||||
if (collection != null)
|
if (collection != null)
|
||||||
|
|
@ -231,18 +212,18 @@ public unsafe class CollectionResolver
|
||||||
var isNpc = actor->ObjectKind != (byte)ObjectKind.Player;
|
var isNpc = actor->ObjectKind != (byte)ObjectKind.Player;
|
||||||
|
|
||||||
var type = CollectionTypeExtensions.FromParts(race, gender, isNpc);
|
var type = CollectionTypeExtensions.FromParts(race, gender, isNpc);
|
||||||
collection = _collectionManager.Active.ByType(type);
|
collection = collectionManager.Active.ByType(type);
|
||||||
collection ??= _collectionManager.Active.ByType(CollectionTypeExtensions.FromParts(gender, isNpc));
|
collection ??= collectionManager.Active.ByType(CollectionTypeExtensions.FromParts(gender, isNpc));
|
||||||
return collection;
|
return collection;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary> Get the collection applying to the owner if it is available. </summary>
|
/// <summary> Get the collection applying to the owner if it is available. </summary>
|
||||||
private ModCollection? CheckOwnedCollection(ActorIdentifier identifier, GameObject* owner, ref bool notYetReady)
|
private ModCollection? CheckOwnedCollection(ActorIdentifier identifier, GameObject* owner, ref bool notYetReady)
|
||||||
{
|
{
|
||||||
if (identifier.Type != IdentifierType.Owned || !_config.UseOwnerNameForCharacterCollection || owner == null)
|
if (identifier.Type != IdentifierType.Owned || !config.UseOwnerNameForCharacterCollection || owner == null)
|
||||||
return null;
|
return null;
|
||||||
|
|
||||||
var id = _actors.CreateIndividualUnchecked(IdentifierType.Player, identifier.PlayerName, identifier.HomeWorld.Id,
|
var id = actors.CreateIndividualUnchecked(IdentifierType.Player, identifier.PlayerName, identifier.HomeWorld.Id,
|
||||||
ObjectKind.None,
|
ObjectKind.None,
|
||||||
uint.MaxValue);
|
uint.MaxValue);
|
||||||
return CheckYourself(id, owner)
|
return CheckYourself(id, owner)
|
||||||
|
|
|
||||||
|
|
@ -1,31 +1,34 @@
|
||||||
using Dalamud.Plugin.Services;
|
using Dalamud.Plugin.Services;
|
||||||
using FFXIVClientStructs.FFXIV.Client.Game.Character;
|
using FFXIVClientStructs.FFXIV.Client.Game.Character;
|
||||||
|
using OtterGui.Services;
|
||||||
using Penumbra.GameData.Enums;
|
using Penumbra.GameData.Enums;
|
||||||
using Penumbra.Interop.Services;
|
using Penumbra.Interop.Hooks;
|
||||||
|
|
||||||
namespace Penumbra.Interop.PathResolving;
|
namespace Penumbra.Interop.PathResolving;
|
||||||
|
|
||||||
public class CutsceneService : IDisposable
|
public sealed class CutsceneService : IService, IDisposable
|
||||||
{
|
{
|
||||||
public const int CutsceneStartIdx = (int)ScreenActor.CutsceneStart;
|
public const int CutsceneStartIdx = (int)ScreenActor.CutsceneStart;
|
||||||
public const int CutsceneEndIdx = (int)ScreenActor.CutsceneEnd;
|
public const int CutsceneEndIdx = (int)ScreenActor.CutsceneEnd;
|
||||||
public const int CutsceneSlots = CutsceneEndIdx - CutsceneStartIdx;
|
public const int CutsceneSlots = CutsceneEndIdx - CutsceneStartIdx;
|
||||||
|
|
||||||
private readonly GameEventManager _events;
|
private readonly IObjectTable _objects;
|
||||||
private readonly IObjectTable _objects;
|
private readonly CopyCharacter _copyCharacter;
|
||||||
private readonly short[] _copiedCharacters = Enumerable.Repeat((short)-1, CutsceneSlots).ToArray();
|
private readonly CharacterDestructor _characterDestructor;
|
||||||
|
private readonly short[] _copiedCharacters = Enumerable.Repeat((short)-1, CutsceneSlots).ToArray();
|
||||||
|
|
||||||
public IEnumerable<KeyValuePair<int, Dalamud.Game.ClientState.Objects.Types.GameObject>> Actors
|
public IEnumerable<KeyValuePair<int, Dalamud.Game.ClientState.Objects.Types.GameObject>> Actors
|
||||||
=> Enumerable.Range(CutsceneStartIdx, CutsceneSlots)
|
=> Enumerable.Range(CutsceneStartIdx, CutsceneSlots)
|
||||||
.Where(i => _objects[i] != null)
|
.Where(i => _objects[i] != null)
|
||||||
.Select(i => KeyValuePair.Create(i, this[i] ?? _objects[i]!));
|
.Select(i => KeyValuePair.Create(i, this[i] ?? _objects[i]!));
|
||||||
|
|
||||||
public unsafe CutsceneService(IObjectTable objects, GameEventManager events)
|
public unsafe CutsceneService(IObjectTable objects, CopyCharacter copyCharacter, CharacterDestructor characterDestructor)
|
||||||
{
|
{
|
||||||
_objects = objects;
|
_objects = objects;
|
||||||
_events = events;
|
_copyCharacter = copyCharacter;
|
||||||
_events.CopyCharacter += OnCharacterCopy;
|
_characterDestructor = characterDestructor;
|
||||||
_events.CharacterDestructor += OnCharacterDestructor;
|
_copyCharacter.Subscribe(OnCharacterCopy, CopyCharacter.Priority.CutsceneService);
|
||||||
|
_characterDestructor.Subscribe(OnCharacterDestructor, CharacterDestructor.Priority.CutsceneService);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|
@ -57,8 +60,8 @@ public class CutsceneService : IDisposable
|
||||||
|
|
||||||
public unsafe void Dispose()
|
public unsafe void Dispose()
|
||||||
{
|
{
|
||||||
_events.CopyCharacter -= OnCharacterCopy;
|
_copyCharacter.Unsubscribe(OnCharacterCopy);
|
||||||
_events.CharacterDestructor -= OnCharacterDestructor;
|
_characterDestructor.Unsubscribe(OnCharacterDestructor);
|
||||||
}
|
}
|
||||||
|
|
||||||
private unsafe void OnCharacterDestructor(Character* character)
|
private unsafe void OnCharacterDestructor(Character* character)
|
||||||
|
|
|
||||||
|
|
@ -1,35 +1,39 @@
|
||||||
using Dalamud.Hooking;
|
|
||||||
using Dalamud.Utility.Signatures;
|
|
||||||
using Dalamud.Plugin.Services;
|
using Dalamud.Plugin.Services;
|
||||||
|
using FFXIVClientStructs.FFXIV.Client.Game.Character;
|
||||||
using FFXIVClientStructs.FFXIV.Client.Game.Object;
|
using FFXIVClientStructs.FFXIV.Client.Game.Object;
|
||||||
using Penumbra.GameData;
|
using FFXIVClientStructs.FFXIV.Client.Graphics.Scene;
|
||||||
using Penumbra.Interop.Services;
|
using OtterGui.Services;
|
||||||
|
using Penumbra.Interop.Hooks;
|
||||||
using Object = FFXIVClientStructs.FFXIV.Client.Graphics.Scene.Object;
|
using Object = FFXIVClientStructs.FFXIV.Client.Graphics.Scene.Object;
|
||||||
|
using Penumbra.GameData.Structs;
|
||||||
|
|
||||||
namespace Penumbra.Interop.PathResolving;
|
namespace Penumbra.Interop.PathResolving;
|
||||||
|
|
||||||
public class DrawObjectState : IDisposable, IReadOnlyDictionary<nint, (nint, bool)>
|
public sealed class DrawObjectState : IDisposable, IReadOnlyDictionary<nint, (nint, bool)>, IService
|
||||||
{
|
{
|
||||||
private readonly IObjectTable _objects;
|
private readonly IObjectTable _objects;
|
||||||
private readonly GameEventManager _gameEvents;
|
private readonly CreateCharacterBase _createCharacterBase;
|
||||||
|
private readonly WeaponReload _weaponReload;
|
||||||
|
private readonly CharacterBaseDestructor _characterBaseDestructor;
|
||||||
|
private readonly GameState _gameState;
|
||||||
|
|
||||||
private readonly Dictionary<nint, (nint GameObject, bool IsChild)> _drawObjectToGameObject = new();
|
private readonly Dictionary<nint, (nint GameObject, bool IsChild)> _drawObjectToGameObject = [];
|
||||||
|
|
||||||
private readonly ThreadLocal<Queue<nint>> _lastGameObject = new(() => new Queue<nint>());
|
|
||||||
|
|
||||||
public nint LastGameObject
|
public nint LastGameObject
|
||||||
=> _lastGameObject.IsValueCreated && _lastGameObject.Value!.Count > 0 ? _lastGameObject.Value.Peek() : nint.Zero;
|
=> _gameState.LastGameObject;
|
||||||
|
|
||||||
public DrawObjectState(IObjectTable objects, GameEventManager gameEvents, IGameInteropProvider interop)
|
public unsafe DrawObjectState(IObjectTable objects, CreateCharacterBase createCharacterBase, WeaponReload weaponReload,
|
||||||
|
CharacterBaseDestructor characterBaseDestructor, GameState gameState)
|
||||||
{
|
{
|
||||||
interop.InitializeFromAttributes(this);
|
_objects = objects;
|
||||||
_enableDrawHook.Enable();
|
_createCharacterBase = createCharacterBase;
|
||||||
_objects = objects;
|
_weaponReload = weaponReload;
|
||||||
_gameEvents = gameEvents;
|
_characterBaseDestructor = characterBaseDestructor;
|
||||||
_gameEvents.WeaponReloading += OnWeaponReloading;
|
_gameState = gameState;
|
||||||
_gameEvents.WeaponReloaded += OnWeaponReloaded;
|
_weaponReload.Subscribe(OnWeaponReloading, WeaponReload.Priority.DrawObjectState);
|
||||||
_gameEvents.CharacterBaseCreated += OnCharacterBaseCreated;
|
_weaponReload.Subscribe(OnWeaponReloaded, WeaponReload.PostEvent.Priority.DrawObjectState);
|
||||||
_gameEvents.CharacterBaseDestructor += OnCharacterBaseDestructor;
|
_createCharacterBase.Subscribe(OnCharacterBaseCreated, CreateCharacterBase.PostEvent.Priority.DrawObjectState);
|
||||||
|
_characterBaseDestructor.Subscribe(OnCharacterBaseDestructor, CharacterBaseDestructor.Priority.DrawObjectState);
|
||||||
InitializeDrawObjects();
|
InitializeDrawObjects();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -57,32 +61,32 @@ public class DrawObjectState : IDisposable, IReadOnlyDictionary<nint, (nint, boo
|
||||||
public IEnumerable<(nint, bool)> Values
|
public IEnumerable<(nint, bool)> Values
|
||||||
=> _drawObjectToGameObject.Values;
|
=> _drawObjectToGameObject.Values;
|
||||||
|
|
||||||
public void Dispose()
|
public unsafe void Dispose()
|
||||||
{
|
{
|
||||||
_gameEvents.WeaponReloading -= OnWeaponReloading;
|
_weaponReload.Unsubscribe(OnWeaponReloading);
|
||||||
_gameEvents.WeaponReloaded -= OnWeaponReloaded;
|
_weaponReload.Unsubscribe(OnWeaponReloaded);
|
||||||
_gameEvents.CharacterBaseCreated -= OnCharacterBaseCreated;
|
_createCharacterBase.Unsubscribe(OnCharacterBaseCreated);
|
||||||
_gameEvents.CharacterBaseDestructor -= OnCharacterBaseDestructor;
|
_characterBaseDestructor.Unsubscribe(OnCharacterBaseDestructor);
|
||||||
_enableDrawHook.Dispose();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void OnWeaponReloading(nint _, nint gameObject)
|
private unsafe void OnWeaponReloading(DrawDataContainer* _, Character* character, CharacterWeapon* _2)
|
||||||
=> _lastGameObject.Value!.Enqueue(gameObject);
|
=> _gameState.QueueGameObject((nint)character);
|
||||||
|
|
||||||
private unsafe void OnWeaponReloaded(nint _, nint gameObject)
|
private unsafe void OnWeaponReloaded(DrawDataContainer* _, Character* character)
|
||||||
{
|
{
|
||||||
_lastGameObject.Value!.Dequeue();
|
_gameState.DequeueGameObject();
|
||||||
IterateDrawObjectTree((Object*)((GameObject*)gameObject)->DrawObject, gameObject, false, false);
|
IterateDrawObjectTree((Object*)character->GameObject.DrawObject, (nint)character, false, false);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void OnCharacterBaseDestructor(nint characterBase)
|
private unsafe void OnCharacterBaseDestructor(CharacterBase* characterBase)
|
||||||
=> _drawObjectToGameObject.Remove(characterBase);
|
=> _drawObjectToGameObject.Remove((nint)characterBase);
|
||||||
|
|
||||||
private void OnCharacterBaseCreated(uint modelCharaId, nint customize, nint equipment, nint drawObject)
|
private unsafe void OnCharacterBaseCreated(ModelCharaId modelCharaId, CustomizeArray* customize, CharacterArmor* equipment,
|
||||||
|
CharacterBase* drawObject)
|
||||||
{
|
{
|
||||||
var gameObject = LastGameObject;
|
var gameObject = LastGameObject;
|
||||||
if (gameObject != nint.Zero)
|
if (gameObject != nint.Zero)
|
||||||
_drawObjectToGameObject[drawObject] = (gameObject, false);
|
_drawObjectToGameObject[(nint)drawObject] = (gameObject, false);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|
@ -123,20 +127,4 @@ public class DrawObjectState : IDisposable, IReadOnlyDictionary<nint, (nint, boo
|
||||||
prevSibling = prevSibling->PreviousSiblingObject;
|
prevSibling = prevSibling->PreviousSiblingObject;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// EnableDraw is what creates DrawObjects for gameObjects,
|
|
||||||
/// so we always keep track of the current GameObject to be able to link it to the DrawObject.
|
|
||||||
/// </summary>
|
|
||||||
private delegate void EnableDrawDelegate(nint gameObject);
|
|
||||||
|
|
||||||
[Signature(Sigs.EnableDraw, DetourName = nameof(EnableDrawDetour))]
|
|
||||||
private readonly Hook<EnableDrawDelegate> _enableDrawHook = null!;
|
|
||||||
|
|
||||||
private void EnableDrawDetour(nint gameObject)
|
|
||||||
{
|
|
||||||
_lastGameObject.Value!.Enqueue(gameObject);
|
|
||||||
_enableDrawHook.Original.Invoke(gameObject);
|
|
||||||
_lastGameObject.Value!.TryDequeue(out _);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,7 @@ using Penumbra.Collections;
|
||||||
using Penumbra.Collections.Manager;
|
using Penumbra.Collections.Manager;
|
||||||
using Penumbra.Communication;
|
using Penumbra.Communication;
|
||||||
using Penumbra.GameData.Actors;
|
using Penumbra.GameData.Actors;
|
||||||
using Penumbra.Interop.Services;
|
using Penumbra.Interop.Hooks;
|
||||||
using Penumbra.Services;
|
using Penumbra.Services;
|
||||||
|
|
||||||
namespace Penumbra.Interop.PathResolving;
|
namespace Penumbra.Interop.PathResolving;
|
||||||
|
|
@ -13,20 +13,20 @@ namespace Penumbra.Interop.PathResolving;
|
||||||
public unsafe class IdentifiedCollectionCache : IDisposable, IEnumerable<(nint Address, ActorIdentifier Identifier, ModCollection Collection)>
|
public unsafe class IdentifiedCollectionCache : IDisposable, IEnumerable<(nint Address, ActorIdentifier Identifier, ModCollection Collection)>
|
||||||
{
|
{
|
||||||
private readonly CommunicatorService _communicator;
|
private readonly CommunicatorService _communicator;
|
||||||
private readonly GameEventManager _events;
|
private readonly CharacterDestructor _characterDestructor;
|
||||||
private readonly IClientState _clientState;
|
private readonly IClientState _clientState;
|
||||||
private readonly Dictionary<nint, (ActorIdentifier, ModCollection)> _cache = new(317);
|
private readonly Dictionary<nint, (ActorIdentifier, ModCollection)> _cache = new(317);
|
||||||
private bool _dirty;
|
private bool _dirty;
|
||||||
|
|
||||||
public IdentifiedCollectionCache(IClientState clientState, CommunicatorService communicator, GameEventManager events)
|
public IdentifiedCollectionCache(IClientState clientState, CommunicatorService communicator, CharacterDestructor characterDestructor)
|
||||||
{
|
{
|
||||||
_clientState = clientState;
|
_clientState = clientState;
|
||||||
_communicator = communicator;
|
_communicator = communicator;
|
||||||
_events = events;
|
_characterDestructor = characterDestructor;
|
||||||
|
|
||||||
_communicator.CollectionChange.Subscribe(CollectionChangeClear, CollectionChange.Priority.IdentifiedCollectionCache);
|
_communicator.CollectionChange.Subscribe(CollectionChangeClear, CollectionChange.Priority.IdentifiedCollectionCache);
|
||||||
_clientState.TerritoryChanged += TerritoryClear;
|
_clientState.TerritoryChanged += TerritoryClear;
|
||||||
_events.CharacterDestructor += OnCharacterDestruct;
|
_characterDestructor.Subscribe(OnCharacterDestructor, CharacterDestructor.Priority.IdentifiedCollectionCache);
|
||||||
}
|
}
|
||||||
|
|
||||||
public ResolveData Set(ModCollection collection, ActorIdentifier identifier, GameObject* data)
|
public ResolveData Set(ModCollection collection, ActorIdentifier identifier, GameObject* data)
|
||||||
|
|
@ -62,7 +62,7 @@ public unsafe class IdentifiedCollectionCache : IDisposable, IEnumerable<(nint A
|
||||||
{
|
{
|
||||||
_communicator.CollectionChange.Unsubscribe(CollectionChangeClear);
|
_communicator.CollectionChange.Unsubscribe(CollectionChangeClear);
|
||||||
_clientState.TerritoryChanged -= TerritoryClear;
|
_clientState.TerritoryChanged -= TerritoryClear;
|
||||||
_events.CharacterDestructor -= OnCharacterDestruct;
|
_characterDestructor.Unsubscribe(OnCharacterDestructor);
|
||||||
}
|
}
|
||||||
|
|
||||||
public IEnumerator<(nint Address, ActorIdentifier Identifier, ModCollection Collection)> GetEnumerator()
|
public IEnumerator<(nint Address, ActorIdentifier Identifier, ModCollection Collection)> GetEnumerator()
|
||||||
|
|
@ -88,6 +88,6 @@ public unsafe class IdentifiedCollectionCache : IDisposable, IEnumerable<(nint A
|
||||||
private void TerritoryClear(ushort _2)
|
private void TerritoryClear(ushort _2)
|
||||||
=> _dirty = _cache.Count > 0;
|
=> _dirty = _cache.Count > 0;
|
||||||
|
|
||||||
private void OnCharacterDestruct(Character* character)
|
private void OnCharacterDestructor(Character* character)
|
||||||
=> _cache.Remove((nint)character);
|
=> _cache.Remove((nint)character);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,22 +1,16 @@
|
||||||
using Dalamud.Hooking;
|
|
||||||
using Dalamud.Plugin.Services;
|
|
||||||
using Dalamud.Utility.Signatures;
|
|
||||||
using FFXIVClientStructs.FFXIV.Client.Game.Character;
|
|
||||||
using FFXIVClientStructs.FFXIV.Client.Game.Object;
|
|
||||||
using FFXIVClientStructs.FFXIV.Client.Graphics.Scene;
|
using FFXIVClientStructs.FFXIV.Client.Graphics.Scene;
|
||||||
using OtterGui.Classes;
|
using OtterGui.Classes;
|
||||||
using Penumbra.Collections;
|
using Penumbra.Collections;
|
||||||
using Penumbra.Api.Enums;
|
using Penumbra.Api.Enums;
|
||||||
using Penumbra.GameData;
|
|
||||||
using Penumbra.GameData.Enums;
|
using Penumbra.GameData.Enums;
|
||||||
|
using Penumbra.GameData.Structs;
|
||||||
|
using Penumbra.Interop.Hooks;
|
||||||
using Penumbra.Interop.ResourceLoading;
|
using Penumbra.Interop.ResourceLoading;
|
||||||
using Penumbra.Interop.Services;
|
using Penumbra.Interop.Services;
|
||||||
using Penumbra.Services;
|
using Penumbra.Services;
|
||||||
using Penumbra.String.Classes;
|
using Penumbra.String.Classes;
|
||||||
using Penumbra.Util;
|
|
||||||
using ObjectType = FFXIVClientStructs.FFXIV.Client.Graphics.Scene.ObjectType;
|
using ObjectType = FFXIVClientStructs.FFXIV.Client.Graphics.Scene.ObjectType;
|
||||||
using CharacterUtility = Penumbra.Interop.Services.CharacterUtility;
|
using CharacterUtility = Penumbra.Interop.Services.CharacterUtility;
|
||||||
using static Penumbra.GameData.Enums.GenderRace;
|
|
||||||
|
|
||||||
namespace Penumbra.Interop.PathResolving;
|
namespace Penumbra.Interop.PathResolving;
|
||||||
|
|
||||||
|
|
@ -42,56 +36,40 @@ namespace Penumbra.Interop.PathResolving;
|
||||||
// ChangeCustomize and RspSetupCharacter, which is hooked here, as well as Character.CalculateHeight.
|
// ChangeCustomize and RspSetupCharacter, which is hooked here, as well as Character.CalculateHeight.
|
||||||
|
|
||||||
// GMP Entries seem to be only used by "48 8B ?? 53 55 57 48 83 ?? ?? 48 8B", which has a DrawObject as its first parameter.
|
// GMP Entries seem to be only used by "48 8B ?? 53 55 57 48 83 ?? ?? 48 8B", which has a DrawObject as its first parameter.
|
||||||
public unsafe class MetaState : IDisposable
|
public sealed unsafe class MetaState : IDisposable
|
||||||
{
|
{
|
||||||
[Signature(Sigs.HumanVTable, ScanType = ScanType.StaticAddress)]
|
|
||||||
private readonly nint* _humanVTable = null!;
|
|
||||||
|
|
||||||
private readonly Configuration _config;
|
private readonly Configuration _config;
|
||||||
private readonly CommunicatorService _communicator;
|
private readonly CommunicatorService _communicator;
|
||||||
private readonly PerformanceTracker _performance;
|
|
||||||
private readonly CollectionResolver _collectionResolver;
|
private readonly CollectionResolver _collectionResolver;
|
||||||
private readonly ResourceLoader _resources;
|
private readonly ResourceLoader _resources;
|
||||||
private readonly GameEventManager _gameEventManager;
|
|
||||||
private readonly CharacterUtility _characterUtility;
|
private readonly CharacterUtility _characterUtility;
|
||||||
|
private readonly CreateCharacterBase _createCharacterBase;
|
||||||
|
|
||||||
|
public ResolveData CustomizeChangeCollection = ResolveData.Invalid;
|
||||||
|
|
||||||
private ResolveData _lastCreatedCollection = ResolveData.Invalid;
|
private ResolveData _lastCreatedCollection = ResolveData.Invalid;
|
||||||
private ResolveData _customizeChangeCollection = ResolveData.Invalid;
|
|
||||||
private DisposableContainer _characterBaseCreateMetaChanges = DisposableContainer.Empty;
|
private DisposableContainer _characterBaseCreateMetaChanges = DisposableContainer.Empty;
|
||||||
|
|
||||||
public MetaState(PerformanceTracker performance, CommunicatorService communicator, CollectionResolver collectionResolver,
|
public MetaState(CommunicatorService communicator, CollectionResolver collectionResolver,
|
||||||
ResourceLoader resources, GameEventManager gameEventManager, CharacterUtility characterUtility, Configuration config,
|
ResourceLoader resources, CreateCharacterBase createCharacterBase, CharacterUtility characterUtility, Configuration config)
|
||||||
IGameInteropProvider interop)
|
|
||||||
{
|
{
|
||||||
_performance = performance;
|
_communicator = communicator;
|
||||||
_communicator = communicator;
|
_collectionResolver = collectionResolver;
|
||||||
_collectionResolver = collectionResolver;
|
_resources = resources;
|
||||||
_resources = resources;
|
_createCharacterBase = createCharacterBase;
|
||||||
_gameEventManager = gameEventManager;
|
_characterUtility = characterUtility;
|
||||||
_characterUtility = characterUtility;
|
_config = config;
|
||||||
_config = config;
|
_createCharacterBase.Subscribe(OnCreatingCharacterBase, CreateCharacterBase.Priority.MetaState);
|
||||||
interop.InitializeFromAttributes(this);
|
_createCharacterBase.Subscribe(OnCharacterBaseCreated, CreateCharacterBase.PostEvent.Priority.MetaState);
|
||||||
_calculateHeightHook =
|
|
||||||
interop.HookFromAddress<CalculateHeightDelegate>((nint)Character.MemberFunctionPointers.CalculateHeight, CalculateHeightDetour);
|
|
||||||
_onModelLoadCompleteHook = interop.HookFromAddress<OnModelLoadCompleteDelegate>(_humanVTable[58], OnModelLoadCompleteDetour);
|
|
||||||
_getEqpIndirectHook.Enable();
|
|
||||||
_updateModelsHook.Enable();
|
|
||||||
_onModelLoadCompleteHook.Enable();
|
|
||||||
_setupVisorHook.Enable();
|
|
||||||
_rspSetupCharacterHook.Enable();
|
|
||||||
_changeCustomize.Enable();
|
|
||||||
_calculateHeightHook.Enable();
|
|
||||||
_gameEventManager.CreatingCharacterBase += OnCreatingCharacterBase;
|
|
||||||
_gameEventManager.CharacterBaseCreated += OnCharacterBaseCreated;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public bool HandleDecalFile(ResourceType type, Utf8GamePath gamePath, out ResolveData resolveData)
|
public bool HandleDecalFile(ResourceType type, Utf8GamePath gamePath, out ResolveData resolveData)
|
||||||
{
|
{
|
||||||
if (type == ResourceType.Tex
|
if (type == ResourceType.Tex
|
||||||
&& (_lastCreatedCollection.Valid || _customizeChangeCollection.Valid)
|
&& (_lastCreatedCollection.Valid || CustomizeChangeCollection.Valid)
|
||||||
&& gamePath.Path.Substring("chara/common/texture/".Length).StartsWith("decal"u8))
|
&& gamePath.Path.Substring("chara/common/texture/".Length).StartsWith("decal"u8))
|
||||||
{
|
{
|
||||||
resolveData = _lastCreatedCollection.Valid ? _lastCreatedCollection : _customizeChangeCollection;
|
resolveData = _lastCreatedCollection.Valid ? _lastCreatedCollection : CustomizeChangeCollection;
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -100,186 +78,81 @@ public unsafe class MetaState : IDisposable
|
||||||
}
|
}
|
||||||
|
|
||||||
public DisposableContainer ResolveEqdpData(ModCollection collection, GenderRace race, bool equipment, bool accessory)
|
public DisposableContainer ResolveEqdpData(ModCollection collection, GenderRace race, bool equipment, bool accessory)
|
||||||
{
|
=> (equipment, accessory) switch
|
||||||
var races = race.Dependencies();
|
{
|
||||||
|
(true, true) => new DisposableContainer(race.Dependencies().SelectMany(r => new[]
|
||||||
|
{
|
||||||
|
collection.TemporarilySetEqdpFile(_characterUtility, r, false),
|
||||||
|
collection.TemporarilySetEqdpFile(_characterUtility, r, true),
|
||||||
|
})),
|
||||||
|
(true, false) => new DisposableContainer(race.Dependencies()
|
||||||
|
.Select(r => collection.TemporarilySetEqdpFile(_characterUtility, r, false))),
|
||||||
|
(false, true) => new DisposableContainer(race.Dependencies()
|
||||||
|
.Select(r => collection.TemporarilySetEqdpFile(_characterUtility, r, true))),
|
||||||
|
_ => DisposableContainer.Empty,
|
||||||
|
};
|
||||||
|
|
||||||
var equipmentEnumerable = equipment
|
public MetaList.MetaReverter ResolveEqpData(ModCollection collection)
|
||||||
? races.Select(r => collection.TemporarilySetEqdpFile(_characterUtility, r, false))
|
=> collection.TemporarilySetEqpFile(_characterUtility);
|
||||||
: Array.Empty<IDisposable?>().AsEnumerable();
|
|
||||||
var accessoryEnumerable = accessory
|
public MetaList.MetaReverter ResolveGmpData(ModCollection collection)
|
||||||
? races.Select(r => collection.TemporarilySetEqdpFile(_characterUtility, r, true))
|
=> collection.TemporarilySetGmpFile(_characterUtility);
|
||||||
: Array.Empty<IDisposable?>().AsEnumerable();
|
|
||||||
return new DisposableContainer(equipmentEnumerable.Concat(accessoryEnumerable));
|
public MetaList.MetaReverter ResolveRspData(ModCollection collection)
|
||||||
}
|
=> collection.TemporarilySetCmpFile(_characterUtility);
|
||||||
|
|
||||||
|
public DecalReverter ResolveDecal(ResolveData resolve, bool which)
|
||||||
|
=> new(_config, _characterUtility, _resources, resolve, which);
|
||||||
|
|
||||||
public static GenderRace GetHumanGenderRace(nint human)
|
public static GenderRace GetHumanGenderRace(nint human)
|
||||||
=> (GenderRace)((Human*)human)->RaceSexId;
|
=> (GenderRace)((Human*)human)->RaceSexId;
|
||||||
|
|
||||||
public void Dispose()
|
public static GenderRace GetDrawObjectGenderRace(nint drawObject)
|
||||||
{
|
{
|
||||||
_getEqpIndirectHook.Dispose();
|
var draw = (DrawObject*)drawObject;
|
||||||
_updateModelsHook.Dispose();
|
if (draw->Object.GetObjectType() != ObjectType.CharacterBase)
|
||||||
_onModelLoadCompleteHook.Dispose();
|
return GenderRace.Unknown;
|
||||||
_setupVisorHook.Dispose();
|
|
||||||
_rspSetupCharacterHook.Dispose();
|
var c = (CharacterBase*)drawObject;
|
||||||
_changeCustomize.Dispose();
|
return c->GetModelType() == CharacterBase.ModelType.Human
|
||||||
_calculateHeightHook.Dispose();
|
? GetHumanGenderRace(drawObject)
|
||||||
_gameEventManager.CreatingCharacterBase -= OnCreatingCharacterBase;
|
: GenderRace.Unknown;
|
||||||
_gameEventManager.CharacterBaseCreated -= OnCharacterBaseCreated;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void OnCreatingCharacterBase(nint modelCharaId, nint customize, nint equipData)
|
public void Dispose()
|
||||||
|
{
|
||||||
|
_createCharacterBase.Unsubscribe(OnCreatingCharacterBase);
|
||||||
|
_createCharacterBase.Unsubscribe(OnCharacterBaseCreated);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnCreatingCharacterBase(ModelCharaId* modelCharaId, CustomizeArray* customize, CharacterArmor* equipData)
|
||||||
{
|
{
|
||||||
_lastCreatedCollection = _collectionResolver.IdentifyLastGameObjectCollection(true);
|
_lastCreatedCollection = _collectionResolver.IdentifyLastGameObjectCollection(true);
|
||||||
if (_lastCreatedCollection.Valid && _lastCreatedCollection.AssociatedGameObject != nint.Zero)
|
if (_lastCreatedCollection.Valid && _lastCreatedCollection.AssociatedGameObject != nint.Zero)
|
||||||
_communicator.CreatingCharacterBase.Invoke(_lastCreatedCollection.AssociatedGameObject,
|
_communicator.CreatingCharacterBase.Invoke(_lastCreatedCollection.AssociatedGameObject,
|
||||||
_lastCreatedCollection.ModCollection.Name, modelCharaId, customize, equipData);
|
_lastCreatedCollection.ModCollection.Name, (nint)modelCharaId, (nint)customize, (nint)equipData);
|
||||||
|
|
||||||
var decal = new DecalReverter(_config, _characterUtility, _resources, _lastCreatedCollection,
|
var decal = new DecalReverter(_config, _characterUtility, _resources, _lastCreatedCollection,
|
||||||
UsesDecal(*(uint*)modelCharaId, customize));
|
UsesDecal(*(uint*)modelCharaId, (nint)customize));
|
||||||
var cmp = _lastCreatedCollection.ModCollection.TemporarilySetCmpFile(_characterUtility);
|
var cmp = _lastCreatedCollection.ModCollection.TemporarilySetCmpFile(_characterUtility);
|
||||||
_characterBaseCreateMetaChanges.Dispose(); // Should always be empty.
|
_characterBaseCreateMetaChanges.Dispose(); // Should always be empty.
|
||||||
_characterBaseCreateMetaChanges = new DisposableContainer(decal, cmp);
|
_characterBaseCreateMetaChanges = new DisposableContainer(decal, cmp);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void OnCharacterBaseCreated(uint _1, nint _2, nint _3, nint drawObject)
|
private void OnCharacterBaseCreated(ModelCharaId _1, CustomizeArray* _2, CharacterArmor* _3, CharacterBase* drawObject)
|
||||||
{
|
{
|
||||||
_characterBaseCreateMetaChanges.Dispose();
|
_characterBaseCreateMetaChanges.Dispose();
|
||||||
_characterBaseCreateMetaChanges = DisposableContainer.Empty;
|
_characterBaseCreateMetaChanges = DisposableContainer.Empty;
|
||||||
if (_lastCreatedCollection.Valid && _lastCreatedCollection.AssociatedGameObject != nint.Zero && drawObject != nint.Zero)
|
if (_lastCreatedCollection.Valid && _lastCreatedCollection.AssociatedGameObject != nint.Zero && drawObject != null)
|
||||||
_communicator.CreatedCharacterBase.Invoke(_lastCreatedCollection.AssociatedGameObject,
|
_communicator.CreatedCharacterBase.Invoke(_lastCreatedCollection.AssociatedGameObject,
|
||||||
_lastCreatedCollection.ModCollection, drawObject);
|
_lastCreatedCollection.ModCollection, (nint)drawObject);
|
||||||
_lastCreatedCollection = ResolveData.Invalid;
|
_lastCreatedCollection = ResolveData.Invalid;
|
||||||
}
|
}
|
||||||
|
|
||||||
private delegate void OnModelLoadCompleteDelegate(nint drawObject);
|
|
||||||
private readonly Hook<OnModelLoadCompleteDelegate> _onModelLoadCompleteHook;
|
|
||||||
|
|
||||||
private void OnModelLoadCompleteDetour(nint drawObject)
|
|
||||||
{
|
|
||||||
var collection = _collectionResolver.IdentifyCollection((DrawObject*)drawObject, true);
|
|
||||||
using var eqp = collection.ModCollection.TemporarilySetEqpFile(_characterUtility);
|
|
||||||
using var eqdp = ResolveEqdpData(collection.ModCollection, GetDrawObjectGenderRace(drawObject), true, true);
|
|
||||||
_onModelLoadCompleteHook.Original.Invoke(drawObject);
|
|
||||||
}
|
|
||||||
|
|
||||||
private delegate void UpdateModelDelegate(nint drawObject);
|
|
||||||
|
|
||||||
[Signature(Sigs.UpdateModel, DetourName = nameof(UpdateModelsDetour))]
|
|
||||||
private readonly Hook<UpdateModelDelegate> _updateModelsHook = null!;
|
|
||||||
|
|
||||||
private void UpdateModelsDetour(nint drawObject)
|
|
||||||
{
|
|
||||||
// Shortcut because this is called all the time.
|
|
||||||
// Same thing is checked at the beginning of the original function.
|
|
||||||
if (*(int*)(drawObject + Offsets.UpdateModelSkip) == 0)
|
|
||||||
return;
|
|
||||||
|
|
||||||
using var performance = _performance.Measure(PerformanceType.UpdateModels);
|
|
||||||
|
|
||||||
var collection = _collectionResolver.IdentifyCollection((DrawObject*)drawObject, true);
|
|
||||||
using var eqp = collection.ModCollection.TemporarilySetEqpFile(_characterUtility);
|
|
||||||
using var eqdp = ResolveEqdpData(collection.ModCollection, GetDrawObjectGenderRace(drawObject), true, true);
|
|
||||||
_updateModelsHook.Original.Invoke(drawObject);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static GenderRace GetDrawObjectGenderRace(nint drawObject)
|
|
||||||
{
|
|
||||||
var draw = (DrawObject*)drawObject;
|
|
||||||
if (draw->Object.GetObjectType() != ObjectType.CharacterBase)
|
|
||||||
return Unknown;
|
|
||||||
|
|
||||||
var c = (CharacterBase*)drawObject;
|
|
||||||
return c->GetModelType() == CharacterBase.ModelType.Human
|
|
||||||
? GetHumanGenderRace(drawObject)
|
|
||||||
: Unknown;
|
|
||||||
}
|
|
||||||
|
|
||||||
[Signature(Sigs.GetEqpIndirect, DetourName = nameof(GetEqpIndirectDetour))]
|
|
||||||
private readonly Hook<OnModelLoadCompleteDelegate> _getEqpIndirectHook = null!;
|
|
||||||
|
|
||||||
private void GetEqpIndirectDetour(nint drawObject)
|
|
||||||
{
|
|
||||||
// Shortcut because this is also called all the time.
|
|
||||||
// Same thing is checked at the beginning of the original function.
|
|
||||||
if ((*(byte*)(drawObject + Offsets.GetEqpIndirectSkip1) & 1) == 0 || *(ulong*)(drawObject + Offsets.GetEqpIndirectSkip2) == 0)
|
|
||||||
return;
|
|
||||||
|
|
||||||
using var performance = _performance.Measure(PerformanceType.GetEqp);
|
|
||||||
var resolveData = _collectionResolver.IdentifyCollection((DrawObject*)drawObject, true);
|
|
||||||
using var eqp = resolveData.ModCollection.TemporarilySetEqpFile(_characterUtility);
|
|
||||||
_getEqpIndirectHook.Original(drawObject);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
// GMP. This gets called every time when changing visor state, and it accesses the gmp file itself,
|
|
||||||
// but it only applies a changed gmp file after a redraw for some reason.
|
|
||||||
private delegate byte SetupVisorDelegate(nint drawObject, ushort modelId, byte visorState);
|
|
||||||
|
|
||||||
[Signature(Sigs.SetupVisor, DetourName = nameof(SetupVisorDetour))]
|
|
||||||
private readonly Hook<SetupVisorDelegate> _setupVisorHook = null!;
|
|
||||||
|
|
||||||
private byte SetupVisorDetour(nint drawObject, ushort modelId, byte visorState)
|
|
||||||
{
|
|
||||||
using var performance = _performance.Measure(PerformanceType.SetupVisor);
|
|
||||||
var resolveData = _collectionResolver.IdentifyCollection((DrawObject*)drawObject, true);
|
|
||||||
using var gmp = resolveData.ModCollection.TemporarilySetGmpFile(_characterUtility);
|
|
||||||
return _setupVisorHook.Original(drawObject, modelId, visorState);
|
|
||||||
}
|
|
||||||
|
|
||||||
// RSP
|
|
||||||
private delegate void RspSetupCharacterDelegate(nint drawObject, nint unk2, float unk3, nint unk4, byte unk5);
|
|
||||||
|
|
||||||
[Signature(Sigs.RspSetupCharacter, DetourName = nameof(RspSetupCharacterDetour))]
|
|
||||||
private readonly Hook<RspSetupCharacterDelegate> _rspSetupCharacterHook = null!;
|
|
||||||
|
|
||||||
private void RspSetupCharacterDetour(nint drawObject, nint unk2, float unk3, nint unk4, byte unk5)
|
|
||||||
{
|
|
||||||
if (_customizeChangeCollection.Valid)
|
|
||||||
{
|
|
||||||
_rspSetupCharacterHook.Original(drawObject, unk2, unk3, unk4, unk5);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
using var performance = _performance.Measure(PerformanceType.SetupCharacter);
|
|
||||||
var resolveData = _collectionResolver.IdentifyCollection((DrawObject*)drawObject, true);
|
|
||||||
using var cmp = resolveData.ModCollection.TemporarilySetCmpFile(_characterUtility);
|
|
||||||
_rspSetupCharacterHook.Original(drawObject, unk2, unk3, unk4, unk5);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private delegate ulong CalculateHeightDelegate(Character* character);
|
|
||||||
|
|
||||||
private readonly Hook<CalculateHeightDelegate> _calculateHeightHook = null!;
|
|
||||||
|
|
||||||
private ulong CalculateHeightDetour(Character* character)
|
|
||||||
{
|
|
||||||
var resolveData = _collectionResolver.IdentifyCollection((GameObject*)character, true);
|
|
||||||
using var cmp = resolveData.ModCollection.TemporarilySetCmpFile(_characterUtility);
|
|
||||||
return _calculateHeightHook.Original(character);
|
|
||||||
}
|
|
||||||
|
|
||||||
private delegate bool ChangeCustomizeDelegate(nint human, nint data, byte skipEquipment);
|
|
||||||
|
|
||||||
[Signature(Sigs.ChangeCustomize, DetourName = nameof(ChangeCustomizeDetour))]
|
|
||||||
private readonly Hook<ChangeCustomizeDelegate> _changeCustomize = null!;
|
|
||||||
|
|
||||||
private bool ChangeCustomizeDetour(nint human, nint data, byte skipEquipment)
|
|
||||||
{
|
|
||||||
using var performance = _performance.Measure(PerformanceType.ChangeCustomize);
|
|
||||||
_customizeChangeCollection = _collectionResolver.IdentifyCollection((DrawObject*)human, true);
|
|
||||||
using var cmp = _customizeChangeCollection.ModCollection.TemporarilySetCmpFile(_characterUtility);
|
|
||||||
using var decals = new DecalReverter(_config, _characterUtility, _resources, _customizeChangeCollection, true);
|
|
||||||
using var decal2 = new DecalReverter(_config, _characterUtility, _resources, _customizeChangeCollection, false);
|
|
||||||
var ret = _changeCustomize.Original(human, data, skipEquipment);
|
|
||||||
_customizeChangeCollection = ResolveData.Invalid;
|
|
||||||
return ret;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Check the customize array for the FaceCustomization byte and the last bit of that.
|
/// Check the customize array for the FaceCustomization byte and the last bit of that.
|
||||||
/// Also check for humans.
|
/// Also check for humans.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public static bool UsesDecal(uint modelId, nint customizeData)
|
private static bool UsesDecal(uint modelId, nint customizeData)
|
||||||
=> modelId == 0 && ((byte*)customizeData)[12] > 0x7F;
|
=> modelId == 0 && ((byte*)customizeData)[12] > 0x7F;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -18,26 +18,28 @@ public class PathResolver : IDisposable
|
||||||
private readonly TempCollectionManager _tempCollections;
|
private readonly TempCollectionManager _tempCollections;
|
||||||
private readonly ResourceLoader _loader;
|
private readonly ResourceLoader _loader;
|
||||||
|
|
||||||
private readonly AnimationHookService _animationHookService;
|
private readonly SubfileHelper _subfileHelper;
|
||||||
private readonly SubfileHelper _subfileHelper;
|
private readonly PathState _pathState;
|
||||||
private readonly PathState _pathState;
|
private readonly MetaState _metaState;
|
||||||
private readonly MetaState _metaState;
|
private readonly GameState _gameState;
|
||||||
|
private readonly CollectionResolver _collectionResolver;
|
||||||
|
|
||||||
public unsafe PathResolver(PerformanceTracker performance, Configuration config, CollectionManager collectionManager,
|
public unsafe PathResolver(PerformanceTracker performance, Configuration config, CollectionManager collectionManager,
|
||||||
TempCollectionManager tempCollections, ResourceLoader loader, AnimationHookService animationHookService, SubfileHelper subfileHelper,
|
TempCollectionManager tempCollections, ResourceLoader loader, SubfileHelper subfileHelper,
|
||||||
PathState pathState, MetaState metaState)
|
PathState pathState, MetaState metaState, CollectionResolver collectionResolver, GameState gameState)
|
||||||
{
|
{
|
||||||
_performance = performance;
|
_performance = performance;
|
||||||
_config = config;
|
_config = config;
|
||||||
_collectionManager = collectionManager;
|
_collectionManager = collectionManager;
|
||||||
_tempCollections = tempCollections;
|
_tempCollections = tempCollections;
|
||||||
_animationHookService = animationHookService;
|
_subfileHelper = subfileHelper;
|
||||||
_subfileHelper = subfileHelper;
|
_pathState = pathState;
|
||||||
_pathState = pathState;
|
_metaState = metaState;
|
||||||
_metaState = metaState;
|
_gameState = gameState;
|
||||||
_loader = loader;
|
_collectionResolver = collectionResolver;
|
||||||
_loader.ResolvePath = ResolvePath;
|
_loader = loader;
|
||||||
_loader.FileLoaded += ImcLoadResource;
|
_loader.ResolvePath = ResolvePath;
|
||||||
|
_loader.FileLoaded += ImcLoadResource;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary> Obtain a temporary or permanent collection by name. </summary>
|
/// <summary> Obtain a temporary or permanent collection by name. </summary>
|
||||||
|
|
@ -98,7 +100,7 @@ public class PathResolver : IDisposable
|
||||||
// A potential next request will add the path anew.
|
// A potential next request will add the path anew.
|
||||||
var nonDefault = _subfileHelper.HandleSubFiles(type, out var resolveData)
|
var nonDefault = _subfileHelper.HandleSubFiles(type, out var resolveData)
|
||||||
|| _pathState.Consume(gamePath.Path, out resolveData)
|
|| _pathState.Consume(gamePath.Path, out resolveData)
|
||||||
|| _animationHookService.HandleFiles(type, gamePath, out resolveData)
|
|| _gameState.HandleFiles(_collectionResolver, type, gamePath, out resolveData)
|
||||||
|| _metaState.HandleDecalFile(type, gamePath, out resolveData);
|
|| _metaState.HandleDecalFile(type, gamePath, out resolveData);
|
||||||
if (!nonDefault || !resolveData.Valid)
|
if (!nonDefault || !resolveData.Valid)
|
||||||
resolveData = _collectionManager.Active.Default.ToResolveData();
|
resolveData = _collectionManager.Active.Default.ToResolveData();
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,7 @@ using Dalamud.Utility.Signatures;
|
||||||
using Penumbra.Api.Enums;
|
using Penumbra.Api.Enums;
|
||||||
using Penumbra.Collections;
|
using Penumbra.Collections;
|
||||||
using Penumbra.GameData;
|
using Penumbra.GameData;
|
||||||
|
using Penumbra.Interop.Hooks;
|
||||||
using Penumbra.Interop.ResourceLoading;
|
using Penumbra.Interop.ResourceLoading;
|
||||||
using Penumbra.Interop.Services;
|
using Penumbra.Interop.Services;
|
||||||
using Penumbra.Interop.Structs;
|
using Penumbra.Interop.Structs;
|
||||||
|
|
@ -21,30 +22,30 @@ namespace Penumbra.Interop.PathResolving;
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public unsafe class SubfileHelper : IDisposable, IReadOnlyCollection<KeyValuePair<nint, ResolveData>>
|
public unsafe class SubfileHelper : IDisposable, IReadOnlyCollection<KeyValuePair<nint, ResolveData>>
|
||||||
{
|
{
|
||||||
private readonly PerformanceTracker _performance;
|
private readonly PerformanceTracker _performance;
|
||||||
private readonly ResourceLoader _loader;
|
private readonly ResourceLoader _loader;
|
||||||
private readonly GameEventManager _events;
|
private readonly ResourceHandleDestructor _resourceHandleDestructor;
|
||||||
private readonly CommunicatorService _communicator;
|
private readonly CommunicatorService _communicator;
|
||||||
|
|
||||||
private readonly ThreadLocal<ResolveData> _mtrlData = new(() => ResolveData.Invalid);
|
private readonly ThreadLocal<ResolveData> _mtrlData = new(() => ResolveData.Invalid);
|
||||||
private readonly ThreadLocal<ResolveData> _avfxData = new(() => ResolveData.Invalid);
|
private readonly ThreadLocal<ResolveData> _avfxData = new(() => ResolveData.Invalid);
|
||||||
|
|
||||||
private readonly ConcurrentDictionary<nint, ResolveData> _subFileCollection = new();
|
private readonly ConcurrentDictionary<nint, ResolveData> _subFileCollection = new();
|
||||||
|
|
||||||
public SubfileHelper(PerformanceTracker performance, ResourceLoader loader, GameEventManager events, CommunicatorService communicator, IGameInteropProvider interop)
|
public SubfileHelper(PerformanceTracker performance, ResourceLoader loader, CommunicatorService communicator, IGameInteropProvider interop, ResourceHandleDestructor resourceHandleDestructor)
|
||||||
{
|
{
|
||||||
interop.InitializeFromAttributes(this);
|
interop.InitializeFromAttributes(this);
|
||||||
|
|
||||||
_performance = performance;
|
_performance = performance;
|
||||||
_loader = loader;
|
_loader = loader;
|
||||||
_events = events;
|
_communicator = communicator;
|
||||||
_communicator = communicator;
|
_resourceHandleDestructor = resourceHandleDestructor;
|
||||||
|
|
||||||
_loadMtrlShpkHook.Enable();
|
_loadMtrlShpkHook.Enable();
|
||||||
_loadMtrlTexHook.Enable();
|
_loadMtrlTexHook.Enable();
|
||||||
_apricotResourceLoadHook.Enable();
|
_apricotResourceLoadHook.Enable();
|
||||||
_loader.ResourceLoaded += SubfileContainerRequested;
|
_loader.ResourceLoaded += SubfileContainerRequested;
|
||||||
_events.ResourceHandleDestructor += ResourceDestroyed;
|
_resourceHandleDestructor.Subscribe(ResourceDestroyed, ResourceHandleDestructor.Priority.SubfileHelper);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -105,7 +106,7 @@ public unsafe class SubfileHelper : IDisposable, IReadOnlyCollection<KeyValuePai
|
||||||
public void Dispose()
|
public void Dispose()
|
||||||
{
|
{
|
||||||
_loader.ResourceLoaded -= SubfileContainerRequested;
|
_loader.ResourceLoaded -= SubfileContainerRequested;
|
||||||
_events.ResourceHandleDestructor -= ResourceDestroyed;
|
_resourceHandleDestructor.Unsubscribe(ResourceDestroyed);
|
||||||
_loadMtrlShpkHook.Dispose();
|
_loadMtrlShpkHook.Dispose();
|
||||||
_loadMtrlTexHook.Dispose();
|
_loadMtrlTexHook.Dispose();
|
||||||
_apricotResourceLoadHook.Dispose();
|
_apricotResourceLoadHook.Dispose();
|
||||||
|
|
|
||||||
|
|
@ -1,299 +0,0 @@
|
||||||
using Dalamud.Hooking;
|
|
||||||
using Dalamud.Plugin.Services;
|
|
||||||
using Dalamud.Utility.Signatures;
|
|
||||||
using Penumbra.GameData;
|
|
||||||
using FFXIVClientStructs.FFXIV.Client.Game.Character;
|
|
||||||
using FFXIVClientStructs.FFXIV.Client.Game.Object;
|
|
||||||
using FFXIVClientStructs.FFXIV.Client.Graphics.Scene;
|
|
||||||
using Penumbra.Interop.Structs;
|
|
||||||
|
|
||||||
namespace Penumbra.Interop.Services;
|
|
||||||
|
|
||||||
public unsafe class GameEventManager : IDisposable
|
|
||||||
{
|
|
||||||
private const string Prefix = $"[{nameof(GameEventManager)}]";
|
|
||||||
|
|
||||||
public event CharacterDestructorEvent? CharacterDestructor;
|
|
||||||
public event CopyCharacterEvent? CopyCharacter;
|
|
||||||
public event ResourceHandleDestructorEvent? ResourceHandleDestructor;
|
|
||||||
public event CreatingCharacterBaseEvent? CreatingCharacterBase;
|
|
||||||
public event CharacterBaseCreatedEvent? CharacterBaseCreated;
|
|
||||||
public event CharacterBaseDestructorEvent? CharacterBaseDestructor;
|
|
||||||
public event WeaponReloadingEvent? WeaponReloading;
|
|
||||||
public event WeaponReloadedEvent? WeaponReloaded;
|
|
||||||
|
|
||||||
public GameEventManager(IGameInteropProvider interop)
|
|
||||||
{
|
|
||||||
interop.InitializeFromAttributes(this);
|
|
||||||
|
|
||||||
_copyCharacterHook =
|
|
||||||
interop.HookFromAddress<CopyCharacterDelegate>((nint)CharacterSetup.MemberFunctionPointers.CopyFromCharacter, CopyCharacterDetour);
|
|
||||||
_characterBaseCreateHook =
|
|
||||||
interop.HookFromAddress<CharacterBaseCreateDelegate>((nint)CharacterBase.MemberFunctionPointers.Create, CharacterBaseCreateDetour);
|
|
||||||
_characterBaseDestructorHook =
|
|
||||||
interop.HookFromAddress<CharacterBaseDestructorEvent>((nint)CharacterBase.MemberFunctionPointers.Destroy,
|
|
||||||
CharacterBaseDestructorDetour);
|
|
||||||
_weaponReloadHook =
|
|
||||||
interop.HookFromAddress<WeaponReloadFunc>((nint)DrawDataContainer.MemberFunctionPointers.LoadWeapon, WeaponReloadDetour);
|
|
||||||
_characterDtorHook.Enable();
|
|
||||||
_copyCharacterHook.Enable();
|
|
||||||
_resourceHandleDestructorHook.Enable();
|
|
||||||
_characterBaseCreateHook.Enable();
|
|
||||||
_characterBaseDestructorHook.Enable();
|
|
||||||
_weaponReloadHook.Enable();
|
|
||||||
EnableDebugHook();
|
|
||||||
Penumbra.Log.Verbose($"{Prefix} Created.");
|
|
||||||
}
|
|
||||||
|
|
||||||
public void Dispose()
|
|
||||||
{
|
|
||||||
_characterDtorHook.Dispose();
|
|
||||||
_copyCharacterHook.Dispose();
|
|
||||||
_resourceHandleDestructorHook.Dispose();
|
|
||||||
_characterBaseCreateHook.Dispose();
|
|
||||||
_characterBaseDestructorHook.Dispose();
|
|
||||||
_weaponReloadHook.Dispose();
|
|
||||||
DisposeDebugHook();
|
|
||||||
Penumbra.Log.Verbose($"{Prefix} Disposed.");
|
|
||||||
}
|
|
||||||
|
|
||||||
#region Character Destructor
|
|
||||||
|
|
||||||
private delegate void CharacterDestructorDelegate(Character* character);
|
|
||||||
|
|
||||||
[Signature(Sigs.CharacterDestructor, DetourName = nameof(CharacterDestructorDetour))]
|
|
||||||
private readonly Hook<CharacterDestructorDelegate> _characterDtorHook = null!;
|
|
||||||
|
|
||||||
private void CharacterDestructorDetour(Character* character)
|
|
||||||
{
|
|
||||||
if (CharacterDestructor != null)
|
|
||||||
foreach (var subscriber in CharacterDestructor.GetInvocationList())
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
((CharacterDestructorEvent)subscriber).Invoke(character);
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
Penumbra.Log.Error($"{Prefix} Error in {nameof(CharacterDestructor)} event when executing {subscriber.Method.Name}:\n{ex}");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Penumbra.Log.Verbose($"{Prefix} {nameof(CharacterDestructor)} triggered with 0x{(nint)character:X}.");
|
|
||||||
_characterDtorHook.Original(character);
|
|
||||||
}
|
|
||||||
|
|
||||||
public delegate void CharacterDestructorEvent(Character* character);
|
|
||||||
|
|
||||||
#endregion
|
|
||||||
|
|
||||||
#region Copy Character
|
|
||||||
|
|
||||||
private delegate ulong CopyCharacterDelegate(CharacterSetup* target, GameObject* source, uint unk);
|
|
||||||
|
|
||||||
private readonly Hook<CopyCharacterDelegate> _copyCharacterHook;
|
|
||||||
|
|
||||||
private ulong CopyCharacterDetour(CharacterSetup* target, GameObject* source, uint unk)
|
|
||||||
{
|
|
||||||
// TODO: update when CS updated.
|
|
||||||
var character = ((Character**)target)[1];
|
|
||||||
if (CopyCharacter != null)
|
|
||||||
foreach (var subscriber in CopyCharacter.GetInvocationList())
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
((CopyCharacterEvent)subscriber).Invoke(character, (Character*)source);
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
Penumbra.Log.Error(
|
|
||||||
$"{Prefix} Error in {nameof(CopyCharacter)} event when executing {subscriber.Method.Name}:\n{ex}");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Penumbra.Log.Verbose(
|
|
||||||
$"{Prefix} {nameof(CopyCharacter)} triggered with target 0x{(nint)target:X} and source 0x{(nint)source:X}.");
|
|
||||||
return _copyCharacterHook.Original(target, source, unk);
|
|
||||||
}
|
|
||||||
|
|
||||||
public delegate void CopyCharacterEvent(Character* target, Character* source);
|
|
||||||
|
|
||||||
#endregion
|
|
||||||
|
|
||||||
#region ResourceHandle Destructor
|
|
||||||
|
|
||||||
private delegate IntPtr ResourceHandleDestructorDelegate(ResourceHandle* handle);
|
|
||||||
|
|
||||||
[Signature(Sigs.ResourceHandleDestructor, DetourName = nameof(ResourceHandleDestructorDetour))]
|
|
||||||
private readonly Hook<ResourceHandleDestructorDelegate> _resourceHandleDestructorHook = null!;
|
|
||||||
|
|
||||||
private IntPtr ResourceHandleDestructorDetour(ResourceHandle* handle)
|
|
||||||
{
|
|
||||||
if (ResourceHandleDestructor != null)
|
|
||||||
foreach (var subscriber in ResourceHandleDestructor.GetInvocationList())
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
((ResourceHandleDestructorEvent)subscriber).Invoke(handle);
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
Penumbra.Log.Error(
|
|
||||||
$"{Prefix} Error in {nameof(ResourceHandleDestructor)} event when executing {subscriber.Method.Name}:\n{ex}");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Penumbra.Log.Excessive($"{Prefix} {nameof(ResourceHandleDestructor)} triggered with 0x{(nint)handle:X}.");
|
|
||||||
return _resourceHandleDestructorHook!.Original(handle);
|
|
||||||
}
|
|
||||||
|
|
||||||
public delegate void ResourceHandleDestructorEvent(ResourceHandle* handle);
|
|
||||||
|
|
||||||
#endregion
|
|
||||||
|
|
||||||
#region CharacterBaseCreate
|
|
||||||
|
|
||||||
private delegate nint CharacterBaseCreateDelegate(uint a, nint b, nint c, byte d);
|
|
||||||
|
|
||||||
private readonly Hook<CharacterBaseCreateDelegate> _characterBaseCreateHook;
|
|
||||||
|
|
||||||
private nint CharacterBaseCreateDetour(uint a, nint b, nint c, byte d)
|
|
||||||
{
|
|
||||||
if (CreatingCharacterBase != null)
|
|
||||||
foreach (var subscriber in CreatingCharacterBase.GetInvocationList())
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
((CreatingCharacterBaseEvent)subscriber).Invoke((nint)(&a), b, c);
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
Penumbra.Log.Error(
|
|
||||||
$"{Prefix} Error in {nameof(CharacterBaseCreateDetour)} event when executing {subscriber.Method.Name}:\n{ex}");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
var ret = _characterBaseCreateHook.Original(a, b, c, d);
|
|
||||||
if (CharacterBaseCreated != null)
|
|
||||||
foreach (var subscriber in CharacterBaseCreated.GetInvocationList())
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
((CharacterBaseCreatedEvent)subscriber).Invoke(a, b, c, ret);
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
Penumbra.Log.Error(
|
|
||||||
$"{Prefix} Error in {nameof(CharacterBaseCreateDetour)} event when executing {subscriber.Method.Name}:\n{ex}");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return ret;
|
|
||||||
}
|
|
||||||
|
|
||||||
public delegate void CreatingCharacterBaseEvent(nint modelCharaId, nint customize, nint equipment);
|
|
||||||
public delegate void CharacterBaseCreatedEvent(uint modelCharaId, nint customize, nint equipment, nint drawObject);
|
|
||||||
|
|
||||||
#endregion
|
|
||||||
|
|
||||||
#region CharacterBase Destructor
|
|
||||||
|
|
||||||
public delegate void CharacterBaseDestructorEvent(nint drawBase);
|
|
||||||
|
|
||||||
private readonly Hook<CharacterBaseDestructorEvent> _characterBaseDestructorHook;
|
|
||||||
|
|
||||||
private void CharacterBaseDestructorDetour(IntPtr drawBase)
|
|
||||||
{
|
|
||||||
if (CharacterBaseDestructor != null)
|
|
||||||
foreach (var subscriber in CharacterBaseDestructor.GetInvocationList())
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
((CharacterBaseDestructorEvent)subscriber).Invoke(drawBase);
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
Penumbra.Log.Error(
|
|
||||||
$"{Prefix} Error in {nameof(CharacterBaseDestructorDetour)} event when executing {subscriber.Method.Name}:\n{ex}");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
_characterBaseDestructorHook.Original.Invoke(drawBase);
|
|
||||||
}
|
|
||||||
|
|
||||||
#endregion
|
|
||||||
|
|
||||||
#region Weapon Reload
|
|
||||||
|
|
||||||
private delegate void WeaponReloadFunc(nint a1, uint a2, nint a3, byte a4, byte a5, byte a6, byte a7);
|
|
||||||
|
|
||||||
private readonly Hook<WeaponReloadFunc> _weaponReloadHook;
|
|
||||||
|
|
||||||
private void WeaponReloadDetour(nint a1, uint a2, nint a3, byte a4, byte a5, byte a6, byte a7)
|
|
||||||
{
|
|
||||||
var gameObject = *(nint*)(a1 + 8);
|
|
||||||
if (WeaponReloading != null)
|
|
||||||
foreach (var subscriber in WeaponReloading.GetInvocationList())
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
((WeaponReloadingEvent)subscriber).Invoke(a1, gameObject);
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
Penumbra.Log.Error(
|
|
||||||
$"{Prefix} Error in {nameof(WeaponReloadDetour)} event when executing {subscriber.Method.Name}:\n{ex}");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
_weaponReloadHook.Original(a1, a2, a3, a4, a5, a6, a7);
|
|
||||||
|
|
||||||
if (WeaponReloaded != null)
|
|
||||||
foreach (var subscriber in WeaponReloaded.GetInvocationList())
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
((WeaponReloadedEvent)subscriber).Invoke(a1, gameObject);
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
Penumbra.Log.Error(
|
|
||||||
$"{Prefix} Error in {nameof(WeaponReloadDetour)} event when executing {subscriber.Method.Name}:\n{ex}");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public delegate void WeaponReloadingEvent(nint drawDataContainer, nint gameObject);
|
|
||||||
public delegate void WeaponReloadedEvent(nint drawDataContainer, nint gameObject);
|
|
||||||
|
|
||||||
#endregion
|
|
||||||
|
|
||||||
#region Testing
|
|
||||||
|
|
||||||
#if DEBUG
|
|
||||||
//[Signature("48 89 5C 24 ?? 48 89 74 24 ?? 89 54 24 ?? 57 48 83 EC ?? 48 8B F9", DetourName = nameof(TestDetour))]
|
|
||||||
private readonly Hook<TestDelegate>? _testHook = null;
|
|
||||||
|
|
||||||
private delegate void TestDelegate(nint a1, int a2);
|
|
||||||
|
|
||||||
private void TestDetour(nint a1, int a2)
|
|
||||||
{
|
|
||||||
Penumbra.Log.Information($"Test: {a1:X} {a2}");
|
|
||||||
_testHook!.Original(a1, a2);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void EnableDebugHook()
|
|
||||||
=> _testHook?.Enable();
|
|
||||||
|
|
||||||
private void DisposeDebugHook()
|
|
||||||
=> _testHook?.Dispose();
|
|
||||||
#else
|
|
||||||
private void EnableDebugHook()
|
|
||||||
{ }
|
|
||||||
|
|
||||||
private void DisposeDebugHook()
|
|
||||||
{ }
|
|
||||||
#endif
|
|
||||||
|
|
||||||
#endregion
|
|
||||||
}
|
|
||||||
|
|
@ -6,6 +6,7 @@ using FFXIVClientStructs.FFXIV.Client.System.Resource.Handle;
|
||||||
using OtterGui.Classes;
|
using OtterGui.Classes;
|
||||||
using Penumbra.Communication;
|
using Penumbra.Communication;
|
||||||
using Penumbra.GameData;
|
using Penumbra.GameData;
|
||||||
|
using Penumbra.Interop.Hooks;
|
||||||
using Penumbra.Services;
|
using Penumbra.Services;
|
||||||
|
|
||||||
namespace Penumbra.Interop.Services;
|
namespace Penumbra.Interop.Services;
|
||||||
|
|
@ -32,9 +33,9 @@ public sealed unsafe class SkinFixer : IDisposable
|
||||||
|
|
||||||
private readonly Hook<OnRenderMaterialDelegate> _onRenderMaterialHook;
|
private readonly Hook<OnRenderMaterialDelegate> _onRenderMaterialHook;
|
||||||
|
|
||||||
private readonly GameEventManager _gameEvents;
|
private readonly ResourceHandleDestructor _resourceHandleDestructor;
|
||||||
private readonly CommunicatorService _communicator;
|
private readonly CommunicatorService _communicator;
|
||||||
private readonly CharacterUtility _utility;
|
private readonly CharacterUtility _utility;
|
||||||
|
|
||||||
// MaterialResourceHandle set
|
// MaterialResourceHandle set
|
||||||
private readonly ConcurrentSet<nint> _moddedSkinShpkMaterials = new();
|
private readonly ConcurrentSet<nint> _moddedSkinShpkMaterials = new();
|
||||||
|
|
@ -50,15 +51,16 @@ public sealed unsafe class SkinFixer : IDisposable
|
||||||
public int ModdedSkinShpkCount
|
public int ModdedSkinShpkCount
|
||||||
=> _moddedSkinShpkCount;
|
=> _moddedSkinShpkCount;
|
||||||
|
|
||||||
public SkinFixer(GameEventManager gameEvents, CharacterUtility utility, CommunicatorService communicator, IGameInteropProvider interop)
|
public SkinFixer(ResourceHandleDestructor resourceHandleDestructor, CharacterUtility utility, CommunicatorService communicator,
|
||||||
|
IGameInteropProvider interop)
|
||||||
{
|
{
|
||||||
interop.InitializeFromAttributes(this);
|
interop.InitializeFromAttributes(this);
|
||||||
_gameEvents = gameEvents;
|
_resourceHandleDestructor = resourceHandleDestructor;
|
||||||
_utility = utility;
|
_utility = utility;
|
||||||
_communicator = communicator;
|
_communicator = communicator;
|
||||||
_onRenderMaterialHook = interop.HookFromAddress<OnRenderMaterialDelegate>(_humanVTable[62], OnRenderHumanMaterial);
|
_onRenderMaterialHook = interop.HookFromAddress<OnRenderMaterialDelegate>(_humanVTable[62], OnRenderHumanMaterial);
|
||||||
_communicator.MtrlShpkLoaded.Subscribe(OnMtrlShpkLoaded, MtrlShpkLoaded.Priority.SkinFixer);
|
_communicator.MtrlShpkLoaded.Subscribe(OnMtrlShpkLoaded, MtrlShpkLoaded.Priority.SkinFixer);
|
||||||
_gameEvents.ResourceHandleDestructor += OnResourceHandleDestructor;
|
_resourceHandleDestructor.Subscribe(OnResourceHandleDestructor, ResourceHandleDestructor.Priority.SkinFixer);
|
||||||
_onRenderMaterialHook.Enable();
|
_onRenderMaterialHook.Enable();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -66,7 +68,7 @@ public sealed unsafe class SkinFixer : IDisposable
|
||||||
{
|
{
|
||||||
_onRenderMaterialHook.Dispose();
|
_onRenderMaterialHook.Dispose();
|
||||||
_communicator.MtrlShpkLoaded.Unsubscribe(OnMtrlShpkLoaded);
|
_communicator.MtrlShpkLoaded.Unsubscribe(OnMtrlShpkLoaded);
|
||||||
_gameEvents.ResourceHandleDestructor -= OnResourceHandleDestructor;
|
_resourceHandleDestructor.Unsubscribe(OnResourceHandleDestructor);
|
||||||
_moddedSkinShpkMaterials.Clear();
|
_moddedSkinShpkMaterials.Clear();
|
||||||
_moddedSkinShpkCount = 0;
|
_moddedSkinShpkCount = 0;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,6 @@ using Dalamud.Utility;
|
||||||
using Newtonsoft.Json.Linq;
|
using Newtonsoft.Json.Linq;
|
||||||
using OtterGui.Classes;
|
using OtterGui.Classes;
|
||||||
using Penumbra.Services;
|
using Penumbra.Services;
|
||||||
using Penumbra.Util;
|
|
||||||
|
|
||||||
namespace Penumbra.Mods.Manager;
|
namespace Penumbra.Mods.Manager;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -80,6 +80,10 @@ public class Penumbra : IDalamudPlugin
|
||||||
_services.GetService<SkinFixer>();
|
_services.GetService<SkinFixer>();
|
||||||
|
|
||||||
_services.GetService<DalamudSubstitutionProvider>(); // Initialize before Interface.
|
_services.GetService<DalamudSubstitutionProvider>(); // Initialize before Interface.
|
||||||
|
|
||||||
|
foreach (var service in _services.GetServicesImplementing<IAwaitedService>())
|
||||||
|
service.Awaiter.Wait();
|
||||||
|
|
||||||
SetupInterface();
|
SetupInterface();
|
||||||
SetupApi();
|
SetupApi();
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -69,6 +69,7 @@
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="EmbedIO" Version="3.4.3" />
|
<PackageReference Include="EmbedIO" Version="3.4.3" />
|
||||||
|
<PackageReference Include="Microsoft.CodeAnalysis.Common" Version="4.8.0" />
|
||||||
<PackageReference Include="SixLabors.ImageSharp" Version="2.1.2" />
|
<PackageReference Include="SixLabors.ImageSharp" Version="2.1.2" />
|
||||||
<PackageReference Include="SharpCompress" Version="0.33.0" />
|
<PackageReference Include="SharpCompress" Version="0.33.0" />
|
||||||
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="7.0.0" />
|
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="7.0.0" />
|
||||||
|
|
@ -94,8 +95,8 @@
|
||||||
|
|
||||||
<Target Name="GetGitHash" BeforeTargets="GetAssemblyVersion" Returns="InformationalVersion">
|
<Target Name="GetGitHash" BeforeTargets="GetAssemblyVersion" Returns="InformationalVersion">
|
||||||
<Exec Command="git rev-parse --short HEAD" ConsoleToMSBuild="true" StandardOutputImportance="low" ContinueOnError="true">
|
<Exec Command="git rev-parse --short HEAD" ConsoleToMSBuild="true" StandardOutputImportance="low" ContinueOnError="true">
|
||||||
<Output TaskParameter="ExitCode" PropertyName="GitCommitHashSuccess"/>
|
<Output TaskParameter="ExitCode" PropertyName="GitCommitHashSuccess" />
|
||||||
<Output TaskParameter="ConsoleOutput" PropertyName="GitCommitHash" Condition="$(GitCommitHashSuccess) == 0"/>
|
<Output TaskParameter="ConsoleOutput" PropertyName="GitCommitHash" Condition="$(GitCommitHashSuccess) == 0" />
|
||||||
</Exec>
|
</Exec>
|
||||||
|
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
|
|
|
||||||
|
|
@ -1,15 +1,24 @@
|
||||||
using Newtonsoft.Json.Linq;
|
using Newtonsoft.Json.Linq;
|
||||||
using OtterGui.Classes;
|
using OtterGui.Classes;
|
||||||
using OtterGui.Log;
|
using OtterGui.Log;
|
||||||
|
using OtterGui.Services;
|
||||||
|
|
||||||
namespace Penumbra.Services;
|
namespace Penumbra.Services;
|
||||||
|
|
||||||
public class BackupService
|
public class BackupService : IAsyncService
|
||||||
{
|
{
|
||||||
|
/// <inheritdoc/>
|
||||||
|
public Task Awaiter { get; }
|
||||||
|
|
||||||
|
/// <inheritdoc/>
|
||||||
|
public bool Finished
|
||||||
|
=> Awaiter.IsCompletedSuccessfully;
|
||||||
|
|
||||||
|
/// <summary> Start a backup process on the collected files. </summary>
|
||||||
public BackupService(Logger logger, FilenameService fileNames)
|
public BackupService(Logger logger, FilenameService fileNames)
|
||||||
{
|
{
|
||||||
var files = PenumbraFiles(fileNames);
|
var files = PenumbraFiles(fileNames);
|
||||||
Backup.CreateAutomaticBackup(logger, new DirectoryInfo(fileNames.ConfigDirectory), files);
|
Awaiter = Task.Run(() => Backup.CreateAutomaticBackup(logger, new DirectoryInfo(fileNames.ConfigDirectory), files));
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary> Collect all relevant files for penumbra configuration. </summary>
|
/// <summary> Collect all relevant files for penumbra configuration. </summary>
|
||||||
|
|
|
||||||
|
|
@ -1,14 +1,15 @@
|
||||||
using OtterGui.Classes;
|
using OtterGui.Classes;
|
||||||
using OtterGui.Log;
|
using OtterGui.Log;
|
||||||
|
using OtterGui.Services;
|
||||||
using Penumbra.Communication;
|
using Penumbra.Communication;
|
||||||
|
|
||||||
namespace Penumbra.Services;
|
namespace Penumbra.Services;
|
||||||
|
|
||||||
public class CommunicatorService : IDisposable
|
public class CommunicatorService : IDisposable, IService
|
||||||
{
|
{
|
||||||
public CommunicatorService(Logger logger)
|
public CommunicatorService(Logger logger)
|
||||||
{
|
{
|
||||||
EventWrapper.ChangeLogger(logger);
|
EventWrapperBase.ChangeLogger(logger);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc cref="Communication.CollectionChange"/>
|
/// <inheritdoc cref="Communication.CollectionChange"/>
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
using Newtonsoft.Json;
|
using Newtonsoft.Json;
|
||||||
using Newtonsoft.Json.Linq;
|
using Newtonsoft.Json.Linq;
|
||||||
using OtterGui.Classes;
|
|
||||||
using OtterGui.Filesystem;
|
using OtterGui.Filesystem;
|
||||||
|
using OtterGui.Services;
|
||||||
using Penumbra.Api.Enums;
|
using Penumbra.Api.Enums;
|
||||||
using Penumbra.Collections;
|
using Penumbra.Collections;
|
||||||
using Penumbra.Collections.Manager;
|
using Penumbra.Collections.Manager;
|
||||||
|
|
@ -22,10 +22,8 @@ namespace Penumbra.Services;
|
||||||
/// Contains everything to migrate from older versions of the config to the current,
|
/// Contains everything to migrate from older versions of the config to the current,
|
||||||
/// including deprecated fields.
|
/// including deprecated fields.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public class ConfigMigrationService
|
public class ConfigMigrationService(SaveService saveService) : IService
|
||||||
{
|
{
|
||||||
private readonly SaveService _saveService;
|
|
||||||
|
|
||||||
private Configuration _config = null!;
|
private Configuration _config = null!;
|
||||||
private JObject _data = null!;
|
private JObject _data = null!;
|
||||||
|
|
||||||
|
|
@ -33,14 +31,11 @@ public class ConfigMigrationService
|
||||||
public string DefaultCollection = ModCollection.DefaultCollectionName;
|
public string DefaultCollection = ModCollection.DefaultCollectionName;
|
||||||
public string ForcedCollection = string.Empty;
|
public string ForcedCollection = string.Empty;
|
||||||
public Dictionary<string, string> CharacterCollections = [];
|
public Dictionary<string, string> CharacterCollections = [];
|
||||||
public Dictionary<string, string> ModSortOrder = new();
|
public Dictionary<string, string> ModSortOrder = [];
|
||||||
public bool InvertModListOrder;
|
public bool InvertModListOrder;
|
||||||
public bool SortFoldersFirst;
|
public bool SortFoldersFirst;
|
||||||
public SortModeV3 SortMode = SortModeV3.FoldersFirst;
|
public SortModeV3 SortMode = SortModeV3.FoldersFirst;
|
||||||
|
|
||||||
public ConfigMigrationService(SaveService saveService)
|
|
||||||
=> _saveService = saveService;
|
|
||||||
|
|
||||||
/// <summary> Add missing colors to the dictionary if necessary. </summary>
|
/// <summary> Add missing colors to the dictionary if necessary. </summary>
|
||||||
private static void AddColors(Configuration config, bool forceSave)
|
private static void AddColors(Configuration config, bool forceSave)
|
||||||
{
|
{
|
||||||
|
|
@ -61,13 +56,13 @@ public class ConfigMigrationService
|
||||||
// because it stayed alive for a bunch of people for some reason.
|
// because it stayed alive for a bunch of people for some reason.
|
||||||
DeleteMetaTmp();
|
DeleteMetaTmp();
|
||||||
|
|
||||||
if (config.Version >= Configuration.Constants.CurrentVersion || !File.Exists(_saveService.FileNames.ConfigFile))
|
if (config.Version >= Configuration.Constants.CurrentVersion || !File.Exists(saveService.FileNames.ConfigFile))
|
||||||
{
|
{
|
||||||
AddColors(config, false);
|
AddColors(config, false);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
_data = JObject.Parse(File.ReadAllText(_saveService.FileNames.ConfigFile));
|
_data = JObject.Parse(File.ReadAllText(saveService.FileNames.ConfigFile));
|
||||||
CreateBackup();
|
CreateBackup();
|
||||||
|
|
||||||
Version0To1();
|
Version0To1();
|
||||||
|
|
@ -118,7 +113,7 @@ public class ConfigMigrationService
|
||||||
if (_config.Version != 6)
|
if (_config.Version != 6)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
ActiveCollectionMigration.MigrateUngenderedCollections(_saveService.FileNames);
|
ActiveCollectionMigration.MigrateUngenderedCollections(saveService.FileNames);
|
||||||
_config.Version = 7;
|
_config.Version = 7;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -223,7 +218,7 @@ public class ConfigMigrationService
|
||||||
return;
|
return;
|
||||||
|
|
||||||
// Add the previous forced collection to all current collections except itself as an inheritance.
|
// Add the previous forced collection to all current collections except itself as an inheritance.
|
||||||
foreach (var collection in _saveService.FileNames.CollectionFiles)
|
foreach (var collection in saveService.FileNames.CollectionFiles)
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
|
|
@ -246,7 +241,7 @@ public class ConfigMigrationService
|
||||||
private void ResettleSortOrder()
|
private void ResettleSortOrder()
|
||||||
{
|
{
|
||||||
ModSortOrder = _data[nameof(ModSortOrder)]?.ToObject<Dictionary<string, string>>() ?? ModSortOrder;
|
ModSortOrder = _data[nameof(ModSortOrder)]?.ToObject<Dictionary<string, string>>() ?? ModSortOrder;
|
||||||
var file = _saveService.FileNames.FilesystemFile;
|
var file = saveService.FileNames.FilesystemFile;
|
||||||
using var stream = File.Open(file, File.Exists(file) ? FileMode.Truncate : FileMode.CreateNew);
|
using var stream = File.Open(file, File.Exists(file) ? FileMode.Truncate : FileMode.CreateNew);
|
||||||
using var writer = new StreamWriter(stream);
|
using var writer = new StreamWriter(stream);
|
||||||
using var j = new JsonTextWriter(writer);
|
using var j = new JsonTextWriter(writer);
|
||||||
|
|
@ -281,7 +276,7 @@ public class ConfigMigrationService
|
||||||
private void SaveActiveCollectionsV0(string def, string ui, string current, IEnumerable<(string, string)> characters,
|
private void SaveActiveCollectionsV0(string def, string ui, string current, IEnumerable<(string, string)> characters,
|
||||||
IEnumerable<(CollectionType, string)> special)
|
IEnumerable<(CollectionType, string)> special)
|
||||||
{
|
{
|
||||||
var file = _saveService.FileNames.ActiveCollectionsFile;
|
var file = saveService.FileNames.ActiveCollectionsFile;
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
using var stream = File.Open(file, File.Exists(file) ? FileMode.Truncate : FileMode.CreateNew);
|
using var stream = File.Open(file, File.Exists(file) ? FileMode.Truncate : FileMode.CreateNew);
|
||||||
|
|
@ -337,7 +332,7 @@ public class ConfigMigrationService
|
||||||
if (!collectionJson.Exists)
|
if (!collectionJson.Exists)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
var defaultCollectionFile = new FileInfo(_saveService.FileNames.CollectionFile(ModCollection.DefaultCollectionName));
|
var defaultCollectionFile = new FileInfo(saveService.FileNames.CollectionFile(ModCollection.DefaultCollectionName));
|
||||||
if (defaultCollectionFile.Exists)
|
if (defaultCollectionFile.Exists)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
|
|
@ -370,9 +365,9 @@ public class ConfigMigrationService
|
||||||
dict = dict.ToDictionary(kvp => kvp.Key, kvp => kvp.Value with { Priority = maxPriority - kvp.Value.Priority });
|
dict = dict.ToDictionary(kvp => kvp.Key, kvp => kvp.Value with { Priority = maxPriority - kvp.Value.Priority });
|
||||||
|
|
||||||
var emptyStorage = new ModStorage();
|
var emptyStorage = new ModStorage();
|
||||||
var collection = ModCollection.CreateFromData(_saveService, emptyStorage, ModCollection.DefaultCollectionName, 0, 1, dict,
|
var collection = ModCollection.CreateFromData(saveService, emptyStorage, ModCollection.DefaultCollectionName, 0, 1, dict,
|
||||||
Array.Empty<string>());
|
Array.Empty<string>());
|
||||||
_saveService.ImmediateSaveSync(new ModCollectionSave(emptyStorage, collection));
|
saveService.ImmediateSaveSync(new ModCollectionSave(emptyStorage, collection));
|
||||||
}
|
}
|
||||||
catch (Exception e)
|
catch (Exception e)
|
||||||
{
|
{
|
||||||
|
|
@ -384,7 +379,7 @@ public class ConfigMigrationService
|
||||||
// Create a backup of the configuration file specifically.
|
// Create a backup of the configuration file specifically.
|
||||||
private void CreateBackup()
|
private void CreateBackup()
|
||||||
{
|
{
|
||||||
var name = _saveService.FileNames.ConfigFile;
|
var name = saveService.FileNames.ConfigFile;
|
||||||
var bakName = name + ".bak";
|
var bakName = name + ".bak";
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -1,21 +1,12 @@
|
||||||
using Dalamud.Game;
|
|
||||||
using Dalamud.Game.ClientState.Objects;
|
|
||||||
using Dalamud.Interface;
|
|
||||||
using Dalamud.IoC;
|
|
||||||
using Dalamud.Plugin;
|
using Dalamud.Plugin;
|
||||||
using Dalamud.Interface.DragDrop;
|
|
||||||
using Dalamud.Plugin.Services;
|
|
||||||
using OtterGui.Services;
|
using OtterGui.Services;
|
||||||
|
|
||||||
// ReSharper disable AutoPropertyCanBeMadeGetOnly.Local
|
|
||||||
|
|
||||||
namespace Penumbra.Services;
|
namespace Penumbra.Services;
|
||||||
|
|
||||||
public class DalamudConfigService
|
public class DalamudConfigService : IService
|
||||||
{
|
{
|
||||||
public DalamudConfigService(DalamudPluginInterface pluginInterface)
|
public DalamudConfigService()
|
||||||
{
|
{
|
||||||
pluginInterface.Inject(this);
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
var serviceType =
|
var serviceType =
|
||||||
|
|
@ -115,29 +106,3 @@ public class DalamudConfigService
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public static class DalamudServices
|
|
||||||
{
|
|
||||||
public static void AddServices(ServiceManager services, DalamudPluginInterface pi)
|
|
||||||
{
|
|
||||||
services.AddExistingService(pi);
|
|
||||||
services.AddExistingService(pi.UiBuilder);
|
|
||||||
services.AddDalamudService<ICommandManager>(pi);
|
|
||||||
services.AddDalamudService<IDataManager>(pi);
|
|
||||||
services.AddDalamudService<IClientState>(pi);
|
|
||||||
services.AddDalamudService<IChatGui>(pi);
|
|
||||||
services.AddDalamudService<IFramework>(pi);
|
|
||||||
services.AddDalamudService<ICondition>(pi);
|
|
||||||
services.AddDalamudService<ITargetManager>(pi);
|
|
||||||
services.AddDalamudService<IObjectTable>(pi);
|
|
||||||
services.AddDalamudService<ITitleScreenMenu>(pi);
|
|
||||||
services.AddDalamudService<IGameGui>(pi);
|
|
||||||
services.AddDalamudService<IKeyState>(pi);
|
|
||||||
services.AddDalamudService<ISigScanner>(pi);
|
|
||||||
services.AddDalamudService<IDragDropManager>(pi);
|
|
||||||
services.AddDalamudService<ITextureProvider>(pi);
|
|
||||||
services.AddDalamudService<ITextureSubstitutionProvider>(pi);
|
|
||||||
services.AddDalamudService<IGameInteropProvider>(pi);
|
|
||||||
services.AddDalamudService<IPluginLog>(pi);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,11 +1,11 @@
|
||||||
using Dalamud.Plugin;
|
using Dalamud.Plugin;
|
||||||
using OtterGui.Filesystem;
|
using OtterGui.Services;
|
||||||
using Penumbra.Collections;
|
using Penumbra.Collections;
|
||||||
using Penumbra.Mods;
|
using Penumbra.Mods;
|
||||||
|
|
||||||
namespace Penumbra.Services;
|
namespace Penumbra.Services;
|
||||||
|
|
||||||
public class FilenameService(DalamudPluginInterface pi)
|
public class FilenameService(DalamudPluginInterface pi) : IService
|
||||||
{
|
{
|
||||||
public readonly string ConfigDirectory = pi.ConfigDirectory.FullName;
|
public readonly string ConfigDirectory = pi.ConfigDirectory.FullName;
|
||||||
public readonly string CollectionDirectory = Path.Combine(pi.ConfigDirectory.FullName, "collections");
|
public readonly string CollectionDirectory = Path.Combine(pi.ConfigDirectory.FullName, "collections");
|
||||||
|
|
|
||||||
|
|
@ -5,15 +5,12 @@ using Dalamud.Interface;
|
||||||
using Dalamud.Plugin.Services;
|
using Dalamud.Plugin.Services;
|
||||||
using Lumina.Excel.GeneratedSheets;
|
using Lumina.Excel.GeneratedSheets;
|
||||||
using OtterGui.Log;
|
using OtterGui.Log;
|
||||||
|
using OtterGui.Services;
|
||||||
|
|
||||||
namespace Penumbra.Services;
|
namespace Penumbra.Services;
|
||||||
|
|
||||||
public class MessageService : OtterGui.Classes.MessageService
|
public class MessageService(Logger log, UiBuilder uiBuilder, IChatGui chat) : OtterGui.Classes.MessageService(log, uiBuilder, chat), IService
|
||||||
{
|
{
|
||||||
public MessageService(Logger log, UiBuilder uiBuilder, IChatGui chat)
|
|
||||||
: base(log, uiBuilder, chat)
|
|
||||||
{ }
|
|
||||||
|
|
||||||
public void LinkItem(Item item)
|
public void LinkItem(Item item)
|
||||||
{
|
{
|
||||||
// @formatter:off
|
// @formatter:off
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
using OtterGui.Classes;
|
using OtterGui.Classes;
|
||||||
using OtterGui.Log;
|
using OtterGui.Log;
|
||||||
|
using OtterGui.Services;
|
||||||
using Penumbra.Mods;
|
using Penumbra.Mods;
|
||||||
using Penumbra.Mods.Subclasses;
|
using Penumbra.Mods.Subclasses;
|
||||||
|
|
||||||
|
|
@ -11,12 +12,9 @@ namespace Penumbra.Services;
|
||||||
public interface ISavable : ISavable<FilenameService>
|
public interface ISavable : ISavable<FilenameService>
|
||||||
{ }
|
{ }
|
||||||
|
|
||||||
public sealed class SaveService : SaveServiceBase<FilenameService>
|
public sealed class SaveService(Logger log, FrameworkManager framework, FilenameService fileNames, BackupService backupService)
|
||||||
|
: SaveServiceBase<FilenameService>(log, framework, fileNames, backupService.Awaiter), IService
|
||||||
{
|
{
|
||||||
public SaveService(Logger log, FrameworkManager framework, FilenameService fileNames)
|
|
||||||
: base(log, framework, fileNames)
|
|
||||||
{ }
|
|
||||||
|
|
||||||
/// <summary> Immediately delete all existing option group files for a mod and save them anew. </summary>
|
/// <summary> Immediately delete all existing option group files for a mod and save them anew. </summary>
|
||||||
public void SaveAllOptionGroups(Mod mod, bool backup, bool onlyAscii)
|
public void SaveAllOptionGroups(Mod mod, bool backup, bool onlyAscii)
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,10 @@
|
||||||
|
using Dalamud.Game;
|
||||||
|
using Dalamud.Game.ClientState.Objects;
|
||||||
|
using Dalamud.Interface.DragDrop;
|
||||||
using Dalamud.Plugin;
|
using Dalamud.Plugin;
|
||||||
|
using Dalamud.Plugin.Services;
|
||||||
using Microsoft.Extensions.DependencyInjection;
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
|
using OtterGui;
|
||||||
using OtterGui.Classes;
|
using OtterGui.Classes;
|
||||||
using OtterGui.Compression;
|
using OtterGui.Compression;
|
||||||
using OtterGui.Log;
|
using OtterGui.Log;
|
||||||
|
|
@ -8,7 +13,6 @@ using Penumbra.Api;
|
||||||
using Penumbra.Collections.Cache;
|
using Penumbra.Collections.Cache;
|
||||||
using Penumbra.Collections.Manager;
|
using Penumbra.Collections.Manager;
|
||||||
using Penumbra.GameData.Actors;
|
using Penumbra.GameData.Actors;
|
||||||
using Penumbra.GameData.Data;
|
|
||||||
using Penumbra.Import.Models;
|
using Penumbra.Import.Models;
|
||||||
using Penumbra.GameData.DataContainers;
|
using Penumbra.GameData.DataContainers;
|
||||||
using Penumbra.GameData.Structs;
|
using Penumbra.GameData.Structs;
|
||||||
|
|
@ -38,10 +42,9 @@ public static class ServiceManagerA
|
||||||
public static ServiceManager CreateProvider(Penumbra penumbra, DalamudPluginInterface pi, Logger log)
|
public static ServiceManager CreateProvider(Penumbra penumbra, DalamudPluginInterface pi, Logger log)
|
||||||
{
|
{
|
||||||
var services = new ServiceManager(log)
|
var services = new ServiceManager(log)
|
||||||
|
.AddDalamudServices(pi)
|
||||||
.AddExistingService(log)
|
.AddExistingService(log)
|
||||||
.AddExistingService(penumbra)
|
.AddExistingService(penumbra)
|
||||||
.AddMeta()
|
|
||||||
.AddGameData()
|
|
||||||
.AddInterop()
|
.AddInterop()
|
||||||
.AddConfiguration()
|
.AddConfiguration()
|
||||||
.AddCollections()
|
.AddCollections()
|
||||||
|
|
@ -53,31 +56,34 @@ public static class ServiceManagerA
|
||||||
.AddApi();
|
.AddApi();
|
||||||
services.AddIServices(typeof(EquipItem).Assembly);
|
services.AddIServices(typeof(EquipItem).Assembly);
|
||||||
services.AddIServices(typeof(Penumbra).Assembly);
|
services.AddIServices(typeof(Penumbra).Assembly);
|
||||||
DalamudServices.AddServices(services, pi);
|
services.AddIServices(typeof(ImGuiUtil).Assembly);
|
||||||
services.CreateProvider();
|
services.CreateProvider();
|
||||||
return services;
|
return services;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static ServiceManager AddMeta(this ServiceManager services)
|
private static ServiceManager AddDalamudServices(this ServiceManager services, DalamudPluginInterface pi)
|
||||||
=> services.AddSingleton<ValidityChecker>()
|
=> services.AddExistingService(pi)
|
||||||
.AddSingleton<PerformanceTracker>()
|
.AddExistingService(pi.UiBuilder)
|
||||||
.AddSingleton<FilenameService>()
|
.AddDalamudService<ICommandManager>(pi)
|
||||||
.AddSingleton<BackupService>()
|
.AddDalamudService<IDataManager>(pi)
|
||||||
.AddSingleton<CommunicatorService>()
|
.AddDalamudService<IClientState>(pi)
|
||||||
.AddSingleton<MessageService>()
|
.AddDalamudService<IChatGui>(pi)
|
||||||
.AddSingleton<SaveService>()
|
.AddDalamudService<IFramework>(pi)
|
||||||
.AddSingleton<FileCompactor>()
|
.AddDalamudService<ICondition>(pi)
|
||||||
.AddSingleton<DalamudConfigService>();
|
.AddDalamudService<ITargetManager>(pi)
|
||||||
|
.AddDalamudService<IObjectTable>(pi)
|
||||||
|
.AddDalamudService<ITitleScreenMenu>(pi)
|
||||||
private static ServiceManager AddGameData(this ServiceManager services)
|
.AddDalamudService<IGameGui>(pi)
|
||||||
=> services.AddSingleton<GamePathParser>()
|
.AddDalamudService<IKeyState>(pi)
|
||||||
.AddSingleton<StainService>()
|
.AddDalamudService<ISigScanner>(pi)
|
||||||
.AddSingleton<HumanModelList>();
|
.AddDalamudService<IDragDropManager>(pi)
|
||||||
|
.AddDalamudService<ITextureProvider>(pi)
|
||||||
|
.AddDalamudService<ITextureSubstitutionProvider>(pi)
|
||||||
|
.AddDalamudService<IGameInteropProvider>(pi)
|
||||||
|
.AddDalamudService<IPluginLog>(pi);
|
||||||
|
|
||||||
private static ServiceManager AddInterop(this ServiceManager services)
|
private static ServiceManager AddInterop(this ServiceManager services)
|
||||||
=> services.AddSingleton<GameEventManager>()
|
=> services.AddSingleton<FrameworkManager>()
|
||||||
.AddSingleton<FrameworkManager>()
|
|
||||||
.AddSingleton<CutsceneService>()
|
.AddSingleton<CutsceneService>()
|
||||||
.AddSingleton(p =>
|
.AddSingleton(p =>
|
||||||
{
|
{
|
||||||
|
|
@ -96,8 +102,7 @@ public static class ServiceManagerA
|
||||||
.AddSingleton<ModelResourceHandleUtility>();
|
.AddSingleton<ModelResourceHandleUtility>();
|
||||||
|
|
||||||
private static ServiceManager AddConfiguration(this ServiceManager services)
|
private static ServiceManager AddConfiguration(this ServiceManager services)
|
||||||
=> services.AddSingleton<ConfigMigrationService>()
|
=> services.AddSingleton<Configuration>()
|
||||||
.AddSingleton<Configuration>()
|
|
||||||
.AddSingleton<EphemeralConfig>();
|
.AddSingleton<EphemeralConfig>();
|
||||||
|
|
||||||
private static ServiceManager AddCollections(this ServiceManager services)
|
private static ServiceManager AddCollections(this ServiceManager services)
|
||||||
|
|
@ -130,8 +135,7 @@ public static class ServiceManagerA
|
||||||
.AddSingleton<SkinFixer>();
|
.AddSingleton<SkinFixer>();
|
||||||
|
|
||||||
private static ServiceManager AddResolvers(this ServiceManager services)
|
private static ServiceManager AddResolvers(this ServiceManager services)
|
||||||
=> services.AddSingleton<AnimationHookService>()
|
=> services.AddSingleton<CollectionResolver>()
|
||||||
.AddSingleton<CollectionResolver>()
|
|
||||||
.AddSingleton<CutsceneService>()
|
.AddSingleton<CutsceneService>()
|
||||||
.AddSingleton<DrawObjectState>()
|
.AddSingleton<DrawObjectState>()
|
||||||
.AddSingleton<MetaState>()
|
.AddSingleton<MetaState>()
|
||||||
|
|
|
||||||
|
|
@ -1,36 +1,26 @@
|
||||||
using Dalamud.Interface;
|
using Dalamud.Interface;
|
||||||
using Dalamud.Interface.Utility.Raii;
|
using Dalamud.Interface.Utility.Raii;
|
||||||
using Dalamud.Plugin;
|
|
||||||
using Dalamud.Plugin.Services;
|
using Dalamud.Plugin.Services;
|
||||||
using ImGuiNET;
|
using ImGuiNET;
|
||||||
using OtterGui.Log;
|
using OtterGui.Services;
|
||||||
using OtterGui.Widgets;
|
using OtterGui.Widgets;
|
||||||
using Penumbra.GameData.DataContainers;
|
using Penumbra.GameData.DataContainers;
|
||||||
using Penumbra.GameData.Files;
|
using Penumbra.GameData.Files;
|
||||||
using Penumbra.UI.AdvancedWindow;
|
using Penumbra.UI.AdvancedWindow;
|
||||||
using Penumbra.Util;
|
|
||||||
|
|
||||||
namespace Penumbra.Services;
|
namespace Penumbra.Services;
|
||||||
|
|
||||||
public class StainService : IDisposable
|
public class StainService : IService
|
||||||
{
|
{
|
||||||
public sealed class StainTemplateCombo : FilterComboCache<ushort>
|
public sealed class StainTemplateCombo(FilterComboColors stainCombo, StmFile stmFile)
|
||||||
|
: FilterComboCache<ushort>(stmFile.Entries.Keys.Prepend((ushort)0), Penumbra.Log)
|
||||||
{
|
{
|
||||||
private readonly StmFile _stmFile;
|
|
||||||
private readonly FilterComboColors _stainCombo;
|
|
||||||
|
|
||||||
public StainTemplateCombo(FilterComboColors stainCombo, StmFile stmFile)
|
|
||||||
: base(stmFile.Entries.Keys.Prepend((ushort)0), Penumbra.Log)
|
|
||||||
{
|
|
||||||
_stainCombo = stainCombo;
|
|
||||||
_stmFile = stmFile;
|
|
||||||
}
|
|
||||||
|
|
||||||
protected override float GetFilterWidth()
|
protected override float GetFilterWidth()
|
||||||
{
|
{
|
||||||
var baseSize = ImGui.CalcTextSize("0000").X + ImGui.GetStyle().ScrollbarSize + ImGui.GetStyle().ItemInnerSpacing.X;
|
var baseSize = ImGui.CalcTextSize("0000").X + ImGui.GetStyle().ScrollbarSize + ImGui.GetStyle().ItemInnerSpacing.X;
|
||||||
if (_stainCombo.CurrentSelection.Key == 0)
|
if (stainCombo.CurrentSelection.Key == 0)
|
||||||
return baseSize;
|
return baseSize;
|
||||||
|
|
||||||
return baseSize + ImGui.GetTextLineHeight() * 3 + ImGui.GetStyle().ItemInnerSpacing.X * 3;
|
return baseSize + ImGui.GetTextLineHeight() * 3 + ImGui.GetStyle().ItemInnerSpacing.X * 3;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -46,19 +36,19 @@ public class StainService : IDisposable
|
||||||
public override bool Draw(string label, string preview, string tooltip, ref int currentSelection, float previewWidth, float itemHeight,
|
public override bool Draw(string label, string preview, string tooltip, ref int currentSelection, float previewWidth, float itemHeight,
|
||||||
ImGuiComboFlags flags = ImGuiComboFlags.None)
|
ImGuiComboFlags flags = ImGuiComboFlags.None)
|
||||||
{
|
{
|
||||||
using var font = ImRaii.PushFont(UiBuilder.MonoFont);
|
using var font = ImRaii.PushFont(UiBuilder.MonoFont);
|
||||||
using var style = ImRaii.PushStyle(ImGuiStyleVar.ButtonTextAlign, new Vector2(1, 0.5f))
|
using var style = ImRaii.PushStyle(ImGuiStyleVar.ButtonTextAlign, new Vector2(1, 0.5f))
|
||||||
.Push(ImGuiStyleVar.ItemSpacing, ImGui.GetStyle().ItemSpacing with { X = ImGui.GetStyle().ItemInnerSpacing.X });
|
.Push(ImGuiStyleVar.ItemSpacing, ImGui.GetStyle().ItemSpacing with { X = ImGui.GetStyle().ItemInnerSpacing.X });
|
||||||
var spaceSize = ImGui.CalcTextSize(" ").X;
|
var spaceSize = ImGui.CalcTextSize(" ").X;
|
||||||
var spaces = (int) (previewWidth / spaceSize) - 1;
|
var spaces = (int)(previewWidth / spaceSize) - 1;
|
||||||
return base.Draw(label, preview.PadLeft(spaces), tooltip, ref currentSelection, previewWidth, itemHeight, flags);
|
return base.Draw(label, preview.PadLeft(spaces), tooltip, ref currentSelection, previewWidth, itemHeight, flags);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override bool DrawSelectable(int globalIdx, bool selected)
|
protected override bool DrawSelectable(int globalIdx, bool selected)
|
||||||
{
|
{
|
||||||
var ret = base.DrawSelectable(globalIdx, selected);
|
var ret = base.DrawSelectable(globalIdx, selected);
|
||||||
var selection = _stainCombo.CurrentSelection.Key;
|
var selection = stainCombo.CurrentSelection.Key;
|
||||||
if (selection == 0 || !_stmFile.TryGetValue(Items[globalIdx], selection, out var colors))
|
if (selection == 0 || !stmFile.TryGetValue(Items[globalIdx], selection, out var colors))
|
||||||
return ret;
|
return ret;
|
||||||
|
|
||||||
ImGui.SameLine();
|
ImGui.SameLine();
|
||||||
|
|
@ -72,25 +62,18 @@ public class StainService : IDisposable
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public readonly DictStains StainData;
|
public readonly DictStain StainData;
|
||||||
public readonly FilterComboColors StainCombo;
|
public readonly FilterComboColors StainCombo;
|
||||||
public readonly StmFile StmFile;
|
public readonly StmFile StmFile;
|
||||||
public readonly StainTemplateCombo TemplateCombo;
|
public readonly StainTemplateCombo TemplateCombo;
|
||||||
|
|
||||||
public StainService(DalamudPluginInterface pluginInterface, IDataManager dataManager, Logger logger)
|
public StainService(IDataManager dataManager, DictStain stainData)
|
||||||
{
|
{
|
||||||
StainData = new DictStains(pluginInterface, logger, dataManager);
|
StainData = stainData;
|
||||||
StainCombo = new FilterComboColors(140,
|
StainCombo = new FilterComboColors(140,
|
||||||
() => StainData.Value.Prepend(new KeyValuePair<byte, (string Name, uint Dye, bool Gloss)>(0, ("None", 0, false))).ToList(),
|
() => StainData.Value.Prepend(new KeyValuePair<byte, (string Name, uint Dye, bool Gloss)>(0, ("None", 0, false))).ToList(),
|
||||||
Penumbra.Log);
|
Penumbra.Log);
|
||||||
StmFile = new StmFile(dataManager);
|
StmFile = new StmFile(dataManager);
|
||||||
TemplateCombo = new StainTemplateCombo(StainCombo, StmFile);
|
TemplateCombo = new StainTemplateCombo(StainCombo, StmFile);
|
||||||
Penumbra.Log.Verbose($"[{nameof(StainService)}] Created.");
|
|
||||||
}
|
|
||||||
|
|
||||||
public void Dispose()
|
|
||||||
{
|
|
||||||
StainData.Dispose();
|
|
||||||
Penumbra.Log.Verbose($"[{nameof(StainService)}] Disposed.");
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,11 @@
|
||||||
using Dalamud.Interface.Internal.Notifications;
|
using Dalamud.Interface.Internal.Notifications;
|
||||||
using Dalamud.Plugin;
|
using Dalamud.Plugin;
|
||||||
using OtterGui.Classes;
|
using OtterGui.Classes;
|
||||||
|
using OtterGui.Services;
|
||||||
|
|
||||||
namespace Penumbra.Services;
|
namespace Penumbra.Services;
|
||||||
|
|
||||||
public class ValidityChecker
|
public class ValidityChecker : IService
|
||||||
{
|
{
|
||||||
public const string Repository = "https://raw.githubusercontent.com/xivdev/Penumbra/master/repo.json";
|
public const string Repository = "https://raw.githubusercontent.com/xivdev/Penumbra/master/repo.json";
|
||||||
public const string SeaOfStars = "https://raw.githubusercontent.com/Ottermandias/SeaOfStars/main/repo.json";
|
public const string SeaOfStars = "https://raw.githubusercontent.com/Ottermandias/SeaOfStars/main/repo.json";
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
using Dalamud.Interface;
|
using Dalamud.Interface;
|
||||||
using Dalamud.Interface.Internal.Notifications;
|
using Dalamud.Interface.Internal.Notifications;
|
||||||
|
using FFXIVClientStructs.FFXIV.Client.Graphics.Scene;
|
||||||
using ImGuiNET;
|
using ImGuiNET;
|
||||||
using Newtonsoft.Json.Linq;
|
using Newtonsoft.Json.Linq;
|
||||||
using OtterGui;
|
using OtterGui;
|
||||||
|
|
@ -8,6 +9,7 @@ using OtterGui.Raii;
|
||||||
using Penumbra.GameData.Data;
|
using Penumbra.GameData.Data;
|
||||||
using Penumbra.GameData.Files;
|
using Penumbra.GameData.Files;
|
||||||
using Penumbra.GameData.Structs;
|
using Penumbra.GameData.Structs;
|
||||||
|
using Penumbra.Interop.Hooks;
|
||||||
using Penumbra.Interop.MaterialPreview;
|
using Penumbra.Interop.MaterialPreview;
|
||||||
using Penumbra.String;
|
using Penumbra.String;
|
||||||
using Penumbra.String.Classes;
|
using Penumbra.String.Classes;
|
||||||
|
|
@ -503,12 +505,12 @@ public partial class ModEditWindow
|
||||||
ColorTablePreviewers.Clear();
|
ColorTablePreviewers.Clear();
|
||||||
}
|
}
|
||||||
|
|
||||||
private unsafe void UnbindFromDrawObjectMaterialInstances(nint characterBase)
|
private unsafe void UnbindFromDrawObjectMaterialInstances(CharacterBase* characterBase)
|
||||||
{
|
{
|
||||||
for (var i = MaterialPreviewers.Count; i-- > 0;)
|
for (var i = MaterialPreviewers.Count; i-- > 0;)
|
||||||
{
|
{
|
||||||
var previewer = MaterialPreviewers[i];
|
var previewer = MaterialPreviewers[i];
|
||||||
if ((nint)previewer.DrawObject != characterBase)
|
if (previewer.DrawObject != characterBase)
|
||||||
continue;
|
continue;
|
||||||
|
|
||||||
previewer.Dispose();
|
previewer.Dispose();
|
||||||
|
|
@ -518,7 +520,7 @@ public partial class ModEditWindow
|
||||||
for (var i = ColorTablePreviewers.Count; i-- > 0;)
|
for (var i = ColorTablePreviewers.Count; i-- > 0;)
|
||||||
{
|
{
|
||||||
var previewer = ColorTablePreviewers[i];
|
var previewer = ColorTablePreviewers[i];
|
||||||
if ((nint)previewer.DrawObject != characterBase)
|
if (previewer.DrawObject != characterBase)
|
||||||
continue;
|
continue;
|
||||||
|
|
||||||
previewer.Dispose();
|
previewer.Dispose();
|
||||||
|
|
@ -663,7 +665,7 @@ public partial class ModEditWindow
|
||||||
UpdateConstants();
|
UpdateConstants();
|
||||||
}
|
}
|
||||||
|
|
||||||
public MtrlTab(ModEditWindow edit, MtrlFile file, string filePath, bool writable)
|
public unsafe MtrlTab(ModEditWindow edit, MtrlFile file, string filePath, bool writable)
|
||||||
{
|
{
|
||||||
_edit = edit;
|
_edit = edit;
|
||||||
Mtrl = file;
|
Mtrl = file;
|
||||||
|
|
@ -673,16 +675,16 @@ public partial class ModEditWindow
|
||||||
LoadShpk(FindAssociatedShpk(out _, out _));
|
LoadShpk(FindAssociatedShpk(out _, out _));
|
||||||
if (writable)
|
if (writable)
|
||||||
{
|
{
|
||||||
_edit._gameEvents.CharacterBaseDestructor += UnbindFromDrawObjectMaterialInstances;
|
_edit._characterBaseDestructor.Subscribe(UnbindFromDrawObjectMaterialInstances, CharacterBaseDestructor.Priority.MtrlTab);
|
||||||
BindToMaterialInstances();
|
BindToMaterialInstances();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public void Dispose()
|
public unsafe void Dispose()
|
||||||
{
|
{
|
||||||
UnbindFromMaterialInstances();
|
UnbindFromMaterialInstances();
|
||||||
if (Writable)
|
if (Writable)
|
||||||
_edit._gameEvents.CharacterBaseDestructor -= UnbindFromDrawObjectMaterialInstances;
|
_edit._characterBaseDestructor.Unsubscribe(UnbindFromDrawObjectMaterialInstances);
|
||||||
}
|
}
|
||||||
|
|
||||||
public bool Valid
|
public bool Valid
|
||||||
|
|
|
||||||
|
|
@ -14,8 +14,8 @@ using Penumbra.GameData.Enums;
|
||||||
using Penumbra.GameData.Files;
|
using Penumbra.GameData.Files;
|
||||||
using Penumbra.Import.Models;
|
using Penumbra.Import.Models;
|
||||||
using Penumbra.Import.Textures;
|
using Penumbra.Import.Textures;
|
||||||
|
using Penumbra.Interop.Hooks;
|
||||||
using Penumbra.Interop.ResourceTree;
|
using Penumbra.Interop.ResourceTree;
|
||||||
using Penumbra.Interop.Services;
|
|
||||||
using Penumbra.Meta;
|
using Penumbra.Meta;
|
||||||
using Penumbra.Mods;
|
using Penumbra.Mods;
|
||||||
using Penumbra.Mods.Editor;
|
using Penumbra.Mods.Editor;
|
||||||
|
|
@ -33,20 +33,20 @@ public partial class ModEditWindow : Window, IDisposable
|
||||||
{
|
{
|
||||||
private const string WindowBaseLabel = "###SubModEdit";
|
private const string WindowBaseLabel = "###SubModEdit";
|
||||||
|
|
||||||
private readonly PerformanceTracker _performance;
|
private readonly PerformanceTracker _performance;
|
||||||
private readonly ModEditor _editor;
|
private readonly ModEditor _editor;
|
||||||
private readonly Configuration _config;
|
private readonly Configuration _config;
|
||||||
private readonly ItemSwapTab _itemSwapTab;
|
private readonly ItemSwapTab _itemSwapTab;
|
||||||
private readonly MetaFileManager _metaFileManager;
|
private readonly MetaFileManager _metaFileManager;
|
||||||
private readonly ActiveCollections _activeCollections;
|
private readonly ActiveCollections _activeCollections;
|
||||||
private readonly StainService _stainService;
|
private readonly StainService _stainService;
|
||||||
private readonly ModMergeTab _modMergeTab;
|
private readonly ModMergeTab _modMergeTab;
|
||||||
private readonly CommunicatorService _communicator;
|
private readonly CommunicatorService _communicator;
|
||||||
private readonly IDragDropManager _dragDropManager;
|
private readonly IDragDropManager _dragDropManager;
|
||||||
private readonly GameEventManager _gameEvents;
|
private readonly IDataManager _gameData;
|
||||||
private readonly IDataManager _gameData;
|
private readonly IFramework _framework;
|
||||||
private readonly IFramework _framework;
|
private readonly IObjectTable _objects;
|
||||||
private readonly IObjectTable _objects;
|
private readonly CharacterBaseDestructor _characterBaseDestructor;
|
||||||
|
|
||||||
private Mod? _mod;
|
private Mod? _mod;
|
||||||
private Vector2 _iconSize = Vector2.Zero;
|
private Vector2 _iconSize = Vector2.Zero;
|
||||||
|
|
@ -566,27 +566,27 @@ public partial class ModEditWindow : Window, IDisposable
|
||||||
public ModEditWindow(PerformanceTracker performance, FileDialogService fileDialog, ItemSwapTab itemSwapTab, IDataManager gameData,
|
public ModEditWindow(PerformanceTracker performance, FileDialogService fileDialog, ItemSwapTab itemSwapTab, IDataManager gameData,
|
||||||
Configuration config, ModEditor editor, ResourceTreeFactory resourceTreeFactory, MetaFileManager metaFileManager,
|
Configuration config, ModEditor editor, ResourceTreeFactory resourceTreeFactory, MetaFileManager metaFileManager,
|
||||||
StainService stainService, ActiveCollections activeCollections, ModMergeTab modMergeTab,
|
StainService stainService, ActiveCollections activeCollections, ModMergeTab modMergeTab,
|
||||||
CommunicatorService communicator, TextureManager textures, ModelManager models, IDragDropManager dragDropManager, GameEventManager gameEvents,
|
CommunicatorService communicator, TextureManager textures, ModelManager models, IDragDropManager dragDropManager,
|
||||||
ChangedItemDrawer changedItemDrawer, IObjectTable objects, IFramework framework)
|
ChangedItemDrawer changedItemDrawer, IObjectTable objects, IFramework framework, CharacterBaseDestructor characterBaseDestructor)
|
||||||
: base(WindowBaseLabel)
|
: base(WindowBaseLabel)
|
||||||
{
|
{
|
||||||
_performance = performance;
|
_performance = performance;
|
||||||
_itemSwapTab = itemSwapTab;
|
_itemSwapTab = itemSwapTab;
|
||||||
_gameData = gameData;
|
_gameData = gameData;
|
||||||
_config = config;
|
_config = config;
|
||||||
_editor = editor;
|
_editor = editor;
|
||||||
_metaFileManager = metaFileManager;
|
_metaFileManager = metaFileManager;
|
||||||
_stainService = stainService;
|
_stainService = stainService;
|
||||||
_activeCollections = activeCollections;
|
_activeCollections = activeCollections;
|
||||||
_modMergeTab = modMergeTab;
|
_modMergeTab = modMergeTab;
|
||||||
_communicator = communicator;
|
_communicator = communicator;
|
||||||
_dragDropManager = dragDropManager;
|
_dragDropManager = dragDropManager;
|
||||||
_textures = textures;
|
_textures = textures;
|
||||||
_models = models;
|
_models = models;
|
||||||
_fileDialog = fileDialog;
|
_fileDialog = fileDialog;
|
||||||
_gameEvents = gameEvents;
|
_objects = objects;
|
||||||
_objects = objects;
|
_framework = framework;
|
||||||
_framework = framework;
|
_characterBaseDestructor = characterBaseDestructor;
|
||||||
_materialTab = new FileEditor<MtrlTab>(this, gameData, config, _editor.Compactor, _fileDialog, "Materials", ".mtrl",
|
_materialTab = new FileEditor<MtrlTab>(this, gameData, config, _editor.Compactor, _fileDialog, "Materials", ".mtrl",
|
||||||
() => PopulateIsOnPlayer(_editor.Files.Mtrl, ResourceType.Mtrl), DrawMaterialPanel, () => _mod?.ModPath.FullName ?? string.Empty,
|
() => PopulateIsOnPlayer(_editor.Files.Mtrl, ResourceType.Mtrl), DrawMaterialPanel, () => _mod?.ModPath.FullName ?? string.Empty,
|
||||||
(bytes, path, writable) => new MtrlTab(this, new MtrlFile(bytes), path, writable));
|
(bytes, path, writable) => new MtrlTab(this, new MtrlFile(bytes), path, writable));
|
||||||
|
|
@ -600,12 +600,12 @@ public partial class ModEditWindow : Window, IDisposable
|
||||||
_resourceTreeFactory = resourceTreeFactory;
|
_resourceTreeFactory = resourceTreeFactory;
|
||||||
_quickImportViewer =
|
_quickImportViewer =
|
||||||
new ResourceTreeViewer(_config, resourceTreeFactory, changedItemDrawer, 2, OnQuickImportRefresh, DrawQuickImportActions);
|
new ResourceTreeViewer(_config, resourceTreeFactory, changedItemDrawer, 2, OnQuickImportRefresh, DrawQuickImportActions);
|
||||||
_communicator.ModPathChanged.Subscribe(OnModPathChanged, ModPathChanged.Priority.ModEditWindow);
|
_communicator.ModPathChanged.Subscribe(OnModPathChange, ModPathChanged.Priority.ModEditWindow);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void Dispose()
|
public void Dispose()
|
||||||
{
|
{
|
||||||
_communicator.ModPathChanged.Unsubscribe(OnModPathChanged);
|
_communicator.ModPathChanged.Unsubscribe(OnModPathChange);
|
||||||
_editor?.Dispose();
|
_editor?.Dispose();
|
||||||
_materialTab.Dispose();
|
_materialTab.Dispose();
|
||||||
_modelTab.Dispose();
|
_modelTab.Dispose();
|
||||||
|
|
@ -615,7 +615,7 @@ public partial class ModEditWindow : Window, IDisposable
|
||||||
_center.Dispose();
|
_center.Dispose();
|
||||||
}
|
}
|
||||||
|
|
||||||
private void OnModPathChanged(ModPathChangeType type, Mod mod, DirectoryInfo? _1, DirectoryInfo? _2)
|
private void OnModPathChange(ModPathChangeType type, Mod mod, DirectoryInfo? _1, DirectoryInfo? _2)
|
||||||
{
|
{
|
||||||
if (type is ModPathChangeType.Reloaded or ModPathChangeType.Moved)
|
if (type is ModPathChangeType.Reloaded or ModPathChangeType.Moved)
|
||||||
ChangeMod(mod);
|
ChangeMod(mod);
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,10 @@
|
||||||
global using PerformanceTracker = OtterGui.Classes.PerformanceTracker<Penumbra.Util.PerformanceType>;
|
using Dalamud.Plugin.Services;
|
||||||
|
using OtterGui.Services;
|
||||||
|
|
||||||
namespace Penumbra.Util;
|
namespace Penumbra.Util;
|
||||||
|
|
||||||
|
public sealed class PerformanceTracker(IFramework framework) : OtterGui.Classes.PerformanceTracker<PerformanceType>(framework), IService;
|
||||||
|
|
||||||
public enum PerformanceType
|
public enum PerformanceType
|
||||||
{
|
{
|
||||||
UiMainWindow,
|
UiMainWindow,
|
||||||
|
|
|
||||||
|
|
@ -11,6 +11,18 @@
|
||||||
"Unosquare.Swan.Lite": "3.0.0"
|
"Unosquare.Swan.Lite": "3.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"Microsoft.CodeAnalysis.Common": {
|
||||||
|
"type": "Direct",
|
||||||
|
"requested": "[4.8.0, )",
|
||||||
|
"resolved": "4.8.0",
|
||||||
|
"contentHash": "/jR+e/9aT+BApoQJABlVCKnnggGQbvGh7BKq2/wI1LamxC+LbzhcLj4Vj7gXCofl1n4E521YfF9w0WcASGg/KA==",
|
||||||
|
"dependencies": {
|
||||||
|
"Microsoft.CodeAnalysis.Analyzers": "3.3.4",
|
||||||
|
"System.Collections.Immutable": "7.0.0",
|
||||||
|
"System.Reflection.Metadata": "7.0.0",
|
||||||
|
"System.Runtime.CompilerServices.Unsafe": "6.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"Microsoft.Extensions.DependencyInjection": {
|
"Microsoft.Extensions.DependencyInjection": {
|
||||||
"type": "Direct",
|
"type": "Direct",
|
||||||
"requested": "[7.0.0, )",
|
"requested": "[7.0.0, )",
|
||||||
|
|
@ -51,6 +63,11 @@
|
||||||
"System.Text.Encoding.CodePages": "5.0.0"
|
"System.Text.Encoding.CodePages": "5.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"Microsoft.CodeAnalysis.Analyzers": {
|
||||||
|
"type": "Transitive",
|
||||||
|
"resolved": "3.3.4",
|
||||||
|
"contentHash": "AxkxcPR+rheX0SmvpLVIGLhOUXAKG56a64kV9VQZ4y9gR9ZmPXnqZvHJnmwLSwzrEP6junUF11vuc+aqo5r68g=="
|
||||||
|
},
|
||||||
"Microsoft.Extensions.DependencyInjection.Abstractions": {
|
"Microsoft.Extensions.DependencyInjection.Abstractions": {
|
||||||
"type": "Transitive",
|
"type": "Transitive",
|
||||||
"resolved": "7.0.0",
|
"resolved": "7.0.0",
|
||||||
|
|
@ -69,10 +86,23 @@
|
||||||
"SharpGLTF.Core": "1.0.0-alpha0030"
|
"SharpGLTF.Core": "1.0.0-alpha0030"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"System.Collections.Immutable": {
|
||||||
|
"type": "Transitive",
|
||||||
|
"resolved": "7.0.0",
|
||||||
|
"contentHash": "dQPcs0U1IKnBdRDBkrCTi1FoajSTBzLcVTpjO4MBCMC7f4pDOIPzgBoX8JjG7X6uZRJ8EBxsi8+DR1JuwjnzOQ=="
|
||||||
|
},
|
||||||
|
"System.Reflection.Metadata": {
|
||||||
|
"type": "Transitive",
|
||||||
|
"resolved": "7.0.0",
|
||||||
|
"contentHash": "MclTG61lsD9sYdpNz9xsKBzjsmsfCtcMZYXz/IUr2zlhaTaABonlr1ESeompTgM+Xk+IwtGYU7/voh3YWB/fWw==",
|
||||||
|
"dependencies": {
|
||||||
|
"System.Collections.Immutable": "7.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"System.Runtime.CompilerServices.Unsafe": {
|
"System.Runtime.CompilerServices.Unsafe": {
|
||||||
"type": "Transitive",
|
"type": "Transitive",
|
||||||
"resolved": "5.0.0",
|
"resolved": "6.0.0",
|
||||||
"contentHash": "ZD9TMpsmYJLrxbbmdvhwt9YEgG5WntEnZ/d1eH8JBX9LBp+Ju8BSBhUGbZMNVHHomWo2KVImJhTDl2hIgw/6MA=="
|
"contentHash": "/iUeP3tq1S0XdNNoMz5C9twLSrM/TH+qElHkXWaPvuNOt+99G75NrV0OS2EqHx5wMN7popYjpc8oTjC1y16DLg=="
|
||||||
},
|
},
|
||||||
"System.Text.Encoding.CodePages": {
|
"System.Text.Encoding.CodePages": {
|
||||||
"type": "Transitive",
|
"type": "Transitive",
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue