Compare commits

...

641 commits

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

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

I'm hoping it doesn't make the window too tall, so if there's a better way to list only the display adapters that are unique, I'm all for it.
2026-02-07 20:58:55 -08:00
Haselnussbomber
0490a71990
Update Network Monitor Widget
- Separate checkboxes for up and down tracking
- Clarify tracking is for ZoneUp/ZoneDown
- Rephrase filter checkbox tooltip
2026-02-07 20:36:20 +01:00
goat
2b347eaff9
Merge pull request #2604 from pohky/patch-1
Some checks are pending
Build Dalamud / Build on Windows (push) Waiting to run
Build Dalamud / Check API Compatibility (push) Blocked by required conditions
Build Dalamud / Deploy dalamud-distrib staging (push) Blocked by required conditions
Tag Build / Tag Build (push) Successful in 4s
Fix null characters in BitmapCodecInfo strings
2026-02-07 13:41:13 +01:00
goat
f4defb735b
Merge pull request #2610 from Infiziert90/character-api-extended
Extend the Character class with CustomizeData
2026-02-07 13:40:56 +01:00
goat
4a75fe73df
Merge pull request #2612 from Soreepeong/fix/tex-from-file
Fix loading `.tex` file from filesystem
2026-02-07 13:39:47 +01:00
Soreepeong
b30a93816b Directly work with TexHeader and TextureBuffer 2026-02-06 18:59:02 +09:00
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
Critical Impact
9e18b843db Change column name 2026-02-02 22:32:33 +10:00
Critical Impact
dc783e0c2b Use plugin internal name for datashare tracking 2026-02-02 16:56:31 +10:00
Haselnussbomber
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
goat
e2a18dee5e build: 14.0.0.2
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-21 15:32:24 +01:00
goat
f803dfbd44
Merge pull request #2546 from Infiziert90/todo-contentid
Mark PartyMember.ContentId type for API15
2025-12-21 14:46:53 +01:00
Infi
4dcfa9da98 - Add ToDo for ulong change 2025-12-21 14:37:03 +01:00
goat
867ae80b22
Merge pull request #2543 from goatcorp/csupdate-master
[master] Update ClientStructs
2025-12-21 13:09:10 +01:00
goat
1b39040072
Merge pull request #2535 from Haselnussbomber/rssb
Slight SeString code updates
2025-12-21 13:04:24 +01:00
goat
ea1bb92d17
Merge pull request #2536 from CMDRNuffin/imgui-textboxes-fix-unnecessary-cloning-on-unchanged-text-2-electric-boogaloo
Prevent unnecessary string creation in ImGui TextInput methods
2025-12-21 13:02:07 +01:00
github-actions[bot]
5513bd1633 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-21 11:59:55 +00:00
KazWolfe
f307aded73
Lumina revert (#2544)
Some checks are pending
Build Dalamud / Build on Windows (push) Waiting to run
Build Dalamud / Check API Compatibility (push) Blocked by required conditions
Build Dalamud / Deploy dalamud-distrib staging (push) Blocked by required conditions
Tag Build / Tag Build (push) Successful in 3s
2025-12-21 02:31:17 +00:00
goat
a59efbd84c Fixes for excel renamings 2025-12-21 02:41:22 +01:00
goat
c3952bbf53
Merge pull request #2542 from goatcorp/csupdate-master
[master] Update ClientStructs
2025-12-21 02:32:41 +01:00
goat
5a8bb73b39
Merge pull request #2541 from goatcorp/schemaupdate-master
[master] Update Excel Schema
2025-12-21 02:31:20 +01:00
github-actions[bot]
96b5ad1b65 Update ClientStructs 2025-12-21 01:23:19 +00:00
github-actions[bot]
69c24fdbb9 Update Excel Schema 2025-12-21 01:23:12 +00:00
goat
3abddbae2c build: 14.0.0.1 2025-12-21 00:51:50 +01:00
goat
b24cadf2d8 Merge branch 'master' of github.com:goatcorp/Dalamud 2025-12-21 00:51:14 +01:00
goat
9205529820
Merge pull request #2539 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 5s
[master] Update ClientStructs
2025-12-20 21:54:48 +01:00
goat
da2b80156a Fix some wording on badge tab 2025-12-20 21:45:47 +01:00
github-actions[bot]
1f5f6f8914 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-20 20:45:31 +00:00
goat
08c4f7dbdd
Merge pull request #2538 from Haselnussbomber/zoneinit-fix
Update ZoneInitEventArgs
2025-12-20 21:36:55 +01:00
goat
451db117a6
Merge pull request #2537 from goatcorp/schemaupdate-master
[master] Update Excel Schema
2025-12-20 21:31:41 +01:00
goat
73168b0e53
Merge pull request #2534 from goatcorp/csupdate-master
[master] Update ClientStructs
2025-12-20 21:30:46 +01:00
Haselnussbomber
3ef6135f15
Fix reading ActiveFestivals in ZoneInitEventArgs 2025-12-20 21:26:10 +01:00
Haselnussbomber
6374c0d6ae
Use UIModuleHandlePacketDetour for ZoneInit 2025-12-20 21:25:49 +01:00
github-actions[bot]
e603af5acc Update ClientStructs 2025-12-20 18:33:10 +00:00
github-actions[bot]
8b0bb343f9 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-20 18:33:04 +00:00
KazWolfe
bc2eac6006
fix: Remove RPC (#2526)
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 5s
2025-12-20 16:18:03 +10:00
CMDRNuffin
66fde2d458 Prevent unnecessary string creation in ImGui TextInput methods
We now only create a new string if we either know the buffer changed or
the EnterReturnsTrue flag was set (because that one does a LOT while
still updating the buffer on every actual input), so I had to choose
between replicating all that behavior in each of the various InputText
methods (hell no, lol), scanning the buffer for actual changes (which
would require making another copy) or accepting that in that case we
would create a new string every frame.

This still makes the GC happy in the majority of cases, while giving
callers the option to take a slight performance hit for the convenience
EnterReturnsTrue provides.
2025-12-20 03:19:20 +01:00
goaaats
c7dd694a53 Revert "Prevent ImGui text box methods from cloning unchanged input every frame"
This reverts commit db5f27518f.
Causes issues with certain flags.
2025-12-20 02:02:57 +01:00
goaaats
efed9ca20b Add badges
Some checks are pending
Build Dalamud / Build on Windows (push) Waiting to run
Build Dalamud / Check API Compatibility (push) Blocked by required conditions
Build Dalamud / Deploy dalamud-distrib staging (push) Blocked by required conditions
Tag Build / Tag Build (push) Successful in 3s
2025-12-19 21:20:06 +01:00
Haselnussbomber
8a49a5ee48
Use spread element for TextArrowPayloads
Also changes CreatePartyFinderLink to use client-language-based text.
2025-12-19 17:39:31 +01:00
Haselnussbomber
a3d930b8e2
Use RentedSeStringBuilder more 2025-12-19 17:34:42 +01:00
goat
5e4ad4a694
Merge pull request #2533 from wolfcomp/patch-7
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 5s
Add new themes and update themed path logic
2025-12-19 12:09:41 +01:00
wolfcomp
c71d8889d7
Access const as non instance 2025-12-19 11:51:09 +01:00
wolfcomp
4ddaaf3809
Add new themes and update themed path logic 2025-12-19 11:41:33 +01:00
goat
3b8917bcc8
Merge pull request #2528 from CMDRNuffin/imgui-textboxes-fix-unnecessary-cloning-on-unchanged-text
Prevent ImGui text box methods from cloning unchanged input every frame
2025-12-19 11:36:19 +01:00
goat
517d5d017b
Merge pull request #2529 from Haselnussbomber/update-uicolorwidget
Update UIColor widget
2025-12-19 11:35:56 +01:00
goat
8fb2c39d80
Merge pull request #2532 from goatcorp/csupdate-master
[master] Update ClientStructs
2025-12-19 11:35:34 +01:00
github-actions[bot]
89c46944b6 Update ClientStructs 2025-12-19 10:20:39 +00:00
goat
cb3881f07d
Merge pull request #2531 from goatcorp/schemaupdate-master
[master] Update Excel Schema
2025-12-19 08:44:59 +01:00
goat
6ead1c8895
Merge pull request #2530 from goatcorp/csupdate-master
[master] Update ClientStructs
2025-12-19 08:44:10 +01:00
github-actions[bot]
f3f4ced049 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-19 06:39:07 +00:00
github-actions[bot]
7af0523e88 Update ClientStructs 2025-12-19 06:38:57 +00:00
bleatbot
7eea7d6182
Update ClientStructs (#2525)
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
Co-authored-by: github-actions[bot] <noreply@github.com>
2025-12-19 02:35:52 +00:00
Haselnussbomber
86e12f411d
Update UIColor widget 2025-12-19 03:26:53 +01:00
CMDRNuffin
db5f27518f Prevent ImGui text box methods from cloning unchanged input every frame
The overloads taking a string by ref for the input text of the various
ways to display a text box would all take the input string, copy it into
a buffer for imgui and then unconditionally produce a new string once
the imgui call returned. Now we only create a new string when the return
value of the native function actually indicates that the text changed.

This makes the GC happy, and also users like me who like to make the GC
happy.

Other side effects: The assumption that the reference doesn't change if
the method returns false, which is very reasonable IMO, is now correct.
2025-12-19 01:24:43 +01:00
goaaats
c005bae265 Revert obsolete as error again, fix warnings, Api14ToDo => Api15ToDo
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
2025-12-18 21:00:07 +01:00
goaaats
19fca721e9 Make obsoletions for ClientState error 2025-12-18 20:55:04 +01:00
goaaats
a56d2cf40b Add verifier for hook signatures
This one is real bad, so we should make sure everyone using a canonical signature
2025-12-18 20:28:03 +01:00
bleatbot
3eb65c85c0
Update ClientStructs (#2524)
Co-authored-by: github-actions[bot] <noreply@github.com>
2025-12-18 18:08:27 +00:00
goat
1b76aec89f
Merge pull request #2521 from goatcorp/csupdate-master
[master] Update ClientStructs
2025-12-18 16:21:45 +01:00
goat
b52024d927
Merge branch 'master' into csupdate-master 2025-12-18 16:12:06 +01:00
goat
56b0ae80b6
Merge pull request #2523 from Infiziert90/fix-cs-build
Fix CS build errors from unknown private change
2025-12-18 16:11:51 +01:00
Infi
0b1a697d4d - Comment out erroring unknown prints 2025-12-18 15:38:57 +01:00
goat
05beea003c
Merge pull request #2522 from Loskh/fix_item_name
fix: EventItem name for Japanese client.
2025-12-18 15:11:13 +01:00
Loskh
3c8cef06dd fix: EventItem name for Japanese client. 2025-12-18 21:54:28 +08:00
github-actions[bot]
0d533c18f8 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-18 13:21:49 +00:00
goat
52166e4b9e
Merge pull request #2520 from Critical-Impact/docfx-dlls
Some checks are pending
Build Dalamud / Build on Windows (push) Waiting to run
Build Dalamud / Check API Compatibility (push) Blocked by required conditions
Build Dalamud / Deploy dalamud-distrib staging (push) Blocked by required conditions
Tag Build / Tag Build (push) Successful in 4s
Use built dlls instead of csproj for docfx
2025-12-18 13:18:49 +01:00
goat
ea756250a8
Merge pull request #2519 from Critical-Impact/versioning-public
Add git hash/scm version properties to DalamudVersionInfo
2025-12-18 13:14:29 +01:00
Critical Impact
984bdbcf0e Use built dlls instead of csproj for docfx 2025-12-18 22:07:58 +10:00
Critical Impact
bd87a3d251 Add git hash/scm version properties to DalamudVersionInfo 2025-12-18 22:01:57 +10:00
goat
77b6d71855
Merge pull request #2514 from MidoriKami/ResolveOriginalAddress
Add AddonLifecycle VirtualAddress helper
2025-12-18 12:08:34 +01:00
goat
6550e71092
Merge pull request #2517 from goatcorp/schemaupdate-master
[master] Update Excel Schema
2025-12-18 12:07:53 +01:00
goat
7ac12627ec
Merge pull request #2515 from goatcorp/csupdate-master
[master] Update ClientStructs
2025-12-18 12:07:12 +01:00
github-actions[bot]
17c0527f2d Update ClientStructs 2025-12-18 06:40:20 +00:00
github-actions[bot]
3a1e1e6425 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-18 06:40:14 +00:00
MidoriKami
37fa40ab58 Make stylecop happy 2025-12-17 16:44:04 -08:00
goat
fb7cd452f6
Merge pull request #2513 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
2025-12-18 01:41:11 +01:00
MidoriKami
7f4352dc43 Add address resolver 2025-12-17 16:38:34 -08:00
goat
707369bfad
Merge pull request #2457 from goatcorp/schemaupdate-master
[master] Update Excel Schema
2025-12-18 01:30:26 +01:00
goat
25dba5e23b
ci: revert global concurrency change again because it breaks PR workflows
Need to figure out something better for this soon, but it's better not to have this at all right now
2025-12-18 01:29:48 +01:00
github-actions[bot]
05037dccc7 Update ClientStructs 2025-12-18 00:24:13 +00:00
github-actions[bot]
574e0d4582 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-18 00:24:11 +00:00
goat
02d4081f2f
ci: disable rollup for now 2025-12-18 01:24:09 +01:00
goat
355ad64eb9
Merge pull request #2508 from Haselnussbomber/excel-bump
Update Lumina and Lumina.Excel
2025-12-18 01:23:18 +01:00
goat
f1f95eda09
Merge pull request #2509 from goatcorp/csupdate-master
[master] Update ClientStructs
2025-12-18 01:21:18 +01:00
github-actions[bot]
fc804ba0d0 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-17 18:40:02 +00:00
goat
108a7a2c2d
Merge pull request #2510 from Haselnussbomber/conditions
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
Rollup changes to next version / check (api14) (push) Failing after 7s
Tag Build / Tag Build (push) Successful in 4s
Update Condition/ConditionFlag
2025-12-17 18:48:25 +01:00
goat
c5d90aef64
Merge pull request #2511 from Haselnussbomber/context-menu-fix
Fix crashing Context Menu
2025-12-17 18:42:32 +01:00
goat
92d6c70358
Merge pull request #2512 from Haselnussbomber/hover-action-kind
Update HoverActionKind
2025-12-17 18:41:56 +01:00
Haselnussbomber
2fc9884aad
Update HoverActionKind 2025-12-17 18:17:44 +01:00
Haselnussbomber
b3c4363e0f
Fix crashing Context Menu 2025-12-17 17:09:18 +01:00
Haselnussbomber
841cdf52bd
Update Lumina and Lumina.Excel 2025-12-17 16:15:33 +01:00
Haselnussbomber
19660a20d9
Update Condition/ConditionFlag 2025-12-17 16:12:33 +01:00
goaaats
f142fb1058 Set language version to preview for now
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
Rollup changes to next version / check (api14) (push) Failing after 7s
Tag Build / Tag Build (push) Successful in 3s
Fixes a docfx error, since they haven't upgraded to a Roslyn version that knows C# 14
2025-12-17 00:50:14 +01:00
goaaats
46954e6add Remove plugin targets from SLN
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
Rollup changes to next version / check (api14) (push) Failing after 6s
Tag Build / Tag Build (push) Successful in 3s
2025-12-16 21:01:58 +01:00
goaaats
01901c237a Downgrade Iced to resolve version conflict between Dalamud and Injector 2025-12-16 21:01:50 +01:00
goaaats
cdf4e27355 Bump version to 14.0.0.0 2025-12-16 19:28:09 +01:00
goat
a843079a6b
Merge pull request #2506 from goatcorp/api14-rollup
[api14] Rollup changes from master
2025-12-16 18:54:13 +01:00
goat
ddd85513ba
Merge pull request #2507 from Haselnussbomber/enumerator-fixes
[API14] Better enumerator code
2025-12-16 18:53:50 +01:00
Haselnussbomber
89fbe6c8b0
Update UiConfigOption 2025-12-16 17:21:19 +01:00
Haselnussbomber
1c1b60efee
Better enumerator code 2025-12-16 09:48:59 +01:00
goat
2e7c48316f
Merge pull request #2481 from MidoriKami/AddonLifecycleRefactor
Some checks failed
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
[API14] AddonLifecycle Refactor
2025-12-16 00:32:09 +01:00
github-actions[bot]
b0a0fafb53 Merge remote-tracking branch 'origin/master' into api14-rollup
Some checks failed
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-15 23:31:02 +00:00
goat
8334836b6a
Merge pull request #2386 from KazWolfe/ipc-context
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
Rollup changes to next version / check (api14) (push) Failing after 8s
Tag Build / Tag Build (push) Successful in 3s
feat: Add IPC Context
2025-12-16 00:30:06 +01:00
goat
8a742e7e59
Merge pull request #2505 from Aireil/patch-25
Remove obsolete enum values from FlyTextKind
2025-12-15 23:25:46 +01:00
Aireil
56325afa7f
Remove obsolete enum values from FlyTextKind
They have been obsolete for nearly 7 months (before 7.3).
2025-12-15 23:14:46 +01:00
MidoriKami
1bff6abae9 Fix oopsie 2025-12-15 13:22:39 -08:00
MidoriKami
d7935d6dd4 Merge remote-tracking branch 'origin/AddonLifecycleRefactor' into AddonLifecycleRefactor 2025-12-15 13:13:19 -08:00
MidoriKami
a715725a9d Add enumerable AtkValue helper 2025-12-15 13:13:08 -08:00
MidoriKami
bc8e986c11
Merge branch 'api14' into AddonLifecycleRefactor 2025-12-15 12:55:35 -08:00
goaaats
ffd99d5791 Add interface to obtain versioning info 2025-12-15 21:43:52 +01:00
goaaats
20af5b40c7 Make all versioning functions internal, move to separate class 2025-12-15 21:31:25 +01:00
goaaats
a1409096fd Redo SeStringRenderer deprecations 2025-12-15 21:20:24 +01:00
goat
fecba89710
Merge pull request #2498 from goatcorp/api14-rollup
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
[api14] Rollup changes from master
2025-12-15 21:12:03 +01:00
goat
b57b96b9a0
Merge pull request #2493 from Haselnussbomber/update-packages
Some checks failed
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
[API14] Update packages
2025-12-13 23:47:14 +01:00
github-actions[bot]
180676fe47 Merge remote-tracking branch 'origin/master' into api14-rollup
Some checks failed
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-13 04:05:56 +00:00
Haselnussbomber
2d096d9b33
Properly initialize GameInventoryItems (#2504)
Some checks failed
Rollup changes to next version / check (api14) (push) Failing after 20s
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-13 14:05:03 +10:00
goaaats
e100ec2abd build: 13.0.0.16
Some checks failed
Rollup changes to next version / check (api14) (push) Failing after 3s
Tag Build / Tag Build (push) Failing after 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
2025-12-12 00:57:04 +01:00
goat
71b0a757e9
Merge pull request #2501 from Haselnussbomber/clear-ImDrawListSplitter
Clear ImDrawListSplitter when disposing SeStringDrawState
2025-12-11 23:15:21 +01:00
Haselnussbomber
0b55dc3e10
Clear ImDrawListSplitter when disposing SeStringDrawState 2025-12-11 22:59:50 +01:00
MidoriKami
4d9751ea5f
Merge branch 'api14' into AddonLifecycleRefactor 2025-12-10 14:16:56 -08:00
goaaats
a39763f161 Mark preset dirty when disabling clickthrough for a window
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
Rollup changes to next version / check (api14) (push) Failing after 3s
Tag Build / Tag Build (push) Successful in 2s
2025-12-10 18:33:37 +01:00
goaaats
201c9cfcf2 Use game window to calculate offsets in fallback mouse position code 2025-12-10 18:13:52 +01:00
goat
e07bda7e58
Merge pull request #2500 from nebel/window-error-pop-dalamud-style
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
Rollup changes to next version / check (api14) (push) Failing after 4s
Tag Build / Tag Build (push) Successful in 2s
Always pop DalamudStandard style if pushed earlier in Draw
2025-12-10 15:26:12 +01:00
nebel
b88a6bb616
Always pop DalamudStandard style if pushed earlier in Draw 2025-12-10 23:12:44 +09:00
goaaats
e53ccdbcc0 build: 13.0.0.15
Some checks failed
Rollup changes to next version / check (api14) (push) Failing after 3s
Tag Build / Tag Build (push) Failing after 3s
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-09 00:18:28 +01:00
goaaats
97df73acea Ensure that we don't catch mouse up events without corresponding mouse down events
Fixes an issue wherein the cursor could get locked by the game if WantCaptureMouse becomes true in between down and up events
2025-12-08 21:00:08 +01:00
goaaats
2806e59dba Also remove borders for dev bar, to prevent themes from causing weirdness 2025-12-08 20:09:31 +01:00
goaaats
24caa1cb18 PresetWindow.IsDefault can be JsonIgnore 2025-12-08 20:05:14 +01:00
goaaats
5d08170333 Keep rendering title bar buttons if one is not available clickthrough 2025-12-08 20:03:43 +01:00
goaaats
d0110f7251 Hardcode HasModifiedGameDataFiles to false for now until XL is fixed 2025-12-08 20:03:22 +01:00
MidoriKami
2dbae05522 Add very thurough exception handling 2025-12-07 16:45:59 -08:00
goaaats
8ed1af30df build: 13.0.0.14
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
Rollup changes to next version / check (api14) (push) Failing after 2s
Tag Build / Tag Build (push) Failing after 2s
2025-12-07 22:55:16 +01:00
goat
e8485dee25
Merge pull request #2492 from goatcorp/api14-rollup
Some checks failed
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
[api14] Rollup changes from master
2025-12-07 22:22:08 +01:00
github-actions[bot]
0072f49fe8 Merge remote-tracking branch 'origin/master' into api14-rollup 2025-12-07 21:13:12 +00:00
goaaats
c45c6aafe1 Don't consider failed index integrity checks as having "modified game data files" 2025-12-07 21:57:54 +01:00
goaaats
2029a0f8a6 Also add fallback for SeStringDrawState.ScreenOffset for now, make sure that it is populated 2025-12-07 21:31:25 +01:00
goat
bcb8094c2d
Merge pull request #2497 from Haselnussbomber/update-fontawesome
[API14] Update Font Awesome to 7.1.0
2025-12-07 17:06:43 +01:00
Haselnussbomber
624191d1e0
Update DalamudAssetPath to FontAwesome710FreeSolid.otf 2025-12-07 16:45:40 +01:00
Haselnussbomber
c254c8600e
Update Font Awesome to 7.1.0 2025-12-07 16:31:03 +01:00
goat
61376fe84e
Merge pull request #2496 from Haselnussbomber/remove-targets
[API14] Remove targets
2025-12-07 16:25:32 +01:00
Haselnussbomber
2f5f52b572
Forgot to remove this too 2025-12-07 16:23:13 +01:00
Haselnussbomber
7199bfb0a9
Remove targets 2025-12-07 16:20:55 +01:00
goat
abcddde591
Merge pull request #2494 from Haselnussbomber/remove-obsoleted-sestring-casts
[API14] Remove obsolete casts from Lumina.Text.SeString
2025-12-07 15:59:33 +01:00
goat
2a99108eb1
Merge pull request #2495 from Haselnussbomber/expose-settings-uibuilder
[API14] Add PluginUISoundEffectsEnabled to UiBuilder
2025-12-07 15:58:58 +01:00
Haselnussbomber
8a5f1fd96d
Add PluginUISoundEffectsEnabled to UiBuilder 2025-12-07 15:55:43 +01:00
goaaats
652ff59672 build: 13.0.0.13
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
Rollup changes to next version / check (api14) (push) Failing after 4s
Tag Build / Tag Build (push) Failing after 2s
2025-12-07 15:52:26 +01:00
goaaats
094483e5a0 List PRs in changelog generator 2025-12-07 15:52:13 +01:00
goaaats
c50237cf66 Add compatibility changes for SeString API breakage 2025-12-07 15:46:01 +01:00
Haselnussbomber
d4fe523d73
Clean up some warnings 2025-12-07 15:38:10 +01:00
Haselnussbomber
9e5723359a
Remove obsolete casts from Lumina.Text.SeString 2025-12-07 15:35:38 +01:00
Haselnussbomber
07f9e03010
Update packages 2025-12-07 15:14:27 +01:00
Haselnussbomber
9cfa81c92d
Remove unused packages 2025-12-07 15:00:20 +01:00
goaaats
b35faf13b5 Show unhandled exceptions through VEH 2025-12-07 13:04:11 +01:00
goaaats
caa869d3ac Clarify exception and docs regarding off-thread drawing with SeStrings, again 2025-12-07 12:54:13 +01:00
goaaats
9fd59f736d Merge from 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
2025-12-06 18:48:31 +01:00
goat
ab5ea34e68
ci: make deploying builds globally blocking, don't cancel in-progress
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
Rollup changes to next version / check (api14) (push) Failing after 3s
Tag Build / Tag Build (push) Successful in 2s
2025-12-06 18:46:06 +01:00
goat
501e30e31c
Merge pull request #2490 from goaaats/feat/catch_clr_errors
Catch CLR exceptions
2025-12-06 18:43:32 +01:00
goaaats
3d29157391 Revert "Add git status checks to workflow to see what's dirty"
This reverts commit a36e11574b.
2025-12-06 18:38:44 +01:00
goaaats
b2d9480f9f Submit nuke schema 2025-12-06 18:38:44 +01:00
goat
1ad1343cbc
Merge pull request #2488 from goatcorp/csupdate-master
[master] Update ClientStructs
2025-12-06 18:36:40 +01:00
goat
61123ce573
Merge pull request #2485 from Haselnussbomber/update-cswin32
[API14] Update Microsoft.Windows.CsWin32
2025-12-06 18:35:41 +01:00
goat
9f565fafd8
Merge pull request #2489 from MidoriKami/Remove-Sigs
Remove AddonEventManagerAddressResolver
2025-12-06 18:33:42 +01:00
goat
88fc933e3f
Merge pull request #2491 from Haselnussbomber/drawstate-fontptr
[API14] Use ImFontPtr in SeStringDrawState
2025-12-06 18:33:09 +01:00
goaaats
e032840ac8 Clean up crash handler window log for external events 2025-12-06 18:32:03 +01:00
Haselnussbomber
1d1db04f04
Use ImFontPtr in SeStringDrawState 2025-12-06 16:09:42 +01:00
goaaats
446c7e3877 Some logging, cleanup 2025-12-06 15:25:04 +01:00
goaaats
e09c43b8de Fix bad exit condition when looping exception records 2025-12-06 15:07:46 +01:00
goaaats
9c2d2b7c1d Report CLR errors through DalamudCrashHandler/VEH by hooking ReportEventW 2025-12-06 15:07:09 +01:00
github-actions[bot]
2e5c560ed7 Update ClientStructs
Some checks failed
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-06 12:48:34 +00:00
MidoriKami
45366efd9f Remove SigScanner from ctor 2025-12-05 17:10:58 -08:00
MidoriKami
3c7dbf9f81 Remove AddonEventManagerAddressResolver.cs 2025-12-05 16:59:17 -08:00
goat
a36e11574b
Add git status checks to workflow to see what's dirty
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-06 01:10:00 +01:00
Haselnussbomber
d94cacaac3
Disable SafeHandles 2025-12-05 19:10:31 +01:00
Haselnussbomber
7cf20fe102
Update Microsoft.Windows.CsWin32 2025-12-05 18:58:10 +01:00
goat
98a4c0d4fd
Merge pull request #2479 from goatcorp/api14-rollup
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
[api14] Rollup changes from master
2025-12-05 18:20:01 +01:00
goat
f85ef995e3
Merge pull request #2486 from Haselnussbomber/update-terrafx
[API14] Update TerraFX.Interop.Windows
2025-12-05 18:19:42 +01:00
goat
e7d4786a1f
Oops, wrong version 2025-12-05 18:18:57 +01:00
goat
4d949e4a07
Merge branch 'api14' into update-terrafx 2025-12-05 18:17:52 +01:00
goat
68ca60fa8c
Merge pull request #2484 from Haselnussbomber/sharpdx-removal
[API14] Remove SharpDX
2025-12-05 18:16:00 +01:00
goat
411067219e
Merge pull request #2487 from Haselnussbomber/update-nuke
[API14] Update Nuke
2025-12-05 18:14:22 +01:00
Haselnussbomber
fc983458fa
Update Nuke 2025-12-05 01:44:18 +01:00
Haselnussbomber
ddc3113244
Update TerraFX.Interop.Windows 2025-12-05 01:34:47 +01:00
Haselnussbomber
da7be64fdf
Remove SharpDX 2025-12-04 23:34:11 +01:00
Haselnussbomber
0112e17fdb
Replace internal SharpDX usage with TerraFX 2025-12-04 23:33:48 +01:00
github-actions[bot]
6f8e33a39c Merge remote-tracking branch 'origin/master' into api14-rollup
Some checks failed
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-04 22:03:13 +00:00
goaaats
ddc743aae1 Note that font ptr must be supplied when setting TargetDrawList
Some checks failed
Rollup changes to next version / check (api14) (push) Failing after 4s
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
2025-12-04 23:00:36 +01:00
goaaats
8dcbd52c22 Merge branch 'Soreepeong-feature/enable-viewport-alpha'
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
Rollup changes to next version / check (api14) (push) Failing after 4s
Tag Build / Tag Build (push) Successful in 2s
2025-12-04 02:07:34 +01:00
goaaats
1b5fbaa82e Access custom font atlas fields directly through bindings 2025-12-04 02:04:45 +01:00
goaaats
9bce0d33a6 Don't try to free CLR memory 2025-12-04 02:04:27 +01:00
goaaats
879c210cc6 Merge 'Enable viewport alpha' (#2362) 2025-12-04 01:47:43 +01:00
goaaats
1fe2d54128 Upgrade cimgui, prep for viewport alpha 2025-12-04 01:29:23 +01:00
goat
bfd592abbe
Merge pull request #2308 from Soreepeong/feature/sestring-to-texture
Add ITextureProvider.CreateTextureFromSeString
2025-12-04 01:19:04 +01:00
goat
df0bfc18c3
Make ImGuiHelpers.CreateDrawData() internal for now 2025-12-04 01:10:51 +01:00
MidoriKami
0480693f92 Merge branch 'api14' into AddonLifecycleRefactor
# Conflicts:
#	Dalamud/Game/Addon/Lifecycle/AddonLifecycleAddressResolver.cs
2025-12-03 16:08:31 -08:00
goat
3fbc24904a
Merge branch 'master' into feature/sestring-to-texture 2025-12-04 00:57:07 +01:00
goat
5bb212bfaa
Merge pull request #2424 from Haselnussbomber/fix-service-namespaces
Some checks failed
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
[API14] Fix services using wrong namespaces
2025-12-04 00:56:10 +01:00
goat
f055af7f7b
Merge pull request #2478 from goatcorp/csupdate-master
[master] Update ClientStructs
2025-12-04 00:51:13 +01:00
goat
a917ebd856
Merge pull request #2468 from KazWolfe/rpc-unix
feat: Add unix sockets
2025-12-04 00:48:23 +01:00
github-actions[bot]
0e6dae9f64 Update ClientStructs
Some checks failed
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-03 18:39:04 +00:00
goat
4fa4d7f338
Merge pull request #2483 from Haselnussbomber/fix-beasttribe-columnoffset
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
Rollup changes to next version / check (api14) (push) Failing after 3s
Tag Build / Tag Build (push) Successful in 2s
Fix NounProcessor BeastTribe column offset
2025-12-03 17:20:02 +01:00
Haselnussbomber
f198ce46dc
Add self tests for ColumnOffset 2025-12-03 16:47:13 +01:00
Haselnussbomber
518b3a4fb3
Fix NounProcessor BeastTribe column offset 2025-12-03 16:43:12 +01:00
goat
85949072ec
Merge pull request #2476 from MidoriKami/ForceErrorStyle
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
Rollup changes to next version / check (api14) (push) Failing after 6s
Tag Build / Tag Build (push) Successful in 2s
Erroring Window Style Fix
2025-12-02 23:20:54 +01:00
MidoriKami
14e97a1a37 Use local variable to track pushed style state 2025-12-01 14:19:12 -08:00
goat
f3c826a54b
Merge pull request #2482 from Haselnussbomber/playerstate-level-fix
Some checks failed
Rollup changes to next version / check (api14) (push) Failing after 4s
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
Fix PlayerState.Level being synced
2025-12-01 13:39:07 +01:00
Haselnussbomber
fb229a0a12
Fix PlayerState.Level being synced 2025-12-01 12:09:24 +01:00
MidoriKami
85a7c60dae Fix name inconsistency 2025-11-30 22:20:02 -08:00
MidoriKami
c923884626 Disable Logging 2025-11-30 22:15:32 -08:00
MidoriKami
78781c8988 Add Move, MouseOver, MouseOut, Focus 2025-11-30 21:43:26 -08:00
MidoriKami
2e24696731 Set flags, and unlock size 2025-11-30 14:47:24 -08:00
MidoriKami
b81cb9c74c Remove generic args class 2025-11-30 14:07:44 -08:00
MidoriKami
8e8d0246bc Restore original hookwidget logic 2025-11-30 14:00:39 -08:00
MidoriKami
d47a41b295 Fix NET14 Spans defaulting to ReadOnlySpan 2025-11-30 12:48:49 -08:00
MidoriKami
c9276b1771 Merge remote-tracking branch 'origin/AddonLifecycleRefactor' into AddonLifecycleRefactor
# Conflicts:
#	Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonArgs.cs
#	Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonDrawArgs.cs
#	Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonFinalizeArgs.cs
#	Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonReceiveEventArgs.cs
#	Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonUpdateArgs.cs
#	Dalamud/Game/Addon/Lifecycle/AddonArgsType.cs
#	Dalamud/Game/Addon/Lifecycle/AddonLifecycle.cs
#	Dalamud/Game/Addon/Lifecycle/AddonLifecycleAddressResolver.cs
#	Dalamud/Game/Addon/Lifecycle/AddonVirtualTable.cs
2025-11-30 12:38:37 -08:00
MidoriKami
386828005b Apply breaking changes 2025-11-30 12:37:51 -08:00
MidoriKami
08c1768286 Bunch of stuff... 2025-11-30 11:33:06 -08:00
MidoriKami
eb9555ee22 Better unload 2025-11-30 11:33:06 -08:00
MidoriKami
be3f71dc73 Fix copy paste error 2025-11-30 11:33:06 -08:00
MidoriKami
e01acb4a80 Remove redundant header 2025-11-30 11:33:05 -08:00
MidoriKami
f8725e5f37 further improve performance 2025-11-30 11:33:05 -08:00
MidoriKami
c3e3e4aa85 Fix accidentally breaking widget 2025-11-30 11:33:05 -08:00
MidoriKami
b82b4f40ce Use hashset to prevent duplicate entries 2025-11-30 11:33:05 -08:00
MidoriKami
4f59e09513 Improve LifecycleInvoke efficiency with Dictionary 2025-11-30 11:33:05 -08:00
MidoriKami
0533872a73 Fix unreachable code complaint 2025-11-30 11:33:05 -08:00
MidoriKami
27a7adfdb9 Minor cleanup 2025-11-30 11:33:05 -08:00
MidoriKami
54bac7f32a Refactor Addon Lifecycle 2025-11-30 11:33:05 -08:00
MidoriKami
26f119096b Bunch of stuff... 2025-11-30 10:39:35 -08:00
MidoriKami
c51e65e0bd Better unload 2025-11-30 10:08:40 -08:00
Kaz Wolfe
874745651b
feat: Add PID, process time, rename ClientIdentifer to ClientState 2025-11-29 21:12:08 -08:00
goaaats
ac2d522415 build: 13.0.0.12
Some checks failed
Rollup changes to next version / check (api14) (push) Failing after 5s
Tag Build / Tag Build (push) Failing after 3s
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-11-30 02:47:07 +01:00
Kaz Wolfe
ead1c705a4
fix: Route URIs to the specified InternalName 2025-11-29 17:07:51 -08:00
goaaats
fadf941fa4 Re-add config properties for XLCore/XoM backwards compatibility 2025-11-30 02:01:01 +01:00
goat
a31dda7865
Merge pull request #2475 from goatcorp/api14-rollup
Some checks failed
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
[api14] Rollup changes from master
2025-11-29 19:39:18 +01:00
github-actions[bot]
d7e04ad4ff Merge remote-tracking branch 'origin/master' into api14-rollup
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-11-29 18:23:24 +00:00
goaaats
7510c032cc Disable Intel CET support, causes CLR crashes on unpatched Windows
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
Rollup changes to next version / check (api14) (push) Failing after 4s
Tag Build / Tag Build (push) Successful in 2s
2025-11-29 19:22:28 +01:00
goaaats
d12a9ec7da Remove DalamudBetaKey, DalamudBetaKind from config
Fix all code that depends on it to use Util.GetActiveTrack() instead
2025-11-29 19:15:37 +01:00
goat
6367a66aad
Merge pull request #2472 from goatcorp/csupdate-master
[master] Update ClientStructs
2025-11-29 18:43:05 +01:00
goat
edc6962296
Merge pull request #2477 from goaaats/feat/lumina_error
Show a sensible error message when Lumina fails to init
2025-11-29 18:39:33 +01:00
github-actions[bot]
78ecb721cd Update ClientStructs 2025-11-29 12:47:45 +00:00
MidoriKami
b8724f7a59 Fix copy paste error 2025-11-28 09:44:35 -08:00
goaaats
d7915c7020 Show a sensible error message when Lumina fails to init 2025-11-28 18:11:31 +01:00
MidoriKami
170f6e0859 Remove redundant header 2025-11-28 09:11:13 -08:00
MidoriKami
325d28ee32 further improve performance 2025-11-28 09:08:24 -08:00
MidoriKami
29c154f9b5 Fix accidentally breaking widget 2025-11-28 08:35:54 -08:00
MidoriKami
2a60bc61a7 Force style vars so erroring window renders at least partially sanely 2025-11-27 15:52:18 -08:00
MidoriKami
166f249e13 Use hashset to prevent duplicate entries 2025-11-27 14:30:40 -08:00
MidoriKami
c525655be6 Improve LifecycleInvoke efficiency with Dictionary 2025-11-27 14:24:35 -08:00
goat
02e0f1d36c
Merge pull request #2474 from goaaats/fix/pinned_escape
Some checks failed
Rollup changes to next version / check (api14) (push) Failing after 3s
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
Don't prevent closing native windows if pinned or clickthrough plugin windows are focused
2025-11-27 18:12:13 +01:00
Haselnussbomber
c661faea6b
Fix services using wrong namespaces 2025-11-27 09:41:02 +01:00
goaaats
4c3ba35f07 Don't inhibit ATK close events if pinned or clickthrough windows are focused 2025-11-27 01:45:13 +01:00
goat
d4f1636dd2
Merge pull request #2473 from goatcorp/api14-rollup
Some checks failed
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
[api14] Rollup changes from master
2025-11-26 22:43:25 +01:00
github-actions[bot]
196a5ef709 Merge remote-tracking branch 'origin/master' into api14-rollup
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-11-26 20:47:44 +00:00
goaaats
c136934aa8 Always pass a key, even for release
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
Rollup changes to next version / check (api14) (push) Failing after 4s
Tag Build / Tag Build (push) Successful in 1s
Fixes an issue wherein the XL commandline parser wouldn't like the empty argument and error out
2025-11-26 21:46:07 +01:00
goaaats
c6b173dd63 build: 13.0.0.11 2025-11-26 21:22:59 +01:00
goat
5e192ef39b
Merge pull request #2467 from goatcorp/api14-rollup
[api14] Rollup changes from master
2025-11-26 21:15:41 +01:00
github-actions[bot]
947518b3d6 Merge remote-tracking branch 'origin/master' into api14-rollup 2025-11-26 20:10:02 +00:00
goaaats
9e5195492e Get active track from env var, instead of git branch 2025-11-26 21:08:50 +01:00
Kaz Wolfe
2cef75bbbe
feat: remove socket cleanup tasks 2025-11-26 11:56:30 -08:00
goat
1e7e7c732d
Merge pull request #2470 from goaaats/feat/handle_draw_errors
Handle errors in Window draw events by displaying an error message
2025-11-26 20:54:48 +01:00
goaaats
efd66fd3f8 Handle errors in Window draw events by displaying an error message 2025-11-26 19:29:14 +01:00
MidoriKami
ab0500ca6f Fix unreachable code complaint 2025-11-25 20:45:54 -08:00
MidoriKami
2c1bb76643 Minor cleanup 2025-11-25 18:56:34 -08:00
MidoriKami
9a1fae8246 Refactor Addon Lifecycle 2025-11-25 17:27:48 -08:00
goaaats
bd427d7b54 build: 13.0.0.10
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
Rollup changes to next version / check (api14) (push) Failing after 8s
Tag Build / Tag Build (push) Failing after 2s
2025-11-25 20:32:39 +01:00
goaaats
d56c7a1963 Add auto-generated changelogs through CI 2025-11-25 20:32:12 +01:00
Kaz Wolfe
8ab7b59ae4
fix: Missing service types causing injection failures
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-11-25 10:17:12 -08:00
Kaz Wolfe
7b286c427c
chore: remove named pipe transport, use startinfo for pathing 2025-11-25 10:08:24 -08:00
goat
8773964de9
Merge pull request #2469 from Haselnussbomber/nullcheck-TestingDalamudApiLevel
Require TestingDalamudApiLevel to be set for testing
2025-11-25 18:53:22 +01:00
Haselnussbomber
1f30ce4c39
Require TestingDalamudApiLevel to be set for testing 2025-11-25 18:41:21 +01:00
bleatbot
ea07f41ab1
Update ClientStructs (#2465)
Some checks failed
Rollup changes to next version / check (api14) (push) Failing after 3s
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
Co-authored-by: github-actions[bot] <noreply@github.com>
2025-11-23 14:47:28 -08:00
goaaats
0656bff1f9 Fix experimental diagnostic IDs
Some checks failed
Rollup changes to next version / check (api14) (push) Failing after 4s
Tag Build / Tag Build (push) Successful in 1s
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-11-20 19:31:50 +01:00
Kaz Wolfe
0d8f577576
feat: add debug link handler as demo 2025-11-18 16:28:03 -08:00
Kaz Wolfe
01d8fc0c7e
fix: log tweaks
- also fix a boot failure
2025-11-18 15:57:37 -08:00
Kaz Wolfe
71927a8bf6
feat: Add unix sockets
- Unix sockets run parallel to Named Pipes
  - Named Pipes will only run on non-Wine
  - If the game crashes, the next run will clean up an orphaned socket.
- Restructure RPC to be a bit tidier
2025-11-18 15:20:22 -08:00
goaaats
0daca30203 Make IReliableFileStorage experimental, add legal diagnostic IDs
Some checks failed
Rollup changes to next version / check (api14) (push) Failing after 3s
Tag Build / Tag Build (push) Successful in 1s
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-11-18 23:17:21 +01:00
goaaats
28941cb69e SelfTestRegistryPluginScoped should inherit from IDalamudService 2025-11-18 21:58:13 +01:00
goaaats
f831a7c010 Wait for pending writes when disposing service 2025-11-18 21:39:47 +01:00
goaaats
05648f019b First draft of IReliableFileStorage service 2025-11-18 20:37:57 +01:00
goaaats
6a69a6e197 Fix some warnings
Some checks failed
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-11-18 00:58:08 +01:00
goaaats
cc91916574 Fix bad merge 2025-11-18 00:52:30 +01:00
goaaats
20041be27c Convert ReliableFileStorage to async 2025-11-18 00:44:04 +01:00
goat
b7dda599fb
Merge pull request #2464 from goatcorp/api14-rollup
[api14] Rollup changes from master
2025-11-17 23:16:58 +01:00
github-actions[bot]
63b7ecf0d7 Merge remote-tracking branch 'origin/master' into api14-rollup
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-11-17 22:05:40 +00:00
goat
e65f441105
Merge pull request #2434 from Haselnussbomber/service-provider
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
Rollup changes to next version / check (api14) (push) Failing after 6s
Tag Build / Tag Build (push) Successful in 1s
Simpler plugin service dependency injection
2025-11-17 23:04:45 +01:00
goat
1822ef1808
Merge pull request #2466 from Haselnussbomber/playerstate-ctor-fix
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
Rollup changes to next version / check (api14) (push) Failing after 4s
Tag Build / Tag Build (push) Successful in 2s
Add empty ServiceConstructor to PlayerState
2025-11-17 21:07:10 +01:00
Haselnussbomber
cb441631e1
Add empty ServiceConstructor to PlayerState 2025-11-17 20:58:14 +01:00
Haselnussbomber
1bdad092ca
Mark IPlayerState with IDalamudService 2025-11-17 20:28:52 +01:00
Haselnussbomber
f4c9c16c68
Mark IUnlockState with IDalamudService 2025-11-17 20:28:25 +01:00
Haselnussbomber
53a082e68d
Check that PluginInterfaces implement IDalamudService 2025-11-17 20:28:25 +01:00
Haselnussbomber
9ea417c9ef
Fix GetService getting stuck in Task 2025-11-17 20:28:25 +01:00
Haselnussbomber
9001c96986
Return the resulting service, not the Task 2025-11-17 20:28:25 +01:00
Haselnussbomber
46dee9a483
Inherit documentation in DalamudPluginInterface 2025-11-17 20:28:24 +01:00
Haselnussbomber
d3c812ba6c
Add IDalamudService marker interface 2025-11-17 20:28:24 +01:00
Haselnussbomber
7ec1de4c76
Let IDalamudPluginInterface inherit from IServiceProvider 2025-11-17 20:28:24 +01:00
goat
05f31265eb
Merge pull request #2422 from Haselnussbomber/playerstate-service
Add IPlayerState service
2025-11-17 20:28:01 +01:00
Haselnussbomber
64d4f7061a
Rename namespace PlayerState to Player 2025-11-17 19:29:48 +01:00
goat
e4eca842d3
Merge pull request #2461 from goatcorp/api14-rollup
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
[api14] Rollup changes from master
2025-11-17 19:18:36 +01:00
goat
a88247bdfc
Merge pull request #2454 from goatcorp/csupdate-master
[master] Update ClientStructs
2025-11-17 19:17:51 +01:00
github-actions[bot]
c79fa96505 Merge remote-tracking branch 'origin/master' into api14-rollup 2025-11-17 17:43:53 +00:00
goat
596af24e95
Merge pull request #2421 from Haselnussbomber/unlockstate-service
Add IUnlockState service
2025-11-17 18:42:49 +01:00
goat
ba0cf4c990
Merge pull request #2458 from Haselnussbomber/struct-enumerators
[API14] Use struct enumerators/types
2025-11-17 18:24:07 +01:00
goat
9a49a9588b
Merge pull request #2462 from KazWolfe/rpc
feat: Dalamud RPC service
2025-11-17 18:11:04 +01:00
github-actions[bot]
750fa58147 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-11-17 12:52:07 +00:00
Kaz Wolfe
19a3926051
Better hello message 2025-11-16 21:35:33 -08:00
Kaz Wolfe
4937a2f4bd
CR changes 2025-11-16 18:14:02 -08:00
Kaz Wolfe
78ed4a2b01
feat: Dalamud RPC service
A draft for a simple RPC service for Dalamud. Enables use of Dalamud URIs, to be added later.
2025-11-16 16:08:24 -08:00
goaaats
72dc094b57 build: 13.0.0.9
Some checks failed
Rollup changes to next version / check (api14) (push) Failing after 4s
Tag Build / Tag Build (push) Failing after 3s
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-11-15 19:41:46 +01:00
goat
62b9c1f2a1
Merge pull request #2460 from goatcorp/api14-rollup
Some checks failed
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
[api14] Rollup changes from master
2025-11-15 01:09:51 +01:00
github-actions[bot]
a2e923b051 Merge remote-tracking branch 'origin/master' into api14-rollup 2025-11-15 00:09:30 +00:00
goaaats
fea7b3676f Start correct XL binary through branch switcher, add build branch to metadata
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
Rollup changes to next version / check (api14) (push) Failing after 5s
Tag Build / Tag Build (push) Successful in 2s
2025-11-15 01:08:23 +01:00
goat
de396e70f8
Merge pull request #2459 from goatcorp/api14-rollup
[api14] Rollup changes from master
2025-11-15 00:51:49 +01:00
github-actions[bot]
7a8f01f418 Merge remote-tracking branch 'origin/master' into api14-rollup 2025-11-14 23:49:59 +00:00
goat
a134c6d064
api14 rollup 2025-11-15 00:49:06 +01:00
Haselnussbomber
9d0879148c
Remove unused StatusEffect struct 2025-11-13 19:06:23 +01:00
Haselnussbomber
778c82fad2
Add struct enumerator to StatusList 2025-11-13 19:06:23 +01:00
Haselnussbomber
7f2ed9adb6
Convert Status to readonly struct and add interface 2025-11-13 19:06:23 +01:00
Haselnussbomber
53b94caeb7
Convert PartyMember to readonly struct 2025-11-13 19:06:18 +01:00
Haselnussbomber
d1dc81318a
Add struct enumerator to PartyList 2025-11-13 19:04:38 +01:00
Haselnussbomber
a48eead85e
Convert Fate to readonly struct 2025-11-13 19:04:35 +01:00
Haselnussbomber
d1bed3ebc5
Add struct enumerator to FateTable 2025-11-13 19:04:12 +01:00
Haselnussbomber
23e7c164d8
Convert BuddyMember to readonly struct 2025-11-13 19:04:11 +01:00
Haselnussbomber
8a9b47c7a4
Add struct enumerator to BuddyList 2025-11-13 19:03:56 +01:00
Haselnussbomber
520e3ea028
Convert AetheryteEntry to readonly struct 2025-11-13 19:03:53 +01:00
Haselnussbomber
dd70c5b8ee
Add struct enumerator to AetheryteList 2025-11-13 18:44:15 +01:00
Haselnussbomber
2b2f628096
Convert ObjectTable enumerator to struct 2025-11-13 18:44:14 +01:00
goaaats
6340afb692 Nuke schema, also remove analyzers from imgui testbed 2025-11-12 21:39:38 +01:00
goaaats
928fbba489 Remove Injector.Boot targets 2025-11-12 21:13:50 +01:00
goaaats
7bc921f543 No analyzers on nuke build 2025-11-12 21:09:21 +01:00
goaaats
a37a13e0ba Use .NET 10 in CI 2025-11-12 21:03:14 +01:00
goaaats
e0eff2fe74 Use standard apphost for Dalamud.Injector 2025-11-12 21:02:07 +01:00
goaaats
7d76d27555 Upgrade packages 2025-11-12 20:31:28 +01:00
goaaats
4e87b4b007 Retarget to .NET 10 2025-11-12 20:15:12 +01:00
goat
b11b769292
Merge pull request #2453 from Exter-N/get-plugin-by-assembly
Some checks failed
Tag Build / Tag Build (push) Successful in 3s
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
Add functions to get a plugin by assembly
2025-11-12 01:33:24 +01:00
goat
dc9ff0a54c
Merge pull request #2448 from Haselnussbomber/dtr-removenode-exception-fix
Fix KeyNotFoundException in DtrBar.RemoveNode
2025-11-12 00:49:40 +01:00
Haselnussbomber
45bd30fcca
Use inherited fields 2025-11-12 00:27:51 +01:00
Haselnussbomber
f635c149a2
Set TextNode to null after destroying it 2025-11-12 00:26:06 +01:00
Haselnussbomber
e1fde804ec
Add warning if RemoveNode can't find the node 2025-11-12 00:21:47 +01:00
goat
bcce4f4216
Merge pull request #2455 from Exter-N/dtr-screen-bounds
Add a property to get the bounds of a DTR entry
2025-11-12 00:04:50 +01:00
goat
9450f65159
Merge pull request #2456 from Haselnussbomber/autoupdatenullable
Fix IsAutoUpdateComplete throwing when unloaded
2025-11-11 23:55:14 +01:00
Haselnussbomber
bf0bd64faf
Fix IsAutoUpdateComplete throwing when unloaded 2025-11-11 23:37:10 +01:00
Exter-N
4cfe561c1c Add a property to get the bounds of a DTR entry 2025-11-11 21:28:30 +01:00
goaaats
f6cd6d31ff Adjust branch switcher to XL 7, pass beta kind and key as arguments 2025-11-11 20:29:06 +01:00
Exter-N
65237f84a2 Add functions to get a plugin by assembly
This is intended for advanced IPC scenarios, for example, accepting
a delegate or an object and identifying which plugin it originates
from, in order to display integration information to the user, and/or
to release references when the originating plugin is unloaded/reloaded
if it forgot to clean after itself.
2025-11-11 20:26:54 +01:00
goaaats
dabe7d777b build: 13.0.0.8 2025-11-11 19:27:46 +01:00
goat
93b95fd813
Merge pull request #2452 from KazWolfe/obsolete-fixes
Some checks are pending
Build Dalamud / Build on Windows (push) Waiting to run
Build Dalamud / Check API Compatibility (push) Blocked by required conditions
Build Dalamud / Deploy dalamud-distrib staging (push) Blocked by required conditions
Tag Build / Tag Build (push) Successful in 3s
fix: some minor IDE complaints
2025-11-11 17:50:32 +01:00
Kaz Wolfe
fe163fbb97
fix: some minor IDE complaints 2025-11-11 08:28:14 -08:00
bleatbot
963b3d9318
Update ClientStructs (#2447)
Co-authored-by: github-actions[bot] <noreply@github.com>
2025-11-11 07:27:07 -08:00
goat
bfb07580fe
Merge pull request #2451 from Haselnussbomber/SelfTestRegistry-fix
Some checks are pending
Build Dalamud / Build on Windows (push) Waiting to run
Build Dalamud / Check API Compatibility (push) Blocked by required conditions
Build Dalamud / Deploy dalamud-distrib staging (push) Blocked by required conditions
Tag Build / Tag Build (push) Successful in 3s
Pass down SelfTestRegistry to SelfTestWindow
2025-11-11 09:19:42 +01:00
Haselnussbomber
af03e292ba
Pass down SelfTestRegistry to SelfTestWindow 2025-11-11 09:09:29 +01:00
bleatbot
5dd121dfcc
Update Excel Schema (#2430)
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
Co-authored-by: github-actions[bot] <noreply@github.com>
2025-11-08 08:02:16 -08:00
Haselnussbomber
497e61f699
Remove comment about removed CS enum 2025-11-08 11:58:54 +01:00
Haselnussbomber
5cc327c5f9
Fix obsolete 2025-11-08 11:49:23 +01:00
Haselnussbomber
af8b61f08a
Update to use AgentUpdate event 2025-11-08 11:49:04 +01:00
Haselnussbomber
700aaa4a5d
Fix Unlock event not firing 2025-11-08 11:45:02 +01:00
Haselnussbomber
69caffeb97
Add support for EmjCostume rows 2025-11-08 11:45:02 +01:00
Haselnussbomber
a06c0e3ed2
Add support for EmjVoiceNpc rows 2025-11-08 11:45:01 +01:00
Haselnussbomber
880add5ab3
Add support for MKDLore rows 2025-11-08 11:45:01 +01:00
Haselnussbomber
193d321103
Add support for Soul Shard items 2025-11-08 11:45:01 +01:00
Haselnussbomber
6e8efabc3b
Add support for Occult Record items 2025-11-08 11:45:01 +01:00
Haselnussbomber
68c02caf37
Fix obsolete warnings 2025-11-08 11:45:01 +01:00
Haselnussbomber
878080d660
Fix IsChocoboTaxiStandUnlocked call 2025-11-08 11:45:00 +01:00
Haselnussbomber
986dfa04d0
Mark IUnlockState as experimental 2025-11-08 11:45:00 +01:00
Haselnussbomber
3746c47a84
Ignore RecipeData updates when not logged in
Just to be safe...
2025-11-08 11:45:00 +01:00
Haselnussbomber
c4dd75bdda
Update RecipeData when levels changed 2025-11-08 11:45:00 +01:00
Haselnussbomber
5905afdf10
Cache completed Quests in RecipeData too 2025-11-08 11:45:00 +01:00
Haselnussbomber
62fdd2c60d
Fix IsChocoboTaxiStandUnlocked 2025-11-08 11:45:00 +01:00
Haselnussbomber
ba159f8c5f
Add IsRecipeUnlocked 2025-11-08 11:44:59 +01:00
Haselnussbomber
6ade5b21cf
Add IUnlockState service 2025-11-08 11:44:59 +01:00
goat
3c3eb9159c
Merge pull request #2420 from Haselnussbomber/add-agent-events
Add events based on AgentUpdateFlag
2025-11-08 11:44:44 +01:00
Haselnussbomber
494d9a04fa
Remove unknown NameplateUpdate flag for now 2025-11-08 11:32:34 +01:00
Haselnussbomber
8fd49f261a
Unify agent update events into AgentUpdate 2025-11-08 11:31:10 +01:00
Haselnussbomber
2a65d1e045
Fix KeyNotFoundException in DtrBar.RemoveNode 2025-11-07 16:04:13 +01:00
goat
832edaf005
Merge pull request #2377 from KazWolfe/assert-blame
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
feat: Identify the plugin causing an assertion failure
2025-11-04 21:44:38 +01:00
KazWolfe
8a49a11dc0
fix: dont nag maintainers when nothing needs to be done (#2418) 2025-11-04 21:18:38 +01:00
goat
32e04458c6
Merge pull request #2416 from Haselnussbomber/fix-testing-api-level
Fix for testing plugins with older stable releases
2025-11-04 20:36:49 +01:00
goat
165060b62b
Merge pull request #2444 from goatcorp/csupdate-master
[master] Update ClientStructs
2025-11-04 20:33:59 +01:00
goat
fc480d8542
Merge pull request #2431 from grittyfrog/push-plttolzpzvkr
Plugin-registerable self tests
2025-11-04 20:32:36 +01:00
github-actions[bot]
7751ea7185 Update ClientStructs
Some checks failed
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-11-04 18:35:02 +00:00
goat
97600b1b2c
Merge pull request #2446 from nebel/nameplate-gameobj-more-null-checks-p2
Some checks are pending
Build Dalamud / Build on Windows (push) Waiting to run
Build Dalamud / Check API Compatibility (push) Blocked by required conditions
Build Dalamud / Deploy dalamud-distrib staging (push) Blocked by required conditions
Tag Build / Tag Build (push) Successful in 3s
Add yet another check to NamePlateUpdateHander.GameObject
2025-11-04 13:24:13 +01:00
nebel
417fe39cd9
Add yet another check to NamePlateUpdateHander.GameObject 2025-11-04 20:28:34 +09:00
goat
81e5793150
Merge pull request #2445 from Haselnussbomber/fix-imgui-callbacks
Some checks failed
Tag Build / Tag Build (push) Successful in 3s
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 ImDrawCallback call
2025-11-02 15:58:24 +01:00
Haselnussbomber
ce16b59f5b
Fix ImDrawCallback call 2025-11-02 15:40:22 +01:00
goaaats
606d58c77e build: 13.0.0.7
Some checks failed
Tag Build / Tag Build (push) Failing after 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
2025-10-29 20:57:06 +01:00
goaaats
55246ab1ec Enable sound effects by default for new users 2025-10-29 20:56:38 +01:00
goat
5290e191a1
Merge pull request #2443 from Haselnussbomber/auto-update-on-free-trial
Some checks are pending
Build Dalamud / Build on Windows (push) Waiting to run
Build Dalamud / Check API Compatibility (push) Blocked by required conditions
Build Dalamud / Deploy dalamud-distrib staging (push) Blocked by required conditions
Tag Build / Tag Build (push) Successful in 3s
Fix systems not working on the Free Trial
2025-10-29 09:18:36 +01:00
Haselnussbomber
08a3998854
Use IsClientIdle in CanUpdateOrNag 2025-10-29 02:31:42 +01:00
Haselnussbomber
e6df536ceb
Don't always flag as idle when on Free Trial 2025-10-29 02:13:58 +01:00
Haselnussbomber
76dab05cbd
Allow auto updates when on Free Trial 2025-10-29 02:11:46 +01:00
goat
22f21614ae
Merge pull request #2432 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
2025-10-29 00:29:18 +01:00
github-actions[bot]
11e85c6619 Update ClientStructs
Some checks failed
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-10-28 23:17:54 +00:00
goat
90dc48f7b2
Merge pull request #2426 from Haselnussbomber/eventtutorial-icon
Add BitmapFontIcon.EventTutorial
2025-10-29 00:16:32 +01:00
goat
c508632150
Merge pull request #2438 from Haselnussbomber/fix-resize
Fix AddonEventType.Resize
2025-10-29 00:16:15 +01:00
KazWolfe
de07a5f1b7
fix: missing args in HandleActionHoverHook (#2440)
- remove verbose logs
2025-10-29 00:15:56 +01:00
goat
31340dda84
Merge pull request #2442 from nebel/nameplate-gameobj-more-null-checks
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
Add more null checks to NamePlateUpdateHander.GameObject
2025-10-28 19:12:08 +01:00
nebel
4fff7dee5a
Add more null checks to NamePlateUpdateHander.GameObject 2025-10-28 09:20:31 +09:00
Haselnussbomber
39e6186ba3
Fix AddonEventType.Resize 2025-10-24 03:07:36 +02:00
goat
7a45c0d661
Merge pull request #2437 from Haselnussbomber/fix-NotifyPluginsForStateChange
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
Lock plugin list in NotifyPluginsForStateChange
2025-10-22 21:50:40 +02:00
Haselnussbomber
87e391958e
Lock plugin list in NotifyPluginsForStateChange 2025-10-22 19:14:29 +02:00
goat
21d4dbec66
Merge pull request #2436 from Haselnussbomber/public-end-objects
Some checks are pending
Build Dalamud / Build on Windows (push) Waiting to run
Build Dalamud / Check API Compatibility (push) Blocked by required conditions
Build Dalamud / Deploy dalamud-distrib staging (push) Blocked by required conditions
Tag Build / Tag Build (push) Successful in 3s
Small ImRaii update
2025-10-22 15:10:41 +02:00
Haselnussbomber
f0568216cb
Provide PopupModal with flags 2025-10-22 07:18:43 +02:00
Haselnussbomber
8ed21b4645
Public ImRaii end objects 2025-10-22 07:16:13 +02:00
Haselnussbomber
0b6f3b8bcf
Add events based on AgentUpdateFlag 2025-10-19 16:51:47 +02:00
goaaats
116e8aadbc build: 13.0.0.6
Some checks failed
Tag Build / Tag Build (push) Failing after 3s
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-10-16 21:10:27 +02:00
goaaats
2be6566e81 Fix broken spacing in update chat message 2025-10-16 20:34:37 +02:00
goat
49eac894a8
Merge pull request #2428 from Haselnussbomber/lumina-ssb-atktweaks
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
Use Luminas SeStringBuilder in DalamudAtkTweaks
2025-10-16 00:46:06 +02:00
goat
16fdf4e32b
Merge pull request #2429 from goatcorp/csupdate-master
[master] Update ClientStructs
2025-10-16 00:01:15 +02:00
goaaats
76afcaf426 Disable CPM for libs 2025-10-15 23:24:21 +02:00
goaaats
7b723687a4 Use NuGet CPM to ensure consistent package versions
Fixes various issues with projects referencing different versions of libraries, causing deployment issues if build order differs
2025-10-15 22:58:26 +02:00
goaaats
168a334756 Injector: remove custom DllImports, replace with CsWin32 2025-10-15 22:58:26 +02:00
goaaats
5fd24f4bed Bump Reloaded.Memory.Buffers, fixes conflict with injector 2025-10-15 22:58:26 +02:00
github-actions[bot]
9fb0a79885 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-10-15 18:34:54 +00:00
goat
db22c5e111
Remove status badge from readme
Some checks are pending
Build Dalamud / Build on Windows (push) Waiting to run
Build Dalamud / Check API Compatibility (push) Blocked by required conditions
Build Dalamud / Deploy dalamud-distrib staging (push) Blocked by required conditions
Tag Build / Tag Build (push) Successful in 3s
2025-10-14 22:59:50 +02:00
goaaats
2207ad0886 Bump Reloaded.Memory.Buffers to simplify hook buffer acquisition 2025-10-14 22:51:17 +02:00
GrittyFrog
ae777000e2 Plugin-registerable self tests
The goal of this change is to let plugins register their own self-tests. 

We do this through the `ISelfTestRegistry` interface. For a plugin it
would look like this:

```csharp
[PluginService]
public ISelfTestRegistry SelfTestRegistry

// Somewhere that gets called by your plugin
SelfTestRegistry.RegisterTestSteps([
  new MySelfTestStep(),
  new MyOtherSelfTestStep()
])
```

Where `MySelfTest` and `MyOtherSelfTest` are instances of
the existing `ISelfTestStep` interface.

The biggest changes are to `SelfTestWindow` and the introduction of
`SelfTestWithResults`. I wanted to make sure test state wasn't lost when
changing the dropdown state and I was finding it a bit annoying to work
with the Dictionary now that we can't just rely on the index of the
item.

To fix this I moved all the "test run" state into `SelfTestWithResults`,
most of the changes to `SelfTestWindow` are derived from that, other
then the addition of the combo box.

The documentation for this service is a bit sparse, but I wanted to put
it up for review first before I invest a bunch of time making nice
documentation. 

I'm keen to hear if we think this is useful or if any changes are
needed.
2025-10-13 19:04:08 +11:00
Haselnussbomber
4a869bad3f
Use Luminas SeStringBuilder in DalamudAtkTweaks 2025-10-08 03:41:34 +02:00
Haselnussbomber
9852feaf08
Add RentedSeStringBuilder
Co-authored-by: Soreepeong <3614868+Soreepeong@users.noreply.github.com>
2025-10-08 03:41:31 +02:00
Haselnussbomber
26d2f764c6
Add BitmapFontIcon.EventTutorial 2025-10-08 03:05:39 +02:00
goaaats
4ac4505d72 build: 13.0.0.5
Some checks failed
Tag Build / Tag Build (push) Failing after 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
2025-10-07 23:03:29 +02:00
Ottermandias
1ae7de26bf
Fix issue in SigScanner. (#2425)
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
2025-10-07 09:40:44 -07:00
bleatbot
1073227b83
Update ClientStructs (#2419)
Co-authored-by: github-actions[bot] <noreply@github.com>
2025-10-07 09:06:43 -07:00
bleatbot
11adffd700
Update Excel Schema (#2423)
Co-authored-by: github-actions[bot] <noreply@github.com>
2025-10-07 08:33:24 -07:00
Haselnussbomber
2cf869872d
Return IReadOnlyList instead of ReadOnlySpan 2025-10-06 02:08:18 +02:00
Haselnussbomber
bcf651b5c1
Fix FavoriteAetherytes 2025-10-06 01:38:35 +02:00
Haselnussbomber
a55c8ca773
Fix warning 2025-10-05 14:38:50 +02:00
Haselnussbomber
153870a053
Add mentor states 2025-10-05 14:37:22 +02:00
Haselnussbomber
c2fc04c3a8
Improve wording 2025-10-05 14:37:11 +02:00
Haselnussbomber
8cac486249
Add PluginInterface attribute to PlayerState 2025-10-05 14:36:55 +02:00
Haselnussbomber
4422622e1e
Add IPlayerState service 2025-10-05 13:49:25 +02:00
bleatbot
7bf79bdea6
Update ClientStructs (#2417)
Some checks failed
Tag Build / Tag Build (push) Successful in 1s
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
Co-authored-by: github-actions[bot] <noreply@github.com>
2025-09-29 21:13:08 +00:00
bleatbot
7813f5d201
Update ClientStructs (#2413)
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
Co-authored-by: github-actions[bot] <noreply@github.com>
2025-09-29 09:09:32 -07:00
srkizer
aa3d6c3efe
Use long running task for plugin ctor (#2406) 2025-09-29 09:09:02 -07:00
Haselnussbomber
d61a35b81f
Update Settings Window (#2400)
* Load new localization before firing change event

* Update texts in SettingsWindow when locale changes

* Localize settings search

* Update settings search input

- Disable when Credits are scrolling,
so Search Results aren't shown instead
- Select all on single click, as usual for a search bar

* Remove unused IsVisible property

* Fix General tab being unselected on language change

* Fix search results throwing, oops

* Missed using LocRef in EnumSettingsEntry

* Set CultureInfo before loading locs

* Change it to LazyLoc instead

So CheapLoc can export localizations...
2025-09-29 09:08:25 -07:00
Haselnussbomber
efaff769b5
Rename Id fields to match CS names (#2405)
* Rename DataId to BaseId

* Fix obsoletes

* Inherit documentation
2025-09-29 09:06:28 -07:00
Haselnussbomber
9091216e1c
Update ClientState (#2410)
* Add MapChanged event to ClientState

* Add PublicInstanceId with event to ClientState

* Set eventhandlers to null

* Rework events and add ZoneInit event
2025-09-29 09:02:53 -07:00
Haselnussbomber
9c5e4f5a32
Add JsonIgnore attribute on IsAvailableForTesting 2025-09-28 16:21:25 +02:00
Haselnussbomber
87adb2dfb7
More expressive code 2025-09-28 16:00:09 +02:00
Haselnussbomber
8edbc0ee78
Ignore user testing opt-in for manifest eligibility check 2025-09-28 15:59:08 +02:00
Haselnussbomber
d1fbee2829
Remove manifest API filter in installer
The API is already checked in `PluginManager.IsManifestEligible`,
so a plugin not matching it doesn't even get here.
2025-09-28 15:39:36 +02:00
Haselnussbomber
191aa8d696
Move IsAvailableForTesting to IPluginManifest 2025-09-28 15:31:46 +02:00
goat
0bc44154aa
Merge pull request #2414 from Haselnussbomber/sestring-evaluator-and-creator-fixes
Some checks failed
Tag Build / Tag Build (push) Successful in 3s
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
SeString Evaluator/Creator fixes
2025-09-23 17:07:16 +02:00
Haselnussbomber
1633e68b76
Fix/simplify range handling 2025-09-23 13:16:39 +02:00
Haselnussbomber
dceeccb242
Load valid sheets for AddFromSheetPopup asynchronously 2025-09-22 23:56:28 +02:00
Haselnussbomber
2625f51021
Fix missing macro strings in AddFromSheetPopup 2025-09-22 23:55:16 +02:00
Haselnussbomber
69a8bdd638
Update GlobalParameters list 2025-09-22 23:49:57 +02:00
Haselnussbomber
9447708058
Fix Copy MacroString button 2025-09-22 23:36:39 +02:00
Haselnussbomber
e2e3a01cc3
Fix SeString Creator input length cutting off long macro strings 2025-09-22 23:33:30 +02:00
Haselnussbomber
9b0c275b8b
Fix ExpressionType.Weekday being off by 1 2025-09-22 23:29:40 +02:00
Caraxi
0b3a5a713e
enforce limits on alpha slider (#2412)
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
2025-09-21 20:24:48 -07:00
bleatbot
c38365bc99
Update ClientStructs (#2398)
Co-authored-by: github-actions[bot] <noreply@github.com>
2025-09-21 20:23:53 -07:00
bleatbot
3d0790e650
Update Excel Schema (#2408)
Co-authored-by: github-actions[bot] <noreply@github.com>
2025-09-21 20:23:03 -07:00
goat
1cc9071ce4
Update SeStringEvaluator, Payload, AutoTranslatePayload (#2411)
Some checks are pending
Build Dalamud / Build on Windows (push) Waiting to run
Build Dalamud / Check API Compatibility (push) Blocked by required conditions
Build Dalamud / Deploy dalamud-distrib staging (push) Blocked by required conditions
Tag Build / Tag Build (push) Successful in 3s
* Handle multiple col- entries in Completion.LookupTable

* Pass correct length to DecodeImpl

* Skip to payload end instead of reading remaining bytes

* Use SeStringEvaluator in AutoTranslatePayload

* Rework range check

- Loops through all entries
- Bumped amount of cols up to 8 for future proofing
- Use Ordinal search
- Actually parse ranges and check if the RowId is allowed

* Only check range if it has ranges

* Fix Group being off by 1

* Add AutoTranslatePayload re-encode test

* Remove redundant checks

* Add some AutoTranslatePayload.Text self-tests

* Remove AutoTranslatePayload text cache

- Lumina lookups aren't that taxing anymore
- Cache wasn't invalidated when the language override changes
2025-09-21 22:34:19 +02:00
Haselnussbomber
ce1faa50cf
Remove AutoTranslatePayload text cache
- Lumina lookups aren't that taxing anymore
- Cache wasn't invalidated when the language override changes
2025-09-21 14:00:23 +02:00
Haselnussbomber
374f9fcbd0
Add some AutoTranslatePayload.Text self-tests 2025-09-21 03:10:00 +02:00
Haselnussbomber
9b224857f1
Remove redundant checks 2025-09-21 03:08:28 +02:00
Haselnussbomber
0bb87d87b7
Add AutoTranslatePayload re-encode test 2025-09-21 02:18:55 +02:00
Haselnussbomber
c03e7ecfe6
Fix Group being off by 1 2025-09-21 02:16:09 +02:00
Haselnussbomber
c264fb134e
Only check range if it has ranges 2025-09-20 04:09:23 +02:00
Haselnussbomber
d8555f207e
Rework range check
- Loops through all entries
- Bumped amount of cols up to 8 for future proofing
- Use Ordinal search
- Actually parse ranges and check if the RowId is allowed
2025-09-20 03:54:41 +02:00
Haselnussbomber
3f037e5d20
Use SeStringEvaluator in AutoTranslatePayload 2025-09-20 02:51:00 +02:00
Haselnussbomber
93a44842ed
Skip to payload end instead of reading remaining bytes 2025-09-20 02:51:00 +02:00
Haselnussbomber
327ebf3bb3
Pass correct length to DecodeImpl 2025-09-20 02:50:54 +02:00
Haselnussbomber
bf4fc7864f
Handle multiple col- entries in Completion.LookupTable 2025-09-20 02:50:49 +02:00
goaaats
fa58d7b3cc build: 13.0.0.4
Some checks failed
Tag Build / Tag Build (push) Failing after 3s
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-09-12 01:42:45 +02:00
Asriel
a2b3fb901e
Fix Lumina.Excel branch in workflow (#2407)
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
2025-09-11 17:06:07 +00:00
Asriel
5b5fdc0c10
Generalize submodules workflow to add Lumina.Excel (#2340)
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
2025-09-10 16:55:25 -07:00
MidoriKami
f07b308757
Add Generic Helper (#2403)
Some checks failed
Tag Build / Tag Build (push) Successful in 3s
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-09-09 17:53:38 +10:00
Caraxi
0047e24031
fix ViewportTextureWrap (#2402)
Some checks failed
Tag Build / Tag Build (push) Successful in 3s
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-09-06 14:32:30 +10:00
goaaats
1b08d986ca build: 13.0.0.3
Some checks failed
Tag Build / Tag Build (push) Failing after 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
2025-09-03 01:51:14 +02:00
Limiana
29c4d4939f
Fix SigScanner.ScanAllText only returning a single match 2025-09-02 18:06:06 +00:00
bleatbot
97196a1e35
Update ClientStructs (#2396)
Co-authored-by: github-actions[bot] <noreply@github.com>
2025-09-02 17:13:17 +00:00
Infi
7aa77aa2eb
- Use ToMacroString for better understanding what changed (#2393)
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
2025-09-02 22:06:02 +10:00
Haselnussbomber
7f3fc5aac1
Use AgentLobbys OnLogout vfunc (#2395) 2025-09-02 22:01:12 +10:00
bleatbot
852b1289e2
Update ClientStructs (#2390)
Co-authored-by: github-actions[bot] <noreply@github.com>
2025-09-02 21:59:20 +10:00
MidoriKami
e3498f1b9c
Fix entries yeeting themselves out of existance when scrolled off (#2392)
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
2025-08-30 12:59:05 +02:00
bleatbot
80c2985795
Update ClientStructs (#2389)
Some checks failed
Tag Build / Tag Build (push) Successful in 1s
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
Co-authored-by: github-actions[bot] <noreply@github.com>
2025-08-26 21:00:59 +00:00
bleatbot
6890e59af5
Update ClientStructs (#2387)
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
Co-authored-by: github-actions[bot] <noreply@github.com>
2025-08-26 06:43:35 +00:00
bleatbot
fcf5c27186
Update ClientStructs (#2378)
Co-authored-by: github-actions[bot] <noreply@github.com>
2025-08-26 16:32:09 +10:00
Kaz Wolfe
8cced4c1d7
fix: use channel threadlocal instead of a ThreadStatic 2025-08-25 13:31:05 -07:00
Kaz Wolfe
b18b8b40e5
feat: Add IPC Context
- Demo of the ipc context idea
- Accessible via ipcPub.GetContext()
2025-08-25 13:14:13 -07:00
Blair
005699e472
Allow versionless bans (#2381)
Some checks failed
Tag Build / Tag Build (push) Successful in 12s
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-08-22 13:03:44 +00:00
Lyna
ba9720cb65
Add Platform/OS to plugin feedback (#2379) 2025-08-20 11:06:52 +02:00
bleatbot
1702c3be29
Update ClientStructs (#2375)
Co-authored-by: github-actions[bot] <noreply@github.com>
2025-08-19 21:44:26 +00:00
R
4a743e9060
Collect additional logs for crash handler (#2376)
* Collect launcher.log on crashes

This has been missing for Crash Handler based tspacks for a while, which makes it harder to get information from XLCore users because of our differing log names. (Really, we should rename both to just be xivlauncher.log or something, but that would break existing tooling)

* Collect wine.log on crashes
2025-08-19 14:35:55 -07:00
Kaz Wolfe
0c9176a8b6
feat: Reword message overview
- Removes extra lines from stack trace
- Use clearer-ish wording for messaging
2025-08-19 12:44:34 -07:00
Kaz Wolfe
9e405b26d2
feat: include line numbers/file info in stacktrace 2025-08-19 12:16:34 -07:00
Kaz Wolfe
32cb6e2127
feat: Identify the plugin causing an assertion failure 2025-08-19 11:07:14 -07:00
goaaats
2affbe3683 build: 13.0.0.2 2025-08-17 13:10:21 +02:00
Soreepeong
544f8b28bf Support make clickthrough 2025-08-16 16:42:30 +09:00
bleatbot
e684e7e208
Update ClientStructs (#2371)
Co-authored-by: github-actions[bot] <noreply@github.com>
2025-08-15 16:20:38 +00:00
Haselnussbomber
f687852879
Set ClientState.TerritoryType on load (#2366)
* Set TerritoryType on load

* Do not fire TerritoryChanged event on load
2025-08-15 16:19:21 +00:00
Haselnussbomber
e2f3fdd0ff
ISeStringEvaluator: Add ReadOnlySpan<byte> support (#2370)
* Add EvaluateMacroString ROS<byte> overload

* Add implicit ROS<byte> to SeStringParameter cast
2025-08-15 09:11:25 -07:00
MidoriKami
ef688c09e2
Now with more child labor (#2374) 2025-08-15 15:38:04 +00:00
srkizer
9092e36b33
Reduce usage of exceptions from Boot (#2373)
* wip

* make pretty

* Remove CRT version check from IM

* fix

* Simplify IsDebuggerPresent hook
2025-08-15 00:02:32 -07:00
Soreepeong
e5451c37af Update InputHandler to match changes in imgui_impl_win32.cpp 2025-08-12 16:18:49 +09:00
Soreepeong
40e63f2d9a Enable viewport alpha 2025-08-12 14:10:55 +09:00
bleatbot
f613b177a2
Update ClientStructs (#2369)
Co-authored-by: github-actions[bot] <noreply@github.com>
2025-08-12 01:28:24 +00:00
wolfcomp
6337e165aa
Update Directory.Build.props (#2368) 2025-08-11 18:13:06 -07:00
goat
86ad1de181
Fix codeberg ssh endpoint 2025-08-11 21:58:25 +02:00
goat
98d9bf3a93
Disable strict host key checking 2025-08-11 21:57:06 +02:00
KazWolfe
6341640243
feat: Add GitLab sync (#2312)
* feat: Add GitLab sync

* Add codeberg to sync script

---------

Co-authored-by: goat <16760685+goaaats@users.noreply.github.com>
2025-08-11 21:51:00 +02:00
marzent
ae58aaff30
avoid c++ exceptions in import hook (#2367) 2025-08-11 11:23:15 -07:00
bleatbot
4a62138b30
Update ClientStructs (#2365)
Co-authored-by: github-actions[bot] <noreply@github.com>
2025-08-11 16:15:27 +00:00
bleatbot
8134cde2b7
Update ClientStructs (#2364)
Co-authored-by: github-actions[bot] <noreply@github.com>
2025-08-10 18:31:52 +00:00
Soreepeong
c19ea6ace3 Add ITextureProvider.CreateTextureFromSeString 2025-08-05 11:48:02 +09:00
608 changed files with 16908 additions and 9894 deletions

308
.github/generate_changelog.py vendored Normal file
View file

@ -0,0 +1,308 @@
#!/usr/bin/env python3
"""
Generate a changelog from git commits between the last two tags and post to Discord webhook.
"""
import subprocess
import re
import sys
import json
import argparse
import os
from typing import List, Tuple, Optional, Dict, Any
def run_git_command(args: List[str]) -> str:
"""Run a git command and return its output."""
try:
result = subprocess.run(
["git"] + args,
capture_output=True,
text=True,
check=True
)
return result.stdout.strip()
except subprocess.CalledProcessError as e:
print(f"Git command failed: {e}", file=sys.stderr)
sys.exit(1)
def get_last_two_tags() -> Tuple[str, str]:
"""Get the latest two git tags."""
tags = run_git_command(["tag", "--sort=-version:refname"])
tag_list = [t for t in tags.split("\n") if t]
# Filter out old tags that start with 'v' (old versioning scheme)
tag_list = [t for t in tag_list if not t.startswith('v')]
if len(tag_list) < 2:
print("Error: Need at least 2 tags in the repository", file=sys.stderr)
sys.exit(1)
return tag_list[0], tag_list[1]
def get_submodule_commit(submodule_path: str, tag: str) -> Optional[str]:
"""Get the commit hash of a submodule at a specific tag."""
try:
# Get the submodule commit at the specified tag
result = run_git_command(["ls-tree", tag, submodule_path])
# Format is: "<mode> commit <hash>\t<path>"
parts = result.split()
if len(parts) >= 3 and parts[1] == "commit":
return parts[2]
return None
except:
return None
def get_repo_info() -> Tuple[str, str]:
"""Get repository owner and name from git remote."""
try:
remote_url = run_git_command(["config", "--get", "remote.origin.url"])
# Handle both HTTPS and SSH URLs
# SSH: git@github.com:owner/repo.git
# HTTPS: https://github.com/owner/repo.git
match = re.search(r'github\.com[:/](.+?)/(.+?)(?:\.git)?$', remote_url)
if match:
owner = match.group(1)
repo = match.group(2)
return owner, repo
else:
print("Error: Could not parse GitHub repository from remote URL", file=sys.stderr)
sys.exit(1)
except:
print("Error: Could not get git remote URL", file=sys.stderr)
sys.exit(1)
def get_commits_between_tags(tag1: str, tag2: str) -> List[str]:
"""Get commit SHAs between two tags."""
log_output = run_git_command([
"log",
f"{tag2}..{tag1}",
"--format=%H"
])
commits = [sha.strip() for sha in log_output.split("\n") if sha.strip()]
return commits
def get_pr_for_commit(commit_sha: str, owner: str, repo: str, token: str) -> Optional[Dict[str, Any]]:
"""Get PR information for a commit using GitHub API."""
try:
import requests
except ImportError:
print("Error: requests library is required. Install it with: pip install requests", file=sys.stderr)
sys.exit(1)
headers = {
"Accept": "application/vnd.github+json",
"X-GitHub-Api-Version": "2022-11-28"
}
if token:
headers["Authorization"] = f"Bearer {token}"
url = f"https://api.github.com/repos/{owner}/{repo}/commits/{commit_sha}/pulls"
try:
response = requests.get(url, headers=headers)
response.raise_for_status()
prs = response.json()
if prs and len(prs) > 0:
# Return the first PR (most relevant one)
pr = prs[0]
return {
"number": pr["number"],
"title": pr["title"],
"author": pr["user"]["login"],
"url": pr["html_url"]
}
except requests.exceptions.HTTPError as e:
if e.response.status_code == 404:
# Commit might not be associated with a PR
return None
elif e.response.status_code == 403:
print("Warning: GitHub API rate limit exceeded. Consider providing a token.", file=sys.stderr)
return None
else:
print(f"Warning: Failed to fetch PR for commit {commit_sha[:7]}: {e}", file=sys.stderr)
return None
except Exception as e:
print(f"Warning: Error fetching PR for commit {commit_sha[:7]}: {e}", file=sys.stderr)
return None
return None
def get_prs_between_tags(tag1: str, tag2: str, owner: str, repo: str, token: str) -> List[Dict[str, Any]]:
"""Get PRs between two tags using GitHub API."""
commits = get_commits_between_tags(tag1, tag2)
print(f"Found {len(commits)} commits, fetching PR information...")
prs = []
seen_pr_numbers = set()
for i, commit_sha in enumerate(commits, 1):
if i % 10 == 0:
print(f"Progress: {i}/{len(commits)} commits processed...")
pr_info = get_pr_for_commit(commit_sha, owner, repo, token)
if pr_info and pr_info["number"] not in seen_pr_numbers:
seen_pr_numbers.add(pr_info["number"])
prs.append(pr_info)
return prs
def filter_prs(prs: List[Dict[str, Any]], ignore_patterns: List[str]) -> List[Dict[str, Any]]:
"""Filter out PRs matching any of the ignore patterns."""
compiled_patterns = [re.compile(pattern) for pattern in ignore_patterns]
filtered = []
for pr in prs:
if not any(pattern.search(pr["title"]) for pattern in compiled_patterns):
filtered.append(pr)
return filtered
def generate_changelog(version: str, prev_version: str, prs: List[Dict[str, Any]],
cs_commit_new: Optional[str], cs_commit_old: Optional[str],
owner: str, repo: str) -> str:
"""Generate markdown changelog."""
# Calculate statistics
pr_count = len(prs)
unique_authors = len(set(pr["author"] for pr in prs))
changelog = f"# Dalamud Release v{version}\n\n"
changelog += f"We just released Dalamud v{version}, which should be available to users within a few minutes. "
changelog += f"This release includes **{pr_count} PR{'s' if pr_count != 1 else ''} from {unique_authors} contributor{'s' if unique_authors != 1 else ''}**.\n"
changelog += f"[Click here](<https://github.com/{owner}/{repo}/compare/{prev_version}...{version}>) to see all Dalamud changes.\n\n"
if cs_commit_new and cs_commit_old and cs_commit_new != cs_commit_old:
changelog += f"It ships with an updated **FFXIVClientStructs [`{cs_commit_new[:7]}`](<https://github.com/aers/FFXIVClientStructs/commit/{cs_commit_new}>)**.\n"
changelog += f"[Click here](<https://github.com/aers/FFXIVClientStructs/compare/{cs_commit_old}...{cs_commit_new}>) to see all CS changes.\n"
elif cs_commit_new:
changelog += f"It ships with **FFXIVClientStructs [`{cs_commit_new[:7]}`](<https://github.com/aers/FFXIVClientStructs/commit/{cs_commit_new}>)**.\n"
changelog += "## Dalamud Changes\n\n"
for pr in prs:
changelog += f"* {pr['title']} ([#**{pr['number']}**](<{pr['url']}>) by **{pr['author']}**)\n"
return changelog
def post_to_discord(webhook_url: str, content: str, version: str) -> None:
"""Post changelog to Discord webhook as a file attachment."""
try:
import requests
except ImportError:
print("Error: requests library is required. Install it with: pip install requests", file=sys.stderr)
sys.exit(1)
filename = f"changelog-v{version}.md"
# Prepare the payload
data = {
"content": f"Dalamud v{version} has been released!",
"attachments": [
{
"id": "0",
"filename": filename
}
]
}
# Prepare the files
files = {
"payload_json": (None, json.dumps(data)),
"files[0]": (filename, content.encode('utf-8'), 'text/markdown')
}
try:
result = requests.post(webhook_url, files=files)
result.raise_for_status()
print(f"Successfully posted to Discord webhook, code {result.status_code}")
except requests.exceptions.HTTPError as err:
print(f"Failed to post to Discord: {err}", file=sys.stderr)
sys.exit(1)
except Exception as e:
print(f"Failed to post to Discord: {e}", file=sys.stderr)
sys.exit(1)
def main():
parser = argparse.ArgumentParser(
description="Generate changelog from git commits and post to Discord webhook"
)
parser.add_argument(
"--webhook-url",
required=True,
help="Discord webhook URL"
)
parser.add_argument(
"--github-token",
default=os.environ.get("GITHUB_TOKEN"),
help="GitHub API token (or set GITHUB_TOKEN env var). Increases rate limit."
)
parser.add_argument(
"--ignore",
action="append",
default=[],
help="Regex patterns to ignore PRs (can be specified multiple times)"
)
parser.add_argument(
"--submodule-path",
default="lib/FFXIVClientStructs",
help="Path to the FFXIVClientStructs submodule (default: lib/FFXIVClientStructs)"
)
args = parser.parse_args()
# Get repository info
owner, repo = get_repo_info()
print(f"Repository: {owner}/{repo}")
# Get the last two tags
latest_tag, previous_tag = get_last_two_tags()
print(f"Generating changelog between {previous_tag} and {latest_tag}")
# Get submodule commits at both tags
cs_commit_new = get_submodule_commit(args.submodule_path, latest_tag)
cs_commit_old = get_submodule_commit(args.submodule_path, previous_tag)
if cs_commit_new:
print(f"FFXIVClientStructs commit (new): {cs_commit_new[:7]}")
if cs_commit_old:
print(f"FFXIVClientStructs commit (old): {cs_commit_old[:7]}")
# Get PRs between tags
prs = get_prs_between_tags(latest_tag, previous_tag, owner, repo, args.github_token)
prs.reverse()
print(f"Found {len(prs)} PRs")
# Filter PRs
filtered_prs = filter_prs(prs, args.ignore)
print(f"After filtering: {len(filtered_prs)} PRs")
# Generate changelog
changelog = generate_changelog(latest_tag, previous_tag, filtered_prs,
cs_commit_new, cs_commit_old, owner, repo)
print("\n" + "="*50)
print("Generated Changelog:")
print("="*50)
print(changelog)
print("="*50 + "\n")
# Post to Discord
post_to_discord(args.webhook_url, changelog, latest_tag)
if __name__ == "__main__":
main()

32
.github/workflows/backup.yml vendored Normal file
View file

@ -0,0 +1,32 @@
name: Back up code to other forges
on:
schedule:
- cron: '0 2 * * *' # Run every day at 2 AM
workflow_dispatch: # Allow manual trigger
jobs:
push-to-forges:
runs-on: ubuntu-latest
if: github.repository == 'goatcorp/Dalamud'
steps:
- name: Checkout the repository
uses: actions/checkout@v4
with:
fetch-depth: 0
- uses: webfactory/ssh-agent@a6f90b1f127823b31d4d4a8d96047790581349bd #v0.9.1
with:
ssh-private-key: |
${{ secrets.MIRROR_GITLAB_SYNC_KEY }}
${{ secrets.MIRROR_CODEBERG_SYNC_KEY }}
- name: Add remotes & push
env:
GIT_SSH_COMMAND: "ssh -o StrictHostKeyChecking=accept-new"
run: |
git remote add gitlab git@gitlab.com:goatcorp/Dalamud.git
git push gitlab --all --force
git remote add codeberg git@codeberg.org:goatcorp/Dalamud.git
git push codeberg --all --force

View file

@ -0,0 +1,48 @@
name: Generate Changelog
on:
workflow_dispatch:
push:
tags:
- '*'
permissions: read-all
jobs:
generate-changelog:
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v4
with:
fetch-depth: 0 # Fetch all history and tags
submodules: true # Fetch submodules
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: '3.14'
- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip install requests
- name: Generate and post changelog
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
GIT_TERMINAL_PROMPT: 0
run: |
python .github/generate_changelog.py \
--webhook-url "${{ secrets.DISCORD_CHANGELOG_WEBHOOK_URL }}" \
--ignore "Update ClientStructs" \
--ignore "^build:"
- name: Upload changelog as artifact
if: always()
uses: actions/upload-artifact@v4
with:
name: changelog
path: changelog-*.md
if-no-files-found: ignore

View file

@ -3,7 +3,7 @@ on: [push, pull_request, workflow_dispatch]
concurrency:
group: build_dalamud_${{ github.ref_name }}
cancel-in-progress: true
cancel-in-progress: false
jobs:
build:
@ -23,7 +23,7 @@ jobs:
uses: microsoft/setup-msbuild@v1.0.2
- uses: actions/setup-dotnet@v3
with:
dotnet-version: '9.0.200'
dotnet-version: '10.0.100'
- name: Define VERSION
run: |
$env:COMMIT = $env:GITHUB_SHA.Substring(0, 7)

View file

@ -11,7 +11,7 @@ jobs:
strategy:
matrix:
branches:
- net9
- api14
defaults:
run:

View file

@ -1,16 +1,26 @@
name: Check for FFXIVCS changes
name: Check for Submodule Changes
on:
schedule:
- cron: "0 0,12,18 */1 * *"
- cron: "0 0,6,12,18 * * *"
workflow_dispatch:
jobs:
check:
name: FFXIVCS Check
name: Check ${{ matrix.submodule.name }}
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
branches: [master]
submodule:
- name: ClientStructs
path: lib/FFXIVClientStructs
branch: main
branch-prefix: csupdate
- name: Excel Schema
path: lib/Lumina.Excel
branch: master
branch-prefix: schemaupdate
defaults:
run:
@ -24,30 +34,41 @@ jobs:
ref: ${{ matrix.branches }}
token: ${{ secrets.UPDATE_PAT }}
- name: Create update branch
run: git checkout -b csupdate/${{ matrix.branches }}
run: git checkout -b ${{ matrix.submodule.branch-prefix }}/${{ matrix.branches }}
- name: Initialize mandatory git config
run: |
git config --global user.name "github-actions[bot]"
git config --global user.email noreply@github.com
git config --global pull.rebase false
- name: Update submodule
id: update-submodule
run: |
git checkout -b csupdate-${{ matrix.branches }}
git checkout -b ${{ matrix.submodule.branch-prefix }}-${{ matrix.branches }}
git reset --hard origin/${{ matrix.branches }}
cd lib/FFXIVClientStructs
cd ${{ matrix.submodule.path }}
git fetch
git reset --hard origin/main
git reset --hard origin/${{ matrix.submodule.branch }}
cd ../..
git add lib/FFXIVClientStructs
git commit --message "Update ClientStructs"
git push origin csupdate-${{ matrix.branches }} --force
git add ${{ matrix.submodule.path }}
if [[ -z "$(git status --porcelain --untracked-files=no)" ]]; then
echo "No changes detected!"
echo "SUBMIT_PR=false" >> "$GITHUB_OUTPUT"
exit 0
fi
git commit --message "Update ${{ matrix.submodule.name }}"
git push origin ${{ matrix.submodule.branch-prefix }}-${{ matrix.branches }} --force
echo "SUBMIT_PR=true" >> "$GITHUB_OUTPUT"
- name: Create PR
if: ${{ steps.update-submodule.outputs.SUBMIT_PR == 'true' }}
run: |
echo ${{ secrets.UPDATE_PAT }} | gh auth login --with-token
prNumber=$(gh pr list --base ${{ matrix.branches }} --head csupdate-${{ matrix.branches }} --state open --json number --template "{{range .}}{{.number}}{{end}}")
prNumber=$(gh pr list --base ${{ matrix.branches }} --head ${{ matrix.submodule.branch-prefix }}-${{ matrix.branches }} --state open --json number --template "{{range .}}{{.number}}{{end}}")
if [ -z "$prNumber" ]; then
echo "No PR found, creating one"
gh pr create --head csupdate-${{ matrix.branches }} --title "[${{ matrix.branches }}] Update ClientStructs" --body "" --base ${{ matrix.branches }}
gh pr create --head ${{ matrix.submodule.branch-prefix }}-${{ matrix.branches }} --title "[${{ matrix.branches }}] Update ${{ matrix.submodule.name }}" --body "" --base ${{ matrix.branches }}
else
echo "PR already exists, ignoring"
fi

View file

@ -1,19 +1,57 @@
{
"$schema": "http://json-schema.org/draft-04/schema#",
"title": "Build Schema",
"$ref": "#/definitions/build",
"definitions": {
"build": {
"type": "object",
"Host": {
"type": "string",
"enum": [
"AppVeyor",
"AzurePipelines",
"Bamboo",
"Bitbucket",
"Bitrise",
"GitHubActions",
"GitLab",
"Jenkins",
"Rider",
"SpaceAutomation",
"TeamCity",
"Terminal",
"TravisCI",
"VisualStudio",
"VSCode"
]
},
"ExecutableTarget": {
"type": "string",
"enum": [
"CI",
"Clean",
"Compile",
"CompileCImGui",
"CompileCImGuizmo",
"CompileCImPlot",
"CompileDalamud",
"CompileDalamudBoot",
"CompileDalamudCrashHandler",
"CompileImGuiNatives",
"CompileInjector",
"Restore",
"SetCILogging",
"Test"
]
},
"Verbosity": {
"type": "string",
"description": "",
"enum": [
"Verbose",
"Normal",
"Minimal",
"Quiet"
]
},
"NukeBuild": {
"properties": {
"Configuration": {
"type": "string",
"description": "Configuration to build - Default is 'Debug' (local) or 'Release' (server)",
"enum": [
"Debug",
"Release"
]
},
"Continue": {
"type": "boolean",
"description": "Indicates to continue a previously failed build attempt"
@ -23,29 +61,8 @@
"description": "Shows the help text for this build assembly"
},
"Host": {
"type": "string",
"description": "Host for execution. Default is 'automatic'",
"enum": [
"AppVeyor",
"AzurePipelines",
"Bamboo",
"Bitbucket",
"Bitrise",
"GitHubActions",
"GitLab",
"Jenkins",
"Rider",
"SpaceAutomation",
"TeamCity",
"Terminal",
"TravisCI",
"VisualStudio",
"VSCode"
]
},
"IsDocsBuild": {
"type": "boolean",
"description": "Whether we are building for documentation - emits generated files"
"$ref": "#/definitions/Host"
},
"NoLogo": {
"type": "boolean",
@ -74,65 +91,46 @@
"type": "array",
"description": "List of targets to be skipped. Empty list skips all dependencies",
"items": {
"type": "string",
"enum": [
"CI",
"Clean",
"Compile",
"CompileCImGui",
"CompileCImGuizmo",
"CompileCImPlot",
"CompileDalamud",
"CompileDalamudBoot",
"CompileDalamudCrashHandler",
"CompileImGuiNatives",
"CompileInjector",
"CompileInjectorBoot",
"Restore",
"SetCILogging",
"Test"
]
"$ref": "#/definitions/ExecutableTarget"
}
},
"Solution": {
"type": "string",
"description": "Path to a solution file that is automatically loaded"
},
"Target": {
"type": "array",
"description": "List of targets to be invoked. Default is '{default_target}'",
"items": {
"type": "string",
"enum": [
"CI",
"Clean",
"Compile",
"CompileCImGui",
"CompileCImGuizmo",
"CompileCImPlot",
"CompileDalamud",
"CompileDalamudBoot",
"CompileDalamudCrashHandler",
"CompileImGuiNatives",
"CompileInjector",
"CompileInjectorBoot",
"Restore",
"SetCILogging",
"Test"
]
"$ref": "#/definitions/ExecutableTarget"
}
},
"Verbosity": {
"type": "string",
"description": "Logging verbosity during build execution. Default is 'Normal'",
"enum": [
"Minimal",
"Normal",
"Quiet",
"Verbose"
]
"$ref": "#/definitions/Verbosity"
}
}
}
}
}
},
"allOf": [
{
"properties": {
"Configuration": {
"type": "string",
"description": "Configuration to build - Default is 'Debug' (local) or 'Release' (server)",
"enum": [
"Debug",
"Release"
]
},
"IsDocsBuild": {
"type": "boolean",
"description": "Whether we are building for documentation - emits generated files"
},
"Solution": {
"type": "string",
"description": "Path to a solution file that is automatically loaded"
}
}
},
{
"$ref": "#/definitions/NukeBuild"
}
]
}

View file

@ -26,6 +26,38 @@ LANGUAGE LANG_ENGLISH, SUBLANG_ENGLISH_US
RT_MANIFEST_THEMES RT_MANIFEST "themes.manifest"
/////////////////////////////////////////////////////////////////////////////
//
// String Table
//
STRINGTABLE
BEGIN
IDS_APPNAME "Dalamud Boot"
IDS_MSVCRT_ACTION_OPENDOWNLOAD
"Download Microsoft Visual C++ Redistributable 2022\nExit the game and download the latest setup file from Microsoft."
IDS_MSVCRT_ACTION_IGNORE
"Ignore and Continue\nAttempt to continue with the currently installed version.\nDalamud or plugins may fail to load."
IDS_MSVCRT_DIALOG_MAININSTRUCTION
"Outdated Microsoft Visual C++ Redistributable"
IDS_MSVCRT_DIALOG_CONTENT
"The Microsoft Visual C++ Redistributable version detected on this computer (v{0}.{1}.{2}.{3}) is out of date and may not work with Dalamud."
IDS_MSVCRT_DOWNLOADURL "https://aka.ms/vs/17/release/vc_redist.x64.exe"
IDS_INITIALIZEFAIL_ACTION_ABORT "Abort\nExit the game."
IDS_INITIALIZEFAIL_ACTION_CONTINUE
"Load game without Dalamud\nThe game will launch without Dalamud enabled."
IDS_INITIALIZEFAIL_DIALOG_MAININSTRUCTION "Failed to load Dalamud."
IDS_INITIALIZEFAIL_DIALOG_CONTENT
"An error is preventing Dalamud from being loaded along with the game."
END
STRINGTABLE
BEGIN
IDS_INITIALIZEFAIL_DIALOG_FOOTER
"Last operation: {0}\nHRESULT: 0x{1:08X}\nDescription: {2}"
END
#endif // English (United States) resources
/////////////////////////////////////////////////////////////////////////////

View file

@ -48,7 +48,7 @@
<WarningLevel>Level3</WarningLevel>
<SDLCheck>true</SDLCheck>
<ConformanceMode>true</ConformanceMode>
<LanguageStandard>stdcpp20</LanguageStandard>
<LanguageStandard>stdcpp23</LanguageStandard>
<PrecompiledHeaderFile>pch.h</PrecompiledHeaderFile>
<DebugInformationFormat>ProgramDatabase</DebugInformationFormat>
<PreprocessorDefinitions>CPPDLLTEMPLATE_EXPORTS;_WINDOWS;_USRDLL;%(PreprocessorDefinitions)</PreprocessorDefinitions>
@ -65,7 +65,7 @@
<ClCompile>
<FunctionLevelLinking>true</FunctionLevelLinking>
<IntrinsicFunctions>false</IntrinsicFunctions>
<RuntimeLibrary>MultiThreadedDebugDLL</RuntimeLibrary>
<RuntimeLibrary>MultiThreadedDebugDLL</RuntimeLibrary>
<PreprocessorDefinitions>_DEBUG;%(PreprocessorDefinitions)</PreprocessorDefinitions>
<PrecompiledHeader Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">Use</PrecompiledHeader>
<DisableSpecificWarnings Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">26812</DisableSpecificWarnings>
@ -80,7 +80,7 @@
<ClCompile>
<FunctionLevelLinking>true</FunctionLevelLinking>
<IntrinsicFunctions>true</IntrinsicFunctions>
<RuntimeLibrary>MultiThreadedDLL</RuntimeLibrary>
<RuntimeLibrary>MultiThreadedDLL</RuntimeLibrary>
<PreprocessorDefinitions>NDEBUG;%(PreprocessorDefinitions)</PreprocessorDefinitions>
<PrecompiledHeader Condition="'$(Configuration)|$(Platform)'=='Release|x64'">Use</PrecompiledHeader>
<DisableSpecificWarnings Condition="'$(Configuration)|$(Platform)'=='Release|x64'">26812</DisableSpecificWarnings>
@ -133,6 +133,10 @@
<PrecompiledHeader Condition="'$(Configuration)|$(Platform)'=='Release|x64'">NotUsing</PrecompiledHeader>
</ClCompile>
<ClCompile Include="DalamudStartInfo.cpp" />
<ClCompile Include="error_info.cpp">
<PrecompiledHeader Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">NotUsing</PrecompiledHeader>
<PrecompiledHeader Condition="'$(Configuration)|$(Platform)'=='Release|x64'">NotUsing</PrecompiledHeader>
</ClCompile>
<ClCompile Include="hooks.cpp" />
<ClCompile Include="logging.cpp">
<PrecompiledHeader Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">NotUsing</PrecompiledHeader>
@ -176,6 +180,7 @@
<ClInclude Include="..\lib\TsudaKageyu-minhook\src\trampoline.h" />
<ClInclude Include="crashhandler_shared.h" />
<ClInclude Include="DalamudStartInfo.h" />
<ClInclude Include="error_info.h" />
<ClInclude Include="hooks.h" />
<ClInclude Include="logging.h" />
<ClInclude Include="ntdll.h" />
@ -206,4 +211,4 @@
<Copy SourceFiles="$(OutDir)$(TargetName).pdb" DestinationFolder="..\bin\$(Configuration)\" />
<Copy SourceFiles="$(OutDir)nethost.dll" DestinationFolder="..\bin\$(Configuration)\" />
</Target>
</Project>
</Project>

View file

@ -76,6 +76,9 @@
<ClCompile Include="ntdll.cpp">
<Filter>Dalamud.Boot DLL</Filter>
</ClCompile>
<ClCompile Include="error_info.cpp">
<Filter>Common Boot</Filter>
</ClCompile>
</ItemGroup>
<ItemGroup>
<ClInclude Include="..\lib\CoreCLR\CoreCLR.h">
@ -146,6 +149,9 @@
<ClInclude Include="ntdll.h">
<Filter>Dalamud.Boot DLL</Filter>
</ClInclude>
<ClInclude Include="error_info.h">
<Filter>Common Boot</Filter>
</ClInclude>
</ItemGroup>
<ItemGroup>
<ResourceCompile Include="Dalamud.Boot.rc" />

View file

@ -108,6 +108,11 @@ void from_json(const nlohmann::json& json, DalamudStartInfo& config) {
config.LogName = json.value("LogName", config.LogName);
config.PluginDirectory = json.value("PluginDirectory", config.PluginDirectory);
config.AssetDirectory = json.value("AssetDirectory", config.AssetDirectory);
if (json.contains("TempDirectory") && !json["TempDirectory"].is_null()) {
config.TempDirectory = json.value("TempDirectory", config.TempDirectory);
}
config.Language = json.value("Language", config.Language);
config.Platform = json.value("Platform", config.Platform);
config.GameVersion = json.value("GameVersion", config.GameVersion);

View file

@ -44,6 +44,7 @@ struct DalamudStartInfo {
std::string ConfigurationPath;
std::string LogPath;
std::string LogName;
std::string TempDirectory;
std::string PluginDirectory;
std::string AssetDirectory;
ClientLanguage Language = ClientLanguage::English;

View file

@ -6,6 +6,8 @@
#define WIN32_LEAN_AND_MEAN
#include <Windows.h>
#define CUSTOM_EXCEPTION_EXTERNAL_EVENT 0x12345679
struct exception_info
{
LPEXCEPTION_POINTERS pExceptionPointers;

View file

@ -9,11 +9,12 @@
#include "utils.h"
#include "veh.h"
#include "xivfixes.h"
#include "resource.h"
HMODULE g_hModule;
HINSTANCE g_hGameInstance = GetModuleHandleW(nullptr);
void CheckMsvcrtVersion() {
static void CheckMsvcrtVersion() {
// Commit introducing inline mutex ctor: tagged vs-2022-17.14 (2024-06-18)
// - https://github.com/microsoft/STL/commit/22a88260db4d754bbc067e2002430144d6ec5391
// MSVC Redist versions:
@ -28,67 +29,102 @@ void CheckMsvcrtVersion() {
| (static_cast<uint64_t>(RequiredMsvcrtVersionComponents[2]) << 16)
| (static_cast<uint64_t>(RequiredMsvcrtVersionComponents[3]) << 0);
#ifdef _DEBUG
constexpr const wchar_t* RuntimeDllNames[] = {
#ifdef _DEBUG
L"msvcp140d.dll",
L"vcruntime140d.dll",
L"vcruntime140_1d.dll",
};
#else
constexpr const wchar_t* RuntimeDllNames[] = {
L"msvcp140.dll",
L"vcruntime140.dll",
L"vcruntime140_1.dll",
};
#endif
};
uint64_t lowestVersion = 0;
for (const auto& runtimeDllName : RuntimeDllNames) {
const utils::loaded_module mod(GetModuleHandleW(runtimeDllName));
if (!mod) {
logging::E("Runtime DLL not found: {}", runtimeDllName);
logging::E("MSVCRT DLL not found: {}", runtimeDllName);
continue;
}
try {
const auto& versionFull = mod.get_file_version();
logging::I("Runtime DLL {} has version {}.", runtimeDllName, utils::format_file_version(versionFull));
const auto path = mod.path()
.transform([](const auto& p) { return p.wstring(); })
.value_or(runtimeDllName);
const auto version = (static_cast<uint64_t>(versionFull.dwFileVersionMS) << 32) |
static_cast<uint64_t>(versionFull.dwFileVersionLS);
if (const auto versionResult = mod.get_file_version()) {
const auto& versionFull = versionResult->get();
logging::I("MSVCRT DLL {} has version {}.", path, utils::format_file_version(versionFull));
const auto version = 0ULL |
(static_cast<uint64_t>(versionFull.dwFileVersionMS) << 32) |
(static_cast<uint64_t>(versionFull.dwFileVersionLS) << 0);
if (version < RequiredMsvcrtVersion && (lowestVersion == 0 || lowestVersion > version))
lowestVersion = version;
} catch (const std::exception& e) {
logging::E("Failed to detect Runtime DLL version for {}: {}", runtimeDllName, e.what());
} else {
logging::E("Failed to detect MSVCRT DLL version for {}: {}", path, versionResult.error().describe());
}
}
if (lowestVersion) {
switch (MessageBoxW(
nullptr,
L"Microsoft Visual C++ Redistributable should be updated, or Dalamud may not work as expected."
L" Do you want to download and install the latest version from Microsoft?"
L"\n"
L"\n* Clicking \"Yes\" will exit the game and open the download page from Microsoft."
L"\n* Clicking \"No\" will continue loading the game with Dalamud. This may fail."
L"\n"
L"\nClick \"X64\" from the table in the download page, regardless of what CPU you have.",
L"Dalamud",
MB_YESNO | MB_ICONWARNING)) {
case IDYES:
ShellExecuteW(
nullptr,
L"open",
L"https://learn.microsoft.com/en-us/cpp/windows/latest-supported-vc-redist?view=msvc-170#latest-microsoft-visual-c-redistributable-version",
nullptr,
nullptr,
SW_SHOW);
ExitProcess(0);
break;
case IDNO:
break;
}
if (!lowestVersion)
return;
enum IdTaskDialogAction {
IdTaskDialogActionOpenDownload = 101,
IdTaskDialogActionIgnore,
};
const TASKDIALOG_BUTTON buttons[]{
{IdTaskDialogActionOpenDownload, MAKEINTRESOURCEW(IDS_MSVCRT_ACTION_OPENDOWNLOAD)},
{IdTaskDialogActionIgnore, MAKEINTRESOURCEW(IDS_MSVCRT_ACTION_IGNORE)},
};
const WORD lowestVersionComponents[]{
static_cast<WORD>(lowestVersion >> 48),
static_cast<WORD>(lowestVersion >> 32),
static_cast<WORD>(lowestVersion >> 16),
static_cast<WORD>(lowestVersion >> 0),
};
const auto dialogContent = std::vformat(
utils::get_string_resource(IDS_MSVCRT_DIALOG_CONTENT),
std::make_wformat_args(
lowestVersionComponents[0],
lowestVersionComponents[1],
lowestVersionComponents[2],
lowestVersionComponents[3]));
const TASKDIALOGCONFIG config{
.cbSize = sizeof config,
.hInstance = g_hModule,
.dwFlags = TDF_CAN_BE_MINIMIZED | TDF_ALLOW_DIALOG_CANCELLATION | TDF_USE_COMMAND_LINKS,
.pszWindowTitle = MAKEINTRESOURCEW(IDS_APPNAME),
.pszMainIcon = MAKEINTRESOURCEW(IDI_ICON1),
.pszMainInstruction = MAKEINTRESOURCEW(IDS_MSVCRT_DIALOG_MAININSTRUCTION),
.pszContent = dialogContent.c_str(),
.cButtons = _countof(buttons),
.pButtons = buttons,
.nDefaultButton = IdTaskDialogActionOpenDownload,
};
int buttonPressed;
if (utils::scoped_dpi_awareness_context ctx;
FAILED(TaskDialogIndirect(&config, &buttonPressed, nullptr, nullptr)))
buttonPressed = IdTaskDialogActionOpenDownload;
switch (buttonPressed) {
case IdTaskDialogActionOpenDownload:
ShellExecuteW(
nullptr,
L"open",
utils::get_string_resource(IDS_MSVCRT_DOWNLOADURL).c_str(),
nullptr,
nullptr,
SW_SHOW);
ExitProcess(0);
break;
}
}
@ -103,7 +139,7 @@ HRESULT WINAPI InitializeImpl(LPVOID lpParam, HANDLE hMainThreadContinue) {
}
if (g_startInfo.BootShowConsole)
ConsoleSetup(L"Dalamud Boot");
ConsoleSetup(utils::get_string_resource(IDS_APPNAME).c_str());
logging::update_dll_load_status(true);
@ -240,7 +276,7 @@ HRESULT WINAPI InitializeImpl(LPVOID lpParam, HANDLE hMainThreadContinue) {
if (minHookLoaded) {
logging::I("Applying fixes...");
xivfixes::apply_all(true);
std::thread([] { xivfixes::apply_all(true); }).join();
logging::I("Fixes OK");
} else {
logging::W("Skipping fixes, as MinHook has failed to load.");
@ -251,11 +287,14 @@ HRESULT WINAPI InitializeImpl(LPVOID lpParam, HANDLE hMainThreadContinue) {
while (!IsDebuggerPresent())
Sleep(100);
logging::I("Debugger attached.");
__debugbreak();
}
const auto fs_module_path = utils::get_module_path(g_hModule);
const auto runtimeconfig_path = std::filesystem::path(fs_module_path).replace_filename(L"Dalamud.runtimeconfig.json").wstring();
const auto module_path = std::filesystem::path(fs_module_path).replace_filename(L"Dalamud.dll").wstring();
const auto fs_module_path = utils::loaded_module(g_hModule).path();
if (!fs_module_path)
return fs_module_path.error();
const auto runtimeconfig_path = std::filesystem::path(*fs_module_path).replace_filename(L"Dalamud.runtimeconfig.json").wstring();
const auto module_path = std::filesystem::path(*fs_module_path).replace_filename(L"Dalamud.dll").wstring();
// ============================== CLR ========================================= //
@ -292,6 +331,51 @@ HRESULT WINAPI InitializeImpl(LPVOID lpParam, HANDLE hMainThreadContinue) {
logging::I("VEH was disabled manually");
}
// ============================== CLR Reporting =================================== //
// This is pretty horrible - CLR just doesn't provide a way for us to handle these events, and the API for it
// was pushed back to .NET 11, so we have to hook ReportEventW and catch them ourselves for now.
// Ideally all of this will go away once they get to it.
static std::shared_ptr<hooks::global_import_hook<decltype(ReportEventW)>> s_report_event_hook;
s_report_event_hook = std::make_shared<hooks::global_import_hook<decltype(ReportEventW)>>(
"advapi32.dll!ReportEventW (global import, hook_clr_report_event)", L"advapi32.dll", "ReportEventW");
s_report_event_hook->set_detour([hook = s_report_event_hook.get()](
HANDLE hEventLog,
WORD wType,
WORD wCategory,
DWORD dwEventID,
PSID lpUserSid,
WORD wNumStrings,
DWORD dwDataSize,
LPCWSTR* lpStrings,
LPVOID lpRawData)-> BOOL {
// Check for CLR Error Event IDs
// https://github.com/dotnet/runtime/blob/v10.0.0/src/coreclr/vm/eventreporter.cpp#L370
if (dwEventID != 1026 && // ERT_UnhandledException: The process was terminated due to an unhandled exception
dwEventID != 1025 && // ERT_ManagedFailFast: The application requested process termination through System.Environment.FailFast
dwEventID != 1023 && // ERT_UnmanagedFailFast: The process was terminated due to an internal error in the .NET Runtime
dwEventID != 1027 && // ERT_StackOverflow: The process was terminated due to a stack overflow
dwEventID != 1028) // ERT_CodeContractFailed: The application encountered a bug. A managed code contract (precondition, postcondition, object invariant, or assert) failed
{
return hook->call_original(hEventLog, wType, wCategory, dwEventID, lpUserSid, wNumStrings, dwDataSize, lpStrings, lpRawData);
}
if (wNumStrings == 0 || lpStrings == nullptr) {
logging::W("ReportEventW called with no strings.");
return hook->call_original(hEventLog, wType, wCategory, dwEventID, lpUserSid, wNumStrings, dwDataSize, lpStrings, lpRawData);
}
// In most cases, DalamudCrashHandler will kill us now, so call original here to make sure we still write to the event log.
const BOOL original_ret = hook->call_original(hEventLog, wType, wCategory, dwEventID, lpUserSid, wNumStrings, dwDataSize, lpStrings, lpRawData);
const std::wstring error_details(lpStrings[0]);
veh::raise_external_event(error_details);
return original_ret;
});
logging::I("ReportEventW hook installed.");
// ============================== Dalamud ==================================== //
if (static_cast<int>(g_startInfo.BootWaitMessageBox) & static_cast<int>(DalamudStartInfo::WaitMessageboxFlags::BeforeDalamudEntrypoint))

View file

@ -0,0 +1,26 @@
#include "error_info.h"
#define WIN32_LEAN_AND_MEAN
#include <Windows.h>
DalamudBootError::DalamudBootError(DalamudBootErrorDescription dalamudErrorDescription, long hresult) noexcept
: m_dalamudErrorDescription(dalamudErrorDescription)
, m_hresult(hresult) {
}
DalamudBootError::DalamudBootError(DalamudBootErrorDescription dalamudErrorDescription) noexcept
: DalamudBootError(dalamudErrorDescription, E_FAIL) {
}
const char* DalamudBootError::describe() const {
switch (m_dalamudErrorDescription) {
case DalamudBootErrorDescription::ModuleResourceLoadFail:
return "Failed to load resource.";
case DalamudBootErrorDescription::ModuleResourceVersionReadFail:
return "Failed to query version information.";
case DalamudBootErrorDescription::ModuleResourceVersionSignatureFail:
return "Invalid version info found.";
default:
return "(unavailable)";
}
}

42
Dalamud.Boot/error_info.h Normal file
View file

@ -0,0 +1,42 @@
#pragma once
#include <expected>
#include <string>
typedef unsigned long DWORD;
typedef _Return_type_success_(return >= 0) long HRESULT;
enum class DalamudBootErrorDescription {
None,
ModulePathResolutionFail,
ModuleResourceLoadFail,
ModuleResourceVersionReadFail,
ModuleResourceVersionSignatureFail,
};
class DalamudBootError {
DalamudBootErrorDescription m_dalamudErrorDescription;
long m_hresult;
public:
DalamudBootError(DalamudBootErrorDescription dalamudErrorDescription, long hresult) noexcept;
DalamudBootError(DalamudBootErrorDescription dalamudErrorDescription) noexcept;
const char* describe() const;
operator HRESULT() const {
return m_hresult;
}
};
template<typename T>
using DalamudExpected = std::expected<
std::conditional_t<
std::is_reference_v<T>,
std::reference_wrapper<std::remove_reference_t<T>>,
T
>,
DalamudBootError
>;
using DalamudUnexpected = std::unexpected<DalamudBootError>;

View file

@ -84,19 +84,13 @@ void hooks::getprocaddress_singleton_import_hook::initialize() {
const auto dllName = unicode::convert<std::string>(pData->Loaded.FullDllName->Buffer);
utils::loaded_module mod(pData->Loaded.DllBase);
std::wstring version, description;
try {
version = utils::format_file_version(mod.get_file_version());
} catch (...) {
version = L"<unknown>";
}
try {
description = mod.get_description();
} catch (...) {
description = L"<unknown>";
}
const auto version = mod.get_file_version()
.transform([](const auto& v) { return utils::format_file_version(v.get()); })
.value_or(L"<unknown>");
const auto description = mod.get_description()
.value_or(L"<unknown>");
logging::I(R"({} "{}" ("{}" ver {}) has been loaded at 0x{:X} ~ 0x{:X} (0x{:X}); finding import table items to hook.)",
LogTag, dllName, description, version,
reinterpret_cast<size_t>(pData->Loaded.DllBase),
@ -125,7 +119,9 @@ void hooks::getprocaddress_singleton_import_hook::hook_module(const utils::loade
if (mod.is_current_process())
return;
const auto path = unicode::convert<std::string>(mod.path().wstring());
const auto path = mod.path()
.transform([](const auto& p) { return unicode::convert<std::string>(p.wstring()); })
.value_or("<unknown>");
for (const auto& [hModule, targetFns] : m_targetFns) {
for (const auto& [targetFn, pfnThunk] : targetFns) {
@ -133,7 +129,7 @@ void hooks::getprocaddress_singleton_import_hook::hook_module(const utils::loade
if (void* pGetProcAddressImport; mod.find_imported_function_pointer(dllName.c_str(), targetFn.c_str(), 0, pGetProcAddressImport)) {
auto& hook = m_hooks[hModule][targetFn][mod];
if (!hook) {
logging::I("{} Hooking {}!{} imported by {}", LogTag, dllName, targetFn, unicode::convert<std::string>(mod.path().wstring()));
logging::I("{} Hooking {}!{} imported by {}", LogTag, dllName, targetFn, path);
hook.emplace(std::format("getprocaddress_singleton_import_hook::hook_module({}!{})", dllName, targetFn), static_cast<void**>(pGetProcAddressImport), pfnThunk);
}

View file

@ -55,6 +55,7 @@
#include <set>
#include <span>
#include <string>
#include <string_view>
#include <type_traits>
// https://www.akenotsuki.com/misc/srell/en/

View file

@ -3,12 +3,23 @@
// Used by Dalamud.Boot.rc
//
#define IDI_ICON1 101
#define IDS_APPNAME 102
#define IDS_MSVCRT_ACTION_OPENDOWNLOAD 103
#define IDS_MSVCRT_ACTION_IGNORE 104
#define IDS_MSVCRT_DIALOG_MAININSTRUCTION 105
#define IDS_MSVCRT_DIALOG_CONTENT 106
#define IDS_MSVCRT_DOWNLOADURL 107
#define IDS_INITIALIZEFAIL_ACTION_ABORT 108
#define IDS_INITIALIZEFAIL_ACTION_CONTINUE 109
#define IDS_INITIALIZEFAIL_DIALOG_MAININSTRUCTION 110
#define IDS_INITIALIZEFAIL_DIALOG_CONTENT 111
#define IDS_INITIALIZEFAIL_DIALOG_FOOTER 112
// Next default values for new objects
//
#ifdef APSTUDIO_INVOKED
#ifndef APSTUDIO_READONLY_SYMBOLS
#define _APS_NEXT_RESOURCE_VALUE 102
#define _APS_NEXT_RESOURCE_VALUE 103
#define _APS_NEXT_COMMAND_VALUE 40001
#define _APS_NEXT_CONTROL_VALUE 1001
#define _APS_NEXT_SYMED_VALUE 101

View file

@ -2,6 +2,7 @@
#include "logging.h"
#include "utils.h"
#include "resource.h"
HRESULT WINAPI InitializeImpl(LPVOID lpParam, HANDLE hMainThreadContinue);
@ -379,12 +380,50 @@ extern "C" void WINAPI RewrittenEntryPoint_AdjustedStack(RewrittenEntryPointPara
auto desc = err.Description();
if (desc.length() == 0)
desc = err.ErrorMessage();
if (MessageBoxW(nullptr, std::format(
L"Failed to load Dalamud. Load game without Dalamud(yes) or abort(no)?\n\n{}\n{}",
last_operation,
desc.GetBSTR()).c_str(),
L"Dalamud.Boot", MB_OK | MB_YESNO) == IDNO)
ExitProcess(-1);
enum IdTaskDialogAction {
IdTaskDialogActionAbort = 101,
IdTaskDialogActionContinue,
};
const TASKDIALOG_BUTTON buttons[]{
{IdTaskDialogActionAbort, MAKEINTRESOURCEW(IDS_INITIALIZEFAIL_ACTION_ABORT)},
{IdTaskDialogActionContinue, MAKEINTRESOURCEW(IDS_INITIALIZEFAIL_ACTION_CONTINUE)},
};
const auto hru32 = static_cast<uint32_t>(hr);
const auto footer = std::vformat(
utils::get_string_resource(IDS_INITIALIZEFAIL_DIALOG_FOOTER),
std::make_wformat_args(
last_operation,
hru32,
desc.GetBSTR()));
const TASKDIALOGCONFIG config{
.cbSize = sizeof config,
.hInstance = g_hModule,
.dwFlags = TDF_CAN_BE_MINIMIZED | TDF_ALLOW_DIALOG_CANCELLATION | TDF_USE_COMMAND_LINKS | TDF_EXPAND_FOOTER_AREA,
.pszWindowTitle = MAKEINTRESOURCEW(IDS_APPNAME),
.pszMainIcon = MAKEINTRESOURCEW(IDI_ICON1),
.pszMainInstruction = MAKEINTRESOURCEW(IDS_INITIALIZEFAIL_DIALOG_MAININSTRUCTION),
.pszContent = MAKEINTRESOURCEW(IDS_INITIALIZEFAIL_DIALOG_CONTENT),
.cButtons = _countof(buttons),
.pButtons = buttons,
.nDefaultButton = IdTaskDialogActionAbort,
.pszFooter = footer.c_str(),
};
int buttonPressed;
if (utils::scoped_dpi_awareness_context ctx;
FAILED(TaskDialogIndirect(&config, &buttonPressed, nullptr, nullptr)))
buttonPressed = IdTaskDialogActionAbort;
switch (buttonPressed) {
case IdTaskDialogActionAbort:
ExitProcess(-1);
break;
}
if (hMainThreadContinue) {
CloseHandle(hMainThreadContinue);
hMainThreadContinue = nullptr;

View file

@ -3,22 +3,27 @@
#include "utils.h"
std::filesystem::path utils::loaded_module::path() const {
std::wstring buf(MAX_PATH, L'\0');
for (;;) {
if (const auto len = GetModuleFileNameExW(GetCurrentProcess(), m_hModule, &buf[0], static_cast<DWORD>(buf.size())); len != buf.size()) {
if (buf.empty())
throw std::runtime_error(std::format("Failed to resolve module path: Win32 error {}", GetLastError()));
DalamudExpected<std::filesystem::path> utils::loaded_module::path() const {
for (std::wstring buf(MAX_PATH, L'\0');; buf.resize(buf.size() * 2)) {
if (const auto len = GetModuleFileNameW(m_hModule, &buf[0], static_cast<DWORD>(buf.size()));
len != buf.size()) {
if (!len) {
return DalamudUnexpected(
std::in_place,
DalamudBootErrorDescription::ModulePathResolutionFail,
HRESULT_FROM_WIN32(GetLastError()));
}
buf.resize(len);
return buf;
}
if (buf.size() * 2 < PATHCCH_MAX_CCH)
buf.resize(buf.size() * 2);
else if (auto p = std::filesystem::path(buf); exists(p))
return p;
else
throw std::runtime_error("Failed to resolve module path: no amount of buffer size would fit the data");
if (buf.size() > PATHCCH_MAX_CCH) {
return DalamudUnexpected(
std::in_place,
DalamudBootErrorDescription::ModulePathResolutionFail,
E_OUTOFMEMORY);
}
}
}
@ -144,21 +149,24 @@ void* utils::loaded_module::get_imported_function_pointer(const char* pcszDllNam
throw std::runtime_error(std::format("Failed to find import for {}!{} ({}).", pcszDllName, pcszFunctionName ? pcszFunctionName : "<unnamed>", hintOrOrdinal));
}
std::unique_ptr<std::remove_pointer_t<HGLOBAL>, decltype(&FreeResource)> utils::loaded_module::get_resource(LPCWSTR lpName, LPCWSTR lpType) const {
DalamudExpected<std::unique_ptr<std::remove_pointer_t<HGLOBAL>, decltype(&FreeResource)>> utils::loaded_module::get_resource(LPCWSTR lpName, LPCWSTR lpType) const {
const auto hres = FindResourceW(m_hModule, lpName, lpType);
if (!hres)
throw std::runtime_error("No such resource");
return DalamudUnexpected(std::in_place, DalamudBootErrorDescription::ModuleResourceLoadFail, GetLastError());
const auto hRes = LoadResource(m_hModule, hres);
if (!hRes)
throw std::runtime_error("LoadResource failure");
return DalamudUnexpected(std::in_place, DalamudBootErrorDescription::ModuleResourceLoadFail, GetLastError());
return {hRes, &FreeResource};
return std::unique_ptr<std::remove_pointer_t<HGLOBAL>, decltype(&FreeResource)>(hRes, &FreeResource);
}
std::wstring utils::loaded_module::get_description() const {
const auto rsrc = get_resource(MAKEINTRESOURCE(VS_VERSION_INFO), RT_VERSION);
const auto pBlock = LockResource(rsrc.get());
DalamudExpected<std::wstring> utils::loaded_module::get_description() const {
auto rsrc = get_resource(MAKEINTRESOURCE(VS_VERSION_INFO), RT_VERSION);
if (!rsrc)
return DalamudUnexpected(std::move(rsrc.error()));
const auto pBlock = LockResource(rsrc->get());
struct LANGANDCODEPAGE {
WORD wLanguage;
@ -166,44 +174,65 @@ std::wstring utils::loaded_module::get_description() const {
} * lpTranslate;
UINT cbTranslate;
if (!VerQueryValueW(pBlock,
TEXT("\\VarFileInfo\\Translation"),
L"\\VarFileInfo\\Translation",
reinterpret_cast<LPVOID*>(&lpTranslate),
&cbTranslate)) {
throw std::runtime_error("Invalid version information (1)");
return DalamudUnexpected(
std::in_place,
DalamudBootErrorDescription::ModuleResourceVersionReadFail,
HRESULT_FROM_WIN32(GetLastError()));
}
for (size_t i = 0; i < (cbTranslate / sizeof(LANGANDCODEPAGE)); i++) {
wchar_t subblockNameBuf[64];
*std::format_to_n(
subblockNameBuf,
_countof(subblockNameBuf),
L"\\StringFileInfo\\{:04x}{:04x}\\FileDescription",
lpTranslate[i].wLanguage,
lpTranslate[i].wCodePage).out = 0;;
wchar_t* buf = nullptr;
UINT size = 0;
if (!VerQueryValueW(pBlock,
std::format(L"\\StringFileInfo\\{:04x}{:04x}\\FileDescription",
lpTranslate[i].wLanguage,
lpTranslate[i].wCodePage).c_str(),
reinterpret_cast<LPVOID*>(&buf),
&size)) {
if (!VerQueryValueW(pBlock, subblockNameBuf, reinterpret_cast<LPVOID*>(&buf), &size))
continue;
}
auto currName = std::wstring_view(buf, size);
while (!currName.empty() && currName.back() == L'\0')
currName = currName.substr(0, currName.size() - 1);
if (const auto p = currName.find(L'\0'); p != std::string::npos)
currName = currName.substr(0, p);
if (currName.empty())
continue;
return std::wstring(currName);
}
throw std::runtime_error("Invalid version information (2)");
return DalamudUnexpected(
std::in_place,
DalamudBootErrorDescription::ModuleResourceVersionReadFail,
HRESULT_FROM_WIN32(ERROR_NOT_FOUND));
}
const VS_FIXEDFILEINFO& utils::loaded_module::get_file_version() const {
const auto rsrc = get_resource(MAKEINTRESOURCE(VS_VERSION_INFO), RT_VERSION);
const auto pBlock = LockResource(rsrc.get());
std::expected<std::reference_wrapper<const VS_FIXEDFILEINFO>, DalamudBootError> utils::loaded_module::get_file_version() const {
auto rsrc = get_resource(MAKEINTRESOURCE(VS_VERSION_INFO), RT_VERSION);
if (!rsrc)
return DalamudUnexpected(std::move(rsrc.error()));
const auto pBlock = LockResource(rsrc->get());
UINT size = 0;
LPVOID lpBuffer = nullptr;
if (!VerQueryValueW(pBlock, L"\\", &lpBuffer, &size))
throw std::runtime_error("Failed to query version information.");
if (!VerQueryValueW(pBlock, L"\\", &lpBuffer, &size)) {
return std::unexpected<DalamudBootError>(
std::in_place,
DalamudBootErrorDescription::ModuleResourceVersionReadFail,
HRESULT_FROM_WIN32(GetLastError()));
}
const VS_FIXEDFILEINFO& versionInfo = *static_cast<const VS_FIXEDFILEINFO*>(lpBuffer);
if (versionInfo.dwSignature != 0xfeef04bd)
throw std::runtime_error("Invalid version info found.");
if (versionInfo.dwSignature != 0xfeef04bd) {
return std::unexpected<DalamudBootError>(
std::in_place,
DalamudBootErrorDescription::ModuleResourceVersionSignatureFail);
}
return versionInfo;
}
@ -589,17 +618,10 @@ bool utils::is_running_on_wine() {
return g_startInfo.Platform != "WINDOWS";
}
std::filesystem::path utils::get_module_path(HMODULE hModule) {
std::wstring buf(MAX_PATH, L'\0');
while (true) {
if (const auto res = GetModuleFileNameW(hModule, &buf[0], static_cast<int>(buf.size())); !res)
throw std::runtime_error(std::format("GetModuleFileName failure: 0x{:X}", GetLastError()));
else if (res < buf.size()) {
buf.resize(res);
return buf;
} else
buf.resize(buf.size() * 2);
}
std::wstring utils::get_string_resource(uint32_t resId) {
LPCWSTR pstr;
const auto len = LoadStringW(g_hModule, resId, reinterpret_cast<LPWSTR>(&pstr), 0);
return std::wstring(pstr, len);
}
HWND utils::try_find_game_window() {
@ -677,3 +699,22 @@ std::wstring utils::format_win32_error(DWORD err) {
return std::format(L"Win32 error ({}=0x{:X})", err, err);
}
utils::scoped_dpi_awareness_context::scoped_dpi_awareness_context()
: scoped_dpi_awareness_context(DPI_AWARENESS_CONTEXT_PER_MONITOR_AWARE_V2) {
}
utils::scoped_dpi_awareness_context::scoped_dpi_awareness_context(DPI_AWARENESS_CONTEXT context) {
const auto user32 = GetModuleHandleW(L"user32.dll");
m_setThreadDpiAwarenessContext =
user32
? reinterpret_cast<decltype(&SetThreadDpiAwarenessContext)>(
GetProcAddress(user32, "SetThreadDpiAwarenessContext"))
: nullptr;
m_old = m_setThreadDpiAwarenessContext ? m_setThreadDpiAwarenessContext(context) : DPI_AWARENESS_CONTEXT_UNAWARE;
}
utils::scoped_dpi_awareness_context::~scoped_dpi_awareness_context() {
if (m_setThreadDpiAwarenessContext)
m_setThreadDpiAwarenessContext(m_old);
}

View file

@ -1,5 +1,6 @@
#pragma once
#include <expected>
#include <filesystem>
#include <functional>
#include <span>
@ -7,6 +8,7 @@
#include <memory>
#include <vector>
#include "error_info.h"
#include "unicode.h"
namespace utils {
@ -18,7 +20,7 @@ namespace utils {
loaded_module(void* hModule) : m_hModule(reinterpret_cast<HMODULE>(hModule)) {}
loaded_module(size_t hModule) : m_hModule(reinterpret_cast<HMODULE>(hModule)) {}
std::filesystem::path path() const;
DalamudExpected<std::filesystem::path> path() const;
bool is_current_process() const { return m_hModule == GetModuleHandleW(nullptr); }
bool owns_address(const void* pAddress) const;
@ -57,9 +59,9 @@ namespace utils {
void* get_imported_function_pointer(const char* pcszDllName, const char* pcszFunctionName, uint32_t hintOrOrdinal) const;
template<typename TFn> TFn** get_imported_function_pointer(const char* pcszDllName, const char* pcszFunctionName, uint32_t hintOrOrdinal) { return reinterpret_cast<TFn**>(get_imported_function_pointer(pcszDllName, pcszFunctionName, hintOrOrdinal)); }
[[nodiscard]] std::unique_ptr<std::remove_pointer_t<HGLOBAL>, decltype(&FreeResource)> get_resource(LPCWSTR lpName, LPCWSTR lpType) const;
[[nodiscard]] std::wstring get_description() const;
[[nodiscard]] const VS_FIXEDFILEINFO& get_file_version() const;
[[nodiscard]] DalamudExpected<std::unique_ptr<std::remove_pointer_t<HGLOBAL>, decltype(&FreeResource)>> get_resource(LPCWSTR lpName, LPCWSTR lpType) const;
[[nodiscard]] DalamudExpected<std::wstring> get_description() const;
[[nodiscard]] DalamudExpected<const VS_FIXEDFILEINFO&> get_file_version() const;
static loaded_module current_process();
static std::vector<loaded_module> all_modules();
@ -268,7 +270,7 @@ namespace utils {
bool is_running_on_wine();
std::filesystem::path get_module_path(HMODULE hModule);
std::wstring get_string_resource(uint32_t resId);
/// @brief Find the game main window.
/// @return Handle to the game main window, or nullptr if it doesn't exist (yet).
@ -279,4 +281,18 @@ namespace utils {
std::wstring escape_shell_arg(const std::wstring& arg);
std::wstring format_win32_error(DWORD err);
class scoped_dpi_awareness_context {
DPI_AWARENESS_CONTEXT m_old;
decltype(&SetThreadDpiAwarenessContext) m_setThreadDpiAwarenessContext;
public:
scoped_dpi_awareness_context();
scoped_dpi_awareness_context(DPI_AWARENESS_CONTEXT);
~scoped_dpi_awareness_context();
scoped_dpi_awareness_context(const scoped_dpi_awareness_context&) = delete;
scoped_dpi_awareness_context(scoped_dpi_awareness_context&&) = delete;
scoped_dpi_awareness_context& operator=(const scoped_dpi_awareness_context&) = delete;
scoped_dpi_awareness_context& operator=(scoped_dpi_awareness_context&&) = delete;
};
}

View file

@ -31,6 +31,8 @@ HANDLE g_crashhandler_process = nullptr;
HANDLE g_crashhandler_event = nullptr;
HANDLE g_crashhandler_pipe_write = nullptr;
wchar_t g_external_event_info[16384] = L"";
std::recursive_mutex g_exception_handler_mutex;
std::chrono::time_point<std::chrono::system_clock> g_time_start;
@ -102,9 +104,13 @@ bool is_ffxiv_address(const wchar_t* module_name, const DWORD64 address)
return false;
}
static void append_injector_launch_args(std::vector<std::wstring>& args)
static DalamudExpected<void> append_injector_launch_args(std::vector<std::wstring>& args)
{
args.emplace_back(L"--game=\"" + utils::loaded_module::current_process().path().wstring() + L"\"");
if (auto path = utils::loaded_module::current_process().path())
args.emplace_back(L"--game=\"" + path->wstring() + L"\"");
else
return DalamudUnexpected(std::in_place, std::move(path.error()));
switch (g_startInfo.DalamudLoadMethod) {
case DalamudStartInfo::LoadMethod::Entrypoint:
args.emplace_back(L"--mode=entrypoint");
@ -118,6 +124,7 @@ static void append_injector_launch_args(std::vector<std::wstring>& args)
args.emplace_back(L"--logname=\"" + unicode::convert<std::wstring>(g_startInfo.LogName) + L"\"");
args.emplace_back(L"--dalamud-plugin-directory=\"" + unicode::convert<std::wstring>(g_startInfo.PluginDirectory) + L"\"");
args.emplace_back(L"--dalamud-asset-directory=\"" + unicode::convert<std::wstring>(g_startInfo.AssetDirectory) + L"\"");
args.emplace_back(L"--dalamud-temp-directory=\"" + unicode::convert<std::wstring>(g_startInfo.TempDirectory) + L"\"");
args.emplace_back(std::format(L"--dalamud-client-language={}", static_cast<int>(g_startInfo.Language)));
args.emplace_back(std::format(L"--dalamud-delay-initialize={}", g_startInfo.DelayInitializeMs));
// NoLoadPlugins/NoLoadThirdPartyPlugins: supplied from DalamudCrashHandler
@ -155,6 +162,8 @@ static void append_injector_launch_args(std::vector<std::wstring>& args)
args.emplace_back(szArgList[i]);
LocalFree(szArgList);
}
return {};
}
LONG exception_handler(EXCEPTION_POINTERS* ex)
@ -184,7 +193,11 @@ LONG exception_handler(EXCEPTION_POINTERS* ex)
DuplicateHandle(GetCurrentProcess(), g_crashhandler_event, g_crashhandler_process, &exinfo.hEventHandle, 0, TRUE, DUPLICATE_SAME_ACCESS);
std::wstring stackTrace;
if (!g_clr)
if (ex->ExceptionRecord->ExceptionCode == CUSTOM_EXCEPTION_EXTERNAL_EVENT)
{
stackTrace = std::wstring(g_external_event_info);
}
else if (!g_clr)
{
stackTrace = L"(no CLR stack trace available)";
}
@ -245,6 +258,12 @@ LONG WINAPI structured_exception_handler(EXCEPTION_POINTERS* ex)
LONG WINAPI vectored_exception_handler(EXCEPTION_POINTERS* ex)
{
// special case for CLR exceptions, always trigger crash handler
if (ex->ExceptionRecord->ExceptionCode == CUSTOM_EXCEPTION_EXTERNAL_EVENT)
{
return exception_handler(ex);
}
if (ex->ExceptionRecord->ExceptionCode == 0x12345678)
{
// pass
@ -262,7 +281,7 @@ LONG WINAPI vectored_exception_handler(EXCEPTION_POINTERS* ex)
if (!is_ffxiv_address(L"ffxiv_dx11.exe", ex->ContextRecord->Rip) &&
!is_ffxiv_address(L"cimgui.dll", ex->ContextRecord->Rip))
return EXCEPTION_CONTINUE_SEARCH;
return EXCEPTION_CONTINUE_SEARCH;
}
return exception_handler(ex);
@ -291,7 +310,7 @@ bool veh::add_handler(bool doFullDump, const std::string& workingDirectory)
if (HANDLE hReadPipeRaw, hWritePipeRaw; CreatePipe(&hReadPipeRaw, &hWritePipeRaw, nullptr, 65536))
{
hWritePipe.emplace(hWritePipeRaw, &CloseHandle);
if (HANDLE hReadPipeInheritableRaw; DuplicateHandle(GetCurrentProcess(), hReadPipeRaw, GetCurrentProcess(), &hReadPipeInheritableRaw, 0, TRUE, DUPLICATE_SAME_ACCESS | DUPLICATE_CLOSE_SOURCE))
{
hReadPipeInheritable.emplace(hReadPipeInheritableRaw, &CloseHandle);
@ -309,9 +328,9 @@ bool veh::add_handler(bool doFullDump, const std::string& workingDirectory)
}
// additional information
STARTUPINFOEXW siex{};
STARTUPINFOEXW siex{};
PROCESS_INFORMATION pi{};
siex.StartupInfo.cb = sizeof siex;
siex.StartupInfo.dwFlags = STARTF_USESHOWWINDOW;
siex.StartupInfo.wShowWindow = g_startInfo.CrashHandlerShow ? SW_SHOW : SW_HIDE;
@ -358,11 +377,20 @@ bool veh::add_handler(bool doFullDump, const std::string& workingDirectory)
args.emplace_back(std::format(L"--process-handle={}", reinterpret_cast<size_t>(hInheritableCurrentProcess)));
args.emplace_back(std::format(L"--exception-info-pipe-read-handle={}", reinterpret_cast<size_t>(hReadPipeInheritable->get())));
args.emplace_back(std::format(L"--asset-directory={}", unicode::convert<std::wstring>(g_startInfo.AssetDirectory)));
args.emplace_back(std::format(L"--log-directory={}", g_startInfo.BootLogPath.empty()
? utils::loaded_module(g_hModule).path().parent_path().wstring()
: std::filesystem::path(unicode::convert<std::wstring>(g_startInfo.BootLogPath)).parent_path().wstring()));
if (const auto path = utils::loaded_module(g_hModule).path()) {
args.emplace_back(std::format(L"--log-directory={}", g_startInfo.BootLogPath.empty()
? path->parent_path().wstring()
: std::filesystem::path(unicode::convert<std::wstring>(g_startInfo.BootLogPath)).parent_path().wstring()));
} else {
logging::W("Failed to read path of the Dalamud Boot module: {}", path.error().describe());
return false;
}
args.emplace_back(L"--");
append_injector_launch_args(args);
if (auto r = append_injector_launch_args(args); !r) {
logging::W("Failed to generate injector launch args: {}", r.error().describe());
return false;
}
for (const auto& arg : args)
{
@ -370,7 +398,7 @@ bool veh::add_handler(bool doFullDump, const std::string& workingDirectory)
argstr.push_back(L' ');
}
argstr.pop_back();
if (!handles.empty() && !UpdateProcThreadAttribute(siex.lpAttributeList, 0, PROC_THREAD_ATTRIBUTE_HANDLE_LIST, &handles[0], std::span(handles).size_bytes(), nullptr, nullptr))
{
logging::W("Failed to launch DalamudCrashHandler.exe: UpdateProcThreadAttribute error 0x{:x}", GetLastError());
@ -385,7 +413,7 @@ bool veh::add_handler(bool doFullDump, const std::string& workingDirectory)
TRUE, // Set handle inheritance to FALSE
EXTENDED_STARTUPINFO_PRESENT, // lpStartupInfo actually points to a STARTUPINFOEX(W)
nullptr, // Use parent's environment block
nullptr, // Use parent's starting directory
nullptr, // Use parent's starting directory
&siex.StartupInfo, // Pointer to STARTUPINFO structure
&pi // Pointer to PROCESS_INFORMATION structure (removed extra parentheses)
))
@ -401,7 +429,7 @@ bool veh::add_handler(bool doFullDump, const std::string& workingDirectory)
}
CloseHandle(pi.hThread);
g_crashhandler_process = pi.hProcess;
g_crashhandler_pipe_write = hWritePipe->release();
logging::I("Launched DalamudCrashHandler.exe: PID {}", pi.dwProcessId);
@ -419,3 +447,16 @@ bool veh::remove_handler()
}
return false;
}
void veh::raise_external_event(const std::wstring& info)
{
const auto info_size = std::min(info.size(), std::size(g_external_event_info) - 1);
wcsncpy_s(g_external_event_info, info.c_str(), info_size);
RaiseException(CUSTOM_EXCEPTION_EXTERNAL_EVENT, 0, 0, nullptr);
}
extern "C" __declspec(dllexport) void BootVehRaiseExternalEventW(LPCWSTR info)
{
const std::wstring info_wstr(info);
veh::raise_external_event(info_wstr);
}

View file

@ -4,4 +4,5 @@ namespace veh
{
bool add_handler(bool doFullDump, const std::string& workingDirectory);
bool remove_handler();
void raise_external_event(const std::wstring& info);
}

View file

@ -8,12 +8,6 @@
#include "ntdll.h"
#include "utils.h"
template<typename T>
static std::span<T> assume_nonempty_span(std::span<T> t, const char* descr) {
if (t.empty())
throw std::runtime_error(std::format("Unexpected empty span found: {}", descr));
return t;
}
void xivfixes::unhook_dll(bool bApply) {
static const auto LogTag = "[xivfixes:unhook_dll]";
static const auto LogTagW = L"[xivfixes:unhook_dll]";
@ -23,77 +17,90 @@ void xivfixes::unhook_dll(bool bApply) {
const auto mods = utils::loaded_module::all_modules();
const auto test_module = [&](size_t i, const utils::loaded_module & mod) {
std::filesystem::path path;
try {
path = mod.path();
std::wstring version, description;
try {
version = utils::format_file_version(mod.get_file_version());
} catch (...) {
version = L"<unknown>";
}
try {
description = mod.get_description();
} catch (...) {
description = L"<unknown>";
}
logging::I(R"({} [{}/{}] Module 0x{:X} ~ 0x{:X} (0x{:X}): "{}" ("{}" ver {}))", LogTagW, i + 1, mods.size(), mod.address_int(), mod.address_int() + mod.image_size(), mod.image_size(), path.wstring(), description, version);
} catch (const std::exception& e) {
logging::W("{} [{}/{}] Module 0x{:X}: Failed to resolve path: {}", LogTag, i + 1, mods.size(), mod.address_int(), e.what());
for (size_t i = 0; i < mods.size(); i++) {
const auto& mod = mods[i];
const auto path = mod.path();
if (!path) {
logging::W(
"{} [{}/{}] Module 0x{:X}: Failed to resolve path: {}",
LogTag,
i + 1,
mods.size(),
mod.address_int(),
path.error().describe());
return;
}
const auto moduleName = unicode::convert<std::string>(path.filename().wstring());
const auto version = mod.get_file_version()
.transform([](const auto& v) { return utils::format_file_version(v.get()); })
.value_or(L"<unknown>");
std::vector<char> buf;
std::string formatBuf;
const auto description = mod.get_description()
.value_or(L"<unknown>");
logging::I(
R"({} [{}/{}] Module 0x{:X} ~ 0x{:X} (0x{:X}): "{}" ("{}" ver {}))",
LogTagW,
i + 1,
mods.size(),
mod.address_int(),
mod.address_int() + mod.image_size(),
mod.image_size(),
path->wstring(),
description,
version);
const auto moduleName = unicode::convert<std::string>(path->filename().wstring());
const auto& sectionHeader = mod.section_header(".text");
const auto section = mod.span_as<char>(sectionHeader.VirtualAddress, sectionHeader.Misc.VirtualSize);
if (section.empty()) {
logging::W("{} Error: .text[VA:VA + VS] is empty", LogTag);
return;
}
auto hFsDllRaw = CreateFileW(path->c_str(), GENERIC_READ, FILE_SHARE_READ, nullptr, OPEN_EXISTING, 0, nullptr);
if (hFsDllRaw == INVALID_HANDLE_VALUE) {
logging::W("{} Module loaded in current process but could not open file: Win32 error {}", LogTag, GetLastError());
return;
}
auto hFsDll = std::unique_ptr<void, decltype(&CloseHandle)>(hFsDllRaw, &CloseHandle);
std::vector<char> buf(section.size());
SetFilePointer(hFsDll.get(), sectionHeader.PointerToRawData, nullptr, FILE_CURRENT);
if (DWORD read{}; ReadFile(hFsDll.get(), &buf[0], static_cast<DWORD>(buf.size()), &read, nullptr)) {
if (read < section.size_bytes()) {
logging::W("{} ReadFile: read {} bytes < requested {} bytes", LogTagW, read, section.size_bytes());
return;
}
} else {
logging::I("{} ReadFile: Win32 error {}", LogTagW, GetLastError());
return;
}
const auto doRestore = g_startInfo.BootUnhookDlls.contains(unicode::convert<std::string>(path->filename().u8string()));
try {
const auto& sectionHeader = mod.section_header(".text");
const auto section = assume_nonempty_span(mod.span_as<char>(sectionHeader.VirtualAddress, sectionHeader.Misc.VirtualSize), ".text[VA:VA+VS]");
auto hFsDllRaw = CreateFileW(path.c_str(), GENERIC_READ, FILE_SHARE_READ, nullptr, OPEN_EXISTING, 0, nullptr);
if (hFsDllRaw == INVALID_HANDLE_VALUE) {
logging::W("{} Module loaded in current process but could not open file: Win32 error {}", LogTag, GetLastError());
return;
}
auto hFsDll = std::unique_ptr<void, decltype(CloseHandle)*>(hFsDllRaw, &CloseHandle);
buf.resize(section.size());
SetFilePointer(hFsDll.get(), sectionHeader.PointerToRawData, nullptr, FILE_CURRENT);
if (DWORD read{}; ReadFile(hFsDll.get(), &buf[0], static_cast<DWORD>(buf.size()), &read, nullptr)) {
if (read < section.size_bytes()) {
logging::W("{} ReadFile: read {} bytes < requested {} bytes", LogTagW, read, section.size_bytes());
return;
}
} else {
logging::I("{} ReadFile: Win32 error {}", LogTagW, GetLastError());
return;
}
const auto doRestore = g_startInfo.BootUnhookDlls.contains(unicode::convert<std::string>(path.filename().u8string()));
std::optional<utils::memory_tenderizer> tenderizer;
for (size_t i = 0, instructionLength = 1, printed = 0; i < buf.size(); i += instructionLength) {
if (section[i] == buf[i]) {
std::string formatBuf;
for (size_t inst = 0, instructionLength = 1, printed = 0; inst < buf.size(); inst += instructionLength) {
if (section[inst] == buf[inst]) {
instructionLength = 1;
continue;
}
const auto rva = sectionHeader.VirtualAddress + i;
const auto rva = sectionHeader.VirtualAddress + inst;
nmd_x86_instruction instruction{};
if (!nmd_x86_decode(&section[i], section.size() - i, &instruction, NMD_X86_MODE_64, NMD_X86_DECODER_FLAGS_ALL)) {
if (!nmd_x86_decode(&section[inst], section.size() - inst, &instruction, NMD_X86_MODE_64, NMD_X86_DECODER_FLAGS_ALL)) {
instructionLength = 1;
if (printed < 64) {
logging::W("{} {}+0x{:0X}: dd {:02X}", LogTag, moduleName, rva, static_cast<uint8_t>(section[i]));
logging::W("{} {}+0x{:0X}: dd {:02X}", LogTag, moduleName, rva, static_cast<uint8_t>(section[inst]));
printed++;
}
} else {
instructionLength = instruction.length;
if (printed < 64) {
formatBuf.resize(128);
nmd_x86_format(&instruction, &formatBuf[0], reinterpret_cast<size_t>(&section[i]), NMD_X86_FORMAT_FLAGS_DEFAULT | NMD_X86_FORMAT_FLAGS_BYTES);
nmd_x86_format(&instruction, &formatBuf[0], reinterpret_cast<size_t>(&section[inst]), NMD_X86_FORMAT_FLAGS_DEFAULT | NMD_X86_FORMAT_FLAGS_BYTES);
formatBuf.resize(strnlen(&formatBuf[0], formatBuf.size()));
const auto& directory = mod.data_directory(IMAGE_DIRECTORY_ENTRY_EXPORT);
@ -103,25 +110,25 @@ void xivfixes::unhook_dll(bool bApply) {
const auto functions = mod.span_as<DWORD>(exportDirectory.AddressOfFunctions, exportDirectory.NumberOfFunctions);
std::string resolvedExportName;
for (size_t j = 0; j < names.size(); ++j) {
for (size_t nameIndex = 0; nameIndex < names.size(); ++nameIndex) {
std::string_view name;
if (const char* pcszName = mod.address_as<char>(names[j]); pcszName < mod.address() || pcszName >= mod.address() + mod.image_size()) {
if (const char* pcszName = mod.address_as<char>(names[nameIndex]); pcszName < mod.address() || pcszName >= mod.address() + mod.image_size()) {
if (IsBadReadPtr(pcszName, 256)) {
logging::W("{} Name #{} points to an invalid address outside the executable. Skipping.", LogTag, j);
logging::W("{} Name #{} points to an invalid address outside the executable. Skipping.", LogTag, nameIndex);
continue;
}
name = std::string_view(pcszName, strnlen(pcszName, 256));
logging::W("{} Name #{} points to a seemingly valid address outside the executable: {}", LogTag, j, name);
logging::W("{} Name #{} points to a seemingly valid address outside the executable: {}", LogTag, nameIndex, name);
}
if (ordinals[j] >= functions.size()) {
logging::W("{} Ordinal #{} points to function index #{} >= #{}. Skipping.", LogTag, j, ordinals[j], functions.size());
if (ordinals[nameIndex] >= functions.size()) {
logging::W("{} Ordinal #{} points to function index #{} >= #{}. Skipping.", LogTag, nameIndex, ordinals[nameIndex], functions.size());
continue;
}
const auto rva = functions[ordinals[j]];
if (rva == &section[i] - mod.address()) {
const auto rva = functions[ordinals[nameIndex]];
if (rva == &section[inst] - mod.address()) {
resolvedExportName = std::format("[export:{}]", name);
break;
}
@ -135,7 +142,7 @@ void xivfixes::unhook_dll(bool bApply) {
if (doRestore) {
if (!tenderizer)
tenderizer.emplace(section, PAGE_EXECUTE_READWRITE);
memcpy(&section[i], &buf[i], instructionLength);
memcpy(&section[inst], &buf[inst], instructionLength);
}
}
@ -147,21 +154,7 @@ void xivfixes::unhook_dll(bool bApply) {
} catch (const std::exception& e) {
logging::W("{} Error: {}", LogTag, e.what());
}
};
// This is needed since try and __try cannot be used in the same function. Lambdas circumvent the limitation.
const auto windows_exception_handler = [&]() {
for (size_t i = 0; i < mods.size(); i++) {
const auto& mod = mods[i];
__try {
test_module(i, mod);
} __except (EXCEPTION_EXECUTE_HANDLER) {
logging::W("{} Error: Access Violation", LogTag);
}
}
};
windows_exception_handler();
}
}
using TFnGetInputDeviceManager = void* ();
@ -294,13 +287,11 @@ static bool is_xivalex(const std::filesystem::path& dllPath) {
static bool is_openprocess_already_dealt_with() {
static const auto s_value = [] {
for (const auto& mod : utils::loaded_module::all_modules()) {
try {
if (is_xivalex(mod.path()))
return true;
} catch (...) {
// pass
}
const auto path = mod.path().value_or({});
if (path.empty())
continue;
if (is_xivalex(path))
return true;
}
return false;
}();
@ -650,43 +641,22 @@ void xivfixes::symbol_load_patches(bool bApply) {
void xivfixes::disable_game_debugging_protection(bool bApply) {
static const char* LogTag = "[xivfixes:disable_game_debugging_protection]";
static const std::vector<uint8_t> patchBytes = {
0x31, 0xC0, // XOR EAX, EAX
0x90, // NOP
0x90, // NOP
0x90, // NOP
0x90 // NOP
};
static std::optional<hooks::import_hook<decltype(IsDebuggerPresent)>> s_hookIsDebuggerPresent;
if (!bApply)
return;
if (bApply) {
if (!g_startInfo.BootEnabledGameFixes.contains("disable_game_debugging_protection")) {
logging::I("{} Turned off via environment variable.", LogTag);
return;
}
if (!g_startInfo.BootEnabledGameFixes.contains("disable_game_debugging_protection")) {
logging::I("{} Turned off via environment variable.", LogTag);
return;
}
// Find IsDebuggerPresent in Framework.Tick()
const char* matchPtr = utils::signature_finder()
.look_in(utils::loaded_module(g_hGameInstance), ".text")
.look_for_hex("FF 15 ?? ?? ?? ?? 85 C0 74 13 41")
.find_one()
.Match.data();
if (!matchPtr) {
logging::E("{} Failed to find signature.", LogTag);
return;
}
void* address = const_cast<void*>(static_cast<const void*>(matchPtr));
DWORD oldProtect;
if (VirtualProtect(address, patchBytes.size(), PAGE_EXECUTE_READWRITE, &oldProtect)) {
memcpy(address, patchBytes.data(), patchBytes.size());
VirtualProtect(address, patchBytes.size(), oldProtect, &oldProtect);
logging::I("{} Patch applied at address 0x{:X}.", LogTag, reinterpret_cast<uintptr_t>(address));
s_hookIsDebuggerPresent.emplace("kernel32.dll!IsDebuggerPresent", "kernel32.dll", "IsDebuggerPresent", 0);
s_hookIsDebuggerPresent->set_detour([]() { return false; });
logging::I("{} Enable", LogTag);
} else {
logging::E("{} Failed to change memory protection.", LogTag);
if (s_hookIsDebuggerPresent) {
logging::I("{} Disable", LogTag);
s_hookIsDebuggerPresent.reset();
}
}
}

View file

@ -6,7 +6,7 @@
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Newtonsoft.Json" Version="13.0.2" />
<PackageReference Include="Newtonsoft.Json" />
</ItemGroup>
</Project>

View file

@ -34,6 +34,12 @@ public record DalamudStartInfo
/// </summary>
public string? ConfigurationPath { get; set; }
/// <summary>
/// Gets or sets the directory for temporary files. This directory needs to exist and be writable to the user.
/// It should also be predictable and easy for launchers to find.
/// </summary>
public string? TempDirectory { get; set; }
/// <summary>
/// Gets or sets the path of the log files.
/// </summary>

View file

@ -25,9 +25,9 @@
</ItemGroup>
<ItemGroup>
<PackageReference Include="Lumina" Version="$(LuminaVersion)" />
<PackageReference Include="Newtonsoft.Json" Version="$(NewtonsoftJsonVersion)" />
<PackageReference Include="StyleCop.Analyzers" Version="1.2.0-beta.333">
<PackageReference Include="Lumina" />
<PackageReference Include="Newtonsoft.Json" />
<PackageReference Include="StyleCop.Analyzers">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>

View file

@ -1,111 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<Project DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<PropertyGroup Label="Globals">
<ProjectGuid>{8874326B-E755-4D13-90B4-59AB263A3E6B}</ProjectGuid>
<RootNamespace>Dalamud_Injector_Boot</RootNamespace>
<Configuration Condition=" '$(Configuration)'=='' ">Debug</Configuration>
<Platform>x64</Platform>
</PropertyGroup>
<ItemGroup Label="ProjectConfigurations">
<ProjectConfiguration Include="Debug|x64">
<Configuration>Debug</Configuration>
<Platform>x64</Platform>
</ProjectConfiguration>
<ProjectConfiguration Include="Release|x64">
<Configuration>Release</Configuration>
<Platform>x64</Platform>
</ProjectConfiguration>
</ItemGroup>
<PropertyGroup Label="Globals">
<VCProjectVersion>16.0</VCProjectVersion>
<Keyword>Win32Proj</Keyword>
<WindowsTargetPlatformVersion>10.0</WindowsTargetPlatformVersion>
<TargetName>Dalamud.Injector</TargetName>
</PropertyGroup>
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.Default.props" />
<PropertyGroup Label="Configuration">
<ConfigurationType>Application</ConfigurationType>
<UseDebugLibraries>true</UseDebugLibraries>
<PlatformToolset>v143</PlatformToolset>
<LinkIncremental>false</LinkIncremental>
<CharacterSet>Unicode</CharacterSet>
<OutDir>..\bin\$(Configuration)\</OutDir>
<IntDir>obj\$(Configuration)\</IntDir>
</PropertyGroup>
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.props" />
<ItemDefinitionGroup>
<ClCompile>
<WarningLevel>Level3</WarningLevel>
<SDLCheck>true</SDLCheck>
<ConformanceMode>true</ConformanceMode>
<LanguageStandard>stdcpplatest</LanguageStandard>
<PrecompiledHeaderFile>pch.h</PrecompiledHeaderFile>
<DebugInformationFormat>ProgramDatabase</DebugInformationFormat>
<PreprocessorDefinitions>CPPDLLTEMPLATE_EXPORTS;_WINDOWS;_USRDLL;%(PreprocessorDefinitions)</PreprocessorDefinitions>
</ClCompile>
<Link>
<SubSystem>Console</SubSystem>
<GenerateDebugInformation>true</GenerateDebugInformation>
<EnableUAC>false</EnableUAC>
<AdditionalLibraryDirectories>..\lib\CoreCLR;%(AdditionalLibraryDirectories)</AdditionalLibraryDirectories>
<ProgramDatabaseFile>$(OutDir)$(TargetName).Boot.pdb</ProgramDatabaseFile>
</Link>
</ItemDefinitionGroup>
<ItemDefinitionGroup Condition="'$(Configuration)'=='Debug'">
<ClCompile>
<FunctionLevelLinking>true</FunctionLevelLinking>
<IntrinsicFunctions>false</IntrinsicFunctions>
<RuntimeLibrary>MultiThreadedDebugDLL</RuntimeLibrary>
<PreprocessorDefinitions>_DEBUG;%(PreprocessorDefinitions)</PreprocessorDefinitions>
</ClCompile>
<Link>
<EnableCOMDATFolding>false</EnableCOMDATFolding>
<OptimizeReferences>false</OptimizeReferences>
</Link>
</ItemDefinitionGroup>
<ItemDefinitionGroup Condition="'$(Configuration)'=='Release'">
<ClCompile>
<FunctionLevelLinking>true</FunctionLevelLinking>
<IntrinsicFunctions>true</IntrinsicFunctions>
<RuntimeLibrary>MultiThreadedDLL</RuntimeLibrary>
<PreprocessorDefinitions>NDEBUG;%(PreprocessorDefinitions)</PreprocessorDefinitions>
</ClCompile>
<Link>
<EnableCOMDATFolding>true</EnableCOMDATFolding>
<OptimizeReferences>true</OptimizeReferences>
</Link>
</ItemDefinitionGroup>
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.targets" />
<ItemGroup>
<Content Include="..\lib\CoreCLR\nethost\nethost.dll">
<Link>nethost.dll</Link>
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>
</ItemGroup>
<ItemGroup>
<Image Include="dalamud.ico" />
</ItemGroup>
<ItemGroup>
<ResourceCompile Include="resources.rc" />
</ItemGroup>
<ItemGroup>
<ClCompile Include="..\Dalamud.Boot\logging.cpp" />
<ClCompile Include="..\Dalamud.Boot\unicode.cpp" />
<ClCompile Include="..\lib\CoreCLR\boot.cpp" />
<ClCompile Include="..\lib\CoreCLR\CoreCLR.cpp" />
<ClCompile Include="main.cpp" />
</ItemGroup>
<ItemGroup>
<ClInclude Include="..\Dalamud.Boot\logging.h" />
<ClInclude Include="..\Dalamud.Boot\unicode.h" />
<ClInclude Include="..\lib\CoreCLR\CoreCLR.h" />
<ClInclude Include="..\lib\CoreCLR\core\coreclr_delegates.h" />
<ClInclude Include="..\lib\CoreCLR\core\hostfxr.h" />
<ClInclude Include="..\lib\CoreCLR\nethost\nethost.h" />
<ClInclude Include="pch.h" />
</ItemGroup>
<Target Name="RemoveExtraFiles" AfterTargets="PostBuildEvent">
<Delete Files="$(OutDir)$(TargetName).lib" />
<Delete Files="$(OutDir)$(TargetName).exp" />
</Target>
</Project>

View file

@ -1,67 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<Project ToolsVersion="4.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<ItemGroup>
<Filter Include="Source Files">
<UniqueIdentifier>{4FC737F1-C7A5-4376-A066-2A32D752A2FF}</UniqueIdentifier>
<Extensions>cpp;c;cc;cxx;c++;cppm;ixx;def;odl;idl;hpj;bat;asm;asmx</Extensions>
</Filter>
<Filter Include="Header Files">
<UniqueIdentifier>{93995380-89BD-4b04-88EB-625FBE52EBFB}</UniqueIdentifier>
<Extensions>h;hh;hpp;hxx;h++;hm;inl;inc;ipp;xsd</Extensions>
</Filter>
<Filter Include="Resource Files">
<UniqueIdentifier>{4faac519-3a73-4b2b-96e7-fb597f02c0be}</UniqueIdentifier>
<Extensions>ico;rc</Extensions>
</Filter>
</ItemGroup>
<ItemGroup>
<Image Include="dalamud.ico">
<Filter>Resource Files</Filter>
</Image>
</ItemGroup>
<ItemGroup>
<ResourceCompile Include="resources.rc">
<Filter>Resource Files</Filter>
</ResourceCompile>
</ItemGroup>
<ItemGroup>
<ClCompile Include="main.cpp">
<Filter>Source Files</Filter>
</ClCompile>
<ClCompile Include="..\lib\CoreCLR\boot.cpp">
<Filter>Source Files</Filter>
</ClCompile>
<ClCompile Include="..\lib\CoreCLR\CoreCLR.cpp">
<Filter>Source Files</Filter>
</ClCompile>
<ClCompile Include="..\Dalamud.Boot\logging.cpp">
<Filter>Source Files</Filter>
</ClCompile>
<ClCompile Include="..\Dalamud.Boot\unicode.cpp">
<Filter>Source Files</Filter>
</ClCompile>
</ItemGroup>
<ItemGroup>
<ClInclude Include="..\lib\CoreCLR\CoreCLR.h">
<Filter>Header Files</Filter>
</ClInclude>
<ClInclude Include="..\lib\CoreCLR\nethost\nethost.h">
<Filter>Header Files</Filter>
</ClInclude>
<ClInclude Include="..\lib\CoreCLR\core\hostfxr.h">
<Filter>Header Files</Filter>
</ClInclude>
<ClInclude Include="..\lib\CoreCLR\core\coreclr_delegates.h">
<Filter>Header Files</Filter>
</ClInclude>
<ClInclude Include="..\Dalamud.Boot\logging.h">
<Filter>Header Files</Filter>
</ClInclude>
<ClInclude Include="pch.h">
<Filter>Header Files</Filter>
</ClInclude>
<ClInclude Include="..\Dalamud.Boot\unicode.h">
<Filter>Header Files</Filter>
</ClInclude>
</ItemGroup>
</Project>

View file

@ -1,48 +0,0 @@
#define WIN32_LEAN_AND_MEAN
#include <filesystem>
#include <Windows.h>
#include <shellapi.h>
#include "..\Dalamud.Boot\logging.h"
#include "..\lib\CoreCLR\CoreCLR.h"
#include "..\lib\CoreCLR\boot.h"
int wmain(int argc, wchar_t** argv)
{
// Take care: don't redirect stderr/out here, we need to write our pid to stdout for XL to read
//logging::start_file_logging("dalamud.injector.boot.log", false);
logging::I("Dalamud Injector, (c) 2021 XIVLauncher Contributors");
logging::I("Built at : " __DATE__ "@" __TIME__);
wchar_t _module_path[MAX_PATH];
GetModuleFileNameW(NULL, _module_path, sizeof _module_path / 2);
std::filesystem::path fs_module_path(_module_path);
std::wstring runtimeconfig_path = _wcsdup(fs_module_path.replace_filename(L"Dalamud.Injector.runtimeconfig.json").c_str());
std::wstring module_path = _wcsdup(fs_module_path.replace_filename(L"Dalamud.Injector.dll").c_str());
// =========================================================================== //
void* entrypoint_vfn;
const auto result = InitializeClrAndGetEntryPoint(
GetModuleHandleW(nullptr),
false,
runtimeconfig_path,
module_path,
L"Dalamud.Injector.EntryPoint, Dalamud.Injector",
L"Main",
L"Dalamud.Injector.EntryPoint+MainDelegate, Dalamud.Injector",
&entrypoint_vfn);
if (FAILED(result))
return result;
typedef int (CORECLR_DELEGATE_CALLTYPE* custom_component_entry_point_fn)(int, wchar_t**);
custom_component_entry_point_fn entrypoint_fn = reinterpret_cast<custom_component_entry_point_fn>(entrypoint_vfn);
logging::I("Running Dalamud Injector...");
const auto ret = entrypoint_fn(argc, argv);
logging::I("Done!");
return ret;
}

View file

@ -1 +0,0 @@
#pragma once

View file

@ -1 +0,0 @@
MAINICON ICON "dalamud.ico"

View file

@ -13,12 +13,13 @@
</PropertyGroup>
<PropertyGroup Label="Output">
<OutputType>Library</OutputType>
<OutputType>Exe</OutputType>
<OutputPath>..\bin\$(Configuration)\</OutputPath>
<AppendTargetFrameworkToOutputPath>false</AppendTargetFrameworkToOutputPath>
<AppendRuntimeIdentifierToOutputPath>false</AppendRuntimeIdentifierToOutputPath>
<GenerateRuntimeConfigurationFiles>true</GenerateRuntimeConfigurationFiles>
<ProduceReferenceAssembly>false</ProduceReferenceAssembly>
<ApplicationIcon>dalamud.ico</ApplicationIcon>
</PropertyGroup>
<PropertyGroup Label="Documentation">
@ -52,17 +53,18 @@
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Iced" Version="1.17.0" />
<PackageReference Include="JetBrains.Annotations" Version="2022.1.0" />
<PackageReference Include="Newtonsoft.Json" Version="13.0.2" />
<PackageReference Include="PeNet" Version="2.6.4" />
<PackageReference Include="Reloaded.Memory" Version="7.0.0" />
<PackageReference Include="Reloaded.Memory.Buffers" Version="2.0.0" />
<PackageReference Include="Serilog" Version="2.11.0" />
<PackageReference Include="Serilog.Sinks.Async" Version="1.5.0" />
<PackageReference Include="Serilog.Sinks.Console" Version="4.0.1" />
<PackageReference Include="Serilog.Sinks.File" Version="5.0.0" />
<PackageReference Include="StyleCop.Analyzers" Version="1.2.0-beta.333">
<PackageReference Include="Iced" />
<PackageReference Include="JetBrains.Annotations" />
<PackageReference Include="Newtonsoft.Json" />
<PackageReference Include="Microsoft.Windows.CsWin32" />
<PackageReference Include="PeNet" />
<PackageReference Include="Reloaded.Memory" />
<PackageReference Include="Reloaded.Memory.Buffers" />
<PackageReference Include="Serilog" />
<PackageReference Include="Serilog.Sinks.Async" />
<PackageReference Include="Serilog.Sinks.Console" />
<PackageReference Include="Serilog.Sinks.File" />
<PackageReference Include="StyleCop.Analyzers">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>

View file

@ -13,8 +13,8 @@ using Reloaded.Memory.Buffers;
using Reloaded.Memory.Sources;
using Reloaded.Memory.Utilities;
using Serilog;
using Windows.Win32.Foundation;
using static Dalamud.Injector.NativeFunctions;
using static Iced.Intel.AssemblerRegisters;
namespace Dalamud.Injector
@ -88,7 +88,7 @@ namespace Dalamud.Injector
if (lpParameter == 0)
throw new Exception("Unable to allocate LoadLibraryW parameter");
this.CallRemoteFunction(this.loadLibraryShellPtr, lpParameter, out var err);
var err = this.CallRemoteFunction(this.loadLibraryShellPtr, lpParameter);
this.extMemory.Read<IntPtr>(this.loadLibraryRetPtr, out address);
if (address == IntPtr.Zero)
throw new Exception($"LoadLibraryW(\"{modulePath}\") failure: {new Win32Exception((int)err).Message} ({err})");
@ -108,7 +108,7 @@ namespace Dalamud.Injector
if (lpParameter == 0)
throw new Exception("Unable to allocate GetProcAddress parameter ptr");
this.CallRemoteFunction(this.getProcAddressShellPtr, lpParameter, out var err);
var err = this.CallRemoteFunction(this.getProcAddressShellPtr, lpParameter);
this.extMemory.Read<nuint>(this.getProcAddressRetPtr, out address);
if (address == 0)
throw new Exception($"GetProcAddress(0x{module:X}, \"{functionName}\") failure: {new Win32Exception((int)err).Message} ({err})");
@ -119,27 +119,30 @@ namespace Dalamud.Injector
/// </summary>
/// <param name="methodAddress">Method address.</param>
/// <param name="parameterAddress">Parameter address.</param>
/// <param name="exitCode">Thread exit code.</param>
public void CallRemoteFunction(nuint methodAddress, nuint parameterAddress, out uint exitCode)
/// <returns>Thread exit code.</returns>
public unsafe uint CallRemoteFunction(nuint methodAddress, nuint parameterAddress)
{
// Create and initialize a thread at our address and parameter address.
var threadHandle = CreateRemoteThread(
this.targetProcess.Handle,
IntPtr.Zero,
var threadHandle = Windows.Win32.PInvoke.CreateRemoteThread(
new HANDLE(this.targetProcess.Handle.ToPointer()),
null,
UIntPtr.Zero,
methodAddress,
parameterAddress,
CreateThreadFlags.RunImmediately,
out _);
(delegate* unmanaged[Stdcall]<void*, uint>)methodAddress,
parameterAddress.ToPointer(),
0, // Run immediately
null);
if (threadHandle == IntPtr.Zero)
throw new Exception($"CreateRemoteThread failure: {Marshal.GetLastWin32Error()}");
_ = WaitForSingleObject(threadHandle, uint.MaxValue);
_ = Windows.Win32.PInvoke.WaitForSingleObject(threadHandle, uint.MaxValue);
GetExitCodeThread(threadHandle, out exitCode);
uint exitCode = 0;
if (!Windows.Win32.PInvoke.GetExitCodeThread(threadHandle, &exitCode))
throw new Exception($"GetExitCodeThread failure: {Marshal.GetLastWin32Error()}");
CloseHandle(threadHandle);
Windows.Win32.PInvoke.CloseHandle(threadHandle);
return exitCode;
}
private void SetupLoadLibrary(ProcessModule kernel32Module, ExportFunction[] kernel32Exports)

View file

@ -1,956 +0,0 @@
using System;
using System.Diagnostics.CodeAnalysis;
using System.Runtime.InteropServices;
namespace Dalamud.Injector
{
/// <summary>
/// Native user32 functions.
/// </summary>
internal static partial class NativeFunctions
{
/// <summary>
/// MB_* from winuser.
/// </summary>
public enum MessageBoxType : uint
{
/// <summary>
/// The default value for any of the various subtypes.
/// </summary>
DefaultValue = 0x0,
// To indicate the buttons displayed in the message box, specify one of the following values.
/// <summary>
/// The message box contains three push buttons: Abort, Retry, and Ignore.
/// </summary>
AbortRetryIgnore = 0x2,
/// <summary>
/// The message box contains three push buttons: Cancel, Try Again, Continue. Use this message box type instead
/// of MB_ABORTRETRYIGNORE.
/// </summary>
CancelTryContinue = 0x6,
/// <summary>
/// Adds a Help button to the message box. When the user clicks the Help button or presses F1, the system sends
/// a WM_HELP message to the owner.
/// </summary>
Help = 0x4000,
/// <summary>
/// The message box contains one push button: OK. This is the default.
/// </summary>
Ok = DefaultValue,
/// <summary>
/// The message box contains two push buttons: OK and Cancel.
/// </summary>
OkCancel = 0x1,
/// <summary>
/// The message box contains two push buttons: Retry and Cancel.
/// </summary>
RetryCancel = 0x5,
/// <summary>
/// The message box contains two push buttons: Yes and No.
/// </summary>
YesNo = 0x4,
/// <summary>
/// The message box contains three push buttons: Yes, No, and Cancel.
/// </summary>
YesNoCancel = 0x3,
// To display an icon in the message box, specify one of the following values.
/// <summary>
/// An exclamation-point icon appears in the message box.
/// </summary>
IconExclamation = 0x30,
/// <summary>
/// An exclamation-point icon appears in the message box.
/// </summary>
IconWarning = IconExclamation,
/// <summary>
/// An icon consisting of a lowercase letter i in a circle appears in the message box.
/// </summary>
IconInformation = 0x40,
/// <summary>
/// An icon consisting of a lowercase letter i in a circle appears in the message box.
/// </summary>
IconAsterisk = IconInformation,
/// <summary>
/// A question-mark icon appears in the message box.
/// The question-mark message icon is no longer recommended because it does not clearly represent a specific type
/// of message and because the phrasing of a message as a question could apply to any message type. In addition,
/// users can confuse the message symbol question mark with Help information. Therefore, do not use this question
/// mark message symbol in your message boxes. The system continues to support its inclusion only for backward
/// compatibility.
/// </summary>
IconQuestion = 0x20,
/// <summary>
/// A stop-sign icon appears in the message box.
/// </summary>
IconStop = 0x10,
/// <summary>
/// A stop-sign icon appears in the message box.
/// </summary>
IconError = IconStop,
/// <summary>
/// A stop-sign icon appears in the message box.
/// </summary>
IconHand = IconStop,
// To indicate the default button, specify one of the following values.
/// <summary>
/// The first button is the default button.
/// MB_DEFBUTTON1 is the default unless MB_DEFBUTTON2, MB_DEFBUTTON3, or MB_DEFBUTTON4 is specified.
/// </summary>
DefButton1 = DefaultValue,
/// <summary>
/// The second button is the default button.
/// </summary>
DefButton2 = 0x100,
/// <summary>
/// The third button is the default button.
/// </summary>
DefButton3 = 0x200,
/// <summary>
/// The fourth button is the default button.
/// </summary>
DefButton4 = 0x300,
// To indicate the modality of the dialog box, specify one of the following values.
/// <summary>
/// The user must respond to the message box before continuing work in the window identified by the hWnd parameter.
/// However, the user can move to the windows of other threads and work in those windows. Depending on the hierarchy
/// of windows in the application, the user may be able to move to other windows within the thread. All child windows
/// of the parent of the message box are automatically disabled, but pop-up windows are not. MB_APPLMODAL is the
/// default if neither MB_SYSTEMMODAL nor MB_TASKMODAL is specified.
/// </summary>
ApplModal = DefaultValue,
/// <summary>
/// Same as MB_APPLMODAL except that the message box has the WS_EX_TOPMOST style.
/// Use system-modal message boxes to notify the user of serious, potentially damaging errors that require immediate
/// attention (for example, running out of memory). This flag has no effect on the user's ability to interact with
/// windows other than those associated with hWnd.
/// </summary>
SystemModal = 0x1000,
/// <summary>
/// Same as MB_APPLMODAL except that all the top-level windows belonging to the current thread are disabled if the
/// hWnd parameter is NULL. Use this flag when the calling application or library does not have a window handle
/// available but still needs to prevent input to other windows in the calling thread without suspending other threads.
/// </summary>
TaskModal = 0x2000,
// To specify other options, use one or more of the following values.
/// <summary>
/// Same as desktop of the interactive window station. For more information, see Window Stations. If the current
/// input desktop is not the default desktop, MessageBox does not return until the user switches to the default
/// desktop.
/// </summary>
DefaultDesktopOnly = 0x20000,
/// <summary>
/// The text is right-justified.
/// </summary>
Right = 0x80000,
/// <summary>
/// Displays message and caption text using right-to-left reading order on Hebrew and Arabic systems.
/// </summary>
RtlReading = 0x100000,
/// <summary>
/// The message box becomes the foreground window. Internally, the system calls the SetForegroundWindow function
/// for the message box.
/// </summary>
SetForeground = 0x10000,
/// <summary>
/// The message box is created with the WS_EX_TOPMOST window style.
/// </summary>
Topmost = 0x40000,
/// <summary>
/// The caller is a service notifying the user of an event. The function displays a message box on the current active
/// desktop, even if there is no user logged on to the computer.
/// </summary>
ServiceNotification = 0x200000,
}
/// <summary>
/// Displays a modal dialog box that contains a system icon, a set of buttons, and a brief application-specific message,
/// such as status or error information. The message box returns an integer value that indicates which button the user
/// clicked.
/// </summary>
/// <param name="hWnd">
/// A handle to the owner window of the message box to be created. If this parameter is NULL, the message box has no
/// owner window.
/// </param>
/// <param name="text">
/// The message to be displayed. If the string consists of more than one line, you can separate the lines using a carriage
/// return and/or linefeed character between each line.
/// </param>
/// <param name="caption">
/// The dialog box title. If this parameter is NULL, the default title is Error.</param>
/// <param name="type">
/// The contents and behavior of the dialog box. This parameter can be a combination of flags from the following groups
/// of flags.
/// </param>
/// <returns>
/// If a message box has a Cancel button, the function returns the IDCANCEL value if either the ESC key is pressed or
/// the Cancel button is selected. If the message box has no Cancel button, pressing ESC will no effect - unless an
/// MB_OK button is present. If an MB_OK button is displayed and the user presses ESC, the return value will be IDOK.
/// If the function fails, the return value is zero.To get extended error information, call GetLastError. If the function
/// succeeds, the return value is one of the ID* enum values.
/// </returns>
[DllImport("user32.dll", SetLastError = true, CharSet = CharSet.Unicode)]
public static extern int MessageBoxW(IntPtr hWnd, string text, string caption, MessageBoxType type);
}
/// <summary>
/// Native kernel32 functions.
/// </summary>
internal static partial class NativeFunctions
{
/// <summary>
/// MEM_* from memoryapi.
/// </summary>
[Flags]
public enum AllocationType
{
/// <summary>
/// To coalesce two adjacent placeholders, specify MEM_RELEASE | MEM_COALESCE_PLACEHOLDERS. When you coalesce
/// placeholders, lpAddress and dwSize must exactly match those of the placeholder.
/// </summary>
CoalescePlaceholders = 0x1,
/// <summary>
/// Frees an allocation back to a placeholder (after you've replaced a placeholder with a private allocation using
/// VirtualAlloc2 or Virtual2AllocFromApp). To split a placeholder into two placeholders, specify
/// MEM_RELEASE | MEM_PRESERVE_PLACEHOLDER.
/// </summary>
PreservePlaceholder = 0x2,
/// <summary>
/// Allocates memory charges (from the overall size of memory and the paging files on disk) for the specified reserved
/// memory pages. The function also guarantees that when the caller later initially accesses the memory, the contents
/// will be zero. Actual physical pages are not allocated unless/until the virtual addresses are actually accessed.
/// To reserve and commit pages in one step, call VirtualAllocEx with MEM_COMMIT | MEM_RESERVE. Attempting to commit
/// a specific address range by specifying MEM_COMMIT without MEM_RESERVE and a non-NULL lpAddress fails unless the
/// entire range has already been reserved. The resulting error code is ERROR_INVALID_ADDRESS. An attempt to commit
/// a page that is already committed does not cause the function to fail. This means that you can commit pages without
/// first determining the current commitment state of each page. If lpAddress specifies an address within an enclave,
/// flAllocationType must be MEM_COMMIT.
/// </summary>
Commit = 0x1000,
/// <summary>
/// Reserves a range of the process's virtual address space without allocating any actual physical storage in memory
/// or in the paging file on disk. You commit reserved pages by calling VirtualAllocEx again with MEM_COMMIT. To
/// reserve and commit pages in one step, call VirtualAllocEx with MEM_COMMIT | MEM_RESERVE. Other memory allocation
/// functions, such as malloc and LocalAlloc, cannot use reserved memory until it has been released.
/// </summary>
Reserve = 0x2000,
/// <summary>
/// Decommits the specified region of committed pages. After the operation, the pages are in the reserved state.
/// The function does not fail if you attempt to decommit an uncommitted page. This means that you can decommit
/// a range of pages without first determining the current commitment state. The MEM_DECOMMIT value is not supported
/// when the lpAddress parameter provides the base address for an enclave.
/// </summary>
Decommit = 0x4000,
/// <summary>
/// Releases the specified region of pages, or placeholder (for a placeholder, the address space is released and
/// available for other allocations). After this operation, the pages are in the free state. If you specify this
/// value, dwSize must be 0 (zero), and lpAddress must point to the base address returned by the VirtualAlloc function
/// when the region is reserved. The function fails if either of these conditions is not met. If any pages in the
/// region are committed currently, the function first decommits, and then releases them. The function does not
/// fail if you attempt to release pages that are in different states, some reserved and some committed. This means
/// that you can release a range of pages without first determining the current commitment state.
/// </summary>
Release = 0x8000,
/// <summary>
/// Indicates that data in the memory range specified by lpAddress and dwSize is no longer of interest. The pages
/// should not be read from or written to the paging file. However, the memory block will be used again later, so
/// it should not be decommitted. This value cannot be used with any other value. Using this value does not guarantee
/// that the range operated on with MEM_RESET will contain zeros. If you want the range to contain zeros, decommit
/// the memory and then recommit it. When you use MEM_RESET, the VirtualAllocEx function ignores the value of fProtect.
/// However, you must still set fProtect to a valid protection value, such as PAGE_NOACCESS. VirtualAllocEx returns
/// an error if you use MEM_RESET and the range of memory is mapped to a file. A shared view is only acceptable
/// if it is mapped to a paging file.
/// </summary>
Reset = 0x80000,
/// <summary>
/// MEM_RESET_UNDO should only be called on an address range to which MEM_RESET was successfully applied earlier.
/// It indicates that the data in the specified memory range specified by lpAddress and dwSize is of interest to
/// the caller and attempts to reverse the effects of MEM_RESET. If the function succeeds, that means all data in
/// the specified address range is intact. If the function fails, at least some of the data in the address range
/// has been replaced with zeroes. This value cannot be used with any other value. If MEM_RESET_UNDO is called on
/// an address range which was not MEM_RESET earlier, the behavior is undefined. When you specify MEM_RESET, the
/// VirtualAllocEx function ignores the value of flProtect. However, you must still set flProtect to a valid
/// protection value, such as PAGE_NOACCESS.
/// </summary>
ResetUndo = 0x1000000,
/// <summary>
/// Reserves an address range that can be used to map Address Windowing Extensions (AWE) pages. This value must
/// be used with MEM_RESERVE and no other values.
/// </summary>
Physical = 0x400000,
/// <summary>
/// Allocates memory at the highest possible address. This can be slower than regular allocations, especially when
/// there are many allocations.
/// </summary>
TopDown = 0x100000,
/// <summary>
/// Causes the system to track pages that are written to in the allocated region. If you specify this value, you
/// must also specify MEM_RESERVE. To retrieve the addresses of the pages that have been written to since the region
/// was allocated or the write-tracking state was reset, call the GetWriteWatch function. To reset the write-tracking
/// state, call GetWriteWatch or ResetWriteWatch. The write-tracking feature remains enabled for the memory region
/// until the region is freed.
/// </summary>
WriteWatch = 0x200000,
/// <summary>
/// Allocates memory using large page support. The size and alignment must be a multiple of the large-page minimum.
/// To obtain this value, use the GetLargePageMinimum function. If you specify this value, you must also specify
/// MEM_RESERVE and MEM_COMMIT.
/// </summary>
LargePages = 0x20000000,
}
/// <summary>
/// Unprefixed flags from CreateRemoteThread.
/// </summary>
[Flags]
public enum CreateThreadFlags
{
/// <summary>
/// The thread runs immediately after creation.
/// </summary>
RunImmediately = 0x0,
/// <summary>
/// The thread is created in a suspended state, and does not run until the ResumeThread function is called.
/// </summary>
CreateSuspended = 0x4,
/// <summary>
/// The dwStackSize parameter specifies the initial reserve size of the stack. If this flag is not specified, dwStackSize specifies the commit size.
/// </summary>
StackSizeParamIsReservation = 0x10000,
}
/// <summary>
/// DUPLICATE_* values for DuplicateHandle's dwDesiredAccess.
/// </summary>
[Flags]
public enum DuplicateOptions : uint
{
/// <summary>
/// Closes the source handle. This occurs regardless of any error status returned.
/// </summary>
CloseSource = 0x00000001,
/// <summary>
/// Ignores the dwDesiredAccess parameter. The duplicate handle has the same access as the source handle.
/// </summary>
SameAccess = 0x00000002,
}
/// <summary>
/// PAGE_* from memoryapi.
/// </summary>
[Flags]
public enum MemoryProtection
{
/// <summary>
/// Enables execute access to the committed region of pages. An attempt to write to the committed region results
/// in an access violation. This flag is not supported by the CreateFileMapping function.
/// </summary>
Execute = 0x10,
/// <summary>
/// Enables execute or read-only access to the committed region of pages. An attempt to write to the committed region
/// results in an access violation.
/// </summary>
ExecuteRead = 0x20,
/// <summary>
/// Enables execute, read-only, or read/write access to the committed region of pages.
/// </summary>
ExecuteReadWrite = 0x40,
/// <summary>
/// Enables execute, read-only, or copy-on-write access to a mapped view of a file mapping object. An attempt to
/// write to a committed copy-on-write page results in a private copy of the page being made for the process. The
/// private page is marked as PAGE_EXECUTE_READWRITE, and the change is written to the new page. This flag is not
/// supported by the VirtualAlloc or VirtualAllocEx functions.
/// </summary>
ExecuteWriteCopy = 0x80,
/// <summary>
/// Disables all access to the committed region of pages. An attempt to read from, write to, or execute the committed
/// region results in an access violation. This flag is not supported by the CreateFileMapping function.
/// </summary>
NoAccess = 0x01,
/// <summary>
/// Enables read-only access to the committed region of pages. An attempt to write to the committed region results
/// in an access violation. If Data Execution Prevention is enabled, an attempt to execute code in the committed
/// region results in an access violation.
/// </summary>
ReadOnly = 0x02,
/// <summary>
/// Enables read-only or read/write access to the committed region of pages. If Data Execution Prevention is enabled,
/// attempting to execute code in the committed region results in an access violation.
/// </summary>
ReadWrite = 0x04,
/// <summary>
/// Enables read-only or copy-on-write access to a mapped view of a file mapping object. An attempt to write to
/// a committed copy-on-write page results in a private copy of the page being made for the process. The private
/// page is marked as PAGE_READWRITE, and the change is written to the new page. If Data Execution Prevention is
/// enabled, attempting to execute code in the committed region results in an access violation. This flag is not
/// supported by the VirtualAlloc or VirtualAllocEx functions.
/// </summary>
WriteCopy = 0x08,
/// <summary>
/// Sets all locations in the pages as invalid targets for CFG. Used along with any execute page protection like
/// PAGE_EXECUTE, PAGE_EXECUTE_READ, PAGE_EXECUTE_READWRITE and PAGE_EXECUTE_WRITECOPY. Any indirect call to locations
/// in those pages will fail CFG checks and the process will be terminated. The default behavior for executable
/// pages allocated is to be marked valid call targets for CFG. This flag is not supported by the VirtualProtect
/// or CreateFileMapping functions.
/// </summary>
TargetsInvalid = 0x40000000,
/// <summary>
/// Pages in the region will not have their CFG information updated while the protection changes for VirtualProtect.
/// For example, if the pages in the region was allocated using PAGE_TARGETS_INVALID, then the invalid information
/// will be maintained while the page protection changes. This flag is only valid when the protection changes to
/// an executable type like PAGE_EXECUTE, PAGE_EXECUTE_READ, PAGE_EXECUTE_READWRITE and PAGE_EXECUTE_WRITECOPY.
/// The default behavior for VirtualProtect protection change to executable is to mark all locations as valid call
/// targets for CFG.
/// </summary>
TargetsNoUpdate = TargetsInvalid,
/// <summary>
/// Pages in the region become guard pages. Any attempt to access a guard page causes the system to raise a
/// STATUS_GUARD_PAGE_VIOLATION exception and turn off the guard page status. Guard pages thus act as a one-time
/// access alarm. For more information, see Creating Guard Pages. When an access attempt leads the system to turn
/// off guard page status, the underlying page protection takes over. If a guard page exception occurs during a
/// system service, the service typically returns a failure status indicator. This value cannot be used with
/// PAGE_NOACCESS. This flag is not supported by the CreateFileMapping function.
/// </summary>
Guard = 0x100,
/// <summary>
/// Sets all pages to be non-cachable. Applications should not use this attribute except when explicitly required
/// for a device. Using the interlocked functions with memory that is mapped with SEC_NOCACHE can result in an
/// EXCEPTION_ILLEGAL_INSTRUCTION exception. The PAGE_NOCACHE flag cannot be used with the PAGE_GUARD, PAGE_NOACCESS,
/// or PAGE_WRITECOMBINE flags. The PAGE_NOCACHE flag can be used only when allocating private memory with the
/// VirtualAlloc, VirtualAllocEx, or VirtualAllocExNuma functions. To enable non-cached memory access for shared
/// memory, specify the SEC_NOCACHE flag when calling the CreateFileMapping function.
/// </summary>
NoCache = 0x200,
/// <summary>
/// Sets all pages to be write-combined. Applications should not use this attribute except when explicitly required
/// for a device. Using the interlocked functions with memory that is mapped as write-combined can result in an
/// EXCEPTION_ILLEGAL_INSTRUCTION exception. The PAGE_WRITECOMBINE flag cannot be specified with the PAGE_NOACCESS,
/// PAGE_GUARD, and PAGE_NOCACHE flags. The PAGE_WRITECOMBINE flag can be used only when allocating private memory
/// with the VirtualAlloc, VirtualAllocEx, or VirtualAllocExNuma functions. To enable write-combined memory access
/// for shared memory, specify the SEC_WRITECOMBINE flag when calling the CreateFileMapping function.
/// </summary>
WriteCombine = 0x400,
}
/// <summary>
/// PROCESS_* from processthreadsapi.
/// </summary>
[Flags]
public enum ProcessAccessFlags : uint
{
/// <summary>
/// All possible access rights for a process object.
/// </summary>
AllAccess = 0x001F0FFF,
/// <summary>
/// Required to create a process.
/// </summary>
CreateProcess = 0x0080,
/// <summary>
/// Required to create a thread.
/// </summary>
CreateThread = 0x0002,
/// <summary>
/// Required to duplicate a handle using DuplicateHandle.
/// </summary>
DupHandle = 0x0040,
/// <summary>
/// Required to retrieve certain information about a process, such as its token, exit code,
/// and priority class (see OpenProcessToken).
/// </summary>
QueryInformation = 0x0400,
/// <summary>
/// Required to retrieve certain information about a process(see GetExitCodeProcess, GetPriorityClass, IsProcessInJob,
/// QueryFullProcessImageName). A handle that has the PROCESS_QUERY_INFORMATION access right is automatically granted
/// PROCESS_QUERY_LIMITED_INFORMATION.
/// </summary>
QueryLimitedInformation = 0x1000,
/// <summary>
/// Required to set certain information about a process, such as its priority class (see SetPriorityClass).
/// </summary>
SetInformation = 0x0200,
/// <summary>
/// Required to set memory limits using SetProcessWorkingSetSize.
/// </summary>
SetQuote = 0x0100,
/// <summary>
/// Required to suspend or resume a process.
/// </summary>
SuspendResume = 0x0800,
/// <summary>
/// Required to terminate a process using TerminateProcess.
/// </summary>
Terminate = 0x0001,
/// <summary>
/// Required to perform an operation on the address space of a process(see VirtualProtectEx and WriteProcessMemory).
/// </summary>
VmOperation = 0x0008,
/// <summary>
/// Required to read memory in a process using ReadProcessMemory.
/// </summary>
VmRead = 0x0010,
/// <summary>
/// Required to write to memory in a process using WriteProcessMemory.
/// </summary>
VmWrite = 0x0020,
/// <summary>
/// Required to wait for the process to terminate using the wait functions.
/// </summary>
Synchronize = 0x00100000,
}
/// <summary>
/// WAIT_* from synchapi.
/// </summary>
public enum WaitResult
{
/// <summary>
/// The specified object is a mutex object that was not released by the thread that owned the mutex object
/// before the owning thread terminated.Ownership of the mutex object is granted to the calling thread and
/// the mutex state is set to nonsignaled. If the mutex was protecting persistent state information, you
/// should check it for consistency.
/// </summary>
Abandoned = 0x80,
/// <summary>
/// The state of the specified object is signaled.
/// </summary>
Object0 = 0x0,
/// <summary>
/// The time-out interval elapsed, and the object's state is nonsignaled.
/// </summary>
Timeout = 0x102,
/// <summary>
/// The function has failed. To get extended error information, call GetLastError.
/// </summary>
WAIT_FAILED = 0xFFFFFFF,
}
/// <summary>
/// Closes an open object handle.
/// </summary>
/// <param name="hObject">
/// A valid handle to an open object.
/// </param>
/// <returns>
/// If the function succeeds, the return value is nonzero. If the function fails, the return value is zero. To get extended error
/// information, call GetLastError. If the application is running under a debugger, the function will throw an exception if it receives
/// either a handle value that is not valid or a pseudo-handle value. This can happen if you close a handle twice, or if you call
/// CloseHandle on a handle returned by the FindFirstFile function instead of calling the FindClose function.
/// </returns>
[DllImport("kernel32.dll", SetLastError = true)]
[return: MarshalAs(UnmanagedType.Bool)]
public static extern bool CloseHandle(IntPtr hObject);
/// <summary>
/// Creates a thread that runs in the virtual address space of another process. Use the CreateRemoteThreadEx function
/// to create a thread that runs in the virtual address space of another process and optionally specify extended attributes.
/// </summary>
/// <param name="hProcess">
/// A handle to the process in which the thread is to be created. The handle must have the PROCESS_CREATE_THREAD,
/// PROCESS_QUERY_INFORMATION, PROCESS_VM_OPERATION, PROCESS_VM_WRITE, and PROCESS_VM_READ access rights, and may fail without
/// these rights on certain platforms. For more information, see Process Security and Access Rights.
/// </param>
/// <param name="lpThreadAttributes">
/// A pointer to a SECURITY_ATTRIBUTES structure that specifies a security descriptor for the new thread and determines whether
/// child processes can inherit the returned handle. If lpThreadAttributes is NULL, the thread gets a default security descriptor
/// and the handle cannot be inherited. The access control lists (ACL) in the default security descriptor for a thread come from
/// the primary token of the creator.
/// </param>
/// <param name="dwStackSize">
/// The initial size of the stack, in bytes. The system rounds this value to the nearest page. If this parameter is 0 (zero), the
/// new thread uses the default size for the executable. For more information, see Thread Stack Size.
/// </param>
/// <param name="lpStartAddress">
/// A pointer to the application-defined function of type LPTHREAD_START_ROUTINE to be executed by the thread and represents the
/// starting address of the thread in the remote process. The function must exist in the remote process. For more information,
/// see ThreadProc.
/// </param>
/// <param name="lpParameter">
/// A pointer to a variable to be passed to the thread function.
/// </param>
/// <param name="dwCreationFlags">
/// The flags that control the creation of the thread.
/// </param>
/// <param name="lpThreadId">
/// A pointer to a variable that receives the thread identifier. If this parameter is NULL, the thread identifier is not returned.
/// </param>
/// <returns>
/// If the function succeeds, the return value is a handle to the new thread. If the function fails, the return value is
/// NULL.To get extended error information, call GetLastError. Note that CreateRemoteThread may succeed even if lpStartAddress
/// points to data, code, or is not accessible. If the start address is invalid when the thread runs, an exception occurs, and
/// the thread terminates. Thread termination due to a invalid start address is handled as an error exit for the thread's process.
/// This behavior is similar to the asynchronous nature of CreateProcess, where the process is created even if it refers to
/// invalid or missing dynamic-link libraries (DLL).
/// </returns>
[DllImport("kernel32.dll", SetLastError = true)]
public static extern IntPtr CreateRemoteThread(
IntPtr hProcess,
IntPtr lpThreadAttributes,
UIntPtr dwStackSize,
nuint lpStartAddress,
nuint lpParameter,
CreateThreadFlags dwCreationFlags,
out uint lpThreadId);
/// <summary>
/// Retrieves the termination status of the specified thread.
/// </summary>
/// <param name="hThread">
/// A handle to the thread. The handle must have the THREAD_QUERY_INFORMATION or THREAD_QUERY_LIMITED_INFORMATION
/// access right.For more information, see Thread Security and Access Rights.
/// </param>
/// <param name="lpExitCode">
/// A pointer to a variable to receive the thread termination status.
/// </param>
/// <returns>
/// If the function succeeds, the return value is nonzero. If the function fails, the return value is zero. To get
/// extended error information, call GetLastError.
/// </returns>
[DllImport("kernel32.dll", SetLastError = true)]
[return: MarshalAs(UnmanagedType.Bool)]
public static extern bool GetExitCodeThread(IntPtr hThread, out uint lpExitCode);
/// <summary>
/// Opens an existing local process object.
/// </summary>
/// <param name="dwDesiredAccess">
/// The access to the process object. This access right is checked against the security descriptor for the process. This parameter can be one or
/// more of the process access rights. If the caller has enabled the SeDebugPrivilege privilege, the requested access is granted regardless of the
/// contents of the security descriptor.
/// </param>
/// <param name="bInheritHandle">
/// If this value is TRUE, processes created by this process will inherit the handle. Otherwise, the processes do not inherit this handle.
/// </param>
/// <param name="dwProcessId">
/// The identifier of the local process to be opened. If the specified process is the System Idle Process(0x00000000), the function fails and the
/// last error code is ERROR_INVALID_PARAMETER.If the specified process is the System process or one of the Client Server Run-Time Subsystem(CSRSS)
/// processes, this function fails and the last error code is ERROR_ACCESS_DENIED because their access restrictions prevent user-level code from
/// opening them. If you are using GetCurrentProcessId as an argument to this function, consider using GetCurrentProcess instead of OpenProcess, for
/// improved performance.
/// </param>
/// <returns>
/// If the function succeeds, the return value is an open handle to the specified process.
/// If the function fails, the return value is NULL.To get extended error information, call GetLastError.
/// </returns>
[DllImport("kernel32.dll", SetLastError = true)]
public static extern IntPtr OpenProcess(
ProcessAccessFlags dwDesiredAccess,
bool bInheritHandle,
int dwProcessId);
/// <summary>
/// See https://docs.microsoft.com/en-us/windows/win32/api/memoryapi/nf-memoryapi-virtualallocex.
/// Reserves, commits, or changes the state of a region of memory within the virtual address space of a specified process.
/// The function initializes the memory it allocates to zero. To specify the NUMA node for the physical memory, see
/// VirtualAllocExNuma.
/// </summary>
/// <param name="hProcess">
/// The handle to a process. The function allocates memory within the virtual address space of this process. The handle
/// must have the PROCESS_VM_OPERATION access right. For more information, see Process Security and Access Rights.
/// </param>
/// <param name="lpAddress">
/// The pointer that specifies a desired starting address for the region of pages that you want to allocate. If you
/// are reserving memory, the function rounds this address down to the nearest multiple of the allocation granularity.
/// If you are committing memory that is already reserved, the function rounds this address down to the nearest page
/// boundary. To determine the size of a page and the allocation granularity on the host computer, use the GetSystemInfo
/// function. If lpAddress is NULL, the function determines where to allocate the region. If this address is within
/// an enclave that you have not initialized by calling InitializeEnclave, VirtualAllocEx allocates a page of zeros
/// for the enclave at that address. The page must be previously uncommitted, and will not be measured with the EEXTEND
/// instruction of the Intel Software Guard Extensions programming model. If the address in within an enclave that you
/// initialized, then the allocation operation fails with the ERROR_INVALID_ADDRESS error.
/// </param>
/// <param name="dwSize">
/// The size of the region of memory to allocate, in bytes. If lpAddress is NULL, the function rounds dwSize up to the
/// next page boundary. If lpAddress is not NULL, the function allocates all pages that contain one or more bytes in
/// the range from lpAddress to lpAddress+dwSize. This means, for example, that a 2-byte range that straddles a page
/// boundary causes the function to allocate both pages.
/// </param>
/// <param name="flAllocationType">
/// The type of memory allocation. This parameter must contain one of the MEM_* enum values.
/// </param>
/// <param name="flProtect">
/// The memory protection for the region of pages to be allocated. If the pages are being committed, you can specify
/// any one of the memory protection constants.
/// </param>
/// <returns>
/// If the function succeeds, the return value is the base address of the allocated region of pages. If the function
/// fails, the return value is NULL.To get extended error information, call GetLastError.
/// </returns>
[DllImport("kernel32.dll", SetLastError = true, ExactSpelling = true)]
public static extern IntPtr VirtualAllocEx(
IntPtr hProcess,
IntPtr lpAddress,
int dwSize,
AllocationType flAllocationType,
MemoryProtection flProtect);
/// <summary>
/// See https://docs.microsoft.com/en-us/windows/win32/api/memoryapi/nf-memoryapi-virtualfreeex.
/// Releases, decommits, or releases and decommits a region of memory within the virtual address space of a specified
/// process.
/// </summary>
/// <param name="hProcess">
/// A handle to a process. The function frees memory within the virtual address space of the process. The handle must
/// have the PROCESS_VM_OPERATION access right.For more information, see Process Security and Access Rights.
/// </param>
/// <param name="lpAddress">
/// A pointer to the starting address of the region of memory to be freed. If the dwFreeType parameter is MEM_RELEASE,
/// lpAddress must be the base address returned by the VirtualAllocEx function when the region is reserved.
/// </param>
/// <param name="dwSize">
/// The size of the region of memory to free, in bytes. If the dwFreeType parameter is MEM_RELEASE, dwSize must be 0
/// (zero). The function frees the entire region that is reserved in the initial allocation call to VirtualAllocEx.
/// If dwFreeType is MEM_DECOMMIT, the function decommits all memory pages that contain one or more bytes in the range
/// from the lpAddress parameter to (lpAddress+dwSize). This means, for example, that a 2-byte region of memory that
/// straddles a page boundary causes both pages to be decommitted. If lpAddress is the base address returned by
/// VirtualAllocEx and dwSize is 0 (zero), the function decommits the entire region that is allocated by VirtualAllocEx.
/// After that, the entire region is in the reserved state.
/// </param>
/// <param name="dwFreeType">
/// The type of free operation. This parameter must be one of the MEM_* enum values.
/// </param>
/// <returns>
/// If the function succeeds, the return value is a nonzero value. If the function fails, the return value is 0 (zero).
/// To get extended error information, call GetLastError.
/// </returns>
[DllImport("kernel32.dll", SetLastError = true, ExactSpelling = true)]
public static extern bool VirtualFreeEx(
IntPtr hProcess,
IntPtr lpAddress,
int dwSize,
AllocationType dwFreeType);
/// <summary>
/// Waits until the specified object is in the signaled state or the time-out interval elapses. To enter an alertable wait
/// state, use the WaitForSingleObjectEx function.To wait for multiple objects, use WaitForMultipleObjects.
/// </summary>
/// <param name="hHandle">
/// A handle to the object. For a list of the object types whose handles can be specified, see the following Remarks section.
/// If this handle is closed while the wait is still pending, the function's behavior is undefined. The handle must have the
/// SYNCHRONIZE access right. For more information, see Standard Access Rights.
/// </param>
/// <param name="dwMilliseconds">
/// The time-out interval, in milliseconds. If a nonzero value is specified, the function waits until the object is signaled
/// or the interval elapses. If dwMilliseconds is zero, the function does not enter a wait state if the object is not signaled;
/// it always returns immediately. If dwMilliseconds is INFINITE, the function will return only when the object is signaled.
/// </param>
/// <returns>
/// If the function succeeds, the return value indicates the event that caused the function to return.
/// It can be one of the WaitResult values.
/// </returns>
[DllImport("kernel32.dll", SetLastError = true)]
public static extern uint WaitForSingleObject(IntPtr hHandle, uint dwMilliseconds);
/// <summary>
/// Writes data to an area of memory in a specified process. The entire area to be written to must be accessible or
/// the operation fails.
/// </summary>
/// <param name="hProcess">
/// A handle to the process memory to be modified. The handle must have PROCESS_VM_WRITE and PROCESS_VM_OPERATION access
/// to the process.
/// </param>
/// <param name="lpBaseAddress">
/// A pointer to the base address in the specified process to which data is written. Before data transfer occurs, the
/// system verifies that all data in the base address and memory of the specified size is accessible for write access,
/// and if it is not accessible, the function fails.
/// </param>
/// <param name="lpBuffer">
/// A pointer to the buffer that contains data to be written in the address space of the specified process.
/// </param>
/// <param name="dwSize">
/// The number of bytes to be written to the specified process.
/// </param>
/// <param name="lpNumberOfBytesWritten">
/// A pointer to a variable that receives the number of bytes transferred into the specified process. This parameter
/// is optional. If lpNumberOfBytesWritten is NULL, the parameter is ignored.
/// </param>
/// <returns>
/// If the function succeeds, the return value is nonzero. If the function fails, the return value is 0 (zero). To get
/// extended error information, call GetLastError.The function fails if the requested write operation crosses into an
/// area of the process that is inaccessible.
/// </returns>
[DllImport("kernel32.dll", SetLastError = true)]
public static extern bool WriteProcessMemory(
IntPtr hProcess,
IntPtr lpBaseAddress,
byte[] lpBuffer,
int dwSize,
out IntPtr lpNumberOfBytesWritten);
/// <summary>
/// Duplicates an object handle.
/// </summary>
/// <param name="hSourceProcessHandle">
/// A handle to the process with the handle to be duplicated.
///
/// The handle must have the PROCESS_DUP_HANDLE access right.
/// </param>
/// <param name="hSourceHandle">
/// The handle to be duplicated. This is an open object handle that is valid in the context of the source process.
/// For a list of objects whose handles can be duplicated, see the following Remarks section.
/// </param>
/// <param name="hTargetProcessHandle">
/// A handle to the process that is to receive the duplicated handle.
///
/// The handle must have the PROCESS_DUP_HANDLE access right.
/// </param>
/// <param name="lpTargetHandle">
/// A pointer to a variable that receives the duplicate handle. This handle value is valid in the context of the target process.
///
/// If hSourceHandle is a pseudo handle returned by GetCurrentProcess or GetCurrentThread, DuplicateHandle converts it to a real handle to a process or thread, respectively.
///
/// If lpTargetHandle is NULL, the function duplicates the handle, but does not return the duplicate handle value to the caller. This behavior exists only for backward compatibility with previous versions of this function. You should not use this feature, as you will lose system resources until the target process terminates.
///
/// This parameter is ignored if hTargetProcessHandle is NULL.
/// </param>
/// <param name="dwDesiredAccess">
/// The access requested for the new handle. For the flags that can be specified for each object type, see the following Remarks section.
///
/// This parameter is ignored if the dwOptions parameter specifies the DUPLICATE_SAME_ACCESS flag. Otherwise, the flags that can be specified depend on the type of object whose handle is to be duplicated.
///
/// This parameter is ignored if hTargetProcessHandle is NULL.
/// </param>
/// <param name="bInheritHandle">
/// A variable that indicates whether the handle is inheritable. If TRUE, the duplicate handle can be inherited by new processes created by the target process. If FALSE, the new handle cannot be inherited.
///
/// This parameter is ignored if hTargetProcessHandle is NULL.
/// </param>
/// <param name="dwOptions">
/// Optional actions.
/// </param>
/// <returns>
/// If the function succeeds, the return value is nonzero.
///
/// If the function fails, the return value is zero. To get extended error information, call GetLastError.
/// </returns>
/// <remarks>
/// See https://docs.microsoft.com/en-us/windows/win32/api/handleapi/nf-handleapi-duplicatehandle.
/// </remarks>
[DllImport("kernel32.dll", SetLastError = true)]
[return: MarshalAs(UnmanagedType.Bool)]
public static extern bool DuplicateHandle(
IntPtr hSourceProcessHandle,
IntPtr hSourceHandle,
IntPtr hTargetProcessHandle,
out IntPtr lpTargetHandle,
uint dwDesiredAccess,
[MarshalAs(UnmanagedType.Bool)] bool bInheritHandle,
DuplicateOptions dwOptions);
/// <summary>
/// See https://docs.microsoft.com/en-us/windows/win32/api/libloaderapi/nf-libloaderapi-getmodulehandlew.
/// Retrieves a module handle for the specified module. The module must have been loaded by the calling process. To
/// avoid the race conditions described in the Remarks section, use the GetModuleHandleEx function.
/// </summary>
/// <param name="lpModuleName">
/// The name of the loaded module (either a .dll or .exe file). If the file name extension is omitted, the default
/// library extension .dll is appended. The file name string can include a trailing point character (.) to indicate
/// that the module name has no extension. The string does not have to specify a path. When specifying a path, be sure
/// to use backslashes (\), not forward slashes (/). The name is compared (case independently) to the names of modules
/// currently mapped into the address space of the calling process. If this parameter is NULL, GetModuleHandle returns
/// a handle to the file used to create the calling process (.exe file). The GetModuleHandle function does not retrieve
/// handles for modules that were loaded using the LOAD_LIBRARY_AS_DATAFILE flag.For more information, see LoadLibraryEx.
/// </param>
/// <returns>
/// If the function succeeds, the return value is a handle to the specified module. If the function fails, the return
/// value is NULL.To get extended error information, call GetLastError.
/// </returns>
[DllImport("kernel32.dll", CharSet = CharSet.Unicode, ExactSpelling = true, SetLastError = true)]
public static extern IntPtr GetModuleHandleW(string lpModuleName);
/// <summary>
/// Retrieves the address of an exported function or variable from the specified dynamic-link library (DLL).
/// </summary>
/// <param name="hModule">
/// A handle to the DLL module that contains the function or variable. The LoadLibrary, LoadLibraryEx, LoadPackagedLibrary,
/// or GetModuleHandle function returns this handle. The GetProcAddress function does not retrieve addresses from modules
/// that were loaded using the LOAD_LIBRARY_AS_DATAFILE flag.For more information, see LoadLibraryEx.
/// </param>
/// <param name="procName">
/// The function or variable name, or the function's ordinal value. If this parameter is an ordinal value, it must be
/// in the low-order word; the high-order word must be zero.
/// </param>
/// <returns>
/// If the function succeeds, the return value is the address of the exported function or variable. If the function
/// fails, the return value is NULL.To get extended error information, call GetLastError.
/// </returns>
[DllImport("kernel32", CharSet = CharSet.Ansi, ExactSpelling = true, SetLastError = true)]
[SuppressMessage("Globalization", "CA2101:Specify marshaling for P/Invoke string arguments", Justification = "Ansi only")]
public static extern IntPtr GetProcAddress(IntPtr hModule, string procName);
}
}

View file

@ -0,0 +1,4 @@
{
"$schema": "https://aka.ms/CsWin32.schema.json",
"allowMarshaling": false
}

View file

@ -0,0 +1,8 @@
CreateRemoteThread
WaitForSingleObject
GetExitCodeThread
DuplicateHandle
MessageBox
GetModuleHandle
GetProcAddress

View file

@ -12,48 +12,33 @@ using System.Text.RegularExpressions;
using Dalamud.Common;
using Dalamud.Common.Game;
using Dalamud.Common.Util;
using Newtonsoft.Json;
using Reloaded.Memory.Buffers;
using Serilog;
using Serilog.Core;
using Serilog.Events;
using static Dalamud.Injector.NativeFunctions;
using Windows.Win32.Foundation;
using Windows.Win32.UI.WindowsAndMessaging;
namespace Dalamud.Injector
{
/// <summary>
/// Entrypoint to the program.
/// </summary>
public sealed class EntryPoint
public sealed class Program
{
/// <summary>
/// A delegate used during initialization of the CLR from Dalamud.Injector.Boot.
/// </summary>
/// <param name="argc">Count of arguments.</param>
/// <param name="argvPtr">char** string arguments.</param>
/// <returns>Return value (HRESULT).</returns>
public delegate int MainDelegate(int argc, IntPtr argvPtr);
/// <summary>
/// Start the Dalamud injector.
/// </summary>
/// <param name="argc">Count of arguments.</param>
/// <param name="argvPtr">byte** string arguments.</param>
/// <param name="argsArray">Command line arguments.</param>
/// <returns>Return value (HRESULT).</returns>
public static int Main(int argc, IntPtr argvPtr)
public static int Main(string[] argsArray)
{
try
{
List<string> args = new(argc);
unsafe
{
var argv = (IntPtr*)argvPtr;
for (var i = 0; i < argc; i++)
args.Add(Marshal.PtrToStringUni(argv[i]));
}
// API14 TODO: Refactor
var args = argsArray.ToList();
args.Insert(0, Assembly.GetExecutingAssembly().Location);
Init(args);
args.Remove("-v"); // Remove "verbose" flag
@ -268,9 +253,9 @@ namespace Dalamud.Injector
private static OSPlatform DetectPlatformHeuristic()
{
var ntdll = NativeFunctions.GetModuleHandleW("ntdll.dll");
var wineServerCallPtr = NativeFunctions.GetProcAddress(ntdll, "wine_server_call");
var wineGetHostVersionPtr = NativeFunctions.GetProcAddress(ntdll, "wine_get_host_version");
var ntdll = Windows.Win32.PInvoke.GetModuleHandle("ntdll.dll");
var wineServerCallPtr = Windows.Win32.PInvoke.GetProcAddress(ntdll, "wine_server_call");
var wineGetHostVersionPtr = Windows.Win32.PInvoke.GetProcAddress(ntdll, "wine_get_host_version");
var winePlatform = GetWinePlatform(wineGetHostVersionPtr);
var isWine = wineServerCallPtr != nint.Zero;
@ -306,6 +291,7 @@ namespace Dalamud.Injector
var configurationPath = startInfo.ConfigurationPath;
var pluginDirectory = startInfo.PluginDirectory;
var assetDirectory = startInfo.AssetDirectory;
var tempDirectory = startInfo.TempDirectory;
var delayInitializeMs = startInfo.DelayInitializeMs;
var logName = startInfo.LogName;
var logPath = startInfo.LogPath;
@ -336,6 +322,10 @@ namespace Dalamud.Injector
{
assetDirectory = args[i][key.Length..];
}
else if (args[i].StartsWith(key = "--dalamud-temp-directory="))
{
tempDirectory = args[i][key.Length..];
}
else if (args[i].StartsWith(key = "--dalamud-delay-initialize="))
{
delayInitializeMs = int.Parse(args[i][key.Length..]);
@ -448,6 +438,7 @@ namespace Dalamud.Injector
startInfo.ConfigurationPath = configurationPath;
startInfo.PluginDirectory = pluginDirectory;
startInfo.AssetDirectory = assetDirectory;
startInfo.TempDirectory = tempDirectory;
startInfo.Language = clientLanguage;
startInfo.Platform = platform;
startInfo.DelayInitializeMs = delayInitializeMs;
@ -621,10 +612,13 @@ namespace Dalamud.Injector
if (warnManualInjection)
{
var result = MessageBoxW(IntPtr.Zero, $"Take care: you are manually injecting Dalamud into FFXIV({string.Join(", ", processes.Select(x => $"{x.Id}"))}).\n\nIf you are doing this to use plugins before they are officially whitelisted on patch days, things may go wrong and you may get into trouble.\nWe discourage you from doing this and you won't be warned again in-game.", "Dalamud", MessageBoxType.IconWarning | MessageBoxType.OkCancel);
var result = Windows.Win32.PInvoke.MessageBox(
HWND.Null,
$"Take care: you are manually injecting Dalamud into FFXIV({string.Join(", ", processes.Select(x => $"{x.Id}"))}).\n\nIf you are doing this to use plugins before they are officially whitelisted on patch days, things may go wrong and you may get into trouble.\nWe discourage you from doing this and you won't be warned again in-game.",
"Dalamud",
MESSAGEBOX_STYLE.MB_ICONWARNING | MESSAGEBOX_STYLE.MB_OKCANCEL);
// IDCANCEL
if (result == 2)
if (result == MESSAGEBOX_RESULT.IDCANCEL)
{
Log.Information("User cancelled injection");
return -2;
@ -934,30 +928,48 @@ namespace Dalamud.Injector
Inject(process, startInfo, false);
}
var processHandleForOwner = IntPtr.Zero;
var processHandleForOwner = HANDLE.Null;
if (handleOwner != IntPtr.Zero)
{
if (!DuplicateHandle(Process.GetCurrentProcess().Handle, process.Handle, handleOwner, out processHandleForOwner, 0, false, DuplicateOptions.SameAccess))
unsafe
{
Log.Warning("Failed to call DuplicateHandle: Win32 error code {0}", Marshal.GetLastWin32Error());
if (!Windows.Win32.PInvoke.DuplicateHandle(
new HANDLE(Process.GetCurrentProcess().Handle.ToPointer()),
new HANDLE(process.Handle.ToPointer()),
new HANDLE(handleOwner),
&processHandleForOwner,
0,
false,
DUPLICATE_HANDLE_OPTIONS.DUPLICATE_SAME_ACCESS))
{
Log.Warning("Failed to call DuplicateHandle: Win32 error code {0}", Marshal.GetLastWin32Error());
}
}
}
Console.WriteLine($"{{\"pid\": {process.Id}, \"handle\": {processHandleForOwner}}}");
Console.WriteLine($"{{\"pid\": {process.Id}, \"handle\": {(IntPtr)processHandleForOwner}}}");
Log.CloseAndFlush();
return 0;
}
private static Process GetInheritableCurrentProcessHandle()
private static unsafe Process GetInheritableCurrentProcessHandle()
{
if (!DuplicateHandle(Process.GetCurrentProcess().Handle, Process.GetCurrentProcess().Handle, Process.GetCurrentProcess().Handle, out var inheritableCurrentProcessHandle, 0, true, DuplicateOptions.SameAccess))
var currentProcessHandle = new HANDLE(Process.GetCurrentProcess().Handle.ToPointer());
var inheritableHandle = HANDLE.Null;
if (!Windows.Win32.PInvoke.DuplicateHandle(
currentProcessHandle,
currentProcessHandle,
currentProcessHandle,
&inheritableHandle,
0,
true,
DUPLICATE_HANDLE_OPTIONS.DUPLICATE_SAME_ACCESS))
{
Log.Error("Failed to call DuplicateHandle: Win32 error code {0}", Marshal.GetLastWin32Error());
return null;
throw new Win32Exception("Failed to call DuplicateHandle");
}
return new ExistingProcess(inheritableCurrentProcessHandle);
return new ExistingProcess(inheritableHandle);
}
private static int ProcessLaunchTestCommand(List<string> args)
@ -1048,13 +1060,13 @@ namespace Dalamud.Injector
}
injector.GetFunctionAddress(bootModule, "Initialize", out var initAddress);
injector.CallRemoteFunction(initAddress, startInfoAddress, out var exitCode);
var exitCode = injector.CallRemoteFunction(initAddress, startInfoAddress);
// ======================================================
if (exitCode > 0)
{
Log.Error($"Dalamud.Boot::Initialize returned {exitCode}");
Log.Error("Dalamud.Boot::Initialize returned {ExitCode}", exitCode);
return;
}

View file

Before

Width:  |  Height:  |  Size: 52 KiB

After

Width:  |  Height:  |  Size: 52 KiB

Before After
Before After

View file

@ -42,19 +42,19 @@
</ItemGroup>
<ItemGroup>
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.10.0" />
<PackageReference Include="xunit" Version="2.4.1" />
<PackageReference Include="xunit.abstractions" Version="2.0.3" />
<PackageReference Include="xunit.analyzers" Version="0.10.0" />
<PackageReference Include="xunit.assert" Version="2.4.1" />
<PackageReference Include="xunit.core" Version="2.4.1" />
<PackageReference Include="xunit.extensibility.core" Version="2.4.1" />
<PackageReference Include="xunit.extensibility.execution" Version="2.4.1" />
<PackageReference Include="xunit.runner.console" Version="2.4.1">
<PackageReference Include="Microsoft.NET.Test.Sdk" />
<PackageReference Include="xunit" />
<PackageReference Include="xunit.abstractions" />
<PackageReference Include="xunit.analyzers" />
<PackageReference Include="xunit.assert" />
<PackageReference Include="xunit.core" />
<PackageReference Include="xunit.extensibility.core" />
<PackageReference Include="xunit.extensibility.execution" />
<PackageReference Include="xunit.runner.console">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="xunit.runner.visualstudio" Version="2.4.3">
<PackageReference Include="xunit.runner.visualstudio">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>

View file

@ -1,6 +1,10 @@
using System;
using System;
using System.IO;
using Dalamud.Configuration;
using Dalamud.Game.Text.SeStringHandling;
using Dalamud.Game.Text.SeStringHandling.Payloads;
using Xunit;
namespace Dalamud.Test.Game.Text.SeStringHandling
@ -50,19 +54,41 @@ namespace Dalamud.Test.Game.Text.SeStringHandling
var config = new MockConfig { Text = seString };
PluginConfigurations.SerializeConfig(config);
}
[Fact]
public void TestConfigDeserializable()
{
var builder = new SeStringBuilder();
var seString = builder.AddText("Some text").Build();
var config = new MockConfig { Text = seString };
// This relies on the type information being maintained, which is why we're using these
// static methods instead of default serialization/deserialization.
var configSerialized = PluginConfigurations.SerializeConfig(config);
var configDeserialized = (MockConfig)PluginConfigurations.DeserializeConfig(configSerialized);
Assert.Equal(config, configDeserialized);
}
[Theory]
[InlineData(49, 209)]
[InlineData(71, 7)]
[InlineData(62, 116)]
public void TestAutoTranslatePayloadReencode(uint group, uint key)
{
var payload = new AutoTranslatePayload(group, key);
Assert.Equal(group, payload.Group);
Assert.Equal(key, payload.Key);
var encoded = payload.Encode();
using var stream = new MemoryStream(encoded);
using var reader = new BinaryReader(stream);
var decodedPayload = Payload.Decode(reader) as AutoTranslatePayload;
Assert.Equal(group, decodedPayload.Group);
Assert.Equal(key, decodedPayload.Key);
Assert.Equal(encoded, decodedPayload.Encode());
}
}
}

View file

@ -31,19 +31,19 @@ public class ReliableFileStorageTests
.Select(
i => Parallel.ForEachAsync(
Enumerable.Range(1, 100),
(j, _) =>
async (j, _) =>
{
if (i % 2 == 0)
{
// ReSharper disable once AccessToDisposedClosure
rfs.Instance.WriteAllText(tempFile, j.ToString());
await rfs.Instance.WriteAllTextAsync(tempFile, j.ToString());
}
else if (i % 3 == 0)
{
try
{
// ReSharper disable once AccessToDisposedClosure
rfs.Instance.ReadAllText(tempFile);
await rfs.Instance.ReadAllTextAsync(tempFile);
}
catch (FileNotFoundException)
{
@ -54,8 +54,6 @@ public class ReliableFileStorageTests
{
File.Delete(tempFile);
}
return ValueTask.CompletedTask;
})));
}
@ -112,41 +110,41 @@ public class ReliableFileStorageTests
}
[Fact]
public void Exists_WhenFileInBackup_ReturnsTrue()
public async Task Exists_WhenFileInBackup_ReturnsTrue()
{
var tempFile = Path.Combine(CreateTempDir(), TestFileName);
using var rfs = CreateRfs();
rfs.Instance.WriteAllText(tempFile, TestFileContent1);
await rfs.Instance.WriteAllTextAsync(tempFile, TestFileContent1);
File.Delete(tempFile);
Assert.True(rfs.Instance.Exists(tempFile));
}
[Fact]
public void Exists_WhenFileInBackup_WithDifferentContainerId_ReturnsFalse()
public async Task Exists_WhenFileInBackup_WithDifferentContainerId_ReturnsFalse()
{
var tempFile = Path.Combine(CreateTempDir(), TestFileName);
using var rfs = CreateRfs();
rfs.Instance.WriteAllText(tempFile, TestFileContent1);
await rfs.Instance.WriteAllTextAsync(tempFile, TestFileContent1);
File.Delete(tempFile);
Assert.False(rfs.Instance.Exists(tempFile, Guid.NewGuid()));
}
[Fact]
public void WriteAllText_ThrowsIfPathIsEmpty()
public async Task WriteAllText_ThrowsIfPathIsEmpty()
{
using var rfs = CreateRfs();
Assert.Throws<ArgumentException>(() => rfs.Instance.WriteAllText("", TestFileContent1));
await Assert.ThrowsAsync<ArgumentException>(async () => await rfs.Instance.WriteAllTextAsync("", TestFileContent1));
}
[Fact]
public void WriteAllText_ThrowsIfPathIsNull()
public async Task WriteAllText_ThrowsIfPathIsNull()
{
using var rfs = CreateRfs();
Assert.Throws<ArgumentNullException>(() => rfs.Instance.WriteAllText(null!, TestFileContent1));
await Assert.ThrowsAsync<ArgumentNullException>(async () => await rfs.Instance.WriteAllTextAsync(null!, TestFileContent1));
}
[Fact]
@ -155,26 +153,26 @@ public class ReliableFileStorageTests
var tempFile = Path.Combine(CreateTempDir(), TestFileName);
using var rfs = CreateRfs();
rfs.Instance.WriteAllText(tempFile, TestFileContent1);
await rfs.Instance.WriteAllTextAsync(tempFile, TestFileContent1);
Assert.True(File.Exists(tempFile));
Assert.Equal(TestFileContent1, rfs.Instance.ReadAllText(tempFile, forceBackup: true));
Assert.Equal(TestFileContent1, await rfs.Instance.ReadAllTextAsync(tempFile, forceBackup: true));
Assert.Equal(TestFileContent1, await File.ReadAllTextAsync(tempFile));
}
[Fact]
public void WriteAllText_SeparatesContainers()
public async Task WriteAllText_SeparatesContainers()
{
var tempFile = Path.Combine(CreateTempDir(), TestFileName);
var containerId = Guid.NewGuid();
using var rfs = CreateRfs();
rfs.Instance.WriteAllText(tempFile, TestFileContent1);
rfs.Instance.WriteAllText(tempFile, TestFileContent2, containerId);
await rfs.Instance.WriteAllTextAsync(tempFile, TestFileContent1);
await rfs.Instance.WriteAllTextAsync(tempFile, TestFileContent2, containerId);
File.Delete(tempFile);
Assert.Equal(TestFileContent1, rfs.Instance.ReadAllText(tempFile, forceBackup: true));
Assert.Equal(TestFileContent2, rfs.Instance.ReadAllText(tempFile, forceBackup: true, containerId));
Assert.Equal(TestFileContent1, await rfs.Instance.ReadAllTextAsync(tempFile, forceBackup: true));
Assert.Equal(TestFileContent2, await rfs.Instance.ReadAllTextAsync(tempFile, forceBackup: true, containerId));
}
[Fact]
@ -183,7 +181,7 @@ public class ReliableFileStorageTests
var tempFile = Path.Combine(CreateTempDir(), TestFileName);
using var rfs = CreateFailedRfs();
rfs.Instance.WriteAllText(tempFile, TestFileContent1);
await rfs.Instance.WriteAllTextAsync(tempFile, TestFileContent1);
Assert.True(File.Exists(tempFile));
Assert.Equal(TestFileContent1, await File.ReadAllTextAsync(tempFile));
@ -195,38 +193,38 @@ public class ReliableFileStorageTests
var tempFile = Path.Combine(CreateTempDir(), TestFileName);
using var rfs = CreateRfs();
rfs.Instance.WriteAllText(tempFile, TestFileContent1);
rfs.Instance.WriteAllText(tempFile, TestFileContent2);
await rfs.Instance.WriteAllTextAsync(tempFile, TestFileContent1);
await rfs.Instance.WriteAllTextAsync(tempFile, TestFileContent2);
Assert.True(File.Exists(tempFile));
Assert.Equal(TestFileContent2, rfs.Instance.ReadAllText(tempFile, forceBackup: true));
Assert.Equal(TestFileContent2, await rfs.Instance.ReadAllTextAsync(tempFile, forceBackup: true));
Assert.Equal(TestFileContent2, await File.ReadAllTextAsync(tempFile));
}
[Fact]
public void WriteAllText_SupportsNullContent()
public async Task WriteAllText_SupportsNullContent()
{
var tempFile = Path.Combine(CreateTempDir(), TestFileName);
using var rfs = CreateRfs();
rfs.Instance.WriteAllText(tempFile, null);
await rfs.Instance.WriteAllTextAsync(tempFile, null);
Assert.True(File.Exists(tempFile));
Assert.Equal("", rfs.Instance.ReadAllText(tempFile));
Assert.Equal("", await rfs.Instance.ReadAllTextAsync(tempFile));
}
[Fact]
public void ReadAllText_ThrowsIfPathIsEmpty()
public async Task ReadAllText_ThrowsIfPathIsEmpty()
{
using var rfs = CreateRfs();
Assert.Throws<ArgumentException>(() => rfs.Instance.ReadAllText(""));
await Assert.ThrowsAsync<ArgumentException>(async () => await rfs.Instance.ReadAllTextAsync(""));
}
[Fact]
public void ReadAllText_ThrowsIfPathIsNull()
public async Task ReadAllText_ThrowsIfPathIsNull()
{
using var rfs = CreateRfs();
Assert.Throws<ArgumentNullException>(() => rfs.Instance.ReadAllText(null!));
await Assert.ThrowsAsync<ArgumentNullException>(async () => await rfs.Instance.ReadAllTextAsync(null!));
}
[Fact]
@ -236,40 +234,40 @@ public class ReliableFileStorageTests
await File.WriteAllTextAsync(tempFile, TestFileContent1);
using var rfs = CreateRfs();
Assert.Equal(TestFileContent1, rfs.Instance.ReadAllText(tempFile));
Assert.Equal(TestFileContent1, await rfs.Instance.ReadAllTextAsync(tempFile));
}
[Fact]
public void ReadAllText_WhenFileMissingWithBackup_ReturnsContent()
public async Task ReadAllText_WhenFileMissingWithBackup_ReturnsContent()
{
var tempFile = Path.Combine(CreateTempDir(), TestFileName);
using var rfs = CreateRfs();
rfs.Instance.WriteAllText(tempFile, TestFileContent1);
await rfs.Instance.WriteAllTextAsync(tempFile, TestFileContent1);
File.Delete(tempFile);
Assert.Equal(TestFileContent1, rfs.Instance.ReadAllText(tempFile));
Assert.Equal(TestFileContent1, await rfs.Instance.ReadAllTextAsync(tempFile));
}
[Fact]
public void ReadAllText_WhenFileMissingWithBackup_ThrowsWithDifferentContainerId()
public async Task ReadAllText_WhenFileMissingWithBackup_ThrowsWithDifferentContainerId()
{
var tempFile = Path.Combine(CreateTempDir(), TestFileName);
var containerId = Guid.NewGuid();
using var rfs = CreateRfs();
rfs.Instance.WriteAllText(tempFile, TestFileContent1);
await rfs.Instance.WriteAllTextAsync(tempFile, TestFileContent1);
File.Delete(tempFile);
Assert.Throws<FileNotFoundException>(() => rfs.Instance.ReadAllText(tempFile, containerId: containerId));
await Assert.ThrowsAsync<FileNotFoundException>(async () => await rfs.Instance.ReadAllTextAsync(tempFile, containerId: containerId));
}
[Fact]
public void ReadAllText_WhenFileMissing_ThrowsIfDbFailed()
public async Task ReadAllText_WhenFileMissing_ThrowsIfDbFailed()
{
var tempFile = Path.Combine(CreateTempDir(), TestFileName);
using var rfs = CreateFailedRfs();
Assert.Throws<FileNotFoundException>(() => rfs.Instance.ReadAllText(tempFile));
await Assert.ThrowsAsync<FileNotFoundException>(async () => await rfs.Instance.ReadAllTextAsync(tempFile));
}
[Fact]
@ -278,7 +276,7 @@ public class ReliableFileStorageTests
var tempFile = Path.Combine(CreateTempDir(), TestFileName);
await File.WriteAllTextAsync(tempFile, TestFileContent1);
using var rfs = CreateRfs();
rfs.Instance.ReadAllText(tempFile, text => Assert.Equal(TestFileContent1, text));
await rfs.Instance.ReadAllTextAsync(tempFile, text => Assert.Equal(TestFileContent1, text));
}
[Fact]
@ -290,7 +288,7 @@ public class ReliableFileStorageTests
var readerCalledOnce = false;
using var rfs = CreateRfs();
Assert.Throws<FileReadException>(() => rfs.Instance.ReadAllText(tempFile, Reader));
await Assert.ThrowsAsync<FileReadException>(async () => await rfs.Instance.ReadAllTextAsync(tempFile, Reader));
return;
@ -303,7 +301,7 @@ public class ReliableFileStorageTests
}
[Fact]
public void ReadAllText_WithReader_WhenReaderThrows_ReadsContentFromBackup()
public async Task ReadAllText_WithReader_WhenReaderThrows_ReadsContentFromBackup()
{
var tempFile = Path.Combine(CreateTempDir(), TestFileName);
@ -311,10 +309,10 @@ public class ReliableFileStorageTests
var assertionCalled = false;
using var rfs = CreateRfs();
rfs.Instance.WriteAllText(tempFile, TestFileContent1);
await rfs.Instance.WriteAllTextAsync(tempFile, TestFileContent1);
File.Delete(tempFile);
rfs.Instance.ReadAllText(tempFile, Reader);
await rfs.Instance.ReadAllTextAsync(tempFile, Reader);
Assert.True(assertionCalled);
return;
@ -335,17 +333,17 @@ public class ReliableFileStorageTests
var tempFile = Path.Combine(CreateTempDir(), TestFileName);
await File.WriteAllTextAsync(tempFile, TestFileContent1);
using var rfs = CreateRfs();
Assert.Throws<FileNotFoundException>(() => rfs.Instance.ReadAllText(tempFile, _ => throw new FileNotFoundException()));
await Assert.ThrowsAsync<FileNotFoundException>(async () => await rfs.Instance.ReadAllTextAsync(tempFile, _ => throw new FileNotFoundException()));
}
[Theory]
[InlineData(true)]
[InlineData(false)]
public void ReadAllText_WhenFileDoesNotExist_Throws(bool forceBackup)
public async Task ReadAllText_WhenFileDoesNotExist_Throws(bool forceBackup)
{
var tempFile = Path.Combine(CreateTempDir(), TestFileName);
using var rfs = CreateRfs();
Assert.Throws<FileNotFoundException>(() => rfs.Instance.ReadAllText(tempFile, forceBackup));
await Assert.ThrowsAsync<FileNotFoundException>(async () => await rfs.Instance.ReadAllTextAsync(tempFile, forceBackup));
}
private static DisposableReliableFileStorage CreateRfs()

View file

@ -1,4 +1,4 @@
Microsoft Visual Studio Solution File, Format Version 12.00
Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio Version 17
VisualStudioVersion = 17.1.32319.34
MinimumVisualStudioVersion = 10.0.40219.1
@ -7,10 +7,9 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution
.editorconfig = .editorconfig
.gitignore = .gitignore
tools\BannedSymbols.txt = tools\BannedSymbols.txt
targets\Dalamud.Plugin.Bootstrap.targets = targets\Dalamud.Plugin.Bootstrap.targets
targets\Dalamud.Plugin.targets = targets\Dalamud.Plugin.targets
tools\dalamud.ruleset = tools\dalamud.ruleset
Directory.Build.props = Directory.Build.props
Directory.Packages.props = Directory.Packages.props
EndProjectSection
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "build", "build\build.csproj", "{94E5B016-02B1-459B-97D9-E783F28764B2}"
@ -26,8 +25,6 @@ Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "Dalamud.Boot", "Dalamud.Boo
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Dalamud.Injector", "Dalamud.Injector\Dalamud.Injector.csproj", "{5B832F73-5F54-4ADC-870F-D0095EF72C9A}"
EndProject
Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "Dalamud.Injector.Boot", "Dalamud.Injector.Boot\Dalamud.Injector.Boot.vcxproj", "{8874326B-E755-4D13-90B4-59AB263A3E6B}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Dalamud.Test", "Dalamud.Test\Dalamud.Test.csproj", "{C8004563-1806-4329-844F-0EF6274291FC}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Dependencies", "Dependencies", "{E15BDA6D-E881-4482-94BA-BE5527E917FF}"
@ -48,8 +45,6 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "InteropGenerator", "lib\FFX
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "InteropGenerator.Runtime", "lib\FFXIVClientStructs\InteropGenerator.Runtime\InteropGenerator.Runtime.csproj", "{A6AA1C3F-9470-4922-9D3F-D4549657AB22}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Injector", "Injector", "{19775C83-7117-4A5F-AA00-18889F46A490}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Utilities", "Utilities", "{8F079208-C227-4D96-9427-2BEBE0003944}"
EndProject
Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "cimgui", "external\cimgui\cimgui.vcxproj", "{8430077C-F736-4246-A052-8EA1CECE844E}"
@ -80,6 +75,17 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Lumina.Excel.Generator", "l
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Lumina.Excel", "lib\Lumina.Excel\src\Lumina.Excel\Lumina.Excel.csproj", "{88FB719B-EB41-73C5-8D25-C03E0C69904F}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Source Generators", "Source Generators", "{50BEC23B-FFFD-427B-A95D-27E1D1958FFF}"
ProjectSection(SolutionItems) = preProject
generators\Directory.Build.props = generators\Directory.Build.props
EndProjectSection
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Dalamud.EnumGenerator", "generators\Dalamud.EnumGenerator\Dalamud.EnumGenerator\Dalamud.EnumGenerator.csproj", "{27AA9F87-D2AA-41D9-A559-0F1EBA38C5F8}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Dalamud.EnumGenerator.Sample", "generators\Dalamud.EnumGenerator\Dalamud.EnumGenerator.Sample\Dalamud.EnumGenerator.Sample.csproj", "{8CDAEB2D-5022-450A-A97F-181C6270185F}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Dalamud.EnumGenerator.Tests", "generators\Dalamud.EnumGenerator\Dalamud.EnumGenerator.Tests\Dalamud.EnumGenerator.Tests.csproj", "{F5D92D2D-D36F-4471-B657-8B9AA6C98AD6}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@ -102,10 +108,6 @@ Global
{5B832F73-5F54-4ADC-870F-D0095EF72C9A}.Debug|Any CPU.Build.0 = Debug|x64
{5B832F73-5F54-4ADC-870F-D0095EF72C9A}.Release|Any CPU.ActiveCfg = Release|x64
{5B832F73-5F54-4ADC-870F-D0095EF72C9A}.Release|Any CPU.Build.0 = Release|x64
{8874326B-E755-4D13-90B4-59AB263A3E6B}.Debug|Any CPU.ActiveCfg = Debug|x64
{8874326B-E755-4D13-90B4-59AB263A3E6B}.Debug|Any CPU.Build.0 = Debug|x64
{8874326B-E755-4D13-90B4-59AB263A3E6B}.Release|Any CPU.ActiveCfg = Release|x64
{8874326B-E755-4D13-90B4-59AB263A3E6B}.Release|Any CPU.Build.0 = Release|x64
{C8004563-1806-4329-844F-0EF6274291FC}.Debug|Any CPU.ActiveCfg = Debug|x64
{C8004563-1806-4329-844F-0EF6274291FC}.Debug|Any CPU.Build.0 = Debug|x64
{C8004563-1806-4329-844F-0EF6274291FC}.Release|Any CPU.ActiveCfg = Release|x64
@ -182,13 +184,23 @@ Global
{88FB719B-EB41-73C5-8D25-C03E0C69904F}.Debug|Any CPU.Build.0 = Debug|Any CPU
{88FB719B-EB41-73C5-8D25-C03E0C69904F}.Release|Any CPU.ActiveCfg = Release|Any CPU
{88FB719B-EB41-73C5-8D25-C03E0C69904F}.Release|Any CPU.Build.0 = Release|Any CPU
{27AA9F87-D2AA-41D9-A559-0F1EBA38C5F8}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{27AA9F87-D2AA-41D9-A559-0F1EBA38C5F8}.Debug|Any CPU.Build.0 = Debug|Any CPU
{27AA9F87-D2AA-41D9-A559-0F1EBA38C5F8}.Release|Any CPU.ActiveCfg = Release|Any CPU
{27AA9F87-D2AA-41D9-A559-0F1EBA38C5F8}.Release|Any CPU.Build.0 = Release|Any CPU
{8CDAEB2D-5022-450A-A97F-181C6270185F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{8CDAEB2D-5022-450A-A97F-181C6270185F}.Debug|Any CPU.Build.0 = Debug|Any CPU
{8CDAEB2D-5022-450A-A97F-181C6270185F}.Release|Any CPU.ActiveCfg = Release|Any CPU
{8CDAEB2D-5022-450A-A97F-181C6270185F}.Release|Any CPU.Build.0 = Release|Any CPU
{F5D92D2D-D36F-4471-B657-8B9AA6C98AD6}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{F5D92D2D-D36F-4471-B657-8B9AA6C98AD6}.Debug|Any CPU.Build.0 = Debug|Any CPU
{F5D92D2D-D36F-4471-B657-8B9AA6C98AD6}.Release|Any CPU.ActiveCfg = Release|Any CPU
{F5D92D2D-D36F-4471-B657-8B9AA6C98AD6}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
EndGlobalSection
GlobalSection(NestedProjects) = preSolution
{5B832F73-5F54-4ADC-870F-D0095EF72C9A} = {19775C83-7117-4A5F-AA00-18889F46A490}
{8874326B-E755-4D13-90B4-59AB263A3E6B} = {19775C83-7117-4A5F-AA00-18889F46A490}
{4AFDB34A-7467-4D41-B067-53BC4101D9D0} = {8F079208-C227-4D96-9427-2BEBE0003944}
{C9B87BD7-AF49-41C3-91F1-D550ADEB7833} = {8BBACF2D-7AB8-4610-A115-0E363D35C291}
{E0D51896-604F-4B40-8CFE-51941607B3A1} = {8BBACF2D-7AB8-4610-A115-0E363D35C291}
@ -208,6 +220,9 @@ Global
{02EA681E-C7D8-13C7-8484-4AC65E1B71E8} = {E15BDA6D-E881-4482-94BA-BE5527E917FF}
{5A44DF0C-C9DA-940F-4D6B-4A11D13AEA3D} = {02EA681E-C7D8-13C7-8484-4AC65E1B71E8}
{88FB719B-EB41-73C5-8D25-C03E0C69904F} = {02EA681E-C7D8-13C7-8484-4AC65E1B71E8}
{27AA9F87-D2AA-41D9-A559-0F1EBA38C5F8} = {50BEC23B-FFFD-427B-A95D-27E1D1958FFF}
{8CDAEB2D-5022-450A-A97F-181C6270185F} = {50BEC23B-FFFD-427B-A95D-27E1D1958FFF}
{F5D92D2D-D36F-4471-B657-8B9AA6C98AD6} = {50BEC23B-FFFD-427B-A95D-27E1D1958FFF}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {79B65AC9-C940-410E-AB61-7EA7E12C7599}

View file

@ -10,7 +10,6 @@ using System.Threading.Tasks;
using Dalamud.Game.Text;
using Dalamud.Interface;
using Dalamud.Interface.FontIdentifier;
using Dalamud.Interface.ImGuiNotification.Internal;
using Dalamud.Interface.Internal;
using Dalamud.Interface.Internal.ReShadeHandling;
using Dalamud.Interface.Style;
@ -20,9 +19,12 @@ using Dalamud.Plugin.Internal.AutoUpdate;
using Dalamud.Plugin.Internal.Profiles;
using Dalamud.Storage;
using Dalamud.Utility;
using Newtonsoft.Json;
using Serilog;
using Serilog.Events;
using Windows.Win32.UI.WindowsAndMessaging;
namespace Dalamud.Configuration.Internal;
@ -91,7 +93,7 @@ internal sealed class DalamudConfiguration : IInternalDisposableService
/// <summary>
/// Gets or sets a dictionary of seen FTUE levels.
/// </summary>
public Dictionary<string, int> SeenFtueLevels { get; set; } = new();
public Dictionary<string, int> SeenFtueLevels { get; set; } = [];
/// <summary>
/// Gets or sets the last loaded Dalamud version.
@ -108,15 +110,10 @@ internal sealed class DalamudConfiguration : IInternalDisposableService
/// </summary>
public bool DoPluginTest { get; set; } = false;
/// <summary>
/// Gets or sets a key to opt into Dalamud staging builds.
/// </summary>
public string? DalamudBetaKey { get; set; } = null;
/// <summary>
/// Gets or sets a list of custom repos.
/// </summary>
public List<ThirdPartyRepoSettings> ThirdRepoList { get; set; } = new();
public List<ThirdPartyRepoSettings> ThirdRepoList { get; set; } = [];
/// <summary>
/// Gets or sets a value indicating whether a disclaimer regarding third-party repos has been dismissed.
@ -126,12 +123,12 @@ internal sealed class DalamudConfiguration : IInternalDisposableService
/// <summary>
/// Gets or sets a list of hidden plugins.
/// </summary>
public List<string> HiddenPluginInternalName { get; set; } = new();
public List<string> HiddenPluginInternalName { get; set; } = [];
/// <summary>
/// Gets or sets a list of seen plugins.
/// </summary>
public List<string> SeenPluginInternalName { get; set; } = new();
public List<string> SeenPluginInternalName { get; set; } = [];
/// <summary>
/// Gets or sets a list of additional settings for devPlugins. The key is the absolute path
@ -139,14 +136,14 @@ internal sealed class DalamudConfiguration : IInternalDisposableService
/// However by specifiying this value manually, you can add arbitrary files outside the normal
/// file paths.
/// </summary>
public Dictionary<string, DevPluginSettings> DevPluginSettings { get; set; } = new();
public Dictionary<string, DevPluginSettings> DevPluginSettings { get; set; } = [];
/// <summary>
/// Gets or sets a list of additional locations that dev plugins should be loaded from. This can
/// be either a DLL or folder, but should be the absolute path, or a path relative to the currently
/// injected Dalamud instance.
/// </summary>
public List<DevPluginLocationSettings> DevPluginLoadLocations { get; set; } = new();
public List<DevPluginLocationSettings> DevPluginLoadLocations { get; set; } = [];
/// <summary>
/// Gets or sets the global UI scale.
@ -228,7 +225,7 @@ internal sealed class DalamudConfiguration : IInternalDisposableService
/// <summary>
/// Gets or sets a list representing the command history for the Dalamud Console.
/// </summary>
public List<string> LogCommandHistory { get; set; } = new();
public List<string> LogCommandHistory { get; set; } = [];
/// <summary>
/// Gets or sets a value indicating whether the dev bar should open at startup.
@ -250,7 +247,7 @@ internal sealed class DalamudConfiguration : IInternalDisposableService
/// This setting is effected by the in-game "System Sounds" option and volume.
/// </summary>
[SuppressMessage("ReSharper", "InconsistentNaming", Justification = "ABI")]
public bool EnablePluginUISoundEffects { get; set; }
public bool EnablePluginUISoundEffects { get; set; } = true;
/// <summary>
/// Gets or sets a value indicating whether an additional button allowing pinning and clickthrough options should be shown
@ -278,11 +275,6 @@ internal sealed class DalamudConfiguration : IInternalDisposableService
/// </summary>
public bool IsResumeGameAfterPluginLoad { get; set; } = false;
/// <summary>
/// Gets or sets the kind of beta to download when <see cref="DalamudBetaKey"/> matches the server value.
/// </summary>
public string? DalamudBetaKind { get; set; }
/// <summary>
/// Gets or sets a value indicating whether any plugin should be loaded when the game is started.
/// It is reset immediately when read.
@ -497,19 +489,37 @@ internal sealed class DalamudConfiguration : IInternalDisposableService
/// </summary>
public Vector2 NotificationAnchorPosition { get; set; } = new(1f, 1f);
#pragma warning disable SA1600
#pragma warning disable SA1516
// XLCore/XoM compatibility until they move it out
public string? DalamudBetaKey { get; set; } = null;
public string? DalamudBetaKind { get; set; }
#pragma warning restore SA1516
#pragma warning restore SA1600
/// <summary>
/// Gets or sets a list of badge passwords used to unlock badges.
/// </summary>
public List<string> UsedBadgePasswords { get; set; } = [];
/// <summary>
/// Gets or sets a value indicating whether badges should be shown on the title screen.
/// </summary>
public bool ShowBadgesOnTitleScreen { get; set; } = true;
/// <summary>
/// Load a configuration from the provided path.
/// </summary>
/// <param name="path">Path to read from.</param>
/// <param name="fs">File storage.</param>
/// <returns>The deserialized configuration file.</returns>
public static DalamudConfiguration Load(string path, ReliableFileStorage fs)
public static async Task<DalamudConfiguration> Load(string path, ReliableFileStorage fs)
{
DalamudConfiguration deserialized = null;
try
{
fs.ReadAllText(path, text =>
await fs.ReadAllTextAsync(path, text =>
{
deserialized =
JsonConvert.DeserializeObject<DalamudConfiguration>(text, SerializerSettings);
@ -580,8 +590,6 @@ internal sealed class DalamudConfiguration : IInternalDisposableService
{
this.Save();
this.isSaveQueued = false;
Log.Verbose("Config saved");
}
}
@ -593,7 +601,7 @@ internal sealed class DalamudConfiguration : IInternalDisposableService
{
// https://source.chromium.org/chromium/chromium/src/+/main:ui/gfx/animation/animation_win.cc;l=29?q=ReducedMotion&ss=chromium
var winAnimEnabled = 0;
var success = false;
bool success;
unsafe
{
success = Windows.Win32.PInvoke.SystemParametersInfo(
@ -630,16 +638,20 @@ internal sealed class DalamudConfiguration : IInternalDisposableService
// Wait for previous write to finish
this.writeTask?.Wait();
this.writeTask = Task.Run(() =>
this.writeTask = Task.Run(async () =>
{
Service<ReliableFileStorage>.Get().WriteAllText(
this.configPath,
JsonConvert.SerializeObject(this, SerializerSettings));
await Service<ReliableFileStorage>.Get().WriteAllTextAsync(
this.configPath,
JsonConvert.SerializeObject(this, SerializerSettings));
Log.Verbose("DalamudConfiguration saved");
}).ContinueWith(t =>
{
if (t.IsFaulted)
{
Log.Error(t.Exception, "Failed to save DalamudConfiguration to {Path}", this.configPath);
Log.Error(
t.Exception,
"Failed to save DalamudConfiguration to {Path}",
this.configPath);
}
});

View file

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

View file

@ -2,6 +2,8 @@ using System.IO;
using System.Reflection;
using Dalamud.Storage;
using Dalamud.Utility;
using Newtonsoft.Json;
namespace Dalamud.Configuration;
@ -9,6 +11,7 @@ namespace Dalamud.Configuration;
/// <summary>
/// Configuration to store settings for a dalamud plugin.
/// </summary>
[Api15ToDo("Make this a service. We need to be able to dispose it reliably to write configs asynchronously. Maybe also let people write files with vfs.")]
public sealed class PluginConfigurations
{
private readonly DirectoryInfo configDirectory;
@ -36,7 +39,7 @@ public sealed class PluginConfigurations
public void Save(IPluginConfiguration config, string pluginName, Guid workingPluginId)
{
Service<ReliableFileStorage>.Get()
.WriteAllText(this.GetConfigFile(pluginName).FullName, SerializeConfig(config), workingPluginId);
.WriteAllTextAsync(this.GetConfigFile(pluginName).FullName, SerializeConfig(config), workingPluginId).GetAwaiter().GetResult();
}
/// <summary>
@ -52,12 +55,12 @@ public sealed class PluginConfigurations
IPluginConfiguration? config = null;
try
{
Service<ReliableFileStorage>.Get().ReadAllText(path.FullName, text =>
Service<ReliableFileStorage>.Get().ReadAllTextAsync(path.FullName, text =>
{
config = DeserializeConfig(text);
if (config == null)
throw new Exception("Read config was null.");
}, workingPluginId);
}, workingPluginId).GetAwaiter().GetResult();
}
catch (FileNotFoundException)
{

View file

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

View file

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

View file

@ -9,11 +9,14 @@ using System.Threading.Tasks;
using Dalamud.Common;
using Dalamud.Configuration.Internal;
using Dalamud.Game;
using Dalamud.Hooking.Internal.Verification;
using Dalamud.Plugin.Internal;
using Dalamud.Storage;
using Dalamud.Utility;
using Dalamud.Utility.Timing;
using Serilog;
using Windows.Win32.Foundation;
using Windows.Win32.Security;
@ -73,6 +76,11 @@ internal sealed unsafe class Dalamud : IServiceType
scanner,
Localization.FromAssets(info.AssetDirectory!, configuration.LanguageOverride));
using (Timings.Start("HookVerifier Init"))
{
HookVerifier.Initialize(scanner);
}
// Set up FFXIVClientStructs
this.SetupClientStructsResolver(cacheDir);

View file

@ -6,7 +6,7 @@
<PropertyGroup Label="Feature">
<Description>XIV Launcher addon framework</Description>
<DalamudVersion>13.0.0.1</DalamudVersion>
<DalamudVersion>14.0.2.2</DalamudVersion>
<AssemblyVersion>$(DalamudVersion)</AssemblyVersion>
<Version>$(DalamudVersion)</Version>
<FileVersion>$(DalamudVersion)</FileVersion>
@ -61,37 +61,41 @@
</PropertyGroup>
<ItemGroup>
<PackageReference Include="BitFaster.Caching" Version="2.4.1" />
<PackageReference Include="CheapLoc" Version="1.1.8" />
<PackageReference Include="DotNet.ReproducibleBuilds" Version="1.2.4" PrivateAssets="all" />
<PackageReference Include="goatcorp.Reloaded.Hooks" Version="4.2.0-goatcorp5" />
<PackageReference Include="goatcorp.Reloaded.Assembler" Version="1.0.14-goatcorp3" />
<PackageReference Include="JetBrains.Annotations" Version="2024.2.0" />
<PackageReference Include="Lumina" Version="$(LuminaVersion)" />
<PackageReference Include="Microsoft.Extensions.ObjectPool" Version="8.0.7" />
<PackageReference Include="Microsoft.Windows.CsWin32" Version="0.3.183">
<PackageReference Include="BitFaster.Caching" />
<PackageReference Include="CheapLoc" />
<PackageReference Include="DotNet.ReproducibleBuilds" PrivateAssets="all" />
<PackageReference Include="goatcorp.Reloaded.Hooks" />
<PackageReference Include="JetBrains.Annotations" />
<PackageReference Include="Lumina" />
<PackageReference Include="Microsoft.Extensions.ObjectPool" />
<PackageReference Include="Microsoft.Windows.CsWin32">
<PrivateAssets>all</PrivateAssets>
</PackageReference>
<PackageReference Include="MinSharp" Version="1.0.4" />
<PackageReference Include="SharpDX.Direct3D11" Version="4.2.0" />
<PackageReference Include="SharpDX.Mathematics" Version="4.2.0" />
<PackageReference Include="Newtonsoft.Json" Version="$(NewtonsoftJsonVersion)" />
<PackageReference Include="Serilog" Version="4.0.2" />
<PackageReference Include="Serilog.Sinks.Async" Version="2.0.0" />
<PackageReference Include="Serilog.Sinks.Console" Version="6.0.0" />
<PackageReference Include="Serilog.Sinks.File" Version="6.0.0" />
<PackageReference Include="sqlite-net-pcl" Version="1.8.116" />
<PackageReference Include="StyleCop.Analyzers" Version="1.2.0-beta.556">
<PackageReference Include="MinSharp" />
<PackageReference Include="Newtonsoft.Json" />
<PackageReference Include="Serilog" />
<PackageReference Include="Serilog.Sinks.Async" />
<PackageReference Include="Serilog.Sinks.Console" />
<PackageReference Include="Serilog.Sinks.File" />
<PackageReference Include="sqlite-net-pcl" />
<PackageReference Include="StyleCop.Analyzers">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="System.Collections.Immutable" Version="8.0.0" />
<PackageReference Include="System.Drawing.Common" Version="8.0.0" />
<PackageReference Include="System.Reactive" Version="5.0.0" />
<PackageReference Include="System.Reflection.MetadataLoadContext" Version="8.0.0" />
<PackageReference Include="System.Resources.Extensions" Version="8.0.0" />
<PackageReference Include="TerraFX.Interop.Windows" Version="10.0.22621.2" />
<PackageReference Include="System.Reactive" />
<PackageReference Include="System.Reflection.MetadataLoadContext" />
<PackageReference Include="TerraFX.Interop.Windows" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\generators\Dalamud.EnumGenerator\Dalamud.EnumGenerator\Dalamud.EnumGenerator.csproj" OutputItemType="Analyzer" ReferenceOutputAssembly="false"/>
</ItemGroup>
<ItemGroup>
<None Remove="EnumCloneMap.txt"/>
<AdditionalFiles Include="EnumCloneMap.txt" />
</ItemGroup>
<ItemGroup>
<EmbeddedResource Include="Interface\ImGuiBackend\Renderers\imgui-frag.hlsl.bytes">
<LogicalName>imgui-frag.hlsl.bytes</LogicalName>
@ -100,6 +104,7 @@
<LogicalName>imgui-vertex.hlsl.bytes</LogicalName>
</EmbeddedResource>
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\Dalamud.Common\Dalamud.Common.csproj" />
<ProjectReference Include="..\imgui\Dalamud.Bindings.ImGuizmo\Dalamud.Bindings.ImGuizmo.csproj" />
@ -120,6 +125,8 @@
<Content Include="licenses.txt">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>
<None Remove="Interface\ImGuiBackend\Renderers\gaussian.hlsl" />
<None Remove="Interface\ImGuiBackend\Renderers\fullscreen-quad.hlsl.bytes" />
</ItemGroup>
<ItemGroup>
@ -160,6 +167,9 @@
<Exec Command="git -C &quot;$(ProjectDir.Replace('\','\\'))&quot; describe --tags --always --dirty" ConsoleToMSBuild="true">
<Output TaskParameter="ConsoleOutput" PropertyName="DalamudGitDescribeOutput" />
</Exec>
<Exec Command="git -C &quot;$(ProjectDir.Replace('\','\\'))&quot; rev-parse --abbrev-ref HEAD" ConsoleToMSBuild="true">
<Output TaskParameter="ConsoleOutput" PropertyName="DalamudGitBranch" />
</Exec>
<Exec Command="git -C &quot;$(ProjectDir.Replace('\','\\'))\..\lib\FFXIVClientStructs&quot; describe --long --always --dirty" ConsoleToMSBuild="true">
<Output TaskParameter="ConsoleOutput" PropertyName="ClientStructsGitDescribeOutput" />
</Exec>
@ -167,6 +177,7 @@
<PropertyGroup>
<CommitCount>$([System.Text.RegularExpressions.Regex]::Replace($(DalamudGitCommitCount), @"\t|\n|\r", ""))</CommitCount>
<CommitHash>$([System.Text.RegularExpressions.Regex]::Replace($(DalamudGitCommitHash), @"\t|\n|\r", ""))</CommitHash>
<Branch>$([System.Text.RegularExpressions.Regex]::Replace($(DalamudGitBranch), @"\t|\n|\r", ""))</Branch>
<SCMVersion>$([System.Text.RegularExpressions.Regex]::Replace($(DalamudGitDescribeOutput), @"\t|\n|\r", ""))</SCMVersion>
<CommitHashClientStructs>$([System.Text.RegularExpressions.Regex]::Replace($(ClientStructsGitDescribeOutput), @"\t|\n|\r", ""))</CommitHashClientStructs>
</PropertyGroup>
@ -180,6 +191,7 @@
<!-- stub out version since it takes a while. -->
<PropertyGroup>
<SCMVersion>Local build at $([System.DateTime]::Now.ToString(yyyy-MM-dd HH:mm:ss))</SCMVersion>
<Branch>???</Branch>
<CommitHashClientStructs>???</CommitHashClientStructs>
</PropertyGroup>
</Target>
@ -203,6 +215,10 @@
<_Parameter1>GitCommitCount</_Parameter1>
<_Parameter2>$(CommitCount)</_Parameter2>
</AssemblyAttributes>
<AssemblyAttributes Include="AssemblyMetadata" Condition="'$(Branch)' != ''">
<_Parameter1>GitBranch</_Parameter1>
<_Parameter2>$(Branch)</_Parameter2>
</AssemblyAttributes>
<AssemblyAttributes Include="AssemblyMetadata" Condition="'$(CommitHashClientStructs)' != ''">
<_Parameter1>GitHashClientStructs</_Parameter1>
<_Parameter2>$(CommitHashClientStructs)</_Parameter2>
@ -215,9 +231,4 @@
<!-- writes the attribute to the customAssemblyInfo file -->
<WriteCodeFragment Language="C#" OutputFile="$(CustomAssemblyInfoFile)" AssemblyAttributes="@(AssemblyAttributes)" />
</Target>
<!-- Copy plugin .targets folder into distrib -->
<Target Name="CopyPluginTargets" AfterTargets="Build">
<Copy SourceFiles="$(ProjectDir)\..\targets\Dalamud.Plugin.targets;$(ProjectDir)\..\targets\Dalamud.Plugin.Bootstrap.targets" DestinationFolder="$(OutDir)\targets" />
</Target>
</Project>

View file

@ -73,7 +73,7 @@ public enum DalamudAsset
[DalamudAsset(DalamudAssetPurpose.TextureFromPng)]
[DalamudAssetPath("UIRes", "troubleIcon.png")]
TroubleIcon = 1006,
/// <summary>
/// <see cref="DalamudAssetPurpose.TextureFromPng"/>: The plugin trouble icon overlay.
/// </summary>
@ -124,6 +124,13 @@ public enum DalamudAsset
[DalamudAssetPath("UIRes", "tsmShade.png")]
TitleScreenMenuShade = 1013,
/// <summary>
/// <see cref="DalamudAssetPurpose.TextureFromPng"/>: Atlas containing badges.
/// </summary>
[DalamudAsset(DalamudAssetPurpose.TextureFromPng)]
[DalamudAssetPath("UIRes", "badgeAtlas.png")]
BadgeAtlas = 1015,
/// <summary>
/// <see cref="DalamudAssetPurpose.Font"/>: Noto Sans CJK JP Medium.
/// </summary>
@ -151,7 +158,7 @@ public enum DalamudAsset
/// <see cref="DalamudAssetPurpose.Font"/>: FontAwesome Free Solid.
/// </summary>
[DalamudAsset(DalamudAssetPurpose.Font)]
[DalamudAssetPath("UIRes", "FontAwesomeFreeSolid.otf")]
[DalamudAssetPath("UIRes", "FontAwesome710FreeSolid.otf")]
FontAwesomeFreeSolid = 2003,
/// <summary>

View file

@ -8,11 +8,13 @@ using Dalamud.IoC.Internal;
using Dalamud.Plugin.Services;
using Dalamud.Utility;
using Dalamud.Utility.Timing;
using Lumina;
using Lumina.Data;
using Lumina.Excel;
using Newtonsoft.Json;
using Serilog;
namespace Dalamud.Data;
@ -41,7 +43,7 @@ internal sealed class DataManager : IInternalDisposableService, IDataManager
try
{
Log.Verbose("Starting data load...");
using (Timings.Start("Lumina Init"))
{
var luminaOptions = new LuminaOptions
@ -53,12 +55,25 @@ internal sealed class DataManager : IInternalDisposableService, IDataManager
DefaultExcelLanguage = this.Language.ToLumina(),
};
this.GameData = new(
Path.Combine(Path.GetDirectoryName(Environment.ProcessPath)!, "sqpack"),
luminaOptions)
try
{
StreamPool = new(),
};
this.GameData = new(
Path.Combine(Path.GetDirectoryName(Environment.ProcessPath)!, "sqpack"),
luminaOptions)
{
StreamPool = new(),
};
}
catch (Exception ex)
{
Log.Error(ex, "Lumina GameData init failed");
Util.Fatal(
"Dalamud could not read required game data files. This likely means your game installation is corrupted or incomplete.\n\n" +
"Please repair your installation by right-clicking the login button in XIVLauncher and choosing \"Repair game files\".",
"Dalamud");
return;
}
Log.Information("Lumina is ready: {0}", this.GameData.DataPath);
@ -69,9 +84,14 @@ internal sealed class DataManager : IInternalDisposableService, IDataManager
var tsInfo =
JsonConvert.DeserializeObject<LauncherTroubleshootingInfo>(
dalamud.StartInfo.TroubleshootingPackData);
this.HasModifiedGameDataFiles =
tsInfo?.IndexIntegrity is LauncherTroubleshootingInfo.IndexIntegrityResult.Failed or LauncherTroubleshootingInfo.IndexIntegrityResult.Exception;
// Don't fail for IndexIntegrityResult.Exception, since the check during launch has a very small timeout
// this.HasModifiedGameDataFiles =
// tsInfo?.IndexIntegrity is LauncherTroubleshootingInfo.IndexIntegrityResult.Failed;
// TODO: Put above back when check in XL is fixed
this.HasModifiedGameDataFiles = false;
if (this.HasModifiedGameDataFiles)
Log.Verbose("Game data integrity check failed!\n{TsData}", dalamud.StartInfo.TroubleshootingPackData);
}
@ -130,7 +150,7 @@ internal sealed class DataManager : IInternalDisposableService, IDataManager
#region Lumina Wrappers
/// <inheritdoc/>
public ExcelSheet<T> GetExcelSheet<T>(ClientLanguage? language = null, string? name = null) where T : struct, IExcelRow<T>
public ExcelSheet<T> GetExcelSheet<T>(ClientLanguage? language = null, string? name = null) where T : struct, IExcelRow<T>
=> this.Excel.GetSheet<T>(language?.ToLumina(), name);
/// <inheritdoc/>
@ -138,7 +158,7 @@ internal sealed class DataManager : IInternalDisposableService, IDataManager
=> this.Excel.GetSubrowSheet<T>(language?.ToLumina(), name);
/// <inheritdoc/>
public FileResource? GetFile(string path)
public FileResource? GetFile(string path)
=> this.GetFile<FileResource>(path);
/// <inheritdoc/>
@ -161,7 +181,7 @@ internal sealed class DataManager : IInternalDisposableService, IDataManager
: Task.FromException<T>(new FileNotFoundException("The file could not be found."));
/// <inheritdoc/>
public bool FileExists(string path)
public bool FileExists(string path)
=> this.GameData.FileExists(path);
#endregion

View file

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

View file

@ -1,8 +1,6 @@
using System.ComponentModel;
using System.Diagnostics;
using System.IO;
using System.Net;
using System.Reflection;
using System.Runtime.InteropServices;
using System.Threading;
using System.Threading.Tasks;
@ -16,10 +14,13 @@ using Dalamud.Plugin.Internal;
using Dalamud.Storage;
using Dalamud.Support;
using Dalamud.Utility;
using Newtonsoft.Json;
using Serilog;
using Serilog.Core;
using Serilog.Events;
using Windows.Win32.Foundation;
using Windows.Win32.UI.WindowsAndMessaging;
@ -144,7 +145,8 @@ public sealed class EntryPoint
// Load configuration first to get some early persistent state, like log level
var fs = new ReliableFileStorage(Path.GetDirectoryName(info.ConfigurationPath)!);
var configuration = DalamudConfiguration.Load(info.ConfigurationPath!, fs);
var configuration = DalamudConfiguration.Load(info.ConfigurationPath!, fs)
.GetAwaiter().GetResult();
// Set the appropriate logging level from the configuration
if (!configuration.LogSynchronously)
@ -191,8 +193,8 @@ public sealed class EntryPoint
var dalamud = new Dalamud(info, fs, configuration, mainThreadContinueEvent);
Log.Information("This is Dalamud - Core: {GitHash}, CS: {CsGitHash} [{CsVersion}]",
Util.GetScmVersion(),
Util.GetGitHashClientStructs(),
Versioning.GetScmVersion(),
Versioning.GetGitHashClientStructs(),
FFXIVClientStructs.ThisAssembly.Git.Commits);
dalamud.WaitForUnload();
@ -262,7 +264,7 @@ public sealed class EntryPoint
var symbolPath = Path.Combine(info.AssetDirectory, "UIRes", "pdb");
var searchPath = $".;{symbolPath}";
var currentProcess = Windows.Win32.PInvoke.GetCurrentProcess_SafeHandle();
var currentProcess = Windows.Win32.PInvoke.GetCurrentProcess();
// Remove any existing Symbol Handler and Init a new one with our search path added
Windows.Win32.PInvoke.SymCleanup(currentProcess);
@ -291,7 +293,6 @@ public sealed class EntryPoint
}
var pluginInfo = string.Empty;
var supportText = ", please visit us on Discord for more help";
try
{
var pm = Service<PluginManager>.GetNullable();
@ -299,9 +300,6 @@ public sealed class EntryPoint
if (plugin != null)
{
pluginInfo = $"Plugin that caused this:\n{plugin.Name}\n\nClick \"Yes\" and remove it.\n\n";
if (plugin.IsThirdParty)
supportText = string.Empty;
}
}
catch
@ -309,31 +307,18 @@ public sealed class EntryPoint
// ignored
}
const MESSAGEBOX_STYLE flags = MESSAGEBOX_STYLE.MB_YESNO | MESSAGEBOX_STYLE.MB_ICONERROR | MESSAGEBOX_STYLE.MB_SYSTEMMODAL;
var result = Windows.Win32.PInvoke.MessageBox(
new HWND(Process.GetCurrentProcess().MainWindowHandle),
$"An internal error in a Dalamud plugin occurred.\nThe game must close.\n\n{ex.GetType().Name}\n{info}\n\n{pluginInfo}More information has been recorded separately{supportText}.\n\nDo you want to disable all plugins the next time you start the game?",
"Dalamud",
flags);
if (result == MESSAGEBOX_RESULT.IDYES)
{
Log.Information("User chose to disable plugins on next launch...");
var config = Service<DalamudConfiguration>.Get();
config.PluginSafeMode = true;
config.ForceSave();
}
Log.CloseAndFlush();
Environment.Exit(-1);
ErrorHandling.CrashWithContext($"{ex}\n\n{info}\n\n{pluginInfo}");
break;
default:
Log.Fatal("Unhandled SEH object on AppDomain: {Object}", args.ExceptionObject);
Log.CloseAndFlush();
Environment.Exit(-1);
break;
}
Environment.Exit(-1);
}
private static void OnUnhandledExceptionStallDebug(object sender, UnhandledExceptionEventArgs args)

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,107 +0,0 @@
using System.Runtime.CompilerServices;
using System.Threading;
using Dalamud.Game.Addon.Lifecycle.AddonArgTypes;
namespace Dalamud.Game.Addon;
/// <summary>Argument pool for Addon Lifecycle services.</summary>
[ServiceManager.EarlyLoadedService]
internal sealed class AddonLifecyclePooledArgs : IServiceType
{
private readonly AddonSetupArgs?[] addonSetupArgPool = new AddonSetupArgs?[64];
private readonly AddonFinalizeArgs?[] addonFinalizeArgPool = new AddonFinalizeArgs?[64];
private readonly AddonDrawArgs?[] addonDrawArgPool = new AddonDrawArgs?[64];
private readonly AddonUpdateArgs?[] addonUpdateArgPool = new AddonUpdateArgs?[64];
private readonly AddonRefreshArgs?[] addonRefreshArgPool = new AddonRefreshArgs?[64];
private readonly AddonRequestedUpdateArgs?[] addonRequestedUpdateArgPool = new AddonRequestedUpdateArgs?[64];
private readonly AddonReceiveEventArgs?[] addonReceiveEventArgPool = new AddonReceiveEventArgs?[64];
[ServiceManager.ServiceConstructor]
private AddonLifecyclePooledArgs()
{
}
/// <summary>Rents an instance of an argument.</summary>
/// <param name="arg">The rented instance.</param>
/// <returns>The returner.</returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public PooledEntry<AddonSetupArgs> Rent(out AddonSetupArgs arg) => new(out arg, this.addonSetupArgPool);
/// <summary>Rents an instance of an argument.</summary>
/// <param name="arg">The rented instance.</param>
/// <returns>The returner.</returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public PooledEntry<AddonFinalizeArgs> Rent(out AddonFinalizeArgs arg) => new(out arg, this.addonFinalizeArgPool);
/// <summary>Rents an instance of an argument.</summary>
/// <param name="arg">The rented instance.</param>
/// <returns>The returner.</returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public PooledEntry<AddonDrawArgs> Rent(out AddonDrawArgs arg) => new(out arg, this.addonDrawArgPool);
/// <summary>Rents an instance of an argument.</summary>
/// <param name="arg">The rented instance.</param>
/// <returns>The returner.</returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public PooledEntry<AddonUpdateArgs> Rent(out AddonUpdateArgs arg) => new(out arg, this.addonUpdateArgPool);
/// <summary>Rents an instance of an argument.</summary>
/// <param name="arg">The rented instance.</param>
/// <returns>The returner.</returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public PooledEntry<AddonRefreshArgs> Rent(out AddonRefreshArgs arg) => new(out arg, this.addonRefreshArgPool);
/// <summary>Rents an instance of an argument.</summary>
/// <param name="arg">The rented instance.</param>
/// <returns>The returner.</returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public PooledEntry<AddonRequestedUpdateArgs> Rent(out AddonRequestedUpdateArgs arg) =>
new(out arg, this.addonRequestedUpdateArgPool);
/// <summary>Rents an instance of an argument.</summary>
/// <param name="arg">The rented instance.</param>
/// <returns>The returner.</returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public PooledEntry<AddonReceiveEventArgs> Rent(out AddonReceiveEventArgs arg) =>
new(out arg, this.addonReceiveEventArgPool);
/// <summary>Returns the object to the pool on dispose.</summary>
/// <typeparam name="T">The type.</typeparam>
public readonly ref struct PooledEntry<T>
where T : AddonArgs, new()
{
private readonly Span<T> pool;
private readonly T obj;
/// <summary>Initializes a new instance of the <see cref="PooledEntry{T}"/> struct.</summary>
/// <param name="arg">An instance of the argument.</param>
/// <param name="pool">The pool to rent from and return to.</param>
public PooledEntry(out T arg, Span<T> pool)
{
this.pool = pool;
foreach (ref var item in pool)
{
if (Interlocked.Exchange(ref item, null) is { } v)
{
this.obj = arg = v;
return;
}
}
this.obj = arg = new();
}
/// <summary>Returns the item to the pool.</summary>
public void Dispose()
{
var tmp = this.obj;
foreach (ref var item in this.pool)
{
if (Interlocked.Exchange(ref item, tmp) is not { } tmp2)
return;
tmp = tmp2;
}
}
}
}

View file

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

View file

@ -9,7 +9,6 @@ using Dalamud.Logging.Internal;
using Dalamud.Plugin.Internal.Types;
using Dalamud.Plugin.Services;
using FFXIVClientStructs.FFXIV.Client.UI;
using FFXIVClientStructs.FFXIV.Component.GUI;
namespace Dalamud.Game.Addon.Events;
@ -25,32 +24,28 @@ internal unsafe class AddonEventManager : IInternalDisposableService
/// </summary>
public static readonly Guid DalamudInternalKey = Guid.NewGuid();
private static readonly ModuleLog Log = new("AddonEventManager");
private static readonly ModuleLog Log = ModuleLog.Create<AddonEventManager>();
[ServiceManager.ServiceDependency]
private readonly AddonLifecycle addonLifecycle = Service<AddonLifecycle>.Get();
private readonly AddonLifecycleEventListener finalizeEventListener;
private readonly AddonEventManagerAddressResolver address;
private readonly Hook<UpdateCursorDelegate> onUpdateCursor;
private readonly Hook<AtkUnitManager.Delegates.UpdateCursor> onUpdateCursor;
private readonly ConcurrentDictionary<Guid, PluginEventController> pluginEventControllers;
private AddonCursorType? cursorOverride;
private AtkCursor.CursorType? cursorOverride;
[ServiceManager.ServiceConstructor]
private AddonEventManager(TargetSigScanner sigScanner)
private AddonEventManager()
{
this.address = new AddonEventManagerAddressResolver();
this.address.Setup(sigScanner);
this.pluginEventControllers = new ConcurrentDictionary<Guid, PluginEventController>();
this.pluginEventControllers.TryAdd(DalamudInternalKey, new PluginEventController());
this.cursorOverride = null;
this.onUpdateCursor = Hook<UpdateCursorDelegate>.FromAddress(this.address.UpdateCursor, this.UpdateCursorDetour);
this.onUpdateCursor = Hook<AtkUnitManager.Delegates.UpdateCursor>.FromAddress(AtkUnitManager.Addresses.UpdateCursor.Value, this.UpdateCursorDetour);
this.finalizeEventListener = new AddonLifecycleEventListener(AddonEvent.PreFinalize, string.Empty, this.OnAddonFinalize);
this.addonLifecycle.RegisterListener(this.finalizeEventListener);
@ -58,8 +53,6 @@ internal unsafe class AddonEventManager : IInternalDisposableService
this.onUpdateCursor.Enable();
}
private delegate nint UpdateCursorDelegate(RaptureAtkModule* module);
/// <inheritdoc/>
void IInternalDisposableService.DisposeService()
{
@ -117,7 +110,7 @@ internal unsafe class AddonEventManager : IInternalDisposableService
/// Force the game cursor to be the specified cursor.
/// </summary>
/// <param name="cursor">Which cursor to use.</param>
internal void SetCursor(AddonCursorType cursor) => this.cursorOverride = cursor;
internal void SetCursor(AddonCursorType cursor) => this.cursorOverride = (AtkCursor.CursorType)cursor;
/// <summary>
/// Un-forces the game cursor.
@ -168,7 +161,7 @@ internal unsafe class AddonEventManager : IInternalDisposableService
}
}
private nint UpdateCursorDetour(RaptureAtkModule* module)
private void UpdateCursorDetour(AtkUnitManager* thisPtr)
{
try
{
@ -176,13 +169,14 @@ internal unsafe class AddonEventManager : IInternalDisposableService
if (this.cursorOverride is not null && atkStage is not null)
{
var cursor = (AddonCursorType)atkStage->AtkCursor.Type;
if (cursor != this.cursorOverride)
ref var atkCursor = ref atkStage->AtkCursor;
if (atkCursor.Type != this.cursorOverride)
{
AtkStage.Instance()->AtkCursor.SetCursorType((AtkCursor.CursorType)this.cursorOverride, 1);
atkCursor.SetCursorType((AtkCursor.CursorType)this.cursorOverride, 1);
}
return nint.Zero;
return;
}
}
catch (Exception e)
@ -190,7 +184,7 @@ internal unsafe class AddonEventManager : IInternalDisposableService
Log.Error(e, "Exception in UpdateCursorDetour.");
}
return this.onUpdateCursor!.Original(module);
this.onUpdateCursor!.Original(thisPtr);
}
}

View file

@ -1,21 +0,0 @@
namespace Dalamud.Game.Addon.Events;
/// <summary>
/// AddonEventManager memory address resolver.
/// </summary>
internal class AddonEventManagerAddressResolver : BaseAddressResolver
{
/// <summary>
/// Gets the address of the AtkModule UpdateCursor method.
/// </summary>
public nint UpdateCursor { get; private set; }
/// <summary>
/// Scan for and setup any configured address pointers.
/// </summary>
/// <param name="scanner">The signature scanner to facilitate setup.</param>
protected override void Setup64Bit(ISigScanner scanner)
{
this.UpdateCursor = scanner.ScanText("48 89 74 24 ?? 48 89 7C 24 ?? 41 56 48 83 EC 20 4C 8B F1 E8 ?? ?? ?? ?? 49 8B CE"); // unnamed in CS
}
}

View file

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

View file

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

View file

@ -5,19 +5,24 @@ namespace Dalamud.Game.Addon.Lifecycle.AddonArgTypes;
/// <summary>
/// Base class for AddonLifecycle AddonArgTypes.
/// </summary>
public abstract unsafe class AddonArgs
public class AddonArgs
{
/// <summary>
/// Constant string representing the name of an addon that is invalid.
/// </summary>
public const string InvalidAddon = "NullAddon";
private string? addonName;
/// <summary>
/// Initializes a new instance of the <see cref="AddonArgs"/> class.
/// </summary>
internal AddonArgs()
{
}
/// <summary>
/// Gets the name of the addon this args referrers to.
/// </summary>
public string AddonName => this.GetAddonName();
public string AddonName { get; private set; } = InvalidAddon;
/// <summary>
/// Gets the pointer to the addons AtkUnitBase.
@ -25,55 +30,17 @@ public abstract unsafe class AddonArgs
public AtkUnitBasePtr Addon
{
get;
internal set;
internal set
{
field = value;
if (!this.Addon.IsNull && !string.IsNullOrEmpty(value.Name))
this.AddonName = value.Name;
}
}
/// <summary>
/// Gets the type of these args.
/// </summary>
public abstract AddonArgsType Type { get; }
/// <summary>
/// Checks if addon name matches the given span of char.
/// </summary>
/// <param name="name">The name to check.</param>
/// <returns>Whether it is the case.</returns>
internal bool IsAddon(string name)
{
if (this.Addon.IsNull)
return false;
if (name.Length is 0 or > 32)
return false;
if (string.IsNullOrEmpty(this.Addon.Name))
return false;
return name == this.Addon.Name;
}
/// <summary>
/// Clears this AddonArgs values.
/// </summary>
internal virtual void Clear()
{
this.addonName = null;
this.Addon = 0;
}
/// <summary>
/// Helper method for ensuring the name of the addon is valid.
/// </summary>
/// <returns>The name of the addon for this object. <see cref="InvalidAddon"/> when invalid.</returns>
private string GetAddonName()
{
if (this.Addon.IsNull) return InvalidAddon;
var name = this.Addon.Name;
if (string.IsNullOrEmpty(name))
return InvalidAddon;
return this.addonName ??= name;
}
public virtual AddonArgsType Type => AddonArgsType.Generic;
}

View file

@ -0,0 +1,22 @@
namespace Dalamud.Game.Addon.Lifecycle.AddonArgTypes;
/// <summary>
/// Addon argument data for Close events.
/// </summary>
public class AddonCloseArgs : AddonArgs
{
/// <summary>
/// Initializes a new instance of the <see cref="AddonCloseArgs"/> class.
/// </summary>
internal AddonCloseArgs()
{
}
/// <inheritdoc/>
public override AddonArgsType Type => AddonArgsType.Close;
/// <summary>
/// Gets or sets a value indicating whether the window should fire the callback method on close.
/// </summary>
public bool FireCallback { get; set; }
}

View file

@ -1,24 +0,0 @@
namespace Dalamud.Game.Addon.Lifecycle.AddonArgTypes;
/// <summary>
/// Addon argument data for Draw events.
/// </summary>
public class AddonDrawArgs : AddonArgs, ICloneable
{
/// <summary>
/// Initializes a new instance of the <see cref="AddonDrawArgs"/> class.
/// </summary>
[Obsolete("Not intended for public construction.", false)]
public AddonDrawArgs()
{
}
/// <inheritdoc/>
public override AddonArgsType Type => AddonArgsType.Draw;
/// <inheritdoc cref="ICloneable.Clone"/>
public AddonDrawArgs Clone() => (AddonDrawArgs)this.MemberwiseClone();
/// <inheritdoc cref="Clone"/>
object ICloneable.Clone() => this.Clone();
}

View file

@ -1,24 +0,0 @@
namespace Dalamud.Game.Addon.Lifecycle.AddonArgTypes;
/// <summary>
/// Addon argument data for ReceiveEvent events.
/// </summary>
public class AddonFinalizeArgs : AddonArgs, ICloneable
{
/// <summary>
/// Initializes a new instance of the <see cref="AddonFinalizeArgs"/> class.
/// </summary>
[Obsolete("Not intended for public construction.", false)]
public AddonFinalizeArgs()
{
}
/// <inheritdoc/>
public override AddonArgsType Type => AddonArgsType.Finalize;
/// <inheritdoc cref="ICloneable.Clone"/>
public AddonFinalizeArgs Clone() => (AddonFinalizeArgs)this.MemberwiseClone();
/// <inheritdoc cref="Clone"/>
object ICloneable.Clone() => this.Clone();
}

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

@ -0,0 +1,32 @@
namespace Dalamud.Game.Addon.Lifecycle.AddonArgTypes;
/// <summary>
/// Addon argument data for Hide events.
/// </summary>
public class AddonHideArgs : AddonArgs
{
/// <summary>
/// Initializes a new instance of the <see cref="AddonHideArgs"/> class.
/// </summary>
internal AddonHideArgs()
{
}
/// <inheritdoc/>
public override AddonArgsType Type => AddonArgsType.Hide;
/// <summary>
/// Gets or sets a value indicating whether to call the hide callback handler when this hides.
/// </summary>
public bool CallHideCallback { get; set; }
/// <summary>
/// Gets or sets the flags that the window will set when it Shows/Hides.
/// </summary>
public uint SetShowHideFlags { get; set; }
/// <summary>
/// Gets or sets a value indicating whether something for this event message.
/// </summary>
internal bool UnknownBool { get; set; }
}

View file

@ -3,13 +3,12 @@ namespace Dalamud.Game.Addon.Lifecycle.AddonArgTypes;
/// <summary>
/// Addon argument data for ReceiveEvent events.
/// </summary>
public class AddonReceiveEventArgs : AddonArgs, ICloneable
public class AddonReceiveEventArgs : AddonArgs
{
/// <summary>
/// Initializes a new instance of the <see cref="AddonReceiveEventArgs"/> class.
/// </summary>
[Obsolete("Not intended for public construction.", false)]
public AddonReceiveEventArgs()
internal AddonReceiveEventArgs()
{
}
@ -32,23 +31,7 @@ public class AddonReceiveEventArgs : AddonArgs, ICloneable
public nint AtkEvent { get; set; }
/// <summary>
/// Gets or sets the pointer to a block of data for this event message.
/// Gets or sets the pointer to an AtkEventData for this event message.
/// </summary>
public nint Data { get; set; }
/// <inheritdoc cref="ICloneable.Clone"/>
public AddonReceiveEventArgs Clone() => (AddonReceiveEventArgs)this.MemberwiseClone();
/// <inheritdoc cref="Clone"/>
object ICloneable.Clone() => this.Clone();
/// <inheritdoc cref="AddonArgs.Clear"/>
internal override void Clear()
{
base.Clear();
this.AtkEventType = default;
this.EventParam = default;
this.AtkEvent = default;
this.Data = default;
}
public nint AtkEventData { get; set; }
}

View file

@ -1,17 +1,22 @@
using System.Collections.Generic;
using Dalamud.Game.NativeWrapper;
using Dalamud.Utility;
using FFXIVClientStructs.FFXIV.Component.GUI;
using FFXIVClientStructs.Interop;
namespace Dalamud.Game.Addon.Lifecycle.AddonArgTypes;
/// <summary>
/// Addon argument data for Refresh events.
/// </summary>
public class AddonRefreshArgs : AddonArgs, ICloneable
public class AddonRefreshArgs : AddonArgs
{
/// <summary>
/// Initializes a new instance of the <see cref="AddonRefreshArgs"/> class.
/// </summary>
[Obsolete("Not intended for public construction.", false)]
public AddonRefreshArgs()
internal AddonRefreshArgs()
{
}
@ -31,19 +36,32 @@ public class AddonRefreshArgs : AddonArgs, ICloneable
/// <summary>
/// Gets the AtkValues in the form of a span.
/// </summary>
[Obsolete("Pending removal, Use AtkValueEnumerable instead.")]
[Api15ToDo("Make this internal, remove obsolete")]
public unsafe Span<AtkValue> AtkValueSpan => new(this.AtkValues.ToPointer(), (int)this.AtkValueCount);
/// <inheritdoc cref="ICloneable.Clone"/>
public AddonRefreshArgs Clone() => (AddonRefreshArgs)this.MemberwiseClone();
/// <inheritdoc cref="Clone"/>
object ICloneable.Clone() => this.Clone();
/// <inheritdoc cref="AddonArgs.Clear"/>
internal override void Clear()
/// <summary>
/// Gets an enumerable collection of <see cref="AtkValuePtr"/> of the event's AtkValues.
/// </summary>
/// <returns>
/// An <see cref="IEnumerable{T}"/> of <see cref="AtkValuePtr"/> corresponding to the event's AtkValues.
/// </returns>
public IEnumerable<AtkValuePtr> AtkValueEnumerable
{
base.Clear();
this.AtkValueCount = default;
this.AtkValues = default;
get
{
for (var i = 0; i < this.AtkValueCount; i++)
{
AtkValuePtr ptr;
unsafe
{
#pragma warning disable CS0618 // Type or member is obsolete
ptr = new AtkValuePtr((nint)this.AtkValueSpan.GetPointer(i));
#pragma warning restore CS0618 // Type or member is obsolete
}
yield return ptr;
}
}
}
}

View file

@ -3,13 +3,12 @@ namespace Dalamud.Game.Addon.Lifecycle.AddonArgTypes;
/// <summary>
/// Addon argument data for OnRequestedUpdate events.
/// </summary>
public class AddonRequestedUpdateArgs : AddonArgs, ICloneable
public class AddonRequestedUpdateArgs : AddonArgs
{
/// <summary>
/// Initializes a new instance of the <see cref="AddonRequestedUpdateArgs"/> class.
/// </summary>
[Obsolete("Not intended for public construction.", false)]
public AddonRequestedUpdateArgs()
internal AddonRequestedUpdateArgs()
{
}
@ -25,18 +24,4 @@ public class AddonRequestedUpdateArgs : AddonArgs, ICloneable
/// Gets or sets the StringArrayData** for this event.
/// </summary>
public nint StringArrayData { get; set; }
/// <inheritdoc cref="ICloneable.Clone"/>
public AddonRequestedUpdateArgs Clone() => (AddonRequestedUpdateArgs)this.MemberwiseClone();
/// <inheritdoc cref="Clone"/>
object ICloneable.Clone() => this.Clone();
/// <inheritdoc cref="AddonArgs.Clear"/>
internal override void Clear()
{
base.Clear();
this.NumberArrayData = default;
this.StringArrayData = default;
}
}

View file

@ -1,17 +1,22 @@
using System.Collections.Generic;
using Dalamud.Game.NativeWrapper;
using Dalamud.Utility;
using FFXIVClientStructs.FFXIV.Component.GUI;
using FFXIVClientStructs.Interop;
namespace Dalamud.Game.Addon.Lifecycle.AddonArgTypes;
/// <summary>
/// Addon argument data for Setup events.
/// </summary>
public class AddonSetupArgs : AddonArgs, ICloneable
public class AddonSetupArgs : AddonArgs
{
/// <summary>
/// Initializes a new instance of the <see cref="AddonSetupArgs"/> class.
/// </summary>
[Obsolete("Not intended for public construction.", false)]
public AddonSetupArgs()
internal AddonSetupArgs()
{
}
@ -31,19 +36,32 @@ public class AddonSetupArgs : AddonArgs, ICloneable
/// <summary>
/// Gets the AtkValues in the form of a span.
/// </summary>
[Obsolete("Pending removal, Use AtkValueEnumerable instead.")]
[Api15ToDo("Make this internal, remove obsolete")]
public unsafe Span<AtkValue> AtkValueSpan => new(this.AtkValues.ToPointer(), (int)this.AtkValueCount);
/// <inheritdoc cref="ICloneable.Clone"/>
public AddonSetupArgs Clone() => (AddonSetupArgs)this.MemberwiseClone();
/// <inheritdoc cref="Clone"/>
object ICloneable.Clone() => this.Clone();
/// <inheritdoc cref="AddonArgs.Clear"/>
internal override void Clear()
/// <summary>
/// Gets an enumerable collection of <see cref="AtkValuePtr"/> of the event's AtkValues.
/// </summary>
/// <returns>
/// An <see cref="IEnumerable{T}"/> of <see cref="AtkValuePtr"/> corresponding to the event's AtkValues.
/// </returns>
public IEnumerable<AtkValuePtr> AtkValueEnumerable
{
base.Clear();
this.AtkValueCount = default;
this.AtkValues = default;
get
{
for (var i = 0; i < this.AtkValueCount; i++)
{
AtkValuePtr ptr;
unsafe
{
#pragma warning disable CS0618 // Type or member is obsolete
ptr = new AtkValuePtr((nint)this.AtkValueSpan.GetPointer(i));
#pragma warning restore CS0618 // Type or member is obsolete
}
yield return ptr;
}
}
}
}

View file

@ -0,0 +1,27 @@
namespace Dalamud.Game.Addon.Lifecycle.AddonArgTypes;
/// <summary>
/// Addon argument data for Show events.
/// </summary>
public class AddonShowArgs : AddonArgs
{
/// <summary>
/// Initializes a new instance of the <see cref="AddonShowArgs"/> class.
/// </summary>
internal AddonShowArgs()
{
}
/// <inheritdoc/>
public override AddonArgsType Type => AddonArgsType.Show;
/// <summary>
/// Gets or sets a value indicating whether the window should play open sound effects.
/// </summary>
public bool SilenceOpenSoundEffect { get; set; }
/// <summary>
/// Gets or sets the flags that the window will unset when it Shows/Hides.
/// </summary>
public uint UnsetShowHideFlags { get; set; }
}

View file

@ -1,45 +0,0 @@
namespace Dalamud.Game.Addon.Lifecycle.AddonArgTypes;
/// <summary>
/// Addon argument data for Update events.
/// </summary>
public class AddonUpdateArgs : AddonArgs, ICloneable
{
/// <summary>
/// Initializes a new instance of the <see cref="AddonUpdateArgs"/> class.
/// </summary>
[Obsolete("Not intended for public construction.", false)]
public AddonUpdateArgs()
{
}
/// <inheritdoc/>
public override AddonArgsType Type => AddonArgsType.Update;
/// <summary>
/// Gets the time since the last update.
/// </summary>
public float TimeDelta
{
get => this.TimeDeltaInternal;
init => this.TimeDeltaInternal = value;
}
/// <summary>
/// Gets or sets the time since the last update.
/// </summary>
internal float TimeDeltaInternal { get; set; }
/// <inheritdoc cref="ICloneable.Clone"/>
public AddonUpdateArgs Clone() => (AddonUpdateArgs)this.MemberwiseClone();
/// <inheritdoc cref="Clone"/>
object ICloneable.Clone() => this.Clone();
/// <inheritdoc cref="AddonArgs.Clear"/>
internal override void Clear()
{
base.Clear();
this.TimeDeltaInternal = default;
}
}

View file

@ -5,38 +5,48 @@
/// </summary>
public enum AddonArgsType
{
/// <summary>
/// Generic arg type that contains no meaningful data.
/// </summary>
Generic,
/// <summary>
/// Contains argument data for Setup.
/// </summary>
Setup,
/// <summary>
/// Contains argument data for Update.
/// </summary>
Update,
/// <summary>
/// Contains argument data for Draw.
/// </summary>
Draw,
/// <summary>
/// Contains argument data for Finalize.
/// </summary>
Finalize,
/// <summary>
/// Contains argument data for RequestedUpdate.
/// </summary>
/// </summary>
RequestedUpdate,
/// <summary>
/// Contains argument data for Refresh.
/// </summary>
/// </summary>
Refresh,
/// <summary>
/// Contains argument data for ReceiveEvent.
/// </summary>
ReceiveEvent,
/// <summary>
/// Contains argument data for Show.
/// </summary>
Show,
/// <summary>
/// Contains argument data for Hide.
/// </summary>
Hide,
/// <summary>
/// Contains argument data for Close.
/// </summary>
Close,
/// <summary>
/// Contains argument data for OnFocusChanged.
/// </summary>
FocusChanged,
}

View file

@ -16,7 +16,7 @@ public enum AddonEvent
/// </summary>
/// <seealso cref="AddonSetupArgs"/>
PreSetup,
/// <summary>
/// An event that is fired after an addon has finished its initial setup. This event is particularly useful for
/// developers seeking to add custom elements to now-initialized and populated node lists, as well as reading data
@ -29,7 +29,6 @@ public enum AddonEvent
/// An event that is fired before an addon begins its update cycle via <see cref="AtkUnitBase.Update"/>. This event
/// is fired every frame that an addon is loaded, regardless of visibility.
/// </summary>
/// <seealso cref="AddonUpdateArgs"/>
PreUpdate,
/// <summary>
@ -42,7 +41,6 @@ public enum AddonEvent
/// An event that is fired before an addon begins drawing to screen via <see cref="AtkUnitBase.Draw"/>. Unlike
/// <see cref="PreUpdate"/>, this event is only fired if an addon is visible or otherwise drawing to screen.
/// </summary>
/// <seealso cref="AddonDrawArgs"/>
PreDraw,
/// <summary>
@ -62,9 +60,8 @@ public enum AddonEvent
/// <br />
/// As this is part of the destruction process for an addon, this event does not have an associated Post event.
/// </remarks>
/// <seealso cref="AddonFinalizeArgs"/>
PreFinalize,
/// <summary>
/// An event that is fired before a call to <see cref="AtkUnitBase.OnRequestedUpdate"/> is made in response to a
/// change in the subscribed <see cref="AddonRequestedUpdateArgs.NumberArrayData"/> or
@ -81,13 +78,13 @@ public enum AddonEvent
/// to the Free Company's overview.
/// </example>
PreRequestedUpdate,
/// <summary>
/// An event that is fired after an addon has finished processing an <c>ArrayData</c> update.
/// See <see cref="PreRequestedUpdate"/> for more information.
/// </summary>
PostRequestedUpdate,
/// <summary>
/// An event that is fired before an addon calls its <see cref="AtkUnitManager.RefreshAddon"/> method. Refreshes are
/// generally triggered in response to certain user interactions such as changing tabs, and are primarily used to
@ -96,13 +93,13 @@ public enum AddonEvent
/// <seealso cref="AddonRefreshArgs"/>
/// <seealso cref="PostRefresh"/>
PreRefresh,
/// <summary>
/// An event that is fired after an addon has finished its refresh.
/// See <see cref="PreRefresh"/> for more information.
/// </summary>
PostRefresh,
/// <summary>
/// An event that is fired before an addon begins processing a user-driven event via
/// <see cref="AtkEventListener.ReceiveEvent"/>, such as mousing over an element or clicking a button. This event
@ -112,10 +109,108 @@ public enum AddonEvent
/// <seealso cref="AddonReceiveEventArgs"/>
/// <seealso cref="PostReceiveEvent"/>
PreReceiveEvent,
/// <summary>
/// An event that is fired after an addon finishes calling its <see cref="AtkEventListener.ReceiveEvent"/> method.
/// See <see cref="PreReceiveEvent"/> for more information.
/// </summary>
PostReceiveEvent,
/// <summary>
/// An event that is fired before an addon processes its open method.
/// </summary>
PreOpen,
/// <summary>
/// An event that is fired after an addon has processed its open method.
/// </summary>
PostOpen,
/// <summary>
/// An even that is fired before an addon processes its Close method.
/// </summary>
PreClose,
/// <summary>
/// An event that is fired after an addon has processed its Close method.
/// </summary>
PostClose,
/// <summary>
/// An event that is fired before an addon processes its Show method.
/// </summary>
PreShow,
/// <summary>
/// An event that is fired after an addon has processed its Show method.
/// </summary>
PostShow,
/// <summary>
/// An event that is fired before an addon processes its Hide method.
/// </summary>
PreHide,
/// <summary>
/// An event that is fired after an addon has processed its Hide method.
/// </summary>
PostHide,
/// <summary>
/// An event that is fired before an addon processes its OnMove method.
/// OnMove is triggered only when a move is completed.
/// </summary>
PreMove,
/// <summary>
/// An event that is fired after an addon has processed its OnMove method.
/// OnMove is triggered only when a move is completed.
/// </summary>
PostMove,
/// <summary>
/// An event that is fired before an addon processes its MouseOver method.
/// </summary>
PreMouseOver,
/// <summary>
/// An event that is fired after an addon has processed its MouseOver method.
/// </summary>
PostMouseOver,
/// <summary>
/// An event that is fired before an addon processes its MouseOut method.
/// </summary>
PreMouseOut,
/// <summary>
/// An event that is fired after an addon has processed its MouseOut method.
/// </summary>
PostMouseOut,
/// <summary>
/// An event that is fired before an addon processes its Focus method.
/// </summary>
/// <remarks>
/// Be aware this is only called for certain popup windows, it is not triggered when clicking on windows.
/// </remarks>
PreFocus,
/// <summary>
/// An event that is fired after an addon has processed its Focus method.
/// </summary>
/// <remarks>
/// Be aware this is only called for certain popup windows, it is not triggered when clicking on windows.
/// </remarks>
PostFocus,
/// <summary>
/// An event that is fired before an addon processes its FocusChanged method.
/// </summary>
PreFocusChanged,
/// <summary>
/// An event that is fired after a addon processes its FocusChanged method.
/// </summary>
PostFocusChanged,
}

View file

@ -1,16 +1,15 @@
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Runtime.CompilerServices;
using Dalamud.Game.Addon.Lifecycle.AddonArgTypes;
using Dalamud.Hooking;
using Dalamud.Hooking.Internal;
using Dalamud.IoC;
using Dalamud.IoC.Internal;
using Dalamud.Logging.Internal;
using Dalamud.Plugin.Services;
using FFXIVClientStructs.FFXIV.Client.UI;
using FFXIVClientStructs.FFXIV.Component.GUI;
namespace Dalamud.Game.Addon.Lifecycle;
@ -21,75 +20,56 @@ namespace Dalamud.Game.Addon.Lifecycle;
[ServiceManager.EarlyLoadedService]
internal unsafe class AddonLifecycle : IInternalDisposableService
{
private static readonly ModuleLog Log = new("AddonLifecycle");
/// <summary>
/// Gets a list of all allocated addon virtual tables.
/// </summary>
public static readonly List<AddonVirtualTable> AllocatedTables = [];
private static readonly ModuleLog Log = ModuleLog.Create<AddonLifecycle>();
[ServiceManager.ServiceDependency]
private readonly Framework framework = Service<Framework>.Get();
[ServiceManager.ServiceDependency]
private readonly AddonLifecyclePooledArgs argsPool = Service<AddonLifecyclePooledArgs>.Get();
private readonly nint disallowedReceiveEventAddress;
private readonly AddonLifecycleAddressResolver address;
private readonly AddonSetupHook<AtkUnitBase.Delegates.OnSetup> onAddonSetupHook;
private readonly Hook<AddonFinalizeDelegate> onAddonFinalizeHook;
private readonly CallHook<AtkUnitBase.Delegates.Draw> onAddonDrawHook;
private readonly CallHook<AtkUnitBase.Delegates.Update> onAddonUpdateHook;
private readonly Hook<AtkUnitManager.Delegates.RefreshAddon> onAddonRefreshHook;
private readonly CallHook<AtkUnitBase.Delegates.OnRequestedUpdate> onAddonRequestedUpdateHook;
private Hook<AtkUnitBase.Delegates.Initialize>? onInitializeAddonHook;
private bool isInvokingListeners;
[ServiceManager.ServiceConstructor]
private AddonLifecycle(TargetSigScanner sigScanner)
private AddonLifecycle()
{
this.address = new AddonLifecycleAddressResolver();
this.address.Setup(sigScanner);
this.disallowedReceiveEventAddress = (nint)AtkUnitBase.StaticVirtualTablePointer->ReceiveEvent;
var refreshAddonAddress = (nint)RaptureAtkUnitManager.StaticVirtualTablePointer->RefreshAddon;
this.onAddonSetupHook = new AddonSetupHook<AtkUnitBase.Delegates.OnSetup>(this.address.AddonSetup, this.OnAddonSetup);
this.onAddonFinalizeHook = Hook<AddonFinalizeDelegate>.FromAddress(this.address.AddonFinalize, this.OnAddonFinalize);
this.onAddonDrawHook = new CallHook<AtkUnitBase.Delegates.Draw>(this.address.AddonDraw, this.OnAddonDraw);
this.onAddonUpdateHook = new CallHook<AtkUnitBase.Delegates.Update>(this.address.AddonUpdate, this.OnAddonUpdate);
this.onAddonRefreshHook = Hook<AtkUnitManager.Delegates.RefreshAddon>.FromAddress(refreshAddonAddress, this.OnAddonRefresh);
this.onAddonRequestedUpdateHook = new CallHook<AtkUnitBase.Delegates.OnRequestedUpdate>(this.address.AddonOnRequestedUpdate, this.OnRequestedUpdate);
this.onAddonSetupHook.Enable();
this.onAddonFinalizeHook.Enable();
this.onAddonDrawHook.Enable();
this.onAddonUpdateHook.Enable();
this.onAddonRefreshHook.Enable();
this.onAddonRequestedUpdateHook.Enable();
this.onInitializeAddonHook = Hook<AtkUnitBase.Delegates.Initialize>.FromAddress((nint)AtkUnitBase.StaticVirtualTablePointer->Initialize, this.OnAddonInitialize);
this.onInitializeAddonHook.Enable();
}
private delegate void AddonFinalizeDelegate(AtkUnitManager* unitManager, AtkUnitBase** atkUnitBase);
/// <summary>
/// Gets a list of all AddonLifecycle ReceiveEvent Listener Hooks.
/// </summary>
internal List<AddonLifecycleReceiveEventListener> ReceiveEventListeners { get; } = new();
/// <summary>
/// Gets a list of all AddonLifecycle Event Listeners.
/// </summary>
internal List<AddonLifecycleEventListener> EventListeners { get; } = new();
/// </summary> <br/>
/// Mapping is: EventType -> AddonName -> ListenerList
internal Dictionary<AddonEvent, Dictionary<string, HashSet<AddonLifecycleEventListener>>> EventListeners { get; } = [];
/// <inheritdoc/>
void IInternalDisposableService.DisposeService()
{
this.onAddonSetupHook.Dispose();
this.onAddonFinalizeHook.Dispose();
this.onAddonDrawHook.Dispose();
this.onAddonUpdateHook.Dispose();
this.onAddonRefreshHook.Dispose();
this.onAddonRequestedUpdateHook.Dispose();
this.onInitializeAddonHook?.Dispose();
this.onInitializeAddonHook = null;
foreach (var receiveEventListener in this.ReceiveEventListeners)
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 AtkUnitBase.AtkUnitBaseVirtualTable* GetOriginalVirtualTable(AtkUnitBase.AtkUnitBaseVirtualTable* tableAddress)
{
var matchedTable = AllocatedTables.FirstOrDefault(table => table.ModifiedVirtualTable == tableAddress);
if (matchedTable == null)
{
receiveEventListener.Dispose();
return null;
}
return matchedTable.OriginalVirtualTable;
}
/// <summary>
@ -98,20 +78,14 @@ internal unsafe class AddonLifecycle : IInternalDisposableService
/// <param name="listener">The listener to register.</param>
internal void RegisterListener(AddonLifecycleEventListener listener)
{
this.framework.RunOnTick(() =>
if (this.isInvokingListeners)
{
this.EventListeners.Add(listener);
// If we want receive event messages have an already active addon, enable the receive event hook.
// If the addon isn't active yet, we'll grab the hook when it sets up.
if (listener is { EventType: AddonEvent.PreReceiveEvent or AddonEvent.PostReceiveEvent })
{
if (this.ReceiveEventListeners.FirstOrDefault(listeners => listeners.AddonNames.Contains(listener.AddonName)) is { } receiveEventListener)
{
receiveEventListener.TryEnable();
}
}
});
this.framework.RunOnTick(() => this.RegisterListenerMethod(listener));
}
else
{
this.framework.RunOnFrameworkThread(() => this.RegisterListenerMethod(listener));
}
}
/// <summary>
@ -120,27 +94,16 @@ internal unsafe class AddonLifecycle : IInternalDisposableService
/// <param name="listener">The listener to unregister.</param>
internal void UnregisterListener(AddonLifecycleEventListener listener)
{
// Set removed state to true immediately, then lazily remove it from the EventListeners list on next Framework Update.
listener.Removed = true;
listener.IsRequestedToClear = true;
this.framework.RunOnTick(() =>
if (this.isInvokingListeners)
{
this.EventListeners.Remove(listener);
// If we are disabling an ReceiveEvent listener, check if we should disable the hook.
if (listener is { EventType: AddonEvent.PreReceiveEvent or AddonEvent.PostReceiveEvent })
{
// Get the ReceiveEvent Listener for this addon
if (this.ReceiveEventListeners.FirstOrDefault(listeners => listeners.AddonNames.Contains(listener.AddonName)) is { } receiveEventListener)
{
// If there are no other listeners listening for this event, disable the hook.
if (!this.EventListeners.Any(listeners => listeners.AddonName.Contains(listener.AddonName) && listener.EventType is AddonEvent.PreReceiveEvent or AddonEvent.PostReceiveEvent))
{
receiveEventListener.Disable();
}
}
}
});
this.framework.RunOnTick(() => this.UnregisterListenerMethod(listener));
}
else
{
this.framework.RunOnFrameworkThread(() => this.UnregisterListenerMethod(listener));
}
}
/// <summary>
@ -151,226 +114,104 @@ internal unsafe class AddonLifecycle : IInternalDisposableService
/// <param name="blame">What to blame on errors.</param>
internal void InvokeListenersSafely(AddonEvent eventType, AddonArgs args, [CallerMemberName] string blame = "")
{
// Do not use linq; this is a high-traffic function, and more heap allocations avoided, the better.
foreach (var listener in this.EventListeners)
this.isInvokingListeners = true;
// Early return if we don't have any listeners of this type
if (!this.EventListeners.TryGetValue(eventType, out var addonListeners)) return;
// Handle listeners for this event type that don't care which addon is triggering it
if (addonListeners.TryGetValue(string.Empty, out var globalListeners))
{
if (listener.EventType != eventType)
continue;
// If the listener is pending removal, and is waiting until the next Framework Update, don't invoke listener.
if (listener.Removed)
continue;
// Match on string.empty for listeners that want events for all addons.
if (!string.IsNullOrWhiteSpace(listener.AddonName) && !args.IsAddon(listener.AddonName))
continue;
try
foreach (var listener in globalListeners)
{
listener.FunctionDelegate.Invoke(eventType, args);
}
catch (Exception e)
{
Log.Error(e, $"Exception in {blame} during {eventType} invoke.");
}
}
}
private void RegisterReceiveEventHook(AtkUnitBase* addon)
{
// Hook the addon's ReceiveEvent function here, but only enable the hook if we have an active listener.
// Disallows hooking the core internal event handler.
var addonName = addon->NameString;
var receiveEventAddress = (nint)addon->VirtualTable->ReceiveEvent;
if (receiveEventAddress != this.disallowedReceiveEventAddress)
{
// If we have a ReceiveEvent listener already made for this hook address, add this addon's name to that handler.
if (this.ReceiveEventListeners.FirstOrDefault(listener => listener.FunctionAddress == receiveEventAddress) is { } existingListener)
{
if (!existingListener.AddonNames.Contains(addonName))
if (listener.IsRequestedToClear) continue;
try
{
existingListener.AddonNames.Add(addonName);
listener.FunctionDelegate.Invoke(eventType, args);
}
}
// Else, we have an addon that we don't have the ReceiveEvent for yet, make it.
else
{
this.ReceiveEventListeners.Add(new AddonLifecycleReceiveEventListener(this, addonName, receiveEventAddress));
}
// If we have an active listener for this addon already, we need to activate this hook.
if (this.EventListeners.Any(listener => (listener.EventType is AddonEvent.PostReceiveEvent or AddonEvent.PreReceiveEvent) && listener.AddonName == addonName))
{
if (this.ReceiveEventListeners.FirstOrDefault(listener => listener.AddonNames.Contains(addonName)) is { } receiveEventListener)
catch (Exception e)
{
receiveEventListener.TryEnable();
Log.Error(e, $"Exception in {blame} during {eventType} invoke, for global addon event listener.");
}
}
}
// Handle listeners that are listening for this addon and event type specifically
if (addonListeners.TryGetValue(args.AddonName, out var addonListener))
{
foreach (var listener in addonListener)
{
if (listener.IsRequestedToClear) continue;
try
{
listener.FunctionDelegate.Invoke(eventType, args);
}
catch (Exception e)
{
Log.Error(e, $"Exception in {blame} during {eventType} invoke, for specific addon {args.AddonName}.");
}
}
}
this.isInvokingListeners = false;
}
private void UnregisterReceiveEventHook(string addonName)
private void RegisterListenerMethod(AddonLifecycleEventListener listener)
{
// Remove this addons ReceiveEvent Registration
if (this.ReceiveEventListeners.FirstOrDefault(listener => listener.AddonNames.Contains(addonName)) is { } eventListener)
if (!this.EventListeners.ContainsKey(listener.EventType))
{
eventListener.AddonNames.Remove(addonName);
// If there are no more listeners let's remove and dispose.
if (eventListener.AddonNames.Count is 0)
if (!this.EventListeners.TryAdd(listener.EventType, []))
{
this.ReceiveEventListeners.Remove(eventListener);
eventListener.Dispose();
return;
}
}
// 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 OnAddonSetup(AtkUnitBase* addon, uint valueCount, AtkValue* values)
private void OnAddonInitialize(AtkUnitBase* addon)
{
try
{
this.RegisterReceiveEventHook(addon);
this.LogInitialize(addon->NameString);
// AddonVirtualTable class handles creating the virtual table, and overriding each of the tracked virtual functions
AllocatedTables.Add(new AddonVirtualTable(addon, this));
}
catch (Exception e)
{
Log.Error(e, "Exception in OnAddonSetup ReceiveEvent Registration.");
Log.Error(e, "Exception in AddonLifecycle during OnAddonInitialize.");
}
using var returner = this.argsPool.Rent(out AddonSetupArgs arg);
arg.Clear();
arg.Addon = (nint)addon;
arg.AtkValueCount = valueCount;
arg.AtkValues = (nint)values;
this.InvokeListenersSafely(AddonEvent.PreSetup, arg);
valueCount = arg.AtkValueCount;
values = (AtkValue*)arg.AtkValues;
try
{
addon->OnSetup(valueCount, values);
}
catch (Exception e)
{
Log.Error(e, "Caught exception when calling original AddonSetup. This may be a bug in the game or another plugin hooking this method.");
}
this.InvokeListenersSafely(AddonEvent.PostSetup, arg);
this.onInitializeAddonHook!.Original(addon);
}
private void OnAddonFinalize(AtkUnitManager* unitManager, AtkUnitBase** atkUnitBase)
[Conditional("DEBUG")]
private void LogInitialize(string addonName)
{
try
{
var addonName = atkUnitBase[0]->NameString;
this.UnregisterReceiveEventHook(addonName);
}
catch (Exception e)
{
Log.Error(e, "Exception in OnAddonFinalize ReceiveEvent Removal.");
}
using var returner = this.argsPool.Rent(out AddonFinalizeArgs arg);
arg.Clear();
arg.Addon = (nint)atkUnitBase[0];
this.InvokeListenersSafely(AddonEvent.PreFinalize, arg);
try
{
this.onAddonFinalizeHook.Original(unitManager, atkUnitBase);
}
catch (Exception e)
{
Log.Error(e, "Caught exception when calling original AddonFinalize. This may be a bug in the game or another plugin hooking this method.");
}
}
private void OnAddonDraw(AtkUnitBase* addon)
{
using var returner = this.argsPool.Rent(out AddonDrawArgs arg);
arg.Clear();
arg.Addon = (nint)addon;
this.InvokeListenersSafely(AddonEvent.PreDraw, arg);
try
{
addon->Draw();
}
catch (Exception e)
{
Log.Error(e, "Caught exception when calling original AddonDraw. This may be a bug in the game or another plugin hooking this method.");
}
this.InvokeListenersSafely(AddonEvent.PostDraw, arg);
}
private void OnAddonUpdate(AtkUnitBase* addon, float delta)
{
using var returner = this.argsPool.Rent(out AddonUpdateArgs arg);
arg.Clear();
arg.Addon = (nint)addon;
arg.TimeDeltaInternal = delta;
this.InvokeListenersSafely(AddonEvent.PreUpdate, arg);
try
{
addon->Update(delta);
}
catch (Exception e)
{
Log.Error(e, "Caught exception when calling original AddonUpdate. This may be a bug in the game or another plugin hooking this method.");
}
this.InvokeListenersSafely(AddonEvent.PostUpdate, arg);
}
private bool OnAddonRefresh(AtkUnitManager* thisPtr, AtkUnitBase* addon, uint valueCount, AtkValue* values)
{
var result = false;
using var returner = this.argsPool.Rent(out AddonRefreshArgs arg);
arg.Clear();
arg.Addon = (nint)addon;
arg.AtkValueCount = valueCount;
arg.AtkValues = (nint)values;
this.InvokeListenersSafely(AddonEvent.PreRefresh, arg);
valueCount = arg.AtkValueCount;
values = (AtkValue*)arg.AtkValues;
try
{
result = this.onAddonRefreshHook.Original(thisPtr, addon, valueCount, values);
}
catch (Exception e)
{
Log.Error(e, "Caught exception when calling original AddonRefresh. This may be a bug in the game or another plugin hooking this method.");
}
this.InvokeListenersSafely(AddonEvent.PostRefresh, arg);
return result;
}
private void OnRequestedUpdate(AtkUnitBase* addon, NumberArrayData** numberArrayData, StringArrayData** stringArrayData)
{
using var returner = this.argsPool.Rent(out AddonRequestedUpdateArgs arg);
arg.Clear();
arg.Addon = (nint)addon;
arg.NumberArrayData = (nint)numberArrayData;
arg.StringArrayData = (nint)stringArrayData;
this.InvokeListenersSafely(AddonEvent.PreRequestedUpdate, arg);
numberArrayData = (NumberArrayData**)arg.NumberArrayData;
stringArrayData = (StringArrayData**)arg.StringArrayData;
try
{
addon->OnRequestedUpdate(numberArrayData, stringArrayData);
}
catch (Exception e)
{
Log.Error(e, "Caught exception when calling original AddonRequestedUpdate. This may be a bug in the game or another plugin hooking this method.");
}
this.InvokeListenersSafely(AddonEvent.PostRequestedUpdate, arg);
Log.Debug($"Initializing {addonName}");
}
}
@ -387,7 +228,7 @@ internal class AddonLifecyclePluginScoped : IInternalDisposableService, IAddonLi
[ServiceManager.ServiceDependency]
private readonly AddonLifecycle addonLifecycleService = Service<AddonLifecycle>.Get();
private readonly List<AddonLifecycleEventListener> eventListeners = new();
private readonly List<AddonLifecycleEventListener> eventListeners = [];
/// <inheritdoc/>
void IInternalDisposableService.DisposeService()
@ -458,10 +299,14 @@ internal class AddonLifecyclePluginScoped : IInternalDisposableService, IAddonLi
this.eventListeners.RemoveAll(entry =>
{
if (entry.FunctionDelegate != handler) return false;
this.addonLifecycleService.UnregisterListener(entry);
return true;
});
}
}
/// <inheritdoc/>
public unsafe nint GetOriginalVirtualTable(nint virtualTableAddress)
=> (nint)AddonLifecycle.GetOriginalVirtualTable((AtkUnitBase.AtkUnitBaseVirtualTable*)virtualTableAddress);
}

View file

@ -1,56 +0,0 @@
using FFXIVClientStructs.FFXIV.Component.GUI;
namespace Dalamud.Game.Addon.Lifecycle;
/// <summary>
/// AddonLifecycleService memory address resolver.
/// </summary>
internal unsafe class AddonLifecycleAddressResolver : BaseAddressResolver
{
/// <summary>
/// Gets the address of the addon setup hook invoked by the AtkUnitManager.
/// There are two callsites for this vFunc, we need to hook both of them to catch both normal UI and special UI cases like dialogue.
/// This is called for a majority of all addon OnSetup's.
/// </summary>
public nint AddonSetup { get; private set; }
/// <summary>
/// Gets the address of the other addon setup hook invoked by the AtkUnitManager.
/// There are two callsites for this vFunc, we need to hook both of them to catch both normal UI and special UI cases like dialogue.
/// This seems to be called rarely for specific addons.
/// </summary>
public nint AddonSetup2 { get; private set; }
/// <summary>
/// Gets the address of the addon finalize hook invoked by the AtkUnitManager.
/// </summary>
public nint AddonFinalize { get; private set; }
/// <summary>
/// Gets the address of the addon draw hook invoked by virtual function call.
/// </summary>
public nint AddonDraw { get; private set; }
/// <summary>
/// Gets the address of the addon update hook invoked by virtual function call.
/// </summary>
public nint AddonUpdate { get; private set; }
/// <summary>
/// Gets the address of the addon onRequestedUpdate hook invoked by virtual function call.
/// </summary>
public nint AddonOnRequestedUpdate { get; private set; }
/// <summary>
/// Scan for and setup any configured address pointers.
/// </summary>
/// <param name="sig">The signature scanner to facilitate setup.</param>
protected override void Setup64Bit(ISigScanner sig)
{
this.AddonSetup = sig.ScanText("4C 8B 88 ?? ?? ?? ?? 66 44 39 BB");
this.AddonFinalize = sig.ScanText("E8 ?? ?? ?? ?? 48 83 EF 01 75 D5");
this.AddonDraw = sig.ScanText("FF 90 ?? ?? ?? ?? 83 EB 01 79 C4 48 81 EF ?? ?? ?? ?? 48 83 ED 01");
this.AddonUpdate = sig.ScanText("FF 90 ?? ?? ?? ?? 40 88 AF ?? ?? ?? ?? 45 33 D2");
this.AddonOnRequestedUpdate = sig.ScanText("FF 90 A0 01 00 00 48 8B 5C 24 30");
}
}

View file

@ -25,19 +25,19 @@ internal class AddonLifecycleEventListener
/// string.Empty if it wants to be called for any addon.
/// </summary>
public string AddonName { get; init; }
/// <summary>
/// Gets or sets a value indicating whether this event has been unregistered.
/// </summary>
public bool Removed { get; set; }
/// <summary>
/// Gets the event type this listener is looking for.
/// </summary>
public AddonEvent EventType { get; init; }
/// <summary>
/// Gets the delegate this listener invokes.
/// </summary>
public IAddonLifecycle.AddonEventDelegate FunctionDelegate { get; init; }
/// <summary>
/// Gets or sets if the listener is requested to be cleared.
/// </summary>
internal bool IsRequestedToClear { get; set; }
}

View file

@ -1,112 +0,0 @@
using System.Collections.Generic;
using Dalamud.Game.Addon.Lifecycle.AddonArgTypes;
using Dalamud.Hooking;
using Dalamud.Logging.Internal;
using FFXIVClientStructs.FFXIV.Component.GUI;
namespace Dalamud.Game.Addon.Lifecycle;
/// <summary>
/// This class is a helper for tracking and invoking listener delegates for Addon_OnReceiveEvent.
/// Multiple addons may use the same ReceiveEvent function, this helper makes sure that those addon events are handled properly.
/// </summary>
internal unsafe class AddonLifecycleReceiveEventListener : IDisposable
{
private static readonly ModuleLog Log = new("AddonLifecycle");
[ServiceManager.ServiceDependency]
private readonly AddonLifecyclePooledArgs argsPool = Service<AddonLifecyclePooledArgs>.Get();
/// <summary>
/// Initializes a new instance of the <see cref="AddonLifecycleReceiveEventListener"/> class.
/// </summary>
/// <param name="service">AddonLifecycle service instance.</param>
/// <param name="addonName">Initial Addon Requesting this listener.</param>
/// <param name="receiveEventAddress">Address of Addon's ReceiveEvent function.</param>
internal AddonLifecycleReceiveEventListener(AddonLifecycle service, string addonName, nint receiveEventAddress)
{
this.AddonLifecycle = service;
this.AddonNames = [addonName];
this.FunctionAddress = receiveEventAddress;
}
/// <summary>
/// Gets the list of addons that use this receive event hook.
/// </summary>
public List<string> AddonNames { get; init; }
/// <summary>
/// Gets the address of the ReceiveEvent function as provided by the vtable on setup.
/// </summary>
public nint FunctionAddress { get; init; }
/// <summary>
/// Gets the contained hook for these addons.
/// </summary>
public Hook<AtkUnitBase.Delegates.ReceiveEvent>? Hook { get; private set; }
/// <summary>
/// Gets or sets the Reference to AddonLifecycle service instance.
/// </summary>
private AddonLifecycle AddonLifecycle { get; set; }
/// <summary>
/// Try to hook and enable this receive event handler.
/// </summary>
public void TryEnable()
{
this.Hook ??= Hook<AtkUnitBase.Delegates.ReceiveEvent>.FromAddress(this.FunctionAddress, this.OnReceiveEvent);
this.Hook?.Enable();
}
/// <summary>
/// Disable the hook for this receive event handler.
/// </summary>
public void Disable()
{
this.Hook?.Disable();
}
/// <inheritdoc/>
public void Dispose()
{
this.Hook?.Dispose();
}
private void OnReceiveEvent(AtkUnitBase* addon, AtkEventType eventType, int eventParam, AtkEvent* atkEvent, AtkEventData* atkEventData)
{
// Check that we didn't get here through a call to another addons handler.
var addonName = addon->NameString;
if (!this.AddonNames.Contains(addonName))
{
this.Hook!.Original(addon, eventType, eventParam, atkEvent, atkEventData);
return;
}
using var returner = this.argsPool.Rent(out AddonReceiveEventArgs arg);
arg.Clear();
arg.Addon = (nint)addon;
arg.AtkEventType = (byte)eventType;
arg.EventParam = eventParam;
arg.AtkEvent = (IntPtr)atkEvent;
arg.Data = (nint)atkEventData;
this.AddonLifecycle.InvokeListenersSafely(AddonEvent.PreReceiveEvent, arg);
eventType = (AtkEventType)arg.AtkEventType;
eventParam = arg.EventParam;
atkEvent = (AtkEvent*)arg.AtkEvent;
atkEventData = (AtkEventData*)arg.Data;
try
{
this.Hook!.Original(addon, eventType, eventParam, atkEvent, atkEventData);
}
catch (Exception e)
{
Log.Error(e, "Caught exception when calling original AddonReceiveEvent. This may be a bug in the game or another plugin hooking this method.");
}
this.AddonLifecycle.InvokeListenersSafely(AddonEvent.PostReceiveEvent, arg);
}
}

View file

@ -1,80 +0,0 @@
using System.Runtime.InteropServices;
using Reloaded.Hooks.Definitions;
namespace Dalamud.Game.Addon.Lifecycle;
/// <summary>
/// This class represents a callsite hook used to replace the address of the OnSetup function in r9.
/// </summary>
/// <typeparam name="T">Delegate signature for this hook.</typeparam>
internal class AddonSetupHook<T> : IDisposable where T : Delegate
{
private readonly Reloaded.Hooks.AsmHook asmHook;
private T? detour;
private bool activated;
/// <summary>
/// Initializes a new instance of the <see cref="AddonSetupHook{T}"/> class.
/// </summary>
/// <param name="address">Address of the instruction to replace.</param>
/// <param name="detour">Delegate to invoke.</param>
internal AddonSetupHook(nint address, T detour)
{
this.detour = detour;
var detourPtr = Marshal.GetFunctionPointerForDelegate(this.detour);
var code = new[]
{
"use64",
$"mov r9, 0x{detourPtr:X8}",
};
var opt = new AsmHookOptions
{
PreferRelativeJump = true,
Behaviour = Reloaded.Hooks.Definitions.Enums.AsmHookBehaviour.DoNotExecuteOriginal,
MaxOpcodeSize = 5,
};
this.asmHook = new Reloaded.Hooks.AsmHook(code, (nuint)address, opt);
}
/// <summary>
/// Gets a value indicating whether the hook is enabled.
/// </summary>
public bool IsEnabled => this.asmHook.IsEnabled;
/// <summary>
/// Starts intercepting a call to the function.
/// </summary>
public void Enable()
{
if (!this.activated)
{
this.activated = true;
this.asmHook.Activate();
return;
}
this.asmHook.Enable();
}
/// <summary>
/// Stops intercepting a call to the function.
/// </summary>
public void Disable()
{
this.asmHook.Disable();
}
/// <summary>
/// Remove a hook from the current process.
/// </summary>
public void Dispose()
{
this.asmHook.Disable();
this.detour = null;
}
}

View file

@ -0,0 +1,679 @@
using System.Diagnostics;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using System.Threading;
using Dalamud.Game.Addon.Lifecycle.AddonArgTypes;
using Dalamud.Logging.Internal;
using FFXIVClientStructs.FFXIV.Client.System.Memory;
using FFXIVClientStructs.FFXIV.Component.GUI;
namespace Dalamud.Game.Addon.Lifecycle;
/// <summary>
/// Represents a class that holds references to an addons original and modified virtual table entries.
/// </summary>
internal unsafe class AddonVirtualTable : IDisposable
{
// This need to be at minimum the largest virtual table size of all addons
// Copying extra entries is not problematic, and is considered safe.
private const int VirtualTableEntryCount = 200;
private const bool EnableLogging = false;
private static readonly ModuleLog Log = new("LifecycleVT");
private readonly AddonLifecycle lifecycleService;
// Each addon gets its own set of args that are used to mutate the original call when used in pre-calls
private readonly AddonSetupArgs setupArgs = new();
private readonly AddonArgs finalizeArgs = new();
private readonly AddonArgs drawArgs = new();
private readonly AddonArgs updateArgs = new();
private readonly AddonRefreshArgs refreshArgs = new();
private readonly AddonRequestedUpdateArgs requestedUpdateArgs = new();
private readonly AddonReceiveEventArgs receiveEventArgs = new();
private readonly AddonArgs openArgs = new();
private readonly AddonCloseArgs closeArgs = new();
private readonly AddonShowArgs showArgs = new();
private readonly AddonHideArgs hideArgs = new();
private readonly AddonArgs onMoveArgs = new();
private readonly AddonArgs onMouseOverArgs = new();
private readonly AddonArgs onMouseOutArgs = new();
private readonly AddonArgs focusArgs = new();
private readonly AddonFocusChangedArgs focusChangedArgs = new();
private readonly AtkUnitBase* atkUnitBase;
// 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 AtkUnitBase.Delegates.Dtor destructorFunction;
private readonly AtkUnitBase.Delegates.OnSetup onSetupFunction;
private readonly AtkUnitBase.Delegates.Finalizer finalizerFunction;
private readonly AtkUnitBase.Delegates.Draw drawFunction;
private readonly AtkUnitBase.Delegates.Update updateFunction;
private readonly AtkUnitBase.Delegates.OnRefresh onRefreshFunction;
private readonly AtkUnitBase.Delegates.OnRequestedUpdate onRequestedUpdateFunction;
private readonly AtkUnitBase.Delegates.ReceiveEvent onReceiveEventFunction;
private readonly AtkUnitBase.Delegates.Open openFunction;
private readonly AtkUnitBase.Delegates.Close closeFunction;
private readonly AtkUnitBase.Delegates.Show showFunction;
private readonly AtkUnitBase.Delegates.Hide hideFunction;
private readonly AtkUnitBase.Delegates.OnMove onMoveFunction;
private readonly AtkUnitBase.Delegates.OnMouseOver onMouseOverFunction;
private readonly AtkUnitBase.Delegates.OnMouseOut onMouseOutFunction;
private readonly AtkUnitBase.Delegates.Focus focusFunction;
private readonly AtkUnitBase.Delegates.OnFocusChange onFocusChangeFunction;
/// <summary>
/// Initializes a new instance of the <see cref="AddonVirtualTable"/> class.
/// </summary>
/// <param name="addon">AtkUnitBase* for the addon to replace the table of.</param>
/// <param name="lifecycleService">Reference to AddonLifecycle service to callback and invoke listeners.</param>
internal AddonVirtualTable(AtkUnitBase* addon, AddonLifecycle lifecycleService)
{
this.atkUnitBase = addon;
this.lifecycleService = lifecycleService;
// Save original virtual table
this.OriginalVirtualTable = addon->VirtualTable;
// Create copy of original table
// Note this will copy any derived/overriden functions that this specific addon has.
// Note: currently there are 73 virtual functions, but there's no harm in copying more for when they add new virtual functions to the game
this.ModifiedVirtualTable = (AtkUnitBase.AtkUnitBaseVirtualTable*)IMemorySpace.GetUISpace()->Malloc(0x8 * VirtualTableEntryCount, 8);
NativeMemory.Copy(addon->VirtualTable, this.ModifiedVirtualTable, 0x8 * VirtualTableEntryCount);
// Overwrite the addons existing virtual table with our own
addon->VirtualTable = this.ModifiedVirtualTable;
// Pin each of our listener functions
this.destructorFunction = this.OnAddonDestructor;
this.onSetupFunction = this.OnAddonSetup;
this.finalizerFunction = this.OnAddonFinalize;
this.drawFunction = this.OnAddonDraw;
this.updateFunction = this.OnAddonUpdate;
this.onRefreshFunction = this.OnAddonRefresh;
this.onRequestedUpdateFunction = this.OnRequestedUpdate;
this.onReceiveEventFunction = this.OnAddonReceiveEvent;
this.openFunction = this.OnAddonOpen;
this.closeFunction = this.OnAddonClose;
this.showFunction = this.OnAddonShow;
this.hideFunction = this.OnAddonHide;
this.onMoveFunction = this.OnAddonMove;
this.onMouseOverFunction = this.OnAddonMouseOver;
this.onMouseOutFunction = this.OnAddonMouseOut;
this.focusFunction = this.OnAddonFocus;
this.onFocusChangeFunction = this.OnAddonFocusChange;
// Overwrite specific virtual table entries
this.ModifiedVirtualTable->Dtor = (delegate* unmanaged<AtkUnitBase*, byte, AtkEventListener*>)Marshal.GetFunctionPointerForDelegate(this.destructorFunction);
this.ModifiedVirtualTable->OnSetup = (delegate* unmanaged<AtkUnitBase*, uint, AtkValue*, void>)Marshal.GetFunctionPointerForDelegate(this.onSetupFunction);
this.ModifiedVirtualTable->Finalizer = (delegate* unmanaged<AtkUnitBase*, void>)Marshal.GetFunctionPointerForDelegate(this.finalizerFunction);
this.ModifiedVirtualTable->Draw = (delegate* unmanaged<AtkUnitBase*, void>)Marshal.GetFunctionPointerForDelegate(this.drawFunction);
this.ModifiedVirtualTable->Update = (delegate* unmanaged<AtkUnitBase*, float, void>)Marshal.GetFunctionPointerForDelegate(this.updateFunction);
this.ModifiedVirtualTable->OnRefresh = (delegate* unmanaged<AtkUnitBase*, uint, AtkValue*, bool>)Marshal.GetFunctionPointerForDelegate(this.onRefreshFunction);
this.ModifiedVirtualTable->OnRequestedUpdate = (delegate* unmanaged<AtkUnitBase*, NumberArrayData**, StringArrayData**, void>)Marshal.GetFunctionPointerForDelegate(this.onRequestedUpdateFunction);
this.ModifiedVirtualTable->ReceiveEvent = (delegate* unmanaged<AtkUnitBase*, AtkEventType, int, AtkEvent*, AtkEventData*, void>)Marshal.GetFunctionPointerForDelegate(this.onReceiveEventFunction);
this.ModifiedVirtualTable->Open = (delegate* unmanaged<AtkUnitBase*, uint, bool>)Marshal.GetFunctionPointerForDelegate(this.openFunction);
this.ModifiedVirtualTable->Close = (delegate* unmanaged<AtkUnitBase*, bool, bool>)Marshal.GetFunctionPointerForDelegate(this.closeFunction);
this.ModifiedVirtualTable->Show = (delegate* unmanaged<AtkUnitBase*, bool, uint, void>)Marshal.GetFunctionPointerForDelegate(this.showFunction);
this.ModifiedVirtualTable->Hide = (delegate* unmanaged<AtkUnitBase*, bool, bool, uint, void>)Marshal.GetFunctionPointerForDelegate(this.hideFunction);
this.ModifiedVirtualTable->OnMove = (delegate* unmanaged<AtkUnitBase*, void>)Marshal.GetFunctionPointerForDelegate(this.onMoveFunction);
this.ModifiedVirtualTable->OnMouseOver = (delegate* unmanaged<AtkUnitBase*, void>)Marshal.GetFunctionPointerForDelegate(this.onMouseOverFunction);
this.ModifiedVirtualTable->OnMouseOut = (delegate* unmanaged<AtkUnitBase*, void>)Marshal.GetFunctionPointerForDelegate(this.onMouseOutFunction);
this.ModifiedVirtualTable->Focus = (delegate* unmanaged<AtkUnitBase*, void>)Marshal.GetFunctionPointerForDelegate(this.focusFunction);
this.ModifiedVirtualTable->OnFocusChange = (delegate* unmanaged<AtkUnitBase*, bool, void>)Marshal.GetFunctionPointerForDelegate(this.onFocusChangeFunction);
}
/// <summary>
/// Gets the original virtual table address for this addon.
/// </summary>
internal AtkUnitBase.AtkUnitBaseVirtualTable* OriginalVirtualTable { get; private set; }
/// <summary>
/// Gets the modified virtual address for this addon.
/// </summary>
internal AtkUnitBase.AtkUnitBaseVirtualTable* ModifiedVirtualTable { get; private set; }
/// <inheritdoc/>
public void Dispose()
{
// Ensure restoration is done atomically.
Interlocked.Exchange(ref *(nint*)&this.atkUnitBase->VirtualTable, (nint)this.OriginalVirtualTable);
IMemorySpace.Free(this.ModifiedVirtualTable, 0x8 * VirtualTableEntryCount);
}
private AtkEventListener* OnAddonDestructor(AtkUnitBase* thisPtr, byte freeFlags)
{
AtkEventListener* result = null;
try
{
this.LogEvent(EnableLogging);
try
{
result = this.OriginalVirtualTable->Dtor(thisPtr, freeFlags);
}
catch (Exception e)
{
Log.Error(e, "Caught exception when calling original Addon Dtor. This may be a bug in the game or another plugin hooking this method.");
}
if ((freeFlags & 1) == 1)
{
IMemorySpace.Free(this.ModifiedVirtualTable, 0x8 * VirtualTableEntryCount);
AddonLifecycle.AllocatedTables.Remove(this);
}
}
catch (Exception e)
{
Log.Error(e, "Caught exception from Dalamud when attempting to process OnAddonDestructor.");
}
return result;
}
private void OnAddonSetup(AtkUnitBase* addon, uint valueCount, AtkValue* values)
{
try
{
this.LogEvent(EnableLogging);
this.setupArgs.Addon = addon;
this.setupArgs.AtkValueCount = valueCount;
this.setupArgs.AtkValues = (nint)values;
this.lifecycleService.InvokeListenersSafely(AddonEvent.PreSetup, this.setupArgs);
valueCount = this.setupArgs.AtkValueCount;
values = (AtkValue*)this.setupArgs.AtkValues;
try
{
this.OriginalVirtualTable->OnSetup(addon, valueCount, values);
}
catch (Exception e)
{
Log.Error(e, "Caught exception when calling original Addon OnSetup. This may be a bug in the game or another plugin hooking this method.");
}
this.lifecycleService.InvokeListenersSafely(AddonEvent.PostSetup, this.setupArgs);
}
catch (Exception e)
{
Log.Error(e, "Caught exception from Dalamud when attempting to process OnAddonSetup.");
}
}
private void OnAddonFinalize(AtkUnitBase* thisPtr)
{
try
{
this.LogEvent(EnableLogging);
this.finalizeArgs.Addon = thisPtr;
this.lifecycleService.InvokeListenersSafely(AddonEvent.PreFinalize, this.finalizeArgs);
try
{
this.OriginalVirtualTable->Finalizer(thisPtr);
}
catch (Exception e)
{
Log.Error(e, "Caught exception when calling original Addon Finalizer. This may be a bug in the game or another plugin hooking this method.");
}
}
catch (Exception e)
{
Log.Error(e, "Caught exception from Dalamud when attempting to process OnAddonFinalize.");
}
}
private void OnAddonDraw(AtkUnitBase* addon)
{
try
{
this.LogEvent(EnableLogging);
this.drawArgs.Addon = addon;
this.lifecycleService.InvokeListenersSafely(AddonEvent.PreDraw, this.drawArgs);
try
{
this.OriginalVirtualTable->Draw(addon);
}
catch (Exception e)
{
Log.Error(e, "Caught exception when calling original Addon Draw. This may be a bug in the game or another plugin hooking this method.");
}
this.lifecycleService.InvokeListenersSafely(AddonEvent.PostDraw, this.drawArgs);
}
catch (Exception e)
{
Log.Error(e, "Caught exception from Dalamud when attempting to process OnAddonDraw.");
}
}
private void OnAddonUpdate(AtkUnitBase* addon, float delta)
{
try
{
this.LogEvent(EnableLogging);
this.updateArgs.Addon = addon;
this.lifecycleService.InvokeListenersSafely(AddonEvent.PreUpdate, this.updateArgs);
// Note: Do not pass or allow manipulation of delta.
// It's realistically not something that should be needed.
// And even if someone does, they are encouraged to hook Update themselves.
try
{
this.OriginalVirtualTable->Update(addon, delta);
}
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(AddonEvent.PostUpdate, this.updateArgs);
}
catch (Exception e)
{
Log.Error(e, "Caught exception from Dalamud when attempting to process OnAddonUpdate.");
}
}
private bool OnAddonRefresh(AtkUnitBase* addon, uint valueCount, AtkValue* values)
{
var result = false;
try
{
this.LogEvent(EnableLogging);
this.refreshArgs.Addon = addon;
this.refreshArgs.AtkValueCount = valueCount;
this.refreshArgs.AtkValues = (nint)values;
this.lifecycleService.InvokeListenersSafely(AddonEvent.PreRefresh, this.refreshArgs);
valueCount = this.refreshArgs.AtkValueCount;
values = (AtkValue*)this.refreshArgs.AtkValues;
try
{
result = this.OriginalVirtualTable->OnRefresh(addon, valueCount, values);
}
catch (Exception e)
{
Log.Error(e, "Caught exception when calling original Addon OnRefresh. This may be a bug in the game or another plugin hooking this method.");
}
this.lifecycleService.InvokeListenersSafely(AddonEvent.PostRefresh, this.refreshArgs);
}
catch (Exception e)
{
Log.Error(e, "Caught exception from Dalamud when attempting to process OnAddonRefresh.");
}
return result;
}
private void OnRequestedUpdate(AtkUnitBase* addon, NumberArrayData** numberArrayData, StringArrayData** stringArrayData)
{
try
{
this.LogEvent(EnableLogging);
this.requestedUpdateArgs.Addon = addon;
this.requestedUpdateArgs.NumberArrayData = (nint)numberArrayData;
this.requestedUpdateArgs.StringArrayData = (nint)stringArrayData;
this.lifecycleService.InvokeListenersSafely(AddonEvent.PreRequestedUpdate, this.requestedUpdateArgs);
numberArrayData = (NumberArrayData**)this.requestedUpdateArgs.NumberArrayData;
stringArrayData = (StringArrayData**)this.requestedUpdateArgs.StringArrayData;
try
{
this.OriginalVirtualTable->OnRequestedUpdate(addon, numberArrayData, stringArrayData);
}
catch (Exception e)
{
Log.Error(e, "Caught exception when calling original Addon OnRequestedUpdate. This may be a bug in the game or another plugin hooking this method.");
}
this.lifecycleService.InvokeListenersSafely(AddonEvent.PostRequestedUpdate, this.requestedUpdateArgs);
}
catch (Exception e)
{
Log.Error(e, "Caught exception from Dalamud when attempting to process OnRequestedUpdate.");
}
}
private void OnAddonReceiveEvent(AtkUnitBase* addon, AtkEventType eventType, int eventParam, AtkEvent* atkEvent, AtkEventData* atkEventData)
{
try
{
this.LogEvent(EnableLogging);
this.receiveEventArgs.Addon = (nint)addon;
this.receiveEventArgs.AtkEventType = (byte)eventType;
this.receiveEventArgs.EventParam = eventParam;
this.receiveEventArgs.AtkEvent = (IntPtr)atkEvent;
this.receiveEventArgs.AtkEventData = (nint)atkEventData;
this.lifecycleService.InvokeListenersSafely(AddonEvent.PreReceiveEvent, this.receiveEventArgs);
eventType = (AtkEventType)this.receiveEventArgs.AtkEventType;
eventParam = this.receiveEventArgs.EventParam;
atkEvent = (AtkEvent*)this.receiveEventArgs.AtkEvent;
atkEventData = (AtkEventData*)this.receiveEventArgs.AtkEventData;
try
{
this.OriginalVirtualTable->ReceiveEvent(addon, eventType, eventParam, atkEvent, atkEventData);
}
catch (Exception e)
{
Log.Error(e, "Caught exception when calling original Addon ReceiveEvent. This may be a bug in the game or another plugin hooking this method.");
}
this.lifecycleService.InvokeListenersSafely(AddonEvent.PostReceiveEvent, this.receiveEventArgs);
}
catch (Exception e)
{
Log.Error(e, "Caught exception from Dalamud when attempting to process OnAddonReceiveEvent.");
}
}
private bool OnAddonOpen(AtkUnitBase* thisPtr, uint depthLayer)
{
var result = false;
try
{
this.LogEvent(EnableLogging);
this.openArgs.Addon = thisPtr;
this.lifecycleService.InvokeListenersSafely(AddonEvent.PreOpen, this.openArgs);
try
{
result = this.OriginalVirtualTable->Open(thisPtr, depthLayer);
}
catch (Exception e)
{
Log.Error(e, "Caught exception when calling original Addon Open. This may be a bug in the game or another plugin hooking this method.");
}
this.lifecycleService.InvokeListenersSafely(AddonEvent.PostOpen, this.openArgs);
}
catch (Exception e)
{
Log.Error(e, "Caught exception from Dalamud when attempting to process OnAddonOpen.");
}
return result;
}
private bool OnAddonClose(AtkUnitBase* thisPtr, bool fireCallback)
{
var result = false;
try
{
this.LogEvent(EnableLogging);
this.closeArgs.Addon = thisPtr;
this.closeArgs.FireCallback = fireCallback;
this.lifecycleService.InvokeListenersSafely(AddonEvent.PreClose, this.closeArgs);
fireCallback = this.closeArgs.FireCallback;
try
{
result = this.OriginalVirtualTable->Close(thisPtr, fireCallback);
}
catch (Exception e)
{
Log.Error(e, "Caught exception when calling original Addon Close. This may be a bug in the game or another plugin hooking this method.");
}
this.lifecycleService.InvokeListenersSafely(AddonEvent.PostClose, this.closeArgs);
}
catch (Exception e)
{
Log.Error(e, "Caught exception from Dalamud when attempting to process OnAddonClose.");
}
return result;
}
private void OnAddonShow(AtkUnitBase* thisPtr, bool silenceOpenSoundEffect, uint unsetShowHideFlags)
{
try
{
this.LogEvent(EnableLogging);
this.showArgs.Addon = thisPtr;
this.showArgs.SilenceOpenSoundEffect = silenceOpenSoundEffect;
this.showArgs.UnsetShowHideFlags = unsetShowHideFlags;
this.lifecycleService.InvokeListenersSafely(AddonEvent.PreShow, this.showArgs);
silenceOpenSoundEffect = this.showArgs.SilenceOpenSoundEffect;
unsetShowHideFlags = this.showArgs.UnsetShowHideFlags;
try
{
this.OriginalVirtualTable->Show(thisPtr, silenceOpenSoundEffect, unsetShowHideFlags);
}
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(AddonEvent.PostShow, this.showArgs);
}
catch (Exception e)
{
Log.Error(e, "Caught exception from Dalamud when attempting to process OnAddonShow.");
}
}
private void OnAddonHide(AtkUnitBase* thisPtr, bool unkBool, bool callHideCallback, uint setShowHideFlags)
{
try
{
this.LogEvent(EnableLogging);
this.hideArgs.Addon = thisPtr;
this.hideArgs.UnknownBool = unkBool;
this.hideArgs.CallHideCallback = callHideCallback;
this.hideArgs.SetShowHideFlags = setShowHideFlags;
this.lifecycleService.InvokeListenersSafely(AddonEvent.PreHide, this.hideArgs);
unkBool = this.hideArgs.UnknownBool;
callHideCallback = this.hideArgs.CallHideCallback;
setShowHideFlags = this.hideArgs.SetShowHideFlags;
try
{
this.OriginalVirtualTable->Hide(thisPtr, unkBool, callHideCallback, setShowHideFlags);
}
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(AddonEvent.PostHide, this.hideArgs);
}
catch (Exception e)
{
Log.Error(e, "Caught exception from Dalamud when attempting to process OnAddonHide.");
}
}
private void OnAddonMove(AtkUnitBase* thisPtr)
{
try
{
this.LogEvent(EnableLogging);
this.onMoveArgs.Addon = thisPtr;
this.lifecycleService.InvokeListenersSafely(AddonEvent.PreMove, this.onMoveArgs);
try
{
this.OriginalVirtualTable->OnMove(thisPtr);
}
catch (Exception e)
{
Log.Error(e, "Caught exception when calling original Addon OnMove. This may be a bug in the game or another plugin hooking this method.");
}
this.lifecycleService.InvokeListenersSafely(AddonEvent.PostMove, this.onMoveArgs);
}
catch (Exception e)
{
Log.Error(e, "Caught exception from Dalamud when attempting to process OnAddonMove.");
}
}
private void OnAddonMouseOver(AtkUnitBase* thisPtr)
{
try
{
this.LogEvent(EnableLogging);
this.onMouseOverArgs.Addon = thisPtr;
this.lifecycleService.InvokeListenersSafely(AddonEvent.PreMouseOver, this.onMouseOverArgs);
try
{
this.OriginalVirtualTable->OnMouseOver(thisPtr);
}
catch (Exception e)
{
Log.Error(e, "Caught exception when calling original Addon OnMouseOver. This may be a bug in the game or another plugin hooking this method.");
}
this.lifecycleService.InvokeListenersSafely(AddonEvent.PostMouseOver, this.onMouseOverArgs);
}
catch (Exception e)
{
Log.Error(e, "Caught exception from Dalamud when attempting to process OnAddonMouseOver.");
}
}
private void OnAddonMouseOut(AtkUnitBase* thisPtr)
{
try
{
this.LogEvent(EnableLogging);
this.onMouseOutArgs.Addon = thisPtr;
this.lifecycleService.InvokeListenersSafely(AddonEvent.PreMouseOut, this.onMouseOutArgs);
try
{
this.OriginalVirtualTable->OnMouseOut(thisPtr);
}
catch (Exception e)
{
Log.Error(e, "Caught exception when calling original Addon OnMouseOut. This may be a bug in the game or another plugin hooking this method.");
}
this.lifecycleService.InvokeListenersSafely(AddonEvent.PostMouseOut, this.onMouseOutArgs);
}
catch (Exception e)
{
Log.Error(e, "Caught exception from Dalamud when attempting to process OnAddonMouseOut.");
}
}
private void OnAddonFocus(AtkUnitBase* thisPtr)
{
try
{
this.LogEvent(EnableLogging);
this.focusArgs.Addon = thisPtr;
this.lifecycleService.InvokeListenersSafely(AddonEvent.PreFocus, this.focusArgs);
try
{
this.OriginalVirtualTable->Focus(thisPtr);
}
catch (Exception e)
{
Log.Error(e, "Caught exception when calling original Addon Focus. This may be a bug in the game or another plugin hooking this method.");
}
this.lifecycleService.InvokeListenersSafely(AddonEvent.PostFocus, this.focusArgs);
}
catch (Exception e)
{
Log.Error(e, "Caught exception from Dalamud when attempting to process OnAddonFocus.");
}
}
private void OnAddonFocusChange(AtkUnitBase* thisPtr, bool isFocused)
{
try
{
this.LogEvent(EnableLogging);
this.focusChangedArgs.Addon = thisPtr;
this.focusChangedArgs.ShouldFocus = isFocused;
this.lifecycleService.InvokeListenersSafely(AddonEvent.PreFocusChanged, this.focusChangedArgs);
isFocused = this.focusChangedArgs.ShouldFocus;
try
{
this.OriginalVirtualTable->OnFocusChange(thisPtr, isFocused);
}
catch (Exception e)
{
Log.Error(e, "Caught exception when calling original Addon OnFocusChanged. This may be a bug in the game or another plugin hooking this method.");
}
this.lifecycleService.InvokeListenersSafely(AddonEvent.PostFocusChanged, this.focusChangedArgs);
}
catch (Exception e)
{
Log.Error(e, "Caught exception from Dalamud when attempting to process OnAddonFocusChange.");
}
}
[Conditional("DEBUG")]
private void LogEvent(bool loggingEnabled, [CallerMemberName] string caller = "")
{
if (loggingEnabled)
{
// Manually disable the really spammy log events, you can comment this out if you need to debug them.
if (caller is "OnAddonUpdate" or "OnAddonDraw" or "OnAddonReceiveEvent" or "OnRequestedUpdate")
return;
Log.Debug($"[{caller}]: {this.atkUnitBase->NameString}");
}
}
}

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,344 @@
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)
{
listener.IsRequestedToClear = true;
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)
{
if (listener.IsRequestedToClear) continue;
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)
{
if (listener.IsRequestedToClear) continue;
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,43 @@
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; }
/// <summary>
/// Gets or sets if the listener is requested to be cleared.
/// </summary>
internal bool IsRequestedToClear { get; set; }
}

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

@ -2,6 +2,8 @@ using System.Collections.Generic;
using System.Linq;
using System.Runtime.InteropServices;
using Dalamud.Plugin.Services;
namespace Dalamud.Game;
/// <summary>
@ -12,7 +14,7 @@ public abstract class BaseAddressResolver
/// <summary>
/// Gets a list of memory addresses that were found, to list in /xldata.
/// </summary>
public static Dictionary<string, List<(string ClassName, IntPtr Address)>> DebugScannedValues { get; } = new();
public static Dictionary<string, List<(string ClassName, IntPtr Address)>> DebugScannedValues { get; } = [];
/// <summary>
/// Gets or sets a value indicating whether the resolver has successfully run <see cref="Setup32Bit(ISigScanner)"/> or <see cref="Setup64Bit(ISigScanner)"/>.

View file

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

View file

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

View file

@ -1,5 +1,4 @@
using System.Linq;
using System.Reflection;
using System.Text.RegularExpressions;
using CheapLoc;
@ -23,7 +22,7 @@ namespace Dalamud.Game;
[ServiceManager.EarlyLoadedService]
internal partial class ChatHandlers : IServiceType
{
private static readonly ModuleLog Log = new("ChatHandlers");
private static readonly ModuleLog Log = ModuleLog.Create<ChatHandlers>();
[ServiceManager.ServiceDependency]
private readonly DalamudConfiguration configuration = Service<DalamudConfiguration>.Get();
@ -77,7 +76,7 @@ internal partial class ChatHandlers : IServiceType
}
// For injections while logged in
if (clientState.LocalPlayer != null && clientState.TerritoryType == 0 && !this.hasSeenLoadingMsg)
if (clientState.IsLoggedIn && clientState.TerritoryType == 0 && !this.hasSeenLoadingMsg)
this.PrintWelcomeMessage();
#if !DEBUG && false
@ -104,7 +103,7 @@ internal partial class ChatHandlers : IServiceType
if (this.configuration.PrintDalamudWelcomeMsg)
{
chatGui.Print(string.Format(Loc.Localize("DalamudWelcome", "Dalamud {0} loaded."), Util.GetScmVersion())
chatGui.Print(string.Format(Loc.Localize("DalamudWelcome", "Dalamud {0} loaded."), Versioning.GetScmVersion())
+ string.Format(Loc.Localize("PluginsWelcome", " {0} plugin(s) loaded."), pluginManager.InstalledPlugins.Count(x => x.IsLoaded)));
}
@ -116,7 +115,7 @@ internal partial class ChatHandlers : IServiceType
}
}
if (string.IsNullOrEmpty(this.configuration.LastVersion) || !Util.AssemblyVersion.StartsWith(this.configuration.LastVersion))
if (string.IsNullOrEmpty(this.configuration.LastVersion) || !Versioning.GetAssemblyVersion().StartsWith(this.configuration.LastVersion))
{
var linkPayload = chatGui.AddChatLinkHandler(
(_, _) => dalamudInterface.OpenPluginInstallerTo(PluginInstallerOpenKind.Changelogs));
@ -124,11 +123,11 @@ internal partial class ChatHandlers : IServiceType
var updateMessage = new SeStringBuilder()
.AddText(Loc.Localize("DalamudUpdated", "Dalamud has been updated successfully!"))
.AddUiForeground(500)
.AddText(" [")
.AddText(" [ ")
.Add(linkPayload)
.AddText(Loc.Localize("DalamudClickToViewChangelogs", " Click here to view the changelog."))
.AddText(Loc.Localize("DalamudClickToViewChangelogs", "Click here to view the changelog."))
.Add(RawPayload.LinkTerminator)
.AddText("]")
.AddText(" ]")
.AddUiForegroundOff();
chatGui.Print(new XivChatEntry
@ -137,7 +136,7 @@ internal partial class ChatHandlers : IServiceType
Type = XivChatType.Notice,
});
this.configuration.LastVersion = Util.AssemblyVersion;
this.configuration.LastVersion = Versioning.GetAssemblyVersion();
this.configuration.QueueSave();
}

View file

@ -63,47 +63,37 @@ public interface IAetheryteEntry
}
/// <summary>
/// Class representing an aetheryte entry available to the game.
/// This struct represents an aetheryte entry available to the game.
/// </summary>
internal sealed class AetheryteEntry : IAetheryteEntry
/// <param name="data">Data read from the Aetheryte List.</param>
internal readonly struct AetheryteEntry(TeleportInfo data) : IAetheryteEntry
{
private readonly TeleportInfo data;
/// <summary>
/// Initializes a new instance of the <see cref="AetheryteEntry"/> class.
/// </summary>
/// <param name="data">Data read from the Aetheryte List.</param>
internal AetheryteEntry(TeleportInfo data)
{
this.data = data;
}
/// <inheritdoc />
public uint AetheryteId => data.AetheryteId;
/// <inheritdoc />
public uint AetheryteId => this.data.AetheryteId;
public uint TerritoryId => data.TerritoryId;
/// <inheritdoc />
public uint TerritoryId => this.data.TerritoryId;
public byte SubIndex => data.SubIndex;
/// <inheritdoc />
public byte SubIndex => this.data.SubIndex;
public byte Ward => data.Ward;
/// <inheritdoc />
public byte Ward => this.data.Ward;
public byte Plot => data.Plot;
/// <inheritdoc />
public byte Plot => this.data.Plot;
public uint GilCost => data.GilCost;
/// <inheritdoc />
public uint GilCost => this.data.GilCost;
public bool IsFavourite => data.IsFavourite;
/// <inheritdoc />
public bool IsFavourite => this.data.IsFavourite;
public bool IsSharedHouse => data.IsSharedHouse;
/// <inheritdoc />
public bool IsSharedHouse => this.data.IsSharedHouse;
/// <inheritdoc />
public bool IsApartment => this.data.IsApartment;
public bool IsApartment => data.IsApartment;
/// <inheritdoc />
public RowRef<Lumina.Excel.Sheets.Aetheryte> AetheryteData => LuminaUtils.CreateRef<Lumina.Excel.Sheets.Aetheryte>(this.AetheryteId);

View file

@ -1,12 +1,14 @@
using System.Collections;
using System.Collections.Generic;
using Dalamud.Game.ClientState.Objects;
using Dalamud.IoC;
using Dalamud.IoC.Internal;
using Dalamud.Plugin.Services;
using Dalamud.Utility;
using FFXIVClientStructs.FFXIV.Client.Game.UI;
using Serilog;
namespace Dalamud.Game.ClientState.Aetherytes;
@ -22,7 +24,7 @@ namespace Dalamud.Game.ClientState.Aetherytes;
internal sealed unsafe partial class AetheryteList : IServiceType, IAetheryteList
{
[ServiceManager.ServiceDependency]
private readonly ClientState clientState = Service<ClientState>.Get();
private readonly ObjectTable objectTable = Service<ObjectTable>.Get();
private readonly Telepo* telepoInstance = Telepo.Instance();
@ -37,7 +39,7 @@ internal sealed unsafe partial class AetheryteList : IServiceType, IAetheryteLis
{
get
{
if (this.clientState.LocalPlayer == null)
if (this.objectTable.LocalPlayer == null)
return 0;
this.Update();
@ -59,7 +61,7 @@ internal sealed unsafe partial class AetheryteList : IServiceType, IAetheryteLis
return null;
}
if (this.clientState.LocalPlayer == null)
if (this.objectTable.LocalPlayer == null)
return null;
return new AetheryteEntry(this.telepoInstance->TeleportList[index]);
@ -69,7 +71,7 @@ internal sealed unsafe partial class AetheryteList : IServiceType, IAetheryteLis
private void Update()
{
// this is very very important as otherwise it crashes
if (this.clientState.LocalPlayer == null)
if (this.objectTable.LocalPlayer == null)
return;
this.telepoInstance->UpdateAetheryteList();
@ -87,10 +89,7 @@ internal sealed partial class AetheryteList
/// <inheritdoc/>
public IEnumerator<IAetheryteEntry> GetEnumerator()
{
for (var i = 0; i < this.Length; i++)
{
yield return this[i];
}
return new Enumerator(this);
}
/// <inheritdoc/>
@ -98,4 +97,34 @@ internal sealed partial class AetheryteList
{
return this.GetEnumerator();
}
private struct Enumerator(AetheryteList aetheryteList) : IEnumerator<IAetheryteEntry>
{
private int index = -1;
public IAetheryteEntry Current { get; private set; }
object IEnumerator.Current => this.Current;
public bool MoveNext()
{
if (++this.index < aetheryteList.Length)
{
this.Current = aetheryteList[this.index];
return true;
}
this.Current = default;
return false;
}
public void Reset()
{
this.index = -1;
}
public void Dispose()
{
}
}
}

View file

@ -2,11 +2,14 @@ using System.Collections;
using System.Collections.Generic;
using System.Runtime.CompilerServices;
using Dalamud.Game.Player;
using Dalamud.IoC;
using Dalamud.IoC.Internal;
using Dalamud.Plugin.Services;
using FFXIVClientStructs.FFXIV.Client.Game.UI;
using CSBuddy = FFXIVClientStructs.FFXIV.Client.Game.UI.Buddy;
using CSBuddyMember = FFXIVClientStructs.FFXIV.Client.Game.UI.Buddy.BuddyMember;
using CSUIState = FFXIVClientStructs.FFXIV.Client.Game.UI.UIState;
namespace Dalamud.Game.ClientState.Buddy;
@ -21,10 +24,10 @@ namespace Dalamud.Game.ClientState.Buddy;
#pragma warning restore SA1015
internal sealed partial class BuddyList : IServiceType, IBuddyList
{
private const uint InvalidObjectID = 0xE0000000;
private const uint InvalidEntityId = 0xE0000000;
[ServiceManager.ServiceDependency]
private readonly ClientState clientState = Service<ClientState>.Get();
private readonly PlayerState playerState = Service<PlayerState>.Get();
[ServiceManager.ServiceConstructor]
private BuddyList()
@ -69,7 +72,7 @@ internal sealed partial class BuddyList : IServiceType, IBuddyList
}
}
private unsafe FFXIVClientStructs.FFXIV.Client.Game.UI.Buddy* BuddyListStruct => &UIState.Instance()->Buddy;
private unsafe CSBuddy* BuddyListStruct => &CSUIState.Instance()->Buddy;
/// <inheritdoc/>
public IBuddyMember? this[int index]
@ -82,37 +85,37 @@ internal sealed partial class BuddyList : IServiceType, IBuddyList
}
/// <inheritdoc/>
public unsafe IntPtr GetCompanionBuddyMemberAddress()
public unsafe nint GetCompanionBuddyMemberAddress()
{
return (IntPtr)this.BuddyListStruct->CompanionInfo.Companion;
return (nint)this.BuddyListStruct->CompanionInfo.Companion;
}
/// <inheritdoc/>
public unsafe IntPtr GetPetBuddyMemberAddress()
public unsafe nint GetPetBuddyMemberAddress()
{
return (IntPtr)this.BuddyListStruct->PetInfo.Pet;
return (nint)this.BuddyListStruct->PetInfo.Pet;
}
/// <inheritdoc/>
public unsafe IntPtr GetBattleBuddyMemberAddress(int index)
public unsafe nint GetBattleBuddyMemberAddress(int index)
{
if (index < 0 || index >= 3)
return IntPtr.Zero;
return 0;
return (IntPtr)Unsafe.AsPointer(ref this.BuddyListStruct->BattleBuddies[index]);
return (nint)Unsafe.AsPointer(ref this.BuddyListStruct->BattleBuddies[index]);
}
/// <inheritdoc/>
public IBuddyMember? CreateBuddyMemberReference(IntPtr address)
public unsafe IBuddyMember? CreateBuddyMemberReference(nint address)
{
if (this.clientState.LocalContentId == 0)
if (address == 0)
return null;
if (address == IntPtr.Zero)
if (this.playerState.ContentId == 0)
return null;
var buddy = new BuddyMember(address);
if (buddy.ObjectId == InvalidObjectID)
var buddy = new BuddyMember((CSBuddyMember*)address);
if (buddy.EntityId == InvalidEntityId)
return null;
return buddy;
@ -130,12 +133,39 @@ internal sealed partial class BuddyList
/// <inheritdoc/>
public IEnumerator<IBuddyMember> GetEnumerator()
{
for (var i = 0; i < this.Length; i++)
{
yield return this[i];
}
return new Enumerator(this);
}
/// <inheritdoc/>
IEnumerator IEnumerable.GetEnumerator() => this.GetEnumerator();
private struct Enumerator(BuddyList buddyList) : IEnumerator<IBuddyMember>
{
private int index = -1;
public IBuddyMember Current { get; private set; }
object IEnumerator.Current => this.Current;
public bool MoveNext()
{
if (++this.index < buddyList.Length)
{
this.Current = buddyList[this.index];
return true;
}
this.Current = default;
return false;
}
public void Reset()
{
this.index = -1;
}
public void Dispose()
{
}
}
}

View file

@ -1,26 +1,36 @@
using System.Diagnostics.CodeAnalysis;
using Dalamud.Data;
using Dalamud.Game.ClientState.Objects;
using Dalamud.Game.ClientState.Objects.Types;
using Lumina.Excel;
using CSBuddyMember = FFXIVClientStructs.FFXIV.Client.Game.UI.Buddy.BuddyMember;
namespace Dalamud.Game.ClientState.Buddy;
/// <summary>
/// Interface representing represents a buddy such as the chocobo companion, summoned pets, squadron groups and trust parties.
/// </summary>
public interface IBuddyMember
public interface IBuddyMember : IEquatable<IBuddyMember>
{
/// <summary>
/// Gets the address of the buddy in memory.
/// </summary>
IntPtr Address { get; }
nint Address { get; }
/// <summary>
/// Gets the object ID of this buddy.
/// </summary>
[Obsolete("Renamed to EntityId")]
uint ObjectId { get; }
/// <summary>
/// Gets the entity ID of this buddy.
/// </summary>
uint EntityId { get; }
/// <summary>
/// Gets the actor associated with this buddy.
/// </summary>
@ -61,39 +71,34 @@ public interface IBuddyMember
}
/// <summary>
/// This class represents a buddy such as the chocobo companion, summoned pets, squadron groups and trust parties.
/// This struct represents a buddy such as the chocobo companion, summoned pets, squadron groups and trust parties.
/// </summary>
internal unsafe class BuddyMember : IBuddyMember
/// <param name="ptr">A pointer to the BuddyMember.</param>
internal readonly unsafe struct BuddyMember(CSBuddyMember* ptr) : IBuddyMember
{
[ServiceManager.ServiceDependency]
private readonly ObjectTable objectTable = Service<ObjectTable>.Get();
/// <summary>
/// Initializes a new instance of the <see cref="BuddyMember"/> class.
/// </summary>
/// <param name="address">Buddy address.</param>
internal BuddyMember(IntPtr address)
{
this.Address = address;
}
/// <inheritdoc />
public nint Address => (nint)ptr;
/// <inheritdoc />
public IntPtr Address { get; }
public uint ObjectId => this.EntityId;
/// <inheritdoc />
public uint ObjectId => this.Struct->EntityId;
public uint EntityId => ptr->EntityId;
/// <inheritdoc />
public IGameObject? GameObject => this.objectTable.SearchById(this.ObjectId);
public IGameObject? GameObject => this.objectTable.SearchById(this.EntityId);
/// <inheritdoc />
public uint CurrentHP => this.Struct->CurrentHealth;
public uint CurrentHP => ptr->CurrentHealth;
/// <inheritdoc />
public uint MaxHP => this.Struct->MaxHealth;
public uint MaxHP => ptr->MaxHealth;
/// <inheritdoc />
public uint DataID => this.Struct->DataId;
public uint DataID => ptr->DataId;
/// <inheritdoc />
public RowRef<Lumina.Excel.Sheets.Mount> MountData => LuminaUtils.CreateRef<Lumina.Excel.Sheets.Mount>(this.DataID);
@ -104,5 +109,25 @@ internal unsafe class BuddyMember : IBuddyMember
/// <inheritdoc />
public RowRef<Lumina.Excel.Sheets.DawnGrowMember> TrustData => LuminaUtils.CreateRef<Lumina.Excel.Sheets.DawnGrowMember>(this.DataID);
private FFXIVClientStructs.FFXIV.Client.Game.UI.Buddy.BuddyMember* Struct => (FFXIVClientStructs.FFXIV.Client.Game.UI.Buddy.BuddyMember*)this.Address;
public static bool operator ==(BuddyMember x, BuddyMember y) => x.Equals(y);
public static bool operator !=(BuddyMember x, BuddyMember y) => !(x == y);
/// <inheritdoc/>
public bool Equals(IBuddyMember? other)
{
return this.EntityId == other.EntityId;
}
/// <inheritdoc/>
public override bool Equals([NotNullWhen(true)] object? obj)
{
return obj is BuddyMember fate && this.Equals(fate);
}
/// <inheritdoc/>
public override int GetHashCode()
{
return this.EntityId.GetHashCode();
}
}

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