Compare commits

...
250 Commits
Author SHA1 Message Date
amacocian 83ddafea62 Aggregate custom metrics when enabled (Closes #1024) (#1027) 2025-06-29 17:32:52 +02:00
amacocian ab1b604aec Code cleanup (#1026)
* Resolve warnings and move shared props to Directory.Build.props and Directory.Packages.props

* Cleanup more messages

* Fix more messages

* Fix build issues
2025-06-29 17:32:52 +02:00
amacocian d445d0c5f2 Release 0.9.9.76 (#1025) 2025-06-29 17:32:52 +02:00
amacocian f5216020b6 Bugfix for LaunchButton (Closes #1020) (#1022) 2025-06-27 13:59:45 +02:00
amacocian 1b7654f70f Allow users to load and save builds from Focus View (Closes #1018) (#1021) 2025-06-27 13:59:45 +02:00
amacocian 06288ec2a2 Release 0.9.9.75 (#1019) 2025-06-27 13:59:45 +02:00
amacocian 670fba36df Setup APM connection (#1016) 2025-06-25 20:26:30 +02:00
amacocian 0d74a9c15e Fix time parsing exception on price charts (Closes #989) (#1015) 2025-06-25 20:26:30 +02:00
amacocian e0729d00ff Setup opt-in telemetry service (under Diagnostics/Telemetry) (#1014)
* Setup opt-in telemetry service (under Diagnostics/Telemetry)

* Set application environment
2025-06-25 20:26:30 +02:00
amacocian 0adce88894 Release 0.9.9.74 (#1013) 2025-06-25 20:26:30 +02:00
amacocian 77bf5e5359 Allow mods to be reinjected if they detect they need to (#1010) (Closes #1005 #724 #639) 2025-06-24 18:44:42 +02:00
amacocian 5a92b916dd Improve Focus View performance and hooking availability (#1009) (Closes #1007) 2025-06-24 18:44:42 +02:00
amacocian accfed2440 Improve game discovery and attach performance (#1008) (Closes #1004) 2025-06-24 18:44:42 +02:00
amacocian bdc45eb4b3 Release 0.9.9.73 (#1006) 2025-06-24 18:44:42 +02:00
amacocian 6916138351 Speed up build process (#1003) 2025-06-23 18:51:14 +02:00
amacocian 741fb9059d Update builds (#1002) 2025-06-23 18:51:14 +02:00
amacocian 6c9e47e58e Fix CI (#1001) 2025-06-23 18:51:14 +02:00
amacocian 0c3873b737 Update extractor and api (#999) 2025-06-23 18:51:14 +02:00
amacocian 411f213d8b Fix Launch button to allow launching multiple configs from the same exe (#998) 2025-06-23 18:51:14 +02:00
amacocian a55c2b78e9 Update dependencies (#997) 2025-06-23 18:51:14 +02:00
amacocian da3d878d90 Enable re-attachment for multi-exe configs (#996) Closes #993 2025-06-23 18:51:14 +02:00
amacocian f08104a851 Implement Daybreak.API. Reorganize FocusView components (#995) 2025-06-23 18:51:14 +02:00
amacocian 052bcaa29d Greatly improve installer performance (#994) 2025-06-23 18:51:14 +02:00
amacocian 51edeae93a Further decouple shared project from main application (#991) 2025-06-23 18:51:14 +02:00
amacocian 6d12b2beaf Release 0.9.9.72 (#990) 2025-06-23 18:51:14 +02:00
amacocian 7347de58af Build editor improvements (Closes #776) (#986) 2025-05-16 13:41:10 +02:00
amacocian f1ee932f23 EventsView scroll to current/upcoming event (Closes #777) (#985) 2025-05-16 13:41:10 +02:00
amacocian cdbc81e310 Performance improvements to Build Synchronization View (Closes #980) (#984) 2025-05-16 13:41:10 +02:00
amacocian 629c6ed14f Support for ReShade latest version (Closes #926) (#983) 2025-05-16 13:41:10 +02:00
amacocian 6577db54bc Update README (#982) 2025-05-16 13:41:10 +02:00
amacocian 926e9b7b48 Release 0.9.9.71 (#981) 2025-05-16 13:41:10 +02:00
amacocian c8433210e3 Hotfix for missing DXVK (#978) 2025-05-15 16:43:07 +02:00
amacocian 3274405c51 Release 0.9.9.70 (Closes #976) (#977) 2025-05-15 16:43:07 +02:00
amacocian 01bde80c39 Use latest DXVK binaries (#974) 2025-05-15 14:18:23 +02:00
amacocian 3f810b8a94 Launch Configurations can select any available executable (Closes #970) (#973) 2025-05-15 14:18:23 +02:00
amacocian 5d7ba2598f DXVK support (Closes #231) (#972) 2025-05-15 14:18:23 +02:00
amacocian 670d6bcbd5 Bump version (#971) 2025-05-15 14:18:23 +02:00
amacocian 9690d649fa Fix plugin example (#968) 2025-05-14 16:15:27 +02:00
amacocian 821ce69547 Show vials of dye in price quotes view (Closes #966) (#967) 2025-05-14 16:15:27 +02:00
amacocian 4f524ada67 Change executable checkmark to foreground color (Closes #774) (#965) 2025-05-14 16:15:27 +02:00
amacocian 0c2234f31b Disable executables during update (Closes #779) (#964) 2025-05-14 16:15:27 +02:00
amacocian 70085efd39 Make AccountsView and ExecutablesView scrollable (Closes #956) (#963) 2025-05-14 16:15:27 +02:00
amacocian bfa2128868 Move Accounts, Executables and LaunchConfigurations on the first positions under Settings (Closes #957) (#962) 2025-05-14 16:15:27 +02:00
amacocian 9b0da9b13c Improve startup speed and responsiveness (Closes #959) (#961) 2025-05-14 16:15:27 +02:00
amacocian 721ee8e494 Huge application refactoring to better support plugins (#960)
* Move shared code to Shared project
* Move from Realm to Squealify
* Rename Daybreak.Shared namespaces
* Setup API project to expose a test API
2025-05-14 16:15:27 +02:00
amacocian 5458db891f Release 0.9.9.68 (#958) 2025-05-14 16:15:27 +02:00
amacocian 9d9c15d0f6 Update dependencies (#954) 2025-05-07 12:01:57 +02:00
amacocian 4f49fc1cdf Expose API for creating menu buttons (#953) 2025-05-07 12:01:57 +02:00
amacocian 7efc7ba698 Release 0.9.9.67 (#952) 2025-05-07 12:01:57 +02:00
amacocian a79a6eae98 Bugfix greyed-out launch button (#949) 2025-04-30 19:20:26 +02:00
amacocian 0801256f4c Release 0.9.9.66 (#948) 2025-04-30 19:20:26 +02:00
amacocian e0799dd100 Remove redundant operations on launch UI. Improve UI performance (#946) 2025-04-30 18:48:03 +02:00
amacocian 24f4607899 Release 0.9.9.65 (#945) 2025-04-30 18:48:03 +02:00
amacocian 7aee0dfc16 Bugfix for multi-launch UI freezes (Closes #941) (#943) 2025-04-29 15:05:45 +02:00
amacocian 5fcb2314bc Release 0.9.9.64 (#942) 2025-04-29 15:05:45 +02:00
amacocian f218b89f6c Match Guildwars folder name structures (#938) 2025-04-27 16:01:22 +02:00
amacocian cc607b37c9 Release 0.9.9.63 (#937) 2025-04-27 16:01:22 +02:00
amacocian f80ea8cf0c Inject toolbox before GW initialization (#935) (#927) 2025-04-26 15:48:54 +02:00
amacocian 39f6707d23 Release 0.9.9.62 (#934) 2025-04-26 15:48:54 +02:00
amacocian eb11029ece Update pipeline runtimes (#933) 2025-04-25 02:42:36 +02:00
amacocian 4583b5b2d3 Fix toolbox injection (Closes #927) (#931) 2025-04-25 02:42:36 +02:00
amacocian 8c01377667 Update .NET9 runtime (#930) 2025-04-25 02:42:36 +02:00
amacocian 9a130d71dd Release 0.9.9.61 (#929) 2025-04-25 02:42:36 +02:00
amacocian 018ed6c5b5 Update dependencies (#928) 2025-04-25 02:42:36 +02:00
amacocianandGitHub cb48248c82 Change VirusTotal publish folder (#913) 2024-12-11 17:18:54 +01:00
amacocianandGitHub e1284375e2 Add debugging info to CD pipeline (#912) 2024-12-11 12:34:00 +00:00
amacocianandGitHub d38d620a84 Add VirusTotal scanning to CD pipeline (#911) 2024-12-11 11:36:39 +00:00
amacocian ce810dafab Support for toolbox builds (#908)
Closes #866
2024-12-10 14:50:59 +01:00
amacocian aabc98e4fc Build template metadata with support for hero positions. Closes #906 (#907) 2024-12-10 14:50:59 +01:00
amacocian 7eb0b7cc4f Release 0.9.9.60 (#905) 2024-12-10 14:50:59 +01:00
amacocian f1eb829ac4 Fix non-persistent notifications showing up after expiring. Closes #901 (#903) 2024-12-07 15:37:53 +01:00
amacocian 4b03a5a16f Increase memory scanner responsiveness (#902) 2024-12-07 15:37:53 +01:00
amacocian 71c90100be Fix memory scanner deadlock. Closes #899 (#900) 2024-12-07 15:37:53 +01:00
amacocian ee512d130d Fix launch button text when Focus View is disabled. Closes #897 (#898) 2024-12-07 15:37:53 +01:00
amacocian 03d0759e57 Release 0.9.9.59 (#896) 2024-12-07 15:37:53 +01:00
amacocian 0db7628d66 Cleanup GWCA dependencies (#895) 2024-12-06 21:19:01 +01:00
amacocian d1d1ba6c08 Fix build pipelines (#894) 2024-12-06 21:19:01 +01:00
amacocian d16a88a9bc Remove submodules file (#893) 2024-12-06 21:19:01 +01:00
amacocian a9d93f4162 Update dependencies (#891) 2024-12-06 21:19:01 +01:00
amacocian 632b5a55ac Remove GWCA (#890) Closes #889 #887 2024-12-06 21:19:01 +01:00
amacocian 6424505631 Release 0.9.9.58 (#888) 2024-12-06 21:19:01 +01:00
amacocian dfc1d3ba0a View for Guild Wars Party Search (#880)
Closes #877
2024-11-22 20:19:04 +01:00
amacocian 5bee88016d Upgrade vulnerable testing package (#879) 2024-11-22 20:19:04 +01:00
amacocian 5a37250680 Release 0.9.9.57 (#878) 2024-11-22 20:19:04 +01:00
amacocian 1c4ccb9fa3 Update dependencies (#874) 2024-11-22 15:09:42 +01:00
amacocian 8403dadca7 Patch GWCA module to not rely on the GW Window to start the http server. To be reverted once GWCA is patched (#871) (#873) 2024-11-22 15:09:42 +01:00
amacocian 4ac4cb0886 Check and update toolbox version (#872) 2024-11-22 15:09:42 +01:00
amacocian 74f13d2447 Update GWCA (#869) 2024-11-22 15:09:42 +01:00
amacocian bf53793729 Release 0.9.9.56 (#868) 2024-11-22 15:09:42 +01:00
amacocian e69dbf6791 Fix error test (#864) 2024-11-17 16:25:36 +01:00
amacocian a0eafa8a13 Setup button for bug reporting (#862)
Overhaul bug exception window
Closes #784
2024-11-17 16:25:36 +01:00
amacocian b0a236beb4 Use safe DateTime conversion (#861) 2024-11-17 16:25:36 +01:00
amacocian c10b81f3a1 Release 0.9.9.55 (#860) 2024-11-17 16:25:36 +01:00
amacocian 6cd51d6584 Fix theme sync (#858)
Closes #857
2024-11-16 00:16:40 +01:00
amacocian a283117cc8 Remove Minimap and InventoryComponent (#856)
Closes #855
2024-11-16 00:16:40 +01:00
amacocian 329e64232c Stop spamming when datetime conversion fails. This is a temporary fix for the spam (#854)
Closes #852
2024-11-16 00:16:40 +01:00
amacocian feb3c9042e Release 0.9.9.54 (#853) 2024-11-16 00:16:40 +01:00
amacocian 71f15f56a4 Update dependencies (#850) 2024-11-13 15:10:02 +01:00
amacocian d37951b832 Update GWCA (#849) 2024-11-13 15:10:02 +01:00
amacocian 908a149df4 Release 0.9.9.53 (#848) 2024-11-13 15:10:02 +01:00
amacocian 38cf140dfa Update GWCA (#838) 2024-10-23 22:57:08 +02:00
amacocian d13e1a2d08 Migrate db engine to Realm (#837) 2024-10-23 22:57:08 +02:00
amacocian a0c7842b84 Update dependencies (#836) 2024-10-23 22:57:08 +02:00
amacocian de13b68633 Release 0.9.9.52 (#835) 2024-10-23 22:57:08 +02:00
amacocian f289602068 Update dependencies - fixes critical System.Text.Json vulnerability (#831) 2024-10-10 12:01:01 +02:00
amacocian 15f4ee8979 Release 0.9.9.51 (#830) 2024-10-10 12:01:01 +02:00
amacocian d826dea457 Use sell quotes. Add sell prices and buy prices to trade quote view (#825) 2024-10-07 11:44:47 +02:00
amacocian b89a6666a3 Update dependencies and replace deprecated packages (#824) 2024-10-07 11:44:47 +02:00
amacocian 1dc69fd0c2 Release 0.9.9.50 (#823) 2024-10-07 11:44:47 +02:00
amacocian 5005b07f09 Update GWCA module (#819) 2024-09-27 17:37:40 +02:00
amacocian 0b692165e0 Update dependencies (#818) 2024-09-27 17:37:40 +02:00
amacocian ffee0fdf4c Release 0.9.9.49 (#817) 2024-09-27 17:37:40 +02:00
amacocian 72b4cd5470 Update GWCA (#808) 2024-08-20 17:34:59 +02:00
dependabot[bot]amacociandependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
dc9be53ace Bump MSTest.TestAdapter from 3.5.0 to 3.5.2 (#795)
Bumps [MSTest.TestAdapter](https://github.com/microsoft/testfx) from 3.5.0 to 3.5.2.
- [Release notes](https://github.com/microsoft/testfx/releases)
- [Changelog](https://github.com/microsoft/testfx/blob/main/docs/Changelog.md)
- [Commits](https://github.com/microsoft/testfx/compare/v3.5.0...v3.5.2)

---
updated-dependencies:
- dependency-name: MSTest.TestAdapter
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Signed-off-by: Macocian Alexandru Victor <amacocian@yahoo.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Macocian Alexandru Victor <amacocian@yahoo.com>
2024-08-20 17:34:59 +02:00
dependabot[bot]amacociandependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
3a8a6b0f7f Bump MSTest.TestFramework from 3.5.0 to 3.5.2 (#797)
Bumps [MSTest.TestFramework](https://github.com/microsoft/testfx) from 3.5.0 to 3.5.2.
- [Release notes](https://github.com/microsoft/testfx/releases)
- [Changelog](https://github.com/microsoft/testfx/blob/main/docs/Changelog.md)
- [Commits](https://github.com/microsoft/testfx/compare/v3.5.0...v3.5.2)

---
updated-dependencies:
- dependency-name: MSTest.TestFramework
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-08-20 17:34:59 +02:00
amacocian 7cdd0534f6 Set version checker as first mod in the list (#807)
Closes #778
2024-08-20 17:34:59 +02:00
amacocian 4b14ab9312 Add database cleanup (#806)
Closes #791
2024-08-20 17:34:59 +02:00
amacocian 57e1ddb8da Release 0.9.9.48 (#805) 2024-08-20 17:34:59 +02:00
amacocian c7609c0f7e Update dependencies (#803) 2024-08-20 16:34:04 +02:00
amacocian 1eca8021d2 Fixes for stuttering on image and notification load (#802) 2024-08-20 16:34:04 +02:00
amacocian 0077f35c4d Fix a bug in credentials migrator that would delete newly created credentials (#801) 2024-08-20 16:34:04 +02:00
amacocian 28b0a0a138 Release 0.9.9.47 (#800) 2024-08-20 16:34:04 +02:00
amacocian cc7ce92013 Update dependencies (#789) 2024-07-18 12:15:25 +03:00
amacocian 5bd02e56ee Ignore operation canceled exceptions (#788) 2024-07-18 12:15:25 +03:00
amacocian 8c7cd0a562 Bugfix for executable paths exception (#787) 2024-07-18 12:15:25 +03:00
amacocian 3ab3f72366 Release 0.9.9.46 (#785) 2024-07-18 12:15:25 +03:00
amacocian 733883a6c2 Check executables for fixes and updates (Closes #763) (#772) 2024-07-07 16:55:34 +03:00
amacocian d1ed8aeaf6 Fix GWCA module (#771) 2024-07-07 16:55:34 +03:00
amacocian 66ad44b8ce Update GWCA (#770) 2024-07-07 16:55:34 +03:00
amacocian 18e51ae408 Fix Nahpui Quarter missing id (Closes #761) (#769) 2024-07-07 16:55:34 +03:00
amacocian 06940dbf38 Update dependencies (#768) 2024-07-07 16:55:34 +03:00
amacocian 875b29f8ba Daybreak maintains and updates executables (#767) 2024-07-07 16:55:34 +03:00
amacocian a953c3a0d3 Release 0.9.9.45 (#766) 2024-07-07 16:55:34 +03:00
amacocian ce198fcfc9 Update GWCA (#759) 2024-06-11 15:03:37 +02:00
amacocian 3d84648af2 Update test dependencies (#757) 2024-06-11 15:03:37 +02:00
amacocian 38bb43bba3 Daybreak paths now fall back to root Daybreak folder (Closes #716) (#756)
* Daybreak paths now fall back to root Daybreak folder (Closes #716)

* Fix some more paths
2024-06-11 15:03:37 +02:00
amacocian cc00709016 Show skill icons in skill list menu (Closes #747) (#755)
* Show skill icons in skill list menu (Closes #747)

* Remove unused code
2024-06-11 15:03:37 +02:00
amacocian ee02b3b49a Download Gw.exe directly (Closes #753) (#754)
* Download Gw.exe directly (Closes #753)

* Downloader improvements
2024-06-11 15:03:37 +02:00
amacocian d1d6b7d757 Update GWCA module (#752) 2024-06-11 15:03:37 +02:00
amacocian 4e1c0c4f50 Save dump on crash (Closes #749) (#751) 2024-06-11 15:03:37 +02:00
amacocian 2a7e2c90fc Update dependencies (#750) 2024-06-11 15:03:37 +02:00
amacocian 0dc957a99d Release 0.9.9.44 2024-06-11 15:03:37 +02:00
amacocian f3f0aeb97e Update GWCA (#736) 2024-05-22 13:09:53 +02:00
amacocian d6dbe6f2cd Freeze layouts calculations on resize (Closes #734) (#735) 2024-05-22 13:09:53 +02:00
amacocian 79da1801bc GWCA Debug Module (#733) 2024-05-22 13:09:53 +02:00
amacocian 2b692b2d88 Update GWCA (#732) 2024-05-22 13:09:53 +02:00
amacocian a7a98f7288 Release 0.9.9.43 (#731) 2024-05-22 13:09:53 +02:00
amacocian 5d27d9c380 Update GWCA plugin (#729) 2024-05-10 23:09:23 +02:00
amacocian fb4185b541 Update httplib (#728) 2024-05-10 23:09:23 +02:00
amacocian d2d6e87e12 Add timeouts to GWCA modules (Closes #725) (#727) 2024-05-10 23:09:23 +02:00
amacocian 354cdda109 Release 0.9.9.42 (#726) 2024-05-10 23:09:23 +02:00
amacocian 83429e264a Update GWCA (#723) 2024-05-09 10:33:05 +02:00
amacocian ffdf374cdd Allow mods to cancel Guild Wars startup (Closes #717) (#721) 2024-05-09 10:33:05 +02:00
amacocian aed34608ee Improve GWCA plugin logging (Closes #718) (#720) 2024-05-09 10:33:05 +02:00
amacocian 18981e131d Release 0.9.9.41 (#719) 2024-05-09 10:33:05 +02:00
amacocian fe0a77d89d Fix automatic screen placer (Closes #708) (#714) 2024-05-06 00:27:48 +03:00
amacocian dbfd555f0d Improve detection of Guild Wars launching successfully (Closes #712) (#713) 2024-05-06 00:27:48 +03:00
amacocian 85d4ac3a29 Remove price checker (now incl. in Toolbox) (Closes #707) (#711) 2024-05-06 00:27:48 +03:00
amacocian dcb983d3de Update GWCA (#710) 2024-05-06 00:27:48 +03:00
amacocian ba1e4348ad Release 0.9.9.40 (#709) 2024-05-06 00:27:48 +03:00
amacocian 04bc7cffc6 Update GWCA (Closes #696) (#705) 2024-05-01 12:35:14 +03:00
amacocian 01b74a936b Rewrite GWCA module for better maintainability (#702)
* Use game thread enqueue method

* Remove manual memory handling

* Use game thread enqueue method

* Remove manual memory handling

* Setup module classes
2024-05-01 12:35:14 +03:00
dependabot[bot]amacociandependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
654b6bc10f Bump HtmlAgilityPack from 1.11.60 to 1.11.61 (#704)
Bumps [HtmlAgilityPack](https://github.com/zzzprojects/html-agility-pack) from 1.11.60 to 1.11.61.
- [Release notes](https://github.com/zzzprojects/html-agility-pack/releases)
- [Commits](https://github.com/zzzprojects/html-agility-pack/compare/v1.11.60...v1.11.61)

---
updated-dependencies:
- dependency-name: HtmlAgilityPack
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-05-01 12:35:14 +03:00
amacocian c9e6b9f626 Shortcut auto-place no longer removes arguments (Closes #695) (#701) 2024-05-01 12:35:14 +03:00
amacocian 554f1aea9b Fix extracted minimap not updating (Closes #698) (#700) 2024-05-01 12:35:14 +03:00
dependabot[bot]amacociandependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
6fb817855c Bump SharpCompress from 0.37.0 to 0.37.2 (#697)
Bumps [SharpCompress](https://github.com/adamhathcock/sharpcompress) from 0.37.0 to 0.37.2.
- [Release notes](https://github.com/adamhathcock/sharpcompress/releases)
- [Commits](https://github.com/adamhathcock/sharpcompress/compare/0.37.0...0.37.2)

---
updated-dependencies:
- dependency-name: SharpCompress
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-05-01 12:35:14 +03:00
amacocian b3f5fadfe3 Release 0.9.9.39 (#699) 2024-05-01 12:35:14 +03:00
DubbleClick 9275b7a7f8 simpler PreGameModule, test this 2024-04-28 23:14:58 +02:00
DubbleClick 2d06ff85b8 ignore .idea folder 2024-04-28 23:00:38 +02:00
amacocian 07d06313d9 Support for GuildWars launch arguments (Closes #691) (#693) 2024-04-27 13:46:53 +02:00
amacocian 546409130c Auto launch configuration (Closes #689) (#692) 2024-04-27 13:46:53 +02:00
amacocian 82436872f8 Launch argument support (Closes #689) (#690) 2024-04-27 13:46:53 +02:00
amacocian ad6f76af0c Event notification expiration set to 15s (Closes #686) (#688) 2024-04-27 13:46:53 +02:00
amacocian 8f0ad91a22 Bundle quote alerts (Closes #676) (#687) 2024-04-27 13:46:53 +02:00
amacocian 6a70c5b7cc Fix 'yearss' typo in Trade chat (Closes #677) (#685) 2024-04-27 13:46:53 +02:00
amacocian 5f5d67fdf2 Release 0.9.9.38 (#684) 2024-04-27 13:46:53 +02:00
dependabot[bot]andamacocian 150b41647a Bump SharpCompress from 0.36.0 to 0.37.0 (#675)
Bumps [SharpCompress](https://github.com/adamhathcock/sharpcompress) from 0.36.0 to 0.37.0.
- [Release notes](https://github.com/adamhathcock/sharpcompress/releases)
- [Commits](https://github.com/adamhathcock/sharpcompress/compare/0.36.0...0.37.0)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-04-26 20:48:21 +02:00
amacocian 66e52e7324 Remove redundant GWCA calls on non-visible components (#681) 2024-04-26 20:48:21 +02:00
amacocian 23328240a9 Fix launch button spamming GWCA (#680) 2024-04-26 20:48:21 +02:00
amacocian 3f06e9486d Setup release 0.9.9.37 (#678) 2024-04-26 20:48:21 +02:00
dependabot[bot]amacociandependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
52a8e912e9 Bump Microsoft.Web.WebView2 from 1.0.2420.47 to 1.0.2478.35 (#669)
Bumps Microsoft.Web.WebView2 from 1.0.2420.47 to 1.0.2478.35.

---
updated-dependencies:
- dependency-name: Microsoft.Web.WebView2
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-04-23 14:59:34 +02:00
amacocian 641aa8250e Minimap player arrow (#673) 2024-04-23 14:59:34 +02:00
amacocian faa47f32c7 Minimap angle (#672) 2024-04-23 14:59:34 +02:00
amacocian e3dac780df Update GWCA (#671) Closes (#668) 2024-04-23 14:59:34 +02:00
amacocian dfc0f12150 Release 0.9.9.36 (#670) 2024-04-23 14:59:34 +02:00
amacocian 638fb38fa0 Fix exception when extracting maximized minimap (Closes #662) (#666) 2024-04-22 14:33:10 +02:00
amacocian 4d6a06ccdf Floodplains of Mahnkelon id (Closes #653) (#665) 2024-04-22 14:33:10 +02:00
amacocian 3228604876 Item modifiers improvements (#664)
* Improve modifiers check for items

* Update minor vigor
2024-04-22 14:33:10 +02:00
amacocian 44879924d6 Release 0.9.9.35 2024-04-22 14:33:10 +02:00
amacocian 6b7478f6ae Save minimap pinned state (Closes #655) (#659) 2024-04-18 19:24:19 +02:00
amacocian f54d95c1e7 Fix word highlighting edge cases (Closes #656) (#658) 2024-04-18 19:24:19 +02:00
amacocian 5315a45a98 Fix browser navigation (Closes #654) (#657) 2024-04-18 19:24:19 +02:00
amacocian 8575d970a3 Setup release 0.9.9.34 2024-04-18 19:24:19 +02:00
amacocian 5c611eca4f GWCA Module Bugfix (#651) 2024-04-16 18:41:44 +02:00
amacocian 71aa37b965 Added 558 id for sparkfly swamp (#650) 2024-04-16 18:41:44 +02:00
amacocian 0f1b1cc83d Release 0.9.9.33 2024-04-16 18:41:44 +02:00
amacocian 198c1e6eb3 Update Daybreak.GWCA to 0.9.9.32 2024-04-16 11:28:18 +02:00
amacocian e7c9111a1f Allow credentials to be backed up (Closes #525) (#648) 2024-04-16 11:28:18 +02:00
amacocian 1191e996db Update GWCA (#647) 2024-04-16 11:28:18 +02:00
amacocian 19de3c99f9 Release 0.9.9.23 2024-04-16 11:28:18 +02:00
amacocian d857b6dc26 Bugfixes in GWCA module (#645) 2024-04-15 21:56:37 +02:00
amacocian 68b5b79035 Release 0.9.9.31 2024-04-15 21:56:37 +02:00
amacocian 318032d998 Inventory component loading widget (#643) 2024-04-15 12:13:52 +02:00
amacocian 483860532f Update GWCA (#642) 2024-04-15 12:13:52 +02:00
amacocian 694dbe82a9 Race condition crashfix - Downgrade LiteDB dependency - Closes #640 (#641) 2024-04-15 12:13:52 +02:00
amacocian a936f24e32 Setup release branch 2024-04-15 12:13:52 +02:00
amacocian a2ec87ced2 Update GWCA module (#638) 2024-04-12 02:46:53 +02:00
amacocian 48955a7dd7 Setup GWCA whisper functionality (#636) 2024-04-12 02:46:53 +02:00
amacocian 9d7c6c29b0 Setup periodical price checker for inventory (#635) 2024-04-12 02:46:53 +02:00
amacocian 902d210dfd Fix pointer in GWCA plugin (#633) 2024-04-12 02:46:53 +02:00
amacocian 57867851a9 Release 0.9.9.29 2024-04-12 02:46:53 +02:00
amacocianandGitHub ca82cc92bf Release 0.9.9.28 (#632)
Update GWCA
2024-04-11 17:58:39 +00:00
amacocianandGitHub 569ce1e656 Release-0.9.9.27
Release 0.9.9.27
Changelog viewer
2024-04-11 19:28:36 +02:00
amacocian a96c6c3f66 Release 0.9.9.27
Implement changelog view (#629)
2024-04-11 19:19:15 +02:00
amacocian 28c3b2f8c1 Fix CD pipeline 2024-04-11 18:29:04 +02:00
amacocianandGitHub deeccd81b4 Release 0.9.9.26 (#628)
* Event Calendar (#625)

* Event calendar view

* Update GWCA (#626)

* Publish changelog (#627)

* Update dependencies
2024-04-11 16:26:22 +00:00
amacocianandGitHub 6a27245ae8 Release 0.9.9.25 (#618) 2024-04-02 16:59:21 +00:00
amacocianandGitHub 6f4063abb7 Release v0.9.9.24 (#617)
Fix price history functionality
Implement all runes
Update dependencies
2024-03-27 21:33:30 +00:00
amacocianandGitHub f307bcd665 Release 0.9.9.23 (#615)
* Create 0.9.9.23 release

* Update dependencies
2024-03-18 17:52:51 +00:00
amacocianandGitHub 02ca41ba6d Release 0.9.9.22 (#612)
* Fix Daybreak restart commands

* Option to disable dynamic backgrounds

* Improve game running detection
2024-03-04 15:33:00 +00:00
amacocianandGitHub a0ebc87b7c Release 0.9.9.21 (#609)
Fix updater packaging logic
2024-02-27 14:08:02 +00:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
d21c55deb0 Bump Xotl/cool-github-releases from 1.1.8 to 1.1.10 (#608)
Bumps [Xotl/cool-github-releases](https://github.com/xotl/cool-github-releases) from 1.1.8 to 1.1.10.
- [Release notes](https://github.com/xotl/cool-github-releases/releases)
- [Commits](https://github.com/xotl/cool-github-releases/compare/v1.1.8...v1.1.10)

---
updated-dependencies:
- dependency-name: Xotl/cool-github-releases
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-02-27 12:53:07 +00:00
amacocianandGitHub 50a8131ec2 Update dependencies (#607)
Improve updater
2024-02-27 12:42:39 +00:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
e70cfa3d99 Bump coverlet.collector from 6.0.0 to 6.0.1 (#596)
Bumps [coverlet.collector](https://github.com/coverlet-coverage/coverlet) from 6.0.0 to 6.0.1.
- [Release notes](https://github.com/coverlet-coverage/coverlet/releases)
- [Commits](https://github.com/coverlet-coverage/coverlet/compare/v6.0.0...v6.0.1)

---
updated-dependencies:
- dependency-name: coverlet.collector
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-02-24 20:46:19 +00:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
4c0c45f1c7 Bump MSTest.TestFramework from 3.2.1 to 3.2.2 (#600)
Bumps [MSTest.TestFramework](https://github.com/microsoft/testfx) from 3.2.1 to 3.2.2.
- [Release notes](https://github.com/microsoft/testfx/releases)
- [Changelog](https://github.com/microsoft/testfx/blob/main/docs/Changelog.md)
- [Commits](https://github.com/microsoft/testfx/compare/v3.2.1...v3.2.2)

---
updated-dependencies:
- dependency-name: MSTest.TestFramework
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-02-24 20:32:37 +00:00
amacocianandGitHub a9e819a83a Release 0.9.9.19 (#602)
Disable GWCA component when Focus View is disabled
2024-02-24 20:20:11 +00:00
amacocianandGitHub 65830f9125 Release 0.9.9.18 (#595)
* Setup release 0.9.9.18

* Minimap transparency
Closes #594

* Toggle to display exploration percentage
Closes #593
2024-02-18 22:55:07 +01:00
amacocianandGitHub 3d9106addf Release 0.9.9.17 (#592)
* Setup release 0.9.9.17

* Save extracted minimap window position (#591)

Closes #590

* Add ability to pin minimap to screen
Closes #585

* Fix ObjectDisposedException
Closes #589
2024-02-18 16:04:52 +00:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>amacocian
2863032d9a Bump MSTest.TestAdapter from 3.2.0 to 3.2.1 (#587)
Bumps [MSTest.TestAdapter](https://github.com/microsoft/testfx) from 3.2.0 to 3.2.1.
- [Release notes](https://github.com/microsoft/testfx/releases)
- [Changelog](https://github.com/microsoft/testfx/blob/main/docs/Changelog.md)
- [Commits](https://github.com/microsoft/testfx/compare/v.3.2.0...v3.2.1)

---
updated-dependencies:
- dependency-name: MSTest.TestAdapter
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Signed-off-by: Macocian Alexandru Victor <amacocian@yahoo.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Macocian Alexandru Victor <amacocian@yahoo.com>
2024-02-14 10:48:58 +00:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
aada189b1b Bump MSTest.TestFramework from 3.2.0 to 3.2.1 (#588)
Bumps [MSTest.TestFramework](https://github.com/microsoft/testfx) from 3.2.0 to 3.2.1.
- [Release notes](https://github.com/microsoft/testfx/releases)
- [Changelog](https://github.com/microsoft/testfx/blob/main/docs/Changelog.md)
- [Commits](https://github.com/microsoft/testfx/compare/v.3.2.0...v3.2.1)

---
updated-dependencies:
- dependency-name: MSTest.TestFramework
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-02-14 11:40:09 +01:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
22ea4de66e Bump NSubstitute.Analyzers.CSharp from 1.0.16 to 1.0.17 (#586)
Bumps [NSubstitute.Analyzers.CSharp](https://github.com/nsubstitute/NSubstitute.Analyzers) from 1.0.16 to 1.0.17.
- [Changelog](https://github.com/nsubstitute/NSubstitute.Analyzers/blob/1.0.17/ReleaseNotes.md)
- [Commits](https://github.com/nsubstitute/NSubstitute.Analyzers/compare/1.0.16...1.0.17)

---
updated-dependencies:
- dependency-name: NSubstitute.Analyzers.CSharp
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-02-12 05:46:15 +01:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
9859617a74 Bump Microsoft.NET.Test.Sdk from 17.8.0 to 17.9.0 (#578)
Bumps [Microsoft.NET.Test.Sdk](https://github.com/microsoft/vstest) from 17.8.0 to 17.9.0.
- [Release notes](https://github.com/microsoft/vstest/releases)
- [Changelog](https://github.com/microsoft/vstest/blob/main/docs/releases.md)
- [Commits](https://github.com/microsoft/vstest/compare/v17.8.0...v17.9.0)

---
updated-dependencies:
- dependency-name: Microsoft.NET.Test.Sdk
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-02-07 15:41:59 +01:00
amacocianandGitHub 0629c1929d Release 0.9.9.16 (#582)
* Setup release 0.9.9.16

* Copy buttons for build templates (#580)

* Extract minimap functionality (#581)

* Improve performance of RowAutoMargin functionality
2024-02-07 15:24:47 +01:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
6ec45396c3 Bump microsoft/setup-msbuild from 1.3.2 to 2.0.0 (#575)
Bumps [microsoft/setup-msbuild](https://github.com/microsoft/setup-msbuild) from 1.3.2 to 2.0.0.
- [Release notes](https://github.com/microsoft/setup-msbuild/releases)
- [Changelog](https://github.com/microsoft/setup-msbuild/blob/main/building-release.md)
- [Commits](https://github.com/microsoft/setup-msbuild/compare/v1.3.2...v2.0.0)

---
updated-dependencies:
- dependency-name: microsoft/setup-msbuild
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-02-05 10:23:57 +01:00
amacocianandGitHub b76a9b80fb Release 0.9.9.15 (#574)
* Setup release 0.9.9.15

* Export team build from focus view (#573)

Closes #573

* Fix load skill template
2024-02-04 19:30:22 +01:00
amacocianandGitHub f4bc58bbf7 Release 0.9.9.14 (#570)
* Team build support (#565)

* Setup support for team builds
Fix parser to load team builds
Fix bug in attribute loading
Adjust encoder to order and skip attributes for consistency

* Small buildtemplate binding fixes
Setup TeamBuildTemplateView

* Setup button to create team build

* Fix launch button (#566)

Closes #563

* Team build improvements (#568)

Closes #564

* Closes #562 (#569)

Closes #561

* Increment version
2024-02-04 14:11:01 +00:00
amacocianandGitHub 09fdbb6f95 Release 0.9.9.13 (#560) 2024-02-01 14:52:15 +00:00
amacocianandGitHub 8ee7ed23e4 Release 0.9.9.12 (#558)
* Release 0.9.9.12

* Move injection to injector executable (#554)

* Fix logview missing history (#556)

Closes #526

* Detect unresponsive Guild Wars instance (#557)

Closes #534
Closes #422
2024-01-31 22:02:14 +00:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>amacocian
8d8a78cf88 Bump microsoft/setup-msbuild from 1.3.1 to 1.3.2 (#552)
Bumps [microsoft/setup-msbuild](https://github.com/microsoft/setup-msbuild) from 1.3.1 to 1.3.2.
- [Release notes](https://github.com/microsoft/setup-msbuild/releases)
- [Changelog](https://github.com/microsoft/setup-msbuild/blob/main/building-release.md)
- [Commits](https://github.com/microsoft/setup-msbuild/compare/v1.3.1...v1.3.2)

---
updated-dependencies:
- dependency-name: microsoft/setup-msbuild
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Macocian Alexandru Victor <amacocian@yahoo.com>
2024-01-29 17:06:36 +01:00
amacocianandGitHub f0652231cb Fix minimap mouse interaction (#553)
* Rework minimap renderer

* Fix dragging

* Fix mouse over detection
2024-01-29 15:50:32 +00:00
amacocianandGitHub d9aa760256 Release 0.9.9.10 (#550)
* Release 0.9.9.10
Option to rotate minimap with the camera
Fix launch button not disabling properly when launching
Remove minimap id overlay
Closes #549
Closes #533
Closes #532

* Increment version

* Increment Daybreak.GWCA version
2024-01-28 14:56:51 +00:00
amacocianandGitHub a65f375255 Release 0.9.9.9 (#548)
Closes #547
Closes #546
Closes #536
2024-01-27 19:34:47 +00:00
amacocianandGitHub 3ee58e2711 Release 0.9.9.8 (#545)
* Synchronize browser history

* Intercept mouse button navigation
Closes #535
Closes #544
2024-01-26 14:24:32 +01:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>amacocian
9b3b083aec Bump MSTest.TestAdapter from 3.1.1 to 3.2.0 (#542)
Bumps [MSTest.TestAdapter](https://github.com/microsoft/testfx) from 3.1.1 to 3.2.0.
- [Release notes](https://github.com/microsoft/testfx/releases)
- [Changelog](https://github.com/microsoft/testfx/blob/main/docs/Changelog.md)
- [Commits](https://github.com/microsoft/testfx/compare/v3.1.1...v.3.2.0)

---
updated-dependencies:
- dependency-name: MSTest.TestAdapter
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Signed-off-by: Macocian Alexandru Victor <amacocian@yahoo.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Macocian Alexandru Victor <amacocian@yahoo.com>
2024-01-26 13:09:38 +01:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
ecb9b8f172 Bump MSTest.TestFramework from 3.1.1 to 3.2.0 (#543)
Bumps [MSTest.TestFramework](https://github.com/microsoft/testfx) from 3.1.1 to 3.2.0.
- [Release notes](https://github.com/microsoft/testfx/releases)
- [Changelog](https://github.com/microsoft/testfx/blob/main/docs/Changelog.md)
- [Commits](https://github.com/microsoft/testfx/compare/v3.1.1...v.3.2.0)

---
updated-dependencies:
- dependency-name: MSTest.TestFramework
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-01-26 13:08:22 +01:00
amacocianandGitHub 5765c27240 Release 0.9.9.7 (#541)
* Introduce support for browser extensions

* Setup ublock origin

* Release 0.9.9.7 branch

* Automatically backup settings
Closes #539

* Compare settings with remote
2024-01-23 18:25:31 +00:00
amacocianandGitHub c53456112a Update GWCA (#538)
Fix inventory bug
2024-01-20 10:19:46 +00:00
amacocianandGitHub ccea21886e Release 0.9.9.5 (#531)
* Remove character name from credentials
Update vulnerable packages

* Update SharpCompress
2024-01-17 20:00:12 +00:00
amacocianandGitHub 37be8de5ac Update httplib (#529)
Fix missing data when player has no titles
Fix http requests stalling
2024-01-15 19:20:55 +00:00
amacocianandGitHub 107e43a15f Update GWCA (#528) 2024-01-15 13:34:41 +00:00
1237 changed files with 28590 additions and 56591 deletions
+46 -22
View File
@@ -46,28 +46,25 @@ jobs:
run: |
$changeLog = git log --no-merges --pretty="%h - %s (%an)<br />" ${{ env.LatestReleaseTag }}..HEAD
echo "::set-env name=Changelog::$changeLog"
mkdir Publish
echo $changeLog > .\Publish\changelog.txt
env:
LatestReleaseTag: ${{steps.getLatestTag.outputs.tag}}
- name: Install .NET Core
uses: actions/setup-dotnet@v4
with:
dotnet-version: '8.x'
dotnet-version: '9.x'
- name: Setup MSBuild.exe
uses: microsoft/setup-msbuild@v1.3.1
- name: Fetch submodules
run: |
git submodule update --init --recursive
- name: Build CMake Files
run: cmake -S . -B build -A Win32
uses: microsoft/setup-msbuild@v2.0.0
- name: Setup project secrets
run: |
dotnet user-secrets --project Daybreak\Daybreak.csproj set AadApplicationId "${{ secrets.AadApplicationId }}"
dotnet user-secrets --project Daybreak\Daybreak.csproj set AadTenantId "${{ secrets.AadTenantId }}"
dotnet user-secrets --project Daybreak\Daybreak.csproj set ApmApiKey "${{ secrets.ApmApiKey }}"
dotnet user-secrets --project Daybreak\Daybreak.csproj set ApmUri "${{ secrets.ApmUri }}"
- name: Restore project
run: msbuild $env:Solution_Path /t:Restore /p:Configuration=$env:Configuration /p:RuntimeIdentifier=$env:RuntimeIdentifier --property:SolutionDir=$GITHUB_WORKSPACE
@@ -87,22 +84,43 @@ jobs:
env:
RuntimeIdentifier: win-${{ matrix.targetplatform }}
- name: Create publish installer files
run: dotnet publish .\Daybreak.Installer\Daybreak.Installer.csproj -c $env:Configuration -r $env:RuntimeIdentifier --property:SolutionDir=$env:GITHUB_WORKSPACE -p:PublishReadyToRun=true -p:PublishSingleFile=true --self-contained true -o .\Publish
env:
RuntimeIdentifier: win-${{ matrix.targetplatform }}
- name: Create publish extractor files
run: dotnet publish .\Daybreak.7ZipExtractor\Daybreak.7ZipExtractor.csproj -c $env:Configuration -r $env:RuntimeIdentifier --property:SolutionDir=$env:GITHUB_WORKSPACE -p:PublishReadyToRun=true -p:PublishSingleFile=false --self-contained true -o .\Publish
env:
RuntimeIdentifier: win-${{ matrix.targetplatform }}
- name: Pack publish files
run: |
Write-Host $env
.\Scripts\BuildRelease.ps1 -version $env:Version
shell: pwsh
- name: Submit virustotal scan
shell: pwsh
run: |
cd Publish
$apiKey = "${{ secrets.VIRUSTOTAL_APIKEY }}"
$archiveName = "daybreakv${{ env.Version }}.zip"
$uploadUrlResponse = curl https://www.virustotal.com/api/v3/files/upload_url -H x-apikey:"$apiKey"
$uploadUrlResponse
$uploadUrlJson = $uploadUrlResponse | ConvertFrom-Json
$uploadUrl = $uploadUrlJson.data
$uploadResponse = curl --request POST --header x-apikey:"$apiKey" --header 'content-type: multipart/form-data' --form file="@$archiveName" --url $uploadUrl
$uploadResponse
$uploadResponseJson = $uploadResponse | ConvertFrom-Json
$resultsUrl = "https://www.virustotal.com/api/v3/analyses/$($uploadResponseJson.data.id)"
$resultsUrl
# Poll the results until the status is "completed"
$status = ""
while ($status -ne "completed") {
Start-Sleep -Seconds 10 # Wait for 10 seconds between checks
$results = curl --header x-apikey:"$apiKey" --url $resultsUrl
$results
$resultsJson = $results | ConvertFrom-Json
$status = $resultsJson.data.attributes.status
Write-Host "Current status: $status"
}
$resultsJson.data.attributes.results | ConvertTo-Json | Out-File -FilePath "VirusTotal.Results.json" -Encoding UTF8
$resultsJson.data.attributes.stats | ConvertTo-Json | Out-File -FilePath "VirusTotal.Stats.json" -Encoding UTF8
$stats = $resultsJson.data.attributes.stats
echo "::set-env name=VirusTotalStats::$stats"
- name: Publish blob files
run: |
Write-Host $env
@@ -110,15 +128,21 @@ jobs:
shell: pwsh
- name: Create release draft
uses: Xotl/cool-github-releases@v1.1.8
uses: Xotl/cool-github-releases@v1.1.10
with:
mode: update
tag_name: v${{ env.Version }}
release_name: Daybreak v${{ env.Version }}
assets: .\Publish\daybreakv${{ env.Version }}.zip
assets: .\Publish\daybreakv${{ env.Version }}.zip;.\Publish\VirusTotal.Results.json;.\Publish\VirusTotal.Stats.json
github_token: ${{ env.GITHUB_TOKEN }}
replace_assets: true
body_mrkdwn: ${{ env.Changelog }}
body_mrkdwn: |
${{ env.Changelog }}
** VirustTotal Stats: **
```
${{ env.VirusTotalStats }}
```
isDraft: true
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+3 -12
View File
@@ -38,21 +38,12 @@ jobs:
- name: Install .NET Core
uses: actions/setup-dotnet@v4
with:
dotnet-version: '8.x'
dotnet-version: '9.x'
architecture: x86
- name: Setup MSBuild.exe
uses: microsoft/setup-msbuild@v1.3.1
uses: microsoft/setup-msbuild@v2.0.0
- name: Fetch submodules
run: |
git submodule update --init --recursive
- name: Build CMake Files
run: cmake -S . -B build -A Win32
- name: Execute Unit Tests
run: dotnet test $env:Test_Project_Path --property:SolutionDir=$env:GITHUB_WORKSPACE
- name: Restore the Wpf application to populate the obj folder
run: msbuild $env:Solution_Path /t:Restore /p:Configuration=$env:Configuration /p:RuntimeIdentifier=$env:RuntimeIdentifier --property:SolutionDir=$env:GITHUB_WORKSPACE
env:
+1 -5
View File
@@ -49,10 +49,6 @@ jobs:
with:
fetch-depth: 0
- name: Fetch submodules
run: |
git submodule update --init --recursive
# Initializes the CodeQL tools for scanning.
- name: Initialize CodeQL
uses: github/codeql-action/init@v3
@@ -72,7 +68,7 @@ jobs:
- name: Install .NET Core
uses: actions/setup-dotnet@v4
with:
dotnet-version: '8.x'
dotnet-version: '9.x'
#- name: Autobuild
# uses: github/codeql-action/autobuild@v2
+1 -8
View File
@@ -32,19 +32,12 @@ jobs:
- name: Install .NET Core
uses: actions/setup-dotnet@v4
with:
dotnet-version: '8.x'
dotnet-version: '9.x'
- name: Get Latest Tag
id: getLatestTag
uses: WyriHaximus/github-action-get-previous-tag@v1
- name: Fetch submodules
run: |
git submodule update --init --recursive
- name: Build CMake Files
run: cmake -S . -B build -A Win32
- name: Build Daybreak project
run: dotnet build Daybreak -c $env:Configuration --property:SolutionDir=$env:GITHUB_WORKSPACE
+3 -1
View File
@@ -362,4 +362,6 @@ MigrationBackup/
.ionide/
# Fody - auto-generated XML schema
FodyWeavers.xsd
FodyWeavers.xsd
.idea
-3
View File
@@ -1,3 +0,0 @@
[submodule "GWCA"]
path = GWCA
url = https://github.com/gwdevhub/GWCA
-16
View File
@@ -1,16 +0,0 @@
# CMakeList.txt : Top-level CMake project file, do global configuration
# and include sub-projects here.
#
cmake_minimum_required (VERSION 3.8)
# Enable Hot Reload for MSVC compilers if supported.
if (POLICY CMP0141)
cmake_policy(SET CMP0141 NEW)
set(CMAKE_MSVC_DEBUG_INFORMATION_FORMAT "$<IF:$<AND:$<C_COMPILER_ID:MSVC>,$<CXX_COMPILER_ID:MSVC>>,$<$<CONFIG:Debug,RelWithDebInfo>:EditAndContinue>,$<$<CONFIG:Debug,RelWithDebInfo>:ProgramDatabase>>")
endif()
project ("Daybreak")
# Include sub-projects.
add_subdirectory ("Daybreak.GWCA")
add_subdirectory ("GWCA")
-42
View File
@@ -1,42 +0,0 @@
{
"version": 3,
"configurePresets": [
{
"name": "windows-base",
"hidden": true,
"generator": "Ninja",
"binaryDir": "${sourceDir}/build/${presetName}",
"installDir": "${sourceDir}/install/${presetName}",
"cacheVariables": {
"CMAKE_C_COMPILER": "cl.exe",
"CMAKE_CXX_COMPILER": "cl.exe",
"CMAKE_CXX_FLAGS": "/EHa"
},
"condition": {
"type": "equals",
"lhs": "${hostSystemName}",
"rhs": "Windows"
}
},
{
"name": "x86-debug",
"displayName": "x86 Debug",
"inherits": "windows-base",
"architecture": {
"value": "x86",
"strategy": "external"
},
"cacheVariables": {
"CMAKE_BUILD_TYPE": "Debug"
}
},
{
"name": "x86-release",
"displayName": "x86 Release",
"inherits": "x86-debug",
"cacheVariables": {
"CMAKE_BUILD_TYPE": "Release"
}
}
]
}
@@ -2,15 +2,14 @@
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net8.0</TargetFramework>
<TargetFramework>net9.0</TargetFramework>
<RootNamespace>Daybreak._7ZipExtractor</RootNamespace>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<Platforms>AnyCPU;x64</Platforms>
<Platforms>x64</Platforms>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="SharpCompress" Version="0.35.0" />
<PackageReference Include="SharpCompress" />
</ItemGroup>
</Project>
+5
View File
@@ -21,6 +21,11 @@ var reader = archive.ExtractAllEntries();
while (reader.MoveToNextEntry())
{
var entry = reader.Entry;
if (entry.Key is null)
{
continue;
}
if (entry.IsDirectory)
{
var directoryName = Path.Combine(Path.GetFullPath(destinationDirectory), entry.Key);
+11
View File
@@ -0,0 +1,11 @@
namespace Daybreak.API.Configuration;
public static class BuildInfo
{
public const string Configuration =
#if DEBUG
"Debug";
#else
"Release";
#endif
}
@@ -0,0 +1,31 @@
namespace Daybreak.API.Configuration;
public static class WebApplicationBuilderExtensions
{
public static WebApplicationBuilder WithConfiguration(this WebApplicationBuilder builder)
{
var config = BuildInfo.Configuration;
var appSettingsStream = GetManifestResourceStream($"Daybreak.API.Configuration.appsettings.json") ?? throw new InvalidOperationException("Failed to load appsettings.json");
var appSettingsConfigStream = GetManifestResourceStream($"Daybreak.API.Configuration.appsettings.{config}.json") ?? throw new InvalidOperationException($"Failed to load appsettings.{config}.json");
builder.Configuration
.AddJsonStream(appSettingsStream)
.AddJsonStream(appSettingsConfigStream)
.AddEnvironmentVariables();
builder.WebHost.UseSetting(WebHostDefaults.PreventHostingStartupKey, "true");
return builder;
}
private static Stream? GetManifestResourceStream(string name)
{
var assembly = typeof(WebApplicationBuilderExtensions).Assembly;
var resourceName = assembly.GetManifestResourceNames()
.FirstOrDefault(n => n.EndsWith(name, StringComparison.OrdinalIgnoreCase));
if (resourceName is null)
{
return default;
}
return assembly.GetManifestResourceStream(resourceName);
}
}
@@ -0,0 +1,2 @@
{
}
@@ -0,0 +1,10 @@
{
"Logging": {
"MinimumLevel": {
"Default": "Debug",
"Override": {
"Microsoft": "Warning"
}
}
}
}
@@ -0,0 +1,2 @@
{
}
@@ -0,0 +1,13 @@
{
"Logging": {
"MinimumLevel": {
"Default": "Debug",
"Override": {
"Microsoft": "Debug"
}
}
},
"Correlation": {
"Header": "X-Correlation-Vector"
}
}
@@ -0,0 +1,51 @@
using Daybreak.API.Interop.GuildWars;
using Daybreak.API.Services;
using Daybreak.Shared.Models.Api;
using Microsoft.AspNetCore.Mvc;
using Net.Sdk.Web;
using System.Core.Extensions;
namespace Daybreak.API.Controllers.Rest;
[GenerateController("api/v1/rest/character-select")]
[Tags("Character Select")]
public sealed class CharacterSelectController(
CharacterSelectService characterSelectService)
{
private readonly CharacterSelectService characterSelectService = characterSelectService.ThrowIfNull();
[GenerateGet]
[EndpointName("GetCharacterSelectInformation")]
[EndpointSummary("Get Character Select Information")]
[EndpointDescription("Get the character select information for the current player. Returns a serialized CharacterSelectInformation object")]
[ProducesResponseType<CharacterSelectInformation>(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status503ServiceUnavailable)]
public async Task<IResult> GetCharacterSelectInformation(
CancellationToken cancellationToken)
{
var characterSelectInfo = await this.characterSelectService.GetCharacterSelectInformation(cancellationToken);
return characterSelectInfo is null ?
Results.StatusCode(StatusCodes.Status503ServiceUnavailable) :
Results.Ok(characterSelectInfo);
}
[GeneratePost("{identifier}")]
[EndpointName("ChangeCharacter")]
[EndpointSummary("Change Character by identifier")]
[EndpointDescription("Logs out from the current character and tries to log in with the desired character. Identifier can be either uuid or character name")]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status500InternalServerError)]
[ProducesResponseType(StatusCodes.Status503ServiceUnavailable)]
public async Task<IResult> ChangeCharacter(
[FromRoute] string identifier,
CancellationToken cancellationToken)
{
var isUuid = Uuid.TryParse(identifier, out _);
var result = isUuid
? await this.characterSelectService.ChangeCharacterByUuid(identifier, cancellationToken)
: await this.characterSelectService.ChangeCharacterByName(identifier, cancellationToken);
return result ?
Results.Ok() :
Results.StatusCode(StatusCodes.Status500InternalServerError);
}
}
@@ -0,0 +1,30 @@
using Daybreak.API.Services;
using Daybreak.Shared.Models.Api;
using Microsoft.AspNetCore.Mvc;
using Net.Sdk.Web;
using System.Core.Extensions;
namespace Daybreak.API.Controllers.Rest;
[GenerateController("api/v1/rest/login")]
[Tags("Login")]
public sealed class LoginController(
LoginService loginService)
{
private readonly LoginService loginService = loginService.ThrowIfNull();
[GenerateGet]
[EndpointName("GetLoginInfo")]
[EndpointSummary("Get Login Information")]
[EndpointDescription("Get the current login information. Returns a serialized LoginInfo object")]
[ProducesResponseType<LoginInfo>(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status503ServiceUnavailable)]
public async Task<IResult> GetLoginInfo(
CancellationToken cancellationToken)
{
var loginInfo = await this.loginService.GetLoginInformation(cancellationToken);
return loginInfo is null ?
Results.StatusCode(StatusCodes.Status503ServiceUnavailable) :
Results.Ok(loginInfo);
}
}
@@ -0,0 +1,114 @@
using Daybreak.API.Services;
using Daybreak.Shared.Models.Api;
using Microsoft.AspNetCore.Mvc;
using Net.Sdk.Web;
using System.Core.Extensions;
namespace Daybreak.API.Controllers.Rest;
[GenerateController("api/v1/rest/main-player")]
[Tags("Main Player")]
public sealed class MainPlayerController(
MainPlayerService mainPlayerService,
ILogger<MainPlayerController> logger)
{
private readonly MainPlayerService mainPlayerService = mainPlayerService.ThrowIfNull();
private readonly ILogger<MainPlayerController> logger = logger.ThrowIfNull();
[GenerateGet("state")]
[EndpointName("GetMainPlayerState")]
[EndpointSummary("Get the current MainPlayerState")]
[EndpointDescription("Get the current MainPlayerState. Returns a json serialized MainPlayerState object. You can use the websocket endpoint to subscribe to MainPlayerState updates on each game thread proc")]
[ProducesResponseType<MainPlayerState>(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status503ServiceUnavailable)]
public async Task<IResult> GetMainPlayerState(CancellationToken cancellationToken)
{
var mainPlayerState = await this.mainPlayerService.GetMainPlayerState(cancellationToken);
return mainPlayerState is not null ? Results.Ok(mainPlayerState) : Results.StatusCode(StatusCodes.Status503ServiceUnavailable);
}
[GenerateGet("quest-log")]
[EndpointName("GetQuestLog")]
[EndpointSummary("Get the current Quest Log")]
[EndpointDescription("Get the current Quest Log. Returns a json serialized QuestLogInformation object")]
[ProducesResponseType<QuestLogInformation>(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status503ServiceUnavailable)]
public async Task<IResult> GetQuestLog(CancellationToken cancellationToken)
{
var questLog = await this.mainPlayerService.GetQuestLog(cancellationToken);
return questLog is not null ? Results.Ok(questLog) : Results.StatusCode(StatusCodes.Status503ServiceUnavailable);
}
[GenerateGet("info")]
[EndpointName("GetMainPlayerInformation")]
[EndpointSummary("Get the current MainPlayerInformation")]
[EndpointDescription("Get the current MainPlayerInformation. Returns a json serialized MainPlayerInformation object")]
[ProducesResponseType<MainPlayerInformation>(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status503ServiceUnavailable)]
public async Task<IResult> GetMainPlayerInformation(CancellationToken cancellationToken)
{
var mainPlayerInformation = await this.mainPlayerService.GetMainPlayerInformation(cancellationToken);
return mainPlayerInformation is not null ? Results.Ok(mainPlayerInformation) : Results.StatusCode(StatusCodes.Status503ServiceUnavailable);
}
[GenerateGet("build")]
[EndpointName("GetMainPlayerBuild")]
[EndpointSummary("Get the current build")]
[EndpointDescription("Get the current build. Returns a json serialized BuildEntry object")]
[ProducesResponseType<BuildEntry>(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status503ServiceUnavailable)]
public async Task<IResult> GetMainPlayerBuild(CancellationToken cancellationToken)
{
var build = await this.mainPlayerService.GetCurrentBuild(cancellationToken);
return build is not null ? Results.Ok(build) : Results.StatusCode(StatusCodes.Status503ServiceUnavailable);
}
[GeneratePost("build")]
[EndpointName("SetMainPlayerBuild")]
[EndpointSummary("Set the current build")]
[EndpointDescription("Set the current build. Returns 200 is the operation has succeeded")]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status400BadRequest)]
[ProducesResponseType(StatusCodes.Status503ServiceUnavailable)]
public async Task<IResult> SetMainPlayerBuild([FromQuery(Name = "code")] string? code, CancellationToken cancellationToken)
{
var result = await this.mainPlayerService.SetCurrentBuild(code ?? string.Empty, cancellationToken);
return result ? Results.Ok() : Results.StatusCode(StatusCodes.Status503ServiceUnavailable);
}
[GenerateGet("instance-info")]
[EndpointName("GetMainPlayerInstanceInfo")]
[EndpointSummary("Get the current instance info")]
[EndpointDescription("Get the current instance info. Returns a json serialized MainPlayerInstanceInfo object")]
[ProducesResponseType<InstanceInfo>(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status503ServiceUnavailable)]
public async Task<IResult> GetMainPlayerInstanceInfo(CancellationToken cancellationToken)
{
var instanceInfo = await this.mainPlayerService.GetMainPlayerInstance(cancellationToken);
return instanceInfo is not null ? Results.Ok(instanceInfo) : Results.StatusCode(StatusCodes.Status503ServiceUnavailable);
}
[GenerateGet("title")]
[EndpointName("GetMainPlayerTitleInfo")]
[EndpointSummary("Get the current title info")]
[EndpointDescription("Get the current title info. Returns a json serialized TitleInfo object")]
[ProducesResponseType<TitleInfo>(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status503ServiceUnavailable)]
public async Task<IResult> GetMainPlayerTitleInfo(CancellationToken cancellationToken)
{
var titleInfo = await this.mainPlayerService.GetTitleInfo(cancellationToken);
return titleInfo is not null ? Results.Ok(titleInfo) : Results.StatusCode(StatusCodes.Status503ServiceUnavailable);
}
[GenerateGet("build-context")]
[EndpointName("GetMainPlayerBuildContext")]
[EndpointSummary("Get the current build context")]
[EndpointDescription("Get the current build context. Returns a json serialized MainPlayerBuildContext object")]
[ProducesResponseType<BuildEntry>(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status503ServiceUnavailable)]
public async Task<IResult> GetMainPlayerBuildContext(CancellationToken cancellationToken)
{
var context = await this.mainPlayerService.GetMainPlayerBuildContext(cancellationToken);
return context is not null ? Results.Ok(context) : Results.StatusCode(StatusCodes.Status503ServiceUnavailable);
}
}
@@ -0,0 +1,38 @@
using Daybreak.API.Services;
using Daybreak.Shared.Models.Api;
using Microsoft.AspNetCore.Mvc;
using Net.Sdk.Web;
namespace Daybreak.API.Controllers.Rest;
[GenerateController("api/v1/rest/party")]
[Tags("Party")]
public sealed class PartyController(PartyService partyService)
{
private readonly PartyService partyService = partyService;
[GenerateGet("loadout")]
[EndpointName("GetPartyLoadout")]
[EndpointSummary("Get the current party loadout")]
[EndpointDescription("Get the current party loadout. Returns a json serialized PartyLoadout object")]
[ProducesResponseType<PartyLoadout>(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status503ServiceUnavailable)]
public async Task<IResult> GetPartyLoadout(
CancellationToken cancellationToken)
{
var partyLoadout = await this.partyService.GetPartyLoadout(cancellationToken);
return partyLoadout is not null ? Results.Ok(partyLoadout) : Results.StatusCode(StatusCodes.Status503ServiceUnavailable);
}
[GeneratePost("loadout")]
[EndpointName("SetPartyLoadout")]
[EndpointSummary("Set the current party loadout")]
[EndpointDescription("Set the current party loadout")]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status503ServiceUnavailable)]
public async Task<IResult> SetPartyLoadout([FromBody] PartyLoadout partyLoadout, CancellationToken cancellationToken)
{
var result = await this.partyService.SetPartyLoadout(partyLoadout, cancellationToken);
return result ? Results.Ok() : Results.StatusCode(StatusCodes.Status503ServiceUnavailable);
}
}
@@ -0,0 +1,67 @@
using Daybreak.API.Models;
using Daybreak.API.Services;
using Daybreak.API.WebSockets;
using Daybreak.Shared.Models.Api;
using System.Buffers;
using System.Core.Extensions;
using System.Extensions.Core;
using System.Net.WebSockets;
namespace Daybreak.API.Controllers.WebSocket.MainPlayer;
public sealed class MainPlayerStateRoute(
MainPlayerService mainPlayerStateService,
ChatService chatService,
ILogger<MainPlayerStateRoute> logger)
: UpdateWebSocketRoute
{
private const int DefaultFrequency = 1000;
private readonly MainPlayerService mainPlayerStateService = mainPlayerStateService.ThrowIfNull();
private readonly ChatService chatService = chatService.ThrowIfNull();
private readonly ILogger<MainPlayerStateRoute> logger = logger.ThrowIfNull();
private CallbackRegistration? mainPlayerStateRegistration;
protected override int BufferSize => 256;
public override Task ExecuteAsync(WebSocketMessageType type, ReadOnlySequence<byte> data, CancellationToken cancellationToken)
{
return Task.CompletedTask;
}
public override async Task SocketAccepted(CancellationToken cancellationToken)
{
var scopedLogger = this.logger.CreateScopedLogger();
if (!int.TryParse(this.Context?.Request.Query["f"].FirstOrDefault(), out var freq))
{
freq = DefaultFrequency;
}
var freqTimeSpan = TimeSpan.FromMilliseconds(freq);
scopedLogger.LogInformation("WebSocket accepted {id} with frequency {frequency}", this.Context?.Connection.Id ?? string.Empty, freqTimeSpan);
this.mainPlayerStateRegistration = this.mainPlayerStateService.RegisterMainStateConsumer(freqTimeSpan, this.SendUpdate);
await this.chatService.AddMessageAsync(
message: $"{nameof(MainPlayerState)} subscriber added: {this.Context?.Connection.RemoteIpAddress?.ToString()}:{this.Context?.Connection.RemotePort} with frequency {freq}ms",
sender: "Daybreak.API",
channel: Channel.Whisper,
cancellationToken: cancellationToken);
}
public override async Task SocketClosed()
{
var scopedLogger = this.logger.CreateScopedLogger();
this.mainPlayerStateRegistration?.Dispose();
this.mainPlayerStateRegistration = default;
scopedLogger.LogInformation("WebSocket closed {id}", this.Context?.Connection.Id ?? string.Empty);
await this.chatService.AddMessageAsync(
message: $"{nameof(MainPlayerState)} subscriber removed: {this.Context?.Connection.RemoteIpAddress?.ToString()}:{this.Context?.Connection.RemotePort}",
sender: "Daybreak.API",
channel: Channel.Whisper,
cancellationToken: CancellationToken.None);
}
}
@@ -0,0 +1,19 @@
using Daybreak.API.Interop;
using System.Text.Json;
using System.Text.Json.Serialization;
namespace Daybreak.API.Converters;
public sealed class PointerValueConverter : JsonConverter<PointerValue>
{
public override PointerValue Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
{
var s = reader.GetString() ?? throw new JsonException("Expected string");
return PointerValue.Parse(s);
}
public override void Write(Utf8JsonWriter writer, PointerValue value, JsonSerializerOptions options)
{
writer.WriteStringValue(value.ToString());
}
}
+64
View File
@@ -0,0 +1,64 @@
<Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup>
<TargetFramework>net9.0-windows</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<OutputType>Library</OutputType>
<RuntimeIdentifier>win-x86</RuntimeIdentifier>
<StripSymbols>true</StripSymbols>
<InteropExports>true</InteropExports>
<EnableRequestDelegateGenerator>true</EnableRequestDelegateGenerator>
<PublishAot>true</PublishAot>
<SelfContained>true</SelfContained>
<AspNetCoreHostingModel>OutOfProcess</AspNetCoreHostingModel>
<UseAppHost>false</UseAppHost>
<EnableIISSupport>false</EnableIISSupport>
<InvariantGlobalization>true</InvariantGlobalization>
<Platforms>x86</Platforms>
</PropertyGroup>
<PropertyGroup>
<!--Ignore trim warnings. NativeAOT support is still experimental so it should be tested with a Publish build generated from Daybreak build-->
<NoWarn>IL2104</NoWarn>
<NoWarn>IL3053</NoWarn>
<!--MinHook.NET does not contain sources for .NET, only .Net Framework. This has no impact on the functionality of the project-->
<NoWarn>NU1701</NoWarn>
</PropertyGroup>
<ItemGroup>
<EmbeddedResource Include="Configuration\appsettings.Debug.json" />
<EmbeddedResource Include="Configuration\appsettings.json" />
<EmbeddedResource Include="Configuration\appsettings.Release.json" />
</ItemGroup>
<ItemGroup>
<RdXmlFile Include="rd.xml" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.OpenApi" />
<PackageReference Include="MinHook.NET" />
<PackageReference Include="Net.Sdk.Web.Extensions" />
<PackageReference Include="Net.Sdk.Web.Extensions.SourceGenerators" />
<PackageReference Include="PeNet" />
<PackageReference Include="Serilog" />
<PackageReference Include="Serilog.Extensions.Logging" />
<PackageReference Include="Serilog.Settings.Configuration" />
<PackageReference Include="Serilog.Sinks.Console" />
<PackageReference Include="Serilog.Sinks.File" />
<PackageReference Include="Swashbuckle.AspNetCore" />
<PackageReference Include="Swashbuckle.AspNetCore.Swagger" />
<PackageReference Include="Swashbuckle.AspNetCore.SwaggerGen" />
<PackageReference Include="Swashbuckle.AspNetCore.SwaggerUI" />
<PackageReference Include="System.Private.Uri" />
<PackageReference Include="SystemExtensions.NetStandard.Generators" PrivateAssets="all" />
<PackageReference Include="ZLinq" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\Daybreak.Shared\Daybreak.Shared.csproj" />
</ItemGroup>
</Project>
+145
View File
@@ -0,0 +1,145 @@
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using System.Extensions.Core;
using System.Net.NetworkInformation;
using System.Runtime.InteropServices;
using Daybreak.API.Configuration;
using Daybreak.API.Extensions;
using Daybreak.API.Health;
using Daybreak.API.Hosting;
using Daybreak.API.Logging;
using Daybreak.API.Serialization;
using Daybreak.API.Services;
using Daybreak.API.Swagger;
using Daybreak.API.WebSockets;
using Microsoft.Extensions.Diagnostics.HealthChecks;
using Net.Sdk.Web;
namespace Daybreak.API;
public class EntryPoint
{
private const int StartPort = 5080;
private static readonly TimeSpan InitializationTimeout = TimeSpan.FromSeconds(5);
private static readonly CancellationTokenSource CancellationTokenSource = new();
[UnmanagedCallersOnly(EntryPoint = "ThreadInit"), STAThread]
[RequiresUnreferencedCode("The handler uses a static method that gets referenced, so there's no unreferenced code to worry about")]
[RequiresDynamicCode("The handler uses a static method, so there's no dynamic code to worry about")]
public static int ThreadInit(IntPtr _, int __)
{
Environment.SetEnvironmentVariable("ASPNETCORE_HOSTINGSTARTUPASSEMBLIES", null, EnvironmentVariableTarget.Process);
Environment.SetEnvironmentVariable("ASPNETCORE_PREVENTHOSTINGSTARTUP", "true", EnvironmentVariableTarget.Process);
#if DEBUG
ConsoleExtensions.AllocateAnsiConsole();
#endif
var port = FindAvailablePort(StartPort);
var app = CreateApplication(port);
var runTask = Task.Run(() => StartServer(app), CancellationTokenSource.Token);
var scopedLogger = app.Services.GetRequiredService<ILogger<EntryPoint>>().CreateScopedLogger();
var healthCheck = app.Services.GetRequiredService<HealthCheckService>();
var sw = Stopwatch.StartNew();
var healthy = false;
while (sw.Elapsed < InitializationTimeout)
{
var status = Task.Run(() => healthCheck.CheckHealthAsync()).Result;
scopedLogger.LogWarning("HealthCheck status: {status} in {duration}. Report: {report}", status.Status, status.TotalDuration, string.Join("\n", status.Entries.Select(e => $"{e.Key}: {e.Value.Status}")));
if (status.Status is HealthStatus.Healthy)
{
healthy = true;
break;
}
else
{
Thread.Sleep(100);
}
}
if (healthy)
{
scopedLogger.LogInformation("Daybreak API is healthy on port {port}. Initialization succeeded in {duration}", port, sw.Elapsed);
return port;
}
else
{
scopedLogger.LogError("Daybreak API failed to initialize in {duration}", sw.Elapsed);
CancellationTokenSource.Cancel();
return -1;
}
}
[RequiresUnreferencedCode("The handler uses a static method that gets referenced, so there's no unreferenced code to worry about")]
[RequiresDynamicCode("The handler uses a static method, so there's no dynamic code to worry about")]
private static WebApplication CreateApplication(int port)
{
var builder = WebApplication.CreateBuilder()
.WithConfiguration()
.WithHosting(port)
.WithSwagger()
.WithSerializationContext()
.WithLogging()
.WithDaybreakServices()
.WithWebSocketRoutes()
.WithRoutes()
.WithHealthChecks();
builder.Services.AddOpenApi();
var app = builder.Build();
app.UseWebSockets(new WebSocketOptions { KeepAliveInterval = TimeSpan.FromSeconds(30) });
return app
.UseHealthChecks()
.UseLogging()
.UseRoutes()
.UseWebSocketRoutes()
.UseSwaggerWithUI();
}
private static async Task StartServer(WebApplication app)
{
try
{
await app.RunAsync();
}
catch (Exception ex)
{
Console.Error.WriteLine(ex);
}
}
private static int FindAvailablePort(int startPort)
{
var port = startPort;
while (!IsPortAvailable(port))
{
port++;
if (port > 65535 || port < 0)
{
return -1;
}
}
return port;
}
private static bool IsPortAvailable(int port)
{
var ipProperties = IPGlobalProperties.GetIPGlobalProperties();
var tcpListeners = ipProperties.GetActiveTcpListeners();
if (tcpListeners.Any(endpoint => endpoint.Port == port))
{
return false;
}
var tcpConnections = ipProperties.GetActiveTcpConnections();
if (tcpConnections.Any(conn => conn.LocalEndPoint.Port == port))
{
return false;
}
return true;
}
}
@@ -0,0 +1,17 @@
using Daybreak.API.Interop.GuildWars;
using Daybreak.Shared.Models.Api;
using ZLinq;
namespace Daybreak.API.Extensions;
public static class AttributeContextExtensions
{
public static List<AttributeEntry> GetAttributeEntryList(this AttributeContext[] attributes)
{
return attributes
.AsValueEnumerable()
.Take(45) // There are only 45 maximum attributes in game
.Where(a => a.LevelBase > 0 && a.Level >= a.LevelBase) // Select only attributes with points in them
.Select(a => new AttributeEntry(a.Id, a.LevelBase, a.Level)).ToList();
}
}
@@ -0,0 +1,19 @@
namespace Daybreak.API.Extensions;
public static class ConsoleExtensions
{
public static void AllocateAnsiConsole()
{
NativeMethods.AllocConsole();
var handle = NativeMethods.GetStdHandle(NativeMethods.STD_OUTPUT_HANDLE);
if (!NativeMethods.GetConsoleMode(handle, out var mode))
{
Console.WriteLine("Failed to get console mode");
}
if (!NativeMethods.SetConsoleMode(handle, mode | NativeMethods.ENABLE_VIRTUAL_TERMINAL_PROCESSING | NativeMethods.ENABLE_PROCESSED_OUTPUT))
{
Console.WriteLine("Failed to enable virtual terminal processing");
}
}
}
@@ -0,0 +1,101 @@
using Daybreak.API.Interop.GuildWars;
using System.Diagnostics.CodeAnalysis;
namespace Daybreak.API.Extensions;
public static unsafe class GameContextExtensions
{
public static bool TryGetPlayerId(
this WrappedPointer<GameContext> gameContext,
[NotNullWhen(true)] out uint? playerId)
{
playerId = 0;
if (gameContext.IsNull ||
gameContext.Pointer->WorldContext is null ||
gameContext.Pointer->WorldContext->PlayerControlledChar is null)
{
return false;
}
playerId = gameContext.Pointer->WorldContext->PlayerControlledChar->AgentId;
return true;
}
public static bool TryGetBuildContext(
this WrappedPointer<GameContext> gameContext,
[NotNullWhen(true)] out GuildWarsArray<SkillbarContext>? skillbars,
[NotNullWhen(true)] out GuildWarsArray<PartyAttribute>? attributes,
[NotNullWhen(true)] out GuildWarsArray<ProfessionsContext>? professions,
[NotNullWhen(true)] out GuildWarsArray<uint>? unlockedSkills)
{
skillbars = default;
attributes = default;
professions = default;
unlockedSkills = default;
if (gameContext.IsNull ||
gameContext.Pointer->WorldContext is null)
{
return false;
}
skillbars = gameContext.Pointer->WorldContext->Skillbars;
attributes = gameContext.Pointer->WorldContext->Attributes;
professions = gameContext.Pointer->WorldContext->Professions;
unlockedSkills = gameContext.Pointer->WorldContext->UnlockedCharacterSkills;
return true;
}
public static bool TryGetPlayerParty(
this WrappedPointer<GameContext> gameContext,
[NotNullWhen(true)] out uint? partyId,
[NotNullWhen(true)] out GuildWarsArray<PlayerPartyMember>? players,
[NotNullWhen(true)] out GuildWarsArray<HeroPartyMember>? heroes,
[NotNullWhen(true)] out GuildWarsArray<HenchmanPartyMember>? henchmen)
{
partyId = default;
players = default;
heroes = default;
henchmen = default;
if (gameContext.IsNull ||
gameContext.Pointer->PartyContext is null)
{
return false;
}
partyId = gameContext.Pointer->PartyContext->PlayerParty->PartyId;
players = gameContext.Pointer->PartyContext->PlayerParty->Players;
heroes = gameContext.Pointer->PartyContext->PlayerParty->Heroes;
henchmen = gameContext.Pointer->PartyContext->PlayerParty->Henchmen;
return true;
}
public static bool TryGetHeroFlags(
this WrappedPointer<GameContext> gameContext,
[NotNullWhen(true)]out GuildWarsArray<HeroFlag>? heroFlags)
{
heroFlags = default;
if (gameContext.IsNull ||
gameContext.Pointer->WorldContext is null)
{
return false;
}
heroFlags = gameContext.Pointer->WorldContext->HeroFlags;
return true;
}
public static bool TryGetAccountContext(
this WrappedPointer<GameContext> gameContext,
out WrappedPointer<AccountGameContext> accountContext)
{
accountContext = default;
if (gameContext.IsNull ||
gameContext.Pointer->AccountContext is null)
{
return false;
}
accountContext = gameContext.Pointer->AccountContext;
return true;
}
}
+31
View File
@@ -0,0 +1,31 @@
using Daybreak.API.Serialization;
using Daybreak.API.Services.Interop;
using Microsoft.Extensions.Diagnostics.HealthChecks;
using System.Text.Json;
using ZLinq;
namespace Daybreak.API.Health;
public sealed class AddressHealthCheck(IEnumerable<IAddressHealthService> addressHealthServices)
: IHealthCheck
{
private readonly IEnumerable<IAddressHealthService> addressHealthServices = addressHealthServices;
public Task<HealthCheckResult> CheckHealthAsync(HealthCheckContext context, CancellationToken cancellationToken = default)
{
var states = this.addressHealthServices.AsValueEnumerable().SelectMany(addressService => addressService.GetAddressStates());
var healthy = !states.Any(s => s.Address == 0x0);
using var statesArray = states.ToArrayPool();
var serialized = JsonSerializer.SerializeToElement(statesArray.ArraySegment, ApiJsonSerializerContext.Default.ArraySegmentAddressState);
var meta = new Dictionary<string, object>
{
["Count"] = statesArray.Size,
["Invalid"] = states.Count(s => s.Address == 0x0),
["States"] = serialized,
};
return Task.FromResult(healthy
? HealthCheckResult.Healthy("Addresses are initialized", meta)
: HealthCheckResult.Unhealthy("Addresses are not initialized", data: meta));
}
}
+31
View File
@@ -0,0 +1,31 @@
using Daybreak.API.Serialization;
using Daybreak.API.Services.Interop;
using Microsoft.Extensions.Diagnostics.HealthChecks;
using System.Text.Json;
using ZLinq;
namespace Daybreak.API.Health;
public sealed class HooksHealthCheck(IEnumerable<IHookHealthService> hookHealthServices)
: IHealthCheck
{
private readonly IEnumerable<IHookHealthService> hookHealthServices = hookHealthServices;
public Task<HealthCheckResult> CheckHealthAsync(HealthCheckContext context, CancellationToken cancellationToken = default)
{
var states = this.hookHealthServices.AsValueEnumerable().SelectMany(hookService => hookService.GetHookStates());
var healthy = !states.Any(s => !s.Hooked);
var statesArray = states.ToArrayPool();
var serialized = JsonSerializer.SerializeToElement(statesArray.ArraySegment, ApiJsonSerializerContext.Default.ArraySegmentHookState);
var meta = new Dictionary<string, object>
{
["Count"] = statesArray.Size,
["Unhooked"] = states.Count(s => !s.Hooked),
["States"] = serialized,
};
return Task.FromResult(healthy
? HealthCheckResult.Healthy("Hooks are initialized", meta)
: HealthCheckResult.Unhealthy("Hooks are not initialized", data: meta));
}
}
@@ -0,0 +1,12 @@
namespace Daybreak.API.Health;
public static class WebApplicationBuilderExtensions
{
public static WebApplicationBuilder WithHealthChecks(this WebApplicationBuilder builder)
{
builder.Services.AddHealthChecks()
.AddCheck<HooksHealthCheck>(nameof(HooksHealthCheck))
.AddCheck<AddressHealthCheck>(nameof(AddressHealthCheck));
return builder;
}
}
@@ -0,0 +1,48 @@
using Daybreak.API.Models;
using Microsoft.Extensions.Diagnostics.HealthChecks;
using System.Diagnostics.CodeAnalysis;
using System.Text.Json;
using ZLinq;
namespace Daybreak.API.Health;
public static class WebApplicationExtensions
{
[RequiresUnreferencedCode("The handler uses a static method that gets referenced, so there's no unreferenced code to worry about")]
[RequiresDynamicCode("The handler uses a static method, so there's no dynamic code to worry about")]
public static WebApplication UseHealthChecks(this WebApplication app)
{
app.MapGet("/api/v1/rest/health", static async (HealthCheckService healthCheckService, CancellationToken cancellationToken) =>
{
var report = await healthCheckService.CheckHealthAsync(cancellationToken);
var response = new HealthCheckResponse
{
ProcessId = Environment.ProcessId,
Status = report.Status,
TotalDuration = report.TotalDuration,
Entries = report.Entries.ToDictionary(
e => e.Key,
e => new HealthCheckEntryResponse
{
Status = e.Value.Status,
Description = e.Value.Description ?? string.Empty,
Tags = [.. e.Value.Tags],
Data = e.Value.Data
.Where(kvp => kvp.Value is JsonElement je)
.ToDictionary(kvp => kvp.Key,
kvp => (JsonElement)kvp.Value)
})
};
return Results.Json(response);
})
.WithName("Health")
.WithSummary("Service health")
.WithDescription($"Current health status of the service. Returns a serialized object of type {nameof(HealthCheckResponse)}")
.WithTags("Service")
.Produces<HealthCheckResponse>(StatusCodes.Status200OK)
.WithOpenApi();
return app;
}
}
@@ -0,0 +1,11 @@
namespace Daybreak.API.Hosting;
public static class WebApplicationBuilderExtensions
{
public static WebApplicationBuilder WithHosting(this WebApplicationBuilder builder, int port)
{
builder.WebHost.ConfigureKestrel(o => o.ListenAnyIP(port));
return builder;
}
}
+23
View File
@@ -0,0 +1,23 @@
#pragma warning disable CS0436 // Type conflicts with imported type
using System.Extensions;
[assembly: GenerateFixedArray<uint>(Size = 4)]
[assembly: GenerateFixedArray<char>(Size = 0x14)]
[assembly: GenerateFixedArray<char>(Size = 0x40)]
[assembly: GenerateFixedArray<byte>(Size = 0x18)]
[assembly: GenerateFixedArray<char>(Size = 256)]
[assembly: GenerateFixedArray<char>(Size = 32)]
[assembly: GenerateFixedArray<char>(Size = 5)]
[assembly: GenerateFixedArray<char>(Size = 8)]
[assembly: GenerateFixedArray<byte>(Size = 56)]
[assembly: GenerateFixedArray<uint>(Size = 17)]
[assembly: GenerateFixedArray<uint>(Size = 12)]
[assembly: GenerateFixedArray<uint>(Size = 13)]
[assembly: GenerateFixedArray<uint>(Size = 8)]
#pragma warning restore CS0436 // Type conflicts with imported type
namespace Daybreak.API.Interop;
public static class FixedArrays
{
}
+26
View File
@@ -0,0 +1,26 @@
using System.Core.Extensions;
namespace Daybreak.API.Interop;
public sealed class GWAddressCache(Func<nuint> provider)
{
private readonly Func<nuint> provider = provider.ThrowIfNull();
private nuint? cachedAddress;
public nuint? GetAddress()
{
if (this.cachedAddress.HasValue)
{
return this.cachedAddress.Value;
}
var addr = this.provider();
if (addr is 0x0)
{
return null;
}
this.cachedAddress = addr;
return addr;
}
}
+19
View File
@@ -0,0 +1,19 @@
using System.Runtime.InteropServices;
namespace Daybreak.API.Interop;
public sealed class GWDelegateCache<TDelegate>(GWAddressCache cache)
where TDelegate : Delegate
{
public GWAddressCache Cache { get; } = cache;
public TDelegate? GetDelegate()
{
if (this.Cache.GetAddress() is not nuint address)
{
return default;
}
return Marshal.GetDelegateForFunctionPointer<TDelegate>((nint)address);
}
}
+240
View File
@@ -0,0 +1,240 @@
using System.ComponentModel;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
namespace Daybreak.API.Interop;
public abstract class GWFastCall(GWAddressCache target)
{
public readonly struct Void { }
public GWAddressCache Cache { get; } = target;
public abstract void Initialize();
protected unsafe static nint BuildFastCallThunk(nuint target)
{
if (target is 0)
{
return 0;
}
var code = stackalloc byte[13]
{
0x58, // pop eax (ret)
0x59, // pop ecx (ctx)
0x5A, // pop edx (edxVal)
0x5B, // pop ebx (wparam)
0x53, // push ebx (wparam)
0x50, // push eax (ret)
0xB8,0,0,0,0, // mov eax, TARGET
0xFF,0xE0 // jmp eax
}; // 13 bytes total
const int TARGET_OFFSET = 7;
Unsafe.WriteUnaligned(ref code[TARGET_OFFSET], (uint)target);
var mem = NativeMethods.VirtualAlloc(0, 13, NativeMethods.MEM_COMMIT | NativeMethods.MEM_RESERVE, NativeMethods.PAGE_EXECUTE_READWRITE);
if (mem is 0)
{
throw new Win32Exception(Marshal.GetLastWin32Error(), "VirtualAlloc");
}
Buffer.MemoryCopy(code, (void*)mem, 13, 13);
return mem;
}
}
public unsafe sealed class GWFastCall<T1, T2, T3>(GWAddressCache target) : GWFastCall(target)
{
private delegate* unmanaged[Stdcall]<T1, T2, T3> func = null;
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public unsafe T3? Invoke(T1 t1, T2 t2)
{
if (this.func is null)
{
this.Initialize();
}
if (this.func is not null)
{
if (typeof(T3) == typeof(Void))
{
((delegate* unmanaged[Stdcall]<T1, T2, void>)this.func)(t1, t2);
return default;
}
return this.func(t1, t2);
}
return default;
}
public override void Initialize()
{
if (this.Cache.GetAddress() is not nuint addr ||
addr is 0)
{
return;
}
var call = BuildFastCallThunk(addr);
this.func = (delegate* unmanaged[Stdcall]<T1, T2, T3>)call;
}
}
public unsafe sealed class GWFastCall<T1, T2, T3, T4>(GWAddressCache target) : GWFastCall(target)
{
private delegate* unmanaged[Stdcall]<T1, T2, T3, T4> func = null;
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public unsafe T4? Invoke(T1 t1, T2 t2, T3 t3)
{
if (this.func is null)
{
this.Initialize();
}
if (this.func is not null)
{
if (typeof(T4) == typeof(Void))
{
((delegate* unmanaged[Stdcall]<T1, T2, T3, void>)this.func)(t1, t2, t3);
return default;
}
return this.func(t1, t2, t3);
}
return default;
}
public override void Initialize()
{
if (this.Cache.GetAddress() is not nuint addr ||
addr is 0)
{
return;
}
var call = BuildFastCallThunk(addr);
this.func = (delegate* unmanaged[Stdcall]<T1, T2, T3, T4>)call;
}
}
public unsafe sealed class GWFastCall<T1, T2, T3, T4, T5>(GWAddressCache target) : GWFastCall(target)
{
private delegate* unmanaged[Stdcall]<T1, T2, T3, T4, T5> func = null;
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public unsafe T5? Invoke(T1 t1, T2 t2, T3 t3, T4 t4)
{
if (this.func is null)
{
this.Initialize();
}
if (this.func is not null)
{
if (typeof(T5) == typeof(Void))
{
((delegate* unmanaged[Stdcall]<T1, T2, T3, T4, void>)this.func)(t1, t2, t3, t4);
return default;
}
return this.func(t1, t2, t3, t4);
}
return default;
}
public override void Initialize()
{
if (this.Cache.GetAddress() is not nuint addr ||
addr is 0)
{
return;
}
var call = BuildFastCallThunk(addr);
this.func = (delegate* unmanaged[Stdcall]<T1, T2, T3, T4, T5>)call;
}
}
public unsafe sealed class GWFastCall<T1, T2, T3, T4, T5, T6>(GWAddressCache target) : GWFastCall(target)
{
private delegate* unmanaged[Stdcall]<T1, T2, T3, T4, T5, T6> func = null;
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public unsafe T6? Invoke(T1 t1, T2 t2, T3 t3, T4 t4, T5 t5)
{
if (this.func is null)
{
this.Initialize();
}
if (this.func is not null)
{
if (typeof(T6) == typeof(Void))
{
((delegate* unmanaged[Stdcall]<T1, T2, T3, T4, T5, void>)this.func)(t1, t2, t3, t4, t5);
return default;
}
return this.func(t1, t2, t3, t4, t5);
}
return default;
}
public override void Initialize()
{
if (this.Cache.GetAddress() is not nuint addr ||
addr is 0)
{
return;
}
var call = BuildFastCallThunk(addr);
this.func = (delegate* unmanaged[Stdcall]<T1, T2, T3, T4, T5, T6>)call;
}
}
public unsafe sealed class GWFastCall<T1, T2, T3, T4, T5, T6, T7>(GWAddressCache target) : GWFastCall(target)
{
private delegate* unmanaged[Stdcall]<T1, T2, T3, T4, T5, T6, T7> func = null;
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public unsafe T7? Invoke(T1 t1, T2 t2, T3 t3, T4 t4, T5 t5, T6 t6)
{
if (this.func is null)
{
this.Initialize();
}
if (this.func is not null)
{
if (typeof(T7) == typeof(Void))
{
((delegate* unmanaged[Stdcall]<T1, T2, T3, T4, T5, T6, void>)this.func)(t1, t2, t3, t4, t5, t6);
return default;
}
return this.func(t1, t2, t3, t4, t5, t6);
}
return default;
}
public override void Initialize()
{
if (this.Cache.GetAddress() is not nuint addr ||
addr is 0)
{
return;
}
var call = BuildFastCallThunk(addr);
this.func = (delegate* unmanaged[Stdcall]<T1, T2, T3, T4, T5, T6, T7>)call;
}
}
+122
View File
@@ -0,0 +1,122 @@
using System.Runtime.InteropServices;
using MinHook;
namespace Daybreak.API.Interop;
/// <summary>
/// A thin convenience wrapper around MinHook that
/// resolves the target address lazily through <see cref="GWAddressCache"/>
/// unwraps an existing “CALL/JMP rel32” stub so we patch the real code
/// exposes <see cref="Original"/> so your detour can chain
/// </summary>
public sealed class GWHook<T>(
GWAddressCache funcAddress,
T detour,
bool bypassPreviousHooks = false) : IHook<T>
where T : Delegate
{
private readonly bool bypassPreviousHooks = bypassPreviousHooks;
private readonly GWAddressCache addressCache = funcAddress ?? throw new ArgumentNullException(nameof(funcAddress));
private readonly T detour = detour ?? throw new ArgumentNullException(nameof(detour));
private readonly SemaphoreSlim semaphore = new(1, 1);
private T? cont; // trampoline returned by MinHook
private HookEngine? engine;
/// <summary>Delegate that calls the next hook / real function.</summary>
public T Continue => this.cont ??
throw new InvalidOperationException("Hook not initialised call EnsureInitialized() first.");
public bool Hooked { get; private set; } = false;
public nuint TargetAddress { get; private set; }
public nuint ContinueAddress { get; private set; }
public nuint DetourAddress { get; private set; }
/// <summary>Installs the hook exactly once (thread-safe).</summary>
public bool EnsureInitialized()
{
if (this.Hooked)
{
return true;
}
this.semaphore.Wait();
try
{
if (this.Hooked)
{
return true;
}
var addr = this.addressCache.GetAddress();
if (addr.HasValue is false ||
addr.Value is 0)
{
return false;
}
var target = this.bypassPreviousHooks
? UnwrapNearBranch(addr.Value)
: addr.Value;
if (target is 0)
{
return false;
}
this.engine = new HookEngine();
this.cont = this.engine.CreateHook((nint)target, this.detour);
this.engine.EnableHooks();
this.TargetAddress = target;
this.ContinueAddress = (nuint)this.cont.Method.MethodHandle.GetFunctionPointer();
this.DetourAddress = (nuint)this.detour.Method.MethodHandle.GetFunctionPointer();
this.Hooked = true;
return true;
}
catch (Exception)
{
return false;
}
finally
{
this.semaphore.Release();
}
}
public void Dispose()
{
this.semaphore.Wait();
try
{
if (!this.Hooked ||
this.engine is null)
{
return;
}
this.engine.DisableHooks();
this.engine.Dispose();
this.cont = null;
this.Hooked = false;
}
finally
{
this.semaphore.Release();
}
}
/// <summary>
/// Follows a <c>CALL rel32</c> (<c>E8</c>) **or** <c>JMP rel32</c> (<c>E9</c>)
/// trampoline that may have been planted by a previous hook.
/// </summary>
private static unsafe nuint UnwrapNearBranch(nuint addr)
{
byte op = Marshal.ReadByte((IntPtr)addr);
if (op != 0xE8 && op != 0xE9) // not CALL/JMP rel32
return addr;
int rel = Marshal.ReadInt32((IntPtr)(addr + 1));
long dst = (long)addr + 5 + rel; // 5-byte instruction
return (nuint)dst;
}
}
@@ -0,0 +1,26 @@
using System.Runtime.InteropServices;
namespace Daybreak.API.Interop.GuildWars;
[StructLayout(LayoutKind.Explicit, Pack = 1, Size = 0xC)]
public readonly struct AccountUnlockedCount
{
[FieldOffset(0x0000)]
public readonly uint Id;
}
[StructLayout(LayoutKind.Explicit, Pack = 1, Size = 0x138)]
public readonly struct AccountGameContext
{
[FieldOffset(0x0000)]
public readonly GuildWarsArray<AccountUnlockedCount> AccountUnlockedCounts;
[FieldOffset(0x00b4)]
public readonly GuildWarsArray<uint> UnlockedPvpHeroes;
[FieldOffset(0x0124)]
public readonly GuildWarsArray<uint> UnlockedAccountSkills;
[FieldOffset(0x0134)]
public readonly uint AccountFlags;
}
@@ -0,0 +1,15 @@
using System.Runtime.InteropServices;
namespace Daybreak.API.Interop.GuildWars;
[StructLayout(LayoutKind.Sequential, Pack = 1, Size = 0x1C)]
public readonly unsafe struct AccountInfoContext
{
public readonly char* AccountName;
public readonly uint Wins;
public readonly uint Losses;
public readonly uint Rating;
public readonly uint QualifierPoints;
public readonly uint Rank;
public readonly uint TournamentRewardPoints;
}
+246
View File
@@ -0,0 +1,246 @@
using System.Numerics;
using System.Runtime.InteropServices;
namespace Daybreak.API.Interop.GuildWars;
[Flags]
public enum AgentType : uint
{
Living = 0xDB,
Gadget = 0x200,
Item = 0x400
}
[Flags]
public enum AgentModelType : ushort
{
NPC = 0x2000,
Player = 0x3000
}
[Flags]
public enum LivingAgentState : uint
{
None = 0,
InCombatStance = 0x000001,
HasQuest = 0x000002,
Dead = 0x000008,
Female = 0x000200,
HasBossGlow = 0x000400,
HidingCape = 0x001000,
CanBeViewedInPartyWindow = 0x020000,
SpawnedSpirit = 0x040000,
BeingObservedOrPlayer = 0x400000
}
public enum LivingAgentAllegiance : byte
{
Ally_NonAttackable = 0x1,
Neutral = 0x2,
Enemy = 0x3,
Spirit_Pet = 0x4,
Minion = 0x5,
Npc_Minipet = 0x6
};
[Flags]
public enum LivingAgentEffects : uint
{
None = 0,
Bleeding = 0x0001,
Conditioned = 0x0002,
Crippled1 = 0x0008, // Part of the Crippled check
Crippled = 0x000A, // Combined flag for crippled (0x0008 | 0x0002)
Dead = 0x0010,
DeepWound = 0x0020,
Poisoned = 0x0040,
Enchanted = 0x0080,
DegenHexed = 0x0400,
Hexed = 0x0800,
WeaponSpelled = 0x8000
}
public enum LivingAgentModelState : ushort
{
// Idle states
Idle1 = 64,
Idle2 = 68,
Idle3 = 100,
// Casting states
Casting1 = 65,
Casting2 = 581,
// Moving states
Moving1 = 12,
Moving2 = 76,
Moving3 = 204,
// Attacking states
Attacking1 = 96,
Attacking2 = 1088,
Attacking3 = 1120,
// Other states
KnockedDown = 1104
}
[StructLayout(LayoutKind.Sequential, Pack = 1)]
public readonly struct AgentEffects
{
public readonly uint AgentId;
public readonly GuildWarsArray<Buff> Buffs;
public readonly GuildWarsArray<Effect> Effects;
}
[StructLayout(LayoutKind.Explicit, Pack = 1)]
public readonly struct AgentGameContext
{
[FieldOffset(0x01AC)]
public readonly uint InstanceTimer;
}
[StructLayout(LayoutKind.Explicit, Pack = 1, Size = 196)]
public readonly struct AgentContext
{
[FieldOffset(0x0014)]
public readonly uint AgentInstanceTimer;
[FieldOffset(0x002C)]
public readonly uint AgentId;
[FieldOffset(0x0074)]
public readonly GamePos Pos;
[FieldOffset(0x009C)]
public readonly AgentType Type;
[FieldOffset(0x00A0)]
public readonly Vector2 Velocity;
}
[StructLayout(LayoutKind.Explicit, Pack = 1, Size = 212)]
public readonly struct AgentItemContext
{
[FieldOffset(0x00C4)]
public readonly uint OwnerId;
[FieldOffset(0x00C8)]
public readonly uint ItemId;
[FieldOffset(0x00D0)]
public readonly uint ExtraType;
}
[StructLayout(LayoutKind.Explicit, Pack = 1, Size = 228)]
public readonly struct AgentGadgetContext
{
[FieldOffset(0x00CC)]
public readonly uint ExtraType;
[FieldOffset(0x00D0)]
public readonly uint GadgetId;
}
[StructLayout(LayoutKind.Explicit, Pack = 1, Size = 0x1C0)]
public readonly unsafe struct AgentLivingContext
{
[FieldOffset(0x00C4)]
public readonly uint OwnerId;
[FieldOffset(0x00F4)]
public readonly ushort AgentType; // All non-player have an identifier. Two of the same mob have the same identifier;
[FieldOffset(0x00F6)]
public readonly AgentModelType AgentModelType;
[FieldOffset(0x0104)]
public readonly TagInfo* Tags;
[FieldOffset(0x010A)]
public readonly byte Primary;
[FieldOffset(0x010B)]
public readonly byte Secondary;
[FieldOffset(0x010C)]
public readonly byte Level;
[FieldOffset(0x010D)]
public readonly byte TeamId;
[FieldOffset(0x0114)]
public readonly float EnergyRegen;
[FieldOffset(0x011C)]
public readonly float Energy;
[FieldOffset(0x0120)]
public readonly uint MaxEnergy;
[FieldOffset(0x0128)]
public readonly float HealthRegen;
[FieldOffset(0x0130)]
public readonly float Health;
[FieldOffset(0x0134)]
public readonly uint MaxHealth;
[FieldOffset(0x0138)]
public readonly uint Effects;
[FieldOffset(0x0154)]
public readonly LivingAgentModelState ModelState;
[FieldOffset(0x0158)]
public readonly LivingAgentState State;
[FieldOffset(0x0180)]
public readonly uint LoginNumber;
[FieldOffset(0x01B1)]
public readonly LivingAgentAllegiance Allegiance;
public readonly bool IsAlive => (!this.State.HasFlag(LivingAgentState.Dead)) && this.Health > 0f;
public readonly bool IsPlayer => this.LoginNumber is not 0;
public readonly bool IsNpc => this.LoginNumber is 0;
}
[StructLayout(LayoutKind.Explicit, Pack = 1, Size = 52)]
public readonly struct MapAgentContext
{
[FieldOffset(0x0000)]
public readonly float CurrentEnergy;
[FieldOffset(0x0004)]
public readonly float MaxEnergy;
[FieldOffset(0x0008)]
public readonly float EnergyRegen;
[FieldOffset(0x000C)]
public readonly uint SkillTimestamp;
[FieldOffset(0x0020)]
public readonly float CurrentHealth;
[FieldOffset(0x0024)]
public readonly float MaxHealth;
[FieldOffset(0x0028)]
public readonly float HealthRegen;
[FieldOffset(0x0030)]
public readonly LivingAgentEffects Effects;
}
[StructLayout(LayoutKind.Explicit, Pack = 1, Size = 0x38)]
public readonly unsafe struct AgentInfo
{
[FieldOffset(0x0034)]
public readonly char* NameEncoded;
}
@@ -0,0 +1,93 @@
using System.Runtime.InteropServices;
namespace Daybreak.API.Interop.GuildWars;
[StructLayout(LayoutKind.Sequential, Pack = 1)]
public readonly struct AttributeContext
{
public readonly uint Id;
public readonly uint LevelBase;
public readonly uint Level;
public readonly uint DecrementPoints;
public readonly uint IncrementPoints;
}
[StructLayout(LayoutKind.Sequential, Pack = 1)]
public readonly struct AttributeInfo
{
public readonly uint ProfessionId;
public readonly uint AttributeId;
public readonly uint NameId;
public readonly uint DescriptionId;
public readonly uint IsPve;
}
[StructLayout(LayoutKind.Sequential, Pack = 1)]
public readonly struct PartyAttribute
{
public readonly uint AgentId;
public readonly AttributeContext Attribute0;
public readonly AttributeContext Attribute1;
public readonly AttributeContext Attribute2;
public readonly AttributeContext Attribute3;
public readonly AttributeContext Attribute4;
public readonly AttributeContext Attribute5;
public readonly AttributeContext Attribute6;
public readonly AttributeContext Attribute7;
public readonly AttributeContext Attribute8;
public readonly AttributeContext Attribute9;
public readonly AttributeContext Attribute10;
public readonly AttributeContext Attribute11;
public readonly AttributeContext Attribute12;
public readonly AttributeContext Attribute13;
public readonly AttributeContext Attribute14;
public readonly AttributeContext Attribute15;
public readonly AttributeContext Attribute16;
public readonly AttributeContext Attribute17;
public readonly AttributeContext Attribute18;
public readonly AttributeContext Attribute19;
public readonly AttributeContext Attribute20;
public readonly AttributeContext Attribute21;
public readonly AttributeContext Attribute22;
public readonly AttributeContext Attribute23;
public readonly AttributeContext Attribute24;
public readonly AttributeContext Attribute25;
public readonly AttributeContext Attribute26;
public readonly AttributeContext Attribute27;
public readonly AttributeContext Attribute28;
public readonly AttributeContext Attribute29;
public readonly AttributeContext Attribute30;
public readonly AttributeContext Attribute31;
public readonly AttributeContext Attribute32;
public readonly AttributeContext Attribute33;
public readonly AttributeContext Attribute34;
public readonly AttributeContext Attribute35;
public readonly AttributeContext Attribute36;
public readonly AttributeContext Attribute37;
public readonly AttributeContext Attribute38;
public readonly AttributeContext Attribute39;
public readonly AttributeContext Attribute40;
public readonly AttributeContext Attribute41;
public readonly AttributeContext Attribute42;
public readonly AttributeContext Attribute43;
public readonly AttributeContext Attribute44;
public readonly AttributeContext Attribute45;
public readonly AttributeContext Attribute46;
public readonly AttributeContext Attribute47;
public readonly AttributeContext Attribute48;
public readonly AttributeContext Attribute49;
public readonly AttributeContext Attribute50;
public readonly AttributeContext Attribute51;
public readonly AttributeContext Attribute52;
public readonly AttributeContext Attribute53;
public readonly AttributeContext[] Attributes =>
[
this.Attribute0, this.Attribute1, this.Attribute2, this.Attribute3, this.Attribute4, this.Attribute5, this.Attribute6, this.Attribute7, this.Attribute8, this.Attribute9,
this.Attribute10, this.Attribute11, this.Attribute12, this.Attribute13, this.Attribute14, this.Attribute15, this.Attribute16, this.Attribute17, this.Attribute18, this.Attribute19,
this.Attribute20, this.Attribute21, this.Attribute22, this.Attribute23, this.Attribute24, this.Attribute25, this.Attribute26, this.Attribute27, this.Attribute28, this.Attribute29,
this.Attribute30, this.Attribute31, this.Attribute32, this.Attribute33, this.Attribute34, this.Attribute35, this.Attribute36, this.Attribute37, this.Attribute38, this.Attribute39,
this.Attribute40, this.Attribute41, this.Attribute42, this.Attribute43, this.Attribute44, this.Attribute45, this.Attribute46, this.Attribute47, this.Attribute48, this.Attribute49,
this.Attribute50, this.Attribute51, this.Attribute52, this.Attribute53
];
}
@@ -0,0 +1,8 @@
namespace Daybreak.API.Interop.GuildWars;
public enum Behavior
{
Fight,
Guard,
AvoidCombat
}
@@ -0,0 +1,50 @@
using System.Extensions;
using System.Runtime.InteropServices;
namespace Daybreak.API.Interop.GuildWars;
[StructLayout(LayoutKind.Explicit, Pack = 1)]
public readonly struct CharContext
{
[FieldOffset(0x0064)]
public readonly Uuid PlayerUuid;
[FieldOffset(0x0074)]
public readonly Array20Char PlayerName;
[FieldOffset(0x0198)]
public readonly uint MapId;
[FieldOffset(0x019C)]
public readonly uint IsExporable;
[FieldOffset(0x01A0)]
public readonly Array24Byte Host;
[FieldOffset(0x01B8)]
public readonly uint PlayerId;
[FieldOffset(0x0220)]
public readonly uint DistrictNumber;
[FieldOffset(0x0224)]
public readonly Language Language;
[FieldOffset(0x0228)]
public readonly uint ObserveMapId;
[FieldOffset(0x022C)]
public readonly uint CurrentMapId;
[FieldOffset(0x0230)]
public readonly uint ObserveMapType;
[FieldOffset(0x0234)]
public readonly uint CurrentMapType;
[FieldOffset(0x02A4)]
public readonly uint PlayerNumber;
[FieldOffset(0x03B8)]
public readonly Array64Char PlayerEmail;
}
@@ -0,0 +1,22 @@
using System.Extensions;
using System.Runtime.InteropServices;
namespace Daybreak.API.Interop.GuildWars;
[StructLayout(LayoutKind.Explicit, Pack = 1)]
public readonly struct CharInfoContext
{
[FieldOffset(0x0008)]
public readonly Uuid Uuid;
[FieldOffset(0x0018)]
public readonly Array20Char Name;
[FieldOffset(0x0040)]
public readonly Array17Uint Props;
public readonly uint MapId => (this.Props[0] >> 16) & 0xFFFF;
public readonly uint Primary => (this.Props[2] >> 20) & 0xF;
public readonly uint Secondary => (this.Props[7] >> 10) & 0xF;
public readonly uint Campaign => this.Props[7] & 0xF;
public readonly uint Level => (this.Props[7] >> 4) & 0x3F;
public readonly bool IsPvp => ((this.Props[7] >> 9) & 0x1) == 0x1;
}
@@ -0,0 +1,19 @@
namespace Daybreak.API.Interop.GuildWars;
public enum District
{
Current,
International,
American,
EuropeEnglish,
EuropeFrench,
EuropeGerman,
EuropeItalian,
EuropeSpanish,
EuropePolish,
EuropeRussian,
AsiaKorean,
AsiaChinese,
AsiaJapanese,
Unknown = 0xff
}
+68
View File
@@ -0,0 +1,68 @@
using Daybreak.API.Models;
using System.Runtime.InteropServices;
namespace Daybreak.API.Interop.GuildWars;
// Very incomplete. GWCA contains a much better definition
[StructLayout(LayoutKind.Explicit, Pack = 1, Size = 0x1AC)]
public readonly struct Frame
{
[FieldOffset(0x0008)]
public readonly uint FrameLayout;
[FieldOffset(0x0018)]
public readonly uint VisibilityFlags;
[FieldOffset(0x0020)]
public readonly uint Type;
[FieldOffset(0x0024)]
public readonly uint TemplateType;
[FieldOffset(0x00A0)]
public readonly GuildWarsArray<FrameInteractionCallback> FrameCallbacks;
[FieldOffset(0x00b0)]
public readonly uint ChildOffsetId;
[FieldOffset(0x00b4)]
public readonly uint FrameId;
[FieldOffset(0x0120)]
public readonly FrameRelation Relation;
[FieldOffset(0x0184)]
public readonly uint Field91;
public bool IsCreated => (this.Field91 & 0x4) != 0;
public bool IsHidden => (this.Field91 & 0x200) != 0;
public bool IsVisible => !this.IsHidden;
public bool IsDIsabled => (this.Field91 & 0x10) != 0;
}
[StructLayout(LayoutKind.Explicit, Pack = 1, Size = 0x1C)]
public readonly unsafe struct FrameRelation
{
[FieldOffset(0x0000)]
public readonly FrameRelation* Parent;
[FieldOffset(0x000C)]
public readonly uint FrameHashId;
}
[StructLayout(LayoutKind.Sequential, Pack =1)]
public readonly unsafe struct InteractionMessage
{
public readonly uint FrameId;
public readonly UIMessage MessageId;
public readonly void** WParam;
}
public unsafe delegate void UIInteractionCallback(InteractionMessage* message, void* wParam, void* lParam);
[StructLayout(LayoutKind.Sequential, Pack = 1, Size = 0xC)]
public unsafe readonly struct FrameInteractionCallback
{
public readonly void* Callback; // UIInteractionCallback
public readonly void* UiCtl_Context;
}
@@ -0,0 +1,28 @@
using System.Runtime.InteropServices;
namespace Daybreak.API.Interop.GuildWars;
[StructLayout(LayoutKind.Explicit, Pack = 1)]
public readonly unsafe struct GameContext
{
[FieldOffset(0x0008)]
public readonly AgentGameContext* AgentGameContext;
[FieldOffset(0x0014)]
public readonly MapContext* MapContext;
[FieldOffset(0x0028)]
public readonly AccountGameContext* AccountContext;
[FieldOffset(0x002C)]
public readonly WorldContext* WorldContext;
[FieldOffset(0x003C)]
public readonly GuildContext* GuildContext;
[FieldOffset(0x0044)]
public readonly CharContext* CharContext;
[FieldOffset(0x004C)]
public readonly PartyContext* PartyContext;
}
+11
View File
@@ -0,0 +1,11 @@
using System.Runtime.InteropServices;
namespace Daybreak.API.Interop.GuildWars;
[StructLayout(LayoutKind.Sequential, Pack = 1)]
public readonly struct GamePos
{
public readonly float X;
public readonly float Y;
public readonly uint Plane;
}
+110
View File
@@ -0,0 +1,110 @@
using System.Extensions;
using System.Runtime.InteropServices;
namespace Daybreak.API.Interop.GuildWars;
[StructLayout(LayoutKind.Explicit, Pack = 1)]
public readonly unsafe struct GuildContext
{
[FieldOffset(0x0034)]
public readonly Array20Char PlayerName;
[FieldOffset(0x0060)]
public readonly uint PlayerBuildIndex;
[FieldOffset(0x0078)]
public readonly Array256Char Announcement;
[FieldOffset(0x0287)]
public readonly Array20Char AnnouncementAuthor;
[FieldOffset(0x02A0)]
public readonly uint PlayerGuildRank;
[FieldOffset(0x02A8)]
public readonly GuildWarsArray<TownAlliance> FactionOutpostGuilds;
[FieldOffset(0x02B8)]
public readonly uint KurzickTownCount;
[FieldOffset(0x02BC)]
public readonly uint LuxonTownCount;
[FieldOffset(0x02F8)]
public readonly GuildWarsArray<WrappedPointer<Guild>> Guilds;
[FieldOffset(0x0358)]
public readonly GuildWarsArray<WrappedPointer<GuildPlayer>> PlayerRoster;
}
[StructLayout(LayoutKind.Sequential, Pack = 1)]
public readonly struct TownAlliance
{
public readonly uint Rank;
public readonly uint Allegiance;
public readonly uint Faction;
public readonly Array32Char Name;
public readonly Array5Char Tag;
public readonly CapeDesign Cape;
public readonly uint MapId;
}
[StructLayout(LayoutKind.Sequential, Pack = 1)]
public readonly struct CapeDesign
{
public readonly uint CapeBgColor;
public readonly uint CapeDetailColor;
public readonly uint CapeEmblemColor;
public readonly uint CapeShape;
public readonly uint CapeDetail;
public readonly uint CapeEmblem;
public readonly uint CapeTrim;
}
[StructLayout(LayoutKind.Explicit, Pack = 1, Size = 0x174)]
public readonly unsafe struct GuildPlayer
{
[FieldOffset(0x0004)]
public readonly char* NamePtr;
[FieldOffset(0x0008)]
public readonly Array20Char InvitedName;
[FieldOffset(0x0030)]
public readonly Array20Char CurrentName;
[FieldOffset(0x0050)]
public readonly Array20Char InviterName;
[FieldOffset(0x0080)]
public readonly uint InviteTime;
[FieldOffset(0x0084)]
public readonly Array20Char PromoterName;
[FieldOffset(0x00DC)]
public readonly uint Offline;
[FieldOffset(0x00E0)]
public readonly uint MemberType;
[FieldOffset(0x00E4)]
public readonly uint Status;
}
[StructLayout(LayoutKind.Explicit, Pack = 1, Size = 0xAC)]
public readonly struct Guild
{
[FieldOffset(0x0024)]
public readonly uint Index;
[FieldOffset(0x0028)]
public readonly uint Rank;
[FieldOffset(0x002C)]
public readonly uint Features;
[FieldOffset(0x0030)]
public readonly Array32Char Name;
[FieldOffset(0x0070)]
public readonly uint Rating;
[FieldOffset(0x0074)]
public readonly uint Faction;
[FieldOffset(0x0078)]
public readonly uint FactionPoints;
[FieldOffset(0x007C)]
public readonly uint QualifierPoints;
[FieldOffset(0x0080)]
public readonly Array8Char Tag;
[FieldOffset(0x0090)]
public readonly CapeDesign Cape;
}
@@ -0,0 +1,52 @@
using System.Collections;
using System.Runtime.InteropServices;
namespace Daybreak.API.Interop.GuildWars;
[StructLayout(LayoutKind.Sequential, Pack = 1)]
public readonly unsafe struct GuildWarsArray<T> : IEnumerable<T>
where T : unmanaged
{
public readonly T* Buffer;
public readonly uint Capacity;
public readonly uint Size;
public readonly uint Param;
public Enumerator GetEnumerator() => new(this.Buffer, this.Size);
IEnumerator<T> IEnumerable<T>.GetEnumerator() => this.GetEnumerator();
IEnumerator IEnumerable.GetEnumerator() => this.GetEnumerator();
public unsafe struct Enumerator : IEnumerator, IEnumerator<T>
{
private readonly T* buffer;
private readonly uint size;
private int index;
internal Enumerator(T* buffer, uint size)
{
this.buffer = buffer;
this.size = size;
this.index = -1;
}
public bool MoveNext()
{
int next = this.index + 1;
if (next >= this.size)
{
return false;
}
this.index = next;
return true;
}
public T Current => this.buffer[this.index];
object IEnumerator.Current => this.Current;
public void Reset() => this.index = -1;
public readonly void Dispose() { }
}
}
+43
View File
@@ -0,0 +1,43 @@
using System.Extensions;
using System.Numerics;
using System.Runtime.InteropServices;
namespace Daybreak.API.Interop.GuildWars;
[StructLayout(LayoutKind.Explicit, Pack = 1, Size = 0x24)]
public readonly struct HeroFlag
{
[FieldOffset(0x0000)]
public readonly uint HeroId;
[FieldOffset(0x0004)]
public readonly uint AgentId;
[FieldOffset(0x0008)]
public readonly uint Level;
[FieldOffset(0x000C)]
public readonly Behavior Behavior;
[FieldOffset(0x0010)]
public readonly Vector2 Flag;
[FieldOffset(0x0020)]
public readonly uint LockedTargetId;
}
[StructLayout(LayoutKind.Explicit, Pack = 1, Size = 0x78)]
public readonly struct HeroInfo
{
[FieldOffset(0x0000)]
public readonly uint HeroId;
[FieldOffset(0x0004)]
public readonly uint AgentId;
[FieldOffset(0x0008)]
public readonly uint Level;
[FieldOffset(0x000C)]
public readonly Profession Primary;
[FieldOffset(0x0010)]
public readonly Profession Secondary;
[FieldOffset(0x0014)]
public readonly uint HeroFileId;
[FieldOffset(0x0018)]
public readonly uint ModelFileId;
[FieldOffset(0x0050)]
public readonly Array20Char Name;
}
@@ -0,0 +1,29 @@
using System.Runtime.InteropServices;
namespace Daybreak.API.Interop.GuildWars;
public enum InstanceType
{
Outpost,
Explorable,
Loading
};
[StructLayout(LayoutKind.Explicit, Pack = 1)]
public readonly unsafe struct InstanceInfoContext
{
[FieldOffset(0x0004)]
public readonly InstanceType InstanceType;
[FieldOffset(0x0008)]
public readonly AreaInfo* CurrentMapInfo;
}
[StructLayout(LayoutKind.Sequential, Pack = 1)]
public readonly struct AreaInfo
{
public readonly uint Campaign;
public readonly uint Continent;
public readonly uint Region;
public readonly uint RegionType;
public readonly uint Flags;
}
@@ -0,0 +1,17 @@
namespace Daybreak.API.Interop.GuildWars;
public enum Language
{
English,
Korean,
French,
German,
Italian,
Spanish,
TraditionalChinese,
Japanese = 8,
Polish,
Russian,
BorkBorkBork = 17,
Unknown = 0xff
}
@@ -0,0 +1,8 @@
using System.Runtime.InteropServices;
namespace Daybreak.API.Interop.GuildWars;
[StructLayout(LayoutKind.Explicit, Pack = 1)]
public readonly struct MapContext
{
}
+61
View File
@@ -0,0 +1,61 @@
using System.Runtime.InteropServices;
namespace Daybreak.API.Interop.GuildWars;
[StructLayout(LayoutKind.Explicit)]
public readonly unsafe struct NpcContext
{
[FieldOffset(0x0000)]
public readonly uint ModelId;
[FieldOffset(0x000C)]
public readonly uint Sex;
[FieldOffset(0x0010)]
public readonly NpcFlags Flags;
[FieldOffset(0x0014)]
public readonly uint Primary;
[FieldOffset(0x001C)]
public readonly byte DefaultLevel;
[FieldOffset(0x0020)]
public readonly char* EncName;
[FieldOffset(0x0028)]
public readonly int FilesCount;
[FieldOffset(0x002C)]
public readonly int FilesCapacity;
[Flags]
public enum NpcFlags : uint
{
None = 0x0000,
Henchman = 0x0010,
Hero = 0x0020,
Spirit = 0x4000,
Minion = 0x0100,
Pet = 0x000D
}
}
[StructLayout(LayoutKind.Sequential, Pack = 1)]
public readonly unsafe struct PetContext
{
public readonly uint AgentId;
public readonly uint OwnerAgentId;
public readonly char* PetName;
public readonly uint ModelFileId1;
public readonly uint ModelFileId2;
public readonly Behavior Behavior;
public readonly uint LockedTargetId;
}
[StructLayout(LayoutKind.Sequential, Pack = 1)]
public readonly struct ControlledMinion
{
public readonly uint AgentId;
public readonly uint MinionCount;
}
+124
View File
@@ -0,0 +1,124 @@
using System.Extensions;
using System.Runtime.InteropServices;
namespace Daybreak.API.Interop.GuildWars;
[Flags]
public enum PlayerPartyMemberState : uint
{
None = 0,
Connected = 1,
Ticked = 2
}
public enum PartySearchType
{
PartySearchType_Hunting = 0,
PartySearchType_Mission = 1,
PartySearchType_Quest = 2,
PartySearchType_Trade = 3,
PartySearchType_Guild = 4,
};
[Flags]
public enum PartyFlags : uint
{
None = 0,
HardMode = 0x10, // Bit 4
Defeated = 0x20, // Bit 5
PartyLeader = 0x80 // Bit 7 (0x7th position)
}
[StructLayout(LayoutKind.Explicit, Pack = 1, Size = 0xC)]
public readonly unsafe struct PartyMoraleContext
{
[FieldOffset(0x000C)]
public readonly PartyMemberMoraleInfo* MoraleInfo;
}
[StructLayout(LayoutKind.Explicit, Pack = 1)]
public readonly struct PartyMemberMoraleInfo
{
[FieldOffset(0x0000)]
public readonly uint AgentId;
[FieldOffset(0x0014)]
public readonly uint Morale;
}
[StructLayout(LayoutKind.Explicit, Pack = 1, Size = 0x58)]
public readonly unsafe struct PartyContext
{
[FieldOffset(0x0014)]
public readonly PartyFlags Flags;
[FieldOffset(0x0040)]
public readonly GuildWarsArray<WrappedPointer<PartyInfo>> Parties;
[FieldOffset(0x0054)]
public readonly PartyInfo* PlayerParty;
[FieldOffset(0x00C0)]
public readonly GuildWarsArray<WrappedPointer<PartySearch>> PartySearches;
}
[StructLayout(LayoutKind.Sequential, Pack = 1, Size = 0xC)]
public readonly struct PlayerPartyMember
{
public readonly uint LogingNumber;
public readonly uint CalledTargetId;
public readonly PlayerPartyMemberState State;
}
[StructLayout(LayoutKind.Explicit, Pack = 1, Size = 0x18)]
public readonly struct HeroPartyMember
{
[FieldOffset(0x0000)]
public readonly uint AgentId;
[FieldOffset(0x0004)]
public readonly uint OwnerPlayerId;
[FieldOffset(0x0008)]
public readonly uint HeroId;
[FieldOffset(0x0014)]
public readonly uint Level;
}
[StructLayout(LayoutKind.Explicit, Pack = 1, Size = 0x34)]
public readonly struct HenchmanPartyMember
{
[FieldOffset(0x0000)]
public readonly uint AgentId;
[FieldOffset(0x002C)]
public readonly Profession Profession;
[FieldOffset(0x0030)]
public readonly uint Level;
}
[StructLayout(LayoutKind.Explicit, Pack = 1, Size = 0x84)]
public readonly struct PartyInfo
{
[FieldOffset(0x0000)]
public readonly uint PartyId;
[FieldOffset(0x0004)]
public readonly GuildWarsArray<PlayerPartyMember> Players;
[FieldOffset(0x0014)]
public readonly GuildWarsArray<HenchmanPartyMember> Henchmen;
[FieldOffset(0x0024)]
public readonly GuildWarsArray<HeroPartyMember> Heroes;
[FieldOffset(0x0034)]
public readonly GuildWarsArray<uint> Others;
}
[StructLayout(LayoutKind.Sequential, Pack = 1)]
public readonly struct PartySearch
{
public readonly uint PartySearchId;
public readonly PartySearchType PartySearchType;
public readonly uint HardMode;
public readonly uint District;
public readonly uint Language;
public readonly uint PartySize;
public readonly uint HeroCount;
public readonly Array32Char Message;
public readonly Array20Char PartyLeader;
public readonly Profession Primary;
public readonly Profession Secondary;
public readonly uint Level;
public readonly uint Timestamp;
}
@@ -1,15 +1,22 @@
using System.Runtime.InteropServices;
namespace Daybreak.Models.Interop;
namespace Daybreak.API.Interop.GuildWars;
[StructLayout(LayoutKind.Explicit)]
public readonly struct PlayerContext
[Flags]
public enum PlayerContextFlags : uint
{
None = 0,
PvP = 0x800
}
[StructLayout(LayoutKind.Explicit, Pack = 1, Size = 0x4C)]
public readonly unsafe struct PlayerContext
{
[FieldOffset(0x0000)]
public readonly int AgentId;
[FieldOffset(0x0014)]
public readonly uint PartyFlags;
public readonly PlayerContextFlags Flags;
[FieldOffset(0x0018)]
public readonly uint PrimaryProfession;
@@ -17,8 +24,11 @@ public readonly struct PlayerContext
[FieldOffset(0x001C)]
public readonly uint SecondaryProfession;
[FieldOffset(0x0024)]
public readonly char* NameEncoded;
[FieldOffset(0x0028)]
public readonly GuildwarsPointer<string> NamePointer;
public readonly char* Name;
[FieldOffset(0x002C)]
public readonly uint PartyLeaderPlayerNumber;
@@ -31,10 +41,4 @@ public readonly struct PlayerContext
[FieldOffset(0x0038)]
public readonly uint PartySize;
[FieldOffset(0x003C)]
//Ignore this field. Added so that the struct will have the proper size and be marshaled properly into the array.
private readonly GuildwarsArray<uint> HH3C;
public bool Pvp => (this.PartyFlags & 0x800) != 0;
}
@@ -0,0 +1,10 @@
using System.Runtime.InteropServices;
namespace Daybreak.API.Interop.GuildWars;
[StructLayout(LayoutKind.Explicit, Pack = 1)]
public readonly struct PlayerControlledCharContext
{
[FieldOffset(0x0014)]
public readonly uint AgentId;
}
@@ -0,0 +1,30 @@
using System.Extensions;
using System.Runtime.InteropServices;
namespace Daybreak.API.Interop.GuildWars;
[StructLayout(LayoutKind.Explicit, Pack = 1)]
public readonly struct PreGameContext
{
[FieldOffset(0x0000)]
public readonly uint FrameId;
[FieldOffset(0x0124)]
public readonly uint ChosenCharacterIndex;
[FieldOffset(0x0140)]
public readonly uint Index1;
[FieldOffset(0x0144)]
public readonly uint Index2;
[FieldOffset(0x00148)]
public readonly GuildWarsArray<LoginCharacterContext> LoginCharacters;
}
[StructLayout(LayoutKind.Explicit, Pack = 1)]
public readonly struct LoginCharacterContext
{
[FieldOffset(0x0004)]
public readonly Array20Char CharacterName;
}
@@ -0,0 +1,47 @@
using System.Runtime.InteropServices;
namespace Daybreak.API.Interop.GuildWars;
public enum Profession : uint
{
None,
Warrior,
Ranger,
Monk,
Necromancer,
Mesmer,
Elementalist,
Assassin,
Ritualist,
Paragon,
Dervish
}
public enum ProfessionByte : byte
{
None,
Warrior,
Ranger,
Monk,
Necromancer,
Mesmer,
Elementalist,
Assassin,
Ritualist,
Paragon,
Dervish
}
[StructLayout(LayoutKind.Sequential, Pack = 1, Size = 0x0014)]
public readonly struct ProfessionsContext
{
public readonly uint AgentId;
public readonly Profession CurrentPrimary;
public readonly Profession CurrentSecondary;
public readonly uint UnlockedProfessionsFlags;
public bool ProfessionUnlocked(int professionId)
{
return (this.UnlockedProfessionsFlags & (1U << professionId)) != 0;
}
}
@@ -1,9 +1,9 @@
using System.Numerics;
using System.Runtime.InteropServices;
namespace Daybreak.Models.Interop;
namespace Daybreak.API.Interop.GuildWars;
[StructLayout(LayoutKind.Explicit)]
[StructLayout(LayoutKind.Explicit, Pack = 1, Size = 0x0034)]
public readonly struct QuestContext
{
[FieldOffset(0x0000)]
@@ -17,8 +17,12 @@ public readonly struct QuestContext
[FieldOffset(0x0028)]
public readonly uint MapTo;
}
[FieldOffset(0x0030)]
//Ignore this field. Added so that the struct will have the proper size and be marshaled properly into the array.
private readonly uint HH30;
[StructLayout(LayoutKind.Sequential, Pack = 1, Size = 0xC)]
public readonly unsafe struct MissionObjectiveContext
{
public readonly uint ObjectiveId;
public readonly char* EncodedString;
public readonly uint Type;
}
@@ -0,0 +1,12 @@
namespace Daybreak.API.Interop.GuildWars;
public enum ServerRegion
{
International = -2,
America = 0,
Korea,
Europe,
China,
Japan,
Unknown = 0xff
}
+58
View File
@@ -0,0 +1,58 @@
using System.Runtime.InteropServices;
namespace Daybreak.API.Interop.GuildWars;
[StructLayout(LayoutKind.Sequential, Pack = 1)]
public readonly struct SkillContext
{
public readonly uint Adrenaline1;
public readonly uint Adrenaline2;
public readonly uint Recharge;
public readonly uint Id;
public readonly uint Event;
}
[StructLayout(LayoutKind.Sequential, Pack = 1, Size = 0xBC)]
public readonly struct SkillbarContext
{
public readonly uint AgentId;
public readonly SkillContext Skill0;
public readonly SkillContext Skill1;
public readonly SkillContext Skill2;
public readonly SkillContext Skill3;
public readonly SkillContext Skill4;
public readonly SkillContext Skill5;
public readonly SkillContext Skill6;
public readonly SkillContext Skill7;
public readonly uint Disabled;
public readonly uint Casting;
}
[StructLayout(LayoutKind.Explicit, Pack = 1, Size = 0x10)]
public readonly struct Buff
{
[FieldOffset(0x0000)]
public readonly uint SkillId;
[FieldOffset(0x0008)]
public readonly uint BuffId;
[FieldOffset(0x000C)]
public readonly uint TargetAgentId;
}
[StructLayout(LayoutKind.Sequential, Pack = 1, Size = 0x18)]
public readonly struct Effect
{
public readonly uint SkillId;
public readonly uint AttributeLevel;
public readonly uint EffectId;
public readonly uint AgentId;
public readonly float Duration;
public readonly uint Timestamp;
}
[StructLayout(LayoutKind.Sequential, Pack = 1)]
public readonly struct DupeSkill
{
public readonly uint SkillId;
public readonly uint Count;
}
@@ -0,0 +1,13 @@
using System.Extensions;
namespace Daybreak.API.Interop.GuildWars;
public readonly struct SkillTemplate(uint primary, uint secondary, uint attributeCount, Array12Uint attributeIds, Array12Uint attributeValues, Array8Uint skills)
{
public readonly uint Primary = primary;
public readonly uint Secondary = secondary;
public readonly uint AttributesCount = attributeCount;
public readonly Array12Uint AttributeIds = attributeIds;
public readonly Array12Uint AttributeValues = attributeValues;
public readonly Array8Uint Skills = skills;
}
+12
View File
@@ -0,0 +1,12 @@
using System.Runtime.InteropServices;
namespace Daybreak.API.Interop.GuildWars;
[StructLayout(LayoutKind.Sequential, Pack = 1)]
public readonly struct TagInfo
{
public readonly ushort GuildId;
public readonly byte Primary;
public readonly byte Secondary;
public readonly ushort Level;
}
+40
View File
@@ -0,0 +1,40 @@
using System.Runtime.InteropServices;
namespace Daybreak.API.Interop.GuildWars;
[Flags]
public enum TitleProps : uint
{
None = 0,
PercentageBased = 1,
HasTiers = 2
// Note: The HasTiers check uses (props & 3) == 2, which means bit 1 is set but bit 0 is not
}
[StructLayout(LayoutKind.Sequential, Pack = 1, Size = 40)]
public readonly struct TitleContext
{
public readonly TitleProps Props;
public readonly uint CurrentPoints;
public readonly uint CurrentTitleTierIndex;
public readonly uint PointsNeededForCurrentRank;
public readonly uint NextTitleTierIndex;
public readonly uint PointsNeededForNextRank;
public readonly uint MaxTitleRank;
public readonly uint MaxTitleTierIndex;
}
[StructLayout(LayoutKind.Sequential, Pack = 1, Size = 0xC)]
public readonly unsafe struct TitleTier
{
public readonly TitleProps Props;
public readonly uint TierNumber;
public readonly char* TierNameEncoded;
}
[StructLayout(LayoutKind.Sequential, Pack = 1)]
public readonly struct TitleClientData
{
public readonly uint TitleId;
public readonly uint NameId;
}
+183
View File
@@ -0,0 +1,183 @@
namespace Daybreak.API.Interop.GuildWars;
public enum UIAction : uint
{
None = 0,
Screenshot = 0xAE,
// Panels
CloseAllPanels = 0x85,
ToggleInventoryWindow = 0x8B,
OpenScoreChart = 0xBD,
OpenTemplateManager = 0xD3,
OpenSaveEquipmentTemplate = 0xD4,
OpenSaveSkillTemplate = 0xD5,
OpenParty = 0xBF,
OpenGuild = 0xBA,
OpenFriends = 0xB9,
ToggleAllBags = 0xB8,
OpenMissionMap = 0xB6,
OpenBag2 = 0xB5,
OpenBag1 = 0xB4,
OpenBelt = 0xB3,
OpenBackpack = 0xB2,
OpenSkillsAndAttributes = 0x8F,
OpenQuestLog = 0x8E,
OpenWorldMap = 0x8C,
OpenOptions = 0x8D,
OpenHero = 0x8A,
// Weapon sets
CycleEquipment = 0x86,
ActivateWeaponSet1 = 0x81,
ActivateWeaponSet2,
ActivateWeaponSet3,
ActivateWeaponSet4,
DropItem = 0xCD, // drops bundle item >> flags, ashes, etc
// Chat
CharReply = 0xBE,
OpenChat = 0xA1,
OpenAlliance = 0x88,
ReverseCamera = 0x90,
StrafeLeft = 0x91,
StrafeRight = 0x92,
TurnLeft = 0xA2,
TurnRight = 0xA3,
MoveBackward = 0xAC,
MoveForward = 0xAD,
CancelAction = 0xAF,
Interact = 0x80,
ReverseDirection = 0xB1,
Autorun = 0xB7,
Follow = 0xCC,
// Targeting
TargetPartyMember1 = 0x96,
TargetPartyMember2,
TargetPartyMember3,
TargetPartyMember4,
TargetPartyMember5,
TargetPartyMember6,
TargetPartyMember7,
TargetPartyMember8,
TargetPartyMember9 = 0xC6,
TargetPartyMember10,
TargetPartyMember11,
TargetPartyMember12,
TargetNearestItem = 0xC3,
TargetNextItem = 0xC4,
TargetPreviousItem = 0xC5,
TargetPartyMemberNext = 0xCA,
TargetPartyMemberPrevious = 0xCB,
TargetAllyNearest = 0xBC,
ClearTarget = 0xE3,
TargetSelf = 0xA0, // also 0x96
TargetPriorityTarget = 0x9F,
TargetNearestEnemy = 0x93,
TargetNextEnemy = 0x95,
TargetPreviousEnemy = 0x9E,
ShowOthers = 0x89,
ShowTargets = 0x94,
CameraZoomIn = 0xCE,
CameraZoomOut = 0xCF,
// Party/Hero commands
ClearPartyCommands = 0xDB,
CommandParty = 0xD6,
CommandHero1,
CommandHero2,
CommandHero3,
CommandHero4 = 0x102,
CommandHero5,
CommandHero6,
CommandHero7,
OpenHero1PetCommander = 0xE0,
OpenHero2PetCommander,
OpenHero3PetCommander,
OpenHero4PetCommander = 0xFE,
OpenHero5PetCommander,
OpenHero6PetCommander,
OpenHero7PetCommander,
OpenHeroCommander1 = 0xDC,
OpenHeroCommander2,
OpenHeroCommander3,
OpenPetCommander,
OpenHeroCommander4 = 0x126,
OpenHeroCommander5,
OpenHeroCommander6,
OpenHeroCommander7,
Hero1Skill1 = 0xE5,
Hero1Skill2,
Hero1Skill3,
Hero1Skill4,
Hero1Skill5,
Hero1Skill6,
Hero1Skill7,
Hero1Skill8,
Hero2Skill1,
Hero2Skill2,
Hero2Skill3,
Hero2Skill4,
Hero2Skill5,
Hero2Skill6,
Hero2Skill7,
Hero2Skill8,
Hero3Skill1,
Hero3Skill2,
Hero3Skill3,
Hero3Skill4,
Hero3Skill5,
Hero3Skill6,
Hero3Skill7,
Hero3Skill8,
Hero4Skill1 = 0x106,
Hero4Skill2,
Hero4Skill3,
Hero4Skill4,
Hero4Skill5,
Hero4Skill6,
Hero4Skill7,
Hero4Skill8,
Hero5Skill1,
Hero5Skill2,
Hero5Skill3,
Hero5Skill4,
Hero5Skill5,
Hero5Skill6,
Hero5Skill7,
Hero5Skill8,
Hero6Skill1,
Hero6Skill2,
Hero6Skill3,
Hero6Skill4,
Hero6Skill5,
Hero6Skill6,
Hero6Skill7,
Hero6Skill8,
Hero7Skill1,
Hero7Skill2,
Hero7Skill3,
Hero7Skill4,
Hero7Skill5,
Hero7Skill6,
Hero7Skill7,
Hero7Skill8,
// Skills
UseSkill1 = 0xA4,
UseSkill2,
UseSkill3,
UseSkill4,
UseSkill5,
UseSkill6,
UseSkill7,
UseSkill8
};
+108
View File
@@ -0,0 +1,108 @@
using System.Globalization;
namespace Daybreak.API.Interop.GuildWars;
public readonly struct Uuid(
uint timeLow,
ushort timeMid,
ushort timeHighAndVersion,
byte clockSqHi,
byte clockSqLow,
byte node0,
byte node1,
byte node2,
byte node3,
byte node4,
byte node5) : IEquatable<Uuid>
{
public const uint NodeLength = 6;
public readonly uint TimeLow = timeLow;
public readonly ushort TimeMid = timeMid;
public readonly ushort TimeHighAndVersion = timeHighAndVersion;
public readonly byte ClockSqHiAndRes = clockSqHi;
public readonly byte ClockSqLow = clockSqLow;
public readonly byte Node0 = node0;
public readonly byte Node1 = node1;
public readonly byte Node2 = node2;
public readonly byte Node3 = node3;
public readonly byte Node4 = node4;
public readonly byte Node5 = node5;
public bool Equals(Uuid other)
{
return this.TimeLow == other.TimeLow &&
this.TimeMid == other.TimeMid &&
this.TimeHighAndVersion == other.TimeHighAndVersion &&
this.ClockSqHiAndRes == other.ClockSqHiAndRes &&
this.ClockSqLow == other.ClockSqLow &&
this.Node0 == other.Node0 &&
this.Node1 == other.Node1 &&
this.Node2 == other.Node2 &&
this.Node3 == other.Node3 &&
this.Node4 == other.Node4 &&
this.Node5 == other.Node5;
}
public override bool Equals(object? obj)
{
return obj is Uuid uuid && this.Equals(uuid);
}
public static bool operator ==(Uuid left, Uuid right)
{
return left.Equals(right);
}
public static bool operator !=(Uuid left, Uuid right)
{
return !(left == right);
}
public static bool TryParse(string uuidString, out Uuid uuid)
{
if (uuidString.Length != 36)
{
uuid = default;
return false;
}
var parts = uuidString.Split('-');
if (parts.Length != 5)
{
uuid = default;
return false;
}
if (!uint.TryParse(parts[0], NumberStyles.HexNumber, CultureInfo.InvariantCulture, out var timeLow) ||
!ushort.TryParse(parts[1], NumberStyles.HexNumber, CultureInfo.InvariantCulture, out var timeMid) ||
!ushort.TryParse(parts[2], NumberStyles.HexNumber, CultureInfo.InvariantCulture, out var timeHighAndVersion) ||
!byte.TryParse(parts[3].AsSpan(0, 2), NumberStyles.HexNumber, CultureInfo.InvariantCulture, out var clockSqHi) ||
!byte.TryParse(parts[3].AsSpan(2, 2), NumberStyles.HexNumber, CultureInfo.InvariantCulture, out var clockSqLow) ||
!byte.TryParse(parts[4].AsSpan(0, 2), NumberStyles.HexNumber, CultureInfo.InvariantCulture, out var node0) ||
!byte.TryParse(parts[4].AsSpan(2, 2), NumberStyles.HexNumber, CultureInfo.InvariantCulture, out var node1) ||
!byte.TryParse(parts[4].AsSpan(4, 2), NumberStyles.HexNumber, CultureInfo.InvariantCulture, out var node2) ||
!byte.TryParse(parts[4].AsSpan(6, 2), NumberStyles.HexNumber, CultureInfo.InvariantCulture, out var node3) ||
!byte.TryParse(parts[4].AsSpan(8, 2), NumberStyles.HexNumber, CultureInfo.InvariantCulture, out var node4) ||
!byte.TryParse(parts[4].AsSpan(10, 2), NumberStyles.HexNumber, CultureInfo.InvariantCulture, out var node5))
{
uuid = default;
return false;
}
uuid = new Uuid(timeLow, timeMid, timeHighAndVersion, clockSqHi, clockSqLow, node0, node1, node2, node3, node4, node5);
return true;
}
public override int GetHashCode()
{
return HashCode.Combine<uint, ushort, ushort, byte, byte, byte[]>(this.TimeLow, this.TimeMid, this.TimeHighAndVersion, this.ClockSqHiAndRes, this.ClockSqLow, [this.Node0, this.Node1, this.Node2, this.Node3, this.Node4, this.Node5]);
}
public override string ToString()
{
return $"{this.TimeLow:X8}-{this.TimeMid:X4}-{this.TimeHighAndVersion:X4}-{this.ClockSqHiAndRes:X2}{this.ClockSqLow:X2}-{this.Node0:X2}{this.Node1:X2}{this.Node2:X2}{this.Node3:X2}{this.Node4:X2}{this.Node5:X2}";
}
public readonly static Uuid Zero = new(0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0);
}
@@ -0,0 +1,173 @@
using System.Numerics;
using System.Runtime.InteropServices;
namespace Daybreak.API.Interop.GuildWars;
[StructLayout(LayoutKind.Explicit, Pack = 1)]
public readonly unsafe struct WorldContext
{
[FieldOffset(0x0000)]
public readonly AccountInfoContext* AccountInfo;
[FieldOffset(0x0024)]
public readonly GuildWarsArray<uint> MerchItems;
[FieldOffset(0x0034)]
public readonly GuildWarsArray<uint> MerchItems2;
[FieldOffset(0x007C)]
public readonly GuildWarsArray<MapAgentContext> MapAgents;
[FieldOffset(0x009C)]
public readonly Vector3 AllFlag;
[FieldOffset(0x00AC)]
public readonly GuildWarsArray<PartyAttribute> Attributes;
[FieldOffset(0x0508)]
public readonly GuildWarsArray<AgentEffects> PartyEffects;
[FieldOffset(0x0528)]
public readonly uint ActiveQuestId;
[FieldOffset(0x052C)]
public readonly GuildWarsArray<QuestContext> QuestLog;
[FieldOffset(0x0564)]
public readonly GuildWarsArray<MissionObjectiveContext> MissionObjectives;
[FieldOffset(0x0574)]
public readonly GuildWarsArray<uint> HenchmenAgentIds;
[FieldOffset(0x0584)]
public readonly GuildWarsArray<HeroFlag> HeroFlags;
[FieldOffset(0x0594)]
public readonly GuildWarsArray<HeroInfo> HeroInfos;
[FieldOffset(0x05A4)]
public readonly GuildWarsArray<nuint> CartographedAreas;
[FieldOffset(0x05BC)]
public readonly GuildWarsArray<ControlledMinion> ControlledMinions;
[FieldOffset(0x05CC)]
public readonly GuildWarsArray<uint> MissionsCompleted;
[FieldOffset(0x05DC)]
public readonly GuildWarsArray<uint> MissionsBonus;
[FieldOffset(0x05EC)]
public readonly GuildWarsArray<uint> MissionsCompletedHardMode;
[FieldOffset(0x05FC)]
public readonly GuildWarsArray<uint> MissionsBonusHardMode;
[FieldOffset(0x060C)]
public readonly GuildWarsArray<uint> UnlockedMap;
[FieldOffset(0x062C)]
public readonly GuildWarsArray<PartyMoraleContext> PartyMorale;
[FieldOffset(0x067C)]
public readonly uint PlayerNumber;
[FieldOffset(0x0680)]
public readonly PlayerControlledCharContext* PlayerControlledChar;
[FieldOffset(0x0684)]
public readonly uint HardModeUnlocked;
[FieldOffset(0x06AC)]
public readonly GuildWarsArray<PetContext> Pets;
[FieldOffset(0x06BC)]
public readonly GuildWarsArray<ProfessionsContext> Professions;
[FieldOffset(0x06F0)]
public readonly GuildWarsArray<SkillbarContext> Skillbars;
[FieldOffset(0x0700)]
public readonly GuildWarsArray<uint> LearnableCharacterSkills;
[FieldOffset(0x0710)]
public readonly GuildWarsArray<uint> UnlockedCharacterSkills;
[FieldOffset(0x0720)]
public readonly GuildWarsArray<DupeSkill> DupeSkills;
[FieldOffset(0x0740)]
public readonly uint Experience;
[FieldOffset(0x0748)]
public readonly uint CurrentKurzick;
[FieldOffset(0x0750)]
public readonly uint TotalKurzick;
[FieldOffset(0x0758)]
public readonly uint CurrentLuxon;
[FieldOffset(0x0760)]
public readonly uint TotalLuxon;
[FieldOffset(0x0768)]
public readonly uint CurrentImperial;
[FieldOffset(0x0770)]
public readonly uint TotalImperial;
[FieldOffset(0x0788)]
public readonly uint Level;
[FieldOffset(0x0790)]
public readonly uint Morale;
[FieldOffset(0x0798)]
public readonly uint CurrentBalthazar;
[FieldOffset(0x07A0)]
public readonly uint TotalBalthazar;
[FieldOffset(0x07A8)]
public readonly uint CurrentSkillPoints;
[FieldOffset(0x07B0)]
public readonly uint TotalSkillPoints;
[FieldOffset(0x07B8)]
public readonly uint MaxKurzick;
[FieldOffset(0x07BC)]
public readonly uint MaxLuxon;
[FieldOffset(0x07C0)]
public readonly uint MaxBalthazar;
[FieldOffset(0x07C4)]
public readonly uint MaxImperial;
[FieldOffset(0x07CC)]
public readonly GuildWarsArray<AgentInfo> AgentInfos;
[FieldOffset(0x07FC)]
public readonly GuildWarsArray<NpcContext> Npcs;
[FieldOffset(0x080C)]
public readonly GuildWarsArray<PlayerContext> Players;
[FieldOffset(0x081C)]
public readonly GuildWarsArray<TitleContext> Titles;
[FieldOffset(0x082C)]
public readonly GuildWarsArray<TitleTier> TitleTiers;
[FieldOffset(0x083C)]
public readonly GuildWarsArray<uint> VanquishedAreas;
[FieldOffset(0x084C)]
public readonly uint FoesKilled;
[FieldOffset(0x0850)]
public readonly uint FoesToKill;
}
@@ -0,0 +1,37 @@
namespace Daybreak.API.Interop.GuildWars;
public readonly unsafe struct WrappedPointer<T>(T* pointer)
where T : unmanaged
{
public readonly T* Pointer = pointer;
public bool IsNull => this.Pointer is null;
public static implicit operator WrappedPointer<T>(T* pointer) => new(pointer);
public static implicit operator T*(WrappedPointer<T> wrappedPointer) => wrappedPointer.Pointer;
public static bool operator ==(T* left, WrappedPointer<T> right) => left == right.Pointer;
public static bool operator !=(T* left, WrappedPointer<T> right) => left != right.Pointer;
public static bool operator ==(WrappedPointer<T> left, T* right) => left.Pointer == right;
public static bool operator !=(WrappedPointer<T> left, T* right) => left.Pointer != right;
public static bool operator ==(WrappedPointer<T> left, WrappedPointer<T> right) => left.Pointer == right.Pointer;
public static bool operator !=(WrappedPointer<T> left, WrappedPointer<T> right) => left.Pointer != right.Pointer;
public override int GetHashCode() => this.Pointer is null ? 0 : this.Pointer->GetHashCode();
public override bool Equals(object? obj)
{
if (obj is WrappedPointer<T> wrappedPointer)
{
return this.Pointer == wrappedPointer.Pointer;
}
return false;
}
}
+9
View File
@@ -0,0 +1,9 @@
namespace Daybreak.API.Interop;
public interface IHook<T>
: IDisposable where T : Delegate
{
public T Continue { get; }
public nuint ContinueAddress { get; }
}
+19
View File
@@ -0,0 +1,19 @@
using Daybreak.API.Interop.GuildWars;
namespace Daybreak.API.Interop;
public readonly struct ManagedFrame(
WrappedPointer<Frame> frame,
Action closeFrame) : IDisposable
{
private readonly Action closeFrame = closeFrame;
public static ManagedFrame Null => new(null, () => { });
public readonly WrappedPointer<Frame> Frame = frame;
public void Dispose()
{
this.closeFrame();
}
}
@@ -0,0 +1,31 @@
using MemoryPack;
using System.Buffers;
namespace Daybreak.API.Interop;
public static class MemoryPackExtensions
{
/// <summary>Serialize <paramref name="value"/> into <paramref name="destination"/>.</summary>
/// <returns>Number of bytes written.</returns>
public static int SerializeToSpan<T>(T value, Span<byte> destination)
{
var dummy = DummyBufferWriter.Instance;
using var state = MemoryPackWriterOptionalStatePool.Rent(null);
var writer = new MemoryPackWriter<DummyBufferWriter>(ref dummy, destination, state);
writer.WriteValue(value);
writer.Flush();
return writer.WrittenCount;
}
private sealed class DummyBufferWriter : IBufferWriter<byte>
{
public static readonly DummyBufferWriter Instance = new();
public void Advance(int count) { /* ignore we never overflow destination */ }
public Memory<byte> GetMemory(int sizeHint = 0) => throw new NotSupportedException();
public Span<byte> GetSpan(int sizeHint = 0) => throw new NotSupportedException();
}
}
+78
View File
@@ -0,0 +1,78 @@
using Daybreak.API.Converters;
using System.Text.Json.Serialization;
namespace Daybreak.API.Interop;
[JsonConverter(typeof(PointerValueConverter))]
public readonly struct PointerValue(nuint address) : IEquatable<PointerValue>, IEquatable<nuint>, IEquatable<nint>
{
public readonly nuint Address = address;
public bool Equals(PointerValue other)
{
return this.Address.Equals(other.Address);
}
public bool Equals(nuint other)
{
return this.Address.Equals(other);
}
public bool Equals(nint other)
{
return this.Address.Equals((nuint)other);
}
public override string ToString()
{
return $"0x{this.Address:X8}";
}
public override bool Equals(object? obj) => obj is PointerValue value && this.Equals(value);
public override int GetHashCode()
{
return HashCode.Combine(this.Address);
}
public static bool operator ==(PointerValue left, PointerValue right)
{
return left.Equals(right);
}
public static bool operator !=(PointerValue left, PointerValue right)
{
return !(left == right);
}
public static implicit operator PointerValue(nuint address)
{
return new PointerValue(address);
}
public static implicit operator PointerValue(nint address)
{
return new PointerValue((nuint)address);
}
public static implicit operator PointerValue(int address)
{
return new PointerValue((nuint)address);
}
public static implicit operator PointerValue(uint address)
{
return new PointerValue(address);
}
public static PointerValue Parse(string s)
{
if (s.StartsWith("0x", StringComparison.InvariantCultureIgnoreCase)
&& nuint.TryParse(s.AsSpan(2), System.Globalization.NumberStyles.HexNumber, null, out var v))
{
return new PointerValue(v);
}
throw new FormatException($"Invalid pointer format: {s}");
}
}
+22
View File
@@ -0,0 +1,22 @@
using System.Runtime.InteropServices;
namespace Daybreak.API.Interop;
public sealed class UnmanagedStruct<T> : IDisposable
where T : struct
{
public nint Address { get; private set; }
public UnmanagedStruct(T value)
{
this.Address = Marshal.AllocHGlobal(Marshal.SizeOf<T>());
Marshal.StructureToPtr(value, this.Address, false);
}
public void Dispose()
{
Marshal.DestroyStructure<T>(this.Address);
Marshal.FreeHGlobal(this.Address);
this.Address = 0x0;
}
}
@@ -0,0 +1,29 @@
using Microsoft.CorrelationVector;
using Microsoft.Extensions.Options;
using Net.Sdk.Web;
using Net.Sdk.Web.Options;
using Serilog.Context;
namespace Daybreak.API.Logging;
public sealed class LoggingEnrichmentMiddleware(IOptions<CorrelationVectorOptions> options)
: IMiddleware
{
private readonly CorrelationVectorOptions options = options.Value;
public Task InvokeAsync(HttpContext context, RequestDelegate next)
{
if (!context.Request.Headers.TryGetValue(this.options.Header, out var value) ||
value.FirstOrDefault() is not string cvString ||
CorrelationVector.Parse(cvString) is not CorrelationVector cv)
{
cv = new CorrelationVector();
}
cv = CorrelationVector.Extend(cv.ToString());
context.SetCorrelationVector(cv);
LogContext.PushProperty("CorrelationVector", cv);
HeaderDictionaryExtensions.Append(context.Response.Headers, this.options.Header, context.GetCorrelationVector().ToString());
return next(context);
}
}
@@ -0,0 +1,31 @@
using Serilog.Settings.Configuration;
using Serilog.Sinks.SystemConsole.Themes;
using Serilog;
using Net.Sdk.Web.Options;
namespace Daybreak.API.Logging;
public static class WebApplicationBuilderExtensions
{
public static WebApplicationBuilder WithLogging(this WebApplicationBuilder builder)
{
builder.Logging.ClearProviders();
builder.Logging.AddSerilog(new LoggerConfiguration()
.ReadFrom.Configuration(builder.Configuration, new ConfigurationReaderOptions
{
SectionName = "Logging"
})
.Enrich.FromLogContext()
.WriteTo.Console(
outputTemplate: "[{Timestamp:yyyy-MM-dd HH:mm:ss}] {Level:u4}: [{SourceContext}] [{CorrelationVector}] {NewLine}{Message:lj}{NewLine}{Exception}",
theme: AnsiConsoleTheme.Sixteen)
.WriteTo.File(
outputTemplate: "[{Timestamp:yyyy-MM-dd HH:mm:ss}] {Level:u4}: [{SourceContext}] [{CorrelationVector}] {NewLine}{Message:lj}{NewLine}{Exception}",
path: "Daybreak.API.log")
.CreateLogger());
builder.Services.AddScoped<LoggingEnrichmentMiddleware>();
builder.Services.AddOptions<CorrelationVectorOptions>()
.Bind(builder.Configuration.GetSection("Correlation"));
return builder;
}
}
@@ -0,0 +1,10 @@
namespace Daybreak.API.Logging;
public static class WebApplicationExtensions
{
public static WebApplication UseLogging(this WebApplication app)
{
app.UseMiddleware<LoggingEnrichmentMiddleware>();
return app;
}
}
+9
View File
@@ -0,0 +1,9 @@
using Daybreak.API.Interop;
namespace Daybreak.API.Models;
public sealed class AddressState
{
public required string Name { get; init; }
public required PointerValue Address { get; init; }
}
+38
View File
@@ -0,0 +1,38 @@
using System.Core.Extensions;
using System.Extensions;
namespace Daybreak.API.Models;
public sealed class AsyncRefreshCache<T>(TimeSpan refreshRate, Func<CancellationToken, ValueTask<T>> refreshFunc)
{
private readonly Func<CancellationToken, ValueTask<T>> refreshFunc = refreshFunc.ThrowIfNull();
private readonly SemaphoreSlim semaphoreSlim = new(1);
private T? cache;
private DateTime lastUpdateDateTime = DateTime.MinValue;
private TimeSpan refreshRate = refreshRate;
public async ValueTask<T> GetAsync(CancellationToken cancellationToken)
{
if (this.cache is not null && DateTime.UtcNow - this.lastUpdateDateTime < this.refreshRate)
{
return this.cache;
}
using var ctx = await this.semaphoreSlim.Acquire(cancellationToken);
if (DateTime.UtcNow - this.lastUpdateDateTime > this.refreshRate ||
this.cache is null)
{
this.lastUpdateDateTime = DateTime.UtcNow;
this.cache = await this.refreshFunc(cancellationToken);
}
return this.cache;
}
public void UpdateRefreshRate(TimeSpan newRefreshRate)
{
this.refreshRate = newRefreshRate;
}
}
+23
View File
@@ -0,0 +1,23 @@
using System.Core.Extensions;
namespace Daybreak.API.Models;
public sealed class ByteConsumerEntry(
Guid id, TimeSpan freq, Action<ReadOnlySpan<byte>> handler)
{
private readonly Action<ReadOnlySpan<byte>> handler = handler.ThrowIfNull();
private DateTimeOffset lastConsume = DateTimeOffset.MinValue;
public Guid Id { get; } = id;
public TimeSpan Frequency { get; } = freq;
public void TryConsume(DateTimeOffset currentTime, ReadOnlySpan<byte> value)
{
if (currentTime - this.lastConsume >= this.Frequency)
{
this.handler(value);
this.lastConsume = currentTime;
}
}
}
@@ -0,0 +1,14 @@
using System.Core.Extensions;
namespace Daybreak.API.Models;
public sealed class CallbackRegistration(Guid uid, Action onDispose) : IDisposable
{
private readonly Guid uid = uid;
private readonly Action onDispose = onDispose.ThrowIfNull();
public void Dispose()
{
this.onDispose();
}
}
+24
View File
@@ -0,0 +1,24 @@
namespace Daybreak.API.Models;
public enum Channel : uint
{
Alliance = 0,
Allies = 1, // coop with two groups for instance.
GWCA1 = 2,
All = 3,
GWCA2 = 4,
Moderator = 5,
Emote = 6,
Warning = 7, // shows in the middle of the screen and does not parse <c> tags
GWCA3 = 8,
Guild = 9,
Global = 10,
Group = 11,
Trade = 12,
Advisory = 13,
Whisper = 14,
Count,
// non-standard channel, but useful.
Command
};
@@ -0,0 +1,14 @@
using Microsoft.Extensions.Diagnostics.HealthChecks;
using System.Text.Json;
using System.Text.Json.Serialization;
namespace Daybreak.API.Models;
public sealed class HealthCheckEntryResponse
{
[JsonConverter(typeof(JsonStringEnumConverter<HealthStatus>))]
public required HealthStatus Status { get; set; }
public required string Description { get; set; }
public required Dictionary<string, JsonElement> Data { get; set; }
public required List<string> Tags { get; set; }
}
@@ -0,0 +1,13 @@
using Microsoft.Extensions.Diagnostics.HealthChecks;
using System.Text.Json.Serialization;
namespace Daybreak.API.Models;
public sealed class HealthCheckResponse
{
public required int ProcessId { get; set; }
[JsonConverter(typeof(JsonStringEnumConverter<HealthStatus>))]
public required HealthStatus Status { get; set; }
public required TimeSpan TotalDuration { get; set; }
public required Dictionary<string, HealthCheckEntryResponse> Entries { get; set; }
}
+12
View File
@@ -0,0 +1,12 @@
using Daybreak.API.Interop;
namespace Daybreak.API.Models;
public sealed class HookState
{
public required bool Hooked { get; init; }
public required string Name { get; init; }
public required PointerValue TargetAddress { get; init; }
public required PointerValue ContinueAddress { get; init; }
public required PointerValue DetourAddress { get; init; }
}
+9
View File
@@ -0,0 +1,9 @@
namespace Daybreak.API.Models;
public interface IWorkItem
{
CancellationToken CancellationToken { get; }
void Execute();
void Cancel();
void Exception(Exception ex);
}
+154
View File
@@ -0,0 +1,154 @@
namespace Daybreak.API.Models;
public enum UIMessage : uint
{
None = 0x0,
Resize = 0x8,
InitFrame = 0x9,
DestroyFrame = 0xb,
KeyDown = 0x1e, // wparam = UIPacket::kKeyAction*
KeyUp = 0x20, // wparam = UIPacket::kKeyAction*
MouseClick = 0x22, // wparam = UIPacket::kMouseClick*
MouseClick2 = 0x2e, // wparam = UIPacket::kMouseAction*
MouseAction = 0x2f, // wparam = UIPacket::kMouseAction*
SetLayout = 0x33,
FrameMessage_0x47 = 0x47, // Multiple uses depending on frame
UpdateAgentEffects = 0x10000000 | 0x9,
RerenderAgentModel = 0x10000000 | 0x7, // wparam = uint32_t agent_id
AgentSpeechBubble = 0x10000000 | 0x17,
ShowAgentNameTag = 0x10000000 | 0x19, // wparam = AgentNameTagInfo*
HideAgentNameTag = 0x10000000 | 0x1A,
SetAgentNameTagAttribs = 0x10000000 | 0x1B, // wparam = AgentNameTagInfo*
ChangeTarget = 0x10000000 | 0x20, // wparam = UIPacket::kChangeTarget*
AgentStartCasting = 0x10000000 | 0x27, // wparam = UIPacket::kAgentStartCasting*
ShowMapEntryMessage = 0x10000000 | 0x29, // wparam = { wchar_t* title, wchar_t* subtitle }
SetCurrentPlayerData = 0x10000000 | 0x2A, // fired after setting the worldcontext player name
PostProcessingEffect = 0x10000000 | 0x34, // Triggered when drunk. wparam = UIPacket::kPostProcessingEffect
HeroAgentAdded = 0x10000000 | 0x38, // hero assigned to agent/inventory/ai mode
HeroDataAdded = 0x10000000 | 0x39, // hero info received from server (name, level etc)
ShowXunlaiChest = 0x10000000 | 0x40,
MinionCountUpdated = 0x10000000 | 0x46,
MoraleChange = 0x10000000 | 0x47, // wparam = {agent id, morale percent }
LoginStateChanged = 0x10000000 | 0x50, // wparam = {bool is_logged_in, bool unk }
EffectAdd = 0x10000000 | 0x55, // wparam = {agent_id, GW::Effect*}
EffectRenew = 0x10000000 | 0x56, // wparam = GW::Effect*
EffectRemove = 0x10000000 | 0x57, // wparam = effect id
SkillActivated = 0x10000000 | 0x5b, // wparam ={ uint32_t agent_id , uint32_t skill_id }
UpdateSkillbar = 0x10000000 | 0x5E, // wparam ={ uint32_t agent_id , ... }
UpdateSkillsAvailable = 0x10000000 | 0x5f, // Triggered on a skill unlock, profession change or map load
TitleProgressUpdated = 0x10000000 | 0x65, // wparam = title_id
ExperienceGained = 0x10000000 | 0x66, // wparam = experience amount
WriteToChatLog = 0x10000000 | 0x7E, // wparam = UIPacket::kWriteToChatLog*. Triggered by the game when it wants to add a new message to chat.
WriteToChatLogWithSender = 0x10000000 | 0x7f, // wparam = UIPacket::kWriteToChatLogWithSender*. Triggered by the game when it wants to add a new message to chat.
AllyOrGuildMessage = 0x10000000 | 0x80, // wparam = UIPacket::kAllyOrGuildMessage*
PlayerChatMessage = 0x10000000 | 0x81, // wparam = UIPacket::kPlayerChatMessage*
FloatingWindowMoved = 0x10000000 | 0x83, // wparam = frame_id
FriendUpdated = 0x10000000 | 0x89, // wparam = { GW::Friend*, ... }
MapLoaded = 0x10000000 | 0x8A,
OpenWhisper = 0x10000000 | 0x90, // wparam = wchar* name
Logout = 0x10000000 | 0x9b, // wparam = { bool unknown, bool character_select }
CompassDraw = 0x10000000 | 0x9c, // wparam = UIPacket::kCompassDraw*
OnScreenMessage = 0x10000000 | 0xA0, // wparam = wchar_** encoded_string
DialogBody = 0x10000000 | 0xA4, // wparam = DialogBodyInfo*
DialogButton = 0x10000000 | 0xA1, // wparam = DialogButtonInfo*
TargetNPCPartyMember = 0x10000000 | 0xB1, // wparam = { uint32_t unk, uint32_t agent_id }
TargetPlayerPartyMember = 0x10000000 | 0xB2, // wparam = { uint32_t unk, uint32_t player_number }
VendorWindow = 0x10000000 | 0xB3, // wparam = UIPacket::kVendorWindow
VendorItems = 0x10000000 | 0xB7, // wparam = UIPacket::kVendorItems
VendorTransComplete = 0x10000000 | 0xB9, // wparam = *TransactionType
VendorQuote = 0x10000000 | 0xBB, // wparam = UIPacket::kVendorQuote
StartMapLoad = 0x10000000 | 0xC0, // wparam = { uint32_t map_id, ...}
WorldMapUpdated = 0x10000000 | 0xC5, // Triggered when an area in the world map has been discovered/updated
GuildMemberUpdated = 0x10000000 | 0xD8, // wparam = { GuildPlayer::name_ptr }
ShowHint = 0x10000000 | 0xDF, // wparam = { uint32_t icon_type, wchar_t* message_enc }
WeaponSetSwapComplete = 0x10000000 | 0xE7, // wparam = UIPacket::kWeaponSwap*
WeaponSetSwapCancel = 0x10000000 | 0xE8,
WeaponSetUpdated = 0x10000000 | 0xE9,
UpdateGoldCharacter = 0x10000000 | 0xEA, // wparam = { uint32_t unk, uint32_t gold_character }
UpdateGoldStorage = 0x10000000 | 0xEB, // wparam = { uint32_t unk, uint32_t gold_storage }
InventorySlotUpdated = 0x10000000 | 0xEC, // undocumented. Triggered when an item is moved into a slot
EquipmentSlotUpdated = 0x10000000 | 0xED, // undocumented. Triggered when an item is moved into a slot
InventorySlotCleared = 0x10000000 | 0xEF, // undocumented. Triggered when an item has been removed from a slot
EquipmentSlotCleared = 0x10000000 | 0xF0, // undocumented. Triggered when an item has been removed from a slot
PvPWindowContent = 0x10000000 | 0xF8,
PreStartSalvage = 0x10000000 | 0x100, // { uint32_t item_id, uint32_t kit_id }
TradePlayerUpdated = 0x10000000 | 0x103, // wparam = GW::TraderPlayer*
ItemUpdated = 0x10000000 | 0x104, // wparam = UIPacket::kItemUpdated*
MapChange = 0x10000000 | 0x10F, // wparam = map id
CalledTargetChange = 0x10000000 | 0x113, // wparam = { player_number, target_id }
ErrorMessage = 0x10000000 | 0x117, // wparam = { int error_index, wchar_t* error_encoded_string }
PartyHardModeChanged = 0x10000000 | 0x118, // wparam = { int is_hard_mode }
PartyAddHenchman = 0x10000000 | 0x119,
PartyRemoveHenchman = 0x10000000 | 0x11a,
PartyAddHero = 0x10000000 | 0x11c,
PartyRemoveHero = 0x10000000 | 0x11d,
PartyAddPlayer = 0x10000000 | 0x122,
PartyRemovePlayer = 0x10000000 | 0x124,
DisableEnterMissionBtn = 0x10000000 | 0x128, // wparam = boolean (1 = disabled, 0 = enabled)
ShowCancelEnterMissionBtn = 0x10000000 | 0x12b,
PartyDefeated = 0x10000000 | 0x12d,
PartySearchInviteReceived = 0x10000000 | 0x135, // wparam = UIPacket::kPartySearchInviteReceived*
PartySearchInviteSent = 0x10000000 | 0x137,
PartyShowConfirmDialog = 0x10000000 | 0x138, // wparam = UIPacket::kPartyShowConfirmDialog
PreferenceEnumChanged = 0x10000000 | 0x13E, // wparam = UiPacket::kPreferenceEnumChanged
PreferenceFlagChanged = 0x10000000 | 0x13F, // wparam = UiPacket::kPreferenceFlagChanged
PreferenceValueChanged = 0x10000000 | 0x140, // wparam = UiPacket::kPreferenceValueChanged
UIPositionChanged = 0x10000000 | 0x141, // wparam = UIPacket::kUIPositionChanged
QuestAdded = 0x10000000 | 0x149, // wparam = { quest_id, ... }
QuestDetailsChanged = 0x10000000 | 0x14A, // wparam = { quest_id, ... }
ClientActiveQuestChanged = 0x10000000 | 0x14C, // wparam = { quest_id, ... }. Triggered when the game requests the current quest to change
ServerActiveQuestChanged = 0x10000000 | 0x14E, // wparam = UIPacket::kServerActiveQuestChanged*. Triggered when the server requests the current quest to change
UnknownQuestRelated = 0x10000000 | 0x14F,
DungeonComplete = 0x10000000 | 0x151, // undocumented
MissionComplete = 0x10000000 | 0x152, // undocumented
VanquishComplete = 0x10000000 | 0x154, // undocumented
ObjectiveAdd = 0x10000000 | 0x155, // wparam = UIPacket::kObjectiveAdd*
ObjectiveComplete = 0x10000000 | 0x156, // wparam = UIPacket::kObjectiveComplete*
ObjectiveUpdated = 0x10000000 | 0x157, // wparam = UIPacket::kObjectiveUpdated*
TradeSessionStart = 0x10000000 | 0x160, // wparam = { trade_state, player_number }
TradeSessionUpdated = 0x10000000 | 0x166, // no args
TriggerLogoutPrompt = 0x10000000 | 0x16C, // no args
ToggleOptionsWindow = 0x10000000 | 0x16D, // no args
CheckUIState = 0x10000000 | 0x170, // Undocumented
RedrawItem = 0x10000000 | 0x172, // wparam = uint32_t item_id
CloseSettings = 0x10000000 | 0x174, // Undocumented
ChangeSettingsTab = 0x10000000 | 0x175, // wparam = uint32_t is_interface_tab
GuildHall = 0x10000000 | 0x177, // wparam = gh key (uint32_t[4])
LeaveGuildHall = 0x10000000 | 0x179,
Travel = 0x10000000 | 0x17A,
OpenWikiUrl = 0x10000000 | 0x17B, // wparam = char* url
AppendMessageToChat = 0x10000000 | 0x189, // wparam = wchar_t* message
HideHeroPanel = 0x10000000 | 0x197, // wparam = hero_id
ShowHeroPanel = 0x10000000 | 0x198, // wparam = hero_id
GetInventoryAgentId = 0x10000000 | 0x19c, // wparam = 0, lparam = uint32_t* agent_id_out. Used to fetch which agent is selected
EquipItem = 0x10000000 | 0x19d, // wparam = { item_id, agent_id }
MoveItem = 0x10000000 | 0x19e, // wparam = { item_id, to_bag, to_slot, bool prompt }
InitiateTrade = 0x10000000 | 0x1A0,
InventoryAgentChanged = 0x10000000 | 0x1b0, // Triggered when inventory needs updating due to agent change; no args
OpenTemplate = 0x10000000 | 0x1B9, // wparam = GW::UI::ChatTemplate*
// GWCA Client to Server commands. Only added the ones that are used for hooks, everything else goes straight into GW
SendLoadSkillTemplate = 0x30000000 | 0x3, // wparam = SkillbarMgr::SkillTemplate*
SendPingWeaponSet = 0x30000000 | 0x4, // wparam = UIPacket::kSendPingWeaponSet*
SendMoveItem = 0x30000000 | 0x5, // wparam = UIPacket::kSendMoveItem*
SendMerchantRequestQuote = 0x30000000 | 0x6, // wparam = UIPacket::kSendMerchantRequestQuote*
SendMerchantTransactItem = 0x30000000 | 0x7, // wparam = UIPacket::kSendMerchantTransactItem*
SendUseItem = 0x30000000 | 0x8, // wparam = UIPacket::kSendUseItem*
SendSetActiveQuest = 0x30000000 | 0x9, // wparam = uint32_t quest_id
SendAbandonQuest = 0x30000000 | 0xA, // wparam = uint32_t quest_id
SendChangeTarget = 0x30000000 | 0xB, // wparam = UIPacket::kSendChangeTarget* // e.g. tell the gw client to focus on a different target
SendCallTarget = 0x30000000 | 0x13, // wparam = { uint32_t call_type, uint32_t agent_id } // also used to broadcast morale, death penalty, "I'm following X", etc
SendDialog = 0x30000000 | 0x16, // wparam = dialog_id // internal use
StartWhisper = 0x30000000 | 0x17, // wparam = UIPacket::kStartWhisper*
GetSenderColor = 0x30000000 | 0x18, // wparam = UIPacket::kGetColor* // Get chat sender color depending on channel, output object passed by reference
GetMessageColor = 0x30000000 | 0x19, // wparam = UIPacket::kGetColor* // Get chat message color depending on channel, output object passed by reference
SendChatMessage = 0x30000000 | 0x1B, // wparam = UIPacket::kSendChatMessage*
LogChatMessage = 0x30000000 | 0x1D, // wparam = UIPacket::kLogChatMessage*. Triggered when a message wants to be added to the persistent chat log.
RecvWhisper = 0x30000000 | 0x1E, // wparam = UIPacket::kRecvWhisper*
PrintChatMessage = 0x30000000 | 0x1F, // wparam = UIPacket::kPrintChatMessage*. Triggered when a message wants to be added to the in-game chat window.
SendWorldAction = 0x30000000 | 0x20, // wparam = UIPacket::kSendWorldAction*
SetRendererValue = 0x30000000 | 0x21 // wparam = UIPacket::kSetRendererValue
};
+46
View File
@@ -0,0 +1,46 @@
using System.Runtime.InteropServices;
namespace Daybreak.API.Models;
public static class UIPackets
{
public enum MouseButtons
{
Left = 0x0,
Middle = 0x1,
Right = 0x2
}
[StructLayout(LayoutKind.Sequential, Pack = 1)]
public readonly struct LogChatMessage(string message, Channel channel)
{
[MarshalAs(UnmanagedType.LPWStr)]
public readonly string Message = message;
public readonly Channel Channel = channel;
};
[StructLayout(LayoutKind.Sequential, Pack = 1)]
public readonly struct UIChatMessage(Channel channel, string message, Channel channel2)
{
public readonly Channel Channel = channel;
[MarshalAs(UnmanagedType.LPWStr)]
public readonly string Message = message;
public readonly Channel Channel2 = channel2;
}
[StructLayout(LayoutKind.Sequential, Pack = 1)]
public readonly struct KeyAction(uint key)
{
public readonly uint Key = key;
public readonly uint WParam = 0x4000;
public readonly uint LParam = 0x0006;
}
[StructLayout(LayoutKind.Sequential, Pack = 1)]
public readonly struct MouseClick(MouseButtons mouseButton, uint isDoubleClick, uint unknownTypeScreenPos)
{
public readonly MouseButtons MouseButton = mouseButton;
public readonly uint IsDoubleClick = isDoubleClick;
public readonly uint UnknownTypeScreenPos = unknownTypeScreenPos;
}
}
+80
View File
@@ -0,0 +1,80 @@
namespace Daybreak.API.Models;
public sealed class WorkItem(
Action action,
TaskCompletionSource tcs,
CancellationToken token) : IWorkItem
{
private readonly Action action = action;
private readonly TaskCompletionSource tcs = tcs;
public CancellationToken CancellationToken { get; } = token;
public void Execute()
{
if (this.CancellationToken.IsCancellationRequested)
{
this.tcs.TrySetCanceled(this.CancellationToken);
return;
}
try
{
this.action();
this.tcs.TrySetResult();
}
catch (Exception ex)
{
this.tcs.TrySetException(ex);
}
}
public void Cancel()
{
this.tcs.TrySetCanceled(this.CancellationToken);
}
public void Exception(Exception ex)
{
this.tcs.TrySetException(ex);
}
}
public sealed class WorkItem<T>(
Func<T> func,
TaskCompletionSource<T> tcs,
CancellationToken token) : IWorkItem
{
private readonly Func<T> func = func;
private readonly TaskCompletionSource<T> tcs = tcs;
public CancellationToken CancellationToken { get; } = token;
public void Execute()
{
if (this.CancellationToken.IsCancellationRequested)
{
this.tcs.TrySetCanceled(this.CancellationToken);
return;
}
try
{
this.tcs.TrySetResult(this.func());
}
catch (Exception ex)
{
this.tcs.TrySetException(ex);
}
}
public void Cancel()
{
this.tcs.TrySetCanceled(this.CancellationToken);
}
public void Exception(Exception ex)
{
this.tcs.TrySetException(ex);
}
}
+51
View File
@@ -0,0 +1,51 @@
using System.Runtime.InteropServices;
namespace Daybreak.API;
internal unsafe static partial class NativeMethods
{
public const int STD_OUTPUT_HANDLE = -11;
public const uint ENABLE_VIRTUAL_TERMINAL_PROCESSING = 0x0004;
public const uint ENABLE_PROCESSED_OUTPUT = 0x0001;
public const int WM_KEYDOWN = 0x0100;
public const int WM_KEYUP = 0x0101;
public const int WM_CHAR = 0x0102;
public const int VK_RIGHT = 0x27;
public const uint MEM_COMMIT = 0x1000;
public const uint MEM_RESERVE = 0x2000;
public const uint PAGE_EXECUTE_READWRITE = 0x40;
[LibraryImport("kernel32.dll", SetLastError = true)]
[return: MarshalAs(UnmanagedType.Bool)]
public static partial bool AllocConsole();
[LibraryImport("kernel32.dll", SetLastError = true)]
public static partial nint GetConsoleWindow();
[LibraryImport("kernel32.dll", SetLastError = true)]
public static partial nint GetStdHandle(int nStdHandle);
[LibraryImport("kernel32.dll")]
[return: MarshalAs(UnmanagedType.Bool)]
public static partial bool GetConsoleMode(nint hConsoleHandle, out uint lpMode);
[LibraryImport("kernel32.dll")]
[return: MarshalAs(UnmanagedType.Bool)]
public static partial bool SetConsoleMode(nint hConsoleHandle, uint dwMode);
[LibraryImport("user32.dll")]
public static partial nint SendMessageW(nint hWnd, int Msg, nint wParam, nint lParam);
[LibraryImport("kernel32.dll", SetLastError = true)]
public static partial nint VirtualAlloc(
nint lpAddress, nuint dwSize,
uint flAllocationType, uint flProtect);
[LibraryImport("kernel32.dll", SetLastError = true)]
[return: MarshalAs(UnmanagedType.Bool)]
public static partial bool VirtualProtect(
void* lpAddress, nuint dwSize,
uint flNewProtect, out uint lpflOldProtect);
}
@@ -0,0 +1,12 @@
{
"profiles": {
"Daybreak.API": {
"commandName": "Project",
"launchBrowser": true,
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
},
"applicationUrl": "https://localhost:51249;http://localhost:51250"
}
}
}
@@ -0,0 +1,39 @@
using Daybreak.API.Models;
using Daybreak.Shared.Models.Api;
using System.Text.Json;
using System.Text.Json.Serialization;
namespace Daybreak.API.Serialization;
[JsonSerializable(typeof(string))]
[JsonSerializable(typeof(byte[]))]
[JsonSerializable(typeof(int))]
[JsonSerializable(typeof(uint))]
[JsonSerializable(typeof(nuint))]
[JsonSerializable(typeof(bool))]
[JsonSerializable(typeof(Dictionary<string, string>))]
[JsonSerializable(typeof(List<string>))]
[JsonSerializable(typeof(JsonElement))]
[JsonSerializable(typeof(HealthCheckResponse))]
[JsonSerializable(typeof(HealthCheckEntryResponse))]
[JsonSerializable(typeof(HookState))]
[JsonSerializable(typeof(ArraySegment<HookState>))]
[JsonSerializable(typeof(AddressState))]
[JsonSerializable(typeof(ArraySegment<AddressState>))]
[JsonSerializable(typeof(MainPlayerState))]
[JsonSerializable(typeof(QuestLogInformation))]
[JsonSerializable(typeof(QuestInformation))]
[JsonSerializable(typeof(MainPlayerInformation))]
[JsonSerializable(typeof(CharacterSelectInformation))]
[JsonSerializable(typeof(CharacterSelectEntry))]
[JsonSerializable(typeof(BuildEntry))]
[JsonSerializable(typeof(List<BuildEntry>))]
[JsonSerializable(typeof(PartyLoadoutEntry))]
[JsonSerializable(typeof(List<PartyLoadout>))]
[JsonSerializable(typeof(InstanceInfo))]
[JsonSerializable(typeof(TitleInfo))]
[JsonSerializable(typeof(LoginInfo))]
[JsonSerializable(typeof(MainPlayerBuildContext))]
public partial class ApiJsonSerializerContext : JsonSerializerContext
{
}
@@ -0,0 +1,14 @@
namespace Daybreak.API.Serialization;
public static class WebApplicationBuilderExtensions
{
public static WebApplicationBuilder WithSerializationContext(this WebApplicationBuilder builder)
{
builder.Services.ConfigureHttpJsonOptions(options =>
{
options.SerializerOptions.TypeInfoResolverChain.Insert(0, new ApiJsonSerializerContext());
});
return builder;
}
}
@@ -0,0 +1,56 @@
using Daybreak.Shared.Models;
using Daybreak.Shared.Services.MDns;
using Microsoft.AspNetCore.Hosting.Server;
using Microsoft.AspNetCore.Hosting.Server.Features;
using System.Extensions.Core;
namespace Daybreak.API.Services;
public sealed class ApiAdvertisingService(
IServer server,
IMDomainNameService mDnsService,
IHostApplicationLifetime lifetime,
ILogger<ApiAdvertisingService> logger) : IHostedService
{
private const string ProcessIdPlaceholder = "{PID}";
private const string ServiceName = $"daybreak-api-{ProcessIdPlaceholder}";
private const string ServiceSubType = "daybreak-api";
private readonly IServerAddressesFeature serverAddressesFeature = server.Features.Get<IServerAddressesFeature>() ?? throw new InvalidOperationException("Server does not support addresses feature.");
private readonly IMDomainNameService mDnsService = mDnsService;
private readonly IHostApplicationLifetime lifetime = lifetime;
private readonly ILogger<ApiAdvertisingService> logger = logger;
private DnsRegistrationToken? dnsToken;
public Task StartAsync(CancellationToken cancellationToken)
{
this.lifetime.ApplicationStarted.Register(this.RegisterDomain);
return Task.CompletedTask;
}
public Task StopAsync(CancellationToken cancellationToken)
{
this.dnsToken?.Dispose();
return Task.CompletedTask;
}
private void RegisterDomain()
{
var scopedLogger = this.logger.CreateScopedLogger();
var addresses = this.serverAddressesFeature?.Addresses.ToArray() ?? [];
var port = addresses
.Select(a => new Uri(a).Port)
.Distinct()
.FirstOrDefault();
scopedLogger.LogInformation("Found port {port}", port);
if (port is 0)
{
throw new InvalidOperationException("Unable to start advertising service. No port found");
}
var dnsEntry = ServiceName.Replace(ProcessIdPlaceholder, Environment.ProcessId.ToString());
scopedLogger.LogInformation("Advertising service {service}:{port}", dnsEntry, port);
this.dnsToken = this.mDnsService.RegisterDomain(dnsEntry, (ushort)port, ServiceSubType);
}
}
@@ -0,0 +1,313 @@
using Daybreak.API.Services.Interop;
using Daybreak.Shared.Models.Api;
using System.Core.Extensions;
using System.Extensions.Core;
using System.Runtime.InteropServices;
using ZLinq;
namespace Daybreak.API.Services;
public sealed class CharacterSelectService(
InstanceContextService instanceContextService,
PlatformContextService platformContextService,
UIContextService uiContextService,
GameThreadService gameThreadService,
GameContextService gameContextService,
ILogger<CharacterSelectService> logger)
{
[StructLayout(LayoutKind.Sequential, Pack = 1)]
private readonly struct LogOutMessage(uint unknown, uint characterSelect)
{
public readonly uint Unknown = unknown;
public readonly uint CharacterSelect = characterSelect;
}
private readonly InstanceContextService instanceContextService = instanceContextService.ThrowIfNull();
private readonly PlatformContextService platformContextService = platformContextService.ThrowIfNull();
private readonly UIContextService uiContextService = uiContextService.ThrowIfNull();
private readonly GameThreadService gameThreadService = gameThreadService.ThrowIfNull();
private readonly GameContextService gameContextService = gameContextService.ThrowIfNull();
private readonly ILogger<CharacterSelectService> logger = logger.ThrowIfNull();
public Task<CharacterSelectInformation?> GetCharacterSelectInformation(CancellationToken cancellationToken)
{
var scopedLogger = this.logger.CreateScopedLogger();
return this.gameThreadService.QueueOnGameThread(() =>
{
unsafe
{
var gameContext = this.gameContextService.GetGameContext();
if (gameContext.IsNull ||
gameContext.Pointer->WorldContext is null)
{
scopedLogger.LogError("Game context is not initialized");
return default;
}
var availableCharsContext = this.gameContextService.GetAvailableChars();
if (availableCharsContext.IsNull)
{
scopedLogger.LogError("Available characters context is not initialized");
return default;
}
var currentUuid = gameContext.Pointer->CharContext->PlayerUuid.ToString();
var availableChars = new List<CharacterSelectEntry>((int)availableCharsContext.Pointer->Size);
foreach(var charContext in *availableCharsContext.Pointer)
{
var nameSpan = charContext.Name.AsSpan();
var name = new string(nameSpan[..nameSpan.IndexOf('\0')]);
availableChars.Add(new CharacterSelectEntry(
charContext.Uuid.ToString(),
name,
charContext.Primary,
charContext.Secondary,
charContext.Campaign,
charContext.MapId,
charContext.Level,
charContext.IsPvp));
}
var currentCharacter = availableChars.FirstOrDefault(c => c.Uuid == currentUuid);
return new CharacterSelectInformation(currentCharacter, availableChars);
}
}, cancellationToken);
}
public async Task<bool> ChangeCharacterByName(string characterName, CancellationToken cancellationToken)
{
var scopedLogger = this.logger.CreateScopedLogger();
if (await this.ValidateState(cancellationToken) is false)
{
scopedLogger.LogError("Cannot change character while in loading state");
return false;
}
await this.TriggerLogOut(cancellationToken);
var indexResult = await this.WaitForLoginScreenAndGetCurrentAndDesiredIndex(characterName, cancellationToken);
if (indexResult is null)
{
scopedLogger.LogError("Failed to find index by name {name}", characterName);
return false;
}
var currentIndex = indexResult.Value.CurrentIndex;
var desiredIndex = indexResult.Value.DesiredIndex;
scopedLogger.LogInformation("Changing character to {name} with index {index}", characterName, desiredIndex);
var navigateResult = await this.NavigateToCharAndPlay(currentIndex, desiredIndex, cancellationToken);
if (!navigateResult)
{
scopedLogger.LogError("Failed to navigate to character {name} with index {index}", characterName, desiredIndex);
return false;
}
return true;
}
public async Task<bool> ChangeCharacterByUuid(string uuid, CancellationToken cancellationToken)
{
var scopedLogger = this.logger.CreateScopedLogger();
if (await this.ValidateState(cancellationToken) is false)
{
scopedLogger.LogError("Cannot change character while in loading state");
return false;
}
var desiredCharName = await this.GetCharNameByUuid(uuid, cancellationToken);
if (desiredCharName is null)
{
scopedLogger.LogError("Failed to find character with UUID: {uuid}", uuid);
return false;
}
await this.TriggerLogOut(cancellationToken);
var indexResult = await this.WaitForLoginScreenAndGetCurrentAndDesiredIndex(desiredCharName, cancellationToken);
if (indexResult is null)
{
scopedLogger.LogError("Resolved character by uuid {uuid} but failed to find index by name {name}", uuid, desiredCharName);
return false;
}
var currentIndex = indexResult.Value.CurrentIndex;
var desiredIndex = indexResult.Value.DesiredIndex;
scopedLogger.LogInformation("Changing character to {name} with index {index} and uuid {uuid}", desiredCharName, desiredIndex, uuid);
var navigateResult = await this.NavigateToCharAndPlay(currentIndex, desiredIndex, cancellationToken);
if (!navigateResult)
{
scopedLogger.LogError("Failed to navigate to character {name} with index {index} and uuid {uuid}", desiredCharName, desiredIndex, uuid);
return false;
}
return true;
}
private async Task<bool> ValidateState(CancellationToken cancellationToken)
{
return await this.gameThreadService.QueueOnGameThread(() =>
{
return this.instanceContextService.GetInstanceType() is not API.Interop.GuildWars.InstanceType.Loading;
}, cancellationToken);
}
private async Task<bool> NavigateToCharAndPlay(uint currentIndex, uint desiredIndex, CancellationToken cancellationToken)
{
var scopedLogger = this.logger.CreateScopedLogger();
while (!cancellationToken.IsCancellationRequested)
{
await Task.Delay(100, cancellationToken);
var result = await this.gameThreadService.QueueOnGameThread(() =>
{
unsafe
{
var preGameContext = this.gameContextService.GetPreGameContext();
if (preGameContext.IsNull)
{
scopedLogger.LogError("Pre-game context is not initialized");
return false;
}
var hwnd = this.platformContextService.GetWindowHandle();
if (hwnd is null or 0)
{
scopedLogger.LogError("Failed to get window handle");
return false;
}
if (preGameContext.Pointer->Index1 == currentIndex)
{
return false; //Not moved yet
}
if (preGameContext.Pointer->Index1 == desiredIndex)
{
// We're on the desired character. Trigger play
NativeMethods.SendMessageW((nint)hwnd.Value, NativeMethods.WM_KEYDOWN, 0x50, 0x00190001);
NativeMethods.SendMessageW((nint)hwnd.Value, NativeMethods.WM_CHAR, 0x70, 0x00190001);
NativeMethods.SendMessageW((nint)hwnd.Value, NativeMethods.WM_KEYUP, 0x50, 0x00190001);
return true;
}
currentIndex = preGameContext.Pointer->Index1;
NativeMethods.SendMessageW((nint)hwnd.Value, NativeMethods.WM_KEYDOWN, NativeMethods.VK_RIGHT, 0x014D0001);
NativeMethods.SendMessageW((nint)hwnd.Value, NativeMethods.WM_KEYUP, NativeMethods.VK_RIGHT, 0x014D0001);
return false;
}
}, cancellationToken);
if (result)
{
return true;
}
}
return false;
}
private async Task<string?> GetCharNameByUuid(string uuid, CancellationToken cancellationToken)
{
var scopedLogger = this.logger.CreateScopedLogger();
return await this.gameThreadService.QueueOnGameThread(() =>
{
unsafe
{
var gameContext = this.gameContextService.GetGameContext();
if (gameContext.IsNull)
{
scopedLogger.LogError("Game context is not initialized");
return default;
}
var availableCharsContext = this.gameContextService.GetAvailableChars();
if (availableCharsContext.IsNull)
{
scopedLogger.LogError("Available characters context is not initialized");
return default;
}
foreach (var charContext in *availableCharsContext.Pointer)
{
if (charContext.Uuid.ToString() != uuid)
{
continue;
}
var nameSpan = charContext.Name.AsSpan();
var name = new string(nameSpan[..nameSpan.IndexOf('\0')]);
return name;
}
return default;
}
}, cancellationToken);
}
private async Task TriggerLogOut(CancellationToken cancellationToken)
{
await this.gameThreadService.QueueOnGameThread(() =>
{
var logoutMessage = new LogOutMessage(0, 0);
unsafe
{
this.uiContextService.SendMessage(Models.UIMessage.Logout, (uint)&logoutMessage, 0);
}
}, cancellationToken);
}
private async Task<(uint DesiredIndex, uint CurrentIndex)?> WaitForLoginScreenAndGetCurrentAndDesiredIndex(string desiredCharName, CancellationToken cancellationToken)
{
uint? desiredIndex = default;
uint? currentIndex = default;
while (!cancellationToken.IsCancellationRequested)
{
await Task.Delay(100, cancellationToken);
var ready = await this.gameThreadService.QueueOnGameThread(() =>
{
unsafe
{
var preGameContext = this.gameContextService.GetPreGameContext();
if (preGameContext.IsNull)
{
return false;
}
var uiState = 10U;
this.uiContextService.SendMessage(Models.UIMessage.CheckUIState, 0, (uint)&uiState);
var loginScreen = uiState == 2;
if (!loginScreen)
{
return false;
}
for (var i = 0U; i < preGameContext.Pointer->LoginCharacters.Size; i++)
{
var loginCharacter = preGameContext.Pointer->LoginCharacters.Buffer[i];
var charNameSpan = loginCharacter.CharacterName.AsSpan();
var charName = new string(charNameSpan[..charNameSpan.IndexOf('\0')]);
if (charName == desiredCharName)
{
desiredIndex = i;
currentIndex = 0xffffffdd;
return true;
}
}
return false;
}
}, cancellationToken);
if (ready)
{
break;
}
}
if (desiredIndex is not null && currentIndex is not null)
{
return (desiredIndex.Value, currentIndex.Value);
}
return default;
}
}
+77
View File
@@ -0,0 +1,77 @@
using Daybreak.API.Interop;
using Daybreak.API.Interop.GuildWars;
using Daybreak.API.Models;
using Daybreak.API.Services.Interop;
using System.Core.Extensions;
using System.Extensions;
namespace Daybreak.API.Services;
public sealed class ChatService(
InstanceContextService instanceContextService,
GameThreadService gameThreadService,
UIContextService uiContextService)
{
private readonly SemaphoreSlim semaphoreSlim = new(1);
private readonly InstanceContextService instanceContextService = instanceContextService.ThrowIfNull();
private readonly GameThreadService gameThreadService = gameThreadService.ThrowIfNull();
private readonly UIContextService uiContextService = uiContextService.ThrowIfNull();
/// <summary>
/// Adds a message to the chat. It does not send the message, it only shows it to the player.
/// </summary>
public async ValueTask AddMessageAsync(string message, string? sender, Channel channel, CancellationToken cancellationToken)
{
using var ctx = await this.semaphoreSlim.Acquire(cancellationToken);
await this.gameThreadService.QueueOnGameThread(() =>
{
if (this.instanceContextService.GetInstanceType() is InstanceType.Loading)
{
return;
}
var encodedMessage = string.Create(
3 + message.Length,
message,
static (span, msg) =>
{
span[0] = '\u0108';
span[1] = '\u0107';
msg.AsSpan().CopyTo(span[2..]);
span[^1] = '\u0001';
});
var encodedSender = sender is not null
? string.Create(
3 + sender.Length,
sender,
static (span, msg) =>
{
span[0] = '\u0108';
span[1] = '\u0107';
msg.AsSpan().CopyTo(span[2..]);
span[^1] = '\u0001';
})
: default;
var encoded = encodedSender is null
? encodedMessage
: string.Create(
encodedMessage.Length + encodedSender.Length + 6,
(encodedMessage, encodedSender),
static (span, state) =>
{
span[0] = '\u076b';
span[1] = '\u010a';
state.encodedSender.AsSpan().CopyTo(span[2..]);
span[2 + state.encodedSender.Length] = '\u0001';
span[3 + state.encodedSender.Length] = '\u010b';
state.encodedMessage.AsSpan().CopyTo(span[(4 + state.encodedSender.Length)..]);
span[^2] = '\u0001';
});
using var packet = new UnmanagedStruct<UIPackets.UIChatMessage>(new UIPackets.UIChatMessage(channel, encoded, channel));
this.uiContextService.SendMessage(UIMessage.WriteToChatLog, (nuint)packet.Address, 0x0);
}, cancellationToken);
}
}
@@ -0,0 +1,101 @@
using Daybreak.API.Interop;
using Daybreak.API.Interop.GuildWars;
using Daybreak.API.Models;
using System.Core.Extensions;
using System.Extensions.Core;
namespace Daybreak.API.Services.Interop;
public unsafe sealed class AgentContextService : IAddressHealthService
{
private const string AgentArrayMask = "xxxxxxx";
private const int AgentArrayOffset = -0x4;
private static readonly byte[] AgentArrayAddressPattern = [0x8b, 0x0c, 0x90, 0x85, 0xc9, 0x74, 0x19];
private const string PlayerAgentIdMask = "xx????xxxx";
private const int PlayerAgentIdOffset = -0xE;
private static readonly byte[] PlayerAgentIdAddressPattern = [0x5d, 0xe9, 0x00, 0x00, 0x00, 0x00, 0x55, 0x8b, 0xec, 0x53];
private readonly MemoryScanningService memoryScanningService;
private readonly GWAddressCache agentArrayAddress;
private readonly GWAddressCache playerAgentIdAddress;
private readonly ILogger<AgentContextService> logger;
public AgentContextService(
MemoryScanningService memoryScanningService,
ILogger<AgentContextService> logger)
{
this.logger = logger.ThrowIfNull();
this.memoryScanningService = memoryScanningService.ThrowIfNull();
this.agentArrayAddress = new GWAddressCache(this.GetAgentArrayAddress);
this.playerAgentIdAddress = new GWAddressCache(this.GetPreGameContextAddress);
}
public List<AddressState> GetAddressStates()
{
return [
new AddressState
{
Address = this.agentArrayAddress.GetAddress() ?? 0,
Name = nameof(this.agentArrayAddress),
},
new AddressState
{
Address = this.playerAgentIdAddress.GetAddress() ?? 0,
Name = nameof(this.playerAgentIdAddress),
},
];
}
public WrappedPointer<GuildWarsArray<WrappedPointer<AgentContext>>> GetAgentArray()
{
var agentArrayAddress = this.agentArrayAddress.GetAddress();
if (agentArrayAddress is null or 0x0)
{
this.logger.LogError("Failed to get agent array address");
return null;
}
return (GuildWarsArray<WrappedPointer<AgentContext>>*)agentArrayAddress;
}
public uint GetPlayerAgentId()
{
var playerAgentIdAddress = this.playerAgentIdAddress.GetAddress();
if (playerAgentIdAddress is null or 0x0)
{
this.logger.LogError("Failed to get player agent id address");
return 0;
}
return *(uint*)playerAgentIdAddress;
}
private nuint GetAgentArrayAddress()
{
var scopedLogger = this.logger.CreateScopedLogger();
var address = this.memoryScanningService.FindAndResolveAddress(AgentArrayAddressPattern, AgentArrayMask, AgentArrayOffset);
if (address is 0)
{
scopedLogger.LogError("Failed to find agent array address");
return 0U;
}
scopedLogger.LogInformation("Agent array address: 0x{address:X8}", address);
return address;
}
private nuint GetPreGameContextAddress()
{
var scopedLogger = this.logger.CreateScopedLogger();
var address = this.memoryScanningService.FindAndResolveAddress(PlayerAgentIdAddressPattern, PlayerAgentIdMask, PlayerAgentIdOffset);
if (address is 0)
{
scopedLogger.LogError("Failed to find player agent id address");
return 0U;
}
scopedLogger.LogInformation("Player agent id address: 0x{address:X8}", address);
return address;
}
}
@@ -0,0 +1,85 @@
using Daybreak.API.Interop;
using Daybreak.API.Models;
using System.Core.Extensions;
using System.Extensions.Core;
using System.Runtime.InteropServices;
using System.Security;
namespace Daybreak.API.Services.Interop;
public sealed class ChatHandlingService
: IHostedService, IHookHealthService
{
private const string AddToChatLogMask = "xxxxxx";
private static readonly byte[] AddToChatLogSeq = [0x40, 0x25, 0xFF, 0x01, 0x00, 0x00];
[SuppressUnmanagedCodeSecurity]
[UnmanagedFunctionPointer(CallingConvention.Cdecl)]
private delegate bool AddToChatLog([MarshalAs(UnmanagedType.LPWStr)] string message, Channel channel);
private readonly MemoryScanningService memoryScanningService;
private readonly GWHook<AddToChatLog> addToChatLogHook;
private readonly ILogger<ChatHandlingService> logger;
private CancellationTokenSource? cts = default;
public ChatHandlingService(
MemoryScanningService memoryScanningService,
ILogger<ChatHandlingService> logger)
{
this.logger = logger.ThrowIfNull();
this.memoryScanningService = memoryScanningService.ThrowIfNull();
this.addToChatLogHook = new GWHook<AddToChatLog>(
new GWAddressCache(() => this.memoryScanningService.ToFunctionStart(
this.memoryScanningService.FindAddress(AddToChatLogSeq, AddToChatLogMask))),
this.OnAddToChatLog);
}
public Task StartAsync(CancellationToken cancellationToken)
{
this.cts?.Dispose();
this.cts = new CancellationTokenSource();
return Task.Factory.StartNew(() => this.InitializeHooks(this.cts.Token), this.cts.Token, TaskCreationOptions.LongRunning, TaskScheduler.Current);
}
public Task StopAsync(CancellationToken cancellationToken)
{
this.cts?.Cancel();
this.cts?.Dispose();
this.addToChatLogHook.Dispose();
return Task.CompletedTask;
}
public List<HookState> GetHookStates()
{
return
[
new HookState
{
Hooked = this.addToChatLogHook.Hooked,
Name = nameof(this.addToChatLogHook),
TargetAddress = new PointerValue(this.addToChatLogHook.TargetAddress),
ContinueAddress = new PointerValue(this.addToChatLogHook.ContinueAddress),
DetourAddress = new PointerValue(this.addToChatLogHook.DetourAddress)
}
];
}
private async ValueTask InitializeHooks(CancellationToken cancellationToken)
{
var scopedLogger = this.logger.CreateScopedLogger();
scopedLogger.LogInformation("Initializing hooks");
while (!this.addToChatLogHook.EnsureInitialized())
{
scopedLogger.LogDebug("Waiting for hook to initialize: {hook}", nameof(this.addToChatLogHook));
await Task.Delay(1000, cancellationToken);
}
scopedLogger.LogInformation("Hooks initialized");
}
private bool OnAddToChatLog(string message, Channel channel)
{
return this.addToChatLogHook.Continue(message, channel);
}
}
@@ -0,0 +1,181 @@
using Daybreak.API.Interop;
using Daybreak.API.Interop.GuildWars;
using Daybreak.API.Models;
using System.Core.Extensions;
using System.Extensions.Core;
namespace Daybreak.API.Services.Interop;
public unsafe sealed class GameContextService : IAddressHealthService
{
private const string BaseAddressMask = "xxxxxxx";
private const int BaseAddressOffset = +7;
private static readonly byte[] BaseAddressPattern = [0x50, 0x6A, 0x0F, 0x6A, 0x00, 0xFF, 0x35];
private const string PreGameContextFile = "UiPregame.cpp";
private const string PreGameContextAssertion = "!s_scene";
private const int PreGameContextOffset = 0x34;
private const string AvailableCharsMask = "xx????xxxxxxx";
private const int AvailableCharsOffset = 0x2;
private static readonly byte[] AvailableCharsPattern = [0x8B, 0x35, 0x00, 0x00, 0x00, 0x00, 0x57, 0x69, 0xF8, 0x84, 0x00, 0x00, 0x00];
private readonly MemoryScanningService memoryScanningService;
private readonly GWAddressCache baseAddress;
private readonly GWAddressCache preGameContextAddress;
private readonly GWAddressCache availableCharsAddress;
private readonly ILogger<GameContextService> logger;
public GameContextService(
MemoryScanningService memoryScanningService,
ILogger<GameContextService> logger)
{
this.logger = logger.ThrowIfNull();
this.memoryScanningService = memoryScanningService.ThrowIfNull();
this.baseAddress = new GWAddressCache(this.GetBaseAddress);
this.preGameContextAddress = new GWAddressCache(this.GetPreGameContextAddress);
this.availableCharsAddress = new GWAddressCache(this.GetAvailableCharactersAddress);
}
public List<AddressState> GetAddressStates()
{
return [
new AddressState
{
Address = this.baseAddress.GetAddress() ?? 0,
Name = nameof(this.baseAddress),
},
new AddressState
{
Address = this.preGameContextAddress.GetAddress() ?? 0,
Name = nameof(this.preGameContextAddress),
},
new AddressState
{
Address = this.availableCharsAddress.GetAddress() ?? 0,
Name = nameof(this.availableCharsAddress),
}
];
}
public WrappedPointer<GameContext> GetGameContext()
{
var gameContextAddress = this.GetGameContextAddress();
if (gameContextAddress is 0x0)
{
this.logger.LogError("Failed to get game context address");
return null;
}
return (GameContext*)gameContextAddress;
}
public WrappedPointer<PreGameContext> GetPreGameContext()
{
var preGameContextAddress = this.preGameContextAddress.GetAddress();
if (preGameContextAddress is null or 0x0)
{
this.logger.LogError("Failed to get pre-game context address");
return null;
}
return *(PreGameContext**)preGameContextAddress;
}
public WrappedPointer<GuildWarsArray<CharInfoContext>> GetAvailableChars()
{
var availableCharsAddress = this.availableCharsAddress.GetAddress();
if (availableCharsAddress is null or 0x0)
{
this.logger.LogError("Failed to get available chars address");
return null;
}
return *(GuildWarsArray<CharInfoContext>**)availableCharsAddress;
}
public bool IsMapLoaded()
{
var gameContext = this.GetGameContext();
if (gameContext.IsNull)
{
return false;
}
if (gameContext.Pointer->MapContext is null)
{
return false;
}
return true;
}
private nuint GetGameContextAddress()
{
var scopedLogger = this.logger.CreateScopedLogger();
if (this.baseAddress.GetAddress() is not nuint basePtr)
{
scopedLogger.LogError("Failed to get base address");
return default;
}
nuint** baseContext = *(nuint***)basePtr;
if (baseContext is null)
{
scopedLogger.LogError("Failed to get base context address");
return default;
}
var globalContextPtr = baseContext[0x6];
if (globalContextPtr is null)
{
scopedLogger.LogError("Failed to get game context address");
return default;
}
return (nuint)globalContextPtr;
}
private nuint GetBaseAddress()
{
var scopedLogger = this.logger.CreateScopedLogger();
var baseAddress = this.memoryScanningService.FindAndResolveAddress(BaseAddressPattern, BaseAddressMask, BaseAddressOffset);
if (baseAddress is 0)
{
scopedLogger.LogError("Failed to find base address");
return 0U;
}
scopedLogger.LogInformation("Base address: 0x{address:X8}", baseAddress);
return baseAddress;
}
private unsafe nuint GetPreGameContextAddress()
{
var scopedLogger = this.logger.CreateScopedLogger();
var preGameContextAddress = this.memoryScanningService.FindAssertion(PreGameContextFile, PreGameContextAssertion, 0, PreGameContextOffset);
if (preGameContextAddress is 0)
{
scopedLogger.LogError("Failed to find pre-game context address");
return 0U;
}
preGameContextAddress = *(nuint*)preGameContextAddress;
scopedLogger.LogInformation("Pre-game context address: 0x{address:X8}", preGameContextAddress);
return preGameContextAddress;
}
private unsafe nuint GetAvailableCharactersAddress()
{
var scopedLogger = this.logger.CreateScopedLogger();
var availableCharsAddress = this.memoryScanningService.FindAddress(AvailableCharsPattern, AvailableCharsMask, AvailableCharsOffset);
if (availableCharsAddress is 0)
{
scopedLogger.LogError("Failed to find available chars address");
return 0U;
}
scopedLogger.LogInformation("Available chars address: 0x{address:X8}", availableCharsAddress);
return availableCharsAddress;
}
}

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