Compare commits

...
135 Commits
Author SHA1 Message Date
amacocian fe4720c5e1 Update GWCA - fixes Character Select widget (#1626) 2026-09-09 22:58:09 +02:00
github-actions[bot]andamacocian b57d2ad5d3 Bump version to 0.9.10.21 2026-09-09 22:58:09 +02:00
amacocian 5faeae64d3 Download service robustness (Closes #1074) (#1624) 2026-09-04 19:34:25 +02:00
amacocian fdcf238d68 Fix wine password param expansion (#1623) 2026-09-04 19:34:25 +02:00
amacocian d9f7d05672 Add widgets for empty views (Closes #1093) (#1622) 2026-09-04 19:34:25 +02:00
amacocian c61ee2ecd7 Fix toolbox assertion error in wine due to long computer name (Closes #1619) (#1621) 2026-09-04 19:34:25 +02:00
amacocian 82512f9461 Improve documentation (#1620) 2026-09-04 19:34:25 +02:00
github-actions[bot]andamacocian 2dc13b7184 Bump version to 0.9.10.20 2026-09-04 19:34:25 +02:00
amacocian da691e3310 Improvements to skill updater (#1617) 2026-08-31 23:07:24 +02:00
amacocian 5e020f51d9 Update skills (#1616) 2026-08-31 23:07:24 +02:00
github-actions[bot]andamacocian 1f63e59b8a Bump version to 0.9.10.19 2026-08-31 23:07:24 +02:00
amacocian dcb9c29505 Update deps (#1612)
* Update dependencies

* Fix GWCA issues
2026-08-30 23:27:31 +02:00
amacocian db83e5a954 Setup stable computer name in Wine prefix (Closes #1609) (#1611) 2026-08-30 23:27:31 +02:00
amacocian c7c5506ee0 Update GWCA (#1610) 2026-08-30 23:27:31 +02:00
github-actions[bot]andamacocian 2988b6b38d Bump version to 0.9.10.18 2026-08-30 23:27:31 +02:00
amacocian debef9a36f Update documentation with Steam support (#1603) 2026-08-02 15:01:35 +02:00
amacocian 839bc57b48 Improve interaction with system shell (#1601) 2026-08-02 15:01:35 +02:00
amacocian 175c16bd00 Notifications show an animation for their expiration (#1600) 2026-08-02 15:01:35 +02:00
amacocian ad356f431e Steam login support (Windows) (Closes #1597) (#1599) 2026-08-02 15:01:35 +02:00
github-actions[bot]andamacocian ebe368e61e Bump version to 0.9.10.17 2026-08-02 15:01:35 +02:00
amacocian d800a630b5 Sanitize secrets (#1595) 2026-07-28 14:55:57 +02:00
github-actions[bot]andamacocian 239f91a65f Bump version to 0.9.10.16 2026-07-28 14:55:57 +02:00
dependabot[bot]amacociandependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
aff7c24b6a Bump actions/setup-dotnet from 5 to 6 (#1588)
Bumps [actions/setup-dotnet](https://github.com/actions/setup-dotnet) from 5 to 6.
- [Release notes](https://github.com/actions/setup-dotnet/releases)
- [Commits](https://github.com/actions/setup-dotnet/compare/v5...v6)

---
updated-dependencies:
- dependency-name: actions/setup-dotnet
  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-07-28 13:54:14 +02:00
amacocian 5bf0af61ba Fix commandline arguments with space (#1593) 2026-07-28 13:54:14 +02:00
amacocian 03ed31e85b Fix telemetry (Closes #1591) (#1592) 2026-07-28 13:54:14 +02:00
amacocian b21a4482bd Redact sensitive logs (#1590) 2026-07-28 13:54:14 +02:00
github-actions[bot]andamacocian 2ad8ae79ce Bump version to 0.9.10.15 2026-07-28 13:54:14 +02:00
amacocianandAlexandru Macocian a4be747dd5 Fix more notifications expiration times (#1587)
Co-authored-by: Alexandru Macocian <amacocian@microsoft.com>
2026-07-18 07:00:00 +02:00
amacocianandAlexandru Macocian a6cd5d6f37 Update dependencies (#1585)
Co-authored-by: Alexandru Macocian <amacocian@microsoft.com>
2026-07-18 07:00:00 +02:00
amacocianandAlexandru Macocian 0e80fb7d86 Update GWCA (#1584)
Co-authored-by: Alexandru Macocian <amacocian@microsoft.com>
2026-07-18 07:00:00 +02:00
github-actions[bot]andamacocian 0793f181bb Bump version to 0.9.10.14 2026-07-18 07:00:00 +02:00
amacocian a9ee0424f7 FocusView browser maximize (Closes #1577) (#1578) 2026-07-03 18:45:45 +02:00
amacocian 49f45b6b76 Fix notifications disappearing too soon (Closes #1573) (#1576) 2026-07-03 18:45:45 +02:00
github-actions[bot]andamacocian 4216a03438 Bump version to 0.9.10.13 2026-07-03 18:45:45 +02:00
amacocianandAlexandru Macocian a9a8346a5b Fix character select (Closes #1561) (#1571)
Co-authored-by: Alexandru Macocian <amacocian@microsoft.com>
2026-06-30 17:09:16 +02:00
amacocianandAlexandru Macocian 70a95b5e08 Executable list eviction limit only on invalid exes (#1570)
Co-authored-by: Alexandru Macocian <amacocian@microsoft.com>
2026-06-30 17:09:16 +02:00
amacocianandAlexandru Macocian 1e8c2e44e2 Fix Kill command on Linux (Closes #1564) (#1569)
Co-authored-by: Alexandru Macocian <amacocian@microsoft.com>
2026-06-30 17:09:16 +02:00
amacocianandAlexandru Macocian d553aadead Update skills after patch update (Closes #1565) (#1568)
Co-authored-by: Alexandru Macocian <amacocian@microsoft.com>
2026-06-30 17:09:16 +02:00
amacocianandAlexandru Macocian fabb98f74e Improve exe matching on Linux (#1567)
Co-authored-by: Alexandru Macocian <amacocian@microsoft.com>
2026-06-30 17:09:16 +02:00
amacocianandAlexandru Macocian 0a0ec5cefc Fix multibox support on Linux (#1566)
Co-authored-by: Alexandru Macocian <amacocian@microsoft.com>
2026-06-30 17:09:16 +02:00
amacocianandAlexandru Macocian 0d31587855 Revert GWCA static linking (Closes #1562) (#1563)
Co-authored-by: Alexandru Macocian <amacocian@microsoft.com>
2026-06-30 17:09:16 +02:00
github-actions[bot]andamacocian f4a08e36dd Bump version to 0.9.10.12 2026-06-30 17:09:16 +02:00
amacocian c71dd6ed9c Update docs (#1559) 2026-06-29 22:36:44 +02:00
amacocianandAlexandru Macocian a61cff8dfd Focus View Grid layout and resizable components (#1558) (Closes #1552)
Co-authored-by: Alexandru Macocian <amacocian@microsoft.com>
2026-06-29 22:36:44 +02:00
amacocianandAlexandru Macocian 9d7f145c25 Sign windows binaries (#1557)
* Sign windows binaries

* Fix temp test

* Remove temporary test

---------

Co-authored-by: Alexandru Macocian <amacocian@microsoft.com>
2026-06-29 22:36:44 +02:00
amacocianandAlexandru Macocian dc340189d6 Improve x11 support in Linux with GDK X11 fallback and scale support (#1556) (Closes #1551)
* Fix GDK X11 support

* Better scaling support for Linux

---------

Co-authored-by: Alexandru Macocian <amacocian@microsoft.com>
2026-06-29 22:36:44 +02:00
amacocianandAlexandru Macocian 5edb5ff780 Update dependencies + move to StbImageSharp (#1555)
Co-authored-by: Alexandru Macocian <amacocian@microsoft.com>
2026-06-29 22:36:44 +02:00
amacocianandAlexandru Macocian 890bb26c01 Make Daybreak.API compile statically against GWCA (#1554)
Co-authored-by: Alexandru Macocian <amacocian@microsoft.com>
2026-06-29 22:36:44 +02:00
amacocianandAlexandru Macocian 6c2574c722 Debug scripts improvements (#1553)
Co-authored-by: Alexandru Macocian <amacocian@microsoft.com>
2026-06-29 22:36:44 +02:00
dependabot[bot]amacociandependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
8760f743ab Bump Microsoft.Web.WebView2 from 1.0.3967.48 to 1.0.4022.49 (#1548)
---
updated-dependencies:
- dependency-name: Microsoft.Web.WebView2
  dependency-version: 1.0.4022.49
  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-06-29 22:36:44 +02:00
dependabot[bot]andamacocian 97da183bd3 Bump actions/checkout from 6 to 7 (#1549) 2026-06-29 22:36:44 +02:00
github-actions[bot]andamacocian 65439f25dc Bump version to 0.9.10.11 2026-06-29 22:36:44 +02:00
amacocian a755319fb1 Add value to inventory service response (Closes #1545) (#1546) 2026-06-03 22:09:58 +02:00
amacocian 7e72cb3d3f Add more testing coverage (#1543) 2026-06-03 22:09:58 +02:00
amacocian ca154827b2 Setup linux api build and some tests (#1542) 2026-06-03 22:09:58 +02:00
amacocian 089a10a056 Reorganize skills (#1541)
* Reorganize skills

* Minor fixes
2026-06-03 22:09:58 +02:00
amacocian 65767d6ab5 Fix build paths for linux (Closes #1539) (#1540) 2026-06-03 22:09:58 +02:00
github-actions[bot]andamacocian 5828263717 Bump version to 0.9.10.10 2026-06-03 22:09:58 +02:00
amacocian 05f73cb306 GWCA fixes (#1538) 2026-05-13 14:37:06 +02:00
amacocian 0271e6b08c Fix gwca generator (#1537) 2026-05-13 14:37:06 +02:00
amacocian 640f28584e Implement logsview filter (#1535) 2026-05-13 14:37:06 +02:00
amacocian dd89e3316e Improve toolbox integration (Closes #1532) (#1534) 2026-05-13 14:37:06 +02:00
dependabot[bot]amacociandependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
10fb70787b Bump Microsoft.Web.WebView2 from 1.0.3912.50 to 1.0.3967.48 (#1533)
---
updated-dependencies:
- dependency-name: Microsoft.Web.WebView2
  dependency-version: 1.0.3967.48
  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-05-13 14:37:06 +02:00
github-actions[bot]andamacocian 1393223826 Bump version to 0.9.10.9 2026-05-13 14:37:06 +02:00
amacocian b23907025d Update dependencies (#1530) 2026-04-29 15:48:06 +02:00
amacocian 09aed18d9d Fix executable update (Closes #1526) (#1529) 2026-04-29 15:48:06 +02:00
amacocian b7d58f5e4c Update GWCA (Closes #1527) (#1528) 2026-04-29 15:48:06 +02:00
github-actions[bot]andamacocian 9888bc741f Bump version to 0.9.10.8 2026-04-29 15:48:06 +02:00
amacocian c1b2ed7548 Revert Daybreak placeholder (#1522) 2026-04-20 14:01:37 +02:00
github-actions[bot]andamacocian c23a44a313 Bump version to 0.9.10.7 2026-04-20 14:01:37 +02:00
amacocian b217f183cb Replace Swagger UI with Scalar and OpenApi (Closes #1517) (#1520) 2026-04-20 12:03:58 +02:00
amacocian d24a0a49e8 Update GWCA (Closes #1515) (#1519) 2026-04-20 12:03:58 +02:00
amacocian cdd620a786 Update dependencies (#1518) 2026-04-20 12:03:58 +02:00
dependabot[bot]amacociandependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
abbf87184b Bump vers-one/dotnet-project-version-updater from 1.7 to 1.8 (#1508)
Bumps [vers-one/dotnet-project-version-updater](https://github.com/vers-one/dotnet-project-version-updater) from 1.7 to 1.8.
- [Release notes](https://github.com/vers-one/dotnet-project-version-updater/releases)
- [Commits](https://github.com/vers-one/dotnet-project-version-updater/compare/v1.7...v1.8)

---
updated-dependencies:
- dependency-name: vers-one/dotnet-project-version-updater
  dependency-version: '1.8'
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-04-20 12:03:58 +02:00
dependabot[bot]amacociandependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
15fa4520aa Bump Microsoft.CodeAnalysis.Analyzers from 5.3.0-2.25625.1 to 5.3.0 (#1509)
---
updated-dependencies:
- dependency-name: Microsoft.CodeAnalysis.Analyzers
  dependency-version: 5.3.0
  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-04-20 12:03:58 +02:00
dependabot[bot]amacociandependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
bc963f486a Bump actions/github-script from 8 to 9 (#1511)
Bumps [actions/github-script](https://github.com/actions/github-script) from 8 to 9.
- [Release notes](https://github.com/actions/github-script/releases)
- [Commits](https://github.com/actions/github-script/compare/v8...v9)

---
updated-dependencies:
- dependency-name: actions/github-script
  dependency-version: '9'
  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-04-20 12:03:58 +02:00
dependabot[bot]amacociandependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
66386a6b85 Bump Microsoft.Web.WebView2 from 1.0.3800.47 to 1.0.3912.50 (#1512)
---
updated-dependencies:
- dependency-name: Microsoft.Web.WebView2
  dependency-version: 1.0.3912.50
  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-04-20 12:03:58 +02:00
amacocian 8d34d5d774 Make character name whitespace (Closes #1507) (#1516) 2026-04-20 12:03:58 +02:00
amacocian d5c8e7ad03 Remove reloaded dependency (use native bytecode) (#1514) 2026-04-20 12:03:58 +02:00
github-actions[bot]andamacocian 8eba2b92dd Bump version to 0.9.10.6 2026-04-20 12:03:58 +02:00
amacocianandAlexandru Macocian d9bd9d5395 Update GWCA (#1505)
Co-authored-by: Alexandru Macocian <amacocian@microsoft.com>
2026-03-13 15:13:57 +01:00
amacocianandAlexandru Macocian a59f88d396 Fix launch configuration reordering (Closes #1488) (#1504)
Co-authored-by: Alexandru Macocian <amacocian@microsoft.com>
2026-03-13 15:13:57 +01:00
amacocianandAlexandru Macocian 3799108f7e Fix ReShade deleting files when disabled and not installed (Closes #1500) (#1503)
Co-authored-by: Alexandru Macocian <amacocian@microsoft.com>
2026-03-13 15:13:57 +01:00
dependabot[bot]amacociandependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
a7f0f31a94 Bump Microsoft.CodeAnalysis.Analyzers and Microsoft.CodeAnalysis.CSharp (#1501)
Bumps Microsoft.CodeAnalysis.Analyzers from 3.11.0 to 5.3.0-2.25625.1
Bumps Microsoft.CodeAnalysis.CSharp from 5.0.0 to 5.3.0

---
updated-dependencies:
- dependency-name: Microsoft.CodeAnalysis.Analyzers
  dependency-version: 5.3.0-2.25625.1
  dependency-type: direct:production
  update-type: version-update:semver-major
- dependency-name: Microsoft.CodeAnalysis.CSharp
  dependency-version: 5.3.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-03-13 15:13:57 +01:00
amacocian 6ffa774270 Update GWCA (Closes #1494 Closes #1483) (#1498) 2026-03-13 15:13:57 +01:00
amacocianandAlexandru Macocian 2124ae92b4 Fix template loading from API (#1495)
Co-authored-by: Alexandru Macocian <amacocian@microsoft.com>
2026-03-13 15:13:57 +01:00
dependabot[bot]amacociandependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
00312a6367 Bump actions/download-artifact from 7 to 8 (#1492)
Bumps [actions/download-artifact](https://github.com/actions/download-artifact) from 7 to 8.
- [Release notes](https://github.com/actions/download-artifact/releases)
- [Commits](https://github.com/actions/download-artifact/compare/v7...v8)

---
updated-dependencies:
- dependency-name: actions/download-artifact
  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-03-13 15:13:57 +01:00
dependabot[bot]amacociandependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
df0d32bc30 Bump actions/upload-artifact from 6 to 7 (#1493)
Bumps [actions/upload-artifact](https://github.com/actions/upload-artifact) from 6 to 7.
- [Release notes](https://github.com/actions/upload-artifact/releases)
- [Commits](https://github.com/actions/upload-artifact/compare/v6...v7)

---
updated-dependencies:
- dependency-name: actions/upload-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-03-13 15:13:57 +01:00
Brendan McCoyandamacocian ecd19b0324 Add dye tint support to inventory item model (#1490)
Switched to using GWCA's GetBagByIndex for safer bag pointer access.
2026-03-13 15:13:57 +01:00
github-actions[bot]andamacocian aa2e9905c1 Bump version to 0.9.10.5 2026-03-13 15:13:57 +01:00
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
459 changed files with 60984 additions and 11017 deletions
+71
View File
@@ -0,0 +1,71 @@
# Copilot instructions for Daybreak
Daybreak is a cross-platform Guild Wars launcher built on .NET 10 and
Photino.Blazor. Read the
[Architecture Overview](../README.md#architecture-overview) for the project
layout and [CONTRIBUTING.md](../CONTRIBUTING.md) for the branching and release
process.
## Workflow
- Branch off the current `release/[version]` branch, never off `master`.
- One feature or fix per branch and per PR.
- Never push to `master` and never edit the version in `Directory.Build.props`.
Both are owned by the release workflows.
- Title PRs `Short description (Closes #123)` so the linked issue gets labelled.
## Build and test
CI does not run on PRs targeting a release branch, so validate locally. Run the
smallest command that covers the change:
```bash
dotnet build Daybreak.Linux/Daybreak.Linux.csproj # or Daybreak.Windows
dotnet test Daybreak.Tests/Daybreak.Tests.csproj --filter "FullyQualifiedName~YourTests"
```
Keep the build warning-free.
## Where code goes
| Change | Project |
| -------------------------------------------------- | ------------------------------------ |
| Models, utilities, interfaces shared by everything | `Daybreak.Shared` |
| Blazor views and services | `Daybreak.Core` |
| Platform-specific implementations | `Daybreak.Windows`, `Daybreak.Linux` |
`Daybreak.Tests` only references `Daybreak.Core`. When adding platform code, put
the logic worth testing in `Daybreak.Shared` and leave the platform project as a
thin caller.
Add NuGet packages through `Directory.Packages.props`.
## C# conventions
`.editorconfig` is the source of truth. The projects are set up to treat
warnings as errors.
## Verify before claiming
Prefer evidence over reasoning about behaviour, especially for the Wine and
injection paths. Wine is scriptable, so reproduce the actual failure and confirm
the fix against it rather than inferring what a Win32 call does.
## Writing style
Applies to docs, comments, PR descriptions and answers.
- Be short. Cut filler, recap and process narration.
- No AI-isms and no inflated language.
- Do not paste code into documentation. Link to the file so there is only one
copy of it.
- Explain the reason for a change, not a summary of the diff.
## Markdown
- No inline HTML. Use `![alt][ref]` with a reference definition instead of an
`img` tag.
- Start a mermaid fence with the diagram type, for example `flowchart LR`. YAML
frontmatter inside the fence is rejected by some renderers.
- Long URLs belong in reference-style link definitions at the bottom of the
file.
+28
View File
@@ -0,0 +1,28 @@
# Pre-configure SimplySign Desktop for unattended use:
# - auto-show the login dialog on launch (so the TOTP keystrokes have a target)
# - cache the smart-card PIN in the CSP, so signtool signs without a per-call
# PIN prompt, and forget it again when the session disconnects.
# Mirrors the settings used by the blinkdisk / docuscope CI signing setups.
$ErrorActionPreference = "Stop"
$RegistryPath = "HKCU:\Software\Certum\SimplySign"
$settings = [ordered]@{
ShowLoginDialogOnStart = 1
ShowLoginDialogOnAppRequest = 1
RememberLastUserName = 0
Autostart = 0
UnregisterCertificatesOnDisconnect = 0
RememberPINinCSP = 1
ForgetPINinCSPonDisconnect = 1
LangID = 9
}
Write-Host "=== Configuring SimplySign Desktop registry ==="
New-Item -Path $RegistryPath -Force | Out-Null
foreach ($name in $settings.Keys) {
Set-ItemProperty -Path $RegistryPath -Name $name -Value $settings[$name] -Type DWord
Write-Host " $name = $($settings[$name])"
}
Write-Host "Done."
+245
View File
@@ -0,0 +1,245 @@
# Authenticate SimplySign Desktop non-interactively.
#
# Certum's SimplySign cloud has no headless login: the certificate only reaches
# the Windows store after the GUI client authenticates. So we generate the
# current TOTP from the otpauth:// secret (CERTUM_OTP_URI) and paste the
# credentials into the login dialog. This needs an interactive desktop session,
# which GitHub-hosted Windows runners provide.
#
# Based on https://www.devas.life/how-to-automate-signing-your-windows-app-with-certum/
# and the refinements in blinkdisk's connect-simplysign.ps1.
param(
[string]$OtpUri = $env:CERTUM_OTP_URI,
[string]$UserId = $env:CERTUM_USERID,
[string]$ExePath = $env:CERTUM_EXE_PATH
)
if (-not $OtpUri) { Write-Host "ERROR: CERTUM_OTP_URI not provided"; exit 1 }
if (-not $UserId) { Write-Host "ERROR: CERTUM_USERID not provided"; exit 1 }
if (-not $ExePath) {
$ExePath = "C:\Program Files\Certum\SimplySign Desktop\SimplySignDesktop.exe"
}
Write-Host "=== SimplySign Desktop TOTP authentication ==="
if (-not (Test-Path $ExePath)) {
Write-Host "ERROR: SimplySign Desktop not found at $ExePath"
exit 1
}
# --- Parse the otpauth:// URI (works on PowerShell 5.1 and 7+) ---------------
$uri = [Uri]$OtpUri
try {
$q = [System.Web.HttpUtility]::ParseQueryString($uri.Query)
} catch {
$q = @{}
foreach ($part in $uri.Query.TrimStart('?') -split '&') {
$kv = $part -split '=', 2
if ($kv.Count -eq 2) { $q[$kv[0]] = [Uri]::UnescapeDataString($kv[1]) }
}
}
$Base32 = $q['secret']
$Digits = if ($q['digits']) { [int]$q['digits'] } else { 6 }
$Period = if ($q['period']) { [int]$q['period'] } else { 30 }
$Algorithm = if ($q['algorithm']) { $q['algorithm'].ToUpper() } else { 'SHA256' }
if (-not $Base32) { Write-Host "ERROR: otpauth URI has no 'secret'"; exit 1 }
if ($Algorithm -notin @('SHA1', 'SHA256', 'SHA512')) {
Write-Host "ERROR: unsupported TOTP algorithm: $Algorithm"
exit 1
}
# --- TOTP generator (RFC 6238), inline C# so there are no dependencies --------
Add-Type -Language CSharp @"
using System;
using System.Security.Cryptography;
public static class Totp
{
private const string B32 = "ABCDEFGHIJKLMNOPQRSTUVWXYZ234567";
private static byte[] Base32Decode(string s)
{
s = s.TrimEnd('=').ToUpperInvariant();
byte[] bytes = new byte[s.Length * 5 / 8];
int bitBuffer = 0, bitsLeft = 0, idx = 0;
foreach (char c in s)
{
int val = B32.IndexOf(c);
if (val < 0) throw new ArgumentException("Invalid Base32 char: " + c);
bitBuffer = (bitBuffer << 5) | val;
bitsLeft += 5;
if (bitsLeft >= 8) { bytes[idx++] = (byte)(bitBuffer >> (bitsLeft - 8)); bitsLeft -= 8; }
}
return bytes;
}
private static HMAC Hmac(string algorithm, byte[] key)
{
switch (algorithm.ToUpper())
{
case "SHA1": return new HMACSHA1(key);
case "SHA256": return new HMACSHA256(key);
case "SHA512": return new HMACSHA512(key);
default: throw new ArgumentException("Unsupported algorithm: " + algorithm);
}
}
public static string Now(string secret, int digits, int period, string algorithm)
{
byte[] key = Base32Decode(secret);
long counter = DateTimeOffset.UtcNow.ToUnixTimeSeconds() / period;
byte[] cnt = BitConverter.GetBytes(counter);
if (BitConverter.IsLittleEndian) Array.Reverse(cnt);
byte[] hash;
using (var h = Hmac(algorithm, key)) { hash = h.ComputeHash(cnt); }
int offset = hash[hash.Length - 1] & 0x0F;
int binary =
((hash[offset] & 0x7F) << 24) |
((hash[offset + 1] & 0xFF) << 16) |
((hash[offset + 2] & 0xFF) << 8) |
(hash[offset + 3] & 0xFF);
int otp = binary % (int)Math.Pow(10, digits);
return otp.ToString(new string('0', digits));
}
}
"@
function Find-UiByName($el, $walker, $name) {
$c = $walker.GetFirstChild($el)
while ($c) {
if ($c.Current.Name -eq $name) { return $c }
$r = Find-UiByName $c $walker $name
if ($r) { return $r }
$c = $walker.GetNextSibling($c)
}
return $null
}
# An outdated SimplySign build pops a modal "New version found - download?" box
# over the login form, which swallows the credential keystrokes. Decline it by
# clicking "No" (Invoke / legacy default action / click the element's point).
function Dismiss-UpdatePrompt {
try { Add-Type -AssemblyName UIAutomationClient, UIAutomationTypes -ErrorAction Stop } catch { return $false }
if (-not ('Win32Mouse' -as [type])) {
Add-Type @"
using System;
using System.Runtime.InteropServices;
public static class Win32Mouse {
[DllImport("user32.dll")] static extern bool SetCursorPos(int x, int y);
[DllImport("user32.dll")] static extern void mouse_event(uint f, uint x, uint y, uint d, int e);
public static void Click(int x, int y){ SetCursorPos(x,y); mouse_event(0x02,0,0,0,0); mouse_event(0x04,0,0,0,0); }
}
"@
}
$procIds = @((Get-Process -Name '*SimplySign*' -ErrorAction SilentlyContinue).Id)
$walker = [System.Windows.Automation.TreeWalker]::ControlViewWalker
$top = $walker.GetFirstChild([System.Windows.Automation.AutomationElement]::RootElement)
while ($top) {
if ($procIds -contains $top.Current.ProcessId) {
$no = Find-UiByName $top $walker "No"
if ($no) {
Write-Host "Update prompt detected; declining (clicking 'No')."
try { ($no.GetCurrentPattern([System.Windows.Automation.InvokePattern]::Pattern)).Invoke(); return $true } catch {}
try { ($no.GetCurrentPattern([System.Windows.Automation.LegacyIAccessiblePattern]::Pattern)).DoDefaultAction(); return $true } catch {}
try { $pt = $no.GetClickablePoint(); [Win32Mouse]::Click([int]$pt.X, [int]$pt.Y); return $true } catch { Write-Host "Click 'No' failed: $($_.Exception.Message)" }
}
}
$top = $walker.GetNextSibling($top)
}
return $false
}
# Start from a clean slate so a stale window/session can't swallow the keystrokes.
if ($existing = Get-Process -Name "SimplySignDesktop" -ErrorAction Ignore) {
Write-Host "Killing existing SimplySignDesktop process..."
$existing | Stop-Process -Force
Start-Sleep -Seconds 1
}
Write-Host "Launching SimplySign Desktop..."
$proc = Start-Process -FilePath $ExePath -PassThru
Start-Sleep -Seconds 3
$wshell = New-Object -ComObject WScript.Shell
Write-Host "Focusing the login window..."
$focused = $wshell.AppActivate($proc.Id)
if (-not $focused) { $focused = $wshell.AppActivate('SimplySign Desktop') }
for ($i = 0; (-not $focused) -and ($i -lt 10); $i++) {
Start-Sleep -Milliseconds 500
$focused = $wshell.AppActivate($proc.Id) -or $wshell.AppActivate('SimplySign Desktop')
}
if (-not $focused) {
Write-Host "ERROR: could not bring SimplySign Desktop to the foreground"
exit 1
}
# Decline the "new version available" modal if it's covering the login form.
if (Dismiss-UpdatePrompt) { Start-Sleep -Milliseconds 800 }
# Re-assert focus: dismissing the dialog can move the foreground window, and
# keystrokes sent to the wrong window are silently dropped.
$wshell.AppActivate($proc.Id) | Out-Null
$wshell.AppActivate('SimplySign Desktop') | Out-Null
Start-Sleep -Milliseconds 400
# Paste rather than type: SendKeys mangles characters like + ^ % ( ) and can
# drop characters; pasting delivers the exact string. Falls back to typing if
# the clipboard is unavailable.
function Set-Field([string]$text) {
try {
Set-Clipboard -Value $text -ErrorAction Stop
Start-Sleep -Milliseconds 150
$wshell.SendKeys("^v")
} catch {
Write-Host "Clipboard unavailable ($($_.Exception.Message)); typing instead."
$wshell.SendKeys($text)
}
Start-Sleep -Milliseconds 250
}
Write-Host "Injecting credentials..."
Set-Field $UserId
$wshell.SendKeys("{TAB}")
Start-Sleep -Milliseconds 200
# Generate the code right before sending it so it can't expire while we focus.
$otp = [Totp]::Now($Base32, $Digits, $Period, $Algorithm)
Set-Field $otp
Start-Sleep -Milliseconds 200
$wshell.SendKeys("{ENTER}")
# Don't leave the OTP / username sitting on the clipboard afterwards.
try { Set-Clipboard -Value " " -ErrorAction Stop } catch {}
Write-Host "Waiting for authentication to settle..."
Start-Sleep -Seconds 5
if (-not (Get-Process -Id $proc.Id -ErrorAction SilentlyContinue)) {
Write-Host "ERROR: SimplySign Desktop exited - authentication failed."
exit 1
}
# Confirm the login actually mounted the signing certificate into the store.
$thumb = if ($env:CERTUM_CERT_SHA1) { ($env:CERTUM_CERT_SHA1 -replace '\s', '').ToUpperInvariant() } else { $null }
Write-Host "Verifying the signing certificate reached the store..."
$deadline = (Get-Date).AddSeconds(60)
$found = $false
do {
$certs = Get-ChildItem Cert:\CurrentUser\My, Cert:\LocalMachine\My -ErrorAction SilentlyContinue
if ($thumb) { $found = [bool]($certs | Where-Object { $_.Thumbprint -eq $thumb }) }
else { $found = [bool]($certs | Where-Object { $_.Subject -like '*Certum*' -or $_.Issuer -like '*Certum*' }) }
if ($found) { break }
Start-Sleep -Seconds 3
} while ((Get-Date) -lt $deadline)
if ($found) {
Write-Host "=== Authentication complete: signing certificate is available. ==="
} else {
Write-Host "ERROR: signing certificate did not appear after authentication."
exit 1
}
+46
View File
@@ -0,0 +1,46 @@
#!/bin/bash
# Install Certum SimplySign Desktop on the (ephemeral) Windows runner.
# SimplySign Desktop is what mounts the cloud signing certificate as a virtual
# smart card into the Windows certificate store; signtool then selects it by
# thumbprint. The version + checksum are pinned for reproducibility.
set -euo pipefail
VERSION="9.3.4.72"
URL="https://files.certum.eu/software/SimplySignDesktop/Windows/${VERSION}/SimplySignDesktop-${VERSION}-64-bit-en.msi"
EXPECTED_SHA256="bd51ebbaaac20fc7d59ab7103b5ed532b7800586df4e31f6999d03a394f9c515"
INSTALLER="SimplySignDesktop.msi"
INSTALL_DIR="/c/Program Files/Certum/SimplySign Desktop"
echo "=== Installing Certum SimplySign Desktop ${VERSION} ==="
# Idempotent: a cached/pre-provisioned image may already have it.
if [ -d "$INSTALL_DIR" ]; then
echo "Already installed at: $INSTALL_DIR"
exit 0
fi
echo "Downloading installer..."
curl -L "$URL" -o "$INSTALLER" --fail --max-time 600
ACTUAL_SHA256=$(sha256sum "$INSTALLER" | awk '{print $1}')
if [ "$EXPECTED_SHA256" != "$ACTUAL_SHA256" ]; then
echo "ERROR: checksum mismatch"
echo " expected: $EXPECTED_SHA256"
echo " actual: $ACTUAL_SHA256"
exit 1
fi
echo "Checksum verified."
echo "Running msiexec (silent)..."
# Start-Process inherits this CWD, so the relative installer path resolves here.
powershell -Command "Start-Process msiexec.exe -ArgumentList '/i','${INSTALLER}','/quiet','/norestart','/l*v','install.log','ALLUSERS=1','REBOOT=ReallySuppress' -Wait -NoNewWindow"
if [ ! -d "$INSTALL_DIR" ]; then
echo "ERROR: installation directory not found after install"
echo "Last 20 lines of install.log:"
tail -20 install.log 2>/dev/null || echo "(no install.log)"
exit 1
fi
echo "SimplySign Desktop installed."
+66
View File
@@ -0,0 +1,66 @@
# Sign the given files with the Certum cloud certificate (selected by SHA1
# thumbprint from the store) and verify each signature. Assumes SimplySign
# Desktop has already authenticated (connect-simplysign.ps1).
[CmdletBinding()]
param(
[Parameter(Mandatory = $true)]
[string[]]$Files,
[string]$CertSha1 = $env:CERTUM_CERT_SHA1,
[string]$TimestampUrl = "http://time.certum.pl"
)
$ErrorActionPreference = "Stop"
if (-not $CertSha1) { throw "CERTUM_CERT_SHA1 not provided." }
$thumb = ($CertSha1 -replace '\s', '').ToUpperInvariant()
# Resolve the newest signtool.exe from the installed Windows SDK(s).
$kitsBin = "C:\Program Files (x86)\Windows Kits\10\bin"
$signtool = Get-ChildItem -Path $kitsBin -Recurse -Filter signtool.exe -File -ErrorAction SilentlyContinue |
Where-Object { $_.FullName -like '*\x64\*' } |
Sort-Object { [version]$_.Directory.Parent.Name } |
Select-Object -Last 1 -ExpandProperty FullName
if (-not $signtool) { throw "signtool.exe not found under $kitsBin" }
Write-Host "Using signtool: $signtool"
# The SimplySign virtual smart card registers in the store a moment after the
# desktop client authenticates; poll for our certificate before signing.
Write-Host "Waiting for certificate $thumb to appear in the store..."
$deadline = (Get-Date).AddSeconds(120)
$cert = $null
$elapsed = 0
do {
$cert = Get-ChildItem Cert:\CurrentUser\My, Cert:\LocalMachine\My -ErrorAction SilentlyContinue |
Where-Object { $_.Thumbprint -eq $thumb } | Select-Object -First 1
if ($cert) { break }
Start-Sleep -Seconds 3
$elapsed += 3
Write-Host " ...still waiting (${elapsed}s)"
} while ((Get-Date) -lt $deadline)
if (-not $cert) {
Write-Host "Certificate not found. CurrentUser\My + LocalMachine\My contents:"
Get-ChildItem Cert:\CurrentUser\My, Cert:\LocalMachine\My -ErrorAction SilentlyContinue |
Select-Object Thumbprint, Subject, HasPrivateKey | Format-Table -AutoSize | Out-String | Write-Host
Write-Host "SimplySign processes:"
Get-Process -Name '*SimplySign*' -ErrorAction SilentlyContinue |
Select-Object Name, Id, Responding, MainWindowTitle | Format-Table -AutoSize | Out-String | Write-Host
throw "Certificate $thumb not found in the store - SimplySign authentication likely failed (see dump above)."
}
Write-Host "Found signing certificate: $($cert.Subject)"
$failed = @()
foreach ($file in $Files) {
if (-not (Test-Path $file)) { throw "File to sign not found: $file" }
Write-Host "=== Signing $file ==="
& $signtool sign /sha1 $thumb /fd sha256 /td sha256 /tr $TimestampUrl /v $file
if ($LASTEXITCODE -ne 0) { $failed += $file; continue }
& $signtool verify /pa /v $file
if ($LASTEXITCODE -ne 0) { $failed += $file }
}
if ($failed.Count -gt 0) {
throw "Signing/verification failed for: $($failed -join ', ')"
}
Write-Host "All binaries signed and verified."
+8 -5
View File
@@ -3,21 +3,24 @@
name: Daybreak Dry-Run CD Pipeline
on:
push:
pull_request:
branches:
- master
paths:
- "Daybreak/**"
- "Daybreak.Core/**"
- "Daybreak.Installer/**"
- "Daybreak.API/**"
- "Daybreak.7ZipExtractor/**"
- "Daybreak.Installer/**"
- "Daybreak.Shared/**"
workflow_dispatch:
- "Daybreak.Windows/**"
- "Daybreak.Linux/**"
- ".github/scripts/**"
- ".github/workflows/cd-template.yml"
- ".github/workflows/cd-dry-run.yml"
jobs:
cd:
uses: ./.github/workflows/cd-template.yml
with:
publishRelease: false
signBinaries: false
secrets: inherit
+95 -44
View File
@@ -9,6 +9,11 @@ on:
required: false
type: boolean
default: false
signBinaries:
description: "Whether to code-sign Windows binaries"
required: false
type: boolean
default: false
jobs:
# ───────────────────────────────────────────────────────
@@ -16,21 +21,34 @@ jobs:
# (Injector + API) that both platforms need.
# ───────────────────────────────────────────────────────
build-x86:
runs-on: windows-latest
runs-on: windows-2025
outputs:
version: ${{ steps.get_version.outputs.version }}
env:
Configuration: Release
Actions_Allow_Unsecure_Commands: true
CERTUM_OTP_URI: ${{ secrets.CERTUM_OTP_URI }}
CERTUM_USERID: ${{ secrets.CERTUM_USERID }}
CERTUM_CERT_SHA1: ${{ secrets.CERTUM_CERT_SHA1 }}
steps:
- name: Checkout
uses: actions/checkout@v6
uses: actions/checkout@v7
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
uses: actions/setup-dotnet@v6
with:
dotnet-version: "10.x"
@@ -38,8 +56,32 @@ jobs:
run: .\Scripts\PublishWindowsX86.ps1
shell: pwsh
# Certum SimplySign code signing. Binaries are signed only when explicitly
# requested and the Certum secrets are provisioned.
- name: Set up Certum SimplySign
if: inputs.signBinaries && env.CERTUM_OTP_URI != ''
shell: bash
run: |
chmod +x ./.github/scripts/install-simplysign.sh
./.github/scripts/install-simplysign.sh
powershell -ExecutionPolicy Bypass -File "./.github/scripts/configure-simplysign.ps1"
- name: Authenticate Certum SimplySign
if: inputs.signBinaries && env.CERTUM_OTP_URI != ''
shell: bash
run: powershell -ExecutionPolicy Bypass -File "./.github/scripts/connect-simplysign.ps1"
- name: Sign x86 binaries
if: inputs.signBinaries && env.CERTUM_OTP_URI != ''
run: |
./.github/scripts/sign-certum.ps1 -Files @(
"Publish/Injector/Daybreak.Injector.exe",
"Publish/Api/Daybreak.API.dll"
)
shell: pwsh
- name: Upload x86 artifacts
uses: actions/upload-artifact@v6
uses: actions/upload-artifact@v7
with:
name: x86-components
path: |
@@ -52,21 +94,24 @@ jobs:
# ───────────────────────────────────────────────────────
build-windows:
needs: build-x86
runs-on: windows-latest
runs-on: windows-2025
env:
Configuration: Release
Actions_Allow_Unsecure_Commands: true
CERTUM_OTP_URI: ${{ secrets.CERTUM_OTP_URI }}
CERTUM_USERID: ${{ secrets.CERTUM_USERID }}
CERTUM_CERT_SHA1: ${{ secrets.CERTUM_CERT_SHA1 }}
steps:
- name: Checkout
uses: actions/checkout@v6
uses: actions/checkout@v7
with:
fetch-depth: 0
submodules: recursive
- name: Install .NET
uses: actions/setup-dotnet@v5
uses: actions/setup-dotnet@v6
with:
dotnet-version: "10.x"
@@ -79,7 +124,7 @@ jobs:
dotnet user-secrets --project Daybreak.Core\Daybreak.Core.csproj set ApmUri "${{ secrets.ApmUri }}"
- name: Download x86 artifacts
uses: actions/download-artifact@v7
uses: actions/download-artifact@v8
with:
name: x86-components
path: Publish/
@@ -88,8 +133,30 @@ jobs:
run: .\Scripts\PublishWindows.ps1
shell: pwsh
- name: Set up Certum SimplySign
if: inputs.signBinaries && env.CERTUM_OTP_URI != ''
shell: bash
run: |
chmod +x ./.github/scripts/install-simplysign.sh
./.github/scripts/install-simplysign.sh
powershell -ExecutionPolicy Bypass -File "./.github/scripts/configure-simplysign.ps1"
- name: Authenticate Certum SimplySign
if: inputs.signBinaries && env.CERTUM_OTP_URI != ''
shell: bash
run: powershell -ExecutionPolicy Bypass -File "./.github/scripts/connect-simplysign.ps1"
- name: Sign Windows build
if: inputs.signBinaries && env.CERTUM_OTP_URI != ''
run: |
./.github/scripts/sign-certum.ps1 -Files @(
"Publish/Daybreak.exe",
"Publish/Installer/Daybreak.Installer.exe"
)
shell: pwsh
- name: Upload Windows build
uses: actions/upload-artifact@v6
uses: actions/upload-artifact@v7
with:
name: windows-build
path: Publish/
@@ -104,13 +171,13 @@ jobs:
steps:
- name: Checkout
uses: actions/checkout@v6
uses: actions/checkout@v7
with:
fetch-depth: 0
submodules: recursive
- name: Install .NET
uses: actions/setup-dotnet@v5
uses: actions/setup-dotnet@v6
with:
dotnet-version: "10.x"
@@ -123,7 +190,7 @@ jobs:
dotnet user-secrets --project Daybreak.Core/Daybreak.Core.csproj set ApmUri "${{ secrets.ApmUri }}"
- name: Download x86 artifacts
uses: actions/download-artifact@v7
uses: actions/download-artifact@v8
with:
name: x86-components
path: Publish/
@@ -139,7 +206,7 @@ jobs:
chmod +x Publish/Installer/Daybreak.Installer
- name: Upload Linux build
uses: actions/upload-artifact@v6
uses: actions/upload-artifact@v7
with:
name: linux-build
path: Publish/
@@ -149,27 +216,17 @@ jobs:
# Stage 3 — Package (always)
# ───────────────────────────────────────────────────────
package:
needs: [build-windows, build-linux]
needs: [build-x86, build-windows, build-linux]
runs-on: ubuntu-latest
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: Set version variable
run: |
version=$(grep '<Version>' Directory.Build.props | sed 's/.*<Version>\(.*\)<\/Version>.*/\1/')
echo "Version=$version" >> $GITHUB_ENV
# ── Windows package ──
- name: Download Windows build
uses: actions/download-artifact@v7
uses: actions/download-artifact@v8
with:
name: windows-build
path: windows-publish/
@@ -187,7 +244,7 @@ jobs:
# ── Linux package ──
- name: Download Linux build
uses: actions/download-artifact@v7
uses: actions/download-artifact@v8
with:
name: linux-build
path: linux-publish/
@@ -206,7 +263,7 @@ jobs:
cd linux-publish && zip -r ../daybreakv${{ env.Version }}-linux.zip . && cd ..
- name: Upload release artifacts
uses: actions/upload-artifact@v6
uses: actions/upload-artifact@v7
with:
name: release-zips
path: |
@@ -218,22 +275,23 @@ jobs:
# Stage 4 — Release (optional)
# ───────────────────────────────────────────────────────
release:
needs: package
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
uses: actions/checkout@v7
with:
fetch-depth: 0
submodules: recursive
- name: Download release artifacts
uses: actions/download-artifact@v7
uses: actions/download-artifact@v8
with:
name: release-zips
path: release-assets/
@@ -243,17 +301,12 @@ jobs:
uses: WyriHaximus/github-action-get-previous-tag@v2
- name: Generate changelog
id: gen_changelog
id: changelog
run: |
changeLog=$(git log --no-merges --pretty="%h - %s (%an)<br />" ${{ steps.getLatestTag.outputs.tag }}..HEAD)
echo "Changelog<<EOF" >> $GITHUB_ENV
echo "$changeLog" >> $GITHUB_ENV
echo "EOF" >> $GITHUB_ENV
- name: Set version variable
run: |
version=$(grep '<Version>' Directory.Build.props | sed 's/.*<Version>\(.*\)<\/Version>.*/\1/')
echo "Version=$version" >> $GITHUB_ENV
echo "content<<EOF" >> $GITHUB_OUTPUT
echo "$changeLog" >> $GITHUB_OUTPUT
echo "EOF" >> $GITHUB_OUTPUT
# ── Release ──
- name: Create release draft
@@ -263,13 +316,11 @@ jobs:
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: ${{ env.GITHUB_TOKEN }}
github_token: ${{ secrets.GITHUB_TOKEN }}
replace_assets: true
body_mrkdwn: |
${{ env.Changelog }}
${{ steps.changelog.outputs.content }}
isDraft: true
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Publish release
run: gh release edit v${{ env.Version }} --draft=false
+7 -3
View File
@@ -9,12 +9,15 @@ on:
branches:
- master
paths:
- "Daybreak/**"
- "Daybreak.Core/**"
- "Daybreak.Installer/**"
- "Daybreak.API/**"
- "Daybreak.7ZipExtractor/**"
- "Daybreak.Installer/**"
- "Daybreak.Shared/**"
- "Daybreak.Windows/**"
- "Daybreak.Linux/**"
- ".github/scripts/**"
- ".github/workflows/cd-template.yml"
- ".github/workflows/cd.yaml"
workflow_dispatch:
jobs:
@@ -22,4 +25,5 @@ jobs:
uses: ./.github/workflows/cd-template.yml
with:
publishRelease: true
signBinaries: true
secrets: inherit
+4 -4
View File
@@ -16,13 +16,13 @@ jobs:
steps:
- name: Checkout
uses: actions/checkout@v6
uses: actions/checkout@v7
with:
fetch-depth: 0
submodules: recursive
- name: Install .NET
uses: actions/setup-dotnet@v5
uses: actions/setup-dotnet@v6
with:
dotnet-version: "10.x"
@@ -49,13 +49,13 @@ jobs:
steps:
- name: Checkout
uses: actions/checkout@v6
uses: actions/checkout@v7
with:
fetch-depth: 0
submodules: recursive
- name: Install .NET
uses: actions/setup-dotnet@v5
uses: actions/setup-dotnet@v6
with:
dotnet-version: "10.x"
+2 -2
View File
@@ -42,7 +42,7 @@ jobs:
steps:
- name: Checkout
uses: actions/checkout@v6
uses: actions/checkout@v7
with:
fetch-depth: 0
@@ -63,7 +63,7 @@ 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@v5
uses: actions/setup-dotnet@v6
with:
dotnet-version: '10.x'
@@ -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@v7
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.8
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
+1 -1
View File
@@ -13,7 +13,7 @@ jobs:
steps:
- name: Label linked issues as merged
uses: actions/github-script@v8
uses: actions/github-script@v9
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
script: |
+10 -13
View File
@@ -5,13 +5,12 @@ on:
branches:
- master
paths:
- "Daybreak/**"
- "Daybreak.Core/**"
- "Daybreak.Installer/**"
- "Daybreak.Linux/**"
- "Daybreak.Windows/**"
- "Daybreak.API/**"
- "Daybreak.Shared/**"
- "Daybreak.Injector/**"
- "Daybreak.Windows/**"
- "Daybreak.Linux/**"
jobs:
check_version:
@@ -28,12 +27,12 @@ jobs:
steps:
- name: Checkout
uses: actions/checkout@v6
uses: actions/checkout@v7
with:
fetch-depth: 0
- name: Install .NET Core
uses: actions/setup-dotnet@v5
uses: actions/setup-dotnet@v6
with:
dotnet-version: "10.x"
@@ -44,15 +43,13 @@ jobs:
- name: Build Daybreak project
run: dotnet build Daybreak.Linux -c $Configuration --property:SolutionDir=$GITHUB_WORKSPACE
- name: Set version variable
- name: Get version
id: get_version
run: |
version=$(grep '<Version>' Directory.Build.props | sed 's/.*<Version>\(.*\)<\/Version>.*/\1/')
echo "Version=$version" >> $GITHUB_ENV
echo "version=$version" >> $GITHUB_OUTPUT
- name: Check version difference
run: pwsh ./Scripts/CompareVersions.ps1 -currentVersion ${{ env.Version }} -lastVersion ${{ env.LatestReleaseTag }}
env:
LatestReleaseTag: ${{ steps.getLatestTag.outputs.tag }}
shell:
pwsh
run: pwsh ./Scripts/CompareVersions.ps1 -currentVersion ${{ steps.get_version.outputs.version }} -lastVersion ${{ steps.getLatestTag.outputs.tag }}
shell: pwsh
+5 -2
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
@@ -178,8 +181,8 @@ DocProject/Help/*.hhp
DocProject/Help/Html2
DocProject/Help/html
# Click-Once directory
publish/
# Click-Once / local publish directories
[Pp]ublish/
# Publish Web Output
*.[Pp]ublish.xml
+108
View File
@@ -0,0 +1,108 @@
-- Per-project Neovim config for the Daybreak repo.
-- Auto-sourced when nvim is started inside the repo, provided the user has
-- `vim.o.exrc = true` in their config (or runs `:set exrc`). Neovim 0.9+
-- prompts to trust this file on first load.
--
-- Commands added:
-- :DaybreakStageX86 [Config] build x86 components via wine and stage
-- them into Daybreak.Linux/bin/<Config>/...
-- /{Injector,Api}/ (default Config: Debug)
-- :DaybreakRunLinux run Daybreak.Linux via `dotnet run` in a
-- terminal split (honors launchSettings.json,
-- which easy-dot-net's launcher does not)
--
-- Note: a Linux/wine attach-debugger workflow was attempted (winedbg --gdb
-- stub *and* native Linux gdb -p attach), but both freeze GW reliably because
-- of ptrace/wineserver interactions that wine doesn't tolerate. For now,
-- prefer logging-based debugging via the API's console + log file in the
-- game directory (Daybreak.API<YYYYMMDD>.log next to Gw.exe).
local repo = vim.fn.fnamemodify(debug.getinfo(1, 'S').source:sub(2), ':p:h')
local scripts = repo .. '/Scripts'
-- ───── Progress notification with spinner ──────────────────────────────────
-- Uses nvim-notify / snacks.nvim's `id`/`replace` semantics to update a
-- single notification in place. Falls back to plain notify if the backend
-- doesn't support replace (the spinner just stops updating).
local SPINNER = { '', '', '', '', '', '', '', '' }
local function start_progress(title, message)
-- stable id so snacks.nvim/nvim-notify replace the same notification
local id = 'daybreak-' .. title:gsub('%s+', '-') .. '-' .. tostring(vim.uv.hrtime())
local state = { title = title, msg = message, frame = 1, done = false, id = id }
local function render()
local icon = state.done and '' or (SPINNER[state.frame] .. ' ')
local opts = {
title = state.title,
id = state.id,
timeout = state.done and 4000 or false,
hide_from_history = not state.done,
icon = state.done and '' or nil,
}
if state.notif then opts.replace = state.notif end
state.notif = vim.notify(icon .. state.msg, vim.log.levels.INFO, opts)
end
render()
state.timer = vim.uv.new_timer()
state.timer:start(80, 80, vim.schedule_wrap(function()
if state.done then return end
state.frame = (state.frame % #SPINNER) + 1
render()
end))
return {
update = function(msg) state.msg = msg; render() end,
finish = function(ok, msg)
state.done = true
if state.timer then state.timer:stop(); state.timer:close(); state.timer = nil end
state.msg = msg or state.msg
local final_opts = {
title = state.title,
id = state.id,
timeout = 4000,
icon = ok and '' or '',
}
if state.notif then final_opts.replace = state.notif end
vim.notify((ok and '' or '') .. state.msg,
ok and vim.log.levels.INFO or vim.log.levels.ERROR, final_opts)
end,
}
end
-- ───── :DaybreakStageX86 [Config] ─────────────────────────────────────────
vim.api.nvim_create_user_command('DaybreakStageX86', function(opts)
local config = opts.args ~= '' and opts.args or 'Debug'
local progress = start_progress('Daybreak StageX86', 'Building x86 components (' .. config .. ')…')
vim.fn.jobstart({ scripts .. '/StageX86ForDebug.sh', config, '' }, {
cwd = repo,
on_exit = function(_, code)
vim.schedule(function()
if code == 0 then
progress.finish(true, 'Staged x86 (' .. config .. ')')
else
progress.finish(false, 'Stage failed (exit ' .. code ..
'). See /tmp/wine-injector-publish.log, /tmp/wine-api-publish.log')
end
end)
end,
stdout_buffered = false,
stderr_buffered = false,
})
end, {
nargs = '?',
complete = function() return { 'Debug', 'Release' } end,
desc = 'Build x86 components in wine and stage into Daybreak.Linux output',
})
-- ───── :DaybreakRunLinux ───────────────────────────────────────────────────
-- easy-dot-net launches the produced binary directly, bypassing launchSettings.json.
-- This runs Daybreak.Linux through `dotnet run`, which honors the environment
-- variables (GDK_BACKEND, WEBKIT_DISABLE_DMABUF_RENDERER, etc.) declared there.
-- Opens in the current window (no split) so it integrates with normal buffer
-- navigation; Ctrl-\ Ctrl-N to leave terminal mode, then your usual buffer
-- bindings work.
vim.api.nvim_create_user_command('DaybreakRunLinux', function()
vim.cmd('enew')
vim.fn.termopen({ 'dotnet', 'run', '--project', 'Daybreak.Linux/Daybreak.Linux.csproj', '-c', 'Debug' },
{ cwd = repo })
vim.cmd('startinsert')
end, { desc = 'Run Daybreak.Linux via dotnet run in the current window' })
+35 -11
View File
@@ -3,19 +3,43 @@
"configurations": [
{
"name": "Debug Daybreak.Linux",
"type": "coreclr",
"type": "dotnet",
"request": "launch",
"preLaunchTask": "build-linux",
"program": "${workspaceFolder}/Daybreak.Linux/bin/Debug/net10.0/linux-x64/Daybreak.dll",
"args": [],
"cwd": "${workspaceFolder}/Daybreak.Linux/bin/Debug/net10.0/linux-x64",
"env": {
"GDK_BACKEND": "x11",
"WEBKIT_DISABLE_DMABUF_RENDERER": "1",
"WEBKIT_DISABLE_COMPOSITING_MODE": "1"
},
"projectPath": "${workspaceFolder}/Daybreak.Linux/Daybreak.Linux.csproj",
"console": "integratedTerminal",
"stopAtEntry": false
"justMyCode": true,
"stopAtEntry": false,
"suppressJITOptimizations": false,
"symbolOptions": {
"searchMicrosoftSymbolServer": false,
"searchNuGetOrgSymbolServer": false
},
"logging": {
"engineLogging": false,
"moduleLoad": false,
"exceptions": false,
"browserStdOut": false
}
},
{
"name": "Debug Daybreak.Windows",
"type": "dotnet",
"request": "launch",
"projectPath": "${workspaceFolder}/Daybreak.Windows/Daybreak.Windows.csproj",
"console": "integratedTerminal",
"justMyCode": true,
"stopAtEntry": false,
"suppressJITOptimizations": false,
"symbolOptions": {
"searchMicrosoftSymbolServer": false,
"searchNuGetOrgSymbolServer": false
},
"logging": {
"engineLogging": false,
"moduleLoad": false,
"exceptions": false,
"browserStdOut": false
}
}
]
}
+12
View File
@@ -12,6 +12,18 @@
"/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"
}
]
}
+113
View File
@@ -0,0 +1,113 @@
# Contributing to Daybreak
## Getting started
Install the prerequisites listed under
[Build Requirements](README.md#build-requirements), then clone with submodules:
```bash
git clone --recurse-submodules https://github.com/AlexMacocian/Daybreak.git
```
Read the [Architecture Overview](README.md#architecture-overview) before making
changes so you put code in the right project.
## Branching model
There is always exactly one active release branch, named `release/[version]`
(for example `release/0.9.10.20`). It is the integration branch: all work
targets it, and `master` only receives finished releases.
```mermaid
flowchart LR
subgraph cycle["Release cycle"]
direction LR
R["release/0.9.10.20<br/><i>active release branch</i>"]
F["feature branch<br/><i>one per change</i>"]
R -->|fork| F
F -->|"PR, squash merge"| R
end
R -->|"PR, rebase merge"| M["master"]
M --> CD["CD pipeline<br/>builds and publishes the release"]
CD -->|"job bumps the revision"| N["release/0.9.10.21<br/><i>next active release branch</i>"]
```
### Feature work
1. Branch off the current release branch.
2. Implement one feature or fix. Keep the branch scoped to a single change.
3. Open a PR against the release branch.
4. Merge with **squash**, so each feature lands as one commit.
### Releasing
1. Open a PR from the release branch onto `master`.
2. Merge with **rebase**, keeping full history. Each feature stays a single
commit on `master`.
3. The [CD pipeline](.github/workflows/cd.yaml) runs on the push to `master`,
builds the Windows and Linux bundles and publishes a GitHub release.
4. On success,
[create-revision-release-branch.yml](.github/workflows/create-revision-release-branch.yml)
bumps the revision and creates the next `release/[version]` branch
automatically. That becomes the new active release branch.
Do not push directly to `master` or create release branches by hand.
## Versions
The version lives in a single place,
[`Directory.Build.props`](Directory.Build.props), and follows
`major.minor.build.revision`. It is bumped by the release branch creation job,
not manually.
Revision bumps happen automatically after every release. For the other
components, run the matching workflow manually before starting the next cycle:
| Workflow | Bumps |
| ------------------------------------------------------------------------------------------ | --------------------- |
| [create-major-release-branch.yml](.github/workflows/create-major-release-branch.yml) | `^.0.0.0` |
| [create-minor-release-branch.yml](.github/workflows/create-minor-release-branch.yml) | `*.^.0.0` |
| [create-build-release-branch.yml](.github/workflows/create-build-release-branch.yml) | `*.*.^.0` |
| [create-revision-release-branch.yml](.github/workflows/create-revision-release-branch.yml) | `*.*.*.^` (automatic) |
All four delegate to
[create-release-branch-template.yml](.github/workflows/create-release-branch-template.yml).
## Pull requests
Title format follows the existing history: a short description, plus the issue
reference when it closes one.
```text
Setup stable computer name in Wine prefix (Closes #1609)
```
[label-merged-issues.yaml](.github/workflows/label-merged-issues.yaml) parses
`close/fix/resolve #N` from the title and body of PRs merged into a release
branch and labels the linked issues, so use those keywords.
## Checks
[CI](.github/workflows/ci.yaml) and
[version_check.yaml](.github/workflows/version_check.yaml) only trigger on PRs
targeting `master`, so a feature PR onto a release branch is not covered by
them. Run the equivalent locally before opening one:
```bash
dotnet build Daybreak.Linux/Daybreak.Linux.csproj # or Daybreak.Windows on Windows
dotnet test Daybreak.Tests/Daybreak.Tests.csproj
```
The version check fails a PR to `master` if the version in
`Directory.Build.props` is not ahead of the latest tag. This is why the bump
must come from the release branch job.
## Code style
Formatting rules are in [`.editorconfig`](.editorconfig) and are enforced by the
compiler and analyzers; keep the build warning-free.
Tests live in [`Daybreak.Tests`](Daybreak.Tests), use MSTest with
FluentAssertions and NSubstitute, and only reference `Daybreak.Core`. If you
want platform code covered by tests, put the testable part in
[`Daybreak.Shared`](Daybreak.Shared).
@@ -7,6 +7,10 @@ public static class WebApplicationBuilderExtensions
var config = BuildInfo.Configuration;
var appSettingsStream = GetManifestResourceStream($"Daybreak.API.Configuration.appsettings.json") ?? throw new InvalidOperationException("Failed to load appsettings.json");
var appSettingsConfigStream = GetManifestResourceStream($"Daybreak.API.Configuration.appsettings.{config}.json") ?? throw new InvalidOperationException($"Failed to load appsettings.{config}.json");
// Clear default file-based configuration sources that cause unnecessary reload events
builder.Configuration.Sources.Clear();
builder.Configuration
.AddJsonStream(appSettingsStream)
.AddJsonStream(appSettingsConfigStream)
@@ -1,67 +0,0 @@
using System.Buffers;
using System.Core.Extensions;
using System.Extensions.Core;
using System.Net.WebSockets;
using Daybreak.API.Models;
using Daybreak.API.Services;
using Daybreak.API.WebSockets;
using Daybreak.Shared.Models.Api;
namespace Daybreak.API.Controllers.WebSocket.MainPlayer;
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());
}
}
+19 -4
View File
@@ -8,6 +8,7 @@
<RuntimeIdentifier Condition="'$(RuntimeIdentifier)' == ''">win-x86</RuntimeIdentifier>
<StripSymbols>true</StripSymbols>
<InteropExports>true</InteropExports>
<EmitCompilerGeneratedFiles>true</EmitCompilerGeneratedFiles>
<EnableRequestDelegateGenerator>true</EnableRequestDelegateGenerator>
<PublishAot>true</PublishAot>
@@ -48,12 +49,24 @@
<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>
@@ -69,15 +82,17 @@
<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="Scalar.AspNetCore" />
<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>
+85 -3
View File
@@ -1,12 +1,15 @@
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using System.Extensions.Core;
using System.Logging;
using System.Net.NetworkInformation;
using System.Reflection;
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;
@@ -20,8 +23,12 @@ namespace Daybreak.API;
public class EntryPoint
{
private const int StartPort = 5080;
private const string GwcaDllName = "gwca.dll";
private static readonly TimeSpan InitializationTimeout = TimeSpan.FromSeconds(5);
private static readonly CancellationTokenSource CancellationTokenSource = new();
private static readonly object GwcaResolverLock = new();
private static IntPtr gwcaHandle;
private static bool gwcaResolverRegistered;
[UnmanagedCallersOnly(EntryPoint = "ThreadInit"), STAThread]
[RequiresUnreferencedCode("The handler uses a static method that gets referenced, so there's no unreferenced code to worry about")]
@@ -85,8 +92,6 @@ public class EntryPoint
.WithRoutes()
.WithHealthChecks();
builder.Services.AddOpenApi();
// Add CORS policy - allow localhost and trusted community sites
builder.Services.AddCors(options =>
{
@@ -131,13 +136,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;
}
}
@@ -173,4 +184,75 @@ public class EntryPoint
return true;
}
private static void PreloadNativeDependencies(ScopedLogger<EntryPoint> logger)
{
// First, check if gwca.dll is already loaded in the process (e.g. by GWToolbox).
// If so, reuse it via a ref-count bump on the existing path rather than loading
// a second copy from the Daybreak.API directory. Loading a second instance of
// gwca.dll would result in duplicate native modules in the process, which can
// lead to inconsistent state across mods that depend on it.
string? daybreakApiDir = default;
foreach (ProcessModule module in Process.GetCurrentProcess().Modules)
{
if (module.ModuleName?.Equals("gwca.dll", StringComparison.OrdinalIgnoreCase) is true)
{
var handle = NativeLibrary.Load(module.FileName);
RegisterGwcaResolver(handle, module.FileName, logger);
logger.LogDebug($"Reused already-loaded gwca.dll from {module.FileName}");
return;
}
if (daybreakApiDir is null &&
module.ModuleName?.Contains("Daybreak.API", StringComparison.OrdinalIgnoreCase) is true)
{
daybreakApiDir = Path.GetDirectoryName(module.FileName);
}
}
if (daybreakApiDir is null)
{
logger.LogError("Could not locate Daybreak.API module directory to preload gwca.dll");
return;
}
var gwcaPath = Path.Combine(daybreakApiDir, "gwca.dll");
if (File.Exists(gwcaPath))
{
var handle = NativeLibrary.Load(gwcaPath);
RegisterGwcaResolver(handle, gwcaPath, logger);
logger.LogDebug($"Preloaded gwca.dll from {gwcaPath}");
}
else
{
logger.LogError($"gwca.dll not found at {gwcaPath}");
}
}
private static void RegisterGwcaResolver(IntPtr handle, string path, ScopedLogger<EntryPoint> logger)
{
lock (GwcaResolverLock)
{
gwcaHandle = handle;
if (gwcaResolverRegistered)
{
logger.LogDebug($"Updated gwca.dll resolver handle for {path}");
return;
}
NativeLibrary.SetDllImportResolver(typeof(GWCA).Assembly, ResolveNativeLibrary);
gwcaResolverRegistered = true;
logger.LogDebug($"Registered gwca.dll import resolver for {path}");
}
}
private static IntPtr ResolveNativeLibrary(string libraryName, Assembly assembly, DllImportSearchPath? searchPath)
{
if (string.Equals(libraryName, GwcaDllName, StringComparison.OrdinalIgnoreCase))
{
return gwcaHandle;
}
return IntPtr.Zero;
}
}
@@ -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);
}
}
-312
View File
@@ -1,312 +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();
/// <summary>
/// Builds a thunk for __fastcall with 1 parameter (ECX only).
/// </summary>
protected unsafe static nint BuildFastCallThunk1(nuint target)
{
if (target is 0)
{
return 0;
}
var code = stackalloc byte[10]
{
0x58, // pop eax (ret)
0x59, // pop ecx (arg1)
0x50, // push eax (ret)
0xB8, 0, 0, 0, 0, // mov eax, TARGET
0xFF, 0xE0 // jmp eax
}; // 10 bytes total
const int TARGET_OFFSET = 4;
Unsafe.WriteUnaligned(ref code[TARGET_OFFSET], (uint)target);
var mem = NativeMethods.VirtualAlloc(0, 10, NativeMethods.MEM_COMMIT | NativeMethods.MEM_RESERVE, NativeMethods.PAGE_EXECUTE_READWRITE);
if (mem is 0)
{
throw new Win32Exception(Marshal.GetLastWin32Error(), "VirtualAlloc");
}
Buffer.MemoryCopy(code, (void*)mem, 10, 10);
return mem;
}
/// <summary>
/// Builds a thunk for __fastcall with 2+ parameters (ECX and EDX).
/// </summary>
protected unsafe static nint BuildFastCallThunk2(nuint target)
{
if (target is 0)
{
return 0;
}
var code = stackalloc byte[11]
{
0x58, // pop eax (ret)
0x59, // pop ecx (ctx)
0x5A, // pop edx (edxVal)
0x50, // push eax (ret)
0xB8,0,0,0,0, // mov eax, TARGET
0xFF,0xE0 // jmp eax
}; // 11 bytes total
const int TARGET_OFFSET = 5;
Unsafe.WriteUnaligned(ref code[TARGET_OFFSET], (uint)target);
var mem = NativeMethods.VirtualAlloc(0, 11, NativeMethods.MEM_COMMIT | NativeMethods.MEM_RESERVE, NativeMethods.PAGE_EXECUTE_READWRITE);
if (mem is 0)
{
throw new Win32Exception(Marshal.GetLastWin32Error(), "VirtualAlloc");
}
Buffer.MemoryCopy(code, (void*)mem, 11, 11);
return mem;
}
}
public unsafe sealed class GWFastCall<T1, T2>(GWAddressCache target) : GWFastCall(target)
{
private delegate* unmanaged[Stdcall]<T1, T2> func = null;
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public unsafe T2? Invoke(T1 t1)
{
if (this.func is null)
{
this.Initialize();
}
if (this.func is not null)
{
if (typeof(T2) == typeof(Void))
{
((delegate* unmanaged[Stdcall]<T1, void>)this.func)(t1);
return default;
}
return this.func(t1);
}
return default;
}
public override void Initialize()
{
if (this.Cache.GetAddress() is not nuint addr ||
addr is 0)
{
return;
}
var call = BuildFastCallThunk1(addr);
this.func = (delegate* unmanaged[Stdcall]<T1, T2>)call;
}
}
public unsafe sealed class GWFastCall<T1, T2, T3>(GWAddressCache target) : GWFastCall(target)
{
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 = BuildFastCallThunk2(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 = BuildFastCallThunk2(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 = BuildFastCallThunk2(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 = BuildFastCallThunk2(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 = BuildFastCallThunk2(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;
}
-246
View File
@@ -1,246 +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
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(0x0108)]
public readonly TagInfo* Tags;
[FieldOffset(0x010E)]
public readonly byte Primary;
[FieldOffset(0x010F)]
public readonly byte Secondary;
[FieldOffset(0x0110)]
public readonly byte Level;
[FieldOffset(0x0111)]
public readonly byte TeamId;
[FieldOffset(0x0118)]
public readonly float EnergyRegen;
[FieldOffset(0x0120)]
public readonly float Energy;
[FieldOffset(0x0124)]
public readonly uint MaxEnergy;
[FieldOffset(0x012C)]
public readonly float HealthRegen;
[FieldOffset(0x0134)]
public readonly float Health;
[FieldOffset(0x0138)]
public readonly uint MaxHealth;
[FieldOffset(0x013C)]
public readonly uint Effects;
[FieldOffset(0x0158)]
public readonly LivingAgentModelState ModelState;
[FieldOffset(0x015C)]
public readonly LivingAgentState State;
[FieldOffset(0x0184)]
public readonly uint LoginNumber;
[FieldOffset(0x01B5)]
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
];
}
-30
View File
@@ -1,30 +0,0 @@
using System.Runtime.InteropServices;
namespace Daybreak.API.Interop.GuildWars;
public enum BagType
{
None,
Inventory,
Equipped,
NotCollected,
Storage,
MaterialsStorage
}
[StructLayout(LayoutKind.Explicit, Pack = 1)]
public readonly unsafe struct Bag
{
[FieldOffset(0x0000)]
public readonly BagType Type;
[FieldOffset(0x0004)]
public readonly uint Index;
[FieldOffset(0x000C)]
public readonly uint ContainerItem;
[FieldOffset(0x0010)]
public readonly uint ItemsCount;
[FieldOffset(0x0014)]
public readonly Bag* Bags;
[FieldOffset(0x0018)]
public readonly GuildWarsArray<WrappedPointer<Item>> Items;
}
@@ -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
}
-89
View File
@@ -1,89 +0,0 @@
using Daybreak.API.Models;
using System.Extensions;
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(0x00A8)]
public readonly GuildWarsArray<FrameInteractionCallback> FrameCallbacks;
[FieldOffset(0x00b8)]
public readonly uint ChildOffsetId;
[FieldOffset(0x00bc)]
public readonly uint FrameId;
[FieldOffset(0x0128)]
public readonly FrameRelation Relation;
[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;
}
[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;
}
[StructLayout(LayoutKind.Explicit, Pack = 1)]
public readonly unsafe struct CharSelectorContext
{
[FieldOffset(0x0000)]
public readonly uint Vtable;
[FieldOffset(0x0004)]
public readonly uint FrameId;
[FieldOffset(0x0008)]
public readonly GuildWarsArray<WrappedPointer<CharSelectorChar>> Chars;
}
[StructLayout(LayoutKind.Explicit, Pack = 1)]
public readonly struct CharSelectorChar
{
[FieldOffset(0x0020)]
public readonly Array20Char Name;
}
@@ -1,34 +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(0x0018)]
public readonly TextParserContext* TextParserContext;
[FieldOffset(0x0028)]
public readonly AccountGameContext* AccountContext;
[FieldOffset(0x002C)]
public readonly WorldContext* WorldContext;
[FieldOffset(0x003C)]
public readonly GuildContext* GuildContext;
[FieldOffset(0x0040)]
public readonly ItemContext* ItemContext;
[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,17 +13,44 @@ public readonly unsafe struct GuildWarsArray<T> : IEnumerable<T>
public readonly uint Size;
public readonly uint Param;
/// <summary>
/// Upper bound on a believable element count. Guild Wars' largest arrays hold at most a
/// few thousand entries, so anything beyond this is uninitialised or misresolved memory
/// rather than a real array.
/// </summary>
private const uint MaxPlausibleCapacity = 0x10000;
/// <summary>
/// Mirrors <c>GW::BaseArray::valid()</c> - the buffer must be null or aligned and the size
/// must fit within the capacity - plus a bound on the capacity itself. Guild Wars leaves
/// these fields transiently inconsistent while it (re)allocates an array, and a
/// misresolved GWCA scan can point this struct at arbitrary bytes that still satisfy
/// <c>size &lt;= capacity</c>.
/// </summary>
public bool IsValid =>
(this.Buffer is null || ((nuint)this.Buffer & 0x3) == 0) &&
this.Size <= this.Capacity &&
this.Capacity <= MaxPlausibleCapacity;
/// <summary>
/// Number of elements that can safely be read. Zero whenever the array is not backed by
/// a buffer, matching <c>GW::BaseArray::get()</c>, which returns null instead of
/// dereferencing. Reading past this is an access violation, and because NativeAOT cannot
/// throw <c>AccessViolationException</c> it fail-fasts and takes Guild Wars down with it.
/// </summary>
public uint Count => this.Buffer is not null && this.IsValid ? this.Size : 0;
public T this[int index]
{
get
{
ArgumentOutOfRangeException.ThrowIfNegative(index);
ArgumentOutOfRangeException.ThrowIfGreaterThanOrEqual((uint)index, this.Size);
ArgumentOutOfRangeException.ThrowIfGreaterThanOrEqual((uint)index, this.Count);
return this.Buffer[index];
}
}
public Enumerator GetEnumerator() => new(this.Buffer, this.Size);
public Enumerator GetEnumerator() => new(this.Buffer, this.Count);
IEnumerator<T> IEnumerable<T>.GetEnumerator() => this.GetEnumerator();
-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,58 +0,0 @@
using System.Runtime.InteropServices;
namespace Daybreak.API.Interop.GuildWars;
[StructLayout(LayoutKind.Sequential, Pack = 1)]
public readonly unsafe struct Inventory
{
public readonly Bag* UnusedBag;
public readonly Bag* Backpack;
public readonly Bag* BeltPouch;
public readonly Bag* Bag1;
public readonly Bag* Bag2;
public readonly Bag* EquipmentPack;
public readonly Bag* MaterialStorage;
public readonly Bag* UnclaimedItems;
public readonly Bag* Storage1;
public readonly Bag* Storage2;
public readonly Bag* Storage3;
public readonly Bag* Storage4;
public readonly Bag* Storage5;
public readonly Bag* Storage6;
public readonly Bag* Storage7;
public readonly Bag* Storage8;
public readonly Bag* Storage9;
public readonly Bag* Storage10;
public readonly Bag* Storage11;
public readonly Bag* Storage12;
public readonly Bag* Storage13;
public readonly Bag* Storage14;
public readonly Bag* EquippedItems;
public readonly Item* Bundle;
public readonly uint StoragePanesUnlocked;
public readonly Bag*[] Bags => [
this.Backpack,
this.BeltPouch,
this.Bag1,
this.Bag2,
this.EquipmentPack,
this.MaterialStorage,
this.UnclaimedItems,
this.Storage1,
this.Storage2,
this.Storage3,
this.Storage4,
this.Storage5,
this.Storage6,
this.Storage7,
this.Storage8,
this.Storage9,
this.Storage10,
this.Storage11,
this.Storage12,
this.Storage13,
this.Storage14,
this.EquippedItems
];
}
-127
View File
@@ -1,127 +0,0 @@
using System.Runtime.InteropServices;
namespace Daybreak.API.Interop.GuildWars;
public enum ItemType : byte
{
Salvage,
Axe = 2,
Bag,
Boots,
Bow,
Bundle,
Chestpiece,
Rune_Mod,
Usable,
Dye,
Materials_Zcoins,
Offhand,
Gloves,
Hammer = 15,
Headpiece,
CC_Shards,
Key,
Leggings,
Gold_Coin,
Quest_Item,
Wand,
Shield = 24,
Staff = 26,
Sword,
Kit = 29,
Trophy,
Scroll,
Daggers,
Present,
Minipet,
Scythe,
Spear,
Storybook = 43,
Costume,
Costume_Headpiece,
Unknown = 0xff
};
[StructLayout(LayoutKind.Sequential, Pack = 1)]
public readonly struct ItemModifier(uint mod)
{
public static readonly ItemModifier Zero = new();
public readonly uint Mod = mod;
public uint Identifier => this.Mod >> 16;
public uint Arg1 => (this.Mod & 0x0000FF00) >> 8;
public uint Arg2 => this.Mod & 0x000000FF;
public uint Arg => this.Mod & 0x0000FFFF;
}
[StructLayout(LayoutKind.Explicit, Pack = 1)]
public readonly unsafe struct Item
{
[FieldOffset(0x0000)]
public readonly uint ItemId;
[FieldOffset(0x0004)]
public readonly uint AgentId;
[FieldOffset(0x0008)]
public readonly Bag* BagEquipped;
[FieldOffset(0x000C)]
public readonly Bag* Bag;
[FieldOffset(0x0010)]
public readonly ItemModifier* Modifiers;
[FieldOffset(0x0014)]
public readonly uint ModifiersCount;
[FieldOffset(0x001C)]
public readonly uint ModelFileId;
[FieldOffset(0x0020)]
public readonly ItemType Type;
[FieldOffset(0x0024)]
public readonly ushort Value;
[FieldOffset(0x0028)]
public readonly uint Interaction;
[FieldOffset(0x002C)]
public readonly uint ModelId;
[FieldOffset(0x0030)]
public readonly char* InfoString;
[FieldOffset(0x0034)]
public readonly char* NameEncoded;
[FieldOffset(0x0038)]
public readonly char* CompleteNameEncoded;
[FieldOffset(0x003C)]
public readonly char* SingleItemName;
[FieldOffset(0x0048)]
public readonly ushort ItemFormula;
[FieldOffset(0x004A)]
public readonly byte IsMaterialSalvageable;
[FieldOffset(0x004C)]
public readonly ushort Quantity;
[FieldOffset(0x004E)]
public readonly byte Equipped;
[FieldOffset(0x004F)]
public readonly byte Profession;
[FieldOffset(0x0050)]
public readonly uint Slot;
public readonly bool Stackable => (this.Interaction & 0x80000) != 0;
public readonly bool Inscribable => (this.Interaction & 0x08000000) != 0;
}
@@ -1,16 +0,0 @@
using System.Runtime.InteropServices;
namespace Daybreak.API.Interop.GuildWars;
[StructLayout(LayoutKind.Explicit, Pack = 1)]
public readonly struct ItemContext
{
[FieldOffset(0x0024)]
public readonly GuildWarsArray<WrappedPointer<Bag>> Bags;
[FieldOffset(0x00B8)]
public readonly GuildWarsArray<WrappedPointer<Item>> Items;
[FieldOffset(0x00F8)]
public readonly WrappedPointer<Inventory> Inventory;
}
@@ -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,20 +0,0 @@
using System.Runtime.InteropServices;
namespace Daybreak.API.Interop.GuildWars;
[StructLayout(LayoutKind.Explicit, Pack = 1, Size = 0x34)]
public unsafe struct PartySearchWindowContext
{
[FieldOffset(0x0000)]
public uint* Vtable;
[FieldOffset(0x0004)]
public uint FrameId;
[FieldOffset(0x0020)]
public uint SelectedPartySearchId;
[FieldOffset(0x0024)]
public uint SelectedHeroId;
[FieldOffset(0x0028)]
public uint SelectedHenchmanId;
[FieldOffset(0x002C)]
public uint CurrentTab;
}
@@ -1,47 +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 = 0x50)]
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 ReforgedOrDhuumsFlags;
[FieldOffset(0x0038)]
public readonly uint PlayerNumber;
[FieldOffset(0x003C)]
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,24 +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(0x0148)]
public readonly GuildWarsArray<LoginCharacterContext> LoginCharacters;
}
[StructLayout(LayoutKind.Explicit, Pack = 1)]
public readonly struct LoginCharacterContext
{
[FieldOffset(0x0004)]
public readonly Array20Char CharacterName;
}
@@ -1,40 +0,0 @@
namespace Daybreak.API.Interop.GuildWars;
public enum EnumPreference : uint
{
CharSortOrder = 0,
AntiAliasing = 1,
Reflections = 2,
ShaderQuality = 3,
ShadowQuality = 4,
TerrainQuality = 5,
InterfaceSize = 6,
FrameLimiter = 7,
Count = 8
}
public enum CharSortOrder : uint
{
None = 0,
Alphabetize = 1,
PvPRP = 2
}
public enum NumberPreference : uint
{
AutoTournPartySort = 0,
ChatState = 1,
Count = 0x2b
}
public enum FlagPreference : uint
{
}
public enum StringPreference : uint
{
Unk1 = 0,
Unk2 = 1,
LastCharacterName = 2,
Count = 3
}
@@ -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;
}
}
-25
View File
@@ -1,25 +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;
}
[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;
}
@@ -1,10 +0,0 @@
using System.Runtime.InteropServices;
namespace Daybreak.API.Interop.GuildWars;
[StructLayout(LayoutKind.Explicit, Pack = 1)]
public struct TextParserContext
{
[FieldOffset(0x01D0)]
public Language Language;
}
-48
View File
@@ -1,48 +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.Explicit, Pack = 1, Size = 0x2C)]
public readonly struct TitleContext
{
[FieldOffset(0x0000)]
public readonly TitleProps Props;
[FieldOffset(0x0004)]
public readonly uint CurrentPoints;
[FieldOffset(0x0008)]
public readonly uint CurrentTitleTierIndex;
[FieldOffset(0x000C)]
public readonly uint PointsNeededForCurrentRank;
[FieldOffset(0x0014)]
public readonly uint NextTitleTierIndex;
[FieldOffset(0x0018)]
public readonly uint PointsNeededForNextRank;
[FieldOffset(0x001C)]
public readonly uint MaxTitleRank;
[FieldOffset(0x0020)]
public readonly uint MaxTitleTierIndex;
}
[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;
}
@@ -7,6 +7,18 @@ public readonly unsafe struct WrappedPointer<T>(T* pointer)
public bool IsNull => this.Pointer is null;
/// <summary>
/// True when the pointer is non-null and plausibly dereferenceable.
/// </summary>
/// <remarks>
/// Guild Wars structures are 4-byte aligned on x86, so a misaligned address is never a
/// real structure. When a GWCA pattern scan fails to resolve, it can hand back an
/// address inside Gw.exe's code section (for example 0x0048CA72); reading through that
/// yields nonsense field values and eventually an access violation. NativeAOT cannot
/// throw <c>AccessViolationException</c>, so it fail-fasts and terminates Guild Wars.
/// </remarks>
public bool IsValid => this.Pointer is not null && ((nuint)this.Pointer & 0x3) == 0;
public static implicit operator WrappedPointer<T>(T* pointer) => new(pointer);
public static implicit operator T*(WrappedPointer<T> wrappedPointer) => wrappedPointer.Pointer;
+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}");
}
}
-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;
}
}
-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
};
-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; }
}
-18
View File
@@ -1,18 +0,0 @@
namespace Daybreak.API.Models;
public enum UIMessage : uint
{
None = 0x0,
InitFrame = 0x9,
DestroyFrame = 0xb,
KeyDown = 0x20, // wparam = UIPacket::kKeyAction*
KeyUp = 0x22, // wparam = UIPacket::kKeyAction*
MouseClick = 0x24, // wparam = UIPacket::kMouseClick*
MouseClick2 = 0x31, // wparam = UIPacket::kMouseAction*
MouseAction = 0x32, // wparam = UIPacket::kMouseAction*
FrameMessage_QuerySelectedIndex = 0x59, // Used to query selected index in character selector
WriteToChatLog = 0x10000000 | 0x7F, // wparam = UIPacket::kWriteToChatLog*. Triggered by the game when it wants to add a new message to chat.
WriteToChatLogWithSender = 0x10000000 | 0x80, // wparam = UIPacket::kWriteToChatLogWithSender*. Triggered by the game when it wants to add a new message to chat.
CheckUIState = 0x10000000 | 0x118, // lparam = uint32_t* out state (2 = char select ready)
Logout = 0x10000000 | 0x9D, // wparam = { bool unknown, bool character_select }
}
-66
View File
@@ -1,66 +0,0 @@
using System.Runtime.InteropServices;
namespace Daybreak.API.Models;
public static class UIPackets
{
public enum MouseButtons
{
Left = 0x0,
Middle = 0x1,
Right = 0x2
}
public enum ActionState
{
MouseDown = 0x6,
MouseUp = 0x7,
MouseClick = 0x8,
MouseDoubleClick = 0x9,
DragRelease = 0xb,
KeyDown = 0xe
}
[StructLayout(LayoutKind.Sequential, Pack = 1)]
public readonly struct LogChatMessage(string message, Channel channel)
{
[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, uint wParam = 0x4000, uint lParam = 0x0006)
{
public readonly uint Key = key;
public readonly uint WParam = wParam;
public readonly uint LParam = lParam;
}
[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;
}
[StructLayout(LayoutKind.Sequential, Pack = 1)]
public readonly struct MouseAction(uint frameId, uint childOffsetId, ActionState currentState, nuint wparam = 0, nuint lparam = 0)
{
public readonly uint FrameId = frameId;
public readonly uint ChildOffsetId = childOffsetId;
public readonly ActionState CurrentState = currentState;
public readonly nuint WParam = wparam;
public readonly nuint LParam = lparam;
}
}
+16
View File
@@ -49,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]
@@ -17,10 +17,6 @@ namespace Daybreak.API.Serialization;
[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))]
@@ -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);
}
}
+227 -83
View File
@@ -1,8 +1,10 @@
using Daybreak.API.Interop.GuildWars;
using Daybreak.API.Models;
using Daybreak.API.Extensions;
using Daybreak.API.Interop;
using Daybreak.API.Interop.GuildWars;
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;
@@ -15,22 +17,13 @@ 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)
@@ -42,34 +35,40 @@ public sealed class CharacterSelectService(
{
var gameContext = this.gameContextService.GetGameContext();
if (gameContext.IsNull ||
gameContext.Pointer->WorldContext is null)
gameContext.Pointer->World is null ||
gameContext.Pointer->Character is null)
{
scopedLogger.LogError("Game context is not initialized");
return default;
}
var availableCharsContext = this.gameContextService.GetAvailableChars();
if (availableCharsContext.IsNull)
if (!availableCharsContext.IsValid ||
!availableCharsContext.Pointer->IsValid)
{
scopedLogger.LogError("Available characters context is not initialized");
scopedLogger.LogError(
"Available characters context is not initialized (array@0x{Array:X8} buffer=0x{Buffer:X8} capacity={Capacity} size={Size})",
(nuint)availableCharsContext.Pointer,
availableCharsContext.IsValid ? (nuint)availableCharsContext.Pointer->Buffer : 0,
availableCharsContext.IsValid ? availableCharsContext.Pointer->Capacity : 0,
availableCharsContext.IsValid ? availableCharsContext.Pointer->Size : 0);
return default;
}
var currentUuid = gameContext.Pointer->CharContext->PlayerUuid.ToString();
var availableChars = new List<CharacterSelectEntry>((int)availableCharsContext.Pointer->Size);
var currentUuid = (*(Uuid*)gameContext.Pointer->Character->PlayerUuid).ToString();
var availableChars = new List<CharacterSelectEntry>((int)availableCharsContext.Pointer->Count);
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()));
}
// TODO: Reforged sometimes returns Zero UUID even when in-game, need to investigate why
@@ -136,6 +135,33 @@ public sealed class CharacterSelectService(
return true;
}
[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();
@@ -143,6 +169,12 @@ public sealed class CharacterSelectService(
{
unsafe
{
if (!this.IsCharSelectReady())
{
scopedLogger.LogError("Character select is not ready");
return false;
}
var selectorFrame = this.GetCharSelectorFrame();
if (selectorFrame is null)
{
@@ -151,7 +183,8 @@ public sealed class CharacterSelectService(
}
var ctx = this.uiContextService.GetFrameContext<CharSelectorContext>(selectorFrame);
if (ctx.IsNull)
if (ctx.IsNull ||
(nuint)ctx.Pointer <= 0xFFFF)
{
scopedLogger.LogError("Character selector context not found");
return false;
@@ -165,13 +198,23 @@ public sealed class CharacterSelectService(
}
// Get current selected index
var selectedIdx = 0;
this.uiContextService.SendFrameUIMessage(panesFrame, UIMessage.FrameMessage_QuerySelectedIndex, null, &selectedIdx);
uint selectedIdx = 0;
this.uiContextService.SendFrameUIMessage(panesFrame, UIMessage.kFrameMessage_0x4a, null, &selectedIdx);
var chosen = false;
for (uint i = 0; !chosen && i < ctx.Pointer->Chars.Size; i++)
// Find target character index
uint targetIdx = 0xFFFF;
// Count (not Size) so a not-yet-allocated roster yields zero iterations
// instead of indexing through a null buffer.
var charCount = ctx.Pointer->Chars.Count;
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)
@@ -180,49 +223,142 @@ public sealed class CharacterSelectService(
}
var charName = new string(charNameSpan[..nullIdx]);
if (!charName.StartsWith(characterName, StringComparison.OrdinalIgnoreCase))
if (!charName.Equals(characterName, StringComparison.OrdinalIgnoreCase))
{
continue;
}
while (selectedIdx != i)
{
var keyAction = new UIPackets.KeyAction(0x1c);
this.uiContextService.SendFrameUIMessage(panesFrame, UIMessage.KeyDown, &keyAction);
var newIdx = selectedIdx;
this.uiContextService.SendFrameUIMessage(panesFrame, UIMessage.FrameMessage_QuerySelectedIndex, null, &newIdx);
if (newIdx == selectedIdx)
{
break; // This shouldn't happen - the character should have changed
}
selectedIdx = newIdx;
}
chosen = selectedIdx == i;
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.
// Empty roster slots are represented by null entries in the character array, so
// step over them instead of bailing out (otherwise navigation gets stuck at the
// slot right before a gap and never reaches characters beyond it).
while (targetIdx < selectedIdx)
{
if (selectedIdx == 0)
{
break;
}
var prevIdx = selectedIdx - 1;
while (prevIdx > 0 && ctx.Pointer->Chars.Buffer[prevIdx].IsNull)
{
prevIdx--;
}
if (ctx.Pointer->Chars.Buffer[prevIdx].IsNull)
{
break;
}
if (!SelectChar(prevIdx))
{
scopedLogger.LogError("Failed to select previous character");
return false;
}
selectedIdx = prevIdx;
}
while (targetIdx > selectedIdx)
{
if (selectedIdx >= charCount - 1)
{
break;
}
var nextIdx = selectedIdx + 1;
while (nextIdx < charCount && ctx.Pointer->Chars.Buffer[nextIdx].IsNull)
{
nextIdx++;
}
if (nextIdx >= charCount)
{
break;
}
if (!SelectChar(nextIdx))
{
scopedLogger.LogError("Failed to select next character");
return false;
}
selectedIdx = nextIdx;
}
var chosen = selectedIdx == targetIdx;
if (!chosen)
{
scopedLogger.LogError("Failed to select character {name}", characterName);
return false;
}
// TODO: This needs to be reworked to use UI messages to click on Play
var hwnd = this.platformContextService.GetWindowHandle();
if (!hwnd.HasValue)
if (!play)
{
scopedLogger.LogError("Failed to get game window handle");
return true;
}
var playButton = this.uiContextService.GetFrameByLabel("Play");
if (playButton.IsNull)
{
scopedLogger.LogError("Play button not found");
return false;
}
NativeMethods.SendMessageW((nint)hwnd.Value, NativeMethods.WM_KEYDOWN, 0x50, 0x00190001);
NativeMethods.SendMessageW((nint)hwnd.Value, NativeMethods.WM_CHAR, 0x70, 0x00190001);
NativeMethods.SendMessageW((nint)hwnd.Value, NativeMethods.WM_KEYUP, 0x50, 0x00190001);
return true;
return this.uiContextService.ButtonClick(playButton);
}
}, cancellationToken);
}
@@ -243,7 +379,7 @@ public sealed class CharacterSelectService(
await Task.Delay(100, cancellationToken);
}
catch(Exception e)
catch (Exception e)
{
scopedLogger.LogError(e, "Error while waiting for character select to be ready");
throw;
@@ -255,25 +391,13 @@ public sealed class CharacterSelectService(
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);
}
// NOTE: Mirrors GWToolbox's GW::LoginMgr::SelectCharacterToPlay, which navigates the
// character carousel purely by stepping prev/next over the selector context's character
// array. It deliberately does NOT touch the in-game CharSortOrder preference: the
// navigation relies on the selector context order matching the visible carousel order,
// and forcing a re-sort here desyncs the two (the click-by-name steps then land on the
// wrong index, so navigation only appears to work in one direction).
return await this.SelectCharacterToPlay(characterName, play: true, cancellationToken);
}
private async Task<string?> GetCharNameByUuid(string uuid, CancellationToken cancellationToken)
@@ -291,7 +415,8 @@ public sealed class CharacterSelectService(
}
var availableCharsContext = this.gameContextService.GetAvailableChars();
if (availableCharsContext.IsNull)
if (!availableCharsContext.IsValid ||
!availableCharsContext.Pointer->IsValid)
{
scopedLogger.LogError("Available characters context is not initialized");
return default;
@@ -299,13 +424,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;
}
@@ -318,10 +442,10 @@ public sealed class CharacterSelectService(
{
await this.gameThreadService.QueueOnGameThread(() =>
{
var logoutMessage = new LogOutMessage(0, 1); // Changed to 1 for character select
var logoutMessage = new kLogout { Unknown = 0, CharacterSelect = 1 };
unsafe
{
this.uiContextService.SendMessage(UIMessage.Logout, (uint)&logoutMessage, 0);
this.uiContextService.SendMessage(UIMessage.kLogout, (uint)&logoutMessage, 0);
}
}, cancellationToken);
}
@@ -336,7 +460,27 @@ public sealed class CharacterSelectService(
private unsafe bool IsCharSelectReady()
{
return this.GetCharSelectorFrame() is not null;
// GW UI state must report char select (2). kCheckUIState fills the value passed via lParam.
uint uiState = 10;
this.uiContextService.SendMessage(UIMessage.kCheckUIState, 0, (nuint)(&uiState));
var selectorFrame = this.GetCharSelectorFrame();
if (uiState != 2 ||
selectorFrame is null ||
!selectorFrame->IsVisible)
{
return false;
}
// The frame context must be fully populated (a real pointer) and belong to this frame.
var ctx = this.uiContextService.GetFrameContext<CharSelectorContext>(selectorFrame);
if (ctx.IsNull ||
(nuint)ctx.Pointer <= 0xFFFF)
{
return false;
}
return ctx.Pointer->FrameId == selectorFrame->FrameId;
}
private unsafe Frame* GetCharSelectorFrame()
+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);
}
}
-117
View File
@@ -1,117 +0,0 @@
namespace Daybreak.API.Services;
public sealed class HashingService
{
private const uint DefaultHash = 0x325D1EAE;
private const uint WStringHashSeed1 = 0xE2C15C9D;
private const uint WStringHashSeed2 = 0x2170A28A;
private static readonly uint[] HashTable = [
0x2EB3C21D, 0xA6F1D482, 0xBE24628F, 0x35699E30, 0xC87EE224, 0x04BA0097, 0x791F0EAF, 0xA162D466,
0x685B376A, 0xF82133BA, 0x4682D46B, 0xCB25C0C5, 0xAC3F9D22, 0xABF17202, 0xEF987728, 0x3E400B83,
0x63003108, 0xD83F1C93, 0x5058F507, 0x032E9991, 0x3518BC24, 0x1F90D44B, 0x92508DCE, 0x42DAA15C,
0x5B4105A3, 0x4D5026AD, 0x07F838B0, 0xE8C4D2A2, 0xF9EF4F54, 0x1FC1A85F, 0xB9041A40, 0x446F6969,
0xACF1EE39, 0x1828CC65, 0xC2F6C59D, 0x2456C3C0, 0x64F70142, 0x9C8ADB42, 0xC76781BF, 0x6AEA07E3,
0xE98B14BF, 0x7DBCB35E, 0x6CE78C80, 0x1E3F93DE, 0x0954FEB7, 0xB3103AEF, 0x6DAAC1EE, 0x6535E301,
0x4A221E68, 0x819CDD81, 0x5378CE3B, 0xE7348236, 0x2C3A7BCC, 0x31C50234, 0x745319DA, 0xF0897A0B,
0x5C182968, 0x48E018F4, 0x593E3BB6, 0xAB588C8D, 0xF1101F1F, 0x1D36E8BC, 0xABC7C7C9, 0xF58AA6C8,
0xF75B4B98, 0xC447B576, 0x6E5290F2, 0xD344EDF8, 0x5E1BCAF1, 0xEE892405, 0x798F7609, 0x8905CC29,
0x8977DEDC, 0x007850BC, 0x0AA3114A, 0x2F61C8F2, 0x376DFAA4, 0x0BABFC12, 0x98E64E82, 0xE6743711,
0xE26B26B6, 0xCBB10C72, 0x87446F8F, 0x96E26C0D, 0xCF140831, 0x81F69B0B, 0x5C9DF30B, 0xB276F929,
0x2712C5B6, 0x80EA90D6, 0x8EB30322, 0xCF7A705A, 0x57A17E8C, 0x556A2425, 0x15104549, 0x5F3D549F,
0x5D98FDEE, 0x9D8FA73E, 0x9206C9EF, 0xEDA0541D, 0xA3164F2D, 0x04C83543, 0x9BB27230, 0xEE97F0BA,
0xB6AEB38C, 0x41FF4DA7, 0x4023EE54, 0xECBB78BC, 0xC2C54733, 0x8850B63F, 0x93BF56EF, 0xC80177D7,
0xE68B0C13, 0x3C1E9D4C, 0x5CB3EACC, 0xF16F5795, 0xAC2B8A5C, 0x952E466E, 0x3095B0D5, 0xEC9D3A43,
0x23D8FCF7, 0x10C7A93F, 0x1B479FB5, 0x3664CF8F, 0xA514A56B, 0xE16C4886, 0x63828188, 0xA47F14C1,
0x391C237C, 0xFB15911F, 0x29825A01, 0xFC66DA40, 0xB2719F4E, 0x7B0D1611, 0x8F1EE7DE, 0xA6A1E77D,
0xDFC25D30, 0x76377036, 0x8145DB82, 0x2300A4E9, 0xA2417F88, 0x1B8C6AFC, 0x68EBDC8E, 0x48FC0C41,
0x040C3DCD, 0x17F02C44, 0xCC40B026, 0x5089EE1E, 0x2A594C28, 0x0CC198EC, 0x3F676F3D, 0xCD0AA239,
0x274A31A4, 0xC6B4D4FA, 0x9480A62F, 0xAF8862EF, 0x627CA44D, 0x572E89D3, 0x4C05136C, 0x60F36E8C,
0x7DE31FEF, 0x1B97FED3, 0xED99E2BF, 0xA034D418, 0xBBDB3E04, 0x1F198B71, 0xED45E889, 0xF7F1B0DC,
0x1592B9A5, 0x96D60F96, 0x03D616EE, 0xA5EF679D, 0x831D791D, 0x579D5FD1, 0x4B44DC48, 0x38F7A2F0,
0x2BF34A28, 0xAF14318C, 0xBD66D20A, 0x9AC72D2A, 0xC41ADB95, 0x860B2205, 0xE0E580E7, 0x3A06669C,
0x2D1A40A2, 0x577EDF60, 0x1A37693A, 0x3E576C35, 0xE3857565, 0xE46E75EE, 0x94E3625D, 0x7D1B9280,
0x2A0CCCE1, 0xBAB36163, 0x66E4E087, 0x940129BB, 0x59C197CD, 0xB0634139, 0xBB2964E1, 0x17207138,
0xD8BD3728, 0xB324876C, 0x7C72FD04, 0xBB5D9D82, 0xD9B40236, 0xC0045BF6, 0xF85100ED, 0x36493582,
0x8D0DF92A, 0xC3328368, 0xC202E412, 0x7ADE0064, 0xD01529C6, 0x4445B2AD, 0x0F6FC63B, 0xEF976FC5,
0x709EC6BE, 0x69D31EA1, 0x794BFEB4, 0xB522DB95, 0x700DE6DF, 0x49098B71, 0x38B7FA82, 0x11EDDC04,
0x5BB21D9F, 0xB14BC6D4, 0x3009D754, 0x451F5F3D, 0xFAA58575, 0xFB190B0D, 0x999DF79F, 0x6798A7AA,
0x0BC434FF, 0x429E6C15, 0xA1EA2298, 0x55A57B81, 0xD7D74B8D, 0x8FECB38B, 0x658AD588, 0xF0190227,
0xBE5D2B36, 0x62C59613, 0xC9FEDD64, 0xADD07A1E, 0x757A7679, 0xD78B6DCC, 0x56F88B2C, 0x4CC020BB,
0xA88B51FE, 0xF47F7691, 0x48587EDD, 0x3FCEC5DB, 0xA7E127AC, 0x561F6161, 0x37AB9607, 0xFE23282B,
];
private static readonly uint[] StringHashTable = [
0x92B9A528, 0x25D4FC88, 0xEDCBEFB8, 0x51063A80,
0x91341C61, 0x0261229D, 0x726F48ED, 0xCE1C088C,
0x76253EB5, 0x31E3A0DE, 0xA2AAD215, 0xCA7D6D27,
0xA5F98970, 0x0541C365, 0x3C14FF04, 0x5056AF4F
];
private static uint HashHelper(uint result, byte b)
{
return result + (HashTable[result >> 24] ^ (result >> 6) ^ HashTable[b]);
}
public uint Hash(byte[] data)
{
var hash = DefaultHash;
foreach(var b in data)
{
hash = HashHelper(hash, b);
}
return hash;
}
public uint Hash(byte data)
{
return HashHelper(DefaultHash, data);
}
public uint Hash(short data)
{
uint hash = DefaultHash;
hash = HashHelper(hash, (byte)(data >> 0));
hash = HashHelper(hash, (byte)(data >> 8));
return hash;
}
public uint Hash(uint data)
{
uint hash = DefaultHash;
hash = HashHelper(hash, (byte)(data >> 0));
hash = HashHelper(hash, (byte)(data >> 8));
hash = HashHelper(hash, (byte)(data >> 16));
hash = HashHelper(hash, (byte)(data >> 24));
return hash;
}
public uint Hash(string data, int maxLength = -1)
{
if (maxLength is -1)
{
maxLength = data.Length;
}
uint seed1 = WStringHashSeed1;
uint seed2 = WStringHashSeed2;
uint hash = DefaultHash;
for(var i = 0; i < maxLength && i < data.Length; i++)
{
var c = data[i];
var uc = (uint)c;
if (c >= 'a' && c <= 'z')
{
uc -= 0x20;
}
seed1 = (seed1 << 3) ^ uc;
seed2 += StringHashTable[seed1 & 0xf];
hash ^= seed2 + seed1;
}
return hash;
}
}
@@ -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????xxxxxxx";
private const int PlayerAgentIdOffset = -0xE;
private static readonly byte[] PlayerAgentIdAddressPattern = [0x5d, 0xe9, 0x00, 0x00, 0x00, 0x00, 0x55, 0x8b, 0xec, 0x56, 0x57, 0x8b, 0x7d];
private readonly MemoryScanningService memoryScanningService;
private readonly GWAddressCache agentArrayAddress;
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,198 +1,16 @@
using Daybreak.API.Interop;
using Daybreak.API.Interop.GuildWars;
using Daybreak.API.Models;
using System.Core.Extensions;
using System.Extensions.Core;
using System.Runtime.InteropServices;
namespace Daybreak.API.Services.Interop;
public unsafe sealed class GameContextService : IAddressHealthService
public unsafe sealed class GameContextService
{
// TLS-based pattern to find GameContext getter function
private const string GameContextTlsMask = "xx????xxxxxxxxxxx????x";
private const int GameContextTlsOffset = 0;
private static readonly byte[] GameContextTlsPattern =
[
0x8B, 0x0D, 0x00, 0x00, 0x00, 0x00, // mov ecx, [TlsIndexPtr] - 6 bytes: xx????
0x64, 0xA1, 0x2C, 0x00, 0x00, 0x00, // mov eax, fs:[0000002C] - 6 bytes: xxxxxx
0x8B, 0x04, 0x88, // mov eax, [eax+ecx*4] - 3 bytes: xxx
0x8B, 0x80, 0x04, 0x00, 0x00, 0x00, // mov eax, [eax+00000004] - 6 bytes: xx????
0xC3 // ret - 1 byte: x
]; // Total: 22 bytes, mask: 22 chars
private const string PreGameContextFile = "UiPregame.cpp";
private const string PreGameContextAssertion = "!s_scene";
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 gameContextGetterAddress;
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.gameContextGetterAddress = new GWAddressCache(this.GetGameContextGetterAddress);
this.preGameContextAddress = new GWAddressCache(this.GetPreGameContextAddress);
this.availableCharsAddress = new GWAddressCache(this.GetAvailableCharactersAddress);
}
[UnmanagedFunctionPointer(CallingConvention.Cdecl)]
private delegate nuint GetGameContextDelegate();
public List<AddressState> GetAddressStates()
{
return [
new AddressState
{
Address = this.gameContextGetterAddress.GetAddress() ?? 0,
Name = nameof(this.gameContextGetterAddress),
},
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();
// Get the address of the GameContext getter function
var getterAddress = this.gameContextGetterAddress.GetAddress();
if (getterAddress is null or 0)
{
scopedLogger.LogError("Failed to get GameContext getter function address");
return 0;
}
// Call the function to get GameContext
var getter = Marshal.GetDelegateForFunctionPointer<GetGameContextDelegate>((nint)getterAddress);
var gameContextPtr = getter();
if (gameContextPtr == 0)
{
scopedLogger.LogError("GameContext getter returned null");
return 0;
}
scopedLogger.LogInformation("GameContext address: 0x{address:X8}", gameContextPtr);
return gameContextPtr;
}
private nuint GetGameContextGetterAddress()
{
var scopedLogger = this.logger.CreateScopedLogger();
var address = this.memoryScanningService.FindAddress(
GameContextTlsPattern,
GameContextTlsMask,
0);
if (address is 0)
{
scopedLogger.LogError("Failed to find GameContext getter function");
return 0U;
}
scopedLogger.LogInformation("GameContext getter function: 0x{address:X8}", address);
return address;
}
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;
}
var actualAddress = *(nuint*)preGameContextAddress;
scopedLogger.LogInformation("Pre-game context address: 0x{address:X8}", actualAddress);
return actualAddress;
}
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 arg1, nint arg2);
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 arg1, nint arg2)
{
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(arg1, arg2);
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;
}
}

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