Compare commits

..
227 Commits
Author SHA1 Message Date
amacocian 384fdaf67c Fix tests (#1487) 2026-02-26 14:13:40 -08:00
amacocian a48cd84e1a Remove character flag when provided by launch config (Closes #1480) (#1485) 2026-02-26 14:13:40 -08:00
amacocianandAlexandru Macocian 97fed608d2 Update GWCA (#1484)
* Improve GWCA bindings generator based on headers

* Move all structs to gwca

* Progress

* Complete parser work

* More generator adjustments

* GWCA fixes

* GWCA improvements

* GWCA fixes

* GWCA fixes

* GWCA fixes

* GWCA fixes

* Fix API usage of GWCA

* Update GWCA

---------

Co-authored-by: Alexandru Macocian <amacocian@microsoft.com>
2026-02-26 14:13:40 -08:00
amacocian 68f241989a Update GWCA (Closes #1477 Closes #1474) (#1482) 2026-02-26 14:13:40 -08:00
amacocian 123d76fe0f Fix stackoverflow risk on ScreenshotService (Closes #1476) (#1481) 2026-02-26 14:13:40 -08:00
Brendan McCoyandamacocian 9d495a35a6 Add Interaction and ModelFileID to Daybreak API InventoryService (#1478)
* Add Interaction and ModelFileId properties to ItemEntry

Signed-off-by: Brendan McCoy  <13607368+brendan-mccoy@users.noreply.github.com>

* Add Interaction and ModelFileId to inventory items

Signed-off-by: Brendan McCoy  <13607368+brendan-mccoy@users.noreply.github.com>

---------

Signed-off-by: Brendan McCoy  <13607368+brendan-mccoy@users.noreply.github.com>
2026-02-26 14:13:40 -08:00
amacocian 5d9d59610a Templates rework (#1479)
* Setup templates rework

* Update docs

* Implement backwards compatible parsing

* Work

* Finish build templates
2026-02-26 14:13:40 -08:00
amacocian 0bdfe69e78 Update docs with details about templates (#1475) 2026-02-26 14:13:40 -08:00
github-actions[bot]andamacocian 8fd1584995 Bump version to 0.9.10.4 2026-02-26 14:13:40 -08:00
amacocian 5da5eea085 Build template manager bugfix (#1472) 2026-02-24 06:37:10 -08:00
amacocian fd9d8934f4 Update docs (Closes #1469) (#1471) 2026-02-24 06:37:10 -08:00
amacocian b9736f051a Cap attribute points in build editor (Closed #1467) (#1470) 2026-02-24 06:37:10 -08:00
amacocian 724e264b77 Builds synchronization view (Closes #1289) (#1468) 2026-02-24 06:37:10 -08:00
amacocian 4974d5f9ed Fix exceptions when mods are enabled but not installed (Closes #1459) (#1466) 2026-02-24 06:37:10 -08:00
amacocian aa1da65538 Ability to reorder launch configs (Closes #1460) (#1465) 2026-02-24 06:37:10 -08:00
amacocian e90ae48c40 Fix item upgrade collisions (Closes #1461) (#1464) 2026-02-24 06:37:10 -08:00
amacociangithub-actions[bot] <github-actions[bot]@users.noreply.github.com>dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>Alexandru Macocian
ef513e1230 Gwca bindings (#1462)
* Bump version to 0.9.10.1

* 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>

* Improve mod view responsiveness (#1443)

* Improve application launcher logging (Closes #1441) (#1444)

* Damage modifier 0x248 (Closes #1438) (#1445)

* Initial commit

* Setup generator

* Progress

* Update generator

* Initial commit

* Setup generator

* Progress

* Update generator

* Fix library loading

* Move code to gwca

* Fix leave party

* Disable console on Release

* Add more exports to gwca

* Upgrade gwca

* Fixes to character selector

* Fix GetWindowHandle

* Move character selector to frame based

* Fix config spamming

---------

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Alexandru Macocian <amacocian@microsoft.com>
2026-02-24 06:37:10 -08:00
github-actions[bot]andamacocian 72fcf64589 Bump version to 0.9.10.3 2026-02-24 06:37:10 -08:00
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
1552 changed files with 81439 additions and 55351 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 }}
+5
View File
@@ -35,6 +35,9 @@ build/
[Ll]og/
[Ll]ogs/
# Local NuGet packages folder (per-machine isolation)
.packages/
# Visual Studio 2015/2017 cache/options directory
.vs/
# Uncomment if you have tasks that create the project's static files in wwwroot
@@ -365,3 +368,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,12 @@ 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");
// Clear default file-based configuration sources that cause unnecessary reload events
builder.Configuration.Sources.Clear();
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;
@@ -1,67 +0,0 @@
using Daybreak.API.Models;
using Daybreak.API.Services;
using Daybreak.API.WebSockets;
using Daybreak.Shared.Models.Api;
using System.Buffers;
using System.Core.Extensions;
using System.Extensions.Core;
using System.Net.WebSockets;
namespace Daybreak.API.Controllers.WebSocket.MainPlayer;
public sealed class MainPlayerStateRoute(
MainPlayerService mainPlayerStateService,
ChatService chatService,
ILogger<MainPlayerStateRoute> logger)
: UpdateWebSocketRoute
{
private const int DefaultFrequency = 1000;
private readonly MainPlayerService mainPlayerStateService = mainPlayerStateService.ThrowIfNull();
private readonly ChatService chatService = chatService.ThrowIfNull();
private readonly ILogger<MainPlayerStateRoute> logger = logger.ThrowIfNull();
private CallbackRegistration? mainPlayerStateRegistration;
protected override int BufferSize => 256;
public override Task ExecuteAsync(WebSocketMessageType type, ReadOnlySequence<byte> data, CancellationToken cancellationToken)
{
return Task.CompletedTask;
}
public override async Task SocketAccepted(CancellationToken cancellationToken)
{
var scopedLogger = this.logger.CreateScopedLogger();
if (!int.TryParse(this.Context?.Request.Query["f"].FirstOrDefault(), out var freq))
{
freq = DefaultFrequency;
}
var freqTimeSpan = TimeSpan.FromMilliseconds(freq);
scopedLogger.LogInformation("WebSocket accepted {id} with frequency {frequency}", this.Context?.Connection.Id ?? string.Empty, freqTimeSpan);
this.mainPlayerStateRegistration = this.mainPlayerStateService.RegisterMainStateConsumer(freqTimeSpan, this.SendUpdate);
await this.chatService.AddMessageAsync(
message: $"{nameof(MainPlayerState)} subscriber added: {this.Context?.Connection.RemoteIpAddress?.ToString()}:{this.Context?.Connection.RemotePort} with frequency {freq}ms",
sender: "Daybreak.API",
channel: Channel.Whisper,
cancellationToken: cancellationToken);
}
public override async Task SocketClosed()
{
var scopedLogger = this.logger.CreateScopedLogger();
this.mainPlayerStateRegistration?.Dispose();
this.mainPlayerStateRegistration = default;
scopedLogger.LogInformation("WebSocket closed {id}", this.Context?.Connection.Id ?? string.Empty);
await this.chatService.AddMessageAsync(
message: $"{nameof(MainPlayerState)} subscriber removed: {this.Context?.Connection.RemoteIpAddress?.ToString()}:{this.Context?.Connection.RemotePort}",
sender: "Daybreak.API",
channel: Channel.Whisper,
cancellationToken: CancellationToken.None);
}
}
@@ -1,19 +0,0 @@
using Daybreak.API.Interop;
using System.Text.Json;
using System.Text.Json.Serialization;
namespace Daybreak.API.Converters;
public sealed class PointerValueConverter : JsonConverter<PointerValue>
{
public override PointerValue Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
{
var s = reader.GetString() ?? throw new JsonException("Expected string");
return PointerValue.Parse(s);
}
public override void Write(Utf8JsonWriter writer, PointerValue value, JsonSerializerOptions options)
{
writer.WriteStringValue(value.ToString());
}
}
+64 -21
View File
@@ -1,16 +1,15 @@
<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>
<EmitCompilerGeneratedFiles>true</EmitCompilerGeneratedFiles>
<EnableRequestDelegateGenerator>true</EnableRequestDelegateGenerator>
<Version>0.9.9.75</Version>
<PublishAot>true</PublishAot>
<SelfContained>true</SelfContained>
@@ -19,40 +18,84 @@
<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>
<Using Include="Daybreak.Generators" />
</ItemGroup>
<ItemGroup>
<EmbeddedResource Include="Configuration\appsettings.Debug.json" />
<EmbeddedResource Include="Configuration\appsettings.json" />
<EmbeddedResource Include="Configuration\appsettings.Release.json" />
</ItemGroup>
<ItemGroup>
<None Include="..\Dependencies\GWCA\gwca.dll">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
<Link>gwca.dll</Link>
</None>
<AdditionalFiles Include="..\Dependencies\GWCA\gwca.dll" />
</ItemGroup>
<ItemGroup>
<RdXmlFile Include="rd.xml" />
</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>
<ProjectReference Include="..\Daybreak.Shared\Daybreak.Shared.csproj" />
<ProjectReference Include="..\Daybreak.Generators\Daybreak.Generators.csproj"
OutputItemType="Analyzer"
ReferenceOutputAssembly="false"
SkipGetTargetFrameworkProperties="true"
UndefineProperties="RuntimeIdentifier;PublishAot;SelfContained;TargetFramework" />
</ItemGroup>
</Project>
+75 -5
View File
@@ -1,11 +1,14 @@
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using System.Extensions.Core;
using System.Logging;
using System.Net.NetworkInformation;
using System.Runtime.InteropServices;
using Daybreak.API.Configuration;
using Daybreak.API.Extensions;
using Daybreak.API.Health;
using Daybreak.API.Hosting;
using Daybreak.API.Interop;
using Daybreak.API.Logging;
using Daybreak.API.Serialization;
using Daybreak.API.Services;
@@ -23,6 +26,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 +44,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 +72,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 +85,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
@@ -92,13 +133,19 @@ public class EntryPoint
private static async Task StartServer(WebApplication app)
{
var scopedLogger = app.Services.GetRequiredService<ILogger<EntryPoint>>().CreateScopedLogger();
try
{
scopedLogger.LogDebug("Initializing GWCA");
PreloadNativeDependencies(scopedLogger);
var result = GWCA.GW.Initialize();
scopedLogger.LogDebug($"GWCA.GW.Initialize() returned: {result}");
await app.RunAsync();
}
catch (Exception ex)
{
Console.Error.WriteLine(ex);
scopedLogger.LogCritical(ex, $"Encountered fatal error while starting API server {ex}");
throw;
}
}
@@ -134,4 +181,27 @@ public class EntryPoint
return true;
}
private static void PreloadNativeDependencies(ScopedLogger<EntryPoint> logger)
{
foreach (ProcessModule module in Process.GetCurrentProcess().Modules)
{
if (module.ModuleName?.Contains("Daybreak.API", StringComparison.OrdinalIgnoreCase) is true)
{
var moduleDir = Path.GetDirectoryName(module.FileName)!;
var gwcaPath = Path.Combine(moduleDir, "gwca.dll");
if (File.Exists(gwcaPath))
{
NativeLibrary.Load(gwcaPath);
logger.LogDebug($"Preloaded gwca.dll from {gwcaPath}");
}
else
{
logger.LogError($"gwca.dll not found at {gwcaPath}");
}
break;
}
}
}
}
@@ -1,17 +1,18 @@
using Daybreak.API.Interop.GuildWars;
using Daybreak.Shared.Models.Api;
using ZLinq;
namespace Daybreak.API.Extensions;
public static class AttributeContextExtensions
{
public static List<AttributeEntry> GetAttributeEntryList(this AttributeContext[] attributes)
public static IEnumerable<AttributeEntry> GetAttributeEntryList(this AttributeStructArray54 attributes)
{
return attributes
.AsValueEnumerable()
.Take(45) // There are only 45 maximum attributes in game
.Where(a => a.LevelBase > 0 && a.Level >= a.LevelBase) // Select only attributes with points in them
.Select(a => new AttributeEntry(a.Id, a.LevelBase, a.Level)).ToList();
foreach (var attribute in attributes)
{
if (attribute.LevelBase > 0 && attribute.Level >= attribute.LevelBase)
{
yield return new AttributeEntry((uint)attribute.Id, attribute.LevelBase, attribute.Level);
}
}
}
}
@@ -0,0 +1,36 @@
using Daybreak.API.Interop.GuildWars;
namespace Daybreak.API.Extensions;
public static class CharacterInformationExtensions
{
public unsafe static Profession GetPrimaryProfession(this CharacterInformation characterInformation)
{
return (Profession)((characterInformation.Props[2] >> 20) & 0xF);
}
public unsafe static Profession GetSecondaryProfession(this CharacterInformation characterInformation)
{
return (Profession)((characterInformation.Props[7] >> 10) & 0xF);
}
public unsafe static uint GetLevel(this CharacterInformation characterInformation)
{
return (characterInformation.Props[7] >> 4) & 0x3F;
}
public unsafe static Campaign GetCampaign(this CharacterInformation characterInformation)
{
return (Campaign)(characterInformation.Props[7] & 0xF);
}
public unsafe static MapID GetMapId(this CharacterInformation characterInformation)
{
return (MapID)((characterInformation.Props[0] >> 16) & 0xFFFF);
}
public unsafe static bool IsPvP(this CharacterInformation characterInformation)
{
return ((characterInformation.Props[7] >> 9) & 0x1) == 0x1;
}
}
@@ -1,4 +1,5 @@
using Daybreak.API.Interop.GuildWars;
using Daybreak.API.Interop;
using Daybreak.API.Interop.GuildWars;
using System.Diagnostics.CodeAnalysis;
namespace Daybreak.API.Extensions;
@@ -11,21 +12,21 @@ public static unsafe class GameContextExtensions
{
playerId = 0;
if (gameContext.IsNull ||
gameContext.Pointer->WorldContext is null ||
gameContext.Pointer->WorldContext->PlayerControlledChar is null)
gameContext.Pointer->World is null ||
gameContext.Pointer->World->PlayerControlledChar is null)
{
return false;
}
playerId = gameContext.Pointer->WorldContext->PlayerControlledChar->AgentId;
playerId = gameContext.Pointer->World->PlayerControlledChar->AgentId;
return true;
}
public static bool TryGetBuildContext(
this WrappedPointer<GameContext> gameContext,
[NotNullWhen(true)] out GuildWarsArray<SkillbarContext>? skillbars,
[NotNullWhen(true)] out GuildWarsArray<SkillbarData>? skillbars,
[NotNullWhen(true)] out GuildWarsArray<PartyAttribute>? attributes,
[NotNullWhen(true)] out GuildWarsArray<ProfessionsContext>? professions,
[NotNullWhen(true)] out GuildWarsArray<ProfessionState>? professions,
[NotNullWhen(true)] out GuildWarsArray<uint>? unlockedSkills)
{
skillbars = default;
@@ -33,15 +34,15 @@ public static unsafe class GameContextExtensions
professions = default;
unlockedSkills = default;
if (gameContext.IsNull ||
gameContext.Pointer->WorldContext is null)
gameContext.Pointer->World is null)
{
return false;
}
skillbars = gameContext.Pointer->WorldContext->Skillbars;
attributes = gameContext.Pointer->WorldContext->Attributes;
professions = gameContext.Pointer->WorldContext->Professions;
unlockedSkills = gameContext.Pointer->WorldContext->UnlockedCharacterSkills;
skillbars = gameContext.Pointer->World->Skillbar.Value;
attributes = gameContext.Pointer->World->Attributes.Value;
professions = gameContext.Pointer->World->PartyProfessionStates;
unlockedSkills = gameContext.Pointer->World->UnlockedCharacterSkills;
return true;
}
@@ -57,15 +58,16 @@ public static unsafe class GameContextExtensions
heroes = default;
henchmen = default;
if (gameContext.IsNull ||
gameContext.Pointer->PartyContext is null)
gameContext.Pointer->Party is 0)
{
return false;
}
partyId = gameContext.Pointer->PartyContext->PlayerParty->PartyId;
players = gameContext.Pointer->PartyContext->PlayerParty->Players;
heroes = gameContext.Pointer->PartyContext->PlayerParty->Heroes;
henchmen = gameContext.Pointer->PartyContext->PlayerParty->Henchmen;
var partyInfo = GWCA.GW.PartyMgr.GetPartyInfo(0);
partyId = partyInfo->PartyId;
players = partyInfo->Players;
heroes = partyInfo->Heroes;
henchmen = partyInfo->Henchmen;
return true;
}
@@ -75,27 +77,27 @@ public static unsafe class GameContextExtensions
{
heroFlags = default;
if (gameContext.IsNull ||
gameContext.Pointer->WorldContext is null)
gameContext.Pointer->World is null)
{
return false;
}
heroFlags = gameContext.Pointer->WorldContext->HeroFlags;
heroFlags = gameContext.Pointer->World->HeroFlags.Value;
return true;
}
public static bool TryGetAccountContext(
this WrappedPointer<GameContext> gameContext,
out WrappedPointer<AccountGameContext> accountContext)
out WrappedPointer<AccountContext> accountContext)
{
accountContext = default;
if (gameContext.IsNull ||
gameContext.Pointer->AccountContext is null)
gameContext.Pointer->Account is null)
{
return false;
}
accountContext = gameContext.Pointer->AccountContext;
accountContext = gameContext.Pointer->Account;
return true;
}
}
@@ -0,0 +1,16 @@
using Daybreak.API.Interop.GuildWars;
namespace Daybreak.API.Extensions;
public static class TitleExtensions
{
public static bool IsPercentageBased(this Title title)
{
return (title.Props & 1) != 0;
}
public static bool HasTiers(this Title title)
{
return (title.Props & 3) == 2;
}
}
-31
View File
@@ -1,31 +0,0 @@
using Daybreak.API.Serialization;
using Daybreak.API.Services.Interop;
using Microsoft.Extensions.Diagnostics.HealthChecks;
using System.Text.Json;
using ZLinq;
namespace Daybreak.API.Health;
public sealed class AddressHealthCheck(IEnumerable<IAddressHealthService> addressHealthServices)
: IHealthCheck
{
private readonly IEnumerable<IAddressHealthService> addressHealthServices = addressHealthServices;
public Task<HealthCheckResult> CheckHealthAsync(HealthCheckContext context, CancellationToken cancellationToken = default)
{
var states = this.addressHealthServices.AsValueEnumerable().SelectMany(addressService => addressService.GetAddressStates());
var healthy = !states.Any(s => s.Address == 0x0);
using var statesArray = states.ToArrayPool();
var serialized = JsonSerializer.SerializeToElement(statesArray.ArraySegment, ApiJsonSerializerContext.Default.ArraySegmentAddressState);
var meta = new Dictionary<string, object>
{
["Count"] = statesArray.Size,
["Invalid"] = states.Count(s => s.Address == 0x0),
["States"] = serialized,
};
return Task.FromResult(healthy
? HealthCheckResult.Healthy("Addresses are initialized", meta)
: HealthCheckResult.Unhealthy("Addresses are not initialized", data: meta));
}
}
-31
View File
@@ -1,31 +0,0 @@
using Daybreak.API.Serialization;
using Daybreak.API.Services.Interop;
using Microsoft.Extensions.Diagnostics.HealthChecks;
using System.Text.Json;
using ZLinq;
namespace Daybreak.API.Health;
public sealed class HooksHealthCheck(IEnumerable<IHookHealthService> hookHealthServices)
: IHealthCheck
{
private readonly IEnumerable<IHookHealthService> hookHealthServices = hookHealthServices;
public Task<HealthCheckResult> CheckHealthAsync(HealthCheckContext context, CancellationToken cancellationToken = default)
{
var states = this.hookHealthServices.AsValueEnumerable().SelectMany(hookService => hookService.GetHookStates());
var healthy = !states.Any(s => !s.Hooked);
var statesArray = states.ToArrayPool();
var serialized = JsonSerializer.SerializeToElement(statesArray.ArraySegment, ApiJsonSerializerContext.Default.ArraySegmentHookState);
var meta = new Dictionary<string, object>
{
["Count"] = statesArray.Size,
["Unhooked"] = states.Count(s => !s.Hooked),
["States"] = serialized,
};
return Task.FromResult(healthy
? HealthCheckResult.Healthy("Hooks are initialized", meta)
: HealthCheckResult.Unhealthy("Hooks are not initialized", data: meta));
}
}
@@ -4,9 +4,7 @@ public static class WebApplicationBuilderExtensions
{
public static WebApplicationBuilder WithHealthChecks(this WebApplicationBuilder builder)
{
builder.Services.AddHealthChecks()
.AddCheck<HooksHealthCheck>(nameof(HooksHealthCheck))
.AddCheck<AddressHealthCheck>(nameof(AddressHealthCheck));
builder.Services.AddHealthChecks();
return builder;
}
}
+34
View File
@@ -0,0 +1,34 @@
using System.Runtime.InteropServices;
namespace Daybreak.API.Interop;
[StructLayout(LayoutKind.Explicit, Pack = 1, Size = 0x1AC)]
[GWCAEquivalent("Frame")]
public readonly struct Frame
{
[FieldOffset(0x0008)]
public readonly uint FrameLayout;
[FieldOffset(0x0018)]
public readonly uint VisibilityFlags;
[FieldOffset(0x0020)]
public readonly uint Type;
[FieldOffset(0x0024)]
public readonly uint TemplateType;
[FieldOffset(0x00b8)]
public readonly uint ChildOffsetId;
[FieldOffset(0x00bc)]
public readonly uint FrameId;
[FieldOffset(0x018C)]
public readonly uint FrameState;
public bool IsCreated => (this.FrameState & 0x4) != 0;
public bool IsHidden => (this.FrameState & 0x200) != 0;
public bool IsVisible => !this.IsHidden;
public bool IsDIsabled => (this.FrameState & 0x10) != 0;
}
-26
View File
@@ -1,26 +0,0 @@
using System.Core.Extensions;
namespace Daybreak.API.Interop;
public sealed class GWAddressCache(Func<nuint> provider)
{
private readonly Func<nuint> provider = provider.ThrowIfNull();
private nuint? cachedAddress;
public nuint? GetAddress()
{
if (this.cachedAddress.HasValue)
{
return this.cachedAddress.Value;
}
var addr = this.provider();
if (addr is 0x0)
{
return null;
}
this.cachedAddress = addr;
return addr;
}
}
File diff suppressed because it is too large Load Diff
-19
View File
@@ -1,19 +0,0 @@
using System.Runtime.InteropServices;
namespace Daybreak.API.Interop;
public sealed class GWDelegateCache<TDelegate>(GWAddressCache cache)
where TDelegate : Delegate
{
public GWAddressCache Cache { get; } = cache;
public TDelegate? GetDelegate()
{
if (this.Cache.GetAddress() is not nuint address)
{
return default;
}
return Marshal.GetDelegateForFunctionPointer<TDelegate>((nint)address);
}
}
-240
View File
@@ -1,240 +0,0 @@
using System.ComponentModel;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
namespace Daybreak.API.Interop;
public abstract class GWFastCall(GWAddressCache target)
{
public readonly struct Void { }
public GWAddressCache Cache { get; } = target;
public abstract void Initialize();
protected unsafe static nint BuildFastCallThunk(nuint target)
{
if (target is 0)
{
return 0;
}
var code = stackalloc byte[13]
{
0x58, // pop eax (ret)
0x59, // pop ecx (ctx)
0x5A, // pop edx (edxVal)
0x5B, // pop ebx (wparam)
0x53, // push ebx (wparam)
0x50, // push eax (ret)
0xB8,0,0,0,0, // mov eax, TARGET
0xFF,0xE0 // jmp eax
}; // 13 bytes total
const int TARGET_OFFSET = 7;
Unsafe.WriteUnaligned(ref code[TARGET_OFFSET], (uint)target);
var mem = NativeMethods.VirtualAlloc(0, 13, NativeMethods.MEM_COMMIT | NativeMethods.MEM_RESERVE, NativeMethods.PAGE_EXECUTE_READWRITE);
if (mem is 0)
{
throw new Win32Exception(Marshal.GetLastWin32Error(), "VirtualAlloc");
}
Buffer.MemoryCopy(code, (void*)mem, 13, 13);
return mem;
}
}
public unsafe sealed class GWFastCall<T1, T2, T3>(GWAddressCache target) : GWFastCall(target)
{
private delegate* unmanaged[Stdcall]<T1, T2, T3> func = null;
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public unsafe T3? Invoke(T1 t1, T2 t2)
{
if (this.func is null)
{
this.Initialize();
}
if (this.func is not null)
{
if (typeof(T3) == typeof(Void))
{
((delegate* unmanaged[Stdcall]<T1, T2, void>)this.func)(t1, t2);
return default;
}
return this.func(t1, t2);
}
return default;
}
public override void Initialize()
{
if (this.Cache.GetAddress() is not nuint addr ||
addr is 0)
{
return;
}
var call = BuildFastCallThunk(addr);
this.func = (delegate* unmanaged[Stdcall]<T1, T2, T3>)call;
}
}
public unsafe sealed class GWFastCall<T1, T2, T3, T4>(GWAddressCache target) : GWFastCall(target)
{
private delegate* unmanaged[Stdcall]<T1, T2, T3, T4> func = null;
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public unsafe T4? Invoke(T1 t1, T2 t2, T3 t3)
{
if (this.func is null)
{
this.Initialize();
}
if (this.func is not null)
{
if (typeof(T4) == typeof(Void))
{
((delegate* unmanaged[Stdcall]<T1, T2, T3, void>)this.func)(t1, t2, t3);
return default;
}
return this.func(t1, t2, t3);
}
return default;
}
public override void Initialize()
{
if (this.Cache.GetAddress() is not nuint addr ||
addr is 0)
{
return;
}
var call = BuildFastCallThunk(addr);
this.func = (delegate* unmanaged[Stdcall]<T1, T2, T3, T4>)call;
}
}
public unsafe sealed class GWFastCall<T1, T2, T3, T4, T5>(GWAddressCache target) : GWFastCall(target)
{
private delegate* unmanaged[Stdcall]<T1, T2, T3, T4, T5> func = null;
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public unsafe T5? Invoke(T1 t1, T2 t2, T3 t3, T4 t4)
{
if (this.func is null)
{
this.Initialize();
}
if (this.func is not null)
{
if (typeof(T5) == typeof(Void))
{
((delegate* unmanaged[Stdcall]<T1, T2, T3, T4, void>)this.func)(t1, t2, t3, t4);
return default;
}
return this.func(t1, t2, t3, t4);
}
return default;
}
public override void Initialize()
{
if (this.Cache.GetAddress() is not nuint addr ||
addr is 0)
{
return;
}
var call = BuildFastCallThunk(addr);
this.func = (delegate* unmanaged[Stdcall]<T1, T2, T3, T4, T5>)call;
}
}
public unsafe sealed class GWFastCall<T1, T2, T3, T4, T5, T6>(GWAddressCache target) : GWFastCall(target)
{
private delegate* unmanaged[Stdcall]<T1, T2, T3, T4, T5, T6> func = null;
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public unsafe T6? Invoke(T1 t1, T2 t2, T3 t3, T4 t4, T5 t5)
{
if (this.func is null)
{
this.Initialize();
}
if (this.func is not null)
{
if (typeof(T6) == typeof(Void))
{
((delegate* unmanaged[Stdcall]<T1, T2, T3, T4, T5, void>)this.func)(t1, t2, t3, t4, t5);
return default;
}
return this.func(t1, t2, t3, t4, t5);
}
return default;
}
public override void Initialize()
{
if (this.Cache.GetAddress() is not nuint addr ||
addr is 0)
{
return;
}
var call = BuildFastCallThunk(addr);
this.func = (delegate* unmanaged[Stdcall]<T1, T2, T3, T4, T5, T6>)call;
}
}
public unsafe sealed class GWFastCall<T1, T2, T3, T4, T5, T6, T7>(GWAddressCache target) : GWFastCall(target)
{
private delegate* unmanaged[Stdcall]<T1, T2, T3, T4, T5, T6, T7> func = null;
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public unsafe T7? Invoke(T1 t1, T2 t2, T3 t3, T4 t4, T5 t5, T6 t6)
{
if (this.func is null)
{
this.Initialize();
}
if (this.func is not null)
{
if (typeof(T7) == typeof(Void))
{
((delegate* unmanaged[Stdcall]<T1, T2, T3, T4, T5, T6, void>)this.func)(t1, t2, t3, t4, t5, t6);
return default;
}
return this.func(t1, t2, t3, t4, t5, t6);
}
return default;
}
public override void Initialize()
{
if (this.Cache.GetAddress() is not nuint addr ||
addr is 0)
{
return;
}
var call = BuildFastCallThunk(addr);
this.func = (delegate* unmanaged[Stdcall]<T1, T2, T3, T4, T5, T6, T7>)call;
}
}
-122
View File
@@ -1,122 +0,0 @@
using System.Runtime.InteropServices;
using MinHook;
namespace Daybreak.API.Interop;
/// <summary>
/// A thin convenience wrapper around MinHook that
/// resolves the target address lazily through <see cref="GWAddressCache"/>
/// unwraps an existing “CALL/JMP rel32” stub so we patch the real code
/// exposes <see cref="Original"/> so your detour can chain
/// </summary>
public sealed class GWHook<T>(
GWAddressCache funcAddress,
T detour,
bool bypassPreviousHooks = false) : IHook<T>
where T : Delegate
{
private readonly bool bypassPreviousHooks = bypassPreviousHooks;
private readonly GWAddressCache addressCache = funcAddress ?? throw new ArgumentNullException(nameof(funcAddress));
private readonly T detour = detour ?? throw new ArgumentNullException(nameof(detour));
private readonly SemaphoreSlim semaphore = new(1, 1);
private T? cont; // trampoline returned by MinHook
private HookEngine? engine;
/// <summary>Delegate that calls the next hook / real function.</summary>
public T Continue => this.cont ??
throw new InvalidOperationException("Hook not initialised call EnsureInitialized() first.");
public bool Hooked { get; private set; } = false;
public nuint TargetAddress { get; private set; }
public nuint ContinueAddress { get; private set; }
public nuint DetourAddress { get; private set; }
/// <summary>Installs the hook exactly once (thread-safe).</summary>
public bool EnsureInitialized()
{
if (this.Hooked)
{
return true;
}
this.semaphore.Wait();
try
{
if (this.Hooked)
{
return true;
}
var addr = this.addressCache.GetAddress();
if (addr.HasValue is false ||
addr.Value is 0)
{
return false;
}
var target = this.bypassPreviousHooks
? UnwrapNearBranch(addr.Value)
: addr.Value;
if (target is 0)
{
return false;
}
this.engine = new HookEngine();
this.cont = this.engine.CreateHook((nint)target, this.detour);
this.engine.EnableHooks();
this.TargetAddress = target;
this.ContinueAddress = (nuint)this.cont.Method.MethodHandle.GetFunctionPointer();
this.DetourAddress = (nuint)this.detour.Method.MethodHandle.GetFunctionPointer();
this.Hooked = true;
return true;
}
catch (Exception)
{
return false;
}
finally
{
this.semaphore.Release();
}
}
public void Dispose()
{
this.semaphore.Wait();
try
{
if (!this.Hooked ||
this.engine is null)
{
return;
}
this.engine.DisableHooks();
this.engine.Dispose();
this.cont = null;
this.Hooked = false;
}
finally
{
this.semaphore.Release();
}
}
/// <summary>
/// Follows a <c>CALL rel32</c> (<c>E8</c>) **or** <c>JMP rel32</c> (<c>E9</c>)
/// trampoline that may have been planted by a previous hook.
/// </summary>
private static unsafe nuint UnwrapNearBranch(nuint addr)
{
byte op = Marshal.ReadByte((IntPtr)addr);
if (op != 0xE8 && op != 0xE9) // not CALL/JMP rel32
return addr;
int rel = Marshal.ReadInt32((IntPtr)(addr + 1));
long dst = (long)addr + 5 + rel; // 5-byte instruction
return (nuint)dst;
}
}
+10
View File
@@ -0,0 +1,10 @@
using System.Runtime.InteropServices;
namespace Daybreak.API.Interop;
[StructLayout(LayoutKind.Sequential, Pack = 1)]
[GWCAEquivalent("HWND__")]
public readonly struct GWHwnd
{
public readonly nint Handle;
}
@@ -1,26 +0,0 @@
using System.Runtime.InteropServices;
namespace Daybreak.API.Interop.GuildWars;
[StructLayout(LayoutKind.Explicit, Pack = 1, Size = 0xC)]
public readonly struct AccountUnlockedCount
{
[FieldOffset(0x0000)]
public readonly uint Id;
}
[StructLayout(LayoutKind.Explicit, Pack = 1, Size = 0x138)]
public readonly struct AccountGameContext
{
[FieldOffset(0x0000)]
public readonly GuildWarsArray<AccountUnlockedCount> AccountUnlockedCounts;
[FieldOffset(0x00b4)]
public readonly GuildWarsArray<uint> UnlockedPvpHeroes;
[FieldOffset(0x0124)]
public readonly GuildWarsArray<uint> UnlockedAccountSkills;
[FieldOffset(0x0134)]
public readonly uint AccountFlags;
}
@@ -1,15 +0,0 @@
using System.Runtime.InteropServices;
namespace Daybreak.API.Interop.GuildWars;
[StructLayout(LayoutKind.Sequential, Pack = 1, Size = 0x1C)]
public readonly unsafe struct AccountInfoContext
{
public readonly char* AccountName;
public readonly uint Wins;
public readonly uint Losses;
public readonly uint Rating;
public readonly uint QualifierPoints;
public readonly uint Rank;
public readonly uint TournamentRewardPoints;
}
-247
View File
@@ -1,247 +0,0 @@
using System.Numerics;
using System.Runtime.InteropServices;
namespace Daybreak.API.Interop.GuildWars;
[Flags]
public enum AgentType : uint
{
Living = 0xDB,
Gadget = 0x200,
Item = 0x400
}
[Flags]
public enum AgentModelType : ushort
{
NPC = 0x2000,
Player = 0x3000
}
[Flags]
public enum LivingAgentState : uint
{
None = 0,
InCombatStance = 0x000001,
HasQuest = 0x000002,
Dead = 0x000008,
Female = 0x000200,
HasBossGlow = 0x000400,
HidingCape = 0x001000,
CanBeViewedInPartyWindow = 0x020000,
SpawnedSpirit = 0x040000,
BeingObservedOrPlayer = 0x400000
}
public enum LivingAgentAllegiance : byte
{
Ally_NonAttackable = 0x1,
Neutral = 0x2,
Enemy = 0x3,
Spirit_Pet = 0x4,
Minion = 0x5,
Npc_Minipet = 0x6
};
[Flags]
public enum LivingAgentEffects : uint
{
None = 0,
Bleeding = 0x0001,
Conditioned = 0x0002,
Crippled1 = 0x0008, // Part of the Crippled check
Crippled2 = 0x0002, // Part of the Crippled check
Crippled = 0x000A, // Combined flag for crippled (0x0008 | 0x0002)
Dead = 0x0010,
DeepWound = 0x0020,
Poisoned = 0x0040,
Enchanted = 0x0080,
DegenHexed = 0x0400,
Hexed = 0x0800,
WeaponSpelled = 0x8000
}
public enum LivingAgentModelState : ushort
{
// Idle states
Idle1 = 64,
Idle2 = 68,
Idle3 = 100,
// Casting states
Casting1 = 65,
Casting2 = 581,
// Moving states
Moving1 = 12,
Moving2 = 76,
Moving3 = 204,
// Attacking states
Attacking1 = 96,
Attacking2 = 1088,
Attacking3 = 1120,
// Other states
KnockedDown = 1104
}
[StructLayout(LayoutKind.Sequential, Pack = 1)]
public readonly struct AgentEffects
{
public readonly uint AgentId;
public readonly GuildWarsArray<Buff> Buffs;
public readonly GuildWarsArray<Effect> Effects;
}
[StructLayout(LayoutKind.Explicit, Pack = 1)]
public readonly struct AgentGameContext
{
[FieldOffset(0x01AC)]
public readonly uint InstanceTimer;
}
[StructLayout(LayoutKind.Explicit, Pack = 1, Size = 196)]
public readonly struct AgentContext
{
[FieldOffset(0x0014)]
public readonly uint AgentInstanceTimer;
[FieldOffset(0x002C)]
public readonly uint AgentId;
[FieldOffset(0x0074)]
public readonly GamePos Pos;
[FieldOffset(0x009C)]
public readonly AgentType Type;
[FieldOffset(0x00A0)]
public readonly Vector2 Velocity;
}
[StructLayout(LayoutKind.Explicit, Pack = 1, Size = 212)]
public readonly struct AgentItemContext
{
[FieldOffset(0x00C4)]
public readonly uint OwnerId;
[FieldOffset(0x00C8)]
public readonly uint ItemId;
[FieldOffset(0x00D0)]
public readonly uint ExtraType;
}
[StructLayout(LayoutKind.Explicit, Pack = 1, Size = 228)]
public readonly struct AgentGadgetContext
{
[FieldOffset(0x00CC)]
public readonly uint ExtraType;
[FieldOffset(0x00D0)]
public readonly uint GadgetId;
}
[StructLayout(LayoutKind.Explicit, Pack = 1, Size = 0x1C0)]
public readonly unsafe struct AgentLivingContext
{
[FieldOffset(0x00C4)]
public readonly uint OwnerId;
[FieldOffset(0x00F4)]
public readonly ushort AgentType; // All non-player have an identifier. Two of the same mob have the same identifier;
[FieldOffset(0x00F6)]
public readonly AgentModelType AgentModelType;
[FieldOffset(0x0104)]
public readonly TagInfo* Tags;
[FieldOffset(0x010A)]
public readonly byte Primary;
[FieldOffset(0x010B)]
public readonly byte Secondary;
[FieldOffset(0x010C)]
public readonly byte Level;
[FieldOffset(0x010D)]
public readonly byte TeamId;
[FieldOffset(0x0114)]
public readonly float EnergyRegen;
[FieldOffset(0x011C)]
public readonly float Energy;
[FieldOffset(0x0120)]
public readonly uint MaxEnergy;
[FieldOffset(0x0128)]
public readonly float HealthRegen;
[FieldOffset(0x0130)]
public readonly float Health;
[FieldOffset(0x0134)]
public readonly uint MaxHealth;
[FieldOffset(0x0138)]
public readonly uint Effects;
[FieldOffset(0x0154)]
public readonly LivingAgentModelState ModelState;
[FieldOffset(0x0158)]
public readonly LivingAgentState State;
[FieldOffset(0x0180)]
public readonly uint LoginNumber;
[FieldOffset(0x01B1)]
public readonly LivingAgentAllegiance Allegiance;
public readonly bool IsAlive => (!this.State.HasFlag(LivingAgentState.Dead)) && this.Health > 0f;
public readonly bool IsPlayer => this.LoginNumber is not 0;
public readonly bool IsNpc => this.LoginNumber is 0;
}
[StructLayout(LayoutKind.Explicit, Pack = 1, Size = 52)]
public readonly struct MapAgentContext
{
[FieldOffset(0x0000)]
public readonly float CurrentEnergy;
[FieldOffset(0x0004)]
public readonly float MaxEnergy;
[FieldOffset(0x0008)]
public readonly float EnergyRegen;
[FieldOffset(0x000C)]
public readonly uint SkillTimestamp;
[FieldOffset(0x0020)]
public readonly float CurrentHealth;
[FieldOffset(0x0024)]
public readonly float MaxHealth;
[FieldOffset(0x0028)]
public readonly float HealthRegen;
[FieldOffset(0x0030)]
public readonly LivingAgentEffects Effects;
}
[StructLayout(LayoutKind.Explicit, Pack = 1, Size = 0x38)]
public readonly unsafe struct AgentInfo
{
[FieldOffset(0x0034)]
public readonly char* NameEncoded;
}
@@ -1,93 +0,0 @@
using System.Runtime.InteropServices;
namespace Daybreak.API.Interop.GuildWars;
[StructLayout(LayoutKind.Sequential, Pack = 1)]
public readonly struct AttributeContext
{
public readonly uint Id;
public readonly uint LevelBase;
public readonly uint Level;
public readonly uint DecrementPoints;
public readonly uint IncrementPoints;
}
[StructLayout(LayoutKind.Sequential, Pack = 1)]
public readonly struct AttributeInfo
{
public readonly uint ProfessionId;
public readonly uint AttributeId;
public readonly uint NameId;
public readonly uint DescriptionId;
public readonly uint IsPve;
}
[StructLayout(LayoutKind.Sequential, Pack = 1)]
public readonly struct PartyAttribute
{
public readonly uint AgentId;
public readonly AttributeContext Attribute0;
public readonly AttributeContext Attribute1;
public readonly AttributeContext Attribute2;
public readonly AttributeContext Attribute3;
public readonly AttributeContext Attribute4;
public readonly AttributeContext Attribute5;
public readonly AttributeContext Attribute6;
public readonly AttributeContext Attribute7;
public readonly AttributeContext Attribute8;
public readonly AttributeContext Attribute9;
public readonly AttributeContext Attribute10;
public readonly AttributeContext Attribute11;
public readonly AttributeContext Attribute12;
public readonly AttributeContext Attribute13;
public readonly AttributeContext Attribute14;
public readonly AttributeContext Attribute15;
public readonly AttributeContext Attribute16;
public readonly AttributeContext Attribute17;
public readonly AttributeContext Attribute18;
public readonly AttributeContext Attribute19;
public readonly AttributeContext Attribute20;
public readonly AttributeContext Attribute21;
public readonly AttributeContext Attribute22;
public readonly AttributeContext Attribute23;
public readonly AttributeContext Attribute24;
public readonly AttributeContext Attribute25;
public readonly AttributeContext Attribute26;
public readonly AttributeContext Attribute27;
public readonly AttributeContext Attribute28;
public readonly AttributeContext Attribute29;
public readonly AttributeContext Attribute30;
public readonly AttributeContext Attribute31;
public readonly AttributeContext Attribute32;
public readonly AttributeContext Attribute33;
public readonly AttributeContext Attribute34;
public readonly AttributeContext Attribute35;
public readonly AttributeContext Attribute36;
public readonly AttributeContext Attribute37;
public readonly AttributeContext Attribute38;
public readonly AttributeContext Attribute39;
public readonly AttributeContext Attribute40;
public readonly AttributeContext Attribute41;
public readonly AttributeContext Attribute42;
public readonly AttributeContext Attribute43;
public readonly AttributeContext Attribute44;
public readonly AttributeContext Attribute45;
public readonly AttributeContext Attribute46;
public readonly AttributeContext Attribute47;
public readonly AttributeContext Attribute48;
public readonly AttributeContext Attribute49;
public readonly AttributeContext Attribute50;
public readonly AttributeContext Attribute51;
public readonly AttributeContext Attribute52;
public readonly AttributeContext Attribute53;
public readonly AttributeContext[] Attributes =>
[
this.Attribute0, this.Attribute1, this.Attribute2, this.Attribute3, this.Attribute4, this.Attribute5, this.Attribute6, this.Attribute7, this.Attribute8, this.Attribute9,
this.Attribute10, this.Attribute11, this.Attribute12, this.Attribute13, this.Attribute14, this.Attribute15, this.Attribute16, this.Attribute17, this.Attribute18, this.Attribute19,
this.Attribute20, this.Attribute21, this.Attribute22, this.Attribute23, this.Attribute24, this.Attribute25, this.Attribute26, this.Attribute27, this.Attribute28, this.Attribute29,
this.Attribute30, this.Attribute31, this.Attribute32, this.Attribute33, this.Attribute34, this.Attribute35, this.Attribute36, this.Attribute37, this.Attribute38, this.Attribute39,
this.Attribute40, this.Attribute41, this.Attribute42, this.Attribute43, this.Attribute44, this.Attribute45, this.Attribute46, this.Attribute47, this.Attribute48, this.Attribute49,
this.Attribute50, this.Attribute51, this.Attribute52, this.Attribute53
];
}
@@ -1,8 +0,0 @@
namespace Daybreak.API.Interop.GuildWars;
public enum Behavior
{
Fight,
Guard,
AvoidCombat
}
@@ -1,50 +0,0 @@
using System.Extensions;
using System.Runtime.InteropServices;
namespace Daybreak.API.Interop.GuildWars;
[StructLayout(LayoutKind.Explicit, Pack = 1)]
public readonly struct CharContext
{
[FieldOffset(0x0064)]
public readonly Uuid PlayerUuid;
[FieldOffset(0x0074)]
public readonly Array20Char PlayerName;
[FieldOffset(0x0198)]
public readonly uint MapId;
[FieldOffset(0x019C)]
public readonly uint IsExporable;
[FieldOffset(0x01A0)]
public readonly Array24Byte Host;
[FieldOffset(0x01B8)]
public readonly uint PlayerId;
[FieldOffset(0x0220)]
public readonly uint DistrictNumber;
[FieldOffset(0x0224)]
public readonly Language Language;
[FieldOffset(0x0228)]
public readonly uint ObserveMapId;
[FieldOffset(0x022C)]
public readonly uint CurrentMapId;
[FieldOffset(0x0230)]
public readonly uint ObserveMapType;
[FieldOffset(0x0234)]
public readonly uint CurrentMapType;
[FieldOffset(0x02A4)]
public readonly uint PlayerNumber;
[FieldOffset(0x03B8)]
public readonly Array64Char PlayerEmail;
}
@@ -1,22 +0,0 @@
using System.Extensions;
using System.Runtime.InteropServices;
namespace Daybreak.API.Interop.GuildWars;
[StructLayout(LayoutKind.Explicit, Pack = 1)]
public readonly struct CharInfoContext
{
[FieldOffset(0x0008)]
public readonly Uuid Uuid;
[FieldOffset(0x0018)]
public readonly Array20Char Name;
[FieldOffset(0x0040)]
public readonly Array17Uint Props;
public readonly uint MapId => (this.Props[0] >> 16) & 0xFFFF;
public readonly uint Primary => (this.Props[2] >> 20) & 0xF;
public readonly uint Secondary => (this.Props[7] >> 10) & 0xF;
public readonly uint Campaign => this.Props[7] & 0xF;
public readonly uint Level => (this.Props[7] >> 4) & 0x3F;
public readonly bool IsPvp => ((this.Props[7] >> 9) & 0x1) == 0x1;
}
@@ -1,19 +0,0 @@
namespace Daybreak.API.Interop.GuildWars;
public enum District
{
Current,
International,
American,
EuropeEnglish,
EuropeFrench,
EuropeGerman,
EuropeItalian,
EuropeSpanish,
EuropePolish,
EuropeRussian,
AsiaKorean,
AsiaChinese,
AsiaJapanese,
Unknown = 0xff
}
-68
View File
@@ -1,68 +0,0 @@
using Daybreak.API.Models;
using System.Runtime.InteropServices;
namespace Daybreak.API.Interop.GuildWars;
// Very incomplete. GWCA contains a much better definition
[StructLayout(LayoutKind.Explicit, Pack = 1, Size = 0x1AC)]
public readonly struct Frame
{
[FieldOffset(0x0008)]
public readonly uint FrameLayout;
[FieldOffset(0x0018)]
public readonly uint VisibilityFlags;
[FieldOffset(0x0020)]
public readonly uint Type;
[FieldOffset(0x0024)]
public readonly uint TemplateType;
[FieldOffset(0x00A0)]
public readonly GuildWarsArray<FrameInteractionCallback> FrameCallbacks;
[FieldOffset(0x00b0)]
public readonly uint ChildOffsetId;
[FieldOffset(0x00b4)]
public readonly uint FrameId;
[FieldOffset(0x0120)]
public readonly FrameRelation Relation;
[FieldOffset(0x0184)]
public readonly uint Field91;
public bool IsCreated => (this.Field91 & 0x4) != 0;
public bool IsHidden => (this.Field91 & 0x200) != 0;
public bool IsVisible => !this.IsHidden;
public bool IsDIsabled => (this.Field91 & 0x10) != 0;
}
[StructLayout(LayoutKind.Explicit, Pack = 1, Size = 0x1C)]
public readonly unsafe struct FrameRelation
{
[FieldOffset(0x0000)]
public readonly FrameRelation* Parent;
[FieldOffset(0x000C)]
public readonly uint FrameHashId;
}
[StructLayout(LayoutKind.Sequential, Pack =1)]
public readonly unsafe struct InteractionMessage
{
public readonly uint FrameId;
public readonly UIMessage MessageId;
public readonly void** WParam;
}
public unsafe delegate void UIInteractionCallback(InteractionMessage* message, void* wParam, void* lParam);
[StructLayout(LayoutKind.Sequential, Pack = 1, Size = 0xC)]
public unsafe readonly struct FrameInteractionCallback
{
public readonly void* Callback; // UIInteractionCallback
public readonly void* UiCtl_Context;
}
@@ -1,28 +0,0 @@
using System.Runtime.InteropServices;
namespace Daybreak.API.Interop.GuildWars;
[StructLayout(LayoutKind.Explicit, Pack = 1)]
public readonly unsafe struct GameContext
{
[FieldOffset(0x0008)]
public readonly AgentGameContext* AgentGameContext;
[FieldOffset(0x0014)]
public readonly MapContext* MapContext;
[FieldOffset(0x0028)]
public readonly AccountGameContext* AccountContext;
[FieldOffset(0x002C)]
public readonly WorldContext* WorldContext;
[FieldOffset(0x003C)]
public readonly GuildContext* GuildContext;
[FieldOffset(0x0044)]
public readonly CharContext* CharContext;
[FieldOffset(0x004C)]
public readonly PartyContext* PartyContext;
}
-11
View File
@@ -1,11 +0,0 @@
using System.Runtime.InteropServices;
namespace Daybreak.API.Interop.GuildWars;
[StructLayout(LayoutKind.Sequential, Pack = 1)]
public readonly struct GamePos
{
public readonly float X;
public readonly float Y;
public readonly uint Plane;
}
-110
View File
@@ -1,110 +0,0 @@
using System.Extensions;
using System.Runtime.InteropServices;
namespace Daybreak.API.Interop.GuildWars;
[StructLayout(LayoutKind.Explicit, Pack = 1)]
public readonly unsafe struct GuildContext
{
[FieldOffset(0x0034)]
public readonly Array20Char PlayerName;
[FieldOffset(0x0060)]
public readonly uint PlayerBuildIndex;
[FieldOffset(0x0078)]
public readonly Array256Char Announcement;
[FieldOffset(0x0287)]
public readonly Array20Char AnnouncementAuthor;
[FieldOffset(0x02A0)]
public readonly uint PlayerGuildRank;
[FieldOffset(0x02A8)]
public readonly GuildWarsArray<TownAlliance> FactionOutpostGuilds;
[FieldOffset(0x02B8)]
public readonly uint KurzickTownCount;
[FieldOffset(0x02BC)]
public readonly uint LuxonTownCount;
[FieldOffset(0x02F8)]
public readonly GuildWarsArray<WrappedPointer<Guild>> Guilds;
[FieldOffset(0x0358)]
public readonly GuildWarsArray<WrappedPointer<GuildPlayer>> PlayerRoster;
}
[StructLayout(LayoutKind.Sequential, Pack = 1)]
public readonly struct TownAlliance
{
public readonly uint Rank;
public readonly uint Allegiance;
public readonly uint Faction;
public readonly Array32Char Name;
public readonly Array5Char Tag;
public readonly CapeDesign Cape;
public readonly uint MapId;
}
[StructLayout(LayoutKind.Sequential, Pack = 1)]
public readonly struct CapeDesign
{
public readonly uint CapeBgColor;
public readonly uint CapeDetailColor;
public readonly uint CapeEmblemColor;
public readonly uint CapeShape;
public readonly uint CapeDetail;
public readonly uint CapeEmblem;
public readonly uint CapeTrim;
}
[StructLayout(LayoutKind.Explicit, Pack = 1, Size = 0x174)]
public readonly unsafe struct GuildPlayer
{
[FieldOffset(0x0004)]
public readonly char* NamePtr;
[FieldOffset(0x0008)]
public readonly Array20Char InvitedName;
[FieldOffset(0x0030)]
public readonly Array20Char CurrentName;
[FieldOffset(0x0050)]
public readonly Array20Char InviterName;
[FieldOffset(0x0080)]
public readonly uint InviteTime;
[FieldOffset(0x0084)]
public readonly Array20Char PromoterName;
[FieldOffset(0x00DC)]
public readonly uint Offline;
[FieldOffset(0x00E0)]
public readonly uint MemberType;
[FieldOffset(0x00E4)]
public readonly uint Status;
}
[StructLayout(LayoutKind.Explicit, Pack = 1, Size = 0xAC)]
public readonly struct Guild
{
[FieldOffset(0x0024)]
public readonly uint Index;
[FieldOffset(0x0028)]
public readonly uint Rank;
[FieldOffset(0x002C)]
public readonly uint Features;
[FieldOffset(0x0030)]
public readonly Array32Char Name;
[FieldOffset(0x0070)]
public readonly uint Rating;
[FieldOffset(0x0074)]
public readonly uint Faction;
[FieldOffset(0x0078)]
public readonly uint FactionPoints;
[FieldOffset(0x007C)]
public readonly uint QualifierPoints;
[FieldOffset(0x0080)]
public readonly Array8Char Tag;
[FieldOffset(0x0090)]
public readonly CapeDesign Cape;
}
@@ -4,6 +4,7 @@ using System.Runtime.InteropServices;
namespace Daybreak.API.Interop.GuildWars;
[StructLayout(LayoutKind.Sequential, Pack = 1)]
[GWCAEquivalent("Array")]
public readonly unsafe struct GuildWarsArray<T> : IEnumerable<T>
where T : unmanaged
{
@@ -12,6 +13,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 +58,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() { }
}
}
-43
View File
@@ -1,43 +0,0 @@
using System.Extensions;
using System.Numerics;
using System.Runtime.InteropServices;
namespace Daybreak.API.Interop.GuildWars;
[StructLayout(LayoutKind.Explicit, Pack = 1, Size = 0x24)]
public readonly struct HeroFlag
{
[FieldOffset(0x0000)]
public readonly uint HeroId;
[FieldOffset(0x0004)]
public readonly uint AgentId;
[FieldOffset(0x0008)]
public readonly uint Level;
[FieldOffset(0x000C)]
public readonly Behavior Behavior;
[FieldOffset(0x0010)]
public readonly Vector2 Flag;
[FieldOffset(0x0020)]
public readonly uint LockedTargetId;
}
[StructLayout(LayoutKind.Explicit, Pack = 1, Size = 0x78)]
public readonly struct HeroInfo
{
[FieldOffset(0x0000)]
public readonly uint HeroId;
[FieldOffset(0x0004)]
public readonly uint AgentId;
[FieldOffset(0x0008)]
public readonly uint Level;
[FieldOffset(0x000C)]
public readonly Profession Primary;
[FieldOffset(0x0010)]
public readonly Profession Secondary;
[FieldOffset(0x0014)]
public readonly uint HeroFileId;
[FieldOffset(0x0018)]
public readonly uint ModelFileId;
[FieldOffset(0x0050)]
public readonly Array20Char Name;
}
@@ -1,29 +0,0 @@
using System.Runtime.InteropServices;
namespace Daybreak.API.Interop.GuildWars;
public enum InstanceType
{
Outpost,
Explorable,
Loading
};
[StructLayout(LayoutKind.Explicit, Pack = 1)]
public readonly unsafe struct InstanceInfoContext
{
[FieldOffset(0x0004)]
public readonly InstanceType InstanceType;
[FieldOffset(0x0008)]
public readonly AreaInfo* CurrentMapInfo;
}
[StructLayout(LayoutKind.Sequential, Pack = 1)]
public readonly struct AreaInfo
{
public readonly uint Campaign;
public readonly uint Continent;
public readonly uint Region;
public readonly uint RegionType;
public readonly uint Flags;
}
@@ -1,17 +0,0 @@
namespace Daybreak.API.Interop.GuildWars;
public enum Language
{
English,
Korean,
French,
German,
Italian,
Spanish,
TraditionalChinese,
Japanese = 8,
Polish,
Russian,
BorkBorkBork = 17,
Unknown = 0xff
}
@@ -1,8 +0,0 @@
using System.Runtime.InteropServices;
namespace Daybreak.API.Interop.GuildWars;
[StructLayout(LayoutKind.Explicit, Pack = 1)]
public readonly struct MapContext
{
}
-61
View File
@@ -1,61 +0,0 @@
using System.Runtime.InteropServices;
namespace Daybreak.API.Interop.GuildWars;
[StructLayout(LayoutKind.Explicit)]
public readonly unsafe struct NpcContext
{
[FieldOffset(0x0000)]
public readonly uint ModelId;
[FieldOffset(0x000C)]
public readonly uint Sex;
[FieldOffset(0x0010)]
public readonly NpcFlags Flags;
[FieldOffset(0x0014)]
public readonly uint Primary;
[FieldOffset(0x001C)]
public readonly byte DefaultLevel;
[FieldOffset(0x0020)]
public readonly char* EncName;
[FieldOffset(0x0028)]
public readonly int FilesCount;
[FieldOffset(0x002C)]
public readonly int FilesCapacity;
[Flags]
public enum NpcFlags : uint
{
None = 0x0000,
Henchman = 0x0010,
Hero = 0x0020,
Spirit = 0x4000,
Minion = 0x0100,
Pet = 0x000D
}
}
[StructLayout(LayoutKind.Sequential, Pack = 1)]
public readonly unsafe struct PetContext
{
public readonly uint AgentId;
public readonly uint OwnerAgentId;
public readonly char* PetName;
public readonly uint ModelFileId1;
public readonly uint ModelFileId2;
public readonly Behavior Behavior;
public readonly uint LockedTargetId;
}
[StructLayout(LayoutKind.Sequential, Pack = 1)]
public readonly struct ControlledMinion
{
public readonly uint AgentId;
public readonly uint MinionCount;
}
-124
View File
@@ -1,124 +0,0 @@
using System.Extensions;
using System.Runtime.InteropServices;
namespace Daybreak.API.Interop.GuildWars;
[Flags]
public enum PlayerPartyMemberState : uint
{
None = 0,
Connected = 1,
Ticked = 2
}
public enum PartySearchType
{
PartySearchType_Hunting = 0,
PartySearchType_Mission = 1,
PartySearchType_Quest = 2,
PartySearchType_Trade = 3,
PartySearchType_Guild = 4,
};
[Flags]
public enum PartyFlags : uint
{
None = 0,
HardMode = 0x10, // Bit 4
Defeated = 0x20, // Bit 5
PartyLeader = 0x80 // Bit 7 (0x7th position)
}
[StructLayout(LayoutKind.Explicit, Pack = 1, Size = 0xC)]
public readonly unsafe struct PartyMoraleContext
{
[FieldOffset(0x000C)]
public readonly PartyMemberMoraleInfo* MoraleInfo;
}
[StructLayout(LayoutKind.Explicit, Pack = 1)]
public readonly struct PartyMemberMoraleInfo
{
[FieldOffset(0x0000)]
public readonly uint AgentId;
[FieldOffset(0x0014)]
public readonly uint Morale;
}
[StructLayout(LayoutKind.Explicit, Pack = 1, Size = 0x58)]
public readonly unsafe struct PartyContext
{
[FieldOffset(0x0014)]
public readonly PartyFlags Flags;
[FieldOffset(0x0040)]
public readonly GuildWarsArray<WrappedPointer<PartyInfo>> Parties;
[FieldOffset(0x0054)]
public readonly PartyInfo* PlayerParty;
[FieldOffset(0x00C0)]
public readonly GuildWarsArray<WrappedPointer<PartySearch>> PartySearches;
}
[StructLayout(LayoutKind.Sequential, Pack = 1, Size = 0xC)]
public readonly struct PlayerPartyMember
{
public readonly uint LogingNumber;
public readonly uint CalledTargetId;
public readonly PlayerPartyMemberState State;
}
[StructLayout(LayoutKind.Explicit, Pack = 1, Size = 0x18)]
public readonly struct HeroPartyMember
{
[FieldOffset(0x0000)]
public readonly uint AgentId;
[FieldOffset(0x0004)]
public readonly uint OwnerPlayerId;
[FieldOffset(0x0008)]
public readonly uint HeroId;
[FieldOffset(0x0014)]
public readonly uint Level;
}
[StructLayout(LayoutKind.Explicit, Pack = 1, Size = 0x34)]
public readonly struct HenchmanPartyMember
{
[FieldOffset(0x0000)]
public readonly uint AgentId;
[FieldOffset(0x002C)]
public readonly Profession Profession;
[FieldOffset(0x0030)]
public readonly uint Level;
}
[StructLayout(LayoutKind.Explicit, Pack = 1, Size = 0x84)]
public readonly struct PartyInfo
{
[FieldOffset(0x0000)]
public readonly uint PartyId;
[FieldOffset(0x0004)]
public readonly GuildWarsArray<PlayerPartyMember> Players;
[FieldOffset(0x0014)]
public readonly GuildWarsArray<HenchmanPartyMember> Henchmen;
[FieldOffset(0x0024)]
public readonly GuildWarsArray<HeroPartyMember> Heroes;
[FieldOffset(0x0034)]
public readonly GuildWarsArray<uint> Others;
}
[StructLayout(LayoutKind.Sequential, Pack = 1)]
public readonly struct PartySearch
{
public readonly uint PartySearchId;
public readonly PartySearchType PartySearchType;
public readonly uint HardMode;
public readonly uint District;
public readonly uint Language;
public readonly uint PartySize;
public readonly uint HeroCount;
public readonly Array32Char Message;
public readonly Array20Char PartyLeader;
public readonly Profession Primary;
public readonly Profession Secondary;
public readonly uint Level;
public readonly uint Timestamp;
}
@@ -1,44 +0,0 @@
using System.Runtime.InteropServices;
namespace Daybreak.API.Interop.GuildWars;
[Flags]
public enum PlayerContextFlags : uint
{
None = 0,
PvP = 0x800
}
[StructLayout(LayoutKind.Explicit, Pack = 1, Size = 0x4C)]
public readonly unsafe struct PlayerContext
{
[FieldOffset(0x0000)]
public readonly int AgentId;
[FieldOffset(0x0014)]
public readonly PlayerContextFlags Flags;
[FieldOffset(0x0018)]
public readonly uint PrimaryProfession;
[FieldOffset(0x001C)]
public readonly uint SecondaryProfession;
[FieldOffset(0x0024)]
public readonly char* NameEncoded;
[FieldOffset(0x0028)]
public readonly char* Name;
[FieldOffset(0x002C)]
public readonly uint PartyLeaderPlayerNumber;
[FieldOffset(0x0030)]
public readonly uint ActiveTitleTier;
[FieldOffset(0x0034)]
public readonly uint PlayerNumber;
[FieldOffset(0x0038)]
public readonly uint PartySize;
}
@@ -1,10 +0,0 @@
using System.Runtime.InteropServices;
namespace Daybreak.API.Interop.GuildWars;
[StructLayout(LayoutKind.Explicit, Pack = 1)]
public readonly struct PlayerControlledCharContext
{
[FieldOffset(0x0014)]
public readonly uint AgentId;
}
@@ -1,30 +0,0 @@
using System.Extensions;
using System.Runtime.InteropServices;
namespace Daybreak.API.Interop.GuildWars;
[StructLayout(LayoutKind.Explicit, Pack = 1)]
public readonly struct PreGameContext
{
[FieldOffset(0x0000)]
public readonly uint FrameId;
[FieldOffset(0x0124)]
public readonly uint ChosenCharacterIndex;
[FieldOffset(0x0140)]
public readonly uint Index1;
[FieldOffset(0x0144)]
public readonly uint Index2;
[FieldOffset(0x00148)]
public readonly GuildWarsArray<LoginCharacterContext> LoginCharacters;
}
[StructLayout(LayoutKind.Explicit, Pack = 1)]
public readonly struct LoginCharacterContext
{
[FieldOffset(0x0004)]
public readonly Array20Char CharacterName;
}
@@ -1,47 +0,0 @@
using System.Runtime.InteropServices;
namespace Daybreak.API.Interop.GuildWars;
public enum Profession : uint
{
None,
Warrior,
Ranger,
Monk,
Necromancer,
Mesmer,
Elementalist,
Assassin,
Ritualist,
Paragon,
Dervish
}
public enum ProfessionByte : byte
{
None,
Warrior,
Ranger,
Monk,
Necromancer,
Mesmer,
Elementalist,
Assassin,
Ritualist,
Paragon,
Dervish
}
[StructLayout(LayoutKind.Sequential, Pack = 1, Size = 0x0014)]
public readonly struct ProfessionsContext
{
public readonly uint AgentId;
public readonly Profession CurrentPrimary;
public readonly Profession CurrentSecondary;
public readonly uint UnlockedProfessionsFlags;
public bool ProfessionUnlocked(int professionId)
{
return (this.UnlockedProfessionsFlags & (1U << professionId)) != 0;
}
}
-28
View File
@@ -1,28 +0,0 @@
using System.Numerics;
using System.Runtime.InteropServices;
namespace Daybreak.API.Interop.GuildWars;
[StructLayout(LayoutKind.Explicit, Pack = 1, Size = 0x0034)]
public readonly struct QuestContext
{
[FieldOffset(0x0000)]
public readonly uint QuestId;
[FieldOffset(0x0014)]
public readonly uint MapFrom;
[FieldOffset(0x0018)]
public readonly Vector3 Marker;
[FieldOffset(0x0028)]
public readonly uint MapTo;
}
[StructLayout(LayoutKind.Sequential, Pack = 1, Size = 0xC)]
public readonly unsafe struct MissionObjectiveContext
{
public readonly uint ObjectiveId;
public readonly char* EncodedString;
public readonly uint Type;
}
@@ -1,12 +0,0 @@
namespace Daybreak.API.Interop.GuildWars;
public enum ServerRegion
{
International = -2,
America = 0,
Korea,
Europe,
China,
Japan,
Unknown = 0xff
}
-58
View File
@@ -1,58 +0,0 @@
using System.Runtime.InteropServices;
namespace Daybreak.API.Interop.GuildWars;
[StructLayout(LayoutKind.Sequential, Pack = 1)]
public readonly struct SkillContext
{
public readonly uint Adrenaline1;
public readonly uint Adrenaline2;
public readonly uint Recharge;
public readonly uint Id;
public readonly uint Event;
}
[StructLayout(LayoutKind.Sequential, Pack = 1, Size = 0xBC)]
public readonly struct SkillbarContext
{
public readonly uint AgentId;
public readonly SkillContext Skill0;
public readonly SkillContext Skill1;
public readonly SkillContext Skill2;
public readonly SkillContext Skill3;
public readonly SkillContext Skill4;
public readonly SkillContext Skill5;
public readonly SkillContext Skill6;
public readonly SkillContext Skill7;
public readonly uint Disabled;
public readonly uint Casting;
}
[StructLayout(LayoutKind.Explicit, Pack = 1, Size = 0x10)]
public readonly struct Buff
{
[FieldOffset(0x0000)]
public readonly uint SkillId;
[FieldOffset(0x0008)]
public readonly uint BuffId;
[FieldOffset(0x000C)]
public readonly uint TargetAgentId;
}
[StructLayout(LayoutKind.Sequential, Pack = 1, Size = 0x18)]
public readonly struct Effect
{
public readonly uint SkillId;
public readonly uint AttributeLevel;
public readonly uint EffectId;
public readonly uint AgentId;
public readonly float Duration;
public readonly uint Timestamp;
}
[StructLayout(LayoutKind.Sequential, Pack = 1)]
public readonly struct DupeSkill
{
public readonly uint SkillId;
public readonly uint Count;
}
@@ -1,13 +0,0 @@
using System.Extensions;
namespace Daybreak.API.Interop.GuildWars;
public readonly struct SkillTemplate(uint primary, uint secondary, uint attributeCount, Array12Uint attributeIds, Array12Uint attributeValues, Array8Uint skills)
{
public readonly uint Primary = primary;
public readonly uint Secondary = secondary;
public readonly uint AttributesCount = attributeCount;
public readonly Array12Uint AttributeIds = attributeIds;
public readonly Array12Uint AttributeValues = attributeValues;
public readonly Array8Uint Skills = skills;
}
-12
View File
@@ -1,12 +0,0 @@
using System.Runtime.InteropServices;
namespace Daybreak.API.Interop.GuildWars;
[StructLayout(LayoutKind.Sequential, Pack = 1)]
public readonly struct TagInfo
{
public readonly ushort GuildId;
public readonly byte Primary;
public readonly byte Secondary;
public readonly ushort Level;
}
-40
View File
@@ -1,40 +0,0 @@
using System.Runtime.InteropServices;
namespace Daybreak.API.Interop.GuildWars;
[Flags]
public enum TitleProps : uint
{
None = 0,
PercentageBased = 1,
HasTiers = 2
// Note: The HasTiers check uses (props & 3) == 2, which means bit 1 is set but bit 0 is not
}
[StructLayout(LayoutKind.Sequential, Pack = 1, Size = 40)]
public readonly struct TitleContext
{
public readonly TitleProps Props;
public readonly uint CurrentPoints;
public readonly uint CurrentTitleTierIndex;
public readonly uint PointsNeededForCurrentRank;
public readonly uint NextTitleTierIndex;
public readonly uint PointsNeededForNextRank;
public readonly uint MaxTitleRank;
public readonly uint MaxTitleTierIndex;
}
[StructLayout(LayoutKind.Sequential, Pack = 1, Size = 0xC)]
public readonly unsafe struct TitleTier
{
public readonly TitleProps Props;
public readonly uint TierNumber;
public readonly char* TierNameEncoded;
}
[StructLayout(LayoutKind.Sequential, Pack = 1)]
public readonly struct TitleClientData
{
public readonly uint TitleId;
public readonly uint NameId;
}
-183
View File
@@ -1,183 +0,0 @@
namespace Daybreak.API.Interop.GuildWars;
public enum UIAction : uint
{
None = 0,
Screenshot = 0xAE,
// Panels
CloseAllPanels = 0x85,
ToggleInventoryWindow = 0x8B,
OpenScoreChart = 0xBD,
OpenTemplateManager = 0xD3,
OpenSaveEquipmentTemplate = 0xD4,
OpenSaveSkillTemplate = 0xD5,
OpenParty = 0xBF,
OpenGuild = 0xBA,
OpenFriends = 0xB9,
ToggleAllBags = 0xB8,
OpenMissionMap = 0xB6,
OpenBag2 = 0xB5,
OpenBag1 = 0xB4,
OpenBelt = 0xB3,
OpenBackpack = 0xB2,
OpenSkillsAndAttributes = 0x8F,
OpenQuestLog = 0x8E,
OpenWorldMap = 0x8C,
OpenOptions = 0x8D,
OpenHero = 0x8A,
// Weapon sets
CycleEquipment = 0x86,
ActivateWeaponSet1 = 0x81,
ActivateWeaponSet2,
ActivateWeaponSet3,
ActivateWeaponSet4,
DropItem = 0xCD, // drops bundle item >> flags, ashes, etc
// Chat
CharReply = 0xBE,
OpenChat = 0xA1,
OpenAlliance = 0x88,
ReverseCamera = 0x90,
StrafeLeft = 0x91,
StrafeRight = 0x92,
TurnLeft = 0xA2,
TurnRight = 0xA3,
MoveBackward = 0xAC,
MoveForward = 0xAD,
CancelAction = 0xAF,
Interact = 0x80,
ReverseDirection = 0xB1,
Autorun = 0xB7,
Follow = 0xCC,
// Targeting
TargetPartyMember1 = 0x96,
TargetPartyMember2,
TargetPartyMember3,
TargetPartyMember4,
TargetPartyMember5,
TargetPartyMember6,
TargetPartyMember7,
TargetPartyMember8,
TargetPartyMember9 = 0xC6,
TargetPartyMember10,
TargetPartyMember11,
TargetPartyMember12,
TargetNearestItem = 0xC3,
TargetNextItem = 0xC4,
TargetPreviousItem = 0xC5,
TargetPartyMemberNext = 0xCA,
TargetPartyMemberPrevious = 0xCB,
TargetAllyNearest = 0xBC,
ClearTarget = 0xE3,
TargetSelf = 0xA0, // also 0x96
TargetPriorityTarget = 0x9F,
TargetNearestEnemy = 0x93,
TargetNextEnemy = 0x95,
TargetPreviousEnemy = 0x9E,
ShowOthers = 0x89,
ShowTargets = 0x94,
CameraZoomIn = 0xCE,
CameraZoomOut = 0xCF,
// Party/Hero commands
ClearPartyCommands = 0xDB,
CommandParty = 0xD6,
CommandHero1,
CommandHero2,
CommandHero3,
CommandHero4 = 0x102,
CommandHero5,
CommandHero6,
CommandHero7,
OpenHero1PetCommander = 0xE0,
OpenHero2PetCommander,
OpenHero3PetCommander,
OpenHero4PetCommander = 0xFE,
OpenHero5PetCommander,
OpenHero6PetCommander,
OpenHero7PetCommander,
OpenHeroCommander1 = 0xDC,
OpenHeroCommander2,
OpenHeroCommander3,
OpenPetCommander,
OpenHeroCommander4 = 0x126,
OpenHeroCommander5,
OpenHeroCommander6,
OpenHeroCommander7,
Hero1Skill1 = 0xE5,
Hero1Skill2,
Hero1Skill3,
Hero1Skill4,
Hero1Skill5,
Hero1Skill6,
Hero1Skill7,
Hero1Skill8,
Hero2Skill1,
Hero2Skill2,
Hero2Skill3,
Hero2Skill4,
Hero2Skill5,
Hero2Skill6,
Hero2Skill7,
Hero2Skill8,
Hero3Skill1,
Hero3Skill2,
Hero3Skill3,
Hero3Skill4,
Hero3Skill5,
Hero3Skill6,
Hero3Skill7,
Hero3Skill8,
Hero4Skill1 = 0x106,
Hero4Skill2,
Hero4Skill3,
Hero4Skill4,
Hero4Skill5,
Hero4Skill6,
Hero4Skill7,
Hero4Skill8,
Hero5Skill1,
Hero5Skill2,
Hero5Skill3,
Hero5Skill4,
Hero5Skill5,
Hero5Skill6,
Hero5Skill7,
Hero5Skill8,
Hero6Skill1,
Hero6Skill2,
Hero6Skill3,
Hero6Skill4,
Hero6Skill5,
Hero6Skill6,
Hero6Skill7,
Hero6Skill8,
Hero7Skill1,
Hero7Skill2,
Hero7Skill3,
Hero7Skill4,
Hero7Skill5,
Hero7Skill6,
Hero7Skill7,
Hero7Skill8,
// Skills
UseSkill1 = 0xA4,
UseSkill2,
UseSkill3,
UseSkill4,
UseSkill5,
UseSkill6,
UseSkill7,
UseSkill8
};
@@ -1,173 +0,0 @@
using System.Numerics;
using System.Runtime.InteropServices;
namespace Daybreak.API.Interop.GuildWars;
[StructLayout(LayoutKind.Explicit, Pack = 1)]
public readonly unsafe struct WorldContext
{
[FieldOffset(0x0000)]
public readonly AccountInfoContext* AccountInfo;
[FieldOffset(0x0024)]
public readonly GuildWarsArray<uint> MerchItems;
[FieldOffset(0x0034)]
public readonly GuildWarsArray<uint> MerchItems2;
[FieldOffset(0x007C)]
public readonly GuildWarsArray<MapAgentContext> MapAgents;
[FieldOffset(0x009C)]
public readonly Vector3 AllFlag;
[FieldOffset(0x00AC)]
public readonly GuildWarsArray<PartyAttribute> Attributes;
[FieldOffset(0x0508)]
public readonly GuildWarsArray<AgentEffects> PartyEffects;
[FieldOffset(0x0528)]
public readonly uint ActiveQuestId;
[FieldOffset(0x052C)]
public readonly GuildWarsArray<QuestContext> QuestLog;
[FieldOffset(0x0564)]
public readonly GuildWarsArray<MissionObjectiveContext> MissionObjectives;
[FieldOffset(0x0574)]
public readonly GuildWarsArray<uint> HenchmenAgentIds;
[FieldOffset(0x0584)]
public readonly GuildWarsArray<HeroFlag> HeroFlags;
[FieldOffset(0x0594)]
public readonly GuildWarsArray<HeroInfo> HeroInfos;
[FieldOffset(0x05A4)]
public readonly GuildWarsArray<nuint> CartographedAreas;
[FieldOffset(0x05BC)]
public readonly GuildWarsArray<ControlledMinion> ControlledMinions;
[FieldOffset(0x05CC)]
public readonly GuildWarsArray<uint> MissionsCompleted;
[FieldOffset(0x05DC)]
public readonly GuildWarsArray<uint> MissionsBonus;
[FieldOffset(0x05EC)]
public readonly GuildWarsArray<uint> MissionsCompletedHardMode;
[FieldOffset(0x05FC)]
public readonly GuildWarsArray<uint> MissionsBonusHardMode;
[FieldOffset(0x060C)]
public readonly GuildWarsArray<uint> UnlockedMap;
[FieldOffset(0x062C)]
public readonly GuildWarsArray<PartyMoraleContext> PartyMorale;
[FieldOffset(0x067C)]
public readonly uint PlayerNumber;
[FieldOffset(0x0680)]
public readonly PlayerControlledCharContext* PlayerControlledChar;
[FieldOffset(0x0684)]
public readonly uint HardModeUnlocked;
[FieldOffset(0x06AC)]
public readonly GuildWarsArray<PetContext> Pets;
[FieldOffset(0x06BC)]
public readonly GuildWarsArray<ProfessionsContext> Professions;
[FieldOffset(0x06F0)]
public readonly GuildWarsArray<SkillbarContext> Skillbars;
[FieldOffset(0x0700)]
public readonly GuildWarsArray<uint> LearnableCharacterSkills;
[FieldOffset(0x0710)]
public readonly GuildWarsArray<uint> UnlockedCharacterSkills;
[FieldOffset(0x0720)]
public readonly GuildWarsArray<DupeSkill> DupeSkills;
[FieldOffset(0x0740)]
public readonly uint Experience;
[FieldOffset(0x0748)]
public readonly uint CurrentKurzick;
[FieldOffset(0x0750)]
public readonly uint TotalKurzick;
[FieldOffset(0x0758)]
public readonly uint CurrentLuxon;
[FieldOffset(0x0760)]
public readonly uint TotalLuxon;
[FieldOffset(0x0768)]
public readonly uint CurrentImperial;
[FieldOffset(0x0770)]
public readonly uint TotalImperial;
[FieldOffset(0x0788)]
public readonly uint Level;
[FieldOffset(0x0790)]
public readonly uint Morale;
[FieldOffset(0x0798)]
public readonly uint CurrentBalthazar;
[FieldOffset(0x07A0)]
public readonly uint TotalBalthazar;
[FieldOffset(0x07A8)]
public readonly uint CurrentSkillPoints;
[FieldOffset(0x07B0)]
public readonly uint TotalSkillPoints;
[FieldOffset(0x07B8)]
public readonly uint MaxKurzick;
[FieldOffset(0x07BC)]
public readonly uint MaxLuxon;
[FieldOffset(0x07C0)]
public readonly uint MaxBalthazar;
[FieldOffset(0x07C4)]
public readonly uint MaxImperial;
[FieldOffset(0x07CC)]
public readonly GuildWarsArray<AgentInfo> AgentInfos;
[FieldOffset(0x07FC)]
public readonly GuildWarsArray<NpcContext> Npcs;
[FieldOffset(0x080C)]
public readonly GuildWarsArray<PlayerContext> Players;
[FieldOffset(0x081C)]
public readonly GuildWarsArray<TitleContext> Titles;
[FieldOffset(0x082C)]
public readonly GuildWarsArray<TitleTier> TitleTiers;
[FieldOffset(0x083C)]
public readonly GuildWarsArray<uint> VanquishedAreas;
[FieldOffset(0x084C)]
public readonly uint FoesKilled;
[FieldOffset(0x0850)]
public readonly uint FoesToKill;
}
+9
View File
@@ -0,0 +1,9 @@
using System.Runtime.InteropServices;
namespace Daybreak.API.Interop;
[StructLayout(LayoutKind.Sequential, Pack = 1)]
[GWCAEquivalent("HookEntry")]
public readonly struct HookEntry
{
}
-9
View File
@@ -1,9 +0,0 @@
namespace Daybreak.API.Interop;
public interface IHook<T>
: IDisposable where T : Delegate
{
public T Continue { get; }
public nuint ContinueAddress { get; }
}
-78
View File
@@ -1,78 +0,0 @@
using Daybreak.API.Converters;
using System.Text.Json.Serialization;
namespace Daybreak.API.Interop;
[JsonConverter(typeof(PointerValueConverter))]
public readonly struct PointerValue(nuint address) : IEquatable<PointerValue>, IEquatable<nuint>, IEquatable<nint>
{
public readonly nuint Address = address;
public bool Equals(PointerValue other)
{
return this.Address.Equals(other.Address);
}
public bool Equals(nuint other)
{
return this.Address.Equals(other);
}
public bool Equals(nint other)
{
return this.Address.Equals((nuint)other);
}
public override string ToString()
{
return $"0x{this.Address:X8}";
}
public override bool Equals(object? obj) => obj is PointerValue value && this.Equals(value);
public override int GetHashCode()
{
return HashCode.Combine(this.Address);
}
public static bool operator ==(PointerValue left, PointerValue right)
{
return left.Equals(right);
}
public static bool operator !=(PointerValue left, PointerValue right)
{
return !(left == right);
}
public static implicit operator PointerValue(nuint address)
{
return new PointerValue(address);
}
public static implicit operator PointerValue(nint address)
{
return new PointerValue((nuint)address);
}
public static implicit operator PointerValue(int address)
{
return new PointerValue((nuint)address);
}
public static implicit operator PointerValue(uint address)
{
return new PointerValue(address);
}
public static PointerValue Parse(string s)
{
if (s.StartsWith("0x", StringComparison.InvariantCultureIgnoreCase)
&& nuint.TryParse(s.AsSpan(2), System.Globalization.NumberStyles.HexNumber, null, out var v))
{
return new PointerValue(v);
}
throw new FormatException($"Invalid pointer format: {s}");
}
}
@@ -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>()
-9
View File
@@ -1,9 +0,0 @@
using Daybreak.API.Interop;
namespace Daybreak.API.Models;
public sealed class AddressState
{
public required string Name { get; init; }
public required PointerValue Address { get; init; }
}
-38
View File
@@ -1,38 +0,0 @@
using System.Core.Extensions;
using System.Extensions;
namespace Daybreak.API.Models;
public sealed class AsyncRefreshCache<T>(TimeSpan refreshRate, Func<CancellationToken, ValueTask<T>> refreshFunc)
{
private readonly Func<CancellationToken, ValueTask<T>> refreshFunc = refreshFunc.ThrowIfNull();
private readonly SemaphoreSlim semaphoreSlim = new(1);
private T? cache;
private DateTime lastUpdateDateTime = DateTime.MinValue;
private TimeSpan refreshRate = refreshRate;
public async ValueTask<T> GetAsync(CancellationToken cancellationToken)
{
if (this.cache is not null && DateTime.UtcNow - this.lastUpdateDateTime < this.refreshRate)
{
return this.cache;
}
using var ctx = await this.semaphoreSlim.Acquire(cancellationToken);
if (DateTime.UtcNow - this.lastUpdateDateTime > this.refreshRate ||
this.cache is null)
{
this.lastUpdateDateTime = DateTime.UtcNow;
this.cache = await this.refreshFunc(cancellationToken);
}
return this.cache;
}
public void UpdateRefreshRate(TimeSpan newRefreshRate)
{
this.refreshRate = newRefreshRate;
}
}
+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)
{
-24
View File
@@ -1,24 +0,0 @@
namespace Daybreak.API.Models;
public enum Channel : uint
{
Alliance = 0,
Allies = 1, // coop with two groups for instance.
GWCA1 = 2,
All = 3,
GWCA2 = 4,
Moderator = 5,
Emote = 6,
Warning = 7, // shows in the middle of the screen and does not parse <c> tags
GWCA3 = 8,
Guild = 9,
Global = 10,
Group = 11,
Trade = 12,
Advisory = 13,
Whisper = 14,
Count,
// non-standard channel, but useful.
Command
};
@@ -0,0 +1,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;
}
-12
View File
@@ -1,12 +0,0 @@
using Daybreak.API.Interop;
namespace Daybreak.API.Models;
public sealed class HookState
{
public required bool Hooked { get; init; }
public required string Name { get; init; }
public required PointerValue TargetAddress { get; init; }
public required PointerValue ContinueAddress { get; init; }
public required PointerValue DetourAddress { get; init; }
}
-154
View File
@@ -1,154 +0,0 @@
namespace Daybreak.API.Models;
public enum UIMessage : uint
{
None = 0x0,
Resize = 0x8,
InitFrame = 0x9,
DestroyFrame = 0xb,
KeyDown = 0x1e, // wparam = UIPacket::kKeyAction*
KeyUp = 0x20, // wparam = UIPacket::kKeyAction*
MouseClick = 0x22, // wparam = UIPacket::kMouseClick*
MouseClick2 = 0x2e, // wparam = UIPacket::kMouseAction*
MouseAction = 0x2f, // wparam = UIPacket::kMouseAction*
SetLayout = 0x33,
FrameMessage_0x47 = 0x47, // Multiple uses depending on frame
UpdateAgentEffects = 0x10000000 | 0x9,
RerenderAgentModel = 0x10000000 | 0x7, // wparam = uint32_t agent_id
AgentSpeechBubble = 0x10000000 | 0x17,
ShowAgentNameTag = 0x10000000 | 0x19, // wparam = AgentNameTagInfo*
HideAgentNameTag = 0x10000000 | 0x1A,
SetAgentNameTagAttribs = 0x10000000 | 0x1B, // wparam = AgentNameTagInfo*
ChangeTarget = 0x10000000 | 0x20, // wparam = UIPacket::kChangeTarget*
AgentStartCasting = 0x10000000 | 0x27, // wparam = UIPacket::kAgentStartCasting*
ShowMapEntryMessage = 0x10000000 | 0x29, // wparam = { wchar_t* title, wchar_t* subtitle }
SetCurrentPlayerData = 0x10000000 | 0x2A, // fired after setting the worldcontext player name
PostProcessingEffect = 0x10000000 | 0x34, // Triggered when drunk. wparam = UIPacket::kPostProcessingEffect
HeroAgentAdded = 0x10000000 | 0x38, // hero assigned to agent/inventory/ai mode
HeroDataAdded = 0x10000000 | 0x39, // hero info received from server (name, level etc)
ShowXunlaiChest = 0x10000000 | 0x40,
MinionCountUpdated = 0x10000000 | 0x46,
MoraleChange = 0x10000000 | 0x47, // wparam = {agent id, morale percent }
LoginStateChanged = 0x10000000 | 0x50, // wparam = {bool is_logged_in, bool unk }
EffectAdd = 0x10000000 | 0x55, // wparam = {agent_id, GW::Effect*}
EffectRenew = 0x10000000 | 0x56, // wparam = GW::Effect*
EffectRemove = 0x10000000 | 0x57, // wparam = effect id
SkillActivated = 0x10000000 | 0x5b, // wparam ={ uint32_t agent_id , uint32_t skill_id }
UpdateSkillbar = 0x10000000 | 0x5E, // wparam ={ uint32_t agent_id , ... }
UpdateSkillsAvailable = 0x10000000 | 0x5f, // Triggered on a skill unlock, profession change or map load
TitleProgressUpdated = 0x10000000 | 0x65, // wparam = title_id
ExperienceGained = 0x10000000 | 0x66, // wparam = experience amount
WriteToChatLog = 0x10000000 | 0x7E, // wparam = UIPacket::kWriteToChatLog*. Triggered by the game when it wants to add a new message to chat.
WriteToChatLogWithSender = 0x10000000 | 0x7f, // wparam = UIPacket::kWriteToChatLogWithSender*. Triggered by the game when it wants to add a new message to chat.
AllyOrGuildMessage = 0x10000000 | 0x80, // wparam = UIPacket::kAllyOrGuildMessage*
PlayerChatMessage = 0x10000000 | 0x81, // wparam = UIPacket::kPlayerChatMessage*
FloatingWindowMoved = 0x10000000 | 0x83, // wparam = frame_id
FriendUpdated = 0x10000000 | 0x89, // wparam = { GW::Friend*, ... }
MapLoaded = 0x10000000 | 0x8A,
OpenWhisper = 0x10000000 | 0x90, // wparam = wchar* name
Logout = 0x10000000 | 0x9b, // wparam = { bool unknown, bool character_select }
CompassDraw = 0x10000000 | 0x9c, // wparam = UIPacket::kCompassDraw*
OnScreenMessage = 0x10000000 | 0xA0, // wparam = wchar_** encoded_string
DialogBody = 0x10000000 | 0xA4, // wparam = DialogBodyInfo*
DialogButton = 0x10000000 | 0xA1, // wparam = DialogButtonInfo*
TargetNPCPartyMember = 0x10000000 | 0xB1, // wparam = { uint32_t unk, uint32_t agent_id }
TargetPlayerPartyMember = 0x10000000 | 0xB2, // wparam = { uint32_t unk, uint32_t player_number }
VendorWindow = 0x10000000 | 0xB3, // wparam = UIPacket::kVendorWindow
VendorItems = 0x10000000 | 0xB7, // wparam = UIPacket::kVendorItems
VendorTransComplete = 0x10000000 | 0xB9, // wparam = *TransactionType
VendorQuote = 0x10000000 | 0xBB, // wparam = UIPacket::kVendorQuote
StartMapLoad = 0x10000000 | 0xC0, // wparam = { uint32_t map_id, ...}
WorldMapUpdated = 0x10000000 | 0xC5, // Triggered when an area in the world map has been discovered/updated
GuildMemberUpdated = 0x10000000 | 0xD8, // wparam = { GuildPlayer::name_ptr }
ShowHint = 0x10000000 | 0xDF, // wparam = { uint32_t icon_type, wchar_t* message_enc }
WeaponSetSwapComplete = 0x10000000 | 0xE7, // wparam = UIPacket::kWeaponSwap*
WeaponSetSwapCancel = 0x10000000 | 0xE8,
WeaponSetUpdated = 0x10000000 | 0xE9,
UpdateGoldCharacter = 0x10000000 | 0xEA, // wparam = { uint32_t unk, uint32_t gold_character }
UpdateGoldStorage = 0x10000000 | 0xEB, // wparam = { uint32_t unk, uint32_t gold_storage }
InventorySlotUpdated = 0x10000000 | 0xEC, // undocumented. Triggered when an item is moved into a slot
EquipmentSlotUpdated = 0x10000000 | 0xED, // undocumented. Triggered when an item is moved into a slot
InventorySlotCleared = 0x10000000 | 0xEF, // undocumented. Triggered when an item has been removed from a slot
EquipmentSlotCleared = 0x10000000 | 0xF0, // undocumented. Triggered when an item has been removed from a slot
PvPWindowContent = 0x10000000 | 0xF8,
PreStartSalvage = 0x10000000 | 0x100, // { uint32_t item_id, uint32_t kit_id }
TradePlayerUpdated = 0x10000000 | 0x103, // wparam = GW::TraderPlayer*
ItemUpdated = 0x10000000 | 0x104, // wparam = UIPacket::kItemUpdated*
MapChange = 0x10000000 | 0x10F, // wparam = map id
CalledTargetChange = 0x10000000 | 0x113, // wparam = { player_number, target_id }
ErrorMessage = 0x10000000 | 0x117, // wparam = { int error_index, wchar_t* error_encoded_string }
PartyHardModeChanged = 0x10000000 | 0x118, // wparam = { int is_hard_mode }
PartyAddHenchman = 0x10000000 | 0x119,
PartyRemoveHenchman = 0x10000000 | 0x11a,
PartyAddHero = 0x10000000 | 0x11c,
PartyRemoveHero = 0x10000000 | 0x11d,
PartyAddPlayer = 0x10000000 | 0x122,
PartyRemovePlayer = 0x10000000 | 0x124,
DisableEnterMissionBtn = 0x10000000 | 0x128, // wparam = boolean (1 = disabled, 0 = enabled)
ShowCancelEnterMissionBtn = 0x10000000 | 0x12b,
PartyDefeated = 0x10000000 | 0x12d,
PartySearchInviteReceived = 0x10000000 | 0x135, // wparam = UIPacket::kPartySearchInviteReceived*
PartySearchInviteSent = 0x10000000 | 0x137,
PartyShowConfirmDialog = 0x10000000 | 0x138, // wparam = UIPacket::kPartyShowConfirmDialog
PreferenceEnumChanged = 0x10000000 | 0x13E, // wparam = UiPacket::kPreferenceEnumChanged
PreferenceFlagChanged = 0x10000000 | 0x13F, // wparam = UiPacket::kPreferenceFlagChanged
PreferenceValueChanged = 0x10000000 | 0x140, // wparam = UiPacket::kPreferenceValueChanged
UIPositionChanged = 0x10000000 | 0x141, // wparam = UIPacket::kUIPositionChanged
QuestAdded = 0x10000000 | 0x149, // wparam = { quest_id, ... }
QuestDetailsChanged = 0x10000000 | 0x14A, // wparam = { quest_id, ... }
ClientActiveQuestChanged = 0x10000000 | 0x14C, // wparam = { quest_id, ... }. Triggered when the game requests the current quest to change
ServerActiveQuestChanged = 0x10000000 | 0x14E, // wparam = UIPacket::kServerActiveQuestChanged*. Triggered when the server requests the current quest to change
UnknownQuestRelated = 0x10000000 | 0x14F,
DungeonComplete = 0x10000000 | 0x151, // undocumented
MissionComplete = 0x10000000 | 0x152, // undocumented
VanquishComplete = 0x10000000 | 0x154, // undocumented
ObjectiveAdd = 0x10000000 | 0x155, // wparam = UIPacket::kObjectiveAdd*
ObjectiveComplete = 0x10000000 | 0x156, // wparam = UIPacket::kObjectiveComplete*
ObjectiveUpdated = 0x10000000 | 0x157, // wparam = UIPacket::kObjectiveUpdated*
TradeSessionStart = 0x10000000 | 0x160, // wparam = { trade_state, player_number }
TradeSessionUpdated = 0x10000000 | 0x166, // no args
TriggerLogoutPrompt = 0x10000000 | 0x16C, // no args
ToggleOptionsWindow = 0x10000000 | 0x16D, // no args
CheckUIState = 0x10000000 | 0x170, // Undocumented
RedrawItem = 0x10000000 | 0x172, // wparam = uint32_t item_id
CloseSettings = 0x10000000 | 0x174, // Undocumented
ChangeSettingsTab = 0x10000000 | 0x175, // wparam = uint32_t is_interface_tab
GuildHall = 0x10000000 | 0x177, // wparam = gh key (uint32_t[4])
LeaveGuildHall = 0x10000000 | 0x179,
Travel = 0x10000000 | 0x17A,
OpenWikiUrl = 0x10000000 | 0x17B, // wparam = char* url
AppendMessageToChat = 0x10000000 | 0x189, // wparam = wchar_t* message
HideHeroPanel = 0x10000000 | 0x197, // wparam = hero_id
ShowHeroPanel = 0x10000000 | 0x198, // wparam = hero_id
GetInventoryAgentId = 0x10000000 | 0x19c, // wparam = 0, lparam = uint32_t* agent_id_out. Used to fetch which agent is selected
EquipItem = 0x10000000 | 0x19d, // wparam = { item_id, agent_id }
MoveItem = 0x10000000 | 0x19e, // wparam = { item_id, to_bag, to_slot, bool prompt }
InitiateTrade = 0x10000000 | 0x1A0,
InventoryAgentChanged = 0x10000000 | 0x1b0, // Triggered when inventory needs updating due to agent change; no args
OpenTemplate = 0x10000000 | 0x1B9, // wparam = GW::UI::ChatTemplate*
// GWCA Client to Server commands. Only added the ones that are used for hooks, everything else goes straight into GW
SendLoadSkillTemplate = 0x30000000 | 0x3, // wparam = SkillbarMgr::SkillTemplate*
SendPingWeaponSet = 0x30000000 | 0x4, // wparam = UIPacket::kSendPingWeaponSet*
SendMoveItem = 0x30000000 | 0x5, // wparam = UIPacket::kSendMoveItem*
SendMerchantRequestQuote = 0x30000000 | 0x6, // wparam = UIPacket::kSendMerchantRequestQuote*
SendMerchantTransactItem = 0x30000000 | 0x7, // wparam = UIPacket::kSendMerchantTransactItem*
SendUseItem = 0x30000000 | 0x8, // wparam = UIPacket::kSendUseItem*
SendSetActiveQuest = 0x30000000 | 0x9, // wparam = uint32_t quest_id
SendAbandonQuest = 0x30000000 | 0xA, // wparam = uint32_t quest_id
SendChangeTarget = 0x30000000 | 0xB, // wparam = UIPacket::kSendChangeTarget* // e.g. tell the gw client to focus on a different target
SendCallTarget = 0x30000000 | 0x13, // wparam = { uint32_t call_type, uint32_t agent_id } // also used to broadcast morale, death penalty, "I'm following X", etc
SendDialog = 0x30000000 | 0x16, // wparam = dialog_id // internal use
StartWhisper = 0x30000000 | 0x17, // wparam = UIPacket::kStartWhisper*
GetSenderColor = 0x30000000 | 0x18, // wparam = UIPacket::kGetColor* // Get chat sender color depending on channel, output object passed by reference
GetMessageColor = 0x30000000 | 0x19, // wparam = UIPacket::kGetColor* // Get chat message color depending on channel, output object passed by reference
SendChatMessage = 0x30000000 | 0x1B, // wparam = UIPacket::kSendChatMessage*
LogChatMessage = 0x30000000 | 0x1D, // wparam = UIPacket::kLogChatMessage*. Triggered when a message wants to be added to the persistent chat log.
RecvWhisper = 0x30000000 | 0x1E, // wparam = UIPacket::kRecvWhisper*
PrintChatMessage = 0x30000000 | 0x1F, // wparam = UIPacket::kPrintChatMessage*. Triggered when a message wants to be added to the in-game chat window.
SendWorldAction = 0x30000000 | 0x20, // wparam = UIPacket::kSendWorldAction*
SetRendererValue = 0x30000000 | 0x21 // wparam = UIPacket::kSetRendererValue
};
-46
View File
@@ -1,46 +0,0 @@
using System.Runtime.InteropServices;
namespace Daybreak.API.Models;
public static class UIPackets
{
public enum MouseButtons
{
Left = 0x0,
Middle = 0x1,
Right = 0x2
}
[StructLayout(LayoutKind.Sequential, Pack = 1)]
public readonly struct LogChatMessage(string message, Channel channel)
{
[MarshalAs(UnmanagedType.LPWStr)]
public readonly string Message = message;
public readonly Channel Channel = channel;
};
[StructLayout(LayoutKind.Sequential, Pack = 1)]
public readonly struct UIChatMessage(Channel channel, string message, Channel channel2)
{
public readonly Channel Channel = channel;
[MarshalAs(UnmanagedType.LPWStr)]
public readonly string Message = message;
public readonly Channel Channel2 = channel2;
}
[StructLayout(LayoutKind.Sequential, Pack = 1)]
public readonly struct KeyAction(uint key)
{
public readonly uint Key = key;
public readonly uint WParam = 0x4000;
public readonly uint LParam = 0x0006;
}
[StructLayout(LayoutKind.Sequential, Pack = 1)]
public readonly struct MouseClick(MouseButtons mouseButton, uint isDoubleClick, uint unknownTypeScreenPos)
{
public readonly MouseButtons MouseButton = mouseButton;
public readonly uint IsDoubleClick = isDoubleClick;
public readonly uint UnknownTypeScreenPos = unknownTypeScreenPos;
}
}
+18 -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(
@@ -48,4 +49,20 @@ internal unsafe static partial class NativeMethods
public static partial bool VirtualProtect(
void* lpAddress, nuint dwSize,
uint flNewProtect, out uint lpflOldProtect);
// Window enumeration
public delegate bool EnumWindowsProc(nint hwnd, nint lParam);
[LibraryImport("user32.dll", SetLastError = true)]
[return: MarshalAs(UnmanagedType.Bool)]
public static partial bool EnumWindows(EnumWindowsProc lpEnumFunc, nint lParam);
[LibraryImport("user32.dll", SetLastError = true)]
public static partial uint GetWindowThreadProcessId(nint hWnd, out uint lpdwProcessId);
[LibraryImport("user32.dll", SetLastError = true, StringMarshalling = StringMarshalling.Utf8)]
public static partial int GetClassNameA(nint hWnd, byte* lpClassName, int nMaxCount);
[LibraryImport("kernel32.dll")]
public static partial uint GetCurrentProcessId();
}
+4
View File
@@ -0,0 +1,4 @@
using System.Runtime.CompilerServices;
// Required for LibraryImport source generator to work with blittable types like Vector2, Vector3
[assembly: DisableRuntimeMarshalling]
@@ -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,17 +12,11 @@ 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))]
[JsonSerializable(typeof(HealthCheckResponse))]
[JsonSerializable(typeof(HealthCheckEntryResponse))]
[JsonSerializable(typeof(HookState))]
[JsonSerializable(typeof(ArraySegment<HookState>))]
[JsonSerializable(typeof(AddressState))]
[JsonSerializable(typeof(ArraySegment<AddressState>))]
[JsonSerializable(typeof(MainPlayerState))]
[JsonSerializable(typeof(QuestLogInformation))]
[JsonSerializable(typeof(QuestInformation))]
@@ -36,6 +31,9 @@ namespace Daybreak.API.Serialization;
[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);
}
}
+279 -143
View File
@@ -1,6 +1,11 @@
using Daybreak.API.Services.Interop;
using Daybreak.API.Extensions;
using Daybreak.API.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;
using System.Extensions.Core;
using System.Runtime.InteropServices;
using ZLinq;
@@ -13,20 +18,15 @@ public sealed class CharacterSelectService(
UIContextService uiContextService,
GameThreadService gameThreadService,
GameContextService gameContextService,
PreferencesService preferencesService,
ILogger<CharacterSelectService> logger)
{
[StructLayout(LayoutKind.Sequential, Pack = 1)]
private readonly struct LogOutMessage(uint unknown, uint characterSelect)
{
public readonly uint Unknown = unknown;
public readonly uint CharacterSelect = characterSelect;
}
private readonly InstanceContextService instanceContextService = instanceContextService.ThrowIfNull();
private readonly PlatformContextService platformContextService = platformContextService.ThrowIfNull();
private readonly UIContextService uiContextService = uiContextService.ThrowIfNull();
private readonly GameThreadService gameThreadService = gameThreadService.ThrowIfNull();
private readonly GameContextService gameContextService = gameContextService.ThrowIfNull();
private readonly PreferencesService preferencesService = preferencesService.ThrowIfNull();
private readonly ILogger<CharacterSelectService> logger = logger.ThrowIfNull();
public Task<CharacterSelectInformation?> GetCharacterSelectInformation(CancellationToken cancellationToken)
@@ -38,7 +38,7 @@ public sealed class CharacterSelectService(
{
var gameContext = this.gameContextService.GetGameContext();
if (gameContext.IsNull ||
gameContext.Pointer->WorldContext is null)
gameContext.Pointer->World is null)
{
scopedLogger.LogError("Game context is not initialized");
return default;
@@ -51,25 +51,33 @@ public sealed class CharacterSelectService(
return default;
}
var currentUuid = gameContext.Pointer->CharContext->PlayerUuid.ToString();
var currentUuid = (*(Uuid*)gameContext.Pointer->Character->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')]);
var name = new string(charContext.Name);
availableChars.Add(new CharacterSelectEntry(
charContext.Uuid.ToString(),
(*(Uuid*)charContext.Uuid).ToString(),
name,
charContext.Primary,
charContext.Secondary,
charContext.Campaign,
charContext.MapId,
charContext.Level,
charContext.IsPvp));
(uint)charContext.GetPrimaryProfession(),
(uint)charContext.GetSecondaryProfession(),
(uint)charContext.GetCampaign(),
(uint)charContext.GetMapId(),
(uint)charContext.GetLevel(),
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 +93,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 +121,257 @@ 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)
[StructLayout(LayoutKind.Sequential, Pack = 1)]
private unsafe struct CharSelectButtonParam
{
public char* Name;
public uint Play;
}
[StructLayout(LayoutKind.Explicit, Pack = 1)]
private 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)]
private readonly struct CharSelectorChar
{
[FieldOffset(0x0020)]
public readonly Array20Char Name;
}
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
uint selectedIdx = 0;
this.uiContextService.SendFrameUIMessage(panesFrame, UIMessage.kFrameMessage_0x4a, null, &selectedIdx);
// Find target character index
uint targetIdx = 0xFFFF;
var charCount = ctx.Pointer->Chars.Size;
for (uint i = 0; i < charCount; i++)
{
var c = ctx.Pointer->Chars.Buffer[i];
if (c.IsNull)
{
continue;
}
var charNameSpan = c.Pointer->Name.AsSpan();
var nullIdx = charNameSpan.IndexOf('\0');
if (nullIdx <= 0)
{
continue;
}
var charName = new string(charNameSpan[..nullIdx]);
if (!charName.Equals(characterName, StringComparison.OrdinalIgnoreCase))
{
continue;
}
targetIdx = i;
break;
}
if (targetIdx >= charCount)
{
scopedLogger.LogError("Character {name} not found in character list", characterName);
return false;
}
var parentFrame = this.uiContextService.GetParentFrame(selectorFrame);
if (parentFrame.IsNull)
{
scopedLogger.LogError("Parent frame not found");
return false;
}
// Helper function to select a character at a specific index
bool SelectChar(uint idx)
{
var charToSelect = ctx.Pointer->Chars.Buffer[idx];
if (charToSelect.IsNull)
{
return false;
}
fixed (char* namePtr = charToSelect.Pointer->Name.AsSpan())
{
var buttonParam = new CharSelectButtonParam
{
Name = namePtr,
Play = 0
};
var action = new kMouseAction
{
FrameId = selectorFrame->FrameId,
ChildOffsetId = selectorFrame->ChildOffsetId,
CurrentState = GWCA.GW.UI.UIPacket.ActionState.MouseClick,
Wparam = (nint)(&buttonParam)
};
if (!this.uiContextService.SendFrameUIMessage(parentFrame, UIMessage.kMouseClick2, &action))
{
return false;
}
uint newSelectedIdx = 0;
this.uiContextService.SendFrameUIMessage(panesFrame, UIMessage.kFrameMessage_0x4a, null, &newSelectedIdx);
return newSelectedIdx == idx;
}
}
// Navigate to target character by selecting previous/next until we reach it
while (targetIdx < selectedIdx)
{
if (selectedIdx == 0)
{
break;
}
if (!SelectChar(selectedIdx - 1))
{
scopedLogger.LogError("Failed to select previous character");
return false;
}
selectedIdx--;
}
while (targetIdx > selectedIdx)
{
if (selectedIdx >= charCount - 1)
{
break;
}
if (!SelectChar(selectedIdx + 1))
{
scopedLogger.LogError("Failed to select next character");
return false;
}
selectedIdx++;
}
var chosen = selectedIdx == targetIdx;
if (!chosen)
{
scopedLogger.LogError("Failed to select character {name}", characterName);
return false;
}
if (!play)
{
return true;
}
var playButton = this.uiContextService.GetFrameByLabel("Play");
if (playButton.IsNull)
{
scopedLogger.LogError("Play button not found");
return false;
}
return this.uiContextService.ButtonClick(playButton);
}
}, 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)
@@ -228,13 +397,12 @@ public sealed class CharacterSelectService(
foreach (var charContext in *availableCharsContext.Pointer)
{
if (charContext.Uuid.ToString() != uuid)
if ((*(Uuid*)charContext.Uuid).ToString() != uuid)
{
continue;
}
var nameSpan = charContext.Name.AsSpan();
var name = new string(nameSpan[..nameSpan.IndexOf('\0')]);
var name = new string(charContext.Name);
return name;
}
@@ -247,67 +415,35 @@ public sealed class CharacterSelectService(
{
await this.gameThreadService.QueueOnGameThread(() =>
{
var logoutMessage = new LogOutMessage(0, 0);
var logoutMessage = new kLogout { Unknown = 0, CharacterSelect = 1 };
unsafe
{
this.uiContextService.SendMessage(Models.UIMessage.Logout, (uint)&logoutMessage, 0);
this.uiContextService.SendMessage(UIMessage.kLogout, (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;
}
}
+9 -3
View File
@@ -1,6 +1,5 @@
using Daybreak.API.Interop;
using Daybreak.API.Interop.GuildWars;
using Daybreak.API.Models;
using Daybreak.API.Services.Interop;
using System.Core.Extensions;
using System.Extensions;
@@ -70,8 +69,15 @@ public sealed class ChatService(
span[^2] = '\u0001';
});
using var packet = new UnmanagedStruct<UIPackets.UIChatMessage>(new UIPackets.UIChatMessage(channel, encoded, channel));
this.uiContextService.SendMessage(UIMessage.WriteToChatLog, (nuint)packet.Address, 0x0);
unsafe
{
fixed (char* encodedPtr = encoded)
{
using var packet = new UnmanagedStruct<kPrintChatMessage>(new kPrintChatMessage { Channel = (GWCA.GW.Chat.Channel)channel, Message = (nint)encodedPtr });
this.uiContextService.SendMessage(UIMessage.kWriteToChatLog, (nuint)packet.Address, 0x0);
}
}
}, cancellationToken);
}
}
@@ -1,101 +1,11 @@
using Daybreak.API.Interop;
using Daybreak.API.Interop.GuildWars;
using Daybreak.API.Models;
using System.Core.Extensions;
using System.Extensions.Core;
namespace Daybreak.API.Services.Interop;
public unsafe sealed class AgentContextService : IAddressHealthService
public unsafe sealed class AgentContextService
{
private const string AgentArrayMask = "xxxxxxx";
private const int AgentArrayOffset = -0x4;
private static readonly byte[] AgentArrayAddressPattern = [0x8b, 0x0c, 0x90, 0x85, 0xc9, 0x74, 0x19];
public uint GetPlayerAgentId() => GWCA.GW.PlayerMgr.GetPlayerAgentId(0);
private const string PlayerAgentIdMask = "xx????xxxx";
private const int PlayerAgentIdOffset = -0xE;
private static readonly byte[] PlayerAgentIdAddressPattern = [0x5d, 0xe9, 0x00, 0x00, 0x00, 0x00, 0x55, 0x8b, 0xec, 0x53];
private readonly MemoryScanningService memoryScanningService;
private readonly GWAddressCache agentArrayAddress;
private readonly GWAddressCache playerAgentIdAddress;
private readonly ILogger<AgentContextService> logger;
public AgentContextService(
MemoryScanningService memoryScanningService,
ILogger<AgentContextService> logger)
{
this.logger = logger.ThrowIfNull();
this.memoryScanningService = memoryScanningService.ThrowIfNull();
this.agentArrayAddress = new GWAddressCache(this.GetAgentArrayAddress);
this.playerAgentIdAddress = new GWAddressCache(this.GetPreGameContextAddress);
}
public List<AddressState> GetAddressStates()
{
return [
new AddressState
{
Address = this.agentArrayAddress.GetAddress() ?? 0,
Name = nameof(this.agentArrayAddress),
},
new AddressState
{
Address = this.playerAgentIdAddress.GetAddress() ?? 0,
Name = nameof(this.playerAgentIdAddress),
},
];
}
public WrappedPointer<GuildWarsArray<WrappedPointer<AgentContext>>> GetAgentArray()
{
var agentArrayAddress = this.agentArrayAddress.GetAddress();
if (agentArrayAddress is null or 0x0)
{
this.logger.LogError("Failed to get agent array address");
return null;
}
return (GuildWarsArray<WrappedPointer<AgentContext>>*)agentArrayAddress;
}
public uint GetPlayerAgentId()
{
var playerAgentIdAddress = this.playerAgentIdAddress.GetAddress();
if (playerAgentIdAddress is null or 0x0)
{
this.logger.LogError("Failed to get player agent id address");
return 0;
}
return *(uint*)playerAgentIdAddress;
}
private nuint GetAgentArrayAddress()
{
var scopedLogger = this.logger.CreateScopedLogger();
var address = this.memoryScanningService.FindAndResolveAddress(AgentArrayAddressPattern, AgentArrayMask, AgentArrayOffset);
if (address is 0)
{
scopedLogger.LogError("Failed to find agent array address");
return 0U;
}
scopedLogger.LogInformation("Agent array address: 0x{address:X8}", address);
return address;
}
private nuint GetPreGameContextAddress()
{
var scopedLogger = this.logger.CreateScopedLogger();
var address = this.memoryScanningService.FindAndResolveAddress(PlayerAgentIdAddressPattern, PlayerAgentIdMask, PlayerAgentIdOffset);
if (address is 0)
{
scopedLogger.LogError("Failed to find player agent id address");
return 0U;
}
scopedLogger.LogInformation("Player agent id address: 0x{address:X8}", address);
return address;
}
public Agent* GetAgentById(uint agentId) => GWCA.GW.Agents.GetAgentByID(agentId);
}
@@ -1,85 +0,0 @@
using Daybreak.API.Interop;
using Daybreak.API.Models;
using System.Core.Extensions;
using System.Extensions.Core;
using System.Runtime.InteropServices;
using System.Security;
namespace Daybreak.API.Services.Interop;
public sealed class ChatHandlingService
: IHostedService, IHookHealthService
{
private const string AddToChatLogMask = "xxxxxx";
private static readonly byte[] AddToChatLogSeq = [0x40, 0x25, 0xFF, 0x01, 0x00, 0x00];
[SuppressUnmanagedCodeSecurity]
[UnmanagedFunctionPointer(CallingConvention.Cdecl)]
private delegate bool AddToChatLog([MarshalAs(UnmanagedType.LPWStr)] string message, Channel channel);
private readonly MemoryScanningService memoryScanningService;
private readonly GWHook<AddToChatLog> addToChatLogHook;
private readonly ILogger<ChatHandlingService> logger;
private CancellationTokenSource? cts = default;
public ChatHandlingService(
MemoryScanningService memoryScanningService,
ILogger<ChatHandlingService> logger)
{
this.logger = logger.ThrowIfNull();
this.memoryScanningService = memoryScanningService.ThrowIfNull();
this.addToChatLogHook = new GWHook<AddToChatLog>(
new GWAddressCache(() => this.memoryScanningService.ToFunctionStart(
this.memoryScanningService.FindAddress(AddToChatLogSeq, AddToChatLogMask))),
this.OnAddToChatLog);
}
public Task StartAsync(CancellationToken cancellationToken)
{
this.cts?.Dispose();
this.cts = new CancellationTokenSource();
return Task.Factory.StartNew(() => this.InitializeHooks(this.cts.Token), this.cts.Token, TaskCreationOptions.LongRunning, TaskScheduler.Current);
}
public Task StopAsync(CancellationToken cancellationToken)
{
this.cts?.Cancel();
this.cts?.Dispose();
this.addToChatLogHook.Dispose();
return Task.CompletedTask;
}
public List<HookState> GetHookStates()
{
return
[
new HookState
{
Hooked = this.addToChatLogHook.Hooked,
Name = nameof(this.addToChatLogHook),
TargetAddress = new PointerValue(this.addToChatLogHook.TargetAddress),
ContinueAddress = new PointerValue(this.addToChatLogHook.ContinueAddress),
DetourAddress = new PointerValue(this.addToChatLogHook.DetourAddress)
}
];
}
private async ValueTask InitializeHooks(CancellationToken cancellationToken)
{
var scopedLogger = this.logger.CreateScopedLogger();
scopedLogger.LogInformation("Initializing hooks");
while (!this.addToChatLogHook.EnsureInitialized())
{
scopedLogger.LogDebug("Waiting for hook to initialize: {hook}", nameof(this.addToChatLogHook));
await Task.Delay(1000, cancellationToken);
}
scopedLogger.LogInformation("Hooks initialized");
}
private bool OnAddToChatLog(string message, Channel channel)
{
return this.addToChatLogHook.Continue(message, channel);
}
}
@@ -1,181 +1,16 @@
using Daybreak.API.Interop;
using Daybreak.API.Interop.GuildWars;
using Daybreak.API.Models;
using System.Core.Extensions;
using System.Extensions.Core;
namespace Daybreak.API.Services.Interop;
public unsafe sealed class GameContextService : IAddressHealthService
public unsafe sealed class GameContextService
{
private const string BaseAddressMask = "xxxxxxx";
private const int BaseAddressOffset = +7;
private static readonly byte[] BaseAddressPattern = [0x50, 0x6A, 0x0F, 0x6A, 0x00, 0xFF, 0x35];
private const string PreGameContextFile = "UiPregame.cpp";
private const string PreGameContextAssertion = "!s_scene";
private const int PreGameContextOffset = 0x34;
public WrappedPointer<GameContext> GetGameContext() => GWCA.GW.GetGameContext();
private const string AvailableCharsMask = "xx????xxxxxxx";
private const int AvailableCharsOffset = 0x2;
private static readonly byte[] AvailableCharsPattern = [0x8B, 0x35, 0x00, 0x00, 0x00, 0x00, 0x57, 0x69, 0xF8, 0x84, 0x00, 0x00, 0x00];
public WrappedPointer<PreGameContext> GetPreGameContext() => GWCA.GW.GetPreGameContext();
private readonly MemoryScanningService memoryScanningService;
private readonly GWAddressCache baseAddress;
private readonly GWAddressCache preGameContextAddress;
private readonly GWAddressCache availableCharsAddress;
private readonly ILogger<GameContextService> logger;
public WrappedPointer<GuildWarsArray<CharacterInformation>> GetAvailableChars() => GWCA.GW.GetAvailableChars();
public GameContextService(
MemoryScanningService memoryScanningService,
ILogger<GameContextService> logger)
{
this.logger = logger.ThrowIfNull();
this.memoryScanningService = memoryScanningService.ThrowIfNull();
this.baseAddress = new GWAddressCache(this.GetBaseAddress);
this.preGameContextAddress = new GWAddressCache(this.GetPreGameContextAddress);
this.availableCharsAddress = new GWAddressCache(this.GetAvailableCharactersAddress);
}
public List<AddressState> GetAddressStates()
{
return [
new AddressState
{
Address = this.baseAddress.GetAddress() ?? 0,
Name = nameof(this.baseAddress),
},
new AddressState
{
Address = this.preGameContextAddress.GetAddress() ?? 0,
Name = nameof(this.preGameContextAddress),
},
new AddressState
{
Address = this.availableCharsAddress.GetAddress() ?? 0,
Name = nameof(this.availableCharsAddress),
}
];
}
public WrappedPointer<GameContext> GetGameContext()
{
var gameContextAddress = this.GetGameContextAddress();
if (gameContextAddress is 0x0)
{
this.logger.LogError("Failed to get game context address");
return null;
}
return (GameContext*)gameContextAddress;
}
public WrappedPointer<PreGameContext> GetPreGameContext()
{
var preGameContextAddress = this.preGameContextAddress.GetAddress();
if (preGameContextAddress is null or 0x0)
{
this.logger.LogError("Failed to get pre-game context address");
return null;
}
return *(PreGameContext**)preGameContextAddress;
}
public WrappedPointer<GuildWarsArray<CharInfoContext>> GetAvailableChars()
{
var availableCharsAddress = this.availableCharsAddress.GetAddress();
if (availableCharsAddress is null or 0x0)
{
this.logger.LogError("Failed to get available chars address");
return null;
}
return *(GuildWarsArray<CharInfoContext>**)availableCharsAddress;
}
public bool IsMapLoaded()
{
var gameContext = this.GetGameContext();
if (gameContext.IsNull)
{
return false;
}
if (gameContext.Pointer->MapContext is null)
{
return false;
}
return true;
}
private nuint GetGameContextAddress()
{
var scopedLogger = this.logger.CreateScopedLogger();
if (this.baseAddress.GetAddress() is not nuint basePtr)
{
scopedLogger.LogError("Failed to get base address");
return default;
}
nuint** baseContext = *(nuint***)basePtr;
if (baseContext is null)
{
scopedLogger.LogError("Failed to get base context address");
return default;
}
var globalContextPtr = baseContext[0x6];
if (globalContextPtr is null)
{
scopedLogger.LogError("Failed to get game context address");
return default;
}
return (nuint)globalContextPtr;
}
private nuint GetBaseAddress()
{
var scopedLogger = this.logger.CreateScopedLogger();
var baseAddress = this.memoryScanningService.FindAndResolveAddress(BaseAddressPattern, BaseAddressMask, BaseAddressOffset);
if (baseAddress is 0)
{
scopedLogger.LogError("Failed to find base address");
return 0U;
}
scopedLogger.LogInformation("Base address: 0x{address:X8}", baseAddress);
return baseAddress;
}
private unsafe nuint GetPreGameContextAddress()
{
var scopedLogger = this.logger.CreateScopedLogger();
var preGameContextAddress = this.memoryScanningService.FindAssertion(PreGameContextFile, PreGameContextAssertion, 0, PreGameContextOffset);
if (preGameContextAddress is 0)
{
scopedLogger.LogError("Failed to find pre-game context address");
return 0U;
}
preGameContextAddress = *(nuint*)preGameContextAddress;
scopedLogger.LogInformation("Pre-game context address: 0x{address:X8}", preGameContextAddress);
return preGameContextAddress;
}
private unsafe nuint GetAvailableCharactersAddress()
{
var scopedLogger = this.logger.CreateScopedLogger();
var availableCharsAddress = this.memoryScanningService.FindAddress(AvailableCharsPattern, AvailableCharsMask, AvailableCharsOffset);
if (availableCharsAddress is 0)
{
scopedLogger.LogError("Failed to find available chars address");
return 0U;
}
scopedLogger.LogInformation("Available chars address: 0x{address:X8}", availableCharsAddress);
return availableCharsAddress;
}
public bool IsMapLoaded() => GWCA.GW.Map.GetIsMapLoaded();
}
@@ -1,142 +1,92 @@
using Daybreak.API.Interop;
using Daybreak.API.Models;
using System.Collections.Concurrent;
using System.Core.Extensions;
using System.Extensions.Core;
using System.Runtime.InteropServices;
using System.Security;
namespace Daybreak.API.Services.Interop;
public sealed class GameThreadService
: IHostedService, IHookHealthService
{
private const string LeaveGameThreadFile = "FrApi.cpp";
private const string LeaveGameThreadAssertion = "renderElapsed >= 0";
[SuppressUnmanagedCodeSecurity]
[UnmanagedFunctionPointer(CallingConvention.Cdecl)]
private delegate void LeaveGameThread(nint ctx);
private delegate void GameThreadCallback();
private readonly MemoryScanningService memoryScanningService;
private readonly GWHook<LeaveGameThread> leaveGameThreadHook;
private readonly ILogger<GameThreadService> logger;
private readonly ConcurrentQueue<IWorkItem> queuedItems = [];
private readonly ConcurrentDictionary<Guid, Action> registeredCallbacks = [];
private CancellationTokenSource? cts = default;
public GameThreadService(
MemoryScanningService memoryScanningService,
ILogger<GameThreadService> logger)
{
this.logger = logger.ThrowIfNull();
this.memoryScanningService = memoryScanningService.ThrowIfNull();
this.leaveGameThreadHook = new GWHook<LeaveGameThread>(
new GWAddressCache(() => this.memoryScanningService.ToFunctionStart(
this.memoryScanningService.FindAssertion(LeaveGameThreadFile, LeaveGameThreadAssertion, 0, 0), 0x300)),
this.OnLeaveGameThread);
}
public Task StartAsync(CancellationToken cancellationToken)
{
this.cts?.Dispose();
this.cts = new CancellationTokenSource();
return Task.Factory.StartNew(() => this.InitializeHooks(this.cts.Token), this.cts.Token, TaskCreationOptions.LongRunning, TaskScheduler.Current);
}
public Task StopAsync(CancellationToken cancellationToken)
{
this.cts?.Cancel();
this.cts?.Dispose();
this.leaveGameThreadHook.Dispose();
return Task.CompletedTask;
}
// Prevent GC of delegates passed to native code
private readonly ConcurrentDictionary<GameThreadCallback, bool> prevent_GC_callbacks = [];
public Task QueueOnGameThread(Action action, CancellationToken cancellationToken)
{
var taskCompletionSource = new TaskCompletionSource();
this.queuedItems.Enqueue(new WorkItem(action, taskCompletionSource, cancellationToken));
return taskCompletionSource.Task;
}
var item = new WorkItem(action, taskCompletionSource, cancellationToken);
public Task<T> QueueOnGameThread<T>(Func<T> action, CancellationToken cancellationToken)
{
var taskCompletionSource = new TaskCompletionSource<T>();
this.queuedItems.Enqueue(new WorkItem<T>(action, taskCompletionSource, cancellationToken));
return taskCompletionSource.Task;
}
public CallbackRegistration RegisterCallback(Action action)
{
var uid = new Guid();
var registration = new CallbackRegistration(uid, () => this.registeredCallbacks.TryRemove(uid, out _));
this.registeredCallbacks[uid] = action;
return registration;
}
public List<HookState> GetHookStates()
{
return
[
new HookState
{
Hooked = this.leaveGameThreadHook.Hooked,
Name = nameof(this.leaveGameThreadHook),
TargetAddress = new PointerValue(this.leaveGameThreadHook.TargetAddress),
ContinueAddress = new PointerValue(this.leaveGameThreadHook.ContinueAddress),
DetourAddress = new PointerValue(this.leaveGameThreadHook.DetourAddress)
}
];
}
private async ValueTask InitializeHooks(CancellationToken cancellationToken)
{
var scopedLogger = this.logger.CreateScopedLogger();
scopedLogger.LogInformation("Initializing hooks");
while (!this.leaveGameThreadHook.EnsureInitialized())
{
scopedLogger.LogDebug("Waiting for hook to initialize: {hook}", nameof(this.leaveGameThreadHook));
await Task.Delay(1000, cancellationToken);
}
scopedLogger.LogInformation("Hooks initialized");
}
private void OnLeaveGameThread(nint ctx)
{
var scopedLogger = this.logger.CreateScopedLogger();
while(this.queuedItems.TryDequeue(out var item))
GameThreadCallback callback = null!;
callback = () =>
{
try
{
if (item.CancellationToken.IsCancellationRequested)
{
item.Cancel();
continue;
return;
}
item.Execute();
}
catch(Exception ex)
catch (Exception ex)
{
scopedLogger.LogError(ex, "Error executing action on game thread");
item.Exception(ex);
}
}
finally
{
this.prevent_GC_callbacks.TryRemove(callback, out _);
}
};
foreach(var callback in this.registeredCallbacks)
// CRITICAL: Keep delegate alive until callback executes
this.prevent_GC_callbacks[callback] = true;
var funcPtr = Marshal.GetFunctionPointerForDelegate(callback);
GWCA.GW.GameThread.Enqueue(funcPtr, false);
return taskCompletionSource.Task;
}
public Task<T> QueueOnGameThread<T>(Func<T> action, CancellationToken cancellationToken)
{
var taskCompletionSource = new TaskCompletionSource<T>();
var item = new WorkItem<T>(action, taskCompletionSource, cancellationToken);
GameThreadCallback callback = null!;
callback = () =>
{
try
{
callback.Value();
}
catch(Exception ex)
{
scopedLogger.LogError(ex, "Error executing registered callback {uid}", callback.Key);
}
}
if (item.CancellationToken.IsCancellationRequested)
{
item.Cancel();
return;
}
this.leaveGameThreadHook.Continue(ctx);
item.Execute();
}
catch (Exception ex)
{
item.Exception(ex);
}
finally
{
// Remove from prevent-GC set after execution
this.prevent_GC_callbacks.TryRemove(callback, out _);
}
};
// CRITICAL: Keep delegate alive until callback executes
this.prevent_GC_callbacks[callback] = true;
var funcPtr = Marshal.GetFunctionPointerForDelegate(callback);
GWCA.GW.GameThread.Enqueue(funcPtr, false);
return taskCompletionSource.Task;
}
}
@@ -1,8 +0,0 @@
using Daybreak.API.Models;
namespace Daybreak.API.Services.Interop;
public interface IAddressHealthService
{
List<AddressState> GetAddressStates();
}
@@ -1,8 +0,0 @@
using Daybreak.API.Models;
namespace Daybreak.API.Services.Interop;
public interface IHookHealthService
{
List<HookState> GetHookStates();
}
@@ -1,104 +1,13 @@
using Daybreak.API.Interop;
using Daybreak.API.Interop.GuildWars;
using Daybreak.API.Models;
using System.Core.Extensions;
namespace Daybreak.API.Services.Interop;
public sealed unsafe class InstanceContextService : IAddressHealthService
public sealed unsafe class InstanceContextService
{
private const string InstanceInfoAddressMask = "xxxx????xxxx";
private const int InstanceInfoAddressOffset = +0xD;
private static readonly byte[] InstanceInfoAddressPattern = [0x6A, 0x2C, 0x50, 0xE8, 0x00, 0x00, 0x00, 0x00, 0x83, 0xC4, 0x08, 0xC7];
private const string AreaInfoAddressMask = "xxxxx";
private const int AreaInfoAddressOffset = +0x5;
private static readonly byte[] AreaInfoAddressPattern = [0x6B, 0xC6, 0x7C, 0x5E, 0x05];
private const string ServerRegionAddressMask = "xxxxxxx";
private const int ServerRegionAddressOffset = -0x4;
private static readonly byte[] ServerRegionAddressPattern = [0x6A, 0x54, 0x8D, 0x46, 0x24, 0x89, 0x08];
public WrappedPointer<AreaInfo> GetAreaInfo() => GWCA.GW.Map.GetMapInfo(0);
private readonly MemoryScanningService memoryScanningService;
private readonly GWAddressCache instanceInfoAddress;
private readonly GWAddressCache areaInfoAddress;
private readonly GWAddressCache serverRegionAddress;
private readonly ILogger<InstanceContextService> logger;
public ServerRegion GetServerRegion() => (Daybreak.API.Interop.GuildWars.ServerRegion)GWCA.GW.Map.GetRegion();
public InstanceContextService(
MemoryScanningService memoryScanningService,
ILogger<InstanceContextService> logger)
{
this.logger = logger.ThrowIfNull();
this.memoryScanningService = memoryScanningService.ThrowIfNull();
this.instanceInfoAddress = new GWAddressCache(() => this.memoryScanningService.FindAndResolveAddress(InstanceInfoAddressPattern, InstanceInfoAddressMask, InstanceInfoAddressOffset));
this.areaInfoAddress = new GWAddressCache(() => this.memoryScanningService.FindAndResolveAddress(AreaInfoAddressPattern, AreaInfoAddressMask, AreaInfoAddressOffset));
this.serverRegionAddress = new GWAddressCache(() => this.memoryScanningService.FindAndResolveAddress(ServerRegionAddressPattern, ServerRegionAddressMask, ServerRegionAddressOffset));
}
public List<AddressState> GetAddressStates()
{
return [
new AddressState
{
Address = this.instanceInfoAddress.GetAddress() ?? 0,
Name = nameof(this.instanceInfoAddress),
},
new AddressState
{
Address = this.areaInfoAddress.GetAddress() ?? 0,
Name = nameof(this.areaInfoAddress),
},
new AddressState
{
Address = this.serverRegionAddress.GetAddress() ?? 0,
Name = nameof(this.serverRegionAddress),
}
];
}
public WrappedPointer<InstanceInfoContext> GetInstanceInfoContext()
{
var instanceInfoAddress = this.instanceInfoAddress.GetAddress();
if (instanceInfoAddress is null)
{
this.logger.LogError("Failed to get instance info address");
return null;
}
return (InstanceInfoContext*)instanceInfoAddress;
}
public WrappedPointer<AreaInfo> GetAreaInfo()
{
var areaInfoAddress = this.areaInfoAddress.GetAddress();
if (areaInfoAddress is null)
{
this.logger.LogError("Failed to get area info address");
return null;
}
return (AreaInfo*)areaInfoAddress;
}
public ServerRegion GetServerRegion()
{
var serverRegionAddress = this.serverRegionAddress.GetAddress();
if (serverRegionAddress is null)
{
this.logger.LogError("Failed to get server region address");
return ServerRegion.Unknown;
}
return *(ServerRegion*)serverRegionAddress;
}
public InstanceType GetInstanceType()
{
var instanceInfo = this.GetInstanceInfoContext();
if (instanceInfo.IsNull)
{
return InstanceType.Loading;
}
return instanceInfo.Pointer->InstanceType;
}
public InstanceType GetInstanceType() => (Daybreak.API.Interop.GuildWars.InstanceType)GWCA.GW.Map.GetInstanceType();
}
@@ -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;
}
}
@@ -1,99 +1,15 @@
using Daybreak.API.Interop;
using Daybreak.API.Models;
using System.Core.Extensions;
using System.Extensions;
using static Daybreak.API.Interop.GWCA.GW.Constants;
namespace Daybreak.API.Services.Interop;
public sealed class PartyContextService
: IAddressHealthService
{
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";
public bool AddHero(uint heroId) => GWCA.GW.PartyMgr.AddHero((HeroID)heroId);
// 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>
private readonly GWFastCall<nint, uint, nint, GWFastCall.Void> searchButtonCallback;
// This fastcall equivalent is <void*, uint, uint*, void>
private readonly GWFastCall<nint, uint, nint, GWFastCall.Void> windowButtonCallback;
public bool KickHero(uint heroId) => GWCA.GW.PartyMgr.KickHero((HeroID)heroId);
private readonly MemoryScanningService memoryScanningService;
private readonly ILogger<PartyContextService> logger;
public bool KickAllHeroes() => GWCA.GW.PartyMgr.KickAllHeroes();
public PartyContextService(
MemoryScanningService memoryScanningService,
ILogger<PartyContextService> logger)
{
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))));
}
public List<AddressState> GetAddressStates()
{
unsafe
{
return
[
new AddressState
{
Address = this.searchButtonCallback.Cache.GetAddress() ?? 0,
Name = nameof(this.searchButtonCallback)
},
new AddressState
{
Address = this.windowButtonCallback.Cache.GetAddress() ?? 0,
Name = nameof(this.windowButtonCallback)
}
];
}
}
public unsafe bool CallSearchButtonCallback(void* ctx, uint edx, uint* wparam)
{
this.searchButtonCallback.Invoke((nint)ctx, edx, (nint)wparam);
return true;
}
public unsafe bool CallWindowButtonCallback(void* ctx, uint edx, uint* wparam)
{
this.windowButtonCallback.Invoke((nint)ctx, edx, (nint)wparam);
return true;
}
public unsafe bool AddHero(uint heroId)
{
var wparam = stackalloc uint[4];
wparam[2] = 0x6;
wparam[1] = 0x1;
var ctx = stackalloc uint[13];
ctx[0xb] = 1;
ctx[9] = heroId;
return this.CallSearchButtonCallback(ctx, 2, wparam);
}
public unsafe bool KickHero(uint heroId)
{
var wparam = stackalloc uint[4];
wparam[2] = 0x6;
wparam[1] = 0x6;
var ctx = stackalloc uint[13];
ctx[0xb] = 1;
ctx[9] = heroId;
return this.CallSearchButtonCallback(ctx, 0, wparam);
}
public bool KickAllHeroes() => this.KickHero(0x26);
public unsafe bool LeaveParty()
{
var ctx = stackalloc uint[14];
ctx[0xd] = 1;
return this.CallWindowButtonCallback(ctx, 0, (uint*)0);
}
public bool LeaveParty() => GWCA.GW.PartyMgr.LeaveParty();
}

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