Compare commits

...
12 Commits
15 changed files with 336 additions and 368 deletions
+4
View File
@@ -12,6 +12,10 @@ on:
paths:
- "Daybreak/**"
- "Daybreak.Installer/**"
- "Daybreak.API/**"
- "Daybreak.7ZipExtractor/**"
- "Daybreak.Installer/**"
- "Daybreak.Shared/**"
workflow_dispatch:
jobs:
+1 -1
View File
@@ -233,7 +233,7 @@ public sealed class PartyService(
{
var scopedLogger = this.logger.CreateScopedLogger();
scopedLogger.LogDebug("Spawning {heroCount} heroes for party loadout", partyLoadout.Entries.AsValueEnumerable().Count(c => c.HeroId != 0));
foreach (var entry in partyLoadout.Entries)
foreach (var entry in partyLoadout.Entries.AsValueEnumerable().OrderBy(e => e.Build.Primary))
{
if (entry.HeroId != 0 &&
Hero.TryParse(entry.HeroId, out var hero))
@@ -4,6 +4,7 @@ using Microsoft.Extensions.Logging;
using System.Extensions.Core;
using System.Net.Http;
using System.Net.Http.Json;
using System.Text.Encodings.Web;
namespace Daybreak.Shared.Services.Api;
public sealed class ScopedApiContext(
@@ -74,7 +75,8 @@ public sealed class ScopedApiContext(
public async Task<bool> PostMainPlayerBuild(string code, CancellationToken cancellationToken)
{
var path = PostMainPlayerBuildPath.Replace(CodePlaceholder, code);
var encodedBuildCode = UrlEncoder.Default.Encode(code);
var path = PostMainPlayerBuildPath.Replace(CodePlaceholder, encodedBuildCode);
using var emptyContent = new StringContent(string.Empty);
return await this.Post(path, request => emptyContent, cancellationToken);
}
@@ -204,15 +204,6 @@ public sealed class BuildTemplateManager(
return (build, compositionEntry, index);
});
var lockedSkill = loadoutEntryComposition
.Select(entry => entry.build.Skills.FirstOrDefault(s => s != Skill.NoSkill && !IsSkillUnlocked(s.Id, mainPlayerBuildContext.UnlockedAccountSkills)))
.OfType<Skill>()
.FirstOrDefault();
if (lockedSkill is not null)
{
scopedLogger.LogDebug("Invalid team build entry {buildName}. Skill {skillName} is not unlocked by the current account", teamBuildEntry.Name ?? string.Empty, lockedSkill.Name);
}
return loadoutEntryComposition.Any(entry => entry.compositionEntry.Type is PartyCompositionMemberType.MainPlayer && this.CanApply(mainPlayerBuildContext, entry.build));
}
@@ -232,12 +223,6 @@ public sealed class BuildTemplateManager(
return false;
}
if (singleBuildEntry.Skills.FirstOrDefault(s => s != Skill.NoSkill && !IsSkillUnlocked(s.Id, mainPlayerBuildContext.UnlockedCharacterSkills)) is Skill lockedSkill)
{
scopedLogger.LogDebug("Invalid build entry {buildName}. Skill {skillName} is not unlocked by the current character", singleBuildEntry.Name ?? string.Empty, lockedSkill.Name);
return false;
}
return true;
}
@@ -257,16 +242,6 @@ public sealed class BuildTemplateManager(
return false;
}
foreach(var skill in request.BuildSkills)
{
if (skill is not 0 &&
!IsSkillUnlocked((int)skill, request.UnlockedSkills))
{
scopedLogger.LogError("Skill {skillId} is not unlocked", skill);
return false;
}
}
return true;
}
@@ -583,10 +558,17 @@ public sealed class BuildTemplateManager(
}
else
{
build.Metadata =
JsonConvert.DeserializeObject<Dictionary<string, string>>(
Encoding.UTF8.GetString(
Convert.FromBase64String(content[1])));
try
{
build.Metadata =
JsonConvert.DeserializeObject<Dictionary<string, string>>(
Encoding.UTF8.GetString(
Convert.FromBase64String(content[1])));
}
catch(Exception ex)
{
return new InvalidOperationException("Failed to parse build metadata", ex);
}
}
}
@@ -872,16 +854,17 @@ public sealed class BuildTemplateManager(
private static bool IsProfessionUnlocked(int professionId, uint unlockedProfessions) => (unlockedProfessions & (1 << professionId)) != 0;
private static bool IsSkillUnlocked(int skillId, uint[] unlockedSkills)
{
var realIndex = skillId / 32;
if (realIndex >= unlockedSkills.Length)
{
return false;
}
// Not using anymore. Skills are marked as locked if they are not part of the primary/secondary of the current character. Cannot rely on this for build viability checks.
//private static bool IsSkillUnlocked(int skillId, uint[] unlockedSkills)
//{
// var realIndex = skillId / 32;
// if (realIndex >= unlockedSkills.Length)
// {
// return false;
// }
var shift = skillId % 32;
var flag = 1U << shift;
return (unlockedSkills[realIndex] & flag) != 0;
}
// var shift = skillId % 32;
// var flag = 1U << shift;
// return (unlockedSkills[realIndex] & flag) != 0;
//}
}
@@ -1,7 +1,4 @@
using Daybreak.Shared.Models.Metrics;
using Daybreak.Shared.Models.Trade;
using LiveChartsCore;
using LiveChartsCore.Kernel;
using LiveChartsCore;
using LiveChartsCore.SkiaSharpView;
using System.Windows.Extensions.Services;
@@ -19,14 +16,6 @@ internal sealed class LiveChartInitializer : ILiveChartInitializer, IApplication
config.AddSkiaSharp()
.AddDefaultMappers()
.AddDarkTheme()
.AddLightTheme()
.HasMap<Metric>((metric, index) =>
{
return new Coordinate(metric.Timestamp.Ticks, Convert.ToDouble(metric.Measurement));
})
.HasMap<TraderQuote>((quote, point) =>
{
return new Coordinate(quote.Timestamp?.Ticks ?? 0, ((double)quote.Price) / 20d);
}));
.AddLightTheme());
}
}
@@ -1,6 +1,5 @@
using Daybreak.Services.Guildwars.Models;
using Daybreak.Services.Guildwars.Utils;
using System.ComponentModel.DataAnnotations;
using System.Core.Extensions;
using System.IO;
@@ -4,11 +4,13 @@ using Daybreak.Shared.Models.Notifications;
using Daybreak.Shared.Models.Notifications.Handling;
using Daybreak.Shared.Services.Notifications;
using Daybreak.Shared.Utils;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Slim;
using System.Collections.Concurrent;
using System.Core.Extensions;
using System.Extensions;
using System.Extensions.Core;
using System.Runtime.CompilerServices;
namespace Daybreak.Services.Notifications;
@@ -72,18 +74,26 @@ internal sealed class NotificationService(
{
if (storeNotification)
{
await this.storage.OpenNotification(new NotificationDTO
try
{
Title = notification.Title,
Description = notification.Description,
Id = notification.Id,
Level = (int)notification.Level,
MetaData = notification.Metadata,
HandlerType = notification.HandlingType?.AssemblyQualifiedName,
ExpirationTime = notification.ExpirationTime.ToSafeDateTimeOffset().ToUnixTimeMilliseconds(),
CreationTime = notification.CreationTime.ToSafeDateTimeOffset().ToUnixTimeMilliseconds(),
Closed = true
}, cancellationToken);
await this.storage.OpenNotification(new NotificationDTO
{
Title = notification.Title,
Description = notification.Description,
Id = notification.Id,
Level = (int)notification.Level,
MetaData = notification.Metadata,
HandlerType = notification.HandlingType?.AssemblyQualifiedName,
ExpirationTime = notification.ExpirationTime.ToSafeDateTimeOffset().ToUnixTimeMilliseconds(),
CreationTime = notification.CreationTime.ToSafeDateTimeOffset().ToUnixTimeMilliseconds(),
Closed = true
}, cancellationToken);
}
catch (Exception ex)
{
this.logger.CreateScopedLogger().LogError(ex, "Failed to open notification");
return;
}
}
if (notification.HandlingType is null)
@@ -91,8 +101,8 @@ internal sealed class NotificationService(
return;
}
var handler = this.serviceManager.GetService(notification.HandlingType) as INotificationHandler;
handler?.OpenNotification(notification);
var handler = (INotificationHandler)this.serviceManager.GetRequiredService(notification.HandlingType);
handler.OpenNotification(notification);
}
async Task INotificationProducer.RemoveNotification(Notification notification, CancellationToken cancellationToken)
@@ -1,53 +1,105 @@
using Daybreak.Services.Notifications.Models;
using Microsoft.Extensions.Logging;
using System.Core.Extensions;
using System.Extensions.Core;
namespace Daybreak.Services.Notifications;
internal sealed class NotificationStorage(
NotificationsDbContext liteCollection) : INotificationStorage
NotificationsDbContext liteCollection,
ILogger<NotificationStorage> logger) : INotificationStorage
{
private List<NotificationDTO>? notificationsCache;
private readonly NotificationsDbContext liteCollection = liteCollection.ThrowIfNull();
private readonly ILogger<NotificationStorage> logger = logger.ThrowIfNull();
public async ValueTask<IEnumerable<NotificationDTO>> GetPendingNotifications(CancellationToken cancellationToken)
{
this.notificationsCache ??= await this.liteCollection.FindAll(cancellationToken).ToListAsync(cancellationToken);
return this.notificationsCache
.Where(dto => dto.Closed == false && dto.ExpirationTime < DateTimeOffset.Now.ToUnixTimeMilliseconds());
try
{
this.notificationsCache ??= await this.liteCollection.FindAll(cancellationToken).ToListAsync(cancellationToken);
return this.notificationsCache
.Where(dto => dto.Closed == false && dto.ExpirationTime < DateTimeOffset.Now.ToUnixTimeMilliseconds());
}
catch (Exception ex)
{
this.logger.CreateScopedLogger().LogError(ex, "Failed to get pending notifications");
throw;
}
}
public async ValueTask<IEnumerable<NotificationDTO>> GetNotifications(CancellationToken cancellationToken)
{
this.notificationsCache ??= await this.liteCollection.FindAll(cancellationToken).ToListAsync(cancellationToken);
return this.notificationsCache;
try
{
this.notificationsCache ??= await this.liteCollection.FindAll(cancellationToken).ToListAsync(cancellationToken);
return this.notificationsCache;
}
catch (Exception ex)
{
this.logger.CreateScopedLogger().LogError(ex, "Failed to get notifications");
throw;
}
}
public async ValueTask StoreNotification(NotificationDTO notification, CancellationToken cancellationToken)
{
notification.ThrowIfNull();
await this.liteCollection.Insert(notification, cancellationToken);
this.notificationsCache ??= await this.liteCollection.FindAll(cancellationToken).ToListAsync(cancellationToken);
this.notificationsCache.Add(notification);
try
{
await this.liteCollection.Insert(notification, cancellationToken);
this.notificationsCache ??= await this.liteCollection.FindAll(cancellationToken).ToListAsync(cancellationToken);
this.notificationsCache.Add(notification);
}
catch (Exception ex)
{
this.logger.CreateScopedLogger().LogError(ex, "Failed to store notification");
throw;
}
}
public async ValueTask OpenNotification(NotificationDTO notificationDTO, CancellationToken cancellationToken)
{
notificationDTO.ThrowIfNull();
notificationDTO.Closed = true;
await this.liteCollection.Update(notificationDTO, cancellationToken);
this.notificationsCache = await this.liteCollection.FindAll(cancellationToken).ToListAsync(cancellationToken);
try
{
notificationDTO.Closed = true;
await this.liteCollection.Update(notificationDTO, cancellationToken);
this.notificationsCache = await this.liteCollection.FindAll(cancellationToken).ToListAsync(cancellationToken);
}
catch (Exception ex)
{
this.logger.CreateScopedLogger().LogError(ex, "Failed to open notification");
throw;
}
}
public async ValueTask RemoveNotification(NotificationDTO notificationDTO, CancellationToken cancellationToken)
{
await this.liteCollection.Delete(notificationDTO.Id, cancellationToken);
this.notificationsCache ??= await this.liteCollection.FindAll(cancellationToken).ToListAsync(cancellationToken);
this.notificationsCache.Remove(notificationDTO);
try
{
await this.liteCollection.Delete(notificationDTO.Id, cancellationToken);
this.notificationsCache ??= await this.liteCollection.FindAll(cancellationToken).ToListAsync(cancellationToken);
this.notificationsCache.Remove(notificationDTO);
}
catch (Exception ex)
{
this.logger.CreateScopedLogger().LogError(ex, "Failed to remove notification");
throw;
}
}
public async ValueTask RemoveAllNotifications(CancellationToken cancellationToken)
{
await this.liteCollection.DeleteAll(cancellationToken);
this.notificationsCache = default;
try
{
await this.liteCollection.DeleteAll(cancellationToken);
this.notificationsCache = default;
}
catch (Exception ex)
{
this.logger.CreateScopedLogger().LogError(ex, "Failed to remove all notifications");
throw;
}
}
}
@@ -114,15 +114,6 @@ internal sealed class Location
IdFormat = "D2",
StartIndex = 1,
Count = 10
},
new Entry
{
Map = Map.GreenHillsCounty,
Url = "https://media.discordapp.net/attachments/279231165045407744/1053453780580044940/image.png?ex=656a6c49&is=6557f749&hm=beab3433db2cac2d519fc8158978a949e06a57df50b587f41afe3718dc4dad20&=&format=webp&width=954&height=521",
Credit = "",
IdFormat = "D2",
StartIndex = 1,
Count = 1
}
]);
public static readonly Location Ascalon = new(
@@ -262,15 +253,6 @@ internal sealed class Location
IdFormat = "D2",
StartIndex = 1,
Count = 1
},
new Entry
{
Map = Map.TheGreatNorthernWall,
Url = "https://media.discordapp.net/attachments/279231165045407744/853784390357876746/gw005.jpg?ex=656d152d&is=655aa02d&hm=03e8464e9fc60f8c832eebc246a4fff422ebb865d27dc189b727f22aacab6fb2&=&format=webp&width=954&height=380",
Credit = "https://discordapp.com/users/Chrono#6655",
IdFormat = "D2",
StartIndex = 1,
Count = 1
}
]);
public static readonly Location NorthernShiverpeaks = new(
@@ -365,15 +347,6 @@ internal sealed class Location
IdFormat = "D2",
StartIndex = 1,
Count = 7
},
new Entry
{
Map = Map.IronHorseMine,
Url = "https://media.discordapp.net/attachments/279231165045407744/927936695230414888/gw064.jpg?ex=656f3864&is=655cc364&hm=75456e75a3a77e0dcbc8b0f1d3dfa292e477b535e90c7a8f40566e0ffa52cb64&=&format=webp&width=903&height=564",
Credit = "https://discordapp.com/users/Pekka#4619",
IdFormat = "D2",
StartIndex = 1,
Count = 1
}
]);
public static readonly Location Kryta = new(
@@ -1025,15 +998,6 @@ internal sealed class Location
IdFormat = "D2",
StartIndex = 1,
Count = 60
},
new Entry
{
Map = Map.TombOfThePrimevalKings,
Url = "https://cdn.discordapp.com/attachments/279231165045407744/1137832214151843930/gw079.jpg?ex=656cd953&is=655a6453&hm=af57440037882d84e959b950945b738e611d976a2f0beb24d9b1a0fcd626086d&",
Credit = "https://discordapp.com/users/Soldrand#2252",
IdFormat = "D2",
StartIndex = 1,
Count = 1
}
]);
public static readonly Location SouthernShiverpeaks = new(
@@ -1264,25 +1228,7 @@ internal sealed class Location
IdFormat = "D2",
StartIndex = 1,
Count = 1
},
new Entry
{
Map = Map.TalusChute,
Url = "https://cdn.discordapp.com/attachments/279231165045407744/1079051027753480232/gw500.jpg?ex=656b4294&is=6558cd94&hm=95afe18ecddc81d5cedc9865e5db76986dc4ee2fe34f4832ffcc35854298f371&",
Credit = "https://discordapp.com/users/miragee#4827",
IdFormat = "D2",
StartIndex = 1,
Count = 1
},
new Entry
{
Map = Map.TalusChute,
Url = "https://cdn.discordapp.com/attachments/279231165045407744/1079051028000948345/gw507.jpg?ex=656b4294&is=6558cd94&hm=9e85f4560d8184fbb3a575515a72bdfb1d5812a64b57e486acb9f2a1ce658b4a&",
Credit = "https://discordapp.com/users/miragee#4827",
IdFormat = "D2",
StartIndex = 1,
Count = 1
},
}
]);
public static readonly Location RingOfFireIslandChain = new(
Region.RingOfFireIslands,
@@ -2484,15 +2430,6 @@ internal sealed class Location
IdFormat = "D2",
StartIndex = 1,
Count = 2
},
new Entry
{
Map = Map.SunquaVale,
Url = "https://media.discordapp.net/attachments/279231165045407744/859817527739547678/gw010.jpg?ex=657092f9&is=655e1df9&hm=b77dd201214212cfc6dd68db2218f550136878f6a2dca37830cc0dcad4201363&=&format=webp&width=954&height=479",
Credit = "https://discordapp.com/users/Sara#2170",
IdFormat = "D2",
StartIndex = 1,
Count = 1
}
]);
public static readonly Location KainengCity = new(
@@ -4204,15 +4141,6 @@ internal sealed class Location
IdFormat = "D2",
StartIndex = 1,
Count = 5
},
new Entry
{
Map = Map.TheSulfurousWastes,
Url = "https://media.discordapp.net/attachments/279231165045407744/1037396920001372240/unknown.png?ex=65709bab&is=655e26ab&hm=8329f7f07e635c9493c12f53054d090908b8643d0932313d49f2b2ff79601a36&=&format=webp",
Credit = "https://discordapp.com/users/Planewalker#5903",
IdFormat = "D2",
StartIndex = 1,
Count = 5
}
]);
public static readonly Location GateOfTorment = new(
+74 -31
View File
@@ -15,6 +15,7 @@ using System.Core.Extensions;
using System.Data;
using System.Diagnostics;
using System.Extensions;
using System.Extensions.Core;
using System.IO;
using System.Net.Http;
using System.Net.Http.Json;
@@ -68,6 +69,7 @@ internal sealed class ApplicationUpdater(
public async Task<bool> DownloadUpdate(Version version, UpdateStatus updateStatus)
{
var scopedLogger = this.logger.CreateScopedLogger(flowIdentifier: version.ToString());
if (version.HasPrefix is false)
{
version.HasPrefix = true;
@@ -78,21 +80,29 @@ internal sealed class ApplicationUpdater(
return await this.DownloadUpdateInternalLegacy(version, updateStatus);
}
var maybeMetadataResponse = await this.httpClient.GetAsync(
try
{
var maybeMetadataResponse = await this.httpClient.GetAsync(
BlobStorageUrl
.Replace(VersionTag, version.ToString().Replace(".", "-"))
.Replace(FileTag, "Metadata.json"));
if (maybeMetadataResponse.IsSuccessStatusCode)
{
var metaData = await maybeMetadataResponse.Content.ReadFromJsonAsync<List<Metadata>>();
if (metaData is not null)
if (maybeMetadataResponse.IsSuccessStatusCode)
{
return await
await new TaskFactory().StartNew(() => this.DownloadUpdateInternalBlob(metaData, version, updateStatus), CancellationToken.None, TaskCreationOptions.LongRunning, TaskScheduler.Current);
var metaData = await maybeMetadataResponse.Content.ReadFromJsonAsync<List<Metadata>>();
if (metaData is not null)
{
return await
await new TaskFactory().StartNew(() => this.DownloadUpdateInternalBlob(metaData, version, updateStatus), CancellationToken.None, TaskCreationOptions.LongRunning, TaskScheduler.Current);
}
}
}
return await this.DownloadUpdateInternalLegacy(version, updateStatus);
return await this.DownloadUpdateInternalLegacy(version, updateStatus);
}
catch (Exception e)
{
scopedLogger.LogError(e, "Failed to download update for version {version}", version);
return false;
}
}
public async Task<bool> DownloadLatestUpdate(UpdateStatus updateStatus)
@@ -122,31 +132,52 @@ internal sealed class ApplicationUpdater(
public async Task<IEnumerable<Version>> GetVersions()
{
this.logger.LogDebug($"Retrieving version list from {VersionListUrl}");
var response = await this.httpClient.GetAsync(VersionListUrl);
if (response.IsSuccessStatusCode)
var scopedLogger = this.logger.CreateScopedLogger();
scopedLogger.LogDebug($"Retrieving version list from {VersionListUrl}");
try
{
var serializedList = await response.Content.ReadAsStringAsync();
var versionList = serializedList.Deserialize<GithubRefTag[]>();
return versionList!.Select(v => v.Ref![RefTagPrefix.Length..]).Select(v => new Version(v));
}
var response = await this.httpClient.GetAsync(VersionListUrl);
if (response.IsSuccessStatusCode)
{
var serializedList = await response.Content.ReadAsStringAsync();
var versionList = serializedList.Deserialize<GithubRefTag[]>();
return versionList!.Select(v => v.Ref![RefTagPrefix.Length..]).Select(v => new Version(v));
}
return [];
scopedLogger.LogError("Failed to retrieve version list. Status code: {statusCode}", response.StatusCode);
return [];
}
catch (Exception e)
{
scopedLogger.LogError(e, "Failed to retrieve version list from {url}", VersionListUrl);
return [];
}
}
public async Task<string?> GetChangelog(Version version)
{
var changeLogResponse = await this.httpClient.GetAsync(
var scopedLogger = this.logger.CreateScopedLogger(flowIdentifier: version.ToString());
try
{
var changeLogResponse = await this.httpClient.GetAsync(
BlobStorageUrl
.Replace(VersionTag, version.ToString().Replace(".", "-"))
.Replace(FileTag, "changelog.txt"));
if (!changeLogResponse.IsSuccessStatusCode)
if (!changeLogResponse.IsSuccessStatusCode)
{
scopedLogger.LogError("Failed to retrieve changelog for version {version}. Status code: {statusCode}", version, changeLogResponse.StatusCode);
return default;
}
scopedLogger.LogDebug("Retrieved changelog for version {version}", version);
return await changeLogResponse.Content.ReadAsStringAsync();
}
catch (Exception e)
{
scopedLogger.LogError(e, "Failed to retrieve changelog for version {version}", version);
return default;
}
return await changeLogResponse.Content.ReadAsStringAsync();
}
public void PeriodicallyCheckForUpdates()
@@ -198,7 +229,7 @@ internal sealed class ApplicationUpdater(
private async Task<bool> DownloadUpdateInternalBlob(List<Metadata> metadata, Version version, UpdateStatus updateStatus)
{
var scopedLogger = this.logger.CreateScopedLogger(nameof(this.DownloadUpdateInternalBlob), version.ToString());
var scopedLogger = this.logger.CreateScopedLogger(flowIdentifier: version.ToString());
updateStatus.CurrentStep = DownloadStatus.InitializingDownload;
// Exclude daybreak packed files
@@ -341,34 +372,46 @@ internal sealed class ApplicationUpdater(
private async Task<bool> DownloadUpdateInternalLegacy(Version version, UpdateStatus updateStatus)
{
var scopedLogger = this.logger.CreateScopedLogger(flowIdentifier: version.VersionString);
updateStatus.CurrentStep = DownloadStatus.InitializingDownload;
var uri = DownloadUrl.Replace(VersionTag, version.ToString());
if (await this.downloadService.DownloadFile(uri, TempFile, updateStatus) is false)
{
this.logger.LogError("Failed to download update file");
scopedLogger.LogError("Failed to download update file");
return false;
}
updateStatus.CurrentStep = UpdateStatus.PendingRestart;
this.logger.LogDebug("Downloaded update file");
scopedLogger.LogDebug("Downloaded update file");
return true;
}
private async Task<Version?> GetLatestVersion()
{
using var response = await this.httpClient.GetAsync(Url);
if (response.IsSuccessStatusCode)
var scopedLogger = this.logger.CreateScopedLogger();
try
{
var versionTag = response.RequestMessage!.RequestUri!.ToString().Split('/').Last().TrimStart('v');
if (Version.TryParse(versionTag, out var parsedVersion))
using var response = await this.httpClient.GetAsync(Url);
if (response.IsSuccessStatusCode)
{
return parsedVersion;
var versionTag = response.RequestMessage!.RequestUri!.ToString().Split('/').Last().TrimStart('v');
if (Version.TryParse(versionTag, out var parsedVersion))
{
return parsedVersion;
}
scopedLogger.LogError("Failed to parse version from {versionTag}", versionTag);
return default;
}
scopedLogger.LogError("Failed to retrieve latest version. Status code: {statusCode}", response.StatusCode);
return default;
}
catch(Exception e)
{
scopedLogger.LogError(e, "Failed to retrieve latest version from {url}", Url);
return default;
}
return default;
}
private void LaunchExtractor()
+14 -24
View File
@@ -1,5 +1,7 @@
using Daybreak.Shared.Models.Metrics;
using Daybreak.Shared.Services.Metrics;
using LiveChartsCore.Defaults;
using LiveChartsCore.Kernel;
using LiveChartsCore.SkiaSharpView;
using LiveChartsCore.SkiaSharpView.Painting;
using LiveChartsCore.SkiaSharpView.VisualElements;
@@ -91,6 +93,7 @@ public partial class MetricsView : UserControl
private void CartesianChart_Loaded(object sender, RoutedEventArgs e)
{
var cartesianChart = sender.Cast<CartesianChart>();
cartesianChart.CoreChart.Update(new ChartUpdateParams { IsAutomaticUpdate = false, Throttling = false });
if (cartesianChart.DataContext is not MetricSetViewModel metricSet)
{
return;
@@ -112,22 +115,11 @@ public partial class MetricsView : UserControl
cartesianChart.DrawMargin = new LiveChartsCore.Measure.Margin(30);
cartesianChart.XAxes =
[
new Axis
new DateTimeAxis(TimeSpan.FromSeconds(1), dateTime => dateTime.ToString("d"))
{
Name = "Time",
Labeler = (ticks) =>
{
return new DateTime((long)ticks).ToString("HH:mm:ss");
},
LabelsPaint = this.foregroundPaint,
SeparatorsPaint = this.transparentPaint,
CrosshairLabelsPaint = this.transparentPaint,
CrosshairPaint = this.transparentPaint,
NamePaint = this.foregroundPaint,
SubseparatorsPaint = this.transparentPaint,
SubticksPaint = this.transparentPaint,
TicksPaint = this.transparentPaint,
ZeroPaint = this.transparentPaint,
}
];
@@ -137,27 +129,20 @@ public partial class MetricsView : UserControl
{
Name = metricSet.Instrument.Unit,
LabelsPaint = this.foregroundPaint,
SeparatorsPaint = this.transparentPaint,
CrosshairLabelsPaint = this.transparentPaint,
CrosshairPaint = this.transparentPaint,
NamePaint = this.foregroundPaint,
SubseparatorsPaint = this.transparentPaint,
SubticksPaint = this.transparentPaint,
TicksPaint = this.transparentPaint,
ZeroPaint = this.transparentPaint,
}
];
cartesianChart.Series =
[
new LineSeries<Metric>
new LineSeries<DateTimePoint>
{
Values = metricSet.AggregationType switch
{
AggregationTypes.NoAggregate => PlotNoAggregation(metricSet.Metrics),
AggregationTypes.P95 => PlotPercentageAggregation(metricSet.Metrics, 0.95),
AggregationTypes.P98 => PlotPercentageAggregation(metricSet.Metrics, 0.98),
AggregationTypes.P99 => PlotPercentageAggregation(metricSet.Metrics, 0.99),
AggregationTypes.NoAggregate => ToDateTimePoint(PlotNoAggregation(metricSet.Metrics)),
AggregationTypes.P95 => ToDateTimePoint(PlotPercentageAggregation(metricSet.Metrics, 0.95)),
AggregationTypes.P98 => ToDateTimePoint(PlotPercentageAggregation(metricSet.Metrics, 0.98)),
AggregationTypes.P99 => ToDateTimePoint(PlotPercentageAggregation(metricSet.Metrics, 0.99)),
_ => throw new InvalidOperationException("Unable to plot metrics. Unknown aggregation")
},
Fill = default,
@@ -198,4 +183,9 @@ public partial class MetricsView : UserControl
return [.. finalDataSet];
}
private static List<DateTimePoint> ToDateTimePoint(IEnumerable<Metric> dataSet)
{
return [.. dataSet.Select(s => new DateTimePoint(s.Timestamp, System.Convert.ToDouble(s.Measurement)))];
}
}
+35 -54
View File
@@ -1,61 +1,42 @@
<UserControl x:Class="Daybreak.Views.Trade.PriceHistoryView"
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.Views.Trade"
xmlns:chart="clr-namespace:LiveChartsCore.SkiaSharpView.WPF;assembly=LiveChartsCore.SkiaSharpView.WPF"
xmlns:lvCore="clr-namespace:LiveChartsCore;assembly=LiveChartsCore"
xmlns:buttons="clr-namespace:Daybreak.Controls.Buttons"
xmlns:converters="clr-namespace:Daybreak.Shared.Converters;assembly=Daybreak.Shared"
xmlns:controls="clr-namespace:Daybreak.Controls"
DataContextChanged="UserControl_DataContextChanged"
x:Name="_this"
mc:Ignorable="d"
d:DesignHeight="450" d:DesignWidth="800">
<Grid
Background="{StaticResource Daybreak.Brushes.Background}">
<chart:CartesianChart Series="{Binding ElementName=_this, Path=Series, Mode=OneWay}"
Title="{Binding ElementName=_this, Path=Title, Mode=OneWay}"
Foreground="{StaticResource MahApps.Brushes.ThemeForeground}"
ZoomMode="X"
EasingFunction="{x:Static lvCore:EasingFunctions.BounceOut}"
AnimationsSpeed="0:0:0.1"
Background="{StaticResource MahApps.Brushes.ThemeBackground}"
XAxes="{Binding ElementName=_this, Path=XAxes, Mode=OneWay}"
YAxes="{Binding ElementName=_this, Path=YAxes, Mode=OneWay}">
</chart:CartesianChart>
<buttons:BackButton
Height="30"
<UserControl
x:Class="Daybreak.Views.Trade.PriceHistoryView"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:buttons="clr-namespace:Daybreak.Controls.Buttons"
xmlns:chart="clr-namespace:LiveChartsCore.SkiaSharpView.WPF;assembly=LiveChartsCore.SkiaSharpView.WPF"
xmlns:controls="clr-namespace:Daybreak.Controls"
xmlns:converters="clr-namespace:Daybreak.Shared.Converters;assembly=Daybreak.Shared"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:local="clr-namespace:Daybreak.Views.Trade"
xmlns:lvCore="clr-namespace:LiveChartsCore;assembly=LiveChartsCore"
xmlns:lvCoreMeasure="clr-namespace:LiveChartsCore.Measure;assembly=LiveChartsCore"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
x:Name="_this"
d:DesignHeight="450"
d:DesignWidth="800"
DataContextChanged="UserControl_DataContextChanged"
mc:Ignorable="d">
<Grid Background="{StaticResource Daybreak.Brushes.Background}">
<chart:CartesianChart
Title="{Binding ElementName=_this, Path=Title, Mode=OneWay}"
AnimationsSpeed="0:0:0.1"
Background="{StaticResource MahApps.Brushes.ThemeBackground}"
EasingFunction="{x:Static lvCore:EasingFunctions.BounceOut}"
Foreground="{StaticResource MahApps.Brushes.ThemeForeground}"
Series="{Binding ElementName=_this, Path=Series, Mode=OneWay}"
XAxes="{Binding ElementName=_this, Path=XAxes, Mode=OneWay}"
YAxes="{Binding ElementName=_this, Path=YAxes, Mode=OneWay}"
ZoomMode="X" />
<buttons:BackButton
Width="30"
Height="30"
Margin="5"
HorizontalAlignment="Left"
VerticalAlignment="Top"
Margin="5"
Clicked="BackButton_Clicked"
ToolTip="Go back to price quotes"/>
<buttons:HighlightButton
Height="30"
Width="100"
Title="Reset Zoom"
Foreground="{StaticResource MahApps.Brushes.ThemeForeground}"
Background="{StaticResource Daybreak.Brushes.Background}"
VerticalAlignment="Top"
HorizontalAlignment="Right"
Margin="5"
FontSize="16"
HighlightBrush="{StaticResource MahApps.Brushes.Accent}"
VerticalContentAlignment="Center"
HorizontalContentAlignment="Center"
BorderBrush="{StaticResource MahApps.Brushes.ThemeForeground}"
BorderThickness="1"
Clicked="HighlightButton_Clicked"
ToolTip="Reset zoom"/>
<Grid
Visibility="{Binding ElementName=_this, Path=Loading, Mode=OneWay, Converter={StaticResource BooleanToVisibilityConverter}}"
Background="{StaticResource Daybreak.Brushes.Background}">
<controls:CircularLoadingWidget
MaxWidth="200"
MaxHeight="200"/>
ToolTip="Go back to price quotes" />
<Grid Background="{StaticResource Daybreak.Brushes.Background}" Visibility="{Binding ElementName=_this, Path=Loading, Mode=OneWay, Converter={StaticResource BooleanToVisibilityConverter}}">
<controls:CircularLoadingWidget MaxWidth="200" MaxHeight="200" />
</Grid>
</Grid>
</UserControl>
+33 -42
View File
@@ -3,6 +3,9 @@ using Daybreak.Shared.Models.Trade;
using Daybreak.Shared.Services.Navigation;
using Daybreak.Shared.Services.TradeChat;
using LiveChartsCore;
using LiveChartsCore.Defaults;
using LiveChartsCore.Drawing;
using LiveChartsCore.Measure;
using LiveChartsCore.SkiaSharpView;
using LiveChartsCore.SkiaSharpView.Painting;
using LiveChartsCore.SkiaSharpView.VisualElements;
@@ -45,6 +48,9 @@ public partial class PriceHistoryView : UserControl
[GenerateDependencyProperty]
private DateTime endDateTime = DateTime.MaxValue;
[GenerateDependencyProperty]
private Margin drawMargin = new(float.NaN, float.NaN, float.NaN, 300);
[GenerateDependencyProperty]
private bool loading = false;
@@ -62,6 +68,7 @@ public partial class PriceHistoryView : UserControl
this.foregroundPaint = new SolidColorPaint(new SKColor(skForegroundBrush.Color.R, skForegroundBrush.Color.G, skForegroundBrush.Color.B, skForegroundBrush.Color.A));
this.accentPaint = new SolidColorPaint(new SKColor(skAccentBrush.Color.R, skAccentBrush.Color.G, skAccentBrush.Color.B, skAccentBrush.Color.A), 2);
this.backgroundPaint = new SolidColorPaint(new SKColor(skBackgroundBrush.Color.R, skBackgroundBrush.Color.G, skBackgroundBrush.Color.B, skBackgroundBrush.Color.A));
this.PopulateChart();
}
private void UserControl_DataContextChanged(object _, DependencyPropertyChangedEventArgs __)
@@ -74,18 +81,6 @@ public partial class PriceHistoryView : UserControl
this.viewManager.ShowView<PriceQuotesView>();
}
private void HighlightButton_Clicked(object sender, EventArgs e)
{
var xAxis = this.XAxes.FirstOrDefault();
if (xAxis is null)
{
return;
}
xAxis.MinLimit = this.StartDateTime.Ticks;
xAxis.MaxLimit = this.EndDateTime.Ticks;
}
private async void FetchPriceHistory()
{
if (this.DataContext is not ItemBase itemBase)
@@ -115,33 +110,15 @@ public partial class PriceHistoryView : UserControl
private void PopulateChart()
{
this.EndDateTime = this.traderQuotes.OrderByDescending(t => t.Timestamp).FirstOrDefault()?.Timestamp ?? DateTime.Now;
this.StartDateTime = this.EndDateTime - TimeSpan.FromDays(1);
this.XAxes =
[
new Axis
new DateTimeAxis(TimeSpan.FromHours(1), dateTime => dateTime.ToString("d"))
{
Name = "Date",
LabelsPaint = this.foregroundPaint,
Labeler = ticks =>
{
if (double.IsNaN(ticks))
{
return string.Empty;
}
var t = (long)Math.Round(ticks);
if (t < DateTime.MinValue.Ticks || t > DateTime.MaxValue.Ticks)
{
return string.Empty;
}
return new DateTime(t, DateTimeKind.Utc).ToString("d");
},
MinLimit = this.StartDateTime.Ticks,
MaxLimit = this.EndDateTime.Ticks,
MinStep = TimeSpan.FromHours(1).Ticks,
NameTextSize = 16,
NamePaint = this.foregroundPaint,
InLineNamePlacement = true
}
];
@@ -150,24 +127,25 @@ public partial class PriceHistoryView : UserControl
new Axis
{
Name = "Price",
NameTextSize = 16,
NamePadding = new Padding(0, 15),
NamePaint = this.foregroundPaint,
LabelsPaint = this.foregroundPaint,
Labeler = (price) => $"{price*20d:0}g",
Labeler = Labelers.Currency,
InLineNamePlacement = true
}
];
this.Series =
[
new LineSeries<TraderQuote>
new StepLineSeries<DateTimePoint>
{
Values = this.traderQuotes,
Fill = default,
IsHoverable = false,
Values = [.. ProcessQuotes(this.traderQuotes).Select(t => new DateTimePoint(t.Timestamp ?? DateTime.MinValue, t.Price))],
Stroke = this.accentPaint,
LineSmoothness = 0,
Name = "Historical Price",
GeometryStroke = default,
GeometryFill = default,
GeometrySize = default,
Name = string.Empty,
}
];
@@ -175,7 +153,20 @@ public partial class PriceHistoryView : UserControl
{
Text = $"{this.traderQuotes.FirstOrDefault()?.Item?.Name} Price Chart",
TextSize = 22,
Paint = this.foregroundPaint,
Paint = this.foregroundPaint
};
}
private static IEnumerable<TraderQuote> ProcessQuotes(IEnumerable<TraderQuote> quotes)
{
var lastPricePoint = double.MinValue;
foreach(var quote in quotes.OrderBy(p => p.Timestamp))
{
if (quote.Price != lastPricePoint)
{
lastPricePoint = quote.Price;
yield return quote;
}
}
}
}
+1 -1
View File
@@ -2,7 +2,7 @@
<PropertyGroup>
<NoWarn>$(NoWarn);IL2104;IL3053;IL3000;IL3002;NU1701;CS0108</NoWarn>
<Version>0.9.9.76</Version>
<Version>0.9.9.78</Version>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
+53 -57
View File
@@ -1,78 +1,74 @@
<Project>
<Project>
<PropertyGroup>
<ManagePackageVersionsCentrally>true</ManagePackageVersionsCentrally>
</PropertyGroup>
<ItemGroup>
<PackageVersion Include="SharpCompress" Version="0.40.0" />
<PackageVersion Include="Elastic.OpenTelemetry" Version="1.0.2" />
<PackageVersion Include="AvalonEdit" Version="6.3.1.120" />
<PackageVersion Include="coverlet.collector" Version="6.0.4" />
<PackageVersion Include="Daybreak.7ZipExtractor" Version="1.0.0" />
<PackageVersion Include="DiffPlex" Version="1.8.0" />
<PackageVersion Include="Elastic.OpenTelemetry" Version="1.1.0" />
<PackageVersion Include="FluentAssertions" Version="8.5.0" />
<PackageVersion Include="HtmlAgilityPack" Version="1.12.2" />
<PackageVersion Include="ini-parser-netstandard" Version="2.5.3" />
<PackageVersion Include="LiveChartsCore.SkiaSharpView.WPF" Version="2.0.0-rc4.5" />
<PackageVersion Include="MahApps.Metro" Version="2.4.10" />
<PackageVersion Include="MeaMod.DNS" Version="1.0.71" />
<PackageVersion Include="MemoryPack" Version="1.21.4" />
<PackageVersion Include="MemoryPack.Generator" Version="1.21.4" />
<PackageVersion Include="Microsoft.AspNetCore.OpenApi" Version="9.0.7" />
<PackageVersion Include="Microsoft.CorrelationVector" Version="1.0.42" />
<PackageVersion Include="Microsoft.Data.Sqlite" Version="9.0.7" />
<PackageVersion Include="Microsoft.Data.Sqlite.Core" Version="9.0.7" />
<PackageVersion Include="Microsoft.Extensions.Configuration.UserSecrets" Version="9.0.7" />
<PackageVersion Include="Microsoft.Extensions.Http" Version="9.0.7" />
<PackageVersion Include="Microsoft.Extensions.Logging.Abstractions" Version="9.0.7" />
<PackageVersion Include="Microsoft.NET.Test.Sdk" Version="17.14.1" />
<PackageVersion Include="Microsoft.Web.WebView2" Version="1.0.3351.48" />
<PackageVersion Include="Microsoft.Xaml.Behaviors.Wpf" Version="1.1.135" />
<PackageVersion Include="MinHook.NET" Version="1.1.1" />
<PackageVersion Include="MSTest.TestAdapter" Version="3.9.3" />
<PackageVersion Include="MSTest.TestFramework" Version="3.9.3" />
<PackageVersion Include="NSubstitute" Version="5.3.0" />
<PackageVersion Include="NSubstitute.Analyzers.CSharp" Version="1.0.17" />
<PackageVersion Include="NAudio" Version="2.2.1" />
<PackageVersion Include="Net.Sdk.Web.Extensions" Version="0.8.10" />
<PackageVersion Include="Net.Sdk.Web.Extensions.SourceGenerators" Version="0.9.5" />
<PackageVersion Include="OpenTelemetry.Exporter.Console" Version="1.12.0" />
<PackageVersion Include="OpenTelemetry.Instrumentation.Http" Version="1.12.0" />
<PackageVersion Include="OpenTelemetry.Instrumentation.Runtime" Version="1.12.0" />
<PackageVersion Include="Reloaded.Assembler" Version="1.0.16" />
<PackageVersion Include="System.Private.Uri" Version="4.3.2" />
<PackageVersion Include="WpfExtended" Version="0.7.9" />
<PackageVersion Include="WpfExtended.SourceGeneration" Version="0.3.0" />
<PackageVersion Include="WpfScreenHelper" Version="2.1.1" />
<PackageVersion Include="WriteableBitmapEx" Version="1.6.8" />
<PackageVersion Include="Squealify" Version="0.8.2.2" />
<PackageVersion Include="System.Formats.Asn1" Version="9.0.6" />
<PackageVersion Include="System.IO.Compression" Version="4.3.0" />
<PackageVersion Include="System.Linq.Async" Version="6.0.3" />
<PackageVersion Include="PeNet" Version="5.1.0" />
<PackageVersion Include="Plumsy" Version="1.1.0" />
<PackageVersion Include="Reloaded.Assembler" Version="1.0.16" />
<PackageVersion Include="securifybv.ShellLink" Version="0.1.0" />
<PackageVersion Include="AvalonEdit" Version="6.3.1.120" />
<PackageVersion Include="DiffPlex" Version="1.8.0" />
<PackageVersion Include="HtmlAgilityPack" Version="1.12.1" />
<PackageVersion Include="ini-parser-netstandard" Version="2.5.3" />
<PackageVersion Include="LiveChartsCore.SkiaSharpView.WPF" Version="2.0.0-rc2" />
<PackageVersion Include="MahApps.Metro" Version="2.4.10" />
<PackageVersion Include="Microsoft.CorrelationVector" Version="1.0.42" />
<PackageVersion Include="Microsoft.Data.Sqlite" Version="9.0.6" />
<PackageVersion Include="Microsoft.Data.Sqlite.Core" Version="9.0.6" />
<PackageVersion Include="Microsoft.Extensions.Configuration.UserSecrets" Version="9.0.6" />
<PackageVersion Include="Microsoft.Extensions.Logging.Abstractions" Version="9.0.6" />
<PackageVersion Include="Microsoft.Xaml.Behaviors.Wpf" Version="1.1.135" />
<PackageVersion Include="NAudio" Version="2.2.1" />
<PackageVersion Include="Microsoft.AspNetCore.OpenApi" Version="9.0.6" />
<PackageVersion Include="MinHook.NET" Version="1.1.1" />
<PackageVersion Include="Net.Sdk.Web.Extensions" Version="0.8.10" />
<PackageVersion Include="Net.Sdk.Web.Extensions.SourceGenerators" Version="0.9.5" />
<PackageVersion Include="Serilog" Version="4.3.0" />
<PackageVersion Include="Serilog.Extensions.Logging" Version="9.0.2" />
<PackageVersion Include="Serilog.Settings.Configuration" Version="9.0.0" />
<PackageVersion Include="Serilog.Sinks.Console" Version="6.0.0" />
<PackageVersion Include="Serilog.Sinks.File" Version="7.0.0" />
<PackageVersion Include="Swashbuckle.AspNetCore" Version="9.0.1" />
<PackageVersion Include="Swashbuckle.AspNetCore.Swagger" Version="9.0.1" />
<PackageVersion Include="Swashbuckle.AspNetCore.SwaggerGen" Version="9.0.1" />
<PackageVersion Include="Swashbuckle.AspNetCore.SwaggerUI" Version="9.0.1" />
<PackageVersion Include="SystemExtensions.NetStandard.Generators" Version="0.1.6" />
<PackageVersion Include="ZLinq" Version="1.4.12" />
<PackageVersion Include="MeaMod.DNS" Version="1.0.71" />
<PackageVersion Include="MemoryPack" Version="1.21.4" />
<PackageVersion Include="MemoryPack.Generator" Version="1.21.4" />
<PackageVersion Include="Microsoft.Extensions.Http" Version="9.0.6" />
<PackageVersion Include="Microsoft.Web.WebView2" Version="1.0.3296.44" />
<PackageVersion Include="SharpCompress" Version="0.40.0" />
<PackageVersion Include="Slim" Version="1.9.2" />
<PackageVersion Include="Squealify" Version="0.8.2.2" />
<PackageVersion Include="Swashbuckle.AspNetCore" Version="9.0.3" />
<PackageVersion Include="Swashbuckle.AspNetCore.Swagger" Version="9.0.3" />
<PackageVersion Include="Swashbuckle.AspNetCore.SwaggerGen" Version="9.0.3" />
<PackageVersion Include="Swashbuckle.AspNetCore.SwaggerUI" Version="9.0.3" />
<PackageVersion Include="System.Formats.Asn1" Version="9.0.7" />
<PackageVersion Include="System.IO.Compression" Version="4.3.0" />
<PackageVersion Include="System.Linq.Async" Version="6.0.3" />
<PackageVersion Include="System.Net.Http" Version="4.3.4" />
<PackageVersion Include="System.Reflection.Metadata" Version="9.0.6" />
<PackageVersion Include="System.Text.Encoding.CodePages" Version="9.0.6" />
<PackageVersion Include="System.Text.Json" Version="9.0.6" />
<PackageVersion Include="System.Private.Uri" Version="4.3.2" />
<PackageVersion Include="System.Reflection.Metadata" Version="9.0.7" />
<PackageVersion Include="System.Text.Encoding.CodePages" Version="9.0.7" />
<PackageVersion Include="System.Text.Json" Version="9.0.7" />
<PackageVersion Include="System.Text.RegularExpressions" Version="4.3.1" />
<PackageVersion Include="SystemExtensions.NetCore" Version="1.6.12" />
<PackageVersion Include="SystemExtensions.NetStandard.DependencyInjection" Version="1.6.9" />
<PackageVersion Include="FluentAssertions" Version="8.3.0" />
<PackageVersion Include="Microsoft.NET.Test.Sdk" Version="17.14.1" />
<PackageVersion Include="MSTest.TestAdapter" Version="3.9.3" />
<PackageVersion Include="MSTest.TestFramework" Version="3.9.3" />
<PackageVersion Include="coverlet.collector" Version="6.0.4" />
<PackageVersion Include="NSubstitute" Version="5.3.0" />
<PackageVersion Include="NSubstitute.Analyzers.CSharp" Version="1.0.17" />
<PackageVersion Include="SystemExtensions.NetStandard.Generators" Version="0.1.6" />
<PackageVersion Include="WpfExtended" Version="0.7.9" />
<PackageVersion Include="WpfExtended.SourceGeneration" Version="0.3.0" />
<PackageVersion Include="WpfScreenHelper" Version="2.1.1" />
<PackageVersion Include="WriteableBitmapEx" Version="1.6.8" />
<PackageVersion Include="ZLinq" Version="1.5.2" />
</ItemGroup>
</Project>
</Project>