Compare commits

..

185 Commits

Author SHA1 Message Date
defnotken
8eb6700017 reflecting latest 2.0.0 changes
All checks were successful
Tag and Release Lightless / tag-and-release (push) Successful in 2m16s
2025-12-20 23:55:57 -06:00
defnotken
1bfb67a5c6 Merge branch '2.0.0' into dev 2025-12-20 23:54:55 -06:00
cake
1c4c73327f Added online filter like visible, seperated them for now. need to refactor. 2025-12-21 02:50:03 +01:00
cake
7b74fa7c4e Attempt to have a minute grace whenever collection get removed. 2025-12-21 01:55:26 +01:00
cake
2a670b3e64 Merge branch '2.0.0' of https://git.lightless-sync.org/Lightless-Sync/LightlessClient into 2.0.0 2025-12-21 01:20:43 +01:00
cake
f225989a00 Trunculate the broadcaster name in the grid view as it was overlapping into the Shell name 2025-12-21 01:20:34 +01:00
f2b17120fa implement focus fade for chat 2025-12-21 09:00:34 +09:00
79539e3db8 moderators will love this one 2025-12-21 07:55:41 +09:00
bcf6aea89d Merge branch '2.0.0' of https://git.lightless-sync.org/Lightless-Sync/LightlessClient into 2.0.0 2025-12-21 07:27:17 +09:00
779ff06981 goodbye lag 2025-12-21 07:26:37 +09:00
defnotken
54530cb16d fixing a typo for help message 2025-12-20 15:23:36 -06:00
defnotken
6db9925693 Dev be building
All checks were successful
Tag and Release Lightless / tag-and-release (push) Successful in 2m27s
2025-12-20 13:45:34 -06:00
defnotken
0ba324bfe5 Merge branch '2.0.0' into dev 2025-12-20 13:43:35 -06:00
03105e0755 fix log level 2025-12-21 04:28:36 +09:00
b99f68a891 collapsible texture details 2025-12-21 02:23:18 +09:00
7c7a98f770 This looks better 2025-12-21 01:19:17 +09:00
ab369d008e can drag chat tabs around as much as u want
syncshell tabs can use notes instead by rightclicking and prefering it
added some visibility settings (hide in combat, etc)
and cleaned up some of the ui
2025-12-21 01:17:00 +09:00
defnotken
f4665a0909 Brio fix
All checks were successful
Tag and Release Lightless / tag-and-release (push) Successful in 2m20s
2025-12-20 00:41:10 -06:00
defnotken
cb4bcec5e9 Merge branch '2.0.0' into dev 2025-12-20 00:40:31 -06:00
Minmoose
e5fa477eee Fix Brio IPC 2025-12-19 19:06:34 -06:00
defnotken
af607e4380 fixes to plugin.
All checks were successful
Tag and Release Lightless / tag-and-release (push) Successful in 2m26s
2025-12-19 15:36:54 -06:00
cake
ac8270e4ad Added chat command in handler 2025-12-19 22:34:04 +01:00
defnotken
29e6555480 Merge branch '2.0.0' into dev 2025-12-19 15:31:20 -06:00
defnotken
9402731b2b Merge branch '2.0.0' into dev 2025-12-19 15:30:44 -06:00
cake
4d0bf2d57e Updated Brio SDK 2025-12-19 22:29:37 +01:00
7f74f88302 Merge branch '2.0.0' of https://git.lightless-sync.org/Lightless-Sync/LightlessClient into 2.0.0 2025-12-20 04:01:20 +09:00
cake
934cdfbcf0 updated nuget packages 2025-12-19 19:57:56 +01:00
cake
d2a68e6533 Disabled sort on payload on group submit 2025-12-19 19:49:30 +01:00
20008f904d fix send button and improve input focus 2025-12-20 03:39:28 +09:00
cake
54b50886c0 Fixed UID scaling on fontsize 2025-12-19 19:38:43 +01:00
cake
234fe5d360 Fixed font size issue on player names. 2025-12-19 19:20:41 +01:00
defnotken
05770d9a5b update workflow 2025-12-19 10:25:29 -06:00
defnotken
f43fb28257 Hello version?
All checks were successful
Tag and Release Lightless / tag-and-release (push) Successful in 2m22s
2025-12-19 10:17:35 -06:00
defnotken
465da1bdf2 pre 2.0 test
All checks were successful
Tag and Release Lightless / tag-and-release (push) Successful in 2m17s
2025-12-19 10:06:00 -06:00
defnotken
0a592c87dd Maybe?
All checks were successful
Tag and Release Lightless / tag-and-release (push) Successful in 2m20s
2025-12-19 09:57:44 -06:00
defnotken
321a9c8b55 lets try this checkout
Some checks failed
Tag and Release Lightless / tag-and-release (push) Failing after 1m7s
2025-12-19 09:54:54 -06:00
defnotken
4aa09ce05e Merge branch '2.0.0' into dev
Some checks failed
Tag and Release Lightless / tag-and-release (push) Failing after 1m4s
2025-12-19 09:43:26 -06:00
defnotken
68dc8aef2f sdk update 2025-12-19 09:42:17 -06:00
defnotken
56143c5f3d bump api + sdk 2025-12-19 09:32:01 -06:00
defnotken
fea633b6f6 Lets test build dev.
Some checks failed
Tag and Release Lightless / tag-and-release (push) Failing after 1m51s
2025-12-19 09:18:37 -06:00
91739536bf Merge pull request 'patch-notes-fixes' (#98) from notif-style-rework into 2.0.0
Reviewed-on: #98
Reviewed-by: defnotken <defnotken@noreply.git.lightless-sync.org>
2025-12-19 15:09:10 +00:00
choco
ae9df103f3 updated bullet points to wrap to the next line when it reaches the edge of the content area. 2025-12-19 15:55:20 +01:00
choco
bade5ab6f5 Merge branch 'refs/heads/2.0.0' into notif-style-rework 2025-12-19 15:48:14 +01:00
5e22f3bff0 Merge pull request 'Update refs and workflow' (#97) from update-refs-and-workflow into 2.0.0
Reviewed-on: #97
2025-12-19 14:14:48 +00:00
defnotken
ed099f322d Update refs and workflow 2025-12-19 08:12:18 -06:00
cake
116e65b220 Updated submodule. updated sdk to 14.0.1 2025-12-19 07:22:03 +01:00
cake
ee175efe41 Fixed some warnings, bumped api 2025-12-19 00:15:42 +01:00
Minmoose
6ca491ac30 Update for Brio API v3 2025-12-18 16:03:35 -06:00
4ffc2247b2 adjust visibility flags, improve chat functionality, bump submodules 2025-12-18 20:49:38 +09:00
7b4e42c487 Merge pull request 'Fixing Sestrings' (#96) from Sestrings-fix-2 into 2.0.0
Reviewed-on: #96
2025-12-18 05:21:27 +00:00
defnotken
5e2afc8bfe Fixed Sestring font + offset 2025-12-17 23:19:18 -06:00
6d57813ef2 Merge pull request 'sestring-font-fix' (#95) from sestring-font-fix into 2.0.0
Reviewed-on: #95
2025-12-18 05:14:02 +00:00
defnotken
8b75063b9d packagejson shenanigans 2025-12-17 23:07:28 -06:00
defnotken
99b49762bb why 2025-12-17 23:06:07 -06:00
defnotken
35e35591f5 Fix sestrings 2025-12-17 23:03:49 -06:00
defnotken
e3c04e31e7 clean up 2025-12-17 20:40:24 -06:00
defnotken
f7fb609c71 clean up 2025-12-17 20:38:15 -06:00
cake
d766c2c42e Added offset, font and fontsize as needed for 14 2025-12-18 01:19:26 +01:00
1d212437f5 API14 2025-12-18 04:46:44 +09:00
choco
9d1d6783ce Merge branch '2.0.0' into notif-style-rework 2025-12-17 09:46:57 +01:00
f47df8fac2 Merge branch '2.0.0' of https://git.lightless-sync.org/Lightless-Sync/LightlessClient into 2.0.0 2025-12-17 17:19:46 +09:00
ecc1e7107f bump submodule 2025-12-17 17:18:53 +09:00
1cc8339307 Merge pull request 'Update changelog.' (#94) from clean-up-and-patch-notes-2.0.0 into 2.0.0
Reviewed-on: #94
Reviewed-by: cake <cake@noreply.git.lightless-sync.org>
2025-12-16 18:22:47 +00:00
defnotken
6522b586d5 Update changelog. 2025-12-16 11:18:54 -06:00
choco
8b9e35283d reworked the animated star banner into a reusable component for reusability 2025-12-16 11:49:56 +01:00
choco
755bae1294 matching the notifications to the new styling 2025-12-16 11:35:52 +01:00
cake
a41f419076 Reduced message size and reason length of report 2025-12-16 07:02:35 +01:00
cake
dec6c4900b bumped version in project 2025-12-16 02:01:30 +01:00
cake
5dabd23d93 Fixed null exception on CID 2025-12-16 01:57:07 +01:00
cake
0dd520d926 Fixed height issue of download box 2025-12-16 01:41:02 +01:00
cake
4e4d19ad00 Removed own broadcast from list, count fixed as well 2025-12-16 00:04:57 +01:00
cake
d5c11cd22f Added tags in the shell finder, button is red when not joinable. Fixed some null errors. 2025-12-15 23:52:06 +01:00
4444a88746 watafak 2025-12-16 06:31:29 +09:00
cake
bdfcf254a8 Added copy text in tooltip of uid 2025-12-15 20:22:31 +01:00
cake
eb11ff0b4c Fix some issues in syncshell admin panel, removed flag row and added it in user name. updated banned list to remove from imgui table. 2025-12-15 20:15:54 +01:00
cake
ee1fcb5661 Fixed certain scenario that could break the event viewer 2025-12-15 19:37:22 +01:00
cake
44e91bef8f Cleaning of registry, fixed typo in object kind as it should be companion in the companion kind. 2025-12-14 04:37:33 +01:00
cake
6891424b0d Fixed that notifcations prevents click around it. 2025-12-14 00:49:39 +01:00
cake
6395b1eb52 Removal of use notifcation for downloads as its not used at all 2025-12-12 06:06:50 +01:00
0671c46e5d more tags meow 2025-12-11 16:31:44 +09:00
cake
09b78e1896 Fixed push color in main tab as well. 2025-12-11 05:41:53 +01:00
cake
1b2db4c698 Fixed pushes to imgui styles so its contained to only admin panel. 2025-12-11 05:38:35 +01:00
6cf0e3daed various 'improvements' 2025-12-11 12:59:32 +09:00
cake
2e14fc2f8f Cleaned up services context 2025-12-09 07:30:52 +01:00
cake
675918624d Redone syncshell admin ui, fixed some bugs on edit profile. 2025-12-09 05:45:19 +01:00
cake
25f0d41581 Fixed spelling 2025-12-06 05:41:14 +01:00
cake
1cb326070b Added option to show green eye in pair list. 2025-12-06 05:35:27 +01:00
cake
b444782b76 Fixed plugin, added 0 zero (15 minutes) option for pruning. 2025-12-05 13:36:12 +01:00
cake
feec5e8ff3 Pushed Imgui plate handler for lightfinder. need to redo options of it. 2025-12-05 04:48:55 +01:00
cake
cc1f381687 Merge branch '2.0.0' of https://git.lightless-sync.org/Lightless-Sync/LightlessClient into 2.0.0 2025-12-05 04:44:10 +01:00
cake
69b504c42f Added the old system of WQPD back. added more options of it. 2025-12-05 04:43:30 +01:00
541d17132d performance cache + queued character data application 2025-12-05 10:49:30 +09:00
1c36db97dc show focus target on visibility hover 2025-12-03 13:59:30 +09:00
cake
c335489cee Fixed null errors 2025-12-02 18:41:10 +01:00
cake
6734021b89 GroupFullinfo be nullable 2025-12-02 18:20:59 +01:00
cake
46a8fc72cb Changed profile opening to use GroupData instead of full info, Added opening of syncshell profile from finder. 2025-12-02 18:19:15 +01:00
cake
962567fbfe Merge branch '2.0.0' of https://git.lightless-sync.org/Lightless-Sync/LightlessClient into 2.0.0 2025-12-02 07:03:48 +01:00
cake
0e076f6290 Forced another sha1 2025-12-02 07:01:25 +01:00
cake
72cd5006db Force SHA1 hashing on updated hash files 2025-12-02 06:33:08 +01:00
023ca2013e biggest fix ever 2025-12-02 13:39:08 +09:00
a77261a096 mid settings improvement attempt 2025-12-02 08:44:34 +09:00
febc47442a goober fix tooltips 2025-12-01 03:12:00 +09:00
cake
481bc99dcd Merge branch '2.0.0' of https://git.lightless-sync.org/Lightless-Sync/LightlessClient into 2.0.0 2025-11-30 15:55:57 +01:00
cake
9d6a0a1257 Fixed colors so it uses dalamud or lightless, added notifications system back 2025-11-30 15:55:32 +01:00
8076d63ce2 add chat message scaling 2025-11-30 21:35:17 +09:00
ba5c8b588e meow timestamp meow 2025-11-30 20:32:22 +09:00
91393bf4a1 added chat report functionality and some other random stuff 2025-11-30 19:59:37 +09:00
cake
e0e2304253 Removed usings 2025-11-30 08:10:15 +01:00
cake
a9181d2592 Removed obselete functions, changed download bars a bit. renamed files correctly 2025-11-30 08:09:58 +01:00
cake
cab13874d8 Allow moderators to use shell broadcasting, distinct shell finder to remove duplicate shells 2025-11-30 01:26:18 +01:00
cake
04cd09cbb9 Fixed seperator 2025-11-30 00:18:36 +01:00
cake
0b36c1bdc2 Changed syncshell admin user list, added filter and copy. show creation date while hoving over text. 2025-11-30 00:00:39 +01:00
cake
1e88fe0cf3 Fixed context menu items, made static function for it to be used 2025-11-29 22:42:55 +01:00
740b58afc4 Initialize migration. (#88)
Co-authored-by: defnotken <itsdefnotken@gmail.com>
Co-authored-by: cake <admin@cakeandbanana.nl>
Reviewed-on: #88
Reviewed-by: cake <cake@noreply.git.lightless-sync.org>
Co-authored-by: defnotken <defnotken@noreply.git.lightless-sync.org>
Co-committed-by: defnotken <defnotken@noreply.git.lightless-sync.org>
2025-11-29 18:02:39 +01:00
cake
9e12725f89 Merge branch '2.0.0' of https://git.lightless-sync.org/Lightless-Sync/LightlessClient into 2.0.0 2025-11-29 04:52:56 +01:00
cake
aa04ab05ab added downscaled as file scanning if exist. made gib to gb for calculations. changed windows detection. 2025-11-29 04:51:53 +01:00
28967d6e17 rebuild the temp collection if cached files don't persist 2025-11-29 09:17:28 +09:00
d995afcf48 work done on the ipc 2025-11-28 00:33:46 +09:00
5ab67c70d6 Redone nameplate service (#93)
Co-authored-by: cake <admin@cakeandbanana.nl>
Reviewed-on: #93
Reviewed-by: defnotken <defnotken@noreply.git.lightless-sync.org>
2025-11-27 00:17:03 +01:00
8cc83bce79 big clean up in progress 1 2025-11-27 00:29:56 +09:00
1cdc0a90f9 basic summaries for all pair classes, create partial classes and condense models into a single file 2025-11-26 17:56:01 +09:00
defnotken
e350e8007a Fixing submodules 2025-11-25 22:22:35 -06:00
defnotken
7a9ade95c3 Remove PenumbraAPI submodule completely 2025-11-25 22:08:14 -06:00
defnotken
01607c275a Remove broken Penumbra and OtterGui submodules 2025-11-25 22:02:07 -06:00
defnotken
1e6109d1e6 Remove PenumbraAPI submodule 2025-11-25 21:59:14 -06:00
defnotken
961092ab87 Re-add Penumbra and OtterGui submodules 2025-11-25 21:58:14 -06:00
defnotken
36166f1399 Merge branch 'master' into 2.0.0 2025-11-25 19:29:22 -06:00
d057c638ab temp fix 2025-11-25 11:22:53 +09:00
28d9110cb0 Restore logic 2025-11-25 09:42:34 +09:00
ef592032b3 init 2 2025-11-25 07:14:59 +09:00
defnotken
9c794137c1 changelog added 2025-11-11 12:43:09 -06:00
defnotken
4a256f7807 update penumbra api 2025-11-11 12:03:18 -06:00
defnotken
25756561b9 updating lightlessapi 2025-11-11 12:02:37 -06:00
defnotken
e8c546c128 update lightless API pt 1 2025-11-11 11:54:21 -06:00
d4ba1cf437 Merge pull request 'Turned off btrfs compression for now as not completed' (#85) from linux-improvements into 1.12.4
Reviewed-on: #85
Reviewed-by: defnotken <defnotken@noreply.git.lightless-sync.org>
2025-11-11 18:41:05 +01:00
e0d1f98c70 Merge branch '1.12.4' into linux-improvements 2025-11-11 17:12:51 +01:00
cake
1862689b1b Changed some commands in file getting, redone compression check commands and turned off btrfs compactor for 1.12.4 2025-11-11 17:09:50 +01:00
325dc8947d Merge pull request 'download notification stuck fix, more x and y offset positions' (#84) from notification-reworks into 1.12.4
Reviewed-on: #84
2025-11-10 22:00:58 +01:00
choco
95e7f2daa7 download notification progress and download bar, chat only option for notifications (idk why you would bother even enabling the lightless nofis then) 2025-11-10 21:59:20 +01:00
choco
41a303dc91 auto dismiss notifs if no updates 2025-11-10 21:29:55 +01:00
choco
25b03aea15 download dismiss message if downloads are complete 2025-11-10 21:22:28 +01:00
choco
b6564156f0 Merge remote-tracking branch 'origin/notification-reworks' into notification-reworks 2025-11-10 21:05:48 +01:00
choco
f89ce900c7 more offset changes accepting minus for notifications till -2500 2025-11-10 21:05:39 +01:00
299abc21ee Merge branch '1.12.4' into notification-reworks 2025-11-10 21:00:01 +01:00
choco
c02a8ed2ee notification clickthrougable, update notes centered in the middle of the screen unmovable 2025-11-10 20:57:43 +01:00
choco
8692e877cf download notification stuck fix, more x and y offset positions 2025-11-10 10:59:42 +01:00
cake
7de72471bb Refactored 2025-11-10 06:25:35 +01:00
cake
d7182e9d57 Hopefully fixes all issues with linux based path finding 2025-11-10 03:52:37 +01:00
2b02de731a Merge pull request 'Some changes on the file compression for linux and windows regards threading.' (#83) from linux-improvements into 1.12.4
Reviewed-on: #83
2025-11-09 06:11:34 +01:00
cake
e9082ab8d0 forget semicolomn.. 2025-11-07 06:07:34 +01:00
2a06a11cbc Merge branch '1.12.4' into linux-improvements 2025-11-07 05:29:34 +01:00
cake
557121a9b7 Added batching for the File Frag command for the iscompressed calls. 2025-11-07 05:27:58 +01:00
b22140a8d4 Merge pull request 'Added more checks in nameplate handler' (#82) from more-checks-nameplate into 1.12.4
Reviewed-on: #82
2025-11-06 18:14:45 +01:00
4db468a480 Merge pull request 'Btrfs Compactor work, defaulted linux on websockets.' (#77) from linux-improvements into 1.12.4
Reviewed-on: #77
2025-11-06 18:13:52 +01:00
8d8f8d20cd Merge pull request 'Syncshell grouped folders pausing function' (#76) from grouped-folder-pause into 1.12.4
Reviewed-on: #76
Reviewed-by: defnotken <defnotken@noreply.git.lightless-sync.org>
Reviewed-by: choco <choco@noreply.git.lightless-sync.org>
2025-11-06 18:13:41 +01:00
3722b79615 Merge pull request 'Fixed amount of lightfinder users, added user names in the server info list for lightfinder. Added /lightless command' (#75) from lightfinder-dtr-changes into 1.12.4
Reviewed-on: #75
Reviewed-by: defnotken <defnotken@noreply.git.lightless-sync.org>
Reviewed-by: choco <choco@noreply.git.lightless-sync.org>
2025-11-06 18:13:33 +01:00
cf97e7e800 Merge pull request 'pair button now has additional checks to show if the user isnt directly paired, and only shows if your own lightfinder is on' (#74) from direct-pair-button into 1.12.4
Reviewed-on: #74
Reviewed-by: defnotken <defnotken@noreply.git.lightless-sync.org>
2025-11-06 18:13:22 +01:00
azyges
1d672d2552 improve checks and add logging 2025-11-06 23:40:58 +09:00
cake
35636f27f6 Cleanup 2025-11-03 21:47:15 +01:00
cake
1b686e45dc Added more null checks and redid active broadcasting cache. 2025-11-03 20:19:02 +01:00
cake
b6aa2bebb1 Added more checks. 2025-11-03 19:59:12 +01:00
cake
cfc9f60176 Added safe checks on enqueue. 2025-11-03 19:27:47 +01:00
cake
d4dca455ba Clean-up, added extra checks on linux in cache monitor, documentation added 2025-11-03 18:54:35 +01:00
76c2777f00 Merge pull request 'Changed warning text for Brio #78' (#81) from character-hub-brio-change into 1.12.4
Reviewed-on: #81
2025-11-01 00:12:49 +01:00
cake
0af2a6134b Changed warning text for Brio 2025-11-01 00:09:54 +01:00
cake
6e3c60f627 Changes in file compression for windows, redone linux side because wine issues. 2025-10-31 23:47:41 +01:00
cake
5feb74c1c0 Added another wine check in parralel with dalamud 2025-10-30 03:46:55 +01:00
cake
c1770528f3 Added wine checks, path fixing on wine -> linux 2025-10-30 03:34:56 +01:00
cake
bf139c128b Added fail safes in compact of WOF incase 2025-10-30 03:11:38 +01:00
cake
b3cc41382f Refactored a bit, added comments on the file systems. 2025-10-30 03:05:53 +01:00
cake
7c4d0fd5e9 Added comments, clean-up 2025-10-29 22:54:50 +01:00
cake
c37e3badf1 Check if wine is used. 2025-10-29 06:09:44 +01:00
f4478f653a Merge branch '1.12.4' into linux-improvements 2025-10-29 04:53:36 +01:00
cake
3f85852618 Added string comparisons 2025-10-29 04:52:17 +01:00
cake
3e626c5e47 Cleanup some code, removed ntfs usage on cache monitor 2025-10-29 04:49:46 +01:00
cake
9a846a37d4 Redone handling of windows compactor handling. 2025-10-29 04:43:18 +01:00
cake
177534d78b Implemented compactor to work on BTRFS, redid cache a bit for better function on linux. Removed error for websockets, it will be forced on wine again. 2025-10-29 04:37:24 +01:00
cake
de75b90703 Added spacing on function 2025-10-28 18:23:01 +01:00
cake
c16891021c Added pause button for all syncshells and grouped syncshells. 2025-10-28 18:20:57 +01:00
d19d1c0a3a Merge branch '1.12.4' into lightfinder-dtr-changes 2025-10-28 15:47:42 +01:00
choco
cabc4ec0fe removed lightfinder on check 2025-10-28 00:57:49 +01:00
cake
8bccdc5ef1 Added lightless command. 2025-10-27 22:26:03 +01:00
cake
ce5f8a43a2 Added list of users names in the dtr entry whenever lightfinder is active 2025-10-27 16:16:40 +01:00
choco
437731749f pair button now has additional checks to show if the user isnt directly paired, and only shows if your own lightfinder is on 2025-10-26 17:34:17 +01:00
defnotken
55e78e088a Initialize .4 2025-10-24 09:45:24 -05:00
32 changed files with 1089 additions and 2503 deletions

View File

@@ -1,44 +1,11 @@
tagline: "Lightless Sync v2.0.1" tagline: "Lightless Sync v2.0.0"
subline: "LIGHTLESS IS EVOLVING!!" subline: "LIGHTLESS IS EVOLVING!!"
changelog: changelog:
- name: "v2.0.1"
tagline: "Some Fixes"
date: "December 23 2025"
# be sure to set this every new version
isCurrent: true
versions:
- number: "Chat"
icon: ""
items:
- "You can turn off the syncshell chat as Owner by going to the Syncshell Admin panel -> Owner -> Enable/Disable Chat."
- "Fixed an issue where you can't chat due to regions being in a different language."
- number: "LightFinder"
icon: ""
items:
- "The icon/Lightfinder Text will be hidden when Game UI is hidden and behind game elements/UI"
- "Able to select an icon for the selected list or a custom glyph if you know the code."
- "Smoothing and reducing jitter on the icon/Lightfinder Text."
- "Fixed so higher scaled UI options (100/150/200% UI scale) wouldn't break the element."
- "Detects if GPose is active, wouldn't render the elements"
- number: "Miscellaneous fixes"
icon: ""
items:
- "Fixed the null error given on GetCID when transferring between zones/housing."
- "Added push/pop on certain ImGUI elements to remove them after being used. "
- "Having all tabs open in the Main UI wouldn't lag out the game anymore."
- "Cycle pause has been adjusted to the old function. There is a separate button to pause normally, now called 'Toggle (Un)Pause State'."
- "Changes have been made to the character redraw to address the issues with the building character data constantly being redrawn and the redrawn behavior with Honorific titles."
- "GPose characters should appear again in the actor screen"
- "Lightspeed download console messages are no longer shown as warnings."
- number: "Server Updates"
icon: ""
items:
- "Changes have been made to the disabling of your profile. It should save again."
- "Ability added to toggle chats from syncshell to be disabled."
- "Files are continuously being deleted due to high volumes in storage, potentially causing MCDOs to have missing files. We have increased the limit of the storage in our configurations to see if that helps."
- name: "v2.0.0" - name: "v2.0.0"
tagline: "Thank you for 4 months!" tagline: "Thank you for 4 months!"
date: "December 2025" date: "December 2025"
# be sure to set this every new version
isCurrent: true
versions: versions:
- number: "Lightless Chat" - number: "Lightless Chat"
icon: "" icon: ""

View File

@@ -6,7 +6,6 @@ using LightlessSync.Utils;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
using System.Collections.Concurrent; using System.Collections.Concurrent;
using System.Collections.Immutable; using System.Collections.Immutable;
using System.IO;
namespace LightlessSync.FileCache; namespace LightlessSync.FileCache;
@@ -22,7 +21,6 @@ public sealed class CacheMonitor : DisposableMediatorSubscriberBase
private CancellationTokenSource _scanCancellationTokenSource = new(); private CancellationTokenSource _scanCancellationTokenSource = new();
private readonly CancellationTokenSource _periodicCalculationTokenSource = new(); private readonly CancellationTokenSource _periodicCalculationTokenSource = new();
public static readonly IImmutableList<string> AllowedFileExtensions = [".mdl", ".tex", ".mtrl", ".tmb", ".pap", ".avfx", ".atex", ".sklb", ".eid", ".phyb", ".pbd", ".scd", ".skp", ".shpk", ".kdb"]; public static readonly IImmutableList<string> AllowedFileExtensions = [".mdl", ".tex", ".mtrl", ".tmb", ".pap", ".avfx", ".atex", ".sklb", ".eid", ".phyb", ".pbd", ".scd", ".skp", ".shpk", ".kdb"];
private static readonly HashSet<string> AllowedFileExtensionSet = new(AllowedFileExtensions, StringComparer.OrdinalIgnoreCase);
public CacheMonitor(ILogger<CacheMonitor> logger, IpcManager ipcManager, LightlessConfigService configService, public CacheMonitor(ILogger<CacheMonitor> logger, IpcManager ipcManager, LightlessConfigService configService,
FileCacheManager fileDbManager, LightlessMediator mediator, PerformanceCollectorService performanceCollector, DalamudUtilService dalamudUtil, FileCacheManager fileDbManager, LightlessMediator mediator, PerformanceCollectorService performanceCollector, DalamudUtilService dalamudUtil,
@@ -165,7 +163,7 @@ public sealed class CacheMonitor : DisposableMediatorSubscriberBase
{ {
Logger.LogTrace("Lightless FSW: FileChanged: {change} => {path}", e.ChangeType, e.FullPath); Logger.LogTrace("Lightless FSW: FileChanged: {change} => {path}", e.ChangeType, e.FullPath);
if (!HasAllowedExtension(e.FullPath)) return; if (!AllowedFileExtensions.Any(ext => e.FullPath.EndsWith(ext, StringComparison.OrdinalIgnoreCase))) return;
lock (_watcherChanges) lock (_watcherChanges)
{ {
@@ -209,7 +207,7 @@ public sealed class CacheMonitor : DisposableMediatorSubscriberBase
private void Fs_Changed(object sender, FileSystemEventArgs e) private void Fs_Changed(object sender, FileSystemEventArgs e)
{ {
if (Directory.Exists(e.FullPath)) return; if (Directory.Exists(e.FullPath)) return;
if (!HasAllowedExtension(e.FullPath)) return; if (!AllowedFileExtensions.Any(ext => e.FullPath.EndsWith(ext, StringComparison.OrdinalIgnoreCase))) return;
if (e.ChangeType is not (WatcherChangeTypes.Changed or WatcherChangeTypes.Deleted or WatcherChangeTypes.Created)) if (e.ChangeType is not (WatcherChangeTypes.Changed or WatcherChangeTypes.Deleted or WatcherChangeTypes.Created))
return; return;
@@ -233,7 +231,7 @@ public sealed class CacheMonitor : DisposableMediatorSubscriberBase
{ {
foreach (var file in directoryFiles) foreach (var file in directoryFiles)
{ {
if (!HasAllowedExtension(file)) continue; if (!AllowedFileExtensions.Any(ext => file.EndsWith(ext, StringComparison.OrdinalIgnoreCase))) continue;
var oldPath = file.Replace(e.FullPath, e.OldFullPath, StringComparison.OrdinalIgnoreCase); var oldPath = file.Replace(e.FullPath, e.OldFullPath, StringComparison.OrdinalIgnoreCase);
_watcherChanges.Remove(oldPath); _watcherChanges.Remove(oldPath);
@@ -245,7 +243,7 @@ public sealed class CacheMonitor : DisposableMediatorSubscriberBase
} }
else else
{ {
if (!HasAllowedExtension(e.FullPath)) return; if (!AllowedFileExtensions.Any(ext => e.FullPath.EndsWith(ext, StringComparison.OrdinalIgnoreCase))) return;
lock (_watcherChanges) lock (_watcherChanges)
{ {
@@ -265,17 +263,6 @@ public sealed class CacheMonitor : DisposableMediatorSubscriberBase
public FileSystemWatcher? PenumbraWatcher { get; private set; } public FileSystemWatcher? PenumbraWatcher { get; private set; }
public FileSystemWatcher? LightlessWatcher { get; private set; } public FileSystemWatcher? LightlessWatcher { get; private set; }
private static bool HasAllowedExtension(string path)
{
if (string.IsNullOrEmpty(path))
{
return false;
}
var extension = Path.GetExtension(path);
return !string.IsNullOrEmpty(extension) && AllowedFileExtensionSet.Contains(extension);
}
private async Task LightlessWatcherExecution() private async Task LightlessWatcherExecution()
{ {
_lightlessFswCts = _lightlessFswCts.CancelRecreate(); _lightlessFswCts = _lightlessFswCts.CancelRecreate();
@@ -619,7 +606,7 @@ public sealed class CacheMonitor : DisposableMediatorSubscriberBase
[ [
.. Directory.GetFiles(folder, "*.*", SearchOption.AllDirectories) .. Directory.GetFiles(folder, "*.*", SearchOption.AllDirectories)
.AsParallel() .AsParallel()
.Where(f => HasAllowedExtension(f) .Where(f => AllowedFileExtensions.Any(e => f.EndsWith(e, StringComparison.OrdinalIgnoreCase))
&& !f.Contains(@"\bg\", StringComparison.OrdinalIgnoreCase) && !f.Contains(@"\bg\", StringComparison.OrdinalIgnoreCase)
&& !f.Contains(@"\bgcommon\", StringComparison.OrdinalIgnoreCase) && !f.Contains(@"\bgcommon\", StringComparison.OrdinalIgnoreCase)
&& !f.Contains(@"\ui\", StringComparison.OrdinalIgnoreCase)), && !f.Contains(@"\ui\", StringComparison.OrdinalIgnoreCase)),

View File

@@ -59,7 +59,7 @@ public sealed class TransientResourceManager : DisposableMediatorSubscriberBase
_playerRelatedPointers.Remove(msg.GameObjectHandler); _playerRelatedPointers.Remove(msg.GameObjectHandler);
}); });
foreach (var descriptor in _actorObjectService.ObjectDescriptors) foreach (var descriptor in _actorObjectService.PlayerDescriptors)
{ {
HandleActorTracked(descriptor); HandleActorTracked(descriptor);
} }
@@ -291,7 +291,7 @@ public sealed class TransientResourceManager : DisposableMediatorSubscriberBase
} }
var activeDescriptors = new Dictionary<nint, ObjectKind>(); var activeDescriptors = new Dictionary<nint, ObjectKind>();
foreach (var descriptor in _actorObjectService.ObjectDescriptors) foreach (var descriptor in _actorObjectService.PlayerDescriptors)
{ {
if (TryResolveObjectKind(descriptor, out var resolvedKind)) if (TryResolveObjectKind(descriptor, out var resolvedKind))
{ {
@@ -372,9 +372,6 @@ public sealed class TransientResourceManager : DisposableMediatorSubscriberBase
private void HandleActorTracked(ActorObjectService.ActorDescriptor descriptor) private void HandleActorTracked(ActorObjectService.ActorDescriptor descriptor)
{ {
if (descriptor.IsInGpose)
return;
if (!TryResolveObjectKind(descriptor, out var resolvedKind)) if (!TryResolveObjectKind(descriptor, out var resolvedKind))
return; return;

View File

@@ -1,4 +1,5 @@
using Dalamud.Plugin; using Dalamud.Game.ClientState.Objects.SubKinds;
using Dalamud.Plugin;
using Dalamud.Plugin.Ipc; using Dalamud.Plugin.Ipc;
using LightlessSync.Interop.Ipc.Framework; using LightlessSync.Interop.Ipc.Framework;
using LightlessSync.Services; using LightlessSync.Services;
@@ -12,7 +13,7 @@ public sealed class IpcCallerMoodles : IpcServiceBase
private static readonly IpcServiceDescriptor MoodlesDescriptor = new("Moodles", "Moodles", new Version(0, 0, 0, 0)); private static readonly IpcServiceDescriptor MoodlesDescriptor = new("Moodles", "Moodles", new Version(0, 0, 0, 0));
private readonly ICallGateSubscriber<int> _moodlesApiVersion; private readonly ICallGateSubscriber<int> _moodlesApiVersion;
private readonly ICallGateSubscriber<nint, object> _moodlesOnChange; private readonly ICallGateSubscriber<IPlayerCharacter, object> _moodlesOnChange;
private readonly ICallGateSubscriber<nint, string> _moodlesGetStatus; private readonly ICallGateSubscriber<nint, string> _moodlesGetStatus;
private readonly ICallGateSubscriber<nint, string, object> _moodlesSetStatus; private readonly ICallGateSubscriber<nint, string, object> _moodlesSetStatus;
private readonly ICallGateSubscriber<nint, object> _moodlesRevertStatus; private readonly ICallGateSubscriber<nint, object> _moodlesRevertStatus;
@@ -28,7 +29,7 @@ public sealed class IpcCallerMoodles : IpcServiceBase
_lightlessMediator = lightlessMediator; _lightlessMediator = lightlessMediator;
_moodlesApiVersion = pi.GetIpcSubscriber<int>("Moodles.Version"); _moodlesApiVersion = pi.GetIpcSubscriber<int>("Moodles.Version");
_moodlesOnChange = pi.GetIpcSubscriber<nint, object>("Moodles.StatusManagerModified"); _moodlesOnChange = pi.GetIpcSubscriber<IPlayerCharacter, object>("Moodles.StatusManagerModified");
_moodlesGetStatus = pi.GetIpcSubscriber<nint, string>("Moodles.GetStatusManagerByPtrV2"); _moodlesGetStatus = pi.GetIpcSubscriber<nint, string>("Moodles.GetStatusManagerByPtrV2");
_moodlesSetStatus = pi.GetIpcSubscriber<nint, string, object>("Moodles.SetStatusManagerByPtrV2"); _moodlesSetStatus = pi.GetIpcSubscriber<nint, string, object>("Moodles.SetStatusManagerByPtrV2");
_moodlesRevertStatus = pi.GetIpcSubscriber<nint, object>("Moodles.ClearStatusManagerByPtrV2"); _moodlesRevertStatus = pi.GetIpcSubscriber<nint, object>("Moodles.ClearStatusManagerByPtrV2");
@@ -38,9 +39,9 @@ public sealed class IpcCallerMoodles : IpcServiceBase
CheckAPI(); CheckAPI();
} }
private void OnMoodlesChange(nint address) private void OnMoodlesChange(IPlayerCharacter character)
{ {
_lightlessMediator.Publish(new MoodlesMessage(address)); _lightlessMediator.Publish(new MoodlesMessage(character.Address));
} }
protected override void Dispose(bool disposing) protected override void Dispose(bool disposing)
@@ -106,7 +107,7 @@ public sealed class IpcCallerMoodles : IpcServiceBase
try try
{ {
return _moodlesApiVersion.InvokeFunc() >= 4 return _moodlesApiVersion.InvokeFunc() == 3
? IpcConnectionState.Available ? IpcConnectionState.Available
: IpcConnectionState.VersionMismatch; : IpcConnectionState.VersionMismatch;
} }

View File

@@ -3,7 +3,7 @@
<PropertyGroup> <PropertyGroup>
<Authors></Authors> <Authors></Authors>
<Company></Company> <Company></Company>
<Version>2.0.1</Version> <Version>1.42.0.73</Version>
<Description></Description> <Description></Description>
<Copyright></Copyright> <Copyright></Copyright>
<PackageProjectUrl>https://github.com/Light-Public-Syncshells/LightlessClient</PackageProjectUrl> <PackageProjectUrl>https://github.com/Light-Public-Syncshells/LightlessClient</PackageProjectUrl>

View File

@@ -119,7 +119,6 @@ public class PlayerDataFactory
CharacterDataFragment fragment = objectKind == ObjectKind.Player ? new CharacterDataFragmentPlayer() : new(); CharacterDataFragment fragment = objectKind == ObjectKind.Player ? new CharacterDataFragmentPlayer() : new();
_logger.LogDebug("Building character data for {obj}", playerRelatedObject); _logger.LogDebug("Building character data for {obj}", playerRelatedObject);
var logDebug = _logger.IsEnabled(LogLevel.Debug);
// wait until chara is not drawing and present so nothing spontaneously explodes // wait until chara is not drawing and present so nothing spontaneously explodes
await _dalamudUtil.WaitWhileCharacterIsDrawing(_logger, playerRelatedObject, Guid.NewGuid(), 30000, ct: ct).ConfigureAwait(false); await _dalamudUtil.WaitWhileCharacterIsDrawing(_logger, playerRelatedObject, Guid.NewGuid(), 30000, ct: ct).ConfigureAwait(false);
@@ -133,6 +132,11 @@ public class PlayerDataFactory
ct.ThrowIfCancellationRequested(); ct.ThrowIfCancellationRequested();
Dictionary<string, List<ushort>>? boneIndices =
objectKind != ObjectKind.Player
? null
: await _dalamudUtil.RunOnFrameworkThread(() => _modelAnalyzer.GetSkeletonBoneIndices(playerRelatedObject)).ConfigureAwait(false);
DateTime start = DateTime.UtcNow; DateTime start = DateTime.UtcNow;
// penumbra call, it's currently broken // penumbra call, it's currently broken
@@ -150,21 +154,11 @@ public class PlayerDataFactory
ct.ThrowIfCancellationRequested(); ct.ThrowIfCancellationRequested();
if (logDebug) _logger.LogDebug("== Static Replacements ==");
foreach (var replacement in fragment.FileReplacements.Where(i => i.HasFileReplacement).OrderBy(i => i.GamePaths.First(), StringComparer.OrdinalIgnoreCase))
{ {
_logger.LogDebug("== Static Replacements =="); _logger.LogDebug("=> {repl}", replacement);
foreach (var replacement in fragment.FileReplacements.Where(i => i.HasFileReplacement).OrderBy(i => i.GamePaths.First(), StringComparer.OrdinalIgnoreCase)) ct.ThrowIfCancellationRequested();
{
_logger.LogDebug("=> {repl}", replacement);
ct.ThrowIfCancellationRequested();
}
}
else
{
foreach (var replacement in fragment.FileReplacements.Where(i => i.HasFileReplacement))
{
ct.ThrowIfCancellationRequested();
}
} }
await _transientResourceManager.WaitForRecording(ct).ConfigureAwait(false); await _transientResourceManager.WaitForRecording(ct).ConfigureAwait(false);
@@ -196,21 +190,11 @@ public class PlayerDataFactory
var transientPaths = ManageSemiTransientData(objectKind); var transientPaths = ManageSemiTransientData(objectKind);
var resolvedTransientPaths = await GetFileReplacementsFromPaths(transientPaths, new HashSet<string>(StringComparer.Ordinal)).ConfigureAwait(false); var resolvedTransientPaths = await GetFileReplacementsFromPaths(transientPaths, new HashSet<string>(StringComparer.Ordinal)).ConfigureAwait(false);
if (logDebug) _logger.LogDebug("== Transient Replacements ==");
foreach (var replacement in resolvedTransientPaths.Select(c => new FileReplacement([.. c.Value], c.Key)).OrderBy(f => f.ResolvedPath, StringComparer.Ordinal))
{ {
_logger.LogDebug("== Transient Replacements =="); _logger.LogDebug("=> {repl}", replacement);
foreach (var replacement in resolvedTransientPaths.Select(c => new FileReplacement([.. c.Value], c.Key)).OrderBy(f => f.ResolvedPath, StringComparer.Ordinal)) fragment.FileReplacements.Add(replacement);
{
_logger.LogDebug("=> {repl}", replacement);
fragment.FileReplacements.Add(replacement);
}
}
else
{
foreach (var replacement in resolvedTransientPaths.Select(c => new FileReplacement([.. c.Value], c.Key)))
{
fragment.FileReplacements.Add(replacement);
}
} }
// clean up all semi transient resources that don't have any file replacement (aka null resolve) // clean up all semi transient resources that don't have any file replacement (aka null resolve)
@@ -268,26 +252,11 @@ public class PlayerDataFactory
ct.ThrowIfCancellationRequested(); ct.ThrowIfCancellationRequested();
Dictionary<string, List<ushort>>? boneIndices = null;
var hasPapFiles = false;
if (objectKind == ObjectKind.Player)
{
hasPapFiles = fragment.FileReplacements.Any(f =>
!f.IsFileSwap && f.GamePaths.First().EndsWith("pap", StringComparison.OrdinalIgnoreCase));
if (hasPapFiles)
{
boneIndices = await _dalamudUtil.RunOnFrameworkThread(() => _modelAnalyzer.GetSkeletonBoneIndices(playerRelatedObject)).ConfigureAwait(false);
}
}
if (objectKind == ObjectKind.Player) if (objectKind == ObjectKind.Player)
{ {
try try
{ {
if (hasPapFiles) await VerifyPlayerAnimationBones(boneIndices, (fragment as CharacterDataFragmentPlayer)!, ct).ConfigureAwait(false);
{
await VerifyPlayerAnimationBones(boneIndices, (fragment as CharacterDataFragmentPlayer)!, ct).ConfigureAwait(false);
}
} }
catch (OperationCanceledException e) catch (OperationCanceledException e)
{ {
@@ -309,16 +278,12 @@ public class PlayerDataFactory
{ {
if (boneIndices == null) return; if (boneIndices == null) return;
if (_logger.IsEnabled(LogLevel.Debug)) foreach (var kvp in boneIndices)
{ {
foreach (var kvp in boneIndices) _logger.LogDebug("Found {skellyname} ({idx} bone indices) on player: {bones}", kvp.Key, kvp.Value.Any() ? kvp.Value.Max() : 0, string.Join(',', kvp.Value));
{
_logger.LogDebug("Found {skellyname} ({idx} bone indices) on player: {bones}", kvp.Key, kvp.Value.Any() ? kvp.Value.Max() : 0, string.Join(',', kvp.Value));
}
} }
var maxPlayerBoneIndex = boneIndices.SelectMany(kvp => kvp.Value).DefaultIfEmpty().Max(); if (boneIndices.All(u => u.Value.Count == 0)) return;
if (maxPlayerBoneIndex <= 0) return;
int noValidationFailed = 0; int noValidationFailed = 0;
foreach (var file in fragment.FileReplacements.Where(f => !f.IsFileSwap && f.GamePaths.First().EndsWith("pap", StringComparison.OrdinalIgnoreCase)).ToList()) foreach (var file in fragment.FileReplacements.Where(f => !f.IsFileSwap && f.GamePaths.First().EndsWith("pap", StringComparison.OrdinalIgnoreCase)).ToList())
@@ -338,13 +303,12 @@ public class PlayerDataFactory
_logger.LogDebug("Verifying bone indices for {path}, found {x} skeletons", file.ResolvedPath, skeletonIndices.Count); _logger.LogDebug("Verifying bone indices for {path}, found {x} skeletons", file.ResolvedPath, skeletonIndices.Count);
foreach (var boneCount in skeletonIndices) foreach (var boneCount in skeletonIndices.Select(k => k).ToList())
{ {
var maxAnimationIndex = boneCount.Value.DefaultIfEmpty().Max(); if (boneCount.Value.Max() > boneIndices.SelectMany(b => b.Value).Max())
if (maxAnimationIndex > maxPlayerBoneIndex)
{ {
_logger.LogWarning("Found more bone indices on the animation {path} skeleton {skl} (max indice {idx}) than on any player related skeleton (max indice {idx2})", _logger.LogWarning("Found more bone indices on the animation {path} skeleton {skl} (max indice {idx}) than on any player related skeleton (max indice {idx2})",
file.ResolvedPath, boneCount.Key, maxAnimationIndex, maxPlayerBoneIndex); file.ResolvedPath, boneCount.Key, boneCount.Value.Max(), boneIndices.SelectMany(b => b.Value).Max());
validationFailed = true; validationFailed = true;
break; break;
} }

View File

@@ -16,8 +16,6 @@ public sealed class GameObjectHandler : DisposableMediatorSubscriberBase, IHighP
private readonly Func<IntPtr> _getAddress; private readonly Func<IntPtr> _getAddress;
private readonly bool _isOwnedObject; private readonly bool _isOwnedObject;
private readonly PerformanceCollectorService _performanceCollector; private readonly PerformanceCollectorService _performanceCollector;
private readonly object _frameworkUpdateGate = new();
private bool _frameworkUpdateSubscribed;
private byte _classJob = 0; private byte _classJob = 0;
private Task? _delayedZoningTask; private Task? _delayedZoningTask;
private bool _haltProcessing = false; private bool _haltProcessing = false;
@@ -49,10 +47,7 @@ public sealed class GameObjectHandler : DisposableMediatorSubscriberBase, IHighP
}); });
} }
if (_isOwnedObject) Mediator.Subscribe<FrameworkUpdateMessage>(this, (_) => FrameworkUpdate());
{
EnableFrameworkUpdates();
}
Mediator.Subscribe<ZoneSwitchEndMessage>(this, (_) => ZoneSwitchEnd()); Mediator.Subscribe<ZoneSwitchEndMessage>(this, (_) => ZoneSwitchEnd());
Mediator.Subscribe<ZoneSwitchStartMessage>(this, (_) => ZoneSwitchStart()); Mediator.Subscribe<ZoneSwitchStartMessage>(this, (_) => ZoneSwitchStart());
@@ -114,7 +109,7 @@ public sealed class GameObjectHandler : DisposableMediatorSubscriberBase, IHighP
{ {
while (await _dalamudUtil.RunOnFrameworkThread(() => while (await _dalamudUtil.RunOnFrameworkThread(() =>
{ {
EnsureLatestObjectState(); if (_haltProcessing) CheckAndUpdateObject();
if (CurrentDrawCondition != DrawCondition.None) return true; if (CurrentDrawCondition != DrawCondition.None) return true;
var gameObj = _dalamudUtil.CreateGameObject(Address); var gameObj = _dalamudUtil.CreateGameObject(Address);
if (gameObj is Dalamud.Game.ClientState.Objects.Types.ICharacter chara) if (gameObj is Dalamud.Game.ClientState.Objects.Types.ICharacter chara)
@@ -153,11 +148,6 @@ public sealed class GameObjectHandler : DisposableMediatorSubscriberBase, IHighP
_haltProcessing = false; _haltProcessing = false;
} }
public void Refresh()
{
_dalamudUtil.RunOnFrameworkThread(CheckAndUpdateObject).GetAwaiter().GetResult();
}
public async Task<bool> IsBeingDrawnRunOnFrameworkAsync() public async Task<bool> IsBeingDrawnRunOnFrameworkAsync()
{ {
return await _dalamudUtil.RunOnFrameworkThread(IsBeingDrawn).ConfigureAwait(false); return await _dalamudUtil.RunOnFrameworkThread(IsBeingDrawn).ConfigureAwait(false);
@@ -371,7 +361,7 @@ public sealed class GameObjectHandler : DisposableMediatorSubscriberBase, IHighP
private bool IsBeingDrawn() private bool IsBeingDrawn()
{ {
EnsureLatestObjectState(); if (_haltProcessing) CheckAndUpdateObject();
if (_dalamudUtil.IsAnythingDrawing) if (_dalamudUtil.IsAnythingDrawing)
{ {
@@ -383,28 +373,6 @@ public sealed class GameObjectHandler : DisposableMediatorSubscriberBase, IHighP
return CurrentDrawCondition != DrawCondition.None; return CurrentDrawCondition != DrawCondition.None;
} }
private void EnsureLatestObjectState()
{
if (_haltProcessing || !_frameworkUpdateSubscribed)
{
CheckAndUpdateObject();
}
}
private void EnableFrameworkUpdates()
{
lock (_frameworkUpdateGate)
{
if (_frameworkUpdateSubscribed)
{
return;
}
Mediator.Subscribe<FrameworkUpdateMessage>(this, _ => FrameworkUpdate());
_frameworkUpdateSubscribed = true;
}
}
private unsafe DrawCondition IsBeingDrawnUnsafe() private unsafe DrawCondition IsBeingDrawnUnsafe()
{ {
if (Address == IntPtr.Zero) return DrawCondition.ObjectZero; if (Address == IntPtr.Zero) return DrawCondition.ObjectZero;

View File

@@ -25,11 +25,6 @@
bool IsDownloading { get; } bool IsDownloading { get; }
int PendingDownloadCount { get; } int PendingDownloadCount { get; }
int ForbiddenDownloadCount { get; } int ForbiddenDownloadCount { get; }
bool PendingModReapply { get; }
bool ModApplyDeferred { get; }
int MissingCriticalMods { get; }
int MissingNonCriticalMods { get; }
int MissingForbiddenMods { get; }
DateTime? InvisibleSinceUtc { get; } DateTime? InvisibleSinceUtc { get; }
DateTime? VisibilityEvictionDueAtUtc { get; } DateTime? VisibilityEvictionDueAtUtc { get; }

View File

@@ -87,25 +87,22 @@ public class Pair
return; return;
} }
if (args.Target is not MenuTargetDefault target || target.TargetObjectId != handler.PlayerCharacterId) if (args.Target is not MenuTargetDefault target || target.TargetObjectId != handler.PlayerCharacterId || IsPaused)
{ {
return; return;
} }
if (!IsPaused) UiSharedService.AddContextMenuItem(args, name: "Open Profile", prefixChar: 'L', colorMenuItem: _lightlessPrefixColor, onClick: () =>
{ {
UiSharedService.AddContextMenuItem(args, name: "Open Profile", prefixChar: 'L', colorMenuItem: _lightlessPrefixColor, onClick: () => _mediator.Publish(new ProfileOpenStandaloneMessage(this));
{ return Task.CompletedTask;
_mediator.Publish(new ProfileOpenStandaloneMessage(this)); });
return Task.CompletedTask;
});
UiSharedService.AddContextMenuItem(args, name: "Reapply last data", prefixChar: 'L', colorMenuItem: _lightlessPrefixColor, onClick: () => UiSharedService.AddContextMenuItem(args, name: "Reapply last data", prefixChar: 'L', colorMenuItem: _lightlessPrefixColor, onClick: () =>
{ {
ApplyLastReceivedData(forced: true); ApplyLastReceivedData(forced: true);
return Task.CompletedTask; return Task.CompletedTask;
}); });
}
UiSharedService.AddContextMenuItem(args, name: "Change Permissions", prefixChar: 'L', colorMenuItem: _lightlessPrefixColor, onClick: () => UiSharedService.AddContextMenuItem(args, name: "Change Permissions", prefixChar: 'L', colorMenuItem: _lightlessPrefixColor, onClick: () =>
{ {
@@ -113,24 +110,7 @@ public class Pair
return Task.CompletedTask; return Task.CompletedTask;
}); });
if (IsPaused) UiSharedService.AddContextMenuItem(args, name: "Cycle pause state", prefixChar: 'L', colorMenuItem: _lightlessPrefixColor, onClick: () =>
{
UiSharedService.AddContextMenuItem(args, name: "Toggle Unpause State", prefixChar: 'L', colorMenuItem: _lightlessPrefixColor, onClick: () =>
{
_ = _apiController.Value.UnpauseAsync(UserData);
return Task.CompletedTask;
});
}
else
{
UiSharedService.AddContextMenuItem(args, name: "Toggle Pause State", prefixChar: 'L', colorMenuItem: _lightlessPrefixColor, onClick: () =>
{
_ = _apiController.Value.PauseAsync(UserData);
return Task.CompletedTask;
});
}
UiSharedService.AddContextMenuItem(args, name: "Cycle Pause State", prefixChar: 'L', colorMenuItem: _lightlessPrefixColor, onClick: () =>
{ {
TriggerCyclePause(); TriggerCyclePause();
return Task.CompletedTask; return Task.CompletedTask;
@@ -238,11 +218,6 @@ public class Pair
handler.IsApplying, handler.IsApplying,
handler.IsDownloading, handler.IsDownloading,
handler.PendingDownloadCount, handler.PendingDownloadCount,
handler.ForbiddenDownloadCount, handler.ForbiddenDownloadCount);
handler.PendingModReapply,
handler.ModApplyDeferred,
handler.MissingCriticalMods,
handler.MissingNonCriticalMods,
handler.MissingForbiddenMods);
} }
} }

View File

@@ -16,12 +16,7 @@ public sealed record PairDebugInfo(
bool IsApplying, bool IsApplying,
bool IsDownloading, bool IsDownloading,
int PendingDownloadCount, int PendingDownloadCount,
int ForbiddenDownloadCount, int ForbiddenDownloadCount)
bool PendingModReapply,
bool ModApplyDeferred,
int MissingCriticalMods,
int MissingNonCriticalMods,
int MissingForbiddenMods)
{ {
public static PairDebugInfo Empty { get; } = new( public static PairDebugInfo Empty { get; } = new(
false, false,
@@ -39,10 +34,5 @@ public sealed record PairDebugInfo(
false, false,
false, false,
0, 0,
0,
false,
false,
0,
0,
0); 0);
} }

View File

@@ -8,7 +8,6 @@ using LightlessSync.Interop.Ipc;
using LightlessSync.PlayerData.Factories; using LightlessSync.PlayerData.Factories;
using LightlessSync.PlayerData.Handlers; using LightlessSync.PlayerData.Handlers;
using LightlessSync.Services; using LightlessSync.Services;
using LightlessSync.Services.ActorTracking;
using LightlessSync.Services.Events; using LightlessSync.Services.Events;
using LightlessSync.Services.Mediator; using LightlessSync.Services.Mediator;
using LightlessSync.Services.PairProcessing; using LightlessSync.Services.PairProcessing;
@@ -19,7 +18,6 @@ using LightlessSync.WebAPI.Files;
using LightlessSync.WebAPI.Files.Models; using LightlessSync.WebAPI.Files.Models;
using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
using DalamudObjectKind = Dalamud.Game.ClientState.Objects.Enums.ObjectKind;
using ObjectKind = LightlessSync.API.Data.Enum.ObjectKind; using ObjectKind = LightlessSync.API.Data.Enum.ObjectKind;
using FileReplacementDataComparer = LightlessSync.PlayerData.Data.FileReplacementDataComparer; using FileReplacementDataComparer = LightlessSync.PlayerData.Data.FileReplacementDataComparer;
@@ -33,7 +31,6 @@ internal sealed class PairHandlerAdapter : DisposableMediatorSubscriberBase, IPa
private sealed record CombatData(Guid ApplicationId, CharacterData CharacterData, bool Forced); private sealed record CombatData(Guid ApplicationId, CharacterData CharacterData, bool Forced);
private readonly DalamudUtilService _dalamudUtil; private readonly DalamudUtilService _dalamudUtil;
private readonly ActorObjectService _actorObjectService;
private readonly FileDownloadManager _downloadManager; private readonly FileDownloadManager _downloadManager;
private readonly FileCacheManager _fileDbManager; private readonly FileCacheManager _fileDbManager;
private readonly GameObjectHandlerFactory _gameObjectHandlerFactory; private readonly GameObjectHandlerFactory _gameObjectHandlerFactory;
@@ -59,15 +56,11 @@ internal sealed class PairHandlerAdapter : DisposableMediatorSubscriberBase, IPa
private bool _forceFullReapply; private bool _forceFullReapply;
private Dictionary<(string GamePath, string? Hash), string>? _lastAppliedModdedPaths; private Dictionary<(string GamePath, string? Hash), string>? _lastAppliedModdedPaths;
private bool _needsCollectionRebuild; private bool _needsCollectionRebuild;
private bool _pendingModReapply;
private bool _lastModApplyDeferred;
private int _lastMissingCriticalMods;
private int _lastMissingNonCriticalMods;
private int _lastMissingForbiddenMods;
private bool _isVisible; private bool _isVisible;
private Guid _penumbraCollection; private Guid _penumbraCollection;
private readonly object _collectionGate = new(); private readonly object _collectionGate = new();
private bool _redrawOnNextApplication = false; private bool _redrawOnNextApplication = false;
private bool _explicitRedrawQueued;
private readonly object _initializationGate = new(); private readonly object _initializationGate = new();
private readonly object _pauseLock = new(); private readonly object _pauseLock = new();
private Task _pauseTransitionTask = Task.CompletedTask; private Task _pauseTransitionTask = Task.CompletedTask;
@@ -80,23 +73,8 @@ internal sealed class PairHandlerAdapter : DisposableMediatorSubscriberBase, IPa
private readonly object _visibilityGraceGate = new(); private readonly object _visibilityGraceGate = new();
private CancellationTokenSource? _visibilityGraceCts; private CancellationTokenSource? _visibilityGraceCts;
private static readonly TimeSpan VisibilityEvictionGrace = TimeSpan.FromMinutes(1); private static readonly TimeSpan VisibilityEvictionGrace = TimeSpan.FromMinutes(1);
private static readonly HashSet<string> NonPriorityModExtensions = new(StringComparer.OrdinalIgnoreCase)
{
".tmb",
".pap",
".atex",
".avfx",
".scd"
};
private DateTime? _invisibleSinceUtc; private DateTime? _invisibleSinceUtc;
private DateTime? _visibilityEvictionDueAtUtc; private DateTime? _visibilityEvictionDueAtUtc;
private DateTime _nextActorLookupUtc = DateTime.MinValue;
private static readonly TimeSpan ActorLookupInterval = TimeSpan.FromSeconds(1);
private static readonly SemaphoreSlim ActorInitializationLimiter = new(1, 1);
private readonly object _actorInitializationGate = new();
private ActorObjectService.ActorDescriptor? _pendingActorDescriptor;
private bool _actorInitializationInProgress;
private bool _frameworkUpdateSubscribed;
public DateTime? InvisibleSinceUtc => _invisibleSinceUtc; public DateTime? InvisibleSinceUtc => _invisibleSinceUtc;
public DateTime? VisibilityEvictionDueAtUtc => _visibilityEvictionDueAtUtc; public DateTime? VisibilityEvictionDueAtUtc => _visibilityEvictionDueAtUtc;
@@ -148,11 +126,6 @@ internal sealed class PairHandlerAdapter : DisposableMediatorSubscriberBase, IPa
public long LastAppliedApproximateVRAMBytes { get; set; } = -1; public long LastAppliedApproximateVRAMBytes { get; set; } = -1;
public long LastAppliedApproximateEffectiveVRAMBytes { get; set; } = -1; public long LastAppliedApproximateEffectiveVRAMBytes { get; set; } = -1;
public CharacterData? LastReceivedCharacterData { get; private set; } public CharacterData? LastReceivedCharacterData { get; private set; }
public bool PendingModReapply => _pendingModReapply;
public bool ModApplyDeferred => _lastModApplyDeferred;
public int MissingCriticalMods => _lastMissingCriticalMods;
public int MissingNonCriticalMods => _lastMissingNonCriticalMods;
public int MissingForbiddenMods => _lastMissingForbiddenMods;
public DateTime? LastDataReceivedAt => _lastDataReceivedAt; public DateTime? LastDataReceivedAt => _lastDataReceivedAt;
public DateTime? LastApplyAttemptAt => _lastApplyAttemptAt; public DateTime? LastApplyAttemptAt => _lastApplyAttemptAt;
public DateTime? LastSuccessfulApplyAt => _lastSuccessfulApplyAt; public DateTime? LastSuccessfulApplyAt => _lastSuccessfulApplyAt;
@@ -173,7 +146,6 @@ internal sealed class PairHandlerAdapter : DisposableMediatorSubscriberBase, IPa
FileDownloadManager transferManager, FileDownloadManager transferManager,
PluginWarningNotificationService pluginWarningNotificationManager, PluginWarningNotificationService pluginWarningNotificationManager,
DalamudUtilService dalamudUtil, DalamudUtilService dalamudUtil,
ActorObjectService actorObjectService,
IHostApplicationLifetime lifetime, IHostApplicationLifetime lifetime,
FileCacheManager fileDbManager, FileCacheManager fileDbManager,
PlayerPerformanceService playerPerformanceService, PlayerPerformanceService playerPerformanceService,
@@ -190,7 +162,6 @@ internal sealed class PairHandlerAdapter : DisposableMediatorSubscriberBase, IPa
_downloadManager = transferManager; _downloadManager = transferManager;
_pluginWarningNotificationManager = pluginWarningNotificationManager; _pluginWarningNotificationManager = pluginWarningNotificationManager;
_dalamudUtil = dalamudUtil; _dalamudUtil = dalamudUtil;
_actorObjectService = actorObjectService;
_lifetime = lifetime; _lifetime = lifetime;
_fileDbManager = fileDbManager; _fileDbManager = fileDbManager;
_playerPerformanceService = playerPerformanceService; _playerPerformanceService = playerPerformanceService;
@@ -214,7 +185,6 @@ internal sealed class PairHandlerAdapter : DisposableMediatorSubscriberBase, IPa
return; return;
} }
ActorObjectService.ActorDescriptor? trackedDescriptor = null;
lock (_initializationGate) lock (_initializationGate)
{ {
if (Initialized) if (Initialized)
@@ -228,12 +198,7 @@ internal sealed class PairHandlerAdapter : DisposableMediatorSubscriberBase, IPa
_forceApplyMods = true; _forceApplyMods = true;
} }
var useFrameworkUpdate = !_actorObjectService.HooksActive; Mediator.Subscribe<FrameworkUpdateMessage>(this, _ => FrameworkUpdate());
if (useFrameworkUpdate)
{
Mediator.Subscribe<FrameworkUpdateMessage>(this, _ => FrameworkUpdate());
_frameworkUpdateSubscribed = true;
}
Mediator.Subscribe<ZoneSwitchStartMessage>(this, _ => Mediator.Subscribe<ZoneSwitchStartMessage>(this, _ =>
{ {
_downloadCancellationTokenSource?.CancelDispose(); _downloadCancellationTokenSource?.CancelDispose();
@@ -269,49 +234,17 @@ internal sealed class PairHandlerAdapter : DisposableMediatorSubscriberBase, IPa
Mediator.Subscribe<CutsceneEndMessage>(this, _ => EnableSync()); Mediator.Subscribe<CutsceneEndMessage>(this, _ => EnableSync());
Mediator.Subscribe<GposeStartMessage>(this, _ => DisableSync()); Mediator.Subscribe<GposeStartMessage>(this, _ => DisableSync());
Mediator.Subscribe<GposeEndMessage>(this, _ => EnableSync()); Mediator.Subscribe<GposeEndMessage>(this, _ => EnableSync());
Mediator.Subscribe<ActorTrackedMessage>(this, msg => HandleActorTracked(msg.Descriptor)); Mediator.Subscribe<DownloadFinishedMessage>(this, msg =>
Mediator.Subscribe<ActorUntrackedMessage>(this, msg => HandleActorUntracked(msg.Descriptor)); {
Mediator.Subscribe<DownloadFinishedMessage>(this, msg => if (_charaHandler is null || !ReferenceEquals(msg.DownloadId, _charaHandler))
{ {
if (_charaHandler is null || !ReferenceEquals(msg.DownloadId, _charaHandler)) return;
{
return;
}
if (_pendingModReapply && IsVisible)
{
if (LastReceivedCharacterData is not null)
{
Logger.LogDebug("Downloads finished for {handler}, reapplying pending mod data", GetLogIdentifier());
ApplyLastReceivedData(forced: true);
return;
}
if (_cachedData is not null)
{
Logger.LogDebug("Downloads finished for {handler}, reapplying pending mod data from cache", GetLogIdentifier());
ApplyCharacterData(Guid.NewGuid(), _cachedData, forceApplyCustomization: true);
return;
}
}
TryApplyQueuedData();
});
if (!useFrameworkUpdate
&& _actorObjectService.TryGetActorByHash(Ident, out var descriptor)
&& descriptor.Address != nint.Zero)
{
trackedDescriptor = descriptor;
} }
TryApplyQueuedData();
});
Initialized = true; Initialized = true;
} }
if (trackedDescriptor.HasValue)
{
HandleActorTracked(trackedDescriptor.Value);
}
} }
private IReadOnlyList<PairConnection> GetCurrentPairs() private IReadOnlyList<PairConnection> GetCurrentPairs()
@@ -804,67 +737,6 @@ internal sealed class PairHandlerAdapter : DisposableMediatorSubscriberBase, IPa
return true; return true;
} }
private bool IsForbiddenHash(string hash)
=> _downloadManager.ForbiddenTransfers.Exists(f => string.Equals(f.Hash, hash, StringComparison.Ordinal));
private static bool IsNonPriorityModPath(string? gamePath)
{
if (string.IsNullOrEmpty(gamePath))
{
return false;
}
var extension = Path.GetExtension(gamePath);
return !string.IsNullOrEmpty(extension) && NonPriorityModExtensions.Contains(extension);
}
private static bool IsCriticalModReplacement(FileReplacementData replacement)
{
foreach (var gamePath in replacement.GamePaths)
{
if (!IsNonPriorityModPath(gamePath))
{
return true;
}
}
return false;
}
private void CountMissingReplacements(IEnumerable<FileReplacementData> missing, out int critical, out int nonCritical, out int forbidden)
{
critical = 0;
nonCritical = 0;
forbidden = 0;
foreach (var replacement in missing)
{
if (IsForbiddenHash(replacement.Hash))
{
forbidden++;
}
if (IsCriticalModReplacement(replacement))
{
critical++;
}
else
{
nonCritical++;
}
}
}
private static void RemoveModApplyChanges(Dictionary<ObjectKind, HashSet<PlayerChanges>> updatedData)
{
foreach (var changes in updatedData.Values)
{
changes.Remove(PlayerChanges.ModFiles);
changes.Remove(PlayerChanges.ModManip);
changes.Remove(PlayerChanges.ForcedRedraw);
}
}
private bool CanApplyNow() private bool CanApplyNow()
{ {
return !_dalamudUtil.IsInCombat return !_dalamudUtil.IsInCombat
@@ -888,16 +760,6 @@ internal sealed class PairHandlerAdapter : DisposableMediatorSubscriberBase, IPa
_lastBlockingConditions = Array.Empty<string>(); _lastBlockingConditions = Array.Empty<string>();
} }
private void DeferApplication(Guid applicationBase, CharacterData characterData, bool forceApplyCustomization, UserData user, string reason,
string failureKey, LogLevel logLevel, string logMessage, params object?[] logArgs)
{
Mediator.Publish(new EventMessage(new Event(PlayerName, user, nameof(PairHandlerAdapter), EventSeverity.Warning, reason)));
Logger.Log(logLevel, logMessage, logArgs);
RecordFailure(reason, failureKey);
_dataReceivedInDowntime = new(applicationBase, characterData, forceApplyCustomization);
SetUploading(false);
}
public void ApplyCharacterData(Guid applicationBase, CharacterData characterData, bool forceApplyCustomization = false) public void ApplyCharacterData(Guid applicationBase, CharacterData characterData, bool forceApplyCustomization = false)
{ {
_lastApplyAttemptAt = DateTime.UtcNow; _lastApplyAttemptAt = DateTime.UtcNow;
@@ -915,48 +777,72 @@ internal sealed class PairHandlerAdapter : DisposableMediatorSubscriberBase, IPa
if (_dalamudUtil.IsInCombat) if (_dalamudUtil.IsInCombat)
{ {
const string reason = "Cannot apply character data: you are in combat, deferring application"; const string reason = "Cannot apply character data: you are in combat, deferring application";
DeferApplication(applicationBase, characterData, forceApplyCustomization, user, reason, "Combat", LogLevel.Debug, Mediator.Publish(new EventMessage(new Event(PlayerName, user, nameof(PairHandlerAdapter), EventSeverity.Warning,
"[BASE-{appBase}] Received data but player is in combat", applicationBase); reason)));
Logger.LogDebug("[BASE-{appBase}] Received data but player is in combat", applicationBase);
RecordFailure(reason, "Combat");
_dataReceivedInDowntime = new(applicationBase, characterData, forceApplyCustomization);
SetUploading(false);
return; return;
} }
if (_dalamudUtil.IsPerforming) if (_dalamudUtil.IsPerforming)
{ {
const string reason = "Cannot apply character data: you are performing music, deferring application"; const string reason = "Cannot apply character data: you are performing music, deferring application";
DeferApplication(applicationBase, characterData, forceApplyCustomization, user, reason, "Performance", LogLevel.Debug, Mediator.Publish(new EventMessage(new Event(PlayerName, user, nameof(PairHandlerAdapter), EventSeverity.Warning,
"[BASE-{appBase}] Received data but player is performing", applicationBase); reason)));
Logger.LogDebug("[BASE-{appBase}] Received data but player is performing", applicationBase);
RecordFailure(reason, "Performance");
_dataReceivedInDowntime = new(applicationBase, characterData, forceApplyCustomization);
SetUploading(false);
return; return;
} }
if (_dalamudUtil.IsInInstance) if (_dalamudUtil.IsInInstance)
{ {
const string reason = "Cannot apply character data: you are in an instance, deferring application"; const string reason = "Cannot apply character data: you are in an instance, deferring application";
DeferApplication(applicationBase, characterData, forceApplyCustomization, user, reason, "Instance", LogLevel.Debug, Mediator.Publish(new EventMessage(new Event(PlayerName, user, nameof(PairHandlerAdapter), EventSeverity.Warning,
"[BASE-{appBase}] Received data but player is in instance", applicationBase); reason)));
Logger.LogDebug("[BASE-{appBase}] Received data but player is in instance", applicationBase);
RecordFailure(reason, "Instance");
_dataReceivedInDowntime = new(applicationBase, characterData, forceApplyCustomization);
SetUploading(false);
return; return;
} }
if (_dalamudUtil.IsInCutscene) if (_dalamudUtil.IsInCutscene)
{ {
const string reason = "Cannot apply character data: you are in a cutscene, deferring application"; const string reason = "Cannot apply character data: you are in a cutscene, deferring application";
DeferApplication(applicationBase, characterData, forceApplyCustomization, user, reason, "Cutscene", LogLevel.Debug, Mediator.Publish(new EventMessage(new Event(PlayerName, user, nameof(PairHandlerAdapter), EventSeverity.Warning,
"[BASE-{appBase}] Received data but player is in a cutscene", applicationBase); reason)));
Logger.LogDebug("[BASE-{appBase}] Received data but player is in a cutscene", applicationBase);
RecordFailure(reason, "Cutscene");
_dataReceivedInDowntime = new(applicationBase, characterData, forceApplyCustomization);
SetUploading(false);
return; return;
} }
if (_dalamudUtil.IsInGpose) if (_dalamudUtil.IsInGpose)
{ {
const string reason = "Cannot apply character data: you are in GPose, deferring application"; const string reason = "Cannot apply character data: you are in GPose, deferring application";
DeferApplication(applicationBase, characterData, forceApplyCustomization, user, reason, "GPose", LogLevel.Debug, Mediator.Publish(new EventMessage(new Event(PlayerName, user, nameof(PairHandlerAdapter), EventSeverity.Warning,
"[BASE-{appBase}] Received data but player is in GPose", applicationBase); reason)));
Logger.LogDebug("[BASE-{appBase}] Received data but player is in GPose", applicationBase);
RecordFailure(reason, "GPose");
_dataReceivedInDowntime = new(applicationBase, characterData, forceApplyCustomization);
SetUploading(false);
return; return;
} }
if (!_ipcManager.Penumbra.APIAvailable || !_ipcManager.Glamourer.APIAvailable) if (!_ipcManager.Penumbra.APIAvailable || !_ipcManager.Glamourer.APIAvailable)
{ {
const string reason = "Cannot apply character data: Penumbra or Glamourer is not available, deferring application"; const string reason = "Cannot apply character data: Penumbra or Glamourer is not available, deferring application";
DeferApplication(applicationBase, characterData, forceApplyCustomization, user, reason, "PluginUnavailable", LogLevel.Information, Mediator.Publish(new EventMessage(new Event(PlayerName, user, nameof(PairHandlerAdapter), EventSeverity.Warning,
"[BASE-{appbase}] Application of data for {player} while Penumbra/Glamourer unavailable, returning", applicationBase, GetLogIdentifier()); reason)));
Logger.LogInformation("[BASE-{appbase}] Application of data for {player} while Penumbra/Glamourer unavailable, returning", applicationBase, GetLogIdentifier());
RecordFailure(reason, "PluginUnavailable");
_dataReceivedInDowntime = new(applicationBase, characterData, forceApplyCustomization);
SetUploading(false);
return; return;
} }
@@ -999,10 +885,13 @@ internal sealed class PairHandlerAdapter : DisposableMediatorSubscriberBase, IPa
_forceApplyMods = false; _forceApplyMods = false;
} }
_explicitRedrawQueued = false;
if (_redrawOnNextApplication && charaDataToUpdate.TryGetValue(ObjectKind.Player, out var player)) if (_redrawOnNextApplication && charaDataToUpdate.TryGetValue(ObjectKind.Player, out var player))
{ {
player.Add(PlayerChanges.ForcedRedraw); player.Add(PlayerChanges.ForcedRedraw);
_redrawOnNextApplication = false; _redrawOnNextApplication = false;
_explicitRedrawQueued = true;
} }
if (charaDataToUpdate.TryGetValue(ObjectKind.Player, out var playerChanges)) if (charaDataToUpdate.TryGetValue(ObjectKind.Player, out var playerChanges))
@@ -1196,14 +1085,7 @@ internal sealed class PairHandlerAdapter : DisposableMediatorSubscriberBase, IPa
Logger.LogDebug("[{applicationId}] Applying Customization Data for {handler}", applicationId, handler); Logger.LogDebug("[{applicationId}] Applying Customization Data for {handler}", applicationId, handler);
await _dalamudUtil.WaitWhileCharacterIsDrawing(Logger, handler, applicationId, 30000, token).ConfigureAwait(false); await _dalamudUtil.WaitWhileCharacterIsDrawing(Logger, handler, applicationId, 30000, token).ConfigureAwait(false);
if (handler.Address != nint.Zero)
{
await _actorObjectService.WaitForFullyLoadedAsync(handler.Address, token).ConfigureAwait(false);
}
token.ThrowIfCancellationRequested(); token.ThrowIfCancellationRequested();
var tasks = new List<Task>();
bool needsRedraw = false;
foreach (var change in changes.Value.OrderBy(p => (int)p)) foreach (var change in changes.Value.OrderBy(p => (int)p))
{ {
Logger.LogDebug("[{applicationId}] Processing {change} for {handler}", applicationId, change, handler); Logger.LogDebug("[{applicationId}] Processing {change} for {handler}", applicationId, change, handler);
@@ -1212,39 +1094,45 @@ internal sealed class PairHandlerAdapter : DisposableMediatorSubscriberBase, IPa
case PlayerChanges.Customize: case PlayerChanges.Customize:
if (charaData.CustomizePlusData.TryGetValue(changes.Key, out var customizePlusData)) if (charaData.CustomizePlusData.TryGetValue(changes.Key, out var customizePlusData))
{ {
tasks.Add(ApplyCustomizeAsync(handler.Address, customizePlusData, changes.Key)); _customizeIds[changes.Key] = await _ipcManager.CustomizePlus.SetBodyScaleAsync(handler.Address, customizePlusData).ConfigureAwait(false);
} }
else if (_customizeIds.TryGetValue(changes.Key, out var customizeId)) else if (_customizeIds.TryGetValue(changes.Key, out var customizeId))
{ {
tasks.Add(RevertCustomizeAsync(customizeId, changes.Key)); await _ipcManager.CustomizePlus.RevertByIdAsync(customizeId).ConfigureAwait(false);
_customizeIds.Remove(changes.Key);
} }
break; break;
case PlayerChanges.Heels: case PlayerChanges.Heels:
tasks.Add(_ipcManager.Heels.SetOffsetForPlayerAsync(handler.Address, charaData.HeelsData)); await _ipcManager.Heels.SetOffsetForPlayerAsync(handler.Address, charaData.HeelsData).ConfigureAwait(false);
break; break;
case PlayerChanges.Honorific: case PlayerChanges.Honorific:
tasks.Add(_ipcManager.Honorific.SetTitleAsync(handler.Address, charaData.HonorificData)); await _ipcManager.Honorific.SetTitleAsync(handler.Address, charaData.HonorificData).ConfigureAwait(false);
break; break;
case PlayerChanges.Glamourer: case PlayerChanges.Glamourer:
if (charaData.GlamourerData.TryGetValue(changes.Key, out var glamourerData)) if (charaData.GlamourerData.TryGetValue(changes.Key, out var glamourerData))
{ {
tasks.Add(_ipcManager.Glamourer.ApplyAllAsync(Logger, handler, glamourerData, applicationId, token)); await _ipcManager.Glamourer.ApplyAllAsync(Logger, handler, glamourerData, applicationId, token).ConfigureAwait(false);
} }
break; break;
case PlayerChanges.Moodles: case PlayerChanges.Moodles:
tasks.Add(_ipcManager.Moodles.SetStatusAsync(handler.Address, charaData.MoodlesData)); await _ipcManager.Moodles.SetStatusAsync(handler.Address, charaData.MoodlesData).ConfigureAwait(false);
break; break;
case PlayerChanges.PetNames: case PlayerChanges.PetNames:
tasks.Add(_ipcManager.PetNames.SetPlayerData(handler.Address, charaData.PetNamesData)); await _ipcManager.PetNames.SetPlayerData(handler.Address, charaData.PetNamesData).ConfigureAwait(false);
break; break;
case PlayerChanges.ForcedRedraw: case PlayerChanges.ForcedRedraw:
needsRedraw = true; if (!ShouldPerformForcedRedraw(changes.Key, changes.Value, charaData))
{
Logger.LogTrace("[{applicationId}] Skipping forced redraw for {handler}", applicationId, handler);
break;
}
await _ipcManager.Penumbra.RedrawAsync(Logger, handler, applicationId, token).ConfigureAwait(false);
break; break;
default: default:
@@ -1252,16 +1140,6 @@ internal sealed class PairHandlerAdapter : DisposableMediatorSubscriberBase, IPa
} }
token.ThrowIfCancellationRequested(); token.ThrowIfCancellationRequested();
} }
if (tasks.Count > 0)
{
await Task.WhenAll(tasks).ConfigureAwait(false);
}
if (needsRedraw)
{
await _ipcManager.Penumbra.RedrawAsync(Logger, handler, applicationId, token).ConfigureAwait(false);
}
} }
finally finally
{ {
@@ -1269,6 +1147,44 @@ internal sealed class PairHandlerAdapter : DisposableMediatorSubscriberBase, IPa
} }
} }
private bool ShouldPerformForcedRedraw(ObjectKind objectKind, ICollection<PlayerChanges> changeSet, CharacterData newData)
{
if (objectKind != ObjectKind.Player)
{
return true;
}
var hasModFiles = changeSet.Contains(PlayerChanges.ModFiles);
var hasManip = changeSet.Contains(PlayerChanges.ModManip);
var modsChanged = hasModFiles && PlayerModFilesChanged(newData, _cachedData);
var manipChanged = hasManip && !string.Equals(_cachedData?.ManipulationData, newData.ManipulationData, StringComparison.Ordinal);
if (modsChanged)
{
_explicitRedrawQueued = false;
return true;
}
if (manipChanged)
{
_explicitRedrawQueued = false;
return true;
}
if (_explicitRedrawQueued)
{
_explicitRedrawQueued = false;
return true;
}
if ((hasModFiles || hasManip) && (_forceFullReapply || _needsCollectionRebuild))
{
_explicitRedrawQueued = false;
return true;
}
return false;
}
private static Dictionary<ObjectKind, HashSet<PlayerChanges>> BuildFullChangeSet(CharacterData characterData) private static Dictionary<ObjectKind, HashSet<PlayerChanges>> BuildFullChangeSet(CharacterData characterData)
{ {
@@ -1423,7 +1339,6 @@ internal sealed class PairHandlerAdapter : DisposableMediatorSubscriberBase, IPa
bool skipDownscaleForPair = ShouldSkipDownscale(); bool skipDownscaleForPair = ShouldSkipDownscale();
var user = GetPrimaryUserData(); var user = GetPrimaryUserData();
Dictionary<(string GamePath, string? Hash), string> moddedPaths; Dictionary<(string GamePath, string? Hash), string> moddedPaths;
List<FileReplacementData> missingReplacements = [];
if (updateModdedPaths) if (updateModdedPaths)
{ {
@@ -1435,7 +1350,6 @@ internal sealed class PairHandlerAdapter : DisposableMediatorSubscriberBase, IPa
{ {
int attempts = 0; int attempts = 0;
List<FileReplacementData> toDownloadReplacements = TryCalculateModdedDictionary(applicationBase, charaData, out moddedPaths, downloadToken); List<FileReplacementData> toDownloadReplacements = TryCalculateModdedDictionary(applicationBase, charaData, out moddedPaths, downloadToken);
missingReplacements = toDownloadReplacements;
while (toDownloadReplacements.Count > 0 && attempts++ <= 10 && !downloadToken.IsCancellationRequested) while (toDownloadReplacements.Count > 0 && attempts++ <= 10 && !downloadToken.IsCancellationRequested)
{ {
@@ -1485,7 +1399,6 @@ internal sealed class PairHandlerAdapter : DisposableMediatorSubscriberBase, IPa
} }
toDownloadReplacements = TryCalculateModdedDictionary(applicationBase, charaData, out moddedPaths, downloadToken); toDownloadReplacements = TryCalculateModdedDictionary(applicationBase, charaData, out moddedPaths, downloadToken);
missingReplacements = toDownloadReplacements;
if (toDownloadReplacements.TrueForAll(c => _downloadManager.ForbiddenTransfers.Exists(f => string.Equals(f.Hash, c.Hash, StringComparison.Ordinal)))) if (toDownloadReplacements.TrueForAll(c => _downloadManager.ForbiddenTransfers.Exists(f => string.Equals(f.Hash, c.Hash, StringComparison.Ordinal))))
{ {
@@ -1509,54 +1422,6 @@ internal sealed class PairHandlerAdapter : DisposableMediatorSubscriberBase, IPa
: []; : [];
} }
var wantsModApply = updateModdedPaths || updateManip;
var pendingModReapply = false;
var deferModApply = false;
if (wantsModApply && missingReplacements.Count > 0)
{
CountMissingReplacements(missingReplacements, out var missingCritical, out var missingNonCritical, out var missingForbidden);
_lastMissingCriticalMods = missingCritical;
_lastMissingNonCriticalMods = missingNonCritical;
_lastMissingForbiddenMods = missingForbidden;
var hasCriticalMissing = missingCritical > 0;
var hasNonCriticalMissing = missingNonCritical > 0;
var hasDownloadableMissing = missingReplacements.Any(replacement => !IsForbiddenHash(replacement.Hash));
var hasDownloadableCriticalMissing = hasCriticalMissing
&& missingReplacements.Any(replacement => !IsForbiddenHash(replacement.Hash) && IsCriticalModReplacement(replacement));
pendingModReapply = hasDownloadableMissing;
_lastModApplyDeferred = false;
if (hasDownloadableCriticalMissing)
{
deferModApply = true;
_lastModApplyDeferred = true;
Logger.LogDebug("[BASE-{appBase}] Critical mod files missing for {handler}, deferring mod apply ({count} missing)",
applicationBase, GetLogIdentifier(), missingReplacements.Count);
}
else if (hasNonCriticalMissing && hasDownloadableMissing)
{
Logger.LogDebug("[BASE-{appBase}] Non-critical mod files missing for {handler}, applying partial mods and reapplying after downloads ({count} missing)",
applicationBase, GetLogIdentifier(), missingReplacements.Count);
}
}
else
{
_lastMissingCriticalMods = 0;
_lastMissingNonCriticalMods = 0;
_lastMissingForbiddenMods = 0;
_lastModApplyDeferred = false;
}
if (deferModApply)
{
updateModdedPaths = false;
updateManip = false;
RemoveModApplyChanges(updatedData);
}
downloadToken.ThrowIfCancellationRequested(); downloadToken.ThrowIfCancellationRequested();
var handlerForApply = _charaHandler; var handlerForApply = _charaHandler;
@@ -1589,7 +1454,7 @@ internal sealed class PairHandlerAdapter : DisposableMediatorSubscriberBase, IPa
_applicationCancellationTokenSource = _applicationCancellationTokenSource.CancelRecreate() ?? new CancellationTokenSource(); _applicationCancellationTokenSource = _applicationCancellationTokenSource.CancelRecreate() ?? new CancellationTokenSource();
var token = _applicationCancellationTokenSource.Token; var token = _applicationCancellationTokenSource.Token;
_applicationTask = ApplyCharacterDataAsync(applicationBase, handlerForApply, charaData, updatedData, updateModdedPaths, updateManip, moddedPaths, wantsModApply, pendingModReapply, token); _applicationTask = ApplyCharacterDataAsync(applicationBase, handlerForApply, charaData, updatedData, updateModdedPaths, updateManip, moddedPaths, token);
} }
finally finally
{ {
@@ -1598,7 +1463,7 @@ internal sealed class PairHandlerAdapter : DisposableMediatorSubscriberBase, IPa
} }
private async Task ApplyCharacterDataAsync(Guid applicationBase, GameObjectHandler handlerForApply, CharacterData charaData, Dictionary<ObjectKind, HashSet<PlayerChanges>> updatedData, bool updateModdedPaths, bool updateManip, private async Task ApplyCharacterDataAsync(Guid applicationBase, GameObjectHandler handlerForApply, CharacterData charaData, Dictionary<ObjectKind, HashSet<PlayerChanges>> updatedData, bool updateModdedPaths, bool updateManip,
Dictionary<(string GamePath, string? Hash), string> moddedPaths, bool wantsModApply, bool pendingModReapply, CancellationToken token) Dictionary<(string GamePath, string? Hash), string> moddedPaths, CancellationToken token)
{ {
try try
{ {
@@ -1607,10 +1472,6 @@ internal sealed class PairHandlerAdapter : DisposableMediatorSubscriberBase, IPa
Logger.LogDebug("[{applicationId}] Waiting for initial draw for for {handler}", _applicationId, handlerForApply); Logger.LogDebug("[{applicationId}] Waiting for initial draw for for {handler}", _applicationId, handlerForApply);
await _dalamudUtil.WaitWhileCharacterIsDrawing(Logger, handlerForApply, _applicationId, 30000, token).ConfigureAwait(false); await _dalamudUtil.WaitWhileCharacterIsDrawing(Logger, handlerForApply, _applicationId, 30000, token).ConfigureAwait(false);
if (handlerForApply.Address != nint.Zero)
{
await _actorObjectService.WaitForFullyLoadedAsync(handlerForApply.Address, token).ConfigureAwait(false);
}
token.ThrowIfCancellationRequested(); token.ThrowIfCancellationRequested();
@@ -1677,11 +1538,7 @@ internal sealed class PairHandlerAdapter : DisposableMediatorSubscriberBase, IPa
_cachedData = charaData; _cachedData = charaData;
_pairStateCache.Store(Ident, charaData); _pairStateCache.Store(Ident, charaData);
if (wantsModApply) _forceFullReapply = false;
{
_pendingModReapply = pendingModReapply;
}
_forceFullReapply = _pendingModReapply;
_needsCollectionRebuild = false; _needsCollectionRebuild = false;
if (LastAppliedApproximateVRAMBytes < 0 || LastAppliedApproximateEffectiveVRAMBytes < 0) if (LastAppliedApproximateVRAMBytes < 0 || LastAppliedApproximateEffectiveVRAMBytes < 0)
{ {
@@ -1727,15 +1584,8 @@ internal sealed class PairHandlerAdapter : DisposableMediatorSubscriberBase, IPa
private void FrameworkUpdate() private void FrameworkUpdate()
{ {
if (string.IsNullOrEmpty(PlayerName) && _charaHandler is null) if (string.IsNullOrEmpty(PlayerName))
{ {
var now = DateTime.UtcNow;
if (now < _nextActorLookupUtc)
{
return;
}
_nextActorLookupUtc = now + ActorLookupInterval;
var pc = _dalamudUtil.FindPlayerByNameHash(Ident); var pc = _dalamudUtil.FindPlayerByNameHash(Ident);
if (pc == default((string, nint))) return; if (pc == default((string, nint))) return;
Logger.LogDebug("One-Time Initializing {handler}", GetLogIdentifier()); Logger.LogDebug("One-Time Initializing {handler}", GetLogIdentifier());
@@ -1745,11 +1595,6 @@ internal sealed class PairHandlerAdapter : DisposableMediatorSubscriberBase, IPa
$"Initializing User For Character {pc.Name}"))); $"Initializing User For Character {pc.Name}")));
} }
TryHandleVisibilityUpdate();
}
private void TryHandleVisibilityUpdate()
{
if (_charaHandler?.Address != nint.Zero && !IsVisible && !_pauseRequested) if (_charaHandler?.Address != nint.Zero && !IsVisible && !_pauseRequested)
{ {
Guid appData = Guid.NewGuid(); Guid appData = Guid.NewGuid();
@@ -1796,24 +1641,16 @@ internal sealed class PairHandlerAdapter : DisposableMediatorSubscriberBase, IPa
} }
else if (_charaHandler?.Address == nint.Zero && IsVisible) else if (_charaHandler?.Address == nint.Zero && IsVisible)
{ {
HandleVisibilityLoss(logChange: true); IsVisible = false;
_charaHandler.Invalidate();
_downloadCancellationTokenSource?.CancelDispose();
_downloadCancellationTokenSource = null;
Logger.LogTrace("{handler} visibility changed, now: {visi}", GetLogIdentifier(), IsVisible);
} }
TryApplyQueuedData(); TryApplyQueuedData();
} }
private void HandleVisibilityLoss(bool logChange)
{
IsVisible = false;
_charaHandler?.Invalidate();
_downloadCancellationTokenSource?.CancelDispose();
_downloadCancellationTokenSource = null;
if (logChange)
{
Logger.LogTrace("{handler} visibility changed, now: {visi}", GetLogIdentifier(), IsVisible);
}
}
private void Initialize(string name) private void Initialize(string name)
{ {
PlayerName = name; PlayerName = name;
@@ -2140,164 +1977,7 @@ internal sealed class PairHandlerAdapter : DisposableMediatorSubscriberBase, IPa
} }
_dataReceivedInDowntime = null; _dataReceivedInDowntime = null;
_ = Task.Run(() => ApplyCharacterData(pending.ApplicationId,
{ pending.CharacterData, pending.Forced);
try
{
ApplyCharacterData(pending.ApplicationId, pending.CharacterData, pending.Forced);
}
catch (Exception ex)
{
Logger.LogError(ex, "Failed applying queued data for {handler}", GetLogIdentifier());
}
});
}
private void HandleActorTracked(ActorObjectService.ActorDescriptor descriptor)
{
if (!TryResolveDescriptorHash(descriptor, out var hashedCid))
return;
if (!string.Equals(hashedCid, Ident, StringComparison.Ordinal))
return;
if (descriptor.Address == nint.Zero)
return;
RefreshTrackedHandler(descriptor);
QueueActorInitialization(descriptor);
}
private void QueueActorInitialization(ActorObjectService.ActorDescriptor descriptor)
{
lock (_actorInitializationGate)
{
_pendingActorDescriptor = descriptor;
if (_actorInitializationInProgress)
{
return;
}
_actorInitializationInProgress = true;
}
_ = Task.Run(InitializeFromTrackedAsync);
}
private async Task InitializeFromTrackedAsync()
{
try
{
await ActorInitializationLimiter.WaitAsync().ConfigureAwait(false);
while (true)
{
ActorObjectService.ActorDescriptor? descriptor;
lock (_actorInitializationGate)
{
descriptor = _pendingActorDescriptor;
_pendingActorDescriptor = null;
}
if (!descriptor.HasValue)
{
break;
}
if (_frameworkUpdateSubscribed && _actorObjectService.HooksActive)
{
Mediator.Unsubscribe<FrameworkUpdateMessage>(this);
_frameworkUpdateSubscribed = false;
}
if (string.IsNullOrEmpty(PlayerName) || _charaHandler is null)
{
Logger.LogDebug("Actor tracked for {handler}, initializing from hook", GetLogIdentifier());
Initialize(descriptor.Value.Name);
Mediator.Publish(new EventMessage(new Event(PlayerName, GetPrimaryUserData(), nameof(PairHandlerAdapter), EventSeverity.Informational,
$"Initializing User For Character {descriptor.Value.Name}")));
}
RefreshTrackedHandler(descriptor.Value);
TryHandleVisibilityUpdate();
}
}
finally
{
ActorInitializationLimiter.Release();
lock (_actorInitializationGate)
{
_actorInitializationInProgress = false;
if (_pendingActorDescriptor.HasValue)
{
_actorInitializationInProgress = true;
_ = Task.Run(InitializeFromTrackedAsync);
}
}
}
}
private void RefreshTrackedHandler(ActorObjectService.ActorDescriptor descriptor)
{
if (_charaHandler is null)
return;
if (descriptor.Address == nint.Zero)
return;
if (_charaHandler.Address == descriptor.Address)
return;
_charaHandler.Refresh();
}
private void HandleActorUntracked(ActorObjectService.ActorDescriptor descriptor)
{
if (!TryResolveDescriptorHash(descriptor, out var hashedCid))
{
if (_charaHandler is null || _charaHandler.Address == nint.Zero)
return;
if (descriptor.Address != _charaHandler.Address)
return;
}
else if (!string.Equals(hashedCid, Ident, StringComparison.Ordinal))
{
return;
}
if (_charaHandler is null || _charaHandler.Address == nint.Zero)
return;
if (descriptor.Address != _charaHandler.Address)
return;
HandleVisibilityLoss(logChange: false);
}
private bool TryResolveDescriptorHash(ActorObjectService.ActorDescriptor descriptor, out string hashedCid)
{
hashedCid = descriptor.HashedContentId ?? string.Empty;
if (!string.IsNullOrEmpty(hashedCid))
return true;
if (descriptor.ObjectKind != DalamudObjectKind.Player || descriptor.Address == nint.Zero)
return false;
hashedCid = DalamudUtilService.GetHashedCIDFromPlayerPointer(descriptor.Address);
return !string.IsNullOrEmpty(hashedCid);
}
private async Task ApplyCustomizeAsync(nint address, string customizeData, ObjectKind kind)
{
_customizeIds[kind] = await _ipcManager.CustomizePlus.SetBodyScaleAsync(address, customizeData).ConfigureAwait(false);
}
private async Task RevertCustomizeAsync(Guid? customizeId, ObjectKind kind)
{
if (!customizeId.HasValue)
return;
await _ipcManager.CustomizePlus.RevertByIdAsync(customizeId.Value).ConfigureAwait(false);
_customizeIds.Remove(kind);
} }
} }

View File

@@ -2,7 +2,6 @@ using LightlessSync.FileCache;
using LightlessSync.Interop.Ipc; using LightlessSync.Interop.Ipc;
using LightlessSync.PlayerData.Factories; using LightlessSync.PlayerData.Factories;
using LightlessSync.Services; using LightlessSync.Services;
using LightlessSync.Services.ActorTracking;
using LightlessSync.Services.Mediator; using LightlessSync.Services.Mediator;
using LightlessSync.Services.PairProcessing; using LightlessSync.Services.PairProcessing;
using LightlessSync.Services.ServerConfiguration; using LightlessSync.Services.ServerConfiguration;
@@ -72,7 +71,6 @@ internal sealed class PairHandlerAdapterFactory : IPairHandlerAdapterFactory
{ {
var downloadManager = _fileDownloadManagerFactory.Create(); var downloadManager = _fileDownloadManagerFactory.Create();
var dalamudUtilService = _serviceProvider.GetRequiredService<DalamudUtilService>(); var dalamudUtilService = _serviceProvider.GetRequiredService<DalamudUtilService>();
var actorObjectService = _serviceProvider.GetRequiredService<ActorObjectService>();
return new PairHandlerAdapter( return new PairHandlerAdapter(
_loggerFactory.CreateLogger<PairHandlerAdapter>(), _loggerFactory.CreateLogger<PairHandlerAdapter>(),
_mediator, _mediator,
@@ -83,7 +81,6 @@ internal sealed class PairHandlerAdapterFactory : IPairHandlerAdapterFactory
downloadManager, downloadManager,
_pluginWarningNotificationManager, _pluginWarningNotificationManager,
dalamudUtilService, dalamudUtilService,
actorObjectService,
_lifetime, _lifetime,
_fileCacheManager, _fileCacheManager,
_playerPerformanceService, _playerPerformanceService,

View File

@@ -201,7 +201,6 @@ public sealed class Plugin : IDalamudPlugin
gameInteropProvider, gameInteropProvider,
objectTable, objectTable,
clientState, clientState,
condition,
sp.GetRequiredService<LightlessMediator>())); sp.GetRequiredService<LightlessMediator>()));
services.AddSingleton(sp => new DalamudUtilService( services.AddSingleton(sp => new DalamudUtilService(
@@ -268,7 +267,6 @@ public sealed class Plugin : IDalamudPlugin
sp.GetRequiredService<ILogger<LightFinderPlateHandler>>(), sp.GetRequiredService<ILogger<LightFinderPlateHandler>>(),
addonLifecycle, addonLifecycle,
gameGui, gameGui,
clientState,
sp.GetRequiredService<LightlessConfigService>(), sp.GetRequiredService<LightlessConfigService>(),
sp.GetRequiredService<LightlessMediator>(), sp.GetRequiredService<LightlessMediator>(),
objectTable, objectTable,

View File

@@ -1,5 +1,4 @@
using System.Collections.Concurrent; using System.Collections.Concurrent;
using Dalamud.Game.ClientState.Conditions;
using Dalamud.Game.ClientState.Objects.SubKinds; using Dalamud.Game.ClientState.Objects.SubKinds;
using Dalamud.Hooking; using Dalamud.Hooking;
using Dalamud.Plugin.Services; using Dalamud.Plugin.Services;
@@ -32,18 +31,13 @@ public sealed class ActorObjectService : IHostedService, IDisposable
private readonly IFramework _framework; private readonly IFramework _framework;
private readonly IGameInteropProvider _interop; private readonly IGameInteropProvider _interop;
private readonly IObjectTable _objectTable; private readonly IObjectTable _objectTable;
private readonly IClientState _clientState;
private readonly ICondition _condition;
private readonly LightlessMediator _mediator; private readonly LightlessMediator _mediator;
private readonly ConcurrentDictionary<nint, ActorDescriptor> _activePlayers = new(); private readonly ConcurrentDictionary<nint, ActorDescriptor> _activePlayers = new();
private readonly ConcurrentDictionary<nint, ActorDescriptor> _gposePlayers = new();
private readonly ConcurrentDictionary<string, ActorDescriptor> _actorsByHash = new(StringComparer.Ordinal); private readonly ConcurrentDictionary<string, ActorDescriptor> _actorsByHash = new(StringComparer.Ordinal);
private readonly ConcurrentDictionary<string, ConcurrentDictionary<nint, ActorDescriptor>> _actorsByName = new(StringComparer.Ordinal); private readonly ConcurrentDictionary<string, ConcurrentDictionary<nint, ActorDescriptor>> _actorsByName = new(StringComparer.Ordinal);
private readonly ConcurrentDictionary<nint, byte> _pendingHashResolutions = new();
private readonly OwnedObjectTracker _ownedTracker = new(); private readonly OwnedObjectTracker _ownedTracker = new();
private ActorSnapshot _snapshot = ActorSnapshot.Empty; private ActorSnapshot _snapshot = ActorSnapshot.Empty;
private GposeSnapshot _gposeSnapshot = GposeSnapshot.Empty;
private Hook<Character.Delegates.OnInitialize>? _onInitializeHook; private Hook<Character.Delegates.OnInitialize>? _onInitializeHook;
private Hook<Character.Delegates.Terminate>? _onTerminateHook; private Hook<Character.Delegates.Terminate>? _onTerminateHook;
@@ -61,29 +55,21 @@ public sealed class ActorObjectService : IHostedService, IDisposable
IGameInteropProvider interop, IGameInteropProvider interop,
IObjectTable objectTable, IObjectTable objectTable,
IClientState clientState, IClientState clientState,
ICondition condition,
LightlessMediator mediator) LightlessMediator mediator)
{ {
_logger = logger; _logger = logger;
_framework = framework; _framework = framework;
_interop = interop; _interop = interop;
_objectTable = objectTable; _objectTable = objectTable;
_clientState = clientState;
_condition = condition;
_mediator = mediator; _mediator = mediator;
} }
private bool IsZoning => _condition[ConditionFlag.BetweenAreas] || _condition[ConditionFlag.BetweenAreas51];
private ActorSnapshot Snapshot => Volatile.Read(ref _snapshot); private ActorSnapshot Snapshot => Volatile.Read(ref _snapshot);
private GposeSnapshot CurrentGposeSnapshot => Volatile.Read(ref _gposeSnapshot);
public IReadOnlyList<nint> PlayerAddresses => Snapshot.PlayerAddresses; public IReadOnlyList<nint> PlayerAddresses => Snapshot.PlayerAddresses;
public IEnumerable<ActorDescriptor> ObjectDescriptors => _activePlayers.Values; public IEnumerable<ActorDescriptor> PlayerDescriptors => _activePlayers.Values;
public IReadOnlyList<ActorDescriptor> PlayerDescriptors => Snapshot.PlayerDescriptors; public IReadOnlyList<ActorDescriptor> PlayerCharacterDescriptors => Snapshot.PlayerDescriptors;
public IReadOnlyList<ActorDescriptor> OwnedDescriptors => Snapshot.OwnedDescriptors;
public IReadOnlyList<ActorDescriptor> GposeDescriptors => CurrentGposeSnapshot.GposeDescriptors;
public bool TryGetActorByHash(string hash, out ActorDescriptor descriptor) => _actorsByHash.TryGetValue(hash, out descriptor); public bool TryGetActorByHash(string hash, out ActorDescriptor descriptor) => _actorsByHash.TryGetValue(hash, out descriptor);
public bool TryGetValidatedActorByHash(string hash, out ActorDescriptor descriptor) public bool TryGetValidatedActorByHash(string hash, out ActorDescriptor descriptor)
@@ -127,7 +113,6 @@ public sealed class ActorObjectService : IHostedService, IDisposable
return false; return false;
} }
public bool HooksActive => _hooksActive; public bool HooksActive => _hooksActive;
public bool HasPendingHashResolutions => !_pendingHashResolutions.IsEmpty;
public IReadOnlyList<nint> RenderedPlayerAddresses => Snapshot.OwnedObjects.RenderedPlayers; public IReadOnlyList<nint> RenderedPlayerAddresses => Snapshot.OwnedObjects.RenderedPlayers;
public IReadOnlyList<nint> RenderedCompanionAddresses => Snapshot.OwnedObjects.RenderedCompanions; public IReadOnlyList<nint> RenderedCompanionAddresses => Snapshot.OwnedObjects.RenderedCompanions;
public IReadOnlyList<nint> OwnedObjectAddresses => Snapshot.OwnedObjects.OwnedAddresses; public IReadOnlyList<nint> OwnedObjectAddresses => Snapshot.OwnedObjects.OwnedAddresses;
@@ -222,7 +207,7 @@ public sealed class ActorObjectService : IHostedService, IDisposable
cancellationToken.ThrowIfCancellationRequested(); cancellationToken.ThrowIfCancellationRequested();
var isLoaded = await _framework.RunOnFrameworkThread(() => IsObjectFullyLoaded(address)).ConfigureAwait(false); var isLoaded = await _framework.RunOnFrameworkThread(() => IsObjectFullyLoaded(address)).ConfigureAwait(false);
if (!IsZoning && isLoaded) if (isLoaded)
return; return;
await Task.Delay(100, cancellationToken).ConfigureAwait(false); await Task.Delay(100, cancellationToken).ConfigureAwait(false);
@@ -312,13 +297,10 @@ public sealed class ActorObjectService : IHostedService, IDisposable
{ {
DisposeHooks(); DisposeHooks();
_activePlayers.Clear(); _activePlayers.Clear();
_gposePlayers.Clear();
_actorsByHash.Clear(); _actorsByHash.Clear();
_actorsByName.Clear(); _actorsByName.Clear();
_pendingHashResolutions.Clear();
_ownedTracker.Reset(); _ownedTracker.Reset();
Volatile.Write(ref _snapshot, ActorSnapshot.Empty); Volatile.Write(ref _snapshot, ActorSnapshot.Empty);
Volatile.Write(ref _gposeSnapshot, GposeSnapshot.Empty);
return Task.CompletedTask; return Task.CompletedTask;
} }
@@ -354,7 +336,7 @@ public sealed class ActorObjectService : IHostedService, IDisposable
_onCompanionTerminateHook.Enable(); _onCompanionTerminateHook.Enable();
_hooksActive = true; _hooksActive = true;
_logger.LogTrace("ActorObjectService hooks enabled."); _logger.LogDebug("ActorObjectService hooks enabled.");
} }
private Task WarmupExistingActors() private Task WarmupExistingActors()
@@ -368,21 +350,36 @@ public sealed class ActorObjectService : IHostedService, IDisposable
private unsafe void OnCharacterInitialized(Character* chara) private unsafe void OnCharacterInitialized(Character* chara)
{ {
ExecuteOriginal(() => _onInitializeHook!.Original(chara), "Error invoking original character initialize."); try
QueueTrack((GameObject*)chara); {
_onInitializeHook!.Original(chara);
}
catch (Exception ex)
{
_logger.LogError(ex, "Error invoking original character initialize.");
}
QueueFrameworkUpdate(() => TrackGameObject((GameObject*)chara));
} }
private unsafe void OnCharacterTerminated(Character* chara) private unsafe void OnCharacterTerminated(Character* chara)
{ {
var address = (nint)chara; var address = (nint)chara;
QueueUntrack(address); QueueFrameworkUpdate(() => UntrackGameObject(address));
ExecuteOriginal(() => _onTerminateHook!.Original(chara), "Error invoking original character terminate."); try
{
_onTerminateHook!.Original(chara);
}
catch (Exception ex)
{
_logger.LogError(ex, "Error invoking original character terminate.");
}
} }
private unsafe GameObject* OnCharacterDisposed(Character* chara, byte freeMemory) private unsafe GameObject* OnCharacterDisposed(Character* chara, byte freeMemory)
{ {
var address = (nint)chara; var address = (nint)chara;
QueueUntrack(address); QueueFrameworkUpdate(() => UntrackGameObject(address));
try try
{ {
return _onDestructorHook!.Original(chara, freeMemory); return _onDestructorHook!.Original(chara, freeMemory);
@@ -419,7 +416,7 @@ public sealed class ActorObjectService : IHostedService, IDisposable
if (_logger.IsEnabled(LogLevel.Debug)) if (_logger.IsEnabled(LogLevel.Debug))
{ {
_logger.LogTrace("Actor tracked: {Name} addr={Address:X} idx={Index} owned={OwnedKind} local={Local} gpose={Gpose}", _logger.LogDebug("Actor tracked: {Name} addr={Address:X} idx={Index} owned={OwnedKind} local={Local} gpose={Gpose}",
descriptor.Name, descriptor.Name,
descriptor.Address, descriptor.Address,
descriptor.ObjectIndex, descriptor.ObjectIndex,
@@ -537,7 +534,7 @@ public sealed class ActorObjectService : IHostedService, IDisposable
RemoveDescriptor(descriptor); RemoveDescriptor(descriptor);
if (_logger.IsEnabled(LogLevel.Debug)) if (_logger.IsEnabled(LogLevel.Debug))
{ {
_logger.LogTrace("Actor untracked: {Name} addr={Address:X} idx={Index} owned={OwnedKind}", _logger.LogDebug("Actor untracked: {Name} addr={Address:X} idx={Index} owned={OwnedKind}",
descriptor.Name, descriptor.Name,
descriptor.Address, descriptor.Address,
descriptor.ObjectIndex, descriptor.ObjectIndex,
@@ -561,14 +558,10 @@ public sealed class ActorObjectService : IHostedService, IDisposable
if (!seen.Add(address)) if (!seen.Add(address))
continue; continue;
var gameObject = (GameObject*)address; if (_activePlayers.ContainsKey(address))
if (_activePlayers.TryGetValue(address, out var existing))
{
RefreshDescriptorIfNeeded(existing, gameObject);
continue; continue;
}
TrackGameObject(gameObject); TrackGameObject((GameObject*)address);
} }
var stale = _activePlayers.Keys.Where(addr => !seen.Contains(addr)).ToList(); var stale = _activePlayers.Keys.Where(addr => !seen.Contains(addr)).ToList();
@@ -581,50 +574,6 @@ public sealed class ActorObjectService : IHostedService, IDisposable
{ {
_nextRefreshAllowed = DateTime.UtcNow + SnapshotRefreshInterval; _nextRefreshAllowed = DateTime.UtcNow + SnapshotRefreshInterval;
} }
if (_clientState.IsGPosing)
{
RefreshGposeActorsInternal();
}
else if (!_gposePlayers.IsEmpty)
{
_gposePlayers.Clear();
PublishGposeSnapshot();
}
}
private unsafe void RefreshDescriptorIfNeeded(ActorDescriptor existing, GameObject* gameObject)
{
if (gameObject == null)
return;
if (existing.ObjectKind != DalamudObjectKind.Player || !string.IsNullOrEmpty(existing.HashedContentId))
return;
var objectKind = (DalamudObjectKind)gameObject->ObjectKind;
if (!IsSupportedObjectKind(objectKind))
return;
if (BuildDescriptor(gameObject, objectKind) is not { } updated)
return;
if (string.IsNullOrEmpty(updated.HashedContentId))
return;
ReplaceDescriptor(existing, updated);
_mediator.Publish(new ActorTrackedMessage(updated));
}
private void ReplaceDescriptor(ActorDescriptor existing, ActorDescriptor updated)
{
RemoveDescriptorFromIndexes(existing);
_ownedTracker.OnDescriptorRemoved(existing);
_activePlayers[updated.Address] = updated;
IndexDescriptor(updated);
_ownedTracker.OnDescriptorAdded(updated);
UpdatePendingHashResolutions(updated);
PublishSnapshot();
} }
private void IndexDescriptor(ActorDescriptor descriptor) private void IndexDescriptor(ActorDescriptor descriptor)
@@ -656,15 +605,30 @@ public sealed class ActorObjectService : IHostedService, IDisposable
private unsafe void OnCompanionInitialized(Companion* companion) private unsafe void OnCompanionInitialized(Companion* companion)
{ {
ExecuteOriginal(() => _onCompanionInitializeHook!.Original(companion), "Error invoking original companion initialize."); try
QueueTrack((GameObject*)companion); {
_onCompanionInitializeHook!.Original(companion);
}
catch (Exception ex)
{
_logger.LogError(ex, "Error invoking original companion initialize.");
}
QueueFrameworkUpdate(() => TrackGameObject((GameObject*)companion));
} }
private unsafe void OnCompanionTerminated(Companion* companion) private unsafe void OnCompanionTerminated(Companion* companion)
{ {
var address = (nint)companion; var address = (nint)companion;
QueueUntrack(address); QueueFrameworkUpdate(() => UntrackGameObject(address));
ExecuteOriginal(() => _onCompanionTerminateHook!.Original(companion), "Error invoking original companion terminate."); try
{
_onCompanionTerminateHook!.Original(companion);
}
catch (Exception ex)
{
_logger.LogError(ex, "Error invoking original companion terminate.");
}
} }
private void RemoveDescriptorFromIndexes(ActorDescriptor descriptor) private void RemoveDescriptorFromIndexes(ActorDescriptor descriptor)
@@ -691,7 +655,6 @@ public sealed class ActorObjectService : IHostedService, IDisposable
_activePlayers[descriptor.Address] = descriptor; _activePlayers[descriptor.Address] = descriptor;
IndexDescriptor(descriptor); IndexDescriptor(descriptor);
_ownedTracker.OnDescriptorAdded(descriptor); _ownedTracker.OnDescriptorAdded(descriptor);
UpdatePendingHashResolutions(descriptor);
PublishSnapshot(); PublishSnapshot();
} }
@@ -699,42 +662,21 @@ public sealed class ActorObjectService : IHostedService, IDisposable
{ {
RemoveDescriptorFromIndexes(descriptor); RemoveDescriptorFromIndexes(descriptor);
_ownedTracker.OnDescriptorRemoved(descriptor); _ownedTracker.OnDescriptorRemoved(descriptor);
_pendingHashResolutions.TryRemove(descriptor.Address, out _);
PublishSnapshot(); PublishSnapshot();
} }
private void UpdatePendingHashResolutions(ActorDescriptor descriptor)
{
if (descriptor.ObjectKind != DalamudObjectKind.Player)
{
_pendingHashResolutions.TryRemove(descriptor.Address, out _);
return;
}
if (string.IsNullOrEmpty(descriptor.HashedContentId))
{
_pendingHashResolutions[descriptor.Address] = 1;
return;
}
_pendingHashResolutions.TryRemove(descriptor.Address, out _);
}
private void PublishSnapshot() private void PublishSnapshot()
{ {
var playerDescriptors = _activePlayers.Values var playerDescriptors = _activePlayers.Values
.Where(descriptor => descriptor.ObjectKind == DalamudObjectKind.Player) .Where(descriptor => descriptor.ObjectKind == DalamudObjectKind.Player)
.ToArray(); .ToArray();
var ownedDescriptors = _activePlayers.Values
.Where(descriptor => descriptor.OwnedKind is not null)
.ToArray();
var playerAddresses = new nint[playerDescriptors.Length]; var playerAddresses = new nint[playerDescriptors.Length];
for (var i = 0; i < playerDescriptors.Length; i++) for (var i = 0; i < playerDescriptors.Length; i++)
playerAddresses[i] = playerDescriptors[i].Address; playerAddresses[i] = playerDescriptors[i].Address;
var ownedSnapshot = _ownedTracker.CreateSnapshot(); var ownedSnapshot = _ownedTracker.CreateSnapshot();
var nextGeneration = Snapshot.Generation + 1; var nextGeneration = Snapshot.Generation + 1;
var snapshot = new ActorSnapshot(playerDescriptors, ownedDescriptors, playerAddresses, ownedSnapshot, nextGeneration); var snapshot = new ActorSnapshot(playerDescriptors, playerAddresses, ownedSnapshot, nextGeneration);
Volatile.Write(ref _snapshot, snapshot); Volatile.Write(ref _snapshot, snapshot);
} }
@@ -752,24 +694,6 @@ public sealed class ActorObjectService : IHostedService, IDisposable
_ = _framework.RunOnFrameworkThread(action); _ = _framework.RunOnFrameworkThread(action);
} }
private void ExecuteOriginal(Action action, string errorMessage)
{
try
{
action();
}
catch (Exception ex)
{
_logger.LogError(ex, errorMessage);
}
}
private unsafe void QueueTrack(GameObject* gameObject)
=> QueueFrameworkUpdate(() => TrackGameObject(gameObject));
private void QueueUntrack(nint address)
=> QueueFrameworkUpdate(() => UntrackGameObject(address));
private void DisposeHooks() private void DisposeHooks()
{ {
var hadHooks = _hooksActive var hadHooks = _hooksActive
@@ -801,7 +725,7 @@ public sealed class ActorObjectService : IHostedService, IDisposable
if (hadHooks) if (hadHooks)
{ {
_logger.LogTrace("ActorObjectService hooks disabled."); _logger.LogDebug("ActorObjectService hooks disabled.");
} }
} }
@@ -846,89 +770,6 @@ public sealed class ActorObjectService : IHostedService, IDisposable
return results; return results;
} }
private unsafe void RefreshGposeActorsInternal()
{
var addresses = EnumerateGposeCharacterAddresses();
HashSet<nint> seen = new(addresses.Count);
foreach (var address in addresses)
{
if (address == nint.Zero)
continue;
if (!seen.Add(address))
continue;
if (_gposePlayers.ContainsKey(address))
continue;
TrackGposeObject((GameObject*)address);
}
var stale = _gposePlayers.Keys.Where(addr => !seen.Contains(addr)).ToList();
foreach (var staleAddress in stale)
{
UntrackGposeObject(staleAddress);
}
PublishGposeSnapshot();
}
private unsafe void TrackGposeObject(GameObject* gameObject)
{
if (gameObject == null)
return;
var objectKind = (DalamudObjectKind)gameObject->ObjectKind;
if (objectKind != DalamudObjectKind.Player)
return;
if (BuildDescriptor(gameObject, objectKind) is not { } descriptor)
return;
if (!descriptor.IsInGpose)
return;
_gposePlayers[descriptor.Address] = descriptor;
}
private void UntrackGposeObject(nint address)
{
if (address == nint.Zero)
return;
_gposePlayers.TryRemove(address, out _);
}
private void PublishGposeSnapshot()
{
var gposeDescriptors = _gposePlayers.Values.ToArray();
var gposeAddresses = new nint[gposeDescriptors.Length];
for (var i = 0; i < gposeDescriptors.Length; i++)
gposeAddresses[i] = gposeDescriptors[i].Address;
var nextGeneration = CurrentGposeSnapshot.Generation + 1;
var snapshot = new GposeSnapshot(gposeDescriptors, gposeAddresses, nextGeneration);
Volatile.Write(ref _gposeSnapshot, snapshot);
}
private List<nint> EnumerateGposeCharacterAddresses()
{
var results = new List<nint>(16);
foreach (var obj in _objectTable)
{
if (obj.ObjectKind != DalamudObjectKind.Player)
continue;
if (obj.ObjectIndex < 200)
continue;
results.Add(obj.Address);
}
return results;
}
private static unsafe bool IsObjectFullyLoaded(nint address) private static unsafe bool IsObjectFullyLoaded(nint address)
{ {
if (address == nint.Zero) if (address == nint.Zero)
@@ -942,10 +783,13 @@ public sealed class ActorObjectService : IHostedService, IDisposable
if (drawObject == null) if (drawObject == null)
return false; return false;
if ((ulong)gameObject->RenderFlags == 2048) if ((gameObject->RenderFlags & VisibilityFlags.Nameplate) != VisibilityFlags.None)
return false; return false;
var characterBase = (CharacterBase*)drawObject; var characterBase = (CharacterBase*)drawObject;
if (characterBase == null)
return false;
if (characterBase->HasModelInSlotLoaded != 0) if (characterBase->HasModelInSlotLoaded != 0)
return false; return false;
@@ -1081,27 +925,14 @@ public sealed class ActorObjectService : IHostedService, IDisposable
private sealed record ActorSnapshot( private sealed record ActorSnapshot(
IReadOnlyList<ActorDescriptor> PlayerDescriptors, IReadOnlyList<ActorDescriptor> PlayerDescriptors,
IReadOnlyList<ActorDescriptor> OwnedDescriptors,
IReadOnlyList<nint> PlayerAddresses, IReadOnlyList<nint> PlayerAddresses,
OwnedObjectSnapshot OwnedObjects, OwnedObjectSnapshot OwnedObjects,
int Generation) int Generation)
{ {
public static ActorSnapshot Empty { get; } = new( public static ActorSnapshot Empty { get; } = new(
Array.Empty<ActorDescriptor>(),
Array.Empty<ActorDescriptor>(), Array.Empty<ActorDescriptor>(),
Array.Empty<nint>(), Array.Empty<nint>(),
OwnedObjectSnapshot.Empty, OwnedObjectSnapshot.Empty,
0); 0);
} }
private sealed record GposeSnapshot(
IReadOnlyList<ActorDescriptor> GposeDescriptors,
IReadOnlyList<nint> GposeAddresses,
int Generation)
{
public static GposeSnapshot Empty { get; } = new(
Array.Empty<ActorDescriptor>(),
Array.Empty<nint>(),
0);
}
} }

View File

@@ -1,5 +1,4 @@
using LightlessSync.API.Dto.Chat; using LightlessSync.API.Dto.Chat;
using LightlessSync.API.Data.Extensions;
using LightlessSync.Services.ActorTracking; using LightlessSync.Services.ActorTracking;
using LightlessSync.Services.Mediator; using LightlessSync.Services.Mediator;
using LightlessSync.WebAPI; using LightlessSync.WebAPI;
@@ -37,8 +36,6 @@ public sealed class ZoneChatService : DisposableMediatorSubscriberBase, IHostedS
private readonly Dictionary<string, bool> _lastPresenceStates = new(StringComparer.Ordinal); private readonly Dictionary<string, bool> _lastPresenceStates = new(StringComparer.Ordinal);
private readonly Dictionary<string, string> _selfTokens = new(StringComparer.Ordinal); private readonly Dictionary<string, string> _selfTokens = new(StringComparer.Ordinal);
private readonly List<PendingSelfMessage> _pendingSelfMessages = new(); private readonly List<PendingSelfMessage> _pendingSelfMessages = new();
private List<ChatChannelSnapshot>? _cachedChannelSnapshots;
private bool _channelsSnapshotDirty = true;
private bool _isLoggedIn; private bool _isLoggedIn;
private bool _isConnected; private bool _isConnected;
@@ -72,11 +69,6 @@ public sealed class ZoneChatService : DisposableMediatorSubscriberBase, IHostedS
{ {
using (_sync.EnterScope()) using (_sync.EnterScope())
{ {
if (!_channelsSnapshotDirty && _cachedChannelSnapshots is not null)
{
return _cachedChannelSnapshots;
}
var snapshots = new List<ChatChannelSnapshot>(_channelOrder.Count); var snapshots = new List<ChatChannelSnapshot>(_channelOrder.Count);
foreach (var key in _channelOrder) foreach (var key in _channelOrder)
{ {
@@ -106,8 +98,6 @@ public sealed class ZoneChatService : DisposableMediatorSubscriberBase, IHostedS
state.Messages.ToList())); state.Messages.ToList()));
} }
_cachedChannelSnapshots = snapshots;
_channelsSnapshotDirty = false;
return snapshots; return snapshots;
} }
} }
@@ -145,8 +135,6 @@ public sealed class ZoneChatService : DisposableMediatorSubscriberBase, IHostedS
state.UnreadCount = 0; state.UnreadCount = 0;
_lastReadCounts[key] = state.Messages.Count; _lastReadCounts[key] = state.Messages.Count;
} }
MarkChannelsSnapshotDirtyLocked();
} }
} }
@@ -198,7 +186,6 @@ public sealed class ZoneChatService : DisposableMediatorSubscriberBase, IHostedS
if (!wasEnabled) if (!wasEnabled)
{ {
_chatEnabled = true; _chatEnabled = true;
MarkChannelsSnapshotDirtyLocked();
} }
} }
@@ -244,8 +231,6 @@ public sealed class ZoneChatService : DisposableMediatorSubscriberBase, IHostedS
state.IsAvailable = false; state.IsAvailable = false;
state.StatusText = "Chat services disabled"; state.StatusText = "Chat services disabled";
} }
MarkChannelsSnapshotDirtyLocked();
} }
UnregisterChatHandler(); UnregisterChatHandler();
@@ -732,7 +717,7 @@ public sealed class ZoneChatService : DisposableMediatorSubscriberBase, IHostedS
_zoneDefinitions[key] = new ZoneChannelDefinition(key, info.DisplayName ?? key, descriptor, territories); _zoneDefinitions[key] = new ZoneChannelDefinition(key, info.DisplayName ?? key, descriptor, territories);
} }
var territoryData = _dalamudUtilService.TerritoryDataEnglish.Value; var territoryData = _dalamudUtilService.TerritoryData.Value;
foreach (var kvp in territoryData) foreach (var kvp in territoryData)
{ {
foreach (var variant in EnumerateTerritoryKeys(kvp.Value)) foreach (var variant in EnumerateTerritoryKeys(kvp.Value))
@@ -868,12 +853,6 @@ public sealed class ZoneChatService : DisposableMediatorSubscriberBase, IHostedS
var infos = new List<GroupChatChannelInfoDto>(groups.Count); var infos = new List<GroupChatChannelInfoDto>(groups.Count);
foreach (var group in groups) foreach (var group in groups)
{ {
// basically prune the channel if it's disabled
if (group.GroupPermissions.IsDisableChat())
{
continue;
}
var descriptor = new ChatChannelDescriptor var descriptor = new ChatChannelDescriptor
{ {
Type = ChatChannelType.Group, Type = ChatChannelType.Group,
@@ -1044,8 +1023,6 @@ public sealed class ZoneChatService : DisposableMediatorSubscriberBase, IHostedS
state.UnreadCount = Math.Min(Math.Max(unreadFromHistory, incrementalUnread), MaxUnreadCount); state.UnreadCount = Math.Min(Math.Max(unreadFromHistory, incrementalUnread), MaxUnreadCount);
state.HasUnread = state.UnreadCount > 0; state.HasUnread = state.UnreadCount > 0;
} }
MarkChannelsSnapshotDirtyLocked();
} }
Mediator.Publish(new ChatChannelMessageAdded(key, message)); Mediator.Publish(new ChatChannelMessageAdded(key, message));
@@ -1227,25 +1204,9 @@ public sealed class ZoneChatService : DisposableMediatorSubscriberBase, IHostedS
{ {
_activeChannelKey = _channelOrder.Count > 0 ? _channelOrder[0] : null; _activeChannelKey = _channelOrder.Count > 0 ? _channelOrder[0] : null;
} }
MarkChannelsSnapshotDirtyLocked();
} }
private void MarkChannelsSnapshotDirty() private void PublishChannelListChanged() => Mediator.Publish(new ChatChannelsUpdated());
{
using (_sync.EnterScope())
{
_channelsSnapshotDirty = true;
}
}
private void MarkChannelsSnapshotDirtyLocked() => _channelsSnapshotDirty = true;
private void PublishChannelListChanged()
{
MarkChannelsSnapshotDirty();
Mediator.Publish(new ChatChannelsUpdated());
}
private static IEnumerable<string> EnumerateTerritoryKeys(string? value) private static IEnumerable<string> EnumerateTerritoryKeys(string? value)
{ {

View File

@@ -129,6 +129,7 @@ internal class ContextMenuService : IHostedService
var snapshot = _pairUiService.GetSnapshot(); var snapshot = _pairUiService.GetSnapshot();
var pair = snapshot.PairsByUid.Values.FirstOrDefault(p => var pair = snapshot.PairsByUid.Values.FirstOrDefault(p =>
p.IsVisible &&
p.PlayerCharacterId != uint.MaxValue && p.PlayerCharacterId != uint.MaxValue &&
p.PlayerCharacterId == target.TargetObjectId); p.PlayerCharacterId == target.TargetObjectId);

View File

@@ -91,10 +91,43 @@ public class DalamudUtilService : IHostedService, IMediatorSubscriber
return gameData.GetExcelSheet<ClassJob>(Dalamud.Game.ClientLanguage.English)! return gameData.GetExcelSheet<ClassJob>(Dalamud.Game.ClientLanguage.English)!
.ToDictionary(k => k.RowId, k => k.NameEnglish.ToString()); .ToDictionary(k => k.RowId, k => k.NameEnglish.ToString());
}); });
var clientLanguage = _clientState.ClientLanguage; TerritoryData = new(() =>
TerritoryData = new(() => BuildTerritoryData(clientLanguage)); {
TerritoryDataEnglish = new(() => BuildTerritoryData(Dalamud.Game.ClientLanguage.English)); return gameData.GetExcelSheet<TerritoryType>(Dalamud.Game.ClientLanguage.English)!
MapData = new(() => BuildMapData(clientLanguage)); .Where(w => w.RowId != 0)
.ToDictionary(w => w.RowId, w =>
{
StringBuilder sb = new();
sb.Append(w.PlaceNameRegion.Value.Name);
if (w.PlaceName.ValueNullable != null)
{
sb.Append(" - ");
sb.Append(w.PlaceName.Value.Name);
}
return sb.ToString();
});
});
MapData = new(() =>
{
return gameData.GetExcelSheet<Map>(Dalamud.Game.ClientLanguage.English)!
.Where(w => w.RowId != 0)
.ToDictionary(w => w.RowId, w =>
{
StringBuilder sb = new();
sb.Append(w.PlaceNameRegion.Value.Name);
if (w.PlaceName.ValueNullable != null)
{
sb.Append(" - ");
sb.Append(w.PlaceName.Value.Name);
}
if (w.PlaceNameSub.ValueNullable != null && !string.IsNullOrEmpty(w.PlaceNameSub.Value.Name.ToString()))
{
sb.Append(" - ");
sb.Append(w.PlaceNameSub.Value.Name);
}
return (w, sb.ToString());
});
});
mediator.Subscribe<TargetPairMessage>(this, (msg) => mediator.Subscribe<TargetPairMessage>(this, (msg) =>
{ {
if (clientState.IsPvP) return; if (clientState.IsPvP) return;
@@ -125,71 +158,6 @@ public class DalamudUtilService : IHostedService, IMediatorSubscriber
private Lazy<ulong> RebuildCID() => new(GetCID); private Lazy<ulong> RebuildCID() => new(GetCID);
public bool IsWine { get; init; } public bool IsWine { get; init; }
private Dictionary<uint, string> BuildTerritoryData(Dalamud.Game.ClientLanguage language)
{
var placeNames = _gameData.GetExcelSheet<PlaceName>(language)!;
return _gameData.GetExcelSheet<TerritoryType>(language)!
.Where(w => w.RowId != 0)
.ToDictionary(w => w.RowId, w =>
{
var regionName = GetPlaceName(placeNames, w.PlaceNameRegion.RowId);
var placeName = GetPlaceName(placeNames, w.PlaceName.RowId);
return BuildPlaceName(regionName, placeName, string.Empty);
});
}
private Dictionary<uint, (Map Map, string MapName)> BuildMapData(Dalamud.Game.ClientLanguage language)
{
var placeNames = _gameData.GetExcelSheet<PlaceName>(language)!;
return _gameData.GetExcelSheet<Map>(language)!
.Where(w => w.RowId != 0)
.ToDictionary(w => w.RowId, w =>
{
var regionName = GetPlaceName(placeNames, w.PlaceNameRegion.RowId);
var placeName = GetPlaceName(placeNames, w.PlaceName.RowId);
var subPlaceName = GetPlaceName(placeNames, w.PlaceNameSub.RowId);
var displayName = BuildPlaceName(regionName, placeName, subPlaceName);
return (w, displayName);
});
}
private static string GetPlaceName(Lumina.Excel.ExcelSheet<PlaceName> placeNames, uint rowId)
{
if (rowId == 0)
{
return string.Empty;
}
return placeNames.GetRow(rowId).Name.ToString();
}
private static string BuildPlaceName(string regionName, string placeName, string subPlaceName)
{
StringBuilder sb = new();
if (!string.IsNullOrWhiteSpace(regionName))
{
sb.Append(regionName);
}
if (!string.IsNullOrWhiteSpace(placeName))
{
if (sb.Length > 0)
{
sb.Append(" - ");
}
sb.Append(placeName);
}
if (!string.IsNullOrWhiteSpace(subPlaceName))
{
if (sb.Length > 0)
{
sb.Append(" - ");
}
sb.Append(subPlaceName);
}
return sb.ToString();
}
private bool ResolvePairAddress(Pair pair, out Pair resolvedPair, out nint address) private bool ResolvePairAddress(Pair pair, out Pair resolvedPair, out nint address)
{ {
resolvedPair = _pairFactory.Value.Create(pair.UniqueIdent) ?? pair; resolvedPair = _pairFactory.Value.Create(pair.UniqueIdent) ?? pair;
@@ -277,7 +245,6 @@ public class DalamudUtilService : IHostedService, IMediatorSubscriber
public Lazy<Dictionary<uint, string>> JobData { get; private set; } public Lazy<Dictionary<uint, string>> JobData { get; private set; }
public Lazy<Dictionary<ushort, string>> WorldData { get; private set; } public Lazy<Dictionary<ushort, string>> WorldData { get; private set; }
public Lazy<Dictionary<uint, string>> TerritoryData { get; private set; } public Lazy<Dictionary<uint, string>> TerritoryData { get; private set; }
public Lazy<Dictionary<uint, string>> TerritoryDataEnglish { get; private set; }
public Lazy<Dictionary<uint, (Map Map, string MapName)>> MapData { get; private set; } public Lazy<Dictionary<uint, (Map Map, string MapName)>> MapData { get; private set; }
public bool IsLodEnabled { get; private set; } public bool IsLodEnabled { get; private set; }
public LightlessMediator Mediator { get; } public LightlessMediator Mediator { get; }
@@ -297,7 +264,7 @@ public class DalamudUtilService : IHostedService, IMediatorSubscriber
return false; return false;
} }
if (!TerritoryDataEnglish.Value.TryGetValue(territoryId, out var name) || string.IsNullOrWhiteSpace(name)) if (!TerritoryData.Value.TryGetValue(territoryId, out var name) || string.IsNullOrWhiteSpace(name))
{ {
return false; return false;
} }
@@ -360,8 +327,8 @@ public class DalamudUtilService : IHostedService, IMediatorSubscriber
public IEnumerable<ICharacter?> GetGposeCharactersFromObjectTable() public IEnumerable<ICharacter?> GetGposeCharactersFromObjectTable()
{ {
foreach (var actor in _objectTable foreach (var actor in _actorObjectService.PlayerDescriptors
.Where(a => a.ObjectIndex > 200 && a.ObjectKind == DalamudObjectKind.Player)) .Where(a => a.ObjectKind == DalamudObjectKind.Player && a.ObjectIndex > 200))
{ {
var character = _objectTable.CreateObjectReference(actor.Address) as ICharacter; var character = _objectTable.CreateObjectReference(actor.Address) as ICharacter;
if (character != null) if (character != null)
@@ -388,8 +355,7 @@ public class DalamudUtilService : IHostedService, IMediatorSubscriber
var playerAddress = playerPointer.Value; var playerAddress = playerPointer.Value;
var ownerEntityId = ((Character*)playerAddress)->EntityId; var ownerEntityId = ((Character*)playerAddress)->EntityId;
var candidateAddress = _objectTable.GetObjectAddress(((GameObject*)playerAddress)->ObjectIndex + 1); if (ownerEntityId == 0) return IntPtr.Zero;
if (ownerEntityId == 0) return candidateAddress;
if (playerAddress == _actorObjectService.LocalPlayerAddress) if (playerAddress == _actorObjectService.LocalPlayerAddress)
{ {
@@ -400,17 +366,6 @@ public class DalamudUtilService : IHostedService, IMediatorSubscriber
} }
} }
if (candidateAddress != nint.Zero)
{
var candidate = (GameObject*)candidateAddress;
var candidateKind = (DalamudObjectKind)candidate->ObjectKind;
if ((candidateKind == DalamudObjectKind.MountType || candidateKind == DalamudObjectKind.Companion)
&& ResolveOwnerId(candidate) == ownerEntityId)
{
return candidateAddress;
}
}
var ownedObject = FindOwnedObject(ownerEntityId, playerAddress, static kind => var ownedObject = FindOwnedObject(ownerEntityId, playerAddress, static kind =>
kind == DalamudObjectKind.MountType || kind == DalamudObjectKind.Companion); kind == DalamudObjectKind.MountType || kind == DalamudObjectKind.Companion);
if (ownedObject != nint.Zero) if (ownedObject != nint.Zero)
@@ -418,7 +373,7 @@ public class DalamudUtilService : IHostedService, IMediatorSubscriber
return ownedObject; return ownedObject;
} }
return candidateAddress; return _objectTable.GetObjectAddress(((GameObject*)playerAddress)->ObjectIndex + 1);
} }
public async Task<IntPtr> GetMinionOrMountAsync(IntPtr? playerPointer = null) public async Task<IntPtr> GetMinionOrMountAsync(IntPtr? playerPointer = null)
@@ -535,10 +490,6 @@ public class DalamudUtilService : IHostedService, IMediatorSubscriber
{ {
EnsureIsOnFramework(); EnsureIsOnFramework();
var playerChar = GetPlayerCharacter(); var playerChar = GetPlayerCharacter();
if (playerChar == null || playerChar.Address == IntPtr.Zero)
return 0;
return ((BattleChara*)playerChar.Address)->Character.ContentId; return ((BattleChara*)playerChar.Address)->Character.ContentId;
} }
@@ -829,7 +780,7 @@ public class DalamudUtilService : IHostedService, IMediatorSubscriber
bool isDrawingChanged = false; bool isDrawingChanged = false;
if ((nint)drawObj != IntPtr.Zero) if ((nint)drawObj != IntPtr.Zero)
{ {
isDrawing = gameObj->RenderFlags == (VisibilityFlags)0b100000000000; isDrawing = (gameObj->RenderFlags & VisibilityFlags.Nameplate) != VisibilityFlags.None;
if (!isDrawing) if (!isDrawing)
{ {
isDrawing = ((CharacterBase*)drawObj)->HasModelInSlotLoaded != 0; isDrawing = ((CharacterBase*)drawObj)->HasModelInSlotLoaded != 0;
@@ -895,12 +846,9 @@ public class DalamudUtilService : IHostedService, IMediatorSubscriber
_performanceCollector.LogPerformance(this, $"TrackedActorsToState", _performanceCollector.LogPerformance(this, $"TrackedActorsToState",
() => () =>
{ {
if (!_actorObjectService.HooksActive || !isNormalFrameworkUpdate || _actorObjectService.HasPendingHashResolutions) _actorObjectService.RefreshTrackedActors();
{
_actorObjectService.RefreshTrackedActors();
}
var playerDescriptors = _actorObjectService.PlayerDescriptors; var playerDescriptors = _actorObjectService.PlayerCharacterDescriptors;
for (var i = 0; i < playerDescriptors.Count; i++) for (var i = 0; i < playerDescriptors.Count; i++)
{ {
var actor = playerDescriptors[i]; var actor = playerDescriptors[i];

View File

@@ -4,7 +4,6 @@ using Dalamud.Game.Addon.Lifecycle.AddonArgTypes;
using Dalamud.Game.ClientState.Objects.Enums; using Dalamud.Game.ClientState.Objects.Enums;
using Dalamud.Game.Text; using Dalamud.Game.Text;
using Dalamud.Interface; using Dalamud.Interface;
using Dalamud.Interface.Utility;
using Dalamud.Plugin; using Dalamud.Plugin;
using Dalamud.Plugin.Services; using Dalamud.Plugin.Services;
using FFXIVClientStructs.FFXIV.Client.System.Framework; using FFXIVClientStructs.FFXIV.Client.System.Framework;
@@ -17,27 +16,22 @@ using LightlessSync.UI;
using LightlessSync.UI.Services; using LightlessSync.UI.Services;
using LightlessSync.Utils; using LightlessSync.Utils;
using LightlessSync.UtilsEnum.Enum; using LightlessSync.UtilsEnum.Enum;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Hosting;
using Pictomancy; using Pictomancy;
using System.Collections.Immutable; using System.Collections.Immutable;
using System.Globalization; using System.Globalization;
using System.Numerics; using System.Numerics;
using System.Runtime.InteropServices;
using Task = System.Threading.Tasks.Task; using Task = System.Threading.Tasks.Task;
namespace LightlessSync.Services.LightFinder; namespace LightlessSync.Services.LightFinder;
/// <summary>
/// The new lightfinder nameplate handler using ImGUI (pictomancy) for rendering the icon/labels.
/// </summary>
public unsafe class LightFinderPlateHandler : IHostedService, IMediatorSubscriber public unsafe class LightFinderPlateHandler : IHostedService, IMediatorSubscriber
{ {
private readonly ILogger<LightFinderPlateHandler> _logger; private readonly ILogger<LightFinderPlateHandler> _logger;
private readonly IAddonLifecycle _addonLifecycle; private readonly IAddonLifecycle _addonLifecycle;
private readonly IGameGui _gameGui; private readonly IGameGui _gameGui;
private readonly IObjectTable _objectTable; private readonly IObjectTable _objectTable;
private readonly IClientState _clientState;
private readonly LightlessConfigService _configService; private readonly LightlessConfigService _configService;
private readonly PairUiService _pairUiService; private readonly PairUiService _pairUiService;
private readonly LightlessMediator _mediator; private readonly LightlessMediator _mediator;
@@ -48,33 +42,21 @@ public unsafe class LightFinderPlateHandler : IHostedService, IMediatorSubscribe
private bool _needsLabelRefresh; private bool _needsLabelRefresh;
private bool _drawSubscribed; private bool _drawSubscribed;
private AddonNamePlate* _mpNameplateAddon; private AddonNamePlate* _mpNameplateAddon;
private readonly Lock _labelLock = new(); private readonly object _labelLock = new();
private readonly NameplateBuffers _buffers = new(); private readonly NameplateBuffers _buffers = new();
private int _labelRenderCount; private int _labelRenderCount;
private const string _defaultLabelText = "LightFinder"; private const string DefaultLabelText = "LightFinder";
private const SeIconChar _defaultIcon = SeIconChar.Hyadelyn; private const SeIconChar DefaultIcon = SeIconChar.Hyadelyn;
private static readonly string _defaultIconGlyph = SeIconCharExtensions.ToIconString(_defaultIcon); private static readonly string DefaultIconGlyph = SeIconCharExtensions.ToIconString(DefaultIcon);
private static readonly Vector2 _defaultPivot = new(0.5f, 1f); private static readonly Vector2 DefaultPivot = new(0.5f, 1f);
private uint _lastNamePlateDrawFrame;
// / Overlay window flags
private const ImGuiWindowFlags _overlayFlags =
ImGuiWindowFlags.NoDecoration |
ImGuiWindowFlags.NoBackground |
ImGuiWindowFlags.NoMove |
ImGuiWindowFlags.NoSavedSettings |
ImGuiWindowFlags.NoNav |
ImGuiWindowFlags.NoInputs;
private readonly List<RectF> _uiRects = new(128);
private ImmutableHashSet<string> _activeBroadcastingCids = []; private ImmutableHashSet<string> _activeBroadcastingCids = [];
public LightFinderPlateHandler( public LightFinderPlateHandler(
ILogger<LightFinderPlateHandler> logger, ILogger<LightFinderPlateHandler> logger,
IAddonLifecycle addonLifecycle, IAddonLifecycle addonLifecycle,
IGameGui gameGui, IGameGui gameGui,
IClientState clientState,
LightlessConfigService configService, LightlessConfigService configService,
LightlessMediator mediator, LightlessMediator mediator,
IObjectTable objectTable, IObjectTable objectTable,
@@ -85,7 +67,6 @@ public unsafe class LightFinderPlateHandler : IHostedService, IMediatorSubscribe
_logger = logger; _logger = logger;
_addonLifecycle = addonLifecycle; _addonLifecycle = addonLifecycle;
_gameGui = gameGui; _gameGui = gameGui;
_clientState = clientState;
_configService = configService; _configService = configService;
_mediator = mediator; _mediator = mediator;
_objectTable = objectTable; _objectTable = objectTable;
@@ -120,9 +101,6 @@ public unsafe class LightFinderPlateHandler : IHostedService, IMediatorSubscribe
_mpNameplateAddon = null; _mpNameplateAddon = null;
} }
/// <summary>
/// Enable nameplate handling.
/// </summary>
internal void EnableNameplate() internal void EnableNameplate()
{ {
if (!_mEnabled) if (!_mEnabled)
@@ -140,9 +118,6 @@ public unsafe class LightFinderPlateHandler : IHostedService, IMediatorSubscribe
} }
} }
/// <summary>
/// Disable nameplate handling.
/// </summary>
internal void DisableNameplate() internal void DisableNameplate()
{ {
if (_mEnabled) if (_mEnabled)
@@ -161,21 +136,8 @@ public unsafe class LightFinderPlateHandler : IHostedService, IMediatorSubscribe
} }
} }
/// <summary>
/// Draw detour for nameplate addon.
/// </summary>
/// <param name="type"></param>
/// <param name="args"></param>
private void NameplateDrawDetour(AddonEvent type, AddonArgs args) private void NameplateDrawDetour(AddonEvent type, AddonArgs args)
{ {
if (_clientState.IsGPosing)
{
ClearLabelBuffer();
Array.Clear(_buffers.HasSmoothed, 0, _buffers.HasSmoothed.Length);
_lastNamePlateDrawFrame = 0;
return;
}
if (args.Addon.Address == nint.Zero) if (args.Addon.Address == nint.Zero)
{ {
if (_logger.IsEnabled(LogLevel.Warning)) if (_logger.IsEnabled(LogLevel.Warning))
@@ -183,10 +145,6 @@ public unsafe class LightFinderPlateHandler : IHostedService, IMediatorSubscribe
return; return;
} }
var fw = Framework.Instance();
if (fw != null)
_lastNamePlateDrawFrame = fw->FrameCounter;
var pNameplateAddon = (AddonNamePlate*)args.Addon.Address; var pNameplateAddon = (AddonNamePlate*)args.Addon.Address;
if (_mpNameplateAddon != pNameplateAddon) if (_mpNameplateAddon != pNameplateAddon)
@@ -198,9 +156,6 @@ public unsafe class LightFinderPlateHandler : IHostedService, IMediatorSubscribe
UpdateNameplateNodes(); UpdateNameplateNodes();
} }
/// <summary>
/// Updates the nameplate nodes with LightFinder objects.
/// </summary>
private void UpdateNameplateNodes() private void UpdateNameplateNodes()
{ {
var currentHandle = _gameGui.GetAddonByName("NamePlate"); var currentHandle = _gameGui.GetAddonByName("NamePlate");
@@ -220,12 +175,6 @@ public unsafe class LightFinderPlateHandler : IHostedService, IMediatorSubscribe
return; return;
} }
if (!IsNamePlateAddonVisible())
{
ClearLabelBuffer();
return;
}
var framework = Framework.Instance(); var framework = Framework.Instance();
if (framework == null) if (framework == null)
{ {
@@ -258,7 +207,7 @@ public unsafe class LightFinderPlateHandler : IHostedService, IMediatorSubscribe
} }
var visibleUserIdsSnapshot = VisibleUserIds; var visibleUserIdsSnapshot = VisibleUserIds;
var safeCount = Math.Min(ui3DModule->NamePlateObjectInfoCount, vec.Length); var safeCount = System.Math.Min(ui3DModule->NamePlateObjectInfoCount, vec.Length);
var currentConfig = _configService.Current; var currentConfig = _configService.Current;
var labelColor = UIColors.Get("Lightfinder"); var labelColor = UIColors.Get("Lightfinder");
var edgeColor = UIColors.Get("LightfinderEdge"); var edgeColor = UIColors.Get("LightfinderEdge");
@@ -266,7 +215,6 @@ public unsafe class LightFinderPlateHandler : IHostedService, IMediatorSubscribe
for (int i = 0; i < safeCount; ++i) for (int i = 0; i < safeCount; ++i)
{ {
var objectInfoPtr = vec[i]; var objectInfoPtr = vec[i];
if (objectInfoPtr == null) if (objectInfoPtr == null)
continue; continue;
@@ -302,6 +250,7 @@ public unsafe class LightFinderPlateHandler : IHostedService, IMediatorSubscribe
var root = nameplateObject.RootComponentNode; var root = nameplateObject.RootComponentNode;
var nameContainer = nameplateObject.NameContainer; var nameContainer = nameplateObject.NameContainer;
var nameText = nameplateObject.NameText; var nameText = nameplateObject.NameText;
var marker = nameplateObject.MarkerIcon;
if (root == null || root->Component == null || nameContainer == null || nameText == null) if (root == null || root->Component == null || nameContainer == null || nameText == null)
{ {
@@ -312,14 +261,14 @@ public unsafe class LightFinderPlateHandler : IHostedService, IMediatorSubscribe
root->Component->UldManager.UpdateDrawNodeList(); root->Component->UldManager.UpdateDrawNodeList();
bool isNameplateVisible = bool isVisible =
nameContainer->IsVisible() && (marker != null && marker->AtkResNode.IsVisible()) ||
nameText->AtkResNode.IsVisible(); (nameContainer->IsVisible() && nameText->AtkResNode.IsVisible()) ||
currentConfig.LightfinderLabelShowHidden;
if (!currentConfig.LightfinderLabelShowHidden && !isNameplateVisible) if (!isVisible)
continue; continue;
// Prepare label content and scaling
var scaleMultiplier = System.Math.Clamp(currentConfig.LightfinderLabelScale, 0.5f, 2.0f); var scaleMultiplier = System.Math.Clamp(currentConfig.LightfinderLabelScale, 0.5f, 2.0f);
var baseScale = currentConfig.LightfinderLabelUseIcon ? 1.0f : 0.5f; var baseScale = currentConfig.LightfinderLabelUseIcon ? 1.0f : 0.5f;
var effectiveScale = baseScale * scaleMultiplier; var effectiveScale = baseScale * scaleMultiplier;
@@ -327,10 +276,10 @@ public unsafe class LightFinderPlateHandler : IHostedService, IMediatorSubscribe
var targetFontSize = (int)System.Math.Round(baseFontSize * scaleMultiplier); var targetFontSize = (int)System.Math.Round(baseFontSize * scaleMultiplier);
var labelContent = currentConfig.LightfinderLabelUseIcon var labelContent = currentConfig.LightfinderLabelUseIcon
? NormalizeIconGlyph(currentConfig.LightfinderLabelIconGlyph) ? NormalizeIconGlyph(currentConfig.LightfinderLabelIconGlyph)
: _defaultLabelText; : DefaultLabelText;
if (!currentConfig.LightfinderLabelUseIcon && (string.IsNullOrWhiteSpace(labelContent) || string.Equals(labelContent, "-", StringComparison.Ordinal))) if (!currentConfig.LightfinderLabelUseIcon && (string.IsNullOrWhiteSpace(labelContent) || string.Equals(labelContent, "-", StringComparison.Ordinal)))
labelContent = _defaultLabelText; labelContent = DefaultLabelText;
var nodeWidth = (int)System.Math.Round(AtkNodeHelpers.DefaultTextNodeWidth * effectiveScale); var nodeWidth = (int)System.Math.Round(AtkNodeHelpers.DefaultTextNodeWidth * effectiveScale);
var nodeHeight = (int)System.Math.Round(AtkNodeHelpers.DefaultTextNodeHeight * effectiveScale); var nodeHeight = (int)System.Math.Round(AtkNodeHelpers.DefaultTextNodeHeight * effectiveScale);
@@ -373,7 +322,6 @@ public unsafe class LightFinderPlateHandler : IHostedService, IMediatorSubscribe
() => GetScaledTextWidth(nameText), () => GetScaledTextWidth(nameText),
nodeWidth); nodeWidth);
// Text offset caching
var textOffset = (int)System.Math.Round(nameText->AtkResNode.X); var textOffset = (int)System.Math.Round(nameText->AtkResNode.X);
var hasValidOffset = TryCacheTextOffset(nameplateIndex, rawTextWidth, textOffset); var hasValidOffset = TryCacheTextOffset(nameplateIndex, rawTextWidth, textOffset);
@@ -384,93 +332,65 @@ public unsafe class LightFinderPlateHandler : IHostedService, IMediatorSubscribe
continue; continue;
} }
var res = nameContainer;
// X scale
var worldScaleX = GetWorldScaleX(res);
if (worldScaleX <= 0f) worldScaleX = 1f;
// Y scale
var worldScaleY = GetWorldScaleY(res);
if (worldScaleY <= 0f) worldScaleY = 1f;
positionY += currentConfig.LightfinderLabelOffsetY;
var positionYScreen = positionY * worldScaleY;
float finalX; float finalX;
if (currentConfig.LightfinderAutoAlign) if (currentConfig.LightfinderAutoAlign)
{ {
// auto X positioning var measuredWidth = System.Math.Max(1, textWidth > 0 ? textWidth : nodeWidth);
var measuredWidth = Math.Max(1, textWidth > 0 ? textWidth : nodeWidth);
var measuredWidthF = (float)measuredWidth; var measuredWidthF = (float)measuredWidth;
var alignmentType = currentConfig.LabelAlignment;
// consider icon width var containerScale = nameContainer->ScaleX;
var containerWidthLocal = res->Width > 0 ? res->Width : measuredWidthF; if (containerScale <= 0f)
var containerWidthScreen = containerWidthLocal * worldScaleX; containerScale = 1f;
var containerWidthRaw = (float)nameContainer->Width;
if (containerWidthRaw <= 0f)
containerWidthRaw = measuredWidthF;
var containerWidth = containerWidthRaw * containerScale;
if (containerWidth <= 0f)
containerWidth = measuredWidthF;
// container bounds for positions var containerLeft = nameContainer->ScreenX;
var containerLeft = res->ScreenX; var containerRight = containerLeft + containerWidth;
var containerRight = containerLeft + containerWidthScreen; var containerCenter = containerLeft + (containerWidth * 0.5f);
var containerCenter = containerLeft + (containerWidthScreen * 0.5f);
var iconMargin = currentConfig.LightfinderLabelUseIcon var iconMargin = currentConfig.LightfinderLabelUseIcon
? MathF.Min(containerWidthScreen * 0.1f, 14f * worldScaleX) ? System.Math.Min(containerWidth * 0.1f, 14f * containerScale)
: 0f; : 0f;
var offsetXScreen = currentConfig.LightfinderLabelOffsetX * worldScaleX; switch (alignmentType)
// alignment based on config
switch (currentConfig.LabelAlignment)
{ {
case LabelAlignment.Left: case LabelAlignment.Left:
finalX = containerLeft + iconMargin + offsetXScreen; finalX = containerLeft + iconMargin;
alignment = AlignmentType.BottomLeft; alignment = AlignmentType.BottomLeft;
break; break;
case LabelAlignment.Right: case LabelAlignment.Right:
finalX = containerRight - iconMargin + offsetXScreen; finalX = containerRight - iconMargin;
alignment = AlignmentType.BottomRight; alignment = AlignmentType.BottomRight;
break; break;
default: default:
finalX = containerCenter + offsetXScreen; finalX = containerCenter;
alignment = AlignmentType.Bottom; alignment = AlignmentType.Bottom;
break; break;
} }
finalX += currentConfig.LightfinderLabelOffsetX;
} }
else else
{ {
// manual X positioning
var cachedTextOffset = _buffers.TextOffsets[nameplateIndex]; var cachedTextOffset = _buffers.TextOffsets[nameplateIndex];
var hasCachedOffset = cachedTextOffset != int.MinValue; var hasCachedOffset = cachedTextOffset != int.MinValue;
var baseOffsetXLocal = (!currentConfig.LightfinderLabelUseIcon && hasValidOffset && hasCachedOffset) var baseOffsetX = (!currentConfig.LightfinderLabelUseIcon && hasValidOffset && hasCachedOffset) ? cachedTextOffset : 0;
? cachedTextOffset finalX = nameContainer->ScreenX + baseOffsetX + 58 + currentConfig.LightfinderLabelOffsetX;
: 0;
finalX =
res->ScreenX
+ (baseOffsetXLocal * worldScaleX)
+ (58f * worldScaleX)
+ (currentConfig.LightfinderLabelOffsetX * worldScaleX);
alignment = AlignmentType.Bottom; alignment = AlignmentType.Bottom;
} }
alignment = (AlignmentType)Math.Clamp((int)alignment, 0, 8); positionY += currentConfig.LightfinderLabelOffsetY;
alignment = (AlignmentType)System.Math.Clamp((int)alignment, 0, 8);
// final position before smoothing var finalPosition = new Vector2(finalX, nameContainer->ScreenY + positionY);
var finalPosition = new Vector2(finalX, res->ScreenY + positionYScreen);
var dpiScale = ImGui.GetIO().DisplayFramebufferScale.X; // often same for Y
var fw = Framework.Instance();
float dt = fw->RealFrameDeltaTime;
//smoothing..
finalPosition = SnapToPixels(finalPosition, dpiScale);
finalPosition = SmoothPosition(nameplateIndex, finalPosition, dt);
finalPosition = SnapToPixels(finalPosition, dpiScale);
// prepare label info
var pivot = (currentConfig.LightfinderAutoAlign || currentConfig.LightfinderLabelUseIcon) var pivot = (currentConfig.LightfinderAutoAlign || currentConfig.LightfinderLabelUseIcon)
? AlignmentToPivot(alignment) ? AlignmentToPivot(alignment)
: _defaultPivot; : DefaultPivot;
var textColorPacked = PackColor(labelColor); var textColorPacked = PackColor(labelColor);
var edgeColorPacked = PackColor(edgeColor); var edgeColorPacked = PackColor(edgeColor);
@@ -498,42 +418,11 @@ public unsafe class LightFinderPlateHandler : IHostedService, IMediatorSubscribe
} }
} }
/// <summary>
/// On each tick, process any needed updates for the UI Builder.
/// </summary>
private void OnUiBuilderDraw() private void OnUiBuilderDraw()
{ {
if (!_mEnabled) if (!_mEnabled)
return; return;
var fw = Framework.Instance();
if (fw == null)
return;
// Frame skip check
var frame = fw->FrameCounter;
if (_lastNamePlateDrawFrame == 0 || (frame - _lastNamePlateDrawFrame) > 1)
{
ClearLabelBuffer();
Array.Clear(_buffers.HasSmoothed, 0, _buffers.HasSmoothed.Length);
return;
}
//Gpose Check
if (_clientState.IsGPosing)
{
ClearLabelBuffer();
Array.Clear(_buffers.HasSmoothed, 0, _buffers.HasSmoothed.Length);
_lastNamePlateDrawFrame = 0;
return;
}
// If nameplate addon is not visible, skip rendering
if (!IsNamePlateAddonVisible())
return;
int copyCount; int copyCount;
lock (_labelLock) lock (_labelLock)
{ {
@@ -544,84 +433,21 @@ public unsafe class LightFinderPlateHandler : IHostedService, IMediatorSubscribe
Array.Copy(_buffers.LabelRender, _buffers.LabelCopy, copyCount); Array.Copy(_buffers.LabelRender, _buffers.LabelCopy, copyCount);
} }
var uiModule = fw != null ? fw->GetUIModule() : null;
if (uiModule != null)
{
var rapture = uiModule->GetRaptureAtkModule();
if (rapture != null)
RefreshUiRects(&rapture->RaptureAtkUnitManager);
else
_uiRects.Clear();
}
else
{
_uiRects.Clear();
}
// Needed for imgui overlay viewport for the multi window view.
var vp = ImGui.GetMainViewport();
var vpPos = vp.Pos;
ImGuiHelpers.ForceNextWindowMainViewport();
ImGui.SetNextWindowPos(vp.Pos);
ImGui.SetNextWindowSize(vp.Size);
ImGui.PushStyleVar(ImGuiStyleVar.WindowRounding, 0);
ImGui.PushStyleVar(ImGuiStyleVar.WindowBorderSize, 0);
ImGui.Begin("##LightFinderOverlay", _overlayFlags);
ImGui.PopStyleVar(2);
using var drawList = PictoService.Draw(); using var drawList = PictoService.Draw();
if (drawList == null) if (drawList == null)
{
ImGui.End();
return; return;
}
for (int i = 0; i < copyCount; ++i) for (int i = 0; i < copyCount; ++i)
{ {
ref var info = ref _buffers.LabelCopy[i]; ref var info = ref _buffers.LabelCopy[i];
// final draw position with viewport offset
var drawPos = info.ScreenPosition + vpPos;
var font = default(ImFontPtr); var font = default(ImFontPtr);
if (info.UseIcon) if (info.UseIcon)
{ {
var ioFonts = ImGui.GetIO().Fonts; var ioFonts = ImGui.GetIO().Fonts;
font = ioFonts.Fonts.Size > 1 ? new ImFontPtr(ioFonts.Fonts[1]) : ImGui.GetFont(); font = ioFonts.Fonts.Size > 1 ? new ImFontPtr(ioFonts.Fonts[1]) : ImGui.GetFont();
} }
else
{
font = ImGui.GetFont();
}
if (!font.IsNull) drawList.AddScreenText(info.ScreenPosition, info.Text, info.TextColor, info.FontSize, info.Pivot, info.EdgeColor, font);
ImGui.PushFont(font);
// calculate size for occlusion checking
var baseSize = ImGui.CalcTextSize(info.Text);
var baseFontSize = ImGui.GetFontSize();
if (!font.IsNull)
ImGui.PopFont();
// scale size based on font size
var scale = baseFontSize > 0 ? (info.FontSize / baseFontSize) : 1f;
var size = baseSize * scale;
// label rect for occlusion checking
var topLeft = info.ScreenPosition - new Vector2(size.X * info.Pivot.X, size.Y * info.Pivot.Y);
var labelRect = new RectF(topLeft.X, topLeft.Y, topLeft.X + size.X, topLeft.Y + size.Y);
// occlusion check
if (IsOccludedByAnyUi(labelRect))
continue;
drawList.AddScreenText(drawPos, info.Text, info.TextColor, info.FontSize, info.Pivot, info.EdgeColor, font);
} }
} }
@@ -634,15 +460,15 @@ public unsafe class LightFinderPlateHandler : IHostedService, IMediatorSubscribe
AlignmentType.Top => new Vector2(0.5f, 0f), AlignmentType.Top => new Vector2(0.5f, 0f),
AlignmentType.Left => new Vector2(0f, 0.5f), AlignmentType.Left => new Vector2(0f, 0.5f),
AlignmentType.Right => new Vector2(1f, 0.5f), AlignmentType.Right => new Vector2(1f, 0.5f),
_ => _defaultPivot _ => DefaultPivot
}; };
private static uint PackColor(Vector4 color) private static uint PackColor(Vector4 color)
{ {
var r = (byte)Math.Clamp(color.X * 255f, 0f, 255f); var r = (byte)System.Math.Clamp(color.X * 255f, 0f, 255f);
var g = (byte)Math.Clamp(color.Y * 255f, 0f, 255f); var g = (byte)System.Math.Clamp(color.Y * 255f, 0f, 255f);
var b = (byte)Math.Clamp(color.Z * 255f, 0f, 255f); var b = (byte)System.Math.Clamp(color.Z * 255f, 0f, 255f);
var a = (byte)Math.Clamp(color.W * 255f, 0f, 255f); var a = (byte)System.Math.Clamp(color.W * 255f, 0f, 255f);
return (uint)((a << 24) | (b << 16) | (g << 8) | r); return (uint)((a << 24) | (b << 16) | (g << 8) | r);
} }
@@ -688,19 +514,10 @@ public unsafe class LightFinderPlateHandler : IHostedService, IMediatorSubscribe
if (scale <= 0f) if (scale <= 0f)
scale = 1f; scale = 1f;
var computed = (int)Math.Round(rawWidth * scale); var computed = (int)System.Math.Round(rawWidth * scale);
return Math.Max(1, computed); return System.Math.Max(1, computed);
} }
/// <summary>
/// Resolves a cached value for the given index.
/// </summary>
/// <param name="cache"></param>
/// <param name="index"></param>
/// <param name="rawValue"></param>
/// <param name="fallback"></param>
/// <param name="fallbackWhenZero"></param>
/// <returns></returns>
private static int ResolveCache( private static int ResolveCache(
int[] cache, int[] cache,
int index, int index,
@@ -728,7 +545,7 @@ public unsafe class LightFinderPlateHandler : IHostedService, IMediatorSubscribe
private bool TryCacheTextOffset(int nameplateIndex, int measuredTextWidth, int textOffset) private bool TryCacheTextOffset(int nameplateIndex, int measuredTextWidth, int textOffset)
{ {
if (Math.Abs(measuredTextWidth) > 0 || textOffset != 0) if (System.Math.Abs(measuredTextWidth) > 0 || textOffset != 0)
{ {
_buffers.TextOffsets[nameplateIndex] = textOffset; _buffers.TextOffsets[nameplateIndex] = textOffset;
return true; return true;
@@ -737,193 +554,10 @@ public unsafe class LightFinderPlateHandler : IHostedService, IMediatorSubscribe
return false; return false;
} }
/// <summary>
/// Snapping a position to pixel grid based on DPI scale.
/// </summary>
/// <param name="p">Position</param>
/// <param name="dpiScale">DPI Scale</param>
/// <returns></returns>
private static Vector2 SnapToPixels(Vector2 p, float dpiScale)
{
// snap to pixel grid
var x = MathF.Round(p.X * dpiScale) / dpiScale;
var y = MathF.Round(p.Y * dpiScale) / dpiScale;
return new Vector2(x, y);
}
/// <summary>
/// Smooths the position using exponential smoothing.
/// </summary>
/// <param name="idx">Nameplate Index</param>
/// <param name="target">Final position</param>
/// <param name="dt">Delta Time</param>
/// <param name="responsiveness">How responssive the smooting should be</param>
/// <returns></returns>
private Vector2 SmoothPosition(int idx, Vector2 target, float dt, float responsiveness = 24f)
{
// exponential smoothing
if (!_buffers.HasSmoothed[idx])
{
_buffers.HasSmoothed[idx] = true;
_buffers.SmoothedPos[idx] = target;
return target;
}
// get current smoothed position
var cur = _buffers.SmoothedPos[idx];
// compute smoothing factor
var a = 1f - MathF.Exp(-responsiveness * dt);
// snap if close enough
if (Vector2.DistanceSquared(cur, target) < 0.25f)
return cur;
// lerp towards target
cur = Vector2.Lerp(cur, target, a);
_buffers.SmoothedPos[idx] = cur;
return cur;
}
/// <summary>
/// Tries to get a valid screen rect for the given addon.
/// </summary>
/// <param name="addon">Addon UI</param>
/// <param name="screen">Screen positioning/param>
/// <param name="rect">RectF of Addon</param>
/// <returns></returns>
private static bool TryGetAddonRect(AtkUnitBase* addon, Vector2 screen, out RectF rect)
{
// Addon existence
rect = default;
if (addon == null)
return false;
// Visibility check
var root = addon->RootNode;
if (root == null || !root->IsVisible())
return false;
// Size check
float w = root->Width;
float h = root->Height;
if (w <= 0 || h <= 0)
return false;
// Local scale
float sx = root->ScaleX; if (sx <= 0f) sx = 1f;
float sy = root->ScaleY; if (sy <= 0f) sy = 1f;
// World/composed scale from Transform
float wsx = GetWorldScaleX(root);
float wsy = GetWorldScaleY(root);
if (wsx <= 0f) wsx = 1f;
if (wsy <= 0f) wsy = 1f;
// World scale may include parent scaling; use it if meaningfully different.
float useX = MathF.Abs(wsx - sx) > 0.01f ? wsx : sx;
float useY = MathF.Abs(wsy - sy) > 0.01f ? wsy : sy;
w *= useX;
h *= useY;
if (w < 4f || h < 4f)
return false;
// Screen coords
float l = root->ScreenX;
float t = root->ScreenY;
float r = l + w;
float b = t + h;
// Drop fullscreen-ish / insane rects
if (w >= screen.X * 0.98f && h >= screen.Y * 0.98f)
return false;
// Drop offscreen rects
if (l < -screen.X || t < -screen.Y || r > screen.X * 2f || b > screen.Y * 2f)
return false;
rect = new RectF(l, t, r, b);
return true;
}
/// <summary>
/// Refreshes the cached UI rects for occlusion checking.
/// </summary>
/// <param name="unitMgr">Unit Manager</param>
private void RefreshUiRects(RaptureAtkUnitManager* unitMgr)
{
_uiRects.Clear();
if (unitMgr == null)
return;
var screen = ImGui.GetIO().DisplaySize;
ref var list = ref unitMgr->AllLoadedUnitsList;
var count = (int)list.Count;
for (int i = 0; i < count; i++)
{
var addon = list.Entries[i].Value;
if (addon == null)
continue;
if (_mpNameplateAddon != null && addon == (AtkUnitBase*)_mpNameplateAddon)
continue;
if (TryGetAddonRect(addon, screen, out var r))
_uiRects.Add(r);
}
}
/// <summary>
/// Is the given label rect occluded by any UI rects?
/// </summary>
/// <param name="labelRect">UI/Label Rect</param>
/// <returns>Is occluded or not</returns>
private bool IsOccludedByAnyUi(RectF labelRect)
{
for (int i = 0; i < _uiRects.Count; i++)
{
if (_uiRects[i].Intersects(labelRect))
return true;
}
return false;
}
/// <summary>
/// Gets the world scale X of the given node.
/// </summary>
/// <param name="n">Node</param>
/// <returns>World Scale of node</returns>
private static float GetWorldScaleX(AtkResNode* n)
{
var t = n->Transform;
return MathF.Sqrt(t.M11 * t.M11 + t.M12 * t.M12);
}
/// <summary>
/// Gets the world scale Y of the given node.
/// </summary>
/// <param name="n">Node</param>
/// <returns>World Scale of node</returns>
private static float GetWorldScaleY(AtkResNode* n)
{
var t = n->Transform;
return MathF.Sqrt(t.M21 * t.M21 + t.M22 * t.M22);
}
/// <summary>
/// Normalize an icon glyph input into a valid string.
/// </summary>
/// <param name="rawInput">Raw glyph input</param>
/// <returns>Normalized glyph input</returns>
internal static string NormalizeIconGlyph(string? rawInput) internal static string NormalizeIconGlyph(string? rawInput)
{ {
if (string.IsNullOrWhiteSpace(rawInput)) if (string.IsNullOrWhiteSpace(rawInput))
return _defaultIconGlyph; return DefaultIconGlyph;
var trimmed = rawInput.Trim(); var trimmed = rawInput.Trim();
@@ -941,36 +575,17 @@ public unsafe class LightFinderPlateHandler : IHostedService, IMediatorSubscribe
if (enumerator.MoveNext()) if (enumerator.MoveNext())
return enumerator.Current.ToString(); return enumerator.Current.ToString();
return _defaultIconGlyph; return DefaultIconGlyph;
} }
/// <summary>
/// Is the nameplate addon visible?
/// </summary>
/// <returns>Is it visible?</returns>
private bool IsNamePlateAddonVisible()
{
if (_mpNameplateAddon == null)
return false;
var root = _mpNameplateAddon->AtkUnitBase.RootNode;
return root != null && root->IsVisible();
}
/// <summary>
/// Converts raw icon glyph input into an icon editor string.
/// </summary>
/// <param name="rawInput">Raw icon glyph input</param>
/// <returns>Icon editor string</returns>
internal static string ToIconEditorString(string? rawInput) internal static string ToIconEditorString(string? rawInput)
{ {
var normalized = NormalizeIconGlyph(rawInput); var normalized = NormalizeIconGlyph(rawInput);
var runeEnumerator = normalized.EnumerateRunes(); var runeEnumerator = normalized.EnumerateRunes();
return runeEnumerator.MoveNext() return runeEnumerator.MoveNext()
? runeEnumerator.Current.Value.ToString("X4", CultureInfo.InvariantCulture) ? runeEnumerator.Current.Value.ToString("X4", CultureInfo.InvariantCulture)
: _defaultIconGlyph; : DefaultIconGlyph;
} }
private readonly struct NameplateLabelInfo private readonly struct NameplateLabelInfo
{ {
public NameplateLabelInfo( public NameplateLabelInfo(
@@ -1000,9 +615,6 @@ public unsafe class LightFinderPlateHandler : IHostedService, IMediatorSubscribe
public bool UseIcon { get; } public bool UseIcon { get; }
} }
/// <summary>
/// Visible paired user IDs snapshot.
/// </summary>
private HashSet<ulong> VisibleUserIds private HashSet<ulong> VisibleUserIds
=> [.. _pairUiService.GetSnapshot().PairsByUid.Values => [.. _pairUiService.GetSnapshot().PairsByUid.Values
.Where(u => u.IsVisible && u.PlayerCharacterId != uint.MaxValue) .Where(u => u.IsVisible && u.PlayerCharacterId != uint.MaxValue)
@@ -1022,10 +634,6 @@ public unsafe class LightFinderPlateHandler : IHostedService, IMediatorSubscribe
} }
} }
/// <summary>
/// Update the active broadcasting CIDs.
/// </summary>
/// <param name="cids">Inbound new CIDs</param>
public void UpdateBroadcastingCids(IEnumerable<string> cids) public void UpdateBroadcastingCids(IEnumerable<string> cids)
{ {
var newSet = cids.ToImmutableHashSet(StringComparer.Ordinal); var newSet = cids.ToImmutableHashSet(StringComparer.Ordinal);
@@ -1038,16 +646,10 @@ public unsafe class LightFinderPlateHandler : IHostedService, IMediatorSubscribe
FlagRefresh(); FlagRefresh();
} }
/// <summary>
/// Clears all nameplate related caches.
/// </summary>
public void ClearNameplateCaches() public void ClearNameplateCaches()
{ {
_buffers.Clear(); _buffers.Clear();
ClearLabelBuffer(); ClearLabelBuffer();
Array.Clear(_buffers.HasSmoothed, 0, _buffers.HasSmoothed.Length);
Array.Clear(_buffers.SmoothedPos, 0, _buffers.SmoothedPos.Length);
} }
private sealed class NameplateBuffers private sealed class NameplateBuffers
@@ -1066,10 +668,6 @@ public unsafe class LightFinderPlateHandler : IHostedService, IMediatorSubscribe
public NameplateLabelInfo[] LabelRender { get; } = new NameplateLabelInfo[AddonNamePlate.NumNamePlateObjects]; public NameplateLabelInfo[] LabelRender { get; } = new NameplateLabelInfo[AddonNamePlate.NumNamePlateObjects];
public NameplateLabelInfo[] LabelCopy { get; } = new NameplateLabelInfo[AddonNamePlate.NumNamePlateObjects]; public NameplateLabelInfo[] LabelCopy { get; } = new NameplateLabelInfo[AddonNamePlate.NumNamePlateObjects];
public Vector2[] SmoothedPos = new Vector2[AddonNamePlate.NumNamePlateObjects];
public bool[] HasSmoothed = new bool[AddonNamePlate.NumNamePlateObjects];
public void Clear() public void Clear()
{ {
System.Array.Clear(TextWidths, 0, TextWidths.Length); System.Array.Clear(TextWidths, 0, TextWidths.Length);
@@ -1079,38 +677,16 @@ public unsafe class LightFinderPlateHandler : IHostedService, IMediatorSubscribe
} }
} }
/// <summary>
/// Starts the LightFinder Plate Handler.
/// </summary>
/// <param name="cancellationToken">Cancellation Token</param>
/// <returns>Task Completed</returns>
public Task StartAsync(CancellationToken cancellationToken) public Task StartAsync(CancellationToken cancellationToken)
{ {
Init(); Init();
return Task.CompletedTask; return Task.CompletedTask;
} }
/// <summary>
/// Stops the LightFinder Plate Handler.
/// </summary>
/// <param name="cancellationToken">Cancellation Token</param>
/// <returns>Task Completed</returns>
public Task StopAsync(CancellationToken cancellationToken) public Task StopAsync(CancellationToken cancellationToken)
{ {
Uninit(); Uninit();
return Task.CompletedTask; return Task.CompletedTask;
} }
/// <summary>
/// Rectangle with float coordinates for intersection testing.
/// </summary>
[StructLayout(LayoutKind.Auto)]
private readonly struct RectF
{
public readonly float L, T, R, B;
public RectF(float l, float t, float r, float b) { L = l; T = t; R = r; B = b; }
public bool Intersects(in RectF o) =>
!(R <= o.L || o.R <= L || B <= o.T || o.B <= T);
}
} }

View File

@@ -148,14 +148,10 @@ public class LightFinderScannerService : DisposableMediatorSubscriberBase
private void UpdateSyncshellBroadcasts() private void UpdateSyncshellBroadcasts()
{ {
var now = DateTime.UtcNow; var now = DateTime.UtcNow;
var nearbyCids = GetNearbyHashedCids(out _); var newSet = _broadcastCache
var newSet = nearbyCids.Count == 0 .Where(e => e.Value.IsBroadcasting && e.Value.ExpiryTime > now && !string.IsNullOrEmpty(e.Value.GID))
? new HashSet<string>(StringComparer.Ordinal) .Select(e => e.Key)
: _broadcastCache .ToHashSet(StringComparer.Ordinal);
.Where(e => e.Value.IsBroadcasting && e.Value.ExpiryTime > now && !string.IsNullOrEmpty(e.Value.GID))
.Where(e => nearbyCids.Contains(e.Key))
.Select(e => e.Key)
.ToHashSet(StringComparer.Ordinal);
if (!_syncshellCids.SetEquals(newSet)) if (!_syncshellCids.SetEquals(newSet))
{ {
@@ -167,17 +163,12 @@ public class LightFinderScannerService : DisposableMediatorSubscriberBase
} }
} }
public List<BroadcastStatusInfoDto> GetActiveSyncshellBroadcasts(bool excludeLocal = false) public List<BroadcastStatusInfoDto> GetActiveSyncshellBroadcasts()
{ {
var now = DateTime.UtcNow; var now = DateTime.UtcNow;
var nearbyCids = GetNearbyHashedCids(out var localCid);
if (nearbyCids.Count == 0)
return [];
return [.. _broadcastCache return [.. _broadcastCache
.Where(e => e.Value.IsBroadcasting && e.Value.ExpiryTime > now && !string.IsNullOrEmpty(e.Value.GID)) .Where(e => e.Value.IsBroadcasting && e.Value.ExpiryTime > now && !string.IsNullOrEmpty(e.Value.GID))
.Where(e => nearbyCids.Contains(e.Key))
.Where(e => !excludeLocal || !string.Equals(e.Key, localCid, StringComparison.Ordinal))
.Select(e => new BroadcastStatusInfoDto .Select(e => new BroadcastStatusInfoDto
{ {
HashedCID = e.Key, HashedCID = e.Key,
@@ -187,47 +178,6 @@ public class LightFinderScannerService : DisposableMediatorSubscriberBase
})]; })];
} }
public bool TryGetLocalHashedCid(out string hashedCid)
{
hashedCid = string.Empty;
var descriptors = _actorTracker.PlayerDescriptors;
if (descriptors.Count == 0)
return false;
foreach (var descriptor in descriptors)
{
if (!descriptor.IsLocalPlayer || string.IsNullOrWhiteSpace(descriptor.HashedContentId))
continue;
hashedCid = descriptor.HashedContentId;
return true;
}
return false;
}
private HashSet<string> GetNearbyHashedCids(out string? localCid)
{
localCid = null;
var descriptors = _actorTracker.PlayerDescriptors;
if (descriptors.Count == 0)
return new HashSet<string>(StringComparer.Ordinal);
var set = new HashSet<string>(StringComparer.Ordinal);
foreach (var descriptor in descriptors)
{
if (string.IsNullOrWhiteSpace(descriptor.HashedContentId))
continue;
if (descriptor.IsLocalPlayer)
localCid = descriptor.HashedContentId;
set.Add(descriptor.HashedContentId);
}
return set;
}
private async Task ExpiredBroadcastCleanupLoop() private async Task ExpiredBroadcastCleanupLoop()
{ {
var token = _cleanupCts.Token; var token = _cleanupCts.Token;

View File

@@ -133,26 +133,6 @@ public class DrawUserPair
UiSharedService.AttachToolTip("This reapplies the last received character data to this character"); UiSharedService.AttachToolTip("This reapplies the last received character data to this character");
} }
var isPaused = _pair.UserPair!.OwnPermissions.IsPaused();
if (!isPaused)
{
if (_uiSharedService.IconTextButton(FontAwesomeIcon.Pause, "Toggle Pause State", _menuWidth, true))
{
_ = _apiController.PauseAsync(_pair.UserData);
ImGui.CloseCurrentPopup();
}
UiSharedService.AttachToolTip("Pauses syncing with this user.");
}
else
{
if (_uiSharedService.IconTextButton(FontAwesomeIcon.Play, "Toggle Unpause State", _menuWidth, true))
{
_ = _apiController.UnpauseAsync(_pair.UserData);
ImGui.CloseCurrentPopup();
}
UiSharedService.AttachToolTip("Resumes syncing with this user.");
}
if (_uiSharedService.IconTextButton(FontAwesomeIcon.PlayCircle, "Cycle pause state", _menuWidth, true)) if (_uiSharedService.IconTextButton(FontAwesomeIcon.PlayCircle, "Cycle pause state", _menuWidth, true))
{ {
_ = _apiController.CyclePauseAsync(_pair); _ = _apiController.CyclePauseAsync(_pair);

View File

@@ -164,18 +164,7 @@ public class DownloadUi : WindowMediatorSubscriberBase
const float rounding = 6f; const float rounding = 6f;
var shadowOffset = new Vector2(2, 2); var shadowOffset = new Vector2(2, 2);
List<KeyValuePair<GameObjectHandler, Dictionary<string, FileDownloadStatus>>> transfers; foreach (var transfer in _currentDownloads.ToList())
try
{
transfers = _currentDownloads.ToList();
}
catch (ArgumentException)
{
return;
}
foreach (var transfer in transfers)
{ {
var transferKey = transfer.Key; var transferKey = transfer.Key;
var rawPos = _dalamudUtilService.WorldToScreen(transferKey.GetGameObject()); var rawPos = _dalamudUtilService.WorldToScreen(transferKey.GetGameObject());

File diff suppressed because it is too large Load Diff

View File

@@ -222,9 +222,7 @@ public class AnimatedHeader
if (ImGui.IsItemHovered() && !string.IsNullOrEmpty(button.Tooltip)) if (ImGui.IsItemHovered() && !string.IsNullOrEmpty(button.Tooltip))
{ {
ImGui.PushFont(UiBuilder.DefaultFont);
ImGui.SetTooltip(button.Tooltip); ImGui.SetTooltip(button.Tooltip);
ImGui.PopFont();
} }
currentX -= buttonSize.X + spacing; currentX -= buttonSize.X + spacing;

View File

@@ -297,25 +297,6 @@ public class SyncshellAdminUI : WindowMediatorSubscriberBase
var ownerTab = ImRaii.TabItem("Owner Settings"); var ownerTab = ImRaii.TabItem("Owner Settings");
if (ownerTab) if (ownerTab)
{ {
bool isChatDisabled = perm.IsDisableChat();
ImGui.AlignTextToFramePadding();
ImGui.TextUnformatted("Syncshell Chat");
_uiSharedService.BooleanToColoredIcon(!isChatDisabled);
ImGui.SameLine(230);
using (ImRaii.PushColor(ImGuiCol.Text, isChatDisabled ? UIColors.Get("PairBlue") : UIColors.Get("DimRed")))
{
if (_uiSharedService.IconTextButton(
isChatDisabled ? FontAwesomeIcon.Comment : FontAwesomeIcon.Ban,
isChatDisabled ? "Enable syncshell chat" : "Disable syncshell chat"))
{
perm.SetDisableChat(!isChatDisabled);
_ = _apiController.GroupChangeGroupPermissionState(new(GroupFullInfo.Group, perm));
}
}
UiSharedService.AttachToolTip("Disables syncshell chat for all members.");
ImGuiHelpers.ScaledDummy(6f);
ImGui.AlignTextToFramePadding(); ImGui.AlignTextToFramePadding();
ImGui.TextUnformatted("New Password"); ImGui.TextUnformatted("New Password");
var availableWidth = ImGui.GetWindowContentRegionMax().X - ImGui.GetWindowContentRegionMin().X; var availableWidth = ImGui.GetWindowContentRegionMax().X - ImGui.GetWindowContentRegionMin().X;

View File

@@ -140,10 +140,19 @@ public class SyncshellFinderUI : WindowMediatorSubscriberBase
return; return;
} }
var broadcasts = _broadcastScannerService.GetActiveSyncshellBroadcasts().ToList() ?? []; string? myHashedCid = null;
_broadcastScannerService.TryGetLocalHashedCid(out var localHashedCid); try
{
var cid = _dalamudUtilService.GetCID();
myHashedCid = cid.ToString().GetHash256();
}
catch (Exception ex)
{
_logger.LogDebug(ex, "Failed to get CID, not excluding own broadcast.");
}
var broadcasts = _broadcastScannerService.GetActiveSyncshellBroadcasts().Where(b => !string.Equals(b.HashedCID, myHashedCid, StringComparison.Ordinal)).ToList() ?? [];
var cardData = new List<(GroupJoinDto Shell, string BroadcasterName, bool IsSelfBroadcast)>(); var cardData = new List<(GroupJoinDto Shell, string BroadcasterName)>();
foreach (var shell in _nearbySyncshells) foreach (var shell in _nearbySyncshells)
{ {
@@ -176,15 +185,9 @@ public class SyncshellFinderUI : WindowMediatorSubscriberBase
broadcasterName = !string.IsNullOrEmpty(worldName) broadcasterName = !string.IsNullOrEmpty(worldName)
? $"{name} ({worldName})" ? $"{name} ({worldName})"
: name; : name;
var isSelfBroadcast = !string.IsNullOrEmpty(localHashedCid)
&& string.Equals(broadcast.HashedCID, localHashedCid, StringComparison.Ordinal);
cardData.Add((shell, broadcasterName, isSelfBroadcast));
continue;
} }
cardData.Add((shell, broadcasterName, false)); cardData.Add((shell, broadcasterName));
} }
if (cardData.Count == 0) if (cardData.Count == 0)
@@ -207,7 +210,7 @@ public class SyncshellFinderUI : WindowMediatorSubscriberBase
DrawConfirmation(); DrawConfirmation();
} }
private void DrawSyncshellList(List<(GroupJoinDto Shell, string BroadcasterName, bool IsSelfBroadcast)> listData) private void DrawSyncshellList(List<(GroupJoinDto Shell, string BroadcasterName)> listData)
{ {
const int shellsPerPage = 3; const int shellsPerPage = 3;
var totalPages = (int)Math.Ceiling(listData.Count / (float)shellsPerPage); var totalPages = (int)Math.Ceiling(listData.Count / (float)shellsPerPage);
@@ -224,10 +227,7 @@ public class SyncshellFinderUI : WindowMediatorSubscriberBase
for (int index = firstIndex; index < lastExclusive; index++) for (int index = firstIndex; index < lastExclusive; index++)
{ {
var (shell, broadcasterName, isSelfBroadcast) = listData[index]; var (shell, broadcasterName) = listData[index];
var broadcasterLabel = string.IsNullOrEmpty(broadcasterName)
? (isSelfBroadcast ? "You" : string.Empty)
: (isSelfBroadcast ? $"{broadcasterName} (You)" : broadcasterName);
ImGui.PushID(shell.Group.GID); ImGui.PushID(shell.Group.GID);
float rowHeight = 74f * ImGuiHelpers.GlobalScale; float rowHeight = 74f * ImGuiHelpers.GlobalScale;
@@ -239,7 +239,7 @@ public class SyncshellFinderUI : WindowMediatorSubscriberBase
var style = ImGui.GetStyle(); var style = ImGui.GetStyle();
float startX = ImGui.GetCursorPosX(); float startX = ImGui.GetCursorPosX();
float regionW = ImGui.GetContentRegionAvail().X; float regionW = ImGui.GetContentRegionAvail().X;
float rightTxtW = ImGui.CalcTextSize(broadcasterLabel).X; float rightTxtW = ImGui.CalcTextSize(broadcasterName).X;
_uiSharedService.MediumText(displayName, UIColors.Get("LightlessPurple")); _uiSharedService.MediumText(displayName, UIColors.Get("LightlessPurple"));
if (ImGui.IsItemHovered()) if (ImGui.IsItemHovered())
@@ -252,7 +252,7 @@ public class SyncshellFinderUI : WindowMediatorSubscriberBase
float rightX = startX + regionW - rightTxtW - style.ItemSpacing.X; float rightX = startX + regionW - rightTxtW - style.ItemSpacing.X;
ImGui.SameLine(); ImGui.SameLine();
ImGui.SetCursorPosX(rightX); ImGui.SetCursorPosX(rightX);
ImGui.TextUnformatted(broadcasterLabel); ImGui.TextUnformatted(broadcasterName);
if (ImGui.IsItemHovered()) if (ImGui.IsItemHovered())
ImGui.SetTooltip("Broadcaster of the syncshell."); ImGui.SetTooltip("Broadcaster of the syncshell.");
@@ -291,7 +291,7 @@ public class SyncshellFinderUI : WindowMediatorSubscriberBase
float joinX = rowStartLocal.X + (tagsWidth > 0 ? tagsWidth + style.ItemSpacing.X : 0f); float joinX = rowStartLocal.X + (tagsWidth > 0 ? tagsWidth + style.ItemSpacing.X : 0f);
ImGui.SetCursorPos(new Vector2(joinX, btnBaselineY)); ImGui.SetCursorPos(new Vector2(joinX, btnBaselineY));
DrawJoinButton(shell, isSelfBroadcast); DrawJoinButton(shell);
float btnHeight = ImGui.GetFrameHeightWithSpacing(); float btnHeight = ImGui.GetFrameHeightWithSpacing();
float rowHeightUsed = MathF.Max(tagsHeight, btnHeight); float rowHeightUsed = MathF.Max(tagsHeight, btnHeight);
@@ -311,7 +311,7 @@ public class SyncshellFinderUI : WindowMediatorSubscriberBase
DrawPagination(totalPages); DrawPagination(totalPages);
} }
private void DrawSyncshellGrid(List<(GroupJoinDto Shell, string BroadcasterName, bool IsSelfBroadcast)> cardData) private void DrawSyncshellGrid(List<(GroupJoinDto Shell, string BroadcasterName)> cardData)
{ {
const int shellsPerPage = 4; const int shellsPerPage = 4;
var totalPages = (int)Math.Ceiling(cardData.Count / (float)shellsPerPage); var totalPages = (int)Math.Ceiling(cardData.Count / (float)shellsPerPage);
@@ -336,10 +336,7 @@ public class SyncshellFinderUI : WindowMediatorSubscriberBase
for (int index = firstIndex; index < lastExclusive; index++) for (int index = firstIndex; index < lastExclusive; index++)
{ {
var localIndex = index - firstIndex; var localIndex = index - firstIndex;
var (shell, broadcasterName, isSelfBroadcast) = cardData[index]; var (shell, broadcasterName) = cardData[index];
var broadcasterLabel = string.IsNullOrEmpty(broadcasterName)
? (isSelfBroadcast ? "You" : string.Empty)
: (isSelfBroadcast ? $"{broadcasterName} (You)" : broadcasterName);
if (localIndex % 2 != 0) if (localIndex % 2 != 0)
ImGui.SameLine(); ImGui.SameLine();
@@ -376,17 +373,17 @@ public class SyncshellFinderUI : WindowMediatorSubscriberBase
float maxBroadcasterWidth = regionRightX - minBroadcasterX; float maxBroadcasterWidth = regionRightX - minBroadcasterX;
string broadcasterToShow = broadcasterLabel; string broadcasterToShow = broadcasterName;
if (!string.IsNullOrEmpty(broadcasterLabel) && maxBroadcasterWidth > 0f) if (!string.IsNullOrEmpty(broadcasterName) && maxBroadcasterWidth > 0f)
{ {
float bcFullWidth = ImGui.CalcTextSize(broadcasterLabel).X; float bcFullWidth = ImGui.CalcTextSize(broadcasterName).X;
string toolTip; string toolTip;
if (bcFullWidth > maxBroadcasterWidth) if (bcFullWidth > maxBroadcasterWidth)
{ {
broadcasterToShow = TruncateTextToWidth(broadcasterLabel, maxBroadcasterWidth); broadcasterToShow = TruncateTextToWidth(broadcasterName, maxBroadcasterWidth);
toolTip = broadcasterLabel + Environment.NewLine + Environment.NewLine + "Broadcaster of the syncshell."; toolTip = broadcasterName + Environment.NewLine + Environment.NewLine + "Broadcaster of the syncshell.";
} }
else else
{ {
@@ -446,7 +443,7 @@ public class SyncshellFinderUI : WindowMediatorSubscriberBase
if (remainingY > 0) if (remainingY > 0)
ImGui.Dummy(new Vector2(0, remainingY)); ImGui.Dummy(new Vector2(0, remainingY));
DrawJoinButton(shell, isSelfBroadcast); DrawJoinButton(shell);
ImGui.EndChild(); ImGui.EndChild();
ImGui.EndGroup(); ImGui.EndGroup();
@@ -492,7 +489,7 @@ public class SyncshellFinderUI : WindowMediatorSubscriberBase
} }
} }
private void DrawJoinButton(GroupJoinDto shell, bool isSelfBroadcast) private void DrawJoinButton(dynamic shell)
{ {
const string visibleLabel = "Join"; const string visibleLabel = "Join";
var label = $"{visibleLabel}##{shell.Group.GID}"; var label = $"{visibleLabel}##{shell.Group.GID}";
@@ -520,7 +517,7 @@ public class SyncshellFinderUI : WindowMediatorSubscriberBase
buttonSize = new Vector2(-1, 0); buttonSize = new Vector2(-1, 0);
} }
if (!isAlreadyMember && !isRecentlyJoined && !isSelfBroadcast) if (!isAlreadyMember && !isRecentlyJoined)
{ {
ImGui.PushStyleColor(ImGuiCol.Button, UIColors.Get("LightlessGreen")); ImGui.PushStyleColor(ImGuiCol.Button, UIColors.Get("LightlessGreen"));
ImGui.PushStyleColor(ImGuiCol.ButtonHovered, UIColors.Get("LightlessGreen").WithAlpha(0.85f)); ImGui.PushStyleColor(ImGuiCol.ButtonHovered, UIColors.Get("LightlessGreen").WithAlpha(0.85f));
@@ -570,9 +567,7 @@ public class SyncshellFinderUI : WindowMediatorSubscriberBase
ImGui.Button(label, buttonSize); ImGui.Button(label, buttonSize);
} }
UiSharedService.AttachToolTip(isSelfBroadcast UiSharedService.AttachToolTip("Already a member or owner of this Syncshell.");
? "This is your own Syncshell."
: "Already a member or owner of this Syncshell.");
} }
ImGui.PopStyleColor(3); ImGui.PopStyleColor(3);

View File

@@ -800,9 +800,22 @@ public class TopTabMenu
if (!_lightFinderService.IsBroadcasting) if (!_lightFinderService.IsBroadcasting)
return "Syncshell Finder"; return "Syncshell Finder";
string? myHashedCid = null;
try
{
var cid = _dalamudUtilService.GetCID();
myHashedCid = cid.ToString().GetHash256();
}
catch (Exception)
{
// Couldnt get own CID, log and return default table
}
var nearbyCount = _lightFinderScannerService var nearbyCount = _lightFinderScannerService
.GetActiveSyncshellBroadcasts(excludeLocal: true) .GetActiveSyncshellBroadcasts()
.Where(b => !string.IsNullOrEmpty(b.GID)) .Where(b =>
!string.IsNullOrEmpty(b.GID) &&
!string.Equals(b.HashedCID, myHashedCid, StringComparison.Ordinal))
.Select(b => b.GID!) .Select(b => b.GID!)
.Distinct(StringComparer.Ordinal) .Distinct(StringComparer.Ordinal)
.Count(); .Count();

View File

@@ -947,16 +947,13 @@ public partial class UiSharedService : DisposableMediatorSubscriberBase
} }
} }
if (_discordOAuthCheck != null && _discordOAuthCheck.IsCompleted && _discordOAuthCheck.Result != null) if (_discordOAuthCheck != null && _discordOAuthCheck.IsCompleted)
{ {
if (_discordOAuthGetCode == null) if (IconTextButton(FontAwesomeIcon.ArrowRight, "Authenticate with Server"))
{ {
if (IconTextButton(FontAwesomeIcon.ArrowRight, "Authenticate with Server")) _discordOAuthGetCode = _serverConfigurationManager.GetDiscordOAuthToken(_discordOAuthCheck.Result!, selectedServer.ServerUri, _discordOAuthGetCts.Token);
{
_discordOAuthGetCode = _serverConfigurationManager.GetDiscordOAuthToken(_discordOAuthCheck.Result, selectedServer.ServerUri, _discordOAuthGetCts.Token);
}
} }
else if (!_discordOAuthGetCode.IsCompleted) else if (_discordOAuthGetCode != null && !_discordOAuthGetCode.IsCompleted)
{ {
TextWrapped("A browser window has been opened, follow it to authenticate. Click the button below if you accidentally closed the window and need to restart the authentication."); TextWrapped("A browser window has been opened, follow it to authenticate. Click the button below if you accidentally closed the window and need to restart the authentication.");
if (IconTextButton(FontAwesomeIcon.Ban, "Cancel Authentication")) if (IconTextButton(FontAwesomeIcon.Ban, "Cancel Authentication"))
@@ -965,7 +962,7 @@ public partial class UiSharedService : DisposableMediatorSubscriberBase
_discordOAuthGetCode = null; _discordOAuthGetCode = null;
} }
} }
else else if (_discordOAuthGetCode != null && _discordOAuthGetCode.IsCompleted)
{ {
TextWrapped("Discord OAuth is completed, status: "); TextWrapped("Discord OAuth is completed, status: ");
ImGui.SameLine(); ImGui.SameLine();

View File

@@ -17,7 +17,6 @@ using LightlessSync.Services.ServerConfiguration;
using LightlessSync.UI.Services; using LightlessSync.UI.Services;
using LightlessSync.UI.Style; using LightlessSync.UI.Style;
using LightlessSync.Utils; using LightlessSync.Utils;
using OtterGui.Text;
using LightlessSync.WebAPI; using LightlessSync.WebAPI;
using LightlessSync.WebAPI.SignalR.Utils; using LightlessSync.WebAPI.SignalR.Utils;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
@@ -83,7 +82,6 @@ public sealed class ZoneChatUi : WindowMediatorSubscriberBase
private string? _dragHoverKey; private string? _dragHoverKey;
private bool _HideStateActive; private bool _HideStateActive;
private bool _HideStateWasOpen; private bool _HideStateWasOpen;
private bool _pushedStyle;
public ZoneChatUi( public ZoneChatUi(
ILogger<ZoneChatUi> logger, ILogger<ZoneChatUi> logger,
@@ -141,8 +139,6 @@ public sealed class ZoneChatUi : WindowMediatorSubscriberBase
var config = _chatConfigService.Current; var config = _chatConfigService.Current;
var baseOpacity = Math.Clamp(config.ChatWindowOpacity, MinWindowOpacity, MaxWindowOpacity); var baseOpacity = Math.Clamp(config.ChatWindowOpacity, MinWindowOpacity, MaxWindowOpacity);
_baseWindowOpacity = baseOpacity; _baseWindowOpacity = baseOpacity;
ImGui.PushStyleVar(ImGuiStyleVar.WindowBorderSize, 0);
_pushedStyle = true;
if (config.FadeWhenUnfocused) if (config.FadeWhenUnfocused)
{ {
@@ -212,6 +208,12 @@ public sealed class ZoneChatUi : WindowMediatorSubscriberBase
protected override void DrawInternal() protected override void DrawInternal()
{ {
if (_titleBarStylePopCount > 0)
{
ImGui.PopStyleColor(_titleBarStylePopCount);
_titleBarStylePopCount = 0;
}
var config = _chatConfigService.Current; var config = _chatConfigService.Current;
var isFocused = ImGui.IsWindowFocused(ImGuiFocusedFlags.RootAndChildWindows); var isFocused = ImGui.IsWindowFocused(ImGuiFocusedFlags.RootAndChildWindows);
var isHovered = ImGui.IsWindowHovered(ImGuiHoveredFlags.RootAndChildWindows); var isHovered = ImGui.IsWindowHovered(ImGuiHoveredFlags.RootAndChildWindows);
@@ -395,57 +397,52 @@ public sealed class ZoneChatUi : WindowMediatorSubscriberBase
} }
else else
{ {
var itemHeight = ImGui.GetTextLineHeightWithSpacing(); for (var i = 0; i < channel.Messages.Count; i++)
using var clipper = ImUtf8.ListClipper(channel.Messages.Count, itemHeight);
while (clipper.Step())
{ {
for (var i = clipper.DisplayStart; i < clipper.DisplayEnd; i++) var message = channel.Messages[i];
ImGui.PushID(i);
if (message.IsSystem)
{ {
var message = channel.Messages[i]; DrawSystemEntry(message);
ImGui.PushID(i);
if (message.IsSystem)
{
DrawSystemEntry(message);
ImGui.PopID();
continue;
}
if (message.Payload is not { } payload)
{
ImGui.PopID();
continue;
}
var timestampText = string.Empty;
if (showTimestamps)
{
timestampText = $"[{message.ReceivedAtUtc.ToLocalTime().ToString("HH:mm", CultureInfo.InvariantCulture)}] ";
}
var color = message.FromSelf ? UIColors.Get("LightlessBlue") : ImGuiColors.DalamudWhite;
ImGui.PushStyleColor(ImGuiCol.Text, color);
ImGui.TextWrapped($"{timestampText}{message.DisplayName}: {payload.Message}");
ImGui.PopStyleColor();
if (ImGui.BeginPopupContextItem($"chat_msg_ctx##{channel.Key}_{i}"))
{
var contextLocalTimestamp = payload.SentAtUtc.ToLocalTime();
var contextTimestampText = contextLocalTimestamp.ToString("yyyy-MM-dd HH:mm:ss 'UTC'z", CultureInfo.InvariantCulture);
ImGui.TextDisabled(contextTimestampText);
ImGui.Separator();
var actionIndex = 0;
foreach (var action in GetContextMenuActions(channel, message))
{
DrawContextMenuAction(action, actionIndex++);
}
ImGui.EndPopup();
}
ImGui.PopID(); ImGui.PopID();
continue;
} }
if (message.Payload is not { } payload)
{
ImGui.PopID();
continue;
}
var timestampText = string.Empty;
if (showTimestamps)
{
timestampText = $"[{message.ReceivedAtUtc.ToLocalTime().ToString("HH:mm", CultureInfo.InvariantCulture)}] ";
}
var color = message.FromSelf ? UIColors.Get("LightlessBlue") : ImGuiColors.DalamudWhite;
ImGui.PushStyleColor(ImGuiCol.Text, color);
ImGui.TextWrapped($"{timestampText}{message.DisplayName}: {payload.Message}");
ImGui.PopStyleColor();
if (ImGui.BeginPopupContextItem($"chat_msg_ctx##{channel.Key}_{i}"))
{
var contextLocalTimestamp = payload.SentAtUtc.ToLocalTime();
var contextTimestampText = contextLocalTimestamp.ToString("yyyy-MM-dd HH:mm:ss 'UTC'z", CultureInfo.InvariantCulture);
ImGui.TextDisabled(contextTimestampText);
ImGui.Separator();
var actionIndex = 0;
foreach (var action in GetContextMenuActions(channel, message))
{
DrawContextMenuAction(action, actionIndex++);
}
ImGui.EndPopup();
}
ImGui.PopID();
} }
} }
@@ -826,21 +823,6 @@ public sealed class ZoneChatUi : WindowMediatorSubscriberBase
ImGui.EndPopup(); ImGui.EndPopup();
} }
public override void PostDraw()
{
if (_pushedStyle)
{
ImGui.PopStyleVar(1);
_pushedStyle = false;
}
if (_titleBarStylePopCount > 0)
{
ImGui.PopStyleColor(_titleBarStylePopCount);
_titleBarStylePopCount = 0;
}
base.PostDraw();
}
private void OpenReportPopup(ChatChannelSnapshot channel, ChatMessageEntry message) private void OpenReportPopup(ChatChannelSnapshot channel, ChatMessageEntry message)
{ {
if (message.Payload is not { } payload) if (message.Payload is not { } payload)

View File

@@ -60,6 +60,16 @@ public static class VariousExtensions
CharacterData? oldData, ILogger logger, IPairPerformanceSubject cachedPlayer, bool forceApplyCustomization, bool forceApplyMods) CharacterData? oldData, ILogger logger, IPairPerformanceSubject cachedPlayer, bool forceApplyCustomization, bool forceApplyMods)
{ {
oldData ??= new(); oldData ??= new();
static bool FileReplacementsEquivalent(ICollection<FileReplacementData> left, ICollection<FileReplacementData> right)
{
if (left.Count != right.Count)
{
return false;
}
var comparer = LightlessSync.PlayerData.Data.FileReplacementDataComparer.Instance;
return !left.Except(right, comparer).Any() && !right.Except(left, comparer).Any();
}
var charaDataToUpdate = new Dictionary<ObjectKind, HashSet<PlayerChanges>>(); var charaDataToUpdate = new Dictionary<ObjectKind, HashSet<PlayerChanges>>();
foreach (ObjectKind objectKind in Enum.GetValues<ObjectKind>()) foreach (ObjectKind objectKind in Enum.GetValues<ObjectKind>())
@@ -95,7 +105,7 @@ public static class VariousExtensions
{ {
var oldList = oldData.FileReplacements[objectKind]; var oldList = oldData.FileReplacements[objectKind];
var newList = newData.FileReplacements[objectKind]; var newList = newData.FileReplacements[objectKind];
var listsAreEqual = oldList.SequenceEqual(newList, PlayerData.Data.FileReplacementDataComparer.Instance); var listsAreEqual = FileReplacementsEquivalent(oldList, newList);
if (!listsAreEqual || forceApplyMods) if (!listsAreEqual || forceApplyMods)
{ {
logger.LogDebug("[BASE-{appBase}] Updating {object}/{kind} (FileReplacements not equal) => {change}", applicationBase, cachedPlayer, objectKind, PlayerChanges.ModFiles); logger.LogDebug("[BASE-{appBase}] Updating {object}/{kind} (FileReplacements not equal) => {change}", applicationBase, cachedPlayer, objectKind, PlayerChanges.ModFiles);
@@ -118,9 +128,9 @@ public static class VariousExtensions
.OrderBy(g => string.IsNullOrEmpty(g.Hash) ? g.FileSwapPath : g.Hash, StringComparer.OrdinalIgnoreCase).ToList(); .OrderBy(g => string.IsNullOrEmpty(g.Hash) ? g.FileSwapPath : g.Hash, StringComparer.OrdinalIgnoreCase).ToList();
var newTail = newFileReplacements.Where(g => g.GamePaths.Any(p => p.Contains("/tail/", StringComparison.OrdinalIgnoreCase))) var newTail = newFileReplacements.Where(g => g.GamePaths.Any(p => p.Contains("/tail/", StringComparison.OrdinalIgnoreCase)))
.OrderBy(g => string.IsNullOrEmpty(g.Hash) ? g.FileSwapPath : g.Hash, StringComparer.OrdinalIgnoreCase).ToList(); .OrderBy(g => string.IsNullOrEmpty(g.Hash) ? g.FileSwapPath : g.Hash, StringComparer.OrdinalIgnoreCase).ToList();
var existingTransients = existingFileReplacements.Where(g => g.GamePaths.Any(g => !g.EndsWith("mdl") && !g.EndsWith("tex") && !g.EndsWith("mtrl"))) var existingTransients = existingFileReplacements.Where(g => g.GamePaths.Any(g => !g.EndsWith("mdl", StringComparison.OrdinalIgnoreCase) && !g.EndsWith("tex", StringComparison.OrdinalIgnoreCase) && !g.EndsWith("mtrl", StringComparison.OrdinalIgnoreCase)))
.OrderBy(g => string.IsNullOrEmpty(g.Hash) ? g.FileSwapPath : g.Hash, StringComparer.OrdinalIgnoreCase).ToList(); .OrderBy(g => string.IsNullOrEmpty(g.Hash) ? g.FileSwapPath : g.Hash, StringComparer.OrdinalIgnoreCase).ToList();
var newTransients = newFileReplacements.Where(g => g.GamePaths.Any(g => !g.EndsWith("mdl") && !g.EndsWith("tex") && !g.EndsWith("mtrl"))) var newTransients = newFileReplacements.Where(g => g.GamePaths.Any(g => !g.EndsWith("mdl", StringComparison.OrdinalIgnoreCase) && !g.EndsWith("tex", StringComparison.OrdinalIgnoreCase) && !g.EndsWith("mtrl", StringComparison.OrdinalIgnoreCase)))
.OrderBy(g => string.IsNullOrEmpty(g.Hash) ? g.FileSwapPath : g.Hash, StringComparer.OrdinalIgnoreCase).ToList(); .OrderBy(g => string.IsNullOrEmpty(g.Hash) ? g.FileSwapPath : g.Hash, StringComparer.OrdinalIgnoreCase).ToList();
logger.LogTrace("[BASE-{appbase}] ExistingFace: {of}, NewFace: {fc}; ExistingHair: {eh}, NewHair: {nh}; ExistingTail: {et}, NewTail: {nt}; ExistingTransient: {etr}, NewTransient: {ntr}", applicationBase, logger.LogTrace("[BASE-{appbase}] ExistingFace: {of}, NewFace: {fc}; ExistingHair: {eh}, NewHair: {nh}; ExistingTail: {et}, NewTail: {nt}; ExistingTransient: {etr}, NewTransient: {ntr}", applicationBase,
@@ -167,7 +177,8 @@ public static class VariousExtensions
if (objectKind != ObjectKind.Player) continue; if (objectKind != ObjectKind.Player) continue;
bool manipDataDifferent = !string.Equals(oldData.ManipulationData, newData.ManipulationData, StringComparison.Ordinal); bool manipDataDifferent = !string.Equals(oldData.ManipulationData, newData.ManipulationData, StringComparison.Ordinal);
if (manipDataDifferent || forceApplyMods) var hasManipulationData = !string.IsNullOrEmpty(newData.ManipulationData);
if (manipDataDifferent || (forceApplyMods && hasManipulationData))
{ {
logger.LogDebug("[BASE-{appBase}] Updating {object}/{kind} (Diff manip data) => {change}", applicationBase, cachedPlayer, objectKind, PlayerChanges.ModManip); logger.LogDebug("[BASE-{appBase}] Updating {object}/{kind} (Diff manip data) => {change}", applicationBase, cachedPlayer, objectKind, PlayerChanges.ModManip);
charaDataToUpdate[objectKind].Add(PlayerChanges.ModManip); charaDataToUpdate[objectKind].Add(PlayerChanges.ModManip);

View File

@@ -563,7 +563,7 @@ public partial class FileDownloadManager : DisposableMediatorSubscriberBase
if (directDownloads.Count > 0 || downloadBatches.Length > 0) if (directDownloads.Count > 0 || downloadBatches.Length > 0)
{ {
Logger.LogInformation("Downloading {direct} files directly, and {batchtotal} in {batches} batches.", directDownloads.Count, batchDownloads.Count, downloadBatches.Length); Logger.LogWarning("Downloading {direct} files directly, and {batchtotal} in {batches} batches.", directDownloads.Count, batchDownloads.Count, downloadBatches.Length);
} }
if (gameObjectHandler is not null) if (gameObjectHandler is not null)

View File

@@ -418,7 +418,7 @@ public sealed partial class ApiController : DisposableMediatorSubscriberBase, IL
public Task CyclePauseAsync(PairUniqueIdentifier ident) public Task CyclePauseAsync(PairUniqueIdentifier ident)
{ {
var timeoutCts = new CancellationTokenSource(TimeSpan.FromSeconds(5)); var timeoutCts = new CancellationTokenSource(TimeSpan.FromSeconds(8));
_ = Task.Run(async () => _ = Task.Run(async () =>
{ {
var token = timeoutCts.Token; var token = timeoutCts.Token;
@@ -430,19 +430,20 @@ public sealed partial class ApiController : DisposableMediatorSubscriberBase, IL
return; return;
} }
var targetPermissions = entry.SelfPermissions; var originalPermissions = entry.SelfPermissions;
targetPermissions.SetPaused(paused: true); var targetPermissions = originalPermissions;
targetPermissions.SetPaused(!originalPermissions.IsPaused());
await UserSetPairPermissions(new UserPermissionsDto(entry.User, targetPermissions)).ConfigureAwait(false); await UserSetPairPermissions(new UserPermissionsDto(entry.User, targetPermissions)).ConfigureAwait(false);
var pauseApplied = false; var applied = false;
while (!token.IsCancellationRequested) while (!token.IsCancellationRequested)
{ {
if (_pairCoordinator.Ledger.TryGetEntry(ident, out var updated) && updated is not null) if (_pairCoordinator.Ledger.TryGetEntry(ident, out var updated) && updated is not null)
{ {
if (updated.SelfPermissions == targetPermissions) if (updated.SelfPermissions == targetPermissions)
{ {
pauseApplied = true; applied = true;
entry = updated; entry = updated;
break; break;
} }
@@ -452,16 +453,13 @@ public sealed partial class ApiController : DisposableMediatorSubscriberBase, IL
Logger.LogTrace("Waiting for permissions change for {uid}", ident.UserId); Logger.LogTrace("Waiting for permissions change for {uid}", ident.UserId);
} }
if (!pauseApplied) if (!applied)
{ {
Logger.LogWarning("CyclePauseAsync timed out waiting for pause acknowledgement for {uid}", ident.UserId); Logger.LogWarning("CyclePauseAsync timed out waiting for pause acknowledgement for {uid}", ident.UserId);
return; return;
} }
targetPermissions.SetPaused(paused: false); Logger.LogDebug("CyclePauseAsync toggled paused for {uid} to {state}", ident.UserId, targetPermissions.IsPaused());
await UserSetPairPermissions(new UserPermissionsDto(entry.User, targetPermissions)).ConfigureAwait(false);
Logger.LogDebug("CyclePauseAsync completed pause cycle for {uid}", ident.UserId);
} }
catch (OperationCanceledException) catch (OperationCanceledException)
{ {
@@ -481,26 +479,16 @@ public sealed partial class ApiController : DisposableMediatorSubscriberBase, IL
} }
public async Task PauseAsync(UserData userData) public async Task PauseAsync(UserData userData)
{
await SetPausedStateAsync(userData, paused: true).ConfigureAwait(false);
}
public async Task UnpauseAsync(UserData userData)
{
await SetPausedStateAsync(userData, paused: false).ConfigureAwait(false);
}
private async Task SetPausedStateAsync(UserData userData, bool paused)
{ {
var pairIdent = new PairUniqueIdentifier(userData.UID); var pairIdent = new PairUniqueIdentifier(userData.UID);
if (!_pairCoordinator.Ledger.TryGetEntry(pairIdent, out var entry) || entry is null) if (!_pairCoordinator.Ledger.TryGetEntry(pairIdent, out var entry) || entry is null)
{ {
Logger.LogWarning("SetPausedStateAsync: pair {uid} not found in ledger", userData.UID); Logger.LogWarning("PauseAsync: pair {uid} not found in ledger", userData.UID);
return; return;
} }
var permissions = entry.SelfPermissions; var permissions = entry.SelfPermissions;
permissions.SetPaused(paused); permissions.SetPaused(paused: true);
await UserSetPairPermissions(new UserPermissionsDto(userData, permissions)).ConfigureAwait(false); await UserSetPairPermissions(new UserPermissionsDto(userData, permissions)).ConfigureAwait(false);
} }