mirror of
https://github.com/gwdevhub/Daybreak.git
synced 2026-07-15 15:19:57 +00:00
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:
@@ -19,7 +19,7 @@ jobs:
|
||||
|
||||
strategy:
|
||||
matrix:
|
||||
targetplatform: [x86]
|
||||
targetplatform: [x64]
|
||||
|
||||
runs-on: windows-latest
|
||||
|
||||
|
||||
@@ -19,7 +19,7 @@ jobs:
|
||||
|
||||
strategy:
|
||||
matrix:
|
||||
targetplatform: [x86]
|
||||
targetplatform: [x64]
|
||||
|
||||
runs-on: windows-latest
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net6.0-windows</TargetFramework>
|
||||
<IsPackable>false</IsPackable>
|
||||
<Platforms>AnyCPU;x86</Platforms>
|
||||
<Platforms>AnyCPU;x86;x64</Platforms>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
||||
@@ -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; }
|
||||
}
|
||||
@@ -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>
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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!;
|
||||
}
|
||||
@@ -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; }
|
||||
}
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
namespace Daybreak.Models.Guildwars;
|
||||
|
||||
public interface IEntity
|
||||
{
|
||||
int Id { get; }
|
||||
uint Timer { get; }
|
||||
Position? Position { get; }
|
||||
}
|
||||
@@ -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; }
|
||||
|
||||
@@ -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; }
|
||||
|
||||
@@ -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,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; }
|
||||
|
||||
@@ -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; }
|
||||
}
|
||||
@@ -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,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;
|
||||
|
||||
|
||||
@@ -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; }
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
namespace Daybreak.Models.Interop;
|
||||
|
||||
public enum EntityState
|
||||
{
|
||||
Dead = 0x08,
|
||||
Boss = 0xC00,
|
||||
Spirit = 0x40000,
|
||||
ToBeCleanedUp = 0x40008,
|
||||
Player = 0x400000,
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
namespace Daybreak.Models.Interop;
|
||||
|
||||
public struct GamePosition
|
||||
{
|
||||
public float X;
|
||||
public float Y;
|
||||
public uint Plane;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
namespace Daybreak.Models.Interop;
|
||||
|
||||
public enum PvpTeam : byte
|
||||
{
|
||||
None = 0,
|
||||
Blue = 1,
|
||||
Red = 2,
|
||||
Yellow = 3
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user