Compare commits

...
110 Commits
Author SHA1 Message Date
amacocian 446f5cffd8 Update wiki with plugin details (Closes #1298) (#1306) 2026-01-17 17:18:07 +01:00
amacocian 21b9cf1726 Setup embedded file provider (Closes #1298) (#1305) 2026-01-17 17:18:07 +01:00
PsidreFelixandamacocian 4f1398a8d1 Added backdrop image 'dervish.png' to wwwroot/img directory. (#1304)
Enabled new theme 'Dervish' in CoreThemes.cs
2026-01-17 17:18:07 +01:00
amacocian 072f00638c Fix title info API (Closes #1290) (#1303) 2026-01-17 17:18:07 +01:00
amacocian f7f22532bf Trade chat iframes allow clipboard access (Closes #1291) (#1302) 2026-01-17 17:18:07 +01:00
amacocian a565017ad2 Label merged issues (#1301) 2026-01-17 17:18:07 +01:00
amacocian 8b5f5c19e4 Simplify update process (Closes #1299) (#1300) 2026-01-17 17:18:07 +01:00
amacocian 6d260a927d Build component improvements (Closes #1292) (#1296)
* Improve build component UX

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

* Move everything to System.Text.Json

* Complete item parser

* Add some more energy + health mods

* Fix more mods

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

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

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

* Change index by frames

* Fixes

---------

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

* Create wiki view (Closes #1118)

---------

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

---
updated-dependencies:
- dependency-name: actions/setup-dotnet
  dependency-version: '5'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-12-09 14:24:06 +01:00
dependabot[bot]amacociandependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
7ee7be4d37 Bump actions/checkout from 4 to 6 (#1079)
Bumps [actions/checkout](https://github.com/actions/checkout) from 4 to 6.
- [Release notes](https://github.com/actions/checkout/releases)
- [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md)
- [Commits](https://github.com/actions/checkout/compare/v4...v6)

---
updated-dependencies:
- dependency-name: actions/checkout
  dependency-version: '6'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-12-09 14:24:06 +01:00
amacocian 3554249d80 Release 0.9.9.80 (#1102) 2025-12-09 14:24:06 +01:00
amacocian 5a6fca6fa7 Upgrade to .net 10 (#1089) 2025-12-08 22:39:48 +01:00
amacocian 8d0a53a812 Improve exception handling (Closes #1087) (#1088) 2025-12-08 22:39:48 +01:00
amacocian 794afa6c2c Blazor UI Migration (#1085)
* Migrate to Fluent UI

* Setup window events for Blazor WebView

* Setup basic FluentUI styling

* FluentUI links

* Setup views and viewmodel infra

* Remove old navigation logic

* Prepare an example of iframe embed

* Style window + navigation menu

* Setup page based navigation

* Add some UI error handling

* Introduce UI resiliency to exceptions

* Setup settings menu dropdown

* Basic setup for options views

* Setup reusable OptionBase

* Setup options views

* Basic styling

* Setup scoped css files.
Split App into components

* Hook option updates to OptionProvider

* Fix option dropdowns

* Setup multiple themes

* Make fonts scalable

* Move ViewModels under Views

* Launch view + styling

* Setup FocusView base

* Finish BuildListView implementation

* Base for build views

* Setup build routing

* Implement single build editor

* Implement skill description on hover

* Scrape skill information from the wiki and cache it locally

* Scrape and fix Guild Wars wiki info

* Improve mouse hover performance by querying position only when needed

* Skill snippet snaps to edges

* Setup accounts and executables view

* Launch configurations view

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

* Fix skilltype overflow

* Adjust navigation menu

* Setup privilege elevation flow

* Setup MSAL

* Options synchronization view

* Telemetry view

* Version management and update view

* Setup plugins view

* Setup notifications panel

* Base for logs view

* Finish LogView

* Setup metrics view

* Setup UI logging interop

* GuildWarsPartySearch view implementation

* Setup gitlab mirror job

* Eventviewer base

* Remove gitlab mirror

* Remove mirror pipeline

* Fix metricsview crashes

* Extract progress component

* Setup trade chat views

* Setup Download view

* Extract progress styling

* Setup guild wars copy view

* Setup trade alerts

* Fix team template view skill selection

* Setup basic onboarding view

* Overhaul asynchronous progress operations and remove redundant versioning code

* Setup modsview base

* Setup modsview with mod states

* Setup mod installation process

* Cleanup more ui migration
Remove price history #1076

* Screen selector manage view

* Upgrade to .net 10
Closes #1078

* gMod management view

* Trader message notification

* ReShade management

* Update for toolbox

* Setup focusview fetching

* Setup CharacterComponent

* Setup vanquishing component

* Setup TitleInformationComponent

* Closes #1082
Fix API project for Guild Wars Reforged

* Small fixes + attempts to get new exe version

* FileId fixes

* Current Map and Player Resources widgets

* Quest log widget

* BuildComponent implementation

* Closes #1075
Embed wwwroot into the executable

* Cleanup

* Fixes to slnx

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

* Patch pipelines
2025-12-08 22:39:48 +01:00
amacocian dcb50e9da3 Release 0.9.9.79 (#1055) 2025-12-08 22:39:48 +01:00
1103 changed files with 29725 additions and 42712 deletions
+10 -54
View File
@@ -24,22 +24,23 @@ jobs:
strategy:
matrix:
targetplatform: [x86]
targetplatform: [x64]
runs-on: windows-latest
env:
Configuration: Release
Solution_Path: Daybreak.sln
Solution_Path: Daybreak.slnx
Test_Project_Path: Daybreak.Tests\Daybreak.Tests.csproj
Wpf_Project_Path: Daybreak\Daybreak.csproj
Actions_Allow_Unsecure_Commands: true
steps:
- name: Checkout
uses: actions/checkout@v4
uses: actions/checkout@v6
with:
fetch-depth: 0
submodules: recursive
- name: Get Latest Tag
id: getLatestTag
@@ -56,9 +57,10 @@ jobs:
LatestReleaseTag: ${{steps.getLatestTag.outputs.tag}}
- name: Install .NET Core
uses: actions/setup-dotnet@v4
uses: actions/setup-dotnet@v5
with:
dotnet-version: '9.x'
dotnet-version: '10.x'
architecture: x64
- name: Setup MSBuild.exe
uses: microsoft/setup-msbuild@v2.0.0
@@ -71,9 +73,7 @@ jobs:
dotnet user-secrets --project Daybreak\Daybreak.csproj set ApmUri "${{ secrets.ApmUri }}"
- name: Restore project
run: msbuild $env:Solution_Path /t:Restore /p:Configuration=$env:Configuration /p:RuntimeIdentifier=$env:RuntimeIdentifier --property:SolutionDir=$GITHUB_WORKSPACE
env:
RuntimeIdentifier: win-${{ matrix.targetplatform }}
run: msbuild $env:Solution_Path /t:Restore /p:Configuration=$env:Configuration --property:SolutionDir=$GITHUB_WORKSPACE
- name: Build Daybreak project
run: dotnet build Daybreak -c $env:Configuration --property:SolutionDir=$env:GITHUB_WORKSPACE
@@ -84,9 +84,7 @@ jobs:
echo "::set-env name=Version::$version"
- name: Create publish launcher files
run: dotnet publish .\Daybreak\Daybreak.csproj -c $env:Configuration -r $env:RuntimeIdentifier --property:SolutionDir=$env:GITHUB_WORKSPACE -p:PublishReadyToRun=true -p:PublishSingleFile=false --self-contained true -o .\Publish
env:
RuntimeIdentifier: win-${{ matrix.targetplatform }}
run: dotnet publish .\Daybreak\Daybreak.csproj -c $env:Configuration --property:SolutionDir=$env:GITHUB_WORKSPACE -p:PublishReadyToRun=true -p:PublishSingleFile=false --self-contained true -o .\Publish
- name: Pack publish files
run: |
@@ -94,59 +92,17 @@ jobs:
.\Scripts\BuildRelease.ps1 -version $env:Version
shell: pwsh
- name: Submit virustotal scan
shell: pwsh
run: |
cd Publish
$apiKey = "${{ secrets.VIRUSTOTAL_APIKEY }}"
$archiveName = "daybreakv${{ env.Version }}.zip"
$uploadUrlResponse = curl https://www.virustotal.com/api/v3/files/upload_url -H x-apikey:"$apiKey"
$uploadUrlResponse
$uploadUrlJson = $uploadUrlResponse | ConvertFrom-Json
$uploadUrl = $uploadUrlJson.data
$uploadResponse = curl --request POST --header x-apikey:"$apiKey" --header 'content-type: multipart/form-data' --form file="@$archiveName" --url $uploadUrl
$uploadResponse
$uploadResponseJson = $uploadResponse | ConvertFrom-Json
$resultsUrl = "https://www.virustotal.com/api/v3/analyses/$($uploadResponseJson.data.id)"
$resultsUrl
# Poll the results until the status is "completed"
$status = ""
while ($status -ne "completed") {
Start-Sleep -Seconds 10 # Wait for 10 seconds between checks
$results = curl --header x-apikey:"$apiKey" --url $resultsUrl
$results
$resultsJson = $results | ConvertFrom-Json
$status = $resultsJson.data.attributes.status
Write-Host "Current status: $status"
}
$resultsJson.data.attributes.results | ConvertTo-Json | Out-File -FilePath "VirusTotal.Results.json" -Encoding UTF8
$resultsJson.data.attributes.stats | ConvertTo-Json | Out-File -FilePath "VirusTotal.Stats.json" -Encoding UTF8
$stats = $resultsJson.data.attributes.stats
echo "::set-env name=VirusTotalStats::$stats"
- name: Publish blob files
run: |
Write-Host $env
.\Scripts\PushFilesToBlobStorage.ps1 -version $env:Version -sourcePath .\Publish\ -connectionString "${{ secrets.BLOBSTORAGE_CONNECTIONSTRING }}"
shell: pwsh
- name: Create release draft
uses: Xotl/cool-github-releases@v1.1.10
with:
mode: update
tag_name: v${{ env.Version }}
release_name: Daybreak v${{ env.Version }}
assets: .\Publish\daybreakv${{ env.Version }}.zip;.\Publish\VirusTotal.Results.json;.\Publish\VirusTotal.Stats.json
assets: .\Publish\daybreakv${{ env.Version }}.zip
github_token: ${{ env.GITHUB_TOKEN }}
replace_assets: true
body_mrkdwn: |
${{ env.Changelog }}
** VirustTotal Stats: **
```
${{ env.VirusTotalStats }}
```
isDraft: true
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+9 -12
View File
@@ -6,9 +6,6 @@
name: Daybreak CI Pipeline
on:
push:
branches:
- master
pull_request:
branches:
- master
@@ -19,33 +16,33 @@ jobs:
strategy:
matrix:
targetplatform: [x86]
targetplatform: [x64]
runs-on: windows-latest
env:
Solution_Path: Daybreak.sln
Solution_Path: Daybreak.slnx
Test_Project_Path: Daybreak.Tests\Daybreak.Tests.csproj
Wpf_Project_Path: Daybreak\Daybreak.csproj
Actions_Allow_Unsecure_Commands: true
steps:
- name: Checkout
uses: actions/checkout@v4
uses: actions/checkout@v6
with:
fetch-depth: 0
submodules: recursive
- name: Install .NET Core
uses: actions/setup-dotnet@v4
uses: actions/setup-dotnet@v5
with:
dotnet-version: '9.x'
architecture: x86
dotnet-version: '10.x'
architecture: x64
- name: Setup MSBuild.exe
uses: microsoft/setup-msbuild@v2.0.0
- name: Restore the Wpf application to populate the obj folder
run: msbuild $env:Solution_Path /t:Restore /p:Configuration=$env:Configuration /p:RuntimeIdentifier=$env:RuntimeIdentifier --property:SolutionDir=$env:GITHUB_WORKSPACE
run: msbuild $env:Solution_Path /t:Restore /p:Configuration=$env:Configuration --property:SolutionDir=$env:GITHUB_WORKSPACE
env:
Configuration: Debug
RuntimeIdentifier: win-${{ matrix.targetplatform }}
Configuration: Debug
+4 -7
View File
@@ -12,10 +12,7 @@
name: "CodeQL"
on:
push:
branches: [ "master" ]
pull_request:
# The branches below must be a subset of the branches above
branches: [ "master" ]
schedule:
- cron: '41 12 * * 6'
@@ -38,14 +35,14 @@ jobs:
env:
Configuration: Release
Solution_Path: Daybreak.sln
Solution_Path: Daybreak.slnx
Test_Project_Path: Daybreak.Tests\Daybreak.Tests.csproj
Wpf_Project_Path: Daybreak\Daybreak.csproj
Actions_Allow_Unsecure_Commands: true
steps:
- name: Checkout
uses: actions/checkout@v4
uses: actions/checkout@v6
with:
fetch-depth: 0
@@ -66,9 +63,9 @@ jobs:
# If this step fails, then you should remove it and run the build manually (see below)
- name: Install .NET Core
uses: actions/setup-dotnet@v4
uses: actions/setup-dotnet@v5
with:
dotnet-version: '9.x'
dotnet-version: '10.x'
#- name: Autobuild
# uses: github/codeql-action/autobuild@v2
@@ -0,0 +1,47 @@
name: Label Merged Issues
on:
pull_request:
types: [closed]
branches:
- 'release/**'
jobs:
label-issues:
if: github.event.pull_request.merged == true
runs-on: ubuntu-latest
steps:
- name: Label linked issues as merged
uses: actions/github-script@v7
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
script: |
const prBody = context.payload.pull_request.body || '';
const prTitle = context.payload.pull_request.title || '';
// Match issue references like #123, fixes #123, closes #123, resolves #123
const issuePattern = /(?:close[sd]?|fix(?:e[sd])?|resolve[sd]?)?[\s:]*#(\d+)/gi;
const issueNumbers = new Set();
// Search in PR body and title
const textToSearch = `${prTitle} ${prBody}`;
let match;
while ((match = issuePattern.exec(textToSearch)) !== null) {
issueNumbers.add(parseInt(match[1]));
}
// Apply 'merged' label to each linked issue
for (const issueNumber of issueNumbers) {
try {
await github.rest.issues.addLabels({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: issueNumber,
labels: ['merged']
});
console.log(`Added 'merged' label to issue #${issueNumber}`);
} catch (error) {
console.log(`Failed to label issue #${issueNumber}: ${error.message}`);
}
}
+3 -3
View File
@@ -20,17 +20,17 @@ jobs:
env:
Configuration: Release
Solution_Path: Daybreak.sln
Solution_Path: Daybreak.slnx
Actions_Allow_Unsecure_Commands: true
steps:
- name: Checkout
uses: actions/checkout@v4
uses: actions/checkout@v6
with:
fetch-depth: 0
- name: Install .NET Core
uses: actions/setup-dotnet@v4
uses: actions/setup-dotnet@v5
with:
dotnet-version: '9.x'
+2
View File
@@ -365,3 +365,5 @@ MigrationBackup/
FodyWeavers.xsd
.idea
Daybreak/wwwroot/wiki/*
+3
View File
@@ -0,0 +1,3 @@
[submodule "Daybreak.wiki"]
path = Daybreak.wiki
url = https://github.com/gwdevhub/Daybreak.wiki.git
@@ -1,15 +0,0 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net9.0</TargetFramework>
<RootNamespace>Daybreak._7ZipExtractor</RootNamespace>
<ImplicitUsings>enable</ImplicitUsings>
<Platforms>x64</Platforms>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="SharpCompress" />
</ItemGroup>
</Project>
-47
View File
@@ -1,47 +0,0 @@
/*
* Extractor is in a different executable because of memory limitations. Daybreak needs to be compiled for x86 and as such has a memory limit of 2GB.
* 7Zip algorithm may require more than 2GB memory to unpack large zip files.
* As such, Daybreak.7ZipExtractor is x64.
*/
using SharpCompress.Archives;
if (args.Length != 2)
{
return -1;
}
var archivePath = args[0];
var destinationDirectory = args[1];
using var fileStream = new FileStream(archivePath, FileMode.Open);
using var archive = ArchiveFactory.Open(fileStream);
var progress = 0d;
var count = archive.Entries.Count();
var reader = archive.ExtractAllEntries();
while (reader.MoveToNextEntry())
{
var entry = reader.Entry;
if (entry.Key is null)
{
continue;
}
if (entry.IsDirectory)
{
var directoryName = Path.Combine(Path.GetFullPath(destinationDirectory), entry.Key);
Directory.CreateDirectory(directoryName);
continue;
}
var fileName = Path.Combine(Path.GetFullPath(destinationDirectory), entry.Key);
using var entryStream = new FileStream(fileName, FileMode.Create);
using (var stream = reader.OpenEntryStream())
{
stream.CopyTo(entryStream);
}
progress += 1d / count;
Console.WriteLine($"{progress} {fileName}");
}
return 0;
@@ -1,9 +1,9 @@
using Daybreak.API.Interop.GuildWars;
using System.Core.Extensions;
using Daybreak.API.Interop.GuildWars;
using Daybreak.API.Services;
using Daybreak.Shared.Models.Api;
using Microsoft.AspNetCore.Mvc;
using Net.Sdk.Web;
using System.Core.Extensions;
namespace Daybreak.API.Controllers.Rest;
@@ -0,0 +1,28 @@
using Daybreak.API.Services;
using Daybreak.Shared.Models.Api;
using Microsoft.AspNetCore.Mvc;
using Net.Sdk.Web;
namespace Daybreak.API.Controllers.Rest;
[GenerateController("api/v1/rest/inventory")]
[Tags("Inventory")]
public sealed class InventoryController(
InventoryService inventoryService)
{
private readonly InventoryService inventoryService = inventoryService;
[GenerateGet]
[EndpointName("GetInventory")]
[EndpointSummary("Get Inventory information")]
[EndpointDescription("Get the inventory of the current player. Returns a serialized InventoryInformation object")]
[ProducesResponseType<InventoryInformation>(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status503ServiceUnavailable)]
public async Task<IResult> GetInventory(CancellationToken cancellationToken)
{
var inventoryInformation = await this.inventoryService.GetInventoryInformation(cancellationToken);
return inventoryInformation is null ?
Results.StatusCode(StatusCodes.Status503ServiceUnavailable) :
Results.Ok(inventoryInformation);
}
}
@@ -1,8 +1,8 @@
using Daybreak.API.Services;
using System.Core.Extensions;
using Daybreak.API.Services;
using Daybreak.Shared.Models.Api;
using Microsoft.AspNetCore.Mvc;
using Net.Sdk.Web;
using System.Core.Extensions;
namespace Daybreak.API.Controllers.Rest;
@@ -1,8 +1,8 @@
using Daybreak.API.Services;
using System.Core.Extensions;
using Daybreak.API.Services;
using Daybreak.Shared.Models.Api;
using Microsoft.AspNetCore.Mvc;
using Net.Sdk.Web;
using System.Core.Extensions;
namespace Daybreak.API.Controllers.Rest;
@@ -1,11 +1,11 @@
using Daybreak.API.Models;
using Daybreak.API.Services;
using Daybreak.API.WebSockets;
using Daybreak.Shared.Models.Api;
using System.Buffers;
using System.Buffers;
using System.Core.Extensions;
using System.Extensions.Core;
using System.Net.WebSockets;
using Daybreak.API.Models;
using Daybreak.API.Services;
using Daybreak.API.WebSockets;
using Daybreak.Shared.Models.Api;
namespace Daybreak.API.Controllers.WebSocket.MainPlayer;
+15 -2
View File
@@ -1,7 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup>
<TargetFramework>net9.0-windows</TargetFramework>
<TargetFramework>$(TargetFrameworkVersionWindows)</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<OutputType>Library</OutputType>
@@ -18,8 +18,22 @@
<EnableIISSupport>false</EnableIISSupport>
<InvariantGlobalization>true</InvariantGlobalization>
<Platforms>x86</Platforms>
<!-- Prevent static web asset conflicts -->
<StaticWebAssetProjectMode>Default</StaticWebAssetProjectMode>
<GenerateStaticWebAssetsManifest>false</GenerateStaticWebAssetsManifest>
<ResolveStaticWebAssetsInputsDependsOn>RemoveDuplicateStaticWebAssets;$(ResolveStaticWebAssetsInputsDependsOn)</ResolveStaticWebAssetsInputsDependsOn>
</PropertyGroup>
<!-- Remove duplicate static web assets coming from referenced projects -->
<Target Name="RemoveDuplicateStaticWebAssets" BeforeTargets="ResolveStaticWebAssetsInputs;GenerateStaticWebAssetsManifest">
<ItemGroup>
<!-- Remove all blazor webview static assets since this is a Web API project, not a Blazor Hybrid app -->
<StaticWebAsset Remove="@(StaticWebAsset)"
Condition="$([System.String]::new('%(RelativePath)').Contains('blazor.modules')) Or $([System.String]::new('%(RelativePath)').Contains('blazor.webview'))" />
</ItemGroup>
</Target>
<PropertyGroup>
<!--Ignore trim warnings. NativeAOT support is still experimental so it should be tested with a Publish build generated from Daybreak build-->
<NoWarn>IL2104</NoWarn>
@@ -53,7 +67,6 @@
<PackageReference Include="Swashbuckle.AspNetCore.Swagger" />
<PackageReference Include="Swashbuckle.AspNetCore.SwaggerGen" />
<PackageReference Include="Swashbuckle.AspNetCore.SwaggerUI" />
<PackageReference Include="System.Private.Uri" />
<PackageReference Include="SystemExtensions.NetStandard.Generators" PrivateAssets="all" />
<PackageReference Include="ZLinq" />
</ItemGroup>
+31
View File
@@ -26,6 +26,8 @@ public class EntryPoint
[UnmanagedCallersOnly(EntryPoint = "ThreadInit"), STAThread]
[RequiresUnreferencedCode("The handler uses a static method that gets referenced, so there's no unreferenced code to worry about")]
[RequiresDynamicCode("The handler uses a static method, so there's no dynamic code to worry about")]
[SuppressMessage("Trimming", "IL2123:The use of 'RequiresUnreferencedCodeAttribute' on entry points is disallowed since the method will be called from outside the visible app.", Justification = "<Pending>")]
[SuppressMessage("AOT", "IL3057:The use of 'RequiresDynamicCodeAttribute' on entry points is disallowed since the method will be called from outside the visible app.", Justification = "<Pending>")]
public static int ThreadInit(IntPtr _, int __)
{
Environment.SetEnvironmentVariable("ASPNETCORE_HOSTINGSTARTUPASSEMBLIES", null, EnvironmentVariableTarget.Process);
@@ -85,8 +87,37 @@ public class EntryPoint
builder.Services.AddOpenApi();
// Add CORS policy - allow localhost and trusted community sites
builder.Services.AddCors(options =>
{
options.AddDefaultPolicy(policy =>
{
policy.SetIsOriginAllowed(origin =>
{
if (Uri.TryCreate(origin, UriKind.Absolute, out var uri))
{
// Always allow localhost
if (uri.Host is "localhost" or "127.0.0.1")
{
return true;
}
// Allow known Guild Wars community sites
return uri.Host.EndsWith("guildwars.app", StringComparison.OrdinalIgnoreCase)
|| uri.Host.EndsWith("gwmarket.net", StringComparison.OrdinalIgnoreCase);
}
return false;
})
.AllowAnyHeader()
.AllowAnyMethod()
.AllowCredentials();
});
});
var app = builder.Build();
app.UseCors();
app.UseWebSockets(new WebSocketOptions { KeepAliveInterval = TimeSpan.FromSeconds(30) });
return app
+16 -16
View File
@@ -155,52 +155,52 @@ public readonly unsafe struct AgentLivingContext
[FieldOffset(0x00F6)]
public readonly AgentModelType AgentModelType;
[FieldOffset(0x0104)]
[FieldOffset(0x0108)]
public readonly TagInfo* Tags;
[FieldOffset(0x010A)]
[FieldOffset(0x010E)]
public readonly byte Primary;
[FieldOffset(0x010B)]
[FieldOffset(0x010F)]
public readonly byte Secondary;
[FieldOffset(0x010C)]
[FieldOffset(0x0110)]
public readonly byte Level;
[FieldOffset(0x010D)]
[FieldOffset(0x0111)]
public readonly byte TeamId;
[FieldOffset(0x0114)]
[FieldOffset(0x0118)]
public readonly float EnergyRegen;
[FieldOffset(0x011C)]
[FieldOffset(0x0120)]
public readonly float Energy;
[FieldOffset(0x0120)]
[FieldOffset(0x0124)]
public readonly uint MaxEnergy;
[FieldOffset(0x0128)]
[FieldOffset(0x012C)]
public readonly float HealthRegen;
[FieldOffset(0x0130)]
[FieldOffset(0x0134)]
public readonly float Health;
[FieldOffset(0x0134)]
[FieldOffset(0x0138)]
public readonly uint MaxHealth;
[FieldOffset(0x0138)]
[FieldOffset(0x013C)]
public readonly uint Effects;
[FieldOffset(0x0154)]
[FieldOffset(0x0158)]
public readonly LivingAgentModelState ModelState;
[FieldOffset(0x0158)]
[FieldOffset(0x015C)]
public readonly LivingAgentState State;
[FieldOffset(0x0180)]
[FieldOffset(0x0184)]
public readonly uint LoginNumber;
[FieldOffset(0x01B1)]
[FieldOffset(0x01B5)]
public readonly LivingAgentAllegiance Allegiance;
public readonly bool IsAlive => (!this.State.HasFlag(LivingAgentState.Dead)) && this.Health > 0f;
+30
View File
@@ -0,0 +1,30 @@
using System.Runtime.InteropServices;
namespace Daybreak.API.Interop.GuildWars;
public enum BagType
{
None,
Inventory,
Equipped,
NotCollected,
Storage,
MaterialsStorage
}
[StructLayout(LayoutKind.Explicit, Pack = 1)]
public readonly unsafe struct Bag
{
[FieldOffset(0x0000)]
public readonly BagType Type;
[FieldOffset(0x0004)]
public readonly uint Index;
[FieldOffset(0x000C)]
public readonly uint ContainerItem;
[FieldOffset(0x0010)]
public readonly uint ItemsCount;
[FieldOffset(0x0014)]
public readonly Bag* Bags;
[FieldOffset(0x0018)]
public readonly GuildWarsArray<WrappedPointer<Item>> Items;
}
+31 -10
View File
@@ -1,4 +1,5 @@
using Daybreak.API.Models;
using System.Extensions;
using System.Runtime.InteropServices;
namespace Daybreak.API.Interop.GuildWars;
@@ -19,25 +20,25 @@ public readonly struct Frame
[FieldOffset(0x0024)]
public readonly uint TemplateType;
[FieldOffset(0x00A0)]
[FieldOffset(0x00A8)]
public readonly GuildWarsArray<FrameInteractionCallback> FrameCallbacks;
[FieldOffset(0x00b0)]
[FieldOffset(0x00b8)]
public readonly uint ChildOffsetId;
[FieldOffset(0x00b4)]
[FieldOffset(0x00bc)]
public readonly uint FrameId;
[FieldOffset(0x0120)]
[FieldOffset(0x0128)]
public readonly FrameRelation Relation;
[FieldOffset(0x0184)]
public readonly uint Field91;
[FieldOffset(0x018C)]
public readonly uint FrameState;
public bool IsCreated => (this.Field91 & 0x4) != 0;
public bool IsHidden => (this.Field91 & 0x200) != 0;
public bool IsCreated => (this.FrameState & 0x4) != 0;
public bool IsHidden => (this.FrameState & 0x200) != 0;
public bool IsVisible => !this.IsHidden;
public bool IsDIsabled => (this.Field91 & 0x10) != 0;
public bool IsDIsabled => (this.FrameState & 0x10) != 0;
}
[StructLayout(LayoutKind.Explicit, Pack = 1, Size = 0x1C)]
@@ -50,7 +51,7 @@ public readonly unsafe struct FrameRelation
public readonly uint FrameHashId;
}
[StructLayout(LayoutKind.Sequential, Pack =1)]
[StructLayout(LayoutKind.Sequential, Pack = 1)]
public readonly unsafe struct InteractionMessage
{
public readonly uint FrameId;
@@ -66,3 +67,23 @@ public unsafe readonly struct FrameInteractionCallback
public readonly void* Callback; // UIInteractionCallback
public readonly void* UiCtl_Context;
}
[StructLayout(LayoutKind.Explicit, Pack = 1)]
public readonly unsafe struct CharSelectorContext
{
[FieldOffset(0x0000)]
public readonly uint Vtable;
[FieldOffset(0x0004)]
public readonly uint FrameId;
[FieldOffset(0x0008)]
public readonly GuildWarsArray<WrappedPointer<CharSelectorChar>> Chars;
}
[StructLayout(LayoutKind.Explicit, Pack = 1)]
public readonly struct CharSelectorChar
{
[FieldOffset(0x0020)]
public readonly Array20Char Name;
}
@@ -11,6 +11,9 @@ public readonly unsafe struct GameContext
[FieldOffset(0x0014)]
public readonly MapContext* MapContext;
[FieldOffset(0x0018)]
public readonly TextParserContext* TextParserContext;
[FieldOffset(0x0028)]
public readonly AccountGameContext* AccountContext;
@@ -20,6 +23,9 @@ public readonly unsafe struct GameContext
[FieldOffset(0x003C)]
public readonly GuildContext* GuildContext;
[FieldOffset(0x0040)]
public readonly ItemContext* ItemContext;
[FieldOffset(0x0044)]
public readonly CharContext* CharContext;
@@ -12,6 +12,16 @@ public readonly unsafe struct GuildWarsArray<T> : IEnumerable<T>
public readonly uint Size;
public readonly uint Param;
public T this[int index]
{
get
{
ArgumentOutOfRangeException.ThrowIfNegative(index);
ArgumentOutOfRangeException.ThrowIfGreaterThanOrEqual((uint)index, this.Size);
return this.Buffer[index];
}
}
public Enumerator GetEnumerator() => new(this.Buffer, this.Size);
IEnumerator<T> IEnumerable<T>.GetEnumerator() => this.GetEnumerator();
@@ -0,0 +1,58 @@
using System.Runtime.InteropServices;
namespace Daybreak.API.Interop.GuildWars;
[StructLayout(LayoutKind.Sequential, Pack = 1)]
public readonly unsafe struct Inventory
{
public readonly Bag* UnusedBag;
public readonly Bag* Backpack;
public readonly Bag* BeltPouch;
public readonly Bag* Bag1;
public readonly Bag* Bag2;
public readonly Bag* EquipmentPack;
public readonly Bag* MaterialStorage;
public readonly Bag* UnclaimedItems;
public readonly Bag* Storage1;
public readonly Bag* Storage2;
public readonly Bag* Storage3;
public readonly Bag* Storage4;
public readonly Bag* Storage5;
public readonly Bag* Storage6;
public readonly Bag* Storage7;
public readonly Bag* Storage8;
public readonly Bag* Storage9;
public readonly Bag* Storage10;
public readonly Bag* Storage11;
public readonly Bag* Storage12;
public readonly Bag* Storage13;
public readonly Bag* Storage14;
public readonly Bag* EquippedItems;
public readonly Item* Bundle;
public readonly uint StoragePanesUnlocked;
public readonly Bag*[] Bags => [
this.Backpack,
this.BeltPouch,
this.Bag1,
this.Bag2,
this.EquipmentPack,
this.MaterialStorage,
this.UnclaimedItems,
this.Storage1,
this.Storage2,
this.Storage3,
this.Storage4,
this.Storage5,
this.Storage6,
this.Storage7,
this.Storage8,
this.Storage9,
this.Storage10,
this.Storage11,
this.Storage12,
this.Storage13,
this.Storage14,
this.EquippedItems
];
}
+127
View File
@@ -0,0 +1,127 @@
using System.Runtime.InteropServices;
namespace Daybreak.API.Interop.GuildWars;
public enum ItemType : byte
{
Salvage,
Axe = 2,
Bag,
Boots,
Bow,
Bundle,
Chestpiece,
Rune_Mod,
Usable,
Dye,
Materials_Zcoins,
Offhand,
Gloves,
Hammer = 15,
Headpiece,
CC_Shards,
Key,
Leggings,
Gold_Coin,
Quest_Item,
Wand,
Shield = 24,
Staff = 26,
Sword,
Kit = 29,
Trophy,
Scroll,
Daggers,
Present,
Minipet,
Scythe,
Spear,
Storybook = 43,
Costume,
Costume_Headpiece,
Unknown = 0xff
};
[StructLayout(LayoutKind.Sequential, Pack = 1)]
public readonly struct ItemModifier(uint mod)
{
public static readonly ItemModifier Zero = new();
public readonly uint Mod = mod;
public uint Identifier => this.Mod >> 16;
public uint Arg1 => (this.Mod & 0x0000FF00) >> 8;
public uint Arg2 => this.Mod & 0x000000FF;
public uint Arg => this.Mod & 0x0000FFFF;
}
[StructLayout(LayoutKind.Explicit, Pack = 1)]
public readonly unsafe struct Item
{
[FieldOffset(0x0000)]
public readonly uint ItemId;
[FieldOffset(0x0004)]
public readonly uint AgentId;
[FieldOffset(0x0008)]
public readonly Bag* BagEquipped;
[FieldOffset(0x000C)]
public readonly Bag* Bag;
[FieldOffset(0x0010)]
public readonly ItemModifier* Modifiers;
[FieldOffset(0x0014)]
public readonly uint ModifiersCount;
[FieldOffset(0x001C)]
public readonly uint ModelFileId;
[FieldOffset(0x0020)]
public readonly ItemType Type;
[FieldOffset(0x0024)]
public readonly ushort Value;
[FieldOffset(0x0028)]
public readonly uint Interaction;
[FieldOffset(0x002C)]
public readonly uint ModelId;
[FieldOffset(0x0030)]
public readonly char* InfoString;
[FieldOffset(0x0034)]
public readonly char* NameEncoded;
[FieldOffset(0x0038)]
public readonly char* CompleteNameEncoded;
[FieldOffset(0x003C)]
public readonly char* SingleItemName;
[FieldOffset(0x0048)]
public readonly ushort ItemFormula;
[FieldOffset(0x004A)]
public readonly byte IsMaterialSalvageable;
[FieldOffset(0x004C)]
public readonly ushort Quantity;
[FieldOffset(0x004E)]
public readonly byte Equipped;
[FieldOffset(0x004F)]
public readonly byte Profession;
[FieldOffset(0x0050)]
public readonly uint Slot;
public readonly bool Stackable => (this.Interaction & 0x80000) != 0;
public readonly bool Inscribable => (this.Interaction & 0x08000000) != 0;
}
@@ -0,0 +1,16 @@
using System.Runtime.InteropServices;
namespace Daybreak.API.Interop.GuildWars;
[StructLayout(LayoutKind.Explicit, Pack = 1)]
public readonly struct ItemContext
{
[FieldOffset(0x0024)]
public readonly GuildWarsArray<WrappedPointer<Bag>> Bags;
[FieldOffset(0x00B8)]
public readonly GuildWarsArray<WrappedPointer<Item>> Items;
[FieldOffset(0x00F8)]
public readonly WrappedPointer<Inventory> Inventory;
}
@@ -0,0 +1,20 @@
using System.Runtime.InteropServices;
namespace Daybreak.API.Interop.GuildWars;
[StructLayout(LayoutKind.Explicit, Pack = 1, Size = 0x34)]
public unsafe struct PartySearchWindowContext
{
[FieldOffset(0x0000)]
public uint* Vtable;
[FieldOffset(0x0004)]
public uint FrameId;
[FieldOffset(0x0020)]
public uint SelectedPartySearchId;
[FieldOffset(0x0024)]
public uint SelectedHeroId;
[FieldOffset(0x0028)]
public uint SelectedHenchmanId;
[FieldOffset(0x002C)]
public uint CurrentTab;
}
@@ -9,7 +9,7 @@ public enum PlayerContextFlags : uint
PvP = 0x800
}
[StructLayout(LayoutKind.Explicit, Pack = 1, Size = 0x4C)]
[StructLayout(LayoutKind.Explicit, Pack = 1, Size = 0x50)]
public readonly unsafe struct PlayerContext
{
[FieldOffset(0x0000)]
@@ -37,8 +37,11 @@ public readonly unsafe struct PlayerContext
public readonly uint ActiveTitleTier;
[FieldOffset(0x0034)]
public readonly uint PlayerNumber;
public readonly uint ReforgedOrDhuumsFlags;
[FieldOffset(0x0038)]
public readonly uint PlayerNumber;
[FieldOffset(0x003C)]
public readonly uint PartySize;
}
@@ -0,0 +1,10 @@
using System.Runtime.InteropServices;
namespace Daybreak.API.Interop.GuildWars;
[StructLayout(LayoutKind.Explicit, Pack = 1)]
public struct TextParserContext
{
[FieldOffset(0x01D0)]
public Language Language;
}
+9 -1
View File
@@ -11,16 +11,24 @@ public enum TitleProps : uint
// Note: The HasTiers check uses (props & 3) == 2, which means bit 1 is set but bit 0 is not
}
[StructLayout(LayoutKind.Sequential, Pack = 1, Size = 40)]
[StructLayout(LayoutKind.Explicit, Pack = 1, Size = 0x2C)]
public readonly struct TitleContext
{
[FieldOffset(0x0000)]
public readonly TitleProps Props;
[FieldOffset(0x0004)]
public readonly uint CurrentPoints;
[FieldOffset(0x0008)]
public readonly uint CurrentTitleTierIndex;
[FieldOffset(0x000C)]
public readonly uint PointsNeededForCurrentRank;
[FieldOffset(0x0014)]
public readonly uint NextTitleTierIndex;
[FieldOffset(0x0018)]
public readonly uint PointsNeededForNextRank;
[FieldOffset(0x001C)]
public readonly uint MaxTitleRank;
[FieldOffset(0x0020)]
public readonly uint MaxTitleTierIndex;
}
@@ -21,7 +21,12 @@ public static class WebApplicationBuilderExtensions
theme: AnsiConsoleTheme.Sixteen)
.WriteTo.File(
outputTemplate: "[{Timestamp:yyyy-MM-dd HH:mm:ss}] {Level:u4}: [{SourceContext}] [{CorrelationVector}] {NewLine}{Message:lj}{NewLine}{Exception}",
path: "Daybreak.API.log")
path: "Daybreak.API.log",
rollingInterval: RollingInterval.Day,
rollOnFileSizeLimit: true,
fileSizeLimitBytes: 50 * 1024 * 1024,
retainedFileCountLimit: 1,
shared: true)
.CreateLogger());
builder.Services.AddScoped<LoggingEnrichmentMiddleware>();
builder.Services.AddOptions<CorrelationVectorOptions>()
+2 -2
View File
@@ -7,12 +7,12 @@ public sealed class ByteConsumerEntry(
{
private readonly Action<ReadOnlySpan<byte>> handler = handler.ThrowIfNull();
private DateTimeOffset lastConsume = DateTimeOffset.MinValue;
private DateTime lastConsume = DateTime.MinValue;
public Guid Id { get; } = id;
public TimeSpan Frequency { get; } = freq;
public void TryConsume(DateTimeOffset currentTime, ReadOnlySpan<byte> value)
public void TryConsume(DateTime currentTime, ReadOnlySpan<byte> value)
{
if (currentTime - this.lastConsume >= this.Frequency)
{
@@ -0,0 +1,9 @@
using Daybreak.API.Interop.GuildWars;
namespace Daybreak.API.Models;
public struct DecodeStringContext(WrappedPointer<char> encodedString, TaskCompletionSource<string?> taskCompletionSource)
{
public WrappedPointer<char> EncodedString = encodedString;
public TaskCompletionSource<string?> TaskCompletionSource = taskCompletionSource;
}
+11 -147
View File
@@ -3,152 +3,16 @@
public enum UIMessage : uint
{
None = 0x0,
Resize = 0x8,
InitFrame = 0x9,
DestroyFrame = 0xb,
KeyDown = 0x1e, // wparam = UIPacket::kKeyAction*
KeyUp = 0x20, // wparam = UIPacket::kKeyAction*
MouseClick = 0x22, // wparam = UIPacket::kMouseClick*
MouseClick2 = 0x2e, // wparam = UIPacket::kMouseAction*
MouseAction = 0x2f, // wparam = UIPacket::kMouseAction*
SetLayout = 0x33,
FrameMessage_0x47 = 0x47, // Multiple uses depending on frame
UpdateAgentEffects = 0x10000000 | 0x9,
RerenderAgentModel = 0x10000000 | 0x7, // wparam = uint32_t agent_id
AgentSpeechBubble = 0x10000000 | 0x17,
ShowAgentNameTag = 0x10000000 | 0x19, // wparam = AgentNameTagInfo*
HideAgentNameTag = 0x10000000 | 0x1A,
SetAgentNameTagAttribs = 0x10000000 | 0x1B, // wparam = AgentNameTagInfo*
ChangeTarget = 0x10000000 | 0x20, // wparam = UIPacket::kChangeTarget*
AgentStartCasting = 0x10000000 | 0x27, // wparam = UIPacket::kAgentStartCasting*
ShowMapEntryMessage = 0x10000000 | 0x29, // wparam = { wchar_t* title, wchar_t* subtitle }
SetCurrentPlayerData = 0x10000000 | 0x2A, // fired after setting the worldcontext player name
PostProcessingEffect = 0x10000000 | 0x34, // Triggered when drunk. wparam = UIPacket::kPostProcessingEffect
HeroAgentAdded = 0x10000000 | 0x38, // hero assigned to agent/inventory/ai mode
HeroDataAdded = 0x10000000 | 0x39, // hero info received from server (name, level etc)
ShowXunlaiChest = 0x10000000 | 0x40,
MinionCountUpdated = 0x10000000 | 0x46,
MoraleChange = 0x10000000 | 0x47, // wparam = {agent id, morale percent }
LoginStateChanged = 0x10000000 | 0x50, // wparam = {bool is_logged_in, bool unk }
EffectAdd = 0x10000000 | 0x55, // wparam = {agent_id, GW::Effect*}
EffectRenew = 0x10000000 | 0x56, // wparam = GW::Effect*
EffectRemove = 0x10000000 | 0x57, // wparam = effect id
SkillActivated = 0x10000000 | 0x5b, // wparam ={ uint32_t agent_id , uint32_t skill_id }
UpdateSkillbar = 0x10000000 | 0x5E, // wparam ={ uint32_t agent_id , ... }
UpdateSkillsAvailable = 0x10000000 | 0x5f, // Triggered on a skill unlock, profession change or map load
TitleProgressUpdated = 0x10000000 | 0x65, // wparam = title_id
ExperienceGained = 0x10000000 | 0x66, // wparam = experience amount
WriteToChatLog = 0x10000000 | 0x7E, // wparam = UIPacket::kWriteToChatLog*. Triggered by the game when it wants to add a new message to chat.
WriteToChatLogWithSender = 0x10000000 | 0x7f, // wparam = UIPacket::kWriteToChatLogWithSender*. Triggered by the game when it wants to add a new message to chat.
AllyOrGuildMessage = 0x10000000 | 0x80, // wparam = UIPacket::kAllyOrGuildMessage*
PlayerChatMessage = 0x10000000 | 0x81, // wparam = UIPacket::kPlayerChatMessage*
FloatingWindowMoved = 0x10000000 | 0x83, // wparam = frame_id
FriendUpdated = 0x10000000 | 0x89, // wparam = { GW::Friend*, ... }
MapLoaded = 0x10000000 | 0x8A,
OpenWhisper = 0x10000000 | 0x90, // wparam = wchar* name
Logout = 0x10000000 | 0x9b, // wparam = { bool unknown, bool character_select }
CompassDraw = 0x10000000 | 0x9c, // wparam = UIPacket::kCompassDraw*
OnScreenMessage = 0x10000000 | 0xA0, // wparam = wchar_** encoded_string
DialogBody = 0x10000000 | 0xA4, // wparam = DialogBodyInfo*
DialogButton = 0x10000000 | 0xA1, // wparam = DialogButtonInfo*
TargetNPCPartyMember = 0x10000000 | 0xB1, // wparam = { uint32_t unk, uint32_t agent_id }
TargetPlayerPartyMember = 0x10000000 | 0xB2, // wparam = { uint32_t unk, uint32_t player_number }
VendorWindow = 0x10000000 | 0xB3, // wparam = UIPacket::kVendorWindow
VendorItems = 0x10000000 | 0xB7, // wparam = UIPacket::kVendorItems
VendorTransComplete = 0x10000000 | 0xB9, // wparam = *TransactionType
VendorQuote = 0x10000000 | 0xBB, // wparam = UIPacket::kVendorQuote
StartMapLoad = 0x10000000 | 0xC0, // wparam = { uint32_t map_id, ...}
WorldMapUpdated = 0x10000000 | 0xC5, // Triggered when an area in the world map has been discovered/updated
GuildMemberUpdated = 0x10000000 | 0xD8, // wparam = { GuildPlayer::name_ptr }
ShowHint = 0x10000000 | 0xDF, // wparam = { uint32_t icon_type, wchar_t* message_enc }
WeaponSetSwapComplete = 0x10000000 | 0xE7, // wparam = UIPacket::kWeaponSwap*
WeaponSetSwapCancel = 0x10000000 | 0xE8,
WeaponSetUpdated = 0x10000000 | 0xE9,
UpdateGoldCharacter = 0x10000000 | 0xEA, // wparam = { uint32_t unk, uint32_t gold_character }
UpdateGoldStorage = 0x10000000 | 0xEB, // wparam = { uint32_t unk, uint32_t gold_storage }
InventorySlotUpdated = 0x10000000 | 0xEC, // undocumented. Triggered when an item is moved into a slot
EquipmentSlotUpdated = 0x10000000 | 0xED, // undocumented. Triggered when an item is moved into a slot
InventorySlotCleared = 0x10000000 | 0xEF, // undocumented. Triggered when an item has been removed from a slot
EquipmentSlotCleared = 0x10000000 | 0xF0, // undocumented. Triggered when an item has been removed from a slot
PvPWindowContent = 0x10000000 | 0xF8,
PreStartSalvage = 0x10000000 | 0x100, // { uint32_t item_id, uint32_t kit_id }
TradePlayerUpdated = 0x10000000 | 0x103, // wparam = GW::TraderPlayer*
ItemUpdated = 0x10000000 | 0x104, // wparam = UIPacket::kItemUpdated*
MapChange = 0x10000000 | 0x10F, // wparam = map id
CalledTargetChange = 0x10000000 | 0x113, // wparam = { player_number, target_id }
ErrorMessage = 0x10000000 | 0x117, // wparam = { int error_index, wchar_t* error_encoded_string }
PartyHardModeChanged = 0x10000000 | 0x118, // wparam = { int is_hard_mode }
PartyAddHenchman = 0x10000000 | 0x119,
PartyRemoveHenchman = 0x10000000 | 0x11a,
PartyAddHero = 0x10000000 | 0x11c,
PartyRemoveHero = 0x10000000 | 0x11d,
PartyAddPlayer = 0x10000000 | 0x122,
PartyRemovePlayer = 0x10000000 | 0x124,
DisableEnterMissionBtn = 0x10000000 | 0x128, // wparam = boolean (1 = disabled, 0 = enabled)
ShowCancelEnterMissionBtn = 0x10000000 | 0x12b,
PartyDefeated = 0x10000000 | 0x12d,
PartySearchInviteReceived = 0x10000000 | 0x135, // wparam = UIPacket::kPartySearchInviteReceived*
PartySearchInviteSent = 0x10000000 | 0x137,
PartyShowConfirmDialog = 0x10000000 | 0x138, // wparam = UIPacket::kPartyShowConfirmDialog
PreferenceEnumChanged = 0x10000000 | 0x13E, // wparam = UiPacket::kPreferenceEnumChanged
PreferenceFlagChanged = 0x10000000 | 0x13F, // wparam = UiPacket::kPreferenceFlagChanged
PreferenceValueChanged = 0x10000000 | 0x140, // wparam = UiPacket::kPreferenceValueChanged
UIPositionChanged = 0x10000000 | 0x141, // wparam = UIPacket::kUIPositionChanged
QuestAdded = 0x10000000 | 0x149, // wparam = { quest_id, ... }
QuestDetailsChanged = 0x10000000 | 0x14A, // wparam = { quest_id, ... }
ClientActiveQuestChanged = 0x10000000 | 0x14C, // wparam = { quest_id, ... }. Triggered when the game requests the current quest to change
ServerActiveQuestChanged = 0x10000000 | 0x14E, // wparam = UIPacket::kServerActiveQuestChanged*. Triggered when the server requests the current quest to change
UnknownQuestRelated = 0x10000000 | 0x14F,
DungeonComplete = 0x10000000 | 0x151, // undocumented
MissionComplete = 0x10000000 | 0x152, // undocumented
VanquishComplete = 0x10000000 | 0x154, // undocumented
ObjectiveAdd = 0x10000000 | 0x155, // wparam = UIPacket::kObjectiveAdd*
ObjectiveComplete = 0x10000000 | 0x156, // wparam = UIPacket::kObjectiveComplete*
ObjectiveUpdated = 0x10000000 | 0x157, // wparam = UIPacket::kObjectiveUpdated*
TradeSessionStart = 0x10000000 | 0x160, // wparam = { trade_state, player_number }
TradeSessionUpdated = 0x10000000 | 0x166, // no args
TriggerLogoutPrompt = 0x10000000 | 0x16C, // no args
ToggleOptionsWindow = 0x10000000 | 0x16D, // no args
CheckUIState = 0x10000000 | 0x170, // Undocumented
RedrawItem = 0x10000000 | 0x172, // wparam = uint32_t item_id
CloseSettings = 0x10000000 | 0x174, // Undocumented
ChangeSettingsTab = 0x10000000 | 0x175, // wparam = uint32_t is_interface_tab
GuildHall = 0x10000000 | 0x177, // wparam = gh key (uint32_t[4])
LeaveGuildHall = 0x10000000 | 0x179,
Travel = 0x10000000 | 0x17A,
OpenWikiUrl = 0x10000000 | 0x17B, // wparam = char* url
AppendMessageToChat = 0x10000000 | 0x189, // wparam = wchar_t* message
HideHeroPanel = 0x10000000 | 0x197, // wparam = hero_id
ShowHeroPanel = 0x10000000 | 0x198, // wparam = hero_id
GetInventoryAgentId = 0x10000000 | 0x19c, // wparam = 0, lparam = uint32_t* agent_id_out. Used to fetch which agent is selected
EquipItem = 0x10000000 | 0x19d, // wparam = { item_id, agent_id }
MoveItem = 0x10000000 | 0x19e, // wparam = { item_id, to_bag, to_slot, bool prompt }
InitiateTrade = 0x10000000 | 0x1A0,
InventoryAgentChanged = 0x10000000 | 0x1b0, // Triggered when inventory needs updating due to agent change; no args
OpenTemplate = 0x10000000 | 0x1B9, // wparam = GW::UI::ChatTemplate*
// GWCA Client to Server commands. Only added the ones that are used for hooks, everything else goes straight into GW
SendLoadSkillTemplate = 0x30000000 | 0x3, // wparam = SkillbarMgr::SkillTemplate*
SendPingWeaponSet = 0x30000000 | 0x4, // wparam = UIPacket::kSendPingWeaponSet*
SendMoveItem = 0x30000000 | 0x5, // wparam = UIPacket::kSendMoveItem*
SendMerchantRequestQuote = 0x30000000 | 0x6, // wparam = UIPacket::kSendMerchantRequestQuote*
SendMerchantTransactItem = 0x30000000 | 0x7, // wparam = UIPacket::kSendMerchantTransactItem*
SendUseItem = 0x30000000 | 0x8, // wparam = UIPacket::kSendUseItem*
SendSetActiveQuest = 0x30000000 | 0x9, // wparam = uint32_t quest_id
SendAbandonQuest = 0x30000000 | 0xA, // wparam = uint32_t quest_id
SendChangeTarget = 0x30000000 | 0xB, // wparam = UIPacket::kSendChangeTarget* // e.g. tell the gw client to focus on a different target
SendCallTarget = 0x30000000 | 0x13, // wparam = { uint32_t call_type, uint32_t agent_id } // also used to broadcast morale, death penalty, "I'm following X", etc
SendDialog = 0x30000000 | 0x16, // wparam = dialog_id // internal use
StartWhisper = 0x30000000 | 0x17, // wparam = UIPacket::kStartWhisper*
GetSenderColor = 0x30000000 | 0x18, // wparam = UIPacket::kGetColor* // Get chat sender color depending on channel, output object passed by reference
GetMessageColor = 0x30000000 | 0x19, // wparam = UIPacket::kGetColor* // Get chat message color depending on channel, output object passed by reference
SendChatMessage = 0x30000000 | 0x1B, // wparam = UIPacket::kSendChatMessage*
LogChatMessage = 0x30000000 | 0x1D, // wparam = UIPacket::kLogChatMessage*. Triggered when a message wants to be added to the persistent chat log.
RecvWhisper = 0x30000000 | 0x1E, // wparam = UIPacket::kRecvWhisper*
PrintChatMessage = 0x30000000 | 0x1F, // wparam = UIPacket::kPrintChatMessage*. Triggered when a message wants to be added to the in-game chat window.
SendWorldAction = 0x30000000 | 0x20, // wparam = UIPacket::kSendWorldAction*
SetRendererValue = 0x30000000 | 0x21 // wparam = UIPacket::kSetRendererValue
};
KeyDown = 0x20, // wparam = UIPacket::kKeyAction*
KeyUp = 0x22, // wparam = UIPacket::kKeyAction*
MouseClick = 0x24, // wparam = UIPacket::kMouseClick*
MouseClick2 = 0x31, // wparam = UIPacket::kMouseAction*
MouseAction = 0x32, // wparam = UIPacket::kMouseAction*
FrameMessage_QuerySelectedIndex = 0x58, // Used to query selected index in character selector
WriteToChatLog = 0x10000000 | 0x7F, // wparam = UIPacket::kWriteToChatLog*. Triggered by the game when it wants to add a new message to chat.
WriteToChatLogWithSender = 0x10000000 | 0x80, // wparam = UIPacket::kWriteToChatLogWithSender*. Triggered by the game when it wants to add a new message to chat.
CheckUIState = 0x10000000 | 0x118, // lparam = uint32_t* out state (2 = char select ready)
Logout = 0x10000000 | 0x9D, // wparam = { bool unknown, bool character_select }
}
+23 -3
View File
@@ -11,6 +11,16 @@ public static class UIPackets
Right = 0x2
}
public enum ActionState
{
MouseDown = 0x6,
MouseUp = 0x7,
MouseClick = 0x8,
MouseDoubleClick = 0x9,
DragRelease = 0xb,
KeyDown = 0xe
}
[StructLayout(LayoutKind.Sequential, Pack = 1)]
public readonly struct LogChatMessage(string message, Channel channel)
{
@@ -29,11 +39,11 @@ public static class UIPackets
}
[StructLayout(LayoutKind.Sequential, Pack = 1)]
public readonly struct KeyAction(uint key)
public readonly struct KeyAction(uint key, uint wParam = 0x4000, uint lParam = 0x0006)
{
public readonly uint Key = key;
public readonly uint WParam = 0x4000;
public readonly uint LParam = 0x0006;
public readonly uint WParam = wParam;
public readonly uint LParam = lParam;
}
[StructLayout(LayoutKind.Sequential, Pack = 1)]
@@ -43,4 +53,14 @@ public static class UIPackets
public readonly uint IsDoubleClick = isDoubleClick;
public readonly uint UnknownTypeScreenPos = unknownTypeScreenPos;
}
[StructLayout(LayoutKind.Sequential, Pack = 1)]
public readonly struct MouseAction(uint frameId, uint childOffsetId, ActionState currentState, nuint wparam = 0, nuint lparam = 0)
{
public readonly uint FrameId = frameId;
public readonly uint ChildOffsetId = childOffsetId;
public readonly ActionState CurrentState = currentState;
public readonly nuint WParam = wparam;
public readonly nuint LParam = lparam;
}
}
@@ -1,5 +1,6 @@
using Daybreak.API.Models;
using Daybreak.Shared.Models.Api;
using Daybreak.Shared.Models.Guildwars;
using System.Text.Json;
using System.Text.Json.Serialization;
@@ -34,6 +35,9 @@ namespace Daybreak.API.Serialization;
[JsonSerializable(typeof(TitleInfo))]
[JsonSerializable(typeof(LoginInfo))]
[JsonSerializable(typeof(MainPlayerBuildContext))]
[JsonSerializable(typeof(InventoryInformation))]
[JsonSerializable(typeof(ItemProperty))]
[JsonSourceGenerationOptions(UseStringEnumConverter = true)]
public partial class ApiJsonSerializerContext : JsonSerializerContext
{
}
+142 -121
View File
@@ -1,4 +1,6 @@
using Daybreak.API.Services.Interop;
using Daybreak.API.Interop.GuildWars;
using Daybreak.API.Models;
using Daybreak.API.Services.Interop;
using Daybreak.Shared.Models.Api;
using System.Core.Extensions;
using System.Extensions.Core;
@@ -53,7 +55,7 @@ public sealed class CharacterSelectService(
var currentUuid = gameContext.Pointer->CharContext->PlayerUuid.ToString();
var availableChars = new List<CharacterSelectEntry>((int)availableCharsContext.Pointer->Size);
foreach(var charContext in *availableCharsContext.Pointer)
foreach (var charContext in *availableCharsContext.Pointer)
{
var nameSpan = charContext.Name.AsSpan();
var name = new string(nameSpan[..nameSpan.IndexOf('\0')]);
@@ -68,8 +70,17 @@ public sealed class CharacterSelectService(
charContext.IsPvp));
}
var currentCharacter = availableChars.FirstOrDefault(c => c.Uuid == currentUuid);
return new CharacterSelectInformation(currentCharacter, availableChars);
// TODO: Reforged sometimes returns Zero UUID even when in-game, need to investigate why
if (currentUuid != Uuid.Zero.ToString())
{
var currentCharacter = availableChars.FirstOrDefault(c => c.Uuid == currentUuid);
return new CharacterSelectInformation(currentCharacter, availableChars);
}
else
{
var currentCharacter = availableChars.FirstOrDefault();
return new CharacterSelectInformation(currentCharacter, availableChars);
}
}
}, cancellationToken);
}
@@ -85,20 +96,10 @@ public sealed class CharacterSelectService(
await this.TriggerLogOut(cancellationToken);
var indexResult = await this.WaitForLoginScreenAndGetCurrentAndDesiredIndex(characterName, cancellationToken);
if (indexResult is null)
var selectResult = await this.WaitForCharSelectAndSelectCharacter(characterName, cancellationToken);
if (!selectResult)
{
scopedLogger.LogError("Failed to find index by name {name}", characterName);
return false;
}
var currentIndex = indexResult.Value.CurrentIndex;
var desiredIndex = indexResult.Value.DesiredIndex;
scopedLogger.LogInformation("Changing character to {name} with index {index}", characterName, desiredIndex);
var navigateResult = await this.NavigateToCharAndPlay(currentIndex, desiredIndex, cancellationToken);
if (!navigateResult)
{
scopedLogger.LogError("Failed to navigate to character {name} with index {index}", characterName, desiredIndex);
scopedLogger.LogError("Failed to select character {name}", characterName);
return false;
}
@@ -123,86 +124,138 @@ public sealed class CharacterSelectService(
await this.TriggerLogOut(cancellationToken);
var indexResult = await this.WaitForLoginScreenAndGetCurrentAndDesiredIndex(desiredCharName, cancellationToken);
if (indexResult is null)
var selectResult = await this.WaitForCharSelectAndSelectCharacter(desiredCharName, cancellationToken);
if (!selectResult)
{
scopedLogger.LogError("Resolved character by uuid {uuid} but failed to find index by name {name}", uuid, desiredCharName);
return false;
}
var currentIndex = indexResult.Value.CurrentIndex;
var desiredIndex = indexResult.Value.DesiredIndex;
scopedLogger.LogInformation("Changing character to {name} with index {index} and uuid {uuid}", desiredCharName, desiredIndex, uuid);
var navigateResult = await this.NavigateToCharAndPlay(currentIndex, desiredIndex, cancellationToken);
if (!navigateResult)
{
scopedLogger.LogError("Failed to navigate to character {name} with index {index} and uuid {uuid}", desiredCharName, desiredIndex, uuid);
scopedLogger.LogError("Failed to select character {name} with uuid {uuid}", desiredCharName, uuid);
return false;
}
return true;
}
private async Task<bool> ValidateState(CancellationToken cancellationToken)
private async Task<bool> SelectCharacterToPlay(string characterName, bool play, CancellationToken cancellationToken)
{
var scopedLogger = this.logger.CreateScopedLogger();
return await this.gameThreadService.QueueOnGameThread(() =>
{
return this.instanceContextService.GetInstanceType() is not API.Interop.GuildWars.InstanceType.Loading;
unsafe
{
var selectorFrame = this.GetCharSelectorFrame();
if (selectorFrame is null)
{
scopedLogger.LogError("Character selector frame not found");
return false;
}
var ctx = this.uiContextService.GetFrameContext<CharSelectorContext>(selectorFrame);
if (ctx.IsNull)
{
scopedLogger.LogError("Character selector context not found");
return false;
}
var panesFrame = this.uiContextService.GetChildFrame(selectorFrame, 0);
if (panesFrame.IsNull)
{
scopedLogger.LogError("Character panes frame not found");
return false;
}
// Get current selected index
var selectedIdx = 0;
this.uiContextService.SendFrameUIMessage(panesFrame, UIMessage.FrameMessage_QuerySelectedIndex, null, &selectedIdx);
var chosen = false;
for (uint i = 0; !chosen && i < ctx.Pointer->Chars.Size; i++)
{
var c = ctx.Pointer->Chars.Buffer[i];
var charNameSpan = c.Pointer->Name.AsSpan();
var nullIdx = charNameSpan.IndexOf('\0');
if (nullIdx <= 0)
{
continue;
}
var charName = new string(charNameSpan[..nullIdx]);
if (!charName.StartsWith(characterName, StringComparison.OrdinalIgnoreCase))
{
continue;
}
while (selectedIdx != i)
{
var keyAction = new UIPackets.KeyAction(0x1c);
this.uiContextService.SendFrameUIMessage(panesFrame, UIMessage.KeyDown, &keyAction);
var newIdx = selectedIdx;
this.uiContextService.SendFrameUIMessage(panesFrame, UIMessage.FrameMessage_QuerySelectedIndex, null, &newIdx);
if (newIdx == selectedIdx)
{
break; // This shouldn't happen - the character should have changed
}
selectedIdx = newIdx;
}
chosen = selectedIdx == i;
break;
}
if (!chosen)
{
scopedLogger.LogError("Failed to select character {name}", characterName);
return false;
}
// TODO: This needs to be reworked to use UI messages to click on Play
var hwnd = this.platformContextService.GetWindowHandle();
if (!hwnd.HasValue)
{
scopedLogger.LogError("Failed to get game window handle");
return false;
}
NativeMethods.SendMessageW((nint)hwnd.Value, NativeMethods.WM_KEYDOWN, 0x50, 0x00190001);
NativeMethods.SendMessageW((nint)hwnd.Value, NativeMethods.WM_CHAR, 0x70, 0x00190001);
NativeMethods.SendMessageW((nint)hwnd.Value, NativeMethods.WM_KEYUP, 0x50, 0x00190001);
return true;
}
}, cancellationToken);
}
private async Task<bool> NavigateToCharAndPlay(uint currentIndex, uint desiredIndex, CancellationToken cancellationToken)
private async Task<bool> WaitForCharSelectAndSelectCharacter(string characterName, CancellationToken cancellationToken)
{
var scopedLogger = this.logger.CreateScopedLogger();
// Wait for character select to be ready
while (!cancellationToken.IsCancellationRequested)
{
await Task.Delay(100, cancellationToken);
var result = await this.gameThreadService.QueueOnGameThread(() =>
try
{
unsafe
if (await this.gameThreadService.QueueOnGameThread(this.IsCharSelectReady, cancellationToken))
{
var preGameContext = this.gameContextService.GetPreGameContext();
if (preGameContext.IsNull)
{
scopedLogger.LogError("Pre-game context is not initialized");
return false;
}
var hwnd = this.platformContextService.GetWindowHandle();
if (hwnd is null or 0)
{
scopedLogger.LogError("Failed to get window handle");
return false;
}
if (preGameContext.Pointer->Index1 == currentIndex)
{
return false; //Not moved yet
}
if (preGameContext.Pointer->Index1 == desiredIndex)
{
// We're on the desired character. Trigger play
NativeMethods.SendMessageW((nint)hwnd.Value, NativeMethods.WM_KEYDOWN, 0x50, 0x00190001);
NativeMethods.SendMessageW((nint)hwnd.Value, NativeMethods.WM_CHAR, 0x70, 0x00190001);
NativeMethods.SendMessageW((nint)hwnd.Value, NativeMethods.WM_KEYUP, 0x50, 0x00190001);
return true;
}
currentIndex = preGameContext.Pointer->Index1;
NativeMethods.SendMessageW((nint)hwnd.Value, NativeMethods.WM_KEYDOWN, NativeMethods.VK_RIGHT, 0x014D0001);
NativeMethods.SendMessageW((nint)hwnd.Value, NativeMethods.WM_KEYUP, NativeMethods.VK_RIGHT, 0x014D0001);
return false;
break;
}
}, cancellationToken);
if (result)
await Task.Delay(100, cancellationToken);
}
catch(Exception e)
{
return true;
scopedLogger.LogError(e, "Error while waiting for character select to be ready");
throw;
}
}
return false;
if (cancellationToken.IsCancellationRequested)
{
return false;
}
scopedLogger.LogInformation("Character select ready, selecting {name}", characterName);
return await this.SelectCharacterToPlay(characterName, play: true, cancellationToken);
}
private async Task<string?> GetCharNameByUuid(string uuid, CancellationToken cancellationToken)
@@ -247,7 +300,7 @@ public sealed class CharacterSelectService(
{
await this.gameThreadService.QueueOnGameThread(() =>
{
var logoutMessage = new LogOutMessage(0, 0);
var logoutMessage = new LogOutMessage(0, 1); // Changed to 1 for character select
unsafe
{
this.uiContextService.SendMessage(Models.UIMessage.Logout, (uint)&logoutMessage, 0);
@@ -255,59 +308,27 @@ public sealed class CharacterSelectService(
}, cancellationToken);
}
private async Task<(uint DesiredIndex, uint CurrentIndex)?> WaitForLoginScreenAndGetCurrentAndDesiredIndex(string desiredCharName, CancellationToken cancellationToken)
private async Task<bool> ValidateState(CancellationToken cancellationToken)
{
uint? desiredIndex = default;
uint? currentIndex = default;
while (!cancellationToken.IsCancellationRequested)
return await this.gameThreadService.QueueOnGameThread(() =>
{
await Task.Delay(100, cancellationToken);
var ready = await this.gameThreadService.QueueOnGameThread(() =>
{
unsafe
{
var preGameContext = this.gameContextService.GetPreGameContext();
if (preGameContext.IsNull)
{
return false;
}
return this.instanceContextService.GetInstanceType() is not API.Interop.GuildWars.InstanceType.Loading;
}, cancellationToken);
}
var uiState = 10U;
this.uiContextService.SendMessage(Models.UIMessage.CheckUIState, 0, (uint)&uiState);
var loginScreen = uiState == 2;
if (!loginScreen)
{
return false;
}
private unsafe bool IsCharSelectReady()
{
return this.GetCharSelectorFrame() is not null;
}
for (var i = 0U; i < preGameContext.Pointer->LoginCharacters.Size; i++)
{
var loginCharacter = preGameContext.Pointer->LoginCharacters.Buffer[i];
var charNameSpan = loginCharacter.CharacterName.AsSpan();
var charName = new string(charNameSpan[..charNameSpan.IndexOf('\0')]);
if (charName == desiredCharName)
{
desiredIndex = i;
currentIndex = 0xffffffdd;
return true;
}
}
return false;
}
}, cancellationToken);
if (ready)
{
break;
}
private unsafe Frame* GetCharSelectorFrame()
{
var selectorFrame = this.uiContextService.GetFrameByLabel("Selector");
if (selectorFrame.IsNull)
{
return null;
}
if (desiredIndex is not null && currentIndex is not null)
{
return (desiredIndex.Value, currentIndex.Value);
}
return default;
return selectorFrame.Pointer;
}
}
+117
View File
@@ -0,0 +1,117 @@
namespace Daybreak.API.Services;
public sealed class HashingService
{
private const uint DefaultHash = 0x325D1EAE;
private const uint WStringHashSeed1 = 0xE2C15C9D;
private const uint WStringHashSeed2 = 0x2170A28A;
private static readonly uint[] HashTable = [
0x2EB3C21D, 0xA6F1D482, 0xBE24628F, 0x35699E30, 0xC87EE224, 0x04BA0097, 0x791F0EAF, 0xA162D466,
0x685B376A, 0xF82133BA, 0x4682D46B, 0xCB25C0C5, 0xAC3F9D22, 0xABF17202, 0xEF987728, 0x3E400B83,
0x63003108, 0xD83F1C93, 0x5058F507, 0x032E9991, 0x3518BC24, 0x1F90D44B, 0x92508DCE, 0x42DAA15C,
0x5B4105A3, 0x4D5026AD, 0x07F838B0, 0xE8C4D2A2, 0xF9EF4F54, 0x1FC1A85F, 0xB9041A40, 0x446F6969,
0xACF1EE39, 0x1828CC65, 0xC2F6C59D, 0x2456C3C0, 0x64F70142, 0x9C8ADB42, 0xC76781BF, 0x6AEA07E3,
0xE98B14BF, 0x7DBCB35E, 0x6CE78C80, 0x1E3F93DE, 0x0954FEB7, 0xB3103AEF, 0x6DAAC1EE, 0x6535E301,
0x4A221E68, 0x819CDD81, 0x5378CE3B, 0xE7348236, 0x2C3A7BCC, 0x31C50234, 0x745319DA, 0xF0897A0B,
0x5C182968, 0x48E018F4, 0x593E3BB6, 0xAB588C8D, 0xF1101F1F, 0x1D36E8BC, 0xABC7C7C9, 0xF58AA6C8,
0xF75B4B98, 0xC447B576, 0x6E5290F2, 0xD344EDF8, 0x5E1BCAF1, 0xEE892405, 0x798F7609, 0x8905CC29,
0x8977DEDC, 0x007850BC, 0x0AA3114A, 0x2F61C8F2, 0x376DFAA4, 0x0BABFC12, 0x98E64E82, 0xE6743711,
0xE26B26B6, 0xCBB10C72, 0x87446F8F, 0x96E26C0D, 0xCF140831, 0x81F69B0B, 0x5C9DF30B, 0xB276F929,
0x2712C5B6, 0x80EA90D6, 0x8EB30322, 0xCF7A705A, 0x57A17E8C, 0x556A2425, 0x15104549, 0x5F3D549F,
0x5D98FDEE, 0x9D8FA73E, 0x9206C9EF, 0xEDA0541D, 0xA3164F2D, 0x04C83543, 0x9BB27230, 0xEE97F0BA,
0xB6AEB38C, 0x41FF4DA7, 0x4023EE54, 0xECBB78BC, 0xC2C54733, 0x8850B63F, 0x93BF56EF, 0xC80177D7,
0xE68B0C13, 0x3C1E9D4C, 0x5CB3EACC, 0xF16F5795, 0xAC2B8A5C, 0x952E466E, 0x3095B0D5, 0xEC9D3A43,
0x23D8FCF7, 0x10C7A93F, 0x1B479FB5, 0x3664CF8F, 0xA514A56B, 0xE16C4886, 0x63828188, 0xA47F14C1,
0x391C237C, 0xFB15911F, 0x29825A01, 0xFC66DA40, 0xB2719F4E, 0x7B0D1611, 0x8F1EE7DE, 0xA6A1E77D,
0xDFC25D30, 0x76377036, 0x8145DB82, 0x2300A4E9, 0xA2417F88, 0x1B8C6AFC, 0x68EBDC8E, 0x48FC0C41,
0x040C3DCD, 0x17F02C44, 0xCC40B026, 0x5089EE1E, 0x2A594C28, 0x0CC198EC, 0x3F676F3D, 0xCD0AA239,
0x274A31A4, 0xC6B4D4FA, 0x9480A62F, 0xAF8862EF, 0x627CA44D, 0x572E89D3, 0x4C05136C, 0x60F36E8C,
0x7DE31FEF, 0x1B97FED3, 0xED99E2BF, 0xA034D418, 0xBBDB3E04, 0x1F198B71, 0xED45E889, 0xF7F1B0DC,
0x1592B9A5, 0x96D60F96, 0x03D616EE, 0xA5EF679D, 0x831D791D, 0x579D5FD1, 0x4B44DC48, 0x38F7A2F0,
0x2BF34A28, 0xAF14318C, 0xBD66D20A, 0x9AC72D2A, 0xC41ADB95, 0x860B2205, 0xE0E580E7, 0x3A06669C,
0x2D1A40A2, 0x577EDF60, 0x1A37693A, 0x3E576C35, 0xE3857565, 0xE46E75EE, 0x94E3625D, 0x7D1B9280,
0x2A0CCCE1, 0xBAB36163, 0x66E4E087, 0x940129BB, 0x59C197CD, 0xB0634139, 0xBB2964E1, 0x17207138,
0xD8BD3728, 0xB324876C, 0x7C72FD04, 0xBB5D9D82, 0xD9B40236, 0xC0045BF6, 0xF85100ED, 0x36493582,
0x8D0DF92A, 0xC3328368, 0xC202E412, 0x7ADE0064, 0xD01529C6, 0x4445B2AD, 0x0F6FC63B, 0xEF976FC5,
0x709EC6BE, 0x69D31EA1, 0x794BFEB4, 0xB522DB95, 0x700DE6DF, 0x49098B71, 0x38B7FA82, 0x11EDDC04,
0x5BB21D9F, 0xB14BC6D4, 0x3009D754, 0x451F5F3D, 0xFAA58575, 0xFB190B0D, 0x999DF79F, 0x6798A7AA,
0x0BC434FF, 0x429E6C15, 0xA1EA2298, 0x55A57B81, 0xD7D74B8D, 0x8FECB38B, 0x658AD588, 0xF0190227,
0xBE5D2B36, 0x62C59613, 0xC9FEDD64, 0xADD07A1E, 0x757A7679, 0xD78B6DCC, 0x56F88B2C, 0x4CC020BB,
0xA88B51FE, 0xF47F7691, 0x48587EDD, 0x3FCEC5DB, 0xA7E127AC, 0x561F6161, 0x37AB9607, 0xFE23282B,
];
private static readonly uint[] StringHashTable = [
0x92B9A528, 0x25D4FC88, 0xEDCBEFB8, 0x51063A80,
0x91341C61, 0x0261229D, 0x726F48ED, 0xCE1C088C,
0x76253EB5, 0x31E3A0DE, 0xA2AAD215, 0xCA7D6D27,
0xA5F98970, 0x0541C365, 0x3C14FF04, 0x5056AF4F
];
private static uint HashHelper(uint result, byte b)
{
return result + (HashTable[result >> 24] ^ (result >> 6) ^ HashTable[b]);
}
public uint Hash(byte[] data)
{
var hash = DefaultHash;
foreach(var b in data)
{
hash = HashHelper(hash, b);
}
return hash;
}
public uint Hash(byte data)
{
return HashHelper(DefaultHash, data);
}
public uint Hash(short data)
{
uint hash = DefaultHash;
hash = HashHelper(hash, (byte)(data >> 0));
hash = HashHelper(hash, (byte)(data >> 8));
return hash;
}
public uint Hash(uint data)
{
uint hash = DefaultHash;
hash = HashHelper(hash, (byte)(data >> 0));
hash = HashHelper(hash, (byte)(data >> 8));
hash = HashHelper(hash, (byte)(data >> 16));
hash = HashHelper(hash, (byte)(data >> 24));
return hash;
}
public uint Hash(string data, int maxLength = -1)
{
if (maxLength is -1)
{
maxLength = data.Length;
}
uint seed1 = WStringHashSeed1;
uint seed2 = WStringHashSeed2;
uint hash = DefaultHash;
for(var i = 0; i < maxLength && i < data.Length; i++)
{
var c = data[i];
var uc = (uint)c;
if (c >= 'a' && c <= 'z')
{
uc -= 0x20;
}
seed1 = (seed1 << 3) ^ uc;
seed2 += StringHashTable[seed1 & 0xf];
hash ^= seed2 + seed1;
}
return hash;
}
}
@@ -12,9 +12,9 @@ public unsafe sealed class AgentContextService : IAddressHealthService
private const int AgentArrayOffset = -0x4;
private static readonly byte[] AgentArrayAddressPattern = [0x8b, 0x0c, 0x90, 0x85, 0xc9, 0x74, 0x19];
private const string PlayerAgentIdMask = "xx????xxxx";
private const string PlayerAgentIdMask = "xx????xxxxxxx";
private const int PlayerAgentIdOffset = -0xE;
private static readonly byte[] PlayerAgentIdAddressPattern = [0x5d, 0xe9, 0x00, 0x00, 0x00, 0x00, 0x55, 0x8b, 0xec, 0x53];
private static readonly byte[] PlayerAgentIdAddressPattern = [0x5d, 0xe9, 0x00, 0x00, 0x00, 0x00, 0x55, 0x8b, 0xec, 0x56, 0x57, 0x8b, 0x7d];
private readonly MemoryScanningService memoryScanningService;
private readonly GWAddressCache agentArrayAddress;
@@ -3,14 +3,23 @@ using Daybreak.API.Interop.GuildWars;
using Daybreak.API.Models;
using System.Core.Extensions;
using System.Extensions.Core;
using System.Runtime.InteropServices;
namespace Daybreak.API.Services.Interop;
public unsafe sealed class GameContextService : IAddressHealthService
{
private const string BaseAddressMask = "xxxxxxx";
private const int BaseAddressOffset = +7;
private static readonly byte[] BaseAddressPattern = [0x50, 0x6A, 0x0F, 0x6A, 0x00, 0xFF, 0x35];
// TLS-based pattern to find GameContext getter function
private const string GameContextTlsMask = "xx????xxxxxxxxxxx????x";
private const int GameContextTlsOffset = 0;
private static readonly byte[] GameContextTlsPattern =
[
0x8B, 0x0D, 0x00, 0x00, 0x00, 0x00, // mov ecx, [TlsIndexPtr] - 6 bytes: xx????
0x64, 0xA1, 0x2C, 0x00, 0x00, 0x00, // mov eax, fs:[0000002C] - 6 bytes: xxxxxx
0x8B, 0x04, 0x88, // mov eax, [eax+ecx*4] - 3 bytes: xxx
0x8B, 0x80, 0x04, 0x00, 0x00, 0x00, // mov eax, [eax+00000004] - 6 bytes: xx????
0xC3 // ret - 1 byte: x
]; // Total: 22 bytes, mask: 22 chars
private const string PreGameContextFile = "UiPregame.cpp";
private const string PreGameContextAssertion = "!s_scene";
@@ -21,7 +30,7 @@ public unsafe sealed class GameContextService : IAddressHealthService
private static readonly byte[] AvailableCharsPattern = [0x8B, 0x35, 0x00, 0x00, 0x00, 0x00, 0x57, 0x69, 0xF8, 0x84, 0x00, 0x00, 0x00];
private readonly MemoryScanningService memoryScanningService;
private readonly GWAddressCache baseAddress;
private readonly GWAddressCache gameContextGetterAddress;
private readonly GWAddressCache preGameContextAddress;
private readonly GWAddressCache availableCharsAddress;
private readonly ILogger<GameContextService> logger;
@@ -32,18 +41,21 @@ public unsafe sealed class GameContextService : IAddressHealthService
{
this.logger = logger.ThrowIfNull();
this.memoryScanningService = memoryScanningService.ThrowIfNull();
this.baseAddress = new GWAddressCache(this.GetBaseAddress);
this.gameContextGetterAddress = new GWAddressCache(this.GetGameContextGetterAddress);
this.preGameContextAddress = new GWAddressCache(this.GetPreGameContextAddress);
this.availableCharsAddress = new GWAddressCache(this.GetAvailableCharactersAddress);
}
[UnmanagedFunctionPointer(CallingConvention.Cdecl)]
private delegate nuint GetGameContextDelegate();
public List<AddressState> GetAddressStates()
{
return [
new AddressState
{
Address = this.baseAddress.GetAddress() ?? 0,
Name = nameof(this.baseAddress),
Address = this.gameContextGetterAddress.GetAddress() ?? 0,
Name = nameof(this.gameContextGetterAddress),
},
new AddressState
{
@@ -113,41 +125,46 @@ public unsafe sealed class GameContextService : IAddressHealthService
private nuint GetGameContextAddress()
{
var scopedLogger = this.logger.CreateScopedLogger();
if (this.baseAddress.GetAddress() is not nuint basePtr)
// Get the address of the GameContext getter function
var getterAddress = this.gameContextGetterAddress.GetAddress();
if (getterAddress is null or 0)
{
scopedLogger.LogError("Failed to get base address");
return default;
scopedLogger.LogError("Failed to get GameContext getter function address");
return 0;
}
nuint** baseContext = *(nuint***)basePtr;
if (baseContext is null)
// Call the function to get GameContext
var getter = Marshal.GetDelegateForFunctionPointer<GetGameContextDelegate>((nint)getterAddress);
var gameContextPtr = getter();
if (gameContextPtr == 0)
{
scopedLogger.LogError("Failed to get base context address");
return default;
scopedLogger.LogError("GameContext getter returned null");
return 0;
}
var globalContextPtr = baseContext[0x6];
if (globalContextPtr is null)
{
scopedLogger.LogError("Failed to get game context address");
return default;
}
return (nuint)globalContextPtr;
scopedLogger.LogInformation("GameContext address: 0x{address:X8}", gameContextPtr);
return gameContextPtr;
}
private nuint GetBaseAddress()
private nuint GetGameContextGetterAddress()
{
var scopedLogger = this.logger.CreateScopedLogger();
var baseAddress = this.memoryScanningService.FindAndResolveAddress(BaseAddressPattern, BaseAddressMask, BaseAddressOffset);
if (baseAddress is 0)
var address = this.memoryScanningService.FindAddress(
GameContextTlsPattern,
GameContextTlsMask,
0);
if (address is 0)
{
scopedLogger.LogError("Failed to find base address");
scopedLogger.LogError("Failed to find GameContext getter function");
return 0U;
}
scopedLogger.LogInformation("Base address: 0x{address:X8}", baseAddress);
return baseAddress;
scopedLogger.LogInformation("GameContext getter function: 0x{address:X8}", address);
return address;
}
private unsafe nuint GetPreGameContextAddress()
@@ -16,7 +16,7 @@ public sealed class GameThreadService
[SuppressUnmanagedCodeSecurity]
[UnmanagedFunctionPointer(CallingConvention.Cdecl)]
private delegate void LeaveGameThread(nint ctx);
private delegate void LeaveGameThread(nint arg1, nint arg2);
private readonly MemoryScanningService memoryScanningService;
private readonly GWHook<LeaveGameThread> leaveGameThreadHook;
@@ -103,7 +103,7 @@ public sealed class GameThreadService
scopedLogger.LogInformation("Hooks initialized");
}
private void OnLeaveGameThread(nint ctx)
private void OnLeaveGameThread(nint arg1, nint arg2)
{
var scopedLogger = this.logger.CreateScopedLogger();
while(this.queuedItems.TryDequeue(out var item))
@@ -137,6 +137,6 @@ public sealed class GameThreadService
}
}
this.leaveGameThreadHook.Continue(ctx);
this.leaveGameThreadHook.Continue(arg1, arg2);
}
}
@@ -16,6 +16,8 @@ public sealed unsafe class MemoryScanningService(
private readonly (nuint BaseAddress, ImageSectionHeader Section) textSection = GetSectionHeader(".text");
private readonly (nuint BaseAddress, ImageSectionHeader Section) dataSection = GetSectionHeader(".rdata");
public uint GameTlsIndex { get; } = GetGameTlsIndex();
public nuint FunctionFromNearCall(nuint callInstructionAddress, bool checkValidPtr = true)
{
var scopedLogger = this.logger.CreateScopedLogger();
@@ -118,7 +120,7 @@ public sealed unsafe class MemoryScanningService(
fileSearchPos = filePtr + 1;
// If what we found is …/file.hpp (no drive letter) fix it
// If what we found is "…/file.hpp" (no drive letter) fix it
if (Marshal.ReadByte((nint)(filePtr + 1)) != (byte)':')
{
// look ≤128 bytes backward for ':' and back-up one char
@@ -199,6 +201,12 @@ public sealed unsafe class MemoryScanningService(
return address;
}
public nuint FindAddress(byte[] pattern, string mask, int offset, nuint startAddress, nuint endAddress)
{
var address = FindAddressInternal(startAddress, endAddress, pattern, mask, offset);
return address;
}
public nuint ToFunctionStart(nuint callInstructionAddress, uint scanRange = 0x500)
{
if (callInstructionAddress == 0)
@@ -216,6 +224,92 @@ public sealed unsafe class MemoryScanningService(
return FindInRange(prologue, mask, 0, start, end);
}
public nuint FindNthUseOfString(string str, uint nth, int offset = 0, bool useTextSection = true)
{
var scopedLogger = this.logger.CreateScopedLogger();
var strBytes = Encoding.ASCII.GetBytes(str);
var mask = new string('x', strBytes.Length);
var foundStr = FindAddressInternal(this.dataSection, strBytes, mask, 0);
if (foundStr is 0)
{
scopedLogger.LogError("Failed to find string in .rdata section");
return 0;
}
var firstNullChar = FindInRange([0x0], "x", 1, foundStr, foundStr - 0x64);
if (firstNullChar is 0)
{
scopedLogger.LogError("Failed to find null terminator");
return 0;
}
return this.FindNthUseOfAddress(firstNullChar, nth, offset, useTextSection);
}
public nuint FindUseOfString(string str, int offset = 0, bool useTextSection = true)
{
return this.FindNthUseOfString(str, 0, offset, useTextSection);
}
public nuint FindNthUseOfWideString(string str, uint nth, int offset = 0, bool useTextSection = true)
{
var scopedLogger = this.logger.CreateScopedLogger();
var strBytes = Encoding.Unicode.GetBytes(str);
var mask = new string('x', strBytes.Length);
var foundStr = FindAddressInternal(this.dataSection, strBytes, mask, 0);
if (foundStr is 0)
{
scopedLogger.LogError("Failed to find wide string in .rdata section");
return 0;
}
var firstNullChar = FindInRange([0x0, 0x0], "xx", 2, foundStr, foundStr - 0x128);
if (firstNullChar is 0)
{
scopedLogger.LogError("Failed to find null terminator for wide string");
return 0;
}
return this.FindNthUseOfAddress(firstNullChar, nth, offset, useTextSection);
}
public nuint FindUseOfWideString(string str, int offset = 0, bool useTextSection = true)
{
return this.FindNthUseOfWideString(str, 0, offset, useTextSection);
}
private nuint FindNthUseOfAddress(nuint address, uint nth, int offset, bool useTextSection)
{
var (BaseAddress, Section) = useTextSection ? this.textSection : this.dataSection;
var addressBytes = BitConverter.GetBytes((uint)address);
var mask = "xxxx";
var currentAddress = BaseAddress + Section.VirtualAddress;
var endAddress = currentAddress + Section.VirtualSize;
uint foundCount = 0;
while (currentAddress < endAddress)
{
var found = FindAddressInternal(currentAddress, endAddress, addressBytes, mask, 0);
if (found is 0)
{
break;
}
if (foundCount == nth)
{
return (nuint)((nint)found + offset);
}
foundCount++;
currentAddress = found + 1;
}
return 0;
}
private static bool IsValidPtr(nuint address, (nuint BaseAddress, ImageSectionHeader Section) section)
{
return address > 0 &&
@@ -276,8 +370,12 @@ public sealed unsafe class MemoryScanningService(
{
var textStart = section.BaseAddress + section.Section.VirtualAddress;
var textEnd = textStart + section.Section.VirtualSize - (uint)pattern.Length;
return FindAddressInternal(textStart, textEnd, pattern, mask, offset);
}
for (var cur = textStart; cur <= textEnd; ++cur)
private static nuint FindAddressInternal(nuint startAddr, nuint endAddr, byte[] pattern, string mask, int offset = 0)
{
for (var cur = startAddr; cur <= endAddr; ++cur)
{
if (Marshal.ReadByte((nint)cur) != pattern[0])
{
@@ -312,4 +410,30 @@ public sealed unsafe class MemoryScanningService(
? throw new InvalidOperationException($"Failed to initialize memory scanner. Failed to find {headerName} section")
: ((nuint, ImageSectionHeader))(baseAddr, textHdr);
}
private static uint GetGameTlsIndex()
{
var m = Process.GetCurrentProcess().MainModule ?? throw new InvalidOperationException("Failed to get TLS index. Failed to find main module");
var baseAddr = (nint)m.BaseAddress;
var pe = new PeFile(m.FileName);
// Get the TLS data directory entry (index 9 = IMAGE_DIRECTORY_ENTRY_TLS)
var tlsDirectory = pe.ImageNtHeaders?.OptionalHeader.DataDirectory[9];
if (tlsDirectory is null || tlsDirectory.VirtualAddress == 0)
{
throw new InvalidOperationException("Failed to get TLS index. No TLS directory found");
}
// Calculate the address of the TLS directory in memory
var tlsDirectoryAddress = baseAddr + (nint)tlsDirectory.VirtualAddress;
// Read the IMAGE_TLS_DIRECTORY structure
// AddressOfIndex is at offset 8 (after StartAddressOfRawData and EndAddressOfRawData)
var addressOfIndexPtr = Marshal.ReadIntPtr(tlsDirectoryAddress + 8);
// Read the TLS index from the location pointed to by AddressOfIndex
var tlsIndex = (uint)Marshal.ReadInt32(addressOfIndexPtr);
return tlsIndex;
}
}
@@ -2,6 +2,7 @@
using Daybreak.API.Models;
using System.Core.Extensions;
using System.Extensions;
using static Daybreak.API.Models.UIPackets;
namespace Daybreak.API.Services.Interop;
@@ -10,13 +11,12 @@ public sealed class PartyContextService
{
private const string SearchButtonFile = "\\Code\\Gw\\Ui\\Game\\Party\\PtSearch.cpp";
private const string SearchButtonAssertion = "m_activeList == LIST_HEROES";
private const string WindowButtonFile = "\\Code\\Gw\\Ui\\Game\\Party\\PtButtons.cpp";
private const string WindowButtonAssertion = "m_selection.agentId";
private const string WindowButtonAssertion = "selection.agentId";
// Fastcall funcs do not work in .NET for x86 right now. https://github.com/dotnet/runtime/issues/113851
// This fastcall equivalent is <void*, uint, uint*, void>
// This fastcall equivalent is <void*, uint, UIPacket::kMouseAction*, void>
private readonly GWFastCall<nint, uint, nint, GWFastCall.Void> searchButtonCallback;
// This fastcall equivalent is <void*, uint, uint*, void>
// This fastcall equivalent is <void*, uint, UIPacket::kMouseAction*, void>
private readonly GWFastCall<nint, uint, nint, GWFastCall.Void> windowButtonCallback;
private readonly MemoryScanningService memoryScanningService;
@@ -29,7 +29,7 @@ public sealed class PartyContextService
this.memoryScanningService = memoryScanningService.ThrowIfNull();
this.logger = logger.ThrowIfNull();
this.searchButtonCallback = new(new GWAddressCache(() => this.memoryScanningService.ToFunctionStart(this.memoryScanningService.FindAssertion(SearchButtonFile, SearchButtonAssertion, 0, 0))));
this.windowButtonCallback = new(new GWAddressCache(() => this.memoryScanningService.ToFunctionStart(this.memoryScanningService.FindAssertion(WindowButtonFile, WindowButtonAssertion, 0, 0))));
this.windowButtonCallback = new(new GWAddressCache(() => this.memoryScanningService.ToFunctionStart(this.memoryScanningService.FindUseOfString(WindowButtonAssertion))));
}
public List<AddressState> GetAddressStates()
@@ -52,13 +52,13 @@ public sealed class PartyContextService
}
}
public unsafe bool CallSearchButtonCallback(void* ctx, uint edx, uint* wparam)
public unsafe bool CallSearchButtonCallback(void* ctx, uint edx, MouseAction* wparam)
{
this.searchButtonCallback.Invoke((nint)ctx, edx, (nint)wparam);
return true;
}
public unsafe bool CallWindowButtonCallback(void* ctx, uint edx, uint* wparam)
public unsafe bool CallWindowButtonCallback(void* ctx, uint edx, MouseAction* wparam)
{
this.windowButtonCallback.Invoke((nint)ctx, edx, (nint)wparam);
return true;
@@ -66,26 +66,28 @@ public sealed class PartyContextService
public unsafe bool AddHero(uint heroId)
{
var wparam = stackalloc uint[4];
wparam[2] = 0x6;
wparam[1] = 0x1;
var action = new MouseAction(
frameId: 0,
childOffsetId: 0x1,
currentState: ActionState.MouseUp);
var ctx = stackalloc uint[13];
ctx[0xb] = 1;
ctx[9] = heroId;
return this.CallSearchButtonCallback(ctx, 2, wparam);
ctx[0x9] = heroId;
return this.CallSearchButtonCallback(ctx, 2, &action);
}
public unsafe bool KickHero(uint heroId)
{
var wparam = stackalloc uint[4];
wparam[2] = 0x6;
wparam[1] = 0x6;
var action = new MouseAction(
frameId: 0,
childOffsetId: 0x6,
currentState: ActionState.MouseUp);
var ctx = stackalloc uint[13];
ctx[0xb] = 1;
ctx[9] = heroId;
return this.CallSearchButtonCallback(ctx, 0, wparam);
ctx[0x9] = heroId;
return this.CallSearchButtonCallback(ctx, 0, &action);
}
public bool KickAllHeroes() => this.KickHero(0x26);
@@ -94,6 +96,6 @@ public sealed class PartyContextService
{
var ctx = stackalloc uint[14];
ctx[0xd] = 1;
return this.CallWindowButtonCallback(ctx, 0, (uint*)0);
return this.CallWindowButtonCallback(ctx, 0, (MouseAction*)0);
}
}
+124 -60
View File
@@ -13,28 +13,30 @@ public sealed class UIContextService
{
private const string GetChildFrameIdFile = "CtlView.cpp";
private const string GetChildFrameIdAssertion = "pageId";
private const int GetChildFrameIdOffset = 0x19;
private const string SetFrameFlagFile = "FrApi.cpp";
private const string SetFrameFlagAssertion = "frameId";
private const int SetFrameVisibleOffset = 0x4f9;
private const int SetFrameDisabledOffset = 0x4e3;
private static readonly byte[] SendFrameUiMessageByIdSeq = [0x83, 0xFB, 0x47, 0x73, 0x14];
private const string SendFrameUiMessageByIdMask = "xxxxx";
private const int SendFrameUiMessageByIdOffset = -0x34;
private const int GetChildFrameIdOffset = 0x12;
private static readonly byte[] GetChildFrameIdPattern = [0xE8];
private const string GetChildFrameIdMask = "x";
private static readonly byte[] SendFrameUiMessageSeq = [
0x83, 0xC1, 0xDC, // add ecx, -24
0xE8, 0x00, 0x00, 0x00, 0x00, // call
0x8B, 0x4D, 0xFC // mov ecx, [ebp-04]
];
private const string SendFrameUiMessageMask = "xxxx????xxx";
private const int SendFrameUiMessageOffset = 0x3;
private static readonly byte[] SendUIMessageSeq = [0xB9, 0x00, 0x00, 0x00, 0x00, 0xE8, 0x00, 0x00, 0x00, 0x00, 0x5D, 0xC3, 0x89, 0x45, 0x08];
private const string SendUIMessageMask = "x????x????xxxxx";
private const string CreateHashFromStringMask = "xxxxxxx";
private const int CreateHashFromStringOffset = 0x7;
private static readonly byte[] CreateHashFromStringSeq = [0x85, 0xC0, 0x74, 0x0D, 0x6A, 0xFF, 0x50];
private const string FrameArrayFile = "\\Code\\Engine\\Frame\\FrMsg.cpp";
private const string FrameArrayAssertion = "frame";
private static readonly byte[] GetRootFrameSeq = [0x05, 0xE0, 0xFE, 0xFF, 0xFF, 0xC3];
private const string GetRootFrameMask = "xxxxxx";
private const int GetRootFrameOffset = -0x3C;
[SuppressUnmanagedCodeSecurity]
[UnmanagedFunctionPointer(CallingConvention.Cdecl)]
private unsafe delegate uint CreateHashFromStringFunc(char* value, int seed);
private static readonly byte[] GetRootFrameSeq = [
0xA1, 0x00, 0x00, 0x00, 0x00, // mov eax,[address]
0x05, 0xD8, 0xFE, 0xFF, 0xFF, // add eax,FFFFFED8
0xC3 // ret
];
private const string GetRootFrameMask = "x????xxxxxx";
[SuppressUnmanagedCodeSecurity]
[UnmanagedFunctionPointer(CallingConvention.Cdecl)]
@@ -52,13 +54,25 @@ public sealed class UIContextService
[UnmanagedFunctionPointer(CallingConvention.Cdecl)]
private unsafe delegate Frame* GetRootFrameFunc();
private readonly SemaphoreSlim semaphoreSlim = new(1);
[SuppressUnmanagedCodeSecurity]
[UnmanagedFunctionPointer(CallingConvention.Cdecl)]
private unsafe delegate void ValidateAsyncDecodeStr(char* s, DecodeStrCallback callback, void* wParam);
[SuppressUnmanagedCodeSecurity]
[UnmanagedFunctionPointer(CallingConvention.Cdecl)]
private unsafe delegate uint DoAsyncDecodeStr(char* encodedStr, DecodeStrCallback callback, void* wParam);
public unsafe delegate void DecodeStrCallback(void* param, char* s);
private readonly SemaphoreSlim sendUiMessageSemaphore = new(1);
private readonly SemaphoreSlim decodeStringSemaphore = new(1);
private readonly MemoryScanningService memoryScanningService;
private readonly GWDelegateCache<CreateHashFromStringFunc> createHashFromString;
private readonly HashingService hashingService;
private readonly GWDelegateCache<GetChildFrameIdFunc> getChildFrameId;
private readonly GWAddressCache frameArrayAddressCache;
private readonly GWHook<SendUIMessageFunc> sendUiMessageHook;
private readonly GWDelegateCache<GetRootFrameFunc> getRootFrame;
private readonly GWDelegateCache<ValidateAsyncDecodeStr> validateAsyncDecodeStr;
// This fastcall equivalent is <GuildWarsArray<FrameInteractionCallback>*, void*, UIMessage, void*, void*>
private readonly GWFastCall<nint, nint, UIMessage, nint, nint, GWFastCall.Void> sendFrameUIMessage;
private readonly ILogger<UIContextService> logger;
@@ -66,13 +80,13 @@ public sealed class UIContextService
private CancellationTokenSource? cts = default;
public UIContextService(
HashingService hashingService,
MemoryScanningService memoryScanningService,
ILogger<UIContextService> logger)
{
this.logger = logger.ThrowIfNull();
this.memoryScanningService = memoryScanningService.ThrowIfNull();
this.createHashFromString = new GWDelegateCache<CreateHashFromStringFunc>(new GWAddressCache(() => this.memoryScanningService.FunctionFromNearCall(
this.memoryScanningService.FindAddress(CreateHashFromStringSeq, CreateHashFromStringMask, CreateHashFromStringOffset))));
this.hashingService = hashingService.ThrowIfNull();
this.frameArrayAddressCache = new GWAddressCache(() => this.memoryScanningService.FindAssertion(FrameArrayFile, FrameArrayAssertion, 0, -0x14));
this.sendUiMessageHook = new GWHook<SendUIMessageFunc>(
new GWAddressCache(() => this.memoryScanningService.ToFunctionStart(
@@ -80,42 +94,50 @@ public sealed class UIContextService
this.OnSendUIMessage);
this.sendFrameUIMessage = new(
new GWAddressCache(() => this.memoryScanningService.FunctionFromNearCall(
this.memoryScanningService.FindAddress(SendFrameUiMessageByIdSeq, SendFrameUiMessageByIdMask, SendFrameUiMessageByIdOffset) + 0x67)));
this.memoryScanningService.FindAddress(SendFrameUiMessageSeq, SendFrameUiMessageMask, SendFrameUiMessageOffset))));
this.getChildFrameId = new GWDelegateCache<GetChildFrameIdFunc>(
new GWAddressCache(() => this.memoryScanningService.FunctionFromNearCall(
this.memoryScanningService.FindAssertion(GetChildFrameIdFile, GetChildFrameIdAssertion, 0, GetChildFrameIdOffset))));
new GWAddressCache(() =>
{
var address = this.memoryScanningService.FindAssertion(GetChildFrameIdFile, GetChildFrameIdAssertion, 0, GetChildFrameIdOffset);
return this.memoryScanningService.FunctionFromNearCall(
this.memoryScanningService.FindAddress(GetChildFrameIdPattern, GetChildFrameIdMask, 0, address, address + 0xFF));
}));
this.getRootFrame = new GWDelegateCache<GetRootFrameFunc>(
new GWAddressCache(() => this.memoryScanningService.FindAddress(GetRootFrameSeq, GetRootFrameMask, GetRootFrameOffset)));
new GWAddressCache(() => this.memoryScanningService.FindAddress(GetRootFrameSeq, GetRootFrameMask, 0)));
this.validateAsyncDecodeStr = new GWDelegateCache<ValidateAsyncDecodeStr>(
new GWAddressCache(() => this.memoryScanningService.ToFunctionStart(
this.memoryScanningService.FindUseOfString("(codedString[0] & ~WORD_BIT_MORE) >= WORD_VALUE_BASE"))));
}
public List<AddressState> GetAddressStates()
{
return
[
new()
{
Name = nameof(this.createHashFromString),
Address = this.createHashFromString.Cache.GetAddress() ?? 0U
},
new()
{
Name = nameof(this.frameArrayAddressCache),
Address = this.frameArrayAddressCache.GetAddress() ?? 0U
},
new()
{
Name = nameof(this.getChildFrameId),
Address = this.getChildFrameId.Cache.GetAddress() ?? 0U
},
new()
{
Name = nameof(this.sendFrameUIMessage),
Address = this.sendFrameUIMessage.Cache.GetAddress() ?? 0U
},
new()
{
Name = nameof(this.getChildFrameId),
Address = this.getChildFrameId.Cache.GetAddress() ?? 0U
},
new()
{
Name = nameof(this.getRootFrame),
Address = this.getRootFrame.Cache.GetAddress() ?? 0U
},
new()
{
Name = nameof(this.validateAsyncDecodeStr),
Address = this.validateAsyncDecodeStr.Cache.GetAddress() ?? 0U
}
];
}
@@ -150,6 +172,26 @@ public sealed class UIContextService
return Task.CompletedTask;
}
public unsafe WrappedPointer<T> GetFrameContext<T>(WrappedPointer<Frame> frame)
where T : unmanaged
{
if (frame.IsNull || frame.Pointer->FrameCallbacks.Size == 0)
{
return null;
}
for (uint i = 0; i < frame.Pointer->FrameCallbacks.Size; i++)
{
var callback = frame.Pointer->FrameCallbacks.Buffer[i];
if (callback.UiCtl_Context != null)
{
return new WrappedPointer<T>((T*)callback.UiCtl_Context);
}
}
return null;
}
public unsafe WrappedPointer<Frame> GetChildFrame(WrappedPointer<Frame> parent, uint childOffset)
{
var scopedLogger = this.logger.CreateScopedLogger();
@@ -227,10 +269,10 @@ public sealed class UIContextService
frame = this.GetButtonActionFrame();
}
this.semaphoreSlim.Wait();
this.sendUiMessageSemaphore.Wait();
var packet = new UIPackets.KeyAction((uint)action);
this.SendFrameUIMessage(frame, UIMessage.KeyDown, &packet);
this.semaphoreSlim.Release();
this.sendUiMessageSemaphore.Release();
return true;
}
@@ -241,10 +283,10 @@ public sealed class UIContextService
frame = this.GetButtonActionFrame();
}
this.semaphoreSlim.Wait();
this.sendUiMessageSemaphore.Wait();
var packet = new UIPackets.KeyAction((uint)action);
this.SendFrameUIMessage(frame, UIMessage.KeyUp, &packet);
this.semaphoreSlim.Release();
this.sendUiMessageSemaphore.Release();
return true;
}
@@ -268,7 +310,7 @@ public sealed class UIContextService
return null;
}
var hash = this.CreateHashFromString(label, -1);
var hash = this.CreateHashFromString(label);
if (hash is 0)
{
scopedLogger.LogError("Failed to get hash for label {label}", label);
@@ -315,26 +357,48 @@ public sealed class UIContextService
return frame;
}
public unsafe uint CreateHashFromString(string value, int seed)
public uint CreateHashFromString(string value)
{
var scopedLogger = this.logger.CreateScopedLogger();
if (this.createHashFromString.GetDelegate() is not CreateHashFromStringFunc del)
{
scopedLogger.LogError("Failed to get CreateHashFromString delegate");
return 0;
}
fixed (char* valuePtr = value)
{
return del(valuePtr, seed);
}
return this.hashingService.Hash(value);
}
public unsafe void SendMessage(UIMessage message, nuint wParam, nuint lParam)
{
this.semaphoreSlim.Wait();
this.sendUiMessageSemaphore.Wait();
this.sendUiMessageHook.Continue(message, wParam, lParam);
this.semaphoreSlim.Release();
this.sendUiMessageSemaphore.Release();
}
/// <summary>
/// Decodes a string asynchronously using the game's internal decoding mechanism.
/// </summary>
/// <remarks>
/// Inside the DecodeStrCallback, the decoded string has to be cleaned up by calling Marshal.FreeHGlobal on the char* pointer.
/// </remarks>
public unsafe void ValidateDecodeString(char* encoded, DecodeStrCallback callback, nuint callbackParam)
{
var scopedLogger = this.logger.CreateScopedLogger();
this.decodeStringSemaphore.Wait();
try
{
var validateDecodeStrFnc = this.validateAsyncDecodeStr.GetDelegate();
if (validateDecodeStrFnc is null)
{
scopedLogger.LogError("{funcName} is null", nameof(this.validateAsyncDecodeStr));
return;
}
validateDecodeStrFnc(encoded, callback, (void*)callbackParam);
}
catch(Exception e)
{
scopedLogger.LogError(e, "Encountered exception while decoding string");
}
finally
{
this.decodeStringSemaphore.Release();
}
}
private async ValueTask InitializeHooks(CancellationToken cancellationToken)
@@ -357,24 +421,24 @@ public sealed class UIContextService
private static unsafe void SetFrameVisibleInternal(WrappedPointer<Frame> frame, bool visible)
{
uint* pState = &frame.Pointer->Field91; // direct field address
uint* pState = &frame.Pointer->FrameState; // direct field address
if (visible)
{
*pState &= ~0x200u; // clear hidden
*pState &= ~0x200u; // clear "hidden"
}
else
{
*pState |= 0x200u; // set hidden
*pState |= 0x200u; // set "hidden"
}
}
private static unsafe void SetFrameDisabledInternal(WrappedPointer<Frame> frame, bool disabled)
{
uint* pState = &frame.Pointer->Field91; // same word (offset 0x184)
uint* pState = &frame.Pointer->FrameState; // same word (offset 0x184)
if (disabled)
{
*pState |= 0x10u; // set disabled
*pState |= 0x10u; // set "disabled"
}
else
{
+122
View File
@@ -0,0 +1,122 @@
using Daybreak.API.Interop.GuildWars;
using Daybreak.API.Services.Interop;
using Daybreak.Shared.Models.Api;
using Daybreak.Shared.Models.Guildwars;
using System.Extensions.Core;
using System.Text;
namespace Daybreak.API.Services;
public sealed class InventoryService(
GameContextService gameContextService,
GameThreadService gameThreadService,
UIService uIService,
ILogger<InventoryService> logger)
{
private readonly GameContextService gameContextService = gameContextService;
private readonly GameThreadService gameThreadService = gameThreadService;
private readonly UIService uIService = uIService;
private readonly ILogger<InventoryService> logger = logger;
public async Task<InventoryInformation?> GetInventoryInformation(CancellationToken cancellationToken)
{
var scopedLogger = this.logger.CreateScopedLogger();
var itemTuples = await this.gameThreadService.QueueOnGameThread(() =>
{
unsafe
{
var gameContext = this.gameContextService.GetGameContext();
if (gameContext.IsNull)
{
scopedLogger.LogError("Game context is null");
return default;
}
var inventory = gameContext.Pointer->ItemContext->Inventory;
if (inventory.IsNull)
{
scopedLogger.LogError("Inventory is null");
return default;
}
var itemTuples = new List<(BagType Type,List<(uint ModelId, string EncodedCompleteName, string EncodedSingleName, string EncodedName, bool Inscribable, int Quantity, uint[] Modifiers, ItemType ItemType)>)>();
foreach (var bag in inventory.Pointer->Bags)
{
if (bag is null)
{
continue;
}
var retBag = new List<(uint ModelId, string EncodedCompleteName, string EncodedSingleName, string EncodedName, bool Inscribable, int Quantity, uint[] Modifiers, ItemType ItemType)>();
itemTuples.Add((bag->Type, retBag));
if (bag->ItemsCount is 0)
{
continue;
}
foreach(var item in bag->Items)
{
if (item.IsNull)
{
continue;
}
var singleItemName = new string(item.Pointer->SingleItemName);
var nameEncoded = new string(item.Pointer->NameEncoded);
var completeNameEncoded = new string(item.Pointer->CompleteNameEncoded);
var modifiers = new uint[item.Pointer->ModifiersCount];
for(var j = 0; j < item.Pointer->ModifiersCount; j++)
{
modifiers[j] = item.Pointer->Modifiers[j].Mod;
}
retBag.Add((item.Pointer->ModelId, completeNameEncoded, singleItemName, nameEncoded, item.Pointer->Inscribable, item.Pointer->Quantity, modifiers, item.Pointer->Type));
}
}
return itemTuples;
}
}, cancellationToken);
if (itemTuples is null)
{
return default;
}
var decodedItemTuples = await Task.WhenAll(itemTuples.Select(async tuple =>
{
var decodedItems = await Task.WhenAll(tuple.Item2.Select(async item =>
{
var decodedName = await this.uIService.DecodeString(item.EncodedName, Language.English, cancellationToken);
var decodedCompleteName = await this.uIService.DecodeString(item.EncodedCompleteName, Language.English, cancellationToken);
var decodedSingleName = await this.uIService.DecodeString(item.EncodedSingleName, Language.English, cancellationToken);
return (item.ModelId, item.EncodedName, DecodedName: decodedName, item.EncodedSingleName, DecodedSingleName: decodedSingleName, item.EncodedCompleteName, DecodedCompleteName: decodedCompleteName, item.Inscribable, item.Quantity, item.Modifiers, item.ItemType);
}));
return (tuple.Type, Items: decodedItems.ToList());
}));
return new InventoryInformation(
Bags: [.. decodedItemTuples.Select(tuple =>
new BagEntry(
BagType: tuple.Type.ToString(),
Items: [.. tuple.Items.Select(item =>
new ItemEntry(
ItemModelId: item.ModelId,
EncodedName: ToBase64(item.EncodedName),
DecodedName: item.DecodedName ?? string.Empty,
EncodedSingleName: ToBase64(item.EncodedSingleName),
DecodedSingleName: item.DecodedSingleName ?? string.Empty,
EncodedCompleteName: ToBase64(item.EncodedCompleteName),
DecodedCompleteName: item.DecodedCompleteName ?? string.Empty,
Inscribable: item.Inscribable,
Quantity: item.Quantity,
Modifiers: item.Modifiers,
Properties: [.. ItemProperty.ParseItemModifiers([.. item.Modifiers.Select(m => (Shared.Models.Guildwars.ItemModifier)m)])],
ItemType: item.ItemType.ToString()))]))]);
}
private static string ToBase64(string encoded)
{
return Convert.ToBase64String(Encoding.Unicode.GetBytes(encoded));
}
}
+31 -21
View File
@@ -20,6 +20,7 @@ namespace Daybreak.API.Services;
public sealed class MainPlayerService : IDisposable
{
private readonly ChatService chatService;
private readonly InstanceContextService instanceContextService;
private readonly IBuildTemplateManager buildTemplateManager;
private readonly SkillbarContextService skillbarContextService;
@@ -35,10 +36,11 @@ public sealed class MainPlayerService : IDisposable
private TimeSpan minUpdateFrequency = TimeSpan.MaxValue;
private MainPlayerState? mainPlayerState;
private DateTimeOffset lastUpdateTime = DateTimeOffset.MinValue;
private DateTimeOffset lastFrequencyUpdate = DateTimeOffset.MinValue;
private DateTime lastUpdateTime = DateTime.MinValue;
private DateTime lastFrequencyUpdate = DateTime.MinValue;
public MainPlayerService(
ChatService chatService,
InstanceContextService instanceContextService,
IBuildTemplateManager buildTemplateManager,
SkillbarContextService skillbarContextService,
@@ -47,6 +49,7 @@ public sealed class MainPlayerService : IDisposable
GameThreadService gameThreadService,
ILogger<MainPlayerService> logger)
{
this.chatService = chatService.ThrowIfNull();
this.instanceContextService = instanceContextService.ThrowIfNull();
this.buildTemplateManager = buildTemplateManager.ThrowIfNull();
this.skillbarContextService = skillbarContextService.ThrowIfNull();
@@ -62,17 +65,17 @@ public sealed class MainPlayerService : IDisposable
this.callbackRegistration?.Dispose();
}
public Task<bool> SetCurrentBuild(string buildCode, CancellationToken cancellationToken)
public async Task<bool> SetCurrentBuild(string buildCode, CancellationToken cancellationToken)
{
var scopedLogger = this.logger.CreateScopedLogger();
if (!this.buildTemplateManager.TryDecodeTemplate(buildCode, out var build) ||
build is not SingleBuildEntry singleBuild)
{
scopedLogger.LogError("Failed to decode build template from code {buildCode}", buildCode);
return Task.FromResult(false);
return false;
}
return this.gameThreadService.QueueOnGameThread(() =>
var result = await this.gameThreadService.QueueOnGameThread(() =>
{
unsafe
{
@@ -137,7 +140,7 @@ public sealed class MainPlayerService : IDisposable
var attributeIds = new Array12Uint();
var attributeValues = new Array12Uint();
var skills = new Array8Uint();
for(var i = 0; i < singleBuild.Attributes.Count && i < 12; i++)
for (var i = 0; i < singleBuild.Attributes.Count && i < 12; i++)
{
if (singleBuild.Attributes[i].Attribute is null)
{
@@ -148,7 +151,7 @@ public sealed class MainPlayerService : IDisposable
attributeValues[i] = (uint)singleBuild.Attributes[i].Points;
}
for(var i =0; i < singleBuild.Skills.Count && i < 8; i++)
for (var i = 0; i < singleBuild.Skills.Count && i < 8; i++)
{
skills[i] = (uint)singleBuild.Skills[i].Id;
}
@@ -165,6 +168,17 @@ public sealed class MainPlayerService : IDisposable
return true;
}
}, cancellationToken);
if (result)
{
await this.chatService.AddMessageAsync("Build applied successfully.", "Daybreak.API", Channel.Moderator, cancellationToken);
}
else
{
await this.chatService.AddMessageAsync("Failed to apply build.", "Daybreak.API", Channel.Moderator, cancellationToken);
}
return result;
}
public Task<BuildEntry?> GetCurrentBuild(CancellationToken cancellationToken)
@@ -378,23 +392,19 @@ public sealed class MainPlayerService : IDisposable
return default;
}
if (!gameContext.TryGetPlayerId(out var playerId))
var playerIndex = gameContext.Pointer->CharContext->PlayerNumber;
var players = gameContext.Pointer->WorldContext->Players;
if (playerIndex < 0 || playerIndex >= players.Size)
{
scopedLogger.LogError("Failed to get player id");
return default;
}
var player = gameContext.Pointer->WorldContext->Players.AsValueEnumerable().FirstOrDefault(p => p.AgentId == playerId);
if (player.AgentId != playerId)
{
scopedLogger.LogError("Failed to find player with id {playerId}", playerId);
scopedLogger.LogError("Failed to get player id. Got {playerNumber}", playerIndex);
return default;
}
var player = gameContext.Pointer->WorldContext->Players[(int)playerIndex];
var currentTier = player.ActiveTitleTier;
TitleContext? currentTitle = default;
int? id = -1;
for(var i = 0; i < gameContext.Pointer->WorldContext->Titles.Size; i++)
for (var i = 0; i < gameContext.Pointer->WorldContext->Titles.Size; i++)
{
var title = gameContext.Pointer->WorldContext->Titles.Skip(i).FirstOrDefault();
if (title.CurrentTitleTierIndex == currentTier)
@@ -479,7 +489,7 @@ public sealed class MainPlayerService : IDisposable
ArgumentOutOfRangeException.ThrowIfLessThanOrEqual(frequency, TimeSpan.Zero);
var id = Guid.NewGuid();
var now = DateTimeOffset.UtcNow;
var now = DateTime.UtcNow;
var entry = new ByteConsumerEntry(id, frequency, onUpdate);
this.consumers[id] = entry;
@@ -495,7 +505,7 @@ public sealed class MainPlayerService : IDisposable
private unsafe void OnGameThreadProc()
{
var scopedLogger = this.logger.CreateScopedLogger();
var now = DateTimeOffset.UtcNow;
var now = DateTime.UtcNow;
/*
* Periodically adjust the frequency of updates based on connected clients.
@@ -613,13 +623,13 @@ public sealed class MainPlayerService : IDisposable
{
this.minUpdateFrequency = TimeSpan.MaxValue;
scopedLogger.LogDebug("No consumers registered, disabling updates");
this.lastFrequencyUpdate = DateTimeOffset.UtcNow;
this.lastFrequencyUpdate = DateTime.UtcNow;
return;
}
var minValue = this.consumers.Values.Min(c => c.Frequency);
scopedLogger.LogDebug("Adjusted update frequency to {frequency}", minValue);
this.lastFrequencyUpdate = DateTimeOffset.UtcNow;
this.lastFrequencyUpdate = DateTime.UtcNow;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
+12 -4
View File
@@ -15,6 +15,7 @@ using InstanceType = Daybreak.API.Interop.GuildWars.InstanceType;
namespace Daybreak.API.Services;
public sealed class PartyService(
ChatService chatService,
IBuildTemplateManager buildTemplateManager,
UIService uiService,
UIContextService uIContextService,
@@ -27,6 +28,7 @@ public sealed class PartyService(
{
private static readonly TimeSpan HeroSpawnDelay = TimeSpan.FromSeconds(1);
private readonly ChatService chatService = chatService.ThrowIfNull();
private readonly IBuildTemplateManager buildTemplateManager = buildTemplateManager.ThrowIfNull();
private readonly UIService uiService = uiService.ThrowIfNull();
private readonly UIContextService uIContextService = uIContextService.ThrowIfNull();
@@ -44,18 +46,21 @@ public sealed class PartyService(
if (!await this.IsInValidOutpost(cancellationToken))
{
scopedLogger.LogError("Could not set party loadout. Not in a valid outpost");
await this.chatService.AddMessageAsync("Cannot set party loadout. Not in a valid outpost.", "Daybreak.API", Channel.Moderator, cancellationToken);
return false;
}
if (!await this.LeaveParty(cancellationToken))
if (!await this.KickAllHeroes(cancellationToken))
{
scopedLogger.LogError("Could not set party loadout. Could not leave party");
await this.chatService.AddMessageAsync("Cannot set party loadout. Could not leave party.", "Daybreak.API", Channel.Moderator, cancellationToken);
return false;
}
if (!await this.gameThreadService.QueueOnGameThread(() => this.SpawnHeroes(partyLoadout), cancellationToken))
{
scopedLogger.LogError("Could not set party loadout. Could not spawn heroes");
await this.chatService.AddMessageAsync("Cannot set party loadout. Could not spawn heroes.", "Daybreak.API", Channel.Moderator, cancellationToken);
return false;
}
@@ -64,18 +69,21 @@ public sealed class PartyService(
if (!await this.gameThreadService.QueueOnGameThread(() => this.ApplyBuilds(partyLoadout), cancellationToken))
{
scopedLogger.LogError("Could not set party loadout. Could not apply builds");
await this.chatService.AddMessageAsync("Cannot set party loadout. Could not apply builds.", "Daybreak.API", Channel.Moderator, cancellationToken);
return false;
}
var heroBehaviorSetup = await this.gameThreadService.QueueOnGameThread(() => this.GetHeroBehaviorSetup(partyLoadout), cancellationToken);
foreach(var heroBehaviorEntry in heroBehaviorSetup ?? [])
foreach (var heroBehaviorEntry in heroBehaviorSetup ?? [])
{
if (!await this.SetHeroBehavior(heroBehaviorEntry.AgentId, heroBehaviorEntry.Behavior, cancellationToken))
{
scopedLogger.LogWarning("Could not set hero behavior for agent {agentId} to {behavior}", heroBehaviorEntry.AgentId, heroBehaviorEntry.Behavior);
await this.chatService.AddMessageAsync($"Cannot set party loadout. Could not set behavior {heroBehaviorEntry.Behavior} for agent {heroBehaviorEntry.AgentId}.", "Daybreak.API", Channel.Moderator, cancellationToken);
}
}
await this.chatService.AddMessageAsync("Party loadout set.", "Daybreak.API", Channel.Moderator, cancellationToken);
return true;
}
@@ -143,7 +151,7 @@ public sealed class PartyService(
}, cancellationToken);
}
public async Task<bool> LeaveParty(CancellationToken cancellationToken)
public async Task<bool> KickAllHeroes(CancellationToken cancellationToken)
{
var partySize = await this.GetPartySize(cancellationToken);
if (partySize is 1 or 0)
@@ -151,7 +159,7 @@ public sealed class PartyService(
return true;
}
return await this.gameThreadService.QueueOnGameThread(this.partyContextService.LeaveParty, cancellationToken);
return await this.gameThreadService.QueueOnGameThread(this.partyContextService.KickAllHeroes, cancellationToken);
}
public async Task<uint> GetPartySize(CancellationToken cancellationToken)
+91
View File
@@ -1,16 +1,21 @@
using Daybreak.API.Interop;
using Daybreak.API.Interop.GuildWars;
using Daybreak.API.Models;
using Daybreak.API.Services.Interop;
using System.Extensions.Core;
using System.Runtime.InteropServices;
using static Daybreak.API.Services.Interop.UIContextService;
namespace Daybreak.API.Services;
public sealed class UIService(
GameThreadService gameThreadService,
GameContextService gameContextService,
UIContextService uIContextService,
ILogger<UIService> logger)
{
private readonly GameThreadService gameThreadService = gameThreadService;
private readonly GameContextService gameContextService = gameContextService;
private readonly UIContextService uIContextService = uIContextService;
private readonly ILogger<UIService> logger = logger;
@@ -85,6 +90,92 @@ public sealed class UIService(
});
}
public async Task<string?> DecodeString(string encoded, Language language, CancellationToken cancellationToken)
{
var scopedLogger = this.logger.CreateScopedLogger();
var taskCompletionSource = new TaskCompletionSource<string?>();
var byteCount = (encoded.Length + 1) * sizeof(char);
var encodedPtr = Marshal.AllocHGlobal(byteCount);
unsafe
{
var encodedPtrC = (char*)encodedPtr;
fixed (char* src = encoded)
{
Buffer.MemoryCopy(src, encodedPtrC, byteCount, encoded.Length * sizeof(char));
}
encodedPtrC[encoded.Length] = '\0';
}
GCHandle callbackHandle = default;
DecodeStrCallback callback;
unsafe
{
callback = (_, decodePtr) =>
{
try
{
if (decodePtr is null)
{
taskCompletionSource.TrySetResult(null);
return;
}
var decoded = new string(decodePtr);
taskCompletionSource.TrySetResult(decoded);
}
catch (Exception ex)
{
taskCompletionSource.TrySetException(ex);
}
};
callbackHandle = GCHandle.Alloc(callback);
}
try
{
await this.gameThreadService.QueueOnGameThread(() =>
{
unsafe
{
var gameContext = this.gameContextService.GetGameContext();
if (gameContext.IsNull)
{
scopedLogger.LogError("Game context is null");
return;
}
var textParser = gameContext.Pointer->TextParserContext;
if (textParser is null)
{
scopedLogger.LogError("Text parser context is null");
return;
}
var prevLanguage = textParser->Language;
if (language is not Language.Unknown)
{
textParser->Language = language;
}
this.uIContextService.ValidateDecodeString((char*)encodedPtr, callback, nuint.Zero);
textParser->Language = prevLanguage;
}
}, cancellationToken);
return await taskCompletionSource.Task;
}
finally
{
Marshal.FreeHGlobal(encodedPtr);
if (callbackHandle.IsAllocated)
{
callbackHandle.Free();
}
}
}
private static UIAction GetUIActionFromFrameLabel(string frameLabel)
{
return frameLabel switch
@@ -9,6 +9,7 @@ public static class WebApplicationBuilderExtensions
{
public static WebApplicationBuilder WithDaybreakServices(this WebApplicationBuilder builder)
{
builder.Services.AddSingleton<HashingService>();
builder.Services.AddSingleton<IBuildTemplateManager, BuildTemplateManager>();
builder.Services.AddSingleton<IMDomainNameService, MDomainNameService>();
builder.Services.AddHostedService<ApiAdvertisingService>();
@@ -19,6 +20,7 @@ public static class WebApplicationBuilderExtensions
builder.Services.AddSingleton<PartyService>();
builder.Services.AddSingleton<UIService>();
builder.Services.AddSingleton<LoginService>();
builder.Services.AddSingleton<InventoryService>();
builder.WithHookHostedService<GameThreadService>();
builder.WithHookHostedService<ChatHandlingService>();
builder.WithAddressService<SkillbarContextService>();
@@ -0,0 +1,23 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>$(TargetFrameworkVersionWindows)</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<Platforms>x86</Platforms>
<SelfContained>true</SelfContained>
<RuntimeIdentifier>win-x86</RuntimeIdentifier>
<PublishAot>true</PublishAot>
<StripSymbols>true</StripSymbols>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Reloaded.Assembler" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\Daybreak.Shared\Daybreak.Shared.csproj" />
</ItemGroup>
</Project>
+138
View File
@@ -0,0 +1,138 @@
using Daybreak.Shared.Models;
using Daybreak.Shared.Utils;
using System.Diagnostics;
using System.Runtime.InteropServices;
using System.Text;
namespace Daybreak.Injector;
public static class ProcessInjector
{
public static InjectorResponses.InjectResult InjectWithApi(Process process, string pathToDll)
{
var modulefullpath = Path.GetFullPath(pathToDll);
if (!File.Exists(modulefullpath))
{
Console.WriteLine("Dll to inject not found");
return InjectorResponses.InjectResult.InvalidDllPath;
}
var hKernel32 = NativeMethods.GetModuleHandle("kernel32.dll");
if (hKernel32 == IntPtr.Zero)
{
Console.WriteLine("Unable to get a handle of kernel32.dll");
return InjectorResponses.InjectResult.InvalidKernel32;
}
var hLoadLib = NativeMethods.GetProcAddress(hKernel32, "LoadLibraryW");
if (hLoadLib == IntPtr.Zero)
{
Console.WriteLine("Unable to get the address of LoadLibraryW");
return InjectorResponses.InjectResult.InvalidLoadLibraryW;
}
var hStringBuffer = NativeMethods.VirtualAllocEx(process.Handle, IntPtr.Zero, new IntPtr(2 * (modulefullpath.Length + 1)),
0x3000 /* MEM_COMMIT | MEM_RESERVE */, 0x4 /* PAGE_READWRITE */);
if (hStringBuffer == IntPtr.Zero)
{
Console.WriteLine("Unable to allocate memory for module path");
return InjectorResponses.InjectResult.ModulePathAllocationFailed;
}
WriteWString(process, hStringBuffer, modulefullpath);
if (ReadWString(process, hStringBuffer, 260) != modulefullpath)
{
Console.WriteLine("Module path string is not correct");
return InjectorResponses.InjectResult.DllPathWriteFailed;
}
var hThread = NativeMethods.CreateRemoteThread(process.Handle, IntPtr.Zero, 0, hLoadLib, hStringBuffer, 0, out _);
if (hThread == IntPtr.Zero)
{
Console.WriteLine("Unable to create remote thread");
return InjectorResponses.InjectResult.CreateRemoteThreadFailed;
}
var threadResult = NativeMethods.WaitForSingleObject(hThread, 30000u);
if (threadResult is 0x102 or 0xFFFFFFFF /* WAIT_FAILED */)
{
Console.WriteLine($"Exception occurred while waiting for the remote thread. Result is {threadResult}");
return InjectorResponses.InjectResult.RemoteThreadTimeout;
}
var dllResult = NativeMethods.GetExitCodeThread(hThread, out _);
if (dllResult == 0)
{
Console.WriteLine($"Injected dll returned non-success status code {dllResult}");
return InjectorResponses.InjectResult.DllExitCodeFailed;
}
var memoryFreeResult = NativeMethods.VirtualFreeEx(process.Handle, hStringBuffer, 0, 0x8000 /* MEM_RELEASE */);
if (!memoryFreeResult)
{
Console.WriteLine($"Failed to free dll memory");
}
return memoryFreeResult
? InjectorResponses.InjectResult.Success
: InjectorResponses.InjectResult.MemoryFreeFailed;
}
private static void WriteBytes(Process process, IntPtr address, byte[] data)
{
var size = data.Length;
var buffer = Marshal.AllocHGlobal(size);
Marshal.Copy(data, 0, buffer, size);
NativeMethods.WriteProcessMemory(
process.Handle,
address,
buffer,
size,
out _);
Marshal.FreeHGlobal(buffer);
}
private static void WriteWString(Process process, IntPtr address, string data)
{
WriteBytes(process, address, Encoding.Unicode.GetBytes(data));
}
private static string ReadWString(Process process, IntPtr address, int maxsize, Encoding? encoding = null)
{
encoding ??= Encoding.Unicode;
var rawbytes = ReadBytes(process, address, maxsize);
if (rawbytes.Length == 0)
{
return "";
}
var ret = encoding.GetString(rawbytes);
if (ret.Contains('\0'))
{
ret = ret[..ret.IndexOf('\0')];
}
return ret;
}
private static byte[] ReadBytes(Process process, IntPtr address, int size)
{
var buffer = Marshal.AllocHGlobal(size);
NativeMethods.ReadProcessMemory(process.Handle,
address,
buffer,
size,
out _
);
var ret = new byte[size];
Marshal.Copy(buffer, ret, 0, size);
Marshal.FreeHGlobal(buffer);
return ret;
}
}
+248
View File
@@ -0,0 +1,248 @@
using Daybreak.Shared.Models;
using Daybreak.Shared.Utils;
using Microsoft.Win32;
using System.Diagnostics;
using System.Runtime.InteropServices;
using static Daybreak.Shared.Utils.NativeMethods;
namespace Daybreak.Injector;
public sealed class ProcessLauncher
{
public static InjectorResponses.LaunchResult LaunchGuildWars(
string path,
string args,
bool elevated,
out int threadId,
out int processId)
{
var sw = Stopwatch.StartNew();
Process? process;
processId = LaunchClient(path, string.Join(" ", args), elevated, out threadId);
if (processId is 0)
{
Console.WriteLine("Failed to launch GuildWars process.");
return InjectorResponses.LaunchResult.LaunchFailed;
}
do
{
if (sw.Elapsed.TotalSeconds > 10)
{
Console.WriteLine("Timed out waiting for GuildWars process to start.");
return InjectorResponses.LaunchResult.LaunchTimeout;
}
process = Process.GetProcessById(processId);
Thread.Sleep(100);
} while (process is null);
if (!McPatch(process.Handle))
{
var lastErr = Marshal.GetLastWin32Error();
Console.WriteLine($"Failed to patch GuildWars process. Error code: {lastErr}");
return InjectorResponses.LaunchResult.PatchFailed;
}
return InjectorResponses.LaunchResult.Success;
}
private static int LaunchClient(string path, string args, bool elevated, out int threadId)
{
var commandLine = $"\"{path}\" {args}";
threadId = 0;
ProcessInformation procinfo;
var startinfo = new StartupInfo
{
cb = Marshal.SizeOf<StartupInfo>()
};
var saProcess = new SecurityAttributes();
saProcess.nLength = (uint)Marshal.SizeOf(saProcess);
var saThread = new SecurityAttributes();
saThread.nLength = (uint)Marshal.SizeOf(saThread);
var lastDirectory = Directory.GetCurrentDirectory();
Directory.SetCurrentDirectory(Path.GetDirectoryName(path)!);
SetRegistryValue(@"Software\ArenaNet\Guild Wars", "Path", path);
SetRegistryValue(@"Software\ArenaNet\Guild Wars", "Src", path);
if (!elevated)
{
if (!SaferCreateLevel(SaferLevelScope.User, SaferLevel.NormalUser, SaferOpen.Open, out var hLevel, IntPtr.Zero))
{
Debug.WriteLine("SaferCreateLevel");
return 0;
}
if (!SaferComputeTokenFromLevel(hLevel, IntPtr.Zero, out var hRestrictedToken, 0, IntPtr.Zero))
{
Debug.WriteLine("SaferComputeTokenFromLevel");
return 0;
}
SaferCloseLevel(hLevel);
TokenMandatoryLabel tml;
tml.Label.Attributes = 0x20; // SE_GROUP_INTEGRITY
if (!ConvertStringSidToSid("S-1-16-8192", out tml.Label.Sid))
{
CloseHandle(hRestrictedToken);
Debug.WriteLine("ConvertStringSidToSid");
}
if (!SetTokenInformation(
hRestrictedToken,
TokenInformationClass.TokenIntegrityLevel,
ref tml,
(uint)Marshal.SizeOf(tml) + GetLengthSid(tml.Label.Sid)))
{
LocalFree(tml.Label.Sid);
CloseHandle(hRestrictedToken);
return 0;
}
LocalFree(tml.Label.Sid);
if (!CreateProcessAsUser(
hRestrictedToken,
null!,
commandLine,
ref saProcess,
ref saProcess,
false,
(uint)CreationFlags.CreateSuspended,
IntPtr.Zero,
null!,
ref startinfo,
out procinfo))
{
var error = Marshal.GetLastWin32Error();
Debug.WriteLine($"CreateProcessAsUser {error}");
CloseHandle(procinfo.hThread);
return 0;
}
CloseHandle(hRestrictedToken);
}
else
{
if (!CreateProcess(
null!,
commandLine,
ref saProcess,
ref saThread,
false,
(uint)CreationFlags.CreateSuspended,
IntPtr.Zero,
null!,
ref startinfo,
out procinfo))
{
var error = Marshal.GetLastWin32Error();
Debug.WriteLine($"CreateProcess {error}");
_ = ResumeThread(procinfo.hThread);
CloseHandle(procinfo.hThread);
return 0;
}
}
Directory.SetCurrentDirectory(lastDirectory);
threadId = procinfo.dwThreadId;
CloseHandle(procinfo.hThread);
CloseHandle(procinfo.hProcess);
return procinfo.dwProcessId;
}
/// <summary>
/// https://github.com/GregLando113/gwlauncher/blob/master/GW%20Launcher/MulticlientPatch.cs
/// </summary>
private static bool McPatch(IntPtr processHandle)
{
byte[] sigPatch =
[
0x56, 0x57, 0x68, 0x00, 0x01, 0x00, 0x00, 0x89, 0x85, 0xF4, 0xFE, 0xFF, 0xFF, 0xC7, 0x00, 0x00, 0x00, 0x00,
0x00
];
var moduleBase = GetProcessModuleBase(processHandle);
var gwdata = new byte[0x48D000];
if (!NativeMethods.ReadProcessMemory(processHandle, moduleBase, gwdata, gwdata.Length, out _))
{
return false;
}
var idx = SearchBytes(gwdata, sigPatch);
if (idx == -1)
{
return false;
}
var mcpatch = moduleBase + idx - 0x1A;
byte[] payload = [0x31, 0xC0, 0x90, 0xC3];
return NativeMethods.WriteProcessMemory(processHandle, mcpatch, payload, payload.Length, out _);
}
/// <summary>
/// https://github.com/GregLando113/gwlauncher/blob/master/GW%20Launcher/MulticlientPatch.cs
/// </summary>
private static int SearchBytes(Memory<byte> haystack, Memory<byte> needle)
{
var len = needle.Length;
var limit = haystack.Length - len;
for (var i = 0; i <= limit; i++)
{
var k = 0;
for (; k < len; k++)
{
if (needle.Span[k] != haystack.Span[i + k])
{
break;
}
}
if (k == len)
{
return i;
}
}
return -1;
}
/// <summary>
/// https://github.com/GregLando113/gwlauncher/blob/master/GW%20Launcher/MulticlientPatch.cs
/// </summary>
private static IntPtr GetProcessModuleBase(IntPtr process)
{
if (NativeMethods.NtQueryInformationProcess(process, NativeMethods.ProcessInfoClass.ProcessBasicInformation, out var pbi,
Marshal.SizeOf<ProcessBasicInformation>(), out _) != 0)
{
return IntPtr.Zero;
}
var buffer = new byte[Marshal.SizeOf<PEB>()];
if (!NativeMethods.ReadProcessMemory(process, pbi.PebBaseAddress, buffer, Marshal.SizeOf<PEB>(), out _))
{
return IntPtr.Zero;
}
PEB peb = new()
{
ImageBaseAddress = (IntPtr)BitConverter.ToInt32(buffer, 8)
};
return peb.ImageBaseAddress + 0x1000;
}
private static void SetRegistryValue(string registryPath, string valueName, object newValue)
{
using var key = Microsoft.Win32.Registry.CurrentUser.CreateSubKey(registryPath);
key.SetValue(valueName, newValue, RegistryValueKind.String);
}
}
+239
View File
@@ -0,0 +1,239 @@
using Daybreak.Injector;
using Daybreak.Shared.Models;
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
static void PrintUsage()
{
Console.WriteLine("======================================================");
Console.WriteLine("Usage: Daybreak.Injector [mode] <params>");
Console.WriteLine("Modes: ");
Console.WriteLine("- winapi");
Console.WriteLine("- stub");
Console.WriteLine("- launch");
Console.WriteLine("- resume");
Console.WriteLine("Examples:");
Console.WriteLine("1) Daybreak.Injector winapi 1234 C:\\path\\to\\dll.dll");
Console.WriteLine("2) Daybreak.Injector stub 1234 entryPoint C:\\path\\to\\dll.dll");
Console.WriteLine("3) Daybreak.Injector launch true C:\\path\\to\\dll.dll arg1 arg2 arg3");
Console.WriteLine("4) Daybreak.Injector resume 1234");
Console.WriteLine("======================================================");
}
static bool TryParseInjectWinApiArgs(
string[] args,
[NotNullWhen(true)] out Process? process,
[NotNullWhen(true)] out string? dllPath,
out InjectorResponses.InjectResult exitCode)
{
process = default;
dllPath = default;
if (!int.TryParse(args[1], out var processId))
{
PrintUsage();
exitCode = InjectorResponses.InjectResult.InvalidProcess;
return false;
}
if (args.Length < 3)
{
PrintUsage();
exitCode = InjectorResponses.InjectResult.InvalidArgs;
return false;
}
dllPath = Path.GetFullPath(args[2]);
process = Process.GetProcessById(processId);
if (process is null)
{
Console.WriteLine($"Process {processId} could not be found");
exitCode = InjectorResponses.InjectResult.InvalidProcess;
return false;
}
if (!File.Exists(dllPath))
{
Console.WriteLine($"DLL path {dllPath} could not be found");
exitCode = InjectorResponses.InjectResult.InvalidDllPath;
return false;
}
exitCode = 0;
return true;
}
static bool TryParseInjectStubArgs(
string[] args,
[NotNullWhen(true)] out Process? process,
[NotNullWhen(true)] out string? dllPath,
[NotNullWhen(true)] out string? entryPoint,
out InjectorResponses.InjectResult exitCode)
{
process = default;
dllPath = default;
entryPoint = default;
if (!int.TryParse(args[1], out var processId))
{
PrintUsage();
exitCode = InjectorResponses.InjectResult.InvalidProcess;
return false;
}
if (args.Length < 4)
{
PrintUsage();
exitCode = InjectorResponses.InjectResult.InvalidArgs;
return false;
}
entryPoint = args[2];
dllPath = Path.GetFullPath(args[3]);
process = Process.GetProcessById(processId);
if (process is null)
{
Console.WriteLine($"Process {processId} could not be found");
exitCode = InjectorResponses.InjectResult.InvalidProcess;
return false;
}
if (!File.Exists(dllPath))
{
Console.WriteLine($"DLL path {dllPath} could not be found");
exitCode = InjectorResponses.InjectResult.InvalidDllPath;
return false;
}
exitCode = 0;
return true;
}
static bool TryParseLaunchArgs(
string[] args,
[NotNullWhen(true)] out string? gwPath,
[NotNullWhen(true)] out string? gwArgs,
[NotNullWhen(true)] out bool? elevated,
out InjectorResponses.LaunchResult exitCode)
{
gwPath = default;
gwArgs = default;
elevated = default;
if (args.Length < 3)
{
PrintUsage();
exitCode = InjectorResponses.LaunchResult.InvalidArgs;
return false;
}
gwPath = Path.GetFullPath(args[2]);
if (!bool.TryParse(args[1], out var parsedElevated))
{
PrintUsage();
exitCode = InjectorResponses.LaunchResult.InvalidElevated;
return false;
}
elevated = parsedElevated;
gwArgs = args.Length > 3 ? string.Join(" ", args.Skip(3)) : string.Empty;
if (!File.Exists(gwPath))
{
Console.WriteLine($"Guild Wars path {gwPath} could not be found");
exitCode = InjectorResponses.LaunchResult.InvalidPath;
return false;
}
exitCode = 0;
return true;
}
static bool TryParseThreadResumeArgs(
string[] args,
[NotNullWhen(true)] out IntPtr? threadHwnd,
out InjectorResponses.ResumeResult exitCode)
{
threadHwnd = default;
if (args.Length < 2)
{
PrintUsage();
exitCode = InjectorResponses.ResumeResult.InvalidArgs;
return false;
}
if (!IntPtr.TryParse(args[1], out var threadInt))
{
PrintUsage();
exitCode = InjectorResponses.ResumeResult.InvalidThreadHandle;
return false;
}
threadHwnd = threadInt;
exitCode = InjectorResponses.ResumeResult.Success;
return true;
}
if (args.Length < 1)
{
PrintUsage();
return (int)InjectorResponses.GenericResults.InvalidArgs;
}
var mode = args[0];
switch (mode)
{
case "winapi":
{
if (!TryParseInjectWinApiArgs(args, out var process, out var dllPath, out var parseResult))
{
return (int)parseResult;
}
Console.WriteLine($"Starting WinAPI injection. Process {process.Id}. DllPath: {dllPath}");
return (int)ProcessInjector.InjectWithApi(process, dllPath);
}
case "stub":
{
if (!TryParseInjectStubArgs(args, out var process, out var dllPath, out var entryPoint, out var parseResult))
{
return (int)parseResult;
}
Console.WriteLine($"Starting stub injection. Process {process.Id}. EntryPoint {entryPoint}. DllPath: {dllPath}");
var result = StubInjector.Inject(process, dllPath, entryPoint, out var exitCode);
if (result is not InjectorResponses.InjectResult.Success)
{
return (int)result;
}
Console.WriteLine($"Stub returned {exitCode}");
return exitCode;
}
case "launch":
{
if (!TryParseLaunchArgs(args, out var gwPath, out var gwArgs, out var elevated, out var parseResult))
{
return (int)parseResult;
}
Console.WriteLine($"Launching Guild Wars. Path {gwPath}. Elevated {elevated}. Args {gwArgs}");
var result = ProcessLauncher.LaunchGuildWars(gwPath, gwArgs, elevated.Value, out var threadHandle, out var processId);
Console.WriteLine($"ThreadHandle: {threadHandle}");
Console.WriteLine($"ProcessId: {processId}");
return (int)result;
}
case "resume":
{
if (!TryParseThreadResumeArgs(args, out var threadHwnd, out var parseResult))
{
return (int)parseResult;
}
Console.WriteLine($"Resuming thread {threadHwnd.Value}");
return (int) ThreadResumer.Resume(threadHwnd.Value);
}
default:
PrintUsage();
return (int)InjectorResponses.GenericResults.InvalidMode;
}
+183
View File
@@ -0,0 +1,183 @@
using Daybreak.Shared.Models;
using Daybreak.Shared.Utils;
using Reloaded.Assembler;
using System.Diagnostics;
using System.Text;
namespace Daybreak.Injector;
public static class StubInjector
{
/// <summary>
/// ebp+8 is the dll path pointer
/// 0xDEADBEEF is a placeholder to be patched later for LoadLibraryA
/// 0xFEEDF00D is a placeholder to be patched later for GetProcAddress
/// </summary>
const string Asm = @"
use32
push ebp
mov ebp, esp
mov esi, [ebp+8] ; ESI = &INJECT_DATA (kept unchanged)
; -------- LoadLibraryA(dllPath) ------------------------------
push dword [esi] ; dllPath
mov eax, 0xDEADBEEF ; patched → LoadLibraryA
call eax ; EAX = hModule
; -------- GetProcAddress(hModule, funcName) ------------------
push dword [esi+4] ; funcName
push eax ; hModule (still in EAX)
mov eax, 0xFEEDF00D ; patched → GetProcAddress
call eax ; EAX = exported EntryPoint
; -------- call EntryPoint() ----------------------------------
call eax ; calls EntryPoint
; xor eax, eax ; thread exit-code 0
; -------- return EntryPoint() response -----------------------
leave ; = mov esp, ebp / pop ebp
ret 4 ; stdcall: pop lpParameter
";
public static InjectorResponses.InjectResult Inject(Process target, string dllPath, string entryPoint, out int exitCode)
{
var hProcess = NativeMethods.OpenProcess(
NativeMethods.ProcessAccessFlags.All, false, (uint)target.Id);
exitCode = 0;
if (hProcess is 0)
{
Console.WriteLine($"Failed to inject with stub. Could not open process by id {target.Id}");
return InjectorResponses.InjectResult.InvalidProcess;
}
using var assembler = new Assembler();
var stubBytes = assembler.Assemble(Asm);
// patch placeholders
var hKernel = NativeMethods.GetModuleHandle("kernel32.dll");
if (hKernel is 0)
{
Console.WriteLine("Failed to inject with stub. Could not get handle of kernel32.dll");
return InjectorResponses.InjectResult.InvalidKernel32;
}
var pLoadLibA = NativeMethods.GetProcAddress(hKernel, "LoadLibraryA");
if (pLoadLibA is 0)
{
Console.WriteLine("Failed to inject with stub. Could not get address of LoadLibraryA");
return InjectorResponses.InjectResult.InvalidLoadLibraryA;
}
var pGetProc = NativeMethods.GetProcAddress(hKernel, "GetProcAddress");
if (pGetProc is 0)
{
Console.WriteLine("Failed to inject with stub. Could not get address of GetProcAddress");
return InjectorResponses.InjectResult.InvalidGetProcAddress;
}
Patch(stubBytes, 0xDEADBEEF, pLoadLibA);
Patch(stubBytes, 0xFEEDF00D, pGetProc);
var dllBytes = Encoding.ASCII.GetBytes(dllPath + '\0');
var funcBytes = Encoding.ASCII.GetBytes(entryPoint + '\0');
var remoteDll = AllocWrite(hProcess, dllBytes, NativeMethods.MemoryProtection.PAGE_READWRITE);
if (remoteDll is 0)
{
Console.WriteLine("Failed to inject with stub. Could not allocate memory for dll path");
return InjectorResponses.InjectResult.DllPathWriteFailed;
}
var remoteFunc = AllocWrite(hProcess, funcBytes, NativeMethods.MemoryProtection.PAGE_READWRITE);
if (remoteFunc is 0)
{
Console.WriteLine("Failed to inject with stub. Could not allocate memory for function name");
return InjectorResponses.InjectResult.FunctionNameWriteFailed;
}
var injectData = new byte[IntPtr.Size * 2];
BitConverter.GetBytes(remoteDll.ToInt32()).CopyTo(injectData, 0);
BitConverter.GetBytes(remoteFunc.ToInt32()).CopyTo(injectData, 4);
var remoteStruct = AllocWrite(hProcess, injectData, NativeMethods.MemoryProtection.PAGE_READWRITE);
if (remoteStruct is 0)
{
Console.WriteLine("Failed to inject with stub. Could not allocate memory for injectData");
return InjectorResponses.InjectResult.InjectDataWriteFailed;
}
var remoteStub = AllocWrite(hProcess, stubBytes, NativeMethods.MemoryProtection.PAGE_EXECUTE_READ_WRITE);
if (remoteStub is 0)
{
Console.WriteLine("Failed to inject with stub. Could not allocate memory for stub");
return InjectorResponses.InjectResult.StubAllocationFailed;
}
var hThread = NativeMethods.CreateRemoteThread(
hProcess, IntPtr.Zero, 0,
remoteStub,
remoteStruct,
0, out _);
if (hThread is 0)
{
Console.WriteLine("Failed to inject with stub. Could not create remote thread");
return InjectorResponses.InjectResult.CreateRemoteThreadFailed;
}
var waitResult = NativeMethods.WaitForSingleObject(hThread, 10000); // Wait up to 10 seconds
if (waitResult is 0) // WAIT_OBJECT_0 - thread completed
{
// Get the thread exit code, which will be our port number
if (NativeMethods.GetExitCodeThread(hThread, out var moduleExitCode) > 0)
{
Console.WriteLine($"Thread completed with result: {moduleExitCode}");
exitCode = (int)moduleExitCode;
}
else
{
Console.WriteLine("Failed to get thread exit code");
}
}
else
{
Console.WriteLine("Thread did not complete within timeout period");
return InjectorResponses.InjectResult.RemoteThreadTimeout;
}
return InjectorResponses.InjectResult.Success;
}
private static void Patch(byte[] blob, uint marker, IntPtr value)
{
byte m0 = (byte)(marker & 0xFF);
byte m1 = (byte)((marker >> 8) & 0xFF);
byte m2 = (byte)((marker >> 16) & 0xFF);
byte m3 = (byte)((marker >> 24) & 0xFF);
for (int i = 0; i <= blob.Length - 4; i++)
{
if (blob[i] == m0 && blob[i + 1] == m1 &&
blob[i + 2] == m2 && blob[i + 3] == m3)
{
BitConverter.GetBytes(value.ToInt32()).CopyTo(blob, i);
return;
}
}
throw new Exception($"Marker 0x{marker:X8} not found");
}
private static IntPtr AllocWrite(IntPtr hProcess, byte[] data,
NativeMethods.MemoryProtection protect)
{
var addr = NativeMethods.VirtualAllocEx(hProcess, IntPtr.Zero,
(nint)data.Length,
(uint)(NativeMethods.AllocationType.Commit |
NativeMethods.AllocationType.Reserve),
(uint)protect);
NativeMethods.WriteProcessMemory(hProcess, addr, data,
data.Length, out _);
return addr;
}
}
+37
View File
@@ -0,0 +1,37 @@
using Daybreak.Shared.Models;
using Daybreak.Shared.Utils;
using System.Runtime.InteropServices;
namespace Daybreak.Injector;
public static class ThreadResumer
{
public static InjectorResponses.ResumeResult Resume(IntPtr threadId)
{
if (threadId == 0)
{
return InjectorResponses.ResumeResult.InvalidThreadHandle;
}
var threadHandle = NativeMethods.OpenThread(NativeMethods.ThreadAccess.SuspendResume, false, (uint)threadId);
if (threadHandle == IntPtr.Zero)
{
var openErr = Marshal.GetLastWin32Error();
Console.WriteLine($"OpenThread failed. Error: {openErr}");
return InjectorResponses.ResumeResult.InvalidThreadHandle;
}
var result = NativeMethods.ResumeThread(threadHandle);
Console.WriteLine($"ResumeThread result: {result}");
NativeMethods.CloseHandle(threadHandle);
if (result == uint.MaxValue)
{
var resumeErr = Marshal.GetLastWin32Error();
Console.WriteLine($"ResumeThread failed. Error: {resumeErr}");
return InjectorResponses.ResumeResult.InvalidThreadHandle;
}
return InjectorResponses.ResumeResult.Success;
}
}
+4 -3
View File
@@ -2,14 +2,15 @@
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net9.0</TargetFramework>
<TargetFramework>$(TargetFrameworkVersion)</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<Platforms>AnyCPU;x86</Platforms>
<Platforms>x64</Platforms>
<Self-Contained>True</Self-Contained>
<RuntimeIdentifier>win-x86</RuntimeIdentifier>
<RuntimeIdentifier>win-x64</RuntimeIdentifier>
<PublishAot>true</PublishAot>
<StripSymbols>true</StripSymbols>
<PublishSingleFile>true</PublishSingleFile>
</PropertyGroup>
</Project>
+289 -98
View File
@@ -1,9 +1,9 @@
// See https://aka.ms/new-console-template for more information
using System.Diagnostics;
using System.Diagnostics;
using System.IO.Compression;
using System.Net;
using System.Text;
var cts = new CancellationTokenSource();
const string LatestUrl = "https://github.com/gwdevhub/Daybreak/releases/latest";
static void RenderProgressBar(int currentStep, int totalSteps, int barSize)
{
@@ -20,9 +20,278 @@ static void RenderProgressBar(int currentStep, int totalSteps, int barSize)
Console.Write($"{pctComplete:P0}"); // Display the percentage completed
}
const string tempFile = "tempfile.zip";
const string updatePkg = "update.pkg";
const string executableName = "Daybreak.exe";
static void CleanWorkingDirectory(string workingDirectory)
{
if (!Directory.Exists(workingDirectory))
{
return;
}
var installerPath = Path.GetFullPath(Path.Combine(workingDirectory, "Installer"));
var optionsPath = Path.GetFullPath(Path.Combine(workingDirectory, "Daybreak.options"));
var preserve = new HashSet<string>(StringComparer.OrdinalIgnoreCase)
{
installerPath,
optionsPath
};
var preserveExtensions = new HashSet<string>(StringComparer.OrdinalIgnoreCase)
{
".tpf"
};
foreach (var entry in Directory.GetFileSystemEntries(workingDirectory))
{
var fullPath = Path.GetFullPath(entry);
if (preserve.Contains(fullPath))
{
continue;
}
if (preserveExtensions.Contains(Path.GetExtension(fullPath)))
{
continue;
}
try
{
if (Directory.Exists(fullPath))
{
Directory.Delete(fullPath, true);
}
else if (File.Exists(fullPath))
{
File.Delete(fullPath);
}
}
catch (Exception e)
{
Console.WriteLine($"Failed to delete {fullPath}.\n{e}");
}
}
}
static async ValueTask PerformUpdate(string workingDirectory)
{
if (!Directory.Exists(workingDirectory))
{
Console.WriteLine("Working directory does not exist");
Console.ReadKey();
return;
}
Console.WriteLine($"Working directory: {workingDirectory}");
var tempFile = Path.GetFullPath("tempfile.zip", workingDirectory);
var updatePkg = Path.GetFullPath("update.pkg", workingDirectory);
var executableName = Path.GetFullPath("Daybreak.exe", workingDirectory);
if (File.Exists(updatePkg))
{
Console.WriteLine("Unpacking files...");
using var fileStream = new FileStream(updatePkg, FileMode.Open);
var copyBuffer = new Memory<byte>(new byte[4096]);
while (fileStream.Position < fileStream.Length - 1)
{
/*
* For each file downloaded, we write to the package the following:
* Size of the name string
* UTF-8 encoded name string
* Size of the relative path string
* UTF-8 encoded relative path string
* Size of the binary
* Binary data
*/
var sizeBuffer = copyBuffer[..4];
await fileStream.ReadExactlyAsync(sizeBuffer);
var fileNameSize = BitConverter.ToInt32(sizeBuffer.Span);
var fileNameBuffer = copyBuffer[..fileNameSize];
await fileStream.ReadExactlyAsync(fileNameBuffer);
var fileName = Encoding.UTF8.GetString(fileNameBuffer.Span);
RenderProgressBar((int)fileStream.Position, (int)fileStream.Length, 40);
await fileStream.ReadExactlyAsync(sizeBuffer);
var relativePathSize = BitConverter.ToInt32(sizeBuffer.Span);
var relativePathBuffer = copyBuffer[..relativePathSize];
await fileStream.ReadExactlyAsync(relativePathBuffer);
var relativePath = Encoding.UTF8.GetString(relativePathBuffer.Span);
RenderProgressBar((int)fileStream.Position, (int)fileStream.Length, 40);
await fileStream.ReadExactlyAsync(sizeBuffer);
var binarySize = BitConverter.ToInt32(sizeBuffer.Span);
var fileInfo = new FileInfo(relativePath);
fileInfo.Directory!.Create();
using var destinationStream = new FileStream(Path.GetFullPath(relativePath, workingDirectory), FileMode.Create);
while (binarySize > 0)
{
var toRead = Math.Min(binarySize, copyBuffer.Length);
var readBuffer = copyBuffer[..toRead];
await fileStream.ReadExactlyAsync(readBuffer);
await destinationStream.WriteAsync(readBuffer);
binarySize -= toRead;
RenderProgressBar((int)fileStream.Position, (int)fileStream.Length, 40);
}
}
fileStream.Close();
fileStream.Dispose();
File.Delete(updatePkg);
}
else if (File.Exists(tempFile))
{
Console.WriteLine("Unpacking files...");
try
{
ZipFile.ExtractToDirectory(tempFile, Path.GetFullPath(workingDirectory), true);
}
catch
{
}
Console.WriteLine("Deleting package");
try
{
File.Delete(tempFile);
}
catch (Exception e)
{
Console.WriteLine($"Failed to delete {tempFile}.\n{e}");
}
}
else
{
Console.ForegroundColor = ConsoleColor.Red;
Console.WriteLine("Unable to find launcher package. Aborting installation");
Console.ReadKey();
return;
}
Console.WriteLine("Launching application");
var process = new Process
{
StartInfo = new ProcessStartInfo
{
FileName = executableName
}
};
if (process.Start() is false)
{
Console.WriteLine("Failed to launch application");
Console.ReadKey();
}
}
static async ValueTask PerformFreshInstall(string workingDirectory)
{
using var handler = new HttpClientHandler
{
AllowAutoRedirect = false
};
var client = new HttpClient(handler);
using var latestResponse = await client.GetAsync(LatestUrl);
var isRedirect = latestResponse.StatusCode is HttpStatusCode.Redirect
or HttpStatusCode.MovedPermanently
or HttpStatusCode.Found
or HttpStatusCode.SeeOther
or HttpStatusCode.TemporaryRedirect
or HttpStatusCode.PermanentRedirect;
if (!isRedirect)
{
Console.WriteLine($"Failed to get latest release information. Status: {latestResponse.StatusCode}");
Console.ReadKey();
return;
}
var redirectTarget = latestResponse.Headers.Location;
if (redirectTarget is null)
{
Console.WriteLine("Latest release redirect location missing.");
Console.ReadKey();
return;
}
var finalUri = redirectTarget.IsAbsoluteUri
? redirectTarget
: new Uri(latestResponse.RequestMessage!.RequestUri!, redirectTarget);
var version = finalUri.Segments.Last().TrimEnd('/');
var downloadUri = new Uri($"https://github.com/gwdevhub/Daybreak/releases/download/{version}/daybreak{version}.zip");
var tempFile = Path.GetFullPath("tempfile.zip", workingDirectory);
Console.WriteLine($"Cleaning installation directory {workingDirectory}");
CleanWorkingDirectory(workingDirectory);
Console.WriteLine($"Downloading Daybreak {version}...");
handler.Dispose();
client.Dispose();
client = new HttpClient();
using var downloadResponse = await client.GetAsync(downloadUri, HttpCompletionOption.ResponseHeadersRead);
if (!downloadResponse.IsSuccessStatusCode)
{
Console.WriteLine($"Failed to download Daybreak package. Status: {downloadResponse.StatusCode}");
Console.ReadKey();
return;
}
var totalBytes = downloadResponse.Content.Headers.ContentLength ?? -1L;
using var contentStream = await downloadResponse.Content.ReadAsStreamAsync();
using var fileStream = new FileStream(tempFile, FileMode.Create, FileAccess.Write, FileShare.None);
var totalRead = 0L;
var buffer = new Memory<byte>(new byte[8192]);
int bytesRead;
while ((bytesRead = await contentStream.ReadAsync(buffer)) > 0)
{
await fileStream.WriteAsync(buffer[..bytesRead]);
totalRead += bytesRead;
if (totalBytes != -1)
{
RenderProgressBar((int)totalRead, (int)totalBytes, 40);
}
}
fileStream.Close();
fileStream.Dispose();
client.Dispose();
Console.WriteLine("\nDownload complete. Extracting files...");
try
{
ZipFile.ExtractToDirectory(tempFile, Path.GetFullPath(workingDirectory), true);
}
catch (Exception e)
{
Console.WriteLine($"Failed to extract package.\n{e}");
Console.ReadKey();
return;
}
File.Delete(tempFile);
Console.WriteLine("Installation complete. Launching application...");
var executableName = Path.GetFullPath("Daybreak.exe", workingDirectory);
var process = new Process
{
StartInfo = new ProcessStartInfo
{
FileName = executableName
}
};
if (process.Start() is false)
{
Console.WriteLine("Failed to launch application");
Console.ReadKey();
}
}
Console.Title = "Daybreak Installer";
Console.WriteLine("Starting installation...");
while (Process.GetProcesses().Where(p => p.ProcessName == "Daybreak").FirstOrDefault()?.HasExited is false)
@@ -31,102 +300,24 @@ while (Process.GetProcesses().Where(p => p.ProcessName == "Daybreak").FirstOrDef
await Task.Delay(5000);
}
if (File.Exists(updatePkg))
while (Process.GetProcessesByName("gw").FirstOrDefault()?.HasExited is false)
{
Console.WriteLine("Unpacking files...");
using var fileStream = new FileStream(updatePkg, FileMode.Open);
var copyBuffer = new Memory<byte>(new byte[4096]);
while (fileStream.Position < fileStream.Length - 1)
{
/*
* For each file downloaded, we write to the package the following:
* Size of the name string
* UTF-8 encoded name string
* Size of the relative path string
* UTF-8 encoded relative path string
* Size of the binary
* Binary data
*/
var sizeBuffer = copyBuffer[..4];
await fileStream.ReadExactlyAsync(sizeBuffer);
var fileNameSize = BitConverter.ToInt32(sizeBuffer.Span);
var fileNameBuffer = copyBuffer[..fileNameSize];
await fileStream.ReadExactlyAsync(fileNameBuffer);
var fileName = Encoding.UTF8.GetString(fileNameBuffer.Span);
RenderProgressBar((int)fileStream.Position, (int)fileStream.Length, 40);
await fileStream.ReadExactlyAsync(sizeBuffer);
var relativePathSize = BitConverter.ToInt32(sizeBuffer.Span);
var relativePathBuffer = copyBuffer[..relativePathSize];
await fileStream.ReadExactlyAsync(relativePathBuffer);
var relativePath = Encoding.UTF8.GetString(relativePathBuffer.Span);
RenderProgressBar((int)fileStream.Position, (int)fileStream.Length, 40);
await fileStream.ReadExactlyAsync(sizeBuffer);
var binarySize = BitConverter.ToInt32(sizeBuffer.Span);
var fileInfo = new FileInfo(relativePath);
fileInfo.Directory!.Create();
using var destinationStream = new FileStream(relativePath, FileMode.Create);
while(binarySize > 0)
{
var toRead = Math.Min(binarySize, copyBuffer.Length);
var readBuffer = copyBuffer[..toRead];
await fileStream.ReadExactlyAsync(readBuffer);
await destinationStream.WriteAsync(readBuffer);
binarySize -= toRead;
RenderProgressBar((int)fileStream.Position, (int)fileStream.Length, 40);
}
}
fileStream.Close();
fileStream.Dispose();
File.Delete(updatePkg);
Console.WriteLine($"Detected Guild Wars process is still running. Waiting 5s and retrying");
await Task.Delay(5000);
}
else if (File.Exists(tempFile))
{
Console.WriteLine("Unpacking files...");
try
{
ZipFile.ExtractToDirectory(tempFile, AppContext.BaseDirectory, true);
}
catch
{
}
Console.WriteLine("Deleting package");
try
{
File.Delete(tempFile);
}
catch (Exception e)
{
Console.WriteLine($"Failed to delete {tempFile}.\n{e}");
}
var mode = args.Length > 1
? args[0] : "install";
var workingDirectory = args.Length > 2
? args[1] : Path.GetFullPath("..", AppContext.BaseDirectory);
// Normalize working directory path
workingDirectory = Path.GetFullPath(workingDirectory);
if (mode is "install")
{
await PerformFreshInstall(workingDirectory);
}
else
{
Console.ForegroundColor = ConsoleColor.Red;
Console.WriteLine("Unable to find launcher package. Aborting installation");
Console.ReadKey();
return;
}
Console.WriteLine("Launching application");
var process = new Process
{
StartInfo = new ProcessStartInfo
{
FileName = executableName
}
};
if (process.Start() is false)
{
Console.WriteLine("Failed to launch application");
Console.ReadKey();
await PerformUpdate(workingDirectory);
}
@@ -1,6 +1,6 @@
using Daybreak.Shared.Validators;
namespace Daybreak.Attributes;
namespace Daybreak.Shared.Attributes;
[AttributeUsage(AttributeTargets.Property)]
public sealed class OptionCustomValidatorAttribute<T> : Attribute
@@ -1,4 +1,4 @@
namespace Daybreak.Attributes;
namespace Daybreak.Shared.Attributes;
[AttributeUsage(AttributeTargets.Property)]
public sealed class OptionIgnoreAttribute : Attribute
@@ -1,4 +1,4 @@
namespace Daybreak.Attributes;
namespace Daybreak.Shared.Attributes;
[AttributeUsage(AttributeTargets.Property)]
public sealed class OptionNameAttribute : Attribute
@@ -1,4 +1,4 @@
namespace Daybreak.Attributes;
namespace Daybreak.Shared.Attributes;
[AttributeUsage(AttributeTargets.Property)]
public sealed class OptionRangeAttribute<T> : Attribute
@@ -1,4 +1,4 @@
namespace Daybreak.Attributes;
namespace Daybreak.Shared.Attributes;
[AttributeUsage(AttributeTargets.Property)]
public sealed class OptionSynchronizationIgnoreAttribute : Attribute
@@ -0,0 +1,7 @@
namespace Daybreak.Shared.Attributes;
[AttributeUsage(AttributeTargets.Property)]
public class OptionValuesAttribute : Attribute
{
public required object[] Values { get; init; }
}
@@ -0,0 +1,7 @@
namespace Daybreak.Shared.Attributes;
[AttributeUsage(AttributeTargets.Property)]
public abstract class OptionValuesFactoryAttribute : Attribute
{
public abstract List<object> ValuesFactory();
}
@@ -1,4 +1,4 @@
namespace Daybreak.Attributes;
namespace Daybreak.Shared.Attributes;
[AttributeUsage(AttributeTargets.Class)]
public sealed class OptionsIgnoreAttribute : Attribute
@@ -1,4 +1,4 @@
namespace Daybreak.Attributes;
namespace Daybreak.Shared.Attributes;
[AttributeUsage(AttributeTargets.Class)]
public sealed class OptionsNameAttribute : Attribute
@@ -1,4 +1,4 @@
namespace Daybreak.Attributes;
namespace Daybreak.Shared.Attributes;
[AttributeUsage(AttributeTargets.Class)]
public sealed class OptionsSynchronizationIgnoreAttribute : Attribute
@@ -1,50 +0,0 @@
using Newtonsoft.Json;
namespace Daybreak.Shared.Converters;
public sealed class AttributeJsonConverter : JsonConverter
{
public override bool CanConvert(Type objectType) => true;
public override object? ReadJson(JsonReader reader, Type objectType, object? existingValue, JsonSerializer serializer)
{
if (!objectType.IsAssignableTo(typeof(Models.Guildwars.Attribute)))
{
return default;
}
switch (reader.TokenType)
{
case JsonToken.String:
var name = reader.ReadAsString();
if (name is null ||
!Models.Guildwars.Attribute.TryParse(name, out var namedAttribute))
{
return default;
}
return namedAttribute;
case JsonToken.Integer:
var id = reader.Value as long?;
if (id is not long ||
!Models.Guildwars.Attribute.TryParse((int)id.Value, out var parsedAttribute))
{
return default;
}
return parsedAttribute;
default:
return default;
}
}
public override void WriteJson(JsonWriter writer, object? value, JsonSerializer serializer)
{
if (value is not Models.Guildwars.Attribute attribute)
{
return;
}
writer.WriteValue(attribute.Id);
}
}
@@ -1,32 +0,0 @@
using System.Globalization;
using System.Windows.Data;
using System.Windows.Media.Effects;
namespace Daybreak.Shared.Converters;
public sealed class BooleanToEffectConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
{
if (value is not bool boolean)
{
return default!;
}
if (parameter is not Effect effect)
{
return default!;
}
if (!boolean)
{
return default!;
}
return effect;
}
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
{
throw new NotImplementedException();
}
}
@@ -1,42 +0,0 @@
using System.Extensions;
using System.Globalization;
using System.Windows;
using System.Windows.Data;
namespace Daybreak.Shared.Converters;
public sealed class BooleanToGridLengthConverter : IValueConverter
{
private static GridLength Collapsed = new(0);
public GridLength VisibleValue { get; set; }
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
{
return this.GetVisibility(value);
}
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
{
throw new NotImplementedException();
}
private GridLength GetVisibility(object value)
{
if (value is not bool)
{
return this.VisibleValue;
}
var objValue = value.Cast<bool>();
if (objValue)
{
return this.VisibleValue;
}
else
{
return Collapsed;
}
}
}
@@ -1,52 +0,0 @@
using System.Extensions;
using System.Globalization;
using System.Windows;
using System.Windows.Data;
namespace Daybreak.Shared.Converters;
public class BooleanToVisibilityConverter : IValueConverter
{
/// <summary>
/// Set to true if you want to show control when boolean value is true.
/// Set to false if you want to hide/collapse control when value is true.
/// </summary>
public bool TriggerValue { get; set; } = true;
/// <summary>
/// Set to true if you just want to hide the control, else set to false if you want to collapse the control.
/// </summary>
public bool IsHidden { get; set; }
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
{
return this.GetVisibility(value);
}
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
{
throw new NotImplementedException();
}
private object GetVisibility(object value)
{
if (value is not bool)
{
return this.IsHidden ?
Visibility.Hidden :
Visibility.Collapsed;
}
var objValue = value.Cast<bool>();
if ((objValue && this.TriggerValue && this.IsHidden) || (!objValue && !this.TriggerValue && this.IsHidden))
{
return Visibility.Hidden;
}
if ((objValue && this.TriggerValue && !this.IsHidden) || (!objValue && !this.TriggerValue && !this.IsHidden))
{
return Visibility.Collapsed;
}
return Visibility.Visible;
}
}
@@ -1,51 +0,0 @@
using Daybreak.Shared.Models.Guildwars;
using Newtonsoft.Json;
namespace Daybreak.Shared.Converters;
public sealed class CampaignJsonConverter : JsonConverter
{
public override bool CanConvert(Type objectType) => true;
public override object? ReadJson(JsonReader reader, Type objectType, object? existingValue, JsonSerializer serializer)
{
if (!objectType.IsAssignableTo(typeof(Campaign)))
{
return default;
}
switch (reader.TokenType)
{
case JsonToken.String:
var name = reader.ReadAsString();
if (name is null ||
!Campaign.TryParse(name, out var namedCampaign))
{
return default;
}
return namedCampaign;
case JsonToken.Integer:
var id = reader.Value as long?;
if (id is not long ||
!Campaign.TryParse((int)id.Value, out var parsedCampaign))
{
return default;
}
return parsedCampaign;
default:
return default;
}
}
public override void WriteJson(JsonWriter writer, object? value, JsonSerializer serializer)
{
if (value is not Campaign campaign)
{
return;
}
writer.WriteValue(campaign.Id);
}
}
@@ -1,51 +0,0 @@
using Daybreak.Shared.Models.Guildwars;
using Newtonsoft.Json;
namespace Daybreak.Shared.Converters;
public sealed class ContinentJsonConverter : JsonConverter
{
public override bool CanConvert(Type objectType) => true;
public override object? ReadJson(JsonReader reader, Type objectType, object? existingValue, JsonSerializer serializer)
{
if (!objectType.IsAssignableTo(typeof(Continent)))
{
return default;
}
switch (reader.TokenType)
{
case JsonToken.String:
var name = reader.ReadAsString();
if (name is null ||
!Continent.TryParse(name, out var namedContinent))
{
return default;
}
return namedContinent;
case JsonToken.Integer:
var id = reader.Value as long?;
if (id is not long ||
!Continent.TryParse((int)id.Value, out var parsedContinent))
{
return default;
}
return parsedContinent;
default:
return default;
}
}
public override void WriteJson(JsonWriter writer, object? value, JsonSerializer serializer)
{
if (value is not Continent continent)
{
return;
}
writer.WriteValue(continent.Id);
}
}
@@ -1,40 +0,0 @@
using System.Globalization;
using System.Windows.Data;
namespace Daybreak.Shared.Converters;
public sealed class DateTimeConverter : IValueConverter
{
public string? Format { get; set; }
public bool LongTime { get; set; }
public bool LongDate { get; set; }
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
{
if (value is not DateTime dateTime)
{
return string.Empty;
}
if (this.Format is not null)
{
return dateTime.ToString(this.Format);
}
if (this.LongTime)
{
return dateTime.ToLongTimeString();
}
if (this.LongDate)
{
return dateTime.ToLongDateString();
}
return dateTime.ToShortDateString();
}
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
{
throw new InvalidOperationException();
}
}
@@ -1,23 +0,0 @@
using System.Globalization;
using System.Windows.Data;
namespace Daybreak.Shared.Converters;
public sealed class DoubleMultiplierConverter : IValueConverter
{
public double Multiplier { get; set; }
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
{
if (value is not double d)
{
return value;
}
return d * this.Multiplier;
}
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
{
throw new NotImplementedException();
}
}
@@ -0,0 +1,39 @@
using System.ComponentModel;
using System.Diagnostics.CodeAnalysis;
using System.Globalization;
namespace Daybreak.Shared.Converters;
public sealed class EnumToStringConverter<TEnumType> : TypeConverter
where TEnumType : struct, Enum
{
public override bool CanConvertFrom(ITypeDescriptorContext? context, Type sourceType)
{
return sourceType == typeof(string) || base.CanConvertFrom(context, sourceType);
}
public override bool CanConvertTo(ITypeDescriptorContext? context, [NotNullWhen(true)] Type? destinationType)
{
return destinationType == typeof(string) || base.CanConvertTo(context, destinationType);
}
public override object? ConvertFrom(ITypeDescriptorContext? context, CultureInfo? culture, object value)
{
if (value is string stringValue && Enum.TryParse<TEnumType>(stringValue, out var enumValue))
{
return enumValue;
}
return base.ConvertFrom(context, culture, value);
}
public override object? ConvertTo(ITypeDescriptorContext? context, CultureInfo? culture, object? value, Type destinationType)
{
if (destinationType == typeof(string) && value is TEnumType enumValue)
{
return enumValue.ToString();
}
return base.ConvertTo(context, culture, value, destinationType);
}
}
@@ -1,24 +0,0 @@
using System.Globalization;
using System.Windows;
using System.Windows.Data;
namespace Daybreak.Shared.Converters;
public sealed class EqualityToVisibilityConverter : IMultiValueConverter
{
public object Convert(object[] values, Type targetType, object parameter, CultureInfo culture)
{
if (values.Length != 2)
{
return Visibility.Collapsed;
}
return values[0] == values[1] ? Visibility.Visible : Visibility.Collapsed;
}
public object[] ConvertBack(object value, Type[] targetTypes, object parameter, CultureInfo culture)
{
throw new NotImplementedException();
}
}
@@ -1,68 +0,0 @@
using Daybreak.Shared.Models.Guildwars;
using Daybreak.Shared.Services.Events;
using Microsoft.Extensions.DependencyInjection;
using System.Globalization;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Media;
using System.Windows.Shapes;
namespace Daybreak.Shared.Converters;
public sealed class EventCalendarDayToBrushConverter : IValueConverter
{
private readonly EventToBrushConverter eventToBrushConverter = new();
private readonly IEventService eventService = Global.GlobalServiceProvider.GetRequiredService<IEventService>();
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
{
if (value is not DateTime dayDate)
{
throw new InvalidOperationException($"Unable to convert from {value.GetType().Name}");
}
dayDate = new DateTime(dayDate.Year, dayDate.Month, dayDate.Day, 19, 0, 0);
var activeEvents = this.eventService.GetActiveEvents(dayDate).ToList();
if (activeEvents.Count == 0)
{
return Brushes.Transparent;
}
if (activeEvents.Count == 1 &&
activeEvents.FirstOrDefault() is Event ev)
{
return this.eventToBrushConverter.Convert(ev, typeof(Brush), parameter, culture);
}
var brushes = this.eventService.GetActiveEvents(dayDate).Select(e => this.eventToBrushConverter.Convert(e, typeof(Brush), parameter, culture)).OfType<SolidColorBrush>().ToList();
var canvas = new Canvas();
double width = 1.0 / brushes.Count; // Assuming a normalized width for each color segment
for (int i = 0; i < brushes.Count; i++)
{
var rect = new Rectangle
{
Width = width,
Height = 1,
Fill = brushes[i],
RenderTransform = new TranslateTransform(i * width, 0)
};
canvas.Children.Add(rect);
}
return new VisualBrush
{
Visual = canvas,
Stretch = Stretch.None,
TileMode = TileMode.None,
Viewbox = new Rect(0, 0, 1, 1),
ViewboxUnits = BrushMappingMode.RelativeToBoundingBox,
Transform = new RotateTransform(45, 0.5, 0.5)
};
}
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
{
throw new NotImplementedException();
}
}
@@ -1,44 +0,0 @@
using Daybreak.Shared.Models;
using Daybreak.Shared.Models.Guildwars;
using System.Globalization;
using System.Windows.Data;
using System.Windows.Media;
using Brush = System.Windows.Media.Brush;
namespace Daybreak.Shared.Converters;
public sealed class EventToBrushConverter : IValueConverter
{
private static readonly Dictionary<Event, Brush> EventMapping = new()
{
{ Event.CanthanNewYear, new SolidColorBrush(ColorPalette.Amber) },
{ Event.LuckyTreatsWeek, new SolidColorBrush(ColorPalette.Green) },
{ Event.AprilFoolsDay, new SolidColorBrush(ColorPalette.DeepPurple) },
{ Event.SweetTreatsWeek, new SolidColorBrush(ColorPalette.Indigo) },
{ Event.AnniversaryCelebration, new SolidColorBrush(ColorPalette.Yellow) },
{ Event.DragonFestival, new SolidColorBrush(ColorPalette.DeepOrange) },
{ Event.WintersdayInJuly, new SolidColorBrush(ColorPalette.LightBlue) },
{ Event.WayfarersReverie, new SolidColorBrush(ColorPalette.Magenta) },
{ Event.PirateWeek, new SolidColorBrush(ColorPalette.Gold) },
{ Event.BreastCancerAwarenessMonth, new SolidColorBrush(ColorPalette.Pink) },
{ Event.Halloween, new SolidColorBrush(ColorPalette.Orange) },
{ Event.SpecialTreatsWeek, new SolidColorBrush(ColorPalette.Lime) },
{ Event.WintersdayJanuary, new SolidColorBrush(ColorPalette.BlueGrey) },
{ Event.WintersdayDecember, new SolidColorBrush(ColorPalette.BlueGrey) }
};
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
{
if (value is not Event e ||
targetType != typeof(Brush))
{
throw new InvalidOperationException($"Unable to convert from {value.GetType().Name} to {targetType.Name}");
}
return EventMapping[e];
}
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
{
throw new NotImplementedException();
}
}
@@ -0,0 +1,26 @@
using System.Text.Json;
using System.Text.Json.Serialization;
namespace Daybreak.Shared.Converters;
public sealed class HexIntConverter : JsonConverter<int>
{
public override int Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
{
if (reader.TokenType == JsonTokenType.String)
{
var hexString = reader.GetString();
if (hexString is not null && hexString.StartsWith("0x", StringComparison.OrdinalIgnoreCase))
{
return Convert.ToInt32(hexString[2..], 16);
}
}
return reader.GetInt32();
}
public override void Write(Utf8JsonWriter writer, int value, JsonSerializerOptions options)
{
writer.WriteStringValue($"0x{value:X4}");
}
}
@@ -0,0 +1,36 @@
using System.Text.Json;
using System.Text.Json.Serialization;
namespace Daybreak.Shared.Converters;
public sealed class HexUIntArrayJsonConverter : JsonConverter<uint[]>
{
private static readonly HexUintConverter ElementConverter = new();
public override uint[]? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
{
if (reader.TokenType != JsonTokenType.StartArray)
{
throw new JsonException();
}
var list = new List<uint>();
while (reader.Read() && reader.TokenType != JsonTokenType.EndArray)
{
list.Add(ElementConverter.Read(ref reader, typeof(uint), options));
}
return [.. list];
}
public override void Write(Utf8JsonWriter writer, uint[] value, JsonSerializerOptions options)
{
writer.WriteStartArray();
foreach (var item in value)
{
ElementConverter.Write(writer, item, options);
}
writer.WriteEndArray();
}
}
@@ -0,0 +1,26 @@
using System.Text.Json;
using System.Text.Json.Serialization;
namespace Daybreak.Shared.Converters;
public sealed class HexUintConverter : JsonConverter<uint>
{
public override uint Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
{
if (reader.TokenType == JsonTokenType.String)
{
var hexString = reader.GetString();
if (hexString is not null && hexString.StartsWith("0x", StringComparison.OrdinalIgnoreCase))
{
return Convert.ToUInt32(hexString[2..], 16);
}
}
return reader.GetUInt32();
}
public override void Write(Utf8JsonWriter writer, uint value, JsonSerializerOptions options)
{
writer.WriteStringValue($"0x{value:X4}");
}
}
@@ -1,30 +0,0 @@
using System.Globalization;
using System.Windows;
using System.Windows.Data;
namespace Daybreak.Shared.Converters;
public class HiddenWhenNull : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
{
return GetVerticalAlignment(value);
}
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
{
throw new NotImplementedException();
}
private static object GetVerticalAlignment(object value)
{
if (value is null)
{
return Visibility.Hidden;
}
else
{
return Visibility.Visible;
}
}
}
@@ -1,43 +0,0 @@
using System.Globalization;
using System.Windows.Data;
namespace Daybreak.Shared.Converters;
public sealed class IntToStringConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
{
if (targetType == typeof(string) &&
value is int i)
{
return i.ToString();
}
else if(targetType == typeof(int) &&
value is string s)
{
return int.Parse(s);
}
else
{
throw new InvalidOperationException($"Unable to convert from {value.GetType().Name} to {targetType.Name}");
}
}
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
{
if (targetType == typeof(string) &&
value is int i)
{
return i.ToString();
}
else if (targetType == typeof(int) &&
value is string s)
{
return int.Parse(s);
}
else
{
throw new InvalidOperationException($"Unable to convert from {value.GetType().Name} to {targetType.Name}");
}
}
}
@@ -1,23 +0,0 @@
using System.Windows.Data;
namespace Daybreak.Shared.Converters;
public class InverseBooleanConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter,
System.Globalization.CultureInfo culture)
{
if (targetType != typeof(bool))
{
throw new InvalidOperationException("The target must be a boolean");
}
return !(bool)value;
}
public object ConvertBack(object value, Type targetType, object parameter,
System.Globalization.CultureInfo culture)
{
throw new NotSupportedException();
}
}
@@ -1,51 +0,0 @@
using Daybreak.Shared.Models.Guildwars;
using Newtonsoft.Json;
namespace Daybreak.Shared.Converters;
public sealed class ItemBaseJsonConverter : JsonConverter
{
public override bool CanConvert(Type objectType) => true;
public override object? ReadJson(JsonReader reader, Type objectType, object? existingValue, JsonSerializer serializer)
{
if (!objectType.IsAssignableTo(typeof(ItemBase)))
{
return default;
}
switch (reader.TokenType)
{
case JsonToken.String:
var name = reader.ReadAsString();
if (name is null ||
!ItemBase.TryParse(name, out var namedItem))
{
return default;
}
return namedItem;
case JsonToken.Integer:
var id = reader.Value as long?;
if (id is not long ||
!ItemBase.TryParse((int)id.Value, null, out var parsedItem))
{
return default;
}
return parsedItem;
default:
return default;
}
}
public override void WriteJson(JsonWriter writer, object? value, JsonSerializer serializer)
{
if (value is not ItemBase itemBase)
{
return;
}
writer.WriteValue(itemBase.Id);
}
}
@@ -1,35 +0,0 @@
using Daybreak.Shared.Models.Guildwars;
using System.Globalization;
using System.Windows.Data;
namespace Daybreak.Shared.Converters;
public sealed class ItemBaseToIntConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
{
if (value is int i)
{
return ItemBase.Parse(i, modifiers: null);
}
else if(value is ItemBase itemBase)
{
return itemBase.Id;
}
throw new InvalidOperationException($"Unable to convert value of type {value.GetType().Name}");
}
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
{
if (value is int i)
{
return ItemBase.Parse(i, modifiers: null);
}
else if (value is ItemBase itemBase)
{
return itemBase.Id;
}
throw new InvalidOperationException($"Unable to convert value of type {value.GetType().Name}");
}
}
@@ -1,35 +0,0 @@
using Daybreak.Shared.Models.Guildwars;
using System.Globalization;
using System.Windows.Data;
namespace Daybreak.Shared.Converters;
public sealed class ItemBaseToStringConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
{
if (value is string s)
{
return ItemBase.Parse(s);
}
else if(value is ItemBase itemBase)
{
return itemBase.Name ?? string.Empty;
}
throw new InvalidOperationException($"Unable to convert value of type {value.GetType().Name}");
}
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
{
if (value is string s)
{
return ItemBase.Parse(s);
}
else if (value is ItemBase itemBase)
{
return itemBase.Name ?? string.Empty;
}
throw new InvalidOperationException($"Unable to convert value of type {value.GetType().Name}");
}
}
@@ -1,51 +0,0 @@
using Daybreak.Shared.Models.Guildwars;
using Newtonsoft.Json;
namespace Daybreak.Shared.Converters;
public sealed class MapJsonConverter : JsonConverter
{
public override bool CanConvert(Type objectType) => true;
public override object? ReadJson(JsonReader reader, Type objectType, object? existingValue, JsonSerializer serializer)
{
if (!objectType.IsAssignableTo(typeof(Map)))
{
return default;
}
switch (reader.TokenType)
{
case JsonToken.String:
var name = reader.ReadAsString();
if (name is null ||
!Map.TryParse(name, out var namedMap))
{
return default;
}
return namedMap;
case JsonToken.Integer:
var id = reader.Value as long?;
if (id is not long ||
!Map.TryParse((int)id.Value, out var parsedMap))
{
return default;
}
return parsedMap;
default:
return default;
}
}
public override void WriteJson(JsonWriter writer, object? value, JsonSerializer serializer)
{
if (value is not Map map)
{
return;
}
writer.WriteValue(map.Id);
}
}
@@ -1,33 +0,0 @@
using System.Globalization;
using System.Windows.Data;
namespace Daybreak.Shared.Converters;
public class MenuButtonHighlightConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
{
if (targetType == typeof(double) &&
value is bool boolean)
{
return boolean ? 0.4 : 0;
}
else
{
throw new NotImplementedException();
}
}
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
{
if (targetType == typeof(bool) &&
value is double doubleValue)
{
return doubleValue == 0.4;
}
else
{
throw new NotImplementedException();
}
}
}
@@ -1,51 +0,0 @@
using Daybreak.Shared.Models.Guildwars;
using Newtonsoft.Json;
namespace Daybreak.Shared.Converters;
public sealed class NpcJsonConverter : JsonConverter
{
public override bool CanConvert(Type objectType) => true;
public override object? ReadJson(JsonReader reader, Type objectType, object? existingValue, JsonSerializer serializer)
{
if (!objectType.IsAssignableTo(typeof(Npc)))
{
return default;
}
switch (reader.TokenType)
{
case JsonToken.String:
var name = reader.ReadAsString();
if (name is null ||
!Npc.TryParse(name, out var namedNpc))
{
return default;
}
return namedNpc;
case JsonToken.Integer:
var id = reader.Value as long?;
if (id is not long ||
!Npc.TryParse((int)id.Value, out var parsedNpc))
{
return default;
}
return parsedNpc;
default:
return default;
}
}
public override void WriteJson(JsonWriter writer, object? value, JsonSerializer serializer)
{
if (value is not Npc npc)
{
return;
}
writer.WriteValue(npc.Ids.FirstOrDefault());
}
}
@@ -1,11 +0,0 @@
using System.Globalization;
using System.Windows.Data;
using System.Windows;
namespace Daybreak.Shared.Converters;
public sealed class NullToUnsetValueConverter : IValueConverter
{
public object Convert(object? value, Type targetType, object? parameter, CultureInfo culture) => value ?? DependencyProperty.UnsetValue;
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) => Binding.DoNothing;
}
@@ -1,16 +1,8 @@
using System.Globalization;
using System.Windows.Data;
namespace Daybreak.Shared.Converters;
public sealed class PriceToStringConverter : IValueConverter
namespace Daybreak.Shared.Converters;
public static class PriceToStringConverter
{
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
public static string Convert(object value)
{
if (targetType != typeof(string))
{
throw new InvalidOperationException($"Unable to convert to {targetType.Name}");
}
var price = value switch
{
byte byteVal => byteVal,
@@ -35,9 +27,4 @@ public sealed class PriceToStringConverter : IValueConverter
return $"{price}{suffix}";
}
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
{
throw new NotImplementedException();
}
}
@@ -1,51 +0,0 @@
using Daybreak.Shared.Models.Guildwars;
using Newtonsoft.Json;
namespace Daybreak.Shared.Converters;
public sealed class ProfessionJsonConverter : JsonConverter
{
public override bool CanConvert(Type objectType) => true;
public override object? ReadJson(JsonReader reader, Type objectType, object? existingValue, JsonSerializer serializer)
{
if (!objectType.IsAssignableTo(typeof(Profession)))
{
return default;
}
switch (reader.TokenType)
{
case JsonToken.String:
var name = reader.ReadAsString();
if (name is null ||
!Profession.TryParse(name, out var namedProfession))
{
return default;
}
return namedProfession;
case JsonToken.Integer:
var id = reader.Value as long?;
if (id is not long ||
!Profession.TryParse((int)id.Value, out var parsedProfession))
{
return default;
}
return parsedProfession;
default:
return default;
}
}
public override void WriteJson(JsonWriter writer, object? value, JsonSerializer serializer)
{
if (value is not Profession profession)
{
return;
}
writer.WriteValue(profession.Id);
}
}
@@ -0,0 +1,129 @@
using Daybreak.Shared.Models.Guildwars;
using System.ComponentModel;
using System.Diagnostics.CodeAnalysis;
using System.Globalization;
namespace Daybreak.Shared.Converters;
public sealed class ProfessionTypeConverter : TypeConverter
{
public override bool CanConvertFrom(ITypeDescriptorContext? context, Type sourceType)
{
return sourceType == typeof(string) ||
sourceType == typeof(int) ||
sourceType == typeof(uint) ||
sourceType == typeof(long) ||
sourceType == typeof(ulong) ||
sourceType == typeof(short) ||
sourceType == typeof(ushort) ||
sourceType == typeof(byte) ||
sourceType == typeof(sbyte) ||
base.CanConvertFrom(context, sourceType);
}
public override bool CanConvertTo(ITypeDescriptorContext? context, [NotNullWhen(true)] Type? destinationType)
{
return destinationType == typeof(string) ||
destinationType == typeof(int) ||
destinationType == typeof(uint) ||
destinationType == typeof(long) ||
destinationType == typeof(ulong) ||
destinationType == typeof(short) ||
destinationType == typeof(ushort) ||
destinationType == typeof(byte) ||
destinationType == typeof(sbyte) ||
base.CanConvertTo(context, destinationType);
}
public override object? ConvertFrom(ITypeDescriptorContext? context, CultureInfo? culture, object value)
{
if (value is int intValue)
{
return Profession.Parse(intValue);
}
else if (value is uint uintValue)
{
return Profession.Parse((int)uintValue);
}
else if (value is long longValue)
{
return Profession.Parse((int)longValue);
}
else if (value is ulong ulongValue)
{
return Profession.Parse((int)ulongValue);
}
else if (value is short shortValue)
{
return Profession.Parse(shortValue);
}
else if (value is ushort ushortValue)
{
return Profession.Parse(ushortValue);
}
else if (value is byte byteValue)
{
return Profession.Parse(byteValue);
}
else if (value is sbyte sbyteValue)
{
return Profession.Parse(sbyteValue);
}
else if (value is string stringValue)
{
return Profession.Parse(stringValue);
}
else
{
return base.ConvertFrom(context, culture, value);
}
}
public override object? ConvertTo(ITypeDescriptorContext? context, CultureInfo? culture, object? value, Type destinationType)
{
if (value is not Profession profession)
{
return base.ConvertTo(context, culture, value, destinationType);
}
if (destinationType == typeof(int))
{
return profession.Id;
}
else if (destinationType == typeof(uint))
{
return (uint)profession.Id;
}
else if (destinationType == typeof(long))
{
return (long)profession.Id;
}
else if (destinationType == typeof(ulong))
{
return (ulong)profession.Id;
}
else if (destinationType == typeof(short))
{
return (short)profession.Id;
}
else if (destinationType == typeof(ushort))
{
return (ushort)profession.Id;
}
else if (destinationType == typeof(byte))
{
return (byte)profession.Id;
}
else if (destinationType == typeof(sbyte))
{
return (sbyte)profession.Id;
}
else if (destinationType == typeof(string))
{
return profession.Name ?? string.Empty;
}
else
{
return base.ConvertTo(context, culture, value, destinationType);
}
}
}
@@ -1,51 +0,0 @@
using Daybreak.Shared.Models.Guildwars;
using Newtonsoft.Json;
namespace Daybreak.Shared.Converters;
public sealed class QuestJsonConverter : JsonConverter
{
public override bool CanConvert(Type objectType) => true;
public override object? ReadJson(JsonReader reader, Type objectType, object? existingValue, JsonSerializer serializer)
{
if (!objectType.IsAssignableTo(typeof(Quest)))
{
return default;
}
switch (reader.TokenType)
{
case JsonToken.String:
var name = reader.ReadAsString();
if (name is null ||
!Quest.TryParse(name, out var namedQuest))
{
return default;
}
return namedQuest;
case JsonToken.Integer:
var id = reader.Value as long?;
if (id is not long ||
!Quest.TryParse((int)id.Value, out var parsedQuest))
{
return default;
}
return parsedQuest;
default:
return default;
}
}
public override void WriteJson(JsonWriter writer, object? value, JsonSerializer serializer)
{
if (value is not Quest quest)
{
return;
}
writer.WriteValue(quest.Id);
}
}
@@ -1,51 +0,0 @@
using Daybreak.Shared.Models.Guildwars;
using Newtonsoft.Json;
namespace Daybreak.Shared.Converters;
public sealed class RegionJsonConverter : JsonConverter
{
public override bool CanConvert(Type objectType) => true;
public override object? ReadJson(JsonReader reader, Type objectType, object? existingValue, JsonSerializer serializer)
{
if (!objectType.IsAssignableTo(typeof(Region)))
{
return default;
}
switch (reader.TokenType)
{
case JsonToken.String:
var name = reader.ReadAsString();
if (name is null ||
!Region.TryParse(name, out var namedRegion))
{
return default;
}
return namedRegion;
case JsonToken.Integer:
var id = reader.Value as long?;
if (id is not long ||
!Region.TryParse((int)id.Value, out var parsedRegion))
{
return default;
}
return parsedRegion;
default:
return default;
}
}
public override void WriteJson(JsonWriter writer, object? value, JsonSerializer serializer)
{
if (value is not Region region)
{
return;
}
writer.WriteValue(region.Id);
}
}
@@ -1,51 +0,0 @@
using Daybreak.Shared.Models.Guildwars;
using Newtonsoft.Json;
namespace Daybreak.Shared.Converters;
public sealed class SkillJsonConverter : JsonConverter
{
public override bool CanConvert(Type objectType) => true;
public override object? ReadJson(JsonReader reader, Type objectType, object? existingValue, JsonSerializer serializer)
{
if (!objectType.IsAssignableTo(typeof(Skill)))
{
return default;
}
switch (reader.TokenType)
{
case JsonToken.String:
var name = reader.ReadAsString();
if (name is null ||
!Skill.TryParse(name, out var namedSkill))
{
return default;
}
return namedSkill;
case JsonToken.Integer:
var id = reader.Value as long?;
if (id is not long ||
!Skill.TryParse((int)id.Value, out var parsedSkill))
{
return default;
}
return parsedSkill;
default:
return default;
}
}
public override void WriteJson(JsonWriter writer, object? value, JsonSerializer serializer)
{
if (value is not Skill skill)
{
return;
}
writer.WriteValue(skill.Id);
}
}
@@ -0,0 +1,26 @@
using Daybreak.Shared.Models.Themes;
using Daybreak.Shared.Services.Themes;
using System.Text.Json;
using System.Text.Json.Serialization;
namespace Daybreak.Shared.Converters;
public sealed class ThemeJsonConverter : JsonConverter<Theme>
{
public override Theme? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
{
return reader.TokenType switch
{
JsonTokenType.String => reader.GetString() is string themeName
? themeName is GameScreenshotsTheme.ThemeName
? Theme.Themes.FirstOrDefault(t => t is GameScreenshotsTheme)
: Theme.Themes.FirstOrDefault(t => t.Name.Equals(themeName, StringComparison.OrdinalIgnoreCase))
: default,
_ => default
};
}
public override void Write(Utf8JsonWriter writer, Theme value, JsonSerializerOptions options)
{
writer.WriteStringValue(value.Name);
}
}
@@ -1,33 +0,0 @@
using System.Globalization;
using System.Windows.Data;
namespace Daybreak.Shared.Converters;
public class TileButtonHighlightConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
{
if (targetType == typeof(double) &&
value is bool boolean)
{
return boolean ? 1 : 0.4;
}
else
{
throw new NotImplementedException();
}
}
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
{
if (targetType == typeof(bool) &&
value is double doubleValue)
{
return doubleValue == 1;
}
else
{
throw new NotImplementedException();
}
}
}
@@ -1,105 +0,0 @@
using System.Globalization;
using System.Windows.Data;
namespace Daybreak.Shared.Converters;
public sealed class TimeSinceDateTimeConverter : IValueConverter
{
private const char PluralAppend = 's';
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
{
return GetTimeString(value) ?? string.Empty;
}
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
{
throw new NotImplementedException();
}
private static string? GetTimeString(object value)
{
if (value is not DateTime dateTime)
{
return default;
}
var difference = DateTime.Now - dateTime;
// If less than a minute, return seconds
if (difference.TotalSeconds < 60)
{
var totalSeconds = (int)difference.TotalSeconds;
return $"{totalSeconds} second{(difference.TotalSeconds > 1 ? PluralAppend : string.Empty)} ago";
}
// If less than an hour, return minutes
if (difference.TotalMinutes < 60)
{
var totalMinutes = (int)difference.TotalMinutes;
return $"{totalMinutes} minute{(difference.TotalMinutes > 1 ? PluralAppend : string.Empty)} ago";
}
// If less than a day, return hours
if (difference.TotalHours < 24)
{
var totalHours = (int)difference.TotalHours;
return $"{totalHours} hour{(difference.TotalHours > 1 ? PluralAppend : string.Empty)} ago";
}
// If less than a week, return days
if (difference.TotalDays < 7)
{
var totalDays = (int)difference.TotalDays;
return $"{totalDays} day{(difference.TotalDays > 1 ? PluralAppend : string.Empty)} ago";
}
// If less than a month, return weeks
var numberOfDaysInCurrentMonth = DateTime.DaysInMonth(DateTime.Now.Year, DateTime.Now.Month);
if (difference.TotalDays < numberOfDaysInCurrentMonth)
{
var numberOfWeeks = (int)difference.TotalDays / 7;
return $"{numberOfWeeks} week{(numberOfWeeks > 1 ? PluralAppend : string.Empty)} ago";
}
// If less than a year, return months
var daysInCurrentYear = DateTime.IsLeapYear(DateTime.Now.Year) ? 366 : 365;
if (difference.TotalDays < daysInCurrentYear)
{
var days = difference.TotalDays;
var year = DateTime.Now.Year;
var month = DateTime.Now.Month;
var monthCount = 0;
while(days > 0)
{
monthCount++;
var numberOfDaysInMonth = DateTime.DaysInMonth(year, month);
days -= numberOfDaysInMonth;
if (month == 1)
{
month = 12;
year--;
}
else
{
month--;
}
}
return $"{monthCount} month{(monthCount > 1 ? PluralAppend : string.Empty)} ago";
}
// Return the number of years
var remainingDays = difference.TotalDays;
var yearCount = 0;
var currentYear = DateTime.Now.Year;
while(remainingDays > 0)
{
yearCount++;
remainingDays -= DateTime.IsLeapYear(currentYear) ? 366 : 365;
currentYear--;
}
return $"{yearCount} year{(yearCount > 1 ? PluralAppend : string.Empty)} ago";
}
}
@@ -1,29 +1,11 @@
using System.Globalization;
using System.Windows.Data;
namespace Daybreak.Shared.Converters;
namespace Daybreak.Shared.Converters;
public sealed class TimespanToETAConverter : IValueConverter
public static class TimespanToETAConverter
{
private const char PluralAppend = 's';
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
public static string GetETAString(TimeSpan timeSpan)
{
return GetETAString(value);
}
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
{
throw new NotImplementedException();
}
private static string GetETAString(object obj)
{
if (obj is not TimeSpan timeSpan)
{
return string.Empty;
}
if ((int)timeSpan.TotalDays > 0)
{
return $"{(int)timeSpan.TotalDays} day{((int)timeSpan.TotalDays > 1 ? PluralAppend : string.Empty)} remaining";

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