mirror of
https://github.com/xivdev/Penumbra.git
synced 2025-12-12 18:27:24 +01:00
Merge branch 'master' into mdl-import
This commit is contained in:
commit
1a88cefd52
93 changed files with 2132 additions and 1744 deletions
2
OtterGui
2
OtterGui
|
|
@ -1 +1 @@
|
|||
Subproject commit 197d23eee167c232000f22ef40a7a2bded913b6c
|
||||
Subproject commit 2c603cea9b1d4dd500e30972b64bd2f25012dc4c
|
||||
|
|
@ -1 +1 @@
|
|||
Subproject commit 821194d0650a2dac98b7cbba9ff4a79e32b32d4d
|
||||
Subproject commit a2db1b309c3121e84c75e639e70575af7d936c3e
|
||||
|
|
@ -259,7 +259,7 @@ public class PenumbraApi : IDisposable, IPenumbraApi
|
|||
}
|
||||
else if (tab != TabType.None)
|
||||
{
|
||||
_communicator.SelectTab.Invoke(tab);
|
||||
_communicator.SelectTab.Invoke(tab, null);
|
||||
}
|
||||
|
||||
return PenumbraApiEc.Success;
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@ namespace Penumbra.Communication;
|
|||
/// <item>Parameter is the clicked object data if any. </item>
|
||||
/// </list>
|
||||
/// </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
|
||||
{
|
||||
|
|
@ -21,11 +21,4 @@ public sealed class ChangedItemClick : EventWrapper<Action<MouseButton, object?>
|
|||
/// <seealso cref="Penumbra.SetupApi"/>
|
||||
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>
|
||||
/// </list>
|
||||
/// </summary>
|
||||
public sealed class ChangedItemHover : EventWrapper<Action<object?>, ChangedItemHover.Priority>
|
||||
public sealed class ChangedItemHover() : EventWrapper<object?, ChangedItemHover.Priority>(nameof(ChangedItemHover))
|
||||
{
|
||||
public enum Priority
|
||||
{
|
||||
|
|
@ -19,13 +19,6 @@ public sealed class ChangedItemHover : EventWrapper<Action<object?>, ChangedItem
|
|||
Link = 1,
|
||||
}
|
||||
|
||||
public ChangedItemHover()
|
||||
: base(nameof(ChangedItemHover))
|
||||
{ }
|
||||
|
||||
public void Invoke(object? data)
|
||||
=> Invoke(this, data);
|
||||
|
||||
public bool HasTooltip
|
||||
=> HasSubscribers;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -12,7 +12,8 @@ namespace Penumbra.Communication;
|
|||
/// <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>
|
||||
/// </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
|
||||
{
|
||||
|
|
@ -46,11 +47,4 @@ public sealed class CollectionChange : EventWrapper<Action<CollectionType, ModCo
|
|||
/// <seealso cref="UI.ModsTab.ModFileSystemSelector.OnCollectionChange"/>
|
||||
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>
|
||||
/// </list>
|
||||
/// </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
|
||||
{
|
||||
|
|
@ -23,11 +24,4 @@ public sealed class CollectionInheritanceChanged : EventWrapper<Action<ModCollec
|
|||
/// <seealso cref="UI.ModsTab.ModFileSystemSelector.OnInheritanceChange"/>
|
||||
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 created draw object. </item>
|
||||
/// </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
|
||||
{
|
||||
/// <seealso cref="PenumbraApi.CreatedCharacterBase"/>
|
||||
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 equip data array. </item>
|
||||
/// </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
|
||||
{
|
||||
/// <seealso cref="PenumbraApi.CreatingCharacterBase"/>
|
||||
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>
|
||||
/// </list>
|
||||
/// </summary>
|
||||
public sealed class EnabledChanged : EventWrapper<Action<bool>, EnabledChanged.Priority>
|
||||
public sealed class EnabledChanged() : EventWrapper<bool, EnabledChanged.Priority>(nameof(EnabledChanged))
|
||||
{
|
||||
public enum Priority
|
||||
{
|
||||
|
|
@ -19,11 +19,4 @@ public sealed class EnabledChanged : EventWrapper<Action<bool>, EnabledChanged.P
|
|||
/// <seealso cref="Api.DalamudSubstitutionProvider.OnEnabledChange"/>
|
||||
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 old name of the mod in case of a name change, and null otherwise. </item>
|
||||
/// </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
|
||||
{
|
||||
|
|
@ -27,11 +27,4 @@ public sealed class ModDataChanged : EventWrapper<Action<ModDataChangeType, Mod,
|
|||
/// <seealso cref="UI.ModsTab.ModPanelHeader.OnModDataChange"/>
|
||||
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>
|
||||
/// </list>
|
||||
/// </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
|
||||
{
|
||||
|
|
@ -20,11 +20,4 @@ public sealed class ModDirectoryChanged : EventWrapper<Action<string, bool>, Mod
|
|||
/// <seealso cref="UI.FileDialogService.OnModDirectoryChange"/>
|
||||
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 Penumbra.Mods.Manager;
|
||||
|
||||
namespace Penumbra.Communication;
|
||||
|
||||
/// <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
|
||||
{
|
||||
|
|
@ -23,11 +22,4 @@ public sealed class ModDiscoveryFinished : EventWrapper<Action, ModDiscoveryFini
|
|||
/// <seealso cref="Mods.Manager.ModFileSystem.Reload"/>
|
||||
ModFileSystem = 0,
|
||||
}
|
||||
|
||||
public ModDiscoveryFinished()
|
||||
: base(nameof(ModDiscoveryFinished))
|
||||
{ }
|
||||
|
||||
public void Invoke()
|
||||
=> Invoke(this);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@ using OtterGui.Classes;
|
|||
namespace Penumbra.Communication;
|
||||
|
||||
/// <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
|
||||
{
|
||||
|
|
@ -16,11 +16,4 @@ public sealed class ModDiscoveryStarted : EventWrapper<Action, ModDiscoveryStart
|
|||
/// <seealso cref="UI.ModsTab.ModFileSystemSelector.StoreCurrentSelection"/>
|
||||
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 group an option was moved to. </item>
|
||||
/// </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
|
||||
{
|
||||
|
|
@ -29,11 +30,4 @@ public sealed class ModOptionChanged : EventWrapper<Action<ModOptionChangeType,
|
|||
/// <seealso cref="Collections.Manager.CollectionStorage.OnModOptionChange"/>
|
||||
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,10 +14,14 @@ namespace Penumbra.Communication;
|
|||
/// <item>Parameter is the new directory on addition, move or reload and null on deletion. </item>
|
||||
/// </list>
|
||||
/// </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
|
||||
{
|
||||
/// <seealso cref="EphemeralConfig.OnModPathChanged"/>
|
||||
EphemeralConfig = -500,
|
||||
|
||||
/// <seealso cref="Collections.Cache.CollectionCacheManager.OnModChangeAddition"/>
|
||||
CollectionCacheManagerAddition = -100,
|
||||
|
||||
|
|
@ -48,11 +52,4 @@ public sealed class ModPathChanged : EventWrapper<Action<ModPathChangeType, Mod,
|
|||
/// <seealso cref="Collections.Cache.CollectionCacheManager.OnModChangeRemoval"/>
|
||||
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>
|
||||
/// </list>
|
||||
/// </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
|
||||
{
|
||||
|
|
@ -33,11 +34,4 @@ public sealed class ModSettingChanged : EventWrapper<Action<ModCollection, ModSe
|
|||
/// <seealso cref="UI.ModsTab.ModFileSystemSelector.OnSettingChange"/>
|
||||
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 associated game object. </item>
|
||||
/// </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
|
||||
{
|
||||
/// <seealso cref="Interop.Services.SkinFixer.OnMtrlShpkLoaded"/>
|
||||
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>
|
||||
/// </list>
|
||||
/// </summary>
|
||||
public sealed class PostSettingsPanelDraw : EventWrapper<Action<string>, PostSettingsPanelDraw.Priority>
|
||||
public sealed class PostSettingsPanelDraw() : EventWrapper<string, PostSettingsPanelDraw.Priority>(nameof(PostSettingsPanelDraw))
|
||||
{
|
||||
public enum Priority
|
||||
{
|
||||
/// <seealso cref="Api.PenumbraApi.PostSettingsPanelDraw"/>
|
||||
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>
|
||||
/// </list>
|
||||
/// </summary>
|
||||
public sealed class PreSettingsPanelDraw : EventWrapper<Action<string>, PreSettingsPanelDraw.Priority>
|
||||
public sealed class PreSettingsPanelDraw() : EventWrapper<string, PreSettingsPanelDraw.Priority>(nameof(PreSettingsPanelDraw))
|
||||
{
|
||||
public enum Priority
|
||||
{
|
||||
/// <seealso cref="Api.PenumbraApi.PreSettingsPanelDraw"/>
|
||||
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 mod responsible for the new redirection if any. </item>
|
||||
/// </list> </summary>
|
||||
public sealed class ResolvedFileChanged : EventWrapper<Action<ModCollection, ResolvedFileChanged.Type, Utf8GamePath, FullPath, FullPath, IMod?>,
|
||||
ResolvedFileChanged.Priority>
|
||||
public sealed class ResolvedFileChanged()
|
||||
: EventWrapper<ModCollection, ResolvedFileChanged.Type, Utf8GamePath, FullPath, FullPath, IMod?, ResolvedFileChanged.Priority>(
|
||||
nameof(ResolvedFileChanged))
|
||||
{
|
||||
public enum Type
|
||||
{
|
||||
|
|
@ -29,14 +30,7 @@ public sealed class ResolvedFileChanged : EventWrapper<Action<ModCollection, Res
|
|||
|
||||
public enum Priority
|
||||
{
|
||||
/// <seealso cref="Api.DalamudSubstitutionProvider.OnResolvedFileChanged"/>
|
||||
/// <seealso cref="Api.DalamudSubstitutionProvider.OnResolvedFileChange"/>
|
||||
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>
|
||||
/// </list>
|
||||
/// </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
|
||||
{
|
||||
/// <seealso cref="UI.Tabs.ConfigTabBar.OnSelectTab"/>
|
||||
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 deleted.</item>
|
||||
/// </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
|
||||
{
|
||||
|
|
@ -20,11 +21,4 @@ public sealed class TemporaryGlobalModChange : EventWrapper<Action<TemporaryMod,
|
|||
/// <seealso cref="Collections.Manager.TempCollectionManager.OnGlobalModChange"/>
|
||||
TempCollectionManager = 0,
|
||||
}
|
||||
|
||||
public TemporaryGlobalModChange()
|
||||
: base(nameof(TemporaryGlobalModChange))
|
||||
{ }
|
||||
|
||||
public void Invoke(TemporaryMod temporaryMod, bool newlyCreated, bool deleted)
|
||||
=> Invoke(this, temporaryMod, newlyCreated, deleted);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,7 +2,10 @@ using Dalamud.Interface.Internal.Notifications;
|
|||
using Newtonsoft.Json;
|
||||
using OtterGui.Classes;
|
||||
using Penumbra.Api.Enums;
|
||||
using Penumbra.Communication;
|
||||
using Penumbra.Enums;
|
||||
using Penumbra.Mods;
|
||||
using Penumbra.Mods.Manager;
|
||||
using Penumbra.Services;
|
||||
using Penumbra.UI;
|
||||
using Penumbra.UI.ResourceWatcher;
|
||||
|
|
@ -11,11 +14,14 @@ using ErrorEventArgs = Newtonsoft.Json.Serialization.ErrorEventArgs;
|
|||
|
||||
namespace Penumbra;
|
||||
|
||||
public class EphemeralConfig : ISavable
|
||||
public class EphemeralConfig : ISavable, IDisposable
|
||||
{
|
||||
[JsonIgnore]
|
||||
private readonly SaveService _saveService;
|
||||
|
||||
[JsonIgnore]
|
||||
private readonly ModPathChanged _modPathChanged;
|
||||
|
||||
public int Version { get; set; } = Configuration.Constants.CurrentVersion;
|
||||
public int LastSeenVersion { get; set; } = PenumbraChangelog.LastChangelogVersion;
|
||||
public bool DebugSeparateWindow { get; set; } = false;
|
||||
|
|
@ -31,17 +37,24 @@ public class EphemeralConfig : ISavable
|
|||
public TabType SelectedTab { get; set; } = TabType.Settings;
|
||||
public ChangedItemDrawer.ChangedItemIcon ChangedItemFilter { get; set; } = ChangedItemDrawer.DefaultFlags;
|
||||
public bool FixMainWindow { get; set; } = false;
|
||||
public string LastModPath { get; set; } = string.Empty;
|
||||
public bool AdvancedEditingOpen { get; set; } = false;
|
||||
|
||||
/// <summary>
|
||||
/// Load the current configuration.
|
||||
/// Includes adding new colors and migrating from old versions.
|
||||
/// </summary>
|
||||
public EphemeralConfig(SaveService saveService)
|
||||
public EphemeralConfig(SaveService saveService, ModPathChanged modPathChanged)
|
||||
{
|
||||
_saveService = saveService;
|
||||
_saveService = saveService;
|
||||
_modPathChanged = modPathChanged;
|
||||
Load();
|
||||
_modPathChanged.Subscribe(OnModPathChanged, ModPathChanged.Priority.EphemeralConfig);
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
=> _modPathChanged.Unsubscribe(OnModPathChanged);
|
||||
|
||||
private void Load()
|
||||
{
|
||||
static void HandleDeserializationError(object? sender, ErrorEventArgs errorArgs)
|
||||
|
|
@ -80,8 +93,19 @@ public class EphemeralConfig : ISavable
|
|||
|
||||
public void Save(StreamWriter writer)
|
||||
{
|
||||
using var jWriter = new JsonTextWriter(writer) { Formatting = Formatting.Indented };
|
||||
using var jWriter = new JsonTextWriter(writer);
|
||||
jWriter.Formatting = Formatting.Indented;
|
||||
var serializer = new JsonSerializer { Formatting = Formatting.Indented };
|
||||
serializer.Serialize(jWriter, this);
|
||||
}
|
||||
|
||||
/// <summary> Overwrite the last saved mod path if it changes. </summary>
|
||||
private void OnModPathChanged(ModPathChangeType type, Mod mod, DirectoryInfo? old, DirectoryInfo? _)
|
||||
{
|
||||
if (type is not ModPathChangeType.Moved || !string.Equals(old?.Name, LastModPath, StringComparison.OrdinalIgnoreCase))
|
||||
return;
|
||||
|
||||
LastModPath = mod.Identifier;
|
||||
Save();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -13,24 +13,17 @@ namespace Penumbra.Import.Models.Export;
|
|||
|
||||
public class MeshExporter
|
||||
{
|
||||
public class Mesh
|
||||
public class Mesh(IEnumerable<IMeshBuilder<MaterialBuilder>> meshes, NodeBuilder[]? joints)
|
||||
{
|
||||
private IMeshBuilder<MaterialBuilder>[] _meshes;
|
||||
private NodeBuilder[]? _joints;
|
||||
|
||||
public Mesh(IMeshBuilder<MaterialBuilder>[] meshes, NodeBuilder[]? joints)
|
||||
{
|
||||
_meshes = meshes;
|
||||
_joints = joints;
|
||||
}
|
||||
|
||||
public void AddToScene(SceneBuilder scene)
|
||||
{
|
||||
foreach (var mesh in _meshes)
|
||||
if (_joints == null)
|
||||
foreach (var mesh in meshes)
|
||||
{
|
||||
if (joints == null)
|
||||
scene.AddRigidMesh(mesh, Matrix4x4.Identity);
|
||||
else
|
||||
scene.AddSkinnedMesh(mesh, Matrix4x4.Identity, _joints);
|
||||
scene.AddSkinnedMesh(mesh, Matrix4x4.Identity, joints);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -43,9 +36,11 @@ public class MeshExporter
|
|||
private const byte MaximumMeshBufferStreams = 3;
|
||||
|
||||
private readonly MdlFile _mdl;
|
||||
private readonly byte _lod;
|
||||
private readonly ushort _meshIndex;
|
||||
private MdlStructs.MeshStruct XivMesh => _mdl.Meshes[_meshIndex];
|
||||
private readonly byte _lod;
|
||||
private readonly ushort _meshIndex;
|
||||
|
||||
private MdlStructs.MeshStruct XivMesh
|
||||
=> _mdl.Meshes[_meshIndex];
|
||||
|
||||
private readonly Dictionary<ushort, int>? _boneIndexMap;
|
||||
|
||||
|
|
@ -53,10 +48,10 @@ public class MeshExporter
|
|||
private readonly Type _materialType;
|
||||
private readonly Type _skinningType;
|
||||
|
||||
private MeshExporter(MdlFile mdl, byte lod, ushort meshIndex, Dictionary<string, int>? boneNameMap)
|
||||
private MeshExporter(MdlFile mdl, byte lod, ushort meshIndex, IReadOnlyDictionary<string, int>? boneNameMap)
|
||||
{
|
||||
_mdl = mdl;
|
||||
_lod = lod;
|
||||
_mdl = mdl;
|
||||
_lod = lod;
|
||||
_meshIndex = meshIndex;
|
||||
|
||||
if (boneNameMap != null)
|
||||
|
|
@ -76,11 +71,12 @@ public class MeshExporter
|
|||
if (_skinningType != typeof(VertexEmpty) && _boneIndexMap == null)
|
||||
Penumbra.Log.Warning($"Mesh {meshIndex} has skinned vertex usages but no bone information was provided.");
|
||||
|
||||
Penumbra.Log.Debug($"Mesh {meshIndex} using vertex types geometry: {_geometryType.Name}, material: {_materialType.Name}, skinning: {_skinningType.Name}");
|
||||
Penumbra.Log.Debug(
|
||||
$"Mesh {meshIndex} using vertex types geometry: {_geometryType.Name}, material: {_materialType.Name}, skinning: {_skinningType.Name}");
|
||||
}
|
||||
|
||||
/// <summary> Build a mapping between indices in this mesh's bone table (if any), and the glTF joint indices provdied. </summary>
|
||||
private Dictionary<ushort, int>? BuildBoneIndexMap(Dictionary<string, int> boneNameMap)
|
||||
/// <summary> Build a mapping between indices in this mesh's bone table (if any), and the glTF joint indices provided. </summary>
|
||||
private Dictionary<ushort, int>? BuildBoneIndexMap(IReadOnlyDictionary<string, int> boneNameMap)
|
||||
{
|
||||
// A BoneTableIndex of 255 means that this mesh is not skinned.
|
||||
if (XivMesh.BoneTableIndex == 255)
|
||||
|
|
@ -105,11 +101,10 @@ public class MeshExporter
|
|||
/// <summary> Build glTF meshes for this XIV mesh. </summary>
|
||||
private IMeshBuilder<MaterialBuilder>[] BuildMeshes()
|
||||
{
|
||||
var indices = BuildIndices();
|
||||
var indices = BuildIndices();
|
||||
var vertices = BuildVertices();
|
||||
|
||||
// NOTE: Index indices are specified relative to the LOD's 0, but we're reading chunks for each mesh, so we're specifying the index base relative to the mesh's base.
|
||||
|
||||
if (XivMesh.SubMeshCount == 0)
|
||||
return [BuildMesh($"mesh {_meshIndex}", indices, vertices, 0, (int)XivMesh.IndexCount)];
|
||||
|
||||
|
|
@ -117,7 +112,8 @@ public class MeshExporter
|
|||
.Skip(XivMesh.SubMeshIndex)
|
||||
.Take(XivMesh.SubMeshCount)
|
||||
.WithIndex()
|
||||
.Select(submesh => BuildMesh($"mesh {_meshIndex}.{submesh.Index}", indices, vertices, (int)(submesh.Value.IndexOffset - XivMesh.StartIndex), (int)submesh.Value.IndexCount))
|
||||
.Select(subMesh => BuildMesh($"mesh {_meshIndex}.{subMesh.Index}", indices, vertices,
|
||||
(int)(subMesh.Value.IndexOffset - XivMesh.StartIndex), (int)subMesh.Value.IndexCount))
|
||||
.ToArray();
|
||||
}
|
||||
|
||||
|
|
@ -161,7 +157,7 @@ public class MeshExporter
|
|||
}
|
||||
|
||||
var primitiveVertices = meshBuilder.Primitives.First().Vertices;
|
||||
var shapeNames = new List<string>();
|
||||
var shapeNames = new List<string>();
|
||||
|
||||
foreach (var shape in _mdl.Shapes)
|
||||
{
|
||||
|
|
@ -177,24 +173,28 @@ public class MeshExporter
|
|||
)
|
||||
.Where(shapeValue =>
|
||||
shapeValue.BaseIndicesIndex >= indexBase
|
||||
&& shapeValue.BaseIndicesIndex < indexBase + indexCount
|
||||
&& shapeValue.BaseIndicesIndex < indexBase + indexCount
|
||||
)
|
||||
.ToList();
|
||||
|
||||
if (shapeValues.Count == 0) continue;
|
||||
if (shapeValues.Count == 0)
|
||||
continue;
|
||||
|
||||
var morphBuilder = meshBuilder.UseMorphTarget(shapeNames.Count);
|
||||
shapeNames.Add(shape.ShapeName);
|
||||
|
||||
foreach (var shapeValue in shapeValues)
|
||||
{
|
||||
morphBuilder.SetVertex(
|
||||
primitiveVertices[gltfIndices[shapeValue.BaseIndicesIndex - indexBase]].GetGeometry(),
|
||||
vertices[shapeValue.ReplacingVertexIndex].GetGeometry()
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
meshBuilder.Extras = JsonContent.CreateFrom(new Dictionary<string, object>() {
|
||||
{"targetNames", shapeNames}
|
||||
meshBuilder.Extras = JsonContent.CreateFrom(new Dictionary<string, object>()
|
||||
{
|
||||
{ "targetNames", shapeNames },
|
||||
});
|
||||
|
||||
return meshBuilder;
|
||||
|
|
@ -249,23 +249,25 @@ public class MeshExporter
|
|||
}
|
||||
|
||||
/// <summary> Read a vertex attribute of the specified type from a vertex buffer stream. </summary>
|
||||
private object ReadVertexAttribute(MdlFile.VertexType type, BinaryReader reader)
|
||||
private static object ReadVertexAttribute(MdlFile.VertexType type, BinaryReader reader)
|
||||
{
|
||||
return type switch
|
||||
{
|
||||
MdlFile.VertexType.Single3 => new Vector3(reader.ReadSingle(), reader.ReadSingle(), reader.ReadSingle()),
|
||||
MdlFile.VertexType.Single4 => new Vector4(reader.ReadSingle(), reader.ReadSingle(), reader.ReadSingle(), reader.ReadSingle()),
|
||||
MdlFile.VertexType.UInt => reader.ReadBytes(4),
|
||||
MdlFile.VertexType.ByteFloat4 => new Vector4(reader.ReadByte() / 255f, reader.ReadByte() / 255f, reader.ReadByte() / 255f, reader.ReadByte() / 255f),
|
||||
MdlFile.VertexType.UInt => reader.ReadBytes(4),
|
||||
MdlFile.VertexType.ByteFloat4 => new Vector4(reader.ReadByte() / 255f, reader.ReadByte() / 255f, reader.ReadByte() / 255f,
|
||||
reader.ReadByte() / 255f),
|
||||
MdlFile.VertexType.Half2 => new Vector2((float)reader.ReadHalf(), (float)reader.ReadHalf()),
|
||||
MdlFile.VertexType.Half4 => new Vector4((float)reader.ReadHalf(), (float)reader.ReadHalf(), (float)reader.ReadHalf(), (float)reader.ReadHalf()),
|
||||
MdlFile.VertexType.Half4 => new Vector4((float)reader.ReadHalf(), (float)reader.ReadHalf(), (float)reader.ReadHalf(),
|
||||
(float)reader.ReadHalf()),
|
||||
|
||||
_ => throw new ArgumentOutOfRangeException()
|
||||
_ => throw new ArgumentOutOfRangeException(),
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary> Get the vertex geometry type for this mesh's vertex usages. </summary>
|
||||
private Type GetGeometryType(IReadOnlyDictionary<MdlFile.VertexUsage, MdlFile.VertexType> usages)
|
||||
private static Type GetGeometryType(IReadOnlyDictionary<MdlFile.VertexUsage, MdlFile.VertexType> usages)
|
||||
{
|
||||
if (!usages.ContainsKey(MdlFile.VertexUsage.Position))
|
||||
throw new Exception("Mesh does not contain position vertex elements.");
|
||||
|
|
@ -304,16 +306,16 @@ public class MeshExporter
|
|||
}
|
||||
|
||||
/// <summary> Get the vertex material type for this mesh's vertex usages. </summary>
|
||||
private Type GetMaterialType(IReadOnlyDictionary<MdlFile.VertexUsage, MdlFile.VertexType> usages)
|
||||
private static Type GetMaterialType(IReadOnlyDictionary<MdlFile.VertexUsage, MdlFile.VertexType> usages)
|
||||
{
|
||||
var uvCount = 0;
|
||||
if (usages.TryGetValue(MdlFile.VertexUsage.UV, out var type))
|
||||
uvCount = type switch
|
||||
{
|
||||
MdlFile.VertexType.Half2 => 1,
|
||||
MdlFile.VertexType.Half4 => 2,
|
||||
MdlFile.VertexType.Half2 => 1,
|
||||
MdlFile.VertexType.Half4 => 2,
|
||||
MdlFile.VertexType.Single4 => 2,
|
||||
_ => throw new Exception($"Unexpected UV vertex type {type}.")
|
||||
_ => throw new Exception($"Unexpected UV vertex type {type}."),
|
||||
};
|
||||
|
||||
var materialUsages = (
|
||||
|
|
@ -323,11 +325,11 @@ public class MeshExporter
|
|||
|
||||
return materialUsages switch
|
||||
{
|
||||
(2, true) => typeof(VertexColor1Texture2),
|
||||
(2, true) => typeof(VertexColor1Texture2),
|
||||
(2, false) => typeof(VertexTexture2),
|
||||
(1, true) => typeof(VertexColor1Texture1),
|
||||
(1, true) => typeof(VertexColor1Texture1),
|
||||
(1, false) => typeof(VertexTexture1),
|
||||
(0, true) => typeof(VertexColor1),
|
||||
(0, true) => typeof(VertexColor1),
|
||||
(0, false) => typeof(VertexEmpty),
|
||||
|
||||
_ => throw new Exception("Unreachable."),
|
||||
|
|
@ -377,7 +379,7 @@ public class MeshExporter
|
|||
}
|
||||
|
||||
/// <summary> Get the vertex skinning type for this mesh's vertex usages. </summary>
|
||||
private Type GetSkinningType(IReadOnlyDictionary<MdlFile.VertexUsage, MdlFile.VertexType> usages)
|
||||
private static Type GetSkinningType(IReadOnlyDictionary<MdlFile.VertexUsage, MdlFile.VertexType> usages)
|
||||
{
|
||||
if (usages.ContainsKey(MdlFile.VertexUsage.BlendWeights) && usages.ContainsKey(MdlFile.VertexUsage.BlendIndices))
|
||||
return typeof(VertexJoints4);
|
||||
|
|
@ -400,7 +402,8 @@ public class MeshExporter
|
|||
var weights = ToVector4(attributes[MdlFile.VertexUsage.BlendWeights]);
|
||||
|
||||
var bindings = Enumerable.Range(0, 4)
|
||||
.Select(bindingIndex => {
|
||||
.Select(bindingIndex =>
|
||||
{
|
||||
// NOTE: I've not seen any files that throw this error that aren't completely broken.
|
||||
var xivBoneIndex = indices[bindingIndex];
|
||||
if (!_boneIndexMap.TryGetValue(xivBoneIndex, out var jointIndex))
|
||||
|
|
@ -417,44 +420,44 @@ public class MeshExporter
|
|||
|
||||
/// <summary> Clamps any tangent W value other than 1 to -1. </summary>
|
||||
/// <remarks> Some XIV models seemingly store -1 as 0, this patches over that. </remarks>
|
||||
private Vector4 FixTangentVector(Vector4 tangent)
|
||||
private static Vector4 FixTangentVector(Vector4 tangent)
|
||||
=> tangent with { W = tangent.W == 1 ? 1 : -1 };
|
||||
|
||||
/// <summary> Convert a vertex attribute value to a Vector2. Supported inputs are Vector2, Vector3, and Vector4. </summary>
|
||||
private Vector2 ToVector2(object data)
|
||||
private static Vector2 ToVector2(object data)
|
||||
=> data switch
|
||||
{
|
||||
Vector2 v2 => v2,
|
||||
Vector3 v3 => new Vector2(v3.X, v3.Y),
|
||||
Vector4 v4 => new Vector2(v4.X, v4.Y),
|
||||
_ => throw new ArgumentOutOfRangeException($"Invalid Vector2 input {data}")
|
||||
_ => throw new ArgumentOutOfRangeException($"Invalid Vector2 input {data}"),
|
||||
};
|
||||
|
||||
/// <summary> Convert a vertex attribute value to a Vector3. Supported inputs are Vector2, Vector3, and Vector4. </summary>
|
||||
private Vector3 ToVector3(object data)
|
||||
private static Vector3 ToVector3(object data)
|
||||
=> data switch
|
||||
{
|
||||
Vector2 v2 => new Vector3(v2.X, v2.Y, 0),
|
||||
Vector3 v3 => v3,
|
||||
Vector4 v4 => new Vector3(v4.X, v4.Y, v4.Z),
|
||||
_ => throw new ArgumentOutOfRangeException($"Invalid Vector3 input {data}")
|
||||
_ => throw new ArgumentOutOfRangeException($"Invalid Vector3 input {data}"),
|
||||
};
|
||||
|
||||
/// <summary> Convert a vertex attribute value to a Vector4. Supported inputs are Vector2, Vector3, and Vector4. </summary>
|
||||
private Vector4 ToVector4(object data)
|
||||
private static Vector4 ToVector4(object data)
|
||||
=> data switch
|
||||
{
|
||||
Vector2 v2 => new Vector4(v2.X, v2.Y, 0, 0),
|
||||
Vector2 v2 => new Vector4(v2.X, v2.Y, 0, 0),
|
||||
Vector3 v3 => new Vector4(v3.X, v3.Y, v3.Z, 1),
|
||||
Vector4 v4 => v4,
|
||||
_ => throw new ArgumentOutOfRangeException($"Invalid Vector3 input {data}")
|
||||
_ => throw new ArgumentOutOfRangeException($"Invalid Vector3 input {data}"),
|
||||
};
|
||||
|
||||
/// <summary> Convert a vertex attribute value to a byte array. </summary>
|
||||
private byte[] ToByteArray(object data)
|
||||
private static byte[] ToByteArray(object data)
|
||||
=> data switch
|
||||
{
|
||||
byte[] value => value,
|
||||
_ => throw new ArgumentOutOfRangeException($"Invalid byte[] input {data}")
|
||||
_ => throw new ArgumentOutOfRangeException($"Invalid byte[] input {data}"),
|
||||
};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -6,26 +6,17 @@ namespace Penumbra.Import.Models.Export;
|
|||
|
||||
public class ModelExporter
|
||||
{
|
||||
public class Model
|
||||
public class Model(List<MeshExporter.Mesh> meshes, GltfSkeleton? skeleton)
|
||||
{
|
||||
private List<MeshExporter.Mesh> _meshes;
|
||||
private GltfSkeleton? _skeleton;
|
||||
|
||||
public Model(List<MeshExporter.Mesh> meshes, GltfSkeleton? skeleton)
|
||||
{
|
||||
_meshes = meshes;
|
||||
_skeleton = skeleton;
|
||||
}
|
||||
|
||||
public void AddToScene(SceneBuilder scene)
|
||||
{
|
||||
// If there's a skeleton, the root node should be added before we add any potentially skinned meshes.
|
||||
var skeletonRoot = _skeleton?.Root;
|
||||
var skeletonRoot = skeleton?.Root;
|
||||
if (skeletonRoot != null)
|
||||
scene.AddNode(skeletonRoot);
|
||||
|
||||
// Add all the meshes to the scene.
|
||||
foreach (var mesh in _meshes)
|
||||
foreach (var mesh in meshes)
|
||||
mesh.AddToScene(scene);
|
||||
}
|
||||
}
|
||||
|
|
@ -64,10 +55,8 @@ public class ModelExporter
|
|||
NodeBuilder? root = null;
|
||||
var names = new Dictionary<string, int>();
|
||||
var joints = new List<NodeBuilder>();
|
||||
for (var boneIndex = 0; boneIndex < skeleton.Bones.Length; boneIndex++)
|
||||
foreach (var bone in skeleton.Bones)
|
||||
{
|
||||
var bone = skeleton.Bones[boneIndex];
|
||||
|
||||
if (names.ContainsKey(bone.Name)) continue;
|
||||
|
||||
var node = new NodeBuilder(bone.Name);
|
||||
|
|
@ -93,10 +82,10 @@ public class ModelExporter
|
|||
if (root == null)
|
||||
return null;
|
||||
|
||||
return new()
|
||||
return new GltfSkeleton
|
||||
{
|
||||
Root = root,
|
||||
Joints = joints.ToArray(),
|
||||
Joints = [.. joints],
|
||||
Names = names,
|
||||
};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,14 +3,9 @@ using SharpGLTF.Scenes;
|
|||
namespace Penumbra.Import.Models.Export;
|
||||
|
||||
/// <summary> Representation of a skeleton within XIV. </summary>
|
||||
public class XivSkeleton
|
||||
public class XivSkeleton(XivSkeleton.Bone[] bones)
|
||||
{
|
||||
public Bone[] Bones;
|
||||
|
||||
public XivSkeleton(Bone[] bones)
|
||||
{
|
||||
Bones = bones;
|
||||
}
|
||||
public Bone[] Bones = bones;
|
||||
|
||||
public struct Bone
|
||||
{
|
||||
|
|
|
|||
|
|
@ -16,17 +16,18 @@ public static unsafe class HavokConverter
|
|||
/// <param name="hkx"> A byte array representing the .hkx file. </param>
|
||||
public static string HkxToXml(byte[] hkx)
|
||||
{
|
||||
const hkSerializeUtil.SaveOptionBits options = hkSerializeUtil.SaveOptionBits.SerializeIgnoredMembers
|
||||
| hkSerializeUtil.SaveOptionBits.TextFormat
|
||||
| hkSerializeUtil.SaveOptionBits.WriteAttributes;
|
||||
|
||||
var tempHkx = CreateTempFile();
|
||||
File.WriteAllBytes(tempHkx, hkx);
|
||||
|
||||
var resource = Read(tempHkx);
|
||||
File.Delete(tempHkx);
|
||||
|
||||
if (resource == null) throw new Exception("Failed to read havok file.");
|
||||
|
||||
var options = hkSerializeUtil.SaveOptionBits.SerializeIgnoredMembers
|
||||
| hkSerializeUtil.SaveOptionBits.TextFormat
|
||||
| hkSerializeUtil.SaveOptionBits.WriteAttributes;
|
||||
if (resource == null)
|
||||
throw new Exception("Failed to read havok file.");
|
||||
|
||||
var file = Write(resource, options);
|
||||
file.Close();
|
||||
|
|
@ -41,16 +42,17 @@ public static unsafe class HavokConverter
|
|||
/// <param name="xml"> A string representing the .xml file. </param>
|
||||
public static byte[] XmlToHkx(string xml)
|
||||
{
|
||||
const hkSerializeUtil.SaveOptionBits options = hkSerializeUtil.SaveOptionBits.SerializeIgnoredMembers
|
||||
| hkSerializeUtil.SaveOptionBits.WriteAttributes;
|
||||
|
||||
var tempXml = CreateTempFile();
|
||||
File.WriteAllText(tempXml, xml);
|
||||
|
||||
var resource = Read(tempXml);
|
||||
File.Delete(tempXml);
|
||||
|
||||
if (resource == null) throw new Exception("Failed to read havok file.");
|
||||
|
||||
var options = hkSerializeUtil.SaveOptionBits.SerializeIgnoredMembers
|
||||
| hkSerializeUtil.SaveOptionBits.WriteAttributes;
|
||||
if (resource == null)
|
||||
throw new Exception("Failed to read havok file.");
|
||||
|
||||
var file = Write(resource, options);
|
||||
file.Close();
|
||||
|
|
@ -74,7 +76,7 @@ public static unsafe class HavokConverter
|
|||
var builtinTypeRegistry = hkBuiltinTypeRegistry.Instance();
|
||||
|
||||
var loadOptions = stackalloc hkSerializeUtil.LoadOptions[1];
|
||||
loadOptions->Flags = new() { Storage = (int)hkSerializeUtil.LoadOptionBits.Default };
|
||||
loadOptions->Flags = new hkFlags<hkSerializeUtil.LoadOptionBits, int> { Storage = (int)hkSerializeUtil.LoadOptionBits.Default };
|
||||
loadOptions->ClassNameRegistry = builtinTypeRegistry->GetClassNameRegistry();
|
||||
loadOptions->TypeInfoRegistry = builtinTypeRegistry->GetTypeInfoRegistry();
|
||||
|
||||
|
|
@ -92,37 +94,42 @@ public static unsafe class HavokConverter
|
|||
)
|
||||
{
|
||||
var tempFile = CreateTempFile();
|
||||
var path = Marshal.StringToHGlobalAnsi(tempFile);
|
||||
var oStream = new hkOstream();
|
||||
var path = Marshal.StringToHGlobalAnsi(tempFile);
|
||||
var oStream = new hkOstream();
|
||||
oStream.Ctor((byte*)path);
|
||||
|
||||
var result = stackalloc hkResult[1];
|
||||
|
||||
var saveOptions = new hkSerializeUtil.SaveOptions()
|
||||
{
|
||||
Flags = new() { Storage = (int)optionBits }
|
||||
Flags = new hkFlags<hkSerializeUtil.SaveOptionBits, int> { Storage = (int)optionBits },
|
||||
};
|
||||
|
||||
|
||||
var builtinTypeRegistry = hkBuiltinTypeRegistry.Instance();
|
||||
var classNameRegistry = builtinTypeRegistry->GetClassNameRegistry();
|
||||
var typeInfoRegistry = builtinTypeRegistry->GetTypeInfoRegistry();
|
||||
var classNameRegistry = builtinTypeRegistry->GetClassNameRegistry();
|
||||
var typeInfoRegistry = builtinTypeRegistry->GetTypeInfoRegistry();
|
||||
|
||||
try
|
||||
{
|
||||
var name = "hkRootLevelContainer";
|
||||
const string name = "hkRootLevelContainer";
|
||||
|
||||
var resourcePtr = (hkRootLevelContainer*)resource->GetContentsPointer(name, typeInfoRegistry);
|
||||
if (resourcePtr == null) throw new Exception("Failed to retrieve havok root level container resource.");
|
||||
if (resourcePtr == null)
|
||||
throw new Exception("Failed to retrieve havok root level container resource.");
|
||||
|
||||
var hkRootLevelContainerClass = classNameRegistry->GetClassByName(name);
|
||||
if (hkRootLevelContainerClass == null) throw new Exception("Failed to retrieve havok root level container type.");
|
||||
if (hkRootLevelContainerClass == null)
|
||||
throw new Exception("Failed to retrieve havok root level container type.");
|
||||
|
||||
hkSerializeUtil.Save(result, resourcePtr, hkRootLevelContainerClass, oStream.StreamWriter.ptr, saveOptions);
|
||||
}
|
||||
finally { oStream.Dtor(); }
|
||||
finally
|
||||
{
|
||||
oStream.Dtor();
|
||||
}
|
||||
|
||||
if (result->Result == hkResult.hkResultEnum.Failure) throw new Exception("Failed to serialize havok file.");
|
||||
if (result->Result == hkResult.hkResultEnum.Failure)
|
||||
throw new Exception("Failed to serialize havok file.");
|
||||
|
||||
return new FileStream(tempFile, FileMode.Open);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
using Dalamud.Plugin.Services;
|
||||
using OtterGui.Tasks;
|
||||
using Penumbra.Collections.Manager;
|
||||
using Penumbra.GameData.Data;
|
||||
using Penumbra.GameData.Enums;
|
||||
using Penumbra.GameData.Files;
|
||||
using Penumbra.Import.Models.Export;
|
||||
using Penumbra.Import.Models.Import;
|
||||
|
|
@ -9,21 +10,13 @@ using SharpGLTF.Schema2;
|
|||
|
||||
namespace Penumbra.Import.Models;
|
||||
|
||||
public sealed class ModelManager : SingleTaskQueue, IDisposable
|
||||
public sealed class ModelManager(IFramework framework, GamePathParser _parser) : SingleTaskQueue, IDisposable
|
||||
{
|
||||
private readonly IFramework _framework;
|
||||
private readonly IDataManager _gameData;
|
||||
private readonly ActiveCollectionData _activeCollectionData;
|
||||
private readonly IFramework _framework = framework;
|
||||
|
||||
private readonly ConcurrentDictionary<IAction, (Task, CancellationTokenSource)> _tasks = new();
|
||||
private bool _disposed = false;
|
||||
|
||||
public ModelManager(IFramework framework, IDataManager gameData, ActiveCollectionData activeCollectionData)
|
||||
{
|
||||
_framework = framework;
|
||||
_gameData = gameData;
|
||||
_activeCollectionData = activeCollectionData;
|
||||
}
|
||||
private bool _disposed;
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
|
|
@ -33,26 +26,6 @@ public sealed class ModelManager : SingleTaskQueue, IDisposable
|
|||
_tasks.Clear();
|
||||
}
|
||||
|
||||
private Task Enqueue(IAction action)
|
||||
{
|
||||
if (_disposed)
|
||||
return Task.FromException(new ObjectDisposedException(nameof(ModelManager)));
|
||||
|
||||
Task task;
|
||||
lock (_tasks)
|
||||
{
|
||||
task = _tasks.GetOrAdd(action, action =>
|
||||
{
|
||||
var token = new CancellationTokenSource();
|
||||
var task = Enqueue(action, token.Token);
|
||||
task.ContinueWith(_ => _tasks.TryRemove(action, out var unused), CancellationToken.None);
|
||||
return (task, token);
|
||||
}).Item1;
|
||||
}
|
||||
|
||||
return task;
|
||||
}
|
||||
|
||||
public Task ExportToGltf(MdlFile mdl, SklbFile? sklb, string outputPath)
|
||||
=> Enqueue(new ExportToGltfAction(this, mdl, sklb, outputPath));
|
||||
|
||||
|
|
@ -62,29 +35,64 @@ public sealed class ModelManager : SingleTaskQueue, IDisposable
|
|||
return Enqueue(action).ContinueWith(_ => action.Out!);
|
||||
}
|
||||
|
||||
private class ExportToGltfAction : IAction
|
||||
/// <summary> Try to find the .sklb path for a .mdl file. </summary>
|
||||
/// <param name="mdlPath"> .mdl file to look up the skeleton for. </param>
|
||||
public string? ResolveSklbForMdl(string mdlPath)
|
||||
{
|
||||
private readonly ModelManager _manager;
|
||||
var info = _parser.GetFileInfo(mdlPath);
|
||||
if (info.FileType is not FileType.Model)
|
||||
return null;
|
||||
|
||||
private readonly MdlFile _mdl;
|
||||
private readonly SklbFile? _sklb;
|
||||
private readonly string _outputPath;
|
||||
|
||||
public ExportToGltfAction(ModelManager manager, MdlFile mdl, SklbFile? sklb, string outputPath)
|
||||
return info.ObjectType switch
|
||||
{
|
||||
_manager = manager;
|
||||
_mdl = mdl;
|
||||
_sklb = sklb;
|
||||
_outputPath = outputPath;
|
||||
ObjectType.Equipment => GamePaths.Skeleton.Sklb.Path(info.GenderRace, "base", 1),
|
||||
ObjectType.Accessory => GamePaths.Skeleton.Sklb.Path(info.GenderRace, "base", 1),
|
||||
ObjectType.Character when info.BodySlot is BodySlot.Body or BodySlot.Tail => GamePaths.Skeleton.Sklb.Path(info.GenderRace, "base",
|
||||
1),
|
||||
ObjectType.Character => throw new Exception($"Currently unsupported human model type \"{info.BodySlot}\"."),
|
||||
ObjectType.DemiHuman => GamePaths.DemiHuman.Sklb.Path(info.PrimaryId),
|
||||
ObjectType.Monster => GamePaths.Monster.Sklb.Path(info.PrimaryId),
|
||||
ObjectType.Weapon => GamePaths.Weapon.Sklb.Path(info.PrimaryId),
|
||||
_ => null,
|
||||
};
|
||||
}
|
||||
|
||||
private Task Enqueue(IAction action)
|
||||
{
|
||||
if (_disposed)
|
||||
return Task.FromException(new ObjectDisposedException(nameof(ModelManager)));
|
||||
|
||||
Task task;
|
||||
lock (_tasks)
|
||||
{
|
||||
task = _tasks.GetOrAdd(action, a =>
|
||||
{
|
||||
var token = new CancellationTokenSource();
|
||||
var t = Enqueue(a, token.Token);
|
||||
t.ContinueWith(_ =>
|
||||
{
|
||||
lock (_tasks)
|
||||
{
|
||||
return _tasks.TryRemove(a, out var unused);
|
||||
}
|
||||
}, CancellationToken.None);
|
||||
return (t, token);
|
||||
}).Item1;
|
||||
}
|
||||
|
||||
return task;
|
||||
}
|
||||
|
||||
private class ExportToGltfAction(ModelManager manager, MdlFile mdl, SklbFile? sklb, string outputPath)
|
||||
: IAction
|
||||
{
|
||||
public void Execute(CancellationToken cancel)
|
||||
{
|
||||
Penumbra.Log.Debug("Reading skeleton.");
|
||||
var xivSkeleton = BuildSkeleton(cancel);
|
||||
|
||||
Penumbra.Log.Debug("Converting model.");
|
||||
var model = ModelExporter.Export(_mdl, xivSkeleton);
|
||||
var model = ModelExporter.Export(mdl, xivSkeleton);
|
||||
|
||||
Penumbra.Log.Debug("Building scene.");
|
||||
var scene = new SceneBuilder();
|
||||
|
|
@ -92,16 +100,16 @@ public sealed class ModelManager : SingleTaskQueue, IDisposable
|
|||
|
||||
Penumbra.Log.Debug("Saving.");
|
||||
var gltfModel = scene.ToGltf2();
|
||||
gltfModel.SaveGLTF(_outputPath);
|
||||
gltfModel.SaveGLTF(outputPath);
|
||||
}
|
||||
|
||||
/// <summary> Attempt to read out the pertinent information from a .sklb. </summary>
|
||||
private XivSkeleton? BuildSkeleton(CancellationToken cancel)
|
||||
{
|
||||
if (_sklb == null)
|
||||
if (sklb == null)
|
||||
return null;
|
||||
|
||||
var xmlTask = _manager._framework.RunOnFrameworkThread(() => HavokConverter.HkxToXml(_sklb.Skeleton));
|
||||
var xmlTask = manager._framework.RunOnFrameworkThread(() => HavokConverter.HkxToXml(sklb.Skeleton));
|
||||
xmlTask.Wait(cancel);
|
||||
var xml = xmlTask.Result;
|
||||
|
||||
|
|
|
|||
|
|
@ -15,16 +15,15 @@ public static class SkeletonConverter
|
|||
|
||||
var mainSkeletonId = GetMainSkeletonId(document);
|
||||
|
||||
var skeletonNode = document.SelectSingleNode($"/hktagfile/object[@type='hkaSkeleton'][@id='{mainSkeletonId}']");
|
||||
if (skeletonNode == null)
|
||||
throw new InvalidDataException($"Failed to find skeleton with id {mainSkeletonId}.");
|
||||
|
||||
var skeletonNode = document.SelectSingleNode($"/hktagfile/object[@type='hkaSkeleton'][@id='{mainSkeletonId}']")
|
||||
?? throw new InvalidDataException($"Failed to find skeleton with id {mainSkeletonId}.");
|
||||
var referencePose = ReadReferencePose(skeletonNode);
|
||||
var parentIndices = ReadParentIndices(skeletonNode);
|
||||
var boneNames = ReadBoneNames(skeletonNode);
|
||||
var boneNames = ReadBoneNames(skeletonNode);
|
||||
|
||||
if (boneNames.Length != parentIndices.Length || boneNames.Length != referencePose.Length)
|
||||
throw new InvalidDataException($"Mismatch in bone value array lengths: names({boneNames.Length}) parents({parentIndices.Length}) pose({referencePose.Length})");
|
||||
throw new InvalidDataException(
|
||||
$"Mismatch in bone value array lengths: names({boneNames.Length}) parents({parentIndices.Length}) pose({referencePose.Length})");
|
||||
|
||||
var bones = referencePose
|
||||
.Zip(parentIndices, boneNames)
|
||||
|
|
@ -33,9 +32,9 @@ public static class SkeletonConverter
|
|||
var (transform, parentIndex, name) = values;
|
||||
return new XivSkeleton.Bone()
|
||||
{
|
||||
Transform = transform,
|
||||
Transform = transform,
|
||||
ParentIndex = parentIndex,
|
||||
Name = name,
|
||||
Name = name,
|
||||
};
|
||||
})
|
||||
.ToArray();
|
||||
|
|
@ -63,14 +62,14 @@ public static class SkeletonConverter
|
|||
{
|
||||
return ReadArray(
|
||||
CheckExists(node.SelectSingleNode("array[@name='referencePose']")),
|
||||
node =>
|
||||
n =>
|
||||
{
|
||||
var raw = ReadVec12(node);
|
||||
var raw = ReadVec12(n);
|
||||
return new XivSkeleton.Transform()
|
||||
{
|
||||
Translation = new(raw[0], raw[1], raw[2]),
|
||||
Rotation = new(raw[4], raw[5], raw[6], raw[7]),
|
||||
Scale = new(raw[8], raw[9], raw[10]),
|
||||
Translation = new Vector3(raw[0], raw[1], raw[2]),
|
||||
Rotation = new Quaternion(raw[4], raw[5], raw[6], raw[7]),
|
||||
Scale = new Vector3(raw[8], raw[9], raw[10]),
|
||||
};
|
||||
}
|
||||
);
|
||||
|
|
@ -82,11 +81,11 @@ public static class SkeletonConverter
|
|||
{
|
||||
var array = node.ChildNodes
|
||||
.Cast<XmlNode>()
|
||||
.Where(node => node.NodeType != XmlNodeType.Comment)
|
||||
.Select(node =>
|
||||
.Where(n => n.NodeType != XmlNodeType.Comment)
|
||||
.Select(n =>
|
||||
{
|
||||
var text = node.InnerText.Trim()[1..];
|
||||
// TODO: surely there's a less shit way to do this i mean seriously
|
||||
var text = n.InnerText.Trim()[1..];
|
||||
// TODO: surely there's a less shit way to do this I mean seriously
|
||||
return BitConverter.ToSingle(BitConverter.GetBytes(int.Parse(text, NumberStyles.HexNumber)));
|
||||
})
|
||||
.ToArray();
|
||||
|
|
@ -100,24 +99,20 @@ public static class SkeletonConverter
|
|||
/// <summary> Read the bone parent relations for a skeleton. </summary>
|
||||
/// <param name="node"> XML node for the skeleton. </param>
|
||||
private static int[] ReadParentIndices(XmlNode node)
|
||||
{
|
||||
// todo: would be neat to genericise array between bare and children
|
||||
return CheckExists(node.SelectSingleNode("array[@name='parentIndices']"))
|
||||
=> CheckExists(node.SelectSingleNode("array[@name='parentIndices']"))
|
||||
.InnerText
|
||||
.Split(new char[] { ' ', '\n' }, StringSplitOptions.RemoveEmptyEntries)
|
||||
.Split((char[]) [' ', '\n'], StringSplitOptions.RemoveEmptyEntries)
|
||||
.Select(int.Parse)
|
||||
.ToArray();
|
||||
}
|
||||
|
||||
/// <summary> Read the names of bones in a skeleton. </summary>
|
||||
/// <param name="node"> XML node for the skeleton. </param>
|
||||
private static string[] ReadBoneNames(XmlNode node)
|
||||
{
|
||||
return ReadArray(
|
||||
=> ReadArray(
|
||||
CheckExists(node.SelectSingleNode("array[@name='bones']")),
|
||||
node => CheckExists(node.SelectSingleNode("string[@name='name']")).InnerText
|
||||
n => CheckExists(n.SelectSingleNode("string[@name='name']")).InnerText
|
||||
);
|
||||
}
|
||||
|
||||
/// <summary> Read an XML tagfile array, converting it via the provided conversion function. </summary>
|
||||
/// <param name="node"> Tagfile XML array node. </param>
|
||||
|
|
@ -125,10 +120,9 @@ public static class SkeletonConverter
|
|||
private static T[] ReadArray<T>(XmlNode node, Func<XmlNode, T> convert)
|
||||
{
|
||||
var element = (XmlElement)node;
|
||||
var size = int.Parse(element.GetAttribute("size"));
|
||||
var array = new T[size];
|
||||
|
||||
var size = int.Parse(element.GetAttribute("size"));
|
||||
|
||||
var array = new T[size];
|
||||
foreach (var (childNode, index) in element.ChildNodes.Cast<XmlElement>().WithIndex())
|
||||
array[index] = convert(childNode);
|
||||
|
||||
|
|
|
|||
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 FFXIVClientStructs.FFXIV.Client.Graphics.Scene;
|
||||
using OtterGui.Services;
|
||||
using Penumbra.Collections;
|
||||
using Penumbra.Collections.Manager;
|
||||
using Penumbra.GameData.Actors;
|
||||
using Penumbra.GameData.DataContainers;
|
||||
using Penumbra.GameData.Enums;
|
||||
using Penumbra.Services;
|
||||
using Penumbra.Util;
|
||||
using Character = FFXIVClientStructs.FFXIV.Client.Game.Character.Character;
|
||||
using GameObject = FFXIVClientStructs.FFXIV.Client.Game.Object.GameObject;
|
||||
|
|
@ -13,70 +13,51 @@ using ObjectKind = Dalamud.Game.ClientState.Objects.Enums.ObjectKind;
|
|||
|
||||
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>
|
||||
/// 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>
|
||||
public ModCollection PlayerCollection()
|
||||
{
|
||||
using var performance = _performance.Measure(PerformanceType.IdentifyCollection);
|
||||
var gameObject = (GameObject*)(_clientState.LocalPlayer?.Address ?? nint.Zero);
|
||||
using var performance1 = performance.Measure(PerformanceType.IdentifyCollection);
|
||||
var gameObject = (GameObject*)(clientState.LocalPlayer?.Address ?? nint.Zero);
|
||||
if (gameObject == null)
|
||||
return _collectionManager.Active.ByType(CollectionType.Yourself)
|
||||
?? _collectionManager.Active.Default;
|
||||
return collectionManager.Active.ByType(CollectionType.Yourself)
|
||||
?? collectionManager.Active.Default;
|
||||
|
||||
var player = _actors.GetCurrentPlayer();
|
||||
var player = actors.GetCurrentPlayer();
|
||||
var _ = false;
|
||||
return CollectionByIdentifier(player)
|
||||
?? CheckYourself(player, gameObject)
|
||||
?? CollectionByAttributes(gameObject, ref _)
|
||||
?? _collectionManager.Active.Default;
|
||||
?? collectionManager.Active.Default;
|
||||
}
|
||||
|
||||
/// <summary> Identify the correct collection for a game object. </summary>
|
||||
public ResolveData IdentifyCollection(GameObject* gameObject, bool useCache)
|
||||
{
|
||||
using var t = _performance.Measure(PerformanceType.IdentifyCollection);
|
||||
using var t = performance.Measure(PerformanceType.IdentifyCollection);
|
||||
|
||||
if (gameObject == null)
|
||||
return _collectionManager.Active.Default.ToResolveData();
|
||||
return collectionManager.Active.Default.ToResolveData();
|
||||
|
||||
try
|
||||
{
|
||||
if (useCache && _cache.TryGetValue(gameObject, out var data))
|
||||
if (useCache && cache.TryGetValue(gameObject, out var data))
|
||||
return data;
|
||||
|
||||
if (LoginScreen(gameObject, out data))
|
||||
|
|
@ -90,26 +71,26 @@ public unsafe class CollectionResolver
|
|||
catch (Exception 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>
|
||||
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>
|
||||
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
|
||||
: _drawObjectState.LastGameObject);
|
||||
: drawObjectState.LastGameObject);
|
||||
return IdentifyCollection(obj, useCache);
|
||||
}
|
||||
|
||||
/// <summary> Return whether the given ModelChara id refers to a human-type model. </summary>
|
||||
public bool IsModelHuman(uint modelCharaId)
|
||||
=> _humanModels.IsHuman(modelCharaId);
|
||||
=> humanModels.IsHuman(modelCharaId);
|
||||
|
||||
/// <summary> Return whether the given character has a human model. </summary>
|
||||
public bool IsModelHuman(Character* character)
|
||||
|
|
@ -124,36 +105,36 @@ public unsafe class CollectionResolver
|
|||
{
|
||||
// Also check for empty names because sometimes named other characters
|
||||
// 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;
|
||||
return false;
|
||||
}
|
||||
|
||||
var notYetReady = false;
|
||||
var collection = _collectionManager.Active.ByType(CollectionType.Yourself)
|
||||
var collection = collectionManager.Active.ByType(CollectionType.Yourself)
|
||||
?? CollectionByAttributes(gameObject, ref notYetReady)
|
||||
?? _collectionManager.Active.Default;
|
||||
ret = notYetReady ? collection.ToResolveData(gameObject) : _cache.Set(collection, ActorIdentifier.Invalid, gameObject);
|
||||
?? collectionManager.Active.Default;
|
||||
ret = notYetReady ? collection.ToResolveData(gameObject) : cache.Set(collection, ActorIdentifier.Invalid, gameObject);
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <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)
|
||||
{
|
||||
if (_gameGui.GetAddonByName("ScreenLog") != IntPtr.Zero)
|
||||
if (gameGui.GetAddonByName("ScreenLog") != IntPtr.Zero)
|
||||
{
|
||||
ret = ResolveData.Invalid;
|
||||
return false;
|
||||
}
|
||||
|
||||
var player = _actors.GetCurrentPlayer();
|
||||
var player = actors.GetCurrentPlayer();
|
||||
var notYetReady = false;
|
||||
var collection = (player.IsValid ? CollectionByIdentifier(player) : null)
|
||||
?? _collectionManager.Active.ByType(CollectionType.Yourself)
|
||||
?? collectionManager.Active.ByType(CollectionType.Yourself)
|
||||
?? CollectionByAttributes(gameObject, ref notYetReady)
|
||||
?? _collectionManager.Active.Default;
|
||||
ret = notYetReady ? collection.ToResolveData(gameObject) : _cache.Set(collection, ActorIdentifier.Invalid, gameObject);
|
||||
?? collectionManager.Active.Default;
|
||||
ret = notYetReady ? collection.ToResolveData(gameObject) : cache.Set(collection, ActorIdentifier.Invalid, gameObject);
|
||||
return true;
|
||||
}
|
||||
|
||||
|
|
@ -163,12 +144,12 @@ public unsafe class CollectionResolver
|
|||
/// </summary>
|
||||
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)
|
||||
{
|
||||
(identifier, var type) = _collectionManager.Active.Individuals.ConvertSpecialIdentifier(identifier);
|
||||
if (_config.UseNoModsInInspect && type == IndividualCollections.SpecialResult.Inspect)
|
||||
return _cache.Set(ModCollection.Empty, identifier, gameObject);
|
||||
(identifier, var type) = collectionManager.Active.Individuals.ConvertSpecialIdentifier(identifier);
|
||||
if (config.UseNoModsInInspect && type == IndividualCollections.SpecialResult.Inspect)
|
||||
return cache.Set(ModCollection.Empty, identifier, gameObject);
|
||||
}
|
||||
|
||||
var notYetReady = false;
|
||||
|
|
@ -176,15 +157,15 @@ public unsafe class CollectionResolver
|
|||
?? CheckYourself(identifier, gameObject)
|
||||
?? CollectionByAttributes(gameObject, 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>
|
||||
private ModCollection? CollectionByIdentifier(ActorIdentifier identifier)
|
||||
=> _tempCollections.Collections.TryGetCollection(identifier, out var collection)
|
||||
|| _collectionManager.Active.Individuals.TryGetCollection(identifier, out collection)
|
||||
=> tempCollections.Collections.TryGetCollection(identifier, out var collection)
|
||||
|| collectionManager.Active.Individuals.TryGetCollection(identifier, out collection)
|
||||
? collection
|
||||
: null;
|
||||
|
||||
|
|
@ -192,9 +173,9 @@ public unsafe class CollectionResolver
|
|||
private ModCollection? CheckYourself(ActorIdentifier identifier, GameObject* actor)
|
||||
{
|
||||
if (actor->ObjectIndex == 0
|
||||
|| _cutscenes.GetParentIndex(actor->ObjectIndex) == 0
|
||||
|| identifier.Equals(_actors.GetCurrentPlayer()))
|
||||
return _collectionManager.Active.ByType(CollectionType.Yourself);
|
||||
|| cutscenes.GetParentIndex(actor->ObjectIndex) == 0
|
||||
|| identifier.Equals(actors.GetCurrentPlayer()))
|
||||
return collectionManager.Active.ByType(CollectionType.Yourself);
|
||||
|
||||
return null;
|
||||
}
|
||||
|
|
@ -219,8 +200,8 @@ public unsafe class CollectionResolver
|
|||
var bodyType = character->DrawData.CustomizeData[2];
|
||||
var collection = bodyType switch
|
||||
{
|
||||
3 => _collectionManager.Active.ByType(CollectionType.NonPlayerElderly),
|
||||
4 => _collectionManager.Active.ByType(CollectionType.NonPlayerChild),
|
||||
3 => collectionManager.Active.ByType(CollectionType.NonPlayerElderly),
|
||||
4 => collectionManager.Active.ByType(CollectionType.NonPlayerChild),
|
||||
_ => null,
|
||||
};
|
||||
if (collection != null)
|
||||
|
|
@ -231,18 +212,18 @@ public unsafe class CollectionResolver
|
|||
var isNpc = actor->ObjectKind != (byte)ObjectKind.Player;
|
||||
|
||||
var type = CollectionTypeExtensions.FromParts(race, gender, isNpc);
|
||||
collection = _collectionManager.Active.ByType(type);
|
||||
collection ??= _collectionManager.Active.ByType(CollectionTypeExtensions.FromParts(gender, isNpc));
|
||||
collection = collectionManager.Active.ByType(type);
|
||||
collection ??= collectionManager.Active.ByType(CollectionTypeExtensions.FromParts(gender, isNpc));
|
||||
return collection;
|
||||
}
|
||||
|
||||
/// <summary> Get the collection applying to the owner if it is available. </summary>
|
||||
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;
|
||||
|
||||
var id = _actors.CreateIndividualUnchecked(IdentifierType.Player, identifier.PlayerName, identifier.HomeWorld.Id,
|
||||
var id = actors.CreateIndividualUnchecked(IdentifierType.Player, identifier.PlayerName, identifier.HomeWorld.Id,
|
||||
ObjectKind.None,
|
||||
uint.MaxValue);
|
||||
return CheckYourself(id, owner)
|
||||
|
|
|
|||
|
|
@ -1,31 +1,34 @@
|
|||
using Dalamud.Plugin.Services;
|
||||
using FFXIVClientStructs.FFXIV.Client.Game.Character;
|
||||
using OtterGui.Services;
|
||||
using Penumbra.GameData.Enums;
|
||||
using Penumbra.Interop.Services;
|
||||
using Penumbra.Interop.Hooks;
|
||||
|
||||
namespace Penumbra.Interop.PathResolving;
|
||||
|
||||
public class CutsceneService : IDisposable
|
||||
public sealed class CutsceneService : IService, IDisposable
|
||||
{
|
||||
public const int CutsceneStartIdx = (int)ScreenActor.CutsceneStart;
|
||||
public const int CutsceneEndIdx = (int)ScreenActor.CutsceneEnd;
|
||||
public const int CutsceneSlots = CutsceneEndIdx - CutsceneStartIdx;
|
||||
|
||||
private readonly GameEventManager _events;
|
||||
private readonly IObjectTable _objects;
|
||||
private readonly short[] _copiedCharacters = Enumerable.Repeat((short)-1, CutsceneSlots).ToArray();
|
||||
private readonly IObjectTable _objects;
|
||||
private readonly CopyCharacter _copyCharacter;
|
||||
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
|
||||
=> Enumerable.Range(CutsceneStartIdx, CutsceneSlots)
|
||||
.Where(i => _objects[i] != null)
|
||||
.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;
|
||||
_events = events;
|
||||
_events.CopyCharacter += OnCharacterCopy;
|
||||
_events.CharacterDestructor += OnCharacterDestructor;
|
||||
_objects = objects;
|
||||
_copyCharacter = copyCharacter;
|
||||
_characterDestructor = characterDestructor;
|
||||
_copyCharacter.Subscribe(OnCharacterCopy, CopyCharacter.Priority.CutsceneService);
|
||||
_characterDestructor.Subscribe(OnCharacterDestructor, CharacterDestructor.Priority.CutsceneService);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
|
@ -57,8 +60,8 @@ public class CutsceneService : IDisposable
|
|||
|
||||
public unsafe void Dispose()
|
||||
{
|
||||
_events.CopyCharacter -= OnCharacterCopy;
|
||||
_events.CharacterDestructor -= OnCharacterDestructor;
|
||||
_copyCharacter.Unsubscribe(OnCharacterCopy);
|
||||
_characterDestructor.Unsubscribe(OnCharacterDestructor);
|
||||
}
|
||||
|
||||
private unsafe void OnCharacterDestructor(Character* character)
|
||||
|
|
|
|||
|
|
@ -1,35 +1,39 @@
|
|||
using Dalamud.Hooking;
|
||||
using Dalamud.Utility.Signatures;
|
||||
using Dalamud.Plugin.Services;
|
||||
using FFXIVClientStructs.FFXIV.Client.Game.Character;
|
||||
using FFXIVClientStructs.FFXIV.Client.Game.Object;
|
||||
using Penumbra.GameData;
|
||||
using Penumbra.Interop.Services;
|
||||
using FFXIVClientStructs.FFXIV.Client.Graphics.Scene;
|
||||
using OtterGui.Services;
|
||||
using Penumbra.Interop.Hooks;
|
||||
using Object = FFXIVClientStructs.FFXIV.Client.Graphics.Scene.Object;
|
||||
using Penumbra.GameData.Structs;
|
||||
|
||||
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 GameEventManager _gameEvents;
|
||||
private readonly IObjectTable _objects;
|
||||
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 ThreadLocal<Queue<nint>> _lastGameObject = new(() => new Queue<nint>());
|
||||
private readonly Dictionary<nint, (nint GameObject, bool IsChild)> _drawObjectToGameObject = [];
|
||||
|
||||
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);
|
||||
_enableDrawHook.Enable();
|
||||
_objects = objects;
|
||||
_gameEvents = gameEvents;
|
||||
_gameEvents.WeaponReloading += OnWeaponReloading;
|
||||
_gameEvents.WeaponReloaded += OnWeaponReloaded;
|
||||
_gameEvents.CharacterBaseCreated += OnCharacterBaseCreated;
|
||||
_gameEvents.CharacterBaseDestructor += OnCharacterBaseDestructor;
|
||||
_objects = objects;
|
||||
_createCharacterBase = createCharacterBase;
|
||||
_weaponReload = weaponReload;
|
||||
_characterBaseDestructor = characterBaseDestructor;
|
||||
_gameState = gameState;
|
||||
_weaponReload.Subscribe(OnWeaponReloading, WeaponReload.Priority.DrawObjectState);
|
||||
_weaponReload.Subscribe(OnWeaponReloaded, WeaponReload.PostEvent.Priority.DrawObjectState);
|
||||
_createCharacterBase.Subscribe(OnCharacterBaseCreated, CreateCharacterBase.PostEvent.Priority.DrawObjectState);
|
||||
_characterBaseDestructor.Subscribe(OnCharacterBaseDestructor, CharacterBaseDestructor.Priority.DrawObjectState);
|
||||
InitializeDrawObjects();
|
||||
}
|
||||
|
||||
|
|
@ -57,32 +61,32 @@ public class DrawObjectState : IDisposable, IReadOnlyDictionary<nint, (nint, boo
|
|||
public IEnumerable<(nint, bool)> Values
|
||||
=> _drawObjectToGameObject.Values;
|
||||
|
||||
public void Dispose()
|
||||
public unsafe void Dispose()
|
||||
{
|
||||
_gameEvents.WeaponReloading -= OnWeaponReloading;
|
||||
_gameEvents.WeaponReloaded -= OnWeaponReloaded;
|
||||
_gameEvents.CharacterBaseCreated -= OnCharacterBaseCreated;
|
||||
_gameEvents.CharacterBaseDestructor -= OnCharacterBaseDestructor;
|
||||
_enableDrawHook.Dispose();
|
||||
_weaponReload.Unsubscribe(OnWeaponReloading);
|
||||
_weaponReload.Unsubscribe(OnWeaponReloaded);
|
||||
_createCharacterBase.Unsubscribe(OnCharacterBaseCreated);
|
||||
_characterBaseDestructor.Unsubscribe(OnCharacterBaseDestructor);
|
||||
}
|
||||
|
||||
private void OnWeaponReloading(nint _, nint gameObject)
|
||||
=> _lastGameObject.Value!.Enqueue(gameObject);
|
||||
private unsafe void OnWeaponReloading(DrawDataContainer* _, Character* character, CharacterWeapon* _2)
|
||||
=> _gameState.QueueGameObject((nint)character);
|
||||
|
||||
private unsafe void OnWeaponReloaded(nint _, nint gameObject)
|
||||
private unsafe void OnWeaponReloaded(DrawDataContainer* _, Character* character)
|
||||
{
|
||||
_lastGameObject.Value!.Dequeue();
|
||||
IterateDrawObjectTree((Object*)((GameObject*)gameObject)->DrawObject, gameObject, false, false);
|
||||
_gameState.DequeueGameObject();
|
||||
IterateDrawObjectTree((Object*)character->GameObject.DrawObject, (nint)character, false, false);
|
||||
}
|
||||
|
||||
private void OnCharacterBaseDestructor(nint characterBase)
|
||||
=> _drawObjectToGameObject.Remove(characterBase);
|
||||
private unsafe void OnCharacterBaseDestructor(CharacterBase* 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;
|
||||
if (gameObject != nint.Zero)
|
||||
_drawObjectToGameObject[drawObject] = (gameObject, false);
|
||||
_drawObjectToGameObject[(nint)drawObject] = (gameObject, false);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
|
@ -123,20 +127,4 @@ public class DrawObjectState : IDisposable, IReadOnlyDictionary<nint, (nint, boo
|
|||
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.Communication;
|
||||
using Penumbra.GameData.Actors;
|
||||
using Penumbra.Interop.Services;
|
||||
using Penumbra.Interop.Hooks;
|
||||
using Penumbra.Services;
|
||||
|
||||
namespace Penumbra.Interop.PathResolving;
|
||||
|
|
@ -13,20 +13,20 @@ namespace Penumbra.Interop.PathResolving;
|
|||
public unsafe class IdentifiedCollectionCache : IDisposable, IEnumerable<(nint Address, ActorIdentifier Identifier, ModCollection Collection)>
|
||||
{
|
||||
private readonly CommunicatorService _communicator;
|
||||
private readonly GameEventManager _events;
|
||||
private readonly CharacterDestructor _characterDestructor;
|
||||
private readonly IClientState _clientState;
|
||||
private readonly Dictionary<nint, (ActorIdentifier, ModCollection)> _cache = new(317);
|
||||
private bool _dirty;
|
||||
|
||||
public IdentifiedCollectionCache(IClientState clientState, CommunicatorService communicator, GameEventManager events)
|
||||
public IdentifiedCollectionCache(IClientState clientState, CommunicatorService communicator, CharacterDestructor characterDestructor)
|
||||
{
|
||||
_clientState = clientState;
|
||||
_communicator = communicator;
|
||||
_events = events;
|
||||
_clientState = clientState;
|
||||
_communicator = communicator;
|
||||
_characterDestructor = characterDestructor;
|
||||
|
||||
_communicator.CollectionChange.Subscribe(CollectionChangeClear, CollectionChange.Priority.IdentifiedCollectionCache);
|
||||
_clientState.TerritoryChanged += TerritoryClear;
|
||||
_events.CharacterDestructor += OnCharacterDestruct;
|
||||
_characterDestructor.Subscribe(OnCharacterDestructor, CharacterDestructor.Priority.IdentifiedCollectionCache);
|
||||
}
|
||||
|
||||
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);
|
||||
_clientState.TerritoryChanged -= TerritoryClear;
|
||||
_events.CharacterDestructor -= OnCharacterDestruct;
|
||||
_characterDestructor.Unsubscribe(OnCharacterDestructor);
|
||||
}
|
||||
|
||||
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)
|
||||
=> _dirty = _cache.Count > 0;
|
||||
|
||||
private void OnCharacterDestruct(Character* character)
|
||||
private void OnCharacterDestructor(Character* 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 OtterGui.Classes;
|
||||
using Penumbra.Collections;
|
||||
using Penumbra.Api.Enums;
|
||||
using Penumbra.GameData;
|
||||
using Penumbra.GameData.Enums;
|
||||
using Penumbra.GameData.Structs;
|
||||
using Penumbra.Interop.Hooks;
|
||||
using Penumbra.Interop.ResourceLoading;
|
||||
using Penumbra.Interop.Services;
|
||||
using Penumbra.Services;
|
||||
using Penumbra.String.Classes;
|
||||
using Penumbra.Util;
|
||||
using ObjectType = FFXIVClientStructs.FFXIV.Client.Graphics.Scene.ObjectType;
|
||||
using CharacterUtility = Penumbra.Interop.Services.CharacterUtility;
|
||||
using static Penumbra.GameData.Enums.GenderRace;
|
||||
|
||||
namespace Penumbra.Interop.PathResolving;
|
||||
|
||||
|
|
@ -42,56 +36,40 @@ namespace Penumbra.Interop.PathResolving;
|
|||
// 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.
|
||||
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 CommunicatorService _communicator;
|
||||
private readonly PerformanceTracker _performance;
|
||||
private readonly CollectionResolver _collectionResolver;
|
||||
private readonly ResourceLoader _resources;
|
||||
private readonly GameEventManager _gameEventManager;
|
||||
private readonly CharacterUtility _characterUtility;
|
||||
private readonly CreateCharacterBase _createCharacterBase;
|
||||
|
||||
public ResolveData CustomizeChangeCollection = ResolveData.Invalid;
|
||||
|
||||
private ResolveData _lastCreatedCollection = ResolveData.Invalid;
|
||||
private ResolveData _customizeChangeCollection = ResolveData.Invalid;
|
||||
private DisposableContainer _characterBaseCreateMetaChanges = DisposableContainer.Empty;
|
||||
|
||||
public MetaState(PerformanceTracker performance, CommunicatorService communicator, CollectionResolver collectionResolver,
|
||||
ResourceLoader resources, GameEventManager gameEventManager, CharacterUtility characterUtility, Configuration config,
|
||||
IGameInteropProvider interop)
|
||||
public MetaState(CommunicatorService communicator, CollectionResolver collectionResolver,
|
||||
ResourceLoader resources, CreateCharacterBase createCharacterBase, CharacterUtility characterUtility, Configuration config)
|
||||
{
|
||||
_performance = performance;
|
||||
_communicator = communicator;
|
||||
_collectionResolver = collectionResolver;
|
||||
_resources = resources;
|
||||
_gameEventManager = gameEventManager;
|
||||
_characterUtility = characterUtility;
|
||||
_config = config;
|
||||
interop.InitializeFromAttributes(this);
|
||||
_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;
|
||||
_communicator = communicator;
|
||||
_collectionResolver = collectionResolver;
|
||||
_resources = resources;
|
||||
_createCharacterBase = createCharacterBase;
|
||||
_characterUtility = characterUtility;
|
||||
_config = config;
|
||||
_createCharacterBase.Subscribe(OnCreatingCharacterBase, CreateCharacterBase.Priority.MetaState);
|
||||
_createCharacterBase.Subscribe(OnCharacterBaseCreated, CreateCharacterBase.PostEvent.Priority.MetaState);
|
||||
}
|
||||
|
||||
public bool HandleDecalFile(ResourceType type, Utf8GamePath gamePath, out ResolveData resolveData)
|
||||
{
|
||||
if (type == ResourceType.Tex
|
||||
&& (_lastCreatedCollection.Valid || _customizeChangeCollection.Valid)
|
||||
&& (_lastCreatedCollection.Valid || CustomizeChangeCollection.Valid)
|
||||
&& gamePath.Path.Substring("chara/common/texture/".Length).StartsWith("decal"u8))
|
||||
{
|
||||
resolveData = _lastCreatedCollection.Valid ? _lastCreatedCollection : _customizeChangeCollection;
|
||||
resolveData = _lastCreatedCollection.Valid ? _lastCreatedCollection : CustomizeChangeCollection;
|
||||
return true;
|
||||
}
|
||||
|
||||
|
|
@ -100,186 +78,81 @@ public unsafe class MetaState : IDisposable
|
|||
}
|
||||
|
||||
public DisposableContainer ResolveEqdpData(ModCollection collection, GenderRace race, bool equipment, bool accessory)
|
||||
{
|
||||
var races = race.Dependencies();
|
||||
=> (equipment, accessory) switch
|
||||
{
|
||||
(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
|
||||
? races.Select(r => collection.TemporarilySetEqdpFile(_characterUtility, r, false))
|
||||
: Array.Empty<IDisposable?>().AsEnumerable();
|
||||
var accessoryEnumerable = accessory
|
||||
? races.Select(r => collection.TemporarilySetEqdpFile(_characterUtility, r, true))
|
||||
: Array.Empty<IDisposable?>().AsEnumerable();
|
||||
return new DisposableContainer(equipmentEnumerable.Concat(accessoryEnumerable));
|
||||
}
|
||||
public MetaList.MetaReverter ResolveEqpData(ModCollection collection)
|
||||
=> collection.TemporarilySetEqpFile(_characterUtility);
|
||||
|
||||
public MetaList.MetaReverter ResolveGmpData(ModCollection collection)
|
||||
=> collection.TemporarilySetGmpFile(_characterUtility);
|
||||
|
||||
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)
|
||||
=> (GenderRace)((Human*)human)->RaceSexId;
|
||||
|
||||
public void Dispose()
|
||||
public static GenderRace GetDrawObjectGenderRace(nint drawObject)
|
||||
{
|
||||
_getEqpIndirectHook.Dispose();
|
||||
_updateModelsHook.Dispose();
|
||||
_onModelLoadCompleteHook.Dispose();
|
||||
_setupVisorHook.Dispose();
|
||||
_rspSetupCharacterHook.Dispose();
|
||||
_changeCustomize.Dispose();
|
||||
_calculateHeightHook.Dispose();
|
||||
_gameEventManager.CreatingCharacterBase -= OnCreatingCharacterBase;
|
||||
_gameEventManager.CharacterBaseCreated -= OnCharacterBaseCreated;
|
||||
var draw = (DrawObject*)drawObject;
|
||||
if (draw->Object.GetObjectType() != ObjectType.CharacterBase)
|
||||
return GenderRace.Unknown;
|
||||
|
||||
var c = (CharacterBase*)drawObject;
|
||||
return c->GetModelType() == CharacterBase.ModelType.Human
|
||||
? GetHumanGenderRace(drawObject)
|
||||
: GenderRace.Unknown;
|
||||
}
|
||||
|
||||
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);
|
||||
if (_lastCreatedCollection.Valid && _lastCreatedCollection.AssociatedGameObject != nint.Zero)
|
||||
_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,
|
||||
UsesDecal(*(uint*)modelCharaId, customize));
|
||||
UsesDecal(*(uint*)modelCharaId, (nint)customize));
|
||||
var cmp = _lastCreatedCollection.ModCollection.TemporarilySetCmpFile(_characterUtility);
|
||||
_characterBaseCreateMetaChanges.Dispose(); // Should always be empty.
|
||||
_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 = 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,
|
||||
_lastCreatedCollection.ModCollection, drawObject);
|
||||
_lastCreatedCollection.ModCollection, (nint)drawObject);
|
||||
_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>
|
||||
/// Check the customize array for the FaceCustomization byte and the last bit of that.
|
||||
/// Also check for humans.
|
||||
/// </summary>
|
||||
public static bool UsesDecal(uint modelId, nint customizeData)
|
||||
private static bool UsesDecal(uint modelId, nint customizeData)
|
||||
=> modelId == 0 && ((byte*)customizeData)[12] > 0x7F;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -18,26 +18,28 @@ public class PathResolver : IDisposable
|
|||
private readonly TempCollectionManager _tempCollections;
|
||||
private readonly ResourceLoader _loader;
|
||||
|
||||
private readonly AnimationHookService _animationHookService;
|
||||
private readonly SubfileHelper _subfileHelper;
|
||||
private readonly PathState _pathState;
|
||||
private readonly MetaState _metaState;
|
||||
private readonly SubfileHelper _subfileHelper;
|
||||
private readonly PathState _pathState;
|
||||
private readonly MetaState _metaState;
|
||||
private readonly GameState _gameState;
|
||||
private readonly CollectionResolver _collectionResolver;
|
||||
|
||||
public unsafe PathResolver(PerformanceTracker performance, Configuration config, CollectionManager collectionManager,
|
||||
TempCollectionManager tempCollections, ResourceLoader loader, AnimationHookService animationHookService, SubfileHelper subfileHelper,
|
||||
PathState pathState, MetaState metaState)
|
||||
TempCollectionManager tempCollections, ResourceLoader loader, SubfileHelper subfileHelper,
|
||||
PathState pathState, MetaState metaState, CollectionResolver collectionResolver, GameState gameState)
|
||||
{
|
||||
_performance = performance;
|
||||
_config = config;
|
||||
_collectionManager = collectionManager;
|
||||
_tempCollections = tempCollections;
|
||||
_animationHookService = animationHookService;
|
||||
_subfileHelper = subfileHelper;
|
||||
_pathState = pathState;
|
||||
_metaState = metaState;
|
||||
_loader = loader;
|
||||
_loader.ResolvePath = ResolvePath;
|
||||
_loader.FileLoaded += ImcLoadResource;
|
||||
_performance = performance;
|
||||
_config = config;
|
||||
_collectionManager = collectionManager;
|
||||
_tempCollections = tempCollections;
|
||||
_subfileHelper = subfileHelper;
|
||||
_pathState = pathState;
|
||||
_metaState = metaState;
|
||||
_gameState = gameState;
|
||||
_collectionResolver = collectionResolver;
|
||||
_loader = loader;
|
||||
_loader.ResolvePath = ResolvePath;
|
||||
_loader.FileLoaded += ImcLoadResource;
|
||||
}
|
||||
|
||||
/// <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.
|
||||
var nonDefault = _subfileHelper.HandleSubFiles(type, out var 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);
|
||||
if (!nonDefault || !resolveData.Valid)
|
||||
resolveData = _collectionManager.Active.Default.ToResolveData();
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ using Dalamud.Utility.Signatures;
|
|||
using Penumbra.Api.Enums;
|
||||
using Penumbra.Collections;
|
||||
using Penumbra.GameData;
|
||||
using Penumbra.Interop.Hooks;
|
||||
using Penumbra.Interop.ResourceLoading;
|
||||
using Penumbra.Interop.Services;
|
||||
using Penumbra.Interop.Structs;
|
||||
|
|
@ -21,30 +22,30 @@ namespace Penumbra.Interop.PathResolving;
|
|||
/// </summary>
|
||||
public unsafe class SubfileHelper : IDisposable, IReadOnlyCollection<KeyValuePair<nint, ResolveData>>
|
||||
{
|
||||
private readonly PerformanceTracker _performance;
|
||||
private readonly ResourceLoader _loader;
|
||||
private readonly GameEventManager _events;
|
||||
private readonly CommunicatorService _communicator;
|
||||
private readonly PerformanceTracker _performance;
|
||||
private readonly ResourceLoader _loader;
|
||||
private readonly ResourceHandleDestructor _resourceHandleDestructor;
|
||||
private readonly CommunicatorService _communicator;
|
||||
|
||||
private readonly ThreadLocal<ResolveData> _mtrlData = new(() => ResolveData.Invalid);
|
||||
private readonly ThreadLocal<ResolveData> _avfxData = new(() => ResolveData.Invalid);
|
||||
|
||||
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);
|
||||
|
||||
_performance = performance;
|
||||
_loader = loader;
|
||||
_events = events;
|
||||
_communicator = communicator;
|
||||
_performance = performance;
|
||||
_loader = loader;
|
||||
_communicator = communicator;
|
||||
_resourceHandleDestructor = resourceHandleDestructor;
|
||||
|
||||
_loadMtrlShpkHook.Enable();
|
||||
_loadMtrlTexHook.Enable();
|
||||
_apricotResourceLoadHook.Enable();
|
||||
_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()
|
||||
{
|
||||
_loader.ResourceLoaded -= SubfileContainerRequested;
|
||||
_events.ResourceHandleDestructor -= ResourceDestroyed;
|
||||
_resourceHandleDestructor.Unsubscribe(ResourceDestroyed);
|
||||
_loadMtrlShpkHook.Dispose();
|
||||
_loadMtrlTexHook.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 Penumbra.Communication;
|
||||
using Penumbra.GameData;
|
||||
using Penumbra.Interop.Hooks;
|
||||
using Penumbra.Services;
|
||||
|
||||
namespace Penumbra.Interop.Services;
|
||||
|
|
@ -32,9 +33,9 @@ public sealed unsafe class SkinFixer : IDisposable
|
|||
|
||||
private readonly Hook<OnRenderMaterialDelegate> _onRenderMaterialHook;
|
||||
|
||||
private readonly GameEventManager _gameEvents;
|
||||
private readonly CommunicatorService _communicator;
|
||||
private readonly CharacterUtility _utility;
|
||||
private readonly ResourceHandleDestructor _resourceHandleDestructor;
|
||||
private readonly CommunicatorService _communicator;
|
||||
private readonly CharacterUtility _utility;
|
||||
|
||||
// MaterialResourceHandle set
|
||||
private readonly ConcurrentSet<nint> _moddedSkinShpkMaterials = new();
|
||||
|
|
@ -50,15 +51,16 @@ public sealed unsafe class SkinFixer : IDisposable
|
|||
public int 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);
|
||||
_gameEvents = gameEvents;
|
||||
_utility = utility;
|
||||
_communicator = communicator;
|
||||
_onRenderMaterialHook = interop.HookFromAddress<OnRenderMaterialDelegate>(_humanVTable[62], OnRenderHumanMaterial);
|
||||
_resourceHandleDestructor = resourceHandleDestructor;
|
||||
_utility = utility;
|
||||
_communicator = communicator;
|
||||
_onRenderMaterialHook = interop.HookFromAddress<OnRenderMaterialDelegate>(_humanVTable[62], OnRenderHumanMaterial);
|
||||
_communicator.MtrlShpkLoaded.Subscribe(OnMtrlShpkLoaded, MtrlShpkLoaded.Priority.SkinFixer);
|
||||
_gameEvents.ResourceHandleDestructor += OnResourceHandleDestructor;
|
||||
_resourceHandleDestructor.Subscribe(OnResourceHandleDestructor, ResourceHandleDestructor.Priority.SkinFixer);
|
||||
_onRenderMaterialHook.Enable();
|
||||
}
|
||||
|
||||
|
|
@ -66,7 +68,7 @@ public sealed unsafe class SkinFixer : IDisposable
|
|||
{
|
||||
_onRenderMaterialHook.Dispose();
|
||||
_communicator.MtrlShpkLoaded.Unsubscribe(OnMtrlShpkLoaded);
|
||||
_gameEvents.ResourceHandleDestructor -= OnResourceHandleDestructor;
|
||||
_resourceHandleDestructor.Unsubscribe(OnResourceHandleDestructor);
|
||||
_moddedSkinShpkMaterials.Clear();
|
||||
_moddedSkinShpkCount = 0;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,7 +2,6 @@ using Dalamud.Utility;
|
|||
using Newtonsoft.Json.Linq;
|
||||
using OtterGui.Classes;
|
||||
using Penumbra.Services;
|
||||
using Penumbra.Util;
|
||||
|
||||
namespace Penumbra.Mods.Manager;
|
||||
|
||||
|
|
|
|||
|
|
@ -55,7 +55,10 @@ public class Penumbra : IDalamudPlugin
|
|||
_services = ServiceManagerA.CreateProvider(this, pluginInterface, Log);
|
||||
Messager = _services.GetService<MessageService>();
|
||||
_validityChecker = _services.GetService<ValidityChecker>();
|
||||
var startup = _services.GetService<DalamudConfigService>().GetDalamudConfig(DalamudConfigService.WaitingForPluginsOption, out bool s)
|
||||
_services.EnsureRequiredServices();
|
||||
|
||||
var startup = _services.GetService<DalamudConfigService>()
|
||||
.GetDalamudConfig(DalamudConfigService.WaitingForPluginsOption, out bool s)
|
||||
? s.ToString()
|
||||
: "Unknown";
|
||||
Log.Information(
|
||||
|
|
@ -80,6 +83,10 @@ public class Penumbra : IDalamudPlugin
|
|||
_services.GetService<SkinFixer>();
|
||||
|
||||
_services.GetService<DalamudSubstitutionProvider>(); // Initialize before Interface.
|
||||
|
||||
foreach (var service in _services.GetServicesImplementing<IAwaitedService>())
|
||||
service.Awaiter.Wait();
|
||||
|
||||
SetupInterface();
|
||||
SetupApi();
|
||||
|
||||
|
|
|
|||
|
|
@ -69,6 +69,7 @@
|
|||
|
||||
<ItemGroup>
|
||||
<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="SharpCompress" Version="0.33.0" />
|
||||
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="7.0.0" />
|
||||
|
|
@ -94,8 +95,8 @@
|
|||
|
||||
<Target Name="GetGitHash" BeforeTargets="GetAssemblyVersion" Returns="InformationalVersion">
|
||||
<Exec Command="git rev-parse --short HEAD" ConsoleToMSBuild="true" StandardOutputImportance="low" ContinueOnError="true">
|
||||
<Output TaskParameter="ExitCode" PropertyName="GitCommitHashSuccess"/>
|
||||
<Output TaskParameter="ConsoleOutput" PropertyName="GitCommitHash" Condition="$(GitCommitHashSuccess) == 0"/>
|
||||
<Output TaskParameter="ExitCode" PropertyName="GitCommitHashSuccess" />
|
||||
<Output TaskParameter="ConsoleOutput" PropertyName="GitCommitHash" Condition="$(GitCommitHashSuccess) == 0" />
|
||||
</Exec>
|
||||
|
||||
<PropertyGroup>
|
||||
|
|
|
|||
|
|
@ -1,15 +1,24 @@
|
|||
using Newtonsoft.Json.Linq;
|
||||
using OtterGui.Classes;
|
||||
using OtterGui.Log;
|
||||
using OtterGui.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)
|
||||
{
|
||||
var files = PenumbraFiles(fileNames);
|
||||
Backup.CreateAutomaticBackup(logger, new DirectoryInfo(fileNames.ConfigDirectory), files);
|
||||
var files = PenumbraFiles(fileNames);
|
||||
Awaiter = Task.Run(() => Backup.CreateAutomaticBackup(logger, new DirectoryInfo(fileNames.ConfigDirectory), files));
|
||||
}
|
||||
|
||||
/// <summary> Collect all relevant files for penumbra configuration. </summary>
|
||||
|
|
|
|||
|
|
@ -1,14 +1,15 @@
|
|||
using OtterGui.Classes;
|
||||
using OtterGui.Log;
|
||||
using OtterGui.Services;
|
||||
using Penumbra.Communication;
|
||||
|
||||
namespace Penumbra.Services;
|
||||
|
||||
public class CommunicatorService : IDisposable
|
||||
public class CommunicatorService : IDisposable, IService
|
||||
{
|
||||
public CommunicatorService(Logger logger)
|
||||
{
|
||||
EventWrapper.ChangeLogger(logger);
|
||||
EventWrapperBase.ChangeLogger(logger);
|
||||
}
|
||||
|
||||
/// <inheritdoc cref="Communication.CollectionChange"/>
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
using Newtonsoft.Json;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using OtterGui.Classes;
|
||||
using OtterGui.Filesystem;
|
||||
using OtterGui.Services;
|
||||
using Penumbra.Api.Enums;
|
||||
using Penumbra.Collections;
|
||||
using Penumbra.Collections.Manager;
|
||||
|
|
@ -22,10 +22,8 @@ namespace Penumbra.Services;
|
|||
/// Contains everything to migrate from older versions of the config to the current,
|
||||
/// including deprecated fields.
|
||||
/// </summary>
|
||||
public class ConfigMigrationService
|
||||
public class ConfigMigrationService(SaveService saveService) : IService
|
||||
{
|
||||
private readonly SaveService _saveService;
|
||||
|
||||
private Configuration _config = null!;
|
||||
private JObject _data = null!;
|
||||
|
||||
|
|
@ -33,14 +31,11 @@ public class ConfigMigrationService
|
|||
public string DefaultCollection = ModCollection.DefaultCollectionName;
|
||||
public string ForcedCollection = string.Empty;
|
||||
public Dictionary<string, string> CharacterCollections = [];
|
||||
public Dictionary<string, string> ModSortOrder = new();
|
||||
public Dictionary<string, string> ModSortOrder = [];
|
||||
public bool InvertModListOrder;
|
||||
public bool SortFoldersFirst;
|
||||
public SortModeV3 SortMode = SortModeV3.FoldersFirst;
|
||||
|
||||
public ConfigMigrationService(SaveService saveService)
|
||||
=> _saveService = saveService;
|
||||
|
||||
/// <summary> Add missing colors to the dictionary if necessary. </summary>
|
||||
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.
|
||||
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);
|
||||
return;
|
||||
}
|
||||
|
||||
_data = JObject.Parse(File.ReadAllText(_saveService.FileNames.ConfigFile));
|
||||
_data = JObject.Parse(File.ReadAllText(saveService.FileNames.ConfigFile));
|
||||
CreateBackup();
|
||||
|
||||
Version0To1();
|
||||
|
|
@ -118,7 +113,7 @@ public class ConfigMigrationService
|
|||
if (_config.Version != 6)
|
||||
return;
|
||||
|
||||
ActiveCollectionMigration.MigrateUngenderedCollections(_saveService.FileNames);
|
||||
ActiveCollectionMigration.MigrateUngenderedCollections(saveService.FileNames);
|
||||
_config.Version = 7;
|
||||
}
|
||||
|
||||
|
|
@ -223,7 +218,7 @@ public class ConfigMigrationService
|
|||
return;
|
||||
|
||||
// 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
|
||||
{
|
||||
|
|
@ -246,7 +241,7 @@ public class ConfigMigrationService
|
|||
private void ResettleSortOrder()
|
||||
{
|
||||
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 writer = new StreamWriter(stream);
|
||||
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,
|
||||
IEnumerable<(CollectionType, string)> special)
|
||||
{
|
||||
var file = _saveService.FileNames.ActiveCollectionsFile;
|
||||
var file = saveService.FileNames.ActiveCollectionsFile;
|
||||
try
|
||||
{
|
||||
using var stream = File.Open(file, File.Exists(file) ? FileMode.Truncate : FileMode.CreateNew);
|
||||
|
|
@ -337,7 +332,7 @@ public class ConfigMigrationService
|
|||
if (!collectionJson.Exists)
|
||||
return;
|
||||
|
||||
var defaultCollectionFile = new FileInfo(_saveService.FileNames.CollectionFile(ModCollection.DefaultCollectionName));
|
||||
var defaultCollectionFile = new FileInfo(saveService.FileNames.CollectionFile(ModCollection.DefaultCollectionName));
|
||||
if (defaultCollectionFile.Exists)
|
||||
return;
|
||||
|
||||
|
|
@ -370,9 +365,9 @@ public class ConfigMigrationService
|
|||
dict = dict.ToDictionary(kvp => kvp.Key, kvp => kvp.Value with { Priority = maxPriority - kvp.Value.Priority });
|
||||
|
||||
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>());
|
||||
_saveService.ImmediateSaveSync(new ModCollectionSave(emptyStorage, collection));
|
||||
saveService.ImmediateSaveSync(new ModCollectionSave(emptyStorage, collection));
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
|
|
@ -384,7 +379,7 @@ public class ConfigMigrationService
|
|||
// Create a backup of the configuration file specifically.
|
||||
private void CreateBackup()
|
||||
{
|
||||
var name = _saveService.FileNames.ConfigFile;
|
||||
var name = saveService.FileNames.ConfigFile;
|
||||
var bakName = name + ".bak";
|
||||
try
|
||||
{
|
||||
|
|
|
|||
|
|
@ -1,21 +1,12 @@
|
|||
using Dalamud.Game;
|
||||
using Dalamud.Game.ClientState.Objects;
|
||||
using Dalamud.Interface;
|
||||
using Dalamud.IoC;
|
||||
using Dalamud.Plugin;
|
||||
using Dalamud.Interface.DragDrop;
|
||||
using Dalamud.Plugin.Services;
|
||||
using OtterGui.Services;
|
||||
|
||||
// ReSharper disable AutoPropertyCanBeMadeGetOnly.Local
|
||||
|
||||
namespace Penumbra.Services;
|
||||
|
||||
public class DalamudConfigService
|
||||
public class DalamudConfigService : IService
|
||||
{
|
||||
public DalamudConfigService(DalamudPluginInterface pluginInterface)
|
||||
public DalamudConfigService()
|
||||
{
|
||||
pluginInterface.Inject(this);
|
||||
try
|
||||
{
|
||||
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 OtterGui.Filesystem;
|
||||
using OtterGui.Services;
|
||||
using Penumbra.Collections;
|
||||
using Penumbra.Mods;
|
||||
|
||||
namespace Penumbra.Services;
|
||||
|
||||
public class FilenameService(DalamudPluginInterface pi)
|
||||
public class FilenameService(DalamudPluginInterface pi) : IService
|
||||
{
|
||||
public readonly string ConfigDirectory = pi.ConfigDirectory.FullName;
|
||||
public readonly string CollectionDirectory = Path.Combine(pi.ConfigDirectory.FullName, "collections");
|
||||
|
|
|
|||
|
|
@ -5,15 +5,12 @@ using Dalamud.Interface;
|
|||
using Dalamud.Plugin.Services;
|
||||
using Lumina.Excel.GeneratedSheets;
|
||||
using OtterGui.Log;
|
||||
using OtterGui.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)
|
||||
{
|
||||
// @formatter:off
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
using OtterGui.Classes;
|
||||
using OtterGui.Log;
|
||||
using OtterGui.Services;
|
||||
using Penumbra.Mods;
|
||||
using Penumbra.Mods.Subclasses;
|
||||
|
||||
|
|
@ -11,12 +12,9 @@ namespace Penumbra.Services;
|
|||
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>
|
||||
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.Services;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using OtterGui;
|
||||
using OtterGui.Classes;
|
||||
using OtterGui.Compression;
|
||||
using OtterGui.Log;
|
||||
|
|
@ -8,7 +13,6 @@ using Penumbra.Api;
|
|||
using Penumbra.Collections.Cache;
|
||||
using Penumbra.Collections.Manager;
|
||||
using Penumbra.GameData.Actors;
|
||||
using Penumbra.GameData.Data;
|
||||
using Penumbra.Import.Models;
|
||||
using Penumbra.GameData.DataContainers;
|
||||
using Penumbra.GameData.Structs;
|
||||
|
|
@ -38,10 +42,9 @@ public static class ServiceManagerA
|
|||
public static ServiceManager CreateProvider(Penumbra penumbra, DalamudPluginInterface pi, Logger log)
|
||||
{
|
||||
var services = new ServiceManager(log)
|
||||
.AddDalamudServices(pi)
|
||||
.AddExistingService(log)
|
||||
.AddExistingService(penumbra)
|
||||
.AddMeta()
|
||||
.AddGameData()
|
||||
.AddInterop()
|
||||
.AddConfiguration()
|
||||
.AddCollections()
|
||||
|
|
@ -53,31 +56,34 @@ public static class ServiceManagerA
|
|||
.AddApi();
|
||||
services.AddIServices(typeof(EquipItem).Assembly);
|
||||
services.AddIServices(typeof(Penumbra).Assembly);
|
||||
DalamudServices.AddServices(services, pi);
|
||||
services.AddIServices(typeof(ImGuiUtil).Assembly);
|
||||
services.CreateProvider();
|
||||
return services;
|
||||
}
|
||||
|
||||
private static ServiceManager AddMeta(this ServiceManager services)
|
||||
=> services.AddSingleton<ValidityChecker>()
|
||||
.AddSingleton<PerformanceTracker>()
|
||||
.AddSingleton<FilenameService>()
|
||||
.AddSingleton<BackupService>()
|
||||
.AddSingleton<CommunicatorService>()
|
||||
.AddSingleton<MessageService>()
|
||||
.AddSingleton<SaveService>()
|
||||
.AddSingleton<FileCompactor>()
|
||||
.AddSingleton<DalamudConfigService>();
|
||||
|
||||
|
||||
private static ServiceManager AddGameData(this ServiceManager services)
|
||||
=> services.AddSingleton<GamePathParser>()
|
||||
.AddSingleton<StainService>()
|
||||
.AddSingleton<HumanModelList>();
|
||||
private static ServiceManager AddDalamudServices(this ServiceManager services, DalamudPluginInterface pi)
|
||||
=> services.AddExistingService(pi)
|
||||
.AddExistingService(pi.UiBuilder)
|
||||
.AddDalamudService<ICommandManager>(pi)
|
||||
.AddDalamudService<IDataManager>(pi)
|
||||
.AddDalamudService<IClientState>(pi)
|
||||
.AddDalamudService<IChatGui>(pi)
|
||||
.AddDalamudService<IFramework>(pi)
|
||||
.AddDalamudService<ICondition>(pi)
|
||||
.AddDalamudService<ITargetManager>(pi)
|
||||
.AddDalamudService<IObjectTable>(pi)
|
||||
.AddDalamudService<ITitleScreenMenu>(pi)
|
||||
.AddDalamudService<IGameGui>(pi)
|
||||
.AddDalamudService<IKeyState>(pi)
|
||||
.AddDalamudService<ISigScanner>(pi)
|
||||
.AddDalamudService<IDragDropManager>(pi)
|
||||
.AddDalamudService<ITextureProvider>(pi)
|
||||
.AddDalamudService<ITextureSubstitutionProvider>(pi)
|
||||
.AddDalamudService<IGameInteropProvider>(pi)
|
||||
.AddDalamudService<IPluginLog>(pi);
|
||||
|
||||
private static ServiceManager AddInterop(this ServiceManager services)
|
||||
=> services.AddSingleton<GameEventManager>()
|
||||
.AddSingleton<FrameworkManager>()
|
||||
=> services.AddSingleton<FrameworkManager>()
|
||||
.AddSingleton<CutsceneService>()
|
||||
.AddSingleton(p =>
|
||||
{
|
||||
|
|
@ -96,8 +102,7 @@ public static class ServiceManagerA
|
|||
.AddSingleton<ModelResourceHandleUtility>();
|
||||
|
||||
private static ServiceManager AddConfiguration(this ServiceManager services)
|
||||
=> services.AddSingleton<ConfigMigrationService>()
|
||||
.AddSingleton<Configuration>()
|
||||
=> services.AddSingleton<Configuration>()
|
||||
.AddSingleton<EphemeralConfig>();
|
||||
|
||||
private static ServiceManager AddCollections(this ServiceManager services)
|
||||
|
|
@ -130,8 +135,7 @@ public static class ServiceManagerA
|
|||
.AddSingleton<SkinFixer>();
|
||||
|
||||
private static ServiceManager AddResolvers(this ServiceManager services)
|
||||
=> services.AddSingleton<AnimationHookService>()
|
||||
.AddSingleton<CollectionResolver>()
|
||||
=> services.AddSingleton<CollectionResolver>()
|
||||
.AddSingleton<CutsceneService>()
|
||||
.AddSingleton<DrawObjectState>()
|
||||
.AddSingleton<MetaState>()
|
||||
|
|
|
|||
|
|
@ -1,36 +1,26 @@
|
|||
using Dalamud.Interface;
|
||||
using Dalamud.Interface.Utility.Raii;
|
||||
using Dalamud.Plugin;
|
||||
using Dalamud.Plugin.Services;
|
||||
using ImGuiNET;
|
||||
using OtterGui.Log;
|
||||
using OtterGui.Services;
|
||||
using OtterGui.Widgets;
|
||||
using Penumbra.GameData.DataContainers;
|
||||
using Penumbra.GameData.Files;
|
||||
using Penumbra.UI.AdvancedWindow;
|
||||
using Penumbra.Util;
|
||||
|
||||
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()
|
||||
{
|
||||
var baseSize = ImGui.CalcTextSize("0000").X + ImGui.GetStyle().ScrollbarSize + ImGui.GetStyle().ItemInnerSpacing.X;
|
||||
if (_stainCombo.CurrentSelection.Key == 0)
|
||||
var baseSize = ImGui.CalcTextSize("0000").X + ImGui.GetStyle().ScrollbarSize + ImGui.GetStyle().ItemInnerSpacing.X;
|
||||
if (stainCombo.CurrentSelection.Key == 0)
|
||||
return baseSize;
|
||||
|
||||
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,
|
||||
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))
|
||||
.Push(ImGuiStyleVar.ItemSpacing, ImGui.GetStyle().ItemSpacing with { X = ImGui.GetStyle().ItemInnerSpacing.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);
|
||||
}
|
||||
|
||||
protected override bool DrawSelectable(int globalIdx, bool selected)
|
||||
{
|
||||
var ret = base.DrawSelectable(globalIdx, selected);
|
||||
var selection = _stainCombo.CurrentSelection.Key;
|
||||
if (selection == 0 || !_stmFile.TryGetValue(Items[globalIdx], selection, out var colors))
|
||||
var selection = stainCombo.CurrentSelection.Key;
|
||||
if (selection == 0 || !stmFile.TryGetValue(Items[globalIdx], selection, out var colors))
|
||||
return ret;
|
||||
|
||||
ImGui.SameLine();
|
||||
|
|
@ -72,25 +62,18 @@ public class StainService : IDisposable
|
|||
}
|
||||
}
|
||||
|
||||
public readonly DictStains StainData;
|
||||
public readonly DictStain StainData;
|
||||
public readonly FilterComboColors StainCombo;
|
||||
public readonly StmFile StmFile;
|
||||
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,
|
||||
() => StainData.Value.Prepend(new KeyValuePair<byte, (string Name, uint Dye, bool Gloss)>(0, ("None", 0, false))).ToList(),
|
||||
Penumbra.Log);
|
||||
StmFile = new StmFile(dataManager);
|
||||
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.Plugin;
|
||||
using OtterGui.Classes;
|
||||
using OtterGui.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 SeaOfStars = "https://raw.githubusercontent.com/Ottermandias/SeaOfStars/main/repo.json";
|
||||
|
|
|
|||
|
|
@ -158,8 +158,6 @@ public class FileEditor<T> : IDisposable where T : class, IWritable
|
|||
|
||||
_quickImport = null;
|
||||
}
|
||||
|
||||
_fileDialog.Draw();
|
||||
}
|
||||
|
||||
public void Reset()
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
using Dalamud.Interface;
|
||||
using Dalamud.Interface.Internal.Notifications;
|
||||
using FFXIVClientStructs.FFXIV.Client.Graphics.Scene;
|
||||
using ImGuiNET;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using OtterGui;
|
||||
|
|
@ -8,6 +9,7 @@ using OtterGui.Raii;
|
|||
using Penumbra.GameData.Data;
|
||||
using Penumbra.GameData.Files;
|
||||
using Penumbra.GameData.Structs;
|
||||
using Penumbra.Interop.Hooks;
|
||||
using Penumbra.Interop.MaterialPreview;
|
||||
using Penumbra.String;
|
||||
using Penumbra.String.Classes;
|
||||
|
|
@ -503,12 +505,12 @@ public partial class ModEditWindow
|
|||
ColorTablePreviewers.Clear();
|
||||
}
|
||||
|
||||
private unsafe void UnbindFromDrawObjectMaterialInstances(nint characterBase)
|
||||
private unsafe void UnbindFromDrawObjectMaterialInstances(CharacterBase* characterBase)
|
||||
{
|
||||
for (var i = MaterialPreviewers.Count; i-- > 0;)
|
||||
{
|
||||
var previewer = MaterialPreviewers[i];
|
||||
if ((nint)previewer.DrawObject != characterBase)
|
||||
if (previewer.DrawObject != characterBase)
|
||||
continue;
|
||||
|
||||
previewer.Dispose();
|
||||
|
|
@ -518,7 +520,7 @@ public partial class ModEditWindow
|
|||
for (var i = ColorTablePreviewers.Count; i-- > 0;)
|
||||
{
|
||||
var previewer = ColorTablePreviewers[i];
|
||||
if ((nint)previewer.DrawObject != characterBase)
|
||||
if (previewer.DrawObject != characterBase)
|
||||
continue;
|
||||
|
||||
previewer.Dispose();
|
||||
|
|
@ -663,7 +665,7 @@ public partial class ModEditWindow
|
|||
UpdateConstants();
|
||||
}
|
||||
|
||||
public MtrlTab(ModEditWindow edit, MtrlFile file, string filePath, bool writable)
|
||||
public unsafe MtrlTab(ModEditWindow edit, MtrlFile file, string filePath, bool writable)
|
||||
{
|
||||
_edit = edit;
|
||||
Mtrl = file;
|
||||
|
|
@ -673,16 +675,16 @@ public partial class ModEditWindow
|
|||
LoadShpk(FindAssociatedShpk(out _, out _));
|
||||
if (writable)
|
||||
{
|
||||
_edit._gameEvents.CharacterBaseDestructor += UnbindFromDrawObjectMaterialInstances;
|
||||
_edit._characterBaseDestructor.Subscribe(UnbindFromDrawObjectMaterialInstances, CharacterBaseDestructor.Priority.MtrlTab);
|
||||
BindToMaterialInstances();
|
||||
}
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
public unsafe void Dispose()
|
||||
{
|
||||
UnbindFromMaterialInstances();
|
||||
if (Writable)
|
||||
_edit._gameEvents.CharacterBaseDestructor -= UnbindFromDrawObjectMaterialInstances;
|
||||
_edit._characterBaseDestructor.Unsubscribe(UnbindFromDrawObjectMaterialInstances);
|
||||
}
|
||||
|
||||
public bool Valid
|
||||
|
|
|
|||
|
|
@ -8,31 +8,22 @@ namespace Penumbra.UI.AdvancedWindow;
|
|||
|
||||
public partial class ModEditWindow
|
||||
{
|
||||
private partial class MdlTab : IWritable
|
||||
private class MdlTab : IWritable
|
||||
{
|
||||
private ModEditWindow _edit;
|
||||
private readonly ModEditWindow _edit;
|
||||
|
||||
public MdlFile Mdl { get; private set; }
|
||||
public MdlFile Mdl { get; private set; }
|
||||
private List<string>[] _attributes;
|
||||
|
||||
public List<Utf8GamePath>? GamePaths { get; private set; }
|
||||
public int GamePathIndex;
|
||||
|
||||
public bool PendingIo { get; private set; } = false;
|
||||
public string? IoException { get; private set; } = null;
|
||||
public int GamePathIndex;
|
||||
|
||||
[GeneratedRegex(@"chara/(?:equipment|accessory)/(?'Set'[a-z]\d{4})/model/(?'Race'c\d{4})\k'Set'_[^/]+\.mdl", RegexOptions.Compiled)]
|
||||
private static partial Regex CharaEquipmentRegex();
|
||||
public bool PendingIo { get; private set; }
|
||||
public string? IoException { get; private set; }
|
||||
|
||||
[GeneratedRegex(@"chara/human/(?'Race'c\d{4})/obj/(?'Type'[^/]+)/(?'Set'[^/]\d{4})/model/(?'Race'c\d{4})\k'Set'_[^/]+\.mdl", RegexOptions.Compiled)]
|
||||
private static partial Regex CharaHumanRegex();
|
||||
|
||||
[GeneratedRegex(@"chara/(?'SubCategory'demihuman|monster|weapon)/(?'Set'w\d{4})/obj/body/(?'Body'b\d{4})/model/\k'Set'\k'Body'.mdl", RegexOptions.Compiled)]
|
||||
private static partial Regex CharaBodyRegex();
|
||||
|
||||
public MdlTab(ModEditWindow edit, byte[] bytes, string path, Mod? mod)
|
||||
public MdlTab(ModEditWindow edit, byte[] bytes, string path, IMod? mod)
|
||||
{
|
||||
_edit = edit;
|
||||
_edit = edit;
|
||||
|
||||
Initialize(new MdlFile(bytes));
|
||||
|
||||
|
|
@ -58,23 +49,31 @@ public partial class ModEditWindow
|
|||
/// <summary> Find the list of game paths that may correspond to this model. </summary>
|
||||
/// <param name="path"> Resolved path to a .mdl. </param>
|
||||
/// <param name="mod"> Mod within which the .mdl is resolved. </param>
|
||||
private void FindGamePaths(string path, Mod mod)
|
||||
private void FindGamePaths(string path, IMod mod)
|
||||
{
|
||||
if (!Path.IsPathRooted(path) && Utf8GamePath.FromString(path, out var p))
|
||||
{
|
||||
GamePaths = [p];
|
||||
return;
|
||||
}
|
||||
|
||||
PendingIo = true;
|
||||
var task = Task.Run(() => {
|
||||
var task = Task.Run(() =>
|
||||
{
|
||||
// TODO: Is it worth trying to order results based on option priorities for cases where more than one match is found?
|
||||
// NOTE: We're using case insensitive comparisons, as option group paths in mods are stored in lower case, but the mod editor uses paths directly from the file system, which may be mixed case.
|
||||
// NOTE: We're using case-insensitive comparisons, as option group paths in mods are stored in lower case, but the mod editor uses paths directly from the file system, which may be mixed case.
|
||||
return mod.AllSubMods
|
||||
.SelectMany(submod => submod.Files.Concat(submod.FileSwaps))
|
||||
.SelectMany(m => m.Files.Concat(m.FileSwaps))
|
||||
.Where(kv => kv.Value.FullName.Equals(path, StringComparison.OrdinalIgnoreCase))
|
||||
.Select(kv => kv.Key)
|
||||
.ToList();
|
||||
});
|
||||
|
||||
task.ContinueWith(task => {
|
||||
IoException = task.Exception?.ToString();
|
||||
PendingIo = false;
|
||||
GamePaths = task.Result;
|
||||
task.ContinueWith(t =>
|
||||
{
|
||||
IoException = t.Exception?.ToString();
|
||||
GamePaths = t.Result;
|
||||
PendingIo = false;
|
||||
});
|
||||
}
|
||||
|
||||
|
|
@ -89,57 +88,24 @@ public partial class ModEditWindow
|
|||
public void Export(string outputPath, Utf8GamePath mdlPath)
|
||||
{
|
||||
SklbFile? sklb = null;
|
||||
try {
|
||||
var sklbPath = GetSklbPath(mdlPath.ToString());
|
||||
try
|
||||
{
|
||||
var sklbPath = _edit._models.ResolveSklbForMdl(mdlPath.ToString());
|
||||
sklb = sklbPath != null ? ReadSklb(sklbPath) : null;
|
||||
} catch (Exception exception) {
|
||||
}
|
||||
catch (Exception exception)
|
||||
{
|
||||
IoException = exception?.ToString();
|
||||
return;
|
||||
}
|
||||
|
||||
PendingIo = true;
|
||||
_edit._models.ExportToGltf(Mdl, sklb, outputPath)
|
||||
.ContinueWith(task => {
|
||||
IoException = task.Exception?.ToString();
|
||||
PendingIo = false;
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary> Try to find the .sklb path for a .mdl file. </summary>
|
||||
/// <param name="mdlPath"> .mdl file to look up the skeleton for. </param>
|
||||
private string? GetSklbPath(string mdlPath)
|
||||
{
|
||||
// Equipment is skinned to the base body skeleton of the race they target.
|
||||
var match = CharaEquipmentRegex().Match(mdlPath);
|
||||
if (match.Success)
|
||||
{
|
||||
var race = match.Groups["Race"].Value;
|
||||
return $"chara/human/{race}/skeleton/base/b0001/skl_{race}b0001.sklb";
|
||||
}
|
||||
|
||||
// Some parts of human have their own skeletons.
|
||||
match = CharaHumanRegex().Match(mdlPath);
|
||||
if (match.Success)
|
||||
{
|
||||
var type = match.Groups["Type"].Value;
|
||||
var race = match.Groups["Race"].Value;
|
||||
return type switch
|
||||
.ContinueWith(task =>
|
||||
{
|
||||
"body" or "tail" => $"chara/human/{race}/skeleton/base/b0001/skl_{race}b0001.sklb",
|
||||
_ => throw new Exception($"Currently unsupported human model type \"{type}\"."),
|
||||
};
|
||||
}
|
||||
|
||||
// A few subcategories - such as weapons, demihumans, and monsters - have dedicated per-"body" skeletons.
|
||||
match = CharaBodyRegex().Match(mdlPath);
|
||||
if (match.Success)
|
||||
{
|
||||
var subCategory = match.Groups["SubCategory"].Value;
|
||||
var set = match.Groups["Set"].Value;
|
||||
return $"chara/{subCategory}/{set}/skeleton/base/b0001/skl_{set}b0001.sklb";
|
||||
}
|
||||
|
||||
return null;
|
||||
IoException = task.Exception?.ToString();
|
||||
PendingIo = false;
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary> Read a .sklb from the active collection or game. </summary>
|
||||
|
|
@ -149,18 +115,14 @@ public partial class ModEditWindow
|
|||
// TODO: if cross-collection lookups are turned off, this conversion can be skipped
|
||||
if (!Utf8GamePath.FromString(sklbPath, out var utf8SklbPath, true))
|
||||
throw new Exception($"Resolved skeleton path {sklbPath} could not be converted to a game path.");
|
||||
|
||||
var resolvedPath = _edit._activeCollections.Current.ResolvePath(utf8SklbPath);
|
||||
// TODO: is it worth trying to use streams for these instead? i'll need to do this for mtrl/tex too, so might be a good idea. that said, the mtrl reader doesn't accept streams, so...
|
||||
var bytes = resolvedPath switch
|
||||
{
|
||||
null => _edit._gameData.GetFile(sklbPath)?.Data,
|
||||
FullPath path => File.ReadAllBytes(path.ToPath()),
|
||||
};
|
||||
if (bytes == null)
|
||||
throw new Exception($"Resolved skeleton path {sklbPath} could not be found. If modded, is it enabled in the current collection?");
|
||||
|
||||
return new SklbFile(bytes);
|
||||
var resolvedPath = _edit._activeCollections.Current.ResolvePath(utf8SklbPath);
|
||||
// TODO: is it worth trying to use streams for these instead? I'll need to do this for mtrl/tex too, so might be a good idea. that said, the mtrl reader doesn't accept streams, so...
|
||||
var bytes = resolvedPath == null ? _edit._gameData.GetFile(sklbPath)?.Data : File.ReadAllBytes(resolvedPath.Value.ToPath());
|
||||
return bytes != null
|
||||
? new SklbFile(bytes)
|
||||
: throw new Exception(
|
||||
$"Resolved skeleton path {sklbPath} could not be found. If modded, is it enabled in the current collection?");
|
||||
}
|
||||
|
||||
/// <summary> Remove the material given by the index. </summary>
|
||||
|
|
|
|||
|
|
@ -15,11 +15,12 @@ public partial class ModEditWindow
|
|||
private const int MdlMaterialMaximum = 4;
|
||||
|
||||
private readonly FileEditor<MdlTab> _modelTab;
|
||||
|
||||
private readonly ModelManager _models;
|
||||
|
||||
private string _modelNewMaterial = string.Empty;
|
||||
private readonly List<TagButtons> _subMeshAttributeTagWidgets = [];
|
||||
private string _customPath = string.Empty;
|
||||
private Utf8GamePath _customGamePath = Utf8GamePath.Empty;
|
||||
|
||||
private bool DrawModelPanel(MdlTab tab, bool disabled)
|
||||
{
|
||||
|
|
@ -57,10 +58,6 @@ public partial class ModEditWindow
|
|||
|
||||
private void DrawExport(MdlTab tab, bool disabled)
|
||||
{
|
||||
// IO on a disabled panel doesn't really make sense.
|
||||
if (disabled)
|
||||
return;
|
||||
|
||||
if (!ImGui.CollapsingHeader("Export"))
|
||||
return;
|
||||
|
||||
|
|
@ -76,16 +73,14 @@ public partial class ModEditWindow
|
|||
|
||||
DrawGamePathCombo(tab);
|
||||
|
||||
if (ImGuiUtil.DrawDisabledButton("Export to glTF", Vector2.Zero, "Exports this mdl file to glTF, for use in 3D authoring applications.", tab.PendingIo))
|
||||
{
|
||||
var gamePath = tab.GamePaths[tab.GamePathIndex];
|
||||
|
||||
_fileDialog.OpenSavePicker(
|
||||
"Save model as glTF.",
|
||||
".gltf",
|
||||
Path.GetFileNameWithoutExtension(gamePath.Filename().ToString()),
|
||||
".gltf",
|
||||
(valid, path) => {
|
||||
var gamePath = tab.GamePathIndex >= 0 && tab.GamePathIndex < tab.GamePaths.Count
|
||||
? tab.GamePaths[tab.GamePathIndex]
|
||||
: _customGamePath;
|
||||
if (ImGuiUtil.DrawDisabledButton("Export to glTF", Vector2.Zero, "Exports this mdl file to glTF, for use in 3D authoring applications.",
|
||||
tab.PendingIo || gamePath.IsEmpty))
|
||||
_fileDialog.OpenSavePicker("Save model as glTF.", ".gltf", Path.GetFileNameWithoutExtension(gamePath.Filename().ToString()),
|
||||
".gltf", (valid, path) =>
|
||||
{
|
||||
if (!valid)
|
||||
return;
|
||||
|
||||
|
|
@ -94,27 +89,62 @@ public partial class ModEditWindow
|
|||
_mod!.ModPath.FullName,
|
||||
false
|
||||
);
|
||||
}
|
||||
|
||||
if (tab.IoException != null)
|
||||
ImGuiUtil.TextWrapped(tab.IoException);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
private void DrawGamePathCombo(MdlTab tab)
|
||||
{
|
||||
using var combo = ImRaii.Combo("Game Path", tab.GamePaths![tab.GamePathIndex].ToString());
|
||||
if (!combo)
|
||||
if (tab.GamePaths!.Count != 0)
|
||||
{
|
||||
DrawComboButton(tab);
|
||||
return;
|
||||
}
|
||||
|
||||
ImGui.TextUnformatted("No associated game path detected. Valid game paths are currently necessary for exporting.");
|
||||
if (!ImGui.InputTextWithHint("##customInput", "Enter custom game path...", ref _customPath, 256))
|
||||
return;
|
||||
|
||||
foreach (var (path, index) in tab.GamePaths.WithIndex())
|
||||
{
|
||||
if (!ImGui.Selectable(path.ToString(), index == tab.GamePathIndex))
|
||||
continue;
|
||||
if (!Utf8GamePath.FromString(_customPath, out _customGamePath, false))
|
||||
_customGamePath = Utf8GamePath.Empty;
|
||||
}
|
||||
|
||||
tab.GamePathIndex = index;
|
||||
/// <summary> I disliked the combo with only one selection so turn it into a button in that case. </summary>
|
||||
private static void DrawComboButton(MdlTab tab)
|
||||
{
|
||||
const string label = "Game Path";
|
||||
var preview = tab.GamePaths![tab.GamePathIndex].ToString();
|
||||
var labelWidth = ImGui.CalcTextSize(label).X + ImGui.GetStyle().ItemInnerSpacing.X;
|
||||
var buttonWidth = ImGui.GetContentRegionAvail().X - labelWidth;
|
||||
if (tab.GamePaths!.Count == 1)
|
||||
{
|
||||
using var style = ImRaii.PushStyle(ImGuiStyleVar.ButtonTextAlign, new Vector2(0, 0.5f));
|
||||
using var color = ImRaii.PushColor(ImGuiCol.Button, ImGui.GetColorU32(ImGuiCol.FrameBg))
|
||||
.Push(ImGuiCol.ButtonHovered, ImGui.GetColorU32(ImGuiCol.FrameBgHovered))
|
||||
.Push(ImGuiCol.ButtonActive, ImGui.GetColorU32(ImGuiCol.FrameBgActive));
|
||||
using var group = ImRaii.Group();
|
||||
ImGui.Button(preview, new Vector2(buttonWidth, 0));
|
||||
ImGui.SameLine(0, ImGui.GetStyle().ItemInnerSpacing.X);
|
||||
ImGui.TextUnformatted("Game Path");
|
||||
}
|
||||
else
|
||||
{
|
||||
ImGui.SetNextItemWidth(buttonWidth);
|
||||
using var combo = ImRaii.Combo("Game Path", preview);
|
||||
if (combo.Success)
|
||||
foreach (var (path, index) in tab.GamePaths.WithIndex())
|
||||
{
|
||||
if (!ImGui.Selectable(path.ToString(), index == tab.GamePathIndex))
|
||||
continue;
|
||||
|
||||
tab.GamePathIndex = index;
|
||||
}
|
||||
}
|
||||
|
||||
if (ImGui.IsItemClicked(ImGuiMouseButton.Right))
|
||||
ImGui.SetClipboardText(preview);
|
||||
ImGuiUtil.HoverTooltip("Right-Click to copy to clipboard.", ImGuiHoveredFlags.AllowWhenDisabled);
|
||||
}
|
||||
|
||||
private bool DrawModelMaterialDetails(MdlTab tab, bool disabled)
|
||||
|
|
|
|||
|
|
@ -6,7 +6,6 @@ using OtterGui.Raii;
|
|||
using OtterGui;
|
||||
using OtterGui.Classes;
|
||||
using Penumbra.GameData;
|
||||
using Penumbra.GameData.Data;
|
||||
using Penumbra.GameData.Files;
|
||||
using Penumbra.GameData.Interop;
|
||||
using Penumbra.String;
|
||||
|
|
@ -43,8 +42,6 @@ public partial class ModEditWindow
|
|||
ImGui.Dummy(new Vector2(ImGui.GetTextLineHeight() / 2));
|
||||
DrawOtherShaderPackageDetails(file);
|
||||
|
||||
file.FileDialog.Draw();
|
||||
|
||||
ret |= file.Shpk.IsChanged();
|
||||
|
||||
return !disabled && ret;
|
||||
|
|
|
|||
|
|
@ -14,8 +14,8 @@ using Penumbra.GameData.Enums;
|
|||
using Penumbra.GameData.Files;
|
||||
using Penumbra.Import.Models;
|
||||
using Penumbra.Import.Textures;
|
||||
using Penumbra.Interop.Hooks;
|
||||
using Penumbra.Interop.ResourceTree;
|
||||
using Penumbra.Interop.Services;
|
||||
using Penumbra.Meta;
|
||||
using Penumbra.Mods;
|
||||
using Penumbra.Mods.Editor;
|
||||
|
|
@ -33,20 +33,20 @@ public partial class ModEditWindow : Window, IDisposable
|
|||
{
|
||||
private const string WindowBaseLabel = "###SubModEdit";
|
||||
|
||||
private readonly PerformanceTracker _performance;
|
||||
private readonly ModEditor _editor;
|
||||
private readonly Configuration _config;
|
||||
private readonly ItemSwapTab _itemSwapTab;
|
||||
private readonly MetaFileManager _metaFileManager;
|
||||
private readonly ActiveCollections _activeCollections;
|
||||
private readonly StainService _stainService;
|
||||
private readonly ModMergeTab _modMergeTab;
|
||||
private readonly CommunicatorService _communicator;
|
||||
private readonly IDragDropManager _dragDropManager;
|
||||
private readonly GameEventManager _gameEvents;
|
||||
private readonly IDataManager _gameData;
|
||||
private readonly IFramework _framework;
|
||||
private readonly IObjectTable _objects;
|
||||
private readonly PerformanceTracker _performance;
|
||||
private readonly ModEditor _editor;
|
||||
private readonly Configuration _config;
|
||||
private readonly ItemSwapTab _itemSwapTab;
|
||||
private readonly MetaFileManager _metaFileManager;
|
||||
private readonly ActiveCollections _activeCollections;
|
||||
private readonly StainService _stainService;
|
||||
private readonly ModMergeTab _modMergeTab;
|
||||
private readonly CommunicatorService _communicator;
|
||||
private readonly IDragDropManager _dragDropManager;
|
||||
private readonly IDataManager _gameData;
|
||||
private readonly IFramework _framework;
|
||||
private readonly IObjectTable _objects;
|
||||
private readonly CharacterBaseDestructor _characterBaseDestructor;
|
||||
|
||||
private Mod? _mod;
|
||||
private Vector2 _iconSize = Vector2.Zero;
|
||||
|
|
@ -145,12 +145,20 @@ public partial class ModEditWindow : Window, IDisposable
|
|||
_materialTab.Reset();
|
||||
_modelTab.Reset();
|
||||
_shaderPackageTab.Reset();
|
||||
_config.Ephemeral.AdvancedEditingOpen = false;
|
||||
_config.Ephemeral.Save();
|
||||
}
|
||||
|
||||
public override void Draw()
|
||||
{
|
||||
using var performance = _performance.Measure(PerformanceType.UiAdvancedWindow);
|
||||
|
||||
if (!_config.Ephemeral.AdvancedEditingOpen)
|
||||
{
|
||||
_config.Ephemeral.AdvancedEditingOpen = true;
|
||||
_config.Ephemeral.Save();
|
||||
}
|
||||
|
||||
using var tabBar = ImRaii.TabBar("##tabs");
|
||||
if (!tabBar)
|
||||
return;
|
||||
|
|
@ -566,46 +574,49 @@ public partial class ModEditWindow : Window, IDisposable
|
|||
public ModEditWindow(PerformanceTracker performance, FileDialogService fileDialog, ItemSwapTab itemSwapTab, IDataManager gameData,
|
||||
Configuration config, ModEditor editor, ResourceTreeFactory resourceTreeFactory, MetaFileManager metaFileManager,
|
||||
StainService stainService, ActiveCollections activeCollections, ModMergeTab modMergeTab,
|
||||
CommunicatorService communicator, TextureManager textures, ModelManager models, IDragDropManager dragDropManager, GameEventManager gameEvents,
|
||||
ChangedItemDrawer changedItemDrawer, IObjectTable objects, IFramework framework)
|
||||
CommunicatorService communicator, TextureManager textures, ModelManager models, IDragDropManager dragDropManager,
|
||||
ChangedItemDrawer changedItemDrawer, IObjectTable objects, IFramework framework, CharacterBaseDestructor characterBaseDestructor)
|
||||
: base(WindowBaseLabel)
|
||||
{
|
||||
_performance = performance;
|
||||
_itemSwapTab = itemSwapTab;
|
||||
_gameData = gameData;
|
||||
_config = config;
|
||||
_editor = editor;
|
||||
_metaFileManager = metaFileManager;
|
||||
_stainService = stainService;
|
||||
_activeCollections = activeCollections;
|
||||
_modMergeTab = modMergeTab;
|
||||
_communicator = communicator;
|
||||
_dragDropManager = dragDropManager;
|
||||
_textures = textures;
|
||||
_models = models;
|
||||
_fileDialog = fileDialog;
|
||||
_gameEvents = gameEvents;
|
||||
_objects = objects;
|
||||
_framework = framework;
|
||||
_performance = performance;
|
||||
_itemSwapTab = itemSwapTab;
|
||||
_gameData = gameData;
|
||||
_config = config;
|
||||
_editor = editor;
|
||||
_metaFileManager = metaFileManager;
|
||||
_stainService = stainService;
|
||||
_activeCollections = activeCollections;
|
||||
_modMergeTab = modMergeTab;
|
||||
_communicator = communicator;
|
||||
_dragDropManager = dragDropManager;
|
||||
_textures = textures;
|
||||
_models = models;
|
||||
_fileDialog = fileDialog;
|
||||
_objects = objects;
|
||||
_framework = framework;
|
||||
_characterBaseDestructor = characterBaseDestructor;
|
||||
_materialTab = new FileEditor<MtrlTab>(this, gameData, config, _editor.Compactor, _fileDialog, "Materials", ".mtrl",
|
||||
() => PopulateIsOnPlayer(_editor.Files.Mtrl, ResourceType.Mtrl), DrawMaterialPanel, () => _mod?.ModPath.FullName ?? string.Empty,
|
||||
(bytes, path, writable) => new MtrlTab(this, new MtrlFile(bytes), path, writable));
|
||||
_modelTab = new FileEditor<MdlTab>(this, gameData, config, _editor.Compactor, _fileDialog, "Models", ".mdl",
|
||||
() => PopulateIsOnPlayer(_editor.Files.Mdl, ResourceType.Mdl), DrawModelPanel, () => _mod?.ModPath.FullName ?? string.Empty, (bytes, path, _) => new MdlTab(this, bytes, path, _mod));
|
||||
() => PopulateIsOnPlayer(_editor.Files.Mdl, ResourceType.Mdl), DrawModelPanel, () => _mod?.ModPath.FullName ?? string.Empty,
|
||||
(bytes, path, _) => new MdlTab(this, bytes, path, _mod));
|
||||
_shaderPackageTab = new FileEditor<ShpkTab>(this, gameData, config, _editor.Compactor, _fileDialog, "Shaders", ".shpk",
|
||||
() => PopulateIsOnPlayer(_editor.Files.Shpk, ResourceType.Shpk), DrawShaderPackagePanel, () => _mod?.ModPath.FullName ?? string.Empty,
|
||||
() => PopulateIsOnPlayer(_editor.Files.Shpk, ResourceType.Shpk), DrawShaderPackagePanel,
|
||||
() => _mod?.ModPath.FullName ?? string.Empty,
|
||||
(bytes, _, _) => new ShpkTab(_fileDialog, bytes));
|
||||
_center = new CombinedTexture(_left, _right);
|
||||
_textureSelectCombo = new TextureDrawer.PathSelectCombo(textures, editor, () => GetPlayerResourcesOfType(ResourceType.Tex));
|
||||
_resourceTreeFactory = resourceTreeFactory;
|
||||
_quickImportViewer =
|
||||
new ResourceTreeViewer(_config, resourceTreeFactory, changedItemDrawer, 2, OnQuickImportRefresh, DrawQuickImportActions);
|
||||
_communicator.ModPathChanged.Subscribe(OnModPathChanged, ModPathChanged.Priority.ModEditWindow);
|
||||
_communicator.ModPathChanged.Subscribe(OnModPathChange, ModPathChanged.Priority.ModEditWindow);
|
||||
IsOpen = _config is { OpenWindowAtStart: true, Ephemeral.AdvancedEditingOpen: true };
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
_communicator.ModPathChanged.Unsubscribe(OnModPathChanged);
|
||||
_communicator.ModPathChanged.Unsubscribe(OnModPathChange);
|
||||
_editor?.Dispose();
|
||||
_materialTab.Dispose();
|
||||
_modelTab.Dispose();
|
||||
|
|
@ -615,7 +626,7 @@ public partial class ModEditWindow : Window, IDisposable
|
|||
_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)
|
||||
ChangeMod(mod);
|
||||
|
|
|
|||
|
|
@ -39,8 +39,7 @@ public sealed class ModFileSystemSelector : FileSystemSelector<Mod, ModFileSyste
|
|||
|
||||
public ModFileSystemSelector(IKeyState keyState, CommunicatorService communicator, ModFileSystem fileSystem, ModManager modManager,
|
||||
CollectionManager collectionManager, Configuration config, TutorialService tutorial, FileDialogService fileDialog,
|
||||
MessageService messager,
|
||||
ModImportManager modImportManager, IDragDropManager dragDrop)
|
||||
MessageService messager, ModImportManager modImportManager, IDragDropManager dragDrop)
|
||||
: base(fileSystem, keyState, Penumbra.Log, HandleException, allowMultipleSelection: true)
|
||||
{
|
||||
_communicator = communicator;
|
||||
|
|
@ -77,7 +76,15 @@ public sealed class ModFileSystemSelector : FileSystemSelector<Mod, ModFileSyste
|
|||
// @formatter:on
|
||||
SetFilterTooltip();
|
||||
|
||||
SelectionChanged += OnSelectionChange;
|
||||
SelectionChanged += OnSelectionChange;
|
||||
if (_config.Ephemeral.LastModPath.Length > 0)
|
||||
{
|
||||
var mod = _modManager.FirstOrDefault(m
|
||||
=> string.Equals(m.Identifier, _config.Ephemeral.LastModPath, StringComparison.OrdinalIgnoreCase));
|
||||
if (mod != null)
|
||||
SelectByValue(mod);
|
||||
}
|
||||
|
||||
_communicator.CollectionChange.Subscribe(OnCollectionChange, CollectionChange.Priority.ModFileSystemSelector);
|
||||
_communicator.ModSettingChanged.Subscribe(OnSettingChange, ModSettingChanged.Priority.ModFileSystemSelector);
|
||||
_communicator.CollectionInheritanceChanged.Subscribe(OnInheritanceChange, CollectionInheritanceChanged.Priority.ModFileSystemSelector);
|
||||
|
|
@ -87,15 +94,15 @@ public sealed class ModFileSystemSelector : FileSystemSelector<Mod, ModFileSyste
|
|||
OnCollectionChange(CollectionType.Current, null, _collectionManager.Active.Current, "");
|
||||
}
|
||||
|
||||
private static readonly string[] ValidModExtensions = new[]
|
||||
{
|
||||
private static readonly string[] ValidModExtensions =
|
||||
[
|
||||
".ttmp",
|
||||
".ttmp2",
|
||||
".pmp",
|
||||
".zip",
|
||||
".rar",
|
||||
".7z",
|
||||
};
|
||||
];
|
||||
|
||||
public new void Draw(float width)
|
||||
{
|
||||
|
|
@ -476,6 +483,13 @@ public sealed class ModFileSystemSelector : FileSystemSelector<Mod, ModFileSyste
|
|||
(var settings, SelectedSettingCollection) = _collectionManager.Active.Current[newSelection.Index];
|
||||
SelectedSettings = settings ?? ModSettings.Empty;
|
||||
}
|
||||
|
||||
var name = newSelection?.Identifier ?? string.Empty;
|
||||
if (name != _config.Ephemeral.LastModPath)
|
||||
{
|
||||
_config.Ephemeral.LastModPath = name;
|
||||
_config.Ephemeral.Save();
|
||||
}
|
||||
}
|
||||
|
||||
// Keep selections across rediscoveries if possible.
|
||||
|
|
@ -522,7 +536,7 @@ public sealed class ModFileSystemSelector : FileSystemSelector<Mod, ModFileSyste
|
|||
+ "Enter t:[string] to filter for mods set to specific tags.\n"
|
||||
+ "Enter n:[string] to filter only for mod names and no paths.\n"
|
||||
+ "Enter a:[string] to filter for mods by specific authors.\n"
|
||||
+ $"Enter s:[string] to filter for mods by the categories of the items they change (1-{ChangedItemDrawer.NumCategories+1} or partial category name).\n"
|
||||
+ $"Enter s:[string] to filter for mods by the categories of the items they change (1-{ChangedItemDrawer.NumCategories + 1} or partial category name).\n"
|
||||
+ "Use None as a placeholder value that only matches empty lists or names.";
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,7 +1,10 @@
|
|||
global using PerformanceTracker = OtterGui.Classes.PerformanceTracker<Penumbra.Util.PerformanceType>;
|
||||
using Dalamud.Plugin.Services;
|
||||
using OtterGui.Services;
|
||||
|
||||
namespace Penumbra.Util;
|
||||
|
||||
public sealed class PerformanceTracker(IFramework framework) : OtterGui.Classes.PerformanceTracker<PerformanceType>(framework), IService;
|
||||
|
||||
public enum PerformanceType
|
||||
{
|
||||
UiMainWindow,
|
||||
|
|
|
|||
|
|
@ -11,6 +11,18 @@
|
|||
"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": {
|
||||
"type": "Direct",
|
||||
"requested": "[7.0.0, )",
|
||||
|
|
@ -51,6 +63,11 @@
|
|||
"System.Text.Encoding.CodePages": "5.0.0"
|
||||
}
|
||||
},
|
||||
"Microsoft.CodeAnalysis.Analyzers": {
|
||||
"type": "Transitive",
|
||||
"resolved": "3.3.4",
|
||||
"contentHash": "AxkxcPR+rheX0SmvpLVIGLhOUXAKG56a64kV9VQZ4y9gR9ZmPXnqZvHJnmwLSwzrEP6junUF11vuc+aqo5r68g=="
|
||||
},
|
||||
"Microsoft.Extensions.DependencyInjection.Abstractions": {
|
||||
"type": "Transitive",
|
||||
"resolved": "7.0.0",
|
||||
|
|
@ -69,10 +86,23 @@
|
|||
"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": {
|
||||
"type": "Transitive",
|
||||
"resolved": "5.0.0",
|
||||
"contentHash": "ZD9TMpsmYJLrxbbmdvhwt9YEgG5WntEnZ/d1eH8JBX9LBp+Ju8BSBhUGbZMNVHHomWo2KVImJhTDl2hIgw/6MA=="
|
||||
"resolved": "6.0.0",
|
||||
"contentHash": "/iUeP3tq1S0XdNNoMz5C9twLSrM/TH+qElHkXWaPvuNOt+99G75NrV0OS2EqHx5wMN7popYjpc8oTjC1y16DLg=="
|
||||
},
|
||||
"System.Text.Encoding.CodePages": {
|
||||
"type": "Transitive",
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@
|
|||
"Description": "Runtime mod loader and manager.",
|
||||
"InternalName": "Penumbra",
|
||||
"AssemblyVersion": "0.8.3.1",
|
||||
"TestingAssemblyVersion": "0.8.3.2",
|
||||
"TestingAssemblyVersion": "0.8.3.3",
|
||||
"RepoUrl": "https://github.com/xivdev/Penumbra",
|
||||
"ApplicableVersion": "any",
|
||||
"DalamudApiLevel": 9,
|
||||
|
|
@ -17,7 +17,7 @@
|
|||
"LoadRequiredState": 2,
|
||||
"LoadSync": true,
|
||||
"DownloadLinkInstall": "https://github.com/xivdev/Penumbra/releases/download/0.8.3.1/Penumbra.zip",
|
||||
"DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/testing_0.8.3.2/Penumbra.zip",
|
||||
"DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/testing_0.8.3.3/Penumbra.zip",
|
||||
"DownloadLinkUpdate": "https://github.com/xivdev/Penumbra/releases/download/0.8.3.1/Penumbra.zip",
|
||||
"IconUrl": "https://raw.githubusercontent.com/xivdev/Penumbra/master/images/icon.png"
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue