Compare commits

..
99 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
376 changed files with 53934 additions and 8970 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."
+4
View File
@@ -13,10 +13,14 @@ on:
- "Daybreak.Shared/**"
- "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
+75 -18
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,7 +21,7 @@ jobs:
# (Injector + API) that both platforms need.
# ───────────────────────────────────────────────────────
build-x86:
runs-on: windows-latest
runs-on: windows-2025
outputs:
version: ${{ steps.get_version.outputs.version }}
@@ -24,10 +29,13 @@ jobs:
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
@@ -40,7 +48,7 @@ jobs:
echo "version=$version" >> $env:GITHUB_OUTPUT
- name: Install .NET
uses: actions/setup-dotnet@v5
uses: actions/setup-dotnet@v6
with:
dotnet-version: "10.x"
@@ -48,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: |
@@ -62,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"
@@ -89,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/
@@ -98,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/
@@ -114,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"
@@ -133,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/
@@ -149,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/
@@ -169,7 +226,7 @@ jobs:
steps:
# ── 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: |
@@ -228,13 +285,13 @@ jobs:
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/
+4
View File
@@ -15,6 +15,9 @@ on:
- "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'
@@ -18,7 +18,7 @@ jobs:
steps:
- name: Checkout
uses: actions/checkout@v6
uses: actions/checkout@v7
with:
fetch-depth: 0
token: ${{ secrets.RELEASE_PAT }}
@@ -30,7 +30,7 @@ jobs:
- name: Bump version in Directory.Build.props
id: bump
uses: vers-one/dotnet-project-version-updater@v1.7
uses: vers-one/dotnet-project-version-updater@v1.8
with:
file: "Directory.Build.props"
version: ${{ inputs.versionBump }}
+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: |
+2 -2
View File
@@ -27,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"
+2 -2
View File
@@ -181,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' })
+30 -2
View File
@@ -5,13 +5,41 @@
"name": "Debug Daybreak.Linux",
"type": "dotnet",
"request": "launch",
"projectPath": "${workspaceFolder}/Daybreak.Linux/Daybreak.Linux.csproj"
"projectPath": "${workspaceFolder}/Daybreak.Linux/Daybreak.Linux.csproj",
"console": "integratedTerminal",
"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"
"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
}
}
]
}
+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).
@@ -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());
}
}
+1 -4
View File
@@ -82,10 +82,7 @@
<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>
+66 -15
View File
@@ -3,6 +3,7 @@ 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;
@@ -22,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")]
@@ -87,8 +92,6 @@ public class EntryPoint
.WithRoutes()
.WithHealthChecks();
builder.Services.AddOpenApi();
// Add CORS policy - allow localhost and trusted community sites
builder.Services.AddCors(options =>
{
@@ -184,24 +187,72 @@ public class EntryPoint
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?.Contains("Daybreak.API", StringComparison.OrdinalIgnoreCase) is true)
if (module.ModuleName?.Equals("gwca.dll", StringComparison.OrdinalIgnoreCase) is true)
{
var moduleDir = Path.GetDirectoryName(module.FileName)!;
var gwcaPath = Path.Combine(moduleDir, "gwca.dll");
if (File.Exists(gwcaPath))
{
NativeLibrary.Load(gwcaPath);
logger.LogDebug($"Preloaded gwca.dll from {gwcaPath}");
}
else
{
logger.LogError($"gwca.dll not found at {gwcaPath}");
}
var handle = NativeLibrary.Load(module.FileName);
RegisterGwcaResolver(handle, module.FileName, logger);
logger.LogDebug($"Reused already-loaded gwca.dll from {module.FileName}");
return;
}
break;
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;
}
}
+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;
}
}
+18518 -730
View File
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;
}
}
@@ -1,27 +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)]
[GWCAEquivalent("AccountContext")]
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,16 +0,0 @@
using System.Runtime.InteropServices;
namespace Daybreak.API.Interop.GuildWars;
[StructLayout(LayoutKind.Sequential, Pack = 1, Size = 0x1C)]
[GWCAEquivalent("AccountInfo")]
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;
}
-254
View File
@@ -1,254 +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)]
[GWCAEquivalent("AgentEffects")]
public readonly struct AgentEffects
{
public readonly uint AgentId;
public readonly GuildWarsArray<Buff> Buffs;
public readonly GuildWarsArray<Effect> Effects;
}
[StructLayout(LayoutKind.Explicit, Pack = 1)]
[GWCAEquivalent("AgentContext")]
public readonly struct AgentGameContext
{
[FieldOffset(0x01AC)]
public readonly uint InstanceTimer;
}
[StructLayout(LayoutKind.Explicit, Pack = 1, Size = 196)]
[GWCAEquivalent("Agent")]
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)]
[GWCAEquivalent("AgentItem")]
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)]
[GWCAEquivalent("AgentGadget")]
public readonly struct AgentGadgetContext
{
[FieldOffset(0x00CC)]
public readonly uint ExtraType;
[FieldOffset(0x00D0)]
public readonly uint GadgetId;
}
[StructLayout(LayoutKind.Explicit, Pack = 1, Size = 0x1C0)]
[GWCAEquivalent("AgentLiving")]
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)]
[GWCAEquivalent("MapAgent")]
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)]
[GWCAEquivalent("AgentInfo")]
public readonly unsafe struct AgentInfo
{
[FieldOffset(0x0034)]
public readonly char* NameEncoded;
}
@@ -1,96 +0,0 @@
using System.Runtime.InteropServices;
namespace Daybreak.API.Interop.GuildWars;
[StructLayout(LayoutKind.Sequential, Pack = 1)]
[GWCAEquivalent("Attribute")]
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)]
[GWCAEquivalent("AttributeInfo")]
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)]
[GWCAEquivalent("PartyAttribute")]
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
];
}
-31
View File
@@ -1,31 +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)]
[GWCAEquivalent("Bag")]
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,9 +0,0 @@
namespace Daybreak.API.Interop.GuildWars;
[GWCAEquivalent("HeroBehavior")]
public enum Behavior
{
Fight,
Guard,
AvoidCombat
}
@@ -1,51 +0,0 @@
using System.Extensions;
using System.Runtime.InteropServices;
namespace Daybreak.API.Interop.GuildWars;
[StructLayout(LayoutKind.Explicit, Pack = 1)]
[GWCAEquivalent("CharContext")]
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,23 +0,0 @@
using System.Extensions;
using System.Runtime.InteropServices;
namespace Daybreak.API.Interop.GuildWars;
[StructLayout(LayoutKind.Explicit, Pack = 1)]
[GWCAEquivalent("CharacterInformation")]
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,184 +0,0 @@
namespace Daybreak.API.Interop.GuildWars;
[GWCAEquivalent("ControlAction")]
public enum ControlAction : 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,20 +0,0 @@
namespace Daybreak.API.Interop.GuildWars;
[GWCAEquivalent("District")]
public enum District
{
Current,
International,
American,
EuropeEnglish,
EuropeFrench,
EuropeGerman,
EuropeItalian,
EuropeSpanish,
EuropePolish,
EuropeRussian,
AsiaKorean,
AsiaChinese,
AsiaJapanese,
Unknown = 0xff
}
-90
View File
@@ -1,90 +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)]
[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(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,35 +0,0 @@
using System.Runtime.InteropServices;
namespace Daybreak.API.Interop.GuildWars;
[StructLayout(LayoutKind.Explicit, Pack = 1)]
[GWCAEquivalent("GameContext")]
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;
}
-12
View File
@@ -1,12 +0,0 @@
using System.Runtime.InteropServices;
namespace Daybreak.API.Interop.GuildWars;
[StructLayout(LayoutKind.Sequential, Pack = 1)]
[GWCAEquivalent("GamePos")]
public readonly struct GamePos
{
public readonly float X;
public readonly float Y;
public readonly uint Plane;
}
@@ -1,11 +0,0 @@
using System.Runtime.InteropServices;
namespace Daybreak.API.Interop.GuildWars;
[StructLayout(LayoutKind.Explicit, Pack = 1, Size = 0x78)]
[GWCAEquivalent("GameplayContext")]
public readonly struct GameplayContext
{
[FieldOffset(0x4C)]
public readonly float MissionMapZoom;
}
-111
View File
@@ -1,111 +0,0 @@
using System.Extensions;
using System.Runtime.InteropServices;
namespace Daybreak.API.Interop.GuildWars;
[StructLayout(LayoutKind.Explicit, Pack = 1)]
[GWCAEquivalent("GuildContext")]
public readonly 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;
}
@@ -13,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();
-44
View File
@@ -1,44 +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)]
[GWCAEquivalent("HeroInfo")]
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,31 +0,0 @@
using System.Runtime.InteropServices;
namespace Daybreak.API.Interop.GuildWars;
[GWCAEquivalent("InstanceType")]
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)]
[GWCAEquivalent("AreaInfo")]
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,59 +0,0 @@
using System.Runtime.InteropServices;
namespace Daybreak.API.Interop.GuildWars;
[StructLayout(LayoutKind.Sequential, Pack = 1)]
[GWCAEquivalent("Inventory")]
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
];
}
-129
View File
@@ -1,129 +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)]
[GWCAEquivalent("ItemModifier")]
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)]
[GWCAEquivalent("Item")]
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,17 +0,0 @@
using System.Runtime.InteropServices;
namespace Daybreak.API.Interop.GuildWars;
[StructLayout(LayoutKind.Explicit, Pack = 1)]
[GWCAEquivalent("ItemContext")]
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,18 +0,0 @@
namespace Daybreak.API.Interop.GuildWars;
[GWCAEquivalent("Language")]
public enum Language
{
English,
Korean,
French,
German,
Italian,
Spanish,
TraditionalChinese,
Japanese = 8,
Polish,
Russian,
BorkBorkBork = 17,
Unknown = 0xff
}
@@ -1,9 +0,0 @@
using System.Runtime.InteropServices;
namespace Daybreak.API.Interop.GuildWars;
[StructLayout(LayoutKind.Explicit, Pack = 1)]
[GWCAEquivalent("MapContext")]
public readonly struct MapContext
{
}
-63
View File
@@ -1,63 +0,0 @@
using System.Runtime.InteropServices;
namespace Daybreak.API.Interop.GuildWars;
[StructLayout(LayoutKind.Explicit)]
[GWCAEquivalent("NPC")]
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)]
[GWCAEquivalent("PetInfo")]
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;
}
-127
View File
@@ -1,127 +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)]
[GWCAEquivalent("PartyContext")]
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)]
[GWCAEquivalent("PartyInfo")]
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)]
[GWCAEquivalent("PartySearch")]
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,48 +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)]
[GWCAEquivalent("PlayerContext")]
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,25 +0,0 @@
using System.Extensions;
using System.Runtime.InteropServices;
namespace Daybreak.API.Interop.GuildWars;
[StructLayout(LayoutKind.Explicit, Pack = 1)]
[GWCAEquivalent("PreGameContext")]
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,45 +0,0 @@
namespace Daybreak.API.Interop.GuildWars;
[GWCAEquivalent("EnumPreference")]
public enum EnumPreference : uint
{
CharSortOrder = 0,
AntiAliasing = 1,
Reflections = 2,
ShaderQuality = 3,
ShadowQuality = 4,
TerrainQuality = 5,
InterfaceSize = 6,
FrameLimiter = 7,
Count = 8
}
[GWCAEquivalent("CharSortOrder")]
public enum CharSortOrder : uint
{
None = 0,
Alphabetize = 1,
PvPRP = 2
}
[GWCAEquivalent("NumberPreference")]
public enum NumberPreference : uint
{
AutoTournPartySort = 0,
ChatState = 1,
Count = 0x2b
}
[GWCAEquivalent("FlagPreference")]
public enum FlagPreference : uint
{
}
[GWCAEquivalent("StringPreference")]
public enum StringPreference : uint
{
Unk1 = 0,
Unk2 = 1,
LastCharacterName = 2,
Count = 3
}
@@ -1,48 +0,0 @@
using System.Runtime.InteropServices;
namespace Daybreak.API.Interop.GuildWars;
[GWCAEquivalent("Profession")]
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;
}
}
-26
View File
@@ -1,26 +0,0 @@
using System.Numerics;
using System.Runtime.InteropServices;
namespace Daybreak.API.Interop.GuildWars;
[StructLayout(LayoutKind.Explicit, Pack = 1, Size = 0x0034)]
[GWCAEquivalent("Quest")]
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,13 +0,0 @@
namespace Daybreak.API.Interop.GuildWars;
[GWCAEquivalent("ServerRegion")]
public enum ServerRegion
{
International = -2,
America = 0,
Korea,
Europe,
China,
Japan,
Unknown = 0xff
}
-62
View File
@@ -1,62 +0,0 @@
using System.Runtime.InteropServices;
namespace Daybreak.API.Interop.GuildWars;
[StructLayout(LayoutKind.Sequential, Pack = 1)]
[GWCAEquivalent("SkillbarSkill")]
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)]
[GWCAEquivalent("Skillbar")]
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)]
[GWCAEquivalent("Buff")]
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)]
[GWCAEquivalent("Effect")]
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,14 +0,0 @@
using System.Extensions;
namespace Daybreak.API.Interop.GuildWars;
[GWCAEquivalent("SkillTemplate")]
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;
}
-13
View File
@@ -1,13 +0,0 @@
using System.Runtime.InteropServices;
namespace Daybreak.API.Interop.GuildWars;
[StructLayout(LayoutKind.Sequential, Pack = 1)]
[GWCAEquivalent("TagInfo")]
public readonly struct TagInfo
{
public readonly ushort GuildId;
public readonly byte Primary;
public readonly byte Secondary;
public readonly ushort Level;
}
@@ -1,11 +0,0 @@
using System.Runtime.InteropServices;
namespace Daybreak.API.Interop.GuildWars;
[StructLayout(LayoutKind.Explicit, Pack = 1)]
[GWCAEquivalent("TextParser")]
public struct TextParserContext
{
[FieldOffset(0x01D0)]
public Language Language;
}
-51
View File
@@ -1,51 +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)]
[GWCAEquivalent("Title")]
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)]
[GWCAEquivalent("TitleTier")]
public readonly unsafe struct TitleTier
{
public readonly TitleProps Props;
public readonly uint TierNumber;
public readonly char* TierNameEncoded;
}
[StructLayout(LayoutKind.Sequential, Pack = 1)]
[GWCAEquivalent("TitleClientData")]
public readonly struct TitleClientData
{
public readonly uint TitleId;
public readonly uint NameId;
}
@@ -1,174 +0,0 @@
using System.Numerics;
using System.Runtime.InteropServices;
namespace Daybreak.API.Interop.GuildWars;
[StructLayout(LayoutKind.Explicit, Pack = 1)]
[GWCAEquivalent("WorldContext")]
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
@@ -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;
}
}
-27
View File
@@ -1,27 +0,0 @@
using Daybreak.API.Interop;
namespace Daybreak.API.Models;
[GWCAEquivalent("Channel")]
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; }
}
-21
View File
@@ -1,21 +0,0 @@
using Daybreak.API.Interop;
namespace Daybreak.API.Models;
[GWCAEquivalent("UIMessage")]
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;
}
}
@@ -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))]
+130 -67
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
@@ -143,6 +142,26 @@ public sealed class CharacterSelectService(
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();
@@ -150,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)
{
@@ -158,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;
@@ -173,11 +199,13 @@ public sealed class CharacterSelectService(
// Get current selected index
uint selectedIdx = 0;
this.uiContextService.SendFrameUIMessage(panesFrame, UIMessage.FrameMessage_QuerySelectedIndex, null, &selectedIdx);
this.uiContextService.SendFrameUIMessage(panesFrame, UIMessage.kFrameMessage_0x4a, null, &selectedIdx);
// Find target character index
uint targetIdx = 0xFFFF;
var charCount = ctx.Pointer->Chars.Size;
// 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++)
{
@@ -234,24 +262,29 @@ public sealed class CharacterSelectService(
Play = 0
};
var action = new UIPackets.MouseAction(
selectorFrame->FrameId,
selectorFrame->ChildOffsetId,
UIPackets.ActionState.MouseClick,
(nuint)(&buttonParam));
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.MouseClick2, &action))
if (!this.uiContextService.SendFrameUIMessage(parentFrame, UIMessage.kMouseClick2, &action))
{
return false;
}
uint newSelectedIdx = 0;
this.uiContextService.SendFrameUIMessage(panesFrame, UIMessage.FrameMessage_QuerySelectedIndex, null, &newSelectedIdx);
this.uiContextService.SendFrameUIMessage(panesFrame, UIMessage.kFrameMessage_0x4a, null, &newSelectedIdx);
return newSelectedIdx == idx;
}
}
// Navigate to target character by selecting previous/next until we reach it
// 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)
@@ -259,13 +292,24 @@ public sealed class CharacterSelectService(
break;
}
if (!SelectChar(selectedIdx - 1))
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--;
selectedIdx = prevIdx;
}
while (targetIdx > selectedIdx)
@@ -275,13 +319,24 @@ public sealed class CharacterSelectService(
break;
}
if (!SelectChar(selectedIdx + 1))
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++;
selectedIdx = nextIdx;
}
var chosen = selectedIdx == targetIdx;
@@ -324,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;
@@ -336,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)
@@ -372,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;
@@ -380,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;
}
@@ -399,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);
}
@@ -417,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);
}
}
@@ -5,15 +5,7 @@ namespace Daybreak.API.Services.Interop;
public unsafe sealed class AgentContextService
{
public WrappedPointer<GuildWarsArray<WrappedPointer<AgentContext>>> GetAgentArray()
{
// GuildWarsArray<nint> and GuildWarsArray<WrappedPointer<AgentContext>> have the same memory layout
// since both nint and WrappedPointer<T> are pointer-sized
return (GuildWarsArray<WrappedPointer<AgentContext>>*)GWCA.GW.Agents.GetAgentArray();
}
public uint GetPlayerAgentId() => GWCA.GW.PlayerMgr.GetPlayerAgentId(0);
public uint GetPlayerAgentId()
{
return GWCA.GW.PlayerMgr.GetPlayerAgentId(0);
}
public Agent* GetAgentById(uint agentId) => GWCA.GW.Agents.GetAgentByID(agentId);
}
@@ -10,7 +10,7 @@ public unsafe sealed class GameContextService
public WrappedPointer<PreGameContext> GetPreGameContext() => GWCA.GW.GetPreGameContext();
public WrappedPointer<GuildWarsArray<CharInfoContext>> GetAvailableChars() => GWCA.GW.GetAvailableChars();
public WrappedPointer<GuildWarsArray<CharacterInformation>> GetAvailableChars() => GWCA.GW.GetAvailableChars();
public bool IsMapLoaded() => GWCA.GW.Map.GetIsMapLoaded();
}
@@ -7,7 +7,7 @@ public sealed unsafe class InstanceContextService
{
public WrappedPointer<AreaInfo> GetAreaInfo() => GWCA.GW.Map.GetMapInfo(0);
public ServerRegion GetServerRegion() => GWCA.GW.Map.GetRegion();
public ServerRegion GetServerRegion() => (Daybreak.API.Interop.GuildWars.ServerRegion)GWCA.GW.Map.GetRegion();
public InstanceType GetInstanceType() => GWCA.GW.Map.GetInstanceType();
public InstanceType GetInstanceType() => (Daybreak.API.Interop.GuildWars.InstanceType)GWCA.GW.Map.GetInstanceType();
}
@@ -1,12 +1,13 @@
using Daybreak.API.Interop;
using static Daybreak.API.Interop.GWCA.GW.Constants;
namespace Daybreak.API.Services.Interop;
public sealed class PartyContextService
{
public bool AddHero(uint heroId) => GWCA.GW.PartyMgr.AddHero((int)heroId);
public bool AddHero(uint heroId) => GWCA.GW.PartyMgr.AddHero((HeroID)heroId);
public bool KickHero(uint heroId) => GWCA.GW.PartyMgr.KickHero((int)heroId);
public bool KickHero(uint heroId) => GWCA.GW.PartyMgr.KickHero((HeroID)heroId);
public bool KickAllHeroes() => GWCA.GW.PartyMgr.KickAllHeroes();
@@ -5,7 +5,7 @@ namespace Daybreak.API.Services.Interop;
public sealed class PreferencesService()
{
public uint? GetEnumPreference(EnumPreference pref) => GWCA.GW.UI.GetPreference(pref);
public uint? GetEnumPreference(EnumPreference pref) => GWCA.GW.UI.GetPreference((GWCA.GW.UI.EnumPreference)pref);
public bool SetEnumPreference(EnumPreference pref, uint value) => GWCA.GW.UI.SetPreference(pref, value);
public bool SetEnumPreference(EnumPreference pref, uint value) => GWCA.GW.UI.SetPreference((GWCA.GW.UI.EnumPreference)pref, value);
}
@@ -1,6 +1,5 @@
using Daybreak.API.Interop;
using Daybreak.API.Interop.GuildWars;
using Daybreak.API.Models;
using System.Collections.Concurrent;
using System.Core.Extensions;
using System.Extensions.Core;
@@ -23,21 +22,7 @@ public sealed class UIContextService(ILogger<UIContextService> logger)
public unsafe WrappedPointer<T> GetFrameContext<T>(WrappedPointer<Frame> frame)
where T : unmanaged
{
if (frame.IsNull || frame.Pointer->FrameCallbacks.Size == 0)
{
return null;
}
for (uint i = 0; i < frame.Pointer->FrameCallbacks.Size; i++)
{
var callback = frame.Pointer->FrameCallbacks.Buffer[i];
if (callback.UiCtl_Context != null)
{
return new WrappedPointer<T>((T*)callback.UiCtl_Context);
}
}
return null;
return (T*)GWCA.GW.UI.GetFrameContext(frame.Pointer);
}
public unsafe WrappedPointer<Frame> GetChildFrame(WrappedPointer<Frame> parent, uint childOffset)
@@ -66,16 +51,16 @@ public sealed class UIContextService(ILogger<UIContextService> logger)
return false;
}
return GWCA.GW.UI.SendFrameUIMessage(frame, messageId, arg1, (nint)arg2);
return GWCA.GW.UI.SendFrameUIMessage(frame, (GWCA.GW.UI.UIMessage)messageId, arg1, (nint)arg2);
}
public unsafe bool SetFrameDisabled(WrappedPointer<Frame> frame, bool disabled) => GWCA.GW.UI.SetFrameDisabled(frame, disabled);
public unsafe bool SetFrameVisible(WrappedPointer<Frame> frame, bool visible) => GWCA.GW.UI.SetFrameVisible(frame, visible);
public unsafe bool KeyDown(ControlAction action, WrappedPointer<Frame> frame) => GWCA.GW.UI.Keydown(action, frame);
public unsafe bool KeyDown(GWCA.GW.UI.ControlAction action, WrappedPointer<Frame> frame) => GWCA.GW.UI.Keydown(action, frame);
public unsafe bool KeyUp(ControlAction action, WrappedPointer<Frame> frame) => GWCA.GW.UI.Keyup(action, frame);
public unsafe bool KeyUp(GWCA.GW.UI.ControlAction action, WrappedPointer<Frame> frame) => GWCA.GW.UI.Keyup(action, frame);
public unsafe WrappedPointer<Frame> GetFrameByLabel(string label)
{
@@ -107,9 +92,9 @@ public sealed class UIContextService(ILogger<UIContextService> logger)
return GWCA.GW.UI.ButtonClick(frame);
}
public unsafe void SendMessage(UIMessage message, nuint wParam, nuint lParam) => GWCA.GW.UI.SendUIMessage(message, (void*)wParam, (nint)lParam);
public unsafe void SendMessage(UIMessage message, nuint wParam, nuint lParam) => GWCA.GW.UI.SendUIMessage((GWCA.GW.UI.UIMessage)message, (void*)wParam, (nint)lParam);
public unsafe Task<string> AsyncDecodeStringAsync(ushort* encodedString, Language language = Language.Unknown, CancellationToken cancellationToken = default)
public unsafe Task<string> AsyncDecodeStringAsync(ushort* encodedString, GWCA.GW.Constants.Language language = GWCA.GW.Constants.Language.Unknown, CancellationToken cancellationToken = default)
{
var taskCompletionSource = new TaskCompletionSource<string>();
cancellationToken.Register(() => taskCompletionSource.TrySetCanceled(cancellationToken));
@@ -150,7 +135,7 @@ public sealed class UIContextService(ILogger<UIContextService> logger)
return taskCompletionSource.Task;
}
public unsafe Task<string> AsyncDecodeStringAsync(string encodedString, Language language = Language.Unknown, CancellationToken cancellationToken = default)
public unsafe Task<string> AsyncDecodeStringAsync(string encodedString, GWCA.GW.Constants.Language language = GWCA.GW.Constants.Language.Unknown, CancellationToken cancellationToken = default)
{
// Pin the string and convert to ushort*
fixed (char* encodedPtr = encodedString)
+33 -23
View File
@@ -1,8 +1,10 @@
using Daybreak.API.Interop.GuildWars;
using Daybreak.API.Interop;
using Daybreak.API.Interop.GuildWars;
using Daybreak.API.Services.Interop;
using Daybreak.Shared.Models.Api;
using Daybreak.Shared.Models.Guildwars;
using System.Extensions.Core;
using System.Runtime.CompilerServices;
using System.Text;
namespace Daybreak.API.Services;
@@ -32,45 +34,48 @@ public sealed class InventoryService(
return default;
}
var inventory = gameContext.Pointer->ItemContext->Inventory;
if (inventory.IsNull)
var inventory = gameContext.Pointer->Items->Inventory;
if (inventory is null)
{
scopedLogger.LogError("Inventory is null");
return default;
}
var itemTuples = new List<(BagType Type,List<(uint ModelId, string EncodedCompleteName, string EncodedSingleName, string EncodedName, bool Inscribable, int Quantity, uint[] Modifiers, ItemType ItemType)>)>();
foreach (var bag in inventory.Pointer->Bags)
var itemTuples = new List<(GWCA.GW.Constants.BagType Type, List<(uint ModelId, string EncodedCompleteName, string EncodedSingleName, string EncodedName, bool Inscribable, int Quantity, uint[] Modifiers, ItemType ItemType, uint Interaction, uint ModelFileId, Byte DyeTint, ushort Value)>)>();
for (var i = 0; i < 23; i++)
{
// Use GWCA helper to fetch bag pointers by index — safer than reading an inline array directly.
var bag = GWCA.GW.Items.GetBagByIndex((uint)i);
if (bag is null)
{
continue;
}
var retBag = new List<(uint ModelId, string EncodedCompleteName, string EncodedSingleName, string EncodedName, bool Inscribable, int Quantity, uint[] Modifiers, ItemType ItemType)>();
itemTuples.Add((bag->Type, retBag));
var retBag = new List<(uint ModelId, string EncodedCompleteName, string EncodedSingleName, string EncodedName, bool Inscribable, int Quantity, uint[] Modifiers, ItemType ItemType, uint Interaction, uint ModelFileId, Byte DyeTint, ushort Value)>();
itemTuples.Add((bag->BagType, retBag));
if (bag->ItemsCount is 0)
{
continue;
}
foreach(var item in bag->Items)
foreach (var itemPtr in bag->Items.Value)
{
if (item.IsNull)
if (itemPtr is 0)
{
continue;
}
var singleItemName = new string(item.Pointer->SingleItemName);
var nameEncoded = new string(item.Pointer->NameEncoded);
var completeNameEncoded = new string(item.Pointer->CompleteNameEncoded);
var modifiers = new uint[item.Pointer->ModifiersCount];
for(var j = 0; j < item.Pointer->ModifiersCount; j++)
var item = (ItemStruct*)itemPtr;
var singleItemName = new string((char*)item->SingleItemName);
var nameEncoded = new string((char*)item->NameEnc);
var completeNameEncoded = new string((char*)item->CompleteNameEnc);
var modifiers = new uint[item->ModStructSize];
for (var j = 0; j < item->ModStructSize; j++)
{
modifiers[j] = item.Pointer->Modifiers[j].Mod;
modifiers[j] = item->ModStruct[j].Mod;
}
retBag.Add((item.Pointer->ModelId, completeNameEncoded, singleItemName, nameEncoded, item.Pointer->Inscribable, item.Pointer->Quantity, modifiers, item.Pointer->Type));
retBag.Add((item->ModelId, completeNameEncoded, singleItemName, nameEncoded, (item->Interaction & 0x08000000) != 0, item->Quantity, modifiers, (ItemType)item->Type, item->Interaction, item->ModelFileId, item->Dye.DyeTint, item->Value));
}
}
@@ -85,17 +90,18 @@ public sealed class InventoryService(
// Decode strings sequentially to avoid race conditions with the game's TextParser language field.
// Parallel decoding causes crashes because multiple operations race to set/restore the language.
var decodedItemTuples = new List<(BagType Type, List<(uint ModelId, string EncodedName, string? DecodedName, string EncodedSingleName, string? DecodedSingleName, string EncodedCompleteName, string? DecodedCompleteName, bool Inscribable, int Quantity, uint[] Modifiers, ItemType ItemType)> Items)>();
var decodedItemTuples = new List<(GWCA.GW.Constants.BagType Type, List<(uint ModelId, string EncodedName, string? DecodedName, string EncodedSingleName, string? DecodedSingleName, string EncodedCompleteName, string? DecodedCompleteName, bool Inscribable, int Quantity, uint[] Modifiers, ItemType ItemType, uint Interaction, uint ModelFileId, byte DyeTint, ushort Value)> Items)>();
foreach (var tuple in itemTuples)
{
var decodedItems = new List<(uint ModelId, string EncodedName, string? DecodedName, string EncodedSingleName, string? DecodedSingleName, string EncodedCompleteName, string? DecodedCompleteName, bool Inscribable, int Quantity, uint[] Modifiers, ItemType ItemType)>();
var decodedItems = new List<(uint ModelId, string EncodedName, string? DecodedName, string EncodedSingleName, string? DecodedSingleName, string EncodedCompleteName, string? DecodedCompleteName, bool Inscribable, int Quantity, uint[] Modifiers, ItemType ItemType, uint Interaction, uint ModelFileId, byte DyeTint, ushort Value)>();
foreach (var item in tuple.Item2)
{
var decodedName = await this.uIService.DecodeString(item.EncodedName, Language.English, cancellationToken);
var decodedCompleteName = await this.uIService.DecodeString(item.EncodedCompleteName, Language.English, cancellationToken);
var decodedSingleName = await this.uIService.DecodeString(item.EncodedSingleName, Language.English, cancellationToken);
decodedItems.Add((item.ModelId, item.EncodedName, decodedName, item.EncodedSingleName, decodedSingleName, item.EncodedCompleteName, decodedCompleteName, item.Inscribable, item.Quantity, item.Modifiers, item.ItemType));
var decodedName = await this.uIService.DecodeString(item.EncodedName, (GWCA.GW.Constants.Language)Language.English, cancellationToken);
var decodedCompleteName = await this.uIService.DecodeString(item.EncodedCompleteName, (GWCA.GW.Constants.Language)Language.English, cancellationToken);
var decodedSingleName = await this.uIService.DecodeString(item.EncodedSingleName, (GWCA.GW.Constants.Language)Language.English, cancellationToken);
decodedItems.Add((item.ModelId, item.EncodedName, decodedName, item.EncodedSingleName, decodedSingleName, item.EncodedCompleteName, decodedCompleteName, item.Inscribable, item.Quantity, item.Modifiers, item.ItemType, item.Interaction, item.ModelFileId, item.DyeTint, item.Value));
}
decodedItemTuples.Add((tuple.Type, decodedItems));
}
@@ -116,7 +122,11 @@ public sealed class InventoryService(
Quantity: item.Quantity,
Modifiers: item.Modifiers,
Properties: [.. ItemProperty.ParseItemModifiers([.. item.Modifiers.Select(m => (Shared.Models.Guildwars.ItemModifier)m)])],
ItemType: item.ItemType.ToString()))]))]);
ItemType: item.ItemType.ToString(),
Interaction: item.Interaction,
ModelFileId: item.ModelFileId,
DyeTint: item.DyeTint,
Value: item.Value))]))]);
}
private static string ToBase64(string encoded)
+3 -5
View File
@@ -22,16 +22,14 @@ public sealed class LoginService(
unsafe
{
var gameContext = this.gameContextService.GetGameContext();
if (gameContext.IsNull || gameContext.Pointer->CharContext is null)
if (gameContext.IsNull || gameContext.Pointer->Character is null)
{
scopedLogger.LogError("Game context is not initialized");
return default;
}
var playerNameSpan = gameContext.Pointer->CharContext->PlayerName.AsSpan();
var playerName = new string(playerNameSpan[..playerNameSpan.IndexOf('\0')]);
var emailSpan = gameContext.Pointer->CharContext->PlayerEmail.AsSpan();
var email = new string(emailSpan[..emailSpan.IndexOf('\0')]);
var playerName = new string(gameContext.Pointer->Character->PlayerName);
var email = new string(gameContext.Pointer->Character->PlayerEmail);
return new LoginInfo(email, playerName);
}
}, cancellationToken);
+100 -120
View File
@@ -1,4 +1,5 @@
using Daybreak.API.Extensions;
using Daybreak.API.Interop;
using Daybreak.API.Interop.GuildWars;
using Daybreak.API.Models;
using Daybreak.API.Services.Interop;
@@ -72,7 +73,7 @@ public sealed class MainPlayerService : IDisposable
var gameContext = this.gameContextService.GetGameContext();
if (gameContext.IsNull ||
gameContext.Pointer->WorldContext is null)
gameContext.Pointer->World is null)
{
scopedLogger.LogError("Failed to get game context");
return false;
@@ -85,23 +86,23 @@ public sealed class MainPlayerService : IDisposable
return false;
}
var playerAgent = this.GetAgentContext(playerAgentId);
var playerAgent = this.agentContextService.GetAgentById(playerAgentId);
if (playerAgent is null ||
playerAgent->Type is not AgentType.Living ||
playerAgent->Type is not (uint)AgentType.Living ||
playerAgent->AgentId != playerAgentId)
{
scopedLogger.LogError("Player agent {playerAgentId} not found in agent array", playerAgentId);
return false;
}
var livingAgent = (AgentLivingContext*)playerAgent;
if (livingAgent->Level != gameContext.Pointer->WorldContext->Level)
var livingAgent = (AgentLiving*)playerAgent;
if (livingAgent->Level != gameContext.Pointer->World->Level)
{
scopedLogger.LogError("Player agent not found. Player level mismatch: {level} != {gameLevel}", livingAgent->Level, gameContext.Pointer->WorldContext->Level);
scopedLogger.LogError("Player agent not found. Player level mismatch: {level} != {gameLevel}", livingAgent->Level, gameContext.Pointer->World->Level);
return false;
}
var agentProfession = gameContext.Pointer->WorldContext->Professions.AsValueEnumerable().FirstOrDefault(p => p.AgentId == playerAgentId);
var agentProfession = gameContext.Pointer->World->PartyProfessionStates.AsValueEnumerable().FirstOrDefault(p => p.AgentId == playerAgentId);
if (agentProfession.AgentId != playerAgentId)
{
scopedLogger.LogError("Failed to find agent profession for player agent id {agentId}", playerAgentId);
@@ -112,9 +113,9 @@ public sealed class MainPlayerService : IDisposable
(uint)singleBuild.Primary.Id,
(uint)singleBuild.Secondary.Id,
singleBuild.Skills.AsValueEnumerable().Select(s => (uint)s.Id).ToArray(),
livingAgent->Primary,
agentProfession.UnlockedProfessionsFlags,
[.. gameContext.Pointer->WorldContext->UnlockedCharacterSkills]);
livingAgent->Tags->Primary,
agentProfession.UnlockedProfessions,
[.. gameContext.Pointer->World->UnlockedCharacterSkills]);
if (!this.buildTemplateManager.CanTemplateApply(validationRequest))
{
@@ -122,9 +123,9 @@ public sealed class MainPlayerService : IDisposable
return false;
}
var attributeIds = new Array12Uint();
var attributeIds = new AttributeArray12();
var attributeValues = new Array12Uint();
var skills = new Array8Uint();
var skills = new SkillIDArray8();
for (var i = 0; i < singleBuild.Attributes.Count && i < 12; i++)
{
if (singleBuild.Attributes[i].Attribute is null)
@@ -132,23 +133,24 @@ public sealed class MainPlayerService : IDisposable
continue;
}
attributeIds[i] = (uint)singleBuild.Attributes[i].Attribute!.Id;
attributeIds[i] = (GWCA.GW.Constants.Attribute)singleBuild.Attributes[i].Attribute!.Id;
attributeValues[i] = (uint)singleBuild.Attributes[i].Points;
}
for (var i = 0; i < singleBuild.Skills.Count && i < 8; i++)
{
skills[i] = (uint)singleBuild.Skills[i].Id;
skills[i] = (GWCA.GW.Constants.SkillID)singleBuild.Skills[i].Id;
}
var skillTemplate = new SkillTemplate(
(uint)singleBuild.Primary.Id,
(uint)singleBuild.Secondary.Id,
(uint)Math.Min(singleBuild.Attributes.Count, 12),
attributeIds,
attributeValues,
skills);
var skillTemplate = new SkillTemplate
{
Primary = (GWCA.GW.Constants.Profession)singleBuild.Primary.Id,
Secondary = (GWCA.GW.Constants.Profession)singleBuild.Secondary.Id,
AttributesCount = (uint)Math.Min(singleBuild.Attributes.Count, 12),
AttributeIds = attributeIds,
Skills = skills,
};
Unsafe.CopyBlock(skillTemplate.AttributeValues, Unsafe.AsPointer(ref attributeValues), (uint)(12 * sizeof(uint)));
this.skillbarContextService.LoadBuild(playerAgentId, &skillTemplate);
return true;
}
@@ -156,11 +158,11 @@ public sealed class MainPlayerService : IDisposable
if (result)
{
await this.chatService.AddMessageAsync("Build applied successfully.", "Daybreak.API", Channel.Moderator, cancellationToken);
await this.chatService.AddMessageAsync("Build applied successfully.", "Daybreak.API", Channel.CHANNEL_MODERATOR, cancellationToken);
}
else
{
await this.chatService.AddMessageAsync("Failed to apply build.", "Daybreak.API", Channel.Moderator, cancellationToken);
await this.chatService.AddMessageAsync("Failed to apply build.", "Daybreak.API", Channel.CHANNEL_MODERATOR, cancellationToken);
}
return result;
@@ -193,21 +195,21 @@ public sealed class MainPlayerService : IDisposable
return default;
}
var skillbarContext = gameContext.Pointer->WorldContext->Skillbars.AsValueEnumerable().FirstOrDefault(s => s.AgentId == playerAgentId);
var skillbarContext = gameContext.Pointer->World->Skillbar.Value.AsValueEnumerable().FirstOrDefault(s => s.AgentId == playerAgentId);
if (skillbarContext.AgentId != playerAgentId)
{
scopedLogger.LogError("Failed to find skillbar context for player agent id {agentId}", playerAgentId);
return default;
}
var agentAttributes = gameContext.Pointer->WorldContext->Attributes.AsValueEnumerable().FirstOrDefault(a => a.AgentId == playerAgentId);
var agentAttributes = gameContext.Pointer->World->Attributes.Value.AsValueEnumerable().FirstOrDefault(a => a.AgentId == playerAgentId);
if (agentAttributes.AgentId != playerAgentId)
{
scopedLogger.LogError("Failed to find agent attributes for player agent id {agentId}", playerAgentId);
return default;
}
var professionContext = gameContext.Pointer->WorldContext->Professions.AsValueEnumerable().FirstOrDefault(p => p.AgentId == playerAgentId);
var professionContext = gameContext.Pointer->World->PartyProfessionStates.AsValueEnumerable().FirstOrDefault(p => p.AgentId == playerAgentId);
if (professionContext.AgentId != playerAgentId)
{
scopedLogger.LogError("Failed to find agent profession for player agent id {agentId}", playerAgentId);
@@ -215,11 +217,11 @@ public sealed class MainPlayerService : IDisposable
}
return new BuildEntry(
(int)professionContext.CurrentPrimary,
(int)professionContext.CurrentSecondary,
agentAttributes.Attributes.GetAttributeEntryList(),
[skillbarContext.Skill0.Id, skillbarContext.Skill1.Id, skillbarContext.Skill2.Id, skillbarContext.Skill3.Id,
skillbarContext.Skill4.Id, skillbarContext.Skill5.Id, skillbarContext.Skill6.Id, skillbarContext.Skill7.Id]);
(int)professionContext.Primary,
(int)professionContext.Secondary,
[.. agentAttributes.Attribute.GetAttributeEntryList()],
[(uint)skillbarContext.Skills[0].SkillId, (uint)skillbarContext.Skills[1].SkillId, (uint)skillbarContext.Skills[2].SkillId, (uint)skillbarContext.Skills[3].SkillId,
(uint)skillbarContext.Skills[4].SkillId, (uint)skillbarContext.Skills[5].SkillId, (uint)skillbarContext.Skills[6].SkillId, (uint)skillbarContext.Skills[7].SkillId]);
}
}, cancellationToken);
}
@@ -238,15 +240,15 @@ public sealed class MainPlayerService : IDisposable
}
var gameContext = this.gameContextService.GetGameContext();
if (gameContext.IsNull || gameContext.Pointer->WorldContext is null)
if (gameContext.IsNull || gameContext.Pointer->World is null)
{
scopedLogger.LogError("Game context is not initialized");
return default;
}
return new QuestLogInformation(
gameContext.Pointer->WorldContext->ActiveQuestId,
gameContext.Pointer->WorldContext->QuestLog.AsValueEnumerable().Select(q => new QuestInformation(q.QuestId, q.MapFrom)).ToList());
(uint)gameContext.Pointer->World->ActiveQuestId,
gameContext.Pointer->World->QuestLog.Value.AsValueEnumerable().Select(q => new QuestInformation((uint)q.QuestId, (uint)q.MapFrom)).ToList());
}
}, cancellationToken);
}
@@ -265,29 +267,27 @@ public sealed class MainPlayerService : IDisposable
}
var gameContext = this.gameContextService.GetGameContext();
if (gameContext.IsNull || gameContext.Pointer->CharContext is null ||
gameContext.Pointer->WorldContext is null)
if (gameContext.IsNull || gameContext.Pointer->Character is null ||
gameContext.Pointer->World is null)
{
scopedLogger.LogError("Game context is not initialized");
return default;
}
var playerNameSpan = gameContext.Pointer->CharContext->PlayerName.AsSpan();
var playerName = new string(playerNameSpan[..playerNameSpan.IndexOf('\0')]);
var emailSpan = gameContext.Pointer->CharContext->PlayerEmail.AsSpan();
var email = new string(emailSpan[..emailSpan.IndexOf('\0')]);
var accountName = new string(gameContext.Pointer->WorldContext->AccountInfo->AccountName);
var playerName = new string (gameContext.Pointer->Character->PlayerName);
var email = new string(gameContext.Pointer->Character->PlayerEmail);
var accountName = new string((char*)gameContext.Pointer->World->AccountInfo->AccountName);
return new MainPlayerInformation(
gameContext.Pointer->CharContext->PlayerUuid.ToString(),
(*(Uuid*)gameContext.Pointer->Character->PlayerUuid).ToString(),
email,
playerName,
accountName,
gameContext.Pointer->WorldContext->AccountInfo->Wins,
gameContext.Pointer->WorldContext->AccountInfo->Losses,
gameContext.Pointer->WorldContext->AccountInfo->Rating,
gameContext.Pointer->WorldContext->AccountInfo->QualifierPoints,
gameContext.Pointer->WorldContext->AccountInfo->Rank,
gameContext.Pointer->WorldContext->AccountInfo->TournamentRewardPoints);
gameContext.Pointer->World->AccountInfo->Wins,
gameContext.Pointer->World->AccountInfo->Losses,
gameContext.Pointer->World->AccountInfo->Rating,
gameContext.Pointer->World->AccountInfo->QualifierPoints,
gameContext.Pointer->World->AccountInfo->Rank,
gameContext.Pointer->World->AccountInfo->TournamentRewardPoints);
}
}, cancellationToken);
}
@@ -306,61 +306,57 @@ public sealed class MainPlayerService : IDisposable
}
var gameContext = this.gameContextService.GetGameContext();
var agentArray = this.agentContextService.GetAgentArray();
var playerAgentId = this.agentContextService.GetPlayerAgentId();
if (gameContext.IsNull ||
gameContext.Pointer->WorldContext is null ||
gameContext.Pointer->CharContext is null ||
gameContext.Pointer->WorldContext->MapAgents.Buffer is null ||
agentArray.IsNull ||
agentArray.Pointer->Buffer is null ||
gameContext.Pointer->World is null ||
gameContext.Pointer->Character is null ||
playerAgentId is 0x0)
{
scopedLogger.LogDebug("Game data is not yet initialized");
return default;
}
var playerAgent = this.GetAgentContext(playerAgentId);
var playerAgent = this.agentContextService.GetAgentById(playerAgentId);
if (playerAgent is null ||
playerAgent->Type is not AgentType.Living ||
playerAgent->Type is not (uint)AgentType.Living ||
playerAgent->AgentId != playerAgentId)
{
scopedLogger.LogError("Player agent {playerAgentId} not found in agent array", playerAgentId);
return default;
}
var livingAgent = (AgentLivingContext*)playerAgent;
if (livingAgent->Level != gameContext.Pointer->WorldContext->Level)
var livingAgent = (AgentLiving*)playerAgent;
if (livingAgent->Level != gameContext.Pointer->World->Level)
{
scopedLogger.LogError("Player agent not found. Player level mismatch: {level} != {gameLevel}", livingAgent->Level, gameContext.Pointer->WorldContext->Level);
scopedLogger.LogError("Player agent not found. Player level mismatch: {level} != {gameLevel}", livingAgent->Level, gameContext.Pointer->World->Level);
return default;
}
return new MainPlayerState(
gameContext.Pointer->WorldContext->Experience,
gameContext.Pointer->WorldContext->Level,
gameContext.Pointer->World->Experience,
gameContext.Pointer->World->Level,
gameContext.Pointer->WorldContext->CurrentLuxon,
gameContext.Pointer->WorldContext->CurrentKurzick,
gameContext.Pointer->WorldContext->CurrentImperial,
gameContext.Pointer->WorldContext->CurrentBalthazar,
gameContext.Pointer->WorldContext->MaxLuxon,
gameContext.Pointer->WorldContext->MaxKurzick,
gameContext.Pointer->WorldContext->MaxImperial,
gameContext.Pointer->WorldContext->MaxBalthazar,
gameContext.Pointer->WorldContext->TotalLuxon,
gameContext.Pointer->WorldContext->TotalKurzick,
gameContext.Pointer->WorldContext->TotalImperial,
gameContext.Pointer->WorldContext->TotalBalthazar,
gameContext.Pointer->World->CurrentLuxon,
gameContext.Pointer->World->CurrentKurzick,
gameContext.Pointer->World->CurrentImperial,
gameContext.Pointer->World->CurrentBalth,
gameContext.Pointer->World->MaxLuxon,
gameContext.Pointer->World->MaxKurzick,
gameContext.Pointer->World->MaxImperial,
gameContext.Pointer->World->MaxBalth,
gameContext.Pointer->World->TotalEarnedLuxon,
gameContext.Pointer->World->TotalEarnedKurzick,
gameContext.Pointer->World->TotalEarnedImperial,
gameContext.Pointer->World->TotalEarnedBalth,
// Energy and health are percentages of Max
livingAgent->Health * livingAgent->MaxHealth,
livingAgent->MaxHealth,
livingAgent->Hp * livingAgent->MaxHp,
livingAgent->MaxHp,
livingAgent->Energy * livingAgent->MaxEnergy,
livingAgent->MaxEnergy,
livingAgent->Primary,
livingAgent->Secondary,
livingAgent->Tags->Primary,
livingAgent->Tags->Secondary,
playerAgent->Pos.X,
playerAgent->Pos.Y
@@ -386,18 +382,17 @@ public sealed class MainPlayerService : IDisposable
var currentMapInfo = this.instanceContextService.GetAreaInfo();
var serverRegion = this.instanceContextService.GetServerRegion();
if (gameContext.IsNull ||
gameContext.Pointer->CharContext is null ||
gameContext.Pointer->WorldContext is null ||
gameContext.Pointer->PartyContext is null ||
gameContext.Pointer->Character is null ||
gameContext.Pointer->World is null ||
gameContext.Pointer->Party is 0 ||
currentMapInfo.IsNull)
{
scopedLogger.LogError("Game context is not initialized");
return new InstanceInfo(0, 0, 0, 0, Shared.Models.Api.InstanceType.Loading, DistrictRegionInfo.Unknown, LanguageInfo.Unknown, CampaignInfo.Unknown, ContinentInfo.Unknown, RegionInfo.Unknown, DifficultyInfo.Unknown);
}
var charContext = *gameContext.Pointer->CharContext;
var worldContext = *gameContext.Pointer->WorldContext;
var partyContext = *gameContext.Pointer->PartyContext;
var charContext = *gameContext.Pointer->Character;
var worldContext = *gameContext.Pointer->World;
var mapInfo = *currentMapInfo.Pointer;
var mapId = charContext.MapId;
var language = charContext.Language;
@@ -406,7 +401,7 @@ public sealed class MainPlayerService : IDisposable
var foesToKill = worldContext.FoesToKill;
return new InstanceInfo(
MapId: charContext.MapId,
MapId: (uint)charContext.MapId,
DistrictNumber: charContext.DistrictNumber,
FoesKilled: worldContext.FoesKilled,
FoesToKill: worldContext.FoesToKill,
@@ -416,7 +411,7 @@ public sealed class MainPlayerService : IDisposable
Campaign: (CampaignInfo)mapInfo.Campaign,
Continent: (ContinentInfo)mapInfo.Continent,
Region: (RegionInfo)mapInfo.Region,
Difficulty: partyContext.Flags.HasFlag(PartyFlags.HardMode) ? DifficultyInfo.Hard : DifficultyInfo.Normal);
Difficulty: GWCA.GW.PartyMgr.GetIsPartyInHardMode() ? DifficultyInfo.Hard : DifficultyInfo.Normal);
}
}, cancellationToken);
}
@@ -435,27 +430,26 @@ public sealed class MainPlayerService : IDisposable
}
var gameContext = this.gameContextService.GetGameContext();
if (gameContext.IsNull || gameContext.Pointer->WorldContext is null)
if (gameContext.IsNull || gameContext.Pointer->World is null)
{
scopedLogger.LogError("Game context is not initialized");
return default;
}
var playerIndex = gameContext.Pointer->CharContext->PlayerNumber;
var players = gameContext.Pointer->WorldContext->Players;
if (playerIndex < 0 || playerIndex >= players.Size)
var playerIndex = gameContext.Pointer->Character->PlayerNumber;
var player = GWCA.GW.PlayerMgr.GetPlayerByID(playerIndex);
if (player is null)
{
scopedLogger.LogError("Failed to get player id. Got {playerNumber}", playerIndex);
scopedLogger.LogError("Failed to get player");
return default;
}
var player = gameContext.Pointer->WorldContext->Players[(int)playerIndex];
var currentTier = player.ActiveTitleTier;
TitleContext? currentTitle = default;
var currentTier = player->ActiveTitleTier;
Title? currentTitle = default;
int? id = -1;
for (var i = 0; i < gameContext.Pointer->WorldContext->Titles.Size; i++)
for (var i = 0; i < gameContext.Pointer->World->Titles.Value.Size; i++)
{
var title = gameContext.Pointer->WorldContext->Titles.Skip(i).FirstOrDefault();
var title = gameContext.Pointer->World->Titles.Value.Skip(i).FirstOrDefault();
if (title.CurrentTitleTierIndex == currentTier)
{
currentTitle = title;
@@ -470,15 +464,15 @@ public sealed class MainPlayerService : IDisposable
return default;
}
var titleTier = gameContext.Pointer->WorldContext->TitleTiers.Skip((int)currentTitle.Value.CurrentTitleTierIndex).FirstOrDefault();
var titleTier = gameContext.Pointer->World->TitleTiers.Skip((int)currentTitle.Value.CurrentTitleTierIndex).FirstOrDefault();
return new TitleInfo
(
Id: (uint)id,
IsPercentage: currentTitle.Value.Props.HasFlag(TitleProps.PercentageBased),
PointsForCurrentRank: currentTitle.Value.PointsNeededForCurrentRank,
IsPercentage: currentTitle.Value.IsPercentageBased(),
PointsForCurrentRank: currentTitle.Value.PointsNeededCurrentRank,
PointsForNextRank: titleTier.TierNumber == currentTitle.Value.MaxTitleRank ?
currentTitle.Value.CurrentPoints :
currentTitle.Value.PointsNeededForNextRank,
currentTitle.Value.PointsNeededNextRank,
CurrentPoints: currentTitle.Value.CurrentPoints,
TierNumber: titleTier.TierNumber,
MaxTierNumber: currentTitle.Value.MaxTitleTierIndex
@@ -502,8 +496,8 @@ public sealed class MainPlayerService : IDisposable
var gameContext = this.gameContextService.GetGameContext();
if (gameContext.IsNull ||
gameContext.Pointer->AccountContext is null ||
gameContext.Pointer->WorldContext is null)
gameContext.Pointer->Account is null ||
gameContext.Pointer->World is null)
{
this.logger.LogError("Game context is not initialized");
return default;
@@ -516,7 +510,7 @@ public sealed class MainPlayerService : IDisposable
return default;
}
var agentProfession = gameContext.Pointer->WorldContext->Professions.AsValueEnumerable().FirstOrDefault(p => p.AgentId == playerAgentId);
var agentProfession = gameContext.Pointer->World->PartyProfessionStates.AsValueEnumerable().FirstOrDefault(p => p.AgentId == playerAgentId);
if (agentProfession.AgentId != playerAgentId)
{
scopedLogger.LogError("Failed to find agent profession for player agent id {agentId}", playerAgentId);
@@ -524,25 +518,11 @@ public sealed class MainPlayerService : IDisposable
}
return new MainPlayerBuildContext(
PrimaryProfessionId: (uint)agentProfession.CurrentPrimary,
UnlockedProfessions: agentProfession.UnlockedProfessionsFlags,
UnlockedAccountSkills: gameContext.Pointer->AccountContext->UnlockedAccountSkills.AsValueEnumerable().ToArray(),
UnlockedCharacterSkills: gameContext.Pointer->WorldContext->UnlockedCharacterSkills.AsValueEnumerable().ToArray());
PrimaryProfessionId: (uint)agentProfession.Primary,
UnlockedProfessions: agentProfession.UnlockedProfessions,
UnlockedCharacterSkills: gameContext.Pointer->World->UnlockedCharacterSkills.AsValueEnumerable().ToArray());
}
}, cancellationToken);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private unsafe AgentContext* GetAgentContext(uint agentId)
{
var agentArray = this.agentContextService.GetAgentArray();
if (agentArray.IsNull || agentArray.Pointer->Buffer is null ||
agentArray.Pointer->Size <= agentId)
{
return null;
}
return agentArray.Pointer->Buffer[agentId].Pointer;
}
}
+52 -55
View File
@@ -1,4 +1,5 @@
using Daybreak.API.Extensions;
using Daybreak.API.Interop;
using Daybreak.API.Interop.GuildWars;
using Daybreak.API.Models;
using Daybreak.API.Services.Interop;
@@ -9,6 +10,7 @@ using Daybreak.Shared.Services.BuildTemplates;
using System.Core.Extensions;
using System.Extensions;
using System.Extensions.Core;
using System.Runtime.CompilerServices;
using ZLinq;
using InstanceType = Daybreak.API.Interop.GuildWars.InstanceType;
@@ -46,21 +48,21 @@ public sealed class PartyService(
if (!await this.IsInValidOutpost(cancellationToken))
{
scopedLogger.LogError("Could not set party loadout. Not in a valid outpost");
await this.chatService.AddMessageAsync("Cannot set party loadout. Not in a valid outpost.", "Daybreak.API", Channel.Moderator, cancellationToken);
await this.chatService.AddMessageAsync("Cannot set party loadout. Not in a valid outpost.", "Daybreak.API", Channel.CHANNEL_MODERATOR, cancellationToken);
return false;
}
if (!await this.KickAllHeroes(cancellationToken))
{
scopedLogger.LogError("Could not set party loadout. Could not leave party");
await this.chatService.AddMessageAsync("Cannot set party loadout. Could not leave party.", "Daybreak.API", Channel.Moderator, cancellationToken);
await this.chatService.AddMessageAsync("Cannot set party loadout. Could not leave party.", "Daybreak.API", Channel.CHANNEL_MODERATOR, cancellationToken);
return false;
}
if (!await this.gameThreadService.QueueOnGameThread(() => this.SpawnHeroes(partyLoadout), cancellationToken))
{
scopedLogger.LogError("Could not set party loadout. Could not spawn heroes");
await this.chatService.AddMessageAsync("Cannot set party loadout. Could not spawn heroes.", "Daybreak.API", Channel.Moderator, cancellationToken);
await this.chatService.AddMessageAsync("Cannot set party loadout. Could not spawn heroes.", "Daybreak.API", Channel.CHANNEL_MODERATOR, cancellationToken);
return false;
}
@@ -69,21 +71,21 @@ public sealed class PartyService(
if (!await this.gameThreadService.QueueOnGameThread(() => this.ApplyBuilds(partyLoadout), cancellationToken))
{
scopedLogger.LogError("Could not set party loadout. Could not apply builds");
await this.chatService.AddMessageAsync("Cannot set party loadout. Could not apply builds.", "Daybreak.API", Channel.Moderator, cancellationToken);
await this.chatService.AddMessageAsync("Cannot set party loadout. Could not apply builds.", "Daybreak.API", Channel.CHANNEL_MODERATOR, cancellationToken);
return false;
}
var heroBehaviorSetup = await this.gameThreadService.QueueOnGameThread(() => this.GetHeroBehaviorSetup(partyLoadout), cancellationToken);
foreach (var heroBehaviorEntry in heroBehaviorSetup ?? [])
foreach (var (AgentId, Behavior) in heroBehaviorSetup ?? [])
{
if (!await this.SetHeroBehavior(heroBehaviorEntry.AgentId, heroBehaviorEntry.Behavior, cancellationToken))
if (!await this.SetHeroBehavior(AgentId, Behavior, cancellationToken))
{
scopedLogger.LogWarning("Could not set hero behavior for agent {agentId} to {behavior}", heroBehaviorEntry.AgentId, heroBehaviorEntry.Behavior);
await this.chatService.AddMessageAsync($"Cannot set party loadout. Could not set behavior {heroBehaviorEntry.Behavior} for agent {heroBehaviorEntry.AgentId}.", "Daybreak.API", Channel.Moderator, cancellationToken);
scopedLogger.LogWarning("Could not set hero behavior for agent {agentId} to {behavior}", AgentId, Behavior);
await this.chatService.AddMessageAsync($"Cannot set party loadout. Could not set behavior {Behavior} for agent {AgentId}.", "Daybreak.API", Channel.CHANNEL_MODERATOR, cancellationToken);
}
}
await this.chatService.AddMessageAsync("Party loadout set.", "Daybreak.API", Channel.Moderator, cancellationToken);
await this.chatService.AddMessageAsync("Party loadout set.", "Daybreak.API", Channel.CHANNEL_MODERATOR, cancellationToken);
return true;
}
@@ -126,9 +128,9 @@ public sealed class PartyService(
var buildTuples = professions.Value.AsValueEnumerable()
.Select(p => (p.AgentId, p, attributes.Value.FirstOrDefault(a => a.AgentId == p.AgentId), skillbars.Value.FirstOrDefault(s => s.AgentId == p.AgentId), heroFlags.Value.FirstOrDefault(f => f.AgentId == p.AgentId)))
.Select(t => (t.AgentId, GetBuildEntryById(t.p, t.Item4, t.Item3), t.Item5.Behavior))
.Select(t => (t.AgentId, GetBuildEntryById(t.p, t.Item4, t.Item3), t.Item5.HeroBehavior))
.Where(t => t.Item2 is not null)
.OfType<(uint AgentId, BuildEntry BuildEntry, Behavior Behavior)>()
.OfType<(uint AgentId, BuildEntry BuildEntry, API.Interop.GWCA.GW.HeroBehavior Behavior)>()
.ToList();
return new PartyLoadout(
@@ -136,16 +138,17 @@ public sealed class PartyService(
{
if (t.AgentId == playerId)
{
return new PartyLoadoutEntry(0, HeroBehavior.Undefined, t.BuildEntry);
return new PartyLoadoutEntry(0, Shared.Models.Api.HeroBehavior.Undefined, t.BuildEntry);
}
else if (heroes.Value.FirstOrDefault(h => h.AgentId == t.AgentId) is HeroPartyMember hero &&
hero.AgentId == t.AgentId)
{
return new PartyLoadoutEntry((int)hero.HeroId, (HeroBehavior)t.Behavior, t.BuildEntry);
return new PartyLoadoutEntry((int)hero.HeroId, (Shared.Models.Api.HeroBehavior)t.Behavior, t.BuildEntry);
}
return default;
})
.Where(t => t is not null)
.OfType<PartyLoadoutEntry>()]);
}
}, cancellationToken);
@@ -179,7 +182,7 @@ public sealed class PartyService(
}, cancellationToken);
}
public async Task<bool> SetHeroBehavior(uint heroAgentId, HeroBehavior behavior, CancellationToken cancellationToken)
public async Task<bool> SetHeroBehavior(uint heroAgentId, Shared.Models.Api.HeroBehavior behavior, CancellationToken cancellationToken)
{
var scopedLogger = this.logger.CreateScopedLogger();
@@ -209,14 +212,14 @@ public sealed class PartyService(
unsafe
{
var btn = this.uIContextService.GetChildFrame(frame.Frame, (uint)behavior);
var packet = new UIPackets.MouseClick(UIPackets.MouseButtons.Left, 0, 0);
var packet = new kMouseClick { MouseButton = 0, IsDoubleclick = 0 };
if (btn.IsNull)
{
scopedLogger.LogError("Could not set hero behavior. Could not find button for behavior {behavior} in frame {frameLabel}", behavior, heroCommanderFrameLabel);
return false;
}
return this.uIContextService.SendFrameUIMessage(btn, UIMessage.MouseClick, &packet);
return this.uIContextService.SendFrameUIMessage(btn, UIMessage.kMouseClick, &packet);
}
}, cancellationToken);
@@ -305,7 +308,7 @@ public sealed class PartyService(
}
else
{
var hero = heroes.Value.AsValueEnumerable().FirstOrDefault(h => h.HeroId == entry.HeroId);
var hero = heroes.Value.AsValueEnumerable().FirstOrDefault(h => (int)h.HeroId == entry.HeroId);
return (entry, hero.AgentId);
}
})
@@ -317,7 +320,7 @@ public sealed class PartyService(
: accountContext.Pointer->UnlockedAccountSkills;
var unlockedProfessionsFlags = t.playerId == playerId
? playerProfessionContext.UnlockedProfessionsFlags
? playerProfessionContext.UnlockedProfessions
: uint.MaxValue;
if (!this.buildTemplateManager.CanTemplateApply(
@@ -325,7 +328,7 @@ public sealed class PartyService(
(uint)t.entry.Build.Primary,
(uint)t.entry.Build.Secondary,
[.. t.entry.Build.Skills],
(uint)t.Item3.CurrentPrimary,
(uint)t.Item3.Primary,
unlockedProfessionsFlags,
[.. validSkills])))
{
@@ -358,28 +361,29 @@ public sealed class PartyService(
continue;
}
var attributeIds = new Array12Uint();
var attributeIds = new AttributeArray12();
var attributeValues = new Array12Uint();
for (var i = 0; i < entry.Build.Attributes.Count && i < 12; i++)
{
attributeIds[i] = (uint)entry.Build.Attributes[i].Id;
attributeIds[i] = (GWCA.GW.Constants.Attribute)entry.Build.Attributes[i].Id;
attributeValues[i] = (uint)entry.Build.Attributes[i].BasePoints;
}
var skills = new Array8Uint();
var skills = new SkillIDArray8();
for (var i = 0; i < entry.Build.Skills.Count && i < 8; i++)
{
skills[i] = entry.Build.Skills[i];
skills[i] = (GWCA.GW.Constants.SkillID)entry.Build.Skills[i];
}
var skillTemplate = new SkillTemplate(
(uint)entry.Build.Primary,
(uint)entry.Build.Secondary,
(uint)entry.Build.Attributes.Count,
attributeIds,
attributeValues,
skills);
var skillTemplate = new SkillTemplate
{
Primary = (GWCA.GW.Constants.Profession)entry.Build.Primary,
Secondary = (GWCA.GW.Constants.Profession)entry.Build.Secondary,
AttributesCount = (uint)entry.Build.Attributes.Count,
AttributeIds = attributeIds,
Skills = skills
};
Unsafe.CopyBlock(skillTemplate.AttributeValues, Unsafe.AsPointer(ref attributeValues), (uint)(12 * sizeof(uint)));
scopedLogger.LogInformation("Applying build for agent {agentId} with primary {primary} and secondary {secondary}", agentId, entry.Build.Primary, entry.Build.Secondary);
this.skillbarContextService.LoadBuild(agentId, &skillTemplate);
}
@@ -387,7 +391,7 @@ public sealed class PartyService(
return true;
}
private unsafe List<(uint AgentId, HeroBehavior Behavior)>? GetHeroBehaviorSetup(PartyLoadout partyLoadout)
private unsafe List<(uint AgentId, Shared.Models.Api.HeroBehavior Behavior)>? GetHeroBehaviorSetup(PartyLoadout partyLoadout)
{
var scopedLogger = this.logger.CreateScopedLogger();
scopedLogger.LogDebug("Getting hero behavior setup");
@@ -402,12 +406,12 @@ public sealed class PartyService(
return heroes.Value.AsValueEnumerable()
.Select(h =>
{
if (h.HeroId is 0)
if (h.HeroId is API.Interop.GWCA.GW.Constants.HeroID.NoHero)
{
return default;
}
var entry = partyLoadout.Entries.AsValueEnumerable().FirstOrDefault(e => (uint)e.HeroId == h.HeroId);
var entry = partyLoadout.Entries.AsValueEnumerable().FirstOrDefault(e => e.HeroId == (int)h.HeroId);
if (entry is null)
{
scopedLogger.LogWarning("No entry found for hero {heroId}", h.HeroId);
@@ -455,21 +459,14 @@ public sealed class PartyService(
return -1;
}
var partyContext = gameContext.Pointer->PartyContext;
if (partyContext is null)
{
scopedLogger.LogError("Party context is null");
return -1;
}
var charContext = gameContext.Pointer->CharContext;
var charContext = gameContext.Pointer->Character;
if (charContext is null)
{
scopedLogger.LogError("Char context is null");
return -1;
}
var playerParty = partyContext->PlayerParty;
var playerParty = GWCA.GW.PartyMgr.GetPartyInfo(0);
var playerId = charContext->PlayerNumber;
var offset = 0;
foreach(var hero in playerParty->Heroes)
@@ -492,22 +489,22 @@ public sealed class PartyService(
return heroNumber is -1 ? default : (uint)heroNumber;
}
private static BuildEntry? GetBuildEntryById(ProfessionsContext professionContext, SkillbarContext skillbar, PartyAttribute attributes)
private static BuildEntry? GetBuildEntryById(ProfessionState professionContext, SkillbarData skillbar, PartyAttribute attributes)
{
return new BuildEntry(
Primary: (int)professionContext.CurrentPrimary,
Secondary: (int)professionContext.CurrentSecondary,
Attributes: attributes.Attributes.GetAttributeEntryList(),
Primary: (int)professionContext.Primary,
Secondary: (int)professionContext.Secondary,
Attributes: attributes.Attribute.GetAttributeEntryList().ToList(),
Skills:
[
skillbar.Skill0.Id,
skillbar.Skill1.Id,
skillbar.Skill2.Id,
skillbar.Skill3.Id,
skillbar.Skill4.Id,
skillbar.Skill5.Id,
skillbar.Skill6.Id,
skillbar.Skill7.Id
(uint)skillbar.Skills[0].SkillId,
(uint)skillbar.Skills[1].SkillId,
(uint)skillbar.Skills[2].SkillId,
(uint)skillbar.Skills[3].SkillId,
(uint)skillbar.Skills[4].SkillId,
(uint)skillbar.Skills[5].SkillId,
(uint)skillbar.Skills[6].SkillId,
(uint)skillbar.Skills[7].SkillId
]);
}
}
+11 -12
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.Extensions.Core;
@@ -20,7 +19,7 @@ public sealed class UIService(
var scopedLogger = this.logger.CreateScopedLogger();
var keyDownResult = await this.gameThreadService.QueueOnGameThread(() =>
{
if (!this.uIContextService.KeyDown(action, frame))
if (!this.uIContextService.KeyDown((GWCA.GW.UI.ControlAction)action, frame))
{
scopedLogger.LogError("Failed to send keydown action {action}", action);
return false;
@@ -36,7 +35,7 @@ public sealed class UIService(
var keyUpResult = await this.gameThreadService.QueueOnGameThread(() =>
{
if (!this.uIContextService.KeyUp(action, frame))
if (!this.uIContextService.KeyUp((GWCA.GW.UI.ControlAction)action, frame))
{
scopedLogger.LogError("Failed to send keyup action {action}", action);
return false;
@@ -86,7 +85,7 @@ public sealed class UIService(
});
}
public async Task<string?> DecodeString(string encoded, Language language, CancellationToken cancellationToken)
public async Task<string?> DecodeString(string encoded, GWCA.GW.Constants.Language language, CancellationToken cancellationToken)
{
// GWCA's AsyncDecodeStr handles language switching internally (sets language, decodes, restores)
return await this.uIContextService.AsyncDecodeStringAsync(encoded, language, cancellationToken);
@@ -96,14 +95,14 @@ public sealed class UIService(
{
return frameLabel switch
{
"AgentCommander0" => ControlAction.OpenHeroCommander1,
"AgentCommander1" => ControlAction.OpenHeroCommander2,
"AgentCommander2" => ControlAction.OpenHeroCommander3,
"AgentCommander3" => ControlAction.OpenHeroCommander4,
"AgentCommander4" => ControlAction.OpenHeroCommander5,
"AgentCommander5" => ControlAction.OpenHeroCommander6,
"AgentCommander6" => ControlAction.OpenHeroCommander7,
_ => ControlAction.None
"AgentCommander0" => ControlAction.ControlAction_OpenHeroCommander1,
"AgentCommander1" => ControlAction.ControlAction_OpenHeroCommander2,
"AgentCommander2" => ControlAction.ControlAction_OpenHeroCommander3,
"AgentCommander3" => ControlAction.ControlAction_OpenHeroCommander4,
"AgentCommander4" => ControlAction.ControlAction_OpenHeroCommander5,
"AgentCommander5" => ControlAction.ControlAction_OpenHeroCommander6,
"AgentCommander6" => ControlAction.ControlAction_OpenHeroCommander7,
_ => ControlAction.ControlAction_None
};
}
}
@@ -1,6 +1,6 @@
using Daybreak.API.Services.Interop;
using Daybreak.Shared.Services.BuildTemplates;
using System.Diagnostics.CodeAnalysis;
using Daybreak.Shared.Services.BuildTemplates.Parsers;
namespace Daybreak.API.Services;
@@ -8,6 +8,9 @@ public static class WebApplicationBuilderExtensions
{
public static WebApplicationBuilder WithDaybreakServices(this WebApplicationBuilder builder)
{
builder.Services.AddSingleton<ITemplateParser, LegacySkillTemplateParser>();
builder.Services.AddSingleton<ITemplateParser, SkillTemplateParser>();
builder.Services.AddSingleton<ITemplateParser, PartyLoadoutTemplateParser>();
builder.Services.AddSingleton<IBuildTemplateManager, BuildTemplateManager>();
builder.Services.AddSingleton<MemoryScanningService>();
builder.Services.AddSingleton<ChatService>();
@@ -1,8 +1,4 @@
using Microsoft.AspNetCore.Http.Json;
using Microsoft.AspNetCore.Routing.Constraints;
using Microsoft.Extensions.Options;
using Swashbuckle.AspNetCore.SwaggerGen;
using System.Text.Json;
using Microsoft.AspNetCore.Routing.Constraints;
namespace Daybreak.API.Swagger;
@@ -11,14 +7,6 @@ public static class WebApplicationBuilderExtensions
public static WebApplicationBuilder WithSwagger(this WebApplicationBuilder builder)
{
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen();
builder.Services.AddTransient<ISerializerDataContractResolver>(sp =>
{
var opts = sp.GetRequiredService<IOptions<JsonOptions>>().Value?.SerializerOptions
?? new JsonSerializerOptions(JsonSerializerDefaults.Web);
return new JsonSerializerDataContractResolver(opts);
});
builder.Services.Configure<RouteOptions>(static options =>
{
options.SetParameterPolicy<RegexInlineRouteConstraint>("regex");
@@ -1,11 +1,13 @@
namespace Daybreak.API.Swagger;
using Scalar.AspNetCore;
namespace Daybreak.API.Swagger;
public static class WebApplicationExtensions
{
public static WebApplication UseSwaggerWithUI(this WebApplication app)
{
app.UseSwagger();
app.UseSwaggerUI();
app.MapOpenApi();
app.MapScalarApiReference();
return app;
}
}
@@ -0,0 +1,40 @@
using System.Text.Json.Serialization;
using Daybreak.Shared.Attributes;
namespace Daybreak.Configuration.Options;
[OptionsName(Name = "Downloads")]
internal sealed class DownloadOptions
{
[JsonPropertyName(nameof(HeaderTimeout))]
[OptionName(
Name = "Header Timeout",
Description = "Amount of seconds Daybreak will wait for a download to respond before giving up on the attempt"
)]
[OptionRange<double>(MinValue = 5, MaxValue = 300)]
public double HeaderTimeout { get; set; } = 30;
[JsonPropertyName(nameof(ChunkTimeout))]
[OptionName(
Name = "Chunk Timeout",
Description = "Amount of seconds Daybreak will wait for the next chunk of a download before giving up on the attempt"
)]
[OptionRange<double>(MinValue = 5, MaxValue = 300)]
public double ChunkTimeout { get; set; } = 30;
[JsonPropertyName(nameof(Retries))]
[OptionName(
Name = "Retries",
Description = "Amount of times Daybreak will retry a failed download before reporting it as failed"
)]
[OptionRange<int>(MinValue = 0, MaxValue = 10)]
public int Retries { get; set; } = 3;
[JsonPropertyName(nameof(RetryDelay))]
[OptionName(
Name = "Retry Delay",
Description = "Amount of seconds Daybreak will wait before retrying a failed download"
)]
[OptionRange<double>(MinValue = 0, MaxValue = 60)]
public double RetryDelay { get; set; } = 2;
}
@@ -39,4 +39,33 @@ public sealed class FocusViewOptions
[JsonPropertyName(nameof(EnergyDisplay))]
[OptionName(Name = "Energy Display Mode", Description = "Sets how should the energy display show the information")]
public PointsDisplay EnergyDisplay { get; set; }
[JsonPropertyName(nameof(GridColumns))]
[OptionName(Name = "Layout Grid Columns", Description = "Number of columns in the focus view layout grid")]
[OptionIgnore]
public int GridColumns { get; set; } = DefaultGridColumns;
[JsonPropertyName(nameof(GridRows))]
[OptionName(Name = "Layout Grid Rows", Description = "Number of rows in the focus view layout grid")]
[OptionIgnore]
public int GridRows { get; set; } = DefaultGridRows;
[JsonPropertyName(nameof(Layout))]
[OptionName(Name = "Layout", Description = "Tile-based layout of the focus view components")]
public List<FocusViewTile> Layout { get; set; } = DefaultLayout();
public const int DefaultGridColumns = 20;
public const int DefaultGridRows = 16;
public static List<FocusViewTile> DefaultLayout() =>
[
new() { Component = FocusViewComponent.Character, Column = 1, Row = 1, ColumnSpan = 10, RowSpan = 3 },
new() { Component = FocusViewComponent.PlayerResources, Column = 1, Row = 4, ColumnSpan = 10, RowSpan = 3 },
new() { Component = FocusViewComponent.Vanquish, Column = 1, Row = 7, ColumnSpan = 10, RowSpan = 1 },
new() { Component = FocusViewComponent.Title, Column = 1, Row = 8, ColumnSpan = 10, RowSpan = 1 },
new() { Component = FocusViewComponent.CurrentMap, Column = 1, Row = 9, ColumnSpan = 10, RowSpan = 1 },
new() { Component = FocusViewComponent.QuestLog, Column = 1, Row = 10, ColumnSpan = 10, RowSpan = 7 },
new() { Component = FocusViewComponent.Build, Column = 11, Row = 1, ColumnSpan = 10, RowSpan = 5 },
new() { Component = FocusViewComponent.Browser, Column = 11, Row = 6, ColumnSpan = 10, RowSpan = 11 },
];
}
@@ -9,6 +9,7 @@ using Daybreak.Services.ApplicationArguments.ArgumentHandling;
using Daybreak.Services.ApplicationLauncher;
using Daybreak.Services.BuildTemplates;
using Daybreak.Services.Credentials;
using Daybreak.Services.Formatting;
using Daybreak.Services.DirectSong;
using Daybreak.Services.Downloads;
using Daybreak.Services.Events;
@@ -48,12 +49,13 @@ using Daybreak.Services.TradeChat.Models;
using Daybreak.Services.TradeChat.Notifications;
using Daybreak.Services.UMod;
using Daybreak.Services.Updater;
using Daybreak.Services.Wiki;
using Daybreak.Shared.Models.Plugins;
using Daybreak.Shared.Services.Api;
using Daybreak.Shared.Services.ApplicationArguments;
using Daybreak.Shared.Services.ApplicationLauncher;
using Daybreak.Shared.Services.BuildTemplates;
using Daybreak.Shared.Services.Formatting;
using Daybreak.Shared.Services.BuildTemplates.Parsers;
using Daybreak.Shared.Services.Credentials;
using Daybreak.Shared.Services.DirectSong;
using Daybreak.Shared.Services.Downloads;
@@ -81,7 +83,6 @@ using Daybreak.Shared.Services.Toolbox;
using Daybreak.Shared.Services.TradeChat;
using Daybreak.Shared.Services.UMod;
using Daybreak.Shared.Services.Updater;
using Daybreak.Shared.Services.Wiki;
using Daybreak.Services.Graph;
using Daybreak.Themes;
using Daybreak.Views;
@@ -127,7 +128,13 @@ public class ProjectConfiguration : PluginConfigurationBase
var resourceBuilder = ResourceBuilder
.CreateDefault()
.AddService("Daybreak", serviceVersion: CurrentVersion.ToString());
var attributes = new List<KeyValuePair<string, object>>();
var attributes = new List<KeyValuePair<string, object>>
{
// Mirror the fleet-wide `service` stream field (promtail sets it for
// container logs) so Daybreak shows up in Grafana Logs Drilldown's
// `service` picker, alongside the `service_name` set by AddService.
new("service", "Daybreak"),
};
#if DEBUG
attributes.Add(new KeyValuePair<string, object>("deployment.environment", "debug"));
#else
@@ -140,8 +147,11 @@ public class ProjectConfiguration : PluginConfigurationBase
});
services.AddSingleton<AppViewModel>();
services.AddSingleton<ViewManager>();
services.AddSingleton<ITemplateParser, LegacySkillTemplateParser>();
services.AddSingleton<ITemplateParser, SkillTemplateParser>();
services.AddSingleton<ITemplateParser, PartyLoadoutTemplateParser>();
services.AddSingleton<IMenuService, MenuService>();
services.AddSingleton<IMenuServiceInitializer, MenuService>(sp =>
sp.GetRequiredService<IMenuService>().Cast<MenuService>()
@@ -221,6 +231,7 @@ public class ProjectConfiguration : PluginConfigurationBase
services.AddScoped<IOnboardingService, OnboardingService>();
services.AddScoped<IExperienceCalculator, ExperienceCalculator>();
services.AddScoped<IAttributePointCalculator, AttributePointCalculator>();
services.AddScoped<ISkillNumberFormatter, SkillNumberFormatter>();
services.AddScoped<IDownloadService, DownloadService>();
services.AddScoped<IGuildWarsInstaller, IntegratedGuildwarsInstaller>();
services.AddScoped<IExceptionHandler, ExceptionHandler>();
@@ -244,14 +255,13 @@ public class ProjectConfiguration : PluginConfigurationBase
services.AddScoped<ILaunchConfigurationService, LaunchConfigurationService>();
services.AddScoped<IEventService, EventService>();
services.AddScoped<IApplicationArgumentService, ApplicationArgumentService>();
services.AddScoped<IWikiService, WikiService>();
services.AddScoped<IScreenshotService, ScreenshotService>();
services.AddHostedSingleton<IApplicationUpdater, ApplicationUpdater>();
services.AddHostedSingleton<IThemeManager, BlazorThemeInteropService>();
services.AddHostedSingleton<IConnectivityStatus, ConnectivityStatus>();
services.AddHostedSingleton<ITradeAlertingService, TradeAlertingService>();
services.AddHostedSingleton<IGuildWarsExecutableManager, GuildWarsExecutableManager>();
services.AddSingleton<IGuildWarsExecutableManager, GuildWarsExecutableManager>();
services.AddHostedSingleton<IEventNotifierService, EventNotifierService>();
services.AddHostedSingleton<IApiScanningService, ApiScanningService>();
services.AddHostedSingleton<GameScreenshotsTheme>();
@@ -339,6 +349,7 @@ public class ProjectConfiguration : PluginConfigurationBase
optionsProducer.RegisterOptions<SynchronizationOptions>();
optionsProducer.RegisterOptions<FocusViewOptions>();
optionsProducer.RegisterOptions<DaybreakApiOptions>();
optionsProducer.RegisterOptions<DownloadOptions>();
optionsProducer.RegisterOptions<ToolboxOptions>();
optionsProducer.RegisterOptions<UModOptions>();
@@ -379,8 +390,11 @@ public class ProjectConfiguration : PluginConfigurationBase
public override void RegisterMods(IModsProducer modsManager)
{
modsManager.RegisterMod<IGuildWarsVersionChecker, GuildWarsVersionChecker>();
modsManager.RegisterMod<IDaybreakApiService, DaybreakApiService>();
// Toolbox must be registered before Daybreak API so that Toolbox injects first
// and loads its gwca.dll. Daybreak API will then reuse the already-loaded gwca.dll
// instead of loading a second copy, avoiding duplicate native module loads.
modsManager.RegisterMod<IToolboxService, ToolboxService>();
modsManager.RegisterMod<IDaybreakApiService, DaybreakApiService>();
modsManager.RegisterMod<IUModService, UModService>();
modsManager.RegisterMod<IDirectSongService, DirectSongService>(singleton: true);
}
@@ -561,10 +575,6 @@ public class ProjectConfiguration : PluginConfigurationBase
.WithMessageHandler(SetupLoggingAndMetrics<ScopedApiContext>)
.WithDefaultRequestHeadersSetup(SetupDaybreakUserAgent)
.Build()
.RegisterHttpClient<WikiService>()
.WithMessageHandler(SetupLoggingAndMetrics<WikiService>)
.WithDefaultRequestHeadersSetup(SetupDaybreakUserAgent)
.Build()
.RegisterHttpClient<ReShadeService>()
.WithMessageHandler(SetupLoggingAndMetrics<ReShadeService>)
.WithDefaultRequestHeadersSetup(SetupDaybreakUserAgent)
+3 -2
View File
@@ -39,14 +39,15 @@
</Target>
<ItemGroup>
<PackageReference Include="Elastic.OpenTelemetry" />
<PackageReference Include="Microsoft.Extensions.FileProviders.Embedded" />
<PackageReference Include="Microsoft.FluentUI.AspNetCore.Components" />
<PackageReference Include="Microsoft.FluentUI.AspNetCore.Components.Icons" />
<PackageReference Include="Microsoft.AspNetCore.Components" />
<PackageReference Include="Microsoft.Identity.Client" />
<PackageReference Include="OpenTelemetry.Exporter.Console" />
<PackageReference Include="OpenTelemetry.Exporter.OpenTelemetryProtocol" />
<PackageReference Include="OpenTelemetry.Instrumentation.Http" />
<PackageReference Include="OpenTelemetry.Instrumentation.Process" />
<PackageReference Include="OpenTelemetry.Instrumentation.Runtime" />
<PackageReference Include="PeNet" />
<PackageReference Include="Plumsy" />
@@ -65,7 +66,7 @@
<PackageReference Include="Serilog.Sinks.Console" />
<PackageReference Include="Serilog.Sinks.Debug" />
<PackageReference Include="SharpCompress" />
<PackageReference Include="SixLabors.ImageSharp" />
<PackageReference Include="StbImageSharp" />
<PackageReference Include="System.Security.Cryptography.ProtectedData" />
</ItemGroup>
@@ -4,5 +4,14 @@ namespace Daybreak.Models;
public sealed class NotificationWrapper
{
public required Notification Notification { get; init; }
public DateTime StartTime { get; set; }
public DateTime ExpirationTime { get; set; }
/// <summary>
/// Incremented every time the notification lifetime is prolonged so the UI can
/// restart the expiration progress animation.
/// </summary>
public int LifetimeGeneration { get; set; }
public double LifetimeMilliseconds => Math.Max(0, (this.ExpirationTime - this.StartTime).TotalMilliseconds);
}

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