Compare commits

...

218 commits

Author SHA1 Message Date
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
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
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
Kaz Wolfe
2cef75bbbe
feat: remove socket cleanup tasks 2025-11-26 11:56:30 -08: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
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
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
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
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
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
github-actions[bot]
c79fa96505 Merge remote-tracking branch 'origin/master' into api14-rollup 2025-11-17 17:43:53 +00: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
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
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
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
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
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
Soreepeong
544f8b28bf Support make clickthrough 2025-08-16 16:42:30 +09: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
Soreepeong
c19ea6ace3 Add ITextureProvider.CreateTextureFromSeString 2025-08-05 11:48:02 +09:00
200 changed files with 5623 additions and 3609 deletions

View file

@ -8,7 +8,8 @@ import re
import sys import sys
import json import json
import argparse import argparse
from typing import List, Tuple, Optional import os
from typing import List, Tuple, Optional, Dict, Any
def run_git_command(args: List[str]) -> str: def run_git_command(args: List[str]) -> str:
@ -55,46 +56,132 @@ def get_submodule_commit(submodule_path: str, tag: str) -> Optional[str]:
return None return None
def get_commits_between_tags(tag1: str, tag2: str) -> List[Tuple[str, str]]: def get_repo_info() -> Tuple[str, str]:
"""Get commits between two tags. Returns list of (message, author) tuples.""" """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_output = run_git_command([
"log", "log",
f"{tag2}..{tag1}", f"{tag2}..{tag1}",
"--format=%s|%an|%h" "--format=%H"
]) ])
commits = [] commits = [sha.strip() for sha in log_output.split("\n") if sha.strip()]
for line in log_output.split("\n"):
if "|" in line:
message, author, sha = line.split("|", 2)
commits.append((message.strip(), author.strip(), sha.strip()))
return commits return commits
def filter_commits(commits: List[Tuple[str, str]], ignore_patterns: List[str]) -> List[Tuple[str, str]]: def get_pr_for_commit(commit_sha: str, owner: str, repo: str, token: str) -> Optional[Dict[str, Any]]:
"""Filter out commits matching any of the ignore patterns.""" """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] compiled_patterns = [re.compile(pattern) for pattern in ignore_patterns]
filtered = [] filtered = []
for message, author, sha in commits: for pr in prs:
if not any(pattern.search(message) for pattern in compiled_patterns): if not any(pattern.search(pr["title"]) for pattern in compiled_patterns):
filtered.append((message, author, sha)) filtered.append(pr)
return filtered return filtered
def generate_changelog(version: str, prev_version: str, commits: List[Tuple[str, str]], def generate_changelog(version: str, prev_version: str, prs: List[Dict[str, Any]],
cs_commit_new: Optional[str], cs_commit_old: Optional[str]) -> str: cs_commit_new: Optional[str], cs_commit_old: Optional[str],
owner: str, repo: str) -> str:
"""Generate markdown changelog.""" """Generate markdown changelog."""
# Calculate statistics # Calculate statistics
commit_count = len(commits) pr_count = len(prs)
unique_authors = len(set(author for _, author, _ in commits)) unique_authors = len(set(pr["author"] for pr in prs))
changelog = f"# Dalamud Release v{version}\n\n" 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"We just released Dalamud v{version}, which should be available to users within a few minutes. "
changelog += f"This release includes **{commit_count} commit{'s' if commit_count != 1 else ''} from {unique_authors} contributor{'s' if unique_authors != 1 else ''}**.\n" 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/goatcorp/Dalamud/compare/{prev_version}...{version}>) to see all Dalamud changes.\n\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: 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"It ships with an updated **FFXIVClientStructs [`{cs_commit_new[:7]}`](<https://github.com/aers/FFXIVClientStructs/commit/{cs_commit_new}>)**.\n"
@ -104,8 +191,8 @@ def generate_changelog(version: str, prev_version: str, commits: List[Tuple[str,
changelog += "## Dalamud Changes\n\n" changelog += "## Dalamud Changes\n\n"
for message, author, sha in commits: for pr in prs:
changelog += f"* {message} (by **{author}** as [`{sha}`](<https://github.com/goatcorp/Dalamud/commit/{sha}>))\n" changelog += f"* {pr['title']} ([#**{pr['number']}**](<{pr['url']}>) by **{pr['author']}**)\n"
return changelog return changelog
@ -158,11 +245,16 @@ def main():
required=True, required=True,
help="Discord webhook URL" 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( parser.add_argument(
"--ignore", "--ignore",
action="append", action="append",
default=[], default=[],
help="Regex patterns to ignore commits (can be specified multiple times)" help="Regex patterns to ignore PRs (can be specified multiple times)"
) )
parser.add_argument( parser.add_argument(
"--submodule-path", "--submodule-path",
@ -172,6 +264,10 @@ def main():
args = parser.parse_args() args = parser.parse_args()
# Get repository info
owner, repo = get_repo_info()
print(f"Repository: {owner}/{repo}")
# Get the last two tags # Get the last two tags
latest_tag, previous_tag = get_last_two_tags() latest_tag, previous_tag = get_last_two_tags()
print(f"Generating changelog between {previous_tag} and {latest_tag}") print(f"Generating changelog between {previous_tag} and {latest_tag}")
@ -185,17 +281,18 @@ def main():
if cs_commit_old: if cs_commit_old:
print(f"FFXIVClientStructs commit (old): {cs_commit_old[:7]}") print(f"FFXIVClientStructs commit (old): {cs_commit_old[:7]}")
# Get commits between tags # Get PRs between tags
commits = get_commits_between_tags(latest_tag, previous_tag) prs = get_prs_between_tags(latest_tag, previous_tag, owner, repo, args.github_token)
print(f"Found {len(commits)} commits") prs.reverse()
print(f"Found {len(prs)} PRs")
# Filter commits # Filter PRs
filtered_commits = filter_commits(commits, args.ignore) filtered_prs = filter_prs(prs, args.ignore)
print(f"After filtering: {len(filtered_commits)} commits") print(f"After filtering: {len(filtered_prs)} PRs")
# Generate changelog # Generate changelog
changelog = generate_changelog(latest_tag, previous_tag, filtered_commits, changelog = generate_changelog(latest_tag, previous_tag, filtered_prs,
cs_commit_new, cs_commit_old) cs_commit_new, cs_commit_old, owner, repo)
print("\n" + "="*50) print("\n" + "="*50)
print("Generated Changelog:") print("Generated Changelog:")

View file

@ -6,6 +6,8 @@ on:
tags: tags:
- '*' - '*'
permissions: read-all
jobs: jobs:
generate-changelog: generate-changelog:
runs-on: ubuntu-latest runs-on: ubuntu-latest
@ -28,14 +30,14 @@ jobs:
pip install requests pip install requests
- name: Generate and post changelog - name: Generate and post changelog
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
GIT_TERMINAL_PROMPT: 0
run: | run: |
python .github/generate_changelog.py \ python .github/generate_changelog.py \
--webhook-url "${{ secrets.DISCORD_CHANGELOG_WEBHOOK_URL }}" \ --webhook-url "${{ secrets.DISCORD_CHANGELOG_WEBHOOK_URL }}" \
--ignore "^Merge" \ --ignore "Update ClientStructs" \
--ignore "^build:" \ --ignore "^build:"
--ignore "^docs:"
env:
GIT_TERMINAL_PROMPT: 0
- name: Upload changelog as artifact - name: Upload changelog as artifact
if: always() if: always()

View file

@ -1,9 +1,10 @@
name: Build Dalamud name: Build Dalamud
on: [push, pull_request, workflow_dispatch] on: [push, pull_request, workflow_dispatch]
# Globally blocking because of git pushes in deploy step
concurrency: concurrency:
group: build_dalamud_${{ github.ref_name }} group: build_dalamud_${{ github.repository_owner }}
cancel-in-progress: true cancel-in-progress: false
jobs: jobs:
build: build:
@ -23,7 +24,7 @@ jobs:
uses: microsoft/setup-msbuild@v1.0.2 uses: microsoft/setup-msbuild@v1.0.2
- uses: actions/setup-dotnet@v3 - uses: actions/setup-dotnet@v3
with: with:
dotnet-version: '9.0.200' dotnet-version: '10.0.100'
- name: Define VERSION - name: Define VERSION
run: | run: |
$env:COMMIT = $env:GITHUB_SHA.Substring(0, 7) $env:COMMIT = $env:GITHUB_SHA.Substring(0, 7)

View file

@ -1,19 +1,57 @@
{ {
"$schema": "http://json-schema.org/draft-04/schema#", "$schema": "http://json-schema.org/draft-04/schema#",
"title": "Build Schema",
"$ref": "#/definitions/build",
"definitions": { "definitions": {
"build": { "Host": {
"type": "object", "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": { "properties": {
"Configuration": {
"type": "string",
"description": "Configuration to build - Default is 'Debug' (local) or 'Release' (server)",
"enum": [
"Debug",
"Release"
]
},
"Continue": { "Continue": {
"type": "boolean", "type": "boolean",
"description": "Indicates to continue a previously failed build attempt" "description": "Indicates to continue a previously failed build attempt"
@ -23,29 +61,8 @@
"description": "Shows the help text for this build assembly" "description": "Shows the help text for this build assembly"
}, },
"Host": { "Host": {
"type": "string",
"description": "Host for execution. Default is 'automatic'", "description": "Host for execution. Default is 'automatic'",
"enum": [ "$ref": "#/definitions/Host"
"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"
}, },
"NoLogo": { "NoLogo": {
"type": "boolean", "type": "boolean",
@ -74,65 +91,46 @@
"type": "array", "type": "array",
"description": "List of targets to be skipped. Empty list skips all dependencies", "description": "List of targets to be skipped. Empty list skips all dependencies",
"items": { "items": {
"type": "string", "$ref": "#/definitions/ExecutableTarget"
"enum": [
"CI",
"Clean",
"Compile",
"CompileCImGui",
"CompileCImGuizmo",
"CompileCImPlot",
"CompileDalamud",
"CompileDalamudBoot",
"CompileDalamudCrashHandler",
"CompileImGuiNatives",
"CompileInjector",
"CompileInjectorBoot",
"Restore",
"SetCILogging",
"Test"
]
} }
}, },
"Solution": {
"type": "string",
"description": "Path to a solution file that is automatically loaded"
},
"Target": { "Target": {
"type": "array", "type": "array",
"description": "List of targets to be invoked. Default is '{default_target}'", "description": "List of targets to be invoked. Default is '{default_target}'",
"items": { "items": {
"type": "string", "$ref": "#/definitions/ExecutableTarget"
"enum": [
"CI",
"Clean",
"Compile",
"CompileCImGui",
"CompileCImGuizmo",
"CompileCImPlot",
"CompileDalamud",
"CompileDalamudBoot",
"CompileDalamudCrashHandler",
"CompileImGuiNatives",
"CompileInjector",
"CompileInjectorBoot",
"Restore",
"SetCILogging",
"Test"
]
} }
}, },
"Verbosity": { "Verbosity": {
"type": "string",
"description": "Logging verbosity during build execution. Default is 'Normal'", "description": "Logging verbosity during build execution. Default is 'Normal'",
"enum": [ "$ref": "#/definitions/Verbosity"
"Minimal",
"Normal",
"Quiet",
"Verbose"
]
} }
} }
} }
} },
"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

@ -108,6 +108,11 @@ void from_json(const nlohmann::json& json, DalamudStartInfo& config) {
config.LogName = json.value("LogName", config.LogName); config.LogName = json.value("LogName", config.LogName);
config.PluginDirectory = json.value("PluginDirectory", config.PluginDirectory); config.PluginDirectory = json.value("PluginDirectory", config.PluginDirectory);
config.AssetDirectory = json.value("AssetDirectory", config.AssetDirectory); 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.Language = json.value("Language", config.Language);
config.Platform = json.value("Platform", config.Platform); config.Platform = json.value("Platform", config.Platform);
config.GameVersion = json.value("GameVersion", config.GameVersion); config.GameVersion = json.value("GameVersion", config.GameVersion);

View file

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

View file

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

View file

@ -331,6 +331,51 @@ HRESULT WINAPI InitializeImpl(LPVOID lpParam, HANDLE hMainThreadContinue) {
logging::I("VEH was disabled manually"); 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 ==================================== // // ============================== Dalamud ==================================== //
if (static_cast<int>(g_startInfo.BootWaitMessageBox) & static_cast<int>(DalamudStartInfo::WaitMessageboxFlags::BeforeDalamudEntrypoint)) if (static_cast<int>(g_startInfo.BootWaitMessageBox) & static_cast<int>(DalamudStartInfo::WaitMessageboxFlags::BeforeDalamudEntrypoint))

View file

@ -31,6 +31,8 @@ HANDLE g_crashhandler_process = nullptr;
HANDLE g_crashhandler_event = nullptr; HANDLE g_crashhandler_event = nullptr;
HANDLE g_crashhandler_pipe_write = nullptr; HANDLE g_crashhandler_pipe_write = nullptr;
wchar_t g_external_event_info[16384] = L"";
std::recursive_mutex g_exception_handler_mutex; std::recursive_mutex g_exception_handler_mutex;
std::chrono::time_point<std::chrono::system_clock> g_time_start; std::chrono::time_point<std::chrono::system_clock> g_time_start;
@ -122,6 +124,7 @@ static DalamudExpected<void> append_injector_launch_args(std::vector<std::wstrin
args.emplace_back(L"--logname=\"" + unicode::convert<std::wstring>(g_startInfo.LogName) + L"\""); 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-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-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-client-language={}", static_cast<int>(g_startInfo.Language)));
args.emplace_back(std::format(L"--dalamud-delay-initialize={}", g_startInfo.DelayInitializeMs)); args.emplace_back(std::format(L"--dalamud-delay-initialize={}", g_startInfo.DelayInitializeMs));
// NoLoadPlugins/NoLoadThirdPartyPlugins: supplied from DalamudCrashHandler // NoLoadPlugins/NoLoadThirdPartyPlugins: supplied from DalamudCrashHandler
@ -190,7 +193,11 @@ LONG exception_handler(EXCEPTION_POINTERS* ex)
DuplicateHandle(GetCurrentProcess(), g_crashhandler_event, g_crashhandler_process, &exinfo.hEventHandle, 0, TRUE, DUPLICATE_SAME_ACCESS); DuplicateHandle(GetCurrentProcess(), g_crashhandler_event, g_crashhandler_process, &exinfo.hEventHandle, 0, TRUE, DUPLICATE_SAME_ACCESS);
std::wstring stackTrace; 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)"; stackTrace = L"(no CLR stack trace available)";
} }
@ -251,6 +258,12 @@ LONG WINAPI structured_exception_handler(EXCEPTION_POINTERS* ex)
LONG WINAPI vectored_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) if (ex->ExceptionRecord->ExceptionCode == 0x12345678)
{ {
// pass // pass
@ -434,3 +447,16 @@ bool veh::remove_handler()
} }
return false; 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 add_handler(bool doFullDump, const std::string& workingDirectory);
bool remove_handler(); bool remove_handler();
void raise_external_event(const std::wstring& info);
} }

View file

@ -34,6 +34,12 @@ public record DalamudStartInfo
/// </summary> /// </summary>
public string? ConfigurationPath { get; set; } 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> /// <summary>
/// Gets or sets the path of the log files. /// Gets or sets the path of the log files.
/// </summary> /// </summary>

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>stdcpp23</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>
<PropertyGroup Label="Output"> <PropertyGroup Label="Output">
<OutputType>Library</OutputType> <OutputType>Exe</OutputType>
<OutputPath>..\bin\$(Configuration)\</OutputPath> <OutputPath>..\bin\$(Configuration)\</OutputPath>
<AppendTargetFrameworkToOutputPath>false</AppendTargetFrameworkToOutputPath> <AppendTargetFrameworkToOutputPath>false</AppendTargetFrameworkToOutputPath>
<AppendRuntimeIdentifierToOutputPath>false</AppendRuntimeIdentifierToOutputPath> <AppendRuntimeIdentifierToOutputPath>false</AppendRuntimeIdentifierToOutputPath>
<GenerateRuntimeConfigurationFiles>true</GenerateRuntimeConfigurationFiles> <GenerateRuntimeConfigurationFiles>true</GenerateRuntimeConfigurationFiles>
<ProduceReferenceAssembly>false</ProduceReferenceAssembly> <ProduceReferenceAssembly>false</ProduceReferenceAssembly>
<ApplicationIcon>dalamud.ico</ApplicationIcon>
</PropertyGroup> </PropertyGroup>
<PropertyGroup Label="Documentation"> <PropertyGroup Label="Documentation">

View file

@ -25,34 +25,20 @@ namespace Dalamud.Injector
/// <summary> /// <summary>
/// Entrypoint to the program. /// Entrypoint to the program.
/// </summary> /// </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> /// <summary>
/// Start the Dalamud injector. /// Start the Dalamud injector.
/// </summary> /// </summary>
/// <param name="argc">Count of arguments.</param> /// <param name="argsArray">Command line arguments.</param>
/// <param name="argvPtr">byte** string arguments.</param>
/// <returns>Return value (HRESULT).</returns> /// <returns>Return value (HRESULT).</returns>
public static int Main(int argc, IntPtr argvPtr) public static int Main(string[] argsArray)
{ {
try try
{ {
List<string> args = new(argc); // API14 TODO: Refactor
var args = argsArray.ToList();
unsafe args.Insert(0, Assembly.GetExecutingAssembly().Location);
{
var argv = (IntPtr*)argvPtr;
for (var i = 0; i < argc; i++)
args.Add(Marshal.PtrToStringUni(argv[i]));
}
Init(args); Init(args);
args.Remove("-v"); // Remove "verbose" flag args.Remove("-v"); // Remove "verbose" flag
@ -305,6 +291,7 @@ namespace Dalamud.Injector
var configurationPath = startInfo.ConfigurationPath; var configurationPath = startInfo.ConfigurationPath;
var pluginDirectory = startInfo.PluginDirectory; var pluginDirectory = startInfo.PluginDirectory;
var assetDirectory = startInfo.AssetDirectory; var assetDirectory = startInfo.AssetDirectory;
var tempDirectory = startInfo.TempDirectory;
var delayInitializeMs = startInfo.DelayInitializeMs; var delayInitializeMs = startInfo.DelayInitializeMs;
var logName = startInfo.LogName; var logName = startInfo.LogName;
var logPath = startInfo.LogPath; var logPath = startInfo.LogPath;
@ -335,6 +322,10 @@ namespace Dalamud.Injector
{ {
assetDirectory = args[i][key.Length..]; 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=")) else if (args[i].StartsWith(key = "--dalamud-delay-initialize="))
{ {
delayInitializeMs = int.Parse(args[i][key.Length..]); delayInitializeMs = int.Parse(args[i][key.Length..]);
@ -447,6 +438,7 @@ namespace Dalamud.Injector
startInfo.ConfigurationPath = configurationPath; startInfo.ConfigurationPath = configurationPath;
startInfo.PluginDirectory = pluginDirectory; startInfo.PluginDirectory = pluginDirectory;
startInfo.AssetDirectory = assetDirectory; startInfo.AssetDirectory = assetDirectory;
startInfo.TempDirectory = tempDirectory;
startInfo.Language = clientLanguage; startInfo.Language = clientLanguage;
startInfo.Platform = platform; startInfo.Platform = platform;
startInfo.DelayInitializeMs = delayInitializeMs; startInfo.DelayInitializeMs = delayInitializeMs;

View file

Before

Width:  |  Height:  |  Size: 52 KiB

After

Width:  |  Height:  |  Size: 52 KiB

Before After
Before After

View file

@ -0,0 +1,108 @@
using System;
using System.Linq;
using Dalamud.Networking.Rpc.Model;
using Xunit;
namespace Dalamud.Test.Rpc
{
public class DalamudUriTests
{
[Theory]
[InlineData("https://www.google.com/", false)]
[InlineData("dalamud://PluginInstaller/Dalamud.FindAnything", true)]
public void ValidatesScheme(string uri, bool valid)
{
Action act = () => { _ = DalamudUri.FromUri(uri); };
var ex = Record.Exception(act);
if (valid)
{
Assert.Null(ex);
}
else
{
Assert.NotNull(ex);
Assert.IsType<ArgumentOutOfRangeException>(ex);
}
}
[Theory]
[InlineData("dalamud://PluginInstaller/Dalamud.FindAnything", "plugininstaller")]
[InlineData("dalamud://Plugin/Dalamud.FindAnything/OpenWindow", "plugin")]
[InlineData("dalamud://Test", "test")]
public void ExtractsNamespace(string uri, string expectedNamespace)
{
var dalamudUri = DalamudUri.FromUri(uri);
Assert.Equal(expectedNamespace, dalamudUri.Namespace);
}
[Theory]
[InlineData("dalamud://foo/bar/baz/qux/?cow=moo", "/bar/baz/qux/")]
[InlineData("dalamud://foo/bar/baz/qux?cow=moo", "/bar/baz/qux")]
[InlineData("dalamud://foo/bar/baz", "/bar/baz")]
[InlineData("dalamud://foo/bar", "/bar")]
[InlineData("dalamud://foo/bar/", "/bar/")]
[InlineData("dalamud://foo/", "/")]
public void ExtractsPath(string uri, string expectedPath)
{
var dalamudUri = DalamudUri.FromUri(uri);
Assert.Equal(expectedPath, dalamudUri.Path);
}
[Theory]
[InlineData("dalamud://foo/bar/baz/qux/?cow=moo#frag", "/bar/baz/qux/?cow=moo#frag")]
[InlineData("dalamud://foo/bar/baz/qux/?cow=moo", "/bar/baz/qux/?cow=moo")]
[InlineData("dalamud://foo/bar/baz/qux?cow=moo", "/bar/baz/qux?cow=moo")]
[InlineData("dalamud://foo/bar/baz", "/bar/baz")]
[InlineData("dalamud://foo/bar?cow=moo", "/bar?cow=moo")]
[InlineData("dalamud://foo/bar", "/bar")]
[InlineData("dalamud://foo/bar/?cow=moo", "/bar/?cow=moo")]
[InlineData("dalamud://foo/bar/", "/bar/")]
[InlineData("dalamud://foo/?cow=moo#chicken", "/?cow=moo#chicken")]
[InlineData("dalamud://foo/?cow=moo", "/?cow=moo")]
[InlineData("dalamud://foo/", "/")]
public void ExtractsData(string uri, string expectedData)
{
var dalamudUri = DalamudUri.FromUri(uri);
Assert.Equal(expectedData, dalamudUri.Data);
}
[Theory]
[InlineData("dalamud://foo/bar", 0)]
[InlineData("dalamud://foo/bar?cow=moo", 1)]
[InlineData("dalamud://foo/bar?cow=moo&wolf=awoo", 2)]
[InlineData("dalamud://foo/bar?cow=moo&wolf=awoo&cat", 3)]
public void ExtractsQueryParams(string uri, int queryCount)
{
var dalamudUri = DalamudUri.FromUri(uri);
Assert.Equal(queryCount, dalamudUri.QueryParams.Count);
}
[Theory]
[InlineData("dalamud://foo/bar/baz/qux/meh/?foo=bar", 5, true)]
[InlineData("dalamud://foo/bar/baz/qux/meh/", 5, true)]
[InlineData("dalamud://foo/bar/baz/qux/meh", 5)]
[InlineData("dalamud://foo/bar/baz/qux", 4)]
[InlineData("dalamud://foo/bar/baz", 3)]
[InlineData("dalamud://foo/bar/", 2)]
[InlineData("dalamud://foo/bar", 2)]
public void ExtractsSegments(string uri, int segmentCount, bool finalSegmentEndsWithSlash = false)
{
var dalamudUri = DalamudUri.FromUri(uri);
var segments = dalamudUri.Segments;
// First segment must always be `/`
Assert.Equal("/", segments[0]);
Assert.Equal(segmentCount, segments.Length);
if (finalSegmentEndsWithSlash)
{
Assert.EndsWith("/", segments.Last());
}
}
}
}

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 # Visual Studio Version 17
VisualStudioVersion = 17.1.32319.34 VisualStudioVersion = 17.1.32319.34
MinimumVisualStudioVersion = 10.0.40219.1 MinimumVisualStudioVersion = 10.0.40219.1
@ -7,8 +7,6 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution
.editorconfig = .editorconfig .editorconfig = .editorconfig
.gitignore = .gitignore .gitignore = .gitignore
tools\BannedSymbols.txt = tools\BannedSymbols.txt 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 tools\dalamud.ruleset = tools\dalamud.ruleset
Directory.Build.props = Directory.Build.props Directory.Build.props = Directory.Build.props
Directory.Packages.props = Directory.Packages.props Directory.Packages.props = Directory.Packages.props
@ -27,8 +25,6 @@ Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "Dalamud.Boot", "Dalamud.Boo
EndProject EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Dalamud.Injector", "Dalamud.Injector\Dalamud.Injector.csproj", "{5B832F73-5F54-4ADC-870F-D0095EF72C9A}" Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Dalamud.Injector", "Dalamud.Injector\Dalamud.Injector.csproj", "{5B832F73-5F54-4ADC-870F-D0095EF72C9A}"
EndProject 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}" Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Dalamud.Test", "Dalamud.Test\Dalamud.Test.csproj", "{C8004563-1806-4329-844F-0EF6274291FC}"
EndProject EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Dependencies", "Dependencies", "{E15BDA6D-E881-4482-94BA-BE5527E917FF}" Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Dependencies", "Dependencies", "{E15BDA6D-E881-4482-94BA-BE5527E917FF}"
@ -49,8 +45,6 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "InteropGenerator", "lib\FFX
EndProject EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "InteropGenerator.Runtime", "lib\FFXIVClientStructs\InteropGenerator.Runtime\InteropGenerator.Runtime.csproj", "{A6AA1C3F-9470-4922-9D3F-D4549657AB22}" Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "InteropGenerator.Runtime", "lib\FFXIVClientStructs\InteropGenerator.Runtime\InteropGenerator.Runtime.csproj", "{A6AA1C3F-9470-4922-9D3F-D4549657AB22}"
EndProject 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}" Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Utilities", "Utilities", "{8F079208-C227-4D96-9427-2BEBE0003944}"
EndProject EndProject
Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "cimgui", "external\cimgui\cimgui.vcxproj", "{8430077C-F736-4246-A052-8EA1CECE844E}" Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "cimgui", "external\cimgui\cimgui.vcxproj", "{8430077C-F736-4246-A052-8EA1CECE844E}"
@ -103,10 +97,6 @@ Global
{5B832F73-5F54-4ADC-870F-D0095EF72C9A}.Debug|Any CPU.Build.0 = Debug|x64 {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.ActiveCfg = Release|x64
{5B832F73-5F54-4ADC-870F-D0095EF72C9A}.Release|Any CPU.Build.0 = 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.ActiveCfg = Debug|x64
{C8004563-1806-4329-844F-0EF6274291FC}.Debug|Any CPU.Build.0 = 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 {C8004563-1806-4329-844F-0EF6274291FC}.Release|Any CPU.ActiveCfg = Release|x64
@ -188,8 +178,6 @@ Global
HideSolutionNode = FALSE HideSolutionNode = FALSE
EndGlobalSection EndGlobalSection
GlobalSection(NestedProjects) = preSolution 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} {4AFDB34A-7467-4D41-B067-53BC4101D9D0} = {8F079208-C227-4D96-9427-2BEBE0003944}
{C9B87BD7-AF49-41C3-91F1-D550ADEB7833} = {8BBACF2D-7AB8-4610-A115-0E363D35C291} {C9B87BD7-AF49-41C3-91F1-D550ADEB7833} = {8BBACF2D-7AB8-4610-A115-0E363D35C291}
{E0D51896-604F-4B40-8CFE-51941607B3A1} = {8BBACF2D-7AB8-4610-A115-0E363D35C291} {E0D51896-604F-4B40-8CFE-51941607B3A1} = {8BBACF2D-7AB8-4610-A115-0E363D35C291}

View file

@ -108,11 +108,6 @@ internal sealed class DalamudConfiguration : IInternalDisposableService
/// </summary> /// </summary>
public bool DoPluginTest { get; set; } = false; 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> /// <summary>
/// Gets or sets a list of custom repos. /// Gets or sets a list of custom repos.
/// </summary> /// </summary>
@ -278,11 +273,6 @@ internal sealed class DalamudConfiguration : IInternalDisposableService
/// </summary> /// </summary>
public bool IsResumeGameAfterPluginLoad { get; set; } = false; 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> /// <summary>
/// Gets or sets a value indicating whether any plugin should be loaded when the game is started. /// Gets or sets a value indicating whether any plugin should be loaded when the game is started.
/// It is reset immediately when read. /// It is reset immediately when read.
@ -497,6 +487,14 @@ internal sealed class DalamudConfiguration : IInternalDisposableService
/// </summary> /// </summary>
public Vector2 NotificationAnchorPosition { get; set; } = new(1f, 1f); 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> /// <summary>
/// Load a configuration from the provided path. /// Load a configuration from the provided path.
/// </summary> /// </summary>

View file

@ -11,7 +11,7 @@ namespace Dalamud.Configuration;
/// <summary> /// <summary>
/// Configuration to store settings for a dalamud plugin. /// Configuration to store settings for a dalamud plugin.
/// </summary> /// </summary>
[Api13ToDo("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.")] [Api14ToDo("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 public sealed class PluginConfigurations
{ {
private readonly DirectoryInfo configDirectory; private readonly DirectoryInfo configDirectory;

View file

@ -6,7 +6,7 @@
<PropertyGroup Label="Feature"> <PropertyGroup Label="Feature">
<Description>XIV Launcher addon framework</Description> <Description>XIV Launcher addon framework</Description>
<DalamudVersion>13.0.0.11</DalamudVersion> <DalamudVersion>14.0.0.0</DalamudVersion>
<AssemblyVersion>$(DalamudVersion)</AssemblyVersion> <AssemblyVersion>$(DalamudVersion)</AssemblyVersion>
<Version>$(DalamudVersion)</Version> <Version>$(DalamudVersion)</Version>
<FileVersion>$(DalamudVersion)</FileVersion> <FileVersion>$(DalamudVersion)</FileVersion>
@ -73,23 +73,19 @@
<PrivateAssets>all</PrivateAssets> <PrivateAssets>all</PrivateAssets>
</PackageReference> </PackageReference>
<PackageReference Include="MinSharp" /> <PackageReference Include="MinSharp" />
<PackageReference Include="SharpDX.Direct3D11" />
<PackageReference Include="SharpDX.Mathematics" />
<PackageReference Include="Newtonsoft.Json" /> <PackageReference Include="Newtonsoft.Json" />
<PackageReference Include="Serilog" /> <PackageReference Include="Serilog" />
<PackageReference Include="Serilog.Sinks.Async" /> <PackageReference Include="Serilog.Sinks.Async" />
<PackageReference Include="Serilog.Sinks.Console" /> <PackageReference Include="Serilog.Sinks.Console" />
<PackageReference Include="Serilog.Sinks.File" /> <PackageReference Include="Serilog.Sinks.File" />
<PackageReference Include="sqlite-net-pcl" /> <PackageReference Include="sqlite-net-pcl" />
<PackageReference Include="StreamJsonRpc" />
<PackageReference Include="StyleCop.Analyzers"> <PackageReference Include="StyleCop.Analyzers">
<PrivateAssets>all</PrivateAssets> <PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets> <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference> </PackageReference>
<PackageReference Include="System.Collections.Immutable" />
<PackageReference Include="System.Drawing.Common" />
<PackageReference Include="System.Reactive" /> <PackageReference Include="System.Reactive" />
<PackageReference Include="System.Reflection.MetadataLoadContext" /> <PackageReference Include="System.Reflection.MetadataLoadContext" />
<PackageReference Include="System.Resources.Extensions" />
<PackageReference Include="TerraFX.Interop.Windows" /> <PackageReference Include="TerraFX.Interop.Windows" />
</ItemGroup> </ItemGroup>
@ -122,6 +118,8 @@
<Content Include="licenses.txt"> <Content Include="licenses.txt">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory> <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content> </Content>
<None Remove="Interface\ImGuiBackend\Renderers\gaussian.hlsl" />
<None Remove="Interface\ImGuiBackend\Renderers\fullscreen-quad.hlsl.bytes" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
@ -226,9 +224,4 @@
<!-- writes the attribute to the customAssemblyInfo file --> <!-- writes the attribute to the customAssemblyInfo file -->
<WriteCodeFragment Language="C#" OutputFile="$(CustomAssemblyInfoFile)" AssemblyAttributes="@(AssemblyAttributes)" /> <WriteCodeFragment Language="C#" OutputFile="$(CustomAssemblyInfoFile)" AssemblyAttributes="@(AssemblyAttributes)" />
</Target> </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> </Project>

View file

@ -151,7 +151,7 @@ public enum DalamudAsset
/// <see cref="DalamudAssetPurpose.Font"/>: FontAwesome Free Solid. /// <see cref="DalamudAssetPurpose.Font"/>: FontAwesome Free Solid.
/// </summary> /// </summary>
[DalamudAsset(DalamudAssetPurpose.Font)] [DalamudAsset(DalamudAssetPurpose.Font)]
[DalamudAssetPath("UIRes", "FontAwesomeFreeSolid.otf")] [DalamudAssetPath("UIRes", "FontAwesome710FreeSolid.otf")]
FontAwesomeFreeSolid = 2003, FontAwesomeFreeSolid = 2003,
/// <summary> /// <summary>

View file

@ -53,12 +53,25 @@ internal sealed class DataManager : IInternalDisposableService, IDataManager
DefaultExcelLanguage = this.Language.ToLumina(), DefaultExcelLanguage = this.Language.ToLumina(),
}; };
this.GameData = new( try
Path.Combine(Path.GetDirectoryName(Environment.ProcessPath)!, "sqpack"),
luminaOptions)
{ {
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); Log.Information("Lumina is ready: {0}", this.GameData.DataPath);
@ -69,8 +82,13 @@ internal sealed class DataManager : IInternalDisposableService, IDataManager
var tsInfo = var tsInfo =
JsonConvert.DeserializeObject<LauncherTroubleshootingInfo>( JsonConvert.DeserializeObject<LauncherTroubleshootingInfo>(
dalamud.StartInfo.TroubleshootingPackData); 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) if (this.HasModifiedGameDataFiles)
Log.Verbose("Game data integrity check failed!\n{TsData}", dalamud.StartInfo.TroubleshootingPackData); Log.Verbose("Game data integrity check failed!\n{TsData}", dalamud.StartInfo.TroubleshootingPackData);

View file

@ -192,8 +192,8 @@ public sealed class EntryPoint
var dalamud = new Dalamud(info, fs, configuration, mainThreadContinueEvent); var dalamud = new Dalamud(info, fs, configuration, mainThreadContinueEvent);
Log.Information("This is Dalamud - Core: {GitHash}, CS: {CsGitHash} [{CsVersion}]", Log.Information("This is Dalamud - Core: {GitHash}, CS: {CsGitHash} [{CsVersion}]",
Util.GetScmVersion(), Versioning.GetScmVersion(),
Util.GetGitHashClientStructs(), Versioning.GetGitHashClientStructs(),
FFXIVClientStructs.ThisAssembly.Git.Commits); FFXIVClientStructs.ThisAssembly.Git.Commits);
dalamud.WaitForUnload(); dalamud.WaitForUnload();
@ -263,7 +263,7 @@ public sealed class EntryPoint
var symbolPath = Path.Combine(info.AssetDirectory, "UIRes", "pdb"); var symbolPath = Path.Combine(info.AssetDirectory, "UIRes", "pdb");
var searchPath = $".;{symbolPath}"; 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 // Remove any existing Symbol Handler and Init a new one with our search path added
Windows.Win32.PInvoke.SymCleanup(currentProcess); Windows.Win32.PInvoke.SymCleanup(currentProcess);
@ -292,7 +292,6 @@ public sealed class EntryPoint
} }
var pluginInfo = string.Empty; var pluginInfo = string.Empty;
var supportText = ", please visit us on Discord for more help";
try try
{ {
var pm = Service<PluginManager>.GetNullable(); var pm = Service<PluginManager>.GetNullable();
@ -300,9 +299,6 @@ public sealed class EntryPoint
if (plugin != null) if (plugin != null)
{ {
pluginInfo = $"Plugin that caused this:\n{plugin.Name}\n\nClick \"Yes\" and remove it.\n\n"; pluginInfo = $"Plugin that caused this:\n{plugin.Name}\n\nClick \"Yes\" and remove it.\n\n";
if (plugin.IsThirdParty)
supportText = string.Empty;
} }
} }
catch catch
@ -310,31 +306,18 @@ public sealed class EntryPoint
// ignored // 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(); Log.CloseAndFlush();
Environment.Exit(-1);
ErrorHandling.CrashWithContext($"{ex}\n\n{info}\n\n{pluginInfo}");
break; break;
default: default:
Log.Fatal("Unhandled SEH object on AppDomain: {Object}", args.ExceptionObject); Log.Fatal("Unhandled SEH object on AppDomain: {Object}", args.ExceptionObject);
Log.CloseAndFlush(); Log.CloseAndFlush();
Environment.Exit(-1);
break; break;
} }
Environment.Exit(-1);
} }
private static void OnUnhandledExceptionStallDebug(object sender, UnhandledExceptionEventArgs args) private static void OnUnhandledExceptionStallDebug(object sender, UnhandledExceptionEventArgs args)

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

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

@ -5,19 +5,24 @@ namespace Dalamud.Game.Addon.Lifecycle.AddonArgTypes;
/// <summary> /// <summary>
/// Base class for AddonLifecycle AddonArgTypes. /// Base class for AddonLifecycle AddonArgTypes.
/// </summary> /// </summary>
public abstract unsafe class AddonArgs public class AddonArgs
{ {
/// <summary> /// <summary>
/// Constant string representing the name of an addon that is invalid. /// Constant string representing the name of an addon that is invalid.
/// </summary> /// </summary>
public const string InvalidAddon = "NullAddon"; public const string InvalidAddon = "NullAddon";
private string? addonName; /// <summary>
/// Initializes a new instance of the <see cref="AddonArgs"/> class.
/// </summary>
internal AddonArgs()
{
}
/// <summary> /// <summary>
/// Gets the name of the addon this args referrers to. /// Gets the name of the addon this args referrers to.
/// </summary> /// </summary>
public string AddonName => this.GetAddonName(); public string AddonName { get; private set; } = InvalidAddon;
/// <summary> /// <summary>
/// Gets the pointer to the addons AtkUnitBase. /// Gets the pointer to the addons AtkUnitBase.
@ -25,55 +30,17 @@ public abstract unsafe class AddonArgs
public AtkUnitBasePtr Addon public AtkUnitBasePtr Addon
{ {
get; get;
internal set; internal set
{
field = value;
if (!this.Addon.IsNull && !string.IsNullOrEmpty(value.Name))
this.AddonName = value.Name;
}
} }
/// <summary> /// <summary>
/// Gets the type of these args. /// Gets the type of these args.
/// </summary> /// </summary>
public abstract AddonArgsType Type { get; } public virtual AddonArgsType Type => AddonArgsType.Generic;
/// <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;
}
} }

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,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> /// <summary>
/// Addon argument data for ReceiveEvent events. /// Addon argument data for ReceiveEvent events.
/// </summary> /// </summary>
public class AddonReceiveEventArgs : AddonArgs, ICloneable public class AddonReceiveEventArgs : AddonArgs
{ {
/// <summary> /// <summary>
/// Initializes a new instance of the <see cref="AddonReceiveEventArgs"/> class. /// Initializes a new instance of the <see cref="AddonReceiveEventArgs"/> class.
/// </summary> /// </summary>
[Obsolete("Not intended for public construction.", false)] internal AddonReceiveEventArgs()
public AddonReceiveEventArgs()
{ {
} }
@ -32,23 +31,7 @@ public class AddonReceiveEventArgs : AddonArgs, ICloneable
public nint AtkEvent { get; set; } public nint AtkEvent { get; set; }
/// <summary> /// <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> /// </summary>
public nint Data { get; set; } public nint AtkEventData { 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;
}
} }

View file

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

View file

@ -3,13 +3,12 @@ namespace Dalamud.Game.Addon.Lifecycle.AddonArgTypes;
/// <summary> /// <summary>
/// Addon argument data for OnRequestedUpdate events. /// Addon argument data for OnRequestedUpdate events.
/// </summary> /// </summary>
public class AddonRequestedUpdateArgs : AddonArgs, ICloneable public class AddonRequestedUpdateArgs : AddonArgs
{ {
/// <summary> /// <summary>
/// Initializes a new instance of the <see cref="AddonRequestedUpdateArgs"/> class. /// Initializes a new instance of the <see cref="AddonRequestedUpdateArgs"/> class.
/// </summary> /// </summary>
[Obsolete("Not intended for public construction.", false)] internal AddonRequestedUpdateArgs()
public AddonRequestedUpdateArgs()
{ {
} }
@ -25,18 +24,4 @@ public class AddonRequestedUpdateArgs : AddonArgs, ICloneable
/// Gets or sets the StringArrayData** for this event. /// Gets or sets the StringArrayData** for this event.
/// </summary> /// </summary>
public nint StringArrayData { get; set; } 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.FFXIV.Component.GUI;
using FFXIVClientStructs.Interop;
namespace Dalamud.Game.Addon.Lifecycle.AddonArgTypes; namespace Dalamud.Game.Addon.Lifecycle.AddonArgTypes;
/// <summary> /// <summary>
/// Addon argument data for Setup events. /// Addon argument data for Setup events.
/// </summary> /// </summary>
public class AddonSetupArgs : AddonArgs, ICloneable public class AddonSetupArgs : AddonArgs
{ {
/// <summary> /// <summary>
/// Initializes a new instance of the <see cref="AddonSetupArgs"/> class. /// Initializes a new instance of the <see cref="AddonSetupArgs"/> class.
/// </summary> /// </summary>
[Obsolete("Not intended for public construction.", false)] internal AddonSetupArgs()
public AddonSetupArgs()
{ {
} }
@ -31,19 +36,30 @@ public class AddonSetupArgs : AddonArgs, ICloneable
/// <summary> /// <summary>
/// Gets the AtkValues in the form of a span. /// Gets the AtkValues in the form of a span.
/// </summary> /// </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); public unsafe Span<AtkValue> AtkValueSpan => new(this.AtkValues.ToPointer(), (int)this.AtkValueCount);
/// <inheritdoc cref="ICloneable.Clone"/> /// <summary>
public AddonSetupArgs Clone() => (AddonSetupArgs)this.MemberwiseClone(); /// Gets an enumerable collection of <see cref="AtkValuePtr"/> of the event's AtkValues.
/// </summary>
/// <inheritdoc cref="Clone"/> /// <returns>
object ICloneable.Clone() => this.Clone(); /// An <see cref="IEnumerable{T}"/> of <see cref="AtkValuePtr"/> corresponding to the event's AtkValues.
/// </returns>
/// <inheritdoc cref="AddonArgs.Clear"/> public IEnumerable<AtkValuePtr> AtkValueEnumerable
internal override void Clear()
{ {
base.Clear(); get
this.AtkValueCount = default; {
this.AtkValues = default; for (var i = 0; i < this.AtkValueCount; i++)
{
AtkValuePtr ptr;
unsafe
{
ptr = new AtkValuePtr((nint)this.AtkValueSpan.GetPointer(i));
}
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,26 +5,16 @@
/// </summary> /// </summary>
public enum AddonArgsType public enum AddonArgsType
{ {
/// <summary>
/// Generic arg type that contains no meaningful data.
/// </summary>
Generic,
/// <summary> /// <summary>
/// Contains argument data for Setup. /// Contains argument data for Setup.
/// </summary> /// </summary>
Setup, 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> /// <summary>
/// Contains argument data for RequestedUpdate. /// Contains argument data for RequestedUpdate.
/// </summary> /// </summary>
@ -39,4 +29,19 @@ public enum AddonArgsType
/// Contains argument data for ReceiveEvent. /// Contains argument data for ReceiveEvent.
/// </summary> /// </summary>
ReceiveEvent, 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,
} }

View file

@ -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 /// 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. /// is fired every frame that an addon is loaded, regardless of visibility.
/// </summary> /// </summary>
/// <seealso cref="AddonUpdateArgs"/>
PreUpdate, PreUpdate,
/// <summary> /// <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 /// 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. /// <see cref="PreUpdate"/>, this event is only fired if an addon is visible or otherwise drawing to screen.
/// </summary> /// </summary>
/// <seealso cref="AddonDrawArgs"/>
PreDraw, PreDraw,
/// <summary> /// <summary>
@ -62,7 +60,6 @@ public enum AddonEvent
/// <br /> /// <br />
/// As this is part of the destruction process for an addon, this event does not have an associated Post event. /// As this is part of the destruction process for an addon, this event does not have an associated Post event.
/// </remarks> /// </remarks>
/// <seealso cref="AddonFinalizeArgs"/>
PreFinalize, PreFinalize,
/// <summary> /// <summary>
@ -118,4 +115,92 @@ public enum AddonEvent
/// See <see cref="PreReceiveEvent"/> for more information. /// See <see cref="PreReceiveEvent"/> for more information.
/// </summary> /// </summary>
PostReceiveEvent, 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,
} }

View file

@ -1,16 +1,14 @@
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Diagnostics;
using System.Runtime.CompilerServices; using System.Runtime.CompilerServices;
using Dalamud.Game.Addon.Lifecycle.AddonArgTypes; using Dalamud.Game.Addon.Lifecycle.AddonArgTypes;
using Dalamud.Hooking; using Dalamud.Hooking;
using Dalamud.Hooking.Internal;
using Dalamud.IoC; using Dalamud.IoC;
using Dalamud.IoC.Internal; using Dalamud.IoC.Internal;
using Dalamud.Logging.Internal; using Dalamud.Logging.Internal;
using Dalamud.Plugin.Services; using Dalamud.Plugin.Services;
using FFXIVClientStructs.FFXIV.Client.UI;
using FFXIVClientStructs.FFXIV.Component.GUI; using FFXIVClientStructs.FFXIV.Component.GUI;
namespace Dalamud.Game.Addon.Lifecycle; namespace Dalamud.Game.Addon.Lifecycle;
@ -21,75 +19,36 @@ namespace Dalamud.Game.Addon.Lifecycle;
[ServiceManager.EarlyLoadedService] [ServiceManager.EarlyLoadedService]
internal unsafe class AddonLifecycle : IInternalDisposableService internal unsafe class AddonLifecycle : IInternalDisposableService
{ {
/// <summary>
/// Gets a list of all allocated addon virtual tables.
/// </summary>
public static readonly List<AddonVirtualTable> AllocatedTables = [];
private static readonly ModuleLog Log = new("AddonLifecycle"); private static readonly ModuleLog Log = new("AddonLifecycle");
[ServiceManager.ServiceDependency] private Hook<AtkUnitBase.Delegates.Initialize>? onInitializeAddonHook;
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;
[ServiceManager.ServiceConstructor] [ServiceManager.ServiceConstructor]
private AddonLifecycle(TargetSigScanner sigScanner) private AddonLifecycle()
{ {
this.address = new AddonLifecycleAddressResolver(); this.onInitializeAddonHook = Hook<AtkUnitBase.Delegates.Initialize>.FromAddress((nint)AtkUnitBase.StaticVirtualTablePointer->Initialize, this.OnAddonInitialize);
this.address.Setup(sigScanner); this.onInitializeAddonHook.Enable();
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();
} }
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> /// <summary>
/// Gets a list of all AddonLifecycle Event Listeners. /// Gets a list of all AddonLifecycle Event Listeners.
/// </summary> /// </summary> <br/>
internal List<AddonLifecycleEventListener> EventListeners { get; } = new(); /// Mapping is: EventType -> AddonName -> ListenerList
internal Dictionary<AddonEvent, Dictionary<string, HashSet<AddonLifecycleEventListener>>> EventListeners { get; } = [];
/// <inheritdoc/> /// <inheritdoc/>
void IInternalDisposableService.DisposeService() void IInternalDisposableService.DisposeService()
{ {
this.onAddonSetupHook.Dispose(); this.onInitializeAddonHook?.Dispose();
this.onAddonFinalizeHook.Dispose(); this.onInitializeAddonHook = null;
this.onAddonDrawHook.Dispose();
this.onAddonUpdateHook.Dispose();
this.onAddonRefreshHook.Dispose();
this.onAddonRequestedUpdateHook.Dispose();
foreach (var receiveEventListener in this.ReceiveEventListeners) AllocatedTables.ForEach(entry => entry.Dispose());
{ AllocatedTables.Clear();
receiveEventListener.Dispose();
}
} }
/// <summary> /// <summary>
@ -98,20 +57,20 @@ internal unsafe class AddonLifecycle : IInternalDisposableService
/// <param name="listener">The listener to register.</param> /// <param name="listener">The listener to register.</param>
internal void RegisterListener(AddonLifecycleEventListener listener) internal void RegisterListener(AddonLifecycleEventListener listener)
{ {
this.framework.RunOnTick(() => if (!this.EventListeners.ContainsKey(listener.EventType))
{ {
this.EventListeners.Add(listener); if (!this.EventListeners.TryAdd(listener.EventType, []))
return;
}
// If we want receive event messages have an already active addon, enable the receive event hook. // Note: string.Empty is a valid addon name, as that will trigger on any addon for this event type
// If the addon isn't active yet, we'll grab the hook when it sets up. if (!this.EventListeners[listener.EventType].ContainsKey(listener.AddonName))
if (listener is { EventType: AddonEvent.PreReceiveEvent or AddonEvent.PostReceiveEvent }) {
{ if (!this.EventListeners[listener.EventType].TryAdd(listener.AddonName, []))
if (this.ReceiveEventListeners.FirstOrDefault(listeners => listeners.AddonNames.Contains(listener.AddonName)) is { } receiveEventListener) return;
{ }
receiveEventListener.TryEnable();
} this.EventListeners[listener.EventType][listener.AddonName].Add(listener);
}
});
} }
/// <summary> /// <summary>
@ -120,27 +79,13 @@ internal unsafe class AddonLifecycle : IInternalDisposableService
/// <param name="listener">The listener to unregister.</param> /// <param name="listener">The listener to unregister.</param>
internal void UnregisterListener(AddonLifecycleEventListener listener) internal void UnregisterListener(AddonLifecycleEventListener listener)
{ {
// Set removed state to true immediately, then lazily remove it from the EventListeners list on next Framework Update. if (this.EventListeners.TryGetValue(listener.EventType, out var addonListeners))
listener.Removed = true;
this.framework.RunOnTick(() =>
{ {
this.EventListeners.Remove(listener); if (addonListeners.TryGetValue(listener.AddonName, out var addonListener))
// 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 addonListener.Remove(listener);
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();
}
}
} }
}); }
} }
/// <summary> /// <summary>
@ -151,226 +96,63 @@ internal unsafe class AddonLifecycle : IInternalDisposableService
/// <param name="blame">What to blame on errors.</param> /// <param name="blame">What to blame on errors.</param>
internal void InvokeListenersSafely(AddonEvent eventType, AddonArgs args, [CallerMemberName] string blame = "") internal void InvokeListenersSafely(AddonEvent eventType, AddonArgs args, [CallerMemberName] string blame = "")
{ {
// Do not use linq; this is a high-traffic function, and more heap allocations avoided, the better. // Early return if we don't have any listeners of this type
foreach (var listener in this.EventListeners) 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) foreach (var listener in globalListeners)
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
{ {
listener.FunctionDelegate.Invoke(eventType, args); try
}
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))
{ {
existingListener.AddonNames.Add(addonName); listener.FunctionDelegate.Invoke(eventType, args);
}
catch (Exception e)
{
Log.Error(e, $"Exception in {blame} during {eventType} invoke, for global addon event listener.");
} }
} }
}
// Else, we have an addon that we don't have the ReceiveEvent for yet, make it. // Handle listeners that are listening for this addon and event type specifically
else if (addonListeners.TryGetValue(args.AddonName, out var addonListener))
{
foreach (var listener in addonListener)
{ {
this.ReceiveEventListeners.Add(new AddonLifecycleReceiveEventListener(this, addonName, receiveEventAddress)); try
}
// 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)
{ {
receiveEventListener.TryEnable(); listener.FunctionDelegate.Invoke(eventType, args);
}
catch (Exception e)
{
Log.Error(e, $"Exception in {blame} during {eventType} invoke, for specific addon {args.AddonName}.");
} }
} }
} }
} }
private void UnregisterReceiveEventHook(string addonName) private void OnAddonInitialize(AtkUnitBase* addon)
{ {
// Remove this addons ReceiveEvent Registration try
if (this.ReceiveEventListeners.FirstOrDefault(listener => listener.AddonNames.Contains(addonName)) is { } eventListener)
{ {
eventListener.AddonNames.Remove(addonName); this.LogInitialize(addon->NameString);
// If there are no more listeners let's remove and dispose. // AddonVirtualTable class handles creating the virtual table, and overriding each of the tracked virtual functions
if (eventListener.AddonNames.Count is 0) AllocatedTables.Add(new AddonVirtualTable(addon, this));
{
this.ReceiveEventListeners.Remove(eventListener);
eventListener.Dispose();
}
} }
catch (Exception e)
{
Log.Error(e, "Exception in AddonLifecycle during OnAddonInitialize.");
}
this.onInitializeAddonHook!.Original(addon);
} }
private void OnAddonSetup(AtkUnitBase* addon, uint valueCount, AtkValue* values) [Conditional("DEBUG")]
private void LogInitialize(string addonName)
{ {
try Log.Debug($"Initializing {addonName}");
{
this.RegisterReceiveEventHook(addon);
}
catch (Exception e)
{
Log.Error(e, "Exception in OnAddonSetup ReceiveEvent Registration.");
}
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);
}
private void OnAddonFinalize(AtkUnitManager* unitManager, AtkUnitBase** atkUnitBase)
{
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);
} }
} }
@ -387,7 +169,7 @@ internal class AddonLifecyclePluginScoped : IInternalDisposableService, IAddonLi
[ServiceManager.ServiceDependency] [ServiceManager.ServiceDependency]
private readonly AddonLifecycle addonLifecycleService = Service<AddonLifecycle>.Get(); private readonly AddonLifecycle addonLifecycleService = Service<AddonLifecycle>.Get();
private readonly List<AddonLifecycleEventListener> eventListeners = new(); private readonly List<AddonLifecycleEventListener> eventListeners = [];
/// <inheritdoc/> /// <inheritdoc/>
void IInternalDisposableService.DisposeService() void IInternalDisposableService.DisposeService()

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

@ -26,11 +26,6 @@ internal class AddonLifecycleEventListener
/// </summary> /// </summary>
public string AddonName { get; init; } 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> /// <summary>
/// Gets the event type this listener is looking for. /// Gets the event type this listener is looking for.
/// </summary> /// </summary>

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,638 @@
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 AtkUnitBase* atkUnitBase;
private readonly AtkUnitBase.AtkUnitBaseVirtualTable* originalVirtualTable;
private readonly AtkUnitBase.AtkUnitBaseVirtualTable* modifiedVirtualTable;
// 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;
/// <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;
// 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);
}
/// <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.");
}
}
[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

@ -2,6 +2,8 @@ using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Runtime.InteropServices; using System.Runtime.InteropServices;
using Dalamud.Plugin.Services;
namespace Dalamud.Game; namespace Dalamud.Game;
/// <summary> /// <summary>

View file

@ -104,7 +104,7 @@ internal partial class ChatHandlers : IServiceType
if (this.configuration.PrintDalamudWelcomeMsg) 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))); + string.Format(Loc.Localize("PluginsWelcome", " {0} plugin(s) loaded."), pluginManager.InstalledPlugins.Count(x => x.IsLoaded)));
} }
@ -116,7 +116,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( var linkPayload = chatGui.AddChatLinkHandler(
(_, _) => dalamudInterface.OpenPluginInstallerTo(PluginInstallerOpenKind.Changelogs)); (_, _) => dalamudInterface.OpenPluginInstallerTo(PluginInstallerOpenKind.Changelogs));
@ -137,7 +137,7 @@ internal partial class ChatHandlers : IServiceType
Type = XivChatType.Notice, Type = XivChatType.Notice,
}); });
this.configuration.LastVersion = Util.AssemblyVersion; this.configuration.LastVersion = Versioning.GetAssemblyVersion();
this.configuration.QueueSave(); this.configuration.QueueSave();
} }

View file

@ -63,47 +63,37 @@ public interface IAetheryteEntry
} }
/// <summary> /// <summary>
/// Class representing an aetheryte entry available to the game. /// This struct represents an aetheryte entry available to the game.
/// </summary> /// </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; /// <inheritdoc />
public uint AetheryteId => data.AetheryteId;
/// <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 /> /// <inheritdoc />
public uint AetheryteId => this.data.AetheryteId; public uint TerritoryId => data.TerritoryId;
/// <inheritdoc /> /// <inheritdoc />
public uint TerritoryId => this.data.TerritoryId; public byte SubIndex => data.SubIndex;
/// <inheritdoc /> /// <inheritdoc />
public byte SubIndex => this.data.SubIndex; public byte Ward => data.Ward;
/// <inheritdoc /> /// <inheritdoc />
public byte Ward => this.data.Ward; public byte Plot => data.Plot;
/// <inheritdoc /> /// <inheritdoc />
public byte Plot => this.data.Plot; public uint GilCost => data.GilCost;
/// <inheritdoc /> /// <inheritdoc />
public uint GilCost => this.data.GilCost; public bool IsFavourite => data.IsFavourite;
/// <inheritdoc /> /// <inheritdoc />
public bool IsFavourite => this.data.IsFavourite; public bool IsSharedHouse => data.IsSharedHouse;
/// <inheritdoc /> /// <inheritdoc />
public bool IsSharedHouse => this.data.IsSharedHouse; public bool IsApartment => data.IsApartment;
/// <inheritdoc />
public bool IsApartment => this.data.IsApartment;
/// <inheritdoc /> /// <inheritdoc />
public RowRef<Lumina.Excel.Sheets.Aetheryte> AetheryteData => LuminaUtils.CreateRef<Lumina.Excel.Sheets.Aetheryte>(this.AetheryteId); public RowRef<Lumina.Excel.Sheets.Aetheryte> AetheryteData => LuminaUtils.CreateRef<Lumina.Excel.Sheets.Aetheryte>(this.AetheryteId);

View file

@ -88,10 +88,7 @@ internal sealed partial class AetheryteList
/// <inheritdoc/> /// <inheritdoc/>
public IEnumerator<IAetheryteEntry> GetEnumerator() public IEnumerator<IAetheryteEntry> GetEnumerator()
{ {
for (var i = 0; i < this.Length; i++) return new Enumerator(this);
{
yield return this[i];
}
} }
/// <inheritdoc/> /// <inheritdoc/>
@ -99,4 +96,34 @@ internal sealed partial class AetheryteList
{ {
return this.GetEnumerator(); 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

@ -8,6 +8,7 @@ using Dalamud.IoC.Internal;
using Dalamud.Plugin.Services; using Dalamud.Plugin.Services;
using CSBuddy = FFXIVClientStructs.FFXIV.Client.Game.UI.Buddy; 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; using CSUIState = FFXIVClientStructs.FFXIV.Client.Game.UI.UIState;
namespace Dalamud.Game.ClientState.Buddy; namespace Dalamud.Game.ClientState.Buddy;
@ -23,7 +24,7 @@ namespace Dalamud.Game.ClientState.Buddy;
#pragma warning restore SA1015 #pragma warning restore SA1015
internal sealed partial class BuddyList : IServiceType, IBuddyList internal sealed partial class BuddyList : IServiceType, IBuddyList
{ {
private const uint InvalidObjectID = 0xE0000000; private const uint InvalidEntityId = 0xE0000000;
[ServiceManager.ServiceDependency] [ServiceManager.ServiceDependency]
private readonly PlayerState playerState = Service<PlayerState>.Get(); private readonly PlayerState playerState = Service<PlayerState>.Get();
@ -84,37 +85,37 @@ internal sealed partial class BuddyList : IServiceType, IBuddyList
} }
/// <inheritdoc/> /// <inheritdoc/>
public unsafe IntPtr GetCompanionBuddyMemberAddress() public unsafe nint GetCompanionBuddyMemberAddress()
{ {
return (IntPtr)this.BuddyListStruct->CompanionInfo.Companion; return (nint)this.BuddyListStruct->CompanionInfo.Companion;
} }
/// <inheritdoc/> /// <inheritdoc/>
public unsafe IntPtr GetPetBuddyMemberAddress() public unsafe nint GetPetBuddyMemberAddress()
{ {
return (IntPtr)this.BuddyListStruct->PetInfo.Pet; return (nint)this.BuddyListStruct->PetInfo.Pet;
} }
/// <inheritdoc/> /// <inheritdoc/>
public unsafe IntPtr GetBattleBuddyMemberAddress(int index) public unsafe nint GetBattleBuddyMemberAddress(int index)
{ {
if (index < 0 || index >= 3) 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/> /// <inheritdoc/>
public IBuddyMember? CreateBuddyMemberReference(IntPtr address) public unsafe IBuddyMember? CreateBuddyMemberReference(nint address)
{ {
if (address == IntPtr.Zero) if (address == 0)
return null; return null;
if (!this.playerState.IsLoaded) if (this.playerState.ContentId == 0)
return null; return null;
var buddy = new BuddyMember(address); var buddy = new BuddyMember((CSBuddyMember*)address);
if (buddy.ObjectId == InvalidObjectID) if (buddy.EntityId == InvalidEntityId)
return null; return null;
return buddy; return buddy;
@ -132,12 +133,39 @@ internal sealed partial class BuddyList
/// <inheritdoc/> /// <inheritdoc/>
public IEnumerator<IBuddyMember> GetEnumerator() public IEnumerator<IBuddyMember> GetEnumerator()
{ {
for (var i = 0; i < this.Length; i++) return new Enumerator(this);
{
yield return this[i];
}
} }
/// <inheritdoc/> /// <inheritdoc/>
IEnumerator IEnumerable.GetEnumerator() => this.GetEnumerator(); 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,20 +1,24 @@
using System.Diagnostics.CodeAnalysis;
using Dalamud.Data; using Dalamud.Data;
using Dalamud.Game.ClientState.Objects; using Dalamud.Game.ClientState.Objects;
using Dalamud.Game.ClientState.Objects.Types; using Dalamud.Game.ClientState.Objects.Types;
using Lumina.Excel; using Lumina.Excel;
using CSBuddyMember = FFXIVClientStructs.FFXIV.Client.Game.UI.Buddy.BuddyMember;
namespace Dalamud.Game.ClientState.Buddy; namespace Dalamud.Game.ClientState.Buddy;
/// <summary> /// <summary>
/// Interface representing represents a buddy such as the chocobo companion, summoned pets, squadron groups and trust parties. /// Interface representing represents a buddy such as the chocobo companion, summoned pets, squadron groups and trust parties.
/// </summary> /// </summary>
public interface IBuddyMember public interface IBuddyMember : IEquatable<IBuddyMember>
{ {
/// <summary> /// <summary>
/// Gets the address of the buddy in memory. /// Gets the address of the buddy in memory.
/// </summary> /// </summary>
IntPtr Address { get; } nint Address { get; }
/// <summary> /// <summary>
/// Gets the object ID of this buddy. /// Gets the object ID of this buddy.
@ -67,42 +71,34 @@ public interface IBuddyMember
} }
/// <summary> /// <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> /// </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] [ServiceManager.ServiceDependency]
private readonly ObjectTable objectTable = Service<ObjectTable>.Get(); private readonly ObjectTable objectTable = Service<ObjectTable>.Get();
/// <summary> /// <inheritdoc />
/// Initializes a new instance of the <see cref="BuddyMember"/> class. public nint Address => (nint)ptr;
/// </summary>
/// <param name="address">Buddy address.</param>
internal BuddyMember(IntPtr address)
{
this.Address = address;
}
/// <inheritdoc /> /// <inheritdoc />
public IntPtr Address { get; } public uint ObjectId => this.EntityId;
/// <inheritdoc /> /// <inheritdoc />
public uint ObjectId => this.Struct->EntityId; public uint EntityId => ptr->EntityId;
/// <inheritdoc /> /// <inheritdoc />
public uint EntityId => this.Struct->EntityId; public IGameObject? GameObject => this.objectTable.SearchById(this.EntityId);
/// <inheritdoc /> /// <inheritdoc />
public IGameObject? GameObject => this.objectTable.SearchById(this.ObjectId); public uint CurrentHP => ptr->CurrentHealth;
/// <inheritdoc /> /// <inheritdoc />
public uint CurrentHP => this.Struct->CurrentHealth; public uint MaxHP => ptr->MaxHealth;
/// <inheritdoc /> /// <inheritdoc />
public uint MaxHP => this.Struct->MaxHealth; public uint DataID => ptr->DataId;
/// <inheritdoc />
public uint DataID => this.Struct->DataId;
/// <inheritdoc /> /// <inheritdoc />
public RowRef<Lumina.Excel.Sheets.Mount> MountData => LuminaUtils.CreateRef<Lumina.Excel.Sheets.Mount>(this.DataID); public RowRef<Lumina.Excel.Sheets.Mount> MountData => LuminaUtils.CreateRef<Lumina.Excel.Sheets.Mount>(this.DataID);
@ -113,5 +109,25 @@ internal unsafe class BuddyMember : IBuddyMember
/// <inheritdoc /> /// <inheritdoc />
public RowRef<Lumina.Excel.Sheets.DawnGrowMember> TrustData => LuminaUtils.CreateRef<Lumina.Excel.Sheets.DawnGrowMember>(this.DataID); 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();
}
} }

View file

@ -1,3 +1,5 @@
using Dalamud.Plugin.Services;
namespace Dalamud.Game.ClientState; namespace Dalamud.Game.ClientState;
/// <summary> /// <summary>

View file

@ -18,7 +18,7 @@ internal sealed class Condition : IInternalDisposableService, ICondition
/// <summary> /// <summary>
/// Gets the current max number of conditions. You can get this just by looking at the condition sheet and how many rows it has. /// Gets the current max number of conditions. You can get this just by looking at the condition sheet and how many rows it has.
/// </summary> /// </summary>
internal const int MaxConditionEntries = 104; internal const int MaxConditionEntries = 112;
[ServiceManager.ServiceDependency] [ServiceManager.ServiceDependency]
private readonly Framework framework = Service<Framework>.Get(); private readonly Framework framework = Service<Framework>.Get();

View file

@ -520,4 +520,17 @@ public enum ConditionFlag
PilotingMech = 102, PilotingMech = 102,
// Unknown103 = 103, // Unknown103 = 103,
/// <summary>
/// Unable to execute command while editing a strategy board.
/// </summary>
EditingStrategyBoard = 104,
// Unknown105 = 105,
// Unknown106 = 106,
// Unknown107 = 107,
// Unknown108 = 108,
// Unknown109 = 109,
// Unknown110 = 110,
// Unknown111 = 111,
} }

View file

@ -1,3 +1,4 @@
using System.Diagnostics.CodeAnalysis;
using System.Numerics; using System.Numerics;
using Dalamud.Data; using Dalamud.Data;
@ -7,10 +8,12 @@ using Dalamud.Memory;
using Lumina.Excel; using Lumina.Excel;
using CSFateContext = FFXIVClientStructs.FFXIV.Client.Game.Fate.FateContext;
namespace Dalamud.Game.ClientState.Fates; namespace Dalamud.Game.ClientState.Fates;
/// <summary> /// <summary>
/// Interface representing an fate entry that can be seen in the current area. /// Interface representing a fate entry that can be seen in the current area.
/// </summary> /// </summary>
public interface IFate : IEquatable<IFate> public interface IFate : IEquatable<IFate>
{ {
@ -112,129 +115,96 @@ public interface IFate : IEquatable<IFate>
/// <summary> /// <summary>
/// Gets the address of this Fate in memory. /// Gets the address of this Fate in memory.
/// </summary> /// </summary>
IntPtr Address { get; } nint Address { get; }
} }
/// <summary> /// <summary>
/// This class represents an FFXIV Fate. /// This struct represents a Fate.
/// </summary> /// </summary>
internal unsafe partial class Fate /// <param name="ptr">A pointer to the FateContext.</param>
internal readonly unsafe struct Fate(CSFateContext* ptr) : IFate
{ {
/// <summary>
/// Initializes a new instance of the <see cref="Fate"/> class.
/// </summary>
/// <param name="address">The address of this fate in memory.</param>
internal Fate(IntPtr address)
{
this.Address = address;
}
/// <inheritdoc /> /// <inheritdoc />
public IntPtr Address { get; } public nint Address => (nint)ptr;
private FFXIVClientStructs.FFXIV.Client.Game.Fate.FateContext* Struct => (FFXIVClientStructs.FFXIV.Client.Game.Fate.FateContext*)this.Address;
public static bool operator ==(Fate fate1, Fate fate2)
{
if (fate1 is null || fate2 is null)
return Equals(fate1, fate2);
return fate1.Equals(fate2);
}
public static bool operator !=(Fate fate1, Fate fate2) => !(fate1 == fate2);
/// <summary>
/// Gets a value indicating whether this Fate is still valid in memory.
/// </summary>
/// <param name="fate">The fate to check.</param>
/// <returns>True or false.</returns>
public static bool IsValid(Fate fate)
{
if (fate == null)
return false;
var playerState = Service<PlayerState>.Get();
return playerState.IsLoaded == true;
}
/// <summary>
/// Gets a value indicating whether this actor is still valid in memory.
/// </summary>
/// <returns>True or false.</returns>
public bool IsValid() => IsValid(this);
/// <inheritdoc/> /// <inheritdoc/>
bool IEquatable<IFate>.Equals(IFate other) => this.FateId == other?.FateId; public ushort FateId => ptr->FateId;
/// <inheritdoc/>
public override bool Equals(object obj) => ((IEquatable<IFate>)this).Equals(obj as IFate);
/// <inheritdoc/>
public override int GetHashCode() => this.FateId.GetHashCode();
}
/// <summary>
/// This class represents an FFXIV Fate.
/// </summary>
internal unsafe partial class Fate : IFate
{
/// <inheritdoc/>
public ushort FateId => this.Struct->FateId;
/// <inheritdoc/> /// <inheritdoc/>
public RowRef<Lumina.Excel.Sheets.Fate> GameData => LuminaUtils.CreateRef<Lumina.Excel.Sheets.Fate>(this.FateId); public RowRef<Lumina.Excel.Sheets.Fate> GameData => LuminaUtils.CreateRef<Lumina.Excel.Sheets.Fate>(this.FateId);
/// <inheritdoc/> /// <inheritdoc/>
public int StartTimeEpoch => this.Struct->StartTimeEpoch; public int StartTimeEpoch => ptr->StartTimeEpoch;
/// <inheritdoc/> /// <inheritdoc/>
public short Duration => this.Struct->Duration; public short Duration => ptr->Duration;
/// <inheritdoc/> /// <inheritdoc/>
public long TimeRemaining => this.StartTimeEpoch + this.Duration - DateTimeOffset.Now.ToUnixTimeSeconds(); public long TimeRemaining => this.StartTimeEpoch + this.Duration - DateTimeOffset.Now.ToUnixTimeSeconds();
/// <inheritdoc/> /// <inheritdoc/>
public SeString Name => MemoryHelper.ReadSeString(&this.Struct->Name); public SeString Name => MemoryHelper.ReadSeString(&ptr->Name);
/// <inheritdoc/> /// <inheritdoc/>
public SeString Description => MemoryHelper.ReadSeString(&this.Struct->Description); public SeString Description => MemoryHelper.ReadSeString(&ptr->Description);
/// <inheritdoc/> /// <inheritdoc/>
public SeString Objective => MemoryHelper.ReadSeString(&this.Struct->Objective); public SeString Objective => MemoryHelper.ReadSeString(&ptr->Objective);
/// <inheritdoc/> /// <inheritdoc/>
public FateState State => (FateState)this.Struct->State; public FateState State => (FateState)ptr->State;
/// <inheritdoc/> /// <inheritdoc/>
public byte HandInCount => this.Struct->HandInCount; public byte HandInCount => ptr->HandInCount;
/// <inheritdoc/> /// <inheritdoc/>
public byte Progress => this.Struct->Progress; public byte Progress => ptr->Progress;
/// <inheritdoc/> /// <inheritdoc/>
public bool HasBonus => this.Struct->IsBonus; public bool HasBonus => ptr->IsBonus;
/// <inheritdoc/> /// <inheritdoc/>
public uint IconId => this.Struct->IconId; public uint IconId => ptr->IconId;
/// <inheritdoc/> /// <inheritdoc/>
public byte Level => this.Struct->Level; public byte Level => ptr->Level;
/// <inheritdoc/> /// <inheritdoc/>
public byte MaxLevel => this.Struct->MaxLevel; public byte MaxLevel => ptr->MaxLevel;
/// <inheritdoc/> /// <inheritdoc/>
public Vector3 Position => this.Struct->Location; public Vector3 Position => ptr->Location;
/// <inheritdoc/> /// <inheritdoc/>
public float Radius => this.Struct->Radius; public float Radius => ptr->Radius;
/// <inheritdoc/> /// <inheritdoc/>
public uint MapIconId => this.Struct->MapIconId; public uint MapIconId => ptr->MapIconId;
/// <summary> /// <summary>
/// Gets the territory this <see cref="Fate"/> is located in. /// Gets the territory this <see cref="Fate"/> is located in.
/// </summary> /// </summary>
public RowRef<Lumina.Excel.Sheets.TerritoryType> TerritoryType => LuminaUtils.CreateRef<Lumina.Excel.Sheets.TerritoryType>(this.Struct->MapMarkers[0].MapMarkerData.TerritoryTypeId); public RowRef<Lumina.Excel.Sheets.TerritoryType> TerritoryType => LuminaUtils.CreateRef<Lumina.Excel.Sheets.TerritoryType>(ptr->MapMarkers[0].MapMarkerData.TerritoryTypeId);
public static bool operator ==(Fate x, Fate y) => x.Equals(y);
public static bool operator !=(Fate x, Fate y) => !(x == y);
/// <inheritdoc/>
public bool Equals(IFate? other)
{
return this.FateId == other.FateId;
}
/// <inheritdoc/>
public override bool Equals([NotNullWhen(true)] object? obj)
{
return obj is Fate fate && this.Equals(fate);
}
/// <inheritdoc/>
public override int GetHashCode()
{
return this.FateId.GetHashCode();
}
} }

View file

@ -6,6 +6,7 @@ using Dalamud.IoC;
using Dalamud.IoC.Internal; using Dalamud.IoC.Internal;
using Dalamud.Plugin.Services; using Dalamud.Plugin.Services;
using CSFateContext = FFXIVClientStructs.FFXIV.Client.Game.Fate.FateContext;
using CSFateManager = FFXIVClientStructs.FFXIV.Client.Game.Fate.FateManager; using CSFateManager = FFXIVClientStructs.FFXIV.Client.Game.Fate.FateManager;
namespace Dalamud.Game.ClientState.Fates; namespace Dalamud.Game.ClientState.Fates;
@ -26,7 +27,7 @@ internal sealed partial class FateTable : IServiceType, IFateTable
} }
/// <inheritdoc/> /// <inheritdoc/>
public unsafe IntPtr Address => (nint)CSFateManager.Instance(); public unsafe nint Address => (nint)CSFateManager.Instance();
/// <inheritdoc/> /// <inheritdoc/>
public unsafe int Length public unsafe int Length
@ -69,29 +70,29 @@ internal sealed partial class FateTable : IServiceType, IFateTable
} }
/// <inheritdoc/> /// <inheritdoc/>
public unsafe IntPtr GetFateAddress(int index) public unsafe nint GetFateAddress(int index)
{ {
if (index >= this.Length) if (index >= this.Length)
return IntPtr.Zero; return 0;
var fateManager = CSFateManager.Instance(); var fateManager = CSFateManager.Instance();
if (fateManager == null) if (fateManager == null)
return IntPtr.Zero; return 0;
return (IntPtr)fateManager->Fates[index].Value; return (nint)fateManager->Fates[index].Value;
} }
/// <inheritdoc/> /// <inheritdoc/>
public IFate? CreateFateReference(IntPtr offset) public unsafe IFate? CreateFateReference(IntPtr address)
{ {
if (offset == IntPtr.Zero) if (address == 0)
return null; return null;
var playerState = Service<PlayerState>.Get(); var clientState = Service<ClientState>.Get();
if (!playerState.IsLoaded) if (clientState.LocalContentId == 0)
return null; return null;
return new Fate(offset); return new Fate((CSFateContext*)address);
} }
} }
@ -106,12 +107,39 @@ internal sealed partial class FateTable
/// <inheritdoc/> /// <inheritdoc/>
public IEnumerator<IFate> GetEnumerator() public IEnumerator<IFate> GetEnumerator()
{ {
for (var i = 0; i < this.Length; i++) return new Enumerator(this);
{
yield return this[i];
}
} }
/// <inheritdoc/> /// <inheritdoc/>
IEnumerator IEnumerable.GetEnumerator() => this.GetEnumerator(); IEnumerator IEnumerable.GetEnumerator() => this.GetEnumerator();
private struct Enumerator(FateTable fateTable) : IEnumerator<IFate>
{
private int index = -1;
public IFate Current { get; private set; }
object IEnumerator.Current => this.Current;
public bool MoveNext()
{
if (++this.index < fateTable.Length)
{
this.Current = fateTable[this.index];
return true;
}
this.Current = default;
return false;
}
public void Reset()
{
this.index = -1;
}
public void Dispose()
{
}
}
} }

View file

@ -13,8 +13,6 @@ using Dalamud.Utility;
using FFXIVClientStructs.Interop; using FFXIVClientStructs.Interop;
using Microsoft.Extensions.ObjectPool;
using CSGameObject = FFXIVClientStructs.FFXIV.Client.Game.Object.GameObject; using CSGameObject = FFXIVClientStructs.FFXIV.Client.Game.Object.GameObject;
using CSGameObjectManager = FFXIVClientStructs.FFXIV.Client.Game.Object.GameObjectManager; using CSGameObjectManager = FFXIVClientStructs.FFXIV.Client.Game.Object.GameObjectManager;
@ -37,8 +35,6 @@ internal sealed partial class ObjectTable : IServiceType, IObjectTable
private readonly CachedEntry[] cachedObjectTable; private readonly CachedEntry[] cachedObjectTable;
private readonly Enumerator?[] frameworkThreadEnumerators = new Enumerator?[4];
[ServiceManager.ServiceConstructor] [ServiceManager.ServiceConstructor]
private unsafe ObjectTable() private unsafe ObjectTable()
{ {
@ -48,9 +44,6 @@ internal sealed partial class ObjectTable : IServiceType, IObjectTable
this.cachedObjectTable = new CachedEntry[objectTableLength]; this.cachedObjectTable = new CachedEntry[objectTableLength];
for (var i = 0; i < this.cachedObjectTable.Length; i++) for (var i = 0; i < this.cachedObjectTable.Length; i++)
this.cachedObjectTable[i] = new(nativeObjectTable.GetPointer(i)); this.cachedObjectTable[i] = new(nativeObjectTable.GetPointer(i));
for (var i = 0; i < this.frameworkThreadEnumerators.Length; i++)
this.frameworkThreadEnumerators[i] = new(this, i);
} }
/// <inheritdoc/> /// <inheritdoc/>
@ -243,43 +236,25 @@ internal sealed partial class ObjectTable
public IEnumerator<IGameObject> GetEnumerator() public IEnumerator<IGameObject> GetEnumerator()
{ {
ThreadSafety.AssertMainThread(); ThreadSafety.AssertMainThread();
return new Enumerator(this);
// If we're on the framework thread, see if there's an already allocated enumerator available for use.
foreach (ref var x in this.frameworkThreadEnumerators.AsSpan())
{
if (x is not null)
{
var t = x;
x = null;
t.Reset();
return t;
}
}
// No reusable enumerator is available; allocate a new temporary one.
return new Enumerator(this, -1);
} }
/// <inheritdoc/> /// <inheritdoc/>
IEnumerator IEnumerable.GetEnumerator() => this.GetEnumerator(); IEnumerator IEnumerable.GetEnumerator() => this.GetEnumerator();
private sealed class Enumerator(ObjectTable owner, int slotId) : IEnumerator<IGameObject>, IResettable private struct Enumerator(ObjectTable owner) : IEnumerator<IGameObject>
{ {
private ObjectTable? owner = owner;
private int index = -1; private int index = -1;
public IGameObject Current { get; private set; } = null!; public IGameObject Current { get; private set; }
object IEnumerator.Current => this.Current; object IEnumerator.Current => this.Current;
public bool MoveNext() public bool MoveNext()
{ {
if (this.index == objectTableLength) var cache = owner.cachedObjectTable.AsSpan();
return false;
var cache = this.owner!.cachedObjectTable.AsSpan(); while (++this.index < objectTableLength)
for (this.index++; this.index < objectTableLength; this.index++)
{ {
if (cache[this.index].Update() is { } ao) if (cache[this.index].Update() is { } ao)
{ {
@ -288,24 +263,17 @@ internal sealed partial class ObjectTable
} }
} }
this.Current = default;
return false; return false;
} }
public void Reset() => this.index = -1; public void Reset()
{
this.index = -1;
}
public void Dispose() public void Dispose()
{ {
if (this.owner is not { } o)
return;
if (slotId != -1)
o.frameworkThreadEnumerators[slotId] = this;
}
public bool TryReset()
{
this.Reset();
return true;
} }
} }
} }

View file

@ -1,6 +1,7 @@
using Dalamud.Game.ClientState.Objects.Types; using Dalamud.Game.ClientState.Objects.Types;
using Dalamud.IoC; using Dalamud.IoC;
using Dalamud.IoC.Internal; using Dalamud.IoC.Internal;
using Dalamud.Plugin.Services;
using FFXIVClientStructs.FFXIV.Client.Game.Control; using FFXIVClientStructs.FFXIV.Client.Game.Control;

View file

@ -9,6 +9,7 @@ using Dalamud.IoC.Internal;
using Dalamud.Plugin.Services; using Dalamud.Plugin.Services;
using CSGroupManager = FFXIVClientStructs.FFXIV.Client.Game.Group.GroupManager; using CSGroupManager = FFXIVClientStructs.FFXIV.Client.Game.Group.GroupManager;
using CSPartyMember = FFXIVClientStructs.FFXIV.Client.Game.Group.PartyMember;
namespace Dalamud.Game.ClientState.Party; namespace Dalamud.Game.ClientState.Party;
@ -43,20 +44,20 @@ internal sealed unsafe partial class PartyList : IServiceType, IPartyList
public bool IsAlliance => this.GroupManagerStruct->MainGroup.AllianceFlags > 0; public bool IsAlliance => this.GroupManagerStruct->MainGroup.AllianceFlags > 0;
/// <inheritdoc/> /// <inheritdoc/>
public unsafe IntPtr GroupManagerAddress => (nint)CSGroupManager.Instance(); public unsafe nint GroupManagerAddress => (nint)CSGroupManager.Instance();
/// <inheritdoc/> /// <inheritdoc/>
public IntPtr GroupListAddress => (IntPtr)Unsafe.AsPointer(ref GroupManagerStruct->MainGroup.PartyMembers[0]); public nint GroupListAddress => (nint)Unsafe.AsPointer(ref GroupManagerStruct->MainGroup.PartyMembers[0]);
/// <inheritdoc/> /// <inheritdoc/>
public IntPtr AllianceListAddress => (IntPtr)Unsafe.AsPointer(ref this.GroupManagerStruct->MainGroup.AllianceMembers[0]); public nint AllianceListAddress => (nint)Unsafe.AsPointer(ref this.GroupManagerStruct->MainGroup.AllianceMembers[0]);
/// <inheritdoc/> /// <inheritdoc/>
public long PartyId => this.GroupManagerStruct->MainGroup.PartyId; public long PartyId => this.GroupManagerStruct->MainGroup.PartyId;
private static int PartyMemberSize { get; } = Marshal.SizeOf<FFXIVClientStructs.FFXIV.Client.Game.Group.PartyMember>(); private static int PartyMemberSize { get; } = Marshal.SizeOf<CSPartyMember>();
private FFXIVClientStructs.FFXIV.Client.Game.Group.GroupManager* GroupManagerStruct => (FFXIVClientStructs.FFXIV.Client.Game.Group.GroupManager*)this.GroupManagerAddress; private CSGroupManager* GroupManagerStruct => (CSGroupManager*)this.GroupManagerAddress;
/// <inheritdoc/> /// <inheritdoc/>
public IPartyMember? this[int index] public IPartyMember? this[int index]
@ -81,39 +82,45 @@ internal sealed unsafe partial class PartyList : IServiceType, IPartyList
} }
/// <inheritdoc/> /// <inheritdoc/>
public IntPtr GetPartyMemberAddress(int index) public nint GetPartyMemberAddress(int index)
{ {
if (index < 0 || index >= GroupLength) if (index < 0 || index >= GroupLength)
return IntPtr.Zero; return 0;
return this.GroupListAddress + (index * PartyMemberSize); return this.GroupListAddress + (index * PartyMemberSize);
} }
/// <inheritdoc/> /// <inheritdoc/>
public IPartyMember? CreatePartyMemberReference(IntPtr address) public IPartyMember? CreatePartyMemberReference(nint address)
{ {
if (address == IntPtr.Zero || !this.playerState.IsLoaded) if (this.playerState.ContentId == 0)
return null; return null;
return new PartyMember(address); if (address == 0)
return null;
return new PartyMember((CSPartyMember*)address);
} }
/// <inheritdoc/> /// <inheritdoc/>
public IntPtr GetAllianceMemberAddress(int index) public nint GetAllianceMemberAddress(int index)
{ {
if (index < 0 || index >= AllianceLength) if (index < 0 || index >= AllianceLength)
return IntPtr.Zero; return 0;
return this.AllianceListAddress + (index * PartyMemberSize); return this.AllianceListAddress + (index * PartyMemberSize);
} }
/// <inheritdoc/> /// <inheritdoc/>
public IPartyMember? CreateAllianceMemberReference(IntPtr address) public IPartyMember? CreateAllianceMemberReference(nint address)
{ {
if (address == IntPtr.Zero || !this.playerState.IsLoaded) if (this.playerState.ContentId == 0)
return null; return null;
return new PartyMember(address); if (address == 0)
return null;
return new PartyMember((CSPartyMember*)address);
} }
} }
@ -128,18 +135,43 @@ internal sealed partial class PartyList
/// <inheritdoc/> /// <inheritdoc/>
public IEnumerator<IPartyMember> GetEnumerator() public IEnumerator<IPartyMember> GetEnumerator()
{ {
// Normally using Length results in a recursion crash, however we know the party size via ptr. return new Enumerator(this);
for (var i = 0; i < this.Length; i++)
{
var member = this[i];
if (member == null)
break;
yield return member;
}
} }
/// <inheritdoc/> /// <inheritdoc/>
IEnumerator IEnumerable.GetEnumerator() => this.GetEnumerator(); IEnumerator IEnumerable.GetEnumerator() => this.GetEnumerator();
private struct Enumerator(PartyList partyList) : IEnumerator<IPartyMember>
{
private int index = -1;
public IPartyMember Current { get; private set; }
object IEnumerator.Current => this.Current;
public bool MoveNext()
{
while (++this.index < partyList.Length)
{
var partyMember = partyList[this.index];
if (partyMember != null)
{
this.Current = partyMember;
return true;
}
}
this.Current = default;
return false;
}
public void Reset()
{
this.index = -1;
}
public void Dispose()
{
}
}
} }

View file

@ -1,26 +1,27 @@
using System.Diagnostics.CodeAnalysis;
using System.Numerics; using System.Numerics;
using System.Runtime.CompilerServices;
using Dalamud.Data; using Dalamud.Data;
using Dalamud.Game.ClientState.Objects; using Dalamud.Game.ClientState.Objects;
using Dalamud.Game.ClientState.Objects.Types; using Dalamud.Game.ClientState.Objects.Types;
using Dalamud.Game.ClientState.Statuses; using Dalamud.Game.ClientState.Statuses;
using Dalamud.Game.Text.SeStringHandling; using Dalamud.Game.Text.SeStringHandling;
using Dalamud.Memory;
using Lumina.Excel; using Lumina.Excel;
using CSPartyMember = FFXIVClientStructs.FFXIV.Client.Game.Group.PartyMember;
namespace Dalamud.Game.ClientState.Party; namespace Dalamud.Game.ClientState.Party;
/// <summary> /// <summary>
/// Interface representing a party member. /// Interface representing a party member.
/// </summary> /// </summary>
public interface IPartyMember public interface IPartyMember : IEquatable<IPartyMember>
{ {
/// <summary> /// <summary>
/// Gets the address of this party member in memory. /// Gets the address of this party member in memory.
/// </summary> /// </summary>
IntPtr Address { get; } nint Address { get; }
/// <summary> /// <summary>
/// Gets a list of buffs or debuffs applied to this party member. /// Gets a list of buffs or debuffs applied to this party member.
@ -108,69 +109,81 @@ public interface IPartyMember
} }
/// <summary> /// <summary>
/// This class represents a party member in the group manager. /// This struct represents a party member in the group manager.
/// </summary> /// </summary>
internal unsafe class PartyMember : IPartyMember /// <param name="ptr">A pointer to the PartyMember.</param>
internal unsafe readonly struct PartyMember(CSPartyMember* ptr) : IPartyMember
{ {
/// <summary> /// <inheritdoc/>
/// Initializes a new instance of the <see cref="PartyMember"/> class. public nint Address => (nint)ptr;
/// </summary>
/// <param name="address">Address of the party member.</param>
internal PartyMember(IntPtr address)
{
this.Address = address;
}
/// <inheritdoc/> /// <inheritdoc/>
public IntPtr Address { get; } public StatusList Statuses => new(&ptr->StatusManager);
/// <inheritdoc/> /// <inheritdoc/>
public StatusList Statuses => new(&this.Struct->StatusManager); public Vector3 Position => ptr->Position;
/// <inheritdoc/> /// <inheritdoc/>
public Vector3 Position => this.Struct->Position; public long ContentId => (long)ptr->ContentId;
/// <inheritdoc/> /// <inheritdoc/>
public long ContentId => (long)this.Struct->ContentId; public uint ObjectId => ptr->EntityId;
/// <inheritdoc/> /// <inheritdoc/>
public uint ObjectId => this.Struct->EntityId; public uint EntityId => ptr->EntityId;
/// <inheritdoc/>
public uint EntityId => this.Struct->EntityId;
/// <inheritdoc/> /// <inheritdoc/>
public IGameObject? GameObject => Service<ObjectTable>.Get().SearchById(this.EntityId); public IGameObject? GameObject => Service<ObjectTable>.Get().SearchById(this.EntityId);
/// <inheritdoc/> /// <inheritdoc/>
public uint CurrentHP => this.Struct->CurrentHP; public uint CurrentHP => ptr->CurrentHP;
/// <inheritdoc/> /// <inheritdoc/>
public uint MaxHP => this.Struct->MaxHP; public uint MaxHP => ptr->MaxHP;
/// <inheritdoc/> /// <inheritdoc/>
public ushort CurrentMP => this.Struct->CurrentMP; public ushort CurrentMP => ptr->CurrentMP;
/// <inheritdoc/> /// <inheritdoc/>
public ushort MaxMP => this.Struct->MaxMP; public ushort MaxMP => ptr->MaxMP;
/// <inheritdoc/> /// <inheritdoc/>
public RowRef<Lumina.Excel.Sheets.TerritoryType> Territory => LuminaUtils.CreateRef<Lumina.Excel.Sheets.TerritoryType>(this.Struct->TerritoryType); public RowRef<Lumina.Excel.Sheets.TerritoryType> Territory => LuminaUtils.CreateRef<Lumina.Excel.Sheets.TerritoryType>(ptr->TerritoryType);
/// <inheritdoc/> /// <inheritdoc/>
public RowRef<Lumina.Excel.Sheets.World> World => LuminaUtils.CreateRef<Lumina.Excel.Sheets.World>(this.Struct->HomeWorld); public RowRef<Lumina.Excel.Sheets.World> World => LuminaUtils.CreateRef<Lumina.Excel.Sheets.World>(ptr->HomeWorld);
/// <inheritdoc/> /// <inheritdoc/>
public SeString Name => SeString.Parse(this.Struct->Name); public SeString Name => SeString.Parse(ptr->Name);
/// <inheritdoc/> /// <inheritdoc/>
public byte Sex => this.Struct->Sex; public byte Sex => ptr->Sex;
/// <inheritdoc/> /// <inheritdoc/>
public RowRef<Lumina.Excel.Sheets.ClassJob> ClassJob => LuminaUtils.CreateRef<Lumina.Excel.Sheets.ClassJob>(this.Struct->ClassJob); public RowRef<Lumina.Excel.Sheets.ClassJob> ClassJob => LuminaUtils.CreateRef<Lumina.Excel.Sheets.ClassJob>(ptr->ClassJob);
/// <inheritdoc/> /// <inheritdoc/>
public byte Level => this.Struct->Level; public byte Level => ptr->Level;
private FFXIVClientStructs.FFXIV.Client.Game.Group.PartyMember* Struct => (FFXIVClientStructs.FFXIV.Client.Game.Group.PartyMember*)this.Address; public static bool operator ==(PartyMember x, PartyMember y) => x.Equals(y);
public static bool operator !=(PartyMember x, PartyMember y) => !(x == y);
/// <inheritdoc/>
public bool Equals(IPartyMember? other)
{
return this.EntityId == other.EntityId;
}
/// <inheritdoc/>
public override bool Equals([NotNullWhen(true)] object? obj)
{
return obj is PartyMember fate && this.Equals(fate);
}
/// <inheritdoc/>
public override int GetHashCode()
{
return this.EntityId.GetHashCode();
}
} }

View file

@ -1,61 +1,49 @@
using System.Diagnostics.CodeAnalysis;
using Dalamud.Data; using Dalamud.Data;
using Dalamud.Game.ClientState.Objects; using Dalamud.Game.ClientState.Objects;
using Dalamud.Game.ClientState.Objects.Types; using Dalamud.Game.ClientState.Objects.Types;
using Lumina.Excel; using Lumina.Excel;
using CSStatus = FFXIVClientStructs.FFXIV.Client.Game.Status;
namespace Dalamud.Game.ClientState.Statuses; namespace Dalamud.Game.ClientState.Statuses;
/// <summary> /// <summary>
/// This class represents a status effect an actor is afflicted by. /// Interface representing a status.
/// </summary> /// </summary>
public unsafe class Status public interface IStatus : IEquatable<IStatus>
{ {
/// <summary>
/// Initializes a new instance of the <see cref="Status"/> class.
/// </summary>
/// <param name="address">Status address.</param>
internal Status(IntPtr address)
{
this.Address = address;
}
/// <summary> /// <summary>
/// Gets the address of the status in memory. /// Gets the address of the status in memory.
/// </summary> /// </summary>
public IntPtr Address { get; } nint Address { get; }
/// <summary> /// <summary>
/// Gets the status ID of this status. /// Gets the status ID of this status.
/// </summary> /// </summary>
public uint StatusId => this.Struct->StatusId; uint StatusId { get; }
/// <summary> /// <summary>
/// Gets the GameData associated with this status. /// Gets the GameData associated with this status.
/// </summary> /// </summary>
public RowRef<Lumina.Excel.Sheets.Status> GameData => LuminaUtils.CreateRef<Lumina.Excel.Sheets.Status>(this.Struct->StatusId); RowRef<Lumina.Excel.Sheets.Status> GameData { get; }
/// <summary> /// <summary>
/// Gets the parameter value of the status. /// Gets the parameter value of the status.
/// </summary> /// </summary>
public ushort Param => this.Struct->Param; ushort Param { get; }
/// <summary>
/// Gets the stack count of this status.
/// Only valid if this is a non-food status.
/// </summary>
[Obsolete($"Replaced with {nameof(Param)}", true)]
public byte StackCount => (byte)this.Struct->Param;
/// <summary> /// <summary>
/// Gets the time remaining of this status. /// Gets the time remaining of this status.
/// </summary> /// </summary>
public float RemainingTime => this.Struct->RemainingTime; float RemainingTime { get; }
/// <summary> /// <summary>
/// Gets the source ID of this status. /// Gets the source ID of this status.
/// </summary> /// </summary>
public uint SourceId => this.Struct->SourceObject.ObjectId; uint SourceId { get; }
/// <summary> /// <summary>
/// Gets the source actor associated with this status. /// Gets the source actor associated with this status.
@ -63,7 +51,55 @@ public unsafe class Status
/// <remarks> /// <remarks>
/// This iterates the actor table, it should be used with care. /// This iterates the actor table, it should be used with care.
/// </remarks> /// </remarks>
IGameObject? SourceObject { get; }
}
/// <summary>
/// This struct represents a status effect an actor is afflicted by.
/// </summary>
/// <param name="ptr">A pointer to the Status.</param>
internal unsafe readonly struct Status(CSStatus* ptr) : IStatus
{
/// <inheritdoc/>
public nint Address => (nint)ptr;
/// <inheritdoc/>
public uint StatusId => ptr->StatusId;
/// <inheritdoc/>
public RowRef<Lumina.Excel.Sheets.Status> GameData => LuminaUtils.CreateRef<Lumina.Excel.Sheets.Status>(ptr->StatusId);
/// <inheritdoc/>
public ushort Param => ptr->Param;
/// <inheritdoc/>
public float RemainingTime => ptr->RemainingTime;
/// <inheritdoc/>
public uint SourceId => ptr->SourceObject.ObjectId;
/// <inheritdoc/>
public IGameObject? SourceObject => Service<ObjectTable>.Get().SearchById(this.SourceId); public IGameObject? SourceObject => Service<ObjectTable>.Get().SearchById(this.SourceId);
private FFXIVClientStructs.FFXIV.Client.Game.Status* Struct => (FFXIVClientStructs.FFXIV.Client.Game.Status*)this.Address; public static bool operator ==(Status x, Status y) => x.Equals(y);
public static bool operator !=(Status x, Status y) => !(x == y);
/// <inheritdoc/>
public bool Equals(IStatus? other)
{
return this.StatusId == other.StatusId && this.SourceId == other.SourceId && this.Param == other.Param && this.RemainingTime == other.RemainingTime;
}
/// <inheritdoc/>
public override bool Equals([NotNullWhen(true)] object? obj)
{
return obj is Status fate && this.Equals(fate);
}
/// <inheritdoc/>
public override int GetHashCode()
{
return HashCode.Combine(this.StatusId, this.SourceId, this.Param, this.RemainingTime);
}
} }

View file

@ -3,7 +3,7 @@ using System.Collections.Generic;
using System.Runtime.CompilerServices; using System.Runtime.CompilerServices;
using System.Runtime.InteropServices; using System.Runtime.InteropServices;
using Dalamud.Game.Player; using CSStatus = FFXIVClientStructs.FFXIV.Client.Game.Status;
namespace Dalamud.Game.ClientState.Statuses; namespace Dalamud.Game.ClientState.Statuses;
@ -16,7 +16,7 @@ public sealed unsafe partial class StatusList
/// Initializes a new instance of the <see cref="StatusList"/> class. /// Initializes a new instance of the <see cref="StatusList"/> class.
/// </summary> /// </summary>
/// <param name="address">Address of the status list.</param> /// <param name="address">Address of the status list.</param>
internal StatusList(IntPtr address) internal StatusList(nint address)
{ {
this.Address = address; this.Address = address;
} }
@ -26,14 +26,14 @@ public sealed unsafe partial class StatusList
/// </summary> /// </summary>
/// <param name="pointer">Pointer to the status list.</param> /// <param name="pointer">Pointer to the status list.</param>
internal unsafe StatusList(void* pointer) internal unsafe StatusList(void* pointer)
: this((IntPtr)pointer) : this((nint)pointer)
{ {
} }
/// <summary> /// <summary>
/// Gets the address of the status list in memory. /// Gets the address of the status list in memory.
/// </summary> /// </summary>
public IntPtr Address { get; } public nint Address { get; }
/// <summary> /// <summary>
/// Gets the amount of status effect slots the actor has. /// Gets the amount of status effect slots the actor has.
@ -49,7 +49,7 @@ public sealed unsafe partial class StatusList
/// </summary> /// </summary>
/// <param name="index">Status Index.</param> /// <param name="index">Status Index.</param>
/// <returns>The status at the specified index.</returns> /// <returns>The status at the specified index.</returns>
public Status? this[int index] public IStatus? this[int index]
{ {
get get
{ {
@ -66,7 +66,7 @@ public sealed unsafe partial class StatusList
/// </summary> /// </summary>
/// <param name="address">The address of the status list in memory.</param> /// <param name="address">The address of the status list in memory.</param>
/// <returns>The status object containing the requested data.</returns> /// <returns>The status object containing the requested data.</returns>
public static StatusList? CreateStatusListReference(IntPtr address) public static StatusList? CreateStatusListReference(nint address)
{ {
if (address == IntPtr.Zero) if (address == IntPtr.Zero)
return null; return null;
@ -74,8 +74,12 @@ public sealed unsafe partial class StatusList
// The use case for CreateStatusListReference and CreateStatusReference to be static is so // The use case for CreateStatusListReference and CreateStatusReference to be static is so
// fake status lists can be generated. Since they aren't exposed as services, it's either // fake status lists can be generated. Since they aren't exposed as services, it's either
// here or somewhere else. // here or somewhere else.
var playerState = Service<PlayerState>.Get(); var clientState = Service<ClientState>.Get();
if (!playerState.IsLoaded)
if (clientState.LocalContentId == 0)
return null;
if (address == 0)
return null; return null;
return new StatusList(address); return new StatusList(address);
@ -86,16 +90,15 @@ public sealed unsafe partial class StatusList
/// </summary> /// </summary>
/// <param name="address">The address of the status effect in memory.</param> /// <param name="address">The address of the status effect in memory.</param>
/// <returns>The status object containing the requested data.</returns> /// <returns>The status object containing the requested data.</returns>
public static Status? CreateStatusReference(IntPtr address) public static IStatus? CreateStatusReference(nint address)
{ {
if (address == IntPtr.Zero) if (address == IntPtr.Zero)
return null; return null;
var playerState = Service<PlayerState>.Get(); if (address == 0)
if (!playerState.IsLoaded)
return null; return null;
return new Status(address); return new Status((CSStatus*)address);
} }
/// <summary> /// <summary>
@ -103,22 +106,22 @@ public sealed unsafe partial class StatusList
/// </summary> /// </summary>
/// <param name="index">The index of the status.</param> /// <param name="index">The index of the status.</param>
/// <returns>The memory address of the status.</returns> /// <returns>The memory address of the status.</returns>
public IntPtr GetStatusAddress(int index) public nint GetStatusAddress(int index)
{ {
if (index < 0 || index >= this.Length) if (index < 0 || index >= this.Length)
return IntPtr.Zero; return 0;
return (IntPtr)Unsafe.AsPointer(ref this.Struct->Status[index]); return (nint)Unsafe.AsPointer(ref this.Struct->Status[index]);
} }
} }
/// <summary> /// <summary>
/// This collection represents the status effects an actor is afflicted by. /// This collection represents the status effects an actor is afflicted by.
/// </summary> /// </summary>
public sealed partial class StatusList : IReadOnlyCollection<Status>, ICollection public sealed partial class StatusList : IReadOnlyCollection<IStatus>, ICollection
{ {
/// <inheritdoc/> /// <inheritdoc/>
int IReadOnlyCollection<Status>.Count => this.Length; int IReadOnlyCollection<IStatus>.Count => this.Length;
/// <inheritdoc/> /// <inheritdoc/>
int ICollection.Count => this.Length; int ICollection.Count => this.Length;
@ -130,17 +133,9 @@ public sealed partial class StatusList : IReadOnlyCollection<Status>, ICollectio
object ICollection.SyncRoot => this; object ICollection.SyncRoot => this;
/// <inheritdoc/> /// <inheritdoc/>
public IEnumerator<Status> GetEnumerator() public IEnumerator<IStatus> GetEnumerator()
{ {
for (var i = 0; i < this.Length; i++) return new Enumerator(this);
{
var status = this[i];
if (status == null || status.StatusId == 0)
continue;
yield return status;
}
} }
/// <inheritdoc/> /// <inheritdoc/>
@ -155,4 +150,38 @@ public sealed partial class StatusList : IReadOnlyCollection<Status>, ICollectio
index++; index++;
} }
} }
private struct Enumerator(StatusList statusList) : IEnumerator<IStatus>
{
private int index = -1;
public IStatus Current { get; private set; }
object IEnumerator.Current => this.Current;
public bool MoveNext()
{
while (++this.index < statusList.Length)
{
var status = statusList[this.index];
if (status != null && status.StatusId != 0)
{
this.Current = status;
return true;
}
}
this.Current = default;
return false;
}
public void Reset()
{
this.index = -1;
}
public void Dispose()
{
}
}
} }

View file

@ -1,35 +0,0 @@
using System.Runtime.InteropServices;
namespace Dalamud.Game.ClientState.Structs;
/// <summary>
/// Native memory representation of a FFXIV status effect.
/// </summary>
[StructLayout(LayoutKind.Sequential)]
public struct StatusEffect
{
/// <summary>
/// The effect ID.
/// </summary>
public short EffectId;
/// <summary>
/// How many stacks are present.
/// </summary>
public byte StackCount;
/// <summary>
/// Additional parameters.
/// </summary>
public byte Param;
/// <summary>
/// The duration remaining.
/// </summary>
public float Duration;
/// <summary>
/// The ID of the actor that caused this effect.
/// </summary>
public int OwnerId;
}

View file

@ -1,4 +1,6 @@
namespace Dalamud.Game.Config; using Dalamud.Plugin.Services;
namespace Dalamud.Game.Config;
/// <summary> /// <summary>
/// Game config system address resolver. /// Game config system address resolver.

View file

@ -4069,6 +4069,13 @@ public enum UiConfigOption
[GameConfigOption("GposePortraitRotateType", ConfigType.UInt)] [GameConfigOption("GposePortraitRotateType", ConfigType.UInt)]
GposePortraitRotateType, GposePortraitRotateType,
/// <summary>
/// UiConfig option with the internal name GroupPosePortraitUnlockAspectLimit.
/// This option is a UInt.
/// </summary>
[GameConfigOption("GroupPosePortraitUnlockAspectLimit", ConfigType.UInt)]
GroupPosePortraitUnlockAspectLimit,
/// <summary> /// <summary>
/// UiConfig option with the internal name LsListSortPriority. /// UiConfig option with the internal name LsListSortPriority.
/// This option is a UInt. /// This option is a UInt.

View file

@ -1,3 +1,5 @@
using Dalamud.Plugin.Services;
namespace Dalamud.Game.DutyState; namespace Dalamud.Game.DutyState;
/// <summary> /// <summary>

View file

@ -31,7 +31,7 @@ internal sealed unsafe class ContextMenu : IInternalDisposableService, IContextM
private static readonly ModuleLog Log = new("ContextMenu"); private static readonly ModuleLog Log = new("ContextMenu");
private readonly Hook<AtkModuleVf22OpenAddonByAgentDelegate> atkModuleVf22OpenAddonByAgentHook; private readonly Hook<AtkModuleVf22OpenAddonByAgentDelegate> atkModuleVf22OpenAddonByAgentHook;
private readonly Hook<AddonContextMenuOnMenuSelectedDelegate> addonContextMenuOnMenuSelectedHook; private readonly Hook<AddonContextMenu.Delegates.OnMenuSelected> addonContextMenuOnMenuSelectedHook;
private uint? addonContextSubNameId; private uint? addonContextSubNameId;
@ -40,7 +40,7 @@ internal sealed unsafe class ContextMenu : IInternalDisposableService, IContextM
{ {
var raptureAtkModuleVtable = (nint*)RaptureAtkModule.StaticVirtualTablePointer; var raptureAtkModuleVtable = (nint*)RaptureAtkModule.StaticVirtualTablePointer;
this.atkModuleVf22OpenAddonByAgentHook = Hook<AtkModuleVf22OpenAddonByAgentDelegate>.FromAddress(raptureAtkModuleVtable[22], this.AtkModuleVf22OpenAddonByAgentDetour); this.atkModuleVf22OpenAddonByAgentHook = Hook<AtkModuleVf22OpenAddonByAgentDelegate>.FromAddress(raptureAtkModuleVtable[22], this.AtkModuleVf22OpenAddonByAgentDetour);
this.addonContextMenuOnMenuSelectedHook = Hook<AddonContextMenuOnMenuSelectedDelegate>.FromAddress((nint)AddonContextMenu.StaticVirtualTablePointer->OnMenuSelected, this.AddonContextMenuOnMenuSelectedDetour); this.addonContextMenuOnMenuSelectedHook = Hook<AddonContextMenu.Delegates.OnMenuSelected>.FromAddress((nint)AddonContextMenu.StaticVirtualTablePointer->OnMenuSelected, this.AddonContextMenuOnMenuSelectedDetour);
this.atkModuleVf22OpenAddonByAgentHook.Enable(); this.atkModuleVf22OpenAddonByAgentHook.Enable();
this.addonContextMenuOnMenuSelectedHook.Enable(); this.addonContextMenuOnMenuSelectedHook.Enable();
@ -48,10 +48,6 @@ internal sealed unsafe class ContextMenu : IInternalDisposableService, IContextM
private delegate ushort AtkModuleVf22OpenAddonByAgentDelegate(AtkModule* module, byte* addonName, int valueCount, AtkValue* values, AgentInterface* agent, nint a7, bool a8); private delegate ushort AtkModuleVf22OpenAddonByAgentDelegate(AtkModule* module, byte* addonName, int valueCount, AtkValue* values, AgentInterface* agent, nint a7, bool a8);
private delegate bool AddonContextMenuOnMenuSelectedDelegate(AddonContextMenu* addon, int selectedIdx, byte a3);
private delegate ushort RaptureAtkModuleOpenAddonDelegate(RaptureAtkModule* a1, uint addonNameId, uint valueCount, AtkValue* values, AgentInterface* parentAgent, ulong unk, ushort parentAddonId, int unk2);
/// <inheritdoc/> /// <inheritdoc/>
public event IContextMenu.OnMenuOpenedDelegate? OnMenuOpened; public event IContextMenu.OnMenuOpenedDelegate? OnMenuOpened;
@ -185,7 +181,7 @@ internal sealed unsafe class ContextMenu : IInternalDisposableService, IContextM
values[0].ChangeType(ValueType.UInt); values[0].ChangeType(ValueType.UInt);
values[0].UInt = 0; values[0].UInt = 0;
values[1].ChangeType(ValueType.String); values[1].ChangeType(ValueType.String);
values[1].SetManagedString(name.Encode().NullTerminate()); values[1].SetManagedString(name.EncodeWithNullTerminator());
values[2].ChangeType(ValueType.Int); values[2].ChangeType(ValueType.Int);
values[2].Int = x; values[2].Int = x;
values[3].ChangeType(ValueType.Int); values[3].ChangeType(ValueType.Int);
@ -265,7 +261,7 @@ internal sealed unsafe class ContextMenu : IInternalDisposableService, IContextM
submenuMask |= 1u << i; submenuMask |= 1u << i;
nameData[i].ChangeType(ValueType.String); nameData[i].ChangeType(ValueType.String);
nameData[i].SetManagedString(this.GetPrefixedName(item).Encode().NullTerminate()); nameData[i].SetManagedString(this.GetPrefixedName(item).EncodeWithNullTerminator());
} }
for (var i = 0; i < prefixMenuSize; ++i) for (var i = 0; i < prefixMenuSize; ++i)
@ -295,8 +291,9 @@ internal sealed unsafe class ContextMenu : IInternalDisposableService, IContextM
// 2: UInt = Return Mask (?) // 2: UInt = Return Mask (?)
// 3: UInt = Submenu Mask // 3: UInt = Submenu Mask
// 4: UInt = OpenAtCursorPosition ? 2 : 1 // 4: UInt = OpenAtCursorPosition ? 2 : 1
// 5: UInt = 0 // 5: UInt = ?
// 6: UInt = 0 // 6: UInt = ?
// 7: UInt = ?
foreach (var item in items) foreach (var item in items)
{ {
@ -312,7 +309,7 @@ internal sealed unsafe class ContextMenu : IInternalDisposableService, IContextM
} }
} }
this.SetupGenericMenu(7, 0, 2, 3, items, ref valueCount, ref values); this.SetupGenericMenu(8, 0, 2, 3, items, ref valueCount, ref values);
} }
private void SetupContextSubMenu(IReadOnlyList<IMenuItem> items, ref int valueCount, ref AtkValue* values) private void SetupContextSubMenu(IReadOnlyList<IMenuItem> items, ref int valueCount, ref AtkValue* values)

View file

@ -150,7 +150,7 @@ internal sealed unsafe class DtrBarEntry : IDisposable, IDtrBarEntry
} }
/// <inheritdoc/> /// <inheritdoc/>
[Api13ToDo("Maybe make this config scoped to internal name?")] [Api14ToDo("Maybe make this config scoped to internal name?")]
public bool UserHidden => this.configuration.DtrIgnore?.Contains(this.Title) ?? false; public bool UserHidden => this.configuration.DtrIgnore?.Contains(this.Title) ?? false;
/// <inheritdoc/> /// <inheritdoc/>

View file

@ -92,34 +92,16 @@ public enum FlyTextKind : int
/// </summary> /// </summary>
IslandExp = 15, IslandExp = 15,
/// <summary>
/// Val1 in serif font next to all caps condensed font Text1 with Text2 in sans-serif as subtitle.
/// </summary>
[Obsolete("Use Dataset instead", true)]
Unknown16 = 16,
/// <summary> /// <summary>
/// Val1 in serif font next to all caps condensed font Text1 with Text2 in sans-serif as subtitle. /// Val1 in serif font next to all caps condensed font Text1 with Text2 in sans-serif as subtitle.
/// </summary> /// </summary>
Dataset = 16, Dataset = 16,
/// <summary>
/// Val1 in serif font, Text2 in sans-serif as subtitle.
/// </summary>
[Obsolete("Use Knowledge instead", true)]
Unknown17 = 17,
/// <summary> /// <summary>
/// Val1 in serif font, Text2 in sans-serif as subtitle. /// Val1 in serif font, Text2 in sans-serif as subtitle.
/// </summary> /// </summary>
Knowledge = 17, Knowledge = 17,
/// <summary>
/// Val1 in serif font, Text2 in sans-serif as subtitle.
/// </summary>
[Obsolete("Use PhantomExp instead", true)]
Unknown18 = 18,
/// <summary> /// <summary>
/// Val1 in serif font, Text2 in sans-serif as subtitle. /// Val1 in serif font, Text2 in sans-serif as subtitle.
/// </summary> /// </summary>

View file

@ -1,3 +1,5 @@
using Dalamud.Plugin.Services;
namespace Dalamud.Game.Gui; namespace Dalamud.Game.Gui;
/// <summary> /// <summary>

View file

@ -14,140 +14,145 @@ public enum HoverActionKind
/// <summary> /// <summary>
/// A regular action is hovered. /// A regular action is hovered.
/// </summary> /// </summary>
Action = 28, Action = 29,
/// <summary> /// <summary>
/// A crafting action is hovered. /// A crafting action is hovered.
/// </summary> /// </summary>
CraftingAction = 29, CraftingAction = 30,
/// <summary> /// <summary>
/// A general action is hovered. /// A general action is hovered.
/// </summary> /// </summary>
GeneralAction = 30, GeneralAction = 31,
/// <summary> /// <summary>
/// A companion order type of action is hovered. /// A companion order type of action is hovered.
/// </summary> /// </summary>
CompanionOrder = 31, // Game Term: BuddyOrder CompanionOrder = 32, // Game Term: BuddyOrder
/// <summary> /// <summary>
/// A main command type of action is hovered. /// A main command type of action is hovered.
/// </summary> /// </summary>
MainCommand = 32, MainCommand = 33,
/// <summary> /// <summary>
/// An extras command type of action is hovered. /// An extras command type of action is hovered.
/// </summary> /// </summary>
ExtraCommand = 33, ExtraCommand = 34,
/// <summary> /// <summary>
/// A companion action is hovered. /// A companion action is hovered.
/// </summary> /// </summary>
Companion = 34, Companion = 35,
/// <summary> /// <summary>
/// A pet order type of action is hovered. /// A pet order type of action is hovered.
/// </summary> /// </summary>
PetOrder = 35, PetOrder = 36,
/// <summary> /// <summary>
/// A trait is hovered. /// A trait is hovered.
/// </summary> /// </summary>
Trait = 36, Trait = 37,
/// <summary> /// <summary>
/// A buddy action is hovered. /// A buddy action is hovered.
/// </summary> /// </summary>
BuddyAction = 37, BuddyAction = 38,
/// <summary> /// <summary>
/// A company action is hovered. /// A company action is hovered.
/// </summary> /// </summary>
CompanyAction = 38, CompanyAction = 39,
/// <summary> /// <summary>
/// A mount is hovered. /// A mount is hovered.
/// </summary> /// </summary>
Mount = 39, Mount = 40,
/// <summary> /// <summary>
/// A chocobo race action is hovered. /// A chocobo race action is hovered.
/// </summary> /// </summary>
ChocoboRaceAction = 40, ChocoboRaceAction = 41,
/// <summary> /// <summary>
/// A chocobo race item is hovered. /// A chocobo race item is hovered.
/// </summary> /// </summary>
ChocoboRaceItem = 41, ChocoboRaceItem = 42,
/// <summary> /// <summary>
/// A deep dungeon equipment is hovered. /// A deep dungeon equipment is hovered.
/// </summary> /// </summary>
DeepDungeonEquipment = 42, DeepDungeonEquipment = 43,
/// <summary> /// <summary>
/// A deep dungeon equipment 2 is hovered. /// A deep dungeon equipment 2 is hovered.
/// </summary> /// </summary>
DeepDungeonEquipment2 = 43, DeepDungeonEquipment2 = 44,
/// <summary> /// <summary>
/// A deep dungeon item is hovered. /// A deep dungeon item is hovered.
/// </summary> /// </summary>
DeepDungeonItem = 44, DeepDungeonItem = 45,
/// <summary> /// <summary>
/// A quick chat is hovered. /// A quick chat is hovered.
/// </summary> /// </summary>
QuickChat = 45, QuickChat = 46,
/// <summary> /// <summary>
/// An action combo route is hovered. /// An action combo route is hovered.
/// </summary> /// </summary>
ActionComboRoute = 46, ActionComboRoute = 47,
/// <summary> /// <summary>
/// A pvp trait is hovered. /// A pvp trait is hovered.
/// </summary> /// </summary>
PvPSelectTrait = 47, PvPSelectTrait = 48,
/// <summary> /// <summary>
/// A squadron action is hovered. /// A squadron action is hovered.
/// </summary> /// </summary>
BgcArmyAction = 48, BgcArmyAction = 49,
/// <summary> /// <summary>
/// A perform action is hovered. /// A perform action is hovered.
/// </summary> /// </summary>
Perform = 49, Perform = 50,
/// <summary> /// <summary>
/// A deep dungeon magic stone is hovered. /// A deep dungeon magic stone is hovered.
/// </summary> /// </summary>
DeepDungeonMagicStone = 50, DeepDungeonMagicStone = 51,
/// <summary> /// <summary>
/// A deep dungeon demiclone is hovered. /// A deep dungeon demiclone is hovered.
/// </summary> /// </summary>
DeepDungeonDemiclone = 51, DeepDungeonDemiclone = 52,
/// <summary> /// <summary>
/// An eureka magia action is hovered. /// An eureka magia action is hovered.
/// </summary> /// </summary>
EurekaMagiaAction = 52, EurekaMagiaAction = 53,
/// <summary> /// <summary>
/// An island sanctuary temporary item is hovered. /// An island sanctuary temporary item is hovered.
/// </summary> /// </summary>
MYCTemporaryItem = 53, MYCTemporaryItem = 54,
/// <summary> /// <summary>
/// An ornament is hovered. /// An ornament is hovered.
/// </summary> /// </summary>
Ornament = 54, Ornament = 55,
/// <summary> /// <summary>
/// Glasses are hovered. /// Glasses are hovered.
/// </summary> /// </summary>
Glasses = 55, Glasses = 56,
/// <summary>
/// Phantom Job Trait is hovered.
/// </summary>
MKDTrait = 58,
} }

View file

@ -1,3 +1,5 @@
using Dalamud.Plugin.Services;
namespace Dalamud.Game.Gui.NamePlate; namespace Dalamud.Game.Gui.NamePlate;
/// <summary> /// <summary>

View file

@ -113,7 +113,7 @@ internal sealed unsafe class DalamudAtkTweaks : IInternalDisposableService
private void AtkUnitBaseReceiveGlobalEventDetour(AtkUnitBase* thisPtr, AtkEventType eventType, int eventParam, AtkEvent* atkEvent, AtkEventData* atkEventData) private void AtkUnitBaseReceiveGlobalEventDetour(AtkUnitBase* thisPtr, AtkEventType eventType, int eventParam, AtkEvent* atkEvent, AtkEventData* atkEventData)
{ {
// 3 == Close // 3 == Close
if (eventType == AtkEventType.InputReceived && WindowSystem.HasAnyWindowSystemFocus && atkEventData != null && *(int*)atkEventData == 3 && this.configuration.IsFocusManagementEnabled) if (eventType == AtkEventType.InputReceived && WindowSystem.ShouldInhibitAtkCloseEvents && atkEventData != null && *(int*)atkEventData == 3 && this.configuration.IsFocusManagementEnabled)
{ {
Log.Verbose($"Cancelling global event SendHotkey command due to WindowSystem {WindowSystem.FocusedWindowSystemNamespace}"); Log.Verbose($"Cancelling global event SendHotkey command due to WindowSystem {WindowSystem.FocusedWindowSystemNamespace}");
return; return;
@ -124,7 +124,7 @@ internal sealed unsafe class DalamudAtkTweaks : IInternalDisposableService
private void AgentHudOpenSystemMenuDetour(AgentHUD* thisPtr, AtkValue* atkValueArgs, uint menuSize) private void AgentHudOpenSystemMenuDetour(AgentHUD* thisPtr, AtkValue* atkValueArgs, uint menuSize)
{ {
if (WindowSystem.HasAnyWindowSystemFocus && this.configuration.IsFocusManagementEnabled) if (WindowSystem.ShouldInhibitAtkCloseEvents && this.configuration.IsFocusManagementEnabled)
{ {
Log.Verbose($"Cancelling OpenSystemMenu due to WindowSystem {WindowSystem.FocusedWindowSystemNamespace}"); Log.Verbose($"Cancelling OpenSystemMenu due to WindowSystem {WindowSystem.FocusedWindowSystemNamespace}");
return; return;

View file

@ -305,7 +305,8 @@ internal class GameInventory : IInternalDisposableService
private GameInventoryItem[] CreateItemsArray(int length) private GameInventoryItem[] CreateItemsArray(int length)
{ {
var items = new GameInventoryItem[length]; var items = new GameInventoryItem[length];
items.Initialize(); foreach (ref var item in items.AsSpan())
item = new();
return items; return items;
} }

View file

@ -1,3 +1,5 @@
using Dalamud.Plugin.Services;
namespace Dalamud.Game.Network; namespace Dalamud.Game.Network;
/// <summary> /// <summary>

View file

@ -1,4 +1,6 @@
namespace Dalamud.Game.Network.Internal; using Dalamud.Plugin.Services;
namespace Dalamud.Game.Network.Internal;
/// <summary> /// <summary>
/// Internal address resolver for the network handlers. /// Internal address resolver for the network handlers.

View file

@ -77,7 +77,7 @@ internal unsafe class PlayerState : IServiceType, IPlayerState
public RowRef<ClassJob> ClassJob => this.IsLoaded ? LuminaUtils.CreateRef<ClassJob>(CSPlayerState.Instance()->CurrentClassJobId) : default; public RowRef<ClassJob> ClassJob => this.IsLoaded ? LuminaUtils.CreateRef<ClassJob>(CSPlayerState.Instance()->CurrentClassJobId) : default;
/// <inheritdoc/> /// <inheritdoc/>
public short Level => this.IsLoaded ? CSPlayerState.Instance()->CurrentLevel : default; public short Level => this.IsLoaded && this.ClassJob.IsValid ? this.GetClassJobLevel(this.ClassJob.Value) : this.EffectiveLevel;
/// <inheritdoc/> /// <inheritdoc/>
public bool IsLevelSynced => this.IsLoaded && CSPlayerState.Instance()->IsLevelSynced; public bool IsLevelSynced => this.IsLoaded && CSPlayerState.Instance()->IsLevelSynced;

View file

@ -8,6 +8,8 @@ using System.Runtime.CompilerServices;
using System.Runtime.InteropServices; using System.Runtime.InteropServices;
using System.Threading; using System.Threading;
using Dalamud.Plugin.Services;
using Iced.Intel; using Iced.Intel;
using Newtonsoft.Json; using Newtonsoft.Json;
using Serilog; using Serilog;

View file

@ -1,8 +1,9 @@
using System.Diagnostics; using System.Diagnostics;
using System.IO; using System.IO;
using Dalamud.IoC; using Dalamud.IoC;
using Dalamud.IoC.Internal; using Dalamud.IoC.Internal;
using Dalamud.Plugin.Services;
namespace Dalamud.Game; namespace Dalamud.Game;

View file

@ -3,7 +3,6 @@ using System.Globalization;
using Lumina.Text.ReadOnly; using Lumina.Text.ReadOnly;
using DSeString = Dalamud.Game.Text.SeStringHandling.SeString; using DSeString = Dalamud.Game.Text.SeStringHandling.SeString;
using LSeString = Lumina.Text.SeString;
namespace Dalamud.Game.Text.Evaluator; namespace Dalamud.Game.Text.Evaluator;
@ -71,9 +70,6 @@ public readonly struct SeStringParameter
public static implicit operator SeStringParameter(ReadOnlySeStringSpan value) => new(new ReadOnlySeString(value)); public static implicit operator SeStringParameter(ReadOnlySeStringSpan value) => new(new ReadOnlySeString(value));
[Obsolete("Switch to using ReadOnlySeString instead of Lumina's SeString.", true)]
public static implicit operator SeStringParameter(LSeString value) => new(new ReadOnlySeString(value.RawData));
public static implicit operator SeStringParameter(DSeString value) => new(new ReadOnlySeString(value.Encode())); public static implicit operator SeStringParameter(DSeString value) => new(new ReadOnlySeString(value.Encode()));
public static implicit operator SeStringParameter(string value) => new(value); public static implicit operator SeStringParameter(string value) => new(value);

View file

@ -60,8 +60,8 @@ internal record struct NounParams()
/// </summary> /// </summary>
public readonly int ColumnOffset => this.SheetName switch public readonly int ColumnOffset => this.SheetName switch
{ {
// See "E8 ?? ?? ?? ?? 44 8B 6B 08" // See "E8 ?? ?? ?? ?? 44 8B 66 ?? 8B E8"
nameof(LSheets.BeastTribe) => 10, nameof(LSheets.BeastTribe) => 11,
nameof(LSheets.DeepDungeonItem) => 1, nameof(LSheets.DeepDungeonItem) => 1,
nameof(LSheets.DeepDungeonEquipment) => 1, nameof(LSheets.DeepDungeonEquipment) => 1,
nameof(LSheets.DeepDungeonMagicStone) => 1, nameof(LSheets.DeepDungeonMagicStone) => 1,

View file

@ -113,14 +113,6 @@ public class SeString
/// <returns>Equivalent SeString.</returns> /// <returns>Equivalent SeString.</returns>
public static implicit operator SeString(string str) => new(new TextPayload(str)); public static implicit operator SeString(string str) => new(new TextPayload(str));
/// <summary>
/// Implicitly convert a string into a SeString containing a <see cref="TextPayload"/>.
/// </summary>
/// <param name="str">string to convert.</param>
/// <returns>Equivalent SeString.</returns>
[Obsolete("Switch to using ReadOnlySeString instead of Lumina's SeString.", true)]
public static explicit operator SeString(Lumina.Text.SeString str) => str.ToDalamudString();
/// <summary> /// <summary>
/// Parse a binary game message into an SeString. /// Parse a binary game message into an SeString.
/// </summary> /// </summary>

View file

@ -21,6 +21,7 @@ using System.Diagnostics.CodeAnalysis;
[assembly: SuppressMessage("StyleCop.CSharp.ReadabilityRules", "SA1116:SplitParametersMustStartOnLineAfterDeclaration", Justification = "Reviewed.")] [assembly: SuppressMessage("StyleCop.CSharp.ReadabilityRules", "SA1116:SplitParametersMustStartOnLineAfterDeclaration", Justification = "Reviewed.")]
[assembly: SuppressMessage("StyleCop.CSharp.MaintainabilityRules", "SA1402:FileMayOnlyContainASingleType", Justification = "This would be nice, but a big refactor")] [assembly: SuppressMessage("StyleCop.CSharp.MaintainabilityRules", "SA1402:FileMayOnlyContainASingleType", Justification = "This would be nice, but a big refactor")]
[assembly: SuppressMessage("StyleCop.CSharp.DocumentationRules", "SA1649:FileNameMustMatchTypeName", Justification = "I don't like this one so much")] [assembly: SuppressMessage("StyleCop.CSharp.DocumentationRules", "SA1649:FileNameMustMatchTypeName", Justification = "I don't like this one so much")]
[assembly: SuppressMessage("StyleCop.CSharp.ReadabilityRules", "SA1108:BlockStatementsMustNotContainEmbeddedComments", Justification = "I like having comments in blocks")]
// ImRAII stuff // ImRAII stuff
[assembly: SuppressMessage("StyleCop.CSharp.DocumentationRules", "SA1600:ElementsMustBeDocumented", Justification = "Reviewed.", Scope = "namespaceanddescendants", Target = "Dalamud.Interface.Utility.Raii")] [assembly: SuppressMessage("StyleCop.CSharp.DocumentationRules", "SA1600:ElementsMustBeDocumented", Justification = "Reviewed.", Scope = "namespaceanddescendants", Target = "Dalamud.Interface.Utility.Raii")]

View file

@ -201,19 +201,19 @@ public abstract class Hook<T> : IDalamudHook where T : Delegate
if (EnvironmentConfiguration.DalamudForceMinHook) if (EnvironmentConfiguration.DalamudForceMinHook)
useMinHook = true; useMinHook = true;
using var moduleHandle = Windows.Win32.PInvoke.GetModuleHandle(moduleName); var moduleHandle = Windows.Win32.PInvoke.GetModuleHandle(moduleName);
if (moduleHandle.IsInvalid) if (moduleHandle.IsNull)
throw new Exception($"Could not get a handle to module {moduleName}"); throw new Exception($"Could not get a handle to module {moduleName}");
var procAddress = (nint)Windows.Win32.PInvoke.GetProcAddress(moduleHandle, exportName); var procAddress = Windows.Win32.PInvoke.GetProcAddress(moduleHandle, exportName);
if (procAddress == IntPtr.Zero) if (procAddress.IsNull)
throw new Exception($"Could not get the address of {moduleName}::{exportName}"); throw new Exception($"Could not get the address of {moduleName}::{exportName}");
procAddress = HookManager.FollowJmp(procAddress); var address = HookManager.FollowJmp(procAddress.Value);
if (useMinHook) if (useMinHook)
return new MinHookHook<T>(procAddress, detour, Assembly.GetCallingAssembly()); return new MinHookHook<T>(address, detour, Assembly.GetCallingAssembly());
else else
return new ReloadedHook<T>(procAddress, detour, Assembly.GetCallingAssembly()); return new ReloadedHook<T>(address, detour, Assembly.GetCallingAssembly());
} }
/// <summary> /// <summary>

View file

@ -1,100 +0,0 @@
using System.Runtime.InteropServices;
using Reloaded.Hooks.Definitions;
namespace Dalamud.Hooking.Internal;
/// <summary>
/// This class represents a callsite hook. Only the specific address's instructions are replaced with this hook.
/// This is a destructive operation, no other callsite hooks can coexist at the same address.
///
/// There's no .Original for this hook type.
/// This is only intended for be for functions where the parameters provided allow you to invoke the original call.
///
/// This class was specifically added for hooking virtual function callsites.
/// Only the specific callsite hooked is modified, if the game calls the virtual function from other locations this hook will not be triggered.
/// </summary>
/// <typeparam name="T">Delegate signature for this hook.</typeparam>
internal class CallHook<T> : IDalamudHook where T : Delegate
{
private readonly Reloaded.Hooks.AsmHook asmHook;
private T? detour;
private bool activated;
/// <summary>
/// Initializes a new instance of the <see cref="CallHook{T}"/> class.
/// </summary>
/// <param name="address">Address of the instruction to replace.</param>
/// <param name="detour">Delegate to invoke.</param>
internal CallHook(nint address, T detour)
{
ArgumentNullException.ThrowIfNull(detour);
this.detour = detour;
this.Address = address;
var detourPtr = Marshal.GetFunctionPointerForDelegate(this.detour);
var code = new[]
{
"use64",
$"mov rax, 0x{detourPtr:X8}",
"call rax",
};
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;
/// <inheritdoc/>
public IntPtr Address { get; }
/// <inheritdoc/>
public string BackendName => "Reloaded AsmHook";
/// <inheritdoc/>
public bool IsDisposed => this.detour == null;
/// <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

@ -48,7 +48,7 @@ public abstract class Easing
/// Gets the current value of the animation, following unclamped logic. /// Gets the current value of the animation, following unclamped logic.
/// </summary> /// </summary>
[Obsolete($"This field has been deprecated. Use either {nameof(ValueClamped)} or {nameof(ValueUnclamped)} instead.", true)] [Obsolete($"This field has been deprecated. Use either {nameof(ValueClamped)} or {nameof(ValueUnclamped)} instead.", true)]
[Api13ToDo("Map this field to ValueClamped, probably.")] [Api14ToDo("Map this field to ValueClamped, probably.")]
public double Value => this.ValueUnclamped; public double Value => this.ValueUnclamped;
/// <summary> /// <summary>

File diff suppressed because it is too large Load diff

View file

@ -64,9 +64,9 @@ public interface IObjectWithLocalizableName
var result = new Dictionary<string, string>((int)count); var result = new Dictionary<string, string>((int)count);
for (var i = 0u; i < count; i++) for (var i = 0u; i < count; i++)
{ {
fn->GetLocaleName(i, (ushort*)buf, maxStrLen).ThrowOnError(); fn->GetLocaleName(i, buf, maxStrLen).ThrowOnError();
var key = new string(buf); var key = new string(buf);
fn->GetString(i, (ushort*)buf, maxStrLen).ThrowOnError(); fn->GetString(i, buf, maxStrLen).ThrowOnError();
var value = new string(buf); var value = new string(buf);
result[key.ToLowerInvariant()] = value; result[key.ToLowerInvariant()] = value;
} }

View file

@ -133,8 +133,8 @@ public sealed class SystemFontFamilyId : IFontFamilyId
var familyIndex = 0u; var familyIndex = 0u;
BOOL exists = false; BOOL exists = false;
fixed (void* pName = this.EnglishName) fixed (char* pName = this.EnglishName)
sfc.Get()->FindFamilyName((ushort*)pName, &familyIndex, &exists).ThrowOnError(); sfc.Get()->FindFamilyName(pName, &familyIndex, &exists).ThrowOnError();
if (!exists) if (!exists)
throw new FileNotFoundException($"Font \"{this.EnglishName}\" not found."); throw new FileNotFoundException($"Font \"{this.EnglishName}\" not found.");

View file

@ -113,8 +113,8 @@ public sealed class SystemFontId : IFontId
var familyIndex = 0u; var familyIndex = 0u;
BOOL exists = false; BOOL exists = false;
fixed (void* name = this.Family.EnglishName) fixed (char* name = this.Family.EnglishName)
sfc.Get()->FindFamilyName((ushort*)name, &familyIndex, &exists).ThrowOnError(); sfc.Get()->FindFamilyName(name, &familyIndex, &exists).ThrowOnError();
if (!exists) if (!exists)
throw new FileNotFoundException($"Font \"{this.Family.EnglishName}\" not found."); throw new FileNotFoundException($"Font \"{this.Family.EnglishName}\" not found.");
@ -151,7 +151,7 @@ public sealed class SystemFontId : IFontId
flocal.Get()->GetFilePathLengthFromKey(refKey, refKeySize, &pathSize).ThrowOnError(); flocal.Get()->GetFilePathLengthFromKey(refKey, refKeySize, &pathSize).ThrowOnError();
var path = stackalloc char[(int)pathSize + 1]; var path = stackalloc char[(int)pathSize + 1];
flocal.Get()->GetFilePathFromKey(refKey, refKeySize, (ushort*)path, pathSize + 1).ThrowOnError(); flocal.Get()->GetFilePathFromKey(refKey, refKeySize, path, pathSize + 1).ThrowOnError();
return (new(path, 0, (int)pathSize), (int)fface.Get()->GetIndex()); return (new(path, 0, (int)pathSize), (int)fface.Get()->GetIndex());
} }

View file

@ -104,19 +104,19 @@ internal static unsafe class ReShadePeeler
fixed (byte* pfn5 = "glBegin"u8) fixed (byte* pfn5 = "glBegin"u8)
fixed (byte* pfn6 = "vkCreateDevice"u8) fixed (byte* pfn6 = "vkCreateDevice"u8)
{ {
if (GetProcAddress((HMODULE)dosh, (sbyte*)pfn0) == 0) if (GetProcAddress((HMODULE)dosh, (sbyte*)pfn0) == null)
continue; continue;
if (GetProcAddress((HMODULE)dosh, (sbyte*)pfn1) == 0) if (GetProcAddress((HMODULE)dosh, (sbyte*)pfn1) == null)
continue; continue;
if (GetProcAddress((HMODULE)dosh, (sbyte*)pfn2) == 0) if (GetProcAddress((HMODULE)dosh, (sbyte*)pfn2) == null)
continue; continue;
if (GetProcAddress((HMODULE)dosh, (sbyte*)pfn3) == 0) if (GetProcAddress((HMODULE)dosh, (sbyte*)pfn3) == null)
continue; continue;
if (GetProcAddress((HMODULE)dosh, (sbyte*)pfn4) == 0) if (GetProcAddress((HMODULE)dosh, (sbyte*)pfn4) == null)
continue; continue;
if (GetProcAddress((HMODULE)dosh, (sbyte*)pfn5) == 0) if (GetProcAddress((HMODULE)dosh, (sbyte*)pfn5) == null)
continue; continue;
if (GetProcAddress((HMODULE)dosh, (sbyte*)pfn6) == 0) if (GetProcAddress((HMODULE)dosh, (sbyte*)pfn6) == null)
continue; continue;
} }

View file

@ -299,11 +299,12 @@ internal sealed partial class Win32InputHandler
private static void ViewportFlagsToWin32Styles(ImGuiViewportFlags flags, out int style, out int exStyle) private static void ViewportFlagsToWin32Styles(ImGuiViewportFlags flags, out int style, out int exStyle)
{ {
style = (int)(flags.HasFlag(ImGuiViewportFlags.NoDecoration) ? WS.WS_POPUP : WS.WS_OVERLAPPEDWINDOW); style = (flags & ImGuiViewportFlags.NoDecoration) != 0 ? unchecked((int)WS.WS_POPUP) : WS.WS_OVERLAPPEDWINDOW;
exStyle = exStyle = (flags & ImGuiViewportFlags.NoTaskBarIcon) != 0 ? WS.WS_EX_TOOLWINDOW : WS.WS_EX_APPWINDOW;
(int)(flags.HasFlag(ImGuiViewportFlags.NoTaskBarIcon) ? WS.WS_EX_TOOLWINDOW : (uint)WS.WS_EX_APPWINDOW);
exStyle |= WS.WS_EX_NOREDIRECTIONBITMAP; exStyle |= WS.WS_EX_NOREDIRECTIONBITMAP;
if (flags.HasFlag(ImGuiViewportFlags.TopMost)) if ((flags & ImGuiViewportFlags.TopMost) != 0)
exStyle |= WS.WS_EX_TOPMOST; exStyle |= WS.WS_EX_TOPMOST;
if ((flags & ImGuiViewportFlags.NoInputs) != 0)
exStyle |= WS.WS_EX_TRANSPARENT | WS.WS_EX_LAYERED;
} }
} }

View file

@ -7,7 +7,9 @@ using System.Runtime.InteropServices;
using System.Text; using System.Text;
using Dalamud.Bindings.ImGui; using Dalamud.Bindings.ImGui;
using Dalamud.Console;
using Dalamud.Memory; using Dalamud.Memory;
using Dalamud.Utility;
using Serilog; using Serilog;
@ -34,11 +36,14 @@ internal sealed unsafe partial class Win32InputHandler : IImGuiInputHandler
private readonly HCURSOR[] cursors; private readonly HCURSOR[] cursors;
private readonly WndProcDelegate wndProcDelegate; private readonly WndProcDelegate wndProcDelegate;
private readonly bool[] imguiMouseIsDown;
private readonly nint platformNamePtr; private readonly nint platformNamePtr;
private readonly IConsoleVariable<bool> cvLogMouseEvents;
private ViewportHandler viewportHandler; private ViewportHandler viewportHandler;
private int mouseButtonsDown;
private bool mouseTracked;
private long lastTime; private long lastTime;
private nint iniPathPtr; private nint iniPathPtr;
@ -64,7 +69,8 @@ internal sealed unsafe partial class Win32InputHandler : IImGuiInputHandler
io.BackendFlags |= ImGuiBackendFlags.HasMouseCursors | io.BackendFlags |= ImGuiBackendFlags.HasMouseCursors |
ImGuiBackendFlags.HasSetMousePos | ImGuiBackendFlags.HasSetMousePos |
ImGuiBackendFlags.RendererHasViewports | ImGuiBackendFlags.RendererHasViewports |
ImGuiBackendFlags.PlatformHasViewports; ImGuiBackendFlags.PlatformHasViewports |
ImGuiBackendFlags.HasMouseHoveredViewport;
this.platformNamePtr = Marshal.StringToHGlobalAnsi("imgui_impl_win32_c#"); this.platformNamePtr = Marshal.StringToHGlobalAnsi("imgui_impl_win32_c#");
io.Handle->BackendPlatformName = (byte*)this.platformNamePtr; io.Handle->BackendPlatformName = (byte*)this.platformNamePtr;
@ -74,8 +80,6 @@ internal sealed unsafe partial class Win32InputHandler : IImGuiInputHandler
if (io.ConfigFlags.HasFlag(ImGuiConfigFlags.ViewportsEnable)) if (io.ConfigFlags.HasFlag(ImGuiConfigFlags.ViewportsEnable))
this.viewportHandler = new(this); this.viewportHandler = new(this);
this.imguiMouseIsDown = new bool[5];
this.cursors = new HCURSOR[9]; this.cursors = new HCURSOR[9];
this.cursors[(int)ImGuiMouseCursor.Arrow] = LoadCursorW(default, IDC.IDC_ARROW); this.cursors[(int)ImGuiMouseCursor.Arrow] = LoadCursorW(default, IDC.IDC_ARROW);
this.cursors[(int)ImGuiMouseCursor.TextInput] = LoadCursorW(default, IDC.IDC_IBEAM); this.cursors[(int)ImGuiMouseCursor.TextInput] = LoadCursorW(default, IDC.IDC_IBEAM);
@ -86,6 +90,11 @@ internal sealed unsafe partial class Win32InputHandler : IImGuiInputHandler
this.cursors[(int)ImGuiMouseCursor.ResizeNwse] = LoadCursorW(default, IDC.IDC_SIZENWSE); this.cursors[(int)ImGuiMouseCursor.ResizeNwse] = LoadCursorW(default, IDC.IDC_SIZENWSE);
this.cursors[(int)ImGuiMouseCursor.Hand] = LoadCursorW(default, IDC.IDC_HAND); this.cursors[(int)ImGuiMouseCursor.Hand] = LoadCursorW(default, IDC.IDC_HAND);
this.cursors[(int)ImGuiMouseCursor.NotAllowed] = LoadCursorW(default, IDC.IDC_NO); this.cursors[(int)ImGuiMouseCursor.NotAllowed] = LoadCursorW(default, IDC.IDC_NO);
this.cvLogMouseEvents = Service<ConsoleManager>.Get().AddVariable(
"imgui.log_mouse_events",
"Log mouse events to console for debugging",
false);
} }
/// <summary> /// <summary>
@ -95,8 +104,6 @@ internal sealed unsafe partial class Win32InputHandler : IImGuiInputHandler
private delegate LRESULT WndProcDelegate(HWND hWnd, uint uMsg, WPARAM wparam, LPARAM lparam); private delegate LRESULT WndProcDelegate(HWND hWnd, uint uMsg, WPARAM wparam, LPARAM lparam);
private delegate BOOL MonitorEnumProcDelegate(HMONITOR monitor, HDC hdc, RECT* rect, LPARAM lparam);
/// <inheritdoc/> /// <inheritdoc/>
public bool UpdateCursor { get; set; } = true; public bool UpdateCursor { get; set; } = true;
@ -155,6 +162,7 @@ internal sealed unsafe partial class Win32InputHandler : IImGuiInputHandler
public void NewFrame(int targetWidth, int targetHeight) public void NewFrame(int targetWidth, int targetHeight)
{ {
var io = ImGui.GetIO(); var io = ImGui.GetIO();
var focusedWindow = GetForegroundWindow();
io.DisplaySize.X = targetWidth; io.DisplaySize.X = targetWidth;
io.DisplaySize.Y = targetHeight; io.DisplaySize.Y = targetHeight;
@ -168,9 +176,9 @@ internal sealed unsafe partial class Win32InputHandler : IImGuiInputHandler
this.viewportHandler.UpdateMonitors(); this.viewportHandler.UpdateMonitors();
this.UpdateMousePos(); this.UpdateMouseData(focusedWindow);
this.ProcessKeyEventsWorkarounds(); this.ProcessKeyEventsWorkarounds(focusedWindow);
// TODO: need to figure out some way to unify all this // TODO: need to figure out some way to unify all this
// The bottom case works(?) if the caller hooks SetCursor, but otherwise causes fps issues // The bottom case works(?) if the caller hooks SetCursor, but otherwise causes fps issues
@ -224,6 +232,40 @@ internal sealed unsafe partial class Win32InputHandler : IImGuiInputHandler
switch (msg) switch (msg)
{ {
case WM.WM_MOUSEMOVE:
{
if (!this.mouseTracked)
{
var tme = new TRACKMOUSEEVENT
{
cbSize = (uint)sizeof(TRACKMOUSEEVENT),
dwFlags = TME.TME_LEAVE,
hwndTrack = hWndCurrent,
};
this.mouseTracked = TrackMouseEvent(&tme);
}
var mousePos = new POINT(GET_X_LPARAM(lParam), GET_Y_LPARAM(lParam));
if ((io.ConfigFlags & ImGuiConfigFlags.ViewportsEnable) != 0)
ClientToScreen(hWndCurrent, &mousePos);
io.AddMousePosEvent(mousePos.x, mousePos.y);
break;
}
case WM.WM_MOUSELEAVE:
{
this.mouseTracked = false;
var mouseScreenPos = new POINT(GET_X_LPARAM(lParam), GET_Y_LPARAM(lParam));
ClientToScreen(hWndCurrent, &mouseScreenPos);
if (this.ViewportFromPoint(mouseScreenPos).IsNull)
{
var fltMax = ImGuiNative.GETFLTMAX();
io.AddMousePosEvent(-fltMax, -fltMax);
}
break;
}
case WM.WM_LBUTTONDOWN: case WM.WM_LBUTTONDOWN:
case WM.WM_LBUTTONDBLCLK: case WM.WM_LBUTTONDBLCLK:
case WM.WM_RBUTTONDOWN: case WM.WM_RBUTTONDOWN:
@ -233,14 +275,25 @@ internal sealed unsafe partial class Win32InputHandler : IImGuiInputHandler
case WM.WM_XBUTTONDOWN: case WM.WM_XBUTTONDOWN:
case WM.WM_XBUTTONDBLCLK: case WM.WM_XBUTTONDBLCLK:
{ {
if (this.cvLogMouseEvents.Value)
{
Log.Verbose(
"Handle MouseDown {Btn} WantCaptureMouse: {Want} mouseButtonsDown: {Down}",
GetButton(msg, wParam),
io.WantCaptureMouse,
this.mouseButtonsDown);
}
var button = GetButton(msg, wParam); var button = GetButton(msg, wParam);
if (io.WantCaptureMouse) if (io.WantCaptureMouse)
{ {
if (!ImGui.IsAnyMouseDown() && GetCapture() == nint.Zero) if (this.mouseButtonsDown == 0 && GetCapture() == nint.Zero)
{
SetCapture(hWndCurrent); SetCapture(hWndCurrent);
}
io.MouseDown[button] = true; this.mouseButtonsDown |= 1 << button;
this.imguiMouseIsDown[button] = true; io.AddMouseButtonEvent(button, true);
return default(LRESULT); return default(LRESULT);
} }
@ -255,14 +308,29 @@ internal sealed unsafe partial class Win32InputHandler : IImGuiInputHandler
case WM.WM_MBUTTONUP: case WM.WM_MBUTTONUP:
case WM.WM_XBUTTONUP: case WM.WM_XBUTTONUP:
{ {
var button = GetButton(msg, wParam); if (this.cvLogMouseEvents.Value)
if (io.WantCaptureMouse && this.imguiMouseIsDown[button])
{ {
if (!ImGui.IsAnyMouseDown() && GetCapture() == hWndCurrent) Log.Verbose(
ReleaseCapture(); "Handle MouseUp {Btn} WantCaptureMouse: {Want} mouseButtonsDown: {Down}",
GetButton(msg, wParam),
io.WantCaptureMouse,
this.mouseButtonsDown);
}
io.MouseDown[button] = false; var button = GetButton(msg, wParam);
this.imguiMouseIsDown[button] = false;
// Need to check if we captured the button event away from the game here, otherwise the game might get
// a down event but no up event, causing the cursor to get stuck.
// Can happen if WantCaptureMouse becomes true in between down and up
if (io.WantCaptureMouse && (this.mouseButtonsDown & (1 << button)) != 0)
{
this.mouseButtonsDown &= ~(1 << button);
if (this.mouseButtonsDown == 0 && GetCapture() == hWndCurrent)
{
ReleaseCapture();
}
io.AddMouseButtonEvent(button, false);
return default(LRESULT); return default(LRESULT);
} }
@ -272,7 +340,7 @@ internal sealed unsafe partial class Win32InputHandler : IImGuiInputHandler
case WM.WM_MOUSEWHEEL: case WM.WM_MOUSEWHEEL:
if (io.WantCaptureMouse) if (io.WantCaptureMouse)
{ {
io.MouseWheel += GET_WHEEL_DELTA_WPARAM(wParam) / (float)WHEEL_DELTA; io.AddMouseWheelEvent(0, GET_WHEEL_DELTA_WPARAM(wParam) / (float)WHEEL_DELTA);
return default(LRESULT); return default(LRESULT);
} }
@ -280,7 +348,7 @@ internal sealed unsafe partial class Win32InputHandler : IImGuiInputHandler
case WM.WM_MOUSEHWHEEL: case WM.WM_MOUSEHWHEEL:
if (io.WantCaptureMouse) if (io.WantCaptureMouse)
{ {
io.MouseWheelH += GET_WHEEL_DELTA_WPARAM(wParam) / (float)WHEEL_DELTA; io.AddMouseWheelEvent(GET_WHEEL_DELTA_WPARAM(wParam) / (float)WHEEL_DELTA, 0);
return default(LRESULT); return default(LRESULT);
} }
@ -374,68 +442,91 @@ internal sealed unsafe partial class Win32InputHandler : IImGuiInputHandler
this.viewportHandler.UpdateMonitors(); this.viewportHandler.UpdateMonitors();
break; break;
case WM.WM_KILLFOCUS when hWndCurrent == this.hWnd: case WM.WM_SETFOCUS when hWndCurrent == this.hWnd:
if (!ImGui.IsAnyMouseDown() && GetCapture() == hWndCurrent) io.AddFocusEvent(true);
ReleaseCapture(); break;
ImGui.GetIO().WantCaptureMouse = false; case WM.WM_KILLFOCUS when hWndCurrent == this.hWnd:
ImGui.ClearWindowFocus(); io.AddFocusEvent(false);
// if (!ImGui.IsAnyMouseDown() && GetCapture() == hWndCurrent)
// ReleaseCapture();
//
// ImGui.GetIO().WantCaptureMouse = false;
// ImGui.ClearWindowFocus();
break; break;
} }
return null; return null;
} }
private void UpdateMousePos() private void UpdateMouseData(HWND focusedWindow)
{ {
var io = ImGui.GetIO(); var io = ImGui.GetIO();
var pt = default(POINT);
// Depending on if Viewports are enabled, we have to change how we process var mouseScreenPos = default(POINT);
// the cursor position. If viewports are enabled, we pass the absolute cursor var hasMouseScreenPos = GetCursorPos(&mouseScreenPos) != 0;
// position to ImGui. Otherwise, we use the old method of passing client-local
// mouse position to ImGui. var isAppFocused =
if (io.ConfigFlags.HasFlag(ImGuiConfigFlags.ViewportsEnable)) focusedWindow != default
&& (focusedWindow == this.hWnd
|| IsChild(focusedWindow, this.hWnd)
|| !ImGui.FindViewportByPlatformHandle(focusedWindow).IsNull);
if (isAppFocused)
{ {
// (Optional) Set OS mouse position from Dear ImGui if requested (rarely used, only when ImGuiConfigFlags_NavEnableSetMousePos is enabled by user)
// When multi-viewports are enabled, all Dear ImGui positions are same as OS positions.
if (io.WantSetMousePos) if (io.WantSetMousePos)
{ {
SetCursorPos((int)io.MousePos.X, (int)io.MousePos.Y); var pos = new POINT((int)io.MousePos.X, (int)io.MousePos.Y);
if ((io.ConfigFlags & ImGuiConfigFlags.ViewportsEnable) != 0)
ClientToScreen(this.hWnd, &pos);
SetCursorPos(pos.x, pos.y);
}
}
// (Optional) Fallback to provide mouse position when focused (WM_MOUSEMOVE already provides this when hovered or captured)
if (!io.WantSetMousePos && !this.mouseTracked && hasMouseScreenPos)
{
// Single viewport mode: mouse position in client window coordinates (io.MousePos is (0,0) when the mouse is on the upper-left corner of the app window)
// (This is the position you can get with ::GetCursorPos() + ::ScreenToClient() or WM_MOUSEMOVE.)
// Multi-viewport mode: mouse position in OS absolute coordinates (io.MousePos is (0,0) when the mouse is on the upper-left of the primary monitor)
// (This is the position you can get with ::GetCursorPos() or WM_MOUSEMOVE + ::ClientToScreen(). In theory adding viewport->Pos to a client position would also be the same.)
var mousePos = mouseScreenPos;
if ((io.ConfigFlags & ImGuiConfigFlags.ViewportsEnable) == 0)
{
// Use game window, otherwise, positions are calculated based on the focused window which might not be the game.
// Leads to offsets.
ClientToScreen(this.hWnd, &mousePos);
} }
if (GetCursorPos(&pt)) io.AddMousePosEvent(mousePos.x, mousePos.y);
{ }
io.MousePos.X = pt.x;
io.MousePos.Y = pt.y; // (Optional) When using multiple viewports: call io.AddMouseViewportEvent() with the viewport the OS mouse cursor is hovering.
} // If ImGuiBackendFlags_HasMouseHoveredViewport is not set by the backend, Dear imGui will ignore this field and infer the information using its flawed heuristic.
else // - [X] Win32 backend correctly ignore viewports with the _NoInputs flag (here using ::WindowFromPoint with WM_NCHITTEST + HTTRANSPARENT in WndProc does that)
{ // Some backend are not able to handle that correctly. If a backend report an hovered viewport that has the _NoInputs flag (e.g. when dragging a window
io.MousePos.X = float.MinValue; // for docking, the viewport has the _NoInputs flag in order to allow us to find the viewport under), then Dear ImGui is forced to ignore the value reported
io.MousePos.Y = float.MinValue; // by the backend, and use its flawed heuristic to guess the viewport behind.
} // - [X] Win32 backend correctly reports this regardless of another viewport behind focused and dragged from (we need this to find a useful drag and drop target).
if (hasMouseScreenPos)
{
var viewport = this.ViewportFromPoint(mouseScreenPos);
io.AddMouseViewportEvent(!viewport.IsNull ? viewport.ID : 0u);
} }
else else
{ {
if (io.WantSetMousePos) io.AddMouseViewportEvent(0);
{
pt.x = (int)io.MousePos.X;
pt.y = (int)io.MousePos.Y;
ClientToScreen(this.hWnd, &pt);
SetCursorPos(pt.x, pt.y);
}
if (GetCursorPos(&pt) && ScreenToClient(this.hWnd, &pt))
{
io.MousePos.X = pt.x;
io.MousePos.Y = pt.y;
}
else
{
io.MousePos.X = float.MinValue;
io.MousePos.Y = float.MinValue;
}
} }
} }
private ImGuiViewportPtr ViewportFromPoint(POINT mouseScreenPos)
{
var hoveredHwnd = WindowFromPoint(mouseScreenPos);
return hoveredHwnd != default ? ImGui.FindViewportByPlatformHandle(hoveredHwnd) : default;
}
private bool UpdateMouseCursor() private bool UpdateMouseCursor()
{ {
var io = ImGui.GetIO(); var io = ImGui.GetIO();
@ -451,7 +542,7 @@ internal sealed unsafe partial class Win32InputHandler : IImGuiInputHandler
return true; return true;
} }
private void ProcessKeyEventsWorkarounds() private void ProcessKeyEventsWorkarounds(HWND focusedWindow)
{ {
// Left & right Shift keys: when both are pressed together, Windows tend to not generate the WM_KEYUP event for the first released one. // Left & right Shift keys: when both are pressed together, Windows tend to not generate the WM_KEYUP event for the first released one.
if (ImGui.IsKeyDown(ImGuiKey.LeftShift) && !IsVkDown(VK.VK_LSHIFT)) if (ImGui.IsKeyDown(ImGuiKey.LeftShift) && !IsVkDown(VK.VK_LSHIFT))
@ -480,7 +571,7 @@ internal sealed unsafe partial class Win32InputHandler : IImGuiInputHandler
{ {
// See: https://github.com/goatcorp/ImGuiScene/pull/13 // See: https://github.com/goatcorp/ImGuiScene/pull/13
// > GetForegroundWindow from winuser.h is a surprisingly expensive function. // > GetForegroundWindow from winuser.h is a surprisingly expensive function.
var isForeground = GetForegroundWindow() == this.hWnd; var isForeground = focusedWindow == this.hWnd;
for (var i = (int)ImGuiKey.NamedKeyBegin; i < (int)ImGuiKey.NamedKeyEnd; i++) for (var i = (int)ImGuiKey.NamedKeyBegin; i < (int)ImGuiKey.NamedKeyEnd; i++)
{ {
// Skip raising modifier keys if the game is focused. // Skip raising modifier keys if the game is focused.
@ -622,7 +713,7 @@ internal sealed unsafe partial class Win32InputHandler : IImGuiInputHandler
hbrBackground = (HBRUSH)(1 + COLOR.COLOR_BACKGROUND), hbrBackground = (HBRUSH)(1 + COLOR.COLOR_BACKGROUND),
lpfnWndProc = (delegate* unmanaged<HWND, uint, WPARAM, LPARAM, LRESULT>)Marshal lpfnWndProc = (delegate* unmanaged<HWND, uint, WPARAM, LPARAM, LRESULT>)Marshal
.GetFunctionPointerForDelegate(this.input.wndProcDelegate), .GetFunctionPointerForDelegate(this.input.wndProcDelegate),
lpszClassName = (ushort*)windowClassNamePtr, lpszClassName = windowClassNamePtr,
}; };
if (RegisterClassExW(&wcex) == 0) if (RegisterClassExW(&wcex) == 0)
@ -646,19 +737,12 @@ internal sealed unsafe partial class Win32InputHandler : IImGuiInputHandler
return; return;
var pio = ImGui.GetPlatformIO(); var pio = ImGui.GetPlatformIO();
ImGui.GetPlatformIO().Handle->Monitors.Free();
if (ImGui.GetPlatformIO().Handle->Monitors.Data != null)
{
// We allocated the platform monitor data in OnUpdateMonitors ourselves,
// so we have to free it ourselves to ImGui doesn't try to, or else it will crash
Marshal.FreeHGlobal(new IntPtr(ImGui.GetPlatformIO().Handle->Monitors.Data));
ImGui.GetPlatformIO().Handle->Monitors = default;
}
fixed (char* windowClassNamePtr = WindowClassName) fixed (char* windowClassNamePtr = WindowClassName)
{ {
UnregisterClassW( UnregisterClassW(
(ushort*)windowClassNamePtr, windowClassNamePtr,
(HINSTANCE)Marshal.GetHINSTANCE(typeof(ViewportHandler).Module)); (HINSTANCE)Marshal.GetHINSTANCE(typeof(ViewportHandler).Module));
} }
@ -693,59 +777,50 @@ internal sealed unsafe partial class Win32InputHandler : IImGuiInputHandler
// Here we use a manual ImVector overload, free the existing monitor data, // Here we use a manual ImVector overload, free the existing monitor data,
// and allocate our own, as we are responsible for telling ImGui about monitors // and allocate our own, as we are responsible for telling ImGui about monitors
var pio = ImGui.GetPlatformIO(); var pio = ImGui.GetPlatformIO();
var numMonitors = GetSystemMetrics(SM.SM_CMONITORS); pio.Handle->Monitors.Resize(0);
var data = Marshal.AllocHGlobal(Marshal.SizeOf<ImGuiPlatformMonitor>() * numMonitors);
if (pio.Handle->Monitors.Data != null)
Marshal.FreeHGlobal(new IntPtr(pio.Handle->Monitors.Data));
pio.Handle->Monitors = new(numMonitors, numMonitors, (ImGuiPlatformMonitor*)data.ToPointer());
// ImGuiPlatformIOPtr platformIO = ImGui.GetPlatformIO(); EnumDisplayMonitors(default, null, &EnumDisplayMonitorsCallback, default);
// Marshal.FreeHGlobal(platformIO.Handle->Monitors.Data);
// int numMonitors = GetSystemMetrics(SystemMetric.SM_CMONITORS);
// nint data = Marshal.AllocHGlobal(Marshal.SizeOf<ImGuiPlatformMonitor>() * numMonitors);
// platformIO.Handle->Monitors = new ImVector(numMonitors, numMonitors, data);
var monitorIndex = -1;
var enumfn = new MonitorEnumProcDelegate(
(hMonitor, _, _, _) =>
{
monitorIndex++;
var info = new MONITORINFO { cbSize = (uint)sizeof(MONITORINFO) };
if (!GetMonitorInfoW(hMonitor, &info))
return true;
var monitorLt = new Vector2(info.rcMonitor.left, info.rcMonitor.top);
var monitorRb = new Vector2(info.rcMonitor.right, info.rcMonitor.bottom);
var workLt = new Vector2(info.rcWork.left, info.rcWork.top);
var workRb = new Vector2(info.rcWork.right, info.rcWork.bottom);
// Give ImGui the info for this display
ref var imMonitor = ref ImGui.GetPlatformIO().Monitors.Ref(monitorIndex);
imMonitor.MainPos = monitorLt;
imMonitor.MainSize = monitorRb - monitorLt;
imMonitor.WorkPos = workLt;
imMonitor.WorkSize = workRb - workLt;
imMonitor.DpiScale = 1f;
return true;
});
EnumDisplayMonitors(
default,
null,
(delegate* unmanaged<HMONITOR, HDC, RECT*, LPARAM, BOOL>)Marshal.GetFunctionPointerForDelegate(enumfn),
default);
Log.Information("Monitors set up!"); Log.Information("Monitors set up!");
for (var i = 0; i < numMonitors; i++) foreach (ref var monitor in pio.Handle->Monitors)
{ {
var monitor = pio.Handle->Monitors[i];
Log.Information( Log.Information(
"Monitor {Index}: {MainPos} {MainSize} {WorkPos} {WorkSize}", "Monitor: {MainPos} {MainSize} {WorkPos} {WorkSize}",
i,
monitor.MainPos, monitor.MainPos,
monitor.MainSize, monitor.MainSize,
monitor.WorkPos, monitor.WorkPos,
monitor.WorkSize); monitor.WorkSize);
} }
return;
[UnmanagedCallersOnly]
static BOOL EnumDisplayMonitorsCallback(HMONITOR hMonitor, HDC hdc, RECT* rect, LPARAM lParam)
{
var info = new MONITORINFO { cbSize = (uint)sizeof(MONITORINFO) };
if (!GetMonitorInfoW(hMonitor, &info))
return true;
var monitorLt = new Vector2(info.rcMonitor.left, info.rcMonitor.top);
var monitorRb = new Vector2(info.rcMonitor.right, info.rcMonitor.bottom);
var workLt = new Vector2(info.rcWork.left, info.rcWork.top);
var workRb = new Vector2(info.rcWork.right, info.rcWork.bottom);
// Give ImGui the info for this display
var imMonitor = new ImGuiPlatformMonitor
{
MainPos = monitorLt,
MainSize = monitorRb - monitorLt,
WorkPos = workLt,
WorkSize = workRb - workLt,
DpiScale = 1f,
};
if ((info.dwFlags & MONITORINFOF_PRIMARY) != 0)
ImGui.GetPlatformIO().Monitors.PushFront(imMonitor);
else
ImGui.GetPlatformIO().Monitors.PushBack(imMonitor);
return true;
}
} }
[UnmanagedCallersOnly(CallConvs = [typeof(CallConvCdecl)])] [UnmanagedCallersOnly(CallConvs = [typeof(CallConvCdecl)])]
@ -781,8 +856,8 @@ internal sealed unsafe partial class Win32InputHandler : IImGuiInputHandler
{ {
data->Hwnd = CreateWindowExW( data->Hwnd = CreateWindowExW(
(uint)data->DwExStyle, (uint)data->DwExStyle,
(ushort*)windowClassNamePtr, windowClassNamePtr,
(ushort*)windowClassNamePtr, windowClassNamePtr,
(uint)data->DwStyle, (uint)data->DwStyle,
rect.left, rect.left,
rect.top, rect.top,
@ -794,6 +869,9 @@ internal sealed unsafe partial class Win32InputHandler : IImGuiInputHandler
null); null);
} }
if (data->Hwnd == 0)
Util.Fatal($"CreateWindowExW failed: {GetLastError()}", "ImGui Viewport error");
data->HwndOwned = true; data->HwndOwned = true;
viewport.PlatformRequestResize = false; viewport.PlatformRequestResize = false;
viewport.PlatformHandle = viewport.PlatformHandleRaw = data->Hwnd; viewport.PlatformHandle = viewport.PlatformHandleRaw = data->Hwnd;
@ -993,7 +1071,7 @@ internal sealed unsafe partial class Win32InputHandler : IImGuiInputHandler
{ {
var data = (ImGuiViewportDataWin32*)viewport.PlatformUserData; var data = (ImGuiViewportDataWin32*)viewport.PlatformUserData;
fixed (char* pwszTitle = MemoryHelper.ReadStringNullTerminated((nint)title)) fixed (char* pwszTitle = MemoryHelper.ReadStringNullTerminated((nint)title))
SetWindowTextW(data->Hwnd, (ushort*)pwszTitle); SetWindowTextW(data->Hwnd, pwszTitle);
} }
[UnmanagedCallersOnly(CallConvs = [typeof(CallConvCdecl)])] [UnmanagedCallersOnly(CallConvs = [typeof(CallConvCdecl)])]

View file

@ -15,10 +15,6 @@ namespace Dalamud.Interface.ImGuiSeStringRenderer.Internal;
/// <summary>Color stacks to use while evaluating a SeString.</summary> /// <summary>Color stacks to use while evaluating a SeString.</summary>
internal sealed class SeStringColorStackSet internal sealed class SeStringColorStackSet
{ {
/// <summary>Parsed <see cref="UIColor"/>, containing colors to use with <see cref="MacroCode.ColorType"/> and
/// <see cref="MacroCode.EdgeColorType"/>.</summary>
private readonly uint[,] colorTypes;
/// <summary>Foreground color stack while evaluating a SeString for rendering.</summary> /// <summary>Foreground color stack while evaluating a SeString for rendering.</summary>
/// <remarks>Touched only from the main thread.</remarks> /// <remarks>Touched only from the main thread.</remarks>
private readonly List<uint> colorStack = []; private readonly List<uint> colorStack = [];
@ -39,30 +35,38 @@ internal sealed class SeStringColorStackSet
foreach (var row in uiColor) foreach (var row in uiColor)
maxId = (int)Math.Max(row.RowId, maxId); maxId = (int)Math.Max(row.RowId, maxId);
this.colorTypes = new uint[maxId + 1, 4]; this.ColorTypes = new uint[maxId + 1, 4];
foreach (var row in uiColor) foreach (var row in uiColor)
{ {
// Contains ABGR. // Contains ABGR.
this.colorTypes[row.RowId, 0] = row.Dark; this.ColorTypes[row.RowId, 0] = row.Dark;
this.colorTypes[row.RowId, 1] = row.Light; this.ColorTypes[row.RowId, 1] = row.Light;
this.colorTypes[row.RowId, 2] = row.ClassicFF; this.ColorTypes[row.RowId, 2] = row.ClassicFF;
this.colorTypes[row.RowId, 3] = row.ClearBlue; this.ColorTypes[row.RowId, 3] = row.ClearBlue;
} }
if (BitConverter.IsLittleEndian) if (BitConverter.IsLittleEndian)
{ {
// ImGui wants RGBA in LE. // ImGui wants RGBA in LE.
fixed (uint* p = this.colorTypes) fixed (uint* p = this.ColorTypes)
{ {
foreach (ref var r in new Span<uint>(p, this.colorTypes.GetLength(0) * this.colorTypes.GetLength(1))) foreach (ref var r in new Span<uint>(p, this.ColorTypes.GetLength(0) * this.ColorTypes.GetLength(1)))
r = BinaryPrimitives.ReverseEndianness(r); r = BinaryPrimitives.ReverseEndianness(r);
} }
} }
} }
/// <summary>Initializes a new instance of the <see cref="SeStringColorStackSet"/> class.</summary>
/// <param name="colorTypes">Color types.</param>
public SeStringColorStackSet(uint[,] colorTypes) => this.ColorTypes = colorTypes;
/// <summary>Gets a value indicating whether at least one color has been pushed to the edge color stack.</summary> /// <summary>Gets a value indicating whether at least one color has been pushed to the edge color stack.</summary>
public bool HasAdditionalEdgeColor { get; private set; } public bool HasAdditionalEdgeColor { get; private set; }
/// <summary>Gets the parsed <see cref="UIColor"/> containing colors to use with <see cref="MacroCode.ColorType"/>
/// and <see cref="MacroCode.EdgeColorType"/>.</summary>
public uint[,] ColorTypes { get; }
/// <summary>Resets the colors in the stack.</summary> /// <summary>Resets the colors in the stack.</summary>
/// <param name="drawState">Draw state.</param> /// <param name="drawState">Draw state.</param>
internal void Initialize(scoped ref SeStringDrawState drawState) internal void Initialize(scoped ref SeStringDrawState drawState)
@ -191,9 +195,9 @@ internal sealed class SeStringColorStackSet
} }
// Opacity component is ignored. // Opacity component is ignored.
var color = themeIndex >= 0 && themeIndex < this.colorTypes.GetLength(1) && var color = themeIndex >= 0 && themeIndex < this.ColorTypes.GetLength(1) &&
colorTypeIndex < this.colorTypes.GetLength(0) colorTypeIndex < this.ColorTypes.GetLength(0)
? this.colorTypes[colorTypeIndex, themeIndex] ? this.ColorTypes[colorTypeIndex, themeIndex]
: 0u; : 0u;
rgbaStack.Add(color | 0xFF000000u); rgbaStack.Add(color | 0xFF000000u);

View file

@ -1,5 +1,6 @@
using System.Collections.Generic; using System.Collections.Generic;
using System.Numerics; using System.Numerics;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices; using System.Runtime.InteropServices;
using System.Text; using System.Text;
@ -25,7 +26,7 @@ namespace Dalamud.Interface.ImGuiSeStringRenderer.Internal;
/// <summary>Draws SeString.</summary> /// <summary>Draws SeString.</summary>
[ServiceManager.EarlyLoadedService] [ServiceManager.EarlyLoadedService]
internal unsafe class SeStringRenderer : IInternalDisposableService internal class SeStringRenderer : IServiceType
{ {
private const int ImGuiContextCurrentWindowOffset = 0x3FF0; private const int ImGuiContextCurrentWindowOffset = 0x3FF0;
private const int ImGuiWindowDcOffset = 0x118; private const int ImGuiWindowDcOffset = 0x118;
@ -47,28 +48,19 @@ internal unsafe class SeStringRenderer : IInternalDisposableService
/// <summary>Parsed text fragments from a SeString.</summary> /// <summary>Parsed text fragments from a SeString.</summary>
/// <remarks>Touched only from the main thread.</remarks> /// <remarks>Touched only from the main thread.</remarks>
private readonly List<TextFragment> fragments = []; private readonly List<TextFragment> fragmentsMainThread = [];
/// <summary>Color stacks to use while evaluating a SeString for rendering.</summary> /// <summary>Color stacks to use while evaluating a SeString for rendering.</summary>
/// <remarks>Touched only from the main thread.</remarks> /// <remarks>Touched only from the main thread.</remarks>
private readonly SeStringColorStackSet colorStackSet; private readonly SeStringColorStackSet colorStackSetMainThread;
/// <summary>Splits a draw list so that different layers of a single glyph can be drawn out of order.</summary>
private ImDrawListSplitter* splitter = ImGui.ImDrawListSplitter();
[ServiceManager.ServiceConstructor] [ServiceManager.ServiceConstructor]
private SeStringRenderer(DataManager dm, TargetSigScanner sigScanner) private SeStringRenderer(DataManager dm, TargetSigScanner sigScanner)
{ {
this.colorStackSet = new(dm.Excel.GetSheet<UIColor>()); this.colorStackSetMainThread = new(dm.Excel.GetSheet<UIColor>());
this.gfd = dm.GetFile<GfdFile>("common/font/gfdata.gfd")!; this.gfd = dm.GetFile<GfdFile>("common/font/gfdata.gfd")!;
} }
/// <summary>Finalizes an instance of the <see cref="SeStringRenderer"/> class.</summary>
~SeStringRenderer() => this.ReleaseUnmanagedResources();
/// <inheritdoc/>
void IInternalDisposableService.DisposeService() => this.ReleaseUnmanagedResources();
/// <summary>Compiles and caches a SeString from a text macro representation.</summary> /// <summary>Compiles and caches a SeString from a text macro representation.</summary>
/// <param name="text">SeString text macro representation. /// <param name="text">SeString text macro representation.
/// Newline characters will be normalized to newline payloads.</param> /// Newline characters will be normalized to newline payloads.</param>
@ -80,6 +72,44 @@ internal unsafe class SeStringRenderer : IInternalDisposableService
text.ReplaceLineEndings("<br>"), text.ReplaceLineEndings("<br>"),
new() { ExceptionMode = MacroStringParseExceptionMode.EmbedError })); new() { ExceptionMode = MacroStringParseExceptionMode.EmbedError }));
/// <summary>Creates a draw data that will draw the given SeString onto it.</summary>
/// <param name="sss">SeString to render.</param>
/// <param name="drawParams">Parameters for drawing.</param>
/// <returns>A new self-contained draw data.</returns>
public unsafe BufferBackedImDrawData CreateDrawData(
ReadOnlySeStringSpan sss,
scoped in SeStringDrawParams drawParams = default)
{
if (drawParams.TargetDrawList is not null)
{
throw new ArgumentException(
$"{nameof(SeStringDrawParams.TargetDrawList)} may not be specified.",
nameof(drawParams));
}
var dd = BufferBackedImDrawData.Create();
try
{
var size = this.Draw(sss, drawParams with { TargetDrawList = dd.ListPtr }).Size;
var offset = drawParams.ScreenOffset ?? Vector2.Zero;
foreach (var vtx in new Span<ImDrawVert>(dd.ListPtr.VtxBuffer.Data, dd.ListPtr.VtxBuffer.Size))
offset = Vector2.Min(offset, vtx.Pos);
dd.Data.DisplayPos = offset;
dd.Data.DisplaySize = size - offset;
dd.Data.Valid = 1;
dd.UpdateDrawDataStatistics();
return dd;
}
catch
{
dd.Dispose();
throw;
}
}
/// <summary>Compiles and caches a SeString from a text macro representation, and then draws it.</summary> /// <summary>Compiles and caches a SeString from a text macro representation, and then draws it.</summary>
/// <param name="text">SeString text macro representation. /// <param name="text">SeString text macro representation.
/// Newline characters will be normalized to newline payloads.</param> /// Newline characters will be normalized to newline payloads.</param>
@ -113,28 +143,43 @@ internal unsafe class SeStringRenderer : IInternalDisposableService
/// <param name="imGuiId">ImGui ID, if link functionality is desired.</param> /// <param name="imGuiId">ImGui ID, if link functionality is desired.</param>
/// <param name="buttonFlags">Button flags to use on link interaction.</param> /// <param name="buttonFlags">Button flags to use on link interaction.</param>
/// <returns>Interaction result of the rendered text.</returns> /// <returns>Interaction result of the rendered text.</returns>
public SeStringDrawResult Draw( public unsafe SeStringDrawResult Draw(
ReadOnlySeStringSpan sss, ReadOnlySeStringSpan sss,
scoped in SeStringDrawParams drawParams = default, scoped in SeStringDrawParams drawParams = default,
ImGuiId imGuiId = default, ImGuiId imGuiId = default,
ImGuiButtonFlags buttonFlags = ImGuiButtonFlags.MouseButtonDefault) ImGuiButtonFlags buttonFlags = ImGuiButtonFlags.MouseButtonDefault)
{ {
// Drawing is only valid if done from the main thread anyway, especially with interactivity. // Interactivity is supported only from the main thread.
ThreadSafety.AssertMainThread(); if (!imGuiId.IsEmpty())
ThreadSafety.AssertMainThread();
if (drawParams.TargetDrawList is not null && imGuiId) if (drawParams.TargetDrawList is not null && imGuiId)
throw new ArgumentException("ImGuiId cannot be set if TargetDrawList is manually set.", nameof(imGuiId)); throw new ArgumentException("ImGuiId cannot be set if TargetDrawList is manually set.", nameof(imGuiId));
// This also does argument validation for drawParams. Do it here. using var cleanup = new DisposeSafety.ScopedFinalizer();
var state = new SeStringDrawState(sss, drawParams, this.colorStackSet, this.splitter);
// Reset and initialize the state. ImFont* font = null;
this.fragments.Clear(); if (drawParams.Font.HasValue)
this.colorStackSet.Initialize(ref state); font = drawParams.Font.Value;
if (ThreadSafety.IsMainThread && drawParams.TargetDrawList is null && font is null)
font = ImGui.GetFont();
if (font is null)
throw new ArgumentException("Specified font is empty.");
// This also does argument validation for drawParams. Do it here.
// `using var` makes a struct read-only, but we do want to modify it.
using var stateStorage = new SeStringDrawState(
sss,
drawParams,
ThreadSafety.IsMainThread ? this.colorStackSetMainThread : new(this.colorStackSetMainThread.ColorTypes),
ThreadSafety.IsMainThread ? this.fragmentsMainThread : [],
font);
ref var state = ref Unsafe.AsRef(in stateStorage);
// Analyze the provided SeString and break it up to text fragments. // Analyze the provided SeString and break it up to text fragments.
this.CreateTextFragments(ref state); this.CreateTextFragments(ref state);
var fragmentSpan = CollectionsMarshal.AsSpan(this.fragments); var fragmentSpan = CollectionsMarshal.AsSpan(state.Fragments);
// Calculate size. // Calculate size.
var size = Vector2.Zero; var size = Vector2.Zero;
@ -147,24 +192,17 @@ internal unsafe class SeStringRenderer : IInternalDisposableService
state.SplitDrawList(); state.SplitDrawList();
// Handle cases where ImGui.AlignTextToFramePadding has been called.
var context = ImGui.GetCurrentContext();
var currLineTextBaseOffset = 0f;
if (!context.IsNull)
{
var currentWindow = context.CurrentWindow;
if (!currentWindow.IsNull)
{
currLineTextBaseOffset = currentWindow.DC.CurrLineTextBaseOffset;
}
}
var itemSize = size; var itemSize = size;
if (currLineTextBaseOffset != 0f) if (drawParams.TargetDrawList is null)
{ {
itemSize.Y += 2 * currLineTextBaseOffset; // Handle cases where ImGui.AlignTextToFramePadding has been called.
foreach (ref var f in fragmentSpan) var currLineTextBaseOffset = ImGui.GetCurrentContext().CurrentWindow.DC.CurrLineTextBaseOffset;
f.Offset += new Vector2(0, currLineTextBaseOffset); if (currLineTextBaseOffset != 0f)
{
itemSize.Y += 2 * currLineTextBaseOffset;
foreach (ref var f in fragmentSpan)
f.Offset += new Vector2(0, currLineTextBaseOffset);
}
} }
// Draw all text fragments. // Draw all text fragments.
@ -280,15 +318,6 @@ internal unsafe class SeStringRenderer : IInternalDisposableService
return displayRune.Value != 0; return displayRune.Value != 0;
} }
private void ReleaseUnmanagedResources()
{
if (this.splitter is not null)
{
this.splitter->Destroy();
this.splitter = null;
}
}
/// <summary>Creates text fragment, taking line and word breaking into account.</summary> /// <summary>Creates text fragment, taking line and word breaking into account.</summary>
/// <param name="state">Draw state.</param> /// <param name="state">Draw state.</param>
private void CreateTextFragments(ref SeStringDrawState state) private void CreateTextFragments(ref SeStringDrawState state)
@ -391,7 +420,7 @@ internal unsafe class SeStringRenderer : IInternalDisposableService
var overflows = Math.Max(w, xy.X + fragment.VisibleWidth) > state.WrapWidth; var overflows = Math.Max(w, xy.X + fragment.VisibleWidth) > state.WrapWidth;
// Test if the fragment does not fit into the current line and the current line is not empty. // Test if the fragment does not fit into the current line and the current line is not empty.
if (xy.X != 0 && this.fragments.Count > 0 && !this.fragments[^1].BreakAfter && overflows) if (xy.X != 0 && state.Fragments.Count > 0 && !state.Fragments[^1].BreakAfter && overflows)
{ {
// Introduce break if this is the first time testing the current break unit or the current fragment // Introduce break if this is the first time testing the current break unit or the current fragment
// is an entity. // is an entity.
@ -401,7 +430,7 @@ internal unsafe class SeStringRenderer : IInternalDisposableService
xy.X = 0; xy.X = 0;
xy.Y += state.LineHeight; xy.Y += state.LineHeight;
w = 0; w = 0;
CollectionsMarshal.AsSpan(this.fragments)[^1].BreakAfter = true; CollectionsMarshal.AsSpan(state.Fragments)[^1].BreakAfter = true;
fragment.Offset = xy; fragment.Offset = xy;
// Now that the fragment is given its own line, test if it overflows again. // Now that the fragment is given its own line, test if it overflows again.
@ -419,16 +448,16 @@ internal unsafe class SeStringRenderer : IInternalDisposableService
fragment = this.CreateFragment(state, prev, curr, true, xy, link, entity, remainingWidth); fragment = this.CreateFragment(state, prev, curr, true, xy, link, entity, remainingWidth);
} }
} }
else if (this.fragments.Count > 0 && xy.X != 0) else if (state.Fragments.Count > 0 && xy.X != 0)
{ {
// New fragment fits into the current line, and it has a previous fragment in the same line. // New fragment fits into the current line, and it has a previous fragment in the same line.
// If the previous fragment ends with a soft hyphen, adjust its width so that the width of its // If the previous fragment ends with a soft hyphen, adjust its width so that the width of its
// trailing soft hyphens are not considered. // trailing soft hyphens are not considered.
if (this.fragments[^1].EndsWithSoftHyphen) if (state.Fragments[^1].EndsWithSoftHyphen)
xy.X += this.fragments[^1].AdvanceWidthWithoutSoftHyphen - this.fragments[^1].AdvanceWidth; xy.X += state.Fragments[^1].AdvanceWidthWithoutSoftHyphen - state.Fragments[^1].AdvanceWidth;
// Adjust this fragment's offset from kerning distance. // Adjust this fragment's offset from kerning distance.
xy.X += state.CalculateScaledDistance(this.fragments[^1].LastRune, fragment.FirstRune); xy.X += state.CalculateScaledDistance(state.Fragments[^1].LastRune, fragment.FirstRune);
fragment.Offset = xy; fragment.Offset = xy;
} }
@ -439,7 +468,7 @@ internal unsafe class SeStringRenderer : IInternalDisposableService
w = Math.Max(w, xy.X + fragment.VisibleWidth); w = Math.Max(w, xy.X + fragment.VisibleWidth);
xy.X += fragment.AdvanceWidth; xy.X += fragment.AdvanceWidth;
prev = fragment.To; prev = fragment.To;
this.fragments.Add(fragment); state.Fragments.Add(fragment);
if (fragment.BreakAfter) if (fragment.BreakAfter)
{ {
@ -491,7 +520,7 @@ internal unsafe class SeStringRenderer : IInternalDisposableService
if (gfdTextureSrv != 0) if (gfdTextureSrv != 0)
{ {
state.Draw( state.Draw(
new ImTextureID(gfdTextureSrv), new(gfdTextureSrv),
offset + new Vector2(x, MathF.Round((state.LineHeight - size.Y) / 2)), offset + new Vector2(x, MathF.Round((state.LineHeight - size.Y) / 2)),
size, size,
useHq ? gfdEntry.HqUv0 : gfdEntry.Uv0, useHq ? gfdEntry.HqUv0 : gfdEntry.Uv0,
@ -528,7 +557,7 @@ internal unsafe class SeStringRenderer : IInternalDisposableService
return; return;
static nint GetGfdTextureSrv() static unsafe nint GetGfdTextureSrv()
{ {
var uim = UIModule.Instance(); var uim = UIModule.Instance();
if (uim is null) if (uim is null)
@ -553,7 +582,7 @@ internal unsafe class SeStringRenderer : IInternalDisposableService
/// <summary>Determines a bitmap icon to display for the given SeString payload.</summary> /// <summary>Determines a bitmap icon to display for the given SeString payload.</summary>
/// <param name="sss">Byte span that should include a SeString payload.</param> /// <param name="sss">Byte span that should include a SeString payload.</param>
/// <returns>Icon to display, or <see cref="None"/> if it should not be displayed as an icon.</returns> /// <returns>Icon to display, or <see cref="None"/> if it should not be displayed as an icon.</returns>
private BitmapFontIcon GetBitmapFontIconFor(ReadOnlySpan<byte> sss) private unsafe BitmapFontIcon GetBitmapFontIconFor(ReadOnlySpan<byte> sss)
{ {
var e = new ReadOnlySeStringSpan(sss).GetEnumerator(); var e = new ReadOnlySeStringSpan(sss).GetEnumerator();
if (!e.MoveNext() || e.Current.MacroCode is not MacroCode.Icon and not MacroCode.Icon2) if (!e.MoveNext() || e.Current.MacroCode is not MacroCode.Icon and not MacroCode.Icon2)
@ -710,38 +739,4 @@ internal unsafe class SeStringRenderer : IInternalDisposableService
firstDisplayRune ?? default, firstDisplayRune ?? default,
lastNonSoftHyphenRune); lastNonSoftHyphenRune);
} }
/// <summary>Represents a text fragment in a SeString span.</summary>
/// <param name="From">Starting byte offset (inclusive) in a SeString.</param>
/// <param name="To">Ending byte offset (exclusive) in a SeString.</param>
/// <param name="Link">Byte offset of the link that decorates this text fragment, or <c>-1</c> if none.</param>
/// <param name="Offset">Offset in pixels w.r.t. <see cref="SeStringDrawParams.ScreenOffset"/>.</param>
/// <param name="Entity">Replacement entity, if any.</param>
/// <param name="VisibleWidth">Visible width of this text fragment. This is the width required to draw everything
/// without clipping.</param>
/// <param name="AdvanceWidth">Advance width of this text fragment. This is the width required to add to the cursor
/// to position the next fragment correctly.</param>
/// <param name="AdvanceWidthWithoutSoftHyphen">Same with <paramref name="AdvanceWidth"/>, but trimming all the
/// trailing soft hyphens.</param>
/// <param name="BreakAfter">Whether to insert a line break after this text fragment.</param>
/// <param name="EndsWithSoftHyphen">Whether this text fragment ends with one or more soft hyphens.</param>
/// <param name="FirstRune">First rune in this text fragment.</param>
/// <param name="LastRune">Last rune in this text fragment, for the purpose of calculating kerning distance with
/// the following text fragment in the same line, if any.</param>
private record struct TextFragment(
int From,
int To,
int Link,
Vector2 Offset,
SeStringReplacementEntity Entity,
float VisibleWidth,
float AdvanceWidth,
float AdvanceWidthWithoutSoftHyphen,
bool BreakAfter,
bool EndsWithSoftHyphen,
Rune FirstRune,
Rune LastRune)
{
public bool IsSoftHyphenVisible => this.EndsWithSoftHyphen && this.BreakAfter;
}
} }

View file

@ -0,0 +1,39 @@
using System.Numerics;
using System.Text;
namespace Dalamud.Interface.ImGuiSeStringRenderer.Internal;
/// <summary>Represents a text fragment in a SeString span.</summary>
/// <param name="From">Starting byte offset (inclusive) in a SeString.</param>
/// <param name="To">Ending byte offset (exclusive) in a SeString.</param>
/// <param name="Link">Byte offset of the link that decorates this text fragment, or <c>-1</c> if none.</param>
/// <param name="Offset">Offset in pixels w.r.t. <see cref="SeStringDrawParams.ScreenOffset"/>.</param>
/// <param name="Entity">Replacement entity, if any.</param>
/// <param name="VisibleWidth">Visible width of this text fragment. This is the width required to draw everything
/// without clipping.</param>
/// <param name="AdvanceWidth">Advance width of this text fragment. This is the width required to add to the cursor
/// to position the next fragment correctly.</param>
/// <param name="AdvanceWidthWithoutSoftHyphen">Same with <paramref name="AdvanceWidth"/>, but trimming all the
/// trailing soft hyphens.</param>
/// <param name="BreakAfter">Whether to insert a line break after this text fragment.</param>
/// <param name="EndsWithSoftHyphen">Whether this text fragment ends with one or more soft hyphens.</param>
/// <param name="FirstRune">First rune in this text fragment.</param>
/// <param name="LastRune">Last rune in this text fragment, for the purpose of calculating kerning distance with
/// the following text fragment in the same line, if any.</param>
internal record struct TextFragment(
int From,
int To,
int Link,
Vector2 Offset,
SeStringReplacementEntity Entity,
float VisibleWidth,
float AdvanceWidth,
float AdvanceWidthWithoutSoftHyphen,
bool BreakAfter,
bool EndsWithSoftHyphen,
Rune FirstRune,
Rune LastRune)
{
/// <summary>Gets a value indicating whether the fragment ends with a visible soft hyphen.</summary>
public bool IsSoftHyphenVisible => this.EndsWithSoftHyphen && this.BreakAfter;
}

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