Compare commits

...

135 commits

Author SHA1 Message Date
Haselnussbomber
1779d2681a Switch to CS in UnlockState
Some checks are pending
Build Dalamud / Build on Windows (push) Waiting to run
Build Dalamud / Check API Compatibility (push) Blocked by required conditions
Build Dalamud / Deploy dalamud-distrib staging (push) Blocked by required conditions
Tag Build / Tag Build (push) Successful in 2s
2026-02-13 20:20:10 +10:00
Haselnussbomber
4651397808 Add support for Adventures to IUnlockState 2026-02-13 20:20:10 +10:00
Haselnussbomber
1ba18e54bf Add support for Titles to IUnlockState 2026-02-13 20:20:10 +10:00
Haselnussbomber
907b585b75 Add support for Achievements to IUnlockState 2026-02-13 20:20:10 +10:00
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 5059 additions and 3410 deletions

View file

@ -75,6 +75,17 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Lumina.Excel.Generator", "l
EndProject EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Lumina.Excel", "lib\Lumina.Excel\src\Lumina.Excel\Lumina.Excel.csproj", "{88FB719B-EB41-73C5-8D25-C03E0C69904F}" Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Lumina.Excel", "lib\Lumina.Excel\src\Lumina.Excel\Lumina.Excel.csproj", "{88FB719B-EB41-73C5-8D25-C03E0C69904F}"
EndProject 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 Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU 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}.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.ActiveCfg = Release|Any CPU
{88FB719B-EB41-73C5-8D25-C03E0C69904F}.Release|Any CPU.Build.0 = 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 EndGlobalSection
GlobalSection(SolutionProperties) = preSolution GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE HideSolutionNode = FALSE
@ -197,6 +220,9 @@ Global
{02EA681E-C7D8-13C7-8484-4AC65E1B71E8} = {E15BDA6D-E881-4482-94BA-BE5527E917FF} {02EA681E-C7D8-13C7-8484-4AC65E1B71E8} = {E15BDA6D-E881-4482-94BA-BE5527E917FF}
{5A44DF0C-C9DA-940F-4D6B-4A11D13AEA3D} = {02EA681E-C7D8-13C7-8484-4AC65E1B71E8} {5A44DF0C-C9DA-940F-4D6B-4A11D13AEA3D} = {02EA681E-C7D8-13C7-8484-4AC65E1B71E8}
{88FB719B-EB41-73C5-8D25-C03E0C69904F} = {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 EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {79B65AC9-C940-410E-AB61-7EA7E12C7599} SolutionGuid = {79B65AC9-C940-410E-AB61-7EA7E12C7599}

View file

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

View file

@ -31,5 +31,5 @@ internal sealed class DevPluginSettings
/// <summary> /// <summary>
/// Gets or sets a list of validation problems that have been dismissed by the user. /// Gets or sets a list of validation problems that have been dismissed by the user.
/// </summary> /// </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.Linq;
using System.Runtime.CompilerServices; using System.Runtime.CompilerServices;
using System.Text.RegularExpressions; using System.Text.RegularExpressions;
@ -17,9 +17,9 @@ namespace Dalamud.Console;
[ServiceManager.BlockingEarlyLoadedService("Console is needed by other blocking early loaded services.")] [ServiceManager.BlockingEarlyLoadedService("Console is needed by other blocking early loaded services.")]
internal partial class ConsoleManager : IServiceType 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> /// <summary>
/// Initializes a new instance of the <see cref="ConsoleManager"/> class. /// Initializes a new instance of the <see cref="ConsoleManager"/> class.
@ -99,10 +99,7 @@ internal partial class ConsoleManager : IServiceType
ArgumentNullException.ThrowIfNull(name); ArgumentNullException.ThrowIfNull(name);
ArgumentNullException.ThrowIfNull(alias); ArgumentNullException.ThrowIfNull(alias);
var target = this.FindEntry(name); var target = this.FindEntry(name) ?? throw new EntryNotFoundException(name);
if (target == null)
throw new EntryNotFoundException(name);
if (this.FindEntry(alias) != null) if (this.FindEntry(alias) != null)
throw new InvalidOperationException($"Entry '{alias}' already exists."); throw new InvalidOperationException($"Entry '{alias}' already exists.");
@ -346,7 +343,7 @@ internal partial class ConsoleManager : IServiceType
private static class Traits 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) if (argument == null && !typeof(T).IsValueType)
throw new ArgumentNullException(paramName); throw new ArgumentNullException(paramName);

View file

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

View file

@ -6,7 +6,7 @@
<PropertyGroup Label="Feature"> <PropertyGroup Label="Feature">
<Description>XIV Launcher addon framework</Description> <Description>XIV Launcher addon framework</Description>
<DalamudVersion>14.0.0.2</DalamudVersion> <DalamudVersion>14.0.2.1</DalamudVersion>
<AssemblyVersion>$(DalamudVersion)</AssemblyVersion> <AssemblyVersion>$(DalamudVersion)</AssemblyVersion>
<Version>$(DalamudVersion)</Version> <Version>$(DalamudVersion)</Version>
<FileVersion>$(DalamudVersion)</FileVersion> <FileVersion>$(DalamudVersion)</FileVersion>
@ -65,7 +65,6 @@
<PackageReference Include="CheapLoc" /> <PackageReference Include="CheapLoc" />
<PackageReference Include="DotNet.ReproducibleBuilds" PrivateAssets="all" /> <PackageReference Include="DotNet.ReproducibleBuilds" PrivateAssets="all" />
<PackageReference Include="goatcorp.Reloaded.Hooks" /> <PackageReference Include="goatcorp.Reloaded.Hooks" />
<PackageReference Include="goatcorp.Reloaded.Assembler" />
<PackageReference Include="JetBrains.Annotations" /> <PackageReference Include="JetBrains.Annotations" />
<PackageReference Include="Lumina" /> <PackageReference Include="Lumina" />
<PackageReference Include="Microsoft.Extensions.ObjectPool" /> <PackageReference Include="Microsoft.Extensions.ObjectPool" />
@ -88,6 +87,15 @@
<PackageReference Include="TerraFX.Interop.Windows" /> <PackageReference Include="TerraFX.Interop.Windows" />
</ItemGroup> </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> <ItemGroup>
<EmbeddedResource Include="Interface\ImGuiBackend\Renderers\imgui-frag.hlsl.bytes"> <EmbeddedResource Include="Interface\ImGuiBackend\Renderers\imgui-frag.hlsl.bytes">
<LogicalName>imgui-frag.hlsl.bytes</LogicalName> <LogicalName>imgui-frag.hlsl.bytes</LogicalName>

View file

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

View file

@ -3,7 +3,9 @@ using System.Collections.Generic;
using Dalamud.Hooking; using Dalamud.Hooking;
using Dalamud.Logging.Internal; using Dalamud.Logging.Internal;
using Dalamud.Memory; using Dalamud.Memory;
using FFXIVClientStructs.FFXIV.Client.LayoutEngine; using FFXIVClientStructs.FFXIV.Client.LayoutEngine;
using Lumina.Text.ReadOnly; using Lumina.Text.ReadOnly;
namespace Dalamud.Data; namespace Dalamud.Data;
@ -13,7 +15,7 @@ namespace Dalamud.Data;
/// </summary> /// </summary>
internal sealed unsafe class RsvResolver : IDisposable 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; private readonly Hook<LayoutWorld.Delegates.AddRsvString> addRsvStringHook;

View file

@ -1,8 +1,6 @@
using System.ComponentModel; using System.ComponentModel;
using System.Diagnostics; using System.Diagnostics;
using System.IO; using System.IO;
using System.Net;
using System.Reflection;
using System.Runtime.InteropServices; using System.Runtime.InteropServices;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
@ -16,10 +14,13 @@ using Dalamud.Plugin.Internal;
using Dalamud.Storage; using Dalamud.Storage;
using Dalamud.Support; using Dalamud.Support;
using Dalamud.Utility; using Dalamud.Utility;
using Newtonsoft.Json; using Newtonsoft.Json;
using Serilog; using Serilog;
using Serilog.Core; using Serilog.Core;
using Serilog.Events; using Serilog.Events;
using Windows.Win32.Foundation; using Windows.Win32.Foundation;
using Windows.Win32.UI.WindowsAndMessaging; 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.Plugin.Services;
using Dalamud.Utility;
using FFXIVClientStructs.FFXIV.Component.GUI; using FFXIVClientStructs.FFXIV.Component.GUI;

View file

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

View file

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

View file

@ -5,7 +5,6 @@ using Dalamud.Game.Addon.Events.EventDataTypes;
using Dalamud.Game.Gui; using Dalamud.Game.Gui;
using Dalamud.Logging.Internal; using Dalamud.Logging.Internal;
using Dalamud.Plugin.Services; using Dalamud.Plugin.Services;
using Dalamud.Utility;
using FFXIVClientStructs.FFXIV.Component.GUI; using FFXIVClientStructs.FFXIV.Component.GUI;
@ -16,7 +15,7 @@ namespace Dalamud.Game.Addon.Events;
/// </summary> /// </summary>
internal unsafe class PluginEventController : IDisposable internal unsafe class PluginEventController : IDisposable
{ {
private static readonly ModuleLog Log = new("AddonEventManager"); private static readonly ModuleLog Log = ModuleLog.Create<AddonEventManager>();
/// <summary> /// <summary>
/// Initializes a new instance of the <see cref="PluginEventController"/> class. /// 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 AddonEventListener EventListener { get; init; }
private List<AddonEventEntry> Events { get; } = new(); private List<AddonEventEntry> Events { get; } = [];
/// <summary> /// <summary>
/// Adds a tracked event. /// 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. /// Contains argument data for Close.
/// </summary> /// </summary>
Close, 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. /// Be aware this is only called for certain popup windows, it is not triggered when clicking on windows.
/// </remarks> /// </remarks>
PostFocus, 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> /// </summary>
public static readonly List<AddonVirtualTable> AllocatedTables = []; 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 Hook<AtkUnitBase.Delegates.Initialize>? onInitializeAddonHook;
private bool isInvokingListeners;
[ServiceManager.ServiceConstructor] [ServiceManager.ServiceConstructor]
private AddonLifecycle() private AddonLifecycle()
@ -52,26 +56,36 @@ internal unsafe class AddonLifecycle : IInternalDisposableService
AllocatedTables.Clear(); 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> /// <summary>
/// Register a listener for the target event and addon. /// Register a listener for the target event and addon.
/// </summary> /// </summary>
/// <param name="listener">The listener to register.</param> /// <param name="listener">The listener to register.</param>
internal void RegisterListener(AddonLifecycleEventListener listener) internal void RegisterListener(AddonLifecycleEventListener listener)
{ {
if (!this.EventListeners.ContainsKey(listener.EventType)) if (this.isInvokingListeners)
{ {
if (!this.EventListeners.TryAdd(listener.EventType, [])) this.framework.RunOnTick(() => this.RegisterListenerMethod(listener));
return;
} }
else
// 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, [])) this.framework.RunOnFrameworkThread(() => this.RegisterListenerMethod(listener));
return;
} }
this.EventListeners[listener.EventType][listener.AddonName].Add(listener);
} }
/// <summary> /// <summary>
@ -80,12 +94,13 @@ internal unsafe class AddonLifecycle : IInternalDisposableService
/// <param name="listener">The listener to unregister.</param> /// <param name="listener">The listener to unregister.</param>
internal void UnregisterListener(AddonLifecycleEventListener listener) internal void UnregisterListener(AddonLifecycleEventListener listener)
{ {
if (this.EventListeners.TryGetValue(listener.EventType, out var addonListeners)) if (this.isInvokingListeners)
{ {
if (addonListeners.TryGetValue(listener.AddonName, out var addonListener)) this.framework.RunOnTick(() => this.UnregisterListenerMethod(listener));
{ }
addonListener.Remove(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> /// <param name="blame">What to blame on errors.</param>
internal void InvokeListenersSafely(AddonEvent eventType, AddonArgs args, [CallerMemberName] string blame = "") 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 // Early return if we don't have any listeners of this type
if (!this.EventListeners.TryGetValue(eventType, out var addonListeners)) return; if (!this.EventListeners.TryGetValue(eventType, out var addonListeners)) return;
@ -131,19 +148,41 @@ internal unsafe class AddonLifecycle : IInternalDisposableService
} }
} }
} }
this.isInvokingListeners = false;
} }
/// <summary> private void RegisterListenerMethod(AddonLifecycleEventListener listener)
/// 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)
{ {
var matchedTable = AllocatedTables.FirstOrDefault(table => table.ModifiedVirtualTable == tableAddress); if (!this.EventListeners.ContainsKey(listener.EventType))
if (matchedTable == null) return null; {
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) private void OnAddonInitialize(AtkUnitBase* addon)
@ -263,5 +302,5 @@ internal class AddonLifecyclePluginScoped : IInternalDisposableService, IAddonLi
/// <inheritdoc/> /// <inheritdoc/>
public unsafe nint GetOriginalVirtualTable(nint virtualTableAddress) 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 onMouseOverArgs = new();
private readonly AddonArgs onMouseOutArgs = new(); private readonly AddonArgs onMouseOutArgs = new();
private readonly AddonArgs focusArgs = new(); private readonly AddonArgs focusArgs = new();
private readonly AddonFocusChangedArgs focusChangedArgs = new();
private readonly AtkUnitBase* atkUnitBase; private readonly AtkUnitBase* atkUnitBase;
@ -63,6 +64,7 @@ internal unsafe class AddonVirtualTable : IDisposable
private readonly AtkUnitBase.Delegates.OnMouseOver onMouseOverFunction; private readonly AtkUnitBase.Delegates.OnMouseOver onMouseOverFunction;
private readonly AtkUnitBase.Delegates.OnMouseOut onMouseOutFunction; private readonly AtkUnitBase.Delegates.OnMouseOut onMouseOutFunction;
private readonly AtkUnitBase.Delegates.Focus focusFunction; private readonly AtkUnitBase.Delegates.Focus focusFunction;
private readonly AtkUnitBase.Delegates.OnFocusChange onFocusChangeFunction;
/// <summary> /// <summary>
/// Initializes a new instance of the <see cref="AddonVirtualTable"/> class. /// Initializes a new instance of the <see cref="AddonVirtualTable"/> class.
@ -103,6 +105,7 @@ internal unsafe class AddonVirtualTable : IDisposable
this.onMouseOverFunction = this.OnAddonMouseOver; this.onMouseOverFunction = this.OnAddonMouseOver;
this.onMouseOutFunction = this.OnAddonMouseOut; this.onMouseOutFunction = this.OnAddonMouseOut;
this.focusFunction = this.OnAddonFocus; this.focusFunction = this.OnAddonFocus;
this.onFocusChangeFunction = this.OnAddonFocusChange;
// Overwrite specific virtual table entries // Overwrite specific virtual table entries
this.ModifiedVirtualTable->Dtor = (delegate* unmanaged<AtkUnitBase*, byte, AtkEventListener*>)Marshal.GetFunctionPointerForDelegate(this.destructorFunction); 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->OnMouseOver = (delegate* unmanaged<AtkUnitBase*, void>)Marshal.GetFunctionPointerForDelegate(this.onMouseOverFunction);
this.ModifiedVirtualTable->OnMouseOut = (delegate* unmanaged<AtkUnitBase*, void>)Marshal.GetFunctionPointerForDelegate(this.onMouseOutFunction); this.ModifiedVirtualTable->OnMouseOut = (delegate* unmanaged<AtkUnitBase*, void>)Marshal.GetFunctionPointerForDelegate(this.onMouseOutFunction);
this.ModifiedVirtualTable->Focus = (delegate* unmanaged<AtkUnitBase*, void>)Marshal.GetFunctionPointerForDelegate(this.focusFunction); this.ModifiedVirtualTable->Focus = (delegate* unmanaged<AtkUnitBase*, void>)Marshal.GetFunctionPointerForDelegate(this.focusFunction);
this.ModifiedVirtualTable->OnFocusChange = (delegate* unmanaged<AtkUnitBase*, bool, void>)Marshal.GetFunctionPointerForDelegate(this.onFocusChangeFunction);
} }
/// <summary> /// <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")] [Conditional("DEBUG")]
private void LogEvent(bool loggingEnabled, [CallerMemberName] string caller = "") 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> /// <summary>
/// Gets a list of memory addresses that were found, to list in /xldata. /// Gets a list of memory addresses that were found, to list in /xldata.
/// </summary> /// </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> /// <summary>
/// Gets or sets a value indicating whether the resolver has successfully run <see cref="Setup32Bit(ISigScanner)"/> or <see cref="Setup64Bit(ISigScanner)"/>. /// 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.Linq;
using System.Reflection;
using System.Text.RegularExpressions; using System.Text.RegularExpressions;
using CheapLoc; using CheapLoc;
@ -23,7 +22,7 @@ namespace Dalamud.Game;
[ServiceManager.EarlyLoadedService] [ServiceManager.EarlyLoadedService]
internal partial class ChatHandlers : IServiceType internal partial class ChatHandlers : IServiceType
{ {
private static readonly ModuleLog Log = new("ChatHandlers"); private static readonly ModuleLog Log = ModuleLog.Create<ChatHandlers>();
[ServiceManager.ServiceDependency] [ServiceManager.ServiceDependency]
private readonly DalamudConfiguration configuration = Service<DalamudConfiguration>.Get(); private readonly DalamudConfiguration configuration = Service<DalamudConfiguration>.Get();

View file

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

View file

@ -33,7 +33,7 @@ namespace Dalamud.Game.ClientState;
[ServiceManager.EarlyLoadedService] [ServiceManager.EarlyLoadedService]
internal sealed class ClientState : IInternalDisposableService, IClientState 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 GameLifecycle lifecycle;
private readonly ClientStateAddressResolver address; 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;
using Dalamud.IoC.Internal; using Dalamud.IoC.Internal;
using Dalamud.Plugin.Services; using Dalamud.Plugin.Services;
using FFXIVClientStructs.FFXIV.Client.System.Input; using FFXIVClientStructs.FFXIV.Client.System.Input;
using Serilog; using Serilog;
namespace Dalamud.Game.ClientState.GamePad; 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. // 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)) 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; return (T)gauge;

View file

@ -1,4 +1,5 @@
using Dalamud.Game.ClientState.JobGauge.Enums; using Dalamud.Game.ClientState.JobGauge.Enums;
using FFXIVClientStructs.FFXIV.Client.Game.Gauge; using FFXIVClientStructs.FFXIV.Client.Game.Gauge;
namespace Dalamud.Game.ClientState.JobGauge.Types; namespace Dalamud.Game.ClientState.JobGauge.Types;
@ -82,12 +83,12 @@ public unsafe class BRDGauge : JobGaugeBase<FFXIVClientStructs.FFXIV.Client.Game
{ {
get get
{ {
return new[] return
{ [
this.Struct->SongFlags.HasFlag(SongFlags.MagesBalladCoda) ? Song.Mage : Song.None, 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.ArmysPaeonCoda) ? Song.Army : Song.None,
this.Struct->SongFlags.HasFlag(SongFlags.WanderersMinuetCoda) ? Song.Wanderer : 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 Dalamud.Game.ClientState.JobGauge.Enums;
using FFXIVClientStructs.FFXIV.Client.Game.Gauge; using FFXIVClientStructs.FFXIV.Client.Game.Gauge;
namespace Dalamud.Game.ClientState.JobGauge.Types; 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 CanvasFlags = Dalamud.Game.ClientState.JobGauge.Enums.CanvasFlags;
using CreatureFlags = Dalamud.Game.ClientState.JobGauge.Enums.CreatureFlags; using CreatureFlags = Dalamud.Game.ClientState.JobGauge.Enums.CreatureFlags;
@ -22,45 +22,45 @@ public unsafe class PCTGauge : JobGaugeBase<PictomancerGauge>
/// <summary> /// <summary>
/// Gets the use of subjective pallete. /// Gets the use of subjective pallete.
/// </summary> /// </summary>
public byte PalleteGauge => Struct->PalleteGauge; public byte PalleteGauge => this.Struct->PalleteGauge;
/// <summary> /// <summary>
/// Gets the amount of paint the player has. /// Gets the amount of paint the player has.
/// </summary> /// </summary>
public byte Paint => Struct->Paint; public byte Paint => this.Struct->Paint;
/// <summary> /// <summary>
/// Gets a value indicating whether a creature motif is drawn. /// Gets a value indicating whether a creature motif is drawn.
/// </summary> /// </summary>
public bool CreatureMotifDrawn => Struct->CreatureMotifDrawn; public bool CreatureMotifDrawn => this.Struct->CreatureMotifDrawn;
/// <summary> /// <summary>
/// Gets a value indicating whether a weapon motif is drawn. /// Gets a value indicating whether a weapon motif is drawn.
/// </summary> /// </summary>
public bool WeaponMotifDrawn => Struct->WeaponMotifDrawn; public bool WeaponMotifDrawn => this.Struct->WeaponMotifDrawn;
/// <summary> /// <summary>
/// Gets a value indicating whether a landscape motif is drawn. /// Gets a value indicating whether a landscape motif is drawn.
/// </summary> /// </summary>
public bool LandscapeMotifDrawn => Struct->LandscapeMotifDrawn; public bool LandscapeMotifDrawn => this.Struct->LandscapeMotifDrawn;
/// <summary> /// <summary>
/// Gets a value indicating whether a moogle portrait is ready. /// Gets a value indicating whether a moogle portrait is ready.
/// </summary> /// </summary>
public bool MooglePortraitReady => Struct->MooglePortraitReady; public bool MooglePortraitReady => this.Struct->MooglePortraitReady;
/// <summary> /// <summary>
/// Gets a value indicating whether a madeen portrait is ready. /// Gets a value indicating whether a madeen portrait is ready.
/// </summary> /// </summary>
public bool MadeenPortraitReady => Struct->MadeenPortraitReady; public bool MadeenPortraitReady => this.Struct->MadeenPortraitReady;
/// <summary> /// <summary>
/// Gets which creature flags are present. /// Gets which creature flags are present.
/// </summary> /// </summary>
public CreatureFlags CreatureFlags => (CreatureFlags)Struct->CreatureFlags; public CreatureFlags CreatureFlags => (CreatureFlags)this.Struct->CreatureFlags;
/// <summary> /// <summary>
/// Gets which canvas flags are present. /// Gets which canvas flags are present.
/// </summary> /// </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 Dalamud.Game.ClientState.JobGauge.Enums;
using FFXIVClientStructs.FFXIV.Client.Game.Gauge; using FFXIVClientStructs.FFXIV.Client.Game.Gauge;
namespace Dalamud.Game.ClientState.JobGauge.Types; namespace Dalamud.Game.ClientState.JobGauge.Types;

View file

@ -1,7 +1,5 @@
using FFXIVClientStructs.FFXIV.Client.Game.Gauge; using FFXIVClientStructs.FFXIV.Client.Game.Gauge;
using Reloaded.Memory;
using DreadCombo = Dalamud.Game.ClientState.JobGauge.Enums.DreadCombo; using DreadCombo = Dalamud.Game.ClientState.JobGauge.Enums.DreadCombo;
using SerpentCombo = Dalamud.Game.ClientState.JobGauge.Enums.SerpentCombo; using SerpentCombo = Dalamud.Game.ClientState.JobGauge.Enums.SerpentCombo;
@ -24,25 +22,25 @@ public unsafe class VPRGauge : JobGaugeBase<ViperGauge>
/// <summary> /// <summary>
/// Gets how many uses of uncoiled fury the player has. /// Gets how many uses of uncoiled fury the player has.
/// </summary> /// </summary>
public byte RattlingCoilStacks => Struct->RattlingCoilStacks; public byte RattlingCoilStacks => this.Struct->RattlingCoilStacks;
/// <summary> /// <summary>
/// Gets Serpent Offering stacks and gauge. /// Gets Serpent Offering stacks and gauge.
/// </summary> /// </summary>
public byte SerpentOffering => Struct->SerpentOffering; public byte SerpentOffering => this.Struct->SerpentOffering;
/// <summary> /// <summary>
/// Gets value indicating the use of 1st, 2nd, 3rd, 4th generation and Ouroboros. /// Gets value indicating the use of 1st, 2nd, 3rd, 4th generation and Ouroboros.
/// </summary> /// </summary>
public byte AnguineTribute => Struct->AnguineTribute; public byte AnguineTribute => this.Struct->AnguineTribute;
/// <summary> /// <summary>
/// Gets the last Weaponskill used in DreadWinder/Pit of Dread combo. /// Gets the last Weaponskill used in DreadWinder/Pit of Dread combo.
/// </summary> /// </summary>
public DreadCombo DreadCombo => (DreadCombo)Struct->DreadCombo; public DreadCombo DreadCombo => (DreadCombo)this.Struct->DreadCombo;
/// <summary> /// <summary>
/// Gets current ability for Serpent's Tail. /// Gets current ability for Serpent's Tail.
/// </summary> /// </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/> /// <inheritdoc/>
public IGameObject? Target public IGameObject? Target
{ {
get => this.objectTable.CreateObjectReference((IntPtr)Struct->GetHardTarget()); get => this.objectTable.CreateObjectReference((IntPtr)this.Struct->GetHardTarget());
set => Struct->SetHardTarget((FFXIVClientStructs.FFXIV.Client.Game.Object.GameObject*)value?.Address); set => this.Struct->SetHardTarget((FFXIVClientStructs.FFXIV.Client.Game.Object.GameObject*)value?.Address);
} }
/// <inheritdoc/> /// <inheritdoc/>
public IGameObject? MouseOverTarget public IGameObject? MouseOverTarget
{ {
get => this.objectTable.CreateObjectReference((IntPtr)Struct->MouseOverTarget); get => this.objectTable.CreateObjectReference((IntPtr)this.Struct->MouseOverTarget);
set => Struct->MouseOverTarget = (FFXIVClientStructs.FFXIV.Client.Game.Object.GameObject*)value?.Address; set => this.Struct->MouseOverTarget = (FFXIVClientStructs.FFXIV.Client.Game.Object.GameObject*)value?.Address;
} }
/// <inheritdoc/> /// <inheritdoc/>
public IGameObject? FocusTarget public IGameObject? FocusTarget
{ {
get => this.objectTable.CreateObjectReference((IntPtr)Struct->FocusTarget); get => this.objectTable.CreateObjectReference((IntPtr)this.Struct->FocusTarget);
set => Struct->FocusTarget = (FFXIVClientStructs.FFXIV.Client.Game.Object.GameObject*)value?.Address; set => this.Struct->FocusTarget = (FFXIVClientStructs.FFXIV.Client.Game.Object.GameObject*)value?.Address;
} }
/// <inheritdoc/> /// <inheritdoc/>
public IGameObject? PreviousTarget public IGameObject? PreviousTarget
{ {
get => this.objectTable.CreateObjectReference((IntPtr)Struct->PreviousTarget); get => this.objectTable.CreateObjectReference((IntPtr)this.Struct->PreviousTarget);
set => Struct->PreviousTarget = (FFXIVClientStructs.FFXIV.Client.Game.Object.GameObject*)value?.Address; set => this.Struct->PreviousTarget = (FFXIVClientStructs.FFXIV.Client.Game.Object.GameObject*)value?.Address;
} }
/// <inheritdoc/> /// <inheritdoc/>
public IGameObject? SoftTarget public IGameObject? SoftTarget
{ {
get => this.objectTable.CreateObjectReference((IntPtr)Struct->GetSoftTarget()); get => this.objectTable.CreateObjectReference((IntPtr)this.Struct->GetSoftTarget());
set => Struct->SetSoftTarget((FFXIVClientStructs.FFXIV.Client.Game.Object.GameObject*)value?.Address); set => this.Struct->SetSoftTarget((FFXIVClientStructs.FFXIV.Client.Game.Object.GameObject*)value?.Address);
} }
/// <inheritdoc/> /// <inheritdoc/>
public IGameObject? GPoseTarget public IGameObject? GPoseTarget
{ {
get => this.objectTable.CreateObjectReference((IntPtr)Struct->GPoseTarget); get => this.objectTable.CreateObjectReference((IntPtr)this.Struct->GPoseTarget);
set => Struct->GPoseTarget = (FFXIVClientStructs.FFXIV.Client.Game.Object.GameObject*)value?.Address; set => this.Struct->GPoseTarget = (FFXIVClientStructs.FFXIV.Client.Game.Object.GameObject*)value?.Address;
} }
/// <inheritdoc/> /// <inheritdoc/>
public IGameObject? MouseOverNameplateTarget public IGameObject? MouseOverNameplateTarget
{ {
get => this.objectTable.CreateObjectReference((IntPtr)Struct->MouseOverNameplateTarget); get => this.objectTable.CreateObjectReference((IntPtr)this.Struct->MouseOverNameplateTarget);
set => Struct->MouseOverNameplateTarget = (FFXIVClientStructs.FFXIV.Client.Game.Object.GameObject*)value?.Address; set => this.Struct->MouseOverNameplateTarget = (FFXIVClientStructs.FFXIV.Client.Game.Object.GameObject*)value?.Address;
} }
private TargetSystem* Struct => TargetSystem.Instance(); private TargetSystem* Struct => TargetSystem.Instance();

View file

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

View file

@ -1,9 +1,8 @@
using System.Runtime.CompilerServices;
using Dalamud.Data; using Dalamud.Data;
using Dalamud.Game.ClientState.Customize;
using Dalamud.Game.ClientState.Objects.Enums; using Dalamud.Game.ClientState.Objects.Enums;
using Dalamud.Game.Text.SeStringHandling; using Dalamud.Game.Text.SeStringHandling;
using Dalamud.Memory; using Dalamud.Utility;
using Lumina.Excel; using Lumina.Excel;
using Lumina.Excel.Sheets; using Lumina.Excel.Sheets;
@ -16,68 +15,73 @@ namespace Dalamud.Game.ClientState.Objects.Types;
public interface ICharacter : IGameObject public interface ICharacter : IGameObject
{ {
/// <summary> /// <summary>
/// Gets the current HP of this Chara. /// Gets the current HP of this character.
/// </summary> /// </summary>
public uint CurrentHp { get; } public uint CurrentHp { get; }
/// <summary> /// <summary>
/// Gets the maximum HP of this Chara. /// Gets the maximum HP of this character.
/// </summary> /// </summary>
public uint MaxHp { get; } public uint MaxHp { get; }
/// <summary> /// <summary>
/// Gets the current MP of this Chara. /// Gets the current MP of this character.
/// </summary> /// </summary>
public uint CurrentMp { get; } public uint CurrentMp { get; }
/// <summary> /// <summary>
/// Gets the maximum MP of this Chara. /// Gets the maximum MP of this character.
/// </summary> /// </summary>
public uint MaxMp { get; } public uint MaxMp { get; }
/// <summary> /// <summary>
/// Gets the current GP of this Chara. /// Gets the current GP of this character.
/// </summary> /// </summary>
public uint CurrentGp { get; } public uint CurrentGp { get; }
/// <summary> /// <summary>
/// Gets the maximum GP of this Chara. /// Gets the maximum GP of this character.
/// </summary> /// </summary>
public uint MaxGp { get; } public uint MaxGp { get; }
/// <summary> /// <summary>
/// Gets the current CP of this Chara. /// Gets the current CP of this character.
/// </summary> /// </summary>
public uint CurrentCp { get; } public uint CurrentCp { get; }
/// <summary> /// <summary>
/// Gets the maximum CP of this Chara. /// Gets the maximum CP of this character.
/// </summary> /// </summary>
public uint MaxCp { get; } public uint MaxCp { get; }
/// <summary> /// <summary>
/// Gets the shield percentage of this Chara. /// Gets the shield percentage of this character.
/// </summary> /// </summary>
public byte ShieldPercentage { get; } public byte ShieldPercentage { get; }
/// <summary> /// <summary>
/// Gets the ClassJob of this Chara. /// Gets the ClassJob of this character.
/// </summary> /// </summary>
public RowRef<ClassJob> ClassJob { get; } public RowRef<ClassJob> ClassJob { get; }
/// <summary> /// <summary>
/// Gets the level of this Chara. /// Gets the level of this character.
/// </summary> /// </summary>
public byte Level { get; } public byte Level { get; }
/// <summary> /// <summary>
/// Gets a byte array describing the visual appearance of this Chara. /// Gets a byte array describing the visual appearance of this character.
/// Indexed by <see cref="CustomizeIndex"/>. /// Indexed by <see cref="CustomizeIndex"/>.
/// </summary> /// </summary>
public byte[] Customize { get; } public byte[] Customize { get; }
/// <summary> /// <summary>
/// Gets the Free Company tag of this chara. /// Gets the underlying CustomizeData struct for this character.
/// </summary>
public ICustomizeData CustomizeData { get; }
/// <summary>
/// Gets the Free Company tag of this character.
/// </summary> /// </summary>
public SeString CompanyTag { get; } public SeString CompanyTag { get; }
@ -119,7 +123,7 @@ internal unsafe class Character : GameObject, ICharacter
/// This represents a non-static entity. /// This represents a non-static entity.
/// </summary> /// </summary>
/// <param name="address">The address of this character in memory.</param> /// <param name="address">The address of this character in memory.</param>
internal Character(IntPtr address) internal Character(nint address)
: base(address) : base(address)
{ {
} }
@ -158,8 +162,12 @@ internal unsafe class Character : GameObject, ICharacter
public byte Level => this.Struct->CharacterData.Level; public byte Level => this.Struct->CharacterData.Level;
/// <inheritdoc/> /// <inheritdoc/>
[Api15ToDo("Do not allocate on each call, use the CS Span and let consumers do allocation if necessary")]
public byte[] Customize => this.Struct->DrawData.CustomizeData.Data.ToArray(); public byte[] Customize => this.Struct->DrawData.CustomizeData.Data.ToArray();
/// <inheritdoc/>
public ICustomizeData CustomizeData => new CustomizeData((nint)(&this.Struct->DrawData.CustomizeData));
/// <inheritdoc/> /// <inheritdoc/>
public SeString CompanyTag => SeString.Parse(this.Struct->FreeCompanyTag); public SeString CompanyTag => SeString.Parse(this.Struct->FreeCompanyTag);

View file

@ -47,7 +47,7 @@ internal sealed unsafe partial class PartyList : IServiceType, IPartyList
public unsafe nint GroupManagerAddress => (nint)CSGroupManager.Instance(); public unsafe nint GroupManagerAddress => (nint)CSGroupManager.Instance();
/// <inheritdoc/> /// <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/> /// <inheritdoc/>
public nint AllianceListAddress => (nint)Unsafe.AsPointer(ref this.GroupManagerStruct->MainGroup.AllianceMembers[0]); 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.Objects.Types;
using Dalamud.Game.ClientState.Statuses; using Dalamud.Game.ClientState.Statuses;
using Dalamud.Game.Text.SeStringHandling; using Dalamud.Game.Text.SeStringHandling;
using Dalamud.Utility; using Dalamud.Utility;
using Lumina.Excel; using Lumina.Excel;

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -1,6 +1,7 @@
using System.Collections.Generic; using System.Collections.Generic;
using System.Collections.Immutable; using System.Collections.Immutable;
using System.Linq; using System.Linq;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices; using System.Runtime.InteropServices;
using Dalamud.Configuration.Internal; using Dalamud.Configuration.Internal;
@ -37,14 +38,16 @@ namespace Dalamud.Game.Gui;
[ServiceManager.EarlyLoadedService] [ServiceManager.EarlyLoadedService]
internal sealed unsafe class ChatGui : IInternalDisposableService, IChatGui 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 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<PrintMessageDelegate> printMessageHook;
private readonly Hook<InventoryItem.Delegates.Copy> inventoryItemCopyHook; private readonly Hook<InventoryItem.Delegates.Copy> inventoryItemCopyHook;
private readonly Hook<LogViewer.Delegates.HandleLinkClick> handleLinkClickHook; private readonly Hook<LogViewer.Delegates.HandleLinkClick> handleLinkClickHook;
private readonly Hook<RaptureLogModule.Delegates.Update> handleLogModuleUpdate;
[ServiceManager.ServiceDependency] [ServiceManager.ServiceDependency]
private readonly DalamudConfiguration configuration = Service<DalamudConfiguration>.Get(); 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.printMessageHook = Hook<PrintMessageDelegate>.FromAddress(RaptureLogModule.Addresses.PrintMessage.Value, this.HandlePrintMessageDetour);
this.inventoryItemCopyHook = Hook<InventoryItem.Delegates.Copy>.FromAddress((nint)InventoryItem.StaticVirtualTablePointer->Copy, this.InventoryItemCopyDetour); 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.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.printMessageHook.Enable();
this.inventoryItemCopyHook.Enable(); this.inventoryItemCopyHook.Enable();
this.handleLinkClickHook.Enable(); this.handleLinkClickHook.Enable();
this.handleLogModuleUpdate.Enable();
} }
[UnmanagedFunctionPointer(CallingConvention.ThisCall)] [UnmanagedFunctionPointer(CallingConvention.ThisCall)]
@ -79,6 +84,9 @@ internal sealed unsafe class ChatGui : IInternalDisposableService, IChatGui
/// <inheritdoc/> /// <inheritdoc/>
public event IChatGui.OnMessageUnhandledDelegate? ChatMessageUnhandled; public event IChatGui.OnMessageUnhandledDelegate? ChatMessageUnhandled;
/// <inheritdoc/>
public event IChatGui.OnLogMessageDelegate? LogMessage;
/// <inheritdoc/> /// <inheritdoc/>
public uint LastLinkedItemId { get; private set; } public uint LastLinkedItemId { get; private set; }
@ -110,6 +118,7 @@ internal sealed unsafe class ChatGui : IInternalDisposableService, IChatGui
this.printMessageHook.Dispose(); this.printMessageHook.Dispose();
this.inventoryItemCopyHook.Dispose(); this.inventoryItemCopyHook.Dispose();
this.handleLinkClickHook.Dispose(); this.handleLinkClickHook.Dispose();
this.handleLogModuleUpdate.Dispose();
} }
#region DalamudSeString #region DalamudSeString
@ -493,6 +502,46 @@ internal sealed unsafe class ChatGui : IInternalDisposableService, IChatGui
Log.Error(ex, "Exception in HandleLinkClickDetour"); 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> /// <summary>
@ -521,6 +570,7 @@ internal class ChatGuiPluginScoped : IInternalDisposableService, IChatGui
this.chatGuiService.CheckMessageHandled += this.OnCheckMessageForward; this.chatGuiService.CheckMessageHandled += this.OnCheckMessageForward;
this.chatGuiService.ChatMessageHandled += this.OnMessageHandledForward; this.chatGuiService.ChatMessageHandled += this.OnMessageHandledForward;
this.chatGuiService.ChatMessageUnhandled += this.OnMessageUnhandledForward; this.chatGuiService.ChatMessageUnhandled += this.OnMessageUnhandledForward;
this.chatGuiService.LogMessage += this.OnLogMessageForward;
} }
/// <inheritdoc/> /// <inheritdoc/>
@ -535,6 +585,9 @@ internal class ChatGuiPluginScoped : IInternalDisposableService, IChatGui
/// <inheritdoc/> /// <inheritdoc/>
public event IChatGui.OnMessageUnhandledDelegate? ChatMessageUnhandled; public event IChatGui.OnMessageUnhandledDelegate? ChatMessageUnhandled;
/// <inheritdoc/>
public event IChatGui.OnLogMessageDelegate? LogMessage;
/// <inheritdoc/> /// <inheritdoc/>
public uint LastLinkedItemId => this.chatGuiService.LastLinkedItemId; public uint LastLinkedItemId => this.chatGuiService.LastLinkedItemId;
@ -551,11 +604,13 @@ internal class ChatGuiPluginScoped : IInternalDisposableService, IChatGui
this.chatGuiService.CheckMessageHandled -= this.OnCheckMessageForward; this.chatGuiService.CheckMessageHandled -= this.OnCheckMessageForward;
this.chatGuiService.ChatMessageHandled -= this.OnMessageHandledForward; this.chatGuiService.ChatMessageHandled -= this.OnMessageHandledForward;
this.chatGuiService.ChatMessageUnhandled -= this.OnMessageUnhandledForward; this.chatGuiService.ChatMessageUnhandled -= this.OnMessageUnhandledForward;
this.chatGuiService.LogMessage -= this.OnLogMessageForward;
this.ChatMessage = null; this.ChatMessage = null;
this.CheckMessageHandled = null; this.CheckMessageHandled = null;
this.ChatMessageHandled = null; this.ChatMessageHandled = null;
this.ChatMessageUnhandled = null; this.ChatMessageUnhandled = null;
this.LogMessage = null;
} }
/// <inheritdoc/> /// <inheritdoc/>
@ -609,4 +664,7 @@ internal class ChatGuiPluginScoped : IInternalDisposableService, IChatGui
private void OnMessageUnhandledForward(XivChatType type, int timestamp, SeString sender, SeString message) private void OnMessageUnhandledForward(XivChatType type, int timestamp, SeString sender, SeString message)
=> this.ChatMessageUnhandled?.Invoke(type, timestamp, sender, 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.Linq;
using System.Runtime.CompilerServices; using System.Runtime.CompilerServices;
using System.Runtime.InteropServices; using System.Runtime.InteropServices;
using System.Threading;
using Dalamud.Game.Text; using Dalamud.Game.Text;
using Dalamud.Game.Text.SeStringHandling; using Dalamud.Game.Text.SeStringHandling;
@ -28,7 +29,7 @@ namespace Dalamud.Game.Gui.ContextMenu;
[ServiceManager.EarlyLoadedService] [ServiceManager.EarlyLoadedService]
internal sealed unsafe class ContextMenu : IInternalDisposableService, IContextMenu 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<AtkModuleVf22OpenAddonByAgentDelegate> atkModuleVf22OpenAddonByAgentHook;
private readonly Hook<AddonContextMenu.Delegates.OnMenuSelected> addonContextMenuOnMenuSelectedHook; 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 Dictionary<ContextMenuType, List<IMenuItem>> MenuItems { get; } = [];
private object MenuItemsLock { get; } = new(); private Lock MenuItemsLock { get; } = new();
private AgentInterface* SelectedAgent { get; set; } private AgentInterface* SelectedAgent { get; set; }
@ -335,7 +336,7 @@ internal sealed unsafe class ContextMenu : IInternalDisposableService, IContextM
this.MenuCallbackIds.Clear(); this.MenuCallbackIds.Clear();
this.SelectedAgent = agent; this.SelectedAgent = agent;
var unitManager = RaptureAtkUnitManager.Instance(); var unitManager = RaptureAtkUnitManager.Instance();
this.SelectedParentAddon = unitManager->GetAddonById(unitManager->GetAddonByName(addonName)->ContextMenuParentId); this.SelectedParentAddon = unitManager->GetAddonById(unitManager->GetAddonByName(addonName)->BlockedParentId);
this.SelectedEventInterfaces.Clear(); this.SelectedEventInterfaces.Clear();
if (this.SelectedAgent == AgentInventoryContext.Instance()) if (this.SelectedAgent == AgentInventoryContext.Instance())
{ {

View file

@ -1,6 +1,5 @@
using System.Collections.Generic; using System.Collections.Generic;
using Dalamud.Memory;
using Dalamud.Plugin.Services; using Dalamud.Plugin.Services;
using FFXIVClientStructs.FFXIV.Client.UI.Agent; 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 const uint BaseNodeId = 1000;
private static readonly ModuleLog Log = new("DtrBar"); private static readonly ModuleLog Log = ModuleLog.Create<DtrBar>();
[ServiceManager.ServiceDependency] [ServiceManager.ServiceDependency]
private readonly Framework framework = Service<Framework>.Get(); 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 ReaderWriterLockSlim entriesLock = new();
private readonly List<DtrBarEntry> entries = []; private readonly List<DtrBarEntry> entries = [];
private readonly Dictionary<uint, List<IAddonEventHandle>> eventHandles = new(); private readonly Dictionary<uint, List<IAddonEventHandle>> eventHandles = [];
private ImmutableList<IReadOnlyDtrBarEntry>? entriesReadOnlyCopy; private ImmutableList<IReadOnlyDtrBarEntry>? entriesReadOnlyCopy;
@ -397,7 +397,15 @@ internal sealed unsafe class DtrBar : IInternalDisposableService, IDtrBar
ushort w = 0, h = 0; ushort w = 0, h = 0;
node->GetTextDrawSize(&w, &h, node->NodeText.StringPtr); 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; 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); 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.eventHandles[node->NodeId].AddRange(new List<IAddonEventHandle>
{ {
this.uiEventManager.AddEvent(AddonEventManager.DalamudInternalKey, (nint)dtr, (nint)node, AddonEventType.MouseOver, this.DtrEventHandler), 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.Configuration.Internal;
using Dalamud.Game.Addon.Events.EventDataTypes;
using Dalamud.Game.Text.SeStringHandling; using Dalamud.Game.Text.SeStringHandling;
using Dalamud.Plugin.Internal.Types; using Dalamud.Plugin.Internal.Types;
using Dalamud.Utility; using Dalamud.Utility;
@ -41,6 +40,11 @@ public interface IReadOnlyDtrBarEntry
/// </summary> /// </summary>
public bool Shown { get; } public bool Shown { get; }
/// <summary>
/// Gets a value indicating this entry's minimum width.
/// </summary>
public ushort MinimumWidth { get; }
/// <summary> /// <summary>
/// Gets a value indicating whether the user has hidden this entry from view through the Dalamud settings. /// Gets a value indicating whether the user has hidden this entry from view through the Dalamud settings.
/// </summary> /// </summary>
@ -77,6 +81,11 @@ public interface IDtrBarEntry : IReadOnlyDtrBarEntry
/// </summary> /// </summary>
public new bool Shown { get; set; } 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> /// <summary>
/// Gets or sets an action to be invoked when the user clicks on the dtr entry. /// Gets or sets an action to be invoked when the user clicks on the dtr entry.
/// </summary> /// </summary>
@ -129,6 +138,25 @@ internal sealed unsafe class DtrBarEntry : IDisposable, IDtrBarEntry
/// <inheritdoc cref="IDtrBarEntry.Tooltip" /> /// <inheritdoc cref="IDtrBarEntry.Tooltip" />
public SeString? Tooltip { get; set; } 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/> /// <inheritdoc/>
public Action<DtrInteractionEvent>? OnClick { get; set; } public Action<DtrInteractionEvent>? OnClick { get; set; }

View file

@ -32,7 +32,7 @@ namespace Dalamud.Game.Gui;
[ServiceManager.EarlyLoadedService] [ServiceManager.EarlyLoadedService]
internal sealed unsafe class GameGui : IInternalDisposableService, IGameGui 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; private readonly GameGuiAddressResolver address;

View file

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

View file

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

View file

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

View file

@ -22,7 +22,7 @@ namespace Dalamud.Game.Internal;
[ServiceManager.EarlyLoadedService] [ServiceManager.EarlyLoadedService]
internal sealed unsafe class DalamudAtkTweaks : IInternalDisposableService 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; private readonly Hook<AgentHUD.Delegates.OpenSystemMenu> hookAgentHudOpenSystemMenu;

View file

@ -18,15 +18,15 @@ namespace Dalamud.Game.Inventory;
[ServiceManager.EarlyLoadedService] [ServiceManager.EarlyLoadedService]
internal class GameInventory : IInternalDisposableService internal class GameInventory : IInternalDisposableService
{ {
private readonly List<GameInventoryPluginScoped> subscribersPendingChange = new(); private readonly List<GameInventoryPluginScoped> subscribersPendingChange = [];
private readonly List<GameInventoryPluginScoped> subscribers = new(); private readonly List<GameInventoryPluginScoped> subscribers = [];
private readonly List<InventoryItemAddedArgs> addedEvents = new(); private readonly List<InventoryItemAddedArgs> addedEvents = [];
private readonly List<InventoryItemRemovedArgs> removedEvents = new(); private readonly List<InventoryItemRemovedArgs> removedEvents = [];
private readonly List<InventoryItemChangedArgs> changedEvents = new(); private readonly List<InventoryItemChangedArgs> changedEvents = [];
private readonly List<InventoryItemMovedArgs> movedEvents = new(); private readonly List<InventoryItemMovedArgs> movedEvents = [];
private readonly List<InventoryItemSplitArgs> splitEvents = new(); private readonly List<InventoryItemSplitArgs> splitEvents = [];
private readonly List<InventoryItemMergedArgs> mergedEvents = new(); private readonly List<InventoryItemMergedArgs> mergedEvents = [];
[ServiceManager.ServiceDependency] [ServiceManager.ServiceDependency]
private readonly Framework framework = Service<Framework>.Get(); private readonly Framework framework = Service<Framework>.Get();
@ -151,7 +151,7 @@ internal class GameInventory : IInternalDisposableService
bool isNew; bool isNew;
lock (this.subscribersPendingChange) lock (this.subscribersPendingChange)
{ {
isNew = this.subscribersPendingChange.Any() && !this.subscribers.Any(); isNew = this.subscribersPendingChange.Count != 0 && this.subscribers.Count == 0;
this.subscribers.Clear(); this.subscribers.Clear();
this.subscribers.AddRange(this.subscribersPendingChange); this.subscribers.AddRange(this.subscribersPendingChange);
this.subscribersChanged = false; this.subscribersChanged = false;
@ -348,7 +348,7 @@ internal class GameInventory : IInternalDisposableService
#pragma warning restore SA1015 #pragma warning restore SA1015
internal class GameInventoryPluginScoped : IInternalDisposableService, IGameInventory internal class GameInventoryPluginScoped : IInternalDisposableService, IGameInventory
{ {
private static readonly ModuleLog Log = new(nameof(GameInventoryPluginScoped)); private static readonly ModuleLog Log = ModuleLog.Create<GameInventoryPluginScoped>();
[ServiceManager.ServiceDependency] [ServiceManager.ServiceDependency]
private readonly GameInventory gameInventoryService = Service<GameInventory>.Get(); 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.Internal;
using Dalamud.Game.Network.Structures; using Dalamud.Game.Network.Structures;
using Dalamud.IoC; using Dalamud.IoC;
@ -95,7 +93,7 @@ internal class MarketBoard : IInternalDisposableService, IMarketBoard
#pragma warning restore SA1015 #pragma warning restore SA1015
internal class MarketBoardPluginScoped : IInternalDisposableService, IMarketBoard internal class MarketBoardPluginScoped : IInternalDisposableService, IMarketBoard
{ {
private static readonly ModuleLog Log = new(nameof(MarketBoardPluginScoped)); private static readonly ModuleLog Log = ModuleLog.Create<MarketBoardPluginScoped>();
[ServiceManager.ServiceDependency] [ServiceManager.ServiceDependency]
private readonly MarketBoard marketBoardService = Service<MarketBoard>.Get(); 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. /// Focuses the AtkUnitBase.
/// </summary> /// </summary>
/// <returns> <c>true</c> when the addon was focused, <c>false</c> otherwise. </returns> /// <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> /// <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> /// <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> /// </returns>
public unsafe bool TryGet<T>([NotNullWhen(true)] out T? result) where T : struct 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) if (value is T typed)
{ {
result = 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.Net.Http;
using System.Text; using System.Text;
using System.Threading.Tasks; using System.Threading.Tasks;
@ -8,6 +7,7 @@ using Dalamud.Game.Network.Structures;
using Dalamud.Networking.Http; using Dalamud.Networking.Http;
using Newtonsoft.Json; using Newtonsoft.Json;
using Serilog; using Serilog;
namespace Dalamud.Game.Network.Internal.MarketBoardUploaders.Universalis; namespace Dalamud.Game.Network.Internal.MarketBoardUploaders.Universalis;
@ -64,7 +64,7 @@ internal class UniversalisMarketBoardUploader : IMarketBoardUploader
PricePerUnit = marketBoardItemListing.PricePerUnit, PricePerUnit = marketBoardItemListing.PricePerUnit,
Quantity = marketBoardItemListing.ItemQuantity, Quantity = marketBoardItemListing.ItemQuantity,
RetainerCity = marketBoardItemListing.RetainerCityId, RetainerCity = marketBoardItemListing.RetainerCityId,
Materia = new List<UniversalisItemMateria>(), Materia = [],
}; };
#pragma warning restore CS0618 // Type or member is obsolete #pragma warning restore CS0618 // Type or member is obsolete

View file

@ -33,7 +33,7 @@ namespace Dalamud.Game.Network.Internal;
[ServiceManager.EarlyLoadedService] [ServiceManager.EarlyLoadedService]
internal unsafe class NetworkHandlers : IInternalDisposableService internal unsafe class NetworkHandlers : IInternalDisposableService
{ {
private readonly IMarketBoardUploader uploader; private readonly UniversalisMarketBoardUploader uploader;
private readonly IDisposable handleMarketBoardItemRequest; private readonly IDisposable handleMarketBoardItemRequest;
private readonly IDisposable handleMarketTaxRates; private readonly IDisposable handleMarketTaxRates;
@ -55,10 +55,7 @@ internal unsafe class NetworkHandlers : IInternalDisposableService
private bool disposing; private bool disposing;
[ServiceManager.ServiceConstructor] [ServiceManager.ServiceConstructor]
private NetworkHandlers( private NetworkHandlers(TargetSigScanner sigScanner, HappyHttpClient happyHttpClient)
GameNetwork gameNetwork,
TargetSigScanner sigScanner,
HappyHttpClient happyHttpClient)
{ {
this.uploader = new UniversalisMarketBoardUploader(happyHttpClient); this.uploader = new UniversalisMarketBoardUploader(happyHttpClient);
@ -419,7 +416,7 @@ internal unsafe class NetworkHandlers : IInternalDisposableService
private IDisposable HandleMarketBoardItemRequest() 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); 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( private void UploadMarketBoardData(
MarketBoardItemRequest request, MarketBoardItemRequest request,
(uint CatalogId, ICollection<MarketBoardHistory.MarketBoardHistoryListing> Sales) sales, (uint CatalogId, ICollection<MarketBoardHistory.MarketBoardHistoryListing> Sales) sales,
ICollection<MarketBoardCurrentOfferings.MarketBoardItemListing> listings, List<MarketBoardCurrentOfferings.MarketBoardItemListing> listings,
ulong uploaderId, ulong uploaderId,
uint worldId) uint worldId)
{ {

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -7,7 +7,7 @@ namespace Dalamud.Game.Text.SeStringHandling.Payloads;
/// </summary> /// </summary>
public class NewLinePayload : Payload, ITextProvider 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> /// <summary>
/// Gets an instance of NewLinePayload. /// Gets an instance of NewLinePayload.

View file

@ -3,6 +3,7 @@ using System.Diagnostics.CodeAnalysis;
using System.IO; using System.IO;
using Lumina.Extensions; using Lumina.Extensions;
using Newtonsoft.Json; using Newtonsoft.Json;
namespace Dalamud.Game.Text.SeStringHandling.Payloads namespace Dalamud.Game.Text.SeStringHandling.Payloads
@ -97,7 +98,7 @@ namespace Dalamud.Game.Text.SeStringHandling.Payloads
reader.ReadByte(); reader.ReadByte();
// if the next byte is 0xF3 then this listing is limited to home world // if the next byte is 0xF3 then this listing is limited to home world
byte nextByte = reader.ReadByte(); var nextByte = reader.ReadByte();
switch (nextByte) switch (nextByte)
{ {
case (byte)PartyFinderLinkType.LimitedToHomeWorld: 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. // 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. // 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 // 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... // back to our regularly scheduled programming...
var listingIDBytes = MakeInteger(this.ListingId); var listingIDBytes = MakeInteger(this.ListingId);
bool isFlagSpecified = this.LinkType != PartyFinderLinkType.NotSpecified; var isFlagSpecified = this.LinkType != PartyFinderLinkType.NotSpecified;
var chunkLen = listingIDBytes.Length + 4; var chunkLen = listingIDBytes.Length + 4;
// 1 more byte for the type flag if it is specified // 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;
using Lumina.Excel.Sheets; using Lumina.Excel.Sheets;
using Newtonsoft.Json; using Newtonsoft.Json;
namespace Dalamud.Game.Text.SeStringHandling.Payloads; namespace Dalamud.Game.Text.SeStringHandling.Payloads;
@ -62,7 +63,7 @@ public class QuestPayload : Payload
}; };
bytes.AddRange(idBytes); bytes.AddRange(idBytes);
bytes.AddRange(new byte[] { 0x01, 0x01, END_BYTE }); bytes.AddRange([0x01, 0x01, END_BYTE]);
return bytes.ToArray(); return bytes.ToArray();
} }

View file

@ -45,7 +45,7 @@ public class RawPayload : Payload
/// <summary> /// <summary>
/// Gets a fixed Payload representing a common link-termination sequence, found in many payload chains. /// Gets a fixed Payload representing a common link-termination sequence, found in many payload chains.
/// </summary> /// </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/> /// <inheritdoc/>
public override PayloadType Type => PayloadType.Unknown; public override PayloadType Type => PayloadType.Unknown;

View file

@ -7,7 +7,7 @@ namespace Dalamud.Game.Text.SeStringHandling.Payloads;
/// </summary> /// </summary>
public class SeHyphenPayload : Payload, ITextProvider 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> /// <summary>
/// Gets an instance of SeHyphenPayload. /// Gets an instance of SeHyphenPayload.

View file

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

View file

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

View file

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

View file

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

View file

@ -3,6 +3,7 @@ using System.Collections.Generic;
using Dalamud.Data; using Dalamud.Data;
using Dalamud.Game.Gui; using Dalamud.Game.Gui;
using Dalamud.Hooking;
using Dalamud.IoC; using Dalamud.IoC;
using Dalamud.IoC.Internal; using Dalamud.IoC.Internal;
using Dalamud.Logging.Internal; using Dalamud.Logging.Internal;
@ -16,14 +17,14 @@ using FFXIVClientStructs.FFXIV.Component.Exd;
using Lumina.Excel; using Lumina.Excel;
using Lumina.Excel.Sheets; using Lumina.Excel.Sheets;
using AchievementSheet = Lumina.Excel.Sheets.Achievement;
using ActionSheet = Lumina.Excel.Sheets.Action; using ActionSheet = Lumina.Excel.Sheets.Action;
using CSAchievement = FFXIVClientStructs.FFXIV.Client.Game.UI.Achievement;
using InstanceContentSheet = Lumina.Excel.Sheets.InstanceContent; using InstanceContentSheet = Lumina.Excel.Sheets.InstanceContent;
using PublicContentSheet = Lumina.Excel.Sheets.PublicContent; using PublicContentSheet = Lumina.Excel.Sheets.PublicContent;
namespace Dalamud.Game.UnlockState; namespace Dalamud.Game.UnlockState;
#pragma warning disable Dalamud001
/// <summary> /// <summary>
/// This class provides unlock state of various content in the game. /// This class provides unlock state of various content in the game.
/// </summary> /// </summary>
@ -32,8 +33,6 @@ internal unsafe class UnlockState : IInternalDisposableService, IUnlockState
{ {
private static readonly ModuleLog Log = new(nameof(UnlockState)); private static readonly ModuleLog Log = new(nameof(UnlockState));
private readonly ConcurrentDictionary<Type, HashSet<uint>> cachedUnlockedRowIds = [];
[ServiceManager.ServiceDependency] [ServiceManager.ServiceDependency]
private readonly DataManager dataManager = Service<DataManager>.Get(); private readonly DataManager dataManager = Service<DataManager>.Get();
@ -46,17 +45,38 @@ internal unsafe class UnlockState : IInternalDisposableService, IUnlockState
[ServiceManager.ServiceDependency] [ServiceManager.ServiceDependency]
private readonly RecipeData recipeData = Service<RecipeData>.Get(); private readonly RecipeData recipeData = Service<RecipeData>.Get();
private readonly ConcurrentDictionary<Type, HashSet<uint>> cachedUnlockedRowIds = [];
private readonly Hook<CSAchievement.Delegates.SetAchievementCompleted> setAchievementCompletedHook;
private readonly Hook<TitleList.Delegates.SetTitleUnlocked> setTitleUnlockedHook;
[ServiceManager.ServiceConstructor] [ServiceManager.ServiceConstructor]
private UnlockState() private UnlockState()
{ {
this.clientState.Login += this.OnLogin; this.clientState.Login += this.OnLogin;
this.clientState.Logout += this.OnLogout; this.clientState.Logout += this.OnLogout;
this.gameGui.AgentUpdate += this.OnAgentUpdate; this.gameGui.AgentUpdate += this.OnAgentUpdate;
this.setAchievementCompletedHook = Hook<CSAchievement.Delegates.SetAchievementCompleted>.FromAddress(
(nint)CSAchievement.MemberFunctionPointers.SetAchievementCompleted,
this.SetAchievementCompletedDetour);
this.setTitleUnlockedHook = Hook<TitleList.Delegates.SetTitleUnlocked>.FromAddress(
(nint)TitleList.MemberFunctionPointers.SetTitleUnlocked,
this.SetTitleUnlockedDetour);
this.setAchievementCompletedHook.Enable();
this.setTitleUnlockedHook.Enable();
} }
/// <inheritdoc/> /// <inheritdoc/>
public event IUnlockState.UnlockDelegate Unlock; public event IUnlockState.UnlockDelegate Unlock;
/// <inheritdoc/>
public bool IsAchievementListLoaded => CSAchievement.Instance()->IsLoaded();
/// <inheritdoc/>
public bool IsTitleListLoaded => UIState.Instance()->TitleList.DataReceived;
private bool IsLoaded => PlayerState.Instance()->IsLoaded; private bool IsLoaded => PlayerState.Instance()->IsLoaded;
/// <inheritdoc/> /// <inheritdoc/>
@ -65,6 +85,21 @@ internal unsafe class UnlockState : IInternalDisposableService, IUnlockState
this.clientState.Login -= this.OnLogin; this.clientState.Login -= this.OnLogin;
this.clientState.Logout -= this.OnLogout; this.clientState.Logout -= this.OnLogout;
this.gameGui.AgentUpdate -= this.OnAgentUpdate; this.gameGui.AgentUpdate -= this.OnAgentUpdate;
this.setAchievementCompletedHook.Dispose();
}
/// <inheritdoc/>
public bool IsAchievementComplete(AchievementSheet row)
{
// Only check for login state here as individual Achievements
// may be flagged as complete when you unlock them, regardless
// of whether the full Achievements list was loaded or not.
if (!this.IsLoaded)
return false;
return CSAchievement.Instance()->IsComplete((int)row.RowId);
} }
/// <inheritdoc/> /// <inheritdoc/>
@ -73,6 +108,15 @@ internal unsafe class UnlockState : IInternalDisposableService, IUnlockState
return this.IsUnlockLinkUnlocked(row.UnlockLink.RowId); return this.IsUnlockLinkUnlocked(row.UnlockLink.RowId);
} }
/// <inheritdoc/>
public bool IsAdventureComplete(Adventure row)
{
if (!this.IsLoaded)
return false;
return PlayerState.Instance()->IsAdventureComplete(row.RowId - 0x210000);
}
/// <inheritdoc/> /// <inheritdoc/>
public bool IsAetherCurrentUnlocked(AetherCurrent row) public bool IsAetherCurrentUnlocked(AetherCurrent row)
{ {
@ -313,9 +357,12 @@ internal unsafe class UnlockState : IInternalDisposableService, IUnlockState
} }
/// <inheritdoc/> /// <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/> /// <inheritdoc/>
@ -330,6 +377,15 @@ internal unsafe class UnlockState : IInternalDisposableService, IUnlockState
return this.IsUnlockLinkUnlocked(row.UnlockLink); return this.IsUnlockLinkUnlocked(row.UnlockLink);
} }
/// <inheritdoc/>
public bool IsMcGuffinUnlocked(McGuffin row)
{
if (!this.IsLoaded)
return false;
return PlayerState.Instance()->IsMcGuffinUnlocked(row.RowId);
}
/// <inheritdoc/> /// <inheritdoc/>
public bool IsMountUnlocked(Mount row) public bool IsMountUnlocked(Mount row)
{ {
@ -378,9 +434,21 @@ internal unsafe class UnlockState : IInternalDisposableService, IUnlockState
return UIState.IsPublicContentUnlocked(row.RowId); return UIState.IsPublicContentUnlocked(row.RowId);
} }
/// <inheritdoc/>
public bool IsQuestCompleted(Quest row)
{
if (!this.IsLoaded)
return false;
return QuestManager.IsQuestComplete(row.RowId);
}
/// <inheritdoc/> /// <inheritdoc/>
public bool IsRecipeUnlocked(Recipe row) public bool IsRecipeUnlocked(Recipe row)
{ {
if (!this.IsLoaded)
return false;
return this.recipeData.IsRecipeUnlocked(row); return this.recipeData.IsRecipeUnlocked(row);
} }
@ -393,6 +461,19 @@ internal unsafe class UnlockState : IInternalDisposableService, IUnlockState
return PlayerState.Instance()->IsSecretRecipeBookUnlocked(row.RowId); return PlayerState.Instance()->IsSecretRecipeBookUnlocked(row.RowId);
} }
/// <inheritdoc/>
public bool IsTitleUnlocked(Title row)
{
// Only check for login state here as individual Titles
// may be flagged as complete when you unlock them, regardless
// of whether the full Titles list was loaded or not.
if (!this.IsLoaded)
return false;
return UIState.Instance()->TitleList.IsTitleUnlocked((ushort)row.RowId);
}
/// <inheritdoc/> /// <inheritdoc/>
public bool IsTraitUnlocked(Trait row) public bool IsTraitUnlocked(Trait row)
{ {
@ -442,9 +523,15 @@ internal unsafe class UnlockState : IInternalDisposableService, IUnlockState
if (!this.IsLoaded || rowRef.IsUntyped) if (!this.IsLoaded || rowRef.IsUntyped)
return false; return false;
if (rowRef.TryGetValue<AchievementSheet>(out var achievementRow))
return this.IsAchievementComplete(achievementRow);
if (rowRef.TryGetValue<ActionSheet>(out var actionRow)) if (rowRef.TryGetValue<ActionSheet>(out var actionRow))
return this.IsActionUnlocked(actionRow); return this.IsActionUnlocked(actionRow);
if (rowRef.TryGetValue<Adventure>(out var adventureRow))
return this.IsAdventureComplete(adventureRow);
if (rowRef.TryGetValue<AetherCurrent>(out var aetherCurrentRow)) if (rowRef.TryGetValue<AetherCurrent>(out var aetherCurrentRow))
return this.IsAetherCurrentUnlocked(aetherCurrentRow); return this.IsAetherCurrentUnlocked(aetherCurrentRow);
@ -511,6 +598,9 @@ internal unsafe class UnlockState : IInternalDisposableService, IUnlockState
if (rowRef.TryGetValue<Item>(out var itemRow)) if (rowRef.TryGetValue<Item>(out var itemRow))
return this.IsItemUnlocked(itemRow); return this.IsItemUnlocked(itemRow);
if (rowRef.TryGetValue<Leve>(out var leveRow))
return this.IsLeveCompleted(leveRow);
if (rowRef.TryGetValue<MJILandmark>(out var mjiLandmarkRow)) if (rowRef.TryGetValue<MJILandmark>(out var mjiLandmarkRow))
return this.IsMJILandmarkUnlocked(mjiLandmarkRow); return this.IsMJILandmarkUnlocked(mjiLandmarkRow);
@ -538,12 +628,18 @@ internal unsafe class UnlockState : IInternalDisposableService, IUnlockState
if (rowRef.TryGetValue<PublicContentSheet>(out var publicContentRow)) if (rowRef.TryGetValue<PublicContentSheet>(out var publicContentRow))
return this.IsPublicContentUnlocked(publicContentRow); return this.IsPublicContentUnlocked(publicContentRow);
if (rowRef.TryGetValue<Quest>(out var questRow))
return this.IsQuestCompleted(questRow);
if (rowRef.TryGetValue<Recipe>(out var recipeRow)) if (rowRef.TryGetValue<Recipe>(out var recipeRow))
return this.IsRecipeUnlocked(recipeRow); return this.IsRecipeUnlocked(recipeRow);
if (rowRef.TryGetValue<SecretRecipeBook>(out var secretRecipeBookRow)) if (rowRef.TryGetValue<SecretRecipeBook>(out var secretRecipeBookRow))
return this.IsSecretRecipeBookUnlocked(secretRecipeBookRow); return this.IsSecretRecipeBookUnlocked(secretRecipeBookRow);
if (rowRef.TryGetValue<Title>(out var titleRow))
return this.IsTitleUnlocked(titleRow);
if (rowRef.TryGetValue<Trait>(out var traitRow)) if (rowRef.TryGetValue<Trait>(out var traitRow))
return this.IsTraitUnlocked(traitRow); return this.IsTraitUnlocked(traitRow);
@ -593,12 +689,37 @@ internal unsafe class UnlockState : IInternalDisposableService, IUnlockState
this.Update(); this.Update();
} }
private void SetAchievementCompletedDetour(CSAchievement* thisPtr, uint id)
{
this.setAchievementCompletedHook.Original(thisPtr, id);
if (!this.IsLoaded)
return;
this.RaiseUnlockSafely((RowRef)LuminaUtils.CreateRef<AchievementSheet>(id));
}
private void SetTitleUnlockedDetour(TitleList* thisPtr, ushort id)
{
this.setTitleUnlockedHook.Original(thisPtr, id);
if (!this.IsLoaded)
return;
this.RaiseUnlockSafely((RowRef)LuminaUtils.CreateRef<Title>(id));
}
private void Update() private void Update()
{ {
if (!this.IsLoaded) if (!this.IsLoaded)
return; return;
Log.Verbose("Checking for new unlocks...");
// Do not check for Achievements or Titles here!
this.UpdateUnlocksForSheet<ActionSheet>(); this.UpdateUnlocksForSheet<ActionSheet>();
this.UpdateUnlocksForSheet<Adventure>();
this.UpdateUnlocksForSheet<AetherCurrent>(); this.UpdateUnlocksForSheet<AetherCurrent>();
this.UpdateUnlocksForSheet<AetherCurrentCompFlgSet>(); this.UpdateUnlocksForSheet<AetherCurrentCompFlgSet>();
this.UpdateUnlocksForSheet<AozAction>(); this.UpdateUnlocksForSheet<AozAction>();
@ -631,6 +752,7 @@ internal unsafe class UnlockState : IInternalDisposableService, IUnlockState
this.UpdateUnlocksForSheet<Ornament>(); this.UpdateUnlocksForSheet<Ornament>();
this.UpdateUnlocksForSheet<Perform>(); this.UpdateUnlocksForSheet<Perform>();
this.UpdateUnlocksForSheet<PublicContentSheet>(); this.UpdateUnlocksForSheet<PublicContentSheet>();
this.UpdateUnlocksForSheet<Quest>();
this.UpdateUnlocksForSheet<Recipe>(); this.UpdateUnlocksForSheet<Recipe>();
this.UpdateUnlocksForSheet<SecretRecipeBook>(); this.UpdateUnlocksForSheet<SecretRecipeBook>();
this.UpdateUnlocksForSheet<Trait>(); this.UpdateUnlocksForSheet<Trait>();
@ -639,11 +761,11 @@ internal unsafe class UnlockState : IInternalDisposableService, IUnlockState
// Not implemented: // Not implemented:
// - DescriptionPage: quite complex // - DescriptionPage: quite complex
// - QuestAcceptAdditionCondition: ignored // - QuestAcceptAdditionCondition: ignored
// - Leve: AgentUpdateFlag.UnlocksUpdate is not set and the completed status can be unset again!
// For some other day: // For some other day:
// - FishingSpot // - FishingSpot
// - Spearfishing // - Spearfishing
// - Adventure (Sightseeing)
// - MinerFolkloreTome // - MinerFolkloreTome
// - BotanistFolkloreTome // - BotanistFolkloreTome
// - FishingFolkloreTome // - FishingFolkloreTome
@ -656,8 +778,6 @@ internal unsafe class UnlockState : IInternalDisposableService, IUnlockState
// - EmjCostume // - EmjCostume
// Probably not happening, because it requires fetching data from server: // Probably not happening, because it requires fetching data from server:
// - Achievements
// - Titles
// - Bozjan Field Notes // - Bozjan Field Notes
// - Support/Phantom Jobs, which require to be in Occult Crescent, because it checks the jobs level for != 0 // - Support/Phantom Jobs, which require to be in Occult Crescent, because it checks the jobs level for != 0
} }
@ -678,18 +798,23 @@ internal unsafe class UnlockState : IInternalDisposableService, IUnlockState
unlockedRowIds.Add(row.RowId); 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)) this.RaiseUnlockSafely((RowRef)rowRef);
}
}
private void RaiseUnlockSafely(RowRef rowRef)
{
foreach (var action in Delegate.EnumerateInvocationList(this.Unlock))
{
try
{ {
try action(rowRef);
{ }
action((RowRef)rowRef); catch (Exception ex)
} {
catch (Exception ex) Log.Error(ex, "Exception during raise of {handler}", action.Method);
{
Log.Error(ex, "Exception during raise of {handler}", action.Method);
}
} }
} }
} }
@ -719,9 +844,21 @@ internal class UnlockStatePluginScoped : IInternalDisposableService, IUnlockStat
/// <inheritdoc/> /// <inheritdoc/>
public event IUnlockState.UnlockDelegate? Unlock; public event IUnlockState.UnlockDelegate? Unlock;
/// <inheritdoc/>
public bool IsAchievementListLoaded => this.unlockStateService.IsAchievementListLoaded;
/// <inheritdoc/>
public bool IsTitleListLoaded => this.unlockStateService.IsTitleListLoaded;
/// <inheritdoc/>
public bool IsAchievementComplete(AchievementSheet row) => this.unlockStateService.IsAchievementComplete(row);
/// <inheritdoc/> /// <inheritdoc/>
public bool IsActionUnlocked(ActionSheet row) => this.unlockStateService.IsActionUnlocked(row); public bool IsActionUnlocked(ActionSheet row) => this.unlockStateService.IsActionUnlocked(row);
/// <inheritdoc/>
public bool IsAdventureComplete(Adventure row) => this.unlockStateService.IsAdventureComplete(row);
/// <inheritdoc/> /// <inheritdoc/>
public bool IsAetherCurrentCompFlgSetUnlocked(AetherCurrentCompFlgSet row) => this.unlockStateService.IsAetherCurrentCompFlgSetUnlocked(row); public bool IsAetherCurrentCompFlgSetUnlocked(AetherCurrentCompFlgSet row) => this.unlockStateService.IsAetherCurrentCompFlgSetUnlocked(row);
@ -798,7 +935,7 @@ internal class UnlockStatePluginScoped : IInternalDisposableService, IUnlockStat
public bool IsItemUnlocked(Item row) => this.unlockStateService.IsItemUnlocked(row); public bool IsItemUnlocked(Item row) => this.unlockStateService.IsItemUnlocked(row);
/// <inheritdoc/> /// <inheritdoc/>
public bool IsMcGuffinUnlocked(McGuffin row) => this.unlockStateService.IsMcGuffinUnlocked(row); public bool IsLeveCompleted(Leve row) => this.unlockStateService.IsLeveCompleted(row);
/// <inheritdoc/> /// <inheritdoc/>
public bool IsMJILandmarkUnlocked(MJILandmark row) => this.unlockStateService.IsMJILandmarkUnlocked(row); public bool IsMJILandmarkUnlocked(MJILandmark row) => this.unlockStateService.IsMJILandmarkUnlocked(row);
@ -806,6 +943,9 @@ internal class UnlockStatePluginScoped : IInternalDisposableService, IUnlockStat
/// <inheritdoc/> /// <inheritdoc/>
public bool IsMKDLoreUnlocked(MKDLore row) => this.unlockStateService.IsMKDLoreUnlocked(row); public bool IsMKDLoreUnlocked(MKDLore row) => this.unlockStateService.IsMKDLoreUnlocked(row);
/// <inheritdoc/>
public bool IsMcGuffinUnlocked(McGuffin row) => this.unlockStateService.IsMcGuffinUnlocked(row);
/// <inheritdoc/> /// <inheritdoc/>
public bool IsMountUnlocked(Mount row) => this.unlockStateService.IsMountUnlocked(row); public bool IsMountUnlocked(Mount row) => this.unlockStateService.IsMountUnlocked(row);
@ -824,6 +964,9 @@ internal class UnlockStatePluginScoped : IInternalDisposableService, IUnlockStat
/// <inheritdoc/> /// <inheritdoc/>
public bool IsPublicContentUnlocked(PublicContentSheet row) => this.unlockStateService.IsPublicContentUnlocked(row); public bool IsPublicContentUnlocked(PublicContentSheet row) => this.unlockStateService.IsPublicContentUnlocked(row);
/// <inheritdoc/>
public bool IsQuestCompleted(Quest row) => this.unlockStateService.IsQuestCompleted(row);
/// <inheritdoc/> /// <inheritdoc/>
public bool IsRecipeUnlocked(Recipe row) => this.unlockStateService.IsRecipeUnlocked(row); public bool IsRecipeUnlocked(Recipe row) => this.unlockStateService.IsRecipeUnlocked(row);
@ -836,6 +979,9 @@ internal class UnlockStatePluginScoped : IInternalDisposableService, IUnlockStat
/// <inheritdoc/> /// <inheritdoc/>
public bool IsSecretRecipeBookUnlocked(SecretRecipeBook row) => this.unlockStateService.IsSecretRecipeBookUnlocked(row); public bool IsSecretRecipeBookUnlocked(SecretRecipeBook row) => this.unlockStateService.IsSecretRecipeBookUnlocked(row);
/// <inheritdoc/>
public bool IsTitleUnlocked(Title row) => this.unlockStateService.IsTitleUnlocked(row);
/// <inheritdoc/> /// <inheritdoc/>
public bool IsTraitUnlocked(Trait row) => this.unlockStateService.IsTraitUnlocked(row); public bool IsTraitUnlocked(Trait row) => this.unlockStateService.IsTraitUnlocked(row);

View file

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

View file

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

View file

@ -1,8 +1,8 @@
using System.Collections.Generic;
using System.Reflection; using System.Reflection;
using System.Runtime.InteropServices; using System.Runtime.InteropServices;
using JetBrains.Annotations; using JetBrains.Annotations;
using Windows.Win32.System.Memory; using Windows.Win32.System.Memory;
using Win32Exception = System.ComponentModel.Win32Exception; 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)) 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; this.detourDelegate = detour;

View file

@ -1,4 +1,4 @@
using System.Diagnostics; using System.Diagnostics;
using System.Linq; using System.Linq;
using Dalamud.Game; using Dalamud.Game;
@ -8,6 +8,7 @@ using Dalamud.Plugin.Internal.Types;
using Dalamud.Plugin.Services; using Dalamud.Plugin.Services;
using Dalamud.Utility; using Dalamud.Utility;
using Dalamud.Utility.Signatures; using Dalamud.Utility.Signatures;
using Serilog; using Serilog;
namespace Dalamud.Hooking.Internal; namespace Dalamud.Hooking.Internal;
@ -25,7 +26,7 @@ internal class GameInteropProviderPluginScoped : IGameInteropProvider, IInternal
private readonly LocalPlugin plugin; private readonly LocalPlugin plugin;
private readonly SigScanner scanner; private readonly SigScanner scanner;
private readonly WeakConcurrentCollection<IDalamudHook> trackedHooks = new(); private readonly WeakConcurrentCollection<IDalamudHook> trackedHooks = [];
/// <summary> /// <summary>
/// Initializes a new instance of the <see cref="GameInteropProviderPluginScoped"/> class. /// 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.Collections.Generic;
using System.Linq; using System.Linq;
using System.Runtime.InteropServices; using System.Runtime.InteropServices;
using System.Threading;
using Dalamud.Logging.Internal; using Dalamud.Logging.Internal;
using Dalamud.Memory; using Dalamud.Memory;
@ -20,7 +21,7 @@ internal class HookManager : IInternalDisposableService
/// <summary> /// <summary>
/// Logger shared with <see cref="Unhooker"/>. /// Logger shared with <see cref="Unhooker"/>.
/// </summary> /// </summary>
internal static readonly ModuleLog Log = new("HM"); internal static readonly ModuleLog Log = ModuleLog.Create<HookManager>();
[ServiceManager.ServiceConstructor] [ServiceManager.ServiceConstructor]
private HookManager() private HookManager()
@ -30,7 +31,7 @@ internal class HookManager : IInternalDisposableService
/// <summary> /// <summary>
/// Gets sync root object for hook enabling/disabling. /// Gets sync root object for hook enabling/disabling.
/// </summary> /// </summary>
internal static object HookEnableSyncRoot { get; } = new(); internal static Lock HookEnableSyncRoot { get; } = new();
/// <summary> /// <summary>
/// Gets a static list of tracked and registered hooks. /// 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); var unhooker = HookManager.RegisterUnhooker(this.Address);
if (!HookManager.MultiHookTracker.TryGetValue(this.Address, out var indexList)) 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; 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.Linq;
using System.Reflection;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using Dalamud.Game; using Dalamud.Game;
using Dalamud.Logging.Internal; using Dalamud.Logging.Internal;
using InteropGenerator.Runtime;
namespace Dalamud.Hooking.Internal.Verification; namespace Dalamud.Hooking.Internal.Verification;
/// <summary> /// <summary>
@ -19,11 +24,13 @@ internal static class HookVerifier
new( new(
"ActorControlSelf", "ActorControlSelf",
"E8 ?? ?? ?? ?? 0F B7 0B 83 E9 64", "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) "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> /// <summary>
/// Initializes a new instance of the <see cref="HookVerifier"/> class. /// Initializes a new instance of the <see cref="HookVerifier"/> class.
@ -71,7 +78,7 @@ internal static class HookVerifier
var enforcedInvoke = entry.TargetDelegateType.GetMethod("Invoke")!; var enforcedInvoke = entry.TargetDelegateType.GetMethod("Invoke")!;
// Compare Return Type // Compare Return Type
var mismatch = passedInvoke.ReturnType != enforcedInvoke.ReturnType; var mismatch = !CheckParam(passedInvoke.ReturnType, enforcedInvoke.ReturnType);
// Compare Parameter Count // Compare Parameter Count
var passedParams = passedInvoke.GetParameters(); var passedParams = passedInvoke.GetParameters();
@ -86,7 +93,7 @@ internal static class HookVerifier
// Compare Parameter Types // Compare Parameter Types
for (var i = 0; i < passedParams.Length; i++) for (var i = 0; i < passedParams.Length; i++)
{ {
if (passedParams[i].ParameterType != enforcedParams[i].ParameterType) if (!CheckParam(passedParams[i].ParameterType, enforcedParams[i].ParameterType))
{ {
mismatch = true; mismatch = true;
break; 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) private record VerificationEntry(string Name, string Signature, Type TargetDelegateType, string Message)
{ {
public nint Address { get; set; } public nint Address { get; set; }

View file

@ -17,10 +17,10 @@ namespace Dalamud.Hooking.WndProcHook;
[ServiceManager.EarlyLoadedService] [ServiceManager.EarlyLoadedService]
internal sealed class WndProcHookManager : IInternalDisposableService 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 Hook<DispatchMessageWDelegate> dispatchMessageWHook;
private readonly Dictionary<HWND, WndProcEventArgs> wndProcOverrides = new(); private readonly Dictionary<HWND, WndProcEventArgs> wndProcOverrides = [];
private HWND mainWindowHwnd; private HWND mainWindowHwnd;

View file

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

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