Files
Daybreak/Daybreak.Core/Views/FocusView.razor.cs
T
amacocianandAlexandru Macocian 97fed608d2 Update GWCA (#1484)
* Improve GWCA bindings generator based on headers

* Move all structs to gwca

* Progress

* Complete parser work

* More generator adjustments

* GWCA fixes

* GWCA improvements

* GWCA fixes

* GWCA fixes

* GWCA fixes

* GWCA fixes

* Fix API usage of GWCA

* Update GWCA

---------

Co-authored-by: Alexandru Macocian <amacocian@microsoft.com>
2026-02-26 14:13:40 -08:00

660 lines
25 KiB
C#

using Daybreak.Configuration.Options;
using Daybreak.Shared.Models.Api;
using Daybreak.Shared.Models.Builds;
using Daybreak.Shared.Models.FocusView;
using Daybreak.Shared.Models.Guildwars;
using Daybreak.Shared.Services.Api;
using Daybreak.Shared.Services.BuildTemplates;
using Daybreak.Shared.Services.Experience;
using Daybreak.Shared.Services.LaunchConfigurations;
using Daybreak.Shared.Services.Notifications;
using Daybreak.Shared.Services.Options;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using System.Core.Extensions;
using System.Diagnostics;
using System.Extensions;
using System.Extensions.Core;
using TrailBlazr.Services;
using TrailBlazr.ViewModels;
namespace Daybreak.Views;
public sealed class FocusViewModel(
IOptionsProvider optionsProvider,
IBuildTemplateManager buildTemplateManager,
IViewManager viewManager,
INotificationService notificationService,
ILaunchConfigurationService launchConfigurationService,
IDaybreakApiService daybreakApiService,
IExperienceCalculator experienceCalculator,
IOptionsMonitor<FocusViewOptions> options,
ILogger<FocusView> logger)
: ViewModelBase<FocusViewModel, FocusView>
{
private const int MaxRetryAttempts = 5;
private static readonly TimeSpan GameInfoFrequency = TimeSpan.FromSeconds(1);
private static readonly TimeSpan GameInfoTimeout = TimeSpan.FromSeconds(5);
private static readonly TimeSpan BuildsCheckFrequency = TimeSpan.FromSeconds(30);
private readonly IOptionsProvider optionsProvider = optionsProvider.ThrowIfNull();
private readonly IBuildTemplateManager buildTemplateManager = buildTemplateManager.ThrowIfNull();
private readonly IViewManager viewManager = viewManager.ThrowIfNull();
private readonly INotificationService notificationService = notificationService.ThrowIfNull();
private readonly ILaunchConfigurationService launchConfigurationService = launchConfigurationService.ThrowIfNull();
private readonly IDaybreakApiService daybreakApiService = daybreakApiService.ThrowIfNull();
private readonly IExperienceCalculator experienceCalculator = experienceCalculator.ThrowIfNull();
private readonly IOptionsMonitor<FocusViewOptions> options = options.ThrowIfNull();
private readonly ILogger<FocusView> logger = logger.ThrowIfNull();
private ScopedApiContext? apiContext;
private Process? process;
private CancellationTokenSource? cancellationSource = default;
private DateTime lastBuildCheck = DateTime.MinValue;
public CharacterComponentContext? CharacterComponentContext { get; private set; }
public CurrentMapComponentContext? CurrentMapComponentContext { get; private set; }
public PlayerResourcesComponentContext? PlayerResourcesComponentContext { get; private set; }
public QuestLogComponentContext? QuestLogComponentContext { get; private set; }
public TitleInformationComponentContext? TitleInformationComponentContext { get; private set; }
public VanquishComponentContext? VanquishComponentContext { get; private set; }
public BuildComponentContext? BuildComponentContext { get; private set; }
public string? BrowserSource { get; private set; } = "https://wiki.guildwars.com/wiki/Main_Page";
public override async ValueTask ParametersSet(FocusView view, CancellationToken cancellationToken)
{
var scopedLogger = this.logger.CreateScopedLogger();
var launchConfigId = view.ConfigurationId;
var daybreakLaunchConfig = this.launchConfigurationService.GetLaunchConfigurations()
.FirstOrDefault(x => x.Identifier == launchConfigId);
if (daybreakLaunchConfig is null)
{
scopedLogger.LogError("Launch configuration with ID {configId} not found", launchConfigId);
this.notificationService.NotifyError(
title: "Focus View Error",
description: $"Could not find launch configuration by id {launchConfigId}");
this.viewManager.ShowView<LaunchView>();
return;
}
if (!int.TryParse(view.ProcessId, out var processId))
{
scopedLogger.LogError("Process id is invalid {processId}", view.ProcessId);
this.notificationService.NotifyError(
title: "Focus View Error",
description: $"Could not find GuildWars process by id {processId}");
this.viewManager.ShowView<LaunchView>();
return;
}
try
{
this.process = Process.GetProcessById(processId);
}
catch(Exception ex)
{
scopedLogger.LogError(ex, "Encountered exception when fetching process with id {processId}", processId);
this.viewManager.ShowView<LaunchView>();
return;
}
var apiContext = await this.daybreakApiService.GetDaybreakApiContext(this.process, cancellationToken);
if (apiContext is null)
{
scopedLogger.LogError("Could not attach to GuildWars process");
this.notificationService.NotifyError(
title: "Focus View Error",
description: $"Could not attach to GuildWars process");
this.viewManager.ShowView<LaunchView>();
return;
}
this.apiContext = apiContext;
this.Initialize();
}
public async void OnCharacterChanged(CharacterSelectComponentEntry selectedCharacter)
{
if (this.apiContext is null ||
this.cancellationSource is null)
{
return;
}
this.SetupDefaultValues();
await this.apiContext.SwitchCharacter(selectedCharacter.CharacterName, this.cancellationSource.Token);
}
public void OnExperienceDisplayClicked()
{
var options = this.options.CurrentValue;
options.ExperienceDisplay = CycleValue(options.ExperienceDisplay);
this.optionsProvider.SaveOption(options);
if (this.CharacterComponentContext is null)
{
return;
}
this.CharacterComponentContext = new CharacterComponentContext
{
Characters = this.CharacterComponentContext.Characters,
CurrentCharacter = this.CharacterComponentContext.CurrentCharacter,
CurrentTotalExperience = this.CharacterComponentContext.CurrentTotalExperience,
ExperienceForCurrentLevel = this.CharacterComponentContext.ExperienceForCurrentLevel,
ExperienceForNextLevel = this.CharacterComponentContext.ExperienceForNextLevel,
NextExperienceThreshold = this.CharacterComponentContext.NextExperienceThreshold,
TotalExperienceForNextLevel = this.CharacterComponentContext.TotalExperienceForNextLevel,
ExperienceDisplay = options.ExperienceDisplay
};
this.RefreshView();
}
public void OnVanquishingDisplayClicked()
{
var options = this.options.CurrentValue;
options.VanquishingDisplay = CycleValue(options.VanquishingDisplay);
this.optionsProvider.SaveOption(options);
if (this.VanquishComponentContext is null)
{
return;
}
this.VanquishComponentContext = new VanquishComponentContext
{
Display = options.VanquishingDisplay,
FoesKilled = this.VanquishComponentContext.FoesKilled,
FoesToKill = this.VanquishComponentContext.FoesToKill,
HardMode = this.VanquishComponentContext.HardMode,
Vanquishing = this.VanquishComponentContext.Vanquishing
};
this.RefreshView();
}
public void OnCurrentMapClicked()
{
if (this.CurrentMapComponentContext?.CurrentMap is not Map map)
{
return;
}
this.BrowserSource = map.WikiUrl;
}
public void OnTitleClicked()
{
if (this.TitleInformationComponentContext?.Title is not Title title)
{
return;
}
this.BrowserSource = title.WikiUrl;
}
public void OnKurzickBarClicked()
{
}
public void OnLuxonBarClicked()
{
}
public void OnBalthazarBarClicked()
{
}
public void OnImperialBarClicked()
{
}
public void OnQuestClicked(QuestMetadata questMetadata)
{
if (questMetadata.Quest is null)
{
return;
}
this.BrowserSource = questMetadata.Quest.WikiUrl;
}
public async void OnBuildClicked(IBuildEntry buildEntry)
{
if (this.apiContext is null)
{
return;
}
if (buildEntry is SingleBuildEntry singleBuild)
{
var code = this.buildTemplateManager.EncodeTemplate(singleBuild);
await this.apiContext.PostMainPlayerBuild(code, this.cancellationSource?.Token ?? CancellationToken.None);
}
else if (buildEntry is TeamBuildEntry teamBuild)
{
var partyLoadout = this.buildTemplateManager.ConvertToPartyLoadout(teamBuild);
await this.apiContext.PostPartyLoadout(partyLoadout, this.cancellationSource?.Token ?? CancellationToken.None);
}
}
public async void OnPlayerSingleBuildClicked()
{
if (this.apiContext is null)
{
return;
}
var entry = await this.apiContext.GetMainPlayerBuild(this.cancellationSource?.Token ?? CancellationToken.None);
if (entry is null)
{
return;
}
var singleBuildEntry = this.buildTemplateManager.CreateSingleBuild(entry);
this.viewManager.ShowView<BuildRoutingView>((nameof(BuildRoutingView.BuildName), Uri.EscapeDataString(singleBuildEntry.Name ?? throw new InvalidOperationException())));
}
public async void OnPlayerTeamBuildClicked()
{
if (this.apiContext is null)
{
return;
}
var loadout = await this.apiContext.GetPartyLoadout(this.cancellationSource?.Token ?? CancellationToken.None);
if (loadout is null)
{
return;
}
var teamBuildEntry = this.buildTemplateManager.CreateTeamBuild(loadout);
this.viewManager.ShowView<BuildRoutingView>((nameof(BuildRoutingView.BuildName), Uri.EscapeDataString(teamBuildEntry.Name ?? throw new InvalidOperationException())));
}
private void ViewManager_ShowViewRequested(object? _, TrailBlazr.Models.ViewRequest e)
{
if (e.ViewModelType == typeof(FocusViewModel))
{
return;
}
this.Cleanup();
}
private void Initialize()
{
this.viewManager.ShowViewRequested += this.ViewManager_ShowViewRequested;
this.cancellationSource?.Cancel();
this.cancellationSource?.Dispose();
this.cancellationSource = new CancellationTokenSource();
var ct = this.cancellationSource.Token;
this.SetupDefaultValues();
Task.Factory.StartNew(() => this.PeriodicallyFetchGameInformation(ct), ct, TaskCreationOptions.LongRunning, TaskScheduler.Current);
}
private void Cleanup()
{
this.viewManager.ShowViewRequested -= this.ViewManager_ShowViewRequested;
this.cancellationSource?.Cancel();
this.cancellationSource?.Dispose();
this.cancellationSource = default;
}
private void SetupDefaultValues()
{
this.CharacterComponentContext = default;
this.CurrentMapComponentContext = default;
this.PlayerResourcesComponentContext = default;
this.QuestLogComponentContext = default;
this.TitleInformationComponentContext = default;
this.VanquishComponentContext = default;
this.BuildComponentContext = default;
this.RefreshView();
}
private async Task PeriodicallyFetchGameInformation(CancellationToken cancellationToken)
{
var scopedLogger = this.logger.CreateScopedLogger();
var retryCount = 0;
while (!cancellationToken.IsCancellationRequested)
{
if (cancellationToken.IsCancellationRequested)
{
scopedLogger.LogInformation("Cancellation requested, stopping game information fetch loop");
this.viewManager.ShowView<LaunchView>();
return;
}
if (this.apiContext is null || this.process is null || this.process.HasExited)
{
scopedLogger.LogInformation("Process has exited or API context is null, stopping game information fetch loop");
this.viewManager.ShowView<LaunchView>();
return;
}
try
{
if (!await this.FetchGameInformation(cancellationToken))
{
retryCount++;
}
else
{
retryCount = 0;
await this.RefreshViewAsync();
}
}
catch (Exception ex)
{
scopedLogger.LogError(ex, "Error occurred while fetching game information");
retryCount++;
}
if (retryCount >= MaxRetryAttempts)
{
this.viewManager.ShowView<LaunchView>();
return;
}
await Task.Delay(GameInfoFrequency, cancellationToken);
}
}
private async ValueTask<bool> FetchGameInformation(CancellationToken cancellationToken)
{
var scopedLogger = this.logger.CreateScopedLogger();
if (this.apiContext is null)
{
return false;
}
var timeoutCts = new CancellationTokenSource(GameInfoTimeout);
var timeoutTokenSource = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken, timeoutCts.Token);
var timeoutToken = timeoutTokenSource.Token;
var characterListTask = this.apiContext.GetCharacters(timeoutToken);
var mainPlayerInstanceTask = this.apiContext.GetMainPlayerInstanceInfo(timeoutToken);
var mainPlayerStateTask = this.apiContext.GetMainPlayerState(timeoutToken);
var titleInfoTask = this.apiContext.GetTitleInfo(timeoutToken);
var questLogTask = this.apiContext.GetMainPlayerQuestLog(timeoutToken);
var mainPlayerBuildContextTask = this.apiContext.GetMainPlayerBuildContext(timeoutToken);
try
{
await Task.WhenAny([
characterListTask,
mainPlayerInstanceTask,
mainPlayerStateTask,
titleInfoTask,
questLogTask,
mainPlayerBuildContextTask
]);
}
catch (TaskCanceledException)
{
scopedLogger.LogWarning("Timeout occurred while fetching game information");
return false;
}
catch (Exception ex)
{
scopedLogger.LogError(ex, "Error fetching game information");
return false;
}
var characterList = await characterListTask;
var mainPlayerInstance = await mainPlayerInstanceTask;
var mainPlayerState = await mainPlayerStateTask;
var titleInformation = await titleInfoTask;
var questLog = await questLogTask;
var mainPlayerBuildContext = await mainPlayerBuildContextTask;
if (mainPlayerInstance is null ||
mainPlayerInstance.Type is Shared.Models.Api.InstanceType.Loading)
{
this.SetupDefaultValues();
return true;
}
this.CharacterComponentContext = this.ParseCharacterComponentContext(characterList, mainPlayerState);
this.CurrentMapComponentContext = ParseCurrentMapComponentContext(mainPlayerInstance);
this.PlayerResourcesComponentContext = ParsePlayerResourcesContext(mainPlayerState);
this.QuestLogComponentContext = ParseQuestLogComponentContext(questLog);
this.TitleInformationComponentContext = ParseTitleInformationComponentContext(titleInformation);
this.VanquishComponentContext = this.ParseVanquishComponentContext(mainPlayerInstance);
this.BuildComponentContext = await this.ParseBuildComponentContext(mainPlayerInstance, mainPlayerBuildContext, cancellationToken);
return true;
}
private CharacterComponentContext? ParseCharacterComponentContext(CharacterSelectInformation? characterSelectInformation, MainPlayerState? mainPlayerState)
{
if (characterSelectInformation is null ||
characterSelectInformation.CharacterNames is null ||
characterSelectInformation.CurrentCharacter is null)
{
return default;
}
var parsedCharacters = characterSelectInformation.CharacterNames.Select(entry =>
{
if (!Profession.TryParse((int)entry.Primary, out var primaryProfession))
{
primaryProfession = Profession.None;
}
if (!Profession.TryParse((int)entry.Secondary, out var secondaryProfession))
{
secondaryProfession = Profession.None;
}
return new CharacterSelectComponentEntry
{
CharacterName = entry.Name,
DisplayName = $"{primaryProfession.Alias}{(secondaryProfession != Profession.None && secondaryProfession is not null ? $"/{secondaryProfession.Alias}" : "")} {entry.Name}"
};
}).ToList();
var selectedCharacter = parsedCharacters.FirstOrDefault(x => x.CharacterName == characterSelectInformation.CurrentCharacter.Name);
if (selectedCharacter is null)
{
return default;
}
var currentExperience = mainPlayerState?.CurrentExperience ?? 0U;
var experienceForCurrentLevel = this.experienceCalculator.GetExperienceForCurrentLevel(currentExperience);
var experienceForNextLevel = this.experienceCalculator.GetRemainingExperienceForNextLevel(currentExperience);
var nextExperienceThreshold = this.experienceCalculator.GetNextExperienceThreshold(currentExperience);
var totalExperienceForNextLevel = this.experienceCalculator.GetTotalExperienceForNextLevel(currentExperience);
return new CharacterComponentContext
{
Characters = parsedCharacters,
CurrentCharacter = selectedCharacter,
CurrentTotalExperience = currentExperience,
ExperienceForCurrentLevel = experienceForCurrentLevel,
ExperienceForNextLevel = experienceForNextLevel,
NextExperienceThreshold = nextExperienceThreshold,
TotalExperienceForNextLevel = totalExperienceForNextLevel,
ExperienceDisplay = this.options.CurrentValue.ExperienceDisplay
};
}
private async Task<BuildComponentContext?> ParseBuildComponentContext(InstanceInfo? instanceInfo, MainPlayerBuildContext? mainPlayerBuildContext, CancellationToken cancellationToken)
{
if (instanceInfo is null || mainPlayerBuildContext is null)
{
return default;
}
if (this.BuildComponentContext?.CharacterUnlockedSkills.SequenceEqual(mainPlayerBuildContext.UnlockedCharacterSkills) is true &&
this.BuildComponentContext?.UnlockedProfessions == mainPlayerBuildContext.UnlockedProfessions &&
this.BuildComponentContext?.PrimaryProfessionId == mainPlayerBuildContext.PrimaryProfessionId &&
DateTime.Now < this.lastBuildCheck + BuildsCheckFrequency)
{
return this.BuildComponentContext;
}
var matchingBuilds = await this.buildTemplateManager.GetBuilds()
.Where(b =>
{
if (b is SingleBuildEntry singleBuild &&
this.buildTemplateManager.CanApply(mainPlayerBuildContext, singleBuild))
{
return true;
}
else if (b is TeamBuildEntry teamBuild &&
this.buildTemplateManager.CanApply(mainPlayerBuildContext, teamBuild))
{
return true;
}
return false;
}).ToListAsync(cancellationToken);
this.lastBuildCheck = DateTime.Now;
return new BuildComponentContext
{
IsInOutpost = instanceInfo.Type is Shared.Models.Api.InstanceType.Outpost,
PrimaryProfessionId = mainPlayerBuildContext.PrimaryProfessionId,
CharacterUnlockedSkills = mainPlayerBuildContext.UnlockedCharacterSkills,
UnlockedProfessions = mainPlayerBuildContext.UnlockedProfessions,
AvailableBuilds = matchingBuilds
};
}
private static CurrentMapComponentContext? ParseCurrentMapComponentContext(InstanceInfo? mainPlayerInstance)
{
_ = Map.TryParse(((int?)mainPlayerInstance?.MapId) ?? -1, out var currentMap);
if (currentMap is null)
{
return default;
}
return new CurrentMapComponentContext { CurrentMap = currentMap };
}
private static PlayerResourcesComponentContext? ParsePlayerResourcesContext(MainPlayerState? mainPlayerState)
{
if (mainPlayerState is null)
{
return default;
}
return new PlayerResourcesComponentContext
{
CurrentBalthazar = mainPlayerState.CurrentBalthazar,
MaxBalthazar = mainPlayerState.MaxBalthazar,
TotalBalthazar = mainPlayerState.TotalBalthazar,
CurrentImperial = mainPlayerState.CurrentImperial,
MaxImperial = mainPlayerState.MaxImperial,
TotalImperial = mainPlayerState.TotalImperial,
CurrentKurzick = mainPlayerState.CurrentKurzick,
MaxKurzick = mainPlayerState.MaxKurzick,
TotalKurzick = mainPlayerState.TotalKurzick,
CurrentLuxon = mainPlayerState.CurrentLuxon,
MaxLuxon = mainPlayerState.MaxLuxon,
TotalLuxon = mainPlayerState.TotalLuxon
};
}
private static QuestLogComponentContext? ParseQuestLogComponentContext(QuestLogInformation? questLog)
{
if (questLog is null)
{
return default;
}
var currentQuestEntry = questLog.Quests.FirstOrDefault(q => q.QuestId == questLog.CurrentQuestId);
QuestMetadata? currentQuestMeta = default;
if (currentQuestEntry is not null)
{
_ = Quest.TryParse((int)questLog.CurrentQuestId, out var currentQuest);
_ = Map.TryParse((int)currentQuestEntry.MapFrom, out var mapFrom);
currentQuestMeta = new QuestMetadata
{
From = mapFrom,
Quest = currentQuest
};
}
return new QuestLogComponentContext
{
CurrentQuest = currentQuestMeta,
Quests = [.. questLog.Quests
.Where(q => q.QuestId != questLog.CurrentQuestId)
.Select(quest =>
{
if (!Quest.TryParse((int)quest.QuestId, out var parsedQuest) ||
!Map.TryParse((int)quest.MapFrom, out var mapFrom))
{
return default;
}
return new QuestMetadata { From = mapFrom, Quest = parsedQuest };
})
.OfType<QuestMetadata>()]
};
}
private static TitleInformationComponentContext? ParseTitleInformationComponentContext(TitleInfo? titleInfo)
{
if (titleInfo is null ||
!Title.TryParse((int)titleInfo.Id, out var title))
{
return default;
}
return new TitleInformationComponentContext
{
Title = title,
CurrentPoints = titleInfo.CurrentPoints,
IsPercentage = titleInfo.IsPercentage,
MaxTierNumber = titleInfo.MaxTierNumber,
TierNumber = titleInfo.TierNumber,
PointsForCurrentRank = titleInfo.PointsForCurrentRank,
PointsForNextRank = titleInfo.PointsForNextRank
};
}
private VanquishComponentContext? ParseVanquishComponentContext(InstanceInfo? instanceInfo)
{
if (instanceInfo is null)
{
return default;
}
return new VanquishComponentContext
{
FoesKilled = instanceInfo.FoesKilled,
FoesToKill = instanceInfo.FoesToKill,
HardMode = instanceInfo.Difficulty is DifficultyInfo.Hard,
Vanquishing = (instanceInfo.FoesToKill + instanceInfo.FoesKilled > 0U) && instanceInfo.Difficulty is DifficultyInfo.Hard,
Display = this.options.CurrentValue.VanquishingDisplay
};
}
private static PointsDisplay CycleValue(PointsDisplay points)
{
return points switch
{
PointsDisplay.CurrentAndMax => PointsDisplay.Remaining,
PointsDisplay.Remaining => PointsDisplay.Percentage,
PointsDisplay.Percentage => PointsDisplay.CurrentAndMax,
_ => throw new InvalidOperationException()
};
}
private static ExperienceDisplay CycleValue(ExperienceDisplay display)
{
return display switch
{
ExperienceDisplay.CurrentLevelCurrentAndCurrentLevelMax => ExperienceDisplay.Percentage,
ExperienceDisplay.Percentage => ExperienceDisplay.RemainingUntilNextLevel,
ExperienceDisplay.RemainingUntilNextLevel => ExperienceDisplay.TotalCurretAndTotalMax,
ExperienceDisplay.TotalCurretAndTotalMax => ExperienceDisplay.CurrentLevelCurrentAndCurrentLevelMax,
_ => throw new InvalidOperationException()
};
}
}