Blazor UI Migration (#1085)

* Migrate to Fluent UI

* Setup window events for Blazor WebView

* Setup basic FluentUI styling

* FluentUI links

* Setup views and viewmodel infra

* Remove old navigation logic

* Prepare an example of iframe embed

* Style window + navigation menu

* Setup page based navigation

* Add some UI error handling

* Introduce UI resiliency to exceptions

* Setup settings menu dropdown

* Basic setup for options views

* Setup reusable OptionBase

* Setup options views

* Basic styling

* Setup scoped css files.
Split App into components

* Hook option updates to OptionProvider

* Fix option dropdowns

* Setup multiple themes

* Make fonts scalable

* Move ViewModels under Views

* Launch view + styling

* Setup FocusView base

* Finish BuildListView implementation

* Base for build views

* Setup build routing

* Implement single build editor

* Implement skill description on hover

* Scrape skill information from the wiki and cache it locally

* Scrape and fix Guild Wars wiki info

* Improve mouse hover performance by querying position only when needed

* Skill snippet snaps to edges

* Setup accounts and executables view

* Launch configurations view

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

* Fix skilltype overflow

* Adjust navigation menu

* Setup privilege elevation flow

* Setup MSAL

* Options synchronization view

* Telemetry view

* Version management and update view

* Setup plugins view

* Setup notifications panel

* Base for logs view

* Finish LogView

* Setup metrics view

* Setup UI logging interop

* GuildWarsPartySearch view implementation

* Setup gitlab mirror job

* Eventviewer base

* Remove gitlab mirror

* Remove mirror pipeline

* Fix metricsview crashes

* Extract progress component

* Setup trade chat views

* Setup Download view

* Extract progress styling

* Setup guild wars copy view

* Setup trade alerts

* Fix team template view skill selection

* Setup basic onboarding view

* Overhaul asynchronous progress operations and remove redundant versioning code

* Setup modsview base

* Setup modsview with mod states

* Setup mod installation process

* Cleanup more ui migration
Remove price history #1076

* Screen selector manage view

* Upgrade to .net 10
Closes #1078

* gMod management view

* Trader message notification

* ReShade management

* Update for toolbox

* Setup focusview fetching

* Setup CharacterComponent

* Setup vanquishing component

* Setup TitleInformationComponent

* Closes #1082
Fix API project for Guild Wars Reforged

* Small fixes + attempts to get new exe version

* FileId fixes

* Current Map and Player Resources widgets

* Quest log widget

* BuildComponent implementation

* Closes #1075
Embed wwwroot into the executable

* Cleanup

* Fixes to slnx

* Fix test project
This commit is contained in:
2025-12-05 17:43:57 +01:00
parent 7d78a89d12
commit 794afa6c2c
822 changed files with 19794 additions and 35809 deletions
+1 -2
View File
@@ -1,7 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup>
<TargetFramework>net9.0-windows</TargetFramework>
<TargetFramework>$(TargetFrameworkVersionWindows)</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<OutputType>Library</OutputType>
@@ -53,7 +53,6 @@
<PackageReference Include="Swashbuckle.AspNetCore.Swagger" />
<PackageReference Include="Swashbuckle.AspNetCore.SwaggerGen" />
<PackageReference Include="Swashbuckle.AspNetCore.SwaggerUI" />
<PackageReference Include="System.Private.Uri" />
<PackageReference Include="SystemExtensions.NetStandard.Generators" PrivateAssets="all" />
<PackageReference Include="ZLinq" />
</ItemGroup>
+16 -16
View File
@@ -155,52 +155,52 @@ public readonly unsafe struct AgentLivingContext
[FieldOffset(0x00F6)]
public readonly AgentModelType AgentModelType;
[FieldOffset(0x0104)]
[FieldOffset(0x0108)]
public readonly TagInfo* Tags;
[FieldOffset(0x010A)]
[FieldOffset(0x010E)]
public readonly byte Primary;
[FieldOffset(0x010B)]
[FieldOffset(0x010F)]
public readonly byte Secondary;
[FieldOffset(0x010C)]
[FieldOffset(0x0110)]
public readonly byte Level;
[FieldOffset(0x010D)]
[FieldOffset(0x0111)]
public readonly byte TeamId;
[FieldOffset(0x0114)]
[FieldOffset(0x0118)]
public readonly float EnergyRegen;
[FieldOffset(0x011C)]
[FieldOffset(0x0120)]
public readonly float Energy;
[FieldOffset(0x0120)]
[FieldOffset(0x0124)]
public readonly uint MaxEnergy;
[FieldOffset(0x0128)]
[FieldOffset(0x012C)]
public readonly float HealthRegen;
[FieldOffset(0x0130)]
[FieldOffset(0x0134)]
public readonly float Health;
[FieldOffset(0x0134)]
[FieldOffset(0x0138)]
public readonly uint MaxHealth;
[FieldOffset(0x0138)]
[FieldOffset(0x013C)]
public readonly uint Effects;
[FieldOffset(0x0154)]
[FieldOffset(0x0158)]
public readonly LivingAgentModelState ModelState;
[FieldOffset(0x0158)]
[FieldOffset(0x015C)]
public readonly LivingAgentState State;
[FieldOffset(0x0180)]
[FieldOffset(0x0184)]
public readonly uint LoginNumber;
[FieldOffset(0x01B1)]
[FieldOffset(0x01B5)]
public readonly LivingAgentAllegiance Allegiance;
public readonly bool IsAlive => (!this.State.HasFlag(LivingAgentState.Dead)) && this.Health > 0f;
+9 -9
View File
@@ -19,25 +19,25 @@ public readonly struct Frame
[FieldOffset(0x0024)]
public readonly uint TemplateType;
[FieldOffset(0x00A0)]
[FieldOffset(0x00A8)]
public readonly GuildWarsArray<FrameInteractionCallback> FrameCallbacks;
[FieldOffset(0x00b0)]
[FieldOffset(0x00b8)]
public readonly uint ChildOffsetId;
[FieldOffset(0x00b4)]
[FieldOffset(0x00bc)]
public readonly uint FrameId;
[FieldOffset(0x0120)]
[FieldOffset(0x0128)]
public readonly FrameRelation Relation;
[FieldOffset(0x0184)]
public readonly uint Field91;
[FieldOffset(0x018C)]
public readonly uint FrameState;
public bool IsCreated => (this.Field91 & 0x4) != 0;
public bool IsHidden => (this.Field91 & 0x200) != 0;
public bool IsCreated => (this.FrameState & 0x4) != 0;
public bool IsHidden => (this.FrameState & 0x200) != 0;
public bool IsVisible => !this.IsHidden;
public bool IsDIsabled => (this.Field91 & 0x10) != 0;
public bool IsDIsabled => (this.FrameState & 0x10) != 0;
}
[StructLayout(LayoutKind.Explicit, Pack = 1, Size = 0x1C)]
@@ -0,0 +1,20 @@
using System.Runtime.InteropServices;
namespace Daybreak.API.Interop.GuildWars;
[StructLayout(LayoutKind.Explicit, Pack = 1, Size = 0x34)]
public unsafe struct PartySearchWindowContext
{
[FieldOffset(0x0000)]
public uint* Vtable;
[FieldOffset(0x0004)]
public uint FrameId;
[FieldOffset(0x0020)]
public uint SelectedPartySearchId;
[FieldOffset(0x0024)]
public uint SelectedHeroId;
[FieldOffset(0x0028)]
public uint SelectedHenchmanId;
[FieldOffset(0x002C)]
public uint CurrentTab;
}
+5 -5
View File
@@ -6,13 +6,13 @@ public enum UIMessage : uint
Resize = 0x8,
InitFrame = 0x9,
DestroyFrame = 0xb,
KeyDown = 0x1e, // wparam = UIPacket::kKeyAction*
KeyUp = 0x20, // wparam = UIPacket::kKeyAction*
KeyDown = 0x20, // wparam = UIPacket::kKeyAction*
KeyUp = 0x22, // wparam = UIPacket::kKeyAction*
MouseClick = 0x22, // wparam = UIPacket::kMouseClick*
MouseClick2 = 0x2e, // wparam = UIPacket::kMouseAction*
MouseClick2 = 0x31, // wparam = UIPacket::kMouseAction*
MouseAction = 0x2f, // wparam = UIPacket::kMouseAction*
SetLayout = 0x33,
FrameMessage_0x47 = 0x47, // Multiple uses depending on frame
FrameMessage_0x47 = 0x55, // Multiple uses depending on frame
UpdateAgentEffects = 0x10000000 | 0x9,
RerenderAgentModel = 0x10000000 | 0x7, // wparam = uint32_t agent_id
AgentSpeechBubble = 0x10000000 | 0x17,
@@ -94,7 +94,7 @@ public enum UIMessage : uint
PreferenceFlagChanged = 0x10000000 | 0x13F, // wparam = UiPacket::kPreferenceFlagChanged
PreferenceValueChanged = 0x10000000 | 0x140, // wparam = UiPacket::kPreferenceValueChanged
UIPositionChanged = 0x10000000 | 0x141, // wparam = UIPacket::kUIPositionChanged
QuestAdded = 0x10000000 | 0x149, // wparam = { quest_id, ... }
QuestAdded = 0x10000000 | 0x14C, // wparam = { quest_id, ... }
QuestDetailsChanged = 0x10000000 | 0x14A, // wparam = { quest_id, ... }
ClientActiveQuestChanged = 0x10000000 | 0x14C, // wparam = { quest_id, ... }. Triggered when the game requests the current quest to change
ServerActiveQuestChanged = 0x10000000 | 0x14E, // wparam = UIPacket::kServerActiveQuestChanged*. Triggered when the server requests the current quest to change
+20
View File
@@ -11,6 +11,16 @@ public static class UIPackets
Right = 0x2
}
public enum ActionState
{
MouseDown = 0x6,
MouseUp = 0x7,
MouseClick = 0x8,
MouseDoubleClick = 0x9,
DragRelease = 0xb,
KeyDown = 0xe
}
[StructLayout(LayoutKind.Sequential, Pack = 1)]
public readonly struct LogChatMessage(string message, Channel channel)
{
@@ -43,4 +53,14 @@ public static class UIPackets
public readonly uint IsDoubleClick = isDoubleClick;
public readonly uint UnknownTypeScreenPos = unknownTypeScreenPos;
}
[StructLayout(LayoutKind.Sequential, Pack = 1)]
public readonly struct MouseAction(uint frameId, uint childOffsetId, ActionState currentState, nuint wparam = 0, nuint lparam = 0)
{
public readonly uint FrameId = frameId;
public readonly uint ChildOffsetId = childOffsetId;
public readonly ActionState CurrentState = currentState;
public readonly nuint WParam = wparam;
public readonly nuint LParam = lparam;
}
}
+22 -10
View File
@@ -1,4 +1,5 @@
using Daybreak.API.Services.Interop;
using Daybreak.API.Interop.GuildWars;
using Daybreak.API.Services.Interop;
using Daybreak.Shared.Models.Api;
using System.Core.Extensions;
using System.Extensions.Core;
@@ -68,8 +69,18 @@ public sealed class CharacterSelectService(
charContext.IsPvp));
}
var currentCharacter = availableChars.FirstOrDefault(c => c.Uuid == currentUuid);
return new CharacterSelectInformation(currentCharacter, availableChars);
// TODO: Reforged sometimes returns Zero UUID even when in-game, need to investigate why
if (currentUuid != Uuid.Zero.ToString())
{
var currentCharacter = availableChars.FirstOrDefault(c => c.Uuid == currentUuid);
return new CharacterSelectInformation(currentCharacter, availableChars);
}
else
{
var currentCharacter = availableChars.FirstOrDefault();
return new CharacterSelectInformation(currentCharacter, availableChars);
}
}
}, cancellationToken);
}
@@ -272,13 +283,14 @@ public sealed class CharacterSelectService(
return false;
}
var uiState = 10U;
this.uiContextService.SendMessage(Models.UIMessage.CheckUIState, 0, (uint)&uiState);
var loginScreen = uiState == 2;
if (!loginScreen)
{
return false;
}
//TODO: Re-enable login screen check once we have a reliable way to detect it
//var uiState = 10U;
//this.uiContextService.SendMessage(Models.UIMessage.CheckUIState, 0, (uint)&uiState);
//var loginScreen = uiState == 2;
//if (!loginScreen)
//{
// return false;
//}
for (var i = 0U; i < preGameContext.Pointer->LoginCharacters.Size; i++)
{
+117
View File
@@ -0,0 +1,117 @@
namespace Daybreak.API.Services;
public sealed class HashingService
{
private const uint DefaultHash = 0x325D1EAE;
private const uint WStringHashSeed1 = 0xE2C15C9D;
private const uint WStringHashSeed2 = 0x2170A28A;
private static readonly uint[] HashTable = [
0x2EB3C21D, 0xA6F1D482, 0xBE24628F, 0x35699E30, 0xC87EE224, 0x04BA0097, 0x791F0EAF, 0xA162D466,
0x685B376A, 0xF82133BA, 0x4682D46B, 0xCB25C0C5, 0xAC3F9D22, 0xABF17202, 0xEF987728, 0x3E400B83,
0x63003108, 0xD83F1C93, 0x5058F507, 0x032E9991, 0x3518BC24, 0x1F90D44B, 0x92508DCE, 0x42DAA15C,
0x5B4105A3, 0x4D5026AD, 0x07F838B0, 0xE8C4D2A2, 0xF9EF4F54, 0x1FC1A85F, 0xB9041A40, 0x446F6969,
0xACF1EE39, 0x1828CC65, 0xC2F6C59D, 0x2456C3C0, 0x64F70142, 0x9C8ADB42, 0xC76781BF, 0x6AEA07E3,
0xE98B14BF, 0x7DBCB35E, 0x6CE78C80, 0x1E3F93DE, 0x0954FEB7, 0xB3103AEF, 0x6DAAC1EE, 0x6535E301,
0x4A221E68, 0x819CDD81, 0x5378CE3B, 0xE7348236, 0x2C3A7BCC, 0x31C50234, 0x745319DA, 0xF0897A0B,
0x5C182968, 0x48E018F4, 0x593E3BB6, 0xAB588C8D, 0xF1101F1F, 0x1D36E8BC, 0xABC7C7C9, 0xF58AA6C8,
0xF75B4B98, 0xC447B576, 0x6E5290F2, 0xD344EDF8, 0x5E1BCAF1, 0xEE892405, 0x798F7609, 0x8905CC29,
0x8977DEDC, 0x007850BC, 0x0AA3114A, 0x2F61C8F2, 0x376DFAA4, 0x0BABFC12, 0x98E64E82, 0xE6743711,
0xE26B26B6, 0xCBB10C72, 0x87446F8F, 0x96E26C0D, 0xCF140831, 0x81F69B0B, 0x5C9DF30B, 0xB276F929,
0x2712C5B6, 0x80EA90D6, 0x8EB30322, 0xCF7A705A, 0x57A17E8C, 0x556A2425, 0x15104549, 0x5F3D549F,
0x5D98FDEE, 0x9D8FA73E, 0x9206C9EF, 0xEDA0541D, 0xA3164F2D, 0x04C83543, 0x9BB27230, 0xEE97F0BA,
0xB6AEB38C, 0x41FF4DA7, 0x4023EE54, 0xECBB78BC, 0xC2C54733, 0x8850B63F, 0x93BF56EF, 0xC80177D7,
0xE68B0C13, 0x3C1E9D4C, 0x5CB3EACC, 0xF16F5795, 0xAC2B8A5C, 0x952E466E, 0x3095B0D5, 0xEC9D3A43,
0x23D8FCF7, 0x10C7A93F, 0x1B479FB5, 0x3664CF8F, 0xA514A56B, 0xE16C4886, 0x63828188, 0xA47F14C1,
0x391C237C, 0xFB15911F, 0x29825A01, 0xFC66DA40, 0xB2719F4E, 0x7B0D1611, 0x8F1EE7DE, 0xA6A1E77D,
0xDFC25D30, 0x76377036, 0x8145DB82, 0x2300A4E9, 0xA2417F88, 0x1B8C6AFC, 0x68EBDC8E, 0x48FC0C41,
0x040C3DCD, 0x17F02C44, 0xCC40B026, 0x5089EE1E, 0x2A594C28, 0x0CC198EC, 0x3F676F3D, 0xCD0AA239,
0x274A31A4, 0xC6B4D4FA, 0x9480A62F, 0xAF8862EF, 0x627CA44D, 0x572E89D3, 0x4C05136C, 0x60F36E8C,
0x7DE31FEF, 0x1B97FED3, 0xED99E2BF, 0xA034D418, 0xBBDB3E04, 0x1F198B71, 0xED45E889, 0xF7F1B0DC,
0x1592B9A5, 0x96D60F96, 0x03D616EE, 0xA5EF679D, 0x831D791D, 0x579D5FD1, 0x4B44DC48, 0x38F7A2F0,
0x2BF34A28, 0xAF14318C, 0xBD66D20A, 0x9AC72D2A, 0xC41ADB95, 0x860B2205, 0xE0E580E7, 0x3A06669C,
0x2D1A40A2, 0x577EDF60, 0x1A37693A, 0x3E576C35, 0xE3857565, 0xE46E75EE, 0x94E3625D, 0x7D1B9280,
0x2A0CCCE1, 0xBAB36163, 0x66E4E087, 0x940129BB, 0x59C197CD, 0xB0634139, 0xBB2964E1, 0x17207138,
0xD8BD3728, 0xB324876C, 0x7C72FD04, 0xBB5D9D82, 0xD9B40236, 0xC0045BF6, 0xF85100ED, 0x36493582,
0x8D0DF92A, 0xC3328368, 0xC202E412, 0x7ADE0064, 0xD01529C6, 0x4445B2AD, 0x0F6FC63B, 0xEF976FC5,
0x709EC6BE, 0x69D31EA1, 0x794BFEB4, 0xB522DB95, 0x700DE6DF, 0x49098B71, 0x38B7FA82, 0x11EDDC04,
0x5BB21D9F, 0xB14BC6D4, 0x3009D754, 0x451F5F3D, 0xFAA58575, 0xFB190B0D, 0x999DF79F, 0x6798A7AA,
0x0BC434FF, 0x429E6C15, 0xA1EA2298, 0x55A57B81, 0xD7D74B8D, 0x8FECB38B, 0x658AD588, 0xF0190227,
0xBE5D2B36, 0x62C59613, 0xC9FEDD64, 0xADD07A1E, 0x757A7679, 0xD78B6DCC, 0x56F88B2C, 0x4CC020BB,
0xA88B51FE, 0xF47F7691, 0x48587EDD, 0x3FCEC5DB, 0xA7E127AC, 0x561F6161, 0x37AB9607, 0xFE23282B,
];
private static readonly uint[] StringHashTable = [
0x92B9A528, 0x25D4FC88, 0xEDCBEFB8, 0x51063A80,
0x91341C61, 0x0261229D, 0x726F48ED, 0xCE1C088C,
0x76253EB5, 0x31E3A0DE, 0xA2AAD215, 0xCA7D6D27,
0xA5F98970, 0x0541C365, 0x3C14FF04, 0x5056AF4F
];
private static uint HashHelper(uint result, byte b)
{
return result + (HashTable[result >> 24] ^ (result >> 6) ^ HashTable[b]);
}
public uint Hash(byte[] data)
{
var hash = DefaultHash;
foreach(var b in data)
{
hash = HashHelper(hash, b);
}
return hash;
}
public uint Hash(byte data)
{
return HashHelper(DefaultHash, data);
}
public uint Hash(short data)
{
uint hash = DefaultHash;
hash = HashHelper(hash, (byte)(data >> 0));
hash = HashHelper(hash, (byte)(data >> 8));
return hash;
}
public uint Hash(uint data)
{
uint hash = DefaultHash;
hash = HashHelper(hash, (byte)(data >> 0));
hash = HashHelper(hash, (byte)(data >> 8));
hash = HashHelper(hash, (byte)(data >> 16));
hash = HashHelper(hash, (byte)(data >> 24));
return hash;
}
public uint Hash(string data, int maxLength = -1)
{
if (maxLength is -1)
{
maxLength = data.Length;
}
uint seed1 = WStringHashSeed1;
uint seed2 = WStringHashSeed2;
uint hash = DefaultHash;
for(var i = 0; i < maxLength && i < data.Length; i++)
{
var c = data[i];
var uc = (uint)c;
if (c >= 'a' && c <= 'z')
{
uc -= 0x20;
}
seed1 = (seed1 << 3) ^ uc;
seed2 += StringHashTable[seed1 & 0xff];
hash ^= seed2 + seed1;
}
return hash;
}
}
@@ -12,9 +12,9 @@ public unsafe sealed class AgentContextService : IAddressHealthService
private const int AgentArrayOffset = -0x4;
private static readonly byte[] AgentArrayAddressPattern = [0x8b, 0x0c, 0x90, 0x85, 0xc9, 0x74, 0x19];
private const string PlayerAgentIdMask = "xx????xxxx";
private const string PlayerAgentIdMask = "xx????xxxxxxx";
private const int PlayerAgentIdOffset = -0xE;
private static readonly byte[] PlayerAgentIdAddressPattern = [0x5d, 0xe9, 0x00, 0x00, 0x00, 0x00, 0x55, 0x8b, 0xec, 0x53];
private static readonly byte[] PlayerAgentIdAddressPattern = [0x5d, 0xe9, 0x00, 0x00, 0x00, 0x00, 0x55, 0x8b, 0xec, 0x56, 0x57, 0x8b, 0x7d];
private readonly MemoryScanningService memoryScanningService;
private readonly GWAddressCache agentArrayAddress;
@@ -3,14 +3,23 @@ using Daybreak.API.Interop.GuildWars;
using Daybreak.API.Models;
using System.Core.Extensions;
using System.Extensions.Core;
using System.Runtime.InteropServices;
namespace Daybreak.API.Services.Interop;
public unsafe sealed class GameContextService : IAddressHealthService
{
private const string BaseAddressMask = "xxxxxxx";
private const int BaseAddressOffset = +7;
private static readonly byte[] BaseAddressPattern = [0x50, 0x6A, 0x0F, 0x6A, 0x00, 0xFF, 0x35];
// TLS-based pattern to find GameContext getter function
private const string GameContextTlsMask = "xx????xxxxxxxxxxx????x";
private const int GameContextTlsOffset = 0;
private static readonly byte[] GameContextTlsPattern =
[
0x8B, 0x0D, 0x00, 0x00, 0x00, 0x00, // mov ecx, [TlsIndexPtr] - 6 bytes: xx????
0x64, 0xA1, 0x2C, 0x00, 0x00, 0x00, // mov eax, fs:[0000002C] - 6 bytes: xxxxxx
0x8B, 0x04, 0x88, // mov eax, [eax+ecx*4] - 3 bytes: xxx
0x8B, 0x80, 0x04, 0x00, 0x00, 0x00, // mov eax, [eax+00000004] - 6 bytes: xx????
0xC3 // ret - 1 byte: x
]; // Total: 22 bytes, mask: 22 chars
private const string PreGameContextFile = "UiPregame.cpp";
private const string PreGameContextAssertion = "!s_scene";
@@ -21,7 +30,7 @@ public unsafe sealed class GameContextService : IAddressHealthService
private static readonly byte[] AvailableCharsPattern = [0x8B, 0x35, 0x00, 0x00, 0x00, 0x00, 0x57, 0x69, 0xF8, 0x84, 0x00, 0x00, 0x00];
private readonly MemoryScanningService memoryScanningService;
private readonly GWAddressCache baseAddress;
private readonly GWAddressCache gameContextGetterAddress;
private readonly GWAddressCache preGameContextAddress;
private readonly GWAddressCache availableCharsAddress;
private readonly ILogger<GameContextService> logger;
@@ -32,18 +41,21 @@ public unsafe sealed class GameContextService : IAddressHealthService
{
this.logger = logger.ThrowIfNull();
this.memoryScanningService = memoryScanningService.ThrowIfNull();
this.baseAddress = new GWAddressCache(this.GetBaseAddress);
this.gameContextGetterAddress = new GWAddressCache(this.GetGameContextGetterAddress);
this.preGameContextAddress = new GWAddressCache(this.GetPreGameContextAddress);
this.availableCharsAddress = new GWAddressCache(this.GetAvailableCharactersAddress);
}
[UnmanagedFunctionPointer(CallingConvention.Cdecl)]
private delegate nuint GetGameContextDelegate();
public List<AddressState> GetAddressStates()
{
return [
new AddressState
{
Address = this.baseAddress.GetAddress() ?? 0,
Name = nameof(this.baseAddress),
Address = this.gameContextGetterAddress.GetAddress() ?? 0,
Name = nameof(this.gameContextGetterAddress),
},
new AddressState
{
@@ -113,41 +125,46 @@ public unsafe sealed class GameContextService : IAddressHealthService
private nuint GetGameContextAddress()
{
var scopedLogger = this.logger.CreateScopedLogger();
if (this.baseAddress.GetAddress() is not nuint basePtr)
// Get the address of the GameContext getter function
var getterAddress = this.gameContextGetterAddress.GetAddress();
if (getterAddress is null or 0)
{
scopedLogger.LogError("Failed to get base address");
return default;
scopedLogger.LogError("Failed to get GameContext getter function address");
return 0;
}
nuint** baseContext = *(nuint***)basePtr;
if (baseContext is null)
// Call the function to get GameContext
var getter = Marshal.GetDelegateForFunctionPointer<GetGameContextDelegate>((nint)getterAddress);
var gameContextPtr = getter();
if (gameContextPtr == 0)
{
scopedLogger.LogError("Failed to get base context address");
return default;
scopedLogger.LogError("GameContext getter returned null");
return 0;
}
var globalContextPtr = baseContext[0x6];
if (globalContextPtr is null)
{
scopedLogger.LogError("Failed to get game context address");
return default;
}
return (nuint)globalContextPtr;
scopedLogger.LogInformation("GameContext address: 0x{address:X8}", gameContextPtr);
return gameContextPtr;
}
private nuint GetBaseAddress()
private nuint GetGameContextGetterAddress()
{
var scopedLogger = this.logger.CreateScopedLogger();
var baseAddress = this.memoryScanningService.FindAndResolveAddress(BaseAddressPattern, BaseAddressMask, BaseAddressOffset);
if (baseAddress is 0)
var address = this.memoryScanningService.FindAddress(
GameContextTlsPattern,
GameContextTlsMask,
0);
if (address is 0)
{
scopedLogger.LogError("Failed to find base address");
scopedLogger.LogError("Failed to find GameContext getter function");
return 0U;
}
scopedLogger.LogInformation("Base address: 0x{address:X8}", baseAddress);
return baseAddress;
scopedLogger.LogInformation("GameContext getter function: 0x{address:X8}", address);
return address;
}
private unsafe nuint GetPreGameContextAddress()
@@ -16,6 +16,8 @@ public sealed unsafe class MemoryScanningService(
private readonly (nuint BaseAddress, ImageSectionHeader Section) textSection = GetSectionHeader(".text");
private readonly (nuint BaseAddress, ImageSectionHeader Section) dataSection = GetSectionHeader(".rdata");
public uint GameTlsIndex { get; } = GetGameTlsIndex();
public nuint FunctionFromNearCall(nuint callInstructionAddress, bool checkValidPtr = true)
{
var scopedLogger = this.logger.CreateScopedLogger();
@@ -118,7 +120,7 @@ public sealed unsafe class MemoryScanningService(
fileSearchPos = filePtr + 1;
// If what we found is …/file.hpp (no drive letter) fix it
// If what we found is "…/file.hpp" (no drive letter) fix it
if (Marshal.ReadByte((nint)(filePtr + 1)) != (byte)':')
{
// look ≤128 bytes backward for ':' and back-up one char
@@ -199,6 +201,12 @@ public sealed unsafe class MemoryScanningService(
return address;
}
public nuint FindAddress(byte[] pattern, string mask, int offset, nuint startAddress, nuint endAddress)
{
var address = FindAddressInternal(startAddress, endAddress, pattern, mask, offset);
return address;
}
public nuint ToFunctionStart(nuint callInstructionAddress, uint scanRange = 0x500)
{
if (callInstructionAddress == 0)
@@ -216,6 +224,92 @@ public sealed unsafe class MemoryScanningService(
return FindInRange(prologue, mask, 0, start, end);
}
public nuint FindNthUseOfString(string str, uint nth, int offset = 0, bool useTextSection = true)
{
var scopedLogger = this.logger.CreateScopedLogger();
var strBytes = Encoding.ASCII.GetBytes(str);
var mask = new string('x', strBytes.Length);
var foundStr = FindAddressInternal(this.dataSection, strBytes, mask, 0);
if (foundStr is 0)
{
scopedLogger.LogError("Failed to find string in .rdata section");
return 0;
}
var firstNullChar = FindInRange([0x0], "x", 1, foundStr, foundStr - 0x64);
if (firstNullChar is 0)
{
scopedLogger.LogError("Failed to find null terminator");
return 0;
}
return this.FindNthUseOfAddress(firstNullChar, nth, offset, useTextSection);
}
public nuint FindUseOfString(string str, int offset = 0, bool useTextSection = true)
{
return this.FindNthUseOfString(str, 0, offset, useTextSection);
}
public nuint FindNthUseOfWideString(string str, uint nth, int offset = 0, bool useTextSection = true)
{
var scopedLogger = this.logger.CreateScopedLogger();
var strBytes = Encoding.Unicode.GetBytes(str);
var mask = new string('x', strBytes.Length);
var foundStr = FindAddressInternal(this.dataSection, strBytes, mask, 0);
if (foundStr is 0)
{
scopedLogger.LogError("Failed to find wide string in .rdata section");
return 0;
}
var firstNullChar = FindInRange([0x0, 0x0], "xx", 2, foundStr, foundStr - 0x128);
if (firstNullChar is 0)
{
scopedLogger.LogError("Failed to find null terminator for wide string");
return 0;
}
return this.FindNthUseOfAddress(firstNullChar, nth, offset, useTextSection);
}
public nuint FindUseOfWideString(string str, int offset = 0, bool useTextSection = true)
{
return this.FindNthUseOfWideString(str, 0, offset, useTextSection);
}
private nuint FindNthUseOfAddress(nuint address, uint nth, int offset, bool useTextSection)
{
var (BaseAddress, Section) = useTextSection ? this.textSection : this.dataSection;
var addressBytes = BitConverter.GetBytes((uint)address);
var mask = "xxxx";
var currentAddress = BaseAddress + Section.VirtualAddress;
var endAddress = currentAddress + Section.VirtualSize;
uint foundCount = 0;
while (currentAddress < endAddress)
{
var found = FindAddressInternal(currentAddress, endAddress, addressBytes, mask, 0);
if (found is 0)
{
break;
}
if (foundCount == nth)
{
return (nuint)((nint)found + offset);
}
foundCount++;
currentAddress = found + 1;
}
return 0;
}
private static bool IsValidPtr(nuint address, (nuint BaseAddress, ImageSectionHeader Section) section)
{
return address > 0 &&
@@ -276,8 +370,12 @@ public sealed unsafe class MemoryScanningService(
{
var textStart = section.BaseAddress + section.Section.VirtualAddress;
var textEnd = textStart + section.Section.VirtualSize - (uint)pattern.Length;
return FindAddressInternal(textStart, textEnd, pattern, mask, offset);
}
for (var cur = textStart; cur <= textEnd; ++cur)
private static nuint FindAddressInternal(nuint startAddr, nuint endAddr, byte[] pattern, string mask, int offset = 0)
{
for (var cur = startAddr; cur <= endAddr; ++cur)
{
if (Marshal.ReadByte((nint)cur) != pattern[0])
{
@@ -312,4 +410,30 @@ public sealed unsafe class MemoryScanningService(
? throw new InvalidOperationException($"Failed to initialize memory scanner. Failed to find {headerName} section")
: ((nuint, ImageSectionHeader))(baseAddr, textHdr);
}
private static uint GetGameTlsIndex()
{
var m = Process.GetCurrentProcess().MainModule ?? throw new InvalidOperationException("Failed to get TLS index. Failed to find main module");
var baseAddr = (nint)m.BaseAddress;
var pe = new PeFile(m.FileName);
// Get the TLS data directory entry (index 9 = IMAGE_DIRECTORY_ENTRY_TLS)
var tlsDirectory = pe.ImageNtHeaders?.OptionalHeader.DataDirectory[9];
if (tlsDirectory is null || tlsDirectory.VirtualAddress == 0)
{
throw new InvalidOperationException("Failed to get TLS index. No TLS directory found");
}
// Calculate the address of the TLS directory in memory
var tlsDirectoryAddress = baseAddr + (nint)tlsDirectory.VirtualAddress;
// Read the IMAGE_TLS_DIRECTORY structure
// AddressOfIndex is at offset 8 (after StartAddressOfRawData and EndAddressOfRawData)
var addressOfIndexPtr = Marshal.ReadIntPtr(tlsDirectoryAddress + 8);
// Read the TLS index from the location pointed to by AddressOfIndex
var tlsIndex = (uint)Marshal.ReadInt32(addressOfIndexPtr);
return tlsIndex;
}
}
@@ -2,6 +2,7 @@
using Daybreak.API.Models;
using System.Core.Extensions;
using System.Extensions;
using static Daybreak.API.Models.UIPackets;
namespace Daybreak.API.Services.Interop;
@@ -10,13 +11,12 @@ public sealed class PartyContextService
{
private const string SearchButtonFile = "\\Code\\Gw\\Ui\\Game\\Party\\PtSearch.cpp";
private const string SearchButtonAssertion = "m_activeList == LIST_HEROES";
private const string WindowButtonFile = "\\Code\\Gw\\Ui\\Game\\Party\\PtButtons.cpp";
private const string WindowButtonAssertion = "m_selection.agentId";
private const string WindowButtonAssertion = "selection.agentId";
// Fastcall funcs do not work in .NET for x86 right now. https://github.com/dotnet/runtime/issues/113851
// This fastcall equivalent is <void*, uint, uint*, void>
// This fastcall equivalent is <void*, uint, UIPacket::kMouseAction*, void>
private readonly GWFastCall<nint, uint, nint, GWFastCall.Void> searchButtonCallback;
// This fastcall equivalent is <void*, uint, uint*, void>
// This fastcall equivalent is <void*, uint, UIPacket::kMouseAction*, void>
private readonly GWFastCall<nint, uint, nint, GWFastCall.Void> windowButtonCallback;
private readonly MemoryScanningService memoryScanningService;
@@ -29,7 +29,7 @@ public sealed class PartyContextService
this.memoryScanningService = memoryScanningService.ThrowIfNull();
this.logger = logger.ThrowIfNull();
this.searchButtonCallback = new(new GWAddressCache(() => this.memoryScanningService.ToFunctionStart(this.memoryScanningService.FindAssertion(SearchButtonFile, SearchButtonAssertion, 0, 0))));
this.windowButtonCallback = new(new GWAddressCache(() => this.memoryScanningService.ToFunctionStart(this.memoryScanningService.FindAssertion(WindowButtonFile, WindowButtonAssertion, 0, 0))));
this.windowButtonCallback = new(new GWAddressCache(() => this.memoryScanningService.ToFunctionStart(this.memoryScanningService.FindUseOfString(WindowButtonAssertion))));
}
public List<AddressState> GetAddressStates()
@@ -52,13 +52,13 @@ public sealed class PartyContextService
}
}
public unsafe bool CallSearchButtonCallback(void* ctx, uint edx, uint* wparam)
public unsafe bool CallSearchButtonCallback(void* ctx, uint edx, MouseAction* wparam)
{
this.searchButtonCallback.Invoke((nint)ctx, edx, (nint)wparam);
return true;
}
public unsafe bool CallWindowButtonCallback(void* ctx, uint edx, uint* wparam)
public unsafe bool CallWindowButtonCallback(void* ctx, uint edx, MouseAction* wparam)
{
this.windowButtonCallback.Invoke((nint)ctx, edx, (nint)wparam);
return true;
@@ -66,26 +66,28 @@ public sealed class PartyContextService
public unsafe bool AddHero(uint heroId)
{
var wparam = stackalloc uint[4];
wparam[2] = 0x6;
wparam[1] = 0x1;
var action = new MouseAction(
frameId: 0,
childOffsetId: 0x1,
currentState: ActionState.MouseUp);
var ctx = stackalloc uint[13];
ctx[0xb] = 1;
ctx[9] = heroId;
return this.CallSearchButtonCallback(ctx, 2, wparam);
ctx[0x9] = heroId;
return this.CallSearchButtonCallback(ctx, 2, &action);
}
public unsafe bool KickHero(uint heroId)
{
var wparam = stackalloc uint[4];
wparam[2] = 0x6;
wparam[1] = 0x6;
var action = new MouseAction(
frameId: 0,
childOffsetId: 0x6,
currentState: ActionState.MouseUp);
var ctx = stackalloc uint[13];
ctx[0xb] = 1;
ctx[9] = heroId;
return this.CallSearchButtonCallback(ctx, 0, wparam);
ctx[0x9] = heroId;
return this.CallSearchButtonCallback(ctx, 0, &action);
}
public bool KickAllHeroes() => this.KickHero(0x26);
@@ -94,6 +96,6 @@ public sealed class PartyContextService
{
var ctx = stackalloc uint[14];
ctx[0xd] = 1;
return this.CallWindowButtonCallback(ctx, 0, (uint*)0);
return this.CallWindowButtonCallback(ctx, 0, (MouseAction*)0);
}
}
@@ -1,6 +1,7 @@
using Daybreak.API.Interop;
using Daybreak.API.Interop.GuildWars;
using Daybreak.API.Models;
using Microsoft.AspNetCore.Mvc.ModelBinding.Binders;
using System.Core.Extensions;
using System.Extensions.Core;
using System.Runtime.InteropServices;
@@ -13,28 +14,30 @@ public sealed class UIContextService
{
private const string GetChildFrameIdFile = "CtlView.cpp";
private const string GetChildFrameIdAssertion = "pageId";
private const int GetChildFrameIdOffset = 0x19;
private const string SetFrameFlagFile = "FrApi.cpp";
private const string SetFrameFlagAssertion = "frameId";
private const int SetFrameVisibleOffset = 0x4f9;
private const int SetFrameDisabledOffset = 0x4e3;
private static readonly byte[] SendFrameUiMessageByIdSeq = [0x83, 0xFB, 0x47, 0x73, 0x14];
private const string SendFrameUiMessageByIdMask = "xxxxx";
private const int SendFrameUiMessageByIdOffset = -0x34;
private const int GetChildFrameIdOffset = 0x12;
private static readonly byte[] GetChildFrameIdPattern = [0xE8];
private const string GetChildFrameIdMask = "x";
private static readonly byte[] SendFrameUiMessageSeq = [
0x83, 0xC1, 0xDC, // add ecx, -24
0xE8, 0x00, 0x00, 0x00, 0x00, // call
0x8B, 0x4D, 0xFC // mov ecx, [ebp-04]
];
private const string SendFrameUiMessageMask = "xxxx????xxx";
private const int SendFrameUiMessageOffset = 0x3;
private static readonly byte[] SendUIMessageSeq = [0xB9, 0x00, 0x00, 0x00, 0x00, 0xE8, 0x00, 0x00, 0x00, 0x00, 0x5D, 0xC3, 0x89, 0x45, 0x08];
private const string SendUIMessageMask = "x????x????xxxxx";
private const string CreateHashFromStringMask = "xxxxxxx";
private const int CreateHashFromStringOffset = 0x7;
private static readonly byte[] CreateHashFromStringSeq = [0x85, 0xC0, 0x74, 0x0D, 0x6A, 0xFF, 0x50];
private const string FrameArrayFile = "\\Code\\Engine\\Frame\\FrMsg.cpp";
private const string FrameArrayAssertion = "frame";
private static readonly byte[] GetRootFrameSeq = [0x05, 0xE0, 0xFE, 0xFF, 0xFF, 0xC3];
private const string GetRootFrameMask = "xxxxxx";
private const int GetRootFrameOffset = -0x3C;
[SuppressUnmanagedCodeSecurity]
[UnmanagedFunctionPointer(CallingConvention.Cdecl)]
private unsafe delegate uint CreateHashFromStringFunc(char* value, int seed);
private static readonly byte[] GetRootFrameSeq = [
0xA1, 0x00, 0x00, 0x00, 0x00, // mov eax,[address]
0x05, 0xD8, 0xFE, 0xFF, 0xFF, // add eax,FFFFFED8
0xC3 // ret
];
private const string GetRootFrameMask = "x????xxxxxx";
[SuppressUnmanagedCodeSecurity]
[UnmanagedFunctionPointer(CallingConvention.Cdecl)]
@@ -54,7 +57,7 @@ public sealed class UIContextService
private readonly SemaphoreSlim semaphoreSlim = new(1);
private readonly MemoryScanningService memoryScanningService;
private readonly GWDelegateCache<CreateHashFromStringFunc> createHashFromString;
private readonly HashingService hashingService;
private readonly GWDelegateCache<GetChildFrameIdFunc> getChildFrameId;
private readonly GWAddressCache frameArrayAddressCache;
private readonly GWHook<SendUIMessageFunc> sendUiMessageHook;
@@ -66,13 +69,13 @@ public sealed class UIContextService
private CancellationTokenSource? cts = default;
public UIContextService(
HashingService hashingService,
MemoryScanningService memoryScanningService,
ILogger<UIContextService> logger)
{
this.logger = logger.ThrowIfNull();
this.memoryScanningService = memoryScanningService.ThrowIfNull();
this.createHashFromString = new GWDelegateCache<CreateHashFromStringFunc>(new GWAddressCache(() => this.memoryScanningService.FunctionFromNearCall(
this.memoryScanningService.FindAddress(CreateHashFromStringSeq, CreateHashFromStringMask, CreateHashFromStringOffset))));
this.hashingService = hashingService.ThrowIfNull();
this.frameArrayAddressCache = new GWAddressCache(() => this.memoryScanningService.FindAssertion(FrameArrayFile, FrameArrayAssertion, 0, -0x14));
this.sendUiMessageHook = new GWHook<SendUIMessageFunc>(
new GWAddressCache(() => this.memoryScanningService.ToFunctionStart(
@@ -80,23 +83,24 @@ public sealed class UIContextService
this.OnSendUIMessage);
this.sendFrameUIMessage = new(
new GWAddressCache(() => this.memoryScanningService.FunctionFromNearCall(
this.memoryScanningService.FindAddress(SendFrameUiMessageByIdSeq, SendFrameUiMessageByIdMask, SendFrameUiMessageByIdOffset) + 0x67)));
this.memoryScanningService.FindAddress(SendFrameUiMessageSeq, SendFrameUiMessageMask, SendFrameUiMessageOffset))));
this.getChildFrameId = new GWDelegateCache<GetChildFrameIdFunc>(
new GWAddressCache(() => this.memoryScanningService.FunctionFromNearCall(
this.memoryScanningService.FindAssertion(GetChildFrameIdFile, GetChildFrameIdAssertion, 0, GetChildFrameIdOffset))));
new GWAddressCache(() =>
{
var address = this.memoryScanningService.FindAssertion(GetChildFrameIdFile, GetChildFrameIdAssertion, 0, GetChildFrameIdOffset);
return this.memoryScanningService.FunctionFromNearCall(
this.memoryScanningService.FindAddress(GetChildFrameIdPattern, GetChildFrameIdMask, 0, address, address + 0xFF));
}));
this.getRootFrame = new GWDelegateCache<GetRootFrameFunc>(
new GWAddressCache(() => this.memoryScanningService.FindAddress(GetRootFrameSeq, GetRootFrameMask, GetRootFrameOffset)));
new GWAddressCache(() => this.memoryScanningService.FindAddress(GetRootFrameSeq, GetRootFrameMask, 0)));
}
public List<AddressState> GetAddressStates()
{
var assertion = this.memoryScanningService.FindAssertion(GetChildFrameIdFile, GetChildFrameIdAssertion, 0, GetChildFrameIdOffset);
Console.WriteLine(assertion);
return
[
new()
{
Name = nameof(this.createHashFromString),
Address = this.createHashFromString.Cache.GetAddress() ?? 0U
},
new()
{
Name = nameof(this.frameArrayAddressCache),
@@ -104,13 +108,13 @@ public sealed class UIContextService
},
new()
{
Name = nameof(this.getChildFrameId),
Address = this.getChildFrameId.Cache.GetAddress() ?? 0U
Name = nameof(this.sendFrameUIMessage),
Address = this.sendFrameUIMessage.Cache.GetAddress() ?? 0U
},
new()
{
Name = nameof(this.sendFrameUIMessage),
Address = this.sendFrameUIMessage.Cache.GetAddress() ?? 0U
Name = nameof(this.getChildFrameId),
Address = this.getChildFrameId.Cache.GetAddress() ?? 0U
},
new()
{
@@ -268,7 +272,7 @@ public sealed class UIContextService
return null;
}
var hash = this.CreateHashFromString(label, -1);
var hash = this.CreateHashFromString(label);
if (hash is 0)
{
scopedLogger.LogError("Failed to get hash for label {label}", label);
@@ -315,19 +319,9 @@ public sealed class UIContextService
return frame;
}
public unsafe uint CreateHashFromString(string value, int seed)
public uint CreateHashFromString(string value)
{
var scopedLogger = this.logger.CreateScopedLogger();
if (this.createHashFromString.GetDelegate() is not CreateHashFromStringFunc del)
{
scopedLogger.LogError("Failed to get CreateHashFromString delegate");
return 0;
}
fixed (char* valuePtr = value)
{
return del(valuePtr, seed);
}
return this.hashingService.Hash(value);
}
public unsafe void SendMessage(UIMessage message, nuint wParam, nuint lParam)
@@ -357,24 +351,24 @@ public sealed class UIContextService
private static unsafe void SetFrameVisibleInternal(WrappedPointer<Frame> frame, bool visible)
{
uint* pState = &frame.Pointer->Field91; // direct field address
uint* pState = &frame.Pointer->FrameState; // direct field address
if (visible)
{
*pState &= ~0x200u; // clear hidden
*pState &= ~0x200u; // clear "hidden"
}
else
{
*pState |= 0x200u; // set hidden
*pState |= 0x200u; // set "hidden"
}
}
private static unsafe void SetFrameDisabledInternal(WrappedPointer<Frame> frame, bool disabled)
{
uint* pState = &frame.Pointer->Field91; // same word (offset 0x184)
uint* pState = &frame.Pointer->FrameState; // same word (offset 0x184)
if (disabled)
{
*pState |= 0x10u; // set disabled
*pState |= 0x10u; // set "disabled"
}
else
{
+13 -11
View File
@@ -9,6 +9,7 @@ using Daybreak.Shared.Services.BuildTemplates;
using System.Core.Extensions;
using System.Extensions;
using System.Extensions.Core;
using System.Windows.Controls;
using ZLinq;
using InstanceType = Daybreak.API.Interop.GuildWars.InstanceType;
@@ -47,7 +48,7 @@ public sealed class PartyService(
return false;
}
if (!await this.LeaveParty(cancellationToken))
if (!await this.KickAllHeroes(cancellationToken))
{
scopedLogger.LogError("Could not set party loadout. Could not leave party");
return false;
@@ -67,14 +68,15 @@ public sealed class PartyService(
return false;
}
var heroBehaviorSetup = await this.gameThreadService.QueueOnGameThread(() => this.GetHeroBehaviorSetup(partyLoadout), cancellationToken);
foreach(var heroBehaviorEntry in heroBehaviorSetup ?? [])
{
if (!await this.SetHeroBehavior(heroBehaviorEntry.AgentId, heroBehaviorEntry.Behavior, cancellationToken))
{
scopedLogger.LogWarning("Could not set hero behavior for agent {agentId} to {behavior}", heroBehaviorEntry.AgentId, heroBehaviorEntry.Behavior);
}
}
// TODO: Hashing is broken
//var heroBehaviorSetup = await this.gameThreadService.QueueOnGameThread(() => this.GetHeroBehaviorSetup(partyLoadout), cancellationToken);
//foreach (var heroBehaviorEntry in heroBehaviorSetup ?? [])
//{
// if (!await this.SetHeroBehavior(heroBehaviorEntry.AgentId, heroBehaviorEntry.Behavior, cancellationToken))
// {
// scopedLogger.LogWarning("Could not set hero behavior for agent {agentId} to {behavior}", heroBehaviorEntry.AgentId, heroBehaviorEntry.Behavior);
// }
//}
return true;
}
@@ -143,7 +145,7 @@ public sealed class PartyService(
}, cancellationToken);
}
public async Task<bool> LeaveParty(CancellationToken cancellationToken)
public async Task<bool> KickAllHeroes(CancellationToken cancellationToken)
{
var partySize = await this.GetPartySize(cancellationToken);
if (partySize is 1 or 0)
@@ -151,7 +153,7 @@ public sealed class PartyService(
return true;
}
return await this.gameThreadService.QueueOnGameThread(this.partyContextService.LeaveParty, cancellationToken);
return await this.gameThreadService.QueueOnGameThread(this.partyContextService.KickAllHeroes, cancellationToken);
}
public async Task<uint> GetPartySize(CancellationToken cancellationToken)
@@ -9,6 +9,7 @@ public static class WebApplicationBuilderExtensions
{
public static WebApplicationBuilder WithDaybreakServices(this WebApplicationBuilder builder)
{
builder.Services.AddSingleton<HashingService>();
builder.Services.AddSingleton<IBuildTemplateManager, BuildTemplateManager>();
builder.Services.AddSingleton<IMDomainNameService, MDomainNameService>();
builder.Services.AddHostedService<ApiAdvertisingService>();