Compare commits

...

131 commits

Author SHA1 Message Date
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
goaaats
7d2f12c6e2 build: 14.0.2.1
Some checks failed
Tag Build / Tag Build (push) Failing after 4s
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
2026-02-05 19:56:25 +01:00
goat
fc2220c4d9
Merge pull request #2606 from goatcorp/csupdate-master
[master] Update ClientStructs
2026-02-05 18:45:43 +01:00
github-actions[bot]
d3b9c75e50 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-05 17:35:21 +00: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
Haselnussbomber
33a7cdefa8
Switch to CS in NetworkMonitorWidget (#2600)
Some checks failed
Tag Build / Tag Build (push) Successful in 2s
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
* Switch to CS in NetworkMonitorWidget

* Lazy-init the hooks

* Fix warnings
2026-01-31 12:57:11 -08:00
bleatbot
aa4ace976e
Update ClientStructs (#2598)
Co-authored-by: github-actions[bot] <noreply@github.com>
2026-01-31 12:56:36 -08: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
goaaats
2b51a2a54e build: 14.0.2.0
Some checks failed
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) Failing after 4s
2026-01-28 00:35:23 +01:00
goaaats
5c250c1725 Make Framework.DelayTicks() deadlock-safe before the game has started 2026-01-27 23:43:51 +01:00
goaaats
470267a185 Restore NetworkMessageDirection enum
Fixes API breakage
2026-01-27 23:17:38 +01:00
MidoriKami
5c7a5295d1
Misc Fixes (#2584)
* Disable default logging, remove log message

* Add IDtrBarEntry.MinimumWidth

* Fix Addon/Agent Lifecycle Register/Unregister

* Rename Agent.ReceiveEvent2

* Add to IReadOnlyDtrBarEntry

* Fix autoformat being terrible

* More style fixes

* Add focused changed lifecycle event

* Fix for obsolete renames
2026-01-27 13:49:35 -08:00
goaaats
e598013e30 Upgrade goatcorp.Reloaded.Hooks, remove goatcorp.Reloaded.Assembler 2026-01-27 18:53:35 +01:00
KazWolfe
c0077b1e26
Revert "Use RowRef in ZoneInitEventArgs (#2540)" (#2597)
This reverts commit 5da79a7dba.
2026-01-27 18:30:34 +01:00
bleatbot
10ef40ddf5
Update ClientStructs (#2595)
Co-authored-by: github-actions[bot] <noreply@github.com>
2026-01-27 16:42:07 +00:00
Limiana
5da79a7dba
Use RowRef in ZoneInitEventArgs (#2540)
* Try-catch packet read

* Actually use RowRef instead
2026-01-27 08:38:42 -08:00
Haselnussbomber
3abf7bb00b
Rework NetworkMonitorWidget, remove GameNetwork (#2593)
* Rework NetworkMonitorWidget, remove GameNetwork

* Rework packet filtering
2026-01-27 08:35:55 -08:00
wolfcomp
afa7b0c1f3
Improve parameter verification logic in HookVerifier (#2596)
* Improve parameter verification logic in HookVerifier

Refactor HookVerifier to enhance parameter type checking and add utility methods for size calculations.

* Reverse bool check

* Fix type size check on return type

* Fix non static member in static class

* Fix compiler errors

* Fix SizeOf calls

* Fix IsStruct call

* Cleanup some warnings
2026-01-27 08:30:58 -08:00
Infi
8f8f4faa12
Apply ImRaii to Widgets Part 2 (#2567)
* Apply ImRaii to multiple widgets

* Apply ImRaii to leftover widgets
2026-01-25 19:21:33 -08:00
Infi
672636c3bf
Remove UiDebug V1 in favor of V2 (#2586)
* - Remove UiDebug1 in favor of UiDebug2

* - Remove all mentions of 2
2026-01-25 19:21:03 -08: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
Haselnussbomber
951290cac7
Fix EnumGenerator configuration mapping (#2590) 2026-01-24 20:06:55 -08:00
Haselnussbomber
61423f1791
Fix being unable to edit TitleBgCollapsed (#2589)
Fixes #999
2026-01-24 20:06:25 -08:00
Haselnussbomber
b601bfdbfb
Add Quest and Leve support to IUnlockState (#2581)
* Fix AgentInterfacePtr.FocusAddon

* Add Quest support to IUnlockState

* Reorder, bail out early in Recipe and McGuffin

* Add Leve support to IUnlockState

* Fix warning

* Disable log spam
2026-01-24 20:03:57 -08:00
bleatbot
3b8f0bc92f
Update ClientStructs (#2579)
Co-authored-by: github-actions[bot] <noreply@github.com>
2026-01-24 20:03:03 -08:00
goaaats
c1df0da9be build: 14.0.1.0 2026-01-11 13:18:32 +01:00
goat
214d9027b5
Merge pull request #2580 from Haselnussbomber/ex-experimental
Remove ExperimentalAttribute from IUnlockState
2026-01-11 01:01:31 +01:00
Haselnussbomber
745b3a4939
Remove ExperimentalAttribute from IUnlockState 2026-01-11 00:49:39 +01:00
goat
f3694a41ff
Merge pull request #2563 from Infiziert90/ImRaii-Widgets
Apply ImRaii to Widgets Part 1
2026-01-11 00:45:19 +01:00
goat
39e60f27f2
Merge pull request #2571 from MidoriKami/AgentLifecycle
Add IAgentLifecycle
2026-01-11 00:44:39 +01:00
MidoriKami
a03e37f700
Merge branch 'master' into AgentLifecycle 2026-01-10 08:56:26 -08:00
goaaats
c545205e66 Remove analyzer for source generator projects 2026-01-10 17:53:49 +01:00
MidoriKami
0c2ce097ed Use generated AgentId 2026-01-10 08:30:15 -08:00
MidoriKami
d689c4763a Merge branch 'master' into AgentLifecycle 2026-01-10 08:16:52 -08:00
goaaats
dd94d10722 Add conversion extension method from source enum 2026-01-10 17:10:05 +01:00
goaaats
8bb6cdd8d6 Add "enum cloning" source generator 2026-01-10 17:10:05 +01:00
Infi
fab7eef244
Update UIColorWidget.cs 2026-01-10 14:25:22 +01:00
Infi
5bfbcbb8f5
Merge branch 'master' into ImRaii-Widgets 2026-01-10 14:13:41 +01:00
goat
55eb7e41d8
Merge pull request #2566 from MidoriKami/AddonLifecycleThreadSafety
IAddonLifecycle Thread Safety
2026-01-10 13:13:36 +01:00
goat
b5028add57
Merge pull request #2550 from RedworkDE/logmessage
Add event for LogMessages being added to the chat
2026-01-10 13:05:08 +01:00
goat
5ee339b5a8
Merge pull request #2572 from Loskh/dark_mode
fix: respect system dark mode setting
2026-01-10 12:50:42 +01:00
goat
7fb43f8707
Merge pull request #2578 from goatcorp/csupdate-master
[master] Update ClientStructs
2026-01-10 12:49:36 +01:00
github-actions[bot]
b2fb6949d2 Update ClientStructs 2026-01-10 06:37:41 +00:00
MidoriKami
6c8b2b4a6d Remove casts 2026-01-09 12:52:33 -08:00
MidoriKami
f635673ce9 Use AgentInterfacePtr 2026-01-09 12:51:08 -08:00
goat
156abbdcbe
Merge branch 'master' into ImRaii-Widgets 2026-01-09 21:42:43 +01:00
goat
47f60eb391
Merge pull request #2568 from Infiziert90/ImRaii-UIDebug2
Improve UIDebug2
2026-01-09 21:39:41 +01:00
goat
ef0d680f06
Merge pull request #2573 from goatcorp/csupdate-master
[master] Update ClientStructs
2026-01-09 21:38:58 +01:00
goat
8afc02b364
Merge pull request #2574 from Haselnussbomber/update-AddonEventType
Update AddonEventType
2026-01-09 21:36:51 +01:00
goat
035be9d67d
Merge pull request #2575 from Haselnussbomber/fix-item-redirect-decorations
Fix leaking colors in sheet redirects for Item
2026-01-09 21:36:26 +01:00
goat
86396946e9
Merge pull request #2576 from goatcorp/schemaupdate-master
[master] Update Excel Schema
2026-01-09 21:36:10 +01:00
github-actions[bot]
b29b7851d9 Update ClientStructs 2026-01-09 18:40:20 +00:00
github-actions[bot]
90c29e5646 Update Excel Schema 2026-01-09 18:40:18 +00:00
Haselnussbomber
290ad9fc41
Fix leaking colors in sheet redirects for Item 2026-01-09 10:48:58 +01:00
RedworkDE
0cc5d301e5 Merge branch 'master' of https://github.com/goatcorp/Dalamud into logmessage 2026-01-06 23:30:16 +01:00
Haselnussbomber
c93f04f0e4
Code cleanup (#2439)
* Use new Lock objects

* Fix CA1513: Use ObjectDisposedException.ThrowIf

* Fix CA1860: Avoid using 'Enumerable.Any()' extension method

* Fix IDE0028: Use collection initializers or expressions

* Fix CA2263: Prefer generic overload when type is known

* Fix CA1862: Use the 'StringComparison' method overloads to perform case-insensitive string comparisons

* Fix IDE0270: Null check can be simplified

* Fix IDE0280: Use 'nameof'

* Fix IDE0009: Add '.this'

* Fix IDE0007: Use 'var' instead of explicit type

* Fix IDE0062: Make local function static

* Fix CA1859: Use concrete types when possible for improved performance

* Fix IDE0066: Use switch expression

Only applied to where it doesn't look horrendous.

* Use is over switch

* Fix CA1847: Use String.Contains(char) instead of String.Contains(string) with single characters

* Fix SYSLIB1045: Use 'GeneratedRegexAttribute' to generate the regular expression implementation at compile-time.

* Fix CA1866: Use 'string.EndsWith(char)' instead of 'string.EndsWith(string)' when you have a string with a single char

* Fix IDE0057: Substring can be simplified

* Fix IDE0059: Remove unnecessary value assignment

* Fix CA1510: Use ArgumentNullException throw helper

* Fix IDE0300: Use collection expression for array

* Fix IDE0250: Struct can be made 'readonly'

* Fix IDE0018: Inline variable declaration

* Fix CA1850: Prefer static HashData method over ComputeHash

* Fi CA1872: Prefer 'Convert.ToHexString' and 'Convert.ToHexStringLower' over call chains based on 'BitConverter.ToString'

* Update ModuleLog instantiations

* Organize usings
2026-01-06 08:36:55 -08:00
Haselnussbomber
9b9a66bdd2
Update AddonEventType 2026-01-06 14:19:21 +01:00
Loskh
e94ded628a fix: respect system dark mode setting 2026-01-05 22:42:04 +08:00
MidoriKami
d0caf98eb3 Add Agent Lifecycle 2026-01-04 21:40:31 -08:00
bleatbot
27414d33dd
Update ClientStructs (#2569)
Co-authored-by: github-actions[bot] <noreply@github.com>
2026-01-05 02:40:08 +00:00
Infi
bd05f4c1a5 - Switch SeString to ReadOnlySeString
- Utf8String to ReadOnlySeString avoiding Marshal
- Remove unnecessary ImRaii checks
- Switch default to 0
2026-01-05 03:15:01 +01:00
wolfcomp
bcc16c9b0e
Add CPU info to crash log (#2565)
* Add CPU info to crash log

Added a function to retrieve CPU vendor and brand information.

* Add missing include

* Remove unused std::strings
2026-01-04 23:00:31 +00:00
MidoriKami
36c3429566 Force to next tick instead of running immediately 2026-01-04 14:41:30 -08:00
MidoriKami
1398054216 Push AddonLifecycle event register/unregister to main thread 2026-01-04 14:03:15 -08:00
RedworkDE
6e19aca481 Fix StyleCop warnings 2026-01-04 16:00:10 +01:00
RedworkDE
790669e60a Battle log exists, selftest with the use action message 2026-01-04 08:12:06 +01:00
RedworkDE
9b55b020ca Switch selftest to using mounts instead of teleporting 2026-01-03 23:11:50 +01:00
RedworkDE
8b0f0fb44e Merge branch 'master' of https://github.com/goatcorp/Dalamud into logmessage 2026-01-03 22:28:43 +01:00
Infi
09a1fd1925 - Apply ImRaii to SeStringRendererTestWidget
- Add new themes
- Remove uses of Dalamud SeString
- Use collection expression
2026-01-03 21:43:12 +01:00
Infi
5fe6df3887 Cleanup TaskSchedulerWidget and ensure color is always popped 2026-01-03 21:31:28 +01:00
Infi
8c26d67739 Apply ImRaii to TexWidget 2026-01-03 21:24:57 +01:00
Infi
e4ef56b878 Apply ImRaii to UldWidget 2026-01-03 21:09:20 +01:00
Infi
e44fda1911 - Apply ImRaii to UIColorWidget 2026-01-03 21:04:01 +01:00
Infi
a1d2e275a7 - Apply ImRaii to FontAwesomeTestWidget
- Adjust array init
2026-01-03 20:54:24 +01:00
Haselnussbomber
9538af0554
UldWidget fixes (#2557)
* Fix missing directory separator in theme path

* Hide themed texture exception when file not found

* Check ThemeSupportBitmask
2026-01-03 11:46:48 -08:00
bleatbot
5a0257e40e
Update Excel Schema (#2555)
Co-authored-by: github-actions[bot] <noreply@github.com>
2026-01-03 11:31:46 -08:00
bleatbot
bbb6e438b1
Update ClientStructs (#2556)
Co-authored-by: github-actions[bot] <noreply@github.com>
2026-01-03 11:31:20 -08:00
goat
e32e4a0c8e
Merge pull request #2562 from nebel/xldata-fontawesome-font-issue
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
Fix font corruption caused by Font Awesome icon button in Data window
2026-01-02 17:03:31 +01:00
nebel
49abb19673
Fix font corruption caused by Font Awesome icon button in Data window 2026-01-03 00:50:25 +09:00
goaaats
79ce2fff0a Revert "Use v145 build tools for C++ components"
Some checks failed
Tag Build / Tag Build (push) Successful in 4s
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
This reverts commit 62b8b0834c.
MS can't manage to get actions images with 2026 for some reason.
2025-12-31 12:15:18 +01:00
goat
fc130e325c Fix warnings
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
2025-12-26 16:15:58 +01:00
goat
a659cd8a49 Adjust to Excel renames 2025-12-26 16:15:51 +01:00
goat
62b8b0834c Use v145 build tools for C++ components 2025-12-26 16:09:32 +01:00
goat
61ba319e98
Merge pull request #2547 from goatcorp/csupdate-master
[master] Update ClientStructs
2025-12-26 16:06:57 +01:00
goat
392e027ae3
Merge pull request #2545 from goatcorp/schemaupdate-master
[master] Update Excel Schema
2025-12-26 16:06:39 +01:00
github-actions[bot]
689d2f01d9 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
2025-12-26 15:02:13 +00:00
github-actions[bot]
558a011e00 Update Excel Schema
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
2025-12-26 15:02:13 +00:00
goat
c00363badf build: 14.0.0.3
Some checks failed
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) Failing after 5s
2025-12-25 11:31:47 +01:00
RedworkDE
c559426d8b Add Data Widget 2025-12-24 12:23:24 +01:00
RedworkDE
31cbf4d8eb Respect null-termination of entity names 2025-12-24 12:17:57 +01:00
RedworkDE
65c604f827 More ReadOnlySeString things 2025-12-24 11:14:37 +01:00
RedworkDE
bf75937cc0 Don't go through SeString to null terminate a string 2025-12-23 21:08:54 +01:00
RedworkDE
186b1b8376 Use SeStringEvaluator instead of RaptureTextModule for the debug display 2025-12-23 14:58:47 +01:00
RedworkDE
f76d77f79d rewview (3) and fix some copy docs comments 2025-12-23 12:41:46 +01:00
RedworkDE
9da178ad56 review (2) 2025-12-23 12:34:04 +01:00
RedworkDE
3aca09d0fb review 2025-12-22 22:28:44 +01:00
RedworkDE
b2397efb25 Add Self Test 2025-12-22 20:21:07 +01:00
RedworkDE
282fa87571 Add event for LogMessages being added to the chat 2025-12-22 19:44:00 +01:00
goat
3be14d4135
Merge pull request #2548 from Soreepeong/fix/sta-cts
Some checks failed
Tag Build / Tag Build (push) Successful in 4s
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
Fix wrong CancellationToken usage
2025-12-22 10:56:59 +01:00
Soreepeong
8ccfac2318 Fix wrong CancellationToken usage 2025-12-22 18:46:29 +09:00
358 changed files with 4904 additions and 3394 deletions

View file

@ -75,6 +75,17 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Lumina.Excel.Generator", "l
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Lumina.Excel", "lib\Lumina.Excel\src\Lumina.Excel\Lumina.Excel.csproj", "{88FB719B-EB41-73C5-8D25-C03E0C69904F}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Source Generators", "Source Generators", "{50BEC23B-FFFD-427B-A95D-27E1D1958FFF}"
ProjectSection(SolutionItems) = preProject
generators\Directory.Build.props = generators\Directory.Build.props
EndProjectSection
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Dalamud.EnumGenerator", "generators\Dalamud.EnumGenerator\Dalamud.EnumGenerator\Dalamud.EnumGenerator.csproj", "{27AA9F87-D2AA-41D9-A559-0F1EBA38C5F8}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Dalamud.EnumGenerator.Sample", "generators\Dalamud.EnumGenerator\Dalamud.EnumGenerator.Sample\Dalamud.EnumGenerator.Sample.csproj", "{8CDAEB2D-5022-450A-A97F-181C6270185F}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Dalamud.EnumGenerator.Tests", "generators\Dalamud.EnumGenerator\Dalamud.EnumGenerator.Tests\Dalamud.EnumGenerator.Tests.csproj", "{F5D92D2D-D36F-4471-B657-8B9AA6C98AD6}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@ -173,6 +184,18 @@ Global
{88FB719B-EB41-73C5-8D25-C03E0C69904F}.Debug|Any CPU.Build.0 = Debug|Any CPU
{88FB719B-EB41-73C5-8D25-C03E0C69904F}.Release|Any CPU.ActiveCfg = Release|Any CPU
{88FB719B-EB41-73C5-8D25-C03E0C69904F}.Release|Any CPU.Build.0 = Release|Any CPU
{27AA9F87-D2AA-41D9-A559-0F1EBA38C5F8}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{27AA9F87-D2AA-41D9-A559-0F1EBA38C5F8}.Debug|Any CPU.Build.0 = Debug|Any CPU
{27AA9F87-D2AA-41D9-A559-0F1EBA38C5F8}.Release|Any CPU.ActiveCfg = Release|Any CPU
{27AA9F87-D2AA-41D9-A559-0F1EBA38C5F8}.Release|Any CPU.Build.0 = Release|Any CPU
{8CDAEB2D-5022-450A-A97F-181C6270185F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{8CDAEB2D-5022-450A-A97F-181C6270185F}.Debug|Any CPU.Build.0 = Debug|Any CPU
{8CDAEB2D-5022-450A-A97F-181C6270185F}.Release|Any CPU.ActiveCfg = Release|Any CPU
{8CDAEB2D-5022-450A-A97F-181C6270185F}.Release|Any CPU.Build.0 = Release|Any CPU
{F5D92D2D-D36F-4471-B657-8B9AA6C98AD6}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{F5D92D2D-D36F-4471-B657-8B9AA6C98AD6}.Debug|Any CPU.Build.0 = Debug|Any CPU
{F5D92D2D-D36F-4471-B657-8B9AA6C98AD6}.Release|Any CPU.ActiveCfg = Release|Any CPU
{F5D92D2D-D36F-4471-B657-8B9AA6C98AD6}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
@ -197,6 +220,9 @@ Global
{02EA681E-C7D8-13C7-8484-4AC65E1B71E8} = {E15BDA6D-E881-4482-94BA-BE5527E917FF}
{5A44DF0C-C9DA-940F-4D6B-4A11D13AEA3D} = {02EA681E-C7D8-13C7-8484-4AC65E1B71E8}
{88FB719B-EB41-73C5-8D25-C03E0C69904F} = {02EA681E-C7D8-13C7-8484-4AC65E1B71E8}
{27AA9F87-D2AA-41D9-A559-0F1EBA38C5F8} = {50BEC23B-FFFD-427B-A95D-27E1D1958FFF}
{8CDAEB2D-5022-450A-A97F-181C6270185F} = {50BEC23B-FFFD-427B-A95D-27E1D1958FFF}
{F5D92D2D-D36F-4471-B657-8B9AA6C98AD6} = {50BEC23B-FFFD-427B-A95D-27E1D1958FFF}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {79B65AC9-C940-410E-AB61-7EA7E12C7599}

View file

@ -10,7 +10,6 @@ using System.Threading.Tasks;
using Dalamud.Game.Text;
using Dalamud.Interface;
using Dalamud.Interface.FontIdentifier;
using Dalamud.Interface.ImGuiNotification.Internal;
using Dalamud.Interface.Internal;
using Dalamud.Interface.Internal.ReShadeHandling;
using Dalamud.Interface.Style;
@ -20,9 +19,12 @@ using Dalamud.Plugin.Internal.AutoUpdate;
using Dalamud.Plugin.Internal.Profiles;
using Dalamud.Storage;
using Dalamud.Utility;
using Newtonsoft.Json;
using Serilog;
using Serilog.Events;
using Windows.Win32.UI.WindowsAndMessaging;
namespace Dalamud.Configuration.Internal;
@ -91,7 +93,7 @@ internal sealed class DalamudConfiguration : IInternalDisposableService
/// <summary>
/// Gets or sets a dictionary of seen FTUE levels.
/// </summary>
public Dictionary<string, int> SeenFtueLevels { get; set; } = new();
public Dictionary<string, int> SeenFtueLevels { get; set; } = [];
/// <summary>
/// Gets or sets the last loaded Dalamud version.
@ -111,7 +113,7 @@ internal sealed class DalamudConfiguration : IInternalDisposableService
/// <summary>
/// Gets or sets a list of custom repos.
/// </summary>
public List<ThirdPartyRepoSettings> ThirdRepoList { get; set; } = new();
public List<ThirdPartyRepoSettings> ThirdRepoList { get; set; } = [];
/// <summary>
/// Gets or sets a value indicating whether a disclaimer regarding third-party repos has been dismissed.
@ -121,12 +123,12 @@ internal sealed class DalamudConfiguration : IInternalDisposableService
/// <summary>
/// Gets or sets a list of hidden plugins.
/// </summary>
public List<string> HiddenPluginInternalName { get; set; } = new();
public List<string> HiddenPluginInternalName { get; set; } = [];
/// <summary>
/// Gets or sets a list of seen plugins.
/// </summary>
public List<string> SeenPluginInternalName { get; set; } = new();
public List<string> SeenPluginInternalName { get; set; } = [];
/// <summary>
/// Gets or sets a list of additional settings for devPlugins. The key is the absolute path
@ -134,14 +136,14 @@ internal sealed class DalamudConfiguration : IInternalDisposableService
/// However by specifiying this value manually, you can add arbitrary files outside the normal
/// file paths.
/// </summary>
public Dictionary<string, DevPluginSettings> DevPluginSettings { get; set; } = new();
public Dictionary<string, DevPluginSettings> DevPluginSettings { get; set; } = [];
/// <summary>
/// Gets or sets a list of additional locations that dev plugins should be loaded from. This can
/// be either a DLL or folder, but should be the absolute path, or a path relative to the currently
/// injected Dalamud instance.
/// </summary>
public List<DevPluginLocationSettings> DevPluginLoadLocations { get; set; } = new();
public List<DevPluginLocationSettings> DevPluginLoadLocations { get; set; } = [];
/// <summary>
/// Gets or sets the global UI scale.
@ -223,7 +225,7 @@ internal sealed class DalamudConfiguration : IInternalDisposableService
/// <summary>
/// Gets or sets a list representing the command history for the Dalamud Console.
/// </summary>
public List<string> LogCommandHistory { get; set; } = new();
public List<string> LogCommandHistory { get; set; } = [];
/// <summary>
/// Gets or sets a value indicating whether the dev bar should open at startup.
@ -599,7 +601,7 @@ internal sealed class DalamudConfiguration : IInternalDisposableService
{
// https://source.chromium.org/chromium/chromium/src/+/main:ui/gfx/animation/animation_win.cc;l=29?q=ReducedMotion&ss=chromium
var winAnimEnabled = 0;
var success = false;
bool success;
unsafe
{
success = Windows.Win32.PInvoke.SystemParametersInfo(

View file

@ -31,5 +31,5 @@ internal sealed class DevPluginSettings
/// <summary>
/// Gets or sets a list of validation problems that have been dismissed by the user.
/// </summary>
public List<string> DismissedValidationProblems { get; set; } = new();
public List<string> DismissedValidationProblems { get; set; } = [];
}

View file

@ -1,4 +1,4 @@
using System.Collections.Generic;
using System.Collections.Generic;
using System.Linq;
using System.Runtime.CompilerServices;
using System.Text.RegularExpressions;
@ -17,9 +17,9 @@ namespace Dalamud.Console;
[ServiceManager.BlockingEarlyLoadedService("Console is needed by other blocking early loaded services.")]
internal partial class ConsoleManager : IServiceType
{
private static readonly ModuleLog Log = new("CON");
private static readonly ModuleLog Log = ModuleLog.Create<ConsoleManager>();
private Dictionary<string, IConsoleEntry> entries = new();
private Dictionary<string, IConsoleEntry> entries = [];
/// <summary>
/// Initializes a new instance of the <see cref="ConsoleManager"/> class.
@ -99,10 +99,7 @@ internal partial class ConsoleManager : IServiceType
ArgumentNullException.ThrowIfNull(name);
ArgumentNullException.ThrowIfNull(alias);
var target = this.FindEntry(name);
if (target == null)
throw new EntryNotFoundException(name);
var target = this.FindEntry(name) ?? throw new EntryNotFoundException(name);
if (this.FindEntry(alias) != null)
throw new InvalidOperationException($"Entry '{alias}' already exists.");
@ -346,7 +343,7 @@ internal partial class ConsoleManager : IServiceType
private static class Traits
{
public static void ThrowIfTIsNullableAndNull<T>(T? argument, [CallerArgumentExpression("argument")] string? paramName = null)
public static void ThrowIfTIsNullableAndNull<T>(T? argument, [CallerArgumentExpression(nameof(argument))] string? paramName = null)
{
if (argument == null && !typeof(T).IsValueType)
throw new ArgumentNullException(paramName);

View file

@ -1,4 +1,4 @@
using System.Collections.Generic;
using System.Collections.Generic;
using System.Linq;
using System.Text.RegularExpressions;
@ -65,7 +65,7 @@ internal class ConsoleManagerPluginScoped : IConsole, IInternalDisposableService
[ServiceManager.ServiceDependency]
private readonly ConsoleManager console = Service<ConsoleManager>.Get();
private readonly List<IConsoleEntry> trackedEntries = new();
private readonly List<IConsoleEntry> trackedEntries = [];
/// <summary>
/// Initializes a new instance of the <see cref="ConsoleManagerPluginScoped"/> class.

View file

@ -14,7 +14,9 @@ using Dalamud.Plugin.Internal;
using Dalamud.Storage;
using Dalamud.Utility;
using Dalamud.Utility.Timing;
using Serilog;
using Windows.Win32.Foundation;
using Windows.Win32.Security;

View file

@ -6,7 +6,7 @@
<PropertyGroup Label="Feature">
<Description>XIV Launcher addon framework</Description>
<DalamudVersion>14.0.0.2</DalamudVersion>
<DalamudVersion>14.0.2.1</DalamudVersion>
<AssemblyVersion>$(DalamudVersion)</AssemblyVersion>
<Version>$(DalamudVersion)</Version>
<FileVersion>$(DalamudVersion)</FileVersion>
@ -65,7 +65,6 @@
<PackageReference Include="CheapLoc" />
<PackageReference Include="DotNet.ReproducibleBuilds" PrivateAssets="all" />
<PackageReference Include="goatcorp.Reloaded.Hooks" />
<PackageReference Include="goatcorp.Reloaded.Assembler" />
<PackageReference Include="JetBrains.Annotations" />
<PackageReference Include="Lumina" />
<PackageReference Include="Microsoft.Extensions.ObjectPool" />
@ -88,6 +87,15 @@
<PackageReference Include="TerraFX.Interop.Windows" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\generators\Dalamud.EnumGenerator\Dalamud.EnumGenerator\Dalamud.EnumGenerator.csproj" OutputItemType="Analyzer" ReferenceOutputAssembly="false"/>
</ItemGroup>
<ItemGroup>
<None Remove="EnumCloneMap.txt"/>
<AdditionalFiles Include="EnumCloneMap.txt" />
</ItemGroup>
<ItemGroup>
<EmbeddedResource Include="Interface\ImGuiBackend\Renderers\imgui-frag.hlsl.bytes">
<LogicalName>imgui-frag.hlsl.bytes</LogicalName>

View file

@ -8,11 +8,13 @@ using Dalamud.IoC.Internal;
using Dalamud.Plugin.Services;
using Dalamud.Utility;
using Dalamud.Utility.Timing;
using Lumina;
using Lumina.Data;
using Lumina.Excel;
using Newtonsoft.Json;
using Serilog;
namespace Dalamud.Data;

View file

@ -3,7 +3,9 @@ using System.Collections.Generic;
using Dalamud.Hooking;
using Dalamud.Logging.Internal;
using Dalamud.Memory;
using FFXIVClientStructs.FFXIV.Client.LayoutEngine;
using Lumina.Text.ReadOnly;
namespace Dalamud.Data;
@ -13,7 +15,7 @@ namespace Dalamud.Data;
/// </summary>
internal sealed unsafe class RsvResolver : IDisposable
{
private static readonly ModuleLog Log = new("RsvProvider");
private static readonly ModuleLog Log = ModuleLog.Create<RsvResolver>();
private readonly Hook<LayoutWorld.Delegates.AddRsvString> addRsvStringHook;

View file

@ -1,8 +1,6 @@
using System.ComponentModel;
using System.Diagnostics;
using System.IO;
using System.Net;
using System.Reflection;
using System.Runtime.InteropServices;
using System.Threading;
using System.Threading.Tasks;
@ -16,10 +14,13 @@ using Dalamud.Plugin.Internal;
using Dalamud.Storage;
using Dalamud.Support;
using Dalamud.Utility;
using Newtonsoft.Json;
using Serilog;
using Serilog.Core;
using Serilog.Events;
using Windows.Win32.Foundation;
using Windows.Win32.UI.WindowsAndMessaging;

3
Dalamud/EnumCloneMap.txt Normal file
View file

@ -0,0 +1,3 @@
# Format: Target.Full.TypeName = Source.Full.EnumTypeName
# Example: Generate a local enum MyGeneratedEnum in namespace Sample.Gen mapped to SourceEnums.SampleSourceEnum
Dalamud.Game.Agent.AgentId = FFXIVClientStructs.FFXIV.Client.UI.Agent.AgentId

View file

@ -1,5 +1,4 @@
using Dalamud.Plugin.Services;
using Dalamud.Utility;
using FFXIVClientStructs.FFXIV.Component.GUI;

View file

@ -24,7 +24,7 @@ internal unsafe class AddonEventManager : IInternalDisposableService
/// </summary>
public static readonly Guid DalamudInternalKey = Guid.NewGuid();
private static readonly ModuleLog Log = new("AddonEventManager");
private static readonly ModuleLog Log = ModuleLog.Create<AddonEventManager>();
[ServiceManager.ServiceDependency]
private readonly AddonLifecycle addonLifecycle = Service<AddonLifecycle>.Get();

View file

@ -61,6 +61,11 @@ public enum AddonEventType : byte
/// </summary>
InputBaseInputReceived = 15,
/// <summary>
/// Fired at the very beginning of AtkInputManager.HandleInput on AtkStage.ViewportEventManager. Used in LovmMiniMap.
/// </summary>
RawInputData = 16,
/// <summary>
/// Focus Start.
/// </summary>
@ -107,7 +112,12 @@ public enum AddonEventType : byte
SliderReleased = 30,
/// <summary>
/// AtkComponentList RollOver.
/// AtkComponentList Button Press.
/// </summary>
ListButtonPress = 31,
/// <summary>
/// AtkComponentList Roll Over.
/// </summary>
ListItemRollOver = 33,
@ -126,11 +136,31 @@ public enum AddonEventType : byte
/// </summary>
ListItemDoubleClick = 36,
/// <summary>
/// AtkComponentList Highlight.
/// </summary>
ListItemHighlight = 37,
/// <summary>
/// AtkComponentList Select.
/// </summary>
ListItemSelect = 38,
/// <summary>
/// AtkComponentList Pad Drag Drop Begin.
/// </summary>
ListItemPadDragDropBegin = 40,
/// <summary>
/// AtkComponentList Pad Drag Drop End.
/// </summary>
ListItemPadDragDropEnd = 41,
/// <summary>
/// AtkComponentList Pad Drag Drop Insert.
/// </summary>
ListItemPadDragDropInsert = 42,
/// <summary>
/// AtkComponentDragDrop Begin.
/// Sent on MouseDown over a draggable icon (will NOT send for a locked icon).
@ -142,12 +172,22 @@ public enum AddonEventType : byte
/// </summary>
DragDropEnd = 51,
/// <summary>
/// AtkComponentDragDrop Insert Attempt.
/// </summary>
DragDropInsertAttempt = 52,
/// <summary>
/// AtkComponentDragDrop Insert.
/// Sent when dropping an icon into a hotbar/inventory slot or similar.
/// </summary>
DragDropInsert = 53,
/// <summary>
/// AtkComponentDragDrop Can Accept Check.
/// </summary>
DragDropCanAcceptCheck = 54,
/// <summary>
/// AtkComponentDragDrop Roll Over.
/// </summary>
@ -165,23 +205,18 @@ public enum AddonEventType : byte
DragDropDiscard = 57,
/// <summary>
/// Drag Drop Unknown.
/// AtkComponentDragDrop Click.
/// Sent on MouseUp if the cursor has not moved since DragDropBegin, OR on MouseDown over a locked icon.
/// </summary>
[Obsolete("Use DragDropDiscard", true)]
DragDropUnk54 = 54,
DragDropClick = 58,
/// <summary>
/// AtkComponentDragDrop Cancel.
/// Sent on MouseUp if the cursor has not moved since DragDropBegin, OR on MouseDown over a locked icon.
/// </summary>
[Obsolete("Renamed to DragDropClick")]
DragDropCancel = 58,
/// <summary>
/// Drag Drop Unknown.
/// </summary>
[Obsolete("Use DragDropCancel", true)]
DragDropUnk55 = 55,
/// <summary>
/// AtkComponentIconText Roll Over.
/// </summary>
@ -217,6 +252,11 @@ public enum AddonEventType : byte
/// </summary>
TimerEnd = 65,
/// <summary>
/// AtkTimer Start.
/// </summary>
TimerStart = 66,
/// <summary>
/// AtkSimpleTween Progress.
/// </summary>
@ -247,6 +287,11 @@ public enum AddonEventType : byte
/// </summary>
WindowChangeScale = 72,
/// <summary>
/// AtkTimeline Active Label Changed.
/// </summary>
TimelineActiveLabelChanged = 75,
/// <summary>
/// AtkTextNode Link Mouse Click.
/// </summary>

View file

@ -5,7 +5,6 @@ using Dalamud.Game.Addon.Events.EventDataTypes;
using Dalamud.Game.Gui;
using Dalamud.Logging.Internal;
using Dalamud.Plugin.Services;
using Dalamud.Utility;
using FFXIVClientStructs.FFXIV.Component.GUI;
@ -16,7 +15,7 @@ namespace Dalamud.Game.Addon.Events;
/// </summary>
internal unsafe class PluginEventController : IDisposable
{
private static readonly ModuleLog Log = new("AddonEventManager");
private static readonly ModuleLog Log = ModuleLog.Create<AddonEventManager>();
/// <summary>
/// Initializes a new instance of the <see cref="PluginEventController"/> class.
@ -28,7 +27,7 @@ internal unsafe class PluginEventController : IDisposable
private AddonEventListener EventListener { get; init; }
private List<AddonEventEntry> Events { get; } = new();
private List<AddonEventEntry> Events { get; } = [];
/// <summary>
/// Adds a tracked event.

View file

@ -0,0 +1,22 @@
namespace Dalamud.Game.Addon.Lifecycle.AddonArgTypes;
/// <summary>
/// Addon argument data for OnFocusChanged events.
/// </summary>
public class AddonFocusChangedArgs : AddonArgs
{
/// <summary>
/// Initializes a new instance of the <see cref="AddonFocusChangedArgs"/> class.
/// </summary>
internal AddonFocusChangedArgs()
{
}
/// <inheritdoc/>
public override AddonArgsType Type => AddonArgsType.FocusChanged;
/// <summary>
/// Gets or sets a value indicating whether the window is being focused or unfocused.
/// </summary>
public bool ShouldFocus { get; set; }
}

View file

@ -44,4 +44,9 @@ public enum AddonArgsType
/// Contains argument data for Close.
/// </summary>
Close,
/// <summary>
/// Contains argument data for OnFocusChanged.
/// </summary>
FocusChanged,
}

View file

@ -203,4 +203,14 @@ public enum AddonEvent
/// Be aware this is only called for certain popup windows, it is not triggered when clicking on windows.
/// </remarks>
PostFocus,
/// <summary>
/// An event that is fired before an addon processes its FocusChanged method.
/// </summary>
PreFocusChanged,
/// <summary>
/// An event that is fired after a addon processes its FocusChanged method.
/// </summary>
PostFocusChanged,
}

View file

@ -25,9 +25,13 @@ internal unsafe class AddonLifecycle : IInternalDisposableService
/// </summary>
public static readonly List<AddonVirtualTable> AllocatedTables = [];
private static readonly ModuleLog Log = new("AddonLifecycle");
private static readonly ModuleLog Log = ModuleLog.Create<AddonLifecycle>();
[ServiceManager.ServiceDependency]
private readonly Framework framework = Service<Framework>.Get();
private Hook<AtkUnitBase.Delegates.Initialize>? onInitializeAddonHook;
private bool isInvokingListeners;
[ServiceManager.ServiceConstructor]
private AddonLifecycle()
@ -52,26 +56,36 @@ internal unsafe class AddonLifecycle : IInternalDisposableService
AllocatedTables.Clear();
}
/// <summary>
/// Resolves a virtual table address to the original virtual table address.
/// </summary>
/// <param name="tableAddress">The modified address to resolve.</param>
/// <returns>The original address.</returns>
internal static AtkUnitBase.AtkUnitBaseVirtualTable* GetOriginalVirtualTable(AtkUnitBase.AtkUnitBaseVirtualTable* tableAddress)
{
var matchedTable = AllocatedTables.FirstOrDefault(table => table.ModifiedVirtualTable == tableAddress);
if (matchedTable == null)
{
return null;
}
return matchedTable.OriginalVirtualTable;
}
/// <summary>
/// Register a listener for the target event and addon.
/// </summary>
/// <param name="listener">The listener to register.</param>
internal void RegisterListener(AddonLifecycleEventListener listener)
{
if (!this.EventListeners.ContainsKey(listener.EventType))
if (this.isInvokingListeners)
{
if (!this.EventListeners.TryAdd(listener.EventType, []))
return;
this.framework.RunOnTick(() => this.RegisterListenerMethod(listener));
}
// Note: string.Empty is a valid addon name, as that will trigger on any addon for this event type
if (!this.EventListeners[listener.EventType].ContainsKey(listener.AddonName))
else
{
if (!this.EventListeners[listener.EventType].TryAdd(listener.AddonName, []))
return;
this.framework.RunOnFrameworkThread(() => this.RegisterListenerMethod(listener));
}
this.EventListeners[listener.EventType][listener.AddonName].Add(listener);
}
/// <summary>
@ -80,12 +94,13 @@ internal unsafe class AddonLifecycle : IInternalDisposableService
/// <param name="listener">The listener to unregister.</param>
internal void UnregisterListener(AddonLifecycleEventListener listener)
{
if (this.EventListeners.TryGetValue(listener.EventType, out var addonListeners))
if (this.isInvokingListeners)
{
if (addonListeners.TryGetValue(listener.AddonName, out var addonListener))
{
addonListener.Remove(listener);
}
this.framework.RunOnTick(() => this.UnregisterListenerMethod(listener));
}
else
{
this.framework.RunOnFrameworkThread(() => this.UnregisterListenerMethod(listener));
}
}
@ -97,6 +112,8 @@ internal unsafe class AddonLifecycle : IInternalDisposableService
/// <param name="blame">What to blame on errors.</param>
internal void InvokeListenersSafely(AddonEvent eventType, AddonArgs args, [CallerMemberName] string blame = "")
{
this.isInvokingListeners = true;
// Early return if we don't have any listeners of this type
if (!this.EventListeners.TryGetValue(eventType, out var addonListeners)) return;
@ -131,19 +148,41 @@ internal unsafe class AddonLifecycle : IInternalDisposableService
}
}
}
this.isInvokingListeners = false;
}
/// <summary>
/// Resolves a virtual table address to the original virtual table address.
/// </summary>
/// <param name="tableAddress">The modified address to resolve.</param>
/// <returns>The original address.</returns>
internal AtkUnitBase.AtkUnitBaseVirtualTable* GetOriginalVirtualTable(AtkUnitBase.AtkUnitBaseVirtualTable* tableAddress)
private void RegisterListenerMethod(AddonLifecycleEventListener listener)
{
var matchedTable = AllocatedTables.FirstOrDefault(table => table.ModifiedVirtualTable == tableAddress);
if (matchedTable == null) return null;
if (!this.EventListeners.ContainsKey(listener.EventType))
{
if (!this.EventListeners.TryAdd(listener.EventType, []))
{
return;
}
}
return matchedTable.OriginalVirtualTable;
// Note: string.Empty is a valid addon name, as that will trigger on any addon for this event type
if (!this.EventListeners[listener.EventType].ContainsKey(listener.AddonName))
{
if (!this.EventListeners[listener.EventType].TryAdd(listener.AddonName, []))
{
return;
}
}
this.EventListeners[listener.EventType][listener.AddonName].Add(listener);
}
private void UnregisterListenerMethod(AddonLifecycleEventListener listener)
{
if (this.EventListeners.TryGetValue(listener.EventType, out var addonListeners))
{
if (addonListeners.TryGetValue(listener.AddonName, out var addonListener))
{
addonListener.Remove(listener);
}
}
}
private void OnAddonInitialize(AtkUnitBase* addon)
@ -263,5 +302,5 @@ internal class AddonLifecyclePluginScoped : IInternalDisposableService, IAddonLi
/// <inheritdoc/>
public unsafe nint GetOriginalVirtualTable(nint virtualTableAddress)
=> (nint)this.addonLifecycleService.GetOriginalVirtualTable((AtkUnitBase.AtkUnitBaseVirtualTable*)virtualTableAddress);
=> (nint)AddonLifecycle.GetOriginalVirtualTable((AtkUnitBase.AtkUnitBaseVirtualTable*)virtualTableAddress);
}

View file

@ -42,6 +42,7 @@ internal unsafe class AddonVirtualTable : IDisposable
private readonly AddonArgs onMouseOverArgs = new();
private readonly AddonArgs onMouseOutArgs = new();
private readonly AddonArgs focusArgs = new();
private readonly AddonFocusChangedArgs focusChangedArgs = new();
private readonly AtkUnitBase* atkUnitBase;
@ -63,6 +64,7 @@ internal unsafe class AddonVirtualTable : IDisposable
private readonly AtkUnitBase.Delegates.OnMouseOver onMouseOverFunction;
private readonly AtkUnitBase.Delegates.OnMouseOut onMouseOutFunction;
private readonly AtkUnitBase.Delegates.Focus focusFunction;
private readonly AtkUnitBase.Delegates.OnFocusChange onFocusChangeFunction;
/// <summary>
/// Initializes a new instance of the <see cref="AddonVirtualTable"/> class.
@ -103,6 +105,7 @@ internal unsafe class AddonVirtualTable : IDisposable
this.onMouseOverFunction = this.OnAddonMouseOver;
this.onMouseOutFunction = this.OnAddonMouseOut;
this.focusFunction = this.OnAddonFocus;
this.onFocusChangeFunction = this.OnAddonFocusChange;
// Overwrite specific virtual table entries
this.ModifiedVirtualTable->Dtor = (delegate* unmanaged<AtkUnitBase*, byte, AtkEventListener*>)Marshal.GetFunctionPointerForDelegate(this.destructorFunction);
@ -121,6 +124,7 @@ internal unsafe class AddonVirtualTable : IDisposable
this.ModifiedVirtualTable->OnMouseOver = (delegate* unmanaged<AtkUnitBase*, void>)Marshal.GetFunctionPointerForDelegate(this.onMouseOverFunction);
this.ModifiedVirtualTable->OnMouseOut = (delegate* unmanaged<AtkUnitBase*, void>)Marshal.GetFunctionPointerForDelegate(this.onMouseOutFunction);
this.ModifiedVirtualTable->Focus = (delegate* unmanaged<AtkUnitBase*, void>)Marshal.GetFunctionPointerForDelegate(this.focusFunction);
this.ModifiedVirtualTable->OnFocusChange = (delegate* unmanaged<AtkUnitBase*, bool, void>)Marshal.GetFunctionPointerForDelegate(this.onFocusChangeFunction);
}
/// <summary>
@ -630,6 +634,36 @@ internal unsafe class AddonVirtualTable : IDisposable
}
}
private void OnAddonFocusChange(AtkUnitBase* thisPtr, bool isFocused)
{
try
{
this.LogEvent(EnableLogging);
this.focusChangedArgs.Addon = thisPtr;
this.focusChangedArgs.ShouldFocus = isFocused;
this.lifecycleService.InvokeListenersSafely(AddonEvent.PreFocusChanged, this.focusChangedArgs);
isFocused = this.focusChangedArgs.ShouldFocus;
try
{
this.OriginalVirtualTable->OnFocusChange(thisPtr, isFocused);
}
catch (Exception e)
{
Log.Error(e, "Caught exception when calling original Addon OnFocusChanged. This may be a bug in the game or another plugin hooking this method.");
}
this.lifecycleService.InvokeListenersSafely(AddonEvent.PostFocusChanged, this.focusChangedArgs);
}
catch (Exception e)
{
Log.Error(e, "Caught exception from Dalamud when attempting to process OnAddonFocusChange.");
}
}
[Conditional("DEBUG")]
private void LogEvent(bool loggingEnabled, [CallerMemberName] string caller = "")
{

View file

@ -0,0 +1,39 @@
using Dalamud.Game.NativeWrapper;
namespace Dalamud.Game.Agent.AgentArgTypes;
/// <summary>
/// Base class for AgentLifecycle AgentArgTypes.
/// </summary>
public unsafe class AgentArgs
{
/// <summary>
/// Initializes a new instance of the <see cref="AgentArgs"/> class.
/// </summary>
internal AgentArgs()
{
}
/// <summary>
/// Gets the pointer to the Agents AgentInterface*.
/// </summary>
public AgentInterfacePtr Agent { get; internal set; }
/// <summary>
/// Gets the agent id.
/// </summary>
public AgentId AgentId { get; internal set; }
/// <summary>
/// Gets the type of these args.
/// </summary>
public virtual AgentArgsType Type => AgentArgsType.Generic;
/// <summary>
/// Gets the typed pointer to the Agents AgentInterface*.
/// </summary>
/// <typeparam name="T">AgentInterface.</typeparam>
/// <returns>Typed pointer to contained Agents AgentInterface.</returns>
public T* GetAgentPointer<T>() where T : unmanaged
=> (T*)this.Agent.Address;
}

View file

@ -0,0 +1,22 @@
namespace Dalamud.Game.Agent.AgentArgTypes;
/// <summary>
/// Agent argument data for game events.
/// </summary>
public class AgentClassJobChangeArgs : AgentArgs
{
/// <summary>
/// Initializes a new instance of the <see cref="AgentClassJobChangeArgs"/> class.
/// </summary>
internal AgentClassJobChangeArgs()
{
}
/// <inheritdoc/>
public override AgentArgsType Type => AgentArgsType.ClassJobChange;
/// <summary>
/// Gets or sets a value indicating what the new ClassJob is.
/// </summary>
public byte ClassJobId { get; set; }
}

View file

@ -0,0 +1,22 @@
namespace Dalamud.Game.Agent.AgentArgTypes;
/// <summary>
/// Agent argument data for game events.
/// </summary>
public class AgentGameEventArgs : AgentArgs
{
/// <summary>
/// Initializes a new instance of the <see cref="AgentGameEventArgs"/> class.
/// </summary>
internal AgentGameEventArgs()
{
}
/// <inheritdoc/>
public override AgentArgsType Type => AgentArgsType.GameEvent;
/// <summary>
/// Gets or sets a value representing which gameEvent was triggered.
/// </summary>
public int GameEvent { get; set; }
}

View file

@ -0,0 +1,27 @@
namespace Dalamud.Game.Agent.AgentArgTypes;
/// <summary>
/// Agent argument data for game events.
/// </summary>
public class AgentLevelChangeArgs : AgentArgs
{
/// <summary>
/// Initializes a new instance of the <see cref="AgentLevelChangeArgs"/> class.
/// </summary>
internal AgentLevelChangeArgs()
{
}
/// <inheritdoc/>
public override AgentArgsType Type => AgentArgsType.LevelChange;
/// <summary>
/// Gets or sets a value indicating which ClassJob was switched to.
/// </summary>
public byte ClassJobId { get; set; }
/// <summary>
/// Gets or sets a value indicating what the new level is.
/// </summary>
public ushort Level { get; set; }
}

View file

@ -0,0 +1,37 @@
namespace Dalamud.Game.Agent.AgentArgTypes;
/// <summary>
/// Agent argument data for ReceiveEvent events.
/// </summary>
public class AgentReceiveEventArgs : AgentArgs
{
/// <summary>
/// Initializes a new instance of the <see cref="AgentReceiveEventArgs"/> class.
/// </summary>
internal AgentReceiveEventArgs()
{
}
/// <inheritdoc/>
public override AgentArgsType Type => AgentArgsType.ReceiveEvent;
/// <summary>
/// Gets or sets the AtkValue return value for this event message.
/// </summary>
public nint ReturnValue { get; set; }
/// <summary>
/// Gets or sets the AtkValue array for this event message.
/// </summary>
public nint AtkValues { get; set; }
/// <summary>
/// Gets or sets the AtkValue count for this event message.
/// </summary>
public uint ValueCount { get; set; }
/// <summary>
/// Gets or sets the event kind for this event message.
/// </summary>
public ulong EventKind { get; set; }
}

View file

@ -0,0 +1,32 @@
namespace Dalamud.Game.Agent;
/// <summary>
/// Enumeration for available AgentLifecycle arg data.
/// </summary>
public enum AgentArgsType
{
/// <summary>
/// Generic arg type that contains no meaningful data.
/// </summary>
Generic,
/// <summary>
/// Contains argument data for ReceiveEvent.
/// </summary>
ReceiveEvent,
/// <summary>
/// Contains argument data for GameEvent.
/// </summary>
GameEvent,
/// <summary>
/// Contains argument data for LevelChange.
/// </summary>
LevelChange,
/// <summary>
/// Contains argument data for ClassJobChange.
/// </summary>
ClassJobChange,
}

View file

@ -0,0 +1,87 @@
namespace Dalamud.Game.Agent;
/// <summary>
/// Enumeration for available AgentLifecycle events.
/// </summary>
public enum AgentEvent
{
/// <summary>
/// An event that is fired before the agent processes its Receive Event Function.
/// </summary>
PreReceiveEvent,
/// <summary>
/// An event that is fired after the agent has processed its Receive Event Function.
/// </summary>
PostReceiveEvent,
/// <summary>
/// An event that is fired before the agent processes its Filtered Receive Event Function.
/// </summary>
PreReceiveEventWithResult,
/// <summary>
/// An event that is fired after the agent has processed its Filtered Receive Event Function.
/// </summary>
PostReceiveEventWithResult,
/// <summary>
/// An event that is fired before the agent processes its Show Function.
/// </summary>
PreShow,
/// <summary>
/// An event that is fired after the agent has processed its Show Function.
/// </summary>
PostShow,
/// <summary>
/// An event that is fired before the agent processes its Hide Function.
/// </summary>
PreHide,
/// <summary>
/// An event that is fired after the agent has processed its Hide Function.
/// </summary>
PostHide,
/// <summary>
/// An event that is fired before the agent processes its Update Function.
/// </summary>
PreUpdate,
/// <summary>
/// An event that is fired after the agent has processed its Update Function.
/// </summary>
PostUpdate,
/// <summary>
/// An event that is fired before the agent processes its Game Event Function.
/// </summary>
PreGameEvent,
/// <summary>
/// An event that is fired after the agent has processed its Game Event Function.
/// </summary>
PostGameEvent,
/// <summary>
/// An event that is fired before the agent processes its Game Event Function.
/// </summary>
PreLevelChange,
/// <summary>
/// An event that is fired after the agent has processed its Level Change Function.
/// </summary>
PostLevelChange,
/// <summary>
/// An event that is fired before the agent processes its ClassJob Change Function.
/// </summary>
PreClassJobChange,
/// <summary>
/// An event that is fired after the agent has processed its ClassJob Change Function.
/// </summary>
PostClassJobChange,
}

View file

@ -0,0 +1,338 @@
using System.Collections.Generic;
using System.Linq;
using System.Runtime.CompilerServices;
using Dalamud.Game.Agent.AgentArgTypes;
using Dalamud.Hooking;
using Dalamud.IoC;
using Dalamud.IoC.Internal;
using Dalamud.Logging.Internal;
using Dalamud.Plugin.Services;
using FFXIVClientStructs.FFXIV.Client.UI;
using FFXIVClientStructs.FFXIV.Client.UI.Agent;
using FFXIVClientStructs.Interop;
namespace Dalamud.Game.Agent;
/// <summary>
/// This class provides events for in-game agent lifecycles.
/// </summary>
[ServiceManager.EarlyLoadedService]
internal unsafe class AgentLifecycle : IInternalDisposableService
{
/// <summary>
/// Gets a list of all allocated agent virtual tables.
/// </summary>
public static readonly List<AgentVirtualTable> AllocatedTables = [];
private static readonly ModuleLog Log = new("AgentLifecycle");
[ServiceManager.ServiceDependency]
private readonly Framework framework = Service<Framework>.Get();
private Hook<AgentModule.Delegates.Ctor>? onInitializeAgentsHook;
private bool isInvokingListeners;
[ServiceManager.ServiceConstructor]
private AgentLifecycle()
{
var agentModuleInstance = AgentModule.Instance();
// Hook is only used to determine appropriate timing for replacing Agent Virtual Tables
// If the agent module is already initialized, then we can replace the tables safely.
if (agentModuleInstance is null)
{
this.onInitializeAgentsHook = Hook<AgentModule.Delegates.Ctor>.FromAddress((nint)AgentModule.MemberFunctionPointers.Ctor, this.OnAgentModuleInitialize);
this.onInitializeAgentsHook.Enable();
}
else
{
// For safety because this might be injected async, we will make sure we are on the main thread first.
this.framework.RunOnFrameworkThread(() => this.ReplaceVirtualTables(agentModuleInstance));
}
}
/// <summary>
/// Gets a list of all AgentLifecycle Event Listeners.
/// </summary> <br/>
/// Mapping is: EventType -> ListenerList
internal Dictionary<AgentEvent, Dictionary<AgentId, HashSet<AgentLifecycleEventListener>>> EventListeners { get; } = [];
/// <inheritdoc/>
void IInternalDisposableService.DisposeService()
{
this.onInitializeAgentsHook?.Dispose();
this.onInitializeAgentsHook = null;
AllocatedTables.ForEach(entry => entry.Dispose());
AllocatedTables.Clear();
}
/// <summary>
/// Resolves a virtual table address to the original virtual table address.
/// </summary>
/// <param name="tableAddress">The modified address to resolve.</param>
/// <returns>The original address.</returns>
internal static AgentInterface.AgentInterfaceVirtualTable* GetOriginalVirtualTable(AgentInterface.AgentInterfaceVirtualTable* tableAddress)
{
var matchedTable = AllocatedTables.FirstOrDefault(table => table.ModifiedVirtualTable == tableAddress);
if (matchedTable == null)
{
return null;
}
return matchedTable.OriginalVirtualTable;
}
/// <summary>
/// Register a listener for the target event and agent.
/// </summary>
/// <param name="listener">The listener to register.</param>
internal void RegisterListener(AgentLifecycleEventListener listener)
{
if (this.isInvokingListeners)
{
this.framework.RunOnTick(() => this.RegisterListenerMethod(listener));
}
else
{
this.framework.RunOnFrameworkThread(() => this.RegisterListenerMethod(listener));
}
}
/// <summary>
/// Unregisters the listener from events.
/// </summary>
/// <param name="listener">The listener to unregister.</param>
internal void UnregisterListener(AgentLifecycleEventListener listener)
{
if (this.isInvokingListeners)
{
this.framework.RunOnTick(() => this.UnregisterListenerMethod(listener));
}
else
{
this.framework.RunOnFrameworkThread(() => this.UnregisterListenerMethod(listener));
}
}
/// <summary>
/// Invoke listeners for the specified event type.
/// </summary>
/// <param name="eventType">Event Type.</param>
/// <param name="args">AgentARgs.</param>
/// <param name="blame">What to blame on errors.</param>
internal void InvokeListenersSafely(AgentEvent eventType, AgentArgs args, [CallerMemberName] string blame = "")
{
this.isInvokingListeners = true;
// Early return if we don't have any listeners of this type
if (!this.EventListeners.TryGetValue(eventType, out var agentListeners)) return;
// Handle listeners for this event type that don't care which agent is triggering it
if (agentListeners.TryGetValue((AgentId)uint.MaxValue, out var globalListeners))
{
foreach (var listener in globalListeners)
{
try
{
listener.FunctionDelegate.Invoke(eventType, args);
}
catch (Exception e)
{
Log.Error(e, $"Exception in {blame} during {eventType} invoke, for global agent event listener.");
}
}
}
// Handle listeners that are listening for this agent and event type specifically
if (agentListeners.TryGetValue(args.AgentId, out var agentListener))
{
foreach (var listener in agentListener)
{
try
{
listener.FunctionDelegate.Invoke(eventType, args);
}
catch (Exception e)
{
Log.Error(e, $"Exception in {blame} during {eventType} invoke, for specific agent {args.AgentId}.");
}
}
}
this.isInvokingListeners = false;
}
private void OnAgentModuleInitialize(AgentModule* thisPtr, UIModule* uiModule)
{
this.onInitializeAgentsHook!.Original(thisPtr, uiModule);
try
{
this.ReplaceVirtualTables(thisPtr);
// We don't need this hook anymore, it did its job!
this.onInitializeAgentsHook!.Dispose();
this.onInitializeAgentsHook = null;
}
catch (Exception e)
{
Log.Error(e, "Exception in AgentLifecycle during AgentModule Ctor.");
}
}
private void RegisterListenerMethod(AgentLifecycleEventListener listener)
{
if (!this.EventListeners.ContainsKey(listener.EventType))
{
if (!this.EventListeners.TryAdd(listener.EventType, []))
{
return;
}
}
// Note: uint.MaxValue is a valid agent id, as that will trigger on any agent for this event type
if (!this.EventListeners[listener.EventType].ContainsKey(listener.AgentId))
{
if (!this.EventListeners[listener.EventType].TryAdd(listener.AgentId, []))
{
return;
}
}
this.EventListeners[listener.EventType][listener.AgentId].Add(listener);
}
private void UnregisterListenerMethod(AgentLifecycleEventListener listener)
{
if (this.EventListeners.TryGetValue(listener.EventType, out var agentListeners))
{
if (agentListeners.TryGetValue(listener.AgentId, out var agentListener))
{
agentListener.Remove(listener);
}
}
}
private void ReplaceVirtualTables(AgentModule* agentModule)
{
foreach (uint index in Enumerable.Range(0, agentModule->Agents.Length))
{
try
{
var agentPointer = agentModule->Agents.GetPointer((int)index);
if (agentPointer is null)
{
Log.Warning("Null Agent Found?");
continue;
}
// AgentVirtualTable class handles creating the virtual table, and overriding each of the tracked virtual functions
AllocatedTables.Add(new AgentVirtualTable(agentPointer->Value, (AgentId)index, this));
}
catch (Exception e)
{
Log.Error(e, "Exception in AgentLifecycle during ReplaceVirtualTables.");
}
}
}
}
/// <summary>
/// Plugin-scoped version of a AgentLifecycle service.
/// </summary>
[PluginInterface]
[ServiceManager.ScopedService]
#pragma warning disable SA1015
[ResolveVia<IAgentLifecycle>]
#pragma warning restore SA1015
internal class AgentLifecyclePluginScoped : IInternalDisposableService, IAgentLifecycle
{
[ServiceManager.ServiceDependency]
private readonly AgentLifecycle agentLifecycleService = Service<AgentLifecycle>.Get();
private readonly List<AgentLifecycleEventListener> eventListeners = [];
/// <inheritdoc/>
void IInternalDisposableService.DisposeService()
{
foreach (var listener in this.eventListeners)
{
this.agentLifecycleService.UnregisterListener(listener);
}
}
/// <inheritdoc/>
public void RegisterListener(AgentEvent eventType, IEnumerable<AgentId> agentIds, IAgentLifecycle.AgentEventDelegate handler)
{
foreach (var agentId in agentIds)
{
this.RegisterListener(eventType, agentId, handler);
}
}
/// <inheritdoc/>
public void RegisterListener(AgentEvent eventType, AgentId agentId, IAgentLifecycle.AgentEventDelegate handler)
{
var listener = new AgentLifecycleEventListener(eventType, agentId, handler);
this.eventListeners.Add(listener);
this.agentLifecycleService.RegisterListener(listener);
}
/// <inheritdoc/>
public void RegisterListener(AgentEvent eventType, IAgentLifecycle.AgentEventDelegate handler)
{
this.RegisterListener(eventType, (AgentId)uint.MaxValue, handler);
}
/// <inheritdoc/>
public void UnregisterListener(AgentEvent eventType, IEnumerable<AgentId> agentIds, IAgentLifecycle.AgentEventDelegate? handler = null)
{
foreach (var agentId in agentIds)
{
this.UnregisterListener(eventType, agentId, handler);
}
}
/// <inheritdoc/>
public void UnregisterListener(AgentEvent eventType, AgentId agentId, IAgentLifecycle.AgentEventDelegate? handler = null)
{
this.eventListeners.RemoveAll(entry =>
{
if (entry.EventType != eventType) return false;
if (entry.AgentId != agentId) return false;
if (handler is not null && entry.FunctionDelegate != handler) return false;
this.agentLifecycleService.UnregisterListener(entry);
return true;
});
}
/// <inheritdoc/>
public void UnregisterListener(AgentEvent eventType, IAgentLifecycle.AgentEventDelegate? handler = null)
{
this.UnregisterListener(eventType, (AgentId)uint.MaxValue, handler);
}
/// <inheritdoc/>
public void UnregisterListener(params IAgentLifecycle.AgentEventDelegate[] handlers)
{
foreach (var handler in handlers)
{
this.eventListeners.RemoveAll(entry =>
{
if (entry.FunctionDelegate != handler) return false;
this.agentLifecycleService.UnregisterListener(entry);
return true;
});
}
}
/// <inheritdoc/>
public unsafe nint GetOriginalVirtualTable(nint virtualTableAddress)
=> (nint)AgentLifecycle.GetOriginalVirtualTable((AgentInterface.AgentInterfaceVirtualTable*)virtualTableAddress);
}

View file

@ -0,0 +1,38 @@
using Dalamud.Plugin.Services;
namespace Dalamud.Game.Agent;
/// <summary>
/// This class is a helper for tracking and invoking listener delegates.
/// </summary>
public class AgentLifecycleEventListener
{
/// <summary>
/// Initializes a new instance of the <see cref="AgentLifecycleEventListener"/> class.
/// </summary>
/// <param name="eventType">Event type to listen for.</param>
/// <param name="agentId">Agent id to listen for.</param>
/// <param name="functionDelegate">Delegate to invoke.</param>
internal AgentLifecycleEventListener(AgentEvent eventType, AgentId agentId, IAgentLifecycle.AgentEventDelegate functionDelegate)
{
this.EventType = eventType;
this.AgentId = agentId;
this.FunctionDelegate = functionDelegate;
}
/// <summary>
/// Gets the agentId of the agent this listener is looking for.
/// uint.MaxValue if it wants to be called for any agent.
/// </summary>
public AgentId AgentId { get; init; }
/// <summary>
/// Gets the event type this listener is looking for.
/// </summary>
public AgentEvent EventType { get; init; }
/// <summary>
/// Gets the delegate this listener invokes.
/// </summary>
public IAgentLifecycle.AgentEventDelegate FunctionDelegate { get; init; }
}

View file

@ -0,0 +1,391 @@
using System.Diagnostics;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using System.Threading;
using Dalamud.Game.Agent.AgentArgTypes;
using Dalamud.Logging.Internal;
using FFXIVClientStructs.FFXIV.Client.System.Memory;
using FFXIVClientStructs.FFXIV.Client.UI.Agent;
using FFXIVClientStructs.FFXIV.Component.GUI;
namespace Dalamud.Game.Agent;
/// <summary>
/// Represents a class that holds references to an agents original and modified virtual table entries.
/// </summary>
internal unsafe class AgentVirtualTable : IDisposable
{
// This need to be at minimum the largest virtual table size of all agents
// Copying extra entries is not problematic, and is considered safe.
private const int VirtualTableEntryCount = 60;
private const bool EnableLogging = false;
private static readonly ModuleLog Log = new("AgentVT");
private readonly AgentLifecycle lifecycleService;
private readonly AgentId agentId;
// Each agent gets its own set of args that are used to mutate the original call when used in pre-calls
private readonly AgentReceiveEventArgs receiveEventArgs = new();
private readonly AgentReceiveEventArgs filteredReceiveEventArgs = new();
private readonly AgentArgs showArgs = new();
private readonly AgentArgs hideArgs = new();
private readonly AgentArgs updateArgs = new();
private readonly AgentGameEventArgs gameEventArgs = new();
private readonly AgentLevelChangeArgs levelChangeArgs = new();
private readonly AgentClassJobChangeArgs classJobChangeArgs = new();
private readonly AgentInterface* agentInterface;
// Pinned Function Delegates, as these functions get assigned to an unmanaged virtual table,
// the CLR needs to know they are in use, or it will invalidate them causing random crashing.
private readonly AgentInterface.Delegates.ReceiveEvent receiveEventFunction;
private readonly AgentInterface.Delegates.ReceiveEventWithResult receiveEventWithResultFunction;
private readonly AgentInterface.Delegates.Show showFunction;
private readonly AgentInterface.Delegates.Hide hideFunction;
private readonly AgentInterface.Delegates.Update updateFunction;
private readonly AgentInterface.Delegates.OnGameEvent gameEventFunction;
private readonly AgentInterface.Delegates.OnLevelChange levelChangeFunction;
private readonly AgentInterface.Delegates.OnClassJobChange classJobChangeFunction;
/// <summary>
/// Initializes a new instance of the <see cref="AgentVirtualTable"/> class.
/// </summary>
/// <param name="agent">AgentInterface* for the agent to replace the table of.</param>
/// <param name="agentId">Agent ID.</param>
/// <param name="lifecycleService">Reference to AgentLifecycle service to callback and invoke listeners.</param>
internal AgentVirtualTable(AgentInterface* agent, AgentId agentId, AgentLifecycle lifecycleService)
{
this.agentInterface = agent;
this.agentId = agentId;
this.lifecycleService = lifecycleService;
// Save original virtual table
this.OriginalVirtualTable = agent->VirtualTable;
// Create copy of original table
// Note this will copy any derived/overriden functions that this specific agent has.
// Note: currently there are 16 virtual functions, but there's no harm in copying more for when they add new virtual functions to the game
this.ModifiedVirtualTable = (AgentInterface.AgentInterfaceVirtualTable*)IMemorySpace.GetUISpace()->Malloc(0x8 * VirtualTableEntryCount, 8);
NativeMemory.Copy(agent->VirtualTable, this.ModifiedVirtualTable, 0x8 * VirtualTableEntryCount);
// Overwrite the agents existing virtual table with our own
agent->VirtualTable = this.ModifiedVirtualTable;
// Pin each of our listener functions
this.receiveEventFunction = this.OnAgentReceiveEvent;
this.receiveEventWithResultFunction = this.OnAgentReceiveEventWithResult;
this.showFunction = this.OnAgentShow;
this.hideFunction = this.OnAgentHide;
this.updateFunction = this.OnAgentUpdate;
this.gameEventFunction = this.OnAgentGameEvent;
this.levelChangeFunction = this.OnAgentLevelChange;
this.classJobChangeFunction = this.OnClassJobChange;
// Overwrite specific virtual table entries
this.ModifiedVirtualTable->ReceiveEvent = (delegate* unmanaged<AgentInterface*, AtkValue*, AtkValue*, uint, ulong, AtkValue*>)Marshal.GetFunctionPointerForDelegate(this.receiveEventFunction);
this.ModifiedVirtualTable->ReceiveEventWithResult = (delegate* unmanaged<AgentInterface*, AtkValue*, AtkValue*, uint, ulong, AtkValue*>)Marshal.GetFunctionPointerForDelegate(this.receiveEventWithResultFunction);
this.ModifiedVirtualTable->Show = (delegate* unmanaged<AgentInterface*, void>)Marshal.GetFunctionPointerForDelegate(this.showFunction);
this.ModifiedVirtualTable->Hide = (delegate* unmanaged<AgentInterface*, void>)Marshal.GetFunctionPointerForDelegate(this.hideFunction);
this.ModifiedVirtualTable->Update = (delegate* unmanaged<AgentInterface*, uint, void>)Marshal.GetFunctionPointerForDelegate(this.updateFunction);
this.ModifiedVirtualTable->OnGameEvent = (delegate* unmanaged<AgentInterface*, AgentInterface.GameEvent, void>)Marshal.GetFunctionPointerForDelegate(this.gameEventFunction);
this.ModifiedVirtualTable->OnLevelChange = (delegate* unmanaged<AgentInterface*, byte, ushort, void>)Marshal.GetFunctionPointerForDelegate(this.levelChangeFunction);
this.ModifiedVirtualTable->OnClassJobChange = (delegate* unmanaged<AgentInterface*, byte, void>)Marshal.GetFunctionPointerForDelegate(this.classJobChangeFunction);
}
/// <summary>
/// Gets the original virtual table address for this agent.
/// </summary>
internal AgentInterface.AgentInterfaceVirtualTable* OriginalVirtualTable { get; private set; }
/// <summary>
/// Gets the modified virtual address for this agent.
/// </summary>
internal AgentInterface.AgentInterfaceVirtualTable* ModifiedVirtualTable { get; private set; }
/// <inheritdoc/>
public void Dispose()
{
// Ensure restoration is done atomically.
Interlocked.Exchange(ref *(nint*)&this.agentInterface->VirtualTable, (nint)this.OriginalVirtualTable);
IMemorySpace.Free(this.ModifiedVirtualTable, 0x8 * VirtualTableEntryCount);
}
private AtkValue* OnAgentReceiveEvent(AgentInterface* thisPtr, AtkValue* returnValue, AtkValue* values, uint valueCount, ulong eventKind)
{
AtkValue* result = null;
try
{
this.LogEvent(EnableLogging);
this.receiveEventArgs.Agent = thisPtr;
this.receiveEventArgs.AgentId = this.agentId;
this.receiveEventArgs.ReturnValue = (nint)returnValue;
this.receiveEventArgs.AtkValues = (nint)values;
this.receiveEventArgs.ValueCount = valueCount;
this.receiveEventArgs.EventKind = eventKind;
this.lifecycleService.InvokeListenersSafely(AgentEvent.PreReceiveEvent, this.receiveEventArgs);
returnValue = (AtkValue*)this.receiveEventArgs.ReturnValue;
values = (AtkValue*)this.receiveEventArgs.AtkValues;
valueCount = this.receiveEventArgs.ValueCount;
eventKind = this.receiveEventArgs.EventKind;
try
{
result = this.OriginalVirtualTable->ReceiveEvent(thisPtr, returnValue, values, valueCount, eventKind);
}
catch (Exception e)
{
Log.Error(e, "Caught exception when calling original Agent ReceiveEvent. This may be a bug in the game or another plugin hooking this method.");
}
this.lifecycleService.InvokeListenersSafely(AgentEvent.PostReceiveEvent, this.receiveEventArgs);
}
catch (Exception e)
{
Log.Error(e, "Caught exception from Dalamud when attempting to process OnAgentReceiveEvent.");
}
return result;
}
private AtkValue* OnAgentReceiveEventWithResult(AgentInterface* thisPtr, AtkValue* returnValue, AtkValue* values, uint valueCount, ulong eventKind)
{
AtkValue* result = null;
try
{
this.LogEvent(EnableLogging);
this.filteredReceiveEventArgs.Agent = thisPtr;
this.filteredReceiveEventArgs.AgentId = this.agentId;
this.filteredReceiveEventArgs.ReturnValue = (nint)returnValue;
this.filteredReceiveEventArgs.AtkValues = (nint)values;
this.filteredReceiveEventArgs.ValueCount = valueCount;
this.filteredReceiveEventArgs.EventKind = eventKind;
this.lifecycleService.InvokeListenersSafely(AgentEvent.PreReceiveEventWithResult, this.filteredReceiveEventArgs);
returnValue = (AtkValue*)this.filteredReceiveEventArgs.ReturnValue;
values = (AtkValue*)this.filteredReceiveEventArgs.AtkValues;
valueCount = this.filteredReceiveEventArgs.ValueCount;
eventKind = this.filteredReceiveEventArgs.EventKind;
try
{
result = this.OriginalVirtualTable->ReceiveEventWithResult(thisPtr, returnValue, values, valueCount, eventKind);
}
catch (Exception e)
{
Log.Error(e, "Caught exception when calling original Agent FilteredReceiveEvent. This may be a bug in the game or another plugin hooking this method.");
}
this.lifecycleService.InvokeListenersSafely(AgentEvent.PostReceiveEventWithResult, this.filteredReceiveEventArgs);
}
catch (Exception e)
{
Log.Error(e, "Caught exception from Dalamud when attempting to process OnAgentReceiveEventWithResult.");
}
return result;
}
private void OnAgentShow(AgentInterface* thisPtr)
{
try
{
this.LogEvent(EnableLogging);
this.showArgs.Agent = thisPtr;
this.showArgs.AgentId = this.agentId;
this.lifecycleService.InvokeListenersSafely(AgentEvent.PreShow, this.showArgs);
try
{
this.OriginalVirtualTable->Show(thisPtr);
}
catch (Exception e)
{
Log.Error(e, "Caught exception when calling original Addon Show. This may be a bug in the game or another plugin hooking this method.");
}
this.lifecycleService.InvokeListenersSafely(AgentEvent.PostShow, this.showArgs);
}
catch (Exception e)
{
Log.Error(e, "Caught exception from Dalamud when attempting to process OnAgentShow.");
}
}
private void OnAgentHide(AgentInterface* thisPtr)
{
try
{
this.LogEvent(EnableLogging);
this.hideArgs.Agent = thisPtr;
this.hideArgs.AgentId = this.agentId;
this.lifecycleService.InvokeListenersSafely(AgentEvent.PreHide, this.hideArgs);
try
{
this.OriginalVirtualTable->Hide(thisPtr);
}
catch (Exception e)
{
Log.Error(e, "Caught exception when calling original Addon Hide. This may be a bug in the game or another plugin hooking this method.");
}
this.lifecycleService.InvokeListenersSafely(AgentEvent.PostHide, this.hideArgs);
}
catch (Exception e)
{
Log.Error(e, "Caught exception from Dalamud when attempting to process OnAgentHide.");
}
}
private void OnAgentUpdate(AgentInterface* thisPtr, uint frameCount)
{
try
{
this.LogEvent(EnableLogging);
this.updateArgs.Agent = thisPtr;
this.updateArgs.AgentId = this.agentId;
this.lifecycleService.InvokeListenersSafely(AgentEvent.PreUpdate, this.updateArgs);
try
{
this.OriginalVirtualTable->Update(thisPtr, frameCount);
}
catch (Exception e)
{
Log.Error(e, "Caught exception when calling original Addon Update. This may be a bug in the game or another plugin hooking this method.");
}
this.lifecycleService.InvokeListenersSafely(AgentEvent.PostUpdate, this.updateArgs);
}
catch (Exception e)
{
Log.Error(e, "Caught exception from Dalamud when attempting to process OnAgentUpdate.");
}
}
private void OnAgentGameEvent(AgentInterface* thisPtr, AgentInterface.GameEvent gameEvent)
{
try
{
this.LogEvent(EnableLogging);
this.gameEventArgs.Agent = thisPtr;
this.gameEventArgs.AgentId = this.agentId;
this.gameEventArgs.GameEvent = (int)gameEvent;
this.lifecycleService.InvokeListenersSafely(AgentEvent.PreGameEvent, this.gameEventArgs);
gameEvent = (AgentInterface.GameEvent)this.gameEventArgs.GameEvent;
try
{
this.OriginalVirtualTable->OnGameEvent(thisPtr, gameEvent);
}
catch (Exception e)
{
Log.Error(e, "Caught exception when calling original Addon OnGameEvent. This may be a bug in the game or another plugin hooking this method.");
}
this.lifecycleService.InvokeListenersSafely(AgentEvent.PostGameEvent, this.gameEventArgs);
}
catch (Exception e)
{
Log.Error(e, "Caught exception from Dalamud when attempting to process OnAgentGameEvent.");
}
}
private void OnAgentLevelChange(AgentInterface* thisPtr, byte classJobId, ushort level)
{
try
{
this.LogEvent(EnableLogging);
this.levelChangeArgs.Agent = thisPtr;
this.levelChangeArgs.AgentId = this.agentId;
this.levelChangeArgs.ClassJobId = classJobId;
this.levelChangeArgs.Level = level;
this.lifecycleService.InvokeListenersSafely(AgentEvent.PreLevelChange, this.levelChangeArgs);
classJobId = this.levelChangeArgs.ClassJobId;
level = this.levelChangeArgs.Level;
try
{
this.OriginalVirtualTable->OnLevelChange(thisPtr, classJobId, level);
}
catch (Exception e)
{
Log.Error(e, "Caught exception when calling original Addon OnLevelChange. This may be a bug in the game or another plugin hooking this method.");
}
this.lifecycleService.InvokeListenersSafely(AgentEvent.PostLevelChange, this.levelChangeArgs);
}
catch (Exception e)
{
Log.Error(e, "Caught exception from Dalamud when attempting to process OnAgentLevelChange.");
}
}
private void OnClassJobChange(AgentInterface* thisPtr, byte classJobId)
{
try
{
this.LogEvent(EnableLogging);
this.classJobChangeArgs.Agent = thisPtr;
this.classJobChangeArgs.AgentId = this.agentId;
this.classJobChangeArgs.ClassJobId = classJobId;
this.lifecycleService.InvokeListenersSafely(AgentEvent.PreClassJobChange, this.classJobChangeArgs);
classJobId = this.classJobChangeArgs.ClassJobId;
try
{
this.OriginalVirtualTable->OnClassJobChange(thisPtr, classJobId);
}
catch (Exception e)
{
Log.Error(e, "Caught exception when calling original Addon OnClassJobChange. This may be a bug in the game or another plugin hooking this method.");
}
this.lifecycleService.InvokeListenersSafely(AgentEvent.PostClassJobChange, this.classJobChangeArgs);
}
catch (Exception e)
{
Log.Error(e, "Caught exception from Dalamud when attempting to process OnClassJobChange.");
}
}
[Conditional("DEBUG")]
private void LogEvent(bool loggingEnabled, [CallerMemberName] string caller = "")
{
if (loggingEnabled)
{
// Manually disable the really spammy log events, you can comment this out if you need to debug them.
if (caller is "OnAgentUpdate" || this.agentId is AgentId.PadMouseMode)
return;
Log.Debug($"[{caller}]: {this.agentId}");
}
}
}

View file

@ -14,7 +14,7 @@ public abstract class BaseAddressResolver
/// <summary>
/// Gets a list of memory addresses that were found, to list in /xldata.
/// </summary>
public static Dictionary<string, List<(string ClassName, IntPtr Address)>> DebugScannedValues { get; } = new();
public static Dictionary<string, List<(string ClassName, IntPtr Address)>> DebugScannedValues { get; } = [];
/// <summary>
/// Gets or sets a value indicating whether the resolver has successfully run <see cref="Setup32Bit(ISigScanner)"/> or <see cref="Setup64Bit(ISigScanner)"/>.

View file

@ -0,0 +1,221 @@
using System.Diagnostics.CodeAnalysis;
using Dalamud.Data;
using Dalamud.Utility;
using FFXIVClientStructs.FFXIV.Client.System.String;
using FFXIVClientStructs.FFXIV.Client.UI.Agent;
using FFXIVClientStructs.FFXIV.Client.UI.Misc;
using FFXIVClientStructs.FFXIV.Component.Text;
using FFXIVClientStructs.Interop;
using Lumina.Excel;
using Lumina.Text.ReadOnly;
namespace Dalamud.Game.Chat;
/// <summary>
/// Interface representing a log message.
/// </summary>
public interface ILogMessage : IEquatable<ILogMessage>
{
/// <summary>
/// Gets the address of the log message in memory.
/// </summary>
nint Address { get; }
/// <summary>
/// Gets the ID of this log message.
/// </summary>
uint LogMessageId { get; }
/// <summary>
/// Gets the GameData associated with this log message.
/// </summary>
RowRef<Lumina.Excel.Sheets.LogMessage> GameData { get; }
/// <summary>
/// Gets the entity that is the source of this log message, if any.
/// </summary>
ILogMessageEntity? SourceEntity { get; }
/// <summary>
/// Gets the entity that is the target of this log message, if any.
/// </summary>
ILogMessageEntity? TargetEntity { get; }
/// <summary>
/// Gets the number of parameters.
/// </summary>
int ParameterCount { get; }
/// <summary>
/// Retrieves the value of a parameter for the log message if it is an int.
/// </summary>
/// <param name="index">The index of the parameter to retrieve.</param>
/// <param name="value">The value of the parameter.</param>
/// <returns><see langword="true"/> if the parameter was retrieved successfully.</returns>
bool TryGetIntParameter(int index, out int value);
/// <summary>
/// Retrieves the value of a parameter for the log message if it is a string.
/// </summary>
/// <param name="index">The index of the parameter to retrieve.</param>
/// <param name="value">The value of the parameter.</param>
/// <returns><see langword="true"/> if the parameter was retrieved successfully.</returns>
bool TryGetStringParameter(int index, out ReadOnlySeString value);
/// <summary>
/// Formats this log message into an approximation of the string that will eventually be shown in the log.
/// </summary>
/// <remarks>This can cause side effects such as playing sound effects and thus should only be used for debugging.</remarks>
/// <returns>The formatted string.</returns>
ReadOnlySeString FormatLogMessageForDebugging();
}
/// <summary>
/// This struct represents log message in the queue to be added to the chat.
/// </summary>
/// <param name="ptr">A pointer to the log message.</param>
internal unsafe readonly struct LogMessage(LogMessageQueueItem* ptr) : ILogMessage
{
/// <inheritdoc/>
public nint Address => (nint)ptr;
/// <inheritdoc/>
public uint LogMessageId => ptr->LogMessageId;
/// <inheritdoc/>
public RowRef<Lumina.Excel.Sheets.LogMessage> GameData => LuminaUtils.CreateRef<Lumina.Excel.Sheets.LogMessage>(ptr->LogMessageId);
/// <inheritdoc/>
ILogMessageEntity? ILogMessage.SourceEntity => ptr->SourceKind == EntityRelationKind.None ? null : this.SourceEntity;
/// <inheritdoc/>
ILogMessageEntity? ILogMessage.TargetEntity => ptr->TargetKind == EntityRelationKind.None ? null : this.TargetEntity;
/// <inheritdoc/>
public int ParameterCount => ptr->Parameters.Count;
private LogMessageEntity SourceEntity => new(ptr, true);
private LogMessageEntity TargetEntity => new(ptr, false);
public static bool operator ==(LogMessage x, LogMessage y) => x.Equals(y);
public static bool operator !=(LogMessage x, LogMessage y) => !(x == y);
/// <inheritdoc/>
public bool Equals(ILogMessage? other)
{
return other is LogMessage logMessage && this.Equals(logMessage);
}
/// <inheritdoc/>
public override bool Equals([NotNullWhen(true)] object? obj)
{
return obj is LogMessage logMessage && this.Equals(logMessage);
}
/// <inheritdoc/>
public override int GetHashCode()
{
return HashCode.Combine(this.LogMessageId, this.SourceEntity, this.TargetEntity);
}
/// <inheritdoc/>
public bool TryGetIntParameter(int index, out int value)
{
value = 0;
if (!this.TryGetParameter(index, out var parameter)) return false;
if (parameter.Type != TextParameterType.Integer) return false;
value = parameter.IntValue;
return true;
}
/// <inheritdoc/>
public bool TryGetStringParameter(int index, out ReadOnlySeString value)
{
value = default;
if (!this.TryGetParameter(index, out var parameter)) return false;
if (parameter.Type == TextParameterType.String)
{
value = new(parameter.StringValue.AsSpan());
return true;
}
if (parameter.Type == TextParameterType.ReferencedUtf8String)
{
value = new(parameter.ReferencedUtf8StringValue->Utf8String.AsSpan());
return true;
}
return false;
}
/// <inheritdoc/>
public ReadOnlySeString FormatLogMessageForDebugging()
{
var logModule = RaptureLogModule.Instance();
// the formatting logic is taken from RaptureLogModule_Update
using var utf8 = new Utf8String();
SetName(logModule, this.SourceEntity);
SetName(logModule, this.TargetEntity);
using var rssb = new RentedSeStringBuilder();
logModule->RaptureTextModule->FormatString(rssb.Builder.Append(this.GameData.Value.Text).GetViewAsSpan(), &ptr->Parameters, &utf8);
return new ReadOnlySeString(utf8.AsSpan());
static void SetName(RaptureLogModule* self, LogMessageEntity item)
{
var name = item.NameSpan.GetPointer(0);
if (item.IsPlayer)
{
var str = self->TempParseMessage.GetPointer(item.IsSourceEntity ? 8 : 9);
self->FormatPlayerLink(name, str, null, 0, item.Kind != 1 /* LocalPlayer */, item.HomeWorldId, false, null, false);
if (item.HomeWorldId != 0 && item.HomeWorldId != AgentLobby.Instance()->LobbyData.HomeWorldId)
{
var crossWorldSymbol = self->RaptureTextModule->UnkStrings0.GetPointer(3);
if (!crossWorldSymbol->StringPtr.HasValue)
self->RaptureTextModule->ProcessMacroCode(crossWorldSymbol, "<icon(88)>\0"u8);
str->Append(crossWorldSymbol);
if (self->UIModule->GetWorldHelper()->AllWorlds.TryGetValuePointer(item.HomeWorldId, out var world))
str->ConcatCStr(world->Name);
}
name = str->StringPtr;
}
if (item.IsSourceEntity)
{
self->RaptureTextModule->SetGlobalTempEntity1(name, item.Sex, item.ObjStrId);
}
else
{
self->RaptureTextModule->SetGlobalTempEntity2(name, item.Sex, item.ObjStrId);
}
}
}
private bool TryGetParameter(int index, out TextParameter value)
{
if (index < 0 || index >= ptr->Parameters.Count)
{
value = default;
return false;
}
value = ptr->Parameters[index];
return true;
}
private bool Equals(LogMessage other)
{
return this.LogMessageId == other.LogMessageId && this.SourceEntity == other.SourceEntity && this.TargetEntity == other.TargetEntity;
}
}

View file

@ -0,0 +1,113 @@
using System.Diagnostics.CodeAnalysis;
using Dalamud.Data;
using Dalamud.Plugin.Services;
using FFXIVClientStructs.FFXIV.Client.UI.Misc;
using Lumina.Excel;
using Lumina.Excel.Sheets;
using Lumina.Text.ReadOnly;
namespace Dalamud.Game.Chat;
/// <summary>
/// Interface representing an entity related to a log message.
/// </summary>
public interface ILogMessageEntity : IEquatable<ILogMessageEntity>
{
/// <summary>
/// Gets the name of this entity.
/// </summary>
ReadOnlySeString Name { get; }
/// <summary>
/// Gets the ID of the homeworld of this entity, if it is a player.
/// </summary>
ushort HomeWorldId { get; }
/// <summary>
/// Gets the homeworld of this entity, if it is a player.
/// </summary>
RowRef<World> HomeWorld { get; }
/// <summary>
/// Gets the ObjStr ID of this entity, if not a player. See <seealso cref="ISeStringEvaluator.EvaluateObjStr"/>.
/// </summary>
uint ObjStrId { get; }
/// <summary>
/// Gets a value indicating whether this entity is a player.
/// </summary>
bool IsPlayer { get; }
}
/// <summary>
/// This struct represents an entity related to a log message.
/// </summary>
/// <param name="ptr">A pointer to the log message item.</param>
/// <param name="source">If <see langword="true"/> represents the source entity of the log message, otherwise represents the target entity.</param>
internal unsafe readonly struct LogMessageEntity(LogMessageQueueItem* ptr, bool source) : ILogMessageEntity
{
/// <inheritdoc/>
public ReadOnlySeString Name => new(this.NameSpan[..this.NameSpan.IndexOf((byte)0)]);
/// <inheritdoc/>
public ushort HomeWorldId => source ? ptr->SourceHomeWorld : ptr->TargetHomeWorld;
/// <inheritdoc/>
public RowRef<World> HomeWorld => LuminaUtils.CreateRef<World>(this.HomeWorldId);
/// <inheritdoc/>
public uint ObjStrId => source ? ptr->SourceObjStrId : ptr->TargetObjStrId;
/// <inheritdoc/>
public bool IsPlayer => source ? ptr->SourceIsPlayer : ptr->TargetIsPlayer;
/// <summary>
/// Gets the Span containing the raw name of this entity.
/// </summary>
internal Span<byte> NameSpan => source ? ptr->SourceName : ptr->TargetName;
/// <summary>
/// Gets the kind of the entity.
/// </summary>
internal byte Kind => source ? (byte)ptr->SourceKind : (byte)ptr->TargetKind;
/// <summary>
/// Gets the Sex of this entity.
/// </summary>
internal byte Sex => source ? ptr->SourceSex : ptr->TargetSex;
/// <summary>
/// Gets a value indicating whether this entity is the source entity of a log message.
/// </summary>
internal bool IsSourceEntity => source;
public static bool operator ==(LogMessageEntity x, LogMessageEntity y) => x.Equals(y);
public static bool operator !=(LogMessageEntity x, LogMessageEntity y) => !(x == y);
/// <inheritdoc/>
public bool Equals(ILogMessageEntity other)
{
return other is LogMessageEntity entity && this.Equals(entity);
}
/// <inheritdoc/>
public override bool Equals([NotNullWhen(true)] object? obj)
{
return obj is LogMessageEntity entity && this.Equals(entity);
}
/// <inheritdoc/>
public override int GetHashCode()
{
return HashCode.Combine(this.Name, this.HomeWorldId, this.ObjStrId, this.Sex, this.IsPlayer);
}
private bool Equals(LogMessageEntity other)
{
return this.Name == other.Name && this.HomeWorldId == other.HomeWorldId && this.ObjStrId == other.ObjStrId && this.Kind == other.Kind && this.Sex == other.Sex && this.IsPlayer == other.IsPlayer;
}
}

View file

@ -1,5 +1,4 @@
using System.Linq;
using System.Reflection;
using System.Text.RegularExpressions;
using CheapLoc;
@ -23,7 +22,7 @@ namespace Dalamud.Game;
[ServiceManager.EarlyLoadedService]
internal partial class ChatHandlers : IServiceType
{
private static readonly ModuleLog Log = new("ChatHandlers");
private static readonly ModuleLog Log = ModuleLog.Create<ChatHandlers>();
[ServiceManager.ServiceDependency]
private readonly DalamudConfiguration configuration = Service<DalamudConfiguration>.Get();

View file

@ -8,6 +8,7 @@ using Dalamud.Plugin.Services;
using Dalamud.Utility;
using FFXIVClientStructs.FFXIV.Client.Game.UI;
using Serilog;
namespace Dalamud.Game.ClientState.Aetherytes;

View file

@ -33,7 +33,7 @@ namespace Dalamud.Game.ClientState;
[ServiceManager.EarlyLoadedService]
internal sealed class ClientState : IInternalDisposableService, IClientState
{
private static readonly ModuleLog Log = new("ClientState");
private static readonly ModuleLog Log = ModuleLog.Create<ClientState>();
private readonly GameLifecycle lifecycle;
private readonly ClientStateAddressResolver address;

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

@ -5,7 +5,9 @@ using Dalamud.Hooking;
using Dalamud.IoC;
using Dalamud.IoC.Internal;
using Dalamud.Plugin.Services;
using FFXIVClientStructs.FFXIV.Client.System.Input;
using Serilog;
namespace Dalamud.Game.ClientState.GamePad;

View file

@ -37,7 +37,7 @@ internal class JobGauges : IServiceType, IJobGauges
// Since the gauge itself reads from live memory, there isn't much downside to doing this.
if (!this.cache.TryGetValue(typeof(T), out var gauge))
{
gauge = this.cache[typeof(T)] = (T)Activator.CreateInstance(typeof(T), BindingFlags.NonPublic | BindingFlags.Instance, null, new object[] { this.Address }, null);
gauge = this.cache[typeof(T)] = (T)Activator.CreateInstance(typeof(T), BindingFlags.NonPublic | BindingFlags.Instance, null, [this.Address], null);
}
return (T)gauge;

View file

@ -1,4 +1,5 @@
using Dalamud.Game.ClientState.JobGauge.Enums;
using FFXIVClientStructs.FFXIV.Client.Game.Gauge;
namespace Dalamud.Game.ClientState.JobGauge.Types;
@ -82,12 +83,12 @@ public unsafe class BRDGauge : JobGaugeBase<FFXIVClientStructs.FFXIV.Client.Game
{
get
{
return new[]
{
return
[
this.Struct->SongFlags.HasFlag(SongFlags.MagesBalladCoda) ? Song.Mage : Song.None,
this.Struct->SongFlags.HasFlag(SongFlags.ArmysPaeonCoda) ? Song.Army : Song.None,
this.Struct->SongFlags.HasFlag(SongFlags.WanderersMinuetCoda) ? Song.Wanderer : Song.None,
};
];
}
}
}

View file

@ -1,4 +1,5 @@
using Dalamud.Game.ClientState.JobGauge.Enums;
using FFXIVClientStructs.FFXIV.Client.Game.Gauge;
namespace Dalamud.Game.ClientState.JobGauge.Types;

View file

@ -1,4 +1,4 @@
using FFXIVClientStructs.FFXIV.Client.Game.Gauge;
using FFXIVClientStructs.FFXIV.Client.Game.Gauge;
using CanvasFlags = Dalamud.Game.ClientState.JobGauge.Enums.CanvasFlags;
using CreatureFlags = Dalamud.Game.ClientState.JobGauge.Enums.CreatureFlags;
@ -22,45 +22,45 @@ public unsafe class PCTGauge : JobGaugeBase<PictomancerGauge>
/// <summary>
/// Gets the use of subjective pallete.
/// </summary>
public byte PalleteGauge => Struct->PalleteGauge;
public byte PalleteGauge => this.Struct->PalleteGauge;
/// <summary>
/// Gets the amount of paint the player has.
/// </summary>
public byte Paint => Struct->Paint;
public byte Paint => this.Struct->Paint;
/// <summary>
/// Gets a value indicating whether a creature motif is drawn.
/// </summary>
public bool CreatureMotifDrawn => Struct->CreatureMotifDrawn;
public bool CreatureMotifDrawn => this.Struct->CreatureMotifDrawn;
/// <summary>
/// Gets a value indicating whether a weapon motif is drawn.
/// </summary>
public bool WeaponMotifDrawn => Struct->WeaponMotifDrawn;
public bool WeaponMotifDrawn => this.Struct->WeaponMotifDrawn;
/// <summary>
/// Gets a value indicating whether a landscape motif is drawn.
/// </summary>
public bool LandscapeMotifDrawn => Struct->LandscapeMotifDrawn;
public bool LandscapeMotifDrawn => this.Struct->LandscapeMotifDrawn;
/// <summary>
/// Gets a value indicating whether a moogle portrait is ready.
/// </summary>
public bool MooglePortraitReady => Struct->MooglePortraitReady;
public bool MooglePortraitReady => this.Struct->MooglePortraitReady;
/// <summary>
/// Gets a value indicating whether a madeen portrait is ready.
/// </summary>
public bool MadeenPortraitReady => Struct->MadeenPortraitReady;
public bool MadeenPortraitReady => this.Struct->MadeenPortraitReady;
/// <summary>
/// Gets which creature flags are present.
/// </summary>
public CreatureFlags CreatureFlags => (CreatureFlags)Struct->CreatureFlags;
public CreatureFlags CreatureFlags => (CreatureFlags)this.Struct->CreatureFlags;
/// <summary>
/// Gets which canvas flags are present.
/// </summary>
public CanvasFlags CanvasFlags => (CanvasFlags)Struct->CanvasFlags;
public CanvasFlags CanvasFlags => (CanvasFlags)this.Struct->CanvasFlags;
}

View file

@ -1,4 +1,5 @@
using Dalamud.Game.ClientState.JobGauge.Enums;
using FFXIVClientStructs.FFXIV.Client.Game.Gauge;
namespace Dalamud.Game.ClientState.JobGauge.Types;

View file

@ -1,7 +1,5 @@
using FFXIVClientStructs.FFXIV.Client.Game.Gauge;
using Reloaded.Memory;
using DreadCombo = Dalamud.Game.ClientState.JobGauge.Enums.DreadCombo;
using SerpentCombo = Dalamud.Game.ClientState.JobGauge.Enums.SerpentCombo;
@ -24,25 +22,25 @@ public unsafe class VPRGauge : JobGaugeBase<ViperGauge>
/// <summary>
/// Gets how many uses of uncoiled fury the player has.
/// </summary>
public byte RattlingCoilStacks => Struct->RattlingCoilStacks;
public byte RattlingCoilStacks => this.Struct->RattlingCoilStacks;
/// <summary>
/// Gets Serpent Offering stacks and gauge.
/// </summary>
public byte SerpentOffering => Struct->SerpentOffering;
public byte SerpentOffering => this.Struct->SerpentOffering;
/// <summary>
/// Gets value indicating the use of 1st, 2nd, 3rd, 4th generation and Ouroboros.
/// </summary>
public byte AnguineTribute => Struct->AnguineTribute;
public byte AnguineTribute => this.Struct->AnguineTribute;
/// <summary>
/// Gets the last Weaponskill used in DreadWinder/Pit of Dread combo.
/// </summary>
public DreadCombo DreadCombo => (DreadCombo)Struct->DreadCombo;
public DreadCombo DreadCombo => (DreadCombo)this.Struct->DreadCombo;
/// <summary>
/// Gets current ability for Serpent's Tail.
/// </summary>
public SerpentCombo SerpentCombo => (SerpentCombo)Struct->SerpentCombo;
public SerpentCombo SerpentCombo => (SerpentCombo)this.Struct->SerpentCombo;
}

View file

@ -30,50 +30,50 @@ internal sealed unsafe class TargetManager : IServiceType, ITargetManager
/// <inheritdoc/>
public IGameObject? Target
{
get => this.objectTable.CreateObjectReference((IntPtr)Struct->GetHardTarget());
set => Struct->SetHardTarget((FFXIVClientStructs.FFXIV.Client.Game.Object.GameObject*)value?.Address);
get => this.objectTable.CreateObjectReference((IntPtr)this.Struct->GetHardTarget());
set => this.Struct->SetHardTarget((FFXIVClientStructs.FFXIV.Client.Game.Object.GameObject*)value?.Address);
}
/// <inheritdoc/>
public IGameObject? MouseOverTarget
{
get => this.objectTable.CreateObjectReference((IntPtr)Struct->MouseOverTarget);
set => Struct->MouseOverTarget = (FFXIVClientStructs.FFXIV.Client.Game.Object.GameObject*)value?.Address;
get => this.objectTable.CreateObjectReference((IntPtr)this.Struct->MouseOverTarget);
set => this.Struct->MouseOverTarget = (FFXIVClientStructs.FFXIV.Client.Game.Object.GameObject*)value?.Address;
}
/// <inheritdoc/>
public IGameObject? FocusTarget
{
get => this.objectTable.CreateObjectReference((IntPtr)Struct->FocusTarget);
set => Struct->FocusTarget = (FFXIVClientStructs.FFXIV.Client.Game.Object.GameObject*)value?.Address;
get => this.objectTable.CreateObjectReference((IntPtr)this.Struct->FocusTarget);
set => this.Struct->FocusTarget = (FFXIVClientStructs.FFXIV.Client.Game.Object.GameObject*)value?.Address;
}
/// <inheritdoc/>
public IGameObject? PreviousTarget
{
get => this.objectTable.CreateObjectReference((IntPtr)Struct->PreviousTarget);
set => Struct->PreviousTarget = (FFXIVClientStructs.FFXIV.Client.Game.Object.GameObject*)value?.Address;
get => this.objectTable.CreateObjectReference((IntPtr)this.Struct->PreviousTarget);
set => this.Struct->PreviousTarget = (FFXIVClientStructs.FFXIV.Client.Game.Object.GameObject*)value?.Address;
}
/// <inheritdoc/>
public IGameObject? SoftTarget
{
get => this.objectTable.CreateObjectReference((IntPtr)Struct->GetSoftTarget());
set => Struct->SetSoftTarget((FFXIVClientStructs.FFXIV.Client.Game.Object.GameObject*)value?.Address);
get => this.objectTable.CreateObjectReference((IntPtr)this.Struct->GetSoftTarget());
set => this.Struct->SetSoftTarget((FFXIVClientStructs.FFXIV.Client.Game.Object.GameObject*)value?.Address);
}
/// <inheritdoc/>
public IGameObject? GPoseTarget
{
get => this.objectTable.CreateObjectReference((IntPtr)Struct->GPoseTarget);
set => Struct->GPoseTarget = (FFXIVClientStructs.FFXIV.Client.Game.Object.GameObject*)value?.Address;
get => this.objectTable.CreateObjectReference((IntPtr)this.Struct->GPoseTarget);
set => this.Struct->GPoseTarget = (FFXIVClientStructs.FFXIV.Client.Game.Object.GameObject*)value?.Address;
}
/// <inheritdoc/>
public IGameObject? MouseOverNameplateTarget
{
get => this.objectTable.CreateObjectReference((IntPtr)Struct->MouseOverNameplateTarget);
set => Struct->MouseOverNameplateTarget = (FFXIVClientStructs.FFXIV.Client.Game.Object.GameObject*)value?.Address;
get => this.objectTable.CreateObjectReference((IntPtr)this.Struct->MouseOverNameplateTarget);
set => this.Struct->MouseOverNameplateTarget = (FFXIVClientStructs.FFXIV.Client.Game.Object.GameObject*)value?.Address;
}
private TargetSystem* Struct => TargetSystem.Instance();

View file

@ -1,5 +1,4 @@
using Dalamud.Game.ClientState.Statuses;
using Dalamud.Utility;
namespace Dalamud.Game.ClientState.Objects.Types;

View file

@ -1,9 +1,8 @@
using System.Runtime.CompilerServices;
using Dalamud.Data;
using Dalamud.Game.ClientState.Customize;
using Dalamud.Game.ClientState.Objects.Enums;
using Dalamud.Game.Text.SeStringHandling;
using Dalamud.Memory;
using Dalamud.Utility;
using Lumina.Excel;
using Lumina.Excel.Sheets;
@ -16,68 +15,73 @@ namespace Dalamud.Game.ClientState.Objects.Types;
public interface ICharacter : IGameObject
{
/// <summary>
/// Gets the current HP of this Chara.
/// Gets the current HP of this character.
/// </summary>
public uint CurrentHp { get; }
/// <summary>
/// Gets the maximum HP of this Chara.
/// Gets the maximum HP of this character.
/// </summary>
public uint MaxHp { get; }
/// <summary>
/// Gets the current MP of this Chara.
/// Gets the current MP of this character.
/// </summary>
public uint CurrentMp { get; }
/// <summary>
/// Gets the maximum MP of this Chara.
/// Gets the maximum MP of this character.
/// </summary>
public uint MaxMp { get; }
/// <summary>
/// Gets the current GP of this Chara.
/// Gets the current GP of this character.
/// </summary>
public uint CurrentGp { get; }
/// <summary>
/// Gets the maximum GP of this Chara.
/// Gets the maximum GP of this character.
/// </summary>
public uint MaxGp { get; }
/// <summary>
/// Gets the current CP of this Chara.
/// Gets the current CP of this character.
/// </summary>
public uint CurrentCp { get; }
/// <summary>
/// Gets the maximum CP of this Chara.
/// Gets the maximum CP of this character.
/// </summary>
public uint MaxCp { get; }
/// <summary>
/// Gets the shield percentage of this Chara.
/// Gets the shield percentage of this character.
/// </summary>
public byte ShieldPercentage { get; }
/// <summary>
/// Gets the ClassJob of this Chara.
/// Gets the ClassJob of this character.
/// </summary>
public RowRef<ClassJob> ClassJob { get; }
/// <summary>
/// Gets the level of this Chara.
/// Gets the level of this character.
/// </summary>
public byte Level { get; }
/// <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"/>.
/// </summary>
public byte[] Customize { get; }
/// <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>
public SeString CompanyTag { get; }
@ -95,12 +99,12 @@ public interface ICharacter : IGameObject
/// Gets the status flags.
/// </summary>
public StatusFlags StatusFlags { get; }
/// <summary>
/// Gets the current mount for this character. Will be <c>null</c> if the character doesn't have a mount.
/// </summary>
public RowRef<Mount>? CurrentMount { get; }
/// <summary>
/// Gets the current minion summoned for this character. Will be <c>null</c> if the character doesn't have a minion.
/// This method *will* return information about a spawned (but invisible) minion, e.g. if the character is riding a
@ -119,7 +123,7 @@ internal unsafe class Character : GameObject, ICharacter
/// This represents a non-static entity.
/// </summary>
/// <param name="address">The address of this character in memory.</param>
internal Character(IntPtr address)
internal Character(nint address)
: base(address)
{
}
@ -158,8 +162,12 @@ internal unsafe class Character : GameObject, ICharacter
public byte Level => this.Struct->CharacterData.Level;
/// <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();
/// <inheritdoc/>
public ICustomizeData CustomizeData => new CustomizeData((nint)(&this.Struct->DrawData.CustomizeData));
/// <inheritdoc/>
public SeString CompanyTag => SeString.Parse(this.Struct->FreeCompanyTag);
@ -186,14 +194,14 @@ internal unsafe class Character : GameObject, ICharacter
(this.Struct->IsAllianceMember ? StatusFlags.AllianceMember : StatusFlags.None) |
(this.Struct->IsFriend ? StatusFlags.Friend : StatusFlags.None) |
(this.Struct->IsCasting ? StatusFlags.IsCasting : StatusFlags.None);
/// <inheritdoc />
public RowRef<Mount>? CurrentMount
{
get
{
if (this.Struct->IsNotMounted()) return null; // just for safety.
var mountId = this.Struct->Mount.MountId;
return mountId == 0 ? null : LuminaUtils.CreateRef<Mount>(mountId);
}
@ -204,7 +212,7 @@ internal unsafe class Character : GameObject, ICharacter
{
get
{
if (this.Struct->CompanionObject != null)
if (this.Struct->CompanionObject != null)
return LuminaUtils.CreateRef<Companion>(this.Struct->CompanionObject->BaseId);
// this is only present if a minion is summoned but hidden (e.g. the player's on a mount).

View file

@ -47,7 +47,7 @@ internal sealed unsafe partial class PartyList : IServiceType, IPartyList
public unsafe nint GroupManagerAddress => (nint)CSGroupManager.Instance();
/// <inheritdoc/>
public nint GroupListAddress => (nint)Unsafe.AsPointer(ref GroupManagerStruct->MainGroup.PartyMembers[0]);
public nint GroupListAddress => (nint)Unsafe.AsPointer(ref this.GroupManagerStruct->MainGroup.PartyMembers[0]);
/// <inheritdoc/>
public nint AllianceListAddress => (nint)Unsafe.AsPointer(ref this.GroupManagerStruct->MainGroup.AllianceMembers[0]);

View file

@ -6,6 +6,7 @@ using Dalamud.Game.ClientState.Objects;
using Dalamud.Game.ClientState.Objects.Types;
using Dalamud.Game.ClientState.Statuses;
using Dalamud.Game.Text.SeStringHandling;
using Dalamud.Utility;
using Lumina.Excel;

View file

@ -38,7 +38,7 @@ public sealed unsafe partial class StatusList
/// <summary>
/// Gets the amount of status effect slots the actor has.
/// </summary>
public int Length => Struct->NumValidStatuses;
public int Length => this.Struct->NumValidStatuses;
private static int StatusSize { get; } = Marshal.SizeOf<FFXIVClientStructs.FFXIV.Client.Game.Status>();

View file

@ -24,7 +24,7 @@ namespace Dalamud.Game.Command;
[ServiceManager.EarlyLoadedService]
internal sealed unsafe class CommandManager : IInternalDisposableService, ICommandManager
{
private static readonly ModuleLog Log = new("Command");
private static readonly ModuleLog Log = ModuleLog.Create<CommandManager>();
private readonly ConcurrentDictionary<string, IReadOnlyCommandInfo> commandMap = new();
private readonly ConcurrentDictionary<(string, IReadOnlyCommandInfo), string> commandAssemblyNameMap = new();
@ -71,7 +71,7 @@ internal sealed unsafe class CommandManager : IInternalDisposableService, IComma
if (separatorPosition + 1 >= content.Length)
{
// Remove the trailing space
command = content.Substring(0, separatorPosition);
command = content[..separatorPosition];
}
else
{
@ -262,12 +262,12 @@ internal sealed unsafe class CommandManager : IInternalDisposableService, IComma
#pragma warning restore SA1015
internal class CommandManagerPluginScoped : IInternalDisposableService, ICommandManager
{
private static readonly ModuleLog Log = new("Command");
private static readonly ModuleLog Log = ModuleLog.Create<CommandManager>();
[ServiceManager.ServiceDependency]
private readonly CommandManager commandManagerService = Service<CommandManager>.Get();
private readonly List<string> pluginRegisteredCommands = new();
private readonly List<string> pluginRegisteredCommands = [];
private readonly LocalPlugin pluginInfo;
/// <summary>

View file

@ -1,11 +1,13 @@
using System.Threading.Tasks;
using System.Threading.Tasks;
using Dalamud.Hooking;
using Dalamud.IoC;
using Dalamud.IoC.Internal;
using Dalamud.Plugin.Services;
using Dalamud.Utility;
using FFXIVClientStructs.FFXIV.Common.Configuration;
using Serilog;
namespace Dalamud.Game.Config;

View file

@ -1,10 +1,12 @@
using System.Collections.Concurrent;
using System.Collections.Concurrent;
using System.Diagnostics;
using System.Text;
using Dalamud.Memory;
using Dalamud.Utility;
using FFXIVClientStructs.FFXIV.Common.Configuration;
using Serilog;
namespace Dalamud.Game.Config;

View file

@ -26,7 +26,7 @@ namespace Dalamud.Game;
[ServiceManager.EarlyLoadedService]
internal sealed class Framework : IInternalDisposableService, IFramework
{
private static readonly ModuleLog Log = new("Framework");
private static readonly ModuleLog Log = ModuleLog.Create<Framework>();
private static readonly Stopwatch StatsStopwatch = new();
@ -86,7 +86,7 @@ internal sealed class Framework : IInternalDisposableService, IFramework
/// <summary>
/// Gets the stats history mapping.
/// </summary>
public static Dictionary<string, List<double>> StatsHistory { get; } = new();
public static Dictionary<string, List<double>> StatsHistory { get; } = [];
/// <inheritdoc/>
public DateTime LastUpdate { get; private set; } = DateTime.MinValue;
@ -106,7 +106,7 @@ internal sealed class Framework : IInternalDisposableService, IFramework
/// <summary>
/// Gets the list of update sub-delegates that didn't get updated this frame.
/// </summary>
internal List<string> NonUpdatedSubDelegates { get; private set; } = new();
internal List<string> NonUpdatedSubDelegates { get; private set; } = [];
/// <summary>
/// Gets or sets a value indicating whether to dispatch update events.
@ -121,9 +121,9 @@ internal sealed class Framework : IInternalDisposableService, IFramework
/// <inheritdoc/>
public Task DelayTicks(long numTicks, CancellationToken cancellationToken = default)
{
if (this.frameworkDestroy.IsCancellationRequested)
if (this.frameworkDestroy.IsCancellationRequested) // Going away
return Task.FromCanceled(this.frameworkDestroy.Token);
if (numTicks <= 0)
if (numTicks <= 0 || this.frameworkThreadTaskScheduler.BoundThread == null) // Nonsense or before first tick
return Task.CompletedTask;
var tcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously);
@ -212,11 +212,10 @@ internal sealed class Framework : IInternalDisposableService, IFramework
if (cancellationToken == default)
cancellationToken = this.FrameworkThreadTaskFactory.CancellationToken;
return this.FrameworkThreadTaskFactory.ContinueWhenAll(
new[]
{
[
Task.Delay(delay, cancellationToken),
this.DelayTicks(delayTicks, cancellationToken),
},
],
_ => func(),
cancellationToken,
TaskContinuationOptions.HideScheduler,
@ -239,11 +238,10 @@ internal sealed class Framework : IInternalDisposableService, IFramework
if (cancellationToken == default)
cancellationToken = this.FrameworkThreadTaskFactory.CancellationToken;
return this.FrameworkThreadTaskFactory.ContinueWhenAll(
new[]
{
[
Task.Delay(delay, cancellationToken),
this.DelayTicks(delayTicks, cancellationToken),
},
],
_ => action(),
cancellationToken,
TaskContinuationOptions.HideScheduler,
@ -266,11 +264,10 @@ internal sealed class Framework : IInternalDisposableService, IFramework
if (cancellationToken == default)
cancellationToken = this.FrameworkThreadTaskFactory.CancellationToken;
return this.FrameworkThreadTaskFactory.ContinueWhenAll(
new[]
{
[
Task.Delay(delay, cancellationToken),
this.DelayTicks(delayTicks, cancellationToken),
},
],
_ => func(),
cancellationToken,
TaskContinuationOptions.HideScheduler,
@ -293,11 +290,10 @@ internal sealed class Framework : IInternalDisposableService, IFramework
if (cancellationToken == default)
cancellationToken = this.FrameworkThreadTaskFactory.CancellationToken;
return this.FrameworkThreadTaskFactory.ContinueWhenAll(
new[]
{
[
Task.Delay(delay, cancellationToken),
this.DelayTicks(delayTicks, cancellationToken),
},
],
_ => func(),
cancellationToken,
TaskContinuationOptions.HideScheduler,
@ -333,7 +329,7 @@ internal sealed class Framework : IInternalDisposableService, IFramework
internal static void AddToStats(string key, double ms)
{
if (!StatsHistory.ContainsKey(key))
StatsHistory.Add(key, new List<double>());
StatsHistory.Add(key, []);
StatsHistory[key].Add(ms);

View file

@ -1,6 +1,7 @@
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Linq;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using Dalamud.Configuration.Internal;
@ -37,14 +38,16 @@ namespace Dalamud.Game.Gui;
[ServiceManager.EarlyLoadedService]
internal sealed unsafe class ChatGui : IInternalDisposableService, IChatGui
{
private static readonly ModuleLog Log = new("ChatGui");
private static readonly ModuleLog Log = ModuleLog.Create<ChatGui>();
private readonly Queue<XivChatEntry> chatQueue = new();
private readonly Dictionary<(string PluginName, uint CommandId), Action<uint, SeString>> dalamudLinkHandlers = new();
private readonly Dictionary<(string PluginName, uint CommandId), Action<uint, SeString>> dalamudLinkHandlers = [];
private readonly List<nint> seenLogMessageObjects = [];
private readonly Hook<PrintMessageDelegate> printMessageHook;
private readonly Hook<InventoryItem.Delegates.Copy> inventoryItemCopyHook;
private readonly Hook<LogViewer.Delegates.HandleLinkClick> handleLinkClickHook;
private readonly Hook<RaptureLogModule.Delegates.Update> handleLogModuleUpdate;
[ServiceManager.ServiceDependency]
private readonly DalamudConfiguration configuration = Service<DalamudConfiguration>.Get();
@ -58,10 +61,12 @@ internal sealed unsafe class ChatGui : IInternalDisposableService, IChatGui
this.printMessageHook = Hook<PrintMessageDelegate>.FromAddress(RaptureLogModule.Addresses.PrintMessage.Value, this.HandlePrintMessageDetour);
this.inventoryItemCopyHook = Hook<InventoryItem.Delegates.Copy>.FromAddress((nint)InventoryItem.StaticVirtualTablePointer->Copy, this.InventoryItemCopyDetour);
this.handleLinkClickHook = Hook<LogViewer.Delegates.HandleLinkClick>.FromAddress(LogViewer.Addresses.HandleLinkClick.Value, this.HandleLinkClickDetour);
this.handleLogModuleUpdate = Hook<RaptureLogModule.Delegates.Update>.FromAddress(RaptureLogModule.Addresses.Update.Value, this.UpdateDetour);
this.printMessageHook.Enable();
this.inventoryItemCopyHook.Enable();
this.handleLinkClickHook.Enable();
this.handleLogModuleUpdate.Enable();
}
[UnmanagedFunctionPointer(CallingConvention.ThisCall)]
@ -79,6 +84,9 @@ internal sealed unsafe class ChatGui : IInternalDisposableService, IChatGui
/// <inheritdoc/>
public event IChatGui.OnMessageUnhandledDelegate? ChatMessageUnhandled;
/// <inheritdoc/>
public event IChatGui.OnLogMessageDelegate? LogMessage;
/// <inheritdoc/>
public uint LastLinkedItemId { get; private set; }
@ -110,6 +118,7 @@ internal sealed unsafe class ChatGui : IInternalDisposableService, IChatGui
this.printMessageHook.Dispose();
this.inventoryItemCopyHook.Dispose();
this.handleLinkClickHook.Dispose();
this.handleLogModuleUpdate.Dispose();
}
#region DalamudSeString
@ -493,6 +502,46 @@ internal sealed unsafe class ChatGui : IInternalDisposableService, IChatGui
Log.Error(ex, "Exception in HandleLinkClickDetour");
}
}
private void UpdateDetour(RaptureLogModule* thisPtr)
{
try
{
foreach (ref var item in thisPtr->LogMessageQueue)
{
var logMessage = new Chat.LogMessage((LogMessageQueueItem*)Unsafe.AsPointer(ref item));
// skip any entries that survived the previous Update call as the event was already called for them
if (this.seenLogMessageObjects.Contains(logMessage.Address))
continue;
foreach (var action in Delegate.EnumerateInvocationList(this.LogMessage))
{
try
{
action(logMessage);
}
catch (Exception e)
{
Log.Error(e, "Could not invoke registered OnLogMessageDelegate for {Name}", action.Method);
}
}
}
this.handleLogModuleUpdate.Original(thisPtr);
// record the log messages for that we already called the event, but are still in the queue
this.seenLogMessageObjects.Clear();
foreach (ref var item in thisPtr->LogMessageQueue)
{
this.seenLogMessageObjects.Add((nint)Unsafe.AsPointer(ref item));
}
}
catch (Exception ex)
{
Log.Error(ex, "Exception in UpdateDetour");
}
}
}
/// <summary>
@ -521,6 +570,7 @@ internal class ChatGuiPluginScoped : IInternalDisposableService, IChatGui
this.chatGuiService.CheckMessageHandled += this.OnCheckMessageForward;
this.chatGuiService.ChatMessageHandled += this.OnMessageHandledForward;
this.chatGuiService.ChatMessageUnhandled += this.OnMessageUnhandledForward;
this.chatGuiService.LogMessage += this.OnLogMessageForward;
}
/// <inheritdoc/>
@ -535,6 +585,9 @@ internal class ChatGuiPluginScoped : IInternalDisposableService, IChatGui
/// <inheritdoc/>
public event IChatGui.OnMessageUnhandledDelegate? ChatMessageUnhandled;
/// <inheritdoc/>
public event IChatGui.OnLogMessageDelegate? LogMessage;
/// <inheritdoc/>
public uint LastLinkedItemId => this.chatGuiService.LastLinkedItemId;
@ -551,11 +604,13 @@ internal class ChatGuiPluginScoped : IInternalDisposableService, IChatGui
this.chatGuiService.CheckMessageHandled -= this.OnCheckMessageForward;
this.chatGuiService.ChatMessageHandled -= this.OnMessageHandledForward;
this.chatGuiService.ChatMessageUnhandled -= this.OnMessageUnhandledForward;
this.chatGuiService.LogMessage -= this.OnLogMessageForward;
this.ChatMessage = null;
this.CheckMessageHandled = null;
this.ChatMessageHandled = null;
this.ChatMessageUnhandled = null;
this.LogMessage = null;
}
/// <inheritdoc/>
@ -609,4 +664,7 @@ internal class ChatGuiPluginScoped : IInternalDisposableService, IChatGui
private void OnMessageUnhandledForward(XivChatType type, int timestamp, SeString sender, SeString message)
=> this.ChatMessageUnhandled?.Invoke(type, timestamp, sender, message);
private void OnLogMessageForward(Chat.ILogMessage message)
=> this.LogMessage?.Invoke(message);
}

View file

@ -2,6 +2,7 @@ using System.Collections.Generic;
using System.Linq;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using System.Threading;
using Dalamud.Game.Text;
using Dalamud.Game.Text.SeStringHandling;
@ -28,7 +29,7 @@ namespace Dalamud.Game.Gui.ContextMenu;
[ServiceManager.EarlyLoadedService]
internal sealed unsafe class ContextMenu : IInternalDisposableService, IContextMenu
{
private static readonly ModuleLog Log = new("ContextMenu");
private static readonly ModuleLog Log = ModuleLog.Create<ContextMenu>();
private readonly Hook<AtkModuleVf22OpenAddonByAgentDelegate> atkModuleVf22OpenAddonByAgentHook;
private readonly Hook<AddonContextMenu.Delegates.OnMenuSelected> addonContextMenuOnMenuSelectedHook;
@ -53,7 +54,7 @@ internal sealed unsafe class ContextMenu : IInternalDisposableService, IContextM
private Dictionary<ContextMenuType, List<IMenuItem>> MenuItems { get; } = [];
private object MenuItemsLock { get; } = new();
private Lock MenuItemsLock { get; } = new();
private AgentInterface* SelectedAgent { get; set; }
@ -335,7 +336,7 @@ internal sealed unsafe class ContextMenu : IInternalDisposableService, IContextM
this.MenuCallbackIds.Clear();
this.SelectedAgent = agent;
var unitManager = RaptureAtkUnitManager.Instance();
this.SelectedParentAddon = unitManager->GetAddonById(unitManager->GetAddonByName(addonName)->ContextMenuParentId);
this.SelectedParentAddon = unitManager->GetAddonById(unitManager->GetAddonByName(addonName)->BlockedParentId);
this.SelectedEventInterfaces.Clear();
if (this.SelectedAgent == AgentInventoryContext.Instance())
{

View file

@ -1,6 +1,5 @@
using System.Collections.Generic;
using Dalamud.Memory;
using Dalamud.Plugin.Services;
using FFXIVClientStructs.FFXIV.Client.UI.Agent;

View file

@ -30,7 +30,7 @@ internal sealed unsafe class DtrBar : IInternalDisposableService, IDtrBar
{
private const uint BaseNodeId = 1000;
private static readonly ModuleLog Log = new("DtrBar");
private static readonly ModuleLog Log = ModuleLog.Create<DtrBar>();
[ServiceManager.ServiceDependency]
private readonly Framework framework = Service<Framework>.Get();
@ -54,7 +54,7 @@ internal sealed unsafe class DtrBar : IInternalDisposableService, IDtrBar
private readonly ReaderWriterLockSlim entriesLock = new();
private readonly List<DtrBarEntry> entries = [];
private readonly Dictionary<uint, List<IAddonEventHandle>> eventHandles = new();
private readonly Dictionary<uint, List<IAddonEventHandle>> eventHandles = [];
private ImmutableList<IReadOnlyDtrBarEntry>? entriesReadOnlyCopy;
@ -397,7 +397,15 @@ internal sealed unsafe class DtrBar : IInternalDisposableService, IDtrBar
ushort w = 0, h = 0;
node->GetTextDrawSize(&w, &h, node->NodeText.StringPtr);
node->SetWidth(w);
if (data.MinimumWidth > 0)
{
node->SetWidth(Math.Max(data.MinimumWidth, w));
}
else
{
node->SetWidth(w);
}
}
var elementWidth = data.TextNode->Width + this.configuration.DtrSpacing;
@ -516,7 +524,7 @@ internal sealed unsafe class DtrBar : IInternalDisposableService, IDtrBar
var node = data.TextNode = this.MakeNode(++this.runningNodeIds);
this.eventHandles.TryAdd(node->NodeId, new List<IAddonEventHandle>());
this.eventHandles.TryAdd(node->NodeId, []);
this.eventHandles[node->NodeId].AddRange(new List<IAddonEventHandle>
{
this.uiEventManager.AddEvent(AddonEventManager.DalamudInternalKey, (nint)dtr, (nint)node, AddonEventType.MouseOver, this.DtrEventHandler),

View file

@ -1,7 +1,6 @@
using System.Numerics;
using System.Numerics;
using Dalamud.Configuration.Internal;
using Dalamud.Game.Addon.Events.EventDataTypes;
using Dalamud.Game.Text.SeStringHandling;
using Dalamud.Plugin.Internal.Types;
using Dalamud.Utility;
@ -41,6 +40,11 @@ public interface IReadOnlyDtrBarEntry
/// </summary>
public bool Shown { get; }
/// <summary>
/// Gets a value indicating this entry's minimum width.
/// </summary>
public ushort MinimumWidth { get; }
/// <summary>
/// Gets a value indicating whether the user has hidden this entry from view through the Dalamud settings.
/// </summary>
@ -77,6 +81,11 @@ public interface IDtrBarEntry : IReadOnlyDtrBarEntry
/// </summary>
public new bool Shown { get; set; }
/// <summary>
/// Gets or sets a value specifying the requested minimum width to make this entry.
/// </summary>
public new ushort MinimumWidth { get; set; }
/// <summary>
/// Gets or sets an action to be invoked when the user clicks on the dtr entry.
/// </summary>
@ -129,6 +138,25 @@ internal sealed unsafe class DtrBarEntry : IDisposable, IDtrBarEntry
/// <inheritdoc cref="IDtrBarEntry.Tooltip" />
public SeString? Tooltip { get; set; }
/// <inheritdoc cref="MinimumWidth" />
public ushort MinimumWidth
{
get;
set
{
field = value;
if (this.TextNode is not null)
{
if (this.TextNode->GetWidth() < value)
{
this.TextNode->SetWidth(value);
}
}
this.Dirty = true;
}
}
/// <inheritdoc/>
public Action<DtrInteractionEvent>? OnClick { get; set; }

View file

@ -32,7 +32,7 @@ namespace Dalamud.Game.Gui;
[ServiceManager.EarlyLoadedService]
internal sealed unsafe class GameGui : IInternalDisposableService, IGameGui
{
private static readonly ModuleLog Log = new("GameGui");
private static readonly ModuleLog Log = ModuleLog.Create<GameGui>();
private readonly GameGuiAddressResolver address;

View file

@ -427,8 +427,8 @@ internal unsafe class NamePlateUpdateHandler : INamePlateUpdateHandler
/// <inheritdoc/>
public int VisibilityFlags
{
get => ObjectData->VisibilityFlags;
set => ObjectData->VisibilityFlags = value;
get => this.ObjectData->VisibilityFlags;
set => this.ObjectData->VisibilityFlags = value;
}
/// <inheritdoc/>

View file

@ -1,4 +1,5 @@
using Dalamud.Plugin.Services;
using Lumina.Excel.Sheets;
namespace Dalamud.Game.Gui.PartyFinder.Types;

View file

@ -23,23 +23,7 @@ public class PartyFinderSlot
/// <summary>
/// Gets a list of jobs that this slot is accepting.
/// </summary>
public IReadOnlyCollection<JobFlags> Accepting
{
get
{
if (this.listAccepting != null)
{
return this.listAccepting;
}
this.listAccepting = Enum.GetValues(typeof(JobFlags))
.Cast<JobFlags>()
.Where(flag => this[flag])
.ToArray();
return this.listAccepting;
}
}
public IReadOnlyCollection<JobFlags> Accepting => this.listAccepting ??= Enum.GetValues<JobFlags>().Where(flag => this[flag]).ToArray();
/// <summary>
/// Tests if this slot is accepting a job.

View file

@ -22,7 +22,7 @@ namespace Dalamud.Game.Internal;
[ServiceManager.EarlyLoadedService]
internal sealed unsafe class DalamudAtkTweaks : IInternalDisposableService
{
private static readonly ModuleLog Log = new("DalamudAtkTweaks");
private static readonly ModuleLog Log = ModuleLog.Create<DalamudAtkTweaks>();
private readonly Hook<AgentHUD.Delegates.OpenSystemMenu> hookAgentHudOpenSystemMenu;

View file

@ -18,15 +18,15 @@ namespace Dalamud.Game.Inventory;
[ServiceManager.EarlyLoadedService]
internal class GameInventory : IInternalDisposableService
{
private readonly List<GameInventoryPluginScoped> subscribersPendingChange = new();
private readonly List<GameInventoryPluginScoped> subscribers = new();
private readonly List<GameInventoryPluginScoped> subscribersPendingChange = [];
private readonly List<GameInventoryPluginScoped> subscribers = [];
private readonly List<InventoryItemAddedArgs> addedEvents = new();
private readonly List<InventoryItemRemovedArgs> removedEvents = new();
private readonly List<InventoryItemChangedArgs> changedEvents = new();
private readonly List<InventoryItemMovedArgs> movedEvents = new();
private readonly List<InventoryItemSplitArgs> splitEvents = new();
private readonly List<InventoryItemMergedArgs> mergedEvents = new();
private readonly List<InventoryItemAddedArgs> addedEvents = [];
private readonly List<InventoryItemRemovedArgs> removedEvents = [];
private readonly List<InventoryItemChangedArgs> changedEvents = [];
private readonly List<InventoryItemMovedArgs> movedEvents = [];
private readonly List<InventoryItemSplitArgs> splitEvents = [];
private readonly List<InventoryItemMergedArgs> mergedEvents = [];
[ServiceManager.ServiceDependency]
private readonly Framework framework = Service<Framework>.Get();
@ -151,7 +151,7 @@ internal class GameInventory : IInternalDisposableService
bool isNew;
lock (this.subscribersPendingChange)
{
isNew = this.subscribersPendingChange.Any() && !this.subscribers.Any();
isNew = this.subscribersPendingChange.Count != 0 && this.subscribers.Count == 0;
this.subscribers.Clear();
this.subscribers.AddRange(this.subscribersPendingChange);
this.subscribersChanged = false;
@ -348,7 +348,7 @@ internal class GameInventory : IInternalDisposableService
#pragma warning restore SA1015
internal class GameInventoryPluginScoped : IInternalDisposableService, IGameInventory
{
private static readonly ModuleLog Log = new(nameof(GameInventoryPluginScoped));
private static readonly ModuleLog Log = ModuleLog.Create<GameInventoryPluginScoped>();
[ServiceManager.ServiceDependency]
private readonly GameInventory gameInventoryService = Service<GameInventory>.Get();

View file

@ -1,5 +1,3 @@
using System.Linq;
using Dalamud.Game.Network.Internal;
using Dalamud.Game.Network.Structures;
using Dalamud.IoC;
@ -95,7 +93,7 @@ internal class MarketBoard : IInternalDisposableService, IMarketBoard
#pragma warning restore SA1015
internal class MarketBoardPluginScoped : IInternalDisposableService, IMarketBoard
{
private static readonly ModuleLog Log = new(nameof(MarketBoardPluginScoped));
private static readonly ModuleLog Log = ModuleLog.Create<MarketBoardPluginScoped>();
[ServiceManager.ServiceDependency]
private readonly MarketBoard marketBoardService = Service<MarketBoard>.Get();

View file

@ -81,7 +81,7 @@ public readonly unsafe struct AgentInterfacePtr(nint address) : IEquatable<Agent
/// Focuses the AtkUnitBase.
/// </summary>
/// <returns> <c>true</c> when the addon was focused, <c>false</c> otherwise. </returns>
public readonly bool FocusAddon() => this.IsNull && this.Struct->FocusAddon();
public readonly bool FocusAddon() => !this.IsNull && this.Struct->FocusAddon();
/// <summary>Determines whether the specified AgentInterfacePtr is equal to the current AgentInterfacePtr.</summary>
/// <param name="other">The AgentInterfacePtr to compare with the current AgentInterfacePtr.</param>

View file

@ -89,7 +89,7 @@ public readonly unsafe struct AtkValuePtr(nint address) : IEquatable<AtkValuePtr
/// </returns>
public unsafe bool TryGet<T>([NotNullWhen(true)] out T? result) where T : struct
{
object? value = this.GetValue();
var value = this.GetValue();
if (value is T typed)
{
result = typed;

View file

@ -1,150 +0,0 @@
using System.Runtime.InteropServices;
using Dalamud.Configuration.Internal;
using Dalamud.Hooking;
using Dalamud.IoC;
using Dalamud.IoC.Internal;
using Dalamud.Plugin.Services;
using Dalamud.Utility;
using FFXIVClientStructs.FFXIV.Client.Network;
using Serilog;
namespace Dalamud.Game.Network;
/// <summary>
/// This class handles interacting with game network events.
/// </summary>
[ServiceManager.EarlyLoadedService]
internal sealed unsafe class GameNetwork : IInternalDisposableService
{
private readonly GameNetworkAddressResolver address;
private readonly Hook<PacketDispatcher.Delegates.OnReceivePacket> processZonePacketDownHook;
private readonly Hook<ProcessZonePacketUpDelegate> processZonePacketUpHook;
private readonly HitchDetector hitchDetectorUp;
private readonly HitchDetector hitchDetectorDown;
[ServiceManager.ServiceDependency]
private readonly DalamudConfiguration configuration = Service<DalamudConfiguration>.Get();
[ServiceManager.ServiceConstructor]
private unsafe GameNetwork(TargetSigScanner sigScanner)
{
this.hitchDetectorUp = new HitchDetector("GameNetworkUp", this.configuration.GameNetworkUpHitch);
this.hitchDetectorDown = new HitchDetector("GameNetworkDown", this.configuration.GameNetworkDownHitch);
this.address = new GameNetworkAddressResolver();
this.address.Setup(sigScanner);
var onReceivePacketAddress = (nint)PacketDispatcher.StaticVirtualTablePointer->OnReceivePacket;
Log.Verbose("===== G A M E N E T W O R K =====");
Log.Verbose($"OnReceivePacket address {Util.DescribeAddress(onReceivePacketAddress)}");
Log.Verbose($"ProcessZonePacketUp address {Util.DescribeAddress(this.address.ProcessZonePacketUp)}");
this.processZonePacketDownHook = Hook<PacketDispatcher.Delegates.OnReceivePacket>.FromAddress(onReceivePacketAddress, this.ProcessZonePacketDownDetour);
this.processZonePacketUpHook = Hook<ProcessZonePacketUpDelegate>.FromAddress(this.address.ProcessZonePacketUp, this.ProcessZonePacketUpDetour);
this.processZonePacketDownHook.Enable();
this.processZonePacketUpHook.Enable();
}
/// <summary>
/// The delegate type of a network message event.
/// </summary>
/// <param name="dataPtr">The pointer to the raw data.</param>
/// <param name="opCode">The operation ID code.</param>
/// <param name="sourceActorId">The source actor ID.</param>
/// <param name="targetActorId">The taret actor ID.</param>
/// <param name="direction">The direction of the packed.</param>
public delegate void OnNetworkMessageDelegate(nint dataPtr, ushort opCode, uint sourceActorId, uint targetActorId, NetworkMessageDirection direction);
[UnmanagedFunctionPointer(CallingConvention.ThisCall)]
private delegate byte ProcessZonePacketUpDelegate(IntPtr a1, IntPtr dataPtr, IntPtr a3, byte a4);
/// <summary>
/// Event that is called when a network message is sent/received.
/// </summary>
public event OnNetworkMessageDelegate? NetworkMessage;
/// <inheritdoc/>
void IInternalDisposableService.DisposeService()
{
this.processZonePacketDownHook.Dispose();
this.processZonePacketUpHook.Dispose();
}
private void ProcessZonePacketDownDetour(PacketDispatcher* dispatcher, uint targetId, IntPtr dataPtr)
{
this.hitchDetectorDown.Start();
// Go back 0x10 to get back to the start of the packet header
dataPtr -= 0x10;
foreach (var d in Delegate.EnumerateInvocationList(this.NetworkMessage))
{
try
{
d.Invoke(
dataPtr + 0x20,
(ushort)Marshal.ReadInt16(dataPtr, 0x12),
0,
targetId,
NetworkMessageDirection.ZoneDown);
}
catch (Exception ex)
{
string header;
try
{
var data = new byte[32];
Marshal.Copy(dataPtr, data, 0, 32);
header = BitConverter.ToString(data);
}
catch (Exception)
{
header = "failed";
}
Log.Error(ex, "Exception on ProcessZonePacketDown hook. Header: " + header);
}
}
this.processZonePacketDownHook.Original(dispatcher, targetId, dataPtr + 0x10);
this.hitchDetectorDown.Stop();
}
private byte ProcessZonePacketUpDetour(IntPtr a1, IntPtr dataPtr, IntPtr a3, byte a4)
{
this.hitchDetectorUp.Start();
try
{
// Call events
// TODO: Implement actor IDs
this.NetworkMessage?.Invoke(dataPtr + 0x20, (ushort)Marshal.ReadInt16(dataPtr), 0x0, 0x0, NetworkMessageDirection.ZoneUp);
}
catch (Exception ex)
{
string header;
try
{
var data = new byte[32];
Marshal.Copy(dataPtr, data, 0, 32);
header = BitConverter.ToString(data);
}
catch (Exception)
{
header = "failed";
}
Log.Error(ex, "Exception on ProcessZonePacketUp hook. Header: " + header);
}
this.hitchDetectorUp.Stop();
return this.processZonePacketUpHook.Original(a1, dataPtr, a3, a4);
}
}

View file

@ -1,20 +0,0 @@
using Dalamud.Plugin.Services;
namespace Dalamud.Game.Network;
/// <summary>
/// The address resolver for the <see cref="GameNetwork"/> class.
/// </summary>
internal sealed class GameNetworkAddressResolver : BaseAddressResolver
{
/// <summary>
/// Gets the address of the ProcessZonePacketUp method.
/// </summary>
public IntPtr ProcessZonePacketUp { get; private set; }
/// <inheritdoc/>
protected override void Setup64Bit(ISigScanner sig)
{
this.ProcessZonePacketUp = sig.ScanText("48 89 5C 24 ?? 48 89 74 24 ?? 4C 89 64 24 ?? 55 41 56 41 57 48 8B EC 48 83 EC 70"); // unnamed in cs
}
}

View file

@ -1,4 +1,3 @@
using System.Collections.Generic;
using System.Net.Http;
using System.Text;
using System.Threading.Tasks;
@ -8,6 +7,7 @@ using Dalamud.Game.Network.Structures;
using Dalamud.Networking.Http;
using Newtonsoft.Json;
using Serilog;
namespace Dalamud.Game.Network.Internal.MarketBoardUploaders.Universalis;
@ -64,7 +64,7 @@ internal class UniversalisMarketBoardUploader : IMarketBoardUploader
PricePerUnit = marketBoardItemListing.PricePerUnit,
Quantity = marketBoardItemListing.ItemQuantity,
RetainerCity = marketBoardItemListing.RetainerCityId,
Materia = new List<UniversalisItemMateria>(),
Materia = [],
};
#pragma warning restore CS0618 // Type or member is obsolete

View file

@ -33,7 +33,7 @@ namespace Dalamud.Game.Network.Internal;
[ServiceManager.EarlyLoadedService]
internal unsafe class NetworkHandlers : IInternalDisposableService
{
private readonly IMarketBoardUploader uploader;
private readonly UniversalisMarketBoardUploader uploader;
private readonly IDisposable handleMarketBoardItemRequest;
private readonly IDisposable handleMarketTaxRates;
@ -55,10 +55,7 @@ internal unsafe class NetworkHandlers : IInternalDisposableService
private bool disposing;
[ServiceManager.ServiceConstructor]
private NetworkHandlers(
GameNetwork gameNetwork,
TargetSigScanner sigScanner,
HappyHttpClient happyHttpClient)
private NetworkHandlers(TargetSigScanner sigScanner, HappyHttpClient happyHttpClient)
{
this.uploader = new UniversalisMarketBoardUploader(happyHttpClient);
@ -419,7 +416,7 @@ internal unsafe class NetworkHandlers : IInternalDisposableService
private IDisposable HandleMarketBoardItemRequest()
{
void LogStartObserved(MarketBoardItemRequest request)
static void LogStartObserved(MarketBoardItemRequest request)
{
Log.Verbose("Observed start of request for item with {NumListings} expected listings", request.AmountToArrive);
}
@ -448,7 +445,7 @@ internal unsafe class NetworkHandlers : IInternalDisposableService
private void UploadMarketBoardData(
MarketBoardItemRequest request,
(uint CatalogId, ICollection<MarketBoardHistory.MarketBoardHistoryListing> Sales) sales,
ICollection<MarketBoardCurrentOfferings.MarketBoardItemListing> listings,
List<MarketBoardCurrentOfferings.MarketBoardItemListing> listings,
ulong uploaderId,
uint worldId)
{

View file

@ -3,6 +3,7 @@ namespace Dalamud.Game.Network;
/// <summary>
/// This represents the direction of a network message.
/// </summary>
[Obsolete("No longer part of public API", true)]
public enum NetworkMessageDirection
{
/// <summary>

View file

@ -11,7 +11,9 @@ using System.Threading;
using Dalamud.Plugin.Services;
using Iced.Intel;
using Newtonsoft.Json;
using Serilog;
namespace Dalamud.Game;

View file

@ -48,7 +48,7 @@ namespace Dalamud.Game.Text.Evaluator;
[ResolveVia<ISeStringEvaluator>]
internal class SeStringEvaluator : IServiceType, ISeStringEvaluator
{
private static readonly ModuleLog Log = new("SeStringEvaluator");
private static readonly ModuleLog Log = ModuleLog.Create<SeStringEvaluator>();
[ServiceManager.ServiceDependency]
private readonly ClientState.ClientState clientState = Service<ClientState.ClientState>.Get();
@ -244,154 +244,67 @@ internal class SeStringEvaluator : IServiceType, ISeStringEvaluator
// if (context.HandlePayload(payload, in context))
// return true;
switch (payload.MacroCode)
return payload.MacroCode switch
{
case MacroCode.SetResetTime:
return this.TryResolveSetResetTime(in context, payload);
case MacroCode.SetTime:
return this.TryResolveSetTime(in context, payload);
case MacroCode.If:
return this.TryResolveIf(in context, payload);
case MacroCode.Switch:
return this.TryResolveSwitch(in context, payload);
case MacroCode.SwitchPlatform:
return this.TryResolveSwitchPlatform(in context, payload);
case MacroCode.PcName:
return this.TryResolvePcName(in context, payload);
case MacroCode.IfPcGender:
return this.TryResolveIfPcGender(in context, payload);
case MacroCode.IfPcName:
return this.TryResolveIfPcName(in context, payload);
// case MacroCode.Josa:
// case MacroCode.Josaro:
case MacroCode.IfSelf:
return this.TryResolveIfSelf(in context, payload);
// case MacroCode.NewLine: // pass through
// case MacroCode.Wait: // pass through
// case MacroCode.Icon: // pass through
case MacroCode.Color:
return this.TryResolveColor(in context, payload);
case MacroCode.EdgeColor:
return this.TryResolveEdgeColor(in context, payload);
case MacroCode.ShadowColor:
return this.TryResolveShadowColor(in context, payload);
// case MacroCode.SoftHyphen: // pass through
// case MacroCode.Key:
// case MacroCode.Scale:
case MacroCode.Bold:
return this.TryResolveBold(in context, payload);
case MacroCode.Italic:
return this.TryResolveItalic(in context, payload);
// case MacroCode.Edge:
// case MacroCode.Shadow:
// case MacroCode.NonBreakingSpace: // pass through
// case MacroCode.Icon2: // pass through
// case MacroCode.Hyphen: // pass through
case MacroCode.Num:
return this.TryResolveNum(in context, payload);
case MacroCode.Hex:
return this.TryResolveHex(in context, payload);
case MacroCode.Kilo:
return this.TryResolveKilo(in context, payload);
// case MacroCode.Byte:
case MacroCode.Sec:
return this.TryResolveSec(in context, payload);
// case MacroCode.Time:
case MacroCode.Float:
return this.TryResolveFloat(in context, payload);
// case MacroCode.Link: // pass through
case MacroCode.Sheet:
return this.TryResolveSheet(in context, payload);
case MacroCode.SheetSub:
return this.TryResolveSheetSub(in context, payload);
case MacroCode.String:
return this.TryResolveString(in context, payload);
case MacroCode.Caps:
return this.TryResolveCaps(in context, payload);
case MacroCode.Head:
return this.TryResolveHead(in context, payload);
case MacroCode.Split:
return this.TryResolveSplit(in context, payload);
case MacroCode.HeadAll:
return this.TryResolveHeadAll(in context, payload);
case MacroCode.Fixed:
return this.TryResolveFixed(in context, payload);
case MacroCode.Lower:
return this.TryResolveLower(in context, payload);
case MacroCode.JaNoun:
return this.TryResolveNoun(ClientLanguage.Japanese, in context, payload);
case MacroCode.EnNoun:
return this.TryResolveNoun(ClientLanguage.English, in context, payload);
case MacroCode.DeNoun:
return this.TryResolveNoun(ClientLanguage.German, in context, payload);
case MacroCode.FrNoun:
return this.TryResolveNoun(ClientLanguage.French, in context, payload);
// case MacroCode.ChNoun:
case MacroCode.LowerHead:
return this.TryResolveLowerHead(in context, payload);
case MacroCode.ColorType:
return this.TryResolveColorType(in context, payload);
case MacroCode.EdgeColorType:
return this.TryResolveEdgeColorType(in context, payload);
// case MacroCode.Ruby:
case MacroCode.Digit:
return this.TryResolveDigit(in context, payload);
case MacroCode.Ordinal:
return this.TryResolveOrdinal(in context, payload);
// case MacroCode.Sound: // pass through
case MacroCode.LevelPos:
return this.TryResolveLevelPos(in context, payload);
default:
return false;
}
MacroCode.SetResetTime => this.TryResolveSetResetTime(in context, payload),
MacroCode.SetTime => this.TryResolveSetTime(in context, payload),
MacroCode.If => this.TryResolveIf(in context, payload),
MacroCode.Switch => this.TryResolveSwitch(in context, payload),
MacroCode.SwitchPlatform => this.TryResolveSwitchPlatform(in context, payload),
MacroCode.PcName => this.TryResolvePcName(in context, payload),
MacroCode.IfPcGender => this.TryResolveIfPcGender(in context, payload),
MacroCode.IfPcName => this.TryResolveIfPcName(in context, payload),
// MacroCode.Josa
// MacroCode.Josaro
MacroCode.IfSelf => this.TryResolveIfSelf(in context, payload),
// MacroCode.NewLine (pass through)
// MacroCode.Wait (pass through)
// MacroCode.Icon (pass through)
MacroCode.Color => this.TryResolveColor(in context, payload),
MacroCode.EdgeColor => this.TryResolveEdgeColor(in context, payload),
MacroCode.ShadowColor => this.TryResolveShadowColor(in context, payload),
// MacroCode.SoftHyphen (pass through)
// MacroCode.Key
// MacroCode.Scale
MacroCode.Bold => this.TryResolveBold(in context, payload),
MacroCode.Italic => this.TryResolveItalic(in context, payload),
// MacroCode.Edge
// MacroCode.Shadow
// MacroCode.NonBreakingSpace (pass through)
// MacroCode.Icon2 (pass through)
// MacroCode.Hyphen (pass through)
MacroCode.Num => this.TryResolveNum(in context, payload),
MacroCode.Hex => this.TryResolveHex(in context, payload),
MacroCode.Kilo => this.TryResolveKilo(in context, payload),
// MacroCode.Byte
MacroCode.Sec => this.TryResolveSec(in context, payload),
// MacroCode.Time
MacroCode.Float => this.TryResolveFloat(in context, payload),
// MacroCode.Link (pass through)
MacroCode.Sheet => this.TryResolveSheet(in context, payload),
MacroCode.SheetSub => this.TryResolveSheetSub(in context, payload),
MacroCode.String => this.TryResolveString(in context, payload),
MacroCode.Caps => this.TryResolveCaps(in context, payload),
MacroCode.Head => this.TryResolveHead(in context, payload),
MacroCode.Split => this.TryResolveSplit(in context, payload),
MacroCode.HeadAll => this.TryResolveHeadAll(in context, payload),
MacroCode.Fixed => this.TryResolveFixed(in context, payload),
MacroCode.Lower => this.TryResolveLower(in context, payload),
MacroCode.JaNoun => this.TryResolveNoun(ClientLanguage.Japanese, in context, payload),
MacroCode.EnNoun => this.TryResolveNoun(ClientLanguage.English, in context, payload),
MacroCode.DeNoun => this.TryResolveNoun(ClientLanguage.German, in context, payload),
MacroCode.FrNoun => this.TryResolveNoun(ClientLanguage.French, in context, payload),
// MacroCode.ChNoun
MacroCode.LowerHead => this.TryResolveLowerHead(in context, payload),
MacroCode.ColorType => this.TryResolveColorType(in context, payload),
MacroCode.EdgeColorType => this.TryResolveEdgeColorType(in context, payload),
// MacroCode.Ruby
MacroCode.Digit => this.TryResolveDigit(in context, payload),
MacroCode.Ordinal => this.TryResolveOrdinal(in context, payload),
// MacroCode.Sound (pass through)
MacroCode.LevelPos => this.TryResolveLevelPos(in context, payload),
_ => false,
};
}
private unsafe bool TryResolveSetResetTime(in SeStringContext context, in ReadOnlySePayloadSpan payload)
@ -932,7 +845,7 @@ internal class SeStringEvaluator : IServiceType, ISeStringEvaluator
using var rssb = new RentedSeStringBuilder();
var sb = rssb.Builder;
sb.Append(this.EvaluateFromAddon(6, [rarity], context.Language));
sb.Append(this.EvaluateFromAddon(6, [rarity], context.Language)); // appends colortype and edgecolortype
if (!skipLink)
sb.PushLink(LinkMacroPayloadType.Item, itemId, rarity, 0u); // arg3 = some LogMessage flag based on LogKind RowId? => "89 5C 24 20 E8 ?? ?? ?? ?? 48 8B 1F"
@ -955,6 +868,9 @@ internal class SeStringEvaluator : IServiceType, ISeStringEvaluator
if (!skipLink)
sb.PopLink();
sb.PopEdgeColorType();
sb.PopColorType();
text = sb.ToReadOnlySeString();
}

View file

@ -85,7 +85,7 @@ internal class NounProcessor : IServiceType
private const int PronounColumnIdx = 6;
private const int ArticleColumnIdx = 7;
private static readonly ModuleLog Log = new("NounProcessor");
private static readonly ModuleLog Log = ModuleLog.Create<NounProcessor>();
[ServiceManager.ServiceDependency]
private readonly DataManager dataManager = Service<DataManager>.Get();

View file

@ -213,11 +213,10 @@ public abstract partial class Payload
return payload;
}
private static Payload DecodeText(BinaryReader reader)
private static TextPayload DecodeText(BinaryReader reader)
{
var payload = new TextPayload();
payload.DecodeImpl(reader, reader.BaseStream.Length);
return payload;
}
}
@ -382,7 +381,7 @@ public abstract partial class Payload
{
if (value < 0xCF)
{
return new byte[] { (byte)(value + 1) };
return [(byte)(value + 1)];
}
var bytes = BitConverter.GetBytes(value);

View file

@ -45,10 +45,10 @@ public class IconPayload : Payload
{
var indexBytes = MakeInteger((uint)this.Icon);
var chunkLen = indexBytes.Length + 1;
var bytes = new List<byte>(new byte[]
{
var bytes = new List<byte>(
[
START_BYTE, (byte)SeStringChunkType.Icon, (byte)chunkLen,
});
]);
bytes.AddRange(indexBytes);
bytes.Add(END_BYTE);
return bytes.ToArray();

View file

@ -8,6 +8,7 @@ using Dalamud.Utility;
using Lumina.Excel;
using Lumina.Excel.Sheets;
using Newtonsoft.Json;
namespace Dalamud.Game.Text.SeStringHandling.Payloads;
@ -172,7 +173,7 @@ public class ItemPayload : Payload
};
bytes.AddRange(idBytes);
// unk
bytes.AddRange(new byte[] { 0x02, 0x01 });
bytes.AddRange([0x02, 0x01]);
// Links don't have to include the name, but if they do, it requires additional work
if (hasName)
@ -183,17 +184,17 @@ public class ItemPayload : Payload
nameLen += 4; // space plus 3 bytes for HQ symbol
}
bytes.AddRange(new byte[]
{
bytes.AddRange(
[
0xFF, // unk
(byte)nameLen,
});
]);
bytes.AddRange(Encoding.UTF8.GetBytes(this.displayName));
if (this.IsHQ)
{
// space and HQ symbol
bytes.AddRange(new byte[] { 0x20, 0xEE, 0x80, 0xBC });
bytes.AddRange([0x20, 0xEE, 0x80, 0xBC]);
}
}

View file

@ -5,6 +5,7 @@ using Dalamud.Data;
using Lumina.Excel;
using Lumina.Excel.Sheets;
using Newtonsoft.Json;
namespace Dalamud.Game.Text.SeStringHandling.Payloads;
@ -174,7 +175,7 @@ public class MapLinkPayload : Payload
bytes.AddRange(yBytes);
// unk
bytes.AddRange(new byte[] { 0xFF, 0x01, END_BYTE });
bytes.AddRange([0xFF, 0x01, END_BYTE]);
return bytes.ToArray();
}

View file

@ -7,7 +7,7 @@ namespace Dalamud.Game.Text.SeStringHandling.Payloads;
/// </summary>
public class NewLinePayload : Payload, ITextProvider
{
private readonly byte[] bytes = { START_BYTE, (byte)SeStringChunkType.NewLine, 0x01, END_BYTE };
private readonly byte[] bytes = [START_BYTE, (byte)SeStringChunkType.NewLine, 0x01, END_BYTE];
/// <summary>
/// Gets an instance of NewLinePayload.

View file

@ -3,6 +3,7 @@ using System.Diagnostics.CodeAnalysis;
using System.IO;
using Lumina.Extensions;
using Newtonsoft.Json;
namespace Dalamud.Game.Text.SeStringHandling.Payloads
@ -97,7 +98,7 @@ namespace Dalamud.Game.Text.SeStringHandling.Payloads
reader.ReadByte();
// if the next byte is 0xF3 then this listing is limited to home world
byte nextByte = reader.ReadByte();
var nextByte = reader.ReadByte();
switch (nextByte)
{
case (byte)PartyFinderLinkType.LimitedToHomeWorld:
@ -121,11 +122,11 @@ namespace Dalamud.Game.Text.SeStringHandling.Payloads
// if the link type is notification, just use premade payload data since it's always the same.
// i have no idea why it is formatted like this, but it is how it is.
// note it is identical to the link terminator payload except the embedded info type is 0x08
if (this.LinkType == PartyFinderLinkType.PartyFinderNotification) return new byte[] { 0x02, 0x27, 0x07, 0x08, 0x01, 0x01, 0x01, 0xFF, 0x01, 0x03, };
if (this.LinkType == PartyFinderLinkType.PartyFinderNotification) return [0x02, 0x27, 0x07, 0x08, 0x01, 0x01, 0x01, 0xFF, 0x01, 0x03,];
// back to our regularly scheduled programming...
var listingIDBytes = MakeInteger(this.ListingId);
bool isFlagSpecified = this.LinkType != PartyFinderLinkType.NotSpecified;
var isFlagSpecified = this.LinkType != PartyFinderLinkType.NotSpecified;
var chunkLen = listingIDBytes.Length + 4;
// 1 more byte for the type flag if it is specified

View file

@ -5,6 +5,7 @@ using Dalamud.Data;
using Lumina.Excel;
using Lumina.Excel.Sheets;
using Newtonsoft.Json;
namespace Dalamud.Game.Text.SeStringHandling.Payloads;
@ -62,7 +63,7 @@ public class QuestPayload : Payload
};
bytes.AddRange(idBytes);
bytes.AddRange(new byte[] { 0x01, 0x01, END_BYTE });
bytes.AddRange([0x01, 0x01, END_BYTE]);
return bytes.ToArray();
}

View file

@ -45,7 +45,7 @@ public class RawPayload : Payload
/// <summary>
/// Gets a fixed Payload representing a common link-termination sequence, found in many payload chains.
/// </summary>
public static RawPayload LinkTerminator => new(new byte[] { 0x02, 0x27, 0x07, 0xCF, 0x01, 0x01, 0x01, 0xFF, 0x01, 0x03 });
public static RawPayload LinkTerminator => new([0x02, 0x27, 0x07, 0xCF, 0x01, 0x01, 0x01, 0xFF, 0x01, 0x03]);
/// <inheritdoc/>
public override PayloadType Type => PayloadType.Unknown;

View file

@ -7,7 +7,7 @@ namespace Dalamud.Game.Text.SeStringHandling.Payloads;
/// </summary>
public class SeHyphenPayload : Payload, ITextProvider
{
private readonly byte[] bytes = { START_BYTE, (byte)SeStringChunkType.SeHyphen, 0x01, END_BYTE };
private readonly byte[] bytes = [START_BYTE, (byte)SeStringChunkType.SeHyphen, 0x01, END_BYTE];
/// <summary>
/// Gets an instance of SeHyphenPayload.

View file

@ -5,6 +5,7 @@ using Dalamud.Data;
using Lumina.Excel;
using Lumina.Excel.Sheets;
using Newtonsoft.Json;
namespace Dalamud.Game.Text.SeStringHandling.Payloads;
@ -63,7 +64,7 @@ public class StatusPayload : Payload
bytes.AddRange(idBytes);
// unk
bytes.AddRange(new byte[] { 0x01, 0x01, 0xFF, 0x02, 0x20, END_BYTE });
bytes.AddRange([0x01, 0x01, 0xFF, 0x02, 0x20, END_BYTE]);
return bytes.ToArray();
}

View file

@ -5,6 +5,7 @@ using Dalamud.Data;
using Lumina.Excel;
using Lumina.Excel.Sheets;
using Newtonsoft.Json;
namespace Dalamud.Game.Text.SeStringHandling.Payloads;
@ -95,10 +96,10 @@ public class UIForegroundPayload : Payload
var colorBytes = MakeInteger(this.colorKey);
var chunkLen = colorBytes.Length + 1;
var bytes = new List<byte>(new byte[]
{
var bytes = new List<byte>(
[
START_BYTE, (byte)SeStringChunkType.UIForeground, (byte)chunkLen,
});
]);
bytes.AddRange(colorBytes);
bytes.Add(END_BYTE);

View file

@ -5,6 +5,7 @@ using Dalamud.Data;
using Lumina.Excel;
using Lumina.Excel.Sheets;
using Newtonsoft.Json;
namespace Dalamud.Game.Text.SeStringHandling.Payloads;
@ -98,10 +99,10 @@ public class UIGlowPayload : Payload
var colorBytes = MakeInteger(this.colorKey);
var chunkLen = colorBytes.Length + 1;
var bytes = new List<byte>(new byte[]
{
var bytes = new List<byte>(
[
START_BYTE, (byte)SeStringChunkType.UIGlow, (byte)chunkLen,
});
]);
bytes.AddRange(colorBytes);
bytes.Add(END_BYTE);

View file

@ -28,7 +28,7 @@ public class SeString
/// </summary>
public SeString()
{
this.Payloads = new List<Payload>();
this.Payloads = [];
}
/// <summary>

View file

@ -22,8 +22,6 @@ using PublicContentSheet = Lumina.Excel.Sheets.PublicContent;
namespace Dalamud.Game.UnlockState;
#pragma warning disable Dalamud001
/// <summary>
/// This class provides unlock state of various content in the game.
/// </summary>
@ -313,9 +311,12 @@ internal unsafe class UnlockState : IInternalDisposableService, IUnlockState
}
/// <inheritdoc/>
public bool IsMcGuffinUnlocked(McGuffin row)
public bool IsLeveCompleted(Leve row)
{
return PlayerState.Instance()->IsMcGuffinUnlocked(row.RowId);
if (!this.IsLoaded)
return false;
return QuestManager.Instance()->IsLevequestComplete((ushort)row.RowId);
}
/// <inheritdoc/>
@ -330,6 +331,15 @@ internal unsafe class UnlockState : IInternalDisposableService, IUnlockState
return this.IsUnlockLinkUnlocked(row.UnlockLink);
}
/// <inheritdoc/>
public bool IsMcGuffinUnlocked(McGuffin row)
{
if (!this.IsLoaded)
return false;
return PlayerState.Instance()->IsMcGuffinUnlocked(row.RowId);
}
/// <inheritdoc/>
public bool IsMountUnlocked(Mount row)
{
@ -378,9 +388,21 @@ internal unsafe class UnlockState : IInternalDisposableService, IUnlockState
return UIState.IsPublicContentUnlocked(row.RowId);
}
/// <inheritdoc/>
public bool IsQuestCompleted(Quest row)
{
if (!this.IsLoaded)
return false;
return QuestManager.IsQuestComplete(row.RowId);
}
/// <inheritdoc/>
public bool IsRecipeUnlocked(Recipe row)
{
if (!this.IsLoaded)
return false;
return this.recipeData.IsRecipeUnlocked(row);
}
@ -511,6 +533,9 @@ internal unsafe class UnlockState : IInternalDisposableService, IUnlockState
if (rowRef.TryGetValue<Item>(out var itemRow))
return this.IsItemUnlocked(itemRow);
if (rowRef.TryGetValue<Leve>(out var leveRow))
return this.IsLeveCompleted(leveRow);
if (rowRef.TryGetValue<MJILandmark>(out var mjiLandmarkRow))
return this.IsMJILandmarkUnlocked(mjiLandmarkRow);
@ -538,6 +563,9 @@ internal unsafe class UnlockState : IInternalDisposableService, IUnlockState
if (rowRef.TryGetValue<PublicContentSheet>(out var publicContentRow))
return this.IsPublicContentUnlocked(publicContentRow);
if (rowRef.TryGetValue<Quest>(out var questRow))
return this.IsQuestCompleted(questRow);
if (rowRef.TryGetValue<Recipe>(out var recipeRow))
return this.IsRecipeUnlocked(recipeRow);
@ -598,6 +626,8 @@ internal unsafe class UnlockState : IInternalDisposableService, IUnlockState
if (!this.IsLoaded)
return;
Log.Verbose("Checking for new unlocks...");
this.UpdateUnlocksForSheet<ActionSheet>();
this.UpdateUnlocksForSheet<AetherCurrent>();
this.UpdateUnlocksForSheet<AetherCurrentCompFlgSet>();
@ -631,6 +661,7 @@ internal unsafe class UnlockState : IInternalDisposableService, IUnlockState
this.UpdateUnlocksForSheet<Ornament>();
this.UpdateUnlocksForSheet<Perform>();
this.UpdateUnlocksForSheet<PublicContentSheet>();
this.UpdateUnlocksForSheet<Quest>();
this.UpdateUnlocksForSheet<Recipe>();
this.UpdateUnlocksForSheet<SecretRecipeBook>();
this.UpdateUnlocksForSheet<Trait>();
@ -639,6 +670,7 @@ internal unsafe class UnlockState : IInternalDisposableService, IUnlockState
// Not implemented:
// - DescriptionPage: quite complex
// - QuestAcceptAdditionCondition: ignored
// - Leve: AgentUpdateFlag.UnlocksUpdate is not set and the completed status can be unset again!
// For some other day:
// - FishingSpot
@ -678,7 +710,7 @@ internal unsafe class UnlockState : IInternalDisposableService, IUnlockState
unlockedRowIds.Add(row.RowId);
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))
{
@ -798,7 +830,7 @@ internal class UnlockStatePluginScoped : IInternalDisposableService, IUnlockStat
public bool IsItemUnlocked(Item row) => this.unlockStateService.IsItemUnlocked(row);
/// <inheritdoc/>
public bool IsMcGuffinUnlocked(McGuffin row) => this.unlockStateService.IsMcGuffinUnlocked(row);
public bool IsLeveCompleted(Leve row) => this.unlockStateService.IsLeveCompleted(row);
/// <inheritdoc/>
public bool IsMJILandmarkUnlocked(MJILandmark row) => this.unlockStateService.IsMJILandmarkUnlocked(row);
@ -806,6 +838,9 @@ internal class UnlockStatePluginScoped : IInternalDisposableService, IUnlockStat
/// <inheritdoc/>
public bool IsMKDLoreUnlocked(MKDLore row) => this.unlockStateService.IsMKDLoreUnlocked(row);
/// <inheritdoc/>
public bool IsMcGuffinUnlocked(McGuffin row) => this.unlockStateService.IsMcGuffinUnlocked(row);
/// <inheritdoc/>
public bool IsMountUnlocked(Mount row) => this.unlockStateService.IsMountUnlocked(row);
@ -824,6 +859,9 @@ internal class UnlockStatePluginScoped : IInternalDisposableService, IUnlockStat
/// <inheritdoc/>
public bool IsPublicContentUnlocked(PublicContentSheet row) => this.unlockStateService.IsPublicContentUnlocked(row);
/// <inheritdoc/>
public bool IsQuestCompleted(Quest row) => this.unlockStateService.IsQuestCompleted(row);
/// <inheritdoc/>
public bool IsRecipeUnlocked(Recipe row) => this.unlockStateService.IsRecipeUnlocked(row);

View file

@ -169,9 +169,6 @@ public sealed class AsmHook : IDisposable, IDalamudHook
/// </summary>
private void CheckDisposed()
{
if (this.IsDisposed)
{
throw new ObjectDisposedException(message: "Hook is already disposed", null);
}
ObjectDisposedException.ThrowIf(this.IsDisposed, this);
}
}

View file

@ -6,6 +6,8 @@ using Dalamud.Configuration.Internal;
using Dalamud.Hooking.Internal;
using Dalamud.Hooking.Internal.Verification;
using TerraFX.Interop.Windows;
namespace Dalamud.Hooking;
/// <summary>
@ -20,6 +22,8 @@ public abstract class Hook<T> : IDalamudHook where T : Delegate
private const ulong IMAGE_ORDINAL_FLAG64 = 0x8000000000000000;
// ReSharper disable once InconsistentNaming
private const uint IMAGE_ORDINAL_FLAG32 = 0x80000000;
// ReSharper disable once InconsistentNaming
private const int IMAGE_DIRECTORY_ENTRY_IMPORT = 1;
#pragma warning restore SA1310
private readonly IntPtr address;
@ -124,25 +128,25 @@ public abstract class Hook<T> : IDalamudHook where T : Delegate
module ??= Process.GetCurrentProcess().MainModule;
if (module == null)
throw new InvalidOperationException("Current module is null?");
var pDos = (PeHeader.IMAGE_DOS_HEADER*)module.BaseAddress;
var pNt = (PeHeader.IMAGE_FILE_HEADER*)(module.BaseAddress + (int)pDos->e_lfanew + 4);
var isPe64 = pNt->SizeOfOptionalHeader == Marshal.SizeOf<PeHeader.IMAGE_OPTIONAL_HEADER64>();
PeHeader.IMAGE_DATA_DIRECTORY* pDataDirectory;
var pDos = (IMAGE_DOS_HEADER*)module.BaseAddress;
var pNt = (IMAGE_FILE_HEADER*)(module.BaseAddress + pDos->e_lfanew + 4);
var isPe64 = pNt->SizeOfOptionalHeader == Marshal.SizeOf<IMAGE_OPTIONAL_HEADER64>();
IMAGE_DATA_DIRECTORY* pDataDirectory;
if (isPe64)
{
var pOpt = (PeHeader.IMAGE_OPTIONAL_HEADER64*)(module.BaseAddress + (int)pDos->e_lfanew + 4 + Marshal.SizeOf<PeHeader.IMAGE_FILE_HEADER>());
pDataDirectory = &pOpt->ImportTable;
var pOpt = (IMAGE_OPTIONAL_HEADER64*)(module.BaseAddress + pDos->e_lfanew + 4 + Marshal.SizeOf<IMAGE_FILE_HEADER>());
pDataDirectory = &pOpt->DataDirectory[IMAGE_DIRECTORY_ENTRY_IMPORT];
}
else
{
var pOpt = (PeHeader.IMAGE_OPTIONAL_HEADER32*)(module.BaseAddress + (int)pDos->e_lfanew + 4 + Marshal.SizeOf<PeHeader.IMAGE_FILE_HEADER>());
pDataDirectory = &pOpt->ImportTable;
var pOpt = (IMAGE_OPTIONAL_HEADER32*)(module.BaseAddress + pDos->e_lfanew + 4 + Marshal.SizeOf<IMAGE_FILE_HEADER>());
pDataDirectory = &pOpt->DataDirectory[IMAGE_DIRECTORY_ENTRY_IMPORT];
}
var moduleNameLowerWithNullTerminator = (moduleName + "\0").ToLowerInvariant();
foreach (ref var importDescriptor in new Span<PeHeader.IMAGE_IMPORT_DESCRIPTOR>(
(PeHeader.IMAGE_IMPORT_DESCRIPTOR*)(module.BaseAddress + (int)pDataDirectory->VirtualAddress),
(int)(pDataDirectory->Size / Marshal.SizeOf<PeHeader.IMAGE_IMPORT_DESCRIPTOR>())))
foreach (ref var importDescriptor in new Span<IMAGE_IMPORT_DESCRIPTOR>(
(IMAGE_IMPORT_DESCRIPTOR*)(module.BaseAddress + (int)pDataDirectory->VirtualAddress),
(int)(pDataDirectory->Size / Marshal.SizeOf<IMAGE_IMPORT_DESCRIPTOR>())))
{
// Having all zero values signals the end of the table. We didn't find anything.
if (importDescriptor.Characteristics == 0)
@ -160,7 +164,7 @@ public abstract class Hook<T> : IDalamudHook where T : Delegate
(int)Math.Min(pDataDirectory->Size + pDataDirectory->VirtualAddress - importDescriptor.Name, moduleNameLowerWithNullTerminator.Length));
// Is this entry about the DLL that we're looking for? (Case insensitive)
if (currentDllNameWithNullTerminator.ToLowerInvariant() != moduleNameLowerWithNullTerminator)
if (!currentDllNameWithNullTerminator.Equals(moduleNameLowerWithNullTerminator, StringComparison.InvariantCultureIgnoreCase))
continue;
if (isPe64)
@ -245,13 +249,10 @@ public abstract class Hook<T> : IDalamudHook where T : Delegate
/// </summary>
protected void CheckDisposed()
{
if (this.IsDisposed)
{
throw new ObjectDisposedException(message: "Hook is already disposed", null);
}
ObjectDisposedException.ThrowIf(this.IsDisposed, this);
}
private static unsafe IntPtr FromImportHelper32(IntPtr baseAddress, ref PeHeader.IMAGE_IMPORT_DESCRIPTOR desc, ref PeHeader.IMAGE_DATA_DIRECTORY dir, string functionName, uint hintOrOrdinal)
private static unsafe IntPtr FromImportHelper32(IntPtr baseAddress, ref IMAGE_IMPORT_DESCRIPTOR desc, ref IMAGE_DATA_DIRECTORY dir, string functionName, uint hintOrOrdinal)
{
var importLookupsOversizedSpan = new Span<uint>((uint*)(baseAddress + (int)desc.OriginalFirstThunk), (int)((dir.Size - desc.OriginalFirstThunk) / Marshal.SizeOf<int>()));
var importAddressesOversizedSpan = new Span<uint>((uint*)(baseAddress + (int)desc.FirstThunk), (int)((dir.Size - desc.FirstThunk) / Marshal.SizeOf<int>()));
@ -301,7 +302,7 @@ public abstract class Hook<T> : IDalamudHook where T : Delegate
throw new MissingMethodException("Specified method not found");
}
private static unsafe IntPtr FromImportHelper64(IntPtr baseAddress, ref PeHeader.IMAGE_IMPORT_DESCRIPTOR desc, ref PeHeader.IMAGE_DATA_DIRECTORY dir, string functionName, uint hintOrOrdinal)
private static unsafe IntPtr FromImportHelper64(IntPtr baseAddress, ref IMAGE_IMPORT_DESCRIPTOR desc, ref IMAGE_DATA_DIRECTORY dir, string functionName, uint hintOrOrdinal)
{
var importLookupsOversizedSpan = new Span<ulong>((ulong*)(baseAddress + (int)desc.OriginalFirstThunk), (int)((dir.Size - desc.OriginalFirstThunk) / Marshal.SizeOf<ulong>()));
var importAddressesOversizedSpan = new Span<ulong>((ulong*)(baseAddress + (int)desc.FirstThunk), (int)((dir.Size - desc.FirstThunk) / Marshal.SizeOf<ulong>()));

View file

@ -1,8 +1,8 @@
using System.Collections.Generic;
using System.Reflection;
using System.Runtime.InteropServices;
using JetBrains.Annotations;
using Windows.Win32.System.Memory;
using Win32Exception = System.ComponentModel.Win32Exception;
@ -45,7 +45,7 @@ internal unsafe class FunctionPointerVariableHook<T> : Hook<T>
if (!HookManager.MultiHookTracker.TryGetValue(this.Address, out var indexList))
{
indexList = HookManager.MultiHookTracker[this.Address] = new List<IDalamudHook>();
indexList = HookManager.MultiHookTracker[this.Address] = [];
}
this.detourDelegate = detour;

View file

@ -1,4 +1,4 @@
using System.Diagnostics;
using System.Diagnostics;
using System.Linq;
using Dalamud.Game;
@ -8,6 +8,7 @@ using Dalamud.Plugin.Internal.Types;
using Dalamud.Plugin.Services;
using Dalamud.Utility;
using Dalamud.Utility.Signatures;
using Serilog;
namespace Dalamud.Hooking.Internal;
@ -25,7 +26,7 @@ internal class GameInteropProviderPluginScoped : IGameInteropProvider, IInternal
private readonly LocalPlugin plugin;
private readonly SigScanner scanner;
private readonly WeakConcurrentCollection<IDalamudHook> trackedHooks = new();
private readonly WeakConcurrentCollection<IDalamudHook> trackedHooks = [];
/// <summary>
/// Initializes a new instance of the <see cref="GameInteropProviderPluginScoped"/> class.

View file

@ -2,6 +2,7 @@ using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Linq;
using System.Runtime.InteropServices;
using System.Threading;
using Dalamud.Logging.Internal;
using Dalamud.Memory;
@ -20,7 +21,7 @@ internal class HookManager : IInternalDisposableService
/// <summary>
/// Logger shared with <see cref="Unhooker"/>.
/// </summary>
internal static readonly ModuleLog Log = new("HM");
internal static readonly ModuleLog Log = ModuleLog.Create<HookManager>();
[ServiceManager.ServiceConstructor]
private HookManager()
@ -30,7 +31,7 @@ internal class HookManager : IInternalDisposableService
/// <summary>
/// Gets sync root object for hook enabling/disabling.
/// </summary>
internal static object HookEnableSyncRoot { get; } = new();
internal static Lock HookEnableSyncRoot { get; } = new();
/// <summary>
/// Gets a static list of tracked and registered hooks.

View file

@ -24,7 +24,7 @@ internal class MinHookHook<T> : Hook<T> where T : Delegate
var unhooker = HookManager.RegisterUnhooker(this.Address);
if (!HookManager.MultiHookTracker.TryGetValue(this.Address, out var indexList))
indexList = HookManager.MultiHookTracker[this.Address] = new();
indexList = HookManager.MultiHookTracker[this.Address] = [];
var index = (ulong)indexList.Count;

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

@ -1,8 +1,13 @@
using System.Linq;
using System.Reflection;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using Dalamud.Game;
using Dalamud.Logging.Internal;
using InteropGenerator.Runtime;
namespace Dalamud.Hooking.Internal.Verification;
/// <summary>
@ -19,11 +24,13 @@ internal static class HookVerifier
new(
"ActorControlSelf",
"E8 ?? ?? ?? ?? 0F B7 0B 83 E9 64",
typeof(ActorControlSelfDelegate),
typeof(ActorControlSelfDelegate), // TODO: change this to CS delegate
"Signature changed in Patch 7.4") // 7.4 (new parameters)
];
private delegate void ActorControlSelfDelegate(uint category, uint eventId, uint param1, uint param2, uint param3, uint param4, uint param5, uint param6, uint param7, uint param8, ulong targetId, byte param9);
private static readonly string ClientStructsInteropNamespacePrefix = string.Join(".", nameof(FFXIVClientStructs), nameof(FFXIVClientStructs.Interop));
private delegate void ActorControlSelfDelegate(uint category, uint eventId, uint param1, uint param2, uint param3, uint param4, uint param5, uint param6, uint param7, uint param8, ulong targetId, byte param9); // TODO: change this to CS delegate
/// <summary>
/// Initializes a new instance of the <see cref="HookVerifier"/> class.
@ -71,7 +78,7 @@ internal static class HookVerifier
var enforcedInvoke = entry.TargetDelegateType.GetMethod("Invoke")!;
// Compare Return Type
var mismatch = passedInvoke.ReturnType != enforcedInvoke.ReturnType;
var mismatch = !CheckParam(passedInvoke.ReturnType, enforcedInvoke.ReturnType);
// Compare Parameter Count
var passedParams = passedInvoke.GetParameters();
@ -86,7 +93,7 @@ internal static class HookVerifier
// Compare Parameter Types
for (var i = 0; i < passedParams.Length; i++)
{
if (passedParams[i].ParameterType != enforcedParams[i].ParameterType)
if (!CheckParam(passedParams[i].ParameterType, enforcedParams[i].ParameterType))
{
mismatch = true;
break;
@ -100,6 +107,45 @@ internal static class HookVerifier
}
}
private static bool CheckParam(Type paramLeft, Type paramRight)
{
var sameType = paramLeft == paramRight;
return sameType || SizeOf(paramLeft) == SizeOf(paramRight);
}
private static int SizeOf(Type type)
{
return type switch {
_ when type == typeof(sbyte) || type == typeof(byte) || type == typeof(bool) => 1,
_ when type == typeof(char) || type == typeof(short) || type == typeof(ushort) || type == typeof(Half) => 2,
_ when type == typeof(int) || type == typeof(uint) || type == typeof(float) => 4,
_ when type == typeof(long) || type == typeof(ulong) || type == typeof(double) || type.IsPointer || type.IsFunctionPointer || type.IsUnmanagedFunctionPointer || (type.Name == "Pointer`1" && type.Namespace.AsSpan().SequenceEqual(ClientStructsInteropNamespacePrefix)) || type == typeof(CStringPointer) => 8,
_ when type.Name.StartsWith("FixedSizeArray") => SizeOf(type.GetGenericArguments()[0]) * int.Parse(type.Name[14..type.Name.IndexOf('`')]),
_ when type.GetCustomAttribute<InlineArrayAttribute>() is { Length: var length } => SizeOf(type.GetGenericArguments()[0]) * length,
_ when 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.IsGenericType => Marshal.SizeOf(Activator.CreateInstance(type)!),
_ => GetSizeOf(type),
};
}
private static int GetSizeOf(Type type)
{
try
{
return Marshal.SizeOf(Activator.CreateInstance(type)!);
}
catch
{
return 0;
}
}
private static bool IsStruct(Type type)
{
return type != typeof(decimal) && type is { IsValueType: true, IsPrimitive: false, IsEnum: false };
}
private record VerificationEntry(string Name, string Signature, Type TargetDelegateType, string Message)
{
public nint Address { get; set; }

View file

@ -17,10 +17,10 @@ namespace Dalamud.Hooking.WndProcHook;
[ServiceManager.EarlyLoadedService]
internal sealed class WndProcHookManager : IInternalDisposableService
{
private static readonly ModuleLog Log = new(nameof(WndProcHookManager));
private static readonly ModuleLog Log = ModuleLog.Create<WndProcHookManager>();
private readonly Hook<DispatchMessageWDelegate> dispatchMessageWHook;
private readonly Dictionary<HWND, WndProcEventArgs> wndProcOverrides = new();
private readonly Dictionary<HWND, WndProcEventArgs> wndProcOverrides = [];
private HWND mainWindowHwnd;

View file

@ -56,11 +56,10 @@ public static class ColorHelpers
var min = Math.Min(r, Math.Min(g, b));
var h = max;
var s = max;
var v = max;
var d = max - min;
s = max == 0 ? 0 : d / max;
var s = max == 0 ? 0 : d / max;
if (max == min)
{

Some files were not shown because too many files have changed in this diff Show more