Compare commits

..

174 Commits

Author SHA1 Message Date
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
72 changed files with 2622 additions and 8186 deletions

3
.gitignore vendored
View File

@@ -348,6 +348,3 @@ MigrationBackup/
# Ionide (cross platform F# VS Code tools) working folder
.ionide/
# idea
/.idea

View File

@@ -1,72 +1,11 @@
tagline: "Lightless Sync v2.0.1"
tagline: "Lightless Sync v2.0.0"
subline: "LIGHTLESS IS EVOLVING!!"
changelog:
- name: "v2.0.2"
tagline: "Last update of 2025!... ... ... If Nothing breaks"
date: "December 28 2025"
# be sure to set this every new version
isCurrent: true
versions:
- number: "Chat"
icon: ""
items:
- "Added a 7TV emote picker to chat. Youll now see a new button next to Send that opens an emote selector."
- "Pin User, Remove User, and Ban User (including Syncshell) have been added when you right click a user in chat."
- "Chatters now show status icons/labels in the Syncshell (e.g., Owner, Moderator, and Pinned when applicable)."
- "The Rules page no longer blocks input for other open Lightless UI windows."
- number: "LightFinder"
icon: ""
items:
- "If the ImGui Lightfinder icons arent working correctly, you can switch back to the Nameplate signature hook. Important warning - USE AT YOUR OWN RISK: The native nameplate hook can crash the game if multiple plugins hook the nameplate function at the same time. We will not provide support about Nameplate crashes, nor will the Dalamud team, **DO NOT BOTHER THEM.**"
- "The LightFinder label in the menu has a counter next to it showing the number of broadcasting users."
- "There is less interference of hidden UI elements for the imGui renderer of LightFinder."
- number: "Miscellaneous fixes"
icon: ""
items:
- "Overhauled transient resources in an attempt to mitigate mount and minion problems."
- "Some file cache entries will now be cached to reduce load on your game."
- "Downloading and decompressing have been redone to fix the locking issues."
- "Disabling the context menu will now hide the context menu on right clicks again. (Thanks @infiniti)"
- "Temporary collections that were not cleared before will now be cleared when the plugin starts."
- "Pair requests will now appear in chat if notifications are not enabled or on chat mode."
- "Fixed an instance were an object may be null in the Download UI."
- "API 14 - Migrate to IPlayerState service"
- name: "v2.0.1"
tagline: "Some Fixes"
date: "December 23 2025"
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"
tagline: "Thank you for 4 months!"
date: "December 2025"
# be sure to set this every new version
isCurrent: true
versions:
- number: "Lightless Chat"
icon: ""

View File

@@ -6,7 +6,6 @@ using LightlessSync.Utils;
using Microsoft.Extensions.Logging;
using System.Collections.Concurrent;
using System.Collections.Immutable;
using System.IO;
namespace LightlessSync.FileCache;
@@ -22,7 +21,6 @@ public sealed class CacheMonitor : DisposableMediatorSubscriberBase
private CancellationTokenSource _scanCancellationTokenSource = 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"];
private static readonly HashSet<string> AllowedFileExtensionSet = new(AllowedFileExtensions, StringComparer.OrdinalIgnoreCase);
public CacheMonitor(ILogger<CacheMonitor> logger, IpcManager ipcManager, LightlessConfigService configService,
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);
if (!HasAllowedExtension(e.FullPath)) return;
if (!AllowedFileExtensions.Any(ext => e.FullPath.EndsWith(ext, StringComparison.OrdinalIgnoreCase))) return;
lock (_watcherChanges)
{
@@ -209,7 +207,7 @@ public sealed class CacheMonitor : DisposableMediatorSubscriberBase
private void Fs_Changed(object sender, FileSystemEventArgs e)
{
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))
return;
@@ -233,7 +231,7 @@ public sealed class CacheMonitor : DisposableMediatorSubscriberBase
{
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);
_watcherChanges.Remove(oldPath);
@@ -245,7 +243,7 @@ public sealed class CacheMonitor : DisposableMediatorSubscriberBase
}
else
{
if (!HasAllowedExtension(e.FullPath)) return;
if (!AllowedFileExtensions.Any(ext => e.FullPath.EndsWith(ext, StringComparison.OrdinalIgnoreCase))) return;
lock (_watcherChanges)
{
@@ -265,17 +263,6 @@ public sealed class CacheMonitor : DisposableMediatorSubscriberBase
public FileSystemWatcher? PenumbraWatcher { 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()
{
_lightlessFswCts = _lightlessFswCts.CancelRecreate();
@@ -619,7 +606,7 @@ public sealed class CacheMonitor : DisposableMediatorSubscriberBase
[
.. Directory.GetFiles(folder, "*.*", SearchOption.AllDirectories)
.AsParallel()
.Where(f => HasAllowedExtension(f)
.Where(f => AllowedFileExtensions.Any(e => f.EndsWith(e, StringComparison.OrdinalIgnoreCase))
&& !f.Contains(@"\bg\", StringComparison.OrdinalIgnoreCase)
&& !f.Contains(@"\bgcommon\", StringComparison.OrdinalIgnoreCase)
&& !f.Contains(@"\ui\", StringComparison.OrdinalIgnoreCase)),

View File

@@ -1,23 +1,15 @@
#nullable disable
using System.Text.Json.Serialization;
namespace LightlessSync.FileCache;
public class FileCacheEntity
{
[JsonConstructor]
public FileCacheEntity(
string hash,
string prefixedFilePath,
string lastModifiedDateTicks,
long? size = null,
long? compressedSize = null)
public FileCacheEntity(string hash, string path, string lastModifiedDateTicks, long? size = null, long? compressedSize = null)
{
Size = size;
CompressedSize = compressedSize;
Hash = hash;
PrefixedFilePath = prefixedFilePath;
PrefixedFilePath = path;
LastModifiedDateTicks = lastModifiedDateTicks;
}
@@ -31,5 +23,7 @@ public class FileCacheEntity
public long? Size { get; set; }
public void SetResolvedFilePath(string filePath)
=> ResolvedFilepath = filePath.ToLowerInvariant().Replace("\\\\", "\\", StringComparison.Ordinal);
{
ResolvedFilepath = filePath.ToLowerInvariant().Replace("\\\\", "\\", StringComparison.Ordinal);
}
}

View File

@@ -7,8 +7,6 @@ using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
using System.Collections.Concurrent;
using System.Globalization;
using System.IO;
using System.Runtime.InteropServices;
using System.Text;
namespace LightlessSync.FileCache;
@@ -33,14 +31,6 @@ public sealed class FileCacheManager : IHostedService
private bool _csvHeaderEnsured;
public string CacheFolder => _configService.Current.CacheFolder;
private const string _compressedCacheExtension = ".llz4";
private readonly ConcurrentDictionary<string, SemaphoreSlim> _compressLocks = new(StringComparer.OrdinalIgnoreCase);
private readonly ConcurrentDictionary<string, SizeInfo> _sizeCache =
new(StringComparer.OrdinalIgnoreCase);
[StructLayout(LayoutKind.Auto)]
public readonly record struct SizeInfo(long Original, long Compressed);
public FileCacheManager(ILogger<FileCacheManager> logger, IpcManager ipcManager, LightlessConfigService configService, LightlessMediator lightlessMediator)
{
_logger = logger;
@@ -55,18 +45,6 @@ public sealed class FileCacheManager : IHostedService
private static string NormalizeSeparators(string path) => path.Replace("/", "\\", StringComparison.Ordinal)
.Replace("\\\\", "\\", StringComparison.Ordinal);
private SemaphoreSlim GetCompressLock(string hash)
=> _compressLocks.GetOrAdd(hash, _ => new SemaphoreSlim(1, 1));
public void SetSizeInfo(string hash, long original, long compressed)
=> _sizeCache[hash] = new SizeInfo(original, compressed);
public bool TryGetSizeInfo(string hash, out SizeInfo info)
=> _sizeCache.TryGetValue(hash, out info);
private string GetCompressedCachePath(string hash)
=> Path.Combine(CacheFolder, hash + _compressedCacheExtension);
private static string NormalizePrefixedPathKey(string prefixedPath)
{
if (string.IsNullOrEmpty(prefixedPath))
@@ -133,114 +111,6 @@ public sealed class FileCacheManager : IHostedService
return int.TryParse(versionSpan, NumberStyles.Integer, CultureInfo.InvariantCulture, out version);
}
public void UpdateSizeInfo(string hash, long? original = null, long? compressed = null)
{
_sizeCache.AddOrUpdate(
hash,
_ => new SizeInfo(original ?? 0, compressed ?? 0),
(_, old) => new SizeInfo(original ?? old.Original, compressed ?? old.Compressed));
}
private void UpdateEntitiesSizes(string hash, long original, long compressed)
{
if (_fileCaches.TryGetValue(hash, out var dict))
{
foreach (var e in dict.Values)
{
e.Size = original;
e.CompressedSize = compressed;
}
}
}
public static void ApplySizesToEntries(IEnumerable<FileCacheEntity?> entries, long original, long compressed)
{
foreach (var e in entries)
{
if (e == null) continue;
e.Size = original;
e.CompressedSize = compressed > 0 ? compressed : null;
}
}
public async Task<long> GetCompressedSizeAsync(string hash, CancellationToken token)
{
if (_sizeCache.TryGetValue(hash, out var info) && info.Compressed > 0)
return info.Compressed;
if (_fileCaches.TryGetValue(hash, out var dict))
{
var any = dict.Values.FirstOrDefault();
if (any != null && any.CompressedSize > 0)
{
UpdateSizeInfo(hash, original: any.Size > 0 ? any.Size : null, compressed: any.CompressedSize);
return (long)any.CompressedSize;
}
}
if (!string.IsNullOrWhiteSpace(CacheFolder))
{
var path = GetCompressedCachePath(hash);
if (File.Exists(path))
{
var len = new FileInfo(path).Length;
UpdateSizeInfo(hash, compressed: len);
return len;
}
var bytes = await EnsureCompressedCacheBytesAsync(hash, token).ConfigureAwait(false);
return bytes.LongLength;
}
var fallback = await GetCompressedFileData(hash, token).ConfigureAwait(false);
return fallback.Item2.LongLength;
}
private async Task<byte[]> EnsureCompressedCacheBytesAsync(string hash, CancellationToken token)
{
if (string.IsNullOrWhiteSpace(CacheFolder))
throw new InvalidOperationException("CacheFolder is not set; cannot persist compressed cache.");
Directory.CreateDirectory(CacheFolder);
var compressedPath = GetCompressedCachePath(hash);
if (File.Exists(compressedPath))
return await File.ReadAllBytesAsync(compressedPath, token).ConfigureAwait(false);
var sem = GetCompressLock(hash);
await sem.WaitAsync(token).ConfigureAwait(false);
try
{
if (File.Exists(compressedPath))
return await File.ReadAllBytesAsync(compressedPath, token).ConfigureAwait(false);
var entity = GetFileCacheByHash(hash);
if (entity == null || string.IsNullOrWhiteSpace(entity.ResolvedFilepath))
throw new InvalidOperationException($"No local file cache found for hash {hash}.");
var sourcePath = entity.ResolvedFilepath;
var originalSize = new FileInfo(sourcePath).Length;
var raw = await File.ReadAllBytesAsync(sourcePath, token).ConfigureAwait(false);
var compressed = LZ4Wrapper.WrapHC(raw, 0, raw.Length);
var tmpPath = compressedPath + ".tmp";
await File.WriteAllBytesAsync(tmpPath, compressed, token).ConfigureAwait(false);
File.Move(tmpPath, compressedPath, overwrite: true);
var compressedSize = compressed.LongLength;
SetSizeInfo(hash, originalSize, compressedSize);
UpdateEntitiesSizes(hash, originalSize, compressedSize);
return compressed;
}
finally
{
sem.Release();
}
}
private string NormalizeToPrefixedPath(string path)
{
if (string.IsNullOrEmpty(path)) return string.Empty;
@@ -448,18 +318,9 @@ public sealed class FileCacheManager : IHostedService
public async Task<(string, byte[])> GetCompressedFileData(string fileHash, CancellationToken uploadToken)
{
if (!string.IsNullOrWhiteSpace(CacheFolder))
{
var bytes = await EnsureCompressedCacheBytesAsync(fileHash, uploadToken).ConfigureAwait(false);
UpdateSizeInfo(fileHash, compressed: bytes.LongLength);
return (fileHash, bytes);
}
var fileCache = GetFileCacheByHash(fileHash)!.ResolvedFilepath;
var raw = await File.ReadAllBytesAsync(fileCache, uploadToken).ConfigureAwait(false);
var compressed = LZ4Wrapper.WrapHC(raw, 0, raw.Length);
UpdateSizeInfo(fileHash, original: raw.LongLength, compressed: compressed.LongLength);
return (fileHash, compressed);
return (fileHash, LZ4Wrapper.WrapHC(await File.ReadAllBytesAsync(fileCache, uploadToken).ConfigureAwait(false), 0,
(int)new FileInfo(fileCache).Length));
}
public FileCacheEntity? GetFileCacheByHash(string hash)
@@ -1030,14 +891,6 @@ public sealed class FileCacheManager : IHostedService
compressed = resultCompressed;
}
}
if (size > 0 || compressed > 0)
{
UpdateSizeInfo(hash,
original: size > 0 ? size : null,
compressed: compressed > 0 ? compressed : null);
}
AddHashedFile(ReplacePathPrefixes(new FileCacheEntity(hash, path, time, size, compressed)));
}
catch (Exception ex)

View File

@@ -10,6 +10,9 @@ using LightlessSync.Services.Mediator;
using LightlessSync.Utils;
using Microsoft.Extensions.Logging;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Threading.Tasks;
using System.Linq;
using DalamudObjectKind = Dalamud.Game.ClientState.Objects.Enums.ObjectKind;
namespace LightlessSync.FileCache;
@@ -25,10 +28,7 @@ public sealed class TransientResourceManager : DisposableMediatorSubscriberBase
private readonly object _ownedHandlerLock = new();
private readonly string[] _handledFileTypes = ["tmb", "pap", "avfx", "atex", "sklb", "eid", "phyb", "scd", "skp", "shpk", "kdb"];
private readonly string[] _handledRecordingFileTypes = ["tex", "mdl", "mtrl"];
private readonly string[] _handledFileTypesWithRecording;
private readonly HashSet<GameObjectHandler> _playerRelatedPointers = [];
private readonly object _playerRelatedLock = new();
private readonly ConcurrentDictionary<nint, GameObjectHandler> _playerRelatedByAddress = new();
private readonly Dictionary<nint, GameObjectHandler> _ownedHandlers = new();
private ConcurrentDictionary<nint, ObjectKind> _cachedFrameAddresses = new();
private ConcurrentDictionary<ObjectKind, HashSet<string>>? _semiTransientResources = null;
@@ -42,7 +42,6 @@ public sealed class TransientResourceManager : DisposableMediatorSubscriberBase
_dalamudUtil = dalamudUtil;
_actorObjectService = actorObjectService;
_gameObjectHandlerFactory = gameObjectHandlerFactory;
_handledFileTypesWithRecording = _handledRecordingFileTypes.Concat(_handledFileTypes).ToArray();
Mediator.Subscribe<PenumbraResourceLoadMessage>(this, Manager_PenumbraResourceLoadEvent);
Mediator.Subscribe<ActorTrackedMessage>(this, msg => HandleActorTracked(msg.Descriptor));
@@ -52,21 +51,15 @@ public sealed class TransientResourceManager : DisposableMediatorSubscriberBase
Mediator.Subscribe<GameObjectHandlerCreatedMessage>(this, (msg) =>
{
if (!msg.OwnedObject) return;
lock (_playerRelatedLock)
{
_playerRelatedPointers.Add(msg.GameObjectHandler);
}
_playerRelatedPointers.Add(msg.GameObjectHandler);
});
Mediator.Subscribe<GameObjectHandlerDestroyedMessage>(this, (msg) =>
{
if (!msg.OwnedObject) return;
lock (_playerRelatedLock)
{
_playerRelatedPointers.Remove(msg.GameObjectHandler);
}
_playerRelatedPointers.Remove(msg.GameObjectHandler);
});
foreach (var descriptor in _actorObjectService.ObjectDescriptors)
foreach (var descriptor in _actorObjectService.PlayerDescriptors)
{
HandleActorTracked(descriptor);
}
@@ -85,7 +78,7 @@ public sealed class TransientResourceManager : DisposableMediatorSubscriberBase
}
}
private string PlayerPersistentDataKey => _dalamudUtil.GetPlayerName() + "_" + _dalamudUtil.GetHomeWorldId();
private string PlayerPersistentDataKey => _dalamudUtil.GetPlayerNameAsync().GetAwaiter().GetResult() + "_" + _dalamudUtil.GetHomeWorldIdAsync().GetAwaiter().GetResult();
private ConcurrentDictionary<ObjectKind, HashSet<string>> SemiTransientResources
{
get
@@ -94,12 +87,9 @@ public sealed class TransientResourceManager : DisposableMediatorSubscriberBase
{
_semiTransientResources = new();
PlayerConfig.JobSpecificCache.TryGetValue(_dalamudUtil.ClassJobId, out var jobSpecificData);
_semiTransientResources[ObjectKind.Player] = PlayerConfig.GlobalPersistentCache.Concat(jobSpecificData ?? [])
.ToHashSet(StringComparer.OrdinalIgnoreCase);
_semiTransientResources[ObjectKind.Player] = PlayerConfig.GlobalPersistentCache.Concat(jobSpecificData ?? []).ToHashSet(StringComparer.Ordinal);
PlayerConfig.JobSpecificPetCache.TryGetValue(_dalamudUtil.ClassJobId, out var petSpecificData);
_semiTransientResources[ObjectKind.Pet] = new HashSet<string>(
petSpecificData ?? [],
StringComparer.OrdinalIgnoreCase);
_semiTransientResources[ObjectKind.Pet] = [.. petSpecificData ?? []];
}
return _semiTransientResources;
@@ -137,14 +127,14 @@ public sealed class TransientResourceManager : DisposableMediatorSubscriberBase
{
SemiTransientResources.TryGetValue(objectKind, out var result);
return result ?? new HashSet<string>(StringComparer.OrdinalIgnoreCase);
return result ?? new HashSet<string>(StringComparer.Ordinal);
}
public void PersistTransientResources(ObjectKind objectKind)
{
if (!SemiTransientResources.TryGetValue(objectKind, out HashSet<string>? semiTransientResources))
{
SemiTransientResources[objectKind] = semiTransientResources = new(StringComparer.OrdinalIgnoreCase);
SemiTransientResources[objectKind] = semiTransientResources = new(StringComparer.Ordinal);
}
if (!TransientResources.TryGetValue(objectKind, out var resources))
@@ -162,7 +152,7 @@ public sealed class TransientResourceManager : DisposableMediatorSubscriberBase
List<string> newlyAddedGamePaths;
lock (semiTransientResources)
{
newlyAddedGamePaths = transientResources.Except(semiTransientResources, StringComparer.OrdinalIgnoreCase).ToList();
newlyAddedGamePaths = transientResources.Except(semiTransientResources, StringComparer.Ordinal).ToList();
foreach (var gamePath in transientResources)
{
semiTransientResources.Add(gamePath);
@@ -207,13 +197,12 @@ public sealed class TransientResourceManager : DisposableMediatorSubscriberBase
public void RemoveTransientResource(ObjectKind objectKind, string path)
{
var normalizedPath = NormalizeGamePath(path);
if (SemiTransientResources.TryGetValue(objectKind, out var resources))
{
resources.Remove(normalizedPath);
resources.RemoveWhere(f => string.Equals(path, f, StringComparison.Ordinal));
if (objectKind == ObjectKind.Player)
{
PlayerConfig.RemovePath(normalizedPath, objectKind);
PlayerConfig.RemovePath(path, objectKind);
Logger.LogTrace("Saving transient.json from {method}", nameof(RemoveTransientResource));
_configurationService.Save();
}
@@ -222,17 +211,16 @@ public sealed class TransientResourceManager : DisposableMediatorSubscriberBase
internal bool AddTransientResource(ObjectKind objectKind, string item)
{
var normalizedItem = NormalizeGamePath(item);
if (SemiTransientResources.TryGetValue(objectKind, out var semiTransient) && semiTransient != null && semiTransient.Contains(normalizedItem))
if (SemiTransientResources.TryGetValue(objectKind, out var semiTransient) && semiTransient != null && semiTransient.Contains(item))
return false;
if (!TransientResources.TryGetValue(objectKind, out HashSet<string>? transientResource))
{
transientResource = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
transientResource = new HashSet<string>(StringComparer.Ordinal);
TransientResources[objectKind] = transientResource;
}
return transientResource.Add(normalizedItem);
return transientResource.Add(item.ToLowerInvariant());
}
internal void ClearTransientPaths(ObjectKind objectKind, List<string> list)
@@ -297,13 +285,33 @@ public sealed class TransientResourceManager : DisposableMediatorSubscriberBase
private void DalamudUtil_FrameworkUpdate()
{
RefreshPlayerRelatedAddressMap();
lock (_cacheAdditionLock)
{
_cachedHandledPaths.Clear();
}
var activeDescriptors = new Dictionary<nint, ObjectKind>();
foreach (var descriptor in _actorObjectService.PlayerDescriptors)
{
if (TryResolveObjectKind(descriptor, out var resolvedKind))
{
activeDescriptors[descriptor.Address] = resolvedKind;
}
}
foreach (var address in _cachedFrameAddresses.Keys.ToList())
{
if (!activeDescriptors.ContainsKey(address))
{
_cachedFrameAddresses.TryRemove(address, out _);
}
}
foreach (var descriptor in activeDescriptors)
{
_cachedFrameAddresses[descriptor.Key] = descriptor.Value;
}
if (_lastClassJobId != _dalamudUtil.ClassJobId)
{
_lastClassJobId = _dalamudUtil.ClassJobId;
@@ -315,9 +323,7 @@ public sealed class TransientResourceManager : DisposableMediatorSubscriberBase
PlayerConfig.JobSpecificCache.TryGetValue(_dalamudUtil.ClassJobId, out var jobSpecificData);
SemiTransientResources[ObjectKind.Player] = PlayerConfig.GlobalPersistentCache.Concat(jobSpecificData ?? []).ToHashSet(StringComparer.OrdinalIgnoreCase);
PlayerConfig.JobSpecificPetCache.TryGetValue(_dalamudUtil.ClassJobId, out var petSpecificData);
SemiTransientResources[ObjectKind.Pet] = new HashSet<string>(
petSpecificData ?? [],
StringComparer.OrdinalIgnoreCase);
SemiTransientResources[ObjectKind.Pet] = [.. petSpecificData ?? []];
}
foreach (var kind in Enum.GetValues(typeof(ObjectKind)).Cast<ObjectKind>())
@@ -334,12 +340,9 @@ public sealed class TransientResourceManager : DisposableMediatorSubscriberBase
_ = Task.Run(() =>
{
Logger.LogDebug("Penumbra Mod Settings changed, verifying SemiTransientResources");
lock (_playerRelatedLock)
foreach (var item in _playerRelatedPointers)
{
foreach (var item in _playerRelatedPointers)
{
Mediator.Publish(new TransientResourceChangedMessage(item.Address));
}
Mediator.Publish(new TransientResourceChangedMessage(item.Address));
}
});
}
@@ -349,40 +352,38 @@ public sealed class TransientResourceManager : DisposableMediatorSubscriberBase
_semiTransientResources = null;
}
private void RefreshPlayerRelatedAddressMap()
private static bool TryResolveObjectKind(ActorObjectService.ActorDescriptor descriptor, out ObjectKind resolvedKind)
{
_playerRelatedByAddress.Clear();
var updatedFrameAddresses = new ConcurrentDictionary<nint, ObjectKind>();
lock (_playerRelatedLock)
if (descriptor.OwnedKind is ObjectKind ownedKind)
{
foreach (var handler in _playerRelatedPointers)
{
var address = (nint)handler.Address;
if (address != nint.Zero)
{
_playerRelatedByAddress[address] = handler;
updatedFrameAddresses[address] = handler.ObjectKind;
}
}
resolvedKind = ownedKind;
return true;
}
_cachedFrameAddresses = updatedFrameAddresses;
if (descriptor.ObjectKind == DalamudObjectKind.Player)
{
resolvedKind = ObjectKind.Player;
return true;
}
resolvedKind = default;
return false;
}
private void HandleActorTracked(ActorObjectService.ActorDescriptor descriptor)
{
if (descriptor.IsInGpose)
return;
if (descriptor.OwnedKind is not ObjectKind ownedKind)
if (!TryResolveObjectKind(descriptor, out var resolvedKind))
return;
if (Logger.IsEnabled(LogLevel.Debug))
{
Logger.LogDebug("ActorObject tracked: {kind} addr={address:X} name={name}", ownedKind, descriptor.Address, descriptor.Name);
Logger.LogDebug("ActorObject tracked: {kind} addr={address:X} name={name}", resolvedKind, descriptor.Address, descriptor.Name);
}
_cachedFrameAddresses[descriptor.Address] = ownedKind;
_cachedFrameAddresses[descriptor.Address] = resolvedKind;
if (descriptor.OwnedKind is not ObjectKind ownedKind)
return;
lock (_ownedHandlerLock)
{
@@ -461,84 +462,53 @@ public sealed class TransientResourceManager : DisposableMediatorSubscriberBase
}
}
private static string NormalizeGamePath(string path)
{
if (string.IsNullOrEmpty(path))
return string.Empty;
return path.Replace("\\", "/", StringComparison.Ordinal).ToLowerInvariant();
}
private static string NormalizeFilePath(string path)
{
if (string.IsNullOrEmpty(path))
return string.Empty;
if (path.StartsWith("|", StringComparison.Ordinal))
{
var lastPipe = path.LastIndexOf('|');
if (lastPipe >= 0 && lastPipe + 1 < path.Length)
{
path = path[(lastPipe + 1)..];
}
}
return NormalizeGamePath(path);
}
private static bool HasHandledFileType(string gamePath, string[] handledTypes)
{
for (var i = 0; i < handledTypes.Length; i++)
{
if (gamePath.EndsWith(handledTypes[i], StringComparison.Ordinal))
return true;
}
return false;
}
private void Manager_PenumbraResourceLoadEvent(PenumbraResourceLoadMessage msg)
{
var gamePath = msg.GamePath.ToLowerInvariant();
var gameObjectAddress = msg.GameObject;
if (!_cachedFrameAddresses.TryGetValue(gameObjectAddress, out var objectKind))
{
if (_actorObjectService.TryGetOwnedKind(gameObjectAddress, out var ownedKind))
{
objectKind = ownedKind;
}
else
{
return;
}
}
var gamePath = NormalizeGamePath(msg.GamePath);
if (string.IsNullOrEmpty(gamePath))
{
return;
}
var filePath = msg.FilePath;
// ignore files already processed this frame
if (_cachedHandledPaths.Contains(gamePath)) return;
lock (_cacheAdditionLock)
{
if (!_cachedHandledPaths.Add(gamePath))
{
return;
}
_cachedHandledPaths.Add(gamePath);
}
// replace individual mtrl stuff
if (filePath.StartsWith("|", StringComparison.OrdinalIgnoreCase))
{
filePath = filePath.Split("|")[2];
}
// replace filepath
filePath = filePath.ToLowerInvariant().Replace("\\", "/", StringComparison.OrdinalIgnoreCase);
// ignore files that are the same
var replacedGamePath = gamePath.ToLowerInvariant().Replace("\\", "/", StringComparison.OrdinalIgnoreCase);
if (string.Equals(filePath, replacedGamePath, StringComparison.OrdinalIgnoreCase))
{
return;
}
// ignore files to not handle
var handledTypes = IsTransientRecording ? _handledFileTypesWithRecording : _handledFileTypes;
if (!HasHandledFileType(gamePath, handledTypes))
var handledTypes = IsTransientRecording ? _handledRecordingFileTypes.Concat(_handledFileTypes) : _handledFileTypes;
if (!handledTypes.Any(type => gamePath.EndsWith(type, StringComparison.OrdinalIgnoreCase)))
{
lock (_cacheAdditionLock)
{
_cachedHandledPaths.Add(gamePath);
}
return;
}
var filePath = NormalizeFilePath(msg.FilePath);
// ignore files that are the same
if (string.Equals(filePath, gamePath, StringComparison.Ordinal))
// ignore files not belonging to anything player related
if (!_cachedFrameAddresses.TryGetValue(gameObjectAddress, out var objectKind))
{
lock (_cacheAdditionLock)
{
_cachedHandledPaths.Add(gamePath);
}
return;
}
@@ -550,15 +520,15 @@ public sealed class TransientResourceManager : DisposableMediatorSubscriberBase
TransientResources[objectKind] = transientResources;
}
_playerRelatedByAddress.TryGetValue(gameObjectAddress, out var owner);
var owner = _playerRelatedPointers.FirstOrDefault(f => f.Address == gameObjectAddress);
bool alreadyTransient = false;
bool transientContains = transientResources.Contains(gamePath);
bool semiTransientContains = SemiTransientResources.Values.Any(value => value.Contains(gamePath));
bool transientContains = transientResources.Contains(replacedGamePath);
bool semiTransientContains = SemiTransientResources.SelectMany(k => k.Value).Any(f => string.Equals(f, gamePath, StringComparison.OrdinalIgnoreCase));
if (transientContains || semiTransientContains)
{
if (!IsTransientRecording)
Logger.LogTrace("Not adding {replacedPath} => {filePath}, Reason: Transient: {contains}, SemiTransient: {contains2}", gamePath, filePath,
Logger.LogTrace("Not adding {replacedPath} => {filePath}, Reason: Transient: {contains}, SemiTransient: {contains2}", replacedGamePath, filePath,
transientContains, semiTransientContains);
alreadyTransient = true;
}
@@ -566,10 +536,10 @@ public sealed class TransientResourceManager : DisposableMediatorSubscriberBase
{
if (!IsTransientRecording)
{
bool isAdded = transientResources.Add(gamePath);
bool isAdded = transientResources.Add(replacedGamePath);
if (isAdded)
{
Logger.LogDebug("Adding {replacedGamePath} for {gameObject} ({filePath})", gamePath, owner?.ToString() ?? gameObjectAddress.ToString("X"), filePath);
Logger.LogDebug("Adding {replacedGamePath} for {gameObject} ({filePath})", replacedGamePath, owner?.ToString() ?? gameObjectAddress.ToString("X"), filePath);
SendTransients(gameObjectAddress, objectKind);
}
}
@@ -577,7 +547,7 @@ public sealed class TransientResourceManager : DisposableMediatorSubscriberBase
if (owner != null && IsTransientRecording)
{
_recordedTransients.Add(new TransientRecord(owner, gamePath, filePath, alreadyTransient) { AddTransient = !alreadyTransient });
_recordedTransients.Add(new TransientRecord(owner, replacedGamePath, filePath, alreadyTransient) { AddTransient = !alreadyTransient });
}
}
@@ -649,7 +619,7 @@ public sealed class TransientResourceManager : DisposableMediatorSubscriberBase
if (!item.AddTransient || item.AlreadyTransient) continue;
if (!TransientResources.TryGetValue(item.Owner.ObjectKind, out var transient))
{
TransientResources[item.Owner.ObjectKind] = transient = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
TransientResources[item.Owner.ObjectKind] = transient = [];
}
Logger.LogTrace("Adding recorded: {gamePath} => {filePath}", item.GamePath, item.FilePath);

View File

@@ -1,4 +1,5 @@
using Dalamud.Plugin;
using Dalamud.Game.ClientState.Objects.SubKinds;
using Dalamud.Plugin;
using Dalamud.Plugin.Ipc;
using LightlessSync.Interop.Ipc.Framework;
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 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, object> _moodlesSetStatus;
private readonly ICallGateSubscriber<nint, object> _moodlesRevertStatus;
@@ -28,7 +29,7 @@ public sealed class IpcCallerMoodles : IpcServiceBase
_lightlessMediator = lightlessMediator;
_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");
_moodlesSetStatus = pi.GetIpcSubscriber<nint, string, object>("Moodles.SetStatusManagerByPtrV2");
_moodlesRevertStatus = pi.GetIpcSubscriber<nint, object>("Moodles.ClearStatusManagerByPtrV2");
@@ -38,9 +39,9 @@ public sealed class IpcCallerMoodles : IpcServiceBase
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)
@@ -106,7 +107,7 @@ public sealed class IpcCallerMoodles : IpcServiceBase
try
{
return _moodlesApiVersion.InvokeFunc() >= 4
return _moodlesApiVersion.InvokeFunc() == 3
? IpcConnectionState.Available
: IpcConnectionState.VersionMismatch;
}

View File

@@ -95,12 +95,6 @@ public sealed class IpcCallerPenumbra : IpcServiceBase
public Task<(string[] forward, string[][] reverse)> ResolvePathsAsync(string[] forward, string[] reverse)
=> _resources.ResolvePathsAsync(forward, reverse);
public string ResolveGameObjectPath(string gamePath, int objectIndex)
=> _resources.ResolveGameObjectPath(gamePath, objectIndex);
public string[] ReverseResolveGameObjectPath(string moddedPath, int objectIndex)
=> _resources.ReverseResolveGameObjectPath(moddedPath, objectIndex);
public Task RedrawAsync(ILogger logger, GameObjectHandler handler, Guid applicationId, CancellationToken token)
=> _redraw.RedrawAsync(logger, handler, applicationId, token);
@@ -177,6 +171,11 @@ public sealed class IpcCallerPenumbra : IpcServiceBase
});
Mediator.Subscribe<DalamudLoginMessage>(this, _ => _shownPenumbraUnavailable = false);
Mediator.Subscribe<ActorTrackedMessage>(this, msg => _resources.TrackActor(msg.Descriptor.Address));
Mediator.Subscribe<ActorUntrackedMessage>(this, msg => _resources.UntrackActor(msg.Descriptor.Address));
Mediator.Subscribe<GameObjectHandlerCreatedMessage>(this, msg => _resources.TrackActor(msg.GameObjectHandler.Address));
Mediator.Subscribe<GameObjectHandlerDestroyedMessage>(this, msg => _resources.UntrackActor(msg.GameObjectHandler.Address));
}
private void HandlePenumbraInitialized()

View File

@@ -92,7 +92,7 @@ public sealed class PenumbraCollections : PenumbraBase
_activeTemporaryCollections.TryRemove(collectionId, out _);
}
public async Task SetTemporaryModsAsync(ILogger logger, Guid applicationId, Guid collectionId, Dictionary<string, string> modPaths)
public async Task SetTemporaryModsAsync(ILogger logger, Guid applicationId, Guid collectionId, IReadOnlyDictionary<string, string> modPaths)
{
if (!IsAvailable || collectionId == Guid.Empty)
{
@@ -109,7 +109,7 @@ public sealed class PenumbraCollections : PenumbraBase
var removeResult = _removeTemporaryMod.Invoke("LightlessChara_Files", collectionId, 0);
logger.LogTrace("[{ApplicationId}] Removing temp files mod for {CollectionId}, Success: {Result}", applicationId, collectionId, removeResult);
var addResult = _addTemporaryMod.Invoke("LightlessChara_Files", collectionId, modPaths, string.Empty, 0);
var addResult = _addTemporaryMod.Invoke("LightlessChara_Files", collectionId, new Dictionary<string, string>(modPaths), string.Empty, 0);
logger.LogTrace("[{ApplicationId}] Setting temp files mod for {CollectionId}, Success: {Result}", applicationId, collectionId, addResult);
}).ConfigureAwait(false);
}

View File

@@ -1,3 +1,4 @@
using System.Collections.Concurrent;
using Dalamud.Plugin;
using LightlessSync.Interop.Ipc.Framework;
using LightlessSync.PlayerData.Handlers;
@@ -14,11 +15,10 @@ public sealed class PenumbraResource : PenumbraBase
{
private readonly ActorObjectService _actorObjectService;
private readonly GetGameObjectResourcePaths _gameObjectResourcePaths;
private readonly ResolveGameObjectPath _resolveGameObjectPath;
private readonly ReverseResolveGameObjectPath _reverseResolveGameObjectPath;
private readonly ResolvePlayerPathsAsync _resolvePlayerPaths;
private readonly GetPlayerMetaManipulations _getPlayerMetaManipulations;
private readonly EventSubscriber<nint, string, string> _gameObjectResourcePathResolved;
private readonly ConcurrentDictionary<IntPtr, byte> _trackedActors = new();
public PenumbraResource(
ILogger logger,
@@ -29,11 +29,14 @@ public sealed class PenumbraResource : PenumbraBase
{
_actorObjectService = actorObjectService;
_gameObjectResourcePaths = new GetGameObjectResourcePaths(pluginInterface);
_resolveGameObjectPath = new ResolveGameObjectPath(pluginInterface);
_reverseResolveGameObjectPath = new ReverseResolveGameObjectPath(pluginInterface);
_resolvePlayerPaths = new ResolvePlayerPathsAsync(pluginInterface);
_getPlayerMetaManipulations = new GetPlayerMetaManipulations(pluginInterface);
_gameObjectResourcePathResolved = GameObjectResourcePathResolved.Subscriber(pluginInterface, HandleResourceLoaded);
foreach (var descriptor in _actorObjectService.PlayerDescriptors)
{
TrackActor(descriptor.Address);
}
}
public override string Name => "Penumbra.Resources";
@@ -71,34 +74,63 @@ public sealed class PenumbraResource : PenumbraBase
return await _resolvePlayerPaths.Invoke(forwardPaths, reversePaths).ConfigureAwait(false);
}
public string ResolveGameObjectPath(string gamePath, int gameObjectIndex)
=> IsAvailable ? _resolveGameObjectPath.Invoke(gamePath, gameObjectIndex) : gamePath;
public void TrackActor(nint address)
{
if (address != nint.Zero)
{
_trackedActors[(IntPtr)address] = 0;
}
}
public string[] ReverseResolveGameObjectPath(string moddedPath, int gameObjectIndex)
=> IsAvailable ? _reverseResolveGameObjectPath.Invoke(moddedPath, gameObjectIndex) : Array.Empty<string>();
public void UntrackActor(nint address)
{
if (address != nint.Zero)
{
_trackedActors.TryRemove((IntPtr)address, out _);
}
}
private void HandleResourceLoaded(nint ptr, string gamePath, string resolvedPath)
private void HandleResourceLoaded(nint ptr, string resolvedPath, string gamePath)
{
if (ptr == nint.Zero)
{
return;
}
if (!_actorObjectService.TryGetOwnedKind(ptr, out _))
if (!_trackedActors.ContainsKey(ptr))
{
var descriptor = _actorObjectService.PlayerDescriptors.FirstOrDefault(d => d.Address == ptr);
if (descriptor.Address != nint.Zero)
{
_trackedActors[ptr] = 0;
}
else
{
return;
}
}
if (string.Compare(resolvedPath, gamePath, StringComparison.OrdinalIgnoreCase) == 0)
{
return;
}
if (string.Compare(gamePath, resolvedPath, StringComparison.OrdinalIgnoreCase) == 0)
{
return;
}
Mediator.Publish(new PenumbraResourceLoadMessage(ptr, gamePath, resolvedPath));
Mediator.Publish(new PenumbraResourceLoadMessage(ptr, resolvedPath, gamePath));
}
protected override void HandleStateChange(IpcConnectionState previous, IpcConnectionState current)
{
if (current != IpcConnectionState.Available)
{
_trackedActors.Clear();
}
else
{
foreach (var descriptor in _actorObjectService.PlayerDescriptors)
{
TrackActor(descriptor.Address);
}
}
}
public override void Dispose()

View File

@@ -19,27 +19,6 @@ public class ConfigurationMigrator(ILogger<ConfigurationMigrator> logger, Transi
transientConfigService.Save();
}
if (transientConfigService.Current.Version == 1)
{
_logger.LogInformation("Migrating Transient Config V1 => V2");
var totalRemoved = 0;
var configCount = 0;
var changedCount = 0;
foreach (var config in transientConfigService.Current.TransientConfigs.Values)
{
if (config.NormalizePaths(out var removed))
changedCount++;
totalRemoved += removed;
configCount++;
}
_logger.LogInformation("Transient config normalization: processed {count} entries, updated {updated}, removed {removed} paths", configCount, changedCount, totalRemoved);
transientConfigService.Current.Version = 2;
transientConfigService.Save();
}
if (serverConfigService.Current.Version == 1)
{
_logger.LogInformation("Migrating Server Config V1 => V2");

View File

@@ -10,10 +10,7 @@ public sealed class ChatConfig : ILightlessConfiguration
public bool AutoEnableChatOnLogin { get; set; } = false;
public bool ShowRulesOverlayOnOpen { get; set; } = true;
public bool ShowMessageTimestamps { get; set; } = true;
public bool ShowNotesInSyncshellChat { get; set; } = true;
public float ChatWindowOpacity { get; set; } = .97f;
public bool FadeWhenUnfocused { get; set; } = false;
public float UnfocusedWindowOpacity { get; set; } = 0.6f;
public bool IsWindowPinned { get; set; } = false;
public bool AutoOpenChatOnPluginLoad { get; set; } = false;
public float ChatFontScale { get; set; } = 1.0f;

View File

@@ -49,8 +49,7 @@ public class LightlessConfig : ILightlessConfiguration
public int DownloadSpeedLimitInBytes { get; set; } = 0;
public DownloadSpeeds DownloadSpeedType { get; set; } = DownloadSpeeds.MBps;
public bool PreferNotesOverNamesForVisible { get; set; } = false;
public VisiblePairSortMode VisiblePairSortMode { get; set; } = VisiblePairSortMode.Alphabetical;
public OnlinePairSortMode OnlinePairSortMode { get; set; } = OnlinePairSortMode.Alphabetical;
public VisiblePairSortMode VisiblePairSortMode { get; set; } = VisiblePairSortMode.Default;
public float ProfileDelay { get; set; } = 1.5f;
public bool ProfilePopoutRight { get; set; } = false;
public bool ProfilesAllowNsfw { get; set; } = false;
@@ -140,7 +139,6 @@ public class LightlessConfig : ILightlessConfiguration
public bool useColoredUIDs { get; set; } = true;
public bool BroadcastEnabled { get; set; } = false;
public bool LightfinderAutoEnableOnConnect { get; set; } = false;
public LightfinderLabelRenderer LightfinderLabelRenderer { get; set; } = LightfinderLabelRenderer.Pictomancy;
public short LightfinderLabelOffsetX { get; set; } = 0;
public short LightfinderLabelOffsetY { get; set; } = 0;
public bool LightfinderLabelUseIcon { get; set; } = false;
@@ -155,5 +153,4 @@ public class LightlessConfig : ILightlessConfiguration
public bool SyncshellFinderEnabled { get; set; } = false;
public string? SelectedFinderSyncshell { get; set; } = null;
public string LastSeenVersion { get; set; } = string.Empty;
public HashSet<Guid> OrphanableTempCollections { get; set; } = [];
}

View File

@@ -5,7 +5,7 @@ namespace LightlessSync.LightlessConfiguration.Configurations;
public class TransientConfig : ILightlessConfiguration
{
public Dictionary<string, TransientPlayerConfig> TransientConfigs { get; set; } = [];
public int Version { get; set; } = 2;
public int Version { get; set; } = 1;
public class TransientPlayerConfig
{
@@ -88,70 +88,5 @@ public class TransientConfig : ILightlessConfiguration
}
}
}
public bool NormalizePaths(out int removedEntries)
{
bool changed = false;
removedEntries = 0;
GlobalPersistentCache = NormalizeList(GlobalPersistentCache, ref changed, ref removedEntries);
foreach (var jobId in JobSpecificCache.Keys.ToList())
{
JobSpecificCache[jobId] = NormalizeList(JobSpecificCache[jobId], ref changed, ref removedEntries);
}
foreach (var jobId in JobSpecificPetCache.Keys.ToList())
{
JobSpecificPetCache[jobId] = NormalizeList(JobSpecificPetCache[jobId], ref changed, ref removedEntries);
}
return changed;
}
private static List<string> NormalizeList(List<string> entries, ref bool changed, ref int removedEntries)
{
if (entries.Count == 0)
return entries;
var result = new List<string>(entries.Count);
var seen = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
foreach (var entry in entries)
{
var normalized = NormalizePath(entry);
if (string.IsNullOrEmpty(normalized))
{
changed = true;
continue;
}
if (!string.Equals(entry, normalized, StringComparison.Ordinal))
{
changed = true;
}
if (seen.Add(normalized))
{
result.Add(normalized);
}
else
{
changed = true;
}
}
removedEntries += entries.Count - result.Count;
return result;
}
private static string NormalizePath(string path)
{
if (string.IsNullOrEmpty(path))
return string.Empty;
return path.Replace("\\", "/", StringComparison.Ordinal).ToLowerInvariant();
}
}
}

View File

@@ -3,7 +3,7 @@
<PropertyGroup>
<Authors></Authors>
<Company></Company>
<Version>2.0.2</Version>
<Version>1.42.0.72</Version>
<Description></Description>
<Copyright></Copyright>
<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();
_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
await _dalamudUtil.WaitWhileCharacterIsDrawing(_logger, playerRelatedObject, Guid.NewGuid(), 30000, ct: ct).ConfigureAwait(false);
@@ -133,6 +132,11 @@ public class PlayerDataFactory
ct.ThrowIfCancellationRequested();
Dictionary<string, List<ushort>>? boneIndices =
objectKind != ObjectKind.Player
? null
: await _dalamudUtil.RunOnFrameworkThread(() => _modelAnalyzer.GetSkeletonBoneIndices(playerRelatedObject)).ConfigureAwait(false);
DateTime start = DateTime.UtcNow;
// penumbra call, it's currently broken
@@ -150,21 +154,11 @@ public class PlayerDataFactory
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 ==");
foreach (var replacement in fragment.FileReplacements.Where(i => i.HasFileReplacement).OrderBy(i => i.GamePaths.First(), StringComparer.OrdinalIgnoreCase))
{
_logger.LogDebug("=> {repl}", replacement);
ct.ThrowIfCancellationRequested();
}
}
else
{
foreach (var replacement in fragment.FileReplacements.Where(i => i.HasFileReplacement))
{
ct.ThrowIfCancellationRequested();
}
_logger.LogDebug("=> {repl}", replacement);
ct.ThrowIfCancellationRequested();
}
await _transientResourceManager.WaitForRecording(ct).ConfigureAwait(false);
@@ -194,23 +188,13 @@ public class PlayerDataFactory
// get all remaining paths and resolve them
var transientPaths = ManageSemiTransientData(objectKind);
var resolvedTransientPaths = await GetFileReplacementsFromPaths(playerRelatedObject, 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 ==");
foreach (var replacement in resolvedTransientPaths.Select(c => new FileReplacement([.. c.Value], c.Key)).OrderBy(f => f.ResolvedPath, StringComparer.Ordinal))
{
_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);
}
_logger.LogDebug("=> {repl}", replacement);
fragment.FileReplacements.Add(replacement);
}
// 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();
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)
{
try
{
if (hasPapFiles)
{
await VerifyPlayerAnimationBones(boneIndices, (fragment as CharacterDataFragmentPlayer)!, ct).ConfigureAwait(false);
}
await VerifyPlayerAnimationBones(boneIndices, (fragment as CharacterDataFragmentPlayer)!, ct).ConfigureAwait(false);
}
catch (OperationCanceledException e)
{
@@ -309,16 +278,12 @@ public class PlayerDataFactory
{
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 (maxPlayerBoneIndex <= 0) return;
if (boneIndices.All(u => u.Value.Count == 0)) return;
int noValidationFailed = 0;
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);
foreach (var boneCount in skeletonIndices)
foreach (var boneCount in skeletonIndices.Select(k => k).ToList())
{
var maxAnimationIndex = boneCount.Value.DefaultIfEmpty().Max();
if (maxAnimationIndex > maxPlayerBoneIndex)
if (boneCount.Value.Max() > boneIndices.SelectMany(b => b.Value).Max())
{
_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;
break;
}
@@ -373,73 +337,11 @@ public class PlayerDataFactory
}
}
private async Task<IReadOnlyDictionary<string, string[]>> GetFileReplacementsFromPaths(GameObjectHandler handler, HashSet<string> forwardResolve, HashSet<string> reverseResolve)
private async Task<IReadOnlyDictionary<string, string[]>> GetFileReplacementsFromPaths(HashSet<string> forwardResolve, HashSet<string> reverseResolve)
{
var forwardPaths = forwardResolve.ToArray();
var reversePaths = reverseResolve.ToArray();
Dictionary<string, List<string>> resolvedPaths = new(StringComparer.Ordinal);
if (handler.ObjectKind != ObjectKind.Player)
{
var (objectIndex, forwardResolved, reverseResolved) = await _dalamudUtil.RunOnFrameworkThread(() =>
{
var idx = handler.GetGameObject()?.ObjectIndex;
if (!idx.HasValue)
{
return ((int?)null, Array.Empty<string>(), Array.Empty<string[]>());
}
var resolvedForward = new string[forwardPaths.Length];
for (int i = 0; i < forwardPaths.Length; i++)
{
resolvedForward[i] = _ipcManager.Penumbra.ResolveGameObjectPath(forwardPaths[i], idx.Value);
}
var resolvedReverse = new string[reversePaths.Length][];
for (int i = 0; i < reversePaths.Length; i++)
{
resolvedReverse[i] = _ipcManager.Penumbra.ReverseResolveGameObjectPath(reversePaths[i], idx.Value);
}
return (idx, resolvedForward, resolvedReverse);
}).ConfigureAwait(false);
if (objectIndex.HasValue)
{
for (int i = 0; i < forwardPaths.Length; i++)
{
var filePath = forwardResolved[i]?.ToLowerInvariant();
if (string.IsNullOrEmpty(filePath))
{
continue;
}
if (resolvedPaths.TryGetValue(filePath, out var list))
{
list.Add(forwardPaths[i].ToLowerInvariant());
}
else
{
resolvedPaths[filePath] = [forwardPaths[i].ToLowerInvariant()];
}
}
for (int i = 0; i < reversePaths.Length; i++)
{
var filePath = reversePaths[i].ToLowerInvariant();
if (resolvedPaths.TryGetValue(filePath, out var list))
{
list.AddRange(reverseResolved[i].Select(c => c.ToLowerInvariant()));
}
else
{
resolvedPaths[filePath] = new List<string>(reverseResolved[i].Select(c => c.ToLowerInvariant()).ToList());
}
}
return resolvedPaths.ToDictionary(k => k.Key, k => k.Value.ToArray(), StringComparer.OrdinalIgnoreCase).AsReadOnly();
}
}
var (forward, reverse) = await _ipcManager.Penumbra.ResolvePathsAsync(forwardPaths, reversePaths).ConfigureAwait(false);
for (int i = 0; i < forwardPaths.Length; i++)
{

View File

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

View File

@@ -1,43 +1,36 @@
using LightlessSync.API.Data;
using LightlessSync.API.Data;
namespace LightlessSync.PlayerData.Pairs;
namespace LightlessSync.PlayerData.Pairs;
/// <summary>
/// orchestrates the lifecycle of a paired character
/// </summary>
public interface IPairHandlerAdapter : IDisposable, IPairPerformanceSubject
{
new string Ident { get; }
bool Initialized { get; }
bool IsVisible { get; }
bool ScheduledForDeletion { get; set; }
CharacterData? LastReceivedCharacterData { get; }
long LastAppliedDataBytes { get; }
new string? PlayerName { get; }
string PlayerNameHash { get; }
uint PlayerCharacterId { get; }
DateTime? LastDataReceivedAt { get; }
DateTime? LastApplyAttemptAt { get; }
DateTime? LastSuccessfulApplyAt { get; }
string? LastFailureReason { get; }
IReadOnlyList<string> LastBlockingConditions { get; }
bool IsApplying { get; }
bool IsDownloading { get; }
int PendingDownloadCount { get; }
int ForbiddenDownloadCount { get; }
bool PendingModReapply { get; }
bool ModApplyDeferred { get; }
int MissingCriticalMods { get; }
int MissingNonCriticalMods { get; }
int MissingForbiddenMods { get; }
DateTime? InvisibleSinceUtc { get; }
DateTime? VisibilityEvictionDueAtUtc { get; }
/// <summary>
/// orchestrates the lifecycle of a paired character
/// </summary>
public interface IPairHandlerAdapter : IDisposable, IPairPerformanceSubject
{
new string Ident { get; }
bool Initialized { get; }
bool IsVisible { get; }
bool ScheduledForDeletion { get; set; }
CharacterData? LastReceivedCharacterData { get; }
long LastAppliedDataBytes { get; }
new string? PlayerName { get; }
string PlayerNameHash { get; }
uint PlayerCharacterId { get; }
DateTime? LastDataReceivedAt { get; }
DateTime? LastApplyAttemptAt { get; }
DateTime? LastSuccessfulApplyAt { get; }
string? LastFailureReason { get; }
IReadOnlyList<string> LastBlockingConditions { get; }
bool IsApplying { get; }
bool IsDownloading { get; }
int PendingDownloadCount { get; }
int ForbiddenDownloadCount { get; }
void Initialize();
void ApplyData(CharacterData data);
void ApplyLastReceivedData(bool forced = false);
bool FetchPerformanceMetricsFromCache();
void LoadCachedCharacterData(CharacterData data);
void SetUploading(bool uploading);
void SetPaused(bool paused);
}
void ApplyData(CharacterData data);
void ApplyLastReceivedData(bool forced = false);
bool FetchPerformanceMetricsFromCache();
void LoadCachedCharacterData(CharacterData data);
void SetUploading(bool uploading);
void SetPaused(bool paused);
}

View File

@@ -87,25 +87,22 @@ public class Pair
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;
}
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: () =>
{
ApplyLastReceivedData(forced: true);
return Task.CompletedTask;
});
}
UiSharedService.AddContextMenuItem(args, name: "Reapply last data", prefixChar: 'L', colorMenuItem: _lightlessPrefixColor, onClick: () =>
{
ApplyLastReceivedData(forced: true);
return Task.CompletedTask;
});
UiSharedService.AddContextMenuItem(args, name: "Change Permissions", prefixChar: 'L', colorMenuItem: _lightlessPrefixColor, onClick: () =>
{
@@ -113,24 +110,7 @@ public class Pair
return Task.CompletedTask;
});
if (IsPaused)
{
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: () =>
UiSharedService.AddContextMenuItem(args, name: "Cycle pause state", prefixChar: 'L', colorMenuItem: _lightlessPrefixColor, onClick: () =>
{
TriggerCyclePause();
return Task.CompletedTask;
@@ -214,13 +194,9 @@ public class Pair
{
var handler = TryGetHandler();
if (handler is null)
{
return PairDebugInfo.Empty;
var now = DateTime.UtcNow;
var dueAt = handler.VisibilityEvictionDueAtUtc;
var remainingSeconds = dueAt.HasValue
? Math.Max(0, (dueAt.Value - now).TotalSeconds)
: (double?)null;
}
return new PairDebugInfo(
true,
@@ -230,19 +206,11 @@ public class Pair
handler.LastDataReceivedAt,
handler.LastApplyAttemptAt,
handler.LastSuccessfulApplyAt,
handler.InvisibleSinceUtc,
handler.VisibilityEvictionDueAtUtc,
remainingSeconds,
handler.LastFailureReason,
handler.LastBlockingConditions,
handler.IsApplying,
handler.IsDownloading,
handler.PendingDownloadCount,
handler.ForbiddenDownloadCount,
handler.PendingModReapply,
handler.ModApplyDeferred,
handler.MissingCriticalMods,
handler.MissingNonCriticalMods,
handler.MissingForbiddenMods);
handler.ForbiddenDownloadCount);
}
}

View File

@@ -8,20 +8,12 @@ public sealed record PairDebugInfo(
DateTime? LastDataReceivedAt,
DateTime? LastApplyAttemptAt,
DateTime? LastSuccessfulApplyAt,
DateTime? InvisibleSinceUtc,
DateTime? VisibilityEvictionDueAtUtc,
double? VisibilityEvictionRemainingSeconds,
string? LastFailureReason,
IReadOnlyList<string> BlockingConditions,
bool IsApplying,
bool IsDownloading,
int PendingDownloadCount,
int ForbiddenDownloadCount,
bool PendingModReapply,
bool ModApplyDeferred,
int MissingCriticalMods,
int MissingNonCriticalMods,
int MissingForbiddenMods)
int ForbiddenDownloadCount)
{
public static PairDebugInfo Empty { get; } = new(
false,
@@ -32,17 +24,9 @@ public sealed record PairDebugInfo(
null,
null,
null,
null,
null,
null,
Array.Empty<string>(),
false,
false,
0,
0,
false,
false,
0,
0,
0);
}

File diff suppressed because it is too large Load Diff

View File

@@ -2,7 +2,6 @@ using LightlessSync.FileCache;
using LightlessSync.Interop.Ipc;
using LightlessSync.PlayerData.Factories;
using LightlessSync.Services;
using LightlessSync.Services.ActorTracking;
using LightlessSync.Services.Mediator;
using LightlessSync.Services.PairProcessing;
using LightlessSync.Services.ServerConfiguration;
@@ -31,7 +30,6 @@ internal sealed class PairHandlerAdapterFactory : IPairHandlerAdapterFactory
private readonly TextureDownscaleService _textureDownscaleService;
private readonly PairStateCache _pairStateCache;
private readonly PairPerformanceMetricsCache _pairPerformanceMetricsCache;
private readonly PenumbraTempCollectionJanitor _tempCollectionJanitor;
public PairHandlerAdapterFactory(
ILoggerFactory loggerFactory,
@@ -49,8 +47,7 @@ internal sealed class PairHandlerAdapterFactory : IPairHandlerAdapterFactory
ServerConfigurationManager serverConfigManager,
TextureDownscaleService textureDownscaleService,
PairStateCache pairStateCache,
PairPerformanceMetricsCache pairPerformanceMetricsCache,
PenumbraTempCollectionJanitor tempCollectionJanitor)
PairPerformanceMetricsCache pairPerformanceMetricsCache)
{
_loggerFactory = loggerFactory;
_mediator = mediator;
@@ -68,14 +65,12 @@ internal sealed class PairHandlerAdapterFactory : IPairHandlerAdapterFactory
_textureDownscaleService = textureDownscaleService;
_pairStateCache = pairStateCache;
_pairPerformanceMetricsCache = pairPerformanceMetricsCache;
_tempCollectionJanitor = tempCollectionJanitor;
}
public IPairHandlerAdapter Create(string ident)
{
var downloadManager = _fileDownloadManagerFactory.Create();
var dalamudUtilService = _serviceProvider.GetRequiredService<DalamudUtilService>();
var actorObjectService = _serviceProvider.GetRequiredService<ActorObjectService>();
return new PairHandlerAdapter(
_loggerFactory.CreateLogger<PairHandlerAdapter>(),
_mediator,
@@ -86,7 +81,6 @@ internal sealed class PairHandlerAdapterFactory : IPairHandlerAdapterFactory
downloadManager,
_pluginWarningNotificationManager,
dalamudUtilService,
actorObjectService,
_lifetime,
_fileCacheManager,
_playerPerformanceService,
@@ -94,7 +88,6 @@ internal sealed class PairHandlerAdapterFactory : IPairHandlerAdapterFactory
_serverConfigManager,
_textureDownscaleService,
_pairStateCache,
_pairPerformanceMetricsCache,
_tempCollectionJanitor);
_pairPerformanceMetricsCache);
}
}

View File

@@ -11,9 +11,7 @@ public sealed class PairHandlerRegistry : IDisposable
{
private readonly object _gate = new();
private readonly object _pendingGate = new();
private readonly object _visibilityGate = new();
private readonly Dictionary<string, PairHandlerEntry> _entriesByIdent = new(StringComparer.Ordinal);
private readonly Dictionary<string, CancellationTokenSource> _pendingInvisibleEvictions = new(StringComparer.Ordinal);
private readonly Dictionary<IPairHandlerAdapter, PairHandlerEntry> _entriesByHandler = new(ReferenceEqualityComparer.Instance);
private readonly IPairHandlerAdapterFactory _handlerFactory;
@@ -146,37 +144,6 @@ public sealed class PairHandlerRegistry : IDisposable
return PairOperationResult<PairUniqueIdentifier>.Ok(registration.PairIdent);
}
private PairOperationResult CancelAllInvisibleEvictions()
{
List<CancellationTokenSource> snapshot;
lock (_visibilityGate)
{
snapshot = [.. _pendingInvisibleEvictions.Values];
_pendingInvisibleEvictions.Clear();
}
List<string>? errors = null;
foreach (var cts in snapshot)
{
try { cts.Cancel(); }
catch (Exception ex)
{
(errors ??= new List<string>()).Add($"Cancel: {ex.Message}");
}
try { cts.Dispose(); }
catch (Exception ex)
{
(errors ??= new List<string>()).Add($"Dispose: {ex.Message}");
}
}
return errors is null
? PairOperationResult.Ok()
: PairOperationResult.Fail($"CancelAllInvisibleEvictions had error(s): {string.Join(" | ", errors)}");
}
public PairOperationResult ApplyCharacterData(PairRegistration registration, OnlineUserCharaDataDto dto)
{
if (registration.CharacterIdent is null)
@@ -333,7 +300,6 @@ public sealed class PairHandlerRegistry : IDisposable
lock (_gate)
{
handlers = _entriesByHandler.Keys.ToList();
CancelAllInvisibleEvictions();
_entriesByIdent.Clear();
_entriesByHandler.Clear();
}
@@ -366,7 +332,6 @@ public sealed class PairHandlerRegistry : IDisposable
lock (_gate)
{
handlers = _entriesByHandler.Keys.ToList();
CancelAllInvisibleEvictions();
_entriesByIdent.Clear();
_entriesByHandler.Clear();
}

View File

@@ -40,7 +40,6 @@ using System.Reflection;
using OtterTex;
using LightlessSync.Services.LightFinder;
using LightlessSync.Services.PairProcessing;
using LightlessSync.UI.Models;
namespace LightlessSync;
@@ -52,7 +51,7 @@ public sealed class Plugin : IDalamudPlugin
IFramework framework, IObjectTable objectTable, IClientState clientState, ICondition condition, IChatGui chatGui,
IGameGui gameGui, IDtrBar dtrBar, IPluginLog pluginLog, ITargetManager targetManager, INotificationManager notificationManager,
ITextureProvider textureProvider, IContextMenu contextMenu, IGameInteropProvider gameInteropProvider, IGameConfig gameConfig,
ISigScanner sigScanner, INamePlateGui namePlateGui, IAddonLifecycle addonLifecycle, IPlayerState playerState)
ISigScanner sigScanner, INamePlateGui namePlateGui, IAddonLifecycle addonLifecycle)
{
NativeDll.Initialize(pluginInterface.AssemblyLocation.DirectoryName);
if (!Directory.Exists(pluginInterface.ConfigDirectory.FullName))
@@ -106,7 +105,6 @@ public sealed class Plugin : IDalamudPlugin
services.AddSingleton<FileDialogManager>();
services.AddSingleton(new Dalamud.Localization("LightlessSync.Localization.", string.Empty, useEmbedded: true));
services.AddSingleton(gameGui);
services.AddSingleton(gameInteropProvider);
services.AddSingleton(addonLifecycle);
services.AddSingleton<IUiBuilder>(pluginInterface.UiBuilder);
@@ -117,7 +115,6 @@ public sealed class Plugin : IDalamudPlugin
services.AddSingleton<ProfileTagService>();
services.AddSingleton<ApiController>();
services.AddSingleton<PerformanceCollectorService>();
services.AddSingleton<NameplateUpdateHookService>();
services.AddSingleton<HubFactory>();
services.AddSingleton<FileUploadManager>();
services.AddSingleton<FileTransferOrchestrator>();
@@ -136,10 +133,8 @@ public sealed class Plugin : IDalamudPlugin
services.AddSingleton<TagHandler>();
services.AddSingleton<PairRequestService>();
services.AddSingleton<ZoneChatService>();
services.AddSingleton<ChatEmoteService>();
services.AddSingleton<IdDisplayHandler>();
services.AddSingleton<PlayerPerformanceService>();
services.AddSingleton<PenumbraTempCollectionJanitor>();
services.AddSingleton<TextureMetadataHelper>(sp =>
new TextureMetadataHelper(sp.GetRequiredService<ILogger<TextureMetadataHelper>>(), gameData));
@@ -206,7 +201,6 @@ public sealed class Plugin : IDalamudPlugin
gameInteropProvider,
objectTable,
clientState,
condition,
sp.GetRequiredService<LightlessMediator>()));
services.AddSingleton(sp => new DalamudUtilService(
@@ -219,7 +213,6 @@ public sealed class Plugin : IDalamudPlugin
gameData,
targetManager,
gameConfig,
playerState,
sp.GetRequiredService<ActorObjectService>(),
sp.GetRequiredService<BlockedCharacterHandler>(),
sp.GetRequiredService<LightlessMediator>(),
@@ -274,7 +267,6 @@ public sealed class Plugin : IDalamudPlugin
sp.GetRequiredService<ILogger<LightFinderPlateHandler>>(),
addonLifecycle,
gameGui,
clientState,
sp.GetRequiredService<LightlessConfigService>(),
sp.GetRequiredService<LightlessMediator>(),
objectTable,
@@ -282,22 +274,12 @@ public sealed class Plugin : IDalamudPlugin
pluginInterface,
sp.GetRequiredService<PictomancyService>()));
services.AddSingleton(sp => new LightFinderNativePlateHandler(
sp.GetRequiredService<ILogger<LightFinderNativePlateHandler>>(),
clientState,
sp.GetRequiredService<LightlessConfigService>(),
sp.GetRequiredService<LightlessMediator>(),
objectTable,
sp.GetRequiredService<PairUiService>(),
sp.GetRequiredService<NameplateUpdateHookService>()));
services.AddSingleton(sp => new LightFinderScannerService(
sp.GetRequiredService<ILogger<LightFinderScannerService>>(),
framework,
sp.GetRequiredService<LightFinderService>(),
sp.GetRequiredService<LightlessMediator>(),
sp.GetRequiredService<LightFinderPlateHandler>(),
sp.GetRequiredService<LightFinderNativePlateHandler>(),
sp.GetRequiredService<ActorObjectService>()));
services.AddSingleton(sp => new ContextMenuService(
@@ -315,10 +297,7 @@ public sealed class Plugin : IDalamudPlugin
sp.GetRequiredService<LightFinderScannerService>(),
sp.GetRequiredService<LightFinderService>(),
sp.GetRequiredService<LightlessProfileManager>(),
sp.GetRequiredService<LightlessMediator>(),
chatGui,
sp.GetRequiredService<NotificationService>())
);
sp.GetRequiredService<LightlessMediator>()));
// IPC callers / manager
services.AddSingleton(sp => new IpcCallerPenumbra(
@@ -479,8 +458,7 @@ public sealed class Plugin : IDalamudPlugin
sp.GetRequiredService<LightlessConfigService>(),
sp.GetRequiredService<UiSharedService>(),
sp.GetRequiredService<ApiController>(),
sp.GetRequiredService<LightFinderScannerService>(),
sp.GetRequiredService<LightFinderPlateHandler>()));
sp.GetRequiredService<LightFinderScannerService>()));
services.AddScoped<WindowMediatorSubscriberBase, SyncshellFinderUI>(sp => new SyncshellFinderUI(
sp.GetRequiredService<ILogger<SyncshellFinderUI>>(),
@@ -549,9 +527,9 @@ public sealed class Plugin : IDalamudPlugin
clientState,
gameGui,
objectTable,
gameInteropProvider,
sp.GetRequiredService<LightlessMediator>(),
sp.GetRequiredService<PairUiService>(),
sp.GetRequiredService<NameplateUpdateHookService>()));
sp.GetRequiredService<PairUiService>()));
// Hosted services
services.AddHostedService(sp => sp.GetRequiredService<ConfigurationSaveService>());
@@ -570,7 +548,6 @@ public sealed class Plugin : IDalamudPlugin
services.AddHostedService(sp => sp.GetRequiredService<ContextMenuService>());
services.AddHostedService(sp => sp.GetRequiredService<LightFinderService>());
services.AddHostedService(sp => sp.GetRequiredService<LightFinderPlateHandler>());
services.AddHostedService(sp => sp.GetRequiredService<LightFinderNativePlateHandler>());
}).Build();
_ = _host.StartAsync();

View File

@@ -1,5 +1,5 @@
using System.Collections.Concurrent;
using Dalamud.Game.ClientState.Conditions;
using Dalamud.Game.ClientState.Objects.SubKinds;
using Dalamud.Hooking;
using Dalamud.Plugin.Services;
using FFXIVClientStructs.Interop;
@@ -9,8 +9,6 @@ using FFXIVClientStructs.FFXIV.Client.Graphics.Scene;
using LightlessSync.Services.Mediator;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
using BattleNpcSubKind = FFXIVClientStructs.FFXIV.Client.Game.Object.BattleNpcSubKind;
using IPlayerCharacter = Dalamud.Game.ClientState.Objects.SubKinds.IPlayerCharacter;
using DalamudObjectKind = Dalamud.Game.ClientState.Objects.Enums.ObjectKind;
using LightlessObjectKind = LightlessSync.API.Data.Enum.ObjectKind;
@@ -33,17 +31,13 @@ public sealed class ActorObjectService : IHostedService, IDisposable
private readonly IFramework _framework;
private readonly IGameInteropProvider _interop;
private readonly IObjectTable _objectTable;
private readonly IClientState _clientState;
private readonly ICondition _condition;
private readonly LightlessMediator _mediator;
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, ConcurrentDictionary<nint, ActorDescriptor>> _actorsByName = new(StringComparer.Ordinal);
private readonly ConcurrentDictionary<nint, byte> _pendingHashResolutions = new();
private readonly OwnedObjectTracker _ownedTracker = new();
private ActorSnapshot _snapshot = ActorSnapshot.Empty;
private GposeSnapshot _gposeSnapshot = GposeSnapshot.Empty;
private Hook<Character.Delegates.OnInitialize>? _onInitializeHook;
private Hook<Character.Delegates.Terminate>? _onTerminateHook;
@@ -61,29 +55,21 @@ public sealed class ActorObjectService : IHostedService, IDisposable
IGameInteropProvider interop,
IObjectTable objectTable,
IClientState clientState,
ICondition condition,
LightlessMediator mediator)
{
_logger = logger;
_framework = framework;
_interop = interop;
_objectTable = objectTable;
_clientState = clientState;
_condition = condition;
_mediator = mediator;
}
private bool IsZoning => _condition[ConditionFlag.BetweenAreas] || _condition[ConditionFlag.BetweenAreas51];
private ActorSnapshot Snapshot => Volatile.Read(ref _snapshot);
private GposeSnapshot CurrentGposeSnapshot => Volatile.Read(ref _gposeSnapshot);
public IReadOnlyList<nint> PlayerAddresses => Snapshot.PlayerAddresses;
public IEnumerable<ActorDescriptor> ObjectDescriptors => _activePlayers.Values;
public IReadOnlyList<ActorDescriptor> PlayerDescriptors => Snapshot.PlayerDescriptors;
public IReadOnlyList<ActorDescriptor> OwnedDescriptors => Snapshot.OwnedDescriptors;
public IReadOnlyList<ActorDescriptor> GposeDescriptors => CurrentGposeSnapshot.GposeDescriptors;
public IEnumerable<ActorDescriptor> PlayerDescriptors => _activePlayers.Values;
public IReadOnlyList<ActorDescriptor> PlayerCharacterDescriptors => Snapshot.PlayerDescriptors;
public bool TryGetActorByHash(string hash, out ActorDescriptor descriptor) => _actorsByHash.TryGetValue(hash, out descriptor);
public bool TryGetValidatedActorByHash(string hash, out ActorDescriptor descriptor)
@@ -127,7 +113,6 @@ public sealed class ActorObjectService : IHostedService, IDisposable
return false;
}
public bool HooksActive => _hooksActive;
public bool HasPendingHashResolutions => !_pendingHashResolutions.IsEmpty;
public IReadOnlyList<nint> RenderedPlayerAddresses => Snapshot.OwnedObjects.RenderedPlayers;
public IReadOnlyList<nint> RenderedCompanionAddresses => Snapshot.OwnedObjects.RenderedCompanions;
public IReadOnlyList<nint> OwnedObjectAddresses => Snapshot.OwnedObjects.OwnedAddresses;
@@ -151,16 +136,15 @@ public sealed class ActorObjectService : IHostedService, IDisposable
public bool TryGetOwnedObjectByIndex(ushort objectIndex, out LightlessObjectKind ownedKind)
{
ownedKind = default;
var ownedDescriptors = OwnedDescriptors;
for (var i = 0; i < ownedDescriptors.Count; i++)
var ownedSnapshot = OwnedObjects;
foreach (var (address, kind) in ownedSnapshot)
{
var descriptor = ownedDescriptors[i];
if (descriptor.ObjectIndex != objectIndex)
if (!TryGetDescriptor(address, out var descriptor))
continue;
if (descriptor.OwnedKind is { } resolvedKind)
if (descriptor.ObjectIndex == objectIndex)
{
ownedKind = resolvedKind;
ownedKind = kind;
return true;
}
}
@@ -223,7 +207,7 @@ public sealed class ActorObjectService : IHostedService, IDisposable
cancellationToken.ThrowIfCancellationRequested();
var isLoaded = await _framework.RunOnFrameworkThread(() => IsObjectFullyLoaded(address)).ConfigureAwait(false);
if (!IsZoning && isLoaded)
if (isLoaded)
return;
await Task.Delay(100, cancellationToken).ConfigureAwait(false);
@@ -313,12 +297,10 @@ public sealed class ActorObjectService : IHostedService, IDisposable
{
DisposeHooks();
_activePlayers.Clear();
_gposePlayers.Clear();
_actorsByHash.Clear();
_actorsByName.Clear();
_pendingHashResolutions.Clear();
_ownedTracker.Reset();
Volatile.Write(ref _snapshot, ActorSnapshot.Empty);
Volatile.Write(ref _gposeSnapshot, GposeSnapshot.Empty);
return Task.CompletedTask;
}
@@ -354,7 +336,7 @@ public sealed class ActorObjectService : IHostedService, IDisposable
_onCompanionTerminateHook.Enable();
_hooksActive = true;
_logger.LogTrace("ActorObjectService hooks enabled.");
_logger.LogDebug("ActorObjectService hooks enabled.");
}
private Task WarmupExistingActors()
@@ -368,21 +350,36 @@ public sealed class ActorObjectService : IHostedService, IDisposable
private unsafe void OnCharacterInitialized(Character* chara)
{
ExecuteOriginal(() => _onInitializeHook!.Original(chara), "Error invoking original character initialize.");
QueueTrack((GameObject*)chara);
try
{
_onInitializeHook!.Original(chara);
}
catch (Exception ex)
{
_logger.LogError(ex, "Error invoking original character initialize.");
}
QueueFrameworkUpdate(() => TrackGameObject((GameObject*)chara));
}
private unsafe void OnCharacterTerminated(Character* chara)
{
var address = (nint)chara;
QueueUntrack(address);
ExecuteOriginal(() => _onTerminateHook!.Original(chara), "Error invoking original character terminate.");
QueueFrameworkUpdate(() => UntrackGameObject(address));
try
{
_onTerminateHook!.Original(chara);
}
catch (Exception ex)
{
_logger.LogError(ex, "Error invoking original character terminate.");
}
}
private unsafe GameObject* OnCharacterDisposed(Character* chara, byte freeMemory)
{
var address = (nint)chara;
QueueUntrack(address);
QueueFrameworkUpdate(() => UntrackGameObject(address));
try
{
return _onDestructorHook!.Original(chara, freeMemory);
@@ -419,7 +416,7 @@ public sealed class ActorObjectService : IHostedService, IDisposable
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.Address,
descriptor.ObjectIndex,
@@ -481,196 +478,50 @@ public sealed class ActorObjectService : IHostedService, IDisposable
return (isLocalPlayer ? LightlessObjectKind.Player : null, entityId);
}
var ownerId = ResolveOwnerId(gameObject);
var localPlayerAddress = _objectTable.LocalPlayer?.Address ?? nint.Zero;
if (localPlayerAddress == nint.Zero)
return (null, ownerId);
var localEntityId = ((Character*)localPlayerAddress)->EntityId;
if (localEntityId == 0)
return (null, ownerId);
if (objectKind is DalamudObjectKind.MountType or DalamudObjectKind.Companion)
if (isLocalPlayer)
{
var expectedMinionOrMount = GetMinionOrMountAddress(localPlayerAddress, localEntityId);
if (expectedMinionOrMount != nint.Zero && (nint)gameObject == expectedMinionOrMount)
var entityId = ((Character*)gameObject)->EntityId;
return (LightlessObjectKind.Player, entityId);
}
if (_objectTable.LocalPlayer is not { } localPlayer)
return (null, 0);
var ownerId = gameObject->OwnerId;
if (ownerId == 0)
{
var character = (Character*)gameObject;
if (character != null)
{
var resolvedOwner = ownerId != 0 ? ownerId : localEntityId;
return (LightlessObjectKind.MinionOrMount, resolvedOwner);
ownerId = character->CompanionOwnerId;
if (ownerId == 0)
{
var parent = character->GetParentCharacter();
if (parent != null)
{
ownerId = parent->EntityId;
}
}
}
}
if (objectKind != DalamudObjectKind.BattleNpc)
if (ownerId == 0 || ownerId != localPlayer.EntityId)
return (null, ownerId);
if (ownerId != localEntityId)
return (null, ownerId);
var expectedPet = GetPetAddress(localPlayerAddress, localEntityId);
if (expectedPet != nint.Zero && (nint)gameObject == expectedPet)
return (LightlessObjectKind.Pet, ownerId);
var expectedCompanion = GetCompanionAddress(localPlayerAddress, localEntityId);
if (expectedCompanion != nint.Zero && (nint)gameObject == expectedCompanion)
return (LightlessObjectKind.Companion, ownerId);
return (null, ownerId);
}
private unsafe nint GetMinionOrMountAddress(nint localPlayerAddress, uint ownerEntityId)
{
if (localPlayerAddress == nint.Zero)
return nint.Zero;
var playerObject = (GameObject*)localPlayerAddress;
var candidateAddress = _objectTable.GetObjectAddress(playerObject->ObjectIndex + 1);
if (candidateAddress != nint.Zero)
var ownedKind = objectKind switch
{
var candidate = (GameObject*)candidateAddress;
var candidateKind = (DalamudObjectKind)candidate->ObjectKind;
if (candidateKind is DalamudObjectKind.MountType or DalamudObjectKind.Companion)
DalamudObjectKind.MountType => LightlessObjectKind.MinionOrMount,
DalamudObjectKind.Companion => LightlessObjectKind.MinionOrMount,
DalamudObjectKind.BattleNpc => gameObject->BattleNpcSubKind switch
{
if (ownerEntityId == 0 || ResolveOwnerId(candidate) == ownerEntityId)
return candidateAddress;
}
}
BattleNpcSubKind.Buddy => LightlessObjectKind.Companion,
BattleNpcSubKind.Pet => LightlessObjectKind.Pet,
_ => (LightlessObjectKind?)null,
},
_ => (LightlessObjectKind?)null,
};
if (ownerEntityId == 0)
return candidateAddress;
foreach (var obj in _objectTable)
{
if (obj is null || obj.Address == nint.Zero || obj.Address == localPlayerAddress)
continue;
if (obj.ObjectKind is not (DalamudObjectKind.MountType or DalamudObjectKind.Companion))
continue;
var candidate = (GameObject*)obj.Address;
if (ResolveOwnerId(candidate) == ownerEntityId)
return obj.Address;
}
return candidateAddress;
}
private unsafe nint GetPetAddress(nint localPlayerAddress, uint ownerEntityId)
{
if (localPlayerAddress == nint.Zero || ownerEntityId == 0)
return nint.Zero;
var manager = CharacterManager.Instance();
if (manager != null)
{
var candidate = (nint)manager->LookupPetByOwnerObject((BattleChara*)localPlayerAddress);
if (candidate != nint.Zero)
{
var candidateObj = (GameObject*)candidate;
if (IsPetMatch(candidateObj, ownerEntityId))
return candidate;
}
}
foreach (var obj in _objectTable)
{
if (obj is null || obj.Address == nint.Zero || obj.Address == localPlayerAddress)
continue;
if (obj.ObjectKind != DalamudObjectKind.BattleNpc)
continue;
var candidate = (GameObject*)obj.Address;
if (candidate->BattleNpcSubKind != BattleNpcSubKind.Pet)
continue;
if (ResolveOwnerId(candidate) == ownerEntityId)
return obj.Address;
}
return nint.Zero;
}
private unsafe nint GetCompanionAddress(nint localPlayerAddress, uint ownerEntityId)
{
if (localPlayerAddress == nint.Zero || ownerEntityId == 0)
return nint.Zero;
var manager = CharacterManager.Instance();
if (manager != null)
{
var candidate = (nint)manager->LookupBuddyByOwnerObject((BattleChara*)localPlayerAddress);
if (candidate != nint.Zero)
{
var candidateObj = (GameObject*)candidate;
if (IsCompanionMatch(candidateObj, ownerEntityId))
return candidate;
}
}
foreach (var obj in _objectTable)
{
if (obj is null || obj.Address == nint.Zero || obj.Address == localPlayerAddress)
continue;
if (obj.ObjectKind != DalamudObjectKind.BattleNpc)
continue;
var candidate = (GameObject*)obj.Address;
if (candidate->BattleNpcSubKind != BattleNpcSubKind.Buddy)
continue;
if (ResolveOwnerId(candidate) == ownerEntityId)
return obj.Address;
}
return nint.Zero;
}
private static unsafe bool IsPetMatch(GameObject* candidate, uint ownerEntityId)
{
if (candidate == null)
return false;
if ((DalamudObjectKind)candidate->ObjectKind != DalamudObjectKind.BattleNpc)
return false;
if (candidate->BattleNpcSubKind != BattleNpcSubKind.Pet)
return false;
return ResolveOwnerId(candidate) == ownerEntityId;
}
private static unsafe bool IsCompanionMatch(GameObject* candidate, uint ownerEntityId)
{
if (candidate == null)
return false;
if ((DalamudObjectKind)candidate->ObjectKind != DalamudObjectKind.BattleNpc)
return false;
if (candidate->BattleNpcSubKind != BattleNpcSubKind.Buddy)
return false;
return ResolveOwnerId(candidate) == ownerEntityId;
}
private static unsafe uint ResolveOwnerId(GameObject* gameObject)
{
if (gameObject == null)
return 0;
if (gameObject->OwnerId != 0)
return gameObject->OwnerId;
var character = (Character*)gameObject;
if (character == null)
return 0;
if (character->CompanionOwnerId != 0)
return character->CompanionOwnerId;
var parent = character->GetParentCharacter();
return parent != null ? parent->EntityId : 0;
return (ownedKind, ownerId);
}
private void UntrackGameObject(nint address)
@@ -683,7 +534,7 @@ public sealed class ActorObjectService : IHostedService, IDisposable
RemoveDescriptor(descriptor);
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.Address,
descriptor.ObjectIndex,
@@ -707,14 +558,10 @@ public sealed class ActorObjectService : IHostedService, IDisposable
if (!seen.Add(address))
continue;
var gameObject = (GameObject*)address;
if (_activePlayers.TryGetValue(address, out var existing))
{
RefreshDescriptorIfNeeded(existing, gameObject);
if (_activePlayers.ContainsKey(address))
continue;
}
TrackGameObject(gameObject);
TrackGameObject((GameObject*)address);
}
var stale = _activePlayers.Keys.Where(addr => !seen.Contains(addr)).ToList();
@@ -727,47 +574,6 @@ public sealed class ActorObjectService : IHostedService, IDisposable
{
_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);
_activePlayers[updated.Address] = updated;
IndexDescriptor(updated);
UpdatePendingHashResolutions(updated);
PublishSnapshot();
}
private void IndexDescriptor(ActorDescriptor descriptor)
@@ -799,15 +605,30 @@ public sealed class ActorObjectService : IHostedService, IDisposable
private unsafe void OnCompanionInitialized(Companion* companion)
{
ExecuteOriginal(() => _onCompanionInitializeHook!.Original(companion), "Error invoking original companion initialize.");
QueueTrack((GameObject*)companion);
try
{
_onCompanionInitializeHook!.Original(companion);
}
catch (Exception ex)
{
_logger.LogError(ex, "Error invoking original companion initialize.");
}
QueueFrameworkUpdate(() => TrackGameObject((GameObject*)companion));
}
private unsafe void OnCompanionTerminated(Companion* companion)
{
var address = (nint)companion;
QueueUntrack(address);
ExecuteOriginal(() => _onCompanionTerminateHook!.Original(companion), "Error invoking original companion terminate.");
QueueFrameworkUpdate(() => UntrackGameObject(address));
try
{
_onCompanionTerminateHook!.Original(companion);
}
catch (Exception ex)
{
_logger.LogError(ex, "Error invoking original companion terminate.");
}
}
private void RemoveDescriptorFromIndexes(ActorDescriptor descriptor)
@@ -833,122 +654,29 @@ public sealed class ActorObjectService : IHostedService, IDisposable
{
_activePlayers[descriptor.Address] = descriptor;
IndexDescriptor(descriptor);
UpdatePendingHashResolutions(descriptor);
_ownedTracker.OnDescriptorAdded(descriptor);
PublishSnapshot();
}
private void RemoveDescriptor(ActorDescriptor descriptor)
{
RemoveDescriptorFromIndexes(descriptor);
_pendingHashResolutions.TryRemove(descriptor.Address, out _);
_ownedTracker.OnDescriptorRemoved(descriptor);
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()
{
var descriptors = _activePlayers.Values.ToArray();
var playerCount = 0;
var ownedCount = 0;
var companionCount = 0;
var playerDescriptors = _activePlayers.Values
.Where(descriptor => descriptor.ObjectKind == DalamudObjectKind.Player)
.ToArray();
var playerAddresses = new nint[playerDescriptors.Length];
for (var i = 0; i < playerDescriptors.Length; i++)
playerAddresses[i] = playerDescriptors[i].Address;
foreach (var descriptor in descriptors)
{
if (descriptor.ObjectKind == DalamudObjectKind.Player)
playerCount++;
if (descriptor.OwnedKind is not null)
ownedCount++;
if (descriptor.ObjectKind == DalamudObjectKind.Companion)
companionCount++;
}
var playerDescriptors = new ActorDescriptor[playerCount];
var ownedDescriptors = new ActorDescriptor[ownedCount];
var playerAddresses = new nint[playerCount];
var renderedCompanions = new nint[companionCount];
var ownedAddresses = new nint[ownedCount];
var ownedMap = new Dictionary<nint, LightlessObjectKind>(ownedCount);
nint localPlayer = nint.Zero;
nint localPet = nint.Zero;
nint localMinionOrMount = nint.Zero;
nint localCompanion = nint.Zero;
var playerIndex = 0;
var ownedIndex = 0;
var companionIndex = 0;
foreach (var descriptor in descriptors)
{
if (descriptor.ObjectKind == DalamudObjectKind.Player)
{
playerDescriptors[playerIndex] = descriptor;
playerAddresses[playerIndex] = descriptor.Address;
playerIndex++;
}
if (descriptor.ObjectKind == DalamudObjectKind.Companion)
{
renderedCompanions[companionIndex] = descriptor.Address;
companionIndex++;
}
if (descriptor.OwnedKind is not { } ownedKind)
{
continue;
}
ownedDescriptors[ownedIndex] = descriptor;
ownedAddresses[ownedIndex] = descriptor.Address;
ownedMap[descriptor.Address] = ownedKind;
switch (ownedKind)
{
case LightlessObjectKind.Player:
localPlayer = descriptor.Address;
break;
case LightlessObjectKind.Pet:
localPet = descriptor.Address;
break;
case LightlessObjectKind.MinionOrMount:
localMinionOrMount = descriptor.Address;
break;
case LightlessObjectKind.Companion:
localCompanion = descriptor.Address;
break;
}
ownedIndex++;
}
var ownedSnapshot = new OwnedObjectSnapshot(
playerAddresses,
renderedCompanions,
ownedAddresses,
ownedMap,
localPlayer,
localPet,
localMinionOrMount,
localCompanion);
var ownedSnapshot = _ownedTracker.CreateSnapshot();
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);
}
@@ -966,24 +694,6 @@ public sealed class ActorObjectService : IHostedService, IDisposable
_ = _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()
{
var hadHooks = _hooksActive
@@ -1015,7 +725,7 @@ public sealed class ActorObjectService : IHostedService, IDisposable
if (hadHooks)
{
_logger.LogTrace("ActorObjectService hooks disabled.");
_logger.LogDebug("ActorObjectService hooks disabled.");
}
}
@@ -1060,89 +770,6 @@ public sealed class ActorObjectService : IHostedService, IDisposable
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)
{
if (address == nint.Zero)
@@ -1156,10 +783,13 @@ public sealed class ActorObjectService : IHostedService, IDisposable
if (drawObject == null)
return false;
if ((ulong)gameObject->RenderFlags == 2048)
if ((gameObject->RenderFlags & VisibilityFlags.Nameplate) != VisibilityFlags.None)
return false;
var characterBase = (CharacterBase*)drawObject;
if (characterBase == null)
return false;
if (characterBase->HasModelInSlotLoaded != 0)
return false;
@@ -1169,6 +799,109 @@ public sealed class ActorObjectService : IHostedService, IDisposable
return true;
}
private sealed class OwnedObjectTracker
{
private readonly HashSet<nint> _renderedPlayers = new();
private readonly HashSet<nint> _renderedCompanions = new();
private readonly Dictionary<nint, LightlessObjectKind> _ownedObjects = new();
private nint _localPlayerAddress = nint.Zero;
private nint _localPetAddress = nint.Zero;
private nint _localMinionMountAddress = nint.Zero;
private nint _localCompanionAddress = nint.Zero;
public void OnDescriptorAdded(ActorDescriptor descriptor)
{
if (descriptor.ObjectKind == DalamudObjectKind.Player)
{
_renderedPlayers.Add(descriptor.Address);
if (descriptor.IsLocalPlayer)
_localPlayerAddress = descriptor.Address;
}
else if (descriptor.ObjectKind == DalamudObjectKind.Companion)
{
_renderedCompanions.Add(descriptor.Address);
}
if (descriptor.OwnedKind is { } ownedKind)
{
_ownedObjects[descriptor.Address] = ownedKind;
switch (ownedKind)
{
case LightlessObjectKind.Player:
_localPlayerAddress = descriptor.Address;
break;
case LightlessObjectKind.Pet:
_localPetAddress = descriptor.Address;
break;
case LightlessObjectKind.MinionOrMount:
_localMinionMountAddress = descriptor.Address;
break;
case LightlessObjectKind.Companion:
_localCompanionAddress = descriptor.Address;
break;
}
}
}
public void OnDescriptorRemoved(ActorDescriptor descriptor)
{
if (descriptor.ObjectKind == DalamudObjectKind.Player)
{
_renderedPlayers.Remove(descriptor.Address);
if (descriptor.IsLocalPlayer && _localPlayerAddress == descriptor.Address)
_localPlayerAddress = nint.Zero;
}
else if (descriptor.ObjectKind == DalamudObjectKind.Companion)
{
_renderedCompanions.Remove(descriptor.Address);
if (_localCompanionAddress == descriptor.Address)
_localCompanionAddress = nint.Zero;
}
if (descriptor.OwnedKind is { } ownedKind)
{
_ownedObjects.Remove(descriptor.Address);
switch (ownedKind)
{
case LightlessObjectKind.Player when _localPlayerAddress == descriptor.Address:
_localPlayerAddress = nint.Zero;
break;
case LightlessObjectKind.Pet when _localPetAddress == descriptor.Address:
_localPetAddress = nint.Zero;
break;
case LightlessObjectKind.MinionOrMount when _localMinionMountAddress == descriptor.Address:
_localMinionMountAddress = nint.Zero;
break;
case LightlessObjectKind.Companion when _localCompanionAddress == descriptor.Address:
_localCompanionAddress = nint.Zero;
break;
}
}
}
public OwnedObjectSnapshot CreateSnapshot()
=> new(
_renderedPlayers.ToArray(),
_renderedCompanions.ToArray(),
_ownedObjects.Keys.ToArray(),
new Dictionary<nint, LightlessObjectKind>(_ownedObjects),
_localPlayerAddress,
_localPetAddress,
_localMinionMountAddress,
_localCompanionAddress);
public void Reset()
{
_renderedPlayers.Clear();
_renderedCompanions.Clear();
_ownedObjects.Clear();
_localPlayerAddress = nint.Zero;
_localPetAddress = nint.Zero;
_localMinionMountAddress = nint.Zero;
_localCompanionAddress = nint.Zero;
}
}
private sealed record OwnedObjectSnapshot(
IReadOnlyList<nint> RenderedPlayers,
IReadOnlyList<nint> RenderedCompanions,
@@ -1192,27 +925,14 @@ public sealed class ActorObjectService : IHostedService, IDisposable
private sealed record ActorSnapshot(
IReadOnlyList<ActorDescriptor> PlayerDescriptors,
IReadOnlyList<ActorDescriptor> OwnedDescriptors,
IReadOnlyList<nint> PlayerAddresses,
OwnedObjectSnapshot OwnedObjects,
int Generation)
{
public static ActorSnapshot Empty { get; } = new(
Array.Empty<ActorDescriptor>(),
Array.Empty<ActorDescriptor>(),
Array.Empty<nint>(),
OwnedObjectSnapshot.Empty,
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,16 +1,13 @@
using Dalamud.Game.ClientState.Objects.SubKinds;
using Dalamud.Game.ClientState.Objects.SubKinds;
using K4os.Compression.LZ4.Legacy;
using LightlessSync.API.Data;
using LightlessSync.API.Data.Enum;
using LightlessSync.API.Dto.CharaData;
using LightlessSync.FileCache;
using LightlessSync.LightlessConfiguration.Models;
using LightlessSync.PlayerData.Factories;
using LightlessSync.PlayerData.Handlers;
using LightlessSync.Services.CharaData;
using LightlessSync.Services.CharaData.Models;
using LightlessSync.Services.Mediator;
using LightlessSync.UI.Models;
using LightlessSync.Utils;
using LightlessSync.WebAPI.Files;
using Microsoft.Extensions.Logging;
@@ -27,11 +24,10 @@ public sealed class CharaDataFileHandler : IDisposable
private readonly ILogger<CharaDataFileHandler> _logger;
private readonly LightlessCharaFileDataFactory _lightlessCharaFileDataFactory;
private readonly PlayerDataFactory _playerDataFactory;
private readonly NotificationService _notificationService;
private int _globalFileCounter = 0;
public CharaDataFileHandler(ILogger<CharaDataFileHandler> logger, FileDownloadManagerFactory fileDownloadManagerFactory, FileUploadManager fileUploadManager, FileCacheManager fileCacheManager,
DalamudUtilService dalamudUtilService, GameObjectHandlerFactory gameObjectHandlerFactory, PlayerDataFactory playerDataFactory, NotificationService notificationService)
DalamudUtilService dalamudUtilService, GameObjectHandlerFactory gameObjectHandlerFactory, PlayerDataFactory playerDataFactory)
{
_fileDownloadManager = fileDownloadManagerFactory.Create();
_logger = logger;
@@ -40,7 +36,6 @@ public sealed class CharaDataFileHandler : IDisposable
_dalamudUtilService = dalamudUtilService;
_gameObjectHandlerFactory = gameObjectHandlerFactory;
_playerDataFactory = playerDataFactory;
_notificationService = notificationService;
_lightlessCharaFileDataFactory = new(fileCacheManager);
}
@@ -253,161 +248,54 @@ public sealed class CharaDataFileHandler : IDisposable
}
internal async Task SaveCharaFileAsync(string description, string filePath)
{
var createPlayerDataStopwatch = System.Diagnostics.Stopwatch.StartNew();
var data = await CreatePlayerData().ConfigureAwait(false);
createPlayerDataStopwatch.Stop();
_logger.LogInformation("CreatePlayerData took {elapsed}ms", createPlayerDataStopwatch.ElapsedMilliseconds);
if (data == null) return;
await Task.Run(async () => await SaveCharaFileAsyncInternal(description, filePath, data).ConfigureAwait(false)).ConfigureAwait(false);
}
private async Task SaveCharaFileAsyncInternal(string description, string filePath, CharacterData data)
{
var tempFilePath = filePath + ".tmp";
var overallStopwatch = System.Diagnostics.Stopwatch.StartNew();
try
{
var data = await CreatePlayerData().ConfigureAwait(false);
if (data == null) return;
var lightlessCharaFileData = _lightlessCharaFileDataFactory.Create(description, data);
LightlessCharaFileHeader output = new(LightlessCharaFileHeader.CurrentVersion, lightlessCharaFileData);
using var fs = new FileStream(tempFilePath, FileMode.Create, FileAccess.ReadWrite, FileShare.None, bufferSize: 65536, useAsync: false);
using var fs = new FileStream(tempFilePath, FileMode.Create, FileAccess.ReadWrite, FileShare.None);
using var lz4 = new LZ4Stream(fs, LZ4StreamMode.Compress, LZ4StreamFlags.HighCompression);
using var writer = new BinaryWriter(lz4);
output.WriteToStream(writer);
int fileIndex = 0;
long totalBytesWritten = 0;
long totalBytesToWrite = output.CharaFileData.Files.Sum(f => f.Length);
var fileWriteStopwatch = System.Diagnostics.Stopwatch.StartNew();
const long updateIntervalMs = 1000;
foreach (var item in output.CharaFileData.Files)
{
fileIndex++;
var fileStopwatch = System.Diagnostics.Stopwatch.StartNew();
var file = _fileCacheManager.GetFileCacheByHash(item.Hash)!;
_logger.LogDebug("Saving to MCDF [{fileNum}/{totalFiles}]: {hash}:{file}", fileIndex, output.CharaFileData.Files.Count, item.Hash, file.ResolvedFilepath);
_logger.LogDebug("Saving to MCDF: {hash}:{file}", item.Hash, file.ResolvedFilepath);
_logger.LogDebug("\tAssociated GamePaths:");
foreach (var path in item.GamePaths)
{
_logger.LogDebug("\t{path}", path);
}
using var fsRead = File.OpenRead(file.ResolvedFilepath);
using var br = new BinaryReader(fsRead);
byte[] buffer = new byte[item.Length];
int bytesRead = br.Read(buffer, 0, item.Length);
if (bytesRead != item.Length)
var fsRead = File.OpenRead(file.ResolvedFilepath);
await using (fsRead.ConfigureAwait(false))
{
_logger.LogWarning("Expected to read {expected} bytes but got {actual} bytes from {file}", item.Length, bytesRead, file.ResolvedFilepath);
}
writer.Write(buffer);
totalBytesWritten += bytesRead;
fileStopwatch.Stop();
_logger.LogDebug("Wrote file [{fileNum}/{totalFiles}] in {elapsed}ms ({sizeKb}kb)", fileIndex, output.CharaFileData.Files.Count, fileStopwatch.ElapsedMilliseconds, item.Length / 1024);
if (fileWriteStopwatch.ElapsedMilliseconds >= updateIntervalMs && totalBytesToWrite > 0)
{
float progress = (float)totalBytesWritten / totalBytesToWrite;
var elapsed = overallStopwatch.Elapsed;
var eta = CalculateEta(elapsed, progress);
var notification = new LightlessNotification
{
Id = "chara_file_save_progress",
Title = "Character Data",
Message = $"Compressing and saving character file... {(progress * 100):F0}%\nETA: {FormatTimespan(eta)}",
Type = NotificationType.Info,
Duration = TimeSpan.FromMinutes(5),
ShowProgress = true,
Progress = progress
};
_notificationService.Mediator.Publish(new LightlessNotificationMessage(notification));
fileWriteStopwatch.Restart();
using var br = new BinaryReader(fsRead);
byte[] buffer = new byte[item.Length];
br.Read(buffer, 0, item.Length);
writer.Write(buffer);
}
}
var flushStopwatch = System.Diagnostics.Stopwatch.StartNew();
writer.Flush();
lz4.Flush();
fs.Flush();
await lz4.FlushAsync().ConfigureAwait(false);
await fs.FlushAsync().ConfigureAwait(false);
fs.Close();
flushStopwatch.Stop();
_logger.LogInformation("Flush operations took {elapsed}ms", flushStopwatch.ElapsedMilliseconds);
var moveStopwatch = System.Diagnostics.Stopwatch.StartNew();
File.Move(tempFilePath, filePath, true);
moveStopwatch.Stop();
_logger.LogInformation("File move took {elapsed}ms", moveStopwatch.ElapsedMilliseconds);
overallStopwatch.Stop();
_logger.LogInformation("SaveCharaFileAsync completed successfully in {elapsed}ms. Total bytes written: {totalBytes}mb", overallStopwatch.ElapsedMilliseconds, totalBytesWritten / (1024 * 1024));
_notificationService.ShowNotification(
"Character Data",
"Character file saved successfully!",
NotificationType.Info,
duration: TimeSpan.FromSeconds(5));
_notificationService.Mediator.Publish(new LightlessNotificationDismissMessage("chara_file_save_progress"));
}
catch (Exception ex)
{
overallStopwatch.Stop();
_logger.LogError(ex, "Failure Saving Lightless Chara File after {elapsed}ms, deleting output", overallStopwatch.ElapsedMilliseconds);
try
{
File.Delete(tempFilePath);
}
catch (Exception deleteEx)
{
_logger.LogError(deleteEx, "Failed to delete temporary file {file}", tempFilePath);
}
_notificationService.ShowErrorNotification(
"Character Data Save Failed",
"Failed to save character file",
ex);
_notificationService.Mediator.Publish(new LightlessNotificationDismissMessage("chara_file_save_progress"));
_logger.LogError(ex, "Failure Saving Lightless Chara File, deleting output");
File.Delete(tempFilePath);
}
}
private static TimeSpan CalculateEta(TimeSpan elapsed, float progress)
{
if (progress <= 0 || elapsed.TotalSeconds < 0.1)
return TimeSpan.Zero;
double totalSeconds = elapsed.TotalSeconds / progress;
double remainingSeconds = totalSeconds - elapsed.TotalSeconds;
return TimeSpan.FromSeconds(Math.Max(0, remainingSeconds));
}
private static string FormatTimespan(TimeSpan ts)
{
if (ts.TotalSeconds < 1)
return "< 1s";
if (ts.TotalSeconds < 60)
return $"{ts.TotalSeconds:F0}s";
if (ts.TotalMinutes < 60)
return $"{ts.TotalMinutes:F1}m";
return $"{ts.TotalHours:F1}h";
}
internal async Task<List<string>> UploadFiles(List<string> fileList, ValueProgress<string> uploadProgress, CancellationToken token)
{
return await _fileUploadManager.UploadFiles(fileList, uploadProgress, token).ConfigureAwait(false);

View File

@@ -450,7 +450,7 @@ public class CharaDataGposeTogetherManager : DisposableMediatorSubscriberBase
};
}
var loc = _dalamudUtil.GetMapData();
var loc = await _dalamudUtil.GetMapDataAsync().ConfigureAwait(false);
worldData.LocationInfo = loc;
if (_forceResendWorldData || worldData != _lastWorldData)

View File

@@ -254,7 +254,7 @@ public sealed partial class CharaDataManager : DisposableMediatorSubscriberBase
Logger.LogTrace("Attaching World data {data}", worldData);
worldData.LocationInfo = _dalamudUtilService.GetMapData();
worldData.LocationInfo = await _dalamudUtilService.GetMapDataAsync().ConfigureAwait(false);
Logger.LogTrace("World data serialized: {data}", worldData);

View File

@@ -186,8 +186,8 @@ public sealed class CharaDataNearbyManager : DisposableMediatorSubscriberBase
var previousPoses = _nearbyData.Keys.ToList();
_nearbyData.Clear();
var ownLocation = _dalamudUtilService.GetMapData();
var player = await _dalamudUtilService.GetPlayerCharacterAsync().ConfigureAwait(false);
var ownLocation = await _dalamudUtilService.RunOnFrameworkThread(() => _dalamudUtilService.GetMapData()).ConfigureAwait(false);
var player = await _dalamudUtilService.RunOnFrameworkThread(() => _dalamudUtilService.GetPlayerCharacter()).ConfigureAwait(false);
var currentServer = player.CurrentWorld;
var playerPos = player.Position;

View File

@@ -1,4 +1,3 @@
using FFXIVClientStructs.FFXIV.Client.Graphics.Render;
using LightlessSync.API.Data;
using LightlessSync.API.Data.Enum;
using LightlessSync.FileCache;
@@ -99,13 +98,11 @@ public sealed class CharacterAnalyzer : MediatorSubscriberBase, IDisposable
_analysisCts = null;
if (print) PrintAnalysis();
}
public void Dispose()
{
_analysisCts.CancelDispose();
_baseAnalysisCts.Dispose();
}
public async Task UpdateFileEntriesAsync(IEnumerable<string> filePaths, CancellationToken token)
{
var normalized = new HashSet<string>(
@@ -128,7 +125,6 @@ public sealed class CharacterAnalyzer : MediatorSubscriberBase, IDisposable
}
}
}
private async Task BaseAnalysis(CharacterData charaData, CancellationToken token)
{
if (string.Equals(charaData.DataHash.Value, _lastDataHash, StringComparison.Ordinal)) return;
@@ -140,47 +136,29 @@ public sealed class CharacterAnalyzer : MediatorSubscriberBase, IDisposable
{
token.ThrowIfCancellationRequested();
var fileCacheEntries = (await _fileCacheManager
.GetAllFileCachesByHashAsync(fileEntry.Hash, ignoreCacheEntries: true, validate: false, token)
.ConfigureAwait(false))
.ToList();
if (fileCacheEntries.Count == 0)
continue;
var resolved = fileCacheEntries[0].ResolvedFilepath;
var extWithDot = Path.GetExtension(resolved);
var ext = string.IsNullOrEmpty(extWithDot) ? "unk?" : extWithDot.TrimStart('.');
var tris = await _xivDataAnalyzer.GetTrianglesByHash(fileEntry.Hash).ConfigureAwait(false);
var distinctFilePaths = fileCacheEntries
.Select(c => c.ResolvedFilepath)
.Where(p => !string.IsNullOrWhiteSpace(p))
.Distinct(StringComparer.OrdinalIgnoreCase)
.ToList();
long orig = 0, comp = 0;
var first = fileCacheEntries[0];
if (first.Size > 0) orig = first.Size.Value;
if (first.CompressedSize > 0) comp = first.CompressedSize.Value;
if (_fileCacheManager.TryGetSizeInfo(fileEntry.Hash, out var cached))
var fileCacheEntries = (await _fileCacheManager.GetAllFileCachesByHashAsync(fileEntry.Hash, ignoreCacheEntries: true, validate: false, token).ConfigureAwait(false)).ToList();
if (fileCacheEntries.Count == 0) continue;
var filePath = fileCacheEntries[0].ResolvedFilepath;
FileInfo fi = new(filePath);
string ext = "unk?";
try
{
if (orig <= 0 && cached.Original > 0) orig = cached.Original;
if (comp <= 0 && cached.Compressed > 0) comp = cached.Compressed;
ext = fi.Extension[1..];
}
catch (Exception ex)
{
Logger.LogWarning(ex, "Could not identify extension for {path}", filePath);
}
var tris = await _xivDataAnalyzer.GetTrianglesByHash(fileEntry.Hash).ConfigureAwait(false);
foreach (var entry in fileCacheEntries)
{
data[fileEntry.Hash] = new FileDataEntry(fileEntry.Hash, ext,
[.. fileEntry.GamePaths],
[.. fileCacheEntries.Select(c => c.ResolvedFilepath).Distinct(StringComparer.Ordinal)],
entry.Size > 0 ? entry.Size.Value : 0,
entry.CompressedSize > 0 ? entry.CompressedSize.Value : 0,
tris);
}
data[fileEntry.Hash] = new FileDataEntry(
fileEntry.Hash,
ext,
[.. fileEntry.GamePaths],
distinctFilePaths,
orig,
comp,
tris,
fileCacheEntries);
}
LastAnalysis[obj.Key] = data;
}
@@ -189,7 +167,6 @@ public sealed class CharacterAnalyzer : MediatorSubscriberBase, IDisposable
Mediator.Publish(new CharacterDataAnalyzedMessage());
_lastDataHash = charaData.DataHash.Value;
}
private void RecalculateSummary()
{
var builder = ImmutableDictionary.CreateBuilder<ObjectKind, CharacterAnalysisObjectSummary>();
@@ -215,7 +192,6 @@ public sealed class CharacterAnalyzer : MediatorSubscriberBase, IDisposable
_latestSummary = new CharacterAnalysisSummary(builder.ToImmutable());
}
private void PrintAnalysis()
{
if (LastAnalysis.Count == 0) return;
@@ -259,79 +235,42 @@ public sealed class CharacterAnalyzer : MediatorSubscriberBase, IDisposable
UiSharedService.ByteToString(LastAnalysis.Values.Sum(c => c.Values.Sum(v => v.CompressedSize))));
Logger.LogInformation("IMPORTANT NOTES:\n\r- For Lightless up- and downloads only the compressed size is relevant.\n\r- An unusually high total files count beyond 200 and up will also increase your download time to others significantly.");
}
internal sealed class FileDataEntry
internal sealed record FileDataEntry(string Hash, string FileType, List<string> GamePaths, List<string> FilePaths, long OriginalSize, long CompressedSize, long Triangles)
{
public string Hash { get; }
public string FileType { get; }
public List<string> GamePaths { get; }
public List<string> FilePaths { get; }
public long OriginalSize { get; private set; }
public long CompressedSize { get; private set; }
public long Triangles { get; private set; }
public IReadOnlyList<FileCacheEntity> CacheEntries { get; }
public bool IsComputed => OriginalSize > 0 && CompressedSize > 0;
public FileDataEntry(
string hash,
string fileType,
List<string> gamePaths,
List<string> filePaths,
long originalSize,
long compressedSize,
long triangles,
IReadOnlyList<FileCacheEntity> cacheEntries)
public async Task ComputeSizes(FileCacheManager fileCacheManager, CancellationToken token)
{
Hash = hash;
FileType = fileType;
GamePaths = gamePaths;
FilePaths = filePaths;
OriginalSize = originalSize;
CompressedSize = compressedSize;
Triangles = triangles;
CacheEntries = cacheEntries;
var compressedsize = await fileCacheManager.GetCompressedFileData(Hash, token).ConfigureAwait(false);
var normalSize = new FileInfo(FilePaths[0]).Length;
var entries = await fileCacheManager.GetAllFileCachesByHashAsync(Hash, ignoreCacheEntries: true, validate: false, token).ConfigureAwait(false);
foreach (var entry in entries)
{
entry.Size = normalSize;
entry.CompressedSize = compressedsize.Item2.LongLength;
}
OriginalSize = normalSize;
CompressedSize = compressedsize.Item2.LongLength;
RefreshFormat();
}
public async Task ComputeSizes(FileCacheManager fileCacheManager, CancellationToken token, bool force = false)
{
if (!force && IsComputed)
return;
if (FilePaths.Count == 0 || string.IsNullOrWhiteSpace(FilePaths[0]))
return;
var path = FilePaths[0];
if (!File.Exists(path))
return;
var original = new FileInfo(path).Length;
var compressedLen = await fileCacheManager.GetCompressedSizeAsync(Hash, token).ConfigureAwait(false);
fileCacheManager.SetSizeInfo(Hash, original, compressedLen);
FileCacheManager.ApplySizesToEntries(CacheEntries, original, compressedLen);
OriginalSize = original;
CompressedSize = compressedLen;
if (string.Equals(FileType, "tex", StringComparison.OrdinalIgnoreCase))
RefreshFormat();
}
public long OriginalSize { get; private set; } = OriginalSize;
public long CompressedSize { get; private set; } = CompressedSize;
public long Triangles { get; private set; } = Triangles;
public Lazy<string> Format => _format ??= CreateFormatValue();
private Lazy<string>? _format;
public void RefreshFormat() => _format = CreateFormatValue();
public void RefreshFormat()
{
_format = CreateFormatValue();
}
private Lazy<string> CreateFormatValue()
=> new(() =>
{
if (!string.Equals(FileType, "tex", StringComparison.OrdinalIgnoreCase))
if (!string.Equals(FileType, "tex", StringComparison.Ordinal))
{
return string.Empty;
}
try
{

View File

@@ -1,275 +0,0 @@
using Dalamud.Interface.Textures.TextureWraps;
using LightlessSync.UI;
using Microsoft.Extensions.Logging;
using System.Collections.Concurrent;
using System.Text.Json;
namespace LightlessSync.Services.Chat;
public sealed class ChatEmoteService : IDisposable
{
private const string GlobalEmoteSetUrl = "https://7tv.io/v3/emote-sets/global";
private readonly ILogger<ChatEmoteService> _logger;
private readonly HttpClient _httpClient;
private readonly UiSharedService _uiSharedService;
private readonly ConcurrentDictionary<string, EmoteEntry> _emotes = new(StringComparer.Ordinal);
private readonly SemaphoreSlim _downloadGate = new(3, 3);
private readonly object _loadLock = new();
private Task? _loadTask;
public ChatEmoteService(ILogger<ChatEmoteService> logger, HttpClient httpClient, UiSharedService uiSharedService)
{
_logger = logger;
_httpClient = httpClient;
_uiSharedService = uiSharedService;
}
public void EnsureGlobalEmotesLoaded()
{
lock (_loadLock)
{
if (_loadTask is not null && !_loadTask.IsCompleted)
{
return;
}
if (_emotes.Count > 0)
{
return;
}
_loadTask = Task.Run(LoadGlobalEmotesAsync);
}
}
public IReadOnlyList<string> GetEmoteNames()
{
EnsureGlobalEmotesLoaded();
var names = _emotes.Keys.ToArray();
Array.Sort(names, StringComparer.OrdinalIgnoreCase);
return names;
}
public bool TryGetEmote(string code, out IDalamudTextureWrap? texture)
{
texture = null;
EnsureGlobalEmotesLoaded();
if (!_emotes.TryGetValue(code, out var entry))
{
return false;
}
if (entry.Texture is not null)
{
texture = entry.Texture;
return true;
}
entry.EnsureLoading(QueueEmoteDownload);
return true;
}
public void Dispose()
{
foreach (var entry in _emotes.Values)
{
entry.Texture?.Dispose();
}
_downloadGate.Dispose();
}
private async Task LoadGlobalEmotesAsync()
{
try
{
using var stream = await _httpClient.GetStreamAsync(GlobalEmoteSetUrl).ConfigureAwait(false);
using var document = await JsonDocument.ParseAsync(stream).ConfigureAwait(false);
if (!document.RootElement.TryGetProperty("emotes", out var emotes))
{
_logger.LogWarning("7TV emote set response missing emotes array");
return;
}
foreach (var emoteElement in emotes.EnumerateArray())
{
if (!emoteElement.TryGetProperty("name", out var nameElement))
{
continue;
}
var name = nameElement.GetString();
if (string.IsNullOrWhiteSpace(name))
{
continue;
}
var url = TryBuildEmoteUrl(emoteElement);
if (string.IsNullOrWhiteSpace(url))
{
continue;
}
_emotes.TryAdd(name, new EmoteEntry(url));
}
}
catch (Exception ex)
{
_logger.LogWarning(ex, "Failed to load 7TV emote set");
}
}
private static string? TryBuildEmoteUrl(JsonElement emoteElement)
{
if (!emoteElement.TryGetProperty("data", out var dataElement))
{
return null;
}
if (!dataElement.TryGetProperty("host", out var hostElement))
{
return null;
}
if (!hostElement.TryGetProperty("url", out var urlElement))
{
return null;
}
var baseUrl = urlElement.GetString();
if (string.IsNullOrWhiteSpace(baseUrl))
{
return null;
}
if (baseUrl.StartsWith("//", StringComparison.Ordinal))
{
baseUrl = "https:" + baseUrl;
}
if (!hostElement.TryGetProperty("files", out var filesElement))
{
return null;
}
var fileName = PickBestStaticFile(filesElement);
if (string.IsNullOrWhiteSpace(fileName))
{
return null;
}
return baseUrl.TrimEnd('/') + "/" + fileName;
}
private static string? PickBestStaticFile(JsonElement filesElement)
{
string? png1x = null;
string? webp1x = null;
string? pngFallback = null;
string? webpFallback = null;
foreach (var file in filesElement.EnumerateArray())
{
if (file.TryGetProperty("static", out var staticElement) && staticElement.ValueKind == JsonValueKind.False)
{
continue;
}
if (!file.TryGetProperty("name", out var nameElement))
{
continue;
}
var name = nameElement.GetString();
if (string.IsNullOrWhiteSpace(name))
{
continue;
}
if (name.Equals("1x.png", StringComparison.OrdinalIgnoreCase))
{
png1x = name;
}
else if (name.Equals("1x.webp", StringComparison.OrdinalIgnoreCase))
{
webp1x = name;
}
else if (name.EndsWith(".png", StringComparison.OrdinalIgnoreCase) && pngFallback is null)
{
pngFallback = name;
}
else if (name.EndsWith(".webp", StringComparison.OrdinalIgnoreCase) && webpFallback is null)
{
webpFallback = name;
}
}
return png1x ?? webp1x ?? pngFallback ?? webpFallback;
}
private void QueueEmoteDownload(EmoteEntry entry)
{
_ = Task.Run(async () =>
{
await _downloadGate.WaitAsync().ConfigureAwait(false);
try
{
var data = await _httpClient.GetByteArrayAsync(entry.Url).ConfigureAwait(false);
var texture = _uiSharedService.LoadImage(data);
entry.SetTexture(texture);
}
catch (Exception ex)
{
_logger.LogDebug(ex, "Failed to load 7TV emote {Url}", entry.Url);
entry.MarkFailed();
}
finally
{
_downloadGate.Release();
}
});
}
private sealed class EmoteEntry
{
private int _loadingState;
public EmoteEntry(string url)
{
Url = url;
}
public string Url { get; }
public IDalamudTextureWrap? Texture { get; private set; }
public void EnsureLoading(Action<EmoteEntry> queueDownload)
{
if (Texture is not null)
{
return;
}
if (Interlocked.CompareExchange(ref _loadingState, 1, 0) != 0)
{
return;
}
queueDownload(this);
}
public void SetTexture(IDalamudTextureWrap texture)
{
Texture = texture;
Interlocked.Exchange(ref _loadingState, 0);
}
public void MarkFailed()
{
Interlocked.Exchange(ref _loadingState, 0);
}
}
}

View File

@@ -1,8 +1,6 @@
using LightlessSync.API.Dto.Chat;
using LightlessSync.API.Data.Extensions;
using LightlessSync.Services.ActorTracking;
using LightlessSync.Services.Mediator;
using LightlessSync.Services.ServerConfiguration;
using LightlessSync.WebAPI;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
@@ -26,7 +24,6 @@ public sealed class ZoneChatService : DisposableMediatorSubscriberBase, IHostedS
private readonly ActorObjectService _actorObjectService;
private readonly PairUiService _pairUiService;
private readonly ChatConfigService _chatConfigService;
private readonly ServerConfigurationManager _serverConfigurationManager;
private readonly Lock _sync = new();
@@ -39,9 +36,6 @@ public sealed class ZoneChatService : DisposableMediatorSubscriberBase, IHostedS
private readonly Dictionary<string, bool> _lastPresenceStates = new(StringComparer.Ordinal);
private readonly Dictionary<string, string> _selfTokens = new(StringComparer.Ordinal);
private readonly List<PendingSelfMessage> _pendingSelfMessages = new();
private readonly Dictionary<string, List<ChatMessageEntry>> _messageHistoryCache = new(StringComparer.Ordinal);
private List<ChatChannelSnapshot>? _cachedChannelSnapshots;
private bool _channelsSnapshotDirty = true;
private bool _isLoggedIn;
private bool _isConnected;
@@ -57,8 +51,7 @@ public sealed class ZoneChatService : DisposableMediatorSubscriberBase, IHostedS
ApiController apiController,
DalamudUtilService dalamudUtilService,
ActorObjectService actorObjectService,
PairUiService pairUiService,
ServerConfigurationManager serverConfigurationManager)
PairUiService pairUiService)
: base(logger, mediator)
{
_apiController = apiController;
@@ -66,7 +59,6 @@ public sealed class ZoneChatService : DisposableMediatorSubscriberBase, IHostedS
_actorObjectService = actorObjectService;
_pairUiService = pairUiService;
_chatConfigService = chatConfigService;
_serverConfigurationManager = serverConfigurationManager;
_isLoggedIn = _dalamudUtilService.IsLoggedIn;
_isConnected = _apiController.IsConnected;
@@ -77,11 +69,6 @@ public sealed class ZoneChatService : DisposableMediatorSubscriberBase, IHostedS
{
using (_sync.EnterScope())
{
if (!_channelsSnapshotDirty && _cachedChannelSnapshots is not null)
{
return _cachedChannelSnapshots;
}
var snapshots = new List<ChatChannelSnapshot>(_channelOrder.Count);
foreach (var key in _channelOrder)
{
@@ -111,8 +98,6 @@ public sealed class ZoneChatService : DisposableMediatorSubscriberBase, IHostedS
state.Messages.ToList()));
}
_cachedChannelSnapshots = snapshots;
_channelsSnapshotDirty = false;
return snapshots;
}
}
@@ -150,8 +135,6 @@ public sealed class ZoneChatService : DisposableMediatorSubscriberBase, IHostedS
state.UnreadCount = 0;
_lastReadCounts[key] = state.Messages.Count;
}
MarkChannelsSnapshotDirtyLocked();
}
}
@@ -203,7 +186,6 @@ public sealed class ZoneChatService : DisposableMediatorSubscriberBase, IHostedS
if (!wasEnabled)
{
_chatEnabled = true;
MarkChannelsSnapshotDirtyLocked();
}
}
@@ -249,8 +231,6 @@ public sealed class ZoneChatService : DisposableMediatorSubscriberBase, IHostedS
state.IsAvailable = false;
state.StatusText = "Chat services disabled";
}
MarkChannelsSnapshotDirtyLocked();
}
UnregisterChatHandler();
@@ -576,7 +556,7 @@ public sealed class ZoneChatService : DisposableMediatorSubscriberBase, IHostedS
try
{
var location = _dalamudUtilService.GetMapData();
var location = await _dalamudUtilService.GetMapDataAsync().ConfigureAwait(false);
var territoryId = (ushort)location.TerritoryId;
var worldId = (ushort)location.ServerId;
@@ -702,7 +682,7 @@ public sealed class ZoneChatService : DisposableMediatorSubscriberBase, IHostedS
{
try
{
var worldId = (ushort)_dalamudUtilService.GetWorldId();
var worldId = (ushort)await _dalamudUtilService.GetWorldIdAsync().ConfigureAwait(false);
return definition.Descriptor with { WorldId = worldId };
}
catch (Exception ex)
@@ -737,7 +717,7 @@ public sealed class ZoneChatService : DisposableMediatorSubscriberBase, IHostedS
_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 variant in EnumerateTerritoryKeys(kvp.Value))
@@ -781,7 +761,6 @@ public sealed class ZoneChatService : DisposableMediatorSubscriberBase, IHostedS
using (_sync.EnterScope())
{
var remainingGroups = new HashSet<string>(_groupDefinitions.Keys, StringComparer.OrdinalIgnoreCase);
var allowRemoval = _isConnected;
foreach (var info in infoList)
{
@@ -797,19 +776,18 @@ public sealed class ZoneChatService : DisposableMediatorSubscriberBase, IHostedS
var key = BuildChannelKey(descriptor);
if (!_channels.TryGetValue(key, out var state))
{
state = new ChatChannelState(key, ChatChannelType.Group, info.DisplayName ?? groupId, descriptor);
var restoredCount = RestoreCachedMessagesLocked(state);
state.IsConnected = _chatEnabled && _isConnected;
state.IsAvailable = _chatEnabled && _isConnected;
state.StatusText = !_chatEnabled
? "Chat services disabled"
: (_isConnected ? null : "Disconnected from chat server");
_channels[key] = state;
_lastReadCounts[key] = restoredCount > 0 ? state.Messages.Count : 0;
if (_chatEnabled)
{
descriptorsToJoin.Add(descriptor);
}
state = new ChatChannelState(key, ChatChannelType.Group, info.DisplayName ?? groupId, descriptor);
state.IsConnected = _chatEnabled && _isConnected;
state.IsAvailable = _chatEnabled && _isConnected;
state.StatusText = !_chatEnabled
? "Chat services disabled"
: (_isConnected ? null : "Disconnected from chat server");
_channels[key] = state;
_lastReadCounts[key] = 0;
if (_chatEnabled)
{
descriptorsToJoin.Add(descriptor);
}
}
else
{
@@ -823,30 +801,26 @@ public sealed class ZoneChatService : DisposableMediatorSubscriberBase, IHostedS
}
}
if (allowRemoval)
foreach (var removedGroupId in remainingGroups)
{
foreach (var removedGroupId in remainingGroups)
if (_groupDefinitions.TryGetValue(removedGroupId, out var definition))
{
if (_groupDefinitions.TryGetValue(removedGroupId, out var definition))
var key = BuildChannelKey(definition.Descriptor);
if (_channels.TryGetValue(key, out var state))
{
var key = BuildChannelKey(definition.Descriptor);
if (_channels.TryGetValue(key, out var state))
descriptorsToLeave.Add(state.Descriptor);
_channels.Remove(key);
_lastReadCounts.Remove(key);
_lastPresenceStates.Remove(BuildPresenceKey(state.Descriptor));
_selfTokens.Remove(key);
_pendingSelfMessages.RemoveAll(p => string.Equals(p.ChannelKey, key, StringComparison.Ordinal));
if (string.Equals(_activeChannelKey, key, StringComparison.Ordinal))
{
CacheMessagesLocked(state);
descriptorsToLeave.Add(state.Descriptor);
_channels.Remove(key);
_lastReadCounts.Remove(key);
_lastPresenceStates.Remove(BuildPresenceKey(state.Descriptor));
_selfTokens.Remove(key);
_pendingSelfMessages.RemoveAll(p => string.Equals(p.ChannelKey, key, StringComparison.Ordinal));
if (string.Equals(_activeChannelKey, key, StringComparison.Ordinal))
{
_activeChannelKey = null;
}
_activeChannelKey = null;
}
_groupDefinitions.Remove(removedGroupId);
}
_groupDefinitions.Remove(removedGroupId);
}
}
@@ -879,12 +853,6 @@ public sealed class ZoneChatService : DisposableMediatorSubscriberBase, IHostedS
var infos = new List<GroupChatChannelInfoDto>(groups.Count);
foreach (var group in groups)
{
// basically prune the channel if it's disabled
if (group.GroupPermissions.IsDisableChat())
{
continue;
}
var descriptor = new ChatChannelDescriptor
{
Type = ChatChannelType.Group,
@@ -1024,14 +992,13 @@ public sealed class ZoneChatService : DisposableMediatorSubscriberBase, IHostedS
descriptor.Type,
displayName,
descriptor.Type == ChatChannelType.Zone ? (_lastZoneDescriptor ?? descriptor) : descriptor);
var restoredCount = RestoreCachedMessagesLocked(state);
state.IsConnected = _isConnected;
state.IsAvailable = descriptor.Type == ChatChannelType.Group && _isConnected;
state.StatusText = descriptor.Type == ChatChannelType.Zone ? ZoneUnavailableMessage : (_isConnected ? null : "Disconnected from chat server");
_channels[key] = state;
_lastReadCounts[key] = restoredCount > 0 ? state.Messages.Count : 0;
_lastReadCounts[key] = 0;
publishChannelList = true;
}
@@ -1056,8 +1023,6 @@ public sealed class ZoneChatService : DisposableMediatorSubscriberBase, IHostedS
state.UnreadCount = Math.Min(Math.Max(unreadFromHistory, incrementalUnread), MaxUnreadCount);
state.HasUnread = state.UnreadCount > 0;
}
MarkChannelsSnapshotDirtyLocked();
}
Mediator.Publish(new ChatChannelMessageAdded(key, message));
@@ -1161,7 +1126,7 @@ public sealed class ZoneChatService : DisposableMediatorSubscriberBase, IHostedS
{
try
{
return _dalamudUtilService.GetPlayerName();
return _dalamudUtilService.GetPlayerNameAsync().ConfigureAwait(false).GetAwaiter().GetResult();
}
catch (Exception ex)
{
@@ -1171,15 +1136,6 @@ public sealed class ZoneChatService : DisposableMediatorSubscriberBase, IHostedS
if (dto.Sender.Kind == ChatSenderKind.IdentifiedUser && dto.Sender.User is not null)
{
if (dto.Channel.Type != ChatChannelType.Group || _chatConfigService.Current.ShowNotesInSyncshellChat)
{
var note = _serverConfigurationManager.GetNoteForUid(dto.Sender.User.UID);
if (!string.IsNullOrWhiteSpace(note))
{
return note;
}
}
return dto.Sender.User.AliasOrUID;
}
@@ -1248,25 +1204,9 @@ public sealed class ZoneChatService : DisposableMediatorSubscriberBase, IHostedS
{
_activeChannelKey = _channelOrder.Count > 0 ? _channelOrder[0] : null;
}
MarkChannelsSnapshotDirtyLocked();
}
private void MarkChannelsSnapshotDirty()
{
using (_sync.EnterScope())
{
_channelsSnapshotDirty = true;
}
}
private void MarkChannelsSnapshotDirtyLocked() => _channelsSnapshotDirty = true;
private void PublishChannelListChanged()
{
MarkChannelsSnapshotDirty();
Mediator.Publish(new ChatChannelsUpdated());
}
private void PublishChannelListChanged() => Mediator.Publish(new ChatChannelsUpdated());
private static IEnumerable<string> EnumerateTerritoryKeys(string? value)
{
@@ -1309,12 +1249,11 @@ public sealed class ZoneChatService : DisposableMediatorSubscriberBase, IHostedS
if (!_channels.TryGetValue(ZoneChannelKey, out var state))
{
state = new ChatChannelState(ZoneChannelKey, ChatChannelType.Zone, "Zone Chat", new ChatChannelDescriptor { Type = ChatChannelType.Zone });
var restoredCount = RestoreCachedMessagesLocked(state);
state.IsConnected = _chatEnabled && _isConnected;
state.IsAvailable = false;
state.StatusText = _chatEnabled ? ZoneUnavailableMessage : "Chat services disabled";
_channels[ZoneChannelKey] = state;
_lastReadCounts[ZoneChannelKey] = restoredCount > 0 ? state.Messages.Count : 0;
_lastReadCounts[ZoneChannelKey] = 0;
UpdateChannelOrderLocked();
}
@@ -1323,11 +1262,6 @@ public sealed class ZoneChatService : DisposableMediatorSubscriberBase, IHostedS
private void RemoveZoneStateLocked()
{
if (_channels.TryGetValue(ZoneChannelKey, out var existing))
{
CacheMessagesLocked(existing);
}
if (_channels.Remove(ZoneChannelKey))
{
_lastReadCounts.Remove(ZoneChannelKey);
@@ -1342,28 +1276,6 @@ public sealed class ZoneChatService : DisposableMediatorSubscriberBase, IHostedS
}
}
private void CacheMessagesLocked(ChatChannelState state)
{
if (state.Messages.Count == 0)
{
return;
}
_messageHistoryCache[state.Key] = new List<ChatMessageEntry>(state.Messages);
}
private int RestoreCachedMessagesLocked(ChatChannelState state)
{
if (_messageHistoryCache.TryGetValue(state.Key, out var cached) && cached.Count > 0)
{
state.Messages.AddRange(cached);
_messageHistoryCache.Remove(state.Key);
return cached.Count;
}
return 0;
}
private sealed class ChatChannelState
{
public ChatChannelState(string key, ChatChannelType type, string displayName, ChatChannelDescriptor descriptor)

View File

@@ -49,7 +49,7 @@ public sealed class CommandManagerService : IDisposable
"\t /light analyze - Opens the Lightless Character Data Analysis window" + Environment.NewLine +
"\t /light settings - Opens the Lightless Settings window" + Environment.NewLine +
"\t /light finder - Opens the Lightfinder window" + Environment.NewLine +
"\t /light chat - Opens the Lightless Chat window"
"\t /light finder - Opens the Lightless Chat window"
});
}

View File

@@ -4,22 +4,21 @@ using Dalamud.Plugin;
using Dalamud.Plugin.Services;
using LightlessSync.LightlessConfiguration;
using LightlessSync.LightlessConfiguration.Models;
using LightlessSync.Services.LightFinder;
using LightlessSync.Services.Mediator;
using LightlessSync.UI;
using LightlessSync.UI.Services;
using LightlessSync.Utils;
using LightlessSync.WebAPI;
using Lumina.Excel.Sheets;
using LightlessSync.UI.Services;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
using LightlessSync.UI;
using LightlessSync.Services.LightFinder;
namespace LightlessSync.Services;
internal class ContextMenuService : IHostedService
{
private readonly IContextMenu _contextMenu;
private readonly IChatGui _chatGui;
private readonly IDalamudPluginInterface _pluginInterface;
private readonly IDataManager _gameData;
private readonly ILogger<ContextMenuService> _logger;
@@ -30,7 +29,6 @@ internal class ContextMenuService : IHostedService
private readonly ApiController _apiController;
private readonly IObjectTable _objectTable;
private readonly LightlessConfigService _configService;
private readonly NotificationService _lightlessNotification;
private readonly LightFinderScannerService _broadcastScannerService;
private readonly LightFinderService _broadcastService;
private readonly LightlessProfileManager _lightlessProfileManager;
@@ -45,7 +43,7 @@ internal class ContextMenuService : IHostedService
ILogger<ContextMenuService> logger,
DalamudUtilService dalamudUtil,
ApiController apiController,
IObjectTable objectTable,
IObjectTable objectTable,
LightlessConfigService configService,
PairRequestService pairRequestService,
PairUiService pairUiService,
@@ -53,9 +51,7 @@ internal class ContextMenuService : IHostedService
LightFinderScannerService broadcastScannerService,
LightFinderService broadcastService,
LightlessProfileManager lightlessProfileManager,
LightlessMediator mediator,
IChatGui chatGui,
NotificationService lightlessNotification)
LightlessMediator mediator)
{
_contextMenu = contextMenu;
_pluginInterface = pluginInterface;
@@ -72,8 +68,6 @@ internal class ContextMenuService : IHostedService
_broadcastService = broadcastService;
_lightlessProfileManager = lightlessProfileManager;
_mediator = mediator;
_chatGui = chatGui;
_lightlessNotification = lightlessNotification;
}
public Task StartAsync(CancellationToken cancellationToken)
@@ -105,12 +99,6 @@ internal class ContextMenuService : IHostedService
if (!_pluginInterface.UiBuilder.ShouldModifyUi)
return;
if (!_configService.Current.EnableRightClickMenus)
{
_logger.LogTrace("Right-click menus are disabled in configuration.");
return;
}
if (args.AddonName != null)
{
var addonName = args.AddonName;
@@ -141,6 +129,7 @@ internal class ContextMenuService : IHostedService
var snapshot = _pairUiService.GetSnapshot();
var pair = snapshot.PairsByUid.Values.FirstOrDefault(p =>
p.IsVisible &&
p.PlayerCharacterId != uint.MaxValue &&
p.PlayerCharacterId == target.TargetObjectId);
@@ -210,18 +199,6 @@ internal class ContextMenuService : IHostedService
.Where(p => p.IsVisible && p.PlayerCharacterId != uint.MaxValue)
.Select(p => (ulong)p.PlayerCharacterId)];
private void NotifyInChat(string message, NotificationType type = NotificationType.Info)
{
if (!_configService.Current.UseLightlessNotifications || (_configService.Current.LightlessPairRequestNotification == NotificationLocation.Chat || _configService.Current.LightlessPairRequestNotification == NotificationLocation.ChatAndLightlessUi))
{
var chatMsg = $"[Lightless] {message}";
if (type == NotificationType.Error)
_chatGui.PrintError(chatMsg);
else
_chatGui.Print(chatMsg);
}
}
private async Task HandleSelection(IMenuArgs args)
{
if (args.Target is not MenuTargetDefault target)
@@ -241,7 +218,7 @@ internal class ContextMenuService : IHostedService
return;
}
var senderCid = _dalamudUtil.GetCID().ToString().GetHash256();
var senderCid = (await _dalamudUtil.GetCIDAsync().ConfigureAwait(false)).ToString().GetHash256();
var receiverCid = DalamudUtilService.GetHashedCIDFromPlayerPointer(targetData.Address);
_logger.LogInformation("Sending pair request: sender {SenderCid}, receiver {ReceiverCid}", senderCid, receiverCid);
@@ -250,9 +227,6 @@ internal class ContextMenuService : IHostedService
{
_pairRequestService.RemoveRequest(receiverCid);
}
// Notify in chat when NotificationService is disabled
NotifyInChat($"Pair request sent to {target.TargetName}@{world.Name}.", NotificationType.Info);
}
catch (Exception ex)
{

View File

@@ -1,4 +1,5 @@
using Dalamud.Game.ClientState.Conditions;
using Dalamud.Game.ClientState.Objects;
using Dalamud.Game.ClientState.Objects.SubKinds;
using Dalamud.Game.ClientState.Objects.Types;
using Dalamud.Plugin.Services;
@@ -23,7 +24,6 @@ using Microsoft.Extensions.Logging;
using System.Numerics;
using System.Runtime.CompilerServices;
using System.Text;
using BattleNpcSubKind = FFXIVClientStructs.FFXIV.Client.Game.Object.BattleNpcSubKind;
using DalamudObjectKind = Dalamud.Game.ClientState.Objects.Enums.ObjectKind;
using GameObject = FFXIVClientStructs.FFXIV.Client.Game.Object.GameObject;
using VisibilityFlags = FFXIVClientStructs.FFXIV.Client.Game.Object.VisibilityFlags;
@@ -37,7 +37,6 @@ public class DalamudUtilService : IHostedService, IMediatorSubscriber
private readonly ICondition _condition;
private readonly IDataManager _gameData;
private readonly IGameConfig _gameConfig;
private readonly IPlayerState _playerState;
private readonly BlockedCharacterHandler _blockedCharacterHandler;
private readonly IFramework _framework;
private readonly IGameGui _gameGui;
@@ -61,7 +60,7 @@ public class DalamudUtilService : IHostedService, IMediatorSubscriber
private Lazy<ulong> _cid;
public DalamudUtilService(ILogger<DalamudUtilService> logger, IClientState clientState, IObjectTable objectTable, IFramework framework,
IGameGui gameGui, ICondition condition, IDataManager gameData, ITargetManager targetManager, IGameConfig gameConfig, IPlayerState playerState,
IGameGui gameGui, ICondition condition, IDataManager gameData, ITargetManager targetManager, IGameConfig gameConfig,
ActorObjectService actorObjectService, BlockedCharacterHandler blockedCharacterHandler, LightlessMediator mediator, PerformanceCollectorService performanceCollector,
LightlessConfigService configService, PlayerPerformanceConfigService playerPerformanceConfigService, Lazy<PairFactory> pairFactory)
{
@@ -73,7 +72,6 @@ public class DalamudUtilService : IHostedService, IMediatorSubscriber
_condition = condition;
_gameData = gameData;
_gameConfig = gameConfig;
_playerState = playerState;
_actorObjectService = actorObjectService;
_targetManager = targetManager;
_blockedCharacterHandler = blockedCharacterHandler;
@@ -82,26 +80,53 @@ public class DalamudUtilService : IHostedService, IMediatorSubscriber
_configService = configService;
_playerPerformanceConfigService = playerPerformanceConfigService;
_pairFactory = pairFactory;
var clientLanguage = _clientState.ClientLanguage;
WorldData = new(() =>
{
return gameData.GetExcelSheet<Lumina.Excel.Sheets.World>(clientLanguage)!
return gameData.GetExcelSheet<Lumina.Excel.Sheets.World>(Dalamud.Game.ClientLanguage.English)!
.Where(w => !w.Name.IsEmpty && w.DataCenter.RowId != 0 && (w.IsPublic || char.IsUpper(w.Name.ToString()[0])))
.ToDictionary(w => (ushort)w.RowId, w => w.Name.ToString());
});
JobData = new(() =>
{
return gameData.GetExcelSheet<ClassJob>(clientLanguage)!
.ToDictionary(k => k.RowId, k => k.Name.ToString());
return gameData.GetExcelSheet<ClassJob>(Dalamud.Game.ClientLanguage.English)!
.ToDictionary(k => k.RowId, k => k.NameEnglish.ToString());
});
TerritoryData = new(() => BuildTerritoryData(clientLanguage));
TerritoryDataEnglish = new(() => BuildTerritoryData(Dalamud.Game.ClientLanguage.English));
MapData = new(() => BuildMapData(clientLanguage));
ContentFinderData = new Lazy<Dictionary<uint, string>>(() =>
TerritoryData = new(() =>
{
return _gameData.GetExcelSheet<TerritoryType>()!
.Where(w => w.RowId != 0 && !string.IsNullOrEmpty(w.ContentFinderCondition.ValueNullable?.Name.ToString()))
.ToDictionary(w => w.RowId, w => w.ContentFinderCondition.Value.Name.ToString());
return gameData.GetExcelSheet<TerritoryType>(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);
}
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) =>
{
@@ -133,71 +158,6 @@ public class DalamudUtilService : IHostedService, IMediatorSubscriber
private Lazy<ulong> RebuildCID() => new(GetCID);
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)
{
resolvedPair = _pairFactory.Value.Create(pair.UniqueIdent) ?? pair;
@@ -273,7 +233,6 @@ public class DalamudUtilService : IHostedService, IMediatorSubscriber
public bool IsAnythingDrawing { get; private set; } = false;
public bool IsInCutscene { get; private set; } = false;
public bool IsInGpose { get; private set; } = false;
public bool IsGameUiHidden => _gameGui.GameUiHidden;
public bool IsLoggedIn { get; private set; }
public bool IsOnFrameworkThread => _framework.IsInFrameworkUpdateThread;
public bool IsZoning => _condition[ConditionFlag.BetweenAreas] || _condition[ConditionFlag.BetweenAreas51];
@@ -286,9 +245,7 @@ public class DalamudUtilService : IHostedService, IMediatorSubscriber
public Lazy<Dictionary<uint, string>> JobData { 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>> TerritoryDataEnglish { get; private set; }
public Lazy<Dictionary<uint, (Map Map, string MapName)>> MapData { get; private set; }
public Lazy<Dictionary<uint, string>> ContentFinderData { get; private set; }
public bool IsLodEnabled { get; private set; }
public LightlessMediator Mediator { get; }
@@ -307,7 +264,7 @@ public class DalamudUtilService : IHostedService, IMediatorSubscriber
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;
}
@@ -370,8 +327,8 @@ public class DalamudUtilService : IHostedService, IMediatorSubscriber
public IEnumerable<ICharacter?> GetGposeCharactersFromObjectTable()
{
foreach (var actor in _objectTable
.Where(a => a.ObjectIndex > 200 && a.ObjectKind == DalamudObjectKind.Player))
foreach (var actor in _actorObjectService.PlayerDescriptors
.Where(a => a.ObjectKind == DalamudObjectKind.Player && a.ObjectIndex > 200))
{
var character = _objectTable.CreateObjectReference(actor.Address) as ICharacter;
if (character != null)
@@ -382,7 +339,7 @@ public class DalamudUtilService : IHostedService, IMediatorSubscriber
public bool GetIsPlayerPresent()
{
EnsureIsOnFramework();
return _objectTable.LocalPlayer != null && _objectTable.LocalPlayer.IsValid() && _playerState.IsLoaded;
return _objectTable.LocalPlayer != null && _objectTable.LocalPlayer.IsValid();
}
public async Task<bool> GetIsPlayerPresentAsync()
@@ -398,8 +355,7 @@ public class DalamudUtilService : IHostedService, IMediatorSubscriber
var playerAddress = playerPointer.Value;
var ownerEntityId = ((Character*)playerAddress)->EntityId;
var candidateAddress = _objectTable.GetObjectAddress(((GameObject*)playerAddress)->ObjectIndex + 1);
if (ownerEntityId == 0) return candidateAddress;
if (ownerEntityId == 0) return IntPtr.Zero;
if (playerAddress == _actorObjectService.LocalPlayerAddress)
{
@@ -410,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 =>
kind == DalamudObjectKind.MountType || kind == DalamudObjectKind.Companion);
if (ownedObject != nint.Zero)
@@ -428,7 +373,7 @@ public class DalamudUtilService : IHostedService, IMediatorSubscriber
return ownedObject;
}
return candidateAddress;
return _objectTable.GetObjectAddress(((GameObject*)playerAddress)->ObjectIndex + 1);
}
public async Task<IntPtr> GetMinionOrMountAsync(IntPtr? playerPointer = null)
@@ -443,22 +388,7 @@ public class DalamudUtilService : IHostedService, IMediatorSubscriber
var mgr = CharacterManager.Instance();
playerPointer ??= GetPlayerPtr();
if (playerPointer == IntPtr.Zero || (IntPtr)mgr == IntPtr.Zero) return IntPtr.Zero;
var ownerAddress = playerPointer.Value;
var ownerEntityId = ((Character*)ownerAddress)->EntityId;
if (ownerEntityId == 0) return IntPtr.Zero;
var candidate = (IntPtr)mgr->LookupPetByOwnerObject((BattleChara*)ownerAddress);
if (candidate != IntPtr.Zero)
{
var candidateObj = (GameObject*)candidate;
if (IsPetMatch(candidateObj, ownerEntityId))
{
return candidate;
}
}
return FindOwnedPet(ownerEntityId, ownerAddress);
return (IntPtr)mgr->LookupPetByOwnerObject((BattleChara*)playerPointer);
}
public async Task<IntPtr> GetPetAsync(IntPtr? playerPointer = null)
@@ -495,60 +425,6 @@ public class DalamudUtilService : IHostedService, IMediatorSubscriber
return nint.Zero;
}
private unsafe nint FindOwnedPet(uint ownerEntityId, nint ownerAddress)
{
if (ownerEntityId == 0)
{
return nint.Zero;
}
foreach (var obj in _objectTable)
{
if (obj is null || obj.Address == nint.Zero || obj.Address == ownerAddress)
{
continue;
}
if (obj.ObjectKind != DalamudObjectKind.BattleNpc)
{
continue;
}
var candidate = (GameObject*)obj.Address;
if (candidate->BattleNpcSubKind != BattleNpcSubKind.Pet)
{
continue;
}
if (ResolveOwnerId(candidate) == ownerEntityId)
{
return obj.Address;
}
}
return nint.Zero;
}
private static unsafe bool IsPetMatch(GameObject* candidate, uint ownerEntityId)
{
if (candidate == null)
{
return false;
}
if ((DalamudObjectKind)candidate->ObjectKind != DalamudObjectKind.BattleNpc)
{
return false;
}
if (candidate->BattleNpcSubKind != BattleNpcSubKind.Pet)
{
return false;
}
return ResolveOwnerId(candidate) == ownerEntityId;
}
private static unsafe uint ResolveOwnerId(GameObject* gameObject)
{
if (gameObject == null)
@@ -596,17 +472,30 @@ public class DalamudUtilService : IHostedService, IMediatorSubscriber
public string GetPlayerName()
{
return _playerState.CharacterName;
EnsureIsOnFramework();
return _objectTable.LocalPlayer?.Name.ToString() ?? "--";
}
public async Task<string> GetPlayerNameAsync()
{
return await RunOnFrameworkThread(GetPlayerName).ConfigureAwait(false);
}
public async Task<ulong> GetCIDAsync()
{
return await RunOnFrameworkThread(GetCID).ConfigureAwait(false);
}
public unsafe ulong GetCID()
{
return _playerState.ContentId;
EnsureIsOnFramework();
var playerChar = GetPlayerCharacter();
return ((BattleChara*)playerChar.Address)->Character.ContentId;
}
public string GetPlayerNameHashed()
public async Task<string> GetPlayerNameHashedAsync()
{
return _cid.Value.ToString().GetHash256();
return await RunOnFrameworkThread(() => _cid.Value.ToString().GetHash256()).ConfigureAwait(false);
}
public static unsafe bool TryGetHashedCID(IPlayerCharacter? playerCharacter, out string hashedCid)
@@ -645,100 +534,54 @@ public class DalamudUtilService : IHostedService, IMediatorSubscriber
public uint GetHomeWorldId()
{
return _playerState.HomeWorld.RowId;
EnsureIsOnFramework();
return _objectTable.LocalPlayer?.HomeWorld.RowId ?? 0;
}
public uint GetWorldId()
{
return _playerState.CurrentWorld.RowId;
EnsureIsOnFramework();
return _objectTable.LocalPlayer!.CurrentWorld.RowId;
}
public unsafe LocationInfo GetMapData()
{
EnsureIsOnFramework();
var agentMap = AgentMap.Instance();
var houseMan = HousingManager.Instance();
var location = new LocationInfo();
location.ServerId = _playerState.CurrentWorld.RowId;
//location.InstanceId = UIState.Instance()->PublicInstance.InstanceId; //TODO:Need API update first
location.TerritoryId = _clientState.TerritoryType;
location.MapId = _clientState.MapId;
if (houseMan != null)
uint serverId = 0;
if (_objectTable.LocalPlayer == null) serverId = 0;
else serverId = _objectTable.LocalPlayer.CurrentWorld.RowId;
uint mapId = agentMap == null ? 0 : agentMap->CurrentMapId;
uint territoryId = agentMap == null ? 0 : agentMap->CurrentTerritoryId;
uint divisionId = houseMan == null ? 0 : (uint)(houseMan->GetCurrentDivision());
uint wardId = houseMan == null ? 0 : (uint)(houseMan->GetCurrentWard() + 1);
uint houseId = 0;
var tempHouseId = houseMan == null ? 0 : (houseMan->GetCurrentPlot());
if (!houseMan->IsInside()) tempHouseId = 0;
if (tempHouseId < -1)
{
if (houseMan->IsInside())
{
location.TerritoryId = HousingManager.GetOriginalHouseTerritoryTypeId();
var house = houseMan->GetCurrentIndoorHouseId();
location.WardId = house.WardIndex + 1u;
location.HouseId = house.IsApartment ? 100 : house.PlotIndex + 1u;
location.RoomId = (uint)house.RoomNumber;
location.DivisionId = house.IsApartment ? house.ApartmentDivision + 1u : houseMan->GetCurrentDivision();
}
else if (houseMan->IsInWorkshop())
{
var workShop = houseMan->WorkshopTerritory;
var house = workShop->HouseId;
location.WardId = house.WardIndex + 1u;
location.HouseId = house.PlotIndex + 1u;
}
else if (houseMan->IsOutside())
{
var outside = houseMan->OutdoorTerritory;
var house = outside->HouseId;
location.WardId = house.WardIndex + 1u;
location.HouseId = (uint)houseMan->GetCurrentPlot() + 1;
location.DivisionId = houseMan->GetCurrentDivision();
}
//_logger.LogWarning(LocationToString(location));
divisionId = tempHouseId == -127 ? 2 : (uint)1;
tempHouseId = 100;
}
return location;
}
public string LocationToString(LocationInfo location)
{
if (location.ServerId is 0 || location.TerritoryId is 0) return String.Empty;
var str = WorldData.Value[(ushort)location.ServerId];
if (ContentFinderData.Value.TryGetValue(location.TerritoryId , out var dutyName))
if (tempHouseId == -1) tempHouseId = 0;
houseId = (uint)tempHouseId;
if (houseId != 0)
{
str += $" - [In Duty]{dutyName}";
territoryId = HousingManager.GetOriginalHouseTerritoryTypeId();
}
else
uint roomId = houseMan == null ? 0 : (uint)(houseMan->GetCurrentRoom());
return new LocationInfo()
{
if (location.HouseId is not 0 || location.MapId is 0) // Dont show mapName when in house/no map available
{
str += $" - {TerritoryData.Value[(ushort)location.TerritoryId]}";
}
else
{
str += $" - {MapData.Value[(ushort)location.MapId].MapName}";
}
// if (location.InstanceId is not 0)
// {
// str += ((SeIconChar)(57520 + location.InstanceId)).ToIconString();
// }
if (location.WardId is not 0)
{
str += $" Ward #{location.WardId}";
}
if (location.HouseId is not 0 and not 100)
{
str += $" House #{location.HouseId}";
}
else if (location.HouseId is 100)
{
str += $" {(location.DivisionId == 2 ? "[Subdivision]" : "")} Apartment";
}
if (location.RoomId is not 0)
{
str += $" Room #{location.RoomId}";
}
}
return str;
ServerId = serverId,
MapId = mapId,
TerritoryId = territoryId,
DivisionId = divisionId,
WardId = wardId,
HouseId = houseId,
RoomId = roomId
};
}
public unsafe void SetMarkerAndOpenMap(Vector3 position, Map map)
@@ -750,6 +593,21 @@ public class DalamudUtilService : IHostedService, IMediatorSubscriber
agentMap->SetFlagMapMarker(map.TerritoryType.RowId, map.RowId, position);
}
public async Task<LocationInfo> GetMapDataAsync()
{
return await RunOnFrameworkThread(GetMapData).ConfigureAwait(false);
}
public async Task<uint> GetWorldIdAsync()
{
return await RunOnFrameworkThread(GetWorldId).ConfigureAwait(false);
}
public async Task<uint> GetHomeWorldIdAsync()
{
return await RunOnFrameworkThread(GetHomeWorldId).ConfigureAwait(false);
}
public unsafe bool IsGameObjectPresent(IntPtr key)
{
return _objectTable.Any(f => f.Address == key);
@@ -922,7 +780,7 @@ public class DalamudUtilService : IHostedService, IMediatorSubscriber
bool isDrawingChanged = false;
if ((nint)drawObj != IntPtr.Zero)
{
isDrawing = gameObj->RenderFlags == (VisibilityFlags)0b100000000000;
isDrawing = (gameObj->RenderFlags & VisibilityFlags.Nameplate) != VisibilityFlags.None;
if (!isDrawing)
{
isDrawing = ((CharacterBase*)drawObj)->HasModelInSlotLoaded != 0;
@@ -988,12 +846,9 @@ public class DalamudUtilService : IHostedService, IMediatorSubscriber
_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++)
{
var actor = playerDescriptors[i];

View File

@@ -1,863 +0,0 @@
using Dalamud.Game.ClientState.Objects.Enums;
using Dalamud.Plugin.Services;
using FFXIVClientStructs.FFXIV.Client.System.Framework;
using FFXIVClientStructs.FFXIV.Client.Game.Character;
using FFXIVClientStructs.FFXIV.Client.UI;
using FFXIVClientStructs.FFXIV.Component.GUI;
using LightlessSync.LightlessConfiguration;
using LightlessSync.Services.Mediator;
using LightlessSync.UI;
using LightlessSync.UI.Services;
using LightlessSync.Utils;
using LightlessSync.UtilsEnum.Enum;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
using System.Collections.Immutable;
using Task = System.Threading.Tasks.Task;
namespace LightlessSync.Services.LightFinder;
/// <summary>
/// Native nameplate handler that injects LightFinder labels via the signature hook path.
/// </summary>
public unsafe class LightFinderNativePlateHandler : DisposableMediatorSubscriberBase, IHostedService
{
private const uint NameplateNodeIdBase = 0x7D99D500;
private const string DefaultLabelText = "LightFinder";
private readonly ILogger<LightFinderNativePlateHandler> _logger;
private readonly IClientState _clientState;
private readonly IObjectTable _objectTable;
private readonly LightlessConfigService _configService;
private readonly PairUiService _pairUiService;
private readonly NameplateUpdateHookService _nameplateUpdateHookService;
private readonly int[] _cachedNameplateTextWidths = new int[AddonNamePlate.NumNamePlateObjects];
private readonly int[] _cachedNameplateTextHeights = new int[AddonNamePlate.NumNamePlateObjects];
private readonly int[] _cachedNameplateContainerHeights = new int[AddonNamePlate.NumNamePlateObjects];
private readonly int[] _cachedNameplateTextOffsets = new int[AddonNamePlate.NumNamePlateObjects];
private readonly string?[] _lastLabelByIndex = new string?[AddonNamePlate.NumNamePlateObjects];
private ImmutableHashSet<string> _activeBroadcastingCids = [];
private LightfinderLabelRenderer _lastRenderer;
private uint _lastSignatureUpdateFrame;
private bool _isUpdating;
private string _lastLabelContent = DefaultLabelText;
public LightFinderNativePlateHandler(
ILogger<LightFinderNativePlateHandler> logger,
IClientState clientState,
LightlessConfigService configService,
LightlessMediator mediator,
IObjectTable objectTable,
PairUiService pairUiService,
NameplateUpdateHookService nameplateUpdateHookService) : base(logger, mediator)
{
_logger = logger;
_clientState = clientState;
_configService = configService;
_objectTable = objectTable;
_pairUiService = pairUiService;
_nameplateUpdateHookService = nameplateUpdateHookService;
_lastRenderer = _configService.Current.LightfinderLabelRenderer;
Array.Fill(_cachedNameplateTextOffsets, int.MinValue);
}
private bool IsSignatureMode => _configService.Current.LightfinderLabelRenderer == LightfinderLabelRenderer.SignatureHook;
/// <summary>
/// Starts listening for nameplate updates from the hook service.
/// </summary>
public Task StartAsync(CancellationToken cancellationToken)
{
_nameplateUpdateHookService.NameplateUpdated += OnNameplateUpdated;
return Task.CompletedTask;
}
/// <summary>
/// Stops listening for nameplate updates and tears down any constructed nodes.
/// </summary>
public Task StopAsync(CancellationToken cancellationToken)
{
_nameplateUpdateHookService.NameplateUpdated -= OnNameplateUpdated;
UnsubscribeAll();
TryDestroyNameplateNodes();
return Task.CompletedTask;
}
/// <summary>
/// Triggered by the sig hook to refresh native nameplate labels.
/// </summary>
private void HandleNameplateUpdate(RaptureAtkModule* raptureAtkModule)
{
if (_isUpdating)
return;
_isUpdating = true;
try
{
RefreshRendererState();
if (!IsSignatureMode)
return;
if (raptureAtkModule == null)
return;
var namePlateAddon = GetNamePlateAddon(raptureAtkModule);
if (namePlateAddon == null)
return;
if (_clientState.IsGPosing)
{
HideAllNameplateNodes(namePlateAddon);
return;
}
var fw = Framework.Instance();
if (fw == null)
return;
var frame = fw->FrameCounter;
if (_lastSignatureUpdateFrame == frame)
return;
_lastSignatureUpdateFrame = frame;
UpdateNameplateNodes(namePlateAddon);
}
finally
{
_isUpdating = false;
}
}
/// <summary>
/// Hook callback from the nameplate update signature.
/// </summary>
private void OnNameplateUpdated(RaptureAtkModule* raptureAtkModule, RaptureAtkModule.NamePlateInfo* namePlateInfo,
NumberArrayData* numArray, StringArrayData* stringArray, BattleChara* battleChara, int numArrayIndex, int stringArrayIndex)
{
HandleNameplateUpdate(raptureAtkModule);
}
/// <summary>
/// Updates the active broadcasting CID set and requests a nameplate redraw.
/// </summary>
public void UpdateBroadcastingCids(IEnumerable<string> cids)
{
var newSet = cids.ToImmutableHashSet(StringComparer.Ordinal);
if (ReferenceEquals(_activeBroadcastingCids, newSet) || _activeBroadcastingCids.SetEquals(newSet))
return;
_activeBroadcastingCids = newSet;
if (_logger.IsEnabled(LogLevel.Trace))
_logger.LogTrace("Active broadcast IDs (native): {Cids}", string.Join(',', _activeBroadcastingCids));
RequestNameplateRedraw();
}
/// <summary>
/// Sync renderer state with config and clear/remove native nodes if needed.
/// </summary>
private void RefreshRendererState()
{
var renderer = _configService.Current.LightfinderLabelRenderer;
if (renderer == _lastRenderer)
return;
_lastRenderer = renderer;
if (renderer == LightfinderLabelRenderer.SignatureHook)
{
ClearNameplateCaches();
RequestNameplateRedraw();
}
else
{
TryDestroyNameplateNodes();
ClearNameplateCaches();
}
}
/// <summary>
/// Requests a full nameplate update through the native addon.
/// </summary>
private void RequestNameplateRedraw()
{
if (!IsSignatureMode)
return;
var raptureAtkModule = GetRaptureAtkModule();
if (raptureAtkModule == null)
return;
var namePlateAddon = GetNamePlateAddon(raptureAtkModule);
if (namePlateAddon == null)
return;
namePlateAddon->DoFullUpdate = 1;
}
private HashSet<ulong> VisibleUserIds
=> [.. _pairUiService.GetSnapshot().PairsByUid.Values
.Where(u => u.IsVisible && u.PlayerCharacterId != uint.MaxValue)
.Select(u => (ulong)u.PlayerCharacterId)];
/// <summary>
/// Creates/updates LightFinder label nodes for active broadcasts.
/// </summary>
private void UpdateNameplateNodes(AddonNamePlate* namePlateAddon)
{
if (namePlateAddon == null)
{
if (_logger.IsEnabled(LogLevel.Debug))
_logger.LogDebug("NamePlate addon unavailable during update, skipping label refresh.");
return;
}
if (!IsNameplateAddonVisible(namePlateAddon))
return;
if (!IsSignatureMode)
{
HideAllNameplateNodes(namePlateAddon);
return;
}
if (_activeBroadcastingCids.Count == 0)
{
HideAllNameplateNodes(namePlateAddon);
return;
}
var framework = Framework.Instance();
if (framework == null)
{
if (_logger.IsEnabled(LogLevel.Debug))
_logger.LogDebug("Framework instance unavailable during nameplate update, skipping.");
return;
}
var uiModule = framework->GetUIModule();
if (uiModule == null)
{
if (_logger.IsEnabled(LogLevel.Debug))
_logger.LogDebug("UI module unavailable during nameplate update, skipping.");
return;
}
var ui3DModule = uiModule->GetUI3DModule();
if (ui3DModule == null)
{
if (_logger.IsEnabled(LogLevel.Debug))
_logger.LogDebug("UI3D module unavailable during nameplate update, skipping.");
return;
}
var vec = ui3DModule->NamePlateObjectInfoPointers;
if (vec.IsEmpty)
return;
var config = _configService.Current;
var visibleUserIdsSnapshot = VisibleUserIds;
var labelColor = UIColors.Get("Lightfinder");
var edgeColor = UIColors.Get("LightfinderEdge");
var scaleMultiplier = Math.Clamp(config.LightfinderLabelScale, 0.5f, 2.0f);
var baseScale = config.LightfinderLabelUseIcon ? 1.0f : 0.5f;
var effectiveScale = baseScale * scaleMultiplier;
var labelContent = config.LightfinderLabelUseIcon
? LightFinderPlateHandler.NormalizeIconGlyph(config.LightfinderLabelIconGlyph)
: DefaultLabelText;
if (!config.LightfinderLabelUseIcon && (string.IsNullOrWhiteSpace(labelContent) || string.Equals(labelContent, "-", StringComparison.Ordinal)))
labelContent = DefaultLabelText;
if (!string.Equals(_lastLabelContent, labelContent, StringComparison.Ordinal))
{
_lastLabelContent = labelContent;
Array.Fill(_lastLabelByIndex, null);
}
var desiredFontType = config.LightfinderLabelUseIcon ? FontType.Axis : FontType.MiedingerMed;
var baseFontSize = config.LightfinderLabelUseIcon ? 36f : 24f;
var desiredFontSize = (byte)Math.Clamp((int)Math.Round(baseFontSize * scaleMultiplier), 1, 255);
var desiredFlags = config.LightfinderLabelUseIcon
? TextFlags.Edge | TextFlags.Glare | TextFlags.AutoAdjustNodeSize
: TextFlags.Edge | TextFlags.Glare;
var desiredLineSpacing = (byte)Math.Clamp((int)Math.Round(24 * scaleMultiplier), 0, byte.MaxValue);
var defaultNodeWidth = (int)Math.Round(AtkNodeHelpers.DefaultTextNodeWidth * effectiveScale);
var defaultNodeHeight = (int)Math.Round(AtkNodeHelpers.DefaultTextNodeHeight * effectiveScale);
var safeCount = Math.Min(ui3DModule->NamePlateObjectInfoCount, vec.Length);
var visibleIndices = new bool[AddonNamePlate.NumNamePlateObjects];
for (int i = 0; i < safeCount; ++i)
{
var objectInfoPtr = vec[i];
if (objectInfoPtr == null)
continue;
var objectInfo = objectInfoPtr.Value;
if (objectInfo == null || objectInfo->GameObject == null)
continue;
var nameplateIndex = objectInfo->NamePlateIndex;
if (nameplateIndex < 0 || nameplateIndex >= AddonNamePlate.NumNamePlateObjects)
continue;
var gameObject = objectInfo->GameObject;
if ((ObjectKind)gameObject->ObjectKind != ObjectKind.Player)
continue;
var cid = DalamudUtilService.GetHashedCIDFromPlayerPointer((nint)gameObject);
if (cid == null || !_activeBroadcastingCids.Contains(cid))
continue;
var local = _objectTable.LocalPlayer;
if (!config.LightfinderLabelShowOwn && local != null &&
objectInfo->GameObject->GetGameObjectId() == local.GameObjectId)
continue;
var hidePaired = !config.LightfinderLabelShowPaired;
var goId = (ulong)gameObject->GetGameObjectId();
if (hidePaired && visibleUserIdsSnapshot.Contains(goId))
continue;
var nameplateObject = namePlateAddon->NamePlateObjectArray[nameplateIndex];
var root = nameplateObject.RootComponentNode;
var nameContainer = nameplateObject.NameContainer;
var nameText = nameplateObject.NameText;
var marker = nameplateObject.MarkerIcon;
if (root == null || root->Component == null || nameContainer == null || nameText == null)
{
if (_logger.IsEnabled(LogLevel.Debug))
_logger.LogDebug("Nameplate {Index} missing required nodes during update, skipping.", nameplateIndex);
continue;
}
var nodeId = GetNameplateNodeId(nameplateIndex);
var pNode = EnsureNameplateTextNode(nameContainer, root, nodeId, out var nodeCreated);
if (pNode == null)
continue;
bool isVisible =
((marker != null) && marker->AtkResNode.IsVisible()) ||
(nameContainer->IsVisible() && nameText->AtkResNode.IsVisible()) ||
config.LightfinderLabelShowHidden;
if (!isVisible)
continue;
if (!pNode->AtkResNode.IsVisible())
pNode->AtkResNode.ToggleVisibility(enable: true);
visibleIndices[nameplateIndex] = true;
if (nodeCreated)
pNode->AtkResNode.SetUseDepthBasedPriority(enable: true);
var scaleMatches = NearlyEqual(pNode->AtkResNode.ScaleX, effectiveScale) &&
NearlyEqual(pNode->AtkResNode.ScaleY, effectiveScale);
if (!scaleMatches)
pNode->AtkResNode.SetScale(effectiveScale, effectiveScale);
var fontTypeChanged = pNode->FontType != desiredFontType;
if (fontTypeChanged)
pNode->FontType = desiredFontType;
var fontSizeChanged = pNode->FontSize != desiredFontSize;
if (fontSizeChanged)
pNode->FontSize = desiredFontSize;
var needsTextUpdate = nodeCreated ||
!string.Equals(_lastLabelByIndex[nameplateIndex], labelContent, StringComparison.Ordinal);
if (needsTextUpdate)
{
pNode->SetText(labelContent);
_lastLabelByIndex[nameplateIndex] = labelContent;
}
var flagsChanged = pNode->TextFlags != desiredFlags;
var nodeWidth = (int)pNode->AtkResNode.GetWidth();
if (nodeWidth <= 0)
nodeWidth = defaultNodeWidth;
var nodeHeight = defaultNodeHeight;
AlignmentType alignment;
var textScaleY = nameText->AtkResNode.ScaleY;
if (textScaleY <= 0f)
textScaleY = 1f;
var blockHeight = Math.Abs((int)nameplateObject.TextH);
if (blockHeight > 0)
{
_cachedNameplateTextHeights[nameplateIndex] = blockHeight;
}
else
{
blockHeight = _cachedNameplateTextHeights[nameplateIndex];
}
if (blockHeight <= 0)
{
blockHeight = GetScaledTextHeight(nameText);
if (blockHeight <= 0)
blockHeight = nodeHeight;
_cachedNameplateTextHeights[nameplateIndex] = blockHeight;
}
var containerHeight = (int)nameContainer->Height;
if (containerHeight > 0)
{
_cachedNameplateContainerHeights[nameplateIndex] = containerHeight;
}
else
{
containerHeight = _cachedNameplateContainerHeights[nameplateIndex];
}
if (containerHeight <= 0)
{
containerHeight = blockHeight + (int)Math.Round(8 * textScaleY);
if (containerHeight <= blockHeight)
containerHeight = blockHeight + 1;
_cachedNameplateContainerHeights[nameplateIndex] = containerHeight;
}
var blockTop = containerHeight - blockHeight;
if (blockTop < 0)
blockTop = 0;
var verticalPadding = (int)Math.Round(4 * effectiveScale);
var positionY = blockTop - verticalPadding - nodeHeight;
var textWidth = Math.Abs((int)nameplateObject.TextW);
if (textWidth <= 0)
{
textWidth = GetScaledTextWidth(nameText);
if (textWidth <= 0)
textWidth = nodeWidth;
}
if (textWidth > 0)
{
_cachedNameplateTextWidths[nameplateIndex] = textWidth;
}
var textOffset = (int)Math.Round(nameText->AtkResNode.X);
var hasValidOffset = false;
if (Math.Abs((int)nameplateObject.TextW) > 0 || textOffset != 0)
{
_cachedNameplateTextOffsets[nameplateIndex] = textOffset;
hasValidOffset = true;
}
else if (_cachedNameplateTextOffsets[nameplateIndex] != int.MinValue)
{
hasValidOffset = true;
}
int positionX;
if (!config.LightfinderLabelUseIcon)
{
var needsWidthRefresh = nodeCreated || needsTextUpdate || !scaleMatches || fontTypeChanged || fontSizeChanged || flagsChanged;
if (flagsChanged)
pNode->TextFlags = desiredFlags;
if (needsWidthRefresh)
{
if (pNode->AtkResNode.Width != 0)
pNode->AtkResNode.Width = 0;
nodeWidth = (int)pNode->AtkResNode.GetWidth();
if (nodeWidth <= 0)
nodeWidth = defaultNodeWidth;
}
if (pNode->AtkResNode.Width != (ushort)nodeWidth)
pNode->AtkResNode.Width = (ushort)nodeWidth;
}
else
{
var needsWidthRefresh = nodeCreated || needsTextUpdate || !scaleMatches || fontTypeChanged || fontSizeChanged || flagsChanged;
if (flagsChanged)
pNode->TextFlags = desiredFlags;
if (needsWidthRefresh && pNode->AtkResNode.Width != 0)
pNode->AtkResNode.Width = 0;
nodeWidth = pNode->AtkResNode.GetWidth();
}
if (config.LightfinderAutoAlign && nameContainer != null && hasValidOffset)
{
var nameplateWidth = (int)nameContainer->Width;
int leftPos = nameplateWidth / 8;
int rightPos = nameplateWidth - nodeWidth - (nameplateWidth / 8);
int centrePos = (nameplateWidth - nodeWidth) / 2;
int staticMargin = 24;
int calcMargin = (int)(nameplateWidth * 0.08f);
switch (config.LabelAlignment)
{
case LabelAlignment.Left:
positionX = config.LightfinderLabelUseIcon ? leftPos + staticMargin : leftPos;
alignment = AlignmentType.BottomLeft;
break;
case LabelAlignment.Right:
positionX = config.LightfinderLabelUseIcon ? rightPos - staticMargin : nameplateWidth - nodeWidth + calcMargin;
alignment = AlignmentType.BottomRight;
break;
default:
positionX = config.LightfinderLabelUseIcon ? centrePos : centrePos + calcMargin;
alignment = AlignmentType.Bottom;
break;
}
}
else
{
positionX = 58 + config.LightfinderLabelOffsetX;
alignment = AlignmentType.Bottom;
}
positionY += config.LightfinderLabelOffsetY;
alignment = (AlignmentType)Math.Clamp((int)alignment, 0, 8);
if (pNode->AtkResNode.Color.A != 255)
pNode->AtkResNode.Color.A = 255;
var textR = (byte)(labelColor.X * 255);
var textG = (byte)(labelColor.Y * 255);
var textB = (byte)(labelColor.Z * 255);
var textA = (byte)(labelColor.W * 255);
if (pNode->TextColor.R != textR || pNode->TextColor.G != textG ||
pNode->TextColor.B != textB || pNode->TextColor.A != textA)
{
pNode->TextColor.R = textR;
pNode->TextColor.G = textG;
pNode->TextColor.B = textB;
pNode->TextColor.A = textA;
}
var edgeR = (byte)(edgeColor.X * 255);
var edgeG = (byte)(edgeColor.Y * 255);
var edgeB = (byte)(edgeColor.Z * 255);
var edgeA = (byte)(edgeColor.W * 255);
if (pNode->EdgeColor.R != edgeR || pNode->EdgeColor.G != edgeG ||
pNode->EdgeColor.B != edgeB || pNode->EdgeColor.A != edgeA)
{
pNode->EdgeColor.R = edgeR;
pNode->EdgeColor.G = edgeG;
pNode->EdgeColor.B = edgeB;
pNode->EdgeColor.A = edgeA;
}
var desiredAlignment = config.LightfinderLabelUseIcon ? alignment : AlignmentType.Bottom;
if (pNode->AlignmentType != desiredAlignment)
pNode->AlignmentType = desiredAlignment;
var desiredX = (short)Math.Clamp(positionX, short.MinValue, short.MaxValue);
var desiredY = (short)Math.Clamp(positionY, short.MinValue, short.MaxValue);
if (!NearlyEqual(pNode->AtkResNode.X, desiredX) || !NearlyEqual(pNode->AtkResNode.Y, desiredY))
pNode->AtkResNode.SetPositionShort(desiredX, desiredY);
if (pNode->LineSpacing != desiredLineSpacing)
pNode->LineSpacing = desiredLineSpacing;
if (pNode->CharSpacing != 1)
pNode->CharSpacing = 1;
}
HideUnmarkedNodes(namePlateAddon, visibleIndices);
}
/// <summary>
/// Resolve the current RaptureAtkModule for native UI access.
/// </summary>
private static RaptureAtkModule* GetRaptureAtkModule()
{
var framework = Framework.Instance();
if (framework == null)
return null;
var uiModule = framework->GetUIModule();
if (uiModule == null)
return null;
return uiModule->GetRaptureAtkModule();
}
/// <summary>
/// Resolve the NamePlate addon from the given RaptureAtkModule.
/// </summary>
private static AddonNamePlate* GetNamePlateAddon(RaptureAtkModule* raptureAtkModule)
{
if (raptureAtkModule == null)
return null;
var addon = raptureAtkModule->RaptureAtkUnitManager.GetAddonByName("NamePlate");
return addon != null ? (AddonNamePlate*)addon : null;
}
private static uint GetNameplateNodeId(int index)
=> NameplateNodeIdBase + (uint)index;
/// <summary>
/// Checks if the NamePlate addon is visible and safe to touch.
/// </summary>
private static bool IsNameplateAddonVisible(AddonNamePlate* namePlateAddon)
{
if (namePlateAddon == null)
return false;
var root = namePlateAddon->AtkUnitBase.RootNode;
return root != null && root->IsVisible();
}
/// <summary>
/// Finds a LightFinder text node by ID in the name container.
/// </summary>
private static AtkTextNode* FindNameplateTextNode(AtkResNode* nameContainer, uint nodeId)
{
if (nameContainer == null)
return null;
var child = nameContainer->ChildNode;
while (child != null)
{
if (child->NodeId == nodeId &&
child->Type == NodeType.Text &&
child->ParentNode == nameContainer)
return (AtkTextNode*)child;
child = child->PrevSiblingNode;
}
return null;
}
/// <summary>
/// Ensures a LightFinder text node exists for the given nameplate index.
/// </summary>
private static AtkTextNode* EnsureNameplateTextNode(AtkResNode* nameContainer, AtkComponentNode* root, uint nodeId, out bool created)
{
created = false;
if (nameContainer == null || root == null || root->Component == null)
return null;
var existing = FindNameplateTextNode(nameContainer, nodeId);
if (existing != null)
return existing;
if (nameContainer->ChildNode == null)
return null;
var newNode = AtkNodeHelpers.CreateOrphanTextNode(nodeId, TextFlags.Edge | TextFlags.Glare);
if (newNode == null)
return null;
var lastChild = nameContainer->ChildNode;
while (lastChild->PrevSiblingNode != null)
lastChild = lastChild->PrevSiblingNode;
newNode->AtkResNode.NextSiblingNode = lastChild;
newNode->AtkResNode.ParentNode = nameContainer;
lastChild->PrevSiblingNode = (AtkResNode*)newNode;
root->Component->UldManager.UpdateDrawNodeList();
newNode->AtkResNode.SetUseDepthBasedPriority(true);
created = true;
return newNode;
}
/// <summary>
/// Hides all native LightFinder nodes on the nameplate addon.
/// </summary>
private static void HideAllNameplateNodes(AddonNamePlate* namePlateAddon)
{
if (namePlateAddon == null)
return;
if (!IsNameplateAddonVisible(namePlateAddon))
return;
for (int i = 0; i < AddonNamePlate.NumNamePlateObjects; ++i)
{
HideNameplateTextNode(namePlateAddon->NamePlateObjectArray[i], GetNameplateNodeId(i));
}
}
/// <summary>
/// Hides all LightFinder nodes not marked as visible this frame.
/// </summary>
private static void HideUnmarkedNodes(AddonNamePlate* namePlateAddon, bool[] visibleIndices)
{
if (namePlateAddon == null)
return;
if (!IsNameplateAddonVisible(namePlateAddon))
return;
var visibleLength = visibleIndices.Length;
for (int i = 0; i < AddonNamePlate.NumNamePlateObjects; ++i)
{
if (i < visibleLength && visibleIndices[i])
continue;
HideNameplateTextNode(namePlateAddon->NamePlateObjectArray[i], GetNameplateNodeId(i));
}
}
/// <summary>
/// Hides the LightFinder text node for a single nameplate object.
/// </summary>
private static void HideNameplateTextNode(AddonNamePlate.NamePlateObject nameplateObject, uint nodeId)
{
var nameContainer = nameplateObject.NameContainer;
if (nameContainer == null)
return;
var node = FindNameplateTextNode(nameContainer, nodeId);
if (!IsValidNameplateTextNode(node, nameContainer))
return;
node->AtkResNode.ToggleVisibility(false);
}
/// <summary>
/// Attempts to destroy all constructed LightFinder nodes safely.
/// </summary>
private void TryDestroyNameplateNodes()
{
var raptureAtkModule = GetRaptureAtkModule();
if (raptureAtkModule == null)
{
if (_logger.IsEnabled(LogLevel.Debug))
_logger.LogDebug("Unable to destroy nameplate nodes because the RaptureAtkModule is not available.");
return;
}
var namePlateAddon = GetNamePlateAddon(raptureAtkModule);
if (namePlateAddon == null)
{
if (_logger.IsEnabled(LogLevel.Debug))
_logger.LogDebug("Unable to destroy nameplate nodes because the NamePlate addon is not available.");
return;
}
DestroyNameplateNodes(namePlateAddon);
}
/// <summary>
/// Removes all constructed LightFinder nodes from the given nameplate addon.
/// </summary>
private void DestroyNameplateNodes(AddonNamePlate* namePlateAddon)
{
if (namePlateAddon == null)
return;
for (int i = 0; i < AddonNamePlate.NumNamePlateObjects; ++i)
{
var nameplateObject = namePlateAddon->NamePlateObjectArray[i];
var root = nameplateObject.RootComponentNode;
var nameContainer = nameplateObject.NameContainer;
if (root == null || root->Component == null || nameContainer == null)
continue;
var nodeId = GetNameplateNodeId(i);
var textNode = FindNameplateTextNode(nameContainer, nodeId);
if (!IsValidNameplateTextNode(textNode, nameContainer))
continue;
try
{
var resNode = &textNode->AtkResNode;
if (resNode->PrevSiblingNode != null)
resNode->PrevSiblingNode->NextSiblingNode = resNode->NextSiblingNode;
if (resNode->NextSiblingNode != null)
resNode->NextSiblingNode->PrevSiblingNode = resNode->PrevSiblingNode;
root->Component->UldManager.UpdateDrawNodeList();
resNode->Destroy(true);
}
catch (Exception e)
{
_logger.LogError(e, "Unknown error while removing text node 0x{Node:X} for nameplate {Index} on component node 0x{Component:X}", (IntPtr)textNode, i, (IntPtr)root);
}
}
ClearNameplateCaches();
}
/// <summary>
/// Validates that a node is a LightFinder text node owned by the container.
/// </summary>
private static bool IsValidNameplateTextNode(AtkTextNode* node, AtkResNode* nameContainer)
{
if (node == null || nameContainer == null)
return false;
var resNode = &node->AtkResNode;
return resNode->Type == NodeType.Text && resNode->ParentNode == nameContainer;
}
/// <summary>
/// Float comparison helper for UI values.
/// </summary>
private static bool NearlyEqual(float a, float b, float epsilon = 0.001f)
=> Math.Abs(a - b) <= epsilon;
private static int GetScaledTextHeight(AtkTextNode* node)
{
if (node == null)
return 0;
var resNode = &node->AtkResNode;
var rawHeight = (int)resNode->GetHeight();
if (rawHeight <= 0 && node->LineSpacing > 0)
rawHeight = node->LineSpacing;
if (rawHeight <= 0)
rawHeight = AtkNodeHelpers.DefaultTextNodeHeight;
var scale = resNode->ScaleY;
if (scale <= 0f)
scale = 1f;
var computed = (int)Math.Round(rawHeight * scale);
return Math.Max(1, computed);
}
private static int GetScaledTextWidth(AtkTextNode* node)
{
if (node == null)
return 0;
var resNode = &node->AtkResNode;
var rawWidth = (int)resNode->GetWidth();
if (rawWidth <= 0)
rawWidth = AtkNodeHelpers.DefaultTextNodeWidth;
var scale = resNode->ScaleX;
if (scale <= 0f)
scale = 1f;
var computed = (int)Math.Round(rawWidth * scale);
return Math.Max(1, computed);
}
/// <summary>
/// Clears cached text sizing and label state for nameplates.
/// </summary>
public void ClearNameplateCaches()
{
Array.Clear(_cachedNameplateTextWidths, 0, _cachedNameplateTextWidths.Length);
Array.Clear(_cachedNameplateTextHeights, 0, _cachedNameplateTextHeights.Length);
Array.Clear(_cachedNameplateContainerHeights, 0, _cachedNameplateContainerHeights.Length);
Array.Fill(_cachedNameplateTextOffsets, int.MinValue);
Array.Fill(_lastLabelByIndex, null);
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -15,14 +15,11 @@ public class LightFinderScannerService : DisposableMediatorSubscriberBase
private readonly LightFinderService _broadcastService;
private readonly LightFinderPlateHandler _lightFinderPlateHandler;
private readonly LightFinderNativePlateHandler _lightFinderNativePlateHandler;
private readonly ConcurrentDictionary<string, BroadcastEntry> _broadcastCache = new(StringComparer.Ordinal);
private readonly Queue<string> _lookupQueue = new();
private readonly HashSet<string> _lookupQueuedCids = [];
private readonly HashSet<string> _syncshellCids = [];
private volatile bool _pendingLocalBroadcast;
private TimeSpan? _pendingLocalTtl;
private static readonly TimeSpan _maxAllowedTtl = TimeSpan.FromMinutes(4);
private static readonly TimeSpan _retryDelay = TimeSpan.FromMinutes(1);
@@ -45,14 +42,12 @@ public class LightFinderScannerService : DisposableMediatorSubscriberBase
LightFinderService broadcastService,
LightlessMediator mediator,
LightFinderPlateHandler lightFinderPlateHandler,
LightFinderNativePlateHandler lightFinderNativePlateHandler,
ActorObjectService actorTracker) : base(logger, mediator)
{
_logger = logger;
_actorTracker = actorTracker;
_broadcastService = broadcastService;
_lightFinderPlateHandler = lightFinderPlateHandler;
_lightFinderNativePlateHandler = lightFinderNativePlateHandler;
_logger = logger;
_framework = framework;
@@ -74,8 +69,6 @@ public class LightFinderScannerService : DisposableMediatorSubscriberBase
if (!_broadcastService.IsBroadcasting)
return;
TryPrimeLocalBroadcastCache();
var now = DateTime.UtcNow;
foreach (var address in _actorTracker.PlayerAddresses)
@@ -136,7 +129,6 @@ public class LightFinderScannerService : DisposableMediatorSubscriberBase
.ToList();
_lightFinderPlateHandler.UpdateBroadcastingCids(activeCids);
_lightFinderNativePlateHandler.UpdateBroadcastingCids(activeCids);
UpdateSyncshellBroadcasts();
}
@@ -148,58 +140,18 @@ public class LightFinderScannerService : DisposableMediatorSubscriberBase
_lookupQueue.Clear();
_lookupQueuedCids.Clear();
_syncshellCids.Clear();
_pendingLocalBroadcast = false;
_pendingLocalTtl = null;
_lightFinderPlateHandler.UpdateBroadcastingCids([]);
_lightFinderNativePlateHandler.UpdateBroadcastingCids([]);
return;
}
_pendingLocalBroadcast = true;
_pendingLocalTtl = msg.Ttl;
TryPrimeLocalBroadcastCache();
}
private void TryPrimeLocalBroadcastCache()
{
if (!_pendingLocalBroadcast)
return;
if (!TryGetLocalHashedCid(out var localCid))
return;
var ttl = _pendingLocalTtl ?? _maxAllowedTtl;
var expiry = DateTime.UtcNow + ttl;
_broadcastCache.AddOrUpdate(localCid,
new BroadcastEntry(true, expiry, null),
(_, old) => new BroadcastEntry(true, expiry, old.GID));
_pendingLocalBroadcast = false;
_pendingLocalTtl = null;
var now = DateTime.UtcNow;
var activeCids = _broadcastCache
.Where(e => e.Value.IsBroadcasting && e.Value.ExpiryTime > now)
.Select(e => e.Key)
.ToList();
_lightFinderPlateHandler.UpdateBroadcastingCids(activeCids);
_lightFinderNativePlateHandler.UpdateBroadcastingCids(activeCids);
}
private void UpdateSyncshellBroadcasts()
{
var now = DateTime.UtcNow;
var nearbyCids = GetNearbyHashedCids(out _);
var newSet = nearbyCids.Count == 0
? new HashSet<string>(StringComparer.Ordinal)
: _broadcastCache
.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);
var newSet = _broadcastCache
.Where(e => e.Value.IsBroadcasting && e.Value.ExpiryTime > now && !string.IsNullOrEmpty(e.Value.GID))
.Select(e => e.Key)
.ToHashSet(StringComparer.Ordinal);
if (!_syncshellCids.SetEquals(newSet))
{
@@ -211,17 +163,12 @@ public class LightFinderScannerService : DisposableMediatorSubscriberBase
}
}
public List<BroadcastStatusInfoDto> GetActiveSyncshellBroadcasts(bool excludeLocal = false)
public List<BroadcastStatusInfoDto> GetActiveSyncshellBroadcasts()
{
var now = DateTime.UtcNow;
var nearbyCids = GetNearbyHashedCids(out var localCid);
if (nearbyCids.Count == 0)
return [];
return [.. _broadcastCache
.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
{
HashedCID = e.Key,
@@ -231,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()
{
var token = _cleanupCts.Token;

View File

@@ -67,7 +67,7 @@ public class LightFinderService : IHostedService, IMediatorSubscriber
{
try
{
var cid = _dalamudUtil.GetCID();
var cid = await _dalamudUtil.GetCIDAsync().ConfigureAwait(false);
return cid.ToString().GetHash256();
}
catch (Exception ex)

View File

@@ -2,8 +2,10 @@ using Dalamud.Game.ClientState.Objects.Enums;
using Dalamud.Game.ClientState.Objects.SubKinds;
using Dalamud.Game.NativeWrapper;
using Dalamud.Game.Text.SeStringHandling;
using Dalamud.Hooking;
using Dalamud.Plugin.Services;
using Dalamud.Utility;
using Dalamud.Utility.Signatures;
using FFXIVClientStructs.FFXIV.Client.Game.Character;
using FFXIVClientStructs.FFXIV.Client.UI;
using FFXIVClientStructs.FFXIV.Component.GUI;
@@ -22,22 +24,27 @@ namespace LightlessSync.Services;
/// </summary>
public unsafe class NameplateService : DisposableMediatorSubscriberBase
{
private delegate nint UpdateNameplateDelegate(RaptureAtkModule* raptureAtkModule, RaptureAtkModule.NamePlateInfo* namePlateInfo, NumberArrayData* numArray, StringArrayData* stringArray, BattleChara* battleChara, int numArrayIndex, int stringArrayIndex);
// Glyceri, Thanks :bow:
[Signature("40 53 55 57 41 56 48 81 EC ?? ?? ?? ?? 48 8B 84 24", DetourName = nameof(UpdateNameplateDetour))]
private readonly Hook<UpdateNameplateDelegate>? _nameplateHook = null;
private readonly ILogger<NameplateService> _logger;
private readonly LightlessConfigService _configService;
private readonly IClientState _clientState;
private readonly IGameGui _gameGui;
private readonly IObjectTable _objectTable;
private readonly PairUiService _pairUiService;
private readonly NameplateUpdateHookService _nameplateUpdateHookService;
public NameplateService(ILogger<NameplateService> logger,
LightlessConfigService configService,
IClientState clientState,
IGameGui gameGui,
IObjectTable objectTable,
IGameInteropProvider interop,
LightlessMediator lightlessMediator,
PairUiService pairUiService,
NameplateUpdateHookService nameplateUpdateHookService) : base(logger, lightlessMediator)
PairUiService pairUiService) : base(logger, lightlessMediator)
{
_logger = logger;
_configService = configService;
@@ -45,18 +52,21 @@ public unsafe class NameplateService : DisposableMediatorSubscriberBase
_gameGui = gameGui;
_objectTable = objectTable;
_pairUiService = pairUiService;
_nameplateUpdateHookService = nameplateUpdateHookService;
_nameplateUpdateHookService.NameplateUpdated += OnNameplateUpdated;
interop.InitializeFromAttributes(this);
_nameplateHook?.Enable();
Refresh();
Mediator.Subscribe<VisibilityChange>(this, (_) => Refresh());
}
/// <summary>
/// Nameplate update handler, triggered by the signature hook service.
/// Detour for the game's internal nameplate update function.
/// This will be called whenever the client updates any nameplate.
///
/// We hook into it to apply our own nameplate coloring logic via <see cref="SetNameplate"/>,
/// </summary>
private void OnNameplateUpdated(RaptureAtkModule* raptureAtkModule, RaptureAtkModule.NamePlateInfo* namePlateInfo, NumberArrayData* numArray, StringArrayData* stringArray, BattleChara* battleChara, int numArrayIndex, int stringArrayIndex)
private nint UpdateNameplateDetour(RaptureAtkModule* raptureAtkModule, RaptureAtkModule.NamePlateInfo* namePlateInfo, NumberArrayData* numArray, StringArrayData* stringArray, BattleChara* battleChara, int numArrayIndex, int stringArrayIndex)
{
try
{
@@ -64,8 +74,10 @@ public unsafe class NameplateService : DisposableMediatorSubscriberBase
}
catch (Exception e)
{
_logger.LogError(e, "Error in NameplateService OnNameplateUpdated");
_logger.LogError(e, "Error in NameplateService UpdateNameplateDetour");
}
return _nameplateHook!.Original(raptureAtkModule, namePlateInfo, numArray, stringArray, battleChara, numArrayIndex, stringArrayIndex);
}
/// <summary>
@@ -234,7 +246,7 @@ public unsafe class NameplateService : DisposableMediatorSubscriberBase
{
if (disposing)
{
_nameplateUpdateHookService.NameplateUpdated -= OnNameplateUpdated;
_nameplateHook?.Dispose();
}
base.Dispose(disposing);

View File

@@ -1,57 +0,0 @@
using Dalamud.Hooking;
using Dalamud.Plugin.Services;
using Dalamud.Utility.Signatures;
using FFXIVClientStructs.FFXIV.Client.Game.Character;
using FFXIVClientStructs.FFXIV.Client.UI;
using FFXIVClientStructs.FFXIV.Component.GUI;
using Microsoft.Extensions.Logging;
namespace LightlessSync.Services;
public unsafe sealed class NameplateUpdateHookService : IDisposable
{
private delegate nint UpdateNameplateDelegate(RaptureAtkModule* raptureAtkModule, RaptureAtkModule.NamePlateInfo* namePlateInfo,
NumberArrayData* numArray, StringArrayData* stringArray, BattleChara* battleChara, int numArrayIndex, int stringArrayIndex);
public delegate void NameplateUpdatedHandler(RaptureAtkModule* raptureAtkModule, RaptureAtkModule.NamePlateInfo* namePlateInfo,
NumberArrayData* numArray, StringArrayData* stringArray, BattleChara* battleChara, int numArrayIndex, int stringArrayIndex);
// Glyceri, Thanks :bow:
[Signature("40 53 55 57 41 56 48 81 EC ?? ?? ?? ?? 48 8B 84 24", DetourName = nameof(UpdateNameplateDetour))]
private readonly Hook<UpdateNameplateDelegate>? _nameplateHook = null;
private readonly ILogger<NameplateUpdateHookService> _logger;
public NameplateUpdateHookService(ILogger<NameplateUpdateHookService> logger, IGameInteropProvider interop)
{
_logger = logger;
interop.InitializeFromAttributes(this);
_nameplateHook?.Enable();
}
public event NameplateUpdatedHandler? NameplateUpdated;
/// <summary>
/// Detour for the game's internal nameplate update function.
/// This will be called whenever the client updates any nameplate.
/// </summary>
private nint UpdateNameplateDetour(RaptureAtkModule* raptureAtkModule, RaptureAtkModule.NamePlateInfo* namePlateInfo,
NumberArrayData* numArray, StringArrayData* stringArray, BattleChara* battleChara, int numArrayIndex, int stringArrayIndex)
{
try
{
NameplateUpdated?.Invoke(raptureAtkModule, namePlateInfo, numArray, stringArray, battleChara, numArrayIndex, stringArrayIndex);
}
catch (Exception e)
{
_logger.LogError(e, "Error in NameplateUpdateHookService UpdateNameplateDetour");
}
return _nameplateHook!.Original(raptureAtkModule, namePlateInfo, numArray, stringArray, battleChara, numArrayIndex, stringArrayIndex);
}
public void Dispose()
{
_nameplateHook?.Dispose();
}
}

View File

@@ -1,71 +0,0 @@
using LightlessSync.Interop.Ipc;
using LightlessSync.LightlessConfiguration;
using LightlessSync.Services.Mediator;
using Microsoft.Extensions.Logging;
namespace LightlessSync.Services;
public sealed class PenumbraTempCollectionJanitor : DisposableMediatorSubscriberBase
{
private readonly IpcManager _ipc;
private readonly LightlessConfigService _config;
private int _ran;
public PenumbraTempCollectionJanitor(
ILogger<PenumbraTempCollectionJanitor> logger,
LightlessMediator mediator,
IpcManager ipc,
LightlessConfigService config) : base(logger, mediator)
{
_ipc = ipc;
_config = config;
Mediator.Subscribe<PenumbraInitializedMessage>(this, _ => CleanupOrphansOnBoot());
}
public void Register(Guid id)
{
if (id == Guid.Empty) return;
if (_config.Current.OrphanableTempCollections.Add(id))
_config.Save();
}
public void Unregister(Guid id)
{
if (id == Guid.Empty) return;
if (_config.Current.OrphanableTempCollections.Remove(id))
_config.Save();
}
private void CleanupOrphansOnBoot()
{
if (Interlocked.Exchange(ref _ran, 1) == 1)
return;
if (!_ipc.Penumbra.APIAvailable)
return;
var ids = _config.Current.OrphanableTempCollections.ToArray();
if (ids.Length == 0)
return;
var appId = Guid.NewGuid();
Logger.LogInformation("Cleaning up {count} orphaned Lightless temp collections found in configuration", ids.Length);
foreach (var id in ids)
{
try
{
_ipc.Penumbra.RemoveTemporaryCollectionAsync(Logger, appId, id)
.GetAwaiter().GetResult();
}
catch (Exception ex)
{
Logger.LogDebug(ex, "Failed removing orphaned temp collection {id}", id);
}
}
_config.Current.OrphanableTempCollections.Clear();
_config.Save();
}
}

View File

@@ -101,9 +101,9 @@ public class ServerConfigurationManager
}
hasMulti = false;
var charaName = _dalamudUtil.GetPlayerName();
var worldId = _dalamudUtil.GetHomeWorldId();
var cid = _dalamudUtil.GetCID();
var charaName = _dalamudUtil.GetPlayerNameAsync().GetAwaiter().GetResult();
var worldId = _dalamudUtil.GetHomeWorldIdAsync().GetAwaiter().GetResult();
var cid = _dalamudUtil.GetCIDAsync().GetAwaiter().GetResult();
var auth = currentServer.Authentications.FindAll(f => string.Equals(f.CharacterName, charaName) && f.WorldId == worldId);
if (auth.Count >= 2)
@@ -148,9 +148,9 @@ public class ServerConfigurationManager
}
hasMulti = false;
var charaName = _dalamudUtil.GetPlayerName();
var worldId = _dalamudUtil.GetHomeWorldId();
var cid = _dalamudUtil.GetCID();
var charaName = _dalamudUtil.GetPlayerNameAsync().GetAwaiter().GetResult();
var worldId = _dalamudUtil.GetHomeWorldIdAsync().GetAwaiter().GetResult();
var cid = _dalamudUtil.GetCIDAsync().GetAwaiter().GetResult();
if (!currentServer.Authentications.Any() && currentServer.SecretKeys.Any())
{
currentServer.Authentications.Add(new Authentication()
@@ -268,16 +268,16 @@ public class ServerConfigurationManager
{
if (serverSelectionIndex == -1) serverSelectionIndex = CurrentServerIndex;
var server = GetServerByIndex(serverSelectionIndex);
if (server.Authentications.Exists(c => string.Equals(c.CharacterName, _dalamudUtil.GetPlayerName(), StringComparison.Ordinal)
&& c.WorldId == _dalamudUtil.GetHomeWorldId()))
if (server.Authentications.Exists(c => string.Equals(c.CharacterName, _dalamudUtil.GetPlayerNameAsync().GetAwaiter().GetResult(), StringComparison.Ordinal)
&& c.WorldId == _dalamudUtil.GetHomeWorldIdAsync().GetAwaiter().GetResult()))
return;
server.Authentications.Add(new Authentication()
{
CharacterName = _dalamudUtil.GetPlayerName(),
WorldId = _dalamudUtil.GetHomeWorldId(),
CharacterName = _dalamudUtil.GetPlayerNameAsync().GetAwaiter().GetResult(),
WorldId = _dalamudUtil.GetHomeWorldIdAsync().GetAwaiter().GetResult(),
SecretKeyIdx = !server.UseOAuth2 ? server.SecretKeys.Last().Key : -1,
LastSeenCID = _dalamudUtil.GetCID()
LastSeenCID = _dalamudUtil.GetCIDAsync().GetAwaiter().GetResult()
});
Save();
}

View File

@@ -126,11 +126,11 @@ public sealed class TextureMetadataHelper
private const string TextureSegment = "/texture/";
private const string MaterialSegment = "/material/";
private const uint NormalSamplerId = ShpkFile.NormalSamplerId;
private const uint IndexSamplerId = ShpkFile.IndexSamplerId;
private const uint SpecularSamplerId = ShpkFile.SpecularSamplerId;
private const uint DiffuseSamplerId = ShpkFile.DiffuseSamplerId;
private const uint MaskSamplerId = ShpkFile.MaskSamplerId;
private const uint NormalSamplerId = 0x0C5EC1F1u;
private const uint IndexSamplerId = 0x565F8FD8u;
private const uint SpecularSamplerId = 0x2B99E025u;
private const uint DiffuseSamplerId = 0x115306BEu;
private const uint MaskSamplerId = 0x8A4E82B6u;
public TextureMetadataHelper(ILogger<TextureMetadataHelper> logger, IDataManager dataManager)
{
@@ -394,21 +394,6 @@ public sealed class TextureMetadataHelper
if (string.IsNullOrEmpty(fileNameWithExtension) && string.IsNullOrEmpty(fileNameWithoutExtension))
return TextureMapKind.Unknown;
if (normalized.Contains("/eye/eyelids_shadow.tex", StringComparison.Ordinal))
return TextureMapKind.Normal;
if (normalized.Contains("/ui/map/", StringComparison.Ordinal) && !string.IsNullOrEmpty(fileNameWithoutExtension))
{
if (fileNameWithoutExtension.EndsWith("m_m", StringComparison.Ordinal)
|| fileNameWithoutExtension.EndsWith("m_s", StringComparison.Ordinal))
return TextureMapKind.Mask;
if (fileNameWithoutExtension.EndsWith("_m", StringComparison.Ordinal)
|| fileNameWithoutExtension.EndsWith("_s", StringComparison.Ordinal)
|| fileNameWithoutExtension.EndsWith("d", StringComparison.Ordinal))
return TextureMapKind.Diffuse;
}
foreach (var (kind, token) in MapTokens)
{
if (!string.IsNullOrEmpty(fileNameWithExtension) &&
@@ -578,16 +563,7 @@ public sealed class TextureMetadataHelper
var normalized = format.ToUpperInvariant();
return normalized.Contains("A8", StringComparison.Ordinal)
|| normalized.Contains("A1", StringComparison.Ordinal)
|| normalized.Contains("A4", StringComparison.Ordinal)
|| normalized.Contains("A16", StringComparison.Ordinal)
|| normalized.Contains("A32", StringComparison.Ordinal)
|| normalized.Contains("ARGB", StringComparison.Ordinal)
|| normalized.Contains("RGBA", StringComparison.Ordinal)
|| normalized.Contains("BGRA", StringComparison.Ordinal)
|| normalized.Contains("DXT3", StringComparison.Ordinal)
|| normalized.Contains("DXT5", StringComparison.Ordinal)
|| normalized.Contains("BC2", StringComparison.Ordinal)
|| normalized.Contains("BC3", StringComparison.Ordinal)
|| normalized.Contains("BC7", StringComparison.Ordinal);
}

View File

@@ -105,7 +105,6 @@ public class UiFactory
groupData: groupData,
isLightfinderContext: isLightfinderContext,
lightfinderCid: lightfinderCid,
performanceCollector: _performanceCollectorService,
_apiController);
performanceCollector: _performanceCollectorService);
}
}

View File

@@ -843,16 +843,12 @@ public class CompactUi : WindowMediatorSubscriberBase
//Filter of not grouped/foldered and offline pairs
var allOnlineNotTaggedPairs = SortEntries(allEntries.Where(FilterNotTaggedUsers));
if (allOnlineNotTaggedPairs.Count > 0 && _configService.Current.ShowOfflineUsersSeparately) {
var filteredOnlineEntries = SortOnlineEntries(filteredEntries.Where(e => FilterNotTaggedUsers(e) && FilterOnlineOrPausedSelf(e)));
var onlineNotTaggedPairs = SortEntries(filteredEntries.Where(e => FilterNotTaggedUsers(e) && FilterOnlineOrPausedSelf(e)));
if (allOnlineNotTaggedPairs.Count > 0)
{
drawFolders.Add(_drawEntityFactory.CreateTagFolder(
TagHandler.CustomOnlineTag,
filteredOnlineEntries,
allOnlineNotTaggedPairs));
} else if (allOnlineNotTaggedPairs.Count > 0 && !_configService.Current.ShowOfflineUsersSeparately) {
var onlineNotTaggedPairs = SortEntries(filteredEntries.Where(FilterNotTaggedUsers));
drawFolders.Add(_drawEntityFactory.CreateTagFolder(
TagHandler.CustomAllTag,
_configService.Current.ShowOfflineUsersSeparately ? TagHandler.CustomOnlineTag : TagHandler.CustomAllTag,
onlineNotTaggedPairs,
allOnlineNotTaggedPairs));
}
@@ -889,7 +885,7 @@ public class CompactUi : WindowMediatorSubscriberBase
}
}
private static bool PassesFilter(PairUiEntry entry, string filter)
private bool PassesFilter(PairUiEntry entry, string filter)
{
if (string.IsNullOrEmpty(filter)) return true;
@@ -950,17 +946,6 @@ public class CompactUi : WindowMediatorSubscriberBase
};
}
private ImmutableList<PairUiEntry> SortOnlineEntries(IEnumerable<PairUiEntry> entries)
{
var entryList = entries.ToList();
return _configService.Current.OnlinePairSortMode switch
{
OnlinePairSortMode.Alphabetical => [.. entryList.OrderBy(e => AlphabeticalSortKey(e), StringComparer.OrdinalIgnoreCase)],
OnlinePairSortMode.PreferredDirectPairs => SortVisibleByPreferred(entryList),
_ => SortEntries(entryList),
};
}
private ImmutableList<PairUiEntry> SortVisibleByMetric(IEnumerable<PairUiEntry> entries, Func<PairUiEntry, long> selector)
{
return [.. entries

View File

@@ -4,8 +4,8 @@ using Dalamud.Interface.Utility.Raii;
using LightlessSync.UI.Handlers;
using LightlessSync.UI.Models;
using System.Collections.Immutable;
using LightlessSync.UI;
using LightlessSync.UI.Style;
using OtterGui.Text;
namespace LightlessSync.UI.Components;
@@ -113,13 +113,9 @@ public abstract class DrawFolderBase : IDrawFolder
using var indent = ImRaii.PushIndent(_uiSharedService.GetIconSize(FontAwesomeIcon.EllipsisV).X + ImGui.GetStyle().ItemSpacing.X, false);
if (DrawPairs.Any())
{
using var clipper = ImUtf8.ListClipper(DrawPairs.Count, ImGui.GetFrameHeightWithSpacing());
while (clipper.Step())
foreach (var item in DrawPairs)
{
for (var i = clipper.DisplayStart; i < clipper.DisplayEnd; i++)
{
DrawPairs[i].DrawPairedClient();
}
item.DrawPairedClient();
}
}
else

View File

@@ -169,16 +169,11 @@ public class DrawFolderTag : DrawFolderBase
protected override float DrawRightSide(float currentRightSideX)
{
if (string.Equals(_id, TagHandler.CustomVisibleTag, StringComparison.Ordinal))
if (_id == TagHandler.CustomVisibleTag)
{
return DrawVisibleFilter(currentRightSideX);
}
if (string.Equals(_id, TagHandler.CustomOnlineTag, StringComparison.Ordinal))
{
return DrawOnlineFilter(currentRightSideX);
}
if (!RenderPause)
{
return currentRightSideX;
@@ -259,7 +254,7 @@ public class DrawFolderTag : DrawFolderBase
foreach (VisiblePairSortMode mode in Enum.GetValues<VisiblePairSortMode>())
{
var selected = _configService.Current.VisiblePairSortMode == mode;
if (ImGui.MenuItem(GetSortVisibleLabel(mode), string.Empty, selected))
if (ImGui.MenuItem(GetSortLabel(mode), string.Empty, selected))
{
if (!selected)
{
@@ -278,49 +273,7 @@ public class DrawFolderTag : DrawFolderBase
return buttonStart - spacingX;
}
private float DrawOnlineFilter(float currentRightSideX)
{
var buttonSize = _uiSharedService.GetIconButtonSize(FontAwesomeIcon.Filter);
var spacingX = ImGui.GetStyle().ItemSpacing.X;
var buttonStart = currentRightSideX - buttonSize.X;
ImGui.SameLine(buttonStart);
if (_uiSharedService.IconButton(FontAwesomeIcon.Filter))
{
SuppressNextRowToggle();
ImGui.OpenPopup($"online-filter-{_id}");
}
UiSharedService.AttachToolTip("Adjust how online pairs are ordered.");
if (ImGui.BeginPopup($"online-filter-{_id}"))
{
ImGui.TextUnformatted("Online Pair Ordering");
ImGui.Separator();
foreach (OnlinePairSortMode mode in Enum.GetValues<OnlinePairSortMode>())
{
var selected = _configService.Current.OnlinePairSortMode == mode;
if (ImGui.MenuItem(GetSortOnlineLabel(mode), string.Empty, selected))
{
if (!selected)
{
_configService.Current.OnlinePairSortMode = mode;
_configService.Save();
_mediator.Publish(new RefreshUiMessage());
}
ImGui.CloseCurrentPopup();
}
}
ImGui.EndPopup();
}
return buttonStart - spacingX;
}
private static string GetSortVisibleLabel(VisiblePairSortMode mode) => mode switch
private static string GetSortLabel(VisiblePairSortMode mode) => mode switch
{
VisiblePairSortMode.Alphabetical => "Alphabetical",
VisiblePairSortMode.VramUsage => "VRAM usage (descending)",
@@ -329,11 +282,4 @@ public class DrawFolderTag : DrawFolderBase
VisiblePairSortMode.PreferredDirectPairs => "Preferred permissions & Direct pairs",
_ => "Default",
};
private static string GetSortOnlineLabel(OnlinePairSortMode mode) => mode switch
{
OnlinePairSortMode.Alphabetical => "Alphabetical",
OnlinePairSortMode.PreferredDirectPairs => "Preferred permissions & Direct pairs",
_ => "Default",
};
}

View File

@@ -133,26 +133,6 @@ public class DrawUserPair
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))
{
_ = _apiController.CyclePauseAsync(_pair);
@@ -757,19 +737,14 @@ public class DrawUserPair
}
UiSharedService.AttachToolTip("Hold CTRL and click to remove user " + (_pair.UserData.AliasOrUID) + " from Syncshell");
var banEnabled = UiSharedService.CtrlPressed();
var banLabel = banEnabled ? "Ban user" : "Ban user (Hold CTRL)";
if (_uiSharedService.IconTextButton(FontAwesomeIcon.UserSlash, banLabel, _menuWidth, true) && banEnabled)
if (_uiSharedService.IconTextButton(FontAwesomeIcon.UserSlash, "Ban User", _menuWidth, true))
{
_mediator.Publish(new OpenBanUserPopupMessage(_pair, group));
ImGui.CloseCurrentPopup();
}
UiSharedService.AttachToolTip("Hold CTRL to ban user " + (_pair.UserData.AliasOrUID) + " from this Syncshell");
UiSharedService.AttachToolTip("Ban user from this Syncshell");
if (showOwnerActions)
{
ImGui.Separator();
}
ImGui.Separator();
}
if (showOwnerActions)

View File

@@ -14,7 +14,6 @@ using LightlessSync.Services.TextureCompression;
using LightlessSync.Utils;
using Microsoft.Extensions.Logging;
using OtterTex;
using System.Buffers.Binary;
using System.Globalization;
using System.Numerics;
using SixLabors.ImageSharp;
@@ -50,7 +49,6 @@ public class DataAnalysisUi : WindowMediatorSubscriberBase
private readonly Dictionary<string, TextureCompressionTarget> _textureSelections = new(StringComparer.OrdinalIgnoreCase);
private readonly HashSet<string> _selectedTextureKeys = new(StringComparer.OrdinalIgnoreCase);
private readonly Dictionary<string, TexturePreviewState> _texturePreviews = new(StringComparer.OrdinalIgnoreCase);
private readonly Dictionary<string, TextureResolutionInfo?> _textureResolutionCache = new(StringComparer.OrdinalIgnoreCase);
private readonly Dictionary<ObjectKind, TextureWorkspaceTab> _textureWorkspaceTabs = new();
private readonly List<string> _storedPathsToRemove = [];
private readonly Dictionary<string, string> _filePathResolve = [];
@@ -90,9 +88,6 @@ public class DataAnalysisUi : WindowMediatorSubscriberBase
private bool _showAlreadyAddedTransients = false;
private bool _acknowledgeReview = false;
private Task<TextureRowBuildResult>? _textureRowsBuildTask;
private CancellationTokenSource? _textureRowsBuildCts;
private ObjectKind _selectedObjectTab;
private TextureUsageCategory? _textureCategoryFilter = null;
@@ -209,9 +204,9 @@ public class DataAnalysisUi : WindowMediatorSubscriberBase
return;
}
_cachedAnalysis = CloneAnalysis(_characterAnalyzer.LastAnalysis);
_cachedAnalysis = _characterAnalyzer.LastAnalysis.DeepClone();
_hasUpdate = false;
InvalidateTextureRows();
_textureRowsDirty = true;
}
private void DrawContentTabs()
@@ -755,7 +750,7 @@ public class DataAnalysisUi : WindowMediatorSubscriberBase
_selectedTextureKeys.Clear();
_textureSelections.Clear();
ResetTextureFilters();
InvalidateTextureRows();
_textureRowsDirty = true;
_conversionFailed = false;
}
@@ -767,8 +762,6 @@ public class DataAnalysisUi : WindowMediatorSubscriberBase
preview.Texture?.Dispose();
}
_texturePreviews.Clear();
_textureRowsBuildCts?.Cancel();
_textureRowsBuildCts?.Dispose();
_conversionProgress.ProgressChanged -= ConversionProgress_ProgressChanged;
}
@@ -782,108 +775,18 @@ public class DataAnalysisUi : WindowMediatorSubscriberBase
private void EnsureTextureRows()
{
if (_cachedAnalysis == null)
if (!_textureRowsDirty || _cachedAnalysis == null)
{
return;
}
if (_textureRowsDirty && _textureRowsBuildTask == null)
{
_textureRowsBuildCts?.Dispose();
_textureRowsBuildCts = new();
var snapshot = _cachedAnalysis;
_textureRowsBuildTask = Task.Run(() => BuildTextureRows(snapshot, _textureRowsBuildCts.Token), _textureRowsBuildCts.Token);
}
if (_textureRowsBuildTask == null || !_textureRowsBuildTask.IsCompleted)
{
return;
}
var completedTask = _textureRowsBuildTask;
_textureRowsBuildTask = null;
_textureRowsBuildCts?.Dispose();
_textureRowsBuildCts = null;
if (completedTask.IsCanceled)
{
return;
}
if (completedTask.IsFaulted)
{
_logger.LogWarning(completedTask.Exception, "Failed to build texture rows.");
_textureRowsDirty = false;
return;
}
ApplyTextureRowBuild(completedTask.Result);
_textureRowsDirty = false;
}
private void ApplyTextureRowBuild(TextureRowBuildResult result)
{
_textureRows.Clear();
_textureRows.AddRange(result.Rows);
foreach (var row in _textureRows)
{
if (row.IsAlreadyCompressed)
{
_selectedTextureKeys.Remove(row.Key);
_textureSelections.Remove(row.Key);
}
}
_selectedTextureKeys.RemoveWhere(key => !result.ValidKeys.Contains(key));
foreach (var key in _texturePreviews.Keys.ToArray())
{
if (!result.ValidKeys.Contains(key) && _texturePreviews.TryGetValue(key, out var preview))
{
preview.Texture?.Dispose();
_texturePreviews.Remove(key);
}
}
foreach (var key in _textureResolutionCache.Keys.ToArray())
{
if (!result.ValidKeys.Contains(key))
{
_textureResolutionCache.Remove(key);
}
}
foreach (var key in _textureSelections.Keys.ToArray())
{
if (!result.ValidKeys.Contains(key))
{
_textureSelections.Remove(key);
continue;
}
_textureSelections[key] = _textureCompressionService.NormalizeTarget(_textureSelections[key]);
}
if (!string.IsNullOrEmpty(_selectedTextureKey) && !result.ValidKeys.Contains(_selectedTextureKey))
{
_selectedTextureKey = string.Empty;
}
}
private TextureRowBuildResult BuildTextureRows(
Dictionary<ObjectKind, Dictionary<string, CharacterAnalyzer.FileDataEntry>> analysis,
CancellationToken token)
{
var rows = new List<TextureRow>();
HashSet<string> validKeys = new(StringComparer.OrdinalIgnoreCase);
foreach (var (objectKind, entries) in analysis)
foreach (var (objectKind, entries) in _cachedAnalysis)
{
foreach (var entry in entries.Values)
{
token.ThrowIfCancellationRequested();
if (!string.Equals(entry.FileType, "tex", StringComparison.Ordinal))
{
continue;
@@ -925,11 +828,17 @@ public class DataAnalysisUi : WindowMediatorSubscriberBase
suggestion?.Reason);
validKeys.Add(row.Key);
rows.Add(row);
_textureRows.Add(row);
if (row.IsAlreadyCompressed)
{
_selectedTextureKeys.Remove(row.Key);
_textureSelections.Remove(row.Key);
}
}
}
rows.Sort((a, b) =>
_textureRows.Sort((a, b) =>
{
var comp = a.ObjectKind.CompareTo(b.ObjectKind);
if (comp != 0)
@@ -942,14 +851,34 @@ public class DataAnalysisUi : WindowMediatorSubscriberBase
return string.Compare(a.DisplayName, b.DisplayName, StringComparison.OrdinalIgnoreCase);
});
return new TextureRowBuildResult(rows, validKeys);
}
_selectedTextureKeys.RemoveWhere(key => !validKeys.Contains(key));
private void InvalidateTextureRows()
{
_textureRowsDirty = true;
_textureRowsBuildCts?.Cancel();
_textureResolutionCache.Clear();
foreach (var key in _texturePreviews.Keys.ToArray())
{
if (!validKeys.Contains(key) && _texturePreviews.TryGetValue(key, out var preview))
{
preview.Texture?.Dispose();
_texturePreviews.Remove(key);
}
}
foreach (var key in _textureSelections.Keys.ToArray())
{
if (!validKeys.Contains(key))
{
_textureSelections.Remove(key);
continue;
}
_textureSelections[key] = _textureCompressionService.NormalizeTarget(_textureSelections[key]);
}
if (!string.IsNullOrEmpty(_selectedTextureKey) && !validKeys.Contains(_selectedTextureKey))
{
_selectedTextureKey = string.Empty;
}
_textureRowsDirty = false;
}
private static string MakeTextureKey(ObjectKind objectKind, string primaryFilePath) =>
@@ -964,35 +893,6 @@ public class DataAnalysisUi : WindowMediatorSubscriberBase
_textureSearch = string.Empty;
}
private static Dictionary<ObjectKind, Dictionary<string, CharacterAnalyzer.FileDataEntry>> CloneAnalysis(
Dictionary<ObjectKind, Dictionary<string, CharacterAnalyzer.FileDataEntry>> source)
{
var clone = new Dictionary<ObjectKind, Dictionary<string, CharacterAnalyzer.FileDataEntry>>(source.Count);
foreach (var (objectKind, entries) in source)
{
var entryClone = new Dictionary<string, CharacterAnalyzer.FileDataEntry>(entries.Count, entries.Comparer);
foreach (var (hash, entry) in entries)
{
entryClone[hash] = new CharacterAnalyzer.FileDataEntry(
hash: hash,
fileType: entry.FileType,
gamePaths: entry.GamePaths?.ToList() ?? [],
filePaths: entry.FilePaths?.ToList() ?? [],
originalSize: entry.OriginalSize,
compressedSize: entry.CompressedSize,
triangles: entry.Triangles,
cacheEntries: entry.CacheEntries
);
}
clone[objectKind] = entryClone;
}
return clone;
}
private void DrawAnalysisOverview(int totalFiles, long totalActualSize, long totalCompressedSize, long totalTriangles, string breakdownTooltip)
{
var scale = ImGuiHelpers.GlobalScale;
@@ -1191,10 +1091,6 @@ public class DataAnalysisUi : WindowMediatorSubscriberBase
public bool IsAlreadyCompressed => CurrentTarget.HasValue;
}
private sealed record TextureRowBuildResult(
List<TextureRow> Rows,
HashSet<string> ValidKeys);
private sealed class TexturePreviewState
{
public Task? LoadTask { get; set; }
@@ -1203,22 +1099,6 @@ public class DataAnalysisUi : WindowMediatorSubscriberBase
public DateTime LastAccessUtc { get; set; } = DateTime.UtcNow;
}
private readonly struct TextureResolutionInfo
{
public TextureResolutionInfo(ushort width, ushort height, ushort depth, ushort mipLevels)
{
Width = width;
Height = height;
Depth = depth;
MipLevels = mipLevels;
}
public ushort Width { get; }
public ushort Height { get; }
public ushort Depth { get; }
public ushort MipLevels { get; }
}
private void DrawTextureWorkspace(ObjectKind objectKind, IReadOnlyList<IGrouping<string, CharacterAnalyzer.FileDataEntry>> otherFileGroups)
{
if (!_textureWorkspaceTabs.ContainsKey(objectKind))
@@ -1263,11 +1143,6 @@ public class DataAnalysisUi : WindowMediatorSubscriberBase
private void DrawTextureTabContent(ObjectKind objectKind)
{
var scale = ImGuiHelpers.GlobalScale;
if (_textureRowsBuildTask != null && !_textureRowsBuildTask.IsCompleted && _textureRows.Count == 0)
{
UiSharedService.ColorText("Building texture list.", ImGuiColors.DalamudGrey);
return;
}
var objectRows = _textureRows.Where(row => row.ObjectKind == objectKind).ToList();
var hasAnyTextureRows = objectRows.Count > 0;
var availableCategories = objectRows.Select(row => row.Category)
@@ -1529,24 +1404,6 @@ public class DataAnalysisUi : WindowMediatorSubscriberBase
{
ResetTextureFilters();
}
ImGuiHelpers.ScaledDummy(6);
ImGui.Separator();
ImGuiHelpers.ScaledDummy(4);
UiSharedService.ColorText("Texture row colors", UIColors.Get("LightlessPurple"));
DrawTextureRowLegendItem("Selected", UIColors.Get("LightlessYellow"), "This row is selected in the texture table.");
DrawTextureRowLegendItem("Already compressed", UIColors.Get("LightlessGreenDefault"), "Texture is already stored in a compressed format.");
DrawTextureRowLegendItem("Missing analysis data", UIColors.Get("DimRed"), "File size data has not been computed yet.");
}
private static void DrawTextureRowLegendItem(string label, Vector4 color, string description)
{
var scale = ImGuiHelpers.GlobalScale;
var swatchSize = new Vector2(12f * scale, 12f * scale);
ImGui.ColorButton($"##textureRowLegend{label}", color, ImGuiColorEditFlags.NoTooltip | ImGuiColorEditFlags.NoDragDrop, swatchSize);
ImGui.SameLine(0f, 6f * scale);
var wrapPos = ImGui.GetCursorPosX() + ImGui.GetContentRegionAvail().X;
UiSharedService.TextWrapped($"{label}: {description}", wrapPos);
}
private static void DrawEnumFilterCombo<T>(
@@ -1953,7 +1810,7 @@ public class DataAnalysisUi : WindowMediatorSubscriberBase
ImGui.SameLine();
if (_uiSharedService.IconTextButton(FontAwesomeIcon.Sync, "Refresh", 130f * scale))
{
InvalidateTextureRows();
_textureRowsDirty = true;
}
TextureRow? lastSelected = null;
@@ -2119,7 +1976,7 @@ public class DataAnalysisUi : WindowMediatorSubscriberBase
{
_selectedTextureKeys.Clear();
_textureSelections.Clear();
InvalidateTextureRows();
_textureRowsDirty = true;
}
}
@@ -2340,68 +2197,6 @@ public class DataAnalysisUi : WindowMediatorSubscriberBase
}
}
private TextureResolutionInfo? GetTextureResolution(TextureRow row)
{
if (_textureResolutionCache.TryGetValue(row.Key, out var cached))
{
return cached;
}
var info = TryReadTextureResolution(row.PrimaryFilePath, out var resolved)
? resolved
: (TextureResolutionInfo?)null;
_textureResolutionCache[row.Key] = info;
return info;
}
private static bool TryReadTextureResolution(string path, out TextureResolutionInfo info)
{
info = default;
try
{
Span<byte> buffer = stackalloc byte[16];
using var stream = new FileStream(path, FileMode.Open, FileAccess.Read, FileShare.ReadWrite | FileShare.Delete);
var read = stream.Read(buffer);
if (read < buffer.Length)
{
return false;
}
var width = BinaryPrimitives.ReadUInt16LittleEndian(buffer[8..10]);
var height = BinaryPrimitives.ReadUInt16LittleEndian(buffer[10..12]);
var depth = BinaryPrimitives.ReadUInt16LittleEndian(buffer[12..14]);
var mipLevels = BinaryPrimitives.ReadUInt16LittleEndian(buffer[14..16]);
if (width == 0 || height == 0)
{
return false;
}
if (depth == 0)
{
depth = 1;
}
if (mipLevels == 0)
{
mipLevels = 1;
}
info = new TextureResolutionInfo(width, height, depth, mipLevels);
return true;
}
catch
{
return false;
}
}
private static string FormatTextureResolution(TextureResolutionInfo info)
=> info.Depth > 1
? $"{info.Width} x {info.Height} x {info.Depth}"
: $"{info.Width} x {info.Height}";
private void DrawTextureRow(TextureRow row, IReadOnlyList<TextureCompressionTarget> targets, int index)
{
var key = row.Key;
@@ -2670,9 +2465,6 @@ public class DataAnalysisUi : WindowMediatorSubscriberBase
MetaRow(FontAwesomeIcon.LayerGroup, "Map Type", row.MapKind.ToString());
MetaRow(FontAwesomeIcon.Fingerprint, "Hash", row.Hash, UIColors.Get("LightlessBlue"));
MetaRow(FontAwesomeIcon.InfoCircle, "Current Format", row.Format);
var resolution = GetTextureResolution(row);
var resolutionLabel = resolution.HasValue ? FormatTextureResolution(resolution.Value) : "Unknown";
MetaRow(FontAwesomeIcon.Images, "Resolution", resolutionLabel);
var selectedLabel = hasSelectedInfo ? selectedInfo!.Title : selectedTarget.ToString();
var selectionColor = hasSelectedInfo ? UIColors.Get("LightlessYellow") : UIColors.Get("LightlessGreen");

View File

@@ -164,25 +164,9 @@ public class DownloadUi : WindowMediatorSubscriberBase
const float rounding = 6f;
var shadowOffset = new Vector2(2, 2);
List<KeyValuePair<GameObjectHandler, Dictionary<string, FileDownloadStatus>>> transfers;
try
{
transfers = _currentDownloads.ToList();
}
catch (ArgumentException)
{
return;
}
foreach (var transfer in transfers)
foreach (var transfer in _currentDownloads.ToList())
{
var transferKey = transfer.Key;
// Skip if no valid game object
if (transferKey.GetGameObject() == null)
continue;
var rawPos = _dalamudUtilService.WorldToScreen(transferKey.GetGameObject());
// If RawPos is zero, remove it from smoothed dictionary

View File

@@ -46,12 +46,10 @@ public sealed class DtrEntry : IDisposable, IHostedService
private string? _lightfinderText;
private string? _lightfinderTooltip;
private Colors _lightfinderColors;
private readonly object _localHashedCidLock = new();
private string? _localHashedCid;
private DateTime _localHashedCidFetchedAt = DateTime.MinValue;
private DateTime _localHashedCidNextErrorLog = DateTime.MinValue;
private DateTime _pairRequestNextErrorLog = DateTime.MinValue;
private int _localHashedCidRefreshActive;
public DtrEntry(
ILogger<DtrEntry> logger,
@@ -341,61 +339,29 @@ public sealed class DtrEntry : IDisposable, IHostedService
private string? GetLocalHashedCid()
{
var now = DateTime.UtcNow;
lock (_localHashedCidLock)
{
if (_localHashedCid is not null && now - _localHashedCidFetchedAt < _localHashedCidCacheDuration)
{
return _localHashedCid;
}
}
QueueLocalHashedCidRefresh();
lock (_localHashedCidLock)
{
if (_localHashedCid is not null && now - _localHashedCidFetchedAt < _localHashedCidCacheDuration)
return _localHashedCid;
}
}
private void QueueLocalHashedCidRefresh()
{
if (Interlocked.Exchange(ref _localHashedCidRefreshActive, 1) != 0)
try
{
return;
var cid = _dalamudUtilService.GetCIDAsync().GetAwaiter().GetResult();
var hashedCid = cid.ToString().GetHash256();
_localHashedCid = hashedCid;
_localHashedCidFetchedAt = now;
return hashedCid;
}
_ = Task.Run(async () =>
catch (Exception ex)
{
try
if (now >= _localHashedCidNextErrorLog)
{
var cid = _dalamudUtilService.GetCID();
var hashedCid = cid.ToString().GetHash256();
lock (_localHashedCidLock)
{
_localHashedCid = hashedCid;
_localHashedCidFetchedAt = DateTime.UtcNow;
}
_logger.LogDebug(ex, "Failed to refresh local hashed CID for Lightfinder DTR entry.");
_localHashedCidNextErrorLog = now + _localHashedCidErrorCooldown;
}
catch (Exception ex)
{
var now = DateTime.UtcNow;
lock (_localHashedCidLock)
{
if (now >= _localHashedCidNextErrorLog)
{
_logger.LogDebug(ex, "Failed to refresh local hashed CID for Lightfinder DTR entry.");
_localHashedCidNextErrorLog = now + _localHashedCidErrorCooldown;
}
_localHashedCid = null;
_localHashedCidFetchedAt = now;
}
}
finally
{
Interlocked.Exchange(ref _localHashedCidRefreshActive, 0);
}
});
_localHashedCid = null;
_localHashedCidFetchedAt = now;
return null;
}
}
private List<string> GetNearbyBroadcasts()

View File

@@ -23,7 +23,6 @@ namespace LightlessSync.UI
private readonly LightFinderService _broadcastService;
private readonly UiSharedService _uiSharedService;
private readonly LightFinderScannerService _broadcastScannerService;
private readonly LightFinderPlateHandler _lightFinderPlateHandler;
private IReadOnlyList<GroupFullInfoDto> _allSyncshells = Array.Empty<GroupFullInfoDto>();
private string _userUid = string.Empty;
@@ -39,8 +38,7 @@ namespace LightlessSync.UI
UiSharedService uiShared,
ApiController apiController,
LightFinderScannerService broadcastScannerService
,
LightFinderPlateHandler lightFinderPlateHandler) : base(logger, mediator, "Lightfinder###LightlessLightfinderUI", performanceCollectorService)
) : base(logger, mediator, "Lightfinder###LightlessLightfinderUI", performanceCollectorService)
{
_broadcastService = broadcastService;
_uiSharedService = uiShared;
@@ -52,7 +50,6 @@ namespace LightlessSync.UI
WindowBuilder.For(this)
.SetSizeConstraints(new Vector2(600, 465), new Vector2(750, 525))
.Apply();
_lightFinderPlateHandler = lightFinderPlateHandler;
}
private void RebuildSyncshellDropdownOptions()
@@ -304,14 +301,6 @@ namespace LightlessSync.UI
bool ShellFinderEnabled = _configService.Current.SyncshellFinderEnabled;
bool isBroadcasting = _broadcastService.IsBroadcasting;
if (isBroadcasting)
{
var warningColor = UIColors.Get("LightlessYellow");
_uiSharedService.DrawNoteLine("! ", warningColor,
new SeStringUtils.RichTextEntry("Syncshell Finder can only be changed while Lightfinder is disabled.", warningColor));
ImGuiHelpers.ScaledDummy(0.2f);
}
if (isBroadcasting)
ImGui.BeginDisabled();
@@ -383,47 +372,9 @@ namespace LightlessSync.UI
#if DEBUG
if (ImGui.BeginTabItem("Debug"))
{
if (ImGui.CollapsingHeader("LightFinder Plates", ImGuiTreeNodeFlags.DefaultOpen))
{
var h = _lightFinderPlateHandler;
var enabled = h.DebugEnabled;
if (ImGui.Checkbox("Enable LightFinder debug", ref enabled))
h.DebugEnabled = enabled;
if (h.DebugEnabled)
{
ImGui.Indent();
var disableOcc = h.DebugDisableOcclusion;
if (ImGui.Checkbox("Disable occlusion (force draw)", ref disableOcc))
h.DebugDisableOcclusion = disableOcc;
var drawUiRects = h.DebugDrawUiRects;
if (ImGui.Checkbox("Draw UI rects", ref drawUiRects))
h.DebugDrawUiRects = drawUiRects;
var drawLabelRects = h.DebugDrawLabelRects;
if (ImGui.Checkbox("Draw label rects", ref drawLabelRects))
h.DebugDrawLabelRects = drawLabelRects;
ImGui.Separator();
ImGui.TextUnformatted($"Labels last frame: {h.DebugLabelCountLastFrame}");
ImGui.TextUnformatted($"UI rects last frame: {h.DebugUiRectCountLastFrame}");
ImGui.TextUnformatted($"Occluded last frame: {h.DebugOccludedCountLastFrame}");
ImGui.TextUnformatted($"Last NamePlate frame: {h.DebugLastNameplateFrame}");
ImGui.Unindent();
}
}
ImGui.Separator();
ImGui.Text("Broadcast Cache");
if (ImGui.BeginTable("##BroadcastCacheTable", 4,
ImGuiTableFlags.RowBg | ImGuiTableFlags.Borders | ImGuiTableFlags.ScrollY,
new Vector2(-1, 225f)))
if (ImGui.BeginTable("##BroadcastCacheTable", 4, ImGuiTableFlags.RowBg | ImGuiTableFlags.Borders | ImGuiTableFlags.ScrollY, new Vector2(-1, 225f)))
{
ImGui.TableSetupColumn("CID", ImGuiTableColumnFlags.WidthStretch);
ImGui.TableSetupColumn("IsBroadcasting", ImGuiTableColumnFlags.WidthStretch);

View File

@@ -1,7 +0,0 @@
namespace LightlessSync.UI.Models;
public enum OnlinePairSortMode
{
Alphabetical = 0,
PreferredDirectPairs = 1,
}

View File

@@ -2,9 +2,10 @@ namespace LightlessSync.UI.Models;
public enum VisiblePairSortMode
{
Alphabetical = 0,
VramUsage = 1,
EffectiveVramUsage = 2,
TriangleCount = 3,
PreferredDirectPairs = 4,
Default = 0,
Alphabetical = 1,
VramUsage = 2,
EffectiveVramUsage = 3,
TriangleCount = 4,
PreferredDirectPairs = 5,
}

File diff suppressed because it is too large Load Diff

View File

@@ -11,7 +11,6 @@ using LightlessSync.Services.ServerConfiguration;
using LightlessSync.UI.Services;
using LightlessSync.UI.Tags;
using LightlessSync.Utils;
using LightlessSync.WebAPI;
using Microsoft.Extensions.Logging;
using System.Numerics;
@@ -23,7 +22,6 @@ public class StandaloneProfileUi : WindowMediatorSubscriberBase
private readonly PairUiService _pairUiService;
private readonly ServerConfigurationManager _serverManager;
private readonly ProfileTagService _profileTagService;
private readonly ApiController _apiController;
private readonly UiSharedService _uiSharedService;
private readonly UserData? _userData;
private readonly GroupData? _groupData;
@@ -62,8 +60,7 @@ public class StandaloneProfileUi : WindowMediatorSubscriberBase
GroupData? groupData,
bool isLightfinderContext,
string? lightfinderCid,
PerformanceCollectorService performanceCollector,
ApiController apiController)
PerformanceCollectorService performanceCollector)
: base(logger, mediator, BuildWindowTitle(
userData,
groupData,
@@ -97,7 +94,6 @@ public class StandaloneProfileUi : WindowMediatorSubscriberBase
.Apply();
IsOpen = true;
_apiController = apiController;
}
public Pair? Pair { get; }
@@ -252,33 +248,19 @@ public class StandaloneProfileUi : WindowMediatorSubscriberBase
ResetBannerTexture();
_lastBannerPicture = bannerBytes;
}
string? noteText = null;
var isSelfProfile = !_isLightfinderContext
&& _userData is not null
&& !string.IsNullOrEmpty(_apiController.UID)
&& string.Equals(_userData.UID, _apiController.UID, StringComparison.Ordinal);
string statusLabel = _isLightfinderContext
? "Exploring"
: isSelfProfile ? "Online" : "Offline";
string statusLabel = _isLightfinderContext ? "Exploring" : "Offline";
string? visiblePlayerName = null;
bool directPair = false;
bool youPaused = false;
bool theyPaused = false;
List<string> syncshellLines = [];
if (!_isLightfinderContext)
{
noteText = _serverManager.GetNoteForUid(_userData!.UID);
}
if (!_isLightfinderContext && Pair != null)
{
var snapshot = _pairUiService.GetSnapshot();
noteText = _serverManager.GetNoteForUid(Pair.UserData.UID);
statusLabel = Pair.IsVisible ? "Visible" : (Pair.IsOnline ? "Online" : "Offline");
visiblePlayerName = Pair.IsVisible ? Pair.PlayerName : null;
@@ -300,15 +282,11 @@ public class StandaloneProfileUi : WindowMediatorSubscriberBase
var groupLabel = snapshot.GroupsByGid.TryGetValue(gid, out var groupInfo)
? groupInfo.GroupAliasOrGID
: gid;
var groupNote = _serverManager.GetNoteForGid(gid);
syncshellLines.Add(string.IsNullOrEmpty(groupNote) ? groupLabel : $"{groupNote} ({groupLabel})");
}
}
}
if (isSelfProfile)
statusLabel = "Online";
}
var presenceTokens = new List<PresenceToken>

View File

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

View File

@@ -116,7 +116,7 @@ public class SyncshellAdminUI : WindowMediatorSubscriberBase
var drawList = ImGui.GetWindowDrawList();
var purple = UIColors.Get("LightlessPurple");
var gradLeft = purple.WithAlpha(0.0f);
var gradLeft = purple.WithAlpha(0.0f);
var gradRight = purple.WithAlpha(0.85f);
uint colTopLeft = ImGui.ColorConvertFloat4ToU32(gradLeft);
@@ -162,7 +162,7 @@ public class SyncshellAdminUI : WindowMediatorSubscriberBase
var subtitlePos = new Vector2(
pMin.X + 12f * scale,
titlePos.Y + titleHeight - 2f * scale);
titlePos.Y + titleHeight - 2f * scale);
ImGui.SetCursorScreenPos(subtitlePos);
ImGui.PushStyleColor(ImGuiCol.Text, ImGuiColors.DalamudGrey);
@@ -297,25 +297,6 @@ public class SyncshellAdminUI : WindowMediatorSubscriberBase
var ownerTab = ImRaii.TabItem("Owner Settings");
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.TextUnformatted("New Password");
var availableWidth = ImGui.GetWindowContentRegionMax().X - ImGui.GetWindowContentRegionMin().X;
@@ -392,27 +373,25 @@ public class SyncshellAdminUI : WindowMediatorSubscriberBase
}
UiSharedService.AttachToolTip("When enabled, inactive non-pinned, non-moderator users will be pruned automatically on the server.");
if (!_autoPruneEnabled)
{
ImGui.BeginDisabled();
}
ImGui.SameLine();
ImGui.SetNextItemWidth(150);
_uiSharedService.DrawCombo(
"Day(s) of inactivity (gets checked hourly)",
[0, 1, 3, 7, 14, 30, 90],
(count) => count == 0 ? "2 hours(s)" : count + " day(s)",
selected =>
{
_autoPruneDays = selected;
SavePruneSettings();
},
_autoPruneDays);
using (ImRaii.Disabled(!_autoPruneEnabled))
{
_uiSharedService.DrawCombo(
"Day(s) of inactivity",
[1, 3, 7, 14, 30, 90],
days => $"{days} day(s)",
selected =>
{
_autoPruneDays = selected;
SavePruneSettings();
},
_autoPruneDays);
}
if (!_autoPruneEnabled)
{
ImGui.EndDisabled();
UiSharedService.ColorTextWrapped(
"Automatic prune is currently disabled. Enable it and choose an inactivity threshold to let the server clean up inactive users automatically.",
ImGuiColors.DalamudGrey);
@@ -595,7 +574,7 @@ public class SyncshellAdminUI : WindowMediatorSubscriberBase
_uiSharedService.DrawCombo(
"Day(s) of inactivity",
[0, 1, 3, 7, 14, 30, 90],
(count) => count == 0 ? "2 hours(s)" : count + " day(s)",
(count) => count == 0 ? "15 minute(s)" : count + " day(s)",
(selected) =>
{
_pruneDays = selected;
@@ -665,8 +644,8 @@ public class SyncshellAdminUI : WindowMediatorSubscriberBase
var style = ImGui.GetStyle();
float fullW = ImGui.GetContentRegionAvail().X;
float colIdentity = fullW * 0.45f;
float colMeta = fullW * 0.35f;
float colIdentity = fullW * 0.45f;
float colMeta = fullW * 0.35f;
float colActions = fullW - colIdentity - colMeta - style.ItemSpacing.X * 2.0f;
// Header
@@ -875,7 +854,7 @@ public class SyncshellAdminUI : WindowMediatorSubscriberBase
var boolcolor = UiSharedService.GetBoolColor(pair.IsOnline);
UiSharedService.ColorText(text, boolcolor);
if (ImGui.IsItemClicked())
ImGui.SetClipboardText(pair.UserData.AliasOrUID);
@@ -1095,7 +1074,6 @@ public class SyncshellAdminUI : WindowMediatorSubscriberBase
ImGui.Dummy(new Vector2(0, 4 * ImGuiHelpers.GlobalScale));
}
private void SavePruneSettings()
{
if (_autoPruneDays <= 0)
@@ -1103,7 +1081,8 @@ public class SyncshellAdminUI : WindowMediatorSubscriberBase
_autoPruneEnabled = false;
}
var dto = new GroupPruneSettingsDto(Group: GroupFullInfo.Group, AutoPruneEnabled: _autoPruneEnabled, AutoPruneDays: _autoPruneDays);
var enabled = _autoPruneEnabled && _autoPruneDays > 0;
var dto = new GroupPruneSettingsDto(Group: GroupFullInfo.Group, AutoPruneEnabled: enabled, AutoPruneDays: enabled ? _autoPruneDays : 0);
try
{

View File

@@ -140,10 +140,19 @@ public class SyncshellFinderUI : WindowMediatorSubscriberBase
return;
}
var broadcasts = _broadcastScannerService.GetActiveSyncshellBroadcasts().ToList() ?? [];
_broadcastScannerService.TryGetLocalHashedCid(out var localHashedCid);
string? myHashedCid = null;
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)
{
@@ -176,15 +185,9 @@ public class SyncshellFinderUI : WindowMediatorSubscriberBase
broadcasterName = !string.IsNullOrEmpty(worldName)
? $"{name} ({worldName})"
: 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)
@@ -207,7 +210,7 @@ public class SyncshellFinderUI : WindowMediatorSubscriberBase
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;
var totalPages = (int)Math.Ceiling(listData.Count / (float)shellsPerPage);
@@ -224,10 +227,7 @@ public class SyncshellFinderUI : WindowMediatorSubscriberBase
for (int index = firstIndex; index < lastExclusive; index++)
{
var (shell, broadcasterName, isSelfBroadcast) = listData[index];
var broadcasterLabel = string.IsNullOrEmpty(broadcasterName)
? (isSelfBroadcast ? "You" : string.Empty)
: (isSelfBroadcast ? $"{broadcasterName} (You)" : broadcasterName);
var (shell, broadcasterName) = listData[index];
ImGui.PushID(shell.Group.GID);
float rowHeight = 74f * ImGuiHelpers.GlobalScale;
@@ -239,7 +239,7 @@ public class SyncshellFinderUI : WindowMediatorSubscriberBase
var style = ImGui.GetStyle();
float startX = ImGui.GetCursorPosX();
float regionW = ImGui.GetContentRegionAvail().X;
float rightTxtW = ImGui.CalcTextSize(broadcasterLabel).X;
float rightTxtW = ImGui.CalcTextSize(broadcasterName).X;
_uiSharedService.MediumText(displayName, UIColors.Get("LightlessPurple"));
if (ImGui.IsItemHovered())
@@ -252,7 +252,7 @@ public class SyncshellFinderUI : WindowMediatorSubscriberBase
float rightX = startX + regionW - rightTxtW - style.ItemSpacing.X;
ImGui.SameLine();
ImGui.SetCursorPosX(rightX);
ImGui.TextUnformatted(broadcasterLabel);
ImGui.TextUnformatted(broadcasterName);
if (ImGui.IsItemHovered())
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);
ImGui.SetCursorPos(new Vector2(joinX, btnBaselineY));
DrawJoinButton(shell, isSelfBroadcast);
DrawJoinButton(shell);
float btnHeight = ImGui.GetFrameHeightWithSpacing();
float rowHeightUsed = MathF.Max(tagsHeight, btnHeight);
@@ -311,7 +311,7 @@ public class SyncshellFinderUI : WindowMediatorSubscriberBase
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;
var totalPages = (int)Math.Ceiling(cardData.Count / (float)shellsPerPage);
@@ -336,10 +336,7 @@ public class SyncshellFinderUI : WindowMediatorSubscriberBase
for (int index = firstIndex; index < lastExclusive; index++)
{
var localIndex = index - firstIndex;
var (shell, broadcasterName, isSelfBroadcast) = cardData[index];
var broadcasterLabel = string.IsNullOrEmpty(broadcasterName)
? (isSelfBroadcast ? "You" : string.Empty)
: (isSelfBroadcast ? $"{broadcasterName} (You)" : broadcasterName);
var (shell, broadcasterName) = cardData[index];
if (localIndex % 2 != 0)
ImGui.SameLine();
@@ -353,9 +350,9 @@ public class SyncshellFinderUI : WindowMediatorSubscriberBase
? shell.Group.Alias
: shell.Group.GID;
var style = ImGui.GetStyle();
float startX = ImGui.GetCursorPosX();
float availW = ImGui.GetContentRegionAvail().X;
float availWidth = ImGui.GetContentRegionAvail().X;
float rightTextW = ImGui.CalcTextSize(broadcasterName).X;
ImGui.BeginGroup();
@@ -367,45 +364,13 @@ public class SyncshellFinderUI : WindowMediatorSubscriberBase
Mediator.Publish(new GroupProfileOpenStandaloneMessage(shell.Group));
}
float nameRightX = ImGui.GetItemRectMax().X;
var regionMinScreen = ImGui.GetCursorScreenPos();
float regionRightX = regionMinScreen.X + availW;
float minBroadcasterX = nameRightX + style.ItemSpacing.X;
float maxBroadcasterWidth = regionRightX - minBroadcasterX;
string broadcasterToShow = broadcasterLabel;
if (!string.IsNullOrEmpty(broadcasterLabel) && maxBroadcasterWidth > 0f)
{
float bcFullWidth = ImGui.CalcTextSize(broadcasterLabel).X;
string toolTip;
if (bcFullWidth > maxBroadcasterWidth)
{
broadcasterToShow = TruncateTextToWidth(broadcasterLabel, maxBroadcasterWidth);
toolTip = broadcasterLabel + Environment.NewLine + Environment.NewLine + "Broadcaster of the syncshell.";
}
else
{
toolTip = "Broadcaster of the syncshell.";
}
float bcWidth = ImGui.CalcTextSize(broadcasterToShow).X;
float broadX = regionRightX - bcWidth;
broadX = MathF.Max(broadX, minBroadcasterX);
ImGui.SameLine();
var curPos = ImGui.GetCursorPos();
ImGui.SetCursorPos(new Vector2(broadX - regionMinScreen.X + startX, curPos.Y + 3f * ImGuiHelpers.GlobalScale));
ImGui.TextUnformatted(broadcasterToShow);
if (ImGui.IsItemHovered())
ImGui.SetTooltip(toolTip);
}
ImGui.SameLine();
float rightX = startX + availWidth - rightTextW;
var pos = ImGui.GetCursorPos();
ImGui.SetCursorPos(new Vector2(rightX, pos.Y + 3f * ImGuiHelpers.GlobalScale));
ImGui.TextUnformatted(broadcasterName);
if (ImGui.IsItemHovered())
ImGui.SetTooltip("Broadcaster of the syncshell.");
ImGui.EndGroup();
@@ -446,7 +411,7 @@ public class SyncshellFinderUI : WindowMediatorSubscriberBase
if (remainingY > 0)
ImGui.Dummy(new Vector2(0, remainingY));
DrawJoinButton(shell, isSelfBroadcast);
DrawJoinButton(shell);
ImGui.EndChild();
ImGui.EndGroup();
@@ -492,7 +457,7 @@ public class SyncshellFinderUI : WindowMediatorSubscriberBase
}
}
private void DrawJoinButton(GroupJoinDto shell, bool isSelfBroadcast)
private void DrawJoinButton(dynamic shell)
{
const string visibleLabel = "Join";
var label = $"{visibleLabel}##{shell.Group.GID}";
@@ -520,7 +485,7 @@ public class SyncshellFinderUI : WindowMediatorSubscriberBase
buttonSize = new Vector2(-1, 0);
}
if (!isAlreadyMember && !isRecentlyJoined && !isSelfBroadcast)
if (!isAlreadyMember && !isRecentlyJoined)
{
ImGui.PushStyleColor(ImGuiCol.Button, UIColors.Get("LightlessGreen"));
ImGui.PushStyleColor(ImGuiCol.ButtonHovered, UIColors.Get("LightlessGreen").WithAlpha(0.85f));
@@ -570,9 +535,7 @@ public class SyncshellFinderUI : WindowMediatorSubscriberBase
ImGui.Button(label, buttonSize);
}
UiSharedService.AttachToolTip(isSelfBroadcast
? "This is your own Syncshell."
: "Already a member or owner of this Syncshell.");
UiSharedService.AttachToolTip("Already a member or owner of this Syncshell.");
}
ImGui.PopStyleColor(3);
@@ -627,40 +590,6 @@ public class SyncshellFinderUI : WindowMediatorSubscriberBase
float widthUsed = cursorLocalX - baseLocal.X;
return (widthUsed, rowHeight);
}
private static string TruncateTextToWidth(string text, float maxWidth)
{
if (string.IsNullOrEmpty(text))
return text;
const string ellipsis = "...";
float ellipsisWidth = ImGui.CalcTextSize(ellipsis).X;
if (maxWidth <= ellipsisWidth)
return ellipsis;
int low = 0;
int high = text.Length;
string best = ellipsis;
while (low <= high)
{
int mid = (low + high) / 2;
string candidate = string.Concat(text.AsSpan(0, mid), ellipsis);
float width = ImGui.CalcTextSize(candidate).X;
if (width <= maxWidth)
{
best = candidate;
low = mid + 1;
}
else
{
high = mid - 1;
}
}
return best;
}
private IDalamudTextureWrap? GetIconWrap(uint iconId)
{

View File

@@ -440,7 +440,7 @@ public class TopTabMenu
try
{
var myCidHash = _dalamudUtilService.GetCID().ToString().GetHash256();
var myCidHash = (await _dalamudUtilService.GetCIDAsync().ConfigureAwait(false)).ToString().GetHash256();
await _apiController.TryPairWithContentId(request.HashedCid).ConfigureAwait(false);
_pairRequestService.RemoveRequest(request.HashedCid);
@@ -781,8 +781,7 @@ public class TopTabMenu
{
var buttonX = (availableWidth - (spacingX)) / 2f;
var lightFinderLabel = GetLightfinderFinderLabel();
if (_uiSharedService.IconTextButton(FontAwesomeIcon.PersonCirclePlus, lightFinderLabel, buttonX, center: true))
if (_uiSharedService.IconTextButton(FontAwesomeIcon.PersonCirclePlus, "Lightfinder", buttonX, center: true))
{
_lightlessMediator.Publish(new UiToggleMessage(typeof(LightFinderUI)));
}
@@ -796,28 +795,27 @@ public class TopTabMenu
}
}
private string GetLightfinderFinderLabel()
{
string label = "Lightfinder";
if (_lightFinderService.IsBroadcasting)
{
var hashExclude = _dalamudUtilService.GetCID().ToString().GetHash256();
var nearbyCount = _lightFinderScannerService.GetActiveBroadcasts(hashExclude).Count;
return $"{label} ({nearbyCount})";
}
return label;
}
private string GetSyncshellFinderLabel()
{
if (!_lightFinderService.IsBroadcasting)
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
.GetActiveSyncshellBroadcasts(excludeLocal: true)
.Where(b => !string.IsNullOrEmpty(b.GID))
.GetActiveSyncshellBroadcasts()
.Where(b =>
!string.IsNullOrEmpty(b.GID) &&
!string.Equals(b.HashedCID, myHashedCid, StringComparison.Ordinal))
.Select(b => b.GID!)
.Distinct(StringComparer.Ordinal)
.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.");
if (IconTextButton(FontAwesomeIcon.Ban, "Cancel Authentication"))
@@ -965,7 +962,7 @@ public partial class UiSharedService : DisposableMediatorSubscriberBase
_discordOAuthGetCode = null;
}
}
else
else if (_discordOAuthGetCode != null && _discordOAuthGetCode.IsCompleted)
{
TextWrapped("Discord OAuth is completed, status: ");
ImGui.SameLine();

File diff suppressed because it is too large Load Diff

View File

@@ -1,4 +1,10 @@
namespace LightlessSync.UtilsEnum.Enum
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace LightlessSync.UtilsEnum.Enum
{
public enum LabelAlignment
{

View File

@@ -1,8 +0,0 @@
namespace LightlessSync.UtilsEnum.Enum
{
public enum LightfinderLabelRenderer
{
Pictomancy,
SignatureHook,
}
}

View File

@@ -60,6 +60,16 @@ public static class VariousExtensions
CharacterData? oldData, ILogger logger, IPairPerformanceSubject cachedPlayer, bool forceApplyCustomization, bool forceApplyMods)
{
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>>();
foreach (ObjectKind objectKind in Enum.GetValues<ObjectKind>())
@@ -95,7 +105,7 @@ public static class VariousExtensions
{
var oldList = oldData.FileReplacements[objectKind];
var newList = newData.FileReplacements[objectKind];
var listsAreEqual = oldList.SequenceEqual(newList, PlayerData.Data.FileReplacementDataComparer.Instance);
var listsAreEqual = FileReplacementsEquivalent(oldList, newList);
if (!listsAreEqual || forceApplyMods)
{
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();
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();
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();
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();
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;
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);
charaDataToUpdate[objectKind].Add(PlayerChanges.ModManip);

File diff suppressed because it is too large Load Diff

View File

@@ -18,72 +18,56 @@ public class FileTransferOrchestrator : DisposableMediatorSubscriberBase
private readonly LightlessConfigService _lightlessConfig;
private readonly object _semaphoreModificationLock = new();
private readonly TokenProvider _tokenProvider;
private int _availableDownloadSlots;
private SemaphoreSlim _downloadSemaphore;
private int CurrentlyUsedDownloadSlots => _availableDownloadSlots - _downloadSemaphore.CurrentCount;
public FileTransferOrchestrator(
ILogger<FileTransferOrchestrator> logger,
LightlessConfigService lightlessConfig,
LightlessMediator mediator,
TokenProvider tokenProvider,
HttpClient httpClient) : base(logger, mediator)
public FileTransferOrchestrator(ILogger<FileTransferOrchestrator> logger, LightlessConfigService lightlessConfig,
LightlessMediator mediator, TokenProvider tokenProvider, HttpClient httpClient) : base(logger, mediator)
{
_lightlessConfig = lightlessConfig;
_tokenProvider = tokenProvider;
_httpClient = httpClient;
var ver = Assembly.GetExecutingAssembly().GetName().Version;
_httpClient.DefaultRequestHeaders.UserAgent.Add(
new ProductInfoHeaderValue("LightlessSync", $"{ver!.Major}.{ver.Minor}.{ver.Build}"));
_httpClient.DefaultRequestHeaders.UserAgent.Add(new ProductInfoHeaderValue("LightlessSync", ver!.Major + "." + ver!.Minor + "." + ver!.Build));
_availableDownloadSlots = Math.Max(1, lightlessConfig.Current.ParallelDownloads);
_downloadSemaphore = new SemaphoreSlim(_availableDownloadSlots, _availableDownloadSlots);
_availableDownloadSlots = lightlessConfig.Current.ParallelDownloads;
_downloadSemaphore = new(_availableDownloadSlots, _availableDownloadSlots);
Mediator.Subscribe<ConnectedMessage>(this, msg => FilesCdnUri = msg.Connection.ServerInfo.FileServerAddress);
Mediator.Subscribe<DisconnectedMessage>(this, _ => FilesCdnUri = null);
Mediator.Subscribe<DownloadReadyMessage>(this, msg => _downloadReady[msg.RequestId] = true);
Mediator.Subscribe<ConnectedMessage>(this, (msg) =>
{
FilesCdnUri = msg.Connection.ServerInfo.FileServerAddress;
});
Mediator.Subscribe<DisconnectedMessage>(this, (msg) =>
{
FilesCdnUri = null;
});
Mediator.Subscribe<DownloadReadyMessage>(this, (msg) =>
{
_downloadReady[msg.RequestId] = true;
});
}
/// <summary>
/// Files CDN Uri from server
/// </summary>
public Uri? FilesCdnUri { private set; get; }
/// <summary>
/// Forbidden file transfers given by server
/// </summary>
public List<FileTransfer> ForbiddenTransfers { get; } = [];
/// <summary>
/// Is the FileTransferOrchestrator initialized
/// </summary>
public bool IsInitialized => FilesCdnUri != null;
/// <summary>
/// Configured parallel downloads in settings (ParallelDownloads)
/// </summary>
public int ConfiguredParallelDownloads => Math.Max(1, _lightlessConfig.Current.ParallelDownloads);
public void ClearDownloadRequest(Guid guid)
{
_downloadReady.Remove(guid, out _);
}
/// <summary>
/// Clears the download request for the given guid
/// </summary>
/// <param name="guid">Guid of download request</param>
public void ClearDownloadRequest(Guid guid) => _downloadReady.Remove(guid, out _);
/// <summary>
/// Is the download ready for the given guid
/// </summary>
/// <param name="guid">Guid of download request</param>
/// <returns>Completion of the download</returns>
public bool IsDownloadReady(Guid guid)
=> _downloadReady.TryGetValue(guid, out bool isReady) && isReady;
{
if (_downloadReady.TryGetValue(guid, out bool isReady) && isReady)
{
return true;
}
return false;
}
/// <summary>
/// Release a download slot after download is complete
/// </summary>
public void ReleaseDownloadSlot()
{
try
@@ -97,26 +81,60 @@ public class FileTransferOrchestrator : DisposableMediatorSubscriberBase
}
}
/// <summary>
/// Wait for an available download slot asyncronously
/// </summary>
/// <param name="token">Cancellation Token</param>
/// <returns>Task of the slot</returns>
public async Task<HttpResponseMessage> SendRequestAsync(HttpMethod method, Uri uri,
CancellationToken? ct = null, HttpCompletionOption httpCompletionOption = HttpCompletionOption.ResponseContentRead,
bool withToken = true)
{
return await SendRequestInternalAsync(() => new HttpRequestMessage(method, uri),
ct, httpCompletionOption, withToken, allowRetry: true).ConfigureAwait(false);
}
public async Task<HttpResponseMessage> SendRequestAsync<T>(HttpMethod method, Uri uri, T content, CancellationToken ct,
bool withToken = true) where T : class
{
return await SendRequestInternalAsync(() =>
{
var requestMessage = new HttpRequestMessage(method, uri);
if (content is not ByteArrayContent byteArrayContent)
{
requestMessage.Content = JsonContent.Create(content);
}
else
{
var clonedContent = new ByteArrayContent(byteArrayContent.ReadAsByteArrayAsync().GetAwaiter().GetResult());
foreach (var header in byteArrayContent.Headers)
{
clonedContent.Headers.TryAddWithoutValidation(header.Key, header.Value);
}
requestMessage.Content = clonedContent;
}
return requestMessage;
}, ct, HttpCompletionOption.ResponseContentRead, withToken,
allowRetry: content is not HttpContent || content is ByteArrayContent).ConfigureAwait(false);
}
public async Task<HttpResponseMessage> SendRequestStreamAsync(HttpMethod method, Uri uri, ProgressableStreamContent content,
CancellationToken ct, bool withToken = true)
{
return await SendRequestInternalAsync(() =>
{
var requestMessage = new HttpRequestMessage(method, uri)
{
Content = content
};
return requestMessage;
}, ct, HttpCompletionOption.ResponseContentRead, withToken, allowRetry: false).ConfigureAwait(false);
}
public async Task WaitForDownloadSlotAsync(CancellationToken token)
{
lock (_semaphoreModificationLock)
{
var desired = Math.Max(1, _lightlessConfig.Current.ParallelDownloads);
if (_availableDownloadSlots != desired &&
_availableDownloadSlots == _downloadSemaphore.CurrentCount)
if (_availableDownloadSlots != _lightlessConfig.Current.ParallelDownloads && _availableDownloadSlots == _downloadSemaphore.CurrentCount)
{
_availableDownloadSlots = desired;
var old = _downloadSemaphore;
_downloadSemaphore = new SemaphoreSlim(_availableDownloadSlots, _availableDownloadSlots);
try { old.Dispose(); } catch { /* ignore */ }
_availableDownloadSlots = _lightlessConfig.Current.ParallelDownloads;
_downloadSemaphore = new(_availableDownloadSlots, _availableDownloadSlots);
}
}
@@ -124,15 +142,10 @@ public class FileTransferOrchestrator : DisposableMediatorSubscriberBase
Mediator.Publish(new DownloadLimitChangedMessage());
}
/// <summary>
/// Download limit per slot in bytes
/// </summary>
/// <returns>Bytes of the download limit</returns>
public long DownloadLimitPerSlot()
{
var limit = _lightlessConfig.Current.DownloadSpeedLimitInBytes;
if (limit <= 0) return 0;
limit = _lightlessConfig.Current.DownloadSpeedType switch
{
LightlessConfiguration.Models.DownloadSpeeds.Bps => limit,
@@ -140,113 +153,22 @@ public class FileTransferOrchestrator : DisposableMediatorSubscriberBase
LightlessConfiguration.Models.DownloadSpeeds.MBps => limit * 1024 * 1024,
_ => limit,
};
var usedSlots = CurrentlyUsedDownloadSlots;
var divided = limit / (usedSlots <= 0 ? 1 : usedSlots);
if (divided < 0)
var currentUsedDlSlots = CurrentlyUsedDownloadSlots;
var avaialble = _availableDownloadSlots;
var currentCount = _downloadSemaphore.CurrentCount;
var dividedLimit = limit / (currentUsedDlSlots == 0 ? 1 : currentUsedDlSlots);
if (dividedLimit < 0)
{
Logger.LogWarning(
"Calculated Bandwidth Limit is negative, returning Infinity: {value}, usedSlots={usedSlots}, limit={limit}, avail={avail}, currentCount={count}",
divided, usedSlots, limit, _availableDownloadSlots, _downloadSemaphore.CurrentCount);
Logger.LogWarning("Calculated Bandwidth Limit is negative, returning Infinity: {value}, CurrentlyUsedDownloadSlots is {currentSlots}, " +
"DownloadSpeedLimit is {limit}, available slots: {avail}, current count: {count}", dividedLimit, currentUsedDlSlots, limit, avaialble, currentCount);
return long.MaxValue;
}
return Math.Clamp(divided, 1, long.MaxValue);
return Math.Clamp(dividedLimit, 1, long.MaxValue);
}
/// <summary>
/// sends an HTTP request without content serialization
/// </summary>
/// <param name="method">HttpMethod for the request</param>
/// <param name="uri">Uri for the request</param>
/// <param name="ct">Cancellation Token</param>
/// <param name="httpCompletionOption">Enum of HttpCollectionOption</param>
/// <param name="withToken">Include Cancellation Token</param>
/// <returns>Http response of the request</returns>
public async Task<HttpResponseMessage> SendRequestAsync(
HttpMethod method,
Uri uri,
CancellationToken? ct = null,
HttpCompletionOption httpCompletionOption = HttpCompletionOption.ResponseContentRead,
bool withToken = true)
{
return await SendRequestInternalAsync(
() => new HttpRequestMessage(method, uri),
ct,
httpCompletionOption,
withToken,
allowRetry: true).ConfigureAwait(false);
}
/// <summary>
/// Sends an HTTP request with JSON content serialization
/// </summary>
/// <typeparam name="T">HttpResponseMessage</typeparam>
/// <param name="method">Http method</param>
/// <param name="uri">Url of the direct download link</param>
/// <param name="content">content of the request</param>
/// <param name="ct">cancellation token</param>
/// <param name="withToken">include cancellation token</param>
/// <returns></returns>
public async Task<HttpResponseMessage> SendRequestAsync<T>(
HttpMethod method,
Uri uri,
T content,
CancellationToken ct,
bool withToken = true) where T : class
{
return await SendRequestInternalAsync(() =>
{
var requestMessage = new HttpRequestMessage(method, uri);
if (content is ByteArrayContent byteArrayContent)
{
var bytes = byteArrayContent.ReadAsByteArrayAsync(ct).GetAwaiter().GetResult();
var cloned = new ByteArrayContent(bytes);
foreach (var header in byteArrayContent.Headers)
cloned.Headers.TryAddWithoutValidation(header.Key, header.Value);
requestMessage.Content = cloned;
}
else
{
requestMessage.Content = JsonContent.Create(content);
}
return requestMessage;
}, ct, HttpCompletionOption.ResponseContentRead, withToken,
allowRetry: content is not HttpContent || content is ByteArrayContent).ConfigureAwait(false);
}
public async Task<HttpResponseMessage> SendRequestStreamAsync(
HttpMethod method,
Uri uri,
ProgressableStreamContent content,
CancellationToken ct,
bool withToken = true)
{
return await SendRequestInternalAsync(() =>
{
return new HttpRequestMessage(method, uri) { Content = content };
}, ct, HttpCompletionOption.ResponseContentRead, withToken, allowRetry: false).ConfigureAwait(false);
}
/// <summary>
/// sends an HTTP request with optional retry logic for transient network errors
/// </summary>
/// <param name="requestFactory">Request factory</param>
/// <param name="ct">Cancellation Token</param>
/// <param name="httpCompletionOption">Http Options</param>
/// <param name="withToken">With cancellation token</param>
/// <param name="allowRetry">Allows retry of request</param>
/// <returns>Response message of request</returns>
private async Task<HttpResponseMessage> SendRequestInternalAsync(
Func<HttpRequestMessage> requestFactory,
CancellationToken? ct = null,
HttpCompletionOption httpCompletionOption = HttpCompletionOption.ResponseContentRead,
bool withToken = true,
bool allowRetry = true)
private async Task<HttpResponseMessage> SendRequestInternalAsync(Func<HttpRequestMessage> requestFactory,
CancellationToken? ct = null, HttpCompletionOption httpCompletionOption = HttpCompletionOption.ResponseContentRead,
bool withToken = true, bool allowRetry = true)
{
const int maxAttempts = 2;
var attempt = 0;
@@ -262,11 +184,8 @@ public class FileTransferOrchestrator : DisposableMediatorSubscriberBase
requestMessage.Headers.Authorization = new AuthenticationHeaderValue("Bearer", token);
}
if (requestMessage.Content != null &&
requestMessage.Content is not StreamContent &&
requestMessage.Content is not ByteArrayContent)
if (requestMessage.Content != null && requestMessage.Content is not StreamContent && requestMessage.Content is not ByteArrayContent)
{
// log content for debugging
var content = await ((JsonContent)requestMessage.Content).ReadAsStringAsync().ConfigureAwait(false);
Logger.LogDebug("Sending {method} to {uri} (Content: {content})", requestMessage.Method, requestMessage.RequestUri, content);
}
@@ -277,10 +196,9 @@ public class FileTransferOrchestrator : DisposableMediatorSubscriberBase
try
{
// send request
return ct != null
? await _httpClient.SendAsync(requestMessage, httpCompletionOption, ct.Value).ConfigureAwait(false)
: await _httpClient.SendAsync(requestMessage, httpCompletionOption).ConfigureAwait(false);
if (ct != null)
return await _httpClient.SendAsync(requestMessage, httpCompletionOption, ct.Value).ConfigureAwait(false);
return await _httpClient.SendAsync(requestMessage, httpCompletionOption).ConfigureAwait(false);
}
catch (TaskCanceledException)
{
@@ -290,11 +208,14 @@ public class FileTransferOrchestrator : DisposableMediatorSubscriberBase
{
Logger.LogWarning(ex, "Transient error during SendRequestInternal for {uri}, retrying attempt {attempt}/{maxAttempts}",
requestMessage.RequestUri, attempt, maxAttempts);
if (ct.HasValue)
{
await Task.Delay(TimeSpan.FromMilliseconds(200), ct.Value).ConfigureAwait(false);
}
else
{
await Task.Delay(TimeSpan.FromMilliseconds(200)).ConfigureAwait(false);
}
}
catch (Exception ex)
{
@@ -304,11 +225,6 @@ public class FileTransferOrchestrator : DisposableMediatorSubscriberBase
}
}
/// <summary>
/// Is the exception a transient network exception
/// </summary>
/// <param name="ex">expection</param>
/// <returns>Is transient network expection</returns>
private static bool IsTransientNetworkException(Exception ex)
{
var current = ex;
@@ -316,13 +232,12 @@ public class FileTransferOrchestrator : DisposableMediatorSubscriberBase
{
if (current is SocketException socketEx)
{
return socketEx.SocketErrorCode is
SocketError.ConnectionReset or
SocketError.ConnectionAborted or
SocketError.TimedOut;
return socketEx.SocketErrorCode is SocketError.ConnectionReset or SocketError.ConnectionAborted or SocketError.TimedOut;
}
current = current.InnerException;
}
return false;
}
}
}

View File

@@ -193,7 +193,7 @@ public partial class ApiController
CensusDataDto? censusDto = null;
if (_serverManager.SendCensusData && _lastCensus != null)
{
var world = _dalamudUtil.GetWorldId();
var world = await _dalamudUtil.GetWorldIdAsync().ConfigureAwait(false);
censusDto = new((ushort)world, _lastCensus.RaceId, _lastCensus.TribeId, _lastCensus.Gender);
Logger.LogDebug("Attaching Census Data: {data}", censusDto);
}

View File

@@ -418,7 +418,7 @@ public sealed partial class ApiController : DisposableMediatorSubscriberBase, IL
public Task CyclePauseAsync(PairUniqueIdentifier ident)
{
var timeoutCts = new CancellationTokenSource(TimeSpan.FromSeconds(5));
var timeoutCts = new CancellationTokenSource(TimeSpan.FromSeconds(8));
_ = Task.Run(async () =>
{
var token = timeoutCts.Token;
@@ -430,19 +430,20 @@ public sealed partial class ApiController : DisposableMediatorSubscriberBase, IL
return;
}
var targetPermissions = entry.SelfPermissions;
targetPermissions.SetPaused(paused: true);
var originalPermissions = entry.SelfPermissions;
var targetPermissions = originalPermissions;
targetPermissions.SetPaused(!originalPermissions.IsPaused());
await UserSetPairPermissions(new UserPermissionsDto(entry.User, targetPermissions)).ConfigureAwait(false);
var pauseApplied = false;
var applied = false;
while (!token.IsCancellationRequested)
{
if (_pairCoordinator.Ledger.TryGetEntry(ident, out var updated) && updated is not null)
{
if (updated.SelfPermissions == targetPermissions)
{
pauseApplied = true;
applied = true;
entry = updated;
break;
}
@@ -452,16 +453,13 @@ public sealed partial class ApiController : DisposableMediatorSubscriberBase, IL
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);
return;
}
targetPermissions.SetPaused(paused: false);
await UserSetPairPermissions(new UserPermissionsDto(entry.User, targetPermissions)).ConfigureAwait(false);
Logger.LogDebug("CyclePauseAsync completed pause cycle for {uid}", ident.UserId);
Logger.LogDebug("CyclePauseAsync toggled paused for {uid} to {state}", ident.UserId, targetPermissions.IsPaused());
}
catch (OperationCanceledException)
{
@@ -481,26 +479,16 @@ public sealed partial class ApiController : DisposableMediatorSubscriberBase, IL
}
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);
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;
}
var permissions = entry.SelfPermissions;
permissions.SetPaused(paused);
permissions.SetPaused(paused: true);
await UserSetPairPermissions(new UserPermissionsDto(userData, permissions)).ConfigureAwait(false);
}
@@ -544,8 +532,8 @@ public sealed partial class ApiController : DisposableMediatorSubscriberBase, IL
private void DalamudUtilOnLogIn()
{
var charaName = _dalamudUtil.GetPlayerName();
var worldId = _dalamudUtil.GetHomeWorldId();
var charaName = _dalamudUtil.GetPlayerNameAsync().GetAwaiter().GetResult();
var worldId = _dalamudUtil.GetHomeWorldIdAsync().GetAwaiter().GetResult();
var auth = _serverManager.CurrentServer.Authentications.Find(f => string.Equals(f.CharacterName, charaName, StringComparison.Ordinal) && f.WorldId == worldId);
if (auth?.AutoLogin ?? false)
{
@@ -653,7 +641,7 @@ public sealed partial class ApiController : DisposableMediatorSubscriberBase, IL
CensusDataDto? dto = null;
if (_serverManager.SendCensusData && _lastCensus != null)
{
var world = _dalamudUtil.GetWorldId();
var world = await _dalamudUtil.GetWorldIdAsync().ConfigureAwait(false);
dto = new((ushort)world, _lastCensus.RaceId, _lastCensus.TribeId, _lastCensus.Gender);
Logger.LogDebug("Attaching Census Data: {data}", dto);
}

View File

@@ -72,7 +72,7 @@ public sealed class TokenProvider : IDisposable, IMediatorSubscriber
result = await _httpClient.PostAsync(tokenUri, new FormUrlEncodedContent(
[
new KeyValuePair<string, string>("auth", auth),
new KeyValuePair<string, string>("charaIdent", _dalamudUtil.GetPlayerNameHashed()),
new KeyValuePair<string, string>("charaIdent", await _dalamudUtil.GetPlayerNameHashedAsync().ConfigureAwait(false)),
]), ct).ConfigureAwait(false);
}
else
@@ -152,7 +152,7 @@ public sealed class TokenProvider : IDisposable, IMediatorSubscriber
JwtIdentifier jwtIdentifier;
try
{
var playerIdentifier = _dalamudUtil.GetPlayerNameHashed();
var playerIdentifier = await _dalamudUtil.GetPlayerNameHashedAsync().ConfigureAwait(false);
if (string.IsNullOrEmpty(playerIdentifier))
{