mirror of
https://github.com/gwdevhub/Daybreak.git
synced 2026-09-16 05:19:23 +00:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
fe4720c5e1 | ||
|
|
b57d2ad5d3 | ||
|
|
5faeae64d3 | ||
|
|
fdcf238d68 | ||
|
|
d9f7d05672 | ||
|
|
c61ee2ecd7 | ||
|
|
82512f9461 | ||
|
|
2dc13b7184 | ||
|
|
da691e3310 | ||
|
|
5e020f51d9 | ||
|
|
1f63e59b8a | ||
|
|
dcb9c29505 | ||
|
|
db83e5a954 | ||
|
|
c7c5506ee0 | ||
|
|
2988b6b38d | ||
|
|
debef9a36f | ||
|
|
839bc57b48 | ||
|
|
175c16bd00 | ||
|
|
ad356f431e | ||
|
|
ebe368e61e | ||
|
|
d800a630b5 | ||
|
|
239f91a65f | ||
|
|
aff7c24b6a | ||
|
|
5bf0af61ba | ||
|
|
03ed31e85b | ||
|
|
b21a4482bd | ||
|
|
2ad8ae79ce | ||
|
|
a4be747dd5 | ||
|
|
a6cd5d6f37 | ||
|
|
0e80fb7d86 | ||
|
|
0793f181bb | ||
|
|
a9ee0424f7 | ||
|
|
49f45b6b76 | ||
|
|
4216a03438 | ||
|
|
a9a8346a5b | ||
|
|
70a95b5e08 | ||
|
|
1e8c2e44e2 | ||
|
|
d553aadead | ||
|
|
fabb98f74e | ||
|
|
0a0ec5cefc | ||
|
|
0d31587855 | ||
|
|
f4a08e36dd | ||
|
|
c71dd6ed9c | ||
|
|
a61cff8dfd | ||
|
|
9d7f145c25 | ||
|
|
dc340189d6 | ||
|
|
5edb5ff780 | ||
|
|
890bb26c01 | ||
|
|
6c2574c722 | ||
|
|
8760f743ab | ||
|
|
97da183bd3 | ||
|
|
65439f25dc | ||
|
|
a755319fb1 | ||
|
|
7e72cb3d3f | ||
|
|
ca154827b2 | ||
|
|
089a10a056 | ||
|
|
65767d6ab5 | ||
|
|
5828263717 | ||
|
|
05f73cb306 | ||
|
|
0271e6b08c | ||
|
|
640f28584e | ||
|
|
dd89e3316e | ||
|
|
10fb70787b | ||
|
|
1393223826 | ||
|
|
b23907025d | ||
|
|
09aed18d9d | ||
|
|
b7d58f5e4c | ||
|
|
9888bc741f | ||
|
|
c1b2ed7548 | ||
|
|
c23a44a313 | ||
|
|
b217f183cb | ||
|
|
d24a0a49e8 | ||
|
|
cdd620a786 | ||
|
|
abbf87184b | ||
|
|
15fa4520aa | ||
|
|
bc963f486a | ||
|
|
66386a6b85 | ||
|
|
8d34d5d774 | ||
|
|
d5c8e7ad03 | ||
|
|
8eba2b92dd | ||
|
|
d9bd9d5395 | ||
|
|
a59f88d396 | ||
|
|
3799108f7e | ||
|
|
a7f0f31a94 | ||
|
|
6ffa774270 | ||
|
|
2124ae92b4 | ||
|
|
00312a6367 | ||
|
|
df0d32bc30 | ||
|
|
ecd19b0324 | ||
|
|
aa2e9905c1 | ||
|
|
384fdaf67c | ||
|
|
a48cd84e1a | ||
|
|
97fed608d2 | ||
|
|
68f241989a | ||
|
|
123d76fe0f | ||
|
|
9d495a35a6 | ||
|
|
5d9d59610a | ||
|
|
0bdfe69e78 | ||
|
|
8fd1584995 |
@@ -0,0 +1,71 @@
|
||||
# Copilot instructions for Daybreak
|
||||
|
||||
Daybreak is a cross-platform Guild Wars launcher built on .NET 10 and
|
||||
Photino.Blazor. Read the
|
||||
[Architecture Overview](../README.md#architecture-overview) for the project
|
||||
layout and [CONTRIBUTING.md](../CONTRIBUTING.md) for the branching and release
|
||||
process.
|
||||
|
||||
## Workflow
|
||||
|
||||
- Branch off the current `release/[version]` branch, never off `master`.
|
||||
- One feature or fix per branch and per PR.
|
||||
- Never push to `master` and never edit the version in `Directory.Build.props`.
|
||||
Both are owned by the release workflows.
|
||||
- Title PRs `Short description (Closes #123)` so the linked issue gets labelled.
|
||||
|
||||
## Build and test
|
||||
|
||||
CI does not run on PRs targeting a release branch, so validate locally. Run the
|
||||
smallest command that covers the change:
|
||||
|
||||
```bash
|
||||
dotnet build Daybreak.Linux/Daybreak.Linux.csproj # or Daybreak.Windows
|
||||
dotnet test Daybreak.Tests/Daybreak.Tests.csproj --filter "FullyQualifiedName~YourTests"
|
||||
```
|
||||
|
||||
Keep the build warning-free.
|
||||
|
||||
## Where code goes
|
||||
|
||||
| Change | Project |
|
||||
| -------------------------------------------------- | ------------------------------------ |
|
||||
| Models, utilities, interfaces shared by everything | `Daybreak.Shared` |
|
||||
| Blazor views and services | `Daybreak.Core` |
|
||||
| Platform-specific implementations | `Daybreak.Windows`, `Daybreak.Linux` |
|
||||
|
||||
`Daybreak.Tests` only references `Daybreak.Core`. When adding platform code, put
|
||||
the logic worth testing in `Daybreak.Shared` and leave the platform project as a
|
||||
thin caller.
|
||||
|
||||
Add NuGet packages through `Directory.Packages.props`.
|
||||
|
||||
## C# conventions
|
||||
|
||||
`.editorconfig` is the source of truth. The projects are set up to treat
|
||||
warnings as errors.
|
||||
|
||||
## Verify before claiming
|
||||
|
||||
Prefer evidence over reasoning about behaviour, especially for the Wine and
|
||||
injection paths. Wine is scriptable, so reproduce the actual failure and confirm
|
||||
the fix against it rather than inferring what a Win32 call does.
|
||||
|
||||
## Writing style
|
||||
|
||||
Applies to docs, comments, PR descriptions and answers.
|
||||
|
||||
- Be short. Cut filler, recap and process narration.
|
||||
- No AI-isms and no inflated language.
|
||||
- Do not paste code into documentation. Link to the file so there is only one
|
||||
copy of it.
|
||||
- Explain the reason for a change, not a summary of the diff.
|
||||
|
||||
## Markdown
|
||||
|
||||
- No inline HTML. Use `![alt][ref]` with a reference definition instead of an
|
||||
`img` tag.
|
||||
- Start a mermaid fence with the diagram type, for example `flowchart LR`. YAML
|
||||
frontmatter inside the fence is rejected by some renderers.
|
||||
- Long URLs belong in reference-style link definitions at the bottom of the
|
||||
file.
|
||||
@@ -0,0 +1,28 @@
|
||||
# Pre-configure SimplySign Desktop for unattended use:
|
||||
# - auto-show the login dialog on launch (so the TOTP keystrokes have a target)
|
||||
# - cache the smart-card PIN in the CSP, so signtool signs without a per-call
|
||||
# PIN prompt, and forget it again when the session disconnects.
|
||||
# Mirrors the settings used by the blinkdisk / docuscope CI signing setups.
|
||||
|
||||
$ErrorActionPreference = "Stop"
|
||||
|
||||
$RegistryPath = "HKCU:\Software\Certum\SimplySign"
|
||||
|
||||
$settings = [ordered]@{
|
||||
ShowLoginDialogOnStart = 1
|
||||
ShowLoginDialogOnAppRequest = 1
|
||||
RememberLastUserName = 0
|
||||
Autostart = 0
|
||||
UnregisterCertificatesOnDisconnect = 0
|
||||
RememberPINinCSP = 1
|
||||
ForgetPINinCSPonDisconnect = 1
|
||||
LangID = 9
|
||||
}
|
||||
|
||||
Write-Host "=== Configuring SimplySign Desktop registry ==="
|
||||
New-Item -Path $RegistryPath -Force | Out-Null
|
||||
foreach ($name in $settings.Keys) {
|
||||
Set-ItemProperty -Path $RegistryPath -Name $name -Value $settings[$name] -Type DWord
|
||||
Write-Host " $name = $($settings[$name])"
|
||||
}
|
||||
Write-Host "Done."
|
||||
@@ -0,0 +1,245 @@
|
||||
# Authenticate SimplySign Desktop non-interactively.
|
||||
#
|
||||
# Certum's SimplySign cloud has no headless login: the certificate only reaches
|
||||
# the Windows store after the GUI client authenticates. So we generate the
|
||||
# current TOTP from the otpauth:// secret (CERTUM_OTP_URI) and paste the
|
||||
# credentials into the login dialog. This needs an interactive desktop session,
|
||||
# which GitHub-hosted Windows runners provide.
|
||||
#
|
||||
# Based on https://www.devas.life/how-to-automate-signing-your-windows-app-with-certum/
|
||||
# and the refinements in blinkdisk's connect-simplysign.ps1.
|
||||
|
||||
param(
|
||||
[string]$OtpUri = $env:CERTUM_OTP_URI,
|
||||
[string]$UserId = $env:CERTUM_USERID,
|
||||
[string]$ExePath = $env:CERTUM_EXE_PATH
|
||||
)
|
||||
|
||||
if (-not $OtpUri) { Write-Host "ERROR: CERTUM_OTP_URI not provided"; exit 1 }
|
||||
if (-not $UserId) { Write-Host "ERROR: CERTUM_USERID not provided"; exit 1 }
|
||||
if (-not $ExePath) {
|
||||
$ExePath = "C:\Program Files\Certum\SimplySign Desktop\SimplySignDesktop.exe"
|
||||
}
|
||||
|
||||
Write-Host "=== SimplySign Desktop TOTP authentication ==="
|
||||
if (-not (Test-Path $ExePath)) {
|
||||
Write-Host "ERROR: SimplySign Desktop not found at $ExePath"
|
||||
exit 1
|
||||
}
|
||||
|
||||
# --- Parse the otpauth:// URI (works on PowerShell 5.1 and 7+) ---------------
|
||||
$uri = [Uri]$OtpUri
|
||||
try {
|
||||
$q = [System.Web.HttpUtility]::ParseQueryString($uri.Query)
|
||||
} catch {
|
||||
$q = @{}
|
||||
foreach ($part in $uri.Query.TrimStart('?') -split '&') {
|
||||
$kv = $part -split '=', 2
|
||||
if ($kv.Count -eq 2) { $q[$kv[0]] = [Uri]::UnescapeDataString($kv[1]) }
|
||||
}
|
||||
}
|
||||
|
||||
$Base32 = $q['secret']
|
||||
$Digits = if ($q['digits']) { [int]$q['digits'] } else { 6 }
|
||||
$Period = if ($q['period']) { [int]$q['period'] } else { 30 }
|
||||
$Algorithm = if ($q['algorithm']) { $q['algorithm'].ToUpper() } else { 'SHA256' }
|
||||
|
||||
if (-not $Base32) { Write-Host "ERROR: otpauth URI has no 'secret'"; exit 1 }
|
||||
if ($Algorithm -notin @('SHA1', 'SHA256', 'SHA512')) {
|
||||
Write-Host "ERROR: unsupported TOTP algorithm: $Algorithm"
|
||||
exit 1
|
||||
}
|
||||
|
||||
# --- TOTP generator (RFC 6238), inline C# so there are no dependencies --------
|
||||
Add-Type -Language CSharp @"
|
||||
using System;
|
||||
using System.Security.Cryptography;
|
||||
|
||||
public static class Totp
|
||||
{
|
||||
private const string B32 = "ABCDEFGHIJKLMNOPQRSTUVWXYZ234567";
|
||||
|
||||
private static byte[] Base32Decode(string s)
|
||||
{
|
||||
s = s.TrimEnd('=').ToUpperInvariant();
|
||||
byte[] bytes = new byte[s.Length * 5 / 8];
|
||||
int bitBuffer = 0, bitsLeft = 0, idx = 0;
|
||||
foreach (char c in s)
|
||||
{
|
||||
int val = B32.IndexOf(c);
|
||||
if (val < 0) throw new ArgumentException("Invalid Base32 char: " + c);
|
||||
bitBuffer = (bitBuffer << 5) | val;
|
||||
bitsLeft += 5;
|
||||
if (bitsLeft >= 8) { bytes[idx++] = (byte)(bitBuffer >> (bitsLeft - 8)); bitsLeft -= 8; }
|
||||
}
|
||||
return bytes;
|
||||
}
|
||||
|
||||
private static HMAC Hmac(string algorithm, byte[] key)
|
||||
{
|
||||
switch (algorithm.ToUpper())
|
||||
{
|
||||
case "SHA1": return new HMACSHA1(key);
|
||||
case "SHA256": return new HMACSHA256(key);
|
||||
case "SHA512": return new HMACSHA512(key);
|
||||
default: throw new ArgumentException("Unsupported algorithm: " + algorithm);
|
||||
}
|
||||
}
|
||||
|
||||
public static string Now(string secret, int digits, int period, string algorithm)
|
||||
{
|
||||
byte[] key = Base32Decode(secret);
|
||||
long counter = DateTimeOffset.UtcNow.ToUnixTimeSeconds() / period;
|
||||
byte[] cnt = BitConverter.GetBytes(counter);
|
||||
if (BitConverter.IsLittleEndian) Array.Reverse(cnt);
|
||||
|
||||
byte[] hash;
|
||||
using (var h = Hmac(algorithm, key)) { hash = h.ComputeHash(cnt); }
|
||||
|
||||
int offset = hash[hash.Length - 1] & 0x0F;
|
||||
int binary =
|
||||
((hash[offset] & 0x7F) << 24) |
|
||||
((hash[offset + 1] & 0xFF) << 16) |
|
||||
((hash[offset + 2] & 0xFF) << 8) |
|
||||
(hash[offset + 3] & 0xFF);
|
||||
int otp = binary % (int)Math.Pow(10, digits);
|
||||
return otp.ToString(new string('0', digits));
|
||||
}
|
||||
}
|
||||
"@
|
||||
|
||||
function Find-UiByName($el, $walker, $name) {
|
||||
$c = $walker.GetFirstChild($el)
|
||||
while ($c) {
|
||||
if ($c.Current.Name -eq $name) { return $c }
|
||||
$r = Find-UiByName $c $walker $name
|
||||
if ($r) { return $r }
|
||||
$c = $walker.GetNextSibling($c)
|
||||
}
|
||||
return $null
|
||||
}
|
||||
|
||||
# An outdated SimplySign build pops a modal "New version found - download?" box
|
||||
# over the login form, which swallows the credential keystrokes. Decline it by
|
||||
# clicking "No" (Invoke / legacy default action / click the element's point).
|
||||
function Dismiss-UpdatePrompt {
|
||||
try { Add-Type -AssemblyName UIAutomationClient, UIAutomationTypes -ErrorAction Stop } catch { return $false }
|
||||
if (-not ('Win32Mouse' -as [type])) {
|
||||
Add-Type @"
|
||||
using System;
|
||||
using System.Runtime.InteropServices;
|
||||
public static class Win32Mouse {
|
||||
[DllImport("user32.dll")] static extern bool SetCursorPos(int x, int y);
|
||||
[DllImport("user32.dll")] static extern void mouse_event(uint f, uint x, uint y, uint d, int e);
|
||||
public static void Click(int x, int y){ SetCursorPos(x,y); mouse_event(0x02,0,0,0,0); mouse_event(0x04,0,0,0,0); }
|
||||
}
|
||||
"@
|
||||
}
|
||||
$procIds = @((Get-Process -Name '*SimplySign*' -ErrorAction SilentlyContinue).Id)
|
||||
$walker = [System.Windows.Automation.TreeWalker]::ControlViewWalker
|
||||
$top = $walker.GetFirstChild([System.Windows.Automation.AutomationElement]::RootElement)
|
||||
while ($top) {
|
||||
if ($procIds -contains $top.Current.ProcessId) {
|
||||
$no = Find-UiByName $top $walker "No"
|
||||
if ($no) {
|
||||
Write-Host "Update prompt detected; declining (clicking 'No')."
|
||||
try { ($no.GetCurrentPattern([System.Windows.Automation.InvokePattern]::Pattern)).Invoke(); return $true } catch {}
|
||||
try { ($no.GetCurrentPattern([System.Windows.Automation.LegacyIAccessiblePattern]::Pattern)).DoDefaultAction(); return $true } catch {}
|
||||
try { $pt = $no.GetClickablePoint(); [Win32Mouse]::Click([int]$pt.X, [int]$pt.Y); return $true } catch { Write-Host "Click 'No' failed: $($_.Exception.Message)" }
|
||||
}
|
||||
}
|
||||
$top = $walker.GetNextSibling($top)
|
||||
}
|
||||
return $false
|
||||
}
|
||||
|
||||
# Start from a clean slate so a stale window/session can't swallow the keystrokes.
|
||||
if ($existing = Get-Process -Name "SimplySignDesktop" -ErrorAction Ignore) {
|
||||
Write-Host "Killing existing SimplySignDesktop process..."
|
||||
$existing | Stop-Process -Force
|
||||
Start-Sleep -Seconds 1
|
||||
}
|
||||
|
||||
Write-Host "Launching SimplySign Desktop..."
|
||||
$proc = Start-Process -FilePath $ExePath -PassThru
|
||||
Start-Sleep -Seconds 3
|
||||
|
||||
$wshell = New-Object -ComObject WScript.Shell
|
||||
|
||||
Write-Host "Focusing the login window..."
|
||||
$focused = $wshell.AppActivate($proc.Id)
|
||||
if (-not $focused) { $focused = $wshell.AppActivate('SimplySign Desktop') }
|
||||
for ($i = 0; (-not $focused) -and ($i -lt 10); $i++) {
|
||||
Start-Sleep -Milliseconds 500
|
||||
$focused = $wshell.AppActivate($proc.Id) -or $wshell.AppActivate('SimplySign Desktop')
|
||||
}
|
||||
if (-not $focused) {
|
||||
Write-Host "ERROR: could not bring SimplySign Desktop to the foreground"
|
||||
exit 1
|
||||
}
|
||||
|
||||
# Decline the "new version available" modal if it's covering the login form.
|
||||
if (Dismiss-UpdatePrompt) { Start-Sleep -Milliseconds 800 }
|
||||
|
||||
# Re-assert focus: dismissing the dialog can move the foreground window, and
|
||||
# keystrokes sent to the wrong window are silently dropped.
|
||||
$wshell.AppActivate($proc.Id) | Out-Null
|
||||
$wshell.AppActivate('SimplySign Desktop') | Out-Null
|
||||
Start-Sleep -Milliseconds 400
|
||||
|
||||
# Paste rather than type: SendKeys mangles characters like + ^ % ( ) and can
|
||||
# drop characters; pasting delivers the exact string. Falls back to typing if
|
||||
# the clipboard is unavailable.
|
||||
function Set-Field([string]$text) {
|
||||
try {
|
||||
Set-Clipboard -Value $text -ErrorAction Stop
|
||||
Start-Sleep -Milliseconds 150
|
||||
$wshell.SendKeys("^v")
|
||||
} catch {
|
||||
Write-Host "Clipboard unavailable ($($_.Exception.Message)); typing instead."
|
||||
$wshell.SendKeys($text)
|
||||
}
|
||||
Start-Sleep -Milliseconds 250
|
||||
}
|
||||
|
||||
Write-Host "Injecting credentials..."
|
||||
Set-Field $UserId
|
||||
$wshell.SendKeys("{TAB}")
|
||||
Start-Sleep -Milliseconds 200
|
||||
|
||||
# Generate the code right before sending it so it can't expire while we focus.
|
||||
$otp = [Totp]::Now($Base32, $Digits, $Period, $Algorithm)
|
||||
Set-Field $otp
|
||||
Start-Sleep -Milliseconds 200
|
||||
$wshell.SendKeys("{ENTER}")
|
||||
|
||||
# Don't leave the OTP / username sitting on the clipboard afterwards.
|
||||
try { Set-Clipboard -Value " " -ErrorAction Stop } catch {}
|
||||
|
||||
Write-Host "Waiting for authentication to settle..."
|
||||
Start-Sleep -Seconds 5
|
||||
|
||||
if (-not (Get-Process -Id $proc.Id -ErrorAction SilentlyContinue)) {
|
||||
Write-Host "ERROR: SimplySign Desktop exited - authentication failed."
|
||||
exit 1
|
||||
}
|
||||
|
||||
# Confirm the login actually mounted the signing certificate into the store.
|
||||
$thumb = if ($env:CERTUM_CERT_SHA1) { ($env:CERTUM_CERT_SHA1 -replace '\s', '').ToUpperInvariant() } else { $null }
|
||||
Write-Host "Verifying the signing certificate reached the store..."
|
||||
$deadline = (Get-Date).AddSeconds(60)
|
||||
$found = $false
|
||||
do {
|
||||
$certs = Get-ChildItem Cert:\CurrentUser\My, Cert:\LocalMachine\My -ErrorAction SilentlyContinue
|
||||
if ($thumb) { $found = [bool]($certs | Where-Object { $_.Thumbprint -eq $thumb }) }
|
||||
else { $found = [bool]($certs | Where-Object { $_.Subject -like '*Certum*' -or $_.Issuer -like '*Certum*' }) }
|
||||
if ($found) { break }
|
||||
Start-Sleep -Seconds 3
|
||||
} while ((Get-Date) -lt $deadline)
|
||||
|
||||
if ($found) {
|
||||
Write-Host "=== Authentication complete: signing certificate is available. ==="
|
||||
} else {
|
||||
Write-Host "ERROR: signing certificate did not appear after authentication."
|
||||
exit 1
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
#!/bin/bash
|
||||
# Install Certum SimplySign Desktop on the (ephemeral) Windows runner.
|
||||
# SimplySign Desktop is what mounts the cloud signing certificate as a virtual
|
||||
# smart card into the Windows certificate store; signtool then selects it by
|
||||
# thumbprint. The version + checksum are pinned for reproducibility.
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
VERSION="9.3.4.72"
|
||||
URL="https://files.certum.eu/software/SimplySignDesktop/Windows/${VERSION}/SimplySignDesktop-${VERSION}-64-bit-en.msi"
|
||||
EXPECTED_SHA256="bd51ebbaaac20fc7d59ab7103b5ed532b7800586df4e31f6999d03a394f9c515"
|
||||
INSTALLER="SimplySignDesktop.msi"
|
||||
INSTALL_DIR="/c/Program Files/Certum/SimplySign Desktop"
|
||||
|
||||
echo "=== Installing Certum SimplySign Desktop ${VERSION} ==="
|
||||
|
||||
# Idempotent: a cached/pre-provisioned image may already have it.
|
||||
if [ -d "$INSTALL_DIR" ]; then
|
||||
echo "Already installed at: $INSTALL_DIR"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
echo "Downloading installer..."
|
||||
curl -L "$URL" -o "$INSTALLER" --fail --max-time 600
|
||||
|
||||
ACTUAL_SHA256=$(sha256sum "$INSTALLER" | awk '{print $1}')
|
||||
if [ "$EXPECTED_SHA256" != "$ACTUAL_SHA256" ]; then
|
||||
echo "ERROR: checksum mismatch"
|
||||
echo " expected: $EXPECTED_SHA256"
|
||||
echo " actual: $ACTUAL_SHA256"
|
||||
exit 1
|
||||
fi
|
||||
echo "Checksum verified."
|
||||
|
||||
echo "Running msiexec (silent)..."
|
||||
# Start-Process inherits this CWD, so the relative installer path resolves here.
|
||||
powershell -Command "Start-Process msiexec.exe -ArgumentList '/i','${INSTALLER}','/quiet','/norestart','/l*v','install.log','ALLUSERS=1','REBOOT=ReallySuppress' -Wait -NoNewWindow"
|
||||
|
||||
if [ ! -d "$INSTALL_DIR" ]; then
|
||||
echo "ERROR: installation directory not found after install"
|
||||
echo "Last 20 lines of install.log:"
|
||||
tail -20 install.log 2>/dev/null || echo "(no install.log)"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "SimplySign Desktop installed."
|
||||
@@ -0,0 +1,66 @@
|
||||
# Sign the given files with the Certum cloud certificate (selected by SHA1
|
||||
# thumbprint from the store) and verify each signature. Assumes SimplySign
|
||||
# Desktop has already authenticated (connect-simplysign.ps1).
|
||||
|
||||
[CmdletBinding()]
|
||||
param(
|
||||
[Parameter(Mandatory = $true)]
|
||||
[string[]]$Files,
|
||||
[string]$CertSha1 = $env:CERTUM_CERT_SHA1,
|
||||
[string]$TimestampUrl = "http://time.certum.pl"
|
||||
)
|
||||
|
||||
$ErrorActionPreference = "Stop"
|
||||
|
||||
if (-not $CertSha1) { throw "CERTUM_CERT_SHA1 not provided." }
|
||||
$thumb = ($CertSha1 -replace '\s', '').ToUpperInvariant()
|
||||
|
||||
# Resolve the newest signtool.exe from the installed Windows SDK(s).
|
||||
$kitsBin = "C:\Program Files (x86)\Windows Kits\10\bin"
|
||||
$signtool = Get-ChildItem -Path $kitsBin -Recurse -Filter signtool.exe -File -ErrorAction SilentlyContinue |
|
||||
Where-Object { $_.FullName -like '*\x64\*' } |
|
||||
Sort-Object { [version]$_.Directory.Parent.Name } |
|
||||
Select-Object -Last 1 -ExpandProperty FullName
|
||||
if (-not $signtool) { throw "signtool.exe not found under $kitsBin" }
|
||||
Write-Host "Using signtool: $signtool"
|
||||
|
||||
# The SimplySign virtual smart card registers in the store a moment after the
|
||||
# desktop client authenticates; poll for our certificate before signing.
|
||||
Write-Host "Waiting for certificate $thumb to appear in the store..."
|
||||
$deadline = (Get-Date).AddSeconds(120)
|
||||
$cert = $null
|
||||
$elapsed = 0
|
||||
do {
|
||||
$cert = Get-ChildItem Cert:\CurrentUser\My, Cert:\LocalMachine\My -ErrorAction SilentlyContinue |
|
||||
Where-Object { $_.Thumbprint -eq $thumb } | Select-Object -First 1
|
||||
if ($cert) { break }
|
||||
Start-Sleep -Seconds 3
|
||||
$elapsed += 3
|
||||
Write-Host " ...still waiting (${elapsed}s)"
|
||||
} while ((Get-Date) -lt $deadline)
|
||||
|
||||
if (-not $cert) {
|
||||
Write-Host "Certificate not found. CurrentUser\My + LocalMachine\My contents:"
|
||||
Get-ChildItem Cert:\CurrentUser\My, Cert:\LocalMachine\My -ErrorAction SilentlyContinue |
|
||||
Select-Object Thumbprint, Subject, HasPrivateKey | Format-Table -AutoSize | Out-String | Write-Host
|
||||
Write-Host "SimplySign processes:"
|
||||
Get-Process -Name '*SimplySign*' -ErrorAction SilentlyContinue |
|
||||
Select-Object Name, Id, Responding, MainWindowTitle | Format-Table -AutoSize | Out-String | Write-Host
|
||||
throw "Certificate $thumb not found in the store - SimplySign authentication likely failed (see dump above)."
|
||||
}
|
||||
Write-Host "Found signing certificate: $($cert.Subject)"
|
||||
|
||||
$failed = @()
|
||||
foreach ($file in $Files) {
|
||||
if (-not (Test-Path $file)) { throw "File to sign not found: $file" }
|
||||
Write-Host "=== Signing $file ==="
|
||||
& $signtool sign /sha1 $thumb /fd sha256 /td sha256 /tr $TimestampUrl /v $file
|
||||
if ($LASTEXITCODE -ne 0) { $failed += $file; continue }
|
||||
& $signtool verify /pa /v $file
|
||||
if ($LASTEXITCODE -ne 0) { $failed += $file }
|
||||
}
|
||||
|
||||
if ($failed.Count -gt 0) {
|
||||
throw "Signing/verification failed for: $($failed -join ', ')"
|
||||
}
|
||||
Write-Host "All binaries signed and verified."
|
||||
@@ -13,10 +13,14 @@ on:
|
||||
- "Daybreak.Shared/**"
|
||||
- "Daybreak.Windows/**"
|
||||
- "Daybreak.Linux/**"
|
||||
- ".github/scripts/**"
|
||||
- ".github/workflows/cd-template.yml"
|
||||
- ".github/workflows/cd-dry-run.yml"
|
||||
|
||||
jobs:
|
||||
cd:
|
||||
uses: ./.github/workflows/cd-template.yml
|
||||
with:
|
||||
publishRelease: false
|
||||
signBinaries: false
|
||||
secrets: inherit
|
||||
|
||||
@@ -9,6 +9,11 @@ on:
|
||||
required: false
|
||||
type: boolean
|
||||
default: false
|
||||
signBinaries:
|
||||
description: "Whether to code-sign Windows binaries"
|
||||
required: false
|
||||
type: boolean
|
||||
default: false
|
||||
|
||||
jobs:
|
||||
# ───────────────────────────────────────────────────────
|
||||
@@ -16,7 +21,7 @@ jobs:
|
||||
# (Injector + API) that both platforms need.
|
||||
# ───────────────────────────────────────────────────────
|
||||
build-x86:
|
||||
runs-on: windows-latest
|
||||
runs-on: windows-2025
|
||||
|
||||
outputs:
|
||||
version: ${{ steps.get_version.outputs.version }}
|
||||
@@ -24,10 +29,13 @@ jobs:
|
||||
env:
|
||||
Configuration: Release
|
||||
Actions_Allow_Unsecure_Commands: true
|
||||
CERTUM_OTP_URI: ${{ secrets.CERTUM_OTP_URI }}
|
||||
CERTUM_USERID: ${{ secrets.CERTUM_USERID }}
|
||||
CERTUM_CERT_SHA1: ${{ secrets.CERTUM_CERT_SHA1 }}
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v6
|
||||
uses: actions/checkout@v7
|
||||
with:
|
||||
fetch-depth: 0
|
||||
submodules: recursive
|
||||
@@ -40,7 +48,7 @@ jobs:
|
||||
echo "version=$version" >> $env:GITHUB_OUTPUT
|
||||
|
||||
- name: Install .NET
|
||||
uses: actions/setup-dotnet@v5
|
||||
uses: actions/setup-dotnet@v6
|
||||
with:
|
||||
dotnet-version: "10.x"
|
||||
|
||||
@@ -48,8 +56,32 @@ jobs:
|
||||
run: .\Scripts\PublishWindowsX86.ps1
|
||||
shell: pwsh
|
||||
|
||||
# Certum SimplySign code signing. Binaries are signed only when explicitly
|
||||
# requested and the Certum secrets are provisioned.
|
||||
- name: Set up Certum SimplySign
|
||||
if: inputs.signBinaries && env.CERTUM_OTP_URI != ''
|
||||
shell: bash
|
||||
run: |
|
||||
chmod +x ./.github/scripts/install-simplysign.sh
|
||||
./.github/scripts/install-simplysign.sh
|
||||
powershell -ExecutionPolicy Bypass -File "./.github/scripts/configure-simplysign.ps1"
|
||||
|
||||
- name: Authenticate Certum SimplySign
|
||||
if: inputs.signBinaries && env.CERTUM_OTP_URI != ''
|
||||
shell: bash
|
||||
run: powershell -ExecutionPolicy Bypass -File "./.github/scripts/connect-simplysign.ps1"
|
||||
|
||||
- name: Sign x86 binaries
|
||||
if: inputs.signBinaries && env.CERTUM_OTP_URI != ''
|
||||
run: |
|
||||
./.github/scripts/sign-certum.ps1 -Files @(
|
||||
"Publish/Injector/Daybreak.Injector.exe",
|
||||
"Publish/Api/Daybreak.API.dll"
|
||||
)
|
||||
shell: pwsh
|
||||
|
||||
- name: Upload x86 artifacts
|
||||
uses: actions/upload-artifact@v6
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: x86-components
|
||||
path: |
|
||||
@@ -62,21 +94,24 @@ jobs:
|
||||
# ───────────────────────────────────────────────────────
|
||||
build-windows:
|
||||
needs: build-x86
|
||||
runs-on: windows-latest
|
||||
runs-on: windows-2025
|
||||
|
||||
env:
|
||||
Configuration: Release
|
||||
Actions_Allow_Unsecure_Commands: true
|
||||
CERTUM_OTP_URI: ${{ secrets.CERTUM_OTP_URI }}
|
||||
CERTUM_USERID: ${{ secrets.CERTUM_USERID }}
|
||||
CERTUM_CERT_SHA1: ${{ secrets.CERTUM_CERT_SHA1 }}
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v6
|
||||
uses: actions/checkout@v7
|
||||
with:
|
||||
fetch-depth: 0
|
||||
submodules: recursive
|
||||
|
||||
- name: Install .NET
|
||||
uses: actions/setup-dotnet@v5
|
||||
uses: actions/setup-dotnet@v6
|
||||
with:
|
||||
dotnet-version: "10.x"
|
||||
|
||||
@@ -89,7 +124,7 @@ jobs:
|
||||
dotnet user-secrets --project Daybreak.Core\Daybreak.Core.csproj set ApmUri "${{ secrets.ApmUri }}"
|
||||
|
||||
- name: Download x86 artifacts
|
||||
uses: actions/download-artifact@v7
|
||||
uses: actions/download-artifact@v8
|
||||
with:
|
||||
name: x86-components
|
||||
path: Publish/
|
||||
@@ -98,8 +133,30 @@ jobs:
|
||||
run: .\Scripts\PublishWindows.ps1
|
||||
shell: pwsh
|
||||
|
||||
- name: Set up Certum SimplySign
|
||||
if: inputs.signBinaries && env.CERTUM_OTP_URI != ''
|
||||
shell: bash
|
||||
run: |
|
||||
chmod +x ./.github/scripts/install-simplysign.sh
|
||||
./.github/scripts/install-simplysign.sh
|
||||
powershell -ExecutionPolicy Bypass -File "./.github/scripts/configure-simplysign.ps1"
|
||||
|
||||
- name: Authenticate Certum SimplySign
|
||||
if: inputs.signBinaries && env.CERTUM_OTP_URI != ''
|
||||
shell: bash
|
||||
run: powershell -ExecutionPolicy Bypass -File "./.github/scripts/connect-simplysign.ps1"
|
||||
|
||||
- name: Sign Windows build
|
||||
if: inputs.signBinaries && env.CERTUM_OTP_URI != ''
|
||||
run: |
|
||||
./.github/scripts/sign-certum.ps1 -Files @(
|
||||
"Publish/Daybreak.exe",
|
||||
"Publish/Installer/Daybreak.Installer.exe"
|
||||
)
|
||||
shell: pwsh
|
||||
|
||||
- name: Upload Windows build
|
||||
uses: actions/upload-artifact@v6
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: windows-build
|
||||
path: Publish/
|
||||
@@ -114,13 +171,13 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v6
|
||||
uses: actions/checkout@v7
|
||||
with:
|
||||
fetch-depth: 0
|
||||
submodules: recursive
|
||||
|
||||
- name: Install .NET
|
||||
uses: actions/setup-dotnet@v5
|
||||
uses: actions/setup-dotnet@v6
|
||||
with:
|
||||
dotnet-version: "10.x"
|
||||
|
||||
@@ -133,7 +190,7 @@ jobs:
|
||||
dotnet user-secrets --project Daybreak.Core/Daybreak.Core.csproj set ApmUri "${{ secrets.ApmUri }}"
|
||||
|
||||
- name: Download x86 artifacts
|
||||
uses: actions/download-artifact@v7
|
||||
uses: actions/download-artifact@v8
|
||||
with:
|
||||
name: x86-components
|
||||
path: Publish/
|
||||
@@ -149,7 +206,7 @@ jobs:
|
||||
chmod +x Publish/Installer/Daybreak.Installer
|
||||
|
||||
- name: Upload Linux build
|
||||
uses: actions/upload-artifact@v6
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: linux-build
|
||||
path: Publish/
|
||||
@@ -169,7 +226,7 @@ jobs:
|
||||
steps:
|
||||
# ── Windows package ──
|
||||
- name: Download Windows build
|
||||
uses: actions/download-artifact@v7
|
||||
uses: actions/download-artifact@v8
|
||||
with:
|
||||
name: windows-build
|
||||
path: windows-publish/
|
||||
@@ -187,7 +244,7 @@ jobs:
|
||||
|
||||
# ── Linux package ──
|
||||
- name: Download Linux build
|
||||
uses: actions/download-artifact@v7
|
||||
uses: actions/download-artifact@v8
|
||||
with:
|
||||
name: linux-build
|
||||
path: linux-publish/
|
||||
@@ -206,7 +263,7 @@ jobs:
|
||||
cd linux-publish && zip -r ../daybreakv${{ env.Version }}-linux.zip . && cd ..
|
||||
|
||||
- name: Upload release artifacts
|
||||
uses: actions/upload-artifact@v6
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: release-zips
|
||||
path: |
|
||||
@@ -228,13 +285,13 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v6
|
||||
uses: actions/checkout@v7
|
||||
with:
|
||||
fetch-depth: 0
|
||||
submodules: recursive
|
||||
|
||||
- name: Download release artifacts
|
||||
uses: actions/download-artifact@v7
|
||||
uses: actions/download-artifact@v8
|
||||
with:
|
||||
name: release-zips
|
||||
path: release-assets/
|
||||
|
||||
@@ -15,6 +15,9 @@ on:
|
||||
- "Daybreak.Shared/**"
|
||||
- "Daybreak.Windows/**"
|
||||
- "Daybreak.Linux/**"
|
||||
- ".github/scripts/**"
|
||||
- ".github/workflows/cd-template.yml"
|
||||
- ".github/workflows/cd.yaml"
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
@@ -22,4 +25,5 @@ jobs:
|
||||
uses: ./.github/workflows/cd-template.yml
|
||||
with:
|
||||
publishRelease: true
|
||||
signBinaries: true
|
||||
secrets: inherit
|
||||
|
||||
@@ -16,13 +16,13 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v6
|
||||
uses: actions/checkout@v7
|
||||
with:
|
||||
fetch-depth: 0
|
||||
submodules: recursive
|
||||
|
||||
- name: Install .NET
|
||||
uses: actions/setup-dotnet@v5
|
||||
uses: actions/setup-dotnet@v6
|
||||
with:
|
||||
dotnet-version: "10.x"
|
||||
|
||||
@@ -49,13 +49,13 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v6
|
||||
uses: actions/checkout@v7
|
||||
with:
|
||||
fetch-depth: 0
|
||||
submodules: recursive
|
||||
|
||||
- name: Install .NET
|
||||
uses: actions/setup-dotnet@v5
|
||||
uses: actions/setup-dotnet@v6
|
||||
with:
|
||||
dotnet-version: "10.x"
|
||||
|
||||
|
||||
@@ -42,7 +42,7 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v6
|
||||
uses: actions/checkout@v7
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
@@ -63,7 +63,7 @@ jobs:
|
||||
# If this step fails, then you should remove it and run the build manually (see below)
|
||||
|
||||
- name: Install .NET Core
|
||||
uses: actions/setup-dotnet@v5
|
||||
uses: actions/setup-dotnet@v6
|
||||
with:
|
||||
dotnet-version: '10.x'
|
||||
|
||||
|
||||
@@ -18,7 +18,7 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v6
|
||||
uses: actions/checkout@v7
|
||||
with:
|
||||
fetch-depth: 0
|
||||
token: ${{ secrets.RELEASE_PAT }}
|
||||
@@ -30,7 +30,7 @@ jobs:
|
||||
|
||||
- name: Bump version in Directory.Build.props
|
||||
id: bump
|
||||
uses: vers-one/dotnet-project-version-updater@v1.7
|
||||
uses: vers-one/dotnet-project-version-updater@v1.8
|
||||
with:
|
||||
file: "Directory.Build.props"
|
||||
version: ${{ inputs.versionBump }}
|
||||
|
||||
@@ -13,7 +13,7 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Label linked issues as merged
|
||||
uses: actions/github-script@v8
|
||||
uses: actions/github-script@v9
|
||||
with:
|
||||
github-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
script: |
|
||||
|
||||
@@ -27,12 +27,12 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v6
|
||||
uses: actions/checkout@v7
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Install .NET Core
|
||||
uses: actions/setup-dotnet@v5
|
||||
uses: actions/setup-dotnet@v6
|
||||
with:
|
||||
dotnet-version: "10.x"
|
||||
|
||||
|
||||
+2
-2
@@ -181,8 +181,8 @@ DocProject/Help/*.hhp
|
||||
DocProject/Help/Html2
|
||||
DocProject/Help/html
|
||||
|
||||
# Click-Once directory
|
||||
publish/
|
||||
# Click-Once / local publish directories
|
||||
[Pp]ublish/
|
||||
|
||||
# Publish Web Output
|
||||
*.[Pp]ublish.xml
|
||||
|
||||
@@ -0,0 +1,108 @@
|
||||
-- Per-project Neovim config for the Daybreak repo.
|
||||
-- Auto-sourced when nvim is started inside the repo, provided the user has
|
||||
-- `vim.o.exrc = true` in their config (or runs `:set exrc`). Neovim 0.9+
|
||||
-- prompts to trust this file on first load.
|
||||
--
|
||||
-- Commands added:
|
||||
-- :DaybreakStageX86 [Config] build x86 components via wine and stage
|
||||
-- them into Daybreak.Linux/bin/<Config>/...
|
||||
-- /{Injector,Api}/ (default Config: Debug)
|
||||
-- :DaybreakRunLinux run Daybreak.Linux via `dotnet run` in a
|
||||
-- terminal split (honors launchSettings.json,
|
||||
-- which easy-dot-net's launcher does not)
|
||||
--
|
||||
-- Note: a Linux/wine attach-debugger workflow was attempted (winedbg --gdb
|
||||
-- stub *and* native Linux gdb -p attach), but both freeze GW reliably because
|
||||
-- of ptrace/wineserver interactions that wine doesn't tolerate. For now,
|
||||
-- prefer logging-based debugging via the API's console + log file in the
|
||||
-- game directory (Daybreak.API<YYYYMMDD>.log next to Gw.exe).
|
||||
|
||||
local repo = vim.fn.fnamemodify(debug.getinfo(1, 'S').source:sub(2), ':p:h')
|
||||
local scripts = repo .. '/Scripts'
|
||||
|
||||
-- ───── Progress notification with spinner ──────────────────────────────────
|
||||
-- Uses nvim-notify / snacks.nvim's `id`/`replace` semantics to update a
|
||||
-- single notification in place. Falls back to plain notify if the backend
|
||||
-- doesn't support replace (the spinner just stops updating).
|
||||
local SPINNER = { '⣷', '⣯', '⣟', '⡿', '⢿', '⣻', '⣽', '⣾' }
|
||||
|
||||
local function start_progress(title, message)
|
||||
-- stable id so snacks.nvim/nvim-notify replace the same notification
|
||||
local id = 'daybreak-' .. title:gsub('%s+', '-') .. '-' .. tostring(vim.uv.hrtime())
|
||||
local state = { title = title, msg = message, frame = 1, done = false, id = id }
|
||||
local function render()
|
||||
local icon = state.done and '' or (SPINNER[state.frame] .. ' ')
|
||||
local opts = {
|
||||
title = state.title,
|
||||
id = state.id,
|
||||
timeout = state.done and 4000 or false,
|
||||
hide_from_history = not state.done,
|
||||
icon = state.done and '✓' or nil,
|
||||
}
|
||||
if state.notif then opts.replace = state.notif end
|
||||
state.notif = vim.notify(icon .. state.msg, vim.log.levels.INFO, opts)
|
||||
end
|
||||
render()
|
||||
state.timer = vim.uv.new_timer()
|
||||
state.timer:start(80, 80, vim.schedule_wrap(function()
|
||||
if state.done then return end
|
||||
state.frame = (state.frame % #SPINNER) + 1
|
||||
render()
|
||||
end))
|
||||
return {
|
||||
update = function(msg) state.msg = msg; render() end,
|
||||
finish = function(ok, msg)
|
||||
state.done = true
|
||||
if state.timer then state.timer:stop(); state.timer:close(); state.timer = nil end
|
||||
state.msg = msg or state.msg
|
||||
local final_opts = {
|
||||
title = state.title,
|
||||
id = state.id,
|
||||
timeout = 4000,
|
||||
icon = ok and '✓' or '✗',
|
||||
}
|
||||
if state.notif then final_opts.replace = state.notif end
|
||||
vim.notify((ok and '✓ ' or '✗ ') .. state.msg,
|
||||
ok and vim.log.levels.INFO or vim.log.levels.ERROR, final_opts)
|
||||
end,
|
||||
}
|
||||
end
|
||||
|
||||
-- ───── :DaybreakStageX86 [Config] ─────────────────────────────────────────
|
||||
vim.api.nvim_create_user_command('DaybreakStageX86', function(opts)
|
||||
local config = opts.args ~= '' and opts.args or 'Debug'
|
||||
local progress = start_progress('Daybreak StageX86', 'Building x86 components (' .. config .. ')…')
|
||||
vim.fn.jobstart({ scripts .. '/StageX86ForDebug.sh', config, '' }, {
|
||||
cwd = repo,
|
||||
on_exit = function(_, code)
|
||||
vim.schedule(function()
|
||||
if code == 0 then
|
||||
progress.finish(true, 'Staged x86 (' .. config .. ')')
|
||||
else
|
||||
progress.finish(false, 'Stage failed (exit ' .. code ..
|
||||
'). See /tmp/wine-injector-publish.log, /tmp/wine-api-publish.log')
|
||||
end
|
||||
end)
|
||||
end,
|
||||
stdout_buffered = false,
|
||||
stderr_buffered = false,
|
||||
})
|
||||
end, {
|
||||
nargs = '?',
|
||||
complete = function() return { 'Debug', 'Release' } end,
|
||||
desc = 'Build x86 components in wine and stage into Daybreak.Linux output',
|
||||
})
|
||||
|
||||
-- ───── :DaybreakRunLinux ───────────────────────────────────────────────────
|
||||
-- easy-dot-net launches the produced binary directly, bypassing launchSettings.json.
|
||||
-- This runs Daybreak.Linux through `dotnet run`, which honors the environment
|
||||
-- variables (GDK_BACKEND, WEBKIT_DISABLE_DMABUF_RENDERER, etc.) declared there.
|
||||
-- Opens in the current window (no split) so it integrates with normal buffer
|
||||
-- navigation; Ctrl-\ Ctrl-N to leave terminal mode, then your usual buffer
|
||||
-- bindings work.
|
||||
vim.api.nvim_create_user_command('DaybreakRunLinux', function()
|
||||
vim.cmd('enew')
|
||||
vim.fn.termopen({ 'dotnet', 'run', '--project', 'Daybreak.Linux/Daybreak.Linux.csproj', '-c', 'Debug' },
|
||||
{ cwd = repo })
|
||||
vim.cmd('startinsert')
|
||||
end, { desc = 'Run Daybreak.Linux via dotnet run in the current window' })
|
||||
Vendored
+30
-2
@@ -5,13 +5,41 @@
|
||||
"name": "Debug Daybreak.Linux",
|
||||
"type": "dotnet",
|
||||
"request": "launch",
|
||||
"projectPath": "${workspaceFolder}/Daybreak.Linux/Daybreak.Linux.csproj"
|
||||
"projectPath": "${workspaceFolder}/Daybreak.Linux/Daybreak.Linux.csproj",
|
||||
"console": "integratedTerminal",
|
||||
"justMyCode": true,
|
||||
"stopAtEntry": false,
|
||||
"suppressJITOptimizations": false,
|
||||
"symbolOptions": {
|
||||
"searchMicrosoftSymbolServer": false,
|
||||
"searchNuGetOrgSymbolServer": false
|
||||
},
|
||||
"logging": {
|
||||
"engineLogging": false,
|
||||
"moduleLoad": false,
|
||||
"exceptions": false,
|
||||
"browserStdOut": false
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "Debug Daybreak.Windows",
|
||||
"type": "dotnet",
|
||||
"request": "launch",
|
||||
"projectPath": "${workspaceFolder}/Daybreak.Windows/Daybreak.Windows.csproj"
|
||||
"projectPath": "${workspaceFolder}/Daybreak.Windows/Daybreak.Windows.csproj",
|
||||
"console": "integratedTerminal",
|
||||
"justMyCode": true,
|
||||
"stopAtEntry": false,
|
||||
"suppressJITOptimizations": false,
|
||||
"symbolOptions": {
|
||||
"searchMicrosoftSymbolServer": false,
|
||||
"searchNuGetOrgSymbolServer": false
|
||||
},
|
||||
"logging": {
|
||||
"engineLogging": false,
|
||||
"moduleLoad": false,
|
||||
"exceptions": false,
|
||||
"browserStdOut": false
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
+113
@@ -0,0 +1,113 @@
|
||||
# Contributing to Daybreak
|
||||
|
||||
## Getting started
|
||||
|
||||
Install the prerequisites listed under
|
||||
[Build Requirements](README.md#build-requirements), then clone with submodules:
|
||||
|
||||
```bash
|
||||
git clone --recurse-submodules https://github.com/AlexMacocian/Daybreak.git
|
||||
```
|
||||
|
||||
Read the [Architecture Overview](README.md#architecture-overview) before making
|
||||
changes so you put code in the right project.
|
||||
|
||||
## Branching model
|
||||
|
||||
There is always exactly one active release branch, named `release/[version]`
|
||||
(for example `release/0.9.10.20`). It is the integration branch: all work
|
||||
targets it, and `master` only receives finished releases.
|
||||
|
||||
```mermaid
|
||||
flowchart LR
|
||||
subgraph cycle["Release cycle"]
|
||||
direction LR
|
||||
R["release/0.9.10.20<br/><i>active release branch</i>"]
|
||||
F["feature branch<br/><i>one per change</i>"]
|
||||
R -->|fork| F
|
||||
F -->|"PR, squash merge"| R
|
||||
end
|
||||
R -->|"PR, rebase merge"| M["master"]
|
||||
M --> CD["CD pipeline<br/>builds and publishes the release"]
|
||||
CD -->|"job bumps the revision"| N["release/0.9.10.21<br/><i>next active release branch</i>"]
|
||||
```
|
||||
|
||||
### Feature work
|
||||
|
||||
1. Branch off the current release branch.
|
||||
2. Implement one feature or fix. Keep the branch scoped to a single change.
|
||||
3. Open a PR against the release branch.
|
||||
4. Merge with **squash**, so each feature lands as one commit.
|
||||
|
||||
### Releasing
|
||||
|
||||
1. Open a PR from the release branch onto `master`.
|
||||
2. Merge with **rebase**, keeping full history. Each feature stays a single
|
||||
commit on `master`.
|
||||
3. The [CD pipeline](.github/workflows/cd.yaml) runs on the push to `master`,
|
||||
builds the Windows and Linux bundles and publishes a GitHub release.
|
||||
4. On success,
|
||||
[create-revision-release-branch.yml](.github/workflows/create-revision-release-branch.yml)
|
||||
bumps the revision and creates the next `release/[version]` branch
|
||||
automatically. That becomes the new active release branch.
|
||||
|
||||
Do not push directly to `master` or create release branches by hand.
|
||||
|
||||
## Versions
|
||||
|
||||
The version lives in a single place,
|
||||
[`Directory.Build.props`](Directory.Build.props), and follows
|
||||
`major.minor.build.revision`. It is bumped by the release branch creation job,
|
||||
not manually.
|
||||
|
||||
Revision bumps happen automatically after every release. For the other
|
||||
components, run the matching workflow manually before starting the next cycle:
|
||||
|
||||
| Workflow | Bumps |
|
||||
| ------------------------------------------------------------------------------------------ | --------------------- |
|
||||
| [create-major-release-branch.yml](.github/workflows/create-major-release-branch.yml) | `^.0.0.0` |
|
||||
| [create-minor-release-branch.yml](.github/workflows/create-minor-release-branch.yml) | `*.^.0.0` |
|
||||
| [create-build-release-branch.yml](.github/workflows/create-build-release-branch.yml) | `*.*.^.0` |
|
||||
| [create-revision-release-branch.yml](.github/workflows/create-revision-release-branch.yml) | `*.*.*.^` (automatic) |
|
||||
|
||||
All four delegate to
|
||||
[create-release-branch-template.yml](.github/workflows/create-release-branch-template.yml).
|
||||
|
||||
## Pull requests
|
||||
|
||||
Title format follows the existing history: a short description, plus the issue
|
||||
reference when it closes one.
|
||||
|
||||
```text
|
||||
Setup stable computer name in Wine prefix (Closes #1609)
|
||||
```
|
||||
|
||||
[label-merged-issues.yaml](.github/workflows/label-merged-issues.yaml) parses
|
||||
`close/fix/resolve #N` from the title and body of PRs merged into a release
|
||||
branch and labels the linked issues, so use those keywords.
|
||||
|
||||
## Checks
|
||||
|
||||
[CI](.github/workflows/ci.yaml) and
|
||||
[version_check.yaml](.github/workflows/version_check.yaml) only trigger on PRs
|
||||
targeting `master`, so a feature PR onto a release branch is not covered by
|
||||
them. Run the equivalent locally before opening one:
|
||||
|
||||
```bash
|
||||
dotnet build Daybreak.Linux/Daybreak.Linux.csproj # or Daybreak.Windows on Windows
|
||||
dotnet test Daybreak.Tests/Daybreak.Tests.csproj
|
||||
```
|
||||
|
||||
The version check fails a PR to `master` if the version in
|
||||
`Directory.Build.props` is not ahead of the latest tag. This is why the bump
|
||||
must come from the release branch job.
|
||||
|
||||
## Code style
|
||||
|
||||
Formatting rules are in [`.editorconfig`](.editorconfig) and are enforced by the
|
||||
compiler and analyzers; keep the build warning-free.
|
||||
|
||||
Tests live in [`Daybreak.Tests`](Daybreak.Tests), use MSTest with
|
||||
FluentAssertions and NSubstitute, and only reference `Daybreak.Core`. If you
|
||||
want platform code covered by tests, put the testable part in
|
||||
[`Daybreak.Shared`](Daybreak.Shared).
|
||||
@@ -1,19 +0,0 @@
|
||||
using Daybreak.API.Interop;
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace Daybreak.API.Converters;
|
||||
|
||||
public sealed class PointerValueConverter : JsonConverter<PointerValue>
|
||||
{
|
||||
public override PointerValue Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
|
||||
{
|
||||
var s = reader.GetString() ?? throw new JsonException("Expected string");
|
||||
return PointerValue.Parse(s);
|
||||
}
|
||||
|
||||
public override void Write(Utf8JsonWriter writer, PointerValue value, JsonSerializerOptions options)
|
||||
{
|
||||
writer.WriteStringValue(value.ToString());
|
||||
}
|
||||
}
|
||||
@@ -82,10 +82,7 @@
|
||||
<PackageReference Include="Serilog.Settings.Configuration" />
|
||||
<PackageReference Include="Serilog.Sinks.Console" />
|
||||
<PackageReference Include="Serilog.Sinks.File" />
|
||||
<PackageReference Include="Swashbuckle.AspNetCore" />
|
||||
<PackageReference Include="Swashbuckle.AspNetCore.Swagger" />
|
||||
<PackageReference Include="Swashbuckle.AspNetCore.SwaggerGen" />
|
||||
<PackageReference Include="Swashbuckle.AspNetCore.SwaggerUI" />
|
||||
<PackageReference Include="Scalar.AspNetCore" />
|
||||
<PackageReference Include="SystemExtensions.NetStandard.Generators" PrivateAssets="all" />
|
||||
<PackageReference Include="ZLinq" />
|
||||
</ItemGroup>
|
||||
|
||||
+66
-15
@@ -3,6 +3,7 @@ using System.Diagnostics.CodeAnalysis;
|
||||
using System.Extensions.Core;
|
||||
using System.Logging;
|
||||
using System.Net.NetworkInformation;
|
||||
using System.Reflection;
|
||||
using System.Runtime.InteropServices;
|
||||
using Daybreak.API.Configuration;
|
||||
using Daybreak.API.Extensions;
|
||||
@@ -22,8 +23,12 @@ namespace Daybreak.API;
|
||||
public class EntryPoint
|
||||
{
|
||||
private const int StartPort = 5080;
|
||||
private const string GwcaDllName = "gwca.dll";
|
||||
private static readonly TimeSpan InitializationTimeout = TimeSpan.FromSeconds(5);
|
||||
private static readonly CancellationTokenSource CancellationTokenSource = new();
|
||||
private static readonly object GwcaResolverLock = new();
|
||||
private static IntPtr gwcaHandle;
|
||||
private static bool gwcaResolverRegistered;
|
||||
|
||||
[UnmanagedCallersOnly(EntryPoint = "ThreadInit"), STAThread]
|
||||
[RequiresUnreferencedCode("The handler uses a static method that gets referenced, so there's no unreferenced code to worry about")]
|
||||
@@ -87,8 +92,6 @@ public class EntryPoint
|
||||
.WithRoutes()
|
||||
.WithHealthChecks();
|
||||
|
||||
builder.Services.AddOpenApi();
|
||||
|
||||
// Add CORS policy - allow localhost and trusted community sites
|
||||
builder.Services.AddCors(options =>
|
||||
{
|
||||
@@ -184,24 +187,72 @@ public class EntryPoint
|
||||
|
||||
private static void PreloadNativeDependencies(ScopedLogger<EntryPoint> logger)
|
||||
{
|
||||
// First, check if gwca.dll is already loaded in the process (e.g. by GWToolbox).
|
||||
// If so, reuse it via a ref-count bump on the existing path rather than loading
|
||||
// a second copy from the Daybreak.API directory. Loading a second instance of
|
||||
// gwca.dll would result in duplicate native modules in the process, which can
|
||||
// lead to inconsistent state across mods that depend on it.
|
||||
string? daybreakApiDir = default;
|
||||
foreach (ProcessModule module in Process.GetCurrentProcess().Modules)
|
||||
{
|
||||
if (module.ModuleName?.Contains("Daybreak.API", StringComparison.OrdinalIgnoreCase) is true)
|
||||
if (module.ModuleName?.Equals("gwca.dll", StringComparison.OrdinalIgnoreCase) is true)
|
||||
{
|
||||
var moduleDir = Path.GetDirectoryName(module.FileName)!;
|
||||
var gwcaPath = Path.Combine(moduleDir, "gwca.dll");
|
||||
if (File.Exists(gwcaPath))
|
||||
{
|
||||
NativeLibrary.Load(gwcaPath);
|
||||
logger.LogDebug($"Preloaded gwca.dll from {gwcaPath}");
|
||||
}
|
||||
else
|
||||
{
|
||||
logger.LogError($"gwca.dll not found at {gwcaPath}");
|
||||
}
|
||||
var handle = NativeLibrary.Load(module.FileName);
|
||||
RegisterGwcaResolver(handle, module.FileName, logger);
|
||||
logger.LogDebug($"Reused already-loaded gwca.dll from {module.FileName}");
|
||||
return;
|
||||
}
|
||||
|
||||
break;
|
||||
if (daybreakApiDir is null &&
|
||||
module.ModuleName?.Contains("Daybreak.API", StringComparison.OrdinalIgnoreCase) is true)
|
||||
{
|
||||
daybreakApiDir = Path.GetDirectoryName(module.FileName);
|
||||
}
|
||||
}
|
||||
|
||||
if (daybreakApiDir is null)
|
||||
{
|
||||
logger.LogError("Could not locate Daybreak.API module directory to preload gwca.dll");
|
||||
return;
|
||||
}
|
||||
|
||||
var gwcaPath = Path.Combine(daybreakApiDir, "gwca.dll");
|
||||
if (File.Exists(gwcaPath))
|
||||
{
|
||||
var handle = NativeLibrary.Load(gwcaPath);
|
||||
RegisterGwcaResolver(handle, gwcaPath, logger);
|
||||
logger.LogDebug($"Preloaded gwca.dll from {gwcaPath}");
|
||||
}
|
||||
else
|
||||
{
|
||||
logger.LogError($"gwca.dll not found at {gwcaPath}");
|
||||
}
|
||||
}
|
||||
|
||||
private static void RegisterGwcaResolver(IntPtr handle, string path, ScopedLogger<EntryPoint> logger)
|
||||
{
|
||||
lock (GwcaResolverLock)
|
||||
{
|
||||
gwcaHandle = handle;
|
||||
if (gwcaResolverRegistered)
|
||||
{
|
||||
logger.LogDebug($"Updated gwca.dll resolver handle for {path}");
|
||||
return;
|
||||
}
|
||||
|
||||
NativeLibrary.SetDllImportResolver(typeof(GWCA).Assembly, ResolveNativeLibrary);
|
||||
gwcaResolverRegistered = true;
|
||||
logger.LogDebug($"Registered gwca.dll import resolver for {path}");
|
||||
}
|
||||
}
|
||||
|
||||
private static IntPtr ResolveNativeLibrary(string libraryName, Assembly assembly, DllImportSearchPath? searchPath)
|
||||
{
|
||||
if (string.Equals(libraryName, GwcaDllName, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return gwcaHandle;
|
||||
}
|
||||
|
||||
return IntPtr.Zero;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,17 +1,18 @@
|
||||
using Daybreak.API.Interop.GuildWars;
|
||||
using Daybreak.Shared.Models.Api;
|
||||
using ZLinq;
|
||||
|
||||
namespace Daybreak.API.Extensions;
|
||||
|
||||
public static class AttributeContextExtensions
|
||||
{
|
||||
public static List<AttributeEntry> GetAttributeEntryList(this AttributeContext[] attributes)
|
||||
public static IEnumerable<AttributeEntry> GetAttributeEntryList(this AttributeStructArray54 attributes)
|
||||
{
|
||||
return attributes
|
||||
.AsValueEnumerable()
|
||||
.Take(45) // There are only 45 maximum attributes in game
|
||||
.Where(a => a.LevelBase > 0 && a.Level >= a.LevelBase) // Select only attributes with points in them
|
||||
.Select(a => new AttributeEntry(a.Id, a.LevelBase, a.Level)).ToList();
|
||||
foreach (var attribute in attributes)
|
||||
{
|
||||
if (attribute.LevelBase > 0 && attribute.Level >= attribute.LevelBase)
|
||||
{
|
||||
yield return new AttributeEntry((uint)attribute.Id, attribute.LevelBase, attribute.Level);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
using Daybreak.API.Interop.GuildWars;
|
||||
|
||||
namespace Daybreak.API.Extensions;
|
||||
|
||||
public static class CharacterInformationExtensions
|
||||
{
|
||||
public unsafe static Profession GetPrimaryProfession(this CharacterInformation characterInformation)
|
||||
{
|
||||
return (Profession)((characterInformation.Props[2] >> 20) & 0xF);
|
||||
}
|
||||
|
||||
public unsafe static Profession GetSecondaryProfession(this CharacterInformation characterInformation)
|
||||
{
|
||||
return (Profession)((characterInformation.Props[7] >> 10) & 0xF);
|
||||
}
|
||||
|
||||
public unsafe static uint GetLevel(this CharacterInformation characterInformation)
|
||||
{
|
||||
return (characterInformation.Props[7] >> 4) & 0x3F;
|
||||
}
|
||||
|
||||
public unsafe static Campaign GetCampaign(this CharacterInformation characterInformation)
|
||||
{
|
||||
return (Campaign)(characterInformation.Props[7] & 0xF);
|
||||
}
|
||||
|
||||
public unsafe static MapID GetMapId(this CharacterInformation characterInformation)
|
||||
{
|
||||
return (MapID)((characterInformation.Props[0] >> 16) & 0xFFFF);
|
||||
}
|
||||
|
||||
public unsafe static bool IsPvP(this CharacterInformation characterInformation)
|
||||
{
|
||||
return ((characterInformation.Props[7] >> 9) & 0x1) == 0x1;
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
using Daybreak.API.Interop.GuildWars;
|
||||
using Daybreak.API.Interop;
|
||||
using Daybreak.API.Interop.GuildWars;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
|
||||
namespace Daybreak.API.Extensions;
|
||||
@@ -11,21 +12,21 @@ public static unsafe class GameContextExtensions
|
||||
{
|
||||
playerId = 0;
|
||||
if (gameContext.IsNull ||
|
||||
gameContext.Pointer->WorldContext is null ||
|
||||
gameContext.Pointer->WorldContext->PlayerControlledChar is null)
|
||||
gameContext.Pointer->World is null ||
|
||||
gameContext.Pointer->World->PlayerControlledChar is null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
playerId = gameContext.Pointer->WorldContext->PlayerControlledChar->AgentId;
|
||||
playerId = gameContext.Pointer->World->PlayerControlledChar->AgentId;
|
||||
return true;
|
||||
}
|
||||
|
||||
public static bool TryGetBuildContext(
|
||||
this WrappedPointer<GameContext> gameContext,
|
||||
[NotNullWhen(true)] out GuildWarsArray<SkillbarContext>? skillbars,
|
||||
[NotNullWhen(true)] out GuildWarsArray<SkillbarData>? skillbars,
|
||||
[NotNullWhen(true)] out GuildWarsArray<PartyAttribute>? attributes,
|
||||
[NotNullWhen(true)] out GuildWarsArray<ProfessionsContext>? professions,
|
||||
[NotNullWhen(true)] out GuildWarsArray<ProfessionState>? professions,
|
||||
[NotNullWhen(true)] out GuildWarsArray<uint>? unlockedSkills)
|
||||
{
|
||||
skillbars = default;
|
||||
@@ -33,15 +34,15 @@ public static unsafe class GameContextExtensions
|
||||
professions = default;
|
||||
unlockedSkills = default;
|
||||
if (gameContext.IsNull ||
|
||||
gameContext.Pointer->WorldContext is null)
|
||||
gameContext.Pointer->World is null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
skillbars = gameContext.Pointer->WorldContext->Skillbars;
|
||||
attributes = gameContext.Pointer->WorldContext->Attributes;
|
||||
professions = gameContext.Pointer->WorldContext->Professions;
|
||||
unlockedSkills = gameContext.Pointer->WorldContext->UnlockedCharacterSkills;
|
||||
skillbars = gameContext.Pointer->World->Skillbar.Value;
|
||||
attributes = gameContext.Pointer->World->Attributes.Value;
|
||||
professions = gameContext.Pointer->World->PartyProfessionStates;
|
||||
unlockedSkills = gameContext.Pointer->World->UnlockedCharacterSkills;
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -57,15 +58,16 @@ public static unsafe class GameContextExtensions
|
||||
heroes = default;
|
||||
henchmen = default;
|
||||
if (gameContext.IsNull ||
|
||||
gameContext.Pointer->PartyContext is null)
|
||||
gameContext.Pointer->Party is 0)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
partyId = gameContext.Pointer->PartyContext->PlayerParty->PartyId;
|
||||
players = gameContext.Pointer->PartyContext->PlayerParty->Players;
|
||||
heroes = gameContext.Pointer->PartyContext->PlayerParty->Heroes;
|
||||
henchmen = gameContext.Pointer->PartyContext->PlayerParty->Henchmen;
|
||||
var partyInfo = GWCA.GW.PartyMgr.GetPartyInfo(0);
|
||||
partyId = partyInfo->PartyId;
|
||||
players = partyInfo->Players;
|
||||
heroes = partyInfo->Heroes;
|
||||
henchmen = partyInfo->Henchmen;
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -75,27 +77,27 @@ public static unsafe class GameContextExtensions
|
||||
{
|
||||
heroFlags = default;
|
||||
if (gameContext.IsNull ||
|
||||
gameContext.Pointer->WorldContext is null)
|
||||
gameContext.Pointer->World is null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
heroFlags = gameContext.Pointer->WorldContext->HeroFlags;
|
||||
heroFlags = gameContext.Pointer->World->HeroFlags.Value;
|
||||
return true;
|
||||
}
|
||||
|
||||
public static bool TryGetAccountContext(
|
||||
this WrappedPointer<GameContext> gameContext,
|
||||
out WrappedPointer<AccountGameContext> accountContext)
|
||||
out WrappedPointer<AccountContext> accountContext)
|
||||
{
|
||||
accountContext = default;
|
||||
if (gameContext.IsNull ||
|
||||
gameContext.Pointer->AccountContext is null)
|
||||
gameContext.Pointer->Account is null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
accountContext = gameContext.Pointer->AccountContext;
|
||||
accountContext = gameContext.Pointer->Account;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
using Daybreak.API.Interop.GuildWars;
|
||||
|
||||
namespace Daybreak.API.Extensions;
|
||||
|
||||
public static class TitleExtensions
|
||||
{
|
||||
public static bool IsPercentageBased(this Title title)
|
||||
{
|
||||
return (title.Props & 1) != 0;
|
||||
}
|
||||
|
||||
public static bool HasTiers(this Title title)
|
||||
{
|
||||
return (title.Props & 3) == 2;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
using System.Runtime.InteropServices;
|
||||
|
||||
namespace Daybreak.API.Interop;
|
||||
|
||||
[StructLayout(LayoutKind.Explicit, Pack = 1, Size = 0x1AC)]
|
||||
[GWCAEquivalent("Frame")]
|
||||
public readonly struct Frame
|
||||
{
|
||||
[FieldOffset(0x0008)]
|
||||
public readonly uint FrameLayout;
|
||||
|
||||
[FieldOffset(0x0018)]
|
||||
public readonly uint VisibilityFlags;
|
||||
|
||||
[FieldOffset(0x0020)]
|
||||
public readonly uint Type;
|
||||
|
||||
[FieldOffset(0x0024)]
|
||||
public readonly uint TemplateType;
|
||||
|
||||
[FieldOffset(0x00b8)]
|
||||
public readonly uint ChildOffsetId;
|
||||
|
||||
[FieldOffset(0x00bc)]
|
||||
public readonly uint FrameId;
|
||||
|
||||
[FieldOffset(0x018C)]
|
||||
public readonly uint FrameState;
|
||||
|
||||
public bool IsCreated => (this.FrameState & 0x4) != 0;
|
||||
public bool IsHidden => (this.FrameState & 0x200) != 0;
|
||||
public bool IsVisible => !this.IsHidden;
|
||||
public bool IsDIsabled => (this.FrameState & 0x10) != 0;
|
||||
}
|
||||
@@ -1,26 +0,0 @@
|
||||
using System.Core.Extensions;
|
||||
|
||||
namespace Daybreak.API.Interop;
|
||||
|
||||
public sealed class GWAddressCache(Func<nuint> provider)
|
||||
{
|
||||
private readonly Func<nuint> provider = provider.ThrowIfNull();
|
||||
private nuint? cachedAddress;
|
||||
|
||||
public nuint? GetAddress()
|
||||
{
|
||||
if (this.cachedAddress.HasValue)
|
||||
{
|
||||
return this.cachedAddress.Value;
|
||||
}
|
||||
|
||||
var addr = this.provider();
|
||||
if (addr is 0x0)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
this.cachedAddress = addr;
|
||||
return addr;
|
||||
}
|
||||
}
|
||||
+18518
-730
File diff suppressed because it is too large
Load Diff
@@ -1,19 +0,0 @@
|
||||
using System.Runtime.InteropServices;
|
||||
|
||||
namespace Daybreak.API.Interop;
|
||||
|
||||
public sealed class GWDelegateCache<TDelegate>(GWAddressCache cache)
|
||||
where TDelegate : Delegate
|
||||
{
|
||||
public GWAddressCache Cache { get; } = cache;
|
||||
|
||||
public TDelegate? GetDelegate()
|
||||
{
|
||||
if (this.Cache.GetAddress() is not nuint address)
|
||||
{
|
||||
return default;
|
||||
}
|
||||
|
||||
return Marshal.GetDelegateForFunctionPointer<TDelegate>((nint)address);
|
||||
}
|
||||
}
|
||||
@@ -1,312 +0,0 @@
|
||||
using System.ComponentModel;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Runtime.InteropServices;
|
||||
|
||||
namespace Daybreak.API.Interop;
|
||||
|
||||
public abstract class GWFastCall(GWAddressCache target)
|
||||
{
|
||||
public readonly struct Void { }
|
||||
|
||||
public GWAddressCache Cache { get; } = target;
|
||||
public abstract void Initialize();
|
||||
|
||||
/// <summary>
|
||||
/// Builds a thunk for __fastcall with 1 parameter (ECX only).
|
||||
/// </summary>
|
||||
protected unsafe static nint BuildFastCallThunk1(nuint target)
|
||||
{
|
||||
if (target is 0)
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
|
||||
var code = stackalloc byte[10]
|
||||
{
|
||||
0x58, // pop eax (ret)
|
||||
0x59, // pop ecx (arg1)
|
||||
0x50, // push eax (ret)
|
||||
0xB8, 0, 0, 0, 0, // mov eax, TARGET
|
||||
0xFF, 0xE0 // jmp eax
|
||||
}; // 10 bytes total
|
||||
|
||||
const int TARGET_OFFSET = 4;
|
||||
Unsafe.WriteUnaligned(ref code[TARGET_OFFSET], (uint)target);
|
||||
|
||||
var mem = NativeMethods.VirtualAlloc(0, 10, NativeMethods.MEM_COMMIT | NativeMethods.MEM_RESERVE, NativeMethods.PAGE_EXECUTE_READWRITE);
|
||||
if (mem is 0)
|
||||
{
|
||||
throw new Win32Exception(Marshal.GetLastWin32Error(), "VirtualAlloc");
|
||||
}
|
||||
|
||||
Buffer.MemoryCopy(code, (void*)mem, 10, 10);
|
||||
return mem;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Builds a thunk for __fastcall with 2+ parameters (ECX and EDX).
|
||||
/// </summary>
|
||||
protected unsafe static nint BuildFastCallThunk2(nuint target)
|
||||
{
|
||||
if (target is 0)
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
|
||||
var code = stackalloc byte[11]
|
||||
{
|
||||
0x58, // pop eax (ret)
|
||||
0x59, // pop ecx (ctx)
|
||||
0x5A, // pop edx (edxVal)
|
||||
0x50, // push eax (ret)
|
||||
0xB8,0,0,0,0, // mov eax, TARGET
|
||||
0xFF,0xE0 // jmp eax
|
||||
}; // 11 bytes total
|
||||
|
||||
const int TARGET_OFFSET = 5;
|
||||
Unsafe.WriteUnaligned(ref code[TARGET_OFFSET], (uint)target);
|
||||
|
||||
var mem = NativeMethods.VirtualAlloc(0, 11, NativeMethods.MEM_COMMIT | NativeMethods.MEM_RESERVE, NativeMethods.PAGE_EXECUTE_READWRITE);
|
||||
if (mem is 0)
|
||||
{
|
||||
throw new Win32Exception(Marshal.GetLastWin32Error(), "VirtualAlloc");
|
||||
}
|
||||
|
||||
Buffer.MemoryCopy(code, (void*)mem, 11, 11);
|
||||
return mem;
|
||||
}
|
||||
}
|
||||
|
||||
public unsafe sealed class GWFastCall<T1, T2>(GWAddressCache target) : GWFastCall(target)
|
||||
{
|
||||
private delegate* unmanaged[Stdcall]<T1, T2> func = null;
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public unsafe T2? Invoke(T1 t1)
|
||||
{
|
||||
if (this.func is null)
|
||||
{
|
||||
this.Initialize();
|
||||
}
|
||||
|
||||
if (this.func is not null)
|
||||
{
|
||||
if (typeof(T2) == typeof(Void))
|
||||
{
|
||||
((delegate* unmanaged[Stdcall]<T1, void>)this.func)(t1);
|
||||
return default;
|
||||
}
|
||||
|
||||
return this.func(t1);
|
||||
}
|
||||
|
||||
return default;
|
||||
}
|
||||
|
||||
public override void Initialize()
|
||||
{
|
||||
if (this.Cache.GetAddress() is not nuint addr ||
|
||||
addr is 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var call = BuildFastCallThunk1(addr);
|
||||
this.func = (delegate* unmanaged[Stdcall]<T1, T2>)call;
|
||||
}
|
||||
}
|
||||
|
||||
public unsafe sealed class GWFastCall<T1, T2, T3>(GWAddressCache target) : GWFastCall(target)
|
||||
{
|
||||
private delegate* unmanaged[Stdcall]<T1, T2, T3> func = null;
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public unsafe T3? Invoke(T1 t1, T2 t2)
|
||||
{
|
||||
if (this.func is null)
|
||||
{
|
||||
this.Initialize();
|
||||
}
|
||||
|
||||
if (this.func is not null)
|
||||
{
|
||||
if (typeof(T3) == typeof(Void))
|
||||
{
|
||||
((delegate* unmanaged[Stdcall]<T1, T2, void>)this.func)(t1, t2);
|
||||
return default;
|
||||
}
|
||||
|
||||
return this.func(t1, t2);
|
||||
}
|
||||
|
||||
return default;
|
||||
}
|
||||
|
||||
public override void Initialize()
|
||||
{
|
||||
if (this.Cache.GetAddress() is not nuint addr ||
|
||||
addr is 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var call = BuildFastCallThunk2(addr);
|
||||
this.func = (delegate* unmanaged[Stdcall]<T1, T2, T3>)call;
|
||||
}
|
||||
}
|
||||
|
||||
public unsafe sealed class GWFastCall<T1, T2, T3, T4>(GWAddressCache target) : GWFastCall(target)
|
||||
{
|
||||
private delegate* unmanaged[Stdcall]<T1, T2, T3, T4> func = null;
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public unsafe T4? Invoke(T1 t1, T2 t2, T3 t3)
|
||||
{
|
||||
if (this.func is null)
|
||||
{
|
||||
this.Initialize();
|
||||
}
|
||||
|
||||
if (this.func is not null)
|
||||
{
|
||||
if (typeof(T4) == typeof(Void))
|
||||
{
|
||||
((delegate* unmanaged[Stdcall]<T1, T2, T3, void>)this.func)(t1, t2, t3);
|
||||
return default;
|
||||
}
|
||||
|
||||
return this.func(t1, t2, t3);
|
||||
}
|
||||
|
||||
return default;
|
||||
}
|
||||
|
||||
public override void Initialize()
|
||||
{
|
||||
if (this.Cache.GetAddress() is not nuint addr ||
|
||||
addr is 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var call = BuildFastCallThunk2(addr);
|
||||
this.func = (delegate* unmanaged[Stdcall]<T1, T2, T3, T4>)call;
|
||||
}
|
||||
}
|
||||
|
||||
public unsafe sealed class GWFastCall<T1, T2, T3, T4, T5>(GWAddressCache target) : GWFastCall(target)
|
||||
{
|
||||
private delegate* unmanaged[Stdcall]<T1, T2, T3, T4, T5> func = null;
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public unsafe T5? Invoke(T1 t1, T2 t2, T3 t3, T4 t4)
|
||||
{
|
||||
if (this.func is null)
|
||||
{
|
||||
this.Initialize();
|
||||
}
|
||||
|
||||
if (this.func is not null)
|
||||
{
|
||||
if (typeof(T5) == typeof(Void))
|
||||
{
|
||||
((delegate* unmanaged[Stdcall]<T1, T2, T3, T4, void>)this.func)(t1, t2, t3, t4);
|
||||
return default;
|
||||
}
|
||||
|
||||
return this.func(t1, t2, t3, t4);
|
||||
}
|
||||
|
||||
return default;
|
||||
}
|
||||
|
||||
public override void Initialize()
|
||||
{
|
||||
if (this.Cache.GetAddress() is not nuint addr ||
|
||||
addr is 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var call = BuildFastCallThunk2(addr);
|
||||
this.func = (delegate* unmanaged[Stdcall]<T1, T2, T3, T4, T5>)call;
|
||||
}
|
||||
}
|
||||
|
||||
public unsafe sealed class GWFastCall<T1, T2, T3, T4, T5, T6>(GWAddressCache target) : GWFastCall(target)
|
||||
{
|
||||
private delegate* unmanaged[Stdcall]<T1, T2, T3, T4, T5, T6> func = null;
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public unsafe T6? Invoke(T1 t1, T2 t2, T3 t3, T4 t4, T5 t5)
|
||||
{
|
||||
if (this.func is null)
|
||||
{
|
||||
this.Initialize();
|
||||
}
|
||||
|
||||
if (this.func is not null)
|
||||
{
|
||||
if (typeof(T6) == typeof(Void))
|
||||
{
|
||||
((delegate* unmanaged[Stdcall]<T1, T2, T3, T4, T5, void>)this.func)(t1, t2, t3, t4, t5);
|
||||
return default;
|
||||
}
|
||||
|
||||
return this.func(t1, t2, t3, t4, t5);
|
||||
}
|
||||
|
||||
return default;
|
||||
}
|
||||
|
||||
public override void Initialize()
|
||||
{
|
||||
if (this.Cache.GetAddress() is not nuint addr ||
|
||||
addr is 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var call = BuildFastCallThunk2(addr);
|
||||
this.func = (delegate* unmanaged[Stdcall]<T1, T2, T3, T4, T5, T6>)call;
|
||||
}
|
||||
}
|
||||
|
||||
public unsafe sealed class GWFastCall<T1, T2, T3, T4, T5, T6, T7>(GWAddressCache target) : GWFastCall(target)
|
||||
{
|
||||
private delegate* unmanaged[Stdcall]<T1, T2, T3, T4, T5, T6, T7> func = null;
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public unsafe T7? Invoke(T1 t1, T2 t2, T3 t3, T4 t4, T5 t5, T6 t6)
|
||||
{
|
||||
if (this.func is null)
|
||||
{
|
||||
this.Initialize();
|
||||
}
|
||||
|
||||
if (this.func is not null)
|
||||
{
|
||||
if (typeof(T7) == typeof(Void))
|
||||
{
|
||||
((delegate* unmanaged[Stdcall]<T1, T2, T3, T4, T5, T6, void>)this.func)(t1, t2, t3, t4, t5, t6);
|
||||
return default;
|
||||
}
|
||||
|
||||
return this.func(t1, t2, t3, t4, t5, t6);
|
||||
}
|
||||
|
||||
return default;
|
||||
}
|
||||
|
||||
public override void Initialize()
|
||||
{
|
||||
if (this.Cache.GetAddress() is not nuint addr ||
|
||||
addr is 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var call = BuildFastCallThunk2(addr);
|
||||
this.func = (delegate* unmanaged[Stdcall]<T1, T2, T3, T4, T5, T6, T7>)call;
|
||||
}
|
||||
}
|
||||
@@ -1,122 +0,0 @@
|
||||
using System.Runtime.InteropServices;
|
||||
using MinHook;
|
||||
|
||||
namespace Daybreak.API.Interop;
|
||||
|
||||
/// <summary>
|
||||
/// A thin convenience wrapper around MinHook that
|
||||
/// resolves the target address lazily through <see cref="GWAddressCache"/>
|
||||
/// unwraps an existing “CALL/JMP rel32” stub so we patch the real code
|
||||
/// exposes <see cref="Original"/> so your detour can chain
|
||||
/// </summary>
|
||||
public sealed class GWHook<T>(
|
||||
GWAddressCache funcAddress,
|
||||
T detour,
|
||||
bool bypassPreviousHooks = false) : IHook<T>
|
||||
where T : Delegate
|
||||
{
|
||||
private readonly bool bypassPreviousHooks = bypassPreviousHooks;
|
||||
private readonly GWAddressCache addressCache = funcAddress ?? throw new ArgumentNullException(nameof(funcAddress));
|
||||
private readonly T detour = detour ?? throw new ArgumentNullException(nameof(detour));
|
||||
private readonly SemaphoreSlim semaphore = new(1, 1);
|
||||
|
||||
private T? cont; // trampoline returned by MinHook
|
||||
private HookEngine? engine;
|
||||
|
||||
/// <summary>Delegate that calls the next hook / real function.</summary>
|
||||
public T Continue => this.cont ??
|
||||
throw new InvalidOperationException("Hook not initialised – call EnsureInitialized() first.");
|
||||
|
||||
public bool Hooked { get; private set; } = false;
|
||||
public nuint TargetAddress { get; private set; }
|
||||
public nuint ContinueAddress { get; private set; }
|
||||
public nuint DetourAddress { get; private set; }
|
||||
|
||||
/// <summary>Installs the hook exactly once (thread-safe).</summary>
|
||||
public bool EnsureInitialized()
|
||||
{
|
||||
if (this.Hooked)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
this.semaphore.Wait();
|
||||
try
|
||||
{
|
||||
if (this.Hooked)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
var addr = this.addressCache.GetAddress();
|
||||
if (addr.HasValue is false ||
|
||||
addr.Value is 0)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var target = this.bypassPreviousHooks
|
||||
? UnwrapNearBranch(addr.Value)
|
||||
: addr.Value;
|
||||
if (target is 0)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
this.engine = new HookEngine();
|
||||
this.cont = this.engine.CreateHook((nint)target, this.detour);
|
||||
this.engine.EnableHooks();
|
||||
this.TargetAddress = target;
|
||||
this.ContinueAddress = (nuint)this.cont.Method.MethodHandle.GetFunctionPointer();
|
||||
this.DetourAddress = (nuint)this.detour.Method.MethodHandle.GetFunctionPointer();
|
||||
this.Hooked = true;
|
||||
return true;
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
finally
|
||||
{
|
||||
this.semaphore.Release();
|
||||
}
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
this.semaphore.Wait();
|
||||
try
|
||||
{
|
||||
if (!this.Hooked ||
|
||||
this.engine is null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
this.engine.DisableHooks();
|
||||
this.engine.Dispose();
|
||||
|
||||
this.cont = null;
|
||||
this.Hooked = false;
|
||||
}
|
||||
finally
|
||||
{
|
||||
this.semaphore.Release();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Follows a <c>CALL rel32</c> (<c>E8</c>) **or** <c>JMP rel32</c> (<c>E9</c>)
|
||||
/// trampoline that may have been planted by a previous hook.
|
||||
/// </summary>
|
||||
private static unsafe nuint UnwrapNearBranch(nuint addr)
|
||||
{
|
||||
byte op = Marshal.ReadByte((IntPtr)addr);
|
||||
if (op != 0xE8 && op != 0xE9) // not CALL/JMP rel32
|
||||
return addr;
|
||||
|
||||
int rel = Marshal.ReadInt32((IntPtr)(addr + 1));
|
||||
long dst = (long)addr + 5 + rel; // 5-byte instruction
|
||||
return (nuint)dst;
|
||||
}
|
||||
}
|
||||
@@ -1,27 +0,0 @@
|
||||
using System.Runtime.InteropServices;
|
||||
|
||||
namespace Daybreak.API.Interop.GuildWars;
|
||||
|
||||
[StructLayout(LayoutKind.Explicit, Pack = 1, Size = 0xC)]
|
||||
public readonly struct AccountUnlockedCount
|
||||
{
|
||||
[FieldOffset(0x0000)]
|
||||
public readonly uint Id;
|
||||
}
|
||||
|
||||
[StructLayout(LayoutKind.Explicit, Pack = 1, Size = 0x138)]
|
||||
[GWCAEquivalent("AccountContext")]
|
||||
public readonly struct AccountGameContext
|
||||
{
|
||||
[FieldOffset(0x0000)]
|
||||
public readonly GuildWarsArray<AccountUnlockedCount> AccountUnlockedCounts;
|
||||
|
||||
[FieldOffset(0x00b4)]
|
||||
public readonly GuildWarsArray<uint> UnlockedPvpHeroes;
|
||||
|
||||
[FieldOffset(0x0124)]
|
||||
public readonly GuildWarsArray<uint> UnlockedAccountSkills;
|
||||
|
||||
[FieldOffset(0x0134)]
|
||||
public readonly uint AccountFlags;
|
||||
}
|
||||
@@ -1,16 +0,0 @@
|
||||
using System.Runtime.InteropServices;
|
||||
|
||||
namespace Daybreak.API.Interop.GuildWars;
|
||||
|
||||
[StructLayout(LayoutKind.Sequential, Pack = 1, Size = 0x1C)]
|
||||
[GWCAEquivalent("AccountInfo")]
|
||||
public readonly unsafe struct AccountInfoContext
|
||||
{
|
||||
public readonly char* AccountName;
|
||||
public readonly uint Wins;
|
||||
public readonly uint Losses;
|
||||
public readonly uint Rating;
|
||||
public readonly uint QualifierPoints;
|
||||
public readonly uint Rank;
|
||||
public readonly uint TournamentRewardPoints;
|
||||
}
|
||||
@@ -1,254 +0,0 @@
|
||||
using System.Numerics;
|
||||
using System.Runtime.InteropServices;
|
||||
|
||||
namespace Daybreak.API.Interop.GuildWars;
|
||||
|
||||
[Flags]
|
||||
public enum AgentType : uint
|
||||
{
|
||||
Living = 0xDB,
|
||||
Gadget = 0x200,
|
||||
Item = 0x400
|
||||
}
|
||||
|
||||
[Flags]
|
||||
public enum AgentModelType : ushort
|
||||
{
|
||||
NPC = 0x2000,
|
||||
Player = 0x3000
|
||||
}
|
||||
|
||||
[Flags]
|
||||
public enum LivingAgentState : uint
|
||||
{
|
||||
None = 0,
|
||||
InCombatStance = 0x000001,
|
||||
HasQuest = 0x000002,
|
||||
Dead = 0x000008,
|
||||
Female = 0x000200,
|
||||
HasBossGlow = 0x000400,
|
||||
HidingCape = 0x001000,
|
||||
CanBeViewedInPartyWindow = 0x020000,
|
||||
SpawnedSpirit = 0x040000,
|
||||
BeingObservedOrPlayer = 0x400000
|
||||
}
|
||||
|
||||
public enum LivingAgentAllegiance : byte
|
||||
{
|
||||
Ally_NonAttackable = 0x1,
|
||||
Neutral = 0x2,
|
||||
Enemy = 0x3,
|
||||
Spirit_Pet = 0x4,
|
||||
Minion = 0x5,
|
||||
Npc_Minipet = 0x6
|
||||
};
|
||||
|
||||
|
||||
[Flags]
|
||||
public enum LivingAgentEffects : uint
|
||||
{
|
||||
None = 0,
|
||||
Bleeding = 0x0001,
|
||||
Conditioned = 0x0002,
|
||||
Crippled1 = 0x0008, // Part of the Crippled check
|
||||
Crippled = 0x000A, // Combined flag for crippled (0x0008 | 0x0002)
|
||||
Dead = 0x0010,
|
||||
DeepWound = 0x0020,
|
||||
Poisoned = 0x0040,
|
||||
Enchanted = 0x0080,
|
||||
DegenHexed = 0x0400,
|
||||
Hexed = 0x0800,
|
||||
WeaponSpelled = 0x8000
|
||||
}
|
||||
|
||||
public enum LivingAgentModelState : ushort
|
||||
{
|
||||
// Idle states
|
||||
Idle1 = 64,
|
||||
Idle2 = 68,
|
||||
Idle3 = 100,
|
||||
|
||||
// Casting states
|
||||
Casting1 = 65,
|
||||
Casting2 = 581,
|
||||
|
||||
// Moving states
|
||||
Moving1 = 12,
|
||||
Moving2 = 76,
|
||||
Moving3 = 204,
|
||||
|
||||
// Attacking states
|
||||
Attacking1 = 96,
|
||||
Attacking2 = 1088,
|
||||
Attacking3 = 1120,
|
||||
|
||||
// Other states
|
||||
KnockedDown = 1104
|
||||
}
|
||||
|
||||
[StructLayout(LayoutKind.Sequential, Pack = 1)]
|
||||
[GWCAEquivalent("AgentEffects")]
|
||||
public readonly struct AgentEffects
|
||||
{
|
||||
public readonly uint AgentId;
|
||||
public readonly GuildWarsArray<Buff> Buffs;
|
||||
public readonly GuildWarsArray<Effect> Effects;
|
||||
}
|
||||
|
||||
[StructLayout(LayoutKind.Explicit, Pack = 1)]
|
||||
[GWCAEquivalent("AgentContext")]
|
||||
public readonly struct AgentGameContext
|
||||
{
|
||||
[FieldOffset(0x01AC)]
|
||||
public readonly uint InstanceTimer;
|
||||
}
|
||||
|
||||
[StructLayout(LayoutKind.Explicit, Pack = 1, Size = 196)]
|
||||
[GWCAEquivalent("Agent")]
|
||||
public readonly struct AgentContext
|
||||
{
|
||||
[FieldOffset(0x0014)]
|
||||
public readonly uint AgentInstanceTimer;
|
||||
|
||||
[FieldOffset(0x002C)]
|
||||
public readonly uint AgentId;
|
||||
|
||||
[FieldOffset(0x0074)]
|
||||
public readonly GamePos Pos;
|
||||
|
||||
[FieldOffset(0x009C)]
|
||||
public readonly AgentType Type;
|
||||
|
||||
[FieldOffset(0x00A0)]
|
||||
public readonly Vector2 Velocity;
|
||||
}
|
||||
|
||||
[StructLayout(LayoutKind.Explicit, Pack = 1, Size = 212)]
|
||||
[GWCAEquivalent("AgentItem")]
|
||||
public readonly struct AgentItemContext
|
||||
{
|
||||
[FieldOffset(0x00C4)]
|
||||
public readonly uint OwnerId;
|
||||
|
||||
[FieldOffset(0x00C8)]
|
||||
public readonly uint ItemId;
|
||||
|
||||
[FieldOffset(0x00D0)]
|
||||
public readonly uint ExtraType;
|
||||
}
|
||||
|
||||
[StructLayout(LayoutKind.Explicit, Pack = 1, Size = 228)]
|
||||
[GWCAEquivalent("AgentGadget")]
|
||||
public readonly struct AgentGadgetContext
|
||||
{
|
||||
[FieldOffset(0x00CC)]
|
||||
public readonly uint ExtraType;
|
||||
|
||||
[FieldOffset(0x00D0)]
|
||||
public readonly uint GadgetId;
|
||||
}
|
||||
|
||||
[StructLayout(LayoutKind.Explicit, Pack = 1, Size = 0x1C0)]
|
||||
[GWCAEquivalent("AgentLiving")]
|
||||
public readonly unsafe struct AgentLivingContext
|
||||
{
|
||||
[FieldOffset(0x00C4)]
|
||||
public readonly uint OwnerId;
|
||||
|
||||
[FieldOffset(0x00F4)]
|
||||
public readonly ushort AgentType; // All non-player have an identifier. Two of the same mob have the same identifier;
|
||||
|
||||
[FieldOffset(0x00F6)]
|
||||
public readonly AgentModelType AgentModelType;
|
||||
|
||||
[FieldOffset(0x0108)]
|
||||
public readonly TagInfo* Tags;
|
||||
|
||||
[FieldOffset(0x010E)]
|
||||
public readonly byte Primary;
|
||||
|
||||
[FieldOffset(0x010F)]
|
||||
public readonly byte Secondary;
|
||||
|
||||
[FieldOffset(0x0110)]
|
||||
public readonly byte Level;
|
||||
|
||||
[FieldOffset(0x0111)]
|
||||
public readonly byte TeamId;
|
||||
|
||||
[FieldOffset(0x0118)]
|
||||
public readonly float EnergyRegen;
|
||||
|
||||
[FieldOffset(0x0120)]
|
||||
public readonly float Energy;
|
||||
|
||||
[FieldOffset(0x0124)]
|
||||
public readonly uint MaxEnergy;
|
||||
|
||||
[FieldOffset(0x012C)]
|
||||
public readonly float HealthRegen;
|
||||
|
||||
[FieldOffset(0x0134)]
|
||||
public readonly float Health;
|
||||
|
||||
[FieldOffset(0x0138)]
|
||||
public readonly uint MaxHealth;
|
||||
|
||||
[FieldOffset(0x013C)]
|
||||
public readonly uint Effects;
|
||||
|
||||
[FieldOffset(0x0158)]
|
||||
public readonly LivingAgentModelState ModelState;
|
||||
|
||||
[FieldOffset(0x015C)]
|
||||
public readonly LivingAgentState State;
|
||||
|
||||
[FieldOffset(0x0184)]
|
||||
public readonly uint LoginNumber;
|
||||
|
||||
[FieldOffset(0x01B5)]
|
||||
public readonly LivingAgentAllegiance Allegiance;
|
||||
|
||||
public readonly bool IsAlive => (!this.State.HasFlag(LivingAgentState.Dead)) && this.Health > 0f;
|
||||
|
||||
public readonly bool IsPlayer => this.LoginNumber is not 0;
|
||||
|
||||
public readonly bool IsNpc => this.LoginNumber is 0;
|
||||
}
|
||||
|
||||
[StructLayout(LayoutKind.Explicit, Pack = 1, Size = 52)]
|
||||
[GWCAEquivalent("MapAgent")]
|
||||
public readonly struct MapAgentContext
|
||||
{
|
||||
[FieldOffset(0x0000)]
|
||||
public readonly float CurrentEnergy;
|
||||
|
||||
[FieldOffset(0x0004)]
|
||||
public readonly float MaxEnergy;
|
||||
|
||||
[FieldOffset(0x0008)]
|
||||
public readonly float EnergyRegen;
|
||||
|
||||
[FieldOffset(0x000C)]
|
||||
public readonly uint SkillTimestamp;
|
||||
|
||||
[FieldOffset(0x0020)]
|
||||
public readonly float CurrentHealth;
|
||||
|
||||
[FieldOffset(0x0024)]
|
||||
public readonly float MaxHealth;
|
||||
|
||||
[FieldOffset(0x0028)]
|
||||
public readonly float HealthRegen;
|
||||
|
||||
[FieldOffset(0x0030)]
|
||||
public readonly LivingAgentEffects Effects;
|
||||
}
|
||||
|
||||
[StructLayout(LayoutKind.Explicit, Pack = 1, Size = 0x38)]
|
||||
[GWCAEquivalent("AgentInfo")]
|
||||
public readonly unsafe struct AgentInfo
|
||||
{
|
||||
[FieldOffset(0x0034)]
|
||||
public readonly char* NameEncoded;
|
||||
}
|
||||
@@ -1,96 +0,0 @@
|
||||
using System.Runtime.InteropServices;
|
||||
|
||||
namespace Daybreak.API.Interop.GuildWars;
|
||||
|
||||
[StructLayout(LayoutKind.Sequential, Pack = 1)]
|
||||
[GWCAEquivalent("Attribute")]
|
||||
public readonly struct AttributeContext
|
||||
{
|
||||
public readonly uint Id;
|
||||
public readonly uint LevelBase;
|
||||
public readonly uint Level;
|
||||
public readonly uint DecrementPoints;
|
||||
public readonly uint IncrementPoints;
|
||||
}
|
||||
|
||||
[StructLayout(LayoutKind.Sequential, Pack = 1)]
|
||||
[GWCAEquivalent("AttributeInfo")]
|
||||
public readonly struct AttributeInfo
|
||||
{
|
||||
public readonly uint ProfessionId;
|
||||
public readonly uint AttributeId;
|
||||
public readonly uint NameId;
|
||||
public readonly uint DescriptionId;
|
||||
public readonly uint IsPve;
|
||||
}
|
||||
|
||||
[StructLayout(LayoutKind.Sequential, Pack = 1)]
|
||||
[GWCAEquivalent("PartyAttribute")]
|
||||
public readonly struct PartyAttribute
|
||||
{
|
||||
public readonly uint AgentId;
|
||||
public readonly AttributeContext Attribute0;
|
||||
public readonly AttributeContext Attribute1;
|
||||
public readonly AttributeContext Attribute2;
|
||||
public readonly AttributeContext Attribute3;
|
||||
public readonly AttributeContext Attribute4;
|
||||
public readonly AttributeContext Attribute5;
|
||||
public readonly AttributeContext Attribute6;
|
||||
public readonly AttributeContext Attribute7;
|
||||
public readonly AttributeContext Attribute8;
|
||||
public readonly AttributeContext Attribute9;
|
||||
public readonly AttributeContext Attribute10;
|
||||
public readonly AttributeContext Attribute11;
|
||||
public readonly AttributeContext Attribute12;
|
||||
public readonly AttributeContext Attribute13;
|
||||
public readonly AttributeContext Attribute14;
|
||||
public readonly AttributeContext Attribute15;
|
||||
public readonly AttributeContext Attribute16;
|
||||
public readonly AttributeContext Attribute17;
|
||||
public readonly AttributeContext Attribute18;
|
||||
public readonly AttributeContext Attribute19;
|
||||
public readonly AttributeContext Attribute20;
|
||||
public readonly AttributeContext Attribute21;
|
||||
public readonly AttributeContext Attribute22;
|
||||
public readonly AttributeContext Attribute23;
|
||||
public readonly AttributeContext Attribute24;
|
||||
public readonly AttributeContext Attribute25;
|
||||
public readonly AttributeContext Attribute26;
|
||||
public readonly AttributeContext Attribute27;
|
||||
public readonly AttributeContext Attribute28;
|
||||
public readonly AttributeContext Attribute29;
|
||||
public readonly AttributeContext Attribute30;
|
||||
public readonly AttributeContext Attribute31;
|
||||
public readonly AttributeContext Attribute32;
|
||||
public readonly AttributeContext Attribute33;
|
||||
public readonly AttributeContext Attribute34;
|
||||
public readonly AttributeContext Attribute35;
|
||||
public readonly AttributeContext Attribute36;
|
||||
public readonly AttributeContext Attribute37;
|
||||
public readonly AttributeContext Attribute38;
|
||||
public readonly AttributeContext Attribute39;
|
||||
public readonly AttributeContext Attribute40;
|
||||
public readonly AttributeContext Attribute41;
|
||||
public readonly AttributeContext Attribute42;
|
||||
public readonly AttributeContext Attribute43;
|
||||
public readonly AttributeContext Attribute44;
|
||||
public readonly AttributeContext Attribute45;
|
||||
public readonly AttributeContext Attribute46;
|
||||
public readonly AttributeContext Attribute47;
|
||||
public readonly AttributeContext Attribute48;
|
||||
public readonly AttributeContext Attribute49;
|
||||
public readonly AttributeContext Attribute50;
|
||||
public readonly AttributeContext Attribute51;
|
||||
public readonly AttributeContext Attribute52;
|
||||
public readonly AttributeContext Attribute53;
|
||||
|
||||
public readonly AttributeContext[] Attributes =>
|
||||
[
|
||||
this.Attribute0, this.Attribute1, this.Attribute2, this.Attribute3, this.Attribute4, this.Attribute5, this.Attribute6, this.Attribute7, this.Attribute8, this.Attribute9,
|
||||
this.Attribute10, this.Attribute11, this.Attribute12, this.Attribute13, this.Attribute14, this.Attribute15, this.Attribute16, this.Attribute17, this.Attribute18, this.Attribute19,
|
||||
this.Attribute20, this.Attribute21, this.Attribute22, this.Attribute23, this.Attribute24, this.Attribute25, this.Attribute26, this.Attribute27, this.Attribute28, this.Attribute29,
|
||||
this.Attribute30, this.Attribute31, this.Attribute32, this.Attribute33, this.Attribute34, this.Attribute35, this.Attribute36, this.Attribute37, this.Attribute38, this.Attribute39,
|
||||
this.Attribute40, this.Attribute41, this.Attribute42, this.Attribute43, this.Attribute44, this.Attribute45, this.Attribute46, this.Attribute47, this.Attribute48, this.Attribute49,
|
||||
this.Attribute50, this.Attribute51, this.Attribute52, this.Attribute53
|
||||
];
|
||||
}
|
||||
@@ -1,31 +0,0 @@
|
||||
using System.Runtime.InteropServices;
|
||||
|
||||
namespace Daybreak.API.Interop.GuildWars;
|
||||
|
||||
public enum BagType
|
||||
{
|
||||
None,
|
||||
Inventory,
|
||||
Equipped,
|
||||
NotCollected,
|
||||
Storage,
|
||||
MaterialsStorage
|
||||
}
|
||||
|
||||
[StructLayout(LayoutKind.Explicit, Pack = 1)]
|
||||
[GWCAEquivalent("Bag")]
|
||||
public readonly unsafe struct Bag
|
||||
{
|
||||
[FieldOffset(0x0000)]
|
||||
public readonly BagType Type;
|
||||
[FieldOffset(0x0004)]
|
||||
public readonly uint Index;
|
||||
[FieldOffset(0x000C)]
|
||||
public readonly uint ContainerItem;
|
||||
[FieldOffset(0x0010)]
|
||||
public readonly uint ItemsCount;
|
||||
[FieldOffset(0x0014)]
|
||||
public readonly Bag* Bags;
|
||||
[FieldOffset(0x0018)]
|
||||
public readonly GuildWarsArray<WrappedPointer<Item>> Items;
|
||||
}
|
||||
@@ -1,9 +0,0 @@
|
||||
namespace Daybreak.API.Interop.GuildWars;
|
||||
|
||||
[GWCAEquivalent("HeroBehavior")]
|
||||
public enum Behavior
|
||||
{
|
||||
Fight,
|
||||
Guard,
|
||||
AvoidCombat
|
||||
}
|
||||
@@ -1,51 +0,0 @@
|
||||
using System.Extensions;
|
||||
using System.Runtime.InteropServices;
|
||||
|
||||
namespace Daybreak.API.Interop.GuildWars;
|
||||
|
||||
[StructLayout(LayoutKind.Explicit, Pack = 1)]
|
||||
[GWCAEquivalent("CharContext")]
|
||||
public readonly struct CharContext
|
||||
{
|
||||
[FieldOffset(0x0064)]
|
||||
public readonly Uuid PlayerUuid;
|
||||
|
||||
[FieldOffset(0x0074)]
|
||||
public readonly Array20Char PlayerName;
|
||||
|
||||
[FieldOffset(0x0198)]
|
||||
public readonly uint MapId;
|
||||
|
||||
[FieldOffset(0x019C)]
|
||||
public readonly uint IsExporable;
|
||||
|
||||
[FieldOffset(0x01A0)]
|
||||
public readonly Array24Byte Host;
|
||||
|
||||
[FieldOffset(0x01B8)]
|
||||
public readonly uint PlayerId;
|
||||
|
||||
[FieldOffset(0x0220)]
|
||||
public readonly uint DistrictNumber;
|
||||
|
||||
[FieldOffset(0x0224)]
|
||||
public readonly Language Language;
|
||||
|
||||
[FieldOffset(0x0228)]
|
||||
public readonly uint ObserveMapId;
|
||||
|
||||
[FieldOffset(0x022C)]
|
||||
public readonly uint CurrentMapId;
|
||||
|
||||
[FieldOffset(0x0230)]
|
||||
public readonly uint ObserveMapType;
|
||||
|
||||
[FieldOffset(0x0234)]
|
||||
public readonly uint CurrentMapType;
|
||||
|
||||
[FieldOffset(0x02A4)]
|
||||
public readonly uint PlayerNumber;
|
||||
|
||||
[FieldOffset(0x03B8)]
|
||||
public readonly Array64Char PlayerEmail;
|
||||
}
|
||||
@@ -1,23 +0,0 @@
|
||||
using System.Extensions;
|
||||
using System.Runtime.InteropServices;
|
||||
|
||||
namespace Daybreak.API.Interop.GuildWars;
|
||||
|
||||
[StructLayout(LayoutKind.Explicit, Pack = 1)]
|
||||
[GWCAEquivalent("CharacterInformation")]
|
||||
public readonly struct CharInfoContext
|
||||
{
|
||||
[FieldOffset(0x0008)]
|
||||
public readonly Uuid Uuid;
|
||||
[FieldOffset(0x0018)]
|
||||
public readonly Array20Char Name;
|
||||
[FieldOffset(0x0040)]
|
||||
public readonly Array17Uint Props;
|
||||
|
||||
public readonly uint MapId => (this.Props[0] >> 16) & 0xFFFF;
|
||||
public readonly uint Primary => (this.Props[2] >> 20) & 0xF;
|
||||
public readonly uint Secondary => (this.Props[7] >> 10) & 0xF;
|
||||
public readonly uint Campaign => this.Props[7] & 0xF;
|
||||
public readonly uint Level => (this.Props[7] >> 4) & 0x3F;
|
||||
public readonly bool IsPvp => ((this.Props[7] >> 9) & 0x1) == 0x1;
|
||||
}
|
||||
@@ -1,184 +0,0 @@
|
||||
namespace Daybreak.API.Interop.GuildWars;
|
||||
|
||||
[GWCAEquivalent("ControlAction")]
|
||||
public enum ControlAction : uint
|
||||
{
|
||||
None = 0,
|
||||
Screenshot = 0xAE,
|
||||
|
||||
// Panels
|
||||
CloseAllPanels = 0x85,
|
||||
ToggleInventoryWindow = 0x8B,
|
||||
OpenScoreChart = 0xBD,
|
||||
OpenTemplateManager = 0xD3,
|
||||
OpenSaveEquipmentTemplate = 0xD4,
|
||||
OpenSaveSkillTemplate = 0xD5,
|
||||
OpenParty = 0xBF,
|
||||
OpenGuild = 0xBA,
|
||||
OpenFriends = 0xB9,
|
||||
ToggleAllBags = 0xB8,
|
||||
OpenMissionMap = 0xB6,
|
||||
OpenBag2 = 0xB5,
|
||||
OpenBag1 = 0xB4,
|
||||
OpenBelt = 0xB3,
|
||||
OpenBackpack = 0xB2,
|
||||
OpenSkillsAndAttributes = 0x8F,
|
||||
OpenQuestLog = 0x8E,
|
||||
OpenWorldMap = 0x8C,
|
||||
OpenOptions = 0x8D,
|
||||
OpenHero = 0x8A,
|
||||
|
||||
// Weapon sets
|
||||
CycleEquipment = 0x86,
|
||||
ActivateWeaponSet1 = 0x81,
|
||||
ActivateWeaponSet2,
|
||||
ActivateWeaponSet3,
|
||||
ActivateWeaponSet4,
|
||||
|
||||
DropItem = 0xCD, // drops bundle item >> flags, ashes, etc
|
||||
|
||||
// Chat
|
||||
CharReply = 0xBE,
|
||||
OpenChat = 0xA1,
|
||||
OpenAlliance = 0x88,
|
||||
|
||||
ReverseCamera = 0x90,
|
||||
StrafeLeft = 0x91,
|
||||
StrafeRight = 0x92,
|
||||
TurnLeft = 0xA2,
|
||||
TurnRight = 0xA3,
|
||||
MoveBackward = 0xAC,
|
||||
MoveForward = 0xAD,
|
||||
CancelAction = 0xAF,
|
||||
Interact = 0x80,
|
||||
ReverseDirection = 0xB1,
|
||||
Autorun = 0xB7,
|
||||
Follow = 0xCC,
|
||||
|
||||
// Targeting
|
||||
TargetPartyMember1 = 0x96,
|
||||
TargetPartyMember2,
|
||||
TargetPartyMember3,
|
||||
TargetPartyMember4,
|
||||
TargetPartyMember5,
|
||||
TargetPartyMember6,
|
||||
TargetPartyMember7,
|
||||
TargetPartyMember8,
|
||||
TargetPartyMember9 = 0xC6,
|
||||
TargetPartyMember10,
|
||||
TargetPartyMember11,
|
||||
TargetPartyMember12,
|
||||
|
||||
TargetNearestItem = 0xC3,
|
||||
TargetNextItem = 0xC4,
|
||||
TargetPreviousItem = 0xC5,
|
||||
TargetPartyMemberNext = 0xCA,
|
||||
TargetPartyMemberPrevious = 0xCB,
|
||||
TargetAllyNearest = 0xBC,
|
||||
ClearTarget = 0xE3,
|
||||
TargetSelf = 0xA0, // also 0x96
|
||||
TargetPriorityTarget = 0x9F,
|
||||
TargetNearestEnemy = 0x93,
|
||||
TargetNextEnemy = 0x95,
|
||||
TargetPreviousEnemy = 0x9E,
|
||||
|
||||
ShowOthers = 0x89,
|
||||
ShowTargets = 0x94,
|
||||
|
||||
CameraZoomIn = 0xCE,
|
||||
CameraZoomOut = 0xCF,
|
||||
|
||||
// Party/Hero commands
|
||||
ClearPartyCommands = 0xDB,
|
||||
CommandParty = 0xD6,
|
||||
CommandHero1,
|
||||
CommandHero2,
|
||||
CommandHero3,
|
||||
CommandHero4 = 0x102,
|
||||
CommandHero5,
|
||||
CommandHero6,
|
||||
CommandHero7,
|
||||
|
||||
OpenHero1PetCommander = 0xE0,
|
||||
OpenHero2PetCommander,
|
||||
OpenHero3PetCommander,
|
||||
OpenHero4PetCommander = 0xFE,
|
||||
OpenHero5PetCommander,
|
||||
OpenHero6PetCommander,
|
||||
OpenHero7PetCommander,
|
||||
OpenHeroCommander1 = 0xDC,
|
||||
OpenHeroCommander2,
|
||||
OpenHeroCommander3,
|
||||
OpenPetCommander,
|
||||
OpenHeroCommander4 = 0x126,
|
||||
OpenHeroCommander5,
|
||||
OpenHeroCommander6,
|
||||
OpenHeroCommander7,
|
||||
|
||||
Hero1Skill1 = 0xE5,
|
||||
Hero1Skill2,
|
||||
Hero1Skill3,
|
||||
Hero1Skill4,
|
||||
Hero1Skill5,
|
||||
Hero1Skill6,
|
||||
Hero1Skill7,
|
||||
Hero1Skill8,
|
||||
Hero2Skill1,
|
||||
Hero2Skill2,
|
||||
Hero2Skill3,
|
||||
Hero2Skill4,
|
||||
Hero2Skill5,
|
||||
Hero2Skill6,
|
||||
Hero2Skill7,
|
||||
Hero2Skill8,
|
||||
Hero3Skill1,
|
||||
Hero3Skill2,
|
||||
Hero3Skill3,
|
||||
Hero3Skill4,
|
||||
Hero3Skill5,
|
||||
Hero3Skill6,
|
||||
Hero3Skill7,
|
||||
Hero3Skill8,
|
||||
Hero4Skill1 = 0x106,
|
||||
Hero4Skill2,
|
||||
Hero4Skill3,
|
||||
Hero4Skill4,
|
||||
Hero4Skill5,
|
||||
Hero4Skill6,
|
||||
Hero4Skill7,
|
||||
Hero4Skill8,
|
||||
Hero5Skill1,
|
||||
Hero5Skill2,
|
||||
Hero5Skill3,
|
||||
Hero5Skill4,
|
||||
Hero5Skill5,
|
||||
Hero5Skill6,
|
||||
Hero5Skill7,
|
||||
Hero5Skill8,
|
||||
Hero6Skill1,
|
||||
Hero6Skill2,
|
||||
Hero6Skill3,
|
||||
Hero6Skill4,
|
||||
Hero6Skill5,
|
||||
Hero6Skill6,
|
||||
Hero6Skill7,
|
||||
Hero6Skill8,
|
||||
Hero7Skill1,
|
||||
Hero7Skill2,
|
||||
Hero7Skill3,
|
||||
Hero7Skill4,
|
||||
Hero7Skill5,
|
||||
Hero7Skill6,
|
||||
Hero7Skill7,
|
||||
Hero7Skill8,
|
||||
|
||||
// Skills
|
||||
UseSkill1 = 0xA4,
|
||||
UseSkill2,
|
||||
UseSkill3,
|
||||
UseSkill4,
|
||||
UseSkill5,
|
||||
UseSkill6,
|
||||
UseSkill7,
|
||||
UseSkill8
|
||||
};
|
||||
@@ -1,20 +0,0 @@
|
||||
namespace Daybreak.API.Interop.GuildWars;
|
||||
|
||||
[GWCAEquivalent("District")]
|
||||
public enum District
|
||||
{
|
||||
Current,
|
||||
International,
|
||||
American,
|
||||
EuropeEnglish,
|
||||
EuropeFrench,
|
||||
EuropeGerman,
|
||||
EuropeItalian,
|
||||
EuropeSpanish,
|
||||
EuropePolish,
|
||||
EuropeRussian,
|
||||
AsiaKorean,
|
||||
AsiaChinese,
|
||||
AsiaJapanese,
|
||||
Unknown = 0xff
|
||||
}
|
||||
@@ -1,90 +0,0 @@
|
||||
using Daybreak.API.Models;
|
||||
using System.Extensions;
|
||||
using System.Runtime.InteropServices;
|
||||
|
||||
namespace Daybreak.API.Interop.GuildWars;
|
||||
|
||||
// Very incomplete. GWCA contains a much better definition
|
||||
[StructLayout(LayoutKind.Explicit, Pack = 1, Size = 0x1AC)]
|
||||
[GWCAEquivalent("Frame")]
|
||||
public readonly struct Frame
|
||||
{
|
||||
[FieldOffset(0x0008)]
|
||||
public readonly uint FrameLayout;
|
||||
|
||||
[FieldOffset(0x0018)]
|
||||
public readonly uint VisibilityFlags;
|
||||
|
||||
[FieldOffset(0x0020)]
|
||||
public readonly uint Type;
|
||||
|
||||
[FieldOffset(0x0024)]
|
||||
public readonly uint TemplateType;
|
||||
|
||||
[FieldOffset(0x00A8)]
|
||||
public readonly GuildWarsArray<FrameInteractionCallback> FrameCallbacks;
|
||||
|
||||
[FieldOffset(0x00b8)]
|
||||
public readonly uint ChildOffsetId;
|
||||
|
||||
[FieldOffset(0x00bc)]
|
||||
public readonly uint FrameId;
|
||||
|
||||
[FieldOffset(0x0128)]
|
||||
public readonly FrameRelation Relation;
|
||||
|
||||
[FieldOffset(0x018C)]
|
||||
public readonly uint FrameState;
|
||||
|
||||
public bool IsCreated => (this.FrameState & 0x4) != 0;
|
||||
public bool IsHidden => (this.FrameState & 0x200) != 0;
|
||||
public bool IsVisible => !this.IsHidden;
|
||||
public bool IsDIsabled => (this.FrameState & 0x10) != 0;
|
||||
}
|
||||
|
||||
[StructLayout(LayoutKind.Explicit, Pack = 1, Size = 0x1C)]
|
||||
public readonly unsafe struct FrameRelation
|
||||
{
|
||||
[FieldOffset(0x0000)]
|
||||
public readonly FrameRelation* Parent;
|
||||
|
||||
[FieldOffset(0x000C)]
|
||||
public readonly uint FrameHashId;
|
||||
}
|
||||
|
||||
[StructLayout(LayoutKind.Sequential, Pack = 1)]
|
||||
public readonly unsafe struct InteractionMessage
|
||||
{
|
||||
public readonly uint FrameId;
|
||||
public readonly UIMessage MessageId;
|
||||
public readonly void** WParam;
|
||||
}
|
||||
|
||||
public unsafe delegate void UIInteractionCallback(InteractionMessage* message, void* wParam, void* lParam);
|
||||
|
||||
[StructLayout(LayoutKind.Sequential, Pack = 1, Size = 0xC)]
|
||||
public unsafe readonly struct FrameInteractionCallback
|
||||
{
|
||||
public readonly void* Callback; // UIInteractionCallback
|
||||
public readonly void* UiCtl_Context;
|
||||
}
|
||||
|
||||
[StructLayout(LayoutKind.Explicit, Pack = 1)]
|
||||
public readonly unsafe struct CharSelectorContext
|
||||
{
|
||||
[FieldOffset(0x0000)]
|
||||
public readonly uint Vtable;
|
||||
|
||||
[FieldOffset(0x0004)]
|
||||
public readonly uint FrameId;
|
||||
|
||||
[FieldOffset(0x0008)]
|
||||
public readonly GuildWarsArray<WrappedPointer<CharSelectorChar>> Chars;
|
||||
}
|
||||
|
||||
[StructLayout(LayoutKind.Explicit, Pack = 1)]
|
||||
public readonly struct CharSelectorChar
|
||||
{
|
||||
[FieldOffset(0x0020)]
|
||||
public readonly Array20Char Name;
|
||||
}
|
||||
@@ -1,35 +0,0 @@
|
||||
using System.Runtime.InteropServices;
|
||||
|
||||
namespace Daybreak.API.Interop.GuildWars;
|
||||
|
||||
[StructLayout(LayoutKind.Explicit, Pack = 1)]
|
||||
[GWCAEquivalent("GameContext")]
|
||||
public readonly unsafe struct GameContext
|
||||
{
|
||||
[FieldOffset(0x0008)]
|
||||
public readonly AgentGameContext* AgentGameContext;
|
||||
|
||||
[FieldOffset(0x0014)]
|
||||
public readonly MapContext* MapContext;
|
||||
|
||||
[FieldOffset(0x0018)]
|
||||
public readonly TextParserContext* TextParserContext;
|
||||
|
||||
[FieldOffset(0x0028)]
|
||||
public readonly AccountGameContext* AccountContext;
|
||||
|
||||
[FieldOffset(0x002C)]
|
||||
public readonly WorldContext* WorldContext;
|
||||
|
||||
[FieldOffset(0x003C)]
|
||||
public readonly GuildContext* GuildContext;
|
||||
|
||||
[FieldOffset(0x0040)]
|
||||
public readonly ItemContext* ItemContext;
|
||||
|
||||
[FieldOffset(0x0044)]
|
||||
public readonly CharContext* CharContext;
|
||||
|
||||
[FieldOffset(0x004C)]
|
||||
public readonly PartyContext* PartyContext;
|
||||
}
|
||||
@@ -1,12 +0,0 @@
|
||||
using System.Runtime.InteropServices;
|
||||
|
||||
namespace Daybreak.API.Interop.GuildWars;
|
||||
|
||||
[StructLayout(LayoutKind.Sequential, Pack = 1)]
|
||||
[GWCAEquivalent("GamePos")]
|
||||
public readonly struct GamePos
|
||||
{
|
||||
public readonly float X;
|
||||
public readonly float Y;
|
||||
public readonly uint Plane;
|
||||
}
|
||||
@@ -1,11 +0,0 @@
|
||||
using System.Runtime.InteropServices;
|
||||
|
||||
namespace Daybreak.API.Interop.GuildWars;
|
||||
|
||||
[StructLayout(LayoutKind.Explicit, Pack = 1, Size = 0x78)]
|
||||
[GWCAEquivalent("GameplayContext")]
|
||||
public readonly struct GameplayContext
|
||||
{
|
||||
[FieldOffset(0x4C)]
|
||||
public readonly float MissionMapZoom;
|
||||
}
|
||||
@@ -1,111 +0,0 @@
|
||||
using System.Extensions;
|
||||
using System.Runtime.InteropServices;
|
||||
|
||||
namespace Daybreak.API.Interop.GuildWars;
|
||||
|
||||
[StructLayout(LayoutKind.Explicit, Pack = 1)]
|
||||
[GWCAEquivalent("GuildContext")]
|
||||
public readonly struct GuildContext
|
||||
{
|
||||
[FieldOffset(0x0034)]
|
||||
public readonly Array20Char PlayerName;
|
||||
|
||||
[FieldOffset(0x0060)]
|
||||
public readonly uint PlayerBuildIndex;
|
||||
|
||||
[FieldOffset(0x0078)]
|
||||
public readonly Array256Char Announcement;
|
||||
|
||||
[FieldOffset(0x0287)]
|
||||
public readonly Array20Char AnnouncementAuthor;
|
||||
|
||||
[FieldOffset(0x02A0)]
|
||||
public readonly uint PlayerGuildRank;
|
||||
|
||||
[FieldOffset(0x02A8)]
|
||||
public readonly GuildWarsArray<TownAlliance> FactionOutpostGuilds;
|
||||
|
||||
[FieldOffset(0x02B8)]
|
||||
public readonly uint KurzickTownCount;
|
||||
|
||||
[FieldOffset(0x02BC)]
|
||||
public readonly uint LuxonTownCount;
|
||||
|
||||
[FieldOffset(0x02F8)]
|
||||
public readonly GuildWarsArray<WrappedPointer<Guild>> Guilds;
|
||||
|
||||
[FieldOffset(0x0358)]
|
||||
public readonly GuildWarsArray<WrappedPointer<GuildPlayer>> PlayerRoster;
|
||||
}
|
||||
|
||||
[StructLayout(LayoutKind.Sequential, Pack = 1)]
|
||||
public readonly struct TownAlliance
|
||||
{
|
||||
public readonly uint Rank;
|
||||
public readonly uint Allegiance;
|
||||
public readonly uint Faction;
|
||||
public readonly Array32Char Name;
|
||||
public readonly Array5Char Tag;
|
||||
public readonly CapeDesign Cape;
|
||||
public readonly uint MapId;
|
||||
}
|
||||
|
||||
[StructLayout(LayoutKind.Sequential, Pack = 1)]
|
||||
public readonly struct CapeDesign
|
||||
{
|
||||
public readonly uint CapeBgColor;
|
||||
public readonly uint CapeDetailColor;
|
||||
public readonly uint CapeEmblemColor;
|
||||
public readonly uint CapeShape;
|
||||
public readonly uint CapeDetail;
|
||||
public readonly uint CapeEmblem;
|
||||
public readonly uint CapeTrim;
|
||||
}
|
||||
|
||||
[StructLayout(LayoutKind.Explicit, Pack = 1, Size = 0x174)]
|
||||
public readonly unsafe struct GuildPlayer
|
||||
{
|
||||
[FieldOffset(0x0004)]
|
||||
public readonly char* NamePtr;
|
||||
[FieldOffset(0x0008)]
|
||||
public readonly Array20Char InvitedName;
|
||||
[FieldOffset(0x0030)]
|
||||
public readonly Array20Char CurrentName;
|
||||
[FieldOffset(0x0050)]
|
||||
public readonly Array20Char InviterName;
|
||||
[FieldOffset(0x0080)]
|
||||
public readonly uint InviteTime;
|
||||
[FieldOffset(0x0084)]
|
||||
public readonly Array20Char PromoterName;
|
||||
[FieldOffset(0x00DC)]
|
||||
public readonly uint Offline;
|
||||
[FieldOffset(0x00E0)]
|
||||
public readonly uint MemberType;
|
||||
[FieldOffset(0x00E4)]
|
||||
public readonly uint Status;
|
||||
}
|
||||
|
||||
[StructLayout(LayoutKind.Explicit, Pack = 1, Size = 0xAC)]
|
||||
public readonly struct Guild
|
||||
{
|
||||
[FieldOffset(0x0024)]
|
||||
public readonly uint Index;
|
||||
[FieldOffset(0x0028)]
|
||||
public readonly uint Rank;
|
||||
[FieldOffset(0x002C)]
|
||||
public readonly uint Features;
|
||||
[FieldOffset(0x0030)]
|
||||
public readonly Array32Char Name;
|
||||
[FieldOffset(0x0070)]
|
||||
public readonly uint Rating;
|
||||
[FieldOffset(0x0074)]
|
||||
public readonly uint Faction;
|
||||
[FieldOffset(0x0078)]
|
||||
public readonly uint FactionPoints;
|
||||
[FieldOffset(0x007C)]
|
||||
public readonly uint QualifierPoints;
|
||||
[FieldOffset(0x0080)]
|
||||
public readonly Array8Char Tag;
|
||||
[FieldOffset(0x0090)]
|
||||
public readonly CapeDesign Cape;
|
||||
}
|
||||
@@ -13,17 +13,44 @@ public readonly unsafe struct GuildWarsArray<T> : IEnumerable<T>
|
||||
public readonly uint Size;
|
||||
public readonly uint Param;
|
||||
|
||||
/// <summary>
|
||||
/// Upper bound on a believable element count. Guild Wars' largest arrays hold at most a
|
||||
/// few thousand entries, so anything beyond this is uninitialised or misresolved memory
|
||||
/// rather than a real array.
|
||||
/// </summary>
|
||||
private const uint MaxPlausibleCapacity = 0x10000;
|
||||
|
||||
/// <summary>
|
||||
/// Mirrors <c>GW::BaseArray::valid()</c> - the buffer must be null or aligned and the size
|
||||
/// must fit within the capacity - plus a bound on the capacity itself. Guild Wars leaves
|
||||
/// these fields transiently inconsistent while it (re)allocates an array, and a
|
||||
/// misresolved GWCA scan can point this struct at arbitrary bytes that still satisfy
|
||||
/// <c>size <= capacity</c>.
|
||||
/// </summary>
|
||||
public bool IsValid =>
|
||||
(this.Buffer is null || ((nuint)this.Buffer & 0x3) == 0) &&
|
||||
this.Size <= this.Capacity &&
|
||||
this.Capacity <= MaxPlausibleCapacity;
|
||||
|
||||
/// <summary>
|
||||
/// Number of elements that can safely be read. Zero whenever the array is not backed by
|
||||
/// a buffer, matching <c>GW::BaseArray::get()</c>, which returns null instead of
|
||||
/// dereferencing. Reading past this is an access violation, and because NativeAOT cannot
|
||||
/// throw <c>AccessViolationException</c> it fail-fasts and takes Guild Wars down with it.
|
||||
/// </summary>
|
||||
public uint Count => this.Buffer is not null && this.IsValid ? this.Size : 0;
|
||||
|
||||
public T this[int index]
|
||||
{
|
||||
get
|
||||
{
|
||||
ArgumentOutOfRangeException.ThrowIfNegative(index);
|
||||
ArgumentOutOfRangeException.ThrowIfGreaterThanOrEqual((uint)index, this.Size);
|
||||
ArgumentOutOfRangeException.ThrowIfGreaterThanOrEqual((uint)index, this.Count);
|
||||
return this.Buffer[index];
|
||||
}
|
||||
}
|
||||
|
||||
public Enumerator GetEnumerator() => new(this.Buffer, this.Size);
|
||||
public Enumerator GetEnumerator() => new(this.Buffer, this.Count);
|
||||
|
||||
IEnumerator<T> IEnumerable<T>.GetEnumerator() => this.GetEnumerator();
|
||||
|
||||
|
||||
@@ -1,44 +0,0 @@
|
||||
using System.Extensions;
|
||||
using System.Numerics;
|
||||
using System.Runtime.InteropServices;
|
||||
|
||||
namespace Daybreak.API.Interop.GuildWars;
|
||||
|
||||
[StructLayout(LayoutKind.Explicit, Pack = 1, Size = 0x24)]
|
||||
public readonly struct HeroFlag
|
||||
{
|
||||
[FieldOffset(0x0000)]
|
||||
public readonly uint HeroId;
|
||||
[FieldOffset(0x0004)]
|
||||
public readonly uint AgentId;
|
||||
[FieldOffset(0x0008)]
|
||||
public readonly uint Level;
|
||||
[FieldOffset(0x000C)]
|
||||
public readonly Behavior Behavior;
|
||||
[FieldOffset(0x0010)]
|
||||
public readonly Vector2 Flag;
|
||||
[FieldOffset(0x0020)]
|
||||
public readonly uint LockedTargetId;
|
||||
}
|
||||
|
||||
[StructLayout(LayoutKind.Explicit, Pack = 1, Size = 0x78)]
|
||||
[GWCAEquivalent("HeroInfo")]
|
||||
public readonly struct HeroInfo
|
||||
{
|
||||
[FieldOffset(0x0000)]
|
||||
public readonly uint HeroId;
|
||||
[FieldOffset(0x0004)]
|
||||
public readonly uint AgentId;
|
||||
[FieldOffset(0x0008)]
|
||||
public readonly uint Level;
|
||||
[FieldOffset(0x000C)]
|
||||
public readonly Profession Primary;
|
||||
[FieldOffset(0x0010)]
|
||||
public readonly Profession Secondary;
|
||||
[FieldOffset(0x0014)]
|
||||
public readonly uint HeroFileId;
|
||||
[FieldOffset(0x0018)]
|
||||
public readonly uint ModelFileId;
|
||||
[FieldOffset(0x0050)]
|
||||
public readonly Array20Char Name;
|
||||
}
|
||||
@@ -1,31 +0,0 @@
|
||||
using System.Runtime.InteropServices;
|
||||
|
||||
namespace Daybreak.API.Interop.GuildWars;
|
||||
|
||||
[GWCAEquivalent("InstanceType")]
|
||||
public enum InstanceType
|
||||
{
|
||||
Outpost,
|
||||
Explorable,
|
||||
Loading
|
||||
};
|
||||
|
||||
[StructLayout(LayoutKind.Explicit, Pack = 1)]
|
||||
public readonly unsafe struct InstanceInfoContext
|
||||
{
|
||||
[FieldOffset(0x0004)]
|
||||
public readonly InstanceType InstanceType;
|
||||
[FieldOffset(0x0008)]
|
||||
public readonly AreaInfo* CurrentMapInfo;
|
||||
}
|
||||
|
||||
[StructLayout(LayoutKind.Sequential, Pack = 1)]
|
||||
[GWCAEquivalent("AreaInfo")]
|
||||
public readonly struct AreaInfo
|
||||
{
|
||||
public readonly uint Campaign;
|
||||
public readonly uint Continent;
|
||||
public readonly uint Region;
|
||||
public readonly uint RegionType;
|
||||
public readonly uint Flags;
|
||||
}
|
||||
@@ -1,59 +0,0 @@
|
||||
using System.Runtime.InteropServices;
|
||||
|
||||
namespace Daybreak.API.Interop.GuildWars;
|
||||
|
||||
[StructLayout(LayoutKind.Sequential, Pack = 1)]
|
||||
[GWCAEquivalent("Inventory")]
|
||||
public readonly unsafe struct Inventory
|
||||
{
|
||||
public readonly Bag* UnusedBag;
|
||||
public readonly Bag* Backpack;
|
||||
public readonly Bag* BeltPouch;
|
||||
public readonly Bag* Bag1;
|
||||
public readonly Bag* Bag2;
|
||||
public readonly Bag* EquipmentPack;
|
||||
public readonly Bag* MaterialStorage;
|
||||
public readonly Bag* UnclaimedItems;
|
||||
public readonly Bag* Storage1;
|
||||
public readonly Bag* Storage2;
|
||||
public readonly Bag* Storage3;
|
||||
public readonly Bag* Storage4;
|
||||
public readonly Bag* Storage5;
|
||||
public readonly Bag* Storage6;
|
||||
public readonly Bag* Storage7;
|
||||
public readonly Bag* Storage8;
|
||||
public readonly Bag* Storage9;
|
||||
public readonly Bag* Storage10;
|
||||
public readonly Bag* Storage11;
|
||||
public readonly Bag* Storage12;
|
||||
public readonly Bag* Storage13;
|
||||
public readonly Bag* Storage14;
|
||||
public readonly Bag* EquippedItems;
|
||||
public readonly Item* Bundle;
|
||||
public readonly uint StoragePanesUnlocked;
|
||||
|
||||
public readonly Bag*[] Bags => [
|
||||
this.Backpack,
|
||||
this.BeltPouch,
|
||||
this.Bag1,
|
||||
this.Bag2,
|
||||
this.EquipmentPack,
|
||||
this.MaterialStorage,
|
||||
this.UnclaimedItems,
|
||||
this.Storage1,
|
||||
this.Storage2,
|
||||
this.Storage3,
|
||||
this.Storage4,
|
||||
this.Storage5,
|
||||
this.Storage6,
|
||||
this.Storage7,
|
||||
this.Storage8,
|
||||
this.Storage9,
|
||||
this.Storage10,
|
||||
this.Storage11,
|
||||
this.Storage12,
|
||||
this.Storage13,
|
||||
this.Storage14,
|
||||
this.EquippedItems
|
||||
];
|
||||
}
|
||||
@@ -1,129 +0,0 @@
|
||||
using System.Runtime.InteropServices;
|
||||
|
||||
namespace Daybreak.API.Interop.GuildWars;
|
||||
|
||||
public enum ItemType : byte
|
||||
{
|
||||
Salvage,
|
||||
Axe = 2,
|
||||
Bag,
|
||||
Boots,
|
||||
Bow,
|
||||
Bundle,
|
||||
Chestpiece,
|
||||
Rune_Mod,
|
||||
Usable,
|
||||
Dye,
|
||||
Materials_Zcoins,
|
||||
Offhand,
|
||||
Gloves,
|
||||
Hammer = 15,
|
||||
Headpiece,
|
||||
CC_Shards,
|
||||
Key,
|
||||
Leggings,
|
||||
Gold_Coin,
|
||||
Quest_Item,
|
||||
Wand,
|
||||
Shield = 24,
|
||||
Staff = 26,
|
||||
Sword,
|
||||
Kit = 29,
|
||||
Trophy,
|
||||
Scroll,
|
||||
Daggers,
|
||||
Present,
|
||||
Minipet,
|
||||
Scythe,
|
||||
Spear,
|
||||
Storybook = 43,
|
||||
Costume,
|
||||
Costume_Headpiece,
|
||||
Unknown = 0xff
|
||||
};
|
||||
|
||||
[StructLayout(LayoutKind.Sequential, Pack = 1)]
|
||||
[GWCAEquivalent("ItemModifier")]
|
||||
public readonly struct ItemModifier(uint mod)
|
||||
{
|
||||
public static readonly ItemModifier Zero = new();
|
||||
|
||||
public readonly uint Mod = mod;
|
||||
|
||||
public uint Identifier => this.Mod >> 16;
|
||||
public uint Arg1 => (this.Mod & 0x0000FF00) >> 8;
|
||||
public uint Arg2 => this.Mod & 0x000000FF;
|
||||
public uint Arg => this.Mod & 0x0000FFFF;
|
||||
}
|
||||
|
||||
[StructLayout(LayoutKind.Explicit, Pack = 1)]
|
||||
[GWCAEquivalent("Item")]
|
||||
public readonly unsafe struct Item
|
||||
{
|
||||
[FieldOffset(0x0000)]
|
||||
public readonly uint ItemId;
|
||||
|
||||
[FieldOffset(0x0004)]
|
||||
public readonly uint AgentId;
|
||||
|
||||
[FieldOffset(0x0008)]
|
||||
public readonly Bag* BagEquipped;
|
||||
|
||||
[FieldOffset(0x000C)]
|
||||
public readonly Bag* Bag;
|
||||
|
||||
[FieldOffset(0x0010)]
|
||||
public readonly ItemModifier* Modifiers;
|
||||
|
||||
[FieldOffset(0x0014)]
|
||||
public readonly uint ModifiersCount;
|
||||
|
||||
[FieldOffset(0x001C)]
|
||||
public readonly uint ModelFileId;
|
||||
|
||||
[FieldOffset(0x0020)]
|
||||
public readonly ItemType Type;
|
||||
|
||||
[FieldOffset(0x0024)]
|
||||
public readonly ushort Value;
|
||||
|
||||
[FieldOffset(0x0028)]
|
||||
public readonly uint Interaction;
|
||||
|
||||
[FieldOffset(0x002C)]
|
||||
public readonly uint ModelId;
|
||||
|
||||
[FieldOffset(0x0030)]
|
||||
public readonly char* InfoString;
|
||||
|
||||
[FieldOffset(0x0034)]
|
||||
public readonly char* NameEncoded;
|
||||
|
||||
[FieldOffset(0x0038)]
|
||||
public readonly char* CompleteNameEncoded;
|
||||
|
||||
[FieldOffset(0x003C)]
|
||||
public readonly char* SingleItemName;
|
||||
|
||||
[FieldOffset(0x0048)]
|
||||
public readonly ushort ItemFormula;
|
||||
|
||||
[FieldOffset(0x004A)]
|
||||
public readonly byte IsMaterialSalvageable;
|
||||
|
||||
[FieldOffset(0x004C)]
|
||||
public readonly ushort Quantity;
|
||||
|
||||
[FieldOffset(0x004E)]
|
||||
public readonly byte Equipped;
|
||||
|
||||
[FieldOffset(0x004F)]
|
||||
public readonly byte Profession;
|
||||
|
||||
[FieldOffset(0x0050)]
|
||||
public readonly uint Slot;
|
||||
|
||||
public readonly bool Stackable => (this.Interaction & 0x80000) != 0;
|
||||
|
||||
public readonly bool Inscribable => (this.Interaction & 0x08000000) != 0;
|
||||
}
|
||||
@@ -1,17 +0,0 @@
|
||||
using System.Runtime.InteropServices;
|
||||
|
||||
namespace Daybreak.API.Interop.GuildWars;
|
||||
|
||||
[StructLayout(LayoutKind.Explicit, Pack = 1)]
|
||||
[GWCAEquivalent("ItemContext")]
|
||||
public readonly struct ItemContext
|
||||
{
|
||||
[FieldOffset(0x0024)]
|
||||
public readonly GuildWarsArray<WrappedPointer<Bag>> Bags;
|
||||
|
||||
[FieldOffset(0x00B8)]
|
||||
public readonly GuildWarsArray<WrappedPointer<Item>> Items;
|
||||
|
||||
[FieldOffset(0x00F8)]
|
||||
public readonly WrappedPointer<Inventory> Inventory;
|
||||
}
|
||||
@@ -1,18 +0,0 @@
|
||||
namespace Daybreak.API.Interop.GuildWars;
|
||||
|
||||
[GWCAEquivalent("Language")]
|
||||
public enum Language
|
||||
{
|
||||
English,
|
||||
Korean,
|
||||
French,
|
||||
German,
|
||||
Italian,
|
||||
Spanish,
|
||||
TraditionalChinese,
|
||||
Japanese = 8,
|
||||
Polish,
|
||||
Russian,
|
||||
BorkBorkBork = 17,
|
||||
Unknown = 0xff
|
||||
}
|
||||
@@ -1,9 +0,0 @@
|
||||
using System.Runtime.InteropServices;
|
||||
|
||||
namespace Daybreak.API.Interop.GuildWars;
|
||||
|
||||
[StructLayout(LayoutKind.Explicit, Pack = 1)]
|
||||
[GWCAEquivalent("MapContext")]
|
||||
public readonly struct MapContext
|
||||
{
|
||||
}
|
||||
@@ -1,63 +0,0 @@
|
||||
using System.Runtime.InteropServices;
|
||||
|
||||
namespace Daybreak.API.Interop.GuildWars;
|
||||
|
||||
[StructLayout(LayoutKind.Explicit)]
|
||||
[GWCAEquivalent("NPC")]
|
||||
public readonly unsafe struct NpcContext
|
||||
{
|
||||
[FieldOffset(0x0000)]
|
||||
public readonly uint ModelId;
|
||||
|
||||
[FieldOffset(0x000C)]
|
||||
public readonly uint Sex;
|
||||
|
||||
[FieldOffset(0x0010)]
|
||||
public readonly NpcFlags Flags;
|
||||
|
||||
[FieldOffset(0x0014)]
|
||||
public readonly uint Primary;
|
||||
|
||||
[FieldOffset(0x001C)]
|
||||
public readonly byte DefaultLevel;
|
||||
|
||||
[FieldOffset(0x0020)]
|
||||
public readonly char* EncName;
|
||||
|
||||
[FieldOffset(0x0028)]
|
||||
public readonly int FilesCount;
|
||||
|
||||
[FieldOffset(0x002C)]
|
||||
public readonly int FilesCapacity;
|
||||
|
||||
[Flags]
|
||||
public enum NpcFlags : uint
|
||||
{
|
||||
None = 0x0000,
|
||||
Henchman = 0x0010,
|
||||
Hero = 0x0020,
|
||||
Spirit = 0x4000,
|
||||
Minion = 0x0100,
|
||||
Pet = 0x000D
|
||||
}
|
||||
}
|
||||
|
||||
[StructLayout(LayoutKind.Sequential, Pack = 1)]
|
||||
[GWCAEquivalent("PetInfo")]
|
||||
public readonly unsafe struct PetContext
|
||||
{
|
||||
public readonly uint AgentId;
|
||||
public readonly uint OwnerAgentId;
|
||||
public readonly char* PetName;
|
||||
public readonly uint ModelFileId1;
|
||||
public readonly uint ModelFileId2;
|
||||
public readonly Behavior Behavior;
|
||||
public readonly uint LockedTargetId;
|
||||
}
|
||||
|
||||
[StructLayout(LayoutKind.Sequential, Pack = 1)]
|
||||
public readonly struct ControlledMinion
|
||||
{
|
||||
public readonly uint AgentId;
|
||||
public readonly uint MinionCount;
|
||||
}
|
||||
@@ -1,127 +0,0 @@
|
||||
using System.Extensions;
|
||||
using System.Runtime.InteropServices;
|
||||
|
||||
namespace Daybreak.API.Interop.GuildWars;
|
||||
|
||||
[Flags]
|
||||
public enum PlayerPartyMemberState : uint
|
||||
{
|
||||
None = 0,
|
||||
Connected = 1,
|
||||
Ticked = 2
|
||||
}
|
||||
|
||||
public enum PartySearchType
|
||||
{
|
||||
PartySearchType_Hunting = 0,
|
||||
PartySearchType_Mission = 1,
|
||||
PartySearchType_Quest = 2,
|
||||
PartySearchType_Trade = 3,
|
||||
PartySearchType_Guild = 4,
|
||||
};
|
||||
|
||||
[Flags]
|
||||
public enum PartyFlags : uint
|
||||
{
|
||||
None = 0,
|
||||
HardMode = 0x10, // Bit 4
|
||||
Defeated = 0x20, // Bit 5
|
||||
PartyLeader = 0x80 // Bit 7 (0x7th position)
|
||||
}
|
||||
|
||||
[StructLayout(LayoutKind.Explicit, Pack = 1, Size = 0xC)]
|
||||
public readonly unsafe struct PartyMoraleContext
|
||||
{
|
||||
[FieldOffset(0x000C)]
|
||||
public readonly PartyMemberMoraleInfo* MoraleInfo;
|
||||
}
|
||||
|
||||
[StructLayout(LayoutKind.Explicit, Pack = 1)]
|
||||
public readonly struct PartyMemberMoraleInfo
|
||||
{
|
||||
[FieldOffset(0x0000)]
|
||||
public readonly uint AgentId;
|
||||
[FieldOffset(0x0014)]
|
||||
public readonly uint Morale;
|
||||
}
|
||||
|
||||
[StructLayout(LayoutKind.Explicit, Pack = 1, Size = 0x58)]
|
||||
[GWCAEquivalent("PartyContext")]
|
||||
public readonly unsafe struct PartyContext
|
||||
{
|
||||
[FieldOffset(0x0014)]
|
||||
public readonly PartyFlags Flags;
|
||||
[FieldOffset(0x0040)]
|
||||
public readonly GuildWarsArray<WrappedPointer<PartyInfo>> Parties;
|
||||
[FieldOffset(0x0054)]
|
||||
public readonly PartyInfo* PlayerParty;
|
||||
[FieldOffset(0x00C0)]
|
||||
public readonly GuildWarsArray<WrappedPointer<PartySearch>> PartySearches;
|
||||
}
|
||||
|
||||
[StructLayout(LayoutKind.Sequential, Pack = 1, Size = 0xC)]
|
||||
public readonly struct PlayerPartyMember
|
||||
{
|
||||
public readonly uint LogingNumber;
|
||||
public readonly uint CalledTargetId;
|
||||
public readonly PlayerPartyMemberState State;
|
||||
}
|
||||
|
||||
[StructLayout(LayoutKind.Explicit, Pack = 1, Size = 0x18)]
|
||||
public readonly struct HeroPartyMember
|
||||
{
|
||||
[FieldOffset(0x0000)]
|
||||
public readonly uint AgentId;
|
||||
[FieldOffset(0x0004)]
|
||||
public readonly uint OwnerPlayerId;
|
||||
[FieldOffset(0x0008)]
|
||||
public readonly uint HeroId;
|
||||
[FieldOffset(0x0014)]
|
||||
public readonly uint Level;
|
||||
}
|
||||
|
||||
[StructLayout(LayoutKind.Explicit, Pack = 1, Size = 0x34)]
|
||||
public readonly struct HenchmanPartyMember
|
||||
{
|
||||
[FieldOffset(0x0000)]
|
||||
public readonly uint AgentId;
|
||||
[FieldOffset(0x002C)]
|
||||
public readonly Profession Profession;
|
||||
[FieldOffset(0x0030)]
|
||||
public readonly uint Level;
|
||||
}
|
||||
|
||||
[StructLayout(LayoutKind.Explicit, Pack = 1, Size = 0x84)]
|
||||
[GWCAEquivalent("PartyInfo")]
|
||||
public readonly struct PartyInfo
|
||||
{
|
||||
[FieldOffset(0x0000)]
|
||||
public readonly uint PartyId;
|
||||
[FieldOffset(0x0004)]
|
||||
public readonly GuildWarsArray<PlayerPartyMember> Players;
|
||||
[FieldOffset(0x0014)]
|
||||
public readonly GuildWarsArray<HenchmanPartyMember> Henchmen;
|
||||
[FieldOffset(0x0024)]
|
||||
public readonly GuildWarsArray<HeroPartyMember> Heroes;
|
||||
[FieldOffset(0x0034)]
|
||||
public readonly GuildWarsArray<uint> Others;
|
||||
}
|
||||
|
||||
[StructLayout(LayoutKind.Sequential, Pack = 1)]
|
||||
[GWCAEquivalent("PartySearch")]
|
||||
public readonly struct PartySearch
|
||||
{
|
||||
public readonly uint PartySearchId;
|
||||
public readonly PartySearchType PartySearchType;
|
||||
public readonly uint HardMode;
|
||||
public readonly uint District;
|
||||
public readonly uint Language;
|
||||
public readonly uint PartySize;
|
||||
public readonly uint HeroCount;
|
||||
public readonly Array32Char Message;
|
||||
public readonly Array20Char PartyLeader;
|
||||
public readonly Profession Primary;
|
||||
public readonly Profession Secondary;
|
||||
public readonly uint Level;
|
||||
public readonly uint Timestamp;
|
||||
}
|
||||
@@ -1,20 +0,0 @@
|
||||
using System.Runtime.InteropServices;
|
||||
|
||||
namespace Daybreak.API.Interop.GuildWars;
|
||||
|
||||
[StructLayout(LayoutKind.Explicit, Pack = 1, Size = 0x34)]
|
||||
public unsafe struct PartySearchWindowContext
|
||||
{
|
||||
[FieldOffset(0x0000)]
|
||||
public uint* Vtable;
|
||||
[FieldOffset(0x0004)]
|
||||
public uint FrameId;
|
||||
[FieldOffset(0x0020)]
|
||||
public uint SelectedPartySearchId;
|
||||
[FieldOffset(0x0024)]
|
||||
public uint SelectedHeroId;
|
||||
[FieldOffset(0x0028)]
|
||||
public uint SelectedHenchmanId;
|
||||
[FieldOffset(0x002C)]
|
||||
public uint CurrentTab;
|
||||
}
|
||||
@@ -1,48 +0,0 @@
|
||||
using System.Runtime.InteropServices;
|
||||
|
||||
namespace Daybreak.API.Interop.GuildWars;
|
||||
|
||||
[Flags]
|
||||
public enum PlayerContextFlags : uint
|
||||
{
|
||||
None = 0,
|
||||
PvP = 0x800
|
||||
}
|
||||
|
||||
[StructLayout(LayoutKind.Explicit, Pack = 1, Size = 0x50)]
|
||||
[GWCAEquivalent("PlayerContext")]
|
||||
public readonly unsafe struct PlayerContext
|
||||
{
|
||||
[FieldOffset(0x0000)]
|
||||
public readonly int AgentId;
|
||||
|
||||
[FieldOffset(0x0014)]
|
||||
public readonly PlayerContextFlags Flags;
|
||||
|
||||
[FieldOffset(0x0018)]
|
||||
public readonly uint PrimaryProfession;
|
||||
|
||||
[FieldOffset(0x001C)]
|
||||
public readonly uint SecondaryProfession;
|
||||
|
||||
[FieldOffset(0x0024)]
|
||||
public readonly char* NameEncoded;
|
||||
|
||||
[FieldOffset(0x0028)]
|
||||
public readonly char* Name;
|
||||
|
||||
[FieldOffset(0x002C)]
|
||||
public readonly uint PartyLeaderPlayerNumber;
|
||||
|
||||
[FieldOffset(0x0030)]
|
||||
public readonly uint ActiveTitleTier;
|
||||
|
||||
[FieldOffset(0x0034)]
|
||||
public readonly uint ReforgedOrDhuumsFlags;
|
||||
|
||||
[FieldOffset(0x0038)]
|
||||
public readonly uint PlayerNumber;
|
||||
|
||||
[FieldOffset(0x003C)]
|
||||
public readonly uint PartySize;
|
||||
}
|
||||
@@ -1,10 +0,0 @@
|
||||
using System.Runtime.InteropServices;
|
||||
|
||||
namespace Daybreak.API.Interop.GuildWars;
|
||||
|
||||
[StructLayout(LayoutKind.Explicit, Pack = 1)]
|
||||
public readonly struct PlayerControlledCharContext
|
||||
{
|
||||
[FieldOffset(0x0014)]
|
||||
public readonly uint AgentId;
|
||||
}
|
||||
@@ -1,25 +0,0 @@
|
||||
using System.Extensions;
|
||||
using System.Runtime.InteropServices;
|
||||
|
||||
namespace Daybreak.API.Interop.GuildWars;
|
||||
|
||||
[StructLayout(LayoutKind.Explicit, Pack = 1)]
|
||||
[GWCAEquivalent("PreGameContext")]
|
||||
public readonly struct PreGameContext
|
||||
{
|
||||
[FieldOffset(0x0000)]
|
||||
public readonly uint FrameId;
|
||||
|
||||
[FieldOffset(0x0124)]
|
||||
public readonly uint ChosenCharacterIndex;
|
||||
|
||||
[FieldOffset(0x0148)]
|
||||
public readonly GuildWarsArray<LoginCharacterContext> LoginCharacters;
|
||||
}
|
||||
|
||||
[StructLayout(LayoutKind.Explicit, Pack = 1)]
|
||||
public readonly struct LoginCharacterContext
|
||||
{
|
||||
[FieldOffset(0x0004)]
|
||||
public readonly Array20Char CharacterName;
|
||||
}
|
||||
@@ -1,45 +0,0 @@
|
||||
namespace Daybreak.API.Interop.GuildWars;
|
||||
|
||||
[GWCAEquivalent("EnumPreference")]
|
||||
public enum EnumPreference : uint
|
||||
{
|
||||
CharSortOrder = 0,
|
||||
AntiAliasing = 1,
|
||||
Reflections = 2,
|
||||
ShaderQuality = 3,
|
||||
ShadowQuality = 4,
|
||||
TerrainQuality = 5,
|
||||
InterfaceSize = 6,
|
||||
FrameLimiter = 7,
|
||||
Count = 8
|
||||
}
|
||||
|
||||
[GWCAEquivalent("CharSortOrder")]
|
||||
public enum CharSortOrder : uint
|
||||
{
|
||||
None = 0,
|
||||
Alphabetize = 1,
|
||||
PvPRP = 2
|
||||
}
|
||||
|
||||
[GWCAEquivalent("NumberPreference")]
|
||||
public enum NumberPreference : uint
|
||||
{
|
||||
AutoTournPartySort = 0,
|
||||
ChatState = 1,
|
||||
Count = 0x2b
|
||||
}
|
||||
|
||||
[GWCAEquivalent("FlagPreference")]
|
||||
public enum FlagPreference : uint
|
||||
{
|
||||
}
|
||||
|
||||
[GWCAEquivalent("StringPreference")]
|
||||
public enum StringPreference : uint
|
||||
{
|
||||
Unk1 = 0,
|
||||
Unk2 = 1,
|
||||
LastCharacterName = 2,
|
||||
Count = 3
|
||||
}
|
||||
@@ -1,48 +0,0 @@
|
||||
using System.Runtime.InteropServices;
|
||||
|
||||
namespace Daybreak.API.Interop.GuildWars;
|
||||
|
||||
[GWCAEquivalent("Profession")]
|
||||
public enum Profession : uint
|
||||
{
|
||||
None,
|
||||
Warrior,
|
||||
Ranger,
|
||||
Monk,
|
||||
Necromancer,
|
||||
Mesmer,
|
||||
Elementalist,
|
||||
Assassin,
|
||||
Ritualist,
|
||||
Paragon,
|
||||
Dervish
|
||||
}
|
||||
|
||||
public enum ProfessionByte : byte
|
||||
{
|
||||
None,
|
||||
Warrior,
|
||||
Ranger,
|
||||
Monk,
|
||||
Necromancer,
|
||||
Mesmer,
|
||||
Elementalist,
|
||||
Assassin,
|
||||
Ritualist,
|
||||
Paragon,
|
||||
Dervish
|
||||
}
|
||||
|
||||
[StructLayout(LayoutKind.Sequential, Pack = 1, Size = 0x0014)]
|
||||
public readonly struct ProfessionsContext
|
||||
{
|
||||
public readonly uint AgentId;
|
||||
public readonly Profession CurrentPrimary;
|
||||
public readonly Profession CurrentSecondary;
|
||||
public readonly uint UnlockedProfessionsFlags;
|
||||
|
||||
public bool ProfessionUnlocked(int professionId)
|
||||
{
|
||||
return (this.UnlockedProfessionsFlags & (1U << professionId)) != 0;
|
||||
}
|
||||
}
|
||||
@@ -1,26 +0,0 @@
|
||||
using System.Numerics;
|
||||
using System.Runtime.InteropServices;
|
||||
|
||||
namespace Daybreak.API.Interop.GuildWars;
|
||||
|
||||
[StructLayout(LayoutKind.Explicit, Pack = 1, Size = 0x0034)]
|
||||
[GWCAEquivalent("Quest")]
|
||||
public readonly struct QuestContext
|
||||
{
|
||||
[FieldOffset(0x0000)]
|
||||
public readonly uint QuestId;
|
||||
|
||||
[FieldOffset(0x0014)]
|
||||
public readonly uint MapFrom;
|
||||
|
||||
[FieldOffset(0x0018)]
|
||||
public readonly Vector3 Marker;
|
||||
}
|
||||
|
||||
[StructLayout(LayoutKind.Sequential, Pack = 1, Size = 0xC)]
|
||||
public readonly unsafe struct MissionObjectiveContext
|
||||
{
|
||||
public readonly uint ObjectiveId;
|
||||
public readonly char* EncodedString;
|
||||
public readonly uint Type;
|
||||
}
|
||||
@@ -1,13 +0,0 @@
|
||||
namespace Daybreak.API.Interop.GuildWars;
|
||||
|
||||
[GWCAEquivalent("ServerRegion")]
|
||||
public enum ServerRegion
|
||||
{
|
||||
International = -2,
|
||||
America = 0,
|
||||
Korea,
|
||||
Europe,
|
||||
China,
|
||||
Japan,
|
||||
Unknown = 0xff
|
||||
}
|
||||
@@ -1,62 +0,0 @@
|
||||
using System.Runtime.InteropServices;
|
||||
|
||||
namespace Daybreak.API.Interop.GuildWars;
|
||||
|
||||
[StructLayout(LayoutKind.Sequential, Pack = 1)]
|
||||
[GWCAEquivalent("SkillbarSkill")]
|
||||
public readonly struct SkillContext
|
||||
{
|
||||
public readonly uint Adrenaline1;
|
||||
public readonly uint Adrenaline2;
|
||||
public readonly uint Recharge;
|
||||
public readonly uint Id;
|
||||
public readonly uint Event;
|
||||
}
|
||||
|
||||
[StructLayout(LayoutKind.Sequential, Pack = 1, Size = 0xBC)]
|
||||
[GWCAEquivalent("Skillbar")]
|
||||
public readonly struct SkillbarContext
|
||||
{
|
||||
public readonly uint AgentId;
|
||||
public readonly SkillContext Skill0;
|
||||
public readonly SkillContext Skill1;
|
||||
public readonly SkillContext Skill2;
|
||||
public readonly SkillContext Skill3;
|
||||
public readonly SkillContext Skill4;
|
||||
public readonly SkillContext Skill5;
|
||||
public readonly SkillContext Skill6;
|
||||
public readonly SkillContext Skill7;
|
||||
public readonly uint Disabled;
|
||||
public readonly uint Casting;
|
||||
}
|
||||
|
||||
[StructLayout(LayoutKind.Explicit, Pack = 1, Size = 0x10)]
|
||||
[GWCAEquivalent("Buff")]
|
||||
public readonly struct Buff
|
||||
{
|
||||
[FieldOffset(0x0000)]
|
||||
public readonly uint SkillId;
|
||||
[FieldOffset(0x0008)]
|
||||
public readonly uint BuffId;
|
||||
[FieldOffset(0x000C)]
|
||||
public readonly uint TargetAgentId;
|
||||
}
|
||||
|
||||
[StructLayout(LayoutKind.Sequential, Pack = 1, Size = 0x18)]
|
||||
[GWCAEquivalent("Effect")]
|
||||
public readonly struct Effect
|
||||
{
|
||||
public readonly uint SkillId;
|
||||
public readonly uint AttributeLevel;
|
||||
public readonly uint EffectId;
|
||||
public readonly uint AgentId;
|
||||
public readonly float Duration;
|
||||
public readonly uint Timestamp;
|
||||
}
|
||||
|
||||
[StructLayout(LayoutKind.Sequential, Pack = 1)]
|
||||
public readonly struct DupeSkill
|
||||
{
|
||||
public readonly uint SkillId;
|
||||
public readonly uint Count;
|
||||
}
|
||||
@@ -1,14 +0,0 @@
|
||||
using System.Extensions;
|
||||
|
||||
namespace Daybreak.API.Interop.GuildWars;
|
||||
|
||||
[GWCAEquivalent("SkillTemplate")]
|
||||
public readonly struct SkillTemplate(uint primary, uint secondary, uint attributeCount, Array12Uint attributeIds, Array12Uint attributeValues, Array8Uint skills)
|
||||
{
|
||||
public readonly uint Primary = primary;
|
||||
public readonly uint Secondary = secondary;
|
||||
public readonly uint AttributesCount = attributeCount;
|
||||
public readonly Array12Uint AttributeIds = attributeIds;
|
||||
public readonly Array12Uint AttributeValues = attributeValues;
|
||||
public readonly Array8Uint Skills = skills;
|
||||
}
|
||||
@@ -1,13 +0,0 @@
|
||||
using System.Runtime.InteropServices;
|
||||
|
||||
namespace Daybreak.API.Interop.GuildWars;
|
||||
|
||||
[StructLayout(LayoutKind.Sequential, Pack = 1)]
|
||||
[GWCAEquivalent("TagInfo")]
|
||||
public readonly struct TagInfo
|
||||
{
|
||||
public readonly ushort GuildId;
|
||||
public readonly byte Primary;
|
||||
public readonly byte Secondary;
|
||||
public readonly ushort Level;
|
||||
}
|
||||
@@ -1,11 +0,0 @@
|
||||
using System.Runtime.InteropServices;
|
||||
|
||||
namespace Daybreak.API.Interop.GuildWars;
|
||||
|
||||
[StructLayout(LayoutKind.Explicit, Pack = 1)]
|
||||
[GWCAEquivalent("TextParser")]
|
||||
public struct TextParserContext
|
||||
{
|
||||
[FieldOffset(0x01D0)]
|
||||
public Language Language;
|
||||
}
|
||||
@@ -1,51 +0,0 @@
|
||||
using System.Runtime.InteropServices;
|
||||
|
||||
namespace Daybreak.API.Interop.GuildWars;
|
||||
|
||||
[Flags]
|
||||
public enum TitleProps : uint
|
||||
{
|
||||
None = 0,
|
||||
PercentageBased = 1,
|
||||
HasTiers = 2
|
||||
// Note: The HasTiers check uses (props & 3) == 2, which means bit 1 is set but bit 0 is not
|
||||
}
|
||||
|
||||
[StructLayout(LayoutKind.Explicit, Pack = 1, Size = 0x2C)]
|
||||
[GWCAEquivalent("Title")]
|
||||
public readonly struct TitleContext
|
||||
{
|
||||
[FieldOffset(0x0000)]
|
||||
public readonly TitleProps Props;
|
||||
[FieldOffset(0x0004)]
|
||||
public readonly uint CurrentPoints;
|
||||
[FieldOffset(0x0008)]
|
||||
public readonly uint CurrentTitleTierIndex;
|
||||
[FieldOffset(0x000C)]
|
||||
public readonly uint PointsNeededForCurrentRank;
|
||||
[FieldOffset(0x0014)]
|
||||
public readonly uint NextTitleTierIndex;
|
||||
[FieldOffset(0x0018)]
|
||||
public readonly uint PointsNeededForNextRank;
|
||||
[FieldOffset(0x001C)]
|
||||
public readonly uint MaxTitleRank;
|
||||
[FieldOffset(0x0020)]
|
||||
public readonly uint MaxTitleTierIndex;
|
||||
}
|
||||
|
||||
[StructLayout(LayoutKind.Sequential, Pack = 1, Size = 0xC)]
|
||||
[GWCAEquivalent("TitleTier")]
|
||||
public readonly unsafe struct TitleTier
|
||||
{
|
||||
public readonly TitleProps Props;
|
||||
public readonly uint TierNumber;
|
||||
public readonly char* TierNameEncoded;
|
||||
}
|
||||
|
||||
[StructLayout(LayoutKind.Sequential, Pack = 1)]
|
||||
[GWCAEquivalent("TitleClientData")]
|
||||
public readonly struct TitleClientData
|
||||
{
|
||||
public readonly uint TitleId;
|
||||
public readonly uint NameId;
|
||||
}
|
||||
@@ -1,174 +0,0 @@
|
||||
using System.Numerics;
|
||||
using System.Runtime.InteropServices;
|
||||
|
||||
namespace Daybreak.API.Interop.GuildWars;
|
||||
|
||||
[StructLayout(LayoutKind.Explicit, Pack = 1)]
|
||||
[GWCAEquivalent("WorldContext")]
|
||||
public readonly unsafe struct WorldContext
|
||||
{
|
||||
[FieldOffset(0x0000)]
|
||||
public readonly AccountInfoContext* AccountInfo;
|
||||
|
||||
[FieldOffset(0x0024)]
|
||||
public readonly GuildWarsArray<uint> MerchItems;
|
||||
|
||||
[FieldOffset(0x0034)]
|
||||
public readonly GuildWarsArray<uint> MerchItems2;
|
||||
|
||||
[FieldOffset(0x007C)]
|
||||
public readonly GuildWarsArray<MapAgentContext> MapAgents;
|
||||
|
||||
[FieldOffset(0x009C)]
|
||||
public readonly Vector3 AllFlag;
|
||||
|
||||
[FieldOffset(0x00AC)]
|
||||
public readonly GuildWarsArray<PartyAttribute> Attributes;
|
||||
|
||||
[FieldOffset(0x0508)]
|
||||
public readonly GuildWarsArray<AgentEffects> PartyEffects;
|
||||
|
||||
[FieldOffset(0x0528)]
|
||||
public readonly uint ActiveQuestId;
|
||||
|
||||
[FieldOffset(0x052C)]
|
||||
public readonly GuildWarsArray<QuestContext> QuestLog;
|
||||
|
||||
[FieldOffset(0x0564)]
|
||||
public readonly GuildWarsArray<MissionObjectiveContext> MissionObjectives;
|
||||
|
||||
[FieldOffset(0x0574)]
|
||||
public readonly GuildWarsArray<uint> HenchmenAgentIds;
|
||||
|
||||
[FieldOffset(0x0584)]
|
||||
public readonly GuildWarsArray<HeroFlag> HeroFlags;
|
||||
|
||||
[FieldOffset(0x0594)]
|
||||
public readonly GuildWarsArray<HeroInfo> HeroInfos;
|
||||
|
||||
[FieldOffset(0x05A4)]
|
||||
public readonly GuildWarsArray<nuint> CartographedAreas;
|
||||
|
||||
[FieldOffset(0x05BC)]
|
||||
public readonly GuildWarsArray<ControlledMinion> ControlledMinions;
|
||||
|
||||
[FieldOffset(0x05CC)]
|
||||
public readonly GuildWarsArray<uint> MissionsCompleted;
|
||||
|
||||
[FieldOffset(0x05DC)]
|
||||
public readonly GuildWarsArray<uint> MissionsBonus;
|
||||
|
||||
[FieldOffset(0x05EC)]
|
||||
public readonly GuildWarsArray<uint> MissionsCompletedHardMode;
|
||||
|
||||
[FieldOffset(0x05FC)]
|
||||
public readonly GuildWarsArray<uint> MissionsBonusHardMode;
|
||||
|
||||
[FieldOffset(0x060C)]
|
||||
public readonly GuildWarsArray<uint> UnlockedMap;
|
||||
|
||||
[FieldOffset(0x062C)]
|
||||
public readonly GuildWarsArray<PartyMoraleContext> PartyMorale;
|
||||
|
||||
[FieldOffset(0x067C)]
|
||||
public readonly uint PlayerNumber;
|
||||
|
||||
[FieldOffset(0x0680)]
|
||||
public readonly PlayerControlledCharContext* PlayerControlledChar;
|
||||
|
||||
[FieldOffset(0x0684)]
|
||||
public readonly uint HardModeUnlocked;
|
||||
|
||||
[FieldOffset(0x06AC)]
|
||||
public readonly GuildWarsArray<PetContext> Pets;
|
||||
|
||||
[FieldOffset(0x06BC)]
|
||||
public readonly GuildWarsArray<ProfessionsContext> Professions;
|
||||
|
||||
[FieldOffset(0x06F0)]
|
||||
public readonly GuildWarsArray<SkillbarContext> Skillbars;
|
||||
|
||||
[FieldOffset(0x0700)]
|
||||
public readonly GuildWarsArray<uint> LearnableCharacterSkills;
|
||||
|
||||
[FieldOffset(0x0710)]
|
||||
public readonly GuildWarsArray<uint> UnlockedCharacterSkills;
|
||||
|
||||
[FieldOffset(0x0720)]
|
||||
public readonly GuildWarsArray<DupeSkill> DupeSkills;
|
||||
|
||||
[FieldOffset(0x0740)]
|
||||
public readonly uint Experience;
|
||||
|
||||
[FieldOffset(0x0748)]
|
||||
public readonly uint CurrentKurzick;
|
||||
|
||||
[FieldOffset(0x0750)]
|
||||
public readonly uint TotalKurzick;
|
||||
|
||||
[FieldOffset(0x0758)]
|
||||
public readonly uint CurrentLuxon;
|
||||
|
||||
[FieldOffset(0x0760)]
|
||||
public readonly uint TotalLuxon;
|
||||
|
||||
[FieldOffset(0x0768)]
|
||||
public readonly uint CurrentImperial;
|
||||
|
||||
[FieldOffset(0x0770)]
|
||||
public readonly uint TotalImperial;
|
||||
|
||||
[FieldOffset(0x0788)]
|
||||
public readonly uint Level;
|
||||
|
||||
[FieldOffset(0x0790)]
|
||||
public readonly uint Morale;
|
||||
|
||||
[FieldOffset(0x0798)]
|
||||
public readonly uint CurrentBalthazar;
|
||||
|
||||
[FieldOffset(0x07A0)]
|
||||
public readonly uint TotalBalthazar;
|
||||
|
||||
[FieldOffset(0x07A8)]
|
||||
public readonly uint CurrentSkillPoints;
|
||||
|
||||
[FieldOffset(0x07B0)]
|
||||
public readonly uint TotalSkillPoints;
|
||||
|
||||
[FieldOffset(0x07B8)]
|
||||
public readonly uint MaxKurzick;
|
||||
|
||||
[FieldOffset(0x07BC)]
|
||||
public readonly uint MaxLuxon;
|
||||
|
||||
[FieldOffset(0x07C0)]
|
||||
public readonly uint MaxBalthazar;
|
||||
|
||||
[FieldOffset(0x07C4)]
|
||||
public readonly uint MaxImperial;
|
||||
|
||||
[FieldOffset(0x07CC)]
|
||||
public readonly GuildWarsArray<AgentInfo> AgentInfos;
|
||||
|
||||
[FieldOffset(0x07FC)]
|
||||
public readonly GuildWarsArray<NpcContext> Npcs;
|
||||
|
||||
[FieldOffset(0x080C)]
|
||||
public readonly GuildWarsArray<PlayerContext> Players;
|
||||
|
||||
[FieldOffset(0x081C)]
|
||||
public readonly GuildWarsArray<TitleContext> Titles;
|
||||
|
||||
[FieldOffset(0x082C)]
|
||||
public readonly GuildWarsArray<TitleTier> TitleTiers;
|
||||
|
||||
[FieldOffset(0x083C)]
|
||||
public readonly GuildWarsArray<uint> VanquishedAreas;
|
||||
|
||||
[FieldOffset(0x084C)]
|
||||
public readonly uint FoesKilled;
|
||||
|
||||
[FieldOffset(0x0850)]
|
||||
public readonly uint FoesToKill;
|
||||
}
|
||||
@@ -7,6 +7,18 @@ public readonly unsafe struct WrappedPointer<T>(T* pointer)
|
||||
|
||||
public bool IsNull => this.Pointer is null;
|
||||
|
||||
/// <summary>
|
||||
/// True when the pointer is non-null and plausibly dereferenceable.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Guild Wars structures are 4-byte aligned on x86, so a misaligned address is never a
|
||||
/// real structure. When a GWCA pattern scan fails to resolve, it can hand back an
|
||||
/// address inside Gw.exe's code section (for example 0x0048CA72); reading through that
|
||||
/// yields nonsense field values and eventually an access violation. NativeAOT cannot
|
||||
/// throw <c>AccessViolationException</c>, so it fail-fasts and terminates Guild Wars.
|
||||
/// </remarks>
|
||||
public bool IsValid => this.Pointer is not null && ((nuint)this.Pointer & 0x3) == 0;
|
||||
|
||||
public static implicit operator WrappedPointer<T>(T* pointer) => new(pointer);
|
||||
|
||||
public static implicit operator T*(WrappedPointer<T> wrappedPointer) => wrappedPointer.Pointer;
|
||||
|
||||
@@ -1,9 +0,0 @@
|
||||
namespace Daybreak.API.Interop;
|
||||
|
||||
public interface IHook<T>
|
||||
: IDisposable where T : Delegate
|
||||
{
|
||||
public T Continue { get; }
|
||||
|
||||
public nuint ContinueAddress { get; }
|
||||
}
|
||||
@@ -1,78 +0,0 @@
|
||||
using Daybreak.API.Converters;
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace Daybreak.API.Interop;
|
||||
|
||||
[JsonConverter(typeof(PointerValueConverter))]
|
||||
public readonly struct PointerValue(nuint address) : IEquatable<PointerValue>, IEquatable<nuint>, IEquatable<nint>
|
||||
{
|
||||
public readonly nuint Address = address;
|
||||
|
||||
public bool Equals(PointerValue other)
|
||||
{
|
||||
return this.Address.Equals(other.Address);
|
||||
}
|
||||
|
||||
public bool Equals(nuint other)
|
||||
{
|
||||
return this.Address.Equals(other);
|
||||
}
|
||||
|
||||
public bool Equals(nint other)
|
||||
{
|
||||
return this.Address.Equals((nuint)other);
|
||||
}
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
return $"0x{this.Address:X8}";
|
||||
}
|
||||
|
||||
public override bool Equals(object? obj) => obj is PointerValue value && this.Equals(value);
|
||||
|
||||
public override int GetHashCode()
|
||||
{
|
||||
return HashCode.Combine(this.Address);
|
||||
}
|
||||
|
||||
public static bool operator ==(PointerValue left, PointerValue right)
|
||||
{
|
||||
return left.Equals(right);
|
||||
}
|
||||
|
||||
public static bool operator !=(PointerValue left, PointerValue right)
|
||||
{
|
||||
return !(left == right);
|
||||
}
|
||||
|
||||
public static implicit operator PointerValue(nuint address)
|
||||
{
|
||||
return new PointerValue(address);
|
||||
}
|
||||
|
||||
public static implicit operator PointerValue(nint address)
|
||||
{
|
||||
return new PointerValue((nuint)address);
|
||||
}
|
||||
|
||||
public static implicit operator PointerValue(int address)
|
||||
{
|
||||
return new PointerValue((nuint)address);
|
||||
}
|
||||
|
||||
public static implicit operator PointerValue(uint address)
|
||||
{
|
||||
return new PointerValue(address);
|
||||
}
|
||||
|
||||
public static PointerValue Parse(string s)
|
||||
{
|
||||
if (s.StartsWith("0x", StringComparison.InvariantCultureIgnoreCase)
|
||||
&& nuint.TryParse(s.AsSpan(2), System.Globalization.NumberStyles.HexNumber, null, out var v))
|
||||
{
|
||||
return new PointerValue(v);
|
||||
}
|
||||
|
||||
throw new FormatException($"Invalid pointer format: {s}");
|
||||
}
|
||||
}
|
||||
@@ -1,9 +0,0 @@
|
||||
using Daybreak.API.Interop;
|
||||
|
||||
namespace Daybreak.API.Models;
|
||||
|
||||
public sealed class AddressState
|
||||
{
|
||||
public required string Name { get; init; }
|
||||
public required PointerValue Address { get; init; }
|
||||
}
|
||||
@@ -1,38 +0,0 @@
|
||||
using System.Core.Extensions;
|
||||
using System.Extensions;
|
||||
|
||||
namespace Daybreak.API.Models;
|
||||
|
||||
public sealed class AsyncRefreshCache<T>(TimeSpan refreshRate, Func<CancellationToken, ValueTask<T>> refreshFunc)
|
||||
{
|
||||
private readonly Func<CancellationToken, ValueTask<T>> refreshFunc = refreshFunc.ThrowIfNull();
|
||||
|
||||
private readonly SemaphoreSlim semaphoreSlim = new(1);
|
||||
|
||||
private T? cache;
|
||||
private DateTime lastUpdateDateTime = DateTime.MinValue;
|
||||
private TimeSpan refreshRate = refreshRate;
|
||||
|
||||
public async ValueTask<T> GetAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
if (this.cache is not null && DateTime.UtcNow - this.lastUpdateDateTime < this.refreshRate)
|
||||
{
|
||||
return this.cache;
|
||||
}
|
||||
|
||||
using var ctx = await this.semaphoreSlim.Acquire(cancellationToken);
|
||||
if (DateTime.UtcNow - this.lastUpdateDateTime > this.refreshRate ||
|
||||
this.cache is null)
|
||||
{
|
||||
this.lastUpdateDateTime = DateTime.UtcNow;
|
||||
this.cache = await this.refreshFunc(cancellationToken);
|
||||
}
|
||||
|
||||
return this.cache;
|
||||
}
|
||||
|
||||
public void UpdateRefreshRate(TimeSpan newRefreshRate)
|
||||
{
|
||||
this.refreshRate = newRefreshRate;
|
||||
}
|
||||
}
|
||||
@@ -1,27 +0,0 @@
|
||||
using Daybreak.API.Interop;
|
||||
|
||||
namespace Daybreak.API.Models;
|
||||
|
||||
[GWCAEquivalent("Channel")]
|
||||
public enum Channel : uint
|
||||
{
|
||||
Alliance = 0,
|
||||
Allies = 1, // coop with two groups for instance.
|
||||
GWCA1 = 2,
|
||||
All = 3,
|
||||
GWCA2 = 4,
|
||||
Moderator = 5,
|
||||
Emote = 6,
|
||||
Warning = 7, // shows in the middle of the screen and does not parse <c> tags
|
||||
GWCA3 = 8,
|
||||
Guild = 9,
|
||||
Global = 10,
|
||||
Group = 11,
|
||||
Trade = 12,
|
||||
Advisory = 13,
|
||||
Whisper = 14,
|
||||
Count,
|
||||
|
||||
// non-standard channel, but useful.
|
||||
Command
|
||||
};
|
||||
@@ -1,12 +0,0 @@
|
||||
using Daybreak.API.Interop;
|
||||
|
||||
namespace Daybreak.API.Models;
|
||||
|
||||
public sealed class HookState
|
||||
{
|
||||
public required bool Hooked { get; init; }
|
||||
public required string Name { get; init; }
|
||||
public required PointerValue TargetAddress { get; init; }
|
||||
public required PointerValue ContinueAddress { get; init; }
|
||||
public required PointerValue DetourAddress { get; init; }
|
||||
}
|
||||
@@ -1,21 +0,0 @@
|
||||
using Daybreak.API.Interop;
|
||||
|
||||
namespace Daybreak.API.Models;
|
||||
|
||||
[GWCAEquivalent("UIMessage")]
|
||||
public enum UIMessage : uint
|
||||
{
|
||||
None = 0x0,
|
||||
InitFrame = 0x9,
|
||||
DestroyFrame = 0xb,
|
||||
KeyDown = 0x20, // wparam = UIPacket::kKeyAction*
|
||||
KeyUp = 0x22, // wparam = UIPacket::kKeyAction*
|
||||
MouseClick = 0x24, // wparam = UIPacket::kMouseClick*
|
||||
MouseClick2 = 0x31, // wparam = UIPacket::kMouseAction*
|
||||
MouseAction = 0x32, // wparam = UIPacket::kMouseAction*
|
||||
FrameMessage_QuerySelectedIndex = 0x59, // Used to query selected index in character selector
|
||||
WriteToChatLog = 0x10000000 | 0x7F, // wparam = UIPacket::kWriteToChatLog*. Triggered by the game when it wants to add a new message to chat.
|
||||
WriteToChatLogWithSender = 0x10000000 | 0x80, // wparam = UIPacket::kWriteToChatLogWithSender*. Triggered by the game when it wants to add a new message to chat.
|
||||
CheckUIState = 0x10000000 | 0x118, // lparam = uint32_t* out state (2 = char select ready)
|
||||
Logout = 0x10000000 | 0x9D, // wparam = { bool unknown, bool character_select }
|
||||
}
|
||||
@@ -1,66 +0,0 @@
|
||||
using System.Runtime.InteropServices;
|
||||
|
||||
namespace Daybreak.API.Models;
|
||||
|
||||
public static class UIPackets
|
||||
{
|
||||
public enum MouseButtons
|
||||
{
|
||||
Left = 0x0,
|
||||
Middle = 0x1,
|
||||
Right = 0x2
|
||||
}
|
||||
|
||||
public enum ActionState
|
||||
{
|
||||
MouseDown = 0x6,
|
||||
MouseUp = 0x7,
|
||||
MouseClick = 0x8,
|
||||
MouseDoubleClick = 0x9,
|
||||
DragRelease = 0xb,
|
||||
KeyDown = 0xe
|
||||
}
|
||||
|
||||
[StructLayout(LayoutKind.Sequential, Pack = 1)]
|
||||
public readonly struct LogChatMessage(string message, Channel channel)
|
||||
{
|
||||
[MarshalAs(UnmanagedType.LPWStr)]
|
||||
public readonly string Message = message;
|
||||
public readonly Channel Channel = channel;
|
||||
};
|
||||
|
||||
[StructLayout(LayoutKind.Sequential, Pack = 1)]
|
||||
public readonly struct UIChatMessage(Channel channel, string message, Channel channel2)
|
||||
{
|
||||
public readonly Channel Channel = channel;
|
||||
[MarshalAs(UnmanagedType.LPWStr)]
|
||||
public readonly string Message = message;
|
||||
public readonly Channel Channel2 = channel2;
|
||||
}
|
||||
|
||||
[StructLayout(LayoutKind.Sequential, Pack = 1)]
|
||||
public readonly struct KeyAction(uint key, uint wParam = 0x4000, uint lParam = 0x0006)
|
||||
{
|
||||
public readonly uint Key = key;
|
||||
public readonly uint WParam = wParam;
|
||||
public readonly uint LParam = lParam;
|
||||
}
|
||||
|
||||
[StructLayout(LayoutKind.Sequential, Pack = 1)]
|
||||
public readonly struct MouseClick(MouseButtons mouseButton, uint isDoubleClick, uint unknownTypeScreenPos)
|
||||
{
|
||||
public readonly MouseButtons MouseButton = mouseButton;
|
||||
public readonly uint IsDoubleClick = isDoubleClick;
|
||||
public readonly uint UnknownTypeScreenPos = unknownTypeScreenPos;
|
||||
}
|
||||
|
||||
[StructLayout(LayoutKind.Sequential, Pack = 1)]
|
||||
public readonly struct MouseAction(uint frameId, uint childOffsetId, ActionState currentState, nuint wparam = 0, nuint lparam = 0)
|
||||
{
|
||||
public readonly uint FrameId = frameId;
|
||||
public readonly uint ChildOffsetId = childOffsetId;
|
||||
public readonly ActionState CurrentState = currentState;
|
||||
public readonly nuint WParam = wparam;
|
||||
public readonly nuint LParam = lparam;
|
||||
}
|
||||
}
|
||||
@@ -17,10 +17,6 @@ namespace Daybreak.API.Serialization;
|
||||
[JsonSerializable(typeof(JsonElement))]
|
||||
[JsonSerializable(typeof(HealthCheckResponse))]
|
||||
[JsonSerializable(typeof(HealthCheckEntryResponse))]
|
||||
[JsonSerializable(typeof(HookState))]
|
||||
[JsonSerializable(typeof(ArraySegment<HookState>))]
|
||||
[JsonSerializable(typeof(AddressState))]
|
||||
[JsonSerializable(typeof(ArraySegment<AddressState>))]
|
||||
[JsonSerializable(typeof(MainPlayerState))]
|
||||
[JsonSerializable(typeof(QuestLogInformation))]
|
||||
[JsonSerializable(typeof(QuestInformation))]
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
using Daybreak.API.Interop.GuildWars;
|
||||
using Daybreak.API.Models;
|
||||
using Daybreak.API.Extensions;
|
||||
using Daybreak.API.Interop;
|
||||
using Daybreak.API.Interop.GuildWars;
|
||||
using Daybreak.API.Services.Interop;
|
||||
using Daybreak.Shared.Models.Api;
|
||||
using System.Core.Extensions;
|
||||
using System.Extensions;
|
||||
using System.Extensions.Core;
|
||||
using System.Runtime.InteropServices;
|
||||
using ZLinq;
|
||||
@@ -15,22 +17,13 @@ public sealed class CharacterSelectService(
|
||||
UIContextService uiContextService,
|
||||
GameThreadService gameThreadService,
|
||||
GameContextService gameContextService,
|
||||
PreferencesService preferencesService,
|
||||
ILogger<CharacterSelectService> logger)
|
||||
{
|
||||
[StructLayout(LayoutKind.Sequential, Pack = 1)]
|
||||
private readonly struct LogOutMessage(uint unknown, uint characterSelect)
|
||||
{
|
||||
public readonly uint Unknown = unknown;
|
||||
public readonly uint CharacterSelect = characterSelect;
|
||||
}
|
||||
|
||||
private readonly InstanceContextService instanceContextService = instanceContextService.ThrowIfNull();
|
||||
private readonly PlatformContextService platformContextService = platformContextService.ThrowIfNull();
|
||||
private readonly UIContextService uiContextService = uiContextService.ThrowIfNull();
|
||||
private readonly GameThreadService gameThreadService = gameThreadService.ThrowIfNull();
|
||||
private readonly GameContextService gameContextService = gameContextService.ThrowIfNull();
|
||||
private readonly PreferencesService preferencesService = preferencesService.ThrowIfNull();
|
||||
private readonly ILogger<CharacterSelectService> logger = logger.ThrowIfNull();
|
||||
|
||||
public Task<CharacterSelectInformation?> GetCharacterSelectInformation(CancellationToken cancellationToken)
|
||||
@@ -42,34 +35,40 @@ public sealed class CharacterSelectService(
|
||||
{
|
||||
var gameContext = this.gameContextService.GetGameContext();
|
||||
if (gameContext.IsNull ||
|
||||
gameContext.Pointer->WorldContext is null)
|
||||
gameContext.Pointer->World is null ||
|
||||
gameContext.Pointer->Character is null)
|
||||
{
|
||||
scopedLogger.LogError("Game context is not initialized");
|
||||
return default;
|
||||
}
|
||||
|
||||
var availableCharsContext = this.gameContextService.GetAvailableChars();
|
||||
if (availableCharsContext.IsNull)
|
||||
if (!availableCharsContext.IsValid ||
|
||||
!availableCharsContext.Pointer->IsValid)
|
||||
{
|
||||
scopedLogger.LogError("Available characters context is not initialized");
|
||||
scopedLogger.LogError(
|
||||
"Available characters context is not initialized (array@0x{Array:X8} buffer=0x{Buffer:X8} capacity={Capacity} size={Size})",
|
||||
(nuint)availableCharsContext.Pointer,
|
||||
availableCharsContext.IsValid ? (nuint)availableCharsContext.Pointer->Buffer : 0,
|
||||
availableCharsContext.IsValid ? availableCharsContext.Pointer->Capacity : 0,
|
||||
availableCharsContext.IsValid ? availableCharsContext.Pointer->Size : 0);
|
||||
return default;
|
||||
}
|
||||
|
||||
var currentUuid = gameContext.Pointer->CharContext->PlayerUuid.ToString();
|
||||
var availableChars = new List<CharacterSelectEntry>((int)availableCharsContext.Pointer->Size);
|
||||
var currentUuid = (*(Uuid*)gameContext.Pointer->Character->PlayerUuid).ToString();
|
||||
var availableChars = new List<CharacterSelectEntry>((int)availableCharsContext.Pointer->Count);
|
||||
foreach (var charContext in *availableCharsContext.Pointer)
|
||||
{
|
||||
var nameSpan = charContext.Name.AsSpan();
|
||||
var name = new string(nameSpan[..nameSpan.IndexOf('\0')]);
|
||||
var name = new string(charContext.Name);
|
||||
availableChars.Add(new CharacterSelectEntry(
|
||||
charContext.Uuid.ToString(),
|
||||
(*(Uuid*)charContext.Uuid).ToString(),
|
||||
name,
|
||||
charContext.Primary,
|
||||
charContext.Secondary,
|
||||
charContext.Campaign,
|
||||
charContext.MapId,
|
||||
charContext.Level,
|
||||
charContext.IsPvp));
|
||||
(uint)charContext.GetPrimaryProfession(),
|
||||
(uint)charContext.GetSecondaryProfession(),
|
||||
(uint)charContext.GetCampaign(),
|
||||
(uint)charContext.GetMapId(),
|
||||
(uint)charContext.GetLevel(),
|
||||
charContext.IsPvP()));
|
||||
}
|
||||
|
||||
// TODO: Reforged sometimes returns Zero UUID even when in-game, need to investigate why
|
||||
@@ -143,6 +142,26 @@ public sealed class CharacterSelectService(
|
||||
public uint Play;
|
||||
}
|
||||
|
||||
[StructLayout(LayoutKind.Explicit, Pack = 1)]
|
||||
private readonly unsafe struct CharSelectorContext
|
||||
{
|
||||
[FieldOffset(0x0000)]
|
||||
public readonly uint Vtable;
|
||||
|
||||
[FieldOffset(0x0004)]
|
||||
public readonly uint FrameId;
|
||||
|
||||
[FieldOffset(0x0008)]
|
||||
public readonly GuildWarsArray<WrappedPointer<CharSelectorChar>> Chars;
|
||||
}
|
||||
|
||||
[StructLayout(LayoutKind.Explicit, Pack = 1)]
|
||||
private readonly struct CharSelectorChar
|
||||
{
|
||||
[FieldOffset(0x0020)]
|
||||
public readonly Array20Char Name;
|
||||
}
|
||||
|
||||
private async Task<bool> SelectCharacterToPlay(string characterName, bool play, CancellationToken cancellationToken)
|
||||
{
|
||||
var scopedLogger = this.logger.CreateScopedLogger();
|
||||
@@ -150,6 +169,12 @@ public sealed class CharacterSelectService(
|
||||
{
|
||||
unsafe
|
||||
{
|
||||
if (!this.IsCharSelectReady())
|
||||
{
|
||||
scopedLogger.LogError("Character select is not ready");
|
||||
return false;
|
||||
}
|
||||
|
||||
var selectorFrame = this.GetCharSelectorFrame();
|
||||
if (selectorFrame is null)
|
||||
{
|
||||
@@ -158,7 +183,8 @@ public sealed class CharacterSelectService(
|
||||
}
|
||||
|
||||
var ctx = this.uiContextService.GetFrameContext<CharSelectorContext>(selectorFrame);
|
||||
if (ctx.IsNull)
|
||||
if (ctx.IsNull ||
|
||||
(nuint)ctx.Pointer <= 0xFFFF)
|
||||
{
|
||||
scopedLogger.LogError("Character selector context not found");
|
||||
return false;
|
||||
@@ -173,11 +199,13 @@ public sealed class CharacterSelectService(
|
||||
|
||||
// Get current selected index
|
||||
uint selectedIdx = 0;
|
||||
this.uiContextService.SendFrameUIMessage(panesFrame, UIMessage.FrameMessage_QuerySelectedIndex, null, &selectedIdx);
|
||||
this.uiContextService.SendFrameUIMessage(panesFrame, UIMessage.kFrameMessage_0x4a, null, &selectedIdx);
|
||||
|
||||
// Find target character index
|
||||
uint targetIdx = 0xFFFF;
|
||||
var charCount = ctx.Pointer->Chars.Size;
|
||||
// Count (not Size) so a not-yet-allocated roster yields zero iterations
|
||||
// instead of indexing through a null buffer.
|
||||
var charCount = ctx.Pointer->Chars.Count;
|
||||
|
||||
for (uint i = 0; i < charCount; i++)
|
||||
{
|
||||
@@ -234,24 +262,29 @@ public sealed class CharacterSelectService(
|
||||
Play = 0
|
||||
};
|
||||
|
||||
var action = new UIPackets.MouseAction(
|
||||
selectorFrame->FrameId,
|
||||
selectorFrame->ChildOffsetId,
|
||||
UIPackets.ActionState.MouseClick,
|
||||
(nuint)(&buttonParam));
|
||||
var action = new kMouseAction
|
||||
{
|
||||
FrameId = selectorFrame->FrameId,
|
||||
ChildOffsetId = selectorFrame->ChildOffsetId,
|
||||
CurrentState = GWCA.GW.UI.UIPacket.ActionState.MouseClick,
|
||||
Wparam = (nint)(&buttonParam)
|
||||
};
|
||||
|
||||
if (!this.uiContextService.SendFrameUIMessage(parentFrame, UIMessage.MouseClick2, &action))
|
||||
if (!this.uiContextService.SendFrameUIMessage(parentFrame, UIMessage.kMouseClick2, &action))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
uint newSelectedIdx = 0;
|
||||
this.uiContextService.SendFrameUIMessage(panesFrame, UIMessage.FrameMessage_QuerySelectedIndex, null, &newSelectedIdx);
|
||||
this.uiContextService.SendFrameUIMessage(panesFrame, UIMessage.kFrameMessage_0x4a, null, &newSelectedIdx);
|
||||
return newSelectedIdx == idx;
|
||||
}
|
||||
}
|
||||
|
||||
// Navigate to target character by selecting previous/next until we reach it
|
||||
// Navigate to target character by selecting previous/next until we reach it.
|
||||
// Empty roster slots are represented by null entries in the character array, so
|
||||
// step over them instead of bailing out (otherwise navigation gets stuck at the
|
||||
// slot right before a gap and never reaches characters beyond it).
|
||||
while (targetIdx < selectedIdx)
|
||||
{
|
||||
if (selectedIdx == 0)
|
||||
@@ -259,13 +292,24 @@ public sealed class CharacterSelectService(
|
||||
break;
|
||||
}
|
||||
|
||||
if (!SelectChar(selectedIdx - 1))
|
||||
var prevIdx = selectedIdx - 1;
|
||||
while (prevIdx > 0 && ctx.Pointer->Chars.Buffer[prevIdx].IsNull)
|
||||
{
|
||||
prevIdx--;
|
||||
}
|
||||
|
||||
if (ctx.Pointer->Chars.Buffer[prevIdx].IsNull)
|
||||
{
|
||||
break;
|
||||
}
|
||||
|
||||
if (!SelectChar(prevIdx))
|
||||
{
|
||||
scopedLogger.LogError("Failed to select previous character");
|
||||
return false;
|
||||
}
|
||||
|
||||
selectedIdx--;
|
||||
selectedIdx = prevIdx;
|
||||
}
|
||||
|
||||
while (targetIdx > selectedIdx)
|
||||
@@ -275,13 +319,24 @@ public sealed class CharacterSelectService(
|
||||
break;
|
||||
}
|
||||
|
||||
if (!SelectChar(selectedIdx + 1))
|
||||
var nextIdx = selectedIdx + 1;
|
||||
while (nextIdx < charCount && ctx.Pointer->Chars.Buffer[nextIdx].IsNull)
|
||||
{
|
||||
nextIdx++;
|
||||
}
|
||||
|
||||
if (nextIdx >= charCount)
|
||||
{
|
||||
break;
|
||||
}
|
||||
|
||||
if (!SelectChar(nextIdx))
|
||||
{
|
||||
scopedLogger.LogError("Failed to select next character");
|
||||
return false;
|
||||
}
|
||||
|
||||
selectedIdx++;
|
||||
selectedIdx = nextIdx;
|
||||
}
|
||||
|
||||
var chosen = selectedIdx == targetIdx;
|
||||
@@ -324,7 +379,7 @@ public sealed class CharacterSelectService(
|
||||
|
||||
await Task.Delay(100, cancellationToken);
|
||||
}
|
||||
catch(Exception e)
|
||||
catch (Exception e)
|
||||
{
|
||||
scopedLogger.LogError(e, "Error while waiting for character select to be ready");
|
||||
throw;
|
||||
@@ -336,25 +391,13 @@ public sealed class CharacterSelectService(
|
||||
return false;
|
||||
}
|
||||
|
||||
var previousOrderPreference = await this.gameThreadService.QueueOnGameThread(() => this.preferencesService.GetEnumPreference(EnumPreference.CharSortOrder), cancellationToken);
|
||||
if (previousOrderPreference is null)
|
||||
{
|
||||
scopedLogger.LogError("Failed to get previous character sort order preference");
|
||||
return false;
|
||||
}
|
||||
|
||||
scopedLogger.LogInformation("Previous character sort order preference: {order}. Setting order to alphabetize", (CharSortOrder)previousOrderPreference);
|
||||
await this.gameThreadService.QueueOnGameThread(() => this.preferencesService.SetEnumPreference(EnumPreference.CharSortOrder, (uint)CharSortOrder.Alphabetize), cancellationToken);
|
||||
try
|
||||
{
|
||||
return await this.SelectCharacterToPlay(characterName, play: true, cancellationToken);
|
||||
}
|
||||
finally
|
||||
{
|
||||
// TODO: Delay to ensure character select has fully processed the selection before reverting sort order. Should be done after loading into the map.
|
||||
await Task.Delay(1000, cancellationToken);
|
||||
await this.gameThreadService.QueueOnGameThread(() => this.preferencesService.SetEnumPreference(EnumPreference.CharSortOrder, previousOrderPreference.Value), cancellationToken);
|
||||
}
|
||||
// NOTE: Mirrors GWToolbox's GW::LoginMgr::SelectCharacterToPlay, which navigates the
|
||||
// character carousel purely by stepping prev/next over the selector context's character
|
||||
// array. It deliberately does NOT touch the in-game CharSortOrder preference: the
|
||||
// navigation relies on the selector context order matching the visible carousel order,
|
||||
// and forcing a re-sort here desyncs the two (the click-by-name steps then land on the
|
||||
// wrong index, so navigation only appears to work in one direction).
|
||||
return await this.SelectCharacterToPlay(characterName, play: true, cancellationToken);
|
||||
}
|
||||
|
||||
private async Task<string?> GetCharNameByUuid(string uuid, CancellationToken cancellationToken)
|
||||
@@ -372,7 +415,8 @@ public sealed class CharacterSelectService(
|
||||
}
|
||||
|
||||
var availableCharsContext = this.gameContextService.GetAvailableChars();
|
||||
if (availableCharsContext.IsNull)
|
||||
if (!availableCharsContext.IsValid ||
|
||||
!availableCharsContext.Pointer->IsValid)
|
||||
{
|
||||
scopedLogger.LogError("Available characters context is not initialized");
|
||||
return default;
|
||||
@@ -380,13 +424,12 @@ public sealed class CharacterSelectService(
|
||||
|
||||
foreach (var charContext in *availableCharsContext.Pointer)
|
||||
{
|
||||
if (charContext.Uuid.ToString() != uuid)
|
||||
if ((*(Uuid*)charContext.Uuid).ToString() != uuid)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var nameSpan = charContext.Name.AsSpan();
|
||||
var name = new string(nameSpan[..nameSpan.IndexOf('\0')]);
|
||||
var name = new string(charContext.Name);
|
||||
return name;
|
||||
}
|
||||
|
||||
@@ -399,10 +442,10 @@ public sealed class CharacterSelectService(
|
||||
{
|
||||
await this.gameThreadService.QueueOnGameThread(() =>
|
||||
{
|
||||
var logoutMessage = new LogOutMessage(0, 1); // Changed to 1 for character select
|
||||
var logoutMessage = new kLogout { Unknown = 0, CharacterSelect = 1 };
|
||||
unsafe
|
||||
{
|
||||
this.uiContextService.SendMessage(UIMessage.Logout, (uint)&logoutMessage, 0);
|
||||
this.uiContextService.SendMessage(UIMessage.kLogout, (uint)&logoutMessage, 0);
|
||||
}
|
||||
}, cancellationToken);
|
||||
}
|
||||
@@ -417,7 +460,27 @@ public sealed class CharacterSelectService(
|
||||
|
||||
private unsafe bool IsCharSelectReady()
|
||||
{
|
||||
return this.GetCharSelectorFrame() is not null;
|
||||
// GW UI state must report char select (2). kCheckUIState fills the value passed via lParam.
|
||||
uint uiState = 10;
|
||||
this.uiContextService.SendMessage(UIMessage.kCheckUIState, 0, (nuint)(&uiState));
|
||||
|
||||
var selectorFrame = this.GetCharSelectorFrame();
|
||||
if (uiState != 2 ||
|
||||
selectorFrame is null ||
|
||||
!selectorFrame->IsVisible)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
// The frame context must be fully populated (a real pointer) and belong to this frame.
|
||||
var ctx = this.uiContextService.GetFrameContext<CharSelectorContext>(selectorFrame);
|
||||
if (ctx.IsNull ||
|
||||
(nuint)ctx.Pointer <= 0xFFFF)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
return ctx.Pointer->FrameId == selectorFrame->FrameId;
|
||||
}
|
||||
|
||||
private unsafe Frame* GetCharSelectorFrame()
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
using Daybreak.API.Interop;
|
||||
using Daybreak.API.Interop.GuildWars;
|
||||
using Daybreak.API.Models;
|
||||
using Daybreak.API.Services.Interop;
|
||||
using System.Core.Extensions;
|
||||
using System.Extensions;
|
||||
@@ -70,8 +69,15 @@ public sealed class ChatService(
|
||||
span[^2] = '\u0001';
|
||||
});
|
||||
|
||||
using var packet = new UnmanagedStruct<UIPackets.UIChatMessage>(new UIPackets.UIChatMessage(channel, encoded, channel));
|
||||
this.uiContextService.SendMessage(UIMessage.WriteToChatLog, (nuint)packet.Address, 0x0);
|
||||
unsafe
|
||||
{
|
||||
fixed (char* encodedPtr = encoded)
|
||||
{
|
||||
using var packet = new UnmanagedStruct<kPrintChatMessage>(new kPrintChatMessage { Channel = (GWCA.GW.Chat.Channel)channel, Message = (nint)encodedPtr });
|
||||
this.uiContextService.SendMessage(UIMessage.kWriteToChatLog, (nuint)packet.Address, 0x0);
|
||||
}
|
||||
}
|
||||
|
||||
}, cancellationToken);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,15 +5,7 @@ namespace Daybreak.API.Services.Interop;
|
||||
|
||||
public unsafe sealed class AgentContextService
|
||||
{
|
||||
public WrappedPointer<GuildWarsArray<WrappedPointer<AgentContext>>> GetAgentArray()
|
||||
{
|
||||
// GuildWarsArray<nint> and GuildWarsArray<WrappedPointer<AgentContext>> have the same memory layout
|
||||
// since both nint and WrappedPointer<T> are pointer-sized
|
||||
return (GuildWarsArray<WrappedPointer<AgentContext>>*)GWCA.GW.Agents.GetAgentArray();
|
||||
}
|
||||
public uint GetPlayerAgentId() => GWCA.GW.PlayerMgr.GetPlayerAgentId(0);
|
||||
|
||||
public uint GetPlayerAgentId()
|
||||
{
|
||||
return GWCA.GW.PlayerMgr.GetPlayerAgentId(0);
|
||||
}
|
||||
public Agent* GetAgentById(uint agentId) => GWCA.GW.Agents.GetAgentByID(agentId);
|
||||
}
|
||||
|
||||
@@ -10,7 +10,7 @@ public unsafe sealed class GameContextService
|
||||
|
||||
public WrappedPointer<PreGameContext> GetPreGameContext() => GWCA.GW.GetPreGameContext();
|
||||
|
||||
public WrappedPointer<GuildWarsArray<CharInfoContext>> GetAvailableChars() => GWCA.GW.GetAvailableChars();
|
||||
public WrappedPointer<GuildWarsArray<CharacterInformation>> GetAvailableChars() => GWCA.GW.GetAvailableChars();
|
||||
|
||||
public bool IsMapLoaded() => GWCA.GW.Map.GetIsMapLoaded();
|
||||
}
|
||||
|
||||
@@ -7,7 +7,7 @@ public sealed unsafe class InstanceContextService
|
||||
{
|
||||
public WrappedPointer<AreaInfo> GetAreaInfo() => GWCA.GW.Map.GetMapInfo(0);
|
||||
|
||||
public ServerRegion GetServerRegion() => GWCA.GW.Map.GetRegion();
|
||||
public ServerRegion GetServerRegion() => (Daybreak.API.Interop.GuildWars.ServerRegion)GWCA.GW.Map.GetRegion();
|
||||
|
||||
public InstanceType GetInstanceType() => GWCA.GW.Map.GetInstanceType();
|
||||
public InstanceType GetInstanceType() => (Daybreak.API.Interop.GuildWars.InstanceType)GWCA.GW.Map.GetInstanceType();
|
||||
}
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
using Daybreak.API.Interop;
|
||||
using static Daybreak.API.Interop.GWCA.GW.Constants;
|
||||
|
||||
namespace Daybreak.API.Services.Interop;
|
||||
|
||||
public sealed class PartyContextService
|
||||
{
|
||||
public bool AddHero(uint heroId) => GWCA.GW.PartyMgr.AddHero((int)heroId);
|
||||
public bool AddHero(uint heroId) => GWCA.GW.PartyMgr.AddHero((HeroID)heroId);
|
||||
|
||||
public bool KickHero(uint heroId) => GWCA.GW.PartyMgr.KickHero((int)heroId);
|
||||
public bool KickHero(uint heroId) => GWCA.GW.PartyMgr.KickHero((HeroID)heroId);
|
||||
|
||||
public bool KickAllHeroes() => GWCA.GW.PartyMgr.KickAllHeroes();
|
||||
|
||||
|
||||
@@ -5,7 +5,7 @@ namespace Daybreak.API.Services.Interop;
|
||||
|
||||
public sealed class PreferencesService()
|
||||
{
|
||||
public uint? GetEnumPreference(EnumPreference pref) => GWCA.GW.UI.GetPreference(pref);
|
||||
public uint? GetEnumPreference(EnumPreference pref) => GWCA.GW.UI.GetPreference((GWCA.GW.UI.EnumPreference)pref);
|
||||
|
||||
public bool SetEnumPreference(EnumPreference pref, uint value) => GWCA.GW.UI.SetPreference(pref, value);
|
||||
public bool SetEnumPreference(EnumPreference pref, uint value) => GWCA.GW.UI.SetPreference((GWCA.GW.UI.EnumPreference)pref, value);
|
||||
}
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
using Daybreak.API.Interop;
|
||||
using Daybreak.API.Interop.GuildWars;
|
||||
using Daybreak.API.Models;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Core.Extensions;
|
||||
using System.Extensions.Core;
|
||||
@@ -23,21 +22,7 @@ public sealed class UIContextService(ILogger<UIContextService> logger)
|
||||
public unsafe WrappedPointer<T> GetFrameContext<T>(WrappedPointer<Frame> frame)
|
||||
where T : unmanaged
|
||||
{
|
||||
if (frame.IsNull || frame.Pointer->FrameCallbacks.Size == 0)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
for (uint i = 0; i < frame.Pointer->FrameCallbacks.Size; i++)
|
||||
{
|
||||
var callback = frame.Pointer->FrameCallbacks.Buffer[i];
|
||||
if (callback.UiCtl_Context != null)
|
||||
{
|
||||
return new WrappedPointer<T>((T*)callback.UiCtl_Context);
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
return (T*)GWCA.GW.UI.GetFrameContext(frame.Pointer);
|
||||
}
|
||||
|
||||
public unsafe WrappedPointer<Frame> GetChildFrame(WrappedPointer<Frame> parent, uint childOffset)
|
||||
@@ -66,16 +51,16 @@ public sealed class UIContextService(ILogger<UIContextService> logger)
|
||||
return false;
|
||||
}
|
||||
|
||||
return GWCA.GW.UI.SendFrameUIMessage(frame, messageId, arg1, (nint)arg2);
|
||||
return GWCA.GW.UI.SendFrameUIMessage(frame, (GWCA.GW.UI.UIMessage)messageId, arg1, (nint)arg2);
|
||||
}
|
||||
|
||||
public unsafe bool SetFrameDisabled(WrappedPointer<Frame> frame, bool disabled) => GWCA.GW.UI.SetFrameDisabled(frame, disabled);
|
||||
|
||||
public unsafe bool SetFrameVisible(WrappedPointer<Frame> frame, bool visible) => GWCA.GW.UI.SetFrameVisible(frame, visible);
|
||||
|
||||
public unsafe bool KeyDown(ControlAction action, WrappedPointer<Frame> frame) => GWCA.GW.UI.Keydown(action, frame);
|
||||
public unsafe bool KeyDown(GWCA.GW.UI.ControlAction action, WrappedPointer<Frame> frame) => GWCA.GW.UI.Keydown(action, frame);
|
||||
|
||||
public unsafe bool KeyUp(ControlAction action, WrappedPointer<Frame> frame) => GWCA.GW.UI.Keyup(action, frame);
|
||||
public unsafe bool KeyUp(GWCA.GW.UI.ControlAction action, WrappedPointer<Frame> frame) => GWCA.GW.UI.Keyup(action, frame);
|
||||
|
||||
public unsafe WrappedPointer<Frame> GetFrameByLabel(string label)
|
||||
{
|
||||
@@ -107,9 +92,9 @@ public sealed class UIContextService(ILogger<UIContextService> logger)
|
||||
return GWCA.GW.UI.ButtonClick(frame);
|
||||
}
|
||||
|
||||
public unsafe void SendMessage(UIMessage message, nuint wParam, nuint lParam) => GWCA.GW.UI.SendUIMessage(message, (void*)wParam, (nint)lParam);
|
||||
public unsafe void SendMessage(UIMessage message, nuint wParam, nuint lParam) => GWCA.GW.UI.SendUIMessage((GWCA.GW.UI.UIMessage)message, (void*)wParam, (nint)lParam);
|
||||
|
||||
public unsafe Task<string> AsyncDecodeStringAsync(ushort* encodedString, Language language = Language.Unknown, CancellationToken cancellationToken = default)
|
||||
public unsafe Task<string> AsyncDecodeStringAsync(ushort* encodedString, GWCA.GW.Constants.Language language = GWCA.GW.Constants.Language.Unknown, CancellationToken cancellationToken = default)
|
||||
{
|
||||
var taskCompletionSource = new TaskCompletionSource<string>();
|
||||
cancellationToken.Register(() => taskCompletionSource.TrySetCanceled(cancellationToken));
|
||||
@@ -150,7 +135,7 @@ public sealed class UIContextService(ILogger<UIContextService> logger)
|
||||
return taskCompletionSource.Task;
|
||||
}
|
||||
|
||||
public unsafe Task<string> AsyncDecodeStringAsync(string encodedString, Language language = Language.Unknown, CancellationToken cancellationToken = default)
|
||||
public unsafe Task<string> AsyncDecodeStringAsync(string encodedString, GWCA.GW.Constants.Language language = GWCA.GW.Constants.Language.Unknown, CancellationToken cancellationToken = default)
|
||||
{
|
||||
// Pin the string and convert to ushort*
|
||||
fixed (char* encodedPtr = encodedString)
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
using Daybreak.API.Interop.GuildWars;
|
||||
using Daybreak.API.Interop;
|
||||
using Daybreak.API.Interop.GuildWars;
|
||||
using Daybreak.API.Services.Interop;
|
||||
using Daybreak.Shared.Models.Api;
|
||||
using Daybreak.Shared.Models.Guildwars;
|
||||
using System.Extensions.Core;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Text;
|
||||
|
||||
namespace Daybreak.API.Services;
|
||||
@@ -32,45 +34,48 @@ public sealed class InventoryService(
|
||||
return default;
|
||||
}
|
||||
|
||||
var inventory = gameContext.Pointer->ItemContext->Inventory;
|
||||
if (inventory.IsNull)
|
||||
var inventory = gameContext.Pointer->Items->Inventory;
|
||||
if (inventory is null)
|
||||
{
|
||||
scopedLogger.LogError("Inventory is null");
|
||||
return default;
|
||||
}
|
||||
|
||||
var itemTuples = new List<(BagType Type,List<(uint ModelId, string EncodedCompleteName, string EncodedSingleName, string EncodedName, bool Inscribable, int Quantity, uint[] Modifiers, ItemType ItemType)>)>();
|
||||
foreach (var bag in inventory.Pointer->Bags)
|
||||
var itemTuples = new List<(GWCA.GW.Constants.BagType Type, List<(uint ModelId, string EncodedCompleteName, string EncodedSingleName, string EncodedName, bool Inscribable, int Quantity, uint[] Modifiers, ItemType ItemType, uint Interaction, uint ModelFileId, Byte DyeTint, ushort Value)>)>();
|
||||
for (var i = 0; i < 23; i++)
|
||||
{
|
||||
// Use GWCA helper to fetch bag pointers by index — safer than reading an inline array directly.
|
||||
var bag = GWCA.GW.Items.GetBagByIndex((uint)i);
|
||||
if (bag is null)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var retBag = new List<(uint ModelId, string EncodedCompleteName, string EncodedSingleName, string EncodedName, bool Inscribable, int Quantity, uint[] Modifiers, ItemType ItemType)>();
|
||||
itemTuples.Add((bag->Type, retBag));
|
||||
var retBag = new List<(uint ModelId, string EncodedCompleteName, string EncodedSingleName, string EncodedName, bool Inscribable, int Quantity, uint[] Modifiers, ItemType ItemType, uint Interaction, uint ModelFileId, Byte DyeTint, ushort Value)>();
|
||||
itemTuples.Add((bag->BagType, retBag));
|
||||
if (bag->ItemsCount is 0)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
foreach(var item in bag->Items)
|
||||
foreach (var itemPtr in bag->Items.Value)
|
||||
{
|
||||
if (item.IsNull)
|
||||
if (itemPtr is 0)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var singleItemName = new string(item.Pointer->SingleItemName);
|
||||
var nameEncoded = new string(item.Pointer->NameEncoded);
|
||||
var completeNameEncoded = new string(item.Pointer->CompleteNameEncoded);
|
||||
var modifiers = new uint[item.Pointer->ModifiersCount];
|
||||
for(var j = 0; j < item.Pointer->ModifiersCount; j++)
|
||||
var item = (ItemStruct*)itemPtr;
|
||||
var singleItemName = new string((char*)item->SingleItemName);
|
||||
var nameEncoded = new string((char*)item->NameEnc);
|
||||
var completeNameEncoded = new string((char*)item->CompleteNameEnc);
|
||||
var modifiers = new uint[item->ModStructSize];
|
||||
for (var j = 0; j < item->ModStructSize; j++)
|
||||
{
|
||||
modifiers[j] = item.Pointer->Modifiers[j].Mod;
|
||||
modifiers[j] = item->ModStruct[j].Mod;
|
||||
}
|
||||
|
||||
retBag.Add((item.Pointer->ModelId, completeNameEncoded, singleItemName, nameEncoded, item.Pointer->Inscribable, item.Pointer->Quantity, modifiers, item.Pointer->Type));
|
||||
retBag.Add((item->ModelId, completeNameEncoded, singleItemName, nameEncoded, (item->Interaction & 0x08000000) != 0, item->Quantity, modifiers, (ItemType)item->Type, item->Interaction, item->ModelFileId, item->Dye.DyeTint, item->Value));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -85,17 +90,18 @@ public sealed class InventoryService(
|
||||
|
||||
// Decode strings sequentially to avoid race conditions with the game's TextParser language field.
|
||||
// Parallel decoding causes crashes because multiple operations race to set/restore the language.
|
||||
var decodedItemTuples = new List<(BagType Type, List<(uint ModelId, string EncodedName, string? DecodedName, string EncodedSingleName, string? DecodedSingleName, string EncodedCompleteName, string? DecodedCompleteName, bool Inscribable, int Quantity, uint[] Modifiers, ItemType ItemType)> Items)>();
|
||||
var decodedItemTuples = new List<(GWCA.GW.Constants.BagType Type, List<(uint ModelId, string EncodedName, string? DecodedName, string EncodedSingleName, string? DecodedSingleName, string EncodedCompleteName, string? DecodedCompleteName, bool Inscribable, int Quantity, uint[] Modifiers, ItemType ItemType, uint Interaction, uint ModelFileId, byte DyeTint, ushort Value)> Items)>();
|
||||
foreach (var tuple in itemTuples)
|
||||
{
|
||||
var decodedItems = new List<(uint ModelId, string EncodedName, string? DecodedName, string EncodedSingleName, string? DecodedSingleName, string EncodedCompleteName, string? DecodedCompleteName, bool Inscribable, int Quantity, uint[] Modifiers, ItemType ItemType)>();
|
||||
var decodedItems = new List<(uint ModelId, string EncodedName, string? DecodedName, string EncodedSingleName, string? DecodedSingleName, string EncodedCompleteName, string? DecodedCompleteName, bool Inscribable, int Quantity, uint[] Modifiers, ItemType ItemType, uint Interaction, uint ModelFileId, byte DyeTint, ushort Value)>();
|
||||
foreach (var item in tuple.Item2)
|
||||
{
|
||||
var decodedName = await this.uIService.DecodeString(item.EncodedName, Language.English, cancellationToken);
|
||||
var decodedCompleteName = await this.uIService.DecodeString(item.EncodedCompleteName, Language.English, cancellationToken);
|
||||
var decodedSingleName = await this.uIService.DecodeString(item.EncodedSingleName, Language.English, cancellationToken);
|
||||
decodedItems.Add((item.ModelId, item.EncodedName, decodedName, item.EncodedSingleName, decodedSingleName, item.EncodedCompleteName, decodedCompleteName, item.Inscribable, item.Quantity, item.Modifiers, item.ItemType));
|
||||
var decodedName = await this.uIService.DecodeString(item.EncodedName, (GWCA.GW.Constants.Language)Language.English, cancellationToken);
|
||||
var decodedCompleteName = await this.uIService.DecodeString(item.EncodedCompleteName, (GWCA.GW.Constants.Language)Language.English, cancellationToken);
|
||||
var decodedSingleName = await this.uIService.DecodeString(item.EncodedSingleName, (GWCA.GW.Constants.Language)Language.English, cancellationToken);
|
||||
decodedItems.Add((item.ModelId, item.EncodedName, decodedName, item.EncodedSingleName, decodedSingleName, item.EncodedCompleteName, decodedCompleteName, item.Inscribable, item.Quantity, item.Modifiers, item.ItemType, item.Interaction, item.ModelFileId, item.DyeTint, item.Value));
|
||||
}
|
||||
|
||||
decodedItemTuples.Add((tuple.Type, decodedItems));
|
||||
}
|
||||
|
||||
@@ -116,7 +122,11 @@ public sealed class InventoryService(
|
||||
Quantity: item.Quantity,
|
||||
Modifiers: item.Modifiers,
|
||||
Properties: [.. ItemProperty.ParseItemModifiers([.. item.Modifiers.Select(m => (Shared.Models.Guildwars.ItemModifier)m)])],
|
||||
ItemType: item.ItemType.ToString()))]))]);
|
||||
ItemType: item.ItemType.ToString(),
|
||||
Interaction: item.Interaction,
|
||||
ModelFileId: item.ModelFileId,
|
||||
DyeTint: item.DyeTint,
|
||||
Value: item.Value))]))]);
|
||||
}
|
||||
|
||||
private static string ToBase64(string encoded)
|
||||
|
||||
@@ -22,16 +22,14 @@ public sealed class LoginService(
|
||||
unsafe
|
||||
{
|
||||
var gameContext = this.gameContextService.GetGameContext();
|
||||
if (gameContext.IsNull || gameContext.Pointer->CharContext is null)
|
||||
if (gameContext.IsNull || gameContext.Pointer->Character is null)
|
||||
{
|
||||
scopedLogger.LogError("Game context is not initialized");
|
||||
return default;
|
||||
}
|
||||
|
||||
var playerNameSpan = gameContext.Pointer->CharContext->PlayerName.AsSpan();
|
||||
var playerName = new string(playerNameSpan[..playerNameSpan.IndexOf('\0')]);
|
||||
var emailSpan = gameContext.Pointer->CharContext->PlayerEmail.AsSpan();
|
||||
var email = new string(emailSpan[..emailSpan.IndexOf('\0')]);
|
||||
var playerName = new string(gameContext.Pointer->Character->PlayerName);
|
||||
var email = new string(gameContext.Pointer->Character->PlayerEmail);
|
||||
return new LoginInfo(email, playerName);
|
||||
}
|
||||
}, cancellationToken);
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using Daybreak.API.Extensions;
|
||||
using Daybreak.API.Interop;
|
||||
using Daybreak.API.Interop.GuildWars;
|
||||
using Daybreak.API.Models;
|
||||
using Daybreak.API.Services.Interop;
|
||||
@@ -72,7 +73,7 @@ public sealed class MainPlayerService : IDisposable
|
||||
|
||||
var gameContext = this.gameContextService.GetGameContext();
|
||||
if (gameContext.IsNull ||
|
||||
gameContext.Pointer->WorldContext is null)
|
||||
gameContext.Pointer->World is null)
|
||||
{
|
||||
scopedLogger.LogError("Failed to get game context");
|
||||
return false;
|
||||
@@ -85,23 +86,23 @@ public sealed class MainPlayerService : IDisposable
|
||||
return false;
|
||||
}
|
||||
|
||||
var playerAgent = this.GetAgentContext(playerAgentId);
|
||||
var playerAgent = this.agentContextService.GetAgentById(playerAgentId);
|
||||
if (playerAgent is null ||
|
||||
playerAgent->Type is not AgentType.Living ||
|
||||
playerAgent->Type is not (uint)AgentType.Living ||
|
||||
playerAgent->AgentId != playerAgentId)
|
||||
{
|
||||
scopedLogger.LogError("Player agent {playerAgentId} not found in agent array", playerAgentId);
|
||||
return false;
|
||||
}
|
||||
|
||||
var livingAgent = (AgentLivingContext*)playerAgent;
|
||||
if (livingAgent->Level != gameContext.Pointer->WorldContext->Level)
|
||||
var livingAgent = (AgentLiving*)playerAgent;
|
||||
if (livingAgent->Level != gameContext.Pointer->World->Level)
|
||||
{
|
||||
scopedLogger.LogError("Player agent not found. Player level mismatch: {level} != {gameLevel}", livingAgent->Level, gameContext.Pointer->WorldContext->Level);
|
||||
scopedLogger.LogError("Player agent not found. Player level mismatch: {level} != {gameLevel}", livingAgent->Level, gameContext.Pointer->World->Level);
|
||||
return false;
|
||||
}
|
||||
|
||||
var agentProfession = gameContext.Pointer->WorldContext->Professions.AsValueEnumerable().FirstOrDefault(p => p.AgentId == playerAgentId);
|
||||
var agentProfession = gameContext.Pointer->World->PartyProfessionStates.AsValueEnumerable().FirstOrDefault(p => p.AgentId == playerAgentId);
|
||||
if (agentProfession.AgentId != playerAgentId)
|
||||
{
|
||||
scopedLogger.LogError("Failed to find agent profession for player agent id {agentId}", playerAgentId);
|
||||
@@ -112,9 +113,9 @@ public sealed class MainPlayerService : IDisposable
|
||||
(uint)singleBuild.Primary.Id,
|
||||
(uint)singleBuild.Secondary.Id,
|
||||
singleBuild.Skills.AsValueEnumerable().Select(s => (uint)s.Id).ToArray(),
|
||||
livingAgent->Primary,
|
||||
agentProfession.UnlockedProfessionsFlags,
|
||||
[.. gameContext.Pointer->WorldContext->UnlockedCharacterSkills]);
|
||||
livingAgent->Tags->Primary,
|
||||
agentProfession.UnlockedProfessions,
|
||||
[.. gameContext.Pointer->World->UnlockedCharacterSkills]);
|
||||
|
||||
if (!this.buildTemplateManager.CanTemplateApply(validationRequest))
|
||||
{
|
||||
@@ -122,9 +123,9 @@ public sealed class MainPlayerService : IDisposable
|
||||
return false;
|
||||
}
|
||||
|
||||
var attributeIds = new Array12Uint();
|
||||
var attributeIds = new AttributeArray12();
|
||||
var attributeValues = new Array12Uint();
|
||||
var skills = new Array8Uint();
|
||||
var skills = new SkillIDArray8();
|
||||
for (var i = 0; i < singleBuild.Attributes.Count && i < 12; i++)
|
||||
{
|
||||
if (singleBuild.Attributes[i].Attribute is null)
|
||||
@@ -132,23 +133,24 @@ public sealed class MainPlayerService : IDisposable
|
||||
continue;
|
||||
}
|
||||
|
||||
attributeIds[i] = (uint)singleBuild.Attributes[i].Attribute!.Id;
|
||||
attributeIds[i] = (GWCA.GW.Constants.Attribute)singleBuild.Attributes[i].Attribute!.Id;
|
||||
attributeValues[i] = (uint)singleBuild.Attributes[i].Points;
|
||||
}
|
||||
|
||||
for (var i = 0; i < singleBuild.Skills.Count && i < 8; i++)
|
||||
{
|
||||
skills[i] = (uint)singleBuild.Skills[i].Id;
|
||||
skills[i] = (GWCA.GW.Constants.SkillID)singleBuild.Skills[i].Id;
|
||||
}
|
||||
|
||||
var skillTemplate = new SkillTemplate(
|
||||
(uint)singleBuild.Primary.Id,
|
||||
(uint)singleBuild.Secondary.Id,
|
||||
(uint)Math.Min(singleBuild.Attributes.Count, 12),
|
||||
attributeIds,
|
||||
attributeValues,
|
||||
skills);
|
||||
|
||||
var skillTemplate = new SkillTemplate
|
||||
{
|
||||
Primary = (GWCA.GW.Constants.Profession)singleBuild.Primary.Id,
|
||||
Secondary = (GWCA.GW.Constants.Profession)singleBuild.Secondary.Id,
|
||||
AttributesCount = (uint)Math.Min(singleBuild.Attributes.Count, 12),
|
||||
AttributeIds = attributeIds,
|
||||
Skills = skills,
|
||||
};
|
||||
Unsafe.CopyBlock(skillTemplate.AttributeValues, Unsafe.AsPointer(ref attributeValues), (uint)(12 * sizeof(uint)));
|
||||
this.skillbarContextService.LoadBuild(playerAgentId, &skillTemplate);
|
||||
return true;
|
||||
}
|
||||
@@ -156,11 +158,11 @@ public sealed class MainPlayerService : IDisposable
|
||||
|
||||
if (result)
|
||||
{
|
||||
await this.chatService.AddMessageAsync("Build applied successfully.", "Daybreak.API", Channel.Moderator, cancellationToken);
|
||||
await this.chatService.AddMessageAsync("Build applied successfully.", "Daybreak.API", Channel.CHANNEL_MODERATOR, cancellationToken);
|
||||
}
|
||||
else
|
||||
{
|
||||
await this.chatService.AddMessageAsync("Failed to apply build.", "Daybreak.API", Channel.Moderator, cancellationToken);
|
||||
await this.chatService.AddMessageAsync("Failed to apply build.", "Daybreak.API", Channel.CHANNEL_MODERATOR, cancellationToken);
|
||||
}
|
||||
|
||||
return result;
|
||||
@@ -193,21 +195,21 @@ public sealed class MainPlayerService : IDisposable
|
||||
return default;
|
||||
}
|
||||
|
||||
var skillbarContext = gameContext.Pointer->WorldContext->Skillbars.AsValueEnumerable().FirstOrDefault(s => s.AgentId == playerAgentId);
|
||||
var skillbarContext = gameContext.Pointer->World->Skillbar.Value.AsValueEnumerable().FirstOrDefault(s => s.AgentId == playerAgentId);
|
||||
if (skillbarContext.AgentId != playerAgentId)
|
||||
{
|
||||
scopedLogger.LogError("Failed to find skillbar context for player agent id {agentId}", playerAgentId);
|
||||
return default;
|
||||
}
|
||||
|
||||
var agentAttributes = gameContext.Pointer->WorldContext->Attributes.AsValueEnumerable().FirstOrDefault(a => a.AgentId == playerAgentId);
|
||||
var agentAttributes = gameContext.Pointer->World->Attributes.Value.AsValueEnumerable().FirstOrDefault(a => a.AgentId == playerAgentId);
|
||||
if (agentAttributes.AgentId != playerAgentId)
|
||||
{
|
||||
scopedLogger.LogError("Failed to find agent attributes for player agent id {agentId}", playerAgentId);
|
||||
return default;
|
||||
}
|
||||
|
||||
var professionContext = gameContext.Pointer->WorldContext->Professions.AsValueEnumerable().FirstOrDefault(p => p.AgentId == playerAgentId);
|
||||
var professionContext = gameContext.Pointer->World->PartyProfessionStates.AsValueEnumerable().FirstOrDefault(p => p.AgentId == playerAgentId);
|
||||
if (professionContext.AgentId != playerAgentId)
|
||||
{
|
||||
scopedLogger.LogError("Failed to find agent profession for player agent id {agentId}", playerAgentId);
|
||||
@@ -215,11 +217,11 @@ public sealed class MainPlayerService : IDisposable
|
||||
}
|
||||
|
||||
return new BuildEntry(
|
||||
(int)professionContext.CurrentPrimary,
|
||||
(int)professionContext.CurrentSecondary,
|
||||
agentAttributes.Attributes.GetAttributeEntryList(),
|
||||
[skillbarContext.Skill0.Id, skillbarContext.Skill1.Id, skillbarContext.Skill2.Id, skillbarContext.Skill3.Id,
|
||||
skillbarContext.Skill4.Id, skillbarContext.Skill5.Id, skillbarContext.Skill6.Id, skillbarContext.Skill7.Id]);
|
||||
(int)professionContext.Primary,
|
||||
(int)professionContext.Secondary,
|
||||
[.. agentAttributes.Attribute.GetAttributeEntryList()],
|
||||
[(uint)skillbarContext.Skills[0].SkillId, (uint)skillbarContext.Skills[1].SkillId, (uint)skillbarContext.Skills[2].SkillId, (uint)skillbarContext.Skills[3].SkillId,
|
||||
(uint)skillbarContext.Skills[4].SkillId, (uint)skillbarContext.Skills[5].SkillId, (uint)skillbarContext.Skills[6].SkillId, (uint)skillbarContext.Skills[7].SkillId]);
|
||||
}
|
||||
}, cancellationToken);
|
||||
}
|
||||
@@ -238,15 +240,15 @@ public sealed class MainPlayerService : IDisposable
|
||||
}
|
||||
|
||||
var gameContext = this.gameContextService.GetGameContext();
|
||||
if (gameContext.IsNull || gameContext.Pointer->WorldContext is null)
|
||||
if (gameContext.IsNull || gameContext.Pointer->World is null)
|
||||
{
|
||||
scopedLogger.LogError("Game context is not initialized");
|
||||
return default;
|
||||
}
|
||||
|
||||
return new QuestLogInformation(
|
||||
gameContext.Pointer->WorldContext->ActiveQuestId,
|
||||
gameContext.Pointer->WorldContext->QuestLog.AsValueEnumerable().Select(q => new QuestInformation(q.QuestId, q.MapFrom)).ToList());
|
||||
(uint)gameContext.Pointer->World->ActiveQuestId,
|
||||
gameContext.Pointer->World->QuestLog.Value.AsValueEnumerable().Select(q => new QuestInformation((uint)q.QuestId, (uint)q.MapFrom)).ToList());
|
||||
}
|
||||
}, cancellationToken);
|
||||
}
|
||||
@@ -265,29 +267,27 @@ public sealed class MainPlayerService : IDisposable
|
||||
}
|
||||
|
||||
var gameContext = this.gameContextService.GetGameContext();
|
||||
if (gameContext.IsNull || gameContext.Pointer->CharContext is null ||
|
||||
gameContext.Pointer->WorldContext is null)
|
||||
if (gameContext.IsNull || gameContext.Pointer->Character is null ||
|
||||
gameContext.Pointer->World is null)
|
||||
{
|
||||
scopedLogger.LogError("Game context is not initialized");
|
||||
return default;
|
||||
}
|
||||
|
||||
var playerNameSpan = gameContext.Pointer->CharContext->PlayerName.AsSpan();
|
||||
var playerName = new string(playerNameSpan[..playerNameSpan.IndexOf('\0')]);
|
||||
var emailSpan = gameContext.Pointer->CharContext->PlayerEmail.AsSpan();
|
||||
var email = new string(emailSpan[..emailSpan.IndexOf('\0')]);
|
||||
var accountName = new string(gameContext.Pointer->WorldContext->AccountInfo->AccountName);
|
||||
var playerName = new string (gameContext.Pointer->Character->PlayerName);
|
||||
var email = new string(gameContext.Pointer->Character->PlayerEmail);
|
||||
var accountName = new string((char*)gameContext.Pointer->World->AccountInfo->AccountName);
|
||||
return new MainPlayerInformation(
|
||||
gameContext.Pointer->CharContext->PlayerUuid.ToString(),
|
||||
(*(Uuid*)gameContext.Pointer->Character->PlayerUuid).ToString(),
|
||||
email,
|
||||
playerName,
|
||||
accountName,
|
||||
gameContext.Pointer->WorldContext->AccountInfo->Wins,
|
||||
gameContext.Pointer->WorldContext->AccountInfo->Losses,
|
||||
gameContext.Pointer->WorldContext->AccountInfo->Rating,
|
||||
gameContext.Pointer->WorldContext->AccountInfo->QualifierPoints,
|
||||
gameContext.Pointer->WorldContext->AccountInfo->Rank,
|
||||
gameContext.Pointer->WorldContext->AccountInfo->TournamentRewardPoints);
|
||||
gameContext.Pointer->World->AccountInfo->Wins,
|
||||
gameContext.Pointer->World->AccountInfo->Losses,
|
||||
gameContext.Pointer->World->AccountInfo->Rating,
|
||||
gameContext.Pointer->World->AccountInfo->QualifierPoints,
|
||||
gameContext.Pointer->World->AccountInfo->Rank,
|
||||
gameContext.Pointer->World->AccountInfo->TournamentRewardPoints);
|
||||
}
|
||||
}, cancellationToken);
|
||||
}
|
||||
@@ -306,61 +306,57 @@ public sealed class MainPlayerService : IDisposable
|
||||
}
|
||||
|
||||
var gameContext = this.gameContextService.GetGameContext();
|
||||
var agentArray = this.agentContextService.GetAgentArray();
|
||||
var playerAgentId = this.agentContextService.GetPlayerAgentId();
|
||||
if (gameContext.IsNull ||
|
||||
gameContext.Pointer->WorldContext is null ||
|
||||
gameContext.Pointer->CharContext is null ||
|
||||
gameContext.Pointer->WorldContext->MapAgents.Buffer is null ||
|
||||
agentArray.IsNull ||
|
||||
agentArray.Pointer->Buffer is null ||
|
||||
gameContext.Pointer->World is null ||
|
||||
gameContext.Pointer->Character is null ||
|
||||
playerAgentId is 0x0)
|
||||
{
|
||||
scopedLogger.LogDebug("Game data is not yet initialized");
|
||||
return default;
|
||||
}
|
||||
|
||||
var playerAgent = this.GetAgentContext(playerAgentId);
|
||||
var playerAgent = this.agentContextService.GetAgentById(playerAgentId);
|
||||
if (playerAgent is null ||
|
||||
playerAgent->Type is not AgentType.Living ||
|
||||
playerAgent->Type is not (uint)AgentType.Living ||
|
||||
playerAgent->AgentId != playerAgentId)
|
||||
{
|
||||
scopedLogger.LogError("Player agent {playerAgentId} not found in agent array", playerAgentId);
|
||||
return default;
|
||||
}
|
||||
|
||||
var livingAgent = (AgentLivingContext*)playerAgent;
|
||||
if (livingAgent->Level != gameContext.Pointer->WorldContext->Level)
|
||||
var livingAgent = (AgentLiving*)playerAgent;
|
||||
if (livingAgent->Level != gameContext.Pointer->World->Level)
|
||||
{
|
||||
scopedLogger.LogError("Player agent not found. Player level mismatch: {level} != {gameLevel}", livingAgent->Level, gameContext.Pointer->WorldContext->Level);
|
||||
scopedLogger.LogError("Player agent not found. Player level mismatch: {level} != {gameLevel}", livingAgent->Level, gameContext.Pointer->World->Level);
|
||||
return default;
|
||||
}
|
||||
|
||||
return new MainPlayerState(
|
||||
gameContext.Pointer->WorldContext->Experience,
|
||||
gameContext.Pointer->WorldContext->Level,
|
||||
gameContext.Pointer->World->Experience,
|
||||
gameContext.Pointer->World->Level,
|
||||
|
||||
gameContext.Pointer->WorldContext->CurrentLuxon,
|
||||
gameContext.Pointer->WorldContext->CurrentKurzick,
|
||||
gameContext.Pointer->WorldContext->CurrentImperial,
|
||||
gameContext.Pointer->WorldContext->CurrentBalthazar,
|
||||
gameContext.Pointer->WorldContext->MaxLuxon,
|
||||
gameContext.Pointer->WorldContext->MaxKurzick,
|
||||
gameContext.Pointer->WorldContext->MaxImperial,
|
||||
gameContext.Pointer->WorldContext->MaxBalthazar,
|
||||
gameContext.Pointer->WorldContext->TotalLuxon,
|
||||
gameContext.Pointer->WorldContext->TotalKurzick,
|
||||
gameContext.Pointer->WorldContext->TotalImperial,
|
||||
gameContext.Pointer->WorldContext->TotalBalthazar,
|
||||
gameContext.Pointer->World->CurrentLuxon,
|
||||
gameContext.Pointer->World->CurrentKurzick,
|
||||
gameContext.Pointer->World->CurrentImperial,
|
||||
gameContext.Pointer->World->CurrentBalth,
|
||||
gameContext.Pointer->World->MaxLuxon,
|
||||
gameContext.Pointer->World->MaxKurzick,
|
||||
gameContext.Pointer->World->MaxImperial,
|
||||
gameContext.Pointer->World->MaxBalth,
|
||||
gameContext.Pointer->World->TotalEarnedLuxon,
|
||||
gameContext.Pointer->World->TotalEarnedKurzick,
|
||||
gameContext.Pointer->World->TotalEarnedImperial,
|
||||
gameContext.Pointer->World->TotalEarnedBalth,
|
||||
|
||||
// Energy and health are percentages of Max
|
||||
livingAgent->Health * livingAgent->MaxHealth,
|
||||
livingAgent->MaxHealth,
|
||||
livingAgent->Hp * livingAgent->MaxHp,
|
||||
livingAgent->MaxHp,
|
||||
livingAgent->Energy * livingAgent->MaxEnergy,
|
||||
livingAgent->MaxEnergy,
|
||||
|
||||
livingAgent->Primary,
|
||||
livingAgent->Secondary,
|
||||
livingAgent->Tags->Primary,
|
||||
livingAgent->Tags->Secondary,
|
||||
|
||||
playerAgent->Pos.X,
|
||||
playerAgent->Pos.Y
|
||||
@@ -386,18 +382,17 @@ public sealed class MainPlayerService : IDisposable
|
||||
var currentMapInfo = this.instanceContextService.GetAreaInfo();
|
||||
var serverRegion = this.instanceContextService.GetServerRegion();
|
||||
if (gameContext.IsNull ||
|
||||
gameContext.Pointer->CharContext is null ||
|
||||
gameContext.Pointer->WorldContext is null ||
|
||||
gameContext.Pointer->PartyContext is null ||
|
||||
gameContext.Pointer->Character is null ||
|
||||
gameContext.Pointer->World is null ||
|
||||
gameContext.Pointer->Party is 0 ||
|
||||
currentMapInfo.IsNull)
|
||||
{
|
||||
scopedLogger.LogError("Game context is not initialized");
|
||||
return new InstanceInfo(0, 0, 0, 0, Shared.Models.Api.InstanceType.Loading, DistrictRegionInfo.Unknown, LanguageInfo.Unknown, CampaignInfo.Unknown, ContinentInfo.Unknown, RegionInfo.Unknown, DifficultyInfo.Unknown);
|
||||
}
|
||||
|
||||
var charContext = *gameContext.Pointer->CharContext;
|
||||
var worldContext = *gameContext.Pointer->WorldContext;
|
||||
var partyContext = *gameContext.Pointer->PartyContext;
|
||||
var charContext = *gameContext.Pointer->Character;
|
||||
var worldContext = *gameContext.Pointer->World;
|
||||
var mapInfo = *currentMapInfo.Pointer;
|
||||
var mapId = charContext.MapId;
|
||||
var language = charContext.Language;
|
||||
@@ -406,7 +401,7 @@ public sealed class MainPlayerService : IDisposable
|
||||
var foesToKill = worldContext.FoesToKill;
|
||||
|
||||
return new InstanceInfo(
|
||||
MapId: charContext.MapId,
|
||||
MapId: (uint)charContext.MapId,
|
||||
DistrictNumber: charContext.DistrictNumber,
|
||||
FoesKilled: worldContext.FoesKilled,
|
||||
FoesToKill: worldContext.FoesToKill,
|
||||
@@ -416,7 +411,7 @@ public sealed class MainPlayerService : IDisposable
|
||||
Campaign: (CampaignInfo)mapInfo.Campaign,
|
||||
Continent: (ContinentInfo)mapInfo.Continent,
|
||||
Region: (RegionInfo)mapInfo.Region,
|
||||
Difficulty: partyContext.Flags.HasFlag(PartyFlags.HardMode) ? DifficultyInfo.Hard : DifficultyInfo.Normal);
|
||||
Difficulty: GWCA.GW.PartyMgr.GetIsPartyInHardMode() ? DifficultyInfo.Hard : DifficultyInfo.Normal);
|
||||
}
|
||||
}, cancellationToken);
|
||||
}
|
||||
@@ -435,27 +430,26 @@ public sealed class MainPlayerService : IDisposable
|
||||
}
|
||||
|
||||
var gameContext = this.gameContextService.GetGameContext();
|
||||
if (gameContext.IsNull || gameContext.Pointer->WorldContext is null)
|
||||
if (gameContext.IsNull || gameContext.Pointer->World is null)
|
||||
{
|
||||
scopedLogger.LogError("Game context is not initialized");
|
||||
return default;
|
||||
}
|
||||
|
||||
var playerIndex = gameContext.Pointer->CharContext->PlayerNumber;
|
||||
var players = gameContext.Pointer->WorldContext->Players;
|
||||
if (playerIndex < 0 || playerIndex >= players.Size)
|
||||
var playerIndex = gameContext.Pointer->Character->PlayerNumber;
|
||||
var player = GWCA.GW.PlayerMgr.GetPlayerByID(playerIndex);
|
||||
if (player is null)
|
||||
{
|
||||
scopedLogger.LogError("Failed to get player id. Got {playerNumber}", playerIndex);
|
||||
scopedLogger.LogError("Failed to get player");
|
||||
return default;
|
||||
}
|
||||
|
||||
var player = gameContext.Pointer->WorldContext->Players[(int)playerIndex];
|
||||
var currentTier = player.ActiveTitleTier;
|
||||
TitleContext? currentTitle = default;
|
||||
var currentTier = player->ActiveTitleTier;
|
||||
Title? currentTitle = default;
|
||||
int? id = -1;
|
||||
for (var i = 0; i < gameContext.Pointer->WorldContext->Titles.Size; i++)
|
||||
for (var i = 0; i < gameContext.Pointer->World->Titles.Value.Size; i++)
|
||||
{
|
||||
var title = gameContext.Pointer->WorldContext->Titles.Skip(i).FirstOrDefault();
|
||||
var title = gameContext.Pointer->World->Titles.Value.Skip(i).FirstOrDefault();
|
||||
if (title.CurrentTitleTierIndex == currentTier)
|
||||
{
|
||||
currentTitle = title;
|
||||
@@ -470,15 +464,15 @@ public sealed class MainPlayerService : IDisposable
|
||||
return default;
|
||||
}
|
||||
|
||||
var titleTier = gameContext.Pointer->WorldContext->TitleTiers.Skip((int)currentTitle.Value.CurrentTitleTierIndex).FirstOrDefault();
|
||||
var titleTier = gameContext.Pointer->World->TitleTiers.Skip((int)currentTitle.Value.CurrentTitleTierIndex).FirstOrDefault();
|
||||
return new TitleInfo
|
||||
(
|
||||
Id: (uint)id,
|
||||
IsPercentage: currentTitle.Value.Props.HasFlag(TitleProps.PercentageBased),
|
||||
PointsForCurrentRank: currentTitle.Value.PointsNeededForCurrentRank,
|
||||
IsPercentage: currentTitle.Value.IsPercentageBased(),
|
||||
PointsForCurrentRank: currentTitle.Value.PointsNeededCurrentRank,
|
||||
PointsForNextRank: titleTier.TierNumber == currentTitle.Value.MaxTitleRank ?
|
||||
currentTitle.Value.CurrentPoints :
|
||||
currentTitle.Value.PointsNeededForNextRank,
|
||||
currentTitle.Value.PointsNeededNextRank,
|
||||
CurrentPoints: currentTitle.Value.CurrentPoints,
|
||||
TierNumber: titleTier.TierNumber,
|
||||
MaxTierNumber: currentTitle.Value.MaxTitleTierIndex
|
||||
@@ -502,8 +496,8 @@ public sealed class MainPlayerService : IDisposable
|
||||
|
||||
var gameContext = this.gameContextService.GetGameContext();
|
||||
if (gameContext.IsNull ||
|
||||
gameContext.Pointer->AccountContext is null ||
|
||||
gameContext.Pointer->WorldContext is null)
|
||||
gameContext.Pointer->Account is null ||
|
||||
gameContext.Pointer->World is null)
|
||||
{
|
||||
this.logger.LogError("Game context is not initialized");
|
||||
return default;
|
||||
@@ -516,7 +510,7 @@ public sealed class MainPlayerService : IDisposable
|
||||
return default;
|
||||
}
|
||||
|
||||
var agentProfession = gameContext.Pointer->WorldContext->Professions.AsValueEnumerable().FirstOrDefault(p => p.AgentId == playerAgentId);
|
||||
var agentProfession = gameContext.Pointer->World->PartyProfessionStates.AsValueEnumerable().FirstOrDefault(p => p.AgentId == playerAgentId);
|
||||
if (agentProfession.AgentId != playerAgentId)
|
||||
{
|
||||
scopedLogger.LogError("Failed to find agent profession for player agent id {agentId}", playerAgentId);
|
||||
@@ -524,25 +518,11 @@ public sealed class MainPlayerService : IDisposable
|
||||
}
|
||||
|
||||
return new MainPlayerBuildContext(
|
||||
PrimaryProfessionId: (uint)agentProfession.CurrentPrimary,
|
||||
UnlockedProfessions: agentProfession.UnlockedProfessionsFlags,
|
||||
UnlockedAccountSkills: gameContext.Pointer->AccountContext->UnlockedAccountSkills.AsValueEnumerable().ToArray(),
|
||||
UnlockedCharacterSkills: gameContext.Pointer->WorldContext->UnlockedCharacterSkills.AsValueEnumerable().ToArray());
|
||||
PrimaryProfessionId: (uint)agentProfession.Primary,
|
||||
UnlockedProfessions: agentProfession.UnlockedProfessions,
|
||||
UnlockedCharacterSkills: gameContext.Pointer->World->UnlockedCharacterSkills.AsValueEnumerable().ToArray());
|
||||
|
||||
}
|
||||
}, cancellationToken);
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
private unsafe AgentContext* GetAgentContext(uint agentId)
|
||||
{
|
||||
var agentArray = this.agentContextService.GetAgentArray();
|
||||
if (agentArray.IsNull || agentArray.Pointer->Buffer is null ||
|
||||
agentArray.Pointer->Size <= agentId)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
return agentArray.Pointer->Buffer[agentId].Pointer;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using Daybreak.API.Extensions;
|
||||
using Daybreak.API.Interop;
|
||||
using Daybreak.API.Interop.GuildWars;
|
||||
using Daybreak.API.Models;
|
||||
using Daybreak.API.Services.Interop;
|
||||
@@ -9,6 +10,7 @@ using Daybreak.Shared.Services.BuildTemplates;
|
||||
using System.Core.Extensions;
|
||||
using System.Extensions;
|
||||
using System.Extensions.Core;
|
||||
using System.Runtime.CompilerServices;
|
||||
using ZLinq;
|
||||
using InstanceType = Daybreak.API.Interop.GuildWars.InstanceType;
|
||||
|
||||
@@ -46,21 +48,21 @@ public sealed class PartyService(
|
||||
if (!await this.IsInValidOutpost(cancellationToken))
|
||||
{
|
||||
scopedLogger.LogError("Could not set party loadout. Not in a valid outpost");
|
||||
await this.chatService.AddMessageAsync("Cannot set party loadout. Not in a valid outpost.", "Daybreak.API", Channel.Moderator, cancellationToken);
|
||||
await this.chatService.AddMessageAsync("Cannot set party loadout. Not in a valid outpost.", "Daybreak.API", Channel.CHANNEL_MODERATOR, cancellationToken);
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!await this.KickAllHeroes(cancellationToken))
|
||||
{
|
||||
scopedLogger.LogError("Could not set party loadout. Could not leave party");
|
||||
await this.chatService.AddMessageAsync("Cannot set party loadout. Could not leave party.", "Daybreak.API", Channel.Moderator, cancellationToken);
|
||||
await this.chatService.AddMessageAsync("Cannot set party loadout. Could not leave party.", "Daybreak.API", Channel.CHANNEL_MODERATOR, cancellationToken);
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!await this.gameThreadService.QueueOnGameThread(() => this.SpawnHeroes(partyLoadout), cancellationToken))
|
||||
{
|
||||
scopedLogger.LogError("Could not set party loadout. Could not spawn heroes");
|
||||
await this.chatService.AddMessageAsync("Cannot set party loadout. Could not spawn heroes.", "Daybreak.API", Channel.Moderator, cancellationToken);
|
||||
await this.chatService.AddMessageAsync("Cannot set party loadout. Could not spawn heroes.", "Daybreak.API", Channel.CHANNEL_MODERATOR, cancellationToken);
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -69,21 +71,21 @@ public sealed class PartyService(
|
||||
if (!await this.gameThreadService.QueueOnGameThread(() => this.ApplyBuilds(partyLoadout), cancellationToken))
|
||||
{
|
||||
scopedLogger.LogError("Could not set party loadout. Could not apply builds");
|
||||
await this.chatService.AddMessageAsync("Cannot set party loadout. Could not apply builds.", "Daybreak.API", Channel.Moderator, cancellationToken);
|
||||
await this.chatService.AddMessageAsync("Cannot set party loadout. Could not apply builds.", "Daybreak.API", Channel.CHANNEL_MODERATOR, cancellationToken);
|
||||
return false;
|
||||
}
|
||||
|
||||
var heroBehaviorSetup = await this.gameThreadService.QueueOnGameThread(() => this.GetHeroBehaviorSetup(partyLoadout), cancellationToken);
|
||||
foreach (var heroBehaviorEntry in heroBehaviorSetup ?? [])
|
||||
foreach (var (AgentId, Behavior) in heroBehaviorSetup ?? [])
|
||||
{
|
||||
if (!await this.SetHeroBehavior(heroBehaviorEntry.AgentId, heroBehaviorEntry.Behavior, cancellationToken))
|
||||
if (!await this.SetHeroBehavior(AgentId, Behavior, cancellationToken))
|
||||
{
|
||||
scopedLogger.LogWarning("Could not set hero behavior for agent {agentId} to {behavior}", heroBehaviorEntry.AgentId, heroBehaviorEntry.Behavior);
|
||||
await this.chatService.AddMessageAsync($"Cannot set party loadout. Could not set behavior {heroBehaviorEntry.Behavior} for agent {heroBehaviorEntry.AgentId}.", "Daybreak.API", Channel.Moderator, cancellationToken);
|
||||
scopedLogger.LogWarning("Could not set hero behavior for agent {agentId} to {behavior}", AgentId, Behavior);
|
||||
await this.chatService.AddMessageAsync($"Cannot set party loadout. Could not set behavior {Behavior} for agent {AgentId}.", "Daybreak.API", Channel.CHANNEL_MODERATOR, cancellationToken);
|
||||
}
|
||||
}
|
||||
|
||||
await this.chatService.AddMessageAsync("Party loadout set.", "Daybreak.API", Channel.Moderator, cancellationToken);
|
||||
await this.chatService.AddMessageAsync("Party loadout set.", "Daybreak.API", Channel.CHANNEL_MODERATOR, cancellationToken);
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -126,9 +128,9 @@ public sealed class PartyService(
|
||||
|
||||
var buildTuples = professions.Value.AsValueEnumerable()
|
||||
.Select(p => (p.AgentId, p, attributes.Value.FirstOrDefault(a => a.AgentId == p.AgentId), skillbars.Value.FirstOrDefault(s => s.AgentId == p.AgentId), heroFlags.Value.FirstOrDefault(f => f.AgentId == p.AgentId)))
|
||||
.Select(t => (t.AgentId, GetBuildEntryById(t.p, t.Item4, t.Item3), t.Item5.Behavior))
|
||||
.Select(t => (t.AgentId, GetBuildEntryById(t.p, t.Item4, t.Item3), t.Item5.HeroBehavior))
|
||||
.Where(t => t.Item2 is not null)
|
||||
.OfType<(uint AgentId, BuildEntry BuildEntry, Behavior Behavior)>()
|
||||
.OfType<(uint AgentId, BuildEntry BuildEntry, API.Interop.GWCA.GW.HeroBehavior Behavior)>()
|
||||
.ToList();
|
||||
|
||||
return new PartyLoadout(
|
||||
@@ -136,16 +138,17 @@ public sealed class PartyService(
|
||||
{
|
||||
if (t.AgentId == playerId)
|
||||
{
|
||||
return new PartyLoadoutEntry(0, HeroBehavior.Undefined, t.BuildEntry);
|
||||
return new PartyLoadoutEntry(0, Shared.Models.Api.HeroBehavior.Undefined, t.BuildEntry);
|
||||
}
|
||||
else if (heroes.Value.FirstOrDefault(h => h.AgentId == t.AgentId) is HeroPartyMember hero &&
|
||||
hero.AgentId == t.AgentId)
|
||||
{
|
||||
return new PartyLoadoutEntry((int)hero.HeroId, (HeroBehavior)t.Behavior, t.BuildEntry);
|
||||
return new PartyLoadoutEntry((int)hero.HeroId, (Shared.Models.Api.HeroBehavior)t.Behavior, t.BuildEntry);
|
||||
}
|
||||
|
||||
return default;
|
||||
})
|
||||
.Where(t => t is not null)
|
||||
.OfType<PartyLoadoutEntry>()]);
|
||||
}
|
||||
}, cancellationToken);
|
||||
@@ -179,7 +182,7 @@ public sealed class PartyService(
|
||||
}, cancellationToken);
|
||||
}
|
||||
|
||||
public async Task<bool> SetHeroBehavior(uint heroAgentId, HeroBehavior behavior, CancellationToken cancellationToken)
|
||||
public async Task<bool> SetHeroBehavior(uint heroAgentId, Shared.Models.Api.HeroBehavior behavior, CancellationToken cancellationToken)
|
||||
{
|
||||
var scopedLogger = this.logger.CreateScopedLogger();
|
||||
|
||||
@@ -209,14 +212,14 @@ public sealed class PartyService(
|
||||
unsafe
|
||||
{
|
||||
var btn = this.uIContextService.GetChildFrame(frame.Frame, (uint)behavior);
|
||||
var packet = new UIPackets.MouseClick(UIPackets.MouseButtons.Left, 0, 0);
|
||||
var packet = new kMouseClick { MouseButton = 0, IsDoubleclick = 0 };
|
||||
if (btn.IsNull)
|
||||
{
|
||||
scopedLogger.LogError("Could not set hero behavior. Could not find button for behavior {behavior} in frame {frameLabel}", behavior, heroCommanderFrameLabel);
|
||||
return false;
|
||||
}
|
||||
|
||||
return this.uIContextService.SendFrameUIMessage(btn, UIMessage.MouseClick, &packet);
|
||||
return this.uIContextService.SendFrameUIMessage(btn, UIMessage.kMouseClick, &packet);
|
||||
}
|
||||
}, cancellationToken);
|
||||
|
||||
@@ -305,7 +308,7 @@ public sealed class PartyService(
|
||||
}
|
||||
else
|
||||
{
|
||||
var hero = heroes.Value.AsValueEnumerable().FirstOrDefault(h => h.HeroId == entry.HeroId);
|
||||
var hero = heroes.Value.AsValueEnumerable().FirstOrDefault(h => (int)h.HeroId == entry.HeroId);
|
||||
return (entry, hero.AgentId);
|
||||
}
|
||||
})
|
||||
@@ -317,7 +320,7 @@ public sealed class PartyService(
|
||||
: accountContext.Pointer->UnlockedAccountSkills;
|
||||
|
||||
var unlockedProfessionsFlags = t.playerId == playerId
|
||||
? playerProfessionContext.UnlockedProfessionsFlags
|
||||
? playerProfessionContext.UnlockedProfessions
|
||||
: uint.MaxValue;
|
||||
|
||||
if (!this.buildTemplateManager.CanTemplateApply(
|
||||
@@ -325,7 +328,7 @@ public sealed class PartyService(
|
||||
(uint)t.entry.Build.Primary,
|
||||
(uint)t.entry.Build.Secondary,
|
||||
[.. t.entry.Build.Skills],
|
||||
(uint)t.Item3.CurrentPrimary,
|
||||
(uint)t.Item3.Primary,
|
||||
unlockedProfessionsFlags,
|
||||
[.. validSkills])))
|
||||
{
|
||||
@@ -358,28 +361,29 @@ public sealed class PartyService(
|
||||
continue;
|
||||
}
|
||||
|
||||
var attributeIds = new Array12Uint();
|
||||
var attributeIds = new AttributeArray12();
|
||||
var attributeValues = new Array12Uint();
|
||||
for (var i = 0; i < entry.Build.Attributes.Count && i < 12; i++)
|
||||
{
|
||||
attributeIds[i] = (uint)entry.Build.Attributes[i].Id;
|
||||
attributeIds[i] = (GWCA.GW.Constants.Attribute)entry.Build.Attributes[i].Id;
|
||||
attributeValues[i] = (uint)entry.Build.Attributes[i].BasePoints;
|
||||
}
|
||||
|
||||
var skills = new Array8Uint();
|
||||
var skills = new SkillIDArray8();
|
||||
for (var i = 0; i < entry.Build.Skills.Count && i < 8; i++)
|
||||
{
|
||||
skills[i] = entry.Build.Skills[i];
|
||||
skills[i] = (GWCA.GW.Constants.SkillID)entry.Build.Skills[i];
|
||||
}
|
||||
|
||||
var skillTemplate = new SkillTemplate(
|
||||
(uint)entry.Build.Primary,
|
||||
(uint)entry.Build.Secondary,
|
||||
(uint)entry.Build.Attributes.Count,
|
||||
attributeIds,
|
||||
attributeValues,
|
||||
skills);
|
||||
|
||||
var skillTemplate = new SkillTemplate
|
||||
{
|
||||
Primary = (GWCA.GW.Constants.Profession)entry.Build.Primary,
|
||||
Secondary = (GWCA.GW.Constants.Profession)entry.Build.Secondary,
|
||||
AttributesCount = (uint)entry.Build.Attributes.Count,
|
||||
AttributeIds = attributeIds,
|
||||
Skills = skills
|
||||
};
|
||||
Unsafe.CopyBlock(skillTemplate.AttributeValues, Unsafe.AsPointer(ref attributeValues), (uint)(12 * sizeof(uint)));
|
||||
scopedLogger.LogInformation("Applying build for agent {agentId} with primary {primary} and secondary {secondary}", agentId, entry.Build.Primary, entry.Build.Secondary);
|
||||
this.skillbarContextService.LoadBuild(agentId, &skillTemplate);
|
||||
}
|
||||
@@ -387,7 +391,7 @@ public sealed class PartyService(
|
||||
return true;
|
||||
}
|
||||
|
||||
private unsafe List<(uint AgentId, HeroBehavior Behavior)>? GetHeroBehaviorSetup(PartyLoadout partyLoadout)
|
||||
private unsafe List<(uint AgentId, Shared.Models.Api.HeroBehavior Behavior)>? GetHeroBehaviorSetup(PartyLoadout partyLoadout)
|
||||
{
|
||||
var scopedLogger = this.logger.CreateScopedLogger();
|
||||
scopedLogger.LogDebug("Getting hero behavior setup");
|
||||
@@ -402,12 +406,12 @@ public sealed class PartyService(
|
||||
return heroes.Value.AsValueEnumerable()
|
||||
.Select(h =>
|
||||
{
|
||||
if (h.HeroId is 0)
|
||||
if (h.HeroId is API.Interop.GWCA.GW.Constants.HeroID.NoHero)
|
||||
{
|
||||
return default;
|
||||
}
|
||||
|
||||
var entry = partyLoadout.Entries.AsValueEnumerable().FirstOrDefault(e => (uint)e.HeroId == h.HeroId);
|
||||
var entry = partyLoadout.Entries.AsValueEnumerable().FirstOrDefault(e => e.HeroId == (int)h.HeroId);
|
||||
if (entry is null)
|
||||
{
|
||||
scopedLogger.LogWarning("No entry found for hero {heroId}", h.HeroId);
|
||||
@@ -455,21 +459,14 @@ public sealed class PartyService(
|
||||
return -1;
|
||||
}
|
||||
|
||||
var partyContext = gameContext.Pointer->PartyContext;
|
||||
if (partyContext is null)
|
||||
{
|
||||
scopedLogger.LogError("Party context is null");
|
||||
return -1;
|
||||
}
|
||||
|
||||
var charContext = gameContext.Pointer->CharContext;
|
||||
var charContext = gameContext.Pointer->Character;
|
||||
if (charContext is null)
|
||||
{
|
||||
scopedLogger.LogError("Char context is null");
|
||||
return -1;
|
||||
}
|
||||
|
||||
var playerParty = partyContext->PlayerParty;
|
||||
var playerParty = GWCA.GW.PartyMgr.GetPartyInfo(0);
|
||||
var playerId = charContext->PlayerNumber;
|
||||
var offset = 0;
|
||||
foreach(var hero in playerParty->Heroes)
|
||||
@@ -492,22 +489,22 @@ public sealed class PartyService(
|
||||
return heroNumber is -1 ? default : (uint)heroNumber;
|
||||
}
|
||||
|
||||
private static BuildEntry? GetBuildEntryById(ProfessionsContext professionContext, SkillbarContext skillbar, PartyAttribute attributes)
|
||||
private static BuildEntry? GetBuildEntryById(ProfessionState professionContext, SkillbarData skillbar, PartyAttribute attributes)
|
||||
{
|
||||
return new BuildEntry(
|
||||
Primary: (int)professionContext.CurrentPrimary,
|
||||
Secondary: (int)professionContext.CurrentSecondary,
|
||||
Attributes: attributes.Attributes.GetAttributeEntryList(),
|
||||
Primary: (int)professionContext.Primary,
|
||||
Secondary: (int)professionContext.Secondary,
|
||||
Attributes: attributes.Attribute.GetAttributeEntryList().ToList(),
|
||||
Skills:
|
||||
[
|
||||
skillbar.Skill0.Id,
|
||||
skillbar.Skill1.Id,
|
||||
skillbar.Skill2.Id,
|
||||
skillbar.Skill3.Id,
|
||||
skillbar.Skill4.Id,
|
||||
skillbar.Skill5.Id,
|
||||
skillbar.Skill6.Id,
|
||||
skillbar.Skill7.Id
|
||||
(uint)skillbar.Skills[0].SkillId,
|
||||
(uint)skillbar.Skills[1].SkillId,
|
||||
(uint)skillbar.Skills[2].SkillId,
|
||||
(uint)skillbar.Skills[3].SkillId,
|
||||
(uint)skillbar.Skills[4].SkillId,
|
||||
(uint)skillbar.Skills[5].SkillId,
|
||||
(uint)skillbar.Skills[6].SkillId,
|
||||
(uint)skillbar.Skills[7].SkillId
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
using Daybreak.API.Interop;
|
||||
using Daybreak.API.Interop.GuildWars;
|
||||
using Daybreak.API.Models;
|
||||
using Daybreak.API.Services.Interop;
|
||||
using System.Extensions.Core;
|
||||
|
||||
@@ -20,7 +19,7 @@ public sealed class UIService(
|
||||
var scopedLogger = this.logger.CreateScopedLogger();
|
||||
var keyDownResult = await this.gameThreadService.QueueOnGameThread(() =>
|
||||
{
|
||||
if (!this.uIContextService.KeyDown(action, frame))
|
||||
if (!this.uIContextService.KeyDown((GWCA.GW.UI.ControlAction)action, frame))
|
||||
{
|
||||
scopedLogger.LogError("Failed to send keydown action {action}", action);
|
||||
return false;
|
||||
@@ -36,7 +35,7 @@ public sealed class UIService(
|
||||
|
||||
var keyUpResult = await this.gameThreadService.QueueOnGameThread(() =>
|
||||
{
|
||||
if (!this.uIContextService.KeyUp(action, frame))
|
||||
if (!this.uIContextService.KeyUp((GWCA.GW.UI.ControlAction)action, frame))
|
||||
{
|
||||
scopedLogger.LogError("Failed to send keyup action {action}", action);
|
||||
return false;
|
||||
@@ -86,7 +85,7 @@ public sealed class UIService(
|
||||
});
|
||||
}
|
||||
|
||||
public async Task<string?> DecodeString(string encoded, Language language, CancellationToken cancellationToken)
|
||||
public async Task<string?> DecodeString(string encoded, GWCA.GW.Constants.Language language, CancellationToken cancellationToken)
|
||||
{
|
||||
// GWCA's AsyncDecodeStr handles language switching internally (sets language, decodes, restores)
|
||||
return await this.uIContextService.AsyncDecodeStringAsync(encoded, language, cancellationToken);
|
||||
@@ -96,14 +95,14 @@ public sealed class UIService(
|
||||
{
|
||||
return frameLabel switch
|
||||
{
|
||||
"AgentCommander0" => ControlAction.OpenHeroCommander1,
|
||||
"AgentCommander1" => ControlAction.OpenHeroCommander2,
|
||||
"AgentCommander2" => ControlAction.OpenHeroCommander3,
|
||||
"AgentCommander3" => ControlAction.OpenHeroCommander4,
|
||||
"AgentCommander4" => ControlAction.OpenHeroCommander5,
|
||||
"AgentCommander5" => ControlAction.OpenHeroCommander6,
|
||||
"AgentCommander6" => ControlAction.OpenHeroCommander7,
|
||||
_ => ControlAction.None
|
||||
"AgentCommander0" => ControlAction.ControlAction_OpenHeroCommander1,
|
||||
"AgentCommander1" => ControlAction.ControlAction_OpenHeroCommander2,
|
||||
"AgentCommander2" => ControlAction.ControlAction_OpenHeroCommander3,
|
||||
"AgentCommander3" => ControlAction.ControlAction_OpenHeroCommander4,
|
||||
"AgentCommander4" => ControlAction.ControlAction_OpenHeroCommander5,
|
||||
"AgentCommander5" => ControlAction.ControlAction_OpenHeroCommander6,
|
||||
"AgentCommander6" => ControlAction.ControlAction_OpenHeroCommander7,
|
||||
_ => ControlAction.ControlAction_None
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
using Daybreak.API.Services.Interop;
|
||||
using Daybreak.Shared.Services.BuildTemplates;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using Daybreak.Shared.Services.BuildTemplates.Parsers;
|
||||
|
||||
namespace Daybreak.API.Services;
|
||||
|
||||
@@ -8,6 +8,9 @@ public static class WebApplicationBuilderExtensions
|
||||
{
|
||||
public static WebApplicationBuilder WithDaybreakServices(this WebApplicationBuilder builder)
|
||||
{
|
||||
builder.Services.AddSingleton<ITemplateParser, LegacySkillTemplateParser>();
|
||||
builder.Services.AddSingleton<ITemplateParser, SkillTemplateParser>();
|
||||
builder.Services.AddSingleton<ITemplateParser, PartyLoadoutTemplateParser>();
|
||||
builder.Services.AddSingleton<IBuildTemplateManager, BuildTemplateManager>();
|
||||
builder.Services.AddSingleton<MemoryScanningService>();
|
||||
builder.Services.AddSingleton<ChatService>();
|
||||
|
||||
@@ -1,8 +1,4 @@
|
||||
using Microsoft.AspNetCore.Http.Json;
|
||||
using Microsoft.AspNetCore.Routing.Constraints;
|
||||
using Microsoft.Extensions.Options;
|
||||
using Swashbuckle.AspNetCore.SwaggerGen;
|
||||
using System.Text.Json;
|
||||
using Microsoft.AspNetCore.Routing.Constraints;
|
||||
|
||||
namespace Daybreak.API.Swagger;
|
||||
|
||||
@@ -11,14 +7,6 @@ public static class WebApplicationBuilderExtensions
|
||||
public static WebApplicationBuilder WithSwagger(this WebApplicationBuilder builder)
|
||||
{
|
||||
builder.Services.AddEndpointsApiExplorer();
|
||||
builder.Services.AddSwaggerGen();
|
||||
builder.Services.AddTransient<ISerializerDataContractResolver>(sp =>
|
||||
{
|
||||
var opts = sp.GetRequiredService<IOptions<JsonOptions>>().Value?.SerializerOptions
|
||||
?? new JsonSerializerOptions(JsonSerializerDefaults.Web);
|
||||
|
||||
return new JsonSerializerDataContractResolver(opts);
|
||||
});
|
||||
builder.Services.Configure<RouteOptions>(static options =>
|
||||
{
|
||||
options.SetParameterPolicy<RegexInlineRouteConstraint>("regex");
|
||||
|
||||
@@ -1,11 +1,13 @@
|
||||
namespace Daybreak.API.Swagger;
|
||||
using Scalar.AspNetCore;
|
||||
|
||||
namespace Daybreak.API.Swagger;
|
||||
|
||||
public static class WebApplicationExtensions
|
||||
{
|
||||
public static WebApplication UseSwaggerWithUI(this WebApplication app)
|
||||
{
|
||||
app.UseSwagger();
|
||||
app.UseSwaggerUI();
|
||||
app.MapOpenApi();
|
||||
app.MapScalarApiReference();
|
||||
return app;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,40 @@
|
||||
using System.Text.Json.Serialization;
|
||||
using Daybreak.Shared.Attributes;
|
||||
|
||||
namespace Daybreak.Configuration.Options;
|
||||
|
||||
[OptionsName(Name = "Downloads")]
|
||||
internal sealed class DownloadOptions
|
||||
{
|
||||
[JsonPropertyName(nameof(HeaderTimeout))]
|
||||
[OptionName(
|
||||
Name = "Header Timeout",
|
||||
Description = "Amount of seconds Daybreak will wait for a download to respond before giving up on the attempt"
|
||||
)]
|
||||
[OptionRange<double>(MinValue = 5, MaxValue = 300)]
|
||||
public double HeaderTimeout { get; set; } = 30;
|
||||
|
||||
[JsonPropertyName(nameof(ChunkTimeout))]
|
||||
[OptionName(
|
||||
Name = "Chunk Timeout",
|
||||
Description = "Amount of seconds Daybreak will wait for the next chunk of a download before giving up on the attempt"
|
||||
)]
|
||||
[OptionRange<double>(MinValue = 5, MaxValue = 300)]
|
||||
public double ChunkTimeout { get; set; } = 30;
|
||||
|
||||
[JsonPropertyName(nameof(Retries))]
|
||||
[OptionName(
|
||||
Name = "Retries",
|
||||
Description = "Amount of times Daybreak will retry a failed download before reporting it as failed"
|
||||
)]
|
||||
[OptionRange<int>(MinValue = 0, MaxValue = 10)]
|
||||
public int Retries { get; set; } = 3;
|
||||
|
||||
[JsonPropertyName(nameof(RetryDelay))]
|
||||
[OptionName(
|
||||
Name = "Retry Delay",
|
||||
Description = "Amount of seconds Daybreak will wait before retrying a failed download"
|
||||
)]
|
||||
[OptionRange<double>(MinValue = 0, MaxValue = 60)]
|
||||
public double RetryDelay { get; set; } = 2;
|
||||
}
|
||||
@@ -39,4 +39,33 @@ public sealed class FocusViewOptions
|
||||
[JsonPropertyName(nameof(EnergyDisplay))]
|
||||
[OptionName(Name = "Energy Display Mode", Description = "Sets how should the energy display show the information")]
|
||||
public PointsDisplay EnergyDisplay { get; set; }
|
||||
|
||||
[JsonPropertyName(nameof(GridColumns))]
|
||||
[OptionName(Name = "Layout Grid Columns", Description = "Number of columns in the focus view layout grid")]
|
||||
[OptionIgnore]
|
||||
public int GridColumns { get; set; } = DefaultGridColumns;
|
||||
|
||||
[JsonPropertyName(nameof(GridRows))]
|
||||
[OptionName(Name = "Layout Grid Rows", Description = "Number of rows in the focus view layout grid")]
|
||||
[OptionIgnore]
|
||||
public int GridRows { get; set; } = DefaultGridRows;
|
||||
|
||||
[JsonPropertyName(nameof(Layout))]
|
||||
[OptionName(Name = "Layout", Description = "Tile-based layout of the focus view components")]
|
||||
public List<FocusViewTile> Layout { get; set; } = DefaultLayout();
|
||||
|
||||
public const int DefaultGridColumns = 20;
|
||||
public const int DefaultGridRows = 16;
|
||||
|
||||
public static List<FocusViewTile> DefaultLayout() =>
|
||||
[
|
||||
new() { Component = FocusViewComponent.Character, Column = 1, Row = 1, ColumnSpan = 10, RowSpan = 3 },
|
||||
new() { Component = FocusViewComponent.PlayerResources, Column = 1, Row = 4, ColumnSpan = 10, RowSpan = 3 },
|
||||
new() { Component = FocusViewComponent.Vanquish, Column = 1, Row = 7, ColumnSpan = 10, RowSpan = 1 },
|
||||
new() { Component = FocusViewComponent.Title, Column = 1, Row = 8, ColumnSpan = 10, RowSpan = 1 },
|
||||
new() { Component = FocusViewComponent.CurrentMap, Column = 1, Row = 9, ColumnSpan = 10, RowSpan = 1 },
|
||||
new() { Component = FocusViewComponent.QuestLog, Column = 1, Row = 10, ColumnSpan = 10, RowSpan = 7 },
|
||||
new() { Component = FocusViewComponent.Build, Column = 11, Row = 1, ColumnSpan = 10, RowSpan = 5 },
|
||||
new() { Component = FocusViewComponent.Browser, Column = 11, Row = 6, ColumnSpan = 10, RowSpan = 11 },
|
||||
];
|
||||
}
|
||||
|
||||
@@ -9,6 +9,7 @@ using Daybreak.Services.ApplicationArguments.ArgumentHandling;
|
||||
using Daybreak.Services.ApplicationLauncher;
|
||||
using Daybreak.Services.BuildTemplates;
|
||||
using Daybreak.Services.Credentials;
|
||||
using Daybreak.Services.Formatting;
|
||||
using Daybreak.Services.DirectSong;
|
||||
using Daybreak.Services.Downloads;
|
||||
using Daybreak.Services.Events;
|
||||
@@ -48,12 +49,13 @@ using Daybreak.Services.TradeChat.Models;
|
||||
using Daybreak.Services.TradeChat.Notifications;
|
||||
using Daybreak.Services.UMod;
|
||||
using Daybreak.Services.Updater;
|
||||
using Daybreak.Services.Wiki;
|
||||
using Daybreak.Shared.Models.Plugins;
|
||||
using Daybreak.Shared.Services.Api;
|
||||
using Daybreak.Shared.Services.ApplicationArguments;
|
||||
using Daybreak.Shared.Services.ApplicationLauncher;
|
||||
using Daybreak.Shared.Services.BuildTemplates;
|
||||
using Daybreak.Shared.Services.Formatting;
|
||||
using Daybreak.Shared.Services.BuildTemplates.Parsers;
|
||||
using Daybreak.Shared.Services.Credentials;
|
||||
using Daybreak.Shared.Services.DirectSong;
|
||||
using Daybreak.Shared.Services.Downloads;
|
||||
@@ -81,7 +83,6 @@ using Daybreak.Shared.Services.Toolbox;
|
||||
using Daybreak.Shared.Services.TradeChat;
|
||||
using Daybreak.Shared.Services.UMod;
|
||||
using Daybreak.Shared.Services.Updater;
|
||||
using Daybreak.Shared.Services.Wiki;
|
||||
using Daybreak.Services.Graph;
|
||||
using Daybreak.Themes;
|
||||
using Daybreak.Views;
|
||||
@@ -127,7 +128,13 @@ public class ProjectConfiguration : PluginConfigurationBase
|
||||
var resourceBuilder = ResourceBuilder
|
||||
.CreateDefault()
|
||||
.AddService("Daybreak", serviceVersion: CurrentVersion.ToString());
|
||||
var attributes = new List<KeyValuePair<string, object>>();
|
||||
var attributes = new List<KeyValuePair<string, object>>
|
||||
{
|
||||
// Mirror the fleet-wide `service` stream field (promtail sets it for
|
||||
// container logs) so Daybreak shows up in Grafana Logs Drilldown's
|
||||
// `service` picker, alongside the `service_name` set by AddService.
|
||||
new("service", "Daybreak"),
|
||||
};
|
||||
#if DEBUG
|
||||
attributes.Add(new KeyValuePair<string, object>("deployment.environment", "debug"));
|
||||
#else
|
||||
@@ -140,8 +147,11 @@ public class ProjectConfiguration : PluginConfigurationBase
|
||||
});
|
||||
|
||||
services.AddSingleton<AppViewModel>();
|
||||
|
||||
services.AddSingleton<ViewManager>();
|
||||
|
||||
services.AddSingleton<ITemplateParser, LegacySkillTemplateParser>();
|
||||
services.AddSingleton<ITemplateParser, SkillTemplateParser>();
|
||||
services.AddSingleton<ITemplateParser, PartyLoadoutTemplateParser>();
|
||||
services.AddSingleton<IMenuService, MenuService>();
|
||||
services.AddSingleton<IMenuServiceInitializer, MenuService>(sp =>
|
||||
sp.GetRequiredService<IMenuService>().Cast<MenuService>()
|
||||
@@ -221,6 +231,7 @@ public class ProjectConfiguration : PluginConfigurationBase
|
||||
services.AddScoped<IOnboardingService, OnboardingService>();
|
||||
services.AddScoped<IExperienceCalculator, ExperienceCalculator>();
|
||||
services.AddScoped<IAttributePointCalculator, AttributePointCalculator>();
|
||||
services.AddScoped<ISkillNumberFormatter, SkillNumberFormatter>();
|
||||
services.AddScoped<IDownloadService, DownloadService>();
|
||||
services.AddScoped<IGuildWarsInstaller, IntegratedGuildwarsInstaller>();
|
||||
services.AddScoped<IExceptionHandler, ExceptionHandler>();
|
||||
@@ -244,14 +255,13 @@ public class ProjectConfiguration : PluginConfigurationBase
|
||||
services.AddScoped<ILaunchConfigurationService, LaunchConfigurationService>();
|
||||
services.AddScoped<IEventService, EventService>();
|
||||
services.AddScoped<IApplicationArgumentService, ApplicationArgumentService>();
|
||||
services.AddScoped<IWikiService, WikiService>();
|
||||
services.AddScoped<IScreenshotService, ScreenshotService>();
|
||||
|
||||
services.AddHostedSingleton<IApplicationUpdater, ApplicationUpdater>();
|
||||
services.AddHostedSingleton<IThemeManager, BlazorThemeInteropService>();
|
||||
services.AddHostedSingleton<IConnectivityStatus, ConnectivityStatus>();
|
||||
services.AddHostedSingleton<ITradeAlertingService, TradeAlertingService>();
|
||||
services.AddHostedSingleton<IGuildWarsExecutableManager, GuildWarsExecutableManager>();
|
||||
services.AddSingleton<IGuildWarsExecutableManager, GuildWarsExecutableManager>();
|
||||
services.AddHostedSingleton<IEventNotifierService, EventNotifierService>();
|
||||
services.AddHostedSingleton<IApiScanningService, ApiScanningService>();
|
||||
services.AddHostedSingleton<GameScreenshotsTheme>();
|
||||
@@ -339,6 +349,7 @@ public class ProjectConfiguration : PluginConfigurationBase
|
||||
optionsProducer.RegisterOptions<SynchronizationOptions>();
|
||||
optionsProducer.RegisterOptions<FocusViewOptions>();
|
||||
optionsProducer.RegisterOptions<DaybreakApiOptions>();
|
||||
optionsProducer.RegisterOptions<DownloadOptions>();
|
||||
|
||||
optionsProducer.RegisterOptions<ToolboxOptions>();
|
||||
optionsProducer.RegisterOptions<UModOptions>();
|
||||
@@ -379,8 +390,11 @@ public class ProjectConfiguration : PluginConfigurationBase
|
||||
public override void RegisterMods(IModsProducer modsManager)
|
||||
{
|
||||
modsManager.RegisterMod<IGuildWarsVersionChecker, GuildWarsVersionChecker>();
|
||||
modsManager.RegisterMod<IDaybreakApiService, DaybreakApiService>();
|
||||
// Toolbox must be registered before Daybreak API so that Toolbox injects first
|
||||
// and loads its gwca.dll. Daybreak API will then reuse the already-loaded gwca.dll
|
||||
// instead of loading a second copy, avoiding duplicate native module loads.
|
||||
modsManager.RegisterMod<IToolboxService, ToolboxService>();
|
||||
modsManager.RegisterMod<IDaybreakApiService, DaybreakApiService>();
|
||||
modsManager.RegisterMod<IUModService, UModService>();
|
||||
modsManager.RegisterMod<IDirectSongService, DirectSongService>(singleton: true);
|
||||
}
|
||||
@@ -561,10 +575,6 @@ public class ProjectConfiguration : PluginConfigurationBase
|
||||
.WithMessageHandler(SetupLoggingAndMetrics<ScopedApiContext>)
|
||||
.WithDefaultRequestHeadersSetup(SetupDaybreakUserAgent)
|
||||
.Build()
|
||||
.RegisterHttpClient<WikiService>()
|
||||
.WithMessageHandler(SetupLoggingAndMetrics<WikiService>)
|
||||
.WithDefaultRequestHeadersSetup(SetupDaybreakUserAgent)
|
||||
.Build()
|
||||
.RegisterHttpClient<ReShadeService>()
|
||||
.WithMessageHandler(SetupLoggingAndMetrics<ReShadeService>)
|
||||
.WithDefaultRequestHeadersSetup(SetupDaybreakUserAgent)
|
||||
|
||||
@@ -39,14 +39,15 @@
|
||||
</Target>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Elastic.OpenTelemetry" />
|
||||
<PackageReference Include="Microsoft.Extensions.FileProviders.Embedded" />
|
||||
<PackageReference Include="Microsoft.FluentUI.AspNetCore.Components" />
|
||||
<PackageReference Include="Microsoft.FluentUI.AspNetCore.Components.Icons" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.Components" />
|
||||
<PackageReference Include="Microsoft.Identity.Client" />
|
||||
<PackageReference Include="OpenTelemetry.Exporter.Console" />
|
||||
<PackageReference Include="OpenTelemetry.Exporter.OpenTelemetryProtocol" />
|
||||
<PackageReference Include="OpenTelemetry.Instrumentation.Http" />
|
||||
<PackageReference Include="OpenTelemetry.Instrumentation.Process" />
|
||||
<PackageReference Include="OpenTelemetry.Instrumentation.Runtime" />
|
||||
<PackageReference Include="PeNet" />
|
||||
<PackageReference Include="Plumsy" />
|
||||
@@ -65,7 +66,7 @@
|
||||
<PackageReference Include="Serilog.Sinks.Console" />
|
||||
<PackageReference Include="Serilog.Sinks.Debug" />
|
||||
<PackageReference Include="SharpCompress" />
|
||||
<PackageReference Include="SixLabors.ImageSharp" />
|
||||
<PackageReference Include="StbImageSharp" />
|
||||
<PackageReference Include="System.Security.Cryptography.ProtectedData" />
|
||||
</ItemGroup>
|
||||
|
||||
|
||||
@@ -4,5 +4,14 @@ namespace Daybreak.Models;
|
||||
public sealed class NotificationWrapper
|
||||
{
|
||||
public required Notification Notification { get; init; }
|
||||
public DateTime StartTime { get; set; }
|
||||
public DateTime ExpirationTime { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Incremented every time the notification lifetime is prolonged so the UI can
|
||||
/// restart the expiration progress animation.
|
||||
/// </summary>
|
||||
public int LifetimeGeneration { get; set; }
|
||||
|
||||
public double LifetimeMilliseconds => Math.Max(0, (this.ExpirationTime - this.StartTime).TotalMilliseconds);
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user