mirror of
https://github.com/gwdevhub/Daybreak.git
synced 2026-09-19 06:45:08 +00:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
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 |
+10
-54
@@ -24,22 +24,23 @@ jobs:
|
||||
|
||||
strategy:
|
||||
matrix:
|
||||
targetplatform: [x86]
|
||||
targetplatform: [x64]
|
||||
|
||||
runs-on: windows-latest
|
||||
|
||||
env:
|
||||
Configuration: Release
|
||||
Solution_Path: Daybreak.sln
|
||||
Solution_Path: Daybreak.slnx
|
||||
Test_Project_Path: Daybreak.Tests\Daybreak.Tests.csproj
|
||||
Wpf_Project_Path: Daybreak\Daybreak.csproj
|
||||
Actions_Allow_Unsecure_Commands: true
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
fetch-depth: 0
|
||||
submodules: recursive
|
||||
|
||||
- name: Get Latest Tag
|
||||
id: getLatestTag
|
||||
@@ -56,9 +57,10 @@ jobs:
|
||||
LatestReleaseTag: ${{steps.getLatestTag.outputs.tag}}
|
||||
|
||||
- name: Install .NET Core
|
||||
uses: actions/setup-dotnet@v4
|
||||
uses: actions/setup-dotnet@v5
|
||||
with:
|
||||
dotnet-version: '9.x'
|
||||
dotnet-version: '10.x'
|
||||
architecture: x64
|
||||
|
||||
- name: Setup MSBuild.exe
|
||||
uses: microsoft/setup-msbuild@v2.0.0
|
||||
@@ -71,9 +73,7 @@ jobs:
|
||||
dotnet user-secrets --project Daybreak\Daybreak.csproj set ApmUri "${{ secrets.ApmUri }}"
|
||||
|
||||
- name: Restore project
|
||||
run: msbuild $env:Solution_Path /t:Restore /p:Configuration=$env:Configuration /p:RuntimeIdentifier=$env:RuntimeIdentifier --property:SolutionDir=$GITHUB_WORKSPACE
|
||||
env:
|
||||
RuntimeIdentifier: win-${{ matrix.targetplatform }}
|
||||
run: msbuild $env:Solution_Path /t:Restore /p:Configuration=$env:Configuration --property:SolutionDir=$GITHUB_WORKSPACE
|
||||
|
||||
- name: Build Daybreak project
|
||||
run: dotnet build Daybreak -c $env:Configuration --property:SolutionDir=$env:GITHUB_WORKSPACE
|
||||
@@ -84,9 +84,7 @@ jobs:
|
||||
echo "::set-env name=Version::$version"
|
||||
|
||||
- name: Create publish launcher files
|
||||
run: dotnet publish .\Daybreak\Daybreak.csproj -c $env:Configuration -r $env:RuntimeIdentifier --property:SolutionDir=$env:GITHUB_WORKSPACE -p:PublishReadyToRun=true -p:PublishSingleFile=false --self-contained true -o .\Publish
|
||||
env:
|
||||
RuntimeIdentifier: win-${{ matrix.targetplatform }}
|
||||
run: dotnet publish .\Daybreak\Daybreak.csproj -c $env:Configuration --property:SolutionDir=$env:GITHUB_WORKSPACE -p:PublishReadyToRun=true -p:PublishSingleFile=false --self-contained true -o .\Publish
|
||||
|
||||
- name: Pack publish files
|
||||
run: |
|
||||
@@ -94,59 +92,17 @@ jobs:
|
||||
.\Scripts\BuildRelease.ps1 -version $env:Version
|
||||
shell: pwsh
|
||||
|
||||
- name: Submit virustotal scan
|
||||
shell: pwsh
|
||||
run: |
|
||||
cd Publish
|
||||
$apiKey = "${{ secrets.VIRUSTOTAL_APIKEY }}"
|
||||
$archiveName = "daybreakv${{ env.Version }}.zip"
|
||||
$uploadUrlResponse = curl https://www.virustotal.com/api/v3/files/upload_url -H x-apikey:"$apiKey"
|
||||
$uploadUrlResponse
|
||||
$uploadUrlJson = $uploadUrlResponse | ConvertFrom-Json
|
||||
$uploadUrl = $uploadUrlJson.data
|
||||
$uploadResponse = curl --request POST --header x-apikey:"$apiKey" --header 'content-type: multipart/form-data' --form file="@$archiveName" --url $uploadUrl
|
||||
$uploadResponse
|
||||
$uploadResponseJson = $uploadResponse | ConvertFrom-Json
|
||||
$resultsUrl = "https://www.virustotal.com/api/v3/analyses/$($uploadResponseJson.data.id)"
|
||||
$resultsUrl
|
||||
# Poll the results until the status is "completed"
|
||||
$status = ""
|
||||
while ($status -ne "completed") {
|
||||
Start-Sleep -Seconds 10 # Wait for 10 seconds between checks
|
||||
$results = curl --header x-apikey:"$apiKey" --url $resultsUrl
|
||||
$results
|
||||
$resultsJson = $results | ConvertFrom-Json
|
||||
$status = $resultsJson.data.attributes.status
|
||||
Write-Host "Current status: $status"
|
||||
}
|
||||
|
||||
$resultsJson.data.attributes.results | ConvertTo-Json | Out-File -FilePath "VirusTotal.Results.json" -Encoding UTF8
|
||||
$resultsJson.data.attributes.stats | ConvertTo-Json | Out-File -FilePath "VirusTotal.Stats.json" -Encoding UTF8
|
||||
$stats = $resultsJson.data.attributes.stats
|
||||
echo "::set-env name=VirusTotalStats::$stats"
|
||||
|
||||
- name: Publish blob files
|
||||
run: |
|
||||
Write-Host $env
|
||||
.\Scripts\PushFilesToBlobStorage.ps1 -version $env:Version -sourcePath .\Publish\ -connectionString "${{ secrets.BLOBSTORAGE_CONNECTIONSTRING }}"
|
||||
shell: pwsh
|
||||
|
||||
- name: Create release draft
|
||||
uses: Xotl/cool-github-releases@v1.1.10
|
||||
with:
|
||||
mode: update
|
||||
tag_name: v${{ env.Version }}
|
||||
release_name: Daybreak v${{ env.Version }}
|
||||
assets: .\Publish\daybreakv${{ env.Version }}.zip;.\Publish\VirusTotal.Results.json;.\Publish\VirusTotal.Stats.json
|
||||
assets: .\Publish\daybreakv${{ env.Version }}.zip
|
||||
github_token: ${{ env.GITHUB_TOKEN }}
|
||||
replace_assets: true
|
||||
body_mrkdwn: |
|
||||
${{ env.Changelog }}
|
||||
|
||||
** VirustTotal Stats: **
|
||||
```
|
||||
${{ env.VirusTotalStats }}
|
||||
```
|
||||
isDraft: true
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
@@ -6,9 +6,6 @@
|
||||
name: Daybreak CI Pipeline
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
pull_request:
|
||||
branches:
|
||||
- master
|
||||
@@ -19,33 +16,33 @@ jobs:
|
||||
|
||||
strategy:
|
||||
matrix:
|
||||
targetplatform: [x86]
|
||||
targetplatform: [x64]
|
||||
|
||||
runs-on: windows-latest
|
||||
|
||||
env:
|
||||
Solution_Path: Daybreak.sln
|
||||
Solution_Path: Daybreak.slnx
|
||||
Test_Project_Path: Daybreak.Tests\Daybreak.Tests.csproj
|
||||
Wpf_Project_Path: Daybreak\Daybreak.csproj
|
||||
Actions_Allow_Unsecure_Commands: true
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
fetch-depth: 0
|
||||
submodules: recursive
|
||||
|
||||
- name: Install .NET Core
|
||||
uses: actions/setup-dotnet@v4
|
||||
uses: actions/setup-dotnet@v5
|
||||
with:
|
||||
dotnet-version: '9.x'
|
||||
architecture: x86
|
||||
dotnet-version: '10.x'
|
||||
architecture: x64
|
||||
|
||||
- name: Setup MSBuild.exe
|
||||
uses: microsoft/setup-msbuild@v2.0.0
|
||||
|
||||
- name: Restore the Wpf application to populate the obj folder
|
||||
run: msbuild $env:Solution_Path /t:Restore /p:Configuration=$env:Configuration /p:RuntimeIdentifier=$env:RuntimeIdentifier --property:SolutionDir=$env:GITHUB_WORKSPACE
|
||||
run: msbuild $env:Solution_Path /t:Restore /p:Configuration=$env:Configuration --property:SolutionDir=$env:GITHUB_WORKSPACE
|
||||
env:
|
||||
Configuration: Debug
|
||||
RuntimeIdentifier: win-${{ matrix.targetplatform }}
|
||||
Configuration: Debug
|
||||
@@ -12,10 +12,7 @@
|
||||
name: "CodeQL"
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [ "master" ]
|
||||
pull_request:
|
||||
# The branches below must be a subset of the branches above
|
||||
branches: [ "master" ]
|
||||
schedule:
|
||||
- cron: '41 12 * * 6'
|
||||
@@ -38,14 +35,14 @@ jobs:
|
||||
|
||||
env:
|
||||
Configuration: Release
|
||||
Solution_Path: Daybreak.sln
|
||||
Solution_Path: Daybreak.slnx
|
||||
Test_Project_Path: Daybreak.Tests\Daybreak.Tests.csproj
|
||||
Wpf_Project_Path: Daybreak\Daybreak.csproj
|
||||
Actions_Allow_Unsecure_Commands: true
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
@@ -66,9 +63,9 @@ jobs:
|
||||
# If this step fails, then you should remove it and run the build manually (see below)
|
||||
|
||||
- name: Install .NET Core
|
||||
uses: actions/setup-dotnet@v4
|
||||
uses: actions/setup-dotnet@v5
|
||||
with:
|
||||
dotnet-version: '9.x'
|
||||
dotnet-version: '10.x'
|
||||
|
||||
#- name: Autobuild
|
||||
# uses: github/codeql-action/autobuild@v2
|
||||
|
||||
@@ -0,0 +1,47 @@
|
||||
name: Label Merged Issues
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
types: [closed]
|
||||
branches:
|
||||
- 'release/**'
|
||||
|
||||
jobs:
|
||||
label-issues:
|
||||
if: github.event.pull_request.merged == true
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Label linked issues as merged
|
||||
uses: actions/github-script@v7
|
||||
with:
|
||||
github-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
script: |
|
||||
const prBody = context.payload.pull_request.body || '';
|
||||
const prTitle = context.payload.pull_request.title || '';
|
||||
|
||||
// Match issue references like #123, fixes #123, closes #123, resolves #123
|
||||
const issuePattern = /(?:close[sd]?|fix(?:e[sd])?|resolve[sd]?)?[\s:]*#(\d+)/gi;
|
||||
const issueNumbers = new Set();
|
||||
|
||||
// Search in PR body and title
|
||||
const textToSearch = `${prTitle} ${prBody}`;
|
||||
let match;
|
||||
while ((match = issuePattern.exec(textToSearch)) !== null) {
|
||||
issueNumbers.add(parseInt(match[1]));
|
||||
}
|
||||
|
||||
// Apply 'merged' label to each linked issue
|
||||
for (const issueNumber of issueNumbers) {
|
||||
try {
|
||||
await github.rest.issues.addLabels({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
issue_number: issueNumber,
|
||||
labels: ['merged']
|
||||
});
|
||||
console.log(`Added 'merged' label to issue #${issueNumber}`);
|
||||
} catch (error) {
|
||||
console.log(`Failed to label issue #${issueNumber}: ${error.message}`);
|
||||
}
|
||||
}
|
||||
@@ -20,17 +20,17 @@ jobs:
|
||||
|
||||
env:
|
||||
Configuration: Release
|
||||
Solution_Path: Daybreak.sln
|
||||
Solution_Path: Daybreak.slnx
|
||||
Actions_Allow_Unsecure_Commands: true
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Install .NET Core
|
||||
uses: actions/setup-dotnet@v4
|
||||
uses: actions/setup-dotnet@v5
|
||||
with:
|
||||
dotnet-version: '9.x'
|
||||
|
||||
|
||||
@@ -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
|
||||
@@ -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,7 +1,7 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk.Web">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net9.0-windows</TargetFramework>
|
||||
<TargetFramework>$(TargetFrameworkVersionWindows)</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
<OutputType>Library</OutputType>
|
||||
@@ -18,8 +18,22 @@
|
||||
<EnableIISSupport>false</EnableIISSupport>
|
||||
<InvariantGlobalization>true</InvariantGlobalization>
|
||||
<Platforms>x86</Platforms>
|
||||
|
||||
<!-- Prevent static web asset conflicts -->
|
||||
<StaticWebAssetProjectMode>Default</StaticWebAssetProjectMode>
|
||||
<GenerateStaticWebAssetsManifest>false</GenerateStaticWebAssetsManifest>
|
||||
<ResolveStaticWebAssetsInputsDependsOn>RemoveDuplicateStaticWebAssets;$(ResolveStaticWebAssetsInputsDependsOn)</ResolveStaticWebAssetsInputsDependsOn>
|
||||
</PropertyGroup>
|
||||
|
||||
<!-- Remove duplicate static web assets coming from referenced projects -->
|
||||
<Target Name="RemoveDuplicateStaticWebAssets" BeforeTargets="ResolveStaticWebAssetsInputs;GenerateStaticWebAssetsManifest">
|
||||
<ItemGroup>
|
||||
<!-- Remove all blazor webview static assets since this is a Web API project, not a Blazor Hybrid app -->
|
||||
<StaticWebAsset Remove="@(StaticWebAsset)"
|
||||
Condition="$([System.String]::new('%(RelativePath)').Contains('blazor.modules')) Or $([System.String]::new('%(RelativePath)').Contains('blazor.webview'))" />
|
||||
</ItemGroup>
|
||||
</Target>
|
||||
|
||||
<PropertyGroup>
|
||||
<!--Ignore trim warnings. NativeAOT support is still experimental so it should be tested with a Publish build generated from Daybreak build-->
|
||||
<NoWarn>IL2104</NoWarn>
|
||||
@@ -53,7 +67,6 @@
|
||||
<PackageReference Include="Swashbuckle.AspNetCore.Swagger" />
|
||||
<PackageReference Include="Swashbuckle.AspNetCore.SwaggerGen" />
|
||||
<PackageReference Include="Swashbuckle.AspNetCore.SwaggerUI" />
|
||||
<PackageReference Include="System.Private.Uri" />
|
||||
<PackageReference Include="SystemExtensions.NetStandard.Generators" PrivateAssets="all" />
|
||||
<PackageReference Include="ZLinq" />
|
||||
</ItemGroup>
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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 = 0x58, // Used to query selected index in character selector
|
||||
WriteToChatLog = 0x10000000 | 0x7F, // wparam = UIPacket::kWriteToChatLog*. Triggered by the game when it wants to add a new message to chat.
|
||||
WriteToChatLogWithSender = 0x10000000 | 0x80, // wparam = UIPacket::kWriteToChatLogWithSender*. Triggered by the game when it wants to add a new message to chat.
|
||||
CheckUIState = 0x10000000 | 0x118, // lparam = uint32_t* out state (2 = char select ready)
|
||||
Logout = 0x10000000 | 0x9D, // wparam = { bool unknown, bool character_select }
|
||||
}
|
||||
|
||||
@@ -11,6 +11,16 @@ public static class UIPackets
|
||||
Right = 0x2
|
||||
}
|
||||
|
||||
public enum ActionState
|
||||
{
|
||||
MouseDown = 0x6,
|
||||
MouseUp = 0x7,
|
||||
MouseClick = 0x8,
|
||||
MouseDoubleClick = 0x9,
|
||||
DragRelease = 0xb,
|
||||
KeyDown = 0xe
|
||||
}
|
||||
|
||||
[StructLayout(LayoutKind.Sequential, Pack = 1)]
|
||||
public readonly struct LogChatMessage(string message, Channel channel)
|
||||
{
|
||||
@@ -29,11 +39,11 @@ public static class UIPackets
|
||||
}
|
||||
|
||||
[StructLayout(LayoutKind.Sequential, Pack = 1)]
|
||||
public readonly struct KeyAction(uint key)
|
||||
public readonly struct KeyAction(uint key, uint wParam = 0x4000, uint lParam = 0x0006)
|
||||
{
|
||||
public readonly uint Key = key;
|
||||
public readonly uint WParam = 0x4000;
|
||||
public readonly uint LParam = 0x0006;
|
||||
public readonly uint WParam = wParam;
|
||||
public readonly uint LParam = lParam;
|
||||
}
|
||||
|
||||
[StructLayout(LayoutKind.Sequential, Pack = 1)]
|
||||
@@ -43,4 +53,14 @@ public static class UIPackets
|
||||
public readonly uint IsDoubleClick = isDoubleClick;
|
||||
public readonly uint UnknownTypeScreenPos = unknownTypeScreenPos;
|
||||
}
|
||||
|
||||
[StructLayout(LayoutKind.Sequential, Pack = 1)]
|
||||
public readonly struct MouseAction(uint frameId, uint childOffsetId, ActionState currentState, nuint wparam = 0, nuint lparam = 0)
|
||||
{
|
||||
public readonly uint FrameId = frameId;
|
||||
public readonly uint ChildOffsetId = childOffsetId;
|
||||
public readonly ActionState CurrentState = currentState;
|
||||
public readonly nuint WParam = wparam;
|
||||
public readonly nuint LParam = lparam;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
using Daybreak.API.Models;
|
||||
using Daybreak.Shared.Models.Api;
|
||||
using Daybreak.Shared.Models.Guildwars;
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
@@ -34,6 +35,9 @@ namespace Daybreak.API.Serialization;
|
||||
[JsonSerializable(typeof(TitleInfo))]
|
||||
[JsonSerializable(typeof(LoginInfo))]
|
||||
[JsonSerializable(typeof(MainPlayerBuildContext))]
|
||||
[JsonSerializable(typeof(InventoryInformation))]
|
||||
[JsonSerializable(typeof(ItemProperty))]
|
||||
[JsonSourceGenerationOptions(UseStringEnumConverter = true)]
|
||||
public partial class ApiJsonSerializerContext : JsonSerializerContext
|
||||
{
|
||||
}
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
using Daybreak.API.Services.Interop;
|
||||
using Daybreak.API.Interop.GuildWars;
|
||||
using Daybreak.API.Models;
|
||||
using Daybreak.API.Services.Interop;
|
||||
using Daybreak.Shared.Models.Api;
|
||||
using System.Core.Extensions;
|
||||
using System.Extensions.Core;
|
||||
@@ -53,7 +55,7 @@ public sealed class CharacterSelectService(
|
||||
|
||||
var currentUuid = gameContext.Pointer->CharContext->PlayerUuid.ToString();
|
||||
var availableChars = new List<CharacterSelectEntry>((int)availableCharsContext.Pointer->Size);
|
||||
foreach(var charContext in *availableCharsContext.Pointer)
|
||||
foreach (var charContext in *availableCharsContext.Pointer)
|
||||
{
|
||||
var nameSpan = charContext.Name.AsSpan();
|
||||
var name = new string(nameSpan[..nameSpan.IndexOf('\0')]);
|
||||
@@ -68,8 +70,17 @@ public sealed class CharacterSelectService(
|
||||
charContext.IsPvp));
|
||||
}
|
||||
|
||||
var currentCharacter = availableChars.FirstOrDefault(c => c.Uuid == currentUuid);
|
||||
return new CharacterSelectInformation(currentCharacter, availableChars);
|
||||
// TODO: Reforged sometimes returns Zero UUID even when in-game, need to investigate why
|
||||
if (currentUuid != Uuid.Zero.ToString())
|
||||
{
|
||||
var currentCharacter = availableChars.FirstOrDefault(c => c.Uuid == currentUuid);
|
||||
return new CharacterSelectInformation(currentCharacter, availableChars);
|
||||
}
|
||||
else
|
||||
{
|
||||
var currentCharacter = availableChars.FirstOrDefault();
|
||||
return new CharacterSelectInformation(currentCharacter, availableChars);
|
||||
}
|
||||
}
|
||||
}, cancellationToken);
|
||||
}
|
||||
@@ -85,20 +96,10 @@ public sealed class CharacterSelectService(
|
||||
|
||||
await this.TriggerLogOut(cancellationToken);
|
||||
|
||||
var indexResult = await this.WaitForLoginScreenAndGetCurrentAndDesiredIndex(characterName, cancellationToken);
|
||||
if (indexResult is null)
|
||||
var selectResult = await this.WaitForCharSelectAndSelectCharacter(characterName, cancellationToken);
|
||||
if (!selectResult)
|
||||
{
|
||||
scopedLogger.LogError("Failed to find index by name {name}", characterName);
|
||||
return false;
|
||||
}
|
||||
|
||||
var currentIndex = indexResult.Value.CurrentIndex;
|
||||
var desiredIndex = indexResult.Value.DesiredIndex;
|
||||
scopedLogger.LogInformation("Changing character to {name} with index {index}", characterName, desiredIndex);
|
||||
var navigateResult = await this.NavigateToCharAndPlay(currentIndex, desiredIndex, cancellationToken);
|
||||
if (!navigateResult)
|
||||
{
|
||||
scopedLogger.LogError("Failed to navigate to character {name} with index {index}", characterName, desiredIndex);
|
||||
scopedLogger.LogError("Failed to select character {name}", characterName);
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -123,86 +124,138 @@ public sealed class CharacterSelectService(
|
||||
|
||||
await this.TriggerLogOut(cancellationToken);
|
||||
|
||||
var indexResult = await this.WaitForLoginScreenAndGetCurrentAndDesiredIndex(desiredCharName, cancellationToken);
|
||||
if (indexResult is null)
|
||||
var selectResult = await this.WaitForCharSelectAndSelectCharacter(desiredCharName, cancellationToken);
|
||||
if (!selectResult)
|
||||
{
|
||||
scopedLogger.LogError("Resolved character by uuid {uuid} but failed to find index by name {name}", uuid, desiredCharName);
|
||||
return false;
|
||||
}
|
||||
|
||||
var currentIndex = indexResult.Value.CurrentIndex;
|
||||
var desiredIndex = indexResult.Value.DesiredIndex;
|
||||
scopedLogger.LogInformation("Changing character to {name} with index {index} and uuid {uuid}", desiredCharName, desiredIndex, uuid);
|
||||
var navigateResult = await this.NavigateToCharAndPlay(currentIndex, desiredIndex, cancellationToken);
|
||||
if (!navigateResult)
|
||||
{
|
||||
scopedLogger.LogError("Failed to navigate to character {name} with index {index} and uuid {uuid}", desiredCharName, desiredIndex, uuid);
|
||||
scopedLogger.LogError("Failed to select character {name} with uuid {uuid}", desiredCharName, uuid);
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private async Task<bool> ValidateState(CancellationToken cancellationToken)
|
||||
private async Task<bool> SelectCharacterToPlay(string characterName, bool play, CancellationToken cancellationToken)
|
||||
{
|
||||
var scopedLogger = this.logger.CreateScopedLogger();
|
||||
|
||||
return await this.gameThreadService.QueueOnGameThread(() =>
|
||||
{
|
||||
return this.instanceContextService.GetInstanceType() is not API.Interop.GuildWars.InstanceType.Loading;
|
||||
unsafe
|
||||
{
|
||||
var selectorFrame = this.GetCharSelectorFrame();
|
||||
if (selectorFrame is null)
|
||||
{
|
||||
scopedLogger.LogError("Character selector frame not found");
|
||||
return false;
|
||||
}
|
||||
|
||||
var ctx = this.uiContextService.GetFrameContext<CharSelectorContext>(selectorFrame);
|
||||
if (ctx.IsNull)
|
||||
{
|
||||
scopedLogger.LogError("Character selector context not found");
|
||||
return false;
|
||||
}
|
||||
|
||||
var panesFrame = this.uiContextService.GetChildFrame(selectorFrame, 0);
|
||||
if (panesFrame.IsNull)
|
||||
{
|
||||
scopedLogger.LogError("Character panes frame not found");
|
||||
return false;
|
||||
}
|
||||
|
||||
// Get current selected index
|
||||
var selectedIdx = 0;
|
||||
this.uiContextService.SendFrameUIMessage(panesFrame, UIMessage.FrameMessage_QuerySelectedIndex, null, &selectedIdx);
|
||||
|
||||
var chosen = false;
|
||||
for (uint i = 0; !chosen && i < ctx.Pointer->Chars.Size; i++)
|
||||
{
|
||||
var c = ctx.Pointer->Chars.Buffer[i];
|
||||
var charNameSpan = c.Pointer->Name.AsSpan();
|
||||
var nullIdx = charNameSpan.IndexOf('\0');
|
||||
if (nullIdx <= 0)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var charName = new string(charNameSpan[..nullIdx]);
|
||||
if (!charName.StartsWith(characterName, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
while (selectedIdx != i)
|
||||
{
|
||||
var keyAction = new UIPackets.KeyAction(0x1c);
|
||||
this.uiContextService.SendFrameUIMessage(panesFrame, UIMessage.KeyDown, &keyAction);
|
||||
|
||||
var newIdx = selectedIdx;
|
||||
this.uiContextService.SendFrameUIMessage(panesFrame, UIMessage.FrameMessage_QuerySelectedIndex, null, &newIdx);
|
||||
|
||||
if (newIdx == selectedIdx)
|
||||
{
|
||||
break; // This shouldn't happen - the character should have changed
|
||||
}
|
||||
|
||||
selectedIdx = newIdx;
|
||||
}
|
||||
|
||||
chosen = selectedIdx == i;
|
||||
break;
|
||||
}
|
||||
|
||||
if (!chosen)
|
||||
{
|
||||
scopedLogger.LogError("Failed to select character {name}", characterName);
|
||||
return false;
|
||||
}
|
||||
|
||||
// TODO: This needs to be reworked to use UI messages to click on Play
|
||||
var hwnd = this.platformContextService.GetWindowHandle();
|
||||
if (!hwnd.HasValue)
|
||||
{
|
||||
scopedLogger.LogError("Failed to get game window handle");
|
||||
return false;
|
||||
}
|
||||
|
||||
NativeMethods.SendMessageW((nint)hwnd.Value, NativeMethods.WM_KEYDOWN, 0x50, 0x00190001);
|
||||
NativeMethods.SendMessageW((nint)hwnd.Value, NativeMethods.WM_CHAR, 0x70, 0x00190001);
|
||||
NativeMethods.SendMessageW((nint)hwnd.Value, NativeMethods.WM_KEYUP, 0x50, 0x00190001);
|
||||
return true;
|
||||
}
|
||||
}, cancellationToken);
|
||||
}
|
||||
|
||||
private async Task<bool> NavigateToCharAndPlay(uint currentIndex, uint desiredIndex, CancellationToken cancellationToken)
|
||||
private async Task<bool> WaitForCharSelectAndSelectCharacter(string characterName, CancellationToken cancellationToken)
|
||||
{
|
||||
var scopedLogger = this.logger.CreateScopedLogger();
|
||||
|
||||
// Wait for character select to be ready
|
||||
while (!cancellationToken.IsCancellationRequested)
|
||||
{
|
||||
await Task.Delay(100, cancellationToken);
|
||||
var result = await this.gameThreadService.QueueOnGameThread(() =>
|
||||
try
|
||||
{
|
||||
unsafe
|
||||
if (await this.gameThreadService.QueueOnGameThread(this.IsCharSelectReady, cancellationToken))
|
||||
{
|
||||
var preGameContext = this.gameContextService.GetPreGameContext();
|
||||
if (preGameContext.IsNull)
|
||||
{
|
||||
scopedLogger.LogError("Pre-game context is not initialized");
|
||||
return false;
|
||||
}
|
||||
|
||||
var hwnd = this.platformContextService.GetWindowHandle();
|
||||
if (hwnd is null or 0)
|
||||
{
|
||||
scopedLogger.LogError("Failed to get window handle");
|
||||
return false;
|
||||
}
|
||||
|
||||
if (preGameContext.Pointer->Index1 == currentIndex)
|
||||
{
|
||||
return false; //Not moved yet
|
||||
}
|
||||
|
||||
if (preGameContext.Pointer->Index1 == desiredIndex)
|
||||
{
|
||||
// We're on the desired character. Trigger play
|
||||
NativeMethods.SendMessageW((nint)hwnd.Value, NativeMethods.WM_KEYDOWN, 0x50, 0x00190001);
|
||||
NativeMethods.SendMessageW((nint)hwnd.Value, NativeMethods.WM_CHAR, 0x70, 0x00190001);
|
||||
NativeMethods.SendMessageW((nint)hwnd.Value, NativeMethods.WM_KEYUP, 0x50, 0x00190001);
|
||||
return true;
|
||||
}
|
||||
|
||||
currentIndex = preGameContext.Pointer->Index1;
|
||||
NativeMethods.SendMessageW((nint)hwnd.Value, NativeMethods.WM_KEYDOWN, NativeMethods.VK_RIGHT, 0x014D0001);
|
||||
NativeMethods.SendMessageW((nint)hwnd.Value, NativeMethods.WM_KEYUP, NativeMethods.VK_RIGHT, 0x014D0001);
|
||||
return false;
|
||||
break;
|
||||
}
|
||||
}, cancellationToken);
|
||||
|
||||
if (result)
|
||||
await Task.Delay(100, cancellationToken);
|
||||
}
|
||||
catch(Exception e)
|
||||
{
|
||||
return true;
|
||||
scopedLogger.LogError(e, "Error while waiting for character select to be ready");
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
if (cancellationToken.IsCancellationRequested)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
scopedLogger.LogInformation("Character select ready, selecting {name}", characterName);
|
||||
return await this.SelectCharacterToPlay(characterName, play: true, cancellationToken);
|
||||
}
|
||||
|
||||
private async Task<string?> GetCharNameByUuid(string uuid, CancellationToken cancellationToken)
|
||||
@@ -247,7 +300,7 @@ public sealed class CharacterSelectService(
|
||||
{
|
||||
await this.gameThreadService.QueueOnGameThread(() =>
|
||||
{
|
||||
var logoutMessage = new LogOutMessage(0, 0);
|
||||
var logoutMessage = new LogOutMessage(0, 1); // Changed to 1 for character select
|
||||
unsafe
|
||||
{
|
||||
this.uiContextService.SendMessage(Models.UIMessage.Logout, (uint)&logoutMessage, 0);
|
||||
@@ -255,59 +308,27 @@ public sealed class CharacterSelectService(
|
||||
}, cancellationToken);
|
||||
}
|
||||
|
||||
private async Task<(uint DesiredIndex, uint CurrentIndex)?> WaitForLoginScreenAndGetCurrentAndDesiredIndex(string desiredCharName, CancellationToken cancellationToken)
|
||||
private async Task<bool> ValidateState(CancellationToken cancellationToken)
|
||||
{
|
||||
uint? desiredIndex = default;
|
||||
uint? currentIndex = default;
|
||||
while (!cancellationToken.IsCancellationRequested)
|
||||
return await this.gameThreadService.QueueOnGameThread(() =>
|
||||
{
|
||||
await Task.Delay(100, cancellationToken);
|
||||
var ready = await this.gameThreadService.QueueOnGameThread(() =>
|
||||
{
|
||||
unsafe
|
||||
{
|
||||
var preGameContext = this.gameContextService.GetPreGameContext();
|
||||
if (preGameContext.IsNull)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
return this.instanceContextService.GetInstanceType() is not API.Interop.GuildWars.InstanceType.Loading;
|
||||
}, cancellationToken);
|
||||
}
|
||||
|
||||
var uiState = 10U;
|
||||
this.uiContextService.SendMessage(Models.UIMessage.CheckUIState, 0, (uint)&uiState);
|
||||
var loginScreen = uiState == 2;
|
||||
if (!loginScreen)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
private unsafe bool IsCharSelectReady()
|
||||
{
|
||||
return this.GetCharSelectorFrame() is not null;
|
||||
}
|
||||
|
||||
for (var i = 0U; i < preGameContext.Pointer->LoginCharacters.Size; i++)
|
||||
{
|
||||
var loginCharacter = preGameContext.Pointer->LoginCharacters.Buffer[i];
|
||||
var charNameSpan = loginCharacter.CharacterName.AsSpan();
|
||||
var charName = new string(charNameSpan[..charNameSpan.IndexOf('\0')]);
|
||||
if (charName == desiredCharName)
|
||||
{
|
||||
desiredIndex = i;
|
||||
currentIndex = 0xffffffdd;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}, cancellationToken);
|
||||
|
||||
if (ready)
|
||||
{
|
||||
break;
|
||||
}
|
||||
private unsafe Frame* GetCharSelectorFrame()
|
||||
{
|
||||
var selectorFrame = this.uiContextService.GetFrameByLabel("Selector");
|
||||
if (selectorFrame.IsNull)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
if (desiredIndex is not null && currentIndex is not null)
|
||||
{
|
||||
return (desiredIndex.Value, currentIndex.Value);
|
||||
}
|
||||
|
||||
return default;
|
||||
return selectorFrame.Pointer;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,117 @@
|
||||
namespace Daybreak.API.Services;
|
||||
|
||||
public sealed class HashingService
|
||||
{
|
||||
private const uint DefaultHash = 0x325D1EAE;
|
||||
private const uint WStringHashSeed1 = 0xE2C15C9D;
|
||||
private const uint WStringHashSeed2 = 0x2170A28A;
|
||||
|
||||
private static readonly uint[] HashTable = [
|
||||
0x2EB3C21D, 0xA6F1D482, 0xBE24628F, 0x35699E30, 0xC87EE224, 0x04BA0097, 0x791F0EAF, 0xA162D466,
|
||||
0x685B376A, 0xF82133BA, 0x4682D46B, 0xCB25C0C5, 0xAC3F9D22, 0xABF17202, 0xEF987728, 0x3E400B83,
|
||||
0x63003108, 0xD83F1C93, 0x5058F507, 0x032E9991, 0x3518BC24, 0x1F90D44B, 0x92508DCE, 0x42DAA15C,
|
||||
0x5B4105A3, 0x4D5026AD, 0x07F838B0, 0xE8C4D2A2, 0xF9EF4F54, 0x1FC1A85F, 0xB9041A40, 0x446F6969,
|
||||
0xACF1EE39, 0x1828CC65, 0xC2F6C59D, 0x2456C3C0, 0x64F70142, 0x9C8ADB42, 0xC76781BF, 0x6AEA07E3,
|
||||
0xE98B14BF, 0x7DBCB35E, 0x6CE78C80, 0x1E3F93DE, 0x0954FEB7, 0xB3103AEF, 0x6DAAC1EE, 0x6535E301,
|
||||
0x4A221E68, 0x819CDD81, 0x5378CE3B, 0xE7348236, 0x2C3A7BCC, 0x31C50234, 0x745319DA, 0xF0897A0B,
|
||||
0x5C182968, 0x48E018F4, 0x593E3BB6, 0xAB588C8D, 0xF1101F1F, 0x1D36E8BC, 0xABC7C7C9, 0xF58AA6C8,
|
||||
0xF75B4B98, 0xC447B576, 0x6E5290F2, 0xD344EDF8, 0x5E1BCAF1, 0xEE892405, 0x798F7609, 0x8905CC29,
|
||||
0x8977DEDC, 0x007850BC, 0x0AA3114A, 0x2F61C8F2, 0x376DFAA4, 0x0BABFC12, 0x98E64E82, 0xE6743711,
|
||||
0xE26B26B6, 0xCBB10C72, 0x87446F8F, 0x96E26C0D, 0xCF140831, 0x81F69B0B, 0x5C9DF30B, 0xB276F929,
|
||||
0x2712C5B6, 0x80EA90D6, 0x8EB30322, 0xCF7A705A, 0x57A17E8C, 0x556A2425, 0x15104549, 0x5F3D549F,
|
||||
0x5D98FDEE, 0x9D8FA73E, 0x9206C9EF, 0xEDA0541D, 0xA3164F2D, 0x04C83543, 0x9BB27230, 0xEE97F0BA,
|
||||
0xB6AEB38C, 0x41FF4DA7, 0x4023EE54, 0xECBB78BC, 0xC2C54733, 0x8850B63F, 0x93BF56EF, 0xC80177D7,
|
||||
0xE68B0C13, 0x3C1E9D4C, 0x5CB3EACC, 0xF16F5795, 0xAC2B8A5C, 0x952E466E, 0x3095B0D5, 0xEC9D3A43,
|
||||
0x23D8FCF7, 0x10C7A93F, 0x1B479FB5, 0x3664CF8F, 0xA514A56B, 0xE16C4886, 0x63828188, 0xA47F14C1,
|
||||
0x391C237C, 0xFB15911F, 0x29825A01, 0xFC66DA40, 0xB2719F4E, 0x7B0D1611, 0x8F1EE7DE, 0xA6A1E77D,
|
||||
0xDFC25D30, 0x76377036, 0x8145DB82, 0x2300A4E9, 0xA2417F88, 0x1B8C6AFC, 0x68EBDC8E, 0x48FC0C41,
|
||||
0x040C3DCD, 0x17F02C44, 0xCC40B026, 0x5089EE1E, 0x2A594C28, 0x0CC198EC, 0x3F676F3D, 0xCD0AA239,
|
||||
0x274A31A4, 0xC6B4D4FA, 0x9480A62F, 0xAF8862EF, 0x627CA44D, 0x572E89D3, 0x4C05136C, 0x60F36E8C,
|
||||
0x7DE31FEF, 0x1B97FED3, 0xED99E2BF, 0xA034D418, 0xBBDB3E04, 0x1F198B71, 0xED45E889, 0xF7F1B0DC,
|
||||
0x1592B9A5, 0x96D60F96, 0x03D616EE, 0xA5EF679D, 0x831D791D, 0x579D5FD1, 0x4B44DC48, 0x38F7A2F0,
|
||||
0x2BF34A28, 0xAF14318C, 0xBD66D20A, 0x9AC72D2A, 0xC41ADB95, 0x860B2205, 0xE0E580E7, 0x3A06669C,
|
||||
0x2D1A40A2, 0x577EDF60, 0x1A37693A, 0x3E576C35, 0xE3857565, 0xE46E75EE, 0x94E3625D, 0x7D1B9280,
|
||||
0x2A0CCCE1, 0xBAB36163, 0x66E4E087, 0x940129BB, 0x59C197CD, 0xB0634139, 0xBB2964E1, 0x17207138,
|
||||
0xD8BD3728, 0xB324876C, 0x7C72FD04, 0xBB5D9D82, 0xD9B40236, 0xC0045BF6, 0xF85100ED, 0x36493582,
|
||||
0x8D0DF92A, 0xC3328368, 0xC202E412, 0x7ADE0064, 0xD01529C6, 0x4445B2AD, 0x0F6FC63B, 0xEF976FC5,
|
||||
0x709EC6BE, 0x69D31EA1, 0x794BFEB4, 0xB522DB95, 0x700DE6DF, 0x49098B71, 0x38B7FA82, 0x11EDDC04,
|
||||
0x5BB21D9F, 0xB14BC6D4, 0x3009D754, 0x451F5F3D, 0xFAA58575, 0xFB190B0D, 0x999DF79F, 0x6798A7AA,
|
||||
0x0BC434FF, 0x429E6C15, 0xA1EA2298, 0x55A57B81, 0xD7D74B8D, 0x8FECB38B, 0x658AD588, 0xF0190227,
|
||||
0xBE5D2B36, 0x62C59613, 0xC9FEDD64, 0xADD07A1E, 0x757A7679, 0xD78B6DCC, 0x56F88B2C, 0x4CC020BB,
|
||||
0xA88B51FE, 0xF47F7691, 0x48587EDD, 0x3FCEC5DB, 0xA7E127AC, 0x561F6161, 0x37AB9607, 0xFE23282B,
|
||||
];
|
||||
|
||||
private static readonly uint[] StringHashTable = [
|
||||
0x92B9A528, 0x25D4FC88, 0xEDCBEFB8, 0x51063A80,
|
||||
0x91341C61, 0x0261229D, 0x726F48ED, 0xCE1C088C,
|
||||
0x76253EB5, 0x31E3A0DE, 0xA2AAD215, 0xCA7D6D27,
|
||||
0xA5F98970, 0x0541C365, 0x3C14FF04, 0x5056AF4F
|
||||
];
|
||||
|
||||
private static uint HashHelper(uint result, byte b)
|
||||
{
|
||||
return result + (HashTable[result >> 24] ^ (result >> 6) ^ HashTable[b]);
|
||||
}
|
||||
|
||||
public uint Hash(byte[] data)
|
||||
{
|
||||
var hash = DefaultHash;
|
||||
foreach(var b in data)
|
||||
{
|
||||
hash = HashHelper(hash, b);
|
||||
}
|
||||
|
||||
return hash;
|
||||
}
|
||||
|
||||
public uint Hash(byte data)
|
||||
{
|
||||
return HashHelper(DefaultHash, data);
|
||||
}
|
||||
|
||||
public uint Hash(short data)
|
||||
{
|
||||
uint hash = DefaultHash;
|
||||
hash = HashHelper(hash, (byte)(data >> 0));
|
||||
hash = HashHelper(hash, (byte)(data >> 8));
|
||||
return hash;
|
||||
}
|
||||
|
||||
public uint Hash(uint data)
|
||||
{
|
||||
uint hash = DefaultHash;
|
||||
hash = HashHelper(hash, (byte)(data >> 0));
|
||||
hash = HashHelper(hash, (byte)(data >> 8));
|
||||
hash = HashHelper(hash, (byte)(data >> 16));
|
||||
hash = HashHelper(hash, (byte)(data >> 24));
|
||||
return hash;
|
||||
}
|
||||
|
||||
public uint Hash(string data, int maxLength = -1)
|
||||
{
|
||||
if (maxLength is -1)
|
||||
{
|
||||
maxLength = data.Length;
|
||||
}
|
||||
|
||||
uint seed1 = WStringHashSeed1;
|
||||
uint seed2 = WStringHashSeed2;
|
||||
uint hash = DefaultHash;
|
||||
|
||||
for(var i = 0; i < maxLength && i < data.Length; i++)
|
||||
{
|
||||
var c = data[i];
|
||||
var uc = (uint)c;
|
||||
if (c >= 'a' && c <= 'z')
|
||||
{
|
||||
uc -= 0x20;
|
||||
}
|
||||
|
||||
seed1 = (seed1 << 3) ^ uc;
|
||||
seed2 += StringHashTable[seed1 & 0xf];
|
||||
hash ^= seed2 + seed1;
|
||||
}
|
||||
|
||||
return hash;
|
||||
}
|
||||
}
|
||||
@@ -12,9 +12,9 @@ public unsafe sealed class AgentContextService : IAddressHealthService
|
||||
private const int AgentArrayOffset = -0x4;
|
||||
private static readonly byte[] AgentArrayAddressPattern = [0x8b, 0x0c, 0x90, 0x85, 0xc9, 0x74, 0x19];
|
||||
|
||||
private const string PlayerAgentIdMask = "xx????xxxx";
|
||||
private const string PlayerAgentIdMask = "xx????xxxxxxx";
|
||||
private const int PlayerAgentIdOffset = -0xE;
|
||||
private static readonly byte[] PlayerAgentIdAddressPattern = [0x5d, 0xe9, 0x00, 0x00, 0x00, 0x00, 0x55, 0x8b, 0xec, 0x53];
|
||||
private static readonly byte[] PlayerAgentIdAddressPattern = [0x5d, 0xe9, 0x00, 0x00, 0x00, 0x00, 0x55, 0x8b, 0xec, 0x56, 0x57, 0x8b, 0x7d];
|
||||
|
||||
private readonly MemoryScanningService memoryScanningService;
|
||||
private readonly GWAddressCache agentArrayAddress;
|
||||
|
||||
@@ -3,14 +3,23 @@ using Daybreak.API.Interop.GuildWars;
|
||||
using Daybreak.API.Models;
|
||||
using System.Core.Extensions;
|
||||
using System.Extensions.Core;
|
||||
using System.Runtime.InteropServices;
|
||||
|
||||
namespace Daybreak.API.Services.Interop;
|
||||
|
||||
public unsafe sealed class GameContextService : IAddressHealthService
|
||||
{
|
||||
private const string BaseAddressMask = "xxxxxxx";
|
||||
private const int BaseAddressOffset = +7;
|
||||
private static readonly byte[] BaseAddressPattern = [0x50, 0x6A, 0x0F, 0x6A, 0x00, 0xFF, 0x35];
|
||||
// TLS-based pattern to find GameContext getter function
|
||||
private const string GameContextTlsMask = "xx????xxxxxxxxxxx????x";
|
||||
private const int GameContextTlsOffset = 0;
|
||||
private static readonly byte[] GameContextTlsPattern =
|
||||
[
|
||||
0x8B, 0x0D, 0x00, 0x00, 0x00, 0x00, // mov ecx, [TlsIndexPtr] - 6 bytes: xx????
|
||||
0x64, 0xA1, 0x2C, 0x00, 0x00, 0x00, // mov eax, fs:[0000002C] - 6 bytes: xxxxxx
|
||||
0x8B, 0x04, 0x88, // mov eax, [eax+ecx*4] - 3 bytes: xxx
|
||||
0x8B, 0x80, 0x04, 0x00, 0x00, 0x00, // mov eax, [eax+00000004] - 6 bytes: xx????
|
||||
0xC3 // ret - 1 byte: x
|
||||
]; // Total: 22 bytes, mask: 22 chars
|
||||
|
||||
private const string PreGameContextFile = "UiPregame.cpp";
|
||||
private const string PreGameContextAssertion = "!s_scene";
|
||||
@@ -21,7 +30,7 @@ public unsafe sealed class GameContextService : IAddressHealthService
|
||||
private static readonly byte[] AvailableCharsPattern = [0x8B, 0x35, 0x00, 0x00, 0x00, 0x00, 0x57, 0x69, 0xF8, 0x84, 0x00, 0x00, 0x00];
|
||||
|
||||
private readonly MemoryScanningService memoryScanningService;
|
||||
private readonly GWAddressCache baseAddress;
|
||||
private readonly GWAddressCache gameContextGetterAddress;
|
||||
private readonly GWAddressCache preGameContextAddress;
|
||||
private readonly GWAddressCache availableCharsAddress;
|
||||
private readonly ILogger<GameContextService> logger;
|
||||
@@ -32,18 +41,21 @@ public unsafe sealed class GameContextService : IAddressHealthService
|
||||
{
|
||||
this.logger = logger.ThrowIfNull();
|
||||
this.memoryScanningService = memoryScanningService.ThrowIfNull();
|
||||
this.baseAddress = new GWAddressCache(this.GetBaseAddress);
|
||||
this.gameContextGetterAddress = new GWAddressCache(this.GetGameContextGetterAddress);
|
||||
this.preGameContextAddress = new GWAddressCache(this.GetPreGameContextAddress);
|
||||
this.availableCharsAddress = new GWAddressCache(this.GetAvailableCharactersAddress);
|
||||
}
|
||||
|
||||
[UnmanagedFunctionPointer(CallingConvention.Cdecl)]
|
||||
private delegate nuint GetGameContextDelegate();
|
||||
|
||||
public List<AddressState> GetAddressStates()
|
||||
{
|
||||
return [
|
||||
new AddressState
|
||||
{
|
||||
Address = this.baseAddress.GetAddress() ?? 0,
|
||||
Name = nameof(this.baseAddress),
|
||||
Address = this.gameContextGetterAddress.GetAddress() ?? 0,
|
||||
Name = nameof(this.gameContextGetterAddress),
|
||||
},
|
||||
new AddressState
|
||||
{
|
||||
@@ -113,41 +125,46 @@ public unsafe sealed class GameContextService : IAddressHealthService
|
||||
private nuint GetGameContextAddress()
|
||||
{
|
||||
var scopedLogger = this.logger.CreateScopedLogger();
|
||||
if (this.baseAddress.GetAddress() is not nuint basePtr)
|
||||
|
||||
// Get the address of the GameContext getter function
|
||||
var getterAddress = this.gameContextGetterAddress.GetAddress();
|
||||
if (getterAddress is null or 0)
|
||||
{
|
||||
scopedLogger.LogError("Failed to get base address");
|
||||
return default;
|
||||
scopedLogger.LogError("Failed to get GameContext getter function address");
|
||||
return 0;
|
||||
}
|
||||
|
||||
nuint** baseContext = *(nuint***)basePtr;
|
||||
if (baseContext is null)
|
||||
// Call the function to get GameContext
|
||||
var getter = Marshal.GetDelegateForFunctionPointer<GetGameContextDelegate>((nint)getterAddress);
|
||||
var gameContextPtr = getter();
|
||||
|
||||
if (gameContextPtr == 0)
|
||||
{
|
||||
scopedLogger.LogError("Failed to get base context address");
|
||||
return default;
|
||||
scopedLogger.LogError("GameContext getter returned null");
|
||||
return 0;
|
||||
}
|
||||
|
||||
var globalContextPtr = baseContext[0x6];
|
||||
if (globalContextPtr is null)
|
||||
{
|
||||
scopedLogger.LogError("Failed to get game context address");
|
||||
return default;
|
||||
}
|
||||
|
||||
return (nuint)globalContextPtr;
|
||||
scopedLogger.LogInformation("GameContext address: 0x{address:X8}", gameContextPtr);
|
||||
return gameContextPtr;
|
||||
}
|
||||
|
||||
private nuint GetBaseAddress()
|
||||
private nuint GetGameContextGetterAddress()
|
||||
{
|
||||
var scopedLogger = this.logger.CreateScopedLogger();
|
||||
var baseAddress = this.memoryScanningService.FindAndResolveAddress(BaseAddressPattern, BaseAddressMask, BaseAddressOffset);
|
||||
if (baseAddress is 0)
|
||||
|
||||
var address = this.memoryScanningService.FindAddress(
|
||||
GameContextTlsPattern,
|
||||
GameContextTlsMask,
|
||||
0);
|
||||
|
||||
if (address is 0)
|
||||
{
|
||||
scopedLogger.LogError("Failed to find base address");
|
||||
scopedLogger.LogError("Failed to find GameContext getter function");
|
||||
return 0U;
|
||||
}
|
||||
|
||||
scopedLogger.LogInformation("Base address: 0x{address:X8}", baseAddress);
|
||||
return baseAddress;
|
||||
scopedLogger.LogInformation("GameContext getter function: 0x{address:X8}", address);
|
||||
return address;
|
||||
}
|
||||
|
||||
private unsafe nuint GetPreGameContextAddress()
|
||||
|
||||
@@ -16,7 +16,7 @@ public sealed class GameThreadService
|
||||
|
||||
[SuppressUnmanagedCodeSecurity]
|
||||
[UnmanagedFunctionPointer(CallingConvention.Cdecl)]
|
||||
private delegate void LeaveGameThread(nint ctx);
|
||||
private delegate void LeaveGameThread(nint arg1, nint arg2);
|
||||
|
||||
private readonly MemoryScanningService memoryScanningService;
|
||||
private readonly GWHook<LeaveGameThread> leaveGameThreadHook;
|
||||
@@ -103,7 +103,7 @@ public sealed class GameThreadService
|
||||
scopedLogger.LogInformation("Hooks initialized");
|
||||
}
|
||||
|
||||
private void OnLeaveGameThread(nint ctx)
|
||||
private void OnLeaveGameThread(nint arg1, nint arg2)
|
||||
{
|
||||
var scopedLogger = this.logger.CreateScopedLogger();
|
||||
while(this.queuedItems.TryDequeue(out var item))
|
||||
@@ -137,6 +137,6 @@ public sealed class GameThreadService
|
||||
}
|
||||
}
|
||||
|
||||
this.leaveGameThreadHook.Continue(ctx);
|
||||
this.leaveGameThreadHook.Continue(arg1, arg2);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,6 +16,8 @@ public sealed unsafe class MemoryScanningService(
|
||||
private readonly (nuint BaseAddress, ImageSectionHeader Section) textSection = GetSectionHeader(".text");
|
||||
private readonly (nuint BaseAddress, ImageSectionHeader Section) dataSection = GetSectionHeader(".rdata");
|
||||
|
||||
public uint GameTlsIndex { get; } = GetGameTlsIndex();
|
||||
|
||||
public nuint FunctionFromNearCall(nuint callInstructionAddress, bool checkValidPtr = true)
|
||||
{
|
||||
var scopedLogger = this.logger.CreateScopedLogger();
|
||||
@@ -118,7 +120,7 @@ public sealed unsafe class MemoryScanningService(
|
||||
|
||||
fileSearchPos = filePtr + 1;
|
||||
|
||||
// If what we found is “…/file.hpp” (no drive letter) fix it
|
||||
// If what we found is "…/file.hpp" (no drive letter) fix it
|
||||
if (Marshal.ReadByte((nint)(filePtr + 1)) != (byte)':')
|
||||
{
|
||||
// look ≤128 bytes backward for ':' and back-up one char
|
||||
@@ -199,6 +201,12 @@ public sealed unsafe class MemoryScanningService(
|
||||
return address;
|
||||
}
|
||||
|
||||
public nuint FindAddress(byte[] pattern, string mask, int offset, nuint startAddress, nuint endAddress)
|
||||
{
|
||||
var address = FindAddressInternal(startAddress, endAddress, pattern, mask, offset);
|
||||
return address;
|
||||
}
|
||||
|
||||
public nuint ToFunctionStart(nuint callInstructionAddress, uint scanRange = 0x500)
|
||||
{
|
||||
if (callInstructionAddress == 0)
|
||||
@@ -216,6 +224,92 @@ public sealed unsafe class MemoryScanningService(
|
||||
return FindInRange(prologue, mask, 0, start, end);
|
||||
}
|
||||
|
||||
public nuint FindNthUseOfString(string str, uint nth, int offset = 0, bool useTextSection = true)
|
||||
{
|
||||
var scopedLogger = this.logger.CreateScopedLogger();
|
||||
var strBytes = Encoding.ASCII.GetBytes(str);
|
||||
var mask = new string('x', strBytes.Length);
|
||||
|
||||
var foundStr = FindAddressInternal(this.dataSection, strBytes, mask, 0);
|
||||
if (foundStr is 0)
|
||||
{
|
||||
scopedLogger.LogError("Failed to find string in .rdata section");
|
||||
return 0;
|
||||
}
|
||||
|
||||
var firstNullChar = FindInRange([0x0], "x", 1, foundStr, foundStr - 0x64);
|
||||
if (firstNullChar is 0)
|
||||
{
|
||||
scopedLogger.LogError("Failed to find null terminator");
|
||||
return 0;
|
||||
}
|
||||
|
||||
return this.FindNthUseOfAddress(firstNullChar, nth, offset, useTextSection);
|
||||
}
|
||||
|
||||
public nuint FindUseOfString(string str, int offset = 0, bool useTextSection = true)
|
||||
{
|
||||
return this.FindNthUseOfString(str, 0, offset, useTextSection);
|
||||
}
|
||||
|
||||
public nuint FindNthUseOfWideString(string str, uint nth, int offset = 0, bool useTextSection = true)
|
||||
{
|
||||
var scopedLogger = this.logger.CreateScopedLogger();
|
||||
var strBytes = Encoding.Unicode.GetBytes(str);
|
||||
var mask = new string('x', strBytes.Length);
|
||||
|
||||
var foundStr = FindAddressInternal(this.dataSection, strBytes, mask, 0);
|
||||
if (foundStr is 0)
|
||||
{
|
||||
scopedLogger.LogError("Failed to find wide string in .rdata section");
|
||||
return 0;
|
||||
}
|
||||
|
||||
var firstNullChar = FindInRange([0x0, 0x0], "xx", 2, foundStr, foundStr - 0x128);
|
||||
if (firstNullChar is 0)
|
||||
{
|
||||
scopedLogger.LogError("Failed to find null terminator for wide string");
|
||||
return 0;
|
||||
}
|
||||
|
||||
return this.FindNthUseOfAddress(firstNullChar, nth, offset, useTextSection);
|
||||
}
|
||||
|
||||
public nuint FindUseOfWideString(string str, int offset = 0, bool useTextSection = true)
|
||||
{
|
||||
return this.FindNthUseOfWideString(str, 0, offset, useTextSection);
|
||||
}
|
||||
|
||||
private nuint FindNthUseOfAddress(nuint address, uint nth, int offset, bool useTextSection)
|
||||
{
|
||||
var (BaseAddress, Section) = useTextSection ? this.textSection : this.dataSection;
|
||||
var addressBytes = BitConverter.GetBytes((uint)address);
|
||||
var mask = "xxxx";
|
||||
|
||||
var currentAddress = BaseAddress + Section.VirtualAddress;
|
||||
var endAddress = currentAddress + Section.VirtualSize;
|
||||
|
||||
uint foundCount = 0;
|
||||
while (currentAddress < endAddress)
|
||||
{
|
||||
var found = FindAddressInternal(currentAddress, endAddress, addressBytes, mask, 0);
|
||||
if (found is 0)
|
||||
{
|
||||
break;
|
||||
}
|
||||
|
||||
if (foundCount == nth)
|
||||
{
|
||||
return (nuint)((nint)found + offset);
|
||||
}
|
||||
|
||||
foundCount++;
|
||||
currentAddress = found + 1;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
private static bool IsValidPtr(nuint address, (nuint BaseAddress, ImageSectionHeader Section) section)
|
||||
{
|
||||
return address > 0 &&
|
||||
@@ -276,8 +370,12 @@ public sealed unsafe class MemoryScanningService(
|
||||
{
|
||||
var textStart = section.BaseAddress + section.Section.VirtualAddress;
|
||||
var textEnd = textStart + section.Section.VirtualSize - (uint)pattern.Length;
|
||||
return FindAddressInternal(textStart, textEnd, pattern, mask, offset);
|
||||
}
|
||||
|
||||
for (var cur = textStart; cur <= textEnd; ++cur)
|
||||
private static nuint FindAddressInternal(nuint startAddr, nuint endAddr, byte[] pattern, string mask, int offset = 0)
|
||||
{
|
||||
for (var cur = startAddr; cur <= endAddr; ++cur)
|
||||
{
|
||||
if (Marshal.ReadByte((nint)cur) != pattern[0])
|
||||
{
|
||||
@@ -312,4 +410,30 @@ public sealed unsafe class MemoryScanningService(
|
||||
? throw new InvalidOperationException($"Failed to initialize memory scanner. Failed to find {headerName} section")
|
||||
: ((nuint, ImageSectionHeader))(baseAddr, textHdr);
|
||||
}
|
||||
|
||||
private static uint GetGameTlsIndex()
|
||||
{
|
||||
var m = Process.GetCurrentProcess().MainModule ?? throw new InvalidOperationException("Failed to get TLS index. Failed to find main module");
|
||||
var baseAddr = (nint)m.BaseAddress;
|
||||
var pe = new PeFile(m.FileName);
|
||||
|
||||
// Get the TLS data directory entry (index 9 = IMAGE_DIRECTORY_ENTRY_TLS)
|
||||
var tlsDirectory = pe.ImageNtHeaders?.OptionalHeader.DataDirectory[9];
|
||||
if (tlsDirectory is null || tlsDirectory.VirtualAddress == 0)
|
||||
{
|
||||
throw new InvalidOperationException("Failed to get TLS index. No TLS directory found");
|
||||
}
|
||||
|
||||
// Calculate the address of the TLS directory in memory
|
||||
var tlsDirectoryAddress = baseAddr + (nint)tlsDirectory.VirtualAddress;
|
||||
|
||||
// Read the IMAGE_TLS_DIRECTORY structure
|
||||
// AddressOfIndex is at offset 8 (after StartAddressOfRawData and EndAddressOfRawData)
|
||||
var addressOfIndexPtr = Marshal.ReadIntPtr(tlsDirectoryAddress + 8);
|
||||
|
||||
// Read the TLS index from the location pointed to by AddressOfIndex
|
||||
var tlsIndex = (uint)Marshal.ReadInt32(addressOfIndexPtr);
|
||||
|
||||
return tlsIndex;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
using Daybreak.API.Models;
|
||||
using System.Core.Extensions;
|
||||
using System.Extensions;
|
||||
using static Daybreak.API.Models.UIPackets;
|
||||
|
||||
namespace Daybreak.API.Services.Interop;
|
||||
|
||||
@@ -10,13 +11,12 @@ public sealed class PartyContextService
|
||||
{
|
||||
private const string SearchButtonFile = "\\Code\\Gw\\Ui\\Game\\Party\\PtSearch.cpp";
|
||||
private const string SearchButtonAssertion = "m_activeList == LIST_HEROES";
|
||||
private const string WindowButtonFile = "\\Code\\Gw\\Ui\\Game\\Party\\PtButtons.cpp";
|
||||
private const string WindowButtonAssertion = "m_selection.agentId";
|
||||
private const string WindowButtonAssertion = "selection.agentId";
|
||||
|
||||
// Fastcall funcs do not work in .NET for x86 right now. https://github.com/dotnet/runtime/issues/113851
|
||||
// This fastcall equivalent is <void*, uint, uint*, void>
|
||||
// This fastcall equivalent is <void*, uint, UIPacket::kMouseAction*, void>
|
||||
private readonly GWFastCall<nint, uint, nint, GWFastCall.Void> searchButtonCallback;
|
||||
// This fastcall equivalent is <void*, uint, uint*, void>
|
||||
// This fastcall equivalent is <void*, uint, UIPacket::kMouseAction*, void>
|
||||
private readonly GWFastCall<nint, uint, nint, GWFastCall.Void> windowButtonCallback;
|
||||
|
||||
private readonly MemoryScanningService memoryScanningService;
|
||||
@@ -29,7 +29,7 @@ public sealed class PartyContextService
|
||||
this.memoryScanningService = memoryScanningService.ThrowIfNull();
|
||||
this.logger = logger.ThrowIfNull();
|
||||
this.searchButtonCallback = new(new GWAddressCache(() => this.memoryScanningService.ToFunctionStart(this.memoryScanningService.FindAssertion(SearchButtonFile, SearchButtonAssertion, 0, 0))));
|
||||
this.windowButtonCallback = new(new GWAddressCache(() => this.memoryScanningService.ToFunctionStart(this.memoryScanningService.FindAssertion(WindowButtonFile, WindowButtonAssertion, 0, 0))));
|
||||
this.windowButtonCallback = new(new GWAddressCache(() => this.memoryScanningService.ToFunctionStart(this.memoryScanningService.FindUseOfString(WindowButtonAssertion))));
|
||||
}
|
||||
|
||||
public List<AddressState> GetAddressStates()
|
||||
@@ -52,13 +52,13 @@ public sealed class PartyContextService
|
||||
}
|
||||
}
|
||||
|
||||
public unsafe bool CallSearchButtonCallback(void* ctx, uint edx, uint* wparam)
|
||||
public unsafe bool CallSearchButtonCallback(void* ctx, uint edx, MouseAction* wparam)
|
||||
{
|
||||
this.searchButtonCallback.Invoke((nint)ctx, edx, (nint)wparam);
|
||||
return true;
|
||||
}
|
||||
|
||||
public unsafe bool CallWindowButtonCallback(void* ctx, uint edx, uint* wparam)
|
||||
public unsafe bool CallWindowButtonCallback(void* ctx, uint edx, MouseAction* wparam)
|
||||
{
|
||||
this.windowButtonCallback.Invoke((nint)ctx, edx, (nint)wparam);
|
||||
return true;
|
||||
@@ -66,26 +66,28 @@ public sealed class PartyContextService
|
||||
|
||||
public unsafe bool AddHero(uint heroId)
|
||||
{
|
||||
var wparam = stackalloc uint[4];
|
||||
wparam[2] = 0x6;
|
||||
wparam[1] = 0x1;
|
||||
var action = new MouseAction(
|
||||
frameId: 0,
|
||||
childOffsetId: 0x1,
|
||||
currentState: ActionState.MouseUp);
|
||||
|
||||
var ctx = stackalloc uint[13];
|
||||
ctx[0xb] = 1;
|
||||
ctx[9] = heroId;
|
||||
return this.CallSearchButtonCallback(ctx, 2, wparam);
|
||||
ctx[0x9] = heroId;
|
||||
return this.CallSearchButtonCallback(ctx, 2, &action);
|
||||
}
|
||||
|
||||
public unsafe bool KickHero(uint heroId)
|
||||
{
|
||||
var wparam = stackalloc uint[4];
|
||||
wparam[2] = 0x6;
|
||||
wparam[1] = 0x6;
|
||||
var action = new MouseAction(
|
||||
frameId: 0,
|
||||
childOffsetId: 0x6,
|
||||
currentState: ActionState.MouseUp);
|
||||
|
||||
var ctx = stackalloc uint[13];
|
||||
ctx[0xb] = 1;
|
||||
ctx[9] = heroId;
|
||||
return this.CallSearchButtonCallback(ctx, 0, wparam);
|
||||
ctx[0x9] = heroId;
|
||||
return this.CallSearchButtonCallback(ctx, 0, &action);
|
||||
}
|
||||
|
||||
public bool KickAllHeroes() => this.KickHero(0x26);
|
||||
@@ -94,6 +96,6 @@ public sealed class PartyContextService
|
||||
{
|
||||
var ctx = stackalloc uint[14];
|
||||
ctx[0xd] = 1;
|
||||
return this.CallWindowButtonCallback(ctx, 0, (uint*)0);
|
||||
return this.CallWindowButtonCallback(ctx, 0, (MouseAction*)0);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,28 +13,30 @@ public sealed class UIContextService
|
||||
{
|
||||
private const string GetChildFrameIdFile = "CtlView.cpp";
|
||||
private const string GetChildFrameIdAssertion = "pageId";
|
||||
private const int GetChildFrameIdOffset = 0x19;
|
||||
private const string SetFrameFlagFile = "FrApi.cpp";
|
||||
private const string SetFrameFlagAssertion = "frameId";
|
||||
private const int SetFrameVisibleOffset = 0x4f9;
|
||||
private const int SetFrameDisabledOffset = 0x4e3;
|
||||
private static readonly byte[] SendFrameUiMessageByIdSeq = [0x83, 0xFB, 0x47, 0x73, 0x14];
|
||||
private const string SendFrameUiMessageByIdMask = "xxxxx";
|
||||
private const int SendFrameUiMessageByIdOffset = -0x34;
|
||||
private const int GetChildFrameIdOffset = 0x12;
|
||||
private static readonly byte[] GetChildFrameIdPattern = [0xE8];
|
||||
private const string GetChildFrameIdMask = "x";
|
||||
|
||||
private static readonly byte[] SendFrameUiMessageSeq = [
|
||||
0x83, 0xC1, 0xDC, // add ecx, -24
|
||||
0xE8, 0x00, 0x00, 0x00, 0x00, // call
|
||||
0x8B, 0x4D, 0xFC // mov ecx, [ebp-04]
|
||||
];
|
||||
private const string SendFrameUiMessageMask = "xxxx????xxx";
|
||||
private const int SendFrameUiMessageOffset = 0x3;
|
||||
|
||||
private static readonly byte[] SendUIMessageSeq = [0xB9, 0x00, 0x00, 0x00, 0x00, 0xE8, 0x00, 0x00, 0x00, 0x00, 0x5D, 0xC3, 0x89, 0x45, 0x08];
|
||||
private const string SendUIMessageMask = "x????x????xxxxx";
|
||||
private const string CreateHashFromStringMask = "xxxxxxx";
|
||||
private const int CreateHashFromStringOffset = 0x7;
|
||||
private static readonly byte[] CreateHashFromStringSeq = [0x85, 0xC0, 0x74, 0x0D, 0x6A, 0xFF, 0x50];
|
||||
|
||||
private const string FrameArrayFile = "\\Code\\Engine\\Frame\\FrMsg.cpp";
|
||||
private const string FrameArrayAssertion = "frame";
|
||||
private static readonly byte[] GetRootFrameSeq = [0x05, 0xE0, 0xFE, 0xFF, 0xFF, 0xC3];
|
||||
private const string GetRootFrameMask = "xxxxxx";
|
||||
private const int GetRootFrameOffset = -0x3C;
|
||||
|
||||
[SuppressUnmanagedCodeSecurity]
|
||||
[UnmanagedFunctionPointer(CallingConvention.Cdecl)]
|
||||
private unsafe delegate uint CreateHashFromStringFunc(char* value, int seed);
|
||||
|
||||
private static readonly byte[] GetRootFrameSeq = [
|
||||
0xA1, 0x00, 0x00, 0x00, 0x00, // mov eax,[address]
|
||||
0x05, 0xD8, 0xFE, 0xFF, 0xFF, // add eax,FFFFFED8
|
||||
0xC3 // ret
|
||||
];
|
||||
private const string GetRootFrameMask = "x????xxxxxx";
|
||||
|
||||
[SuppressUnmanagedCodeSecurity]
|
||||
[UnmanagedFunctionPointer(CallingConvention.Cdecl)]
|
||||
@@ -52,13 +54,25 @@ public sealed class UIContextService
|
||||
[UnmanagedFunctionPointer(CallingConvention.Cdecl)]
|
||||
private unsafe delegate Frame* GetRootFrameFunc();
|
||||
|
||||
private readonly SemaphoreSlim semaphoreSlim = new(1);
|
||||
[SuppressUnmanagedCodeSecurity]
|
||||
[UnmanagedFunctionPointer(CallingConvention.Cdecl)]
|
||||
private unsafe delegate void ValidateAsyncDecodeStr(char* s, DecodeStrCallback callback, void* wParam);
|
||||
|
||||
[SuppressUnmanagedCodeSecurity]
|
||||
[UnmanagedFunctionPointer(CallingConvention.Cdecl)]
|
||||
private unsafe delegate uint DoAsyncDecodeStr(char* encodedStr, DecodeStrCallback callback, void* wParam);
|
||||
|
||||
public unsafe delegate void DecodeStrCallback(void* param, char* s);
|
||||
|
||||
private readonly SemaphoreSlim sendUiMessageSemaphore = new(1);
|
||||
private readonly SemaphoreSlim decodeStringSemaphore = new(1);
|
||||
private readonly MemoryScanningService memoryScanningService;
|
||||
private readonly GWDelegateCache<CreateHashFromStringFunc> createHashFromString;
|
||||
private readonly HashingService hashingService;
|
||||
private readonly GWDelegateCache<GetChildFrameIdFunc> getChildFrameId;
|
||||
private readonly GWAddressCache frameArrayAddressCache;
|
||||
private readonly GWHook<SendUIMessageFunc> sendUiMessageHook;
|
||||
private readonly GWDelegateCache<GetRootFrameFunc> getRootFrame;
|
||||
private readonly GWDelegateCache<ValidateAsyncDecodeStr> validateAsyncDecodeStr;
|
||||
// This fastcall equivalent is <GuildWarsArray<FrameInteractionCallback>*, void*, UIMessage, void*, void*>
|
||||
private readonly GWFastCall<nint, nint, UIMessage, nint, nint, GWFastCall.Void> sendFrameUIMessage;
|
||||
private readonly ILogger<UIContextService> logger;
|
||||
@@ -66,13 +80,13 @@ public sealed class UIContextService
|
||||
private CancellationTokenSource? cts = default;
|
||||
|
||||
public UIContextService(
|
||||
HashingService hashingService,
|
||||
MemoryScanningService memoryScanningService,
|
||||
ILogger<UIContextService> logger)
|
||||
{
|
||||
this.logger = logger.ThrowIfNull();
|
||||
this.memoryScanningService = memoryScanningService.ThrowIfNull();
|
||||
this.createHashFromString = new GWDelegateCache<CreateHashFromStringFunc>(new GWAddressCache(() => this.memoryScanningService.FunctionFromNearCall(
|
||||
this.memoryScanningService.FindAddress(CreateHashFromStringSeq, CreateHashFromStringMask, CreateHashFromStringOffset))));
|
||||
this.hashingService = hashingService.ThrowIfNull();
|
||||
this.frameArrayAddressCache = new GWAddressCache(() => this.memoryScanningService.FindAssertion(FrameArrayFile, FrameArrayAssertion, 0, -0x14));
|
||||
this.sendUiMessageHook = new GWHook<SendUIMessageFunc>(
|
||||
new GWAddressCache(() => this.memoryScanningService.ToFunctionStart(
|
||||
@@ -80,42 +94,50 @@ public sealed class UIContextService
|
||||
this.OnSendUIMessage);
|
||||
this.sendFrameUIMessage = new(
|
||||
new GWAddressCache(() => this.memoryScanningService.FunctionFromNearCall(
|
||||
this.memoryScanningService.FindAddress(SendFrameUiMessageByIdSeq, SendFrameUiMessageByIdMask, SendFrameUiMessageByIdOffset) + 0x67)));
|
||||
this.memoryScanningService.FindAddress(SendFrameUiMessageSeq, SendFrameUiMessageMask, SendFrameUiMessageOffset))));
|
||||
this.getChildFrameId = new GWDelegateCache<GetChildFrameIdFunc>(
|
||||
new GWAddressCache(() => this.memoryScanningService.FunctionFromNearCall(
|
||||
this.memoryScanningService.FindAssertion(GetChildFrameIdFile, GetChildFrameIdAssertion, 0, GetChildFrameIdOffset))));
|
||||
new GWAddressCache(() =>
|
||||
{
|
||||
var address = this.memoryScanningService.FindAssertion(GetChildFrameIdFile, GetChildFrameIdAssertion, 0, GetChildFrameIdOffset);
|
||||
return this.memoryScanningService.FunctionFromNearCall(
|
||||
this.memoryScanningService.FindAddress(GetChildFrameIdPattern, GetChildFrameIdMask, 0, address, address + 0xFF));
|
||||
}));
|
||||
this.getRootFrame = new GWDelegateCache<GetRootFrameFunc>(
|
||||
new GWAddressCache(() => this.memoryScanningService.FindAddress(GetRootFrameSeq, GetRootFrameMask, GetRootFrameOffset)));
|
||||
new GWAddressCache(() => this.memoryScanningService.FindAddress(GetRootFrameSeq, GetRootFrameMask, 0)));
|
||||
|
||||
this.validateAsyncDecodeStr = new GWDelegateCache<ValidateAsyncDecodeStr>(
|
||||
new GWAddressCache(() => this.memoryScanningService.ToFunctionStart(
|
||||
this.memoryScanningService.FindUseOfString("(codedString[0] & ~WORD_BIT_MORE) >= WORD_VALUE_BASE"))));
|
||||
}
|
||||
|
||||
public List<AddressState> GetAddressStates()
|
||||
{
|
||||
return
|
||||
[
|
||||
new()
|
||||
{
|
||||
Name = nameof(this.createHashFromString),
|
||||
Address = this.createHashFromString.Cache.GetAddress() ?? 0U
|
||||
},
|
||||
new()
|
||||
{
|
||||
Name = nameof(this.frameArrayAddressCache),
|
||||
Address = this.frameArrayAddressCache.GetAddress() ?? 0U
|
||||
},
|
||||
new()
|
||||
{
|
||||
Name = nameof(this.getChildFrameId),
|
||||
Address = this.getChildFrameId.Cache.GetAddress() ?? 0U
|
||||
},
|
||||
new()
|
||||
{
|
||||
Name = nameof(this.sendFrameUIMessage),
|
||||
Address = this.sendFrameUIMessage.Cache.GetAddress() ?? 0U
|
||||
},
|
||||
new()
|
||||
{
|
||||
Name = nameof(this.getChildFrameId),
|
||||
Address = this.getChildFrameId.Cache.GetAddress() ?? 0U
|
||||
},
|
||||
new()
|
||||
{
|
||||
Name = nameof(this.getRootFrame),
|
||||
Address = this.getRootFrame.Cache.GetAddress() ?? 0U
|
||||
},
|
||||
new()
|
||||
{
|
||||
Name = nameof(this.validateAsyncDecodeStr),
|
||||
Address = this.validateAsyncDecodeStr.Cache.GetAddress() ?? 0U
|
||||
}
|
||||
];
|
||||
}
|
||||
@@ -150,6 +172,26 @@ public sealed class UIContextService
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
public unsafe WrappedPointer<T> GetFrameContext<T>(WrappedPointer<Frame> frame)
|
||||
where T : unmanaged
|
||||
{
|
||||
if (frame.IsNull || frame.Pointer->FrameCallbacks.Size == 0)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
for (uint i = 0; i < frame.Pointer->FrameCallbacks.Size; i++)
|
||||
{
|
||||
var callback = frame.Pointer->FrameCallbacks.Buffer[i];
|
||||
if (callback.UiCtl_Context != null)
|
||||
{
|
||||
return new WrappedPointer<T>((T*)callback.UiCtl_Context);
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public unsafe WrappedPointer<Frame> GetChildFrame(WrappedPointer<Frame> parent, uint childOffset)
|
||||
{
|
||||
var scopedLogger = this.logger.CreateScopedLogger();
|
||||
@@ -227,10 +269,10 @@ public sealed class UIContextService
|
||||
frame = this.GetButtonActionFrame();
|
||||
}
|
||||
|
||||
this.semaphoreSlim.Wait();
|
||||
this.sendUiMessageSemaphore.Wait();
|
||||
var packet = new UIPackets.KeyAction((uint)action);
|
||||
this.SendFrameUIMessage(frame, UIMessage.KeyDown, &packet);
|
||||
this.semaphoreSlim.Release();
|
||||
this.sendUiMessageSemaphore.Release();
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -241,10 +283,10 @@ public sealed class UIContextService
|
||||
frame = this.GetButtonActionFrame();
|
||||
}
|
||||
|
||||
this.semaphoreSlim.Wait();
|
||||
this.sendUiMessageSemaphore.Wait();
|
||||
var packet = new UIPackets.KeyAction((uint)action);
|
||||
this.SendFrameUIMessage(frame, UIMessage.KeyUp, &packet);
|
||||
this.semaphoreSlim.Release();
|
||||
this.sendUiMessageSemaphore.Release();
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -268,7 +310,7 @@ public sealed class UIContextService
|
||||
return null;
|
||||
}
|
||||
|
||||
var hash = this.CreateHashFromString(label, -1);
|
||||
var hash = this.CreateHashFromString(label);
|
||||
if (hash is 0)
|
||||
{
|
||||
scopedLogger.LogError("Failed to get hash for label {label}", label);
|
||||
@@ -315,26 +357,48 @@ public sealed class UIContextService
|
||||
return frame;
|
||||
}
|
||||
|
||||
public unsafe uint CreateHashFromString(string value, int seed)
|
||||
public uint CreateHashFromString(string value)
|
||||
{
|
||||
var scopedLogger = this.logger.CreateScopedLogger();
|
||||
if (this.createHashFromString.GetDelegate() is not CreateHashFromStringFunc del)
|
||||
{
|
||||
scopedLogger.LogError("Failed to get CreateHashFromString delegate");
|
||||
return 0;
|
||||
}
|
||||
|
||||
fixed (char* valuePtr = value)
|
||||
{
|
||||
return del(valuePtr, seed);
|
||||
}
|
||||
return this.hashingService.Hash(value);
|
||||
}
|
||||
|
||||
public unsafe void SendMessage(UIMessage message, nuint wParam, nuint lParam)
|
||||
{
|
||||
this.semaphoreSlim.Wait();
|
||||
this.sendUiMessageSemaphore.Wait();
|
||||
this.sendUiMessageHook.Continue(message, wParam, lParam);
|
||||
this.semaphoreSlim.Release();
|
||||
this.sendUiMessageSemaphore.Release();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Decodes a string asynchronously using the game's internal decoding mechanism.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Inside the DecodeStrCallback, the decoded string has to be cleaned up by calling Marshal.FreeHGlobal on the char* pointer.
|
||||
/// </remarks>
|
||||
public unsafe void ValidateDecodeString(char* encoded, DecodeStrCallback callback, nuint callbackParam)
|
||||
{
|
||||
var scopedLogger = this.logger.CreateScopedLogger();
|
||||
this.decodeStringSemaphore.Wait();
|
||||
try
|
||||
{
|
||||
|
||||
var validateDecodeStrFnc = this.validateAsyncDecodeStr.GetDelegate();
|
||||
if (validateDecodeStrFnc is null)
|
||||
{
|
||||
scopedLogger.LogError("{funcName} is null", nameof(this.validateAsyncDecodeStr));
|
||||
return;
|
||||
}
|
||||
|
||||
validateDecodeStrFnc(encoded, callback, (void*)callbackParam);
|
||||
}
|
||||
catch(Exception e)
|
||||
{
|
||||
scopedLogger.LogError(e, "Encountered exception while decoding string");
|
||||
}
|
||||
finally
|
||||
{
|
||||
this.decodeStringSemaphore.Release();
|
||||
}
|
||||
}
|
||||
|
||||
private async ValueTask InitializeHooks(CancellationToken cancellationToken)
|
||||
@@ -357,24 +421,24 @@ public sealed class UIContextService
|
||||
|
||||
private static unsafe void SetFrameVisibleInternal(WrappedPointer<Frame> frame, bool visible)
|
||||
{
|
||||
uint* pState = &frame.Pointer->Field91; // direct field address
|
||||
uint* pState = &frame.Pointer->FrameState; // direct field address
|
||||
if (visible)
|
||||
{
|
||||
*pState &= ~0x200u; // clear “hidden”
|
||||
*pState &= ~0x200u; // clear "hidden"
|
||||
}
|
||||
else
|
||||
{
|
||||
*pState |= 0x200u; // set “hidden”
|
||||
*pState |= 0x200u; // set "hidden"
|
||||
}
|
||||
}
|
||||
|
||||
private static unsafe void SetFrameDisabledInternal(WrappedPointer<Frame> frame, bool disabled)
|
||||
{
|
||||
uint* pState = &frame.Pointer->Field91; // same word (offset 0x184)
|
||||
uint* pState = &frame.Pointer->FrameState; // same word (offset 0x184)
|
||||
|
||||
if (disabled)
|
||||
{
|
||||
*pState |= 0x10u; // set “disabled”
|
||||
*pState |= 0x10u; // set "disabled"
|
||||
}
|
||||
else
|
||||
{
|
||||
|
||||
@@ -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)
|
||||
@@ -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)
|
||||
|
||||
@@ -1,16 +1,21 @@
|
||||
using Daybreak.API.Interop;
|
||||
using Daybreak.API.Interop.GuildWars;
|
||||
using Daybreak.API.Models;
|
||||
using Daybreak.API.Services.Interop;
|
||||
using System.Extensions.Core;
|
||||
using System.Runtime.InteropServices;
|
||||
using static Daybreak.API.Services.Interop.UIContextService;
|
||||
|
||||
namespace Daybreak.API.Services;
|
||||
|
||||
public sealed class UIService(
|
||||
GameThreadService gameThreadService,
|
||||
GameContextService gameContextService,
|
||||
UIContextService uIContextService,
|
||||
ILogger<UIService> logger)
|
||||
{
|
||||
private readonly GameThreadService gameThreadService = gameThreadService;
|
||||
private readonly GameContextService gameContextService = gameContextService;
|
||||
private readonly UIContextService uIContextService = uIContextService;
|
||||
private readonly ILogger<UIService> logger = logger;
|
||||
|
||||
@@ -85,6 +90,92 @@ public sealed class UIService(
|
||||
});
|
||||
}
|
||||
|
||||
public async Task<string?> DecodeString(string encoded, Language language, CancellationToken cancellationToken)
|
||||
{
|
||||
var scopedLogger = this.logger.CreateScopedLogger();
|
||||
var taskCompletionSource = new TaskCompletionSource<string?>();
|
||||
var byteCount = (encoded.Length + 1) * sizeof(char);
|
||||
var encodedPtr = Marshal.AllocHGlobal(byteCount);
|
||||
unsafe
|
||||
{
|
||||
var encodedPtrC = (char*)encodedPtr;
|
||||
fixed (char* src = encoded)
|
||||
{
|
||||
Buffer.MemoryCopy(src, encodedPtrC, byteCount, encoded.Length * sizeof(char));
|
||||
}
|
||||
|
||||
encodedPtrC[encoded.Length] = '\0';
|
||||
}
|
||||
|
||||
GCHandle callbackHandle = default;
|
||||
DecodeStrCallback callback;
|
||||
unsafe
|
||||
{
|
||||
callback = (_, decodePtr) =>
|
||||
{
|
||||
try
|
||||
{
|
||||
if (decodePtr is null)
|
||||
{
|
||||
taskCompletionSource.TrySetResult(null);
|
||||
return;
|
||||
}
|
||||
|
||||
var decoded = new string(decodePtr);
|
||||
taskCompletionSource.TrySetResult(decoded);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
taskCompletionSource.TrySetException(ex);
|
||||
}
|
||||
};
|
||||
|
||||
callbackHandle = GCHandle.Alloc(callback);
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
await this.gameThreadService.QueueOnGameThread(() =>
|
||||
{
|
||||
unsafe
|
||||
{
|
||||
var gameContext = this.gameContextService.GetGameContext();
|
||||
if (gameContext.IsNull)
|
||||
{
|
||||
scopedLogger.LogError("Game context is null");
|
||||
return;
|
||||
}
|
||||
|
||||
var textParser = gameContext.Pointer->TextParserContext;
|
||||
if (textParser is null)
|
||||
{
|
||||
scopedLogger.LogError("Text parser context is null");
|
||||
return;
|
||||
}
|
||||
|
||||
var prevLanguage = textParser->Language;
|
||||
if (language is not Language.Unknown)
|
||||
{
|
||||
textParser->Language = language;
|
||||
}
|
||||
|
||||
this.uIContextService.ValidateDecodeString((char*)encodedPtr, callback, nuint.Zero);
|
||||
textParser->Language = prevLanguage;
|
||||
}
|
||||
}, cancellationToken);
|
||||
|
||||
return await taskCompletionSource.Task;
|
||||
}
|
||||
finally
|
||||
{
|
||||
Marshal.FreeHGlobal(encodedPtr);
|
||||
if (callbackHandle.IsAllocated)
|
||||
{
|
||||
callbackHandle.Free();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static UIAction GetUIActionFromFrameLabel(string frameLabel)
|
||||
{
|
||||
return frameLabel switch
|
||||
|
||||
@@ -9,6 +9,7 @@ public static class WebApplicationBuilderExtensions
|
||||
{
|
||||
public static WebApplicationBuilder WithDaybreakServices(this WebApplicationBuilder builder)
|
||||
{
|
||||
builder.Services.AddSingleton<HashingService>();
|
||||
builder.Services.AddSingleton<IBuildTemplateManager, BuildTemplateManager>();
|
||||
builder.Services.AddSingleton<IMDomainNameService, MDomainNameService>();
|
||||
builder.Services.AddHostedService<ApiAdvertisingService>();
|
||||
@@ -19,6 +20,7 @@ public static class WebApplicationBuilderExtensions
|
||||
builder.Services.AddSingleton<PartyService>();
|
||||
builder.Services.AddSingleton<UIService>();
|
||||
builder.Services.AddSingleton<LoginService>();
|
||||
builder.Services.AddSingleton<InventoryService>();
|
||||
builder.WithHookHostedService<GameThreadService>();
|
||||
builder.WithHookHostedService<ChatHandlingService>();
|
||||
builder.WithAddressService<SkillbarContextService>();
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<OutputType>Exe</OutputType>
|
||||
<TargetFramework>$(TargetFrameworkVersionWindows)</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
<Platforms>x86</Platforms>
|
||||
<SelfContained>true</SelfContained>
|
||||
<RuntimeIdentifier>win-x86</RuntimeIdentifier>
|
||||
<PublishAot>true</PublishAot>
|
||||
<StripSymbols>true</StripSymbols>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Reloaded.Assembler" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\Daybreak.Shared\Daybreak.Shared.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
@@ -0,0 +1,138 @@
|
||||
using Daybreak.Shared.Models;
|
||||
using Daybreak.Shared.Utils;
|
||||
using System.Diagnostics;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Text;
|
||||
|
||||
namespace Daybreak.Injector;
|
||||
|
||||
public static class ProcessInjector
|
||||
{
|
||||
public static InjectorResponses.InjectResult InjectWithApi(Process process, string pathToDll)
|
||||
{
|
||||
var modulefullpath = Path.GetFullPath(pathToDll);
|
||||
|
||||
if (!File.Exists(modulefullpath))
|
||||
{
|
||||
Console.WriteLine("Dll to inject not found");
|
||||
return InjectorResponses.InjectResult.InvalidDllPath;
|
||||
}
|
||||
|
||||
var hKernel32 = NativeMethods.GetModuleHandle("kernel32.dll");
|
||||
if (hKernel32 == IntPtr.Zero)
|
||||
{
|
||||
Console.WriteLine("Unable to get a handle of kernel32.dll");
|
||||
return InjectorResponses.InjectResult.InvalidKernel32;
|
||||
}
|
||||
|
||||
var hLoadLib = NativeMethods.GetProcAddress(hKernel32, "LoadLibraryW");
|
||||
if (hLoadLib == IntPtr.Zero)
|
||||
{
|
||||
Console.WriteLine("Unable to get the address of LoadLibraryW");
|
||||
return InjectorResponses.InjectResult.InvalidLoadLibraryW;
|
||||
}
|
||||
|
||||
var hStringBuffer = NativeMethods.VirtualAllocEx(process.Handle, IntPtr.Zero, new IntPtr(2 * (modulefullpath.Length + 1)),
|
||||
0x3000 /* MEM_COMMIT | MEM_RESERVE */, 0x4 /* PAGE_READWRITE */);
|
||||
if (hStringBuffer == IntPtr.Zero)
|
||||
{
|
||||
Console.WriteLine("Unable to allocate memory for module path");
|
||||
return InjectorResponses.InjectResult.ModulePathAllocationFailed;
|
||||
}
|
||||
|
||||
WriteWString(process, hStringBuffer, modulefullpath);
|
||||
if (ReadWString(process, hStringBuffer, 260) != modulefullpath)
|
||||
{
|
||||
Console.WriteLine("Module path string is not correct");
|
||||
return InjectorResponses.InjectResult.DllPathWriteFailed;
|
||||
}
|
||||
|
||||
var hThread = NativeMethods.CreateRemoteThread(process.Handle, IntPtr.Zero, 0, hLoadLib, hStringBuffer, 0, out _);
|
||||
if (hThread == IntPtr.Zero)
|
||||
{
|
||||
Console.WriteLine("Unable to create remote thread");
|
||||
return InjectorResponses.InjectResult.CreateRemoteThreadFailed;
|
||||
}
|
||||
|
||||
var threadResult = NativeMethods.WaitForSingleObject(hThread, 30000u);
|
||||
if (threadResult is 0x102 or 0xFFFFFFFF /* WAIT_FAILED */)
|
||||
{
|
||||
Console.WriteLine($"Exception occurred while waiting for the remote thread. Result is {threadResult}");
|
||||
return InjectorResponses.InjectResult.RemoteThreadTimeout;
|
||||
}
|
||||
|
||||
var dllResult = NativeMethods.GetExitCodeThread(hThread, out _);
|
||||
if (dllResult == 0)
|
||||
{
|
||||
Console.WriteLine($"Injected dll returned non-success status code {dllResult}");
|
||||
return InjectorResponses.InjectResult.DllExitCodeFailed;
|
||||
}
|
||||
|
||||
var memoryFreeResult = NativeMethods.VirtualFreeEx(process.Handle, hStringBuffer, 0, 0x8000 /* MEM_RELEASE */);
|
||||
if (!memoryFreeResult)
|
||||
{
|
||||
Console.WriteLine($"Failed to free dll memory");
|
||||
}
|
||||
|
||||
return memoryFreeResult
|
||||
? InjectorResponses.InjectResult.Success
|
||||
: InjectorResponses.InjectResult.MemoryFreeFailed;
|
||||
}
|
||||
|
||||
private static void WriteBytes(Process process, IntPtr address, byte[] data)
|
||||
{
|
||||
var size = data.Length;
|
||||
var buffer = Marshal.AllocHGlobal(size);
|
||||
Marshal.Copy(data, 0, buffer, size);
|
||||
|
||||
NativeMethods.WriteProcessMemory(
|
||||
process.Handle,
|
||||
address,
|
||||
buffer,
|
||||
size,
|
||||
out _);
|
||||
|
||||
Marshal.FreeHGlobal(buffer);
|
||||
}
|
||||
|
||||
private static void WriteWString(Process process, IntPtr address, string data)
|
||||
{
|
||||
WriteBytes(process, address, Encoding.Unicode.GetBytes(data));
|
||||
}
|
||||
|
||||
private static string ReadWString(Process process, IntPtr address, int maxsize, Encoding? encoding = null)
|
||||
{
|
||||
encoding ??= Encoding.Unicode;
|
||||
var rawbytes = ReadBytes(process, address, maxsize);
|
||||
if (rawbytes.Length == 0)
|
||||
{
|
||||
return "";
|
||||
}
|
||||
|
||||
var ret = encoding.GetString(rawbytes);
|
||||
if (ret.Contains('\0'))
|
||||
{
|
||||
ret = ret[..ret.IndexOf('\0')];
|
||||
}
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
private static byte[] ReadBytes(Process process, IntPtr address, int size)
|
||||
{
|
||||
var buffer = Marshal.AllocHGlobal(size);
|
||||
|
||||
NativeMethods.ReadProcessMemory(process.Handle,
|
||||
address,
|
||||
buffer,
|
||||
size,
|
||||
out _
|
||||
);
|
||||
|
||||
var ret = new byte[size];
|
||||
Marshal.Copy(buffer, ret, 0, size);
|
||||
Marshal.FreeHGlobal(buffer);
|
||||
|
||||
return ret;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,248 @@
|
||||
using Daybreak.Shared.Models;
|
||||
using Daybreak.Shared.Utils;
|
||||
using Microsoft.Win32;
|
||||
using System.Diagnostics;
|
||||
using System.Runtime.InteropServices;
|
||||
using static Daybreak.Shared.Utils.NativeMethods;
|
||||
|
||||
namespace Daybreak.Injector;
|
||||
|
||||
public sealed class ProcessLauncher
|
||||
{
|
||||
public static InjectorResponses.LaunchResult LaunchGuildWars(
|
||||
string path,
|
||||
string args,
|
||||
bool elevated,
|
||||
out int threadId,
|
||||
out int processId)
|
||||
{
|
||||
var sw = Stopwatch.StartNew();
|
||||
Process? process;
|
||||
processId = LaunchClient(path, string.Join(" ", args), elevated, out threadId);
|
||||
if (processId is 0)
|
||||
{
|
||||
Console.WriteLine("Failed to launch GuildWars process.");
|
||||
return InjectorResponses.LaunchResult.LaunchFailed;
|
||||
}
|
||||
|
||||
do
|
||||
{
|
||||
if (sw.Elapsed.TotalSeconds > 10)
|
||||
{
|
||||
Console.WriteLine("Timed out waiting for GuildWars process to start.");
|
||||
return InjectorResponses.LaunchResult.LaunchTimeout;
|
||||
}
|
||||
|
||||
process = Process.GetProcessById(processId);
|
||||
Thread.Sleep(100);
|
||||
} while (process is null);
|
||||
|
||||
if (!McPatch(process.Handle))
|
||||
{
|
||||
var lastErr = Marshal.GetLastWin32Error();
|
||||
Console.WriteLine($"Failed to patch GuildWars process. Error code: {lastErr}");
|
||||
return InjectorResponses.LaunchResult.PatchFailed;
|
||||
}
|
||||
|
||||
return InjectorResponses.LaunchResult.Success;
|
||||
}
|
||||
|
||||
private static int LaunchClient(string path, string args, bool elevated, out int threadId)
|
||||
{
|
||||
var commandLine = $"\"{path}\" {args}";
|
||||
threadId = 0;
|
||||
|
||||
ProcessInformation procinfo;
|
||||
var startinfo = new StartupInfo
|
||||
{
|
||||
cb = Marshal.SizeOf<StartupInfo>()
|
||||
};
|
||||
var saProcess = new SecurityAttributes();
|
||||
saProcess.nLength = (uint)Marshal.SizeOf(saProcess);
|
||||
var saThread = new SecurityAttributes();
|
||||
saThread.nLength = (uint)Marshal.SizeOf(saThread);
|
||||
|
||||
var lastDirectory = Directory.GetCurrentDirectory();
|
||||
Directory.SetCurrentDirectory(Path.GetDirectoryName(path)!);
|
||||
|
||||
SetRegistryValue(@"Software\ArenaNet\Guild Wars", "Path", path);
|
||||
SetRegistryValue(@"Software\ArenaNet\Guild Wars", "Src", path);
|
||||
|
||||
if (!elevated)
|
||||
{
|
||||
if (!SaferCreateLevel(SaferLevelScope.User, SaferLevel.NormalUser, SaferOpen.Open, out var hLevel, IntPtr.Zero))
|
||||
{
|
||||
Debug.WriteLine("SaferCreateLevel");
|
||||
return 0;
|
||||
}
|
||||
|
||||
if (!SaferComputeTokenFromLevel(hLevel, IntPtr.Zero, out var hRestrictedToken, 0, IntPtr.Zero))
|
||||
{
|
||||
Debug.WriteLine("SaferComputeTokenFromLevel");
|
||||
return 0;
|
||||
}
|
||||
|
||||
SaferCloseLevel(hLevel);
|
||||
|
||||
TokenMandatoryLabel tml;
|
||||
tml.Label.Attributes = 0x20; // SE_GROUP_INTEGRITY
|
||||
if (!ConvertStringSidToSid("S-1-16-8192", out tml.Label.Sid))
|
||||
{
|
||||
CloseHandle(hRestrictedToken);
|
||||
Debug.WriteLine("ConvertStringSidToSid");
|
||||
}
|
||||
|
||||
if (!SetTokenInformation(
|
||||
hRestrictedToken,
|
||||
TokenInformationClass.TokenIntegrityLevel,
|
||||
ref tml,
|
||||
(uint)Marshal.SizeOf(tml) + GetLengthSid(tml.Label.Sid)))
|
||||
{
|
||||
LocalFree(tml.Label.Sid);
|
||||
CloseHandle(hRestrictedToken);
|
||||
return 0;
|
||||
}
|
||||
|
||||
LocalFree(tml.Label.Sid);
|
||||
if (!CreateProcessAsUser(
|
||||
hRestrictedToken,
|
||||
null!,
|
||||
commandLine,
|
||||
ref saProcess,
|
||||
ref saProcess,
|
||||
false,
|
||||
(uint)CreationFlags.CreateSuspended,
|
||||
IntPtr.Zero,
|
||||
null!,
|
||||
ref startinfo,
|
||||
out procinfo))
|
||||
{
|
||||
var error = Marshal.GetLastWin32Error();
|
||||
Debug.WriteLine($"CreateProcessAsUser {error}");
|
||||
CloseHandle(procinfo.hThread);
|
||||
return 0;
|
||||
}
|
||||
|
||||
CloseHandle(hRestrictedToken);
|
||||
}
|
||||
else
|
||||
{
|
||||
if (!CreateProcess(
|
||||
null!,
|
||||
commandLine,
|
||||
ref saProcess,
|
||||
ref saThread,
|
||||
false,
|
||||
(uint)CreationFlags.CreateSuspended,
|
||||
IntPtr.Zero,
|
||||
null!,
|
||||
ref startinfo,
|
||||
out procinfo))
|
||||
{
|
||||
var error = Marshal.GetLastWin32Error();
|
||||
Debug.WriteLine($"CreateProcess {error}");
|
||||
_ = ResumeThread(procinfo.hThread);
|
||||
CloseHandle(procinfo.hThread);
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
Directory.SetCurrentDirectory(lastDirectory);
|
||||
|
||||
threadId = procinfo.dwThreadId;
|
||||
CloseHandle(procinfo.hThread);
|
||||
CloseHandle(procinfo.hProcess);
|
||||
return procinfo.dwProcessId;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// https://github.com/GregLando113/gwlauncher/blob/master/GW%20Launcher/MulticlientPatch.cs
|
||||
/// </summary>
|
||||
private static bool McPatch(IntPtr processHandle)
|
||||
{
|
||||
byte[] sigPatch =
|
||||
[
|
||||
0x56, 0x57, 0x68, 0x00, 0x01, 0x00, 0x00, 0x89, 0x85, 0xF4, 0xFE, 0xFF, 0xFF, 0xC7, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00
|
||||
];
|
||||
var moduleBase = GetProcessModuleBase(processHandle);
|
||||
var gwdata = new byte[0x48D000];
|
||||
|
||||
if (!NativeMethods.ReadProcessMemory(processHandle, moduleBase, gwdata, gwdata.Length, out _))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var idx = SearchBytes(gwdata, sigPatch);
|
||||
|
||||
if (idx == -1)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var mcpatch = moduleBase + idx - 0x1A;
|
||||
|
||||
byte[] payload = [0x31, 0xC0, 0x90, 0xC3];
|
||||
|
||||
return NativeMethods.WriteProcessMemory(processHandle, mcpatch, payload, payload.Length, out _);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// https://github.com/GregLando113/gwlauncher/blob/master/GW%20Launcher/MulticlientPatch.cs
|
||||
/// </summary>
|
||||
private static int SearchBytes(Memory<byte> haystack, Memory<byte> needle)
|
||||
{
|
||||
var len = needle.Length;
|
||||
var limit = haystack.Length - len;
|
||||
for (var i = 0; i <= limit; i++)
|
||||
{
|
||||
var k = 0;
|
||||
for (; k < len; k++)
|
||||
{
|
||||
if (needle.Span[k] != haystack.Span[i + k])
|
||||
{
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (k == len)
|
||||
{
|
||||
return i;
|
||||
}
|
||||
}
|
||||
|
||||
return -1;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// https://github.com/GregLando113/gwlauncher/blob/master/GW%20Launcher/MulticlientPatch.cs
|
||||
/// </summary>
|
||||
private static IntPtr GetProcessModuleBase(IntPtr process)
|
||||
{
|
||||
if (NativeMethods.NtQueryInformationProcess(process, NativeMethods.ProcessInfoClass.ProcessBasicInformation, out var pbi,
|
||||
Marshal.SizeOf<ProcessBasicInformation>(), out _) != 0)
|
||||
{
|
||||
return IntPtr.Zero;
|
||||
}
|
||||
|
||||
var buffer = new byte[Marshal.SizeOf<PEB>()];
|
||||
|
||||
if (!NativeMethods.ReadProcessMemory(process, pbi.PebBaseAddress, buffer, Marshal.SizeOf<PEB>(), out _))
|
||||
{
|
||||
return IntPtr.Zero;
|
||||
}
|
||||
|
||||
PEB peb = new()
|
||||
{
|
||||
ImageBaseAddress = (IntPtr)BitConverter.ToInt32(buffer, 8)
|
||||
};
|
||||
|
||||
return peb.ImageBaseAddress + 0x1000;
|
||||
}
|
||||
|
||||
private static void SetRegistryValue(string registryPath, string valueName, object newValue)
|
||||
{
|
||||
using var key = Microsoft.Win32.Registry.CurrentUser.CreateSubKey(registryPath);
|
||||
key.SetValue(valueName, newValue, RegistryValueKind.String);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,239 @@
|
||||
using Daybreak.Injector;
|
||||
using Daybreak.Shared.Models;
|
||||
using System.Diagnostics;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
|
||||
static void PrintUsage()
|
||||
{
|
||||
Console.WriteLine("======================================================");
|
||||
Console.WriteLine("Usage: Daybreak.Injector [mode] <params>");
|
||||
Console.WriteLine("Modes: ");
|
||||
Console.WriteLine("- winapi");
|
||||
Console.WriteLine("- stub");
|
||||
Console.WriteLine("- launch");
|
||||
Console.WriteLine("- resume");
|
||||
Console.WriteLine("Examples:");
|
||||
Console.WriteLine("1) Daybreak.Injector winapi 1234 C:\\path\\to\\dll.dll");
|
||||
Console.WriteLine("2) Daybreak.Injector stub 1234 entryPoint C:\\path\\to\\dll.dll");
|
||||
Console.WriteLine("3) Daybreak.Injector launch true C:\\path\\to\\dll.dll arg1 arg2 arg3");
|
||||
Console.WriteLine("4) Daybreak.Injector resume 1234");
|
||||
Console.WriteLine("======================================================");
|
||||
}
|
||||
|
||||
static bool TryParseInjectWinApiArgs(
|
||||
string[] args,
|
||||
[NotNullWhen(true)] out Process? process,
|
||||
[NotNullWhen(true)] out string? dllPath,
|
||||
out InjectorResponses.InjectResult exitCode)
|
||||
{
|
||||
process = default;
|
||||
dllPath = default;
|
||||
if (!int.TryParse(args[1], out var processId))
|
||||
{
|
||||
PrintUsage();
|
||||
exitCode = InjectorResponses.InjectResult.InvalidProcess;
|
||||
return false;
|
||||
}
|
||||
|
||||
if (args.Length < 3)
|
||||
{
|
||||
PrintUsage();
|
||||
exitCode = InjectorResponses.InjectResult.InvalidArgs;
|
||||
return false;
|
||||
}
|
||||
|
||||
dllPath = Path.GetFullPath(args[2]);
|
||||
process = Process.GetProcessById(processId);
|
||||
if (process is null)
|
||||
{
|
||||
Console.WriteLine($"Process {processId} could not be found");
|
||||
exitCode = InjectorResponses.InjectResult.InvalidProcess;
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
if (!File.Exists(dllPath))
|
||||
{
|
||||
Console.WriteLine($"DLL path {dllPath} could not be found");
|
||||
exitCode = InjectorResponses.InjectResult.InvalidDllPath;
|
||||
return false;
|
||||
}
|
||||
|
||||
exitCode = 0;
|
||||
return true;
|
||||
}
|
||||
|
||||
static bool TryParseInjectStubArgs(
|
||||
string[] args,
|
||||
[NotNullWhen(true)] out Process? process,
|
||||
[NotNullWhen(true)] out string? dllPath,
|
||||
[NotNullWhen(true)] out string? entryPoint,
|
||||
out InjectorResponses.InjectResult exitCode)
|
||||
{
|
||||
process = default;
|
||||
dllPath = default;
|
||||
entryPoint = default;
|
||||
if (!int.TryParse(args[1], out var processId))
|
||||
{
|
||||
PrintUsage();
|
||||
exitCode = InjectorResponses.InjectResult.InvalidProcess;
|
||||
return false;
|
||||
}
|
||||
|
||||
if (args.Length < 4)
|
||||
{
|
||||
PrintUsage();
|
||||
exitCode = InjectorResponses.InjectResult.InvalidArgs;
|
||||
return false;
|
||||
}
|
||||
|
||||
entryPoint = args[2];
|
||||
dllPath = Path.GetFullPath(args[3]);
|
||||
process = Process.GetProcessById(processId);
|
||||
if (process is null)
|
||||
{
|
||||
Console.WriteLine($"Process {processId} could not be found");
|
||||
exitCode = InjectorResponses.InjectResult.InvalidProcess;
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
if (!File.Exists(dllPath))
|
||||
{
|
||||
Console.WriteLine($"DLL path {dllPath} could not be found");
|
||||
exitCode = InjectorResponses.InjectResult.InvalidDllPath;
|
||||
return false;
|
||||
}
|
||||
|
||||
exitCode = 0;
|
||||
return true;
|
||||
}
|
||||
|
||||
static bool TryParseLaunchArgs(
|
||||
string[] args,
|
||||
[NotNullWhen(true)] out string? gwPath,
|
||||
[NotNullWhen(true)] out string? gwArgs,
|
||||
[NotNullWhen(true)] out bool? elevated,
|
||||
out InjectorResponses.LaunchResult exitCode)
|
||||
{
|
||||
gwPath = default;
|
||||
gwArgs = default;
|
||||
elevated = default;
|
||||
if (args.Length < 3)
|
||||
{
|
||||
PrintUsage();
|
||||
exitCode = InjectorResponses.LaunchResult.InvalidArgs;
|
||||
return false;
|
||||
}
|
||||
|
||||
gwPath = Path.GetFullPath(args[2]);
|
||||
if (!bool.TryParse(args[1], out var parsedElevated))
|
||||
{
|
||||
PrintUsage();
|
||||
exitCode = InjectorResponses.LaunchResult.InvalidElevated;
|
||||
return false;
|
||||
}
|
||||
|
||||
elevated = parsedElevated;
|
||||
gwArgs = args.Length > 3 ? string.Join(" ", args.Skip(3)) : string.Empty;
|
||||
|
||||
if (!File.Exists(gwPath))
|
||||
{
|
||||
Console.WriteLine($"Guild Wars path {gwPath} could not be found");
|
||||
exitCode = InjectorResponses.LaunchResult.InvalidPath;
|
||||
return false;
|
||||
}
|
||||
|
||||
exitCode = 0;
|
||||
return true;
|
||||
}
|
||||
|
||||
static bool TryParseThreadResumeArgs(
|
||||
string[] args,
|
||||
[NotNullWhen(true)] out IntPtr? threadHwnd,
|
||||
out InjectorResponses.ResumeResult exitCode)
|
||||
{
|
||||
threadHwnd = default;
|
||||
if (args.Length < 2)
|
||||
{
|
||||
PrintUsage();
|
||||
exitCode = InjectorResponses.ResumeResult.InvalidArgs;
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!IntPtr.TryParse(args[1], out var threadInt))
|
||||
{
|
||||
PrintUsage();
|
||||
exitCode = InjectorResponses.ResumeResult.InvalidThreadHandle;
|
||||
return false;
|
||||
}
|
||||
|
||||
threadHwnd = threadInt;
|
||||
exitCode = InjectorResponses.ResumeResult.Success;
|
||||
return true;
|
||||
}
|
||||
|
||||
if (args.Length < 1)
|
||||
{
|
||||
PrintUsage();
|
||||
return (int)InjectorResponses.GenericResults.InvalidArgs;
|
||||
}
|
||||
|
||||
var mode = args[0];
|
||||
|
||||
switch (mode)
|
||||
{
|
||||
case "winapi":
|
||||
{
|
||||
if (!TryParseInjectWinApiArgs(args, out var process, out var dllPath, out var parseResult))
|
||||
{
|
||||
return (int)parseResult;
|
||||
}
|
||||
|
||||
Console.WriteLine($"Starting WinAPI injection. Process {process.Id}. DllPath: {dllPath}");
|
||||
return (int)ProcessInjector.InjectWithApi(process, dllPath);
|
||||
}
|
||||
case "stub":
|
||||
{
|
||||
if (!TryParseInjectStubArgs(args, out var process, out var dllPath, out var entryPoint, out var parseResult))
|
||||
{
|
||||
return (int)parseResult;
|
||||
}
|
||||
|
||||
Console.WriteLine($"Starting stub injection. Process {process.Id}. EntryPoint {entryPoint}. DllPath: {dllPath}");
|
||||
var result = StubInjector.Inject(process, dllPath, entryPoint, out var exitCode);
|
||||
if (result is not InjectorResponses.InjectResult.Success)
|
||||
{
|
||||
return (int)result;
|
||||
}
|
||||
|
||||
Console.WriteLine($"Stub returned {exitCode}");
|
||||
return exitCode;
|
||||
}
|
||||
case "launch":
|
||||
{
|
||||
if (!TryParseLaunchArgs(args, out var gwPath, out var gwArgs, out var elevated, out var parseResult))
|
||||
{
|
||||
return (int)parseResult;
|
||||
}
|
||||
|
||||
Console.WriteLine($"Launching Guild Wars. Path {gwPath}. Elevated {elevated}. Args {gwArgs}");
|
||||
var result = ProcessLauncher.LaunchGuildWars(gwPath, gwArgs, elevated.Value, out var threadHandle, out var processId);
|
||||
Console.WriteLine($"ThreadHandle: {threadHandle}");
|
||||
Console.WriteLine($"ProcessId: {processId}");
|
||||
return (int)result;
|
||||
}
|
||||
case "resume":
|
||||
{
|
||||
if (!TryParseThreadResumeArgs(args, out var threadHwnd, out var parseResult))
|
||||
{
|
||||
return (int)parseResult;
|
||||
}
|
||||
|
||||
Console.WriteLine($"Resuming thread {threadHwnd.Value}");
|
||||
return (int) ThreadResumer.Resume(threadHwnd.Value);
|
||||
}
|
||||
default:
|
||||
PrintUsage();
|
||||
return (int)InjectorResponses.GenericResults.InvalidMode;
|
||||
}
|
||||
@@ -0,0 +1,183 @@
|
||||
using Daybreak.Shared.Models;
|
||||
using Daybreak.Shared.Utils;
|
||||
using Reloaded.Assembler;
|
||||
using System.Diagnostics;
|
||||
using System.Text;
|
||||
|
||||
namespace Daybreak.Injector;
|
||||
|
||||
public static class StubInjector
|
||||
{
|
||||
/// <summary>
|
||||
/// ebp+8 is the dll path pointer
|
||||
/// 0xDEADBEEF is a placeholder to be patched later for LoadLibraryA
|
||||
/// 0xFEEDF00D is a placeholder to be patched later for GetProcAddress
|
||||
/// </summary>
|
||||
const string Asm = @"
|
||||
use32
|
||||
push ebp
|
||||
mov ebp, esp
|
||||
|
||||
mov esi, [ebp+8] ; ESI = &INJECT_DATA (kept unchanged)
|
||||
|
||||
; -------- LoadLibraryA(dllPath) ------------------------------
|
||||
push dword [esi] ; dllPath
|
||||
mov eax, 0xDEADBEEF ; patched → LoadLibraryA
|
||||
call eax ; EAX = hModule
|
||||
|
||||
; -------- GetProcAddress(hModule, funcName) ------------------
|
||||
push dword [esi+4] ; funcName
|
||||
push eax ; hModule (still in EAX)
|
||||
mov eax, 0xFEEDF00D ; patched → GetProcAddress
|
||||
call eax ; EAX = exported EntryPoint
|
||||
|
||||
; -------- call EntryPoint() ----------------------------------
|
||||
call eax ; calls EntryPoint
|
||||
; xor eax, eax ; thread exit-code 0
|
||||
|
||||
; -------- return EntryPoint() response -----------------------
|
||||
leave ; = mov esp, ebp / pop ebp
|
||||
ret 4 ; stdcall: pop lpParameter
|
||||
";
|
||||
|
||||
public static InjectorResponses.InjectResult Inject(Process target, string dllPath, string entryPoint, out int exitCode)
|
||||
{
|
||||
var hProcess = NativeMethods.OpenProcess(
|
||||
NativeMethods.ProcessAccessFlags.All, false, (uint)target.Id);
|
||||
|
||||
exitCode = 0;
|
||||
if (hProcess is 0)
|
||||
{
|
||||
Console.WriteLine($"Failed to inject with stub. Could not open process by id {target.Id}");
|
||||
return InjectorResponses.InjectResult.InvalidProcess;
|
||||
}
|
||||
|
||||
using var assembler = new Assembler();
|
||||
var stubBytes = assembler.Assemble(Asm);
|
||||
|
||||
// patch placeholders
|
||||
var hKernel = NativeMethods.GetModuleHandle("kernel32.dll");
|
||||
if (hKernel is 0)
|
||||
{
|
||||
Console.WriteLine("Failed to inject with stub. Could not get handle of kernel32.dll");
|
||||
return InjectorResponses.InjectResult.InvalidKernel32;
|
||||
}
|
||||
|
||||
var pLoadLibA = NativeMethods.GetProcAddress(hKernel, "LoadLibraryA");
|
||||
if (pLoadLibA is 0)
|
||||
{
|
||||
Console.WriteLine("Failed to inject with stub. Could not get address of LoadLibraryA");
|
||||
return InjectorResponses.InjectResult.InvalidLoadLibraryA;
|
||||
}
|
||||
|
||||
var pGetProc = NativeMethods.GetProcAddress(hKernel, "GetProcAddress");
|
||||
if (pGetProc is 0)
|
||||
{
|
||||
Console.WriteLine("Failed to inject with stub. Could not get address of GetProcAddress");
|
||||
return InjectorResponses.InjectResult.InvalidGetProcAddress;
|
||||
}
|
||||
|
||||
Patch(stubBytes, 0xDEADBEEF, pLoadLibA);
|
||||
Patch(stubBytes, 0xFEEDF00D, pGetProc);
|
||||
|
||||
var dllBytes = Encoding.ASCII.GetBytes(dllPath + '\0');
|
||||
var funcBytes = Encoding.ASCII.GetBytes(entryPoint + '\0');
|
||||
var remoteDll = AllocWrite(hProcess, dllBytes, NativeMethods.MemoryProtection.PAGE_READWRITE);
|
||||
if (remoteDll is 0)
|
||||
{
|
||||
Console.WriteLine("Failed to inject with stub. Could not allocate memory for dll path");
|
||||
return InjectorResponses.InjectResult.DllPathWriteFailed;
|
||||
}
|
||||
|
||||
var remoteFunc = AllocWrite(hProcess, funcBytes, NativeMethods.MemoryProtection.PAGE_READWRITE);
|
||||
if (remoteFunc is 0)
|
||||
{
|
||||
Console.WriteLine("Failed to inject with stub. Could not allocate memory for function name");
|
||||
return InjectorResponses.InjectResult.FunctionNameWriteFailed;
|
||||
}
|
||||
|
||||
var injectData = new byte[IntPtr.Size * 2];
|
||||
BitConverter.GetBytes(remoteDll.ToInt32()).CopyTo(injectData, 0);
|
||||
BitConverter.GetBytes(remoteFunc.ToInt32()).CopyTo(injectData, 4);
|
||||
var remoteStruct = AllocWrite(hProcess, injectData, NativeMethods.MemoryProtection.PAGE_READWRITE);
|
||||
if (remoteStruct is 0)
|
||||
{
|
||||
Console.WriteLine("Failed to inject with stub. Could not allocate memory for injectData");
|
||||
return InjectorResponses.InjectResult.InjectDataWriteFailed;
|
||||
}
|
||||
|
||||
var remoteStub = AllocWrite(hProcess, stubBytes, NativeMethods.MemoryProtection.PAGE_EXECUTE_READ_WRITE);
|
||||
if (remoteStub is 0)
|
||||
{
|
||||
Console.WriteLine("Failed to inject with stub. Could not allocate memory for stub");
|
||||
return InjectorResponses.InjectResult.StubAllocationFailed;
|
||||
}
|
||||
|
||||
var hThread = NativeMethods.CreateRemoteThread(
|
||||
hProcess, IntPtr.Zero, 0,
|
||||
remoteStub,
|
||||
remoteStruct,
|
||||
0, out _);
|
||||
|
||||
if (hThread is 0)
|
||||
{
|
||||
Console.WriteLine("Failed to inject with stub. Could not create remote thread");
|
||||
return InjectorResponses.InjectResult.CreateRemoteThreadFailed;
|
||||
}
|
||||
|
||||
var waitResult = NativeMethods.WaitForSingleObject(hThread, 10000); // Wait up to 10 seconds
|
||||
if (waitResult is 0) // WAIT_OBJECT_0 - thread completed
|
||||
{
|
||||
// Get the thread exit code, which will be our port number
|
||||
if (NativeMethods.GetExitCodeThread(hThread, out var moduleExitCode) > 0)
|
||||
{
|
||||
Console.WriteLine($"Thread completed with result: {moduleExitCode}");
|
||||
exitCode = (int)moduleExitCode;
|
||||
}
|
||||
else
|
||||
{
|
||||
Console.WriteLine("Failed to get thread exit code");
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
Console.WriteLine("Thread did not complete within timeout period");
|
||||
return InjectorResponses.InjectResult.RemoteThreadTimeout;
|
||||
}
|
||||
|
||||
return InjectorResponses.InjectResult.Success;
|
||||
}
|
||||
|
||||
private static void Patch(byte[] blob, uint marker, IntPtr value)
|
||||
{
|
||||
byte m0 = (byte)(marker & 0xFF);
|
||||
byte m1 = (byte)((marker >> 8) & 0xFF);
|
||||
byte m2 = (byte)((marker >> 16) & 0xFF);
|
||||
byte m3 = (byte)((marker >> 24) & 0xFF);
|
||||
|
||||
for (int i = 0; i <= blob.Length - 4; i++)
|
||||
{
|
||||
if (blob[i] == m0 && blob[i + 1] == m1 &&
|
||||
blob[i + 2] == m2 && blob[i + 3] == m3)
|
||||
{
|
||||
BitConverter.GetBytes(value.ToInt32()).CopyTo(blob, i);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
throw new Exception($"Marker 0x{marker:X8} not found");
|
||||
}
|
||||
|
||||
private static IntPtr AllocWrite(IntPtr hProcess, byte[] data,
|
||||
NativeMethods.MemoryProtection protect)
|
||||
{
|
||||
var addr = NativeMethods.VirtualAllocEx(hProcess, IntPtr.Zero,
|
||||
(nint)data.Length,
|
||||
(uint)(NativeMethods.AllocationType.Commit |
|
||||
NativeMethods.AllocationType.Reserve),
|
||||
(uint)protect);
|
||||
NativeMethods.WriteProcessMemory(hProcess, addr, data,
|
||||
data.Length, out _);
|
||||
return addr;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
using Daybreak.Shared.Models;
|
||||
using Daybreak.Shared.Utils;
|
||||
using System.Runtime.InteropServices;
|
||||
|
||||
namespace Daybreak.Injector;
|
||||
|
||||
public static class ThreadResumer
|
||||
{
|
||||
public static InjectorResponses.ResumeResult Resume(IntPtr threadId)
|
||||
{
|
||||
if (threadId == 0)
|
||||
{
|
||||
return InjectorResponses.ResumeResult.InvalidThreadHandle;
|
||||
}
|
||||
|
||||
var threadHandle = NativeMethods.OpenThread(NativeMethods.ThreadAccess.SuspendResume, false, (uint)threadId);
|
||||
if (threadHandle == IntPtr.Zero)
|
||||
{
|
||||
var openErr = Marshal.GetLastWin32Error();
|
||||
Console.WriteLine($"OpenThread failed. Error: {openErr}");
|
||||
return InjectorResponses.ResumeResult.InvalidThreadHandle;
|
||||
}
|
||||
|
||||
var result = NativeMethods.ResumeThread(threadHandle);
|
||||
Console.WriteLine($"ResumeThread result: {result}");
|
||||
NativeMethods.CloseHandle(threadHandle);
|
||||
|
||||
if (result == uint.MaxValue)
|
||||
{
|
||||
var resumeErr = Marshal.GetLastWin32Error();
|
||||
Console.WriteLine($"ResumeThread failed. Error: {resumeErr}");
|
||||
return InjectorResponses.ResumeResult.InvalidThreadHandle;
|
||||
}
|
||||
|
||||
return InjectorResponses.ResumeResult.Success;
|
||||
}
|
||||
}
|
||||
@@ -2,14 +2,15 @@
|
||||
|
||||
<PropertyGroup>
|
||||
<OutputType>Exe</OutputType>
|
||||
<TargetFramework>net9.0</TargetFramework>
|
||||
<TargetFramework>$(TargetFrameworkVersion)</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
<Platforms>AnyCPU;x86</Platforms>
|
||||
<Platforms>x64</Platforms>
|
||||
<Self-Contained>True</Self-Contained>
|
||||
<RuntimeIdentifier>win-x86</RuntimeIdentifier>
|
||||
<RuntimeIdentifier>win-x64</RuntimeIdentifier>
|
||||
<PublishAot>true</PublishAot>
|
||||
<StripSymbols>true</StripSymbols>
|
||||
<PublishSingleFile>true</PublishSingleFile>
|
||||
</PropertyGroup>
|
||||
|
||||
</Project>
|
||||
|
||||
+289
-98
@@ -1,9 +1,9 @@
|
||||
// See https://aka.ms/new-console-template for more information
|
||||
using System.Diagnostics;
|
||||
using System.Diagnostics;
|
||||
using System.IO.Compression;
|
||||
using System.Net;
|
||||
using System.Text;
|
||||
|
||||
var cts = new CancellationTokenSource();
|
||||
const string LatestUrl = "https://github.com/gwdevhub/Daybreak/releases/latest";
|
||||
|
||||
static void RenderProgressBar(int currentStep, int totalSteps, int barSize)
|
||||
{
|
||||
@@ -20,9 +20,278 @@ static void RenderProgressBar(int currentStep, int totalSteps, int barSize)
|
||||
Console.Write($"{pctComplete:P0}"); // Display the percentage completed
|
||||
}
|
||||
|
||||
const string tempFile = "tempfile.zip";
|
||||
const string updatePkg = "update.pkg";
|
||||
const string executableName = "Daybreak.exe";
|
||||
static void CleanWorkingDirectory(string workingDirectory)
|
||||
{
|
||||
if (!Directory.Exists(workingDirectory))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var installerPath = Path.GetFullPath(Path.Combine(workingDirectory, "Installer"));
|
||||
var optionsPath = Path.GetFullPath(Path.Combine(workingDirectory, "Daybreak.options"));
|
||||
var preserve = new HashSet<string>(StringComparer.OrdinalIgnoreCase)
|
||||
{
|
||||
installerPath,
|
||||
optionsPath
|
||||
};
|
||||
var preserveExtensions = new HashSet<string>(StringComparer.OrdinalIgnoreCase)
|
||||
{
|
||||
".tpf"
|
||||
};
|
||||
|
||||
foreach (var entry in Directory.GetFileSystemEntries(workingDirectory))
|
||||
{
|
||||
var fullPath = Path.GetFullPath(entry);
|
||||
if (preserve.Contains(fullPath))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (preserveExtensions.Contains(Path.GetExtension(fullPath)))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
if (Directory.Exists(fullPath))
|
||||
{
|
||||
Directory.Delete(fullPath, true);
|
||||
}
|
||||
else if (File.Exists(fullPath))
|
||||
{
|
||||
File.Delete(fullPath);
|
||||
}
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Console.WriteLine($"Failed to delete {fullPath}.\n{e}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static async ValueTask PerformUpdate(string workingDirectory)
|
||||
{
|
||||
if (!Directory.Exists(workingDirectory))
|
||||
{
|
||||
Console.WriteLine("Working directory does not exist");
|
||||
Console.ReadKey();
|
||||
return;
|
||||
}
|
||||
|
||||
Console.WriteLine($"Working directory: {workingDirectory}");
|
||||
|
||||
var tempFile = Path.GetFullPath("tempfile.zip", workingDirectory);
|
||||
var updatePkg = Path.GetFullPath("update.pkg", workingDirectory);
|
||||
var executableName = Path.GetFullPath("Daybreak.exe", workingDirectory);
|
||||
|
||||
if (File.Exists(updatePkg))
|
||||
{
|
||||
Console.WriteLine("Unpacking files...");
|
||||
|
||||
using var fileStream = new FileStream(updatePkg, FileMode.Open);
|
||||
var copyBuffer = new Memory<byte>(new byte[4096]);
|
||||
while (fileStream.Position < fileStream.Length - 1)
|
||||
{
|
||||
/*
|
||||
* For each file downloaded, we write to the package the following:
|
||||
* Size of the name string
|
||||
* UTF-8 encoded name string
|
||||
* Size of the relative path string
|
||||
* UTF-8 encoded relative path string
|
||||
* Size of the binary
|
||||
* Binary data
|
||||
*/
|
||||
|
||||
var sizeBuffer = copyBuffer[..4];
|
||||
await fileStream.ReadExactlyAsync(sizeBuffer);
|
||||
var fileNameSize = BitConverter.ToInt32(sizeBuffer.Span);
|
||||
var fileNameBuffer = copyBuffer[..fileNameSize];
|
||||
await fileStream.ReadExactlyAsync(fileNameBuffer);
|
||||
var fileName = Encoding.UTF8.GetString(fileNameBuffer.Span);
|
||||
|
||||
RenderProgressBar((int)fileStream.Position, (int)fileStream.Length, 40);
|
||||
|
||||
await fileStream.ReadExactlyAsync(sizeBuffer);
|
||||
var relativePathSize = BitConverter.ToInt32(sizeBuffer.Span);
|
||||
var relativePathBuffer = copyBuffer[..relativePathSize];
|
||||
await fileStream.ReadExactlyAsync(relativePathBuffer);
|
||||
var relativePath = Encoding.UTF8.GetString(relativePathBuffer.Span);
|
||||
|
||||
RenderProgressBar((int)fileStream.Position, (int)fileStream.Length, 40);
|
||||
|
||||
await fileStream.ReadExactlyAsync(sizeBuffer);
|
||||
var binarySize = BitConverter.ToInt32(sizeBuffer.Span);
|
||||
var fileInfo = new FileInfo(relativePath);
|
||||
fileInfo.Directory!.Create();
|
||||
using var destinationStream = new FileStream(Path.GetFullPath(relativePath, workingDirectory), FileMode.Create);
|
||||
while (binarySize > 0)
|
||||
{
|
||||
var toRead = Math.Min(binarySize, copyBuffer.Length);
|
||||
var readBuffer = copyBuffer[..toRead];
|
||||
await fileStream.ReadExactlyAsync(readBuffer);
|
||||
await destinationStream.WriteAsync(readBuffer);
|
||||
binarySize -= toRead;
|
||||
|
||||
RenderProgressBar((int)fileStream.Position, (int)fileStream.Length, 40);
|
||||
}
|
||||
}
|
||||
|
||||
fileStream.Close();
|
||||
fileStream.Dispose();
|
||||
File.Delete(updatePkg);
|
||||
}
|
||||
else if (File.Exists(tempFile))
|
||||
{
|
||||
Console.WriteLine("Unpacking files...");
|
||||
try
|
||||
{
|
||||
ZipFile.ExtractToDirectory(tempFile, Path.GetFullPath(workingDirectory), true);
|
||||
}
|
||||
catch
|
||||
{
|
||||
}
|
||||
|
||||
Console.WriteLine("Deleting package");
|
||||
try
|
||||
{
|
||||
File.Delete(tempFile);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Console.WriteLine($"Failed to delete {tempFile}.\n{e}");
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
Console.ForegroundColor = ConsoleColor.Red;
|
||||
Console.WriteLine("Unable to find launcher package. Aborting installation");
|
||||
Console.ReadKey();
|
||||
return;
|
||||
}
|
||||
|
||||
Console.WriteLine("Launching application");
|
||||
var process = new Process
|
||||
{
|
||||
StartInfo = new ProcessStartInfo
|
||||
{
|
||||
FileName = executableName
|
||||
}
|
||||
};
|
||||
|
||||
if (process.Start() is false)
|
||||
{
|
||||
Console.WriteLine("Failed to launch application");
|
||||
Console.ReadKey();
|
||||
}
|
||||
}
|
||||
|
||||
static async ValueTask PerformFreshInstall(string workingDirectory)
|
||||
{
|
||||
using var handler = new HttpClientHandler
|
||||
{
|
||||
AllowAutoRedirect = false
|
||||
};
|
||||
var client = new HttpClient(handler);
|
||||
using var latestResponse = await client.GetAsync(LatestUrl);
|
||||
|
||||
var isRedirect = latestResponse.StatusCode is HttpStatusCode.Redirect
|
||||
or HttpStatusCode.MovedPermanently
|
||||
or HttpStatusCode.Found
|
||||
or HttpStatusCode.SeeOther
|
||||
or HttpStatusCode.TemporaryRedirect
|
||||
or HttpStatusCode.PermanentRedirect;
|
||||
|
||||
if (!isRedirect)
|
||||
{
|
||||
Console.WriteLine($"Failed to get latest release information. Status: {latestResponse.StatusCode}");
|
||||
Console.ReadKey();
|
||||
return;
|
||||
}
|
||||
|
||||
var redirectTarget = latestResponse.Headers.Location;
|
||||
if (redirectTarget is null)
|
||||
{
|
||||
Console.WriteLine("Latest release redirect location missing.");
|
||||
Console.ReadKey();
|
||||
return;
|
||||
}
|
||||
|
||||
var finalUri = redirectTarget.IsAbsoluteUri
|
||||
? redirectTarget
|
||||
: new Uri(latestResponse.RequestMessage!.RequestUri!, redirectTarget);
|
||||
|
||||
var version = finalUri.Segments.Last().TrimEnd('/');
|
||||
var downloadUri = new Uri($"https://github.com/gwdevhub/Daybreak/releases/download/{version}/daybreak{version}.zip");
|
||||
var tempFile = Path.GetFullPath("tempfile.zip", workingDirectory);
|
||||
|
||||
Console.WriteLine($"Cleaning installation directory {workingDirectory}");
|
||||
CleanWorkingDirectory(workingDirectory);
|
||||
|
||||
Console.WriteLine($"Downloading Daybreak {version}...");
|
||||
|
||||
handler.Dispose();
|
||||
client.Dispose();
|
||||
|
||||
client = new HttpClient();
|
||||
using var downloadResponse = await client.GetAsync(downloadUri, HttpCompletionOption.ResponseHeadersRead);
|
||||
if (!downloadResponse.IsSuccessStatusCode)
|
||||
{
|
||||
Console.WriteLine($"Failed to download Daybreak package. Status: {downloadResponse.StatusCode}");
|
||||
Console.ReadKey();
|
||||
return;
|
||||
}
|
||||
|
||||
var totalBytes = downloadResponse.Content.Headers.ContentLength ?? -1L;
|
||||
using var contentStream = await downloadResponse.Content.ReadAsStreamAsync();
|
||||
using var fileStream = new FileStream(tempFile, FileMode.Create, FileAccess.Write, FileShare.None);
|
||||
var totalRead = 0L;
|
||||
var buffer = new Memory<byte>(new byte[8192]);
|
||||
int bytesRead;
|
||||
while ((bytesRead = await contentStream.ReadAsync(buffer)) > 0)
|
||||
{
|
||||
await fileStream.WriteAsync(buffer[..bytesRead]);
|
||||
totalRead += bytesRead;
|
||||
if (totalBytes != -1)
|
||||
{
|
||||
RenderProgressBar((int)totalRead, (int)totalBytes, 40);
|
||||
}
|
||||
}
|
||||
|
||||
fileStream.Close();
|
||||
fileStream.Dispose();
|
||||
client.Dispose();
|
||||
Console.WriteLine("\nDownload complete. Extracting files...");
|
||||
try
|
||||
{
|
||||
ZipFile.ExtractToDirectory(tempFile, Path.GetFullPath(workingDirectory), true);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Console.WriteLine($"Failed to extract package.\n{e}");
|
||||
Console.ReadKey();
|
||||
return;
|
||||
}
|
||||
|
||||
File.Delete(tempFile);
|
||||
|
||||
Console.WriteLine("Installation complete. Launching application...");
|
||||
var executableName = Path.GetFullPath("Daybreak.exe", workingDirectory);
|
||||
var process = new Process
|
||||
{
|
||||
StartInfo = new ProcessStartInfo
|
||||
{
|
||||
FileName = executableName
|
||||
}
|
||||
};
|
||||
|
||||
if (process.Start() is false)
|
||||
{
|
||||
Console.WriteLine("Failed to launch application");
|
||||
Console.ReadKey();
|
||||
}
|
||||
}
|
||||
|
||||
Console.Title = "Daybreak Installer";
|
||||
Console.WriteLine("Starting installation...");
|
||||
while (Process.GetProcesses().Where(p => p.ProcessName == "Daybreak").FirstOrDefault()?.HasExited is false)
|
||||
@@ -31,102 +300,24 @@ while (Process.GetProcesses().Where(p => p.ProcessName == "Daybreak").FirstOrDef
|
||||
await Task.Delay(5000);
|
||||
}
|
||||
|
||||
if (File.Exists(updatePkg))
|
||||
while (Process.GetProcessesByName("gw").FirstOrDefault()?.HasExited is false)
|
||||
{
|
||||
Console.WriteLine("Unpacking files...");
|
||||
|
||||
using var fileStream = new FileStream(updatePkg, FileMode.Open);
|
||||
var copyBuffer = new Memory<byte>(new byte[4096]);
|
||||
while (fileStream.Position < fileStream.Length - 1)
|
||||
{
|
||||
/*
|
||||
* For each file downloaded, we write to the package the following:
|
||||
* Size of the name string
|
||||
* UTF-8 encoded name string
|
||||
* Size of the relative path string
|
||||
* UTF-8 encoded relative path string
|
||||
* Size of the binary
|
||||
* Binary data
|
||||
*/
|
||||
|
||||
var sizeBuffer = copyBuffer[..4];
|
||||
await fileStream.ReadExactlyAsync(sizeBuffer);
|
||||
var fileNameSize = BitConverter.ToInt32(sizeBuffer.Span);
|
||||
var fileNameBuffer = copyBuffer[..fileNameSize];
|
||||
await fileStream.ReadExactlyAsync(fileNameBuffer);
|
||||
var fileName = Encoding.UTF8.GetString(fileNameBuffer.Span);
|
||||
|
||||
RenderProgressBar((int)fileStream.Position, (int)fileStream.Length, 40);
|
||||
|
||||
await fileStream.ReadExactlyAsync(sizeBuffer);
|
||||
var relativePathSize = BitConverter.ToInt32(sizeBuffer.Span);
|
||||
var relativePathBuffer = copyBuffer[..relativePathSize];
|
||||
await fileStream.ReadExactlyAsync(relativePathBuffer);
|
||||
var relativePath = Encoding.UTF8.GetString(relativePathBuffer.Span);
|
||||
|
||||
RenderProgressBar((int)fileStream.Position, (int)fileStream.Length, 40);
|
||||
|
||||
await fileStream.ReadExactlyAsync(sizeBuffer);
|
||||
var binarySize = BitConverter.ToInt32(sizeBuffer.Span);
|
||||
var fileInfo = new FileInfo(relativePath);
|
||||
fileInfo.Directory!.Create();
|
||||
using var destinationStream = new FileStream(relativePath, FileMode.Create);
|
||||
while(binarySize > 0)
|
||||
{
|
||||
var toRead = Math.Min(binarySize, copyBuffer.Length);
|
||||
var readBuffer = copyBuffer[..toRead];
|
||||
await fileStream.ReadExactlyAsync(readBuffer);
|
||||
await destinationStream.WriteAsync(readBuffer);
|
||||
binarySize -= toRead;
|
||||
|
||||
RenderProgressBar((int)fileStream.Position, (int)fileStream.Length, 40);
|
||||
}
|
||||
}
|
||||
|
||||
fileStream.Close();
|
||||
fileStream.Dispose();
|
||||
File.Delete(updatePkg);
|
||||
Console.WriteLine($"Detected Guild Wars process is still running. Waiting 5s and retrying");
|
||||
await Task.Delay(5000);
|
||||
}
|
||||
else if (File.Exists(tempFile))
|
||||
{
|
||||
Console.WriteLine("Unpacking files...");
|
||||
try
|
||||
{
|
||||
ZipFile.ExtractToDirectory(tempFile, AppContext.BaseDirectory, true);
|
||||
}
|
||||
catch
|
||||
{
|
||||
}
|
||||
|
||||
Console.WriteLine("Deleting package");
|
||||
try
|
||||
{
|
||||
File.Delete(tempFile);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Console.WriteLine($"Failed to delete {tempFile}.\n{e}");
|
||||
}
|
||||
var mode = args.Length > 1
|
||||
? args[0] : "install";
|
||||
var workingDirectory = args.Length > 2
|
||||
? args[1] : Path.GetFullPath("..", AppContext.BaseDirectory);
|
||||
|
||||
// Normalize working directory path
|
||||
workingDirectory = Path.GetFullPath(workingDirectory);
|
||||
if (mode is "install")
|
||||
{
|
||||
await PerformFreshInstall(workingDirectory);
|
||||
}
|
||||
else
|
||||
{
|
||||
Console.ForegroundColor = ConsoleColor.Red;
|
||||
Console.WriteLine("Unable to find launcher package. Aborting installation");
|
||||
Console.ReadKey();
|
||||
return;
|
||||
}
|
||||
|
||||
Console.WriteLine("Launching application");
|
||||
var process = new Process
|
||||
{
|
||||
StartInfo = new ProcessStartInfo
|
||||
{
|
||||
FileName = executableName
|
||||
}
|
||||
};
|
||||
|
||||
if (process.Start() is false)
|
||||
{
|
||||
Console.WriteLine("Failed to launch application");
|
||||
Console.ReadKey();
|
||||
await PerformUpdate(workingDirectory);
|
||||
}
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
using Daybreak.Shared.Validators;
|
||||
|
||||
namespace Daybreak.Attributes;
|
||||
namespace Daybreak.Shared.Attributes;
|
||||
|
||||
[AttributeUsage(AttributeTargets.Property)]
|
||||
public sealed class OptionCustomValidatorAttribute<T> : Attribute
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
namespace Daybreak.Attributes;
|
||||
namespace Daybreak.Shared.Attributes;
|
||||
|
||||
[AttributeUsage(AttributeTargets.Property)]
|
||||
public sealed class OptionIgnoreAttribute : Attribute
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
namespace Daybreak.Attributes;
|
||||
namespace Daybreak.Shared.Attributes;
|
||||
|
||||
[AttributeUsage(AttributeTargets.Property)]
|
||||
public sealed class OptionNameAttribute : Attribute
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
namespace Daybreak.Attributes;
|
||||
namespace Daybreak.Shared.Attributes;
|
||||
|
||||
[AttributeUsage(AttributeTargets.Property)]
|
||||
public sealed class OptionRangeAttribute<T> : Attribute
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
namespace Daybreak.Attributes;
|
||||
namespace Daybreak.Shared.Attributes;
|
||||
|
||||
[AttributeUsage(AttributeTargets.Property)]
|
||||
public sealed class OptionSynchronizationIgnoreAttribute : Attribute
|
||||
@@ -0,0 +1,7 @@
|
||||
namespace Daybreak.Shared.Attributes;
|
||||
|
||||
[AttributeUsage(AttributeTargets.Property)]
|
||||
public class OptionValuesAttribute : Attribute
|
||||
{
|
||||
public required object[] Values { get; init; }
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
namespace Daybreak.Shared.Attributes;
|
||||
|
||||
[AttributeUsage(AttributeTargets.Property)]
|
||||
public abstract class OptionValuesFactoryAttribute : Attribute
|
||||
{
|
||||
public abstract List<object> ValuesFactory();
|
||||
}
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
namespace Daybreak.Attributes;
|
||||
namespace Daybreak.Shared.Attributes;
|
||||
|
||||
[AttributeUsage(AttributeTargets.Class)]
|
||||
public sealed class OptionsIgnoreAttribute : Attribute
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
namespace Daybreak.Attributes;
|
||||
namespace Daybreak.Shared.Attributes;
|
||||
|
||||
[AttributeUsage(AttributeTargets.Class)]
|
||||
public sealed class OptionsNameAttribute : Attribute
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
namespace Daybreak.Attributes;
|
||||
namespace Daybreak.Shared.Attributes;
|
||||
|
||||
[AttributeUsage(AttributeTargets.Class)]
|
||||
public sealed class OptionsSynchronizationIgnoreAttribute : Attribute
|
||||
@@ -1,50 +0,0 @@
|
||||
using Newtonsoft.Json;
|
||||
|
||||
namespace Daybreak.Shared.Converters;
|
||||
public sealed class AttributeJsonConverter : JsonConverter
|
||||
{
|
||||
public override bool CanConvert(Type objectType) => true;
|
||||
|
||||
public override object? ReadJson(JsonReader reader, Type objectType, object? existingValue, JsonSerializer serializer)
|
||||
{
|
||||
if (!objectType.IsAssignableTo(typeof(Models.Guildwars.Attribute)))
|
||||
{
|
||||
return default;
|
||||
}
|
||||
|
||||
switch (reader.TokenType)
|
||||
{
|
||||
case JsonToken.String:
|
||||
var name = reader.ReadAsString();
|
||||
if (name is null ||
|
||||
!Models.Guildwars.Attribute.TryParse(name, out var namedAttribute))
|
||||
{
|
||||
return default;
|
||||
}
|
||||
|
||||
return namedAttribute;
|
||||
case JsonToken.Integer:
|
||||
var id = reader.Value as long?;
|
||||
if (id is not long ||
|
||||
!Models.Guildwars.Attribute.TryParse((int)id.Value, out var parsedAttribute))
|
||||
{
|
||||
return default;
|
||||
}
|
||||
|
||||
return parsedAttribute;
|
||||
|
||||
default:
|
||||
return default;
|
||||
}
|
||||
}
|
||||
|
||||
public override void WriteJson(JsonWriter writer, object? value, JsonSerializer serializer)
|
||||
{
|
||||
if (value is not Models.Guildwars.Attribute attribute)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
writer.WriteValue(attribute.Id);
|
||||
}
|
||||
}
|
||||
@@ -1,32 +0,0 @@
|
||||
using System.Globalization;
|
||||
using System.Windows.Data;
|
||||
using System.Windows.Media.Effects;
|
||||
|
||||
namespace Daybreak.Shared.Converters;
|
||||
public sealed class BooleanToEffectConverter : IValueConverter
|
||||
{
|
||||
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
|
||||
{
|
||||
if (value is not bool boolean)
|
||||
{
|
||||
return default!;
|
||||
}
|
||||
|
||||
if (parameter is not Effect effect)
|
||||
{
|
||||
return default!;
|
||||
}
|
||||
|
||||
if (!boolean)
|
||||
{
|
||||
return default!;
|
||||
}
|
||||
|
||||
return effect;
|
||||
}
|
||||
|
||||
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
}
|
||||
@@ -1,42 +0,0 @@
|
||||
using System.Extensions;
|
||||
using System.Globalization;
|
||||
using System.Windows;
|
||||
using System.Windows.Data;
|
||||
|
||||
namespace Daybreak.Shared.Converters;
|
||||
|
||||
public sealed class BooleanToGridLengthConverter : IValueConverter
|
||||
{
|
||||
private static GridLength Collapsed = new(0);
|
||||
|
||||
public GridLength VisibleValue { get; set; }
|
||||
|
||||
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
|
||||
{
|
||||
return this.GetVisibility(value);
|
||||
}
|
||||
|
||||
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
|
||||
private GridLength GetVisibility(object value)
|
||||
{
|
||||
if (value is not bool)
|
||||
{
|
||||
return this.VisibleValue;
|
||||
}
|
||||
|
||||
var objValue = value.Cast<bool>();
|
||||
if (objValue)
|
||||
{
|
||||
return this.VisibleValue;
|
||||
}
|
||||
else
|
||||
{
|
||||
return Collapsed;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,52 +0,0 @@
|
||||
using System.Extensions;
|
||||
using System.Globalization;
|
||||
using System.Windows;
|
||||
using System.Windows.Data;
|
||||
|
||||
namespace Daybreak.Shared.Converters;
|
||||
|
||||
public class BooleanToVisibilityConverter : IValueConverter
|
||||
{
|
||||
/// <summary>
|
||||
/// Set to true if you want to show control when boolean value is true.
|
||||
/// Set to false if you want to hide/collapse control when value is true.
|
||||
/// </summary>
|
||||
public bool TriggerValue { get; set; } = true;
|
||||
/// <summary>
|
||||
/// Set to true if you just want to hide the control, else set to false if you want to collapse the control.
|
||||
/// </summary>
|
||||
public bool IsHidden { get; set; }
|
||||
|
||||
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
|
||||
{
|
||||
return this.GetVisibility(value);
|
||||
}
|
||||
|
||||
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
|
||||
private object GetVisibility(object value)
|
||||
{
|
||||
if (value is not bool)
|
||||
{
|
||||
return this.IsHidden ?
|
||||
Visibility.Hidden :
|
||||
Visibility.Collapsed;
|
||||
}
|
||||
|
||||
var objValue = value.Cast<bool>();
|
||||
if ((objValue && this.TriggerValue && this.IsHidden) || (!objValue && !this.TriggerValue && this.IsHidden))
|
||||
{
|
||||
return Visibility.Hidden;
|
||||
}
|
||||
|
||||
if ((objValue && this.TriggerValue && !this.IsHidden) || (!objValue && !this.TriggerValue && !this.IsHidden))
|
||||
{
|
||||
return Visibility.Collapsed;
|
||||
}
|
||||
|
||||
return Visibility.Visible;
|
||||
}
|
||||
}
|
||||
@@ -1,51 +0,0 @@
|
||||
using Daybreak.Shared.Models.Guildwars;
|
||||
using Newtonsoft.Json;
|
||||
|
||||
namespace Daybreak.Shared.Converters;
|
||||
public sealed class CampaignJsonConverter : JsonConverter
|
||||
{
|
||||
public override bool CanConvert(Type objectType) => true;
|
||||
|
||||
public override object? ReadJson(JsonReader reader, Type objectType, object? existingValue, JsonSerializer serializer)
|
||||
{
|
||||
if (!objectType.IsAssignableTo(typeof(Campaign)))
|
||||
{
|
||||
return default;
|
||||
}
|
||||
|
||||
switch (reader.TokenType)
|
||||
{
|
||||
case JsonToken.String:
|
||||
var name = reader.ReadAsString();
|
||||
if (name is null ||
|
||||
!Campaign.TryParse(name, out var namedCampaign))
|
||||
{
|
||||
return default;
|
||||
}
|
||||
|
||||
return namedCampaign;
|
||||
case JsonToken.Integer:
|
||||
var id = reader.Value as long?;
|
||||
if (id is not long ||
|
||||
!Campaign.TryParse((int)id.Value, out var parsedCampaign))
|
||||
{
|
||||
return default;
|
||||
}
|
||||
|
||||
return parsedCampaign;
|
||||
|
||||
default:
|
||||
return default;
|
||||
}
|
||||
}
|
||||
|
||||
public override void WriteJson(JsonWriter writer, object? value, JsonSerializer serializer)
|
||||
{
|
||||
if (value is not Campaign campaign)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
writer.WriteValue(campaign.Id);
|
||||
}
|
||||
}
|
||||
@@ -1,51 +0,0 @@
|
||||
using Daybreak.Shared.Models.Guildwars;
|
||||
using Newtonsoft.Json;
|
||||
|
||||
namespace Daybreak.Shared.Converters;
|
||||
public sealed class ContinentJsonConverter : JsonConverter
|
||||
{
|
||||
public override bool CanConvert(Type objectType) => true;
|
||||
|
||||
public override object? ReadJson(JsonReader reader, Type objectType, object? existingValue, JsonSerializer serializer)
|
||||
{
|
||||
if (!objectType.IsAssignableTo(typeof(Continent)))
|
||||
{
|
||||
return default;
|
||||
}
|
||||
|
||||
switch (reader.TokenType)
|
||||
{
|
||||
case JsonToken.String:
|
||||
var name = reader.ReadAsString();
|
||||
if (name is null ||
|
||||
!Continent.TryParse(name, out var namedContinent))
|
||||
{
|
||||
return default;
|
||||
}
|
||||
|
||||
return namedContinent;
|
||||
case JsonToken.Integer:
|
||||
var id = reader.Value as long?;
|
||||
if (id is not long ||
|
||||
!Continent.TryParse((int)id.Value, out var parsedContinent))
|
||||
{
|
||||
return default;
|
||||
}
|
||||
|
||||
return parsedContinent;
|
||||
|
||||
default:
|
||||
return default;
|
||||
}
|
||||
}
|
||||
|
||||
public override void WriteJson(JsonWriter writer, object? value, JsonSerializer serializer)
|
||||
{
|
||||
if (value is not Continent continent)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
writer.WriteValue(continent.Id);
|
||||
}
|
||||
}
|
||||
@@ -1,40 +0,0 @@
|
||||
using System.Globalization;
|
||||
using System.Windows.Data;
|
||||
|
||||
namespace Daybreak.Shared.Converters;
|
||||
public sealed class DateTimeConverter : IValueConverter
|
||||
{
|
||||
public string? Format { get; set; }
|
||||
public bool LongTime { get; set; }
|
||||
public bool LongDate { get; set; }
|
||||
|
||||
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
|
||||
{
|
||||
if (value is not DateTime dateTime)
|
||||
{
|
||||
return string.Empty;
|
||||
}
|
||||
|
||||
if (this.Format is not null)
|
||||
{
|
||||
return dateTime.ToString(this.Format);
|
||||
}
|
||||
|
||||
if (this.LongTime)
|
||||
{
|
||||
return dateTime.ToLongTimeString();
|
||||
}
|
||||
|
||||
if (this.LongDate)
|
||||
{
|
||||
return dateTime.ToLongDateString();
|
||||
}
|
||||
|
||||
return dateTime.ToShortDateString();
|
||||
}
|
||||
|
||||
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
|
||||
{
|
||||
throw new InvalidOperationException();
|
||||
}
|
||||
}
|
||||
@@ -1,23 +0,0 @@
|
||||
using System.Globalization;
|
||||
using System.Windows.Data;
|
||||
|
||||
namespace Daybreak.Shared.Converters;
|
||||
public sealed class DoubleMultiplierConverter : IValueConverter
|
||||
{
|
||||
public double Multiplier { get; set; }
|
||||
|
||||
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
|
||||
{
|
||||
if (value is not double d)
|
||||
{
|
||||
return value;
|
||||
}
|
||||
|
||||
return d * this.Multiplier;
|
||||
}
|
||||
|
||||
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
using System.ComponentModel;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Globalization;
|
||||
|
||||
namespace Daybreak.Shared.Converters;
|
||||
|
||||
public sealed class EnumToStringConverter<TEnumType> : TypeConverter
|
||||
where TEnumType : struct, Enum
|
||||
{
|
||||
public override bool CanConvertFrom(ITypeDescriptorContext? context, Type sourceType)
|
||||
{
|
||||
return sourceType == typeof(string) || base.CanConvertFrom(context, sourceType);
|
||||
}
|
||||
|
||||
public override bool CanConvertTo(ITypeDescriptorContext? context, [NotNullWhen(true)] Type? destinationType)
|
||||
{
|
||||
return destinationType == typeof(string) || base.CanConvertTo(context, destinationType);
|
||||
}
|
||||
|
||||
public override object? ConvertFrom(ITypeDescriptorContext? context, CultureInfo? culture, object value)
|
||||
{
|
||||
if (value is string stringValue && Enum.TryParse<TEnumType>(stringValue, out var enumValue))
|
||||
{
|
||||
return enumValue;
|
||||
}
|
||||
|
||||
return base.ConvertFrom(context, culture, value);
|
||||
}
|
||||
|
||||
public override object? ConvertTo(ITypeDescriptorContext? context, CultureInfo? culture, object? value, Type destinationType)
|
||||
{
|
||||
if (destinationType == typeof(string) && value is TEnumType enumValue)
|
||||
{
|
||||
return enumValue.ToString();
|
||||
}
|
||||
|
||||
return base.ConvertTo(context, culture, value, destinationType);
|
||||
}
|
||||
}
|
||||
@@ -1,24 +0,0 @@
|
||||
using System.Globalization;
|
||||
using System.Windows;
|
||||
using System.Windows.Data;
|
||||
|
||||
namespace Daybreak.Shared.Converters;
|
||||
|
||||
public sealed class EqualityToVisibilityConverter : IMultiValueConverter
|
||||
{
|
||||
public object Convert(object[] values, Type targetType, object parameter, CultureInfo culture)
|
||||
{
|
||||
if (values.Length != 2)
|
||||
{
|
||||
return Visibility.Collapsed;
|
||||
}
|
||||
|
||||
return values[0] == values[1] ? Visibility.Visible : Visibility.Collapsed;
|
||||
}
|
||||
|
||||
public object[] ConvertBack(object value, Type[] targetTypes, object parameter, CultureInfo culture)
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,68 +0,0 @@
|
||||
using Daybreak.Shared.Models.Guildwars;
|
||||
using Daybreak.Shared.Services.Events;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using System.Globalization;
|
||||
using System.Windows;
|
||||
using System.Windows.Controls;
|
||||
using System.Windows.Data;
|
||||
using System.Windows.Media;
|
||||
using System.Windows.Shapes;
|
||||
|
||||
namespace Daybreak.Shared.Converters;
|
||||
public sealed class EventCalendarDayToBrushConverter : IValueConverter
|
||||
{
|
||||
private readonly EventToBrushConverter eventToBrushConverter = new();
|
||||
private readonly IEventService eventService = Global.GlobalServiceProvider.GetRequiredService<IEventService>();
|
||||
|
||||
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
|
||||
{
|
||||
if (value is not DateTime dayDate)
|
||||
{
|
||||
throw new InvalidOperationException($"Unable to convert from {value.GetType().Name}");
|
||||
}
|
||||
|
||||
dayDate = new DateTime(dayDate.Year, dayDate.Month, dayDate.Day, 19, 0, 0);
|
||||
var activeEvents = this.eventService.GetActiveEvents(dayDate).ToList();
|
||||
if (activeEvents.Count == 0)
|
||||
{
|
||||
return Brushes.Transparent;
|
||||
}
|
||||
|
||||
if (activeEvents.Count == 1 &&
|
||||
activeEvents.FirstOrDefault() is Event ev)
|
||||
{
|
||||
return this.eventToBrushConverter.Convert(ev, typeof(Brush), parameter, culture);
|
||||
}
|
||||
|
||||
var brushes = this.eventService.GetActiveEvents(dayDate).Select(e => this.eventToBrushConverter.Convert(e, typeof(Brush), parameter, culture)).OfType<SolidColorBrush>().ToList();
|
||||
var canvas = new Canvas();
|
||||
double width = 1.0 / brushes.Count; // Assuming a normalized width for each color segment
|
||||
|
||||
for (int i = 0; i < brushes.Count; i++)
|
||||
{
|
||||
var rect = new Rectangle
|
||||
{
|
||||
Width = width,
|
||||
Height = 1,
|
||||
Fill = brushes[i],
|
||||
RenderTransform = new TranslateTransform(i * width, 0)
|
||||
};
|
||||
canvas.Children.Add(rect);
|
||||
}
|
||||
|
||||
return new VisualBrush
|
||||
{
|
||||
Visual = canvas,
|
||||
Stretch = Stretch.None,
|
||||
TileMode = TileMode.None,
|
||||
Viewbox = new Rect(0, 0, 1, 1),
|
||||
ViewboxUnits = BrushMappingMode.RelativeToBoundingBox,
|
||||
Transform = new RotateTransform(45, 0.5, 0.5)
|
||||
};
|
||||
}
|
||||
|
||||
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
}
|
||||
@@ -1,44 +0,0 @@
|
||||
using Daybreak.Shared.Models;
|
||||
using Daybreak.Shared.Models.Guildwars;
|
||||
using System.Globalization;
|
||||
using System.Windows.Data;
|
||||
using System.Windows.Media;
|
||||
using Brush = System.Windows.Media.Brush;
|
||||
|
||||
namespace Daybreak.Shared.Converters;
|
||||
public sealed class EventToBrushConverter : IValueConverter
|
||||
{
|
||||
private static readonly Dictionary<Event, Brush> EventMapping = new()
|
||||
{
|
||||
{ Event.CanthanNewYear, new SolidColorBrush(ColorPalette.Amber) },
|
||||
{ Event.LuckyTreatsWeek, new SolidColorBrush(ColorPalette.Green) },
|
||||
{ Event.AprilFoolsDay, new SolidColorBrush(ColorPalette.DeepPurple) },
|
||||
{ Event.SweetTreatsWeek, new SolidColorBrush(ColorPalette.Indigo) },
|
||||
{ Event.AnniversaryCelebration, new SolidColorBrush(ColorPalette.Yellow) },
|
||||
{ Event.DragonFestival, new SolidColorBrush(ColorPalette.DeepOrange) },
|
||||
{ Event.WintersdayInJuly, new SolidColorBrush(ColorPalette.LightBlue) },
|
||||
{ Event.WayfarersReverie, new SolidColorBrush(ColorPalette.Magenta) },
|
||||
{ Event.PirateWeek, new SolidColorBrush(ColorPalette.Gold) },
|
||||
{ Event.BreastCancerAwarenessMonth, new SolidColorBrush(ColorPalette.Pink) },
|
||||
{ Event.Halloween, new SolidColorBrush(ColorPalette.Orange) },
|
||||
{ Event.SpecialTreatsWeek, new SolidColorBrush(ColorPalette.Lime) },
|
||||
{ Event.WintersdayJanuary, new SolidColorBrush(ColorPalette.BlueGrey) },
|
||||
{ Event.WintersdayDecember, new SolidColorBrush(ColorPalette.BlueGrey) }
|
||||
};
|
||||
|
||||
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
|
||||
{
|
||||
if (value is not Event e ||
|
||||
targetType != typeof(Brush))
|
||||
{
|
||||
throw new InvalidOperationException($"Unable to convert from {value.GetType().Name} to {targetType.Name}");
|
||||
}
|
||||
|
||||
return EventMapping[e];
|
||||
}
|
||||
|
||||
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace Daybreak.Shared.Converters;
|
||||
|
||||
public sealed class HexIntConverter : JsonConverter<int>
|
||||
{
|
||||
public override int Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
|
||||
{
|
||||
if (reader.TokenType == JsonTokenType.String)
|
||||
{
|
||||
var hexString = reader.GetString();
|
||||
if (hexString is not null && hexString.StartsWith("0x", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return Convert.ToInt32(hexString[2..], 16);
|
||||
}
|
||||
}
|
||||
|
||||
return reader.GetInt32();
|
||||
}
|
||||
|
||||
public override void Write(Utf8JsonWriter writer, int value, JsonSerializerOptions options)
|
||||
{
|
||||
writer.WriteStringValue($"0x{value:X4}");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace Daybreak.Shared.Converters;
|
||||
|
||||
public sealed class HexUIntArrayJsonConverter : JsonConverter<uint[]>
|
||||
{
|
||||
private static readonly HexUintConverter ElementConverter = new();
|
||||
|
||||
public override uint[]? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
|
||||
{
|
||||
if (reader.TokenType != JsonTokenType.StartArray)
|
||||
{
|
||||
throw new JsonException();
|
||||
}
|
||||
|
||||
var list = new List<uint>();
|
||||
while (reader.Read() && reader.TokenType != JsonTokenType.EndArray)
|
||||
{
|
||||
list.Add(ElementConverter.Read(ref reader, typeof(uint), options));
|
||||
}
|
||||
|
||||
return [.. list];
|
||||
}
|
||||
|
||||
public override void Write(Utf8JsonWriter writer, uint[] value, JsonSerializerOptions options)
|
||||
{
|
||||
writer.WriteStartArray();
|
||||
foreach (var item in value)
|
||||
{
|
||||
ElementConverter.Write(writer, item, options);
|
||||
}
|
||||
|
||||
writer.WriteEndArray();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace Daybreak.Shared.Converters;
|
||||
|
||||
public sealed class HexUintConverter : JsonConverter<uint>
|
||||
{
|
||||
public override uint Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
|
||||
{
|
||||
if (reader.TokenType == JsonTokenType.String)
|
||||
{
|
||||
var hexString = reader.GetString();
|
||||
if (hexString is not null && hexString.StartsWith("0x", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return Convert.ToUInt32(hexString[2..], 16);
|
||||
}
|
||||
}
|
||||
|
||||
return reader.GetUInt32();
|
||||
}
|
||||
|
||||
public override void Write(Utf8JsonWriter writer, uint value, JsonSerializerOptions options)
|
||||
{
|
||||
writer.WriteStringValue($"0x{value:X4}");
|
||||
}
|
||||
}
|
||||
@@ -1,30 +0,0 @@
|
||||
using System.Globalization;
|
||||
using System.Windows;
|
||||
using System.Windows.Data;
|
||||
|
||||
namespace Daybreak.Shared.Converters;
|
||||
|
||||
public class HiddenWhenNull : IValueConverter
|
||||
{
|
||||
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
|
||||
{
|
||||
return GetVerticalAlignment(value);
|
||||
}
|
||||
|
||||
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
|
||||
private static object GetVerticalAlignment(object value)
|
||||
{
|
||||
if (value is null)
|
||||
{
|
||||
return Visibility.Hidden;
|
||||
}
|
||||
else
|
||||
{
|
||||
return Visibility.Visible;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,43 +0,0 @@
|
||||
using System.Globalization;
|
||||
using System.Windows.Data;
|
||||
|
||||
namespace Daybreak.Shared.Converters;
|
||||
|
||||
public sealed class IntToStringConverter : IValueConverter
|
||||
{
|
||||
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
|
||||
{
|
||||
if (targetType == typeof(string) &&
|
||||
value is int i)
|
||||
{
|
||||
return i.ToString();
|
||||
}
|
||||
else if(targetType == typeof(int) &&
|
||||
value is string s)
|
||||
{
|
||||
return int.Parse(s);
|
||||
}
|
||||
else
|
||||
{
|
||||
throw new InvalidOperationException($"Unable to convert from {value.GetType().Name} to {targetType.Name}");
|
||||
}
|
||||
}
|
||||
|
||||
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
|
||||
{
|
||||
if (targetType == typeof(string) &&
|
||||
value is int i)
|
||||
{
|
||||
return i.ToString();
|
||||
}
|
||||
else if (targetType == typeof(int) &&
|
||||
value is string s)
|
||||
{
|
||||
return int.Parse(s);
|
||||
}
|
||||
else
|
||||
{
|
||||
throw new InvalidOperationException($"Unable to convert from {value.GetType().Name} to {targetType.Name}");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,23 +0,0 @@
|
||||
using System.Windows.Data;
|
||||
|
||||
namespace Daybreak.Shared.Converters;
|
||||
|
||||
public class InverseBooleanConverter : IValueConverter
|
||||
{
|
||||
public object Convert(object value, Type targetType, object parameter,
|
||||
System.Globalization.CultureInfo culture)
|
||||
{
|
||||
if (targetType != typeof(bool))
|
||||
{
|
||||
throw new InvalidOperationException("The target must be a boolean");
|
||||
}
|
||||
|
||||
return !(bool)value;
|
||||
}
|
||||
|
||||
public object ConvertBack(object value, Type targetType, object parameter,
|
||||
System.Globalization.CultureInfo culture)
|
||||
{
|
||||
throw new NotSupportedException();
|
||||
}
|
||||
}
|
||||
@@ -1,51 +0,0 @@
|
||||
using Daybreak.Shared.Models.Guildwars;
|
||||
using Newtonsoft.Json;
|
||||
|
||||
namespace Daybreak.Shared.Converters;
|
||||
public sealed class ItemBaseJsonConverter : JsonConverter
|
||||
{
|
||||
public override bool CanConvert(Type objectType) => true;
|
||||
|
||||
public override object? ReadJson(JsonReader reader, Type objectType, object? existingValue, JsonSerializer serializer)
|
||||
{
|
||||
if (!objectType.IsAssignableTo(typeof(ItemBase)))
|
||||
{
|
||||
return default;
|
||||
}
|
||||
|
||||
switch (reader.TokenType)
|
||||
{
|
||||
case JsonToken.String:
|
||||
var name = reader.ReadAsString();
|
||||
if (name is null ||
|
||||
!ItemBase.TryParse(name, out var namedItem))
|
||||
{
|
||||
return default;
|
||||
}
|
||||
|
||||
return namedItem;
|
||||
case JsonToken.Integer:
|
||||
var id = reader.Value as long?;
|
||||
if (id is not long ||
|
||||
!ItemBase.TryParse((int)id.Value, null, out var parsedItem))
|
||||
{
|
||||
return default;
|
||||
}
|
||||
|
||||
return parsedItem;
|
||||
|
||||
default:
|
||||
return default;
|
||||
}
|
||||
}
|
||||
|
||||
public override void WriteJson(JsonWriter writer, object? value, JsonSerializer serializer)
|
||||
{
|
||||
if (value is not ItemBase itemBase)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
writer.WriteValue(itemBase.Id);
|
||||
}
|
||||
}
|
||||
@@ -1,35 +0,0 @@
|
||||
using Daybreak.Shared.Models.Guildwars;
|
||||
using System.Globalization;
|
||||
using System.Windows.Data;
|
||||
|
||||
namespace Daybreak.Shared.Converters;
|
||||
public sealed class ItemBaseToIntConverter : IValueConverter
|
||||
{
|
||||
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
|
||||
{
|
||||
if (value is int i)
|
||||
{
|
||||
return ItemBase.Parse(i, modifiers: null);
|
||||
}
|
||||
else if(value is ItemBase itemBase)
|
||||
{
|
||||
return itemBase.Id;
|
||||
}
|
||||
|
||||
throw new InvalidOperationException($"Unable to convert value of type {value.GetType().Name}");
|
||||
}
|
||||
|
||||
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
|
||||
{
|
||||
if (value is int i)
|
||||
{
|
||||
return ItemBase.Parse(i, modifiers: null);
|
||||
}
|
||||
else if (value is ItemBase itemBase)
|
||||
{
|
||||
return itemBase.Id;
|
||||
}
|
||||
|
||||
throw new InvalidOperationException($"Unable to convert value of type {value.GetType().Name}");
|
||||
}
|
||||
}
|
||||
@@ -1,35 +0,0 @@
|
||||
using Daybreak.Shared.Models.Guildwars;
|
||||
using System.Globalization;
|
||||
using System.Windows.Data;
|
||||
|
||||
namespace Daybreak.Shared.Converters;
|
||||
public sealed class ItemBaseToStringConverter : IValueConverter
|
||||
{
|
||||
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
|
||||
{
|
||||
if (value is string s)
|
||||
{
|
||||
return ItemBase.Parse(s);
|
||||
}
|
||||
else if(value is ItemBase itemBase)
|
||||
{
|
||||
return itemBase.Name ?? string.Empty;
|
||||
}
|
||||
|
||||
throw new InvalidOperationException($"Unable to convert value of type {value.GetType().Name}");
|
||||
}
|
||||
|
||||
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
|
||||
{
|
||||
if (value is string s)
|
||||
{
|
||||
return ItemBase.Parse(s);
|
||||
}
|
||||
else if (value is ItemBase itemBase)
|
||||
{
|
||||
return itemBase.Name ?? string.Empty;
|
||||
}
|
||||
|
||||
throw new InvalidOperationException($"Unable to convert value of type {value.GetType().Name}");
|
||||
}
|
||||
}
|
||||
@@ -1,51 +0,0 @@
|
||||
using Daybreak.Shared.Models.Guildwars;
|
||||
using Newtonsoft.Json;
|
||||
|
||||
namespace Daybreak.Shared.Converters;
|
||||
public sealed class MapJsonConverter : JsonConverter
|
||||
{
|
||||
public override bool CanConvert(Type objectType) => true;
|
||||
|
||||
public override object? ReadJson(JsonReader reader, Type objectType, object? existingValue, JsonSerializer serializer)
|
||||
{
|
||||
if (!objectType.IsAssignableTo(typeof(Map)))
|
||||
{
|
||||
return default;
|
||||
}
|
||||
|
||||
switch (reader.TokenType)
|
||||
{
|
||||
case JsonToken.String:
|
||||
var name = reader.ReadAsString();
|
||||
if (name is null ||
|
||||
!Map.TryParse(name, out var namedMap))
|
||||
{
|
||||
return default;
|
||||
}
|
||||
|
||||
return namedMap;
|
||||
case JsonToken.Integer:
|
||||
var id = reader.Value as long?;
|
||||
if (id is not long ||
|
||||
!Map.TryParse((int)id.Value, out var parsedMap))
|
||||
{
|
||||
return default;
|
||||
}
|
||||
|
||||
return parsedMap;
|
||||
|
||||
default:
|
||||
return default;
|
||||
}
|
||||
}
|
||||
|
||||
public override void WriteJson(JsonWriter writer, object? value, JsonSerializer serializer)
|
||||
{
|
||||
if (value is not Map map)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
writer.WriteValue(map.Id);
|
||||
}
|
||||
}
|
||||
@@ -1,33 +0,0 @@
|
||||
using System.Globalization;
|
||||
using System.Windows.Data;
|
||||
|
||||
namespace Daybreak.Shared.Converters;
|
||||
|
||||
public class MenuButtonHighlightConverter : IValueConverter
|
||||
{
|
||||
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
|
||||
{
|
||||
if (targetType == typeof(double) &&
|
||||
value is bool boolean)
|
||||
{
|
||||
return boolean ? 0.4 : 0;
|
||||
}
|
||||
else
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
}
|
||||
|
||||
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
|
||||
{
|
||||
if (targetType == typeof(bool) &&
|
||||
value is double doubleValue)
|
||||
{
|
||||
return doubleValue == 0.4;
|
||||
}
|
||||
else
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,51 +0,0 @@
|
||||
using Daybreak.Shared.Models.Guildwars;
|
||||
using Newtonsoft.Json;
|
||||
|
||||
namespace Daybreak.Shared.Converters;
|
||||
public sealed class NpcJsonConverter : JsonConverter
|
||||
{
|
||||
public override bool CanConvert(Type objectType) => true;
|
||||
|
||||
public override object? ReadJson(JsonReader reader, Type objectType, object? existingValue, JsonSerializer serializer)
|
||||
{
|
||||
if (!objectType.IsAssignableTo(typeof(Npc)))
|
||||
{
|
||||
return default;
|
||||
}
|
||||
|
||||
switch (reader.TokenType)
|
||||
{
|
||||
case JsonToken.String:
|
||||
var name = reader.ReadAsString();
|
||||
if (name is null ||
|
||||
!Npc.TryParse(name, out var namedNpc))
|
||||
{
|
||||
return default;
|
||||
}
|
||||
|
||||
return namedNpc;
|
||||
case JsonToken.Integer:
|
||||
var id = reader.Value as long?;
|
||||
if (id is not long ||
|
||||
!Npc.TryParse((int)id.Value, out var parsedNpc))
|
||||
{
|
||||
return default;
|
||||
}
|
||||
|
||||
return parsedNpc;
|
||||
|
||||
default:
|
||||
return default;
|
||||
}
|
||||
}
|
||||
|
||||
public override void WriteJson(JsonWriter writer, object? value, JsonSerializer serializer)
|
||||
{
|
||||
if (value is not Npc npc)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
writer.WriteValue(npc.Ids.FirstOrDefault());
|
||||
}
|
||||
}
|
||||
@@ -1,11 +0,0 @@
|
||||
using System.Globalization;
|
||||
using System.Windows.Data;
|
||||
using System.Windows;
|
||||
|
||||
namespace Daybreak.Shared.Converters;
|
||||
public sealed class NullToUnsetValueConverter : IValueConverter
|
||||
{
|
||||
public object Convert(object? value, Type targetType, object? parameter, CultureInfo culture) => value ?? DependencyProperty.UnsetValue;
|
||||
|
||||
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) => Binding.DoNothing;
|
||||
}
|
||||
@@ -1,16 +1,8 @@
|
||||
using System.Globalization;
|
||||
using System.Windows.Data;
|
||||
|
||||
namespace Daybreak.Shared.Converters;
|
||||
public sealed class PriceToStringConverter : IValueConverter
|
||||
namespace Daybreak.Shared.Converters;
|
||||
public static class PriceToStringConverter
|
||||
{
|
||||
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
|
||||
public static string Convert(object value)
|
||||
{
|
||||
if (targetType != typeof(string))
|
||||
{
|
||||
throw new InvalidOperationException($"Unable to convert to {targetType.Name}");
|
||||
}
|
||||
|
||||
var price = value switch
|
||||
{
|
||||
byte byteVal => byteVal,
|
||||
@@ -35,9 +27,4 @@ public sealed class PriceToStringConverter : IValueConverter
|
||||
|
||||
return $"{price}{suffix}";
|
||||
}
|
||||
|
||||
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,51 +0,0 @@
|
||||
using Daybreak.Shared.Models.Guildwars;
|
||||
using Newtonsoft.Json;
|
||||
|
||||
namespace Daybreak.Shared.Converters;
|
||||
public sealed class ProfessionJsonConverter : JsonConverter
|
||||
{
|
||||
public override bool CanConvert(Type objectType) => true;
|
||||
|
||||
public override object? ReadJson(JsonReader reader, Type objectType, object? existingValue, JsonSerializer serializer)
|
||||
{
|
||||
if (!objectType.IsAssignableTo(typeof(Profession)))
|
||||
{
|
||||
return default;
|
||||
}
|
||||
|
||||
switch (reader.TokenType)
|
||||
{
|
||||
case JsonToken.String:
|
||||
var name = reader.ReadAsString();
|
||||
if (name is null ||
|
||||
!Profession.TryParse(name, out var namedProfession))
|
||||
{
|
||||
return default;
|
||||
}
|
||||
|
||||
return namedProfession;
|
||||
case JsonToken.Integer:
|
||||
var id = reader.Value as long?;
|
||||
if (id is not long ||
|
||||
!Profession.TryParse((int)id.Value, out var parsedProfession))
|
||||
{
|
||||
return default;
|
||||
}
|
||||
|
||||
return parsedProfession;
|
||||
|
||||
default:
|
||||
return default;
|
||||
}
|
||||
}
|
||||
|
||||
public override void WriteJson(JsonWriter writer, object? value, JsonSerializer serializer)
|
||||
{
|
||||
if (value is not Profession profession)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
writer.WriteValue(profession.Id);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,129 @@
|
||||
using Daybreak.Shared.Models.Guildwars;
|
||||
using System.ComponentModel;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Globalization;
|
||||
|
||||
namespace Daybreak.Shared.Converters;
|
||||
public sealed class ProfessionTypeConverter : TypeConverter
|
||||
{
|
||||
public override bool CanConvertFrom(ITypeDescriptorContext? context, Type sourceType)
|
||||
{
|
||||
return sourceType == typeof(string) ||
|
||||
sourceType == typeof(int) ||
|
||||
sourceType == typeof(uint) ||
|
||||
sourceType == typeof(long) ||
|
||||
sourceType == typeof(ulong) ||
|
||||
sourceType == typeof(short) ||
|
||||
sourceType == typeof(ushort) ||
|
||||
sourceType == typeof(byte) ||
|
||||
sourceType == typeof(sbyte) ||
|
||||
base.CanConvertFrom(context, sourceType);
|
||||
}
|
||||
|
||||
public override bool CanConvertTo(ITypeDescriptorContext? context, [NotNullWhen(true)] Type? destinationType)
|
||||
{
|
||||
return destinationType == typeof(string) ||
|
||||
destinationType == typeof(int) ||
|
||||
destinationType == typeof(uint) ||
|
||||
destinationType == typeof(long) ||
|
||||
destinationType == typeof(ulong) ||
|
||||
destinationType == typeof(short) ||
|
||||
destinationType == typeof(ushort) ||
|
||||
destinationType == typeof(byte) ||
|
||||
destinationType == typeof(sbyte) ||
|
||||
base.CanConvertTo(context, destinationType);
|
||||
}
|
||||
|
||||
public override object? ConvertFrom(ITypeDescriptorContext? context, CultureInfo? culture, object value)
|
||||
{
|
||||
if (value is int intValue)
|
||||
{
|
||||
return Profession.Parse(intValue);
|
||||
}
|
||||
else if (value is uint uintValue)
|
||||
{
|
||||
return Profession.Parse((int)uintValue);
|
||||
}
|
||||
else if (value is long longValue)
|
||||
{
|
||||
return Profession.Parse((int)longValue);
|
||||
}
|
||||
else if (value is ulong ulongValue)
|
||||
{
|
||||
return Profession.Parse((int)ulongValue);
|
||||
}
|
||||
else if (value is short shortValue)
|
||||
{
|
||||
return Profession.Parse(shortValue);
|
||||
}
|
||||
else if (value is ushort ushortValue)
|
||||
{
|
||||
return Profession.Parse(ushortValue);
|
||||
}
|
||||
else if (value is byte byteValue)
|
||||
{
|
||||
return Profession.Parse(byteValue);
|
||||
}
|
||||
else if (value is sbyte sbyteValue)
|
||||
{
|
||||
return Profession.Parse(sbyteValue);
|
||||
}
|
||||
else if (value is string stringValue)
|
||||
{
|
||||
return Profession.Parse(stringValue);
|
||||
}
|
||||
else
|
||||
{
|
||||
return base.ConvertFrom(context, culture, value);
|
||||
}
|
||||
}
|
||||
|
||||
public override object? ConvertTo(ITypeDescriptorContext? context, CultureInfo? culture, object? value, Type destinationType)
|
||||
{
|
||||
if (value is not Profession profession)
|
||||
{
|
||||
return base.ConvertTo(context, culture, value, destinationType);
|
||||
}
|
||||
|
||||
if (destinationType == typeof(int))
|
||||
{
|
||||
return profession.Id;
|
||||
}
|
||||
else if (destinationType == typeof(uint))
|
||||
{
|
||||
return (uint)profession.Id;
|
||||
}
|
||||
else if (destinationType == typeof(long))
|
||||
{
|
||||
return (long)profession.Id;
|
||||
}
|
||||
else if (destinationType == typeof(ulong))
|
||||
{
|
||||
return (ulong)profession.Id;
|
||||
}
|
||||
else if (destinationType == typeof(short))
|
||||
{
|
||||
return (short)profession.Id;
|
||||
}
|
||||
else if (destinationType == typeof(ushort))
|
||||
{
|
||||
return (ushort)profession.Id;
|
||||
}
|
||||
else if (destinationType == typeof(byte))
|
||||
{
|
||||
return (byte)profession.Id;
|
||||
}
|
||||
else if (destinationType == typeof(sbyte))
|
||||
{
|
||||
return (sbyte)profession.Id;
|
||||
}
|
||||
else if (destinationType == typeof(string))
|
||||
{
|
||||
return profession.Name ?? string.Empty;
|
||||
}
|
||||
else
|
||||
{
|
||||
return base.ConvertTo(context, culture, value, destinationType);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,51 +0,0 @@
|
||||
using Daybreak.Shared.Models.Guildwars;
|
||||
using Newtonsoft.Json;
|
||||
|
||||
namespace Daybreak.Shared.Converters;
|
||||
public sealed class QuestJsonConverter : JsonConverter
|
||||
{
|
||||
public override bool CanConvert(Type objectType) => true;
|
||||
|
||||
public override object? ReadJson(JsonReader reader, Type objectType, object? existingValue, JsonSerializer serializer)
|
||||
{
|
||||
if (!objectType.IsAssignableTo(typeof(Quest)))
|
||||
{
|
||||
return default;
|
||||
}
|
||||
|
||||
switch (reader.TokenType)
|
||||
{
|
||||
case JsonToken.String:
|
||||
var name = reader.ReadAsString();
|
||||
if (name is null ||
|
||||
!Quest.TryParse(name, out var namedQuest))
|
||||
{
|
||||
return default;
|
||||
}
|
||||
|
||||
return namedQuest;
|
||||
case JsonToken.Integer:
|
||||
var id = reader.Value as long?;
|
||||
if (id is not long ||
|
||||
!Quest.TryParse((int)id.Value, out var parsedQuest))
|
||||
{
|
||||
return default;
|
||||
}
|
||||
|
||||
return parsedQuest;
|
||||
|
||||
default:
|
||||
return default;
|
||||
}
|
||||
}
|
||||
|
||||
public override void WriteJson(JsonWriter writer, object? value, JsonSerializer serializer)
|
||||
{
|
||||
if (value is not Quest quest)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
writer.WriteValue(quest.Id);
|
||||
}
|
||||
}
|
||||
@@ -1,51 +0,0 @@
|
||||
using Daybreak.Shared.Models.Guildwars;
|
||||
using Newtonsoft.Json;
|
||||
|
||||
namespace Daybreak.Shared.Converters;
|
||||
public sealed class RegionJsonConverter : JsonConverter
|
||||
{
|
||||
public override bool CanConvert(Type objectType) => true;
|
||||
|
||||
public override object? ReadJson(JsonReader reader, Type objectType, object? existingValue, JsonSerializer serializer)
|
||||
{
|
||||
if (!objectType.IsAssignableTo(typeof(Region)))
|
||||
{
|
||||
return default;
|
||||
}
|
||||
|
||||
switch (reader.TokenType)
|
||||
{
|
||||
case JsonToken.String:
|
||||
var name = reader.ReadAsString();
|
||||
if (name is null ||
|
||||
!Region.TryParse(name, out var namedRegion))
|
||||
{
|
||||
return default;
|
||||
}
|
||||
|
||||
return namedRegion;
|
||||
case JsonToken.Integer:
|
||||
var id = reader.Value as long?;
|
||||
if (id is not long ||
|
||||
!Region.TryParse((int)id.Value, out var parsedRegion))
|
||||
{
|
||||
return default;
|
||||
}
|
||||
|
||||
return parsedRegion;
|
||||
|
||||
default:
|
||||
return default;
|
||||
}
|
||||
}
|
||||
|
||||
public override void WriteJson(JsonWriter writer, object? value, JsonSerializer serializer)
|
||||
{
|
||||
if (value is not Region region)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
writer.WriteValue(region.Id);
|
||||
}
|
||||
}
|
||||
@@ -1,51 +0,0 @@
|
||||
using Daybreak.Shared.Models.Guildwars;
|
||||
using Newtonsoft.Json;
|
||||
|
||||
namespace Daybreak.Shared.Converters;
|
||||
public sealed class SkillJsonConverter : JsonConverter
|
||||
{
|
||||
public override bool CanConvert(Type objectType) => true;
|
||||
|
||||
public override object? ReadJson(JsonReader reader, Type objectType, object? existingValue, JsonSerializer serializer)
|
||||
{
|
||||
if (!objectType.IsAssignableTo(typeof(Skill)))
|
||||
{
|
||||
return default;
|
||||
}
|
||||
|
||||
switch (reader.TokenType)
|
||||
{
|
||||
case JsonToken.String:
|
||||
var name = reader.ReadAsString();
|
||||
if (name is null ||
|
||||
!Skill.TryParse(name, out var namedSkill))
|
||||
{
|
||||
return default;
|
||||
}
|
||||
|
||||
return namedSkill;
|
||||
case JsonToken.Integer:
|
||||
var id = reader.Value as long?;
|
||||
if (id is not long ||
|
||||
!Skill.TryParse((int)id.Value, out var parsedSkill))
|
||||
{
|
||||
return default;
|
||||
}
|
||||
|
||||
return parsedSkill;
|
||||
|
||||
default:
|
||||
return default;
|
||||
}
|
||||
}
|
||||
|
||||
public override void WriteJson(JsonWriter writer, object? value, JsonSerializer serializer)
|
||||
{
|
||||
if (value is not Skill skill)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
writer.WriteValue(skill.Id);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
using Daybreak.Shared.Models.Themes;
|
||||
using Daybreak.Shared.Services.Themes;
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace Daybreak.Shared.Converters;
|
||||
public sealed class ThemeJsonConverter : JsonConverter<Theme>
|
||||
{
|
||||
public override Theme? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
|
||||
{
|
||||
return reader.TokenType switch
|
||||
{
|
||||
JsonTokenType.String => reader.GetString() is string themeName
|
||||
? themeName is GameScreenshotsTheme.ThemeName
|
||||
? Theme.Themes.FirstOrDefault(t => t is GameScreenshotsTheme)
|
||||
: Theme.Themes.FirstOrDefault(t => t.Name.Equals(themeName, StringComparison.OrdinalIgnoreCase))
|
||||
: default,
|
||||
_ => default
|
||||
};
|
||||
}
|
||||
|
||||
public override void Write(Utf8JsonWriter writer, Theme value, JsonSerializerOptions options)
|
||||
{
|
||||
writer.WriteStringValue(value.Name);
|
||||
}
|
||||
}
|
||||
@@ -1,33 +0,0 @@
|
||||
using System.Globalization;
|
||||
using System.Windows.Data;
|
||||
|
||||
namespace Daybreak.Shared.Converters;
|
||||
|
||||
public class TileButtonHighlightConverter : IValueConverter
|
||||
{
|
||||
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
|
||||
{
|
||||
if (targetType == typeof(double) &&
|
||||
value is bool boolean)
|
||||
{
|
||||
return boolean ? 1 : 0.4;
|
||||
}
|
||||
else
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
}
|
||||
|
||||
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
|
||||
{
|
||||
if (targetType == typeof(bool) &&
|
||||
value is double doubleValue)
|
||||
{
|
||||
return doubleValue == 1;
|
||||
}
|
||||
else
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,105 +0,0 @@
|
||||
using System.Globalization;
|
||||
using System.Windows.Data;
|
||||
|
||||
namespace Daybreak.Shared.Converters;
|
||||
|
||||
public sealed class TimeSinceDateTimeConverter : IValueConverter
|
||||
{
|
||||
private const char PluralAppend = 's';
|
||||
|
||||
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
|
||||
{
|
||||
return GetTimeString(value) ?? string.Empty;
|
||||
}
|
||||
|
||||
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
|
||||
private static string? GetTimeString(object value)
|
||||
{
|
||||
if (value is not DateTime dateTime)
|
||||
{
|
||||
return default;
|
||||
}
|
||||
|
||||
var difference = DateTime.Now - dateTime;
|
||||
|
||||
// If less than a minute, return seconds
|
||||
if (difference.TotalSeconds < 60)
|
||||
{
|
||||
var totalSeconds = (int)difference.TotalSeconds;
|
||||
return $"{totalSeconds} second{(difference.TotalSeconds > 1 ? PluralAppend : string.Empty)} ago";
|
||||
}
|
||||
|
||||
// If less than an hour, return minutes
|
||||
if (difference.TotalMinutes < 60)
|
||||
{
|
||||
var totalMinutes = (int)difference.TotalMinutes;
|
||||
return $"{totalMinutes} minute{(difference.TotalMinutes > 1 ? PluralAppend : string.Empty)} ago";
|
||||
}
|
||||
|
||||
// If less than a day, return hours
|
||||
if (difference.TotalHours < 24)
|
||||
{
|
||||
var totalHours = (int)difference.TotalHours;
|
||||
return $"{totalHours} hour{(difference.TotalHours > 1 ? PluralAppend : string.Empty)} ago";
|
||||
}
|
||||
|
||||
// If less than a week, return days
|
||||
if (difference.TotalDays < 7)
|
||||
{
|
||||
var totalDays = (int)difference.TotalDays;
|
||||
return $"{totalDays} day{(difference.TotalDays > 1 ? PluralAppend : string.Empty)} ago";
|
||||
}
|
||||
|
||||
// If less than a month, return weeks
|
||||
var numberOfDaysInCurrentMonth = DateTime.DaysInMonth(DateTime.Now.Year, DateTime.Now.Month);
|
||||
if (difference.TotalDays < numberOfDaysInCurrentMonth)
|
||||
{
|
||||
var numberOfWeeks = (int)difference.TotalDays / 7;
|
||||
return $"{numberOfWeeks} week{(numberOfWeeks > 1 ? PluralAppend : string.Empty)} ago";
|
||||
}
|
||||
|
||||
// If less than a year, return months
|
||||
var daysInCurrentYear = DateTime.IsLeapYear(DateTime.Now.Year) ? 366 : 365;
|
||||
if (difference.TotalDays < daysInCurrentYear)
|
||||
{
|
||||
var days = difference.TotalDays;
|
||||
var year = DateTime.Now.Year;
|
||||
var month = DateTime.Now.Month;
|
||||
var monthCount = 0;
|
||||
while(days > 0)
|
||||
{
|
||||
monthCount++;
|
||||
var numberOfDaysInMonth = DateTime.DaysInMonth(year, month);
|
||||
days -= numberOfDaysInMonth;
|
||||
if (month == 1)
|
||||
{
|
||||
month = 12;
|
||||
year--;
|
||||
}
|
||||
else
|
||||
{
|
||||
month--;
|
||||
}
|
||||
}
|
||||
|
||||
return $"{monthCount} month{(monthCount > 1 ? PluralAppend : string.Empty)} ago";
|
||||
}
|
||||
|
||||
// Return the number of years
|
||||
var remainingDays = difference.TotalDays;
|
||||
var yearCount = 0;
|
||||
var currentYear = DateTime.Now.Year;
|
||||
while(remainingDays > 0)
|
||||
{
|
||||
yearCount++;
|
||||
remainingDays -= DateTime.IsLeapYear(currentYear) ? 366 : 365;
|
||||
currentYear--;
|
||||
}
|
||||
|
||||
return $"{yearCount} year{(yearCount > 1 ? PluralAppend : string.Empty)} ago";
|
||||
}
|
||||
}
|
||||
@@ -1,29 +1,11 @@
|
||||
using System.Globalization;
|
||||
using System.Windows.Data;
|
||||
namespace Daybreak.Shared.Converters;
|
||||
|
||||
namespace Daybreak.Shared.Converters;
|
||||
|
||||
public sealed class TimespanToETAConverter : IValueConverter
|
||||
public static class TimespanToETAConverter
|
||||
{
|
||||
private const char PluralAppend = 's';
|
||||
|
||||
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
|
||||
public static string GetETAString(TimeSpan timeSpan)
|
||||
{
|
||||
return GetETAString(value);
|
||||
}
|
||||
|
||||
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
|
||||
private static string GetETAString(object obj)
|
||||
{
|
||||
if (obj is not TimeSpan timeSpan)
|
||||
{
|
||||
return string.Empty;
|
||||
}
|
||||
|
||||
if ((int)timeSpan.TotalDays > 0)
|
||||
{
|
||||
return $"{(int)timeSpan.TotalDays} day{((int)timeSpan.TotalDays > 1 ? PluralAppend : string.Empty)} remaining";
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user