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
125 changed files with 4552 additions and 24748 deletions

3
.gitignore vendored
View File

@@ -348,6 +348,3 @@ MigrationBackup/
# Ionide (cross platform F# VS Code tools) working folder # Ionide (cross platform F# VS Code tools) working folder
.ionide/ .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!!" subline: "LIGHTLESS IS EVOLVING!!"
changelog: 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" - name: "v2.0.0"
tagline: "Thank you for 4 months!" tagline: "Thank you for 4 months!"
date: "December 2025" date: "December 2025"
# be sure to set this every new version
isCurrent: true
versions: versions:
- number: "Lightless Chat" - number: "Lightless Chat"
icon: "" icon: ""

View File

@@ -6,7 +6,6 @@ using LightlessSync.Utils;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
using System.Collections.Concurrent; using System.Collections.Concurrent;
using System.Collections.Immutable; using System.Collections.Immutable;
using System.IO;
namespace LightlessSync.FileCache; namespace LightlessSync.FileCache;
@@ -22,7 +21,6 @@ public sealed class CacheMonitor : DisposableMediatorSubscriberBase
private CancellationTokenSource _scanCancellationTokenSource = new(); private CancellationTokenSource _scanCancellationTokenSource = new();
private readonly CancellationTokenSource _periodicCalculationTokenSource = new(); private readonly CancellationTokenSource _periodicCalculationTokenSource = new();
public static readonly IImmutableList<string> AllowedFileExtensions = [".mdl", ".tex", ".mtrl", ".tmb", ".pap", ".avfx", ".atex", ".sklb", ".eid", ".phyb", ".pbd", ".scd", ".skp", ".shpk", ".kdb"]; public static readonly IImmutableList<string> AllowedFileExtensions = [".mdl", ".tex", ".mtrl", ".tmb", ".pap", ".avfx", ".atex", ".sklb", ".eid", ".phyb", ".pbd", ".scd", ".skp", ".shpk", ".kdb"];
private static readonly HashSet<string> AllowedFileExtensionSet = new(AllowedFileExtensions, StringComparer.OrdinalIgnoreCase);
public CacheMonitor(ILogger<CacheMonitor> logger, IpcManager ipcManager, LightlessConfigService configService, public CacheMonitor(ILogger<CacheMonitor> logger, IpcManager ipcManager, LightlessConfigService configService,
FileCacheManager fileDbManager, LightlessMediator mediator, PerformanceCollectorService performanceCollector, DalamudUtilService dalamudUtil, FileCacheManager fileDbManager, LightlessMediator mediator, PerformanceCollectorService performanceCollector, DalamudUtilService dalamudUtil,
@@ -103,7 +101,6 @@ public sealed class CacheMonitor : DisposableMediatorSubscriberBase
} }
record WatcherChange(WatcherChangeTypes ChangeType, string? OldPath = null); record WatcherChange(WatcherChangeTypes ChangeType, string? OldPath = null);
private readonly record struct CacheEvictionCandidate(string FullPath, long Size, DateTime LastAccessTime);
private readonly Dictionary<string, WatcherChange> _watcherChanges = new(StringComparer.OrdinalIgnoreCase); private readonly Dictionary<string, WatcherChange> _watcherChanges = new(StringComparer.OrdinalIgnoreCase);
private readonly Dictionary<string, WatcherChange> _lightlessChanges = new(StringComparer.OrdinalIgnoreCase); private readonly Dictionary<string, WatcherChange> _lightlessChanges = new(StringComparer.OrdinalIgnoreCase);
@@ -166,7 +163,7 @@ public sealed class CacheMonitor : DisposableMediatorSubscriberBase
{ {
Logger.LogTrace("Lightless FSW: FileChanged: {change} => {path}", e.ChangeType, e.FullPath); Logger.LogTrace("Lightless FSW: FileChanged: {change} => {path}", e.ChangeType, e.FullPath);
if (!HasAllowedExtension(e.FullPath)) return; if (!AllowedFileExtensions.Any(ext => e.FullPath.EndsWith(ext, StringComparison.OrdinalIgnoreCase))) return;
lock (_watcherChanges) lock (_watcherChanges)
{ {
@@ -210,7 +207,7 @@ public sealed class CacheMonitor : DisposableMediatorSubscriberBase
private void Fs_Changed(object sender, FileSystemEventArgs e) private void Fs_Changed(object sender, FileSystemEventArgs e)
{ {
if (Directory.Exists(e.FullPath)) return; if (Directory.Exists(e.FullPath)) return;
if (!HasAllowedExtension(e.FullPath)) return; if (!AllowedFileExtensions.Any(ext => e.FullPath.EndsWith(ext, StringComparison.OrdinalIgnoreCase))) return;
if (e.ChangeType is not (WatcherChangeTypes.Changed or WatcherChangeTypes.Deleted or WatcherChangeTypes.Created)) if (e.ChangeType is not (WatcherChangeTypes.Changed or WatcherChangeTypes.Deleted or WatcherChangeTypes.Created))
return; return;
@@ -234,7 +231,7 @@ public sealed class CacheMonitor : DisposableMediatorSubscriberBase
{ {
foreach (var file in directoryFiles) foreach (var file in directoryFiles)
{ {
if (!HasAllowedExtension(file)) continue; if (!AllowedFileExtensions.Any(ext => file.EndsWith(ext, StringComparison.OrdinalIgnoreCase))) continue;
var oldPath = file.Replace(e.FullPath, e.OldFullPath, StringComparison.OrdinalIgnoreCase); var oldPath = file.Replace(e.FullPath, e.OldFullPath, StringComparison.OrdinalIgnoreCase);
_watcherChanges.Remove(oldPath); _watcherChanges.Remove(oldPath);
@@ -246,7 +243,7 @@ public sealed class CacheMonitor : DisposableMediatorSubscriberBase
} }
else else
{ {
if (!HasAllowedExtension(e.FullPath)) return; if (!AllowedFileExtensions.Any(ext => e.FullPath.EndsWith(ext, StringComparison.OrdinalIgnoreCase))) return;
lock (_watcherChanges) lock (_watcherChanges)
{ {
@@ -266,17 +263,6 @@ public sealed class CacheMonitor : DisposableMediatorSubscriberBase
public FileSystemWatcher? PenumbraWatcher { get; private set; } public FileSystemWatcher? PenumbraWatcher { get; private set; }
public FileSystemWatcher? LightlessWatcher { get; private set; } public FileSystemWatcher? LightlessWatcher { get; private set; }
private static bool HasAllowedExtension(string path)
{
if (string.IsNullOrEmpty(path))
{
return false;
}
var extension = Path.GetExtension(path);
return !string.IsNullOrEmpty(extension) && AllowedFileExtensionSet.Contains(extension);
}
private async Task LightlessWatcherExecution() private async Task LightlessWatcherExecution()
{ {
_lightlessFswCts = _lightlessFswCts.CancelRecreate(); _lightlessFswCts = _lightlessFswCts.CancelRecreate();
@@ -442,40 +428,116 @@ public sealed class CacheMonitor : DisposableMediatorSubscriberBase
Logger.LogWarning(ex, "Could not determine drive size for storage folder {folder}", _configService.Current.CacheFolder); Logger.LogWarning(ex, "Could not determine drive size for storage folder {folder}", _configService.Current.CacheFolder);
} }
var cacheFolder = _configService.Current.CacheFolder; var files = Directory.EnumerateFiles(_configService.Current.CacheFolder)
var candidates = new List<CacheEvictionCandidate>(); .Select(f => new FileInfo(f))
.OrderBy(f => f.LastAccessTime)
.ToList();
long totalSize = 0; long totalSize = 0;
totalSize += AddFolderCandidates(cacheFolder, candidates, token, isWine);
totalSize += AddFolderCandidates(Path.Combine(cacheFolder, "downscaled"), candidates, token, isWine); foreach (var f in files)
totalSize += AddFolderCandidates(Path.Combine(cacheFolder, "decimated"), candidates, token, isWine); {
token.ThrowIfCancellationRequested();
try
{
long size = 0;
if (!isWine)
{
try
{
size = _fileCompactor.GetFileSizeOnDisk(f);
}
catch (Exception ex)
{
Logger.LogTrace(ex, "GetFileSizeOnDisk failed for {file}, using fallback length", f.FullName);
size = f.Length;
}
}
else
{
size = f.Length;
}
totalSize += size;
}
catch (Exception ex)
{
Logger.LogTrace(ex, "Error getting size for {file}", f.FullName);
}
}
FileCacheSize = totalSize; FileCacheSize = totalSize;
if (Directory.Exists(_configService.Current.CacheFolder + "/downscaled"))
{
var filesDownscaled = Directory.EnumerateFiles(_configService.Current.CacheFolder + "/downscaled").Select(f => new FileInfo(f)).OrderBy(f => f.LastAccessTime).ToList();
long totalSizeDownscaled = 0;
foreach (var f in filesDownscaled)
{
token.ThrowIfCancellationRequested();
try
{
long size = 0;
if (!isWine)
{
try
{
size = _fileCompactor.GetFileSizeOnDisk(f);
}
catch (Exception ex)
{
Logger.LogTrace(ex, "GetFileSizeOnDisk failed for {file}, using fallback length", f.FullName);
size = f.Length;
}
}
else
{
size = f.Length;
}
totalSizeDownscaled += size;
}
catch (Exception ex)
{
Logger.LogTrace(ex, "Error getting size for {file}", f.FullName);
}
}
FileCacheSize = (totalSize + totalSizeDownscaled);
}
else
{
FileCacheSize = totalSize;
}
var maxCacheInBytes = (long)(_configService.Current.MaxLocalCacheInGiB * 1024d * 1024d * 1024d); var maxCacheInBytes = (long)(_configService.Current.MaxLocalCacheInGiB * 1024d * 1024d * 1024d);
if (FileCacheSize < maxCacheInBytes) if (FileCacheSize < maxCacheInBytes)
return; return;
var maxCacheBuffer = maxCacheInBytes * 0.05d; var maxCacheBuffer = maxCacheInBytes * 0.05d;
candidates.Sort(static (a, b) => a.LastAccessTime.CompareTo(b.LastAccessTime)); while (FileCacheSize > maxCacheInBytes - (long)maxCacheBuffer && files.Count > 0)
var evictionTarget = maxCacheInBytes - (long)maxCacheBuffer;
var index = 0;
while (FileCacheSize > evictionTarget && index < candidates.Count)
{ {
var oldestFile = candidates[index]; var oldestFile = files[0];
try try
{ {
EvictCacheCandidate(oldestFile, cacheFolder); long fileSize = oldestFile.Length;
FileCacheSize -= oldestFile.Size; File.Delete(oldestFile.FullName);
FileCacheSize -= fileSize;
} }
catch (Exception ex) catch (Exception ex)
{ {
Logger.LogTrace(ex, "Failed to delete old file {file}", oldestFile.FullPath); Logger.LogTrace(ex, "Failed to delete old file {file}", oldestFile.FullName);
} }
index++; files.RemoveAt(0);
} }
} }
@@ -484,114 +546,6 @@ public sealed class CacheMonitor : DisposableMediatorSubscriberBase
HaltScanLocks.Clear(); HaltScanLocks.Clear();
} }
private long AddFolderCandidates(string directory, List<CacheEvictionCandidate> candidates, CancellationToken token, bool isWine)
{
if (!Directory.Exists(directory))
{
return 0;
}
long totalSize = 0;
foreach (var path in Directory.EnumerateFiles(directory))
{
token.ThrowIfCancellationRequested();
try
{
var file = new FileInfo(path);
var size = GetFileSizeOnDisk(file, isWine);
totalSize += size;
candidates.Add(new CacheEvictionCandidate(file.FullName, size, file.LastAccessTime));
}
catch (Exception ex)
{
Logger.LogTrace(ex, "Error getting size for {file}", path);
}
}
return totalSize;
}
private long GetFileSizeOnDisk(FileInfo file, bool isWine)
{
if (isWine)
{
return file.Length;
}
try
{
return _fileCompactor.GetFileSizeOnDisk(file);
}
catch (Exception ex)
{
Logger.LogTrace(ex, "GetFileSizeOnDisk failed for {file}, using fallback length", file.FullName);
return file.Length;
}
}
private void EvictCacheCandidate(CacheEvictionCandidate candidate, string cacheFolder)
{
if (TryGetCacheHashAndPrefixedPath(candidate.FullPath, cacheFolder, out var hash, out var prefixedPath))
{
_fileDbManager.RemoveHashedFile(hash, prefixedPath);
}
try
{
if (File.Exists(candidate.FullPath))
{
File.Delete(candidate.FullPath);
}
}
catch (Exception ex)
{
Logger.LogTrace(ex, "Failed to delete old file {file}", candidate.FullPath);
}
}
private static bool TryGetCacheHashAndPrefixedPath(string filePath, string cacheFolder, out string hash, out string prefixedPath)
{
hash = string.Empty;
prefixedPath = string.Empty;
if (string.IsNullOrEmpty(cacheFolder))
{
return false;
}
var fileName = Path.GetFileNameWithoutExtension(filePath);
if (string.IsNullOrEmpty(fileName) || !IsSha1Hash(fileName))
{
return false;
}
var relative = Path.GetRelativePath(cacheFolder, filePath)
.Replace(Path.AltDirectorySeparatorChar, Path.DirectorySeparatorChar);
var sanitizedRelative = relative.TrimStart(Path.DirectorySeparatorChar);
prefixedPath = Path.Combine(FileCacheManager.CachePrefix, sanitizedRelative);
hash = fileName;
return true;
}
private static bool IsSha1Hash(string value)
{
if (value.Length != 40)
{
return false;
}
foreach (var ch in value)
{
if (!Uri.IsHexDigit(ch))
{
return false;
}
}
return true;
}
public void ResumeScan(string source) public void ResumeScan(string source)
{ {
if (!HaltScanLocks.ContainsKey(source)) HaltScanLocks[source] = 0; if (!HaltScanLocks.ContainsKey(source)) HaltScanLocks[source] = 0;
@@ -652,7 +606,7 @@ public sealed class CacheMonitor : DisposableMediatorSubscriberBase
[ [
.. Directory.GetFiles(folder, "*.*", SearchOption.AllDirectories) .. Directory.GetFiles(folder, "*.*", SearchOption.AllDirectories)
.AsParallel() .AsParallel()
.Where(f => HasAllowedExtension(f) .Where(f => AllowedFileExtensions.Any(e => f.EndsWith(e, StringComparison.OrdinalIgnoreCase))
&& !f.Contains(@"\bg\", StringComparison.OrdinalIgnoreCase) && !f.Contains(@"\bg\", StringComparison.OrdinalIgnoreCase)
&& !f.Contains(@"\bgcommon\", StringComparison.OrdinalIgnoreCase) && !f.Contains(@"\bgcommon\", StringComparison.OrdinalIgnoreCase)
&& !f.Contains(@"\ui\", StringComparison.OrdinalIgnoreCase)), && !f.Contains(@"\ui\", StringComparison.OrdinalIgnoreCase)),

View File

@@ -1,23 +1,15 @@
#nullable disable #nullable disable
using System.Text.Json.Serialization;
namespace LightlessSync.FileCache; namespace LightlessSync.FileCache;
public class FileCacheEntity public class FileCacheEntity
{ {
[JsonConstructor] public FileCacheEntity(string hash, string path, string lastModifiedDateTicks, long? size = null, long? compressedSize = null)
public FileCacheEntity(
string hash,
string prefixedFilePath,
string lastModifiedDateTicks,
long? size = null,
long? compressedSize = null)
{ {
Size = size; Size = size;
CompressedSize = compressedSize; CompressedSize = compressedSize;
Hash = hash; Hash = hash;
PrefixedFilePath = prefixedFilePath; PrefixedFilePath = path;
LastModifiedDateTicks = lastModifiedDateTicks; LastModifiedDateTicks = lastModifiedDateTicks;
} }
@@ -31,5 +23,7 @@ public class FileCacheEntity
public long? Size { get; set; } public long? Size { get; set; }
public void SetResolvedFilePath(string filePath) 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 Microsoft.Extensions.Logging;
using System.Collections.Concurrent; using System.Collections.Concurrent;
using System.Globalization; using System.Globalization;
using System.IO;
using System.Runtime.InteropServices;
using System.Text; using System.Text;
namespace LightlessSync.FileCache; namespace LightlessSync.FileCache;
@@ -27,21 +25,12 @@ public sealed class FileCacheManager : IHostedService
private readonly ConcurrentDictionary<string, ConcurrentDictionary<string, FileCacheEntity>> _fileCaches = new(StringComparer.Ordinal); private readonly ConcurrentDictionary<string, ConcurrentDictionary<string, FileCacheEntity>> _fileCaches = new(StringComparer.Ordinal);
private readonly ConcurrentDictionary<string, FileCacheEntity> _fileCachesByPrefixedPath = new(StringComparer.OrdinalIgnoreCase); private readonly ConcurrentDictionary<string, FileCacheEntity> _fileCachesByPrefixedPath = new(StringComparer.OrdinalIgnoreCase);
private readonly SemaphoreSlim _getCachesByPathsSemaphore = new(1, 1); private readonly SemaphoreSlim _getCachesByPathsSemaphore = new(1, 1);
private readonly SemaphoreSlim _evictSemaphore = new(1, 1);
private readonly Lock _fileWriteLock = new(); private readonly Lock _fileWriteLock = new();
private readonly IpcManager _ipcManager; private readonly IpcManager _ipcManager;
private readonly ILogger<FileCacheManager> _logger; private readonly ILogger<FileCacheManager> _logger;
private bool _csvHeaderEnsured; private bool _csvHeaderEnsured;
public string CacheFolder => _configService.Current.CacheFolder; 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) public FileCacheManager(ILogger<FileCacheManager> logger, IpcManager ipcManager, LightlessConfigService configService, LightlessMediator lightlessMediator)
{ {
_logger = logger; _logger = logger;
@@ -56,18 +45,6 @@ public sealed class FileCacheManager : IHostedService
private static string NormalizeSeparators(string path) => path.Replace("/", "\\", StringComparison.Ordinal) private static string NormalizeSeparators(string path) => path.Replace("/", "\\", StringComparison.Ordinal)
.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) private static string NormalizePrefixedPathKey(string prefixedPath)
{ {
if (string.IsNullOrEmpty(prefixedPath)) if (string.IsNullOrEmpty(prefixedPath))
@@ -134,124 +111,6 @@ public sealed class FileCacheManager : IHostedService
return int.TryParse(versionSpan, NumberStyles.Integer, CultureInfo.InvariantCulture, out version); 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";
try
{
await File.WriteAllBytesAsync(tmpPath, compressed, token).ConfigureAwait(false);
File.Move(tmpPath, compressedPath, overwrite: true);
}
finally
{
try { if (File.Exists(tmpPath)) File.Delete(tmpPath); } catch { /* ignore */ }
}
var compressedSize = new FileInfo(compressedPath).Length;
SetSizeInfo(hash, originalSize, compressedSize);
UpdateEntitiesSizes(hash, originalSize, compressedSize);
var maxBytes = GiBToBytes(_configService.Current.MaxLocalCacheInGiB);
await EnforceCacheLimitAsync(maxBytes, token).ConfigureAwait(false);
return compressed;
}
finally
{
sem.Release();
}
}
private string NormalizeToPrefixedPath(string path) private string NormalizeToPrefixedPath(string path)
{ {
if (string.IsNullOrEmpty(path)) return string.Empty; if (string.IsNullOrEmpty(path)) return string.Empty;
@@ -291,26 +150,6 @@ public sealed class FileCacheManager : IHostedService
return CreateFileEntity(cacheFolder, CachePrefix, fi); return CreateFileEntity(cacheFolder, CachePrefix, fi);
} }
public FileCacheEntity? CreateCacheEntryWithKnownHash(string path, string hash)
{
if (string.IsNullOrWhiteSpace(hash))
{
return CreateCacheEntry(path);
}
FileInfo fi = new(path);
if (!fi.Exists) return null;
_logger.LogTrace("Creating cache entry for {path} using provided hash", path);
var cacheFolder = _configService.Current.CacheFolder;
if (string.IsNullOrEmpty(cacheFolder)) return null;
if (!TryBuildPrefixedPath(fi.FullName, cacheFolder, CachePrefix, out var prefixedPath, out _))
{
return null;
}
return CreateFileCacheEntity(fi, prefixedPath, hash);
}
public FileCacheEntity? CreateFileEntry(string path) public FileCacheEntity? CreateFileEntry(string path)
{ {
FileInfo fi = new(path); FileInfo fi = new(path);
@@ -479,18 +318,9 @@ public sealed class FileCacheManager : IHostedService
public async Task<(string, byte[])> GetCompressedFileData(string fileHash, CancellationToken uploadToken) 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 fileCache = GetFileCacheByHash(fileHash)!.ResolvedFilepath;
var raw = await File.ReadAllBytesAsync(fileCache, uploadToken).ConfigureAwait(false); return (fileHash, LZ4Wrapper.WrapHC(await File.ReadAllBytesAsync(fileCache, uploadToken).ConfigureAwait(false), 0,
var compressed = LZ4Wrapper.WrapHC(raw, 0, raw.Length); (int)new FileInfo(fileCache).Length));
UpdateSizeInfo(fileHash, original: raw.LongLength, compressed: compressed.LongLength);
return (fileHash, compressed);
} }
public FileCacheEntity? GetFileCacheByHash(string hash) public FileCacheEntity? GetFileCacheByHash(string hash)
@@ -593,10 +423,9 @@ public sealed class FileCacheManager : IHostedService
} }
} }
public void RemoveHashedFile(string hash, string prefixedFilePath, bool removeDerivedFiles = true) public void RemoveHashedFile(string hash, string prefixedFilePath)
{ {
var normalizedPath = NormalizePrefixedPathKey(prefixedFilePath); var normalizedPath = NormalizePrefixedPathKey(prefixedFilePath);
var removedHash = false;
if (_fileCaches.TryGetValue(hash, out var caches)) if (_fileCaches.TryGetValue(hash, out var caches))
{ {
@@ -609,16 +438,11 @@ public sealed class FileCacheManager : IHostedService
if (caches.IsEmpty) if (caches.IsEmpty)
{ {
removedHash = _fileCaches.TryRemove(hash, out _); _fileCaches.TryRemove(hash, out _);
} }
} }
_fileCachesByPrefixedPath.TryRemove(normalizedPath, out _); _fileCachesByPrefixedPath.TryRemove(normalizedPath, out _);
if (removeDerivedFiles && removedHash)
{
RemoveDerivedCacheFiles(hash);
}
} }
public void UpdateHashedFile(FileCacheEntity fileCache, bool computeProperties = true) public void UpdateHashedFile(FileCacheEntity fileCache, bool computeProperties = true)
@@ -634,8 +458,7 @@ public sealed class FileCacheManager : IHostedService
fileCache.Hash = Crypto.ComputeFileHash(fileCache.ResolvedFilepath, Crypto.HashAlgo.Sha1); fileCache.Hash = Crypto.ComputeFileHash(fileCache.ResolvedFilepath, Crypto.HashAlgo.Sha1);
fileCache.LastModifiedDateTicks = fi.LastWriteTimeUtc.Ticks.ToString(CultureInfo.InvariantCulture); fileCache.LastModifiedDateTicks = fi.LastWriteTimeUtc.Ticks.ToString(CultureInfo.InvariantCulture);
} }
var removeDerivedFiles = !string.Equals(oldHash, fileCache.Hash, StringComparison.OrdinalIgnoreCase); RemoveHashedFile(oldHash, prefixedPath);
RemoveHashedFile(oldHash, prefixedPath, removeDerivedFiles);
AddHashedFile(fileCache); AddHashedFile(fileCache);
} }
@@ -785,7 +608,7 @@ public sealed class FileCacheManager : IHostedService
{ {
try try
{ {
RemoveHashedFile(fileCache.Hash, fileCache.PrefixedFilePath, removeDerivedFiles: false); RemoveHashedFile(fileCache.Hash, fileCache.PrefixedFilePath);
var extensionPath = fileCache.ResolvedFilepath.ToUpper(CultureInfo.InvariantCulture) + "." + ext; var extensionPath = fileCache.ResolvedFilepath.ToUpper(CultureInfo.InvariantCulture) + "." + ext;
File.Move(fileCache.ResolvedFilepath, extensionPath, overwrite: true); File.Move(fileCache.ResolvedFilepath, extensionPath, overwrite: true);
var newHashedEntity = new FileCacheEntity(fileCache.Hash, fileCache.PrefixedFilePath + "." + ext, DateTime.UtcNow.Ticks.ToString(CultureInfo.InvariantCulture)); var newHashedEntity = new FileCacheEntity(fileCache.Hash, fileCache.PrefixedFilePath + "." + ext, DateTime.UtcNow.Ticks.ToString(CultureInfo.InvariantCulture));
@@ -802,33 +625,6 @@ public sealed class FileCacheManager : IHostedService
} }
} }
private void RemoveDerivedCacheFiles(string hash)
{
var cacheFolder = _configService.Current.CacheFolder;
if (string.IsNullOrWhiteSpace(cacheFolder))
{
return;
}
TryDeleteDerivedCacheFile(Path.Combine(cacheFolder, "downscaled", $"{hash}.tex"));
TryDeleteDerivedCacheFile(Path.Combine(cacheFolder, "decimated", $"{hash}.mdl"));
}
private void TryDeleteDerivedCacheFile(string path)
{
try
{
if (File.Exists(path))
{
File.Delete(path);
}
}
catch (Exception ex)
{
_logger.LogTrace(ex, "Failed to delete derived cache file {path}", path);
}
}
private void AddHashedFile(FileCacheEntity fileCache) private void AddHashedFile(FileCacheEntity fileCache)
{ {
var normalizedPath = NormalizePrefixedPathKey(fileCache.PrefixedFilePath); var normalizedPath = NormalizePrefixedPathKey(fileCache.PrefixedFilePath);
@@ -942,83 +738,6 @@ public sealed class FileCacheManager : IHostedService
}, token).ConfigureAwait(false); }, token).ConfigureAwait(false);
} }
private async Task EnforceCacheLimitAsync(long maxBytes, CancellationToken token)
{
if (string.IsNullOrWhiteSpace(CacheFolder) || maxBytes <= 0) return;
await _evictSemaphore.WaitAsync(token).ConfigureAwait(false);
try
{
Directory.CreateDirectory(CacheFolder);
foreach (var tmp in Directory.EnumerateFiles(CacheFolder, "*" + _compressedCacheExtension + ".tmp"))
{
try { File.Delete(tmp); } catch { /* ignore */ }
}
var files = Directory.EnumerateFiles(CacheFolder, "*" + _compressedCacheExtension, SearchOption.TopDirectoryOnly)
.Select(p => new FileInfo(p))
.Where(fi => fi.Exists)
.OrderBy(fi => fi.LastWriteTimeUtc)
.ToList();
long total = files.Sum(f => f.Length);
if (total <= maxBytes) return;
foreach (var fi in files)
{
token.ThrowIfCancellationRequested();
if (total <= maxBytes) break;
var hash = Path.GetFileNameWithoutExtension(fi.Name);
try
{
var len = fi.Length;
fi.Delete();
total -= len;
_sizeCache.TryRemove(hash, out _);
}
catch (Exception ex)
{
_logger.LogWarning(ex, "Failed to evict cache file {file}", fi.FullName);
}
}
}
finally
{
_evictSemaphore.Release();
}
}
private static long GiBToBytes(double gib)
{
if (double.IsNaN(gib) || double.IsInfinity(gib) || gib <= 0)
return 0;
var bytes = gib * 1024d * 1024d * 1024d;
if (bytes >= long.MaxValue) return long.MaxValue;
return (long)Math.Round(bytes, MidpointRounding.AwayFromZero);
}
private void CleanupOrphanCompressedCache()
{
if (string.IsNullOrWhiteSpace(CacheFolder) || !Directory.Exists(CacheFolder))
return;
foreach (var path in Directory.EnumerateFiles(CacheFolder, "*" + _compressedCacheExtension))
{
var hash = Path.GetFileNameWithoutExtension(path);
if (!_fileCaches.ContainsKey(hash))
{
try { File.Delete(path); }
catch (Exception ex) { _logger.LogWarning(ex, "Failed deleting orphan {file}", path); }
}
}
}
public async Task StartAsync(CancellationToken cancellationToken) public async Task StartAsync(CancellationToken cancellationToken)
{ {
_logger.LogInformation("Starting FileCacheManager"); _logger.LogInformation("Starting FileCacheManager");
@@ -1172,14 +891,6 @@ public sealed class FileCacheManager : IHostedService
compressed = resultCompressed; 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))); AddHashedFile(ReplacePathPrefixes(new FileCacheEntity(hash, path, time, size, compressed)));
} }
catch (Exception ex) catch (Exception ex)
@@ -1202,8 +913,6 @@ public sealed class FileCacheManager : IHostedService
{ {
await WriteOutFullCsvAsync(cancellationToken).ConfigureAwait(false); await WriteOutFullCsvAsync(cancellationToken).ConfigureAwait(false);
} }
CleanupOrphanCompressedCache();
} }
_logger.LogInformation("Started FileCacheManager"); _logger.LogInformation("Started FileCacheManager");

View File

@@ -10,6 +10,9 @@ using LightlessSync.Services.Mediator;
using LightlessSync.Utils; using LightlessSync.Utils;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
using System.Collections.Concurrent; using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Threading.Tasks;
using System.Linq;
using DalamudObjectKind = Dalamud.Game.ClientState.Objects.Enums.ObjectKind; using DalamudObjectKind = Dalamud.Game.ClientState.Objects.Enums.ObjectKind;
namespace LightlessSync.FileCache; namespace LightlessSync.FileCache;
@@ -25,10 +28,7 @@ public sealed class TransientResourceManager : DisposableMediatorSubscriberBase
private readonly object _ownedHandlerLock = new(); private readonly object _ownedHandlerLock = new();
private readonly string[] _handledFileTypes = ["tmb", "pap", "avfx", "atex", "sklb", "eid", "phyb", "scd", "skp", "shpk", "kdb"]; 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[] _handledRecordingFileTypes = ["tex", "mdl", "mtrl"];
private readonly string[] _handledFileTypesWithRecording;
private readonly HashSet<GameObjectHandler> _playerRelatedPointers = []; 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 readonly Dictionary<nint, GameObjectHandler> _ownedHandlers = new();
private ConcurrentDictionary<nint, ObjectKind> _cachedFrameAddresses = new(); private ConcurrentDictionary<nint, ObjectKind> _cachedFrameAddresses = new();
private ConcurrentDictionary<ObjectKind, HashSet<string>>? _semiTransientResources = null; private ConcurrentDictionary<ObjectKind, HashSet<string>>? _semiTransientResources = null;
@@ -42,7 +42,6 @@ public sealed class TransientResourceManager : DisposableMediatorSubscriberBase
_dalamudUtil = dalamudUtil; _dalamudUtil = dalamudUtil;
_actorObjectService = actorObjectService; _actorObjectService = actorObjectService;
_gameObjectHandlerFactory = gameObjectHandlerFactory; _gameObjectHandlerFactory = gameObjectHandlerFactory;
_handledFileTypesWithRecording = _handledRecordingFileTypes.Concat(_handledFileTypes).ToArray();
Mediator.Subscribe<PenumbraResourceLoadMessage>(this, Manager_PenumbraResourceLoadEvent); Mediator.Subscribe<PenumbraResourceLoadMessage>(this, Manager_PenumbraResourceLoadEvent);
Mediator.Subscribe<ActorTrackedMessage>(this, msg => HandleActorTracked(msg.Descriptor)); Mediator.Subscribe<ActorTrackedMessage>(this, msg => HandleActorTracked(msg.Descriptor));
@@ -52,21 +51,15 @@ public sealed class TransientResourceManager : DisposableMediatorSubscriberBase
Mediator.Subscribe<GameObjectHandlerCreatedMessage>(this, (msg) => Mediator.Subscribe<GameObjectHandlerCreatedMessage>(this, (msg) =>
{ {
if (!msg.OwnedObject) return; if (!msg.OwnedObject) return;
lock (_playerRelatedLock) _playerRelatedPointers.Add(msg.GameObjectHandler);
{
_playerRelatedPointers.Add(msg.GameObjectHandler);
}
}); });
Mediator.Subscribe<GameObjectHandlerDestroyedMessage>(this, (msg) => Mediator.Subscribe<GameObjectHandlerDestroyedMessage>(this, (msg) =>
{ {
if (!msg.OwnedObject) return; 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); 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 private ConcurrentDictionary<ObjectKind, HashSet<string>> SemiTransientResources
{ {
get get
@@ -94,12 +87,9 @@ public sealed class TransientResourceManager : DisposableMediatorSubscriberBase
{ {
_semiTransientResources = new(); _semiTransientResources = new();
PlayerConfig.JobSpecificCache.TryGetValue(_dalamudUtil.ClassJobId, out var jobSpecificData); PlayerConfig.JobSpecificCache.TryGetValue(_dalamudUtil.ClassJobId, out var jobSpecificData);
_semiTransientResources[ObjectKind.Player] = PlayerConfig.GlobalPersistentCache.Concat(jobSpecificData ?? []) _semiTransientResources[ObjectKind.Player] = PlayerConfig.GlobalPersistentCache.Concat(jobSpecificData ?? []).ToHashSet(StringComparer.Ordinal);
.ToHashSet(StringComparer.OrdinalIgnoreCase);
PlayerConfig.JobSpecificPetCache.TryGetValue(_dalamudUtil.ClassJobId, out var petSpecificData); PlayerConfig.JobSpecificPetCache.TryGetValue(_dalamudUtil.ClassJobId, out var petSpecificData);
_semiTransientResources[ObjectKind.Pet] = new HashSet<string>( _semiTransientResources[ObjectKind.Pet] = [.. petSpecificData ?? []];
petSpecificData ?? [],
StringComparer.OrdinalIgnoreCase);
} }
return _semiTransientResources; return _semiTransientResources;
@@ -137,14 +127,14 @@ public sealed class TransientResourceManager : DisposableMediatorSubscriberBase
{ {
SemiTransientResources.TryGetValue(objectKind, out var result); 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) public void PersistTransientResources(ObjectKind objectKind)
{ {
if (!SemiTransientResources.TryGetValue(objectKind, out HashSet<string>? semiTransientResources)) 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)) if (!TransientResources.TryGetValue(objectKind, out var resources))
@@ -162,7 +152,7 @@ public sealed class TransientResourceManager : DisposableMediatorSubscriberBase
List<string> newlyAddedGamePaths; List<string> newlyAddedGamePaths;
lock (semiTransientResources) lock (semiTransientResources)
{ {
newlyAddedGamePaths = transientResources.Except(semiTransientResources, StringComparer.OrdinalIgnoreCase).ToList(); newlyAddedGamePaths = transientResources.Except(semiTransientResources, StringComparer.Ordinal).ToList();
foreach (var gamePath in transientResources) foreach (var gamePath in transientResources)
{ {
semiTransientResources.Add(gamePath); semiTransientResources.Add(gamePath);
@@ -207,13 +197,12 @@ public sealed class TransientResourceManager : DisposableMediatorSubscriberBase
public void RemoveTransientResource(ObjectKind objectKind, string path) public void RemoveTransientResource(ObjectKind objectKind, string path)
{ {
var normalizedPath = NormalizeGamePath(path);
if (SemiTransientResources.TryGetValue(objectKind, out var resources)) if (SemiTransientResources.TryGetValue(objectKind, out var resources))
{ {
resources.Remove(normalizedPath); resources.RemoveWhere(f => string.Equals(path, f, StringComparison.Ordinal));
if (objectKind == ObjectKind.Player) if (objectKind == ObjectKind.Player)
{ {
PlayerConfig.RemovePath(normalizedPath, objectKind); PlayerConfig.RemovePath(path, objectKind);
Logger.LogTrace("Saving transient.json from {method}", nameof(RemoveTransientResource)); Logger.LogTrace("Saving transient.json from {method}", nameof(RemoveTransientResource));
_configurationService.Save(); _configurationService.Save();
} }
@@ -222,17 +211,16 @@ public sealed class TransientResourceManager : DisposableMediatorSubscriberBase
internal bool AddTransientResource(ObjectKind objectKind, string item) internal bool AddTransientResource(ObjectKind objectKind, string item)
{ {
var normalizedItem = NormalizeGamePath(item); if (SemiTransientResources.TryGetValue(objectKind, out var semiTransient) && semiTransient != null && semiTransient.Contains(item))
if (SemiTransientResources.TryGetValue(objectKind, out var semiTransient) && semiTransient != null && semiTransient.Contains(normalizedItem))
return false; return false;
if (!TransientResources.TryGetValue(objectKind, out HashSet<string>? transientResource)) if (!TransientResources.TryGetValue(objectKind, out HashSet<string>? transientResource))
{ {
transientResource = new HashSet<string>(StringComparer.OrdinalIgnoreCase); transientResource = new HashSet<string>(StringComparer.Ordinal);
TransientResources[objectKind] = transientResource; TransientResources[objectKind] = transientResource;
} }
return transientResource.Add(normalizedItem); return transientResource.Add(item.ToLowerInvariant());
} }
internal void ClearTransientPaths(ObjectKind objectKind, List<string> list) internal void ClearTransientPaths(ObjectKind objectKind, List<string> list)
@@ -297,73 +285,47 @@ public sealed class TransientResourceManager : DisposableMediatorSubscriberBase
private void DalamudUtil_FrameworkUpdate() private void DalamudUtil_FrameworkUpdate()
{ {
_ = Task.Run(() => RefreshPlayerRelatedAddressMap());
lock (_cacheAdditionLock) lock (_cacheAdditionLock)
{ {
_cachedHandledPaths.Clear(); _cachedHandledPaths.Clear();
} }
if (_lastClassJobId != _dalamudUtil.ClassJobId) var activeDescriptors = new Dictionary<nint, ObjectKind>();
foreach (var descriptor in _actorObjectService.PlayerDescriptors)
{ {
UpdateClassJobCache(); if (TryResolveObjectKind(descriptor, out var resolvedKind))
}
CleanupAbsentObjects();
}
private void RefreshPlayerRelatedAddressMap()
{
var tempMap = new ConcurrentDictionary<nint, GameObjectHandler>();
var updatedFrameAddresses = new ConcurrentDictionary<nint, ObjectKind>();
lock (_playerRelatedLock)
{
foreach (var handler in _playerRelatedPointers)
{ {
var address = (nint)handler.Address; activeDescriptors[descriptor.Address] = resolvedKind;
if (address != nint.Zero)
{
tempMap[address] = handler;
updatedFrameAddresses[address] = handler.ObjectKind;
}
} }
} }
_playerRelatedByAddress.Clear(); foreach (var address in _cachedFrameAddresses.Keys.ToList())
foreach (var kvp in tempMap)
{ {
_playerRelatedByAddress[kvp.Key] = kvp.Value; if (!activeDescriptors.ContainsKey(address))
{
_cachedFrameAddresses.TryRemove(address, out _);
}
} }
_cachedFrameAddresses.Clear(); foreach (var descriptor in activeDescriptors)
foreach (var kvp in updatedFrameAddresses)
{ {
_cachedFrameAddresses[kvp.Key] = kvp.Value; _cachedFrameAddresses[descriptor.Key] = descriptor.Value;
}
}
private void UpdateClassJobCache()
{
_lastClassJobId = _dalamudUtil.ClassJobId;
if (SemiTransientResources.TryGetValue(ObjectKind.Pet, out HashSet<string>? value))
{
value?.Clear();
} }
PlayerConfig.JobSpecificCache.TryGetValue(_dalamudUtil.ClassJobId, out var jobSpecificData); if (_lastClassJobId != _dalamudUtil.ClassJobId)
SemiTransientResources[ObjectKind.Player] = PlayerConfig.GlobalPersistentCache {
.Concat(jobSpecificData ?? []) _lastClassJobId = _dalamudUtil.ClassJobId;
.ToHashSet(StringComparer.OrdinalIgnoreCase); if (SemiTransientResources.TryGetValue(ObjectKind.Pet, out HashSet<string>? value))
{
value?.Clear();
}
PlayerConfig.JobSpecificPetCache.TryGetValue(_dalamudUtil.ClassJobId, out var petSpecificData); PlayerConfig.JobSpecificCache.TryGetValue(_dalamudUtil.ClassJobId, out var jobSpecificData);
SemiTransientResources[ObjectKind.Pet] = new HashSet<string>( SemiTransientResources[ObjectKind.Player] = PlayerConfig.GlobalPersistentCache.Concat(jobSpecificData ?? []).ToHashSet(StringComparer.OrdinalIgnoreCase);
petSpecificData ?? [], PlayerConfig.JobSpecificPetCache.TryGetValue(_dalamudUtil.ClassJobId, out var petSpecificData);
StringComparer.OrdinalIgnoreCase); SemiTransientResources[ObjectKind.Pet] = [.. petSpecificData ?? []];
} }
private void CleanupAbsentObjects()
{
foreach (var kind in Enum.GetValues(typeof(ObjectKind)).Cast<ObjectKind>()) foreach (var kind in Enum.GetValues(typeof(ObjectKind)).Cast<ObjectKind>())
{ {
if (!_cachedFrameAddresses.Any(k => k.Value == kind) && TransientResources.Remove(kind, out _)) if (!_cachedFrameAddresses.Any(k => k.Value == kind) && TransientResources.Remove(kind, out _))
@@ -378,12 +340,9 @@ public sealed class TransientResourceManager : DisposableMediatorSubscriberBase
_ = Task.Run(() => _ = Task.Run(() =>
{ {
Logger.LogDebug("Penumbra Mod Settings changed, verifying SemiTransientResources"); 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));
}
} }
}); });
} }
@@ -393,20 +352,38 @@ public sealed class TransientResourceManager : DisposableMediatorSubscriberBase
_semiTransientResources = null; _semiTransientResources = null;
} }
private static bool TryResolveObjectKind(ActorObjectService.ActorDescriptor descriptor, out ObjectKind resolvedKind)
{
if (descriptor.OwnedKind is ObjectKind ownedKind)
{
resolvedKind = ownedKind;
return true;
}
if (descriptor.ObjectKind == DalamudObjectKind.Player)
{
resolvedKind = ObjectKind.Player;
return true;
}
resolvedKind = default;
return false;
}
private void HandleActorTracked(ActorObjectService.ActorDescriptor descriptor) private void HandleActorTracked(ActorObjectService.ActorDescriptor descriptor)
{ {
if (descriptor.IsInGpose) if (!TryResolveObjectKind(descriptor, out var resolvedKind))
return;
if (descriptor.OwnedKind is not ObjectKind ownedKind)
return; return;
if (Logger.IsEnabled(LogLevel.Debug)) 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) lock (_ownedHandlerLock)
{ {
@@ -485,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) private void Manager_PenumbraResourceLoadEvent(PenumbraResourceLoadMessage msg)
{ {
var gamePath = msg.GamePath.ToLowerInvariant();
var gameObjectAddress = msg.GameObject; var gameObjectAddress = msg.GameObject;
if (!_cachedFrameAddresses.TryGetValue(gameObjectAddress, out var objectKind)) var filePath = msg.FilePath;
{
if (_actorObjectService.TryGetOwnedKind(gameObjectAddress, out var ownedKind))
{
objectKind = ownedKind;
}
else
{
return;
}
}
var gamePath = NormalizeGamePath(msg.GamePath);
if (string.IsNullOrEmpty(gamePath))
{
return;
}
// ignore files already processed this frame // ignore files already processed this frame
if (_cachedHandledPaths.Contains(gamePath)) return;
lock (_cacheAdditionLock) lock (_cacheAdditionLock)
{ {
if (!_cachedHandledPaths.Add(gamePath)) _cachedHandledPaths.Add(gamePath);
{ }
return;
} // 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 // ignore files to not handle
var handledTypes = IsTransientRecording ? _handledFileTypesWithRecording : _handledFileTypes; var handledTypes = IsTransientRecording ? _handledRecordingFileTypes.Concat(_handledFileTypes) : _handledFileTypes;
if (!HasHandledFileType(gamePath, handledTypes)) if (!handledTypes.Any(type => gamePath.EndsWith(type, StringComparison.OrdinalIgnoreCase)))
{ {
lock (_cacheAdditionLock)
{
_cachedHandledPaths.Add(gamePath);
}
return; return;
} }
var filePath = NormalizeFilePath(msg.FilePath); // ignore files not belonging to anything player related
if (!_cachedFrameAddresses.TryGetValue(gameObjectAddress, out var objectKind))
// ignore files that are the same
if (string.Equals(filePath, gamePath, StringComparison.Ordinal))
{ {
lock (_cacheAdditionLock)
{
_cachedHandledPaths.Add(gamePath);
}
return; return;
} }
@@ -574,15 +520,15 @@ public sealed class TransientResourceManager : DisposableMediatorSubscriberBase
TransientResources[objectKind] = transientResources; TransientResources[objectKind] = transientResources;
} }
_playerRelatedByAddress.TryGetValue(gameObjectAddress, out var owner); var owner = _playerRelatedPointers.FirstOrDefault(f => f.Address == gameObjectAddress);
bool alreadyTransient = false; bool alreadyTransient = false;
bool transientContains = transientResources.Contains(gamePath); bool transientContains = transientResources.Contains(replacedGamePath);
bool semiTransientContains = SemiTransientResources.Values.Any(value => value.Contains(gamePath)); bool semiTransientContains = SemiTransientResources.SelectMany(k => k.Value).Any(f => string.Equals(f, gamePath, StringComparison.OrdinalIgnoreCase));
if (transientContains || semiTransientContains) if (transientContains || semiTransientContains)
{ {
if (!IsTransientRecording) 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); transientContains, semiTransientContains);
alreadyTransient = true; alreadyTransient = true;
} }
@@ -590,10 +536,10 @@ public sealed class TransientResourceManager : DisposableMediatorSubscriberBase
{ {
if (!IsTransientRecording) if (!IsTransientRecording)
{ {
bool isAdded = transientResources.Add(gamePath); bool isAdded = transientResources.Add(replacedGamePath);
if (isAdded) 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); SendTransients(gameObjectAddress, objectKind);
} }
} }
@@ -601,7 +547,7 @@ public sealed class TransientResourceManager : DisposableMediatorSubscriberBase
if (owner != null && IsTransientRecording) if (owner != null && IsTransientRecording)
{ {
_recordedTransients.Add(new TransientRecord(owner, gamePath, filePath, alreadyTransient) { AddTransient = !alreadyTransient }); _recordedTransients.Add(new TransientRecord(owner, replacedGamePath, filePath, alreadyTransient) { AddTransient = !alreadyTransient });
} }
} }
@@ -673,7 +619,7 @@ public sealed class TransientResourceManager : DisposableMediatorSubscriberBase
if (!item.AddTransient || item.AlreadyTransient) continue; if (!item.AddTransient || item.AlreadyTransient) continue;
if (!TransientResources.TryGetValue(item.Owner.ObjectKind, out var transient)) 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); Logger.LogTrace("Adding recorded: {gamePath} => {filePath}", item.GamePath, item.FilePath);

View File

@@ -1,5 +1,4 @@
using Dalamud.Plugin.Services; using Dalamud.Plugin.Services;
using Dalamud.Game.ClientState.Objects.SubKinds;
using FFXIVClientStructs.FFXIV.Client.Game.Character; using FFXIVClientStructs.FFXIV.Client.Game.Character;
using FFXIVClientStructs.FFXIV.Client.UI.Info; using FFXIVClientStructs.FFXIV.Client.UI.Info;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
@@ -12,35 +11,24 @@ public unsafe class BlockedCharacterHandler
private readonly Dictionary<CharaData, bool> _blockedCharacterCache = new(); private readonly Dictionary<CharaData, bool> _blockedCharacterCache = new();
private readonly ILogger<BlockedCharacterHandler> _logger; private readonly ILogger<BlockedCharacterHandler> _logger;
private readonly IObjectTable _objectTable;
public BlockedCharacterHandler(ILogger<BlockedCharacterHandler> logger, IGameInteropProvider gameInteropProvider, IObjectTable objectTable) public BlockedCharacterHandler(ILogger<BlockedCharacterHandler> logger, IGameInteropProvider gameInteropProvider)
{ {
gameInteropProvider.InitializeFromAttributes(this); gameInteropProvider.InitializeFromAttributes(this);
_logger = logger; _logger = logger;
_objectTable = objectTable;
} }
private CharaData? TryGetIdsFromPlayerPointer(nint ptr, ushort objectIndex) private static CharaData GetIdsFromPlayerPointer(nint ptr)
{ {
if (ptr == nint.Zero || objectIndex >= 200) if (ptr == nint.Zero) return new(0, 0);
return null; var castChar = ((BattleChara*)ptr);
var obj = _objectTable[objectIndex];
if (obj is not IPlayerCharacter player || player.Address != ptr)
return null;
var castChar = (BattleChara*)player.Address;
return new(castChar->Character.AccountId, castChar->Character.ContentId); return new(castChar->Character.AccountId, castChar->Character.ContentId);
} }
public bool IsCharacterBlocked(nint ptr, ushort objectIndex, out bool firstTime) public bool IsCharacterBlocked(nint ptr, out bool firstTime)
{ {
firstTime = false; firstTime = false;
var combined = TryGetIdsFromPlayerPointer(ptr, objectIndex); var combined = GetIdsFromPlayerPointer(ptr);
if (combined == null)
return false;
if (_blockedCharacterCache.TryGetValue(combined, out var isBlocked)) if (_blockedCharacterCache.TryGetValue(combined, out var isBlocked))
return isBlocked; return isBlocked;

View File

@@ -1,11 +0,0 @@
namespace Lifestream.Enums;
public enum ResidentialAetheryteKind
{
None = -1,
Uldah = 9,
Gridania = 2,
Limsa = 8,
Foundation = 70,
Kugane = 111,
}

View File

@@ -1 +0,0 @@
global using AddressBookEntryTuple = (string Name, int World, int City, int Ward, int PropertyType, int Plot, int Apartment, bool ApartmentSubdivision, bool AliasEnabled, string Alias);

View File

@@ -1,129 +0,0 @@
using Dalamud.Plugin;
using Dalamud.Plugin.Ipc;
using Lifestream.Enums;
using LightlessSync.Interop.Ipc.Framework;
using LightlessSync.Services.Mediator;
using Microsoft.Extensions.Logging;
namespace LightlessSync.Interop.Ipc;
public sealed class IpcCallerLifestream : IpcServiceBase
{
private static readonly IpcServiceDescriptor LifestreamDescriptor = new("Lifestream", "Lifestream", new Version(0, 0, 0, 0));
private readonly ICallGateSubscriber<string, object> _executeLifestreamCommand;
private readonly ICallGateSubscriber<AddressBookEntryTuple, bool> _isHere;
private readonly ICallGateSubscriber<AddressBookEntryTuple, object> _goToHousingAddress;
private readonly ICallGateSubscriber<bool> _isBusy;
private readonly ICallGateSubscriber<object> _abort;
private readonly ICallGateSubscriber<string, bool> _changeWorld;
private readonly ICallGateSubscriber<uint, bool> _changeWorldById;
private readonly ICallGateSubscriber<string, bool> _aetheryteTeleport;
private readonly ICallGateSubscriber<uint, bool> _aetheryteTeleportById;
private readonly ICallGateSubscriber<bool> _canChangeInstance;
private readonly ICallGateSubscriber<int> _getCurrentInstance;
private readonly ICallGateSubscriber<int> _getNumberOfInstances;
private readonly ICallGateSubscriber<int, object> _changeInstance;
private readonly ICallGateSubscriber<(ResidentialAetheryteKind, int, int)> _getCurrentPlotInfo;
public IpcCallerLifestream(IDalamudPluginInterface pi, LightlessMediator lightlessMediator, ILogger<IpcCallerLifestream> logger)
: base(logger, lightlessMediator, pi, LifestreamDescriptor)
{
_executeLifestreamCommand = pi.GetIpcSubscriber<string, object>("Lifestream.ExecuteCommand");
_isHere = pi.GetIpcSubscriber<AddressBookEntryTuple, bool>("Lifestream.IsHere");
_goToHousingAddress = pi.GetIpcSubscriber<AddressBookEntryTuple, object>("Lifestream.GoToHousingAddress");
_isBusy = pi.GetIpcSubscriber<bool>("Lifestream.IsBusy");
_abort = pi.GetIpcSubscriber<object>("Lifestream.Abort");
_changeWorld = pi.GetIpcSubscriber<string, bool>("Lifestream.ChangeWorld");
_changeWorldById = pi.GetIpcSubscriber<uint, bool>("Lifestream.ChangeWorldById");
_aetheryteTeleport = pi.GetIpcSubscriber<string, bool>("Lifestream.AetheryteTeleport");
_aetheryteTeleportById = pi.GetIpcSubscriber<uint, bool>("Lifestream.AetheryteTeleportById");
_canChangeInstance = pi.GetIpcSubscriber<bool>("Lifestream.CanChangeInstance");
_getCurrentInstance = pi.GetIpcSubscriber<int>("Lifestream.GetCurrentInstance");
_getNumberOfInstances = pi.GetIpcSubscriber<int>("Lifestream.GetNumberOfInstances");
_changeInstance = pi.GetIpcSubscriber<int, object>("Lifestream.ChangeInstance");
_getCurrentPlotInfo = pi.GetIpcSubscriber<(ResidentialAetheryteKind, int, int)>("Lifestream.GetCurrentPlotInfo");
CheckAPI();
}
public void ExecuteLifestreamCommand(string command)
{
if (!APIAvailable) return;
_executeLifestreamCommand.InvokeAction(command);
}
public bool IsHere(AddressBookEntryTuple entry)
{
if (!APIAvailable) return false;
return _isHere.InvokeFunc(entry);
}
public void GoToHousingAddress(AddressBookEntryTuple entry)
{
if (!APIAvailable) return;
_goToHousingAddress.InvokeAction(entry);
}
public bool IsBusy()
{
if (!APIAvailable) return false;
return _isBusy.InvokeFunc();
}
public void Abort()
{
if (!APIAvailable) return;
_abort.InvokeAction();
}
public bool ChangeWorld(string worldName)
{
if (!APIAvailable) return false;
return _changeWorld.InvokeFunc(worldName);
}
public bool AetheryteTeleport(string aetheryteName)
{
if (!APIAvailable) return false;
return _aetheryteTeleport.InvokeFunc(aetheryteName);
}
public bool ChangeWorldById(uint worldId)
{
if (!APIAvailable) return false;
return _changeWorldById.InvokeFunc(worldId);
}
public bool AetheryteTeleportById(uint aetheryteId)
{
if (!APIAvailable) return false;
return _aetheryteTeleportById.InvokeFunc(aetheryteId);
}
public bool CanChangeInstance()
{
if (!APIAvailable) return false;
return _canChangeInstance.InvokeFunc();
}
public int GetCurrentInstance()
{
if (!APIAvailable) return -1;
return _getCurrentInstance.InvokeFunc();
}
public int GetNumberOfInstances()
{
if (!APIAvailable) return -1;
return _getNumberOfInstances.InvokeFunc();
}
public void ChangeInstance(int instanceNumber)
{
if (!APIAvailable) return;
_changeInstance.InvokeAction(instanceNumber);
}
public (ResidentialAetheryteKind, int, int)? GetCurrentPlotInfo()
{
if (!APIAvailable) return (ResidentialAetheryteKind.None, -1, -1);
return _getCurrentPlotInfo.InvokeFunc();
}
}

View File

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

View File

@@ -95,12 +95,6 @@ public sealed class IpcCallerPenumbra : IpcServiceBase
public Task<(string[] forward, string[][] reverse)> ResolvePathsAsync(string[] forward, string[] reverse) public Task<(string[] forward, string[][] reverse)> ResolvePathsAsync(string[] forward, string[] reverse)
=> _resources.ResolvePathsAsync(forward, 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) public Task RedrawAsync(ILogger logger, GameObjectHandler handler, Guid applicationId, CancellationToken token)
=> _redraw.RedrawAsync(logger, handler, applicationId, token); => _redraw.RedrawAsync(logger, handler, applicationId, token);
@@ -177,6 +171,11 @@ public sealed class IpcCallerPenumbra : IpcServiceBase
}); });
Mediator.Subscribe<DalamudLoginMessage>(this, _ => _shownPenumbraUnavailable = false); 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() private void HandlePenumbraInitialized()

View File

@@ -5,12 +5,9 @@ namespace LightlessSync.Interop.Ipc;
public sealed partial class IpcManager : DisposableMediatorSubscriberBase public sealed partial class IpcManager : DisposableMediatorSubscriberBase
{ {
private bool _wasInitialized;
public IpcManager(ILogger<IpcManager> logger, LightlessMediator mediator, public IpcManager(ILogger<IpcManager> logger, LightlessMediator mediator,
IpcCallerPenumbra penumbraIpc, IpcCallerGlamourer glamourerIpc, IpcCallerCustomize customizeIpc, IpcCallerHeels heelsIpc, IpcCallerPenumbra penumbraIpc, IpcCallerGlamourer glamourerIpc, IpcCallerCustomize customizeIpc, IpcCallerHeels heelsIpc,
IpcCallerHonorific honorificIpc, IpcCallerMoodles moodlesIpc, IpcCallerPetNames ipcCallerPetNames, IpcCallerBrio ipcCallerBrio, IpcCallerHonorific honorificIpc, IpcCallerMoodles moodlesIpc, IpcCallerPetNames ipcCallerPetNames, IpcCallerBrio ipcCallerBrio) : base(logger, mediator)
IpcCallerLifestream ipcCallerLifestream) : base(logger, mediator)
{ {
CustomizePlus = customizeIpc; CustomizePlus = customizeIpc;
Heels = heelsIpc; Heels = heelsIpc;
@@ -20,10 +17,8 @@ public sealed partial class IpcManager : DisposableMediatorSubscriberBase
Moodles = moodlesIpc; Moodles = moodlesIpc;
PetNames = ipcCallerPetNames; PetNames = ipcCallerPetNames;
Brio = ipcCallerBrio; Brio = ipcCallerBrio;
Lifestream = ipcCallerLifestream;
_wasInitialized = Initialized; if (Initialized)
if (_wasInitialized)
{ {
Mediator.Publish(new PenumbraInitializedMessage()); Mediator.Publish(new PenumbraInitializedMessage());
} }
@@ -49,8 +44,8 @@ public sealed partial class IpcManager : DisposableMediatorSubscriberBase
public IpcCallerPenumbra Penumbra { get; } public IpcCallerPenumbra Penumbra { get; }
public IpcCallerMoodles Moodles { get; } public IpcCallerMoodles Moodles { get; }
public IpcCallerPetNames PetNames { get; } public IpcCallerPetNames PetNames { get; }
public IpcCallerBrio Brio { get; } public IpcCallerBrio Brio { get; }
public IpcCallerLifestream Lifestream { get; }
private void PeriodicApiStateCheck() private void PeriodicApiStateCheck()
{ {
@@ -63,14 +58,5 @@ public sealed partial class IpcManager : DisposableMediatorSubscriberBase
Moodles.CheckAPI(); Moodles.CheckAPI();
PetNames.CheckAPI(); PetNames.CheckAPI();
Brio.CheckAPI(); Brio.CheckAPI();
var initialized = Initialized;
if (initialized && !_wasInitialized)
{
Mediator.Publish(new PenumbraInitializedMessage());
}
_wasInitialized = initialized;
Lifestream.CheckAPI();
} }
} }

View File

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

View File

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

View File

@@ -19,27 +19,6 @@ public class ConfigurationMigrator(ILogger<ConfigurationMigrator> logger, Transi
transientConfigService.Save(); 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) if (serverConfigService.Current.Version == 1)
{ {
_logger.LogInformation("Migrating Server Config V1 => V2"); _logger.LogInformation("Migrating Server Config V1 => V2");

View File

@@ -10,11 +10,7 @@ public sealed class ChatConfig : ILightlessConfiguration
public bool AutoEnableChatOnLogin { get; set; } = false; public bool AutoEnableChatOnLogin { get; set; } = false;
public bool ShowRulesOverlayOnOpen { get; set; } = true; public bool ShowRulesOverlayOnOpen { get; set; } = true;
public bool ShowMessageTimestamps { get; set; } = true; public bool ShowMessageTimestamps { get; set; } = true;
public bool ShowNotesInSyncshellChat { get; set; } = true;
public bool EnableAnimatedEmotes { get; set; } = true;
public float ChatWindowOpacity { get; set; } = .97f; 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 IsWindowPinned { get; set; } = false;
public bool AutoOpenChatOnPluginLoad { get; set; } = false; public bool AutoOpenChatOnPluginLoad { get; set; } = false;
public float ChatFontScale { get; set; } = 1.0f; public float ChatFontScale { get; set; } = 1.0f;

View File

@@ -4,7 +4,6 @@ using LightlessSync.LightlessConfiguration.Models;
using LightlessSync.UI; using LightlessSync.UI;
using LightlessSync.UI.Models; using LightlessSync.UI.Models;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
using LightlessSync.PlayerData.Factories;
namespace LightlessSync.LightlessConfiguration.Configurations; namespace LightlessSync.LightlessConfiguration.Configurations;
@@ -50,9 +49,7 @@ public class LightlessConfig : ILightlessConfiguration
public int DownloadSpeedLimitInBytes { get; set; } = 0; public int DownloadSpeedLimitInBytes { get; set; } = 0;
public DownloadSpeeds DownloadSpeedType { get; set; } = DownloadSpeeds.MBps; public DownloadSpeeds DownloadSpeedType { get; set; } = DownloadSpeeds.MBps;
public bool PreferNotesOverNamesForVisible { get; set; } = false; public bool PreferNotesOverNamesForVisible { get; set; } = false;
public VisiblePairSortMode VisiblePairSortMode { get; set; } = VisiblePairSortMode.Alphabetical; public VisiblePairSortMode VisiblePairSortMode { get; set; } = VisiblePairSortMode.Default;
public OnlinePairSortMode OnlinePairSortMode { get; set; } = OnlinePairSortMode.Alphabetical;
public TextureFormatSortMode TextureFormatSortMode { get; set; } = TextureFormatSortMode.None;
public float ProfileDelay { get; set; } = 1.5f; public float ProfileDelay { get; set; } = 1.5f;
public bool ProfilePopoutRight { get; set; } = false; public bool ProfilePopoutRight { get; set; } = false;
public bool ProfilesAllowNsfw { get; set; } = false; public bool ProfilesAllowNsfw { get; set; } = false;
@@ -142,7 +139,6 @@ public class LightlessConfig : ILightlessConfiguration
public bool useColoredUIDs { get; set; } = true; public bool useColoredUIDs { get; set; } = true;
public bool BroadcastEnabled { get; set; } = false; public bool BroadcastEnabled { get; set; } = false;
public bool LightfinderAutoEnableOnConnect { get; set; } = false; public bool LightfinderAutoEnableOnConnect { get; set; } = false;
public LightfinderLabelRenderer LightfinderLabelRenderer { get; set; } = LightfinderLabelRenderer.Pictomancy;
public short LightfinderLabelOffsetX { get; set; } = 0; public short LightfinderLabelOffsetX { get; set; } = 0;
public short LightfinderLabelOffsetY { get; set; } = 0; public short LightfinderLabelOffsetY { get; set; } = 0;
public bool LightfinderLabelUseIcon { get; set; } = false; public bool LightfinderLabelUseIcon { get; set; } = false;
@@ -157,10 +153,4 @@ public class LightlessConfig : ILightlessConfiguration
public bool SyncshellFinderEnabled { get; set; } = false; public bool SyncshellFinderEnabled { get; set; } = false;
public string? SelectedFinderSyncshell { get; set; } = null; public string? SelectedFinderSyncshell { get; set; } = null;
public string LastSeenVersion { get; set; } = string.Empty; public string LastSeenVersion { get; set; } = string.Empty;
public bool EnableParticleEffects { get; set; } = true;
public HashSet<Guid> OrphanableTempCollections { get; set; } = [];
public AnimationValidationMode AnimationValidationMode { get; set; } = AnimationValidationMode.Safe;
public bool AnimationAllowOneBasedShift { get; set; } = true;
public bool AnimationAllowNeighborIndexTolerance { get; set; } = false;
} }

View File

@@ -22,15 +22,4 @@ public class PlayerPerformanceConfig : ILightlessConfiguration
public int TextureDownscaleMaxDimension { get; set; } = 2048; public int TextureDownscaleMaxDimension { get; set; } = 2048;
public bool OnlyDownscaleUncompressedTextures { get; set; } = true; public bool OnlyDownscaleUncompressedTextures { get; set; } = true;
public bool KeepOriginalTextureFiles { get; set; } = false; public bool KeepOriginalTextureFiles { get; set; } = false;
public bool SkipTextureDownscaleForPreferredPairs { get; set; } = true;
public bool EnableModelDecimation { get; set; } = false;
public int ModelDecimationTriangleThreshold { get; set; } = 20_000;
public double ModelDecimationTargetRatio { get; set; } = 0.8;
public bool KeepOriginalModelFiles { get; set; } = true;
public bool SkipModelDecimationForPreferredPairs { get; set; } = true;
public bool ModelDecimationAllowBody { get; set; } = false;
public bool ModelDecimationAllowFaceHead { get; set; } = false;
public bool ModelDecimationAllowTail { get; set; } = false;
public bool ModelDecimationAllowClothing { get; set; } = true;
public bool ModelDecimationAllowAccessories { get; set; } = true;
} }

View File

@@ -5,7 +5,7 @@ namespace LightlessSync.LightlessConfiguration.Configurations;
public class TransientConfig : ILightlessConfiguration public class TransientConfig : ILightlessConfiguration
{ {
public Dictionary<string, TransientPlayerConfig> TransientConfigs { get; set; } = []; public Dictionary<string, TransientPlayerConfig> TransientConfigs { get; set; } = [];
public int Version { get; set; } = 2; public int Version { get; set; } = 1;
public class TransientPlayerConfig 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

@@ -5,7 +5,6 @@ namespace LightlessSync.LightlessConfiguration.Configurations;
public class XivDataStorageConfig : ILightlessConfiguration public class XivDataStorageConfig : ILightlessConfiguration
{ {
public ConcurrentDictionary<string, long> TriangleDictionary { get; set; } = new(StringComparer.OrdinalIgnoreCase); public ConcurrentDictionary<string, long> TriangleDictionary { get; set; } = new(StringComparer.OrdinalIgnoreCase);
public ConcurrentDictionary<string, long> EffectiveTriangleDictionary { get; set; } = new(StringComparer.OrdinalIgnoreCase);
public ConcurrentDictionary<string, Dictionary<string, List<ushort>>> BonesDictionary { get; set; } = new(StringComparer.OrdinalIgnoreCase); public ConcurrentDictionary<string, Dictionary<string, List<ushort>>> BonesDictionary { get; set; } = new(StringComparer.OrdinalIgnoreCase);
public int Version { get; set; } = 0; public int Version { get; set; } = 0;
} }

View File

@@ -74,7 +74,6 @@ public class LightlessPlugin : MediatorSubscriberBase, IHostedService
private readonly DalamudUtilService _dalamudUtil; private readonly DalamudUtilService _dalamudUtil;
private readonly LightlessConfigService _lightlessConfigService; private readonly LightlessConfigService _lightlessConfigService;
private readonly ServerConfigurationManager _serverConfigurationManager; private readonly ServerConfigurationManager _serverConfigurationManager;
private readonly PairHandlerRegistry _pairHandlerRegistry;
private readonly IServiceScopeFactory _serviceScopeFactory; private readonly IServiceScopeFactory _serviceScopeFactory;
private IServiceScope? _runtimeServiceScope; private IServiceScope? _runtimeServiceScope;
private Task? _launchTask = null; private Task? _launchTask = null;
@@ -82,13 +81,11 @@ public class LightlessPlugin : MediatorSubscriberBase, IHostedService
public LightlessPlugin(ILogger<LightlessPlugin> logger, LightlessConfigService lightlessConfigService, public LightlessPlugin(ILogger<LightlessPlugin> logger, LightlessConfigService lightlessConfigService,
ServerConfigurationManager serverConfigurationManager, ServerConfigurationManager serverConfigurationManager,
DalamudUtilService dalamudUtil, DalamudUtilService dalamudUtil,
PairHandlerRegistry pairHandlerRegistry,
IServiceScopeFactory serviceScopeFactory, LightlessMediator mediator) : base(logger, mediator) IServiceScopeFactory serviceScopeFactory, LightlessMediator mediator) : base(logger, mediator)
{ {
_lightlessConfigService = lightlessConfigService; _lightlessConfigService = lightlessConfigService;
_serverConfigurationManager = serverConfigurationManager; _serverConfigurationManager = serverConfigurationManager;
_dalamudUtil = dalamudUtil; _dalamudUtil = dalamudUtil;
_pairHandlerRegistry = pairHandlerRegistry;
_serviceScopeFactory = serviceScopeFactory; _serviceScopeFactory = serviceScopeFactory;
} }
@@ -111,20 +108,12 @@ public class LightlessPlugin : MediatorSubscriberBase, IHostedService
public Task StopAsync(CancellationToken cancellationToken) public Task StopAsync(CancellationToken cancellationToken)
{ {
Logger.LogDebug("Halting LightlessPlugin");
try
{
_pairHandlerRegistry.ResetAllHandlers();
}
catch (Exception ex)
{
Logger.LogWarning(ex, "Failed to reset pair handlers on shutdown");
}
UnsubscribeAll(); UnsubscribeAll();
DalamudUtilOnLogOut(); DalamudUtilOnLogOut();
Logger.LogDebug("Halting LightlessPlugin");
return Task.CompletedTask; return Task.CompletedTask;
} }

View File

@@ -3,7 +3,7 @@
<PropertyGroup> <PropertyGroup>
<Authors></Authors> <Authors></Authors>
<Company></Company> <Company></Company>
<Version>2.0.2.71</Version> <Version>1.42.0.72</Version>
<Description></Description> <Description></Description>
<Copyright></Copyright> <Copyright></Copyright>
<PackageProjectUrl>https://github.com/Light-Public-Syncshells/LightlessClient</PackageProjectUrl> <PackageProjectUrl>https://github.com/Light-Public-Syncshells/LightlessClient</PackageProjectUrl>
@@ -37,7 +37,6 @@
</PackageReference> </PackageReference>
<PackageReference Include="Microsoft.AspNetCore.SignalR.Client" Version="10.0.1" /> <PackageReference Include="Microsoft.AspNetCore.SignalR.Client" Version="10.0.1" />
<PackageReference Include="Microsoft.AspNetCore.SignalR.Protocols.MessagePack" Version="10.0.1" /> <PackageReference Include="Microsoft.AspNetCore.SignalR.Protocols.MessagePack" Version="10.0.1" />
<PackageReference Include="Microsoft.Extensions.Caching.Memory" Version="10.0.1" />
<PackageReference Include="Microsoft.Extensions.Hosting" Version="10.0.1" /> <PackageReference Include="Microsoft.Extensions.Hosting" Version="10.0.1" />
<PackageReference Include="Glamourer.Api" Version="2.8.0" /> <PackageReference Include="Glamourer.Api" Version="2.8.0" />
<PackageReference Include="NReco.Logging.File" Version="1.3.1" /> <PackageReference Include="NReco.Logging.File" Version="1.3.1" />

View File

@@ -1,9 +0,0 @@
namespace LightlessSync.PlayerData.Factories
{
public enum AnimationValidationMode
{
Unsafe = 0,
Safe = 1,
Safest = 2,
}
}

View File

@@ -1,7 +1,6 @@
using LightlessSync.FileCache; using LightlessSync.FileCache;
using LightlessSync.LightlessConfiguration; using LightlessSync.LightlessConfiguration;
using LightlessSync.Services.Mediator; using LightlessSync.Services.Mediator;
using LightlessSync.Services.ModelDecimation;
using LightlessSync.Services.TextureCompression; using LightlessSync.Services.TextureCompression;
using LightlessSync.WebAPI.Files; using LightlessSync.WebAPI.Files;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
@@ -17,7 +16,6 @@ public class FileDownloadManagerFactory
private readonly FileCompactor _fileCompactor; private readonly FileCompactor _fileCompactor;
private readonly LightlessConfigService _configService; private readonly LightlessConfigService _configService;
private readonly TextureDownscaleService _textureDownscaleService; private readonly TextureDownscaleService _textureDownscaleService;
private readonly ModelDecimationService _modelDecimationService;
private readonly TextureMetadataHelper _textureMetadataHelper; private readonly TextureMetadataHelper _textureMetadataHelper;
public FileDownloadManagerFactory( public FileDownloadManagerFactory(
@@ -28,7 +26,6 @@ public class FileDownloadManagerFactory
FileCompactor fileCompactor, FileCompactor fileCompactor,
LightlessConfigService configService, LightlessConfigService configService,
TextureDownscaleService textureDownscaleService, TextureDownscaleService textureDownscaleService,
ModelDecimationService modelDecimationService,
TextureMetadataHelper textureMetadataHelper) TextureMetadataHelper textureMetadataHelper)
{ {
_loggerFactory = loggerFactory; _loggerFactory = loggerFactory;
@@ -38,7 +35,6 @@ public class FileDownloadManagerFactory
_fileCompactor = fileCompactor; _fileCompactor = fileCompactor;
_configService = configService; _configService = configService;
_textureDownscaleService = textureDownscaleService; _textureDownscaleService = textureDownscaleService;
_modelDecimationService = modelDecimationService;
_textureMetadataHelper = textureMetadataHelper; _textureMetadataHelper = textureMetadataHelper;
} }
@@ -52,7 +48,6 @@ public class FileDownloadManagerFactory
_fileCompactor, _fileCompactor,
_configService, _configService,
_textureDownscaleService, _textureDownscaleService,
_modelDecimationService,
_textureMetadataHelper); _textureMetadataHelper);
} }
} }

View File

@@ -2,15 +2,12 @@
using LightlessSync.API.Data.Enum; using LightlessSync.API.Data.Enum;
using LightlessSync.FileCache; using LightlessSync.FileCache;
using LightlessSync.Interop.Ipc; using LightlessSync.Interop.Ipc;
using LightlessSync.LightlessConfiguration;
using LightlessSync.LightlessConfiguration.Models; using LightlessSync.LightlessConfiguration.Models;
using LightlessSync.PlayerData.Data; using LightlessSync.PlayerData.Data;
using LightlessSync.PlayerData.Handlers; using LightlessSync.PlayerData.Handlers;
using LightlessSync.Services; using LightlessSync.Services;
using LightlessSync.Services.Mediator; using LightlessSync.Services.Mediator;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
using System.Collections.Concurrent;
using System.Diagnostics;
namespace LightlessSync.PlayerData.Factories; namespace LightlessSync.PlayerData.Factories;
@@ -21,34 +18,13 @@ public class PlayerDataFactory
private readonly IpcManager _ipcManager; private readonly IpcManager _ipcManager;
private readonly ILogger<PlayerDataFactory> _logger; private readonly ILogger<PlayerDataFactory> _logger;
private readonly PerformanceCollectorService _performanceCollector; private readonly PerformanceCollectorService _performanceCollector;
private readonly LightlessConfigService _configService;
private readonly XivDataAnalyzer _modelAnalyzer; private readonly XivDataAnalyzer _modelAnalyzer;
private readonly LightlessMediator _lightlessMediator; private readonly LightlessMediator _lightlessMediator;
private readonly TransientResourceManager _transientResourceManager; private readonly TransientResourceManager _transientResourceManager;
private static readonly SemaphoreSlim _papParseLimiter = new(1, 1);
// Transient resolved entries threshold public PlayerDataFactory(ILogger<PlayerDataFactory> logger, DalamudUtilService dalamudUtil, IpcManager ipcManager,
private const int _maxTransientResolvedEntries = 1000; TransientResourceManager transientResourceManager, FileCacheManager fileReplacementFactory,
PerformanceCollectorService performanceCollector, XivDataAnalyzer modelAnalyzer, LightlessMediator lightlessMediator)
// Character build caches
private readonly ConcurrentDictionary<nint, Task<CharacterDataFragment>> _characterBuildInflight = new();
private readonly ConcurrentDictionary<nint, CacheEntry> _characterBuildCache = new();
// Time out thresholds
private static readonly TimeSpan _characterCacheTtl = TimeSpan.FromMilliseconds(750);
private static readonly TimeSpan _softReturnIfBusyAfter = TimeSpan.FromMilliseconds(250);
private static readonly TimeSpan _hardBuildTimeout = TimeSpan.FromSeconds(30);
public PlayerDataFactory(
ILogger<PlayerDataFactory> logger,
DalamudUtilService dalamudUtil,
IpcManager ipcManager,
TransientResourceManager transientResourceManager,
FileCacheManager fileReplacementFactory,
PerformanceCollectorService performanceCollector,
XivDataAnalyzer modelAnalyzer,
LightlessMediator lightlessMediator,
LightlessConfigService configService)
{ {
_logger = logger; _logger = logger;
_dalamudUtil = dalamudUtil; _dalamudUtil = dalamudUtil;
@@ -58,15 +34,15 @@ public class PlayerDataFactory
_performanceCollector = performanceCollector; _performanceCollector = performanceCollector;
_modelAnalyzer = modelAnalyzer; _modelAnalyzer = modelAnalyzer;
_lightlessMediator = lightlessMediator; _lightlessMediator = lightlessMediator;
_configService = configService;
_logger.LogTrace("Creating {this}", nameof(PlayerDataFactory)); _logger.LogTrace("Creating {this}", nameof(PlayerDataFactory));
} }
private sealed record CacheEntry(CharacterDataFragment Fragment, DateTime CreatedUtc);
public async Task<CharacterDataFragment?> BuildCharacterData(GameObjectHandler playerRelatedObject, CancellationToken token) public async Task<CharacterDataFragment?> BuildCharacterData(GameObjectHandler playerRelatedObject, CancellationToken token)
{ {
if (!_ipcManager.Initialized) if (!_ipcManager.Initialized)
{
throw new InvalidOperationException("Penumbra or Glamourer is not connected"); throw new InvalidOperationException("Penumbra or Glamourer is not connected");
}
if (playerRelatedObject == null) return null; if (playerRelatedObject == null) return null;
@@ -91,17 +67,16 @@ public class PlayerDataFactory
if (pointerIsZero) if (pointerIsZero)
{ {
_logger.LogTrace("Pointer was zero for {objectKind}; couldn't build character", playerRelatedObject.ObjectKind); _logger.LogTrace("Pointer was zero for {objectKind}", playerRelatedObject.ObjectKind);
return null; return null;
} }
try try
{ {
return await _performanceCollector.LogPerformance( return await _performanceCollector.LogPerformance(this, $"CreateCharacterData>{playerRelatedObject.ObjectKind}", async () =>
this, {
$"CreateCharacterData>{playerRelatedObject.ObjectKind}", return await CreateCharacterData(playerRelatedObject, token).ConfigureAwait(false);
async () => await CreateCharacterData(playerRelatedObject, token).ConfigureAwait(false) }).ConfigureAwait(true);
).ConfigureAwait(false);
} }
catch (OperationCanceledException) catch (OperationCanceledException)
{ {
@@ -117,259 +92,171 @@ public class PlayerDataFactory
} }
private async Task<bool> CheckForNullDrawObject(IntPtr playerPointer) private async Task<bool> CheckForNullDrawObject(IntPtr playerPointer)
=> await _dalamudUtil.RunOnFrameworkThread(() => CheckForNullDrawObjectUnsafe(playerPointer)).ConfigureAwait(false); {
return await _dalamudUtil.RunOnFrameworkThread(() => CheckForNullDrawObjectUnsafe(playerPointer)).ConfigureAwait(false);
}
private unsafe static bool CheckForNullDrawObjectUnsafe(IntPtr playerPointer) private unsafe bool CheckForNullDrawObjectUnsafe(IntPtr playerPointer)
{ {
if (playerPointer == IntPtr.Zero) if (playerPointer == IntPtr.Zero)
return true; return true;
try
{
var character = (Character*)playerPointer;
if (character == null)
return true;
var gameObject = &character->GameObject; var character = (Character*)playerPointer;
if (gameObject == null)
return true;
return gameObject->DrawObject == null; if (character == null)
}
catch (AccessViolationException)
{
return true; return true;
}
var gameObject = &character->GameObject;
if (gameObject == null)
return true;
return gameObject->DrawObject == null;
} }
private static bool IsCacheFresh(CacheEntry entry) private async Task<CharacterDataFragment> CreateCharacterData(GameObjectHandler playerRelatedObject, CancellationToken ct)
=> (DateTime.UtcNow - entry.CreatedUtc) <= _characterCacheTtl;
private Task<CharacterDataFragment> CreateCharacterData(GameObjectHandler playerRelatedObject, CancellationToken ct)
=> CreateCharacterDataCoalesced(playerRelatedObject, ct);
private async Task<CharacterDataFragment> CreateCharacterDataCoalesced(GameObjectHandler obj, CancellationToken ct)
{
var key = obj.Address;
if (_characterBuildCache.TryGetValue(key, out var cached) && IsCacheFresh(cached) && !_characterBuildInflight.ContainsKey(key))
return cached.Fragment;
var buildTask = _characterBuildInflight.GetOrAdd(key, _ => BuildAndCacheAsync(obj, key));
if (_characterBuildCache.TryGetValue(key, out cached))
{
var completed = await Task.WhenAny(buildTask, Task.Delay(_softReturnIfBusyAfter, ct)).ConfigureAwait(false);
if (completed != buildTask && (DateTime.UtcNow - cached.CreatedUtc) <= TimeSpan.FromSeconds(5))
{
return cached.Fragment;
}
}
return await WithCancellation(buildTask, ct).ConfigureAwait(false);
}
private async Task<CharacterDataFragment> BuildAndCacheAsync(GameObjectHandler obj, nint key)
{
try
{
using var cts = new CancellationTokenSource(_hardBuildTimeout);
var fragment = await CreateCharacterDataInternal(obj, cts.Token).ConfigureAwait(false);
_characterBuildCache[key] = new CacheEntry(fragment, DateTime.UtcNow);
PruneCharacterCacheIfNeeded();
return fragment;
}
finally
{
_characterBuildInflight.TryRemove(key, out _);
}
}
private void PruneCharacterCacheIfNeeded()
{
if (_characterBuildCache.Count < 2048) return;
var cutoff = DateTime.UtcNow - TimeSpan.FromSeconds(10);
foreach (var kv in _characterBuildCache)
{
if (kv.Value.CreatedUtc < cutoff)
_characterBuildCache.TryRemove(kv.Key, out _);
}
}
private static async Task<T> WithCancellation<T>(Task<T> task, CancellationToken ct)
=> await task.WaitAsync(ct).ConfigureAwait(false);
private async Task<CharacterDataFragment> CreateCharacterDataInternal(GameObjectHandler playerRelatedObject, CancellationToken ct)
{ {
var objectKind = playerRelatedObject.ObjectKind; var objectKind = playerRelatedObject.ObjectKind;
CharacterDataFragment fragment = objectKind == ObjectKind.Player ? new CharacterDataFragmentPlayer() : new(); CharacterDataFragment fragment = objectKind == ObjectKind.Player ? new CharacterDataFragmentPlayer() : new();
var logDebug = _logger.IsEnabled(LogLevel.Debug);
var sw = Stopwatch.StartNew();
_logger.LogDebug("Building character data for {obj}", playerRelatedObject); _logger.LogDebug("Building character data for {obj}", playerRelatedObject);
await EnsureObjectPresentAsync(playerRelatedObject, ct).ConfigureAwait(false); // wait until chara is not drawing and present so nothing spontaneously explodes
await _dalamudUtil.WaitWhileCharacterIsDrawing(_logger, playerRelatedObject, Guid.NewGuid(), 30000, ct: ct).ConfigureAwait(false);
int totalWaitTime = 10000;
while (!await _dalamudUtil.IsObjectPresentAsync(await _dalamudUtil.CreateGameObjectAsync(playerRelatedObject.Address).ConfigureAwait(false)).ConfigureAwait(false) && totalWaitTime > 0)
{
_logger.LogTrace("Character is null but it shouldn't be, waiting");
await Task.Delay(50, ct).ConfigureAwait(false);
totalWaitTime -= 50;
}
ct.ThrowIfCancellationRequested(); ct.ThrowIfCancellationRequested();
var waitRecordingTask = _transientResourceManager.WaitForRecording(ct); Dictionary<string, List<ushort>>? boneIndices =
objectKind != ObjectKind.Player
? null
: await _dalamudUtil.RunOnFrameworkThread(() => _modelAnalyzer.GetSkeletonBoneIndices(playerRelatedObject)).ConfigureAwait(false);
await _dalamudUtil.WaitWhileCharacterIsDrawing(_logger, playerRelatedObject, Guid.NewGuid(), 30000, ct: ct) DateTime start = DateTime.UtcNow;
.ConfigureAwait(false);
// penumbra call, it's currently broken
Dictionary<string, HashSet<string>>? resolvedPaths;
resolvedPaths = (await _ipcManager.Penumbra.GetCharacterData(_logger, playerRelatedObject).ConfigureAwait(false));
if (resolvedPaths == null) throw new InvalidOperationException("Penumbra returned null data");
ct.ThrowIfCancellationRequested();
fragment.FileReplacements =
new HashSet<FileReplacement>(resolvedPaths.Select(c => new FileReplacement([.. c.Value], c.Key)), FileReplacementComparer.Instance)
.Where(p => p.HasFileReplacement).ToHashSet();
fragment.FileReplacements.RemoveWhere(c => c.GamePaths.Any(g => !CacheMonitor.AllowedFileExtensions.Any(e => g.EndsWith(e, StringComparison.OrdinalIgnoreCase))));
ct.ThrowIfCancellationRequested();
_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();
}
await _transientResourceManager.WaitForRecording(ct).ConfigureAwait(false);
// if it's pet then it's summoner, if it's summoner we actually want to keep all filereplacements alive at all times
// or we get into redraw city for every change and nothing works properly
if (objectKind == ObjectKind.Pet)
{
foreach (var item in fragment.FileReplacements.Where(i => i.HasFileReplacement).SelectMany(p => p.GamePaths))
{
if (_transientResourceManager.AddTransientResource(objectKind, item))
{
_logger.LogDebug("Marking static {item} for Pet as transient", item);
}
}
_logger.LogTrace("Clearing {count} Static Replacements for Pet", fragment.FileReplacements.Count);
fragment.FileReplacements.Clear();
}
ct.ThrowIfCancellationRequested();
_logger.LogDebug("Handling transient update for {obj}", playerRelatedObject);
// remove all potentially gathered paths from the transient resource manager that are resolved through static resolving
_transientResourceManager.ClearTransientPaths(objectKind, fragment.FileReplacements.SelectMany(c => c.GamePaths).ToList());
// get all remaining paths and resolve them // get all remaining paths and resolve them
var transientPaths = ManageSemiTransientData(objectKind);
var resolvedTransientPaths = await GetFileReplacementsFromPaths(transientPaths, new HashSet<string>(StringComparer.Ordinal)).ConfigureAwait(false);
_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);
}
// clean up all semi transient resources that don't have any file replacement (aka null resolve)
_transientResourceManager.CleanUpSemiTransientResources(objectKind, [.. fragment.FileReplacements]);
ct.ThrowIfCancellationRequested(); ct.ThrowIfCancellationRequested();
if (await CheckForNullDrawObject(playerRelatedObject.Address).ConfigureAwait(false)) // make sure we only return data that actually has file replacements
throw new InvalidOperationException("DrawObject became null during build (actor despawned)"); fragment.FileReplacements = new HashSet<FileReplacement>(fragment.FileReplacements.Where(v => v.HasFileReplacement).OrderBy(v => v.ResolvedPath, StringComparer.Ordinal), FileReplacementComparer.Instance);
// gather up data from ipc
Task<string> getHeelsOffset = _ipcManager.Heels.GetOffsetAsync();
Task<string> getGlamourerData = _ipcManager.Glamourer.GetCharacterCustomizationAsync(playerRelatedObject.Address); Task<string> getGlamourerData = _ipcManager.Glamourer.GetCharacterCustomizationAsync(playerRelatedObject.Address);
Task<string?> getCustomizeData = _ipcManager.CustomizePlus.GetScaleAsync(playerRelatedObject.Address); Task<string?> getCustomizeData = _ipcManager.CustomizePlus.GetScaleAsync(playerRelatedObject.Address);
Task<string?>? getMoodlesData = null; Task<string> getHonorificTitle = _ipcManager.Honorific.GetTitle();
Task<string>? getHeelsOffset = null;
Task<string>? getHonorificTitle = null;
if (objectKind == ObjectKind.Player)
{
getHeelsOffset = _ipcManager.Heels.GetOffsetAsync();
getHonorificTitle = _ipcManager.Honorific.GetTitle();
getMoodlesData = _ipcManager.Moodles.GetStatusAsync(playerRelatedObject.Address);
}
var resolvedPaths = await _ipcManager.Penumbra.GetCharacterData(_logger, playerRelatedObject).ConfigureAwait(false) ?? throw new InvalidOperationException("Penumbra returned null data; couldn't proceed with character");
ct.ThrowIfCancellationRequested();
var staticBuildTask = Task.Run(() => BuildStaticReplacements(resolvedPaths), ct);
fragment.FileReplacements = await staticBuildTask.ConfigureAwait(false);
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("=> {repl}", replacement);
ct.ThrowIfCancellationRequested();
}
}
var staticReplacements = new HashSet<FileReplacement>(fragment.FileReplacements, FileReplacementComparer.Instance);
var transientTask = ResolveTransientReplacementsAsync(
playerRelatedObject,
objectKind,
staticReplacements,
waitRecordingTask,
ct);
fragment.GlamourerString = await getGlamourerData.ConfigureAwait(false); fragment.GlamourerString = await getGlamourerData.ConfigureAwait(false);
_logger.LogDebug("Glamourer is now: {data}", fragment.GlamourerString); _logger.LogDebug("Glamourer is now: {data}", fragment.GlamourerString);
var customizeScale = await getCustomizeData.ConfigureAwait(false); var customizeScale = await getCustomizeData.ConfigureAwait(false);
fragment.CustomizePlusScale = customizeScale ?? string.Empty; fragment.CustomizePlusScale = customizeScale ?? string.Empty;
_logger.LogDebug("Customize is now: {data}", fragment.CustomizePlusScale); _logger.LogDebug("Customize is now: {data}", fragment.CustomizePlusScale);
if (objectKind == ObjectKind.Player) if (objectKind == ObjectKind.Player)
{ {
CharacterDataFragmentPlayer? playerFragment = fragment as CharacterDataFragmentPlayer ?? throw new InvalidOperationException("Failed to cast CharacterDataFragment to Player variant"); var playerFragment = (fragment as CharacterDataFragmentPlayer)!;
playerFragment.ManipulationString = _ipcManager.Penumbra.GetMetaManipulations(); playerFragment.ManipulationString = _ipcManager.Penumbra.GetMetaManipulations();
playerFragment.HonorificData = await getHonorificTitle!.ConfigureAwait(false);
playerFragment!.HonorificData = await getHonorificTitle.ConfigureAwait(false);
_logger.LogDebug("Honorific is now: {data}", playerFragment!.HonorificData); _logger.LogDebug("Honorific is now: {data}", playerFragment!.HonorificData);
playerFragment.PetNamesData = _ipcManager.PetNames.GetLocalNames(); playerFragment!.HeelsData = await getHeelsOffset.ConfigureAwait(false);
_logger.LogDebug("Pet Nicknames is now: {petnames}", playerFragment!.PetNamesData);
playerFragment.HeelsData = await getHeelsOffset!.ConfigureAwait(false);
_logger.LogDebug("Heels is now: {heels}", playerFragment!.HeelsData); _logger.LogDebug("Heels is now: {heels}", playerFragment!.HeelsData);
playerFragment.MoodlesData = (await getMoodlesData!.ConfigureAwait(false)) ?? string.Empty; playerFragment!.MoodlesData = await _ipcManager.Moodles.GetStatusAsync(playerRelatedObject.Address).ConfigureAwait(false) ?? string.Empty;
_logger.LogDebug("Moodles is now: {moodles}", playerFragment!.MoodlesData); _logger.LogDebug("Moodles is now: {moodles}", playerFragment!.MoodlesData);
playerFragment!.PetNamesData = _ipcManager.PetNames.GetLocalNames();
_logger.LogDebug("Pet Nicknames is now: {petnames}", playerFragment!.PetNamesData);
} }
ct.ThrowIfCancellationRequested(); ct.ThrowIfCancellationRequested();
var (resolvedTransientPaths, clearedForPet) = await transientTask.ConfigureAwait(false);
if (clearedForPet != null)
fragment.FileReplacements.Clear();
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("=> {repl}", replacement);
fragment.FileReplacements.Add(replacement);
}
}
else
{
foreach (var replacement in resolvedTransientPaths.Select(c => new FileReplacement([.. c.Value], c.Key)))
fragment.FileReplacements.Add(replacement);
}
_transientResourceManager.CleanUpSemiTransientResources(objectKind, [.. fragment.FileReplacements]);
fragment.FileReplacements = new HashSet<FileReplacement>(
fragment.FileReplacements
.Where(v => v.HasFileReplacement)
.OrderBy(v => v.ResolvedPath, StringComparer.Ordinal),
FileReplacementComparer.Instance);
ct.ThrowIfCancellationRequested();
var toCompute = fragment.FileReplacements.Where(f => !f.IsFileSwap).ToArray(); var toCompute = fragment.FileReplacements.Where(f => !f.IsFileSwap).ToArray();
_logger.LogDebug("Getting Hashes for {amount} Files", toCompute.Length); _logger.LogDebug("Getting Hashes for {amount} Files", toCompute.Length);
var computedPaths = _fileCacheManager.GetFileCachesByPaths(toCompute.Select(c => c.ResolvedPath).ToArray());
await Task.Run(() => foreach (var file in toCompute)
{ {
var computedPaths = _fileCacheManager.GetFileCachesByPaths([.. toCompute.Select(c => c.ResolvedPath)]); ct.ThrowIfCancellationRequested();
foreach (var file in toCompute) file.Hash = computedPaths[file.ResolvedPath]?.Hash ?? string.Empty;
{ }
ct.ThrowIfCancellationRequested();
file.Hash = computedPaths[file.ResolvedPath]?.Hash ?? string.Empty;
}
}, ct).ConfigureAwait(false);
var removed = fragment.FileReplacements.RemoveWhere(f => !f.IsFileSwap && string.IsNullOrEmpty(f.Hash)); var removed = fragment.FileReplacements.RemoveWhere(f => !f.IsFileSwap && string.IsNullOrEmpty(f.Hash));
if (removed > 0) if (removed > 0)
{
_logger.LogDebug("Removed {amount} of invalid files", removed); _logger.LogDebug("Removed {amount} of invalid files", removed);
}
ct.ThrowIfCancellationRequested(); ct.ThrowIfCancellationRequested();
Dictionary<string, List<ushort>>? boneIndices = null;
var hasPapFiles = false;
if (objectKind == ObjectKind.Player) if (objectKind == ObjectKind.Player)
{ {
hasPapFiles = fragment.FileReplacements.Any(f =>
!f.IsFileSwap && f.GamePaths.Any(p => p.EndsWith(".pap", StringComparison.OrdinalIgnoreCase)));
if (hasPapFiles)
{
boneIndices = await _dalamudUtil
.RunOnFrameworkThread(() => _modelAnalyzer.GetSkeletonBoneIndices(playerRelatedObject))
.ConfigureAwait(false);
}
try try
{ {
#if DEBUG await VerifyPlayerAnimationBones(boneIndices, (fragment as CharacterDataFragmentPlayer)!, ct).ConfigureAwait(false);
if (hasPapFiles && boneIndices != null)
_modelAnalyzer.DumpLocalSkeletonIndices(playerRelatedObject);
#endif
if (hasPapFiles)
{
await VerifyPlayerAnimationBones(boneIndices, (CharacterDataFragmentPlayer)fragment, ct)
.ConfigureAwait(false);
}
} }
catch (OperationCanceledException e) catch (OperationCanceledException e)
{ {
@@ -382,338 +269,104 @@ public class PlayerDataFactory
} }
} }
_logger.LogInformation("Building character data for {obj} took {time}ms", _logger.LogInformation("Building character data for {obj} took {time}ms", objectKind, TimeSpan.FromTicks(DateTime.UtcNow.Ticks - start.Ticks).TotalMilliseconds);
objectKind, sw.Elapsed.TotalMilliseconds);
return fragment; return fragment;
} }
private async Task EnsureObjectPresentAsync(GameObjectHandler handler, CancellationToken ct) private async Task VerifyPlayerAnimationBones(Dictionary<string, List<ushort>>? boneIndices, CharacterDataFragmentPlayer fragment, CancellationToken ct)
{ {
var remaining = 10000; if (boneIndices == null) return;
while (remaining > 0)
foreach (var kvp in boneIndices)
{ {
ct.ThrowIfCancellationRequested(); _logger.LogDebug("Found {skellyname} ({idx} bone indices) on player: {bones}", kvp.Key, kvp.Value.Any() ? kvp.Value.Max() : 0, string.Join(',', kvp.Value));
var obj = await _dalamudUtil.CreateGameObjectAsync(handler.Address).ConfigureAwait(false);
if (await _dalamudUtil.IsObjectPresentAsync(obj).ConfigureAwait(false))
return;
_logger.LogTrace("Character is null but it shouldn't be, waiting");
await Task.Delay(50, ct).ConfigureAwait(false);
remaining -= 50;
}
}
private static HashSet<FileReplacement> BuildStaticReplacements(Dictionary<string, HashSet<string>> resolvedPaths)
{
var set = new HashSet<FileReplacement>(FileReplacementComparer.Instance);
foreach (var kvp in resolvedPaths)
{
var fr = new FileReplacement([.. kvp.Value], kvp.Key);
if (!fr.HasFileReplacement) continue;
var allAllowed = fr.GamePaths.All(g =>
CacheMonitor.AllowedFileExtensions.Any(e => g.EndsWith(e, StringComparison.OrdinalIgnoreCase)));
if (!allAllowed) continue;
set.Add(fr);
} }
return set; if (boneIndices.All(u => u.Value.Count == 0)) return;
}
private async Task<(IReadOnlyDictionary<string, string[]> ResolvedPaths, HashSet<FileReplacement>? ClearedReplacements)>
ResolveTransientReplacementsAsync(
GameObjectHandler obj,
ObjectKind objectKind,
HashSet<FileReplacement> staticReplacements,
Task waitRecordingTask,
CancellationToken ct)
{
await waitRecordingTask.ConfigureAwait(false);
HashSet<FileReplacement>? clearedReplacements = null;
if (objectKind == ObjectKind.Pet)
{
foreach (var item in staticReplacements.Where(i => i.HasFileReplacement).SelectMany(p => p.GamePaths))
{
if (_transientResourceManager.AddTransientResource(objectKind, item))
_logger.LogDebug("Marking static {item} for Pet as transient", item);
}
_logger.LogTrace("Clearing {count} Static Replacements for Pet", staticReplacements.Count);
clearedReplacements = staticReplacements;
}
ct.ThrowIfCancellationRequested();
_transientResourceManager.ClearTransientPaths(objectKind, [.. staticReplacements.SelectMany(c => c.GamePaths)]);
var transientPaths = ManageSemiTransientData(objectKind);
if (transientPaths.Count == 0)
return (new Dictionary<string, string[]>(StringComparer.Ordinal), clearedReplacements);
var resolved = await GetFileReplacementsFromPaths(obj, transientPaths, new HashSet<string>(StringComparer.Ordinal))
.ConfigureAwait(false);
if (_maxTransientResolvedEntries > 0 && resolved.Count > _maxTransientResolvedEntries)
{
_logger.LogWarning("Transient entries ({resolved}) are above the threshold {max}; Please consider disable some mods (VFX have heavy load) to reduce transient load",
resolved.Count,
_maxTransientResolvedEntries);
}
return (resolved, clearedReplacements);
}
private async Task VerifyPlayerAnimationBones(
Dictionary<string, List<ushort>>? playerBoneIndices,
CharacterDataFragmentPlayer fragment,
CancellationToken ct)
{
var mode = _configService.Current.AnimationValidationMode;
var allowBasedShift = _configService.Current.AnimationAllowOneBasedShift;
var allownNightIndex = _configService.Current.AnimationAllowNeighborIndexTolerance;
if (mode == AnimationValidationMode.Unsafe)
return;
if (playerBoneIndices == null || playerBoneIndices.Count == 0)
return;
var localBoneSets = new Dictionary<string, HashSet<ushort>>(StringComparer.OrdinalIgnoreCase);
foreach (var (rawLocalKey, indices) in playerBoneIndices)
{
if (indices is not { Count: > 0 })
continue;
var key = XivDataAnalyzer.CanonicalizeSkeletonKey(rawLocalKey);
if (string.IsNullOrEmpty(key))
continue;
if (!localBoneSets.TryGetValue(key, out var set))
localBoneSets[key] = set = [];
foreach (var idx in indices)
set.Add(idx);
}
if (localBoneSets.Count == 0)
return;
if (_logger.IsEnabled(LogLevel.Debug))
{
_logger.LogDebug("SEND local buckets: {b}",
string.Join(", ", localBoneSets.Keys.Order(StringComparer.Ordinal)));
foreach (var kvp in localBoneSets.OrderBy(k => k.Key, StringComparer.OrdinalIgnoreCase))
{
var min = kvp.Value.Count > 0 ? kvp.Value.Min() : 0;
var max = kvp.Value.Count > 0 ? kvp.Value.Max() : 0;
_logger.LogDebug("Local bucket {bucket}: count={count} min={min} max={max}",
kvp.Key, kvp.Value.Count, min, max);
}
}
var papGroups = fragment.FileReplacements
.Where(f => !f.IsFileSwap
&& !string.IsNullOrEmpty(f.Hash)
&& f.GamePaths is { Count: > 0 }
&& f.GamePaths.Any(p => p.EndsWith(".pap", StringComparison.OrdinalIgnoreCase)))
.GroupBy(f => f.Hash!, StringComparer.OrdinalIgnoreCase)
.ToList();
int noValidationFailed = 0; int noValidationFailed = 0;
foreach (var file in fragment.FileReplacements.Where(f => !f.IsFileSwap && f.GamePaths.First().EndsWith("pap", StringComparison.OrdinalIgnoreCase)).ToList())
foreach (var g in papGroups)
{ {
ct.ThrowIfCancellationRequested(); ct.ThrowIfCancellationRequested();
var hash = g.Key; var skeletonIndices = await _dalamudUtil.RunOnFrameworkThread(() => _modelAnalyzer.GetBoneIndicesFromPap(file.Hash)).ConfigureAwait(false);
bool validationFailed = false;
Dictionary<string, List<ushort>>? papIndices = null; if (skeletonIndices != null)
await _papParseLimiter.WaitAsync(ct).ConfigureAwait(false);
try
{ {
papIndices = await Task.Run(() => _modelAnalyzer.GetBoneIndicesFromPap(hash), ct) // 105 is the maximum vanilla skellington spoopy bone index
.ConfigureAwait(false); if (skeletonIndices.All(k => k.Value.Max() <= 105))
} {
finally _logger.LogTrace("All indices of {path} are <= 105, ignoring", file.ResolvedPath);
{ continue;
_papParseLimiter.Release(); }
}
if (papIndices == null || papIndices.Count == 0) _logger.LogDebug("Verifying bone indices for {path}, found {x} skeletons", file.ResolvedPath, skeletonIndices.Count);
continue;
if (papIndices.All(k => k.Value.DefaultIfEmpty().Max() <= 105)) foreach (var boneCount in skeletonIndices.Select(k => k).ToList())
continue; {
if (boneCount.Value.Max() > boneIndices.SelectMany(b => b.Value).Max())
if (_logger.IsEnabled(LogLevel.Debug))
{
var papBuckets = papIndices
.Select(kvp => new
{ {
Raw = kvp.Key, _logger.LogWarning("Found more bone indices on the animation {path} skeleton {skl} (max indice {idx}) than on any player related skeleton (max indice {idx2})",
Key = XivDataAnalyzer.CanonicalizeSkeletonKey(kvp.Key), file.ResolvedPath, boneCount.Key, boneCount.Value.Max(), boneIndices.SelectMany(b => b.Value).Max());
Indices = kvp.Value validationFailed = true;
}) break;
.Where(x => x.Indices is { Count: > 0 }) }
.GroupBy(x => string.IsNullOrEmpty(x.Key) ? x.Raw : x.Key!, StringComparer.OrdinalIgnoreCase) }
.Select(grp =>
{
var all = grp.SelectMany(v => v.Indices).ToList();
var min = all.Count > 0 ? all.Min() : 0;
var max = all.Count > 0 ? all.Max() : 0;
var raws = string.Join(',', grp.Select(v => v.Raw).Distinct(StringComparer.OrdinalIgnoreCase));
return $"{grp.Key}(min={min},max={max},raw=[{raws}])";
})
.ToList();
_logger.LogDebug("SEND pap buckets for hash={hash}: {b}",
hash,
string.Join(" | ", papBuckets));
} }
if (XivDataAnalyzer.IsPapCompatible(localBoneSets, papIndices, mode, allowBasedShift, allownNightIndex, out var reason)) if (validationFailed)
continue; {
noValidationFailed++;
_logger.LogDebug("Removing {file} from sent file replacements and transient data", file.ResolvedPath);
fragment.FileReplacements.Remove(file);
foreach (var gamePath in file.GamePaths)
{
_transientResourceManager.RemoveTransientResource(ObjectKind.Player, gamePath);
}
}
noValidationFailed++;
_logger.LogWarning(
"Animation PAP hash {hash} is not compatible with local skeletons; dropping all mappings for this hash. Reason: {reason}",
hash,
reason);
var removedGamePaths = fragment.FileReplacements
.Where(fr => !fr.IsFileSwap
&& string.Equals(fr.Hash, hash, StringComparison.OrdinalIgnoreCase)
&& fr.GamePaths.Any(p => p.EndsWith(".pap", StringComparison.OrdinalIgnoreCase)))
.SelectMany(fr => fr.GamePaths.Where(p => p.EndsWith(".pap", StringComparison.OrdinalIgnoreCase)))
.Distinct(StringComparer.OrdinalIgnoreCase)
.ToList();
fragment.FileReplacements.RemoveWhere(fr =>
!fr.IsFileSwap
&& string.Equals(fr.Hash, hash, StringComparison.OrdinalIgnoreCase)
&& fr.GamePaths.Any(p => p.EndsWith(".pap", StringComparison.OrdinalIgnoreCase)));
foreach (var gp in removedGamePaths)
_transientResourceManager.RemoveTransientResource(ObjectKind.Player, gp);
} }
if (noValidationFailed > 0) if (noValidationFailed > 0)
{ {
_lightlessMediator.Publish(new NotificationMessage( _lightlessMediator.Publish(new NotificationMessage("Invalid Skeleton Setup",
"Invalid Skeleton Setup", $"Your client is attempting to send {noValidationFailed} animation files with invalid bone data. Those animation files have been removed from your sent data. " +
$"Your client is attempting to send {noValidationFailed} animation files that don't match your current skeleton validation mode ({mode}). " + $"Verify that you are using the correct skeleton for those animation files (Check /xllog for more information).",
"Please adjust your skeleton/mods or change the validation mode if this is unexpected. " + NotificationType.Warning, TimeSpan.FromSeconds(10)));
"Those animation files have been removed from your sent (player) data. (Check /xllog for details).",
NotificationType.Warning,
TimeSpan.FromSeconds(10)));
} }
} }
private async Task<IReadOnlyDictionary<string, string[]>> GetFileReplacementsFromPaths(HashSet<string> forwardResolve, HashSet<string> reverseResolve)
private async Task<IReadOnlyDictionary<string, string[]>> GetFileReplacementsFromPaths(
GameObjectHandler handler,
HashSet<string> forwardResolve,
HashSet<string> reverseResolve)
{ {
var forwardPaths = forwardResolve.ToArray(); var forwardPaths = forwardResolve.ToArray();
var reversePaths = reverseResolve.ToArray(); var reversePaths = reverseResolve.ToArray();
if (forwardPaths.Length == 0 && reversePaths.Length == 0) Dictionary<string, List<string>> resolvedPaths = new(StringComparer.Ordinal);
{
return new Dictionary<string, string[]>(StringComparer.OrdinalIgnoreCase).AsReadOnly();
}
var forwardPathsLower = forwardPaths.Length == 0 ? Array.Empty<string>() : forwardPaths.Select(p => p.ToLowerInvariant()).ToArray();
var reversePathsLower = reversePaths.Length == 0 ? Array.Empty<string>() : reversePaths.Select(p => p.ToLowerInvariant()).ToArray();
Dictionary<string, List<string>> resolvedPaths = new(forwardPaths.Length + reversePaths.Length, 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] = [forwardPathsLower[i]];
}
}
for (int i = 0; i < reversePaths.Length; i++)
{
var filePath = reversePathsLower[i];
var reverseResolvedLower = new string[reverseResolved[i].Length];
for (var j = 0; j < reverseResolvedLower.Length; j++)
{
reverseResolvedLower[j] = reverseResolved[i][j].ToLowerInvariant();
}
if (resolvedPaths.TryGetValue(filePath, out var list))
list.AddRange(reverseResolved[i].Select(c => c.ToLowerInvariant()));
else
resolvedPaths[filePath] = [.. 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); var (forward, reverse) = await _ipcManager.Penumbra.ResolvePathsAsync(forwardPaths, reversePaths).ConfigureAwait(false);
for (int i = 0; i < forwardPaths.Length; i++) for (int i = 0; i < forwardPaths.Length; i++)
{ {
var filePath = forward[i].ToLowerInvariant(); var filePath = forward[i].ToLowerInvariant();
if (resolvedPaths.TryGetValue(filePath, out var list)) if (resolvedPaths.TryGetValue(filePath, out var list))
{
list.Add(forwardPaths[i].ToLowerInvariant()); list.Add(forwardPaths[i].ToLowerInvariant());
}
else else
{
resolvedPaths[filePath] = [forwardPaths[i].ToLowerInvariant()]; resolvedPaths[filePath] = [forwardPaths[i].ToLowerInvariant()];
}
} }
for (int i = 0; i < reversePaths.Length; i++) for (int i = 0; i < reversePaths.Length; i++)
{ {
var filePath = reversePathsLower[i]; var filePath = reversePaths[i].ToLowerInvariant();
var reverseResolvedLower = new string[reverse[i].Length];
for (var j = 0; j < reverseResolvedLower.Length; j++)
{
reverseResolvedLower[j] = reverse[i][j].ToLowerInvariant();
}
if (resolvedPaths.TryGetValue(filePath, out var list)) if (resolvedPaths.TryGetValue(filePath, out var list))
{
list.AddRange(reverse[i].Select(c => c.ToLowerInvariant())); list.AddRange(reverse[i].Select(c => c.ToLowerInvariant()));
}
else else
resolvedPaths[filePath] = [.. reverse[i].Select(c => c.ToLowerInvariant()).ToList()]; {
resolvedPaths[filePath] = new List<string>(reverse[i].Select(c => c.ToLowerInvariant()).ToList());
}
} }
return resolvedPaths.ToDictionary(k => k.Key, k => k.Value.ToArray(), StringComparer.OrdinalIgnoreCase).AsReadOnly(); return resolvedPaths.ToDictionary(k => k.Key, k => k.Value.ToArray(), StringComparer.OrdinalIgnoreCase).AsReadOnly();
@@ -724,29 +377,11 @@ public class PlayerDataFactory
_transientResourceManager.PersistTransientResources(objectKind); _transientResourceManager.PersistTransientResources(objectKind);
HashSet<string> pathsToResolve = new(StringComparer.Ordinal); HashSet<string> pathsToResolve = new(StringComparer.Ordinal);
foreach (var path in _transientResourceManager.GetSemiTransientResources(objectKind).Where(path => !string.IsNullOrEmpty(path)))
int scanned = 0, skippedEmpty = 0, skippedVfx = 0;
foreach (var path in _transientResourceManager.GetSemiTransientResources(objectKind))
{ {
scanned++;
if (string.IsNullOrEmpty(path))
{
skippedEmpty++;
continue;
}
pathsToResolve.Add(path); pathsToResolve.Add(path);
} }
if (_logger.IsEnabled(LogLevel.Debug))
{
_logger.LogDebug(
"ManageSemiTransientData({kind}): scanned={scanned}, added={added}, skippedEmpty={skippedEmpty}, skippedVfx={skippedVfx}",
objectKind, scanned, pathsToResolve.Count, skippedEmpty, skippedVfx);
}
return pathsToResolve; return pathsToResolve;
} }
} }

View File

@@ -16,8 +16,6 @@ public sealed class GameObjectHandler : DisposableMediatorSubscriberBase, IHighP
private readonly Func<IntPtr> _getAddress; private readonly Func<IntPtr> _getAddress;
private readonly bool _isOwnedObject; private readonly bool _isOwnedObject;
private readonly PerformanceCollectorService _performanceCollector; private readonly PerformanceCollectorService _performanceCollector;
private readonly object _frameworkUpdateGate = new();
private bool _frameworkUpdateSubscribed;
private byte _classJob = 0; private byte _classJob = 0;
private Task? _delayedZoningTask; private Task? _delayedZoningTask;
private bool _haltProcessing = false; private bool _haltProcessing = false;
@@ -49,10 +47,7 @@ public sealed class GameObjectHandler : DisposableMediatorSubscriberBase, IHighP
}); });
} }
if (_isOwnedObject) Mediator.Subscribe<FrameworkUpdateMessage>(this, (_) => FrameworkUpdate());
{
EnableFrameworkUpdates();
}
Mediator.Subscribe<ZoneSwitchEndMessage>(this, (_) => ZoneSwitchEnd()); Mediator.Subscribe<ZoneSwitchEndMessage>(this, (_) => ZoneSwitchEnd());
Mediator.Subscribe<ZoneSwitchStartMessage>(this, (_) => ZoneSwitchStart()); Mediator.Subscribe<ZoneSwitchStartMessage>(this, (_) => ZoneSwitchStart());
@@ -78,7 +73,6 @@ public sealed class GameObjectHandler : DisposableMediatorSubscriberBase, IHighP
if (msg.Address == Address) if (msg.Address == Address)
{ {
_haltProcessing = false; _haltProcessing = false;
Refresh();
} }
}); });
@@ -115,7 +109,7 @@ public sealed class GameObjectHandler : DisposableMediatorSubscriberBase, IHighP
{ {
while (await _dalamudUtil.RunOnFrameworkThread(() => while (await _dalamudUtil.RunOnFrameworkThread(() =>
{ {
EnsureLatestObjectState(); if (_haltProcessing) CheckAndUpdateObject();
if (CurrentDrawCondition != DrawCondition.None) return true; if (CurrentDrawCondition != DrawCondition.None) return true;
var gameObj = _dalamudUtil.CreateGameObject(Address); var gameObj = _dalamudUtil.CreateGameObject(Address);
if (gameObj is Dalamud.Game.ClientState.Objects.Types.ICharacter chara) if (gameObj is Dalamud.Game.ClientState.Objects.Types.ICharacter chara)
@@ -154,11 +148,6 @@ public sealed class GameObjectHandler : DisposableMediatorSubscriberBase, IHighP
_haltProcessing = false; _haltProcessing = false;
} }
public void Refresh()
{
_dalamudUtil.RunOnFrameworkThread(CheckAndUpdateObject).GetAwaiter().GetResult();
}
public async Task<bool> IsBeingDrawnRunOnFrameworkAsync() public async Task<bool> IsBeingDrawnRunOnFrameworkAsync()
{ {
return await _dalamudUtil.RunOnFrameworkThread(IsBeingDrawn).ConfigureAwait(false); return await _dalamudUtil.RunOnFrameworkThread(IsBeingDrawn).ConfigureAwait(false);
@@ -177,36 +166,30 @@ public sealed class GameObjectHandler : DisposableMediatorSubscriberBase, IHighP
Mediator.Publish(new GameObjectHandlerDestroyedMessage(this, _isOwnedObject)); Mediator.Publish(new GameObjectHandlerDestroyedMessage(this, _isOwnedObject));
} }
private void CheckAndUpdateObject() => CheckAndUpdateObject(allowPublish: true); private unsafe void CheckAndUpdateObject()
private unsafe void CheckAndUpdateObject(bool allowPublish = true)
{ {
var prevAddr = Address; var prevAddr = Address;
var prevDrawObj = DrawObjectAddress; var prevDrawObj = DrawObjectAddress;
Address = _getAddress(); Address = _getAddress();
if (Address != IntPtr.Zero) if (Address != IntPtr.Zero)
{ {
var gameObject = (FFXIVClientStructs.FFXIV.Client.Game.Object.GameObject*)Address; var gameObject = (FFXIVClientStructs.FFXIV.Client.Game.Object.GameObject*)Address;
DrawObjectAddress = (IntPtr)gameObject->DrawObject; var drawObjAddr = (IntPtr)gameObject->DrawObject;
DrawObjectAddress = drawObjAddr;
EntityId = gameObject->EntityId; EntityId = gameObject->EntityId;
CurrentDrawCondition = DrawCondition.None;
var chara = (Character*)Address;
var newName = chara->GameObject.NameString;
if (!string.IsNullOrEmpty(newName) && !string.Equals(newName, Name, StringComparison.Ordinal))
Name = newName;
} }
else else
{ {
DrawObjectAddress = IntPtr.Zero; DrawObjectAddress = IntPtr.Zero;
EntityId = uint.MaxValue; EntityId = uint.MaxValue;
CurrentDrawCondition = DrawCondition.DrawObjectZero;
} }
CurrentDrawCondition = IsBeingDrawnUnsafe(); CurrentDrawCondition = IsBeingDrawnUnsafe();
if (_haltProcessing || !allowPublish) return; if (_haltProcessing) return;
bool drawObjDiff = DrawObjectAddress != prevDrawObj; bool drawObjDiff = DrawObjectAddress != prevDrawObj;
bool addrDiff = Address != prevAddr; bool addrDiff = Address != prevAddr;
@@ -363,10 +346,12 @@ public sealed class GameObjectHandler : DisposableMediatorSubscriberBase, IHighP
private void FrameworkUpdate() private void FrameworkUpdate()
{ {
if (!_delayedZoningTask?.IsCompleted ?? false) return;
try try
{ {
var zoningDelayActive = !(_delayedZoningTask?.IsCompleted ?? true); _performanceCollector.LogPerformance(this, $"CheckAndUpdateObject>{(_isOwnedObject ? "Self" : "Other")}+{ObjectKind}/{(string.IsNullOrEmpty(Name) ? "Unk" : Name)}"
_performanceCollector.LogPerformance(this, $"CheckAndUpdateObject>{(_isOwnedObject ? "Self" : "Other")}+{ObjectKind}/{(string.IsNullOrEmpty(Name) ? "Unk" : Name)}", () => CheckAndUpdateObject(allowPublish: !zoningDelayActive)); + $"+{Address.ToString("X")}", CheckAndUpdateObject);
} }
catch (Exception ex) catch (Exception ex)
{ {
@@ -376,7 +361,7 @@ public sealed class GameObjectHandler : DisposableMediatorSubscriberBase, IHighP
private bool IsBeingDrawn() private bool IsBeingDrawn()
{ {
EnsureLatestObjectState(); if (_haltProcessing) CheckAndUpdateObject();
if (_dalamudUtil.IsAnythingDrawing) if (_dalamudUtil.IsAnythingDrawing)
{ {
@@ -388,28 +373,6 @@ public sealed class GameObjectHandler : DisposableMediatorSubscriberBase, IHighP
return CurrentDrawCondition != DrawCondition.None; return CurrentDrawCondition != DrawCondition.None;
} }
private void EnsureLatestObjectState()
{
if (_haltProcessing || !_frameworkUpdateSubscribed)
{
CheckAndUpdateObject();
}
}
private void EnableFrameworkUpdates()
{
lock (_frameworkUpdateGate)
{
if (_frameworkUpdateSubscribed)
{
return;
}
Mediator.Subscribe<FrameworkUpdateMessage>(this, _ => FrameworkUpdate());
_frameworkUpdateSubscribed = true;
}
}
private unsafe DrawCondition IsBeingDrawnUnsafe() private unsafe DrawCondition IsBeingDrawnUnsafe()
{ {
if (Address == IntPtr.Zero) return DrawCondition.ObjectZero; if (Address == IntPtr.Zero) return DrawCondition.ObjectZero;
@@ -467,6 +430,6 @@ public sealed class GameObjectHandler : DisposableMediatorSubscriberBase, IHighP
Logger.LogDebug("[{this}] Delay after zoning complete", this); Logger.LogDebug("[{this}] Delay after zoning complete", this);
_zoningCts.Dispose(); _zoningCts.Dispose();
} }
}, _zoningCts.Token); });
} }
} }

View File

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

View File

@@ -16,5 +16,4 @@ public interface IPairPerformanceSubject
long LastAppliedApproximateVRAMBytes { get; set; } long LastAppliedApproximateVRAMBytes { get; set; }
long LastAppliedApproximateEffectiveVRAMBytes { get; set; } long LastAppliedApproximateEffectiveVRAMBytes { get; set; }
long LastAppliedDataTris { get; set; } long LastAppliedDataTris { get; set; }
long LastAppliedApproximateEffectiveTris { get; set; }
} }

View File

@@ -69,7 +69,6 @@ public class Pair
public string? PlayerName => TryGetHandler()?.PlayerName ?? UserPair.User.AliasOrUID; public string? PlayerName => TryGetHandler()?.PlayerName ?? UserPair.User.AliasOrUID;
public long LastAppliedDataBytes => TryGetHandler()?.LastAppliedDataBytes ?? -1; public long LastAppliedDataBytes => TryGetHandler()?.LastAppliedDataBytes ?? -1;
public long LastAppliedDataTris => TryGetHandler()?.LastAppliedDataTris ?? -1; public long LastAppliedDataTris => TryGetHandler()?.LastAppliedDataTris ?? -1;
public long LastAppliedApproximateEffectiveTris => TryGetHandler()?.LastAppliedApproximateEffectiveTris ?? -1;
public long LastAppliedApproximateVRAMBytes => TryGetHandler()?.LastAppliedApproximateVRAMBytes ?? -1; public long LastAppliedApproximateVRAMBytes => TryGetHandler()?.LastAppliedApproximateVRAMBytes ?? -1;
public long LastAppliedApproximateEffectiveVRAMBytes => TryGetHandler()?.LastAppliedApproximateEffectiveVRAMBytes ?? -1; public long LastAppliedApproximateEffectiveVRAMBytes => TryGetHandler()?.LastAppliedApproximateEffectiveVRAMBytes ?? -1;
public string Ident => TryGetHandler()?.Ident ?? TryGetConnection()?.Ident ?? string.Empty; public string Ident => TryGetHandler()?.Ident ?? TryGetConnection()?.Ident ?? string.Empty;
@@ -88,25 +87,22 @@ public class Pair
return; return;
} }
if (args.Target is not MenuTargetDefault target || target.TargetObjectId != handler.PlayerCharacterId) if (args.Target is not MenuTargetDefault target || target.TargetObjectId != handler.PlayerCharacterId || IsPaused)
{ {
return; return;
} }
if (!IsPaused) UiSharedService.AddContextMenuItem(args, name: "Open Profile", prefixChar: 'L', colorMenuItem: _lightlessPrefixColor, onClick: () =>
{ {
UiSharedService.AddContextMenuItem(args, name: "Open Profile", prefixChar: 'L', colorMenuItem: _lightlessPrefixColor, onClick: () => _mediator.Publish(new ProfileOpenStandaloneMessage(this));
{ return Task.CompletedTask;
_mediator.Publish(new ProfileOpenStandaloneMessage(this)); });
return Task.CompletedTask;
});
UiSharedService.AddContextMenuItem(args, name: "Reapply last data", prefixChar: 'L', colorMenuItem: _lightlessPrefixColor, onClick: () => UiSharedService.AddContextMenuItem(args, name: "Reapply last data", prefixChar: 'L', colorMenuItem: _lightlessPrefixColor, onClick: () =>
{ {
ApplyLastReceivedData(forced: true); ApplyLastReceivedData(forced: true);
return Task.CompletedTask; return Task.CompletedTask;
}); });
}
UiSharedService.AddContextMenuItem(args, name: "Change Permissions", prefixChar: 'L', colorMenuItem: _lightlessPrefixColor, onClick: () => UiSharedService.AddContextMenuItem(args, name: "Change Permissions", prefixChar: 'L', colorMenuItem: _lightlessPrefixColor, onClick: () =>
{ {
@@ -114,24 +110,7 @@ public class Pair
return Task.CompletedTask; return Task.CompletedTask;
}); });
if (IsPaused) UiSharedService.AddContextMenuItem(args, name: "Cycle pause state", prefixChar: 'L', colorMenuItem: _lightlessPrefixColor, onClick: () =>
{
UiSharedService.AddContextMenuItem(args, name: "Toggle Unpause State", prefixChar: 'L', colorMenuItem: _lightlessPrefixColor, onClick: () =>
{
_ = _apiController.Value.UnpauseAsync(UserData);
return Task.CompletedTask;
});
}
else
{
UiSharedService.AddContextMenuItem(args, name: "Toggle Pause State", prefixChar: 'L', colorMenuItem: _lightlessPrefixColor, onClick: () =>
{
_ = _apiController.Value.PauseAsync(UserData);
return Task.CompletedTask;
});
}
UiSharedService.AddContextMenuItem(args, name: "Cycle Pause State", prefixChar: 'L', colorMenuItem: _lightlessPrefixColor, onClick: () =>
{ {
TriggerCyclePause(); TriggerCyclePause();
return Task.CompletedTask; return Task.CompletedTask;
@@ -215,13 +194,9 @@ public class Pair
{ {
var handler = TryGetHandler(); var handler = TryGetHandler();
if (handler is null) if (handler is null)
{
return PairDebugInfo.Empty; 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( return new PairDebugInfo(
true, true,
@@ -231,19 +206,11 @@ public class Pair
handler.LastDataReceivedAt, handler.LastDataReceivedAt,
handler.LastApplyAttemptAt, handler.LastApplyAttemptAt,
handler.LastSuccessfulApplyAt, handler.LastSuccessfulApplyAt,
handler.InvisibleSinceUtc,
handler.VisibilityEvictionDueAtUtc,
remainingSeconds,
handler.LastFailureReason, handler.LastFailureReason,
handler.LastBlockingConditions, handler.LastBlockingConditions,
handler.IsApplying, handler.IsApplying,
handler.IsDownloading, handler.IsDownloading,
handler.PendingDownloadCount, handler.PendingDownloadCount,
handler.ForbiddenDownloadCount, handler.ForbiddenDownloadCount);
handler.PendingModReapply,
handler.ModApplyDeferred,
handler.MissingCriticalMods,
handler.MissingNonCriticalMods,
handler.MissingForbiddenMods);
} }
} }

View File

@@ -125,7 +125,6 @@ public sealed partial class PairCoordinator
} }
} }
_mediator.Publish(new PairOnlineMessage(new PairUniqueIdentifier(dto.User.UID)));
PublishPairDataChanged(); PublishPairDataChanged();
} }

View File

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

File diff suppressed because it is too large Load Diff

View File

@@ -1,15 +1,11 @@
using LightlessSync.FileCache; using LightlessSync.FileCache;
using LightlessSync.Interop.Ipc; using LightlessSync.Interop.Ipc;
using LightlessSync.LightlessConfiguration;
using LightlessSync.PlayerData.Factories; using LightlessSync.PlayerData.Factories;
using LightlessSync.Services; using LightlessSync.Services;
using LightlessSync.Services.ActorTracking;
using LightlessSync.Services.Mediator; using LightlessSync.Services.Mediator;
using LightlessSync.Services.ModelDecimation;
using LightlessSync.Services.PairProcessing; using LightlessSync.Services.PairProcessing;
using LightlessSync.Services.ServerConfiguration; using LightlessSync.Services.ServerConfiguration;
using LightlessSync.Services.TextureCompression; using LightlessSync.Services.TextureCompression;
using Dalamud.Plugin.Services;
using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
@@ -28,18 +24,12 @@ internal sealed class PairHandlerAdapterFactory : IPairHandlerAdapterFactory
private readonly IServiceProvider _serviceProvider; private readonly IServiceProvider _serviceProvider;
private readonly IHostApplicationLifetime _lifetime; private readonly IHostApplicationLifetime _lifetime;
private readonly FileCacheManager _fileCacheManager; private readonly FileCacheManager _fileCacheManager;
private readonly PlayerPerformanceConfigService _playerPerformanceConfigService;
private readonly PlayerPerformanceService _playerPerformanceService; private readonly PlayerPerformanceService _playerPerformanceService;
private readonly PairProcessingLimiter _pairProcessingLimiter; private readonly PairProcessingLimiter _pairProcessingLimiter;
private readonly ServerConfigurationManager _serverConfigManager; private readonly ServerConfigurationManager _serverConfigManager;
private readonly TextureDownscaleService _textureDownscaleService; private readonly TextureDownscaleService _textureDownscaleService;
private readonly ModelDecimationService _modelDecimationService;
private readonly PairStateCache _pairStateCache; private readonly PairStateCache _pairStateCache;
private readonly PairPerformanceMetricsCache _pairPerformanceMetricsCache; private readonly PairPerformanceMetricsCache _pairPerformanceMetricsCache;
private readonly PenumbraTempCollectionJanitor _tempCollectionJanitor;
private readonly LightlessConfigService _configService;
private readonly XivDataAnalyzer _modelAnalyzer;
private readonly IFramework _framework;
public PairHandlerAdapterFactory( public PairHandlerAdapterFactory(
ILoggerFactory loggerFactory, ILoggerFactory loggerFactory,
@@ -50,20 +40,14 @@ internal sealed class PairHandlerAdapterFactory : IPairHandlerAdapterFactory
FileDownloadManagerFactory fileDownloadManagerFactory, FileDownloadManagerFactory fileDownloadManagerFactory,
PluginWarningNotificationService pluginWarningNotificationManager, PluginWarningNotificationService pluginWarningNotificationManager,
IServiceProvider serviceProvider, IServiceProvider serviceProvider,
IFramework framework,
IHostApplicationLifetime lifetime, IHostApplicationLifetime lifetime,
FileCacheManager fileCacheManager, FileCacheManager fileCacheManager,
PlayerPerformanceConfigService playerPerformanceConfigService,
PlayerPerformanceService playerPerformanceService, PlayerPerformanceService playerPerformanceService,
PairProcessingLimiter pairProcessingLimiter, PairProcessingLimiter pairProcessingLimiter,
ServerConfigurationManager serverConfigManager, ServerConfigurationManager serverConfigManager,
TextureDownscaleService textureDownscaleService, TextureDownscaleService textureDownscaleService,
ModelDecimationService modelDecimationService,
PairStateCache pairStateCache, PairStateCache pairStateCache,
PairPerformanceMetricsCache pairPerformanceMetricsCache, PairPerformanceMetricsCache pairPerformanceMetricsCache)
PenumbraTempCollectionJanitor tempCollectionJanitor,
XivDataAnalyzer modelAnalyzer,
LightlessConfigService configService)
{ {
_loggerFactory = loggerFactory; _loggerFactory = loggerFactory;
_mediator = mediator; _mediator = mediator;
@@ -73,27 +57,20 @@ internal sealed class PairHandlerAdapterFactory : IPairHandlerAdapterFactory
_fileDownloadManagerFactory = fileDownloadManagerFactory; _fileDownloadManagerFactory = fileDownloadManagerFactory;
_pluginWarningNotificationManager = pluginWarningNotificationManager; _pluginWarningNotificationManager = pluginWarningNotificationManager;
_serviceProvider = serviceProvider; _serviceProvider = serviceProvider;
_framework = framework;
_lifetime = lifetime; _lifetime = lifetime;
_fileCacheManager = fileCacheManager; _fileCacheManager = fileCacheManager;
_playerPerformanceConfigService = playerPerformanceConfigService;
_playerPerformanceService = playerPerformanceService; _playerPerformanceService = playerPerformanceService;
_pairProcessingLimiter = pairProcessingLimiter; _pairProcessingLimiter = pairProcessingLimiter;
_serverConfigManager = serverConfigManager; _serverConfigManager = serverConfigManager;
_textureDownscaleService = textureDownscaleService; _textureDownscaleService = textureDownscaleService;
_modelDecimationService = modelDecimationService;
_pairStateCache = pairStateCache; _pairStateCache = pairStateCache;
_pairPerformanceMetricsCache = pairPerformanceMetricsCache; _pairPerformanceMetricsCache = pairPerformanceMetricsCache;
_tempCollectionJanitor = tempCollectionJanitor;
_modelAnalyzer = modelAnalyzer;
_configService = configService;
} }
public IPairHandlerAdapter Create(string ident) public IPairHandlerAdapter Create(string ident)
{ {
var downloadManager = _fileDownloadManagerFactory.Create(); var downloadManager = _fileDownloadManagerFactory.Create();
var dalamudUtilService = _serviceProvider.GetRequiredService<DalamudUtilService>(); var dalamudUtilService = _serviceProvider.GetRequiredService<DalamudUtilService>();
var actorObjectService = _serviceProvider.GetRequiredService<ActorObjectService>();
return new PairHandlerAdapter( return new PairHandlerAdapter(
_loggerFactory.CreateLogger<PairHandlerAdapter>(), _loggerFactory.CreateLogger<PairHandlerAdapter>(),
_mediator, _mediator,
@@ -104,20 +81,13 @@ internal sealed class PairHandlerAdapterFactory : IPairHandlerAdapterFactory
downloadManager, downloadManager,
_pluginWarningNotificationManager, _pluginWarningNotificationManager,
dalamudUtilService, dalamudUtilService,
_framework,
actorObjectService,
_lifetime, _lifetime,
_fileCacheManager, _fileCacheManager,
_playerPerformanceConfigService,
_playerPerformanceService, _playerPerformanceService,
_pairProcessingLimiter, _pairProcessingLimiter,
_serverConfigManager, _serverConfigManager,
_textureDownscaleService, _textureDownscaleService,
_modelDecimationService,
_pairStateCache, _pairStateCache,
_pairPerformanceMetricsCache, _pairPerformanceMetricsCache);
_tempCollectionJanitor,
_modelAnalyzer,
_configService);
} }
} }

View File

@@ -11,9 +11,7 @@ public sealed class PairHandlerRegistry : IDisposable
{ {
private readonly object _gate = new(); private readonly object _gate = new();
private readonly object _pendingGate = new(); private readonly object _pendingGate = new();
private readonly object _visibilityGate = new();
private readonly Dictionary<string, PairHandlerEntry> _entriesByIdent = new(StringComparer.Ordinal); 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 Dictionary<IPairHandlerAdapter, PairHandlerEntry> _entriesByHandler = new(ReferenceEqualityComparer.Instance);
private readonly IPairHandlerAdapterFactory _handlerFactory; private readonly IPairHandlerAdapterFactory _handlerFactory;
@@ -89,7 +87,7 @@ public sealed class PairHandlerRegistry : IDisposable
} }
if (handler.LastReceivedCharacterData is not null && if (handler.LastReceivedCharacterData is not null &&
(handler.LastAppliedApproximateVRAMBytes < 0 || handler.LastAppliedDataTris < 0 || handler.LastAppliedApproximateEffectiveTris < 0)) (handler.LastAppliedApproximateVRAMBytes < 0 || handler.LastAppliedDataTris < 0))
{ {
handler.ApplyLastReceivedData(forced: true); handler.ApplyLastReceivedData(forced: true);
} }
@@ -146,37 +144,6 @@ public sealed class PairHandlerRegistry : IDisposable
return PairOperationResult<PairUniqueIdentifier>.Ok(registration.PairIdent); 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) public PairOperationResult ApplyCharacterData(PairRegistration registration, OnlineUserCharaDataDto dto)
{ {
if (registration.CharacterIdent is null) if (registration.CharacterIdent is null)
@@ -333,7 +300,6 @@ public sealed class PairHandlerRegistry : IDisposable
lock (_gate) lock (_gate)
{ {
handlers = _entriesByHandler.Keys.ToList(); handlers = _entriesByHandler.Keys.ToList();
CancelAllInvisibleEvictions();
_entriesByIdent.Clear(); _entriesByIdent.Clear();
_entriesByHandler.Clear(); _entriesByHandler.Clear();
} }
@@ -366,7 +332,6 @@ public sealed class PairHandlerRegistry : IDisposable
lock (_gate) lock (_gate)
{ {
handlers = _entriesByHandler.Keys.ToList(); handlers = _entriesByHandler.Keys.ToList();
CancelAllInvisibleEvictions();
_entriesByIdent.Clear(); _entriesByIdent.Clear();
_entriesByHandler.Clear(); _entriesByHandler.Clear();
} }

View File

@@ -258,8 +258,7 @@ public sealed class PairLedger : DisposableMediatorSubscriberBase
if (handler.LastAppliedApproximateVRAMBytes >= 0 if (handler.LastAppliedApproximateVRAMBytes >= 0
&& handler.LastAppliedDataTris >= 0 && handler.LastAppliedDataTris >= 0
&& handler.LastAppliedApproximateEffectiveVRAMBytes >= 0 && handler.LastAppliedApproximateEffectiveVRAMBytes >= 0)
&& handler.LastAppliedApproximateEffectiveTris >= 0)
{ {
continue; continue;
} }

View File

@@ -5,8 +5,7 @@ namespace LightlessSync.PlayerData.Pairs;
public readonly record struct PairPerformanceMetrics( public readonly record struct PairPerformanceMetrics(
long TriangleCount, long TriangleCount,
long ApproximateVramBytes, long ApproximateVramBytes,
long ApproximateEffectiveVramBytes, long ApproximateEffectiveVramBytes);
long ApproximateEffectiveTris);
/// <summary> /// <summary>
/// caches performance metrics keyed by pair ident /// caches performance metrics keyed by pair ident

View File

@@ -50,7 +50,6 @@ public class VisibleUserDataDistributor : DisposableMediatorSubscriberBase
}); });
Mediator.Subscribe<ConnectedMessage>(this, (_) => PushToAllVisibleUsers()); Mediator.Subscribe<ConnectedMessage>(this, (_) => PushToAllVisibleUsers());
Mediator.Subscribe<PairOnlineMessage>(this, (msg) => HandlePairOnline(msg.PairIdent));
Mediator.Subscribe<DisconnectedMessage>(this, (_) => Mediator.Subscribe<DisconnectedMessage>(this, (_) =>
{ {
_fileTransferManager.CancelUpload(); _fileTransferManager.CancelUpload();
@@ -112,20 +111,6 @@ public class VisibleUserDataDistributor : DisposableMediatorSubscriberBase
_ = PushCharacterDataAsync(forced); _ = PushCharacterDataAsync(forced);
} }
private void HandlePairOnline(PairUniqueIdentifier pairIdent)
{
if (!_apiController.IsConnected || !_pairLedger.IsPairVisible(pairIdent))
{
return;
}
if (_pairLedger.GetHandler(pairIdent)?.UserData is { } user)
{
_usersToPushDataTo.Add(user);
PushCharacterData(forced: true);
}
}
private async Task PushCharacterDataAsync(bool forced = false) private async Task PushCharacterDataAsync(bool forced = false)
{ {
await _pushLock.WaitAsync(_runtimeCts.Token).ConfigureAwait(false); await _pushLock.WaitAsync(_runtimeCts.Token).ConfigureAwait(false);
@@ -167,6 +152,5 @@ public class VisibleUserDataDistributor : DisposableMediatorSubscriberBase
} }
} }
private List<UserData> GetVisibleUsers() private List<UserData> GetVisibleUsers() => [.. _pairLedger.GetVisiblePairs().Select(connection => connection.User)];
=> [.. _pairLedger.GetVisiblePairs().Where(connection => connection.IsOnline).Select(connection => connection.User)];
} }

View File

@@ -40,8 +40,6 @@ using System.Reflection;
using OtterTex; using OtterTex;
using LightlessSync.Services.LightFinder; using LightlessSync.Services.LightFinder;
using LightlessSync.Services.PairProcessing; using LightlessSync.Services.PairProcessing;
using LightlessSync.Services.ModelDecimation;
using LightlessSync.UI.Models;
namespace LightlessSync; namespace LightlessSync;
@@ -53,7 +51,7 @@ public sealed class Plugin : IDalamudPlugin
IFramework framework, IObjectTable objectTable, IClientState clientState, ICondition condition, IChatGui chatGui, IFramework framework, IObjectTable objectTable, IClientState clientState, ICondition condition, IChatGui chatGui,
IGameGui gameGui, IDtrBar dtrBar, IPluginLog pluginLog, ITargetManager targetManager, INotificationManager notificationManager, IGameGui gameGui, IDtrBar dtrBar, IPluginLog pluginLog, ITargetManager targetManager, INotificationManager notificationManager,
ITextureProvider textureProvider, IContextMenu contextMenu, IGameInteropProvider gameInteropProvider, IGameConfig gameConfig, 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); NativeDll.Initialize(pluginInterface.AssemblyLocation.DirectoryName);
if (!Directory.Exists(pluginInterface.ConfigDirectory.FullName)) if (!Directory.Exists(pluginInterface.ConfigDirectory.FullName))
@@ -106,9 +104,7 @@ public sealed class Plugin : IDalamudPlugin
services.AddSingleton(new WindowSystem("LightlessSync")); services.AddSingleton(new WindowSystem("LightlessSync"));
services.AddSingleton<FileDialogManager>(); services.AddSingleton<FileDialogManager>();
services.AddSingleton(new Dalamud.Localization("LightlessSync.Localization.", string.Empty, useEmbedded: true)); services.AddSingleton(new Dalamud.Localization("LightlessSync.Localization.", string.Empty, useEmbedded: true));
services.AddSingleton(framework);
services.AddSingleton(gameGui); services.AddSingleton(gameGui);
services.AddSingleton(gameInteropProvider);
services.AddSingleton(addonLifecycle); services.AddSingleton(addonLifecycle);
services.AddSingleton<IUiBuilder>(pluginInterface.UiBuilder); services.AddSingleton<IUiBuilder>(pluginInterface.UiBuilder);
@@ -119,7 +115,6 @@ public sealed class Plugin : IDalamudPlugin
services.AddSingleton<ProfileTagService>(); services.AddSingleton<ProfileTagService>();
services.AddSingleton<ApiController>(); services.AddSingleton<ApiController>();
services.AddSingleton<PerformanceCollectorService>(); services.AddSingleton<PerformanceCollectorService>();
services.AddSingleton<NameplateUpdateHookService>();
services.AddSingleton<HubFactory>(); services.AddSingleton<HubFactory>();
services.AddSingleton<FileUploadManager>(); services.AddSingleton<FileUploadManager>();
services.AddSingleton<FileTransferOrchestrator>(); services.AddSingleton<FileTransferOrchestrator>();
@@ -127,7 +122,6 @@ public sealed class Plugin : IDalamudPlugin
services.AddSingleton<LightlessProfileManager>(); services.AddSingleton<LightlessProfileManager>();
services.AddSingleton<TextureCompressionService>(); services.AddSingleton<TextureCompressionService>();
services.AddSingleton<TextureDownscaleService>(); services.AddSingleton<TextureDownscaleService>();
services.AddSingleton<ModelDecimationService>();
services.AddSingleton<GameObjectHandlerFactory>(); services.AddSingleton<GameObjectHandlerFactory>();
services.AddSingleton<FileDownloadManagerFactory>(); services.AddSingleton<FileDownloadManagerFactory>();
services.AddSingleton<PairProcessingLimiter>(); services.AddSingleton<PairProcessingLimiter>();
@@ -139,11 +133,8 @@ public sealed class Plugin : IDalamudPlugin
services.AddSingleton<TagHandler>(); services.AddSingleton<TagHandler>();
services.AddSingleton<PairRequestService>(); services.AddSingleton<PairRequestService>();
services.AddSingleton<ZoneChatService>(); services.AddSingleton<ZoneChatService>();
services.AddSingleton<ChatEmoteService>();
services.AddSingleton<IdDisplayHandler>(); services.AddSingleton<IdDisplayHandler>();
services.AddSingleton<PlayerPerformanceService>(); services.AddSingleton<PlayerPerformanceService>();
services.AddSingleton<PenumbraTempCollectionJanitor>();
services.AddSingleton<LocationShareService>();
services.AddSingleton<TextureMetadataHelper>(sp => services.AddSingleton<TextureMetadataHelper>(sp =>
new TextureMetadataHelper(sp.GetRequiredService<ILogger<TextureMetadataHelper>>(), gameData)); new TextureMetadataHelper(sp.GetRequiredService<ILogger<TextureMetadataHelper>>(), gameData));
@@ -180,8 +171,7 @@ public sealed class Plugin : IDalamudPlugin
services.AddSingleton(sp => new BlockedCharacterHandler( services.AddSingleton(sp => new BlockedCharacterHandler(
sp.GetRequiredService<ILogger<BlockedCharacterHandler>>(), sp.GetRequiredService<ILogger<BlockedCharacterHandler>>(),
gameInteropProvider, gameInteropProvider));
objectTable));
services.AddSingleton(sp => new IpcProvider( services.AddSingleton(sp => new IpcProvider(
sp.GetRequiredService<ILogger<IpcProvider>>(), sp.GetRequiredService<ILogger<IpcProvider>>(),
@@ -211,7 +201,6 @@ public sealed class Plugin : IDalamudPlugin
gameInteropProvider, gameInteropProvider,
objectTable, objectTable,
clientState, clientState,
condition,
sp.GetRequiredService<LightlessMediator>())); sp.GetRequiredService<LightlessMediator>()));
services.AddSingleton(sp => new DalamudUtilService( services.AddSingleton(sp => new DalamudUtilService(
@@ -224,7 +213,6 @@ public sealed class Plugin : IDalamudPlugin
gameData, gameData,
targetManager, targetManager,
gameConfig, gameConfig,
playerState,
sp.GetRequiredService<ActorObjectService>(), sp.GetRequiredService<ActorObjectService>(),
sp.GetRequiredService<BlockedCharacterHandler>(), sp.GetRequiredService<BlockedCharacterHandler>(),
sp.GetRequiredService<LightlessMediator>(), sp.GetRequiredService<LightlessMediator>(),
@@ -279,7 +267,6 @@ public sealed class Plugin : IDalamudPlugin
sp.GetRequiredService<ILogger<LightFinderPlateHandler>>(), sp.GetRequiredService<ILogger<LightFinderPlateHandler>>(),
addonLifecycle, addonLifecycle,
gameGui, gameGui,
clientState,
sp.GetRequiredService<LightlessConfigService>(), sp.GetRequiredService<LightlessConfigService>(),
sp.GetRequiredService<LightlessMediator>(), sp.GetRequiredService<LightlessMediator>(),
objectTable, objectTable,
@@ -287,22 +274,12 @@ public sealed class Plugin : IDalamudPlugin
pluginInterface, pluginInterface,
sp.GetRequiredService<PictomancyService>())); 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( services.AddSingleton(sp => new LightFinderScannerService(
sp.GetRequiredService<ILogger<LightFinderScannerService>>(), sp.GetRequiredService<ILogger<LightFinderScannerService>>(),
framework, framework,
sp.GetRequiredService<LightFinderService>(), sp.GetRequiredService<LightFinderService>(),
sp.GetRequiredService<LightlessMediator>(), sp.GetRequiredService<LightlessMediator>(),
sp.GetRequiredService<LightFinderPlateHandler>(), sp.GetRequiredService<LightFinderPlateHandler>(),
sp.GetRequiredService<LightFinderNativePlateHandler>(),
sp.GetRequiredService<ActorObjectService>())); sp.GetRequiredService<ActorObjectService>()));
services.AddSingleton(sp => new ContextMenuService( services.AddSingleton(sp => new ContextMenuService(
@@ -320,10 +297,7 @@ public sealed class Plugin : IDalamudPlugin
sp.GetRequiredService<LightFinderScannerService>(), sp.GetRequiredService<LightFinderScannerService>(),
sp.GetRequiredService<LightFinderService>(), sp.GetRequiredService<LightFinderService>(),
sp.GetRequiredService<LightlessProfileManager>(), sp.GetRequiredService<LightlessProfileManager>(),
sp.GetRequiredService<LightlessMediator>(), sp.GetRequiredService<LightlessMediator>()));
chatGui,
sp.GetRequiredService<NotificationService>())
);
// IPC callers / manager // IPC callers / manager
services.AddSingleton(sp => new IpcCallerPenumbra( services.AddSingleton(sp => new IpcCallerPenumbra(
@@ -377,11 +351,6 @@ public sealed class Plugin : IDalamudPlugin
sp.GetRequiredService<DalamudUtilService>(), sp.GetRequiredService<DalamudUtilService>(),
sp.GetRequiredService<LightlessMediator>())); sp.GetRequiredService<LightlessMediator>()));
services.AddSingleton(sp => new IpcCallerLifestream(
pluginInterface,
sp.GetRequiredService<LightlessMediator>(),
sp.GetRequiredService<ILogger<IpcCallerLifestream>>()));
services.AddSingleton(sp => new IpcManager( services.AddSingleton(sp => new IpcManager(
sp.GetRequiredService<ILogger<IpcManager>>(), sp.GetRequiredService<ILogger<IpcManager>>(),
sp.GetRequiredService<LightlessMediator>(), sp.GetRequiredService<LightlessMediator>(),
@@ -392,9 +361,7 @@ public sealed class Plugin : IDalamudPlugin
sp.GetRequiredService<IpcCallerHonorific>(), sp.GetRequiredService<IpcCallerHonorific>(),
sp.GetRequiredService<IpcCallerMoodles>(), sp.GetRequiredService<IpcCallerMoodles>(),
sp.GetRequiredService<IpcCallerPetNames>(), sp.GetRequiredService<IpcCallerPetNames>(),
sp.GetRequiredService<IpcCallerBrio>(), sp.GetRequiredService<IpcCallerBrio>()));
sp.GetRequiredService<IpcCallerLifestream>()
));
// Notifications / HTTP // Notifications / HTTP
services.AddSingleton(sp => new NotificationService( services.AddSingleton(sp => new NotificationService(
@@ -491,12 +458,19 @@ public sealed class Plugin : IDalamudPlugin
sp.GetRequiredService<LightlessConfigService>(), sp.GetRequiredService<LightlessConfigService>(),
sp.GetRequiredService<UiSharedService>(), sp.GetRequiredService<UiSharedService>(),
sp.GetRequiredService<ApiController>(), sp.GetRequiredService<ApiController>(),
sp.GetRequiredService<LightFinderScannerService>()));
services.AddScoped<WindowMediatorSubscriberBase, SyncshellFinderUI>(sp => new SyncshellFinderUI(
sp.GetRequiredService<ILogger<SyncshellFinderUI>>(),
sp.GetRequiredService<LightlessMediator>(),
sp.GetRequiredService<PerformanceCollectorService>(),
sp.GetRequiredService<LightFinderService>(),
sp.GetRequiredService<UiSharedService>(),
sp.GetRequiredService<ApiController>(),
sp.GetRequiredService<LightFinderScannerService>(), sp.GetRequiredService<LightFinderScannerService>(),
sp.GetRequiredService<PairUiService>(), sp.GetRequiredService<PairUiService>(),
sp.GetRequiredService<DalamudUtilService>(), sp.GetRequiredService<DalamudUtilService>(),
sp.GetRequiredService<LightlessProfileManager>(), sp.GetRequiredService<LightlessProfileManager>()));
sp.GetRequiredService<ActorObjectService>(),
sp.GetRequiredService<LightFinderPlateHandler>()));
services.AddScoped<IPopupHandler, BanUserPopupHandler>(); services.AddScoped<IPopupHandler, BanUserPopupHandler>();
services.AddScoped<IPopupHandler, CensusPopupHandler>(); services.AddScoped<IPopupHandler, CensusPopupHandler>();
@@ -553,9 +527,9 @@ public sealed class Plugin : IDalamudPlugin
clientState, clientState,
gameGui, gameGui,
objectTable, objectTable,
gameInteropProvider,
sp.GetRequiredService<LightlessMediator>(), sp.GetRequiredService<LightlessMediator>(),
sp.GetRequiredService<PairUiService>(), sp.GetRequiredService<PairUiService>()));
sp.GetRequiredService<NameplateUpdateHookService>()));
// Hosted services // Hosted services
services.AddHostedService(sp => sp.GetRequiredService<ConfigurationSaveService>()); services.AddHostedService(sp => sp.GetRequiredService<ConfigurationSaveService>());
@@ -574,7 +548,6 @@ public sealed class Plugin : IDalamudPlugin
services.AddHostedService(sp => sp.GetRequiredService<ContextMenuService>()); services.AddHostedService(sp => sp.GetRequiredService<ContextMenuService>());
services.AddHostedService(sp => sp.GetRequiredService<LightFinderService>()); services.AddHostedService(sp => sp.GetRequiredService<LightFinderService>());
services.AddHostedService(sp => sp.GetRequiredService<LightFinderPlateHandler>()); services.AddHostedService(sp => sp.GetRequiredService<LightFinderPlateHandler>());
services.AddHostedService(sp => sp.GetRequiredService<LightFinderNativePlateHandler>());
}).Build(); }).Build();
_ = _host.StartAsync(); _ = _host.StartAsync();
@@ -582,6 +555,7 @@ public sealed class Plugin : IDalamudPlugin
public void Dispose() public void Dispose()
{ {
_host.StopAsync().ContinueWith(_ => _host.Dispose()).Wait(TimeSpan.FromSeconds(5)); _host.StopAsync().GetAwaiter().GetResult();
_host.Dispose();
} }
} }

File diff suppressed because it is too large Load Diff

View File

@@ -1,16 +1,13 @@
using Dalamud.Game.ClientState.Objects.SubKinds; using Dalamud.Game.ClientState.Objects.SubKinds;
using K4os.Compression.LZ4.Legacy; using K4os.Compression.LZ4.Legacy;
using LightlessSync.API.Data; using LightlessSync.API.Data;
using LightlessSync.API.Data.Enum; using LightlessSync.API.Data.Enum;
using LightlessSync.API.Dto.CharaData; using LightlessSync.API.Dto.CharaData;
using LightlessSync.FileCache; using LightlessSync.FileCache;
using LightlessSync.LightlessConfiguration.Models;
using LightlessSync.PlayerData.Factories; using LightlessSync.PlayerData.Factories;
using LightlessSync.PlayerData.Handlers; using LightlessSync.PlayerData.Handlers;
using LightlessSync.Services.CharaData; using LightlessSync.Services.CharaData;
using LightlessSync.Services.CharaData.Models; using LightlessSync.Services.CharaData.Models;
using LightlessSync.Services.Mediator;
using LightlessSync.UI.Models;
using LightlessSync.Utils; using LightlessSync.Utils;
using LightlessSync.WebAPI.Files; using LightlessSync.WebAPI.Files;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
@@ -27,11 +24,10 @@ public sealed class CharaDataFileHandler : IDisposable
private readonly ILogger<CharaDataFileHandler> _logger; private readonly ILogger<CharaDataFileHandler> _logger;
private readonly LightlessCharaFileDataFactory _lightlessCharaFileDataFactory; private readonly LightlessCharaFileDataFactory _lightlessCharaFileDataFactory;
private readonly PlayerDataFactory _playerDataFactory; private readonly PlayerDataFactory _playerDataFactory;
private readonly NotificationService _notificationService;
private int _globalFileCounter = 0; private int _globalFileCounter = 0;
public CharaDataFileHandler(ILogger<CharaDataFileHandler> logger, FileDownloadManagerFactory fileDownloadManagerFactory, FileUploadManager fileUploadManager, FileCacheManager fileCacheManager, 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(); _fileDownloadManager = fileDownloadManagerFactory.Create();
_logger = logger; _logger = logger;
@@ -40,7 +36,6 @@ public sealed class CharaDataFileHandler : IDisposable
_dalamudUtilService = dalamudUtilService; _dalamudUtilService = dalamudUtilService;
_gameObjectHandlerFactory = gameObjectHandlerFactory; _gameObjectHandlerFactory = gameObjectHandlerFactory;
_playerDataFactory = playerDataFactory; _playerDataFactory = playerDataFactory;
_notificationService = notificationService;
_lightlessCharaFileDataFactory = new(fileCacheManager); _lightlessCharaFileDataFactory = new(fileCacheManager);
} }
@@ -253,161 +248,54 @@ public sealed class CharaDataFileHandler : IDisposable
} }
internal async Task SaveCharaFileAsync(string description, string filePath) 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 tempFilePath = filePath + ".tmp";
var overallStopwatch = System.Diagnostics.Stopwatch.StartNew();
try try
{ {
var data = await CreatePlayerData().ConfigureAwait(false);
if (data == null) return;
var lightlessCharaFileData = _lightlessCharaFileDataFactory.Create(description, data); var lightlessCharaFileData = _lightlessCharaFileDataFactory.Create(description, data);
LightlessCharaFileHeader output = new(LightlessCharaFileHeader.CurrentVersion, lightlessCharaFileData); 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 lz4 = new LZ4Stream(fs, LZ4StreamMode.Compress, LZ4StreamFlags.HighCompression);
using var writer = new BinaryWriter(lz4); using var writer = new BinaryWriter(lz4);
output.WriteToStream(writer); 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) foreach (var item in output.CharaFileData.Files)
{ {
fileIndex++;
var fileStopwatch = System.Diagnostics.Stopwatch.StartNew();
var file = _fileCacheManager.GetFileCacheByHash(item.Hash)!; 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:"); _logger.LogDebug("\tAssociated GamePaths:");
foreach (var path in item.GamePaths) foreach (var path in item.GamePaths)
{ {
_logger.LogDebug("\t{path}", path); _logger.LogDebug("\t{path}", path);
} }
using var fsRead = File.OpenRead(file.ResolvedFilepath); var fsRead = File.OpenRead(file.ResolvedFilepath);
using var br = new BinaryReader(fsRead); await using (fsRead.ConfigureAwait(false))
byte[] buffer = new byte[item.Length];
int bytesRead = br.Read(buffer, 0, item.Length);
if (bytesRead != item.Length)
{ {
_logger.LogWarning("Expected to read {expected} bytes but got {actual} bytes from {file}", item.Length, bytesRead, file.ResolvedFilepath); using var br = new BinaryReader(fsRead);
} byte[] buffer = new byte[item.Length];
br.Read(buffer, 0, item.Length);
writer.Write(buffer); 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();
} }
} }
var flushStopwatch = System.Diagnostics.Stopwatch.StartNew();
writer.Flush(); writer.Flush();
lz4.Flush(); await lz4.FlushAsync().ConfigureAwait(false);
fs.Flush(); await fs.FlushAsync().ConfigureAwait(false);
fs.Close(); fs.Close();
flushStopwatch.Stop();
_logger.LogInformation("Flush operations took {elapsed}ms", flushStopwatch.ElapsedMilliseconds);
var moveStopwatch = System.Diagnostics.Stopwatch.StartNew();
File.Move(tempFilePath, filePath, true); 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) catch (Exception ex)
{ {
overallStopwatch.Stop(); _logger.LogError(ex, "Failure Saving Lightless Chara File, deleting output");
_logger.LogError(ex, "Failure Saving Lightless Chara File after {elapsed}ms, deleting output", overallStopwatch.ElapsedMilliseconds); File.Delete(tempFilePath);
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"));
} }
} }
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) internal async Task<List<string>> UploadFiles(List<string> fileList, ValueProgress<string> uploadProgress, CancellationToken token)
{ {
return await _fileUploadManager.UploadFiles(fileList, uploadProgress, token).ConfigureAwait(false); 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; worldData.LocationInfo = loc;
if (_forceResendWorldData || worldData != _lastWorldData) if (_forceResendWorldData || worldData != _lastWorldData)

View File

@@ -254,7 +254,7 @@ public sealed partial class CharaDataManager : DisposableMediatorSubscriberBase
Logger.LogTrace("Attaching World data {data}", worldData); 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); Logger.LogTrace("World data serialized: {data}", worldData);

View File

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

View File

@@ -1,4 +1,3 @@
using FFXIVClientStructs.FFXIV.Client.Graphics.Render;
using LightlessSync.API.Data; using LightlessSync.API.Data;
using LightlessSync.API.Data.Enum; using LightlessSync.API.Data.Enum;
using LightlessSync.FileCache; using LightlessSync.FileCache;
@@ -28,7 +27,7 @@ public sealed class CharacterAnalyzer : MediatorSubscriberBase, IDisposable
{ {
_baseAnalysisCts = _baseAnalysisCts.CancelRecreate(); _baseAnalysisCts = _baseAnalysisCts.CancelRecreate();
var token = _baseAnalysisCts.Token; var token = _baseAnalysisCts.Token;
_ = Task.Run(async () => await BaseAnalysis(msg.CharacterData, token).ConfigureAwait(false), token); _ = BaseAnalysis(msg.CharacterData, token);
}); });
_fileCacheManager = fileCacheManager; _fileCacheManager = fileCacheManager;
_xivDataAnalyzer = modelAnalyzer; _xivDataAnalyzer = modelAnalyzer;
@@ -99,13 +98,11 @@ public sealed class CharacterAnalyzer : MediatorSubscriberBase, IDisposable
_analysisCts = null; _analysisCts = null;
if (print) PrintAnalysis(); if (print) PrintAnalysis();
} }
public void Dispose() public void Dispose()
{ {
_analysisCts.CancelDispose(); _analysisCts.CancelDispose();
_baseAnalysisCts.Dispose(); _baseAnalysisCts.Dispose();
} }
public async Task UpdateFileEntriesAsync(IEnumerable<string> filePaths, CancellationToken token) public async Task UpdateFileEntriesAsync(IEnumerable<string> filePaths, CancellationToken token)
{ {
var normalized = new HashSet<string>( var normalized = new HashSet<string>(
@@ -128,7 +125,6 @@ public sealed class CharacterAnalyzer : MediatorSubscriberBase, IDisposable
} }
} }
} }
private async Task BaseAnalysis(CharacterData charaData, CancellationToken token) private async Task BaseAnalysis(CharacterData charaData, CancellationToken token)
{ {
if (string.Equals(charaData.DataHash.Value, _lastDataHash, StringComparison.Ordinal)) return; if (string.Equals(charaData.DataHash.Value, _lastDataHash, StringComparison.Ordinal)) return;
@@ -140,47 +136,29 @@ public sealed class CharacterAnalyzer : MediatorSubscriberBase, IDisposable
{ {
token.ThrowIfCancellationRequested(); token.ThrowIfCancellationRequested();
var fileCacheEntries = (await _fileCacheManager var fileCacheEntries = (await _fileCacheManager.GetAllFileCachesByHashAsync(fileEntry.Hash, ignoreCacheEntries: true, validate: false, token).ConfigureAwait(false)).ToList();
.GetAllFileCachesByHashAsync(fileEntry.Hash, ignoreCacheEntries: true, validate: false, token) if (fileCacheEntries.Count == 0) continue;
.ConfigureAwait(false)) var filePath = fileCacheEntries[0].ResolvedFilepath;
.ToList(); FileInfo fi = new(filePath);
string ext = "unk?";
if (fileCacheEntries.Count == 0) try
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))
{ {
if (orig <= 0 && cached.Original > 0) orig = cached.Original; ext = fi.Extension[1..];
if (comp <= 0 && cached.Compressed > 0) comp = cached.Compressed; }
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; LastAnalysis[obj.Key] = data;
} }
@@ -189,7 +167,6 @@ public sealed class CharacterAnalyzer : MediatorSubscriberBase, IDisposable
Mediator.Publish(new CharacterDataAnalyzedMessage()); Mediator.Publish(new CharacterDataAnalyzedMessage());
_lastDataHash = charaData.DataHash.Value; _lastDataHash = charaData.DataHash.Value;
} }
private void RecalculateSummary() private void RecalculateSummary()
{ {
var builder = ImmutableDictionary.CreateBuilder<ObjectKind, CharacterAnalysisObjectSummary>(); var builder = ImmutableDictionary.CreateBuilder<ObjectKind, CharacterAnalysisObjectSummary>();
@@ -215,7 +192,6 @@ public sealed class CharacterAnalyzer : MediatorSubscriberBase, IDisposable
_latestSummary = new CharacterAnalysisSummary(builder.ToImmutable()); _latestSummary = new CharacterAnalysisSummary(builder.ToImmutable());
} }
private void PrintAnalysis() private void PrintAnalysis()
{ {
if (LastAnalysis.Count == 0) return; 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)))); 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."); 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 record FileDataEntry(string Hash, string FileType, List<string> GamePaths, List<string> FilePaths, long OriginalSize, long CompressedSize, long Triangles)
internal sealed class FileDataEntry
{ {
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 bool IsComputed => OriginalSize > 0 && CompressedSize > 0;
public async Task ComputeSizes(FileCacheManager fileCacheManager, CancellationToken token)
public FileDataEntry(
string hash,
string fileType,
List<string> gamePaths,
List<string> filePaths,
long originalSize,
long compressedSize,
long triangles,
IReadOnlyList<FileCacheEntity> cacheEntries)
{ {
Hash = hash; var compressedsize = await fileCacheManager.GetCompressedFileData(Hash, token).ConfigureAwait(false);
FileType = fileType; var normalSize = new FileInfo(FilePaths[0]).Length;
GamePaths = gamePaths; var entries = await fileCacheManager.GetAllFileCachesByHashAsync(Hash, ignoreCacheEntries: true, validate: false, token).ConfigureAwait(false);
FilePaths = filePaths; foreach (var entry in entries)
OriginalSize = originalSize; {
CompressedSize = compressedSize; entry.Size = normalSize;
Triangles = triangles; entry.CompressedSize = compressedsize.Item2.LongLength;
CacheEntries = cacheEntries; }
OriginalSize = normalSize;
CompressedSize = compressedsize.Item2.LongLength;
RefreshFormat();
} }
public long OriginalSize { get; private set; } = OriginalSize;
public async Task ComputeSizes(FileCacheManager fileCacheManager, CancellationToken token, bool force = false) public long CompressedSize { get; private set; } = CompressedSize;
{ public long Triangles { get; private set; } = Triangles;
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 Lazy<string> Format => _format ??= CreateFormatValue(); public Lazy<string> Format => _format ??= CreateFormatValue();
private Lazy<string>? _format; private Lazy<string>? _format;
public void RefreshFormat() => _format = CreateFormatValue(); public void RefreshFormat()
{
_format = CreateFormatValue();
}
private Lazy<string> CreateFormatValue() private Lazy<string> CreateFormatValue()
=> new(() => => new(() =>
{ {
if (!string.Equals(FileType, "tex", StringComparison.OrdinalIgnoreCase)) if (!string.Equals(FileType, "tex", StringComparison.Ordinal))
{
return string.Empty; return string.Empty;
}
try try
{ {

View File

@@ -1,770 +0,0 @@
using Dalamud.Interface.Textures.TextureWraps;
using LightlessSync.LightlessConfiguration;
using LightlessSync.UI;
using Microsoft.Extensions.Logging;
using System.Collections.Concurrent;
using System.Diagnostics;
using System.Text.Json;
using SixLabors.ImageSharp;
using SixLabors.ImageSharp.Formats.Gif;
using SixLabors.ImageSharp.Formats.Webp;
using SixLabors.ImageSharp.Metadata;
using SixLabors.ImageSharp.PixelFormats;
using SixLabors.ImageSharp.Processing;
namespace LightlessSync.Services.Chat;
public sealed class ChatEmoteService : IDisposable
{
private const string GlobalEmoteSetUrl = "https://7tv.io/v3/emote-sets/global";
private const int DefaultFrameDelayMs = 100;
private const int MinFrameDelayMs = 20;
private readonly ILogger<ChatEmoteService> _logger;
private readonly HttpClient _httpClient;
private readonly UiSharedService _uiSharedService;
private readonly ChatConfigService _chatConfigService;
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, ChatConfigService chatConfigService)
{
_logger = logger;
_httpClient = httpClient;
_uiSharedService = uiSharedService;
_chatConfigService = chatConfigService;
}
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;
}
var allowAnimation = _chatConfigService.Current.EnableAnimatedEmotes;
if (entry.TryGetTexture(allowAnimation, out texture))
{
if (allowAnimation && entry.NeedsAnimationLoad && !entry.HasAttemptedAnimation)
{
entry.EnsureLoading(allowAnimation, QueueEmoteDownload, allowWhenStaticLoaded: true);
}
return true;
}
entry.EnsureLoading(allowAnimation, QueueEmoteDownload);
return true;
}
public void Dispose()
{
foreach (var entry in _emotes.Values)
{
entry.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 source = TryBuildEmoteSource(emoteElement);
if (source is null || (!source.Value.HasStatic && !source.Value.HasAnimation))
{
continue;
}
_emotes.TryAdd(name, new EmoteEntry(name, source.Value));
}
}
catch (Exception ex)
{
_logger.LogWarning(ex, "Failed to load 7TV emote set");
}
}
private static EmoteSource? TryBuildEmoteSource(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 files = ReadEmoteFiles(filesElement);
if (files.Count == 0)
{
return null;
}
var animatedFile = PickBestAnimatedFile(files);
var animatedUrl = animatedFile is null ? null : BuildEmoteUrl(baseUrl, animatedFile.Value.Name);
var staticName = animatedFile?.StaticName;
if (string.IsNullOrWhiteSpace(staticName))
{
staticName = PickBestStaticFileName(files);
}
var staticUrl = string.IsNullOrWhiteSpace(staticName) ? null : BuildEmoteUrl(baseUrl, staticName);
if (string.IsNullOrWhiteSpace(animatedUrl) && string.IsNullOrWhiteSpace(staticUrl))
{
return null;
}
return new EmoteSource(staticUrl, animatedUrl);
}
private static string BuildEmoteUrl(string baseUrl, string fileName)
=> baseUrl.TrimEnd('/') + "/" + fileName;
private static List<EmoteFile> ReadEmoteFiles(JsonElement filesElement)
{
var files = new List<EmoteFile>();
foreach (var file in filesElement.EnumerateArray())
{
if (!file.TryGetProperty("name", out var nameElement))
{
continue;
}
var name = nameElement.GetString();
if (string.IsNullOrWhiteSpace(name))
{
continue;
}
string? staticName = null;
if (file.TryGetProperty("static_name", out var staticNameElement) && staticNameElement.ValueKind == JsonValueKind.String)
{
staticName = staticNameElement.GetString();
}
var frameCount = 1;
if (file.TryGetProperty("frame_count", out var frameCountElement) && frameCountElement.ValueKind == JsonValueKind.Number)
{
frameCountElement.TryGetInt32(out frameCount);
frameCount = Math.Max(frameCount, 1);
}
string? format = null;
if (file.TryGetProperty("format", out var formatElement) && formatElement.ValueKind == JsonValueKind.String)
{
format = formatElement.GetString();
}
files.Add(new EmoteFile(name, staticName, frameCount, format));
}
return files;
}
private static EmoteFile? PickBestAnimatedFile(IReadOnlyList<EmoteFile> files)
{
EmoteFile? webp1x = null;
EmoteFile? gif1x = null;
EmoteFile? webpFallback = null;
EmoteFile? gifFallback = null;
foreach (var file in files)
{
if (file.FrameCount <= 1 || !IsAnimatedFormatSupported(file))
{
continue;
}
if (file.Name.Equals("1x.webp", StringComparison.OrdinalIgnoreCase))
{
webp1x = file;
}
else if (file.Name.Equals("1x.gif", StringComparison.OrdinalIgnoreCase))
{
gif1x = file;
}
else if (file.Name.EndsWith(".webp", StringComparison.OrdinalIgnoreCase) && webpFallback is null)
{
webpFallback = file;
}
else if (file.Name.EndsWith(".gif", StringComparison.OrdinalIgnoreCase) && gifFallback is null)
{
gifFallback = file;
}
}
return webp1x ?? gif1x ?? webpFallback ?? gifFallback;
}
private static string? PickBestStaticFileName(IReadOnlyList<EmoteFile> files)
{
string? png1x = null;
string? webp1x = null;
string? gif1x = null;
string? pngFallback = null;
string? webpFallback = null;
string? gifFallback = null;
foreach (var file in files)
{
if (file.FrameCount > 1)
{
continue;
}
var name = file.StaticName ?? file.Name;
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.Equals("1x.gif", StringComparison.OrdinalIgnoreCase))
{
gif1x = 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;
}
else if (name.EndsWith(".gif", StringComparison.OrdinalIgnoreCase) && gifFallback is null)
{
gifFallback = name;
}
}
return png1x ?? webp1x ?? gif1x ?? pngFallback ?? webpFallback ?? gifFallback;
}
private static bool IsAnimatedFormatSupported(EmoteFile file)
{
if (!string.IsNullOrWhiteSpace(file.Format))
{
return file.Format.Equals("WEBP", StringComparison.OrdinalIgnoreCase)
|| file.Format.Equals("GIF", StringComparison.OrdinalIgnoreCase);
}
return file.Name.EndsWith(".webp", StringComparison.OrdinalIgnoreCase)
|| file.Name.EndsWith(".gif", StringComparison.OrdinalIgnoreCase);
}
private readonly record struct EmoteSource(string? StaticUrl, string? AnimatedUrl)
{
public bool HasStatic => !string.IsNullOrWhiteSpace(StaticUrl);
public bool HasAnimation => !string.IsNullOrWhiteSpace(AnimatedUrl);
}
private readonly record struct EmoteFile(string Name, string? StaticName, int FrameCount, string? Format);
private void QueueEmoteDownload(EmoteEntry entry, bool allowAnimation)
{
_ = Task.Run(async () =>
{
await _downloadGate.WaitAsync().ConfigureAwait(false);
try
{
if (allowAnimation)
{
if (entry.HasAnimatedSource)
{
entry.MarkAnimationAttempted();
if (await TryLoadAnimatedEmoteAsync(entry).ConfigureAwait(false))
{
return;
}
}
if (entry.HasStaticSource && !entry.HasStaticTexture && await TryLoadStaticEmoteAsync(entry).ConfigureAwait(false))
{
return;
}
}
else
{
if (entry.HasStaticSource && await TryLoadStaticEmoteAsync(entry).ConfigureAwait(false))
{
return;
}
if (entry.HasAnimatedSource)
{
entry.MarkAnimationAttempted();
if (await TryLoadAnimatedEmoteAsync(entry).ConfigureAwait(false))
{
return;
}
}
}
entry.MarkFailed();
}
catch (Exception ex)
{
_logger.LogDebug(ex, "Failed to load 7TV emote {Emote}", entry.Code);
entry.MarkFailed();
}
finally
{
_downloadGate.Release();
}
});
}
private async Task<bool> TryLoadAnimatedEmoteAsync(EmoteEntry entry)
{
if (string.IsNullOrWhiteSpace(entry.AnimatedUrl))
{
return false;
}
try
{
var data = await _httpClient.GetByteArrayAsync(entry.AnimatedUrl).ConfigureAwait(false);
var isWebp = entry.AnimatedUrl.EndsWith(".webp", StringComparison.OrdinalIgnoreCase);
if (!TryDecodeAnimation(data, isWebp, out var animation))
{
return false;
}
entry.SetAnimation(animation);
return true;
}
catch (Exception ex)
{
_logger.LogDebug(ex, "Failed to decode animated 7TV emote {Emote}", entry.Code);
return false;
}
}
private async Task<bool> TryLoadStaticEmoteAsync(EmoteEntry entry)
{
if (string.IsNullOrWhiteSpace(entry.StaticUrl))
{
return false;
}
try
{
var data = await _httpClient.GetByteArrayAsync(entry.StaticUrl).ConfigureAwait(false);
var texture = _uiSharedService.LoadImage(data);
entry.SetStaticTexture(texture);
return true;
}
catch (Exception ex)
{
_logger.LogDebug(ex, "Failed to decode static 7TV emote {Emote}", entry.Code);
return false;
}
}
private bool TryDecodeAnimation(byte[] data, bool isWebp, out EmoteAnimation? animation)
{
animation = null;
List<EmoteFrame>? frames = null;
try
{
Image<Rgba32> image;
if (isWebp)
{
using var stream = new MemoryStream(data);
image = WebpDecoder.Instance.Decode<Rgba32>(
new WebpDecoderOptions { BackgroundColorHandling = BackgroundColorHandling.Ignore },
stream);
}
else
{
image = Image.Load<Rgba32>(data);
}
using (image)
{
if (image.Frames.Count <= 1)
{
return false;
}
using var composite = new Image<Rgba32>(image.Width, image.Height, Color.Transparent);
Image<Rgba32>? restoreCanvas = null;
GifDisposalMethod? pendingGifDisposal = null;
WebpDisposalMethod? pendingWebpDisposal = null;
frames = new List<EmoteFrame>(image.Frames.Count);
for (var i = 0; i < image.Frames.Count; i++)
{
var frameMetadata = image.Frames[i].Metadata;
var delayMs = GetFrameDelayMs(frameMetadata);
ApplyDisposal(composite, ref restoreCanvas, pendingGifDisposal, pendingWebpDisposal);
GifDisposalMethod? currentGifDisposal = null;
WebpDisposalMethod? currentWebpDisposal = null;
var blendMethod = WebpBlendMethod.Over;
if (isWebp)
{
if (frameMetadata.TryGetWebpFrameMetadata(out var webpMetadata))
{
currentWebpDisposal = webpMetadata.DisposalMethod;
blendMethod = webpMetadata.BlendMethod;
}
}
else if (frameMetadata.TryGetGifMetadata(out var gifMetadata))
{
currentGifDisposal = gifMetadata.DisposalMethod;
}
if (currentGifDisposal == GifDisposalMethod.RestoreToPrevious)
{
restoreCanvas?.Dispose();
restoreCanvas = composite.Clone();
}
using var frameImage = image.Frames.CloneFrame(i);
var alphaMode = blendMethod == WebpBlendMethod.Source
? PixelAlphaCompositionMode.Src
: PixelAlphaCompositionMode.SrcOver;
composite.Mutate(ctx => ctx.DrawImage(frameImage, PixelColorBlendingMode.Normal, alphaMode, 1f));
using var renderedFrame = composite.Clone();
using var ms = new MemoryStream();
renderedFrame.SaveAsPng(ms);
var texture = _uiSharedService.LoadImage(ms.ToArray());
frames.Add(new EmoteFrame(texture, delayMs));
pendingGifDisposal = currentGifDisposal;
pendingWebpDisposal = currentWebpDisposal;
}
restoreCanvas?.Dispose();
animation = new EmoteAnimation(frames);
return true;
}
}
catch
{
if (frames is not null)
{
foreach (var frame in frames)
{
frame.Texture.Dispose();
}
}
return false;
}
}
private static int GetFrameDelayMs(ImageFrameMetadata metadata)
{
if (metadata.TryGetGifMetadata(out var gifMetadata))
{
var delayMs = (long)gifMetadata.FrameDelay * 10L;
return NormalizeFrameDelayMs(delayMs);
}
if (metadata.TryGetWebpFrameMetadata(out var webpMetadata))
{
return NormalizeFrameDelayMs(webpMetadata.FrameDelay);
}
return DefaultFrameDelayMs;
}
private static int NormalizeFrameDelayMs(long delayMs)
{
if (delayMs <= 0)
{
return DefaultFrameDelayMs;
}
var clamped = delayMs > int.MaxValue ? int.MaxValue : (int)delayMs;
return Math.Max(clamped, MinFrameDelayMs);
}
private static void ApplyDisposal(
Image<Rgba32> composite,
ref Image<Rgba32>? restoreCanvas,
GifDisposalMethod? gifDisposal,
WebpDisposalMethod? webpDisposal)
{
if (gifDisposal is not null)
{
switch (gifDisposal)
{
case GifDisposalMethod.RestoreToBackground:
composite.Mutate(ctx => ctx.BackgroundColor(Color.Transparent));
break;
case GifDisposalMethod.RestoreToPrevious:
if (restoreCanvas is not null)
{
composite.Mutate(ctx => ctx.BackgroundColor(Color.Transparent));
var restoreSnapshot = restoreCanvas;
composite.Mutate(ctx => ctx.DrawImage(restoreSnapshot, PixelColorBlendingMode.Normal, PixelAlphaCompositionMode.Src, 1f));
restoreCanvas.Dispose();
restoreCanvas = null;
}
break;
}
}
else if (webpDisposal == WebpDisposalMethod.RestoreToBackground)
{
composite.Mutate(ctx => ctx.BackgroundColor(Color.Transparent));
}
}
private sealed class EmoteAnimation : IDisposable
{
private readonly EmoteFrame[] _frames;
private readonly int _durationMs;
private readonly long _startTimestamp;
public EmoteAnimation(IReadOnlyList<EmoteFrame> frames)
{
_frames = frames.ToArray();
_durationMs = Math.Max(1, frames.Sum(frame => frame.DurationMs));
_startTimestamp = Stopwatch.GetTimestamp();
}
public IDalamudTextureWrap? GetCurrentFrame()
{
if (_frames.Length == 0)
{
return null;
}
if (_frames.Length == 1)
{
return _frames[0].Texture;
}
var elapsedTicks = Stopwatch.GetTimestamp() - _startTimestamp;
var elapsedMs = (elapsedTicks * 1000L) / Stopwatch.Frequency;
var targetMs = (int)(elapsedMs % _durationMs);
var accumulated = 0;
foreach (var frame in _frames)
{
accumulated += frame.DurationMs;
if (targetMs < accumulated)
{
return frame.Texture;
}
}
return _frames[^1].Texture;
}
public IDalamudTextureWrap? GetStaticFrame()
{
if (_frames.Length == 0)
{
return null;
}
return _frames[0].Texture;
}
public void Dispose()
{
foreach (var frame in _frames)
{
frame.Texture.Dispose();
}
}
}
private readonly record struct EmoteFrame(IDalamudTextureWrap Texture, int DurationMs);
private sealed class EmoteEntry : IDisposable
{
private int _loadingState;
private int _animationAttempted;
private IDalamudTextureWrap? _staticTexture;
private EmoteAnimation? _animation;
public EmoteEntry(string code, EmoteSource source)
{
Code = code;
StaticUrl = source.StaticUrl;
AnimatedUrl = source.AnimatedUrl;
}
public string Code { get; }
public string? StaticUrl { get; }
public string? AnimatedUrl { get; }
public bool HasStaticSource => !string.IsNullOrWhiteSpace(StaticUrl);
public bool HasAnimatedSource => !string.IsNullOrWhiteSpace(AnimatedUrl);
public bool HasStaticTexture => _staticTexture is not null;
public bool HasAttemptedAnimation => Interlocked.CompareExchange(ref _animationAttempted, 0, 0) != 0;
public bool NeedsAnimationLoad => _animation is null && HasAnimatedSource;
public void MarkAnimationAttempted()
{
Interlocked.Exchange(ref _animationAttempted, 1);
}
public bool TryGetTexture(bool allowAnimation, out IDalamudTextureWrap? texture)
{
if (allowAnimation && _animation is not null)
{
texture = _animation.GetCurrentFrame();
return true;
}
if (_staticTexture is not null)
{
texture = _staticTexture;
return true;
}
if (!allowAnimation && _animation is not null)
{
texture = _animation.GetStaticFrame();
return true;
}
texture = null;
return false;
}
public void EnsureLoading(bool allowAnimation, Action<EmoteEntry, bool> queueDownload, bool allowWhenStaticLoaded = false)
{
if (_animation is not null)
{
return;
}
if (!allowWhenStaticLoaded && _staticTexture is not null)
{
return;
}
if (Interlocked.CompareExchange(ref _loadingState, 1, 0) != 0)
{
return;
}
queueDownload(this, allowAnimation);
}
public void SetAnimation(EmoteAnimation animation)
{
_staticTexture?.Dispose();
_staticTexture = null;
_animation?.Dispose();
_animation = animation;
Interlocked.Exchange(ref _loadingState, 0);
}
public void SetStaticTexture(IDalamudTextureWrap texture)
{
_staticTexture?.Dispose();
_staticTexture = texture;
Interlocked.Exchange(ref _loadingState, 0);
}
public void MarkFailed()
{
Interlocked.Exchange(ref _loadingState, 0);
}
public void Dispose()
{
_animation?.Dispose();
_staticTexture?.Dispose();
}
}
}

View File

@@ -1,8 +1,6 @@
using LightlessSync.API.Dto.Chat; using LightlessSync.API.Dto.Chat;
using LightlessSync.API.Data.Extensions;
using LightlessSync.Services.ActorTracking; using LightlessSync.Services.ActorTracking;
using LightlessSync.Services.Mediator; using LightlessSync.Services.Mediator;
using LightlessSync.Services.ServerConfiguration;
using LightlessSync.WebAPI; using LightlessSync.WebAPI;
using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
@@ -26,7 +24,6 @@ public sealed class ZoneChatService : DisposableMediatorSubscriberBase, IHostedS
private readonly ActorObjectService _actorObjectService; private readonly ActorObjectService _actorObjectService;
private readonly PairUiService _pairUiService; private readonly PairUiService _pairUiService;
private readonly ChatConfigService _chatConfigService; private readonly ChatConfigService _chatConfigService;
private readonly ServerConfigurationManager _serverConfigurationManager;
private readonly Lock _sync = new(); 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, bool> _lastPresenceStates = new(StringComparer.Ordinal);
private readonly Dictionary<string, string> _selfTokens = new(StringComparer.Ordinal); private readonly Dictionary<string, string> _selfTokens = new(StringComparer.Ordinal);
private readonly List<PendingSelfMessage> _pendingSelfMessages = new(); private readonly List<PendingSelfMessage> _pendingSelfMessages = new();
private readonly Dictionary<string, List<ChatMessageEntry>> _messageHistoryCache = new(StringComparer.Ordinal);
private List<ChatChannelSnapshot>? _cachedChannelSnapshots;
private bool _channelsSnapshotDirty = true;
private bool _isLoggedIn; private bool _isLoggedIn;
private bool _isConnected; private bool _isConnected;
@@ -57,8 +51,7 @@ public sealed class ZoneChatService : DisposableMediatorSubscriberBase, IHostedS
ApiController apiController, ApiController apiController,
DalamudUtilService dalamudUtilService, DalamudUtilService dalamudUtilService,
ActorObjectService actorObjectService, ActorObjectService actorObjectService,
PairUiService pairUiService, PairUiService pairUiService)
ServerConfigurationManager serverConfigurationManager)
: base(logger, mediator) : base(logger, mediator)
{ {
_apiController = apiController; _apiController = apiController;
@@ -66,7 +59,6 @@ public sealed class ZoneChatService : DisposableMediatorSubscriberBase, IHostedS
_actorObjectService = actorObjectService; _actorObjectService = actorObjectService;
_pairUiService = pairUiService; _pairUiService = pairUiService;
_chatConfigService = chatConfigService; _chatConfigService = chatConfigService;
_serverConfigurationManager = serverConfigurationManager;
_isLoggedIn = _dalamudUtilService.IsLoggedIn; _isLoggedIn = _dalamudUtilService.IsLoggedIn;
_isConnected = _apiController.IsConnected; _isConnected = _apiController.IsConnected;
@@ -77,11 +69,6 @@ public sealed class ZoneChatService : DisposableMediatorSubscriberBase, IHostedS
{ {
using (_sync.EnterScope()) using (_sync.EnterScope())
{ {
if (!_channelsSnapshotDirty && _cachedChannelSnapshots is not null)
{
return _cachedChannelSnapshots;
}
var snapshots = new List<ChatChannelSnapshot>(_channelOrder.Count); var snapshots = new List<ChatChannelSnapshot>(_channelOrder.Count);
foreach (var key in _channelOrder) foreach (var key in _channelOrder)
{ {
@@ -111,8 +98,6 @@ public sealed class ZoneChatService : DisposableMediatorSubscriberBase, IHostedS
state.Messages.ToList())); state.Messages.ToList()));
} }
_cachedChannelSnapshots = snapshots;
_channelsSnapshotDirty = false;
return snapshots; return snapshots;
} }
} }
@@ -150,8 +135,6 @@ public sealed class ZoneChatService : DisposableMediatorSubscriberBase, IHostedS
state.UnreadCount = 0; state.UnreadCount = 0;
_lastReadCounts[key] = state.Messages.Count; _lastReadCounts[key] = state.Messages.Count;
} }
MarkChannelsSnapshotDirtyLocked();
} }
} }
@@ -203,7 +186,6 @@ public sealed class ZoneChatService : DisposableMediatorSubscriberBase, IHostedS
if (!wasEnabled) if (!wasEnabled)
{ {
_chatEnabled = true; _chatEnabled = true;
MarkChannelsSnapshotDirtyLocked();
} }
} }
@@ -249,8 +231,6 @@ public sealed class ZoneChatService : DisposableMediatorSubscriberBase, IHostedS
state.IsAvailable = false; state.IsAvailable = false;
state.StatusText = "Chat services disabled"; state.StatusText = "Chat services disabled";
} }
MarkChannelsSnapshotDirtyLocked();
} }
UnregisterChatHandler(); UnregisterChatHandler();
@@ -576,7 +556,7 @@ public sealed class ZoneChatService : DisposableMediatorSubscriberBase, IHostedS
try try
{ {
var location = _dalamudUtilService.GetMapData(); var location = await _dalamudUtilService.GetMapDataAsync().ConfigureAwait(false);
var territoryId = (ushort)location.TerritoryId; var territoryId = (ushort)location.TerritoryId;
var worldId = (ushort)location.ServerId; var worldId = (ushort)location.ServerId;
@@ -702,7 +682,7 @@ public sealed class ZoneChatService : DisposableMediatorSubscriberBase, IHostedS
{ {
try try
{ {
var worldId = (ushort)_dalamudUtilService.GetWorldId(); var worldId = (ushort)await _dalamudUtilService.GetWorldIdAsync().ConfigureAwait(false);
return definition.Descriptor with { WorldId = worldId }; return definition.Descriptor with { WorldId = worldId };
} }
catch (Exception ex) catch (Exception ex)
@@ -737,7 +717,7 @@ public sealed class ZoneChatService : DisposableMediatorSubscriberBase, IHostedS
_zoneDefinitions[key] = new ZoneChannelDefinition(key, info.DisplayName ?? key, descriptor, territories); _zoneDefinitions[key] = new ZoneChannelDefinition(key, info.DisplayName ?? key, descriptor, territories);
} }
var territoryData = _dalamudUtilService.TerritoryDataEnglish.Value; var territoryData = _dalamudUtilService.TerritoryData.Value;
foreach (var kvp in territoryData) foreach (var kvp in territoryData)
{ {
foreach (var variant in EnumerateTerritoryKeys(kvp.Value)) foreach (var variant in EnumerateTerritoryKeys(kvp.Value))
@@ -781,7 +761,6 @@ public sealed class ZoneChatService : DisposableMediatorSubscriberBase, IHostedS
using (_sync.EnterScope()) using (_sync.EnterScope())
{ {
var remainingGroups = new HashSet<string>(_groupDefinitions.Keys, StringComparer.OrdinalIgnoreCase); var remainingGroups = new HashSet<string>(_groupDefinitions.Keys, StringComparer.OrdinalIgnoreCase);
var allowRemoval = _isConnected;
foreach (var info in infoList) foreach (var info in infoList)
{ {
@@ -797,19 +776,18 @@ public sealed class ZoneChatService : DisposableMediatorSubscriberBase, IHostedS
var key = BuildChannelKey(descriptor); var key = BuildChannelKey(descriptor);
if (!_channels.TryGetValue(key, out var state)) if (!_channels.TryGetValue(key, out var state))
{ {
state = new ChatChannelState(key, ChatChannelType.Group, info.DisplayName ?? groupId, descriptor); state = new ChatChannelState(key, ChatChannelType.Group, info.DisplayName ?? groupId, descriptor);
var restoredCount = RestoreCachedMessagesLocked(state); state.IsConnected = _chatEnabled && _isConnected;
state.IsConnected = _chatEnabled && _isConnected; state.IsAvailable = _chatEnabled && _isConnected;
state.IsAvailable = _chatEnabled && _isConnected; state.StatusText = !_chatEnabled
state.StatusText = !_chatEnabled ? "Chat services disabled"
? "Chat services disabled" : (_isConnected ? null : "Disconnected from chat server");
: (_isConnected ? null : "Disconnected from chat server"); _channels[key] = state;
_channels[key] = state; _lastReadCounts[key] = 0;
_lastReadCounts[key] = restoredCount > 0 ? state.Messages.Count : 0; if (_chatEnabled)
if (_chatEnabled) {
{ descriptorsToJoin.Add(descriptor);
descriptorsToJoin.Add(descriptor); }
}
} }
else 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); descriptorsToLeave.Add(state.Descriptor);
if (_channels.TryGetValue(key, out var state)) _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); _activeChannelKey = null;
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;
}
} }
_groupDefinitions.Remove(removedGroupId);
} }
_groupDefinitions.Remove(removedGroupId);
} }
} }
@@ -879,12 +853,6 @@ public sealed class ZoneChatService : DisposableMediatorSubscriberBase, IHostedS
var infos = new List<GroupChatChannelInfoDto>(groups.Count); var infos = new List<GroupChatChannelInfoDto>(groups.Count);
foreach (var group in groups) foreach (var group in groups)
{ {
// basically prune the channel if it's disabled
if (group.GroupPermissions.IsDisableChat())
{
continue;
}
var descriptor = new ChatChannelDescriptor var descriptor = new ChatChannelDescriptor
{ {
Type = ChatChannelType.Group, Type = ChatChannelType.Group,
@@ -1024,14 +992,13 @@ public sealed class ZoneChatService : DisposableMediatorSubscriberBase, IHostedS
descriptor.Type, descriptor.Type,
displayName, displayName,
descriptor.Type == ChatChannelType.Zone ? (_lastZoneDescriptor ?? descriptor) : descriptor); descriptor.Type == ChatChannelType.Zone ? (_lastZoneDescriptor ?? descriptor) : descriptor);
var restoredCount = RestoreCachedMessagesLocked(state);
state.IsConnected = _isConnected; state.IsConnected = _isConnected;
state.IsAvailable = descriptor.Type == ChatChannelType.Group && _isConnected; state.IsAvailable = descriptor.Type == ChatChannelType.Group && _isConnected;
state.StatusText = descriptor.Type == ChatChannelType.Zone ? ZoneUnavailableMessage : (_isConnected ? null : "Disconnected from chat server"); state.StatusText = descriptor.Type == ChatChannelType.Zone ? ZoneUnavailableMessage : (_isConnected ? null : "Disconnected from chat server");
_channels[key] = state; _channels[key] = state;
_lastReadCounts[key] = restoredCount > 0 ? state.Messages.Count : 0; _lastReadCounts[key] = 0;
publishChannelList = true; publishChannelList = true;
} }
@@ -1056,8 +1023,6 @@ public sealed class ZoneChatService : DisposableMediatorSubscriberBase, IHostedS
state.UnreadCount = Math.Min(Math.Max(unreadFromHistory, incrementalUnread), MaxUnreadCount); state.UnreadCount = Math.Min(Math.Max(unreadFromHistory, incrementalUnread), MaxUnreadCount);
state.HasUnread = state.UnreadCount > 0; state.HasUnread = state.UnreadCount > 0;
} }
MarkChannelsSnapshotDirtyLocked();
} }
Mediator.Publish(new ChatChannelMessageAdded(key, message)); Mediator.Publish(new ChatChannelMessageAdded(key, message));
@@ -1161,7 +1126,7 @@ public sealed class ZoneChatService : DisposableMediatorSubscriberBase, IHostedS
{ {
try try
{ {
return _dalamudUtilService.GetPlayerName(); return _dalamudUtilService.GetPlayerNameAsync().ConfigureAwait(false).GetAwaiter().GetResult();
} }
catch (Exception ex) 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.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; return dto.Sender.User.AliasOrUID;
} }
@@ -1248,25 +1204,9 @@ public sealed class ZoneChatService : DisposableMediatorSubscriberBase, IHostedS
{ {
_activeChannelKey = _channelOrder.Count > 0 ? _channelOrder[0] : null; _activeChannelKey = _channelOrder.Count > 0 ? _channelOrder[0] : null;
} }
MarkChannelsSnapshotDirtyLocked();
} }
private void MarkChannelsSnapshotDirty() private void PublishChannelListChanged() => Mediator.Publish(new ChatChannelsUpdated());
{
using (_sync.EnterScope())
{
_channelsSnapshotDirty = true;
}
}
private void MarkChannelsSnapshotDirtyLocked() => _channelsSnapshotDirty = true;
private void PublishChannelListChanged()
{
MarkChannelsSnapshotDirty();
Mediator.Publish(new ChatChannelsUpdated());
}
private static IEnumerable<string> EnumerateTerritoryKeys(string? value) private static IEnumerable<string> EnumerateTerritoryKeys(string? value)
{ {
@@ -1309,12 +1249,11 @@ public sealed class ZoneChatService : DisposableMediatorSubscriberBase, IHostedS
if (!_channels.TryGetValue(ZoneChannelKey, out var state)) if (!_channels.TryGetValue(ZoneChannelKey, out var state))
{ {
state = new ChatChannelState(ZoneChannelKey, ChatChannelType.Zone, "Zone Chat", new ChatChannelDescriptor { Type = ChatChannelType.Zone }); state = new ChatChannelState(ZoneChannelKey, ChatChannelType.Zone, "Zone Chat", new ChatChannelDescriptor { Type = ChatChannelType.Zone });
var restoredCount = RestoreCachedMessagesLocked(state);
state.IsConnected = _chatEnabled && _isConnected; state.IsConnected = _chatEnabled && _isConnected;
state.IsAvailable = false; state.IsAvailable = false;
state.StatusText = _chatEnabled ? ZoneUnavailableMessage : "Chat services disabled"; state.StatusText = _chatEnabled ? ZoneUnavailableMessage : "Chat services disabled";
_channels[ZoneChannelKey] = state; _channels[ZoneChannelKey] = state;
_lastReadCounts[ZoneChannelKey] = restoredCount > 0 ? state.Messages.Count : 0; _lastReadCounts[ZoneChannelKey] = 0;
UpdateChannelOrderLocked(); UpdateChannelOrderLocked();
} }
@@ -1323,11 +1262,6 @@ public sealed class ZoneChatService : DisposableMediatorSubscriberBase, IHostedS
private void RemoveZoneStateLocked() private void RemoveZoneStateLocked()
{ {
if (_channels.TryGetValue(ZoneChannelKey, out var existing))
{
CacheMessagesLocked(existing);
}
if (_channels.Remove(ZoneChannelKey)) if (_channels.Remove(ZoneChannelKey))
{ {
_lastReadCounts.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 private sealed class ChatChannelState
{ {
public ChatChannelState(string key, ChatChannelType type, string displayName, ChatChannelDescriptor descriptor) 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 analyze - Opens the Lightless Character Data Analysis window" + Environment.NewLine +
"\t /light settings - Opens the Lightless Settings window" + Environment.NewLine + "\t /light settings - Opens the Lightless Settings window" + Environment.NewLine +
"\t /light finder - Opens the Lightfinder 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,21 +4,21 @@ using Dalamud.Plugin;
using Dalamud.Plugin.Services; using Dalamud.Plugin.Services;
using LightlessSync.LightlessConfiguration; using LightlessSync.LightlessConfiguration;
using LightlessSync.LightlessConfiguration.Models; using LightlessSync.LightlessConfiguration.Models;
using LightlessSync.Services.LightFinder;
using LightlessSync.Services.Mediator; using LightlessSync.Services.Mediator;
using LightlessSync.UI;
using LightlessSync.UI.Services;
using LightlessSync.Utils; using LightlessSync.Utils;
using LightlessSync.WebAPI; using LightlessSync.WebAPI;
using Lumina.Excel.Sheets;
using LightlessSync.UI.Services;
using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
using LightlessSync.UI;
using LightlessSync.Services.LightFinder;
namespace LightlessSync.Services; namespace LightlessSync.Services;
internal class ContextMenuService : IHostedService internal class ContextMenuService : IHostedService
{ {
private readonly IContextMenu _contextMenu; private readonly IContextMenu _contextMenu;
private readonly IChatGui _chatGui;
private readonly IDalamudPluginInterface _pluginInterface; private readonly IDalamudPluginInterface _pluginInterface;
private readonly IDataManager _gameData; private readonly IDataManager _gameData;
private readonly ILogger<ContextMenuService> _logger; private readonly ILogger<ContextMenuService> _logger;
@@ -29,7 +29,6 @@ internal class ContextMenuService : IHostedService
private readonly ApiController _apiController; private readonly ApiController _apiController;
private readonly IObjectTable _objectTable; private readonly IObjectTable _objectTable;
private readonly LightlessConfigService _configService; private readonly LightlessConfigService _configService;
private readonly NotificationService _lightlessNotification;
private readonly LightFinderScannerService _broadcastScannerService; private readonly LightFinderScannerService _broadcastScannerService;
private readonly LightFinderService _broadcastService; private readonly LightFinderService _broadcastService;
private readonly LightlessProfileManager _lightlessProfileManager; private readonly LightlessProfileManager _lightlessProfileManager;
@@ -44,7 +43,7 @@ internal class ContextMenuService : IHostedService
ILogger<ContextMenuService> logger, ILogger<ContextMenuService> logger,
DalamudUtilService dalamudUtil, DalamudUtilService dalamudUtil,
ApiController apiController, ApiController apiController,
IObjectTable objectTable, IObjectTable objectTable,
LightlessConfigService configService, LightlessConfigService configService,
PairRequestService pairRequestService, PairRequestService pairRequestService,
PairUiService pairUiService, PairUiService pairUiService,
@@ -52,9 +51,7 @@ internal class ContextMenuService : IHostedService
LightFinderScannerService broadcastScannerService, LightFinderScannerService broadcastScannerService,
LightFinderService broadcastService, LightFinderService broadcastService,
LightlessProfileManager lightlessProfileManager, LightlessProfileManager lightlessProfileManager,
LightlessMediator mediator, LightlessMediator mediator)
IChatGui chatGui,
NotificationService lightlessNotification)
{ {
_contextMenu = contextMenu; _contextMenu = contextMenu;
_pluginInterface = pluginInterface; _pluginInterface = pluginInterface;
@@ -71,8 +68,6 @@ internal class ContextMenuService : IHostedService
_broadcastService = broadcastService; _broadcastService = broadcastService;
_lightlessProfileManager = lightlessProfileManager; _lightlessProfileManager = lightlessProfileManager;
_mediator = mediator; _mediator = mediator;
_chatGui = chatGui;
_lightlessNotification = lightlessNotification;
} }
public Task StartAsync(CancellationToken cancellationToken) public Task StartAsync(CancellationToken cancellationToken)
@@ -104,12 +99,6 @@ internal class ContextMenuService : IHostedService
if (!_pluginInterface.UiBuilder.ShouldModifyUi) if (!_pluginInterface.UiBuilder.ShouldModifyUi)
return; return;
if (!_configService.Current.EnableRightClickMenus)
{
_logger.LogTrace("Right-click menus are disabled in configuration.");
return;
}
if (args.AddonName != null) if (args.AddonName != null)
{ {
var addonName = args.AddonName; var addonName = args.AddonName;
@@ -140,6 +129,7 @@ internal class ContextMenuService : IHostedService
var snapshot = _pairUiService.GetSnapshot(); var snapshot = _pairUiService.GetSnapshot();
var pair = snapshot.PairsByUid.Values.FirstOrDefault(p => var pair = snapshot.PairsByUid.Values.FirstOrDefault(p =>
p.IsVisible &&
p.PlayerCharacterId != uint.MaxValue && p.PlayerCharacterId != uint.MaxValue &&
p.PlayerCharacterId == target.TargetObjectId); p.PlayerCharacterId == target.TargetObjectId);
@@ -171,8 +161,9 @@ internal class ContextMenuService : IHostedService
_logger.LogTrace("Cannot send pair request to {TargetName}@{World} while in PvP or GPose.", target.TargetName, target.TargetHomeWorld.RowId); _logger.LogTrace("Cannot send pair request to {TargetName}@{World} while in PvP or GPose.", target.TargetName, target.TargetHomeWorld.RowId);
return; return;
} }
if (!IsWorldValid(target.TargetHomeWorld.RowId)) var world = GetWorld(target.TargetHomeWorld.RowId);
if (!IsWorldValid(world))
{ {
_logger.LogTrace("Target player {TargetName}@{World} is on an invalid world.", target.TargetName, target.TargetHomeWorld.RowId); _logger.LogTrace("Target player {TargetName}@{World} is on an invalid world.", target.TargetName, target.TargetHomeWorld.RowId);
return; return;
@@ -208,24 +199,13 @@ internal class ContextMenuService : IHostedService
.Where(p => p.IsVisible && p.PlayerCharacterId != uint.MaxValue) .Where(p => p.IsVisible && p.PlayerCharacterId != uint.MaxValue)
.Select(p => (ulong)p.PlayerCharacterId)]; .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) private async Task HandleSelection(IMenuArgs args)
{ {
if (args.Target is not MenuTargetDefault target) if (args.Target is not MenuTargetDefault target)
return; return;
if (!target.TargetHomeWorld.IsValid || !IsWorldValid(target.TargetHomeWorld.RowId)) var world = GetWorld(target.TargetHomeWorld.RowId);
if (!IsWorldValid(world))
return; return;
try try
@@ -234,11 +214,11 @@ internal class ContextMenuService : IHostedService
if (targetData == null || targetData.Address == nint.Zero) if (targetData == null || targetData.Address == nint.Zero)
{ {
_logger.LogWarning("Target player {TargetName}@{World} not found in object table.", target.TargetName, target.TargetHomeWorld.Value.Name); _logger.LogWarning("Target player {TargetName}@{World} not found in object table.", target.TargetName, world.Name);
return; return;
} }
var senderCid = _dalamudUtil.GetCID().ToString().GetHash256(); var senderCid = (await _dalamudUtil.GetCIDAsync().ConfigureAwait(false)).ToString().GetHash256();
var receiverCid = DalamudUtilService.GetHashedCIDFromPlayerPointer(targetData.Address); var receiverCid = DalamudUtilService.GetHashedCIDFromPlayerPointer(targetData.Address);
_logger.LogInformation("Sending pair request: sender {SenderCid}, receiver {ReceiverCid}", senderCid, receiverCid); _logger.LogInformation("Sending pair request: sender {SenderCid}, receiver {ReceiverCid}", senderCid, receiverCid);
@@ -247,9 +227,6 @@ internal class ContextMenuService : IHostedService
{ {
_pairRequestService.RemoveRequest(receiverCid); _pairRequestService.RemoveRequest(receiverCid);
} }
// Notify in chat when NotificationService is disabled
NotifyInChat($"Pair request sent to {target.TargetName}@{target.TargetHomeWorld.Value.Name}.", NotificationType.Info);
} }
catch (Exception ex) catch (Exception ex)
{ {
@@ -309,8 +286,37 @@ internal class ContextMenuService : IHostedService
p.HomeWorld.RowId == target.TargetHomeWorld.RowId); p.HomeWorld.RowId == target.TargetHomeWorld.RowId);
} }
private bool IsWorldValid(uint worldId) private World GetWorld(uint worldId)
{ {
return _dalamudUtil.WorldData.Value.ContainsKey((ushort)worldId); var sheet = _gameData.GetExcelSheet<World>()!;
var luminaWorlds = sheet.Where(x =>
{
var dc = x.DataCenter.ValueNullable;
var name = x.Name.ExtractText();
var internalName = x.InternalName.ExtractText();
if (dc == null || dc.Value.Region == 0 || string.IsNullOrWhiteSpace(dc.Value.Name.ExtractText()))
return false;
if (string.IsNullOrWhiteSpace(name) || string.IsNullOrWhiteSpace(internalName))
return false;
if (name.Contains('-', StringComparison.Ordinal) || name.Contains('_', StringComparison.Ordinal))
return false;
return x.DataCenter.Value.Region != 5 || x.RowId > 3001 && x.RowId != 1200 && IsChineseJapaneseKoreanString(name);
});
return luminaWorlds.FirstOrDefault(x => x.RowId == worldId);
}
private static bool IsChineseJapaneseKoreanString(string text) => text.All(IsChineseJapaneseKoreanCharacter);
private static bool IsChineseJapaneseKoreanCharacter(char c) => c >= 0x4E00 && c <= 0x9FFF;
public static bool IsWorldValid(World world)
{
var name = world.Name.ToString();
return !string.IsNullOrWhiteSpace(name) && char.IsUpper(name[0]);
} }
} }

View File

@@ -1,13 +1,12 @@
using Dalamud.Game.ClientState.Conditions; using Dalamud.Game.ClientState.Conditions;
using Dalamud.Game.ClientState.Objects;
using Dalamud.Game.ClientState.Objects.SubKinds; using Dalamud.Game.ClientState.Objects.SubKinds;
using Dalamud.Game.ClientState.Objects.Types; using Dalamud.Game.ClientState.Objects.Types;
using Dalamud.Game.Text;
using Dalamud.Plugin.Services; using Dalamud.Plugin.Services;
using Dalamud.Utility; using Dalamud.Utility;
using FFXIVClientStructs.FFXIV.Client.Game; using FFXIVClientStructs.FFXIV.Client.Game;
using FFXIVClientStructs.FFXIV.Client.Game.Character; using FFXIVClientStructs.FFXIV.Client.Game.Character;
using FFXIVClientStructs.FFXIV.Client.Game.Control; using FFXIVClientStructs.FFXIV.Client.Game.Control;
using FFXIVClientStructs.FFXIV.Client.Game.UI;
using FFXIVClientStructs.FFXIV.Client.Graphics.Scene; using FFXIVClientStructs.FFXIV.Client.Graphics.Scene;
using FFXIVClientStructs.FFXIV.Client.UI.Agent; using FFXIVClientStructs.FFXIV.Client.UI.Agent;
using LightlessSync.API.Dto.CharaData; using LightlessSync.API.Dto.CharaData;
@@ -22,15 +21,11 @@ using LightlessSync.Utils;
using Lumina.Excel.Sheets; using Lumina.Excel.Sheets;
using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
using System.Diagnostics;
using System.Numerics; using System.Numerics;
using System.Runtime.CompilerServices; using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using System.Text; using System.Text;
using BattleNpcSubKind = FFXIVClientStructs.FFXIV.Client.Game.Object.BattleNpcSubKind;
using DalamudObjectKind = Dalamud.Game.ClientState.Objects.Enums.ObjectKind; using DalamudObjectKind = Dalamud.Game.ClientState.Objects.Enums.ObjectKind;
using GameObject = FFXIVClientStructs.FFXIV.Client.Game.Object.GameObject; using GameObject = FFXIVClientStructs.FFXIV.Client.Game.Object.GameObject;
using Map = Lumina.Excel.Sheets.Map;
using VisibilityFlags = FFXIVClientStructs.FFXIV.Client.Game.Object.VisibilityFlags; using VisibilityFlags = FFXIVClientStructs.FFXIV.Client.Game.Object.VisibilityFlags;
namespace LightlessSync.Services; namespace LightlessSync.Services;
@@ -42,7 +37,6 @@ public class DalamudUtilService : IHostedService, IMediatorSubscriber
private readonly ICondition _condition; private readonly ICondition _condition;
private readonly IDataManager _gameData; private readonly IDataManager _gameData;
private readonly IGameConfig _gameConfig; private readonly IGameConfig _gameConfig;
private readonly IPlayerState _playerState;
private readonly BlockedCharacterHandler _blockedCharacterHandler; private readonly BlockedCharacterHandler _blockedCharacterHandler;
private readonly IFramework _framework; private readonly IFramework _framework;
private readonly IGameGui _gameGui; private readonly IGameGui _gameGui;
@@ -62,12 +56,11 @@ public class DalamudUtilService : IHostedService, IMediatorSubscriber
private string _lastGlobalBlockReason = string.Empty; private string _lastGlobalBlockReason = string.Empty;
private ushort _lastZone = 0; private ushort _lastZone = 0;
private ushort _lastWorldId = 0; private ushort _lastWorldId = 0;
private uint _lastMapId = 0;
private bool _sentBetweenAreas = false; private bool _sentBetweenAreas = false;
private Lazy<ulong> _cid; private Lazy<ulong> _cid;
public DalamudUtilService(ILogger<DalamudUtilService> logger, IClientState clientState, IObjectTable objectTable, IFramework framework, 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, ActorObjectService actorObjectService, BlockedCharacterHandler blockedCharacterHandler, LightlessMediator mediator, PerformanceCollectorService performanceCollector,
LightlessConfigService configService, PlayerPerformanceConfigService playerPerformanceConfigService, Lazy<PairFactory> pairFactory) LightlessConfigService configService, PlayerPerformanceConfigService playerPerformanceConfigService, Lazy<PairFactory> pairFactory)
{ {
@@ -79,7 +72,6 @@ public class DalamudUtilService : IHostedService, IMediatorSubscriber
_condition = condition; _condition = condition;
_gameData = gameData; _gameData = gameData;
_gameConfig = gameConfig; _gameConfig = gameConfig;
_playerState = playerState;
_actorObjectService = actorObjectService; _actorObjectService = actorObjectService;
_targetManager = targetManager; _targetManager = targetManager;
_blockedCharacterHandler = blockedCharacterHandler; _blockedCharacterHandler = blockedCharacterHandler;
@@ -88,27 +80,53 @@ public class DalamudUtilService : IHostedService, IMediatorSubscriber
_configService = configService; _configService = configService;
_playerPerformanceConfigService = playerPerformanceConfigService; _playerPerformanceConfigService = playerPerformanceConfigService;
_pairFactory = pairFactory; _pairFactory = pairFactory;
var clientLanguage = _clientState.ClientLanguage;
WorldData = new(() => 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]) .Where(w => !w.Name.IsEmpty && w.DataCenter.RowId != 0 && (w.IsPublic || char.IsUpper(w.Name.ToString()[0])))
|| w is { RowId: > 1000, Region: 101 or 201 }))
.ToDictionary(w => (ushort)w.RowId, w => w.Name.ToString()); .ToDictionary(w => (ushort)w.RowId, w => w.Name.ToString());
}); });
JobData = new(() => JobData = new(() =>
{ {
return gameData.GetExcelSheet<ClassJob>(clientLanguage)! return gameData.GetExcelSheet<ClassJob>(Dalamud.Game.ClientLanguage.English)!
.ToDictionary(k => k.RowId, k => k.Name.ToString()); .ToDictionary(k => k.RowId, k => k.NameEnglish.ToString());
}); });
TerritoryData = new(() => BuildTerritoryData(clientLanguage)); TerritoryData = new(() =>
TerritoryDataEnglish = new(() => BuildTerritoryData(Dalamud.Game.ClientLanguage.English));
MapData = new(() => BuildMapData(clientLanguage));
ContentFinderData = new Lazy<Dictionary<uint, string>>(() =>
{ {
return _gameData.GetExcelSheet<TerritoryType>()! return gameData.GetExcelSheet<TerritoryType>(Dalamud.Game.ClientLanguage.English)!
.Where(w => w.RowId != 0 && !string.IsNullOrEmpty(w.ContentFinderCondition.ValueNullable?.Name.ToString())) .Where(w => w.RowId != 0)
.ToDictionary(w => w.RowId, w => w.ContentFinderCondition.Value.Name.ToString()); .ToDictionary(w => w.RowId, w =>
{
StringBuilder sb = new();
sb.Append(w.PlaceNameRegion.Value.Name);
if (w.PlaceName.ValueNullable != null)
{
sb.Append(" - ");
sb.Append(w.PlaceName.Value.Name);
}
return sb.ToString();
});
});
MapData = new(() =>
{
return gameData.GetExcelSheet<Map>(Dalamud.Game.ClientLanguage.English)!
.Where(w => w.RowId != 0)
.ToDictionary(w => w.RowId, w =>
{
StringBuilder sb = new();
sb.Append(w.PlaceNameRegion.Value.Name);
if (w.PlaceName.ValueNullable != null)
{
sb.Append(" - ");
sb.Append(w.PlaceName.Value.Name);
}
if (w.PlaceNameSub.ValueNullable != null && !string.IsNullOrEmpty(w.PlaceNameSub.Value.Name.ToString()))
{
sb.Append(" - ");
sb.Append(w.PlaceNameSub.Value.Name);
}
return (w, sb.ToString());
});
}); });
mediator.Subscribe<TargetPairMessage>(this, (msg) => mediator.Subscribe<TargetPairMessage>(this, (msg) =>
{ {
@@ -140,71 +158,6 @@ public class DalamudUtilService : IHostedService, IMediatorSubscriber
private Lazy<ulong> RebuildCID() => new(GetCID); private Lazy<ulong> RebuildCID() => new(GetCID);
public bool IsWine { get; init; } public bool IsWine { get; init; }
private Dictionary<uint, string> BuildTerritoryData(Dalamud.Game.ClientLanguage language)
{
var placeNames = _gameData.GetExcelSheet<PlaceName>(language)!;
return _gameData.GetExcelSheet<TerritoryType>(language)!
.Where(w => w.RowId != 0)
.ToDictionary(w => w.RowId, w =>
{
var regionName = GetPlaceName(placeNames, w.PlaceNameRegion.RowId);
var placeName = GetPlaceName(placeNames, w.PlaceName.RowId);
return BuildPlaceName(regionName, placeName, string.Empty);
});
}
private Dictionary<uint, (Map Map, string MapName)> BuildMapData(Dalamud.Game.ClientLanguage language)
{
var placeNames = _gameData.GetExcelSheet<PlaceName>(language)!;
return _gameData.GetExcelSheet<Map>(language)!
.Where(w => w.RowId != 0)
.ToDictionary(w => w.RowId, w =>
{
var regionName = GetPlaceName(placeNames, w.PlaceNameRegion.RowId);
var placeName = GetPlaceName(placeNames, w.PlaceName.RowId);
var subPlaceName = GetPlaceName(placeNames, w.PlaceNameSub.RowId);
var displayName = BuildPlaceName(regionName, placeName, subPlaceName);
return (w, displayName);
});
}
private static string GetPlaceName(Lumina.Excel.ExcelSheet<PlaceName> placeNames, uint rowId)
{
if (rowId == 0)
{
return string.Empty;
}
return placeNames.GetRow(rowId).Name.ToString();
}
private static string BuildPlaceName(string regionName, string placeName, string subPlaceName)
{
StringBuilder sb = new();
if (!string.IsNullOrWhiteSpace(regionName))
{
sb.Append(regionName);
}
if (!string.IsNullOrWhiteSpace(placeName))
{
if (sb.Length > 0)
{
sb.Append(" - ");
}
sb.Append(placeName);
}
if (!string.IsNullOrWhiteSpace(subPlaceName))
{
if (sb.Length > 0)
{
sb.Append(" - ");
}
sb.Append(subPlaceName);
}
return sb.ToString();
}
private bool ResolvePairAddress(Pair pair, out Pair resolvedPair, out nint address) private bool ResolvePairAddress(Pair pair, out Pair resolvedPair, out nint address)
{ {
resolvedPair = _pairFactory.Value.Create(pair.UniqueIdent) ?? pair; resolvedPair = _pairFactory.Value.Create(pair.UniqueIdent) ?? pair;
@@ -280,7 +233,6 @@ public class DalamudUtilService : IHostedService, IMediatorSubscriber
public bool IsAnythingDrawing { get; private set; } = false; public bool IsAnythingDrawing { get; private set; } = false;
public bool IsInCutscene { get; private set; } = false; public bool IsInCutscene { get; private set; } = false;
public bool IsInGpose { get; private set; } = false; public bool IsInGpose { get; private set; } = false;
public bool IsGameUiHidden => _gameGui.GameUiHidden;
public bool IsLoggedIn { get; private set; } public bool IsLoggedIn { get; private set; }
public bool IsOnFrameworkThread => _framework.IsInFrameworkUpdateThread; public bool IsOnFrameworkThread => _framework.IsInFrameworkUpdateThread;
public bool IsZoning => _condition[ConditionFlag.BetweenAreas] || _condition[ConditionFlag.BetweenAreas51]; public bool IsZoning => _condition[ConditionFlag.BetweenAreas] || _condition[ConditionFlag.BetweenAreas51];
@@ -293,9 +245,7 @@ public class DalamudUtilService : IHostedService, IMediatorSubscriber
public Lazy<Dictionary<uint, string>> JobData { get; private set; } public Lazy<Dictionary<uint, string>> JobData { get; private set; }
public Lazy<Dictionary<ushort, string>> WorldData { get; private set; } public Lazy<Dictionary<ushort, string>> WorldData { get; private set; }
public Lazy<Dictionary<uint, string>> TerritoryData { get; private set; } public Lazy<Dictionary<uint, string>> TerritoryData { get; private set; }
public Lazy<Dictionary<uint, string>> TerritoryDataEnglish { get; private set; }
public Lazy<Dictionary<uint, (Map Map, string MapName)>> MapData { get; private set; } public Lazy<Dictionary<uint, (Map Map, string MapName)>> MapData { get; private set; }
public Lazy<Dictionary<uint, string>> ContentFinderData { get; private set; }
public bool IsLodEnabled { get; private set; } public bool IsLodEnabled { get; private set; }
public LightlessMediator Mediator { get; } public LightlessMediator Mediator { get; }
@@ -314,7 +264,7 @@ public class DalamudUtilService : IHostedService, IMediatorSubscriber
return false; return false;
} }
if (!TerritoryDataEnglish.Value.TryGetValue(territoryId, out var name) || string.IsNullOrWhiteSpace(name)) if (!TerritoryData.Value.TryGetValue(territoryId, out var name) || string.IsNullOrWhiteSpace(name))
{ {
return false; return false;
} }
@@ -377,8 +327,8 @@ public class DalamudUtilService : IHostedService, IMediatorSubscriber
public IEnumerable<ICharacter?> GetGposeCharactersFromObjectTable() public IEnumerable<ICharacter?> GetGposeCharactersFromObjectTable()
{ {
foreach (var actor in _objectTable foreach (var actor in _actorObjectService.PlayerDescriptors
.Where(a => a.ObjectIndex > 200 && a.ObjectKind == DalamudObjectKind.Player)) .Where(a => a.ObjectKind == DalamudObjectKind.Player && a.ObjectIndex > 200))
{ {
var character = _objectTable.CreateObjectReference(actor.Address) as ICharacter; var character = _objectTable.CreateObjectReference(actor.Address) as ICharacter;
if (character != null) if (character != null)
@@ -389,7 +339,7 @@ public class DalamudUtilService : IHostedService, IMediatorSubscriber
public bool GetIsPlayerPresent() public bool GetIsPlayerPresent()
{ {
EnsureIsOnFramework(); EnsureIsOnFramework();
return _objectTable.LocalPlayer != null && _objectTable.LocalPlayer.IsValid() && _playerState.IsLoaded; return _objectTable.LocalPlayer != null && _objectTable.LocalPlayer.IsValid();
} }
public async Task<bool> GetIsPlayerPresentAsync() public async Task<bool> GetIsPlayerPresentAsync()
@@ -405,8 +355,7 @@ public class DalamudUtilService : IHostedService, IMediatorSubscriber
var playerAddress = playerPointer.Value; var playerAddress = playerPointer.Value;
var ownerEntityId = ((Character*)playerAddress)->EntityId; var ownerEntityId = ((Character*)playerAddress)->EntityId;
var candidateAddress = _objectTable.GetObjectAddress(((GameObject*)playerAddress)->ObjectIndex + 1); if (ownerEntityId == 0) return IntPtr.Zero;
if (ownerEntityId == 0) return candidateAddress;
if (playerAddress == _actorObjectService.LocalPlayerAddress) if (playerAddress == _actorObjectService.LocalPlayerAddress)
{ {
@@ -417,17 +366,6 @@ public class DalamudUtilService : IHostedService, IMediatorSubscriber
} }
} }
if (candidateAddress != nint.Zero)
{
var candidate = (GameObject*)candidateAddress;
var candidateKind = (DalamudObjectKind)candidate->ObjectKind;
if ((candidateKind == DalamudObjectKind.MountType || candidateKind == DalamudObjectKind.Companion)
&& ResolveOwnerId(candidate) == ownerEntityId)
{
return candidateAddress;
}
}
var ownedObject = FindOwnedObject(ownerEntityId, playerAddress, static kind => var ownedObject = FindOwnedObject(ownerEntityId, playerAddress, static kind =>
kind == DalamudObjectKind.MountType || kind == DalamudObjectKind.Companion); kind == DalamudObjectKind.MountType || kind == DalamudObjectKind.Companion);
if (ownedObject != nint.Zero) if (ownedObject != nint.Zero)
@@ -435,7 +373,7 @@ public class DalamudUtilService : IHostedService, IMediatorSubscriber
return ownedObject; return ownedObject;
} }
return candidateAddress; return _objectTable.GetObjectAddress(((GameObject*)playerAddress)->ObjectIndex + 1);
} }
public async Task<IntPtr> GetMinionOrMountAsync(IntPtr? playerPointer = null) public async Task<IntPtr> GetMinionOrMountAsync(IntPtr? playerPointer = null)
@@ -450,22 +388,7 @@ public class DalamudUtilService : IHostedService, IMediatorSubscriber
var mgr = CharacterManager.Instance(); var mgr = CharacterManager.Instance();
playerPointer ??= GetPlayerPtr(); playerPointer ??= GetPlayerPtr();
if (playerPointer == IntPtr.Zero || (IntPtr)mgr == IntPtr.Zero) return IntPtr.Zero; if (playerPointer == IntPtr.Zero || (IntPtr)mgr == IntPtr.Zero) return IntPtr.Zero;
return (IntPtr)mgr->LookupPetByOwnerObject((BattleChara*)playerPointer);
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);
} }
public async Task<IntPtr> GetPetAsync(IntPtr? playerPointer = null) public async Task<IntPtr> GetPetAsync(IntPtr? playerPointer = null)
@@ -502,60 +425,6 @@ public class DalamudUtilService : IHostedService, IMediatorSubscriber
return nint.Zero; 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) private static unsafe uint ResolveOwnerId(GameObject* gameObject)
{ {
if (gameObject == null) if (gameObject == null)
@@ -603,17 +472,30 @@ public class DalamudUtilService : IHostedService, IMediatorSubscriber
public string GetPlayerName() 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() 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) public static unsafe bool TryGetHashedCID(IPlayerCharacter? playerCharacter, out string hashedCid)
@@ -652,100 +534,54 @@ public class DalamudUtilService : IHostedService, IMediatorSubscriber
public uint GetHomeWorldId() public uint GetHomeWorldId()
{ {
return _playerState.HomeWorld.RowId; EnsureIsOnFramework();
return _objectTable.LocalPlayer?.HomeWorld.RowId ?? 0;
} }
public uint GetWorldId() public uint GetWorldId()
{ {
return _playerState.CurrentWorld.RowId; EnsureIsOnFramework();
return _objectTable.LocalPlayer!.CurrentWorld.RowId;
} }
public unsafe LocationInfo GetMapData() public unsafe LocationInfo GetMapData()
{ {
EnsureIsOnFramework();
var agentMap = AgentMap.Instance();
var houseMan = HousingManager.Instance(); var houseMan = HousingManager.Instance();
uint serverId = 0;
var location = new LocationInfo(); if (_objectTable.LocalPlayer == null) serverId = 0;
location.ServerId = _playerState.CurrentWorld.RowId; else serverId = _objectTable.LocalPlayer.CurrentWorld.RowId;
location.InstanceId = UIState.Instance()->PublicInstance.InstanceId; uint mapId = agentMap == null ? 0 : agentMap->CurrentMapId;
location.TerritoryId = _clientState.TerritoryType; uint territoryId = agentMap == null ? 0 : agentMap->CurrentTerritoryId;
location.MapId = _clientState.MapId; uint divisionId = houseMan == null ? 0 : (uint)(houseMan->GetCurrentDivision());
if (houseMan != null) 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()) divisionId = tempHouseId == -127 ? 2 : (uint)1;
{ tempHouseId = 100;
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));
} }
return location; if (tempHouseId == -1) tempHouseId = 0;
} houseId = (uint)tempHouseId;
if (houseId != 0)
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))
{ {
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 ServerId = serverId,
{ MapId = mapId,
str += $" - {TerritoryData.Value[(ushort)location.TerritoryId]}"; TerritoryId = territoryId,
} DivisionId = divisionId,
else WardId = wardId,
{ HouseId = houseId,
str += $" - {MapData.Value[(ushort)location.MapId].MapName}"; RoomId = roomId
} };
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;
} }
public unsafe void SetMarkerAndOpenMap(Vector3 position, Map map) public unsafe void SetMarkerAndOpenMap(Vector3 position, Map map)
@@ -757,6 +593,21 @@ public class DalamudUtilService : IHostedService, IMediatorSubscriber
agentMap->SetFlagMapMarker(map.TerritoryType.RowId, map.RowId, position); 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) public unsafe bool IsGameObjectPresent(IntPtr key)
{ {
return _objectTable.Any(f => f.Address == key); return _objectTable.Any(f => f.Address == key);
@@ -845,41 +696,31 @@ public class DalamudUtilService : IHostedService, IMediatorSubscriber
return Task.CompletedTask; return Task.CompletedTask;
} }
public async Task WaitWhileCharacterIsDrawing( public async Task WaitWhileCharacterIsDrawing(ILogger logger, GameObjectHandler handler, Guid redrawId, int timeOut = 5000, CancellationToken? ct = null)
ILogger logger,
GameObjectHandler handler,
Guid redrawId,
int timeOut = 5000,
CancellationToken? ct = null)
{ {
if (!_clientState.IsLoggedIn) return; if (!_clientState.IsLoggedIn) return;
var token = ct ?? CancellationToken.None; if (ct == null)
ct = CancellationToken.None;
const int tick = 250;
const int initialSettle = 50;
var sw = Stopwatch.StartNew();
const int tick = 250;
int curWaitTime = 0;
try try
{ {
logger.LogTrace("[{redrawId}] Starting wait for {handler} to draw", redrawId, handler); logger.LogTrace("[{redrawId}] Starting wait for {handler} to draw", redrawId, handler);
await Task.Delay(tick, ct.Value).ConfigureAwait(true);
curWaitTime += tick;
await Task.Delay(initialSettle, token).ConfigureAwait(false); while ((!ct.Value.IsCancellationRequested)
&& curWaitTime < timeOut
while (!token.IsCancellationRequested && await handler.IsBeingDrawnRunOnFrameworkAsync().ConfigureAwait(false)) // 0b100000000000 is "still rendering" or something
&& sw.ElapsedMilliseconds < timeOut
&& await handler.IsBeingDrawnRunOnFrameworkAsync().ConfigureAwait(false))
{ {
logger.LogTrace("[{redrawId}] Waiting for {handler} to finish drawing", redrawId, handler); logger.LogTrace("[{redrawId}] Waiting for {handler} to finish drawing", redrawId, handler);
await Task.Delay(tick, token).ConfigureAwait(false); curWaitTime += tick;
await Task.Delay(tick, ct.Value).ConfigureAwait(true);
} }
logger.LogTrace("[{redrawId}] Finished drawing after {ms}ms", redrawId, sw.ElapsedMilliseconds); logger.LogTrace("[{redrawId}] Finished drawing after {curWaitTime}ms", redrawId, curWaitTime);
}
catch (OperationCanceledException)
{
// ignore
} }
catch (AccessViolationException ex) catch (AccessViolationException ex)
{ {
@@ -931,43 +772,15 @@ public class DalamudUtilService : IHostedService, IMediatorSubscriber
return WorldData.Value.TryGetValue(worldId, out var worldName) ? worldName : null; return WorldData.Value.TryGetValue(worldId, out var worldName) ? worldName : null;
} }
public void TargetPlayerByAddress(nint address)
{
if (address == nint.Zero) return;
if (_clientState.IsPvP) return;
_ = RunOnFrameworkThread(() =>
{
var gameObject = CreateGameObject(address);
if (gameObject is null) return;
var useFocusTarget = _configService.Current.UseFocusTarget;
if (useFocusTarget)
{
_targetManager.FocusTarget = gameObject;
}
else
{
_targetManager.Target = gameObject;
}
});
}
private unsafe void CheckCharacterForDrawing(nint address, string characterName) private unsafe void CheckCharacterForDrawing(nint address, string characterName)
{ {
if (address == nint.Zero)
return;
var gameObj = (GameObject*)address; var gameObj = (GameObject*)address;
if (gameObj == null || gameObj->ObjectKind == 0)
return;
var drawObj = gameObj->DrawObject; var drawObj = gameObj->DrawObject;
bool isDrawing = false; bool isDrawing = false;
bool isDrawingChanged = false; bool isDrawingChanged = false;
if ((nint)drawObj != IntPtr.Zero) if ((nint)drawObj != IntPtr.Zero)
{ {
isDrawing = gameObj->RenderFlags == (VisibilityFlags)0b100000000000; isDrawing = (gameObj->RenderFlags & VisibilityFlags.Nameplate) != VisibilityFlags.None;
if (!isDrawing) if (!isDrawing)
{ {
isDrawing = ((CharacterBase*)drawObj)->HasModelInSlotLoaded != 0; isDrawing = ((CharacterBase*)drawObj)->HasModelInSlotLoaded != 0;
@@ -1033,12 +846,9 @@ public class DalamudUtilService : IHostedService, IMediatorSubscriber
_performanceCollector.LogPerformance(this, $"TrackedActorsToState", _performanceCollector.LogPerformance(this, $"TrackedActorsToState",
() => () =>
{ {
if (!_actorObjectService.HooksActive || !isNormalFrameworkUpdate || _actorObjectService.HasPendingHashResolutions) _actorObjectService.RefreshTrackedActors();
{
_actorObjectService.RefreshTrackedActors();
}
var playerDescriptors = _actorObjectService.PlayerDescriptors; var playerDescriptors = _actorObjectService.PlayerCharacterDescriptors;
for (var i = 0; i < playerDescriptors.Count; i++) for (var i = 0; i < playerDescriptors.Count; i++)
{ {
var actor = playerDescriptors[i]; var actor = playerDescriptors[i];
@@ -1050,7 +860,7 @@ public class DalamudUtilService : IHostedService, IMediatorSubscriber
if (actor.ObjectIndex >= 200) if (actor.ObjectIndex >= 200)
continue; continue;
if (_blockedCharacterHandler.IsCharacterBlocked(playerAddress, actor.ObjectIndex, out bool firstTime) && firstTime) if (_blockedCharacterHandler.IsCharacterBlocked(playerAddress, out bool firstTime) && firstTime)
{ {
_logger.LogTrace("Skipping character {addr}, blocked/muted", playerAddress.ToString("X")); _logger.LogTrace("Skipping character {addr}, blocked/muted", playerAddress.ToString("X"));
continue; continue;
@@ -1058,28 +868,16 @@ public class DalamudUtilService : IHostedService, IMediatorSubscriber
if (!IsAnythingDrawing) if (!IsAnythingDrawing)
{ {
try var gameObj = (GameObject*)playerAddress;
{ var currentName = gameObj != null ? gameObj->NameString ?? string.Empty : string.Empty;
var gameObj = (GameObject*)playerAddress; var charaName = string.IsNullOrEmpty(currentName) ? actor.Name : currentName;
CheckCharacterForDrawing(playerAddress, charaName);
if (gameObj == null || gameObj->ObjectKind == 0) if (IsAnythingDrawing)
{ break;
continue; }
} else
{
var currentName = gameObj->NameString ?? string.Empty; break;
var charaName = string.IsNullOrEmpty(currentName) ? actor.Name : currentName;
CheckCharacterForDrawing(playerAddress, charaName);
if (IsAnythingDrawing)
break;
}
catch (AccessViolationException ex)
{
_logger.LogWarning(ex, "Memory access violation reading character at {addr}", playerAddress.ToString("X"));
continue;
}
} }
} }
}); });
@@ -1192,18 +990,6 @@ public class DalamudUtilService : IHostedService, IMediatorSubscriber
Mediator.Publish(new ZoneSwitchEndMessage()); Mediator.Publish(new ZoneSwitchEndMessage());
Mediator.Publish(new ResumeScanMessage(nameof(ConditionFlag.BetweenAreas))); Mediator.Publish(new ResumeScanMessage(nameof(ConditionFlag.BetweenAreas)));
} }
//Map
if (!_sentBetweenAreas)
{
var mapid = _clientState.MapId;
if (mapid != _lastMapId)
{
_lastMapId = mapid;
Mediator.Publish(new MapChangedMessage(mapid));
}
}
var localPlayer = _objectTable.LocalPlayer; var localPlayer = _objectTable.LocalPlayer;
if (localPlayer != null) if (localPlayer != null)

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

@@ -1,4 +1,4 @@
using Dalamud.Plugin.Services; using Dalamud.Plugin.Services;
using LightlessSync.API.Dto.User; using LightlessSync.API.Dto.User;
using LightlessSync.Services.ActorTracking; using LightlessSync.Services.ActorTracking;
using LightlessSync.Services.Mediator; using LightlessSync.Services.Mediator;
@@ -15,15 +15,11 @@ public class LightFinderScannerService : DisposableMediatorSubscriberBase
private readonly LightFinderService _broadcastService; private readonly LightFinderService _broadcastService;
private readonly LightFinderPlateHandler _lightFinderPlateHandler; private readonly LightFinderPlateHandler _lightFinderPlateHandler;
private readonly LightFinderNativePlateHandler _lightFinderNativePlateHandler;
private readonly ConcurrentDictionary<string, BroadcastEntry> _broadcastCache = new(StringComparer.Ordinal); private readonly ConcurrentDictionary<string, BroadcastEntry> _broadcastCache = new(StringComparer.Ordinal);
private readonly Queue<string> _lookupQueue = new(); private readonly Queue<string> _lookupQueue = new();
private readonly HashSet<string> _lookupQueuedCids = []; private readonly HashSet<string> _lookupQueuedCids = [];
private readonly HashSet<string> _syncshellCids = []; private readonly HashSet<string> _syncshellCids = [];
private volatile bool _pendingLocalBroadcast;
private TimeSpan? _pendingLocalTtl;
private string? _pendingLocalGid;
private static readonly TimeSpan _maxAllowedTtl = TimeSpan.FromMinutes(4); private static readonly TimeSpan _maxAllowedTtl = TimeSpan.FromMinutes(4);
private static readonly TimeSpan _retryDelay = TimeSpan.FromMinutes(1); private static readonly TimeSpan _retryDelay = TimeSpan.FromMinutes(1);
@@ -37,7 +33,6 @@ public class LightFinderScannerService : DisposableMediatorSubscriberBase
private const int _maxQueueSize = 100; private const int _maxQueueSize = 100;
private volatile bool _batchRunning = false; private volatile bool _batchRunning = false;
private volatile bool _disposed = false;
public IReadOnlyDictionary<string, BroadcastEntry> BroadcastCache => _broadcastCache; public IReadOnlyDictionary<string, BroadcastEntry> BroadcastCache => _broadcastCache;
public readonly record struct BroadcastEntry(bool IsBroadcasting, DateTime ExpiryTime, string? GID); public readonly record struct BroadcastEntry(bool IsBroadcasting, DateTime ExpiryTime, string? GID);
@@ -47,14 +42,12 @@ public class LightFinderScannerService : DisposableMediatorSubscriberBase
LightFinderService broadcastService, LightFinderService broadcastService,
LightlessMediator mediator, LightlessMediator mediator,
LightFinderPlateHandler lightFinderPlateHandler, LightFinderPlateHandler lightFinderPlateHandler,
LightFinderNativePlateHandler lightFinderNativePlateHandler,
ActorObjectService actorTracker) : base(logger, mediator) ActorObjectService actorTracker) : base(logger, mediator)
{ {
_logger = logger; _logger = logger;
_actorTracker = actorTracker; _actorTracker = actorTracker;
_broadcastService = broadcastService; _broadcastService = broadcastService;
_lightFinderPlateHandler = lightFinderPlateHandler; _lightFinderPlateHandler = lightFinderPlateHandler;
_lightFinderNativePlateHandler = lightFinderNativePlateHandler;
_logger = logger; _logger = logger;
_framework = framework; _framework = framework;
@@ -70,25 +63,20 @@ public class LightFinderScannerService : DisposableMediatorSubscriberBase
public void Update() public void Update()
{ {
if (_disposed)
return;
_frameCounter++; _frameCounter++;
var lookupsThisFrame = 0; var lookupsThisFrame = 0;
if (!_broadcastService.IsBroadcasting) if (!_broadcastService.IsBroadcasting)
return; return;
TryPrimeLocalBroadcastCache();
var now = DateTime.UtcNow; var now = DateTime.UtcNow;
foreach (var descriptor in _actorTracker.PlayerDescriptors) foreach (var address in _actorTracker.PlayerAddresses)
{ {
if (string.IsNullOrEmpty(descriptor.HashedContentId)) if (address == nint.Zero)
continue; continue;
var cid = descriptor.HashedContentId; var cid = DalamudUtilService.GetHashedCIDFromPlayerPointer(address);
var isStale = !_broadcastCache.TryGetValue(cid, out var entry) || entry.ExpiryTime <= now; var isStale = !_broadcastCache.TryGetValue(cid, out var entry) || entry.ExpiryTime <= now;
if (isStale && _lookupQueuedCids.Add(cid) && _lookupQueue.Count < _maxQueueSize) if (isStale && _lookupQueuedCids.Add(cid) && _lookupQueue.Count < _maxQueueSize)
@@ -116,14 +104,7 @@ public class LightFinderScannerService : DisposableMediatorSubscriberBase
private async Task BatchUpdateBroadcastCacheAsync(List<string> cids) private async Task BatchUpdateBroadcastCacheAsync(List<string> cids)
{ {
if (_disposed)
return;
var results = await _broadcastService.AreUsersBroadcastingAsync(cids).ConfigureAwait(false); var results = await _broadcastService.AreUsersBroadcastingAsync(cids).ConfigureAwait(false);
if (_disposed)
return;
var now = DateTime.UtcNow; var now = DateTime.UtcNow;
foreach (var (cid, info) in results) foreach (var (cid, info) in results)
@@ -142,88 +123,35 @@ public class LightFinderScannerService : DisposableMediatorSubscriberBase
(_, old) => new BroadcastEntry(info.IsBroadcasting, expiry, info.GID)); (_, old) => new BroadcastEntry(info.IsBroadcasting, expiry, info.GID));
} }
if (_disposed)
return;
var activeCids = _broadcastCache var activeCids = _broadcastCache
.Where(e => e.Value.IsBroadcasting && e.Value.ExpiryTime > now) .Where(e => e.Value.IsBroadcasting && e.Value.ExpiryTime > now)
.Select(e => e.Key) .Select(e => e.Key)
.ToList(); .ToList();
_lightFinderPlateHandler.UpdateBroadcastingCids(activeCids); _lightFinderPlateHandler.UpdateBroadcastingCids(activeCids);
_lightFinderNativePlateHandler.UpdateBroadcastingCids(activeCids);
UpdateSyncshellBroadcasts(); UpdateSyncshellBroadcasts();
} }
private void OnBroadcastStatusChanged(BroadcastStatusChangedMessage msg) private void OnBroadcastStatusChanged(BroadcastStatusChangedMessage msg)
{ {
if (_disposed)
return;
if (!msg.Enabled) if (!msg.Enabled)
{ {
_broadcastCache.Clear(); _broadcastCache.Clear();
_lookupQueue.Clear(); _lookupQueue.Clear();
_lookupQueuedCids.Clear(); _lookupQueuedCids.Clear();
_syncshellCids.Clear(); _syncshellCids.Clear();
_pendingLocalBroadcast = false;
_pendingLocalTtl = null;
_lightFinderPlateHandler.UpdateBroadcastingCids([]); _lightFinderPlateHandler.UpdateBroadcastingCids([]);
_lightFinderNativePlateHandler.UpdateBroadcastingCids([]);
return;
} }
_pendingLocalBroadcast = true;
_pendingLocalTtl = msg.Ttl;
_pendingLocalGid = msg.Gid;
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, _pendingLocalGid),
(_, old) => new BroadcastEntry(true, expiry, _pendingLocalGid ?? old.GID));
_pendingLocalBroadcast = false;
_pendingLocalTtl = null;
_pendingLocalGid = 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);
UpdateSyncshellBroadcasts();
} }
private void UpdateSyncshellBroadcasts() private void UpdateSyncshellBroadcasts()
{ {
if (_disposed)
return;
var now = DateTime.UtcNow; var now = DateTime.UtcNow;
var nearbyCids = GetNearbyHashedCids(out _); var newSet = _broadcastCache
var newSet = nearbyCids.Count == 0 .Where(e => e.Value.IsBroadcasting && e.Value.ExpiryTime > now && !string.IsNullOrEmpty(e.Value.GID))
? new HashSet<string>(StringComparer.Ordinal) .Select(e => e.Key)
: _broadcastCache .ToHashSet(StringComparer.Ordinal);
.Where(e => e.Value.IsBroadcasting && e.Value.ExpiryTime > now && !string.IsNullOrEmpty(e.Value.GID))
.Where(e => nearbyCids.Contains(e.Key))
.Select(e => e.Key)
.ToHashSet(StringComparer.Ordinal);
if (!_syncshellCids.SetEquals(newSet)) if (!_syncshellCids.SetEquals(newSet))
{ {
@@ -235,17 +163,12 @@ public class LightFinderScannerService : DisposableMediatorSubscriberBase
} }
} }
public List<BroadcastStatusInfoDto> GetActiveSyncshellBroadcasts(bool excludeLocal = false) public List<BroadcastStatusInfoDto> GetActiveSyncshellBroadcasts()
{ {
var now = DateTime.UtcNow; var now = DateTime.UtcNow;
var nearbyCids = GetNearbyHashedCids(out var localCid);
if (nearbyCids.Count == 0)
return [];
return [.. _broadcastCache return [.. _broadcastCache
.Where(e => e.Value.IsBroadcasting && e.Value.ExpiryTime > now && !string.IsNullOrEmpty(e.Value.GID)) .Where(e => e.Value.IsBroadcasting && e.Value.ExpiryTime > now && !string.IsNullOrEmpty(e.Value.GID))
.Where(e => nearbyCids.Contains(e.Key))
.Where(e => !excludeLocal || !string.Equals(e.Key, localCid, StringComparison.Ordinal))
.Select(e => new BroadcastStatusInfoDto .Select(e => new BroadcastStatusInfoDto
{ {
HashedCID = e.Key, HashedCID = e.Key,
@@ -255,47 +178,6 @@ public class LightFinderScannerService : DisposableMediatorSubscriberBase
})]; })];
} }
public bool TryGetLocalHashedCid(out string hashedCid)
{
hashedCid = string.Empty;
var descriptors = _actorTracker.PlayerDescriptors;
if (descriptors.Count == 0)
return false;
foreach (var descriptor in descriptors)
{
if (!descriptor.IsLocalPlayer || string.IsNullOrWhiteSpace(descriptor.HashedContentId))
continue;
hashedCid = descriptor.HashedContentId;
return true;
}
return false;
}
private HashSet<string> GetNearbyHashedCids(out string? localCid)
{
localCid = null;
var descriptors = _actorTracker.PlayerDescriptors;
if (descriptors.Count == 0)
return new HashSet<string>(StringComparer.Ordinal);
var set = new HashSet<string>(StringComparer.Ordinal);
foreach (var descriptor in descriptors)
{
if (string.IsNullOrWhiteSpace(descriptor.HashedContentId))
continue;
if (descriptor.IsLocalPlayer)
localCid = descriptor.HashedContentId;
set.Add(descriptor.HashedContentId);
}
return set;
}
private async Task ExpiredBroadcastCleanupLoop() private async Task ExpiredBroadcastCleanupLoop()
{ {
var token = _cleanupCts.Token; var token = _cleanupCts.Token;
@@ -348,35 +230,17 @@ public class LightFinderScannerService : DisposableMediatorSubscriberBase
protected override void Dispose(bool disposing) protected override void Dispose(bool disposing)
{ {
_disposed = true;
base.Dispose(disposing); base.Dispose(disposing);
_framework.Update -= OnFrameworkUpdate; _framework.Update -= OnFrameworkUpdate;
if (_cleanupTask != null)
try
{ {
_cleanupCts.Cancel(); _cleanupTask?.Wait(100, _cleanupCts.Token);
}
catch (ObjectDisposedException)
{
// Already disposed, can be ignored :)
} }
try _cleanupCts.Cancel();
{ _cleanupCts.Dispose();
_cleanupTask?.Wait(100);
}
catch (Exception)
{
// Task may have already completed or been cancelled?
}
try _cleanupTask?.Wait(100);
{ _cleanupCts.Dispose();
_cleanupCts.Dispose();
}
catch (ObjectDisposedException)
{
// Already disposed, ignore
}
} }
} }

View File

@@ -1,4 +1,4 @@
using Dalamud.Interface; using Dalamud.Interface;
using LightlessSync.API.Dto.Group; using LightlessSync.API.Dto.Group;
using LightlessSync.API.Dto.User; using LightlessSync.API.Dto.User;
using LightlessSync.LightlessConfiguration; using LightlessSync.LightlessConfiguration;
@@ -67,7 +67,7 @@ public class LightFinderService : IHostedService, IMediatorSubscriber
{ {
try try
{ {
var cid = _dalamudUtil.GetCID(); var cid = await _dalamudUtil.GetCIDAsync().ConfigureAwait(false);
return cid.ToString().GetHash256(); return cid.ToString().GetHash256();
} }
catch (Exception ex) catch (Exception ex)
@@ -121,10 +121,7 @@ public class LightFinderService : IHostedService, IMediatorSubscriber
_waitingForTtlFetch = false; _waitingForTtlFetch = false;
if (!wasEnabled || previousRemaining != validTtl) if (!wasEnabled || previousRemaining != validTtl)
{ _mediator.Publish(new BroadcastStatusChangedMessage(true, validTtl));
var gid = _config.Current.SyncshellFinderEnabled ? _config.Current.SelectedFinderSyncshell : null;
_mediator.Publish(new BroadcastStatusChangedMessage(true, validTtl, gid));
}
_logger.LogInformation("Lightfinder broadcast enabled ({Context}), TTL: {TTL}", context, validTtl); _logger.LogInformation("Lightfinder broadcast enabled ({Context}), TTL: {TTL}", context, validTtl);
return true; return true;

View File

@@ -1,137 +0,0 @@
using LightlessSync.API.Data;
using LightlessSync.API.Dto.CharaData;
using LightlessSync.API.Dto.User;
using LightlessSync.Services.Mediator;
using LightlessSync.WebAPI;
using Microsoft.Extensions.Caching.Memory;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Primitives;
namespace LightlessSync.Services
{
public class LocationShareService : DisposableMediatorSubscriberBase
{
private readonly DalamudUtilService _dalamudUtilService;
private readonly ApiController _apiController;
private IMemoryCache _locations = new MemoryCache(new MemoryCacheOptions());
private IMemoryCache _sharingStatus = new MemoryCache(new MemoryCacheOptions());
private CancellationTokenSource _resetToken = new CancellationTokenSource();
public LocationShareService(ILogger<LocationShareService> logger, LightlessMediator mediator, DalamudUtilService dalamudUtilService, ApiController apiController) : base(logger, mediator)
{
_dalamudUtilService = dalamudUtilService;
_apiController = apiController;
Mediator.Subscribe<DisconnectedMessage>(this, (msg) =>
{
_resetToken.Cancel();
_resetToken.Dispose();
_resetToken = new CancellationTokenSource();
});
Mediator.Subscribe<ConnectedMessage>(this, (msg) =>
{
_ = _apiController.UpdateLocation(new LocationDto(new UserData(_apiController.UID, apiController.DisplayName), _dalamudUtilService.GetMapData()));
_ = RequestAllLocation();
} );
Mediator.Subscribe<LocationSharingMessage>(this, UpdateLocationList);
Mediator.Subscribe<MapChangedMessage>(this,
msg => _ = _apiController.UpdateLocation(new LocationDto(new UserData(_apiController.UID, _apiController.DisplayName), _dalamudUtilService.GetMapData())));
}
private void UpdateLocationList(LocationSharingMessage msg)
{
if (_locations.TryGetValue(msg.User.UID, out _) && msg.LocationInfo.ServerId is 0)
{
_locations.Remove(msg.User.UID);
return;
}
if ( msg.LocationInfo.ServerId is not 0 && msg.ExpireAt > DateTime.UtcNow)
{
AddLocationInfo(msg.User.UID, msg.LocationInfo, msg.ExpireAt);
}
}
private void AddLocationInfo(string uid, LocationInfo location, DateTimeOffset expireAt)
{
var options = new MemoryCacheEntryOptions()
.SetAbsoluteExpiration(expireAt)
.AddExpirationToken(new CancellationChangeToken(_resetToken.Token));
_locations.Set(uid, location, options);
}
private async Task RequestAllLocation()
{
try
{
var (data, status) = await _apiController.RequestAllLocationInfo().ConfigureAwait(false);
foreach (var dto in data)
{
AddLocationInfo(dto.LocationDto.User.UID, dto.LocationDto.Location, dto.ExpireAt);
}
foreach (var dto in status)
{
AddStatus(dto.User.UID, dto.ExpireAt);
}
}
catch (Exception e)
{
Logger.LogError(e,"RequestAllLocation error : ");
throw;
}
}
private void AddStatus(string uid, DateTimeOffset expireAt)
{
var options = new MemoryCacheEntryOptions()
.SetAbsoluteExpiration(expireAt)
.AddExpirationToken(new CancellationChangeToken(_resetToken.Token));
_sharingStatus.Set(uid, expireAt, options);
}
public string GetUserLocation(string uid)
{
try
{
if (_locations.TryGetValue<LocationInfo>(uid, out var location))
{
return _dalamudUtilService.LocationToString(location);
}
return String.Empty;
}
catch (Exception e)
{
Logger.LogError(e,"GetUserLocation error : ");
throw;
}
}
public DateTimeOffset GetSharingStatus(string uid)
{
try
{
if (_sharingStatus.TryGetValue<DateTimeOffset>(uid, out var expireAt))
{
return expireAt;
}
return DateTimeOffset.MinValue;
}
catch (Exception e)
{
Logger.LogError(e,"GetSharingStatus error : ");
throw;
}
}
public void UpdateSharingStatus(List<string> users, DateTimeOffset expireAt)
{
foreach (var user in users)
{
AddStatus(user, expireAt);
}
}
}
}

View File

@@ -63,31 +63,23 @@ public sealed class LightlessMediator : IHostedService
_ = Task.Run(async () => _ = Task.Run(async () =>
{ {
try while (!_loopCts.Token.IsCancellationRequested)
{ {
while (!_loopCts.Token.IsCancellationRequested) while (!_processQueue)
{ {
while (!_processQueue)
{
await Task.Delay(100, _loopCts.Token).ConfigureAwait(false);
}
await Task.Delay(100, _loopCts.Token).ConfigureAwait(false); await Task.Delay(100, _loopCts.Token).ConfigureAwait(false);
HashSet<MessageBase> processedMessages = [];
while (_messageQueue.TryDequeue(out var message))
{
if (processedMessages.Contains(message)) { continue; }
processedMessages.Add(message);
ExecuteMessage(message);
}
} }
}
catch (OperationCanceledException) await Task.Delay(100, _loopCts.Token).ConfigureAwait(false);
{
_logger.LogInformation("LightlessMediator stopped"); HashSet<MessageBase> processedMessages = [];
while (_messageQueue.TryDequeue(out var message))
{
if (processedMessages.Contains(message)) { continue; }
processedMessages.Add(message);
ExecuteMessage(message);
}
} }
}); });

View File

@@ -73,7 +73,7 @@ public record HubClosedMessage(Exception? Exception) : SameThreadMessage;
public record ResumeScanMessage(string Source) : MessageBase; public record ResumeScanMessage(string Source) : MessageBase;
public record FileCacheInitializedMessage : MessageBase; public record FileCacheInitializedMessage : MessageBase;
public record DownloadReadyMessage(Guid RequestId) : MessageBase; public record DownloadReadyMessage(Guid RequestId) : MessageBase;
public record DownloadStartedMessage(GameObjectHandler DownloadId, IReadOnlyDictionary<string, FileDownloadStatus> DownloadStatus) : MessageBase; public record DownloadStartedMessage(GameObjectHandler DownloadId, Dictionary<string, FileDownloadStatus> DownloadStatus) : MessageBase;
public record DownloadFinishedMessage(GameObjectHandler DownloadId) : MessageBase; public record DownloadFinishedMessage(GameObjectHandler DownloadId) : MessageBase;
public record UiToggleMessage(Type UiType) : MessageBase; public record UiToggleMessage(Type UiType) : MessageBase;
public record PlayerUploadingMessage(GameObjectHandler Handler, bool IsUploading) : MessageBase; public record PlayerUploadingMessage(GameObjectHandler Handler, bool IsUploading) : MessageBase;
@@ -104,7 +104,6 @@ public record PairUiUpdatedMessage(PairUiSnapshot Snapshot) : MessageBase;
public record CensusUpdateMessage(byte Gender, byte RaceId, byte TribeId) : MessageBase; public record CensusUpdateMessage(byte Gender, byte RaceId, byte TribeId) : MessageBase;
public record TargetPairMessage(Pair Pair) : MessageBase; public record TargetPairMessage(Pair Pair) : MessageBase;
public record PairFocusCharacterMessage(Pair Pair) : SameThreadMessage; public record PairFocusCharacterMessage(Pair Pair) : SameThreadMessage;
public record PairOnlineMessage(PairUniqueIdentifier PairIdent) : MessageBase;
public record CombatStartMessage : MessageBase; public record CombatStartMessage : MessageBase;
public record CombatEndMessage : MessageBase; public record CombatEndMessage : MessageBase;
public record PerformanceStartMessage : MessageBase; public record PerformanceStartMessage : MessageBase;
@@ -124,7 +123,7 @@ public record GPoseLobbyReceivePoseData(UserData UserData, PoseData PoseData) :
public record GPoseLobbyReceiveWorldData(UserData UserData, WorldData WorldData) : MessageBase; public record GPoseLobbyReceiveWorldData(UserData UserData, WorldData WorldData) : MessageBase;
public record OpenCharaDataHubWithFilterMessage(UserData UserData) : MessageBase; public record OpenCharaDataHubWithFilterMessage(UserData UserData) : MessageBase;
public record EnableBroadcastMessage(string HashedCid, bool Enabled) : MessageBase; public record EnableBroadcastMessage(string HashedCid, bool Enabled) : MessageBase;
public record BroadcastStatusChangedMessage(bool Enabled, TimeSpan? Ttl, string? Gid = null) : MessageBase; public record BroadcastStatusChangedMessage(bool Enabled, TimeSpan? Ttl) : MessageBase;
public record UserLeftSyncshell(string gid) : MessageBase; public record UserLeftSyncshell(string gid) : MessageBase;
public record UserJoinedSyncshell(string gid) : MessageBase; public record UserJoinedSyncshell(string gid) : MessageBase;
public record SyncshellBroadcastsUpdatedMessage : MessageBase; public record SyncshellBroadcastsUpdatedMessage : MessageBase;
@@ -136,7 +135,5 @@ public record ChatChannelsUpdated : MessageBase;
public record ChatChannelMessageAdded(string ChannelKey, ChatMessageEntry Message) : MessageBase; public record ChatChannelMessageAdded(string ChannelKey, ChatMessageEntry Message) : MessageBase;
public record GroupCollectionChangedMessage : MessageBase; public record GroupCollectionChangedMessage : MessageBase;
public record OpenUserProfileMessage(UserData User) : MessageBase; public record OpenUserProfileMessage(UserData User) : MessageBase;
public record LocationSharingMessage(UserData User, LocationInfo LocationInfo, DateTimeOffset ExpireAt) : MessageBase;
public record MapChangedMessage(uint MapId) : MessageBase;
#pragma warning restore S2094 #pragma warning restore S2094
#pragma warning restore MA0048 // File name must match type name #pragma warning restore MA0048 // File name must match type name

File diff suppressed because it is too large Load Diff

View File

@@ -1,381 +0,0 @@
using LightlessSync.FileCache;
using LightlessSync.LightlessConfiguration;
using Microsoft.Extensions.Logging;
using System.Collections.Concurrent;
using System.Globalization;
namespace LightlessSync.Services.ModelDecimation;
public sealed class ModelDecimationService
{
private const int MaxConcurrentJobs = 1;
private const double MinTargetRatio = 0.01;
private const double MaxTargetRatio = 0.99;
private readonly ILogger<ModelDecimationService> _logger;
private readonly LightlessConfigService _configService;
private readonly FileCacheManager _fileCacheManager;
private readonly PlayerPerformanceConfigService _performanceConfigService;
private readonly XivDataStorageService _xivDataStorageService;
private readonly SemaphoreSlim _decimationSemaphore = new(MaxConcurrentJobs);
private readonly ConcurrentDictionary<string, Task> _activeJobs = new(StringComparer.OrdinalIgnoreCase);
private readonly ConcurrentDictionary<string, string> _decimatedPaths = new(StringComparer.OrdinalIgnoreCase);
private readonly ConcurrentDictionary<string, byte> _failedHashes = new(StringComparer.OrdinalIgnoreCase);
public ModelDecimationService(
ILogger<ModelDecimationService> logger,
LightlessConfigService configService,
FileCacheManager fileCacheManager,
PlayerPerformanceConfigService performanceConfigService,
XivDataStorageService xivDataStorageService)
{
_logger = logger;
_configService = configService;
_fileCacheManager = fileCacheManager;
_performanceConfigService = performanceConfigService;
_xivDataStorageService = xivDataStorageService;
}
public void ScheduleDecimation(string hash, string filePath, string? gamePath = null)
{
if (!ShouldScheduleDecimation(hash, filePath, gamePath))
{
return;
}
if (_decimatedPaths.ContainsKey(hash) || _failedHashes.ContainsKey(hash) || _activeJobs.ContainsKey(hash))
{
return;
}
_logger.LogInformation("Queued model decimation for {Hash}", hash);
_activeJobs[hash] = Task.Run(async () =>
{
await _decimationSemaphore.WaitAsync().ConfigureAwait(false);
try
{
await DecimateInternalAsync(hash, filePath).ConfigureAwait(false);
}
catch (Exception ex)
{
_failedHashes[hash] = 1;
_logger.LogWarning(ex, "Model decimation failed for {Hash}", hash);
}
finally
{
_decimationSemaphore.Release();
_activeJobs.TryRemove(hash, out _);
}
}, CancellationToken.None);
}
public bool ShouldScheduleDecimation(string hash, string filePath, string? gamePath = null)
=> IsDecimationEnabled()
&& filePath.EndsWith(".mdl", StringComparison.OrdinalIgnoreCase)
&& IsDecimationAllowed(gamePath)
&& !ShouldSkipByTriangleCache(hash);
public string GetPreferredPath(string hash, string originalPath)
{
if (!IsDecimationEnabled())
{
return originalPath;
}
if (_decimatedPaths.TryGetValue(hash, out var existing) && File.Exists(existing))
{
return existing;
}
var resolved = GetExistingDecimatedPath(hash);
if (!string.IsNullOrEmpty(resolved))
{
_decimatedPaths[hash] = resolved;
return resolved;
}
return originalPath;
}
public Task WaitForPendingJobsAsync(IEnumerable<string>? hashes, CancellationToken token)
{
if (hashes is null)
{
return Task.CompletedTask;
}
var pending = new List<Task>();
var seen = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
foreach (var hash in hashes)
{
if (string.IsNullOrEmpty(hash) || !seen.Add(hash))
{
continue;
}
if (_activeJobs.TryGetValue(hash, out var job))
{
pending.Add(job);
}
}
if (pending.Count == 0)
{
return Task.CompletedTask;
}
return Task.WhenAll(pending).WaitAsync(token);
}
private Task DecimateInternalAsync(string hash, string sourcePath)
{
if (!File.Exists(sourcePath))
{
_failedHashes[hash] = 1;
_logger.LogWarning("Cannot decimate model {Hash}; source path missing: {Path}", hash, sourcePath);
return Task.CompletedTask;
}
if (!TryGetDecimationSettings(out var triangleThreshold, out var targetRatio))
{
_logger.LogInformation("Model decimation disabled or invalid settings for {Hash}", hash);
return Task.CompletedTask;
}
_logger.LogInformation("Starting model decimation for {Hash} (threshold {Threshold}, ratio {Ratio:0.##})", hash, triangleThreshold, targetRatio);
var destination = Path.Combine(GetDecimatedDirectory(), $"{hash}.mdl");
if (File.Exists(destination))
{
RegisterDecimatedModel(hash, sourcePath, destination);
return Task.CompletedTask;
}
if (!MdlDecimator.TryDecimate(sourcePath, destination, triangleThreshold, targetRatio, _logger))
{
_failedHashes[hash] = 1;
_logger.LogInformation("Model decimation skipped for {Hash}", hash);
return Task.CompletedTask;
}
RegisterDecimatedModel(hash, sourcePath, destination);
_logger.LogInformation("Decimated model {Hash} -> {Path}", hash, destination);
return Task.CompletedTask;
}
private void RegisterDecimatedModel(string hash, string sourcePath, string destination)
{
_decimatedPaths[hash] = destination;
var performanceConfig = _performanceConfigService.Current;
if (performanceConfig.KeepOriginalModelFiles)
{
return;
}
if (string.Equals(sourcePath, destination, StringComparison.OrdinalIgnoreCase))
{
return;
}
if (!TryReplaceCacheEntryWithDecimated(hash, sourcePath, destination))
{
return;
}
TryDelete(sourcePath);
}
private bool TryReplaceCacheEntryWithDecimated(string hash, string sourcePath, string destination)
{
try
{
var cacheEntry = _fileCacheManager.GetFileCacheByHash(hash);
if (cacheEntry is null || !cacheEntry.IsCacheEntry)
{
return File.Exists(sourcePath) ? false : true;
}
var cacheFolder = _configService.Current.CacheFolder;
if (string.IsNullOrEmpty(cacheFolder))
{
return false;
}
if (!destination.StartsWith(cacheFolder, StringComparison.OrdinalIgnoreCase))
{
return false;
}
var info = new FileInfo(destination);
if (!info.Exists)
{
return false;
}
var relative = Path.GetRelativePath(cacheFolder, destination)
.Replace(Path.AltDirectorySeparatorChar, Path.DirectorySeparatorChar);
var sanitizedRelative = relative.TrimStart(Path.DirectorySeparatorChar);
var prefixed = Path.Combine(FileCacheManager.CachePrefix, sanitizedRelative);
var replacement = new FileCacheEntity(
hash,
prefixed,
info.LastWriteTimeUtc.Ticks.ToString(CultureInfo.InvariantCulture),
info.Length,
cacheEntry.CompressedSize);
replacement.SetResolvedFilePath(destination);
if (!string.Equals(cacheEntry.PrefixedFilePath, prefixed, StringComparison.OrdinalIgnoreCase))
{
_fileCacheManager.RemoveHashedFile(cacheEntry.Hash, cacheEntry.PrefixedFilePath, removeDerivedFiles: false);
}
_fileCacheManager.UpdateHashedFile(replacement, computeProperties: false);
_fileCacheManager.WriteOutFullCsv();
_logger.LogTrace("Replaced cache entry for model {Hash} to decimated path {Path}", hash, destination);
return true;
}
catch (Exception ex)
{
_logger.LogTrace(ex, "Failed to replace cache entry for model {Hash}", hash);
return false;
}
}
private bool IsDecimationEnabled()
=> _performanceConfigService.Current.EnableModelDecimation;
private bool ShouldSkipByTriangleCache(string hash)
{
if (string.IsNullOrEmpty(hash))
{
return false;
}
if (!_xivDataStorageService.Current.TriangleDictionary.TryGetValue(hash, out var cachedTris) || cachedTris <= 0)
{
return false;
}
var threshold = Math.Max(0, _performanceConfigService.Current.ModelDecimationTriangleThreshold);
return threshold > 0 && cachedTris < threshold;
}
private bool IsDecimationAllowed(string? gamePath)
{
if (string.IsNullOrWhiteSpace(gamePath))
{
return true;
}
var normalized = NormalizeGamePath(gamePath);
if (normalized.Contains("/hair/", StringComparison.Ordinal))
{
return false;
}
if (normalized.Contains("/chara/equipment/", StringComparison.Ordinal))
{
return _performanceConfigService.Current.ModelDecimationAllowClothing;
}
if (normalized.Contains("/chara/accessory/", StringComparison.Ordinal))
{
return _performanceConfigService.Current.ModelDecimationAllowAccessories;
}
if (normalized.Contains("/chara/human/", StringComparison.Ordinal))
{
if (normalized.Contains("/body/", StringComparison.Ordinal))
{
return _performanceConfigService.Current.ModelDecimationAllowBody;
}
if (normalized.Contains("/face/", StringComparison.Ordinal) || normalized.Contains("/head/", StringComparison.Ordinal))
{
return _performanceConfigService.Current.ModelDecimationAllowFaceHead;
}
if (normalized.Contains("/tail/", StringComparison.Ordinal))
{
return _performanceConfigService.Current.ModelDecimationAllowTail;
}
}
return true;
}
private static string NormalizeGamePath(string path)
=> path.Replace('\\', '/').ToLowerInvariant();
private bool TryGetDecimationSettings(out int triangleThreshold, out double targetRatio)
{
triangleThreshold = 15_000;
targetRatio = 0.8;
var config = _performanceConfigService.Current;
if (!config.EnableModelDecimation)
{
return false;
}
triangleThreshold = Math.Max(0, config.ModelDecimationTriangleThreshold);
targetRatio = config.ModelDecimationTargetRatio;
if (double.IsNaN(targetRatio) || double.IsInfinity(targetRatio))
{
return false;
}
targetRatio = Math.Clamp(targetRatio, MinTargetRatio, MaxTargetRatio);
return true;
}
private string? GetExistingDecimatedPath(string hash)
{
var candidate = Path.Combine(GetDecimatedDirectory(), $"{hash}.mdl");
return File.Exists(candidate) ? candidate : null;
}
private string GetDecimatedDirectory()
{
var directory = Path.Combine(_configService.Current.CacheFolder, "decimated");
if (!Directory.Exists(directory))
{
try
{
Directory.CreateDirectory(directory);
}
catch (Exception ex)
{
_logger.LogTrace(ex, "Failed to create decimated directory {Directory}", directory);
}
}
return directory;
}
private static void TryDelete(string? path)
{
if (string.IsNullOrEmpty(path))
{
return;
}
try
{
if (File.Exists(path))
{
File.Delete(path);
}
}
catch
{
// ignored
}
}
}

View File

@@ -2,8 +2,10 @@ using Dalamud.Game.ClientState.Objects.Enums;
using Dalamud.Game.ClientState.Objects.SubKinds; using Dalamud.Game.ClientState.Objects.SubKinds;
using Dalamud.Game.NativeWrapper; using Dalamud.Game.NativeWrapper;
using Dalamud.Game.Text.SeStringHandling; using Dalamud.Game.Text.SeStringHandling;
using Dalamud.Hooking;
using Dalamud.Plugin.Services; using Dalamud.Plugin.Services;
using Dalamud.Utility; using Dalamud.Utility;
using Dalamud.Utility.Signatures;
using FFXIVClientStructs.FFXIV.Client.Game.Character; using FFXIVClientStructs.FFXIV.Client.Game.Character;
using FFXIVClientStructs.FFXIV.Client.UI; using FFXIVClientStructs.FFXIV.Client.UI;
using FFXIVClientStructs.FFXIV.Component.GUI; using FFXIVClientStructs.FFXIV.Component.GUI;
@@ -22,22 +24,27 @@ namespace LightlessSync.Services;
/// </summary> /// </summary>
public unsafe class NameplateService : DisposableMediatorSubscriberBase 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 ILogger<NameplateService> _logger;
private readonly LightlessConfigService _configService; private readonly LightlessConfigService _configService;
private readonly IClientState _clientState; private readonly IClientState _clientState;
private readonly IGameGui _gameGui; private readonly IGameGui _gameGui;
private readonly IObjectTable _objectTable; private readonly IObjectTable _objectTable;
private readonly PairUiService _pairUiService; private readonly PairUiService _pairUiService;
private readonly NameplateUpdateHookService _nameplateUpdateHookService;
public NameplateService(ILogger<NameplateService> logger, public NameplateService(ILogger<NameplateService> logger,
LightlessConfigService configService, LightlessConfigService configService,
IClientState clientState, IClientState clientState,
IGameGui gameGui, IGameGui gameGui,
IObjectTable objectTable, IObjectTable objectTable,
IGameInteropProvider interop,
LightlessMediator lightlessMediator, LightlessMediator lightlessMediator,
PairUiService pairUiService, PairUiService pairUiService) : base(logger, lightlessMediator)
NameplateUpdateHookService nameplateUpdateHookService) : base(logger, lightlessMediator)
{ {
_logger = logger; _logger = logger;
_configService = configService; _configService = configService;
@@ -45,18 +52,21 @@ public unsafe class NameplateService : DisposableMediatorSubscriberBase
_gameGui = gameGui; _gameGui = gameGui;
_objectTable = objectTable; _objectTable = objectTable;
_pairUiService = pairUiService; _pairUiService = pairUiService;
_nameplateUpdateHookService = nameplateUpdateHookService;
_nameplateUpdateHookService.NameplateUpdated += OnNameplateUpdated; interop.InitializeFromAttributes(this);
_nameplateHook?.Enable();
Refresh(); Refresh();
Mediator.Subscribe<VisibilityChange>(this, (_) => Refresh()); Mediator.Subscribe<VisibilityChange>(this, (_) => Refresh());
} }
/// <summary> /// <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> /// </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 try
{ {
@@ -64,8 +74,10 @@ public unsafe class NameplateService : DisposableMediatorSubscriberBase
} }
catch (Exception e) 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> /// <summary>
@@ -234,7 +246,7 @@ public unsafe class NameplateService : DisposableMediatorSubscriberBase
{ {
if (disposing) if (disposing)
{ {
_nameplateUpdateHookService.NameplateUpdated -= OnNameplateUpdated; _nameplateHook?.Dispose();
} }
base.Dispose(disposing); 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

@@ -4,7 +4,6 @@ using LightlessSync.LightlessConfiguration;
using LightlessSync.PlayerData.Pairs; using LightlessSync.PlayerData.Pairs;
using LightlessSync.Services.Events; using LightlessSync.Services.Events;
using LightlessSync.Services.Mediator; using LightlessSync.Services.Mediator;
using LightlessSync.Services.ModelDecimation;
using LightlessSync.Services.TextureCompression; using LightlessSync.Services.TextureCompression;
using LightlessSync.UI; using LightlessSync.UI;
using LightlessSync.WebAPI.Files.Models; using LightlessSync.WebAPI.Files.Models;
@@ -19,14 +18,12 @@ public class PlayerPerformanceService
private readonly ILogger<PlayerPerformanceService> _logger; private readonly ILogger<PlayerPerformanceService> _logger;
private readonly LightlessMediator _mediator; private readonly LightlessMediator _mediator;
private readonly PlayerPerformanceConfigService _playerPerformanceConfigService; private readonly PlayerPerformanceConfigService _playerPerformanceConfigService;
private readonly ModelDecimationService _modelDecimationService;
private readonly TextureDownscaleService _textureDownscaleService; private readonly TextureDownscaleService _textureDownscaleService;
private readonly Dictionary<string, bool> _warnedForPlayers = new(StringComparer.Ordinal); private readonly Dictionary<string, bool> _warnedForPlayers = new(StringComparer.Ordinal);
public PlayerPerformanceService(ILogger<PlayerPerformanceService> logger, LightlessMediator mediator, public PlayerPerformanceService(ILogger<PlayerPerformanceService> logger, LightlessMediator mediator,
PlayerPerformanceConfigService playerPerformanceConfigService, FileCacheManager fileCacheManager, PlayerPerformanceConfigService playerPerformanceConfigService, FileCacheManager fileCacheManager,
XivDataAnalyzer xivDataAnalyzer, TextureDownscaleService textureDownscaleService, XivDataAnalyzer xivDataAnalyzer, TextureDownscaleService textureDownscaleService)
ModelDecimationService modelDecimationService)
{ {
_logger = logger; _logger = logger;
_mediator = mediator; _mediator = mediator;
@@ -34,7 +31,6 @@ public class PlayerPerformanceService
_fileCacheManager = fileCacheManager; _fileCacheManager = fileCacheManager;
_xivDataAnalyzer = xivDataAnalyzer; _xivDataAnalyzer = xivDataAnalyzer;
_textureDownscaleService = textureDownscaleService; _textureDownscaleService = textureDownscaleService;
_modelDecimationService = modelDecimationService;
} }
public async Task<bool> CheckBothThresholds(IPairPerformanceSubject pairHandler, CharacterData charaData) public async Task<bool> CheckBothThresholds(IPairPerformanceSubject pairHandler, CharacterData charaData)
@@ -115,12 +111,10 @@ public class PlayerPerformanceService
var config = _playerPerformanceConfigService.Current; var config = _playerPerformanceConfigService.Current;
long triUsage = 0; long triUsage = 0;
long effectiveTriUsage = 0;
if (!charaData.FileReplacements.TryGetValue(API.Data.Enum.ObjectKind.Player, out List<FileReplacementData>? playerReplacements)) if (!charaData.FileReplacements.TryGetValue(API.Data.Enum.ObjectKind.Player, out List<FileReplacementData>? playerReplacements))
{ {
pairHandler.LastAppliedDataTris = 0; pairHandler.LastAppliedDataTris = 0;
pairHandler.LastAppliedApproximateEffectiveTris = 0;
return true; return true;
} }
@@ -129,40 +123,14 @@ public class PlayerPerformanceService
.Distinct(StringComparer.OrdinalIgnoreCase) .Distinct(StringComparer.OrdinalIgnoreCase)
.ToList(); .ToList();
var skipDecimation = config.SkipModelDecimationForPreferredPairs && pairHandler.IsDirectlyPaired && pairHandler.HasStickyPermissions;
foreach (var hash in moddedModelHashes) foreach (var hash in moddedModelHashes)
{ {
var tris = await _xivDataAnalyzer.GetTrianglesByHash(hash).ConfigureAwait(false); triUsage += await _xivDataAnalyzer.GetTrianglesByHash(hash).ConfigureAwait(false);
triUsage += tris;
long effectiveTris = tris;
var fileEntry = _fileCacheManager.GetFileCacheByHash(hash);
if (fileEntry != null)
{
var preferredPath = fileEntry.ResolvedFilepath;
if (!skipDecimation)
{
preferredPath = _modelDecimationService.GetPreferredPath(hash, fileEntry.ResolvedFilepath);
}
if (!string.Equals(preferredPath, fileEntry.ResolvedFilepath, StringComparison.OrdinalIgnoreCase))
{
var decimatedTris = await _xivDataAnalyzer.GetEffectiveTrianglesByHash(hash, preferredPath).ConfigureAwait(false);
if (decimatedTris > 0)
{
effectiveTris = decimatedTris;
}
}
}
effectiveTriUsage += effectiveTris;
} }
pairHandler.LastAppliedDataTris = triUsage; pairHandler.LastAppliedDataTris = triUsage;
pairHandler.LastAppliedApproximateEffectiveTris = effectiveTriUsage;
_logger.LogDebug("Calculated triangle usage for {p}", pairHandler); _logger.LogDebug("Calculated VRAM usage for {p}", pairHandler);
// no warning of any kind on ignored pairs // no warning of any kind on ignored pairs
if (config.UIDsToIgnore if (config.UIDsToIgnore
@@ -199,9 +167,7 @@ public class PlayerPerformanceService
public bool ComputeAndAutoPauseOnVRAMUsageThresholds(IPairPerformanceSubject pairHandler, CharacterData charaData, List<DownloadFileTransfer> toDownloadFiles) public bool ComputeAndAutoPauseOnVRAMUsageThresholds(IPairPerformanceSubject pairHandler, CharacterData charaData, List<DownloadFileTransfer> toDownloadFiles)
{ {
var config = _playerPerformanceConfigService.Current; var config = _playerPerformanceConfigService.Current;
bool skipDownscale = config.SkipTextureDownscaleForPreferredPairs bool skipDownscale = pairHandler.IsDirectlyPaired && pairHandler.HasStickyPermissions;
&& pairHandler.IsDirectlyPaired
&& pairHandler.HasStickyPermissions;
long vramUsage = 0; long vramUsage = 0;
long effectiveVramUsage = 0; long effectiveVramUsage = 0;
@@ -308,4 +274,4 @@ public class PlayerPerformanceService
private static bool CheckForThreshold(bool thresholdEnabled, long threshold, long value, bool checkForPrefPerm, bool isPrefPerm) => private static bool CheckForThreshold(bool thresholdEnabled, long threshold, long value, bool checkForPrefPerm, bool isPrefPerm) =>
thresholdEnabled && threshold > 0 && threshold < value && ((checkForPrefPerm && isPrefPerm) || !isPrefPerm); thresholdEnabled && threshold > 0 && threshold < value && ((checkForPrefPerm && isPrefPerm) || !isPrefPerm);
} }

View File

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

View File

@@ -77,39 +77,16 @@ public sealed class TextureDownscaleService
} }
public void ScheduleDownscale(string hash, string filePath, TextureMapKind mapKind) public void ScheduleDownscale(string hash, string filePath, TextureMapKind mapKind)
=> ScheduleDownscale(hash, filePath, () => mapKind);
public void ScheduleDownscale(string hash, string filePath, Func<TextureMapKind> mapKindFactory)
{ {
if (!filePath.EndsWith(".tex", StringComparison.OrdinalIgnoreCase)) return; if (!filePath.EndsWith(".tex", StringComparison.OrdinalIgnoreCase)) return;
if (_activeJobs.ContainsKey(hash)) return; if (_activeJobs.ContainsKey(hash)) return;
_activeJobs[hash] = Task.Run(async () => _activeJobs[hash] = Task.Run(async () =>
{ {
TextureMapKind mapKind;
try
{
mapKind = mapKindFactory();
}
catch (Exception ex)
{
_logger.LogWarning(ex, "Failed to determine texture map kind for {Hash}; skipping downscale", hash);
return;
}
await DownscaleInternalAsync(hash, filePath, mapKind).ConfigureAwait(false); await DownscaleInternalAsync(hash, filePath, mapKind).ConfigureAwait(false);
}, CancellationToken.None); }, CancellationToken.None);
} }
public bool ShouldScheduleDownscale(string filePath)
{
if (!filePath.EndsWith(".tex", StringComparison.OrdinalIgnoreCase))
return false;
var performanceConfig = _playerPerformanceConfigService.Current;
return performanceConfig.EnableNonIndexTextureMipTrim || performanceConfig.EnableIndexTextureDownscale;
}
public string GetPreferredPath(string hash, string originalPath) public string GetPreferredPath(string hash, string originalPath)
{ {
if (_downscaledPaths.TryGetValue(hash, out var existing) && File.Exists(existing)) if (_downscaledPaths.TryGetValue(hash, out var existing) && File.Exists(existing))
@@ -678,7 +655,7 @@ public sealed class TextureDownscaleService
if (!string.Equals(cacheEntry.PrefixedFilePath, prefixed, StringComparison.OrdinalIgnoreCase)) if (!string.Equals(cacheEntry.PrefixedFilePath, prefixed, StringComparison.OrdinalIgnoreCase))
{ {
_fileCacheManager.RemoveHashedFile(cacheEntry.Hash, cacheEntry.PrefixedFilePath, removeDerivedFiles: false); _fileCacheManager.RemoveHashedFile(cacheEntry.Hash, cacheEntry.PrefixedFilePath);
} }
_fileCacheManager.UpdateHashedFile(replacement, computeProperties: false); _fileCacheManager.UpdateHashedFile(replacement, computeProperties: false);

View File

@@ -126,11 +126,11 @@ public sealed class TextureMetadataHelper
private const string TextureSegment = "/texture/"; private const string TextureSegment = "/texture/";
private const string MaterialSegment = "/material/"; private const string MaterialSegment = "/material/";
private const uint NormalSamplerId = ShpkFile.NormalSamplerId; private const uint NormalSamplerId = 0x0C5EC1F1u;
private const uint IndexSamplerId = ShpkFile.IndexSamplerId; private const uint IndexSamplerId = 0x565F8FD8u;
private const uint SpecularSamplerId = ShpkFile.SpecularSamplerId; private const uint SpecularSamplerId = 0x2B99E025u;
private const uint DiffuseSamplerId = ShpkFile.DiffuseSamplerId; private const uint DiffuseSamplerId = 0x115306BEu;
private const uint MaskSamplerId = ShpkFile.MaskSamplerId; private const uint MaskSamplerId = 0x8A4E82B6u;
public TextureMetadataHelper(ILogger<TextureMetadataHelper> logger, IDataManager dataManager) public TextureMetadataHelper(ILogger<TextureMetadataHelper> logger, IDataManager dataManager)
{ {
@@ -394,21 +394,6 @@ public sealed class TextureMetadataHelper
if (string.IsNullOrEmpty(fileNameWithExtension) && string.IsNullOrEmpty(fileNameWithoutExtension)) if (string.IsNullOrEmpty(fileNameWithExtension) && string.IsNullOrEmpty(fileNameWithoutExtension))
return TextureMapKind.Unknown; 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) foreach (var (kind, token) in MapTokens)
{ {
if (!string.IsNullOrEmpty(fileNameWithExtension) && if (!string.IsNullOrEmpty(fileNameWithExtension) &&
@@ -578,16 +563,7 @@ public sealed class TextureMetadataHelper
var normalized = format.ToUpperInvariant(); var normalized = format.ToUpperInvariant();
return normalized.Contains("A8", StringComparison.Ordinal) 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("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("BC3", StringComparison.Ordinal)
|| normalized.Contains("BC7", StringComparison.Ordinal); || normalized.Contains("BC7", StringComparison.Ordinal);
} }

View File

@@ -8,7 +8,6 @@ using LightlessSync.UI.Tags;
using LightlessSync.WebAPI; using LightlessSync.WebAPI;
using LightlessSync.UI.Services; using LightlessSync.UI.Services;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
using LightlessSync.PlayerData.Factories;
namespace LightlessSync.Services; namespace LightlessSync.Services;
@@ -24,7 +23,6 @@ public class UiFactory
private readonly PerformanceCollectorService _performanceCollectorService; private readonly PerformanceCollectorService _performanceCollectorService;
private readonly ProfileTagService _profileTagService; private readonly ProfileTagService _profileTagService;
private readonly DalamudUtilService _dalamudUtilService; private readonly DalamudUtilService _dalamudUtilService;
private readonly PairFactory _pairFactory;
public UiFactory( public UiFactory(
ILoggerFactory loggerFactory, ILoggerFactory loggerFactory,
@@ -36,8 +34,7 @@ public class UiFactory
LightlessProfileManager lightlessProfileManager, LightlessProfileManager lightlessProfileManager,
PerformanceCollectorService performanceCollectorService, PerformanceCollectorService performanceCollectorService,
ProfileTagService profileTagService, ProfileTagService profileTagService,
DalamudUtilService dalamudUtilService, DalamudUtilService dalamudUtilService)
PairFactory pairFactory)
{ {
_loggerFactory = loggerFactory; _loggerFactory = loggerFactory;
_lightlessMediator = lightlessMediator; _lightlessMediator = lightlessMediator;
@@ -49,7 +46,6 @@ public class UiFactory
_performanceCollectorService = performanceCollectorService; _performanceCollectorService = performanceCollectorService;
_profileTagService = profileTagService; _profileTagService = profileTagService;
_dalamudUtilService = dalamudUtilService; _dalamudUtilService = dalamudUtilService;
_pairFactory = pairFactory;
} }
public SyncshellAdminUI CreateSyncshellAdminUi(GroupFullInfoDto dto) public SyncshellAdminUI CreateSyncshellAdminUi(GroupFullInfoDto dto)
@@ -62,8 +58,7 @@ public class UiFactory
_pairUiService, _pairUiService,
dto, dto,
_performanceCollectorService, _performanceCollectorService,
_lightlessProfileManager, _lightlessProfileManager);
_pairFactory);
} }
public StandaloneProfileUi CreateStandaloneProfileUi(Pair pair) public StandaloneProfileUi CreateStandaloneProfileUi(Pair pair)
@@ -110,7 +105,6 @@ public class UiFactory
groupData: groupData, groupData: groupData,
isLightfinderContext: isLightfinderContext, isLightfinderContext: isLightfinderContext,
lightfinderCid: lightfinderCid, lightfinderCid: lightfinderCid,
performanceCollector: _performanceCollectorService, performanceCollector: _performanceCollectorService);
_apiController);
} }
} }

View File

@@ -6,22 +6,18 @@ using FFXIVClientStructs.Havok.Common.Serialize.Util;
using LightlessSync.FileCache; using LightlessSync.FileCache;
using LightlessSync.Interop.GameModel; using LightlessSync.Interop.GameModel;
using LightlessSync.LightlessConfiguration; using LightlessSync.LightlessConfiguration;
using LightlessSync.PlayerData.Factories;
using LightlessSync.PlayerData.Handlers; using LightlessSync.PlayerData.Handlers;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
using System.Collections.Concurrent;
using System.Runtime.InteropServices; using System.Runtime.InteropServices;
using System.Text.RegularExpressions;
namespace LightlessSync.Services; namespace LightlessSync.Services;
public sealed partial class XivDataAnalyzer public sealed class XivDataAnalyzer
{ {
private readonly ILogger<XivDataAnalyzer> _logger; private readonly ILogger<XivDataAnalyzer> _logger;
private readonly FileCacheManager _fileCacheManager; private readonly FileCacheManager _fileCacheManager;
private readonly XivDataStorageService _configService; private readonly XivDataStorageService _configService;
private readonly List<string> _failedCalculatedTris = []; private readonly List<string> _failedCalculatedTris = [];
private readonly List<string> _failedCalculatedEffectiveTris = [];
public XivDataAnalyzer(ILogger<XivDataAnalyzer> logger, FileCacheManager fileCacheManager, public XivDataAnalyzer(ILogger<XivDataAnalyzer> logger, FileCacheManager fileCacheManager,
XivDataStorageService configService) XivDataStorageService configService)
@@ -33,441 +29,127 @@ public sealed partial class XivDataAnalyzer
public unsafe Dictionary<string, List<ushort>>? GetSkeletonBoneIndices(GameObjectHandler handler) public unsafe Dictionary<string, List<ushort>>? GetSkeletonBoneIndices(GameObjectHandler handler)
{ {
if (handler is null || handler.Address == nint.Zero) if (handler.Address == nint.Zero) return null;
return null; var chara = (CharacterBase*)(((Character*)handler.Address)->GameObject.DrawObject);
if (chara->GetModelType() != CharacterBase.ModelType.Human) return null;
Dictionary<string, HashSet<ushort>> sets = new(StringComparer.OrdinalIgnoreCase); var resHandles = chara->Skeleton->SkeletonResourceHandles;
Dictionary<string, List<ushort>> outputIndices = [];
try try
{ {
var drawObject = ((Character*)handler.Address)->GameObject.DrawObject; for (int i = 0; i < chara->Skeleton->PartialSkeletonCount; i++)
if (drawObject == null)
return null;
var chara = (CharacterBase*)drawObject;
if (chara->GetModelType() != CharacterBase.ModelType.Human)
return null;
var skeleton = chara->Skeleton;
if (skeleton == null)
return null;
var resHandles = skeleton->SkeletonResourceHandles;
var partialCount = skeleton->PartialSkeletonCount;
if (partialCount <= 0)
return null;
for (int i = 0; i < partialCount; i++)
{ {
var handle = *(resHandles + i); var handle = *(resHandles + i);
if ((nint)handle == nint.Zero) _logger.LogTrace("Iterating over SkeletonResourceHandle #{i}:{x}", i, ((nint)handle).ToString("X"));
continue; if ((nint)handle == nint.Zero) continue;
var curBones = handle->BoneCount;
if (handle->FileName.Length > 1024) // this is unrealistic, the filename shouldn't ever be that long
continue; if (handle->FileName.Length > 1024) continue;
var skeletonName = handle->FileName.ToString();
var rawName = handle->FileName.ToString(); if (string.IsNullOrEmpty(skeletonName)) continue;
if (string.IsNullOrWhiteSpace(rawName)) outputIndices[skeletonName] = [];
continue; for (ushort boneIdx = 0; boneIdx < curBones; boneIdx++)
var skeletonKey = CanonicalizeSkeletonKey(rawName);
if (string.IsNullOrEmpty(skeletonKey))
continue;
var boneCount = handle->BoneCount;
if (boneCount == 0)
continue;
var havokSkel = handle->HavokSkeleton;
if ((nint)havokSkel == nint.Zero)
continue;
if (!sets.TryGetValue(skeletonKey, out var set))
{ {
set = []; var boneName = handle->HavokSkeleton->Bones[boneIdx].Name.String;
sets[skeletonKey] = set; if (boneName == null) continue;
outputIndices[skeletonName].Add((ushort)(boneIdx + 1));
} }
uint maxExclusive = boneCount;
uint ushortExclusive = (uint)ushort.MaxValue + 1u;
if (maxExclusive > ushortExclusive)
maxExclusive = ushortExclusive;
for (uint boneIdx = 0; boneIdx < maxExclusive; boneIdx++)
{
var name = havokSkel->Bones[boneIdx].Name.String;
if (name == null)
continue;
set.Add((ushort)boneIdx);
}
_logger.LogTrace("Local skeleton raw file='{raw}', key='{key}', boneCount={count}",
rawName, skeletonKey, boneCount);
} }
} }
catch (Exception ex) catch (Exception ex)
{ {
_logger.LogWarning(ex, "Could not process skeleton data"); _logger.LogWarning(ex, "Could not process skeleton data");
return null;
} }
if (sets.Count == 0) return (outputIndices.Count != 0 && outputIndices.Values.All(u => u.Count > 0)) ? outputIndices : null;
return null;
var output = new Dictionary<string, List<ushort>>(sets.Count, StringComparer.OrdinalIgnoreCase);
foreach (var (key, set) in sets)
{
if (set.Count == 0)
continue;
var list = set.ToList();
list.Sort();
output[key] = list;
}
return (output.Count != 0 && output.Values.All(v => v.Count > 0)) ? output : null;
} }
public unsafe Dictionary<string, List<ushort>>? GetBoneIndicesFromPap(string hash, bool persistToConfig = true) public unsafe Dictionary<string, List<ushort>>? GetBoneIndicesFromPap(string hash)
{ {
if (string.IsNullOrWhiteSpace(hash)) if (_configService.Current.BonesDictionary.TryGetValue(hash, out var bones)) return bones;
return null;
if (_configService.Current.BonesDictionary.TryGetValue(hash, out var cached) && cached is not null)
return cached;
var cacheEntity = _fileCacheManager.GetFileCacheByHash(hash); var cacheEntity = _fileCacheManager.GetFileCacheByHash(hash);
if (cacheEntity == null || string.IsNullOrEmpty(cacheEntity.ResolvedFilepath) || !File.Exists(cacheEntity.ResolvedFilepath)) if (cacheEntity == null) return null;
return null;
using var fs = File.Open(cacheEntity.ResolvedFilepath, FileMode.Open, FileAccess.Read, FileShare.Read); using BinaryReader reader = new(File.Open(cacheEntity.ResolvedFilepath, FileMode.Open, FileAccess.Read, FileShare.Read));
using var reader = new BinaryReader(fs);
// PAP header (mostly from vfxeditor) // most of this shit is from vfxeditor, surely nothing will change in the pap format :copium:
_ = reader.ReadInt32(); // ignore reader.ReadInt32(); // ignore
_ = reader.ReadInt32(); // ignore reader.ReadInt32(); // ignore
_ = reader.ReadInt16(); // num animations reader.ReadInt16(); // read 2 (num animations)
_ = reader.ReadInt16(); // modelid reader.ReadInt16(); // read 2 (modelid)
var type = reader.ReadByte();// read 1 (type)
var type = reader.ReadByte(); // type if (type != 0) return null; // it's not human, just ignore it, whatever
if (type != 0)
return null; // not human
_ = reader.ReadByte(); // variant
_ = reader.ReadInt32(); // ignore
reader.ReadByte(); // read 1 (variant)
reader.ReadInt32(); // ignore
var havokPosition = reader.ReadInt32(); var havokPosition = reader.ReadInt32();
var footerPosition = reader.ReadInt32(); var footerPosition = reader.ReadInt32();
var havokDataSize = footerPosition - havokPosition;
// sanity checks
if (havokPosition <= 0 || footerPosition <= havokPosition || footerPosition > fs.Length)
return null;
var havokDataSizeLong = (long)footerPosition - havokPosition;
if (havokDataSizeLong <= 8 || havokDataSizeLong > int.MaxValue)
return null;
var havokDataSize = (int)havokDataSizeLong;
reader.BaseStream.Position = havokPosition; reader.BaseStream.Position = havokPosition;
var havokData = reader.ReadBytes(havokDataSize); var havokData = reader.ReadBytes(havokDataSize);
if (havokData.Length <= 8) if (havokData.Length <= 8) return null; // no havok data
return null;
var tempSets = new Dictionary<string, HashSet<ushort>>(StringComparer.OrdinalIgnoreCase); var output = new Dictionary<string, List<ushort>>(StringComparer.OrdinalIgnoreCase);
var tempHavokDataPath = Path.Combine(Path.GetTempPath(), Path.GetRandomFileName()) + ".hkx";
var tempHavokDataPath = Path.Combine(Path.GetTempPath(), $"lightless_{Guid.NewGuid():N}.hkx"); var tempHavokDataPathAnsi = Marshal.StringToHGlobalAnsi(tempHavokDataPath);
IntPtr tempHavokDataPathAnsi = IntPtr.Zero;
try try
{ {
File.WriteAllBytes(tempHavokDataPath, havokData); File.WriteAllBytes(tempHavokDataPath, havokData);
if (!File.Exists(tempHavokDataPath))
{
_logger.LogTrace("Temporary havok file did not exist when attempting to load: {path}", tempHavokDataPath);
return null;
}
tempHavokDataPathAnsi = Marshal.StringToHGlobalAnsi(tempHavokDataPath);
var loadoptions = stackalloc hkSerializeUtil.LoadOptions[1]; var loadoptions = stackalloc hkSerializeUtil.LoadOptions[1];
loadoptions->TypeInfoRegistry = hkBuiltinTypeRegistry.Instance()->GetTypeInfoRegistry(); loadoptions->TypeInfoRegistry = hkBuiltinTypeRegistry.Instance()->GetTypeInfoRegistry();
loadoptions->ClassNameRegistry = hkBuiltinTypeRegistry.Instance()->GetClassNameRegistry(); loadoptions->ClassNameRegistry = hkBuiltinTypeRegistry.Instance()->GetClassNameRegistry();
loadoptions->Flags = new hkFlags<hkSerializeUtil.LoadOptionBits, int> loadoptions->Flags = new hkFlags<hkSerializeUtil.LoadOptionBits, int>
{ {
Storage = (int)hkSerializeUtil.LoadOptionBits.Default Storage = (int)(hkSerializeUtil.LoadOptionBits.Default)
}; };
var resource = hkSerializeUtil.LoadFromFile((byte*)tempHavokDataPathAnsi, null, loadoptions); var resource = hkSerializeUtil.LoadFromFile((byte*)tempHavokDataPathAnsi, null, loadoptions);
if (resource == null) if (resource == null)
{ {
_logger.LogWarning("Havok resource was null after loading from {path}", tempHavokDataPath); throw new InvalidOperationException("Resource was null after loading");
return null;
} }
var rootLevelName = @"hkRootLevelContainer"u8; var rootLevelName = @"hkRootLevelContainer"u8;
fixed (byte* n1 = rootLevelName) fixed (byte* n1 = rootLevelName)
{ {
var container = (hkRootLevelContainer*)resource->GetContentsPointer(n1, hkBuiltinTypeRegistry.Instance()->GetTypeInfoRegistry()); var container = (hkRootLevelContainer*)resource->GetContentsPointer(n1, hkBuiltinTypeRegistry.Instance()->GetTypeInfoRegistry());
if (container == null)
return null;
var animationName = @"hkaAnimationContainer"u8; var animationName = @"hkaAnimationContainer"u8;
fixed (byte* n2 = animationName) fixed (byte* n2 = animationName)
{ {
var animContainer = (hkaAnimationContainer*)container->findObjectByName(n2, null); var animContainer = (hkaAnimationContainer*)container->findObjectByName(n2, null);
if (animContainer == null)
return null;
for (int i = 0; i < animContainer->Bindings.Length; i++) for (int i = 0; i < animContainer->Bindings.Length; i++)
{ {
var binding = animContainer->Bindings[i].ptr; var binding = animContainer->Bindings[i].ptr;
if (binding == null)
continue;
var rawSkel = binding->OriginalSkeletonName.String;
var skeletonKey = CanonicalizeSkeletonKey(rawSkel);
if (string.IsNullOrEmpty(skeletonKey))
continue;
var boneTransform = binding->TransformTrackToBoneIndices; var boneTransform = binding->TransformTrackToBoneIndices;
if (boneTransform.Length <= 0) string name = binding->OriginalSkeletonName.String! + "_" + i;
continue; output[name] = [];
if (!tempSets.TryGetValue(skeletonKey, out var set))
{
set = [];
tempSets[skeletonKey] = set;
}
for (int boneIdx = 0; boneIdx < boneTransform.Length; boneIdx++) for (int boneIdx = 0; boneIdx < boneTransform.Length; boneIdx++)
{ {
var v = boneTransform[boneIdx]; output[name].Add((ushort)boneTransform[boneIdx]);
if (v < 0) continue;
set.Add((ushort)v);
} }
output[name].Sort();
} }
} }
} }
} }
catch (Exception ex) catch (Exception ex)
{ {
_logger.LogWarning(ex, "Could not load havok file in {path}", tempHavokDataPath); _logger.LogWarning(ex, "Could not load havok file in {path}", tempHavokDataPath);
return null;
} }
finally finally
{ {
if (tempHavokDataPathAnsi != IntPtr.Zero) Marshal.FreeHGlobal(tempHavokDataPathAnsi);
Marshal.FreeHGlobal(tempHavokDataPathAnsi); File.Delete(tempHavokDataPath);
try
{
if (File.Exists(tempHavokDataPath))
File.Delete(tempHavokDataPath);
}
catch (Exception ex)
{
_logger.LogTrace(ex, "Could not delete temporary havok file: {path}", tempHavokDataPath);
}
} }
if (tempSets.Count == 0)
return null;
var output = new Dictionary<string, List<ushort>>(tempSets.Count, StringComparer.OrdinalIgnoreCase);
foreach (var (key, set) in tempSets)
{
if (set.Count == 0) continue;
var list = set.ToList();
list.Sort();
output[key] = list;
}
if (output.Count == 0)
return null;
_configService.Current.BonesDictionary[hash] = output; _configService.Current.BonesDictionary[hash] = output;
_configService.Save();
if (persistToConfig)
_configService.Save();
return output; return output;
} }
public static string CanonicalizeSkeletonKey(string? raw)
{
if (string.IsNullOrWhiteSpace(raw))
return string.Empty;
var s = raw.Replace('\\', '/').Trim();
var underscore = s.LastIndexOf('_');
if (underscore > 0 && underscore + 1 < s.Length && char.IsDigit(s[underscore + 1]))
s = s[..underscore];
if (s.StartsWith("skeleton", StringComparison.OrdinalIgnoreCase))
return "skeleton";
var m = _bucketPathRegex.Match(s);
if (m.Success)
return m.Groups["bucket"].Value.ToLowerInvariant();
m = _bucketSklRegex.Match(s);
if (m.Success)
return m.Groups["bucket"].Value.ToLowerInvariant();
m = _bucketLooseRegex.Match(s);
if (m.Success)
return m.Groups["bucket"].Value.ToLowerInvariant();
return string.Empty;
}
public static bool ContainsIndexCompat(
HashSet<ushort> available,
ushort idx,
bool papLikelyOneBased,
bool allowOneBasedShift,
bool allowNeighborTolerance)
{
Span<ushort> candidates = stackalloc ushort[2];
int count = 0;
candidates[count++] = idx;
if (allowOneBasedShift && papLikelyOneBased && idx > 0)
candidates[count++] = (ushort)(idx - 1);
for (int i = 0; i < count; i++)
{
var c = candidates[i];
if (available.Contains(c))
return true;
if (allowNeighborTolerance)
{
if (c > 0 && available.Contains((ushort)(c - 1)))
return true;
if (c < ushort.MaxValue && available.Contains((ushort)(c + 1)))
return true;
}
}
return false;
}
public static bool IsPapCompatible(
IReadOnlyDictionary<string, HashSet<ushort>> localBoneSets,
IReadOnlyDictionary<string, List<ushort>> papBoneIndices,
AnimationValidationMode mode,
bool allowOneBasedShift,
bool allowNeighborTolerance,
out string reason)
{
reason = string.Empty;
if (mode == AnimationValidationMode.Unsafe)
return true;
var papBuckets = papBoneIndices.Keys
.Select(CanonicalizeSkeletonKey)
.Where(k => !string.IsNullOrEmpty(k))
.Distinct(StringComparer.OrdinalIgnoreCase)
.ToList();
if (papBuckets.Count == 0)
{
reason = "No skeleton bucket bindings found in the PAP";
return false;
}
if (mode == AnimationValidationMode.Safe)
{
if (papBuckets.Any(b => localBoneSets.ContainsKey(b)))
return true;
reason = $"No matching skeleton bucket between PAP [{string.Join(", ", papBuckets)}] and local [{string.Join(", ", localBoneSets.Keys.Order())}].";
return false;
}
foreach (var bucket in papBuckets)
{
if (!localBoneSets.TryGetValue(bucket, out var available))
{
reason = $"Missing skeleton bucket '{bucket}' on local actor.";
return false;
}
var indices = papBoneIndices
.Where(kvp => string.Equals(CanonicalizeSkeletonKey(kvp.Key), bucket, StringComparison.OrdinalIgnoreCase))
.SelectMany(kvp => kvp.Value ?? Enumerable.Empty<ushort>())
.Distinct()
.ToList();
if (indices.Count == 0)
continue;
bool has0 = false, has1 = false;
ushort min = ushort.MaxValue;
foreach (var v in indices)
{
if (v == 0) has0 = true;
if (v == 1) has1 = true;
if (v < min) min = v;
}
bool papLikelyOneBased = allowOneBasedShift && (min == 1) && has1 && !has0;
foreach (var idx in indices)
{
if (!ContainsIndexCompat(available, idx, papLikelyOneBased, allowOneBasedShift, allowNeighborTolerance))
{
reason = $"No compatible local skeleton for PAP '{bucket}': missing bone index {idx}.";
return false;
}
}
}
return true;
}
public void DumpLocalSkeletonIndices(GameObjectHandler handler, string? filter = null)
{
var skels = GetSkeletonBoneIndices(handler);
if (skels == null)
{
_logger.LogTrace("DumpLocalSkeletonIndices: local skeleton indices are null or not found");
return;
}
var keys = skels.Keys
.Order(StringComparer.OrdinalIgnoreCase)
.ToArray();
_logger.LogTrace("Local skeleton indices found ({count}): {keys}",
keys.Length,
string.Join(", ", keys));
if (!string.IsNullOrWhiteSpace(filter))
{
var hits = keys.Where(k =>
k.Equals(filter, StringComparison.OrdinalIgnoreCase) ||
k.StartsWith(filter + "_", StringComparison.OrdinalIgnoreCase) ||
filter.StartsWith(k + "_", StringComparison.OrdinalIgnoreCase) ||
k.Contains(filter, StringComparison.OrdinalIgnoreCase))
.ToArray();
_logger.LogTrace("Matches found for '{filter}': {hits}",
filter,
hits.Length == 0 ? "<none>" : string.Join(", ", hits));
}
}
public async Task<long> GetTrianglesByHash(string hash) public async Task<long> GetTrianglesByHash(string hash)
{ {
if (_configService.Current.TriangleDictionary.TryGetValue(hash, out var cachedTris) && cachedTris > 0) if (_configService.Current.TriangleDictionary.TryGetValue(hash, out var cachedTris) && cachedTris > 0)
@@ -480,41 +162,16 @@ public sealed partial class XivDataAnalyzer
if (path == null || !path.ResolvedFilepath.EndsWith(".mdl", StringComparison.OrdinalIgnoreCase)) if (path == null || !path.ResolvedFilepath.EndsWith(".mdl", StringComparison.OrdinalIgnoreCase))
return 0; return 0;
return CalculateTrianglesFromPath(hash, path.ResolvedFilepath, _configService.Current.TriangleDictionary, _failedCalculatedTris); var filePath = path.ResolvedFilepath;
}
public async Task<long> GetEffectiveTrianglesByHash(string hash, string filePath)
{
if (_configService.Current.EffectiveTriangleDictionary.TryGetValue(hash, out var cachedTris) && cachedTris > 0)
return cachedTris;
if (_failedCalculatedEffectiveTris.Contains(hash, StringComparer.Ordinal))
return 0;
if (string.IsNullOrEmpty(filePath)
|| !filePath.EndsWith(".mdl", StringComparison.OrdinalIgnoreCase)
|| !File.Exists(filePath))
{
return 0;
}
return CalculateTrianglesFromPath(hash, filePath, _configService.Current.EffectiveTriangleDictionary, _failedCalculatedEffectiveTris);
}
private long CalculateTrianglesFromPath(
string hash,
string filePath,
ConcurrentDictionary<string, long> cache,
List<string> failedList)
{
try try
{ {
_logger.LogDebug("Detected Model File {path}, calculating Tris", filePath); _logger.LogDebug("Detected Model File {path}, calculating Tris", filePath);
var file = new MdlFile(filePath); var file = new MdlFile(filePath);
if (file.LodCount <= 0) if (file.LodCount <= 0)
{ {
failedList.Add(hash); _failedCalculatedTris.Add(hash);
cache[hash] = 0; _configService.Current.TriangleDictionary[hash] = 0;
_configService.Save(); _configService.Save();
return 0; return 0;
} }
@@ -538,7 +195,7 @@ public sealed partial class XivDataAnalyzer
if (tris > 0) if (tris > 0)
{ {
_logger.LogDebug("TriAnalysis: {filePath} => {tris} triangles", filePath, tris); _logger.LogDebug("TriAnalysis: {filePath} => {tris} triangles", filePath, tris);
cache[hash] = tris; _configService.Current.TriangleDictionary[hash] = tris;
_configService.Save(); _configService.Save();
break; break;
} }
@@ -548,30 +205,11 @@ public sealed partial class XivDataAnalyzer
} }
catch (Exception e) catch (Exception e)
{ {
failedList.Add(hash); _failedCalculatedTris.Add(hash);
cache[hash] = 0; _configService.Current.TriangleDictionary[hash] = 0;
_configService.Save(); _configService.Save();
_logger.LogWarning(e, "Could not parse file {file}", filePath); _logger.LogWarning(e, "Could not parse file {file}", filePath);
return 0; return 0;
} }
} }
// Regexes for canonicalizing skeleton keys
private static readonly Regex _bucketPathRegex =
BucketRegex();
private static readonly Regex _bucketSklRegex =
SklRegex();
private static readonly Regex _bucketLooseRegex =
LooseBucketRegex();
[GeneratedRegex(@"(?i)(?:^|/)(?<bucket>c\d{4})(?:/|$)", RegexOptions.Compiled, "en-NL")]
private static partial Regex BucketRegex();
[GeneratedRegex(@"(?i)\bskl_(?<bucket>c\d{4})[a-z]\d{4}\b", RegexOptions.Compiled, "en-NL")]
private static partial Regex SklRegex();
[GeneratedRegex(@"(?i)(?<![a-z0-9])(?<bucket>c\d{4})(?!\d)", RegexOptions.Compiled, "en-NL")]
private static partial Regex LooseBucketRegex();
} }

View File

@@ -1,169 +0,0 @@
#region License
/*
MIT License
Copyright(c) 2017-2018 Mattias Edlund
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
*/
#endregion
using System;
using Microsoft.Extensions.Logging;
namespace MeshDecimator.Algorithms
{
/// <summary>
/// A decimation algorithm.
/// </summary>
public abstract class DecimationAlgorithm
{
#region Delegates
/// <summary>
/// A callback for decimation status reports.
/// </summary>
/// <param name="iteration">The current iteration, starting at zero.</param>
/// <param name="originalTris">The original count of triangles.</param>
/// <param name="currentTris">The current count of triangles.</param>
/// <param name="targetTris">The target count of triangles.</param>
public delegate void StatusReportCallback(int iteration, int originalTris, int currentTris, int targetTris);
#endregion
#region Fields
private bool preserveBorders = false;
private int maxVertexCount = 0;
private bool verbose = false;
private StatusReportCallback statusReportInvoker = null;
#endregion
#region Properties
/// <summary>
/// Gets or sets if borders should be kept.
/// Default value: false
/// </summary>
[Obsolete("Use the 'DecimationAlgorithm.PreserveBorders' property instead.", false)]
public bool KeepBorders
{
get { return preserveBorders; }
set { preserveBorders = value; }
}
/// <summary>
/// Gets or sets if borders should be preserved.
/// Default value: false
/// </summary>
public bool PreserveBorders
{
get { return preserveBorders; }
set { preserveBorders = value; }
}
/// <summary>
/// Gets or sets if linked vertices should be kept.
/// Default value: false
/// </summary>
[Obsolete("This feature has been removed, for more details why please read the readme.", true)]
public bool KeepLinkedVertices
{
get { return false; }
set { }
}
/// <summary>
/// Gets or sets the maximum vertex count. Set to zero for no limitation.
/// Default value: 0 (no limitation)
/// </summary>
public int MaxVertexCount
{
get { return maxVertexCount; }
set { maxVertexCount = Math.MathHelper.Max(value, 0); }
}
/// <summary>
/// Gets or sets if verbose information should be printed in the console.
/// Default value: false
/// </summary>
public bool Verbose
{
get { return verbose; }
set { verbose = value; }
}
/// <summary>
/// Gets or sets the logger used for diagnostics.
/// </summary>
public ILogger? Logger { get; set; }
#endregion
#region Events
/// <summary>
/// An event for status reports for this algorithm.
/// </summary>
public event StatusReportCallback StatusReport
{
add { statusReportInvoker += value; }
remove { statusReportInvoker -= value; }
}
#endregion
#region Protected Methods
/// <summary>
/// Reports the current status of the decimation.
/// </summary>
/// <param name="iteration">The current iteration, starting at zero.</param>
/// <param name="originalTris">The original count of triangles.</param>
/// <param name="currentTris">The current count of triangles.</param>
/// <param name="targetTris">The target count of triangles.</param>
protected void ReportStatus(int iteration, int originalTris, int currentTris, int targetTris)
{
var statusReportInvoker = this.statusReportInvoker;
if (statusReportInvoker != null)
{
statusReportInvoker.Invoke(iteration, originalTris, currentTris, targetTris);
}
}
#endregion
#region Public Methods
/// <summary>
/// Initializes the algorithm with the original mesh.
/// </summary>
/// <param name="mesh">The mesh.</param>
public abstract void Initialize(Mesh mesh);
/// <summary>
/// Decimates the mesh.
/// </summary>
/// <param name="targetTrisCount">The target triangle count.</param>
public abstract void DecimateMesh(int targetTrisCount);
/// <summary>
/// Decimates the mesh without losing any quality.
/// </summary>
public abstract void DecimateMeshLossless();
/// <summary>
/// Returns the resulting mesh.
/// </summary>
/// <returns>The resulting mesh.</returns>
public abstract Mesh ToMesh();
#endregion
}
}

View File

@@ -1,249 +0,0 @@
#region License
/*
MIT License
Copyright(c) 2017-2018 Mattias Edlund
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
*/
#endregion
using System;
using MeshDecimator.Math;
namespace MeshDecimator
{
/// <summary>
/// A bone weight.
/// </summary>
public struct BoneWeight : IEquatable<BoneWeight>
{
#region Fields
/// <summary>
/// The first bone index.
/// </summary>
public int boneIndex0;
/// <summary>
/// The second bone index.
/// </summary>
public int boneIndex1;
/// <summary>
/// The third bone index.
/// </summary>
public int boneIndex2;
/// <summary>
/// The fourth bone index.
/// </summary>
public int boneIndex3;
/// <summary>
/// The first bone weight.
/// </summary>
public float boneWeight0;
/// <summary>
/// The second bone weight.
/// </summary>
public float boneWeight1;
/// <summary>
/// The third bone weight.
/// </summary>
public float boneWeight2;
/// <summary>
/// The fourth bone weight.
/// </summary>
public float boneWeight3;
#endregion
#region Constructor
/// <summary>
/// Creates a new bone weight.
/// </summary>
/// <param name="boneIndex0">The first bone index.</param>
/// <param name="boneIndex1">The second bone index.</param>
/// <param name="boneIndex2">The third bone index.</param>
/// <param name="boneIndex3">The fourth bone index.</param>
/// <param name="boneWeight0">The first bone weight.</param>
/// <param name="boneWeight1">The second bone weight.</param>
/// <param name="boneWeight2">The third bone weight.</param>
/// <param name="boneWeight3">The fourth bone weight.</param>
public BoneWeight(int boneIndex0, int boneIndex1, int boneIndex2, int boneIndex3, float boneWeight0, float boneWeight1, float boneWeight2, float boneWeight3)
{
this.boneIndex0 = boneIndex0;
this.boneIndex1 = boneIndex1;
this.boneIndex2 = boneIndex2;
this.boneIndex3 = boneIndex3;
this.boneWeight0 = boneWeight0;
this.boneWeight1 = boneWeight1;
this.boneWeight2 = boneWeight2;
this.boneWeight3 = boneWeight3;
}
#endregion
#region Operators
/// <summary>
/// Returns if two bone weights equals eachother.
/// </summary>
/// <param name="lhs">The left hand side bone weight.</param>
/// <param name="rhs">The right hand side bone weight.</param>
/// <returns>If equals.</returns>
public static bool operator ==(BoneWeight lhs, BoneWeight rhs)
{
return (lhs.boneIndex0 == rhs.boneIndex0 && lhs.boneIndex1 == rhs.boneIndex1 && lhs.boneIndex2 == rhs.boneIndex2 && lhs.boneIndex3 == rhs.boneIndex3 &&
new Vector4(lhs.boneWeight0, lhs.boneWeight1, lhs.boneWeight2, lhs.boneWeight3) == new Vector4(rhs.boneWeight0, rhs.boneWeight1, rhs.boneWeight2, rhs.boneWeight3));
}
/// <summary>
/// Returns if two bone weights don't equal eachother.
/// </summary>
/// <param name="lhs">The left hand side bone weight.</param>
/// <param name="rhs">The right hand side bone weight.</param>
/// <returns>If not equals.</returns>
public static bool operator !=(BoneWeight lhs, BoneWeight rhs)
{
return !(lhs == rhs);
}
#endregion
#region Private Methods
private void MergeBoneWeight(int boneIndex, float weight)
{
if (boneIndex == boneIndex0)
{
boneWeight0 = (boneWeight0 + weight) * 0.5f;
}
else if (boneIndex == boneIndex1)
{
boneWeight1 = (boneWeight1 + weight) * 0.5f;
}
else if (boneIndex == boneIndex2)
{
boneWeight2 = (boneWeight2 + weight) * 0.5f;
}
else if (boneIndex == boneIndex3)
{
boneWeight3 = (boneWeight3 + weight) * 0.5f;
}
else if(boneWeight0 == 0f)
{
boneIndex0 = boneIndex;
boneWeight0 = weight;
}
else if (boneWeight1 == 0f)
{
boneIndex1 = boneIndex;
boneWeight1 = weight;
}
else if (boneWeight2 == 0f)
{
boneIndex2 = boneIndex;
boneWeight2 = weight;
}
else if (boneWeight3 == 0f)
{
boneIndex3 = boneIndex;
boneWeight3 = weight;
}
Normalize();
}
private void Normalize()
{
float mag = (float)System.Math.Sqrt(boneWeight0 * boneWeight0 + boneWeight1 * boneWeight1 + boneWeight2 * boneWeight2 + boneWeight3 * boneWeight3);
if (mag > float.Epsilon)
{
boneWeight0 /= mag;
boneWeight1 /= mag;
boneWeight2 /= mag;
boneWeight3 /= mag;
}
else
{
boneWeight0 = boneWeight1 = boneWeight2 = boneWeight3 = 0f;
}
}
#endregion
#region Public Methods
#region Object
/// <summary>
/// Returns a hash code for this vector.
/// </summary>
/// <returns>The hash code.</returns>
public override int GetHashCode()
{
return boneIndex0.GetHashCode() ^ boneIndex1.GetHashCode() << 2 ^ boneIndex2.GetHashCode() >> 2 ^ boneIndex3.GetHashCode() >>
1 ^ boneWeight0.GetHashCode() << 5 ^ boneWeight1.GetHashCode() << 4 ^ boneWeight2.GetHashCode() >> 4 ^ boneWeight3.GetHashCode() >> 3;
}
/// <summary>
/// Returns if this bone weight is equal to another object.
/// </summary>
/// <param name="obj">The other object to compare to.</param>
/// <returns>If equals.</returns>
public override bool Equals(object obj)
{
if (!(obj is BoneWeight))
{
return false;
}
BoneWeight other = (BoneWeight)obj;
return (boneIndex0 == other.boneIndex0 && boneIndex1 == other.boneIndex1 && boneIndex2 == other.boneIndex2 && boneIndex3 == other.boneIndex3 &&
boneWeight0 == other.boneWeight0 && boneWeight1 == other.boneWeight1 && boneWeight2 == other.boneWeight2 && boneWeight3 == other.boneWeight3);
}
/// <summary>
/// Returns if this bone weight is equal to another one.
/// </summary>
/// <param name="other">The other bone weight to compare to.</param>
/// <returns>If equals.</returns>
public bool Equals(BoneWeight other)
{
return (boneIndex0 == other.boneIndex0 && boneIndex1 == other.boneIndex1 && boneIndex2 == other.boneIndex2 && boneIndex3 == other.boneIndex3 &&
boneWeight0 == other.boneWeight0 && boneWeight1 == other.boneWeight1 && boneWeight2 == other.boneWeight2 && boneWeight3 == other.boneWeight3);
}
/// <summary>
/// Returns a nicely formatted string for this bone weight.
/// </summary>
/// <returns>The string.</returns>
public override string ToString()
{
return string.Format("({0}:{4:F1}, {1}:{5:F1}, {2}:{6:F1}, {3}:{7:F1})",
boneIndex0, boneIndex1, boneIndex2, boneIndex3, boneWeight0, boneWeight1, boneWeight2, boneWeight3);
}
#endregion
#region Static
/// <summary>
/// Merges two bone weights and stores the merged result in the first parameter.
/// </summary>
/// <param name="a">The first bone weight, also stores result.</param>
/// <param name="b">The second bone weight.</param>
public static void Merge(ref BoneWeight a, ref BoneWeight b)
{
if (b.boneWeight0 > 0f) a.MergeBoneWeight(b.boneIndex0, b.boneWeight0);
if (b.boneWeight1 > 0f) a.MergeBoneWeight(b.boneIndex1, b.boneWeight1);
if (b.boneWeight2 > 0f) a.MergeBoneWeight(b.boneIndex2, b.boneWeight2);
if (b.boneWeight3 > 0f) a.MergeBoneWeight(b.boneIndex3, b.boneWeight3);
}
#endregion
#endregion
}
}

View File

@@ -1,179 +0,0 @@
#region License
/*
MIT License
Copyright(c) 2017-2018 Mattias Edlund
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
*/
#endregion
using System;
namespace MeshDecimator.Collections
{
/// <summary>
/// A resizable array.
/// </summary>
/// <typeparam name="T">The item type.</typeparam>
internal sealed class ResizableArray<T>
{
#region Fields
private T[] items = null;
private int length = 0;
private static T[] emptyArr = new T[0];
#endregion
#region Properties
/// <summary>
/// Gets the length of this array.
/// </summary>
public int Length
{
get { return length; }
}
/// <summary>
/// Gets the internal data buffer for this array.
/// </summary>
public T[] Data
{
get { return items; }
}
/// <summary>
/// Gets or sets the element value at a specific index.
/// </summary>
/// <param name="index">The element index.</param>
/// <returns>The element value.</returns>
public T this[int index]
{
get { return items[index]; }
set { items[index] = value; }
}
#endregion
#region Constructor
/// <summary>
/// Creates a new resizable array.
/// </summary>
/// <param name="capacity">The initial array capacity.</param>
public ResizableArray(int capacity)
: this(capacity, 0)
{
}
/// <summary>
/// Creates a new resizable array.
/// </summary>
/// <param name="capacity">The initial array capacity.</param>
/// <param name="length">The initial length of the array.</param>
public ResizableArray(int capacity, int length)
{
if (capacity < 0)
throw new ArgumentOutOfRangeException("capacity");
else if (length < 0 || length > capacity)
throw new ArgumentOutOfRangeException("length");
if (capacity > 0)
items = new T[capacity];
else
items = emptyArr;
this.length = length;
}
#endregion
#region Private Methods
private void IncreaseCapacity(int capacity)
{
T[] newItems = new T[capacity];
Array.Copy(items, 0, newItems, 0, System.Math.Min(length, capacity));
items = newItems;
}
#endregion
#region Public Methods
/// <summary>
/// Clears this array.
/// </summary>
public void Clear()
{
Array.Clear(items, 0, length);
length = 0;
}
/// <summary>
/// Resizes this array.
/// </summary>
/// <param name="length">The new length.</param>
/// <param name="trimExess">If exess memory should be trimmed.</param>
public void Resize(int length, bool trimExess = false)
{
if (length < 0)
throw new ArgumentOutOfRangeException("capacity");
if (length > items.Length)
{
IncreaseCapacity(length);
}
else if (length < this.length)
{
//Array.Clear(items, capacity, length - capacity);
}
this.length = length;
if (trimExess)
{
TrimExcess();
}
}
/// <summary>
/// Trims any excess memory for this array.
/// </summary>
public void TrimExcess()
{
if (items.Length == length) // Nothing to do
return;
T[] newItems = new T[length];
Array.Copy(items, 0, newItems, 0, length);
items = newItems;
}
/// <summary>
/// Adds a new item to the end of this array.
/// </summary>
/// <param name="item">The new item.</param>
public void Add(T item)
{
if (length >= items.Length)
{
IncreaseCapacity(items.Length << 1);
}
items[length++] = item;
}
#endregion
}
}

View File

@@ -1,79 +0,0 @@
using System;
namespace MeshDecimator.Collections
{
/// <summary>
/// A collection of UV channels.
/// </summary>
/// <typeparam name="TVec">The UV vector type.</typeparam>
internal sealed class UVChannels<TVec>
{
#region Fields
private ResizableArray<TVec>[] channels = null;
private TVec[][] channelsData = null;
#endregion
#region Properties
/// <summary>
/// Gets the channel collection data.
/// </summary>
public TVec[][] Data
{
get
{
for (int i = 0; i < Mesh.UVChannelCount; i++)
{
if (channels[i] != null)
{
channelsData[i] = channels[i].Data;
}
else
{
channelsData[i] = null;
}
}
return channelsData;
}
}
/// <summary>
/// Gets or sets a specific channel by index.
/// </summary>
/// <param name="index">The channel index.</param>
public ResizableArray<TVec> this[int index]
{
get { return channels[index]; }
set { channels[index] = value; }
}
#endregion
#region Constructor
/// <summary>
/// Creates a new collection of UV channels.
/// </summary>
public UVChannels()
{
channels = new ResizableArray<TVec>[Mesh.UVChannelCount];
channelsData = new TVec[Mesh.UVChannelCount][];
}
#endregion
#region Public Methods
/// <summary>
/// Resizes all channels at once.
/// </summary>
/// <param name="capacity">The new capacity.</param>
/// <param name="trimExess">If exess memory should be trimmed.</param>
public void Resize(int capacity, bool trimExess = false)
{
for (int i = 0; i < Mesh.UVChannelCount; i++)
{
if (channels[i] != null)
{
channels[i].Resize(capacity, trimExess);
}
}
}
#endregion
}
}

View File

@@ -1,21 +0,0 @@
MIT License
Copyright (c) 2017-2018 Mattias Edlund
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View File

@@ -1,286 +0,0 @@
#region License
/*
MIT License
Copyright(c) 2017-2018 Mattias Edlund
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
*/
#endregion
using System;
namespace MeshDecimator.Math
{
/// <summary>
/// Math helpers.
/// </summary>
public static class MathHelper
{
#region Consts
/// <summary>
/// The Pi constant.
/// </summary>
public const float PI = 3.14159274f;
/// <summary>
/// The Pi constant.
/// </summary>
public const double PId = 3.1415926535897932384626433832795;
/// <summary>
/// Degrees to radian constant.
/// </summary>
public const float Deg2Rad = PI / 180f;
/// <summary>
/// Degrees to radian constant.
/// </summary>
public const double Deg2Radd = PId / 180.0;
/// <summary>
/// Radians to degrees constant.
/// </summary>
public const float Rad2Deg = 180f / PI;
/// <summary>
/// Radians to degrees constant.
/// </summary>
public const double Rad2Degd = 180.0 / PId;
#endregion
#region Min
/// <summary>
/// Returns the minimum of two values.
/// </summary>
/// <param name="val1">The first value.</param>
/// <param name="val2">The second value.</param>
/// <returns>The minimum value.</returns>
public static int Min(int val1, int val2)
{
return (val1 < val2 ? val1 : val2);
}
/// <summary>
/// Returns the minimum of three values.
/// </summary>
/// <param name="val1">The first value.</param>
/// <param name="val2">The second value.</param>
/// <param name="val3">The third value.</param>
/// <returns>The minimum value.</returns>
public static int Min(int val1, int val2, int val3)
{
return (val1 < val2 ? (val1 < val3 ? val1 : val3) : (val2 < val3 ? val2 : val3));
}
/// <summary>
/// Returns the minimum of two values.
/// </summary>
/// <param name="val1">The first value.</param>
/// <param name="val2">The second value.</param>
/// <returns>The minimum value.</returns>
public static float Min(float val1, float val2)
{
return (val1 < val2 ? val1 : val2);
}
/// <summary>
/// Returns the minimum of three values.
/// </summary>
/// <param name="val1">The first value.</param>
/// <param name="val2">The second value.</param>
/// <param name="val3">The third value.</param>
/// <returns>The minimum value.</returns>
public static float Min(float val1, float val2, float val3)
{
return (val1 < val2 ? (val1 < val3 ? val1 : val3) : (val2 < val3 ? val2 : val3));
}
/// <summary>
/// Returns the minimum of two values.
/// </summary>
/// <param name="val1">The first value.</param>
/// <param name="val2">The second value.</param>
/// <returns>The minimum value.</returns>
public static double Min(double val1, double val2)
{
return (val1 < val2 ? val1 : val2);
}
/// <summary>
/// Returns the minimum of three values.
/// </summary>
/// <param name="val1">The first value.</param>
/// <param name="val2">The second value.</param>
/// <param name="val3">The third value.</param>
/// <returns>The minimum value.</returns>
public static double Min(double val1, double val2, double val3)
{
return (val1 < val2 ? (val1 < val3 ? val1 : val3) : (val2 < val3 ? val2 : val3));
}
#endregion
#region Max
/// <summary>
/// Returns the maximum of two values.
/// </summary>
/// <param name="val1">The first value.</param>
/// <param name="val2">The second value.</param>
/// <returns>The maximum value.</returns>
public static int Max(int val1, int val2)
{
return (val1 > val2 ? val1 : val2);
}
/// <summary>
/// Returns the maximum of three values.
/// </summary>
/// <param name="val1">The first value.</param>
/// <param name="val2">The second value.</param>
/// <param name="val3">The third value.</param>
/// <returns>The maximum value.</returns>
public static int Max(int val1, int val2, int val3)
{
return (val1 > val2 ? (val1 > val3 ? val1 : val3) : (val2 > val3 ? val2 : val3));
}
/// <summary>
/// Returns the maximum of two values.
/// </summary>
/// <param name="val1">The first value.</param>
/// <param name="val2">The second value.</param>
/// <returns>The maximum value.</returns>
public static float Max(float val1, float val2)
{
return (val1 > val2 ? val1 : val2);
}
/// <summary>
/// Returns the maximum of three values.
/// </summary>
/// <param name="val1">The first value.</param>
/// <param name="val2">The second value.</param>
/// <param name="val3">The third value.</param>
/// <returns>The maximum value.</returns>
public static float Max(float val1, float val2, float val3)
{
return (val1 > val2 ? (val1 > val3 ? val1 : val3) : (val2 > val3 ? val2 : val3));
}
/// <summary>
/// Returns the maximum of two values.
/// </summary>
/// <param name="val1">The first value.</param>
/// <param name="val2">The second value.</param>
/// <returns>The maximum value.</returns>
public static double Max(double val1, double val2)
{
return (val1 > val2 ? val1 : val2);
}
/// <summary>
/// Returns the maximum of three values.
/// </summary>
/// <param name="val1">The first value.</param>
/// <param name="val2">The second value.</param>
/// <param name="val3">The third value.</param>
/// <returns>The maximum value.</returns>
public static double Max(double val1, double val2, double val3)
{
return (val1 > val2 ? (val1 > val3 ? val1 : val3) : (val2 > val3 ? val2 : val3));
}
#endregion
#region Clamping
/// <summary>
/// Clamps a value between a minimum and a maximum value.
/// </summary>
/// <param name="value">The value to clamp.</param>
/// <param name="min">The minimum value.</param>
/// <param name="max">The maximum value.</param>
/// <returns>The clamped value.</returns>
public static float Clamp(float value, float min, float max)
{
return (value >= min ? (value <= max ? value : max) : min);
}
/// <summary>
/// Clamps a value between a minimum and a maximum value.
/// </summary>
/// <param name="value">The value to clamp.</param>
/// <param name="min">The minimum value.</param>
/// <param name="max">The maximum value.</param>
/// <returns>The clamped value.</returns>
public static double Clamp(double value, double min, double max)
{
return (value >= min ? (value <= max ? value : max) : min);
}
/// <summary>
/// Clamps the value between 0 and 1.
/// </summary>
/// <param name="value">The value to clamp.</param>
/// <returns>The clamped value.</returns>
public static float Clamp01(float value)
{
return (value > 0f ? (value < 1f ? value : 1f) : 0f);
}
/// <summary>
/// Clamps the value between 0 and 1.
/// </summary>
/// <param name="value">The value to clamp.</param>
/// <returns>The clamped value.</returns>
public static double Clamp01(double value)
{
return (value > 0.0 ? (value < 1.0 ? value : 1.0) : 0.0);
}
#endregion
#region Triangle Area
/// <summary>
/// Calculates the area of a triangle.
/// </summary>
/// <param name="p0">The first point.</param>
/// <param name="p1">The second point.</param>
/// <param name="p2">The third point.</param>
/// <returns>The triangle area.</returns>
public static float TriangleArea(ref Vector3 p0, ref Vector3 p1, ref Vector3 p2)
{
var dx = p1 - p0;
var dy = p2 - p0;
return dx.Magnitude * ((float)System.Math.Sin(Vector3.Angle(ref dx, ref dy) * Deg2Rad) * dy.Magnitude) * 0.5f;
}
/// <summary>
/// Calculates the area of a triangle.
/// </summary>
/// <param name="p0">The first point.</param>
/// <param name="p1">The second point.</param>
/// <param name="p2">The third point.</param>
/// <returns>The triangle area.</returns>
public static double TriangleArea(ref Vector3d p0, ref Vector3d p1, ref Vector3d p2)
{
var dx = p1 - p0;
var dy = p2 - p0;
return dx.Magnitude * (System.Math.Sin(Vector3d.Angle(ref dx, ref dy) * Deg2Radd) * dy.Magnitude) * 0.5f;
}
#endregion
}
}

View File

@@ -1,303 +0,0 @@
#region License
/*
MIT License
Copyright(c) 2017-2018 Mattias Edlund
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
*/
#endregion
using System;
namespace MeshDecimator.Math
{
/// <summary>
/// A symmetric matrix.
/// </summary>
public struct SymmetricMatrix
{
#region Fields
/// <summary>
/// The m11 component.
/// </summary>
public double m0;
/// <summary>
/// The m12 component.
/// </summary>
public double m1;
/// <summary>
/// The m13 component.
/// </summary>
public double m2;
/// <summary>
/// The m14 component.
/// </summary>
public double m3;
/// <summary>
/// The m22 component.
/// </summary>
public double m4;
/// <summary>
/// The m23 component.
/// </summary>
public double m5;
/// <summary>
/// The m24 component.
/// </summary>
public double m6;
/// <summary>
/// The m33 component.
/// </summary>
public double m7;
/// <summary>
/// The m34 component.
/// </summary>
public double m8;
/// <summary>
/// The m44 component.
/// </summary>
public double m9;
#endregion
#region Properties
/// <summary>
/// Gets the component value with a specific index.
/// </summary>
/// <param name="index">The component index.</param>
/// <returns>The value.</returns>
public double this[int index]
{
get
{
switch (index)
{
case 0:
return m0;
case 1:
return m1;
case 2:
return m2;
case 3:
return m3;
case 4:
return m4;
case 5:
return m5;
case 6:
return m6;
case 7:
return m7;
case 8:
return m8;
case 9:
return m9;
default:
throw new IndexOutOfRangeException();
}
}
}
#endregion
#region Constructor
/// <summary>
/// Creates a symmetric matrix with a value in each component.
/// </summary>
/// <param name="c">The component value.</param>
public SymmetricMatrix(double c)
{
this.m0 = c;
this.m1 = c;
this.m2 = c;
this.m3 = c;
this.m4 = c;
this.m5 = c;
this.m6 = c;
this.m7 = c;
this.m8 = c;
this.m9 = c;
}
/// <summary>
/// Creates a symmetric matrix.
/// </summary>
/// <param name="m0">The m11 component.</param>
/// <param name="m1">The m12 component.</param>
/// <param name="m2">The m13 component.</param>
/// <param name="m3">The m14 component.</param>
/// <param name="m4">The m22 component.</param>
/// <param name="m5">The m23 component.</param>
/// <param name="m6">The m24 component.</param>
/// <param name="m7">The m33 component.</param>
/// <param name="m8">The m34 component.</param>
/// <param name="m9">The m44 component.</param>
public SymmetricMatrix(double m0, double m1, double m2, double m3,
double m4, double m5, double m6, double m7, double m8, double m9)
{
this.m0 = m0;
this.m1 = m1;
this.m2 = m2;
this.m3 = m3;
this.m4 = m4;
this.m5 = m5;
this.m6 = m6;
this.m7 = m7;
this.m8 = m8;
this.m9 = m9;
}
/// <summary>
/// Creates a symmetric matrix from a plane.
/// </summary>
/// <param name="a">The plane x-component.</param>
/// <param name="b">The plane y-component</param>
/// <param name="c">The plane z-component</param>
/// <param name="d">The plane w-component</param>
public SymmetricMatrix(double a, double b, double c, double d)
{
this.m0 = a * a;
this.m1 = a * b;
this.m2 = a * c;
this.m3 = a * d;
this.m4 = b * b;
this.m5 = b * c;
this.m6 = b * d;
this.m7 = c * c;
this.m8 = c * d;
this.m9 = d * d;
}
#endregion
#region Operators
/// <summary>
/// Adds two matrixes together.
/// </summary>
/// <param name="a">The left hand side.</param>
/// <param name="b">The right hand side.</param>
/// <returns>The resulting matrix.</returns>
public static SymmetricMatrix operator +(SymmetricMatrix a, SymmetricMatrix b)
{
return new SymmetricMatrix(
a.m0 + b.m0, a.m1 + b.m1, a.m2 + b.m2, a.m3 + b.m3,
a.m4 + b.m4, a.m5 + b.m5, a.m6 + b.m6,
a.m7 + b.m7, a.m8 + b.m8,
a.m9 + b.m9
);
}
#endregion
#region Internal Methods
/// <summary>
/// Determinant(0, 1, 2, 1, 4, 5, 2, 5, 7)
/// </summary>
/// <returns></returns>
internal double Determinant1()
{
double det =
m0 * m4 * m7 +
m2 * m1 * m5 +
m1 * m5 * m2 -
m2 * m4 * m2 -
m0 * m5 * m5 -
m1 * m1 * m7;
return det;
}
/// <summary>
/// Determinant(1, 2, 3, 4, 5, 6, 5, 7, 8)
/// </summary>
/// <returns></returns>
internal double Determinant2()
{
double det =
m1 * m5 * m8 +
m3 * m4 * m7 +
m2 * m6 * m5 -
m3 * m5 * m5 -
m1 * m6 * m7 -
m2 * m4 * m8;
return det;
}
/// <summary>
/// Determinant(0, 2, 3, 1, 5, 6, 2, 7, 8)
/// </summary>
/// <returns></returns>
internal double Determinant3()
{
double det =
m0 * m5 * m8 +
m3 * m1 * m7 +
m2 * m6 * m2 -
m3 * m5 * m2 -
m0 * m6 * m7 -
m2 * m1 * m8;
return det;
}
/// <summary>
/// Determinant(0, 1, 3, 1, 4, 6, 2, 5, 8)
/// </summary>
/// <returns></returns>
internal double Determinant4()
{
double det =
m0 * m4 * m8 +
m3 * m1 * m5 +
m1 * m6 * m2 -
m3 * m4 * m2 -
m0 * m6 * m5 -
m1 * m1 * m8;
return det;
}
#endregion
#region Public Methods
/// <summary>
/// Computes the determinant of this matrix.
/// </summary>
/// <param name="a11">The a11 index.</param>
/// <param name="a12">The a12 index.</param>
/// <param name="a13">The a13 index.</param>
/// <param name="a21">The a21 index.</param>
/// <param name="a22">The a22 index.</param>
/// <param name="a23">The a23 index.</param>
/// <param name="a31">The a31 index.</param>
/// <param name="a32">The a32 index.</param>
/// <param name="a33">The a33 index.</param>
/// <returns>The determinant value.</returns>
public double Determinant(int a11, int a12, int a13,
int a21, int a22, int a23,
int a31, int a32, int a33)
{
double det =
this[a11] * this[a22] * this[a33] +
this[a13] * this[a21] * this[a32] +
this[a12] * this[a23] * this[a31] -
this[a13] * this[a22] * this[a31] -
this[a11] * this[a23] * this[a32] -
this[a12] * this[a21] * this[a33];
return det;
}
#endregion
}
}

View File

@@ -1,425 +0,0 @@
#region License
/*
MIT License
Copyright(c) 2017-2018 Mattias Edlund
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
*/
#endregion
using System;
using System.Globalization;
namespace MeshDecimator.Math
{
/// <summary>
/// A single precision 2D vector.
/// </summary>
public struct Vector2 : IEquatable<Vector2>
{
#region Static Read-Only
/// <summary>
/// The zero vector.
/// </summary>
public static readonly Vector2 zero = new Vector2(0, 0);
#endregion
#region Consts
/// <summary>
/// The vector epsilon.
/// </summary>
public const float Epsilon = 9.99999944E-11f;
#endregion
#region Fields
/// <summary>
/// The x component.
/// </summary>
public float x;
/// <summary>
/// The y component.
/// </summary>
public float y;
#endregion
#region Properties
/// <summary>
/// Gets the magnitude of this vector.
/// </summary>
public float Magnitude
{
get { return (float)System.Math.Sqrt(x * x + y * y); }
}
/// <summary>
/// Gets the squared magnitude of this vector.
/// </summary>
public float MagnitudeSqr
{
get { return (x * x + y * y); }
}
/// <summary>
/// Gets a normalized vector from this vector.
/// </summary>
public Vector2 Normalized
{
get
{
Vector2 result;
Normalize(ref this, out result);
return result;
}
}
/// <summary>
/// Gets or sets a specific component by index in this vector.
/// </summary>
/// <param name="index">The component index.</param>
public float this[int index]
{
get
{
switch (index)
{
case 0:
return x;
case 1:
return y;
default:
throw new IndexOutOfRangeException("Invalid Vector2 index!");
}
}
set
{
switch (index)
{
case 0:
x = value;
break;
case 1:
y = value;
break;
default:
throw new IndexOutOfRangeException("Invalid Vector2 index!");
}
}
}
#endregion
#region Constructor
/// <summary>
/// Creates a new vector with one value for all components.
/// </summary>
/// <param name="value">The value.</param>
public Vector2(float value)
{
this.x = value;
this.y = value;
}
/// <summary>
/// Creates a new vector.
/// </summary>
/// <param name="x">The x value.</param>
/// <param name="y">The y value.</param>
public Vector2(float x, float y)
{
this.x = x;
this.y = y;
}
#endregion
#region Operators
/// <summary>
/// Adds two vectors.
/// </summary>
/// <param name="a">The first vector.</param>
/// <param name="b">The second vector.</param>
/// <returns>The resulting vector.</returns>
public static Vector2 operator +(Vector2 a, Vector2 b)
{
return new Vector2(a.x + b.x, a.y + b.y);
}
/// <summary>
/// Subtracts two vectors.
/// </summary>
/// <param name="a">The first vector.</param>
/// <param name="b">The second vector.</param>
/// <returns>The resulting vector.</returns>
public static Vector2 operator -(Vector2 a, Vector2 b)
{
return new Vector2(a.x - b.x, a.y - b.y);
}
/// <summary>
/// Scales the vector uniformly.
/// </summary>
/// <param name="a">The vector.</param>
/// <param name="d">The scaling value.</param>
/// <returns>The resulting vector.</returns>
public static Vector2 operator *(Vector2 a, float d)
{
return new Vector2(a.x * d, a.y * d);
}
/// <summary>
/// Scales the vector uniformly.
/// </summary>
/// <param name="d">The scaling value.</param>
/// <param name="a">The vector.</param>
/// <returns>The resulting vector.</returns>
public static Vector2 operator *(float d, Vector2 a)
{
return new Vector2(a.x * d, a.y * d);
}
/// <summary>
/// Divides the vector with a float.
/// </summary>
/// <param name="a">The vector.</param>
/// <param name="d">The dividing float value.</param>
/// <returns>The resulting vector.</returns>
public static Vector2 operator /(Vector2 a, float d)
{
return new Vector2(a.x / d, a.y / d);
}
/// <summary>
/// Subtracts the vector from a zero vector.
/// </summary>
/// <param name="a">The vector.</param>
/// <returns>The resulting vector.</returns>
public static Vector2 operator -(Vector2 a)
{
return new Vector2(-a.x, -a.y);
}
/// <summary>
/// Returns if two vectors equals eachother.
/// </summary>
/// <param name="lhs">The left hand side vector.</param>
/// <param name="rhs">The right hand side vector.</param>
/// <returns>If equals.</returns>
public static bool operator ==(Vector2 lhs, Vector2 rhs)
{
return (lhs - rhs).MagnitudeSqr < Epsilon;
}
/// <summary>
/// Returns if two vectors don't equal eachother.
/// </summary>
/// <param name="lhs">The left hand side vector.</param>
/// <param name="rhs">The right hand side vector.</param>
/// <returns>If not equals.</returns>
public static bool operator !=(Vector2 lhs, Vector2 rhs)
{
return (lhs - rhs).MagnitudeSqr >= Epsilon;
}
/// <summary>
/// Explicitly converts from a double-precision vector into a single-precision vector.
/// </summary>
/// <param name="v">The double-precision vector.</param>
public static explicit operator Vector2(Vector2d v)
{
return new Vector2((float)v.x, (float)v.y);
}
/// <summary>
/// Implicitly converts from an integer vector into a single-precision vector.
/// </summary>
/// <param name="v">The integer vector.</param>
public static implicit operator Vector2(Vector2i v)
{
return new Vector2(v.x, v.y);
}
#endregion
#region Public Methods
#region Instance
/// <summary>
/// Set x and y components of an existing vector.
/// </summary>
/// <param name="x">The x value.</param>
/// <param name="y">The y value.</param>
public void Set(float x, float y)
{
this.x = x;
this.y = y;
}
/// <summary>
/// Multiplies with another vector component-wise.
/// </summary>
/// <param name="scale">The vector to multiply with.</param>
public void Scale(ref Vector2 scale)
{
x *= scale.x;
y *= scale.y;
}
/// <summary>
/// Normalizes this vector.
/// </summary>
public void Normalize()
{
float mag = this.Magnitude;
if (mag > Epsilon)
{
x /= mag;
y /= mag;
}
else
{
x = y = 0;
}
}
/// <summary>
/// Clamps this vector between a specific range.
/// </summary>
/// <param name="min">The minimum component value.</param>
/// <param name="max">The maximum component value.</param>
public void Clamp(float min, float max)
{
if (x < min) x = min;
else if (x > max) x = max;
if (y < min) y = min;
else if (y > max) y = max;
}
#endregion
#region Object
/// <summary>
/// Returns a hash code for this vector.
/// </summary>
/// <returns>The hash code.</returns>
public override int GetHashCode()
{
return x.GetHashCode() ^ y.GetHashCode() << 2;
}
/// <summary>
/// Returns if this vector is equal to another one.
/// </summary>
/// <param name="other">The other vector to compare to.</param>
/// <returns>If equals.</returns>
public override bool Equals(object other)
{
if (!(other is Vector2))
{
return false;
}
Vector2 vector = (Vector2)other;
return (x == vector.x && y == vector.y);
}
/// <summary>
/// Returns if this vector is equal to another one.
/// </summary>
/// <param name="other">The other vector to compare to.</param>
/// <returns>If equals.</returns>
public bool Equals(Vector2 other)
{
return (x == other.x && y == other.y);
}
/// <summary>
/// Returns a nicely formatted string for this vector.
/// </summary>
/// <returns>The string.</returns>
public override string ToString()
{
return string.Format("({0}, {1})",
x.ToString("F1", CultureInfo.InvariantCulture),
y.ToString("F1", CultureInfo.InvariantCulture));
}
/// <summary>
/// Returns a nicely formatted string for this vector.
/// </summary>
/// <param name="format">The float format.</param>
/// <returns>The string.</returns>
public string ToString(string format)
{
return string.Format("({0}, {1})",
x.ToString(format, CultureInfo.InvariantCulture),
y.ToString(format, CultureInfo.InvariantCulture));
}
#endregion
#region Static
/// <summary>
/// Dot Product of two vectors.
/// </summary>
/// <param name="lhs">The left hand side vector.</param>
/// <param name="rhs">The right hand side vector.</param>
public static float Dot(ref Vector2 lhs, ref Vector2 rhs)
{
return lhs.x * rhs.x + lhs.y * rhs.y;
}
/// <summary>
/// Performs a linear interpolation between two vectors.
/// </summary>
/// <param name="a">The vector to interpolate from.</param>
/// <param name="b">The vector to interpolate to.</param>
/// <param name="t">The time fraction.</param>
/// <param name="result">The resulting vector.</param>
public static void Lerp(ref Vector2 a, ref Vector2 b, float t, out Vector2 result)
{
result = new Vector2(a.x + (b.x - a.x) * t, a.y + (b.y - a.y) * t);
}
/// <summary>
/// Multiplies two vectors component-wise.
/// </summary>
/// <param name="a">The first vector.</param>
/// <param name="b">The second vector.</param>
/// <param name="result">The resulting vector.</param>
public static void Scale(ref Vector2 a, ref Vector2 b, out Vector2 result)
{
result = new Vector2(a.x * b.x, a.y * b.y);
}
/// <summary>
/// Normalizes a vector.
/// </summary>
/// <param name="value">The vector to normalize.</param>
/// <param name="result">The resulting normalized vector.</param>
public static void Normalize(ref Vector2 value, out Vector2 result)
{
float mag = value.Magnitude;
if (mag > Epsilon)
{
result = new Vector2(value.x / mag, value.y / mag);
}
else
{
result = Vector2.zero;
}
}
#endregion
#endregion
}
}

View File

@@ -1,425 +0,0 @@
#region License
/*
MIT License
Copyright(c) 2017-2018 Mattias Edlund
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
*/
#endregion
using System;
using System.Globalization;
namespace MeshDecimator.Math
{
/// <summary>
/// A double precision 2D vector.
/// </summary>
public struct Vector2d : IEquatable<Vector2d>
{
#region Static Read-Only
/// <summary>
/// The zero vector.
/// </summary>
public static readonly Vector2d zero = new Vector2d(0, 0);
#endregion
#region Consts
/// <summary>
/// The vector epsilon.
/// </summary>
public const double Epsilon = double.Epsilon;
#endregion
#region Fields
/// <summary>
/// The x component.
/// </summary>
public double x;
/// <summary>
/// The y component.
/// </summary>
public double y;
#endregion
#region Properties
/// <summary>
/// Gets the magnitude of this vector.
/// </summary>
public double Magnitude
{
get { return System.Math.Sqrt(x * x + y * y); }
}
/// <summary>
/// Gets the squared magnitude of this vector.
/// </summary>
public double MagnitudeSqr
{
get { return (x * x + y * y); }
}
/// <summary>
/// Gets a normalized vector from this vector.
/// </summary>
public Vector2d Normalized
{
get
{
Vector2d result;
Normalize(ref this, out result);
return result;
}
}
/// <summary>
/// Gets or sets a specific component by index in this vector.
/// </summary>
/// <param name="index">The component index.</param>
public double this[int index]
{
get
{
switch (index)
{
case 0:
return x;
case 1:
return y;
default:
throw new IndexOutOfRangeException("Invalid Vector2d index!");
}
}
set
{
switch (index)
{
case 0:
x = value;
break;
case 1:
y = value;
break;
default:
throw new IndexOutOfRangeException("Invalid Vector2d index!");
}
}
}
#endregion
#region Constructor
/// <summary>
/// Creates a new vector with one value for all components.
/// </summary>
/// <param name="value">The value.</param>
public Vector2d(double value)
{
this.x = value;
this.y = value;
}
/// <summary>
/// Creates a new vector.
/// </summary>
/// <param name="x">The x value.</param>
/// <param name="y">The y value.</param>
public Vector2d(double x, double y)
{
this.x = x;
this.y = y;
}
#endregion
#region Operators
/// <summary>
/// Adds two vectors.
/// </summary>
/// <param name="a">The first vector.</param>
/// <param name="b">The second vector.</param>
/// <returns>The resulting vector.</returns>
public static Vector2d operator +(Vector2d a, Vector2d b)
{
return new Vector2d(a.x + b.x, a.y + b.y);
}
/// <summary>
/// Subtracts two vectors.
/// </summary>
/// <param name="a">The first vector.</param>
/// <param name="b">The second vector.</param>
/// <returns>The resulting vector.</returns>
public static Vector2d operator -(Vector2d a, Vector2d b)
{
return new Vector2d(a.x - b.x, a.y - b.y);
}
/// <summary>
/// Scales the vector uniformly.
/// </summary>
/// <param name="a">The vector.</param>
/// <param name="d">The scaling value.</param>
/// <returns>The resulting vector.</returns>
public static Vector2d operator *(Vector2d a, double d)
{
return new Vector2d(a.x * d, a.y * d);
}
/// <summary>
/// Scales the vector uniformly.
/// </summary>
/// <param name="d">The scaling value.</param>
/// <param name="a">The vector.</param>
/// <returns>The resulting vector.</returns>
public static Vector2d operator *(double d, Vector2d a)
{
return new Vector2d(a.x * d, a.y * d);
}
/// <summary>
/// Divides the vector with a float.
/// </summary>
/// <param name="a">The vector.</param>
/// <param name="d">The dividing float value.</param>
/// <returns>The resulting vector.</returns>
public static Vector2d operator /(Vector2d a, double d)
{
return new Vector2d(a.x / d, a.y / d);
}
/// <summary>
/// Subtracts the vector from a zero vector.
/// </summary>
/// <param name="a">The vector.</param>
/// <returns>The resulting vector.</returns>
public static Vector2d operator -(Vector2d a)
{
return new Vector2d(-a.x, -a.y);
}
/// <summary>
/// Returns if two vectors equals eachother.
/// </summary>
/// <param name="lhs">The left hand side vector.</param>
/// <param name="rhs">The right hand side vector.</param>
/// <returns>If equals.</returns>
public static bool operator ==(Vector2d lhs, Vector2d rhs)
{
return (lhs - rhs).MagnitudeSqr < Epsilon;
}
/// <summary>
/// Returns if two vectors don't equal eachother.
/// </summary>
/// <param name="lhs">The left hand side vector.</param>
/// <param name="rhs">The right hand side vector.</param>
/// <returns>If not equals.</returns>
public static bool operator !=(Vector2d lhs, Vector2d rhs)
{
return (lhs - rhs).MagnitudeSqr >= Epsilon;
}
/// <summary>
/// Implicitly converts from a single-precision vector into a double-precision vector.
/// </summary>
/// <param name="v">The single-precision vector.</param>
public static implicit operator Vector2d(Vector2 v)
{
return new Vector2d(v.x, v.y);
}
/// <summary>
/// Implicitly converts from an integer vector into a double-precision vector.
/// </summary>
/// <param name="v">The integer vector.</param>
public static implicit operator Vector2d(Vector2i v)
{
return new Vector2d(v.x, v.y);
}
#endregion
#region Public Methods
#region Instance
/// <summary>
/// Set x and y components of an existing vector.
/// </summary>
/// <param name="x">The x value.</param>
/// <param name="y">The y value.</param>
public void Set(double x, double y)
{
this.x = x;
this.y = y;
}
/// <summary>
/// Multiplies with another vector component-wise.
/// </summary>
/// <param name="scale">The vector to multiply with.</param>
public void Scale(ref Vector2d scale)
{
x *= scale.x;
y *= scale.y;
}
/// <summary>
/// Normalizes this vector.
/// </summary>
public void Normalize()
{
double mag = this.Magnitude;
if (mag > Epsilon)
{
x /= mag;
y /= mag;
}
else
{
x = y = 0;
}
}
/// <summary>
/// Clamps this vector between a specific range.
/// </summary>
/// <param name="min">The minimum component value.</param>
/// <param name="max">The maximum component value.</param>
public void Clamp(double min, double max)
{
if (x < min) x = min;
else if (x > max) x = max;
if (y < min) y = min;
else if (y > max) y = max;
}
#endregion
#region Object
/// <summary>
/// Returns a hash code for this vector.
/// </summary>
/// <returns>The hash code.</returns>
public override int GetHashCode()
{
return x.GetHashCode() ^ y.GetHashCode() << 2;
}
/// <summary>
/// Returns if this vector is equal to another one.
/// </summary>
/// <param name="other">The other vector to compare to.</param>
/// <returns>If equals.</returns>
public override bool Equals(object other)
{
if (!(other is Vector2d))
{
return false;
}
Vector2d vector = (Vector2d)other;
return (x == vector.x && y == vector.y);
}
/// <summary>
/// Returns if this vector is equal to another one.
/// </summary>
/// <param name="other">The other vector to compare to.</param>
/// <returns>If equals.</returns>
public bool Equals(Vector2d other)
{
return (x == other.x && y == other.y);
}
/// <summary>
/// Returns a nicely formatted string for this vector.
/// </summary>
/// <returns>The string.</returns>
public override string ToString()
{
return string.Format("({0}, {1})",
x.ToString("F1", CultureInfo.InvariantCulture),
y.ToString("F1", CultureInfo.InvariantCulture));
}
/// <summary>
/// Returns a nicely formatted string for this vector.
/// </summary>
/// <param name="format">The float format.</param>
/// <returns>The string.</returns>
public string ToString(string format)
{
return string.Format("({0}, {1})",
x.ToString(format, CultureInfo.InvariantCulture),
y.ToString(format, CultureInfo.InvariantCulture));
}
#endregion
#region Static
/// <summary>
/// Dot Product of two vectors.
/// </summary>
/// <param name="lhs">The left hand side vector.</param>
/// <param name="rhs">The right hand side vector.</param>
public static double Dot(ref Vector2d lhs, ref Vector2d rhs)
{
return lhs.x * rhs.x + lhs.y * rhs.y;
}
/// <summary>
/// Performs a linear interpolation between two vectors.
/// </summary>
/// <param name="a">The vector to interpolate from.</param>
/// <param name="b">The vector to interpolate to.</param>
/// <param name="t">The time fraction.</param>
/// <param name="result">The resulting vector.</param>
public static void Lerp(ref Vector2d a, ref Vector2d b, double t, out Vector2d result)
{
result = new Vector2d(a.x + (b.x - a.x) * t, a.y + (b.y - a.y) * t);
}
/// <summary>
/// Multiplies two vectors component-wise.
/// </summary>
/// <param name="a">The first vector.</param>
/// <param name="b">The second vector.</param>
/// <param name="result">The resulting vector.</param>
public static void Scale(ref Vector2d a, ref Vector2d b, out Vector2d result)
{
result = new Vector2d(a.x * b.x, a.y * b.y);
}
/// <summary>
/// Normalizes a vector.
/// </summary>
/// <param name="value">The vector to normalize.</param>
/// <param name="result">The resulting normalized vector.</param>
public static void Normalize(ref Vector2d value, out Vector2d result)
{
double mag = value.Magnitude;
if (mag > Epsilon)
{
result = new Vector2d(value.x / mag, value.y / mag);
}
else
{
result = Vector2d.zero;
}
}
#endregion
#endregion
}
}

View File

@@ -1,348 +0,0 @@
#region License
/*
MIT License
Copyright(c) 2017-2018 Mattias Edlund
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
*/
#endregion
using System;
using System.Globalization;
namespace MeshDecimator.Math
{
/// <summary>
/// A 2D integer vector.
/// </summary>
public struct Vector2i : IEquatable<Vector2i>
{
#region Static Read-Only
/// <summary>
/// The zero vector.
/// </summary>
public static readonly Vector2i zero = new Vector2i(0, 0);
#endregion
#region Fields
/// <summary>
/// The x component.
/// </summary>
public int x;
/// <summary>
/// The y component.
/// </summary>
public int y;
#endregion
#region Properties
/// <summary>
/// Gets the magnitude of this vector.
/// </summary>
public int Magnitude
{
get { return (int)System.Math.Sqrt(x * x + y * y); }
}
/// <summary>
/// Gets the squared magnitude of this vector.
/// </summary>
public int MagnitudeSqr
{
get { return (x * x + y * y); }
}
/// <summary>
/// Gets or sets a specific component by index in this vector.
/// </summary>
/// <param name="index">The component index.</param>
public int this[int index]
{
get
{
switch (index)
{
case 0:
return x;
case 1:
return y;
default:
throw new IndexOutOfRangeException("Invalid Vector2i index!");
}
}
set
{
switch (index)
{
case 0:
x = value;
break;
case 1:
y = value;
break;
default:
throw new IndexOutOfRangeException("Invalid Vector2i index!");
}
}
}
#endregion
#region Constructor
/// <summary>
/// Creates a new vector with one value for all components.
/// </summary>
/// <param name="value">The value.</param>
public Vector2i(int value)
{
this.x = value;
this.y = value;
}
/// <summary>
/// Creates a new vector.
/// </summary>
/// <param name="x">The x value.</param>
/// <param name="y">The y value.</param>
public Vector2i(int x, int y)
{
this.x = x;
this.y = y;
}
#endregion
#region Operators
/// <summary>
/// Adds two vectors.
/// </summary>
/// <param name="a">The first vector.</param>
/// <param name="b">The second vector.</param>
/// <returns>The resulting vector.</returns>
public static Vector2i operator +(Vector2i a, Vector2i b)
{
return new Vector2i(a.x + b.x, a.y + b.y);
}
/// <summary>
/// Subtracts two vectors.
/// </summary>
/// <param name="a">The first vector.</param>
/// <param name="b">The second vector.</param>
/// <returns>The resulting vector.</returns>
public static Vector2i operator -(Vector2i a, Vector2i b)
{
return new Vector2i(a.x - b.x, a.y - b.y);
}
/// <summary>
/// Scales the vector uniformly.
/// </summary>
/// <param name="a">The vector.</param>
/// <param name="d">The scaling value.</param>
/// <returns>The resulting vector.</returns>
public static Vector2i operator *(Vector2i a, int d)
{
return new Vector2i(a.x * d, a.y * d);
}
/// <summary>
/// Scales the vector uniformly.
/// </summary>
/// <param name="d">The scaling value.</param>
/// <param name="a">The vector.</param>
/// <returns>The resulting vector.</returns>
public static Vector2i operator *(int d, Vector2i a)
{
return new Vector2i(a.x * d, a.y * d);
}
/// <summary>
/// Divides the vector with a float.
/// </summary>
/// <param name="a">The vector.</param>
/// <param name="d">The dividing float value.</param>
/// <returns>The resulting vector.</returns>
public static Vector2i operator /(Vector2i a, int d)
{
return new Vector2i(a.x / d, a.y / d);
}
/// <summary>
/// Subtracts the vector from a zero vector.
/// </summary>
/// <param name="a">The vector.</param>
/// <returns>The resulting vector.</returns>
public static Vector2i operator -(Vector2i a)
{
return new Vector2i(-a.x, -a.y);
}
/// <summary>
/// Returns if two vectors equals eachother.
/// </summary>
/// <param name="lhs">The left hand side vector.</param>
/// <param name="rhs">The right hand side vector.</param>
/// <returns>If equals.</returns>
public static bool operator ==(Vector2i lhs, Vector2i rhs)
{
return (lhs.x == rhs.x && lhs.y == rhs.y);
}
/// <summary>
/// Returns if two vectors don't equal eachother.
/// </summary>
/// <param name="lhs">The left hand side vector.</param>
/// <param name="rhs">The right hand side vector.</param>
/// <returns>If not equals.</returns>
public static bool operator !=(Vector2i lhs, Vector2i rhs)
{
return (lhs.x != rhs.x || lhs.y != rhs.y);
}
/// <summary>
/// Explicitly converts from a single-precision vector into an integer vector.
/// </summary>
/// <param name="v">The single-precision vector.</param>
public static explicit operator Vector2i(Vector2 v)
{
return new Vector2i((int)v.x, (int)v.y);
}
/// <summary>
/// Explicitly converts from a double-precision vector into an integer vector.
/// </summary>
/// <param name="v">The double-precision vector.</param>
public static explicit operator Vector2i(Vector2d v)
{
return new Vector2i((int)v.x, (int)v.y);
}
#endregion
#region Public Methods
#region Instance
/// <summary>
/// Set x and y components of an existing vector.
/// </summary>
/// <param name="x">The x value.</param>
/// <param name="y">The y value.</param>
public void Set(int x, int y)
{
this.x = x;
this.y = y;
}
/// <summary>
/// Multiplies with another vector component-wise.
/// </summary>
/// <param name="scale">The vector to multiply with.</param>
public void Scale(ref Vector2i scale)
{
x *= scale.x;
y *= scale.y;
}
/// <summary>
/// Clamps this vector between a specific range.
/// </summary>
/// <param name="min">The minimum component value.</param>
/// <param name="max">The maximum component value.</param>
public void Clamp(int min, int max)
{
if (x < min) x = min;
else if (x > max) x = max;
if (y < min) y = min;
else if (y > max) y = max;
}
#endregion
#region Object
/// <summary>
/// Returns a hash code for this vector.
/// </summary>
/// <returns>The hash code.</returns>
public override int GetHashCode()
{
return x.GetHashCode() ^ y.GetHashCode() << 2;
}
/// <summary>
/// Returns if this vector is equal to another one.
/// </summary>
/// <param name="other">The other vector to compare to.</param>
/// <returns>If equals.</returns>
public override bool Equals(object other)
{
if (!(other is Vector2i))
{
return false;
}
Vector2i vector = (Vector2i)other;
return (x == vector.x && y == vector.y);
}
/// <summary>
/// Returns if this vector is equal to another one.
/// </summary>
/// <param name="other">The other vector to compare to.</param>
/// <returns>If equals.</returns>
public bool Equals(Vector2i other)
{
return (x == other.x && y == other.y);
}
/// <summary>
/// Returns a nicely formatted string for this vector.
/// </summary>
/// <returns>The string.</returns>
public override string ToString()
{
return string.Format("({0}, {1})",
x.ToString(CultureInfo.InvariantCulture),
y.ToString(CultureInfo.InvariantCulture));
}
/// <summary>
/// Returns a nicely formatted string for this vector.
/// </summary>
/// <param name="format">The integer format.</param>
/// <returns>The string.</returns>
public string ToString(string format)
{
return string.Format("({0}, {1})",
x.ToString(format, CultureInfo.InvariantCulture),
y.ToString(format, CultureInfo.InvariantCulture));
}
#endregion
#region Static
/// <summary>
/// Multiplies two vectors component-wise.
/// </summary>
/// <param name="a">The first vector.</param>
/// <param name="b">The second vector.</param>
/// <param name="result">The resulting vector.</param>
public static void Scale(ref Vector2i a, ref Vector2i b, out Vector2i result)
{
result = new Vector2i(a.x * b.x, a.y * b.y);
}
#endregion
#endregion
}
}

View File

@@ -1,494 +0,0 @@
#region License
/*
MIT License
Copyright(c) 2017-2018 Mattias Edlund
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
*/
#endregion
using System;
using System.Globalization;
namespace MeshDecimator.Math
{
/// <summary>
/// A single precision 3D vector.
/// </summary>
public struct Vector3 : IEquatable<Vector3>
{
#region Static Read-Only
/// <summary>
/// The zero vector.
/// </summary>
public static readonly Vector3 zero = new Vector3(0, 0, 0);
#endregion
#region Consts
/// <summary>
/// The vector epsilon.
/// </summary>
public const float Epsilon = 9.99999944E-11f;
#endregion
#region Fields
/// <summary>
/// The x component.
/// </summary>
public float x;
/// <summary>
/// The y component.
/// </summary>
public float y;
/// <summary>
/// The z component.
/// </summary>
public float z;
#endregion
#region Properties
/// <summary>
/// Gets the magnitude of this vector.
/// </summary>
public float Magnitude
{
get { return (float)System.Math.Sqrt(x * x + y * y + z * z); }
}
/// <summary>
/// Gets the squared magnitude of this vector.
/// </summary>
public float MagnitudeSqr
{
get { return (x * x + y * y + z * z); }
}
/// <summary>
/// Gets a normalized vector from this vector.
/// </summary>
public Vector3 Normalized
{
get
{
Vector3 result;
Normalize(ref this, out result);
return result;
}
}
/// <summary>
/// Gets or sets a specific component by index in this vector.
/// </summary>
/// <param name="index">The component index.</param>
public float this[int index]
{
get
{
switch (index)
{
case 0:
return x;
case 1:
return y;
case 2:
return z;
default:
throw new IndexOutOfRangeException("Invalid Vector3 index!");
}
}
set
{
switch (index)
{
case 0:
x = value;
break;
case 1:
y = value;
break;
case 2:
z = value;
break;
default:
throw new IndexOutOfRangeException("Invalid Vector3 index!");
}
}
}
#endregion
#region Constructor
/// <summary>
/// Creates a new vector with one value for all components.
/// </summary>
/// <param name="value">The value.</param>
public Vector3(float value)
{
this.x = value;
this.y = value;
this.z = value;
}
/// <summary>
/// Creates a new vector.
/// </summary>
/// <param name="x">The x value.</param>
/// <param name="y">The y value.</param>
/// <param name="z">The z value.</param>
public Vector3(float x, float y, float z)
{
this.x = x;
this.y = y;
this.z = z;
}
/// <summary>
/// Creates a new vector from a double precision vector.
/// </summary>
/// <param name="vector">The double precision vector.</param>
public Vector3(Vector3d vector)
{
this.x = (float)vector.x;
this.y = (float)vector.y;
this.z = (float)vector.z;
}
#endregion
#region Operators
/// <summary>
/// Adds two vectors.
/// </summary>
/// <param name="a">The first vector.</param>
/// <param name="b">The second vector.</param>
/// <returns>The resulting vector.</returns>
public static Vector3 operator +(Vector3 a, Vector3 b)
{
return new Vector3(a.x + b.x, a.y + b.y, a.z + b.z);
}
/// <summary>
/// Subtracts two vectors.
/// </summary>
/// <param name="a">The first vector.</param>
/// <param name="b">The second vector.</param>
/// <returns>The resulting vector.</returns>
public static Vector3 operator -(Vector3 a, Vector3 b)
{
return new Vector3(a.x - b.x, a.y - b.y, a.z - b.z);
}
/// <summary>
/// Scales the vector uniformly.
/// </summary>
/// <param name="a">The vector.</param>
/// <param name="d">The scaling value.</param>
/// <returns>The resulting vector.</returns>
public static Vector3 operator *(Vector3 a, float d)
{
return new Vector3(a.x * d, a.y * d, a.z * d);
}
/// <summary>
/// Scales the vector uniformly.
/// </summary>
/// <param name="d">The scaling value.</param>
/// <param name="a">The vector.</param>
/// <returns>The resulting vector.</returns>
public static Vector3 operator *(float d, Vector3 a)
{
return new Vector3(a.x * d, a.y * d, a.z * d);
}
/// <summary>
/// Divides the vector with a float.
/// </summary>
/// <param name="a">The vector.</param>
/// <param name="d">The dividing float value.</param>
/// <returns>The resulting vector.</returns>
public static Vector3 operator /(Vector3 a, float d)
{
return new Vector3(a.x / d, a.y / d, a.z / d);
}
/// <summary>
/// Subtracts the vector from a zero vector.
/// </summary>
/// <param name="a">The vector.</param>
/// <returns>The resulting vector.</returns>
public static Vector3 operator -(Vector3 a)
{
return new Vector3(-a.x, -a.y, -a.z);
}
/// <summary>
/// Returns if two vectors equals eachother.
/// </summary>
/// <param name="lhs">The left hand side vector.</param>
/// <param name="rhs">The right hand side vector.</param>
/// <returns>If equals.</returns>
public static bool operator ==(Vector3 lhs, Vector3 rhs)
{
return (lhs - rhs).MagnitudeSqr < Epsilon;
}
/// <summary>
/// Returns if two vectors don't equal eachother.
/// </summary>
/// <param name="lhs">The left hand side vector.</param>
/// <param name="rhs">The right hand side vector.</param>
/// <returns>If not equals.</returns>
public static bool operator !=(Vector3 lhs, Vector3 rhs)
{
return (lhs - rhs).MagnitudeSqr >= Epsilon;
}
/// <summary>
/// Explicitly converts from a double-precision vector into a single-precision vector.
/// </summary>
/// <param name="v">The double-precision vector.</param>
public static explicit operator Vector3(Vector3d v)
{
return new Vector3((float)v.x, (float)v.y, (float)v.z);
}
/// <summary>
/// Implicitly converts from an integer vector into a single-precision vector.
/// </summary>
/// <param name="v">The integer vector.</param>
public static implicit operator Vector3(Vector3i v)
{
return new Vector3(v.x, v.y, v.z);
}
#endregion
#region Public Methods
#region Instance
/// <summary>
/// Set x, y and z components of an existing vector.
/// </summary>
/// <param name="x">The x value.</param>
/// <param name="y">The y value.</param>
/// <param name="z">The z value.</param>
public void Set(float x, float y, float z)
{
this.x = x;
this.y = y;
this.z = z;
}
/// <summary>
/// Multiplies with another vector component-wise.
/// </summary>
/// <param name="scale">The vector to multiply with.</param>
public void Scale(ref Vector3 scale)
{
x *= scale.x;
y *= scale.y;
z *= scale.z;
}
/// <summary>
/// Normalizes this vector.
/// </summary>
public void Normalize()
{
float mag = this.Magnitude;
if (mag > Epsilon)
{
x /= mag;
y /= mag;
z /= mag;
}
else
{
x = y = z = 0;
}
}
/// <summary>
/// Clamps this vector between a specific range.
/// </summary>
/// <param name="min">The minimum component value.</param>
/// <param name="max">The maximum component value.</param>
public void Clamp(float min, float max)
{
if (x < min) x = min;
else if (x > max) x = max;
if (y < min) y = min;
else if (y > max) y = max;
if (z < min) z = min;
else if (z > max) z = max;
}
#endregion
#region Object
/// <summary>
/// Returns a hash code for this vector.
/// </summary>
/// <returns>The hash code.</returns>
public override int GetHashCode()
{
return x.GetHashCode() ^ y.GetHashCode() << 2 ^ z.GetHashCode() >> 2;
}
/// <summary>
/// Returns if this vector is equal to another one.
/// </summary>
/// <param name="other">The other vector to compare to.</param>
/// <returns>If equals.</returns>
public override bool Equals(object other)
{
if (!(other is Vector3))
{
return false;
}
Vector3 vector = (Vector3)other;
return (x == vector.x && y == vector.y && z == vector.z);
}
/// <summary>
/// Returns if this vector is equal to another one.
/// </summary>
/// <param name="other">The other vector to compare to.</param>
/// <returns>If equals.</returns>
public bool Equals(Vector3 other)
{
return (x == other.x && y == other.y && z == other.z);
}
/// <summary>
/// Returns a nicely formatted string for this vector.
/// </summary>
/// <returns>The string.</returns>
public override string ToString()
{
return string.Format("({0}, {1}, {2})",
x.ToString("F1", CultureInfo.InvariantCulture),
y.ToString("F1", CultureInfo.InvariantCulture),
z.ToString("F1", CultureInfo.InvariantCulture));
}
/// <summary>
/// Returns a nicely formatted string for this vector.
/// </summary>
/// <param name="format">The float format.</param>
/// <returns>The string.</returns>
public string ToString(string format)
{
return string.Format("({0}, {1}, {2})",
x.ToString(format, CultureInfo.InvariantCulture),
y.ToString(format, CultureInfo.InvariantCulture),
z.ToString(format, CultureInfo.InvariantCulture));
}
#endregion
#region Static
/// <summary>
/// Dot Product of two vectors.
/// </summary>
/// <param name="lhs">The left hand side vector.</param>
/// <param name="rhs">The right hand side vector.</param>
public static float Dot(ref Vector3 lhs, ref Vector3 rhs)
{
return lhs.x * rhs.x + lhs.y * rhs.y + lhs.z * rhs.z;
}
/// <summary>
/// Cross Product of two vectors.
/// </summary>
/// <param name="lhs">The left hand side vector.</param>
/// <param name="rhs">The right hand side vector.</param>
/// <param name="result">The resulting vector.</param>
public static void Cross(ref Vector3 lhs, ref Vector3 rhs, out Vector3 result)
{
result = new Vector3(lhs.y * rhs.z - lhs.z * rhs.y, lhs.z * rhs.x - lhs.x * rhs.z, lhs.x * rhs.y - lhs.y * rhs.x);
}
/// <summary>
/// Calculates the angle between two vectors.
/// </summary>
/// <param name="from">The from vector.</param>
/// <param name="to">The to vector.</param>
/// <returns>The angle.</returns>
public static float Angle(ref Vector3 from, ref Vector3 to)
{
Vector3 fromNormalized = from.Normalized;
Vector3 toNormalized = to.Normalized;
return (float)System.Math.Acos(MathHelper.Clamp(Vector3.Dot(ref fromNormalized, ref toNormalized), -1f, 1f)) * MathHelper.Rad2Deg;
}
/// <summary>
/// Performs a linear interpolation between two vectors.
/// </summary>
/// <param name="a">The vector to interpolate from.</param>
/// <param name="b">The vector to interpolate to.</param>
/// <param name="t">The time fraction.</param>
/// <param name="result">The resulting vector.</param>
public static void Lerp(ref Vector3 a, ref Vector3 b, float t, out Vector3 result)
{
result = new Vector3(a.x + (b.x - a.x) * t, a.y + (b.y - a.y) * t, a.z + (b.z - a.z) * t);
}
/// <summary>
/// Multiplies two vectors component-wise.
/// </summary>
/// <param name="a">The first vector.</param>
/// <param name="b">The second vector.</param>
/// <param name="result">The resulting vector.</param>
public static void Scale(ref Vector3 a, ref Vector3 b, out Vector3 result)
{
result = new Vector3(a.x * b.x, a.y * b.y, a.z * b.z);
}
/// <summary>
/// Normalizes a vector.
/// </summary>
/// <param name="value">The vector to normalize.</param>
/// <param name="result">The resulting normalized vector.</param>
public static void Normalize(ref Vector3 value, out Vector3 result)
{
float mag = value.Magnitude;
if (mag > Epsilon)
{
result = new Vector3(value.x / mag, value.y / mag, value.z / mag);
}
else
{
result = Vector3.zero;
}
}
/// <summary>
/// Normalizes both vectors and makes them orthogonal to each other.
/// </summary>
/// <param name="normal">The normal vector.</param>
/// <param name="tangent">The tangent.</param>
public static void OrthoNormalize(ref Vector3 normal, ref Vector3 tangent)
{
normal.Normalize();
Vector3 proj = normal * Vector3.Dot(ref tangent, ref normal);
tangent -= proj;
tangent.Normalize();
}
#endregion
#endregion
}
}

View File

@@ -1,481 +0,0 @@
#region License
/*
MIT License
Copyright(c) 2017-2018 Mattias Edlund
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
*/
#endregion
using System;
using System.Globalization;
namespace MeshDecimator.Math
{
/// <summary>
/// A double precision 3D vector.
/// </summary>
public struct Vector3d : IEquatable<Vector3d>
{
#region Static Read-Only
/// <summary>
/// The zero vector.
/// </summary>
public static readonly Vector3d zero = new Vector3d(0, 0, 0);
#endregion
#region Consts
/// <summary>
/// The vector epsilon.
/// </summary>
public const double Epsilon = double.Epsilon;
#endregion
#region Fields
/// <summary>
/// The x component.
/// </summary>
public double x;
/// <summary>
/// The y component.
/// </summary>
public double y;
/// <summary>
/// The z component.
/// </summary>
public double z;
#endregion
#region Properties
/// <summary>
/// Gets the magnitude of this vector.
/// </summary>
public double Magnitude
{
get { return System.Math.Sqrt(x * x + y * y + z * z); }
}
/// <summary>
/// Gets the squared magnitude of this vector.
/// </summary>
public double MagnitudeSqr
{
get { return (x * x + y * y + z * z); }
}
/// <summary>
/// Gets a normalized vector from this vector.
/// </summary>
public Vector3d Normalized
{
get
{
Vector3d result;
Normalize(ref this, out result);
return result;
}
}
/// <summary>
/// Gets or sets a specific component by index in this vector.
/// </summary>
/// <param name="index">The component index.</param>
public double this[int index]
{
get
{
switch (index)
{
case 0:
return x;
case 1:
return y;
case 2:
return z;
default:
throw new IndexOutOfRangeException("Invalid Vector3d index!");
}
}
set
{
switch (index)
{
case 0:
x = value;
break;
case 1:
y = value;
break;
case 2:
z = value;
break;
default:
throw new IndexOutOfRangeException("Invalid Vector3d index!");
}
}
}
#endregion
#region Constructor
/// <summary>
/// Creates a new vector with one value for all components.
/// </summary>
/// <param name="value">The value.</param>
public Vector3d(double value)
{
this.x = value;
this.y = value;
this.z = value;
}
/// <summary>
/// Creates a new vector.
/// </summary>
/// <param name="x">The x value.</param>
/// <param name="y">The y value.</param>
/// <param name="z">The z value.</param>
public Vector3d(double x, double y, double z)
{
this.x = x;
this.y = y;
this.z = z;
}
/// <summary>
/// Creates a new vector from a single precision vector.
/// </summary>
/// <param name="vector">The single precision vector.</param>
public Vector3d(Vector3 vector)
{
this.x = vector.x;
this.y = vector.y;
this.z = vector.z;
}
#endregion
#region Operators
/// <summary>
/// Adds two vectors.
/// </summary>
/// <param name="a">The first vector.</param>
/// <param name="b">The second vector.</param>
/// <returns>The resulting vector.</returns>
public static Vector3d operator +(Vector3d a, Vector3d b)
{
return new Vector3d(a.x + b.x, a.y + b.y, a.z + b.z);
}
/// <summary>
/// Subtracts two vectors.
/// </summary>
/// <param name="a">The first vector.</param>
/// <param name="b">The second vector.</param>
/// <returns>The resulting vector.</returns>
public static Vector3d operator -(Vector3d a, Vector3d b)
{
return new Vector3d(a.x - b.x, a.y - b.y, a.z - b.z);
}
/// <summary>
/// Scales the vector uniformly.
/// </summary>
/// <param name="a">The vector.</param>
/// <param name="d">The scaling value.</param>
/// <returns>The resulting vector.</returns>
public static Vector3d operator *(Vector3d a, double d)
{
return new Vector3d(a.x * d, a.y * d, a.z * d);
}
/// <summary>
/// Scales the vector uniformly.
/// </summary>
/// <param name="d">The scaling value.</param>
/// <param name="a">The vector.</param>
/// <returns>The resulting vector.</returns>
public static Vector3d operator *(double d, Vector3d a)
{
return new Vector3d(a.x * d, a.y * d, a.z * d);
}
/// <summary>
/// Divides the vector with a float.
/// </summary>
/// <param name="a">The vector.</param>
/// <param name="d">The dividing float value.</param>
/// <returns>The resulting vector.</returns>
public static Vector3d operator /(Vector3d a, double d)
{
return new Vector3d(a.x / d, a.y / d, a.z / d);
}
/// <summary>
/// Subtracts the vector from a zero vector.
/// </summary>
/// <param name="a">The vector.</param>
/// <returns>The resulting vector.</returns>
public static Vector3d operator -(Vector3d a)
{
return new Vector3d(-a.x, -a.y, -a.z);
}
/// <summary>
/// Returns if two vectors equals eachother.
/// </summary>
/// <param name="lhs">The left hand side vector.</param>
/// <param name="rhs">The right hand side vector.</param>
/// <returns>If equals.</returns>
public static bool operator ==(Vector3d lhs, Vector3d rhs)
{
return (lhs - rhs).MagnitudeSqr < Epsilon;
}
/// <summary>
/// Returns if two vectors don't equal eachother.
/// </summary>
/// <param name="lhs">The left hand side vector.</param>
/// <param name="rhs">The right hand side vector.</param>
/// <returns>If not equals.</returns>
public static bool operator !=(Vector3d lhs, Vector3d rhs)
{
return (lhs - rhs).MagnitudeSqr >= Epsilon;
}
/// <summary>
/// Implicitly converts from a single-precision vector into a double-precision vector.
/// </summary>
/// <param name="v">The single-precision vector.</param>
public static implicit operator Vector3d(Vector3 v)
{
return new Vector3d(v.x, v.y, v.z);
}
/// <summary>
/// Implicitly converts from an integer vector into a double-precision vector.
/// </summary>
/// <param name="v">The integer vector.</param>
public static implicit operator Vector3d(Vector3i v)
{
return new Vector3d(v.x, v.y, v.z);
}
#endregion
#region Public Methods
#region Instance
/// <summary>
/// Set x, y and z components of an existing vector.
/// </summary>
/// <param name="x">The x value.</param>
/// <param name="y">The y value.</param>
/// <param name="z">The z value.</param>
public void Set(double x, double y, double z)
{
this.x = x;
this.y = y;
this.z = z;
}
/// <summary>
/// Multiplies with another vector component-wise.
/// </summary>
/// <param name="scale">The vector to multiply with.</param>
public void Scale(ref Vector3d scale)
{
x *= scale.x;
y *= scale.y;
z *= scale.z;
}
/// <summary>
/// Normalizes this vector.
/// </summary>
public void Normalize()
{
double mag = this.Magnitude;
if (mag > Epsilon)
{
x /= mag;
y /= mag;
z /= mag;
}
else
{
x = y = z = 0;
}
}
/// <summary>
/// Clamps this vector between a specific range.
/// </summary>
/// <param name="min">The minimum component value.</param>
/// <param name="max">The maximum component value.</param>
public void Clamp(double min, double max)
{
if (x < min) x = min;
else if (x > max) x = max;
if (y < min) y = min;
else if (y > max) y = max;
if (z < min) z = min;
else if (z > max) z = max;
}
#endregion
#region Object
/// <summary>
/// Returns a hash code for this vector.
/// </summary>
/// <returns>The hash code.</returns>
public override int GetHashCode()
{
return x.GetHashCode() ^ y.GetHashCode() << 2 ^ z.GetHashCode() >> 2;
}
/// <summary>
/// Returns if this vector is equal to another one.
/// </summary>
/// <param name="other">The other vector to compare to.</param>
/// <returns>If equals.</returns>
public override bool Equals(object other)
{
if (!(other is Vector3d))
{
return false;
}
Vector3d vector = (Vector3d)other;
return (x == vector.x && y == vector.y && z == vector.z);
}
/// <summary>
/// Returns if this vector is equal to another one.
/// </summary>
/// <param name="other">The other vector to compare to.</param>
/// <returns>If equals.</returns>
public bool Equals(Vector3d other)
{
return (x == other.x && y == other.y && z == other.z);
}
/// <summary>
/// Returns a nicely formatted string for this vector.
/// </summary>
/// <returns>The string.</returns>
public override string ToString()
{
return string.Format("({0}, {1}, {2})",
x.ToString("F1", CultureInfo.InvariantCulture),
y.ToString("F1", CultureInfo.InvariantCulture),
z.ToString("F1", CultureInfo.InvariantCulture));
}
/// <summary>
/// Returns a nicely formatted string for this vector.
/// </summary>
/// <param name="format">The float format.</param>
/// <returns>The string.</returns>
public string ToString(string format)
{
return string.Format("({0}, {1}, {2})",
x.ToString(format, CultureInfo.InvariantCulture),
y.ToString(format, CultureInfo.InvariantCulture),
z.ToString(format, CultureInfo.InvariantCulture));
}
#endregion
#region Static
/// <summary>
/// Dot Product of two vectors.
/// </summary>
/// <param name="lhs">The left hand side vector.</param>
/// <param name="rhs">The right hand side vector.</param>
public static double Dot(ref Vector3d lhs, ref Vector3d rhs)
{
return lhs.x * rhs.x + lhs.y * rhs.y + lhs.z * rhs.z;
}
/// <summary>
/// Cross Product of two vectors.
/// </summary>
/// <param name="lhs">The left hand side vector.</param>
/// <param name="rhs">The right hand side vector.</param>
/// <param name="result">The resulting vector.</param>
public static void Cross(ref Vector3d lhs, ref Vector3d rhs, out Vector3d result)
{
result = new Vector3d(lhs.y * rhs.z - lhs.z * rhs.y, lhs.z * rhs.x - lhs.x * rhs.z, lhs.x * rhs.y - lhs.y * rhs.x);
}
/// <summary>
/// Calculates the angle between two vectors.
/// </summary>
/// <param name="from">The from vector.</param>
/// <param name="to">The to vector.</param>
/// <returns>The angle.</returns>
public static double Angle(ref Vector3d from, ref Vector3d to)
{
Vector3d fromNormalized = from.Normalized;
Vector3d toNormalized = to.Normalized;
return System.Math.Acos(MathHelper.Clamp(Vector3d.Dot(ref fromNormalized, ref toNormalized), -1.0, 1.0)) * MathHelper.Rad2Degd;
}
/// <summary>
/// Performs a linear interpolation between two vectors.
/// </summary>
/// <param name="a">The vector to interpolate from.</param>
/// <param name="b">The vector to interpolate to.</param>
/// <param name="t">The time fraction.</param>
/// <param name="result">The resulting vector.</param>
public static void Lerp(ref Vector3d a, ref Vector3d b, double t, out Vector3d result)
{
result = new Vector3d(a.x + (b.x - a.x) * t, a.y + (b.y - a.y) * t, a.z + (b.z - a.z) * t);
}
/// <summary>
/// Multiplies two vectors component-wise.
/// </summary>
/// <param name="a">The first vector.</param>
/// <param name="b">The second vector.</param>
/// <param name="result">The resulting vector.</param>
public static void Scale(ref Vector3d a, ref Vector3d b, out Vector3d result)
{
result = new Vector3d(a.x * b.x, a.y * b.y, a.z * b.z);
}
/// <summary>
/// Normalizes a vector.
/// </summary>
/// <param name="value">The vector to normalize.</param>
/// <param name="result">The resulting normalized vector.</param>
public static void Normalize(ref Vector3d value, out Vector3d result)
{
double mag = value.Magnitude;
if (mag > Epsilon)
{
result = new Vector3d(value.x / mag, value.y / mag, value.z / mag);
}
else
{
result = Vector3d.zero;
}
}
#endregion
#endregion
}
}

View File

@@ -1,368 +0,0 @@
#region License
/*
MIT License
Copyright(c) 2017-2018 Mattias Edlund
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
*/
#endregion
using System;
using System.Globalization;
namespace MeshDecimator.Math
{
/// <summary>
/// A 3D integer vector.
/// </summary>
public struct Vector3i : IEquatable<Vector3i>
{
#region Static Read-Only
/// <summary>
/// The zero vector.
/// </summary>
public static readonly Vector3i zero = new Vector3i(0, 0, 0);
#endregion
#region Fields
/// <summary>
/// The x component.
/// </summary>
public int x;
/// <summary>
/// The y component.
/// </summary>
public int y;
/// <summary>
/// The z component.
/// </summary>
public int z;
#endregion
#region Properties
/// <summary>
/// Gets the magnitude of this vector.
/// </summary>
public int Magnitude
{
get { return (int)System.Math.Sqrt(x * x + y * y + z * z); }
}
/// <summary>
/// Gets the squared magnitude of this vector.
/// </summary>
public int MagnitudeSqr
{
get { return (x * x + y * y + z * z); }
}
/// <summary>
/// Gets or sets a specific component by index in this vector.
/// </summary>
/// <param name="index">The component index.</param>
public int this[int index]
{
get
{
switch (index)
{
case 0:
return x;
case 1:
return y;
case 2:
return z;
default:
throw new IndexOutOfRangeException("Invalid Vector3i index!");
}
}
set
{
switch (index)
{
case 0:
x = value;
break;
case 1:
y = value;
break;
case 2:
z = value;
break;
default:
throw new IndexOutOfRangeException("Invalid Vector3i index!");
}
}
}
#endregion
#region Constructor
/// <summary>
/// Creates a new vector with one value for all components.
/// </summary>
/// <param name="value">The value.</param>
public Vector3i(int value)
{
this.x = value;
this.y = value;
this.z = value;
}
/// <summary>
/// Creates a new vector.
/// </summary>
/// <param name="x">The x value.</param>
/// <param name="y">The y value.</param>
/// <param name="z">The z value.</param>
public Vector3i(int x, int y, int z)
{
this.x = x;
this.y = y;
this.z = z;
}
#endregion
#region Operators
/// <summary>
/// Adds two vectors.
/// </summary>
/// <param name="a">The first vector.</param>
/// <param name="b">The second vector.</param>
/// <returns>The resulting vector.</returns>
public static Vector3i operator +(Vector3i a, Vector3i b)
{
return new Vector3i(a.x + b.x, a.y + b.y, a.z + b.z);
}
/// <summary>
/// Subtracts two vectors.
/// </summary>
/// <param name="a">The first vector.</param>
/// <param name="b">The second vector.</param>
/// <returns>The resulting vector.</returns>
public static Vector3i operator -(Vector3i a, Vector3i b)
{
return new Vector3i(a.x - b.x, a.y - b.y, a.z - b.z);
}
/// <summary>
/// Scales the vector uniformly.
/// </summary>
/// <param name="a">The vector.</param>
/// <param name="d">The scaling value.</param>
/// <returns>The resulting vector.</returns>
public static Vector3i operator *(Vector3i a, int d)
{
return new Vector3i(a.x * d, a.y * d, a.z * d);
}
/// <summary>
/// Scales the vector uniformly.
/// </summary>
/// <param name="d">The scaling value.</param>
/// <param name="a">The vector.</param>
/// <returns>The resulting vector.</returns>
public static Vector3i operator *(int d, Vector3i a)
{
return new Vector3i(a.x * d, a.y * d, a.z * d);
}
/// <summary>
/// Divides the vector with a float.
/// </summary>
/// <param name="a">The vector.</param>
/// <param name="d">The dividing float value.</param>
/// <returns>The resulting vector.</returns>
public static Vector3i operator /(Vector3i a, int d)
{
return new Vector3i(a.x / d, a.y / d, a.z / d);
}
/// <summary>
/// Subtracts the vector from a zero vector.
/// </summary>
/// <param name="a">The vector.</param>
/// <returns>The resulting vector.</returns>
public static Vector3i operator -(Vector3i a)
{
return new Vector3i(-a.x, -a.y, -a.z);
}
/// <summary>
/// Returns if two vectors equals eachother.
/// </summary>
/// <param name="lhs">The left hand side vector.</param>
/// <param name="rhs">The right hand side vector.</param>
/// <returns>If equals.</returns>
public static bool operator ==(Vector3i lhs, Vector3i rhs)
{
return (lhs.x == rhs.x && lhs.y == rhs.y && lhs.z == rhs.z);
}
/// <summary>
/// Returns if two vectors don't equal eachother.
/// </summary>
/// <param name="lhs">The left hand side vector.</param>
/// <param name="rhs">The right hand side vector.</param>
/// <returns>If not equals.</returns>
public static bool operator !=(Vector3i lhs, Vector3i rhs)
{
return (lhs.x != rhs.x || lhs.y != rhs.y || lhs.z != rhs.z);
}
/// <summary>
/// Explicitly converts from a single-precision vector into an integer vector.
/// </summary>
/// <param name="v">The single-precision vector.</param>
public static implicit operator Vector3i(Vector3 v)
{
return new Vector3i((int)v.x, (int)v.y, (int)v.z);
}
/// <summary>
/// Explicitly converts from a double-precision vector into an integer vector.
/// </summary>
/// <param name="v">The double-precision vector.</param>
public static explicit operator Vector3i(Vector3d v)
{
return new Vector3i((int)v.x, (int)v.y, (int)v.z);
}
#endregion
#region Public Methods
#region Instance
/// <summary>
/// Set x, y and z components of an existing vector.
/// </summary>
/// <param name="x">The x value.</param>
/// <param name="y">The y value.</param>
/// <param name="z">The z value.</param>
public void Set(int x, int y, int z)
{
this.x = x;
this.y = y;
this.z = z;
}
/// <summary>
/// Multiplies with another vector component-wise.
/// </summary>
/// <param name="scale">The vector to multiply with.</param>
public void Scale(ref Vector3i scale)
{
x *= scale.x;
y *= scale.y;
z *= scale.z;
}
/// <summary>
/// Clamps this vector between a specific range.
/// </summary>
/// <param name="min">The minimum component value.</param>
/// <param name="max">The maximum component value.</param>
public void Clamp(int min, int max)
{
if (x < min) x = min;
else if (x > max) x = max;
if (y < min) y = min;
else if (y > max) y = max;
if (z < min) z = min;
else if (z > max) z = max;
}
#endregion
#region Object
/// <summary>
/// Returns a hash code for this vector.
/// </summary>
/// <returns>The hash code.</returns>
public override int GetHashCode()
{
return x.GetHashCode() ^ y.GetHashCode() << 2 ^ z.GetHashCode() >> 2;
}
/// <summary>
/// Returns if this vector is equal to another one.
/// </summary>
/// <param name="other">The other vector to compare to.</param>
/// <returns>If equals.</returns>
public override bool Equals(object other)
{
if (!(other is Vector3i))
{
return false;
}
Vector3i vector = (Vector3i)other;
return (x == vector.x && y == vector.y && z == vector.z);
}
/// <summary>
/// Returns if this vector is equal to another one.
/// </summary>
/// <param name="other">The other vector to compare to.</param>
/// <returns>If equals.</returns>
public bool Equals(Vector3i other)
{
return (x == other.x && y == other.y && z == other.z);
}
/// <summary>
/// Returns a nicely formatted string for this vector.
/// </summary>
/// <returns>The string.</returns>
public override string ToString()
{
return string.Format("({0}, {1}, {2})",
x.ToString(CultureInfo.InvariantCulture),
y.ToString(CultureInfo.InvariantCulture),
z.ToString(CultureInfo.InvariantCulture));
}
/// <summary>
/// Returns a nicely formatted string for this vector.
/// </summary>
/// <param name="format">The integer format.</param>
/// <returns>The string.</returns>
public string ToString(string format)
{
return string.Format("({0}, {1}, {2})",
x.ToString(format, CultureInfo.InvariantCulture),
y.ToString(format, CultureInfo.InvariantCulture),
z.ToString(format, CultureInfo.InvariantCulture));
}
#endregion
#region Static
/// <summary>
/// Multiplies two vectors component-wise.
/// </summary>
/// <param name="a">The first vector.</param>
/// <param name="b">The second vector.</param>
/// <param name="result">The resulting vector.</param>
public static void Scale(ref Vector3i a, ref Vector3i b, out Vector3i result)
{
result = new Vector3i(a.x * b.x, a.y * b.y, a.z * b.z);
}
#endregion
#endregion
}
}

View File

@@ -1,467 +0,0 @@
#region License
/*
MIT License
Copyright(c) 2017-2018 Mattias Edlund
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
*/
#endregion
using System;
using System.Globalization;
namespace MeshDecimator.Math
{
/// <summary>
/// A single precision 4D vector.
/// </summary>
public struct Vector4 : IEquatable<Vector4>
{
#region Static Read-Only
/// <summary>
/// The zero vector.
/// </summary>
public static readonly Vector4 zero = new Vector4(0, 0, 0, 0);
#endregion
#region Consts
/// <summary>
/// The vector epsilon.
/// </summary>
public const float Epsilon = 9.99999944E-11f;
#endregion
#region Fields
/// <summary>
/// The x component.
/// </summary>
public float x;
/// <summary>
/// The y component.
/// </summary>
public float y;
/// <summary>
/// The z component.
/// </summary>
public float z;
/// <summary>
/// The w component.
/// </summary>
public float w;
#endregion
#region Properties
/// <summary>
/// Gets the magnitude of this vector.
/// </summary>
public float Magnitude
{
get { return (float)System.Math.Sqrt(x * x + y * y + z * z + w * w); }
}
/// <summary>
/// Gets the squared magnitude of this vector.
/// </summary>
public float MagnitudeSqr
{
get { return (x * x + y * y + z * z + w * w); }
}
/// <summary>
/// Gets a normalized vector from this vector.
/// </summary>
public Vector4 Normalized
{
get
{
Vector4 result;
Normalize(ref this, out result);
return result;
}
}
/// <summary>
/// Gets or sets a specific component by index in this vector.
/// </summary>
/// <param name="index">The component index.</param>
public float this[int index]
{
get
{
switch (index)
{
case 0:
return x;
case 1:
return y;
case 2:
return z;
case 3:
return w;
default:
throw new IndexOutOfRangeException("Invalid Vector4 index!");
}
}
set
{
switch (index)
{
case 0:
x = value;
break;
case 1:
y = value;
break;
case 2:
z = value;
break;
case 3:
w = value;
break;
default:
throw new IndexOutOfRangeException("Invalid Vector4 index!");
}
}
}
#endregion
#region Constructor
/// <summary>
/// Creates a new vector with one value for all components.
/// </summary>
/// <param name="value">The value.</param>
public Vector4(float value)
{
this.x = value;
this.y = value;
this.z = value;
this.w = value;
}
/// <summary>
/// Creates a new vector.
/// </summary>
/// <param name="x">The x value.</param>
/// <param name="y">The y value.</param>
/// <param name="z">The z value.</param>
/// <param name="w">The w value.</param>
public Vector4(float x, float y, float z, float w)
{
this.x = x;
this.y = y;
this.z = z;
this.w = w;
}
#endregion
#region Operators
/// <summary>
/// Adds two vectors.
/// </summary>
/// <param name="a">The first vector.</param>
/// <param name="b">The second vector.</param>
/// <returns>The resulting vector.</returns>
public static Vector4 operator +(Vector4 a, Vector4 b)
{
return new Vector4(a.x + b.x, a.y + b.y, a.z + b.z, a.w + b.w);
}
/// <summary>
/// Subtracts two vectors.
/// </summary>
/// <param name="a">The first vector.</param>
/// <param name="b">The second vector.</param>
/// <returns>The resulting vector.</returns>
public static Vector4 operator -(Vector4 a, Vector4 b)
{
return new Vector4(a.x - b.x, a.y - b.y, a.z - b.z, a.w - b.w);
}
/// <summary>
/// Scales the vector uniformly.
/// </summary>
/// <param name="a">The vector.</param>
/// <param name="d">The scaling value.</param>
/// <returns>The resulting vector.</returns>
public static Vector4 operator *(Vector4 a, float d)
{
return new Vector4(a.x * d, a.y * d, a.z * d, a.w * d);
}
/// <summary>
/// Scales the vector uniformly.
/// </summary>
/// <param name="d">The scaling value.</param>
/// <param name="a">The vector.</param>
/// <returns>The resulting vector.</returns>
public static Vector4 operator *(float d, Vector4 a)
{
return new Vector4(a.x * d, a.y * d, a.z * d, a.w * d);
}
/// <summary>
/// Divides the vector with a float.
/// </summary>
/// <param name="a">The vector.</param>
/// <param name="d">The dividing float value.</param>
/// <returns>The resulting vector.</returns>
public static Vector4 operator /(Vector4 a, float d)
{
return new Vector4(a.x / d, a.y / d, a.z / d, a.w / d);
}
/// <summary>
/// Subtracts the vector from a zero vector.
/// </summary>
/// <param name="a">The vector.</param>
/// <returns>The resulting vector.</returns>
public static Vector4 operator -(Vector4 a)
{
return new Vector4(-a.x, -a.y, -a.z, -a.w);
}
/// <summary>
/// Returns if two vectors equals eachother.
/// </summary>
/// <param name="lhs">The left hand side vector.</param>
/// <param name="rhs">The right hand side vector.</param>
/// <returns>If equals.</returns>
public static bool operator ==(Vector4 lhs, Vector4 rhs)
{
return (lhs - rhs).MagnitudeSqr < Epsilon;
}
/// <summary>
/// Returns if two vectors don't equal eachother.
/// </summary>
/// <param name="lhs">The left hand side vector.</param>
/// <param name="rhs">The right hand side vector.</param>
/// <returns>If not equals.</returns>
public static bool operator !=(Vector4 lhs, Vector4 rhs)
{
return (lhs - rhs).MagnitudeSqr >= Epsilon;
}
/// <summary>
/// Explicitly converts from a double-precision vector into a single-precision vector.
/// </summary>
/// <param name="v">The double-precision vector.</param>
public static explicit operator Vector4(Vector4d v)
{
return new Vector4((float)v.x, (float)v.y, (float)v.z, (float)v.w);
}
/// <summary>
/// Implicitly converts from an integer vector into a single-precision vector.
/// </summary>
/// <param name="v">The integer vector.</param>
public static implicit operator Vector4(Vector4i v)
{
return new Vector4(v.x, v.y, v.z, v.w);
}
#endregion
#region Public Methods
#region Instance
/// <summary>
/// Set x, y and z components of an existing vector.
/// </summary>
/// <param name="x">The x value.</param>
/// <param name="y">The y value.</param>
/// <param name="z">The z value.</param>
/// <param name="w">The w value.</param>
public void Set(float x, float y, float z, float w)
{
this.x = x;
this.y = y;
this.z = z;
this.w = w;
}
/// <summary>
/// Multiplies with another vector component-wise.
/// </summary>
/// <param name="scale">The vector to multiply with.</param>
public void Scale(ref Vector4 scale)
{
x *= scale.x;
y *= scale.y;
z *= scale.z;
w *= scale.w;
}
/// <summary>
/// Normalizes this vector.
/// </summary>
public void Normalize()
{
float mag = this.Magnitude;
if (mag > Epsilon)
{
x /= mag;
y /= mag;
z /= mag;
w /= mag;
}
else
{
x = y = z = w = 0;
}
}
/// <summary>
/// Clamps this vector between a specific range.
/// </summary>
/// <param name="min">The minimum component value.</param>
/// <param name="max">The maximum component value.</param>
public void Clamp(float min, float max)
{
if (x < min) x = min;
else if (x > max) x = max;
if (y < min) y = min;
else if (y > max) y = max;
if (z < min) z = min;
else if (z > max) z = max;
if (w < min) w = min;
else if (w > max) w = max;
}
#endregion
#region Object
/// <summary>
/// Returns a hash code for this vector.
/// </summary>
/// <returns>The hash code.</returns>
public override int GetHashCode()
{
return x.GetHashCode() ^ y.GetHashCode() << 2 ^ z.GetHashCode() >> 2 ^ w.GetHashCode() >> 1;
}
/// <summary>
/// Returns if this vector is equal to another one.
/// </summary>
/// <param name="other">The other vector to compare to.</param>
/// <returns>If equals.</returns>
public override bool Equals(object other)
{
if (!(other is Vector4))
{
return false;
}
Vector4 vector = (Vector4)other;
return (x == vector.x && y == vector.y && z == vector.z && w == vector.w);
}
/// <summary>
/// Returns if this vector is equal to another one.
/// </summary>
/// <param name="other">The other vector to compare to.</param>
/// <returns>If equals.</returns>
public bool Equals(Vector4 other)
{
return (x == other.x && y == other.y && z == other.z && w == other.w);
}
/// <summary>
/// Returns a nicely formatted string for this vector.
/// </summary>
/// <returns>The string.</returns>
public override string ToString()
{
return string.Format("({0}, {1}, {2}, {3})",
x.ToString("F1", CultureInfo.InvariantCulture),
y.ToString("F1", CultureInfo.InvariantCulture),
z.ToString("F1", CultureInfo.InvariantCulture),
w.ToString("F1", CultureInfo.InvariantCulture));
}
/// <summary>
/// Returns a nicely formatted string for this vector.
/// </summary>
/// <param name="format">The float format.</param>
/// <returns>The string.</returns>
public string ToString(string format)
{
return string.Format("({0}, {1}, {2}, {3})",
x.ToString(format, CultureInfo.InvariantCulture),
y.ToString(format, CultureInfo.InvariantCulture),
z.ToString(format, CultureInfo.InvariantCulture),
w.ToString(format, CultureInfo.InvariantCulture));
}
#endregion
#region Static
/// <summary>
/// Dot Product of two vectors.
/// </summary>
/// <param name="lhs">The left hand side vector.</param>
/// <param name="rhs">The right hand side vector.</param>
public static float Dot(ref Vector4 lhs, ref Vector4 rhs)
{
return lhs.x * rhs.x + lhs.y * rhs.y + lhs.z * rhs.z + lhs.w * rhs.w;
}
/// <summary>
/// Performs a linear interpolation between two vectors.
/// </summary>
/// <param name="a">The vector to interpolate from.</param>
/// <param name="b">The vector to interpolate to.</param>
/// <param name="t">The time fraction.</param>
/// <param name="result">The resulting vector.</param>
public static void Lerp(ref Vector4 a, ref Vector4 b, float t, out Vector4 result)
{
result = new Vector4(a.x + (b.x - a.x) * t, a.y + (b.y - a.y) * t, a.z + (b.z - a.z) * t, a.w + (b.w - a.w) * t);
}
/// <summary>
/// Multiplies two vectors component-wise.
/// </summary>
/// <param name="a">The first vector.</param>
/// <param name="b">The second vector.</param>
/// <param name="result">The resulting vector.</param>
public static void Scale(ref Vector4 a, ref Vector4 b, out Vector4 result)
{
result = new Vector4(a.x * b.x, a.y * b.y, a.z * b.z, a.w * b.w);
}
/// <summary>
/// Normalizes a vector.
/// </summary>
/// <param name="value">The vector to normalize.</param>
/// <param name="result">The resulting normalized vector.</param>
public static void Normalize(ref Vector4 value, out Vector4 result)
{
float mag = value.Magnitude;
if (mag > Epsilon)
{
result = new Vector4(value.x / mag, value.y / mag, value.z / mag, value.w / mag);
}
else
{
result = Vector4.zero;
}
}
#endregion
#endregion
}
}

View File

@@ -1,467 +0,0 @@
#region License
/*
MIT License
Copyright(c) 2017-2018 Mattias Edlund
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
*/
#endregion
using System;
using System.Globalization;
namespace MeshDecimator.Math
{
/// <summary>
/// A double precision 4D vector.
/// </summary>
public struct Vector4d : IEquatable<Vector4d>
{
#region Static Read-Only
/// <summary>
/// The zero vector.
/// </summary>
public static readonly Vector4d zero = new Vector4d(0, 0, 0, 0);
#endregion
#region Consts
/// <summary>
/// The vector epsilon.
/// </summary>
public const double Epsilon = double.Epsilon;
#endregion
#region Fields
/// <summary>
/// The x component.
/// </summary>
public double x;
/// <summary>
/// The y component.
/// </summary>
public double y;
/// <summary>
/// The z component.
/// </summary>
public double z;
/// <summary>
/// The w component.
/// </summary>
public double w;
#endregion
#region Properties
/// <summary>
/// Gets the magnitude of this vector.
/// </summary>
public double Magnitude
{
get { return System.Math.Sqrt(x * x + y * y + z * z + w * w); }
}
/// <summary>
/// Gets the squared magnitude of this vector.
/// </summary>
public double MagnitudeSqr
{
get { return (x * x + y * y + z * z + w * w); }
}
/// <summary>
/// Gets a normalized vector from this vector.
/// </summary>
public Vector4d Normalized
{
get
{
Vector4d result;
Normalize(ref this, out result);
return result;
}
}
/// <summary>
/// Gets or sets a specific component by index in this vector.
/// </summary>
/// <param name="index">The component index.</param>
public double this[int index]
{
get
{
switch (index)
{
case 0:
return x;
case 1:
return y;
case 2:
return z;
case 3:
return w;
default:
throw new IndexOutOfRangeException("Invalid Vector4d index!");
}
}
set
{
switch (index)
{
case 0:
x = value;
break;
case 1:
y = value;
break;
case 2:
z = value;
break;
case 3:
w = value;
break;
default:
throw new IndexOutOfRangeException("Invalid Vector4d index!");
}
}
}
#endregion
#region Constructor
/// <summary>
/// Creates a new vector with one value for all components.
/// </summary>
/// <param name="value">The value.</param>
public Vector4d(double value)
{
this.x = value;
this.y = value;
this.z = value;
this.w = value;
}
/// <summary>
/// Creates a new vector.
/// </summary>
/// <param name="x">The x value.</param>
/// <param name="y">The y value.</param>
/// <param name="z">The z value.</param>
/// <param name="w">The w value.</param>
public Vector4d(double x, double y, double z, double w)
{
this.x = x;
this.y = y;
this.z = z;
this.w = w;
}
#endregion
#region Operators
/// <summary>
/// Adds two vectors.
/// </summary>
/// <param name="a">The first vector.</param>
/// <param name="b">The second vector.</param>
/// <returns>The resulting vector.</returns>
public static Vector4d operator +(Vector4d a, Vector4d b)
{
return new Vector4d(a.x + b.x, a.y + b.y, a.z + b.z, a.w + b.w);
}
/// <summary>
/// Subtracts two vectors.
/// </summary>
/// <param name="a">The first vector.</param>
/// <param name="b">The second vector.</param>
/// <returns>The resulting vector.</returns>
public static Vector4d operator -(Vector4d a, Vector4d b)
{
return new Vector4d(a.x - b.x, a.y - b.y, a.z - b.z, a.w - b.w);
}
/// <summary>
/// Scales the vector uniformly.
/// </summary>
/// <param name="a">The vector.</param>
/// <param name="d">The scaling value.</param>
/// <returns>The resulting vector.</returns>
public static Vector4d operator *(Vector4d a, double d)
{
return new Vector4d(a.x * d, a.y * d, a.z * d, a.w * d);
}
/// <summary>
/// Scales the vector uniformly.
/// </summary>
/// <param name="d">The scaling value.</param>
/// <param name="a">The vector.</param>
/// <returns>The resulting vector.</returns>
public static Vector4d operator *(double d, Vector4d a)
{
return new Vector4d(a.x * d, a.y * d, a.z * d, a.w * d);
}
/// <summary>
/// Divides the vector with a float.
/// </summary>
/// <param name="a">The vector.</param>
/// <param name="d">The dividing float value.</param>
/// <returns>The resulting vector.</returns>
public static Vector4d operator /(Vector4d a, double d)
{
return new Vector4d(a.x / d, a.y / d, a.z / d, a.w / d);
}
/// <summary>
/// Subtracts the vector from a zero vector.
/// </summary>
/// <param name="a">The vector.</param>
/// <returns>The resulting vector.</returns>
public static Vector4d operator -(Vector4d a)
{
return new Vector4d(-a.x, -a.y, -a.z, -a.w);
}
/// <summary>
/// Returns if two vectors equals eachother.
/// </summary>
/// <param name="lhs">The left hand side vector.</param>
/// <param name="rhs">The right hand side vector.</param>
/// <returns>If equals.</returns>
public static bool operator ==(Vector4d lhs, Vector4d rhs)
{
return (lhs - rhs).MagnitudeSqr < Epsilon;
}
/// <summary>
/// Returns if two vectors don't equal eachother.
/// </summary>
/// <param name="lhs">The left hand side vector.</param>
/// <param name="rhs">The right hand side vector.</param>
/// <returns>If not equals.</returns>
public static bool operator !=(Vector4d lhs, Vector4d rhs)
{
return (lhs - rhs).MagnitudeSqr >= Epsilon;
}
/// <summary>
/// Implicitly converts from a single-precision vector into a double-precision vector.
/// </summary>
/// <param name="v">The single-precision vector.</param>
public static implicit operator Vector4d(Vector4 v)
{
return new Vector4d(v.x, v.y, v.z, v.w);
}
/// <summary>
/// Implicitly converts from an integer vector into a double-precision vector.
/// </summary>
/// <param name="v">The integer vector.</param>
public static implicit operator Vector4d(Vector4i v)
{
return new Vector4d(v.x, v.y, v.z, v.w);
}
#endregion
#region Public Methods
#region Instance
/// <summary>
/// Set x, y and z components of an existing vector.
/// </summary>
/// <param name="x">The x value.</param>
/// <param name="y">The y value.</param>
/// <param name="z">The z value.</param>
/// <param name="w">The w value.</param>
public void Set(double x, double y, double z, double w)
{
this.x = x;
this.y = y;
this.z = z;
this.w = w;
}
/// <summary>
/// Multiplies with another vector component-wise.
/// </summary>
/// <param name="scale">The vector to multiply with.</param>
public void Scale(ref Vector4d scale)
{
x *= scale.x;
y *= scale.y;
z *= scale.z;
w *= scale.w;
}
/// <summary>
/// Normalizes this vector.
/// </summary>
public void Normalize()
{
double mag = this.Magnitude;
if (mag > Epsilon)
{
x /= mag;
y /= mag;
z /= mag;
w /= mag;
}
else
{
x = y = z = w = 0;
}
}
/// <summary>
/// Clamps this vector between a specific range.
/// </summary>
/// <param name="min">The minimum component value.</param>
/// <param name="max">The maximum component value.</param>
public void Clamp(double min, double max)
{
if (x < min) x = min;
else if (x > max) x = max;
if (y < min) y = min;
else if (y > max) y = max;
if (z < min) z = min;
else if (z > max) z = max;
if (w < min) w = min;
else if (w > max) w = max;
}
#endregion
#region Object
/// <summary>
/// Returns a hash code for this vector.
/// </summary>
/// <returns>The hash code.</returns>
public override int GetHashCode()
{
return x.GetHashCode() ^ y.GetHashCode() << 2 ^ z.GetHashCode() >> 2 ^ w.GetHashCode() >> 1;
}
/// <summary>
/// Returns if this vector is equal to another one.
/// </summary>
/// <param name="other">The other vector to compare to.</param>
/// <returns>If equals.</returns>
public override bool Equals(object other)
{
if (!(other is Vector4d))
{
return false;
}
Vector4d vector = (Vector4d)other;
return (x == vector.x && y == vector.y && z == vector.z && w == vector.w);
}
/// <summary>
/// Returns if this vector is equal to another one.
/// </summary>
/// <param name="other">The other vector to compare to.</param>
/// <returns>If equals.</returns>
public bool Equals(Vector4d other)
{
return (x == other.x && y == other.y && z == other.z && w == other.w);
}
/// <summary>
/// Returns a nicely formatted string for this vector.
/// </summary>
/// <returns>The string.</returns>
public override string ToString()
{
return string.Format("({0}, {1}, {2}, {3})",
x.ToString("F1", CultureInfo.InvariantCulture),
y.ToString("F1", CultureInfo.InvariantCulture),
z.ToString("F1", CultureInfo.InvariantCulture),
w.ToString("F1", CultureInfo.InvariantCulture));
}
/// <summary>
/// Returns a nicely formatted string for this vector.
/// </summary>
/// <param name="format">The float format.</param>
/// <returns>The string.</returns>
public string ToString(string format)
{
return string.Format("({0}, {1}, {2}, {3})",
x.ToString(format, CultureInfo.InvariantCulture),
y.ToString(format, CultureInfo.InvariantCulture),
z.ToString(format, CultureInfo.InvariantCulture),
w.ToString(format, CultureInfo.InvariantCulture));
}
#endregion
#region Static
/// <summary>
/// Dot Product of two vectors.
/// </summary>
/// <param name="lhs">The left hand side vector.</param>
/// <param name="rhs">The right hand side vector.</param>
public static double Dot(ref Vector4d lhs, ref Vector4d rhs)
{
return lhs.x * rhs.x + lhs.y * rhs.y + lhs.z * rhs.z + lhs.w * rhs.w;
}
/// <summary>
/// Performs a linear interpolation between two vectors.
/// </summary>
/// <param name="a">The vector to interpolate from.</param>
/// <param name="b">The vector to interpolate to.</param>
/// <param name="t">The time fraction.</param>
/// <param name="result">The resulting vector.</param>
public static void Lerp(ref Vector4d a, ref Vector4d b, double t, out Vector4d result)
{
result = new Vector4d(a.x + (b.x - a.x) * t, a.y + (b.y - a.y) * t, a.z + (b.z - a.z) * t, a.w + (b.w - a.w) * t);
}
/// <summary>
/// Multiplies two vectors component-wise.
/// </summary>
/// <param name="a">The first vector.</param>
/// <param name="b">The second vector.</param>
/// <param name="result">The resulting vector.</param>
public static void Scale(ref Vector4d a, ref Vector4d b, out Vector4d result)
{
result = new Vector4d(a.x * b.x, a.y * b.y, a.z * b.z, a.w * b.w);
}
/// <summary>
/// Normalizes a vector.
/// </summary>
/// <param name="value">The vector to normalize.</param>
/// <param name="result">The resulting normalized vector.</param>
public static void Normalize(ref Vector4d value, out Vector4d result)
{
double mag = value.Magnitude;
if (mag > Epsilon)
{
result = new Vector4d(value.x / mag, value.y / mag, value.z / mag, value.w / mag);
}
else
{
result = Vector4d.zero;
}
}
#endregion
#endregion
}
}

View File

@@ -1,388 +0,0 @@
#region License
/*
MIT License
Copyright(c) 2017-2018 Mattias Edlund
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
*/
#endregion
using System;
using System.Globalization;
namespace MeshDecimator.Math
{
/// <summary>
/// A 4D integer vector.
/// </summary>
public struct Vector4i : IEquatable<Vector4i>
{
#region Static Read-Only
/// <summary>
/// The zero vector.
/// </summary>
public static readonly Vector4i zero = new Vector4i(0, 0, 0, 0);
#endregion
#region Fields
/// <summary>
/// The x component.
/// </summary>
public int x;
/// <summary>
/// The y component.
/// </summary>
public int y;
/// <summary>
/// The z component.
/// </summary>
public int z;
/// <summary>
/// The w component.
/// </summary>
public int w;
#endregion
#region Properties
/// <summary>
/// Gets the magnitude of this vector.
/// </summary>
public int Magnitude
{
get { return (int)System.Math.Sqrt(x * x + y * y + z * z + w * w); }
}
/// <summary>
/// Gets the squared magnitude of this vector.
/// </summary>
public int MagnitudeSqr
{
get { return (x * x + y * y + z * z + w * w); }
}
/// <summary>
/// Gets or sets a specific component by index in this vector.
/// </summary>
/// <param name="index">The component index.</param>
public int this[int index]
{
get
{
switch (index)
{
case 0:
return x;
case 1:
return y;
case 2:
return z;
case 3:
return w;
default:
throw new IndexOutOfRangeException("Invalid Vector4i index!");
}
}
set
{
switch (index)
{
case 0:
x = value;
break;
case 1:
y = value;
break;
case 2:
z = value;
break;
case 3:
w = value;
break;
default:
throw new IndexOutOfRangeException("Invalid Vector4i index!");
}
}
}
#endregion
#region Constructor
/// <summary>
/// Creates a new vector with one value for all components.
/// </summary>
/// <param name="value">The value.</param>
public Vector4i(int value)
{
this.x = value;
this.y = value;
this.z = value;
this.w = value;
}
/// <summary>
/// Creates a new vector.
/// </summary>
/// <param name="x">The x value.</param>
/// <param name="y">The y value.</param>
/// <param name="z">The z value.</param>
/// <param name="w">The w value.</param>
public Vector4i(int x, int y, int z, int w)
{
this.x = x;
this.y = y;
this.z = z;
this.w = w;
}
#endregion
#region Operators
/// <summary>
/// Adds two vectors.
/// </summary>
/// <param name="a">The first vector.</param>
/// <param name="b">The second vector.</param>
/// <returns>The resulting vector.</returns>
public static Vector4i operator +(Vector4i a, Vector4i b)
{
return new Vector4i(a.x + b.x, a.y + b.y, a.z + b.z, a.w + b.w);
}
/// <summary>
/// Subtracts two vectors.
/// </summary>
/// <param name="a">The first vector.</param>
/// <param name="b">The second vector.</param>
/// <returns>The resulting vector.</returns>
public static Vector4i operator -(Vector4i a, Vector4i b)
{
return new Vector4i(a.x - b.x, a.y - b.y, a.z - b.z, a.w - b.w);
}
/// <summary>
/// Scales the vector uniformly.
/// </summary>
/// <param name="a">The vector.</param>
/// <param name="d">The scaling value.</param>
/// <returns>The resulting vector.</returns>
public static Vector4i operator *(Vector4i a, int d)
{
return new Vector4i(a.x * d, a.y * d, a.z * d, a.w * d);
}
/// <summary>
/// Scales the vector uniformly.
/// </summary>
/// <param name="d">The scaling value.</param>
/// <param name="a">The vector.</param>
/// <returns>The resulting vector.</returns>
public static Vector4i operator *(int d, Vector4i a)
{
return new Vector4i(a.x * d, a.y * d, a.z * d, a.w * d);
}
/// <summary>
/// Divides the vector with a float.
/// </summary>
/// <param name="a">The vector.</param>
/// <param name="d">The dividing float value.</param>
/// <returns>The resulting vector.</returns>
public static Vector4i operator /(Vector4i a, int d)
{
return new Vector4i(a.x / d, a.y / d, a.z / d, a.w / d);
}
/// <summary>
/// Subtracts the vector from a zero vector.
/// </summary>
/// <param name="a">The vector.</param>
/// <returns>The resulting vector.</returns>
public static Vector4i operator -(Vector4i a)
{
return new Vector4i(-a.x, -a.y, -a.z, -a.w);
}
/// <summary>
/// Returns if two vectors equals eachother.
/// </summary>
/// <param name="lhs">The left hand side vector.</param>
/// <param name="rhs">The right hand side vector.</param>
/// <returns>If equals.</returns>
public static bool operator ==(Vector4i lhs, Vector4i rhs)
{
return (lhs.x == rhs.x && lhs.y == rhs.y && lhs.z == rhs.z && lhs.w == rhs.w);
}
/// <summary>
/// Returns if two vectors don't equal eachother.
/// </summary>
/// <param name="lhs">The left hand side vector.</param>
/// <param name="rhs">The right hand side vector.</param>
/// <returns>If not equals.</returns>
public static bool operator !=(Vector4i lhs, Vector4i rhs)
{
return (lhs.x != rhs.x || lhs.y != rhs.y || lhs.z != rhs.z || lhs.w != rhs.w);
}
/// <summary>
/// Explicitly converts from a single-precision vector into an integer vector.
/// </summary>
/// <param name="v">The single-precision vector.</param>
public static explicit operator Vector4i(Vector4 v)
{
return new Vector4i((int)v.x, (int)v.y, (int)v.z, (int)v.w);
}
/// <summary>
/// Explicitly converts from a double-precision vector into an integer vector.
/// </summary>
/// <param name="v">The double-precision vector.</param>
public static explicit operator Vector4i(Vector4d v)
{
return new Vector4i((int)v.x, (int)v.y, (int)v.z, (int)v.w);
}
#endregion
#region Public Methods
#region Instance
/// <summary>
/// Set x, y and z components of an existing vector.
/// </summary>
/// <param name="x">The x value.</param>
/// <param name="y">The y value.</param>
/// <param name="z">The z value.</param>
/// <param name="w">The w value.</param>
public void Set(int x, int y, int z, int w)
{
this.x = x;
this.y = y;
this.z = z;
this.w = w;
}
/// <summary>
/// Multiplies with another vector component-wise.
/// </summary>
/// <param name="scale">The vector to multiply with.</param>
public void Scale(ref Vector4i scale)
{
x *= scale.x;
y *= scale.y;
z *= scale.z;
w *= scale.w;
}
/// <summary>
/// Clamps this vector between a specific range.
/// </summary>
/// <param name="min">The minimum component value.</param>
/// <param name="max">The maximum component value.</param>
public void Clamp(int min, int max)
{
if (x < min) x = min;
else if (x > max) x = max;
if (y < min) y = min;
else if (y > max) y = max;
if (z < min) z = min;
else if (z > max) z = max;
if (w < min) w = min;
else if (w > max) w = max;
}
#endregion
#region Object
/// <summary>
/// Returns a hash code for this vector.
/// </summary>
/// <returns>The hash code.</returns>
public override int GetHashCode()
{
return x.GetHashCode() ^ y.GetHashCode() << 2 ^ z.GetHashCode() >> 2 ^ w.GetHashCode() >> 1;
}
/// <summary>
/// Returns if this vector is equal to another one.
/// </summary>
/// <param name="other">The other vector to compare to.</param>
/// <returns>If equals.</returns>
public override bool Equals(object other)
{
if (!(other is Vector4i))
{
return false;
}
Vector4i vector = (Vector4i)other;
return (x == vector.x && y == vector.y && z == vector.z && w == vector.w);
}
/// <summary>
/// Returns if this vector is equal to another one.
/// </summary>
/// <param name="other">The other vector to compare to.</param>
/// <returns>If equals.</returns>
public bool Equals(Vector4i other)
{
return (x == other.x && y == other.y && z == other.z && w == other.w);
}
/// <summary>
/// Returns a nicely formatted string for this vector.
/// </summary>
/// <returns>The string.</returns>
public override string ToString()
{
return string.Format("({0}, {1}, {2}, {3})",
x.ToString(CultureInfo.InvariantCulture),
y.ToString(CultureInfo.InvariantCulture),
z.ToString(CultureInfo.InvariantCulture),
w.ToString(CultureInfo.InvariantCulture));
}
/// <summary>
/// Returns a nicely formatted string for this vector.
/// </summary>
/// <param name="format">The integer format.</param>
/// <returns>The string.</returns>
public string ToString(string format)
{
return string.Format("({0}, {1}, {2}, {3})",
x.ToString(format, CultureInfo.InvariantCulture),
y.ToString(format, CultureInfo.InvariantCulture),
z.ToString(format, CultureInfo.InvariantCulture),
w.ToString(format, CultureInfo.InvariantCulture));
}
#endregion
#region Static
/// <summary>
/// Multiplies two vectors component-wise.
/// </summary>
/// <param name="a">The first vector.</param>
/// <param name="b">The second vector.</param>
/// <param name="result">The resulting vector.</param>
public static void Scale(ref Vector4i a, ref Vector4i b, out Vector4i result)
{
result = new Vector4i(a.x * b.x, a.y * b.y, a.z * b.z, a.w * b.w);
}
#endregion
#endregion
}
}

View File

@@ -1,955 +0,0 @@
#region License
/*
MIT License
Copyright(c) 2017-2018 Mattias Edlund
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
*/
#endregion
using System;
using System.Collections.Generic;
using MeshDecimator.Math;
namespace MeshDecimator
{
/// <summary>
/// A mesh.
/// </summary>
public sealed class Mesh
{
#region Consts
/// <summary>
/// The count of supported UV channels.
/// </summary>
public const int UVChannelCount = 4;
#endregion
#region Fields
private Vector3d[] vertices = null;
private int[][] indices = null;
private Vector3[] normals = null;
private Vector4[] tangents = null;
private Vector2[][] uvs2D = null;
private Vector3[][] uvs3D = null;
private Vector4[][] uvs4D = null;
private Vector4[] colors = null;
private BoneWeight[] boneWeights = null;
private static readonly int[] emptyIndices = new int[0];
#endregion
#region Properties
/// <summary>
/// Gets the count of vertices of this mesh.
/// </summary>
public int VertexCount
{
get { return vertices.Length; }
}
/// <summary>
/// Gets or sets the count of submeshes in this mesh.
/// </summary>
public int SubMeshCount
{
get { return indices.Length; }
set
{
if (value <= 0)
throw new ArgumentOutOfRangeException("value");
int[][] newIndices = new int[value][];
Array.Copy(indices, 0, newIndices, 0, MathHelper.Min(indices.Length, newIndices.Length));
indices = newIndices;
}
}
/// <summary>
/// Gets the total count of triangles in this mesh.
/// </summary>
public int TriangleCount
{
get
{
int triangleCount = 0;
for (int i = 0; i < indices.Length; i++)
{
if (indices[i] != null)
{
triangleCount += indices[i].Length / 3;
}
}
return triangleCount;
}
}
/// <summary>
/// Gets or sets the vertices for this mesh. Note that this resets all other vertex attributes.
/// </summary>
public Vector3d[] Vertices
{
get { return vertices; }
set
{
if (value == null)
throw new ArgumentNullException("value");
vertices = value;
ClearVertexAttributes();
}
}
/// <summary>
/// Gets or sets the combined indices for this mesh. Once set, the sub-mesh count gets set to 1.
/// </summary>
public int[] Indices
{
get
{
if (indices.Length == 1)
{
return indices[0] ?? emptyIndices;
}
else
{
List<int> indexList = new List<int>(TriangleCount * 3);
for (int i = 0; i < indices.Length; i++)
{
if (indices[i] != null)
{
indexList.AddRange(indices[i]);
}
}
return indexList.ToArray();
}
}
set
{
if (value == null)
throw new ArgumentNullException("value");
else if ((value.Length % 3) != 0)
throw new ArgumentException("The index count must be multiple by 3.", "value");
SubMeshCount = 1;
SetIndices(0, value);
}
}
/// <summary>
/// Gets or sets the normals for this mesh.
/// </summary>
public Vector3[] Normals
{
get { return normals; }
set
{
if (value != null && value.Length != vertices.Length)
throw new ArgumentException(string.Format("The vertex normals must be as many as the vertices. Assigned: {0} Require: {1}", value.Length, vertices.Length));
normals = value;
}
}
/// <summary>
/// Gets or sets the tangents for this mesh.
/// </summary>
public Vector4[] Tangents
{
get { return tangents; }
set
{
if (value != null && value.Length != vertices.Length)
throw new ArgumentException(string.Format("The vertex tangents must be as many as the vertices. Assigned: {0} Require: {1}", value.Length, vertices.Length));
tangents = value;
}
}
/// <summary>
/// Gets or sets the first UV set for this mesh.
/// </summary>
public Vector2[] UV1
{
get { return GetUVs2D(0); }
set { SetUVs(0, value); }
}
/// <summary>
/// Gets or sets the second UV set for this mesh.
/// </summary>
public Vector2[] UV2
{
get { return GetUVs2D(1); }
set { SetUVs(1, value); }
}
/// <summary>
/// Gets or sets the third UV set for this mesh.
/// </summary>
public Vector2[] UV3
{
get { return GetUVs2D(2); }
set { SetUVs(2, value); }
}
/// <summary>
/// Gets or sets the fourth UV set for this mesh.
/// </summary>
public Vector2[] UV4
{
get { return GetUVs2D(3); }
set { SetUVs(3, value); }
}
/// <summary>
/// Gets or sets the vertex colors for this mesh.
/// </summary>
public Vector4[] Colors
{
get { return colors; }
set
{
if (value != null && value.Length != vertices.Length)
throw new ArgumentException(string.Format("The vertex colors must be as many as the vertices. Assigned: {0} Require: {1}", value.Length, vertices.Length));
colors = value;
}
}
/// <summary>
/// Gets or sets the vertex bone weights for this mesh.
/// </summary>
public BoneWeight[] BoneWeights
{
get { return boneWeights; }
set
{
if (value != null && value.Length != vertices.Length)
throw new ArgumentException(string.Format("The vertex bone weights must be as many as the vertices. Assigned: {0} Require: {1}", value.Length, vertices.Length));
boneWeights = value;
}
}
#endregion
#region Constructor
/// <summary>
/// Creates a new mesh.
/// </summary>
/// <param name="vertices">The mesh vertices.</param>
/// <param name="indices">The mesh indices.</param>
public Mesh(Vector3d[] vertices, int[] indices)
{
if (vertices == null)
throw new ArgumentNullException("vertices");
else if (indices == null)
throw new ArgumentNullException("indices");
else if ((indices.Length % 3) != 0)
throw new ArgumentException("The index count must be multiple by 3.", "indices");
this.vertices = vertices;
this.indices = new int[1][];
this.indices[0] = indices;
}
/// <summary>
/// Creates a new mesh.
/// </summary>
/// <param name="vertices">The mesh vertices.</param>
/// <param name="indices">The mesh indices.</param>
public Mesh(Vector3d[] vertices, int[][] indices)
{
if (vertices == null)
throw new ArgumentNullException("vertices");
else if (indices == null)
throw new ArgumentNullException("indices");
for (int i = 0; i < indices.Length; i++)
{
if (indices[i] != null && (indices[i].Length % 3) != 0)
throw new ArgumentException(string.Format("The index count must be multiple by 3 at sub-mesh index {0}.", i), "indices");
}
this.vertices = vertices;
this.indices = indices;
}
#endregion
#region Private Methods
private void ClearVertexAttributes()
{
normals = null;
tangents = null;
uvs2D = null;
uvs3D = null;
uvs4D = null;
colors = null;
boneWeights = null;
}
#endregion
#region Public Methods
#region Recalculate Normals
/// <summary>
/// Recalculates the normals for this mesh smoothly.
/// </summary>
public void RecalculateNormals()
{
int vertexCount = vertices.Length;
Vector3[] normals = new Vector3[vertexCount];
int subMeshCount = this.indices.Length;
for (int subMeshIndex = 0; subMeshIndex < subMeshCount; subMeshIndex++)
{
int[] indices = this.indices[subMeshIndex];
if (indices == null)
continue;
int indexCount = indices.Length;
for (int i = 0; i < indexCount; i += 3)
{
int i0 = indices[i];
int i1 = indices[i + 1];
int i2 = indices[i + 2];
var v0 = (Vector3)vertices[i0];
var v1 = (Vector3)vertices[i1];
var v2 = (Vector3)vertices[i2];
var nx = v1 - v0;
var ny = v2 - v0;
Vector3 normal;
Vector3.Cross(ref nx, ref ny, out normal);
normal.Normalize();
normals[i0] += normal;
normals[i1] += normal;
normals[i2] += normal;
}
}
for (int i = 0; i < vertexCount; i++)
{
normals[i].Normalize();
}
this.normals = normals;
}
#endregion
#region Recalculate Tangents
/// <summary>
/// Recalculates the tangents for this mesh.
/// </summary>
public void RecalculateTangents()
{
// Make sure we have the normals first
if (normals == null)
return;
// Also make sure that we have the first UV set
bool uvIs2D = (uvs2D != null && uvs2D[0] != null);
bool uvIs3D = (uvs3D != null && uvs3D[0] != null);
bool uvIs4D = (uvs4D != null && uvs4D[0] != null);
if (!uvIs2D && !uvIs3D && !uvIs4D)
return;
int vertexCount = vertices.Length;
var tangents = new Vector4[vertexCount];
var tan1 = new Vector3[vertexCount];
var tan2 = new Vector3[vertexCount];
Vector2[] uv2D = (uvIs2D ? uvs2D[0] : null);
Vector3[] uv3D = (uvIs3D ? uvs3D[0] : null);
Vector4[] uv4D = (uvIs4D ? uvs4D[0] : null);
int subMeshCount = this.indices.Length;
for (int subMeshIndex = 0; subMeshIndex < subMeshCount; subMeshIndex++)
{
int[] indices = this.indices[subMeshIndex];
if (indices == null)
continue;
int indexCount = indices.Length;
for (int i = 0; i < indexCount; i += 3)
{
int i0 = indices[i];
int i1 = indices[i + 1];
int i2 = indices[i + 2];
var v0 = vertices[i0];
var v1 = vertices[i1];
var v2 = vertices[i2];
float s1, s2, t1, t2;
if (uvIs2D)
{
var w0 = uv2D[i0];
var w1 = uv2D[i1];
var w2 = uv2D[i2];
s1 = w1.x - w0.x;
s2 = w2.x - w0.x;
t1 = w1.y - w0.y;
t2 = w2.y - w0.y;
}
else if (uvIs3D)
{
var w0 = uv3D[i0];
var w1 = uv3D[i1];
var w2 = uv3D[i2];
s1 = w1.x - w0.x;
s2 = w2.x - w0.x;
t1 = w1.y - w0.y;
t2 = w2.y - w0.y;
}
else
{
var w0 = uv4D[i0];
var w1 = uv4D[i1];
var w2 = uv4D[i2];
s1 = w1.x - w0.x;
s2 = w2.x - w0.x;
t1 = w1.y - w0.y;
t2 = w2.y - w0.y;
}
float x1 = (float)(v1.x - v0.x);
float x2 = (float)(v2.x - v0.x);
float y1 = (float)(v1.y - v0.y);
float y2 = (float)(v2.y - v0.y);
float z1 = (float)(v1.z - v0.z);
float z2 = (float)(v2.z - v0.z);
float r = 1f / (s1 * t2 - s2 * t1);
var sdir = new Vector3((t2 * x1 - t1 * x2) * r, (t2 * y1 - t1 * y2) * r, (t2 * z1 - t1 * z2) * r);
var tdir = new Vector3((s1 * x2 - s2 * x1) * r, (s1 * y2 - s2 * y1) * r, (s1 * z2 - s2 * z1) * r);
tan1[i0] += sdir;
tan1[i1] += sdir;
tan1[i2] += sdir;
tan2[i0] += tdir;
tan2[i1] += tdir;
tan2[i2] += tdir;
}
}
for (int i = 0; i < vertexCount; i++)
{
var n = normals[i];
var t = tan1[i];
var tmp = (t - n * Vector3.Dot(ref n, ref t));
tmp.Normalize();
Vector3 c;
Vector3.Cross(ref n, ref t, out c);
float dot = Vector3.Dot(ref c, ref tan2[i]);
float w = (dot < 0f ? -1f : 1f);
tangents[i] = new Vector4(tmp.x, tmp.y, tmp.z, w);
}
this.tangents = tangents;
}
#endregion
#region Triangles
/// <summary>
/// Returns the count of triangles for a specific sub-mesh in this mesh.
/// </summary>
/// <param name="subMeshIndex">The sub-mesh index.</param>
/// <returns>The triangle count.</returns>
public int GetTriangleCount(int subMeshIndex)
{
if (subMeshIndex < 0 || subMeshIndex >= indices.Length)
throw new IndexOutOfRangeException();
return indices[subMeshIndex].Length / 3;
}
/// <summary>
/// Returns the triangle indices of a specific sub-mesh in this mesh.
/// </summary>
/// <param name="subMeshIndex">The sub-mesh index.</param>
/// <returns>The triangle indices.</returns>
public int[] GetIndices(int subMeshIndex)
{
if (subMeshIndex < 0 || subMeshIndex >= indices.Length)
throw new IndexOutOfRangeException();
return indices[subMeshIndex] ?? emptyIndices;
}
/// <summary>
/// Returns the triangle indices for all sub-meshes in this mesh.
/// </summary>
/// <returns>The sub-mesh triangle indices.</returns>
public int[][] GetSubMeshIndices()
{
var subMeshIndices = new int[indices.Length][];
for (int subMeshIndex = 0; subMeshIndex < indices.Length; subMeshIndex++)
{
subMeshIndices[subMeshIndex] = indices[subMeshIndex] ?? emptyIndices;
}
return subMeshIndices;
}
/// <summary>
/// Sets the triangle indices of a specific sub-mesh in this mesh.
/// </summary>
/// <param name="subMeshIndex">The sub-mesh index.</param>
/// <param name="indices">The triangle indices.</param>
public void SetIndices(int subMeshIndex, int[] indices)
{
if (subMeshIndex < 0 || subMeshIndex >= this.indices.Length)
throw new IndexOutOfRangeException();
else if (indices == null)
throw new ArgumentNullException("indices");
else if ((indices.Length % 3) != 0)
throw new ArgumentException("The index count must be multiple by 3.", "indices");
this.indices[subMeshIndex] = indices;
}
#endregion
#region UV Sets
#region Getting
/// <summary>
/// Returns the UV dimension for a specific channel.
/// </summary>
/// <param name="channel"></param>
/// <returns>The UV dimension count.</returns>
public int GetUVDimension(int channel)
{
if (channel < 0 || channel >= UVChannelCount)
throw new ArgumentOutOfRangeException("channel");
if (uvs2D != null && uvs2D[channel] != null)
{
return 2;
}
else if (uvs3D != null && uvs3D[channel] != null)
{
return 3;
}
else if (uvs4D != null && uvs4D[channel] != null)
{
return 4;
}
else
{
return 0;
}
}
/// <summary>
/// Returns the UVs (2D) from a specific channel.
/// </summary>
/// <param name="channel">The channel index.</param>
/// <returns>The UVs.</returns>
public Vector2[] GetUVs2D(int channel)
{
if (channel < 0 || channel >= UVChannelCount)
throw new ArgumentOutOfRangeException("channel");
if (uvs2D != null && uvs2D[channel] != null)
{
return uvs2D[channel];
}
else
{
return null;
}
}
/// <summary>
/// Returns the UVs (3D) from a specific channel.
/// </summary>
/// <param name="channel">The channel index.</param>
/// <returns>The UVs.</returns>
public Vector3[] GetUVs3D(int channel)
{
if (channel < 0 || channel >= UVChannelCount)
throw new ArgumentOutOfRangeException("channel");
if (uvs3D != null && uvs3D[channel] != null)
{
return uvs3D[channel];
}
else
{
return null;
}
}
/// <summary>
/// Returns the UVs (4D) from a specific channel.
/// </summary>
/// <param name="channel">The channel index.</param>
/// <returns>The UVs.</returns>
public Vector4[] GetUVs4D(int channel)
{
if (channel < 0 || channel >= UVChannelCount)
throw new ArgumentOutOfRangeException("channel");
if (uvs4D != null && uvs4D[channel] != null)
{
return uvs4D[channel];
}
else
{
return null;
}
}
/// <summary>
/// Returns the UVs (2D) from a specific channel.
/// </summary>
/// <param name="channel">The channel index.</param>
/// <param name="uvs">The UVs.</param>
public void GetUVs(int channel, List<Vector2> uvs)
{
if (channel < 0 || channel >= UVChannelCount)
throw new ArgumentOutOfRangeException("channel");
else if (uvs == null)
throw new ArgumentNullException("uvs");
uvs.Clear();
if (uvs2D != null && uvs2D[channel] != null)
{
var uvData = uvs2D[channel];
if (uvData != null)
{
uvs.AddRange(uvData);
}
}
}
/// <summary>
/// Returns the UVs (3D) from a specific channel.
/// </summary>
/// <param name="channel">The channel index.</param>
/// <param name="uvs">The UVs.</param>
public void GetUVs(int channel, List<Vector3> uvs)
{
if (channel < 0 || channel >= UVChannelCount)
throw new ArgumentOutOfRangeException("channel");
else if (uvs == null)
throw new ArgumentNullException("uvs");
uvs.Clear();
if (uvs3D != null && uvs3D[channel] != null)
{
var uvData = uvs3D[channel];
if (uvData != null)
{
uvs.AddRange(uvData);
}
}
}
/// <summary>
/// Returns the UVs (4D) from a specific channel.
/// </summary>
/// <param name="channel">The channel index.</param>
/// <param name="uvs">The UVs.</param>
public void GetUVs(int channel, List<Vector4> uvs)
{
if (channel < 0 || channel >= UVChannelCount)
throw new ArgumentOutOfRangeException("channel");
else if (uvs == null)
throw new ArgumentNullException("uvs");
uvs.Clear();
if (uvs4D != null && uvs4D[channel] != null)
{
var uvData = uvs4D[channel];
if (uvData != null)
{
uvs.AddRange(uvData);
}
}
}
#endregion
#region Setting
/// <summary>
/// Sets the UVs (2D) for a specific channel.
/// </summary>
/// <param name="channel">The channel index.</param>
/// <param name="uvs">The UVs.</param>
public void SetUVs(int channel, Vector2[] uvs)
{
if (channel < 0 || channel >= UVChannelCount)
throw new ArgumentOutOfRangeException("channel");
if (uvs != null && uvs.Length > 0)
{
if (uvs.Length != vertices.Length)
throw new ArgumentException(string.Format("The vertex UVs must be as many as the vertices. Assigned: {0} Require: {1}", uvs.Length, vertices.Length));
if (uvs2D == null)
uvs2D = new Vector2[UVChannelCount][];
int uvCount = uvs.Length;
var uvSet = new Vector2[uvCount];
uvs2D[channel] = uvSet;
uvs.CopyTo(uvSet, 0);
}
else
{
if (uvs2D != null)
{
uvs2D[channel] = null;
}
}
if (uvs3D != null)
{
uvs3D[channel] = null;
}
if (uvs4D != null)
{
uvs4D[channel] = null;
}
}
/// <summary>
/// Sets the UVs (3D) for a specific channel.
/// </summary>
/// <param name="channel">The channel index.</param>
/// <param name="uvs">The UVs.</param>
public void SetUVs(int channel, Vector3[] uvs)
{
if (channel < 0 || channel >= UVChannelCount)
throw new ArgumentOutOfRangeException("channel");
if (uvs != null && uvs.Length > 0)
{
int uvCount = uvs.Length;
if (uvCount != vertices.Length)
throw new ArgumentException(string.Format("The vertex UVs must be as many as the vertices. Assigned: {0} Require: {1}", uvCount, vertices.Length), "uvs");
if (uvs3D == null)
uvs3D = new Vector3[UVChannelCount][];
var uvSet = new Vector3[uvCount];
uvs3D[channel] = uvSet;
uvs.CopyTo(uvSet, 0);
}
else
{
if (uvs3D != null)
{
uvs3D[channel] = null;
}
}
if (uvs2D != null)
{
uvs2D[channel] = null;
}
if (uvs4D != null)
{
uvs4D[channel] = null;
}
}
/// <summary>
/// Sets the UVs (4D) for a specific channel.
/// </summary>
/// <param name="channel">The channel index.</param>
/// <param name="uvs">The UVs.</param>
public void SetUVs(int channel, Vector4[] uvs)
{
if (channel < 0 || channel >= UVChannelCount)
throw new ArgumentOutOfRangeException("channel");
if (uvs != null && uvs.Length > 0)
{
int uvCount = uvs.Length;
if (uvCount != vertices.Length)
throw new ArgumentException(string.Format("The vertex UVs must be as many as the vertices. Assigned: {0} Require: {1}", uvCount, vertices.Length), "uvs");
if (uvs4D == null)
uvs4D = new Vector4[UVChannelCount][];
var uvSet = new Vector4[uvCount];
uvs4D[channel] = uvSet;
uvs.CopyTo(uvSet, 0);
}
else
{
if (uvs4D != null)
{
uvs4D[channel] = null;
}
}
if (uvs2D != null)
{
uvs2D[channel] = null;
}
if (uvs3D != null)
{
uvs3D[channel] = null;
}
}
/// <summary>
/// Sets the UVs (2D) for a specific channel.
/// </summary>
/// <param name="channel">The channel index.</param>
/// <param name="uvs">The UVs.</param>
public void SetUVs(int channel, List<Vector2> uvs)
{
if (channel < 0 || channel >= UVChannelCount)
throw new ArgumentOutOfRangeException("channel");
if (uvs != null && uvs.Count > 0)
{
int uvCount = uvs.Count;
if (uvCount != vertices.Length)
throw new ArgumentException(string.Format("The vertex UVs must be as many as the vertices. Assigned: {0} Require: {1}", uvCount, vertices.Length), "uvs");
if (uvs2D == null)
uvs2D = new Vector2[UVChannelCount][];
var uvSet = new Vector2[uvCount];
uvs2D[channel] = uvSet;
uvs.CopyTo(uvSet, 0);
}
else
{
if (uvs2D != null)
{
uvs2D[channel] = null;
}
}
if (uvs3D != null)
{
uvs3D[channel] = null;
}
if (uvs4D != null)
{
uvs4D[channel] = null;
}
}
/// <summary>
/// Sets the UVs (3D) for a specific channel.
/// </summary>
/// <param name="channel">The channel index.</param>
/// <param name="uvs">The UVs.</param>
public void SetUVs(int channel, List<Vector3> uvs)
{
if (channel < 0 || channel >= UVChannelCount)
throw new ArgumentOutOfRangeException("channel");
if (uvs != null && uvs.Count > 0)
{
int uvCount = uvs.Count;
if (uvCount != vertices.Length)
throw new ArgumentException(string.Format("The vertex UVs must be as many as the vertices. Assigned: {0} Require: {1}", uvCount, vertices.Length), "uvs");
if (uvs3D == null)
uvs3D = new Vector3[UVChannelCount][];
var uvSet = new Vector3[uvCount];
uvs3D[channel] = uvSet;
uvs.CopyTo(uvSet, 0);
}
else
{
if (uvs3D != null)
{
uvs3D[channel] = null;
}
}
if (uvs2D != null)
{
uvs2D[channel] = null;
}
if (uvs4D != null)
{
uvs4D[channel] = null;
}
}
/// <summary>
/// Sets the UVs (4D) for a specific channel.
/// </summary>
/// <param name="channel">The channel index.</param>
/// <param name="uvs">The UVs.</param>
public void SetUVs(int channel, List<Vector4> uvs)
{
if (channel < 0 || channel >= UVChannelCount)
throw new ArgumentOutOfRangeException("channel");
if (uvs != null && uvs.Count > 0)
{
int uvCount = uvs.Count;
if (uvCount != vertices.Length)
throw new ArgumentException(string.Format("The vertex UVs must be as many as the vertices. Assigned: {0} Require: {1}", uvCount, vertices.Length), "uvs");
if (uvs4D == null)
uvs4D = new Vector4[UVChannelCount][];
var uvSet = new Vector4[uvCount];
uvs4D[channel] = uvSet;
uvs.CopyTo(uvSet, 0);
}
else
{
if (uvs4D != null)
{
uvs4D[channel] = null;
}
}
if (uvs2D != null)
{
uvs2D[channel] = null;
}
if (uvs3D != null)
{
uvs3D[channel] = null;
}
}
#endregion
#endregion
#region To String
/// <summary>
/// Returns the text-representation of this mesh.
/// </summary>
/// <returns>The text-representation.</returns>
public override string ToString()
{
return string.Format("Vertices: {0}", vertices.Length);
}
#endregion
#endregion
}
}

View File

@@ -1,180 +0,0 @@
#region License
/*
MIT License
Copyright(c) 2017-2018 Mattias Edlund
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
*/
#endregion
using System;
using MeshDecimator.Algorithms;
namespace MeshDecimator
{
#region Algorithm
/// <summary>
/// The decimation algorithms.
/// </summary>
public enum Algorithm
{
/// <summary>
/// The default algorithm.
/// </summary>
Default,
/// <summary>
/// The fast quadric mesh simplification algorithm.
/// </summary>
FastQuadricMesh
}
#endregion
/// <summary>
/// The mesh decimation API.
/// </summary>
public static class MeshDecimation
{
#region Public Methods
#region Create Algorithm
/// <summary>
/// Creates a specific decimation algorithm.
/// </summary>
/// <param name="algorithm">The desired algorithm.</param>
/// <returns>The decimation algorithm.</returns>
public static DecimationAlgorithm CreateAlgorithm(Algorithm algorithm)
{
DecimationAlgorithm alg = null;
switch (algorithm)
{
case Algorithm.Default:
case Algorithm.FastQuadricMesh:
alg = new FastQuadricMeshSimplification();
break;
default:
throw new ArgumentException("The specified algorithm is not supported.", "algorithm");
}
return alg;
}
#endregion
#region Decimate Mesh
/// <summary>
/// Decimates a mesh.
/// </summary>
/// <param name="mesh">The mesh to decimate.</param>
/// <param name="targetTriangleCount">The target triangle count.</param>
/// <returns>The decimated mesh.</returns>
public static Mesh DecimateMesh(Mesh mesh, int targetTriangleCount)
{
return DecimateMesh(Algorithm.Default, mesh, targetTriangleCount);
}
/// <summary>
/// Decimates a mesh.
/// </summary>
/// <param name="algorithm">The desired algorithm.</param>
/// <param name="mesh">The mesh to decimate.</param>
/// <param name="targetTriangleCount">The target triangle count.</param>
/// <returns>The decimated mesh.</returns>
public static Mesh DecimateMesh(Algorithm algorithm, Mesh mesh, int targetTriangleCount)
{
if (mesh == null)
throw new ArgumentNullException("mesh");
var decimationAlgorithm = CreateAlgorithm(algorithm);
return DecimateMesh(decimationAlgorithm, mesh, targetTriangleCount);
}
/// <summary>
/// Decimates a mesh.
/// </summary>
/// <param name="algorithm">The decimation algorithm.</param>
/// <param name="mesh">The mesh to decimate.</param>
/// <param name="targetTriangleCount">The target triangle count.</param>
/// <returns>The decimated mesh.</returns>
public static Mesh DecimateMesh(DecimationAlgorithm algorithm, Mesh mesh, int targetTriangleCount)
{
if (algorithm == null)
throw new ArgumentNullException("algorithm");
else if (mesh == null)
throw new ArgumentNullException("mesh");
int currentTriangleCount = mesh.TriangleCount;
if (targetTriangleCount > currentTriangleCount)
targetTriangleCount = currentTriangleCount;
else if (targetTriangleCount < 0)
targetTriangleCount = 0;
algorithm.Initialize(mesh);
algorithm.DecimateMesh(targetTriangleCount);
return algorithm.ToMesh();
}
#endregion
#region Decimate Mesh Lossless
/// <summary>
/// Decimates a mesh without losing any quality.
/// </summary>
/// <param name="mesh">The mesh to decimate.</param>
/// <returns>The decimated mesh.</returns>
public static Mesh DecimateMeshLossless(Mesh mesh)
{
return DecimateMeshLossless(Algorithm.Default, mesh);
}
/// <summary>
/// Decimates a mesh without losing any quality.
/// </summary>
/// <param name="algorithm">The desired algorithm.</param>
/// <param name="mesh">The mesh to decimate.</param>
/// <returns>The decimated mesh.</returns>
public static Mesh DecimateMeshLossless(Algorithm algorithm, Mesh mesh)
{
if (mesh == null)
throw new ArgumentNullException("mesh");
var decimationAlgorithm = CreateAlgorithm(algorithm);
return DecimateMeshLossless(decimationAlgorithm, mesh);
}
/// <summary>
/// Decimates a mesh without losing any quality.
/// </summary>
/// <param name="algorithm">The decimation algorithm.</param>
/// <param name="mesh">The mesh to decimate.</param>
/// <returns>The decimated mesh.</returns>
public static Mesh DecimateMeshLossless(DecimationAlgorithm algorithm, Mesh mesh)
{
if (algorithm == null)
throw new ArgumentNullException("algorithm");
else if (mesh == null)
throw new ArgumentNullException("mesh");
int currentTriangleCount = mesh.TriangleCount;
algorithm.Initialize(mesh);
algorithm.DecimateMeshLossless();
return algorithm.ToMesh();
}
#endregion
#endregion
}
}

View File

@@ -34,65 +34,44 @@ namespace LightlessSync.UI;
public class CompactUi : WindowMediatorSubscriberBase public class CompactUi : WindowMediatorSubscriberBase
{ {
#region Constants
private const float ConnectButtonHighlightThickness = 14f;
#endregion
#region Services
private readonly ApiController _apiController;
private readonly CharacterAnalyzer _characterAnalyzer; private readonly CharacterAnalyzer _characterAnalyzer;
private readonly DalamudUtilService _dalamudUtilService; private readonly ApiController _apiController;
private readonly DrawEntityFactory _drawEntityFactory;
private readonly FileUploadManager _fileTransferManager;
private readonly IpcManager _ipcManager;
private readonly LightFinderService _broadcastService;
private readonly LightlessConfigService _configService; private readonly LightlessConfigService _configService;
private readonly LightlessMediator _lightlessMediator; private readonly LightlessMediator _lightlessMediator;
private readonly PairLedger _pairLedger; private readonly PairLedger _pairLedger;
private readonly ConcurrentDictionary<GameObjectHandler, Dictionary<string, FileDownloadStatus>> _currentDownloads = new();
private readonly DrawEntityFactory _drawEntityFactory;
private readonly FileUploadManager _fileTransferManager;
private readonly PlayerPerformanceConfigService _playerPerformanceConfig; private readonly PlayerPerformanceConfigService _playerPerformanceConfig;
private readonly PairUiService _pairUiService; private readonly PairUiService _pairUiService;
private readonly ServerConfigurationManager _serverManager;
private readonly TagHandler _tagHandler;
private readonly UiSharedService _uiSharedService;
#endregion
#region UI Components
private readonly AnimatedHeader _animatedHeader = new();
private readonly RenamePairTagUi _renamePairTagUi;
private readonly RenameSyncshellTagUi _renameSyncshellTagUi;
private readonly SelectPairForTagUi _selectPairsForGroupUi;
private readonly SelectSyncshellForTagUi _selectSyncshellForTagUi;
private readonly SelectTagForPairUi _selectTagForPairUi; private readonly SelectTagForPairUi _selectTagForPairUi;
private readonly SelectTagForSyncshellUi _selectTagForSyncshellUi; private readonly SelectTagForSyncshellUi _selectTagForSyncshellUi;
private readonly SeluneBrush _seluneBrush = new(); private readonly SelectSyncshellForTagUi _selectSyncshellForTagUi;
private readonly RenameSyncshellTagUi _renameSyncshellTagUi;
private readonly SelectPairForTagUi _selectPairsForGroupUi;
private readonly RenamePairTagUi _renamePairTagUi;
private readonly IpcManager _ipcManager;
private readonly ServerConfigurationManager _serverManager;
private readonly TopTabMenu _tabMenu; private readonly TopTabMenu _tabMenu;
private readonly TagHandler _tagHandler;
#endregion private readonly UiSharedService _uiSharedService;
private readonly LightFinderService _broadcastService;
#region State private readonly DalamudUtilService _dalamudUtilService;
private readonly ConcurrentDictionary<GameObjectHandler, Dictionary<string, FileDownloadStatus>> _currentDownloads = new();
private List<IDrawFolder> _drawFolders; private List<IDrawFolder> _drawFolders;
private Pair? _focusedPair;
private Pair? _lastAddedUser; private Pair? _lastAddedUser;
private string _lastAddedUserComment = string.Empty; private string _lastAddedUserComment = string.Empty;
private Vector2 _lastPosition = Vector2.One; private Vector2 _lastPosition = Vector2.One;
private Vector2 _lastSize = Vector2.One; private Vector2 _lastSize = Vector2.One;
private int _pendingFocusFrame = -1;
private Pair? _pendingFocusPair;
private bool _showModalForUserAddition; private bool _showModalForUserAddition;
private float _transferPartHeight; private float _transferPartHeight;
private bool _wasOpen; private bool _wasOpen;
private float _windowContentWidth; private float _windowContentWidth;
private readonly SeluneBrush _seluneBrush = new();
#endregion private const float _connectButtonHighlightThickness = 14f;
private Pair? _focusedPair;
#region Constructor private Pair? _pendingFocusPair;
private int _pendingFocusFrame = -1;
public CompactUi( public CompactUi(
ILogger<CompactUi> logger, ILogger<CompactUi> logger,
@@ -148,11 +127,6 @@ public class CompactUi : WindowMediatorSubscriberBase
.Apply(); .Apply();
_drawFolders = [.. DrawFolders]; _drawFolders = [.. DrawFolders];
_animatedHeader.Height = 120f;
_animatedHeader.EnableBottomGradient = true;
_animatedHeader.GradientHeight = 250f;
_animatedHeader.EnableParticles = _configService.Current.EnableParticleEffects;
#if DEBUG #if DEBUG
string dev = "Dev Build"; string dev = "Dev Build";
@@ -167,26 +141,18 @@ public class CompactUi : WindowMediatorSubscriberBase
Mediator.Subscribe<SwitchToIntroUiMessage>(this, (_) => IsOpen = false); Mediator.Subscribe<SwitchToIntroUiMessage>(this, (_) => IsOpen = false);
Mediator.Subscribe<CutsceneStartMessage>(this, (_) => UiSharedService_GposeStart()); Mediator.Subscribe<CutsceneStartMessage>(this, (_) => UiSharedService_GposeStart());
Mediator.Subscribe<CutsceneEndMessage>(this, (_) => UiSharedService_GposeEnd()); Mediator.Subscribe<CutsceneEndMessage>(this, (_) => UiSharedService_GposeEnd());
Mediator.Subscribe<DownloadStartedMessage>(this, msg => Mediator.Subscribe<DownloadStartedMessage>(this, (msg) => _currentDownloads[msg.DownloadId] = msg.DownloadStatus);
{
_currentDownloads[msg.DownloadId] = new Dictionary<string, FileDownloadStatus>(msg.DownloadStatus, StringComparer.Ordinal);
});
Mediator.Subscribe<DownloadFinishedMessage>(this, (msg) => _currentDownloads.TryRemove(msg.DownloadId, out _)); Mediator.Subscribe<DownloadFinishedMessage>(this, (msg) => _currentDownloads.TryRemove(msg.DownloadId, out _));
Mediator.Subscribe<RefreshUiMessage>(this, (msg) => _drawFolders = [.. DrawFolders]); Mediator.Subscribe<RefreshUiMessage>(this, (msg) => _drawFolders = DrawFolders.ToList());
_characterAnalyzer = characterAnalyzer; _characterAnalyzer = characterAnalyzer;
_playerPerformanceConfig = playerPerformanceConfig; _playerPerformanceConfig = playerPerformanceConfig;
_lightlessMediator = mediator; _lightlessMediator = mediator;
} }
#endregion
#region Lifecycle
public override void OnClose() public override void OnClose()
{ {
ForceReleaseFocus(); ForceReleaseFocus();
_animatedHeader.ClearParticles();
base.OnClose(); base.OnClose();
} }
@@ -198,13 +164,6 @@ public class CompactUi : WindowMediatorSubscriberBase
using var selune = Selune.Begin(_seluneBrush, drawList, windowPos, windowSize); using var selune = Selune.Begin(_seluneBrush, drawList, windowPos, windowSize);
_windowContentWidth = UiSharedService.GetWindowContentRegionWidth(); _windowContentWidth = UiSharedService.GetWindowContentRegionWidth();
// Draw animated header background (just the gradient/particles, content drawn by existing methods)
var startCursorY = ImGui.GetCursorPosY();
_animatedHeader.Draw(_windowContentWidth, (_, _) => { });
// Reset cursor to draw content on top of the header background
ImGui.SetCursorPosY(startCursorY);
if (!_apiController.IsCurrentVersion) if (!_apiController.IsCurrentVersion)
{ {
var ver = _apiController.CurrentClientVersion; var ver = _apiController.CurrentClientVersion;
@@ -250,11 +209,17 @@ public class CompactUi : WindowMediatorSubscriberBase
} }
using (ImRaii.PushId("header")) DrawUIDHeader(); using (ImRaii.PushId("header")) DrawUIDHeader();
_uiSharedService.RoundedSeparator(UIColors.Get("LightlessPurple"), 2.5f, 1f, 12f);
using (ImRaii.PushId("serverstatus"))
{
DrawServerStatus();
}
selune.DrawHighlightOnly(ImGui.GetIO().DeltaTime); selune.DrawHighlightOnly(ImGui.GetIO().DeltaTime);
var style = ImGui.GetStyle(); var style = ImGui.GetStyle();
var contentMinY = windowPos.Y + ImGui.GetWindowContentRegionMin().Y; var contentMinY = windowPos.Y + ImGui.GetWindowContentRegionMin().Y;
var gradientInset = 4f * ImGuiHelpers.GlobalScale; var gradientInset = 4f * ImGuiHelpers.GlobalScale;
var gradientTop = MathF.Max(contentMinY, ImGui.GetCursorScreenPos().Y - style.ItemSpacing.Y + gradientInset); var gradientTop = MathF.Max(contentMinY, ImGui.GetCursorScreenPos().Y - style.ItemSpacing.Y + gradientInset);
ImGui.Separator();
if (_apiController.ServerState is ServerState.Connected) if (_apiController.ServerState is ServerState.Connected)
{ {
@@ -262,6 +227,7 @@ public class CompactUi : WindowMediatorSubscriberBase
using (ImRaii.PushId("global-topmenu")) _tabMenu.Draw(pairSnapshot); using (ImRaii.PushId("global-topmenu")) _tabMenu.Draw(pairSnapshot);
using (ImRaii.PushId("pairlist")) DrawPairs(); using (ImRaii.PushId("pairlist")) DrawPairs();
ImGui.Separator();
var transfersTop = ImGui.GetCursorScreenPos().Y; var transfersTop = ImGui.GetCursorScreenPos().Y;
var gradientBottom = MathF.Max(gradientTop, transfersTop - style.ItemSpacing.Y - gradientInset); var gradientBottom = MathF.Max(gradientTop, transfersTop - style.ItemSpacing.Y - gradientInset);
selune.DrawGradient(gradientTop, gradientBottom, ImGui.GetIO().DeltaTime); selune.DrawGradient(gradientTop, gradientBottom, ImGui.GetIO().DeltaTime);
@@ -324,10 +290,6 @@ public class CompactUi : WindowMediatorSubscriberBase
} }
} }
#endregion
#region Content Drawing
private void DrawPairs() private void DrawPairs()
{ {
float ySize = Math.Abs(_transferPartHeight) < 0.0001f float ySize = Math.Abs(_transferPartHeight) < 0.0001f
@@ -346,6 +308,95 @@ public class CompactUi : WindowMediatorSubscriberBase
ImGui.EndChild(); ImGui.EndChild();
} }
private void DrawServerStatus()
{
var buttonSize = _uiSharedService.GetIconButtonSize(FontAwesomeIcon.Link);
var userCount = _apiController.OnlineUsers.ToString(CultureInfo.InvariantCulture);
var userSize = ImGui.CalcTextSize(userCount);
var textSize = ImGui.CalcTextSize("Users Online");
#if DEBUG
string shardConnection = $"Shard: {_apiController.ServerInfo.ShardName}";
#else
string shardConnection = string.Equals(_apiController.ServerInfo.ShardName, "Main", StringComparison.OrdinalIgnoreCase) ? string.Empty : $"Shard: {_apiController.ServerInfo.ShardName}";
#endif
var shardTextSize = ImGui.CalcTextSize(shardConnection);
var printShard = !string.IsNullOrEmpty(_apiController.ServerInfo.ShardName) && shardConnection != string.Empty;
if (_apiController.ServerState is ServerState.Connected)
{
ImGui.SetCursorPosX((ImGui.GetWindowContentRegionMin().X + UiSharedService.GetWindowContentRegionWidth()) / 2 - (userSize.X + textSize.X) / 2 - ImGui.GetStyle().ItemSpacing.X / 2);
if (!printShard) ImGui.AlignTextToFramePadding();
ImGui.TextColored(UIColors.Get("LightlessPurple"), userCount);
ImGui.SameLine();
if (!printShard) ImGui.AlignTextToFramePadding();
ImGui.TextUnformatted("Users Online");
}
else
{
ImGui.AlignTextToFramePadding();
ImGui.TextColored(UIColors.Get("DimRed"), "Not connected to any server");
}
if (printShard)
{
ImGui.SetCursorPosY(ImGui.GetCursorPosY() - ImGui.GetStyle().ItemSpacing.Y);
ImGui.SetCursorPosX((ImGui.GetWindowContentRegionMin().X + UiSharedService.GetWindowContentRegionWidth()) / 2 - shardTextSize.X / 2);
ImGui.TextUnformatted(shardConnection);
}
ImGui.SameLine();
if (printShard)
{
ImGui.SetCursorPosY(ImGui.GetCursorPosY() - ((userSize.Y + textSize.Y) / 2 + shardTextSize.Y) / 2 - ImGui.GetStyle().ItemSpacing.Y + buttonSize.Y / 2);
}
bool isConnectingOrConnected = _apiController.ServerState is ServerState.Connected or ServerState.Connecting or ServerState.Reconnecting;
var color = UiSharedService.GetBoolColor(!isConnectingOrConnected);
var connectedIcon = isConnectingOrConnected ? FontAwesomeIcon.Unlink : FontAwesomeIcon.Link;
ImGui.SameLine(ImGui.GetWindowContentRegionMin().X + UiSharedService.GetWindowContentRegionWidth() - buttonSize.X);
if (printShard)
{
ImGui.SetCursorPosY(ImGui.GetCursorPosY() - ((userSize.Y + textSize.Y) / 2 + shardTextSize.Y) / 2 - ImGui.GetStyle().ItemSpacing.Y + buttonSize.Y / 2);
}
if (_apiController.ServerState is not (ServerState.Reconnecting or ServerState.Disconnecting))
{
using (ImRaii.PushColor(ImGuiCol.Text, color))
{
if (_uiSharedService.IconButton(connectedIcon))
{
if (isConnectingOrConnected && !_serverManager.CurrentServer.FullPause)
{
_serverManager.CurrentServer.FullPause = true;
_serverManager.Save();
}
else if (!isConnectingOrConnected && _serverManager.CurrentServer.FullPause)
{
_serverManager.CurrentServer.FullPause = false;
_serverManager.Save();
}
_ = _apiController.CreateConnectionsAsync();
}
}
if (ImGui.IsItemHovered(ImGuiHoveredFlags.AllowWhenBlockedByActiveItem) || ImGui.IsItemActive())
{
Selune.RegisterHighlight(
ImGui.GetItemRectMin(),
ImGui.GetItemRectMax(),
SeluneHighlightMode.Both,
borderOnly: true,
borderThicknessOverride: _connectButtonHighlightThickness,
exactSize: true,
clipToElement: true,
roundingOverride: ImGui.GetStyle().FrameRounding);
}
UiSharedService.AttachToolTip(isConnectingOrConnected ? "Disconnect from " + _serverManager.CurrentServer.ServerName : "Connect to " + _serverManager.CurrentServer.ServerName);
}
}
private void DrawTransfers() private void DrawTransfers()
{ {
var currentUploads = _fileTransferManager.GetCurrentUploadsSnapshot(); var currentUploads = _fileTransferManager.GetCurrentUploadsSnapshot();
@@ -441,9 +492,11 @@ public class CompactUi : WindowMediatorSubscriberBase
return new DownloadSummary(totalFiles, transferredFiles, transferredBytes, totalBytes); return new DownloadSummary(totalFiles, transferredFiles, transferredBytes, totalBytes);
} }
#endregion [StructLayout(LayoutKind.Auto)]
private readonly record struct DownloadSummary(int TotalFiles, int TransferredFiles, long TransferredBytes, long TotalBytes)
#region Header Drawing {
public bool HasDownloads => TotalFiles > 0 || TotalBytes > 0;
}
private void DrawUIDHeader() private void DrawUIDHeader()
{ {
@@ -479,52 +532,21 @@ public class CompactUi : WindowMediatorSubscriberBase
using (_uiSharedService.IconFont.Push()) using (_uiSharedService.IconFont.Push())
iconSize = ImGui.CalcTextSize(FontAwesomeIcon.PersonCirclePlus.ToIconString()); iconSize = ImGui.CalcTextSize(FontAwesomeIcon.PersonCirclePlus.ToIconString());
float uidStartX = 25f; float contentWidth = ImGui.GetWindowContentRegionMax().X - ImGui.GetWindowContentRegionMin().X;
float uidStartX = (contentWidth - uidTextSize.X) / 2f;
float cursorY = ImGui.GetCursorPosY(); float cursorY = ImGui.GetCursorPosY();
ImGui.SetCursorPosY(cursorY);
ImGui.SetCursorPosX(uidStartX);
bool headerItemClicked;
using (_uiSharedService.UidFont.Push())
{
if (useVanityColors)
{
var seString = SeStringUtils.BuildFormattedPlayerName(uidText, vanityTextColor, vanityGlowColor);
var cursorPos = ImGui.GetCursorScreenPos();
var targetFontSize = ImGui.GetFontSize();
var font = ImGui.GetFont();
SeStringUtils.RenderSeStringWithHitbox(seString, cursorPos, targetFontSize ,font , "uid-header");
}
else
{
ImGui.TextColored(uidColor, uidText);
}
}
// Get the actual rendered text rect for proper icon alignment
var uidTextRect = ImGui.GetItemRectMax() - ImGui.GetItemRectMin();
var uidTextRectMin = ImGui.GetItemRectMin();
var uidTextHovered = ImGui.IsItemHovered();
headerItemClicked = ImGui.IsItemClicked();
// Track position for icons next to UID text
// Use uidTextSize.Y (actual font height) for vertical centering, not hitbox height
float nextIconX = uidTextRectMin.X + uidTextRect.X + 10f;
float iconYOffset = (uidTextSize.Y - iconSize.Y) * 0.5f;
float textVerticalOffset = (uidTextRect.Y - uidTextSize.Y) * 0.5f;
var buttonSize = new Vector2(iconSize.X, uidTextSize.Y);
if (_configService.Current.BroadcastEnabled && _apiController.IsConnected) if (_configService.Current.BroadcastEnabled && _apiController.IsConnected)
{ {
ImGui.SetCursorScreenPos(new Vector2(nextIconX, uidTextRectMin.Y + textVerticalOffset)); float iconYOffset = (uidTextSize.Y - iconSize.Y) * 0.5f;
var buttonSize = new Vector2(iconSize.X, uidTextSize.Y);
ImGui.SetCursorPos(new Vector2(ImGui.GetStyle().ItemSpacing.X + 5f, cursorY));
ImGui.InvisibleButton("BroadcastIcon", buttonSize); ImGui.InvisibleButton("BroadcastIcon", buttonSize);
var iconPos = ImGui.GetItemRectMin() + new Vector2(0f, iconYOffset); var iconPos = ImGui.GetItemRectMin() + new Vector2(0f, iconYOffset);
using (_uiSharedService.IconFont.Push()) using (_uiSharedService.IconFont.Push())
ImGui.GetWindowDrawList().AddText(iconPos, ImGui.GetColorU32(UIColors.Get("LightlessGreen")), FontAwesomeIcon.Wifi.ToIconString()); ImGui.GetWindowDrawList().AddText(iconPos, ImGui.GetColorU32(UIColors.Get("LightlessGreen")), FontAwesomeIcon.PersonCirclePlus.ToIconString());
nextIconX = ImGui.GetItemRectMax().X + 6f;
if (ImGui.IsItemHovered()) if (ImGui.IsItemHovered())
@@ -596,8 +618,50 @@ public class CompactUi : WindowMediatorSubscriberBase
if (ImGui.IsItemClicked()) if (ImGui.IsItemClicked())
_lightlessMediator.Publish(new UiToggleMessage(typeof(LightFinderUI))); _lightlessMediator.Publish(new UiToggleMessage(typeof(LightFinderUI)));
} }
// Warning threshold icon (next to lightfinder or UID text) ImGui.SetCursorPosY(cursorY);
ImGui.SetCursorPosX(uidStartX);
bool headerItemClicked;
using (_uiSharedService.UidFont.Push())
{
if (useVanityColors)
{
var seString = SeStringUtils.BuildFormattedPlayerName(uidText, vanityTextColor, vanityGlowColor);
var cursorPos = ImGui.GetCursorScreenPos();
var targetFontSize = ImGui.GetFontSize();
var font = ImGui.GetFont();
SeStringUtils.RenderSeStringWithHitbox(seString, cursorPos, targetFontSize ,font , "uid-header");
}
else
{
ImGui.TextColored(uidColor, uidText);
}
}
if (ImGui.IsItemHovered())
{
var padding = new Vector2(35f * ImGuiHelpers.GlobalScale);
Selune.RegisterHighlight(
ImGui.GetItemRectMin() - padding,
ImGui.GetItemRectMax() + padding,
SeluneHighlightMode.Point,
exactSize: true,
clipToElement: true,
clipPadding: padding,
highlightColorOverride: vanityGlowColor,
highlightAlphaOverride: 0.05f);
}
headerItemClicked = ImGui.IsItemClicked();
if (headerItemClicked)
{
ImGui.SetClipboardText(uidText);
}
UiSharedService.AttachToolTip("Click to copy");
if (_apiController.ServerState is ServerState.Connected && analysisSummary.HasData) if (_apiController.ServerState is ServerState.Connected && analysisSummary.HasData)
{ {
var objectSummary = analysisSummary.Objects.Values.FirstOrDefault(summary => summary.HasEntries); var objectSummary = analysisSummary.Objects.Values.FirstOrDefault(summary => summary.HasEntries);
@@ -611,30 +675,24 @@ public class CompactUi : WindowMediatorSubscriberBase
if ((isOverTriHold || isOverVRAMUsage) && _playerPerformanceConfig.Current.WarnOnExceedingThresholds) if ((isOverTriHold || isOverVRAMUsage) && _playerPerformanceConfig.Current.WarnOnExceedingThresholds)
{ {
ImGui.SetCursorScreenPos(new Vector2(nextIconX, uidTextRectMin.Y + textVerticalOffset)); ImGui.SameLine();
ImGui.SetCursorPosY(cursorY + 15f);
ImGui.InvisibleButton("WarningThresholdIcon", buttonSize); _uiSharedService.IconText(FontAwesomeIcon.ExclamationTriangle, UIColors.Get("LightlessYellow"));
var warningIconPos = ImGui.GetItemRectMin() + new Vector2(0f, iconYOffset);
using (_uiSharedService.IconFont.Push())
ImGui.GetWindowDrawList().AddText(warningIconPos, ImGui.GetColorU32(UIColors.Get("LightlessYellow")), FontAwesomeIcon.ExclamationTriangle.ToIconString());
if (ImGui.IsItemHovered()) string warningMessage = "";
if (isOverTriHold)
{ {
string warningMessage = ""; warningMessage += $"You exceed your own triangles threshold by " +
if (isOverTriHold) $"{actualTriCount - _playerPerformanceConfig.Current.TrisWarningThresholdThousands * 1000} triangles.";
{ warningMessage += Environment.NewLine;
warningMessage += $"You exceed your own triangles threshold by " +
$"{actualTriCount - _playerPerformanceConfig.Current.TrisWarningThresholdThousands * 1000} triangles.";
warningMessage += Environment.NewLine;
}
if (isOverVRAMUsage)
{
warningMessage += $"You exceed your own VRAM threshold by " +
$"{UiSharedService.ByteToString(actualVramUsage - (_playerPerformanceConfig.Current.VRAMSizeWarningThresholdMiB * 1024 * 1024))}.";
}
UiSharedService.AttachToolTip(warningMessage);
} }
if (isOverVRAMUsage)
{
warningMessage += $"You exceed your own VRAM threshold by " +
$"{UiSharedService.ByteToString(actualVramUsage - (_playerPerformanceConfig.Current.VRAMSizeWarningThresholdMiB * 1024 * 1024))}.";
}
UiSharedService.AttachToolTip(warningMessage);
if (ImGui.IsItemClicked()) if (ImGui.IsItemClicked())
{ {
_lightlessMediator.Publish(new UiToggleMessage(typeof(DataAnalysisUi))); _lightlessMediator.Publish(new UiToggleMessage(typeof(DataAnalysisUi)));
@@ -643,34 +701,6 @@ public class CompactUi : WindowMediatorSubscriberBase
} }
} }
if (uidTextHovered)
{
var padding = new Vector2(35f * ImGuiHelpers.GlobalScale);
Selune.RegisterHighlight(
uidTextRectMin - padding,
uidTextRectMin + uidTextRect + padding,
SeluneHighlightMode.Point,
exactSize: true,
clipToElement: true,
clipPadding: padding,
highlightColorOverride: vanityGlowColor,
highlightAlphaOverride: 0.05f);
ImGui.SetTooltip("Click to copy");
}
if (headerItemClicked)
{
ImGui.SetClipboardText(uidText);
}
// Connect/Disconnect button next to big UID (use screen pos to avoid affecting layout)
DrawConnectButton(uidTextRectMin.Y + textVerticalOffset, uidTextSize.Y);
// Add spacing below the big UID
ImGuiHelpers.ScaledDummy(5f);
if (_apiController.ServerState is ServerState.Connected) if (_apiController.ServerState is ServerState.Connected)
{ {
if (headerItemClicked) if (headerItemClicked)
@@ -678,12 +708,10 @@ public class CompactUi : WindowMediatorSubscriberBase
ImGui.SetClipboardText(_apiController.DisplayName); ImGui.SetClipboardText(_apiController.DisplayName);
} }
// Only show smaller UID line if DisplayName differs from UID (custom vanity name) if (!string.Equals(_apiController.DisplayName, _apiController.UID, StringComparison.Ordinal))
bool hasCustomName = !string.Equals(_apiController.DisplayName, _apiController.UID, StringComparison.OrdinalIgnoreCase);
if (hasCustomName)
{ {
ImGui.SetCursorPosX(uidStartX); var origTextSize = ImGui.CalcTextSize(_apiController.UID);
ImGui.SetCursorPosX((ImGui.GetWindowContentRegionMax().X - ImGui.GetWindowContentRegionMin().X) / 2 - (origTextSize.X / 2));
if (useVanityColors) if (useVanityColors)
{ {
@@ -718,88 +746,14 @@ public class CompactUi : WindowMediatorSubscriberBase
{ {
ImGui.SetClipboardText(_apiController.UID); ImGui.SetClipboardText(_apiController.UID);
} }
// Users Online on same line as smaller UID (with separator)
ImGui.SameLine();
ImGui.AlignTextToFramePadding();
ImGui.TextUnformatted("|");
ImGui.SameLine();
ImGui.TextColored(UIColors.Get("LightlessGreen"), _apiController.OnlineUsers.ToString(CultureInfo.InvariantCulture));
ImGui.SameLine();
ImGui.TextUnformatted("Users Online");
}
else
{
// No custom name - just show Users Online aligned to uidStartX
ImGui.SetCursorPosX(uidStartX);
ImGui.TextColored(UIColors.Get("LightlessGreen"), _apiController.OnlineUsers.ToString(CultureInfo.InvariantCulture));
ImGui.SameLine();
ImGui.TextUnformatted("Users Online");
} }
} }
else else
{ {
ImGui.SetCursorPosX(uidStartX);
UiSharedService.ColorTextWrapped(_apiController.ServerState.GetServerError(_apiController.AuthFailureMessage), uidColor); UiSharedService.ColorTextWrapped(_apiController.ServerState.GetServerError(_apiController.AuthFailureMessage), uidColor);
} }
} }
private void DrawConnectButton(float screenY, float textHeight)
{
var buttonSize = _uiSharedService.GetIconButtonSize(FontAwesomeIcon.Link);
bool isConnectingOrConnected = _apiController.ServerState is ServerState.Connected or ServerState.Connecting or ServerState.Reconnecting;
var color = UiSharedService.GetBoolColor(!isConnectingOrConnected);
var connectedIcon = isConnectingOrConnected ? FontAwesomeIcon.Unlink : FontAwesomeIcon.Link;
// Position on right side, vertically centered with text
if (_apiController.ServerState is not (ServerState.Reconnecting or ServerState.Disconnecting))
{
var windowPos = ImGui.GetWindowPos();
var screenX = windowPos.X + UiSharedService.GetWindowContentRegionWidth() - buttonSize.X - 13f;
var yOffset = (textHeight - buttonSize.Y) * 0.5f;
ImGui.SetCursorScreenPos(new Vector2(screenX, screenY + yOffset));
using (ImRaii.PushColor(ImGuiCol.Text, color))
using (ImRaii.PushColor(ImGuiCol.Button, ImGui.ColorConvertFloat4ToU32(new(0, 0, 0, 0))))
{
if (_uiSharedService.IconButton(connectedIcon, buttonSize.Y))
{
if (isConnectingOrConnected && !_serverManager.CurrentServer.FullPause)
{
_serverManager.CurrentServer.FullPause = true;
_serverManager.Save();
}
else if (!isConnectingOrConnected && _serverManager.CurrentServer.FullPause)
{
_serverManager.CurrentServer.FullPause = false;
_serverManager.Save();
}
_ = _apiController.CreateConnectionsAsync();
}
}
if (ImGui.IsItemHovered(ImGuiHoveredFlags.AllowWhenBlockedByActiveItem) || ImGui.IsItemActive())
{
Selune.RegisterHighlight(
ImGui.GetItemRectMin(),
ImGui.GetItemRectMax(),
SeluneHighlightMode.Both,
borderOnly: true,
borderThicknessOverride: ConnectButtonHighlightThickness,
exactSize: true,
clipToElement: true,
roundingOverride: ImGui.GetStyle().FrameRounding);
}
UiSharedService.AttachToolTip(isConnectingOrConnected ? "Disconnect from " + _serverManager.CurrentServer.ServerName : "Connect to " + _serverManager.CurrentServer.ServerName);
}
}
#endregion
#region Folder Building
private IEnumerable<IDrawFolder> DrawFolders private IEnumerable<IDrawFolder> DrawFolders
{ {
get get
@@ -889,16 +843,12 @@ public class CompactUi : WindowMediatorSubscriberBase
//Filter of not grouped/foldered and offline pairs //Filter of not grouped/foldered and offline pairs
var allOnlineNotTaggedPairs = SortEntries(allEntries.Where(FilterNotTaggedUsers)); var allOnlineNotTaggedPairs = SortEntries(allEntries.Where(FilterNotTaggedUsers));
if (allOnlineNotTaggedPairs.Count > 0 && _configService.Current.ShowOfflineUsersSeparately) { var onlineNotTaggedPairs = SortEntries(filteredEntries.Where(e => FilterNotTaggedUsers(e) && FilterOnlineOrPausedSelf(e)));
var filteredOnlineEntries = SortOnlineEntries(filteredEntries.Where(e => FilterNotTaggedUsers(e) && FilterOnlineOrPausedSelf(e)));
if (allOnlineNotTaggedPairs.Count > 0)
{
drawFolders.Add(_drawEntityFactory.CreateTagFolder( drawFolders.Add(_drawEntityFactory.CreateTagFolder(
TagHandler.CustomOnlineTag, _configService.Current.ShowOfflineUsersSeparately ? TagHandler.CustomOnlineTag : TagHandler.CustomAllTag,
filteredOnlineEntries,
allOnlineNotTaggedPairs));
} else if (allOnlineNotTaggedPairs.Count > 0 && !_configService.Current.ShowOfflineUsersSeparately) {
var onlineNotTaggedPairs = SortEntries(filteredEntries.Where(FilterNotTaggedUsers));
drawFolders.Add(_drawEntityFactory.CreateTagFolder(
TagHandler.CustomAllTag,
onlineNotTaggedPairs, onlineNotTaggedPairs,
allOnlineNotTaggedPairs)); allOnlineNotTaggedPairs));
} }
@@ -935,11 +885,7 @@ public class CompactUi : WindowMediatorSubscriberBase
} }
} }
#endregion private bool PassesFilter(PairUiEntry entry, string filter)
#region Filtering & Sorting
private static bool PassesFilter(PairUiEntry entry, string filter)
{ {
if (string.IsNullOrEmpty(filter)) return true; if (string.IsNullOrEmpty(filter)) return true;
@@ -994,24 +940,12 @@ public class CompactUi : WindowMediatorSubscriberBase
VisiblePairSortMode.VramUsage => SortVisibleByMetric(entryList, e => e.LastAppliedApproximateVramBytes), VisiblePairSortMode.VramUsage => SortVisibleByMetric(entryList, e => e.LastAppliedApproximateVramBytes),
VisiblePairSortMode.EffectiveVramUsage => SortVisibleByMetric(entryList, e => e.LastAppliedApproximateEffectiveVramBytes), VisiblePairSortMode.EffectiveVramUsage => SortVisibleByMetric(entryList, e => e.LastAppliedApproximateEffectiveVramBytes),
VisiblePairSortMode.TriangleCount => SortVisibleByMetric(entryList, e => e.LastAppliedDataTris), VisiblePairSortMode.TriangleCount => SortVisibleByMetric(entryList, e => e.LastAppliedDataTris),
VisiblePairSortMode.EffectiveTriangleCount => SortVisibleByMetric(entryList, e => e.LastAppliedApproximateEffectiveTris),
VisiblePairSortMode.Alphabetical => [.. entryList.OrderBy(e => AlphabeticalSortKey(e), StringComparer.OrdinalIgnoreCase)], VisiblePairSortMode.Alphabetical => [.. entryList.OrderBy(e => AlphabeticalSortKey(e), StringComparer.OrdinalIgnoreCase)],
VisiblePairSortMode.PreferredDirectPairs => SortVisibleByPreferred(entryList), VisiblePairSortMode.PreferredDirectPairs => SortVisibleByPreferred(entryList),
_ => SortEntries(entryList), _ => SortEntries(entryList),
}; };
} }
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) private ImmutableList<PairUiEntry> SortVisibleByMetric(IEnumerable<PairUiEntry> entries, Func<PairUiEntry, long> selector)
{ {
return [.. entries return [.. entries
@@ -1083,11 +1017,10 @@ public class CompactUi : WindowMediatorSubscriberBase
return SortGroupEntries(entries, group); return SortGroupEntries(entries, group);
} }
#endregion private void UiSharedService_GposeEnd()
{
#region GPose Handlers IsOpen = _wasOpen;
}
private void UiSharedService_GposeEnd() => IsOpen = _wasOpen;
private void UiSharedService_GposeStart() private void UiSharedService_GposeStart()
{ {
@@ -1095,10 +1028,6 @@ public class CompactUi : WindowMediatorSubscriberBase
IsOpen = false; IsOpen = false;
} }
#endregion
#region Focus Tracking
private void RegisterFocusCharacter(Pair pair) private void RegisterFocusCharacter(Pair pair)
{ {
_pendingFocusPair = pair; _pendingFocusPair = pair;
@@ -1144,16 +1073,4 @@ public class CompactUi : WindowMediatorSubscriberBase
_pendingFocusPair = null; _pendingFocusPair = null;
_pendingFocusFrame = -1; _pendingFocusFrame = -1;
} }
#endregion
#region Helper Types
[StructLayout(LayoutKind.Auto)]
private readonly record struct DownloadSummary(int TotalFiles, int TransferredFiles, long TransferredBytes, long TotalBytes)
{
public bool HasDownloads => TotalFiles > 0 || TotalBytes > 0;
}
#endregion
} }

View File

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

View File

@@ -169,16 +169,11 @@ public class DrawFolderTag : DrawFolderBase
protected override float DrawRightSide(float currentRightSideX) protected override float DrawRightSide(float currentRightSideX)
{ {
if (string.Equals(_id, TagHandler.CustomVisibleTag, StringComparison.Ordinal)) if (_id == TagHandler.CustomVisibleTag)
{ {
return DrawVisibleFilter(currentRightSideX); return DrawVisibleFilter(currentRightSideX);
} }
if (string.Equals(_id, TagHandler.CustomOnlineTag, StringComparison.Ordinal))
{
return DrawOnlineFilter(currentRightSideX);
}
if (!RenderPause) if (!RenderPause)
{ {
return currentRightSideX; return currentRightSideX;
@@ -259,7 +254,7 @@ public class DrawFolderTag : DrawFolderBase
foreach (VisiblePairSortMode mode in Enum.GetValues<VisiblePairSortMode>()) foreach (VisiblePairSortMode mode in Enum.GetValues<VisiblePairSortMode>())
{ {
var selected = _configService.Current.VisiblePairSortMode == mode; var selected = _configService.Current.VisiblePairSortMode == mode;
if (ImGui.MenuItem(GetSortVisibleLabel(mode), string.Empty, selected)) if (ImGui.MenuItem(GetSortLabel(mode), string.Empty, selected))
{ {
if (!selected) if (!selected)
{ {
@@ -278,63 +273,13 @@ public class DrawFolderTag : DrawFolderBase
return buttonStart - spacingX; return buttonStart - spacingX;
} }
private float DrawOnlineFilter(float currentRightSideX) private static string GetSortLabel(VisiblePairSortMode mode) => mode switch
{
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
{ {
VisiblePairSortMode.Alphabetical => "Alphabetical", VisiblePairSortMode.Alphabetical => "Alphabetical",
VisiblePairSortMode.VramUsage => "VRAM usage (descending)", VisiblePairSortMode.VramUsage => "VRAM usage (descending)",
VisiblePairSortMode.EffectiveVramUsage => "Effective VRAM usage (descending)", VisiblePairSortMode.EffectiveVramUsage => "Effective VRAM usage (descending)",
VisiblePairSortMode.TriangleCount => "Triangle count (descending)", VisiblePairSortMode.TriangleCount => "Triangle count (descending)",
VisiblePairSortMode.EffectiveTriangleCount => "Effective triangle count (descending)",
VisiblePairSortMode.PreferredDirectPairs => "Preferred permissions & Direct pairs", VisiblePairSortMode.PreferredDirectPairs => "Preferred permissions & Direct pairs",
_ => "Default", _ => "Default",
}; };
private static string GetSortOnlineLabel(OnlinePairSortMode mode) => mode switch
{
OnlinePairSortMode.Alphabetical => "Alphabetical",
OnlinePairSortMode.PreferredDirectPairs => "Preferred permissions & Direct pairs",
_ => "Default",
};
} }

View File

@@ -37,7 +37,6 @@ public class DrawUserPair
private readonly UiSharedService _uiSharedService; private readonly UiSharedService _uiSharedService;
private readonly PlayerPerformanceConfigService _performanceConfigService; private readonly PlayerPerformanceConfigService _performanceConfigService;
private readonly LightlessConfigService _configService; private readonly LightlessConfigService _configService;
private readonly LocationShareService _locationShareService;
private readonly CharaDataManager _charaDataManager; private readonly CharaDataManager _charaDataManager;
private readonly PairLedger _pairLedger; private readonly PairLedger _pairLedger;
private float _menuWidth = -1; private float _menuWidth = -1;
@@ -58,7 +57,6 @@ public class DrawUserPair
UiSharedService uiSharedService, UiSharedService uiSharedService,
PlayerPerformanceConfigService performanceConfigService, PlayerPerformanceConfigService performanceConfigService,
LightlessConfigService configService, LightlessConfigService configService,
LocationShareService locationShareService,
CharaDataManager charaDataManager, CharaDataManager charaDataManager,
PairLedger pairLedger) PairLedger pairLedger)
{ {
@@ -76,7 +74,6 @@ public class DrawUserPair
_uiSharedService = uiSharedService; _uiSharedService = uiSharedService;
_performanceConfigService = performanceConfigService; _performanceConfigService = performanceConfigService;
_configService = configService; _configService = configService;
_locationShareService = locationShareService;
_charaDataManager = charaDataManager; _charaDataManager = charaDataManager;
_pairLedger = pairLedger; _pairLedger = pairLedger;
} }
@@ -136,26 +133,6 @@ public class DrawUserPair
UiSharedService.AttachToolTip("This reapplies the last received character data to this character"); UiSharedService.AttachToolTip("This reapplies the last received character data to this character");
} }
var isPaused = _pair.UserPair!.OwnPermissions.IsPaused();
if (!isPaused)
{
if (_uiSharedService.IconTextButton(FontAwesomeIcon.Pause, "Toggle Pause State", _menuWidth, true))
{
_ = _apiController.PauseAsync(_pair.UserData);
ImGui.CloseCurrentPopup();
}
UiSharedService.AttachToolTip("Pauses syncing with this user.");
}
else
{
if (_uiSharedService.IconTextButton(FontAwesomeIcon.Play, "Toggle Unpause State", _menuWidth, true))
{
_ = _apiController.UnpauseAsync(_pair.UserData);
ImGui.CloseCurrentPopup();
}
UiSharedService.AttachToolTip("Resumes syncing with this user.");
}
if (_uiSharedService.IconTextButton(FontAwesomeIcon.PlayCircle, "Cycle pause state", _menuWidth, true)) if (_uiSharedService.IconTextButton(FontAwesomeIcon.PlayCircle, "Cycle pause state", _menuWidth, true))
{ {
_ = _apiController.CyclePauseAsync(_pair); _ = _apiController.CyclePauseAsync(_pair);
@@ -219,48 +196,6 @@ public class DrawUserPair
_ = _apiController.UserSetPairPermissions(new UserPermissionsDto(_pair.UserData, permissions)); _ = _apiController.UserSetPairPermissions(new UserPermissionsDto(_pair.UserData, permissions));
} }
UiSharedService.AttachToolTip("Changes VFX sync permissions with this user." + (individual ? individualText : string.Empty)); UiSharedService.AttachToolTip("Changes VFX sync permissions with this user." + (individual ? individualText : string.Empty));
ImGui.SetCursorPosX(10f);
_uiSharedService.IconText(FontAwesomeIcon.Globe);
ImGui.SameLine();
if (ImGui.BeginMenu("Toggle Location sharing"))
{
if (ImGui.MenuItem("Share for 30 Mins"))
{
_ = ToggleLocationSharing([_pair.UserData.UID], DateTimeOffset.UtcNow.AddMinutes(30));
}
if (ImGui.MenuItem("Share for 1 Hour"))
{
_ = ToggleLocationSharing([_pair.UserData.UID], DateTimeOffset.UtcNow.AddHours(1));
}
if (ImGui.MenuItem("Share for 3 Hours"))
{
_ = ToggleLocationSharing([_pair.UserData.UID], DateTimeOffset.UtcNow.AddHours(3));
}
if (ImGui.MenuItem("Share until manually stop"))
{
_ = ToggleLocationSharing([_pair.UserData.UID], DateTimeOffset.MaxValue);
}
ImGui.Separator();
if (ImGui.MenuItem("Stop Sharing"))
{
_ = ToggleLocationSharing([_pair.UserData.UID], DateTimeOffset.MinValue);
}
ImGui.EndMenu();
}
}
private async Task ToggleLocationSharing(List<string> users, DateTimeOffset expireAt)
{
var updated = await _apiController.ToggleLocationSharing(new LocationSharingToggleDto(users, expireAt)).ConfigureAwait(false);
if (updated)
{
_locationShareService.UpdateSharingStatus(users, expireAt);
}
} }
private void DrawIndividualMenu() private void DrawIndividualMenu()
@@ -429,7 +364,6 @@ public class DrawUserPair
_pair.LastAppliedApproximateVRAMBytes, _pair.LastAppliedApproximateVRAMBytes,
_pair.LastAppliedApproximateEffectiveVRAMBytes, _pair.LastAppliedApproximateEffectiveVRAMBytes,
_pair.LastAppliedDataTris, _pair.LastAppliedDataTris,
_pair.LastAppliedApproximateEffectiveTris,
_pair.IsPaired, _pair.IsPaired,
groupDisplays is null ? ImmutableArray<string>.Empty : ImmutableArray.CreateRange(groupDisplays)); groupDisplays is null ? ImmutableArray<string>.Empty : ImmutableArray.CreateRange(groupDisplays));
@@ -445,8 +379,6 @@ public class DrawUserPair
private static string BuildTooltip(in TooltipSnapshot snapshot) private static string BuildTooltip(in TooltipSnapshot snapshot)
{ {
var builder = new StringBuilder(256); var builder = new StringBuilder(256);
static string FormatTriangles(long count) =>
count > 1000 ? (count / 1000d).ToString("0.0'k'") : count.ToString();
if (snapshot.IsPaused) if (snapshot.IsPaused)
{ {
@@ -513,13 +445,9 @@ public class DrawUserPair
{ {
builder.Append(Environment.NewLine); builder.Append(Environment.NewLine);
builder.Append("Approx. Triangle Count (excl. Vanilla): "); builder.Append("Approx. Triangle Count (excl. Vanilla): ");
builder.Append(FormatTriangles(snapshot.LastAppliedDataTris)); builder.Append(snapshot.LastAppliedDataTris > 1000
if (snapshot.LastAppliedApproximateEffectiveTris >= 0) ? (snapshot.LastAppliedDataTris / 1000d).ToString("0.0'k'")
{ : snapshot.LastAppliedDataTris);
builder.Append(" (Effective: ");
builder.Append(FormatTriangles(snapshot.LastAppliedApproximateEffectiveTris));
builder.Append(')');
}
} }
} }
@@ -551,12 +479,11 @@ public class DrawUserPair
long LastAppliedApproximateVRAMBytes, long LastAppliedApproximateVRAMBytes,
long LastAppliedApproximateEffectiveVRAMBytes, long LastAppliedApproximateEffectiveVRAMBytes,
long LastAppliedDataTris, long LastAppliedDataTris,
long LastAppliedApproximateEffectiveTris,
bool IsPaired, bool IsPaired,
ImmutableArray<string> GroupDisplays) ImmutableArray<string> GroupDisplays)
{ {
public static TooltipSnapshot Empty { get; } = public static TooltipSnapshot Empty { get; } =
new(false, false, false, IndividualPairStatus.None, string.Empty, string.Empty, -1, -1, -1, -1, -1, false, ImmutableArray<string>.Empty); new(false, false, false, IndividualPairStatus.None, string.Empty, string.Empty, -1, -1, -1, -1, false, ImmutableArray<string>.Empty);
} }
private void DrawPairedClientMenu() private void DrawPairedClientMenu()
@@ -627,71 +554,6 @@ public class DrawUserPair
var individualVFXDisabled = (_pair.UserPair?.OwnPermissions.IsDisableVFX() ?? false) || (_pair.UserPair?.OtherPermissions.IsDisableVFX() ?? false); var individualVFXDisabled = (_pair.UserPair?.OwnPermissions.IsDisableVFX() ?? false) || (_pair.UserPair?.OtherPermissions.IsDisableVFX() ?? false);
var individualIsSticky = _pair.UserPair!.OwnPermissions.IsSticky(); var individualIsSticky = _pair.UserPair!.OwnPermissions.IsSticky();
var individualIcon = individualIsSticky ? FontAwesomeIcon.ArrowCircleUp : FontAwesomeIcon.InfoCircle; var individualIcon = individualIsSticky ? FontAwesomeIcon.ArrowCircleUp : FontAwesomeIcon.InfoCircle;
var shareLocationIcon = FontAwesomeIcon.Globe;
var location = _locationShareService.GetUserLocation(_pair.UserPair!.User.UID);
var shareLocation = !string.IsNullOrEmpty(location);
var expireAt = _locationShareService.GetSharingStatus(_pair.UserPair!.User.UID);
var shareLocationToOther = expireAt > DateTimeOffset.UtcNow;
var shareColor = shareLocation switch
{
true when shareLocationToOther => UIColors.Get("LightlessGreen"),
true when !shareLocationToOther => UIColors.Get("LightlessBlue"),
_ => UIColors.Get("LightlessYellow"),
};
if (shareLocation || shareLocationToOther)
{
currentRightSide -= (_uiSharedService.GetIconSize(shareLocationIcon).X + spacingX);
ImGui.SameLine(currentRightSide);
using (ImRaii.PushColor(ImGuiCol.Text, shareColor, shareLocation || shareLocationToOther))
_uiSharedService.IconText(shareLocationIcon);
if (ImGui.IsItemHovered())
{
ImGui.BeginTooltip();
if (_pair.IsOnline)
{
if (shareLocation)
{
if (!string.IsNullOrEmpty(location))
{
_uiSharedService.IconText(FontAwesomeIcon.LocationArrow);
ImGui.SameLine();
ImGui.TextUnformatted(location);
}
else
{
ImGui.TextUnformatted("Location info not updated, reconnect or wait for update.");
}
}
else
{
ImGui.TextUnformatted("NOT Sharing location with you. o(TヘTo)");
}
}
else
{
ImGui.TextUnformatted("User not online. (´・ω・`)?");
}
ImGui.Separator();
if (shareLocationToOther)
{
ImGui.TextUnformatted("Sharing your location. ヾ(•ω•`)o");
if (expireAt != DateTimeOffset.MaxValue)
{
ImGui.TextUnformatted("Expires at " + expireAt.ToLocalTime().ToString("g"));
}
}
else
{
ImGui.TextUnformatted("NOT sharing your location.  ̄へ ̄");
}
ImGui.EndTooltip();
}
}
if (individualAnimDisabled || individualSoundsDisabled || individualVFXDisabled || individualIsSticky) if (individualAnimDisabled || individualSoundsDisabled || individualVFXDisabled || individualIsSticky)
{ {
@@ -875,19 +737,14 @@ public class DrawUserPair
} }
UiSharedService.AttachToolTip("Hold CTRL and click to remove user " + (_pair.UserData.AliasOrUID) + " from Syncshell"); UiSharedService.AttachToolTip("Hold CTRL and click to remove user " + (_pair.UserData.AliasOrUID) + " from Syncshell");
var banEnabled = UiSharedService.CtrlPressed(); if (_uiSharedService.IconTextButton(FontAwesomeIcon.UserSlash, "Ban User", _menuWidth, true))
var banLabel = banEnabled ? "Ban user" : "Ban user (Hold CTRL)";
if (_uiSharedService.IconTextButton(FontAwesomeIcon.UserSlash, banLabel, _menuWidth, true) && banEnabled)
{ {
_mediator.Publish(new OpenBanUserPopupMessage(_pair, group)); _mediator.Publish(new OpenBanUserPopupMessage(_pair, group));
ImGui.CloseCurrentPopup(); 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) if (showOwnerActions)

File diff suppressed because it is too large Load Diff

View File

@@ -17,7 +17,7 @@ namespace LightlessSync.UI;
public class DownloadUi : WindowMediatorSubscriberBase public class DownloadUi : WindowMediatorSubscriberBase
{ {
private readonly LightlessConfigService _configService; private readonly LightlessConfigService _configService;
private readonly ConcurrentDictionary<GameObjectHandler, IReadOnlyDictionary<string, FileDownloadStatus>> _currentDownloads = new(); private readonly ConcurrentDictionary<GameObjectHandler, Dictionary<string, FileDownloadStatus>> _currentDownloads = new();
private readonly DalamudUtilService _dalamudUtilService; private readonly DalamudUtilService _dalamudUtilService;
private readonly FileUploadManager _fileTransferManager; private readonly FileUploadManager _fileTransferManager;
private readonly UiSharedService _uiShared; private readonly UiSharedService _uiShared;
@@ -25,8 +25,6 @@ public class DownloadUi : WindowMediatorSubscriberBase
private readonly ConcurrentDictionary<GameObjectHandler, bool> _uploadingPlayers = new(); private readonly ConcurrentDictionary<GameObjectHandler, bool> _uploadingPlayers = new();
private readonly Dictionary<GameObjectHandler, Vector2> _smoothed = []; private readonly Dictionary<GameObjectHandler, Vector2> _smoothed = [];
private readonly Dictionary<GameObjectHandler, DownloadSpeedTracker> _downloadSpeeds = []; private readonly Dictionary<GameObjectHandler, DownloadSpeedTracker> _downloadSpeeds = [];
private readonly Dictionary<GameObjectHandler, (int TotalFiles, long TotalBytes)> _downloadInitialTotals = [];
private byte _transferBoxTransparency = 100; private byte _transferBoxTransparency = 100;
private bool _notificationDismissed = true; private bool _notificationDismissed = true;
@@ -65,15 +63,9 @@ public class DownloadUi : WindowMediatorSubscriberBase
IsOpen = true; IsOpen = true;
Mediator.Subscribe<DownloadStartedMessage>(this, msg => Mediator.Subscribe<DownloadStartedMessage>(this, (msg) =>
{ {
_currentDownloads[msg.DownloadId] = msg.DownloadStatus; _currentDownloads[msg.DownloadId] = msg.DownloadStatus;
var snap = msg.DownloadStatus.ToArray();
var totalFiles = snap.Sum(kv => kv.Value?.TotalFiles ?? 0);
var totalBytes = snap.Sum(kv => kv.Value?.TotalBytes ?? 0);
_downloadInitialTotals[msg.DownloadId] = (totalFiles, totalBytes);
_notificationDismissed = false; _notificationDismissed = false;
}); });
Mediator.Subscribe<DownloadFinishedMessage>(this, (msg) => Mediator.Subscribe<DownloadFinishedMessage>(this, (msg) =>
@@ -81,7 +73,7 @@ public class DownloadUi : WindowMediatorSubscriberBase
_currentDownloads.TryRemove(msg.DownloadId, out _); _currentDownloads.TryRemove(msg.DownloadId, out _);
// Dismiss notification if all downloads are complete // Dismiss notification if all downloads are complete
if (_currentDownloads.IsEmpty && !_notificationDismissed) if (!_currentDownloads.Any() && !_notificationDismissed)
{ {
Mediator.Publish(new LightlessNotificationDismissMessage("pair_download_progress")); Mediator.Publish(new LightlessNotificationDismissMessage("pair_download_progress"));
_notificationDismissed = true; _notificationDismissed = true;
@@ -172,25 +164,9 @@ public class DownloadUi : WindowMediatorSubscriberBase
const float rounding = 6f; const float rounding = 6f;
var shadowOffset = new Vector2(2, 2); var shadowOffset = new Vector2(2, 2);
List<KeyValuePair<GameObjectHandler, IReadOnlyDictionary<string, FileDownloadStatus>>> transfers; foreach (var transfer in _currentDownloads.ToList())
try
{
transfers = [.. _currentDownloads];
}
catch (ArgumentException)
{
return;
}
foreach (var transfer in transfers)
{ {
var transferKey = transfer.Key; var transferKey = transfer.Key;
// Skip if no valid game object
if (transferKey.GetGameObject() == null)
continue;
var rawPos = _dalamudUtilService.WorldToScreen(transferKey.GetGameObject()); var rawPos = _dalamudUtilService.WorldToScreen(transferKey.GetGameObject());
// If RawPos is zero, remove it from smoothed dictionary // If RawPos is zero, remove it from smoothed dictionary
@@ -214,16 +190,12 @@ public class DownloadUi : WindowMediatorSubscriberBase
var dlQueue = 0; var dlQueue = 0;
var dlProg = 0; var dlProg = 0;
var dlDecomp = 0; var dlDecomp = 0;
var dlComplete = 0;
foreach (var entry in transfer.Value) foreach (var entry in transfer.Value)
{ {
var fileStatus = entry.Value; var fileStatus = entry.Value;
switch (fileStatus.DownloadStatus) switch (fileStatus.DownloadStatus)
{ {
case DownloadStatus.Initializing:
dlQueue++;
break;
case DownloadStatus.WaitingForSlot: case DownloadStatus.WaitingForSlot:
dlSlot++; dlSlot++;
break; break;
@@ -236,20 +208,15 @@ public class DownloadUi : WindowMediatorSubscriberBase
case DownloadStatus.Decompressing: case DownloadStatus.Decompressing:
dlDecomp++; dlDecomp++;
break; break;
case DownloadStatus.Completed:
dlComplete++;
break;
} }
} }
var isAllComplete = dlComplete > 0 && dlProg == 0 && dlDecomp == 0 && dlQueue == 0 && dlSlot == 0;
string statusText; string statusText;
if (dlProg > 0) if (dlProg > 0)
{ {
statusText = "Downloading"; statusText = "Downloading";
} }
else if (dlDecomp > 0) else if (dlDecomp > 0 || (totalBytes > 0 && transferredBytes >= totalBytes))
{ {
statusText = "Decompressing"; statusText = "Decompressing";
} }
@@ -261,10 +228,6 @@ public class DownloadUi : WindowMediatorSubscriberBase
{ {
statusText = "Waiting for slot"; statusText = "Waiting for slot";
} }
else if (isAllComplete)
{
statusText = "Completed";
}
else else
{ {
statusText = "Waiting"; statusText = "Waiting";
@@ -330,7 +293,7 @@ public class DownloadUi : WindowMediatorSubscriberBase
fillPercent = transferredBytes / (double)totalBytes; fillPercent = transferredBytes / (double)totalBytes;
showFill = true; showFill = true;
} }
else if (dlDecomp > 0 || dlComplete > 0 || transferredBytes >= totalBytes) else if (dlDecomp > 0 || transferredBytes >= totalBytes)
{ {
fillPercent = 1.0; fillPercent = 1.0;
showFill = true; showFill = true;
@@ -362,14 +325,10 @@ public class DownloadUi : WindowMediatorSubscriberBase
downloadText = downloadText =
$"{statusText} {UiSharedService.ByteToString(transferredBytes, addSuffix: false)}/{UiSharedService.ByteToString(totalBytes)}"; $"{statusText} {UiSharedService.ByteToString(transferredBytes, addSuffix: false)}/{UiSharedService.ByteToString(totalBytes)}";
} }
else if (dlDecomp > 0) else if ((dlDecomp > 0 || transferredBytes >= totalBytes) && hasValidSize)
{ {
downloadText = "Decompressing"; downloadText = "Decompressing";
} }
else if (isAllComplete)
{
downloadText = "Completed";
}
else else
{ {
// Waiting states // Waiting states
@@ -442,7 +401,6 @@ public class DownloadUi : WindowMediatorSubscriberBase
var totalDlQueue = 0; var totalDlQueue = 0;
var totalDlProg = 0; var totalDlProg = 0;
var totalDlDecomp = 0; var totalDlDecomp = 0;
var totalDlComplete = 0;
var perPlayer = new List<( var perPlayer = new List<(
string Name, string Name,
@@ -454,21 +412,16 @@ public class DownloadUi : WindowMediatorSubscriberBase
int DlSlot, int DlSlot,
int DlQueue, int DlQueue,
int DlProg, int DlProg,
int DlDecomp, int DlDecomp)>();
int DlComplete)>();
foreach (var transfer in _currentDownloads) foreach (var transfer in _currentDownloads)
{ {
var handler = transfer.Key; var handler = transfer.Key;
var statuses = transfer.Value.Values; var statuses = transfer.Value.Values;
var (playerTotalFiles, playerTotalBytes) = _downloadInitialTotals.TryGetValue(handler, out var totals) var playerTotalFiles = statuses.Sum(s => s.TotalFiles);
? totals var playerTransferredFiles = statuses.Sum(s => s.TransferredFiles);
: (statuses.Sum(s => s.TotalFiles), statuses.Sum(s => s.TotalBytes)); var playerTotalBytes = statuses.Sum(s => s.TotalBytes);
var playerTransferredFiles = statuses.Count(s =>
s.DownloadStatus == DownloadStatus.Decompressing ||
s.TransferredBytes >= s.TotalBytes);
var playerTransferredBytes = statuses.Sum(s => s.TransferredBytes); var playerTransferredBytes = statuses.Sum(s => s.TransferredBytes);
totalFiles += playerTotalFiles; totalFiles += playerTotalFiles;
@@ -476,27 +429,25 @@ public class DownloadUi : WindowMediatorSubscriberBase
totalBytes += playerTotalBytes; totalBytes += playerTotalBytes;
transferredBytes += playerTransferredBytes; transferredBytes += playerTransferredBytes;
// per-player W/Q/P/D/C // per-player W/Q/P/D
var playerDlSlot = 0; var playerDlSlot = 0;
var playerDlQueue = 0; var playerDlQueue = 0;
var playerDlProg = 0; var playerDlProg = 0;
var playerDlDecomp = 0; var playerDlDecomp = 0;
var playerDlComplete = 0;
foreach (var entry in transfer.Value) foreach (var entry in transfer.Value)
{ {
var fileStatus = entry.Value; var fileStatus = entry.Value;
switch (fileStatus.DownloadStatus) switch (fileStatus.DownloadStatus)
{ {
case DownloadStatus.Initializing:
case DownloadStatus.WaitingForQueue:
playerDlQueue++;
totalDlQueue++;
break;
case DownloadStatus.WaitingForSlot: case DownloadStatus.WaitingForSlot:
playerDlSlot++; playerDlSlot++;
totalDlSlot++; totalDlSlot++;
break; break;
case DownloadStatus.WaitingForQueue:
playerDlQueue++;
totalDlQueue++;
break;
case DownloadStatus.Downloading: case DownloadStatus.Downloading:
playerDlProg++; playerDlProg++;
totalDlProg++; totalDlProg++;
@@ -505,10 +456,6 @@ public class DownloadUi : WindowMediatorSubscriberBase
playerDlDecomp++; playerDlDecomp++;
totalDlDecomp++; totalDlDecomp++;
break; break;
case DownloadStatus.Completed:
playerDlComplete++;
totalDlComplete++;
break;
} }
} }
@@ -534,8 +481,7 @@ public class DownloadUi : WindowMediatorSubscriberBase
playerDlSlot, playerDlSlot,
playerDlQueue, playerDlQueue,
playerDlProg, playerDlProg,
playerDlDecomp, playerDlDecomp
playerDlComplete
)); ));
} }
@@ -549,12 +495,17 @@ public class DownloadUi : WindowMediatorSubscriberBase
if (totalFiles == 0 || totalBytes == 0) if (totalFiles == 0 || totalBytes == 0)
return; return;
// max speed for per-player bar scale (clamped)
double maxSpeed = perPlayer.Count > 0 ? perPlayer.Max(p => p.SpeedBytesPerSecond) : 0;
if (maxSpeed <= 0)
maxSpeed = 1;
var drawList = ImGui.GetBackgroundDrawList(); var drawList = ImGui.GetBackgroundDrawList();
var windowPos = ImGui.GetWindowPos(); var windowPos = ImGui.GetWindowPos();
// Overall texts // Overall texts
var headerText = var headerText =
$"Downloading {transferredFiles}/{totalFiles} files [W:{totalDlSlot}/Q:{totalDlQueue}/P:{totalDlProg}/D:{totalDlDecomp}/C:{totalDlComplete}]"; $"Downloading {transferredFiles}/{totalFiles} files [W:{totalDlSlot}/Q:{totalDlQueue}/P:{totalDlProg}/D:{totalDlDecomp}]";
var bytesText = var bytesText =
$"{UiSharedService.ByteToString(transferredBytes, addSuffix: false)}/{UiSharedService.ByteToString(totalBytes)}"; $"{UiSharedService.ByteToString(transferredBytes, addSuffix: false)}/{UiSharedService.ByteToString(totalBytes)}";
@@ -577,7 +528,7 @@ public class DownloadUi : WindowMediatorSubscriberBase
foreach (var p in perPlayer) foreach (var p in perPlayer)
{ {
var line = var line =
$"{p.Name} [W:{p.DlSlot}/Q:{p.DlQueue}/P:{p.DlProg}/D:{p.DlDecomp}/C:{p.DlComplete}] {p.TransferredFiles}/{p.TotalFiles}"; $"{p.Name} [W:{p.DlSlot}/Q:{p.DlQueue}/P:{p.DlProg}/D:{p.DlDecomp}] {p.TransferredFiles}/{p.TotalFiles}";
var lineSize = ImGui.CalcTextSize(line); var lineSize = ImGui.CalcTextSize(line);
if (lineSize.X > contentWidth) if (lineSize.X > contentWidth)
@@ -695,7 +646,7 @@ public class DownloadUi : WindowMediatorSubscriberBase
&& p.TransferredBytes > 0; && p.TransferredBytes > 0;
var labelLine = var labelLine =
$"{p.Name} [W:{p.DlSlot}/Q:{p.DlQueue}/P:{p.DlProg}/D:{p.DlDecomp}/C:{p.DlComplete}] {p.TransferredFiles}/{p.TotalFiles}"; $"{p.Name} [W:{p.DlSlot}/Q:{p.DlQueue}/P:{p.DlProg}/D:{p.DlDecomp}] {p.TransferredFiles}/{p.TotalFiles}";
if (!showBar) if (!showBar)
{ {
@@ -754,18 +705,13 @@ public class DownloadUi : WindowMediatorSubscriberBase
// Text inside bar: downloading vs decompressing // Text inside bar: downloading vs decompressing
string barText; string barText;
var isDecompressing = p.DlDecomp > 0; var isDecompressing = p.DlDecomp > 0 && p.TransferredBytes >= p.TotalBytes && p.TotalBytes > 0;
var isAllComplete = p.DlComplete > 0 && p.DlProg == 0 && p.DlDecomp == 0 && p.DlQueue == 0 && p.DlSlot == 0;
if (isDecompressing) if (isDecompressing)
{ {
// Keep bar full, static text showing decompressing // Keep bar full, static text showing decompressing
barText = "Decompressing..."; barText = "Decompressing...";
} }
else if (isAllComplete)
{
barText = "Completed";
}
else else
{ {
var bytesInside = var bytesInside =
@@ -846,7 +792,6 @@ public class DownloadUi : WindowMediatorSubscriberBase
var dlQueue = 0; var dlQueue = 0;
var dlProg = 0; var dlProg = 0;
var dlDecomp = 0; var dlDecomp = 0;
var dlComplete = 0;
long totalBytes = 0; long totalBytes = 0;
long transferredBytes = 0; long transferredBytes = 0;
@@ -856,29 +801,22 @@ public class DownloadUi : WindowMediatorSubscriberBase
var fileStatus = entry.Value; var fileStatus = entry.Value;
switch (fileStatus.DownloadStatus) switch (fileStatus.DownloadStatus)
{ {
case DownloadStatus.Initializing: dlQueue++; break;
case DownloadStatus.WaitingForSlot: dlSlot++; break; case DownloadStatus.WaitingForSlot: dlSlot++; break;
case DownloadStatus.WaitingForQueue: dlQueue++; break; case DownloadStatus.WaitingForQueue: dlQueue++; break;
case DownloadStatus.Downloading: dlProg++; break; case DownloadStatus.Downloading: dlProg++; break;
case DownloadStatus.Decompressing: dlDecomp++; break; case DownloadStatus.Decompressing: dlDecomp++; break;
case DownloadStatus.Completed: dlComplete++; break;
} }
totalBytes += fileStatus.TotalBytes; totalBytes += fileStatus.TotalBytes;
transferredBytes += fileStatus.TransferredBytes; transferredBytes += fileStatus.TransferredBytes;
} }
var progress = totalBytes > 0 ? (float)transferredBytes / totalBytes : 0f; var progress = totalBytes > 0 ? (float)transferredBytes / totalBytes : 0f;
if (dlComplete > 0 && dlProg == 0 && dlDecomp == 0 && dlQueue == 0 && dlSlot == 0)
{
progress = 1f;
}
string status; string status;
if (dlDecomp > 0) status = "decompressing"; if (dlDecomp > 0) status = "decompressing";
else if (dlProg > 0) status = "downloading"; else if (dlProg > 0) status = "downloading";
else if (dlQueue > 0) status = "queued"; else if (dlQueue > 0) status = "queued";
else if (dlSlot > 0) status = "waiting"; else if (dlSlot > 0) status = "waiting";
else if (dlComplete > 0) status = "completed";
else status = "completed"; else status = "completed";
downloadStatus.Add((item.Key.Name, progress, status)); downloadStatus.Add((item.Key.Name, progress, status));

View File

@@ -29,7 +29,6 @@ public class DrawEntityFactory
private readonly LightlessConfigService _configService; private readonly LightlessConfigService _configService;
private readonly UiSharedService _uiSharedService; private readonly UiSharedService _uiSharedService;
private readonly PlayerPerformanceConfigService _playerPerformanceConfigService; private readonly PlayerPerformanceConfigService _playerPerformanceConfigService;
private readonly LocationShareService _locationShareService;
private readonly CharaDataManager _charaDataManager; private readonly CharaDataManager _charaDataManager;
private readonly SelectTagForPairUi _selectTagForPairUi; private readonly SelectTagForPairUi _selectTagForPairUi;
private readonly RenamePairTagUi _renamePairTagUi; private readonly RenamePairTagUi _renamePairTagUi;
@@ -54,7 +53,6 @@ public class DrawEntityFactory
LightlessConfigService configService, LightlessConfigService configService,
UiSharedService uiSharedService, UiSharedService uiSharedService,
PlayerPerformanceConfigService playerPerformanceConfigService, PlayerPerformanceConfigService playerPerformanceConfigService,
LocationShareService locationShareService,
CharaDataManager charaDataManager, CharaDataManager charaDataManager,
SelectTagForSyncshellUi selectTagForSyncshellUi, SelectTagForSyncshellUi selectTagForSyncshellUi,
RenameSyncshellTagUi renameSyncshellTagUi, RenameSyncshellTagUi renameSyncshellTagUi,
@@ -74,7 +72,6 @@ public class DrawEntityFactory
_configService = configService; _configService = configService;
_uiSharedService = uiSharedService; _uiSharedService = uiSharedService;
_playerPerformanceConfigService = playerPerformanceConfigService; _playerPerformanceConfigService = playerPerformanceConfigService;
_locationShareService = locationShareService;
_charaDataManager = charaDataManager; _charaDataManager = charaDataManager;
_selectTagForSyncshellUi = selectTagForSyncshellUi; _selectTagForSyncshellUi = selectTagForSyncshellUi;
_renameSyncshellTagUi = renameSyncshellTagUi; _renameSyncshellTagUi = renameSyncshellTagUi;
@@ -165,7 +162,6 @@ public class DrawEntityFactory
_uiSharedService, _uiSharedService,
_playerPerformanceConfigService, _playerPerformanceConfigService,
_configService, _configService,
_locationShareService,
_charaDataManager, _charaDataManager,
_pairLedger); _pairLedger);
} }
@@ -217,7 +213,6 @@ public class DrawEntityFactory
entry.PairStatus, entry.PairStatus,
handler?.LastAppliedDataBytes ?? -1, handler?.LastAppliedDataBytes ?? -1,
handler?.LastAppliedDataTris ?? -1, handler?.LastAppliedDataTris ?? -1,
handler?.LastAppliedApproximateEffectiveTris ?? -1,
handler?.LastAppliedApproximateVRAMBytes ?? -1, handler?.LastAppliedApproximateVRAMBytes ?? -1,
handler?.LastAppliedApproximateEffectiveVRAMBytes ?? -1, handler?.LastAppliedApproximateEffectiveVRAMBytes ?? -1,
handler); handler);

View File

@@ -46,12 +46,10 @@ public sealed class DtrEntry : IDisposable, IHostedService
private string? _lightfinderText; private string? _lightfinderText;
private string? _lightfinderTooltip; private string? _lightfinderTooltip;
private Colors _lightfinderColors; private Colors _lightfinderColors;
private readonly object _localHashedCidLock = new();
private string? _localHashedCid; private string? _localHashedCid;
private DateTime _localHashedCidFetchedAt = DateTime.MinValue; private DateTime _localHashedCidFetchedAt = DateTime.MinValue;
private DateTime _localHashedCidNextErrorLog = DateTime.MinValue; private DateTime _localHashedCidNextErrorLog = DateTime.MinValue;
private DateTime _pairRequestNextErrorLog = DateTime.MinValue; private DateTime _pairRequestNextErrorLog = DateTime.MinValue;
private int _localHashedCidRefreshActive;
public DtrEntry( public DtrEntry(
ILogger<DtrEntry> logger, ILogger<DtrEntry> logger,
@@ -103,19 +101,10 @@ public sealed class DtrEntry : IDisposable, IHostedService
public async Task StopAsync(CancellationToken cancellationToken) public async Task StopAsync(CancellationToken cancellationToken)
{ {
_cancellationTokenSource.Cancel(); await _cancellationTokenSource.CancelAsync().ConfigureAwait(false);
if (_dalamudUtilService.IsOnFrameworkThread)
{
_logger.LogDebug("Skipping Lightfinder DTR wait on framework thread during shutdown.");
_cancellationTokenSource.Dispose();
return;
}
try try
{ {
if (_runTask != null) await _runTask!.ConfigureAwait(false);
await _runTask.ConfigureAwait(false);
} }
catch (OperationCanceledException) catch (OperationCanceledException)
{ {
@@ -350,61 +339,29 @@ public sealed class DtrEntry : IDisposable, IHostedService
private string? GetLocalHashedCid() private string? GetLocalHashedCid()
{ {
var now = DateTime.UtcNow; var now = DateTime.UtcNow;
lock (_localHashedCidLock) if (_localHashedCid is not null && now - _localHashedCidFetchedAt < _localHashedCidCacheDuration)
{
if (_localHashedCid is not null && now - _localHashedCidFetchedAt < _localHashedCidCacheDuration)
{
return _localHashedCid;
}
}
QueueLocalHashedCidRefresh();
lock (_localHashedCidLock)
{
return _localHashedCid; return _localHashedCid;
}
}
private void QueueLocalHashedCidRefresh() try
{
if (Interlocked.Exchange(ref _localHashedCidRefreshActive, 1) != 0)
{ {
return; var cid = _dalamudUtilService.GetCIDAsync().GetAwaiter().GetResult();
var hashedCid = cid.ToString().GetHash256();
_localHashedCid = hashedCid;
_localHashedCidFetchedAt = now;
return hashedCid;
} }
catch (Exception ex)
_ = Task.Run(async () =>
{ {
try if (now >= _localHashedCidNextErrorLog)
{ {
var cid = _dalamudUtilService.GetCID(); _logger.LogDebug(ex, "Failed to refresh local hashed CID for Lightfinder DTR entry.");
var hashedCid = cid.ToString().GetHash256(); _localHashedCidNextErrorLog = now + _localHashedCidErrorCooldown;
lock (_localHashedCidLock)
{
_localHashedCid = hashedCid;
_localHashedCidFetchedAt = DateTime.UtcNow;
}
} }
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; _localHashedCid = null;
_localHashedCidFetchedAt = now; _localHashedCidFetchedAt = now;
} return null;
} }
finally
{
Interlocked.Exchange(ref _localHashedCidRefreshActive, 0);
}
});
} }
private List<string> GetNearbyBroadcasts() private List<string> GetNearbyBroadcasts()

View File

@@ -415,9 +415,7 @@ public class IdDisplayHandler
var vramBytes = pair.LastAppliedApproximateEffectiveVRAMBytes >= 0 var vramBytes = pair.LastAppliedApproximateEffectiveVRAMBytes >= 0
? pair.LastAppliedApproximateEffectiveVRAMBytes ? pair.LastAppliedApproximateEffectiveVRAMBytes
: pair.LastAppliedApproximateVRAMBytes; : pair.LastAppliedApproximateVRAMBytes;
var triangleCount = pair.LastAppliedApproximateEffectiveTris >= 0 var triangleCount = pair.LastAppliedDataTris;
? pair.LastAppliedApproximateEffectiveTris
: pair.LastAppliedDataTris;
if (vramBytes < 0 && triangleCount < 0) if (vramBytes < 0 && triangleCount < 0)
{ {
return null; return null;

File diff suppressed because it is too large Load Diff

View File

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

View File

@@ -21,7 +21,6 @@ public sealed record PairUiEntry(
IndividualPairStatus? PairStatus, IndividualPairStatus? PairStatus,
long LastAppliedDataBytes, long LastAppliedDataBytes,
long LastAppliedDataTris, long LastAppliedDataTris,
long LastAppliedApproximateEffectiveTris,
long LastAppliedApproximateVramBytes, long LastAppliedApproximateVramBytes,
long LastAppliedApproximateEffectiveVramBytes, long LastAppliedApproximateEffectiveVramBytes,
IPairHandlerAdapter? Handler) IPairHandlerAdapter? Handler)

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