Compare commits

...

48 commits

Author SHA1 Message Date
goat
dbe61a426e
Merge pull request #2607 from Critical-Impact/datashare
Some checks are pending
Build Dalamud / Build on Windows (push) Waiting to run
Build Dalamud / Check API Compatibility (push) Blocked by required conditions
Build Dalamud / Deploy dalamud-distrib staging (push) Blocked by required conditions
Tag Build / Tag Build (push) Successful in 4s
Use plugin internal name for DataShare tracking
2026-02-13 21:16:51 +01:00
wolfcomp
eb8e431267 Invert condition for assembly marshaling check
Some checks are pending
Build Dalamud / Build on Windows (push) Waiting to run
Build Dalamud / Check API Compatibility (push) Blocked by required conditions
Build Dalamud / Deploy dalamud-distrib staging (push) Blocked by required conditions
Tag Build / Tag Build (push) Successful in 3s
2026-02-13 21:38:37 +10:00
wolfcomp
87a0c69020 Fix missing args 2026-02-13 21:38:37 +10:00
wolfcomp
7752b0f918 Add SendPacket delegate to HookVerifier 2026-02-13 21:38:37 +10:00
wolfcomp
fd85a8d3bc Enhance HookVerifier to check marshaled types 2026-02-13 21:38:37 +10:00
AtmoOmen
f01971a7d7 fix Addon/AgentLifyCycle unreg
(cherry picked from commit 29e1715ff589b9dddf5a747b8655ea382e08cf58)
2026-02-13 20:59:39 +10:00
Haselnussbomber
1779d2681a Switch to CS in UnlockState
Some checks are pending
Build Dalamud / Build on Windows (push) Waiting to run
Build Dalamud / Check API Compatibility (push) Blocked by required conditions
Build Dalamud / Deploy dalamud-distrib staging (push) Blocked by required conditions
Tag Build / Tag Build (push) Successful in 2s
2026-02-13 20:20:10 +10:00
Haselnussbomber
4651397808 Add support for Adventures to IUnlockState 2026-02-13 20:20:10 +10:00
Haselnussbomber
1ba18e54bf Add support for Titles to IUnlockState 2026-02-13 20:20:10 +10:00
Haselnussbomber
907b585b75 Add support for Achievements to IUnlockState 2026-02-13 20:20:10 +10:00
Critical Impact
b963e83cba Use effective working ID + internal name 2026-02-13 19:04:33 +10:00
goat
ec450da054
Merge pull request #2615 from goatcorp/csupdate-master
Some checks are pending
Build Dalamud / Build on Windows (push) Waiting to run
Build Dalamud / Check API Compatibility (push) Blocked by required conditions
Build Dalamud / Deploy dalamud-distrib staging (push) Blocked by required conditions
Tag Build / Tag Build (push) Successful in 4s
[master] Update ClientStructs
2026-02-12 22:12:23 +01:00
goat
990c4fd7e8
Merge pull request #2614 from reiichi001/add_gpu_to_crashinfo
Add GPU Info to Crash Handler
2026-02-12 22:11:25 +01:00
goat
b1b99bae13
Use correct variable name 2026-02-12 21:03:38 +01:00
goat
c4faf84a2d
Merge pull request #2616 from Glorou/FileOnSelect
Add a file selection changed event to FileDialog and it's manager
2026-02-12 20:48:39 +01:00
goat
abe27891c3
Tidy tidy 2026-02-12 20:45:23 +01:00
goat
1286dbd279
Merge branch 'master' into FileOnSelect 2026-02-12 20:38:23 +01:00
goat
0f14f5dab7
Merge pull request #2618 from marzent/troubleshoot-error
Fix troubleshooting json error on non-Windows platforms
2026-02-12 20:37:34 +01:00
github-actions[bot]
49e281e573 Update ClientStructs
Some checks are pending
Build Dalamud / Build on Windows (push) Waiting to run
Build Dalamud / Check API Compatibility (push) Blocked by required conditions
Build Dalamud / Deploy dalamud-distrib staging (push) Blocked by required conditions
2026-02-12 19:10:35 +00:00
marzent
0a070970a0 Fix troubleshooting json error on non-Windows platforms 2026-02-11 13:03:32 +01:00
balloon41
3de8c511bf
Update IPlayerState.cs (#2617)
Some checks failed
Tag Build / Tag Build (push) Successful in 5s
Build Dalamud / Build on Windows (push) Has been cancelled
Build Dalamud / Check API Compatibility (push) Has been cancelled
Build Dalamud / Deploy dalamud-distrib staging (push) Has been cancelled
Fixed type in BaseRestedExperience summary
2026-02-10 19:37:58 +00:00
Glorou
e2297661f3 Added doc 2026-02-09 11:17:11 -05:00
Glorou
8285aa1014
Clean up FileDialogManager by removing unused code
Removed unused GetCurrentPath method and unnecessary using directives.
2026-02-09 10:50:46 -05:00
Glorou
256ab9dc9c add whitespace 2026-02-09 10:38:32 -05:00
Glorou
332d0d0cf5 Tweaked 2026-02-09 10:38:14 -05:00
Glorou
78912c1552 Init 2026-02-08 21:56:48 -05:00
goat
28e39ab9e2
Merge pull request #2594 from Haselnussbomber/troubleshooting-json
Some checks failed
Tag Build / Tag Build (push) Successful in 5s
Build Dalamud / Build on Windows (push) Has been cancelled
Build Dalamud / Check API Compatibility (push) Has been cancelled
Build Dalamud / Deploy dalamud-distrib staging (push) Has been cancelled
Write troubleshooting to json file
2026-02-08 12:40:08 +01:00
goat
526e651750
Merge pull request #2602 from nebel/fix-search-tag-case
Properly lowercase for tags for plugin installer search
2026-02-08 12:38:03 +01:00
goat
4a33d34a3f
Merge pull request #2605 from Haselnussbomber/remove-peheader
Replace PeHeader with TerraFX
2026-02-08 12:37:48 +01:00
goat
0aa746e3bf
Merge pull request #2611 from goatcorp/csupdate-master
[master] Update ClientStructs
2026-02-08 12:37:30 +01:00
goat
5044aeda2b
Merge pull request #2613 from Haselnussbomber/update-netmonwidget
Update Network Monitor Widget
2026-02-08 12:37:04 +01:00
github-actions[bot]
34f13b3823 Update ClientStructs
Some checks are pending
Build Dalamud / Build on Windows (push) Waiting to run
Build Dalamud / Check API Compatibility (push) Blocked by required conditions
Build Dalamud / Deploy dalamud-distrib staging (push) Blocked by required conditions
2026-02-08 06:57:53 +00:00
Robert Baker
73447f205d
Add GPU Info to Crash Handler
I'm sure there's a better way to do this, but I also shouldn't be allowed to touch any cpp code.

This loops through all dxgi adapters based on example code I ripped from Microsoft and StackOverflow and dumps that into the crash log.

I'm hoping it doesn't make the window too tall, so if there's a better way to list only the display adapters that are unique, I'm all for it.
2026-02-07 20:58:55 -08:00
Haselnussbomber
0490a71990
Update Network Monitor Widget
- Separate checkboxes for up and down tracking
- Clarify tracking is for ZoneUp/ZoneDown
- Rephrase filter checkbox tooltip
2026-02-07 20:36:20 +01:00
goat
2b347eaff9
Merge pull request #2604 from pohky/patch-1
Some checks are pending
Build Dalamud / Build on Windows (push) Waiting to run
Build Dalamud / Check API Compatibility (push) Blocked by required conditions
Build Dalamud / Deploy dalamud-distrib staging (push) Blocked by required conditions
Tag Build / Tag Build (push) Successful in 4s
Fix null characters in BitmapCodecInfo strings
2026-02-07 13:41:13 +01:00
goat
f4defb735b
Merge pull request #2610 from Infiziert90/character-api-extended
Extend the Character class with CustomizeData
2026-02-07 13:40:56 +01:00
goat
4a75fe73df
Merge pull request #2612 from Soreepeong/fix/tex-from-file
Fix loading `.tex` file from filesystem
2026-02-07 13:39:47 +01:00
Soreepeong
b30a93816b Directly work with TexHeader and TextureBuffer 2026-02-06 18:59:02 +09:00
Infi
bcf4f396d6 - Adjust comments 2026-02-05 01:12:00 +01:00
Infi
dc77235c96 - Fix style cop warnings 2026-02-05 00:37:12 +01:00
Infi
d8a13a72aa - Add the CustomizeData struct to ICharacter
- API 15 note the Customize array
2026-02-05 00:20:39 +01:00
Critical Impact
9e18b843db Change column name 2026-02-02 22:32:33 +10:00
Critical Impact
dc783e0c2b Use plugin internal name for datashare tracking 2026-02-02 16:56:31 +10:00
Haselnussbomber
252b7eeb9b
Replace PeHeader with TerraFX 2026-01-30 19:21:46 +01:00
pohky
73edaadbca
Fix null characters in BitmapCodecInfo strings 2026-01-30 14:51:29 +01:00
nebel
934df7da8a
Properly lowercase for tags for plugin installer search 2026-01-29 00:16:41 +09:00
Haselnussbomber
b9c4c97eba
Add timestamp to TroubleshootingPayload 2026-01-25 16:23:24 +01:00
Haselnussbomber
ac7c4e889a
Write troubleshooting to json file 2026-01-25 12:40:51 +01:00
30 changed files with 748 additions and 559 deletions

View file

@ -94,6 +94,8 @@ internal unsafe class AddonLifecycle : IInternalDisposableService
/// <param name="listener">The listener to unregister.</param> /// <param name="listener">The listener to unregister.</param>
internal void UnregisterListener(AddonLifecycleEventListener listener) internal void UnregisterListener(AddonLifecycleEventListener listener)
{ {
listener.IsRequestedToClear = true;
if (this.isInvokingListeners) if (this.isInvokingListeners)
{ {
this.framework.RunOnTick(() => this.UnregisterListenerMethod(listener)); this.framework.RunOnTick(() => this.UnregisterListenerMethod(listener));
@ -122,6 +124,8 @@ internal unsafe class AddonLifecycle : IInternalDisposableService
{ {
foreach (var listener in globalListeners) foreach (var listener in globalListeners)
{ {
if (listener.IsRequestedToClear) continue;
try try
{ {
listener.FunctionDelegate.Invoke(eventType, args); listener.FunctionDelegate.Invoke(eventType, args);
@ -138,6 +142,8 @@ internal unsafe class AddonLifecycle : IInternalDisposableService
{ {
foreach (var listener in addonListener) foreach (var listener in addonListener)
{ {
if (listener.IsRequestedToClear) continue;
try try
{ {
listener.FunctionDelegate.Invoke(eventType, args); listener.FunctionDelegate.Invoke(eventType, args);

View file

@ -35,4 +35,9 @@ internal class AddonLifecycleEventListener
/// Gets the delegate this listener invokes. /// Gets the delegate this listener invokes.
/// </summary> /// </summary>
public IAddonLifecycle.AddonEventDelegate FunctionDelegate { get; init; } public IAddonLifecycle.AddonEventDelegate FunctionDelegate { get; init; }
/// <summary>
/// Gets or sets if the listener is requested to be cleared.
/// </summary>
internal bool IsRequestedToClear { get; set; }
} }

View file

@ -107,6 +107,8 @@ internal unsafe class AgentLifecycle : IInternalDisposableService
/// <param name="listener">The listener to unregister.</param> /// <param name="listener">The listener to unregister.</param>
internal void UnregisterListener(AgentLifecycleEventListener listener) internal void UnregisterListener(AgentLifecycleEventListener listener)
{ {
listener.IsRequestedToClear = true;
if (this.isInvokingListeners) if (this.isInvokingListeners)
{ {
this.framework.RunOnTick(() => this.UnregisterListenerMethod(listener)); this.framework.RunOnTick(() => this.UnregisterListenerMethod(listener));
@ -135,6 +137,8 @@ internal unsafe class AgentLifecycle : IInternalDisposableService
{ {
foreach (var listener in globalListeners) foreach (var listener in globalListeners)
{ {
if (listener.IsRequestedToClear) continue;
try try
{ {
listener.FunctionDelegate.Invoke(eventType, args); listener.FunctionDelegate.Invoke(eventType, args);
@ -151,6 +155,8 @@ internal unsafe class AgentLifecycle : IInternalDisposableService
{ {
foreach (var listener in agentListener) foreach (var listener in agentListener)
{ {
if (listener.IsRequestedToClear) continue;
try try
{ {
listener.FunctionDelegate.Invoke(eventType, args); listener.FunctionDelegate.Invoke(eventType, args);

View file

@ -35,4 +35,9 @@ public class AgentLifecycleEventListener
/// Gets the delegate this listener invokes. /// Gets the delegate this listener invokes.
/// </summary> /// </summary>
public IAgentLifecycle.AgentEventDelegate FunctionDelegate { get; init; } public IAgentLifecycle.AgentEventDelegate FunctionDelegate { get; init; }
/// <summary>
/// Gets or sets if the listener is requested to be cleared.
/// </summary>
internal bool IsRequestedToClear { get; set; }
} }

View file

@ -0,0 +1,311 @@
using Dalamud.Game.ClientState.Objects.Types;
namespace Dalamud.Game.ClientState.Customize;
/// <summary>
/// This collection represents customization data a <see cref="ICharacter"/> has.
/// </summary>
public interface ICustomizeData
{
/// <summary>
/// Gets the current race.
/// E.g., Miqo'te, Aura.
/// </summary>
public byte Race { get; }
/// <summary>
/// Gets the current sex.
/// </summary>
public byte Sex { get; }
/// <summary>
/// Gets the current body type.
/// </summary>
public byte BodyType { get; }
/// <summary>
/// Gets the current height (0 to 100).
/// </summary>
public byte Height { get; }
/// <summary>
/// Gets the current tribe.
/// E.g., Seeker of the Sun, Keeper of the Moon.
/// </summary>
public byte Tribe { get; }
/// <summary>
/// Gets the current face (1 to 4).
/// </summary>
public byte Face { get; }
/// <summary>
/// Gets the current hairstyle.
/// </summary>
public byte Hairstyle { get; }
/// <summary>
/// Gets the current skin color.
/// </summary>
public byte SkinColor { get; }
/// <summary>
/// Gets the current color of the left eye.
/// </summary>
public byte EyeColorLeft { get; }
/// <summary>
/// Gets the current color of the right eye.
/// </summary>
public byte EyeColorRight { get; }
/// <summary>
/// Gets the current main hair color.
/// </summary>
public byte HairColor { get; }
/// <summary>
/// Gets the current highlight hair color.
/// </summary>
public byte HighlightsColor { get; }
/// <summary>
/// Gets the current tattoo color.
/// </summary>
public byte TattooColor { get; }
/// <summary>
/// Gets the current eyebrow type.
/// </summary>
public byte Eyebrows { get; }
/// <summary>
/// Gets the current nose type.
/// </summary>
public byte Nose { get; }
/// <summary>
/// Gets the current jaw type.
/// </summary>
public byte Jaw { get; }
/// <summary>
/// Gets the current lip color fur pattern.
/// </summary>
public byte LipColorFurPattern { get; }
/// <summary>
/// Gets the current muscle mass value.
/// </summary>
public byte MuscleMass { get; }
/// <summary>
/// Gets the current tail type (1 to 4).
/// </summary>
public byte TailShape { get; }
/// <summary>
/// Gets the current bust size (0 to 100).
/// </summary>
public byte BustSize { get; }
/// <summary>
/// Gets the current color of the face paint.
/// </summary>
public byte FacePaintColor { get; }
/// <summary>
/// Gets a value indicating whether highlight color is used.
/// </summary>
public bool Highlights { get; }
/// <summary>
/// Gets a value indicating whether this facial feature is used.
/// </summary>
public bool FacialFeature1 { get; }
/// <inheritdoc cref="FacialFeature1"/>
public bool FacialFeature2 { get; }
/// <inheritdoc cref="FacialFeature1"/>
public bool FacialFeature3 { get; }
/// <inheritdoc cref="FacialFeature1"/>
public bool FacialFeature4 { get; }
/// <inheritdoc cref="FacialFeature1"/>
public bool FacialFeature5 { get; }
/// <inheritdoc cref="FacialFeature1"/>
public bool FacialFeature6 { get; }
/// <inheritdoc cref="FacialFeature1"/>
public bool FacialFeature7 { get; }
/// <summary>
/// Gets a value indicating whether the legacy tattoo is used.
/// </summary>
public bool LegacyTattoo { get; }
/// <summary>
/// Gets the current eye shape type.
/// </summary>
public byte EyeShape { get; }
/// <summary>
/// Gets a value indicating whether small iris is used.
/// </summary>
public bool SmallIris { get; }
/// <summary>
/// Gets the current mouth type.
/// </summary>
public byte Mouth { get; }
/// <summary>
/// Gets a value indicating whether lipstick is used.
/// </summary>
public bool Lipstick { get; }
/// <summary>
/// Gets the current face paint type.
/// </summary>
public byte FacePaint { get; }
/// <summary>
/// Gets a value indicating whether face paint reversed is used.
/// </summary>
public bool FacePaintReversed { get; }
}
/// <inheritdoc/>
internal readonly unsafe struct CustomizeData : ICustomizeData
{
/// <summary>
/// Gets or sets the address of the customize data struct in memory.
/// </summary>
public readonly nint Address;
/// <summary>
/// Initializes a new instance of the <see cref="CustomizeData"/> struct.
/// </summary>
/// <param name="address">Address of the status list.</param>
internal CustomizeData(nint address)
{
this.Address = address;
}
/// <inheritdoc/>
public byte Race => this.Struct->Race;
/// <inheritdoc/>
public byte Sex => this.Struct->Sex;
/// <inheritdoc/>
public byte BodyType => this.Struct->BodyType;
/// <inheritdoc/>
public byte Height => this.Struct->Height;
/// <inheritdoc/>
public byte Tribe => this.Struct->Tribe;
/// <inheritdoc/>
public byte Face => this.Struct->Face;
/// <inheritdoc/>
public byte Hairstyle => this.Struct->Hairstyle;
/// <inheritdoc/>
public byte SkinColor => this.Struct->SkinColor;
/// <inheritdoc/>
public byte EyeColorLeft => this.Struct->EyeColorLeft;
/// <inheritdoc/>
public byte EyeColorRight => this.Struct->EyeColorRight;
/// <inheritdoc/>
public byte HairColor => this.Struct->HairColor;
/// <inheritdoc/>
public byte HighlightsColor => this.Struct->HighlightsColor;
/// <inheritdoc/>
public byte TattooColor => this.Struct->TattooColor;
/// <inheritdoc/>
public byte Eyebrows => this.Struct->Eyebrows;
/// <inheritdoc/>
public byte Nose => this.Struct->Nose;
/// <inheritdoc/>
public byte Jaw => this.Struct->Jaw;
/// <inheritdoc/>
public byte LipColorFurPattern => this.Struct->LipColorFurPattern;
/// <inheritdoc/>
public byte MuscleMass => this.Struct->MuscleMass;
/// <inheritdoc/>
public byte TailShape => this.Struct->TailShape;
/// <inheritdoc/>
public byte BustSize => this.Struct->BustSize;
/// <inheritdoc/>
public byte FacePaintColor => this.Struct->FacePaintColor;
/// <inheritdoc/>
public bool Highlights => this.Struct->Highlights;
/// <inheritdoc/>
public bool FacialFeature1 => this.Struct->FacialFeature1;
/// <inheritdoc/>
public bool FacialFeature2 => this.Struct->FacialFeature2;
/// <inheritdoc/>
public bool FacialFeature3 => this.Struct->FacialFeature3;
/// <inheritdoc/>
public bool FacialFeature4 => this.Struct->FacialFeature4;
/// <inheritdoc/>
public bool FacialFeature5 => this.Struct->FacialFeature5;
/// <inheritdoc/>
public bool FacialFeature6 => this.Struct->FacialFeature6;
/// <inheritdoc/>
public bool FacialFeature7 => this.Struct->FacialFeature7;
/// <inheritdoc/>
public bool LegacyTattoo => this.Struct->LegacyTattoo;
/// <inheritdoc/>
public byte EyeShape => this.Struct->EyeShape;
/// <inheritdoc/>
public bool SmallIris => this.Struct->SmallIris;
/// <inheritdoc/>
public byte Mouth => this.Struct->Mouth;
/// <inheritdoc/>
public bool Lipstick => this.Struct->Lipstick;
/// <inheritdoc/>
public byte FacePaint => this.Struct->FacePaint;
/// <inheritdoc/>
public bool FacePaintReversed => this.Struct->FacePaintReversed;
/// <summary>
/// Gets the underlying structure.
/// </summary>
internal FFXIVClientStructs.FFXIV.Client.Game.Character.CustomizeData* Struct =>
(FFXIVClientStructs.FFXIV.Client.Game.Character.CustomizeData*)this.Address;
}

View file

@ -1,6 +1,8 @@
using Dalamud.Data; using Dalamud.Data;
using Dalamud.Game.ClientState.Customize;
using Dalamud.Game.ClientState.Objects.Enums; using Dalamud.Game.ClientState.Objects.Enums;
using Dalamud.Game.Text.SeStringHandling; using Dalamud.Game.Text.SeStringHandling;
using Dalamud.Utility;
using Lumina.Excel; using Lumina.Excel;
using Lumina.Excel.Sheets; using Lumina.Excel.Sheets;
@ -13,68 +15,73 @@ namespace Dalamud.Game.ClientState.Objects.Types;
public interface ICharacter : IGameObject public interface ICharacter : IGameObject
{ {
/// <summary> /// <summary>
/// Gets the current HP of this Chara. /// Gets the current HP of this character.
/// </summary> /// </summary>
public uint CurrentHp { get; } public uint CurrentHp { get; }
/// <summary> /// <summary>
/// Gets the maximum HP of this Chara. /// Gets the maximum HP of this character.
/// </summary> /// </summary>
public uint MaxHp { get; } public uint MaxHp { get; }
/// <summary> /// <summary>
/// Gets the current MP of this Chara. /// Gets the current MP of this character.
/// </summary> /// </summary>
public uint CurrentMp { get; } public uint CurrentMp { get; }
/// <summary> /// <summary>
/// Gets the maximum MP of this Chara. /// Gets the maximum MP of this character.
/// </summary> /// </summary>
public uint MaxMp { get; } public uint MaxMp { get; }
/// <summary> /// <summary>
/// Gets the current GP of this Chara. /// Gets the current GP of this character.
/// </summary> /// </summary>
public uint CurrentGp { get; } public uint CurrentGp { get; }
/// <summary> /// <summary>
/// Gets the maximum GP of this Chara. /// Gets the maximum GP of this character.
/// </summary> /// </summary>
public uint MaxGp { get; } public uint MaxGp { get; }
/// <summary> /// <summary>
/// Gets the current CP of this Chara. /// Gets the current CP of this character.
/// </summary> /// </summary>
public uint CurrentCp { get; } public uint CurrentCp { get; }
/// <summary> /// <summary>
/// Gets the maximum CP of this Chara. /// Gets the maximum CP of this character.
/// </summary> /// </summary>
public uint MaxCp { get; } public uint MaxCp { get; }
/// <summary> /// <summary>
/// Gets the shield percentage of this Chara. /// Gets the shield percentage of this character.
/// </summary> /// </summary>
public byte ShieldPercentage { get; } public byte ShieldPercentage { get; }
/// <summary> /// <summary>
/// Gets the ClassJob of this Chara. /// Gets the ClassJob of this character.
/// </summary> /// </summary>
public RowRef<ClassJob> ClassJob { get; } public RowRef<ClassJob> ClassJob { get; }
/// <summary> /// <summary>
/// Gets the level of this Chara. /// Gets the level of this character.
/// </summary> /// </summary>
public byte Level { get; } public byte Level { get; }
/// <summary> /// <summary>
/// Gets a byte array describing the visual appearance of this Chara. /// Gets a byte array describing the visual appearance of this character.
/// Indexed by <see cref="CustomizeIndex"/>. /// Indexed by <see cref="CustomizeIndex"/>.
/// </summary> /// </summary>
public byte[] Customize { get; } public byte[] Customize { get; }
/// <summary> /// <summary>
/// Gets the Free Company tag of this chara. /// Gets the underlying CustomizeData struct for this character.
/// </summary>
public ICustomizeData CustomizeData { get; }
/// <summary>
/// Gets the Free Company tag of this character.
/// </summary> /// </summary>
public SeString CompanyTag { get; } public SeString CompanyTag { get; }
@ -116,7 +123,7 @@ internal unsafe class Character : GameObject, ICharacter
/// This represents a non-static entity. /// This represents a non-static entity.
/// </summary> /// </summary>
/// <param name="address">The address of this character in memory.</param> /// <param name="address">The address of this character in memory.</param>
internal Character(IntPtr address) internal Character(nint address)
: base(address) : base(address)
{ {
} }
@ -155,8 +162,12 @@ internal unsafe class Character : GameObject, ICharacter
public byte Level => this.Struct->CharacterData.Level; public byte Level => this.Struct->CharacterData.Level;
/// <inheritdoc/> /// <inheritdoc/>
[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 byte[] Customize => this.Struct->DrawData.CustomizeData.Data.ToArray();
/// <inheritdoc/>
public ICustomizeData CustomizeData => new CustomizeData((nint)(&this.Struct->DrawData.CustomizeData));
/// <inheritdoc/> /// <inheritdoc/>
public SeString CompanyTag => SeString.Parse(this.Struct->FreeCompanyTag); public SeString CompanyTag => SeString.Parse(this.Struct->FreeCompanyTag);

View file

@ -3,6 +3,7 @@ using System.Collections.Generic;
using Dalamud.Data; using Dalamud.Data;
using Dalamud.Game.Gui; using Dalamud.Game.Gui;
using Dalamud.Hooking;
using Dalamud.IoC; using Dalamud.IoC;
using Dalamud.IoC.Internal; using Dalamud.IoC.Internal;
using Dalamud.Logging.Internal; using Dalamud.Logging.Internal;
@ -16,7 +17,9 @@ using FFXIVClientStructs.FFXIV.Component.Exd;
using Lumina.Excel; using Lumina.Excel;
using Lumina.Excel.Sheets; using Lumina.Excel.Sheets;
using AchievementSheet = Lumina.Excel.Sheets.Achievement;
using ActionSheet = Lumina.Excel.Sheets.Action; using ActionSheet = Lumina.Excel.Sheets.Action;
using CSAchievement = FFXIVClientStructs.FFXIV.Client.Game.UI.Achievement;
using InstanceContentSheet = Lumina.Excel.Sheets.InstanceContent; using InstanceContentSheet = Lumina.Excel.Sheets.InstanceContent;
using PublicContentSheet = Lumina.Excel.Sheets.PublicContent; 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 static readonly ModuleLog Log = new(nameof(UnlockState));
private readonly ConcurrentDictionary<Type, HashSet<uint>> cachedUnlockedRowIds = [];
[ServiceManager.ServiceDependency] [ServiceManager.ServiceDependency]
private readonly DataManager dataManager = Service<DataManager>.Get(); private readonly DataManager dataManager = Service<DataManager>.Get();
@ -44,17 +45,38 @@ internal unsafe class UnlockState : IInternalDisposableService, IUnlockState
[ServiceManager.ServiceDependency] [ServiceManager.ServiceDependency]
private readonly RecipeData recipeData = Service<RecipeData>.Get(); private readonly RecipeData recipeData = Service<RecipeData>.Get();
private readonly ConcurrentDictionary<Type, HashSet<uint>> cachedUnlockedRowIds = [];
private readonly Hook<CSAchievement.Delegates.SetAchievementCompleted> setAchievementCompletedHook;
private readonly Hook<TitleList.Delegates.SetTitleUnlocked> setTitleUnlockedHook;
[ServiceManager.ServiceConstructor] [ServiceManager.ServiceConstructor]
private UnlockState() private UnlockState()
{ {
this.clientState.Login += this.OnLogin; this.clientState.Login += this.OnLogin;
this.clientState.Logout += this.OnLogout; this.clientState.Logout += this.OnLogout;
this.gameGui.AgentUpdate += this.OnAgentUpdate; this.gameGui.AgentUpdate += this.OnAgentUpdate;
this.setAchievementCompletedHook = Hook<CSAchievement.Delegates.SetAchievementCompleted>.FromAddress(
(nint)CSAchievement.MemberFunctionPointers.SetAchievementCompleted,
this.SetAchievementCompletedDetour);
this.setTitleUnlockedHook = Hook<TitleList.Delegates.SetTitleUnlocked>.FromAddress(
(nint)TitleList.MemberFunctionPointers.SetTitleUnlocked,
this.SetTitleUnlockedDetour);
this.setAchievementCompletedHook.Enable();
this.setTitleUnlockedHook.Enable();
} }
/// <inheritdoc/> /// <inheritdoc/>
public event IUnlockState.UnlockDelegate Unlock; public event IUnlockState.UnlockDelegate Unlock;
/// <inheritdoc/>
public bool IsAchievementListLoaded => CSAchievement.Instance()->IsLoaded();
/// <inheritdoc/>
public bool IsTitleListLoaded => UIState.Instance()->TitleList.DataReceived;
private bool IsLoaded => PlayerState.Instance()->IsLoaded; private bool IsLoaded => PlayerState.Instance()->IsLoaded;
/// <inheritdoc/> /// <inheritdoc/>
@ -63,6 +85,21 @@ internal unsafe class UnlockState : IInternalDisposableService, IUnlockState
this.clientState.Login -= this.OnLogin; this.clientState.Login -= this.OnLogin;
this.clientState.Logout -= this.OnLogout; this.clientState.Logout -= this.OnLogout;
this.gameGui.AgentUpdate -= this.OnAgentUpdate; this.gameGui.AgentUpdate -= this.OnAgentUpdate;
this.setAchievementCompletedHook.Dispose();
}
/// <inheritdoc/>
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);
} }
/// <inheritdoc/> /// <inheritdoc/>
@ -71,6 +108,15 @@ internal unsafe class UnlockState : IInternalDisposableService, IUnlockState
return this.IsUnlockLinkUnlocked(row.UnlockLink.RowId); return this.IsUnlockLinkUnlocked(row.UnlockLink.RowId);
} }
/// <inheritdoc/>
public bool IsAdventureComplete(Adventure row)
{
if (!this.IsLoaded)
return false;
return PlayerState.Instance()->IsAdventureComplete(row.RowId - 0x210000);
}
/// <inheritdoc/> /// <inheritdoc/>
public bool IsAetherCurrentUnlocked(AetherCurrent row) public bool IsAetherCurrentUnlocked(AetherCurrent row)
{ {
@ -415,6 +461,19 @@ internal unsafe class UnlockState : IInternalDisposableService, IUnlockState
return PlayerState.Instance()->IsSecretRecipeBookUnlocked(row.RowId); return PlayerState.Instance()->IsSecretRecipeBookUnlocked(row.RowId);
} }
/// <inheritdoc/>
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);
}
/// <inheritdoc/> /// <inheritdoc/>
public bool IsTraitUnlocked(Trait row) public bool IsTraitUnlocked(Trait row)
{ {
@ -464,9 +523,15 @@ internal unsafe class UnlockState : IInternalDisposableService, IUnlockState
if (!this.IsLoaded || rowRef.IsUntyped) if (!this.IsLoaded || rowRef.IsUntyped)
return false; return false;
if (rowRef.TryGetValue<AchievementSheet>(out var achievementRow))
return this.IsAchievementComplete(achievementRow);
if (rowRef.TryGetValue<ActionSheet>(out var actionRow)) if (rowRef.TryGetValue<ActionSheet>(out var actionRow))
return this.IsActionUnlocked(actionRow); return this.IsActionUnlocked(actionRow);
if (rowRef.TryGetValue<Adventure>(out var adventureRow))
return this.IsAdventureComplete(adventureRow);
if (rowRef.TryGetValue<AetherCurrent>(out var aetherCurrentRow)) if (rowRef.TryGetValue<AetherCurrent>(out var aetherCurrentRow))
return this.IsAetherCurrentUnlocked(aetherCurrentRow); return this.IsAetherCurrentUnlocked(aetherCurrentRow);
@ -572,6 +637,9 @@ internal unsafe class UnlockState : IInternalDisposableService, IUnlockState
if (rowRef.TryGetValue<SecretRecipeBook>(out var secretRecipeBookRow)) if (rowRef.TryGetValue<SecretRecipeBook>(out var secretRecipeBookRow))
return this.IsSecretRecipeBookUnlocked(secretRecipeBookRow); return this.IsSecretRecipeBookUnlocked(secretRecipeBookRow);
if (rowRef.TryGetValue<Title>(out var titleRow))
return this.IsTitleUnlocked(titleRow);
if (rowRef.TryGetValue<Trait>(out var traitRow)) if (rowRef.TryGetValue<Trait>(out var traitRow))
return this.IsTraitUnlocked(traitRow); return this.IsTraitUnlocked(traitRow);
@ -621,6 +689,26 @@ internal unsafe class UnlockState : IInternalDisposableService, IUnlockState
this.Update(); 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() private void Update()
{ {
if (!this.IsLoaded) if (!this.IsLoaded)
@ -628,7 +716,10 @@ internal unsafe class UnlockState : IInternalDisposableService, IUnlockState
Log.Verbose("Checking for new unlocks..."); Log.Verbose("Checking for new unlocks...");
// Do not check for Achievements or Titles here!
this.UpdateUnlocksForSheet<ActionSheet>(); this.UpdateUnlocksForSheet<ActionSheet>();
this.UpdateUnlocksForSheet<Adventure>();
this.UpdateUnlocksForSheet<AetherCurrent>(); this.UpdateUnlocksForSheet<AetherCurrent>();
this.UpdateUnlocksForSheet<AetherCurrentCompFlgSet>(); this.UpdateUnlocksForSheet<AetherCurrentCompFlgSet>();
this.UpdateUnlocksForSheet<AozAction>(); this.UpdateUnlocksForSheet<AozAction>();
@ -675,7 +766,6 @@ internal unsafe class UnlockState : IInternalDisposableService, IUnlockState
// For some other day: // For some other day:
// - FishingSpot // - FishingSpot
// - Spearfishing // - Spearfishing
// - Adventure (Sightseeing)
// - MinerFolkloreTome // - MinerFolkloreTome
// - BotanistFolkloreTome // - BotanistFolkloreTome
// - FishingFolkloreTome // - FishingFolkloreTome
@ -688,8 +778,6 @@ internal unsafe class UnlockState : IInternalDisposableService, IUnlockState
// - EmjCostume // - EmjCostume
// Probably not happening, because it requires fetching data from server: // Probably not happening, because it requires fetching data from server:
// - Achievements
// - Titles
// - Bozjan Field Notes // - Bozjan Field Notes
// - Support/Phantom Jobs, which require to be in Occult Crescent, because it checks the jobs level for != 0 // - 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}"); // 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);
{ }
action((RowRef)rowRef); catch (Exception ex)
} {
catch (Exception ex) Log.Error(ex, "Exception during raise of {handler}", action.Method);
{
Log.Error(ex, "Exception during raise of {handler}", action.Method);
}
} }
} }
} }
@ -751,9 +844,21 @@ internal class UnlockStatePluginScoped : IInternalDisposableService, IUnlockStat
/// <inheritdoc/> /// <inheritdoc/>
public event IUnlockState.UnlockDelegate? Unlock; 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/> /// <inheritdoc/>
public bool IsActionUnlocked(ActionSheet row) => this.unlockStateService.IsActionUnlocked(row); public bool IsActionUnlocked(ActionSheet row) => this.unlockStateService.IsActionUnlocked(row);
/// <inheritdoc/>
public bool IsAdventureComplete(Adventure row) => this.unlockStateService.IsAdventureComplete(row);
/// <inheritdoc/> /// <inheritdoc/>
public bool IsAetherCurrentCompFlgSetUnlocked(AetherCurrentCompFlgSet row) => this.unlockStateService.IsAetherCurrentCompFlgSetUnlocked(row); public bool IsAetherCurrentCompFlgSetUnlocked(AetherCurrentCompFlgSet row) => this.unlockStateService.IsAetherCurrentCompFlgSetUnlocked(row);
@ -874,6 +979,9 @@ internal class UnlockStatePluginScoped : IInternalDisposableService, IUnlockStat
/// <inheritdoc/> /// <inheritdoc/>
public bool IsSecretRecipeBookUnlocked(SecretRecipeBook row) => this.unlockStateService.IsSecretRecipeBookUnlocked(row); public bool IsSecretRecipeBookUnlocked(SecretRecipeBook row) => this.unlockStateService.IsSecretRecipeBookUnlocked(row);
/// <inheritdoc/>
public bool IsTitleUnlocked(Title row) => this.unlockStateService.IsTitleUnlocked(row);
/// <inheritdoc/> /// <inheritdoc/>
public bool IsTraitUnlocked(Trait row) => this.unlockStateService.IsTraitUnlocked(row); public bool IsTraitUnlocked(Trait row) => this.unlockStateService.IsTraitUnlocked(row);

View file

@ -6,6 +6,8 @@ using Dalamud.Configuration.Internal;
using Dalamud.Hooking.Internal; using Dalamud.Hooking.Internal;
using Dalamud.Hooking.Internal.Verification; using Dalamud.Hooking.Internal.Verification;
using TerraFX.Interop.Windows;
namespace Dalamud.Hooking; namespace Dalamud.Hooking;
/// <summary> /// <summary>
@ -20,6 +22,8 @@ public abstract class Hook<T> : IDalamudHook where T : Delegate
private const ulong IMAGE_ORDINAL_FLAG64 = 0x8000000000000000; private const ulong IMAGE_ORDINAL_FLAG64 = 0x8000000000000000;
// ReSharper disable once InconsistentNaming // ReSharper disable once InconsistentNaming
private const uint IMAGE_ORDINAL_FLAG32 = 0x80000000; private const uint IMAGE_ORDINAL_FLAG32 = 0x80000000;
// ReSharper disable once InconsistentNaming
private const int IMAGE_DIRECTORY_ENTRY_IMPORT = 1;
#pragma warning restore SA1310 #pragma warning restore SA1310
private readonly IntPtr address; private readonly IntPtr address;
@ -124,25 +128,25 @@ public abstract class Hook<T> : IDalamudHook where T : Delegate
module ??= Process.GetCurrentProcess().MainModule; module ??= Process.GetCurrentProcess().MainModule;
if (module == null) if (module == null)
throw new InvalidOperationException("Current module is null?"); throw new InvalidOperationException("Current module is null?");
var pDos = (PeHeader.IMAGE_DOS_HEADER*)module.BaseAddress; var pDos = (IMAGE_DOS_HEADER*)module.BaseAddress;
var pNt = (PeHeader.IMAGE_FILE_HEADER*)(module.BaseAddress + (int)pDos->e_lfanew + 4); var pNt = (IMAGE_FILE_HEADER*)(module.BaseAddress + pDos->e_lfanew + 4);
var isPe64 = pNt->SizeOfOptionalHeader == Marshal.SizeOf<PeHeader.IMAGE_OPTIONAL_HEADER64>(); var isPe64 = pNt->SizeOfOptionalHeader == Marshal.SizeOf<IMAGE_OPTIONAL_HEADER64>();
PeHeader.IMAGE_DATA_DIRECTORY* pDataDirectory; IMAGE_DATA_DIRECTORY* pDataDirectory;
if (isPe64) if (isPe64)
{ {
var pOpt = (PeHeader.IMAGE_OPTIONAL_HEADER64*)(module.BaseAddress + (int)pDos->e_lfanew + 4 + Marshal.SizeOf<PeHeader.IMAGE_FILE_HEADER>()); var pOpt = (IMAGE_OPTIONAL_HEADER64*)(module.BaseAddress + pDos->e_lfanew + 4 + Marshal.SizeOf<IMAGE_FILE_HEADER>());
pDataDirectory = &pOpt->ImportTable; pDataDirectory = &pOpt->DataDirectory[IMAGE_DIRECTORY_ENTRY_IMPORT];
} }
else else
{ {
var pOpt = (PeHeader.IMAGE_OPTIONAL_HEADER32*)(module.BaseAddress + (int)pDos->e_lfanew + 4 + Marshal.SizeOf<PeHeader.IMAGE_FILE_HEADER>()); var pOpt = (IMAGE_OPTIONAL_HEADER32*)(module.BaseAddress + pDos->e_lfanew + 4 + Marshal.SizeOf<IMAGE_FILE_HEADER>());
pDataDirectory = &pOpt->ImportTable; pDataDirectory = &pOpt->DataDirectory[IMAGE_DIRECTORY_ENTRY_IMPORT];
} }
var moduleNameLowerWithNullTerminator = (moduleName + "\0").ToLowerInvariant(); var moduleNameLowerWithNullTerminator = (moduleName + "\0").ToLowerInvariant();
foreach (ref var importDescriptor in new Span<PeHeader.IMAGE_IMPORT_DESCRIPTOR>( foreach (ref var importDescriptor in new Span<IMAGE_IMPORT_DESCRIPTOR>(
(PeHeader.IMAGE_IMPORT_DESCRIPTOR*)(module.BaseAddress + (int)pDataDirectory->VirtualAddress), (IMAGE_IMPORT_DESCRIPTOR*)(module.BaseAddress + (int)pDataDirectory->VirtualAddress),
(int)(pDataDirectory->Size / Marshal.SizeOf<PeHeader.IMAGE_IMPORT_DESCRIPTOR>()))) (int)(pDataDirectory->Size / Marshal.SizeOf<IMAGE_IMPORT_DESCRIPTOR>())))
{ {
// Having all zero values signals the end of the table. We didn't find anything. // Having all zero values signals the end of the table. We didn't find anything.
if (importDescriptor.Characteristics == 0) if (importDescriptor.Characteristics == 0)
@ -248,7 +252,7 @@ public abstract class Hook<T> : IDalamudHook where T : Delegate
ObjectDisposedException.ThrowIf(this.IsDisposed, this); 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 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>())); 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"); 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 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>())); var importAddressesOversizedSpan = new Span<ulong>((ulong*)(baseAddress + (int)desc.FirstThunk), (int)((dir.Size - desc.FirstThunk) / Marshal.SizeOf<ulong>()));

View file

@ -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;
}
}

View file

@ -6,6 +6,8 @@ using System.Runtime.InteropServices;
using Dalamud.Game; using Dalamud.Game;
using Dalamud.Logging.Internal; using Dalamud.Logging.Internal;
using FFXIVClientStructs.FFXIV.Application.Network;
using InteropGenerator.Runtime; using InteropGenerator.Runtime;
namespace Dalamud.Hooking.Internal.Verification; namespace Dalamud.Hooking.Internal.Verification;
@ -25,7 +27,12 @@ internal static class HookVerifier
"ActorControlSelf", "ActorControlSelf",
"E8 ?? ?? ?? ?? 0F B7 0B 83 E9 64", "E8 ?? ?? ?? ?? 0F B7 0B 83 E9 64",
typeof(ActorControlSelfDelegate), // TODO: change this to CS delegate 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)); 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 passedType = typeof(T);
var isAssemblyMarshaled = passedType.Assembly.GetCustomAttribute<DisableRuntimeMarshallingAttribute>() is null;
// Directly compare delegates // Directly compare delegates
if (passedType == entry.TargetDelegateType) if (passedType == entry.TargetDelegateType)
@ -78,7 +86,7 @@ internal static class HookVerifier
var enforcedInvoke = entry.TargetDelegateType.GetMethod("Invoke")!; var enforcedInvoke = entry.TargetDelegateType.GetMethod("Invoke")!;
// Compare Return Type // Compare Return Type
var mismatch = !CheckParam(passedInvoke.ReturnType, enforcedInvoke.ReturnType); var mismatch = !CheckParam(passedInvoke.ReturnType, enforcedInvoke.ReturnType, isAssemblyMarshaled);
// Compare Parameter Count // Compare Parameter Count
var passedParams = passedInvoke.GetParameters(); var passedParams = passedInvoke.GetParameters();
@ -93,7 +101,7 @@ internal static class HookVerifier
// Compare Parameter Types // Compare Parameter Types
for (var i = 0; i < passedParams.Length; i++) 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; mismatch = true;
break; 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; 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 { 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(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 == 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.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]) * length, _ 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 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)!), _ when type.IsGenericType => Marshal.SizeOf(Activator.CreateInstance(type)!),
_ => GetSizeOf(type), _ => GetSizeOf(type),
}; };

View file

@ -657,6 +657,7 @@ public partial class FileDialog
this.fileNameBuffer = $"{this.selectedFileNames.Count} files Selected"; this.fileNameBuffer = $"{this.selectedFileNames.Count} files Selected";
} }
this.SelectionChanged(this, this.GetFilePathName());
if (setLastSelection) if (setLastSelection)
{ {
this.lastSelectedFileName = name; this.lastSelectedFileName = name;

View file

@ -97,6 +97,8 @@ public partial class FileDialog
this.SetupSideBar(); this.SetupSideBar();
} }
public event EventHandler<string>? SelectionChanged;
/// <summary> /// <summary>
/// Shows the dialog. /// Shows the dialog.
/// </summary> /// </summary>

View file

@ -30,6 +30,12 @@ public class FileDialogManager
private Action<bool, List<string>>? multiCallback; private Action<bool, List<string>>? multiCallback;
private string savedPath = "."; 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> /// <summary>
/// Create a dialog which selects an already existing folder. /// Create a dialog which selects an already existing folder.
/// </summary> /// </summary>
@ -175,6 +181,8 @@ public class FileDialogManager
this.multiCallback = null; this.multiCallback = null;
} }
private void OnSelectionChange(object sender, string path) => this.SelectionChanged?.Invoke(sender, path);
private void SetDialog( private void SetDialog(
string id, string id,
string title, string title,
@ -200,6 +208,7 @@ public class FileDialogManager
if (this.dialog is not null) if (this.dialog is not null)
{ {
this.dialog.SortOrderChanged -= this.OnSortOrderChange; this.dialog.SortOrderChanged -= this.OnSortOrderChange;
this.dialog.SelectionChanged -= this.OnSelectionChange;
} }
this.dialog = new FileDialog(id, title, filters, path, defaultFileName, defaultExtension, selectionCountMax, isModal, flags); 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.SortOrderChanged += this.OnSortOrderChange;
this.dialog.SelectionChanged += this.OnSelectionChange;
this.dialog.WindowFlags |= this.AddedWindowFlags; this.dialog.WindowFlags |= this.AddedWindowFlags;
foreach (var (name, location, icon, position) in this.CustomSideBarItems) foreach (var (name, location, icon, position) in this.CustomSideBarItems)
this.dialog.SetQuickAccess(name, location, icon, position); this.dialog.SetQuickAccess(name, location, icon, position);

View file

@ -87,7 +87,7 @@ internal class DataShareWidget : IDataWindowWidget
try try
{ {
var dataShare = Service<DataShare>.Get(); var dataShare = Service<DataShare>.Get();
var data2 = dataShare.GetData<object>(name); var data2 = dataShare.GetData<object>(name, new DataCachePluginId("DataShareWidget", Guid.Empty));
try try
{ {
data = Encoding.UTF8.GetBytes( data = Encoding.UTF8.GetBytes(
@ -98,7 +98,7 @@ internal class DataShareWidget : IDataWindowWidget
} }
finally finally
{ {
dataShare.RelinquishData(name); dataShare.RelinquishData(name, new DataCachePluginId("DataShareWidget", Guid.Empty));
} }
} }
catch (Exception e) catch (Exception e)
@ -284,7 +284,7 @@ internal class DataShareWidget : IDataWindowWidget
ImGui.TableSetupColumn("Shared Tag"u8); ImGui.TableSetupColumn("Shared Tag"u8);
ImGui.TableSetupColumn("Show"u8); ImGui.TableSetupColumn("Show"u8);
ImGui.TableSetupColumn("Creator Assembly"u8); ImGui.TableSetupColumn("Creator"u8);
ImGui.TableSetupColumn("#"u8, ImGuiTableColumnFlags.WidthFixed, 30 * ImGuiHelpers.GlobalScale); ImGui.TableSetupColumn("#"u8, ImGuiTableColumnFlags.WidthFixed, 30 * ImGuiHelpers.GlobalScale);
ImGui.TableSetupColumn("Consumers"u8); ImGui.TableSetupColumn("Consumers"u8);
ImGui.TableHeadersRow(); ImGui.TableHeadersRow();
@ -312,9 +312,9 @@ internal class DataShareWidget : IDataWindowWidget
this.nextTab = 2 + index; this.nextTab = 2 + index;
} }
this.DrawTextCell(share.CreatorAssembly, null, true); this.DrawTextCell(share.CreatorPluginId.InternalName, () => share.CreatorPluginId.EffectiveWorkingId.ToString(), true);
this.DrawTextCell(share.Users.Length.ToString(), null, true); this.DrawTextCell(share.UserPluginIds.Length.ToString(), null, true);
this.DrawTextCell(string.Join(", ", share.Users), 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);
} }
} }
} }

View file

@ -21,10 +21,11 @@ internal unsafe class NetworkMonitorWidget : IDataWindowWidget
{ {
private readonly ConcurrentQueue<NetworkPacketData> packets = new(); private readonly ConcurrentQueue<NetworkPacketData> packets = new();
private Hook<PacketDispatcher.Delegates.OnReceivePacket>? hookDown; private Hook<PacketDispatcher.Delegates.OnReceivePacket>? hookZoneDown;
private Hook<ZoneClient.Delegates.SendPacket>? hookUp; private Hook<ZoneClient.Delegates.SendPacket>? hookZoneUp;
private bool trackNetwork; private bool trackZoneUp;
private bool trackZoneDown;
private int trackedPackets = 20; private int trackedPackets = 20;
private ulong nextPacketIndex; private ulong nextPacketIndex;
private string filterString = string.Empty; 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> /// <summary> Finalizes an instance of the <see cref="NetworkMonitorWidget"/> class. </summary>
~NetworkMonitorWidget() ~NetworkMonitorWidget()
{ {
this.hookDown?.Dispose(); this.hookZoneDown?.Dispose();
this.hookUp?.Dispose(); this.hookZoneUp?.Dispose();
} }
private enum NetworkMessageDirection private enum NetworkMessageDirection
@ -60,26 +61,41 @@ internal unsafe class NetworkMonitorWidget : IDataWindowWidget
/// <inheritdoc/> /// <inheritdoc/>
public void Draw() public void Draw()
{ {
this.hookDown ??= Hook<PacketDispatcher.Delegates.OnReceivePacket>.FromAddress( this.hookZoneDown ??= Hook<PacketDispatcher.Delegates.OnReceivePacket>.FromAddress(
(nint)PacketDispatcher.StaticVirtualTablePointer->OnReceivePacket, (nint)PacketDispatcher.StaticVirtualTablePointer->OnReceivePacket,
this.OnReceivePacketDetour); this.OnReceivePacketDetour);
this.hookUp ??= Hook<ZoneClient.Delegates.SendPacket>.FromAddress( this.hookZoneUp ??= Hook<ZoneClient.Delegates.SendPacket>.FromAddress(
(nint)ZoneClient.MemberFunctionPointers.SendPacket, (nint)ZoneClient.MemberFunctionPointers.SendPacket,
this.SendPacketDetour); 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; if (!this.trackZoneDown)
this.hookDown?.Enable(); this.nextPacketIndex = 0;
this.hookUp?.Enable();
this.hookZoneUp?.Enable();
} }
else else
{ {
this.hookDown?.Disable(); this.hookZoneUp?.Disable();
this.hookUp?.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)) if (ImGui.Button("Clear Stored Packets"u8))
{ {
this.packets.Clear(); this.packets.Clear();
this.nextPacketIndex = 0;
} }
ImGui.SameLine(); ImGui.SameLine();
@ -102,7 +119,7 @@ internal unsafe class NetworkMonitorWidget : IDataWindowWidget
ImGui.SameLine(0, ImGui.GetStyle().ItemInnerSpacing.X); ImGui.SameLine(0, ImGui.GetStyle().ItemInnerSpacing.X);
ImGui.Checkbox("##FilterRecording"u8, ref this.filterRecording); ImGui.Checkbox("##FilterRecording"u8, ref this.filterRecording);
if (ImGui.IsItemHovered()) 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); 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"); 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 opCode = *(ushort*)(packet + 2);
var targetName = GetTargetName(targetId); var targetName = GetTargetName(targetId);
this.RecordPacket(new NetworkPacketData(Interlocked.Increment(ref this.nextPacketIndex), DateTime.Now, opCode, NetworkMessageDirection.ZoneDown, targetId, targetName)); 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) private bool SendPacketDetour(ZoneClient* thisPtr, nint packet, uint a3, uint a4, bool a5)
{ {
var opCode = *(ushort*)packet; var opCode = *(ushort*)packet;
this.RecordPacket(new NetworkPacketData(Interlocked.Increment(ref this.nextPacketIndex), DateTime.Now, opCode, NetworkMessageDirection.ZoneUp, 0, string.Empty)); 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) private void RecordPacket(NetworkPacketData packet)

View file

@ -3804,7 +3804,7 @@ internal class PluginInstallerWindow : Window, IDisposable
if (!manifest.Punchline.IsNullOrEmpty()) if (!manifest.Punchline.IsNullOrEmpty())
scores.Add(matcher.Matches(manifest.Punchline.ToLowerInvariant()) * 100); scores.Add(matcher.Matches(manifest.Punchline.ToLowerInvariant()) * 100);
if (manifest.Tags != null) 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(); return scores.Max();
} }

View file

@ -50,6 +50,6 @@ internal sealed class BitmapCodecInfo : IBitmapCodecInfo
_ = readFuncPtr(codecInfo, 0, null, &cch); _ = readFuncPtr(codecInfo, 0, null, &cch);
var buf = stackalloc char[(int)cch + 1]; var buf = stackalloc char[(int)cch + 1];
Marshal.ThrowExceptionForHR(readFuncPtr(codecInfo, cch + 1, buf, &cch)); Marshal.ThrowExceptionForHR(readFuncPtr(codecInfo, cch + 1, buf, &cch));
return new(buf, 0, (int)cch); return new string(buf, 0, (int)cch).Trim('\0');
} }
} }

View file

@ -70,7 +70,7 @@ internal sealed class GamePathSharedImmediateTexture : SharedImmediateTexture
} }
cancellationToken.ThrowIfCancellationRequested(); cancellationToken.ThrowIfCancellationRequested();
var wrap = tm.NoThrottleCreateFromTexFile(file); var wrap = tm.NoThrottleCreateFromTexFile(file.Header, file.TextureBuffer);
tm.BlameSetName(wrap, this.ToString()); tm.BlameSetName(wrap, this.ToString());
return wrap; return wrap;
} }

View file

@ -6,7 +6,6 @@ using System.Threading.Tasks;
using Dalamud.Configuration.Internal; using Dalamud.Configuration.Internal;
using Dalamud.Data; using Dalamud.Data;
using Dalamud.Game; using Dalamud.Game;
using Dalamud.Interface.ImGuiSeStringRenderer.Internal;
using Dalamud.Interface.Internal; using Dalamud.Interface.Internal;
using Dalamud.Interface.Textures.Internal.SharedImmediateTextures; using Dalamud.Interface.Textures.Internal.SharedImmediateTextures;
using Dalamud.Interface.Textures.TextureWraps; using Dalamud.Interface.Textures.TextureWraps;
@ -20,6 +19,7 @@ using Dalamud.Utility.TerraFxCom;
using Lumina.Data; using Lumina.Data;
using Lumina.Data.Files; using Lumina.Data.Files;
using Lumina.Data.Parsing.Tex.Buffers;
using TerraFX.Interop.DirectX; using TerraFX.Interop.DirectX;
using TerraFX.Interop.Windows; using TerraFX.Interop.Windows;
@ -219,7 +219,7 @@ internal sealed partial class TextureManager
null, null,
_ => Task.FromResult( _ => Task.FromResult(
this.BlameSetName( this.BlameSetName(
this.NoThrottleCreateFromTexFile(file), this.NoThrottleCreateFromTexFile(file.Header, file.TextureBuffer),
debugName ?? $"{nameof(this.CreateFromTexFile)}({ForceNullable(file.FilePath)?.Path})")), debugName ?? $"{nameof(this.CreateFromTexFile)}({ForceNullable(file.FilePath)?.Path})")),
cancellationToken); 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 /// <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> /// 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> /// <returns>The loaded texture.</returns>
internal IDalamudTextureWrap NoThrottleCreateFromTexFile(TexFile file) internal IDalamudTextureWrap NoThrottleCreateFromTexFile(TexFile.TexHeader header, TextureBuffer buffer)
{ {
ObjectDisposedException.ThrowIf(this.disposeCts.IsCancellationRequested, this); ObjectDisposedException.ThrowIf(this.disposeCts.IsCancellationRequested, this);
var buffer = file.TextureBuffer; var (dxgiFormat, conversion) = TexFile.GetDxgiFormatFromTextureFormat(header.Format, false);
var (dxgiFormat, conversion) = TexFile.GetDxgiFormatFromTextureFormat(file.Header.Format, false);
if (conversion != TexFile.DxgiFormatConversion.NoConversion || if (conversion != TexFile.DxgiFormatConversion.NoConversion ||
!this.IsDxgiFormatSupported((DXGI_FORMAT)dxgiFormat)) !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); 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; 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 /// <summary>Creates a texture from the given <paramref name="fileBytes"/>, trying to interpret it as a
/// <see cref="TexFile"/>.</summary> /// <see cref="TexFile"/>.</summary>
/// <param name="fileBytes">The file bytes.</param> /// <param name="fileBytes">The file bytes.</param>
/// <returns>The loaded texture.</returns> /// <returns>The loaded texture.</returns>
internal IDalamudTextureWrap NoThrottleCreateFromTexFile(ReadOnlySpan<byte> fileBytes) internal unsafe IDalamudTextureWrap NoThrottleCreateFromTexFile(ReadOnlySpan<byte> fileBytes)
{ {
ObjectDisposedException.ThrowIf(this.disposeCts.IsCancellationRequested, this); ObjectDisposedException.ThrowIf(this.disposeCts.IsCancellationRequested, this);
if (!TexFileExtensions.IsPossiblyTexFile2D(fileBytes)) if (!TexFileExtensions.IsPossiblyTexFile2D(fileBytes))
throw new InvalidDataException("The file is not a TexFile."); throw new InvalidDataException("The file is not a TexFile.");
var bytesArray = fileBytes.ToArray(); TexFile.TexHeader header;
var tf = new TexFile(); TextureBuffer buffer;
typeof(TexFile).GetProperty(nameof(tf.Data))!.GetSetMethod(true)!.Invoke( fixed (byte* p = fileBytes)
tf, {
[bytesArray]); var lbr = new LuminaBinaryReader(new UnmanagedMemoryStream(p, fileBytes.Length));
typeof(TexFile).GetProperty(nameof(tf.Reader))!.GetSetMethod(true)!.Invoke( header = lbr.ReadStructure<TexFile.TexHeader>();
tf, buffer = TextureBuffer.FromStream(header, lbr);
[new LuminaBinaryReader(bytesArray)]); }
// Note: FileInfo and FilePath are not used from TexFile; skip it.
var wrap = this.NoThrottleCreateFromTexFile(tf); var wrap = this.NoThrottleCreateFromTexFile(header, buffer);
this.BlameSetName(wrap, $"{nameof(this.NoThrottleCreateFromTexFile)}({fileBytes.Length:n0})"); this.BlameSetName(wrap, $"{nameof(this.NoThrottleCreateFromTexFile)}({fileBytes.Length:n0})");
return wrap; return wrap;
} }

View file

@ -227,19 +227,19 @@ internal sealed class DalamudPluginInterface : IDalamudPluginInterface, IDisposa
/// <inheritdoc/> /// <inheritdoc/>
public T GetOrCreateData<T>(string tag, Func<T> dataGenerator) where T : class 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/> /// <inheritdoc/>
public void RelinquishData(string tag) public void RelinquishData(string tag)
=> Service<DataShare>.Get().RelinquishData(tag); => Service<DataShare>.Get().RelinquishData(tag, new DataCachePluginId(this.plugin.InternalName, this.plugin.EffectiveWorkingPluginId));
/// <inheritdoc/> /// <inheritdoc/>
public bool TryGetData<T>(string tag, [NotNullWhen(true)] out T? data) where T : class 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/> /// <inheritdoc/>
public T? GetData<T>(string tag) where T : class 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/> /// <inheritdoc/>
public ICallGateProvider<TRet> GetIpcProvider<TRet>(string name) public ICallGateProvider<TRet> GetIpcProvider<TRet>(string name)

View file

@ -1,3 +1,5 @@
using Dalamud.Plugin.Ipc.Internal;
namespace Dalamud.Plugin.Ipc.Exceptions; namespace Dalamud.Plugin.Ipc.Exceptions;
/// <summary> /// <summary>
@ -9,11 +11,11 @@ public class DataCacheCreationError : IpcError
/// Initializes a new instance of the <see cref="DataCacheCreationError"/> class. /// Initializes a new instance of the <see cref="DataCacheCreationError"/> class.
/// </summary> /// </summary>
/// <param name="tag">Tag of the data cache.</param> /// <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="expectedType">The type expected.</param>
/// <param name="ex">The thrown exception.</param> /// <param name="ex">The thrown exception.</param>
public DataCacheCreationError(string tag, string creator, Type expectedType, Exception ex) public DataCacheCreationError(string tag, DataCachePluginId creatorPluginId, Type expectedType, Exception ex)
: base($"The creation of the {expectedType} data cache {tag} initialized by {creator} was unsuccessful.", ex) : base($"The creation of the {expectedType} data cache {tag} initialized by {creatorPluginId.InternalName} ({creatorPluginId.EffectiveWorkingId}) was unsuccessful.", ex)
{ {
} }
} }

View file

@ -1,3 +1,5 @@
using Dalamud.Plugin.Ipc.Internal;
namespace Dalamud.Plugin.Ipc.Exceptions; namespace Dalamud.Plugin.Ipc.Exceptions;
/// <summary> /// <summary>
@ -9,11 +11,11 @@ public class DataCacheTypeMismatchError : IpcError
/// Initializes a new instance of the <see cref="DataCacheTypeMismatchError"/> class. /// Initializes a new instance of the <see cref="DataCacheTypeMismatchError"/> class.
/// </summary> /// </summary>
/// <param name="tag">Tag of the data cache.</param> /// <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="requestedType">The requested type.</param>
/// <param name="actualType">The stored type.</param> /// <param name="actualType">The stored type.</param>
public DataCacheTypeMismatchError(string tag, string creator, Type requestedType, Type actualType) public DataCacheTypeMismatchError(string tag, DataCachePluginId creatorPluginId, Type requestedType, Type actualType)
: base($"Data cache {tag} was requested with type {requestedType}, but {creator} created type {actualType}.") : base($"Data cache {tag} was requested with type {requestedType}, but {creatorPluginId.InternalName} ({creatorPluginId.EffectiveWorkingId}) created type {actualType}.")
{ {
} }
} }

View file

@ -1,5 +1,6 @@
using System.Collections.Generic; using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis; using System.Diagnostics.CodeAnalysis;
using System.Linq;
using System.Runtime.ExceptionServices; using System.Runtime.ExceptionServices;
using Dalamud.Plugin.Ipc.Exceptions; using Dalamud.Plugin.Ipc.Exceptions;
@ -16,12 +17,12 @@ internal readonly struct DataCache
/// <summary> Name of the data. </summary> /// <summary> Name of the data. </summary>
internal readonly string Tag; internal readonly string Tag;
/// <summary> The assembly name of the initial creator. </summary> /// <summary> The creating plugin ID of this DataCache entry. </summary>
internal readonly string CreatorAssemblyName; 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> /// <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> /// <summary> The type the data was registered as. </summary>
internal readonly Type Type; internal readonly Type Type;
@ -33,14 +34,14 @@ internal readonly struct DataCache
/// Initializes a new instance of the <see cref="DataCache"/> struct. /// Initializes a new instance of the <see cref="DataCache"/> struct.
/// </summary> /// </summary>
/// <param name="tag">Name of the data.</param> /// <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="data">A reference to data.</param>
/// <param name="type">The type of the 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.Tag = tag;
this.CreatorAssemblyName = creatorAssemblyName; this.CreatorPluginId = creatorPluginId;
this.UserAssemblyNames = []; this.UserPluginIds = [];
this.Data = data; this.Data = data;
this.Type = type; 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. /// Creates a new instance of the <see cref="DataCache"/> struct, using the given data generator function.
/// </summary> /// </summary>
/// <param name="tag">The name for the data cache.</param> /// <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> /// <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> /// <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> /// <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 where T : class
{ {
try try
{ {
var result = new DataCache(tag, creatorAssemblyName, dataGenerator.Invoke(), typeof(T)); var result = new DataCache(tag, creatorPluginId, dataGenerator.Invoke(), typeof(T));
Log.Verbose( Log.Verbose(
"[{who}] Created new data for [{Tag:l}] for creator {Creator:l}.", "[{who}] Created new data for [{Tag:l}] for creator {Creator:l}.",
nameof(DataShare), nameof(DataShare),
tag, tag,
creatorAssemblyName); creatorPluginId);
return result; return result;
} }
catch (Exception e) catch (Exception e)
{ {
throw ExceptionDispatchInfo.SetCurrentStackTrace( throw ExceptionDispatchInfo.SetCurrentStackTrace(
new DataCacheCreationError(tag, creatorAssemblyName, typeof(T), e)); new DataCacheCreationError(tag, creatorPluginId, typeof(T), e));
} }
} }
/// <summary> /// <summary>
/// Attempts to fetch the data. /// Attempts to fetch the data.
/// </summary> /// </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="value">The value, if succeeded.</param>
/// <param name="ex">The exception, if failed.</param> /// <param name="ex">The exception, if failed.</param>
/// <typeparam name="T">Desired type of the data.</typeparam> /// <typeparam name="T">Desired type of the data.</typeparam>
/// <returns><c>true</c> on success.</returns> /// <returns><c>true</c> on success.</returns>
public bool TryGetData<T>( public bool TryGetData<T>(
string callerName, DataCachePluginId callingPluginId,
[NotNullWhen(true)] out T? value, [NotNullWhen(true)] out T? value,
[NotNullWhen(false)] out Exception? ex) [NotNullWhen(false)] out Exception? ex)
where T : class where T : class
@ -98,16 +99,21 @@ internal readonly struct DataCache
value = data; value = data;
ex = null; ex = null;
// Register the access history // Register the access history. The effective working ID is unique per plugin and persists between reloads, so only add it once.
lock (this.UserAssemblyNames) lock (this.UserPluginIds)
this.UserAssemblyNames.Add(callerName); {
if (this.UserPluginIds.All(c => c.EffectiveWorkingId != callingPluginId.EffectiveWorkingId))
{
this.UserPluginIds.Add(callingPluginId);
}
}
return true; return true;
default: default:
value = null; value = null;
ex = ExceptionDispatchInfo.SetCurrentStackTrace( 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; return false;
} }
} }

View file

@ -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);

View file

@ -33,24 +33,23 @@ internal class DataShare : IServiceType
/// </summary> /// </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> /// <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="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> /// <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> /// <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="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="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> /// <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 where T : class
{ {
var callerName = GetCallerName();
Lazy<DataCache> cacheLazy; Lazy<DataCache> cacheLazy;
lock (this.caches) lock (this.caches)
{ {
if (!this.caches.TryGetValue(tag, out cacheLazy)) 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> /// <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. /// 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> /// </summary>
/// <param name="tag">The name for the data cache.</param> /// <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; DataCache cache;
lock (this.caches) lock (this.caches)
@ -66,10 +66,8 @@ internal class DataShare : IServiceType
if (!this.caches.TryGetValue(tag, out var cacheLazy)) if (!this.caches.TryGetValue(tag, out var cacheLazy))
return; return;
var callerName = GetCallerName();
cache = cacheLazy.Value; cache = cacheLazy.Value;
if (!cache.UserAssemblyNames.Remove(callerName) || cache.UserAssemblyNames.Count > 0) if (!cache.UserPluginIds.Remove(callingPluginId) || cache.UserPluginIds.Count > 0)
return; return;
if (!this.caches.Remove(tag)) if (!this.caches.Remove(tag))
return; return;
@ -99,9 +97,10 @@ internal class DataShare : IServiceType
/// </summary> /// </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> /// <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="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> /// <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> /// <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 where T : class
{ {
data = null; data = null;
@ -112,7 +111,7 @@ internal class DataShare : IServiceType
return false; return false;
} }
return cacheLazy.Value.TryGetData(GetCallerName(), out data, out _); return cacheLazy.Value.TryGetData(callingPluginId, out data, out _);
} }
/// <summary> /// <summary>
@ -121,11 +120,12 @@ internal class DataShare : IServiceType
/// </summary> /// </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> /// <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="tag">The name for the data cache.</param>
/// <param name="callingPluginId">The ID of the calling plugin.</param>
/// <returns>The requested data.</returns> /// <returns>The requested data.</returns>
/// <exception cref="KeyNotFoundException">Thrown if <paramref name="tag"/> is not registered.</exception> /// <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="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="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 where T : class
{ {
Lazy<DataCache> cacheLazy; Lazy<DataCache> cacheLazy;
@ -135,35 +135,19 @@ internal class DataShare : IServiceType
throw new KeyNotFoundException($"The data cache [{tag}] is not registered."); 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> /// <summary>
/// Obtain a read-only list of data shares. /// Obtain a read-only list of data shares.
/// </summary> /// </summary>
/// <returns>All currently subscribed tags, their creator names and all their users.</returns> /// <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) lock (this.caches)
{ {
return this.caches.Select( 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";
}
} }

View file

@ -159,7 +159,7 @@ public interface IPlayerState : IDalamudService
RowRef<Aetheryte> FreeAetheryte { get; } RowRef<Aetheryte> FreeAetheryte { get; }
/// <summary> /// <summary>
/// Gets the amount of received player commendations of the local player. /// Gets the amount of rested experience available to the local player.
/// </summary> /// </summary>
uint BaseRestedExperience { get; } uint BaseRestedExperience { get; }

View file

@ -1,5 +1,3 @@
using System.Diagnostics.CodeAnalysis;
using Lumina.Excel; using Lumina.Excel;
using Lumina.Excel.Sheets; using Lumina.Excel.Sheets;
@ -23,6 +21,24 @@ public interface IUnlockState : IDalamudService
/// </summary> /// </summary>
event UnlockDelegate? Unlock; 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> /// <summary>
/// Determines whether the specified Action is unlocked. /// Determines whether the specified Action is unlocked.
/// </summary> /// </summary>
@ -30,6 +46,13 @@ public interface IUnlockState : IDalamudService
/// <returns><see langword="true"/> if unlocked; otherwise, <see langword="false"/>.</returns> /// <returns><see langword="true"/> if unlocked; otherwise, <see langword="false"/>.</returns>
bool IsActionUnlocked(Lumina.Excel.Sheets.Action row); 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> /// <summary>
/// Determines whether the specified AetherCurrentCompFlgSet is unlocked. /// Determines whether the specified AetherCurrentCompFlgSet is unlocked.
/// </summary> /// </summary>
@ -311,6 +334,14 @@ public interface IUnlockState : IDalamudService
/// <returns><see langword="true"/> if unlocked; otherwise, <see langword="false"/>.</returns> /// <returns><see langword="true"/> if unlocked; otherwise, <see langword="false"/>.</returns>
bool IsSecretRecipeBookUnlocked(SecretRecipeBook row); 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> /// <summary>
/// Determines whether the specified Trait is unlocked. /// Determines whether the specified Trait is unlocked.
/// </summary> /// </summary>

View file

@ -1,4 +1,5 @@
using System.Collections.Generic; using System.Collections.Generic;
using System.IO;
using System.Linq; using System.Linq;
using System.Text; using System.Text;
@ -68,6 +69,7 @@ public static class Troubleshooting
{ {
var payload = new TroubleshootingPayload var payload = new TroubleshootingPayload
{ {
Timestamp = DateTimeOffset.UtcNow.ToUnixTimeSeconds(),
LoadedPlugins = pluginManager?.InstalledPlugins?.Select(x => x.Manifest as LocalPluginManifest)?.OrderByDescending(x => x.InternalName).ToArray(), 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()), 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(), 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))); var encodedPayload = Convert.ToBase64String(Encoding.UTF8.GetBytes(JsonConvert.SerializeObject(payload)));
Log.Information($"TROUBLESHOOTING:{encodedPayload}"); Log.Information($"TROUBLESHOOTING:{encodedPayload}");
File.WriteAllText(
Path.Join(
startInfo.LogPath,
"dalamud.troubleshooting.json"),
JsonConvert.SerializeObject(payload, Formatting.Indented));
} }
catch (Exception ex) catch (Exception ex)
{ {
@ -103,6 +111,8 @@ public static class Troubleshooting
private class TroubleshootingPayload private class TroubleshootingPayload
{ {
public long Timestamp { get; set; }
public LocalPluginManifest[]? LoadedPlugins { get; set; } public LocalPluginManifest[]? LoadedPlugins { get; set; }
public Dictionary<string, string>? PluginStates { get; set; } public Dictionary<string, string>? PluginStates { get; set; }

View file

@ -28,6 +28,9 @@
#include <ShObjIdl.h> #include <ShObjIdl.h>
#include <shlobj_core.h> #include <shlobj_core.h>
#include <dxgi.h>
#pragma comment(lib, "dxgi.lib")
#pragma comment(lib, "comctl32.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='*'\"") #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); 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) { void export_tspack(HWND hWndParent, const std::filesystem::path& logDir, const std::string& crashLog, const std::string& troubleshootingPackData) {
static const char* SourceLogFiles[] = { static const char* SourceLogFiles[] = {
"output.log", // XIVLauncher for Windows "output.log", // XIVLauncher for Windows
"launcher.log", // XIVLauncher.Core for [mostly] Linux "launcher.log", // XIVLauncher.Core for [mostly] Linux
"patcher.log", "patcher.log",
"dalamud.log", "dalamud.log",
"dalamud.troubleshooting.json",
"dalamud.injector.log", "dalamud.injector.log",
"dalamud.boot.log", "dalamud.boot.log",
"aria.log", "aria.log",
@ -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"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 Vendor: {}", vendor) << std::endl;
log << std::format(L"CPU Brand: {}", brand) << 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; log << L"\n" << stackTrace << std::endl;
if (pProgressDialog) if (pProgressDialog)

@ -1 +1 @@
Subproject commit 9ba281cab958049b47bbf7199ab14742c609cd4b Subproject commit a97e9f89d72d40eabd0f3b52266862dca3eba872