mirror of
https://github.com/gwdevhub/Daybreak.git
synced 2026-09-19 06:45:08 +00:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
53fc08bcb5 | ||
|
|
4077777cd9 | ||
|
|
77c2403a40 | ||
|
|
fa3c5e95a6 | ||
|
|
db012ae094 | ||
|
|
d124197e43 | ||
|
|
85593ea94b | ||
|
|
fe9178c66d | ||
|
|
e5e4e42e1e | ||
|
|
cc3236f4d6 | ||
|
|
6d598cdf95 | ||
|
|
974975eb78 | ||
|
|
4346ed758b | ||
|
|
a7320ac32c | ||
|
|
dce365b227 | ||
|
|
3ad32bee8f | ||
|
|
9e3fa738bb | ||
|
|
4a6eccca24 | ||
|
|
b1898c460c | ||
|
|
5c39a99b8f | ||
|
|
e873b0da6f | ||
|
|
9d9c533df7 | ||
|
|
9f15df9b6d | ||
|
|
5447633500 | ||
|
|
47369851f2 | ||
|
|
176365b4a4 | ||
|
|
ca526d352e | ||
|
|
c2d30ac60c | ||
|
|
3984b5a66d | ||
|
|
5be32e32ac | ||
|
|
5102ad0fe7 | ||
|
|
067da8a091 | ||
|
|
20d0bade79 | ||
|
|
a40c28711e | ||
|
|
1b878a73ae | ||
|
|
f4bbd3dbc6 | ||
|
|
cd46572c22 | ||
|
|
33dc001604 | ||
|
|
c9676d59d1 | ||
|
|
7f05738a63 | ||
|
|
f6687e0c72 | ||
|
|
1bf2fb07cc | ||
|
|
c826849bb7 | ||
|
|
82fdf5f18b | ||
|
|
5216cfcea3 | ||
|
|
a6bee38a19 | ||
|
|
2394059ca3 | ||
|
|
d4fb2e0545 | ||
|
|
cd52c20977 | ||
|
|
2070ae2689 | ||
|
|
3ca5117875 | ||
|
|
ea0a7a1edb | ||
|
|
824881c108 | ||
|
|
77e3d5ddf2 | ||
|
|
9626d7a42e | ||
|
|
75a6fb2b6c | ||
|
|
d887f2c901 | ||
|
|
b079be2445 | ||
|
|
f26ac481e9 | ||
|
|
6bfa52a0bf | ||
|
|
0ce4940342 | ||
|
|
969d24640f | ||
|
|
51916c0707 | ||
|
|
05910734fc | ||
|
|
55ccc6e367 | ||
|
|
6391b13cb4 | ||
|
|
32f08793ed | ||
|
|
d6237764d6 | ||
|
|
15ff789942 | ||
|
|
f4c4b5bac2 | ||
|
|
26b02ac94d | ||
|
|
1f8d4c126e | ||
|
|
8306e10ffe | ||
|
|
cc5fe38daa | ||
|
|
cc1bd1d77f | ||
|
|
437210858c | ||
|
|
47b7a480bf | ||
|
|
fe66d8e8b0 | ||
|
|
4b138e02d4 | ||
|
|
32bc16c695 | ||
|
|
0fae63f902 | ||
|
|
2447f7e517 | ||
|
|
f5483352f0 | ||
|
|
f081858d60 | ||
|
|
446f5cffd8 | ||
|
|
21b9cf1726 | ||
|
|
4f1398a8d1 | ||
|
|
072f00638c | ||
|
|
f7f22532bf | ||
|
|
a565017ad2 | ||
|
|
8b5f5c19e4 | ||
|
|
6d260a927d | ||
|
|
97eb3af350 | ||
|
|
ad70e80d4d | ||
|
|
dcffce5298 | ||
|
|
9ddcb6ba80 | ||
|
|
d77cefb699 | ||
|
|
bc8917692b | ||
|
|
02409f10fe | ||
|
|
f167ee538a | ||
|
|
c5dccb67dc | ||
|
|
2cb105acde | ||
|
|
fbc0c39a37 | ||
|
|
1da19b2e1d | ||
|
|
ef2076022e | ||
|
|
d6ad8429c8 | ||
|
|
7535be7134 | ||
|
|
33f8f73066 | ||
|
|
4c29a5617b | ||
|
|
2116285cac | ||
|
|
e85c286120 | ||
|
|
730722a0e0 | ||
|
|
8b90f13bd5 | ||
|
|
1360c86395 | ||
|
|
f6f0afc5d6 | ||
|
|
194fca5861 | ||
|
|
114ddacfcc | ||
|
|
dc5dfc8b07 | ||
|
|
976586649e | ||
|
|
4322b636ef | ||
|
|
cd8581f86a | ||
|
|
9417a1a486 | ||
|
|
f838d16543 | ||
|
|
0ec1c9c96f | ||
|
|
bab997b64a | ||
|
|
a0c6b7d956 | ||
|
|
fea568959b | ||
|
|
443582fda6 | ||
|
|
54e4c17af2 | ||
|
|
645c94f21b | ||
|
|
8e7b12b760 | ||
|
|
3e6ac4149f | ||
|
|
ebe045a962 | ||
|
|
5298ab1e83 | ||
|
|
64df882406 | ||
|
|
9dd39c0d93 | ||
|
|
2f5108fd3b | ||
|
|
9510fdc5de | ||
|
|
09781b5516 | ||
|
|
3044861652 | ||
|
|
8077d36e77 | ||
|
|
4ac9daa984 | ||
|
|
f1bfa66e79 | ||
|
|
a4485fc60f | ||
|
|
04c9d86ac4 | ||
|
|
0d30adf63b | ||
|
|
2949d47a29 | ||
|
|
f300f81d00 | ||
|
|
20ff35fb12 | ||
|
|
8c02b63051 | ||
|
|
3bae3cf64f | ||
|
|
948532a694 | ||
|
|
c43195d0bc | ||
|
|
7d48fde5ea | ||
|
|
69096795f7 | ||
|
|
7b2320e158 | ||
|
|
baa2628c6f | ||
|
|
62a6a018e1 | ||
|
|
77c15c9d63 | ||
|
|
0f338f5a53 | ||
|
|
a714fd7c68 | ||
|
|
1e933a4764 | ||
|
|
6cfa0d8976 | ||
|
|
b7d3bf67cc | ||
|
|
099d745c79 | ||
|
|
4ed61b3cae | ||
|
|
fab37a5ea0 | ||
|
|
1baa373f48 | ||
|
|
1b234c249d | ||
|
|
fc177a3007 | ||
|
|
de94b93f2d | ||
|
|
b211ab9409 | ||
|
|
440ccf4518 | ||
|
|
3f7a1cc310 | ||
|
|
d3cce2e24e | ||
|
|
8fa05c8854 | ||
|
|
d71ac6df06 | ||
|
|
cc3173ffe5 | ||
|
|
a92f566119 | ||
|
|
8df32b1009 | ||
|
|
26cb97caf4 | ||
|
|
66e2c5eb95 | ||
|
|
99444df04e | ||
|
|
b08d553c56 | ||
|
|
d7a48db83f | ||
|
|
b5d7719f48 | ||
|
|
6dfdc7376e | ||
|
|
7ee7be4d37 | ||
|
|
3554249d80 | ||
|
|
5a6fca6fa7 | ||
|
|
8d0a53a812 | ||
|
|
794afa6c2c | ||
|
|
7d78a89d12 | ||
|
|
dcb50e9da3 | ||
|
|
86e3579383 | ||
|
|
ea39346678 | ||
|
|
e4cdc4f939 | ||
|
|
5acac1ae7a | ||
|
|
8c0e734c71 | ||
|
|
a56eb20aa0 | ||
|
|
482854801c | ||
|
|
1a37b8b08f | ||
|
|
40d8280ba6 | ||
|
|
9533ef0607 | ||
|
|
ac963f60b5 | ||
|
|
5b6ea99f79 |
@@ -0,0 +1,22 @@
|
||||
# This action simulates a CD pipeline without the package publishing
|
||||
# to ensure that changes don't break the real run
|
||||
name: Daybreak Dry-Run CD Pipeline
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
branches:
|
||||
- master
|
||||
paths:
|
||||
- "Daybreak.Core/**"
|
||||
- "Daybreak.Installer/**"
|
||||
- "Daybreak.API/**"
|
||||
- "Daybreak.Shared/**"
|
||||
- "Daybreak.Windows/**"
|
||||
- "Daybreak.Linux/**"
|
||||
|
||||
jobs:
|
||||
cd:
|
||||
uses: ./.github/workflows/cd-template.yml
|
||||
with:
|
||||
publishRelease: false
|
||||
secrets: inherit
|
||||
@@ -0,0 +1,271 @@
|
||||
# Reusable CD pipeline with optional release publishing
|
||||
name: Daybreak CD Template
|
||||
|
||||
on:
|
||||
workflow_call:
|
||||
inputs:
|
||||
publishRelease:
|
||||
description: "Whether to create and publish the GitHub release"
|
||||
required: false
|
||||
type: boolean
|
||||
default: false
|
||||
|
||||
jobs:
|
||||
# ───────────────────────────────────────────────────────
|
||||
# Stage 1 — Build the x86 Windows-only components
|
||||
# (Injector + API) that both platforms need.
|
||||
# ───────────────────────────────────────────────────────
|
||||
build-x86:
|
||||
runs-on: windows-latest
|
||||
|
||||
outputs:
|
||||
version: ${{ steps.get_version.outputs.version }}
|
||||
|
||||
env:
|
||||
Configuration: Release
|
||||
Actions_Allow_Unsecure_Commands: true
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
fetch-depth: 0
|
||||
submodules: recursive
|
||||
|
||||
- name: Get version
|
||||
id: get_version
|
||||
shell: pwsh
|
||||
run: |
|
||||
$version = (Select-String -Path Directory.Build.props -Pattern '<Version>(.*)</Version>').Matches.Groups[1].Value
|
||||
echo "version=$version" >> $env:GITHUB_OUTPUT
|
||||
|
||||
- name: Install .NET
|
||||
uses: actions/setup-dotnet@v5
|
||||
with:
|
||||
dotnet-version: "10.x"
|
||||
|
||||
- name: Publish x86 components
|
||||
run: .\Scripts\PublishWindowsX86.ps1
|
||||
shell: pwsh
|
||||
|
||||
- name: Upload x86 artifacts
|
||||
uses: actions/upload-artifact@v6
|
||||
with:
|
||||
name: x86-components
|
||||
path: |
|
||||
Publish/Injector/
|
||||
Publish/Api/
|
||||
retention-days: 1
|
||||
|
||||
# ───────────────────────────────────────────────────────
|
||||
# Stage 2 — Platform builds (parallel)
|
||||
# ───────────────────────────────────────────────────────
|
||||
build-windows:
|
||||
needs: build-x86
|
||||
runs-on: windows-latest
|
||||
|
||||
env:
|
||||
Configuration: Release
|
||||
Actions_Allow_Unsecure_Commands: true
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
fetch-depth: 0
|
||||
submodules: recursive
|
||||
|
||||
- name: Install .NET
|
||||
uses: actions/setup-dotnet@v5
|
||||
with:
|
||||
dotnet-version: "10.x"
|
||||
|
||||
- name: Setup project secrets
|
||||
run: |
|
||||
dotnet user-secrets --project Daybreak.Core\Daybreak.Core.csproj set AadApplicationId "${{ secrets.AadApplicationId }}"
|
||||
dotnet user-secrets --project Daybreak.Core\Daybreak.Core.csproj set AadTenantId "${{ secrets.AadTenantId }}"
|
||||
dotnet user-secrets --project Daybreak.Core\Daybreak.Core.csproj set ApmServiceAccount "${{ secrets.ApmServiceAccount }}"
|
||||
dotnet user-secrets --project Daybreak.Core\Daybreak.Core.csproj set ApmServiceKey "${{ secrets.ApmServiceKey }}"
|
||||
dotnet user-secrets --project Daybreak.Core\Daybreak.Core.csproj set ApmUri "${{ secrets.ApmUri }}"
|
||||
|
||||
- name: Download x86 artifacts
|
||||
uses: actions/download-artifact@v7
|
||||
with:
|
||||
name: x86-components
|
||||
path: Publish/
|
||||
|
||||
- name: Publish Windows x64
|
||||
run: .\Scripts\PublishWindows.ps1
|
||||
shell: pwsh
|
||||
|
||||
- name: Upload Windows build
|
||||
uses: actions/upload-artifact@v6
|
||||
with:
|
||||
name: windows-build
|
||||
path: Publish/
|
||||
retention-days: 1
|
||||
|
||||
build-linux:
|
||||
needs: build-x86
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
env:
|
||||
Configuration: Release
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
fetch-depth: 0
|
||||
submodules: recursive
|
||||
|
||||
- name: Install .NET
|
||||
uses: actions/setup-dotnet@v5
|
||||
with:
|
||||
dotnet-version: "10.x"
|
||||
|
||||
- name: Setup project secrets
|
||||
run: |
|
||||
dotnet user-secrets --project Daybreak.Core/Daybreak.Core.csproj set AadApplicationId "${{ secrets.AadApplicationId }}"
|
||||
dotnet user-secrets --project Daybreak.Core/Daybreak.Core.csproj set AadTenantId "${{ secrets.AadTenantId }}"
|
||||
dotnet user-secrets --project Daybreak.Core/Daybreak.Core.csproj set ApmServiceAccount "${{ secrets.ApmServiceAccount }}"
|
||||
dotnet user-secrets --project Daybreak.Core/Daybreak.Core.csproj set ApmServiceKey "${{ secrets.ApmServiceKey }}"
|
||||
dotnet user-secrets --project Daybreak.Core/Daybreak.Core.csproj set ApmUri "${{ secrets.ApmUri }}"
|
||||
|
||||
- name: Download x86 artifacts
|
||||
uses: actions/download-artifact@v7
|
||||
with:
|
||||
name: x86-components
|
||||
path: Publish/
|
||||
|
||||
- name: Publish Linux x64
|
||||
run: pwsh ./Scripts/PublishLinux.ps1
|
||||
shell: bash
|
||||
|
||||
- name: Set executable permissions
|
||||
run: |
|
||||
chmod +x Publish/Daybreak
|
||||
chmod +x Publish/Daybreak.Wayland.sh
|
||||
chmod +x Publish/Installer/Daybreak.Installer
|
||||
|
||||
- name: Upload Linux build
|
||||
uses: actions/upload-artifact@v6
|
||||
with:
|
||||
name: linux-build
|
||||
path: Publish/
|
||||
retention-days: 1
|
||||
|
||||
# ───────────────────────────────────────────────────────
|
||||
# Stage 3 — Package (always)
|
||||
# ───────────────────────────────────────────────────────
|
||||
package:
|
||||
needs: [build-x86, build-windows, build-linux]
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
env:
|
||||
Actions_Allow_Unsecure_Commands: true
|
||||
Version: ${{ needs.build-x86.outputs.version }}
|
||||
|
||||
steps:
|
||||
# ── Windows package ──
|
||||
- name: Download Windows build
|
||||
uses: actions/download-artifact@v7
|
||||
with:
|
||||
name: windows-build
|
||||
path: windows-publish/
|
||||
|
||||
- name: Clean and zip Windows build
|
||||
run: |
|
||||
cd windows-publish
|
||||
find . -name '*.pdb' -delete
|
||||
find . -name '*.dbg' -delete
|
||||
if [ -f Installer/Daybreak.Installer.exe ]; then
|
||||
mv Installer/Daybreak.Installer.exe Installer/Daybreak.Installer.Temp.exe
|
||||
fi
|
||||
cd ..
|
||||
cd windows-publish && zip -r ../daybreakv${{ env.Version }}.zip . && cd ..
|
||||
|
||||
# ── Linux package ──
|
||||
- name: Download Linux build
|
||||
uses: actions/download-artifact@v7
|
||||
with:
|
||||
name: linux-build
|
||||
path: linux-publish/
|
||||
|
||||
- name: Clean and zip Linux build
|
||||
run: |
|
||||
cd linux-publish
|
||||
find . -name '*.pdb' -delete
|
||||
find . -name '*.dbg' -delete
|
||||
if [ -f Installer/Daybreak.Installer ]; then
|
||||
chmod +x Installer/Daybreak.Installer
|
||||
mv Installer/Daybreak.Installer Installer/Daybreak.Installer.Temp
|
||||
fi
|
||||
chmod +x Daybreak Daybreak.Wayland.sh
|
||||
cd ..
|
||||
cd linux-publish && zip -r ../daybreakv${{ env.Version }}-linux.zip . && cd ..
|
||||
|
||||
- name: Upload release artifacts
|
||||
uses: actions/upload-artifact@v6
|
||||
with:
|
||||
name: release-zips
|
||||
path: |
|
||||
daybreakv${{ env.Version }}.zip
|
||||
daybreakv${{ env.Version }}-linux.zip
|
||||
retention-days: 7
|
||||
|
||||
# ───────────────────────────────────────────────────────
|
||||
# Stage 4 — Release (optional)
|
||||
# ───────────────────────────────────────────────────────
|
||||
release:
|
||||
needs: [build-x86, package]
|
||||
runs-on: ubuntu-latest
|
||||
if: ${{ inputs.publishRelease }}
|
||||
|
||||
env:
|
||||
Actions_Allow_Unsecure_Commands: true
|
||||
Version: ${{ needs.build-x86.outputs.version }}
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
fetch-depth: 0
|
||||
submodules: recursive
|
||||
|
||||
- name: Download release artifacts
|
||||
uses: actions/download-artifact@v7
|
||||
with:
|
||||
name: release-zips
|
||||
path: release-assets/
|
||||
|
||||
- name: Get Latest Tag
|
||||
id: getLatestTag
|
||||
uses: WyriHaximus/github-action-get-previous-tag@v2
|
||||
|
||||
- name: Generate changelog
|
||||
id: changelog
|
||||
run: |
|
||||
changeLog=$(git log --no-merges --pretty="%h - %s (%an)<br />" ${{ steps.getLatestTag.outputs.tag }}..HEAD)
|
||||
echo "content<<EOF" >> $GITHUB_OUTPUT
|
||||
echo "$changeLog" >> $GITHUB_OUTPUT
|
||||
echo "EOF" >> $GITHUB_OUTPUT
|
||||
|
||||
# ── Release ──
|
||||
- name: Create release draft
|
||||
uses: Xotl/cool-github-releases@v1.1.10
|
||||
with:
|
||||
mode: update
|
||||
tag_name: v${{ env.Version }}
|
||||
release_name: Daybreak v${{ env.Version }}
|
||||
assets: release-assets/daybreakv${{ env.Version }}.zip;release-assets/daybreakv${{ env.Version }}-linux.zip
|
||||
github_token: ${{ secrets.GITHUB_TOKEN }}
|
||||
replace_assets: true
|
||||
body_mrkdwn: |
|
||||
${{ steps.changelog.outputs.content }}
|
||||
isDraft: true
|
||||
|
||||
- name: Publish release
|
||||
run: gh release edit v${{ env.Version }} --draft=false
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
+16
-147
@@ -1,156 +1,25 @@
|
||||
# Copyright (c) Microsoft Corporation.
|
||||
# Licensed under the MIT license.
|
||||
|
||||
# This continuous integration pipeline is triggered anytime a user pushes code to the repo.
|
||||
# This pipeline builds the Wpf project, runs unit tests, then saves the MSIX build artifact.
|
||||
name: Daybreak CD Pipeline
|
||||
# This continuous delivery pipeline is triggered on pushes to master.
|
||||
# It builds the Windows x86 components (Injector + API), then builds
|
||||
# the Windows x64 and Linux x64 platform bundles in parallel, and
|
||||
# finally packages both into release zips and publishes a GitHub release.
|
||||
name: Daybreak CD Pipeline
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
paths:
|
||||
- "Daybreak/**"
|
||||
- "Daybreak.Core/**"
|
||||
- "Daybreak.Installer/**"
|
||||
- "Daybreak.API/**"
|
||||
- "Daybreak.Shared/**"
|
||||
- "Daybreak.Windows/**"
|
||||
- "Daybreak.Linux/**"
|
||||
workflow_dispatch:
|
||||
|
||||
|
||||
jobs:
|
||||
|
||||
build:
|
||||
|
||||
strategy:
|
||||
matrix:
|
||||
targetplatform: [x86]
|
||||
|
||||
runs-on: windows-latest
|
||||
|
||||
env:
|
||||
Configuration: Release
|
||||
Solution_Path: Daybreak.sln
|
||||
Test_Project_Path: Daybreak.Tests\Daybreak.Tests.csproj
|
||||
Wpf_Project_Path: Daybreak\Daybreak.csproj
|
||||
Actions_Allow_Unsecure_Commands: true
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Get Latest Tag
|
||||
id: getLatestTag
|
||||
uses: WyriHaximus/github-action-get-previous-tag@v1
|
||||
|
||||
- name: Generate changelog
|
||||
id: gen_changelog
|
||||
run: |
|
||||
$changeLog = git log --no-merges --pretty="%h - %s (%an)<br />" ${{ env.LatestReleaseTag }}..HEAD
|
||||
echo "::set-env name=Changelog::$changeLog"
|
||||
mkdir Publish
|
||||
echo $changeLog > .\Publish\changelog.txt
|
||||
env:
|
||||
LatestReleaseTag: ${{steps.getLatestTag.outputs.tag}}
|
||||
|
||||
- name: Install .NET Core
|
||||
uses: actions/setup-dotnet@v4
|
||||
with:
|
||||
dotnet-version: '9.x'
|
||||
|
||||
- name: Setup MSBuild.exe
|
||||
uses: microsoft/setup-msbuild@v2.0.0
|
||||
|
||||
- name: Setup project secrets
|
||||
run: |
|
||||
dotnet user-secrets --project Daybreak\Daybreak.csproj set AadApplicationId "${{ secrets.AadApplicationId }}"
|
||||
dotnet user-secrets --project Daybreak\Daybreak.csproj set AadTenantId "${{ secrets.AadTenantId }}"
|
||||
dotnet user-secrets --project Daybreak\Daybreak.csproj set ApmApiKey "${{ secrets.ApmApiKey }}"
|
||||
dotnet user-secrets --project Daybreak\Daybreak.csproj set ApmUri "${{ secrets.ApmUri }}"
|
||||
|
||||
- name: Restore project
|
||||
run: msbuild $env:Solution_Path /t:Restore /p:Configuration=$env:Configuration /p:RuntimeIdentifier=$env:RuntimeIdentifier --property:SolutionDir=$GITHUB_WORKSPACE
|
||||
env:
|
||||
RuntimeIdentifier: win-${{ matrix.targetplatform }}
|
||||
|
||||
- name: Build Daybreak project
|
||||
run: dotnet build Daybreak -c $env:Configuration --property:SolutionDir=$env:GITHUB_WORKSPACE
|
||||
|
||||
- name: Set version variable
|
||||
run: |
|
||||
$version = .\Scripts\GetBuildVersion.ps1
|
||||
echo "::set-env name=Version::$version"
|
||||
|
||||
- name: Create publish launcher files
|
||||
run: dotnet publish .\Daybreak\Daybreak.csproj -c $env:Configuration -r $env:RuntimeIdentifier --property:SolutionDir=$env:GITHUB_WORKSPACE -p:PublishReadyToRun=true -p:PublishSingleFile=false --self-contained true -o .\Publish
|
||||
env:
|
||||
RuntimeIdentifier: win-${{ matrix.targetplatform }}
|
||||
|
||||
- name: Pack publish files
|
||||
run: |
|
||||
Write-Host $env
|
||||
.\Scripts\BuildRelease.ps1 -version $env:Version
|
||||
shell: pwsh
|
||||
|
||||
- name: Submit virustotal scan
|
||||
shell: pwsh
|
||||
run: |
|
||||
cd Publish
|
||||
$apiKey = "${{ secrets.VIRUSTOTAL_APIKEY }}"
|
||||
$archiveName = "daybreakv${{ env.Version }}.zip"
|
||||
$uploadUrlResponse = curl https://www.virustotal.com/api/v3/files/upload_url -H x-apikey:"$apiKey"
|
||||
$uploadUrlResponse
|
||||
$uploadUrlJson = $uploadUrlResponse | ConvertFrom-Json
|
||||
$uploadUrl = $uploadUrlJson.data
|
||||
$uploadResponse = curl --request POST --header x-apikey:"$apiKey" --header 'content-type: multipart/form-data' --form file="@$archiveName" --url $uploadUrl
|
||||
$uploadResponse
|
||||
$uploadResponseJson = $uploadResponse | ConvertFrom-Json
|
||||
$resultsUrl = "https://www.virustotal.com/api/v3/analyses/$($uploadResponseJson.data.id)"
|
||||
$resultsUrl
|
||||
# Poll the results until the status is "completed"
|
||||
$status = ""
|
||||
while ($status -ne "completed") {
|
||||
Start-Sleep -Seconds 10 # Wait for 10 seconds between checks
|
||||
$results = curl --header x-apikey:"$apiKey" --url $resultsUrl
|
||||
$results
|
||||
$resultsJson = $results | ConvertFrom-Json
|
||||
$status = $resultsJson.data.attributes.status
|
||||
Write-Host "Current status: $status"
|
||||
}
|
||||
|
||||
$resultsJson.data.attributes.results | ConvertTo-Json | Out-File -FilePath "VirusTotal.Results.json" -Encoding UTF8
|
||||
$resultsJson.data.attributes.stats | ConvertTo-Json | Out-File -FilePath "VirusTotal.Stats.json" -Encoding UTF8
|
||||
$stats = $resultsJson.data.attributes.stats
|
||||
echo "::set-env name=VirusTotalStats::$stats"
|
||||
|
||||
- name: Publish blob files
|
||||
run: |
|
||||
Write-Host $env
|
||||
.\Scripts\PushFilesToBlobStorage.ps1 -version $env:Version -sourcePath .\Publish\ -connectionString "${{ secrets.BLOBSTORAGE_CONNECTIONSTRING }}"
|
||||
shell: pwsh
|
||||
|
||||
- name: Create release draft
|
||||
uses: Xotl/cool-github-releases@v1.1.10
|
||||
with:
|
||||
mode: update
|
||||
tag_name: v${{ env.Version }}
|
||||
release_name: Daybreak v${{ env.Version }}
|
||||
assets: .\Publish\daybreakv${{ env.Version }}.zip;.\Publish\VirusTotal.Results.json;.\Publish\VirusTotal.Stats.json
|
||||
github_token: ${{ env.GITHUB_TOKEN }}
|
||||
replace_assets: true
|
||||
body_mrkdwn: |
|
||||
${{ env.Changelog }}
|
||||
|
||||
** VirustTotal Stats: **
|
||||
```
|
||||
${{ env.VirusTotalStats }}
|
||||
```
|
||||
isDraft: true
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Publish release
|
||||
run: |
|
||||
gh release edit v${{ env.Version }} --draft=false
|
||||
shell: powershell
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
cd:
|
||||
uses: ./.github/workflows/cd-template.yml
|
||||
with:
|
||||
publishRelease: true
|
||||
secrets: inherit
|
||||
|
||||
+51
-36
@@ -1,51 +1,66 @@
|
||||
# Copyright (c) Microsoft Corporation.
|
||||
# Licensed under the MIT license.
|
||||
|
||||
# This continuous integration pipeline is triggered anytime a user pushes code to the repo.
|
||||
# This pipeline builds the Wpf project, runs unit tests, then saves the MSIX build artifact.
|
||||
name: Daybreak CI Pipeline
|
||||
# This continuous integration pipeline is triggered on pull requests to master.
|
||||
# It validates that both the Windows and Linux builds compile successfully.
|
||||
name: Daybreak CI Pipeline
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
pull_request:
|
||||
branches:
|
||||
- master
|
||||
|
||||
jobs:
|
||||
|
||||
build:
|
||||
|
||||
strategy:
|
||||
matrix:
|
||||
targetplatform: [x86]
|
||||
|
||||
build-windows:
|
||||
runs-on: windows-latest
|
||||
|
||||
env:
|
||||
Solution_Path: Daybreak.sln
|
||||
Test_Project_Path: Daybreak.Tests\Daybreak.Tests.csproj
|
||||
Wpf_Project_Path: Daybreak\Daybreak.csproj
|
||||
Actions_Allow_Unsecure_Commands: true
|
||||
Configuration: Debug
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
fetch-depth: 0
|
||||
submodules: recursive
|
||||
|
||||
- name: Install .NET Core
|
||||
uses: actions/setup-dotnet@v4
|
||||
with:
|
||||
dotnet-version: '9.x'
|
||||
architecture: x86
|
||||
- name: Install .NET
|
||||
uses: actions/setup-dotnet@v5
|
||||
with:
|
||||
dotnet-version: "10.x"
|
||||
|
||||
- name: Setup MSBuild.exe
|
||||
uses: microsoft/setup-msbuild@v2.0.0
|
||||
- name: Build Windows project
|
||||
run: dotnet build Daybreak.Windows/Daybreak.Windows.csproj -c $env:Configuration
|
||||
|
||||
- name: Restore the Wpf application to populate the obj folder
|
||||
run: msbuild $env:Solution_Path /t:Restore /p:Configuration=$env:Configuration /p:RuntimeIdentifier=$env:RuntimeIdentifier --property:SolutionDir=$env:GITHUB_WORKSPACE
|
||||
env:
|
||||
Configuration: Debug
|
||||
RuntimeIdentifier: win-${{ matrix.targetplatform }}
|
||||
- name: Build Injector
|
||||
run: dotnet build Daybreak.Injector/Daybreak.Injector.csproj -c $env:Configuration
|
||||
|
||||
- name: Build API
|
||||
run: dotnet build Daybreak.API/Daybreak.API.csproj -c $env:Configuration
|
||||
|
||||
- name: Build Installer
|
||||
run: dotnet build Daybreak.Installer/Daybreak.Installer.csproj -c $env:Configuration
|
||||
|
||||
- name: Run tests
|
||||
run: dotnet test Daybreak.Tests/Daybreak.Tests.csproj -c $env:Configuration
|
||||
|
||||
build-linux:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
env:
|
||||
Configuration: Debug
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
fetch-depth: 0
|
||||
submodules: recursive
|
||||
|
||||
- name: Install .NET
|
||||
uses: actions/setup-dotnet@v5
|
||||
with:
|
||||
dotnet-version: "10.x"
|
||||
|
||||
- name: Build Linux project
|
||||
run: dotnet build Daybreak.Linux/Daybreak.Linux.csproj -c $Configuration
|
||||
|
||||
- name: Build Installer (linux-x64)
|
||||
run: dotnet build Daybreak.Installer/Daybreak.Installer.csproj -c $Configuration
|
||||
@@ -12,10 +12,7 @@
|
||||
name: "CodeQL"
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [ "master" ]
|
||||
pull_request:
|
||||
# The branches below must be a subset of the branches above
|
||||
branches: [ "master" ]
|
||||
schedule:
|
||||
- cron: '41 12 * * 6'
|
||||
@@ -38,14 +35,14 @@ jobs:
|
||||
|
||||
env:
|
||||
Configuration: Release
|
||||
Solution_Path: Daybreak.sln
|
||||
Solution_Path: Daybreak.slnx
|
||||
Test_Project_Path: Daybreak.Tests\Daybreak.Tests.csproj
|
||||
Wpf_Project_Path: Daybreak\Daybreak.csproj
|
||||
Wpf_Project_Path: Daybreak.Windows\Daybreak.Windows.csproj
|
||||
Actions_Allow_Unsecure_Commands: true
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
@@ -66,9 +63,9 @@ jobs:
|
||||
# If this step fails, then you should remove it and run the build manually (see below)
|
||||
|
||||
- name: Install .NET Core
|
||||
uses: actions/setup-dotnet@v4
|
||||
uses: actions/setup-dotnet@v5
|
||||
with:
|
||||
dotnet-version: '9.x'
|
||||
dotnet-version: '10.x'
|
||||
|
||||
#- name: Autobuild
|
||||
# uses: github/codeql-action/autobuild@v2
|
||||
@@ -84,7 +81,7 @@ jobs:
|
||||
# ./location_of_script_within_repo/buildscript.sh
|
||||
|
||||
- name: Build Daybreak project
|
||||
run: dotnet build Daybreak -c $env:Configuration --property:SolutionDir=$env:GITHUB_WORKSPACE
|
||||
run: dotnet build Daybreak.Windows -c $env:Configuration --property:SolutionDir=$env:GITHUB_WORKSPACE
|
||||
|
||||
- name: Perform CodeQL Analysis
|
||||
uses: github/codeql-action/analyze@v3
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
# Manually trigger to create a new release branch with bumped build version
|
||||
name: Create Build Release Branch
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
create-release:
|
||||
uses: ./.github/workflows/create-release-branch-template.yml
|
||||
with:
|
||||
versionBump: "*.*.^.0"
|
||||
secrets: inherit
|
||||
@@ -0,0 +1,12 @@
|
||||
# Manually trigger to create a new release branch with bumped major version
|
||||
name: Create Major Release Branch
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
create-release:
|
||||
uses: ./.github/workflows/create-release-branch-template.yml
|
||||
with:
|
||||
versionBump: "^.0.0.0"
|
||||
secrets: inherit
|
||||
@@ -0,0 +1,12 @@
|
||||
# Manually trigger to create a new release branch with bumped minor version
|
||||
name: Create Minor Release Branch
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
create-release:
|
||||
uses: ./.github/workflows/create-release-branch-template.yml
|
||||
with:
|
||||
versionBump: "*.^.0.0"
|
||||
secrets: inherit
|
||||
@@ -0,0 +1,66 @@
|
||||
# Reusable template for creating release branches with version bumps
|
||||
name: Create Release Branch Template
|
||||
|
||||
on:
|
||||
workflow_call:
|
||||
inputs:
|
||||
versionBump:
|
||||
description: "Version bump command: bump-major, bump-minor, bump-build, bump-revision"
|
||||
required: true
|
||||
type: string
|
||||
|
||||
jobs:
|
||||
create-release-branch:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
fetch-depth: 0
|
||||
token: ${{ secrets.RELEASE_PAT }}
|
||||
|
||||
- name: Configure Git
|
||||
run: |
|
||||
git config user.name "github-actions[bot]"
|
||||
git config user.email "github-actions[bot]@users.noreply.github.com"
|
||||
|
||||
- name: Bump version in Directory.Build.props
|
||||
id: bump
|
||||
uses: vers-one/dotnet-project-version-updater@v1.7
|
||||
with:
|
||||
file: "Directory.Build.props"
|
||||
version: ${{ inputs.versionBump }}
|
||||
|
||||
- name: Check if release branch already exists
|
||||
id: check_branch
|
||||
run: |
|
||||
branch_name="release/${{ steps.bump.outputs.newVersion }}"
|
||||
if git ls-remote --heads origin "$branch_name" | grep -q "$branch_name"; then
|
||||
echo "Branch $branch_name already exists"
|
||||
echo "exists=true" >> $GITHUB_OUTPUT
|
||||
else
|
||||
echo "Branch $branch_name does not exist"
|
||||
echo "exists=false" >> $GITHUB_OUTPUT
|
||||
fi
|
||||
|
||||
- name: Create release branch
|
||||
if: steps.check_branch.outputs.exists == 'false'
|
||||
run: |
|
||||
new_version="${{ steps.bump.outputs.newVersion }}"
|
||||
branch_name="release/$new_version"
|
||||
|
||||
# Create new branch from master
|
||||
git checkout -b "$branch_name"
|
||||
|
||||
# Commit the version change (already updated by the action)
|
||||
git add Directory.Build.props
|
||||
git commit -m "Bump version to $new_version"
|
||||
|
||||
# Push the new branch
|
||||
git push origin "$branch_name"
|
||||
|
||||
echo "✅ Created release branch: $branch_name with version $new_version"
|
||||
@@ -0,0 +1,20 @@
|
||||
# Automatically creates a new release branch with bumped revision
|
||||
# after a successful CD pipeline run on master
|
||||
name: Create Revision Release Branch
|
||||
|
||||
on:
|
||||
workflow_run:
|
||||
workflows: ["Daybreak CD Pipeline"]
|
||||
types:
|
||||
- completed
|
||||
branches:
|
||||
- master
|
||||
|
||||
jobs:
|
||||
create-release:
|
||||
# Only run if the CD pipeline succeeded
|
||||
if: ${{ github.event.workflow_run.conclusion == 'success' }}
|
||||
uses: ./.github/workflows/create-release-branch-template.yml
|
||||
with:
|
||||
versionBump: bump-revision
|
||||
secrets: inherit
|
||||
@@ -0,0 +1,47 @@
|
||||
name: Label Merged Issues
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
types: [closed]
|
||||
branches:
|
||||
- 'release/**'
|
||||
|
||||
jobs:
|
||||
label-issues:
|
||||
if: github.event.pull_request.merged == true
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Label linked issues as merged
|
||||
uses: actions/github-script@v8
|
||||
with:
|
||||
github-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
script: |
|
||||
const prBody = context.payload.pull_request.body || '';
|
||||
const prTitle = context.payload.pull_request.title || '';
|
||||
|
||||
// Match issue references like #123, fixes #123, closes #123, resolves #123
|
||||
const issuePattern = /(?:close[sd]?|fix(?:e[sd])?|resolve[sd]?)?[\s:]*#(\d+)/gi;
|
||||
const issueNumbers = new Set();
|
||||
|
||||
// Search in PR body and title
|
||||
const textToSearch = `${prTitle} ${prBody}`;
|
||||
let match;
|
||||
while ((match = issuePattern.exec(textToSearch)) !== null) {
|
||||
issueNumbers.add(parseInt(match[1]));
|
||||
}
|
||||
|
||||
// Apply 'merged' label to each linked issue
|
||||
for (const issueNumber of issueNumbers) {
|
||||
try {
|
||||
await github.rest.issues.addLabels({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
issue_number: issueNumber,
|
||||
labels: ['merged']
|
||||
});
|
||||
console.log(`Added 'merged' label to issue #${issueNumber}`);
|
||||
} catch (error) {
|
||||
console.log(`Failed to label issue #${issueNumber}: ${error.message}`);
|
||||
}
|
||||
}
|
||||
@@ -1,53 +1,55 @@
|
||||
name: Daybreak Version Check
|
||||
name: Daybreak Version Check
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
branches:
|
||||
- master
|
||||
paths:
|
||||
- "Daybreak/**"
|
||||
- "Daybreak.Core/**"
|
||||
- "Daybreak.Installer/**"
|
||||
- "Daybreak.API/**"
|
||||
- "Daybreak.Shared/**"
|
||||
- "Daybreak.Windows/**"
|
||||
- "Daybreak.Linux/**"
|
||||
|
||||
jobs:
|
||||
|
||||
check_version:
|
||||
|
||||
strategy:
|
||||
matrix:
|
||||
targetplatform: [x86]
|
||||
targetplatform: [x64]
|
||||
|
||||
runs-on: windows-latest
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
env:
|
||||
Configuration: Release
|
||||
Solution_Path: Daybreak.sln
|
||||
Solution_Path: Daybreak.slnx
|
||||
Actions_Allow_Unsecure_Commands: true
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Install .NET Core
|
||||
uses: actions/setup-dotnet@v4
|
||||
with:
|
||||
dotnet-version: '9.x'
|
||||
- name: Install .NET Core
|
||||
uses: actions/setup-dotnet@v5
|
||||
with:
|
||||
dotnet-version: "10.x"
|
||||
|
||||
- name: Get Latest Tag
|
||||
id: getLatestTag
|
||||
uses: WyriHaximus/github-action-get-previous-tag@v1
|
||||
- name: Get Latest Tag
|
||||
id: getLatestTag
|
||||
uses: WyriHaximus/github-action-get-previous-tag@v2
|
||||
|
||||
- name: Build Daybreak project
|
||||
run: dotnet build Daybreak -c $env:Configuration --property:SolutionDir=$env:GITHUB_WORKSPACE
|
||||
- name: Build Daybreak project
|
||||
run: dotnet build Daybreak.Linux -c $Configuration --property:SolutionDir=$GITHUB_WORKSPACE
|
||||
|
||||
- name: Set version variable
|
||||
run: |
|
||||
$version = .\Scripts\GetBuildVersion.ps1
|
||||
echo "::set-env name=Version::$version"
|
||||
- name: Get version
|
||||
id: get_version
|
||||
run: |
|
||||
version=$(grep '<Version>' Directory.Build.props | sed 's/.*<Version>\(.*\)<\/Version>.*/\1/')
|
||||
echo "version=$version" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Check version difference
|
||||
run: pwsh ./Scripts/CompareVersions.ps1 -currentVersion ${{ steps.get_version.outputs.version }} -lastVersion ${{ steps.getLatestTag.outputs.tag }}
|
||||
shell: pwsh
|
||||
|
||||
- name: Check version difference
|
||||
run: |
|
||||
.\Scripts\CompareVersions -currentVersion ${{ env.Version }} -lastVersion ${{ env.LatestReleaseTag }}
|
||||
env:
|
||||
LatestReleaseTag: ${{ steps.getLatestTag.outputs.tag }}
|
||||
@@ -365,3 +365,5 @@ MigrationBackup/
|
||||
FodyWeavers.xsd
|
||||
|
||||
.idea
|
||||
|
||||
Daybreak/wwwroot/wiki/*
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
[submodule "Daybreak.wiki"]
|
||||
path = Daybreak.wiki
|
||||
url = https://github.com/gwdevhub/Daybreak.wiki.git
|
||||
Vendored
+17
@@ -0,0 +1,17 @@
|
||||
{
|
||||
"version": "0.2.0",
|
||||
"configurations": [
|
||||
{
|
||||
"name": "Debug Daybreak.Linux",
|
||||
"type": "dotnet",
|
||||
"request": "launch",
|
||||
"projectPath": "${workspaceFolder}/Daybreak.Linux/Daybreak.Linux.csproj"
|
||||
},
|
||||
{
|
||||
"name": "Debug Daybreak.Windows",
|
||||
"type": "dotnet",
|
||||
"request": "launch",
|
||||
"projectPath": "${workspaceFolder}/Daybreak.Windows/Daybreak.Windows.csproj"
|
||||
}
|
||||
]
|
||||
}
|
||||
Vendored
+29
@@ -0,0 +1,29 @@
|
||||
{
|
||||
"dotnet.defaultSolution": "Daybreak.slnx",
|
||||
"omnisharp.projectLoadTimeout": 120,
|
||||
"csharp.semanticHighlighting.enabled": true,
|
||||
"search.followSymlinks": false,
|
||||
"search.exclude": {
|
||||
"**/.git": true,
|
||||
"**/.vs": true,
|
||||
"**/bin": true,
|
||||
"**/obj": true,
|
||||
"**/node_modules": true,
|
||||
"**/.idea": true,
|
||||
"**/Publish": true,
|
||||
},
|
||||
"files.watcherExclude": {
|
||||
"**/.git/**": true,
|
||||
"**/.vs/**": true,
|
||||
"**/bin/**": true,
|
||||
"**/obj/**": true,
|
||||
"**/node_modules/**": true,
|
||||
"**/Publish": true,
|
||||
},
|
||||
"files.exclude": {
|
||||
"**/.vs": true,
|
||||
"**/bin": true,
|
||||
"**/obj": true,
|
||||
"**/Publish": true
|
||||
}
|
||||
}
|
||||
Vendored
+29
@@ -0,0 +1,29 @@
|
||||
{
|
||||
"version": "2.0.0",
|
||||
"tasks": [
|
||||
{
|
||||
"label": "build-linux",
|
||||
"command": "dotnet",
|
||||
"type": "process",
|
||||
"args": [
|
||||
"build",
|
||||
"${workspaceFolder}/Daybreak.Linux/Daybreak.Linux.csproj",
|
||||
"/property:GenerateFullPaths=true",
|
||||
"/consoleloggerparameters:NoSummary"
|
||||
],
|
||||
"problemMatcher": "$msCompile"
|
||||
},
|
||||
{
|
||||
"label": "build-windows",
|
||||
"command": "dotnet",
|
||||
"type": "process",
|
||||
"args": [
|
||||
"build",
|
||||
"${workspaceFolder}/Daybreak.Windows/Daybreak.Windows.csproj",
|
||||
"/property:GenerateFullPaths=true",
|
||||
"/consoleloggerparameters:NoSummary"
|
||||
],
|
||||
"problemMatcher": "$msCompile"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -1,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>
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk.Web">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net9.0-windows</TargetFramework>
|
||||
<TargetFramework>$(TargetFrameworkVersionWindows)</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
<OutputType>Library</OutputType>
|
||||
<RuntimeIdentifier>win-x86</RuntimeIdentifier>
|
||||
<RuntimeIdentifier Condition="'$(RuntimeIdentifier)' == ''">win-x86</RuntimeIdentifier>
|
||||
<StripSymbols>true</StripSymbols>
|
||||
<InteropExports>true</InteropExports>
|
||||
<EnableRequestDelegateGenerator>true</EnableRequestDelegateGenerator>
|
||||
@@ -17,9 +17,29 @@
|
||||
<UseAppHost>false</UseAppHost>
|
||||
<EnableIISSupport>false</EnableIISSupport>
|
||||
<InvariantGlobalization>true</InvariantGlobalization>
|
||||
<AllowMissingPrunePackageData>true</AllowMissingPrunePackageData>
|
||||
<Platforms>x86</Platforms>
|
||||
|
||||
<!-- Prevent static web asset conflicts -->
|
||||
<StaticWebAssetProjectMode>Default</StaticWebAssetProjectMode>
|
||||
<GenerateStaticWebAssetsManifest>false</GenerateStaticWebAssetsManifest>
|
||||
<ResolveStaticWebAssetsInputsDependsOn>RemoveDuplicateStaticWebAssets;$(ResolveStaticWebAssetsInputsDependsOn)</ResolveStaticWebAssetsInputsDependsOn>
|
||||
</PropertyGroup>
|
||||
|
||||
<PropertyGroup Condition="'$(Configuration)' == 'Release'">
|
||||
<DebugType>none</DebugType>
|
||||
<DebugSymbols>false</DebugSymbols>
|
||||
</PropertyGroup>
|
||||
|
||||
<!-- Remove duplicate static web assets coming from referenced projects -->
|
||||
<Target Name="RemoveDuplicateStaticWebAssets" BeforeTargets="ResolveStaticWebAssetsInputs;GenerateStaticWebAssetsManifest">
|
||||
<ItemGroup>
|
||||
<!-- Remove all blazor webview static assets since this is a Web API project, not a Blazor Hybrid app -->
|
||||
<StaticWebAsset Remove="@(StaticWebAsset)"
|
||||
Condition="$([System.String]::new('%(RelativePath)').Contains('blazor.modules')) Or $([System.String]::new('%(RelativePath)').Contains('blazor.webview'))" />
|
||||
</ItemGroup>
|
||||
</Target>
|
||||
|
||||
<PropertyGroup>
|
||||
<!--Ignore trim warnings. NativeAOT support is still experimental so it should be tested with a Publish build generated from Daybreak build-->
|
||||
<NoWarn>IL2104</NoWarn>
|
||||
@@ -53,7 +73,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>
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -11,37 +11,109 @@ public abstract class GWFastCall(GWAddressCache target)
|
||||
public GWAddressCache Cache { get; } = target;
|
||||
public abstract void Initialize();
|
||||
|
||||
protected unsafe static nint BuildFastCallThunk(nuint target)
|
||||
/// <summary>
|
||||
/// Builds a thunk for __fastcall with 1 parameter (ECX only).
|
||||
/// </summary>
|
||||
protected unsafe static nint BuildFastCallThunk1(nuint target)
|
||||
{
|
||||
if (target is 0)
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
|
||||
var code = stackalloc byte[13]
|
||||
var code = stackalloc byte[10]
|
||||
{
|
||||
0x58, // pop eax (ret)
|
||||
0x59, // pop ecx (ctx)
|
||||
0x5A, // pop edx (edxVal)
|
||||
0x5B, // pop ebx (wparam)
|
||||
0x53, // push ebx (wparam)
|
||||
0x59, // pop ecx (arg1)
|
||||
0x50, // push eax (ret)
|
||||
0xB8,0,0,0,0, // mov eax, TARGET
|
||||
0xFF,0xE0 // jmp eax
|
||||
}; // 13 bytes total
|
||||
0xB8, 0, 0, 0, 0, // mov eax, TARGET
|
||||
0xFF, 0xE0 // jmp eax
|
||||
}; // 10 bytes total
|
||||
|
||||
const int TARGET_OFFSET = 7;
|
||||
const int TARGET_OFFSET = 4;
|
||||
Unsafe.WriteUnaligned(ref code[TARGET_OFFSET], (uint)target);
|
||||
|
||||
var mem = NativeMethods.VirtualAlloc(0, 13, NativeMethods.MEM_COMMIT | NativeMethods.MEM_RESERVE, NativeMethods.PAGE_EXECUTE_READWRITE);
|
||||
var mem = NativeMethods.VirtualAlloc(0, 10, NativeMethods.MEM_COMMIT | NativeMethods.MEM_RESERVE, NativeMethods.PAGE_EXECUTE_READWRITE);
|
||||
if (mem is 0)
|
||||
{
|
||||
throw new Win32Exception(Marshal.GetLastWin32Error(), "VirtualAlloc");
|
||||
}
|
||||
|
||||
Buffer.MemoryCopy(code, (void*)mem, 13, 13);
|
||||
Buffer.MemoryCopy(code, (void*)mem, 10, 10);
|
||||
return mem;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Builds a thunk for __fastcall with 2+ parameters (ECX and EDX).
|
||||
/// </summary>
|
||||
protected unsafe static nint BuildFastCallThunk2(nuint target)
|
||||
{
|
||||
if (target is 0)
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
|
||||
var code = stackalloc byte[11]
|
||||
{
|
||||
0x58, // pop eax (ret)
|
||||
0x59, // pop ecx (ctx)
|
||||
0x5A, // pop edx (edxVal)
|
||||
0x50, // push eax (ret)
|
||||
0xB8,0,0,0,0, // mov eax, TARGET
|
||||
0xFF,0xE0 // jmp eax
|
||||
}; // 11 bytes total
|
||||
|
||||
const int TARGET_OFFSET = 5;
|
||||
Unsafe.WriteUnaligned(ref code[TARGET_OFFSET], (uint)target);
|
||||
|
||||
var mem = NativeMethods.VirtualAlloc(0, 11, NativeMethods.MEM_COMMIT | NativeMethods.MEM_RESERVE, NativeMethods.PAGE_EXECUTE_READWRITE);
|
||||
if (mem is 0)
|
||||
{
|
||||
throw new Win32Exception(Marshal.GetLastWin32Error(), "VirtualAlloc");
|
||||
}
|
||||
|
||||
Buffer.MemoryCopy(code, (void*)mem, 11, 11);
|
||||
return mem;
|
||||
}
|
||||
}
|
||||
|
||||
public unsafe sealed class GWFastCall<T1, T2>(GWAddressCache target) : GWFastCall(target)
|
||||
{
|
||||
private delegate* unmanaged[Stdcall]<T1, T2> func = null;
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public unsafe T2? Invoke(T1 t1)
|
||||
{
|
||||
if (this.func is null)
|
||||
{
|
||||
this.Initialize();
|
||||
}
|
||||
|
||||
if (this.func is not null)
|
||||
{
|
||||
if (typeof(T2) == typeof(Void))
|
||||
{
|
||||
((delegate* unmanaged[Stdcall]<T1, void>)this.func)(t1);
|
||||
return default;
|
||||
}
|
||||
|
||||
return this.func(t1);
|
||||
}
|
||||
|
||||
return default;
|
||||
}
|
||||
|
||||
public override void Initialize()
|
||||
{
|
||||
if (this.Cache.GetAddress() is not nuint addr ||
|
||||
addr is 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var call = BuildFastCallThunk1(addr);
|
||||
this.func = (delegate* unmanaged[Stdcall]<T1, T2>)call;
|
||||
}
|
||||
}
|
||||
|
||||
public unsafe sealed class GWFastCall<T1, T2, T3>(GWAddressCache target) : GWFastCall(target)
|
||||
@@ -78,7 +150,7 @@ public unsafe sealed class GWFastCall<T1, T2, T3>(GWAddressCache target) : GWFas
|
||||
return;
|
||||
}
|
||||
|
||||
var call = BuildFastCallThunk(addr);
|
||||
var call = BuildFastCallThunk2(addr);
|
||||
this.func = (delegate* unmanaged[Stdcall]<T1, T2, T3>)call;
|
||||
}
|
||||
}
|
||||
@@ -117,7 +189,7 @@ public unsafe sealed class GWFastCall<T1, T2, T3, T4>(GWAddressCache target) : G
|
||||
return;
|
||||
}
|
||||
|
||||
var call = BuildFastCallThunk(addr);
|
||||
var call = BuildFastCallThunk2(addr);
|
||||
this.func = (delegate* unmanaged[Stdcall]<T1, T2, T3, T4>)call;
|
||||
}
|
||||
}
|
||||
@@ -156,7 +228,7 @@ public unsafe sealed class GWFastCall<T1, T2, T3, T4, T5>(GWAddressCache target)
|
||||
return;
|
||||
}
|
||||
|
||||
var call = BuildFastCallThunk(addr);
|
||||
var call = BuildFastCallThunk2(addr);
|
||||
this.func = (delegate* unmanaged[Stdcall]<T1, T2, T3, T4, T5>)call;
|
||||
}
|
||||
}
|
||||
@@ -195,7 +267,7 @@ public unsafe sealed class GWFastCall<T1, T2, T3, T4, T5, T6>(GWAddressCache tar
|
||||
return;
|
||||
}
|
||||
|
||||
var call = BuildFastCallThunk(addr);
|
||||
var call = BuildFastCallThunk2(addr);
|
||||
this.func = (delegate* unmanaged[Stdcall]<T1, T2, T3, T4, T5, T6>)call;
|
||||
}
|
||||
}
|
||||
@@ -234,7 +306,7 @@ public unsafe sealed class GWFastCall<T1, T2, T3, T4, T5, T6, T7>(GWAddressCache
|
||||
return;
|
||||
}
|
||||
|
||||
var call = BuildFastCallThunk(addr);
|
||||
var call = BuildFastCallThunk2(addr);
|
||||
this.func = (delegate* unmanaged[Stdcall]<T1, T2, T3, T4, T5, T6, T7>)call;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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
|
||||
];
|
||||
}
|
||||
@@ -0,0 +1,127 @@
|
||||
using System.Runtime.InteropServices;
|
||||
|
||||
namespace Daybreak.API.Interop.GuildWars;
|
||||
|
||||
public enum ItemType : byte
|
||||
{
|
||||
Salvage,
|
||||
Axe = 2,
|
||||
Bag,
|
||||
Boots,
|
||||
Bow,
|
||||
Bundle,
|
||||
Chestpiece,
|
||||
Rune_Mod,
|
||||
Usable,
|
||||
Dye,
|
||||
Materials_Zcoins,
|
||||
Offhand,
|
||||
Gloves,
|
||||
Hammer = 15,
|
||||
Headpiece,
|
||||
CC_Shards,
|
||||
Key,
|
||||
Leggings,
|
||||
Gold_Coin,
|
||||
Quest_Item,
|
||||
Wand,
|
||||
Shield = 24,
|
||||
Staff = 26,
|
||||
Sword,
|
||||
Kit = 29,
|
||||
Trophy,
|
||||
Scroll,
|
||||
Daggers,
|
||||
Present,
|
||||
Minipet,
|
||||
Scythe,
|
||||
Spear,
|
||||
Storybook = 43,
|
||||
Costume,
|
||||
Costume_Headpiece,
|
||||
Unknown = 0xff
|
||||
};
|
||||
|
||||
[StructLayout(LayoutKind.Sequential, Pack = 1)]
|
||||
public readonly struct ItemModifier(uint mod)
|
||||
{
|
||||
public static readonly ItemModifier Zero = new();
|
||||
|
||||
public readonly uint Mod = mod;
|
||||
|
||||
public uint Identifier => this.Mod >> 16;
|
||||
public uint Arg1 => (this.Mod & 0x0000FF00) >> 8;
|
||||
public uint Arg2 => this.Mod & 0x000000FF;
|
||||
public uint Arg => this.Mod & 0x0000FFFF;
|
||||
}
|
||||
|
||||
[StructLayout(LayoutKind.Explicit, Pack = 1)]
|
||||
public readonly unsafe struct Item
|
||||
{
|
||||
[FieldOffset(0x0000)]
|
||||
public readonly uint ItemId;
|
||||
|
||||
[FieldOffset(0x0004)]
|
||||
public readonly uint AgentId;
|
||||
|
||||
[FieldOffset(0x0008)]
|
||||
public readonly Bag* BagEquipped;
|
||||
|
||||
[FieldOffset(0x000C)]
|
||||
public readonly Bag* Bag;
|
||||
|
||||
[FieldOffset(0x0010)]
|
||||
public readonly ItemModifier* Modifiers;
|
||||
|
||||
[FieldOffset(0x0014)]
|
||||
public readonly uint ModifiersCount;
|
||||
|
||||
[FieldOffset(0x001C)]
|
||||
public readonly uint ModelFileId;
|
||||
|
||||
[FieldOffset(0x0020)]
|
||||
public readonly ItemType Type;
|
||||
|
||||
[FieldOffset(0x0024)]
|
||||
public readonly ushort Value;
|
||||
|
||||
[FieldOffset(0x0028)]
|
||||
public readonly uint Interaction;
|
||||
|
||||
[FieldOffset(0x002C)]
|
||||
public readonly uint ModelId;
|
||||
|
||||
[FieldOffset(0x0030)]
|
||||
public readonly char* InfoString;
|
||||
|
||||
[FieldOffset(0x0034)]
|
||||
public readonly char* NameEncoded;
|
||||
|
||||
[FieldOffset(0x0038)]
|
||||
public readonly char* CompleteNameEncoded;
|
||||
|
||||
[FieldOffset(0x003C)]
|
||||
public readonly char* SingleItemName;
|
||||
|
||||
[FieldOffset(0x0048)]
|
||||
public readonly ushort ItemFormula;
|
||||
|
||||
[FieldOffset(0x004A)]
|
||||
public readonly byte IsMaterialSalvageable;
|
||||
|
||||
[FieldOffset(0x004C)]
|
||||
public readonly ushort Quantity;
|
||||
|
||||
[FieldOffset(0x004E)]
|
||||
public readonly byte Equipped;
|
||||
|
||||
[FieldOffset(0x004F)]
|
||||
public readonly byte Profession;
|
||||
|
||||
[FieldOffset(0x0050)]
|
||||
public readonly uint Slot;
|
||||
|
||||
public readonly bool Stackable => (this.Interaction & 0x80000) != 0;
|
||||
|
||||
public readonly bool Inscribable => (this.Interaction & 0x08000000) != 0;
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
using System.Runtime.InteropServices;
|
||||
|
||||
namespace Daybreak.API.Interop.GuildWars;
|
||||
|
||||
[StructLayout(LayoutKind.Explicit, Pack = 1)]
|
||||
public readonly struct ItemContext
|
||||
{
|
||||
[FieldOffset(0x0024)]
|
||||
public readonly GuildWarsArray<WrappedPointer<Bag>> Bags;
|
||||
|
||||
[FieldOffset(0x00B8)]
|
||||
public readonly GuildWarsArray<WrappedPointer<Item>> Items;
|
||||
|
||||
[FieldOffset(0x00F8)]
|
||||
public readonly WrappedPointer<Inventory> Inventory;
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
using System.Runtime.InteropServices;
|
||||
|
||||
namespace Daybreak.API.Interop.GuildWars;
|
||||
|
||||
[StructLayout(LayoutKind.Explicit, Pack = 1, Size = 0x34)]
|
||||
public unsafe struct PartySearchWindowContext
|
||||
{
|
||||
[FieldOffset(0x0000)]
|
||||
public uint* Vtable;
|
||||
[FieldOffset(0x0004)]
|
||||
public uint FrameId;
|
||||
[FieldOffset(0x0020)]
|
||||
public uint SelectedPartySearchId;
|
||||
[FieldOffset(0x0024)]
|
||||
public uint SelectedHeroId;
|
||||
[FieldOffset(0x0028)]
|
||||
public uint SelectedHenchmanId;
|
||||
[FieldOffset(0x002C)]
|
||||
public uint CurrentTab;
|
||||
}
|
||||
@@ -9,7 +9,7 @@ public enum PlayerContextFlags : uint
|
||||
PvP = 0x800
|
||||
}
|
||||
|
||||
[StructLayout(LayoutKind.Explicit, Pack = 1, Size = 0x4C)]
|
||||
[StructLayout(LayoutKind.Explicit, Pack = 1, Size = 0x50)]
|
||||
public readonly unsafe struct PlayerContext
|
||||
{
|
||||
[FieldOffset(0x0000)]
|
||||
@@ -37,8 +37,11 @@ public readonly unsafe struct PlayerContext
|
||||
public readonly uint ActiveTitleTier;
|
||||
|
||||
[FieldOffset(0x0034)]
|
||||
public readonly uint PlayerNumber;
|
||||
public readonly uint ReforgedOrDhuumsFlags;
|
||||
|
||||
[FieldOffset(0x0038)]
|
||||
public readonly uint PlayerNumber;
|
||||
|
||||
[FieldOffset(0x003C)]
|
||||
public readonly uint PartySize;
|
||||
}
|
||||
|
||||
@@ -12,13 +12,7 @@ public readonly struct PreGameContext
|
||||
[FieldOffset(0x0124)]
|
||||
public readonly uint ChosenCharacterIndex;
|
||||
|
||||
[FieldOffset(0x0140)]
|
||||
public readonly uint Index1;
|
||||
|
||||
[FieldOffset(0x0144)]
|
||||
public readonly uint Index2;
|
||||
|
||||
[FieldOffset(0x00148)]
|
||||
[FieldOffset(0x0148)]
|
||||
public readonly GuildWarsArray<LoginCharacterContext> LoginCharacters;
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,40 @@
|
||||
namespace Daybreak.API.Interop.GuildWars;
|
||||
|
||||
public enum EnumPreference : uint
|
||||
{
|
||||
CharSortOrder = 0,
|
||||
AntiAliasing = 1,
|
||||
Reflections = 2,
|
||||
ShaderQuality = 3,
|
||||
ShadowQuality = 4,
|
||||
TerrainQuality = 5,
|
||||
InterfaceSize = 6,
|
||||
FrameLimiter = 7,
|
||||
Count = 8
|
||||
}
|
||||
|
||||
public enum CharSortOrder : uint
|
||||
{
|
||||
None = 0,
|
||||
Alphabetize = 1,
|
||||
PvPRP = 2
|
||||
}
|
||||
|
||||
public enum NumberPreference : uint
|
||||
{
|
||||
AutoTournPartySort = 0,
|
||||
ChatState = 1,
|
||||
Count = 0x2b
|
||||
}
|
||||
|
||||
public enum FlagPreference : uint
|
||||
{
|
||||
}
|
||||
|
||||
public enum StringPreference : uint
|
||||
{
|
||||
Unk1 = 0,
|
||||
Unk2 = 1,
|
||||
LastCharacterName = 2,
|
||||
Count = 3
|
||||
}
|
||||
@@ -14,9 +14,6 @@ public readonly struct QuestContext
|
||||
|
||||
[FieldOffset(0x0018)]
|
||||
public readonly Vector3 Marker;
|
||||
|
||||
[FieldOffset(0x0028)]
|
||||
public readonly uint MapTo;
|
||||
}
|
||||
|
||||
[StructLayout(LayoutKind.Sequential, Pack = 1, Size = 0xC)]
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
using System.Runtime.InteropServices;
|
||||
|
||||
namespace Daybreak.API.Interop.GuildWars;
|
||||
|
||||
[StructLayout(LayoutKind.Explicit, Pack = 1)]
|
||||
public struct TextParserContext
|
||||
{
|
||||
[FieldOffset(0x01D0)]
|
||||
public Language Language;
|
||||
}
|
||||
@@ -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>()
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -3,152 +3,16 @@
|
||||
public enum UIMessage : uint
|
||||
{
|
||||
None = 0x0,
|
||||
Resize = 0x8,
|
||||
InitFrame = 0x9,
|
||||
DestroyFrame = 0xb,
|
||||
KeyDown = 0x1e, // wparam = UIPacket::kKeyAction*
|
||||
KeyUp = 0x20, // wparam = UIPacket::kKeyAction*
|
||||
MouseClick = 0x22, // wparam = UIPacket::kMouseClick*
|
||||
MouseClick2 = 0x2e, // wparam = UIPacket::kMouseAction*
|
||||
MouseAction = 0x2f, // wparam = UIPacket::kMouseAction*
|
||||
SetLayout = 0x33,
|
||||
FrameMessage_0x47 = 0x47, // Multiple uses depending on frame
|
||||
UpdateAgentEffects = 0x10000000 | 0x9,
|
||||
RerenderAgentModel = 0x10000000 | 0x7, // wparam = uint32_t agent_id
|
||||
AgentSpeechBubble = 0x10000000 | 0x17,
|
||||
ShowAgentNameTag = 0x10000000 | 0x19, // wparam = AgentNameTagInfo*
|
||||
HideAgentNameTag = 0x10000000 | 0x1A,
|
||||
SetAgentNameTagAttribs = 0x10000000 | 0x1B, // wparam = AgentNameTagInfo*
|
||||
ChangeTarget = 0x10000000 | 0x20, // wparam = UIPacket::kChangeTarget*
|
||||
AgentStartCasting = 0x10000000 | 0x27, // wparam = UIPacket::kAgentStartCasting*
|
||||
ShowMapEntryMessage = 0x10000000 | 0x29, // wparam = { wchar_t* title, wchar_t* subtitle }
|
||||
SetCurrentPlayerData = 0x10000000 | 0x2A, // fired after setting the worldcontext player name
|
||||
PostProcessingEffect = 0x10000000 | 0x34, // Triggered when drunk. wparam = UIPacket::kPostProcessingEffect
|
||||
HeroAgentAdded = 0x10000000 | 0x38, // hero assigned to agent/inventory/ai mode
|
||||
HeroDataAdded = 0x10000000 | 0x39, // hero info received from server (name, level etc)
|
||||
ShowXunlaiChest = 0x10000000 | 0x40,
|
||||
MinionCountUpdated = 0x10000000 | 0x46,
|
||||
MoraleChange = 0x10000000 | 0x47, // wparam = {agent id, morale percent }
|
||||
LoginStateChanged = 0x10000000 | 0x50, // wparam = {bool is_logged_in, bool unk }
|
||||
EffectAdd = 0x10000000 | 0x55, // wparam = {agent_id, GW::Effect*}
|
||||
EffectRenew = 0x10000000 | 0x56, // wparam = GW::Effect*
|
||||
EffectRemove = 0x10000000 | 0x57, // wparam = effect id
|
||||
SkillActivated = 0x10000000 | 0x5b, // wparam ={ uint32_t agent_id , uint32_t skill_id }
|
||||
UpdateSkillbar = 0x10000000 | 0x5E, // wparam ={ uint32_t agent_id , ... }
|
||||
UpdateSkillsAvailable = 0x10000000 | 0x5f, // Triggered on a skill unlock, profession change or map load
|
||||
TitleProgressUpdated = 0x10000000 | 0x65, // wparam = title_id
|
||||
ExperienceGained = 0x10000000 | 0x66, // wparam = experience amount
|
||||
WriteToChatLog = 0x10000000 | 0x7E, // wparam = UIPacket::kWriteToChatLog*. Triggered by the game when it wants to add a new message to chat.
|
||||
WriteToChatLogWithSender = 0x10000000 | 0x7f, // wparam = UIPacket::kWriteToChatLogWithSender*. Triggered by the game when it wants to add a new message to chat.
|
||||
AllyOrGuildMessage = 0x10000000 | 0x80, // wparam = UIPacket::kAllyOrGuildMessage*
|
||||
PlayerChatMessage = 0x10000000 | 0x81, // wparam = UIPacket::kPlayerChatMessage*
|
||||
FloatingWindowMoved = 0x10000000 | 0x83, // wparam = frame_id
|
||||
FriendUpdated = 0x10000000 | 0x89, // wparam = { GW::Friend*, ... }
|
||||
MapLoaded = 0x10000000 | 0x8A,
|
||||
OpenWhisper = 0x10000000 | 0x90, // wparam = wchar* name
|
||||
Logout = 0x10000000 | 0x9b, // wparam = { bool unknown, bool character_select }
|
||||
CompassDraw = 0x10000000 | 0x9c, // wparam = UIPacket::kCompassDraw*
|
||||
OnScreenMessage = 0x10000000 | 0xA0, // wparam = wchar_** encoded_string
|
||||
DialogBody = 0x10000000 | 0xA4, // wparam = DialogBodyInfo*
|
||||
DialogButton = 0x10000000 | 0xA1, // wparam = DialogButtonInfo*
|
||||
TargetNPCPartyMember = 0x10000000 | 0xB1, // wparam = { uint32_t unk, uint32_t agent_id }
|
||||
TargetPlayerPartyMember = 0x10000000 | 0xB2, // wparam = { uint32_t unk, uint32_t player_number }
|
||||
VendorWindow = 0x10000000 | 0xB3, // wparam = UIPacket::kVendorWindow
|
||||
VendorItems = 0x10000000 | 0xB7, // wparam = UIPacket::kVendorItems
|
||||
VendorTransComplete = 0x10000000 | 0xB9, // wparam = *TransactionType
|
||||
VendorQuote = 0x10000000 | 0xBB, // wparam = UIPacket::kVendorQuote
|
||||
StartMapLoad = 0x10000000 | 0xC0, // wparam = { uint32_t map_id, ...}
|
||||
WorldMapUpdated = 0x10000000 | 0xC5, // Triggered when an area in the world map has been discovered/updated
|
||||
GuildMemberUpdated = 0x10000000 | 0xD8, // wparam = { GuildPlayer::name_ptr }
|
||||
ShowHint = 0x10000000 | 0xDF, // wparam = { uint32_t icon_type, wchar_t* message_enc }
|
||||
WeaponSetSwapComplete = 0x10000000 | 0xE7, // wparam = UIPacket::kWeaponSwap*
|
||||
WeaponSetSwapCancel = 0x10000000 | 0xE8,
|
||||
WeaponSetUpdated = 0x10000000 | 0xE9,
|
||||
UpdateGoldCharacter = 0x10000000 | 0xEA, // wparam = { uint32_t unk, uint32_t gold_character }
|
||||
UpdateGoldStorage = 0x10000000 | 0xEB, // wparam = { uint32_t unk, uint32_t gold_storage }
|
||||
InventorySlotUpdated = 0x10000000 | 0xEC, // undocumented. Triggered when an item is moved into a slot
|
||||
EquipmentSlotUpdated = 0x10000000 | 0xED, // undocumented. Triggered when an item is moved into a slot
|
||||
InventorySlotCleared = 0x10000000 | 0xEF, // undocumented. Triggered when an item has been removed from a slot
|
||||
EquipmentSlotCleared = 0x10000000 | 0xF0, // undocumented. Triggered when an item has been removed from a slot
|
||||
PvPWindowContent = 0x10000000 | 0xF8,
|
||||
PreStartSalvage = 0x10000000 | 0x100, // { uint32_t item_id, uint32_t kit_id }
|
||||
TradePlayerUpdated = 0x10000000 | 0x103, // wparam = GW::TraderPlayer*
|
||||
ItemUpdated = 0x10000000 | 0x104, // wparam = UIPacket::kItemUpdated*
|
||||
MapChange = 0x10000000 | 0x10F, // wparam = map id
|
||||
CalledTargetChange = 0x10000000 | 0x113, // wparam = { player_number, target_id }
|
||||
ErrorMessage = 0x10000000 | 0x117, // wparam = { int error_index, wchar_t* error_encoded_string }
|
||||
PartyHardModeChanged = 0x10000000 | 0x118, // wparam = { int is_hard_mode }
|
||||
PartyAddHenchman = 0x10000000 | 0x119,
|
||||
PartyRemoveHenchman = 0x10000000 | 0x11a,
|
||||
PartyAddHero = 0x10000000 | 0x11c,
|
||||
PartyRemoveHero = 0x10000000 | 0x11d,
|
||||
PartyAddPlayer = 0x10000000 | 0x122,
|
||||
PartyRemovePlayer = 0x10000000 | 0x124,
|
||||
DisableEnterMissionBtn = 0x10000000 | 0x128, // wparam = boolean (1 = disabled, 0 = enabled)
|
||||
ShowCancelEnterMissionBtn = 0x10000000 | 0x12b,
|
||||
PartyDefeated = 0x10000000 | 0x12d,
|
||||
PartySearchInviteReceived = 0x10000000 | 0x135, // wparam = UIPacket::kPartySearchInviteReceived*
|
||||
PartySearchInviteSent = 0x10000000 | 0x137,
|
||||
PartyShowConfirmDialog = 0x10000000 | 0x138, // wparam = UIPacket::kPartyShowConfirmDialog
|
||||
PreferenceEnumChanged = 0x10000000 | 0x13E, // wparam = UiPacket::kPreferenceEnumChanged
|
||||
PreferenceFlagChanged = 0x10000000 | 0x13F, // wparam = UiPacket::kPreferenceFlagChanged
|
||||
PreferenceValueChanged = 0x10000000 | 0x140, // wparam = UiPacket::kPreferenceValueChanged
|
||||
UIPositionChanged = 0x10000000 | 0x141, // wparam = UIPacket::kUIPositionChanged
|
||||
QuestAdded = 0x10000000 | 0x149, // wparam = { quest_id, ... }
|
||||
QuestDetailsChanged = 0x10000000 | 0x14A, // wparam = { quest_id, ... }
|
||||
ClientActiveQuestChanged = 0x10000000 | 0x14C, // wparam = { quest_id, ... }. Triggered when the game requests the current quest to change
|
||||
ServerActiveQuestChanged = 0x10000000 | 0x14E, // wparam = UIPacket::kServerActiveQuestChanged*. Triggered when the server requests the current quest to change
|
||||
UnknownQuestRelated = 0x10000000 | 0x14F,
|
||||
DungeonComplete = 0x10000000 | 0x151, // undocumented
|
||||
MissionComplete = 0x10000000 | 0x152, // undocumented
|
||||
VanquishComplete = 0x10000000 | 0x154, // undocumented
|
||||
ObjectiveAdd = 0x10000000 | 0x155, // wparam = UIPacket::kObjectiveAdd*
|
||||
ObjectiveComplete = 0x10000000 | 0x156, // wparam = UIPacket::kObjectiveComplete*
|
||||
ObjectiveUpdated = 0x10000000 | 0x157, // wparam = UIPacket::kObjectiveUpdated*
|
||||
TradeSessionStart = 0x10000000 | 0x160, // wparam = { trade_state, player_number }
|
||||
TradeSessionUpdated = 0x10000000 | 0x166, // no args
|
||||
TriggerLogoutPrompt = 0x10000000 | 0x16C, // no args
|
||||
ToggleOptionsWindow = 0x10000000 | 0x16D, // no args
|
||||
CheckUIState = 0x10000000 | 0x170, // Undocumented
|
||||
RedrawItem = 0x10000000 | 0x172, // wparam = uint32_t item_id
|
||||
CloseSettings = 0x10000000 | 0x174, // Undocumented
|
||||
ChangeSettingsTab = 0x10000000 | 0x175, // wparam = uint32_t is_interface_tab
|
||||
GuildHall = 0x10000000 | 0x177, // wparam = gh key (uint32_t[4])
|
||||
LeaveGuildHall = 0x10000000 | 0x179,
|
||||
Travel = 0x10000000 | 0x17A,
|
||||
OpenWikiUrl = 0x10000000 | 0x17B, // wparam = char* url
|
||||
AppendMessageToChat = 0x10000000 | 0x189, // wparam = wchar_t* message
|
||||
HideHeroPanel = 0x10000000 | 0x197, // wparam = hero_id
|
||||
ShowHeroPanel = 0x10000000 | 0x198, // wparam = hero_id
|
||||
GetInventoryAgentId = 0x10000000 | 0x19c, // wparam = 0, lparam = uint32_t* agent_id_out. Used to fetch which agent is selected
|
||||
EquipItem = 0x10000000 | 0x19d, // wparam = { item_id, agent_id }
|
||||
MoveItem = 0x10000000 | 0x19e, // wparam = { item_id, to_bag, to_slot, bool prompt }
|
||||
InitiateTrade = 0x10000000 | 0x1A0,
|
||||
InventoryAgentChanged = 0x10000000 | 0x1b0, // Triggered when inventory needs updating due to agent change; no args
|
||||
OpenTemplate = 0x10000000 | 0x1B9, // wparam = GW::UI::ChatTemplate*
|
||||
|
||||
// GWCA Client to Server commands. Only added the ones that are used for hooks, everything else goes straight into GW
|
||||
|
||||
SendLoadSkillTemplate = 0x30000000 | 0x3, // wparam = SkillbarMgr::SkillTemplate*
|
||||
SendPingWeaponSet = 0x30000000 | 0x4, // wparam = UIPacket::kSendPingWeaponSet*
|
||||
SendMoveItem = 0x30000000 | 0x5, // wparam = UIPacket::kSendMoveItem*
|
||||
SendMerchantRequestQuote = 0x30000000 | 0x6, // wparam = UIPacket::kSendMerchantRequestQuote*
|
||||
SendMerchantTransactItem = 0x30000000 | 0x7, // wparam = UIPacket::kSendMerchantTransactItem*
|
||||
SendUseItem = 0x30000000 | 0x8, // wparam = UIPacket::kSendUseItem*
|
||||
SendSetActiveQuest = 0x30000000 | 0x9, // wparam = uint32_t quest_id
|
||||
SendAbandonQuest = 0x30000000 | 0xA, // wparam = uint32_t quest_id
|
||||
SendChangeTarget = 0x30000000 | 0xB, // wparam = UIPacket::kSendChangeTarget* // e.g. tell the gw client to focus on a different target
|
||||
SendCallTarget = 0x30000000 | 0x13, // wparam = { uint32_t call_type, uint32_t agent_id } // also used to broadcast morale, death penalty, "I'm following X", etc
|
||||
SendDialog = 0x30000000 | 0x16, // wparam = dialog_id // internal use
|
||||
|
||||
|
||||
StartWhisper = 0x30000000 | 0x17, // wparam = UIPacket::kStartWhisper*
|
||||
GetSenderColor = 0x30000000 | 0x18, // wparam = UIPacket::kGetColor* // Get chat sender color depending on channel, output object passed by reference
|
||||
GetMessageColor = 0x30000000 | 0x19, // wparam = UIPacket::kGetColor* // Get chat message color depending on channel, output object passed by reference
|
||||
SendChatMessage = 0x30000000 | 0x1B, // wparam = UIPacket::kSendChatMessage*
|
||||
LogChatMessage = 0x30000000 | 0x1D, // wparam = UIPacket::kLogChatMessage*. Triggered when a message wants to be added to the persistent chat log.
|
||||
RecvWhisper = 0x30000000 | 0x1E, // wparam = UIPacket::kRecvWhisper*
|
||||
PrintChatMessage = 0x30000000 | 0x1F, // wparam = UIPacket::kPrintChatMessage*. Triggered when a message wants to be added to the in-game chat window.
|
||||
SendWorldAction = 0x30000000 | 0x20, // wparam = UIPacket::kSendWorldAction*
|
||||
SetRendererValue = 0x30000000 | 0x21 // wparam = UIPacket::kSetRendererValue
|
||||
};
|
||||
KeyDown = 0x20, // wparam = UIPacket::kKeyAction*
|
||||
KeyUp = 0x22, // wparam = UIPacket::kKeyAction*
|
||||
MouseClick = 0x24, // wparam = UIPacket::kMouseClick*
|
||||
MouseClick2 = 0x31, // wparam = UIPacket::kMouseAction*
|
||||
MouseAction = 0x32, // wparam = UIPacket::kMouseAction*
|
||||
FrameMessage_QuerySelectedIndex = 0x59, // Used to query selected index in character selector
|
||||
WriteToChatLog = 0x10000000 | 0x7F, // wparam = UIPacket::kWriteToChatLog*. Triggered by the game when it wants to add a new message to chat.
|
||||
WriteToChatLogWithSender = 0x10000000 | 0x80, // wparam = UIPacket::kWriteToChatLogWithSender*. Triggered by the game when it wants to add a new message to chat.
|
||||
CheckUIState = 0x10000000 | 0x118, // lparam = uint32_t* out state (2 = char select ready)
|
||||
Logout = 0x10000000 | 0x9D, // wparam = { bool unknown, bool character_select }
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,6 +11,7 @@ internal unsafe static partial class NativeMethods
|
||||
public const int WM_KEYDOWN = 0x0100;
|
||||
public const int WM_KEYUP = 0x0101;
|
||||
public const int WM_CHAR = 0x0102;
|
||||
public const int VK_LEFT = 0x25;
|
||||
public const int VK_RIGHT = 0x27;
|
||||
|
||||
public const uint MEM_COMMIT = 0x1000;
|
||||
@@ -36,7 +37,7 @@ internal unsafe static partial class NativeMethods
|
||||
public static partial bool SetConsoleMode(nint hConsoleHandle, uint dwMode);
|
||||
|
||||
[LibraryImport("user32.dll")]
|
||||
public static partial nint SendMessageW(nint hWnd, int Msg, nint wParam, nint lParam);
|
||||
public static partial nint SendMessageW(nint hWnd, int Msg, nint wParam, nuint lParam);
|
||||
|
||||
[LibraryImport("kernel32.dll", SetLastError = true)]
|
||||
public static partial nint VirtualAlloc(
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
using Daybreak.API.Models;
|
||||
using Daybreak.Shared.Models.Api;
|
||||
using Daybreak.Shared.Models.Guildwars;
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
@@ -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
|
||||
{
|
||||
}
|
||||
|
||||
@@ -1,56 +0,0 @@
|
||||
using Daybreak.Shared.Models;
|
||||
using Daybreak.Shared.Services.MDns;
|
||||
using Microsoft.AspNetCore.Hosting.Server;
|
||||
using Microsoft.AspNetCore.Hosting.Server.Features;
|
||||
using System.Extensions.Core;
|
||||
|
||||
namespace Daybreak.API.Services;
|
||||
|
||||
public sealed class ApiAdvertisingService(
|
||||
IServer server,
|
||||
IMDomainNameService mDnsService,
|
||||
IHostApplicationLifetime lifetime,
|
||||
ILogger<ApiAdvertisingService> logger) : IHostedService
|
||||
{
|
||||
private const string ProcessIdPlaceholder = "{PID}";
|
||||
private const string ServiceName = $"daybreak-api-{ProcessIdPlaceholder}";
|
||||
private const string ServiceSubType = "daybreak-api";
|
||||
private readonly IServerAddressesFeature serverAddressesFeature = server.Features.Get<IServerAddressesFeature>() ?? throw new InvalidOperationException("Server does not support addresses feature.");
|
||||
private readonly IMDomainNameService mDnsService = mDnsService;
|
||||
private readonly IHostApplicationLifetime lifetime = lifetime;
|
||||
private readonly ILogger<ApiAdvertisingService> logger = logger;
|
||||
|
||||
private DnsRegistrationToken? dnsToken;
|
||||
|
||||
public Task StartAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
this.lifetime.ApplicationStarted.Register(this.RegisterDomain);
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
public Task StopAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
this.dnsToken?.Dispose();
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
private void RegisterDomain()
|
||||
{
|
||||
var scopedLogger = this.logger.CreateScopedLogger();
|
||||
var addresses = this.serverAddressesFeature?.Addresses.ToArray() ?? [];
|
||||
var port = addresses
|
||||
.Select(a => new Uri(a).Port)
|
||||
.Distinct()
|
||||
.FirstOrDefault();
|
||||
|
||||
scopedLogger.LogInformation("Found port {port}", port);
|
||||
if (port is 0)
|
||||
{
|
||||
throw new InvalidOperationException("Unable to start advertising service. No port found");
|
||||
}
|
||||
|
||||
var dnsEntry = ServiceName.Replace(ProcessIdPlaceholder, Environment.ProcessId.ToString());
|
||||
scopedLogger.LogInformation("Advertising service {service}:{port}", dnsEntry, port);
|
||||
this.dnsToken = this.mDnsService.RegisterDomain(dnsEntry, (ushort)port, ServiceSubType);
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,6 @@
|
||||
using Daybreak.API.Services.Interop;
|
||||
using Daybreak.API.Interop.GuildWars;
|
||||
using Daybreak.API.Models;
|
||||
using Daybreak.API.Services.Interop;
|
||||
using Daybreak.Shared.Models.Api;
|
||||
using System.Core.Extensions;
|
||||
using System.Extensions.Core;
|
||||
@@ -13,6 +15,7 @@ public sealed class CharacterSelectService(
|
||||
UIContextService uiContextService,
|
||||
GameThreadService gameThreadService,
|
||||
GameContextService gameContextService,
|
||||
PreferencesService preferencesService,
|
||||
ILogger<CharacterSelectService> logger)
|
||||
{
|
||||
[StructLayout(LayoutKind.Sequential, Pack = 1)]
|
||||
@@ -27,6 +30,7 @@ public sealed class CharacterSelectService(
|
||||
private readonly UIContextService uiContextService = uiContextService.ThrowIfNull();
|
||||
private readonly GameThreadService gameThreadService = gameThreadService.ThrowIfNull();
|
||||
private readonly GameContextService gameContextService = gameContextService.ThrowIfNull();
|
||||
private readonly PreferencesService preferencesService = preferencesService.ThrowIfNull();
|
||||
private readonly ILogger<CharacterSelectService> logger = logger.ThrowIfNull();
|
||||
|
||||
public Task<CharacterSelectInformation?> GetCharacterSelectInformation(CancellationToken cancellationToken)
|
||||
@@ -53,7 +57,7 @@ public sealed class CharacterSelectService(
|
||||
|
||||
var currentUuid = gameContext.Pointer->CharContext->PlayerUuid.ToString();
|
||||
var availableChars = new List<CharacterSelectEntry>((int)availableCharsContext.Pointer->Size);
|
||||
foreach(var charContext in *availableCharsContext.Pointer)
|
||||
foreach (var charContext in *availableCharsContext.Pointer)
|
||||
{
|
||||
var nameSpan = charContext.Name.AsSpan();
|
||||
var name = new string(nameSpan[..nameSpan.IndexOf('\0')]);
|
||||
@@ -68,8 +72,17 @@ public sealed class CharacterSelectService(
|
||||
charContext.IsPvp));
|
||||
}
|
||||
|
||||
var currentCharacter = availableChars.FirstOrDefault(c => c.Uuid == currentUuid);
|
||||
return new CharacterSelectInformation(currentCharacter, availableChars);
|
||||
// TODO: Reforged sometimes returns Zero UUID even when in-game, need to investigate why
|
||||
if (currentUuid != Uuid.Zero.ToString())
|
||||
{
|
||||
var currentCharacter = availableChars.FirstOrDefault(c => c.Uuid == currentUuid);
|
||||
return new CharacterSelectInformation(currentCharacter, availableChars);
|
||||
}
|
||||
else
|
||||
{
|
||||
var currentCharacter = availableChars.FirstOrDefault();
|
||||
return new CharacterSelectInformation(currentCharacter, availableChars);
|
||||
}
|
||||
}
|
||||
}, cancellationToken);
|
||||
}
|
||||
@@ -85,20 +98,10 @@ public sealed class CharacterSelectService(
|
||||
|
||||
await this.TriggerLogOut(cancellationToken);
|
||||
|
||||
var indexResult = await this.WaitForLoginScreenAndGetCurrentAndDesiredIndex(characterName, cancellationToken);
|
||||
if (indexResult is null)
|
||||
var selectResult = await this.WaitForCharSelectAndSelectCharacter(characterName, cancellationToken);
|
||||
if (!selectResult)
|
||||
{
|
||||
scopedLogger.LogError("Failed to find index by name {name}", characterName);
|
||||
return false;
|
||||
}
|
||||
|
||||
var currentIndex = indexResult.Value.CurrentIndex;
|
||||
var desiredIndex = indexResult.Value.DesiredIndex;
|
||||
scopedLogger.LogInformation("Changing character to {name} with index {index}", characterName, desiredIndex);
|
||||
var navigateResult = await this.NavigateToCharAndPlay(currentIndex, desiredIndex, cancellationToken);
|
||||
if (!navigateResult)
|
||||
{
|
||||
scopedLogger.LogError("Failed to navigate to character {name} with index {index}", characterName, desiredIndex);
|
||||
scopedLogger.LogError("Failed to select character {name}", characterName);
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -123,86 +126,154 @@ public sealed class CharacterSelectService(
|
||||
|
||||
await this.TriggerLogOut(cancellationToken);
|
||||
|
||||
var indexResult = await this.WaitForLoginScreenAndGetCurrentAndDesiredIndex(desiredCharName, cancellationToken);
|
||||
if (indexResult is null)
|
||||
var selectResult = await this.WaitForCharSelectAndSelectCharacter(desiredCharName, cancellationToken);
|
||||
if (!selectResult)
|
||||
{
|
||||
scopedLogger.LogError("Resolved character by uuid {uuid} but failed to find index by name {name}", uuid, desiredCharName);
|
||||
return false;
|
||||
}
|
||||
|
||||
var currentIndex = indexResult.Value.CurrentIndex;
|
||||
var desiredIndex = indexResult.Value.DesiredIndex;
|
||||
scopedLogger.LogInformation("Changing character to {name} with index {index} and uuid {uuid}", desiredCharName, desiredIndex, uuid);
|
||||
var navigateResult = await this.NavigateToCharAndPlay(currentIndex, desiredIndex, cancellationToken);
|
||||
if (!navigateResult)
|
||||
{
|
||||
scopedLogger.LogError("Failed to navigate to character {name} with index {index} and uuid {uuid}", desiredCharName, desiredIndex, uuid);
|
||||
scopedLogger.LogError("Failed to select character {name} with uuid {uuid}", desiredCharName, uuid);
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private async Task<bool> ValidateState(CancellationToken cancellationToken)
|
||||
private async Task<bool> SelectCharacterToPlay(string characterName, bool play, CancellationToken cancellationToken)
|
||||
{
|
||||
var scopedLogger = this.logger.CreateScopedLogger();
|
||||
return await this.gameThreadService.QueueOnGameThread(() =>
|
||||
{
|
||||
return this.instanceContextService.GetInstanceType() is not API.Interop.GuildWars.InstanceType.Loading;
|
||||
unsafe
|
||||
{
|
||||
var selectorFrame = this.GetCharSelectorFrame();
|
||||
if (selectorFrame is null)
|
||||
{
|
||||
scopedLogger.LogError("Character selector frame not found");
|
||||
return false;
|
||||
}
|
||||
|
||||
var ctx = this.uiContextService.GetFrameContext<CharSelectorContext>(selectorFrame);
|
||||
if (ctx.IsNull)
|
||||
{
|
||||
scopedLogger.LogError("Character selector context not found");
|
||||
return false;
|
||||
}
|
||||
|
||||
var panesFrame = this.uiContextService.GetChildFrame(selectorFrame, 0);
|
||||
if (panesFrame.IsNull)
|
||||
{
|
||||
scopedLogger.LogError("Character panes frame not found");
|
||||
return false;
|
||||
}
|
||||
|
||||
// Get current selected index
|
||||
var selectedIdx = 0;
|
||||
this.uiContextService.SendFrameUIMessage(panesFrame, UIMessage.FrameMessage_QuerySelectedIndex, null, &selectedIdx);
|
||||
|
||||
var chosen = false;
|
||||
for (uint i = 0; !chosen && i < ctx.Pointer->Chars.Size; i++)
|
||||
{
|
||||
var c = ctx.Pointer->Chars.Buffer[i];
|
||||
var charNameSpan = c.Pointer->Name.AsSpan();
|
||||
var nullIdx = charNameSpan.IndexOf('\0');
|
||||
if (nullIdx <= 0)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var charName = new string(charNameSpan[..nullIdx]);
|
||||
if (!charName.StartsWith(characterName, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
while (selectedIdx != i)
|
||||
{
|
||||
var keyAction = new UIPackets.KeyAction(0x1c);
|
||||
this.uiContextService.SendFrameUIMessage(panesFrame, UIMessage.KeyDown, &keyAction);
|
||||
|
||||
var newIdx = selectedIdx;
|
||||
this.uiContextService.SendFrameUIMessage(panesFrame, UIMessage.FrameMessage_QuerySelectedIndex, null, &newIdx);
|
||||
|
||||
if (newIdx == selectedIdx)
|
||||
{
|
||||
break; // This shouldn't happen - the character should have changed
|
||||
}
|
||||
|
||||
selectedIdx = newIdx;
|
||||
}
|
||||
|
||||
chosen = selectedIdx == i;
|
||||
break;
|
||||
}
|
||||
|
||||
if (!chosen)
|
||||
{
|
||||
scopedLogger.LogError("Failed to select character {name}", characterName);
|
||||
return false;
|
||||
}
|
||||
|
||||
// TODO: This needs to be reworked to use UI messages to click on Play
|
||||
var hwnd = this.platformContextService.GetWindowHandle();
|
||||
if (!hwnd.HasValue)
|
||||
{
|
||||
scopedLogger.LogError("Failed to get game window handle");
|
||||
return false;
|
||||
}
|
||||
|
||||
NativeMethods.SendMessageW((nint)hwnd.Value, NativeMethods.WM_KEYDOWN, 0x50, 0x00190001);
|
||||
NativeMethods.SendMessageW((nint)hwnd.Value, NativeMethods.WM_CHAR, 0x70, 0x00190001);
|
||||
NativeMethods.SendMessageW((nint)hwnd.Value, NativeMethods.WM_KEYUP, 0x50, 0x00190001);
|
||||
return true;
|
||||
}
|
||||
}, cancellationToken);
|
||||
}
|
||||
|
||||
private async Task<bool> NavigateToCharAndPlay(uint currentIndex, uint desiredIndex, CancellationToken cancellationToken)
|
||||
private async Task<bool> WaitForCharSelectAndSelectCharacter(string characterName, CancellationToken cancellationToken)
|
||||
{
|
||||
var scopedLogger = this.logger.CreateScopedLogger();
|
||||
|
||||
// Wait for character select to be ready
|
||||
while (!cancellationToken.IsCancellationRequested)
|
||||
{
|
||||
await Task.Delay(100, cancellationToken);
|
||||
var result = await this.gameThreadService.QueueOnGameThread(() =>
|
||||
try
|
||||
{
|
||||
unsafe
|
||||
if (await this.gameThreadService.QueueOnGameThread(this.IsCharSelectReady, cancellationToken))
|
||||
{
|
||||
var preGameContext = this.gameContextService.GetPreGameContext();
|
||||
if (preGameContext.IsNull)
|
||||
{
|
||||
scopedLogger.LogError("Pre-game context is not initialized");
|
||||
return false;
|
||||
}
|
||||
|
||||
var hwnd = this.platformContextService.GetWindowHandle();
|
||||
if (hwnd is null or 0)
|
||||
{
|
||||
scopedLogger.LogError("Failed to get window handle");
|
||||
return false;
|
||||
}
|
||||
|
||||
if (preGameContext.Pointer->Index1 == currentIndex)
|
||||
{
|
||||
return false; //Not moved yet
|
||||
}
|
||||
|
||||
if (preGameContext.Pointer->Index1 == desiredIndex)
|
||||
{
|
||||
// We're on the desired character. Trigger play
|
||||
NativeMethods.SendMessageW((nint)hwnd.Value, NativeMethods.WM_KEYDOWN, 0x50, 0x00190001);
|
||||
NativeMethods.SendMessageW((nint)hwnd.Value, NativeMethods.WM_CHAR, 0x70, 0x00190001);
|
||||
NativeMethods.SendMessageW((nint)hwnd.Value, NativeMethods.WM_KEYUP, 0x50, 0x00190001);
|
||||
return true;
|
||||
}
|
||||
|
||||
currentIndex = preGameContext.Pointer->Index1;
|
||||
NativeMethods.SendMessageW((nint)hwnd.Value, NativeMethods.WM_KEYDOWN, NativeMethods.VK_RIGHT, 0x014D0001);
|
||||
NativeMethods.SendMessageW((nint)hwnd.Value, NativeMethods.WM_KEYUP, NativeMethods.VK_RIGHT, 0x014D0001);
|
||||
return false;
|
||||
break;
|
||||
}
|
||||
}, cancellationToken);
|
||||
|
||||
if (result)
|
||||
await Task.Delay(100, cancellationToken);
|
||||
}
|
||||
catch(Exception e)
|
||||
{
|
||||
return true;
|
||||
scopedLogger.LogError(e, "Error while waiting for character select to be ready");
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
if (cancellationToken.IsCancellationRequested)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var previousOrderPreference = await this.gameThreadService.QueueOnGameThread(() => this.preferencesService.GetEnumPreference(EnumPreference.CharSortOrder), cancellationToken);
|
||||
if (previousOrderPreference is null)
|
||||
{
|
||||
scopedLogger.LogError("Failed to get previous character sort order preference");
|
||||
return false;
|
||||
}
|
||||
|
||||
scopedLogger.LogInformation("Previous character sort order preference: {order}. Setting order to alphabetize", (CharSortOrder)previousOrderPreference);
|
||||
await this.gameThreadService.QueueOnGameThread(() => this.preferencesService.SetEnumPreference(EnumPreference.CharSortOrder, (uint)CharSortOrder.Alphabetize), cancellationToken);
|
||||
try
|
||||
{
|
||||
return await this.SelectCharacterToPlay(characterName, play: true, cancellationToken);
|
||||
}
|
||||
finally
|
||||
{
|
||||
// TODO: Delay to ensure character select has fully processed the selection before reverting sort order. Should be done after loading into the map.
|
||||
await Task.Delay(1000, cancellationToken);
|
||||
await this.gameThreadService.QueueOnGameThread(() => this.preferencesService.SetEnumPreference(EnumPreference.CharSortOrder, previousOrderPreference.Value), cancellationToken);
|
||||
}
|
||||
}
|
||||
|
||||
private async Task<string?> GetCharNameByUuid(string uuid, CancellationToken cancellationToken)
|
||||
@@ -247,67 +318,35 @@ public sealed class CharacterSelectService(
|
||||
{
|
||||
await this.gameThreadService.QueueOnGameThread(() =>
|
||||
{
|
||||
var logoutMessage = new LogOutMessage(0, 0);
|
||||
var logoutMessage = new LogOutMessage(0, 1); // Changed to 1 for character select
|
||||
unsafe
|
||||
{
|
||||
this.uiContextService.SendMessage(Models.UIMessage.Logout, (uint)&logoutMessage, 0);
|
||||
this.uiContextService.SendMessage(UIMessage.Logout, (uint)&logoutMessage, 0);
|
||||
}
|
||||
}, cancellationToken);
|
||||
}
|
||||
|
||||
private async Task<(uint DesiredIndex, uint CurrentIndex)?> WaitForLoginScreenAndGetCurrentAndDesiredIndex(string desiredCharName, CancellationToken cancellationToken)
|
||||
private async Task<bool> ValidateState(CancellationToken cancellationToken)
|
||||
{
|
||||
uint? desiredIndex = default;
|
||||
uint? currentIndex = default;
|
||||
while (!cancellationToken.IsCancellationRequested)
|
||||
return await this.gameThreadService.QueueOnGameThread(() =>
|
||||
{
|
||||
await Task.Delay(100, cancellationToken);
|
||||
var ready = await this.gameThreadService.QueueOnGameThread(() =>
|
||||
{
|
||||
unsafe
|
||||
{
|
||||
var preGameContext = this.gameContextService.GetPreGameContext();
|
||||
if (preGameContext.IsNull)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
return this.instanceContextService.GetInstanceType() is not API.Interop.GuildWars.InstanceType.Loading;
|
||||
}, cancellationToken);
|
||||
}
|
||||
|
||||
var uiState = 10U;
|
||||
this.uiContextService.SendMessage(Models.UIMessage.CheckUIState, 0, (uint)&uiState);
|
||||
var loginScreen = uiState == 2;
|
||||
if (!loginScreen)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
private unsafe bool IsCharSelectReady()
|
||||
{
|
||||
return this.GetCharSelectorFrame() is not null;
|
||||
}
|
||||
|
||||
for (var i = 0U; i < preGameContext.Pointer->LoginCharacters.Size; i++)
|
||||
{
|
||||
var loginCharacter = preGameContext.Pointer->LoginCharacters.Buffer[i];
|
||||
var charNameSpan = loginCharacter.CharacterName.AsSpan();
|
||||
var charName = new string(charNameSpan[..charNameSpan.IndexOf('\0')]);
|
||||
if (charName == desiredCharName)
|
||||
{
|
||||
desiredIndex = i;
|
||||
currentIndex = 0xffffffdd;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}, cancellationToken);
|
||||
|
||||
if (ready)
|
||||
{
|
||||
break;
|
||||
}
|
||||
private unsafe Frame* GetCharSelectorFrame()
|
||||
{
|
||||
var selectorFrame = this.uiContextService.GetFrameByLabel("Selector");
|
||||
if (selectorFrame.IsNull)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
if (desiredIndex is not null && currentIndex is not null)
|
||||
{
|
||||
return (desiredIndex.Value, currentIndex.Value);
|
||||
}
|
||||
|
||||
return default;
|
||||
return selectorFrame.Pointer;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,117 @@
|
||||
namespace Daybreak.API.Services;
|
||||
|
||||
public sealed class HashingService
|
||||
{
|
||||
private const uint DefaultHash = 0x325D1EAE;
|
||||
private const uint WStringHashSeed1 = 0xE2C15C9D;
|
||||
private const uint WStringHashSeed2 = 0x2170A28A;
|
||||
|
||||
private static readonly uint[] HashTable = [
|
||||
0x2EB3C21D, 0xA6F1D482, 0xBE24628F, 0x35699E30, 0xC87EE224, 0x04BA0097, 0x791F0EAF, 0xA162D466,
|
||||
0x685B376A, 0xF82133BA, 0x4682D46B, 0xCB25C0C5, 0xAC3F9D22, 0xABF17202, 0xEF987728, 0x3E400B83,
|
||||
0x63003108, 0xD83F1C93, 0x5058F507, 0x032E9991, 0x3518BC24, 0x1F90D44B, 0x92508DCE, 0x42DAA15C,
|
||||
0x5B4105A3, 0x4D5026AD, 0x07F838B0, 0xE8C4D2A2, 0xF9EF4F54, 0x1FC1A85F, 0xB9041A40, 0x446F6969,
|
||||
0xACF1EE39, 0x1828CC65, 0xC2F6C59D, 0x2456C3C0, 0x64F70142, 0x9C8ADB42, 0xC76781BF, 0x6AEA07E3,
|
||||
0xE98B14BF, 0x7DBCB35E, 0x6CE78C80, 0x1E3F93DE, 0x0954FEB7, 0xB3103AEF, 0x6DAAC1EE, 0x6535E301,
|
||||
0x4A221E68, 0x819CDD81, 0x5378CE3B, 0xE7348236, 0x2C3A7BCC, 0x31C50234, 0x745319DA, 0xF0897A0B,
|
||||
0x5C182968, 0x48E018F4, 0x593E3BB6, 0xAB588C8D, 0xF1101F1F, 0x1D36E8BC, 0xABC7C7C9, 0xF58AA6C8,
|
||||
0xF75B4B98, 0xC447B576, 0x6E5290F2, 0xD344EDF8, 0x5E1BCAF1, 0xEE892405, 0x798F7609, 0x8905CC29,
|
||||
0x8977DEDC, 0x007850BC, 0x0AA3114A, 0x2F61C8F2, 0x376DFAA4, 0x0BABFC12, 0x98E64E82, 0xE6743711,
|
||||
0xE26B26B6, 0xCBB10C72, 0x87446F8F, 0x96E26C0D, 0xCF140831, 0x81F69B0B, 0x5C9DF30B, 0xB276F929,
|
||||
0x2712C5B6, 0x80EA90D6, 0x8EB30322, 0xCF7A705A, 0x57A17E8C, 0x556A2425, 0x15104549, 0x5F3D549F,
|
||||
0x5D98FDEE, 0x9D8FA73E, 0x9206C9EF, 0xEDA0541D, 0xA3164F2D, 0x04C83543, 0x9BB27230, 0xEE97F0BA,
|
||||
0xB6AEB38C, 0x41FF4DA7, 0x4023EE54, 0xECBB78BC, 0xC2C54733, 0x8850B63F, 0x93BF56EF, 0xC80177D7,
|
||||
0xE68B0C13, 0x3C1E9D4C, 0x5CB3EACC, 0xF16F5795, 0xAC2B8A5C, 0x952E466E, 0x3095B0D5, 0xEC9D3A43,
|
||||
0x23D8FCF7, 0x10C7A93F, 0x1B479FB5, 0x3664CF8F, 0xA514A56B, 0xE16C4886, 0x63828188, 0xA47F14C1,
|
||||
0x391C237C, 0xFB15911F, 0x29825A01, 0xFC66DA40, 0xB2719F4E, 0x7B0D1611, 0x8F1EE7DE, 0xA6A1E77D,
|
||||
0xDFC25D30, 0x76377036, 0x8145DB82, 0x2300A4E9, 0xA2417F88, 0x1B8C6AFC, 0x68EBDC8E, 0x48FC0C41,
|
||||
0x040C3DCD, 0x17F02C44, 0xCC40B026, 0x5089EE1E, 0x2A594C28, 0x0CC198EC, 0x3F676F3D, 0xCD0AA239,
|
||||
0x274A31A4, 0xC6B4D4FA, 0x9480A62F, 0xAF8862EF, 0x627CA44D, 0x572E89D3, 0x4C05136C, 0x60F36E8C,
|
||||
0x7DE31FEF, 0x1B97FED3, 0xED99E2BF, 0xA034D418, 0xBBDB3E04, 0x1F198B71, 0xED45E889, 0xF7F1B0DC,
|
||||
0x1592B9A5, 0x96D60F96, 0x03D616EE, 0xA5EF679D, 0x831D791D, 0x579D5FD1, 0x4B44DC48, 0x38F7A2F0,
|
||||
0x2BF34A28, 0xAF14318C, 0xBD66D20A, 0x9AC72D2A, 0xC41ADB95, 0x860B2205, 0xE0E580E7, 0x3A06669C,
|
||||
0x2D1A40A2, 0x577EDF60, 0x1A37693A, 0x3E576C35, 0xE3857565, 0xE46E75EE, 0x94E3625D, 0x7D1B9280,
|
||||
0x2A0CCCE1, 0xBAB36163, 0x66E4E087, 0x940129BB, 0x59C197CD, 0xB0634139, 0xBB2964E1, 0x17207138,
|
||||
0xD8BD3728, 0xB324876C, 0x7C72FD04, 0xBB5D9D82, 0xD9B40236, 0xC0045BF6, 0xF85100ED, 0x36493582,
|
||||
0x8D0DF92A, 0xC3328368, 0xC202E412, 0x7ADE0064, 0xD01529C6, 0x4445B2AD, 0x0F6FC63B, 0xEF976FC5,
|
||||
0x709EC6BE, 0x69D31EA1, 0x794BFEB4, 0xB522DB95, 0x700DE6DF, 0x49098B71, 0x38B7FA82, 0x11EDDC04,
|
||||
0x5BB21D9F, 0xB14BC6D4, 0x3009D754, 0x451F5F3D, 0xFAA58575, 0xFB190B0D, 0x999DF79F, 0x6798A7AA,
|
||||
0x0BC434FF, 0x429E6C15, 0xA1EA2298, 0x55A57B81, 0xD7D74B8D, 0x8FECB38B, 0x658AD588, 0xF0190227,
|
||||
0xBE5D2B36, 0x62C59613, 0xC9FEDD64, 0xADD07A1E, 0x757A7679, 0xD78B6DCC, 0x56F88B2C, 0x4CC020BB,
|
||||
0xA88B51FE, 0xF47F7691, 0x48587EDD, 0x3FCEC5DB, 0xA7E127AC, 0x561F6161, 0x37AB9607, 0xFE23282B,
|
||||
];
|
||||
|
||||
private static readonly uint[] StringHashTable = [
|
||||
0x92B9A528, 0x25D4FC88, 0xEDCBEFB8, 0x51063A80,
|
||||
0x91341C61, 0x0261229D, 0x726F48ED, 0xCE1C088C,
|
||||
0x76253EB5, 0x31E3A0DE, 0xA2AAD215, 0xCA7D6D27,
|
||||
0xA5F98970, 0x0541C365, 0x3C14FF04, 0x5056AF4F
|
||||
];
|
||||
|
||||
private static uint HashHelper(uint result, byte b)
|
||||
{
|
||||
return result + (HashTable[result >> 24] ^ (result >> 6) ^ HashTable[b]);
|
||||
}
|
||||
|
||||
public uint Hash(byte[] data)
|
||||
{
|
||||
var hash = DefaultHash;
|
||||
foreach(var b in data)
|
||||
{
|
||||
hash = HashHelper(hash, b);
|
||||
}
|
||||
|
||||
return hash;
|
||||
}
|
||||
|
||||
public uint Hash(byte data)
|
||||
{
|
||||
return HashHelper(DefaultHash, data);
|
||||
}
|
||||
|
||||
public uint Hash(short data)
|
||||
{
|
||||
uint hash = DefaultHash;
|
||||
hash = HashHelper(hash, (byte)(data >> 0));
|
||||
hash = HashHelper(hash, (byte)(data >> 8));
|
||||
return hash;
|
||||
}
|
||||
|
||||
public uint Hash(uint data)
|
||||
{
|
||||
uint hash = DefaultHash;
|
||||
hash = HashHelper(hash, (byte)(data >> 0));
|
||||
hash = HashHelper(hash, (byte)(data >> 8));
|
||||
hash = HashHelper(hash, (byte)(data >> 16));
|
||||
hash = HashHelper(hash, (byte)(data >> 24));
|
||||
return hash;
|
||||
}
|
||||
|
||||
public uint Hash(string data, int maxLength = -1)
|
||||
{
|
||||
if (maxLength is -1)
|
||||
{
|
||||
maxLength = data.Length;
|
||||
}
|
||||
|
||||
uint seed1 = WStringHashSeed1;
|
||||
uint seed2 = WStringHashSeed2;
|
||||
uint hash = DefaultHash;
|
||||
|
||||
for(var i = 0; i < maxLength && i < data.Length; i++)
|
||||
{
|
||||
var c = data[i];
|
||||
var uc = (uint)c;
|
||||
if (c >= 'a' && c <= 'z')
|
||||
{
|
||||
uc -= 0x20;
|
||||
}
|
||||
|
||||
seed1 = (seed1 << 3) ^ uc;
|
||||
seed2 += StringHashTable[seed1 & 0xf];
|
||||
hash ^= seed2 + seed1;
|
||||
}
|
||||
|
||||
return hash;
|
||||
}
|
||||
}
|
||||
@@ -12,9 +12,9 @@ public unsafe sealed class AgentContextService : IAddressHealthService
|
||||
private const int AgentArrayOffset = -0x4;
|
||||
private static readonly byte[] AgentArrayAddressPattern = [0x8b, 0x0c, 0x90, 0x85, 0xc9, 0x74, 0x19];
|
||||
|
||||
private const string PlayerAgentIdMask = "xx????xxxx";
|
||||
private const string PlayerAgentIdMask = "xx????xxxxxxx";
|
||||
private const int PlayerAgentIdOffset = -0xE;
|
||||
private static readonly byte[] PlayerAgentIdAddressPattern = [0x5d, 0xe9, 0x00, 0x00, 0x00, 0x00, 0x55, 0x8b, 0xec, 0x53];
|
||||
private static readonly byte[] PlayerAgentIdAddressPattern = [0x5d, 0xe9, 0x00, 0x00, 0x00, 0x00, 0x55, 0x8b, 0xec, 0x56, 0x57, 0x8b, 0x7d];
|
||||
|
||||
private readonly MemoryScanningService memoryScanningService;
|
||||
private readonly GWAddressCache agentArrayAddress;
|
||||
|
||||
@@ -3,14 +3,23 @@ using Daybreak.API.Interop.GuildWars;
|
||||
using Daybreak.API.Models;
|
||||
using System.Core.Extensions;
|
||||
using System.Extensions.Core;
|
||||
using System.Runtime.InteropServices;
|
||||
|
||||
namespace Daybreak.API.Services.Interop;
|
||||
|
||||
public unsafe sealed class GameContextService : IAddressHealthService
|
||||
{
|
||||
private const string BaseAddressMask = "xxxxxxx";
|
||||
private const int BaseAddressOffset = +7;
|
||||
private static readonly byte[] BaseAddressPattern = [0x50, 0x6A, 0x0F, 0x6A, 0x00, 0xFF, 0x35];
|
||||
// TLS-based pattern to find GameContext getter function
|
||||
private const string GameContextTlsMask = "xx????xxxxxxxxxxx????x";
|
||||
private const int GameContextTlsOffset = 0;
|
||||
private static readonly byte[] GameContextTlsPattern =
|
||||
[
|
||||
0x8B, 0x0D, 0x00, 0x00, 0x00, 0x00, // mov ecx, [TlsIndexPtr] - 6 bytes: xx????
|
||||
0x64, 0xA1, 0x2C, 0x00, 0x00, 0x00, // mov eax, fs:[0000002C] - 6 bytes: xxxxxx
|
||||
0x8B, 0x04, 0x88, // mov eax, [eax+ecx*4] - 3 bytes: xxx
|
||||
0x8B, 0x80, 0x04, 0x00, 0x00, 0x00, // mov eax, [eax+00000004] - 6 bytes: xx????
|
||||
0xC3 // ret - 1 byte: x
|
||||
]; // Total: 22 bytes, mask: 22 chars
|
||||
|
||||
private const string PreGameContextFile = "UiPregame.cpp";
|
||||
private const string PreGameContextAssertion = "!s_scene";
|
||||
@@ -21,7 +30,7 @@ public unsafe sealed class GameContextService : IAddressHealthService
|
||||
private static readonly byte[] AvailableCharsPattern = [0x8B, 0x35, 0x00, 0x00, 0x00, 0x00, 0x57, 0x69, 0xF8, 0x84, 0x00, 0x00, 0x00];
|
||||
|
||||
private readonly MemoryScanningService memoryScanningService;
|
||||
private readonly GWAddressCache baseAddress;
|
||||
private readonly GWAddressCache gameContextGetterAddress;
|
||||
private readonly GWAddressCache preGameContextAddress;
|
||||
private readonly GWAddressCache availableCharsAddress;
|
||||
private readonly ILogger<GameContextService> logger;
|
||||
@@ -32,18 +41,21 @@ public unsafe sealed class GameContextService : IAddressHealthService
|
||||
{
|
||||
this.logger = logger.ThrowIfNull();
|
||||
this.memoryScanningService = memoryScanningService.ThrowIfNull();
|
||||
this.baseAddress = new GWAddressCache(this.GetBaseAddress);
|
||||
this.gameContextGetterAddress = new GWAddressCache(this.GetGameContextGetterAddress);
|
||||
this.preGameContextAddress = new GWAddressCache(this.GetPreGameContextAddress);
|
||||
this.availableCharsAddress = new GWAddressCache(this.GetAvailableCharactersAddress);
|
||||
}
|
||||
|
||||
[UnmanagedFunctionPointer(CallingConvention.Cdecl)]
|
||||
private delegate nuint GetGameContextDelegate();
|
||||
|
||||
public List<AddressState> GetAddressStates()
|
||||
{
|
||||
return [
|
||||
new AddressState
|
||||
{
|
||||
Address = this.baseAddress.GetAddress() ?? 0,
|
||||
Name = nameof(this.baseAddress),
|
||||
Address = this.gameContextGetterAddress.GetAddress() ?? 0,
|
||||
Name = nameof(this.gameContextGetterAddress),
|
||||
},
|
||||
new AddressState
|
||||
{
|
||||
@@ -113,41 +125,46 @@ public unsafe sealed class GameContextService : IAddressHealthService
|
||||
private nuint GetGameContextAddress()
|
||||
{
|
||||
var scopedLogger = this.logger.CreateScopedLogger();
|
||||
if (this.baseAddress.GetAddress() is not nuint basePtr)
|
||||
|
||||
// Get the address of the GameContext getter function
|
||||
var getterAddress = this.gameContextGetterAddress.GetAddress();
|
||||
if (getterAddress is null or 0)
|
||||
{
|
||||
scopedLogger.LogError("Failed to get base address");
|
||||
return default;
|
||||
scopedLogger.LogError("Failed to get GameContext getter function address");
|
||||
return 0;
|
||||
}
|
||||
|
||||
nuint** baseContext = *(nuint***)basePtr;
|
||||
if (baseContext is null)
|
||||
// Call the function to get GameContext
|
||||
var getter = Marshal.GetDelegateForFunctionPointer<GetGameContextDelegate>((nint)getterAddress);
|
||||
var gameContextPtr = getter();
|
||||
|
||||
if (gameContextPtr == 0)
|
||||
{
|
||||
scopedLogger.LogError("Failed to get base context address");
|
||||
return default;
|
||||
scopedLogger.LogError("GameContext getter returned null");
|
||||
return 0;
|
||||
}
|
||||
|
||||
var globalContextPtr = baseContext[0x6];
|
||||
if (globalContextPtr is null)
|
||||
{
|
||||
scopedLogger.LogError("Failed to get game context address");
|
||||
return default;
|
||||
}
|
||||
|
||||
return (nuint)globalContextPtr;
|
||||
scopedLogger.LogInformation("GameContext address: 0x{address:X8}", gameContextPtr);
|
||||
return gameContextPtr;
|
||||
}
|
||||
|
||||
private nuint GetBaseAddress()
|
||||
private nuint GetGameContextGetterAddress()
|
||||
{
|
||||
var scopedLogger = this.logger.CreateScopedLogger();
|
||||
var baseAddress = this.memoryScanningService.FindAndResolveAddress(BaseAddressPattern, BaseAddressMask, BaseAddressOffset);
|
||||
if (baseAddress is 0)
|
||||
|
||||
var address = this.memoryScanningService.FindAddress(
|
||||
GameContextTlsPattern,
|
||||
GameContextTlsMask,
|
||||
0);
|
||||
|
||||
if (address is 0)
|
||||
{
|
||||
scopedLogger.LogError("Failed to find base address");
|
||||
scopedLogger.LogError("Failed to find GameContext getter function");
|
||||
return 0U;
|
||||
}
|
||||
|
||||
scopedLogger.LogInformation("Base address: 0x{address:X8}", baseAddress);
|
||||
return baseAddress;
|
||||
scopedLogger.LogInformation("GameContext getter function: 0x{address:X8}", address);
|
||||
return address;
|
||||
}
|
||||
|
||||
private unsafe nuint GetPreGameContextAddress()
|
||||
@@ -160,9 +177,9 @@ public unsafe sealed class GameContextService : IAddressHealthService
|
||||
return 0U;
|
||||
}
|
||||
|
||||
preGameContextAddress = *(nuint*)preGameContextAddress;
|
||||
scopedLogger.LogInformation("Pre-game context address: 0x{address:X8}", preGameContextAddress);
|
||||
return preGameContextAddress;
|
||||
var actualAddress = *(nuint*)preGameContextAddress;
|
||||
scopedLogger.LogInformation("Pre-game context address: 0x{address:X8}", actualAddress);
|
||||
return actualAddress;
|
||||
}
|
||||
|
||||
private unsafe nuint GetAvailableCharactersAddress()
|
||||
|
||||
@@ -16,7 +16,7 @@ public sealed class GameThreadService
|
||||
|
||||
[SuppressUnmanagedCodeSecurity]
|
||||
[UnmanagedFunctionPointer(CallingConvention.Cdecl)]
|
||||
private delegate void LeaveGameThread(nint ctx);
|
||||
private delegate void LeaveGameThread(nint arg1, nint arg2);
|
||||
|
||||
private readonly MemoryScanningService memoryScanningService;
|
||||
private readonly GWHook<LeaveGameThread> leaveGameThreadHook;
|
||||
@@ -103,7 +103,7 @@ public sealed class GameThreadService
|
||||
scopedLogger.LogInformation("Hooks initialized");
|
||||
}
|
||||
|
||||
private void OnLeaveGameThread(nint ctx)
|
||||
private void OnLeaveGameThread(nint arg1, nint arg2)
|
||||
{
|
||||
var scopedLogger = this.logger.CreateScopedLogger();
|
||||
while(this.queuedItems.TryDequeue(out var item))
|
||||
@@ -137,6 +137,6 @@ public sealed class GameThreadService
|
||||
}
|
||||
}
|
||||
|
||||
this.leaveGameThreadHook.Continue(ctx);
|
||||
this.leaveGameThreadHook.Continue(arg1, arg2);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,441 @@
|
||||
using Daybreak.API.Interop;
|
||||
using Daybreak.API.Interop.GuildWars;
|
||||
using Daybreak.API.Models;
|
||||
using System.Core.Extensions;
|
||||
using System.Extensions.Core;
|
||||
using System.Runtime.InteropServices;
|
||||
|
||||
namespace Daybreak.API.Services.Interop;
|
||||
|
||||
public sealed class PreferencesService
|
||||
: IAddressHealthService
|
||||
{
|
||||
private readonly MemoryScanningService memoryScanningService;
|
||||
private readonly ILogger<PreferencesService> logger;
|
||||
|
||||
private readonly GWAddressCache preferencesInitializedAddress;
|
||||
private readonly GWAddressCache getEnumPreferenceAddress;
|
||||
private readonly GWAddressCache setEnumPreferenceAddress;
|
||||
private readonly GWAddressCache getNumberPreferenceAddress;
|
||||
private readonly GWAddressCache setNumberPreferenceAddress;
|
||||
private readonly GWAddressCache getFlagPreferenceAddress;
|
||||
private readonly GWAddressCache setFlagPreferenceAddress;
|
||||
private readonly GWAddressCache getStringPreferenceAddress;
|
||||
private readonly GWAddressCache setStringPreferenceAddress;
|
||||
|
||||
[UnmanagedFunctionPointer(CallingConvention.Cdecl)]
|
||||
private delegate uint GetEnumPreferenceDelegate(uint prefId);
|
||||
|
||||
[UnmanagedFunctionPointer(CallingConvention.Cdecl)]
|
||||
private delegate void SetEnumPreferenceDelegate(uint prefId, uint value);
|
||||
|
||||
[UnmanagedFunctionPointer(CallingConvention.Cdecl)]
|
||||
private delegate uint GetNumberPreferenceDelegate(uint prefId);
|
||||
|
||||
[UnmanagedFunctionPointer(CallingConvention.Cdecl)]
|
||||
private delegate void SetNumberPreferenceDelegate(uint prefId, uint value);
|
||||
|
||||
[UnmanagedFunctionPointer(CallingConvention.Cdecl)]
|
||||
[return: MarshalAs(UnmanagedType.I1)]
|
||||
private delegate bool GetFlagPreferenceDelegate(uint prefId);
|
||||
|
||||
[UnmanagedFunctionPointer(CallingConvention.Cdecl)]
|
||||
private delegate void SetFlagPreferenceDelegate(uint prefId, [MarshalAs(UnmanagedType.I1)] bool value);
|
||||
|
||||
[UnmanagedFunctionPointer(CallingConvention.Cdecl)]
|
||||
private delegate nint GetStringPreferenceDelegate(uint prefId);
|
||||
|
||||
[UnmanagedFunctionPointer(CallingConvention.Cdecl)]
|
||||
private delegate void SetStringPreferenceDelegate(uint prefId, nint value);
|
||||
|
||||
public PreferencesService(
|
||||
MemoryScanningService memoryScanningService,
|
||||
ILogger<PreferencesService> logger)
|
||||
{
|
||||
this.logger = logger.ThrowIfNull();
|
||||
this.memoryScanningService = memoryScanningService.ThrowIfNull();
|
||||
|
||||
this.preferencesInitializedAddress = new GWAddressCache(this.GetPreferencesInitializedAddress);
|
||||
this.getEnumPreferenceAddress = new GWAddressCache(this.GetGetEnumPreferenceAddress);
|
||||
this.setEnumPreferenceAddress = new GWAddressCache(this.GetSetEnumPreferenceAddress);
|
||||
this.getNumberPreferenceAddress = new GWAddressCache(this.GetGetNumberPreferenceAddress);
|
||||
this.setNumberPreferenceAddress = new GWAddressCache(this.GetSetNumberPreferenceAddress);
|
||||
this.getFlagPreferenceAddress = new GWAddressCache(this.GetGetFlagPreferenceAddress);
|
||||
this.setFlagPreferenceAddress = new GWAddressCache(this.GetSetFlagPreferenceAddress);
|
||||
this.getStringPreferenceAddress = new GWAddressCache(this.GetGetStringPreferenceAddress);
|
||||
this.setStringPreferenceAddress = new GWAddressCache(this.GetSetStringPreferenceAddress);
|
||||
}
|
||||
|
||||
public List<AddressState> GetAddressStates()
|
||||
{
|
||||
return
|
||||
[
|
||||
new AddressState { Address = this.preferencesInitializedAddress.GetAddress() ?? 0, Name = nameof(this.preferencesInitializedAddress) },
|
||||
new AddressState { Address = this.getEnumPreferenceAddress.GetAddress() ?? 0, Name = nameof(this.getEnumPreferenceAddress) },
|
||||
new AddressState { Address = this.setEnumPreferenceAddress.GetAddress() ?? 0, Name = nameof(this.setEnumPreferenceAddress) },
|
||||
new AddressState { Address = this.getNumberPreferenceAddress.GetAddress() ?? 0, Name = nameof(this.getNumberPreferenceAddress) },
|
||||
new AddressState { Address = this.setNumberPreferenceAddress.GetAddress() ?? 0, Name = nameof(this.setNumberPreferenceAddress) },
|
||||
new AddressState { Address = this.getFlagPreferenceAddress.GetAddress() ?? 0, Name = nameof(this.getFlagPreferenceAddress) },
|
||||
new AddressState { Address = this.setFlagPreferenceAddress.GetAddress() ?? 0, Name = nameof(this.setFlagPreferenceAddress) },
|
||||
new AddressState { Address = this.getStringPreferenceAddress.GetAddress() ?? 0, Name = nameof(this.getStringPreferenceAddress) },
|
||||
new AddressState { Address = this.setStringPreferenceAddress.GetAddress() ?? 0, Name = nameof(this.setStringPreferenceAddress) },
|
||||
];
|
||||
}
|
||||
|
||||
public unsafe bool PreferencesInitialized()
|
||||
{
|
||||
var addr = this.preferencesInitializedAddress.GetAddress();
|
||||
return addr is not null && addr != 0 && *(byte*)addr != 0;
|
||||
}
|
||||
|
||||
public uint? GetEnumPreference(EnumPreference pref)
|
||||
{
|
||||
if (!this.PreferencesInitialized())
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var funcAddr = this.getEnumPreferenceAddress.GetAddress();
|
||||
if (funcAddr is null or 0)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var func = Marshal.GetDelegateForFunctionPointer<GetEnumPreferenceDelegate>((nint)funcAddr);
|
||||
return func((uint)pref);
|
||||
}
|
||||
|
||||
public bool SetEnumPreference(EnumPreference pref, uint value)
|
||||
{
|
||||
if (!this.PreferencesInitialized())
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var funcAddr = this.setEnumPreferenceAddress.GetAddress();
|
||||
if (funcAddr is null or 0)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var func = Marshal.GetDelegateForFunctionPointer<SetEnumPreferenceDelegate>((nint)funcAddr);
|
||||
func((uint)pref, value);
|
||||
return true;
|
||||
}
|
||||
|
||||
public uint? GetNumberPreference(NumberPreference pref)
|
||||
{
|
||||
if (!this.PreferencesInitialized())
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var funcAddr = this.getNumberPreferenceAddress.GetAddress();
|
||||
if (funcAddr is null or 0)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var func = Marshal.GetDelegateForFunctionPointer<GetNumberPreferenceDelegate>((nint)funcAddr);
|
||||
return func((uint)pref);
|
||||
}
|
||||
|
||||
public bool SetNumberPreference(NumberPreference pref, uint value)
|
||||
{
|
||||
if (!this.PreferencesInitialized())
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var funcAddr = this.setNumberPreferenceAddress.GetAddress();
|
||||
if (funcAddr is null or 0)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var func = Marshal.GetDelegateForFunctionPointer<SetNumberPreferenceDelegate>((nint)funcAddr);
|
||||
func((uint)pref, value);
|
||||
return true;
|
||||
}
|
||||
|
||||
public bool? GetFlagPreference(FlagPreference pref)
|
||||
{
|
||||
if (!this.PreferencesInitialized())
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var funcAddr = this.getFlagPreferenceAddress.GetAddress();
|
||||
if (funcAddr is null or 0)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var func = Marshal.GetDelegateForFunctionPointer<GetFlagPreferenceDelegate>((nint)funcAddr);
|
||||
return func((uint)pref);
|
||||
}
|
||||
|
||||
public bool SetFlagPreference(FlagPreference pref, bool value)
|
||||
{
|
||||
if (!this.PreferencesInitialized())
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var funcAddr = this.setFlagPreferenceAddress.GetAddress();
|
||||
if (funcAddr is null or 0)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var func = Marshal.GetDelegateForFunctionPointer<SetFlagPreferenceDelegate>((nint)funcAddr);
|
||||
func((uint)pref, value);
|
||||
return true;
|
||||
}
|
||||
|
||||
public unsafe string? GetStringPreference(StringPreference pref)
|
||||
{
|
||||
if (!this.PreferencesInitialized())
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var funcAddr = this.getStringPreferenceAddress.GetAddress();
|
||||
if (funcAddr is null or 0)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var func = Marshal.GetDelegateForFunctionPointer<GetStringPreferenceDelegate>((nint)funcAddr);
|
||||
var ptr = func((uint)pref);
|
||||
if (ptr == 0)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
return Marshal.PtrToStringUni(ptr);
|
||||
}
|
||||
|
||||
public bool SetStringPreference(StringPreference pref, string value)
|
||||
{
|
||||
if (!this.PreferencesInitialized())
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var funcAddr = this.setStringPreferenceAddress.GetAddress();
|
||||
if (funcAddr is null or 0)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var strPtr = Marshal.StringToHGlobalUni(value);
|
||||
try
|
||||
{
|
||||
var func = Marshal.GetDelegateForFunctionPointer<SetStringPreferenceDelegate>((nint)funcAddr);
|
||||
func((uint)pref, strPtr);
|
||||
return true;
|
||||
}
|
||||
finally
|
||||
{
|
||||
Marshal.FreeHGlobal(strPtr);
|
||||
}
|
||||
}
|
||||
|
||||
private nuint GetPreferencesInitializedAddress()
|
||||
{
|
||||
var scopedLogger = this.logger.CreateScopedLogger();
|
||||
|
||||
// GWCA: FindAssertion("\\Code\\Gw\\Pref\\PrApi.cpp", "location < arrsize(s_flushDelay)", 0, -0x12)
|
||||
var address = this.memoryScanningService.FindAssertion(
|
||||
@"\Code\Gw\Pref\PrApi.cpp",
|
||||
"location < arrsize(s_flushDelay)",
|
||||
0,
|
||||
-0x12);
|
||||
|
||||
if (address is 0)
|
||||
{
|
||||
scopedLogger.LogError("Failed to find preferences initialized assertion");
|
||||
return 0;
|
||||
}
|
||||
|
||||
// Dereference to get actual address
|
||||
var ptr = (nuint)Marshal.ReadIntPtr((nint)address);
|
||||
if (ptr is 0)
|
||||
{
|
||||
scopedLogger.LogError("Failed to dereference preferences initialized address");
|
||||
return 0;
|
||||
}
|
||||
|
||||
scopedLogger.LogInformation("Preferences initialized address: 0x{address:X8}", ptr);
|
||||
return ptr;
|
||||
}
|
||||
|
||||
private nuint GetGetEnumPreferenceAddress()
|
||||
{
|
||||
var scopedLogger = this.logger.CreateScopedLogger();
|
||||
var stringUsage = this.memoryScanningService.FindUseOfString("pref < PREF_ENUMS");
|
||||
if (stringUsage is 0)
|
||||
{
|
||||
scopedLogger.LogError("Failed to find 'pref < PREF_ENUMS' string usage");
|
||||
return 0;
|
||||
}
|
||||
|
||||
var address = this.memoryScanningService.ToFunctionStart(stringUsage);
|
||||
if (address is 0)
|
||||
{
|
||||
scopedLogger.LogError("Failed to resolve GetEnumPreference function start");
|
||||
return 0;
|
||||
}
|
||||
|
||||
scopedLogger.LogInformation("GetEnumPreference address: 0x{address:X8}", address);
|
||||
return address;
|
||||
}
|
||||
|
||||
private nuint GetSetEnumPreferenceAddress()
|
||||
{
|
||||
var scopedLogger = this.logger.CreateScopedLogger();
|
||||
var stringUsage = this.memoryScanningService.FindUseOfString("tableCount");
|
||||
if (stringUsage is 0)
|
||||
{
|
||||
scopedLogger.LogError("Failed to find 'tableCount' string usage");
|
||||
return 0;
|
||||
}
|
||||
|
||||
var address = this.memoryScanningService.ToFunctionStart(stringUsage);
|
||||
if (address is 0)
|
||||
{
|
||||
scopedLogger.LogError("Failed to resolve SetEnumPreference function start");
|
||||
return 0;
|
||||
}
|
||||
|
||||
scopedLogger.LogInformation("SetEnumPreference address: 0x{address:X8}", address);
|
||||
return address;
|
||||
}
|
||||
|
||||
private nuint GetGetNumberPreferenceAddress()
|
||||
{
|
||||
var scopedLogger = this.logger.CreateScopedLogger();
|
||||
var stringUsage = this.memoryScanningService.FindUseOfString("pref < PREF_VALUES");
|
||||
if (stringUsage is 0)
|
||||
{
|
||||
scopedLogger.LogError("Failed to find 'pref < PREF_VALUES' string usage");
|
||||
return 0;
|
||||
}
|
||||
|
||||
var address = this.memoryScanningService.ToFunctionStart(stringUsage);
|
||||
if (address is 0)
|
||||
{
|
||||
scopedLogger.LogError("Failed to resolve GetNumberPreference function start");
|
||||
return 0;
|
||||
}
|
||||
|
||||
scopedLogger.LogInformation("GetNumberPreference address: 0x{address:X8}", address);
|
||||
return address;
|
||||
}
|
||||
|
||||
private nuint GetSetNumberPreferenceAddress()
|
||||
{
|
||||
var scopedLogger = this.logger.CreateScopedLogger();
|
||||
var stringUsage = this.memoryScanningService.FindNthUseOfString("pref < PREF_VALUES", 1);
|
||||
if (stringUsage is 0)
|
||||
{
|
||||
scopedLogger.LogError("Failed to find second 'pref < PREF_VALUES' string usage");
|
||||
return 0;
|
||||
}
|
||||
|
||||
var address = this.memoryScanningService.ToFunctionStart(stringUsage);
|
||||
if (address is 0)
|
||||
{
|
||||
scopedLogger.LogError("Failed to resolve SetNumberPreference function start");
|
||||
return 0;
|
||||
}
|
||||
|
||||
scopedLogger.LogInformation("SetNumberPreference address: 0x{address:X8}", address);
|
||||
return address;
|
||||
}
|
||||
|
||||
private nuint GetGetFlagPreferenceAddress()
|
||||
{
|
||||
var scopedLogger = this.logger.CreateScopedLogger();
|
||||
var stringUsage = this.memoryScanningService.FindUseOfString("pref < PREF_FLAGS");
|
||||
if (stringUsage is 0)
|
||||
{
|
||||
scopedLogger.LogError("Failed to find 'pref < PREF_FLAGS' string usage");
|
||||
return 0;
|
||||
}
|
||||
|
||||
var address = this.memoryScanningService.ToFunctionStart(stringUsage);
|
||||
if (address is 0)
|
||||
{
|
||||
scopedLogger.LogError("Failed to resolve GetFlagPreference function start");
|
||||
return 0;
|
||||
}
|
||||
|
||||
scopedLogger.LogInformation("GetFlagPreference address: 0x{address:X8}", address);
|
||||
return address;
|
||||
}
|
||||
|
||||
private nuint GetSetFlagPreferenceAddress()
|
||||
{
|
||||
var scopedLogger = this.logger.CreateScopedLogger();
|
||||
var stringUsage = this.memoryScanningService.FindNthUseOfString("pref < PREF_FLAGS", 1);
|
||||
if (stringUsage is 0)
|
||||
{
|
||||
scopedLogger.LogError("Failed to find second 'pref < PREF_FLAGS' string usage");
|
||||
return 0;
|
||||
}
|
||||
|
||||
var address = this.memoryScanningService.ToFunctionStart(stringUsage);
|
||||
if (address is 0)
|
||||
{
|
||||
scopedLogger.LogError("Failed to resolve SetFlagPreference function start");
|
||||
return 0;
|
||||
}
|
||||
|
||||
scopedLogger.LogInformation("SetFlagPreference address: 0x{address:X8}", address);
|
||||
return address;
|
||||
}
|
||||
|
||||
private nuint GetGetStringPreferenceAddress()
|
||||
{
|
||||
var scopedLogger = this.logger.CreateScopedLogger();
|
||||
var stringUsage = this.memoryScanningService.FindUseOfString("pref < PREF_STRINGS");
|
||||
if (stringUsage is 0)
|
||||
{
|
||||
scopedLogger.LogError("Failed to find 'pref < PREF_STRINGS' string usage");
|
||||
return 0;
|
||||
}
|
||||
|
||||
var address = this.memoryScanningService.ToFunctionStart(stringUsage);
|
||||
if (address is 0)
|
||||
{
|
||||
scopedLogger.LogError("Failed to resolve GetStringPreference function start");
|
||||
return 0;
|
||||
}
|
||||
|
||||
scopedLogger.LogInformation("GetStringPreference address: 0x{address:X8}", address);
|
||||
return address;
|
||||
}
|
||||
|
||||
private nuint GetSetStringPreferenceAddress()
|
||||
{
|
||||
var scopedLogger = this.logger.CreateScopedLogger();
|
||||
var stringUsage = this.memoryScanningService.FindNthUseOfString("pref < PREF_STRINGS", 1);
|
||||
if (stringUsage is 0)
|
||||
{
|
||||
scopedLogger.LogError("Failed to find second 'pref < PREF_STRINGS' string usage");
|
||||
return 0;
|
||||
}
|
||||
|
||||
var address = this.memoryScanningService.ToFunctionStart(stringUsage);
|
||||
if (address is 0)
|
||||
{
|
||||
scopedLogger.LogError("Failed to resolve SetStringPreference function start");
|
||||
return 0;
|
||||
}
|
||||
|
||||
scopedLogger.LogInformation("SetStringPreference address: 0x{address:X8}", address);
|
||||
return address;
|
||||
}
|
||||
}
|
||||
@@ -13,37 +13,35 @@ public sealed class UIContextService
|
||||
{
|
||||
private const string GetChildFrameIdFile = "CtlView.cpp";
|
||||
private const string GetChildFrameIdAssertion = "pageId";
|
||||
private const int GetChildFrameIdOffset = 0x19;
|
||||
private const string SetFrameFlagFile = "FrApi.cpp";
|
||||
private const string SetFrameFlagAssertion = "frameId";
|
||||
private const int SetFrameVisibleOffset = 0x4f9;
|
||||
private const int SetFrameDisabledOffset = 0x4e3;
|
||||
private static readonly byte[] SendFrameUiMessageByIdSeq = [0x83, 0xFB, 0x47, 0x73, 0x14];
|
||||
private const string SendFrameUiMessageByIdMask = "xxxxx";
|
||||
private const int SendFrameUiMessageByIdOffset = -0x34;
|
||||
private const int GetChildFrameIdOffset = 0x12;
|
||||
private static readonly byte[] GetChildFrameIdPattern = [0xE8];
|
||||
private const string GetChildFrameIdMask = "x";
|
||||
|
||||
private static readonly byte[] SendFrameUiMessageSeq = [
|
||||
0x83, 0xC1, 0xDC, // add ecx, -24
|
||||
0xE8, 0x00, 0x00, 0x00, 0x00, // call
|
||||
0x8B, 0x4D, 0xFC // mov ecx, [ebp-04]
|
||||
];
|
||||
private const string SendFrameUiMessageMask = "xxxx????xxx";
|
||||
private const int SendFrameUiMessageOffset = 0x3;
|
||||
|
||||
private static readonly byte[] SendUIMessageSeq = [0xB9, 0x00, 0x00, 0x00, 0x00, 0xE8, 0x00, 0x00, 0x00, 0x00, 0x5D, 0xC3, 0x89, 0x45, 0x08];
|
||||
private const string SendUIMessageMask = "x????x????xxxxx";
|
||||
private const string CreateHashFromStringMask = "xxxxxxx";
|
||||
private const int CreateHashFromStringOffset = 0x7;
|
||||
private static readonly byte[] CreateHashFromStringSeq = [0x85, 0xC0, 0x74, 0x0D, 0x6A, 0xFF, 0x50];
|
||||
|
||||
private const string FrameArrayFile = "\\Code\\Engine\\Frame\\FrMsg.cpp";
|
||||
private const string FrameArrayAssertion = "frame";
|
||||
private static readonly byte[] GetRootFrameSeq = [0x05, 0xE0, 0xFE, 0xFF, 0xFF, 0xC3];
|
||||
private const string GetRootFrameMask = "xxxxxx";
|
||||
private const int GetRootFrameOffset = -0x3C;
|
||||
|
||||
[SuppressUnmanagedCodeSecurity]
|
||||
[UnmanagedFunctionPointer(CallingConvention.Cdecl)]
|
||||
private unsafe delegate uint CreateHashFromStringFunc(char* value, int seed);
|
||||
private static readonly byte[] GetRootFrameSeq = [
|
||||
0xA1, 0x00, 0x00, 0x00, 0x00, // mov eax,[address]
|
||||
0x05, 0xD8, 0xFE, 0xFF, 0xFF, // add eax,FFFFFED8
|
||||
0xC3 // ret
|
||||
];
|
||||
private const string GetRootFrameMask = "x????xxxxxx";
|
||||
|
||||
[SuppressUnmanagedCodeSecurity]
|
||||
[UnmanagedFunctionPointer(CallingConvention.Cdecl)]
|
||||
private delegate void SendUIMessageFunc(UIMessage uiMessage, nuint wParam, nuint lParam);
|
||||
|
||||
[SuppressUnmanagedCodeSecurity]
|
||||
[UnmanagedFunctionPointer(CallingConvention.Cdecl)]
|
||||
private delegate void SetFrameFlagFunc(uint frameId, uint flag);
|
||||
|
||||
[SuppressUnmanagedCodeSecurity]
|
||||
[UnmanagedFunctionPointer(CallingConvention.Cdecl)]
|
||||
private delegate uint GetChildFrameIdFunc(uint parentFrameId, uint childOffset);
|
||||
@@ -52,27 +50,35 @@ public sealed class UIContextService
|
||||
[UnmanagedFunctionPointer(CallingConvention.Cdecl)]
|
||||
private unsafe delegate Frame* GetRootFrameFunc();
|
||||
|
||||
private readonly SemaphoreSlim semaphoreSlim = new(1);
|
||||
[SuppressUnmanagedCodeSecurity]
|
||||
[UnmanagedFunctionPointer(CallingConvention.Cdecl)]
|
||||
private unsafe delegate void ValidateAsyncDecodeStr(char* s, DecodeStrCallback callback, void* wParam);
|
||||
|
||||
public unsafe delegate void DecodeStrCallback(void* param, char* s);
|
||||
|
||||
private readonly SemaphoreSlim sendUiMessageSemaphore = new(1);
|
||||
private readonly SemaphoreSlim decodeStringSemaphore = new(1);
|
||||
private readonly MemoryScanningService memoryScanningService;
|
||||
private readonly GWDelegateCache<CreateHashFromStringFunc> createHashFromString;
|
||||
private readonly HashingService hashingService;
|
||||
private readonly GWDelegateCache<GetChildFrameIdFunc> getChildFrameId;
|
||||
private readonly GWAddressCache frameArrayAddressCache;
|
||||
private readonly GWHook<SendUIMessageFunc> sendUiMessageHook;
|
||||
private readonly GWDelegateCache<GetRootFrameFunc> getRootFrame;
|
||||
private readonly GWDelegateCache<ValidateAsyncDecodeStr> validateAsyncDecodeStr;
|
||||
// This fastcall equivalent is <GuildWarsArray<FrameInteractionCallback>*, void*, UIMessage, void*, void*>
|
||||
private readonly GWFastCall<nint, nint, UIMessage, nint, nint, GWFastCall.Void> sendFrameUIMessage;
|
||||
private readonly ILogger<UIContextService> logger;
|
||||
|
||||
private CancellationTokenSource? cts = default;
|
||||
private CancellationTokenSource? cts;
|
||||
|
||||
public UIContextService(
|
||||
HashingService hashingService,
|
||||
MemoryScanningService memoryScanningService,
|
||||
ILogger<UIContextService> logger)
|
||||
{
|
||||
this.logger = logger.ThrowIfNull();
|
||||
this.memoryScanningService = memoryScanningService.ThrowIfNull();
|
||||
this.createHashFromString = new GWDelegateCache<CreateHashFromStringFunc>(new GWAddressCache(() => this.memoryScanningService.FunctionFromNearCall(
|
||||
this.memoryScanningService.FindAddress(CreateHashFromStringSeq, CreateHashFromStringMask, CreateHashFromStringOffset))));
|
||||
this.hashingService = hashingService.ThrowIfNull();
|
||||
this.frameArrayAddressCache = new GWAddressCache(() => this.memoryScanningService.FindAssertion(FrameArrayFile, FrameArrayAssertion, 0, -0x14));
|
||||
this.sendUiMessageHook = new GWHook<SendUIMessageFunc>(
|
||||
new GWAddressCache(() => this.memoryScanningService.ToFunctionStart(
|
||||
@@ -80,42 +86,50 @@ public sealed class UIContextService
|
||||
this.OnSendUIMessage);
|
||||
this.sendFrameUIMessage = new(
|
||||
new GWAddressCache(() => this.memoryScanningService.FunctionFromNearCall(
|
||||
this.memoryScanningService.FindAddress(SendFrameUiMessageByIdSeq, SendFrameUiMessageByIdMask, SendFrameUiMessageByIdOffset) + 0x67)));
|
||||
this.memoryScanningService.FindAddress(SendFrameUiMessageSeq, SendFrameUiMessageMask, SendFrameUiMessageOffset))));
|
||||
this.getChildFrameId = new GWDelegateCache<GetChildFrameIdFunc>(
|
||||
new GWAddressCache(() => this.memoryScanningService.FunctionFromNearCall(
|
||||
this.memoryScanningService.FindAssertion(GetChildFrameIdFile, GetChildFrameIdAssertion, 0, GetChildFrameIdOffset))));
|
||||
new GWAddressCache(() =>
|
||||
{
|
||||
var address = this.memoryScanningService.FindAssertion(GetChildFrameIdFile, GetChildFrameIdAssertion, 0, GetChildFrameIdOffset);
|
||||
return this.memoryScanningService.FunctionFromNearCall(
|
||||
this.memoryScanningService.FindAddress(GetChildFrameIdPattern, GetChildFrameIdMask, 0, address, address + 0xFF));
|
||||
}));
|
||||
this.getRootFrame = new GWDelegateCache<GetRootFrameFunc>(
|
||||
new GWAddressCache(() => this.memoryScanningService.FindAddress(GetRootFrameSeq, GetRootFrameMask, GetRootFrameOffset)));
|
||||
new GWAddressCache(() => this.memoryScanningService.FindAddress(GetRootFrameSeq, GetRootFrameMask, 0)));
|
||||
|
||||
this.validateAsyncDecodeStr = new GWDelegateCache<ValidateAsyncDecodeStr>(
|
||||
new GWAddressCache(() => this.memoryScanningService.ToFunctionStart(
|
||||
this.memoryScanningService.FindUseOfString("(codedString[0] & ~WORD_BIT_MORE) >= WORD_VALUE_BASE"))));
|
||||
}
|
||||
|
||||
public List<AddressState> GetAddressStates()
|
||||
{
|
||||
return
|
||||
[
|
||||
new()
|
||||
{
|
||||
Name = nameof(this.createHashFromString),
|
||||
Address = this.createHashFromString.Cache.GetAddress() ?? 0U
|
||||
},
|
||||
new()
|
||||
{
|
||||
Name = nameof(this.frameArrayAddressCache),
|
||||
Address = this.frameArrayAddressCache.GetAddress() ?? 0U
|
||||
},
|
||||
new()
|
||||
{
|
||||
Name = nameof(this.getChildFrameId),
|
||||
Address = this.getChildFrameId.Cache.GetAddress() ?? 0U
|
||||
},
|
||||
new()
|
||||
{
|
||||
Name = nameof(this.sendFrameUIMessage),
|
||||
Address = this.sendFrameUIMessage.Cache.GetAddress() ?? 0U
|
||||
},
|
||||
new()
|
||||
{
|
||||
Name = nameof(this.getChildFrameId),
|
||||
Address = this.getChildFrameId.Cache.GetAddress() ?? 0U
|
||||
},
|
||||
new()
|
||||
{
|
||||
Name = nameof(this.getRootFrame),
|
||||
Address = this.getRootFrame.Cache.GetAddress() ?? 0U
|
||||
},
|
||||
new()
|
||||
{
|
||||
Name = nameof(this.validateAsyncDecodeStr),
|
||||
Address = this.validateAsyncDecodeStr.Cache.GetAddress() ?? 0U
|
||||
}
|
||||
];
|
||||
}
|
||||
@@ -150,6 +164,26 @@ public sealed class UIContextService
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
public unsafe WrappedPointer<T> GetFrameContext<T>(WrappedPointer<Frame> frame)
|
||||
where T : unmanaged
|
||||
{
|
||||
if (frame.IsNull || frame.Pointer->FrameCallbacks.Size == 0)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
for (uint i = 0; i < frame.Pointer->FrameCallbacks.Size; i++)
|
||||
{
|
||||
var callback = frame.Pointer->FrameCallbacks.Buffer[i];
|
||||
if (callback.UiCtl_Context != null)
|
||||
{
|
||||
return new WrappedPointer<T>((T*)callback.UiCtl_Context);
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public unsafe WrappedPointer<Frame> GetChildFrame(WrappedPointer<Frame> parent, uint childOffset)
|
||||
{
|
||||
var scopedLogger = this.logger.CreateScopedLogger();
|
||||
@@ -194,7 +228,7 @@ public sealed class UIContextService
|
||||
return true;
|
||||
}
|
||||
|
||||
public unsafe bool SetFrameDisabled(WrappedPointer<Frame> frame, bool disabled)
|
||||
public bool SetFrameDisabled(WrappedPointer<Frame> frame, bool disabled)
|
||||
{
|
||||
var scopedLogger = this.logger.CreateScopedLogger();
|
||||
if (frame.IsNull)
|
||||
@@ -207,7 +241,7 @@ public sealed class UIContextService
|
||||
return true;
|
||||
}
|
||||
|
||||
public unsafe bool SetFrameVisible(WrappedPointer<Frame> frame, bool visible)
|
||||
public bool SetFrameVisible(WrappedPointer<Frame> frame, bool visible)
|
||||
{
|
||||
var scopedLogger = this.logger.CreateScopedLogger();
|
||||
if (frame.IsNull)
|
||||
@@ -227,10 +261,10 @@ public sealed class UIContextService
|
||||
frame = this.GetButtonActionFrame();
|
||||
}
|
||||
|
||||
this.semaphoreSlim.Wait();
|
||||
this.sendUiMessageSemaphore.Wait();
|
||||
var packet = new UIPackets.KeyAction((uint)action);
|
||||
this.SendFrameUIMessage(frame, UIMessage.KeyDown, &packet);
|
||||
this.semaphoreSlim.Release();
|
||||
this.sendUiMessageSemaphore.Release();
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -241,10 +275,10 @@ public sealed class UIContextService
|
||||
frame = this.GetButtonActionFrame();
|
||||
}
|
||||
|
||||
this.semaphoreSlim.Wait();
|
||||
this.sendUiMessageSemaphore.Wait();
|
||||
var packet = new UIPackets.KeyAction((uint)action);
|
||||
this.SendFrameUIMessage(frame, UIMessage.KeyUp, &packet);
|
||||
this.semaphoreSlim.Release();
|
||||
this.sendUiMessageSemaphore.Release();
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -268,14 +302,14 @@ public sealed class UIContextService
|
||||
return null;
|
||||
}
|
||||
|
||||
var hash = this.CreateHashFromString(label, -1);
|
||||
var hash = this.CreateHashFromString(label);
|
||||
if (hash is 0)
|
||||
{
|
||||
scopedLogger.LogError("Failed to get hash for label {label}", label);
|
||||
return null;
|
||||
}
|
||||
|
||||
for(var i = 0; i < frameArray.Pointer->Size; i++)
|
||||
for (var i = 0; i < frameArray.Pointer->Size; i++)
|
||||
{
|
||||
var frame = frameArray.Pointer->Buffer[i];
|
||||
if (frame.Pointer is null ||
|
||||
@@ -315,26 +349,47 @@ public sealed class UIContextService
|
||||
return frame;
|
||||
}
|
||||
|
||||
public unsafe uint CreateHashFromString(string value, int seed)
|
||||
public uint CreateHashFromString(string value)
|
||||
{
|
||||
var scopedLogger = this.logger.CreateScopedLogger();
|
||||
if (this.createHashFromString.GetDelegate() is not CreateHashFromStringFunc del)
|
||||
{
|
||||
scopedLogger.LogError("Failed to get CreateHashFromString delegate");
|
||||
return 0;
|
||||
}
|
||||
|
||||
fixed (char* valuePtr = value)
|
||||
{
|
||||
return del(valuePtr, seed);
|
||||
}
|
||||
return this.hashingService.Hash(value);
|
||||
}
|
||||
|
||||
public unsafe void SendMessage(UIMessage message, nuint wParam, nuint lParam)
|
||||
public void SendMessage(UIMessage message, nuint wParam, nuint lParam)
|
||||
{
|
||||
this.semaphoreSlim.Wait();
|
||||
this.sendUiMessageSemaphore.Wait();
|
||||
this.sendUiMessageHook.Continue(message, wParam, lParam);
|
||||
this.semaphoreSlim.Release();
|
||||
this.sendUiMessageSemaphore.Release();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Decodes a string asynchronously using the game's internal decoding mechanism.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Inside the DecodeStrCallback, the decoded string has to be cleaned up by calling Marshal.FreeHGlobal on the char* pointer.
|
||||
/// </remarks>
|
||||
public unsafe void ValidateDecodeString(char* encoded, DecodeStrCallback callback, nuint callbackParam)
|
||||
{
|
||||
var scopedLogger = this.logger.CreateScopedLogger();
|
||||
this.decodeStringSemaphore.Wait();
|
||||
try
|
||||
{
|
||||
var validateDecodeStrFnc = this.validateAsyncDecodeStr.GetDelegate();
|
||||
if (validateDecodeStrFnc is null)
|
||||
{
|
||||
scopedLogger.LogError("{funcName} is null", nameof(this.validateAsyncDecodeStr));
|
||||
return;
|
||||
}
|
||||
|
||||
validateDecodeStrFnc(encoded, callback, (void*)callbackParam);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
scopedLogger.LogError(e, "Encountered exception while decoding string");
|
||||
}
|
||||
finally
|
||||
{
|
||||
this.decodeStringSemaphore.Release();
|
||||
}
|
||||
}
|
||||
|
||||
private async ValueTask InitializeHooks(CancellationToken cancellationToken)
|
||||
@@ -357,24 +412,24 @@ public sealed class UIContextService
|
||||
|
||||
private static unsafe void SetFrameVisibleInternal(WrappedPointer<Frame> frame, bool visible)
|
||||
{
|
||||
uint* pState = &frame.Pointer->Field91; // direct field address
|
||||
uint* pState = &frame.Pointer->FrameState; // direct field address
|
||||
if (visible)
|
||||
{
|
||||
*pState &= ~0x200u; // clear “hidden”
|
||||
*pState &= ~0x200u; // clear "hidden"
|
||||
}
|
||||
else
|
||||
{
|
||||
*pState |= 0x200u; // set “hidden”
|
||||
*pState |= 0x200u; // set "hidden"
|
||||
}
|
||||
}
|
||||
|
||||
private static unsafe void SetFrameDisabledInternal(WrappedPointer<Frame> frame, bool disabled)
|
||||
{
|
||||
uint* pState = &frame.Pointer->Field91; // same word (offset 0x184)
|
||||
uint* pState = &frame.Pointer->FrameState; // same word (offset 0x184)
|
||||
|
||||
if (disabled)
|
||||
{
|
||||
*pState |= 0x10u; // set “disabled”
|
||||
*pState |= 0x10u; // set "disabled"
|
||||
}
|
||||
else
|
||||
{
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
@@ -20,6 +20,7 @@ namespace Daybreak.API.Services;
|
||||
|
||||
public sealed class MainPlayerService : IDisposable
|
||||
{
|
||||
private readonly ChatService chatService;
|
||||
private readonly InstanceContextService instanceContextService;
|
||||
private readonly IBuildTemplateManager buildTemplateManager;
|
||||
private readonly SkillbarContextService skillbarContextService;
|
||||
@@ -35,10 +36,11 @@ public sealed class MainPlayerService : IDisposable
|
||||
|
||||
private TimeSpan minUpdateFrequency = TimeSpan.MaxValue;
|
||||
private MainPlayerState? mainPlayerState;
|
||||
private DateTimeOffset lastUpdateTime = DateTimeOffset.MinValue;
|
||||
private DateTimeOffset lastFrequencyUpdate = DateTimeOffset.MinValue;
|
||||
private DateTime lastUpdateTime = DateTime.MinValue;
|
||||
private DateTime lastFrequencyUpdate = DateTime.MinValue;
|
||||
|
||||
public MainPlayerService(
|
||||
ChatService chatService,
|
||||
InstanceContextService instanceContextService,
|
||||
IBuildTemplateManager buildTemplateManager,
|
||||
SkillbarContextService skillbarContextService,
|
||||
@@ -47,6 +49,7 @@ public sealed class MainPlayerService : IDisposable
|
||||
GameThreadService gameThreadService,
|
||||
ILogger<MainPlayerService> logger)
|
||||
{
|
||||
this.chatService = chatService.ThrowIfNull();
|
||||
this.instanceContextService = instanceContextService.ThrowIfNull();
|
||||
this.buildTemplateManager = buildTemplateManager.ThrowIfNull();
|
||||
this.skillbarContextService = skillbarContextService.ThrowIfNull();
|
||||
@@ -62,17 +65,17 @@ public sealed class MainPlayerService : IDisposable
|
||||
this.callbackRegistration?.Dispose();
|
||||
}
|
||||
|
||||
public Task<bool> SetCurrentBuild(string buildCode, CancellationToken cancellationToken)
|
||||
public async Task<bool> SetCurrentBuild(string buildCode, CancellationToken cancellationToken)
|
||||
{
|
||||
var scopedLogger = this.logger.CreateScopedLogger();
|
||||
if (!this.buildTemplateManager.TryDecodeTemplate(buildCode, out var build) ||
|
||||
build is not SingleBuildEntry singleBuild)
|
||||
{
|
||||
scopedLogger.LogError("Failed to decode build template from code {buildCode}", buildCode);
|
||||
return Task.FromResult(false);
|
||||
return false;
|
||||
}
|
||||
|
||||
return this.gameThreadService.QueueOnGameThread(() =>
|
||||
var result = await this.gameThreadService.QueueOnGameThread(() =>
|
||||
{
|
||||
unsafe
|
||||
{
|
||||
@@ -137,7 +140,7 @@ public sealed class MainPlayerService : IDisposable
|
||||
var attributeIds = new Array12Uint();
|
||||
var attributeValues = new Array12Uint();
|
||||
var skills = new Array8Uint();
|
||||
for(var i = 0; i < singleBuild.Attributes.Count && i < 12; i++)
|
||||
for (var i = 0; i < singleBuild.Attributes.Count && i < 12; i++)
|
||||
{
|
||||
if (singleBuild.Attributes[i].Attribute is null)
|
||||
{
|
||||
@@ -148,7 +151,7 @@ public sealed class MainPlayerService : IDisposable
|
||||
attributeValues[i] = (uint)singleBuild.Attributes[i].Points;
|
||||
}
|
||||
|
||||
for(var i =0; i < singleBuild.Skills.Count && i < 8; i++)
|
||||
for (var i = 0; i < singleBuild.Skills.Count && i < 8; i++)
|
||||
{
|
||||
skills[i] = (uint)singleBuild.Skills[i].Id;
|
||||
}
|
||||
@@ -165,6 +168,17 @@ public sealed class MainPlayerService : IDisposable
|
||||
return true;
|
||||
}
|
||||
}, cancellationToken);
|
||||
|
||||
if (result)
|
||||
{
|
||||
await this.chatService.AddMessageAsync("Build applied successfully.", "Daybreak.API", Channel.Moderator, cancellationToken);
|
||||
}
|
||||
else
|
||||
{
|
||||
await this.chatService.AddMessageAsync("Failed to apply build.", "Daybreak.API", Channel.Moderator, cancellationToken);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
public Task<BuildEntry?> GetCurrentBuild(CancellationToken cancellationToken)
|
||||
@@ -247,7 +261,7 @@ public sealed class MainPlayerService : IDisposable
|
||||
|
||||
return new QuestLogInformation(
|
||||
gameContext.Pointer->WorldContext->ActiveQuestId,
|
||||
gameContext.Pointer->WorldContext->QuestLog.AsValueEnumerable().Select(q => new QuestInformation(q.QuestId, q.MapFrom, q.MapTo)).ToList());
|
||||
gameContext.Pointer->WorldContext->QuestLog.AsValueEnumerable().Select(q => new QuestInformation(q.QuestId, q.MapFrom)).ToList());
|
||||
}
|
||||
}, cancellationToken);
|
||||
}
|
||||
@@ -378,23 +392,19 @@ public sealed class MainPlayerService : IDisposable
|
||||
return default;
|
||||
}
|
||||
|
||||
if (!gameContext.TryGetPlayerId(out var playerId))
|
||||
var playerIndex = gameContext.Pointer->CharContext->PlayerNumber;
|
||||
var players = gameContext.Pointer->WorldContext->Players;
|
||||
if (playerIndex < 0 || playerIndex >= players.Size)
|
||||
{
|
||||
scopedLogger.LogError("Failed to get player id");
|
||||
return default;
|
||||
}
|
||||
|
||||
var player = gameContext.Pointer->WorldContext->Players.AsValueEnumerable().FirstOrDefault(p => p.AgentId == playerId);
|
||||
if (player.AgentId != playerId)
|
||||
{
|
||||
scopedLogger.LogError("Failed to find player with id {playerId}", playerId);
|
||||
scopedLogger.LogError("Failed to get player id. Got {playerNumber}", playerIndex);
|
||||
return default;
|
||||
}
|
||||
|
||||
var player = gameContext.Pointer->WorldContext->Players[(int)playerIndex];
|
||||
var currentTier = player.ActiveTitleTier;
|
||||
TitleContext? currentTitle = default;
|
||||
int? id = -1;
|
||||
for(var i = 0; i < gameContext.Pointer->WorldContext->Titles.Size; i++)
|
||||
for (var i = 0; i < gameContext.Pointer->WorldContext->Titles.Size; i++)
|
||||
{
|
||||
var title = gameContext.Pointer->WorldContext->Titles.Skip(i).FirstOrDefault();
|
||||
if (title.CurrentTitleTierIndex == currentTier)
|
||||
@@ -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)]
|
||||
|
||||
@@ -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)
|
||||
@@ -233,7 +241,7 @@ public sealed class PartyService(
|
||||
{
|
||||
var scopedLogger = this.logger.CreateScopedLogger();
|
||||
scopedLogger.LogDebug("Spawning {heroCount} heroes for party loadout", partyLoadout.Entries.AsValueEnumerable().Count(c => c.HeroId != 0));
|
||||
foreach (var entry in partyLoadout.Entries)
|
||||
foreach (var entry in partyLoadout.Entries.AsValueEnumerable().OrderBy(e => e.Build.Primary))
|
||||
{
|
||||
if (entry.HeroId != 0 &&
|
||||
Hero.TryParse(entry.HeroId, out var hero))
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
using Daybreak.API.Services.Interop;
|
||||
using Daybreak.Shared.Services.BuildTemplates;
|
||||
using Daybreak.Shared.Services.MDns;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
|
||||
namespace Daybreak.API.Services;
|
||||
@@ -9,9 +8,8 @@ public static class WebApplicationBuilderExtensions
|
||||
{
|
||||
public static WebApplicationBuilder WithDaybreakServices(this WebApplicationBuilder builder)
|
||||
{
|
||||
builder.Services.AddSingleton<HashingService>();
|
||||
builder.Services.AddSingleton<IBuildTemplateManager, BuildTemplateManager>();
|
||||
builder.Services.AddSingleton<IMDomainNameService, MDomainNameService>();
|
||||
builder.Services.AddHostedService<ApiAdvertisingService>();
|
||||
builder.Services.AddSingleton<MemoryScanningService>();
|
||||
builder.Services.AddSingleton<ChatService>();
|
||||
builder.Services.AddSingleton<MainPlayerService>();
|
||||
@@ -19,6 +17,7 @@ public static class WebApplicationBuilderExtensions
|
||||
builder.Services.AddSingleton<PartyService>();
|
||||
builder.Services.AddSingleton<UIService>();
|
||||
builder.Services.AddSingleton<LoginService>();
|
||||
builder.Services.AddSingleton<InventoryService>();
|
||||
builder.WithHookHostedService<GameThreadService>();
|
||||
builder.WithHookHostedService<ChatHandlingService>();
|
||||
builder.WithAddressService<SkillbarContextService>();
|
||||
@@ -27,6 +26,7 @@ public static class WebApplicationBuilderExtensions
|
||||
builder.WithAddressService<AgentContextService>();
|
||||
builder.WithAddressService<PlatformContextService>();
|
||||
builder.WithAddressService<PartyContextService>();
|
||||
builder.WithAddressService<PreferencesService>();
|
||||
builder.WithHookAddressHostedService<UIContextService>();
|
||||
return builder;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
using Daybreak.Shared.Attributes;
|
||||
using Daybreak.Shared.Models.Themes;
|
||||
|
||||
namespace Daybreak.Attributes;
|
||||
public sealed class DaybreakThemeOptionsAttribute : OptionValuesFactoryAttribute
|
||||
{
|
||||
public override List<object> ValuesFactory()
|
||||
{
|
||||
return [.. Theme.Themes.Cast<object>()];
|
||||
}
|
||||
}
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
using Daybreak.Attributes;
|
||||
using Daybreak.Shared.Attributes;
|
||||
using Daybreak.Shared.Configuration.Options;
|
||||
|
||||
namespace Daybreak.Configuration.Options;
|
||||
+5
-5
@@ -1,15 +1,15 @@
|
||||
using Daybreak.Attributes;
|
||||
using Newtonsoft.Json;
|
||||
using Daybreak.Shared.Attributes;
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace Daybreak.Configuration.Options;
|
||||
|
||||
[OptionsName(Name = "Synchronization")]
|
||||
[OptionsIgnore]
|
||||
[OptionsSynchronizationIgnore]
|
||||
internal sealed class SynchronizationOptions
|
||||
public sealed class SynchronizationOptions
|
||||
{
|
||||
[JsonProperty(nameof(ProtectedGraphAccessToken))]
|
||||
[JsonPropertyName(nameof(ProtectedGraphAccessToken))]
|
||||
public string? ProtectedGraphAccessToken { get; set; }
|
||||
[JsonProperty(nameof(ProtectedGraphRefreshToken))]
|
||||
[JsonPropertyName(nameof(ProtectedGraphRefreshToken))]
|
||||
public string? ProtectedGraphRefreshToken { get; set; }
|
||||
}
|
||||
+3
-3
@@ -1,6 +1,6 @@
|
||||
using Daybreak.Attributes;
|
||||
using Daybreak.Shared.Attributes;
|
||||
using Daybreak.Shared.Models;
|
||||
using Newtonsoft.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace Daybreak.Configuration.Options;
|
||||
|
||||
@@ -8,6 +8,6 @@ namespace Daybreak.Configuration.Options;
|
||||
[OptionsName(Name = "Credentials")]
|
||||
internal sealed class CredentialManagerOptions
|
||||
{
|
||||
[JsonProperty(nameof(ProtectedLoginCredentials))]
|
||||
[JsonPropertyName(nameof(ProtectedLoginCredentials))]
|
||||
public List<ProtectedLoginCredentials> ProtectedLoginCredentials { get; set; } = [];
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
using Daybreak.Shared.Attributes;
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace Daybreak.Configuration.Options;
|
||||
|
||||
[OptionsName(Name = "DSOAL")]
|
||||
[OptionsIgnore]
|
||||
internal sealed class DSOALOptions
|
||||
{
|
||||
[JsonPropertyName(nameof(Enabled))]
|
||||
[OptionName(Name = "Enabled", Description = "If true, the launcher will also launch DSOAL when launching GuildWars")]
|
||||
public bool Enabled { get; set; }
|
||||
}
|
||||
+6
-9
@@ -1,21 +1,18 @@
|
||||
using Daybreak.Attributes;
|
||||
using Newtonsoft.Json;
|
||||
using Daybreak.Shared.Attributes;
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace Daybreak.Configuration.Options;
|
||||
|
||||
[OptionsName(Name = "DXVK")]
|
||||
[OptionsIgnore]
|
||||
internal sealed class DXVKOptions
|
||||
{
|
||||
[JsonProperty(nameof(Path))]
|
||||
[OptionName(Name = "Path", Description = "The path to the DXVK installation")]
|
||||
[OptionSynchronizationIgnore]
|
||||
public string? Path { get; set; }
|
||||
|
||||
[JsonProperty(nameof(Version))]
|
||||
[JsonPropertyName(nameof(Version))]
|
||||
[OptionSynchronizationIgnore]
|
||||
[OptionIgnore]
|
||||
public string? Version { get; set; }
|
||||
|
||||
[JsonProperty(nameof(Enabled))]
|
||||
[JsonPropertyName(nameof(Enabled))]
|
||||
[OptionName(Name = "Enabled", Description = "If true, the launcher will also launch DXVK when launching GuildWars")]
|
||||
public bool Enabled { get; set; }
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
using Daybreak.Shared.Attributes;
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace Daybreak.Configuration.Options;
|
||||
|
||||
[OptionsName(Name = "Daybreak API")]
|
||||
[OptionsIgnore]
|
||||
public sealed class DaybreakApiOptions
|
||||
{
|
||||
[JsonPropertyName(nameof(Enabled))]
|
||||
[OptionName(Name = "Enabled", Description = "If true, the Daybreak API is injected into Guild Wars, enabling extended functionality such as loading builds, character switching, and the focus view")]
|
||||
public bool Enabled { get; set; } = false;
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
using Daybreak.Shared.Attributes;
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace Daybreak.Configuration.Options;
|
||||
|
||||
[OptionsName(Name = "DirectSong")]
|
||||
[OptionsIgnore]
|
||||
internal sealed class DirectSongOptions
|
||||
{
|
||||
[JsonPropertyName(nameof(Enabled))]
|
||||
[OptionName(Name = "Enabled", Description = "If true, Daybreak will setup DirectSong when launching Guild Wars")]
|
||||
public bool Enabled { get; set; } = false;
|
||||
}
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
using Daybreak.Attributes;
|
||||
using Daybreak.Shared.Attributes;
|
||||
|
||||
namespace Daybreak.Configuration.Options;
|
||||
|
||||
+12
-20
@@ -1,50 +1,42 @@
|
||||
using Daybreak.Attributes;
|
||||
using Daybreak.Configuration.FocusView;
|
||||
using Daybreak.Shared.Models.Browser;
|
||||
using Newtonsoft.Json;
|
||||
using Daybreak.Shared.Attributes;
|
||||
using Daybreak.Shared.Models.FocusView;
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace Daybreak.Configuration.Options;
|
||||
|
||||
[OptionsName(Name = "Focus View")]
|
||||
[OptionsIgnore]
|
||||
public sealed class FocusViewOptions
|
||||
{
|
||||
[JsonProperty(nameof(Enabled))]
|
||||
[OptionName(Name = "Enabled", Description = "If true, the focus view is enabled, showing live information from the game")]
|
||||
public bool Enabled { get; set; } = true;
|
||||
|
||||
[JsonProperty(nameof(ExperienceDisplay))]
|
||||
[JsonPropertyName(nameof(ExperienceDisplay))]
|
||||
[OptionName(Name = "Experience Display Mode", Description = "Sets how should the experience display show the information")]
|
||||
public ExperienceDisplay ExperienceDisplay { get; set; }
|
||||
|
||||
[JsonProperty(nameof(KurzickPointsDisplay))]
|
||||
[JsonPropertyName(nameof(KurzickPointsDisplay))]
|
||||
[OptionName(Name = "Kurzick Points Display Mode", Description = "Sets how should the kurzick points display show the information")]
|
||||
public PointsDisplay KurzickPointsDisplay { get; set; }
|
||||
|
||||
[JsonProperty(nameof(LuxonPointsDisplay))]
|
||||
[JsonPropertyName(nameof(LuxonPointsDisplay))]
|
||||
[OptionName(Name = "Luxon Points Display Mode", Description = "Sets how should the luxon points display show the information")]
|
||||
public PointsDisplay LuxonPointsDisplay { get; set; }
|
||||
|
||||
[JsonProperty(nameof(BalthazarPointsDisplay))]
|
||||
[JsonPropertyName(nameof(BalthazarPointsDisplay))]
|
||||
[OptionName(Name = "Balthazar Points Display Mode", Description = "Sets how should the balthazar points display show the information")]
|
||||
public PointsDisplay BalthazarPointsDisplay { get; set; }
|
||||
|
||||
[JsonProperty(nameof(ImperialPointsDisplay))]
|
||||
[JsonPropertyName(nameof(ImperialPointsDisplay))]
|
||||
[OptionName(Name = "Imperial Points Display Mode", Description = "Sets how should the imperial points display show the information")]
|
||||
public PointsDisplay ImperialPointsDisplay { get; set; }
|
||||
|
||||
[JsonProperty(nameof(VanquishingDisplay))]
|
||||
[JsonPropertyName(nameof(VanquishingDisplay))]
|
||||
[OptionName(Name = "Vanquishing Display Mode", Description = "Sets how should the vanquishing display show the information")]
|
||||
public PointsDisplay VanquishingDisplay { get; set; }
|
||||
|
||||
[JsonProperty(nameof(HealthDisplay))]
|
||||
[JsonPropertyName(nameof(HealthDisplay))]
|
||||
[OptionName(Name = "Health Display Mode", Description = "Sets how should the health display show the information")]
|
||||
public PointsDisplay HealthDisplay { get; set; }
|
||||
|
||||
[JsonProperty(nameof(EnergyDisplay))]
|
||||
[JsonPropertyName(nameof(EnergyDisplay))]
|
||||
[OptionName(Name = "Energy Display Mode", Description = "Sets how should the energy display show the information")]
|
||||
public PointsDisplay EnergyDisplay { get; set; }
|
||||
|
||||
[JsonProperty(nameof(BrowserHistory))]
|
||||
[OptionIgnore]
|
||||
public BrowserHistory BrowserHistory { get; set; } = new();
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
using Daybreak.Shared.Attributes;
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace Daybreak.Configuration.Options;
|
||||
|
||||
[OptionsSynchronizationIgnore]
|
||||
[OptionsIgnore]
|
||||
public sealed class GuildWarsScreenPlacerOptions
|
||||
{
|
||||
[JsonPropertyName(nameof(Enabled))]
|
||||
public bool Enabled { get; set; }
|
||||
|
||||
[JsonPropertyName(nameof(DesiredScreen))]
|
||||
public int DesiredScreen { get; set; }
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
using Daybreak.Shared.Attributes;
|
||||
|
||||
namespace Daybreak.Configuration.Options;
|
||||
|
||||
[OptionsIgnore]
|
||||
public sealed class GuildWarsVersionCheckerOptions
|
||||
{
|
||||
public bool IsEnabled { get; set; } = true;
|
||||
}
|
||||
+3
-3
@@ -1,11 +1,11 @@
|
||||
using Daybreak.Attributes;
|
||||
using Newtonsoft.Json;
|
||||
using Daybreak.Shared.Attributes;
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace Daybreak.Configuration.Options;
|
||||
[OptionsIgnore]
|
||||
[OptionsSynchronizationIgnore]
|
||||
internal sealed class GuildwarsExecutableOptions
|
||||
{
|
||||
[JsonProperty(nameof(ExecutablePaths))]
|
||||
[JsonPropertyName(nameof(ExecutablePaths))]
|
||||
public List<string> ExecutablePaths { get; set; } = [];
|
||||
}
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
using Daybreak.Attributes;
|
||||
using Daybreak.Shared.Attributes;
|
||||
using Daybreak.Shared.Configuration.Options;
|
||||
|
||||
namespace Daybreak.Configuration.Options;
|
||||
+3
-3
@@ -1,6 +1,6 @@
|
||||
using Daybreak.Attributes;
|
||||
using Daybreak.Shared.Attributes;
|
||||
using Daybreak.Shared.Models.LaunchConfigurations;
|
||||
using Newtonsoft.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace Daybreak.Configuration.Options;
|
||||
|
||||
@@ -8,6 +8,6 @@ namespace Daybreak.Configuration.Options;
|
||||
[OptionsSynchronizationIgnore]
|
||||
internal sealed class LaunchConfigurationServiceOptions
|
||||
{
|
||||
[JsonProperty(nameof(LaunchConfigurations))]
|
||||
[JsonPropertyName(nameof(LaunchConfigurations))]
|
||||
public List<LaunchConfiguration> LaunchConfigurations { get; set; } = [];
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
using System.Text.Json.Serialization;
|
||||
using Daybreak.Shared.Attributes;
|
||||
|
||||
namespace Daybreak.Configuration.Options;
|
||||
|
||||
[OptionsName(Name = "Launcher")]
|
||||
public sealed class LauncherOptions
|
||||
{
|
||||
[JsonPropertyName(nameof(ShortcutLocation))]
|
||||
[OptionName(
|
||||
Name = "Shortcut Location",
|
||||
Description = "Location where the shortcut will be placed"
|
||||
)]
|
||||
[OptionSynchronizationIgnore]
|
||||
public string? ShortcutLocation { get; set; } =
|
||||
Environment.GetFolderPath(Environment.SpecialFolder.Desktop);
|
||||
|
||||
[JsonPropertyName(nameof(PlaceShortcut))]
|
||||
[OptionName(
|
||||
Name = "Shortcut Placed",
|
||||
Description = "If true, the shortcut will be placed. If false, the shortcut will be deleted"
|
||||
)]
|
||||
public bool PlaceShortcut { get; set; }
|
||||
|
||||
[JsonPropertyName(nameof(AutoCheckUpdate))]
|
||||
[OptionName(Name = "Auto-Check For Updates", Description = "Automatically check for updates")]
|
||||
public bool AutoCheckUpdate { get; set; } = true;
|
||||
|
||||
[JsonPropertyName(nameof(MultiLaunchSupport))]
|
||||
[OptionName(
|
||||
Name = "Multi-Launch Support",
|
||||
Description = "If true, the launcher will support multiple executables being launched at the same time"
|
||||
)]
|
||||
public bool MultiLaunchSupport { get; set; }
|
||||
|
||||
[JsonPropertyName(nameof(ModStartupTimeout))]
|
||||
[OptionName(
|
||||
Name = "Mod Startup Timeout",
|
||||
Description = "Amount of seconds that Daybreak will wait for each mod to start-up before cancelling the tasks"
|
||||
)]
|
||||
[OptionRange<double>(MinValue = 30, MaxValue = 300)]
|
||||
public double ModStartupTimeout { get; set; } = 30;
|
||||
|
||||
[JsonPropertyName(nameof(CancelLaunchOutOfDateMods))]
|
||||
[OptionName(
|
||||
Name = "Cancel launch on mod updates",
|
||||
Description = "If true, the launcher will stop launching Guild Wars when it detects that an active mod has an update available"
|
||||
)]
|
||||
public bool CancelLaunchOutOfDateMods { get; set; }
|
||||
|
||||
[JsonPropertyName(nameof(SaveWindowPosition))]
|
||||
[OptionName(
|
||||
Name = "Save Window Position",
|
||||
Description = "If true, the launcher will save its position on the screen between launching"
|
||||
)]
|
||||
public bool SaveWindowPosition { get; set; }
|
||||
}
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
using Daybreak.Attributes;
|
||||
using Daybreak.Shared.Attributes;
|
||||
using Daybreak.Services.Plugins.Models;
|
||||
|
||||
namespace Daybreak.Configuration.Options;
|
||||
+5
-4
@@ -1,16 +1,17 @@
|
||||
using Daybreak.Attributes;
|
||||
using Newtonsoft.Json;
|
||||
using Daybreak.Shared.Attributes;
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace Daybreak.Configuration.Options;
|
||||
|
||||
[OptionsName(Name = "ReShade")]
|
||||
[OptionsIgnore]
|
||||
internal sealed class ReShadeOptions
|
||||
{
|
||||
[JsonProperty(nameof(Enabled))]
|
||||
[JsonPropertyName(nameof(Enabled))]
|
||||
[OptionName(Name = "Enabled", Description = "If true, Daybreak will attempt to inject ReShade into the starting Guild Wars executable")]
|
||||
public bool Enabled { get; set; } = false;
|
||||
|
||||
[JsonProperty(nameof(AutoUpdate))]
|
||||
[JsonPropertyName(nameof(AutoUpdate))]
|
||||
[OptionName(Name = "Auto-update", Description = "If true, Daybreak will periodically check ReShade for updates")]
|
||||
public bool AutoUpdate { get; set; } = true;
|
||||
}
|
||||
+4
-4
@@ -1,16 +1,16 @@
|
||||
using Daybreak.Attributes;
|
||||
using Daybreak.Shared.Attributes;
|
||||
|
||||
namespace Daybreak.Configuration.Options;
|
||||
|
||||
[OptionsName(Name = "Screen Manager")]
|
||||
[OptionsIgnore]
|
||||
[OptionsSynchronizationIgnore]
|
||||
internal sealed class ScreenManagerOptions
|
||||
public sealed class ScreenManagerOptions
|
||||
{
|
||||
public double X { get; set; }
|
||||
public double Y { get; set; }
|
||||
public double Width { get; set; }
|
||||
public double Height { get; set; }
|
||||
public double DpiX { get; set; }
|
||||
public double DpiY { get; set; }
|
||||
public int DpiX { get; set; } = 96;
|
||||
public int DpiY { get; set; } = 96;
|
||||
}
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
using Daybreak.Attributes;
|
||||
using Daybreak.Shared.Attributes;
|
||||
|
||||
namespace Daybreak.Configuration.Options;
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
using Daybreak.Attributes;
|
||||
using Daybreak.Shared.Attributes;
|
||||
using Daybreak.Shared.Models.Themes;
|
||||
using Daybreak.Themes;
|
||||
|
||||
namespace Daybreak.Configuration.Options;
|
||||
|
||||
[OptionsName(Name = "Theme")]
|
||||
public sealed class ThemeOptions
|
||||
{
|
||||
[OptionName(Name = "Theme", Description = "The application theme dictates the color scheme of the application")]
|
||||
[DaybreakThemeOptions]
|
||||
public Theme ApplicationTheme { get; set; } = CoreThemes.Daybreak;
|
||||
|
||||
[OptionName(Name = "UI Scale", Description = "The UI scale adjusts the size of the user interface elements")]
|
||||
[OptionRange<double>(MaxValue = 1.5d, MinValue = 0.5d)]
|
||||
public double ApplicationScale { get; set; } = 1.0;
|
||||
}
|
||||
+4
-8
@@ -1,17 +1,13 @@
|
||||
using Daybreak.Attributes;
|
||||
using Newtonsoft.Json;
|
||||
using Daybreak.Shared.Attributes;
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace Daybreak.Configuration.Options;
|
||||
|
||||
[OptionsName(Name = "GWToolbox")]
|
||||
[OptionsIgnore]
|
||||
internal sealed class ToolboxOptions
|
||||
{
|
||||
[JsonProperty(nameof(DllPath))]
|
||||
[OptionName(Name = "DllPath", Description = "The path to GWToolboxdll.dll")]
|
||||
[OptionSynchronizationIgnore]
|
||||
public string? DllPath { get; set; }
|
||||
|
||||
[JsonProperty(nameof(Enabled))]
|
||||
[JsonPropertyName(nameof(Enabled))]
|
||||
[OptionName(Name = "Enabled", Description = "If true, Daybreak will also launch GWToolboxdll when launching GuildWars")]
|
||||
public bool Enabled { get; set; }
|
||||
}
|
||||
+2
-2
@@ -1,4 +1,4 @@
|
||||
using Daybreak.Attributes;
|
||||
using Daybreak.Shared.Attributes;
|
||||
using Daybreak.Shared.Models.Trade;
|
||||
|
||||
namespace Daybreak.Configuration.Options;
|
||||
@@ -17,7 +17,7 @@ internal sealed class TradeAlertingOptions
|
||||
[OptionIgnore]
|
||||
public List<ITradeAlert> Alerts { get; set; } = [];
|
||||
|
||||
[OptionName(Name = "Quote Alerts Interval", Description = "The amount of seconds in between checking for alerts based on trade quotes")]
|
||||
[OptionName(Name = "Alerts Interval", Description = "The amount of seconds in between checking for alerts based on trade messages and quotes")]
|
||||
[OptionRange<double>(MinValue = 30d, MaxValue = 1440d)]
|
||||
public double QuoteAlertsInterval { get; set; } = 30;
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
using Daybreak.Shared.Attributes;
|
||||
|
||||
namespace Daybreak.Configuration.Options;
|
||||
|
||||
[OptionsName(Name = "Trader Quotes")]
|
||||
[OptionsSynchronizationIgnore]
|
||||
[OptionsIgnore]
|
||||
internal sealed class TraderQuotesOptions
|
||||
{
|
||||
[OptionIgnore]
|
||||
public string HttpsUri { get; set; } = "https://kamadan.gwtoolbox.com/";
|
||||
}
|
||||
+6
-5
@@ -1,21 +1,22 @@
|
||||
using Daybreak.Attributes;
|
||||
using Daybreak.Shared.Attributes;
|
||||
using Daybreak.Shared.Models.UMod;
|
||||
using Newtonsoft.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace Daybreak.Configuration.Options;
|
||||
|
||||
[OptionsName(Name = "uMod")]
|
||||
[OptionsIgnore]
|
||||
public sealed class UModOptions
|
||||
{
|
||||
[JsonProperty(nameof(Enabled))]
|
||||
[JsonPropertyName(nameof(Enabled))]
|
||||
[OptionName(Name = "Enabled", Description = "If true, Daybreak will also launch uMod when launching GuildWars")]
|
||||
public bool Enabled { get; set; }
|
||||
|
||||
[JsonProperty(nameof(AutoEnableMods))]
|
||||
[JsonPropertyName(nameof(AutoEnableMods))]
|
||||
[OptionName(Name = "Auto-Enable Mods", Description = "If true, mods loaded into Daybreak will be enabled by default")]
|
||||
public bool AutoEnableMods { get; set; } = true;
|
||||
|
||||
[JsonProperty(nameof(Mods))]
|
||||
[JsonPropertyName(nameof(Mods))]
|
||||
[OptionIgnore]
|
||||
[OptionSynchronizationIgnore]
|
||||
public List<UModEntry> Mods { get; set; } = [];
|
||||
@@ -0,0 +1,578 @@
|
||||
using System.Core.Extensions;
|
||||
using System.Extensions;
|
||||
using System.Reflection;
|
||||
using Daybreak.Configuration.Options;
|
||||
using Daybreak.Extensions;
|
||||
using Daybreak.Services.Api;
|
||||
using Daybreak.Services.ApplicationArguments;
|
||||
using Daybreak.Services.ApplicationArguments.ArgumentHandling;
|
||||
using Daybreak.Services.ApplicationLauncher;
|
||||
using Daybreak.Services.BuildTemplates;
|
||||
using Daybreak.Services.Credentials;
|
||||
using Daybreak.Services.DirectSong;
|
||||
using Daybreak.Services.Downloads;
|
||||
using Daybreak.Services.Events;
|
||||
using Daybreak.Services.ExceptionHandling;
|
||||
using Daybreak.Services.ExecutableManagement;
|
||||
using Daybreak.Services.Experience;
|
||||
using Daybreak.Services.FileProviders;
|
||||
using Daybreak.Services.Github;
|
||||
using Daybreak.Services.Graph.Models;
|
||||
using Daybreak.Services.Guildwars;
|
||||
using Daybreak.Services.GuildWars;
|
||||
using Daybreak.Services.Injection;
|
||||
using Daybreak.Services.InternetChecker;
|
||||
using Daybreak.Services.LaunchConfigurations;
|
||||
using Daybreak.Services.Logging;
|
||||
using Daybreak.Services.Metrics;
|
||||
using Daybreak.Services.Mods;
|
||||
using Daybreak.Services.Monitoring;
|
||||
using Daybreak.Services.Notifications;
|
||||
using Daybreak.Services.Notifications.Handlers;
|
||||
using Daybreak.Services.Onboarding;
|
||||
using Daybreak.Services.Options;
|
||||
using Daybreak.Services.Plugins;
|
||||
using Daybreak.Services.Privilege;
|
||||
using Daybreak.Services.ReShade;
|
||||
using Daybreak.Services.Screenshots;
|
||||
using Daybreak.Services.Startup;
|
||||
using Daybreak.Services.Startup.Actions;
|
||||
using Daybreak.Services.TarGz;
|
||||
using Daybreak.Services.Telemetry;
|
||||
using Daybreak.Services.Themes;
|
||||
using Daybreak.Services.Toolbox;
|
||||
using Daybreak.Services.Toolbox.Notifications;
|
||||
using Daybreak.Services.Toolbox.Utilities;
|
||||
using Daybreak.Services.TradeChat;
|
||||
using Daybreak.Services.TradeChat.Models;
|
||||
using Daybreak.Services.TradeChat.Notifications;
|
||||
using Daybreak.Services.UMod;
|
||||
using Daybreak.Services.Updater;
|
||||
using Daybreak.Services.Wiki;
|
||||
using Daybreak.Shared.Models.Plugins;
|
||||
using Daybreak.Shared.Services.Api;
|
||||
using Daybreak.Shared.Services.ApplicationArguments;
|
||||
using Daybreak.Shared.Services.ApplicationLauncher;
|
||||
using Daybreak.Shared.Services.BuildTemplates;
|
||||
using Daybreak.Shared.Services.Credentials;
|
||||
using Daybreak.Shared.Services.DirectSong;
|
||||
using Daybreak.Shared.Services.Downloads;
|
||||
using Daybreak.Shared.Services.Events;
|
||||
using Daybreak.Shared.Services.ExecutableManagement;
|
||||
using Daybreak.Shared.Services.Experience;
|
||||
using Daybreak.Shared.Services.FileProviders;
|
||||
using Daybreak.Shared.Services.Github;
|
||||
using Daybreak.Shared.Services.Guildwars;
|
||||
using Daybreak.Shared.Services.Initialization;
|
||||
using Daybreak.Shared.Services.Injection;
|
||||
using Daybreak.Shared.Services.InternetChecker;
|
||||
using Daybreak.Shared.Services.LaunchConfigurations;
|
||||
using Daybreak.Shared.Services.Menu;
|
||||
using Daybreak.Shared.Services.Metrics;
|
||||
using Daybreak.Shared.Services.Mods;
|
||||
using Daybreak.Shared.Services.Notifications;
|
||||
using Daybreak.Shared.Services.Onboarding;
|
||||
using Daybreak.Shared.Services.Options;
|
||||
using Daybreak.Shared.Services.Plugins;
|
||||
using Daybreak.Shared.Services.Screenshots;
|
||||
using Daybreak.Shared.Services.TarGz;
|
||||
using Daybreak.Shared.Services.Themes;
|
||||
using Daybreak.Shared.Services.Toolbox;
|
||||
using Daybreak.Shared.Services.TradeChat;
|
||||
using Daybreak.Shared.Services.UMod;
|
||||
using Daybreak.Shared.Services.Updater;
|
||||
using Daybreak.Shared.Services.Wiki;
|
||||
using Daybreak.Services.Graph;
|
||||
using Daybreak.Themes;
|
||||
using Daybreak.Views;
|
||||
using Daybreak.Views.Copy;
|
||||
using Daybreak.Views.Installation;
|
||||
using Daybreak.Views.Mods;
|
||||
using Daybreak.Views.Trade;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.FileProviders;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.FluentUI.AspNetCore.Components;
|
||||
using Microsoft.Identity.Client;
|
||||
using OpenTelemetry.Resources;
|
||||
using TrailBlazr.Extensions;
|
||||
using TrailBlazr.Services;
|
||||
using IMenuService = Daybreak.Shared.Services.Menu.IMenuService;
|
||||
using MenuService = Daybreak.Services.Menu.MenuService;
|
||||
using LogLevel = Microsoft.Extensions.Logging.LogLevel;
|
||||
|
||||
namespace Daybreak.Configuration;
|
||||
|
||||
public class ProjectConfiguration : PluginConfigurationBase
|
||||
{
|
||||
public static readonly Version CurrentVersion = Version.Parse(
|
||||
Assembly.GetExecutingAssembly().GetName().Version?.ToString()
|
||||
?? throw new InvalidOperationException("Unable to get current version")
|
||||
);
|
||||
|
||||
public override void RegisterServices(IServiceCollection services)
|
||||
{
|
||||
services.ThrowIfNull();
|
||||
|
||||
services.AddFluentUIComponents();
|
||||
services.AddTrailBlazr();
|
||||
services.AddSingleton<IFileProvider, DaybreakFileProvider>();
|
||||
|
||||
RegisterHttpClients(services);
|
||||
services.RegisterClientWebSocket<TradeChatService<KamadanTradeChatOptions>>().Build();
|
||||
services.RegisterClientWebSocket<TradeChatService<AscalonTradeChatOptions>>().Build();
|
||||
|
||||
services.AddSingleton(_ =>
|
||||
{
|
||||
var resourceBuilder = ResourceBuilder
|
||||
.CreateDefault()
|
||||
.AddService("Daybreak", serviceVersion: CurrentVersion.ToString());
|
||||
var attributes = new List<KeyValuePair<string, object>>();
|
||||
#if DEBUG
|
||||
attributes.Add(new KeyValuePair<string, object>("deployment.environment", "debug"));
|
||||
#else
|
||||
attributes.Add(
|
||||
new KeyValuePair<string, object>("deployment.environment", "production")
|
||||
);
|
||||
#endif
|
||||
resourceBuilder.AddAttributes(attributes);
|
||||
return resourceBuilder;
|
||||
});
|
||||
|
||||
services.AddSingleton<AppViewModel>();
|
||||
|
||||
services.AddSingleton<ViewManager>();
|
||||
services.AddSingleton<IMenuService, MenuService>();
|
||||
services.AddSingleton<IMenuServiceInitializer, MenuService>(sp =>
|
||||
sp.GetRequiredService<IMenuService>().Cast<MenuService>()
|
||||
);
|
||||
services.AddSingleton<IMenuServiceButtonHandler, MenuService>(sp =>
|
||||
sp.GetRequiredService<IMenuService>().Cast<MenuService>()
|
||||
);
|
||||
services.AddSingleton<IMetricsService, MetricsService>();
|
||||
services.AddSingleton<INotificationService, NotificationService>();
|
||||
services.AddSingleton<INotificationProducer, NotificationService>(sp =>
|
||||
sp.GetRequiredService<INotificationService>().Cast<NotificationService>()
|
||||
);
|
||||
services.AddSingleton<IInternetCheckingService, InternetCheckingService>();
|
||||
services.AddSingleton<INotificationStorage, InMemoryNotificationStorage>();
|
||||
services.AddSingleton<IModsManager, ModsManager>();
|
||||
services.AddSingleton<IPluginsService, PluginsService>();
|
||||
services.AddSingleton<IAttachedApiAccessor, AttachedApiAccessor>();
|
||||
services.AddSingleton<PrivilegeContext>();
|
||||
services.AddSingleton<ViewRedirectContext>();
|
||||
services.AddSingleton<JSConsoleInterop>();
|
||||
services.AddSingleton<OptionsManager>();
|
||||
services.AddSingleton<IOptionsProvider, OptionsManager>(sp =>
|
||||
sp.GetRequiredService<OptionsManager>()
|
||||
);
|
||||
services.AddSingleton<IOptionsSynchronizationService, OptionsSynchronizationService>();
|
||||
|
||||
// Register MSAL IPublicClientApplication for cross-platform OAuth
|
||||
services.AddSingleton<IPublicClientApplication>(sp =>
|
||||
{
|
||||
var logger = sp.GetRequiredService<ILogger<PublicClientApplication>>();
|
||||
try
|
||||
{
|
||||
var clientId = SecretManager.GetSecret(SecretKeys.AadApplicationId);
|
||||
if (string.IsNullOrWhiteSpace(clientId))
|
||||
{
|
||||
logger.LogWarning("AAD Application ID not configured. OneDrive sync will not be available.");
|
||||
return new DummyPublicClientApplication();
|
||||
}
|
||||
|
||||
var builder = PublicClientApplicationBuilder.Create(clientId)
|
||||
.WithLogging((logLevel, message, containsPii) =>
|
||||
{
|
||||
if (containsPii && logLevel > Microsoft.Identity.Client.LogLevel.Info)
|
||||
{
|
||||
message = "[REDACTED]";
|
||||
}
|
||||
|
||||
var equivalentLogLevel = logLevel switch
|
||||
{
|
||||
Microsoft.Identity.Client.LogLevel.Error => LogLevel.Error,
|
||||
Microsoft.Identity.Client.LogLevel.Warning => LogLevel.Warning,
|
||||
Microsoft.Identity.Client.LogLevel.Info => LogLevel.Information,
|
||||
Microsoft.Identity.Client.LogLevel.Verbose => LogLevel.Debug,
|
||||
_ => LogLevel.None
|
||||
};
|
||||
|
||||
logger.Log(equivalentLogLevel, message);
|
||||
}, enablePiiLogging: true, enableDefaultPlatformLogging: true)
|
||||
.WithCacheOptions(new CacheOptions { UseSharedCache = true })
|
||||
.WithRedirectUri(GraphClient.RedirectUri)
|
||||
.WithHttpClientFactory(new DaybreakMsalHttpClientProvider(sp.GetRequiredService<ILogger<GraphClient>>()));
|
||||
|
||||
return builder.Build();
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
logger.LogError(e, "Failed to create PublicClientApplication, using dummy implementation");
|
||||
return new DummyPublicClientApplication();
|
||||
}
|
||||
});
|
||||
|
||||
services.AddScoped<IGraphClient, GraphClient>();
|
||||
|
||||
services.AddScoped<ICredentialManager, CredentialManager>();
|
||||
services.AddScoped<IApplicationLauncher, ApplicationLauncher>();
|
||||
services.AddScoped<IBuildTemplateManager, BuildTemplateManager>();
|
||||
services.AddScoped<IOnboardingService, OnboardingService>();
|
||||
services.AddScoped<IExperienceCalculator, ExperienceCalculator>();
|
||||
services.AddScoped<IAttributePointCalculator, AttributePointCalculator>();
|
||||
services.AddScoped<IDownloadService, DownloadService>();
|
||||
services.AddScoped<IGuildWarsInstaller, IntegratedGuildwarsInstaller>();
|
||||
services.AddScoped<IExceptionHandler, ExceptionHandler>();
|
||||
services.AddScoped<
|
||||
ITradeChatService<KamadanTradeChatOptions>,
|
||||
TradeChatService<KamadanTradeChatOptions>
|
||||
>();
|
||||
services.AddScoped<
|
||||
ITradeChatService<AscalonTradeChatOptions>,
|
||||
TradeChatService<AscalonTradeChatOptions>
|
||||
>();
|
||||
services.AddScoped<ITraderQuoteService, TraderQuoteService>();
|
||||
services.AddScoped<ITradeHistoryDatabase, TradeHistoryDatabase>();
|
||||
services.AddScoped<IGuildWarsCopyService, GuildWarsCopyService>();
|
||||
services.AddScoped<IItemHashService, ItemHashService>();
|
||||
services.AddScoped<IToolboxClient, ToolboxClient>();
|
||||
services.AddSingleton<IGithubClient, GithubClient>();
|
||||
services.AddScoped<IProcessInjector, ProcessInjector>();
|
||||
services.AddScoped<IStubInjector, StubInjector>();
|
||||
services.AddScoped<ITarGzExtractor, TarGzExtractor>();
|
||||
services.AddScoped<ILaunchConfigurationService, LaunchConfigurationService>();
|
||||
services.AddScoped<IEventService, EventService>();
|
||||
services.AddScoped<IApplicationArgumentService, ApplicationArgumentService>();
|
||||
services.AddScoped<IWikiService, WikiService>();
|
||||
services.AddScoped<IScreenshotService, ScreenshotService>();
|
||||
|
||||
services.AddHostedSingleton<IApplicationUpdater, ApplicationUpdater>();
|
||||
services.AddHostedSingleton<IThemeManager, BlazorThemeInteropService>();
|
||||
services.AddHostedSingleton<IConnectivityStatus, ConnectivityStatus>();
|
||||
services.AddHostedSingleton<ITradeAlertingService, TradeAlertingService>();
|
||||
services.AddHostedSingleton<IGuildWarsExecutableManager, GuildWarsExecutableManager>();
|
||||
services.AddHostedSingleton<IEventNotifierService, EventNotifierService>();
|
||||
services.AddHostedSingleton<IApiScanningService, ApiScanningService>();
|
||||
services.AddHostedSingleton<GameScreenshotsTheme>();
|
||||
services.AddHostedService<StartupActionManager>();
|
||||
services.AddHostedService<ProcessorUsageMonitor>();
|
||||
services.AddHostedService<TelemetryHost>();
|
||||
}
|
||||
|
||||
public override void RegisterViews(IViewProducer viewProducer)
|
||||
{
|
||||
viewProducer.ThrowIfNull();
|
||||
|
||||
viewProducer.RegisterView<LaunchView, LaunchViewModel>(isSingleton: true);
|
||||
viewProducer.RegisterView<OptionView, OptionViewModel>();
|
||||
viewProducer.RegisterView<FocusView, FocusViewModel>();
|
||||
viewProducer.RegisterView<BuildListView, BuildListViewModel>();
|
||||
viewProducer.RegisterView<BuildRoutingView, BuildRoutingViewModel>();
|
||||
viewProducer.RegisterView<SingleBuildTemplateView, SingleBuildTemplateViewModel>();
|
||||
viewProducer.RegisterView<TeamBuildTemplateView, TeamBuildTemplateViewModel>();
|
||||
viewProducer.RegisterView<AccountsView, AccountsViewModel>();
|
||||
viewProducer.RegisterView<ExecutablesView, ExecutablesViewModel>();
|
||||
viewProducer.RegisterView<LaunchConfigurationsView, LaunchConfigurationsViewModel>();
|
||||
viewProducer.RegisterView<RequestElevationView, RequestElevationViewModel>();
|
||||
viewProducer.RegisterView<RequestDelevationView, RequestDelevationViewModel>();
|
||||
viewProducer.RegisterView<SettingsSynchronizationView, SettingsSynchronizationViewModel>();
|
||||
viewProducer.RegisterView<TelemetryView, TelemetryViewModel>();
|
||||
viewProducer.RegisterView<VersionManagementView, VersionManagementViewModel>();
|
||||
viewProducer.RegisterView<UpdateConfirmationView, UpdateConfirmationViewModel>();
|
||||
viewProducer.RegisterView<UpdateView, UpdateViewModel>();
|
||||
viewProducer.RegisterView<PluginsView, PluginsViewModel>();
|
||||
viewProducer.RegisterView<LogsView, LogsViewModel>();
|
||||
viewProducer.RegisterView<GuildWarsPartySearchView, GuildWarsPartySearchViewModel>();
|
||||
viewProducer.RegisterView<EventCalendarView, EventCalendarViewModel>();
|
||||
viewProducer.RegisterView<TradeChatView, TradeChatViewModel>();
|
||||
viewProducer.RegisterView<GuildWarsDownloadView, GuildWarsDownloadViewModel>();
|
||||
viewProducer.RegisterView<GuildWarsCopySelectionView, GuildWarsCopySelectionViewModel>();
|
||||
viewProducer.RegisterView<GuildWarsCopyView, GuildWarsCopyViewModel>();
|
||||
viewProducer.RegisterView<TradeAlertsView, TradeAlertsViewModel>();
|
||||
viewProducer.RegisterView<TradeMessageView, TradeMessageViewModel>();
|
||||
viewProducer.RegisterView<TradeQuoteView, TradeQuoteViewModel>();
|
||||
viewProducer.RegisterView<LauncherOnboardingView, LauncherOnboardingViewModel>();
|
||||
viewProducer.RegisterView<ModsView, ModsViewModel>();
|
||||
viewProducer.RegisterView<ModInstallationView, ModInstallationViewModel>();
|
||||
viewProducer.RegisterView<
|
||||
ModInstallationConfirmationView,
|
||||
ModInstallationConfirmationViewModel
|
||||
>();
|
||||
viewProducer.RegisterView<ScreenSelectorView, ScreenSelectorViewModel>();
|
||||
viewProducer.RegisterView<UModManagementView, UModManagementViewModel>();
|
||||
viewProducer.RegisterView<TradeNotificationView, TradeNotificationViewModel>();
|
||||
viewProducer.RegisterView<WikiView, WikiViewModel>();
|
||||
viewProducer.RegisterView<ReShadeManagementView, ReShadeManagementViewModel>();
|
||||
viewProducer.RegisterView<GuildWarsMarketView, GuildWarsMarketViewModel>();
|
||||
}
|
||||
|
||||
public override void RegisterStartupActions(IStartupActionProducer startupActionProducer)
|
||||
{
|
||||
startupActionProducer.ThrowIfNull();
|
||||
|
||||
startupActionProducer.RegisterAction<VerifyNativeComponentsAction>();
|
||||
startupActionProducer.RegisterAction<RenameInstallerAction>();
|
||||
startupActionProducer.RegisterAction<UpdateUModAction>();
|
||||
startupActionProducer.RegisterAction<CredentialsOptionsMigrator>();
|
||||
startupActionProducer.RegisterAction<UpdateToolboxAction>();
|
||||
startupActionProducer.RegisterAction<UpdateGuildWarsExecutable>();
|
||||
startupActionProducer.RegisterAction<UpdateDirectSongAction>();
|
||||
}
|
||||
|
||||
public override void RegisterPostUpdateActions(
|
||||
IPostUpdateActionProducer postUpdateActionProducer
|
||||
)
|
||||
{
|
||||
postUpdateActionProducer.ThrowIfNull();
|
||||
}
|
||||
|
||||
public override void RegisterOptions(IOptionsProducer optionsProducer)
|
||||
{
|
||||
optionsProducer.ThrowIfNull();
|
||||
|
||||
optionsProducer.RegisterOptions<TelemetryOptions>();
|
||||
optionsProducer.RegisterOptions<LauncherOptions>();
|
||||
optionsProducer.RegisterOptions<ThemeOptions>();
|
||||
|
||||
optionsProducer.RegisterOptions<SynchronizationOptions>();
|
||||
optionsProducer.RegisterOptions<FocusViewOptions>();
|
||||
optionsProducer.RegisterOptions<DaybreakApiOptions>();
|
||||
|
||||
optionsProducer.RegisterOptions<ToolboxOptions>();
|
||||
optionsProducer.RegisterOptions<UModOptions>();
|
||||
optionsProducer.RegisterOptions<DSOALOptions>();
|
||||
optionsProducer.RegisterOptions<ReShadeOptions>();
|
||||
optionsProducer.RegisterOptions<DXVKOptions>();
|
||||
|
||||
optionsProducer.RegisterOptions<ScreenManagerOptions>();
|
||||
optionsProducer.RegisterOptions<KamadanTradeChatOptions>();
|
||||
optionsProducer.RegisterOptions<AscalonTradeChatOptions>();
|
||||
optionsProducer.RegisterOptions<TraderQuotesOptions>();
|
||||
optionsProducer.RegisterOptions<TradeAlertingOptions>();
|
||||
optionsProducer.RegisterOptions<EventNotifierOptions>();
|
||||
optionsProducer.RegisterOptions<PluginsServiceOptions>();
|
||||
optionsProducer.RegisterOptions<GuildwarsExecutableOptions>();
|
||||
optionsProducer.RegisterOptions<CredentialManagerOptions>();
|
||||
optionsProducer.RegisterOptions<LaunchConfigurationServiceOptions>();
|
||||
optionsProducer.RegisterOptions<DirectSongOptions>();
|
||||
optionsProducer.RegisterOptions<GuildWarsVersionCheckerOptions>();
|
||||
|
||||
optionsProducer.RegisterOptions<GuildWarsScreenPlacerOptions>();
|
||||
}
|
||||
|
||||
public override void RegisterNotificationHandlers(
|
||||
INotificationHandlerProducer notificationHandlerProducer
|
||||
)
|
||||
{
|
||||
notificationHandlerProducer.RegisterNotificationHandler<NoActionHandler>();
|
||||
notificationHandlerProducer.RegisterNotificationHandler<MessageBoxHandler>();
|
||||
notificationHandlerProducer.RegisterNotificationHandler<TradeMessageNotificationHandler>();
|
||||
notificationHandlerProducer.RegisterNotificationHandler<UpdateNotificationHandler>();
|
||||
notificationHandlerProducer.RegisterNotificationHandler<NavigateToCalendarViewHandler>();
|
||||
notificationHandlerProducer.RegisterNotificationHandler<GuildWarsBatchUpdateNotificationHandler>();
|
||||
notificationHandlerProducer.RegisterNotificationHandler<ToolboxUpdateHandler>();
|
||||
notificationHandlerProducer.RegisterNotificationHandler<NavigateToModsViewHandler>();
|
||||
}
|
||||
|
||||
public override void RegisterMods(IModsProducer modsManager)
|
||||
{
|
||||
modsManager.RegisterMod<IGuildWarsVersionChecker, GuildWarsVersionChecker>();
|
||||
modsManager.RegisterMod<IDaybreakApiService, DaybreakApiService>();
|
||||
modsManager.RegisterMod<IToolboxService, ToolboxService>();
|
||||
modsManager.RegisterMod<IUModService, UModService>();
|
||||
modsManager.RegisterMod<IDirectSongService, DirectSongService>(singleton: true);
|
||||
}
|
||||
|
||||
public override void RegisterLaunchArgumentHandlers(
|
||||
IArgumentHandlerProducer argumentHandlerProducer
|
||||
)
|
||||
{
|
||||
argumentHandlerProducer.ThrowIfNull();
|
||||
argumentHandlerProducer.RegisterArgumentHandler<AutoLaunchArgumentHandler>();
|
||||
argumentHandlerProducer.RegisterArgumentHandler<ResetWindowPositionArgumentHandler>();
|
||||
}
|
||||
|
||||
public override void RegisterMenuButtons(IMenuServiceProducer menuServiceProducer)
|
||||
{
|
||||
menuServiceProducer.ThrowIfNull();
|
||||
menuServiceProducer
|
||||
.CreateIfNotExistCategory("Guild Wars")
|
||||
.RegisterButton(
|
||||
"Game Companion",
|
||||
"Open game companion",
|
||||
sp => sp.GetRequiredService<IViewManager>().ShowView<LaunchView>()
|
||||
)
|
||||
.RegisterButton(
|
||||
"Manage Builds",
|
||||
"Open builds manager",
|
||||
sp => sp.GetRequiredService<IViewManager>().ShowView<BuildListView>()
|
||||
)
|
||||
.RegisterButton(
|
||||
"Manage Mods",
|
||||
"Open Guild Wars mods manager",
|
||||
sp => sp.GetRequiredService<IViewManager>().ShowView<ModsView>()
|
||||
)
|
||||
.RegisterButton(
|
||||
"Download Guild Wars",
|
||||
"Download Guild Wars installer",
|
||||
sp => sp.GetRequiredService<IViewManager>().ShowView<GuildWarsDownloadView>()
|
||||
)
|
||||
.RegisterButton(
|
||||
"Copy Guild Wars",
|
||||
"Copy Guild Wars from an existing installation",
|
||||
sp => sp.GetRequiredService<IViewManager>().ShowView<GuildWarsCopySelectionView>()
|
||||
);
|
||||
menuServiceProducer
|
||||
.CreateIfNotExistCategory("Daybreak")
|
||||
//TODO: Implement Notifications view
|
||||
//.RegisterButton("Notifications", "Open notifications view", sp => { })
|
||||
.RegisterButton(
|
||||
"Manage Plugins",
|
||||
"Open plugins view",
|
||||
sp => sp.GetRequiredService<IViewManager>().ShowView<PluginsView>()
|
||||
)
|
||||
.RegisterButton(
|
||||
"Manage version",
|
||||
"Open version manager",
|
||||
sp => sp.GetRequiredService<IViewManager>().ShowView<VersionManagementView>()
|
||||
)
|
||||
.RegisterButton(
|
||||
"Help",
|
||||
"Open Daybreak wiki",
|
||||
sp =>
|
||||
sp.GetRequiredService<IViewManager>()
|
||||
.ShowView<WikiView>((nameof(WikiView.Page), "Home"))
|
||||
);
|
||||
menuServiceProducer
|
||||
.CreateIfNotExistCategory("Services")
|
||||
.RegisterButton(
|
||||
"Alerts",
|
||||
"Open trade alerts manager",
|
||||
sp => sp.GetRequiredService<IViewManager>().ShowView<TradeAlertsView>()
|
||||
)
|
||||
.RegisterButton(
|
||||
"Kamadan",
|
||||
"Open kamadan trade chat",
|
||||
sp =>
|
||||
sp.GetRequiredService<IViewManager>()
|
||||
.ShowView<TradeChatView>(
|
||||
(nameof(TradeChatView.Source), nameof(TraderSource.Kamadan))
|
||||
)
|
||||
)
|
||||
.RegisterButton(
|
||||
"Ascalon",
|
||||
"Open ascalon trade chat",
|
||||
sp =>
|
||||
sp.GetRequiredService<IViewManager>()
|
||||
.ShowView<TradeChatView>(
|
||||
(nameof(TradeChatView.Source), nameof(TraderSource.Ascalon))
|
||||
)
|
||||
)
|
||||
.RegisterButton(
|
||||
"Guild Wars Party Search",
|
||||
"Show party search broadcasts",
|
||||
sp => sp.GetRequiredService<IViewManager>().ShowView<GuildWarsPartySearchView>()
|
||||
)
|
||||
.RegisterButton(
|
||||
"Guild Wars Marketplace",
|
||||
"Show guild wars marketplace",
|
||||
sp => sp.GetRequiredService<IViewManager>().ShowView<GuildWarsMarketView>()
|
||||
)
|
||||
.RegisterButton(
|
||||
"Event Calendar",
|
||||
"Show current and upcoming events",
|
||||
sp => sp.GetRequiredService<IViewManager>().ShowView<EventCalendarView>()
|
||||
);
|
||||
menuServiceProducer
|
||||
.CreateIfNotExistCategory("Settings")
|
||||
.RegisterButton(
|
||||
"Accounts",
|
||||
"Accounts Settings",
|
||||
sp => sp.GetRequiredService<IViewManager>().ShowView<AccountsView>()
|
||||
)
|
||||
.RegisterButton(
|
||||
"Executables",
|
||||
"Executables Settings",
|
||||
sp => sp.GetRequiredService<IViewManager>().ShowView<ExecutablesView>()
|
||||
)
|
||||
.RegisterButton(
|
||||
"Launch configurations",
|
||||
"Launch configurations settings",
|
||||
sp => sp.GetRequiredService<IViewManager>().ShowView<LaunchConfigurationsView>()
|
||||
);
|
||||
menuServiceProducer
|
||||
.CreateIfNotExistCategory("Diagnostics")
|
||||
.RegisterButton(
|
||||
"Telemetry",
|
||||
"Open telemetry view",
|
||||
sp => sp.GetRequiredService<IViewManager>().ShowView<TelemetryView>()
|
||||
)
|
||||
.RegisterButton(
|
||||
"Logs",
|
||||
"Open logs view",
|
||||
sp => sp.GetRequiredService<IViewManager>().ShowView<LogsView>()
|
||||
);
|
||||
}
|
||||
|
||||
public override void RegisterThemes(IThemeProducer themeProducer)
|
||||
{
|
||||
themeProducer.ThrowIfNull();
|
||||
var themes = CoreThemes.Themes.Concat(HeroThemes.Themes).Concat(LiveThemes.Themes);
|
||||
foreach (var theme in themes)
|
||||
{
|
||||
themeProducer.RegisterTheme(theme);
|
||||
}
|
||||
}
|
||||
|
||||
public override void RegisterProviderAssemblies(IFileProviderProducer fileProviderProducer)
|
||||
{
|
||||
fileProviderProducer.RegisterAssembly(typeof(ProjectConfiguration).Assembly);
|
||||
}
|
||||
|
||||
private static IServiceCollection RegisterHttpClients(IServiceCollection services)
|
||||
{
|
||||
return services
|
||||
.ThrowIfNull()
|
||||
.RegisterHttpClient<DownloadService>()
|
||||
.WithMessageHandler(SetupLoggingAndMetrics<DownloadService>)
|
||||
.WithDefaultRequestHeadersSetup(SetupDaybreakUserAgent)
|
||||
.Build()
|
||||
.RegisterHttpClient<TradeChatService<KamadanTradeChatOptions>>()
|
||||
.WithMessageHandler(SetupLoggingAndMetrics<TradeChatService<KamadanTradeChatOptions>>)
|
||||
.WithDefaultRequestHeadersSetup(SetupDaybreakUserAgent)
|
||||
.Build()
|
||||
.RegisterHttpClient<TradeChatService<AscalonTradeChatOptions>>()
|
||||
.WithMessageHandler(SetupLoggingAndMetrics<TradeChatService<AscalonTradeChatOptions>>)
|
||||
.WithDefaultRequestHeadersSetup(SetupDaybreakUserAgent)
|
||||
.Build()
|
||||
.RegisterHttpClient<TraderQuoteService>()
|
||||
.WithMessageHandler(SetupLoggingAndMetrics<TraderQuoteService>)
|
||||
.WithDefaultRequestHeadersSetup(SetupDaybreakUserAgent)
|
||||
.Build()
|
||||
.RegisterHttpClient<InternetCheckingService>()
|
||||
.WithMessageHandler(SetupLoggingAndMetrics<InternetCheckingService>)
|
||||
.WithDefaultRequestHeadersSetup(SetupDaybreakUserAgent)
|
||||
.Build()
|
||||
.RegisterHttpClient<GithubClient>()
|
||||
.WithMessageHandler(SetupLoggingAndMetrics<GithubClient>)
|
||||
.WithDefaultRequestHeadersSetup(SetupDaybreakUserAgent)
|
||||
.Build()
|
||||
.RegisterHttpClient<ScopedApiContext>()
|
||||
.WithMessageHandler(SetupLoggingAndMetrics<ScopedApiContext>)
|
||||
.WithDefaultRequestHeadersSetup(SetupDaybreakUserAgent)
|
||||
.Build()
|
||||
.RegisterHttpClient<WikiService>()
|
||||
.WithMessageHandler(SetupLoggingAndMetrics<WikiService>)
|
||||
.WithDefaultRequestHeadersSetup(SetupDaybreakUserAgent)
|
||||
.Build()
|
||||
.RegisterHttpClient<ReShadeService>()
|
||||
.WithMessageHandler(SetupLoggingAndMetrics<ReShadeService>)
|
||||
.WithDefaultRequestHeadersSetup(SetupDaybreakUserAgent)
|
||||
.Build()
|
||||
.RegisterHttpClient<GraphClient>()
|
||||
.WithMessageHandler(SetupLoggingAndMetrics<GraphClient>)
|
||||
.WithDefaultRequestHeadersSetup(SetupDaybreakUserAgent)
|
||||
.Build();
|
||||
}
|
||||
}
|
||||
@@ -4,8 +4,9 @@ public class SecretKeys
|
||||
{
|
||||
public static readonly SecretKeys AadApplicationId = new() { Key = "AadApplicationId" };
|
||||
public static readonly SecretKeys AadTenantId = new() { Key = "AadTenantId" };
|
||||
public static readonly SecretKeys ApmApiKey = new() { Key = "ApmApiKey" };
|
||||
public static readonly SecretKeys ApmUri = new() { Key = "ApmUri" };
|
||||
public static readonly SecretKeys ApmServiceAccount = new() { Key = "ApmServiceAccount" };
|
||||
public static readonly SecretKeys ApmServiceKey = new() { Key = "ApmServiceKey" };
|
||||
|
||||
public string Key { get; private set; } = string.Empty;
|
||||
private SecretKeys()
|
||||
@@ -0,0 +1,45 @@
|
||||
using System.Extensions;
|
||||
using System.Reflection;
|
||||
using System.Text.Json;
|
||||
|
||||
namespace Daybreak.Configuration;
|
||||
|
||||
public static class SecretManager
|
||||
{
|
||||
private static JsonDocument? SecretsHolder;
|
||||
|
||||
public static string GetSecret(SecretKeys secretKey)
|
||||
{
|
||||
SecretsHolder ??= LoadSecrets();
|
||||
if (SecretsHolder.RootElement.TryGetProperty(secretKey.Key, out var element))
|
||||
{
|
||||
return element.GetString() ?? string.Empty;
|
||||
}
|
||||
|
||||
return string.Empty;
|
||||
}
|
||||
|
||||
private static JsonDocument LoadSecrets()
|
||||
{
|
||||
try
|
||||
{
|
||||
var serializedSecrets = Assembly.GetExecutingAssembly().GetManifestResourceStream("Daybreak.secrets.json")?.ReadAllBytes().GetString() ?? throw new InvalidOperationException("Could not load Daybreak.secrets.json from assembly");
|
||||
serializedSecrets = TrimUnwantedCharacters(serializedSecrets);
|
||||
return JsonDocument.Parse(serializedSecrets);
|
||||
}
|
||||
catch
|
||||
{
|
||||
return JsonDocument.Parse("{}");
|
||||
}
|
||||
}
|
||||
|
||||
private static string TrimUnwantedCharacters(string s)
|
||||
{
|
||||
return new string([.. s.Where(c =>
|
||||
char.IsWhiteSpace(c) ||
|
||||
char.IsLetterOrDigit(c) ||
|
||||
char.IsSymbol(c) ||
|
||||
char.IsSeparator(c) ||
|
||||
char.IsPunctuation(c))]);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,111 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk.Razor">
|
||||
|
||||
<PropertyGroup>
|
||||
<RootNamespace>Daybreak</RootNamespace>
|
||||
<TargetFrameworks>$(TargetFrameworkVersion);$(TargetFrameworkVersionWindows)</TargetFrameworks>
|
||||
<ScopedCssEnabled>true</ScopedCssEnabled>
|
||||
<Nullable>enable</Nullable>
|
||||
<Platforms>x64</Platforms>
|
||||
<CopyLocalLockFileAssemblies>true</CopyLocalLockFileAssemblies>
|
||||
<LangVersion>preview</LangVersion>
|
||||
<EnableWindowsTargeting>true</EnableWindowsTargeting>
|
||||
<UserSecretsId>cfb2a489-db80-448d-a969-80270f314c46</UserSecretsId>
|
||||
<SatelliteResourceLanguages>en</SatelliteResourceLanguages>
|
||||
</PropertyGroup>
|
||||
|
||||
<!-- Define WINDOWS constant when targeting Windows -->
|
||||
<PropertyGroup Condition="$(TargetFramework.Contains('windows'))">
|
||||
<DefineConstants>$(DefineConstants);WINDOWS</DefineConstants>
|
||||
</PropertyGroup>
|
||||
|
||||
<PropertyGroup Condition="'$(Configuration)' == 'Release'">
|
||||
<StripSymbols>true</StripSymbols>
|
||||
<DebugType>none</DebugType>
|
||||
<DebugSymbols>false</DebugSymbols>
|
||||
</PropertyGroup>
|
||||
|
||||
<Target Name="AddUserSecrets" BeforeTargets="PrepareForBuild" Condition=" '$(UserSecretsId)' != '' ">
|
||||
<PropertyGroup>
|
||||
<UserSecretsFilePath Condition=" '$(OS)' == 'Windows_NT' ">
|
||||
$([System.Environment]::GetFolderPath(SpecialFolder.UserProfile))\AppData\Roaming\Microsoft\UserSecrets\$(UserSecretsId)\secrets.json
|
||||
</UserSecretsFilePath>
|
||||
<UserSecretsFilePath Condition=" '$(OS)' == 'Unix' ">
|
||||
$([System.Environment]::GetFolderPath(SpecialFolder.UserProfile))/.microsoft/usersecrets/$(UserSecretsId)/secrets.json
|
||||
</UserSecretsFilePath>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<EmbeddedResource Include="$(UserSecretsFilePath)" Condition="Exists($(UserSecretsFilePath))" />
|
||||
</ItemGroup>
|
||||
</Target>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Elastic.OpenTelemetry" />
|
||||
<PackageReference Include="Microsoft.Extensions.FileProviders.Embedded" />
|
||||
<PackageReference Include="Microsoft.FluentUI.AspNetCore.Components" />
|
||||
<PackageReference Include="Microsoft.FluentUI.AspNetCore.Components.Icons" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.Components" />
|
||||
<PackageReference Include="Microsoft.Identity.Client" />
|
||||
<PackageReference Include="OpenTelemetry.Exporter.Console" />
|
||||
<PackageReference Include="OpenTelemetry.Instrumentation.Http" />
|
||||
<PackageReference Include="OpenTelemetry.Instrumentation.Runtime" />
|
||||
<PackageReference Include="PeNet" />
|
||||
<PackageReference Include="Plumsy" />
|
||||
<PackageReference Include="DiffPlex" />
|
||||
<PackageReference Include="HtmlAgilityPack" />
|
||||
<PackageReference Include="ini-parser-netstandard" />
|
||||
<PackageReference Include="MegaApiClient" />
|
||||
<PackageReference Include="Microsoft.CorrelationVector" />
|
||||
<PackageReference Include="Microsoft.Extensions.Configuration.UserSecrets" />
|
||||
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" />
|
||||
<PackageReference Include="Newtonsoft.Json" />
|
||||
<PackageReference Include="Serilog" />
|
||||
<PackageReference Include="Serilog.Enrichers.Environment" />
|
||||
<PackageReference Include="Serilog.Enrichers.Thread" />
|
||||
<PackageReference Include="Serilog.Extensions.Logging" />
|
||||
<PackageReference Include="Serilog.Sinks.Console" />
|
||||
<PackageReference Include="Serilog.Sinks.Debug" />
|
||||
<PackageReference Include="SharpCompress" />
|
||||
<PackageReference Include="SixLabors.ImageSharp" />
|
||||
<PackageReference Include="System.Security.Cryptography.ProtectedData" />
|
||||
</ItemGroup>
|
||||
|
||||
<!-- Windows-only packages -->
|
||||
<ItemGroup Condition="$(TargetFramework.Contains('windows'))">
|
||||
<PackageReference Include="securifybv.ShellLink" />
|
||||
<PackageReference Include="System.Drawing.Common" />
|
||||
<!-- Exclude WPF assets from WebView2 to avoid WindowsBase version conflict (MSB3277) -->
|
||||
<PackageReference Include="Microsoft.Web.WebView2" ExcludeAssets="buildTransitive" PrivateAssets="all">
|
||||
<IncludeAssets>runtime; compile; native; contentfiles; analyzers</IncludeAssets>
|
||||
</PackageReference>
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\Daybreak.Shared\Daybreak.Shared.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
<!-- Embed Daybreak.wiki files as embedded resources with web-friendly logical names -->
|
||||
<PropertyGroup>
|
||||
<WikiSourcePath>..\Daybreak.wiki</WikiSourcePath>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<EmbeddedResource Include="$(WikiSourcePath)\**\*.md" Exclude="$(WikiSourcePath)\.git\**">
|
||||
<LogicalName>wiki/%(RecursiveDir)%(Filename)%(Extension)</LogicalName>
|
||||
</EmbeddedResource>
|
||||
<EmbeddedResource Include="$(WikiSourcePath)\**\*.png" Exclude="$(WikiSourcePath)\.git\**">
|
||||
<LogicalName>wiki/%(RecursiveDir)%(Filename)%(Extension)</LogicalName>
|
||||
</EmbeddedResource>
|
||||
<EmbeddedResource Include="$(WikiSourcePath)\**\*.jpg" Exclude="$(WikiSourcePath)\.git\**">
|
||||
<LogicalName>wiki/%(RecursiveDir)%(Filename)%(Extension)</LogicalName>
|
||||
</EmbeddedResource>
|
||||
<EmbeddedResource Include="$(WikiSourcePath)\**\*.gif" Exclude="$(WikiSourcePath)\.git\**">
|
||||
<LogicalName>wiki/%(RecursiveDir)%(Filename)%(Extension)</LogicalName>
|
||||
</EmbeddedResource>
|
||||
</ItemGroup>
|
||||
|
||||
<Target Name="PostBuild" AfterTargets="PostBuildEvent" Condition="'$(Configuration)' == 'Release'">
|
||||
<!-- Cross-platform: use Touch task instead of Windows-only echo.> -->
|
||||
<Touch Files="$(Version).version" AlwaysCreate="true" />
|
||||
</Target>
|
||||
|
||||
</Project>
|
||||
@@ -0,0 +1,60 @@
|
||||
using Daybreak.Models;
|
||||
using Daybreak.Services.Options;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Hosting;
|
||||
using Microsoft.Extensions.Options;
|
||||
using System.Extensions;
|
||||
|
||||
namespace Daybreak.Extensions;
|
||||
|
||||
public static class ServiceCollectionExtensions
|
||||
{
|
||||
public static IServiceCollection AddDaybreakOptions<TOptions>(this IServiceCollection services)
|
||||
where TOptions : class, new()
|
||||
{
|
||||
services.AddSingleton(new OptionEntry(typeof(TOptions)));
|
||||
services.AddSingleton<IOptionsFactory<TOptions>>(sp =>
|
||||
new DaybreakOptionsFactory<TOptions>(sp.GetRequiredService<OptionsManager>()));
|
||||
|
||||
services.AddSingleton<IOptionsChangeTokenSource<TOptions>>(sp =>
|
||||
new DaybreakOptionsChangeTokenSource<TOptions>(sp.GetRequiredService<OptionsManager>()));
|
||||
|
||||
services.AddSingleton<IOptionsMonitorCache<TOptions>, OptionsCache<TOptions>>();
|
||||
|
||||
services.AddSingleton<IOptionsMonitor<TOptions>>(sp =>
|
||||
{
|
||||
var factory = sp.GetRequiredService<IOptionsFactory<TOptions>>();
|
||||
var sources = sp.GetServices<IOptionsChangeTokenSource<TOptions>>();
|
||||
var cache = sp.GetRequiredService<IOptionsMonitorCache<TOptions>>();
|
||||
var monitor = new OptionsMonitor<TOptions>(factory, sources, cache);
|
||||
monitor.OnChange((_, name) => cache.TryRemove(name ?? Options.DefaultName));
|
||||
return new OptionsMonitor<TOptions>(factory, sources, cache);
|
||||
});
|
||||
|
||||
services.AddSingleton<IOptions<TOptions>>(sp =>
|
||||
new OptionsWrapper<TOptions>(sp.GetRequiredService<IOptionsMonitor<TOptions>>().CurrentValue));
|
||||
|
||||
services.AddScoped<IOptionsSnapshot<TOptions>>(sp =>
|
||||
new OptionsManager<TOptions>(
|
||||
sp.GetRequiredService<IOptionsFactory<TOptions>>()));
|
||||
|
||||
return services;
|
||||
}
|
||||
|
||||
public static IServiceCollection AddHostedSingleton<TInterface, TImplementation>(this IServiceCollection services)
|
||||
where TImplementation : class, TInterface, IHostedService
|
||||
where TInterface : class
|
||||
{
|
||||
services.AddSingleton<TInterface, TImplementation>();
|
||||
services.AddHostedService(sp => sp.GetRequiredService<TInterface>().Cast<TImplementation>());
|
||||
return services;
|
||||
}
|
||||
|
||||
public static IServiceCollection AddHostedSingleton<TImplementation>(this IServiceCollection services)
|
||||
where TImplementation : class, IHostedService
|
||||
{
|
||||
services.AddSingleton<TImplementation>();
|
||||
services.AddHostedService(sp => sp.GetRequiredService<TImplementation>());
|
||||
return services;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
using System.Runtime.CompilerServices;
|
||||
|
||||
[assembly: InternalsVisibleTo("Daybreak")]
|
||||
@@ -0,0 +1,51 @@
|
||||
using Daybreak.Configuration.Options;
|
||||
using Daybreak.Extensions;
|
||||
using Daybreak.Services.Options;
|
||||
using Daybreak.Services.Plugins;
|
||||
using Daybreak.Services.Themes;
|
||||
using Daybreak.Shared.Services.Options;
|
||||
using Daybreak.Shared.Services.Plugins;
|
||||
using Daybreak.Shared.Services.Themes;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using System.Extensions;
|
||||
|
||||
namespace Daybreak.Launch;
|
||||
|
||||
public partial class Launcher
|
||||
{
|
||||
public static ServiceProvider SetupBootstrap()
|
||||
{
|
||||
var serviceCollection = new ServiceCollection();
|
||||
SetupLogging(serviceCollection);
|
||||
BootstrapOptionsManager(serviceCollection);
|
||||
BootstrapThemeOptions(serviceCollection);
|
||||
BootstrapScreenManagerOptions(serviceCollection);
|
||||
BootstrapPluginsService(serviceCollection);
|
||||
return serviceCollection.BuildServiceProvider();
|
||||
}
|
||||
|
||||
private static void BootstrapPluginsService(IServiceCollection services)
|
||||
{
|
||||
services.AddDaybreakOptions<PluginsServiceOptions>();
|
||||
services.AddSingleton<IPluginsService, PluginsService>();
|
||||
}
|
||||
|
||||
private static void BootstrapOptionsManager(IServiceCollection services)
|
||||
{
|
||||
services.AddSingleton<OptionsManager>();
|
||||
services.AddSingleton<IOptionsProvider>(sp => sp.GetRequiredService<OptionsManager>());
|
||||
}
|
||||
|
||||
private static void BootstrapThemeOptions(IServiceCollection services)
|
||||
{
|
||||
services.AddDaybreakOptions<ThemeOptions>();
|
||||
services.AddSingleton<IThemeManager, BlazorThemeInteropService>();
|
||||
services.AddHostedService(sp => sp.GetRequiredService<IThemeManager>().Cast<BlazorThemeInteropService>());
|
||||
}
|
||||
|
||||
private static void BootstrapScreenManagerOptions(IServiceCollection services)
|
||||
{
|
||||
// Only register options here - actual ScreenManager is registered by platform-specific configuration
|
||||
services.AddDaybreakOptions<ScreenManagerOptions>();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,92 @@
|
||||
using System.Extensions.Core;
|
||||
using Daybreak.Shared.Services.Screens;
|
||||
using Daybreak.Views;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Photino.Blazor;
|
||||
|
||||
namespace Daybreak.Launch;
|
||||
|
||||
public partial class Launcher
|
||||
{
|
||||
#if DEBUG
|
||||
public const bool IsDebug = true;
|
||||
#else
|
||||
public const bool IsDebug = false;
|
||||
#endif
|
||||
|
||||
public static PhotinoBlazorApp? MainApp { get; private set; }
|
||||
public static PhotinoBlazorAppBuilder? MainBuilder { get; private set; }
|
||||
|
||||
private static PhotinoBlazorAppBuilder CreateMainBuilder(string[] args)
|
||||
{
|
||||
var mainBuilder = PhotinoBlazorAppBuilder.CreateDefault(args);
|
||||
mainBuilder.RootComponents.Add<App>("#app");
|
||||
mainBuilder.Services.AddBlazorDesktop();
|
||||
SetupLogging(mainBuilder.Services);
|
||||
MainBuilder = mainBuilder;
|
||||
return mainBuilder;
|
||||
}
|
||||
|
||||
private static PhotinoBlazorApp CreateMainApp(
|
||||
PhotinoBlazorAppBuilder mainBuilder,
|
||||
string[] args
|
||||
)
|
||||
{
|
||||
var app = mainBuilder.Build();
|
||||
|
||||
var cts = new CancellationTokenSource();
|
||||
app.MainWindow.SetTitle("Daybreak")
|
||||
.SetContextMenuEnabled(!IsDebug || args.Any(s => string.Equals(s, "--enable-context-menu")))
|
||||
.SetDevToolsEnabled(!IsDebug || args.Any(s => string.Equals(s, "--enable-dev-tools")))
|
||||
.SetSmoothScrollingEnabled(true)
|
||||
.SetChromeless(!args.Any(s => string.Equals(s, "--disable-chromeless")));
|
||||
app.MainWindow.SetLogVerbosity(0);
|
||||
|
||||
app.MainWindow.RegisterWindowCreatedHandler((_, __) => StartHostedServices(app, cts));
|
||||
app.MainWindow.RegisterWindowClosingHandler((_, __) => StopHostedServices(app, cts));
|
||||
|
||||
SetWindowPosition(app);
|
||||
MainApp = app;
|
||||
return app;
|
||||
}
|
||||
|
||||
private static void RunMainApp(PhotinoBlazorApp app)
|
||||
{
|
||||
app.Run();
|
||||
}
|
||||
|
||||
private static void SetWindowPosition(PhotinoBlazorApp app)
|
||||
{
|
||||
var scopedLogger = app
|
||||
.Services.GetRequiredService<ILogger<Launcher>>()
|
||||
.CreateScopedLogger();
|
||||
var screenManager = app.Services.GetRequiredService<IScreenManager>();
|
||||
var savedPosition = screenManager.GetSavedPosition();
|
||||
|
||||
if (savedPosition.Width is 0 || savedPosition.Height is 0)
|
||||
{
|
||||
scopedLogger.LogDebug("No saved window position found, centering the window");
|
||||
var firstScreen = screenManager.Screens.FirstOrDefault();
|
||||
if (firstScreen.Size.IsEmpty)
|
||||
{
|
||||
scopedLogger.LogWarning(
|
||||
"No valid screen found to center the window. Window will spawn with minimal size"
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
app.MainWindow.Center();
|
||||
app.MainWindow.Width = firstScreen.Size.Width / 2;
|
||||
app.MainWindow.Height = firstScreen.Size.Height / 2;
|
||||
}
|
||||
else
|
||||
{
|
||||
scopedLogger.LogDebug("Restoring saved window position");
|
||||
app.MainWindow.Left = savedPosition.X;
|
||||
app.MainWindow.Top = savedPosition.Y;
|
||||
app.MainWindow.Width = savedPosition.Width;
|
||||
app.MainWindow.Height = savedPosition.Height;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,69 @@
|
||||
using Daybreak.Services.Logging;
|
||||
using Daybreak.Services.Telemetry;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Hosting;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Photino.Blazor;
|
||||
using Serilog;
|
||||
using Serilog.Events;
|
||||
using Serilog.Sinks.SystemConsole.Themes;
|
||||
using System.Extensions.Core;
|
||||
|
||||
namespace Daybreak.Launch;
|
||||
|
||||
public partial class Launcher
|
||||
{
|
||||
public const string OutputTemplate = "[{Timestamp:yyyy-MM-dd HH:mm:ss}] {Level:u4}: [{EnvironmentName}] [{ThreadId}:{ThreadName}] [{SourceContext}]{NewLine}{Message:lj}{NewLine}{Exception}";
|
||||
|
||||
public static void SetupLogging(IServiceCollection services)
|
||||
{
|
||||
Log.Logger = new LoggerConfiguration()
|
||||
.MinimumLevel.Debug()
|
||||
.MinimumLevel.Override("Microsoft", LogEventLevel.Warning)
|
||||
.MinimumLevel.Override("Microsoft.AspNetCore", LogEventLevel.Warning)
|
||||
.MinimumLevel.Override("System", LogEventLevel.Warning)
|
||||
.MinimumLevel.Override("Daybreak.Services.TradeChat.TradeChatService", LogEventLevel.Information)
|
||||
.MinimumLevel.Override("Daybreak.Services.Credentials.CredentialManager", LogEventLevel.Information)
|
||||
.MinimumLevel.Override("Daybreak.Shared.Services.BuildTemplates.BuildTemplateManager", LogEventLevel.Information)
|
||||
|
||||
.Enrich.FromLogContext()
|
||||
|
||||
.Enrich.WithThreadId()
|
||||
.Enrich.WithThreadName()
|
||||
|
||||
.Enrich.WithEnvironmentName()
|
||||
|
||||
.WriteTo.Console(
|
||||
outputTemplate: OutputTemplate,
|
||||
theme: AnsiConsoleTheme.Sixteen)
|
||||
.WriteTo.Sink(InMemorySink.Instance)
|
||||
.WriteTo.Sink(TelemetryLogSink.Instance)
|
||||
.CreateLogger();
|
||||
|
||||
services.AddLogging(loggingBuilder =>
|
||||
{
|
||||
loggingBuilder.ClearProviders();
|
||||
loggingBuilder.AddSerilog(dispose: true);
|
||||
});
|
||||
}
|
||||
|
||||
private static void StartHostedServices(PhotinoBlazorApp app, CancellationTokenSource cts)
|
||||
{
|
||||
var hostedServices = app.Services.GetServices<IHostedService>();
|
||||
foreach (var hostedService in hostedServices)
|
||||
{
|
||||
Task.Factory.StartNew(() => hostedService.StartAsync(cts.Token), cts.Token, TaskCreationOptions.LongRunning, TaskScheduler.Current);
|
||||
}
|
||||
}
|
||||
|
||||
private static bool StopHostedServices(PhotinoBlazorApp app, CancellationTokenSource cts)
|
||||
{
|
||||
cts.CancelAfter(TimeSpan.FromSeconds(2));
|
||||
var hostedServices = app.Services.GetServices<IHostedService>();
|
||||
var tasks = hostedServices.Select(s => s.StopAsync(cts.Token));
|
||||
Task.WaitAll(tasks, cts.Token);
|
||||
|
||||
cts.Dispose();
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,266 @@
|
||||
using Daybreak.Configuration;
|
||||
using Daybreak.Services.FileProviders;
|
||||
using Daybreak.Services.Initialization;
|
||||
using Daybreak.Shared.Models.Menu;
|
||||
using Daybreak.Shared.Models.Plugins;
|
||||
using Daybreak.Shared.Models.Themes;
|
||||
using Daybreak.Shared.Services.ApplicationArguments;
|
||||
using Daybreak.Shared.Services.Keyboard;
|
||||
using Daybreak.Shared.Services.Plugins;
|
||||
using Daybreak.Shared.Services.Themes;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Photino.Blazor;
|
||||
using System.Extensions.Core;
|
||||
using System.Logging;
|
||||
using System.Runtime.CompilerServices;
|
||||
|
||||
[assembly: InternalsVisibleTo("Daybreak.Tests")]
|
||||
[assembly: InternalsVisibleTo("DynamicProxyGenAssembly2")]
|
||||
namespace Daybreak.Launch;
|
||||
|
||||
public partial class Launcher
|
||||
{
|
||||
public static void LaunchSequence(string[] args, IServiceProvider bootstrap, PluginConfigurationBase? platformConfiguration = null)
|
||||
{
|
||||
var scopedLogger = bootstrap.GetRequiredService<ILogger<Launcher>>().CreateScopedLogger();
|
||||
scopedLogger.LogDebug("Starting Daybreak Launcher...");
|
||||
var builder = CreateMainBuilder(args);
|
||||
var pluginsService = bootstrap.GetRequiredService<IPluginsService>();
|
||||
var loadedPlugins = pluginsService.LoadPlugins();
|
||||
var configurations = loadedPlugins.Select<AvailablePlugin, (string, PluginConfigurationBase?, AvailablePlugin?)>(p => (p.Name, p.Configuration, (AvailablePlugin?)p))
|
||||
.Prepend(("Daybreak Core", new ProjectConfiguration(), default)).ToList();
|
||||
|
||||
// Add platform-specific configuration if provided
|
||||
if (platformConfiguration is not null)
|
||||
{
|
||||
configurations = configurations.Prepend(("Platform-Specific", platformConfiguration, default)).ToList();
|
||||
}
|
||||
|
||||
var menuEntryProducer = new MenuProducer(bootstrap.GetRequiredService<ILogger<MenuProducer>>());
|
||||
foreach (var (pluginName, configuration, plugin) in configurations)
|
||||
{
|
||||
if (configuration is null)
|
||||
{
|
||||
scopedLogger.LogWarning("No configuration found for plugin {PluginName}, skipping...", pluginName);
|
||||
continue;
|
||||
}
|
||||
|
||||
LoadConfiguration(pluginName, bootstrap, builder, configuration, menuEntryProducer);
|
||||
}
|
||||
|
||||
builder.Services.AddSingleton<IReadOnlyDictionary<string, MenuCategory>>(menuEntryProducer.categories.AsReadOnly());
|
||||
var mainApp = CreateMainApp(builder, args);
|
||||
|
||||
// Allow platform-specific and plugin configurations to customize the window
|
||||
foreach (var (pluginName, configuration, plugin) in configurations)
|
||||
{
|
||||
if (configuration is null)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
configuration.ConfigureWindow(mainApp);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
scopedLogger.LogError(ex, "An error occurred while configuring window for {PluginName}", pluginName);
|
||||
}
|
||||
}
|
||||
|
||||
foreach (var (pluginName, configuration, plugin) in configurations)
|
||||
{
|
||||
if (configuration is null)
|
||||
{
|
||||
scopedLogger.LogWarning("No configuration found for plugin {PluginName}, skipping theme registration...", pluginName);
|
||||
continue;
|
||||
}
|
||||
|
||||
RegisterThemes(bootstrap, configuration, scopedLogger);
|
||||
}
|
||||
|
||||
scopedLogger.LogInformation("Registering loaded plugins");
|
||||
var finalPluginService = mainApp.Services.GetRequiredService<IPluginsService>();
|
||||
finalPluginService.UpdateLoadedPlugins(loadedPlugins);
|
||||
|
||||
var theme = mainApp.Services.GetRequiredService<GameScreenshotsTheme>();
|
||||
Theme.Themes.Add(theme);
|
||||
var keyboardHook = mainApp.Services.GetRequiredService<IKeyboardHookService>();
|
||||
keyboardHook.Start();
|
||||
ExecuteArgumentHandlers(mainApp, args);
|
||||
RunMainApp(mainApp);
|
||||
}
|
||||
|
||||
private static void ExecuteArgumentHandlers(PhotinoBlazorApp app, string[] args)
|
||||
{
|
||||
var applicationArgumentService = app.Services.GetRequiredService<IApplicationArgumentService>();
|
||||
applicationArgumentService.HandleArguments(args);
|
||||
}
|
||||
|
||||
private static void LoadConfiguration(
|
||||
string name,
|
||||
IServiceProvider bootstrap,
|
||||
PhotinoBlazorAppBuilder builder,
|
||||
PluginConfigurationBase configuration,
|
||||
MenuProducer menuProducer)
|
||||
{
|
||||
var scopedLogger = bootstrap.GetRequiredService<ILogger<Launcher>>().CreateScopedLogger();
|
||||
scopedLogger.LogInformation("Loading configuration for {Configuration.Name}", name);
|
||||
|
||||
RegisterServices(builder, configuration, scopedLogger);
|
||||
RegisterOptions(bootstrap, builder, configuration, scopedLogger);
|
||||
RegisterViews(bootstrap, builder, configuration, scopedLogger);
|
||||
RegisterMods(bootstrap, builder, configuration, scopedLogger);
|
||||
RegisterStartupActions(bootstrap, builder, configuration, scopedLogger);
|
||||
RegisterPostUpdateActions(bootstrap, builder, configuration, scopedLogger);
|
||||
RegisterNotificationHandlers(bootstrap, builder, configuration, scopedLogger);
|
||||
RegisterArgumentHandlers(bootstrap, builder, configuration, scopedLogger);
|
||||
RegisterMenuEntries(configuration, menuProducer, scopedLogger);
|
||||
RegisterFileProviders(bootstrap, builder, configuration, scopedLogger);
|
||||
|
||||
scopedLogger.LogInformation("Finished loading configuration for {Configuration.Name}", name);
|
||||
}
|
||||
|
||||
private static void RegisterServices(PhotinoBlazorAppBuilder builder, PluginConfigurationBase configuration, ScopedLogger<Launcher> scopedLogger)
|
||||
{
|
||||
try
|
||||
{
|
||||
configuration.RegisterServices(builder.Services);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
scopedLogger.LogError(ex, "An error occurred while registering services");
|
||||
}
|
||||
}
|
||||
|
||||
private static void RegisterOptions(IServiceProvider bootstrap, PhotinoBlazorAppBuilder builder, PluginConfigurationBase configuration, ScopedLogger<Launcher> scopedLogger)
|
||||
{
|
||||
try
|
||||
{
|
||||
var optionsProducer = new OptionProducer(builder.Services, bootstrap.GetRequiredService<ILogger<OptionProducer>>());
|
||||
configuration.RegisterOptions(optionsProducer);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
scopedLogger.LogError(ex, "An error occurred while registering options");
|
||||
}
|
||||
}
|
||||
|
||||
private static void RegisterViews(IServiceProvider bootstrap, PhotinoBlazorAppBuilder builder, PluginConfigurationBase configuration, ScopedLogger<Launcher> scopedLogger)
|
||||
{
|
||||
try
|
||||
{
|
||||
var viewProducer = new TrailBlazrViewProducer(builder.Services, bootstrap.GetRequiredService<ILogger<TrailBlazrViewProducer>>());
|
||||
configuration.RegisterViews(viewProducer);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
scopedLogger.LogError(ex, "An error occurred while registering views");
|
||||
}
|
||||
}
|
||||
|
||||
private static void RegisterMods(IServiceProvider bootstrap, PhotinoBlazorAppBuilder builder, PluginConfigurationBase configuration, ScopedLogger<Launcher> scopedLogger)
|
||||
{
|
||||
try
|
||||
{
|
||||
var modsProducer = new ModsProducer(builder.Services, bootstrap.GetRequiredService<ILogger<ModsProducer>>());
|
||||
configuration.RegisterMods(modsProducer);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
scopedLogger.LogError(ex, "An error occurred while registering mods");
|
||||
}
|
||||
}
|
||||
|
||||
private static void RegisterStartupActions(IServiceProvider bootstrap, PhotinoBlazorAppBuilder builder, PluginConfigurationBase configuration, ScopedLogger<Launcher> scopedLogger)
|
||||
{
|
||||
try
|
||||
{
|
||||
var startupActionProducer = new StartupActionsProducer(builder.Services, bootstrap.GetRequiredService<ILogger<StartupActionsProducer>>());
|
||||
configuration.RegisterStartupActions(startupActionProducer);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
scopedLogger.LogError(ex, "An error occurred while registering startup actions");
|
||||
}
|
||||
}
|
||||
|
||||
private static void RegisterPostUpdateActions(IServiceProvider bootstrap, PhotinoBlazorAppBuilder builder, PluginConfigurationBase configuration, ScopedLogger<Launcher> scopedLogger)
|
||||
{
|
||||
try
|
||||
{
|
||||
var postUpdateActionProducer = new PostUpdateActionProducer(builder.Services, bootstrap.GetRequiredService<ILogger<PostUpdateActionProducer>>());
|
||||
configuration.RegisterPostUpdateActions(postUpdateActionProducer);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
scopedLogger.LogError(ex, "An error occurred while registering post-update actions");
|
||||
}
|
||||
}
|
||||
|
||||
private static void RegisterNotificationHandlers(IServiceProvider bootstrap, PhotinoBlazorAppBuilder builder, PluginConfigurationBase configuration, ScopedLogger<Launcher> scopedLogger)
|
||||
{
|
||||
try
|
||||
{
|
||||
var notificationHandlerProducer = new NotificationHandlerProducer(builder.Services, bootstrap.GetRequiredService<ILogger<NotificationHandlerProducer>>());
|
||||
configuration.RegisterNotificationHandlers(notificationHandlerProducer);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
scopedLogger.LogError(ex, "An error occurred while registering notification handlers");
|
||||
}
|
||||
}
|
||||
|
||||
private static void RegisterArgumentHandlers(IServiceProvider bootstrap, PhotinoBlazorAppBuilder builder, PluginConfigurationBase configuration, ScopedLogger<Launcher> scopedLogger)
|
||||
{
|
||||
try
|
||||
{
|
||||
var argumentHandlerProducer = new ArgumentHandlerProducer(builder.Services, bootstrap.GetRequiredService<ILogger<ArgumentHandlerProducer>>());
|
||||
configuration.RegisterLaunchArgumentHandlers(argumentHandlerProducer);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
scopedLogger.LogError(ex, "An error occurred while registering argument handlers");
|
||||
}
|
||||
}
|
||||
|
||||
private static void RegisterThemes(IServiceProvider bootstrap, PluginConfigurationBase configuration, ScopedLogger<Launcher> scopedLogger)
|
||||
{
|
||||
try
|
||||
{
|
||||
var themeProducer = new ThemeProducer(bootstrap.GetRequiredService<ILogger<ThemeProducer>>());
|
||||
configuration.RegisterThemes(themeProducer);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
scopedLogger.LogError(ex, "An error occurred while registering themes");
|
||||
}
|
||||
}
|
||||
|
||||
private static void RegisterMenuEntries(PluginConfigurationBase configuration, MenuProducer menuProducer, ScopedLogger<Launcher> scopedLogger)
|
||||
{
|
||||
try
|
||||
{
|
||||
configuration.RegisterMenuButtons(menuProducer);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
scopedLogger.LogError(ex, "An error occurred while registering menu entries");
|
||||
}
|
||||
}
|
||||
|
||||
private static void RegisterFileProviders(IServiceProvider bootstrap, PhotinoBlazorAppBuilder builder, PluginConfigurationBase configuration, ScopedLogger<Launcher> scopedLogger)
|
||||
{
|
||||
try
|
||||
{
|
||||
var fileProviderProducer = new AssemblyFileProviderProducer(builder.Services, bootstrap.GetRequiredService<ILogger<AssemblyFileProviderProducer>>());
|
||||
configuration.RegisterProviderAssemblies(fileProviderProducer);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
scopedLogger.LogError(ex, "An error occurred while registering file providers");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
using Daybreak.Shared.Models.Builds;
|
||||
using Daybreak.Shared.Models.Guildwars;
|
||||
|
||||
namespace Daybreak.Models;
|
||||
public sealed class BuildListEntry
|
||||
{
|
||||
public required IBuildEntry BuildEntry { get; init; }
|
||||
public required Profession? PrimaryProfession { get; init; }
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user