diff --git a/Dalamud/Game/Addon/Lifecycle/AddonLifecycle.cs b/Dalamud/Game/Addon/Lifecycle/AddonLifecycle.cs index 6520ee4cf..2e5439ed0 100644 --- a/Dalamud/Game/Addon/Lifecycle/AddonLifecycle.cs +++ b/Dalamud/Game/Addon/Lifecycle/AddonLifecycle.cs @@ -94,6 +94,8 @@ internal unsafe class AddonLifecycle : IInternalDisposableService /// The listener to unregister. internal void UnregisterListener(AddonLifecycleEventListener listener) { + listener.IsRequestedToClear = true; + if (this.isInvokingListeners) { this.framework.RunOnTick(() => this.UnregisterListenerMethod(listener)); @@ -122,6 +124,8 @@ internal unsafe class AddonLifecycle : IInternalDisposableService { foreach (var listener in globalListeners) { + if (listener.IsRequestedToClear) continue; + try { listener.FunctionDelegate.Invoke(eventType, args); @@ -138,6 +142,8 @@ internal unsafe class AddonLifecycle : IInternalDisposableService { foreach (var listener in addonListener) { + if (listener.IsRequestedToClear) continue; + try { listener.FunctionDelegate.Invoke(eventType, args); diff --git a/Dalamud/Game/Addon/Lifecycle/AddonLifecycleEventListener.cs b/Dalamud/Game/Addon/Lifecycle/AddonLifecycleEventListener.cs index fc82e0582..38c081e65 100644 --- a/Dalamud/Game/Addon/Lifecycle/AddonLifecycleEventListener.cs +++ b/Dalamud/Game/Addon/Lifecycle/AddonLifecycleEventListener.cs @@ -35,4 +35,9 @@ internal class AddonLifecycleEventListener /// Gets the delegate this listener invokes. /// public IAddonLifecycle.AddonEventDelegate FunctionDelegate { get; init; } + + /// + /// Gets or sets if the listener is requested to be cleared. + /// + internal bool IsRequestedToClear { get; set; } } diff --git a/Dalamud/Game/Agent/AgentLifecycle.cs b/Dalamud/Game/Agent/AgentLifecycle.cs index 45f0dec5c..1c895f9da 100644 --- a/Dalamud/Game/Agent/AgentLifecycle.cs +++ b/Dalamud/Game/Agent/AgentLifecycle.cs @@ -107,6 +107,8 @@ internal unsafe class AgentLifecycle : IInternalDisposableService /// The listener to unregister. internal void UnregisterListener(AgentLifecycleEventListener listener) { + listener.IsRequestedToClear = true; + if (this.isInvokingListeners) { this.framework.RunOnTick(() => this.UnregisterListenerMethod(listener)); @@ -135,6 +137,8 @@ internal unsafe class AgentLifecycle : IInternalDisposableService { foreach (var listener in globalListeners) { + if (listener.IsRequestedToClear) continue; + try { listener.FunctionDelegate.Invoke(eventType, args); @@ -151,6 +155,8 @@ internal unsafe class AgentLifecycle : IInternalDisposableService { foreach (var listener in agentListener) { + if (listener.IsRequestedToClear) continue; + try { listener.FunctionDelegate.Invoke(eventType, args); diff --git a/Dalamud/Game/Agent/AgentLifecycleEventListener.cs b/Dalamud/Game/Agent/AgentLifecycleEventListener.cs index 91f8aa3d3..592c126ba 100644 --- a/Dalamud/Game/Agent/AgentLifecycleEventListener.cs +++ b/Dalamud/Game/Agent/AgentLifecycleEventListener.cs @@ -35,4 +35,9 @@ public class AgentLifecycleEventListener /// Gets the delegate this listener invokes. /// public IAgentLifecycle.AgentEventDelegate FunctionDelegate { get; init; } + + /// + /// Gets or sets if the listener is requested to be cleared. + /// + internal bool IsRequestedToClear { get; set; } } diff --git a/Dalamud/Game/ClientState/Customize/CustomizeData.cs b/Dalamud/Game/ClientState/Customize/CustomizeData.cs new file mode 100644 index 000000000..baf8d3a0a --- /dev/null +++ b/Dalamud/Game/ClientState/Customize/CustomizeData.cs @@ -0,0 +1,311 @@ +using Dalamud.Game.ClientState.Objects.Types; + +namespace Dalamud.Game.ClientState.Customize; + +/// +/// This collection represents customization data a has. +/// +public interface ICustomizeData +{ + /// + /// Gets the current race. + /// E.g., Miqo'te, Aura. + /// + public byte Race { get; } + + /// + /// Gets the current sex. + /// + public byte Sex { get; } + + /// + /// Gets the current body type. + /// + public byte BodyType { get; } + + /// + /// Gets the current height (0 to 100). + /// + public byte Height { get; } + + /// + /// Gets the current tribe. + /// E.g., Seeker of the Sun, Keeper of the Moon. + /// + public byte Tribe { get; } + + /// + /// Gets the current face (1 to 4). + /// + public byte Face { get; } + + /// + /// Gets the current hairstyle. + /// + public byte Hairstyle { get; } + + /// + /// Gets the current skin color. + /// + public byte SkinColor { get; } + + /// + /// Gets the current color of the left eye. + /// + public byte EyeColorLeft { get; } + + /// + /// Gets the current color of the right eye. + /// + public byte EyeColorRight { get; } + + /// + /// Gets the current main hair color. + /// + public byte HairColor { get; } + + /// + /// Gets the current highlight hair color. + /// + public byte HighlightsColor { get; } + + /// + /// Gets the current tattoo color. + /// + public byte TattooColor { get; } + + /// + /// Gets the current eyebrow type. + /// + public byte Eyebrows { get; } + + /// + /// Gets the current nose type. + /// + public byte Nose { get; } + + /// + /// Gets the current jaw type. + /// + public byte Jaw { get; } + + /// + /// Gets the current lip color fur pattern. + /// + public byte LipColorFurPattern { get; } + + /// + /// Gets the current muscle mass value. + /// + public byte MuscleMass { get; } + + /// + /// Gets the current tail type (1 to 4). + /// + public byte TailShape { get; } + + /// + /// Gets the current bust size (0 to 100). + /// + public byte BustSize { get; } + + /// + /// Gets the current color of the face paint. + /// + public byte FacePaintColor { get; } + + /// + /// Gets a value indicating whether highlight color is used. + /// + public bool Highlights { get; } + + /// + /// Gets a value indicating whether this facial feature is used. + /// + public bool FacialFeature1 { get; } + + /// + public bool FacialFeature2 { get; } + + /// + public bool FacialFeature3 { get; } + + /// + public bool FacialFeature4 { get; } + + /// + public bool FacialFeature5 { get; } + + /// + public bool FacialFeature6 { get; } + + /// + public bool FacialFeature7 { get; } + + /// + /// Gets a value indicating whether the legacy tattoo is used. + /// + public bool LegacyTattoo { get; } + + /// + /// Gets the current eye shape type. + /// + public byte EyeShape { get; } + + /// + /// Gets a value indicating whether small iris is used. + /// + public bool SmallIris { get; } + + /// + /// Gets the current mouth type. + /// + public byte Mouth { get; } + + /// + /// Gets a value indicating whether lipstick is used. + /// + public bool Lipstick { get; } + + /// + /// Gets the current face paint type. + /// + public byte FacePaint { get; } + + /// + /// Gets a value indicating whether face paint reversed is used. + /// + public bool FacePaintReversed { get; } +} + +/// +internal readonly unsafe struct CustomizeData : ICustomizeData +{ + /// + /// Gets or sets the address of the customize data struct in memory. + /// + public readonly nint Address; + + /// + /// Initializes a new instance of the struct. + /// + /// Address of the status list. + internal CustomizeData(nint address) + { + this.Address = address; + } + + /// + public byte Race => this.Struct->Race; + + /// + public byte Sex => this.Struct->Sex; + + /// + public byte BodyType => this.Struct->BodyType; + + /// + public byte Height => this.Struct->Height; + + /// + public byte Tribe => this.Struct->Tribe; + + /// + public byte Face => this.Struct->Face; + + /// + public byte Hairstyle => this.Struct->Hairstyle; + + /// + public byte SkinColor => this.Struct->SkinColor; + + /// + public byte EyeColorLeft => this.Struct->EyeColorLeft; + + /// + public byte EyeColorRight => this.Struct->EyeColorRight; + + /// + public byte HairColor => this.Struct->HairColor; + + /// + public byte HighlightsColor => this.Struct->HighlightsColor; + + /// + public byte TattooColor => this.Struct->TattooColor; + + /// + public byte Eyebrows => this.Struct->Eyebrows; + + /// + public byte Nose => this.Struct->Nose; + + /// + public byte Jaw => this.Struct->Jaw; + + /// + public byte LipColorFurPattern => this.Struct->LipColorFurPattern; + + /// + public byte MuscleMass => this.Struct->MuscleMass; + + /// + public byte TailShape => this.Struct->TailShape; + + /// + public byte BustSize => this.Struct->BustSize; + + /// + public byte FacePaintColor => this.Struct->FacePaintColor; + + /// + public bool Highlights => this.Struct->Highlights; + + /// + public bool FacialFeature1 => this.Struct->FacialFeature1; + + /// + public bool FacialFeature2 => this.Struct->FacialFeature2; + + /// + public bool FacialFeature3 => this.Struct->FacialFeature3; + + /// + public bool FacialFeature4 => this.Struct->FacialFeature4; + + /// + public bool FacialFeature5 => this.Struct->FacialFeature5; + + /// + public bool FacialFeature6 => this.Struct->FacialFeature6; + + /// + public bool FacialFeature7 => this.Struct->FacialFeature7; + + /// + public bool LegacyTattoo => this.Struct->LegacyTattoo; + + /// + public byte EyeShape => this.Struct->EyeShape; + + /// + public bool SmallIris => this.Struct->SmallIris; + + /// + public byte Mouth => this.Struct->Mouth; + + /// + public bool Lipstick => this.Struct->Lipstick; + + /// + public byte FacePaint => this.Struct->FacePaint; + + /// + public bool FacePaintReversed => this.Struct->FacePaintReversed; + + /// + /// Gets the underlying structure. + /// + internal FFXIVClientStructs.FFXIV.Client.Game.Character.CustomizeData* Struct => + (FFXIVClientStructs.FFXIV.Client.Game.Character.CustomizeData*)this.Address; +} diff --git a/Dalamud/Game/ClientState/Objects/Types/Character.cs b/Dalamud/Game/ClientState/Objects/Types/Character.cs index 2002a16b8..f122f1f27 100644 --- a/Dalamud/Game/ClientState/Objects/Types/Character.cs +++ b/Dalamud/Game/ClientState/Objects/Types/Character.cs @@ -1,6 +1,8 @@ using Dalamud.Data; +using Dalamud.Game.ClientState.Customize; using Dalamud.Game.ClientState.Objects.Enums; using Dalamud.Game.Text.SeStringHandling; +using Dalamud.Utility; using Lumina.Excel; using Lumina.Excel.Sheets; @@ -13,68 +15,73 @@ namespace Dalamud.Game.ClientState.Objects.Types; public interface ICharacter : IGameObject { /// - /// Gets the current HP of this Chara. + /// Gets the current HP of this character. /// public uint CurrentHp { get; } /// - /// Gets the maximum HP of this Chara. + /// Gets the maximum HP of this character. /// public uint MaxHp { get; } /// - /// Gets the current MP of this Chara. + /// Gets the current MP of this character. /// public uint CurrentMp { get; } /// - /// Gets the maximum MP of this Chara. + /// Gets the maximum MP of this character. /// public uint MaxMp { get; } /// - /// Gets the current GP of this Chara. + /// Gets the current GP of this character. /// public uint CurrentGp { get; } /// - /// Gets the maximum GP of this Chara. + /// Gets the maximum GP of this character. /// public uint MaxGp { get; } /// - /// Gets the current CP of this Chara. + /// Gets the current CP of this character. /// public uint CurrentCp { get; } /// - /// Gets the maximum CP of this Chara. + /// Gets the maximum CP of this character. /// public uint MaxCp { get; } /// - /// Gets the shield percentage of this Chara. + /// Gets the shield percentage of this character. /// public byte ShieldPercentage { get; } /// - /// Gets the ClassJob of this Chara. + /// Gets the ClassJob of this character. /// public RowRef ClassJob { get; } /// - /// Gets the level of this Chara. + /// Gets the level of this character. /// public byte Level { get; } /// - /// Gets a byte array describing the visual appearance of this Chara. + /// Gets a byte array describing the visual appearance of this character. /// Indexed by . /// public byte[] Customize { get; } /// - /// Gets the Free Company tag of this chara. + /// Gets the underlying CustomizeData struct for this character. + /// + public ICustomizeData CustomizeData { get; } + + /// + /// Gets the Free Company tag of this character. /// public SeString CompanyTag { get; } @@ -92,12 +99,12 @@ public interface ICharacter : IGameObject /// Gets the status flags. /// public StatusFlags StatusFlags { get; } - + /// /// Gets the current mount for this character. Will be null if the character doesn't have a mount. /// public RowRef? CurrentMount { get; } - + /// /// Gets the current minion summoned for this character. Will be null if the character doesn't have a minion. /// This method *will* return information about a spawned (but invisible) minion, e.g. if the character is riding a @@ -116,7 +123,7 @@ internal unsafe class Character : GameObject, ICharacter /// This represents a non-static entity. /// /// The address of this character in memory. - internal Character(IntPtr address) + internal Character(nint address) : base(address) { } @@ -155,8 +162,12 @@ internal unsafe class Character : GameObject, ICharacter public byte Level => this.Struct->CharacterData.Level; /// + [Api15ToDo("Do not allocate on each call, use the CS Span and let consumers do allocation if necessary")] public byte[] Customize => this.Struct->DrawData.CustomizeData.Data.ToArray(); + /// + public ICustomizeData CustomizeData => new CustomizeData((nint)(&this.Struct->DrawData.CustomizeData)); + /// public SeString CompanyTag => SeString.Parse(this.Struct->FreeCompanyTag); @@ -183,14 +194,14 @@ internal unsafe class Character : GameObject, ICharacter (this.Struct->IsAllianceMember ? StatusFlags.AllianceMember : StatusFlags.None) | (this.Struct->IsFriend ? StatusFlags.Friend : StatusFlags.None) | (this.Struct->IsCasting ? StatusFlags.IsCasting : StatusFlags.None); - + /// public RowRef? CurrentMount { get { if (this.Struct->IsNotMounted()) return null; // just for safety. - + var mountId = this.Struct->Mount.MountId; return mountId == 0 ? null : LuminaUtils.CreateRef(mountId); } @@ -201,7 +212,7 @@ internal unsafe class Character : GameObject, ICharacter { get { - if (this.Struct->CompanionObject != null) + if (this.Struct->CompanionObject != null) return LuminaUtils.CreateRef(this.Struct->CompanionObject->BaseId); // this is only present if a minion is summoned but hidden (e.g. the player's on a mount). diff --git a/Dalamud/Game/UnlockState/UnlockState.cs b/Dalamud/Game/UnlockState/UnlockState.cs index 5ccd7fadb..4b83e114a 100644 --- a/Dalamud/Game/UnlockState/UnlockState.cs +++ b/Dalamud/Game/UnlockState/UnlockState.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using Dalamud.Data; using Dalamud.Game.Gui; +using Dalamud.Hooking; using Dalamud.IoC; using Dalamud.IoC.Internal; using Dalamud.Logging.Internal; @@ -16,7 +17,9 @@ using FFXIVClientStructs.FFXIV.Component.Exd; using Lumina.Excel; using Lumina.Excel.Sheets; +using AchievementSheet = Lumina.Excel.Sheets.Achievement; using ActionSheet = Lumina.Excel.Sheets.Action; +using CSAchievement = FFXIVClientStructs.FFXIV.Client.Game.UI.Achievement; using InstanceContentSheet = Lumina.Excel.Sheets.InstanceContent; using PublicContentSheet = Lumina.Excel.Sheets.PublicContent; @@ -30,8 +33,6 @@ internal unsafe class UnlockState : IInternalDisposableService, IUnlockState { private static readonly ModuleLog Log = new(nameof(UnlockState)); - private readonly ConcurrentDictionary> cachedUnlockedRowIds = []; - [ServiceManager.ServiceDependency] private readonly DataManager dataManager = Service.Get(); @@ -44,17 +45,38 @@ internal unsafe class UnlockState : IInternalDisposableService, IUnlockState [ServiceManager.ServiceDependency] private readonly RecipeData recipeData = Service.Get(); + private readonly ConcurrentDictionary> cachedUnlockedRowIds = []; + private readonly Hook setAchievementCompletedHook; + private readonly Hook setTitleUnlockedHook; + [ServiceManager.ServiceConstructor] private UnlockState() { this.clientState.Login += this.OnLogin; this.clientState.Logout += this.OnLogout; this.gameGui.AgentUpdate += this.OnAgentUpdate; + + this.setAchievementCompletedHook = Hook.FromAddress( + (nint)CSAchievement.MemberFunctionPointers.SetAchievementCompleted, + this.SetAchievementCompletedDetour); + + this.setTitleUnlockedHook = Hook.FromAddress( + (nint)TitleList.MemberFunctionPointers.SetTitleUnlocked, + this.SetTitleUnlockedDetour); + + this.setAchievementCompletedHook.Enable(); + this.setTitleUnlockedHook.Enable(); } /// public event IUnlockState.UnlockDelegate Unlock; + /// + public bool IsAchievementListLoaded => CSAchievement.Instance()->IsLoaded(); + + /// + public bool IsTitleListLoaded => UIState.Instance()->TitleList.DataReceived; + private bool IsLoaded => PlayerState.Instance()->IsLoaded; /// @@ -63,6 +85,21 @@ internal unsafe class UnlockState : IInternalDisposableService, IUnlockState this.clientState.Login -= this.OnLogin; this.clientState.Logout -= this.OnLogout; this.gameGui.AgentUpdate -= this.OnAgentUpdate; + + this.setAchievementCompletedHook.Dispose(); + } + + /// + public bool IsAchievementComplete(AchievementSheet row) + { + // Only check for login state here as individual Achievements + // may be flagged as complete when you unlock them, regardless + // of whether the full Achievements list was loaded or not. + + if (!this.IsLoaded) + return false; + + return CSAchievement.Instance()->IsComplete((int)row.RowId); } /// @@ -71,6 +108,15 @@ internal unsafe class UnlockState : IInternalDisposableService, IUnlockState return this.IsUnlockLinkUnlocked(row.UnlockLink.RowId); } + /// + public bool IsAdventureComplete(Adventure row) + { + if (!this.IsLoaded) + return false; + + return PlayerState.Instance()->IsAdventureComplete(row.RowId - 0x210000); + } + /// public bool IsAetherCurrentUnlocked(AetherCurrent row) { @@ -415,6 +461,19 @@ internal unsafe class UnlockState : IInternalDisposableService, IUnlockState return PlayerState.Instance()->IsSecretRecipeBookUnlocked(row.RowId); } + /// + public bool IsTitleUnlocked(Title row) + { + // Only check for login state here as individual Titles + // may be flagged as complete when you unlock them, regardless + // of whether the full Titles list was loaded or not. + + if (!this.IsLoaded) + return false; + + return UIState.Instance()->TitleList.IsTitleUnlocked((ushort)row.RowId); + } + /// public bool IsTraitUnlocked(Trait row) { @@ -464,9 +523,15 @@ internal unsafe class UnlockState : IInternalDisposableService, IUnlockState if (!this.IsLoaded || rowRef.IsUntyped) return false; + if (rowRef.TryGetValue(out var achievementRow)) + return this.IsAchievementComplete(achievementRow); + if (rowRef.TryGetValue(out var actionRow)) return this.IsActionUnlocked(actionRow); + if (rowRef.TryGetValue(out var adventureRow)) + return this.IsAdventureComplete(adventureRow); + if (rowRef.TryGetValue(out var aetherCurrentRow)) return this.IsAetherCurrentUnlocked(aetherCurrentRow); @@ -572,6 +637,9 @@ internal unsafe class UnlockState : IInternalDisposableService, IUnlockState if (rowRef.TryGetValue(out var secretRecipeBookRow)) return this.IsSecretRecipeBookUnlocked(secretRecipeBookRow); + if (rowRef.TryGetValue(out var titleRow)) + return this.IsTitleUnlocked(titleRow); + if (rowRef.TryGetValue<Trait>(out var traitRow)) return this.IsTraitUnlocked(traitRow); @@ -621,6 +689,26 @@ internal unsafe class UnlockState : IInternalDisposableService, IUnlockState this.Update(); } + private void SetAchievementCompletedDetour(CSAchievement* thisPtr, uint id) + { + this.setAchievementCompletedHook.Original(thisPtr, id); + + if (!this.IsLoaded) + return; + + this.RaiseUnlockSafely((RowRef)LuminaUtils.CreateRef<AchievementSheet>(id)); + } + + private void SetTitleUnlockedDetour(TitleList* thisPtr, ushort id) + { + this.setTitleUnlockedHook.Original(thisPtr, id); + + if (!this.IsLoaded) + return; + + this.RaiseUnlockSafely((RowRef)LuminaUtils.CreateRef<Title>(id)); + } + private void Update() { if (!this.IsLoaded) @@ -628,7 +716,10 @@ internal unsafe class UnlockState : IInternalDisposableService, IUnlockState Log.Verbose("Checking for new unlocks..."); + // Do not check for Achievements or Titles here! + this.UpdateUnlocksForSheet<ActionSheet>(); + this.UpdateUnlocksForSheet<Adventure>(); this.UpdateUnlocksForSheet<AetherCurrent>(); this.UpdateUnlocksForSheet<AetherCurrentCompFlgSet>(); this.UpdateUnlocksForSheet<AozAction>(); @@ -675,7 +766,6 @@ internal unsafe class UnlockState : IInternalDisposableService, IUnlockState // For some other day: // - FishingSpot // - Spearfishing - // - Adventure (Sightseeing) // - MinerFolkloreTome // - BotanistFolkloreTome // - FishingFolkloreTome @@ -688,8 +778,6 @@ internal unsafe class UnlockState : IInternalDisposableService, IUnlockState // - EmjCostume // Probably not happening, because it requires fetching data from server: - // - Achievements - // - Titles // - Bozjan Field Notes // - Support/Phantom Jobs, which require to be in Occult Crescent, because it checks the jobs level for != 0 } @@ -712,16 +800,21 @@ internal unsafe class UnlockState : IInternalDisposableService, IUnlockState // Log.Verbose($"Unlock detected: {typeof(T).Name}#{row.RowId}"); - foreach (var action in Delegate.EnumerateInvocationList(this.Unlock)) + this.RaiseUnlockSafely((RowRef)rowRef); + } + } + + private void RaiseUnlockSafely(RowRef rowRef) + { + foreach (var action in Delegate.EnumerateInvocationList(this.Unlock)) + { + try { - try - { - action((RowRef)rowRef); - } - catch (Exception ex) - { - Log.Error(ex, "Exception during raise of {handler}", action.Method); - } + action(rowRef); + } + catch (Exception ex) + { + Log.Error(ex, "Exception during raise of {handler}", action.Method); } } } @@ -751,9 +844,21 @@ internal class UnlockStatePluginScoped : IInternalDisposableService, IUnlockStat /// <inheritdoc/> public event IUnlockState.UnlockDelegate? Unlock; + /// <inheritdoc/> + public bool IsAchievementListLoaded => this.unlockStateService.IsAchievementListLoaded; + + /// <inheritdoc/> + public bool IsTitleListLoaded => this.unlockStateService.IsTitleListLoaded; + + /// <inheritdoc/> + public bool IsAchievementComplete(AchievementSheet row) => this.unlockStateService.IsAchievementComplete(row); + /// <inheritdoc/> public bool IsActionUnlocked(ActionSheet row) => this.unlockStateService.IsActionUnlocked(row); + /// <inheritdoc/> + public bool IsAdventureComplete(Adventure row) => this.unlockStateService.IsAdventureComplete(row); + /// <inheritdoc/> public bool IsAetherCurrentCompFlgSetUnlocked(AetherCurrentCompFlgSet row) => this.unlockStateService.IsAetherCurrentCompFlgSetUnlocked(row); @@ -874,6 +979,9 @@ internal class UnlockStatePluginScoped : IInternalDisposableService, IUnlockStat /// <inheritdoc/> public bool IsSecretRecipeBookUnlocked(SecretRecipeBook row) => this.unlockStateService.IsSecretRecipeBookUnlocked(row); + /// <inheritdoc/> + public bool IsTitleUnlocked(Title row) => this.unlockStateService.IsTitleUnlocked(row); + /// <inheritdoc/> public bool IsTraitUnlocked(Trait row) => this.unlockStateService.IsTraitUnlocked(row); diff --git a/Dalamud/Hooking/Hook.cs b/Dalamud/Hooking/Hook.cs index c54a8f399..265b118bc 100644 --- a/Dalamud/Hooking/Hook.cs +++ b/Dalamud/Hooking/Hook.cs @@ -6,6 +6,8 @@ using Dalamud.Configuration.Internal; using Dalamud.Hooking.Internal; using Dalamud.Hooking.Internal.Verification; +using TerraFX.Interop.Windows; + namespace Dalamud.Hooking; /// <summary> @@ -20,6 +22,8 @@ public abstract class Hook<T> : IDalamudHook where T : Delegate private const ulong IMAGE_ORDINAL_FLAG64 = 0x8000000000000000; // ReSharper disable once InconsistentNaming private const uint IMAGE_ORDINAL_FLAG32 = 0x80000000; + // ReSharper disable once InconsistentNaming + private const int IMAGE_DIRECTORY_ENTRY_IMPORT = 1; #pragma warning restore SA1310 private readonly IntPtr address; @@ -124,25 +128,25 @@ public abstract class Hook<T> : IDalamudHook where T : Delegate module ??= Process.GetCurrentProcess().MainModule; if (module == null) throw new InvalidOperationException("Current module is null?"); - var pDos = (PeHeader.IMAGE_DOS_HEADER*)module.BaseAddress; - var pNt = (PeHeader.IMAGE_FILE_HEADER*)(module.BaseAddress + (int)pDos->e_lfanew + 4); - var isPe64 = pNt->SizeOfOptionalHeader == Marshal.SizeOf<PeHeader.IMAGE_OPTIONAL_HEADER64>(); - PeHeader.IMAGE_DATA_DIRECTORY* pDataDirectory; + var pDos = (IMAGE_DOS_HEADER*)module.BaseAddress; + var pNt = (IMAGE_FILE_HEADER*)(module.BaseAddress + pDos->e_lfanew + 4); + var isPe64 = pNt->SizeOfOptionalHeader == Marshal.SizeOf<IMAGE_OPTIONAL_HEADER64>(); + IMAGE_DATA_DIRECTORY* pDataDirectory; if (isPe64) { - var pOpt = (PeHeader.IMAGE_OPTIONAL_HEADER64*)(module.BaseAddress + (int)pDos->e_lfanew + 4 + Marshal.SizeOf<PeHeader.IMAGE_FILE_HEADER>()); - pDataDirectory = &pOpt->ImportTable; + var pOpt = (IMAGE_OPTIONAL_HEADER64*)(module.BaseAddress + pDos->e_lfanew + 4 + Marshal.SizeOf<IMAGE_FILE_HEADER>()); + pDataDirectory = &pOpt->DataDirectory[IMAGE_DIRECTORY_ENTRY_IMPORT]; } else { - var pOpt = (PeHeader.IMAGE_OPTIONAL_HEADER32*)(module.BaseAddress + (int)pDos->e_lfanew + 4 + Marshal.SizeOf<PeHeader.IMAGE_FILE_HEADER>()); - pDataDirectory = &pOpt->ImportTable; + var pOpt = (IMAGE_OPTIONAL_HEADER32*)(module.BaseAddress + pDos->e_lfanew + 4 + Marshal.SizeOf<IMAGE_FILE_HEADER>()); + pDataDirectory = &pOpt->DataDirectory[IMAGE_DIRECTORY_ENTRY_IMPORT]; } var moduleNameLowerWithNullTerminator = (moduleName + "\0").ToLowerInvariant(); - foreach (ref var importDescriptor in new Span<PeHeader.IMAGE_IMPORT_DESCRIPTOR>( - (PeHeader.IMAGE_IMPORT_DESCRIPTOR*)(module.BaseAddress + (int)pDataDirectory->VirtualAddress), - (int)(pDataDirectory->Size / Marshal.SizeOf<PeHeader.IMAGE_IMPORT_DESCRIPTOR>()))) + foreach (ref var importDescriptor in new Span<IMAGE_IMPORT_DESCRIPTOR>( + (IMAGE_IMPORT_DESCRIPTOR*)(module.BaseAddress + (int)pDataDirectory->VirtualAddress), + (int)(pDataDirectory->Size / Marshal.SizeOf<IMAGE_IMPORT_DESCRIPTOR>()))) { // Having all zero values signals the end of the table. We didn't find anything. if (importDescriptor.Characteristics == 0) @@ -248,7 +252,7 @@ public abstract class Hook<T> : IDalamudHook where T : Delegate ObjectDisposedException.ThrowIf(this.IsDisposed, this); } - private static unsafe IntPtr FromImportHelper32(IntPtr baseAddress, ref PeHeader.IMAGE_IMPORT_DESCRIPTOR desc, ref PeHeader.IMAGE_DATA_DIRECTORY dir, string functionName, uint hintOrOrdinal) + private static unsafe IntPtr FromImportHelper32(IntPtr baseAddress, ref IMAGE_IMPORT_DESCRIPTOR desc, ref IMAGE_DATA_DIRECTORY dir, string functionName, uint hintOrOrdinal) { var importLookupsOversizedSpan = new Span<uint>((uint*)(baseAddress + (int)desc.OriginalFirstThunk), (int)((dir.Size - desc.OriginalFirstThunk) / Marshal.SizeOf<int>())); var importAddressesOversizedSpan = new Span<uint>((uint*)(baseAddress + (int)desc.FirstThunk), (int)((dir.Size - desc.FirstThunk) / Marshal.SizeOf<int>())); @@ -298,7 +302,7 @@ public abstract class Hook<T> : IDalamudHook where T : Delegate throw new MissingMethodException("Specified method not found"); } - private static unsafe IntPtr FromImportHelper64(IntPtr baseAddress, ref PeHeader.IMAGE_IMPORT_DESCRIPTOR desc, ref PeHeader.IMAGE_DATA_DIRECTORY dir, string functionName, uint hintOrOrdinal) + private static unsafe IntPtr FromImportHelper64(IntPtr baseAddress, ref IMAGE_IMPORT_DESCRIPTOR desc, ref IMAGE_DATA_DIRECTORY dir, string functionName, uint hintOrOrdinal) { var importLookupsOversizedSpan = new Span<ulong>((ulong*)(baseAddress + (int)desc.OriginalFirstThunk), (int)((dir.Size - desc.OriginalFirstThunk) / Marshal.SizeOf<ulong>())); var importAddressesOversizedSpan = new Span<ulong>((ulong*)(baseAddress + (int)desc.FirstThunk), (int)((dir.Size - desc.FirstThunk) / Marshal.SizeOf<ulong>())); diff --git a/Dalamud/Hooking/Internal/PeHeader.cs b/Dalamud/Hooking/Internal/PeHeader.cs deleted file mode 100644 index 51df4a174..000000000 --- a/Dalamud/Hooking/Internal/PeHeader.cs +++ /dev/null @@ -1,390 +0,0 @@ -using System.Runtime.InteropServices; - -#pragma warning disable -namespace Dalamud.Hooking.Internal; - -internal class PeHeader -{ - public struct IMAGE_DOS_HEADER - { - public UInt16 e_magic; - public UInt16 e_cblp; - public UInt16 e_cp; - public UInt16 e_crlc; - public UInt16 e_cparhdr; - public UInt16 e_minalloc; - public UInt16 e_maxalloc; - public UInt16 e_ss; - public UInt16 e_sp; - public UInt16 e_csum; - public UInt16 e_ip; - public UInt16 e_cs; - public UInt16 e_lfarlc; - public UInt16 e_ovno; - public UInt16 e_res_0; - public UInt16 e_res_1; - public UInt16 e_res_2; - public UInt16 e_res_3; - public UInt16 e_oemid; - public UInt16 e_oeminfo; - public UInt16 e_res2_0; - public UInt16 e_res2_1; - public UInt16 e_res2_2; - public UInt16 e_res2_3; - public UInt16 e_res2_4; - public UInt16 e_res2_5; - public UInt16 e_res2_6; - public UInt16 e_res2_7; - public UInt16 e_res2_8; - public UInt16 e_res2_9; - public UInt32 e_lfanew; - } - - [StructLayout(LayoutKind.Sequential)] - public struct IMAGE_DATA_DIRECTORY - { - public UInt32 VirtualAddress; - public UInt32 Size; - } - - [StructLayout(LayoutKind.Sequential, Pack = 1)] - public struct IMAGE_OPTIONAL_HEADER32 - { - public UInt16 Magic; - public Byte MajorLinkerVersion; - public Byte MinorLinkerVersion; - public UInt32 SizeOfCode; - public UInt32 SizeOfInitializedData; - public UInt32 SizeOfUninitializedData; - public UInt32 AddressOfEntryPoint; - public UInt32 BaseOfCode; - public UInt32 BaseOfData; - public UInt32 ImageBase; - public UInt32 SectionAlignment; - public UInt32 FileAlignment; - public UInt16 MajorOperatingSystemVersion; - public UInt16 MinorOperatingSystemVersion; - public UInt16 MajorImageVersion; - public UInt16 MinorImageVersion; - public UInt16 MajorSubsystemVersion; - public UInt16 MinorSubsystemVersion; - public UInt32 Win32VersionValue; - public UInt32 SizeOfImage; - public UInt32 SizeOfHeaders; - public UInt32 CheckSum; - public UInt16 Subsystem; - public UInt16 DllCharacteristics; - public UInt32 SizeOfStackReserve; - public UInt32 SizeOfStackCommit; - public UInt32 SizeOfHeapReserve; - public UInt32 SizeOfHeapCommit; - public UInt32 LoaderFlags; - public UInt32 NumberOfRvaAndSizes; - - public IMAGE_DATA_DIRECTORY ExportTable; - public IMAGE_DATA_DIRECTORY ImportTable; - public IMAGE_DATA_DIRECTORY ResourceTable; - public IMAGE_DATA_DIRECTORY ExceptionTable; - public IMAGE_DATA_DIRECTORY CertificateTable; - public IMAGE_DATA_DIRECTORY BaseRelocationTable; - public IMAGE_DATA_DIRECTORY Debug; - public IMAGE_DATA_DIRECTORY Architecture; - public IMAGE_DATA_DIRECTORY GlobalPtr; - public IMAGE_DATA_DIRECTORY TLSTable; - public IMAGE_DATA_DIRECTORY LoadConfigTable; - public IMAGE_DATA_DIRECTORY BoundImport; - public IMAGE_DATA_DIRECTORY IAT; - public IMAGE_DATA_DIRECTORY DelayImportDescriptor; - public IMAGE_DATA_DIRECTORY CLRRuntimeHeader; - public IMAGE_DATA_DIRECTORY Reserved; - } - - [StructLayout(LayoutKind.Sequential, Pack = 1)] - public struct IMAGE_OPTIONAL_HEADER64 - { - public UInt16 Magic; - public Byte MajorLinkerVersion; - public Byte MinorLinkerVersion; - public UInt32 SizeOfCode; - public UInt32 SizeOfInitializedData; - public UInt32 SizeOfUninitializedData; - public UInt32 AddressOfEntryPoint; - public UInt32 BaseOfCode; - public UInt64 ImageBase; - public UInt32 SectionAlignment; - public UInt32 FileAlignment; - public UInt16 MajorOperatingSystemVersion; - public UInt16 MinorOperatingSystemVersion; - public UInt16 MajorImageVersion; - public UInt16 MinorImageVersion; - public UInt16 MajorSubsystemVersion; - public UInt16 MinorSubsystemVersion; - public UInt32 Win32VersionValue; - public UInt32 SizeOfImage; - public UInt32 SizeOfHeaders; - public UInt32 CheckSum; - public UInt16 Subsystem; - public UInt16 DllCharacteristics; - public UInt64 SizeOfStackReserve; - public UInt64 SizeOfStackCommit; - public UInt64 SizeOfHeapReserve; - public UInt64 SizeOfHeapCommit; - public UInt32 LoaderFlags; - public UInt32 NumberOfRvaAndSizes; - - public IMAGE_DATA_DIRECTORY ExportTable; - public IMAGE_DATA_DIRECTORY ImportTable; - public IMAGE_DATA_DIRECTORY ResourceTable; - public IMAGE_DATA_DIRECTORY ExceptionTable; - public IMAGE_DATA_DIRECTORY CertificateTable; - public IMAGE_DATA_DIRECTORY BaseRelocationTable; - public IMAGE_DATA_DIRECTORY Debug; - public IMAGE_DATA_DIRECTORY Architecture; - public IMAGE_DATA_DIRECTORY GlobalPtr; - public IMAGE_DATA_DIRECTORY TLSTable; - public IMAGE_DATA_DIRECTORY LoadConfigTable; - public IMAGE_DATA_DIRECTORY BoundImport; - public IMAGE_DATA_DIRECTORY IAT; - public IMAGE_DATA_DIRECTORY DelayImportDescriptor; - public IMAGE_DATA_DIRECTORY CLRRuntimeHeader; - public IMAGE_DATA_DIRECTORY Reserved; - } - - [StructLayout(LayoutKind.Sequential, Pack = 1)] - public struct IMAGE_FILE_HEADER - { - public UInt16 Machine; - public UInt16 NumberOfSections; - public UInt32 TimeDateStamp; - public UInt32 PointerToSymbolTable; - public UInt32 NumberOfSymbols; - public UInt16 SizeOfOptionalHeader; - public UInt16 Characteristics; - } - - [StructLayout(LayoutKind.Explicit)] - public struct IMAGE_SECTION_HEADER - { - [FieldOffset(0)] - [MarshalAs(UnmanagedType.ByValArray, SizeConst = 8)] - public char[] Name; - [FieldOffset(8)] - public UInt32 VirtualSize; - [FieldOffset(12)] - public UInt32 VirtualAddress; - [FieldOffset(16)] - public UInt32 SizeOfRawData; - [FieldOffset(20)] - public UInt32 PointerToRawData; - [FieldOffset(24)] - public UInt32 PointerToRelocations; - [FieldOffset(28)] - public UInt32 PointerToLinenumbers; - [FieldOffset(32)] - public UInt16 NumberOfRelocations; - [FieldOffset(34)] - public UInt16 NumberOfLinenumbers; - [FieldOffset(36)] - public DataSectionFlags Characteristics; - - public string Section - { - get { return new string(Name); } - } - } - - [Flags] - public enum DataSectionFlags : uint - { - /// <summary> - /// Reserved for future use. - /// </summary> - TypeReg = 0x00000000, - /// <summary> - /// Reserved for future use. - /// </summary> - TypeDsect = 0x00000001, - /// <summary> - /// Reserved for future use. - /// </summary> - TypeNoLoad = 0x00000002, - /// <summary> - /// Reserved for future use. - /// </summary> - TypeGroup = 0x00000004, - /// <summary> - /// The section should not be padded to the next boundary. This flag is obsolete and is replaced by IMAGE_SCN_ALIGN_1BYTES. This is valid only for object files. - /// </summary> - TypeNoPadded = 0x00000008, - /// <summary> - /// Reserved for future use. - /// </summary> - TypeCopy = 0x00000010, - /// <summary> - /// The section contains executable code. - /// </summary> - ContentCode = 0x00000020, - /// <summary> - /// The section contains initialized data. - /// </summary> - ContentInitializedData = 0x00000040, - /// <summary> - /// The section contains uninitialized data. - /// </summary> - ContentUninitializedData = 0x00000080, - /// <summary> - /// Reserved for future use. - /// </summary> - LinkOther = 0x00000100, - /// <summary> - /// The section contains comments or other information. The .drectve section has this type. This is valid for object files only. - /// </summary> - LinkInfo = 0x00000200, - /// <summary> - /// Reserved for future use. - /// </summary> - TypeOver = 0x00000400, - /// <summary> - /// The section will not become part of the image. This is valid only for object files. - /// </summary> - LinkRemove = 0x00000800, - /// <summary> - /// The section contains COMDAT data. For more information, see section 5.5.6, COMDAT Sections (Object Only). This is valid only for object files. - /// </summary> - LinkComDat = 0x00001000, - /// <summary> - /// Reset speculative exceptions handling bits in the TLB entries for this section. - /// </summary> - NoDeferSpecExceptions = 0x00004000, - /// <summary> - /// The section contains data referenced through the global pointer (GP). - /// </summary> - RelativeGP = 0x00008000, - /// <summary> - /// Reserved for future use. - /// </summary> - MemPurgeable = 0x00020000, - /// <summary> - /// Reserved for future use. - /// </summary> - Memory16Bit = 0x00020000, - /// <summary> - /// Reserved for future use. - /// </summary> - MemoryLocked = 0x00040000, - /// <summary> - /// Reserved for future use. - /// </summary> - MemoryPreload = 0x00080000, - /// <summary> - /// Align data on a 1-byte boundary. Valid only for object files. - /// </summary> - Align1Bytes = 0x00100000, - /// <summary> - /// Align data on a 2-byte boundary. Valid only for object files. - /// </summary> - Align2Bytes = 0x00200000, - /// <summary> - /// Align data on a 4-byte boundary. Valid only for object files. - /// </summary> - Align4Bytes = 0x00300000, - /// <summary> - /// Align data on an 8-byte boundary. Valid only for object files. - /// </summary> - Align8Bytes = 0x00400000, - /// <summary> - /// Align data on a 16-byte boundary. Valid only for object files. - /// </summary> - Align16Bytes = 0x00500000, - /// <summary> - /// Align data on a 32-byte boundary. Valid only for object files. - /// </summary> - Align32Bytes = 0x00600000, - /// <summary> - /// Align data on a 64-byte boundary. Valid only for object files. - /// </summary> - Align64Bytes = 0x00700000, - /// <summary> - /// Align data on a 128-byte boundary. Valid only for object files. - /// </summary> - Align128Bytes = 0x00800000, - /// <summary> - /// Align data on a 256-byte boundary. Valid only for object files. - /// </summary> - Align256Bytes = 0x00900000, - /// <summary> - /// Align data on a 512-byte boundary. Valid only for object files. - /// </summary> - Align512Bytes = 0x00A00000, - /// <summary> - /// Align data on a 1024-byte boundary. Valid only for object files. - /// </summary> - Align1024Bytes = 0x00B00000, - /// <summary> - /// Align data on a 2048-byte boundary. Valid only for object files. - /// </summary> - Align2048Bytes = 0x00C00000, - /// <summary> - /// Align data on a 4096-byte boundary. Valid only for object files. - /// </summary> - Align4096Bytes = 0x00D00000, - /// <summary> - /// Align data on an 8192-byte boundary. Valid only for object files. - /// </summary> - Align8192Bytes = 0x00E00000, - /// <summary> - /// The section contains extended relocations. - /// </summary> - LinkExtendedRelocationOverflow = 0x01000000, - /// <summary> - /// The section can be discarded as needed. - /// </summary> - MemoryDiscardable = 0x02000000, - /// <summary> - /// The section cannot be cached. - /// </summary> - MemoryNotCached = 0x04000000, - /// <summary> - /// The section is not pageable. - /// </summary> - MemoryNotPaged = 0x08000000, - /// <summary> - /// The section can be shared in memory. - /// </summary> - MemoryShared = 0x10000000, - /// <summary> - /// The section can be executed as code. - /// </summary> - MemoryExecute = 0x20000000, - /// <summary> - /// The section can be read. - /// </summary> - MemoryRead = 0x40000000, - /// <summary> - /// The section can be written to. - /// </summary> - MemoryWrite = 0x80000000 - } - - [StructLayout(LayoutKind.Explicit)] - public struct IMAGE_IMPORT_DESCRIPTOR - { - [FieldOffset(0)] - public uint Characteristics; - - [FieldOffset(0)] - public uint OriginalFirstThunk; - - [FieldOffset(4)] - public uint TimeDateStamp; - - [FieldOffset(8)] - public uint ForwarderChain; - - [FieldOffset(12)] - public uint Name; - - [FieldOffset(16)] - public uint FirstThunk; - } -} diff --git a/Dalamud/Hooking/Internal/Verification/HookVerifier.cs b/Dalamud/Hooking/Internal/Verification/HookVerifier.cs index ebe6851ce..98568a567 100644 --- a/Dalamud/Hooking/Internal/Verification/HookVerifier.cs +++ b/Dalamud/Hooking/Internal/Verification/HookVerifier.cs @@ -6,6 +6,8 @@ using System.Runtime.InteropServices; using Dalamud.Game; using Dalamud.Logging.Internal; +using FFXIVClientStructs.FFXIV.Application.Network; + using InteropGenerator.Runtime; namespace Dalamud.Hooking.Internal.Verification; @@ -25,7 +27,12 @@ internal static class HookVerifier "ActorControlSelf", "E8 ?? ?? ?? ?? 0F B7 0B 83 E9 64", typeof(ActorControlSelfDelegate), // TODO: change this to CS delegate - "Signature changed in Patch 7.4") // 7.4 (new parameters) + "Signature changed in Patch 7.4"), // 7.4 (new parameters) + new( + "SendPacket", + ZoneClient.Addresses.SendPacket.String, + typeof(ZoneClient.Delegates.SendPacket), + "Force marshaling context") // If people hook with 4 byte return this locks people out from logging in ]; private static readonly string ClientStructsInteropNamespacePrefix = string.Join(".", nameof(FFXIVClientStructs), nameof(FFXIVClientStructs.Interop)); @@ -67,6 +74,7 @@ internal static class HookVerifier } var passedType = typeof(T); + var isAssemblyMarshaled = passedType.Assembly.GetCustomAttribute<DisableRuntimeMarshallingAttribute>() is null; // Directly compare delegates if (passedType == entry.TargetDelegateType) @@ -78,7 +86,7 @@ internal static class HookVerifier var enforcedInvoke = entry.TargetDelegateType.GetMethod("Invoke")!; // Compare Return Type - var mismatch = !CheckParam(passedInvoke.ReturnType, enforcedInvoke.ReturnType); + var mismatch = !CheckParam(passedInvoke.ReturnType, enforcedInvoke.ReturnType, isAssemblyMarshaled); // Compare Parameter Count var passedParams = passedInvoke.GetParameters(); @@ -93,7 +101,7 @@ internal static class HookVerifier // Compare Parameter Types for (var i = 0; i < passedParams.Length; i++) { - if (!CheckParam(passedParams[i].ParameterType, enforcedParams[i].ParameterType)) + if (!CheckParam(passedParams[i].ParameterType, enforcedParams[i].ParameterType, isAssemblyMarshaled)) { mismatch = true; break; @@ -107,23 +115,23 @@ internal static class HookVerifier } } - private static bool CheckParam(Type paramLeft, Type paramRight) + private static bool CheckParam(Type paramLeft, Type paramRight, bool isMarshaled) { var sameType = paramLeft == paramRight; - return sameType || SizeOf(paramLeft) == SizeOf(paramRight); + return sameType || SizeOf(paramLeft, isMarshaled) == SizeOf(paramRight, false); } - private static int SizeOf(Type type) + private static int SizeOf(Type type, bool isMarshaled) { return type switch { - _ when type == typeof(sbyte) || type == typeof(byte) || type == typeof(bool) => 1, + _ when type == typeof(sbyte) || type == typeof(byte) || (type == typeof(bool) && !isMarshaled) => 1, _ when type == typeof(char) || type == typeof(short) || type == typeof(ushort) || type == typeof(Half) => 2, - _ when type == typeof(int) || type == typeof(uint) || type == typeof(float) => 4, + _ when type == typeof(int) || type == typeof(uint) || type == typeof(float) || (type == typeof(bool) && isMarshaled) => 4, _ when type == typeof(long) || type == typeof(ulong) || type == typeof(double) || type.IsPointer || type.IsFunctionPointer || type.IsUnmanagedFunctionPointer || (type.Name == "Pointer`1" && type.Namespace.AsSpan().SequenceEqual(ClientStructsInteropNamespacePrefix)) || type == typeof(CStringPointer) => 8, - _ when type.Name.StartsWith("FixedSizeArray") => SizeOf(type.GetGenericArguments()[0]) * int.Parse(type.Name[14..type.Name.IndexOf('`')]), - _ when type.GetCustomAttribute<InlineArrayAttribute>() is { Length: var length } => SizeOf(type.GetGenericArguments()[0]) * length, + _ when type.Name.StartsWith("FixedSizeArray") => SizeOf(type.GetGenericArguments()[0], isMarshaled) * int.Parse(type.Name[14..type.Name.IndexOf('`')]), + _ when type.GetCustomAttribute<InlineArrayAttribute>() is { Length: var length } => SizeOf(type.GetGenericArguments()[0], isMarshaled) * length, _ when IsStruct(type) && !type.IsGenericType && (type.StructLayoutAttribute?.Value ?? LayoutKind.Sequential) != LayoutKind.Sequential => type.StructLayoutAttribute?.Size ?? (int?)typeof(Unsafe).GetMethod("SizeOf")?.MakeGenericMethod(type).Invoke(null, null) ?? 0, - _ when type.IsEnum => SizeOf(Enum.GetUnderlyingType(type)), + _ when type.IsEnum => SizeOf(Enum.GetUnderlyingType(type), isMarshaled), _ when type.IsGenericType => Marshal.SizeOf(Activator.CreateInstance(type)!), _ => GetSizeOf(type), }; diff --git a/Dalamud/Interface/ImGuiFileDialog/FileDialog.UI.cs b/Dalamud/Interface/ImGuiFileDialog/FileDialog.UI.cs index ef886e957..b1fc6e049 100644 --- a/Dalamud/Interface/ImGuiFileDialog/FileDialog.UI.cs +++ b/Dalamud/Interface/ImGuiFileDialog/FileDialog.UI.cs @@ -657,6 +657,7 @@ public partial class FileDialog this.fileNameBuffer = $"{this.selectedFileNames.Count} files Selected"; } + this.SelectionChanged(this, this.GetFilePathName()); if (setLastSelection) { this.lastSelectedFileName = name; diff --git a/Dalamud/Interface/ImGuiFileDialog/FileDialog.cs b/Dalamud/Interface/ImGuiFileDialog/FileDialog.cs index e33fc2fc4..b9ac634ab 100644 --- a/Dalamud/Interface/ImGuiFileDialog/FileDialog.cs +++ b/Dalamud/Interface/ImGuiFileDialog/FileDialog.cs @@ -97,6 +97,8 @@ public partial class FileDialog this.SetupSideBar(); } + public event EventHandler<string>? SelectionChanged; + /// <summary> /// Shows the dialog. /// </summary> diff --git a/Dalamud/Interface/ImGuiFileDialog/FileDialogManager.cs b/Dalamud/Interface/ImGuiFileDialog/FileDialogManager.cs index ee12e7424..7332cd735 100644 --- a/Dalamud/Interface/ImGuiFileDialog/FileDialogManager.cs +++ b/Dalamud/Interface/ImGuiFileDialog/FileDialogManager.cs @@ -30,6 +30,12 @@ public class FileDialogManager private Action<bool, List<string>>? multiCallback; private string savedPath = "."; + /// <summary> + /// Event fires when a new file is selected by the user + /// </summary> + /// <returns>Returns the path of the file as a string</returns> + public event EventHandler<string>? SelectionChanged; + /// <summary> /// Create a dialog which selects an already existing folder. /// </summary> @@ -175,6 +181,8 @@ public class FileDialogManager this.multiCallback = null; } + private void OnSelectionChange(object sender, string path) => this.SelectionChanged?.Invoke(sender, path); + private void SetDialog( string id, string title, @@ -200,6 +208,7 @@ public class FileDialogManager if (this.dialog is not null) { this.dialog.SortOrderChanged -= this.OnSortOrderChange; + this.dialog.SelectionChanged -= this.OnSelectionChange; } this.dialog = new FileDialog(id, title, filters, path, defaultFileName, defaultExtension, selectionCountMax, isModal, flags); @@ -217,6 +226,7 @@ public class FileDialogManager } this.dialog.SortOrderChanged += this.OnSortOrderChange; + this.dialog.SelectionChanged += this.OnSelectionChange; this.dialog.WindowFlags |= this.AddedWindowFlags; foreach (var (name, location, icon, position) in this.CustomSideBarItems) this.dialog.SetQuickAccess(name, location, icon, position); diff --git a/Dalamud/Interface/Internal/Windows/Data/Widgets/DataShareWidget.cs b/Dalamud/Interface/Internal/Windows/Data/Widgets/DataShareWidget.cs index 73e9d18f8..ebb5b6581 100644 --- a/Dalamud/Interface/Internal/Windows/Data/Widgets/DataShareWidget.cs +++ b/Dalamud/Interface/Internal/Windows/Data/Widgets/DataShareWidget.cs @@ -87,7 +87,7 @@ internal class DataShareWidget : IDataWindowWidget try { var dataShare = Service<DataShare>.Get(); - var data2 = dataShare.GetData<object>(name); + var data2 = dataShare.GetData<object>(name, new DataCachePluginId("DataShareWidget", Guid.Empty)); try { data = Encoding.UTF8.GetBytes( @@ -98,7 +98,7 @@ internal class DataShareWidget : IDataWindowWidget } finally { - dataShare.RelinquishData(name); + dataShare.RelinquishData(name, new DataCachePluginId("DataShareWidget", Guid.Empty)); } } catch (Exception e) @@ -284,7 +284,7 @@ internal class DataShareWidget : IDataWindowWidget ImGui.TableSetupColumn("Shared Tag"u8); ImGui.TableSetupColumn("Show"u8); - ImGui.TableSetupColumn("Creator Assembly"u8); + ImGui.TableSetupColumn("Creator"u8); ImGui.TableSetupColumn("#"u8, ImGuiTableColumnFlags.WidthFixed, 30 * ImGuiHelpers.GlobalScale); ImGui.TableSetupColumn("Consumers"u8); ImGui.TableHeadersRow(); @@ -312,9 +312,9 @@ internal class DataShareWidget : IDataWindowWidget this.nextTab = 2 + index; } - this.DrawTextCell(share.CreatorAssembly, null, true); - this.DrawTextCell(share.Users.Length.ToString(), null, true); - this.DrawTextCell(string.Join(", ", share.Users), null, true); + this.DrawTextCell(share.CreatorPluginId.InternalName, () => share.CreatorPluginId.EffectiveWorkingId.ToString(), true); + this.DrawTextCell(share.UserPluginIds.Length.ToString(), null, true); + this.DrawTextCell(string.Join(", ", share.UserPluginIds.Select(c => c.InternalName)), () => string.Join("\n", share.UserPluginIds.Select(c => $"{c.InternalName} ({c.EffectiveWorkingId.ToString()}")), true); } } } diff --git a/Dalamud/Interface/Internal/Windows/Data/Widgets/NetworkMonitorWidget.cs b/Dalamud/Interface/Internal/Windows/Data/Widgets/NetworkMonitorWidget.cs index 4460a9f9a..73916761b 100644 --- a/Dalamud/Interface/Internal/Windows/Data/Widgets/NetworkMonitorWidget.cs +++ b/Dalamud/Interface/Internal/Windows/Data/Widgets/NetworkMonitorWidget.cs @@ -21,10 +21,11 @@ internal unsafe class NetworkMonitorWidget : IDataWindowWidget { private readonly ConcurrentQueue<NetworkPacketData> packets = new(); - private Hook<PacketDispatcher.Delegates.OnReceivePacket>? hookDown; - private Hook<ZoneClient.Delegates.SendPacket>? hookUp; + private Hook<PacketDispatcher.Delegates.OnReceivePacket>? hookZoneDown; + private Hook<ZoneClient.Delegates.SendPacket>? hookZoneUp; - private bool trackNetwork; + private bool trackZoneUp; + private bool trackZoneDown; private int trackedPackets = 20; private ulong nextPacketIndex; private string filterString = string.Empty; @@ -35,8 +36,8 @@ internal unsafe class NetworkMonitorWidget : IDataWindowWidget /// <summary> Finalizes an instance of the <see cref="NetworkMonitorWidget"/> class. </summary> ~NetworkMonitorWidget() { - this.hookDown?.Dispose(); - this.hookUp?.Dispose(); + this.hookZoneDown?.Dispose(); + this.hookZoneUp?.Dispose(); } private enum NetworkMessageDirection @@ -60,26 +61,41 @@ internal unsafe class NetworkMonitorWidget : IDataWindowWidget /// <inheritdoc/> public void Draw() { - this.hookDown ??= Hook<PacketDispatcher.Delegates.OnReceivePacket>.FromAddress( + this.hookZoneDown ??= Hook<PacketDispatcher.Delegates.OnReceivePacket>.FromAddress( (nint)PacketDispatcher.StaticVirtualTablePointer->OnReceivePacket, this.OnReceivePacketDetour); - this.hookUp ??= Hook<ZoneClient.Delegates.SendPacket>.FromAddress( + this.hookZoneUp ??= Hook<ZoneClient.Delegates.SendPacket>.FromAddress( (nint)ZoneClient.MemberFunctionPointers.SendPacket, this.SendPacketDetour); - if (ImGui.Checkbox("Track Network Packets"u8, ref this.trackNetwork)) + if (ImGui.Checkbox("Track ZoneUp"u8, ref this.trackZoneUp)) { - if (this.trackNetwork) + if (this.trackZoneUp) { - this.nextPacketIndex = 0; - this.hookDown?.Enable(); - this.hookUp?.Enable(); + if (!this.trackZoneDown) + this.nextPacketIndex = 0; + + this.hookZoneUp?.Enable(); } else { - this.hookDown?.Disable(); - this.hookUp?.Disable(); + this.hookZoneUp?.Disable(); + } + } + + if (ImGui.Checkbox("Track ZoneDown"u8, ref this.trackZoneDown)) + { + if (this.trackZoneDown) + { + if (!this.trackZoneUp) + this.nextPacketIndex = 0; + + this.hookZoneDown?.Enable(); + } + else + { + this.hookZoneDown?.Disable(); } } @@ -92,6 +108,7 @@ internal unsafe class NetworkMonitorWidget : IDataWindowWidget if (ImGui.Button("Clear Stored Packets"u8)) { this.packets.Clear(); + this.nextPacketIndex = 0; } ImGui.SameLine(); @@ -102,7 +119,7 @@ internal unsafe class NetworkMonitorWidget : IDataWindowWidget ImGui.SameLine(0, ImGui.GetStyle().ItemInnerSpacing.X); ImGui.Checkbox("##FilterRecording"u8, ref this.filterRecording); if (ImGui.IsItemHovered()) - ImGui.SetTooltip("Apply filter to incoming packets.\nUncheck to record all packets and filter the table instead."u8); + ImGui.SetTooltip("When enabled, packets are filtered before being recorded.\nWhen disabled, all packets are recorded and filtering only affects packets displayed in the table."u8); ImGui.SameLine(0, ImGui.GetStyle().ItemInnerSpacing.X); ImGuiComponents.HelpMarker("Enter OpCodes in a comma-separated list.\nRanges are supported. Exclude OpCodes with exclamation mark.\nExample: -400,!50-100,650,700-980,!941"); @@ -204,14 +221,14 @@ internal unsafe class NetworkMonitorWidget : IDataWindowWidget var opCode = *(ushort*)(packet + 2); var targetName = GetTargetName(targetId); this.RecordPacket(new NetworkPacketData(Interlocked.Increment(ref this.nextPacketIndex), DateTime.Now, opCode, NetworkMessageDirection.ZoneDown, targetId, targetName)); - this.hookDown.OriginalDisposeSafe(thisPtr, targetId, packet); + this.hookZoneDown.OriginalDisposeSafe(thisPtr, targetId, packet); } private bool SendPacketDetour(ZoneClient* thisPtr, nint packet, uint a3, uint a4, bool a5) { var opCode = *(ushort*)packet; this.RecordPacket(new NetworkPacketData(Interlocked.Increment(ref this.nextPacketIndex), DateTime.Now, opCode, NetworkMessageDirection.ZoneUp, 0, string.Empty)); - return this.hookUp.OriginalDisposeSafe(thisPtr, packet, a3, a4, a5); + return this.hookZoneUp.OriginalDisposeSafe(thisPtr, packet, a3, a4, a5); } private void RecordPacket(NetworkPacketData packet) diff --git a/Dalamud/Interface/Internal/Windows/PluginInstaller/PluginInstallerWindow.cs b/Dalamud/Interface/Internal/Windows/PluginInstaller/PluginInstallerWindow.cs index e32d31181..d132f0d8d 100644 --- a/Dalamud/Interface/Internal/Windows/PluginInstaller/PluginInstallerWindow.cs +++ b/Dalamud/Interface/Internal/Windows/PluginInstaller/PluginInstallerWindow.cs @@ -3804,7 +3804,7 @@ internal class PluginInstallerWindow : Window, IDisposable if (!manifest.Punchline.IsNullOrEmpty()) scores.Add(matcher.Matches(manifest.Punchline.ToLowerInvariant()) * 100); if (manifest.Tags != null) - scores.Add(matcher.MatchesAny(manifest.Tags.ToArray()) * 100); + scores.Add(matcher.MatchesAny(manifest.Tags.Select(tag => tag.ToLowerInvariant()).ToArray()) * 100); return scores.Max(); } diff --git a/Dalamud/Interface/Textures/Internal/BitmapCodecInfo.cs b/Dalamud/Interface/Textures/Internal/BitmapCodecInfo.cs index ec56caadd..7f5ed4fda 100644 --- a/Dalamud/Interface/Textures/Internal/BitmapCodecInfo.cs +++ b/Dalamud/Interface/Textures/Internal/BitmapCodecInfo.cs @@ -50,6 +50,6 @@ internal sealed class BitmapCodecInfo : IBitmapCodecInfo _ = readFuncPtr(codecInfo, 0, null, &cch); var buf = stackalloc char[(int)cch + 1]; Marshal.ThrowExceptionForHR(readFuncPtr(codecInfo, cch + 1, buf, &cch)); - return new(buf, 0, (int)cch); + return new string(buf, 0, (int)cch).Trim('\0'); } } diff --git a/Dalamud/Interface/Textures/Internal/SharedImmediateTextures/GamePathSharedImmediateTexture.cs b/Dalamud/Interface/Textures/Internal/SharedImmediateTextures/GamePathSharedImmediateTexture.cs index 185ae07b9..a320d921e 100644 --- a/Dalamud/Interface/Textures/Internal/SharedImmediateTextures/GamePathSharedImmediateTexture.cs +++ b/Dalamud/Interface/Textures/Internal/SharedImmediateTextures/GamePathSharedImmediateTexture.cs @@ -70,7 +70,7 @@ internal sealed class GamePathSharedImmediateTexture : SharedImmediateTexture } cancellationToken.ThrowIfCancellationRequested(); - var wrap = tm.NoThrottleCreateFromTexFile(file); + var wrap = tm.NoThrottleCreateFromTexFile(file.Header, file.TextureBuffer); tm.BlameSetName(wrap, this.ToString()); return wrap; } diff --git a/Dalamud/Interface/Textures/Internal/TextureManager.cs b/Dalamud/Interface/Textures/Internal/TextureManager.cs index 982b5c58d..22a257395 100644 --- a/Dalamud/Interface/Textures/Internal/TextureManager.cs +++ b/Dalamud/Interface/Textures/Internal/TextureManager.cs @@ -6,7 +6,6 @@ using System.Threading.Tasks; using Dalamud.Configuration.Internal; using Dalamud.Data; using Dalamud.Game; -using Dalamud.Interface.ImGuiSeStringRenderer.Internal; using Dalamud.Interface.Internal; using Dalamud.Interface.Textures.Internal.SharedImmediateTextures; using Dalamud.Interface.Textures.TextureWraps; @@ -20,6 +19,7 @@ using Dalamud.Utility.TerraFxCom; using Lumina.Data; using Lumina.Data.Files; +using Lumina.Data.Parsing.Tex.Buffers; using TerraFX.Interop.DirectX; using TerraFX.Interop.Windows; @@ -219,7 +219,7 @@ internal sealed partial class TextureManager null, _ => Task.FromResult( this.BlameSetName( - this.NoThrottleCreateFromTexFile(file), + this.NoThrottleCreateFromTexFile(file.Header, file.TextureBuffer), debugName ?? $"{nameof(this.CreateFromTexFile)}({ForceNullable(file.FilePath)?.Path})")), cancellationToken); @@ -345,14 +345,14 @@ internal sealed partial class TextureManager /// <summary>Creates a texture from the given <see cref="TexFile"/>. Skips the load throttler; intended to be used /// from implementation of <see cref="SharedImmediateTexture"/>s.</summary> - /// <param name="file">The data.</param> + /// <param name="header">Header of a <c>.tex</c> file.</param> + /// <param name="buffer">Texture buffer.</param> /// <returns>The loaded texture.</returns> - internal IDalamudTextureWrap NoThrottleCreateFromTexFile(TexFile file) + internal IDalamudTextureWrap NoThrottleCreateFromTexFile(TexFile.TexHeader header, TextureBuffer buffer) { ObjectDisposedException.ThrowIf(this.disposeCts.IsCancellationRequested, this); - var buffer = file.TextureBuffer; - var (dxgiFormat, conversion) = TexFile.GetDxgiFormatFromTextureFormat(file.Header.Format, false); + var (dxgiFormat, conversion) = TexFile.GetDxgiFormatFromTextureFormat(header.Format, false); if (conversion != TexFile.DxgiFormatConversion.NoConversion || !this.IsDxgiFormatSupported((DXGI_FORMAT)dxgiFormat)) { @@ -361,34 +361,31 @@ internal sealed partial class TextureManager } var wrap = this.NoThrottleCreateFromRaw(new(buffer.Width, buffer.Height, dxgiFormat), buffer.RawData); - this.BlameSetName(wrap, $"{nameof(this.NoThrottleCreateFromTexFile)}({ForceNullable(file.FilePath).Path})"); + this.BlameSetName(wrap, $"{nameof(this.NoThrottleCreateFromTexFile)}({header.Width} x {header.Height})"); return wrap; - - static T? ForceNullable<T>(T s) => s; } /// <summary>Creates a texture from the given <paramref name="fileBytes"/>, trying to interpret it as a /// <see cref="TexFile"/>.</summary> /// <param name="fileBytes">The file bytes.</param> /// <returns>The loaded texture.</returns> - internal IDalamudTextureWrap NoThrottleCreateFromTexFile(ReadOnlySpan<byte> fileBytes) + internal unsafe IDalamudTextureWrap NoThrottleCreateFromTexFile(ReadOnlySpan<byte> fileBytes) { ObjectDisposedException.ThrowIf(this.disposeCts.IsCancellationRequested, this); if (!TexFileExtensions.IsPossiblyTexFile2D(fileBytes)) throw new InvalidDataException("The file is not a TexFile."); - var bytesArray = fileBytes.ToArray(); - var tf = new TexFile(); - typeof(TexFile).GetProperty(nameof(tf.Data))!.GetSetMethod(true)!.Invoke( - tf, - [bytesArray]); - typeof(TexFile).GetProperty(nameof(tf.Reader))!.GetSetMethod(true)!.Invoke( - tf, - [new LuminaBinaryReader(bytesArray)]); - // Note: FileInfo and FilePath are not used from TexFile; skip it. + TexFile.TexHeader header; + TextureBuffer buffer; + fixed (byte* p = fileBytes) + { + var lbr = new LuminaBinaryReader(new UnmanagedMemoryStream(p, fileBytes.Length)); + header = lbr.ReadStructure<TexFile.TexHeader>(); + buffer = TextureBuffer.FromStream(header, lbr); + } - var wrap = this.NoThrottleCreateFromTexFile(tf); + var wrap = this.NoThrottleCreateFromTexFile(header, buffer); this.BlameSetName(wrap, $"{nameof(this.NoThrottleCreateFromTexFile)}({fileBytes.Length:n0})"); return wrap; } diff --git a/Dalamud/Plugin/DalamudPluginInterface.cs b/Dalamud/Plugin/DalamudPluginInterface.cs index df1d0f6e9..48f91b250 100644 --- a/Dalamud/Plugin/DalamudPluginInterface.cs +++ b/Dalamud/Plugin/DalamudPluginInterface.cs @@ -227,19 +227,19 @@ internal sealed class DalamudPluginInterface : IDalamudPluginInterface, IDisposa /// <inheritdoc/> public T GetOrCreateData<T>(string tag, Func<T> dataGenerator) where T : class - => Service<DataShare>.Get().GetOrCreateData(tag, dataGenerator); + => Service<DataShare>.Get().GetOrCreateData(tag, new DataCachePluginId(this.plugin.InternalName, this.plugin.EffectiveWorkingPluginId), dataGenerator); /// <inheritdoc/> public void RelinquishData(string tag) - => Service<DataShare>.Get().RelinquishData(tag); + => Service<DataShare>.Get().RelinquishData(tag, new DataCachePluginId(this.plugin.InternalName, this.plugin.EffectiveWorkingPluginId)); /// <inheritdoc/> public bool TryGetData<T>(string tag, [NotNullWhen(true)] out T? data) where T : class - => Service<DataShare>.Get().TryGetData(tag, out data); + => Service<DataShare>.Get().TryGetData(tag, new DataCachePluginId(this.plugin.InternalName, this.plugin.EffectiveWorkingPluginId), out data); /// <inheritdoc/> public T? GetData<T>(string tag) where T : class - => Service<DataShare>.Get().GetData<T>(tag); + => Service<DataShare>.Get().GetData<T>(tag, new DataCachePluginId(this.plugin.InternalName, this.plugin.EffectiveWorkingPluginId)); /// <inheritdoc/> public ICallGateProvider<TRet> GetIpcProvider<TRet>(string name) diff --git a/Dalamud/Plugin/Ipc/Exceptions/DataCacheCreationError.cs b/Dalamud/Plugin/Ipc/Exceptions/DataCacheCreationError.cs index db095bad9..38b729616 100644 --- a/Dalamud/Plugin/Ipc/Exceptions/DataCacheCreationError.cs +++ b/Dalamud/Plugin/Ipc/Exceptions/DataCacheCreationError.cs @@ -1,3 +1,5 @@ +using Dalamud.Plugin.Ipc.Internal; + namespace Dalamud.Plugin.Ipc.Exceptions; /// <summary> @@ -9,11 +11,11 @@ public class DataCacheCreationError : IpcError /// Initializes a new instance of the <see cref="DataCacheCreationError"/> class. /// </summary> /// <param name="tag">Tag of the data cache.</param> - /// <param name="creator">The assembly name of the caller.</param> + /// <param name="creatorPluginId">The plugin ID of the creating plugin.</param> /// <param name="expectedType">The type expected.</param> /// <param name="ex">The thrown exception.</param> - public DataCacheCreationError(string tag, string creator, Type expectedType, Exception ex) - : base($"The creation of the {expectedType} data cache {tag} initialized by {creator} was unsuccessful.", ex) + public DataCacheCreationError(string tag, DataCachePluginId creatorPluginId, Type expectedType, Exception ex) + : base($"The creation of the {expectedType} data cache {tag} initialized by {creatorPluginId.InternalName} ({creatorPluginId.EffectiveWorkingId}) was unsuccessful.", ex) { } } diff --git a/Dalamud/Plugin/Ipc/Exceptions/DataCacheTypeMismatchError.cs b/Dalamud/Plugin/Ipc/Exceptions/DataCacheTypeMismatchError.cs index e5d9cc4db..bfe09b120 100644 --- a/Dalamud/Plugin/Ipc/Exceptions/DataCacheTypeMismatchError.cs +++ b/Dalamud/Plugin/Ipc/Exceptions/DataCacheTypeMismatchError.cs @@ -1,3 +1,5 @@ +using Dalamud.Plugin.Ipc.Internal; + namespace Dalamud.Plugin.Ipc.Exceptions; /// <summary> @@ -9,11 +11,11 @@ public class DataCacheTypeMismatchError : IpcError /// Initializes a new instance of the <see cref="DataCacheTypeMismatchError"/> class. /// </summary> /// <param name="tag">Tag of the data cache.</param> - /// <param name="creator">Assembly name of the plugin creating the cache.</param> + /// <param name="creatorPluginId">The plugin ID of the creating plugin.</param> /// <param name="requestedType">The requested type.</param> /// <param name="actualType">The stored type.</param> - public DataCacheTypeMismatchError(string tag, string creator, Type requestedType, Type actualType) - : base($"Data cache {tag} was requested with type {requestedType}, but {creator} created type {actualType}.") + public DataCacheTypeMismatchError(string tag, DataCachePluginId creatorPluginId, Type requestedType, Type actualType) + : base($"Data cache {tag} was requested with type {requestedType}, but {creatorPluginId.InternalName} ({creatorPluginId.EffectiveWorkingId}) created type {actualType}.") { } } diff --git a/Dalamud/Plugin/Ipc/Internal/DataCache.cs b/Dalamud/Plugin/Ipc/Internal/DataCache.cs index d565c8b35..cbb5bb342 100644 --- a/Dalamud/Plugin/Ipc/Internal/DataCache.cs +++ b/Dalamud/Plugin/Ipc/Internal/DataCache.cs @@ -1,5 +1,6 @@ using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; +using System.Linq; using System.Runtime.ExceptionServices; using Dalamud.Plugin.Ipc.Exceptions; @@ -16,12 +17,12 @@ internal readonly struct DataCache /// <summary> Name of the data. </summary> internal readonly string Tag; - /// <summary> The assembly name of the initial creator. </summary> - internal readonly string CreatorAssemblyName; + /// <summary> The creating plugin ID of this DataCache entry. </summary> + internal readonly DataCachePluginId CreatorPluginId; - /// <summary> A not-necessarily distinct list of current users. </summary> + /// <summary> A distinct list of plugin IDs that are using this data. </summary> /// <remarks> Also used as a reference count tracker. </remarks> - internal readonly List<string> UserAssemblyNames; + internal readonly List<DataCachePluginId> UserPluginIds; /// <summary> The type the data was registered as. </summary> internal readonly Type Type; @@ -33,14 +34,14 @@ internal readonly struct DataCache /// Initializes a new instance of the <see cref="DataCache"/> struct. /// </summary> /// <param name="tag">Name of the data.</param> - /// <param name="creatorAssemblyName">The assembly name of the initial creator.</param> + /// <param name="creatorPluginId">The internal name and effective working ID of the creating plugin.</param> /// <param name="data">A reference to data.</param> /// <param name="type">The type of the data.</param> - public DataCache(string tag, string creatorAssemblyName, object? data, Type type) + public DataCache(string tag, DataCachePluginId creatorPluginId, object? data, Type type) { this.Tag = tag; - this.CreatorAssemblyName = creatorAssemblyName; - this.UserAssemblyNames = []; + this.CreatorPluginId = creatorPluginId; + this.UserPluginIds = []; this.Data = data; this.Type = type; } @@ -49,40 +50,40 @@ internal readonly struct DataCache /// Creates a new instance of the <see cref="DataCache"/> struct, using the given data generator function. /// </summary> /// <param name="tag">The name for the data cache.</param> - /// <param name="creatorAssemblyName">The assembly name of the initial creator.</param> + /// <param name="creatorPluginId">The internal name and effective working ID of the creating plugin.</param> /// <param name="dataGenerator">The function that generates the data if it does not already exist.</param> /// <typeparam name="T">The type of the stored data - needs to be a reference type that is shared through Dalamud itself, not loaded by the plugin.</typeparam> /// <returns>The new instance of <see cref="DataCache"/>.</returns> - public static DataCache From<T>(string tag, string creatorAssemblyName, Func<T> dataGenerator) + public static DataCache From<T>(string tag, DataCachePluginId creatorPluginId, Func<T> dataGenerator) where T : class { try { - var result = new DataCache(tag, creatorAssemblyName, dataGenerator.Invoke(), typeof(T)); + var result = new DataCache(tag, creatorPluginId, dataGenerator.Invoke(), typeof(T)); Log.Verbose( "[{who}] Created new data for [{Tag:l}] for creator {Creator:l}.", nameof(DataShare), tag, - creatorAssemblyName); + creatorPluginId); return result; } catch (Exception e) { throw ExceptionDispatchInfo.SetCurrentStackTrace( - new DataCacheCreationError(tag, creatorAssemblyName, typeof(T), e)); + new DataCacheCreationError(tag, creatorPluginId, typeof(T), e)); } } /// <summary> /// Attempts to fetch the data. /// </summary> - /// <param name="callerName">The name of the caller assembly.</param> + /// <param name="callingPluginId">The calling plugin ID.</param> /// <param name="value">The value, if succeeded.</param> /// <param name="ex">The exception, if failed.</param> /// <typeparam name="T">Desired type of the data.</typeparam> /// <returns><c>true</c> on success.</returns> public bool TryGetData<T>( - string callerName, + DataCachePluginId callingPluginId, [NotNullWhen(true)] out T? value, [NotNullWhen(false)] out Exception? ex) where T : class @@ -98,16 +99,21 @@ internal readonly struct DataCache value = data; ex = null; - // Register the access history - lock (this.UserAssemblyNames) - this.UserAssemblyNames.Add(callerName); + // Register the access history. The effective working ID is unique per plugin and persists between reloads, so only add it once. + lock (this.UserPluginIds) + { + if (this.UserPluginIds.All(c => c.EffectiveWorkingId != callingPluginId.EffectiveWorkingId)) + { + this.UserPluginIds.Add(callingPluginId); + } + } return true; default: value = null; ex = ExceptionDispatchInfo.SetCurrentStackTrace( - new DataCacheTypeMismatchError(this.Tag, this.CreatorAssemblyName, typeof(T), this.Type)); + new DataCacheTypeMismatchError(this.Tag, this.CreatorPluginId, typeof(T), this.Type)); return false; } } diff --git a/Dalamud/Plugin/Ipc/Internal/DataCachePluginId.cs b/Dalamud/Plugin/Ipc/Internal/DataCachePluginId.cs new file mode 100644 index 000000000..c68dc7c06 --- /dev/null +++ b/Dalamud/Plugin/Ipc/Internal/DataCachePluginId.cs @@ -0,0 +1,16 @@ +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using System.Runtime.ExceptionServices; + +using Dalamud.Plugin.Ipc.Exceptions; + +using Serilog; + +namespace Dalamud.Plugin.Ipc.Internal; + +/// <summary> +/// Stores the internal name and effective working ID of a plugin accessing datashare. +/// </summary> +/// <param name="InternalName">The internal name of the plugin.</param> +/// <param name="EffectiveWorkingId">The effective working ID of the plugin.</param> +public record DataCachePluginId(string InternalName, Guid EffectiveWorkingId); diff --git a/Dalamud/Plugin/Ipc/Internal/DataShare.cs b/Dalamud/Plugin/Ipc/Internal/DataShare.cs index becbe1211..ffad4876e 100644 --- a/Dalamud/Plugin/Ipc/Internal/DataShare.cs +++ b/Dalamud/Plugin/Ipc/Internal/DataShare.cs @@ -33,24 +33,23 @@ internal class DataShare : IServiceType /// </summary> /// <typeparam name="T">The type of the stored data - needs to be a reference type that is shared through Dalamud itself, not loaded by the plugin.</typeparam> /// <param name="tag">The name for the data cache.</param> + /// <param name="callingPluginId">The ID of the calling plugin.</param> /// <param name="dataGenerator">The function that generates the data if it does not already exist.</param> /// <returns>Either the existing data for <paramref name="tag"/> or the data generated by <paramref name="dataGenerator"/>.</returns> /// <exception cref="DataCacheTypeMismatchError">Thrown if a cache for <paramref name="tag"/> exists, but contains data of a type not assignable to <typeparamref name="T>"/>.</exception> /// <exception cref="DataCacheValueNullError">Thrown if the stored data for a cache is null.</exception> /// <exception cref="DataCacheCreationError">Thrown if <paramref name="dataGenerator"/> throws an exception or returns null.</exception> - public T GetOrCreateData<T>(string tag, Func<T> dataGenerator) + public T GetOrCreateData<T>(string tag, DataCachePluginId callingPluginId, Func<T> dataGenerator) where T : class { - var callerName = GetCallerName(); - Lazy<DataCache> cacheLazy; lock (this.caches) { if (!this.caches.TryGetValue(tag, out cacheLazy)) - this.caches[tag] = cacheLazy = new(() => DataCache.From(tag, callerName, dataGenerator)); + this.caches[tag] = cacheLazy = new(() => DataCache.From(tag, callingPluginId, dataGenerator)); } - return cacheLazy.Value.TryGetData<T>(callerName, out var value, out var ex) ? value : throw ex; + return cacheLazy.Value.TryGetData<T>(callingPluginId, out var value, out var ex) ? value : throw ex; } /// <summary> @@ -58,7 +57,8 @@ internal class DataShare : IServiceType /// If no assembly uses the data anymore, the cache will be removed from the data share and if it is an IDisposable, Dispose will be called on it. /// </summary> /// <param name="tag">The name for the data cache.</param> - public void RelinquishData(string tag) + /// <param name="callingPluginId">The ID of the calling plugin.</param> + public void RelinquishData(string tag, DataCachePluginId callingPluginId) { DataCache cache; lock (this.caches) @@ -66,10 +66,8 @@ internal class DataShare : IServiceType if (!this.caches.TryGetValue(tag, out var cacheLazy)) return; - var callerName = GetCallerName(); - cache = cacheLazy.Value; - if (!cache.UserAssemblyNames.Remove(callerName) || cache.UserAssemblyNames.Count > 0) + if (!cache.UserPluginIds.Remove(callingPluginId) || cache.UserPluginIds.Count > 0) return; if (!this.caches.Remove(tag)) return; @@ -84,7 +82,7 @@ internal class DataShare : IServiceType } catch (Exception e) { - Log.Error(e, "[DataShare] Failed to dispose [{Tag:l}] after it was removed from all shares.", tag); + Log.Error(e, "[DataShare] Failed to dispose [{Tag:l}] after it was removed from all shares.", tag); } } else @@ -99,9 +97,10 @@ internal class DataShare : IServiceType /// </summary> /// <typeparam name="T">The type of the stored data - needs to be a reference type that is shared through Dalamud itself, not loaded by the plugin.</typeparam> /// <param name="tag">The name for the data cache.</param> + /// <param name="callingPluginId">The ID of the calling plugin.</param> /// <param name="data">The requested data on success, null otherwise.</param> /// <returns>True if the requested data exists and is assignable to the requested type.</returns> - public bool TryGetData<T>(string tag, [NotNullWhen(true)] out T? data) + public bool TryGetData<T>(string tag, DataCachePluginId callingPluginId, [NotNullWhen(true)] out T? data) where T : class { data = null; @@ -112,7 +111,7 @@ internal class DataShare : IServiceType return false; } - return cacheLazy.Value.TryGetData(GetCallerName(), out data, out _); + return cacheLazy.Value.TryGetData(callingPluginId, out data, out _); } /// <summary> @@ -121,11 +120,12 @@ internal class DataShare : IServiceType /// </summary> /// <typeparam name="T">The type of the stored data - needs to be a reference type that is shared through Dalamud itself, not loaded by the plugin.</typeparam> /// <param name="tag">The name for the data cache.</param> + /// <param name="callingPluginId">The ID of the calling plugin.</param> /// <returns>The requested data.</returns> /// <exception cref="KeyNotFoundException">Thrown if <paramref name="tag"/> is not registered.</exception> /// <exception cref="DataCacheTypeMismatchError">Thrown if a cache for <paramref name="tag"/> exists, but contains data of a type not assignable to <typeparamref name="T>"/>.</exception> /// <exception cref="DataCacheValueNullError">Thrown if the stored data for a cache is null.</exception> - public T GetData<T>(string tag) + public T GetData<T>(string tag, DataCachePluginId callingPluginId) where T : class { Lazy<DataCache> cacheLazy; @@ -135,35 +135,19 @@ internal class DataShare : IServiceType throw new KeyNotFoundException($"The data cache [{tag}] is not registered."); } - return cacheLazy.Value.TryGetData<T>(GetCallerName(), out var value, out var ex) ? value : throw ex; + return cacheLazy.Value.TryGetData<T>(callingPluginId, out var value, out var ex) ? value : throw ex; } /// <summary> /// Obtain a read-only list of data shares. /// </summary> /// <returns>All currently subscribed tags, their creator names and all their users.</returns> - internal IEnumerable<(string Tag, string CreatorAssembly, string[] Users)> GetAllShares() + internal IEnumerable<(string Tag, DataCachePluginId CreatorPluginId, DataCachePluginId[] UserPluginIds)> GetAllShares() { lock (this.caches) { return this.caches.Select( - kvp => (kvp.Key, kvp.Value.Value.CreatorAssemblyName, kvp.Value.Value.UserAssemblyNames.ToArray())); + kvp => (kvp.Key, kvp.Value.Value.CreatorPluginId, kvp.Value.Value.UserPluginIds.ToArray())); } } - - /// <summary> Obtain the last assembly name in the stack trace that is not a system or dalamud assembly. </summary> - private static string GetCallerName() - { - var frames = new StackTrace().GetFrames(); - foreach (var frame in frames.Reverse()) - { - var name = frame.GetMethod()?.DeclaringType?.Assembly.GetName().Name ?? "Unknown"; - if (!name.StartsWith("System") && !name.StartsWith("Dalamud")) - { - return name; - } - } - - return "Unknown"; - } } diff --git a/Dalamud/Plugin/Services/IPlayerState.cs b/Dalamud/Plugin/Services/IPlayerState.cs index 21d88010b..838d5a346 100644 --- a/Dalamud/Plugin/Services/IPlayerState.cs +++ b/Dalamud/Plugin/Services/IPlayerState.cs @@ -159,7 +159,7 @@ public interface IPlayerState : IDalamudService RowRef<Aetheryte> FreeAetheryte { get; } /// <summary> - /// Gets the amount of received player commendations of the local player. + /// Gets the amount of rested experience available to the local player. /// </summary> uint BaseRestedExperience { get; } diff --git a/Dalamud/Plugin/Services/IUnlockState.cs b/Dalamud/Plugin/Services/IUnlockState.cs index f51222ba1..4f92b2b3d 100644 --- a/Dalamud/Plugin/Services/IUnlockState.cs +++ b/Dalamud/Plugin/Services/IUnlockState.cs @@ -1,5 +1,3 @@ -using System.Diagnostics.CodeAnalysis; - using Lumina.Excel; using Lumina.Excel.Sheets; @@ -23,6 +21,24 @@ public interface IUnlockState : IDalamudService /// </summary> event UnlockDelegate? Unlock; + /// <summary> + /// Gets a value indicating whether the full Achievements list was received. + /// </summary> + bool IsAchievementListLoaded { get; } + + /// <summary> + /// Gets a value indicating whether the full Titles list was received. + /// </summary> + bool IsTitleListLoaded { get; } + + /// <summary> + /// Determines whether the specified Achievement is completed.<br/> + /// Requires that the player requested the Achievements list (can be chcked with <see cref="IsAchievementListLoaded"/>). + /// </summary> + /// <param name="row">The Achievement row to check.</param> + /// <returns><see langword="true"/> if completed; otherwise, <see langword="false"/>.</returns> + bool IsAchievementComplete(Achievement row); + /// <summary> /// Determines whether the specified Action is unlocked. /// </summary> @@ -30,6 +46,13 @@ public interface IUnlockState : IDalamudService /// <returns><see langword="true"/> if unlocked; otherwise, <see langword="false"/>.</returns> bool IsActionUnlocked(Lumina.Excel.Sheets.Action row); + /// <summary> + /// Determines whether the specified Adventure is completed. + /// </summary> + /// <param name="row">The Adventure row to check.</param> + /// <returns><see langword="true"/> if completed; otherwise, <see langword="false"/>.</returns> + public bool IsAdventureComplete(Adventure row); + /// <summary> /// Determines whether the specified AetherCurrentCompFlgSet is unlocked. /// </summary> @@ -311,6 +334,14 @@ public interface IUnlockState : IDalamudService /// <returns><see langword="true"/> if unlocked; otherwise, <see langword="false"/>.</returns> bool IsSecretRecipeBookUnlocked(SecretRecipeBook row); + /// <summary> + /// Determines whether the specified Title is unlocked.<br/> + /// Requires that the player requested the Titles list (can be chcked with <see cref="IsTitleListLoaded"/>). + /// </summary> + /// <param name="row">The Title row to check.</param> + /// <returns><see langword="true"/> if unlocked; otherwise, <see langword="false"/>.</returns> + bool IsTitleUnlocked(Title row); + /// <summary> /// Determines whether the specified Trait is unlocked. /// </summary> diff --git a/Dalamud/Support/Troubleshooting.cs b/Dalamud/Support/Troubleshooting.cs index 2dd0fb623..779754ee8 100644 --- a/Dalamud/Support/Troubleshooting.cs +++ b/Dalamud/Support/Troubleshooting.cs @@ -1,4 +1,5 @@ using System.Collections.Generic; +using System.IO; using System.Linq; using System.Text; @@ -68,6 +69,7 @@ public static class Troubleshooting { var payload = new TroubleshootingPayload { + Timestamp = DateTimeOffset.UtcNow.ToUnixTimeSeconds(), LoadedPlugins = pluginManager?.InstalledPlugins?.Select(x => x.Manifest as LocalPluginManifest)?.OrderByDescending(x => x.InternalName).ToArray(), PluginStates = pluginManager?.InstalledPlugins?.Where(x => !x.IsDev).ToDictionary(x => x.Manifest.InternalName, x => x.IsBanned ? "Banned" : x.State.ToString()), EverStartedLoadingPlugins = pluginManager?.InstalledPlugins.Where(x => x.HasEverStartedLoad).Select(x => x.InternalName).ToList(), @@ -85,6 +87,12 @@ public static class Troubleshooting var encodedPayload = Convert.ToBase64String(Encoding.UTF8.GetBytes(JsonConvert.SerializeObject(payload))); Log.Information($"TROUBLESHOOTING:{encodedPayload}"); + + File.WriteAllText( + Path.Join( + startInfo.LogPath, + "dalamud.troubleshooting.json"), + JsonConvert.SerializeObject(payload, Formatting.Indented)); } catch (Exception ex) { @@ -103,6 +111,8 @@ public static class Troubleshooting private class TroubleshootingPayload { + public long Timestamp { get; set; } + public LocalPluginManifest[]? LoadedPlugins { get; set; } public Dictionary<string, string>? PluginStates { get; set; } diff --git a/DalamudCrashHandler/DalamudCrashHandler.cpp b/DalamudCrashHandler/DalamudCrashHandler.cpp index f28715dc1..f883ba55b 100644 --- a/DalamudCrashHandler/DalamudCrashHandler.cpp +++ b/DalamudCrashHandler/DalamudCrashHandler.cpp @@ -28,6 +28,9 @@ #include <ShObjIdl.h> #include <shlobj_core.h> +#include <dxgi.h> +#pragma comment(lib, "dxgi.lib") + #pragma comment(lib, "comctl32.lib") #pragma comment(linker, "/manifestdependency:\"type='win32' name='Microsoft.Windows.Common-Controls' version='6.0.0.0' processorArchitecture='*' publicKeyToken='6595b64144ccf1df' language='*'\"") @@ -470,12 +473,39 @@ void open_folder_and_select_items(HWND hwndOpener, const std::wstring& path) { ILFree(piid); } +std::vector<IDXGIAdapter1*> enum_dxgi_adapters() +{ + std::vector<IDXGIAdapter1*> vAdapters; + + IDXGIFactory1* pFactory = NULL; + if (FAILED(CreateDXGIFactory1(__uuidof(IDXGIFactory1), (void**)&pFactory))) + { + return vAdapters; + } + + IDXGIAdapter1* pAdapter; + for (UINT i = 0; + pFactory->EnumAdapters1(i, &pAdapter) != DXGI_ERROR_NOT_FOUND; + ++i) + { + vAdapters.push_back(pAdapter); + } + + if (pFactory) + { + pFactory->Release(); + } + + return vAdapters; +} + void export_tspack(HWND hWndParent, const std::filesystem::path& logDir, const std::string& crashLog, const std::string& troubleshootingPackData) { static const char* SourceLogFiles[] = { "output.log", // XIVLauncher for Windows "launcher.log", // XIVLauncher.Core for [mostly] Linux "patcher.log", "dalamud.log", + "dalamud.troubleshooting.json", "dalamud.injector.log", "dalamud.boot.log", "aria.log", @@ -693,7 +723,7 @@ void restart_game_using_injector(int nRadioButton, const std::vector<std::wstrin void get_cpu_info(wchar_t *vendor, wchar_t *brand) { // Gotten and reformatted to not include all data as listed at https://learn.microsoft.com/en-us/cpp/intrinsics/cpuid-cpuidex?view=msvc-170#example - + // int cpuInfo[4] = {-1}; std::array<int, 4> cpui; int nIds_; @@ -1022,6 +1052,13 @@ int main() { log << std::format(L"System Time: {0:%F} {0:%T} {0:%Ez}", std::chrono::system_clock::now()) << std::endl; log << std::format(L"CPU Vendor: {}", vendor) << std::endl; log << std::format(L"CPU Brand: {}", brand) << std::endl; + + for (IDXGIAdapter1* adapter : enum_dxgi_adapters()) { + DXGI_ADAPTER_DESC1 adapterDescription{}; + adapter->GetDesc1(&adapterDescription); + log << std::format(L"GPU Desc: {}", adapterDescription.Description) << std::endl; + } + log << L"\n" << stackTrace << std::endl; if (pProgressDialog) diff --git a/lib/FFXIVClientStructs b/lib/FFXIVClientStructs index 9ba281cab..a97e9f89d 160000 --- a/lib/FFXIVClientStructs +++ b/lib/FFXIVClientStructs @@ -1 +1 @@ -Subproject commit 9ba281cab958049b47bbf7199ab14742c609cd4b +Subproject commit a97e9f89d72d40eabd0f3b52266862dca3eba872