Daybreak 0.9.8.18 (#179)

* Minimap implementation

* Fix minimap

* Add debounce logic to entities positions

* Cleanup

* Fix x64 build memory reading
Move build to x64
Fix entity debouncing
Added some backstops to prevent reading corrupted memory

* Add minimap zooming and panning functionality
Add main window position saving procedure

* Increment version

---------

Co-authored-by: Alex Macocian <amacocian@hotmail.com>
This commit is contained in:
2023-03-20 21:38:53 +02:00
committed by GitHub
parent 6ea77f7e05
commit f2cc59a4ab
63 changed files with 1736 additions and 273 deletions
+1 -1
View File
@@ -19,7 +19,7 @@ jobs:
strategy:
matrix:
targetplatform: [x86]
targetplatform: [x64]
runs-on: windows-latest
+1 -1
View File
@@ -19,7 +19,7 @@ jobs:
strategy:
matrix:
targetplatform: [x86]
targetplatform: [x64]
runs-on: windows-latest
+1 -1
View File
@@ -5,7 +5,7 @@
<TargetFramework>net6.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<Platforms>AnyCPU;x86</Platforms>
<Platforms>AnyCPU;x86;x64</Platforms>
</PropertyGroup>
</Project>
+1 -1
View File
@@ -3,7 +3,7 @@
<PropertyGroup>
<TargetFramework>net6.0-windows</TargetFramework>
<IsPackable>false</IsPackable>
<Platforms>AnyCPU;x86</Platforms>
<Platforms>AnyCPU;x86;x64</Platforms>
</PropertyGroup>
<ItemGroup>
+14
View File
@@ -34,33 +34,47 @@ EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Debug|x64 = Debug|x64
Debug|x86 = Debug|x86
Release|Any CPU = Release|Any CPU
Release|x64 = Release|x64
Release|x86 = Release|x86
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
{AA45C2B1-8BD0-466C-9271-699F168905AF}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{AA45C2B1-8BD0-466C-9271-699F168905AF}.Debug|Any CPU.Build.0 = Debug|Any CPU
{AA45C2B1-8BD0-466C-9271-699F168905AF}.Debug|x64.ActiveCfg = Debug|x64
{AA45C2B1-8BD0-466C-9271-699F168905AF}.Debug|x64.Build.0 = Debug|x64
{AA45C2B1-8BD0-466C-9271-699F168905AF}.Debug|x86.ActiveCfg = Debug|x86
{AA45C2B1-8BD0-466C-9271-699F168905AF}.Debug|x86.Build.0 = Debug|x86
{AA45C2B1-8BD0-466C-9271-699F168905AF}.Release|Any CPU.ActiveCfg = Release|Any CPU
{AA45C2B1-8BD0-466C-9271-699F168905AF}.Release|Any CPU.Build.0 = Release|Any CPU
{AA45C2B1-8BD0-466C-9271-699F168905AF}.Release|x64.ActiveCfg = Release|x64
{AA45C2B1-8BD0-466C-9271-699F168905AF}.Release|x64.Build.0 = Release|x64
{AA45C2B1-8BD0-466C-9271-699F168905AF}.Release|x86.ActiveCfg = Release|x86
{AA45C2B1-8BD0-466C-9271-699F168905AF}.Release|x86.Build.0 = Release|x86
{C911C1C2-6566-419C-89C7-F802EB3FE709}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{C911C1C2-6566-419C-89C7-F802EB3FE709}.Debug|Any CPU.Build.0 = Debug|Any CPU
{C911C1C2-6566-419C-89C7-F802EB3FE709}.Debug|x64.ActiveCfg = Debug|x64
{C911C1C2-6566-419C-89C7-F802EB3FE709}.Debug|x64.Build.0 = Debug|x64
{C911C1C2-6566-419C-89C7-F802EB3FE709}.Debug|x86.ActiveCfg = Debug|x86
{C911C1C2-6566-419C-89C7-F802EB3FE709}.Debug|x86.Build.0 = Debug|x86
{C911C1C2-6566-419C-89C7-F802EB3FE709}.Release|Any CPU.ActiveCfg = Release|Any CPU
{C911C1C2-6566-419C-89C7-F802EB3FE709}.Release|Any CPU.Build.0 = Release|Any CPU
{C911C1C2-6566-419C-89C7-F802EB3FE709}.Release|x64.ActiveCfg = Release|x64
{C911C1C2-6566-419C-89C7-F802EB3FE709}.Release|x64.Build.0 = Release|x64
{C911C1C2-6566-419C-89C7-F802EB3FE709}.Release|x86.ActiveCfg = Release|x86
{C911C1C2-6566-419C-89C7-F802EB3FE709}.Release|x86.Build.0 = Release|x86
{4E2BB805-135D-4F02-8C53-3D8B6876D323}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{4E2BB805-135D-4F02-8C53-3D8B6876D323}.Debug|Any CPU.Build.0 = Debug|Any CPU
{4E2BB805-135D-4F02-8C53-3D8B6876D323}.Debug|x64.ActiveCfg = Debug|x64
{4E2BB805-135D-4F02-8C53-3D8B6876D323}.Debug|x64.Build.0 = Debug|x64
{4E2BB805-135D-4F02-8C53-3D8B6876D323}.Debug|x86.ActiveCfg = Debug|x86
{4E2BB805-135D-4F02-8C53-3D8B6876D323}.Debug|x86.Build.0 = Debug|x86
{4E2BB805-135D-4F02-8C53-3D8B6876D323}.Release|Any CPU.ActiveCfg = Release|Any CPU
{4E2BB805-135D-4F02-8C53-3D8B6876D323}.Release|Any CPU.Build.0 = Release|Any CPU
{4E2BB805-135D-4F02-8C53-3D8B6876D323}.Release|x64.ActiveCfg = Release|x64
{4E2BB805-135D-4F02-8C53-3D8B6876D323}.Release|x64.Build.0 = Release|x64
{4E2BB805-135D-4F02-8C53-3D8B6876D323}.Release|x86.ActiveCfg = Release|x86
{4E2BB805-135D-4F02-8C53-3D8B6876D323}.Release|x86.Build.0 = Release|x86
EndGlobalSection
@@ -44,4 +44,6 @@ public sealed class ApplicationConfiguration
public string? ProtectedGraphRefreshToken { get; set; }
[JsonProperty("FocusViewOptions")]
public FocusViewOptions FocusViewOptions { get; set; } = new FocusViewOptions();
[JsonProperty("ScreenManagerOptions")]
public ScreenManagerOptions ScreenManagerOptions { get; set; } = new ScreenManagerOptions();
}
@@ -138,6 +138,7 @@ public static class ProjectConfiguration
services.AddScoped<IExperienceCalculator, ExperienceCalculator>();
services.AddScoped<IAttributePointCalculator, AttributePointCalculator>();
services.AddScoped<IDownloadService, DownloadService>();
services.AddScoped<IGuildwarsEntityDebouncer, GuildwarsEntityDebouncer>();
}
public static void RegisterViews(IViewProducer viewProducer)
@@ -0,0 +1,9 @@
namespace Daybreak.Configuration;
public sealed class ScreenManagerOptions
{
public double X { get; set; }
public double Y { get; set; }
public double Width { get; set; }
public double Height { get; set; }
}
+28
View File
@@ -0,0 +1,28 @@
<UserControl x:Class="Daybreak.Controls.Glyphs.MapGlyph"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:local="clr-namespace:Daybreak.Controls.Glyphs"
mc:Ignorable="d"
Name="_this"
d:DesignHeight="450" d:DesignWidth="800">
<Viewbox>
<Grid>
<Path Stroke="{Binding ElementName=_this, Path=Foreground, Mode=OneWay}"
Data="m18.53,14.53a9.37,9.37 0 0 1 -9.26,9.47l0,0a9.5,9.5 0 0 1 -3.26,-18.33a9.61,9.61 0 0 0 0.77,1.82a14.22,14.22 0 0 0 2.51,3.25a14.06,14.06 0 0 0 2.49,-3.24a9.2,9.2 0 0 0 0.75,-1.83a9.47,9.47 0 0 1 6,8.86z"></Path>
<Polyline Stroke="{Binding ElementName=_this, Path=Foreground, Mode=OneWay}"
Points="9.270000457763672,24 9.270000457763672,23.989999771118164 9.270000457763672,10.739999771118164"></Polyline>
<Path Stroke="{Binding ElementName=_this, Path=Foreground, Mode=OneWay}"
Data="m9.27,24l0,0c-0.32,-0.22 -5.27,-3.85 -4.94,-10.1a11.36,11.36 0 0 1 2.44,-6.4"></Path>
<Path Stroke="{Binding ElementName=_this, Path=Foreground, Mode=OneWay}"
Data="m9.25,24a0,0 0 0 1 0,0l0,0c0.4,-0.29 5.25,-3.91 4.92,-10.1a11.35,11.35 0 0 0 -2.43,-6.39"></Path>
<Line Stroke="{Binding ElementName=_this, Path=Foreground, Mode=OneWay}"
X1="0" X2="18.53" Y1="14.53" Y2="14.53"></Line>
<Path Stroke="{Binding ElementName=_this, Path=Foreground, Mode=OneWay}"
Data="m12.35,1.89a3.81,3.81 0 0 0 -3.08,-1.89a3.82,3.82 0 0 0 -3.09,1.89a4.75,4.75 0 0 0 -0.19,3.78a9.61,9.61 0 0 0 0.77,1.82a14.22,14.22 0 0 0 2.51,3.25a14.06,14.06 0 0 0 2.49,-3.24a9.2,9.2 0 0 0 0.77,-1.83a4.79,4.79 0 0 0 -0.18,-3.78z"></Path>
<Ellipse Stroke="{Binding ElementName=_this, Path=Foreground, Mode=OneWay}"
Width="1.24" Height="1.26" Margin="8.6, 3" HorizontalAlignment="Left" VerticalAlignment="Top"></Ellipse>
</Grid>
</Viewbox>
</UserControl>
+15
View File
@@ -0,0 +1,15 @@
using System.Windows.Controls;
namespace Daybreak.Controls.Glyphs
{
/// <summary>
/// Interaction logic for MapGlyph.xaml
/// </summary>
public partial class MapGlyph : UserControl
{
public MapGlyph()
{
this.InitializeComponent();
}
}
}
+19
View File
@@ -0,0 +1,19 @@
<UserControl x:Class="Daybreak.Controls.GuildwarsMinimap"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:local="clr-namespace:Daybreak.Controls"
x:Name="_this"
mc:Ignorable="d"
d:DesignHeight="450" d:DesignWidth="800"
MouseWheel="GuildwarsMinimap_MouseWheel"
MouseDown="GuildwarsMinimap_MouseDown"
MouseUp="GuildwarsMinimap_MouseUp"
MouseMove="GuildwarsMinimap_MouseMove">
<Grid>
<Image x:Name="MapDrawingHost" VerticalAlignment="Top" HorizontalAlignment="Left" Stretch="None">
</Image>
<Image x:Name="EntitiesDrawingHost" VerticalAlignment="Stretch" HorizontalAlignment="Stretch" />
</Grid>
</UserControl>
+330
View File
@@ -0,0 +1,330 @@
using Daybreak.Models.Guildwars;
using Daybreak.Models;
using System.Windows.Controls;
using System.Windows.Extensions;
using System.Windows;
using System.Windows.Media.Imaging;
using System.Windows.Media;
using System;
using System.Extensions;
using Daybreak.Services.Scanner;
using Microsoft.Extensions.DependencyInjection;
using System.Core.Extensions;
using System.Windows.Input;
using System.Linq;
namespace Daybreak.Controls;
/// <summary>
/// Interaction logic for GuildwarsMinimap.xaml
/// </summary>
public partial class GuildwarsMinimap : UserControl
{
private readonly IGuildwarsEntityDebouncer guildwarsEntityDebouncer;
private bool resizeEntities;
private bool dragging;
private double mapMinWidth;
private double mapMinHeight;
private double mapWidth;
private double mapHeight;
private Point originPoint = new(0, 0);
private Vector originOffset = new(0, 0);
private Point initialClickPoint = new(0, 0);
private DebounceResponse? cachedDebounceResponse;
[GenerateDependencyProperty]
private PathingData pathingData = new();
[GenerateDependencyProperty]
private GameData gameData = new();
[GenerateDependencyProperty]
private double zoom = 0.08;
public GuildwarsMinimap()
:this(Launch.Launcher.Instance.ApplicationServiceProvider.GetRequiredService<IGuildwarsEntityDebouncer>())
{
}
public GuildwarsMinimap(
IGuildwarsEntityDebouncer guildwarsEntityDebouncer)
{
this.guildwarsEntityDebouncer = guildwarsEntityDebouncer.ThrowIfNull();
this.InitializeComponent();
}
protected override void OnPropertyChanged(DependencyPropertyChangedEventArgs e)
{
base.OnPropertyChanged(e);
if (e.Property == ActualHeightProperty ||
e.Property == ActualWidthProperty)
{
this.resizeEntities = true;
}
else if (e.Property == PathingDataProperty ||
e.Property == ZoomProperty)
{
this.guildwarsEntityDebouncer.ClearCaches();
this.DrawMap();
}
else if (e.Property == GameDataProperty &&
this.GameData.Valid)
{
this.UpdateGameData();
}
}
private void UpdateGameData()
{
var debounceResponse = this.guildwarsEntityDebouncer.DebounceEntities(this.gameData);
if (!double.IsFinite(this.mapWidth) ||
!double.IsFinite(this.mapHeight) ||
!double.IsFinite(this.mapMinWidth) ||
!double.IsFinite(this.mapMinHeight))
{
return;
}
var screenVirtualWidth = this.ActualWidth / this.Zoom;
var screenVirtualHeight = this.ActualHeight / this.Zoom;
var position = debounceResponse.MainPlayer.Position!.Value;
this.originPoint = new Point(
position.X - (screenVirtualWidth / 2) - (this.originOffset.X / this.Zoom),
position.Y + (screenVirtualHeight / 2) + (this.originOffset.Y / this.Zoom));
var adjustedPosition = new Point((int)((position.X - this.mapMinWidth / this.Zoom) * this.Zoom), (int)(this.mapHeight / this.Zoom - position.Y + this.mapMinHeight / this.Zoom) * this.Zoom);
this.MapDrawingHost.Margin = new Thickness(
(-adjustedPosition.X + this.ActualWidth / 2) + this.originOffset.X,
(-adjustedPosition.Y + this.ActualHeight / 2) + this.originOffset.Y,
0,
0);
this.DrawEntities(debounceResponse);
this.cachedDebounceResponse = debounceResponse;
}
private void DrawMap()
{
if (this.PathingData.Trapezoids is null)
{
return;
}
var maxWidth = double.MinValue;
var maxHeight = double.MinValue;
var minWidth = double.MaxValue;
var minHeight = double.MaxValue;
foreach (var trapezoid in this.PathingData.Trapezoids!)
{
if (trapezoid.YT < minHeight)
{
minHeight = trapezoid.YT;
}
if (trapezoid.YB > maxHeight)
{
maxHeight = trapezoid.YB;
}
if (trapezoid.XTL < minWidth)
{
minWidth = trapezoid.XTL;
}
if (trapezoid.XBL < minWidth)
{
minWidth = trapezoid.XBL;
}
if (trapezoid.XTR > maxWidth)
{
maxWidth = trapezoid.XTR;
}
if (trapezoid.XBR > maxWidth)
{
maxWidth = trapezoid.XBR;
}
}
var width = maxWidth - minWidth;
var height = maxHeight - minHeight;
this.mapMinHeight = minHeight * this.Zoom;
this.mapMinWidth = minWidth * this.Zoom;
this.mapWidth = width * this.Zoom;
this.mapHeight = height * this.Zoom;
if (width <= 0 || height <= 0 ||
!double.IsFinite(width) ||
!double.IsFinite(height))
{
return;
}
var bitmap = BitmapFactory.New((int)(width * this.Zoom), (int)(height * this.Zoom));
this.MapDrawingHost.Source = bitmap;
this.MapDrawingHost.Width = width * this.Zoom;
this.MapDrawingHost.Height = height * this.Zoom;
using var bitmapContext = bitmap.GetBitmapContext();
bitmap.Clear(Colors.Transparent);
foreach (var trapezoid in this.PathingData.Trapezoids!)
{
var a = new Point((int)((trapezoid.XTL - minWidth) * this.Zoom), (int)((height - trapezoid.YT + minHeight) * this.Zoom));
var b = new Point((int)((trapezoid.XTR - minWidth) * this.Zoom), (int)((height - trapezoid.YT + minHeight) * this.Zoom));
var c = new Point((int)((trapezoid.XBR - minWidth) * this.Zoom), (int)((height - trapezoid.YB + minHeight) * this.Zoom));
var d = new Point((int)((trapezoid.XBL - minWidth) * this.Zoom), (int)((height - trapezoid.YB + minHeight) * this.Zoom));
var e = new Point((int)((trapezoid.XTL - minWidth) * this.Zoom), (int)((height - trapezoid.YT + minHeight) * this.Zoom));
bitmap.FillPolygon(new int[] { (int)a.X, (int)a.Y, (int)b.X, (int)b.Y, (int)c.X, (int)c.Y, (int)d.X, (int)d.Y, (int)e.X, (int)e.Y, (int)a.X, (int)a.Y }, Colors.White);
}
}
private void DrawEntities(DebounceResponse debounceResponse)
{
if (this.EntitiesDrawingHost.Source is not WriteableBitmap bitmap || this.resizeEntities)
{
bitmap = BitmapFactory.New((int)this.ActualWidth, (int)this.ActualHeight);
this.EntitiesDrawingHost.Source = bitmap;
this.resizeEntities = false;
}
bitmap.Clear(Colors.Transparent);
using var context = bitmap.GetBitmapContext();
this.FillEllipse(debounceResponse.MainPlayer.Position, bitmap, Colors.Green);
foreach (var partyMember in debounceResponse.Party)
{
this.FillEllipse(partyMember.Position, bitmap, Colors.Green);
}
foreach (var player in debounceResponse.WorldPlayers)
{
this.FillEllipse(player.Position, bitmap, Colors.CornflowerBlue);
}
foreach (var livingEntity in debounceResponse.LivingEntities)
{
if (livingEntity.State is LivingEntityState.ToBeCleanedUp)
{
continue;
}
else if (livingEntity.State is LivingEntityState.Dead)
{
this.FillEllipse(livingEntity.Position, bitmap, Colors.Gray);
}
else if (livingEntity.State is LivingEntityState.Boss)
{
this.FillEllipse(livingEntity.Position, bitmap, Colors.DarkRed);
}
else if (livingEntity.Allegiance is LivingEntityAllegiance.AllyNonAttackable)
{
this.FillEllipse(livingEntity.Position, bitmap, Colors.Green);
}
else if (livingEntity.Allegiance is LivingEntityAllegiance.Neutral)
{
this.FillEllipse(livingEntity.Position, bitmap, Colors.Gray);
}
else if (livingEntity.Allegiance is LivingEntityAllegiance.Enemy)
{
this.FillTriangle(livingEntity.Position, bitmap, Colors.Red);
}
else if (livingEntity.Allegiance is LivingEntityAllegiance.SpiritOrPet)
{
this.FillTriangle(livingEntity.Position, bitmap, Colors.Green);
}
else if (livingEntity.Allegiance is LivingEntityAllegiance.Minion)
{
this.FillTriangle(livingEntity.Position, bitmap, Colors.Green);
}
else if (livingEntity.Allegiance is LivingEntityAllegiance.NpcOrMinipet)
{
this.FillTriangle(livingEntity.Position, bitmap, Colors.LimeGreen);
}
}
}
private void FillEllipse(Position? position, WriteableBitmap bitmap, Color color)
{
if (position.HasValue is false)
{
return;
}
var x = (int)((position.Value.X - this.originPoint.X) * this.Zoom);
var y = 0 - (int)((position.Value.Y - this.originPoint.Y) * this.Zoom);
if (x < 0 || x > bitmap.Width ||
y < 0 || y > bitmap.Height)
{
return;
}
bitmap.FillEllipseCentered(
x,
y,
(int)(100 * this.Zoom),
(int)(100 * this.Zoom),
color);
}
private void FillTriangle(Position? position, WriteableBitmap bitmap, Color color)
{
if (position.HasValue is false)
{
return;
}
var x = (int)((position.Value.X - this.originPoint.X) * this.Zoom);
var y = 0 - (int)((position.Value.Y - this.originPoint.Y) * this.Zoom);
if (x < 0 || x > bitmap.Width ||
y < 0 || y > bitmap.Height)
{
return;
}
bitmap.FillTriangle(
(int)((position.Value.X - this.originPoint.X - 100) * this.Zoom),
0 - (int)((position.Value.Y - this.originPoint.Y + 100) * this.Zoom),
(int)((position.Value.X - this.originPoint.X) * this.Zoom),
0 - (int)((position.Value.Y - this.originPoint.Y - 100) * this.Zoom),
(int)((position.Value.X - this.originPoint.X + 100) * this.Zoom),
0 - (int)((position.Value.Y - this.originPoint.Y + 100) * this.Zoom),
color);
}
private bool MouseOverEntity(IEntity entity, Point mousePosition)
{
var x = (int)((entity.Position!.Value.X - this.originPoint.X) * this.Zoom);
var y = 0 - (int)((entity.Position!.Value.Y - this.originPoint.Y) * this.Zoom);
return Math.Pow(mousePosition.X - x, 2) + Math.Pow(mousePosition.Y - y, 2) < Math.Pow(100 * this.Zoom, 2);
}
private void GuildwarsMinimap_MouseDown(object sender, MouseButtonEventArgs e)
{
this.initialClickPoint = Mouse.GetPosition(this);
this.dragging = true;
}
private void GuildwarsMinimap_MouseUp(object sender, MouseButtonEventArgs e)
{
this.dragging = false;
this.originOffset = new(0, 0);
}
private void GuildwarsMinimap_MouseMove(object sender, MouseEventArgs e)
{
if (this.dragging is false)
{
return;
}
var mousePosition = e.GetPosition(this);
this.originOffset = mousePosition - this.initialClickPoint;
}
private void GuildwarsMinimap_MouseWheel(object sender, MouseWheelEventArgs e)
{
var delta = e.Delta > 0 ? 0.1 : -0.1;
this.Zoom += this.Zoom * delta;
}
}
+7 -2
View File
@@ -8,14 +8,15 @@
<UseWPF>true</UseWPF>
<Nullable>enable</Nullable>
<UseWindowsForms>true</UseWindowsForms>
<Platforms>AnyCPU;x86</Platforms>
<Platforms>AnyCPU;x86;x64</Platforms>
<CopyLocalLockFileAssemblies>true</CopyLocalLockFileAssemblies>
<LangVersion>preview</LangVersion>
<ApplicationIcon>Daybreak.ico</ApplicationIcon>
<IncludePackageReferencesDuringMarkupCompilation>true</IncludePackageReferencesDuringMarkupCompilation>
<Version>0.9.8.17</Version>
<Version>0.9.8.18</Version>
<EnableWindowsTargeting>true</EnableWindowsTargeting>
<UserSecretsId>cfb2a489-db80-448d-a969-80270f314c46</UserSecretsId>
<AllowUnsafeBlocks>True</AllowUnsafeBlocks>
</PropertyGroup>
<ItemGroup>
@@ -55,6 +56,7 @@
<PackageReference Include="WpfExtended" Version="0.7.1" />
<PackageReference Include="WpfExtended.SourceGeneration" Version="0.1.1" />
<PackageReference Include="WpfScreenHelper" Version="2.1.0" />
<PackageReference Include="WriteableBitmapEx" Version="1.6.8" />
</ItemGroup>
<ItemGroup>
@@ -67,6 +69,9 @@
<Compile Update="Controls\Buttons\MinusButton.xaml.cs">
<SubType>Code</SubType>
</Compile>
<Compile Update="Controls\Glyphs\MapGlyph.xaml.cs">
<SubType>Code</SubType>
</Compile>
<Compile Update="Controls\Glyphs\StarGlyph.xaml.cs">
<SubType>Code</SubType>
</Compile>
@@ -0,0 +1,11 @@
using System.Collections.Generic;
namespace Daybreak.Models.Guildwars;
public sealed class DebounceResponse
{
public MainPlayerInformation MainPlayer { get; init; } = default;
public IEnumerable<WorldPlayerInformation> WorldPlayers { get; init; } = default!;
public IEnumerable<PlayerInformation> Party { get; init; } = default!;
public IEnumerable<LivingEntity> LivingEntities { get; init; } = default!;
}
+2 -1
View File
@@ -3,7 +3,7 @@ using System.Collections.Generic;
namespace Daybreak.Models;
public sealed class GameData
public readonly struct GameData
{
public bool Valid { get; init; }
public MainPlayerInformation? MainPlayer { get; init; }
@@ -11,4 +11,5 @@ public sealed class GameData
public UserInformation? User { get; init; }
public SessionInformation? Session { get; init; }
public List<WorldPlayerInformation>? WorldPlayers { get; init; }
public List<LivingEntity>? LivingEntities { get; init; }
}
+8
View File
@@ -0,0 +1,8 @@
namespace Daybreak.Models.Guildwars;
public interface IEntity
{
int Id { get; }
uint Timer { get; }
Position? Position { get; }
}
+20
View File
@@ -0,0 +1,20 @@
namespace Daybreak.Models.Guildwars;
public readonly struct LivingEntity : IEntity
{
public int Id { get; init; }
public uint Timer { get; init; }
public Position? Position { get; init; }
public Profession? PrimaryProfession { get; init; }
public Profession? SecondaryProfession { get; init; }
public int Level { get; init; }
public LivingEntityState State { get; init; }
public LivingEntityAllegiance Allegiance { get; init; }
}
@@ -0,0 +1,12 @@
namespace Daybreak.Models.Guildwars;
public enum LivingEntityAllegiance
{
Unknown,
AllyNonAttackable,
Neutral,
Enemy,
SpiritOrPet,
Minion,
NpcOrMinipet
}
@@ -0,0 +1,11 @@
namespace Daybreak.Models.Guildwars;
public enum LivingEntityState
{
Unknown,
Dead,
Boss,
Spirit,
Player,
ToBeCleanedUp
}
@@ -2,11 +2,26 @@
namespace Daybreak.Models.Guildwars;
public sealed class MainPlayerInformation : WorldPlayerInformation
public readonly struct MainPlayerInformation : IEntity
{
public string? Name { get; init; }
public uint Timer { get; init; }
public TitleInformation? TitleInformation { get; init; }
public int Id { get; init; }
public int Level { get; init; }
public Position? Position { get; init; }
public Profession? PrimaryProfession { get; init; }
public Profession? SecondaryProfession { get; init; }
public List<Profession>? UnlockedProfession { get; init; }
public Build? CurrentBuild { get; init; }
public float CurrentHealth { get; init; }
public float MaxHealth { get; init; }
public float CurrentEnergy { get; init; }
public float MaxEnergy { get; init; }
public float HealthRegen { get; init; }
public float EnergyRegen { get; init; }
public bool HardModeUnlocked { get; init; }
public uint Experience { get; init; }
public uint Level { get; init; }
public uint Morale { get; init; }
public Quest? Quest { get; init; }
public List<QuestMetadata>? QuestLog { get; init; }
+8
View File
@@ -0,0 +1,8 @@
using System.Collections.Generic;
namespace Daybreak.Models.Guildwars;
public readonly struct PathingData
{
public List<Trapezoid> Trapezoids { get; init; }
}
@@ -0,0 +1,6 @@
namespace Daybreak.Models.Guildwars;
public readonly struct PathingMetadata
{
public int TrapezoidCount { get; init; }
}
@@ -2,8 +2,12 @@
namespace Daybreak.Models.Guildwars;
public class PlayerInformation
public readonly struct PlayerInformation : IEntity
{
public int Id { get; init; }
public uint Timer { get; init; }
public int Level { get; init; }
public Position? Position { get; init; }
public Profession? PrimaryProfession { get; init; }
public Profession? SecondaryProfession { get; init; }
public List<Profession>? UnlockedProfession { get; init; }
+34
View File
@@ -0,0 +1,34 @@
using System;
namespace Daybreak.Models.Guildwars;
public readonly struct Position : IEquatable<Position>
{
public float X { get; init; }
public float Y { get; init; }
public bool Equals(Position other)
{
return this.X == other.X && this.Y == other.Y;
}
public override bool Equals(object? obj)
{
return obj is Position position && this.Equals(position);
}
public override int GetHashCode()
{
return HashCode.Combine(this.X, this.Y);
}
public static bool operator ==(Position left, Position right)
{
return left.Equals(right);
}
public static bool operator !=(Position left, Position right)
{
return !(left == right);
}
}
+1 -1
View File
@@ -1,6 +1,6 @@
namespace Daybreak.Models.Guildwars;
public sealed class QuestMetadata
public readonly struct QuestMetadata
{
public Quest? Quest { get; init; }
public Map? From { get; init; }
+115
View File
@@ -0,0 +1,115 @@
using System;
using System.Collections.Generic;
using System.Linq;
namespace Daybreak.Models.Guildwars;
public sealed class Region
{
public static Region Kryta { get; } = new Region { Id = 0, Name = "Kryta", WikiUrl = "https://wiki.guildwars.com/wiki/Kryta" };
public static Region MaguumaJungle { get; } = new Region { Id = 1, Name = "Maguuma Jungle", WikiUrl = "https://wiki.guildwars.com/wiki/Maguuma_Jungle" };
public static Region Ascalon { get; } = new Region { Id = 2, Name = "Ascalon", WikiUrl = "https://wiki.guildwars.com/wiki/Ascalon" };
public static Region NorthernShiverpeaks { get; } = new Region { Id = 3, Name = "Northern Shiverpeaks", WikiUrl = "https://wiki.guildwars.com/wiki/Northern_Shiverpeaks" };
public static Region HeroesAscent { get; } = new Region { Id = 4, Name = "Heroes' Ascent", WikiUrl = "https://wiki.guildwars.com/wiki/Heroes%27_Ascent" };
public static Region CrystalDesert { get; } = new Region { Id = 5, Name = "Crystal Desert", WikiUrl = "https://wiki.guildwars.com/wiki/Crystal_Desert" };
public static Region FissureOfWoe { get; } = new Region { Id = 6, Name = "Fissure Of Woe", WikiUrl = "https://wiki.guildwars.com/wiki/The_Fissure_of_Woe" };
public static Region PresearingAscalon { get; } = new Region { Id = 7, Name = "Pre Searing Ascalon", WikiUrl = "https://wiki.guildwars.com/wiki/Ascalon_(pre-Searing)" };
public static Region KainengCity { get; } = new Region { Id = 8, Name = "Kaineng City", WikiUrl = "https://wiki.guildwars.com/wiki/Kaineng_City" };
public static Region EchovaldForest { get; } = new Region { Id = 9, Name = "Echovald Forest", WikiUrl = "https://wiki.guildwars.com/wiki/Echovald_Forest" };
public static Region TheJadeSea { get; } = new Region { Id = 10, Name = "The Jade Sea", WikiUrl = "https://wiki.guildwars.com/wiki/The_Jade_Sea" };
public static Region ShingJeaIsland { get; } = new Region { Id = 11, Name = "Shing Jea Island", WikiUrl = "https://wiki.guildwars.com/wiki/Shing_Jea_Island" };
public static Region Kourna { get; } = new Region { Id = 12, Name = "Kourna", WikiUrl = "https://wiki.guildwars.com/wiki/Kourna" };
public static Region Vabbi { get; } = new Region { Id = 13, Name = "Vabbi", WikiUrl = "https://wiki.guildwars.com/wiki/Vabbi" };
public static Region TheDesolation { get; } = new Region { Id = 14, Name = "TheDesolation", WikiUrl = "https://wiki.guildwars.com/wiki/The_Desolation" };
public static Region Istan { get; } = new Region { Id = 15, Name = "Istan", WikiUrl = "https://wiki.guildwars.com/wiki/Istan" };
public static Region DomainOfAnguish { get; } = new Region { Id = 16, Name = "Domain Of Anguish", WikiUrl = "https://wiki.guildwars.com/wiki/Domain_of_Anguish" };
public static Region TarnishedCoast { get; } = new Region { Id = 17, Name = "Tarnished Coast", WikiUrl = "https://wiki.guildwars.com/wiki/Tarnished_Coast" };
public static Region DepthsOfTyria { get; } = new Region { Id = 18, Name = "Depths Of Tyria", WikiUrl = "https://wiki.guildwars.com/wiki/Depths_of_Tyria" };
public static Region FarShiverpeaks { get; } = new Region { Id = 19, Name = "Far Shiverpeaks", WikiUrl = "https://wiki.guildwars.com/wiki/Far_Shiverpeaks" };
public static Region CharrHomelands { get; } = new Region { Id = 20, Name = "Charr Homelands", WikiUrl = "https://wiki.guildwars.com/wiki/Charr_Homelands" };
public static Region TheBattleIsles { get; } = new Region { Id = 21, Name = "The Battle Isles", WikiUrl = "https://wiki.guildwars.com/wiki/The_Battle_Isles" };
public static Region TheBattleOfJahai { get; } = new Region { Id = 22, Name = "The Battle Of Jahai", WikiUrl = "https://wiki.guildwars.com/wiki/The_Battle_of_Jahai" };
public static Region TheFlightNorth { get; } = new Region { Id = 23, Name = "The Flight North", WikiUrl = "https://wiki.guildwars.com/wiki/The_Flight_North" };
public static Region TheTenguAccords { get; } = new Region { Id = 24, Name = "The Tengu Accords", WikiUrl = "https://wiki.guildwars.com/wiki/The_Tengu_Accords" };
public static Region TheRiseOfTheWhiteMantle { get; } = new Region { Id = 25, Name = "The Rise Of The White Mantle", WikiUrl = "https://wiki.guildwars.com/wiki/The_Rise_of_the_White_Mantle" };
public static Region Swat { get; } = new Region { Id = 26 };
public static Region DevRegion { get; } = new Region { Id = 27 };
public static List<Region> Regions { get; } = new()
{
Kryta,
MaguumaJungle,
Ascalon,
NorthernShiverpeaks,
HeroesAscent,
CrystalDesert,
FissureOfWoe,
PresearingAscalon,
KainengCity,
EchovaldForest,
TheJadeSea,
Kourna,
Vabbi,
TheDesolation,
Istan,
DomainOfAnguish,
TarnishedCoast,
DepthsOfTyria,
FarShiverpeaks,
CharrHomelands,
TheBattleIsles,
TheBattleOfJahai,
TheFlightNorth,
TheTenguAccords,
TheRiseOfTheWhiteMantle,
Swat,
DevRegion
};
public static bool TryParse(int id, out Region region)
{
region = Regions.Where(region => region.Id == id).FirstOrDefault()!;
if (region is null)
{
return false;
}
return true;
}
public static bool TryParse(string name, out Region region)
{
region = Regions.Where(region => region.Name == name).FirstOrDefault()!;
if (region is null)
{
return false;
}
return true;
}
public static Region Parse(int id)
{
if (TryParse(id, out var region) is false)
{
throw new InvalidOperationException($"Could not find a region with id {id}");
}
return region;
}
public static Region Parse(string name)
{
if (TryParse(name, out var region) is false)
{
throw new InvalidOperationException($"Could not find a region with name {name}");
}
return region;
}
private Region()
{
}
public int Id { get; init; }
public string? Name { get; init; }
public string? WikiUrl { get; init; }
}
+27
View File
@@ -0,0 +1,27 @@
namespace Daybreak.Models.Guildwars;
public enum RegionType
{
AllianceBattle,
Arena,
ExplorableZone,
GuildBattleArea,
GuildHall,
MissionOutpost,
CooperativeMission,
CompetitiveMission,
EliteMission,
Challenge,
Outpost,
ZaishenBattle,
HeroesAscent,
City,
MissionArea,
HeroBattleOutpost,
HeroBattleArea,
EotnMission,
Dungeon,
Marketplace,
Unknown,
DevRegion
}
@@ -1,8 +1,9 @@
namespace Daybreak.Models.Guildwars;
public sealed class SessionInformation
public readonly struct SessionInformation
{
public uint FoesKilled { get; init; }
public uint FoesToKill { get; init; }
public Map? CurrentMap { get; init; }
public uint InstanceTimer { get; init; }
}
+1 -1
View File
@@ -1,6 +1,6 @@
namespace Daybreak.Models.Guildwars;
public sealed class SkillMetadata
public readonly struct SkillMetadata
{
public Skill? Skill { get; init; }
public uint Adrenaline1 { get; init; }
@@ -1,6 +1,6 @@
namespace Daybreak.Models.Guildwars;
public sealed class TitleInformation
public readonly struct TitleInformation
{
public bool IsValid => this.CurrentPoints != 0 && this.PointsForCurrentRank != 0 && this.PointsForNextRank != 0 && this.TierNumber != 0 && this.MaxTierNumber != 0;
+18
View File
@@ -0,0 +1,18 @@
namespace Daybreak.Models.Guildwars;
public readonly struct Trapezoid
{
public uint Id { get; init; }
public float XTL { get; init; }
public float XTR { get; init; }
public float YT { get; init; }
public float XBL { get; init; }
public float XBR { get; init; }
public float YB { get; init; }
}
@@ -1,7 +1,23 @@
namespace Daybreak.Models.Guildwars;
using System.Collections.Generic;
public class WorldPlayerInformation : PlayerInformation
namespace Daybreak.Models.Guildwars;
public readonly struct WorldPlayerInformation : IEntity
{
public string? Name { get; init; }
public uint Timer { get; init; }
public TitleInformation? TitleInformation { get; init; }
public int Id { get; init; }
public int Level { get; init; }
public Position? Position { get; init; }
public Profession? PrimaryProfession { get; init; }
public Profession? SecondaryProfession { get; init; }
public List<Profession>? UnlockedProfession { get; init; }
public Build? CurrentBuild { get; init; }
public float CurrentHealth { get; init; }
public float MaxHealth { get; init; }
public float CurrentEnergy { get; init; }
public float MaxEnergy { get; init; }
public float HealthRegen { get; init; }
public float EnergyRegen { get; init; }
}
+58
View File
@@ -0,0 +1,58 @@
using System.Runtime.InteropServices;
namespace Daybreak.Models.Interop;
[StructLayout(LayoutKind.Explicit)]
public readonly struct AreaContext
{
[FieldOffset(0x0000)]
public readonly uint Campaign;
[FieldOffset(0x0004)]
public readonly uint Continent;
[FieldOffset(0x0008)]
public readonly uint RegionId;
[FieldOffset(0x000C)]
public readonly uint RegionType;
[FieldOffset(0x0010)]
public readonly uint Flags;
[FieldOffset(0x0018)]
public readonly uint MinPartySize;
[FieldOffset(0x001C)]
public readonly uint MaxPartySize;
[FieldOffset(0x0020)]
public readonly uint MinPlayerSize;
[FieldOffset(0x0024)]
public readonly uint MaxPlayerSize;
[FieldOffset(0x0028)]
public readonly uint ControlledOutpostId;
[FieldOffset(0x0030)]
public readonly uint MinLevel;
[FieldOffset(0x0034)]
public readonly uint MaxLevel;
[FieldOffset(0x003C)]
public readonly uint MissionMapsTo;
[FieldOffset(0x0040)]
public readonly uint IconPositionX;
[FieldOffset(0x0044)]
public readonly uint IconPositionY;
[FieldOffset(0x0074)]
public readonly uint NameId;
[FieldOffset(0x0078)]
public readonly uint DescriptionId;
}
@@ -0,0 +1,11 @@
namespace Daybreak.Models.Interop;
public enum EntityAllegiance : byte
{
AllyNonAttackable = 0x1,
Neutral = 0x2,
Enemy = 0x3,
SpiritOrPet = 0x4,
Minion = 0x5,
NpcOrMinipet = 0x6
}
+40 -6
View File
@@ -1,14 +1,48 @@
using System;
using System.Runtime.InteropServices;
using System.Runtime.InteropServices;
namespace Daybreak.Models.Interop;
[StructLayout(LayoutKind.Explicit)]
public readonly struct EntityContext
{
[FieldOffset(0x2C)]
public readonly uint EntityId;
public const int EntityContextBaseOffset = 0x0014;
[FieldOffset(0x9C)]
public readonly EntityType Type;
[FieldOffset(0x0000)]
public readonly uint Timer;
[FieldOffset(0x0018)]
public readonly uint AgentId;
[FieldOffset(0x001C)]
public readonly uint ZCoords;
[FieldOffset(0x0060)]
public readonly GamePosition Position;
[FieldOffset(0x0088)]
public readonly EntityType EntityType;
[FieldOffset(0x00E0)]
public readonly ushort PlayerNumber;
[FieldOffset(0x00E2)]
public readonly ushort AgentModelType;
[FieldOffset(0x00F6)]
public readonly byte PrimaryProfessionId;
[FieldOffset(0x00F7)]
public readonly byte SecondaryProfessionId;
[FieldOffset(0x00F8)]
public readonly byte Level;
[FieldOffset(0x00F9)]
public readonly PvpTeam TeamId;
[FieldOffset(0x0144)]
public readonly EntityState State;
[FieldOffset(0x019D)]
public readonly EntityAllegiance Allegiance;
}
+10
View File
@@ -0,0 +1,10 @@
namespace Daybreak.Models.Interop;
public enum EntityState
{
Dead = 0x08,
Boss = 0xC00,
Spirit = 0x40000,
ToBeCleanedUp = 0x40008,
Player = 0x400000,
}
+8
View File
@@ -0,0 +1,8 @@
namespace Daybreak.Models.Interop;
public struct GamePosition
{
public float X;
public float Y;
public uint Plane;
}
+6 -3
View File
@@ -7,11 +7,14 @@ namespace Daybreak.Models.Interop;
public readonly struct GlobalContext
{
[FieldOffset(0x0008)]
public readonly IntPtr InstanceContext;
public readonly int InstanceContext;
[FieldOffset(0x0014)]
public readonly int MapContext;
[FieldOffset(0x002C)]
public readonly IntPtr GameContext;
public readonly int GameContext;
[FieldOffset(0x0044)]
public readonly IntPtr UserContext;
public readonly int UserContext;
}
+1 -1
View File
@@ -6,7 +6,7 @@ namespace Daybreak.Models.Interop;
[StructLayout(LayoutKind.Sequential)]
public readonly struct GuildwarsArray
{
public readonly IntPtr Buffer;
public readonly int Buffer;
public readonly uint Capacity;
+18
View File
@@ -0,0 +1,18 @@
using System;
using System.Runtime.InteropServices;
namespace Daybreak.Models.Interop;
[StructLayout(LayoutKind.Explicit)]
public readonly struct MapContext
{
[FieldOffset(0x0000)]
[MarshalAs(UnmanagedType.ByValArray, SizeConst = 0x14)]
public readonly float[] Boundaries;
/// <summary>
/// Ptr for the PathingMap. Needs to follow this pointer and then next pointer as well to reach the PathingMapContext.
/// </summary>
[FieldOffset(0x0074)]
public readonly int PathingMapContextPtr;
}
+21
View File
@@ -0,0 +1,21 @@
using System;
using System.Runtime.InteropServices;
namespace Daybreak.Models.Interop;
[StructLayout(LayoutKind.Explicit)]
public readonly struct PathingMap
{
[FieldOffset(0x0000)]
public readonly uint ZPlane;
[FieldOffset(0x0018)]
public readonly int TrapezoidArray;
[FieldOffset(0x001C)]
public readonly uint TrapezoidCount;
[FieldOffset(0x0020)]
[MarshalAs(UnmanagedType.ByValArray, SizeConst = 0xD)]
private readonly uint[] H0020;
}
@@ -0,0 +1,10 @@
using System.Runtime.InteropServices;
namespace Daybreak.Models.Interop;
[StructLayout(LayoutKind.Explicit)]
public readonly struct PathingMapContext
{
[FieldOffset(0x0018)]
public readonly GuildwarsArray PathingMapArray;
}
@@ -0,0 +1,26 @@
using System;
using System.Runtime.InteropServices;
namespace Daybreak.Models.Interop;
public readonly struct PathingTrapezoid
{
public readonly uint Id;
[MarshalAs(UnmanagedType.ByValArray, SizeConst = 0x4)]
public readonly int[] AdjacentPathingTrapezoids;
private readonly uint H0014;
public readonly float XTL;
public readonly float XTR;
public readonly float YT;
public readonly float XBL;
public readonly float XBR;
public readonly float YB;
}
+1 -1
View File
@@ -19,7 +19,7 @@ public readonly struct PlayerContext
public readonly uint SecondaryProfession;
[FieldOffset(0x0028)]
public readonly IntPtr NamePointer;
public readonly int NamePointer;
[FieldOffset(0x002C)]
public readonly uint PartyLeaderPlayerNumber;
+9
View File
@@ -0,0 +1,9 @@
namespace Daybreak.Models.Interop;
public enum PvpTeam : byte
{
None = 0,
Blue = 1,
Red = 2,
Yellow = 3
}
+17 -9
View File
@@ -2,22 +2,30 @@
namespace Daybreak.Models.Interop;
[StructLayout(LayoutKind.Explicit)]
public readonly struct SkillbarContext
[StructLayout(LayoutKind.Sequential, Pack = 0, Size = 0xBC)]
public readonly unsafe struct SkillbarContext
{
[FieldOffset(0x0000)]
public readonly uint AgentId;
[FieldOffset(0x0004)]
[MarshalAs(UnmanagedType.ByValArray, SizeConst = 0x08)]
public readonly SkillContext[] Skills;
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;
[FieldOffset(0x00A4)]
public readonly uint Disabled;
[FieldOffset(0x00B0)]
public readonly uint Casting;
[FieldOffset(0x00B8)]
public readonly uint H00B8;
}
+2 -2
View File
@@ -20,9 +20,9 @@ public readonly struct TitleContext
public readonly uint MaxTitleTierIndex;
public readonly IntPtr H0020;
public readonly int H0020;
public readonly IntPtr H0024;
public readonly int H0024;
public bool IsPercentage => (this.Props & 1U) != 0;
+1 -1
View File
@@ -8,7 +8,7 @@ public readonly struct TitleTierContext
public readonly uint TierNumber;
public readonly IntPtr TierNamePtr;
public readonly int TierNamePtr;
public bool IsPercentageBased => (this.Props & 1) != 0;
}
+12 -5
View File
@@ -2,14 +2,14 @@
namespace Daybreak.Models.Interop;
[StructLayout(LayoutKind.Explicit)]
[StructLayout(LayoutKind.Explicit, CharSet = CharSet.Unicode)]
public readonly struct UserContext
{
public const int BaseOffset = 0x0074;
[FieldOffset(0x0000)]
[MarshalAs(UnmanagedType.ByValArray, SizeConst = 0x28)]
public readonly byte[] PlayerNameBytes;
[MarshalAs(UnmanagedType.ByValTStr, SizeConst = 0x28)]
public readonly string PlayerName;
[FieldOffset(0x0124)]
public readonly uint MapId;
@@ -20,7 +20,14 @@ public readonly struct UserContext
[FieldOffset(0x0230)]
public readonly uint PlayerNumber;
// The following 3 fields have to be offset like this due to a bug in CLR when reading x32 mapped memory from x64 processes.
[FieldOffset(0x0344)]
[MarshalAs(UnmanagedType.ByValArray, SizeConst = 0x80)]
public readonly byte[] PlayerEmailBytes;
public readonly char PlayerEmailFirstChar;
[FieldOffset(0x346)]
public readonly char PlayerEmailSecondChar;
[FieldOffset(0x348)]
[MarshalAs(UnmanagedType.ByValTStr, SizeConst = 0x7E)]
public readonly string PlayerEmailRemaining;
}
@@ -8,19 +8,33 @@ public sealed class EventViewerLogsWriter : IEventViewerLogsWriter
{
private const string AppName = "Daybreak";
private const string LogType = "Application";
private readonly bool initialized = true;
public EventViewerLogsWriter()
{
if (EventLog.SourceExists(AppName))
try
{
return;
if (EventLog.SourceExists(AppName))
{
return;
}
EventLog.CreateEventSource(AppName, LogType);
}
catch
{
this.initialized = false;
}
EventLog.CreateEventSource(AppName, LogType);
}
public void WriteLog(Log log)
{
if (this.initialized is false)
{
return;
}
if (log.LogLevel is not LogLevel.Critical)
{
return;
@@ -0,0 +1,135 @@
using Daybreak.Models;
using Daybreak.Models.Guildwars;
using Daybreak.Services.Scanner.Models;
using System;
using System.Collections.Generic;
using System.Linq;
namespace Daybreak.Services.Scanner;
public sealed class GuildwarsEntityDebouncer : IGuildwarsEntityDebouncer
{
private const uint MaxMismatchTimer = 100;
private DebouncePositionalEntityCache<MainPlayerInformation> mainPlayer = new();
private List<DebouncePositionalEntityCache<WorldPlayerInformation>> worldPlayers = new();
private List<DebouncePositionalEntityCache<PlayerInformation>> party = new();
private List<DebouncePositionalEntityCache<LivingEntity>> livingEntities = new();
public DebounceResponse DebounceEntities(GameData gameData)
{
this.DebounceEntitiesInternal(gameData);
return new DebounceResponse
{
MainPlayer = this.mainPlayer.Entity,
WorldPlayers = this.worldPlayers.Select(c => c.Entity),
Party = this.party.Select(c => c.Entity),
LivingEntities = this.livingEntities.Select(c => c.Entity)
};
}
public void ClearCaches()
{
this.ClearInternal();
}
private void ClearInternal()
{
this.mainPlayer = new();
this.worldPlayers.Clear();
this.party.Clear();
this.livingEntities.Clear();
}
private void DebounceEntitiesInternal(GameData gameData)
{
if (gameData.MainPlayer is MainPlayerInformation newMainPlayer)
{
this.DebounceEntityInternal(newMainPlayer, this.mainPlayer);
}
if (gameData.WorldPlayers is List<WorldPlayerInformation> newWorldPlayers)
{
this.worldPlayers = this.DebounceEntityListInternal(newWorldPlayers, this.worldPlayers).ToList();
}
if (gameData.Party is List<PlayerInformation> newPartyPlayers)
{
this.party = this.DebounceEntityListInternal(newPartyPlayers, this.party).ToList();
}
if (gameData.LivingEntities is List<LivingEntity> newLivingEntities)
{
this.livingEntities = this.DebounceEntityListInternal(newLivingEntities, this.livingEntities).ToList();
}
}
private IEnumerable<DebouncePositionalEntityCache<T>> DebounceEntityListInternal<T>(List<T> newEntityList, List<DebouncePositionalEntityCache<T>> cacheList)
where T : IEntity
{
var debouncingList = new List<(T, DebouncePositionalEntityCache<T>)>(newEntityList.Count);
foreach(var entity in newEntityList)
{
if (cacheList.FirstOrDefault(e => e.Entity!.Id == entity.Id) is not DebouncePositionalEntityCache<T> cache)
{
cache = new DebouncePositionalEntityCache<T> { Entity = entity };
cacheList.Add(cache);
}
cache.Entity = entity;
debouncingList.Add((entity, cache));
}
foreach(var debouncingTuple in debouncingList)
{
this.DebounceEntityInternal(debouncingTuple.Item1, debouncingTuple.Item2);
}
return debouncingList.Select(e => e.Item2);
}
private void DebounceEntityInternal<T>(T newEntityData, DebouncePositionalEntityCache<T> cachedEntityData)
where T : IEntity
{
// If the new entity has no position, ignore.
if (newEntityData.Position is not Position newPosition)
{
return;
}
// If the cache is empty, initialize the cache.
if (cachedEntityData.Entity is null)
{
cachedEntityData.Entity = newEntityData;
cachedEntityData.PositionCache.Add(newPosition);
return;
}
// If the new data is older than the old data, ignore the new data.
// The second if clause is supposed to catch uint overflows.
if (cachedEntityData.Entity.Timer - newEntityData.Timer > 0 &&
cachedEntityData.Entity.Timer - newEntityData.Timer < MaxMismatchTimer)
{
return;
}
// If the position is already in the cache, ignore the new data.
if (cachedEntityData.PositionCache.TryGetValue(newPosition, out _))
{
return;
}
// Add the position to the cache. If the cache is full, clear a portion of the cache.
cachedEntityData.PositionCache.Add(newPosition);
cachedEntityData.PositionList.AddLast(newPosition);
cachedEntityData.Entity = newEntityData;
if (cachedEntityData.PositionCache.Count > DebouncePositionalEntityCache<T>.CacheCapacity)
{
for (var i = 0; i < DebouncePositionalEntityCache<T>.CacheStep; i++)
{
cachedEntityData.PositionCache.Remove(cachedEntityData.PositionList.First());
cachedEntityData.PositionList.RemoveFirst();
}
}
}
}
@@ -23,6 +23,7 @@ namespace Daybreak.Services.Scanner;
public sealed class GuildwarsMemoryReader : IGuildwarsMemoryReader
{
private const int MaxTrapezoidCount = 1000000;
private const int RetryInitializationCount = 15;
private const string LatencyMeterName = "Memory Reader Latency";
private const string LatencyMeterUnitsName = "Milliseconds";
@@ -34,9 +35,9 @@ public sealed class GuildwarsMemoryReader : IGuildwarsMemoryReader
private readonly ILiveOptions<ApplicationConfiguration> liveOptions;
private readonly ILogger<GuildwarsMemoryReader> logger;
private IntPtr playerIdPointer;
private IntPtr entityArrayPointer;
private IntPtr titleDataPointer;
private int playerIdPointer;
private int entityArrayPointer;
private int titleDataPointer;
public GuildwarsMemoryReader(
IApplicationLauncher applicationLauncher,
@@ -86,7 +87,27 @@ public sealed class GuildwarsMemoryReader : IGuildwarsMemoryReader
return Task.FromResult<GameData?>(default);
}
return Task.Run(this.SafeReadGameMemory);
return Task.Run(() => this.SafeReadGameMemory(this.ReadGameDataInternal));
}
public Task<PathingData?> ReadPathingData()
{
if (this.memoryScanner.Scanning is false)
{
return Task.FromResult<PathingData?>(default);
}
return Task.Run(() => this.SafeReadGameMemory(this.ReadPathingDataInternal));
}
public Task<PathingMetadata?> ReadPathingMetaData()
{
if (this.memoryScanner.Scanning is false)
{
return Task.FromResult<PathingMetadata?>(default);
}
return Task.Run(() => this.SafeReadGameMemory(this.ReadPathingMetaDataInternal));
}
private async Task InitializeSafe(Process process, ScopedLogger<GuildwarsMemoryReader> scopedLogger)
@@ -129,16 +150,16 @@ public sealed class GuildwarsMemoryReader : IGuildwarsMemoryReader
}
}
private GameData? SafeReadGameMemory()
private T? SafeReadGameMemory<T>(Func<T> readingFunction)
{
try
{
var stopWatch = Stopwatch.StartNew();
var gameData = this.ReadGameMemory();
var ret = readingFunction();
stopWatch.Stop();
this.latencyMeter.Record(stopWatch.Elapsed.TotalMilliseconds);
return gameData;
return ret;
}
catch(Exception e)
{
@@ -147,7 +168,7 @@ public sealed class GuildwarsMemoryReader : IGuildwarsMemoryReader
}
}
private GameData? ReadGameMemory()
private GameData? ReadGameDataInternal()
{
/*
* The following offsets were reverse-engineered using pointer scanning. All of the following ones seem to currently work.
@@ -160,9 +181,9 @@ public sealed class GuildwarsMemoryReader : IGuildwarsMemoryReader
*/
var globalContext = this.memoryScanner.ReadPtrChain<GlobalContext>(this.memoryScanner.ModuleStartAddress, finalPointerOffset: 0x0, 0x00629244, 0xC, 0xC);
if (globalContext.GameContext.ToInt32() == 0x0 ||
globalContext.UserContext.ToInt32() == 0x0 ||
globalContext.InstanceContext.ToInt32() == 0x0)
if (globalContext.GameContext == 0x0 ||
globalContext.UserContext == 0x0 ||
globalContext.InstanceContext == 0x0)
{
return new GameData { Valid = false };
}
@@ -173,6 +194,18 @@ public sealed class GuildwarsMemoryReader : IGuildwarsMemoryReader
var userContext = this.memoryScanner.Read<UserContext>(globalContext.UserContext + UserContext.BaseOffset);
// InstanceContext struct is offset by 0x01AC due to memory layout of the structure.
var instanceContext = this.memoryScanner.Read<InstanceContext>(globalContext.InstanceContext + InstanceContext.BaseOffset);
if (gameContext.MapEntities.Size > 10000 ||
gameContext.Professions.Size > 10000 ||
gameContext.Players.Size > 1000 ||
gameContext.QuestLog.Size > 10000 ||
gameContext.Skillbars.Size > 100 ||
gameContext.PartyAttributes.Size > 1000 ||
gameContext.Titles.Size > 1000 ||
gameContext.TitlesTiers.Size > 10000)
{
return new GameData { Valid = false };
}
var mapEntities = this.memoryScanner.ReadArray<MapEntityContext>(gameContext.MapEntities);
var professions = this.memoryScanner.ReadArray<ProfessionsContext>(gameContext.Professions);
@@ -183,21 +216,22 @@ public sealed class GuildwarsMemoryReader : IGuildwarsMemoryReader
var playerEntityId = this.memoryScanner.ReadPtrChain<int>(this.GetPlayerIdPointer(), 0x0, 0x0);
var titles = this.memoryScanner.ReadArray<TitleContext>(gameContext.Titles);
var titleTiers = this.memoryScanner.ReadArray<TitleTierContext>(gameContext.TitlesTiers);
// The following lines would retrieve all titles infos.
//var titleDataPtr = this.memoryScanner.Read<IntPtr>(this.GetTitleDataPointer());
//var titleInfos = this.memoryScanner.ReadArray<TitleInfo>(titleDataPtr, 48);
// The following lines would retrieve all entities, including item entities.
//var entityArray = this.memoryScanner.ReadPtrChain<GuildwarsArray>(this.GetEntityArrayPointer(), 0x0, 0x0);
//var entities = this.memoryScanner.ReadArray<EntityContext>(entityArray);
var entityPointersArray = this.memoryScanner.ReadPtrChain<GuildwarsArray>(this.GetEntityArrayPointer(), 0x0, 0x0);
if (entityPointersArray.Size > 10000)
{
return new GameData { Valid = false };
}
var entityPointers = this.memoryScanner.ReadArray<int>(entityPointersArray);
var entities = entityPointers.Select(ptr => this.memoryScanner.Read<EntityContext>(ptr + EntityContext.EntityContextBaseOffset)).ToArray();
return this.AggregateGameData(
gameContext,
instanceContext,
mapEntities,
entities,
players,
professions,
quests,
@@ -209,9 +243,60 @@ public sealed class GuildwarsMemoryReader : IGuildwarsMemoryReader
playerEntityId);
}
private IntPtr GetPlayerIdPointer()
private PathingData? ReadPathingDataInternal()
{
if (this.playerIdPointer == IntPtr.Zero)
var globalContext = this.memoryScanner.ReadPtrChain<GlobalContext>(this.memoryScanner.ModuleStartAddress, finalPointerOffset: 0x0, 0x00629244, 0xC, 0xC);
var mapContext = this.memoryScanner.Read<MapContext>(globalContext.MapContext);
var pathingMapContext = this.memoryScanner.ReadPtrChain<PathingMapContext>(mapContext.PathingMapContextPtr, 0x0, 0x0);
if (pathingMapContext.PathingMapArray.Size > 10000)
{
return default;
}
var pathingMaps = this.memoryScanner.ReadArray<PathingMap>(pathingMapContext.PathingMapArray);
var trapezoidsCount = (int)pathingMaps.Select(p => p.TrapezoidCount).Sum(count => count);
if (trapezoidsCount > MaxTrapezoidCount)
{
return default;
}
var trapezoidList = new List<Trapezoid>();
foreach(var pathingMap in pathingMaps)
{
var pathingTrapezoids = this.memoryScanner.ReadArray<PathingTrapezoid>(pathingMap.TrapezoidArray, (int)pathingMap.TrapezoidCount);
foreach(var trapezoid in pathingTrapezoids)
{
trapezoidList.Add(new Trapezoid
{
Id = trapezoid.Id,
XTL = trapezoid.XTL,
XTR = trapezoid.XTR,
YT = trapezoid.YT,
XBL = trapezoid.XBL,
XBR = trapezoid.XBR,
YB = trapezoid.YB,
});
}
}
return new PathingData { Trapezoids = trapezoidList };
}
private PathingMetadata? ReadPathingMetaDataInternal()
{
var globalContext = this.memoryScanner.ReadPtrChain<GlobalContext>(this.memoryScanner.ModuleStartAddress, finalPointerOffset: 0x0, 0x00629244, 0xC, 0xC);
var mapContext = this.memoryScanner.Read<MapContext>(globalContext.MapContext);
var pathingMapContext = this.memoryScanner.ReadPtrChain<PathingMapContext>(mapContext.PathingMapContextPtr, 0x0, 0x0);
var pathingMaps = this.memoryScanner.ReadArray<PathingMap>(pathingMapContext.PathingMapArray);
return new PathingMetadata { TrapezoidCount = (int)pathingMaps.Select(p => p.TrapezoidCount).Sum(count => count) };
}
private int GetPlayerIdPointer()
{
if (this.playerIdPointer == 0)
{
this.playerIdPointer = this.memoryScanner.ScanForPtr(new byte[] { 0x5D, 0xE9, 0x00, 0x00, 0x00, 0x00, 0x55, 0x8B, 0xEC, 0x53 }, "xx????xxxx") - 0xE;
}
@@ -219,9 +304,9 @@ public sealed class GuildwarsMemoryReader : IGuildwarsMemoryReader
return this.playerIdPointer;
}
private IntPtr GetEntityArrayPointer()
private int GetEntityArrayPointer()
{
if (this.entityArrayPointer == IntPtr.Zero)
if (this.entityArrayPointer == 0)
{
this.entityArrayPointer = this.memoryScanner.ScanForPtr(new byte[] { 0xFF, 0x50, 0x10, 0x47, 0x83, 0xC6, 0x04, 0x3B, 0xFB, 0x75, 0xE1 }, "xxxxxxxxxxx") + 0xD;
}
@@ -229,9 +314,9 @@ public sealed class GuildwarsMemoryReader : IGuildwarsMemoryReader
return this.entityArrayPointer;
}
private IntPtr GetTitleDataPointer()
private int GetTitleDataPointer()
{
if (this.titleDataPointer == IntPtr.Zero)
if (this.titleDataPointer == 0)
{
this.titleDataPointer = this.memoryScanner.ScanForAssertion("p:\\code\\gw\\const\\consttitle.cpp", "index < arrsize(s_titleClientData)") + 0x12;
}
@@ -239,10 +324,11 @@ public sealed class GuildwarsMemoryReader : IGuildwarsMemoryReader
return this.titleDataPointer;
}
private GameData AggregateGameData(
private unsafe GameData AggregateGameData(
GameContext gameContext,
InstanceContext instanceContext,
MapEntityContext[] entities,
MapEntityContext[] mapEntities,
EntityContext[] entities,
PlayerContext[] players,
ProfessionsContext[] professions,
QuestContext[] quests,
@@ -253,36 +339,41 @@ public sealed class GuildwarsMemoryReader : IGuildwarsMemoryReader
TitleTierContext[] titleTiers,
int mainPlayerEntityId)
{
var email = ParseAndCleanWCharArray(userContext.PlayerEmailBytes);
var name = ParseAndCleanWCharArray(userContext.PlayerNameBytes);
var name = userContext.PlayerName;
_ = Map.TryParse((int)userContext.MapId, out var currentMap);
var partyMembers = professions
.Where(p => p.AgentId != mainPlayerEntityId)
.Select(p => GetPlayerInformation((int)p.AgentId, instanceContext, entities, professions, skills, partyAttributes))
.Select(p => GetPlayerInformation((int)p.AgentId, instanceContext, mapEntities, professions, skills, partyAttributes, entities))
.ToList();
var mainPlayer = this.GetMainPlayerInformation(
gameContext,
instanceContext,
players.FirstOrDefault(p => p.AgentId == mainPlayerEntityId),
entities,
mapEntities,
professions,
quests,
skills,
partyAttributes,
titles,
titleTiers);
titleTiers,
entities);
var worldPlayers = players
.Where(p => p.AgentId != mainPlayerEntityId)
.Select(p => this.GetWorldPlayerInformation(p, instanceContext, entities, professions, skills, partyAttributes, titles, titleTiers))
.Select(p => this.GetWorldPlayerInformation(p, instanceContext, mapEntities, professions, skills, partyAttributes, titles, titleTiers, entities))
.ToList();
var remainingEntities = entities
.Where(e => e.EntityType == EntityType.Living)
.Where(e => e.AgentId != mainPlayer.Id && partyMembers.None(p => p.Id == e.AgentId) && worldPlayers.None(p => p.Id == e.AgentId))
.ToArray();
var livingEntities = this.GetLivingEntities(remainingEntities);
var userInformation = new UserInformation
{
Email = email,
Email = userContext.PlayerEmailFirstChar + (userContext.PlayerEmailSecondChar + userContext.PlayerEmailRemaining),
CurrentKurzickPoints = gameContext.CurrentKurzick,
TotalKurzickPoints = gameContext.TotalKurzick,
MaxKurzickPoints = gameContext.MaxKurzick,
@@ -303,7 +394,8 @@ public sealed class GuildwarsMemoryReader : IGuildwarsMemoryReader
{
FoesKilled = gameContext.FoesKilled,
FoesToKill = gameContext.FoesToKill,
CurrentMap = currentMap
CurrentMap = currentMap,
InstanceTimer = instanceContext.Timer
};
return new GameData
@@ -313,23 +405,91 @@ public sealed class GuildwarsMemoryReader : IGuildwarsMemoryReader
Session = sessionInformation,
User = userInformation,
WorldPlayers = worldPlayers,
Valid = entities.Length > 0 || players.Length > 0 || professions.Length > 0 || quests.Length > 0 || skills.Length > 0 || partyAttributes.Length > 0
LivingEntities = livingEntities,
Valid = mapEntities.Length > 0 || players.Length > 0 || professions.Length > 0 || quests.Length > 0 || skills.Length > 0 || partyAttributes.Length > 0 || entities.Length > 0
};
}
private List<LivingEntity> GetLivingEntities(EntityContext[] livingEntities)
{
var list = new List<LivingEntity>();
foreach(var livingEntity in livingEntities)
{
_ = Profession.TryParse((int)livingEntity.PrimaryProfessionId, out var primaryProfession);
_ = Profession.TryParse((int)livingEntity.SecondaryProfessionId, out var secondaryProfession);
var state = LivingEntityState.Unknown;
switch (livingEntity.State)
{
case EntityState.Player:
state = LivingEntityState.Player;
break;
case EntityState.Spirit:
state = LivingEntityState.Spirit;
break;
case EntityState.Boss:
state = LivingEntityState.Boss;
break;
case EntityState.Dead:
state = LivingEntityState.Dead;
break;
case EntityState.ToBeCleanedUp:
state = LivingEntityState.ToBeCleanedUp;
break;
}
var allegiance = LivingEntityAllegiance.Unknown;
switch (livingEntity.Allegiance)
{
case EntityAllegiance.AllyNonAttackable:
allegiance = LivingEntityAllegiance.AllyNonAttackable;
break;
case EntityAllegiance.Neutral:
allegiance = LivingEntityAllegiance.Neutral;
break;
case EntityAllegiance.Enemy:
allegiance = LivingEntityAllegiance.Enemy;
break;
case EntityAllegiance.SpiritOrPet:
allegiance = LivingEntityAllegiance.SpiritOrPet;
break;
case EntityAllegiance.Minion:
allegiance = LivingEntityAllegiance.Minion;
break;
case EntityAllegiance.NpcOrMinipet:
allegiance = LivingEntityAllegiance.NpcOrMinipet;
break;
}
list.Add(new LivingEntity
{
Id = (int)livingEntity.AgentId,
Timer = livingEntity.Timer,
Level = (int)livingEntity.Level,
Position = new Position { X = livingEntity.Position.X, Y = livingEntity.Position.Y },
PrimaryProfession = primaryProfession,
SecondaryProfession = secondaryProfession,
State = state,
Allegiance = allegiance
});
}
return list;
}
private MainPlayerInformation GetMainPlayerInformation(
GameContext gameContext,
InstanceContext instanceContext,
PlayerContext playerContext,
MapEntityContext[] entities,
MapEntityContext[] mapEntities,
ProfessionsContext[] professions,
QuestContext[] quests,
SkillbarContext[] skillbars,
PartyAttributesContext[] partyAttributes,
TitleContext[] titles,
TitleTierContext[] titleTiers)
TitleTierContext[] titleTiers,
EntityContext[] entities)
{
var playerInformation = this.GetWorldPlayerInformation(playerContext, instanceContext, entities, professions, skillbars, partyAttributes, titles, titleTiers);
var playerInformation = this.GetWorldPlayerInformation(playerContext, instanceContext, mapEntities, professions, skillbars, partyAttributes, titles, titleTiers, entities);
_ = Quest.TryParse((int)gameContext.QuestId, out var quest);
var questLog = quests
.Select(q =>
@@ -339,11 +499,14 @@ public sealed class GuildwarsMemoryReader : IGuildwarsMemoryReader
_ = Map.TryParse((int)q.MapTo, out var mapTo);
return new QuestMetadata { Quest = parsedQuest, From = mapFrom, To = mapTo };
})
.Where(q => q?.Quest is not null)
.Where(q => q.Quest is not null)
.ToList();
return new MainPlayerInformation
{
Id = playerInformation.Id,
Timer = playerInformation.Timer,
Position = playerInformation.Position,
PrimaryProfession = playerInformation.PrimaryProfession,
SecondaryProfession = playerInformation.SecondaryProfession,
UnlockedProfession = playerInformation.UnlockedProfession,
@@ -358,7 +521,7 @@ public sealed class GuildwarsMemoryReader : IGuildwarsMemoryReader
QuestLog = questLog,
Name = playerInformation.Name,
Experience = gameContext.Experience,
Level = gameContext.Level,
Level = (int)gameContext.Level,
Morale = gameContext.Morale,
CurrentBuild = playerInformation.CurrentBuild,
TitleInformation = playerInformation.TitleInformation
@@ -368,15 +531,16 @@ public sealed class GuildwarsMemoryReader : IGuildwarsMemoryReader
private WorldPlayerInformation GetWorldPlayerInformation(
PlayerContext playerContext,
InstanceContext instanceContext,
MapEntityContext[] entities,
MapEntityContext[] mapEntities,
ProfessionsContext[] professions,
SkillbarContext[] skillbars,
PartyAttributesContext[] partyAttributes,
TitleContext[] titles,
TitleTierContext[] titleTiers)
TitleTierContext[] titleTiers,
EntityContext[] entities)
{
var name = this.memoryScanner.ReadWString(playerContext.NamePointer, 0x40);
var playerInformation = GetPlayerInformation(playerContext.AgentId, instanceContext, entities, professions, skillbars, partyAttributes);
var playerInformation = GetPlayerInformation(playerContext.AgentId, instanceContext, mapEntities, professions, skillbars, partyAttributes, entities);
var maybeCurrentTitleContext = (TitleContext?)null;
var maybeCurrentTitle = (Title?)null;
for(var i = 0; i < titles.Length; i++)
@@ -415,6 +579,10 @@ public sealed class GuildwarsMemoryReader : IGuildwarsMemoryReader
return new WorldPlayerInformation
{
Id = playerInformation.Id,
Timer = playerInformation.Timer,
Level = playerInformation.Level,
Position = playerInformation.Position,
PrimaryProfession = playerInformation.PrimaryProfession,
SecondaryProfession = playerInformation.SecondaryProfession,
UnlockedProfession = playerInformation.UnlockedProfession,
@@ -426,19 +594,20 @@ public sealed class GuildwarsMemoryReader : IGuildwarsMemoryReader
HealthRegen = playerInformation.HealthRegen,
Name = name,
CurrentBuild = playerInformation.CurrentBuild,
TitleInformation = titleInformation
TitleInformation = titleInformation,
};
}
private static PlayerInformation GetPlayerInformation(
int playerId,
InstanceContext instanceContext,
MapEntityContext[] entities,
MapEntityContext[] mapEntities,
ProfessionsContext[] professions,
SkillbarContext[] skillbars,
PartyAttributesContext[] partyAttributes)
PartyAttributesContext[] partyAttributes,
EntityContext[] entities)
{
var entityContext = entities.Skip(playerId).FirstOrDefault();
var mapEntityContext = mapEntities.Skip(playerId).FirstOrDefault();
var professionContext = professions.Where(p => p.AgentId == playerId).FirstOrDefault();
_ = Profession.TryParse((int)professionContext.CurrentPrimary, out var primaryProfession);
_ = Profession.TryParse((int)professionContext.CurrentSecondary, out var secondaryProfession);
@@ -457,8 +626,8 @@ public sealed class GuildwarsMemoryReader : IGuildwarsMemoryReader
secondaryProfession is not null)
{
var attributes = (primaryProfession.PrimaryAttribute is null ?
new List<Models.Guildwars.Attribute>() :
new List<Models.Guildwars.Attribute> { primaryProfession.PrimaryAttribute! })
new List<Daybreak.Models.Guildwars.Attribute>() :
new List<Daybreak.Models.Guildwars.Attribute> { primaryProfession.PrimaryAttribute! })
.Concat(primaryProfession.Attributes)
.Concat(secondaryProfession.Attributes)
.Select(a => new AttributeEntry { Attribute = a })
@@ -479,31 +648,55 @@ public sealed class GuildwarsMemoryReader : IGuildwarsMemoryReader
attributeEntry.Points = (int)attribute.BaseLevel;
}
var skillContexts = new SkillContext[]
{
skillbarContext.Skill0,
skillbarContext.Skill1,
skillbarContext.Skill2,
skillbarContext.Skill3,
skillbarContext.Skill4,
skillbarContext.Skill5,
skillbarContext.Skill6,
skillbarContext.Skill7,
};
build = new Build
{
Primary = primaryProfession,
Secondary = secondaryProfession,
Attributes = attributes,
Skills = skillbarContext.Skills.Select(s =>
Skills = skillContexts.Select(s =>
Skill.TryParse((int)s.Id, out var parsedSkill) ?
parsedSkill :
Skill.NoSkill).ToList()
};
}
(var currentHp, var currentEnergy) = ApplyEnergyAndHealthRegen(instanceContext, entityContext);
(var currentHp, var currentEnergy) = ApplyEnergyAndHealthRegen(instanceContext, mapEntityContext);
EntityContext? entityContext = entities.FirstOrDefault(e => e.AgentId == playerId);
return new PlayerInformation
{
Id = playerId,
Timer = entityContext.Value.Timer,
Level = entityContext.Value.Level,
PrimaryProfession = primaryProfession,
SecondaryProfession = secondaryProfession,
UnlockedProfession = unlockedProfessions,
CurrentHealth = currentHp,
CurrentEnergy = currentEnergy,
MaxHealth = entityContext.MaxHealth,
MaxEnergy = entityContext.MaxEnergy,
HealthRegen = entityContext.HealthRegen,
EnergyRegen = entityContext.EnergyRegen,
CurrentBuild = build
MaxHealth = mapEntityContext.MaxHealth,
MaxEnergy = mapEntityContext.MaxEnergy,
HealthRegen = mapEntityContext.HealthRegen,
EnergyRegen = mapEntityContext.EnergyRegen,
CurrentBuild = build,
Position = entityContext is not null ?
new Position
{
X = entityContext.Value.Position.X,
Y = entityContext.Value.Position.Y
} :
default
};
}
@@ -532,4 +725,16 @@ public sealed class GuildwarsMemoryReader : IGuildwarsMemoryReader
return str;
}
private static unsafe string ParseAndCleanWCharPointer(byte* bytes, int byteCount)
{
var str = Encoding.Unicode.GetString(bytes, byteCount);
var indexOfNull = str.IndexOf('\0');
if (indexOfNull > 0)
{
str = str[..indexOfNull];
}
return str;
}
}
@@ -0,0 +1,11 @@
using Daybreak.Models;
using Daybreak.Models.Guildwars;
namespace Daybreak.Services.Scanner;
public interface IGuildwarsEntityDebouncer
{
DebounceResponse DebounceEntities(GameData gameData);
void ClearCaches();
}
@@ -1,4 +1,5 @@
using Daybreak.Models;
using Daybreak.Models.Guildwars;
using System.Threading.Tasks;
namespace Daybreak.Services.Scanner;
@@ -7,5 +8,7 @@ public interface IGuildwarsMemoryReader
{
Task EnsureInitialized();
Task<GameData?> ReadGameData();
Task<PathingData?> ReadPathingData();
Task<PathingMetadata?> ReadPathingMetaData();
void Stop();
}
+8 -10
View File
@@ -1,13 +1,11 @@
using Daybreak.Models.Interop;
using System;
using System.Collections.Generic;
using System.Diagnostics;
namespace Daybreak.Services.Scanner;
public interface IMemoryScanner
{
public IntPtr ModuleStartAddress { get; }
public int ModuleStartAddress { get; }
public byte[]? Memory { get; }
public int Size { get; }
public bool Scanning { get; }
@@ -15,12 +13,12 @@ public interface IMemoryScanner
void BeginScanner(Process process);
void EndScanner();
T Read<T>(IntPtr address);
T[] ReadArray<T>(IntPtr address, int size);
T Read<T>(int address);
T[] ReadArray<T>(int address, int size);
T[] ReadArray<T>(GuildwarsArray guildwarsArray);
byte[]? ReadBytes(IntPtr address, int size);
string ReadWString(IntPtr address, int maxsize);
T ReadPtrChain<T>(IntPtr Base, int finalPointerOffset = 0, params int[] offsets);
IntPtr ScanForAssertion(string? assertionFile, string? assertionMessage);
IntPtr ScanForPtr(byte[] pattern, string? mask = default, bool readptr = false);
byte[]? ReadBytes(int address, int size);
string ReadWString(int address, int maxsize);
T ReadPtrChain<T>(int Base, int finalPointerOffset = 0, params int[] offsets);
int ScanForAssertion(string? assertionFile, string? assertionMessage);
int ScanForPtr(byte[] pattern, string? mask = default, bool readptr = false);
}
+32 -27
View File
@@ -17,14 +17,15 @@ namespace Daybreak.Services.Scanner;
/// </summary>
public sealed class MemoryScanner : IMemoryScanner
{
private const double MaximumReadSize = 10e9;
private const double MaximumReadSize = 10e8;
private const double MaximumArraySize = 10e5;
private static readonly object LockObject = new();
private readonly ILogger<MemoryScanner> logger;
private bool scanning;
public IntPtr ModuleStartAddress { get; private set; }
public int ModuleStartAddress { get; private set; }
public byte[]? Memory { get; private set; }
public int Size { get; private set; }
public bool Scanning
@@ -101,7 +102,7 @@ public sealed class MemoryScanner : IMemoryScanner
Monitor.Exit(LockObject);
}
public T Read<T>(IntPtr address)
public T Read<T>(int address)
{
this.ValidateReadScanner();
var size = Marshal.SizeOf(typeof(T));
@@ -120,9 +121,13 @@ public sealed class MemoryScanner : IMemoryScanner
return ret;
}
public T[] ReadArray<T>(IntPtr address, int size)
public T[] ReadArray<T>(int address, int size)
{
this.ValidateReadScanner();
if (size > MaximumArraySize)
{
throw new InvalidOperationException($"Expected size to read is too large. Array size {size}");
}
var itemSize = Marshal.SizeOf(typeof(T));
var readSize = size * itemSize;
@@ -157,13 +162,13 @@ public sealed class MemoryScanner : IMemoryScanner
return this.ReadArray<T>(guildwarsArray.Buffer, (int)guildwarsArray.Size);
}
public byte[]? ReadBytes(IntPtr address, int size)
public byte[]? ReadBytes(int address, int size)
{
this.ValidateReadScanner();
return this.ReadBytesNonLocking(address, size);
}
public string ReadWString(IntPtr address, int maxsize)
public string ReadWString(int address, int maxsize)
{
this.ValidateReadScanner();
var rawbytes = this.ReadBytes(address, maxsize);
@@ -181,18 +186,18 @@ public sealed class MemoryScanner : IMemoryScanner
return ret;
}
public T ReadPtrChain<T>(IntPtr Base, int finalPointerOffset = 0, params int[] offsets)
public T ReadPtrChain<T>(int Base, int finalPointerOffset = 0, params int[] offsets)
{
this.ValidateReadScanner();
foreach (var offset in offsets)
{
Base = this.Read<IntPtr>(Base + offset);
Base = this.Read<int>(Base + offset);
}
return this.Read<T>(Base + finalPointerOffset);
}
public IntPtr ScanForPtr(byte[] pattern, string? mask = default, bool readptr = false)
public int ScanForPtr(byte[] pattern, string? mask = default, bool readptr = false)
{
this.ValidateReadScanner();
if (pattern?.Length == 0)
@@ -230,17 +235,17 @@ public sealed class MemoryScanner : IMemoryScanner
{
if (readptr)
{
return new IntPtr(BitConverter.ToUInt32(this.Memory, scan));
return (int)BitConverter.ToUInt32(this.Memory, scan);
}
return new IntPtr(this.ModuleStartAddress.ToInt32() + scan);
return this.ModuleStartAddress + scan;
}
}
return IntPtr.Zero;
return 0;
}
public IntPtr ScanForAssertion(string? assertionFile, string? assertionMessage)
public int ScanForAssertion(string? assertionFile, string? assertionMessage)
{
this.ValidateReadScanner();
var mask = new StringBuilder(64);
@@ -262,15 +267,15 @@ public sealed class MemoryScanner : IMemoryScanner
mask[assertionMessage.Length] = 'x';
mask[assertionMessage.Length + 1] = '\0';
var rdataPtr = this.ScanForPtr(assertionMessageBytes, mask.ToString(), false);
if (rdataPtr == IntPtr.Zero)
if (rdataPtr == 0)
{
return IntPtr.Zero;
return 0;
}
assertionBytes[6] = (byte)rdataPtr.ToInt32();
assertionBytes[7] = (byte)(rdataPtr.ToInt32() >> 8);
assertionBytes[8] = (byte)(rdataPtr.ToInt32() >> 16);
assertionBytes[9] = (byte)(rdataPtr.ToInt32() >> 24);
assertionBytes[6] = (byte)rdataPtr;
assertionBytes[7] = (byte)(rdataPtr >> 8);
assertionBytes[8] = (byte)(rdataPtr >> 16);
assertionBytes[9] = (byte)(rdataPtr >> 24);
}
if (assertionFile is not null)
@@ -285,16 +290,16 @@ public sealed class MemoryScanner : IMemoryScanner
mask[assertionFile.Length + 1] = '\0';
var rdataPtr = this.ScanForPtr(assertionFileBytes, mask.ToString(), false);
assertionBytes[1] = (byte)rdataPtr.ToInt32();
assertionBytes[2] = (byte)(rdataPtr.ToInt32() >> 8);
assertionBytes[3] = (byte)(rdataPtr.ToInt32() >> 16);
assertionBytes[4] = (byte)(rdataPtr.ToInt32() >> 24);
assertionBytes[1] = (byte)rdataPtr;
assertionBytes[2] = (byte)(rdataPtr >> 8);
assertionBytes[3] = (byte)(rdataPtr >> 16);
assertionBytes[4] = (byte)(rdataPtr >> 24);
}
return this.ScanForPtr(assertionBytes, assertionMask, false);
}
private byte[]? ReadBytesNonLocking(IntPtr address, int size)
private byte[]? ReadBytesNonLocking(int address, int size)
{
if (size > MaximumReadSize)
{
@@ -325,7 +330,7 @@ public sealed class MemoryScanner : IMemoryScanner
}
}
private (IntPtr StartAddress, int Size) GetModuleInfo(Process process)
private (int StartAddress, int Size) GetModuleInfo(Process process)
{
try
{
@@ -336,7 +341,7 @@ public sealed class MemoryScanner : IMemoryScanner
if (module.ModuleName != null &&
module.ModuleName.StartsWith(name, StringComparison.OrdinalIgnoreCase))
{
return (module.BaseAddress, module.ModuleMemorySize);
return (module.BaseAddress.ToInt32(), module.ModuleMemorySize);
}
}
}
@@ -345,6 +350,6 @@ public sealed class MemoryScanner : IMemoryScanner
this.logger.LogError(e, "Failed to get module info");
}
return (IntPtr.Zero, 0);
return (0, 0);
}
}
@@ -0,0 +1,14 @@
using Daybreak.Models.Guildwars;
using System.Collections.Generic;
namespace Daybreak.Services.Scanner.Models;
public sealed class DebouncePositionalEntityCache<T> where T : IEntity
{
public const int CacheCapacity = 1000;
public const int CacheStep = 100;
public T? Entity { get; set; } = default!;
public HashSet<Position> PositionCache { get; } = new HashSet<Position>(CacheCapacity);
public LinkedList<Position> PositionList { get; } = new LinkedList<Position>();
}
@@ -6,5 +6,7 @@ namespace Daybreak.Services.Screens;
public interface IScreenManager
{
IEnumerable<Screen> Screens { get; }
void SaveWindowPositionAndSize();
void MoveWindowToSavedPosition();
void MoveGuildwarsToScreen(Screen screen);
}
+77 -3
View File
@@ -1,25 +1,89 @@
using Daybreak.Models;
using Daybreak.Configuration;
using Daybreak.Launch;
using Daybreak.Models;
using Daybreak.Utils;
using Microsoft.Extensions.Logging;
using System;
using System.Collections.Generic;
using System.Configuration;
using System.Core.Extensions;
using System.Diagnostics;
using System.Extensions;
using System.Linq;
using System.Windows;
using System.Windows.Extensions.Services;
using System.Windows.Media;
namespace Daybreak.Services.Screens;
public sealed class ScreenManager : IScreenManager
public sealed class ScreenManager : IScreenManager, IApplicationLifetimeService
{
private readonly MainWindow host;
private readonly ILiveUpdateableOptions<ApplicationConfiguration> liveUpdateableOptions;
private readonly ILogger<ScreenManager> logger;
public IEnumerable<Screen> Screens { get; } = WpfScreenHelper.Screen.AllScreens
.Select((screen, index) => new Screen { Id = index, Size = screen.Bounds });
public ScreenManager(
MainWindow host,
ILiveUpdateableOptions<ApplicationConfiguration> liveUpdateableOptions,
ILogger<ScreenManager> logger)
{
this.logger = logger.ThrowIfNull(nameof(logger));
this.host = host.ThrowIfNull();
this.liveUpdateableOptions = liveUpdateableOptions.ThrowIfNull();
this.logger = logger.ThrowIfNull();
}
public void MoveWindowToSavedPosition()
{
var screenOptions = this.liveUpdateableOptions.Value.ScreenManagerOptions;
var desiredX = screenOptions.X;
var desiredY = screenOptions.Y;
var desiredWidth = screenOptions.Width;
var desiredHeight = screenOptions.Height;
// Validate that the desired position will be at least partially on screen.
var validPosition = false;
foreach(var screen in this.Screens)
{
if (desiredX < screen.Size.Right - 10 &&
desiredY < screen.Size.Bottom - 10)
{
validPosition = true;
break;
}
}
// If the desired window position is not valid, we let the window use the default values.
if (validPosition is true)
{
this.host.Left = desiredX;
this.host.Top = desiredY;
}
// If the desired size of the window is not valid, we let the window use the default values.
if (desiredWidth > 0 &&
desiredHeight > 0)
{
this.host.Width = desiredWidth;
this.host.Height = desiredHeight;
}
}
public void SaveWindowPositionAndSize()
{
var dpiScale = VisualTreeHelper.GetDpi(this.host);
var options = new ScreenManagerOptions
{
X = this.host.WindowState is WindowState.Normal ? this.host.Left * dpiScale.DpiScaleX : 0,
Y = this.host.WindowState is WindowState.Normal ? this.host.Top * dpiScale.DpiScaleY : 0,
Width = this.host.WindowState is WindowState.Normal ? this.host.ActualWidth * dpiScale.DpiScaleX : 0,
Height = this.host.WindowState is WindowState.Normal ? this.host.ActualHeight * dpiScale.DpiScaleY : 0,
};
this.liveUpdateableOptions.Value.ScreenManagerOptions = options;
this.liveUpdateableOptions.UpdateOption();
}
public void MoveGuildwarsToScreen(Screen screen)
@@ -34,4 +98,14 @@ public sealed class ScreenManager : IScreenManager
var process = Process.GetProcessesByName("gw").FirstOrDefault();
return process is not null ? process.MainWindowHandle : throw new InvalidOperationException("Could not find guildwars process");
}
public void OnStartup()
{
this.MoveWindowToSavedPosition();
}
public void OnClosing()
{
this.SaveWindowPositionAndSize();
}
}
+1 -1
View File
@@ -134,5 +134,5 @@ internal static class NativeMethods
[DllImport("user32.dll")]
public static extern IntPtr GetForegroundWindow();
[DllImport("kernel32.dll", SetLastError = true)]
internal static extern bool ReadProcessMemory(IntPtr hProcess, IntPtr lpBaseAddress, IntPtr lpBuffer, int nSize, out IntPtr lpNumberOfBytesRead);
internal static extern bool ReadProcessMemory(IntPtr hProcess, int lpBaseAddress, IntPtr lpBuffer, int nSize, out int lpNumberOfBytesRead);
}
+28 -16
View File
@@ -26,14 +26,15 @@
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition Height="0*"></RowDefinition>
<RowDefinition Height="1*"></RowDefinition>
<RowDefinition Height="0.5*"></RowDefinition>
<RowDefinition Height="0.5*"></RowDefinition>
<RowDefinition Height="118"></RowDefinition>
</Grid.RowDefinitions>
<Grid
Margin="0, 2, 0, 0"
Grid.Column="1"
Grid.ColumnSpan="2"
Grid.Row="2"
Grid.Row="3"
Grid.RowSpan="2"
Visibility="{Binding ElementName=_this, Path=MainPlayerDataValid, Mode=OneWay, Converter={StaticResource BooleanToVisibilityConverter}}">
<Grid.ColumnDefinitions>
@@ -255,6 +256,7 @@
</Grid>
<Grid
Grid.Row="1"
Grid.RowSpan="2"
Grid.Column="1"
Visibility="{Binding ElementName=_this, Path=MainPlayerDataValid, Mode=OneWay, Converter={StaticResource BooleanToVisibilityConverter}}">
<Grid.RowDefinitions>
@@ -430,22 +432,32 @@
</templates:QuestLogTemplate>
</ScrollViewer>
</Grid>
<controls:GuildwarsMinimap
Grid.Column="2"
Grid.Row="1"
Margin="5, 0, 0, 5"
Background="#F0212121"
Visibility="{Binding ElementName=_this, Path=MainPlayerDataValid, Mode=OneWay, Converter={StaticResource BooleanToVisibilityConverter}}"
GameData="{Binding ElementName=_this, Path=GameData, Mode=OneWay}"
PathingData="{Binding ElementName=_this, Path=PathingData, Mode=OneWay}"
Zoom="0.04"
ClipToBounds="True"></controls:GuildwarsMinimap>
<controls:ChromiumBrowserWrapper
x:Name="Browser"
Grid.Column="2"
Grid.Row="1"
Margin="5, 0, 0, 0"
ControlsEnabled="True"
AddressBarReadonly="False"
CanNavigate="True"
CanDownloadBuild="True"
Address="{Binding ElementName=_this, Path=BrowserAddress, Mode=TwoWay}"
MaximizeClicked="Browser_MaximizeClicked"
Visibility="Hidden"
BuildDecoded="Browser_BuildDecoded"
PreventDispose="True"></controls:ChromiumBrowserWrapper>
x:Name="Browser"
Grid.Row="2"
Grid.Column="2"
Margin="5, 0, 0, 0"
ControlsEnabled="True"
AddressBarReadonly="False"
CanNavigate="True"
CanDownloadBuild="True"
Address="{Binding ElementName=_this, Path=BrowserAddress, Mode=TwoWay}"
MaximizeClicked="Browser_MaximizeClicked"
Visibility="Hidden"
BuildDecoded="Browser_BuildDecoded"
PreventDispose="True"></controls:ChromiumBrowserWrapper>
<controls:CircularLoadingWidget Grid.ColumnSpan="4"
Grid.RowSpan="3"
Grid.RowSpan="4"
Width="100"
Height="100"
Visibility="{Binding ElementName=_this, Path=MainPlayerDataValid, Mode=OneWay, Converter={StaticResource InverseBooleanToVisibilityConverter}}"></controls:CircularLoadingWidget>
+142 -107
View File
@@ -16,7 +16,6 @@ using System.Threading.Tasks;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Extensions;
using System.Windows.Media.Animation;
namespace Daybreak.Views;
@@ -38,6 +37,9 @@ public partial class FocusView : UserControl
[GenerateDependencyProperty]
private GameData gameData;
[GenerateDependencyProperty]
private PathingData pathingData;
[GenerateDependencyProperty]
private bool mainPlayerDataValid;
@@ -83,12 +85,14 @@ public partial class FocusView : UserControl
private double leftSideBarSize;
[GenerateDependencyProperty]
private double rightSideBarSize;
[GenerateDependencyProperty]
private bool faultyPathingData;
[GenerateDependencyProperty]
private string browserAddress = string.Empty;
private CancellationTokenSource? cancellationTokenSource;
private bool browserMaximized = false;
private CancellationTokenSource? cancellationTokenSource;
public FocusView(
IBuildTemplateManager buildTemplateManager,
@@ -107,6 +111,7 @@ public partial class FocusView : UserControl
this.liveUpdateableOptions = liveUpdateableOptions.ThrowIfNull();
this.logger = logger.ThrowIfNull();
this.gameData = new GameData();
this.pathingData = new PathingData();
this.InitializeComponent();
@@ -116,8 +121,7 @@ public partial class FocusView : UserControl
protected override void OnPropertyChanged(DependencyPropertyChangedEventArgs e)
{
if (e.Property == BrowserAddressProperty &&
this.Browser.BrowserEnabled &&
this.cancellationTokenSource is not null) //CancellationToken is null only when the view is unloaded. Using this to not overwrite the previous browserurl with the uninitialized value of the address.
this.Browser.BrowserEnabled)
{
this.liveUpdateableOptions.Value.FocusViewOptions.BrowserUrl = this.BrowserAddress;
this.liveUpdateableOptions.UpdateOption();
@@ -136,7 +140,12 @@ public partial class FocusView : UserControl
base.OnPropertyChanged(e);
}
private async void UpdateGameData()
private TimeSpan GetMemoryReaderLatency()
{
return TimeSpan.FromMilliseconds(this.liveUpdateableOptions.Value.ExperimentalFeatures.MemoryReaderFrequency);
}
private async Task UpdatePathingData()
{
if (this.applicationLauncher.IsGuildwarsRunning is false)
{
@@ -144,73 +153,102 @@ public partial class FocusView : UserControl
this.viewManager.ShowView<LauncherView>();
}
await this.guildwarsMemoryReader.EnsureInitialized();
var maybeGameData = await this.guildwarsMemoryReader.ReadGameData();
if (maybeGameData is not GameData gameData)
await this.guildwarsMemoryReader.EnsureInitialized().ConfigureAwait(true);
var maybePathingData = await this.guildwarsMemoryReader.ReadPathingData().ConfigureAwait(true);
if (maybePathingData is not PathingData pathingData ||
pathingData.Trapezoids is null ||
pathingData.Trapezoids.Count == 0)
{
return;
}
this.Dispatcher.Invoke(() =>
this.PathingData = pathingData;
}
private async Task UpdateGameData()
{
if (this.applicationLauncher.IsGuildwarsRunning is false)
{
this.GameData = gameData;
if (this.GameData?.MainPlayer is null ||
this.GameData?.User is null ||
this.GameData?.Session is null)
{
return;
}
this.logger.LogInformation($"Executable is not running. Returning to {nameof(LauncherView)}");
this.viewManager.ShowView<LauncherView>();
}
this.CurrentExperienceInLevel = this.experienceCalculator.GetExperienceForCurrentLevel(this.GameData.MainPlayer!.Experience);
this.NextLevelExperienceThreshold = this.experienceCalculator.GetNextExperienceThreshold(this.GameData.MainPlayer!.Experience);
this.TotalFoes = (int)(this.GameData.Session.FoesKilled + this.GameData.Session.FoesToKill);
this.Vanquishing = this.GameData.Session.FoesToKill + this.GameData.Session.FoesKilled > 0U;
this.TitleActive = this.GameData.MainPlayer.TitleInformation is not null && this.GameData.MainPlayer.TitleInformation.IsValid;
this.MainPlayerDataValid = this.GameData.Valid && this.GameData.MainPlayer.MaxHealth > 0U && this.GameData.MainPlayer.MaxEnergy > 0U;
if (this.GameData.MainPlayer.TitleInformation is TitleInformation titleInformation && titleInformation.IsValid)
{
if (titleInformation.Title is not null &&
titleInformation.Title.Tiers!.Count > titleInformation.TierNumber - 1)
{
var rankIndex = (int)titleInformation.TierNumber! - 1;
this.TitleRankName = $"{titleInformation.Title.Tiers![rankIndex]} ({titleInformation.TierNumber}/{titleInformation.MaxTierNumber})";
}
else
{
this.TitleRankName = string.Empty;
}
if (titleInformation.MaxTierNumber == titleInformation.TierNumber)
{
this.PointsInCurrentRank = (int)titleInformation.CurrentPoints!;
this.PointsForNextRank = (int)titleInformation.CurrentPoints!;
}
else if (titleInformation.IsPercentage is false)
{
this.PointsInCurrentRank = (int)((uint)titleInformation.CurrentPoints! - (uint)titleInformation.PointsForCurrentRank!);
this.PointsForNextRank = (int)((uint)titleInformation.PointsForNextRank! - (uint)titleInformation.PointsForCurrentRank!);
}
else
{
this.PointsInCurrentRank = (int)(uint)titleInformation.CurrentPoints!;
this.PointsForNextRank = (int)(uint)titleInformation.PointsForNextRank!;
}
}
await this.guildwarsMemoryReader.EnsureInitialized().ConfigureAwait(true);
var maybeGameData = await this.guildwarsMemoryReader.ReadGameData().ConfigureAwait(true);
if (maybeGameData is not GameData gameData)
{
this.MainPlayerDataValid = false;
this.Browser.Visibility = this.MainPlayerDataValid is true ? Visibility.Visible : Visibility.Collapsed;
this.UpdateExperienceText();
this.UpdateLuxonText();
this.UpdateKurzickText();
this.UpdateImperialText();
this.UpdateBalthazarText();
this.UpdateVanquishingText();
this.UpdateHealthText();
this.UpdateEnergyText();
this.UpdateTitleText();
},
System.Windows.Threading.DispatcherPriority.ApplicationIdle);
return;
}
if (gameData.MainPlayer is null ||
gameData.User is null ||
gameData.Session is null ||
gameData.Valid is false)
{
this.MainPlayerDataValid = false;
this.Browser.Visibility = this.MainPlayerDataValid is true ? Visibility.Visible : Visibility.Collapsed;
return;
}
this.GameData = gameData;
var pathingMeta = await this.guildwarsMemoryReader.ReadPathingMetaData();
if (pathingMeta?.TrapezoidCount != this.PathingData.Trapezoids?.Count)
{
await this.UpdatePathingData();
}
this.CurrentExperienceInLevel = this.experienceCalculator.GetExperienceForCurrentLevel(this.GameData.MainPlayer!.Value.Experience);
this.NextLevelExperienceThreshold = this.experienceCalculator.GetNextExperienceThreshold(this.GameData.MainPlayer!.Value.Experience);
this.TotalFoes = (int)(this.GameData.Session.Value.FoesKilled + this.GameData.Session.Value.FoesToKill);
this.Vanquishing = this.GameData.Session.Value.FoesToKill + this.GameData.Session.Value.FoesKilled > 0U;
this.TitleActive = this.GameData.MainPlayer.Value.TitleInformation is not null && this.GameData.MainPlayer.Value.TitleInformation.Value.IsValid;
this.MainPlayerDataValid = this.GameData.Valid && this.GameData.MainPlayer.Value.MaxHealth > 0U && this.GameData.MainPlayer.Value.MaxEnergy > 0U;
if (this.GameData.MainPlayer.Value.TitleInformation is TitleInformation titleInformation && titleInformation.IsValid)
{
if (titleInformation.Title is not null &&
titleInformation.Title.Tiers!.Count > titleInformation.TierNumber - 1)
{
var rankIndex = (int)titleInformation.TierNumber! - 1;
this.TitleRankName = $"{titleInformation.Title.Tiers![rankIndex]} ({titleInformation.TierNumber}/{titleInformation.MaxTierNumber})";
}
else
{
this.TitleRankName = string.Empty;
}
if (titleInformation.MaxTierNumber == titleInformation.TierNumber)
{
this.PointsInCurrentRank = (int)titleInformation.CurrentPoints!;
this.PointsForNextRank = (int)titleInformation.CurrentPoints!;
}
else if (titleInformation.IsPercentage is false)
{
this.PointsInCurrentRank = (int)((uint)titleInformation.CurrentPoints! - (uint)titleInformation.PointsForCurrentRank!);
this.PointsForNextRank = (int)((uint)titleInformation.PointsForNextRank! - (uint)titleInformation.PointsForCurrentRank!);
}
else
{
this.PointsInCurrentRank = (int)(uint)titleInformation.CurrentPoints!;
this.PointsForNextRank = (int)(uint)titleInformation.PointsForNextRank!;
}
}
this.Browser.Visibility = this.MainPlayerDataValid is true ? Visibility.Visible : Visibility.Collapsed;
this.UpdateExperienceText();
this.UpdateLuxonText();
this.UpdateKurzickText();
this.UpdateImperialText();
this.UpdateBalthazarText();
this.UpdateVanquishingText();
this.UpdateHealthText();
this.UpdateEnergyText();
this.UpdateTitleText();
}
private void UpdateRightSideBarsLayout()
@@ -237,22 +275,22 @@ public partial class FocusView : UserControl
switch (this.liveUpdateableOptions.Value.FocusViewOptions.ExperienceDisplay)
{
case Configuration.FocusView.ExperienceDisplay.CurrentLevelCurrentAndCurrentLevelMax:
var currentExperienceInLevel = this.experienceCalculator.GetExperienceForCurrentLevel(this.GameData.MainPlayer!.Experience);
var nextLevelExperienceThreshold = this.experienceCalculator.GetNextExperienceThreshold(this.GameData.MainPlayer!.Experience);
var currentExperienceInLevel = this.experienceCalculator.GetExperienceForCurrentLevel(this.GameData.MainPlayer!.Value.Experience);
var nextLevelExperienceThreshold = this.experienceCalculator.GetNextExperienceThreshold(this.GameData.MainPlayer!.Value.Experience);
this.ExperienceBarText = $"{(int)currentExperienceInLevel} / {(int)nextLevelExperienceThreshold} XP";
break;
case Configuration.FocusView.ExperienceDisplay.TotalCurretAndTotalMax:
var currentTotalExperience = this.GameData.MainPlayer!.Experience;
var currentTotalExperience = this.GameData.MainPlayer!.Value.Experience;
var requiredTotalExperience = this.experienceCalculator.GetTotalExperienceForNextLevel(currentTotalExperience);
this.ExperienceBarText = $"{(int)currentTotalExperience} / {(int)requiredTotalExperience} XP";
break;
case Configuration.FocusView.ExperienceDisplay.RemainingUntilNextLevel:
var remainingExperience = this.experienceCalculator.GetRemainingExperienceForNextLevel(this.GameData.MainPlayer!.Experience);
var remainingExperience = this.experienceCalculator.GetRemainingExperienceForNextLevel(this.GameData.MainPlayer!.Value.Experience);
this.ExperienceBarText = $"Remaining {(int)remainingExperience} XP";
break;
case Configuration.FocusView.ExperienceDisplay.Percentage:
var currentExperienceInLevel2 = this.experienceCalculator.GetExperienceForCurrentLevel(this.GameData.MainPlayer!.Experience);
var nextLevelExperienceThreshold2 = this.experienceCalculator.GetNextExperienceThreshold(this.GameData.MainPlayer!.Experience);
var currentExperienceInLevel2 = this.experienceCalculator.GetExperienceForCurrentLevel(this.GameData.MainPlayer!.Value.Experience);
var nextLevelExperienceThreshold2 = this.experienceCalculator.GetNextExperienceThreshold(this.GameData.MainPlayer!.Value.Experience);
this.ExperienceBarText = $"{(int)((double)currentExperienceInLevel2 / (double)nextLevelExperienceThreshold2 * 100)}% XP";
break;
}
@@ -327,13 +365,13 @@ public partial class FocusView : UserControl
switch (this.liveUpdateableOptions.Value.FocusViewOptions.VanquishingDisplay)
{
case Configuration.FocusView.PointsDisplay.CurrentAndMax:
this.VanquishingText = $"{this.GameData.Session!.FoesKilled} / {(int)this.TotalFoes} Foes Killed";
this.VanquishingText = $"{this.GameData.Session!.Value.FoesKilled} / {(int)this.TotalFoes} Foes Killed";
break;
case Configuration.FocusView.PointsDisplay.Remaining:
this.VanquishingText = $"Remaining {this.GameData.Session!.FoesToKill} Foes";
this.VanquishingText = $"Remaining {this.GameData.Session!.Value.FoesToKill} Foes";
break;
case Configuration.FocusView.PointsDisplay.Percentage:
this.VanquishingText = $"{(int)((double)this.GameData.Session!.FoesKilled / (double)this.TotalFoes * 100)}% Foes Killed";
this.VanquishingText = $"{(int)((double)this.GameData.Session!.Value.FoesKilled / (double)this.TotalFoes * 100)}% Foes Killed";
break;
}
}
@@ -343,13 +381,13 @@ public partial class FocusView : UserControl
switch (this.liveUpdateableOptions.Value.FocusViewOptions.HealthDisplay)
{
case Configuration.FocusView.PointsDisplay.CurrentAndMax:
this.HealthBarText = $"{(int)this.GameData.MainPlayer!.CurrentHealth} / {(int)this.GameData.MainPlayer.MaxHealth} Health";
this.HealthBarText = $"{(int)this.GameData.MainPlayer!.Value.CurrentHealth} / {(int)this.GameData.MainPlayer.Value.MaxHealth} Health";
break;
case Configuration.FocusView.PointsDisplay.Remaining:
this.HealthBarText = $"Remaining {(int)this.GameData.MainPlayer!.CurrentHealth} Health";
this.HealthBarText = $"Remaining {(int)this.GameData.MainPlayer!.Value.CurrentHealth} Health";
break;
case Configuration.FocusView.PointsDisplay.Percentage:
this.HealthBarText = $"{(int)(this.GameData.MainPlayer!.CurrentHealth / this.GameData.MainPlayer.MaxHealth * 100)}% Health";
this.HealthBarText = $"{(int)(this.GameData.MainPlayer!.Value.CurrentHealth / this.GameData.MainPlayer.Value.MaxHealth * 100)}% Health";
break;
}
}
@@ -359,13 +397,13 @@ public partial class FocusView : UserControl
switch (this.liveUpdateableOptions.Value.FocusViewOptions.EnergyDisplay)
{
case Configuration.FocusView.PointsDisplay.CurrentAndMax:
this.EnergyBarText = $"{(int)this.GameData.MainPlayer!.CurrentEnergy} / {(int)this.GameData.MainPlayer.MaxEnergy} Energy";
this.EnergyBarText = $"{(int)this.GameData.MainPlayer!.Value.CurrentEnergy} / {(int)this.GameData.MainPlayer.Value.MaxEnergy} Energy";
break;
case Configuration.FocusView.PointsDisplay.Remaining:
this.EnergyBarText = $"Remaining {(int)this.GameData.MainPlayer!.CurrentEnergy} Energy";
this.EnergyBarText = $"Remaining {(int)this.GameData.MainPlayer!.Value.CurrentEnergy} Energy";
break;
case Configuration.FocusView.PointsDisplay.Percentage:
this.EnergyBarText = $"{(int)(this.GameData.MainPlayer!.CurrentEnergy / this.GameData.MainPlayer.MaxEnergy * 100)}% Energy";
this.EnergyBarText = $"{(int)(this.GameData.MainPlayer!.Value.CurrentEnergy / this.GameData.MainPlayer.Value.MaxEnergy * 100)}% Energy";
break;
}
}
@@ -382,32 +420,29 @@ public partial class FocusView : UserControl
}
}
private async Task PeriodicallyUpdateGameData(CancellationToken cancellationToken)
{
if (this.cancellationTokenSource is null ||
this.cancellationTokenSource?.IsCancellationRequested is true)
{
return;
}
this.UpdateGameData();
await Task.Delay((int)this.liveUpdateableOptions.Value.ExperimentalFeatures.MemoryReaderFrequency, cancellationToken);
_ = Task.Run(() => this.PeriodicallyUpdateGameData(this.cancellationTokenSource!.Token), this.cancellationTokenSource!.Token);
}
private void FocusView_Loaded(object _, RoutedEventArgs e)
private async void FocusView_Loaded(object _, RoutedEventArgs e)
{
this.UpdateRightSideBarsLayout();
this.BrowserAddress = this.liveUpdateableOptions.Value.FocusViewOptions.BrowserUrl;
this.cancellationTokenSource?.Cancel();
this.cancellationTokenSource?.Dispose();
this.cancellationTokenSource = new CancellationTokenSource();
Task.Run(() => this.PeriodicallyUpdateGameData(this.cancellationTokenSource.Token));
var cancellationToken = this.cancellationTokenSource.Token;
var memoryReaderLatency = this.GetMemoryReaderLatency();
while (!cancellationToken.IsCancellationRequested)
{
await this.UpdateGameData().ConfigureAwait(true);
if (cancellationToken.IsCancellationRequested)
{
return;
}
await Task.Delay(memoryReaderLatency, cancellationToken).ConfigureAwait(true);
}
}
private void FocusView_Unloaded(object _, RoutedEventArgs e)
{
this.cancellationTokenSource?.Cancel();
this.cancellationTokenSource = null;
this.guildwarsMemoryReader?.Stop();
}
@@ -584,28 +619,28 @@ public partial class FocusView : UserControl
private void MetaBuilds_MouseLeftButtonDown(object _, System.Windows.Input.MouseButtonEventArgs e)
{
if (this.GameData?.MainPlayer?.PrimaryProfession is not null &&
this.GameData.MainPlayer.PrimaryProfession != Profession.None)
if (this.GameData.MainPlayer?.PrimaryProfession is not null &&
this.GameData.MainPlayer?.PrimaryProfession != Profession.None)
{
this.BrowserAddress = this.GameData.MainPlayer.PrimaryProfession.BuildsUrl;
this.BrowserAddress = this.GameData.MainPlayer!.Value.PrimaryProfession.BuildsUrl;
}
}
private void PrimaryProfession_MouseLeftButtonDown(object _, System.Windows.Input.MouseButtonEventArgs e)
{
if (this.GameData?.MainPlayer?.PrimaryProfession is not null &&
this.GameData.MainPlayer.PrimaryProfession != Profession.None)
if (this.GameData.MainPlayer?.PrimaryProfession is not null &&
this.GameData.MainPlayer?.PrimaryProfession != Profession.None)
{
this.BrowserAddress = this.GameData.MainPlayer.PrimaryProfession.WikiUrl;
this.BrowserAddress = this.GameData.MainPlayer!.Value.PrimaryProfession.WikiUrl;
}
}
private void SecondaryProfession_MouseLeftButtonDown(object _, System.Windows.Input.MouseButtonEventArgs e)
{
if (this.GameData?.MainPlayer?.SecondaryProfession is not null &&
this.GameData.MainPlayer.SecondaryProfession != Profession.None)
if (this.GameData.MainPlayer?.SecondaryProfession is not null &&
this.GameData.MainPlayer?.SecondaryProfession != Profession.None)
{
this.BrowserAddress = this.GameData.MainPlayer.SecondaryProfession.WikiUrl;
this.BrowserAddress = this.GameData.MainPlayer!.Value.SecondaryProfession.WikiUrl;
}
}
@@ -640,11 +675,11 @@ public partial class FocusView : UserControl
}
else
{
Grid.SetRow(this.Browser, 1);
Grid.SetRow(this.Browser, 2);
Grid.SetColumn(this.Browser, 2);
Grid.SetRowSpan(this.Browser, 1);
Grid.SetColumnSpan(this.Browser, 1);
this.Browser.Margin = new Thickness(10, 0, 0, 0);
this.Browser.Margin = new Thickness(5, 0, 0, 0);
}
}
+3 -1
View File
@@ -10,7 +10,8 @@
x:Name="_this"
d:DesignHeight="450" d:DesignWidth="800">
<Grid Background="#F0212121">
<ListBox ItemsSource="{Binding ElementName=_this, Path=Metrics, Mode=OneWay}"
<ScrollViewer HorizontalScrollBarVisibility="Auto" VerticalScrollBarVisibility="Auto">
<ListBox ItemsSource="{Binding ElementName=_this, Path=Metrics, Mode=OneWay}"
Background="Transparent">
<ListBox.ItemsPanel>
<ItemsPanelTemplate>
@@ -40,5 +41,6 @@
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
</ScrollViewer>
</Grid>
</UserControl>