Compare commits

...
212 Commits
Author SHA1 Message Date
amacocian 53fc08bcb5 Simplify readychecker (#1457)
* Fix windows ready checker (Closes #1456)

* Add extra debugging line

* Extra debug info
2026-02-19 08:23:43 -08:00
amacocian 4077777cd9 Deprecate mDNS scanning (Closes #1454) (#1455) 2026-02-19 08:23:43 -08:00
amacocian 77c2403a40 Fix companion view attach after restart (Closes #1452) (#1453) 2026-02-19 08:23:43 -08:00
github-actions[bot]andamacocian fa3c5e95a6 Bump version to 0.9.10.2 2026-02-19 08:23:43 -08:00
amacocian db012ae094 Update dependencies (#1450) 2026-02-18 12:23:33 -08:00
amacocian d124197e43 Fix installer temp (Closes #1436) (#1449) 2026-02-18 12:23:33 -08:00
amacocian 85593ea94b Linux support for color extraction (Closes #1447) (#1448) 2026-02-18 12:23:33 -08:00
amacocian fe9178c66d Damage modifier 0x248 (Closes #1438) (#1445) 2026-02-18 12:23:33 -08:00
amacocian e5e4e42e1e Improve application launcher logging (Closes #1441) (#1444) 2026-02-18 12:23:33 -08:00
amacocian cc3236f4d6 Improve mod view responsiveness (#1443) 2026-02-18 12:23:33 -08:00
dependabot[bot]amacociandependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
6d598cdf95 Bump Microsoft.Web.WebView2 from 1.0.3719.77 to 1.0.3800.47 (#1442)
---
updated-dependencies:
- dependency-name: Microsoft.Web.WebView2
  dependency-version: 1.0.3800.47
  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>
2026-02-18 12:23:33 -08:00
github-actions[bot]andamacocian 974975eb78 Bump version to 0.9.10.1 2026-02-18 12:23:33 -08:00
amacocian 4346ed758b Fix compare version script (#1435) 2026-02-13 06:25:46 -08:00
amacocianandAlexandru Macocian a7320ac32c Update dependencies (#1433)
Co-authored-by: Alexandru Macocian <amacocian@microsoft.com>
2026-02-13 06:25:46 -08:00
amacocian dce365b227 Update wiki (Closes #1428) (#1432) 2026-02-13 06:25:46 -08:00
amacocian 3ad32bee8f Update gwmarket url (Closes #1430) (#1431) 2026-02-13 06:25:46 -08:00
amacocian 9e3fa738bb DirectSong improvements (#1429) (Closes #1189)
* DirectSong improvements

* Use ChthonVIIs directsong remix

* DirectSong fetches remix mp3s

* Stop using ds executable to register entries on Windows

* DirectSong don't rely on admin rights

* Windows fixes
2026-02-13 06:25:46 -08:00
github-actions[bot]andamacocian 4a6eccca24 Bump version to 0.9.10.0 2026-02-13 06:25:46 -08:00
amacocian b1898c460c Bump versions 2026-02-12 11:35:38 +01:00
amacocian 5c39a99b8f Use PAT to create release branches 2026-02-12 11:32:23 +01:00
amacocian e873b0da6f Cleanup pipelines 2026-02-12 11:25:00 +01:00
amacocian 9d9c533df7 Update documentation (#1426) 2026-02-11 14:22:43 -08:00
amacocian 9f15df9b6d Color-coded launch configurations (Closes #1319) (#1425) 2026-02-11 14:22:43 -08:00
amacocian 5447633500 DXVK Support for Linux (Closes #1394)
* DXVK support (Closes #1394)

* Enable DXVK only on Linux for now
2026-02-11 14:22:43 -08:00
amacocian 47369851f2 Configuration scoped modlist (#1423) (Closes #1225)
* Mod selection for each launch configuration (Closes #1225)

* Store enabled mods with the launched application
2026-02-11 14:22:43 -08:00
amacocianandAlexandru Macocian 176365b4a4 Build improvements (#1422)
Co-authored-by: Alexandru Macocian <amacocian@microsoft.com>
2026-02-11 14:22:43 -08:00
amacocian ca526d352e Release 0.9.9.108 (#1421) 2026-02-11 14:22:43 -08:00
amacocian c2d30ac60c Fix changelog (Closes #1417) (#1419) 2026-02-10 06:06:54 -08:00
amacocian 3984b5a66d Make installer executable (Closes #1416) (#1418) 2026-02-10 06:06:54 -08:00
amacocian 5be32e32ac Release 0.9.9.107 (#1415) 2026-02-10 06:06:54 -08:00
amacocian 5102ad0fe7 Fix hotkeys running when window is in focus (Closes #1411) (#1413) 2026-02-10 05:40:05 -08:00
amacocian 067da8a091 Fix updater and installer (Closes #1407) (#1412) 2026-02-10 05:40:05 -08:00
amacocian 20d0bade79 Increase duration of startup notifications (Closes #1408) (#1410) 2026-02-10 05:40:05 -08:00
amacocian a40c28711e Release 0.9.9.106 (#1409) 2026-02-10 05:40:05 -08:00
amacocian 1b878a73ae Fix injector properties 2026-02-09 20:18:11 +01:00
amacocian f4bbd3dbc6 Make option synchronization cross platform (Closes #1390) (#1405) 2026-02-09 10:46:23 -08:00
amacocian cd46572c22 Window manipulation fixes (Closes #1391) (#1404) 2026-02-09 10:46:23 -08:00
amacocian 33dc001604 Flag to disable launch when mods are outdated (Closes #1388) (#1403) 2026-02-09 10:46:23 -08:00
amacocian c9676d59d1 Fix missing dependency notification after first launch (Closes 1389) (#1402) 2026-02-09 10:46:23 -08:00
amacocian 7f05738a63 Make injector global invariant (Closes #1393) (#1401) 2026-02-09 10:46:23 -08:00
amacocian f6687e0c72 Release pipeline improvements (#1400) 2026-02-09 10:46:23 -08:00
dependabot[bot]amacociandependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
1bf2fb07cc Bump WyriHaximus/github-action-get-previous-tag from 1 to 2 (#1395)
Bumps [WyriHaximus/github-action-get-previous-tag](https://github.com/wyrihaximus/github-action-get-previous-tag) from 1 to 2.
- [Release notes](https://github.com/wyrihaximus/github-action-get-previous-tag/releases)
- [Commits](https://github.com/wyrihaximus/github-action-get-previous-tag/compare/v1...v2)

---
updated-dependencies:
- dependency-name: WyriHaximus/github-action-get-previous-tag
  dependency-version: '2'
  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>
2026-02-09 10:46:23 -08:00
dependabot[bot]amacociandependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
c826849bb7 Bump actions/download-artifact from 4 to 7 (#1396)
Bumps [actions/download-artifact](https://github.com/actions/download-artifact) from 4 to 7.
- [Release notes](https://github.com/actions/download-artifact/releases)
- [Commits](https://github.com/actions/download-artifact/compare/v4...v7)

---
updated-dependencies:
- dependency-name: actions/download-artifact
  dependency-version: '7'
  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>
2026-02-09 10:46:23 -08:00
dependabot[bot]amacociandependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
82fdf5f18b Bump actions/upload-artifact from 4 to 6 (#1397)
Bumps [actions/upload-artifact](https://github.com/actions/upload-artifact) from 4 to 6.
- [Release notes](https://github.com/actions/upload-artifact/releases)
- [Commits](https://github.com/actions/upload-artifact/compare/v4...v6)

---
updated-dependencies:
- dependency-name: actions/upload-artifact
  dependency-version: '6'
  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>
2026-02-09 10:46:23 -08:00
dependabot[bot]amacociandependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
5216cfcea3 Bump Microsoft.Web.WebView2 from 1.0.3650.58 to 1.0.3719.77 (#1398)
---
updated-dependencies:
- dependency-name: Microsoft.Web.WebView2
  dependency-version: 1.0.3719.77
  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>
2026-02-09 10:46:23 -08:00
amacocian a6bee38a19 Release 0.9.9.105 (#1399) 2026-02-09 10:46:23 -08:00
amacocianandAlexandru Macocian 2394059ca3 Fix codeql (#1387)
Co-authored-by: Alexandru Macocian <amacocian@microsoft.com>
2026-02-08 12:28:44 -08:00
amacocian d4fb2e0545 Fix pipelines (#1386) 2026-02-08 12:28:44 -08:00
amacocian cd52c20977 Update documentation (#1384) 2026-02-08 12:28:44 -08:00
amacocian 2070ae2689 Fix wiki (Closes #1379) (#1383) 2026-02-08 12:28:44 -08:00
amacocianandAlexandru Macocian 3ca5117875 Make application DPI aware (Closes #1369) (#1381)
Co-authored-by: Alexandru Macocian <amacocian@microsoft.com>
2026-02-08 12:28:44 -08:00
amacocian ea0a7a1edb Screen position toggle (Closes #1379) (#1380) 2026-02-08 12:28:44 -08:00
amacocian 824881c108 Native linux support (#1378) (Closes #1375 Closes #1376 Closes #1377 Closes #1374 Closes #1373 Closes #1372 Closes #1371) 2026-02-08 12:28:44 -08:00
amacocian 77e3d5ddf2 Pass arguments to the photino window and webview (#1370) 2026-02-08 12:28:44 -08:00
amacocian 9626d7a42e Make build more linux friendly (#1368) 2026-02-08 12:28:44 -08:00
amacocian 75a6fb2b6c Release 0.9.9.104 (#1367) 2026-02-08 12:28:44 -08:00
amacocian d887f2c901 Make build code writable (Closes #1358) (#1364) 2026-02-01 13:32:13 +01:00
amacocian b079be2445 Fix exception when saving new builds (Closes #1359) (#1363) 2026-02-01 13:32:13 +01:00
amacocian f26ac481e9 Fix version checker updater (Closes #1360) (#1362) 2026-02-01 13:32:13 +01:00
amacocian 6bfa52a0bf Release 0.9.9.103 (#1361) 2026-02-01 13:32:13 +01:00
amacocian 0ce4940342 Fix character select (Closes #1348) (#1356) 2026-01-30 21:57:59 +01:00
amacocian 969d24640f Increase update notification timeout (Closes #1350) (#1355) 2026-01-30 21:57:59 +01:00
amacocian 51916c0707 Reset window position launch argument (Closes #1353) (#1354) 2026-01-30 21:57:59 +01:00
amacocian 05910734fc Update wiki with plugin list (Closes #1342) (#1352) 2026-01-30 21:57:59 +01:00
amacocian 55ccc6e367 Release 0.9.9.102 (#1351) 2026-01-30 21:57:59 +01:00
amacocian 6391b13cb4 Fix quest log parsing (Closes #1344) (#1346) 2026-01-28 21:00:56 +01:00
amacocian 32f08793ed Release 0.9.9.101 (#1345) 2026-01-28 21:00:56 +01:00
amacocian d6237764d6 Show build snippet on build list view (Closes #1330) (#1340) 2026-01-23 18:57:39 +01:00
amacocian 15ff789942 Adjust build metadata logging (Closes #1334) (#1339) 2026-01-23 18:57:39 +01:00
amacocian f4c4b5bac2 Remove metricsview (Closes #1333) (#1338) 2026-01-23 18:57:39 +01:00
amacocian 26b02ac94d Improve telemetry (#1337) (Closes #1335)
* Improve telemetry (Closes #1335)

* Fix CD pipeline
2026-01-23 18:57:39 +01:00
amacocian 1f8d4c126e Improve x86 __fastcall wrapper (Closes #1331) (#1332) 2026-01-23 18:57:39 +01:00
amacocian 8306e10ffe Release 0.9.9.100 (#1329) 2026-01-23 18:57:39 +01:00
amacocian cc5fe38daa Improve Skill.None display in build snippet (Closes #1316) (#1327) 2026-01-20 12:37:08 +01:00
amacocian cc1bd1d77f Fix telemetry warnings (Closes #1325) (#1326) 2026-01-20 12:37:08 +01:00
amacocian 437210858c Fix gwmarket clipboard API permissions (Closes #1318) (#1324) 2026-01-20 12:37:08 +01:00
amacocian 47b7a480bf Update gwmarket to v2 (Closes #1321) Allow downloads for gwmarket (Closes #1318) (#1323) 2026-01-20 12:37:08 +01:00
amacocian fe66d8e8b0 Add more item modifiers (#1320)
* Add more item modifiers

* Fix armor penetration mod
2026-01-20 12:37:08 +01:00
dependabot[bot]amacociandependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
4b138e02d4 Bump actions/github-script from 7 to 8 (#1317)
Bumps [actions/github-script](https://github.com/actions/github-script) from 7 to 8.
- [Release notes](https://github.com/actions/github-script/releases)
- [Commits](https://github.com/actions/github-script/compare/v7...v8)

---
updated-dependencies:
- dependency-name: actions/github-script
  dependency-version: '8'
  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>
2026-01-20 12:37:08 +01:00
amacocian 32bc16c695 Release 0.9.9.99 (#1315) 2026-01-20 12:37:08 +01:00
amacocian 0fae63f902 Update readme (Closes #1311) (#1313)
* Update README.md with new build details (Closes #1311)

* Change image source in README.md

Updated image link in README.

Signed-off-by: Macocian Alexandru Victor <amacocian@yahoo.com>

---------

Signed-off-by: Macocian Alexandru Victor <amacocian@yahoo.com>
2026-01-18 21:20:03 +01:00
amacocian 2447f7e517 Update wiki (Closes #1310) (Closes #1294) (#1312) 2026-01-18 21:20:03 +01:00
amacocian f5483352f0 Add more modifiers (#1309) 2026-01-18 21:20:03 +01:00
amacocian f081858d60 Release 0.9.9.98 (#1308) 2026-01-18 21:20:03 +01:00
amacocian 446f5cffd8 Update wiki with plugin details (Closes #1298) (#1306) 2026-01-17 17:18:07 +01:00
amacocian 21b9cf1726 Setup embedded file provider (Closes #1298) (#1305) 2026-01-17 17:18:07 +01:00
PsidreFelixandamacocian 4f1398a8d1 Added backdrop image 'dervish.png' to wwwroot/img directory. (#1304)
Enabled new theme 'Dervish' in CoreThemes.cs
2026-01-17 17:18:07 +01:00
amacocian 072f00638c Fix title info API (Closes #1290) (#1303) 2026-01-17 17:18:07 +01:00
amacocian f7f22532bf Trade chat iframes allow clipboard access (Closes #1291) (#1302) 2026-01-17 17:18:07 +01:00
amacocian a565017ad2 Label merged issues (#1301) 2026-01-17 17:18:07 +01:00
amacocian 8b5f5c19e4 Simplify update process (Closes #1299) (#1300) 2026-01-17 17:18:07 +01:00
amacocian 6d260a927d Build component improvements (Closes #1292) (#1296)
* Improve build component UX

* Make builds show pop-up
2026-01-17 17:18:07 +01:00
amacocian 97eb3af350 Release 0.9.9.97 (#1293) 2026-01-17 17:18:07 +01:00
amacocian ad70e80d4d Item modifiers parser (#1287)
* Parse item modifiers

* Move everything to System.Text.Json

* Complete item parser

* Add some more energy + health mods

* Fix more mods

* Output ids as hex
2026-01-13 21:41:04 +01:00
amacocianandAlexandru Macocian dcffce5298 Release 0.9.9.96 (#1285)
Co-authored-by: Alexandru Macocian <amacocian@microsoft.com>
2026-01-13 21:41:04 +01:00
amacocianandAlexandru Macocian 9ddcb6ba80 Improve changelog when missing Metadata.json (Closes #1278) (#1283)
Co-authored-by: Alexandru Macocian <amacocian@microsoft.com>
2026-01-12 13:38:52 +01:00
amacocianandAlexandru Macocian d77cefb699 Reduce the incidence of github actions (#1282)
Co-authored-by: Alexandru Macocian <amacocian@microsoft.com>
2026-01-12 13:38:52 +01:00
amacocianandAlexandru Macocian bc8917692b Fix installer working directory (Closes #1279) (#1281)
Co-authored-by: Alexandru Macocian <amacocian@microsoft.com>
2026-01-12 13:38:52 +01:00
amacocian 02409f10fe Release 0.9.9.95 (#1277) 2026-01-12 13:38:52 +01:00
amacocian f167ee538a Update wiki and help menu (#1275) 2026-01-12 02:23:36 +01:00
amacocian c5dccb67dc Implement GwMarket view (Closes #1273) (#1274) 2026-01-12 02:23:36 +01:00
amacocian 2cb105acde Fix Guild Wars copy functionality (Closes #1263) (#1272) 2026-01-12 02:23:36 +01:00
amacocian fbc0c39a37 Fix taskbar icon (#1271) 2026-01-12 02:23:36 +01:00
amacocian 1da19b2e1d Fix restart (Closes #1267) (#1270) 2026-01-12 02:23:36 +01:00
amacocian ef2076022e Release 0.9.9.94 (#1266) 2026-01-12 02:23:36 +01:00
amacocianandGitHub d6ad8429c8 Add options example to SimplePlugin (#1265) 2026-01-11 22:03:46 +00:00
amacocianandGitHub 7535be7134 Update SimplePlugin example (#1264) 2026-01-11 21:49:43 +00:00
amacocian 33f8f73066 Hotfix Win10 (Closes #1529) and installer (Closes #1528) (#1260) 2026-01-11 15:51:58 +01:00
amacocian 4c29a5617b Disable VirusTotal check in CD (Closes #1256) (#1257)
Related Work Items: #125
2026-01-11 15:51:58 +01:00
amacocian 2116285cac Release 0.9.9.93 (#1255) 2026-01-11 15:51:58 +01:00
amacocian e85c286120 Fix inventory endpoint not returning all items (Closes #1235) (#1253) 2026-01-11 04:03:52 +01:00
amacocian 730722a0e0 Setup two-step plugin loading sequence (Closes #1244) (#1251) 2026-01-11 04:03:52 +01:00
amacocian 8b90f13bd5 Repair tool exclude tpf files (Closes #1240) (#1250) 2026-01-11 04:03:52 +01:00
amacocian 1360c86395 Fix copy functionality (Closes #1246) (#1249) 2026-01-11 04:03:52 +01:00
amacocian f6f0afc5d6 Fix auto-launch (Closes #1080) (#1248) 2026-01-11 04:03:52 +01:00
amacocian 194fca5861 Update wiki (#1247) 2026-01-11 04:03:52 +01:00
amacocian 114ddacfcc Upgrade renderer (#1245). Fix log view fetching logs. Disable version checker. Closes #1243 #1242 #1239 #1226 2026-01-11 04:03:52 +01:00
amacocianandAlexandru Macocian dc5dfc8b07 Release 0.9.9.92 (#1241)
Co-authored-by: Alexandru Macocian <amacocian@microsoft.com>
2026-01-11 04:03:52 +01:00
amacocian 976586649e Hotfix installer (#1233) 2026-01-07 22:41:42 +01:00
amacocian 4322b636ef Release 0.9.9.91 (#1232) 2026-01-07 22:41:42 +01:00
amacocian cd8581f86a Setup log rotation policy for Daybreak.API (Closes #1220) (#1230) 2026-01-07 22:04:49 +01:00
amacocian 9417a1a486 Repurpose installer into a repair tool (Closes #1100) (#1229) 2026-01-07 22:04:49 +01:00
amacocian f838d16543 Extract injector to its own executable. Convert Daybreak to x64 (#1228) (Closes #1222 #1223) 2026-01-07 22:04:49 +01:00
amacocian 0ec1c9c96f Pin WebView WPF packages to lower stable version due to performance reasons (#1226) (#1227)
Related Work Items: #1225
2026-01-07 22:04:49 +01:00
amacocian bab997b64a Update dependencies (#1224)
* Update dependencies

* UI fixes
2026-01-07 22:04:49 +01:00
amacocian a0c6b7d956 Release 0.9.9.90 (#1219) 2026-01-07 22:04:49 +01:00
amacocian fea568959b Fix dynamic theme crash (Closes #1211) (#1217) 2026-01-05 14:46:18 +01:00
amacocian 443582fda6 Add item type to inventory response (#1216) 2026-01-05 14:46:18 +01:00
amacocian 54e4c17af2 Release 0.9.9.89 (#1215) 2026-01-05 14:46:18 +01:00
amacocian 645c94f21b More handling for Dynamic Theme errors (Addresses #1211) (#1213) 2026-01-05 13:49:38 +01:00
amacocian 8e7b12b760 Inventory api (#1212)
* Expose inventory API

* Update wiki
2026-01-05 13:49:38 +01:00
amacocian 3e6ac4149f Release 0.9.9.88 (#1210) 2026-01-05 13:49:38 +01:00
amacocian ebe045a962 Installer improvements (Closes #1203) (#1207) 2026-01-04 12:57:07 +01:00
amacocian 5298ab1e83 Handle dynamic screenshots exceptions (Closes #1204) (#1206) 2026-01-04 12:57:07 +01:00
amacocian 64df882406 Release 0.9.9.87 (#1205) 2026-01-04 12:57:07 +01:00
amacocianandAlexandru Macocian 9dd39c0d93 Rework character select (#1201)
* Rework character select

* Change index by frames

* Fixes

---------

Co-authored-by: Alexandru Macocian <amacocian@microsoft.com>
2026-01-02 20:15:00 +02:00
amacocianandAlexandru Macocian 2f5108fd3b Fix party loadout loading (#1200)
Co-authored-by: Alexandru Macocian <amacocian@microsoft.com>
2026-01-02 20:15:00 +02:00
amacocianandAlexandru Macocian 9510fdc5de Setup CORS policy for Daybreak API (Closes #1198) (#1199)
Co-authored-by: Alexandru Macocian <amacocian@microsoft.com>
2026-01-02 20:15:00 +02:00
amacocianandAlexandru Macocian 09781b5516 Synchronize trade chat light/dark mode with Daybreak (Closes #1169) (#1197)
Co-authored-by: Alexandru Macocian <amacocian@microsoft.com>
2026-01-02 20:15:00 +02:00
amacocianandAlexandru Macocian 3044861652 Switch to DateTime from DateTimeOffset (Addresses #1187) (#1195)
Co-authored-by: Alexandru Macocian <amacocian@microsoft.com>
2026-01-02 20:15:00 +02:00
amacocianandAlexandru Macocian 8077d36e77 Simplify launch button logic (#1194)
Co-authored-by: Alexandru Macocian <amacocian@microsoft.com>
2026-01-02 20:15:00 +02:00
amacocianandAlexandru Macocian 4ac9daa984 Release 0.9.9.86 (#1192)
Co-authored-by: Alexandru Macocian <amacocian@microsoft.com>
2026-01-02 20:15:00 +02:00
amacocianandAlexandru Macocian f1bfa66e79 Fix for load build into game (Closes #1181) (#1190)
Co-authored-by: Alexandru Macocian <amacocian@microsoft.com>
2025-12-31 20:31:38 +02:00
amacocianandAlexandru Macocian a4485fc60f Fix dropdown UI for launch configs (Closes #1177) (#1186)
Co-authored-by: Alexandru Macocian <amacocian@microsoft.com>
2025-12-31 20:31:38 +02:00
amacocianandAlexandru Macocian 04c9d86ac4 Uniformize executables and launch configs UI (Closes #1178) (#1185)
Co-authored-by: Alexandru Macocian <amacocian@microsoft.com>
2025-12-31 20:31:38 +02:00
amacocianandAlexandru Macocian 0d30adf63b Update mod management wiki (Closes #1176) (#1184)
Co-authored-by: Alexandru Macocian <amacocian@microsoft.com>
2025-12-31 20:31:38 +02:00
amacocianandAlexandru Macocian 2949d47a29 Fix notification close button (Closes #1180) (#1183)
Co-authored-by: Alexandru Macocian <amacocian@microsoft.com>
2025-12-31 20:31:38 +02:00
amacocianandAlexandru Macocian f300f81d00 Improve accessibility for Trade UIs (#1182)
Co-authored-by: Alexandru Macocian <amacocian@microsoft.com>
2025-12-31 20:31:38 +02:00
amacocianandAlexandru Macocian 20ff35fb12 Release 0.9.9.85 (#1179)
Co-authored-by: Alexandru Macocian <amacocian@microsoft.com>
2025-12-31 20:31:38 +02:00
amacocian 8c02b63051 Pop-up launch configuration path on hover (Closes #1167) (#1174) 2025-12-21 15:22:21 +01:00
amacocian 3bae3cf64f Support for mod uninstall (Closes #321) (#1173) 2025-12-21 15:22:21 +01:00
amacocian 948532a694 Support for mod updates (#1172) 2025-12-21 15:22:21 +01:00
amacocian c43195d0bc Fix notification time conversion (Closes #1163 #1168 #1165) (#1171) 2025-12-21 15:22:21 +01:00
amacocian 7d48fde5ea Deprecate sound support (#1170) 2025-12-21 15:22:21 +01:00
amacocianandAlexandru Macocian 69096795f7 Setup support for live themes (#1162)
Co-authored-by: Alexandru Macocian <amacocian@microsoft.com>
2025-12-21 15:22:21 +01:00
amacocianandAlexandru Macocian 7b2320e158 Optimize package sizes (#1161)
Co-authored-by: Alexandru Macocian <amacocian@microsoft.com>
2025-12-21 15:22:21 +01:00
amacocianandAlexandru Macocian baa2628c6f Release 0.9.9.84 (#1160)
Co-authored-by: Alexandru Macocian <amacocian@microsoft.com>
2025-12-21 15:22:21 +01:00
amacocianandAlexandru Macocian 62a6a018e1 Improve wiki with GW install details (Closes #1064) (#1158)
Co-authored-by: Alexandru Macocian <amacocian@microsoft.com>
2025-12-16 20:56:41 +01:00
amacocianandAlexandru Macocian 77c15c9d63 Fix to allow searches in iframes (Closes #1146) (#1157)
Co-authored-by: Alexandru Macocian <amacocian@microsoft.com>
2025-12-16 20:56:41 +01:00
amacocianandAlexandru Macocian 0f338f5a53 Setup simple ReShade management view (Closes #1147) (#1156)
Co-authored-by: Alexandru Macocian <amacocian@microsoft.com>
2025-12-16 20:56:41 +01:00
amacocianandAlexandru Macocian a714fd7c68 gMod create notification on launch when mods are missing (Closes #1141) (#1155)
Co-authored-by: Alexandru Macocian <amacocian@microsoft.com>
2025-12-16 20:56:41 +01:00
amacocianandAlexandru Macocian 1e933a4764 Reword trade alerts setting (Closes #1145) (#1154)
Co-authored-by: Alexandru Macocian <amacocian@microsoft.com>
2025-12-16 20:56:41 +01:00
amacocianandAlexandru Macocian 6cfa0d8976 Launch view sets latest config (Closes #1140) (#1153)
Co-authored-by: Alexandru Macocian <amacocian@microsoft.com>
2025-12-16 20:56:41 +01:00
amacocianandAlexandru Macocian b7d3bf67cc Steam support (Closes #418) (#1152)
Co-authored-by: Alexandru Macocian <amacocian@microsoft.com>
2025-12-16 20:56:41 +01:00
amacocianandAlexandru Macocian 099d745c79 Allow empty accounts (Closes #1131) (#1151)
Co-authored-by: Alexandru Macocian <amacocian@microsoft.com>
2025-12-16 20:56:41 +01:00
amacocianandAlexandru Macocian 4ed61b3cae Remove trader quotes settings (Closes #1144) (#1150)
Co-authored-by: Alexandru Macocian <amacocian@microsoft.com>
2025-12-16 20:56:41 +01:00
amacocian fab37a5ea0 Release 0.9.9.83 (#1139) 2025-12-16 20:56:41 +01:00
amacocian 1baa373f48 Fix trade chat notification titles (Closes #1127) (#1136) 2025-12-12 15:39:58 +01:00
amacocian 1b234c249d Fix duplicate menus on hard refresh (Closes #1133) (#1135) 2025-12-12 15:39:58 +01:00
amacocianandAlexandru Macocian fc177a3007 Wiki view (Closes #1118) (#1134)
* Setup wiki files deploy

* Create wiki view (Closes #1118)

---------

Co-authored-by: Alexandru Macocian <amacocian@microsoft.com>
2025-12-12 15:39:58 +01:00
amacocianandAlexandru Macocian de94b93f2d Fix screenshot service missing directory exception (Closes #1128) (#1130)
Co-authored-by: Alexandru Macocian <amacocian@microsoft.com>
2025-12-12 15:39:58 +01:00
amacocianandAlexandru Macocian b211ab9409 Deprecate local database (#1129)
Co-authored-by: Alexandru Macocian <amacocian@microsoft.com>
2025-12-12 15:39:58 +01:00
amacocianandAlexandru Macocian 440ccf4518 Add Daybreak wiki as a submodule (#1118) (#1126)
Co-authored-by: Alexandru Macocian <amacocian@microsoft.com>
2025-12-12 15:39:58 +01:00
amacocianandAlexandru Macocian 3f7a1cc310 Release 0.9.9.82 (#1125)
Co-authored-by: Alexandru Macocian <amacocian@microsoft.com>
2025-12-12 15:39:58 +01:00
amacocian d3cce2e24e Add toggle for alerts in the alerts list (Closes #1117) (#1123) 2025-12-10 01:14:07 +01:00
amacocian 8fa05c8854 Launch button improvements (Closes #1086 #1114) (#1122) 2025-12-10 01:14:07 +01:00
amacocian d71ac6df06 Fix version management links (Closes #1113) (#1121) 2025-12-10 01:14:07 +01:00
amacocian cc3173ffe5 Rotating game screenshots theme (Closes #1115) (#1120) 2025-12-10 01:14:07 +01:00
amacocian a92f566119 Release 0.9.9.81 (#1119) 2025-12-10 01:14:07 +01:00
amacocian 8df32b1009 Fix for spinning widget on missing title (Closes #1091) (#1111) 2025-12-09 14:24:06 +01:00
amacocian 26cb97caf4 Daybreak.API writes a message when a build is loaded (Closes #1092) (#1110) 2025-12-09 14:24:06 +01:00
amacocian 66e2c5eb95 Fix gMod version parser (Closes #1105) (#1109) 2025-12-09 14:24:06 +01:00
amacocian 99444df04e Use gstatic (#1108)
* Internet service uses faster gstatic (Closes #1061)
2025-12-09 14:24:06 +01:00
amacocian b08d553c56 Fix telemetry host (Closes #1106) (#1107) 2025-12-09 14:24:06 +01:00
amacocian d7a48db83f Deprecate DXVK and DSOAL (#1104) 2025-12-09 14:24:06 +01:00
amacocian b5d7719f48 Adjust toolbox injection process. Closes #1101 (#1103) 2025-12-09 14:24:06 +01:00
dependabot[bot]amacociandependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
6dfdc7376e Bump actions/setup-dotnet from 4 to 5 (#1062)
Bumps [actions/setup-dotnet](https://github.com/actions/setup-dotnet) from 4 to 5.
- [Release notes](https://github.com/actions/setup-dotnet/releases)
- [Commits](https://github.com/actions/setup-dotnet/compare/v4...v5)

---
updated-dependencies:
- dependency-name: actions/setup-dotnet
  dependency-version: '5'
  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>
2025-12-09 14:24:06 +01:00
dependabot[bot]amacociandependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
7ee7be4d37 Bump actions/checkout from 4 to 6 (#1079)
Bumps [actions/checkout](https://github.com/actions/checkout) from 4 to 6.
- [Release notes](https://github.com/actions/checkout/releases)
- [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md)
- [Commits](https://github.com/actions/checkout/compare/v4...v6)

---
updated-dependencies:
- dependency-name: actions/checkout
  dependency-version: '6'
  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>
2025-12-09 14:24:06 +01:00
amacocian 3554249d80 Release 0.9.9.80 (#1102) 2025-12-09 14:24:06 +01:00
amacocian 5a6fca6fa7 Upgrade to .net 10 (#1089) 2025-12-08 22:39:48 +01:00
amacocian 8d0a53a812 Improve exception handling (Closes #1087) (#1088) 2025-12-08 22:39:48 +01:00
amacocian 794afa6c2c Blazor UI Migration (#1085)
* Migrate to Fluent UI

* Setup window events for Blazor WebView

* Setup basic FluentUI styling

* FluentUI links

* Setup views and viewmodel infra

* Remove old navigation logic

* Prepare an example of iframe embed

* Style window + navigation menu

* Setup page based navigation

* Add some UI error handling

* Introduce UI resiliency to exceptions

* Setup settings menu dropdown

* Basic setup for options views

* Setup reusable OptionBase

* Setup options views

* Basic styling

* Setup scoped css files.
Split App into components

* Hook option updates to OptionProvider

* Fix option dropdowns

* Setup multiple themes

* Make fonts scalable

* Move ViewModels under Views

* Launch view + styling

* Setup FocusView base

* Finish BuildListView implementation

* Base for build views

* Setup build routing

* Implement single build editor

* Implement skill description on hover

* Scrape skill information from the wiki and cache it locally

* Scrape and fix Guild Wars wiki info

* Improve mouse hover performance by querying position only when needed

* Skill snippet snaps to edges

* Setup accounts and executables view

* Launch configurations view

* Fix build skill icons
Attach skill types to skills
Scrape upkeep from wiki

* Fix skilltype overflow

* Adjust navigation menu

* Setup privilege elevation flow

* Setup MSAL

* Options synchronization view

* Telemetry view

* Version management and update view

* Setup plugins view

* Setup notifications panel

* Base for logs view

* Finish LogView

* Setup metrics view

* Setup UI logging interop

* GuildWarsPartySearch view implementation

* Setup gitlab mirror job

* Eventviewer base

* Remove gitlab mirror

* Remove mirror pipeline

* Fix metricsview crashes

* Extract progress component

* Setup trade chat views

* Setup Download view

* Extract progress styling

* Setup guild wars copy view

* Setup trade alerts

* Fix team template view skill selection

* Setup basic onboarding view

* Overhaul asynchronous progress operations and remove redundant versioning code

* Setup modsview base

* Setup modsview with mod states

* Setup mod installation process

* Cleanup more ui migration
Remove price history #1076

* Screen selector manage view

* Upgrade to .net 10
Closes #1078

* gMod management view

* Trader message notification

* ReShade management

* Update for toolbox

* Setup focusview fetching

* Setup CharacterComponent

* Setup vanquishing component

* Setup TitleInformationComponent

* Closes #1082
Fix API project for Guild Wars Reforged

* Small fixes + attempts to get new exe version

* FileId fixes

* Current Map and Player Resources widgets

* Quest log widget

* BuildComponent implementation

* Closes #1075
Embed wwwroot into the executable

* Cleanup

* Fixes to slnx

* Fix test project
2025-12-08 22:39:48 +01:00
amacocian 7d78a89d12 Migrate sln to slnx (#1056)
* Migrate sln to slnx

* Patch pipelines
2025-12-08 22:39:48 +01:00
amacocian dcb50e9da3 Release 0.9.9.79 (#1055) 2025-12-08 22:39:48 +01:00
amacocian 86e3579383 Adjust CD pipeline triggers for the component projects (Closes #1038) (#1053) 2025-07-26 22:09:54 +02:00
amacocian ea39346678 Remove screenshots that are no longer available (#1052) 2025-07-26 22:09:54 +02:00
amacocian e4cdc4f939 Fix chart drawing (#1051)
* Reorder dependencies

* Fix visual bug with graphs
2025-07-26 22:09:54 +02:00
amacocian 5acac1ae7a Update dependencies (#1050)
* Update dependencies (Closes #940)

* Update dependencies

* Chart adjustment
2025-07-26 22:09:54 +02:00
amacocian 8c0e734c71 Fix aggregate exception due to concurrency issue on db initialization at startup (Closes #1029) (#1047) 2025-07-26 22:09:54 +02:00
amacocian a56eb20aa0 Fix updater HttpRequestException when connection cannot be established (Closes #1042) (#1046) 2025-07-26 22:09:54 +02:00
amacocian 482854801c Fix format exception popping up during build parse (Closes #1041) (#1045) 2025-07-26 22:09:54 +02:00
amacocian 1a37b8b08f Release 0.9.9.78 (#1044) 2025-07-26 22:09:54 +02:00
amacocian 40d8280ba6 Sort party members by profession when applying party loadout (Closes #1030) (#1036) 2025-06-30 11:09:16 +02:00
amacocian 9533ef0607 Fix builds not showing in FocusView due to locked skills (Closes #1031) (#1035) 2025-06-30 11:09:16 +02:00
amacocian ac963f60b5 Fix build codes not applying due to unescaped url characters (Closes #1032) (#1034) 2025-06-30 11:09:16 +02:00
amacocian 5b6ea99f79 Release 0.9.9.77 (#1033) 2025-06-30 11:09:16 +02:00
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
1395 changed files with 43843 additions and 50348 deletions
+22
View File
@@ -0,0 +1,22 @@
# This action simulates a CD pipeline without the package publishing
# to ensure that changes don't break the real run
name: Daybreak Dry-Run CD Pipeline
on:
pull_request:
branches:
- master
paths:
- "Daybreak.Core/**"
- "Daybreak.Installer/**"
- "Daybreak.API/**"
- "Daybreak.Shared/**"
- "Daybreak.Windows/**"
- "Daybreak.Linux/**"
jobs:
cd:
uses: ./.github/workflows/cd-template.yml
with:
publishRelease: false
secrets: inherit
+271
View File
@@ -0,0 +1,271 @@
# Reusable CD pipeline with optional release publishing
name: Daybreak CD Template
on:
workflow_call:
inputs:
publishRelease:
description: "Whether to create and publish the GitHub release"
required: false
type: boolean
default: false
jobs:
# ───────────────────────────────────────────────────────
# Stage 1 — Build the x86 Windows-only components
# (Injector + API) that both platforms need.
# ───────────────────────────────────────────────────────
build-x86:
runs-on: windows-latest
outputs:
version: ${{ steps.get_version.outputs.version }}
env:
Configuration: Release
Actions_Allow_Unsecure_Commands: true
steps:
- name: Checkout
uses: actions/checkout@v6
with:
fetch-depth: 0
submodules: recursive
- name: Get version
id: get_version
shell: pwsh
run: |
$version = (Select-String -Path Directory.Build.props -Pattern '<Version>(.*)</Version>').Matches.Groups[1].Value
echo "version=$version" >> $env:GITHUB_OUTPUT
- name: Install .NET
uses: actions/setup-dotnet@v5
with:
dotnet-version: "10.x"
- name: Publish x86 components
run: .\Scripts\PublishWindowsX86.ps1
shell: pwsh
- name: Upload x86 artifacts
uses: actions/upload-artifact@v6
with:
name: x86-components
path: |
Publish/Injector/
Publish/Api/
retention-days: 1
# ───────────────────────────────────────────────────────
# Stage 2 — Platform builds (parallel)
# ───────────────────────────────────────────────────────
build-windows:
needs: build-x86
runs-on: windows-latest
env:
Configuration: Release
Actions_Allow_Unsecure_Commands: true
steps:
- name: Checkout
uses: actions/checkout@v6
with:
fetch-depth: 0
submodules: recursive
- name: Install .NET
uses: actions/setup-dotnet@v5
with:
dotnet-version: "10.x"
- name: Setup project secrets
run: |
dotnet user-secrets --project Daybreak.Core\Daybreak.Core.csproj set AadApplicationId "${{ secrets.AadApplicationId }}"
dotnet user-secrets --project Daybreak.Core\Daybreak.Core.csproj set AadTenantId "${{ secrets.AadTenantId }}"
dotnet user-secrets --project Daybreak.Core\Daybreak.Core.csproj set ApmServiceAccount "${{ secrets.ApmServiceAccount }}"
dotnet user-secrets --project Daybreak.Core\Daybreak.Core.csproj set ApmServiceKey "${{ secrets.ApmServiceKey }}"
dotnet user-secrets --project Daybreak.Core\Daybreak.Core.csproj set ApmUri "${{ secrets.ApmUri }}"
- name: Download x86 artifacts
uses: actions/download-artifact@v7
with:
name: x86-components
path: Publish/
- name: Publish Windows x64
run: .\Scripts\PublishWindows.ps1
shell: pwsh
- name: Upload Windows build
uses: actions/upload-artifact@v6
with:
name: windows-build
path: Publish/
retention-days: 1
build-linux:
needs: build-x86
runs-on: ubuntu-latest
env:
Configuration: Release
steps:
- name: Checkout
uses: actions/checkout@v6
with:
fetch-depth: 0
submodules: recursive
- name: Install .NET
uses: actions/setup-dotnet@v5
with:
dotnet-version: "10.x"
- name: Setup project secrets
run: |
dotnet user-secrets --project Daybreak.Core/Daybreak.Core.csproj set AadApplicationId "${{ secrets.AadApplicationId }}"
dotnet user-secrets --project Daybreak.Core/Daybreak.Core.csproj set AadTenantId "${{ secrets.AadTenantId }}"
dotnet user-secrets --project Daybreak.Core/Daybreak.Core.csproj set ApmServiceAccount "${{ secrets.ApmServiceAccount }}"
dotnet user-secrets --project Daybreak.Core/Daybreak.Core.csproj set ApmServiceKey "${{ secrets.ApmServiceKey }}"
dotnet user-secrets --project Daybreak.Core/Daybreak.Core.csproj set ApmUri "${{ secrets.ApmUri }}"
- name: Download x86 artifacts
uses: actions/download-artifact@v7
with:
name: x86-components
path: Publish/
- name: Publish Linux x64
run: pwsh ./Scripts/PublishLinux.ps1
shell: bash
- name: Set executable permissions
run: |
chmod +x Publish/Daybreak
chmod +x Publish/Daybreak.Wayland.sh
chmod +x Publish/Installer/Daybreak.Installer
- name: Upload Linux build
uses: actions/upload-artifact@v6
with:
name: linux-build
path: Publish/
retention-days: 1
# ───────────────────────────────────────────────────────
# Stage 3 — Package (always)
# ───────────────────────────────────────────────────────
package:
needs: [build-x86, build-windows, build-linux]
runs-on: ubuntu-latest
env:
Actions_Allow_Unsecure_Commands: true
Version: ${{ needs.build-x86.outputs.version }}
steps:
# ── Windows package ──
- name: Download Windows build
uses: actions/download-artifact@v7
with:
name: windows-build
path: windows-publish/
- name: Clean and zip Windows build
run: |
cd windows-publish
find . -name '*.pdb' -delete
find . -name '*.dbg' -delete
if [ -f Installer/Daybreak.Installer.exe ]; then
mv Installer/Daybreak.Installer.exe Installer/Daybreak.Installer.Temp.exe
fi
cd ..
cd windows-publish && zip -r ../daybreakv${{ env.Version }}.zip . && cd ..
# ── Linux package ──
- name: Download Linux build
uses: actions/download-artifact@v7
with:
name: linux-build
path: linux-publish/
- name: Clean and zip Linux build
run: |
cd linux-publish
find . -name '*.pdb' -delete
find . -name '*.dbg' -delete
if [ -f Installer/Daybreak.Installer ]; then
chmod +x Installer/Daybreak.Installer
mv Installer/Daybreak.Installer Installer/Daybreak.Installer.Temp
fi
chmod +x Daybreak Daybreak.Wayland.sh
cd ..
cd linux-publish && zip -r ../daybreakv${{ env.Version }}-linux.zip . && cd ..
- name: Upload release artifacts
uses: actions/upload-artifact@v6
with:
name: release-zips
path: |
daybreakv${{ env.Version }}.zip
daybreakv${{ env.Version }}-linux.zip
retention-days: 7
# ───────────────────────────────────────────────────────
# Stage 4 — Release (optional)
# ───────────────────────────────────────────────────────
release:
needs: [build-x86, package]
runs-on: ubuntu-latest
if: ${{ inputs.publishRelease }}
env:
Actions_Allow_Unsecure_Commands: true
Version: ${{ needs.build-x86.outputs.version }}
steps:
- name: Checkout
uses: actions/checkout@v6
with:
fetch-depth: 0
submodules: recursive
- name: Download release artifacts
uses: actions/download-artifact@v7
with:
name: release-zips
path: release-assets/
- name: Get Latest Tag
id: getLatestTag
uses: WyriHaximus/github-action-get-previous-tag@v2
- name: Generate changelog
id: changelog
run: |
changeLog=$(git log --no-merges --pretty="%h - %s (%an)<br />" ${{ steps.getLatestTag.outputs.tag }}..HEAD)
echo "content<<EOF" >> $GITHUB_OUTPUT
echo "$changeLog" >> $GITHUB_OUTPUT
echo "EOF" >> $GITHUB_OUTPUT
# ── Release ──
- name: Create release draft
uses: Xotl/cool-github-releases@v1.1.10
with:
mode: update
tag_name: v${{ env.Version }}
release_name: Daybreak v${{ env.Version }}
assets: release-assets/daybreakv${{ env.Version }}.zip;release-assets/daybreakv${{ env.Version }}-linux.zip
github_token: ${{ secrets.GITHUB_TOKEN }}
replace_assets: true
body_mrkdwn: |
${{ steps.changelog.outputs.content }}
isDraft: true
- name: Publish release
run: gh release edit v${{ env.Version }} --draft=false
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+16 -147
View File
@@ -1,156 +1,25 @@
# Copyright (c) Microsoft Corporation.
# Licensed under the MIT license.
# This continuous integration pipeline is triggered anytime a user pushes code to the repo.
# This pipeline builds the Wpf project, runs unit tests, then saves the MSIX build artifact.
name: Daybreak CD Pipeline
# This continuous delivery pipeline is triggered on pushes to master.
# It builds the Windows x86 components (Injector + API), then builds
# the Windows x64 and Linux x64 platform bundles in parallel, and
# finally packages both into release zips and publishes a GitHub release.
name: Daybreak CD Pipeline
on:
push:
branches:
- master
paths:
- "Daybreak/**"
- "Daybreak.Core/**"
- "Daybreak.Installer/**"
- "Daybreak.API/**"
- "Daybreak.Shared/**"
- "Daybreak.Windows/**"
- "Daybreak.Linux/**"
workflow_dispatch:
jobs:
build:
strategy:
matrix:
targetplatform: [x86]
runs-on: windows-latest
env:
Configuration: Release
Solution_Path: Daybreak.sln
Test_Project_Path: Daybreak.Tests\Daybreak.Tests.csproj
Wpf_Project_Path: Daybreak\Daybreak.csproj
Actions_Allow_Unsecure_Commands: true
steps:
- name: Checkout
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Get Latest Tag
id: getLatestTag
uses: WyriHaximus/github-action-get-previous-tag@v1
- name: Generate changelog
id: gen_changelog
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: '9.x'
- name: Setup MSBuild.exe
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
env:
RuntimeIdentifier: win-${{ matrix.targetplatform }}
- name: Build Daybreak project
run: dotnet build Daybreak -c $env:Configuration --property:SolutionDir=$env:GITHUB_WORKSPACE
- name: Set version variable
run: |
$version = .\Scripts\GetBuildVersion.ps1
echo "::set-env name=Version::$version"
- name: Create publish launcher files
run: dotnet publish .\Daybreak\Daybreak.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
.\Scripts\PushFilesToBlobStorage.ps1 -version $env:Version -sourcePath .\Publish\ -connectionString "${{ secrets.BLOBSTORAGE_CONNECTIONSTRING }}"
shell: pwsh
- name: Create release draft
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;.\Publish\VirusTotal.Results.json;.\Publish\VirusTotal.Stats.json
github_token: ${{ env.GITHUB_TOKEN }}
replace_assets: true
body_mrkdwn: |
${{ env.Changelog }}
** VirustTotal Stats: **
```
${{ env.VirusTotalStats }}
```
isDraft: true
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Publish release
run: |
gh release edit v${{ env.Version }} --draft=false
shell: powershell
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
cd:
uses: ./.github/workflows/cd-template.yml
with:
publishRelease: true
secrets: inherit
+51 -36
View File
@@ -1,51 +1,66 @@
# Copyright (c) Microsoft Corporation.
# Licensed under the MIT license.
# This continuous integration pipeline is triggered anytime a user pushes code to the repo.
# This pipeline builds the Wpf project, runs unit tests, then saves the MSIX build artifact.
name: Daybreak CI Pipeline
# This continuous integration pipeline is triggered on pull requests to master.
# It validates that both the Windows and Linux builds compile successfully.
name: Daybreak CI Pipeline
on:
push:
branches:
- master
pull_request:
branches:
- master
jobs:
build:
strategy:
matrix:
targetplatform: [x86]
build-windows:
runs-on: windows-latest
env:
Solution_Path: Daybreak.sln
Test_Project_Path: Daybreak.Tests\Daybreak.Tests.csproj
Wpf_Project_Path: Daybreak\Daybreak.csproj
Actions_Allow_Unsecure_Commands: true
Configuration: Debug
steps:
- name: Checkout
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Checkout
uses: actions/checkout@v6
with:
fetch-depth: 0
submodules: recursive
- name: Install .NET Core
uses: actions/setup-dotnet@v4
with:
dotnet-version: '9.x'
architecture: x86
- name: Install .NET
uses: actions/setup-dotnet@v5
with:
dotnet-version: "10.x"
- name: Setup MSBuild.exe
uses: microsoft/setup-msbuild@v2.0.0
- name: Build Windows project
run: dotnet build Daybreak.Windows/Daybreak.Windows.csproj -c $env:Configuration
- 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:
Configuration: Debug
RuntimeIdentifier: win-${{ matrix.targetplatform }}
- name: Build Injector
run: dotnet build Daybreak.Injector/Daybreak.Injector.csproj -c $env:Configuration
- name: Build API
run: dotnet build Daybreak.API/Daybreak.API.csproj -c $env:Configuration
- name: Build Installer
run: dotnet build Daybreak.Installer/Daybreak.Installer.csproj -c $env:Configuration
- name: Run tests
run: dotnet test Daybreak.Tests/Daybreak.Tests.csproj -c $env:Configuration
build-linux:
runs-on: ubuntu-latest
env:
Configuration: Debug
steps:
- name: Checkout
uses: actions/checkout@v6
with:
fetch-depth: 0
submodules: recursive
- name: Install .NET
uses: actions/setup-dotnet@v5
with:
dotnet-version: "10.x"
- name: Build Linux project
run: dotnet build Daybreak.Linux/Daybreak.Linux.csproj -c $Configuration
- name: Build Installer (linux-x64)
run: dotnet build Daybreak.Installer/Daybreak.Installer.csproj -c $Configuration
+6 -9
View File
@@ -12,10 +12,7 @@
name: "CodeQL"
on:
push:
branches: [ "master" ]
pull_request:
# The branches below must be a subset of the branches above
branches: [ "master" ]
schedule:
- cron: '41 12 * * 6'
@@ -38,14 +35,14 @@ jobs:
env:
Configuration: Release
Solution_Path: Daybreak.sln
Solution_Path: Daybreak.slnx
Test_Project_Path: Daybreak.Tests\Daybreak.Tests.csproj
Wpf_Project_Path: Daybreak\Daybreak.csproj
Wpf_Project_Path: Daybreak.Windows\Daybreak.Windows.csproj
Actions_Allow_Unsecure_Commands: true
steps:
- name: Checkout
uses: actions/checkout@v4
uses: actions/checkout@v6
with:
fetch-depth: 0
@@ -66,9 +63,9 @@ jobs:
# If this step fails, then you should remove it and run the build manually (see below)
- name: Install .NET Core
uses: actions/setup-dotnet@v4
uses: actions/setup-dotnet@v5
with:
dotnet-version: '9.x'
dotnet-version: '10.x'
#- name: Autobuild
# uses: github/codeql-action/autobuild@v2
@@ -84,7 +81,7 @@ jobs:
# ./location_of_script_within_repo/buildscript.sh
- name: Build Daybreak project
run: dotnet build Daybreak -c $env:Configuration --property:SolutionDir=$env:GITHUB_WORKSPACE
run: dotnet build Daybreak.Windows -c $env:Configuration --property:SolutionDir=$env:GITHUB_WORKSPACE
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@v3
@@ -0,0 +1,12 @@
# Manually trigger to create a new release branch with bumped build version
name: Create Build Release Branch
on:
workflow_dispatch:
jobs:
create-release:
uses: ./.github/workflows/create-release-branch-template.yml
with:
versionBump: "*.*.^.0"
secrets: inherit
@@ -0,0 +1,12 @@
# Manually trigger to create a new release branch with bumped major version
name: Create Major Release Branch
on:
workflow_dispatch:
jobs:
create-release:
uses: ./.github/workflows/create-release-branch-template.yml
with:
versionBump: "^.0.0.0"
secrets: inherit
@@ -0,0 +1,12 @@
# Manually trigger to create a new release branch with bumped minor version
name: Create Minor Release Branch
on:
workflow_dispatch:
jobs:
create-release:
uses: ./.github/workflows/create-release-branch-template.yml
with:
versionBump: "*.^.0.0"
secrets: inherit
@@ -0,0 +1,66 @@
# Reusable template for creating release branches with version bumps
name: Create Release Branch Template
on:
workflow_call:
inputs:
versionBump:
description: "Version bump command: bump-major, bump-minor, bump-build, bump-revision"
required: true
type: string
jobs:
create-release-branch:
runs-on: ubuntu-latest
permissions:
contents: write
steps:
- name: Checkout
uses: actions/checkout@v6
with:
fetch-depth: 0
token: ${{ secrets.RELEASE_PAT }}
- name: Configure Git
run: |
git config user.name "github-actions[bot]"
git config user.email "github-actions[bot]@users.noreply.github.com"
- name: Bump version in Directory.Build.props
id: bump
uses: vers-one/dotnet-project-version-updater@v1.7
with:
file: "Directory.Build.props"
version: ${{ inputs.versionBump }}
- name: Check if release branch already exists
id: check_branch
run: |
branch_name="release/${{ steps.bump.outputs.newVersion }}"
if git ls-remote --heads origin "$branch_name" | grep -q "$branch_name"; then
echo "Branch $branch_name already exists"
echo "exists=true" >> $GITHUB_OUTPUT
else
echo "Branch $branch_name does not exist"
echo "exists=false" >> $GITHUB_OUTPUT
fi
- name: Create release branch
if: steps.check_branch.outputs.exists == 'false'
run: |
new_version="${{ steps.bump.outputs.newVersion }}"
branch_name="release/$new_version"
# Create new branch from master
git checkout -b "$branch_name"
# Commit the version change (already updated by the action)
git add Directory.Build.props
git commit -m "Bump version to $new_version"
# Push the new branch
git push origin "$branch_name"
echo "✅ Created release branch: $branch_name with version $new_version"
@@ -0,0 +1,20 @@
# Automatically creates a new release branch with bumped revision
# after a successful CD pipeline run on master
name: Create Revision Release Branch
on:
workflow_run:
workflows: ["Daybreak CD Pipeline"]
types:
- completed
branches:
- master
jobs:
create-release:
# Only run if the CD pipeline succeeded
if: ${{ github.event.workflow_run.conclusion == 'success' }}
uses: ./.github/workflows/create-release-branch-template.yml
with:
versionBump: bump-revision
secrets: inherit
@@ -0,0 +1,47 @@
name: Label Merged Issues
on:
pull_request:
types: [closed]
branches:
- 'release/**'
jobs:
label-issues:
if: github.event.pull_request.merged == true
runs-on: ubuntu-latest
steps:
- name: Label linked issues as merged
uses: actions/github-script@v8
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
script: |
const prBody = context.payload.pull_request.body || '';
const prTitle = context.payload.pull_request.title || '';
// Match issue references like #123, fixes #123, closes #123, resolves #123
const issuePattern = /(?:close[sd]?|fix(?:e[sd])?|resolve[sd]?)?[\s:]*#(\d+)/gi;
const issueNumbers = new Set();
// Search in PR body and title
const textToSearch = `${prTitle} ${prBody}`;
let match;
while ((match = issuePattern.exec(textToSearch)) !== null) {
issueNumbers.add(parseInt(match[1]));
}
// Apply 'merged' label to each linked issue
for (const issueNumber of issueNumbers) {
try {
await github.rest.issues.addLabels({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: issueNumber,
labels: ['merged']
});
console.log(`Added 'merged' label to issue #${issueNumber}`);
} catch (error) {
console.log(`Failed to label issue #${issueNumber}: ${error.message}`);
}
}
+31 -29
View File
@@ -1,53 +1,55 @@
name: Daybreak Version Check
name: Daybreak Version Check
on:
pull_request:
branches:
- master
paths:
- "Daybreak/**"
- "Daybreak.Core/**"
- "Daybreak.Installer/**"
- "Daybreak.API/**"
- "Daybreak.Shared/**"
- "Daybreak.Windows/**"
- "Daybreak.Linux/**"
jobs:
check_version:
strategy:
matrix:
targetplatform: [x86]
targetplatform: [x64]
runs-on: windows-latest
runs-on: ubuntu-latest
env:
Configuration: Release
Solution_Path: Daybreak.sln
Solution_Path: Daybreak.slnx
Actions_Allow_Unsecure_Commands: true
steps:
- name: Checkout
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Checkout
uses: actions/checkout@v6
with:
fetch-depth: 0
- name: Install .NET Core
uses: actions/setup-dotnet@v4
with:
dotnet-version: '9.x'
- name: Install .NET Core
uses: actions/setup-dotnet@v5
with:
dotnet-version: "10.x"
- name: Get Latest Tag
id: getLatestTag
uses: WyriHaximus/github-action-get-previous-tag@v1
- name: Get Latest Tag
id: getLatestTag
uses: WyriHaximus/github-action-get-previous-tag@v2
- name: Build Daybreak project
run: dotnet build Daybreak -c $env:Configuration --property:SolutionDir=$env:GITHUB_WORKSPACE
- name: Build Daybreak project
run: dotnet build Daybreak.Linux -c $Configuration --property:SolutionDir=$GITHUB_WORKSPACE
- name: Set version variable
run: |
$version = .\Scripts\GetBuildVersion.ps1
echo "::set-env name=Version::$version"
- name: Get version
id: get_version
run: |
version=$(grep '<Version>' Directory.Build.props | sed 's/.*<Version>\(.*\)<\/Version>.*/\1/')
echo "version=$version" >> $GITHUB_OUTPUT
- name: Check version difference
run: pwsh ./Scripts/CompareVersions.ps1 -currentVersion ${{ steps.get_version.outputs.version }} -lastVersion ${{ steps.getLatestTag.outputs.tag }}
shell: pwsh
- name: Check version difference
run: |
.\Scripts\CompareVersions -currentVersion ${{ env.Version }} -lastVersion ${{ env.LatestReleaseTag }}
env:
LatestReleaseTag: ${{ steps.getLatestTag.outputs.tag }}
+2
View File
@@ -365,3 +365,5 @@ MigrationBackup/
FodyWeavers.xsd
.idea
Daybreak/wwwroot/wiki/*
+3
View File
@@ -0,0 +1,3 @@
[submodule "Daybreak.wiki"]
path = Daybreak.wiki
url = https://github.com/gwdevhub/Daybreak.wiki.git
+17
View File
@@ -0,0 +1,17 @@
{
"version": "0.2.0",
"configurations": [
{
"name": "Debug Daybreak.Linux",
"type": "dotnet",
"request": "launch",
"projectPath": "${workspaceFolder}/Daybreak.Linux/Daybreak.Linux.csproj"
},
{
"name": "Debug Daybreak.Windows",
"type": "dotnet",
"request": "launch",
"projectPath": "${workspaceFolder}/Daybreak.Windows/Daybreak.Windows.csproj"
}
]
}
+29
View File
@@ -0,0 +1,29 @@
{
"dotnet.defaultSolution": "Daybreak.slnx",
"omnisharp.projectLoadTimeout": 120,
"csharp.semanticHighlighting.enabled": true,
"search.followSymlinks": false,
"search.exclude": {
"**/.git": true,
"**/.vs": true,
"**/bin": true,
"**/obj": true,
"**/node_modules": true,
"**/.idea": true,
"**/Publish": true,
},
"files.watcherExclude": {
"**/.git/**": true,
"**/.vs/**": true,
"**/bin/**": true,
"**/obj/**": true,
"**/node_modules/**": true,
"**/Publish": true,
},
"files.exclude": {
"**/.vs": true,
"**/bin": true,
"**/obj": true,
"**/Publish": true
}
}
+29
View File
@@ -0,0 +1,29 @@
{
"version": "2.0.0",
"tasks": [
{
"label": "build-linux",
"command": "dotnet",
"type": "process",
"args": [
"build",
"${workspaceFolder}/Daybreak.Linux/Daybreak.Linux.csproj",
"/property:GenerateFullPaths=true",
"/consoleloggerparameters:NoSummary"
],
"problemMatcher": "$msCompile"
},
{
"label": "build-windows",
"command": "dotnet",
"type": "process",
"args": [
"build",
"${workspaceFolder}/Daybreak.Windows/Daybreak.Windows.csproj",
"/property:GenerateFullPaths=true",
"/consoleloggerparameters:NoSummary"
],
"problemMatcher": "$msCompile"
}
]
}
@@ -1,17 +0,0 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net9.0</TargetFramework>
<RootNamespace>Daybreak._7ZipExtractor</RootNamespace>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<Platforms>x64</Platforms>
<Version>0.9.9.73</Version>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="SharpCompress" Version="0.40.0" />
</ItemGroup>
</Project>
-42
View File
@@ -1,42 +0,0 @@
/*
* Extractor is in a different executable because of memory limitations. Daybreak needs to be compiled for x86 and as such has a memory limit of 2GB.
* 7Zip algorithm may require more than 2GB memory to unpack large zip files.
* As such, Daybreak.7ZipExtractor is x64.
*/
using SharpCompress.Archives;
if (args.Length != 2)
{
return -1;
}
var archivePath = args[0];
var destinationDirectory = args[1];
using var fileStream = new FileStream(archivePath, FileMode.Open);
using var archive = ArchiveFactory.Open(fileStream);
var progress = 0d;
var count = archive.Entries.Count();
var reader = archive.ExtractAllEntries();
while (reader.MoveToNextEntry())
{
var entry = reader.Entry;
if (entry.IsDirectory)
{
var directoryName = Path.Combine(Path.GetFullPath(destinationDirectory), entry.Key);
Directory.CreateDirectory(directoryName);
continue;
}
var fileName = Path.Combine(Path.GetFullPath(destinationDirectory), entry.Key);
using var entryStream = new FileStream(fileName, FileMode.Create);
using (var stream = reader.OpenEntryStream())
{
stream.CopyTo(entryStream);
}
progress += 1d / count;
Console.WriteLine($"{progress} {fileName}");
}
return 0;
@@ -5,18 +5,8 @@ public static class WebApplicationBuilderExtensions
public static WebApplicationBuilder WithConfiguration(this WebApplicationBuilder builder)
{
var config = BuildInfo.Configuration;
var appSettingsStream = GetManifestResourceStream($"Daybreak.API.Configuration.appsettings.json");
if (appSettingsStream is null)
{
throw new InvalidOperationException("Failed to load appsettings.json");
}
var appSettingsConfigStream = GetManifestResourceStream($"Daybreak.API.Configuration.appsettings.{config}.json");
if (appSettingsConfigStream is null)
{
throw new InvalidOperationException($"Failed to load appsettings.{config}.json");
}
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)
@@ -1,9 +1,9 @@
using Daybreak.API.Interop.GuildWars;
using System.Core.Extensions;
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;
@@ -0,0 +1,28 @@
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/inventory")]
[Tags("Inventory")]
public sealed class InventoryController(
InventoryService inventoryService)
{
private readonly InventoryService inventoryService = inventoryService;
[GenerateGet]
[EndpointName("GetInventory")]
[EndpointSummary("Get Inventory information")]
[EndpointDescription("Get the inventory of the current player. Returns a serialized InventoryInformation object")]
[ProducesResponseType<InventoryInformation>(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status503ServiceUnavailable)]
public async Task<IResult> GetInventory(CancellationToken cancellationToken)
{
var inventoryInformation = await this.inventoryService.GetInventoryInformation(cancellationToken);
return inventoryInformation is null ?
Results.StatusCode(StatusCodes.Status503ServiceUnavailable) :
Results.Ok(inventoryInformation);
}
}
@@ -1,8 +1,8 @@
using Daybreak.API.Services;
using System.Core.Extensions;
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;
@@ -1,8 +1,8 @@
using Daybreak.API.Services;
using System.Core.Extensions;
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;
@@ -99,4 +99,16 @@ public sealed class MainPlayerController(
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);
}
}
@@ -1,11 +1,11 @@
using Daybreak.API.Models;
using Daybreak.API.Services;
using Daybreak.API.WebSockets;
using Daybreak.Shared.Models.Api;
using System.Buffers;
using System.Buffers;
using System.Core.Extensions;
using System.Extensions.Core;
using System.Net.WebSockets;
using Daybreak.API.Models;
using Daybreak.API.Services;
using Daybreak.API.WebSockets;
using Daybreak.Shared.Models.Api;
namespace Daybreak.API.Controllers.WebSocket.MainPlayer;
+46 -21
View File
@@ -1,16 +1,14 @@
<Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup>
<TargetFramework>net9.0-windows</TargetFramework>
<TargetFramework>$(TargetFrameworkVersionWindows)</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<OutputType>Library</OutputType>
<RuntimeIdentifier>win-x86</RuntimeIdentifier>
<RuntimeIdentifier Condition="'$(RuntimeIdentifier)' == ''">win-x86</RuntimeIdentifier>
<StripSymbols>true</StripSymbols>
<InteropExports>true</InteropExports>
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
<EnableRequestDelegateGenerator>true</EnableRequestDelegateGenerator>
<Version>0.9.9.73</Version>
<PublishAot>true</PublishAot>
<SelfContained>true</SelfContained>
@@ -19,7 +17,35 @@
<UseAppHost>false</UseAppHost>
<EnableIISSupport>false</EnableIISSupport>
<InvariantGlobalization>true</InvariantGlobalization>
<AllowMissingPrunePackageData>true</AllowMissingPrunePackageData>
<Platforms>x86</Platforms>
<!-- Prevent static web asset conflicts -->
<StaticWebAssetProjectMode>Default</StaticWebAssetProjectMode>
<GenerateStaticWebAssetsManifest>false</GenerateStaticWebAssetsManifest>
<ResolveStaticWebAssetsInputsDependsOn>RemoveDuplicateStaticWebAssets;$(ResolveStaticWebAssetsInputsDependsOn)</ResolveStaticWebAssetsInputsDependsOn>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)' == 'Release'">
<DebugType>none</DebugType>
<DebugSymbols>false</DebugSymbols>
</PropertyGroup>
<!-- Remove duplicate static web assets coming from referenced projects -->
<Target Name="RemoveDuplicateStaticWebAssets" BeforeTargets="ResolveStaticWebAssetsInputs;GenerateStaticWebAssetsManifest">
<ItemGroup>
<!-- Remove all blazor webview static assets since this is a Web API project, not a Blazor Hybrid app -->
<StaticWebAsset Remove="@(StaticWebAsset)"
Condition="$([System.String]::new('%(RelativePath)').Contains('blazor.modules')) Or $([System.String]::new('%(RelativePath)').Contains('blazor.webview'))" />
</ItemGroup>
</Target>
<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>
@@ -33,23 +59,22 @@
</ItemGroup>
<ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="9.0.6" />
<PackageReference Include="MinHook.NET" Version="1.1.1" />
<PackageReference Include="Net.Sdk.Web.Extensions" Version="0.8.10" />
<PackageReference Include="Net.Sdk.Web.Extensions.SourceGenerators" Version="0.9.3" />
<PackageReference Include="PeNet" Version="5.1.0" />
<PackageReference Include="Serilog" Version="4.3.0" />
<PackageReference Include="Serilog.Extensions.Logging" Version="9.0.2" />
<PackageReference Include="Serilog.Settings.Configuration" Version="9.0.0" />
<PackageReference Include="Serilog.Sinks.Console" Version="6.0.0" />
<PackageReference Include="Serilog.Sinks.File" Version="7.0.0" />
<PackageReference Include="Swashbuckle.AspNetCore" Version="9.0.1" />
<PackageReference Include="Swashbuckle.AspNetCore.Swagger" Version="9.0.1" />
<PackageReference Include="Swashbuckle.AspNetCore.SwaggerGen" Version="9.0.1" />
<PackageReference Include="Swashbuckle.AspNetCore.SwaggerUI" Version="9.0.1" />
<PackageReference Include="System.Private.Uri" Version="4.3.2" />
<PackageReference Include="SystemExtensions.NetStandard.Generators" Version="0.1.5" PrivateAssets="all" />
<PackageReference Include="ZLinq" Version="1.4.12" />
<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="SystemExtensions.NetStandard.Generators" PrivateAssets="all" />
<PackageReference Include="ZLinq" />
</ItemGroup>
<ItemGroup>
+43 -4
View File
@@ -1,4 +1,5 @@
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using System.Extensions.Core;
using System.Net.NetworkInformation;
using System.Runtime.InteropServices;
@@ -23,6 +24,10 @@ public class EntryPoint
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")]
[SuppressMessage("Trimming", "IL2123:The use of 'RequiresUnreferencedCodeAttribute' on entry points is disallowed since the method will be called from outside the visible app.", Justification = "<Pending>")]
[SuppressMessage("AOT", "IL3057:The use of 'RequiresDynamicCodeAttribute' on entry points is disallowed since the method will be called from outside the visible app.", Justification = "<Pending>")]
public static int ThreadInit(IntPtr _, int __)
{
Environment.SetEnvironmentVariable("ASPNETCORE_HOSTINGSTARTUPASSEMBLIES", null, EnvironmentVariableTarget.Process);
@@ -37,7 +42,7 @@ public class EntryPoint
var healthCheck = app.Services.GetRequiredService<HealthCheckService>();
var sw = Stopwatch.StartNew();
var healthy = false;
while(sw.Elapsed < InitializationTimeout)
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}")));
@@ -65,9 +70,11 @@ public class EntryPoint
}
}
[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 app = WebApplication.CreateBuilder()
var builder = WebApplication.CreateBuilder()
.WithConfiguration()
.WithHosting(port)
.WithSwagger()
@@ -76,9 +83,41 @@ public class EntryPoint
.WithDaybreakServices()
.WithWebSocketRoutes()
.WithRoutes()
.WithHealthChecks()
.Build();
.WithHealthChecks();
builder.Services.AddOpenApi();
// Add CORS policy - allow localhost and trusted community sites
builder.Services.AddCors(options =>
{
options.AddDefaultPolicy(policy =>
{
policy.SetIsOriginAllowed(origin =>
{
if (Uri.TryCreate(origin, UriKind.Absolute, out var uri))
{
// Always allow localhost
if (uri.Host is "localhost" or "127.0.0.1")
{
return true;
}
// Allow known Guild Wars community sites
return uri.Host.EndsWith("guildwars.app", StringComparison.OrdinalIgnoreCase)
|| uri.Host.EndsWith("gwmarket.net", StringComparison.OrdinalIgnoreCase);
}
return false;
})
.AllowAnyHeader()
.AllowAnyMethod()
.AllowCredentials();
});
});
var app = builder.Build();
app.UseCors();
app.UseWebSockets(new WebSocketOptions { KeepAliveInterval = TimeSpan.FromSeconds(30) });
return app
+89 -17
View File
@@ -11,37 +11,109 @@ public abstract class GWFastCall(GWAddressCache target)
public GWAddressCache Cache { get; } = target;
public abstract void Initialize();
protected unsafe static nint BuildFastCallThunk(nuint target)
/// <summary>
/// Builds a thunk for __fastcall with 1 parameter (ECX only).
/// </summary>
protected unsafe static nint BuildFastCallThunk1(nuint target)
{
if (target is 0)
{
return 0;
}
var code = stackalloc byte[13]
var code = stackalloc byte[10]
{
0x58, // pop eax (ret)
0x59, // pop ecx (ctx)
0x5A, // pop edx (edxVal)
0x5B, // pop ebx (wparam)
0x53, // push ebx (wparam)
0x59, // pop ecx (arg1)
0x50, // push eax (ret)
0xB8,0,0,0,0, // mov eax, TARGET
0xFF,0xE0 // jmp eax
}; // 13 bytes total
0xB8, 0, 0, 0, 0, // mov eax, TARGET
0xFF, 0xE0 // jmp eax
}; // 10 bytes total
const int TARGET_OFFSET = 7;
const int TARGET_OFFSET = 4;
Unsafe.WriteUnaligned(ref code[TARGET_OFFSET], (uint)target);
var mem = NativeMethods.VirtualAlloc(0, 13, NativeMethods.MEM_COMMIT | NativeMethods.MEM_RESERVE, NativeMethods.PAGE_EXECUTE_READWRITE);
var mem = NativeMethods.VirtualAlloc(0, 10, 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);
Buffer.MemoryCopy(code, (void*)mem, 10, 10);
return mem;
}
/// <summary>
/// Builds a thunk for __fastcall with 2+ parameters (ECX and EDX).
/// </summary>
protected unsafe static nint BuildFastCallThunk2(nuint target)
{
if (target is 0)
{
return 0;
}
var code = stackalloc byte[11]
{
0x58, // pop eax (ret)
0x59, // pop ecx (ctx)
0x5A, // pop edx (edxVal)
0x50, // push eax (ret)
0xB8,0,0,0,0, // mov eax, TARGET
0xFF,0xE0 // jmp eax
}; // 11 bytes total
const int TARGET_OFFSET = 5;
Unsafe.WriteUnaligned(ref code[TARGET_OFFSET], (uint)target);
var mem = NativeMethods.VirtualAlloc(0, 11, 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, 11, 11);
return mem;
}
}
public unsafe sealed class GWFastCall<T1, T2>(GWAddressCache target) : GWFastCall(target)
{
private delegate* unmanaged[Stdcall]<T1, T2> func = null;
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public unsafe T2? Invoke(T1 t1)
{
if (this.func is null)
{
this.Initialize();
}
if (this.func is not null)
{
if (typeof(T2) == typeof(Void))
{
((delegate* unmanaged[Stdcall]<T1, void>)this.func)(t1);
return default;
}
return this.func(t1);
}
return default;
}
public override void Initialize()
{
if (this.Cache.GetAddress() is not nuint addr ||
addr is 0)
{
return;
}
var call = BuildFastCallThunk1(addr);
this.func = (delegate* unmanaged[Stdcall]<T1, T2>)call;
}
}
public unsafe sealed class GWFastCall<T1, T2, T3>(GWAddressCache target) : GWFastCall(target)
@@ -78,7 +150,7 @@ public unsafe sealed class GWFastCall<T1, T2, T3>(GWAddressCache target) : GWFas
return;
}
var call = BuildFastCallThunk(addr);
var call = BuildFastCallThunk2(addr);
this.func = (delegate* unmanaged[Stdcall]<T1, T2, T3>)call;
}
}
@@ -117,7 +189,7 @@ public unsafe sealed class GWFastCall<T1, T2, T3, T4>(GWAddressCache target) : G
return;
}
var call = BuildFastCallThunk(addr);
var call = BuildFastCallThunk2(addr);
this.func = (delegate* unmanaged[Stdcall]<T1, T2, T3, T4>)call;
}
}
@@ -156,7 +228,7 @@ public unsafe sealed class GWFastCall<T1, T2, T3, T4, T5>(GWAddressCache target)
return;
}
var call = BuildFastCallThunk(addr);
var call = BuildFastCallThunk2(addr);
this.func = (delegate* unmanaged[Stdcall]<T1, T2, T3, T4, T5>)call;
}
}
@@ -195,7 +267,7 @@ public unsafe sealed class GWFastCall<T1, T2, T3, T4, T5, T6>(GWAddressCache tar
return;
}
var call = BuildFastCallThunk(addr);
var call = BuildFastCallThunk2(addr);
this.func = (delegate* unmanaged[Stdcall]<T1, T2, T3, T4, T5, T6>)call;
}
}
@@ -234,7 +306,7 @@ public unsafe sealed class GWFastCall<T1, T2, T3, T4, T5, T6, T7>(GWAddressCache
return;
}
var call = BuildFastCallThunk(addr);
var call = BuildFastCallThunk2(addr);
this.func = (delegate* unmanaged[Stdcall]<T1, T2, T3, T4, T5, T6, T7>)call;
}
}
+16 -17
View File
@@ -51,7 +51,6 @@ public enum LivingAgentEffects : uint
Bleeding = 0x0001,
Conditioned = 0x0002,
Crippled1 = 0x0008, // Part of the Crippled check
Crippled2 = 0x0002, // Part of the Crippled check
Crippled = 0x000A, // Combined flag for crippled (0x0008 | 0x0002)
Dead = 0x0010,
DeepWound = 0x0020,
@@ -156,52 +155,52 @@ public readonly unsafe struct AgentLivingContext
[FieldOffset(0x00F6)]
public readonly AgentModelType AgentModelType;
[FieldOffset(0x0104)]
[FieldOffset(0x0108)]
public readonly TagInfo* Tags;
[FieldOffset(0x010A)]
[FieldOffset(0x010E)]
public readonly byte Primary;
[FieldOffset(0x010B)]
[FieldOffset(0x010F)]
public readonly byte Secondary;
[FieldOffset(0x010C)]
[FieldOffset(0x0110)]
public readonly byte Level;
[FieldOffset(0x010D)]
[FieldOffset(0x0111)]
public readonly byte TeamId;
[FieldOffset(0x0114)]
[FieldOffset(0x0118)]
public readonly float EnergyRegen;
[FieldOffset(0x011C)]
[FieldOffset(0x0120)]
public readonly float Energy;
[FieldOffset(0x0120)]
[FieldOffset(0x0124)]
public readonly uint MaxEnergy;
[FieldOffset(0x0128)]
[FieldOffset(0x012C)]
public readonly float HealthRegen;
[FieldOffset(0x0130)]
[FieldOffset(0x0134)]
public readonly float Health;
[FieldOffset(0x0134)]
[FieldOffset(0x0138)]
public readonly uint MaxHealth;
[FieldOffset(0x0138)]
[FieldOffset(0x013C)]
public readonly uint Effects;
[FieldOffset(0x0154)]
[FieldOffset(0x0158)]
public readonly LivingAgentModelState ModelState;
[FieldOffset(0x0158)]
[FieldOffset(0x015C)]
public readonly LivingAgentState State;
[FieldOffset(0x0180)]
[FieldOffset(0x0184)]
public readonly uint LoginNumber;
[FieldOffset(0x01B1)]
[FieldOffset(0x01B5)]
public readonly LivingAgentAllegiance Allegiance;
public readonly bool IsAlive => (!this.State.HasFlag(LivingAgentState.Dead)) && this.Health > 0f;
+30
View File
@@ -0,0 +1,30 @@
using System.Runtime.InteropServices;
namespace Daybreak.API.Interop.GuildWars;
public enum BagType
{
None,
Inventory,
Equipped,
NotCollected,
Storage,
MaterialsStorage
}
[StructLayout(LayoutKind.Explicit, Pack = 1)]
public readonly unsafe struct Bag
{
[FieldOffset(0x0000)]
public readonly BagType Type;
[FieldOffset(0x0004)]
public readonly uint Index;
[FieldOffset(0x000C)]
public readonly uint ContainerItem;
[FieldOffset(0x0010)]
public readonly uint ItemsCount;
[FieldOffset(0x0014)]
public readonly Bag* Bags;
[FieldOffset(0x0018)]
public readonly GuildWarsArray<WrappedPointer<Item>> Items;
}
+31 -10
View File
@@ -1,4 +1,5 @@
using Daybreak.API.Models;
using System.Extensions;
using System.Runtime.InteropServices;
namespace Daybreak.API.Interop.GuildWars;
@@ -19,25 +20,25 @@ public readonly struct Frame
[FieldOffset(0x0024)]
public readonly uint TemplateType;
[FieldOffset(0x00A0)]
[FieldOffset(0x00A8)]
public readonly GuildWarsArray<FrameInteractionCallback> FrameCallbacks;
[FieldOffset(0x00b0)]
[FieldOffset(0x00b8)]
public readonly uint ChildOffsetId;
[FieldOffset(0x00b4)]
[FieldOffset(0x00bc)]
public readonly uint FrameId;
[FieldOffset(0x0120)]
[FieldOffset(0x0128)]
public readonly FrameRelation Relation;
[FieldOffset(0x0184)]
public readonly uint Field91;
[FieldOffset(0x018C)]
public readonly uint FrameState;
public bool IsCreated => (this.Field91 & 0x4) != 0;
public bool IsHidden => (this.Field91 & 0x200) != 0;
public bool IsCreated => (this.FrameState & 0x4) != 0;
public bool IsHidden => (this.FrameState & 0x200) != 0;
public bool IsVisible => !this.IsHidden;
public bool IsDIsabled => (this.Field91 & 0x10) != 0;
public bool IsDIsabled => (this.FrameState & 0x10) != 0;
}
[StructLayout(LayoutKind.Explicit, Pack = 1, Size = 0x1C)]
@@ -50,7 +51,7 @@ public readonly unsafe struct FrameRelation
public readonly uint FrameHashId;
}
[StructLayout(LayoutKind.Sequential, Pack =1)]
[StructLayout(LayoutKind.Sequential, Pack = 1)]
public readonly unsafe struct InteractionMessage
{
public readonly uint FrameId;
@@ -66,3 +67,23 @@ public unsafe readonly struct FrameInteractionCallback
public readonly void* Callback; // UIInteractionCallback
public readonly void* UiCtl_Context;
}
[StructLayout(LayoutKind.Explicit, Pack = 1)]
public readonly unsafe struct CharSelectorContext
{
[FieldOffset(0x0000)]
public readonly uint Vtable;
[FieldOffset(0x0004)]
public readonly uint FrameId;
[FieldOffset(0x0008)]
public readonly GuildWarsArray<WrappedPointer<CharSelectorChar>> Chars;
}
[StructLayout(LayoutKind.Explicit, Pack = 1)]
public readonly struct CharSelectorChar
{
[FieldOffset(0x0020)]
public readonly Array20Char Name;
}
@@ -11,6 +11,9 @@ public readonly unsafe struct GameContext
[FieldOffset(0x0014)]
public readonly MapContext* MapContext;
[FieldOffset(0x0018)]
public readonly TextParserContext* TextParserContext;
[FieldOffset(0x0028)]
public readonly AccountGameContext* AccountContext;
@@ -20,6 +23,9 @@ public readonly unsafe struct GameContext
[FieldOffset(0x003C)]
public readonly GuildContext* GuildContext;
[FieldOffset(0x0040)]
public readonly ItemContext* ItemContext;
[FieldOffset(0x0044)]
public readonly CharContext* CharContext;
@@ -12,6 +12,16 @@ public readonly unsafe struct GuildWarsArray<T> : IEnumerable<T>
public readonly uint Size;
public readonly uint Param;
public T this[int index]
{
get
{
ArgumentOutOfRangeException.ThrowIfNegative(index);
ArgumentOutOfRangeException.ThrowIfGreaterThanOrEqual((uint)index, this.Size);
return this.Buffer[index];
}
}
public Enumerator GetEnumerator() => new(this.Buffer, this.Size);
IEnumerator<T> IEnumerable<T>.GetEnumerator() => this.GetEnumerator();
@@ -47,6 +57,6 @@ public readonly unsafe struct GuildWarsArray<T> : IEnumerable<T>
object IEnumerator.Current => this.Current;
public void Reset() => this.index = -1;
public void Dispose() { }
public readonly void Dispose() { }
}
}
@@ -0,0 +1,58 @@
using System.Runtime.InteropServices;
namespace Daybreak.API.Interop.GuildWars;
[StructLayout(LayoutKind.Sequential, Pack = 1)]
public readonly unsafe struct Inventory
{
public readonly Bag* UnusedBag;
public readonly Bag* Backpack;
public readonly Bag* BeltPouch;
public readonly Bag* Bag1;
public readonly Bag* Bag2;
public readonly Bag* EquipmentPack;
public readonly Bag* MaterialStorage;
public readonly Bag* UnclaimedItems;
public readonly Bag* Storage1;
public readonly Bag* Storage2;
public readonly Bag* Storage3;
public readonly Bag* Storage4;
public readonly Bag* Storage5;
public readonly Bag* Storage6;
public readonly Bag* Storage7;
public readonly Bag* Storage8;
public readonly Bag* Storage9;
public readonly Bag* Storage10;
public readonly Bag* Storage11;
public readonly Bag* Storage12;
public readonly Bag* Storage13;
public readonly Bag* Storage14;
public readonly Bag* EquippedItems;
public readonly Item* Bundle;
public readonly uint StoragePanesUnlocked;
public readonly Bag*[] Bags => [
this.Backpack,
this.BeltPouch,
this.Bag1,
this.Bag2,
this.EquipmentPack,
this.MaterialStorage,
this.UnclaimedItems,
this.Storage1,
this.Storage2,
this.Storage3,
this.Storage4,
this.Storage5,
this.Storage6,
this.Storage7,
this.Storage8,
this.Storage9,
this.Storage10,
this.Storage11,
this.Storage12,
this.Storage13,
this.Storage14,
this.EquippedItems
];
}
+127
View File
@@ -0,0 +1,127 @@
using System.Runtime.InteropServices;
namespace Daybreak.API.Interop.GuildWars;
public enum ItemType : byte
{
Salvage,
Axe = 2,
Bag,
Boots,
Bow,
Bundle,
Chestpiece,
Rune_Mod,
Usable,
Dye,
Materials_Zcoins,
Offhand,
Gloves,
Hammer = 15,
Headpiece,
CC_Shards,
Key,
Leggings,
Gold_Coin,
Quest_Item,
Wand,
Shield = 24,
Staff = 26,
Sword,
Kit = 29,
Trophy,
Scroll,
Daggers,
Present,
Minipet,
Scythe,
Spear,
Storybook = 43,
Costume,
Costume_Headpiece,
Unknown = 0xff
};
[StructLayout(LayoutKind.Sequential, Pack = 1)]
public readonly struct ItemModifier(uint mod)
{
public static readonly ItemModifier Zero = new();
public readonly uint Mod = mod;
public uint Identifier => this.Mod >> 16;
public uint Arg1 => (this.Mod & 0x0000FF00) >> 8;
public uint Arg2 => this.Mod & 0x000000FF;
public uint Arg => this.Mod & 0x0000FFFF;
}
[StructLayout(LayoutKind.Explicit, Pack = 1)]
public readonly unsafe struct Item
{
[FieldOffset(0x0000)]
public readonly uint ItemId;
[FieldOffset(0x0004)]
public readonly uint AgentId;
[FieldOffset(0x0008)]
public readonly Bag* BagEquipped;
[FieldOffset(0x000C)]
public readonly Bag* Bag;
[FieldOffset(0x0010)]
public readonly ItemModifier* Modifiers;
[FieldOffset(0x0014)]
public readonly uint ModifiersCount;
[FieldOffset(0x001C)]
public readonly uint ModelFileId;
[FieldOffset(0x0020)]
public readonly ItemType Type;
[FieldOffset(0x0024)]
public readonly ushort Value;
[FieldOffset(0x0028)]
public readonly uint Interaction;
[FieldOffset(0x002C)]
public readonly uint ModelId;
[FieldOffset(0x0030)]
public readonly char* InfoString;
[FieldOffset(0x0034)]
public readonly char* NameEncoded;
[FieldOffset(0x0038)]
public readonly char* CompleteNameEncoded;
[FieldOffset(0x003C)]
public readonly char* SingleItemName;
[FieldOffset(0x0048)]
public readonly ushort ItemFormula;
[FieldOffset(0x004A)]
public readonly byte IsMaterialSalvageable;
[FieldOffset(0x004C)]
public readonly ushort Quantity;
[FieldOffset(0x004E)]
public readonly byte Equipped;
[FieldOffset(0x004F)]
public readonly byte Profession;
[FieldOffset(0x0050)]
public readonly uint Slot;
public readonly bool Stackable => (this.Interaction & 0x80000) != 0;
public readonly bool Inscribable => (this.Interaction & 0x08000000) != 0;
}
@@ -0,0 +1,16 @@
using System.Runtime.InteropServices;
namespace Daybreak.API.Interop.GuildWars;
[StructLayout(LayoutKind.Explicit, Pack = 1)]
public readonly struct ItemContext
{
[FieldOffset(0x0024)]
public readonly GuildWarsArray<WrappedPointer<Bag>> Bags;
[FieldOffset(0x00B8)]
public readonly GuildWarsArray<WrappedPointer<Item>> Items;
[FieldOffset(0x00F8)]
public readonly WrappedPointer<Inventory> Inventory;
}
@@ -0,0 +1,20 @@
using System.Runtime.InteropServices;
namespace Daybreak.API.Interop.GuildWars;
[StructLayout(LayoutKind.Explicit, Pack = 1, Size = 0x34)]
public unsafe struct PartySearchWindowContext
{
[FieldOffset(0x0000)]
public uint* Vtable;
[FieldOffset(0x0004)]
public uint FrameId;
[FieldOffset(0x0020)]
public uint SelectedPartySearchId;
[FieldOffset(0x0024)]
public uint SelectedHeroId;
[FieldOffset(0x0028)]
public uint SelectedHenchmanId;
[FieldOffset(0x002C)]
public uint CurrentTab;
}
@@ -9,7 +9,7 @@ public enum PlayerContextFlags : uint
PvP = 0x800
}
[StructLayout(LayoutKind.Explicit, Pack = 1, Size = 0x4C)]
[StructLayout(LayoutKind.Explicit, Pack = 1, Size = 0x50)]
public readonly unsafe struct PlayerContext
{
[FieldOffset(0x0000)]
@@ -37,8 +37,11 @@ public readonly unsafe struct PlayerContext
public readonly uint ActiveTitleTier;
[FieldOffset(0x0034)]
public readonly uint PlayerNumber;
public readonly uint ReforgedOrDhuumsFlags;
[FieldOffset(0x0038)]
public readonly uint PlayerNumber;
[FieldOffset(0x003C)]
public readonly uint PartySize;
}
@@ -12,13 +12,7 @@ public readonly struct PreGameContext
[FieldOffset(0x0124)]
public readonly uint ChosenCharacterIndex;
[FieldOffset(0x0140)]
public readonly uint Index1;
[FieldOffset(0x0144)]
public readonly uint Index2;
[FieldOffset(0x00148)]
[FieldOffset(0x0148)]
public readonly GuildWarsArray<LoginCharacterContext> LoginCharacters;
}
@@ -0,0 +1,40 @@
namespace Daybreak.API.Interop.GuildWars;
public enum EnumPreference : uint
{
CharSortOrder = 0,
AntiAliasing = 1,
Reflections = 2,
ShaderQuality = 3,
ShadowQuality = 4,
TerrainQuality = 5,
InterfaceSize = 6,
FrameLimiter = 7,
Count = 8
}
public enum CharSortOrder : uint
{
None = 0,
Alphabetize = 1,
PvPRP = 2
}
public enum NumberPreference : uint
{
AutoTournPartySort = 0,
ChatState = 1,
Count = 0x2b
}
public enum FlagPreference : uint
{
}
public enum StringPreference : uint
{
Unk1 = 0,
Unk2 = 1,
LastCharacterName = 2,
Count = 3
}
-3
View File
@@ -14,9 +14,6 @@ public readonly struct QuestContext
[FieldOffset(0x0018)]
public readonly Vector3 Marker;
[FieldOffset(0x0028)]
public readonly uint MapTo;
}
[StructLayout(LayoutKind.Sequential, Pack = 1, Size = 0xC)]
@@ -0,0 +1,10 @@
using System.Runtime.InteropServices;
namespace Daybreak.API.Interop.GuildWars;
[StructLayout(LayoutKind.Explicit, Pack = 1)]
public struct TextParserContext
{
[FieldOffset(0x01D0)]
public Language Language;
}
+9 -1
View File
@@ -11,16 +11,24 @@ public enum TitleProps : uint
// 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)]
[StructLayout(LayoutKind.Explicit, Pack = 1, Size = 0x2C)]
public readonly struct TitleContext
{
[FieldOffset(0x0000)]
public readonly TitleProps Props;
[FieldOffset(0x0004)]
public readonly uint CurrentPoints;
[FieldOffset(0x0008)]
public readonly uint CurrentTitleTierIndex;
[FieldOffset(0x000C)]
public readonly uint PointsNeededForCurrentRank;
[FieldOffset(0x0014)]
public readonly uint NextTitleTierIndex;
[FieldOffset(0x0018)]
public readonly uint PointsNeededForNextRank;
[FieldOffset(0x001C)]
public readonly uint MaxTitleRank;
[FieldOffset(0x0020)]
public readonly uint MaxTitleTierIndex;
}
@@ -21,7 +21,12 @@ public static class WebApplicationBuilderExtensions
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")
path: "Daybreak.API.log",
rollingInterval: RollingInterval.Day,
rollOnFileSizeLimit: true,
fileSizeLimitBytes: 50 * 1024 * 1024,
retainedFileCountLimit: 1,
shared: true)
.CreateLogger());
builder.Services.AddScoped<LoggingEnrichmentMiddleware>();
builder.Services.AddOptions<CorrelationVectorOptions>()
+2 -2
View File
@@ -7,12 +7,12 @@ public sealed class ByteConsumerEntry(
{
private readonly Action<ReadOnlySpan<byte>> handler = handler.ThrowIfNull();
private DateTimeOffset lastConsume = DateTimeOffset.MinValue;
private DateTime lastConsume = DateTime.MinValue;
public Guid Id { get; } = id;
public TimeSpan Frequency { get; } = freq;
public void TryConsume(DateTimeOffset currentTime, ReadOnlySpan<byte> value)
public void TryConsume(DateTime currentTime, ReadOnlySpan<byte> value)
{
if (currentTime - this.lastConsume >= this.Frequency)
{
@@ -0,0 +1,9 @@
using Daybreak.API.Interop.GuildWars;
namespace Daybreak.API.Models;
public struct DecodeStringContext(WrappedPointer<char> encodedString, TaskCompletionSource<string?> taskCompletionSource)
{
public WrappedPointer<char> EncodedString = encodedString;
public TaskCompletionSource<string?> TaskCompletionSource = taskCompletionSource;
}
+11 -147
View File
@@ -3,152 +3,16 @@
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
};
KeyDown = 0x20, // wparam = UIPacket::kKeyAction*
KeyUp = 0x22, // wparam = UIPacket::kKeyAction*
MouseClick = 0x24, // wparam = UIPacket::kMouseClick*
MouseClick2 = 0x31, // wparam = UIPacket::kMouseAction*
MouseAction = 0x32, // wparam = UIPacket::kMouseAction*
FrameMessage_QuerySelectedIndex = 0x59, // Used to query selected index in character selector
WriteToChatLog = 0x10000000 | 0x7F, // wparam = UIPacket::kWriteToChatLog*. Triggered by the game when it wants to add a new message to chat.
WriteToChatLogWithSender = 0x10000000 | 0x80, // wparam = UIPacket::kWriteToChatLogWithSender*. Triggered by the game when it wants to add a new message to chat.
CheckUIState = 0x10000000 | 0x118, // lparam = uint32_t* out state (2 = char select ready)
Logout = 0x10000000 | 0x9D, // wparam = { bool unknown, bool character_select }
}
+23 -3
View File
@@ -11,6 +11,16 @@ public static class UIPackets
Right = 0x2
}
public enum ActionState
{
MouseDown = 0x6,
MouseUp = 0x7,
MouseClick = 0x8,
MouseDoubleClick = 0x9,
DragRelease = 0xb,
KeyDown = 0xe
}
[StructLayout(LayoutKind.Sequential, Pack = 1)]
public readonly struct LogChatMessage(string message, Channel channel)
{
@@ -29,11 +39,11 @@ public static class UIPackets
}
[StructLayout(LayoutKind.Sequential, Pack = 1)]
public readonly struct KeyAction(uint key)
public readonly struct KeyAction(uint key, uint wParam = 0x4000, uint lParam = 0x0006)
{
public readonly uint Key = key;
public readonly uint WParam = 0x4000;
public readonly uint LParam = 0x0006;
public readonly uint WParam = wParam;
public readonly uint LParam = lParam;
}
[StructLayout(LayoutKind.Sequential, Pack = 1)]
@@ -43,4 +53,14 @@ public static class UIPackets
public readonly uint IsDoubleClick = isDoubleClick;
public readonly uint UnknownTypeScreenPos = unknownTypeScreenPos;
}
[StructLayout(LayoutKind.Sequential, Pack = 1)]
public readonly struct MouseAction(uint frameId, uint childOffsetId, ActionState currentState, nuint wparam = 0, nuint lparam = 0)
{
public readonly uint FrameId = frameId;
public readonly uint ChildOffsetId = childOffsetId;
public readonly ActionState CurrentState = currentState;
public readonly nuint WParam = wparam;
public readonly nuint LParam = lparam;
}
}
+2 -1
View File
@@ -11,6 +11,7 @@ internal unsafe static partial class NativeMethods
public const int WM_KEYDOWN = 0x0100;
public const int WM_KEYUP = 0x0101;
public const int WM_CHAR = 0x0102;
public const int VK_LEFT = 0x25;
public const int VK_RIGHT = 0x27;
public const uint MEM_COMMIT = 0x1000;
@@ -36,7 +37,7 @@ internal unsafe static partial class NativeMethods
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);
public static partial nint SendMessageW(nint hWnd, int Msg, nint wParam, nuint lParam);
[LibraryImport("kernel32.dll", SetLastError = true)]
public static partial nint VirtualAlloc(
@@ -1,5 +1,6 @@
using Daybreak.API.Models;
using Daybreak.Shared.Models.Api;
using Daybreak.Shared.Models.Guildwars;
using System.Text.Json;
using System.Text.Json.Serialization;
@@ -11,8 +12,6 @@ namespace Daybreak.API.Serialization;
[JsonSerializable(typeof(uint))]
[JsonSerializable(typeof(nuint))]
[JsonSerializable(typeof(bool))]
[JsonSerializable(typeof(Task<IResult>))]
[JsonSerializable(typeof(Task))]
[JsonSerializable(typeof(Dictionary<string, string>))]
[JsonSerializable(typeof(List<string>))]
[JsonSerializable(typeof(JsonElement))]
@@ -35,6 +34,10 @@ namespace Daybreak.API.Serialization;
[JsonSerializable(typeof(InstanceInfo))]
[JsonSerializable(typeof(TitleInfo))]
[JsonSerializable(typeof(LoginInfo))]
[JsonSerializable(typeof(MainPlayerBuildContext))]
[JsonSerializable(typeof(InventoryInformation))]
[JsonSerializable(typeof(ItemProperty))]
[JsonSourceGenerationOptions(UseStringEnumConverter = true)]
public partial class ApiJsonSerializerContext : JsonSerializerContext
{
}
@@ -1,56 +0,0 @@
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);
}
}
+161 -122
View File
@@ -1,4 +1,6 @@
using Daybreak.API.Services.Interop;
using Daybreak.API.Interop.GuildWars;
using Daybreak.API.Models;
using Daybreak.API.Services.Interop;
using Daybreak.Shared.Models.Api;
using System.Core.Extensions;
using System.Extensions.Core;
@@ -13,6 +15,7 @@ public sealed class CharacterSelectService(
UIContextService uiContextService,
GameThreadService gameThreadService,
GameContextService gameContextService,
PreferencesService preferencesService,
ILogger<CharacterSelectService> logger)
{
[StructLayout(LayoutKind.Sequential, Pack = 1)]
@@ -27,6 +30,7 @@ public sealed class CharacterSelectService(
private readonly UIContextService uiContextService = uiContextService.ThrowIfNull();
private readonly GameThreadService gameThreadService = gameThreadService.ThrowIfNull();
private readonly GameContextService gameContextService = gameContextService.ThrowIfNull();
private readonly PreferencesService preferencesService = preferencesService.ThrowIfNull();
private readonly ILogger<CharacterSelectService> logger = logger.ThrowIfNull();
public Task<CharacterSelectInformation?> GetCharacterSelectInformation(CancellationToken cancellationToken)
@@ -53,7 +57,7 @@ public sealed class CharacterSelectService(
var currentUuid = gameContext.Pointer->CharContext->PlayerUuid.ToString();
var availableChars = new List<CharacterSelectEntry>((int)availableCharsContext.Pointer->Size);
foreach(var charContext in *availableCharsContext.Pointer)
foreach (var charContext in *availableCharsContext.Pointer)
{
var nameSpan = charContext.Name.AsSpan();
var name = new string(nameSpan[..nameSpan.IndexOf('\0')]);
@@ -68,8 +72,17 @@ public sealed class CharacterSelectService(
charContext.IsPvp));
}
var currentCharacter = availableChars.FirstOrDefault(c => c.Uuid == currentUuid);
return new CharacterSelectInformation(currentCharacter, availableChars);
// TODO: Reforged sometimes returns Zero UUID even when in-game, need to investigate why
if (currentUuid != Uuid.Zero.ToString())
{
var currentCharacter = availableChars.FirstOrDefault(c => c.Uuid == currentUuid);
return new CharacterSelectInformation(currentCharacter, availableChars);
}
else
{
var currentCharacter = availableChars.FirstOrDefault();
return new CharacterSelectInformation(currentCharacter, availableChars);
}
}
}, cancellationToken);
}
@@ -85,20 +98,10 @@ public sealed class CharacterSelectService(
await this.TriggerLogOut(cancellationToken);
var indexResult = await this.WaitForLoginScreenAndGetCurrentAndDesiredIndex(characterName, cancellationToken);
if (indexResult is null)
var selectResult = await this.WaitForCharSelectAndSelectCharacter(characterName, cancellationToken);
if (!selectResult)
{
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);
scopedLogger.LogError("Failed to select character {name}", characterName);
return false;
}
@@ -123,86 +126,154 @@ public sealed class CharacterSelectService(
await this.TriggerLogOut(cancellationToken);
var indexResult = await this.WaitForLoginScreenAndGetCurrentAndDesiredIndex(desiredCharName, cancellationToken);
if (indexResult is null)
var selectResult = await this.WaitForCharSelectAndSelectCharacter(desiredCharName, cancellationToken);
if (!selectResult)
{
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);
scopedLogger.LogError("Failed to select character {name} with uuid {uuid}", desiredCharName, uuid);
return false;
}
return true;
}
private async Task<bool> ValidateState(CancellationToken cancellationToken)
private async Task<bool> SelectCharacterToPlay(string characterName, bool play, CancellationToken cancellationToken)
{
var scopedLogger = this.logger.CreateScopedLogger();
return await this.gameThreadService.QueueOnGameThread(() =>
{
return this.instanceContextService.GetInstanceType() is not API.Interop.GuildWars.InstanceType.Loading;
unsafe
{
var selectorFrame = this.GetCharSelectorFrame();
if (selectorFrame is null)
{
scopedLogger.LogError("Character selector frame not found");
return false;
}
var ctx = this.uiContextService.GetFrameContext<CharSelectorContext>(selectorFrame);
if (ctx.IsNull)
{
scopedLogger.LogError("Character selector context not found");
return false;
}
var panesFrame = this.uiContextService.GetChildFrame(selectorFrame, 0);
if (panesFrame.IsNull)
{
scopedLogger.LogError("Character panes frame not found");
return false;
}
// Get current selected index
var selectedIdx = 0;
this.uiContextService.SendFrameUIMessage(panesFrame, UIMessage.FrameMessage_QuerySelectedIndex, null, &selectedIdx);
var chosen = false;
for (uint i = 0; !chosen && i < ctx.Pointer->Chars.Size; i++)
{
var c = ctx.Pointer->Chars.Buffer[i];
var charNameSpan = c.Pointer->Name.AsSpan();
var nullIdx = charNameSpan.IndexOf('\0');
if (nullIdx <= 0)
{
continue;
}
var charName = new string(charNameSpan[..nullIdx]);
if (!charName.StartsWith(characterName, StringComparison.OrdinalIgnoreCase))
{
continue;
}
while (selectedIdx != i)
{
var keyAction = new UIPackets.KeyAction(0x1c);
this.uiContextService.SendFrameUIMessage(panesFrame, UIMessage.KeyDown, &keyAction);
var newIdx = selectedIdx;
this.uiContextService.SendFrameUIMessage(panesFrame, UIMessage.FrameMessage_QuerySelectedIndex, null, &newIdx);
if (newIdx == selectedIdx)
{
break; // This shouldn't happen - the character should have changed
}
selectedIdx = newIdx;
}
chosen = selectedIdx == i;
break;
}
if (!chosen)
{
scopedLogger.LogError("Failed to select character {name}", characterName);
return false;
}
// TODO: This needs to be reworked to use UI messages to click on Play
var hwnd = this.platformContextService.GetWindowHandle();
if (!hwnd.HasValue)
{
scopedLogger.LogError("Failed to get game window handle");
return false;
}
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;
}
}, cancellationToken);
}
private async Task<bool> NavigateToCharAndPlay(uint currentIndex, uint desiredIndex, CancellationToken cancellationToken)
private async Task<bool> WaitForCharSelectAndSelectCharacter(string characterName, CancellationToken cancellationToken)
{
var scopedLogger = this.logger.CreateScopedLogger();
// Wait for character select to be ready
while (!cancellationToken.IsCancellationRequested)
{
await Task.Delay(100, cancellationToken);
var result = await this.gameThreadService.QueueOnGameThread(() =>
try
{
unsafe
if (await this.gameThreadService.QueueOnGameThread(this.IsCharSelectReady, cancellationToken))
{
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;
break;
}
}, cancellationToken);
if (result)
await Task.Delay(100, cancellationToken);
}
catch(Exception e)
{
return true;
scopedLogger.LogError(e, "Error while waiting for character select to be ready");
throw;
}
}
return false;
if (cancellationToken.IsCancellationRequested)
{
return false;
}
var previousOrderPreference = await this.gameThreadService.QueueOnGameThread(() => this.preferencesService.GetEnumPreference(EnumPreference.CharSortOrder), cancellationToken);
if (previousOrderPreference is null)
{
scopedLogger.LogError("Failed to get previous character sort order preference");
return false;
}
scopedLogger.LogInformation("Previous character sort order preference: {order}. Setting order to alphabetize", (CharSortOrder)previousOrderPreference);
await this.gameThreadService.QueueOnGameThread(() => this.preferencesService.SetEnumPreference(EnumPreference.CharSortOrder, (uint)CharSortOrder.Alphabetize), cancellationToken);
try
{
return await this.SelectCharacterToPlay(characterName, play: true, cancellationToken);
}
finally
{
// TODO: Delay to ensure character select has fully processed the selection before reverting sort order. Should be done after loading into the map.
await Task.Delay(1000, cancellationToken);
await this.gameThreadService.QueueOnGameThread(() => this.preferencesService.SetEnumPreference(EnumPreference.CharSortOrder, previousOrderPreference.Value), cancellationToken);
}
}
private async Task<string?> GetCharNameByUuid(string uuid, CancellationToken cancellationToken)
@@ -247,67 +318,35 @@ public sealed class CharacterSelectService(
{
await this.gameThreadService.QueueOnGameThread(() =>
{
var logoutMessage = new LogOutMessage(0, 0);
var logoutMessage = new LogOutMessage(0, 1); // Changed to 1 for character select
unsafe
{
this.uiContextService.SendMessage(Models.UIMessage.Logout, (uint)&logoutMessage, 0);
this.uiContextService.SendMessage(UIMessage.Logout, (uint)&logoutMessage, 0);
}
}, cancellationToken);
}
private async Task<(uint DesiredIndex, uint CurrentIndex)?> WaitForLoginScreenAndGetCurrentAndDesiredIndex(string desiredCharName, CancellationToken cancellationToken)
private async Task<bool> ValidateState(CancellationToken cancellationToken)
{
uint? desiredIndex = default;
uint? currentIndex = default;
while (!cancellationToken.IsCancellationRequested)
return await this.gameThreadService.QueueOnGameThread(() =>
{
await Task.Delay(100, cancellationToken);
var ready = await this.gameThreadService.QueueOnGameThread(() =>
{
unsafe
{
var preGameContext = this.gameContextService.GetPreGameContext();
if (preGameContext.IsNull)
{
return false;
}
return this.instanceContextService.GetInstanceType() is not API.Interop.GuildWars.InstanceType.Loading;
}, cancellationToken);
}
var uiState = 10U;
this.uiContextService.SendMessage(Models.UIMessage.CheckUIState, 0, (uint)&uiState);
var loginScreen = uiState == 2;
if (!loginScreen)
{
return false;
}
private unsafe bool IsCharSelectReady()
{
return this.GetCharSelectorFrame() is not null;
}
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;
}
private unsafe Frame* GetCharSelectorFrame()
{
var selectorFrame = this.uiContextService.GetFrameByLabel("Selector");
if (selectorFrame.IsNull)
{
return null;
}
if (desiredIndex is not null && currentIndex is not null)
{
return (desiredIndex.Value, currentIndex.Value);
}
return default;
return selectorFrame.Pointer;
}
}
+117
View File
@@ -0,0 +1,117 @@
namespace Daybreak.API.Services;
public sealed class HashingService
{
private const uint DefaultHash = 0x325D1EAE;
private const uint WStringHashSeed1 = 0xE2C15C9D;
private const uint WStringHashSeed2 = 0x2170A28A;
private static readonly uint[] HashTable = [
0x2EB3C21D, 0xA6F1D482, 0xBE24628F, 0x35699E30, 0xC87EE224, 0x04BA0097, 0x791F0EAF, 0xA162D466,
0x685B376A, 0xF82133BA, 0x4682D46B, 0xCB25C0C5, 0xAC3F9D22, 0xABF17202, 0xEF987728, 0x3E400B83,
0x63003108, 0xD83F1C93, 0x5058F507, 0x032E9991, 0x3518BC24, 0x1F90D44B, 0x92508DCE, 0x42DAA15C,
0x5B4105A3, 0x4D5026AD, 0x07F838B0, 0xE8C4D2A2, 0xF9EF4F54, 0x1FC1A85F, 0xB9041A40, 0x446F6969,
0xACF1EE39, 0x1828CC65, 0xC2F6C59D, 0x2456C3C0, 0x64F70142, 0x9C8ADB42, 0xC76781BF, 0x6AEA07E3,
0xE98B14BF, 0x7DBCB35E, 0x6CE78C80, 0x1E3F93DE, 0x0954FEB7, 0xB3103AEF, 0x6DAAC1EE, 0x6535E301,
0x4A221E68, 0x819CDD81, 0x5378CE3B, 0xE7348236, 0x2C3A7BCC, 0x31C50234, 0x745319DA, 0xF0897A0B,
0x5C182968, 0x48E018F4, 0x593E3BB6, 0xAB588C8D, 0xF1101F1F, 0x1D36E8BC, 0xABC7C7C9, 0xF58AA6C8,
0xF75B4B98, 0xC447B576, 0x6E5290F2, 0xD344EDF8, 0x5E1BCAF1, 0xEE892405, 0x798F7609, 0x8905CC29,
0x8977DEDC, 0x007850BC, 0x0AA3114A, 0x2F61C8F2, 0x376DFAA4, 0x0BABFC12, 0x98E64E82, 0xE6743711,
0xE26B26B6, 0xCBB10C72, 0x87446F8F, 0x96E26C0D, 0xCF140831, 0x81F69B0B, 0x5C9DF30B, 0xB276F929,
0x2712C5B6, 0x80EA90D6, 0x8EB30322, 0xCF7A705A, 0x57A17E8C, 0x556A2425, 0x15104549, 0x5F3D549F,
0x5D98FDEE, 0x9D8FA73E, 0x9206C9EF, 0xEDA0541D, 0xA3164F2D, 0x04C83543, 0x9BB27230, 0xEE97F0BA,
0xB6AEB38C, 0x41FF4DA7, 0x4023EE54, 0xECBB78BC, 0xC2C54733, 0x8850B63F, 0x93BF56EF, 0xC80177D7,
0xE68B0C13, 0x3C1E9D4C, 0x5CB3EACC, 0xF16F5795, 0xAC2B8A5C, 0x952E466E, 0x3095B0D5, 0xEC9D3A43,
0x23D8FCF7, 0x10C7A93F, 0x1B479FB5, 0x3664CF8F, 0xA514A56B, 0xE16C4886, 0x63828188, 0xA47F14C1,
0x391C237C, 0xFB15911F, 0x29825A01, 0xFC66DA40, 0xB2719F4E, 0x7B0D1611, 0x8F1EE7DE, 0xA6A1E77D,
0xDFC25D30, 0x76377036, 0x8145DB82, 0x2300A4E9, 0xA2417F88, 0x1B8C6AFC, 0x68EBDC8E, 0x48FC0C41,
0x040C3DCD, 0x17F02C44, 0xCC40B026, 0x5089EE1E, 0x2A594C28, 0x0CC198EC, 0x3F676F3D, 0xCD0AA239,
0x274A31A4, 0xC6B4D4FA, 0x9480A62F, 0xAF8862EF, 0x627CA44D, 0x572E89D3, 0x4C05136C, 0x60F36E8C,
0x7DE31FEF, 0x1B97FED3, 0xED99E2BF, 0xA034D418, 0xBBDB3E04, 0x1F198B71, 0xED45E889, 0xF7F1B0DC,
0x1592B9A5, 0x96D60F96, 0x03D616EE, 0xA5EF679D, 0x831D791D, 0x579D5FD1, 0x4B44DC48, 0x38F7A2F0,
0x2BF34A28, 0xAF14318C, 0xBD66D20A, 0x9AC72D2A, 0xC41ADB95, 0x860B2205, 0xE0E580E7, 0x3A06669C,
0x2D1A40A2, 0x577EDF60, 0x1A37693A, 0x3E576C35, 0xE3857565, 0xE46E75EE, 0x94E3625D, 0x7D1B9280,
0x2A0CCCE1, 0xBAB36163, 0x66E4E087, 0x940129BB, 0x59C197CD, 0xB0634139, 0xBB2964E1, 0x17207138,
0xD8BD3728, 0xB324876C, 0x7C72FD04, 0xBB5D9D82, 0xD9B40236, 0xC0045BF6, 0xF85100ED, 0x36493582,
0x8D0DF92A, 0xC3328368, 0xC202E412, 0x7ADE0064, 0xD01529C6, 0x4445B2AD, 0x0F6FC63B, 0xEF976FC5,
0x709EC6BE, 0x69D31EA1, 0x794BFEB4, 0xB522DB95, 0x700DE6DF, 0x49098B71, 0x38B7FA82, 0x11EDDC04,
0x5BB21D9F, 0xB14BC6D4, 0x3009D754, 0x451F5F3D, 0xFAA58575, 0xFB190B0D, 0x999DF79F, 0x6798A7AA,
0x0BC434FF, 0x429E6C15, 0xA1EA2298, 0x55A57B81, 0xD7D74B8D, 0x8FECB38B, 0x658AD588, 0xF0190227,
0xBE5D2B36, 0x62C59613, 0xC9FEDD64, 0xADD07A1E, 0x757A7679, 0xD78B6DCC, 0x56F88B2C, 0x4CC020BB,
0xA88B51FE, 0xF47F7691, 0x48587EDD, 0x3FCEC5DB, 0xA7E127AC, 0x561F6161, 0x37AB9607, 0xFE23282B,
];
private static readonly uint[] StringHashTable = [
0x92B9A528, 0x25D4FC88, 0xEDCBEFB8, 0x51063A80,
0x91341C61, 0x0261229D, 0x726F48ED, 0xCE1C088C,
0x76253EB5, 0x31E3A0DE, 0xA2AAD215, 0xCA7D6D27,
0xA5F98970, 0x0541C365, 0x3C14FF04, 0x5056AF4F
];
private static uint HashHelper(uint result, byte b)
{
return result + (HashTable[result >> 24] ^ (result >> 6) ^ HashTable[b]);
}
public uint Hash(byte[] data)
{
var hash = DefaultHash;
foreach(var b in data)
{
hash = HashHelper(hash, b);
}
return hash;
}
public uint Hash(byte data)
{
return HashHelper(DefaultHash, data);
}
public uint Hash(short data)
{
uint hash = DefaultHash;
hash = HashHelper(hash, (byte)(data >> 0));
hash = HashHelper(hash, (byte)(data >> 8));
return hash;
}
public uint Hash(uint data)
{
uint hash = DefaultHash;
hash = HashHelper(hash, (byte)(data >> 0));
hash = HashHelper(hash, (byte)(data >> 8));
hash = HashHelper(hash, (byte)(data >> 16));
hash = HashHelper(hash, (byte)(data >> 24));
return hash;
}
public uint Hash(string data, int maxLength = -1)
{
if (maxLength is -1)
{
maxLength = data.Length;
}
uint seed1 = WStringHashSeed1;
uint seed2 = WStringHashSeed2;
uint hash = DefaultHash;
for(var i = 0; i < maxLength && i < data.Length; i++)
{
var c = data[i];
var uc = (uint)c;
if (c >= 'a' && c <= 'z')
{
uc -= 0x20;
}
seed1 = (seed1 << 3) ^ uc;
seed2 += StringHashTable[seed1 & 0xf];
hash ^= seed2 + seed1;
}
return hash;
}
}
@@ -12,9 +12,9 @@ public unsafe sealed class AgentContextService : IAddressHealthService
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 string PlayerAgentIdMask = "xx????xxxxxxx";
private const int PlayerAgentIdOffset = -0xE;
private static readonly byte[] PlayerAgentIdAddressPattern = [0x5d, 0xe9, 0x00, 0x00, 0x00, 0x00, 0x55, 0x8b, 0xec, 0x53];
private static readonly byte[] PlayerAgentIdAddressPattern = [0x5d, 0xe9, 0x00, 0x00, 0x00, 0x00, 0x55, 0x8b, 0xec, 0x56, 0x57, 0x8b, 0x7d];
private readonly MemoryScanningService memoryScanningService;
private readonly GWAddressCache agentArrayAddress;
@@ -3,14 +3,23 @@ using Daybreak.API.Interop.GuildWars;
using Daybreak.API.Models;
using System.Core.Extensions;
using System.Extensions.Core;
using System.Runtime.InteropServices;
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];
// TLS-based pattern to find GameContext getter function
private const string GameContextTlsMask = "xx????xxxxxxxxxxx????x";
private const int GameContextTlsOffset = 0;
private static readonly byte[] GameContextTlsPattern =
[
0x8B, 0x0D, 0x00, 0x00, 0x00, 0x00, // mov ecx, [TlsIndexPtr] - 6 bytes: xx????
0x64, 0xA1, 0x2C, 0x00, 0x00, 0x00, // mov eax, fs:[0000002C] - 6 bytes: xxxxxx
0x8B, 0x04, 0x88, // mov eax, [eax+ecx*4] - 3 bytes: xxx
0x8B, 0x80, 0x04, 0x00, 0x00, 0x00, // mov eax, [eax+00000004] - 6 bytes: xx????
0xC3 // ret - 1 byte: x
]; // Total: 22 bytes, mask: 22 chars
private const string PreGameContextFile = "UiPregame.cpp";
private const string PreGameContextAssertion = "!s_scene";
@@ -21,7 +30,7 @@ public unsafe sealed class GameContextService : IAddressHealthService
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 gameContextGetterAddress;
private readonly GWAddressCache preGameContextAddress;
private readonly GWAddressCache availableCharsAddress;
private readonly ILogger<GameContextService> logger;
@@ -32,18 +41,21 @@ public unsafe sealed class GameContextService : IAddressHealthService
{
this.logger = logger.ThrowIfNull();
this.memoryScanningService = memoryScanningService.ThrowIfNull();
this.baseAddress = new GWAddressCache(this.GetBaseAddress);
this.gameContextGetterAddress = new GWAddressCache(this.GetGameContextGetterAddress);
this.preGameContextAddress = new GWAddressCache(this.GetPreGameContextAddress);
this.availableCharsAddress = new GWAddressCache(this.GetAvailableCharactersAddress);
}
[UnmanagedFunctionPointer(CallingConvention.Cdecl)]
private delegate nuint GetGameContextDelegate();
public List<AddressState> GetAddressStates()
{
return [
new AddressState
{
Address = this.baseAddress.GetAddress() ?? 0,
Name = nameof(this.baseAddress),
Address = this.gameContextGetterAddress.GetAddress() ?? 0,
Name = nameof(this.gameContextGetterAddress),
},
new AddressState
{
@@ -113,41 +125,46 @@ public unsafe sealed class GameContextService : IAddressHealthService
private nuint GetGameContextAddress()
{
var scopedLogger = this.logger.CreateScopedLogger();
if (this.baseAddress.GetAddress() is not nuint basePtr)
// Get the address of the GameContext getter function
var getterAddress = this.gameContextGetterAddress.GetAddress();
if (getterAddress is null or 0)
{
scopedLogger.LogError("Failed to get base address");
return default;
scopedLogger.LogError("Failed to get GameContext getter function address");
return 0;
}
nuint** baseContext = *(nuint***)basePtr;
if (baseContext is null)
// Call the function to get GameContext
var getter = Marshal.GetDelegateForFunctionPointer<GetGameContextDelegate>((nint)getterAddress);
var gameContextPtr = getter();
if (gameContextPtr == 0)
{
scopedLogger.LogError("Failed to get base context address");
return default;
scopedLogger.LogError("GameContext getter returned null");
return 0;
}
var globalContextPtr = baseContext[0x6];
if (globalContextPtr is null)
{
scopedLogger.LogError("Failed to get game context address");
return default;
}
return (nuint)globalContextPtr;
scopedLogger.LogInformation("GameContext address: 0x{address:X8}", gameContextPtr);
return gameContextPtr;
}
private nuint GetBaseAddress()
private nuint GetGameContextGetterAddress()
{
var scopedLogger = this.logger.CreateScopedLogger();
var baseAddress = this.memoryScanningService.FindAndResolveAddress(BaseAddressPattern, BaseAddressMask, BaseAddressOffset);
if (baseAddress is 0)
var address = this.memoryScanningService.FindAddress(
GameContextTlsPattern,
GameContextTlsMask,
0);
if (address is 0)
{
scopedLogger.LogError("Failed to find base address");
scopedLogger.LogError("Failed to find GameContext getter function");
return 0U;
}
scopedLogger.LogInformation("Base address: 0x{address:X8}", baseAddress);
return baseAddress;
scopedLogger.LogInformation("GameContext getter function: 0x{address:X8}", address);
return address;
}
private unsafe nuint GetPreGameContextAddress()
@@ -160,9 +177,9 @@ public unsafe sealed class GameContextService : IAddressHealthService
return 0U;
}
preGameContextAddress = *(nuint*)preGameContextAddress;
scopedLogger.LogInformation("Pre-game context address: 0x{address:X8}", preGameContextAddress);
return preGameContextAddress;
var actualAddress = *(nuint*)preGameContextAddress;
scopedLogger.LogInformation("Pre-game context address: 0x{address:X8}", actualAddress);
return actualAddress;
}
private unsafe nuint GetAvailableCharactersAddress()
@@ -16,7 +16,7 @@ public sealed class GameThreadService
[SuppressUnmanagedCodeSecurity]
[UnmanagedFunctionPointer(CallingConvention.Cdecl)]
private delegate void LeaveGameThread(nint ctx);
private delegate void LeaveGameThread(nint arg1, nint arg2);
private readonly MemoryScanningService memoryScanningService;
private readonly GWHook<LeaveGameThread> leaveGameThreadHook;
@@ -103,7 +103,7 @@ public sealed class GameThreadService
scopedLogger.LogInformation("Hooks initialized");
}
private void OnLeaveGameThread(nint ctx)
private void OnLeaveGameThread(nint arg1, nint arg2)
{
var scopedLogger = this.logger.CreateScopedLogger();
while(this.queuedItems.TryDequeue(out var item))
@@ -137,6 +137,6 @@ public sealed class GameThreadService
}
}
this.leaveGameThreadHook.Continue(ctx);
this.leaveGameThreadHook.Continue(arg1, arg2);
}
}
@@ -1,9 +1,7 @@
using Daybreak.Shared.Models.Interop;
using PeNet;
using PeNet;
using PeNet.Header.Pe;
using System.Core.Extensions;
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using System.Extensions.Core;
using System.Globalization;
using System.Runtime.InteropServices;
@@ -11,30 +9,14 @@ using System.Text;
namespace Daybreak.API.Services.Interop;
public sealed unsafe class MemoryScanningService
public sealed unsafe class MemoryScanningService(
ILogger<MemoryScanningService> logger)
{
private readonly ILogger<MemoryScanningService> logger;
private readonly ILogger<MemoryScanningService> logger = logger.ThrowIfNull();
private readonly (nuint BaseAddress, ImageSectionHeader Section) textSection = GetSectionHeader(".text");
private readonly (nuint BaseAddress, ImageSectionHeader Section) dataSection = GetSectionHeader(".rdata");
public MemoryScanningService(
ILogger<MemoryScanningService> logger)
{
this.logger = logger.ThrowIfNull();
}
public T? ReadPointer<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors | DynamicallyAccessedMemberTypes.NonPublicConstructors)] T>(GuildwarsPointer<T> ptr)
where T : struct
{
var scopedLogger = this.logger.CreateScopedLogger();
scopedLogger.LogInformation("Reading pointer: 0x{ptr:X8}", ptr.Address);
if (ptr.Address is 0)
{
return default;
}
return Marshal.PtrToStructure<T>((nint)ptr.Address);
}
public uint GameTlsIndex { get; } = GetGameTlsIndex();
public nuint FunctionFromNearCall(nuint callInstructionAddress, bool checkValidPtr = true)
{
@@ -138,7 +120,7 @@ public sealed unsafe class MemoryScanningService
fileSearchPos = filePtr + 1;
// If what we found is …/file.hpp (no drive letter) fix it
// If what we found is "…/file.hpp" (no drive letter) fix it
if (Marshal.ReadByte((nint)(filePtr + 1)) != (byte)':')
{
// look ≤128 bytes backward for ':' and back-up one char
@@ -219,6 +201,12 @@ public sealed unsafe class MemoryScanningService
return address;
}
public nuint FindAddress(byte[] pattern, string mask, int offset, nuint startAddress, nuint endAddress)
{
var address = FindAddressInternal(startAddress, endAddress, pattern, mask, offset);
return address;
}
public nuint ToFunctionStart(nuint callInstructionAddress, uint scanRange = 0x500)
{
if (callInstructionAddress == 0)
@@ -236,6 +224,92 @@ public sealed unsafe class MemoryScanningService
return FindInRange(prologue, mask, 0, start, end);
}
public nuint FindNthUseOfString(string str, uint nth, int offset = 0, bool useTextSection = true)
{
var scopedLogger = this.logger.CreateScopedLogger();
var strBytes = Encoding.ASCII.GetBytes(str);
var mask = new string('x', strBytes.Length);
var foundStr = FindAddressInternal(this.dataSection, strBytes, mask, 0);
if (foundStr is 0)
{
scopedLogger.LogError("Failed to find string in .rdata section");
return 0;
}
var firstNullChar = FindInRange([0x0], "x", 1, foundStr, foundStr - 0x64);
if (firstNullChar is 0)
{
scopedLogger.LogError("Failed to find null terminator");
return 0;
}
return this.FindNthUseOfAddress(firstNullChar, nth, offset, useTextSection);
}
public nuint FindUseOfString(string str, int offset = 0, bool useTextSection = true)
{
return this.FindNthUseOfString(str, 0, offset, useTextSection);
}
public nuint FindNthUseOfWideString(string str, uint nth, int offset = 0, bool useTextSection = true)
{
var scopedLogger = this.logger.CreateScopedLogger();
var strBytes = Encoding.Unicode.GetBytes(str);
var mask = new string('x', strBytes.Length);
var foundStr = FindAddressInternal(this.dataSection, strBytes, mask, 0);
if (foundStr is 0)
{
scopedLogger.LogError("Failed to find wide string in .rdata section");
return 0;
}
var firstNullChar = FindInRange([0x0, 0x0], "xx", 2, foundStr, foundStr - 0x128);
if (firstNullChar is 0)
{
scopedLogger.LogError("Failed to find null terminator for wide string");
return 0;
}
return this.FindNthUseOfAddress(firstNullChar, nth, offset, useTextSection);
}
public nuint FindUseOfWideString(string str, int offset = 0, bool useTextSection = true)
{
return this.FindNthUseOfWideString(str, 0, offset, useTextSection);
}
private nuint FindNthUseOfAddress(nuint address, uint nth, int offset, bool useTextSection)
{
var (BaseAddress, Section) = useTextSection ? this.textSection : this.dataSection;
var addressBytes = BitConverter.GetBytes((uint)address);
var mask = "xxxx";
var currentAddress = BaseAddress + Section.VirtualAddress;
var endAddress = currentAddress + Section.VirtualSize;
uint foundCount = 0;
while (currentAddress < endAddress)
{
var found = FindAddressInternal(currentAddress, endAddress, addressBytes, mask, 0);
if (found is 0)
{
break;
}
if (foundCount == nth)
{
return (nuint)((nint)found + offset);
}
foundCount++;
currentAddress = found + 1;
}
return 0;
}
private static bool IsValidPtr(nuint address, (nuint BaseAddress, ImageSectionHeader Section) section)
{
return address > 0 &&
@@ -296,8 +370,12 @@ public sealed unsafe class MemoryScanningService
{
var textStart = section.BaseAddress + section.Section.VirtualAddress;
var textEnd = textStart + section.Section.VirtualSize - (uint)pattern.Length;
return FindAddressInternal(textStart, textEnd, pattern, mask, offset);
}
for (var cur = textStart; cur <= textEnd; ++cur)
private static nuint FindAddressInternal(nuint startAddr, nuint endAddr, byte[] pattern, string mask, int offset = 0)
{
for (var cur = startAddr; cur <= endAddr; ++cur)
{
if (Marshal.ReadByte((nint)cur) != pattern[0])
{
@@ -332,4 +410,30 @@ public sealed unsafe class MemoryScanningService
? throw new InvalidOperationException($"Failed to initialize memory scanner. Failed to find {headerName} section")
: ((nuint, ImageSectionHeader))(baseAddr, textHdr);
}
private static uint GetGameTlsIndex()
{
var m = Process.GetCurrentProcess().MainModule ?? throw new InvalidOperationException("Failed to get TLS index. Failed to find main module");
var baseAddr = (nint)m.BaseAddress;
var pe = new PeFile(m.FileName);
// Get the TLS data directory entry (index 9 = IMAGE_DIRECTORY_ENTRY_TLS)
var tlsDirectory = pe.ImageNtHeaders?.OptionalHeader.DataDirectory[9];
if (tlsDirectory is null || tlsDirectory.VirtualAddress == 0)
{
throw new InvalidOperationException("Failed to get TLS index. No TLS directory found");
}
// Calculate the address of the TLS directory in memory
var tlsDirectoryAddress = baseAddr + (nint)tlsDirectory.VirtualAddress;
// Read the IMAGE_TLS_DIRECTORY structure
// AddressOfIndex is at offset 8 (after StartAddressOfRawData and EndAddressOfRawData)
var addressOfIndexPtr = Marshal.ReadIntPtr(tlsDirectoryAddress + 8);
// Read the TLS index from the location pointed to by AddressOfIndex
var tlsIndex = (uint)Marshal.ReadInt32(addressOfIndexPtr);
return tlsIndex;
}
}
@@ -2,6 +2,7 @@
using Daybreak.API.Models;
using System.Core.Extensions;
using System.Extensions;
using static Daybreak.API.Models.UIPackets;
namespace Daybreak.API.Services.Interop;
@@ -10,13 +11,12 @@ public sealed class PartyContextService
{
private const string SearchButtonFile = "\\Code\\Gw\\Ui\\Game\\Party\\PtSearch.cpp";
private const string SearchButtonAssertion = "m_activeList == LIST_HEROES";
private const string WindowButtonFile = "\\Code\\Gw\\Ui\\Game\\Party\\PtButtons.cpp";
private const string WindowButtonAssertion = "m_selection.agentId";
private const string WindowButtonAssertion = "selection.agentId";
// Fastcall funcs do not work in .NET for x86 right now. https://github.com/dotnet/runtime/issues/113851
// This fastcall equivalent is <void*, uint, uint*, void>
// This fastcall equivalent is <void*, uint, UIPacket::kMouseAction*, void>
private readonly GWFastCall<nint, uint, nint, GWFastCall.Void> searchButtonCallback;
// This fastcall equivalent is <void*, uint, uint*, void>
// This fastcall equivalent is <void*, uint, UIPacket::kMouseAction*, void>
private readonly GWFastCall<nint, uint, nint, GWFastCall.Void> windowButtonCallback;
private readonly MemoryScanningService memoryScanningService;
@@ -29,7 +29,7 @@ public sealed class PartyContextService
this.memoryScanningService = memoryScanningService.ThrowIfNull();
this.logger = logger.ThrowIfNull();
this.searchButtonCallback = new(new GWAddressCache(() => this.memoryScanningService.ToFunctionStart(this.memoryScanningService.FindAssertion(SearchButtonFile, SearchButtonAssertion, 0, 0))));
this.windowButtonCallback = new(new GWAddressCache(() => this.memoryScanningService.ToFunctionStart(this.memoryScanningService.FindAssertion(WindowButtonFile, WindowButtonAssertion, 0, 0))));
this.windowButtonCallback = new(new GWAddressCache(() => this.memoryScanningService.ToFunctionStart(this.memoryScanningService.FindUseOfString(WindowButtonAssertion))));
}
public List<AddressState> GetAddressStates()
@@ -52,13 +52,13 @@ public sealed class PartyContextService
}
}
public unsafe bool CallSearchButtonCallback(void* ctx, uint edx, uint* wparam)
public unsafe bool CallSearchButtonCallback(void* ctx, uint edx, MouseAction* wparam)
{
this.searchButtonCallback.Invoke((nint)ctx, edx, (nint)wparam);
return true;
}
public unsafe bool CallWindowButtonCallback(void* ctx, uint edx, uint* wparam)
public unsafe bool CallWindowButtonCallback(void* ctx, uint edx, MouseAction* wparam)
{
this.windowButtonCallback.Invoke((nint)ctx, edx, (nint)wparam);
return true;
@@ -66,26 +66,28 @@ public sealed class PartyContextService
public unsafe bool AddHero(uint heroId)
{
var wparam = stackalloc uint[4];
wparam[2] = 0x6;
wparam[1] = 0x1;
var action = new MouseAction(
frameId: 0,
childOffsetId: 0x1,
currentState: ActionState.MouseUp);
var ctx = stackalloc uint[13];
ctx[0xb] = 1;
ctx[9] = heroId;
return this.CallSearchButtonCallback(ctx, 2, wparam);
ctx[0x9] = heroId;
return this.CallSearchButtonCallback(ctx, 2, &action);
}
public unsafe bool KickHero(uint heroId)
{
var wparam = stackalloc uint[4];
wparam[2] = 0x6;
wparam[1] = 0x6;
var action = new MouseAction(
frameId: 0,
childOffsetId: 0x6,
currentState: ActionState.MouseUp);
var ctx = stackalloc uint[13];
ctx[0xb] = 1;
ctx[9] = heroId;
return this.CallSearchButtonCallback(ctx, 0, wparam);
ctx[0x9] = heroId;
return this.CallSearchButtonCallback(ctx, 0, &action);
}
public bool KickAllHeroes() => this.KickHero(0x26);
@@ -94,6 +96,6 @@ public sealed class PartyContextService
{
var ctx = stackalloc uint[14];
ctx[0xd] = 1;
return this.CallWindowButtonCallback(ctx, 0, (uint*)0);
return this.CallWindowButtonCallback(ctx, 0, (MouseAction*)0);
}
}
@@ -0,0 +1,441 @@
using Daybreak.API.Interop;
using Daybreak.API.Interop.GuildWars;
using Daybreak.API.Models;
using System.Core.Extensions;
using System.Extensions.Core;
using System.Runtime.InteropServices;
namespace Daybreak.API.Services.Interop;
public sealed class PreferencesService
: IAddressHealthService
{
private readonly MemoryScanningService memoryScanningService;
private readonly ILogger<PreferencesService> logger;
private readonly GWAddressCache preferencesInitializedAddress;
private readonly GWAddressCache getEnumPreferenceAddress;
private readonly GWAddressCache setEnumPreferenceAddress;
private readonly GWAddressCache getNumberPreferenceAddress;
private readonly GWAddressCache setNumberPreferenceAddress;
private readonly GWAddressCache getFlagPreferenceAddress;
private readonly GWAddressCache setFlagPreferenceAddress;
private readonly GWAddressCache getStringPreferenceAddress;
private readonly GWAddressCache setStringPreferenceAddress;
[UnmanagedFunctionPointer(CallingConvention.Cdecl)]
private delegate uint GetEnumPreferenceDelegate(uint prefId);
[UnmanagedFunctionPointer(CallingConvention.Cdecl)]
private delegate void SetEnumPreferenceDelegate(uint prefId, uint value);
[UnmanagedFunctionPointer(CallingConvention.Cdecl)]
private delegate uint GetNumberPreferenceDelegate(uint prefId);
[UnmanagedFunctionPointer(CallingConvention.Cdecl)]
private delegate void SetNumberPreferenceDelegate(uint prefId, uint value);
[UnmanagedFunctionPointer(CallingConvention.Cdecl)]
[return: MarshalAs(UnmanagedType.I1)]
private delegate bool GetFlagPreferenceDelegate(uint prefId);
[UnmanagedFunctionPointer(CallingConvention.Cdecl)]
private delegate void SetFlagPreferenceDelegate(uint prefId, [MarshalAs(UnmanagedType.I1)] bool value);
[UnmanagedFunctionPointer(CallingConvention.Cdecl)]
private delegate nint GetStringPreferenceDelegate(uint prefId);
[UnmanagedFunctionPointer(CallingConvention.Cdecl)]
private delegate void SetStringPreferenceDelegate(uint prefId, nint value);
public PreferencesService(
MemoryScanningService memoryScanningService,
ILogger<PreferencesService> logger)
{
this.logger = logger.ThrowIfNull();
this.memoryScanningService = memoryScanningService.ThrowIfNull();
this.preferencesInitializedAddress = new GWAddressCache(this.GetPreferencesInitializedAddress);
this.getEnumPreferenceAddress = new GWAddressCache(this.GetGetEnumPreferenceAddress);
this.setEnumPreferenceAddress = new GWAddressCache(this.GetSetEnumPreferenceAddress);
this.getNumberPreferenceAddress = new GWAddressCache(this.GetGetNumberPreferenceAddress);
this.setNumberPreferenceAddress = new GWAddressCache(this.GetSetNumberPreferenceAddress);
this.getFlagPreferenceAddress = new GWAddressCache(this.GetGetFlagPreferenceAddress);
this.setFlagPreferenceAddress = new GWAddressCache(this.GetSetFlagPreferenceAddress);
this.getStringPreferenceAddress = new GWAddressCache(this.GetGetStringPreferenceAddress);
this.setStringPreferenceAddress = new GWAddressCache(this.GetSetStringPreferenceAddress);
}
public List<AddressState> GetAddressStates()
{
return
[
new AddressState { Address = this.preferencesInitializedAddress.GetAddress() ?? 0, Name = nameof(this.preferencesInitializedAddress) },
new AddressState { Address = this.getEnumPreferenceAddress.GetAddress() ?? 0, Name = nameof(this.getEnumPreferenceAddress) },
new AddressState { Address = this.setEnumPreferenceAddress.GetAddress() ?? 0, Name = nameof(this.setEnumPreferenceAddress) },
new AddressState { Address = this.getNumberPreferenceAddress.GetAddress() ?? 0, Name = nameof(this.getNumberPreferenceAddress) },
new AddressState { Address = this.setNumberPreferenceAddress.GetAddress() ?? 0, Name = nameof(this.setNumberPreferenceAddress) },
new AddressState { Address = this.getFlagPreferenceAddress.GetAddress() ?? 0, Name = nameof(this.getFlagPreferenceAddress) },
new AddressState { Address = this.setFlagPreferenceAddress.GetAddress() ?? 0, Name = nameof(this.setFlagPreferenceAddress) },
new AddressState { Address = this.getStringPreferenceAddress.GetAddress() ?? 0, Name = nameof(this.getStringPreferenceAddress) },
new AddressState { Address = this.setStringPreferenceAddress.GetAddress() ?? 0, Name = nameof(this.setStringPreferenceAddress) },
];
}
public unsafe bool PreferencesInitialized()
{
var addr = this.preferencesInitializedAddress.GetAddress();
return addr is not null && addr != 0 && *(byte*)addr != 0;
}
public uint? GetEnumPreference(EnumPreference pref)
{
if (!this.PreferencesInitialized())
{
return null;
}
var funcAddr = this.getEnumPreferenceAddress.GetAddress();
if (funcAddr is null or 0)
{
return null;
}
var func = Marshal.GetDelegateForFunctionPointer<GetEnumPreferenceDelegate>((nint)funcAddr);
return func((uint)pref);
}
public bool SetEnumPreference(EnumPreference pref, uint value)
{
if (!this.PreferencesInitialized())
{
return false;
}
var funcAddr = this.setEnumPreferenceAddress.GetAddress();
if (funcAddr is null or 0)
{
return false;
}
var func = Marshal.GetDelegateForFunctionPointer<SetEnumPreferenceDelegate>((nint)funcAddr);
func((uint)pref, value);
return true;
}
public uint? GetNumberPreference(NumberPreference pref)
{
if (!this.PreferencesInitialized())
{
return null;
}
var funcAddr = this.getNumberPreferenceAddress.GetAddress();
if (funcAddr is null or 0)
{
return null;
}
var func = Marshal.GetDelegateForFunctionPointer<GetNumberPreferenceDelegate>((nint)funcAddr);
return func((uint)pref);
}
public bool SetNumberPreference(NumberPreference pref, uint value)
{
if (!this.PreferencesInitialized())
{
return false;
}
var funcAddr = this.setNumberPreferenceAddress.GetAddress();
if (funcAddr is null or 0)
{
return false;
}
var func = Marshal.GetDelegateForFunctionPointer<SetNumberPreferenceDelegate>((nint)funcAddr);
func((uint)pref, value);
return true;
}
public bool? GetFlagPreference(FlagPreference pref)
{
if (!this.PreferencesInitialized())
{
return null;
}
var funcAddr = this.getFlagPreferenceAddress.GetAddress();
if (funcAddr is null or 0)
{
return null;
}
var func = Marshal.GetDelegateForFunctionPointer<GetFlagPreferenceDelegate>((nint)funcAddr);
return func((uint)pref);
}
public bool SetFlagPreference(FlagPreference pref, bool value)
{
if (!this.PreferencesInitialized())
{
return false;
}
var funcAddr = this.setFlagPreferenceAddress.GetAddress();
if (funcAddr is null or 0)
{
return false;
}
var func = Marshal.GetDelegateForFunctionPointer<SetFlagPreferenceDelegate>((nint)funcAddr);
func((uint)pref, value);
return true;
}
public unsafe string? GetStringPreference(StringPreference pref)
{
if (!this.PreferencesInitialized())
{
return null;
}
var funcAddr = this.getStringPreferenceAddress.GetAddress();
if (funcAddr is null or 0)
{
return null;
}
var func = Marshal.GetDelegateForFunctionPointer<GetStringPreferenceDelegate>((nint)funcAddr);
var ptr = func((uint)pref);
if (ptr == 0)
{
return null;
}
return Marshal.PtrToStringUni(ptr);
}
public bool SetStringPreference(StringPreference pref, string value)
{
if (!this.PreferencesInitialized())
{
return false;
}
var funcAddr = this.setStringPreferenceAddress.GetAddress();
if (funcAddr is null or 0)
{
return false;
}
var strPtr = Marshal.StringToHGlobalUni(value);
try
{
var func = Marshal.GetDelegateForFunctionPointer<SetStringPreferenceDelegate>((nint)funcAddr);
func((uint)pref, strPtr);
return true;
}
finally
{
Marshal.FreeHGlobal(strPtr);
}
}
private nuint GetPreferencesInitializedAddress()
{
var scopedLogger = this.logger.CreateScopedLogger();
// GWCA: FindAssertion("\\Code\\Gw\\Pref\\PrApi.cpp", "location < arrsize(s_flushDelay)", 0, -0x12)
var address = this.memoryScanningService.FindAssertion(
@"\Code\Gw\Pref\PrApi.cpp",
"location < arrsize(s_flushDelay)",
0,
-0x12);
if (address is 0)
{
scopedLogger.LogError("Failed to find preferences initialized assertion");
return 0;
}
// Dereference to get actual address
var ptr = (nuint)Marshal.ReadIntPtr((nint)address);
if (ptr is 0)
{
scopedLogger.LogError("Failed to dereference preferences initialized address");
return 0;
}
scopedLogger.LogInformation("Preferences initialized address: 0x{address:X8}", ptr);
return ptr;
}
private nuint GetGetEnumPreferenceAddress()
{
var scopedLogger = this.logger.CreateScopedLogger();
var stringUsage = this.memoryScanningService.FindUseOfString("pref < PREF_ENUMS");
if (stringUsage is 0)
{
scopedLogger.LogError("Failed to find 'pref < PREF_ENUMS' string usage");
return 0;
}
var address = this.memoryScanningService.ToFunctionStart(stringUsage);
if (address is 0)
{
scopedLogger.LogError("Failed to resolve GetEnumPreference function start");
return 0;
}
scopedLogger.LogInformation("GetEnumPreference address: 0x{address:X8}", address);
return address;
}
private nuint GetSetEnumPreferenceAddress()
{
var scopedLogger = this.logger.CreateScopedLogger();
var stringUsage = this.memoryScanningService.FindUseOfString("tableCount");
if (stringUsage is 0)
{
scopedLogger.LogError("Failed to find 'tableCount' string usage");
return 0;
}
var address = this.memoryScanningService.ToFunctionStart(stringUsage);
if (address is 0)
{
scopedLogger.LogError("Failed to resolve SetEnumPreference function start");
return 0;
}
scopedLogger.LogInformation("SetEnumPreference address: 0x{address:X8}", address);
return address;
}
private nuint GetGetNumberPreferenceAddress()
{
var scopedLogger = this.logger.CreateScopedLogger();
var stringUsage = this.memoryScanningService.FindUseOfString("pref < PREF_VALUES");
if (stringUsage is 0)
{
scopedLogger.LogError("Failed to find 'pref < PREF_VALUES' string usage");
return 0;
}
var address = this.memoryScanningService.ToFunctionStart(stringUsage);
if (address is 0)
{
scopedLogger.LogError("Failed to resolve GetNumberPreference function start");
return 0;
}
scopedLogger.LogInformation("GetNumberPreference address: 0x{address:X8}", address);
return address;
}
private nuint GetSetNumberPreferenceAddress()
{
var scopedLogger = this.logger.CreateScopedLogger();
var stringUsage = this.memoryScanningService.FindNthUseOfString("pref < PREF_VALUES", 1);
if (stringUsage is 0)
{
scopedLogger.LogError("Failed to find second 'pref < PREF_VALUES' string usage");
return 0;
}
var address = this.memoryScanningService.ToFunctionStart(stringUsage);
if (address is 0)
{
scopedLogger.LogError("Failed to resolve SetNumberPreference function start");
return 0;
}
scopedLogger.LogInformation("SetNumberPreference address: 0x{address:X8}", address);
return address;
}
private nuint GetGetFlagPreferenceAddress()
{
var scopedLogger = this.logger.CreateScopedLogger();
var stringUsage = this.memoryScanningService.FindUseOfString("pref < PREF_FLAGS");
if (stringUsage is 0)
{
scopedLogger.LogError("Failed to find 'pref < PREF_FLAGS' string usage");
return 0;
}
var address = this.memoryScanningService.ToFunctionStart(stringUsage);
if (address is 0)
{
scopedLogger.LogError("Failed to resolve GetFlagPreference function start");
return 0;
}
scopedLogger.LogInformation("GetFlagPreference address: 0x{address:X8}", address);
return address;
}
private nuint GetSetFlagPreferenceAddress()
{
var scopedLogger = this.logger.CreateScopedLogger();
var stringUsage = this.memoryScanningService.FindNthUseOfString("pref < PREF_FLAGS", 1);
if (stringUsage is 0)
{
scopedLogger.LogError("Failed to find second 'pref < PREF_FLAGS' string usage");
return 0;
}
var address = this.memoryScanningService.ToFunctionStart(stringUsage);
if (address is 0)
{
scopedLogger.LogError("Failed to resolve SetFlagPreference function start");
return 0;
}
scopedLogger.LogInformation("SetFlagPreference address: 0x{address:X8}", address);
return address;
}
private nuint GetGetStringPreferenceAddress()
{
var scopedLogger = this.logger.CreateScopedLogger();
var stringUsage = this.memoryScanningService.FindUseOfString("pref < PREF_STRINGS");
if (stringUsage is 0)
{
scopedLogger.LogError("Failed to find 'pref < PREF_STRINGS' string usage");
return 0;
}
var address = this.memoryScanningService.ToFunctionStart(stringUsage);
if (address is 0)
{
scopedLogger.LogError("Failed to resolve GetStringPreference function start");
return 0;
}
scopedLogger.LogInformation("GetStringPreference address: 0x{address:X8}", address);
return address;
}
private nuint GetSetStringPreferenceAddress()
{
var scopedLogger = this.logger.CreateScopedLogger();
var stringUsage = this.memoryScanningService.FindNthUseOfString("pref < PREF_STRINGS", 1);
if (stringUsage is 0)
{
scopedLogger.LogError("Failed to find second 'pref < PREF_STRINGS' string usage");
return 0;
}
var address = this.memoryScanningService.ToFunctionStart(stringUsage);
if (address is 0)
{
scopedLogger.LogError("Failed to resolve SetStringPreference function start");
return 0;
}
scopedLogger.LogInformation("SetStringPreference address: 0x{address:X8}", address);
return address;
}
}
+123 -69
View File
@@ -13,37 +13,35 @@ public sealed class UIContextService
{
private const string GetChildFrameIdFile = "CtlView.cpp";
private const string GetChildFrameIdAssertion = "pageId";
private const int GetChildFrameIdOffset = 0x19;
private const string SetFrameFlagFile = "FrApi.cpp";
private const string SetFrameFlagAssertion = "frameId";
private const int SetFrameVisibleOffset = 0x4f9;
private const int SetFrameDisabledOffset = 0x4e3;
private static readonly byte[] SendFrameUiMessageByIdSeq = [0x83, 0xFB, 0x47, 0x73, 0x14];
private const string SendFrameUiMessageByIdMask = "xxxxx";
private const int SendFrameUiMessageByIdOffset = -0x34;
private const int GetChildFrameIdOffset = 0x12;
private static readonly byte[] GetChildFrameIdPattern = [0xE8];
private const string GetChildFrameIdMask = "x";
private static readonly byte[] SendFrameUiMessageSeq = [
0x83, 0xC1, 0xDC, // add ecx, -24
0xE8, 0x00, 0x00, 0x00, 0x00, // call
0x8B, 0x4D, 0xFC // mov ecx, [ebp-04]
];
private const string SendFrameUiMessageMask = "xxxx????xxx";
private const int SendFrameUiMessageOffset = 0x3;
private static readonly byte[] SendUIMessageSeq = [0xB9, 0x00, 0x00, 0x00, 0x00, 0xE8, 0x00, 0x00, 0x00, 0x00, 0x5D, 0xC3, 0x89, 0x45, 0x08];
private const string SendUIMessageMask = "x????x????xxxxx";
private const string CreateHashFromStringMask = "xxxxxxx";
private const int CreateHashFromStringOffset = 0x7;
private static readonly byte[] CreateHashFromStringSeq = [0x85, 0xC0, 0x74, 0x0D, 0x6A, 0xFF, 0x50];
private const string FrameArrayFile = "\\Code\\Engine\\Frame\\FrMsg.cpp";
private const string FrameArrayAssertion = "frame";
private static readonly byte[] GetRootFrameSeq = [0x05, 0xE0, 0xFE, 0xFF, 0xFF, 0xC3];
private const string GetRootFrameMask = "xxxxxx";
private const int GetRootFrameOffset = -0x3C;
[SuppressUnmanagedCodeSecurity]
[UnmanagedFunctionPointer(CallingConvention.Cdecl)]
private unsafe delegate uint CreateHashFromStringFunc(char* value, int seed);
private static readonly byte[] GetRootFrameSeq = [
0xA1, 0x00, 0x00, 0x00, 0x00, // mov eax,[address]
0x05, 0xD8, 0xFE, 0xFF, 0xFF, // add eax,FFFFFED8
0xC3 // ret
];
private const string GetRootFrameMask = "x????xxxxxx";
[SuppressUnmanagedCodeSecurity]
[UnmanagedFunctionPointer(CallingConvention.Cdecl)]
private delegate void SendUIMessageFunc(UIMessage uiMessage, nuint wParam, nuint lParam);
[SuppressUnmanagedCodeSecurity]
[UnmanagedFunctionPointer(CallingConvention.Cdecl)]
private delegate void SetFrameFlagFunc(uint frameId, uint flag);
[SuppressUnmanagedCodeSecurity]
[UnmanagedFunctionPointer(CallingConvention.Cdecl)]
private delegate uint GetChildFrameIdFunc(uint parentFrameId, uint childOffset);
@@ -52,27 +50,35 @@ public sealed class UIContextService
[UnmanagedFunctionPointer(CallingConvention.Cdecl)]
private unsafe delegate Frame* GetRootFrameFunc();
private readonly SemaphoreSlim semaphoreSlim = new(1);
[SuppressUnmanagedCodeSecurity]
[UnmanagedFunctionPointer(CallingConvention.Cdecl)]
private unsafe delegate void ValidateAsyncDecodeStr(char* s, DecodeStrCallback callback, void* wParam);
public unsafe delegate void DecodeStrCallback(void* param, char* s);
private readonly SemaphoreSlim sendUiMessageSemaphore = new(1);
private readonly SemaphoreSlim decodeStringSemaphore = new(1);
private readonly MemoryScanningService memoryScanningService;
private readonly GWDelegateCache<CreateHashFromStringFunc> createHashFromString;
private readonly HashingService hashingService;
private readonly GWDelegateCache<GetChildFrameIdFunc> getChildFrameId;
private readonly GWAddressCache frameArrayAddressCache;
private readonly GWHook<SendUIMessageFunc> sendUiMessageHook;
private readonly GWDelegateCache<GetRootFrameFunc> getRootFrame;
private readonly GWDelegateCache<ValidateAsyncDecodeStr> validateAsyncDecodeStr;
// This fastcall equivalent is <GuildWarsArray<FrameInteractionCallback>*, void*, UIMessage, void*, void*>
private readonly GWFastCall<nint, nint, UIMessage, nint, nint, GWFastCall.Void> sendFrameUIMessage;
private readonly ILogger<UIContextService> logger;
private CancellationTokenSource? cts = default;
private CancellationTokenSource? cts;
public UIContextService(
HashingService hashingService,
MemoryScanningService memoryScanningService,
ILogger<UIContextService> logger)
{
this.logger = logger.ThrowIfNull();
this.memoryScanningService = memoryScanningService.ThrowIfNull();
this.createHashFromString = new GWDelegateCache<CreateHashFromStringFunc>(new GWAddressCache(() => this.memoryScanningService.FunctionFromNearCall(
this.memoryScanningService.FindAddress(CreateHashFromStringSeq, CreateHashFromStringMask, CreateHashFromStringOffset))));
this.hashingService = hashingService.ThrowIfNull();
this.frameArrayAddressCache = new GWAddressCache(() => this.memoryScanningService.FindAssertion(FrameArrayFile, FrameArrayAssertion, 0, -0x14));
this.sendUiMessageHook = new GWHook<SendUIMessageFunc>(
new GWAddressCache(() => this.memoryScanningService.ToFunctionStart(
@@ -80,42 +86,50 @@ public sealed class UIContextService
this.OnSendUIMessage);
this.sendFrameUIMessage = new(
new GWAddressCache(() => this.memoryScanningService.FunctionFromNearCall(
this.memoryScanningService.FindAddress(SendFrameUiMessageByIdSeq, SendFrameUiMessageByIdMask, SendFrameUiMessageByIdOffset) + 0x67)));
this.memoryScanningService.FindAddress(SendFrameUiMessageSeq, SendFrameUiMessageMask, SendFrameUiMessageOffset))));
this.getChildFrameId = new GWDelegateCache<GetChildFrameIdFunc>(
new GWAddressCache(() => this.memoryScanningService.FunctionFromNearCall(
this.memoryScanningService.FindAssertion(GetChildFrameIdFile, GetChildFrameIdAssertion, 0, GetChildFrameIdOffset))));
new GWAddressCache(() =>
{
var address = this.memoryScanningService.FindAssertion(GetChildFrameIdFile, GetChildFrameIdAssertion, 0, GetChildFrameIdOffset);
return this.memoryScanningService.FunctionFromNearCall(
this.memoryScanningService.FindAddress(GetChildFrameIdPattern, GetChildFrameIdMask, 0, address, address + 0xFF));
}));
this.getRootFrame = new GWDelegateCache<GetRootFrameFunc>(
new GWAddressCache(() => this.memoryScanningService.FindAddress(GetRootFrameSeq, GetRootFrameMask, GetRootFrameOffset)));
new GWAddressCache(() => this.memoryScanningService.FindAddress(GetRootFrameSeq, GetRootFrameMask, 0)));
this.validateAsyncDecodeStr = new GWDelegateCache<ValidateAsyncDecodeStr>(
new GWAddressCache(() => this.memoryScanningService.ToFunctionStart(
this.memoryScanningService.FindUseOfString("(codedString[0] & ~WORD_BIT_MORE) >= WORD_VALUE_BASE"))));
}
public List<AddressState> GetAddressStates()
{
return
[
new()
{
Name = nameof(this.createHashFromString),
Address = this.createHashFromString.Cache.GetAddress() ?? 0U
},
new()
{
Name = nameof(this.frameArrayAddressCache),
Address = this.frameArrayAddressCache.GetAddress() ?? 0U
},
new()
{
Name = nameof(this.getChildFrameId),
Address = this.getChildFrameId.Cache.GetAddress() ?? 0U
},
new()
{
Name = nameof(this.sendFrameUIMessage),
Address = this.sendFrameUIMessage.Cache.GetAddress() ?? 0U
},
new()
{
Name = nameof(this.getChildFrameId),
Address = this.getChildFrameId.Cache.GetAddress() ?? 0U
},
new()
{
Name = nameof(this.getRootFrame),
Address = this.getRootFrame.Cache.GetAddress() ?? 0U
},
new()
{
Name = nameof(this.validateAsyncDecodeStr),
Address = this.validateAsyncDecodeStr.Cache.GetAddress() ?? 0U
}
];
}
@@ -150,6 +164,26 @@ public sealed class UIContextService
return Task.CompletedTask;
}
public unsafe WrappedPointer<T> GetFrameContext<T>(WrappedPointer<Frame> frame)
where T : unmanaged
{
if (frame.IsNull || frame.Pointer->FrameCallbacks.Size == 0)
{
return null;
}
for (uint i = 0; i < frame.Pointer->FrameCallbacks.Size; i++)
{
var callback = frame.Pointer->FrameCallbacks.Buffer[i];
if (callback.UiCtl_Context != null)
{
return new WrappedPointer<T>((T*)callback.UiCtl_Context);
}
}
return null;
}
public unsafe WrappedPointer<Frame> GetChildFrame(WrappedPointer<Frame> parent, uint childOffset)
{
var scopedLogger = this.logger.CreateScopedLogger();
@@ -194,7 +228,7 @@ public sealed class UIContextService
return true;
}
public unsafe bool SetFrameDisabled(WrappedPointer<Frame> frame, bool disabled)
public bool SetFrameDisabled(WrappedPointer<Frame> frame, bool disabled)
{
var scopedLogger = this.logger.CreateScopedLogger();
if (frame.IsNull)
@@ -207,7 +241,7 @@ public sealed class UIContextService
return true;
}
public unsafe bool SetFrameVisible(WrappedPointer<Frame> frame, bool visible)
public bool SetFrameVisible(WrappedPointer<Frame> frame, bool visible)
{
var scopedLogger = this.logger.CreateScopedLogger();
if (frame.IsNull)
@@ -227,10 +261,10 @@ public sealed class UIContextService
frame = this.GetButtonActionFrame();
}
this.semaphoreSlim.Wait();
this.sendUiMessageSemaphore.Wait();
var packet = new UIPackets.KeyAction((uint)action);
this.SendFrameUIMessage(frame, UIMessage.KeyDown, &packet);
this.semaphoreSlim.Release();
this.sendUiMessageSemaphore.Release();
return true;
}
@@ -241,10 +275,10 @@ public sealed class UIContextService
frame = this.GetButtonActionFrame();
}
this.semaphoreSlim.Wait();
this.sendUiMessageSemaphore.Wait();
var packet = new UIPackets.KeyAction((uint)action);
this.SendFrameUIMessage(frame, UIMessage.KeyUp, &packet);
this.semaphoreSlim.Release();
this.sendUiMessageSemaphore.Release();
return true;
}
@@ -268,14 +302,14 @@ public sealed class UIContextService
return null;
}
var hash = this.CreateHashFromString(label, -1);
var hash = this.CreateHashFromString(label);
if (hash is 0)
{
scopedLogger.LogError("Failed to get hash for label {label}", label);
return null;
}
for(var i = 0; i < frameArray.Pointer->Size; i++)
for (var i = 0; i < frameArray.Pointer->Size; i++)
{
var frame = frameArray.Pointer->Buffer[i];
if (frame.Pointer is null ||
@@ -315,26 +349,47 @@ public sealed class UIContextService
return frame;
}
public unsafe uint CreateHashFromString(string value, int seed)
public uint CreateHashFromString(string value)
{
var scopedLogger = this.logger.CreateScopedLogger();
if (this.createHashFromString.GetDelegate() is not CreateHashFromStringFunc del)
{
scopedLogger.LogError("Failed to get CreateHashFromString delegate");
return 0;
}
fixed (char* valuePtr = value)
{
return del(valuePtr, seed);
}
return this.hashingService.Hash(value);
}
public unsafe void SendMessage(UIMessage message, nuint wParam, nuint lParam)
public void SendMessage(UIMessage message, nuint wParam, nuint lParam)
{
this.semaphoreSlim.Wait();
this.sendUiMessageSemaphore.Wait();
this.sendUiMessageHook.Continue(message, wParam, lParam);
this.semaphoreSlim.Release();
this.sendUiMessageSemaphore.Release();
}
/// <summary>
/// Decodes a string asynchronously using the game's internal decoding mechanism.
/// </summary>
/// <remarks>
/// Inside the DecodeStrCallback, the decoded string has to be cleaned up by calling Marshal.FreeHGlobal on the char* pointer.
/// </remarks>
public unsafe void ValidateDecodeString(char* encoded, DecodeStrCallback callback, nuint callbackParam)
{
var scopedLogger = this.logger.CreateScopedLogger();
this.decodeStringSemaphore.Wait();
try
{
var validateDecodeStrFnc = this.validateAsyncDecodeStr.GetDelegate();
if (validateDecodeStrFnc is null)
{
scopedLogger.LogError("{funcName} is null", nameof(this.validateAsyncDecodeStr));
return;
}
validateDecodeStrFnc(encoded, callback, (void*)callbackParam);
}
catch (Exception e)
{
scopedLogger.LogError(e, "Encountered exception while decoding string");
}
finally
{
this.decodeStringSemaphore.Release();
}
}
private async ValueTask InitializeHooks(CancellationToken cancellationToken)
@@ -357,25 +412,24 @@ public sealed class UIContextService
private static unsafe void SetFrameVisibleInternal(WrappedPointer<Frame> frame, bool visible)
{
uint* pState = &frame.Pointer->Field91; // direct field address
uint* pState = &frame.Pointer->FrameState; // direct field address
if (visible)
{
*pState &= ~0x200u; // clear hidden
*pState &= ~0x200u; // clear "hidden"
}
else
{
*pState |= 0x200u; // set hidden
*pState |= 0x200u; // set "hidden"
}
}
private static unsafe void SetFrameDisabledInternal(WrappedPointer<Frame> frame, bool disabled)
{
uint* pState = &frame.Pointer->Field91; // same word (offset 0x184)
uint* pState = &frame.Pointer->FrameState; // same word (offset 0x184)
if (disabled)
{
*pState |= 0x10u; // set disabled
*pState |= 0x10u; // set "disabled"
}
else
{
+122
View File
@@ -0,0 +1,122 @@
using Daybreak.API.Interop.GuildWars;
using Daybreak.API.Services.Interop;
using Daybreak.Shared.Models.Api;
using Daybreak.Shared.Models.Guildwars;
using System.Extensions.Core;
using System.Text;
namespace Daybreak.API.Services;
public sealed class InventoryService(
GameContextService gameContextService,
GameThreadService gameThreadService,
UIService uIService,
ILogger<InventoryService> logger)
{
private readonly GameContextService gameContextService = gameContextService;
private readonly GameThreadService gameThreadService = gameThreadService;
private readonly UIService uIService = uIService;
private readonly ILogger<InventoryService> logger = logger;
public async Task<InventoryInformation?> GetInventoryInformation(CancellationToken cancellationToken)
{
var scopedLogger = this.logger.CreateScopedLogger();
var itemTuples = await this.gameThreadService.QueueOnGameThread(() =>
{
unsafe
{
var gameContext = this.gameContextService.GetGameContext();
if (gameContext.IsNull)
{
scopedLogger.LogError("Game context is null");
return default;
}
var inventory = gameContext.Pointer->ItemContext->Inventory;
if (inventory.IsNull)
{
scopedLogger.LogError("Inventory is null");
return default;
}
var itemTuples = new List<(BagType Type,List<(uint ModelId, string EncodedCompleteName, string EncodedSingleName, string EncodedName, bool Inscribable, int Quantity, uint[] Modifiers, ItemType ItemType)>)>();
foreach (var bag in inventory.Pointer->Bags)
{
if (bag is null)
{
continue;
}
var retBag = new List<(uint ModelId, string EncodedCompleteName, string EncodedSingleName, string EncodedName, bool Inscribable, int Quantity, uint[] Modifiers, ItemType ItemType)>();
itemTuples.Add((bag->Type, retBag));
if (bag->ItemsCount is 0)
{
continue;
}
foreach(var item in bag->Items)
{
if (item.IsNull)
{
continue;
}
var singleItemName = new string(item.Pointer->SingleItemName);
var nameEncoded = new string(item.Pointer->NameEncoded);
var completeNameEncoded = new string(item.Pointer->CompleteNameEncoded);
var modifiers = new uint[item.Pointer->ModifiersCount];
for(var j = 0; j < item.Pointer->ModifiersCount; j++)
{
modifiers[j] = item.Pointer->Modifiers[j].Mod;
}
retBag.Add((item.Pointer->ModelId, completeNameEncoded, singleItemName, nameEncoded, item.Pointer->Inscribable, item.Pointer->Quantity, modifiers, item.Pointer->Type));
}
}
return itemTuples;
}
}, cancellationToken);
if (itemTuples is null)
{
return default;
}
var decodedItemTuples = await Task.WhenAll(itemTuples.Select(async tuple =>
{
var decodedItems = await Task.WhenAll(tuple.Item2.Select(async item =>
{
var decodedName = await this.uIService.DecodeString(item.EncodedName, Language.English, cancellationToken);
var decodedCompleteName = await this.uIService.DecodeString(item.EncodedCompleteName, Language.English, cancellationToken);
var decodedSingleName = await this.uIService.DecodeString(item.EncodedSingleName, Language.English, cancellationToken);
return (item.ModelId, item.EncodedName, DecodedName: decodedName, item.EncodedSingleName, DecodedSingleName: decodedSingleName, item.EncodedCompleteName, DecodedCompleteName: decodedCompleteName, item.Inscribable, item.Quantity, item.Modifiers, item.ItemType);
}));
return (tuple.Type, Items: decodedItems.ToList());
}));
return new InventoryInformation(
Bags: [.. decodedItemTuples.Select(tuple =>
new BagEntry(
BagType: tuple.Type.ToString(),
Items: [.. tuple.Items.Select(item =>
new ItemEntry(
ItemModelId: item.ModelId,
EncodedName: ToBase64(item.EncodedName),
DecodedName: item.DecodedName ?? string.Empty,
EncodedSingleName: ToBase64(item.EncodedSingleName),
DecodedSingleName: item.DecodedSingleName ?? string.Empty,
EncodedCompleteName: ToBase64(item.EncodedCompleteName),
DecodedCompleteName: item.DecodedCompleteName ?? string.Empty,
Inscribable: item.Inscribable,
Quantity: item.Quantity,
Modifiers: item.Modifiers,
Properties: [.. ItemProperty.ParseItemModifiers([.. item.Modifiers.Select(m => (Shared.Models.Guildwars.ItemModifier)m)])],
ItemType: item.ItemType.ToString()))]))]);
}
private static string ToBase64(string encoded)
{
return Convert.ToBase64String(Encoding.Unicode.GetBytes(encoded));
}
}
+78 -22
View File
@@ -20,6 +20,7 @@ namespace Daybreak.API.Services;
public sealed class MainPlayerService : IDisposable
{
private readonly ChatService chatService;
private readonly InstanceContextService instanceContextService;
private readonly IBuildTemplateManager buildTemplateManager;
private readonly SkillbarContextService skillbarContextService;
@@ -35,10 +36,11 @@ public sealed class MainPlayerService : IDisposable
private TimeSpan minUpdateFrequency = TimeSpan.MaxValue;
private MainPlayerState? mainPlayerState;
private DateTimeOffset lastUpdateTime = DateTimeOffset.MinValue;
private DateTimeOffset lastFrequencyUpdate = DateTimeOffset.MinValue;
private DateTime lastUpdateTime = DateTime.MinValue;
private DateTime lastFrequencyUpdate = DateTime.MinValue;
public MainPlayerService(
ChatService chatService,
InstanceContextService instanceContextService,
IBuildTemplateManager buildTemplateManager,
SkillbarContextService skillbarContextService,
@@ -47,6 +49,7 @@ public sealed class MainPlayerService : IDisposable
GameThreadService gameThreadService,
ILogger<MainPlayerService> logger)
{
this.chatService = chatService.ThrowIfNull();
this.instanceContextService = instanceContextService.ThrowIfNull();
this.buildTemplateManager = buildTemplateManager.ThrowIfNull();
this.skillbarContextService = skillbarContextService.ThrowIfNull();
@@ -62,17 +65,17 @@ public sealed class MainPlayerService : IDisposable
this.callbackRegistration?.Dispose();
}
public Task<bool> SetCurrentBuild(string buildCode, CancellationToken cancellationToken)
public async Task<bool> SetCurrentBuild(string buildCode, CancellationToken cancellationToken)
{
var scopedLogger = this.logger.CreateScopedLogger();
if (!this.buildTemplateManager.TryDecodeTemplate(buildCode, out var build) ||
build is not SingleBuildEntry singleBuild)
{
scopedLogger.LogError("Failed to decode build template from code {buildCode}", buildCode);
return Task.FromResult(false);
return false;
}
return this.gameThreadService.QueueOnGameThread(() =>
var result = await this.gameThreadService.QueueOnGameThread(() =>
{
unsafe
{
@@ -137,7 +140,7 @@ public sealed class MainPlayerService : IDisposable
var attributeIds = new Array12Uint();
var attributeValues = new Array12Uint();
var skills = new Array8Uint();
for(var i = 0; i < singleBuild.Attributes.Count && i < 12; i++)
for (var i = 0; i < singleBuild.Attributes.Count && i < 12; i++)
{
if (singleBuild.Attributes[i].Attribute is null)
{
@@ -148,7 +151,7 @@ public sealed class MainPlayerService : IDisposable
attributeValues[i] = (uint)singleBuild.Attributes[i].Points;
}
for(var i =0; i < singleBuild.Skills.Count && i < 8; i++)
for (var i = 0; i < singleBuild.Skills.Count && i < 8; i++)
{
skills[i] = (uint)singleBuild.Skills[i].Id;
}
@@ -165,6 +168,17 @@ public sealed class MainPlayerService : IDisposable
return true;
}
}, cancellationToken);
if (result)
{
await this.chatService.AddMessageAsync("Build applied successfully.", "Daybreak.API", Channel.Moderator, cancellationToken);
}
else
{
await this.chatService.AddMessageAsync("Failed to apply build.", "Daybreak.API", Channel.Moderator, cancellationToken);
}
return result;
}
public Task<BuildEntry?> GetCurrentBuild(CancellationToken cancellationToken)
@@ -247,7 +261,7 @@ public sealed class MainPlayerService : IDisposable
return new QuestLogInformation(
gameContext.Pointer->WorldContext->ActiveQuestId,
gameContext.Pointer->WorldContext->QuestLog.AsValueEnumerable().Select(q => new QuestInformation(q.QuestId, q.MapFrom, q.MapTo)).ToList());
gameContext.Pointer->WorldContext->QuestLog.AsValueEnumerable().Select(q => new QuestInformation(q.QuestId, q.MapFrom)).ToList());
}
}, cancellationToken);
}
@@ -378,23 +392,19 @@ public sealed class MainPlayerService : IDisposable
return default;
}
if (!gameContext.TryGetPlayerId(out var playerId))
var playerIndex = gameContext.Pointer->CharContext->PlayerNumber;
var players = gameContext.Pointer->WorldContext->Players;
if (playerIndex < 0 || playerIndex >= players.Size)
{
scopedLogger.LogError("Failed to get player id");
return default;
}
var player = gameContext.Pointer->WorldContext->Players.AsValueEnumerable().FirstOrDefault(p => p.AgentId == playerId);
if (player.AgentId != playerId)
{
scopedLogger.LogError("Failed to find player with id {playerId}", playerId);
scopedLogger.LogError("Failed to get player id. Got {playerNumber}", playerIndex);
return default;
}
var player = gameContext.Pointer->WorldContext->Players[(int)playerIndex];
var currentTier = player.ActiveTitleTier;
TitleContext? currentTitle = default;
int? id = -1;
for(var i = 0; i < gameContext.Pointer->WorldContext->Titles.Size; i++)
for (var i = 0; i < gameContext.Pointer->WorldContext->Titles.Size; i++)
{
var title = gameContext.Pointer->WorldContext->Titles.Skip(i).FirstOrDefault();
if (title.CurrentTitleTierIndex == currentTier)
@@ -428,12 +438,58 @@ public sealed class MainPlayerService : IDisposable
}, cancellationToken);
}
public Task<MainPlayerBuildContext?> GetMainPlayerBuildContext(CancellationToken cancellationToken)
{
var scopedLogger = this.logger.CreateScopedLogger();
return this.gameThreadService.QueueOnGameThread(() =>
{
unsafe
{
if (this.instanceContextService.GetInstanceType() is InstanceType.Loading)
{
scopedLogger.LogError("Not loaded");
return default;
}
var gameContext = this.gameContextService.GetGameContext();
if (gameContext.IsNull ||
gameContext.Pointer->AccountContext is null ||
gameContext.Pointer->WorldContext is null)
{
this.logger.LogError("Game context is not initialized");
return default;
}
var playerAgentId = this.agentContextService.GetPlayerAgentId();
if (playerAgentId is 0x0)
{
scopedLogger.LogError("Failed to get player agent id");
return default;
}
var agentProfession = gameContext.Pointer->WorldContext->Professions.AsValueEnumerable().FirstOrDefault(p => p.AgentId == playerAgentId);
if (agentProfession.AgentId != playerAgentId)
{
scopedLogger.LogError("Failed to find agent profession for player agent id {agentId}", playerAgentId);
return default;
}
return new MainPlayerBuildContext(
PrimaryProfessionId: (uint)agentProfession.CurrentPrimary,
UnlockedProfessions: agentProfession.UnlockedProfessionsFlags,
UnlockedAccountSkills: gameContext.Pointer->AccountContext->UnlockedAccountSkills.AsValueEnumerable().ToArray(),
UnlockedCharacterSkills: gameContext.Pointer->WorldContext->UnlockedCharacterSkills.AsValueEnumerable().ToArray());
}
}, cancellationToken);
}
public CallbackRegistration RegisterMainStateConsumer(TimeSpan frequency, Action<ReadOnlySpan<byte>> onUpdate)
{
ArgumentOutOfRangeException.ThrowIfLessThanOrEqual(frequency, TimeSpan.Zero);
var id = Guid.NewGuid();
var now = DateTimeOffset.UtcNow;
var now = DateTime.UtcNow;
var entry = new ByteConsumerEntry(id, frequency, onUpdate);
this.consumers[id] = entry;
@@ -449,7 +505,7 @@ public sealed class MainPlayerService : IDisposable
private unsafe void OnGameThreadProc()
{
var scopedLogger = this.logger.CreateScopedLogger();
var now = DateTimeOffset.UtcNow;
var now = DateTime.UtcNow;
/*
* Periodically adjust the frequency of updates based on connected clients.
@@ -567,13 +623,13 @@ public sealed class MainPlayerService : IDisposable
{
this.minUpdateFrequency = TimeSpan.MaxValue;
scopedLogger.LogDebug("No consumers registered, disabling updates");
this.lastFrequencyUpdate = DateTimeOffset.UtcNow;
this.lastFrequencyUpdate = DateTime.UtcNow;
return;
}
var minValue = this.consumers.Values.Min(c => c.Frequency);
scopedLogger.LogDebug("Adjusted update frequency to {frequency}", minValue);
this.lastFrequencyUpdate = DateTimeOffset.UtcNow;
this.lastFrequencyUpdate = DateTime.UtcNow;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
+13 -6
View File
@@ -9,13 +9,13 @@ using Daybreak.Shared.Services.BuildTemplates;
using System.Core.Extensions;
using System.Extensions;
using System.Extensions.Core;
using System.Windows.Navigation;
using ZLinq;
using InstanceType = Daybreak.API.Interop.GuildWars.InstanceType;
namespace Daybreak.API.Services;
public sealed class PartyService(
ChatService chatService,
IBuildTemplateManager buildTemplateManager,
UIService uiService,
UIContextService uIContextService,
@@ -28,6 +28,7 @@ public sealed class PartyService(
{
private static readonly TimeSpan HeroSpawnDelay = TimeSpan.FromSeconds(1);
private readonly ChatService chatService = chatService.ThrowIfNull();
private readonly IBuildTemplateManager buildTemplateManager = buildTemplateManager.ThrowIfNull();
private readonly UIService uiService = uiService.ThrowIfNull();
private readonly UIContextService uIContextService = uIContextService.ThrowIfNull();
@@ -45,18 +46,21 @@ public sealed class PartyService(
if (!await this.IsInValidOutpost(cancellationToken))
{
scopedLogger.LogError("Could not set party loadout. Not in a valid outpost");
await this.chatService.AddMessageAsync("Cannot set party loadout. Not in a valid outpost.", "Daybreak.API", Channel.Moderator, cancellationToken);
return false;
}
if (!await this.LeaveParty(cancellationToken))
if (!await this.KickAllHeroes(cancellationToken))
{
scopedLogger.LogError("Could not set party loadout. Could not leave party");
await this.chatService.AddMessageAsync("Cannot set party loadout. Could not leave party.", "Daybreak.API", Channel.Moderator, cancellationToken);
return false;
}
if (!await this.gameThreadService.QueueOnGameThread(() => this.SpawnHeroes(partyLoadout), cancellationToken))
{
scopedLogger.LogError("Could not set party loadout. Could not spawn heroes");
await this.chatService.AddMessageAsync("Cannot set party loadout. Could not spawn heroes.", "Daybreak.API", Channel.Moderator, cancellationToken);
return false;
}
@@ -65,18 +69,21 @@ public sealed class PartyService(
if (!await this.gameThreadService.QueueOnGameThread(() => this.ApplyBuilds(partyLoadout), cancellationToken))
{
scopedLogger.LogError("Could not set party loadout. Could not apply builds");
await this.chatService.AddMessageAsync("Cannot set party loadout. Could not apply builds.", "Daybreak.API", Channel.Moderator, cancellationToken);
return false;
}
var heroBehaviorSetup = await this.gameThreadService.QueueOnGameThread(() => this.GetHeroBehaviorSetup(partyLoadout), cancellationToken);
foreach(var heroBehaviorEntry in heroBehaviorSetup ?? [])
foreach (var heroBehaviorEntry in heroBehaviorSetup ?? [])
{
if (!await this.SetHeroBehavior(heroBehaviorEntry.AgentId, heroBehaviorEntry.Behavior, cancellationToken))
{
scopedLogger.LogWarning("Could not set hero behavior for agent {agentId} to {behavior}", heroBehaviorEntry.AgentId, heroBehaviorEntry.Behavior);
await this.chatService.AddMessageAsync($"Cannot set party loadout. Could not set behavior {heroBehaviorEntry.Behavior} for agent {heroBehaviorEntry.AgentId}.", "Daybreak.API", Channel.Moderator, cancellationToken);
}
}
await this.chatService.AddMessageAsync("Party loadout set.", "Daybreak.API", Channel.Moderator, cancellationToken);
return true;
}
@@ -144,7 +151,7 @@ public sealed class PartyService(
}, cancellationToken);
}
public async Task<bool> LeaveParty(CancellationToken cancellationToken)
public async Task<bool> KickAllHeroes(CancellationToken cancellationToken)
{
var partySize = await this.GetPartySize(cancellationToken);
if (partySize is 1 or 0)
@@ -152,7 +159,7 @@ public sealed class PartyService(
return true;
}
return await this.gameThreadService.QueueOnGameThread(this.partyContextService.LeaveParty, cancellationToken);
return await this.gameThreadService.QueueOnGameThread(this.partyContextService.KickAllHeroes, cancellationToken);
}
public async Task<uint> GetPartySize(CancellationToken cancellationToken)
@@ -234,7 +241,7 @@ public sealed class PartyService(
{
var scopedLogger = this.logger.CreateScopedLogger();
scopedLogger.LogDebug("Spawning {heroCount} heroes for party loadout", partyLoadout.Entries.AsValueEnumerable().Count(c => c.HeroId != 0));
foreach (var entry in partyLoadout.Entries)
foreach (var entry in partyLoadout.Entries.AsValueEnumerable().OrderBy(e => e.Build.Primary))
{
if (entry.HeroId != 0 &&
Hero.TryParse(entry.HeroId, out var hero))
+91 -1
View File
@@ -1,16 +1,21 @@
using Daybreak.API.Interop;
using Daybreak.API.Interop.GuildWars;
using Daybreak.API.Models;
using Daybreak.API.Services.Interop;
using System.Extensions.Core;
using System.Runtime.InteropServices;
using static Daybreak.API.Services.Interop.UIContextService;
namespace Daybreak.API.Services;
public sealed class UIService(
GameThreadService gameThreadService,
GameContextService gameContextService,
UIContextService uIContextService,
ILogger<UIService> logger)
{
private readonly GameThreadService gameThreadService = gameThreadService;
private readonly GameContextService gameContextService = gameContextService;
private readonly UIContextService uIContextService = uIContextService;
private readonly ILogger<UIService> logger = logger;
@@ -81,11 +86,96 @@ public sealed class UIService(
{
await this.Keypress(uiAction, null, CancellationToken.None);
}
}, CancellationToken.None);
});
}
public async Task<string?> DecodeString(string encoded, Language language, CancellationToken cancellationToken)
{
var scopedLogger = this.logger.CreateScopedLogger();
var taskCompletionSource = new TaskCompletionSource<string?>();
var byteCount = (encoded.Length + 1) * sizeof(char);
var encodedPtr = Marshal.AllocHGlobal(byteCount);
unsafe
{
var encodedPtrC = (char*)encodedPtr;
fixed (char* src = encoded)
{
Buffer.MemoryCopy(src, encodedPtrC, byteCount, encoded.Length * sizeof(char));
}
encodedPtrC[encoded.Length] = '\0';
}
GCHandle callbackHandle = default;
DecodeStrCallback callback;
unsafe
{
callback = (_, decodePtr) =>
{
try
{
if (decodePtr is null)
{
taskCompletionSource.TrySetResult(null);
return;
}
var decoded = new string(decodePtr);
taskCompletionSource.TrySetResult(decoded);
}
catch (Exception ex)
{
taskCompletionSource.TrySetException(ex);
}
};
callbackHandle = GCHandle.Alloc(callback);
}
try
{
await this.gameThreadService.QueueOnGameThread(() =>
{
unsafe
{
var gameContext = this.gameContextService.GetGameContext();
if (gameContext.IsNull)
{
scopedLogger.LogError("Game context is null");
return;
}
var textParser = gameContext.Pointer->TextParserContext;
if (textParser is null)
{
scopedLogger.LogError("Text parser context is null");
return;
}
var prevLanguage = textParser->Language;
if (language is not Language.Unknown)
{
textParser->Language = language;
}
this.uIContextService.ValidateDecodeString((char*)encodedPtr, callback, nuint.Zero);
textParser->Language = prevLanguage;
}
}, cancellationToken);
return await taskCompletionSource.Task;
}
finally
{
Marshal.FreeHGlobal(encodedPtr);
if (callbackHandle.IsAllocated)
{
callbackHandle.Free();
}
}
}
private static UIAction GetUIActionFromFrameLabel(string frameLabel)
{
return frameLabel switch
@@ -1,6 +1,5 @@
using Daybreak.API.Services.Interop;
using Daybreak.Shared.Services.BuildTemplates;
using Daybreak.Shared.Services.MDns;
using System.Diagnostics.CodeAnalysis;
namespace Daybreak.API.Services;
@@ -9,9 +8,8 @@ public static class WebApplicationBuilderExtensions
{
public static WebApplicationBuilder WithDaybreakServices(this WebApplicationBuilder builder)
{
builder.Services.AddSingleton<HashingService>();
builder.Services.AddSingleton<IBuildTemplateManager, BuildTemplateManager>();
builder.Services.AddSingleton<IMDomainNameService, MDomainNameService>();
builder.Services.AddHostedService<ApiAdvertisingService>();
builder.Services.AddSingleton<MemoryScanningService>();
builder.Services.AddSingleton<ChatService>();
builder.Services.AddSingleton<MainPlayerService>();
@@ -19,6 +17,7 @@ public static class WebApplicationBuilderExtensions
builder.Services.AddSingleton<PartyService>();
builder.Services.AddSingleton<UIService>();
builder.Services.AddSingleton<LoginService>();
builder.Services.AddSingleton<InventoryService>();
builder.WithHookHostedService<GameThreadService>();
builder.WithHookHostedService<ChatHandlingService>();
builder.WithAddressService<SkillbarContextService>();
@@ -27,6 +26,7 @@ public static class WebApplicationBuilderExtensions
builder.WithAddressService<AgentContextService>();
builder.WithAddressService<PlatformContextService>();
builder.WithAddressService<PartyContextService>();
builder.WithAddressService<PreferencesService>();
builder.WithHookAddressHostedService<UIContextService>();
return builder;
}
@@ -0,0 +1,11 @@
using Daybreak.Shared.Attributes;
using Daybreak.Shared.Models.Themes;
namespace Daybreak.Attributes;
public sealed class DaybreakThemeOptionsAttribute : OptionValuesFactoryAttribute
{
public override List<object> ValuesFactory()
{
return [.. Theme.Themes.Cast<object>()];
}
}
@@ -1,4 +1,4 @@
using Daybreak.Attributes;
using Daybreak.Shared.Attributes;
using Daybreak.Shared.Configuration.Options;
namespace Daybreak.Configuration.Options;
@@ -1,15 +1,15 @@
using Daybreak.Attributes;
using Newtonsoft.Json;
using Daybreak.Shared.Attributes;
using System.Text.Json.Serialization;
namespace Daybreak.Configuration.Options;
[OptionsName(Name = "Synchronization")]
[OptionsIgnore]
[OptionsSynchronizationIgnore]
internal sealed class SynchronizationOptions
public sealed class SynchronizationOptions
{
[JsonProperty(nameof(ProtectedGraphAccessToken))]
[JsonPropertyName(nameof(ProtectedGraphAccessToken))]
public string? ProtectedGraphAccessToken { get; set; }
[JsonProperty(nameof(ProtectedGraphRefreshToken))]
[JsonPropertyName(nameof(ProtectedGraphRefreshToken))]
public string? ProtectedGraphRefreshToken { get; set; }
}
@@ -1,7 +1,6 @@
using Daybreak.Attributes;
using Daybreak.Shared.Attributes;
using Daybreak.Shared.Models;
using Newtonsoft.Json;
using System.Collections.Generic;
using System.Text.Json.Serialization;
namespace Daybreak.Configuration.Options;
@@ -9,6 +8,6 @@ namespace Daybreak.Configuration.Options;
[OptionsName(Name = "Credentials")]
internal sealed class CredentialManagerOptions
{
[JsonProperty(nameof(ProtectedLoginCredentials))]
[JsonPropertyName(nameof(ProtectedLoginCredentials))]
public List<ProtectedLoginCredentials> ProtectedLoginCredentials { get; set; } = [];
}
@@ -0,0 +1,13 @@
using Daybreak.Shared.Attributes;
using System.Text.Json.Serialization;
namespace Daybreak.Configuration.Options;
[OptionsName(Name = "DSOAL")]
[OptionsIgnore]
internal sealed class DSOALOptions
{
[JsonPropertyName(nameof(Enabled))]
[OptionName(Name = "Enabled", Description = "If true, the launcher will also launch DSOAL when launching GuildWars")]
public bool Enabled { get; set; }
}
@@ -1,21 +1,18 @@
using Daybreak.Attributes;
using Newtonsoft.Json;
using Daybreak.Shared.Attributes;
using System.Text.Json.Serialization;
namespace Daybreak.Configuration.Options;
[OptionsName(Name = "DXVK")]
[OptionsIgnore]
internal sealed class DXVKOptions
{
[JsonProperty(nameof(Path))]
[OptionName(Name = "Path", Description = "The path to the DXVK installation")]
[OptionSynchronizationIgnore]
public string? Path { get; set; }
[JsonProperty(nameof(Version))]
[JsonPropertyName(nameof(Version))]
[OptionSynchronizationIgnore]
[OptionIgnore]
public string? Version { get; set; }
[JsonProperty(nameof(Enabled))]
[JsonPropertyName(nameof(Enabled))]
[OptionName(Name = "Enabled", Description = "If true, the launcher will also launch DXVK when launching GuildWars")]
public bool Enabled { get; set; }
}
@@ -0,0 +1,13 @@
using Daybreak.Shared.Attributes;
using System.Text.Json.Serialization;
namespace Daybreak.Configuration.Options;
[OptionsName(Name = "Daybreak API")]
[OptionsIgnore]
public sealed class DaybreakApiOptions
{
[JsonPropertyName(nameof(Enabled))]
[OptionName(Name = "Enabled", Description = "If true, the Daybreak API is injected into Guild Wars, enabling extended functionality such as loading builds, character switching, and the focus view")]
public bool Enabled { get; set; } = false;
}
@@ -0,0 +1,13 @@
using Daybreak.Shared.Attributes;
using System.Text.Json.Serialization;
namespace Daybreak.Configuration.Options;
[OptionsName(Name = "DirectSong")]
[OptionsIgnore]
internal sealed class DirectSongOptions
{
[JsonPropertyName(nameof(Enabled))]
[OptionName(Name = "Enabled", Description = "If true, Daybreak will setup DirectSong when launching Guild Wars")]
public bool Enabled { get; set; } = false;
}
@@ -1,4 +1,4 @@
using Daybreak.Attributes;
using Daybreak.Shared.Attributes;
namespace Daybreak.Configuration.Options;
@@ -1,50 +1,42 @@
using Daybreak.Attributes;
using Daybreak.Configuration.FocusView;
using Daybreak.Shared.Models.Browser;
using Newtonsoft.Json;
using Daybreak.Shared.Attributes;
using Daybreak.Shared.Models.FocusView;
using System.Text.Json.Serialization;
namespace Daybreak.Configuration.Options;
[OptionsName(Name = "Focus View")]
[OptionsIgnore]
public sealed class FocusViewOptions
{
[JsonProperty(nameof(Enabled))]
[OptionName(Name = "Enabled", Description = "If true, the focus view is enabled, showing live information from the game")]
public bool Enabled { get; set; } = true;
[JsonProperty(nameof(ExperienceDisplay))]
[JsonPropertyName(nameof(ExperienceDisplay))]
[OptionName(Name = "Experience Display Mode", Description = "Sets how should the experience display show the information")]
public ExperienceDisplay ExperienceDisplay { get; set; }
[JsonProperty(nameof(KurzickPointsDisplay))]
[JsonPropertyName(nameof(KurzickPointsDisplay))]
[OptionName(Name = "Kurzick Points Display Mode", Description = "Sets how should the kurzick points display show the information")]
public PointsDisplay KurzickPointsDisplay { get; set; }
[JsonProperty(nameof(LuxonPointsDisplay))]
[JsonPropertyName(nameof(LuxonPointsDisplay))]
[OptionName(Name = "Luxon Points Display Mode", Description = "Sets how should the luxon points display show the information")]
public PointsDisplay LuxonPointsDisplay { get; set; }
[JsonProperty(nameof(BalthazarPointsDisplay))]
[JsonPropertyName(nameof(BalthazarPointsDisplay))]
[OptionName(Name = "Balthazar Points Display Mode", Description = "Sets how should the balthazar points display show the information")]
public PointsDisplay BalthazarPointsDisplay { get; set; }
[JsonProperty(nameof(ImperialPointsDisplay))]
[JsonPropertyName(nameof(ImperialPointsDisplay))]
[OptionName(Name = "Imperial Points Display Mode", Description = "Sets how should the imperial points display show the information")]
public PointsDisplay ImperialPointsDisplay { get; set; }
[JsonProperty(nameof(VanquishingDisplay))]
[JsonPropertyName(nameof(VanquishingDisplay))]
[OptionName(Name = "Vanquishing Display Mode", Description = "Sets how should the vanquishing display show the information")]
public PointsDisplay VanquishingDisplay { get; set; }
[JsonProperty(nameof(HealthDisplay))]
[JsonPropertyName(nameof(HealthDisplay))]
[OptionName(Name = "Health Display Mode", Description = "Sets how should the health display show the information")]
public PointsDisplay HealthDisplay { get; set; }
[JsonProperty(nameof(EnergyDisplay))]
[JsonPropertyName(nameof(EnergyDisplay))]
[OptionName(Name = "Energy Display Mode", Description = "Sets how should the energy display show the information")]
public PointsDisplay EnergyDisplay { get; set; }
[JsonProperty(nameof(BrowserHistory))]
[OptionIgnore]
public BrowserHistory BrowserHistory { get; set; } = new();
}
@@ -0,0 +1,15 @@
using Daybreak.Shared.Attributes;
using System.Text.Json.Serialization;
namespace Daybreak.Configuration.Options;
[OptionsSynchronizationIgnore]
[OptionsIgnore]
public sealed class GuildWarsScreenPlacerOptions
{
[JsonPropertyName(nameof(Enabled))]
public bool Enabled { get; set; }
[JsonPropertyName(nameof(DesiredScreen))]
public int DesiredScreen { get; set; }
}
@@ -0,0 +1,9 @@
using Daybreak.Shared.Attributes;
namespace Daybreak.Configuration.Options;
[OptionsIgnore]
public sealed class GuildWarsVersionCheckerOptions
{
public bool IsEnabled { get; set; } = true;
}
@@ -1,12 +1,11 @@
using Daybreak.Attributes;
using Newtonsoft.Json;
using System.Collections.Generic;
using Daybreak.Shared.Attributes;
using System.Text.Json.Serialization;
namespace Daybreak.Configuration.Options;
[OptionsIgnore]
[OptionsSynchronizationIgnore]
internal sealed class GuildwarsExecutableOptions
{
[JsonProperty(nameof(ExecutablePaths))]
[JsonPropertyName(nameof(ExecutablePaths))]
public List<string> ExecutablePaths { get; set; } = [];
}
@@ -1,4 +1,4 @@
using Daybreak.Attributes;
using Daybreak.Shared.Attributes;
using Daybreak.Shared.Configuration.Options;
namespace Daybreak.Configuration.Options;
@@ -1,7 +1,6 @@
using Daybreak.Attributes;
using Daybreak.Shared.Attributes;
using Daybreak.Shared.Models.LaunchConfigurations;
using Newtonsoft.Json;
using System.Collections.Generic;
using System.Text.Json.Serialization;
namespace Daybreak.Configuration.Options;
@@ -9,6 +8,6 @@ namespace Daybreak.Configuration.Options;
[OptionsSynchronizationIgnore]
internal sealed class LaunchConfigurationServiceOptions
{
[JsonProperty(nameof(LaunchConfigurations))]
[JsonPropertyName(nameof(LaunchConfigurations))]
public List<LaunchConfiguration> LaunchConfigurations { get; set; } = [];
}
@@ -0,0 +1,57 @@
using System.Text.Json.Serialization;
using Daybreak.Shared.Attributes;
namespace Daybreak.Configuration.Options;
[OptionsName(Name = "Launcher")]
public sealed class LauncherOptions
{
[JsonPropertyName(nameof(ShortcutLocation))]
[OptionName(
Name = "Shortcut Location",
Description = "Location where the shortcut will be placed"
)]
[OptionSynchronizationIgnore]
public string? ShortcutLocation { get; set; } =
Environment.GetFolderPath(Environment.SpecialFolder.Desktop);
[JsonPropertyName(nameof(PlaceShortcut))]
[OptionName(
Name = "Shortcut Placed",
Description = "If true, the shortcut will be placed. If false, the shortcut will be deleted"
)]
public bool PlaceShortcut { get; set; }
[JsonPropertyName(nameof(AutoCheckUpdate))]
[OptionName(Name = "Auto-Check For Updates", Description = "Automatically check for updates")]
public bool AutoCheckUpdate { get; set; } = true;
[JsonPropertyName(nameof(MultiLaunchSupport))]
[OptionName(
Name = "Multi-Launch Support",
Description = "If true, the launcher will support multiple executables being launched at the same time"
)]
public bool MultiLaunchSupport { get; set; }
[JsonPropertyName(nameof(ModStartupTimeout))]
[OptionName(
Name = "Mod Startup Timeout",
Description = "Amount of seconds that Daybreak will wait for each mod to start-up before cancelling the tasks"
)]
[OptionRange<double>(MinValue = 30, MaxValue = 300)]
public double ModStartupTimeout { get; set; } = 30;
[JsonPropertyName(nameof(CancelLaunchOutOfDateMods))]
[OptionName(
Name = "Cancel launch on mod updates",
Description = "If true, the launcher will stop launching Guild Wars when it detects that an active mod has an update available"
)]
public bool CancelLaunchOutOfDateMods { get; set; }
[JsonPropertyName(nameof(SaveWindowPosition))]
[OptionName(
Name = "Save Window Position",
Description = "If true, the launcher will save its position on the screen between launching"
)]
public bool SaveWindowPosition { get; set; }
}
@@ -1,6 +1,5 @@
using Daybreak.Attributes;
using Daybreak.Shared.Attributes;
using Daybreak.Services.Plugins.Models;
using System.Collections.Generic;
namespace Daybreak.Configuration.Options;
@@ -1,16 +1,17 @@
using Daybreak.Attributes;
using Newtonsoft.Json;
using Daybreak.Shared.Attributes;
using System.Text.Json.Serialization;
namespace Daybreak.Configuration.Options;
[OptionsName(Name = "ReShade")]
[OptionsIgnore]
internal sealed class ReShadeOptions
{
[JsonProperty(nameof(Enabled))]
[JsonPropertyName(nameof(Enabled))]
[OptionName(Name = "Enabled", Description = "If true, Daybreak will attempt to inject ReShade into the starting Guild Wars executable")]
public bool Enabled { get; set; } = false;
[JsonProperty(nameof(AutoUpdate))]
[JsonPropertyName(nameof(AutoUpdate))]
[OptionName(Name = "Auto-update", Description = "If true, Daybreak will periodically check ReShade for updates")]
public bool AutoUpdate { get; set; } = true;
}
@@ -1,16 +1,16 @@
using Daybreak.Attributes;
using Daybreak.Shared.Attributes;
namespace Daybreak.Configuration.Options;
[OptionsName(Name = "Screen Manager")]
[OptionsIgnore]
[OptionsSynchronizationIgnore]
internal sealed class ScreenManagerOptions
public sealed class ScreenManagerOptions
{
public double X { get; set; }
public double Y { get; set; }
public double Width { get; set; }
public double Height { get; set; }
public double DpiX { get; set; }
public double DpiY { get; set; }
public int DpiX { get; set; } = 96;
public int DpiY { get; set; } = 96;
}
@@ -1,4 +1,4 @@
using Daybreak.Attributes;
using Daybreak.Shared.Attributes;
namespace Daybreak.Configuration.Options;
@@ -0,0 +1,18 @@
using Daybreak.Attributes;
using Daybreak.Shared.Attributes;
using Daybreak.Shared.Models.Themes;
using Daybreak.Themes;
namespace Daybreak.Configuration.Options;
[OptionsName(Name = "Theme")]
public sealed class ThemeOptions
{
[OptionName(Name = "Theme", Description = "The application theme dictates the color scheme of the application")]
[DaybreakThemeOptions]
public Theme ApplicationTheme { get; set; } = CoreThemes.Daybreak;
[OptionName(Name = "UI Scale", Description = "The UI scale adjusts the size of the user interface elements")]
[OptionRange<double>(MaxValue = 1.5d, MinValue = 0.5d)]
public double ApplicationScale { get; set; } = 1.0;
}
@@ -1,17 +1,13 @@
using Daybreak.Attributes;
using Newtonsoft.Json;
using Daybreak.Shared.Attributes;
using System.Text.Json.Serialization;
namespace Daybreak.Configuration.Options;
[OptionsName(Name = "GWToolbox")]
[OptionsIgnore]
internal sealed class ToolboxOptions
{
[JsonProperty(nameof(DllPath))]
[OptionName(Name = "DllPath", Description = "The path to GWToolboxdll.dll")]
[OptionSynchronizationIgnore]
public string? DllPath { get; set; }
[JsonProperty(nameof(Enabled))]
[JsonPropertyName(nameof(Enabled))]
[OptionName(Name = "Enabled", Description = "If true, Daybreak will also launch GWToolboxdll when launching GuildWars")]
public bool Enabled { get; set; }
}
@@ -1,7 +1,5 @@
using Daybreak.Attributes;
using Daybreak.Shared.Attributes;
using Daybreak.Shared.Models.Trade;
using System;
using System.Collections.Generic;
namespace Daybreak.Configuration.Options;
@@ -19,7 +17,7 @@ internal sealed class TradeAlertingOptions
[OptionIgnore]
public List<ITradeAlert> Alerts { get; set; } = [];
[OptionName(Name = "Quote Alerts Interval", Description = "The amount of seconds in between checking for alerts based on trade quotes")]
[OptionName(Name = "Alerts Interval", Description = "The amount of seconds in between checking for alerts based on trade messages and quotes")]
[OptionRange<double>(MinValue = 30d, MaxValue = 1440d)]
public double QuoteAlertsInterval { get; set; } = 30;
}
@@ -0,0 +1,12 @@
using Daybreak.Shared.Attributes;
namespace Daybreak.Configuration.Options;
[OptionsName(Name = "Trader Quotes")]
[OptionsSynchronizationIgnore]
[OptionsIgnore]
internal sealed class TraderQuotesOptions
{
[OptionIgnore]
public string HttpsUri { get; set; } = "https://kamadan.gwtoolbox.com/";
}
@@ -1,22 +1,22 @@
using Daybreak.Attributes;
using Daybreak.Shared.Attributes;
using Daybreak.Shared.Models.UMod;
using Newtonsoft.Json;
using System.Collections.Generic;
using System.Text.Json.Serialization;
namespace Daybreak.Configuration.Options;
[OptionsName(Name = "uMod")]
[OptionsIgnore]
public sealed class UModOptions
{
[JsonProperty(nameof(Enabled))]
[JsonPropertyName(nameof(Enabled))]
[OptionName(Name = "Enabled", Description = "If true, Daybreak will also launch uMod when launching GuildWars")]
public bool Enabled { get; set; }
[JsonProperty(nameof(AutoEnableMods))]
[JsonPropertyName(nameof(AutoEnableMods))]
[OptionName(Name = "Auto-Enable Mods", Description = "If true, mods loaded into Daybreak will be enabled by default")]
public bool AutoEnableMods { get; set; } = true;
[JsonProperty(nameof(Mods))]
[JsonPropertyName(nameof(Mods))]
[OptionIgnore]
[OptionSynchronizationIgnore]
public List<UModEntry> Mods { get; set; } = [];
@@ -0,0 +1,578 @@
using System.Core.Extensions;
using System.Extensions;
using System.Reflection;
using Daybreak.Configuration.Options;
using Daybreak.Extensions;
using Daybreak.Services.Api;
using Daybreak.Services.ApplicationArguments;
using Daybreak.Services.ApplicationArguments.ArgumentHandling;
using Daybreak.Services.ApplicationLauncher;
using Daybreak.Services.BuildTemplates;
using Daybreak.Services.Credentials;
using Daybreak.Services.DirectSong;
using Daybreak.Services.Downloads;
using Daybreak.Services.Events;
using Daybreak.Services.ExceptionHandling;
using Daybreak.Services.ExecutableManagement;
using Daybreak.Services.Experience;
using Daybreak.Services.FileProviders;
using Daybreak.Services.Github;
using Daybreak.Services.Graph.Models;
using Daybreak.Services.Guildwars;
using Daybreak.Services.GuildWars;
using Daybreak.Services.Injection;
using Daybreak.Services.InternetChecker;
using Daybreak.Services.LaunchConfigurations;
using Daybreak.Services.Logging;
using Daybreak.Services.Metrics;
using Daybreak.Services.Mods;
using Daybreak.Services.Monitoring;
using Daybreak.Services.Notifications;
using Daybreak.Services.Notifications.Handlers;
using Daybreak.Services.Onboarding;
using Daybreak.Services.Options;
using Daybreak.Services.Plugins;
using Daybreak.Services.Privilege;
using Daybreak.Services.ReShade;
using Daybreak.Services.Screenshots;
using Daybreak.Services.Startup;
using Daybreak.Services.Startup.Actions;
using Daybreak.Services.TarGz;
using Daybreak.Services.Telemetry;
using Daybreak.Services.Themes;
using Daybreak.Services.Toolbox;
using Daybreak.Services.Toolbox.Notifications;
using Daybreak.Services.Toolbox.Utilities;
using Daybreak.Services.TradeChat;
using Daybreak.Services.TradeChat.Models;
using Daybreak.Services.TradeChat.Notifications;
using Daybreak.Services.UMod;
using Daybreak.Services.Updater;
using Daybreak.Services.Wiki;
using Daybreak.Shared.Models.Plugins;
using Daybreak.Shared.Services.Api;
using Daybreak.Shared.Services.ApplicationArguments;
using Daybreak.Shared.Services.ApplicationLauncher;
using Daybreak.Shared.Services.BuildTemplates;
using Daybreak.Shared.Services.Credentials;
using Daybreak.Shared.Services.DirectSong;
using Daybreak.Shared.Services.Downloads;
using Daybreak.Shared.Services.Events;
using Daybreak.Shared.Services.ExecutableManagement;
using Daybreak.Shared.Services.Experience;
using Daybreak.Shared.Services.FileProviders;
using Daybreak.Shared.Services.Github;
using Daybreak.Shared.Services.Guildwars;
using Daybreak.Shared.Services.Initialization;
using Daybreak.Shared.Services.Injection;
using Daybreak.Shared.Services.InternetChecker;
using Daybreak.Shared.Services.LaunchConfigurations;
using Daybreak.Shared.Services.Menu;
using Daybreak.Shared.Services.Metrics;
using Daybreak.Shared.Services.Mods;
using Daybreak.Shared.Services.Notifications;
using Daybreak.Shared.Services.Onboarding;
using Daybreak.Shared.Services.Options;
using Daybreak.Shared.Services.Plugins;
using Daybreak.Shared.Services.Screenshots;
using Daybreak.Shared.Services.TarGz;
using Daybreak.Shared.Services.Themes;
using Daybreak.Shared.Services.Toolbox;
using Daybreak.Shared.Services.TradeChat;
using Daybreak.Shared.Services.UMod;
using Daybreak.Shared.Services.Updater;
using Daybreak.Shared.Services.Wiki;
using Daybreak.Services.Graph;
using Daybreak.Themes;
using Daybreak.Views;
using Daybreak.Views.Copy;
using Daybreak.Views.Installation;
using Daybreak.Views.Mods;
using Daybreak.Views.Trade;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.FileProviders;
using Microsoft.Extensions.Logging;
using Microsoft.FluentUI.AspNetCore.Components;
using Microsoft.Identity.Client;
using OpenTelemetry.Resources;
using TrailBlazr.Extensions;
using TrailBlazr.Services;
using IMenuService = Daybreak.Shared.Services.Menu.IMenuService;
using MenuService = Daybreak.Services.Menu.MenuService;
using LogLevel = Microsoft.Extensions.Logging.LogLevel;
namespace Daybreak.Configuration;
public class ProjectConfiguration : PluginConfigurationBase
{
public static readonly Version CurrentVersion = Version.Parse(
Assembly.GetExecutingAssembly().GetName().Version?.ToString()
?? throw new InvalidOperationException("Unable to get current version")
);
public override void RegisterServices(IServiceCollection services)
{
services.ThrowIfNull();
services.AddFluentUIComponents();
services.AddTrailBlazr();
services.AddSingleton<IFileProvider, DaybreakFileProvider>();
RegisterHttpClients(services);
services.RegisterClientWebSocket<TradeChatService<KamadanTradeChatOptions>>().Build();
services.RegisterClientWebSocket<TradeChatService<AscalonTradeChatOptions>>().Build();
services.AddSingleton(_ =>
{
var resourceBuilder = ResourceBuilder
.CreateDefault()
.AddService("Daybreak", serviceVersion: CurrentVersion.ToString());
var attributes = new List<KeyValuePair<string, object>>();
#if DEBUG
attributes.Add(new KeyValuePair<string, object>("deployment.environment", "debug"));
#else
attributes.Add(
new KeyValuePair<string, object>("deployment.environment", "production")
);
#endif
resourceBuilder.AddAttributes(attributes);
return resourceBuilder;
});
services.AddSingleton<AppViewModel>();
services.AddSingleton<ViewManager>();
services.AddSingleton<IMenuService, MenuService>();
services.AddSingleton<IMenuServiceInitializer, MenuService>(sp =>
sp.GetRequiredService<IMenuService>().Cast<MenuService>()
);
services.AddSingleton<IMenuServiceButtonHandler, MenuService>(sp =>
sp.GetRequiredService<IMenuService>().Cast<MenuService>()
);
services.AddSingleton<IMetricsService, MetricsService>();
services.AddSingleton<INotificationService, NotificationService>();
services.AddSingleton<INotificationProducer, NotificationService>(sp =>
sp.GetRequiredService<INotificationService>().Cast<NotificationService>()
);
services.AddSingleton<IInternetCheckingService, InternetCheckingService>();
services.AddSingleton<INotificationStorage, InMemoryNotificationStorage>();
services.AddSingleton<IModsManager, ModsManager>();
services.AddSingleton<IPluginsService, PluginsService>();
services.AddSingleton<IAttachedApiAccessor, AttachedApiAccessor>();
services.AddSingleton<PrivilegeContext>();
services.AddSingleton<ViewRedirectContext>();
services.AddSingleton<JSConsoleInterop>();
services.AddSingleton<OptionsManager>();
services.AddSingleton<IOptionsProvider, OptionsManager>(sp =>
sp.GetRequiredService<OptionsManager>()
);
services.AddSingleton<IOptionsSynchronizationService, OptionsSynchronizationService>();
// Register MSAL IPublicClientApplication for cross-platform OAuth
services.AddSingleton<IPublicClientApplication>(sp =>
{
var logger = sp.GetRequiredService<ILogger<PublicClientApplication>>();
try
{
var clientId = SecretManager.GetSecret(SecretKeys.AadApplicationId);
if (string.IsNullOrWhiteSpace(clientId))
{
logger.LogWarning("AAD Application ID not configured. OneDrive sync will not be available.");
return new DummyPublicClientApplication();
}
var builder = PublicClientApplicationBuilder.Create(clientId)
.WithLogging((logLevel, message, containsPii) =>
{
if (containsPii && logLevel > Microsoft.Identity.Client.LogLevel.Info)
{
message = "[REDACTED]";
}
var equivalentLogLevel = logLevel switch
{
Microsoft.Identity.Client.LogLevel.Error => LogLevel.Error,
Microsoft.Identity.Client.LogLevel.Warning => LogLevel.Warning,
Microsoft.Identity.Client.LogLevel.Info => LogLevel.Information,
Microsoft.Identity.Client.LogLevel.Verbose => LogLevel.Debug,
_ => LogLevel.None
};
logger.Log(equivalentLogLevel, message);
}, enablePiiLogging: true, enableDefaultPlatformLogging: true)
.WithCacheOptions(new CacheOptions { UseSharedCache = true })
.WithRedirectUri(GraphClient.RedirectUri)
.WithHttpClientFactory(new DaybreakMsalHttpClientProvider(sp.GetRequiredService<ILogger<GraphClient>>()));
return builder.Build();
}
catch (Exception e)
{
logger.LogError(e, "Failed to create PublicClientApplication, using dummy implementation");
return new DummyPublicClientApplication();
}
});
services.AddScoped<IGraphClient, GraphClient>();
services.AddScoped<ICredentialManager, CredentialManager>();
services.AddScoped<IApplicationLauncher, ApplicationLauncher>();
services.AddScoped<IBuildTemplateManager, BuildTemplateManager>();
services.AddScoped<IOnboardingService, OnboardingService>();
services.AddScoped<IExperienceCalculator, ExperienceCalculator>();
services.AddScoped<IAttributePointCalculator, AttributePointCalculator>();
services.AddScoped<IDownloadService, DownloadService>();
services.AddScoped<IGuildWarsInstaller, IntegratedGuildwarsInstaller>();
services.AddScoped<IExceptionHandler, ExceptionHandler>();
services.AddScoped<
ITradeChatService<KamadanTradeChatOptions>,
TradeChatService<KamadanTradeChatOptions>
>();
services.AddScoped<
ITradeChatService<AscalonTradeChatOptions>,
TradeChatService<AscalonTradeChatOptions>
>();
services.AddScoped<ITraderQuoteService, TraderQuoteService>();
services.AddScoped<ITradeHistoryDatabase, TradeHistoryDatabase>();
services.AddScoped<IGuildWarsCopyService, GuildWarsCopyService>();
services.AddScoped<IItemHashService, ItemHashService>();
services.AddScoped<IToolboxClient, ToolboxClient>();
services.AddSingleton<IGithubClient, GithubClient>();
services.AddScoped<IProcessInjector, ProcessInjector>();
services.AddScoped<IStubInjector, StubInjector>();
services.AddScoped<ITarGzExtractor, TarGzExtractor>();
services.AddScoped<ILaunchConfigurationService, LaunchConfigurationService>();
services.AddScoped<IEventService, EventService>();
services.AddScoped<IApplicationArgumentService, ApplicationArgumentService>();
services.AddScoped<IWikiService, WikiService>();
services.AddScoped<IScreenshotService, ScreenshotService>();
services.AddHostedSingleton<IApplicationUpdater, ApplicationUpdater>();
services.AddHostedSingleton<IThemeManager, BlazorThemeInteropService>();
services.AddHostedSingleton<IConnectivityStatus, ConnectivityStatus>();
services.AddHostedSingleton<ITradeAlertingService, TradeAlertingService>();
services.AddHostedSingleton<IGuildWarsExecutableManager, GuildWarsExecutableManager>();
services.AddHostedSingleton<IEventNotifierService, EventNotifierService>();
services.AddHostedSingleton<IApiScanningService, ApiScanningService>();
services.AddHostedSingleton<GameScreenshotsTheme>();
services.AddHostedService<StartupActionManager>();
services.AddHostedService<ProcessorUsageMonitor>();
services.AddHostedService<TelemetryHost>();
}
public override void RegisterViews(IViewProducer viewProducer)
{
viewProducer.ThrowIfNull();
viewProducer.RegisterView<LaunchView, LaunchViewModel>(isSingleton: true);
viewProducer.RegisterView<OptionView, OptionViewModel>();
viewProducer.RegisterView<FocusView, FocusViewModel>();
viewProducer.RegisterView<BuildListView, BuildListViewModel>();
viewProducer.RegisterView<BuildRoutingView, BuildRoutingViewModel>();
viewProducer.RegisterView<SingleBuildTemplateView, SingleBuildTemplateViewModel>();
viewProducer.RegisterView<TeamBuildTemplateView, TeamBuildTemplateViewModel>();
viewProducer.RegisterView<AccountsView, AccountsViewModel>();
viewProducer.RegisterView<ExecutablesView, ExecutablesViewModel>();
viewProducer.RegisterView<LaunchConfigurationsView, LaunchConfigurationsViewModel>();
viewProducer.RegisterView<RequestElevationView, RequestElevationViewModel>();
viewProducer.RegisterView<RequestDelevationView, RequestDelevationViewModel>();
viewProducer.RegisterView<SettingsSynchronizationView, SettingsSynchronizationViewModel>();
viewProducer.RegisterView<TelemetryView, TelemetryViewModel>();
viewProducer.RegisterView<VersionManagementView, VersionManagementViewModel>();
viewProducer.RegisterView<UpdateConfirmationView, UpdateConfirmationViewModel>();
viewProducer.RegisterView<UpdateView, UpdateViewModel>();
viewProducer.RegisterView<PluginsView, PluginsViewModel>();
viewProducer.RegisterView<LogsView, LogsViewModel>();
viewProducer.RegisterView<GuildWarsPartySearchView, GuildWarsPartySearchViewModel>();
viewProducer.RegisterView<EventCalendarView, EventCalendarViewModel>();
viewProducer.RegisterView<TradeChatView, TradeChatViewModel>();
viewProducer.RegisterView<GuildWarsDownloadView, GuildWarsDownloadViewModel>();
viewProducer.RegisterView<GuildWarsCopySelectionView, GuildWarsCopySelectionViewModel>();
viewProducer.RegisterView<GuildWarsCopyView, GuildWarsCopyViewModel>();
viewProducer.RegisterView<TradeAlertsView, TradeAlertsViewModel>();
viewProducer.RegisterView<TradeMessageView, TradeMessageViewModel>();
viewProducer.RegisterView<TradeQuoteView, TradeQuoteViewModel>();
viewProducer.RegisterView<LauncherOnboardingView, LauncherOnboardingViewModel>();
viewProducer.RegisterView<ModsView, ModsViewModel>();
viewProducer.RegisterView<ModInstallationView, ModInstallationViewModel>();
viewProducer.RegisterView<
ModInstallationConfirmationView,
ModInstallationConfirmationViewModel
>();
viewProducer.RegisterView<ScreenSelectorView, ScreenSelectorViewModel>();
viewProducer.RegisterView<UModManagementView, UModManagementViewModel>();
viewProducer.RegisterView<TradeNotificationView, TradeNotificationViewModel>();
viewProducer.RegisterView<WikiView, WikiViewModel>();
viewProducer.RegisterView<ReShadeManagementView, ReShadeManagementViewModel>();
viewProducer.RegisterView<GuildWarsMarketView, GuildWarsMarketViewModel>();
}
public override void RegisterStartupActions(IStartupActionProducer startupActionProducer)
{
startupActionProducer.ThrowIfNull();
startupActionProducer.RegisterAction<VerifyNativeComponentsAction>();
startupActionProducer.RegisterAction<RenameInstallerAction>();
startupActionProducer.RegisterAction<UpdateUModAction>();
startupActionProducer.RegisterAction<CredentialsOptionsMigrator>();
startupActionProducer.RegisterAction<UpdateToolboxAction>();
startupActionProducer.RegisterAction<UpdateGuildWarsExecutable>();
startupActionProducer.RegisterAction<UpdateDirectSongAction>();
}
public override void RegisterPostUpdateActions(
IPostUpdateActionProducer postUpdateActionProducer
)
{
postUpdateActionProducer.ThrowIfNull();
}
public override void RegisterOptions(IOptionsProducer optionsProducer)
{
optionsProducer.ThrowIfNull();
optionsProducer.RegisterOptions<TelemetryOptions>();
optionsProducer.RegisterOptions<LauncherOptions>();
optionsProducer.RegisterOptions<ThemeOptions>();
optionsProducer.RegisterOptions<SynchronizationOptions>();
optionsProducer.RegisterOptions<FocusViewOptions>();
optionsProducer.RegisterOptions<DaybreakApiOptions>();
optionsProducer.RegisterOptions<ToolboxOptions>();
optionsProducer.RegisterOptions<UModOptions>();
optionsProducer.RegisterOptions<DSOALOptions>();
optionsProducer.RegisterOptions<ReShadeOptions>();
optionsProducer.RegisterOptions<DXVKOptions>();
optionsProducer.RegisterOptions<ScreenManagerOptions>();
optionsProducer.RegisterOptions<KamadanTradeChatOptions>();
optionsProducer.RegisterOptions<AscalonTradeChatOptions>();
optionsProducer.RegisterOptions<TraderQuotesOptions>();
optionsProducer.RegisterOptions<TradeAlertingOptions>();
optionsProducer.RegisterOptions<EventNotifierOptions>();
optionsProducer.RegisterOptions<PluginsServiceOptions>();
optionsProducer.RegisterOptions<GuildwarsExecutableOptions>();
optionsProducer.RegisterOptions<CredentialManagerOptions>();
optionsProducer.RegisterOptions<LaunchConfigurationServiceOptions>();
optionsProducer.RegisterOptions<DirectSongOptions>();
optionsProducer.RegisterOptions<GuildWarsVersionCheckerOptions>();
optionsProducer.RegisterOptions<GuildWarsScreenPlacerOptions>();
}
public override void RegisterNotificationHandlers(
INotificationHandlerProducer notificationHandlerProducer
)
{
notificationHandlerProducer.RegisterNotificationHandler<NoActionHandler>();
notificationHandlerProducer.RegisterNotificationHandler<MessageBoxHandler>();
notificationHandlerProducer.RegisterNotificationHandler<TradeMessageNotificationHandler>();
notificationHandlerProducer.RegisterNotificationHandler<UpdateNotificationHandler>();
notificationHandlerProducer.RegisterNotificationHandler<NavigateToCalendarViewHandler>();
notificationHandlerProducer.RegisterNotificationHandler<GuildWarsBatchUpdateNotificationHandler>();
notificationHandlerProducer.RegisterNotificationHandler<ToolboxUpdateHandler>();
notificationHandlerProducer.RegisterNotificationHandler<NavigateToModsViewHandler>();
}
public override void RegisterMods(IModsProducer modsManager)
{
modsManager.RegisterMod<IGuildWarsVersionChecker, GuildWarsVersionChecker>();
modsManager.RegisterMod<IDaybreakApiService, DaybreakApiService>();
modsManager.RegisterMod<IToolboxService, ToolboxService>();
modsManager.RegisterMod<IUModService, UModService>();
modsManager.RegisterMod<IDirectSongService, DirectSongService>(singleton: true);
}
public override void RegisterLaunchArgumentHandlers(
IArgumentHandlerProducer argumentHandlerProducer
)
{
argumentHandlerProducer.ThrowIfNull();
argumentHandlerProducer.RegisterArgumentHandler<AutoLaunchArgumentHandler>();
argumentHandlerProducer.RegisterArgumentHandler<ResetWindowPositionArgumentHandler>();
}
public override void RegisterMenuButtons(IMenuServiceProducer menuServiceProducer)
{
menuServiceProducer.ThrowIfNull();
menuServiceProducer
.CreateIfNotExistCategory("Guild Wars")
.RegisterButton(
"Game Companion",
"Open game companion",
sp => sp.GetRequiredService<IViewManager>().ShowView<LaunchView>()
)
.RegisterButton(
"Manage Builds",
"Open builds manager",
sp => sp.GetRequiredService<IViewManager>().ShowView<BuildListView>()
)
.RegisterButton(
"Manage Mods",
"Open Guild Wars mods manager",
sp => sp.GetRequiredService<IViewManager>().ShowView<ModsView>()
)
.RegisterButton(
"Download Guild Wars",
"Download Guild Wars installer",
sp => sp.GetRequiredService<IViewManager>().ShowView<GuildWarsDownloadView>()
)
.RegisterButton(
"Copy Guild Wars",
"Copy Guild Wars from an existing installation",
sp => sp.GetRequiredService<IViewManager>().ShowView<GuildWarsCopySelectionView>()
);
menuServiceProducer
.CreateIfNotExistCategory("Daybreak")
//TODO: Implement Notifications view
//.RegisterButton("Notifications", "Open notifications view", sp => { })
.RegisterButton(
"Manage Plugins",
"Open plugins view",
sp => sp.GetRequiredService<IViewManager>().ShowView<PluginsView>()
)
.RegisterButton(
"Manage version",
"Open version manager",
sp => sp.GetRequiredService<IViewManager>().ShowView<VersionManagementView>()
)
.RegisterButton(
"Help",
"Open Daybreak wiki",
sp =>
sp.GetRequiredService<IViewManager>()
.ShowView<WikiView>((nameof(WikiView.Page), "Home"))
);
menuServiceProducer
.CreateIfNotExistCategory("Services")
.RegisterButton(
"Alerts",
"Open trade alerts manager",
sp => sp.GetRequiredService<IViewManager>().ShowView<TradeAlertsView>()
)
.RegisterButton(
"Kamadan",
"Open kamadan trade chat",
sp =>
sp.GetRequiredService<IViewManager>()
.ShowView<TradeChatView>(
(nameof(TradeChatView.Source), nameof(TraderSource.Kamadan))
)
)
.RegisterButton(
"Ascalon",
"Open ascalon trade chat",
sp =>
sp.GetRequiredService<IViewManager>()
.ShowView<TradeChatView>(
(nameof(TradeChatView.Source), nameof(TraderSource.Ascalon))
)
)
.RegisterButton(
"Guild Wars Party Search",
"Show party search broadcasts",
sp => sp.GetRequiredService<IViewManager>().ShowView<GuildWarsPartySearchView>()
)
.RegisterButton(
"Guild Wars Marketplace",
"Show guild wars marketplace",
sp => sp.GetRequiredService<IViewManager>().ShowView<GuildWarsMarketView>()
)
.RegisterButton(
"Event Calendar",
"Show current and upcoming events",
sp => sp.GetRequiredService<IViewManager>().ShowView<EventCalendarView>()
);
menuServiceProducer
.CreateIfNotExistCategory("Settings")
.RegisterButton(
"Accounts",
"Accounts Settings",
sp => sp.GetRequiredService<IViewManager>().ShowView<AccountsView>()
)
.RegisterButton(
"Executables",
"Executables Settings",
sp => sp.GetRequiredService<IViewManager>().ShowView<ExecutablesView>()
)
.RegisterButton(
"Launch configurations",
"Launch configurations settings",
sp => sp.GetRequiredService<IViewManager>().ShowView<LaunchConfigurationsView>()
);
menuServiceProducer
.CreateIfNotExistCategory("Diagnostics")
.RegisterButton(
"Telemetry",
"Open telemetry view",
sp => sp.GetRequiredService<IViewManager>().ShowView<TelemetryView>()
)
.RegisterButton(
"Logs",
"Open logs view",
sp => sp.GetRequiredService<IViewManager>().ShowView<LogsView>()
);
}
public override void RegisterThemes(IThemeProducer themeProducer)
{
themeProducer.ThrowIfNull();
var themes = CoreThemes.Themes.Concat(HeroThemes.Themes).Concat(LiveThemes.Themes);
foreach (var theme in themes)
{
themeProducer.RegisterTheme(theme);
}
}
public override void RegisterProviderAssemblies(IFileProviderProducer fileProviderProducer)
{
fileProviderProducer.RegisterAssembly(typeof(ProjectConfiguration).Assembly);
}
private static IServiceCollection RegisterHttpClients(IServiceCollection services)
{
return services
.ThrowIfNull()
.RegisterHttpClient<DownloadService>()
.WithMessageHandler(SetupLoggingAndMetrics<DownloadService>)
.WithDefaultRequestHeadersSetup(SetupDaybreakUserAgent)
.Build()
.RegisterHttpClient<TradeChatService<KamadanTradeChatOptions>>()
.WithMessageHandler(SetupLoggingAndMetrics<TradeChatService<KamadanTradeChatOptions>>)
.WithDefaultRequestHeadersSetup(SetupDaybreakUserAgent)
.Build()
.RegisterHttpClient<TradeChatService<AscalonTradeChatOptions>>()
.WithMessageHandler(SetupLoggingAndMetrics<TradeChatService<AscalonTradeChatOptions>>)
.WithDefaultRequestHeadersSetup(SetupDaybreakUserAgent)
.Build()
.RegisterHttpClient<TraderQuoteService>()
.WithMessageHandler(SetupLoggingAndMetrics<TraderQuoteService>)
.WithDefaultRequestHeadersSetup(SetupDaybreakUserAgent)
.Build()
.RegisterHttpClient<InternetCheckingService>()
.WithMessageHandler(SetupLoggingAndMetrics<InternetCheckingService>)
.WithDefaultRequestHeadersSetup(SetupDaybreakUserAgent)
.Build()
.RegisterHttpClient<GithubClient>()
.WithMessageHandler(SetupLoggingAndMetrics<GithubClient>)
.WithDefaultRequestHeadersSetup(SetupDaybreakUserAgent)
.Build()
.RegisterHttpClient<ScopedApiContext>()
.WithMessageHandler(SetupLoggingAndMetrics<ScopedApiContext>)
.WithDefaultRequestHeadersSetup(SetupDaybreakUserAgent)
.Build()
.RegisterHttpClient<WikiService>()
.WithMessageHandler(SetupLoggingAndMetrics<WikiService>)
.WithDefaultRequestHeadersSetup(SetupDaybreakUserAgent)
.Build()
.RegisterHttpClient<ReShadeService>()
.WithMessageHandler(SetupLoggingAndMetrics<ReShadeService>)
.WithDefaultRequestHeadersSetup(SetupDaybreakUserAgent)
.Build()
.RegisterHttpClient<GraphClient>()
.WithMessageHandler(SetupLoggingAndMetrics<GraphClient>)
.WithDefaultRequestHeadersSetup(SetupDaybreakUserAgent)
.Build();
}
}
@@ -4,8 +4,9 @@ public class SecretKeys
{
public static readonly SecretKeys AadApplicationId = new() { Key = "AadApplicationId" };
public static readonly SecretKeys AadTenantId = new() { Key = "AadTenantId" };
public static readonly SecretKeys ApmApiKey = new() { Key = "ApmApiKey" };
public static readonly SecretKeys ApmUri = new() { Key = "ApmUri" };
public static readonly SecretKeys ApmServiceAccount = new() { Key = "ApmServiceAccount" };
public static readonly SecretKeys ApmServiceKey = new() { Key = "ApmServiceKey" };
public string Key { get; private set; } = string.Empty;
private SecretKeys()
@@ -0,0 +1,45 @@
using System.Extensions;
using System.Reflection;
using System.Text.Json;
namespace Daybreak.Configuration;
public static class SecretManager
{
private static JsonDocument? SecretsHolder;
public static string GetSecret(SecretKeys secretKey)
{
SecretsHolder ??= LoadSecrets();
if (SecretsHolder.RootElement.TryGetProperty(secretKey.Key, out var element))
{
return element.GetString() ?? string.Empty;
}
return string.Empty;
}
private static JsonDocument LoadSecrets()
{
try
{
var serializedSecrets = Assembly.GetExecutingAssembly().GetManifestResourceStream("Daybreak.secrets.json")?.ReadAllBytes().GetString() ?? throw new InvalidOperationException("Could not load Daybreak.secrets.json from assembly");
serializedSecrets = TrimUnwantedCharacters(serializedSecrets);
return JsonDocument.Parse(serializedSecrets);
}
catch
{
return JsonDocument.Parse("{}");
}
}
private static string TrimUnwantedCharacters(string s)
{
return new string([.. s.Where(c =>
char.IsWhiteSpace(c) ||
char.IsLetterOrDigit(c) ||
char.IsSymbol(c) ||
char.IsSeparator(c) ||
char.IsPunctuation(c))]);
}
}
+111
View File
@@ -0,0 +1,111 @@
<Project Sdk="Microsoft.NET.Sdk.Razor">
<PropertyGroup>
<RootNamespace>Daybreak</RootNamespace>
<TargetFrameworks>$(TargetFrameworkVersion);$(TargetFrameworkVersionWindows)</TargetFrameworks>
<ScopedCssEnabled>true</ScopedCssEnabled>
<Nullable>enable</Nullable>
<Platforms>x64</Platforms>
<CopyLocalLockFileAssemblies>true</CopyLocalLockFileAssemblies>
<LangVersion>preview</LangVersion>
<EnableWindowsTargeting>true</EnableWindowsTargeting>
<UserSecretsId>cfb2a489-db80-448d-a969-80270f314c46</UserSecretsId>
<SatelliteResourceLanguages>en</SatelliteResourceLanguages>
</PropertyGroup>
<!-- Define WINDOWS constant when targeting Windows -->
<PropertyGroup Condition="$(TargetFramework.Contains('windows'))">
<DefineConstants>$(DefineConstants);WINDOWS</DefineConstants>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)' == 'Release'">
<StripSymbols>true</StripSymbols>
<DebugType>none</DebugType>
<DebugSymbols>false</DebugSymbols>
</PropertyGroup>
<Target Name="AddUserSecrets" BeforeTargets="PrepareForBuild" Condition=" '$(UserSecretsId)' != '' ">
<PropertyGroup>
<UserSecretsFilePath Condition=" '$(OS)' == 'Windows_NT' ">
$([System.Environment]::GetFolderPath(SpecialFolder.UserProfile))\AppData\Roaming\Microsoft\UserSecrets\$(UserSecretsId)\secrets.json
</UserSecretsFilePath>
<UserSecretsFilePath Condition=" '$(OS)' == 'Unix' ">
$([System.Environment]::GetFolderPath(SpecialFolder.UserProfile))/.microsoft/usersecrets/$(UserSecretsId)/secrets.json
</UserSecretsFilePath>
</PropertyGroup>
<ItemGroup>
<EmbeddedResource Include="$(UserSecretsFilePath)" Condition="Exists($(UserSecretsFilePath))" />
</ItemGroup>
</Target>
<ItemGroup>
<PackageReference Include="Elastic.OpenTelemetry" />
<PackageReference Include="Microsoft.Extensions.FileProviders.Embedded" />
<PackageReference Include="Microsoft.FluentUI.AspNetCore.Components" />
<PackageReference Include="Microsoft.FluentUI.AspNetCore.Components.Icons" />
<PackageReference Include="Microsoft.AspNetCore.Components" />
<PackageReference Include="Microsoft.Identity.Client" />
<PackageReference Include="OpenTelemetry.Exporter.Console" />
<PackageReference Include="OpenTelemetry.Instrumentation.Http" />
<PackageReference Include="OpenTelemetry.Instrumentation.Runtime" />
<PackageReference Include="PeNet" />
<PackageReference Include="Plumsy" />
<PackageReference Include="DiffPlex" />
<PackageReference Include="HtmlAgilityPack" />
<PackageReference Include="ini-parser-netstandard" />
<PackageReference Include="MegaApiClient" />
<PackageReference Include="Microsoft.CorrelationVector" />
<PackageReference Include="Microsoft.Extensions.Configuration.UserSecrets" />
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" />
<PackageReference Include="Newtonsoft.Json" />
<PackageReference Include="Serilog" />
<PackageReference Include="Serilog.Enrichers.Environment" />
<PackageReference Include="Serilog.Enrichers.Thread" />
<PackageReference Include="Serilog.Extensions.Logging" />
<PackageReference Include="Serilog.Sinks.Console" />
<PackageReference Include="Serilog.Sinks.Debug" />
<PackageReference Include="SharpCompress" />
<PackageReference Include="SixLabors.ImageSharp" />
<PackageReference Include="System.Security.Cryptography.ProtectedData" />
</ItemGroup>
<!-- Windows-only packages -->
<ItemGroup Condition="$(TargetFramework.Contains('windows'))">
<PackageReference Include="securifybv.ShellLink" />
<PackageReference Include="System.Drawing.Common" />
<!-- Exclude WPF assets from WebView2 to avoid WindowsBase version conflict (MSB3277) -->
<PackageReference Include="Microsoft.Web.WebView2" ExcludeAssets="buildTransitive" PrivateAssets="all">
<IncludeAssets>runtime; compile; native; contentfiles; analyzers</IncludeAssets>
</PackageReference>
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\Daybreak.Shared\Daybreak.Shared.csproj" />
</ItemGroup>
<!-- Embed Daybreak.wiki files as embedded resources with web-friendly logical names -->
<PropertyGroup>
<WikiSourcePath>..\Daybreak.wiki</WikiSourcePath>
</PropertyGroup>
<ItemGroup>
<EmbeddedResource Include="$(WikiSourcePath)\**\*.md" Exclude="$(WikiSourcePath)\.git\**">
<LogicalName>wiki/%(RecursiveDir)%(Filename)%(Extension)</LogicalName>
</EmbeddedResource>
<EmbeddedResource Include="$(WikiSourcePath)\**\*.png" Exclude="$(WikiSourcePath)\.git\**">
<LogicalName>wiki/%(RecursiveDir)%(Filename)%(Extension)</LogicalName>
</EmbeddedResource>
<EmbeddedResource Include="$(WikiSourcePath)\**\*.jpg" Exclude="$(WikiSourcePath)\.git\**">
<LogicalName>wiki/%(RecursiveDir)%(Filename)%(Extension)</LogicalName>
</EmbeddedResource>
<EmbeddedResource Include="$(WikiSourcePath)\**\*.gif" Exclude="$(WikiSourcePath)\.git\**">
<LogicalName>wiki/%(RecursiveDir)%(Filename)%(Extension)</LogicalName>
</EmbeddedResource>
</ItemGroup>
<Target Name="PostBuild" AfterTargets="PostBuildEvent" Condition="'$(Configuration)' == 'Release'">
<!-- Cross-platform: use Touch task instead of Windows-only echo.> -->
<Touch Files="$(Version).version" AlwaysCreate="true" />
</Target>
</Project>
@@ -0,0 +1,60 @@
using Daybreak.Models;
using Daybreak.Services.Options;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Options;
using System.Extensions;
namespace Daybreak.Extensions;
public static class ServiceCollectionExtensions
{
public static IServiceCollection AddDaybreakOptions<TOptions>(this IServiceCollection services)
where TOptions : class, new()
{
services.AddSingleton(new OptionEntry(typeof(TOptions)));
services.AddSingleton<IOptionsFactory<TOptions>>(sp =>
new DaybreakOptionsFactory<TOptions>(sp.GetRequiredService<OptionsManager>()));
services.AddSingleton<IOptionsChangeTokenSource<TOptions>>(sp =>
new DaybreakOptionsChangeTokenSource<TOptions>(sp.GetRequiredService<OptionsManager>()));
services.AddSingleton<IOptionsMonitorCache<TOptions>, OptionsCache<TOptions>>();
services.AddSingleton<IOptionsMonitor<TOptions>>(sp =>
{
var factory = sp.GetRequiredService<IOptionsFactory<TOptions>>();
var sources = sp.GetServices<IOptionsChangeTokenSource<TOptions>>();
var cache = sp.GetRequiredService<IOptionsMonitorCache<TOptions>>();
var monitor = new OptionsMonitor<TOptions>(factory, sources, cache);
monitor.OnChange((_, name) => cache.TryRemove(name ?? Options.DefaultName));
return new OptionsMonitor<TOptions>(factory, sources, cache);
});
services.AddSingleton<IOptions<TOptions>>(sp =>
new OptionsWrapper<TOptions>(sp.GetRequiredService<IOptionsMonitor<TOptions>>().CurrentValue));
services.AddScoped<IOptionsSnapshot<TOptions>>(sp =>
new OptionsManager<TOptions>(
sp.GetRequiredService<IOptionsFactory<TOptions>>()));
return services;
}
public static IServiceCollection AddHostedSingleton<TInterface, TImplementation>(this IServiceCollection services)
where TImplementation : class, TInterface, IHostedService
where TInterface : class
{
services.AddSingleton<TInterface, TImplementation>();
services.AddHostedService(sp => sp.GetRequiredService<TInterface>().Cast<TImplementation>());
return services;
}
public static IServiceCollection AddHostedSingleton<TImplementation>(this IServiceCollection services)
where TImplementation : class, IHostedService
{
services.AddSingleton<TImplementation>();
services.AddHostedService(sp => sp.GetRequiredService<TImplementation>());
return services;
}
}
+3
View File
@@ -0,0 +1,3 @@
using System.Runtime.CompilerServices;
[assembly: InternalsVisibleTo("Daybreak")]
@@ -0,0 +1,51 @@
using Daybreak.Configuration.Options;
using Daybreak.Extensions;
using Daybreak.Services.Options;
using Daybreak.Services.Plugins;
using Daybreak.Services.Themes;
using Daybreak.Shared.Services.Options;
using Daybreak.Shared.Services.Plugins;
using Daybreak.Shared.Services.Themes;
using Microsoft.Extensions.DependencyInjection;
using System.Extensions;
namespace Daybreak.Launch;
public partial class Launcher
{
public static ServiceProvider SetupBootstrap()
{
var serviceCollection = new ServiceCollection();
SetupLogging(serviceCollection);
BootstrapOptionsManager(serviceCollection);
BootstrapThemeOptions(serviceCollection);
BootstrapScreenManagerOptions(serviceCollection);
BootstrapPluginsService(serviceCollection);
return serviceCollection.BuildServiceProvider();
}
private static void BootstrapPluginsService(IServiceCollection services)
{
services.AddDaybreakOptions<PluginsServiceOptions>();
services.AddSingleton<IPluginsService, PluginsService>();
}
private static void BootstrapOptionsManager(IServiceCollection services)
{
services.AddSingleton<OptionsManager>();
services.AddSingleton<IOptionsProvider>(sp => sp.GetRequiredService<OptionsManager>());
}
private static void BootstrapThemeOptions(IServiceCollection services)
{
services.AddDaybreakOptions<ThemeOptions>();
services.AddSingleton<IThemeManager, BlazorThemeInteropService>();
services.AddHostedService(sp => sp.GetRequiredService<IThemeManager>().Cast<BlazorThemeInteropService>());
}
private static void BootstrapScreenManagerOptions(IServiceCollection services)
{
// Only register options here - actual ScreenManager is registered by platform-specific configuration
services.AddDaybreakOptions<ScreenManagerOptions>();
}
}
+92
View File
@@ -0,0 +1,92 @@
using System.Extensions.Core;
using Daybreak.Shared.Services.Screens;
using Daybreak.Views;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Photino.Blazor;
namespace Daybreak.Launch;
public partial class Launcher
{
#if DEBUG
public const bool IsDebug = true;
#else
public const bool IsDebug = false;
#endif
public static PhotinoBlazorApp? MainApp { get; private set; }
public static PhotinoBlazorAppBuilder? MainBuilder { get; private set; }
private static PhotinoBlazorAppBuilder CreateMainBuilder(string[] args)
{
var mainBuilder = PhotinoBlazorAppBuilder.CreateDefault(args);
mainBuilder.RootComponents.Add<App>("#app");
mainBuilder.Services.AddBlazorDesktop();
SetupLogging(mainBuilder.Services);
MainBuilder = mainBuilder;
return mainBuilder;
}
private static PhotinoBlazorApp CreateMainApp(
PhotinoBlazorAppBuilder mainBuilder,
string[] args
)
{
var app = mainBuilder.Build();
var cts = new CancellationTokenSource();
app.MainWindow.SetTitle("Daybreak")
.SetContextMenuEnabled(!IsDebug || args.Any(s => string.Equals(s, "--enable-context-menu")))
.SetDevToolsEnabled(!IsDebug || args.Any(s => string.Equals(s, "--enable-dev-tools")))
.SetSmoothScrollingEnabled(true)
.SetChromeless(!args.Any(s => string.Equals(s, "--disable-chromeless")));
app.MainWindow.SetLogVerbosity(0);
app.MainWindow.RegisterWindowCreatedHandler((_, __) => StartHostedServices(app, cts));
app.MainWindow.RegisterWindowClosingHandler((_, __) => StopHostedServices(app, cts));
SetWindowPosition(app);
MainApp = app;
return app;
}
private static void RunMainApp(PhotinoBlazorApp app)
{
app.Run();
}
private static void SetWindowPosition(PhotinoBlazorApp app)
{
var scopedLogger = app
.Services.GetRequiredService<ILogger<Launcher>>()
.CreateScopedLogger();
var screenManager = app.Services.GetRequiredService<IScreenManager>();
var savedPosition = screenManager.GetSavedPosition();
if (savedPosition.Width is 0 || savedPosition.Height is 0)
{
scopedLogger.LogDebug("No saved window position found, centering the window");
var firstScreen = screenManager.Screens.FirstOrDefault();
if (firstScreen.Size.IsEmpty)
{
scopedLogger.LogWarning(
"No valid screen found to center the window. Window will spawn with minimal size"
);
return;
}
app.MainWindow.Center();
app.MainWindow.Width = firstScreen.Size.Width / 2;
app.MainWindow.Height = firstScreen.Size.Height / 2;
}
else
{
scopedLogger.LogDebug("Restoring saved window position");
app.MainWindow.Left = savedPosition.X;
app.MainWindow.Top = savedPosition.Y;
app.MainWindow.Width = savedPosition.Width;
app.MainWindow.Height = savedPosition.Height;
}
}
}
+69
View File
@@ -0,0 +1,69 @@
using Daybreak.Services.Logging;
using Daybreak.Services.Telemetry;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
using Photino.Blazor;
using Serilog;
using Serilog.Events;
using Serilog.Sinks.SystemConsole.Themes;
using System.Extensions.Core;
namespace Daybreak.Launch;
public partial class Launcher
{
public const string OutputTemplate = "[{Timestamp:yyyy-MM-dd HH:mm:ss}] {Level:u4}: [{EnvironmentName}] [{ThreadId}:{ThreadName}] [{SourceContext}]{NewLine}{Message:lj}{NewLine}{Exception}";
public static void SetupLogging(IServiceCollection services)
{
Log.Logger = new LoggerConfiguration()
.MinimumLevel.Debug()
.MinimumLevel.Override("Microsoft", LogEventLevel.Warning)
.MinimumLevel.Override("Microsoft.AspNetCore", LogEventLevel.Warning)
.MinimumLevel.Override("System", LogEventLevel.Warning)
.MinimumLevel.Override("Daybreak.Services.TradeChat.TradeChatService", LogEventLevel.Information)
.MinimumLevel.Override("Daybreak.Services.Credentials.CredentialManager", LogEventLevel.Information)
.MinimumLevel.Override("Daybreak.Shared.Services.BuildTemplates.BuildTemplateManager", LogEventLevel.Information)
.Enrich.FromLogContext()
.Enrich.WithThreadId()
.Enrich.WithThreadName()
.Enrich.WithEnvironmentName()
.WriteTo.Console(
outputTemplate: OutputTemplate,
theme: AnsiConsoleTheme.Sixteen)
.WriteTo.Sink(InMemorySink.Instance)
.WriteTo.Sink(TelemetryLogSink.Instance)
.CreateLogger();
services.AddLogging(loggingBuilder =>
{
loggingBuilder.ClearProviders();
loggingBuilder.AddSerilog(dispose: true);
});
}
private static void StartHostedServices(PhotinoBlazorApp app, CancellationTokenSource cts)
{
var hostedServices = app.Services.GetServices<IHostedService>();
foreach (var hostedService in hostedServices)
{
Task.Factory.StartNew(() => hostedService.StartAsync(cts.Token), cts.Token, TaskCreationOptions.LongRunning, TaskScheduler.Current);
}
}
private static bool StopHostedServices(PhotinoBlazorApp app, CancellationTokenSource cts)
{
cts.CancelAfter(TimeSpan.FromSeconds(2));
var hostedServices = app.Services.GetServices<IHostedService>();
var tasks = hostedServices.Select(s => s.StopAsync(cts.Token));
Task.WaitAll(tasks, cts.Token);
cts.Dispose();
return false;
}
}
+266
View File
@@ -0,0 +1,266 @@
using Daybreak.Configuration;
using Daybreak.Services.FileProviders;
using Daybreak.Services.Initialization;
using Daybreak.Shared.Models.Menu;
using Daybreak.Shared.Models.Plugins;
using Daybreak.Shared.Models.Themes;
using Daybreak.Shared.Services.ApplicationArguments;
using Daybreak.Shared.Services.Keyboard;
using Daybreak.Shared.Services.Plugins;
using Daybreak.Shared.Services.Themes;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Photino.Blazor;
using System.Extensions.Core;
using System.Logging;
using System.Runtime.CompilerServices;
[assembly: InternalsVisibleTo("Daybreak.Tests")]
[assembly: InternalsVisibleTo("DynamicProxyGenAssembly2")]
namespace Daybreak.Launch;
public partial class Launcher
{
public static void LaunchSequence(string[] args, IServiceProvider bootstrap, PluginConfigurationBase? platformConfiguration = null)
{
var scopedLogger = bootstrap.GetRequiredService<ILogger<Launcher>>().CreateScopedLogger();
scopedLogger.LogDebug("Starting Daybreak Launcher...");
var builder = CreateMainBuilder(args);
var pluginsService = bootstrap.GetRequiredService<IPluginsService>();
var loadedPlugins = pluginsService.LoadPlugins();
var configurations = loadedPlugins.Select<AvailablePlugin, (string, PluginConfigurationBase?, AvailablePlugin?)>(p => (p.Name, p.Configuration, (AvailablePlugin?)p))
.Prepend(("Daybreak Core", new ProjectConfiguration(), default)).ToList();
// Add platform-specific configuration if provided
if (platformConfiguration is not null)
{
configurations = configurations.Prepend(("Platform-Specific", platformConfiguration, default)).ToList();
}
var menuEntryProducer = new MenuProducer(bootstrap.GetRequiredService<ILogger<MenuProducer>>());
foreach (var (pluginName, configuration, plugin) in configurations)
{
if (configuration is null)
{
scopedLogger.LogWarning("No configuration found for plugin {PluginName}, skipping...", pluginName);
continue;
}
LoadConfiguration(pluginName, bootstrap, builder, configuration, menuEntryProducer);
}
builder.Services.AddSingleton<IReadOnlyDictionary<string, MenuCategory>>(menuEntryProducer.categories.AsReadOnly());
var mainApp = CreateMainApp(builder, args);
// Allow platform-specific and plugin configurations to customize the window
foreach (var (pluginName, configuration, plugin) in configurations)
{
if (configuration is null)
{
continue;
}
try
{
configuration.ConfigureWindow(mainApp);
}
catch (Exception ex)
{
scopedLogger.LogError(ex, "An error occurred while configuring window for {PluginName}", pluginName);
}
}
foreach (var (pluginName, configuration, plugin) in configurations)
{
if (configuration is null)
{
scopedLogger.LogWarning("No configuration found for plugin {PluginName}, skipping theme registration...", pluginName);
continue;
}
RegisterThemes(bootstrap, configuration, scopedLogger);
}
scopedLogger.LogInformation("Registering loaded plugins");
var finalPluginService = mainApp.Services.GetRequiredService<IPluginsService>();
finalPluginService.UpdateLoadedPlugins(loadedPlugins);
var theme = mainApp.Services.GetRequiredService<GameScreenshotsTheme>();
Theme.Themes.Add(theme);
var keyboardHook = mainApp.Services.GetRequiredService<IKeyboardHookService>();
keyboardHook.Start();
ExecuteArgumentHandlers(mainApp, args);
RunMainApp(mainApp);
}
private static void ExecuteArgumentHandlers(PhotinoBlazorApp app, string[] args)
{
var applicationArgumentService = app.Services.GetRequiredService<IApplicationArgumentService>();
applicationArgumentService.HandleArguments(args);
}
private static void LoadConfiguration(
string name,
IServiceProvider bootstrap,
PhotinoBlazorAppBuilder builder,
PluginConfigurationBase configuration,
MenuProducer menuProducer)
{
var scopedLogger = bootstrap.GetRequiredService<ILogger<Launcher>>().CreateScopedLogger();
scopedLogger.LogInformation("Loading configuration for {Configuration.Name}", name);
RegisterServices(builder, configuration, scopedLogger);
RegisterOptions(bootstrap, builder, configuration, scopedLogger);
RegisterViews(bootstrap, builder, configuration, scopedLogger);
RegisterMods(bootstrap, builder, configuration, scopedLogger);
RegisterStartupActions(bootstrap, builder, configuration, scopedLogger);
RegisterPostUpdateActions(bootstrap, builder, configuration, scopedLogger);
RegisterNotificationHandlers(bootstrap, builder, configuration, scopedLogger);
RegisterArgumentHandlers(bootstrap, builder, configuration, scopedLogger);
RegisterMenuEntries(configuration, menuProducer, scopedLogger);
RegisterFileProviders(bootstrap, builder, configuration, scopedLogger);
scopedLogger.LogInformation("Finished loading configuration for {Configuration.Name}", name);
}
private static void RegisterServices(PhotinoBlazorAppBuilder builder, PluginConfigurationBase configuration, ScopedLogger<Launcher> scopedLogger)
{
try
{
configuration.RegisterServices(builder.Services);
}
catch (Exception ex)
{
scopedLogger.LogError(ex, "An error occurred while registering services");
}
}
private static void RegisterOptions(IServiceProvider bootstrap, PhotinoBlazorAppBuilder builder, PluginConfigurationBase configuration, ScopedLogger<Launcher> scopedLogger)
{
try
{
var optionsProducer = new OptionProducer(builder.Services, bootstrap.GetRequiredService<ILogger<OptionProducer>>());
configuration.RegisterOptions(optionsProducer);
}
catch (Exception ex)
{
scopedLogger.LogError(ex, "An error occurred while registering options");
}
}
private static void RegisterViews(IServiceProvider bootstrap, PhotinoBlazorAppBuilder builder, PluginConfigurationBase configuration, ScopedLogger<Launcher> scopedLogger)
{
try
{
var viewProducer = new TrailBlazrViewProducer(builder.Services, bootstrap.GetRequiredService<ILogger<TrailBlazrViewProducer>>());
configuration.RegisterViews(viewProducer);
}
catch (Exception ex)
{
scopedLogger.LogError(ex, "An error occurred while registering views");
}
}
private static void RegisterMods(IServiceProvider bootstrap, PhotinoBlazorAppBuilder builder, PluginConfigurationBase configuration, ScopedLogger<Launcher> scopedLogger)
{
try
{
var modsProducer = new ModsProducer(builder.Services, bootstrap.GetRequiredService<ILogger<ModsProducer>>());
configuration.RegisterMods(modsProducer);
}
catch (Exception ex)
{
scopedLogger.LogError(ex, "An error occurred while registering mods");
}
}
private static void RegisterStartupActions(IServiceProvider bootstrap, PhotinoBlazorAppBuilder builder, PluginConfigurationBase configuration, ScopedLogger<Launcher> scopedLogger)
{
try
{
var startupActionProducer = new StartupActionsProducer(builder.Services, bootstrap.GetRequiredService<ILogger<StartupActionsProducer>>());
configuration.RegisterStartupActions(startupActionProducer);
}
catch (Exception ex)
{
scopedLogger.LogError(ex, "An error occurred while registering startup actions");
}
}
private static void RegisterPostUpdateActions(IServiceProvider bootstrap, PhotinoBlazorAppBuilder builder, PluginConfigurationBase configuration, ScopedLogger<Launcher> scopedLogger)
{
try
{
var postUpdateActionProducer = new PostUpdateActionProducer(builder.Services, bootstrap.GetRequiredService<ILogger<PostUpdateActionProducer>>());
configuration.RegisterPostUpdateActions(postUpdateActionProducer);
}
catch (Exception ex)
{
scopedLogger.LogError(ex, "An error occurred while registering post-update actions");
}
}
private static void RegisterNotificationHandlers(IServiceProvider bootstrap, PhotinoBlazorAppBuilder builder, PluginConfigurationBase configuration, ScopedLogger<Launcher> scopedLogger)
{
try
{
var notificationHandlerProducer = new NotificationHandlerProducer(builder.Services, bootstrap.GetRequiredService<ILogger<NotificationHandlerProducer>>());
configuration.RegisterNotificationHandlers(notificationHandlerProducer);
}
catch (Exception ex)
{
scopedLogger.LogError(ex, "An error occurred while registering notification handlers");
}
}
private static void RegisterArgumentHandlers(IServiceProvider bootstrap, PhotinoBlazorAppBuilder builder, PluginConfigurationBase configuration, ScopedLogger<Launcher> scopedLogger)
{
try
{
var argumentHandlerProducer = new ArgumentHandlerProducer(builder.Services, bootstrap.GetRequiredService<ILogger<ArgumentHandlerProducer>>());
configuration.RegisterLaunchArgumentHandlers(argumentHandlerProducer);
}
catch (Exception ex)
{
scopedLogger.LogError(ex, "An error occurred while registering argument handlers");
}
}
private static void RegisterThemes(IServiceProvider bootstrap, PluginConfigurationBase configuration, ScopedLogger<Launcher> scopedLogger)
{
try
{
var themeProducer = new ThemeProducer(bootstrap.GetRequiredService<ILogger<ThemeProducer>>());
configuration.RegisterThemes(themeProducer);
}
catch (Exception ex)
{
scopedLogger.LogError(ex, "An error occurred while registering themes");
}
}
private static void RegisterMenuEntries(PluginConfigurationBase configuration, MenuProducer menuProducer, ScopedLogger<Launcher> scopedLogger)
{
try
{
configuration.RegisterMenuButtons(menuProducer);
}
catch (Exception ex)
{
scopedLogger.LogError(ex, "An error occurred while registering menu entries");
}
}
private static void RegisterFileProviders(IServiceProvider bootstrap, PhotinoBlazorAppBuilder builder, PluginConfigurationBase configuration, ScopedLogger<Launcher> scopedLogger)
{
try
{
var fileProviderProducer = new AssemblyFileProviderProducer(builder.Services, bootstrap.GetRequiredService<ILogger<AssemblyFileProviderProducer>>());
configuration.RegisterProviderAssemblies(fileProviderProducer);
}
catch (Exception ex)
{
scopedLogger.LogError(ex, "An error occurred while registering file providers");
}
}
}

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