Compare commits

...
7 Commits
Author SHA1 Message Date
amacocianandGitHub 986d09867c Cache duplication fix (#292)
* Cache duplication fix
Performance improvement
Closes #290

* Increment version
2023-06-29 10:08:00 +02:00
amacocianandGitHub 97cb2151f9 Memory reader cache to better control memory access (#289)
Expand guildwars locations, campaign, continent
Make bloogum use image cache
Make bloogum show images based on character location
Link Campaign -> Continent -> Region -> Map
2023-06-28 15:32:19 +00:00
amacocianandGitHub 338ac100a1 Realm of torment npcs (#288) 2023-06-27 16:55:44 +00:00
amacocianandGitHub 01badcee6a More nightfall npcs (#287) 2023-06-27 11:26:25 +00:00
amacocianandGitHub 302cf1114c Fix bug with trader messages options (#286)
Reorder options
2023-06-26 19:05:13 +00:00
amacocianandGitHub 613e03dc75 High def skill icons (#284)
* Use high-definition skill icons

* Increment version
2023-06-26 17:42:38 +00:00
amacocianandGitHub 4267359cb2 DSOAL support (#282)
Reworked mod support
2023-06-26 13:16:29 +00:00
85 changed files with 3568 additions and 1053 deletions
@@ -0,0 +1,15 @@
using Daybreak.Attributes;
using Newtonsoft.Json;
namespace Daybreak.Configuration.Options;
[OptionsName(Name = "DSOAL")]
public sealed class DSOALOptions
{
[JsonProperty(nameof(Path))]
[OptionName(Name = "Path", Description = "The path to the DSOAL installation")]
public string? Path { get; set; }
[JsonProperty(nameof(Enabled))]
[OptionName(Name = "Enabled", Description = "If true, the launcher will also launch DSOAL when launching GuildWars")]
public bool Enabled { get; set; }
}
@@ -11,14 +11,12 @@ public sealed class FocusViewOptions
[OptionName(Name = "Enabled", Description = "If true, the focus view is enabled, showing live information from the game")]
public bool Enabled { get; set; }
[JsonProperty(nameof(MemoryReaderFrequency))]
[OptionRange<double>(MinValue = 0, MaxValue = 1000)]
[OptionName(Name = "Memory Reader Frequency", Description = "Measured in ms. Sets how often should the launcher poll information from the game. Actual frequency is capped by the memory reading speed")]
public double MemoryReaderFrequency { get; set; } = 0;
[OptionName(Name = "Inventory Component Enabled", Description = "If true, the focus view will show a component with the inventory contents")]
public bool InventoryComponentVisible { get; set; }
[OptionName(Name = "Minimap Component Enabled", Description = "If true, the focus view will show a minimap component")]
public bool MinimapComponentVisible { get; set; }
[JsonProperty(nameof(ExperienceDisplay))]
[OptionName(Name = "Experience Display Mode", Description = "Sets how should the experience display show the information")]
public ExperienceDisplay ExperienceDisplay { get; set; }
@@ -0,0 +1,13 @@
using Daybreak.Attributes;
using Newtonsoft.Json;
namespace Daybreak.Configuration.Options;
[OptionsName(Name = "Memory Reader")]
public sealed class MemoryReaderOptions
{
[JsonProperty(nameof(MemoryReaderFrequency))]
[OptionRange<double>(MinValue = 0, MaxValue = 1000)]
[OptionName(Name = "Memory Reader Frequency", Description = "Measured in ms. Sets how often should the launcher polls information from the game. Actual frequency is capped by the memory reading speed")]
public double MemoryReaderFrequency { get; set; } = 0;
}
@@ -1,7 +1,9 @@
using Daybreak.Services.TradeChat.Models;
using Daybreak.Attributes;
using Daybreak.Services.TradeChat.Models;
namespace Daybreak.Configuration.Options;
[OptionsIgnore]
public sealed class TraderMessagesOptions : ILiteCollectionOptions<TraderMessageDTO>
{
public string CollectionName => "trader_messages";
+19 -2
View File
@@ -67,6 +67,9 @@ using Daybreak.Services.Notifications.Models;
using Daybreak.Models.Notifications.Handling;
using Daybreak.Services.TradeChat.Notifications;
using Daybreak.Views.Copy;
using Daybreak.Services.DSOAL;
using Daybreak.Services.Mods;
using Daybreak.Views.Onboarding.DSOAL;
namespace Daybreak.Configuration;
@@ -173,6 +176,8 @@ public static class ProjectConfiguration
services.AddSingleton<IConnectivityStatus, ConnectivityStatus>();
services.AddSingleton<INotificationStorage, NotificationStorage>();
services.AddSingleton<ITradeAlertingService, TradeAlertingService>();
services.AddSingleton<IModsManager, ModsManager>();
services.AddSingleton<IGuildwarsMemoryCache, GuildwarsMemoryCache>();
services.AddScoped<ICredentialManager, CredentialManager>();
services.AddScoped<IApplicationLauncher, ApplicationLauncher>();
services.AddScoped<IScreenshotProvider, ScreenshotProvider>();
@@ -195,8 +200,6 @@ public static class ProjectConfiguration
services.AddScoped<IPathfinder, StupidPathfinder>();
services.AddScoped<IDrawingService, DrawingService>();
services.AddScoped<IDrawingModuleProducer, DrawingService>(sp => sp.GetRequiredService<IDrawingService>().As<DrawingService>()!);
services.AddScoped<IUModService, UModService>();
services.AddScoped<IToolboxService, ToolboxService>();
services.AddScoped<ITradeChatService<KamadanTradeChatOptions>, TradeChatService<KamadanTradeChatOptions>>();
services.AddScoped<ITradeChatService<AscalonTradeChatOptions>, TradeChatService<AscalonTradeChatOptions>>();
services.AddScoped<ITraderQuoteService, TraderQuoteService>();
@@ -250,6 +253,11 @@ public static class ProjectConfiguration
viewProducer.RegisterView<TradeNotificationView>();
viewProducer.RegisterView<GuildwarsCopySelectionView>();
viewProducer.RegisterView<GuildwarsCopyView>();
viewProducer.RegisterView<DSOALInstallingView>();
viewProducer.RegisterView<DSOALOnboardingEntryView>();
viewProducer.RegisterView<DSOALSwitchView>();
viewProducer.RegisterView<DSOALHomepageView>();
viewProducer.RegisterView<DSOALBrowserView>();
}
public static void RegisterStartupActions(IStartupActionProducer startupActionProducer)
@@ -321,9 +329,11 @@ public static class ProjectConfiguration
optionsProducer.RegisterOptions<BrowserOptions>();
optionsProducer.RegisterOptions<BuildSynchronizationOptions>();
optionsProducer.RegisterOptions<FocusViewOptions>();
optionsProducer.RegisterOptions<MemoryReaderOptions>();
optionsProducer.RegisterOptions<LauncherOptions>();
optionsProducer.RegisterOptions<ToolboxOptions>();
optionsProducer.RegisterOptions<UModOptions>();
optionsProducer.RegisterOptions<DSOALOptions>();
optionsProducer.RegisterOptions<ScreenManagerOptions>();
optionsProducer.RegisterOptions<KamadanTradeChatOptions>();
optionsProducer.RegisterOptions<AscalonTradeChatOptions>();
@@ -351,6 +361,13 @@ public static class ProjectConfiguration
notificationHandlerProducer.RegisterNotificationHandler<TradeMessageNotificationHandler>();
}
public static void RegisterMods(IModsManager modsManager)
{
modsManager.RegisterMod<IToolboxService, ToolboxService>();
modsManager.RegisterMod<IUModService, UModService>();
modsManager.RegisterMod<IDSOALService, DSOALService>();
}
private static void RegisterLiteCollection<TCollectionType, TOptionsType>(IServiceCollection services)
where TOptionsType : class, ILiteCollectionOptions<TCollectionType>
{
@@ -281,18 +281,19 @@ public partial class PlayerResourcesComponent : UserControl
private void UpdateGameData()
{
if (this.DataContext is not GameData gameData ||
gameData.MainPlayer is not MainPlayerInformation)
gameData.MainPlayer is null ||
gameData.Session is null)
{
return;
}
this.CurrentExperienceInLevel = this.experienceCalculator.GetExperienceForCurrentLevel(gameData.MainPlayer!.Value.Experience);
this.NextLevelExperienceThreshold = this.experienceCalculator.GetNextExperienceThreshold(gameData.MainPlayer!.Value.Experience);
this.TotalFoes = (int)(gameData.Session!.Value.FoesKilled + gameData.Session.Value.FoesToKill);
this.Vanquishing = gameData.Session.Value.FoesToKill + gameData.Session.Value.FoesKilled > 0U;
this.TitleActive = gameData.MainPlayer.Value.TitleInformation is not null && gameData.MainPlayer.Value.TitleInformation.Value.IsValid;
this.CurrentExperienceInLevel = this.experienceCalculator.GetExperienceForCurrentLevel(gameData.MainPlayer.Experience);
this.NextLevelExperienceThreshold = this.experienceCalculator.GetNextExperienceThreshold(gameData.MainPlayer.Experience);
this.TotalFoes = (int)(gameData.Session.FoesKilled + gameData.Session.FoesToKill);
this.Vanquishing = gameData.Session.FoesToKill + gameData.Session.FoesKilled > 0U;
this.TitleActive = gameData.MainPlayer.TitleInformation is not null && gameData.MainPlayer.TitleInformation.IsValid;
if (gameData.MainPlayer.Value.TitleInformation is TitleInformation titleInformation && titleInformation.IsValid)
if (gameData.MainPlayer.TitleInformation is TitleInformation titleInformation && titleInformation.IsValid)
{
if (titleInformation.MaxTierNumber == titleInformation.TierNumber)
{
@@ -324,7 +325,8 @@ public partial class PlayerResourcesComponent : UserControl
private void UpdateExperienceText()
{
if (this.DataContext is not GameData gameData)
if (this.DataContext is not GameData gameData ||
gameData.MainPlayer is null)
{
return;
}
@@ -332,22 +334,22 @@ public partial class PlayerResourcesComponent : UserControl
switch (this.liveOptions.Value.ExperienceDisplay)
{
case Configuration.FocusView.ExperienceDisplay.CurrentLevelCurrentAndCurrentLevelMax:
var currentExperienceInLevel = this.experienceCalculator.GetExperienceForCurrentLevel(gameData.MainPlayer!.Value.Experience);
var nextLevelExperienceThreshold = this.experienceCalculator.GetNextExperienceThreshold(gameData.MainPlayer!.Value.Experience);
var currentExperienceInLevel = this.experienceCalculator.GetExperienceForCurrentLevel(gameData.MainPlayer.Experience);
var nextLevelExperienceThreshold = this.experienceCalculator.GetNextExperienceThreshold(gameData.MainPlayer.Experience);
this.ExperienceBarText = $"{(int)currentExperienceInLevel} / {(int)nextLevelExperienceThreshold} XP";
break;
case Configuration.FocusView.ExperienceDisplay.TotalCurretAndTotalMax:
var currentTotalExperience = gameData.MainPlayer!.Value.Experience;
var currentTotalExperience = gameData.MainPlayer.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(gameData.MainPlayer!.Value.Experience);
var remainingExperience = this.experienceCalculator.GetRemainingExperienceForNextLevel(gameData.MainPlayer.Experience);
this.ExperienceBarText = $"Remaining {(int)remainingExperience} XP";
break;
case Configuration.FocusView.ExperienceDisplay.Percentage:
var currentExperienceInLevel2 = this.experienceCalculator.GetExperienceForCurrentLevel(gameData.MainPlayer!.Value.Experience);
var nextLevelExperienceThreshold2 = this.experienceCalculator.GetNextExperienceThreshold(gameData.MainPlayer!.Value.Experience);
var currentExperienceInLevel2 = this.experienceCalculator.GetExperienceForCurrentLevel(gameData.MainPlayer.Experience);
var nextLevelExperienceThreshold2 = this.experienceCalculator.GetNextExperienceThreshold(gameData.MainPlayer.Experience);
this.ExperienceBarText = $"{(int)((double)currentExperienceInLevel2 / (double)nextLevelExperienceThreshold2 * 100)}% XP";
break;
}
@@ -439,7 +441,8 @@ public partial class PlayerResourcesComponent : UserControl
private void UpdateVanquishingText()
{
if (this.DataContext is not GameData gameData)
if (this.DataContext is not GameData gameData ||
gameData.Session is null)
{
return;
}
@@ -447,20 +450,21 @@ public partial class PlayerResourcesComponent : UserControl
switch (this.liveOptions.Value.VanquishingDisplay)
{
case Configuration.FocusView.PointsDisplay.CurrentAndMax:
this.VanquishingText = $"{gameData.Session!.Value.FoesKilled} / {(int)this.TotalFoes} Foes Killed";
this.VanquishingText = $"{gameData.Session.FoesKilled} / {(int)this.TotalFoes} Foes Killed";
break;
case Configuration.FocusView.PointsDisplay.Remaining:
this.VanquishingText = $"Remaining {gameData.Session!.Value.FoesToKill} Foes";
this.VanquishingText = $"Remaining {gameData.Session.FoesToKill} Foes";
break;
case Configuration.FocusView.PointsDisplay.Percentage:
this.VanquishingText = $"{(int)((double)gameData.Session!.Value.FoesKilled / (double)this.TotalFoes * 100)}% Foes Killed";
this.VanquishingText = $"{(int)((double)gameData.Session.FoesKilled / (double)this.TotalFoes * 100)}% Foes Killed";
break;
}
}
private void UpdateHealthText()
{
if (this.DataContext is not GameData gameData)
if (this.DataContext is not GameData gameData ||
gameData.MainPlayer is null)
{
return;
}
@@ -468,20 +472,21 @@ public partial class PlayerResourcesComponent : UserControl
switch (this.liveOptions.Value.HealthDisplay)
{
case Configuration.FocusView.PointsDisplay.CurrentAndMax:
this.HealthBarText = $"{(int)gameData.MainPlayer!.Value.CurrentHealth} / {(int)gameData.MainPlayer.Value.MaxHealth} Health";
this.HealthBarText = $"{(int)gameData.MainPlayer.CurrentHealth} / {(int)gameData.MainPlayer.MaxHealth} Health";
break;
case Configuration.FocusView.PointsDisplay.Remaining:
this.HealthBarText = $"Remaining {(int)gameData.MainPlayer!.Value.CurrentHealth} Health";
this.HealthBarText = $"Remaining {(int)gameData.MainPlayer.CurrentHealth} Health";
break;
case Configuration.FocusView.PointsDisplay.Percentage:
this.HealthBarText = $"{(int)(gameData.MainPlayer!.Value.CurrentHealth / gameData.MainPlayer.Value.MaxHealth * 100)}% Health";
this.HealthBarText = $"{(int)(gameData.MainPlayer.CurrentHealth / gameData.MainPlayer.MaxHealth * 100)}% Health";
break;
}
}
private void UpdateEnergyText()
{
if (this.DataContext is not GameData gameData)
if (this.DataContext is not GameData gameData ||
gameData.MainPlayer is null)
{
return;
}
@@ -489,13 +494,13 @@ public partial class PlayerResourcesComponent : UserControl
switch (this.liveOptions.Value.EnergyDisplay)
{
case Configuration.FocusView.PointsDisplay.CurrentAndMax:
this.EnergyBarText = $"{(int)gameData.MainPlayer!.Value.CurrentEnergy} / {(int)gameData.MainPlayer.Value.MaxEnergy} Energy";
this.EnergyBarText = $"{(int)gameData.MainPlayer.CurrentEnergy} / {(int)gameData.MainPlayer.MaxEnergy} Energy";
break;
case Configuration.FocusView.PointsDisplay.Remaining:
this.EnergyBarText = $"Remaining {(int)gameData.MainPlayer!.Value.CurrentEnergy} Energy";
this.EnergyBarText = $"Remaining {(int)gameData.MainPlayer.CurrentEnergy} Energy";
break;
case Configuration.FocusView.PointsDisplay.Percentage:
this.EnergyBarText = $"{(int)(gameData.MainPlayer!.Value.CurrentEnergy / gameData.MainPlayer.Value.MaxEnergy * 100)}% Energy";
this.EnergyBarText = $"{(int)(gameData.MainPlayer.CurrentEnergy / gameData.MainPlayer.MaxEnergy * 100)}% Energy";
break;
}
}
@@ -43,7 +43,7 @@ public partial class LivingEntityContextMenu : UserControl
private void NpcDefinitionTextBlock_MouseLeftButtonDown(object _, MouseButtonEventArgs e)
{
this.LivingEntityContextMenuClicked?.Invoke(this, this.DataContext as LivingEntity? ?? default);
this.LivingEntityContextMenuClicked?.Invoke(this, this.DataContext as LivingEntity ?? default);
}
private void PrimaryProfessionTextBlock_MouseLeftButtonDown(object _, MouseButtonEventArgs e)
+1 -1
View File
@@ -19,6 +19,6 @@ public partial class MapIconContextMenu : UserControl
private void TextBlock_MouseLeftButtonDown(object _, MouseButtonEventArgs e)
{
this.MapIconContextMenuClicked?.Invoke(this, this.DataContext as MapIcon? ?? default);
this.MapIconContextMenuClicked?.Invoke(this, this.DataContext as MapIcon ?? default);
}
}
+8 -1
View File
@@ -116,8 +116,15 @@
Cursor="Hand"
Clicked="ToolboxButton_Clicked">
</buttons:MenuButton>
<buttons:MenuButton Title="DSOAL"
Foreground="{DynamicResource MahApps.Brushes.ThemeForeground}"
HighlightColor="{DynamicResource MahApps.Brushes.Accent}"
Height="30"
Cursor="Hand"
Clicked="DSOALButton_Clicked">
</buttons:MenuButton>
</local:ExpandableMenuSection.Children>
</local:ExpandableMenuSection>
</local:ExpandableMenuSection>
<local:ExpandableMenuSection
SectionTitle="Settings" Foreground="{DynamicResource MahApps.Brushes.ThemeForeground}"
FontSize="16">
+6
View File
@@ -4,6 +4,7 @@ using Daybreak.Services.Navigation;
using Daybreak.Services.Notifications;
using Daybreak.Views;
using Daybreak.Views.Copy;
using Daybreak.Views.Onboarding.DSOAL;
using Daybreak.Views.Onboarding.Toolbox;
using Daybreak.Views.Onboarding.UMod;
using Daybreak.Views.Trade;
@@ -109,6 +110,11 @@ public partial class MenuList : UserControl
this.viewManager.ShowView<ToolboxOnboardingEntryView>();
}
private void DSOALButton_Clicked(object sender, EventArgs e)
{
this.viewManager.ShowView<DSOALOnboardingEntryView>();
}
private void KamadanButton_Clicked(object sender, EventArgs e)
{
this.viewManager.ShowView<KamadanTradeChatView>();
@@ -86,7 +86,7 @@ public partial class GuildwarsMinimap : UserControl
public event EventHandler<LivingEntity>? LivingEntityClicked;
public event EventHandler<PlayerInformation>? PlayerInformationClicked;
public event EventHandler<MapIcon>? MapIconClicked;
public event EventHandler<Profession?> ProfessionClicked;
public event EventHandler<Profession>? ProfessionClicked;
public GuildwarsMinimap()
:this(
@@ -119,6 +119,11 @@ public partial class GuildwarsMinimap : UserControl
protected override void OnPropertyChanged(DependencyPropertyChangedEventArgs e)
{
base.OnPropertyChanged(e);
if (!this.IsEnabled)
{
return;
}
if (e.Property == ActualHeightProperty ||
e.Property == ActualWidthProperty)
{
@@ -146,7 +151,8 @@ public partial class GuildwarsMinimap : UserControl
private void UpdateGameData()
{
if(this.GameData.Valid is false)
if(this.GameData is null ||
this.GameData.Valid is false)
{
return;
}
@@ -162,7 +168,7 @@ public partial class GuildwarsMinimap : UserControl
}
this.TargetEntityId = this.GameData.Session?.CurrentTargetId ?? 0;
this.TargetEntityModelId = (int?)this.GameData.LivingEntities?.FirstOrDefault(e => e.Id == this.TargetEntityId).ModelType ?? 0;
this.TargetEntityModelId = (int?)this.GameData.LivingEntities?.FirstOrDefault(e => e.Id == this.TargetEntityId)?.ModelType ?? 0;
var screenVirtualWidth = this.ActualWidth / this.Zoom;
var screenVirtualHeight = this.ActualHeight / this.Zoom;
var position = debounceResponse.MainPlayer.Position!.Value;
@@ -202,7 +208,8 @@ public partial class GuildwarsMinimap : UserControl
private void DrawMap()
{
if (this.PathingData.Trapezoids is null)
if (this.PathingData is null ||
this.PathingData.Trapezoids is null)
{
return;
}
@@ -582,23 +589,26 @@ public partial class GuildwarsMinimap : UserControl
private void GuildwarsMinimap_MouseMove(object sender, MouseEventArgs e)
{
if (this.GameData.Valid is false)
if (this.GameData.Valid is false ||
this.GameData.MainPlayer is null ||
this.GameData.Party is null ||
this.GameData.WorldPlayers is null)
{
return;
}
this.DragMinimap();
if (this.CheckMouseOverEntity(this.GameData.Party!.OfType<IEntity>()) is not null)
if (this.CheckMouseOverEntity(this.GameData.Party.OfType<IEntity>()) is not null)
{
return;
}
if (this.CheckMouseOverEntity(this.GameData.WorldPlayers!.OfType<IEntity>()) is not null)
if (this.CheckMouseOverEntity(this.GameData.WorldPlayers.OfType<IEntity>()) is not null)
{
return;
}
if (this.CheckMouseOverEntity(Enumerable.Repeat(this.GameData.MainPlayer.As<IEntity>(), 1)) is not null)
if (this.CheckMouseOverEntity(Enumerable.Repeat(this.GameData.MainPlayer.Cast<IEntity>(), 1)) is not null)
{
return;
}
@@ -613,14 +623,14 @@ public partial class GuildwarsMinimap : UserControl
return;
}
if (this.CheckMouseOverEntity(this.GameData.MainPlayer!.Value.QuestLog!
if (this.CheckMouseOverEntity(this.GameData.MainPlayer.QuestLog!
.Where(entity => this.drawingService.IsEntityOnScreen(entity.Position, out _, out _))
.OfType<IPositionalEntity>()) is not null)
{
return;
}
if (this.CheckMouseOverEntity(this.GameData.MainPlayer!.Value.QuestLog!
if (this.CheckMouseOverEntity(this.GameData.MainPlayer.QuestLog!
.Where(entity => !this.drawingService.IsEntityOnScreen(entity.Position, out _, out _))
.Select(oldQuestMetadata =>
{
@@ -665,32 +675,32 @@ public partial class GuildwarsMinimap : UserControl
private void QuestContextMenu_QuestContextMenuClicked(object _, QuestMetadata? quest)
{
if (quest is not QuestMetadata)
if (quest is null)
{
return;
}
this.QuestMetadataClicked?.Invoke(this, quest.Value);
this.QuestMetadataClicked?.Invoke(this, quest);
}
private void PlayerContextMenu_PlayerContextMenuClicked(object _, PlayerInformation? playerInformation)
{
if (playerInformation is not PlayerInformation)
if (playerInformation is null)
{
return;
}
this.PlayerInformationClicked?.Invoke(this, playerInformation.Value);
this.PlayerInformationClicked?.Invoke(this, playerInformation);
}
private void LivingEntityContextMenu_LivingEntityContextMenuClicked(object _, LivingEntity? livingEntity)
{
if (livingEntity is not LivingEntity)
if (livingEntity is null)
{
return;
}
this.LivingEntityClicked?.Invoke(this, livingEntity.Value);
this.LivingEntityClicked?.Invoke(this, livingEntity);
}
private void LivingEntityContextMenu_LivingEntityProfessionContextMenuClicked(object _, Profession? e)
@@ -705,12 +715,12 @@ public partial class GuildwarsMinimap : UserControl
private void MapIconContextMenu_MapIconContextMenuClicked(object _, MapIcon? mapIcon)
{
if (mapIcon is not MapIcon)
if (mapIcon is null)
{
return;
}
this.MapIconClicked?.Invoke(this, mapIcon.Value);
this.MapIconClicked?.Invoke(this, mapIcon);
}
private void MaximizeButton_Clicked(object sender, EventArgs e)
+1 -1
View File
@@ -18,6 +18,6 @@ public partial class PlayerContextMenu : UserControl
private void TextBlock_MouseLeftButtonDown(object _, MouseButtonEventArgs e)
{
this.PlayerContextMenuClicked?.Invoke(this, this.DataContext as PlayerInformation? ?? default);
this.PlayerContextMenuClicked?.Invoke(this, this.DataContext as PlayerInformation ?? default);
}
}
+1 -1
View File
@@ -19,6 +19,6 @@ public partial class QuestContextMenu : UserControl
private void TextBlock_MouseLeftButtonDown(object _, MouseButtonEventArgs e)
{
this.QuestContextMenuClicked?.Invoke(this, this.DataContext as QuestMetadata? ?? default);
this.QuestContextMenuClicked?.Invoke(this, this.DataContext as QuestMetadata ?? default);
}
}
@@ -9,7 +9,6 @@ using System.Windows;
using System.Windows.Controls;
using System.Windows.Extensions;
using System.Windows.Media;
using System.Windows.Media.Imaging;
namespace Daybreak.Controls;
@@ -0,0 +1,43 @@
using System;
using System.Extensions;
using System.Globalization;
using System.Windows;
using System.Windows.Data;
namespace Daybreak.Converters;
public sealed class BooleanToGridLengthConverter : IValueConverter
{
private static GridLength Collapsed = new(0);
public GridLength VisibleValue { get; set; }
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
{
return this.GetVisibility(value);
}
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
{
throw new NotImplementedException();
}
private object GetVisibility(object value)
{
if (value is not bool)
{
return this.VisibleValue;
}
var objValue = value.Cast<bool>();
if (objValue)
{
return this.VisibleValue;
}
else
{
return Collapsed;
}
}
}
+2 -2
View File
@@ -13,7 +13,7 @@
<LangVersion>preview</LangVersion>
<ApplicationIcon>Daybreak.ico</ApplicationIcon>
<IncludePackageReferencesDuringMarkupCompilation>true</IncludePackageReferencesDuringMarkupCompilation>
<Version>0.9.8.71</Version>
<Version>0.9.8.78</Version>
<EnableWindowsTargeting>true</EnableWindowsTargeting>
<UserSecretsId>cfb2a489-db80-448d-a969-80270f314c46</UserSecretsId>
<AllowUnsafeBlocks>True</AllowUnsafeBlocks>
@@ -101,7 +101,7 @@
<PackageReference Include="WpfScreenHelper" Version="2.1.0" />
<PackageReference Include="WriteableBitmapEx" Version="1.6.8" />
</ItemGroup>
<Target Name="PostBuild" AfterTargets="PostBuildEvent" Condition="'$(Configuration)' == 'Release'">
<Exec Command="echo.&gt;$(Version).version" />
+2
View File
@@ -1,6 +1,7 @@
using Daybreak.Configuration;
using Daybreak.Services.Drawing;
using Daybreak.Services.ExceptionHandling;
using Daybreak.Services.Mods;
using Daybreak.Services.Navigation;
using Daybreak.Services.Notifications;
using Daybreak.Services.Options;
@@ -59,6 +60,7 @@ public sealed class Launcher : ExtendedApplication<MainWindow>
ProjectConfiguration.RegisterStartupActions(this.ServiceProvider.GetRequiredService<IStartupActionProducer>()!);
ProjectConfiguration.RegisterDrawingModules(this.ServiceProvider.GetRequiredService<IDrawingModuleProducer>()!);
ProjectConfiguration.RegisterNotificationHandlers(this.ServiceProvider.GetRequiredService<INotificationHandlerProducer>()!);
ProjectConfiguration.RegisterMods(this.ServiceProvider.GetRequiredService<IModsManager>()!);
this.logger = this.ServiceProvider.GetRequiredService<ILogger<Launcher>>();
this.exceptionHandler = this.ServiceProvider.GetRequiredService<IExceptionHandler>();
+8 -11
View File
@@ -123,17 +123,14 @@ public partial class MainWindow : MetroWindow
},
onNone: async () =>
{
var maybeImageStream = await this.bloogumClient.GetRandomScreenShot().ConfigureAwait(true);
maybeImageStream.DoAny(
onSome: (stream) =>
{
var bitmapImage = new BitmapImage();
bitmapImage.BeginInit();
bitmapImage.StreamSource = stream;
bitmapImage.EndInit();
this.SetImage(bitmapImage);
this.CreditText = "http://bloogum.net/guildwars";
});
var maybeImageSource = await this.bloogumClient.GetImage(true).ConfigureAwait(true);
if (maybeImageSource is null)
{
return;
}
this.SetImage(maybeImageSource);
this.CreditText = "http://bloogum.net/guildwars";
});
}
+1 -1
View File
@@ -2,7 +2,7 @@
namespace Daybreak.Models.Guildwars;
public readonly struct Bag
public sealed class Bag
{
public List<IBagContent> Items { get; init; }
public int Capacity { get; init; }
+1 -1
View File
@@ -2,7 +2,7 @@
namespace Daybreak.Models.Guildwars;
public readonly struct BagItem : IBagContent
public sealed class BagItem : IBagContent
{
public ItemBase Item { get; init; }
public uint Slot { get; init; }
+136
View File
@@ -0,0 +1,136 @@
using System;
using System.Collections.Generic;
using System.Linq;
namespace Daybreak.Models.Guildwars;
public sealed class Campaign
{
public static Campaign Core { get; } = new()
{
Id = 0,
Name = "Core",
WikiUrl = "https://wiki.guildwars.com/wiki/Guild_Wars_Core",
Continents = new List<Continent>
{
Continent.TheBattleIsles,
Continent.TheMists
}
};
public static Campaign Prophecies { get; } = new()
{
Id = 1,
Name = "Prophecies",
WikiUrl = "https://wiki.guildwars.com/wiki/Guild_Wars_Prophecies",
Continents = new List<Continent>
{
Continent.Tyria
}
};
public static Campaign Factions { get; } = new()
{
Id = 2,
Name = "Factions",
WikiUrl = "https://wiki.guildwars.com/wiki/Guild_Wars_Factions",
Continents = new List<Continent>
{
Continent.Cantha
}
};
public static Campaign Nightfall { get; } = new()
{
Id = 3,
Name = "Nightfall",
WikiUrl = "https://wiki.guildwars.com/wiki/Guild_Wars_Nightfall",
Continents = new List<Continent>
{
Continent.Elona,
Continent.RealmOfTorment
}
};
public static Campaign EyeOfTheNorth { get; } = new()
{
Id = 4,
Name = "Eye of the North",
WikiUrl = "https://wiki.guildwars.com/wiki/Guild_Wars_Eye_of_the_North",
Continents = new List<Continent>
{
Continent.Tyria
}
};
public static Campaign BonusMissionPack { get; } = new()
{
Id = 5,
Name = "Bonus Mission Pack",
WikiUrl = "https://wiki.guildwars.com/wiki/Guild_Wars_Bonus_Mission_Pack",
Continents = new List<Continent>
{
Continent.Tyria,
Continent.Elona,
Continent.Cantha
}
};
public static IReadOnlyList<Campaign> Campaigns { get; } = new List<Campaign>
{
Core,
Prophecies,
Factions,
Nightfall,
EyeOfTheNorth,
BonusMissionPack
};
public static bool TryParse(int id, out Campaign campaign)
{
campaign = Campaigns.Where(campaign => campaign.Id == id).FirstOrDefault()!;
if (campaign is null)
{
return false;
}
return true;
}
public static bool TryParse(string name, out Campaign campaign)
{
campaign = Campaigns.Where(region => region.Name == name).FirstOrDefault()!;
if (campaign is null)
{
return false;
}
return true;
}
public static Campaign Parse(int id)
{
if (TryParse(id, out var campaign) is false)
{
throw new InvalidOperationException($"Could not find a campaign with id {id}");
}
return campaign;
}
public static Campaign Parse(string name)
{
if (TryParse(name, out var region) is false)
{
throw new InvalidOperationException($"Could not find a campaign with name {name}");
}
return region;
}
private Campaign()
{
}
public int Id { get; init; }
public string? Name { get; init; }
public string? WikiUrl { get; init; }
public IReadOnlyList<Continent>? Continents { get; init; }
}
+152
View File
@@ -0,0 +1,152 @@
using System;
using System.Collections.Generic;
using System.Linq;
namespace Daybreak.Models.Guildwars;
public sealed class Continent
{
public static Continent Tyria { get; } = new Continent
{
Id = 0,
Name = "Tyria",
WikiUrl = "https://wiki.guildwars.com/wiki/Tyria",
Regions = new List<Region>
{
Region.Ascalon,
Region.PresearingAscalon,
Region.CrystalDesert,
Region.Kryta,
Region.MaguumaJungle,
Region.RingOfFireIslands,
Region.ShiverpeakMountains,
Region.CharrHomelands,
Region.DepthsOfTyria,
Region.FarShiverpeaks,
Region.TarnishedCoast,
Region.TheFlightNorth,
Region.TheRiseOfTheWhiteMantle
}
};
public static Continent TheMists { get; } = new Continent
{
Id = 1,
Name = "The Mists",
WikiUrl = "https://wiki.guildwars.com/wiki/The_Mists",
Regions = new List<Region>
{
Region.HeroesAscent
}
};
public static Continent Cantha { get; } = new Continent
{
Id = 2,
Name = "Cantha",
WikiUrl = "https://wiki.guildwars.com/wiki/Cantha",
Regions = new List<Region>
{
Region.ShingJeaIsland,
Region.KainengCity,
Region.EchovaldForest,
Region.TheJadeSea,
Region.TheTenguAccords
}
};
public static Continent TheBattleIsles { get; } = new Continent
{
Id = 3,
Name = "The Battle Isles",
WikiUrl = "https://wiki.guildwars.com/wiki/The_Battle_Isles",
Regions = new List<Region>
{
Region.TheBattleIsles,
}
};
public static Continent Elona { get; } = new Continent
{
Id = 4,
Name = "Elona",
WikiUrl = "https://wiki.guildwars.com/wiki/Elona",
Regions = new List<Region>
{
Region.Istan,
Region.Kourna,
Region.Vabbi,
Region.TheDesolation,
Region.TheBattleOfJahai
}
};
public static Continent RealmOfTorment { get; } = new Continent
{
Id = 5,
Name = "Realm of Torment",
WikiUrl = "https://wiki.guildwars.com/wiki/Realm_of_Torment",
Regions = new List<Region>
{
Region.RealmOfTorment
}
};
public static IReadOnlyList<Continent> Continents { get; } = new List<Continent>
{
Tyria,
TheMists,
Cantha,
TheBattleIsles,
Elona,
RealmOfTorment
};
public static bool TryParse(int id, out Continent continent)
{
continent = Continents.Where(continent => continent.Id == id).FirstOrDefault()!;
if (continent is null)
{
return false;
}
return true;
}
public static bool TryParse(string name, out Continent continent)
{
continent = Continents.Where(continent => continent.Name == name).FirstOrDefault()!;
if (continent is null)
{
return false;
}
return true;
}
public static Continent Parse(int id)
{
if (TryParse(id, out var continent) is false)
{
throw new InvalidOperationException($"Could not find a continent with id {id}");
}
return continent;
}
public static Continent Parse(string name)
{
if (TryParse(name, out var continent) is false)
{
throw new InvalidOperationException($"Could not find a continent with name {name}");
}
return continent;
}
private Continent()
{
}
public int Id { get; init; }
public string? Name { get; init; }
public string? WikiUrl { get; init; }
public IReadOnlyList<Region>? Regions { get; init; }
}
+3 -4
View File
@@ -1,9 +1,8 @@
using Daybreak.Models.Guildwars;
using System.Collections.Generic;
using System.Collections.Generic;
namespace Daybreak.Models;
namespace Daybreak.Models.Guildwars;
public readonly struct GameData
public sealed class GameData
{
public bool Valid { get; init; }
public MainPlayerInformation? MainPlayer { get; init; }
+1 -1
View File
@@ -2,7 +2,7 @@
namespace Daybreak.Models.Guildwars;
public readonly struct InventoryData
public sealed class InventoryData
{
public Bag? Backpack { get; init; }
public Bag? BeltPouch { get; init; }
+1 -1
View File
@@ -1,6 +1,6 @@
namespace Daybreak.Models.Guildwars;
public readonly struct LivingEntity : IEntity
public sealed class LivingEntity : IEntity
{
public int Id { get; init; }
+1 -1
View File
@@ -1,6 +1,6 @@
namespace Daybreak.Models.Guildwars;
public readonly struct LoginData
public sealed class LoginData
{
public string Email { get; init; }
public string PlayerName { get; init; }
@@ -2,7 +2,7 @@
namespace Daybreak.Models.Guildwars;
public readonly struct MainPlayerInformation : IEntity
public sealed class MainPlayerInformation : IEntity
{
public string? Name { get; init; }
public uint Timer { get; init; }
File diff suppressed because it is too large Load Diff
+1 -1
View File
@@ -1,6 +1,6 @@
namespace Daybreak.Models.Guildwars;
public readonly struct MapIcon : IPositionalEntity
public sealed class MapIcon : IPositionalEntity
{
public Position? Position { get; init; }
public GuildwarsIcon? Icon { get; init; }
+143 -27
View File
@@ -77,9 +77,8 @@ public sealed class Npc
public static readonly Npc Krytan = new() { Ids = new int[] { 1492, 1960, 1964, 1966, 1967, 1991, 2008, 2009, 2020, 2032, 2049, 2120, 2858, 2867, 2869 }, Name = "Krytan", WikiUrl = "https://wiki.guildwars.com/wiki/Krytan" };
public static readonly Npc Gwen = new() { Ids = new int[] { 1493, 1494, 5966, 5970 }, Name = "Gwen", WikiUrl = "https://wiki.guildwars.com/wiki/Gwen" };
public static readonly Npc FarrahCappo = new() { Ids = new int[] { 1497, 2863 }, Name = "Farrah Cappo", WikiUrl = "https://wiki.guildwars.com/wiki/Farrah_Cappo" };
public static readonly Npc Cynn = new() { Ids = new int[] { 1501, 1924, 2128, 2853, 3489, 4565, 4575, 5988 }, Name = "Cynn", WikiUrl = "https://wiki.guildwars.com/wiki/Cynn" };
public static readonly Npc Devona = new() { Ids = new int[] { 1502, 1932, 2132, 2854, 3492, 3509, 4569, 4579, 5993 }, Name = "Devona", WikiUrl = "https://wiki.guildwars.com/wiki/Devona" };
public static readonly Npc BrotherMhenlo = new() { Ids = new int[] { 1503 }, Name = "Brother Mhenlo", WikiUrl = "https://wiki.guildwars.com/wiki/Brother_Mhenlo" };
public static readonly Npc Cynn = new() { Ids = new int[] { 1501, 1924, 2128, 2853, 3489, 4565, 4575, 5988, 4585 }, Name = "Cynn", WikiUrl = "https://wiki.guildwars.com/wiki/Cynn" };
public static readonly Npc Devona = new() { Ids = new int[] { 1502, 1932, 2132, 2854, 3492, 3509, 4569, 4579, 5993, 4589 }, Name = "Devona", WikiUrl = "https://wiki.guildwars.com/wiki/Devona" };
public static readonly Npc AscalonDuke = new() { Ids = new int[] { 1504, 1511 }, Name = "Ascalon Duke", WikiUrl = "https://wiki.guildwars.com/wiki/Guild_Wars_Wiki:Projects/NPC_models/Humans/Ascalon" };
public static readonly Npc PrinceRurik = new() { Ids = new int[] { 1505 }, Name = "Prince Rurik", WikiUrl = "https://wiki.guildwars.com/wiki/Prince_Rurik" };
public static readonly Npc LadyAlthea = new() { Ids = new int[] { 1507 }, Name = "Lady Althea", WikiUrl = "https://wiki.guildwars.com/wiki/Lady_Althea" };
@@ -111,7 +110,7 @@ public sealed class Npc
public static readonly Npc DwarvenScout = new() { Ids = new int[] { 1567 }, Name = "Dwarven Scout", WikiUrl = "https://wiki.guildwars.com/wiki/Dwarven_Scout" };
public static readonly Npc CharrChaot = new() { Ids = new int[] { 1635 }, Name = "Charr Chaot", WikiUrl = "https://wiki.guildwars.com/wiki/Charr_Chaot" };
public static readonly Npc CharrAshenClaw = new() { Ids = new int[] { 1638 }, Name = "Charr Ashen Claw", WikiUrl = "https://wiki.guildwars.com/wiki/Charr_Ashen_Claw" };
public static readonly Npc CharrShaman = new() { Ids = new int[] { 1646, 5711 }, Name = "Charr Shaman", WikiUrl = "https://wiki.guildwars.com/wiki/Charr_Shaman" };
public static readonly Npc CharrShaman = new() { Ids = new int[] { 1646, 5711, 4972 }, Name = "Charr Shaman", WikiUrl = "https://wiki.guildwars.com/wiki/Charr_Shaman" };
public static readonly Npc CharrAxeFiend = new() { Ids = new int[] { 1651, 7802 }, Name = "Charr Axe Fiend", WikiUrl = "https://wiki.guildwars.com/wiki/Charr_Axe_Fiend" };
public static readonly Npc CharrBladeStorm = new() { Ids = new int[] { 1653 }, Name = "Charr Blade Storm", WikiUrl = "https://wiki.guildwars.com/wiki/Charr_Blade_Storm" };
public static readonly Npc CharrHunter = new() { Ids = new int[] { 1656 }, Name = "Charr Hunter", WikiUrl = "https://wiki.guildwars.com/wiki/Charr_Hunter" };
@@ -175,7 +174,7 @@ public sealed class Npc
public static readonly Npc EnemyPriest = new() { Ids = new int[] { 1871 }, Name = "Enemy Priest", WikiUrl = "https://wiki.guildwars.com/wiki/Enemy_Priest" };
public static readonly Npc ForgottenChampion = new() { Ids = new int[] { 1872 }, Name = "Forgotten Champion", WikiUrl = "https://wiki.guildwars.com/wiki/Forgotten_Champion" };
public static readonly Npc ForgottenAvenger = new() { Ids = new int[] { 1873 }, Name = "Forgotten Avenger", WikiUrl = "https://wiki.guildwars.com/wiki/Forgotten_Avenger" };
public static readonly Npc Forgotten = new() { Ids = new int[] { 1874 }, Name = "Forgotten", WikiUrl = "https://wiki.guildwars.com/wiki/Forgotten" };
public static readonly Npc Forgotten = new() { Ids = new int[] { 1874, 5002 }, Name = "Forgotten", WikiUrl = "https://wiki.guildwars.com/wiki/Forgotten" };
public static readonly Npc Dunham = new() { Ids = new int[] { 1890, 1897, 1904, 1911, 1919, 1920, 2847 }, Name = "Dunham", WikiUrl = "https://wiki.guildwars.com/wiki/Dunham" };
public static readonly Npc Claude = new() { Ids = new int[] { 1891, 1898, 1905, 1912, 1921, 2848 }, Name = "Claude", WikiUrl = "https://wiki.guildwars.com/wiki/Claude" };
public static readonly Npc Orion = new() { Ids = new int[] { 1892, 1899, 1906, 1913, 1923, 2849 }, Name = "Orion", WikiUrl = "https://wiki.guildwars.com/wiki/Orion" };
@@ -184,10 +183,10 @@ public sealed class Npc
public static readonly Npc Stefan = new() { Ids = new int[] { 1895, 1902, 1909, 1917, 1931, 2845 }, Name = "Stefan", WikiUrl = "https://wiki.guildwars.com/wiki/Stefan" };
public static readonly Npc Reyna = new() { Ids = new int[] { 1896, 1903, 1910, 1918, 1933, 2846 }, Name = "Reyna", WikiUrl = "https://wiki.guildwars.com/wiki/Reyna" };
public static readonly Npc Lina = new() { Ids = new int[] { 1915, 1927, 1928, 2851, 5991 }, Name = "Lina", WikiUrl = "https://wiki.guildwars.com/wiki/Lina" };
public static readonly Npc Eve = new() { Ids = new int[] { 1922, 2873, 3488, 4564, 4574, 5987 }, Name = "Eve", WikiUrl = "https://wiki.guildwars.com/wiki/Eve" };
public static readonly Npc Mhenlo = new() { Ids = new int[] { 1926, 2136, 2855, 3121, 4577, 5990 }, Name = "Mhenlo", WikiUrl = "https://wiki.guildwars.com/wiki/Mhenlo" };
public static readonly Npc Aidan = new() { Ids = new int[] { 1934, 2124, 2852, 3493, 4570, 4580, 5994 }, Name = "Aidan", WikiUrl = "https://wiki.guildwars.com/wiki/Aidan" };
public static readonly Npc Olias = new() { Ids = new int[] { 1935 }, Name = "Olias", WikiUrl = "https://wiki.guildwars.com/wiki/Olias" };
public static readonly Npc Eve = new() { Ids = new int[] { 1922, 2873, 3488, 4564, 4574, 5987, 4584 }, Name = "Eve", WikiUrl = "https://wiki.guildwars.com/wiki/Eve" };
public static readonly Npc Mhenlo = new() { Ids = new int[] { 1926, 2136, 2855, 3121, 4577, 5990, 4587, 1503 }, Name = "Mhenlo", WikiUrl = "https://wiki.guildwars.com/wiki/Mhenlo" };
public static readonly Npc Aidan = new() { Ids = new int[] { 1934, 2124, 2852, 3493, 4570, 4580, 5994, 4590 }, Name = "Aidan", WikiUrl = "https://wiki.guildwars.com/wiki/Aidan" };
public static readonly Npc Olias = new() { Ids = new int[] { 1935, 1938 }, Name = "Olias", WikiUrl = "https://wiki.guildwars.com/wiki/Olias" };
public static readonly Npc LyssasMuse = new() { Ids = new int[] { 1944 }, Name = "Lyssa's Muse", WikiUrl = "https://wiki.guildwars.com/wiki/Lyssa%27s_Muse" };
public static readonly Npc VoiceOfGrenth = new() { Ids = new int[] { 1945 }, Name = "Voice Of Grenth", WikiUrl = "https://wiki.guildwars.com/wiki/Voice_of_Grenth" };
public static readonly Npc AvatarOfDwayna = new() { Ids = new int[] { 1946 }, Name = "Avatar Of Dwayna", WikiUrl = "https://wiki.guildwars.com/wiki/Avatar_of_Dwayna" };
@@ -200,7 +199,7 @@ public sealed class Npc
public static readonly Npc AscalonSettler = new() { Ids = new int[] { 1986, 1987 }, Name = "Ascalon Settler", WikiUrl = "https://wiki.guildwars.com/wiki/Ascalon_Settler" };
public static readonly Npc SettlementGuard = new() { Ids = new int[] { 1989 }, Name = "Settlement Guard", WikiUrl = "https://wiki.guildwars.com/wiki/Settlement_Guard" };
public static readonly Npc CaptainGreywind = new() { Ids = new int[] { 1990 }, Name = "Captain Greywind", WikiUrl = "https://wiki.guildwars.com/wiki/Captain_Greywind" };
public static readonly Npc AscalonianGhost = new() { Ids = new int[] { 1998, 2141, 2353, 2354, 2355, 2534 }, Name = "Ascalonian Ghost", WikiUrl = "https://wiki.guildwars.com/wiki/Guild_Wars_Wiki:Projects/NPC_models/Ghosts" };
public static readonly Npc AscalonianGhost = new() { Ids = new int[] { 1998, 2141, 2353, 2354, 2355, 2534, 5617 }, Name = "Ascalonian Ghost", WikiUrl = "https://wiki.guildwars.com/wiki/Guild_Wars_Wiki:Projects/NPC_models/Ghosts" };
public static readonly Npc KrytanSmith = new() { Ids = new int[] { 2000 }, Name = "Krytan Smith", WikiUrl = "https://wiki.guildwars.com/wiki/Guild_Wars_Wiki:Projects/NPC_models/Humans/Kryta" };
public static readonly Npc WhiteMantleZealot = new() { Ids = new int[] { 2006, 2206, 2207 }, Name = "White Mantle Zealot", WikiUrl = "https://wiki.guildwars.com/wiki/White_Mantle_Zealot" };
public static readonly Npc CaptainGrumby = new() { Ids = new int[] { 2016 }, Name = "Captain Grumby", WikiUrl = "https://wiki.guildwars.com/wiki/Captain_Grumby" };
@@ -318,7 +317,7 @@ public sealed class Npc
public static readonly Npc BurningTitan = new() { Ids = new int[] { 2670 }, Name = "Burning Titan", WikiUrl = "https://wiki.guildwars.com/wiki/Burning_Titan" };
public static readonly Npc HandOfTheTitans = new() { Ids = new int[] { 2671 }, Name = "Hand Of The Titans", WikiUrl = "https://wiki.guildwars.com/wiki/Hand_of_the_Titans" };
public static readonly Npc FistOfTheTitans = new() { Ids = new int[] { 2672 }, Name = "Fist Of The Titans", WikiUrl = "https://wiki.guildwars.com/wiki/Fist_of_the_Titans" };
public static readonly Npc UndeadLich = new() { Ids = new int[] { 2695 }, Name = "Undead Lich", WikiUrl = "https://wiki.guildwars.com/wiki/Undead_Lich" };
public static readonly Npc UndeadLich = new() { Ids = new int[] { 2695, 4953 }, Name = "Undead Lich", WikiUrl = "https://wiki.guildwars.com/wiki/Undead_Lich" };
public static readonly Npc UndeadPrinceRurik = new() { Ids = new int[] { 2698 }, Name = "Undead Prince Rurik", WikiUrl = "https://wiki.guildwars.com/wiki/Undead_Prince_Rurik" };
public static readonly Npc ZaimGrimeclaw = new() { Ids = new int[] { 2700 }, Name = "Zaim Grimeclaw", WikiUrl = "https://wiki.guildwars.com/wiki/Zaim_Grimeclaw" };
public static readonly Npc Ruinwing = new() { Ids = new int[] { 2729 }, Name = "Ruinwing", WikiUrl = "https://wiki.guildwars.com/wiki/Ruinwing" };
@@ -626,7 +625,7 @@ public sealed class Npc
public static readonly Npc AssassinsConstruct = new() { Ids = new int[] { 4081, 4090 }, Name = "Assassin's Construct", WikiUrl = "https://wiki.guildwars.com/wiki/Assassin%27s_Construct" };
public static readonly Npc ElementalsConstruct = new() { Ids = new int[] { 4093 }, Name = "Elemental's Construct", WikiUrl = "https://wiki.guildwars.com/wiki/Elemental%27s_Construct" };
public static readonly Npc RangersConstruct = new() { Ids = new int[] { 4096 }, Name = "Ranger's Construct", WikiUrl = "https://wiki.guildwars.com/wiki/Ranger%27s_Construct" };
public static readonly Npc ShiroTagachi = new() { Ids = new int[] { 4120 }, Name = "Shiro Tagachi", WikiUrl = "https://wiki.guildwars.com/wiki/Shiro_Tagachi" };
public static readonly Npc ShiroTagachi = new() { Ids = new int[] { 4120, 4952 }, Name = "Shiro Tagachi", WikiUrl = "https://wiki.guildwars.com/wiki/Shiro_Tagachi" };
public static readonly Npc ShirokenAssassin = new() { Ids = new int[] { 4125 }, Name = "Shiro'ken Assassin", WikiUrl = "https://wiki.guildwars.com/wiki/Shiro%27ken_Assassin" };
public static readonly Npc ShirokenNecromancer = new() { Ids = new int[] { 4127 }, Name = "Shiro'ken Necromancer", WikiUrl = "https://wiki.guildwars.com/wiki/Shiro%27ken_Necromancer" };
public static readonly Npc ShirokenWarrior = new() { Ids = new int[] { 4130 }, Name = "Shiro'ken Warrior", WikiUrl = "https://wiki.guildwars.com/wiki/Shiro%27ken_Warrior" };
@@ -692,18 +691,18 @@ public sealed class Npc
public static readonly Npc Dehjah = new() { Ids = new int[] { 4406 }, Name = "Dehjah", WikiUrl = "https://wiki.guildwars.com/wiki/Dehjah" };
public static readonly Npc BoklonBlackwater = new() { Ids = new int[] { 4414 }, Name = "Boklon Blackwater", WikiUrl = "https://wiki.guildwars.com/wiki/Boklon_Blackwater" };
public static readonly Npc GlugKlugg = new() { Ids = new int[] { 4420 }, Name = "Glug Klugg", WikiUrl = "https://wiki.guildwars.com/wiki/Glug_Klugg" };
public static readonly Npc ZhedShadowhoof = new() { Ids = new int[] { 4437, 4940 }, Name = "Zhed Shadowhoof", WikiUrl = "https://wiki.guildwars.com/wiki/Zhed_Shadowhoof" };
public static readonly Npc ZhedShadowhoof = new() { Ids = new int[] { 4437, 4940, 4440 }, Name = "Zhed Shadowhoof", WikiUrl = "https://wiki.guildwars.com/wiki/Zhed_Shadowhoof" };
public static readonly Npc Tahlkora = new() { Ids = new int[] { 4456, 4459 }, Name = "Tahlkora", WikiUrl = "https://wiki.guildwars.com/wiki/Tahlkora" };
public static readonly Npc MasterOfWhispers = new() { Ids = new int[] { 4464 }, Name = "Master Of Whispers", WikiUrl = "https://wiki.guildwars.com/wiki/Master_of_Whispers" };
public static readonly Npc AcolyteJin = new() { Ids = new int[] { 4469 }, Name = "Acolyte Jin", WikiUrl = "https://wiki.guildwars.com/wiki/Acolyte_Jin" };
public static readonly Npc AcolyteSousuke = new() { Ids = new int[] { 4487 }, Name = "Acolyte Sousuke", WikiUrl = "https://wiki.guildwars.com/wiki/Acolyte_Sousuke" };
public static readonly Npc AcolyteSousuke = new() { Ids = new int[] { 4487, 4490 }, Name = "Acolyte Sousuke", WikiUrl = "https://wiki.guildwars.com/wiki/Acolyte_Sousuke" };
public static readonly Npc Melonni = new() { Ids = new int[] { 4493, 4496 }, Name = "Melonni ", WikiUrl = "https://wiki.guildwars.com/wiki/Melonni" };
public static readonly Npc Khim = new() { Ids = new int[] { 4531, 4549, 4578 }, Name = "Khim", WikiUrl = "https://wiki.guildwars.com/wiki/Khim" };
public static readonly Npc Herta = new() { Ids = new int[] { 4532, 4537, 4544, 4551, 4566, 4576, 5989 }, Name = "Herta", WikiUrl = "https://wiki.guildwars.com/wiki/Herta" };
public static readonly Npc Gehraz = new() { Ids = new int[] { 4533, 4540, 4547, 4554, 4571, 4581 }, Name = "Gehraz", WikiUrl = "https://wiki.guildwars.com/wiki/Gehraz" };
public static readonly Npc Sogolon = new() { Ids = new int[] { 4534, 4541, 4548, 4555, 4572, 4582 }, Name = "Sogolon", WikiUrl = "https://wiki.guildwars.com/wiki/Sogolon" };
public static readonly Npc Herta = new() { Ids = new int[] { 4532, 4537, 4544, 4551, 4566, 4576, 5989, 4586 }, Name = "Herta", WikiUrl = "https://wiki.guildwars.com/wiki/Herta" };
public static readonly Npc Gehraz = new() { Ids = new int[] { 4533, 4540, 4547, 4554, 4571, 4581, 4591 }, Name = "Gehraz", WikiUrl = "https://wiki.guildwars.com/wiki/Gehraz" };
public static readonly Npc Sogolon = new() { Ids = new int[] { 4534, 4541, 4548, 4555, 4572, 4582, 4592 }, Name = "Sogolon", WikiUrl = "https://wiki.guildwars.com/wiki/Sogolon" };
public static readonly Npc Kihm = new() { Ids = new int[] { 4535, 4542, 4568 }, Name = "Kihm", WikiUrl = "https://wiki.guildwars.com/wiki/Kihm" };
public static readonly Npc Odurra = new() { Ids = new int[] { 4536, 4543, 4550, 4563, 4573 }, Name = "Odurra", WikiUrl = "https://wiki.guildwars.com/wiki/Odurra" };
public static readonly Npc Odurra = new() { Ids = new int[] { 4536, 4543, 4550, 4563, 4573, 4583 }, Name = "Odurra", WikiUrl = "https://wiki.guildwars.com/wiki/Odurra" };
public static readonly Npc Timera = new() { Ids = new int[] { 4538, 4545, 4552 }, Name = "Timera", WikiUrl = "https://wiki.guildwars.com/wiki/Timera" };
public static readonly Npc Abasi = new() { Ids = new int[] { 4539, 4553, 4546 }, Name = "Abasi", WikiUrl = "https://wiki.guildwars.com/wiki/Abasi" };
public static readonly Npc Sunspear = new() { Ids = new int[] { 4698, 4726, 4727, 4774, 4778, 4779, 4781, 4782, 4784, 4786, 4811, 4739, 4738 }, Name = "Sunspear", WikiUrl = "https://wiki.guildwars.com/wiki/Sunspear" };
@@ -1112,8 +1111,8 @@ public sealed class Npc
public static readonly Npc VabbianBlacksmith = new() { Ids = new int[] { 5667 }, Name = "Vabbian Blacksmith", WikiUrl = "https://wiki.guildwars.com/wiki/Guild_Wars_Wiki:Projects/NPC_models/Humans/Vabbians" };
public static readonly Npc YammironEtherLord = new () { Ids = new int[] { 4636 }, Name = "Yammiron, Ether Lord", WikiUrl = "https://wiki.guildwars.com/wiki/Yammiron,_Ether_Lord" };
public static readonly Npc RoaringEther = new() { Ids = new int[] { 4675, 4676 }, Name = "Roaring Ether", WikiUrl = "https://wiki.guildwars.com/wiki/Roaring_Ether" };
public static readonly Npc SapphireDjinn = new() { Ids = new int[] { 4673 }, Name = "Sapphire Djinn", WikiUrl = "https://wiki.guildwars.com/wiki/Sapphire_Djinn" };
public static readonly Npc RubyDjinn = new() { Ids = new int[] { 4670 }, Name = "Ruby Djinn", WikiUrl = "https://wiki.guildwars.com/wiki/Ruby_Djinn" };
public static readonly Npc SapphireDjinn = new() { Ids = new int[] { 4673, 4312 }, Name = "Sapphire Djinn", WikiUrl = "https://wiki.guildwars.com/wiki/Sapphire_Djinn" };
public static readonly Npc RubyDjinn = new() { Ids = new int[] { 4670, 4311 }, Name = "Ruby Djinn", WikiUrl = "https://wiki.guildwars.com/wiki/Ruby_Djinn" };
public static readonly Npc CobaltMokele = new() { Ids = new int[] { 4664 }, Name = "Cobalt Mokele", WikiUrl = "https://wiki.guildwars.com/wiki/Cobalt_Mokele" };
public static readonly Npc JisholDarksong = new() { Ids = new int[] { 4626 }, Name = "Jishol Darksong", WikiUrl = "https://wiki.guildwars.com/wiki/Jishol_Darksong" };
public static readonly Npc CobaltShrieker = new() { Ids = new int[] { 4665 }, Name = "Cobalt Shrieker", WikiUrl = "https://wiki.guildwars.com/wiki/Cobalt_Shrieker" };
@@ -1153,7 +1152,7 @@ public sealed class Npc
public static readonly Npc KorrLivingFlame = new () { Ids = new int[] { 4877 }, Name = "Korr Living Flame", WikiUrl = "https://wiki.guildwars.com/wiki/Korr,_Living_Flame" };
public static readonly Npc ImmolatedDjinn = new() { Ids = new int[] { 4906 }, Name = "Immolated Djinn", WikiUrl = "https://wiki.guildwars.com/wiki/Immolated_Djinn" };
public static readonly Npc AscensionPilgrim = new() { Ids = new int[] { 5007 }, Name = "Ascension Pilgrim", WikiUrl = "https://wiki.guildwars.com/wiki/Ascension_Pilgrim" };
public static readonly Npc DynasticSpirit = new() { Ids = new int[] { 5006, 5611, 5005 }, Name = "Dynastic Spirit", WikiUrl = "https://wiki.guildwars.com/wiki/Dynastic_Spirit" };
public static readonly Npc DynasticSpirit = new() { Ids = new int[] { 5006, 5611, 5005, 5613 }, Name = "Dynastic Spirit", WikiUrl = "https://wiki.guildwars.com/wiki/Dynastic_Spirit" };
public static readonly Npc DiamondDjinn = new() { Ids = new int[] { 4671 }, Name = "Diamond Djinn", WikiUrl = "https://wiki.guildwars.com/wiki/Diamond_Djinn" };
public static readonly Npc VahlenTheSilent = new() { Ids = new int[] { 5566 }, Name = "Vahlen the Silent", WikiUrl = "https://wiki.guildwars.com/wiki/Vahlen_the_Silent" };
public static readonly Npc AwakenedAcolyte = new() { Ids = new int[] { 5577 }, Name = "Awakened Acolyte", WikiUrl = "https://wiki.guildwars.com/wiki/Awakened_Acolyte" };
@@ -1181,19 +1180,78 @@ public sealed class Npc
public static readonly Npc RavenousMandragor = new() { Ids = new int[] { 4305 }, Name = "Ravenous Mandragor", WikiUrl = "https://wiki.guildwars.com/wiki/Ravenous_Mandragor" };
public static readonly Npc MandragorTerror = new() { Ids = new int[] { 4307 }, Name = "Mandragor Terror", WikiUrl = "https://wiki.guildwars.com/wiki/Mandragor_Terror" };
public static readonly Npc MandragorSandDevil = new() { Ids = new int[] { 4306 }, Name = "Mandragor Sand Devil", WikiUrl = "https://wiki.guildwars.com/wiki/Mandragor_Sand_Devil" };
public static readonly Npc GhostlyScout = new() { Ids = new int[] { 5547 }, Name = "Ghostly Scout", WikiUrl = "https://wiki.guildwars.com/wiki/Ghostly_Scout" };
public static readonly Npc GhostlyScout = new() { Ids = new int[] { 5547, 5548 }, Name = "Ghostly Scout", WikiUrl = "https://wiki.guildwars.com/wiki/Ghostly_Scout" };
public static readonly Npc BladedDuneTermite = new() { Ids = new int[] { 4319 }, Name = "Bladed Dune Termite", WikiUrl = "https://wiki.guildwars.com/wiki/Bladed_Dune_Termite" };
public static readonly Npc DuneBeetleLance = new() { Ids = new int[] { 4320 }, Name = "Dune Beetle Lance", WikiUrl = "https://wiki.guildwars.com/wiki/Dune_Beetle_Lance" };
public static readonly Npc DuneSpider = new() { Ids = new int[] { 4303 }, Name = "Dune Spider", WikiUrl = "https://wiki.guildwars.com/wiki/Dune_Spider" };
public static readonly Npc AwakenedHead = new() { Ids = new int[] { 5580 }, Name = "Awakened Head", WikiUrl = "https://wiki.guildwars.com/wiki/Awakened_Head" };
public static readonly Npc GeneralHuduh = new() { Ids = new int[] { 5606 }, Name = "General Huduh", WikiUrl = "https://wiki.guildwars.com/wiki/General_Huduh" };
public static readonly Npc UndeadGeneral = new() { Ids = new int[] { 5606 }, Name = "Undead General", WikiUrl = "https://wiki.guildwars.com/wiki/Guild_Wars_Wiki:Projects/NPC_models/Skeletons" };
public static readonly Npc Chirah = new() { Ids = new int[] { 5272 }, Name = "Chirah", WikiUrl = "https://wiki.guildwars.com/wiki/Chirah" };
public static readonly Npc AwakenedMonk = new() { Ids = new int[] { 5626 }, Name = "Awakened Monk", WikiUrl = "https://wiki.guildwars.com/wiki/Awakened" };
public static readonly Npc AwakenedMesmer = new() { Ids = new int[] { 5625 }, Name = "Awakened Mesmer", WikiUrl = "https://wiki.guildwars.com/wiki/Awakened" };
public static readonly Npc AwakenedDervish = new() { Ids = new int[] { 5630 }, Name = "Awakened Dervish", WikiUrl = "https://wiki.guildwars.com/wiki/Awakened" };
public static readonly Npc Awata = new() { Ids = new int[] { 5624 }, Name = "Awata", WikiUrl = "https://wiki.guildwars.com/wiki/Awata" };
public static readonly Npc Thrall = new() { Ids = new int[] { 5588, 5589, 5585 }, Name = "Thrall", WikiUrl = "https://wiki.guildwars.com/wiki/Thrall" };
public static readonly Npc Thrall = new() { Ids = new int[] { 5588, 5589, 5585, 5590 }, Name = "Thrall", WikiUrl = "https://wiki.guildwars.com/wiki/Thrall" };
public static readonly Npc GhostlyPriest = new() { Ids = new int[] { 5615 }, Name = "Ghostly Priest", WikiUrl = "https://wiki.guildwars.com/wiki/Ghostly_Priest" };
public static readonly Npc PrimevalKingJahnus = new() { Ids = new int[] { 5596 }, Name = "Primeval King Jahnus", WikiUrl = "https://wiki.guildwars.com/wiki/Primeval_King_Jahnus" };
public static readonly Npc NomadGiant = new() { Ids = new int[] { 4314 }, Name = "Nomad Giant", WikiUrl = "https://wiki.guildwars.com/wiki/Nomad_Giant" };
public static readonly Npc KoahmTheWeary = new() { Ids = new int[] { 5559 }, Name = "Koahm the Weary", WikiUrl = "https://wiki.guildwars.com/wiki/Koahm_the_Weary" };
public static readonly Npc SadisticGiant = new() { Ids = new int[] { 4313 }, Name = "Sadistic Giant", WikiUrl = "https://wiki.guildwars.com/wiki/Sadistic_Giant" };
public static readonly Npc UhiwiTheSmoky = new() { Ids = new int[] { 4300 }, Name = "Uhiwi the Smoky", WikiUrl = "https://wiki.guildwars.com/wiki/Uhiwi_the_Smoky" };
public static readonly Npc ElderSiegeWurm = new() { Ids = new int[] { 5610 }, Name = "Elder Siege Wurm", WikiUrl = "https://wiki.guildwars.com/wiki/Elder_Siege_Wurm" };
public static readonly Npc AmireshThePious = new() { Ids = new int[] { 5563 }, Name = "Amiresh the Pious", WikiUrl = "https://wiki.guildwars.com/wiki/Amiresh_the_Pious" };
public static readonly Npc HordeofDarkness = new() { Ids = new int[] { 5486 }, Name = "Horde of Darkness", WikiUrl = "https://wiki.guildwars.com/wiki/Horde_of_Darkness" };
public static readonly Npc DesertWurm = new() { Ids = new int[] { 4323 }, Name = "Desert Wurm", WikiUrl = "https://wiki.guildwars.com/wiki/Desert_Wurm" };
public static readonly Npc BladeOfCorruption = new() { Ids = new int[] { 5447 }, Name = "Blade of Corruption", WikiUrl = "https://wiki.guildwars.com/wiki/Blade_of_Corruption" };
public static readonly Npc CaptainMehhan = new() { Ids = new int[] { 5366 }, Name = "Captain Mehhan", WikiUrl = "https://wiki.guildwars.com/wiki/Captain_Mehhan" };
public static readonly Npc AwakenedParagon = new() { Ids = new int[] { 5631 }, Name = "Awakened Paragon", WikiUrl = "https://wiki.guildwars.com/wiki/Guild_Wars_Wiki:Projects/NPC_models/Zombies#Mummies_(Awakened)" };
public static readonly Npc TormentedSoul = new() { Ids = new int[] { 4983, 4984, 4988, 4989 }, Name = "Tormented Soul", WikiUrl = "https://wiki.guildwars.com/wiki/Tormented_Soul" };
public static readonly Npc SecretKeeper = new() { Ids = new int[] { 4985 }, Name = "Secret Keeper", WikiUrl = "https://wiki.guildwars.com/wiki/Secret_Keeper" };
public static readonly Npc WordOfMadness = new() { Ids = new int[] { 5446 }, Name = "Word of Madness", WikiUrl = "https://wiki.guildwars.com/wiki/Word_of_Madness" };
public static readonly Npc RainOfTerror = new() { Ids = new int[] { 5445 }, Name = "Rain of Terror", WikiUrl = "https://wiki.guildwars.com/wiki/Rain_of_Terror" };
public static readonly Npc ShadowOfFear = new() { Ids = new int[] { 5444 }, Name = "Shadow of Fear", WikiUrl = "https://wiki.guildwars.com/wiki/Shadow_of_Fear" };
public static readonly Npc ScytheOfChaos = new() { Ids = new int[] { 5449, 4959 }, Name = "Scythe of Chaos", WikiUrl = "https://wiki.guildwars.com/wiki/Scythe_of_Chaos" };
public static readonly Npc SpearOfTorment = new() { Ids = new int[] { 5450 }, Name = "Spear of Torment", WikiUrl = "https://wiki.guildwars.com/wiki/Spear_of_Torment" };
public static readonly Npc HeraldOfNightmares = new() { Ids = new int[] { 5443 }, Name = "Herald of Nightmares", WikiUrl = "https://wiki.guildwars.com/wiki/Herald_of_Nightmares" };
public static readonly Npc ArmOfInsanity = new() { Ids = new int[] { 5448 }, Name = "Arm of Insanity", WikiUrl = "https://wiki.guildwars.com/wiki/Arm_of_Insanity" };
public static readonly Npc OnslaughtOfTerror = new() { Ids = new int[] { 5430 }, Name = "Onslaught of Terror", WikiUrl = "https://wiki.guildwars.com/wiki/Onslaught_of_Terror" };
public static readonly Npc ZombieMonk = new() { Ids = new int[] { 5003 }, Name = "Zombie Monk", WikiUrl = "https://wiki.guildwars.com/wiki/Guild_Wars_Wiki:Projects/NPC_models/Zombies#Zombies" };
public static readonly Npc ChimorTheLightblooded = new() { Ids = new int[] { 5520 }, Name = "Chimor the Lightblooded", WikiUrl = "https://wiki.guildwars.com/wiki/Chimor_the_Lightblooded" };
public static readonly Npc ScoutAhktum = new() { Ids = new int[] { 5017 }, Name = "Scout Ahktum", WikiUrl = "https://wiki.guildwars.com/wiki/Scout_Ahktum" };
public static readonly Npc Thenemi = new() { Ids = new int[] { 5018 }, Name = "Thenemi", WikiUrl = "https://wiki.guildwars.com/wiki/Thenemi" };
public static readonly Npc GarfazSteelfur = new() { Ids = new int[] { 5014 }, Name = "Garfaz Steelfur", WikiUrl = "https://wiki.guildwars.com/wiki/Garfaz_Steelfur" };
public static readonly Npc CaptainYithlis = new() { Ids = new int[] { 5013 }, Name = "Captain Yithlis", WikiUrl = "https://wiki.guildwars.com/wiki/Captain_Yithlis" };
public static readonly Npc Igraine = new() { Ids = new int[] { 5015 }, Name = "Igraine", WikiUrl = "https://wiki.guildwars.com/wiki/Igraine" };
public static readonly Npc TheLost = new() { Ids = new int[] { 5016 }, Name = "The Lost", WikiUrl = "https://wiki.guildwars.com/wiki/The_Lost" };
public static readonly Npc AbaddonsAdjutant = new() { Ids = new int[] { 5483 }, Name = "Abaddon's Adjutant", WikiUrl = "https://wiki.guildwars.com/wiki/Abaddon%27s_Adjutant" };
public static readonly Npc EmissaryOfDhuum = new() { Ids = new int[] { 5412, 5425 }, Name = "Emissary of Dhuum", WikiUrl = "https://wiki.guildwars.com/wiki/Emissary_of_Dhuum" };
public static readonly Npc TerrorwebDryder = new() { Ids = new int[] { 2321 }, Name = "Terrorweb Dryder", WikiUrl = "https://wiki.guildwars.com/wiki/Terrorweb_Dryder" };
public static readonly Npc Rukkassa = new() { Ids = new int[] { 4973 }, Name = "Rukkassa", WikiUrl = "https://wiki.guildwars.com/wiki/Rukkassa" };
public static readonly Npc TorturewebDryder = new() { Ids = new int[] { 5427 }, Name = "Tortureweb Dryder", WikiUrl = "https://wiki.guildwars.com/wiki/Tortureweb_Dryder" };
public static readonly Npc LostSoul = new() { Ids = new int[] { 2358 }, Name = "Lost Soul", WikiUrl = "https://wiki.guildwars.com/wiki/Lost_Soul" };
public static readonly Npc Apostate = new() { Ids = new int[] { 5491 }, Name = "Apostate", WikiUrl = "https://wiki.guildwars.com/wiki/Apostate" };
public static readonly Npc StormOfAnguish = new() { Ids = new int[] { 5436 }, Name = "Storm of Anguish", WikiUrl = "https://wiki.guildwars.com/wiki/Storm_of_Anguish" };
public static readonly Npc ShriekerOfDread = new() { Ids = new int[] { 5439 }, Name = "Shrieker of Dread", WikiUrl = "https://wiki.guildwars.com/wiki/Shrieker_of_Dread" };
public static readonly Npc ShirokenElementalist = new() { Ids = new int[] { 4128 }, Name = "Shiro'ken Elementalist", WikiUrl = "https://wiki.guildwars.com/wiki/Shiro%27ken_Elementalist" };
public static readonly Npc PainTitan = new() { Ids = new int[] { 4969 }, Name = "Pain Titan", WikiUrl = "https://wiki.guildwars.com/wiki/Pain_Titan" };
public static readonly Npc ShirokenMonk = new() { Ids = new int[] { 4129 }, Name = "Shiro'ken Monk", WikiUrl = "https://wiki.guildwars.com/wiki/Shiro%27ken_Monk" };
public static readonly Npc ShirokenMesmer = new() { Ids = new int[] { 4126 }, Name = "Shiro'ken Mesmer", WikiUrl = "https://wiki.guildwars.com/wiki/Shiro%27ken_Mesmer" };
public static readonly Npc TitanAbomination = new() { Ids = new int[] { 4968 }, Name = "Titan Abomination", WikiUrl = "https://wiki.guildwars.com/wiki/Titan_Abomination" };
public static readonly Npc BoundTiendi = new() { Ids = new int[] { 4087 }, Name = "Bound Tiendi", WikiUrl = "https://wiki.guildwars.com/wiki/Bound_Tiendi" };
public static readonly Npc BoundKaichen = new() { Ids = new int[] { 4085 }, Name = "Bound Kaichen", WikiUrl = "https://wiki.guildwars.com/wiki/Bound_Kaichen" };
public static readonly Npc WrathfulStorm = new() { Ids = new int[] { 4961 }, Name = "Wrathful Storm", WikiUrl = "https://wiki.guildwars.com/wiki/Wrathful_Storm" };
public static readonly Npc GraspOfInsanity = new() { Ids = new int[] { 4960 }, Name = "Grasp of Insanity", WikiUrl = "https://wiki.guildwars.com/wiki/Grasp_of_Insanity" };
public static readonly Npc TormentClaw = new() { Ids = new int[] { 5461 }, Name = "Torment Claw", WikiUrl = "https://wiki.guildwars.com/wiki/Torment_Claw" };
public static readonly Npc BoundHaoLi = new() { Ids = new int[] { 4082 }, Name = "Bound Hao Li", WikiUrl = "https://wiki.guildwars.com/wiki/Bound_Hao_Li" };
public static readonly Npc IgnisCruor = new() { Ids = new int[] { 4978 }, Name = "Ignis Cruor", WikiUrl = "https://wiki.guildwars.com/wiki/Ignis_Cruor" };
public static readonly Npc ShadowBeast = new() { Ids = new int[] { 4967 }, Name = "Shadow Beast", WikiUrl = "https://wiki.guildwars.com/wiki/Shadow_Beast" };
public static readonly Npc CreoVulnero = new() { Ids = new int[] { 4957 }, Name = "Creo Vulnero", WikiUrl = "https://wiki.guildwars.com/wiki/Creo_Vulnero" };
public static readonly Npc PortalWraith = new() { Ids = new int[] { 2764 }, Name = "Portal Wraith", WikiUrl = "https://wiki.guildwars.com/wiki/Portal_Wraith" };
public static readonly Npc ArmageddonLord = new() { Ids = new int[] { 2674 }, Name = "Armageddon Lord", WikiUrl = "https://wiki.guildwars.com/wiki/Armageddon_Lord" };
public static readonly Npc JoyousSoul = new() { Ids = new int[] { 5011, 5009, 5010, 5012, 5008 }, Name = "Joyous Soul", WikiUrl = "https://wiki.guildwars.com/wiki/Joyous_Soul" };
public static readonly Npc MagridTheSly = new() { Ids = new int[] { 4451 }, Name = "Magrid the Sly", WikiUrl = "https://wiki.guildwars.com/wiki/Magrid_the_Sly" };
public static readonly Npc Zenmai = new() { Ids = new int[] { 3552 }, Name = "Zenmai", WikiUrl = "https://wiki.guildwars.com/wiki/Zenmai" };
public static readonly Npc Abaddon = new() { Ids = new int[] { 5142 }, Name = "Abaddon", WikiUrl = "https://wiki.guildwars.com/wiki/Abaddon" };
public static IEnumerable<Npc> Npcs { get; } = new List<Npc>()
{
@@ -1269,7 +1327,6 @@ public sealed class Npc
FarrahCappo,
Cynn,
Devona,
BrotherMhenlo,
AscalonDuke,
PrinceRurik,
LadyAlthea,
@@ -2376,14 +2433,73 @@ public sealed class Npc
DuneBeetleLance,
DuneSpider,
AwakenedHead,
GeneralHuduh,
UndeadGeneral,
Chirah,
AwakenedMonk,
AwakenedMesmer,
AwakenedDervish,
Awata,
Thrall,
GhostlyPriest
GhostlyPriest,
PrimevalKingJahnus,
NomadGiant,
KoahmTheWeary,
SadisticGiant,
UhiwiTheSmoky,
ElderSiegeWurm,
AmireshThePious,
HordeofDarkness,
DesertWurm,
BladeOfCorruption,
CaptainMehhan,
AwakenedParagon,
TormentedSoul,
SecretKeeper,
WordOfMadness,
RainOfTerror,
ShadowOfFear,
ScytheOfChaos,
SpearOfTorment,
HeraldOfNightmares,
ArmOfInsanity,
OnslaughtOfTerror,
ZombieMonk,
ChimorTheLightblooded,
ScoutAhktum,
Thenemi,
GarfazSteelfur,
CaptainYithlis,
Igraine,
TheLost,
AbaddonsAdjutant,
EmissaryOfDhuum,
TerrorwebDryder,
Rukkassa,
TorturewebDryder,
LostSoul,
Apostate,
StormOfAnguish,
ShriekerOfDread,
ShirokenElementalist,
PainTitan,
ShirokenMonk,
ShirokenMesmer,
TitanAbomination,
BoundTiendi,
BoundKaichen,
WrathfulStorm,
GraspOfInsanity,
TormentClaw,
BoundHaoLi,
IgnisCruor,
ShadowBeast,
CreoVulnero,
PortalWraith,
ArmageddonLord,
JoyousSoul,
MagridTheSly,
Zenmai,
Abaddon
};
+1 -1
View File
@@ -2,7 +2,7 @@
namespace Daybreak.Models.Guildwars;
public readonly struct PathingData
public sealed class PathingData
{
public List<Trapezoid> Trapezoids { get; init; }
public List<List<int>> ComputedPathingMaps { get; init; }
+1 -1
View File
@@ -1,6 +1,6 @@
namespace Daybreak.Models.Guildwars;
public readonly struct PathingMetadata
public sealed class PathingMetadata
{
public int TrapezoidCount { get; init; }
}
@@ -2,7 +2,7 @@
namespace Daybreak.Models.Guildwars;
public readonly struct PlayerInformation : IEntity
public sealed class PlayerInformation : IEntity
{
public int Id { get; init; }
public uint Timer { get; init; }
+1 -1
View File
@@ -1,6 +1,6 @@
namespace Daybreak.Models.Guildwars;
public readonly struct QuestMetadata : IPositionalEntity
public sealed class QuestMetadata : IPositionalEntity
{
public Quest? Quest { get; init; }
public Position? Position { get; init; }
+811 -30
View File
@@ -6,44 +6,824 @@ namespace Daybreak.Models.Guildwars;
public sealed class Region : IWikiEntity
{
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 Kryta { get; } = new Region
{
Id = 0,
Name = "Kryta",
WikiUrl = "https://wiki.guildwars.com/wiki/Kryta",
Maps = new List<Map>
{
Map.LionsArchCanthanNewYearOutpost,
Map.LionsArchHalloweenOutpost,
Map.LionsArchOutpost,
Map.LionsArchSunspearsinKryta,
Map.LionsArchWintersdayOutpost,
Map.BeetletunOutpost,
Map.BergenHotSpringsOutpost,
Map.FishermensHavenOutpost,
Map.TempleOfTheAges,
Map.TempleOfTheAgesROX,
Map.DAlessioSeaboard,
Map.DivinityCoast,
Map.GatesOfKryta,
Map.RiversideProvince,
Map.WarinKrytaRiversideProvince,
Map.SanctumCay,
Map.CursedLands,
Map.KessexPeak,
Map.LionsGate,
Map.MajestysRest,
Map.NeboTerrace,
Map.NorthKrytaProvince,
Map.ScoundrelsRise,
Map.StingrayStrand,
Map.TalmarkWilderness,
Map.TearsOfTheFallen,
Map.TheBlackCurtain,
Map.TwinSerpentLakes,
Map.WatchtowerCoast
}
};
public static Region MaguumaJungle { get; } = new Region
{
Id = 1,
Name = "Maguuma Jungle",
WikiUrl = "https://wiki.guildwars.com/wiki/Maguuma_Jungle",
Maps = new List<Map>
{
Map.HengeOfDenraviOutpost,
Map.DruidsOverlookOutpost,
Map.MaguumaStadeOutpost,
Map.QuarrelFallsOutpost,
Map.VentarisRefugeOutpost,
Map.AuroraGlade,
Map.BloodstoneFen,
Map.BloodstoneFenQuest,
Map.TheWilds,
Map.DryTop,
Map.EttinsBack,
Map.MajestysRest,
Map.MamnoonLagoon,
Map.ReedBog,
Map.SageLands,
Map.Silverwood,
Map.TangleRoot,
Map.TheFalls
}
};
public static Region Ascalon { get; } = new Region
{
Id = 2,
Name = "Ascalon",
WikiUrl = "https://wiki.guildwars.com/wiki/Ascalon",
Maps = new List<Map>
{
Map.AscalonCityOutpost,
Map.AscalonCityWintersdayOutpost,
Map.FrontierGateOutpost,
Map.EasternFrontier,
Map.GrendichCourthouseOutpost,
Map.SpecialOpsGrendichCourthouse,
Map.PikenSquareOutpost,
Map.SardelacSanitariumOutpost,
Map.SerenityTempleOutpost,
Map.FortRanik,
Map.RuinsOfSurmia,
Map.NolaniAcademy,
Map.TheGreatNorthernWall,
Map.AscalonArena,
Map.AscalonArenaMission,
Map.AscalonFoothills,
Map.DiessaLowlands,
Map.DragonsGullet,
Map.EasternFrontier,
Map.FlameTempleCorridor,
Map.OldAscalon,
Map.PockmarkFlats,
Map.RegentValley,
Map.TheBreach
}
};
public static Region ShiverpeakMountains { get; } = new Region
{
Id = 3,
Name = "Shiverpeak Mountains",
WikiUrl = "https://wiki.guildwars.com/wiki/Shiverpeak_Mountains",
Maps = new List<Map>
{
Map.BeaconsPerchOutpost,
Map.IceToothCaveOutpost,
Map.YaksBendOutpost,
Map.BorlisPass,
Map.TheFrostGate,
Map.ShiverpeakArena,
Map.ShiverpeakArenaMission,
Map.ShiverpeakArenaMission2,
Map.ShiverpeakArenaMission3,
Map.AnvilRock,
Map.DeldrimorBowl,
Map.GriffonsMouth,
Map.IronHorseMine,
Map.TravelersVale,
Map.DroknarsForgeOutpost,
Map.DroknarsForgeWintersdayOutpost,
Map.DroknarsForgeHalloweenOutpost,
Map.DroknarsForgeCinematic,
Map.CampRankorOutpost,
Map.CopperhammerMinesOutpost,
Map.DeldrimorWarCampOutpost,
Map.MarhansGrottoOutpost,
Map.PortSledgeOutpost,
Map.TheGraniteCitadelOutpost,
Map.IceCavesofSorrow,
Map.IronMinesofMoladune,
Map.ThunderheadKeep,
Map.DreadnoughtsDrift,
Map.FrozenForest,
Map.GrenthsFootprint,
Map.IceFloe,
Map.Icedome,
Map.LornarsPass,
Map.MineralSprings,
Map.SnakeDance,
Map.SorrowsFurnace,
Map.SpearheadPeak,
Map.TalusChute,
Map.TascasDemise,
Map.WitmansFolly
}
};
public static Region HeroesAscent { get; } = new Region
{
Id = 4,
Name = "Heroes' Ascent",
WikiUrl = "https://wiki.guildwars.com/wiki/Heroes%27_Ascent",
Maps = new List<Map>
{
Map.BurialMoundsMission,
Map.FetidRiverMission,
Map.TheUnderworldArenaMission,
Map.UnholyTemplesMission,
Map.ForgottenShrinesMission,
Map.GoldenGatesMission,
Map.TheCourtyardArenaMission,
Map.TheVaultMission,
Map.TheHallOfHeroesArenaMission,
Map.BrokenTowerMission,
Map.SacredTemplesMission,
Map.ScarredEarth,
Map.ScarredEarth2
}
};
public static Region CrystalDesert { get; } = new Region
{
Id = 5,
Name = "Crystal Desert",
WikiUrl = "https://wiki.guildwars.com/wiki/Crystal_Desert",
Maps = new List<Map>
{
Map.TheAmnoonOasisOutpost,
Map.DestinysGorgeOutpost,
Map.HeroesAudienceOutpost,
Map.SeekersPassageOutpost,
Map.TombOfThePrimevalKings,
Map.TombOfThePrimevalKingsHalloweenOutpost,
Map.AuguryRockMission,
Map.AuguryRockOutpost,
Map.DunesOfDespair,
Map.ElonaReach,
Map.ThirstyRiver,
Map.TheDragonsLair,
Map.DivinersAscent,
Map.ProphetsPath,
Map.SaltFlats,
Map.SkywardReach,
Map.TheAridSea,
Map.TheScar,
Map.VultureDrifts
}
};
public static Region RingOfFireIslands { get; } = new Region
{
Id = 6,
Name = "Ring of Fire Islands",
WikiUrl = "https://wiki.guildwars.com/wiki/Ring_of_Fire_Islands",
Maps = new List<Map>
{
Map.EmberLightCampOutpost,
Map.AbaddonsMouth,
Map.HellsPrecipice,
Map.RingOfFire,
Map.PerditionRock,
Map.TheFissureofWoe
}
};
public static Region PresearingAscalon { get; } = new Region
{
Id = 7,
Name = "Pre Searing Ascalon",
WikiUrl = "https://wiki.guildwars.com/wiki/Ascalon_(pre-Searing)",
Maps = new List<Map>
{
Map.AscalonCityPresearing,
Map.AshfordAbbeyOutpost,
Map.AshfordCatacombs1070AE,
Map.FoiblesFairOutpost,
Map.FortRanikPreSearingOutpost,
Map.TheBarradinEstateOutpost,
Map.AscalonAcademyPvPBattleMission,
Map.GreenHillsCounty,
Map.LakesideCounty,
Map.LakesideCounty1070AE,
Map.RegentValleyPreSearing,
Map.TheCatacombs,
Map.TheNorthlands,
Map.WizardsFolly
}
};
public static Region KainengCity { get; } = new Region
{
Id = 8,
Name = "Kaineng City",
WikiUrl = "https://wiki.guildwars.com/wiki/Kaineng_City",
Maps = new List<Map>
{
Map.KainengCenterOutpost,
Map.KainengCenterCanthanNewYearOutpost,
Map.KainengCenterSunspearsInCantha,
Map.KainengCenterWindsOfChangeAChanceEncounter,
Map.KainengCenterWindsOfChangeRaidonKainengCenter,
Map.MaatuKeepOutpost,
Map.SenjisCornerOutpost,
Map.TheMarketplaceOutpost,
Map.TheMarketplaceAreaTrackingtheCorruption,
Map.ZinKuCorridorOutpost,
Map.DragonsThroat,
Map.DragonsThroatAreaWhatWaitsInShadow,
Map.ImperialSanctumOutpostMission,
Map.NahpuiQuarterOutpostMission,
Map.NahpuiQuarterExplorable,
Map.RaisuPalace,
Map.RaisuPalaceOutpostMission,
Map.SunjiangDistrictExplorable,
Map.SunjiangDistrictOutpostMission,
Map.TahnnakaiTempleOutpostMission,
Map.TahnnakaiTempleExplorable,
Map.TahnnakaiTempleWindsOfChangeTheRescueAttempt,
Map.VizunahSquareMission,
Map.VizunahSquareForeignQuarterOutpost,
Map.VizunahSquareLocalQuarterOutpost,
Map.BejunkanPier,
Map.BukdekByway,
Map.BukdekBywayWindsOfChangeCanthaCourierCrisis,
Map.DivinePath,
Map.KainengDocks,
Map.PongmeiValley,
Map.RaisuPavilion,
Map.ShadowsPassage,
Map.ShenzunTunnels,
Map.TheUndercity,
Map.WajjunBazaar,
Map.WajjunBazaarPOX,
Map.WajjunBazaarWindsOfChangeMinistryOfOppression,
Map.WajjunBazaarWindsOfChangeViolenceInTheStreets,
Map.XaquangSkyway
}
};
public static Region EchovaldForest { get; } = new Region
{
Id = 9,
Name = "Echovald Forest",
WikiUrl = "https://wiki.guildwars.com/wiki/Echovald_Forest",
Maps = new List<Map>
{
Map.HouseZuHeltzerOutpost,
Map.AspenwoodGateKurzickOutpost,
Map.BrauerAcademyOutpost,
Map.DurheimArchivesOutpost,
Map.JadeFlatsKurzickOutpost,
Map.LutgardisConservatoryOutpost,
Map.SaintAnjekasShrineOutpost,
Map.TanglewoodCopseOutpost,
Map.VasburgArmoryOutpost,
Map.AltrummRuins,
Map.AltrummRuinsFindingJinnai,
Map.AmatzBasin,
Map.ArborstoneOutpostMission,
Map.ArborstoneExplorable,
Map.FortAspenwoodKurzickOutpost,
Map.FortAspenwoodMission,
Map.TheEternalGrove,
Map.TheEternalGroveOutpostMission,
Map.TheJadeQuarryKurzickOutpost,
Map.UnwakingWatersKurzickOutpost,
Map.UrgozsWarren,
Map.DrazachThicket,
Map.Ferndale,
Map.MelandrusHope,
Map.MorostavTrail,
Map.MourningVeilFalls
}
};
public static Region TheJadeSea { get; } = new Region
{
Id = 10,
Name = "The Jade Sea",
WikiUrl = "https://wiki.guildwars.com/wiki/The_Jade_Sea",
Maps = new List<Map>
{
Map.CavalonOutpost,
Map.AspenwoodGateLuxonOutpost,
Map.BaiPaasuReachOutpost,
Map.BreakerHollowOutpost,
Map.EredonTerraceOutpost,
Map.HarvestTempleOutpost,
Map.JadeFlatsLuxonOutpost,
Map.LeviathanPitsOutpost,
Map.SeafarersRestOutpost,
Map.BoreasSeabedExplorable,
Map.BoreasSeabedOutpostMission,
Map.FortAspenwoodLuxonOutpost,
Map.GyalaHatchery,
Map.GyalaHatcheryOutpostMission,
Map.TheAuriosMines,
Map.TheDeep,
Map.TheJadeQuarryLuxonOutpost,
Map.TheJadeQuarryMission,
Map.UnwakingWaters,
Map.UnwakingWatersLuxonOutpost,
Map.UnwakingWatersMission,
Map.ZosShivrosChannel,
Map.Archipelagos,
Map.MaishangHills,
Map.MountQinkai,
Map.RheasCrater,
Map.SilentSurf
}
};
public static Region ShingJeaIsland { get; } = new Region
{
Id = 11,
Name = "Shing Jea Island",
WikiUrl = "https://wiki.guildwars.com/wiki/Shing_Jea_Island",
Maps = new List<Map>
{
Map.ShingJeaArena,
Map.ShingJeaArenaMission,
Map.ShingJeaMonasteryCanthanNewYearOutpost,
Map.ShingJeaMonasteryDragonFestivalOutpost,
Map.ShingJeaMonasteryMission,
Map.ShingJeaMonasteryOutpost,
Map.ShingJeaMonasteryRaidOnShingJeaMonastery,
Map.RanMusuGardensOutpost,
Map.SeitungHarborAreaDeadlyCargo,
Map.SeitungHarborMission,
Map.SeitungHarborMission2,
Map.SeitungHarborOutpost,
Map.TsumeiVillageMission,
Map.TsumeiVillageMission2,
Map.TsumeiVillageOutpost,
Map.TsumeiVillageWindsOfChangeATreatysATreaty,
Map.MinisterChosEstateExplorable,
Map.MinisterChosEstateMission2,
Map.MinisterChosEstateOutpostMission,
Map.ZenDaijunExplorable,
Map.ZenDaijunOutpostMission,
Map.HaijuLagoon,
Map.HaijuLagoonMission,
Map.JayaBluffs,
Map.JayaBluffsMission,
Map.KinyaProvince,
Map.LinnokCourtyard,
Map.MonasteryOverlook1,
Map.MonasteryOverlook2,
Map.PanjiangPeninsula,
Map.SaoshangTrail,
Map.SunquaVale
}
};
public static Region Kourna { get; } = new Region
{
Id = 12,
Name = "Kourna",
WikiUrl = "https://wiki.guildwars.com/wiki/Kourna",
Maps = new List<Map>
{
Map.SunspearSanctuaryOutpost,
Map.CampHojanuOutpost,
Map.WehhanTerracesOutpost,
Map.YohlonHavenOutpost,
Map.DajkahInlet,
Map.KodonurCrossroads,
Map.ModdokCrevice,
Map.NunduBay,
Map.PogahnPassage,
Map.RilohnRefuge,
Map.VentaCemetery,
Map.ArkjokWard,
Map.BahdokCaverns,
Map.BarbarousShore,
Map.CommandPost,
Map.DejarinEstate,
Map.GandaraTheMoonFortress,
Map.JahaiBluffs,
Map.MargaCoast,
Map.SunwardMarches,
Map.TheFloodplainOfMahnkelon,
Map.TuraisProcession,
Map.NightfallenCoast
}
};
public static Region Vabbi { get; } = new Region
{
Id = 13,
Name = "Vabbi",
WikiUrl = "https://wiki.guildwars.com/wiki/Vabbi",
Maps = new List<Map>
{
Map.TheKodashBazaarOutpost,
Map.BasaltGrottoOutpost,
Map.ChantryOfSecretsOutpost,
Map.HonurHillOutpost,
Map.MihanuTownshipOutpost,
Map.YahnurMarketOutpost,
Map.DashaVestibule,
Map.DzagonurBastion,
Map.GrandCourtOfSebelkeh,
Map.JennursHorde,
Map.TiharkOrchard,
Map.BokkaAmphitheatre,
Map.BokkaAmphitheatreNOX,
Map.ForumHighlands,
Map.GardenOfSeborhin,
Map.HoldingsOfChokhin,
Map.ResplendentMakuun,
Map.ResplendentMakuun2,
Map.TheHiddenCityOfAhdashim,
Map.TheMirrorOfLyss,
Map.VehjinMines,
Map.VehtendiValley,
Map.WildernessOfBahdza,
Map.YatendiCanyons
}
};
public static Region TheDesolation { get; } = new Region
{
Id = 14,
Name = "TheDesolation",
WikiUrl = "https://wiki.guildwars.com/wiki/The_Desolation",
Maps = new List<Map>
{
Map.BonePalaceOutpost,
Map.LairOfTheForgottenOutpost,
Map.TheMouthOfTormentOutpost,
Map.GateOfDesolation,
Map.RuinsOfMorah,
Map.RemainsOfSahlahja,
Map.CrystalOverlook,
Map.JokosDomain,
Map.PoisonedOutcrops,
Map.TheAlkaliPan,
Map.TheRupturedHeart,
Map.TheShatteredRavines,
Map.TheSulfurousWastes
}
};
public static Region Istan { get; } = new Region
{
Id = 15,
Name = "Istan",
WikiUrl = "https://wiki.guildwars.com/wiki/Istan",
Maps = new List<Map>
{
Map.KamadanJewelOfIstanCanthanNewYearOutpost,
Map.KamadanJewelOfIstanExplorable,
Map.KamadanJewelOfIstanHalloweenOutpost,
Map.KamadanJewelOfIstanOutpost,
Map.KamadanJewelOfIstanWintersdayOutpost,
Map.KamadanMission,
Map.BeknurHarbor,
Map.BeknurHarborOutpost,
Map.ChampionsDawnOutpost,
Map.KodlonuHamletOutpost,
Map.SunspearArena,
Map.SunspearArenaMission,
Map.SunspearGreatHallOutpost,
Map.TheAstralariumOutpost,
Map.BlacktideDen,
Map.ChahbekVillage,
Map.Consulate,
Map.ConsulateDocks,
Map.JokanurDiggings,
Map.ChurrhirFields,
Map.CliffsOfDohjok,
Map.FahranurMission,
Map.FahranurTheFirstCity,
Map.IslandOfShehkah,
Map.IssnurIsles,
Map.LahtendaBog,
Map.MehtaniKeys,
Map.PlainsOfJarin,
Map.SunDocks,
Map.ZehlonReach
}
};
public static Region RealmOfTorment { get; } = new Region
{
Id = 16,
Name = "Realm of Torment",
WikiUrl = "https://wiki.guildwars.com/wiki/Realm_of_Torment",
Maps = new List<Map>
{
Map.DomainOfAnguish,
Map.GateOfTormentOutpost,
Map.GateOfFearOutpost,
Map.GateOfSecretsOutpost,
Map.GateOftheNightfallenLandsOutpost,
Map.AbaddonsGate,
Map.GateOfMadness,
Map.GateOfPain,
Map.TheShadowNexus,
Map.TheEbonyCitadelOfMallyxMission,
Map.DepthsOfMadness,
Map.DomainOfFear,
Map.DomainOfPain,
Map.DomainOfSecrets,
Map.HeartOfAbaddon,
Map.NightfallenGarden,
Map.NightfallenJahai,
Map.ThroneOfSecrets
}
};
public static Region TarnishedCoast { get; } = new Region
{
Id = 17,
Name = "Tarnished Coast",
WikiUrl = "https://wiki.guildwars.com/wiki/Tarnished_Coast",
Maps = new List<Map>
{
Map.RataSumOutpost,
Map.GaddsEncampmentOutpost,
Map.TarnishedHavenOutpost,
Map.UmbralGrottoOutpost,
Map.VloxsFalls,
Map.FindingTheBloodstoneMission,
Map.FindingTheBloodstoneLevel1,
Map.FindingTheBloodstoneLevel2,
Map.FindingTheBloodstoneLevel3,
Map.TheElusiveGolemancerMission,
Map.TheElusiveGolemancerLevel1,
Map.TheElusiveGolemancerLevel2,
Map.TheElusiveGolemancerLevel3,
Map.GeniusOperatedLivingEnchantedManifestation,
Map.GeniusOperatedLivingEnchantedManifestationMission,
Map.ArborBay,
Map.AlcaziaTangle,
Map.MagusStones,
Map.PolymockColiseum,
Map.RivenEarth,
Map.SparkflySwamp,
Map.VerdantCascades
}
};
public static Region DepthsOfTyria { get; } = new Region
{
Id = 18,
Name = "Depths Of Tyria",
WikiUrl = "https://wiki.guildwars.com/wiki/Depths_of_Tyria",
Maps = new List<Map>
{
Map.CentralTransferChamberOutpost,
Map.DestructionsDepthsMission,
Map.DestructionsDepthsLevel1,
Map.DestructionsDepthsLevel2,
Map.DestructionsDepthsLevel3,
Map.ATimeForHeroes,
Map.ATimeForHeroesMission,
Map.BattledepthsLevel1,
Map.BattledepthsLevel2,
Map.BattledepthsLevel3,
Map.BeneathLionsArch,
Map.CavernsBelowKamadan,
Map.TunnelsBelowCantha,
Map.WarinKrytaTheMausoleum,
Map.ArachnisHauntLevel1,
Map.ArachnisHauntLevel2,
Map.BloodstoneCavesLevel1,
Map.BloodstoneCavesLevel2,
Map.BloodstoneCavesLevel3,
Map.BogrootGrowthsLevel1,
Map.BogrootGrowthsLevel2,
Map.CatacombsofKathandraxLevel1,
Map.CatacombsofKathandraxLevel2,
Map.CatacombsofKathandraxLevel3,
Map.CathedralofFlamesLevel1,
Map.CathedralofFlamesLevel2,
Map.CathedralofFlamesLevel3,
Map.DarkrimeDelvesLevel1,
Map.DarkrimeDelvesLevel2,
Map.DarkrimeDelvesLevel3,
Map.FronisIrontoesLairMission,
Map.HeartOftheShiverpeaksLevel1,
Map.HeartOftheShiverpeaksLevel2,
Map.HeartOftheShiverpeaksLevel3,
Map.OolasLabLevel1,
Map.OolasLabLevel2,
Map.OolasLabLevel3,
Map.OozePit,
Map.OozePitMission,
Map.RavensPointLevel1,
Map.RavensPointLevel2,
Map.RavensPointLevel3,
Map.RragarsMenagerieLevel1,
Map.RragarsMenagerieLevel2,
Map.RragarsMenagerieLevel3,
Map.SecretLairOftheSnowmen,
Map.SecretLairOftheSnowmen2,
Map.SecretLairOftheSnowmen3,
Map.SepulchreOfDragrimmarLevel1,
Map.SepulchreOfDragrimmarLevel2,
Map.ShardsOfOrrLevel1,
Map.ShardsOfOrrLevel2,
Map.ShardsOfOrrLevel3,
Map.SlaversExileLevel1,
Map.SlaversExileLevel2,
Map.SlaversExileLevel3,
Map.SlaversExileLevel4,
Map.SlaversExileLevel5,
Map.VloxenExcavationsLevel1,
Map.VloxenExcavationsLevel2,
Map.VloxenExcavationsLevel3
}
};
public static Region FarShiverpeaks { get; } = new Region
{
Id = 19,
Name = "Far Shiverpeaks",
WikiUrl = "https://wiki.guildwars.com/wiki/Far_Shiverpeaks",
Maps = new List<Map>
{
Map.GunnarsHoldOutpost,
Map.BorealStationOutpost,
Map.EyeOfTheNorthOutpost,
Map.EyeOfTheNorthOutpostWintersdayOutpost,
Map.LongeyesLedgeOutpost,
Map.OlafsteadOutpost,
Map.OlafsteadCinematic,
Map.SifhallaOutpost,
Map.CurseOfTheNornbear,
Map.CurseOfTheNornbearMission,
Map.CinematicCaveNornCursed,
Map.AGateTooFarMission,
Map.AGateTooFarLevel1,
Map.AGateTooFarLevel2,
Map.AGateTooFarLevel3,
Map.BloodWashesBlood,
Map.BloodWashesBloodMission,
Map.TheNornFightingTournament,
Map.BjoraMarches,
Map.DrakkarLake,
Map.Epilogue,
Map.HallOfMonuments,
Map.IceCliffChasms,
Map.JagaMoraine,
Map.NorrhartDomains,
Map.PolymockGlacier,
Map.VarajarFells
}
};
public static Region CharrHomelands { get; } = new Region
{
Id = 20,
Name = "Charr Homelands",
WikiUrl = "https://wiki.guildwars.com/wiki/Charr_Homelands",
Maps = new List<Map>
{
Map.DoomloreShrineOutpost,
Map.AgainstTheCharr,
Map.AgainstTheCharrMission,
Map.WarbandOfBrothersMission,
Map.WarbandOfBrothersLevel1,
Map.WarbandOfBrothersLevel2,
Map.WarbandOfBrothersLevel3,
Map.AssaultOnTheStronghold,
Map.AssaultOnTheStrongholdMission,
Map.DaladaUplands,
Map.GrothmarWardowns,
Map.PolymockCrossing,
Map.SacnothValley
}
};
public static Region TheBattleIsles { get; } = new Region
{
Id = 21,
Name = "The Battle Isles",
WikiUrl = "https://wiki.guildwars.com/wiki/The_Battle_Isles",
Maps = new List<Map>
{
Map.GreatTempleOfBalthazarOutpost,
Map.IsleOfTheDeadGuildHall,
Map.IsleOfTheDeadGuildHallMission,
Map.IsleOfTheDeadGuildHallOutpost,
Map.BurningIsle,
Map.BurningIsleMission,
Map.BurningIsleOutpost,
Map.DruidsIsle,
Map.DruidsIsleMission,
Map.DruidsIsleOutpost,
Map.FrozenIsle,
Map.FrozenIsleMission,
Map.FrozenIsleOutpost,
Map.HuntersIsle,
Map.HuntersIsleMission,
Map.HuntersIsleOutpost,
Map.NomadsIsle,
Map.NomadsIsleMission,
Map.NomadsIsleOutpost,
Map.WarriorsIsle,
Map.WarriorsIsleMission,
Map.WarriorsIsleOutpost,
Map.WizardsIsle,
Map.WizardsIsleMission,
Map.WizardsIsleOutpost,
Map.ImperialIsle,
Map.ImperialIsleMission,
Map.ImperialIsleOutpost,
Map.IsleOfJade,
Map.IsleOfJadeMission,
Map.IsleOfJadeOutpost,
Map.IsleOfMeditation,
Map.IsleOfMeditationMission,
Map.IsleOfMeditationOutpost,
Map.IsleOfWeepingStone,
Map.IsleOfWeepingStoneMission,
Map.IsleOfWeepingStoneOutpost,
Map.CorruptedIsle,
Map.CorruptedIsleMission,
Map.CorruptedIsleOutpost,
Map.IsleOfSolitude,
Map.IsleOfSolitudeMission,
Map.IsleOfSolitudeOutpost,
Map.IsleOfWurms,
Map.IsleOfWurmsMission,
Map.IsleOfWurmsOutpost,
Map.UnchartedIsle,
Map.UnchartedIsleMission,
Map.UnchartedIsleOutpost
}
};
public static Region TheBattleOfJahai { get; } = new Region
{
Id = 22,
Name = "The Battle Of Jahai",
WikiUrl = "https://wiki.guildwars.com/wiki/The_Battle_of_Jahai",
Maps = new List<Map>
{
Map.TheBattleOfJahai
}
};
public static Region TheFlightNorth { get; } = new Region
{
Id = 23,
Name = "The Flight North",
WikiUrl = "https://wiki.guildwars.com/wiki/The_Flight_North",
Maps = new List<Map>
{
Map.TheFlightNorth
}
};
public static Region TheTenguAccords { get; } = new Region
{
Id = 24,
Name = "The Tengu Accords",
WikiUrl = "https://wiki.guildwars.com/wiki/The_Tengu_Accords",
Maps = new List<Map>
{
Map.TheTenguAccords
}
};
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",
Maps = new List<Map>
{
Map.TheRiseOfTheWhiteMantle
}
};
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()
public static IReadOnlyList<Region> Regions { get; } = new List<Region>()
{
Kryta,
MaguumaJungle,
Ascalon,
NorthernShiverpeaks,
ShiverpeakMountains,
HeroesAscent,
CrystalDesert,
FissureOfWoe,
RingOfFireIslands,
PresearingAscalon,
KainengCity,
EchovaldForest,
@@ -52,7 +832,7 @@ public sealed class Region : IWikiEntity
Vabbi,
TheDesolation,
Istan,
DomainOfAnguish,
RealmOfTorment,
TarnishedCoast,
DepthsOfTyria,
FarShiverpeaks,
@@ -112,4 +892,5 @@ public sealed class Region : IWikiEntity
public int Id { get; init; }
public string? Name { get; init; }
public string? WikiUrl { get; init; }
public IReadOnlyList<Map>? Maps { get; init; }
}
@@ -1,6 +1,6 @@
namespace Daybreak.Models.Guildwars;
public readonly struct SessionInformation
public sealed class SessionInformation
{
public uint FoesKilled { get; init; }
public uint FoesToKill { get; init; }
+1 -1
View File
@@ -1,6 +1,6 @@
namespace Daybreak.Models.Guildwars;
public readonly struct SkillMetadata
public sealed class SkillMetadata
{
public Skill? Skill { get; init; }
public uint Adrenaline1 { get; init; }
@@ -1,6 +1,6 @@
namespace Daybreak.Models.Guildwars;
public readonly struct TitleInformation
public sealed class TitleInformation
{
public bool IsValid => this.CurrentPoints != 0 && this.PointsForCurrentRank != 0 && this.PointsForNextRank != 0 && this.TierNumber != 0 && this.MaxTierNumber != 0;
+1 -1
View File
@@ -2,7 +2,7 @@
namespace Daybreak.Models.Guildwars;
public readonly struct UnknownBagItem : IBagContent
public sealed class UnknownBagItem : IBagContent
{
public uint ItemId { get; init; }
public uint Slot { get; init; }
+9
View File
@@ -0,0 +1,9 @@
namespace Daybreak.Models.Guildwars;
public sealed class WorldData
{
public Campaign? Campaign { get; init; }
public Continent? Continent { get; init; }
public Region? Region { get; init; }
public Map? Map { get; init; }
}
@@ -2,7 +2,7 @@
namespace Daybreak.Models.Guildwars;
public readonly struct WorldPlayerInformation : IEntity
public sealed class WorldPlayerInformation : IEntity
{
public string? Name { get; init; }
public uint Timer { get; init; }
@@ -0,0 +1,47 @@
using System.Runtime.InteropServices;
namespace Daybreak.Models.Interop;
[StructLayout(LayoutKind.Explicit)]
public readonly struct AreaInfoContext
{
[FieldOffset(0x0000)]
public readonly uint CampaignId;
[FieldOffset(0x0004)]
public readonly uint ContinentId;
[FieldOffset(0x0008)]
public readonly uint RegionId;
[FieldOffset(0x000C)]
public readonly RegionType 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(0x0030)]
public readonly uint MinLevel;
[FieldOffset(0x0034)]
public readonly uint MaxLevel;
[FieldOffset(0x0040)]
public readonly uint IconPositionX;
[FieldOffset(0x0044)]
public readonly uint IconPositionY;
[FieldOffset(0x0048)]
public readonly uint IconStartX;
[FieldOffset(0x004C)]
public readonly uint IconStartY;
[FieldOffset(0x0050)]
public readonly uint IconEndX;
[FieldOffset(0x0054)]
public readonly uint IconEndY;
public bool HasEnterButton => (this.Flags & 0x100) != 0 || (this.Flags & 0x40000) != 0;
public bool IsOnWorldMap => (this.Flags & 0x20) == 0;
public bool IsPvp => (this.Flags & 0x1) != 0;
public bool IsGuildHall => (this.Flags & 0x800000) != 0;
}
@@ -0,0 +1,9 @@
namespace Daybreak.Models.Interop;
public readonly struct InstanceInfoContext
{
public readonly GuildwarsPointer<uint> TerrainInfo;
public readonly InstanceType InstanceType;
public readonly GuildwarsPointer<AreaInfoContext> AreaInfo;
public readonly uint TerrainCount;
public readonly GuildwarsPointer<uint> TerrainInfo2;
}
+8
View File
@@ -0,0 +1,8 @@
namespace Daybreak.Models.Interop;
public enum InstanceType
{
Outpost,
Explorable,
Loading
}
+27
View File
@@ -0,0 +1,27 @@
namespace Daybreak.Models.Interop;
public enum RegionType : uint
{
AllianceBattle,
Arena,
ExplorableZone,
GuildBattleArea,
GuildHall,
MissionOutpost,
CooperativeMission,
CompetitiveMission,
EliteMission,
Challenge,
Outpost,
ZaishenBattle,
HeroesAscent,
City,
MissionArea,
HeroBattleOutpost,
HeroBattleArea,
EotnMission,
Dungeon,
Marketplace,
Unknown,
DevRegion
}
@@ -0,0 +1,20 @@
namespace Daybreak.Models.Progress;
public sealed class DSOALInstallationStatus : DownloadStatus
{
public static readonly LoadStatus StartingStep = new DSOALInstallationStep("Starting");
public static readonly LoadStatus ExtractingFiles = new DSOALInstallationStep("Extracting files");
public static readonly LoadStatus SetupOpenALFiles = new DSOALInstallationStep("Setting up OpenAL files");
public static readonly LoadStatus Finished = new DSOALInstallationStep("Installation has finished");
public DSOALInstallationStatus()
{
this.CurrentStep = StartingStep;
}
public sealed class DSOALInstallationStep : LoadStatus
{
internal DSOALInstallationStep(string name) : base(name)
{
}
}
}
@@ -1,9 +1,9 @@
using Daybreak.Configuration.Options;
using Daybreak.Exceptions;
using Daybreak.Services.Credentials;
using Daybreak.Services.Mods;
using Daybreak.Services.Mutex;
using Daybreak.Services.Privilege;
using Daybreak.Services.Scanner;
using Daybreak.Utils;
using Daybreak.Views;
using Microsoft.Extensions.Logging;
@@ -18,7 +18,6 @@ using System.IO;
using System.Linq;
using System.Security;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using System.Windows;
@@ -30,11 +29,10 @@ public class ApplicationLauncher : IApplicationLauncher
private const string ProcessName = "gw";
private const string ArenaNetMutex = "AN-Mute";
private readonly ILiveOptions<UModOptions> uModOptions;
private readonly ILiveOptions<ToolboxOptions> toolboxOptions;
private readonly ILiveOptions<LauncherOptions> launcherOptions;
private readonly ICredentialManager credentialManager;
private readonly IMutexHandler mutexHandler;
private readonly IModsManager modsManager;
private readonly ILogger<ApplicationLauncher> logger;
private readonly IPrivilegeManager privilegeManager;
@@ -42,11 +40,10 @@ public class ApplicationLauncher : IApplicationLauncher
public Process? RunningGuildwarsProcess => this.GetGuildwarsProcess();
public ApplicationLauncher(
ILiveOptions<UModOptions> uModOptions,
ILiveOptions<ToolboxOptions> toolboxOptions,
ILiveOptions<LauncherOptions> launcherOptions,
ICredentialManager credentialManager,
IMutexHandler mutexHandler,
IModsManager modsManager,
ILogger<ApplicationLauncher> logger,
IPrivilegeManager privilegeManager)
{
@@ -54,8 +51,7 @@ public class ApplicationLauncher : IApplicationLauncher
this.mutexHandler = mutexHandler.ThrowIfNull();
this.credentialManager = credentialManager.ThrowIfNull();
this.launcherOptions = launcherOptions.ThrowIfNull();
this.uModOptions = uModOptions.ThrowIfNull();
this.toolboxOptions = toolboxOptions.ThrowIfNull();
this.modsManager = modsManager.ThrowIfNull();
this.privilegeManager = privilegeManager.ThrowIfNull();
}
@@ -66,6 +62,7 @@ public class ApplicationLauncher : IApplicationLauncher
return await auth.Switch<Task<Process?>>(
onSome: async (credentials) =>
{
credentials.ThrowIfNull();
if (configuration.MultiLaunchSupport is true)
{
if (this.privilegeManager.AdminPrivileges is false)
@@ -77,19 +74,13 @@ public class ApplicationLauncher : IApplicationLauncher
this.ClearGwLocks();
}
if (this.uModOptions.Value.Enabled)
{
await this.LaunchUMod();
}
var gwProcess = await this.LaunchGuildwarsProcess(credentials.Username!, credentials.Password!, credentials.CharacterName!);
if (this.toolboxOptions.Value.Enabled)
if (gwProcess is null)
{
await Task.Delay(5000);
await this.LaunchToolbox();
return default;
}
return gwProcess;
},
onNone: () =>
@@ -147,6 +138,11 @@ public class ApplicationLauncher : IApplicationLauncher
args.Add($"\"{character}\"");
}
foreach(var mod in this.modsManager.GetMods())
{
args.AddRange(mod.GetCustomArguments());
}
var identity = this.launcherOptions.Value.LaunchGuildwarsAsCurrentUser ?
System.Security.Principal.WindowsIdentity.GetCurrent().Name :
System.Security.Principal.WindowsIdentity.GetAnonymous().Name;
@@ -156,9 +152,12 @@ public class ApplicationLauncher : IApplicationLauncher
StartInfo = new ProcessStartInfo
{
Arguments = string.Join(" ", args),
FileName = executable.Path
FileName = executable.Path,
}
};
var preLaunchActions = this.modsManager.GetMods().Select(m => m.OnGuildwarsStarting(process));
await Task.WhenAll(preLaunchActions);
if (process.Start() is false)
{
throw new InvalidOperationException($"Unable to launch {executable}");
@@ -200,141 +199,12 @@ public class ApplicationLauncher : IApplicationLauncher
continue;
}
var postLaunchActions = this.modsManager.GetMods().Select(m => m.OnGuildwarsStarted(gwProcess!));
await Task.WhenAll(postLaunchActions);
return gwProcess;
}
}
private async Task<Process?> LaunchUMod()
{
if(this.uModOptions.Value.Enabled is false)
{
throw new InvalidOperationException("Cannot launch uMod. uMod is disabled");
}
var executable = this.uModOptions.Value.Path;
if (File.Exists(executable) is false)
{
throw new ExecutableNotFoundException($"uMod executable doesn't exist at {executable}");
}
if (Process.GetProcessesByName("uMod").FirstOrDefault() is Process existingProcess)
{
this.logger.LogInformation("uMod is already running");
return existingProcess;
}
this.logger.LogInformation($"Launching uMod");
var process = new Process()
{
StartInfo = new ProcessStartInfo
{
FileName = executable,
WorkingDirectory = Path.GetDirectoryName(executable)
}
};
if (process.Start() is false)
{
throw new InvalidOperationException($"Unable to launch {executable}");
}
var retries = 0;
while (true)
{
await Task.Delay(100);
retries++;
var uModProcess = Process.GetProcessesByName("uMod").FirstOrDefault();
if (uModProcess is null && retries < MaxRetries)
{
continue;
}
else if (uModProcess is null && retries >= MaxRetries)
{
throw new InvalidOperationException("Newly launched uMod process not detected");
}
if (uModProcess!.MainWindowHandle == IntPtr.Zero)
{
continue;
}
var titleLength = NativeMethods.GetWindowTextLength(uModProcess.MainWindowHandle);
var titleBuffer = new StringBuilder(titleLength);
_ = NativeMethods.GetWindowText(uModProcess.MainWindowHandle, titleBuffer, titleLength + 1);
var title = titleBuffer.ToString();
if (title != "uMod V 1.0")
{
continue;
}
return uModProcess;
}
}
private async Task<Process?> LaunchToolbox()
{
if (this.toolboxOptions.Value.Enabled is false)
{
throw new InvalidOperationException("Cannot launch GWToolbox. GWToolbox is disabled");
}
var executable = this.toolboxOptions.Value.Path;
if (File.Exists(executable) is false)
{
throw new ExecutableNotFoundException($"GWToolbox executable doesn't exist at {executable}");
}
if (Process.GetProcessesByName("GWToolboxpp").FirstOrDefault() is Process existingProcess)
{
this.logger.LogInformation("GWToolboxpp is already running");
return existingProcess;
}
this.logger.LogInformation($"Launching GWToolbox");
var process = new Process()
{
StartInfo = new ProcessStartInfo
{
FileName = executable
}
};
if (process.Start() is false)
{
throw new InvalidOperationException($"Unable to launch {executable}");
}
var retries = 0;
while (true)
{
await Task.Delay(100);
retries++;
var toolboxProcess = Process.GetProcessesByName("GWToolboxpp").FirstOrDefault();
if (toolboxProcess is null && retries < MaxRetries)
{
continue;
}
else if (toolboxProcess is null && retries >= MaxRetries)
{
throw new InvalidOperationException("Newly launched GWToolbox process not detected");
}
if (toolboxProcess!.MainWindowHandle == IntPtr.Zero)
{
continue;
}
var titleLength = NativeMethods.GetWindowTextLength(toolboxProcess.MainWindowHandle);
var titleBuffer = new StringBuilder(titleLength);
_ = NativeMethods.GetWindowText(toolboxProcess.MainWindowHandle, titleBuffer, titleLength + 1);
var title = titleBuffer.ToString();
if (title != "GWToolbox - Launch")
{
continue;
}
return toolboxProcess;
}
}
private Process? GetGuildwarsProcess()
{
if (this.launcherOptions.Value.MultiLaunchSupport is true)
+113 -12
View File
@@ -1,39 +1,113 @@
using Daybreak.Services.Bloogum.Models;
using Daybreak.Services.Images;
using Daybreak.Services.Scanner;
using Microsoft.Extensions.Logging;
using System;
using System.Core.Extensions;
using System.Extensions;
using System.IO;
using System.Linq;
using System.Net.Http;
using System.Threading;
using System.Threading.Tasks;
using System.Windows.Media;
namespace Daybreak.Services.Bloogum;
public sealed class BloogumClient : IBloogumClient
{
private const string CacheFolder = "Bloogum";
private const string BaseAddress = "http://bloogum.net/guildwars";
private readonly IImageCache imageCache;
private readonly IGuildwarsMemoryCache guildwarsMemoryCache;
private readonly IHttpClient<BloogumClient> httpClient;
private readonly ILogger logger;
private readonly Random random = new();
public BloogumClient(
IImageCache imageCache,
IGuildwarsMemoryCache guildwarsMemoryCache,
ILogger<BloogumClient> logger,
IHttpClient<BloogumClient> httpClient)
{
this.logger = logger.ThrowIfNull(nameof(logger));
this.httpClient = httpClient.ThrowIfNull(nameof(httpClient));
this.imageCache = imageCache.ThrowIfNull();
this.guildwarsMemoryCache = guildwarsMemoryCache.ThrowIfNull();
this.logger = logger.ThrowIfNull();
this.httpClient = httpClient.ThrowIfNull();
}
public async Task<Optional<Stream>> GetRandomScreenShot()
public async Task<ImageSource?> GetImage(bool localized)
{
var location = Location.Locations[this.random.Next(0, Location.Locations.Count)];
var category = location.Categories[this.random.Next(0, location.Categories.Count)];
var picture = this.random.Next(0, category.ImageCount) + 1;
var uri = await this.GetImageUri(localized);
var localUri = Path.GetFullPath(Path.Combine(CacheFolder, uri));
if (!File.Exists(localUri))
{
var imageStream = await this.GetRemoteImage(uri);
if (imageStream is null)
{
return default;
}
var uri = $"{location.LocationName}/{category.CategoryName}/{picture:00}.jpg";
return await this.GetImage(uri).ConfigureAwait(false);
await CacheImage(localUri, imageStream);
imageStream.Dispose();
}
return await this.imageCache.GetImage(localUri);
}
private async Task<Optional<Stream>> GetImage(string url)
private async Task<string> GetImageUri(bool localized)
{
if (!localized)
{
return GetRandomScreenShot();
}
var worldInfo = await this.guildwarsMemoryCache.ReadWorldData(CancellationToken.None);
if (worldInfo is null)
{
return GetRandomScreenShot();
}
var validLocations = Location.Locations.Where(l => l.Region == worldInfo.Region).ToList();
var validCategories = validLocations.SelectMany(l => l.Categories).Where(c => c.Map == worldInfo.Map).ToList();
if (validCategories.None())
{
if (validLocations.None())
{
return GetRandomScreenShot();
}
var location = validLocations[Random.Shared.Next(0, validLocations.Count)];
return GetRandomScreenShot(location);
}
var selectedCategory = validCategories[Random.Shared.Next(0, validCategories.Count)];
if (selectedCategory.ImageCount == 0)
{
if (validLocations.None())
{
return GetRandomScreenShot();
}
var location = validLocations[Random.Shared.Next(0, validLocations.Count)];
return GetRandomScreenShot(location);
}
var selectedLocation = Location.Locations.FirstOrDefault(l => l.Categories.Contains(selectedCategory));
if (selectedLocation is null)
{
if (validLocations.None())
{
return GetRandomScreenShot();
}
var location = validLocations[Random.Shared.Next(0, validLocations.Count)];
return GetRandomScreenShot(location);
}
return GetScreenshotName(selectedLocation, selectedCategory, Random.Shared.Next(0, selectedCategory.ImageCount));
}
private async Task<Stream?> GetRemoteImage(string url)
{
this.logger.LogInformation($"Retrieving image from {BaseAddress}/{url}");
try
@@ -48,13 +122,40 @@ public sealed class BloogumClient : IBloogumClient
else
{
this.logger.LogError($"Failed to retrive image. Status code {response.StatusCode}. Reason {response.ReasonPhrase}");
return Optional.None<Stream>();
return default;
}
}
catch(Exception e)
{
this.logger.LogError(e.ToString());
return Optional.None<Stream>();
return default;
}
}
private static string GetRandomScreenShot()
{
var location = Location.Locations[Random.Shared.Next(0, Location.Locations.Count)];
return GetRandomScreenShot(location);
}
private static string GetRandomScreenShot(Location location)
{
var category = location.Categories[Random.Shared.Next(0, location.Categories.Count)];
var picture = Random.Shared.Next(0, category.ImageCount) + 1;
return GetScreenshotName(location, category, picture);
}
private static string GetScreenshotName(Location location, Category category, int picture)
{
return $"{location.LocationName}/{category.CategoryName}/{picture:00}.jpg";
}
private static async Task CacheImage(string uri, Stream imageStream)
{
var directoryName = Path.GetDirectoryName(uri);
Directory.CreateDirectory(directoryName);
using var fs = File.Create(uri);
await imageStream.CopyToAsync(fs);
}
}
+3 -4
View File
@@ -1,10 +1,9 @@
using System.Extensions;
using System.IO;
using System.Threading.Tasks;
using System.Threading.Tasks;
using System.Windows.Media;
namespace Daybreak.Services.Bloogum;
public interface IBloogumClient
{
Task<Optional<Stream>> GetRandomScreenShot();
Task<ImageSource?> GetImage(bool localized);
}
+6 -2
View File
@@ -1,12 +1,16 @@
namespace Daybreak.Services.Bloogum.Models;
using Daybreak.Models.Guildwars;
namespace Daybreak.Services.Bloogum.Models;
public sealed class Category
{
public Map Map { get; }
public string CategoryName { get; }
public int ImageCount { get; }
public Category(string categoryName, int imageCount)
public Category(Map map, string categoryName, int imageCount)
{
this.Map = map;
this.CategoryName = categoryName;
this.ImageCount = imageCount;
}
File diff suppressed because it is too large Load Diff
+160
View File
@@ -0,0 +1,160 @@
using Daybreak.Configuration.Options;
using Daybreak.Models.Progress;
using Daybreak.Services.Downloads;
using Microsoft.Extensions.Logging;
using System;
using System.Collections.Generic;
using System.Configuration;
using System.Core.Extensions;
using System.Diagnostics;
using System.IO;
using System.IO.Compression;
using System.Linq;
using System.Threading.Tasks;
namespace Daybreak.Services.DSOAL;
/// <summary>
/// Service for managing DSOAL for GW1. Credits to: https://lemmy.wtf/post/27911
/// </summary>
public sealed class DSOALService : IDSOALService
{
private const string DownloadUrl = "https://github.com/ChthonVII/dsoal-GW1/releases/download/r420%2Bgw1_rev1/dsoal-GW1_r420+gw1_rev1.zip";
private const string ArchiveName = "dsoal-GW1_r420+gw1_rev1.zip";
private const string DSOALDirectory = "DSOAL";
private const string HRTFArchiveName = "HRTF_OAL_1.19.0.zip";
private const string DsoundDll = "dsound.dll";
private const string DSOALAldrvDll = "dsoal-aldrv.dll";
private const string AlsoftIni = "alsoft.ini";
private const string OpenAlDirectory = "openal";
private readonly IDownloadService downloadService;
private readonly ILiveUpdateableOptions<DSOALOptions> options;
private readonly ILogger<DSOALService> logger;
public bool IsEnabled
{
get => this.options.Value.Enabled;
set
{
this.options.Value.Enabled = value;
this.options.UpdateOption();
}
}
public bool IsInstalled => Directory.Exists(this.options.Value.Path) &&
Directory.Exists(Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), OpenAlDirectory)) &&
File.Exists(Path.Combine(DSOALDirectory, DsoundDll)) &&
File.Exists(Path.Combine(DSOALDirectory, AlsoftIni)) &&
File.Exists(Path.Combine(DSOALDirectory, AlsoftIni));
public DSOALService(
IDownloadService downloadService,
ILiveUpdateableOptions<DSOALOptions> options,
ILogger<DSOALService> logger)
{
this.downloadService = downloadService.ThrowIfNull();
this.options = options.ThrowIfNull();
this.logger = logger.ThrowIfNull();
}
public async Task<bool> SetupDSOAL(DSOALInstallationStatus dSOALInstallationStatus)
{
if (this.IsInstalled)
{
return true;
}
if ((await this.downloadService.DownloadFile(DownloadUrl, ArchiveName, dSOALInstallationStatus)) is false)
{
this.logger.LogError("Failed to install DSOAL");
return false;
}
this.logger.LogInformation("Extracting DSOAL files");
dSOALInstallationStatus.CurrentStep = DSOALInstallationStatus.ExtractingFiles;
this.ExtractFiles();
dSOALInstallationStatus.CurrentStep = DSOALInstallationStatus.SetupOpenALFiles;
this.SetupHrtfAndPresetFiles();
dSOALInstallationStatus.CurrentStep = DSOALInstallationStatus.Finished;
return true;
}
public IEnumerable<string> GetCustomArguments()
{
if (this.options.Value.Enabled)
{
return new List<string> { "-dsound" };
}
else
{
return Enumerable.Empty<string>();
}
}
public Task OnGuildwarsStarted(Process process)
{
return Task.CompletedTask;
}
public Task OnGuildwarsStarting(Process process)
{
var guildwarsDirectory = new FileInfo(process.StartInfo.FileName).Directory!.FullName;
if (this.options.Value.Enabled)
{
EnsureFileExistsInGuildwarsDirectory(DsoundDll, guildwarsDirectory);
EnsureFileExistsInGuildwarsDirectory(DSOALAldrvDll, guildwarsDirectory);
EnsureFileExistsInGuildwarsDirectory(AlsoftIni, guildwarsDirectory);
}
else
{
EnsureFileDoesNotExistInGuildwarsDirectory(DsoundDll, guildwarsDirectory);
EnsureFileDoesNotExistInGuildwarsDirectory(DSOALAldrvDll, guildwarsDirectory);
EnsureFileDoesNotExistInGuildwarsDirectory(AlsoftIni, guildwarsDirectory);
}
return Task.CompletedTask;
}
private static void EnsureFileExistsInGuildwarsDirectory(string fileName, string destinationDirectoryName)
{
var sourcePath = Path.Combine(DSOALDirectory, fileName);
var destinationPath = Path.Combine(destinationDirectoryName, fileName);
if (File.Exists(destinationPath))
{
return;
}
File.Copy(sourcePath, destinationPath, true);
}
private static void EnsureFileDoesNotExistInGuildwarsDirectory(string fileName, string destinationDirectoryName)
{
var destinationPath = Path.Combine(destinationDirectoryName, fileName);
if (!File.Exists(destinationPath))
{
return;
}
File.Delete(destinationPath);
}
private void ExtractFiles()
{
ZipFile.ExtractToDirectory(ArchiveName, Path.Combine(Directory.GetCurrentDirectory(), DSOALDirectory), true);
ZipFile.ExtractToDirectory(Path.Combine(DSOALDirectory, HRTFArchiveName), Path.Combine(Directory.GetCurrentDirectory(), DSOALDirectory), true);
var options = this.options.Value;
options.Path = Path.GetFullPath(DSOALDirectory);
this.options.UpdateOption();
File.Delete(ArchiveName);
File.Delete(Path.Combine(DSOALDirectory, HRTFArchiveName));
}
private void SetupHrtfAndPresetFiles()
{
var appDataPath = Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData);
var openalPath = Path.Combine(appDataPath, OpenAlDirectory);
Directory.CreateSymbolicLink(openalPath, Path.GetFullPath(DSOALDirectory));
}
}
+10
View File
@@ -0,0 +1,10 @@
using Daybreak.Models.Progress;
using Daybreak.Services.Mods;
using System.Threading.Tasks;
namespace Daybreak.Services.DSOAL;
public interface IDSOALService : IModService
{
Task<bool> SetupDSOAL(DSOALInstallationStatus dSOALInstallationStatus);
}
@@ -15,6 +15,7 @@ namespace Daybreak.Services.IconRetrieve;
public sealed class IconCache : IIconCache
{
private const string HighResolutionGalleryUrl = $"https://wiki.guildwars.com/wiki/File:{NamePlaceholder}_(large).jpg";
private const string WikiUrl = "https://wiki.guildwars.com";
private const string NamePlaceholder = "[NAME]";
private const string IconsDirectoryName = "Icons";
@@ -55,6 +56,13 @@ public sealed class IconCache : IIconCache
return default;
}
var highResWikiUri = $"{WikiUrl}/wiki/File:{curedSkillName}_(large).jpg";
var highResResult = await this.GetIconUriInternal(curedSkillName!, fileName!, highResWikiUri, false);
if (highResResult is string)
{
return highResResult;
}
var wikiUri = $"{WikiUrl}/wiki/{curedSkillName}";
return await this.GetIconUriInternal(curedSkillName!, fileName!, wikiUri, false);
}
+1 -2
View File
@@ -1,5 +1,4 @@
using System;
using System.Threading.Tasks;
using System.Threading.Tasks;
using System.Windows.Media;
namespace Daybreak.Services.Images;
+14
View File
@@ -0,0 +1,14 @@
using System.Collections.Generic;
using System.Diagnostics;
using System.Threading.Tasks;
namespace Daybreak.Services.Mods;
public interface IModService
{
bool IsEnabled { get; set; }
bool IsInstalled { get; }
IEnumerable<string> GetCustomArguments();
Task OnGuildwarsStarting(Process process);
Task OnGuildwarsStarted(Process process);
}
+12
View File
@@ -0,0 +1,12 @@
using System.Collections.Generic;
namespace Daybreak.Services.Mods;
public interface IModsManager
{
void RegisterMod<TInterface, TImplementation>()
where TInterface : class, IModService
where TImplementation : TInterface;
public IEnumerable<IModService> GetMods();
}
+33
View File
@@ -0,0 +1,33 @@
using Microsoft.Extensions.Logging;
using Slim;
using System.Collections.Generic;
using System.Core.Extensions;
namespace Daybreak.Services.Mods;
public sealed class ModsManager : IModsManager
{
private readonly IServiceManager serviceManager;
private readonly ILogger<IModsManager> logger;
public ModsManager(
IServiceManager serviceManager,
ILogger<ModsManager> logger)
{
this.serviceManager = serviceManager.ThrowIfNull();
this.logger = logger.ThrowIfNull();
}
public IEnumerable<IModService> GetMods()
{
return this.serviceManager.GetServicesOfType<IModService>();
}
public void RegisterMod<TInterface, TImplementation>()
where TInterface : class, IModService
where TImplementation : TInterface
{
this.serviceManager.RegisterScoped<TInterface, TImplementation>();
this.logger.LogInformation($"Registered mod [{typeof(TImplementation).Name}]");
}
}
+6 -1
View File
@@ -68,7 +68,12 @@ public sealed class ViewManager : IViewManager
throw new InvalidOperationException("Cannot show a view without a registered container");
}
var view = scopedManager.GetService(viewType).As<UserControl>();
var view = scopedManager.GetService(viewType)?.As<UserControl>();
if (view is null)
{
throw new InvalidOperationException($"Unexpected error occured when attempting to show view {viewType.Name}");
}
this.container.Children.Clear();
this.container.Children.Add(view);
view.DataContext = dataContext;
@@ -73,6 +73,13 @@ public sealed class StupidPathfinder : IPathfinder
return new PathfindingFailure.PathfindingDisabled();
}
if (map is null ||
map.Trapezoids is null)
{
scopedLogger.LogError("Null pathfinding map");
return new PathfindingFailure.UnexpectedFailure();
}
if (GetContainingTrapezoid(map, startPoint) is not Trapezoid startTrapezoid)
{
scopedLogger.LogInformation("Start point not in map. Getting closest start point in map");
@@ -163,7 +170,8 @@ public sealed class StupidPathfinder : IPathfinder
private static Trapezoid? GetContainingTrapezoid(PathingData map, Point point)
{
if (map.Trapezoids is null)
if (map is null ||
map.Trapezoids is null)
{
return default;
}
@@ -0,0 +1,78 @@
using Daybreak.Configuration.Options;
using Daybreak.Models.Guildwars;
using Daybreak.Services.Scanner.Models;
using System;
using System.Configuration;
using System.Core.Extensions;
using System.Threading;
using System.Threading.Tasks;
namespace Daybreak.Services.Scanner;
public sealed class GuildwarsMemoryCache : IGuildwarsMemoryCache
{
private readonly IGuildwarsMemoryReader guildwarsMemoryReader;
private readonly ILiveOptions<MemoryReaderOptions> liveOptions;
private readonly CachedData<GameData?> gameDataCache = new();
private readonly CachedData<InventoryData?> inventoryDataCache = new();
private readonly CachedData<LoginData?> loginDataCache = new();
private readonly CachedData<PathingData?> pathingDataCache = new();
private readonly CachedData<PathingMetadata?> pathingMetadataCache = new();
private readonly CachedData<WorldData?> worldDataCache = new();
public GuildwarsMemoryCache(
IGuildwarsMemoryReader guildwarsMemoryReader,
ILiveOptions<MemoryReaderOptions> liveOptions)
{
this.guildwarsMemoryReader = guildwarsMemoryReader.ThrowIfNull();
this.liveOptions = liveOptions.ThrowIfNull();
}
public Task<GameData?> ReadGameData(CancellationToken cancellationToken)
{
return this.ReadDataInternal(this.gameDataCache, this.guildwarsMemoryReader.ReadGameData, cancellationToken);
}
public Task<InventoryData?> ReadInventoryData(CancellationToken cancellationToken)
{
return this.ReadDataInternal(this.inventoryDataCache, this.guildwarsMemoryReader.ReadInventoryData, cancellationToken);
}
public Task<LoginData?> ReadLoginData(CancellationToken cancellationToken)
{
return this.ReadDataInternal(this.loginDataCache, this.guildwarsMemoryReader.ReadLoginData, cancellationToken);
}
public Task<PathingData?> ReadPathingData(CancellationToken cancellationToken)
{
return this.ReadDataInternal(this.pathingDataCache, this.guildwarsMemoryReader.ReadPathingData, cancellationToken);
}
public Task<PathingMetadata?> ReadPathingMetaData(CancellationToken cancellationToken)
{
return this.ReadDataInternal(this.pathingMetadataCache, this.guildwarsMemoryReader.ReadPathingMetaData, cancellationToken);
}
public Task<WorldData?> ReadWorldData(CancellationToken cancellationToken)
{
return this.ReadDataInternal(this.worldDataCache, this.guildwarsMemoryReader.ReadWorldData, cancellationToken);
}
private async Task<T?> ReadDataInternal<T>(CachedData<T?> cachedData, Func<CancellationToken, Task<T?>> task, CancellationToken cancellationToken)
{
if (DateTime.Now - cachedData.SetTime <= TimeSpan.FromMilliseconds(this.liveOptions.Value.MemoryReaderFrequency))
{
return cachedData.Data;
}
await this.guildwarsMemoryReader.EnsureInitialized(cancellationToken);
var data = await task(cancellationToken);
if (data is null)
{
return data;
}
cachedData.SetData(data);
return cachedData.Data;
}
}
@@ -40,6 +40,7 @@ public sealed class GuildwarsMemoryReader : IGuildwarsMemoryReader
private uint entityArrayPointer;
private uint titleDataPointer;
private uint targetIdPointer;
private uint instanceInfoPointer;
public GuildwarsMemoryReader(
IApplicationLauncher applicationLauncher,
@@ -135,6 +136,16 @@ public sealed class GuildwarsMemoryReader : IGuildwarsMemoryReader
return Task.Run(() => this.SafeReadGameMemory(this.ReadInventoryDataInternal), cancellationToken);
}
public Task<WorldData?> ReadWorldData(CancellationToken cancellationToken)
{
if (this.memoryScanner.Scanning is false)
{
return Task.FromResult<WorldData?>(default);
}
return Task.Run(() => this.SafeReadGameMemory(this.ReadWorldDataInternal), cancellationToken);
}
private async Task InitializeSafe(Process process, ScopedLogger<GuildwarsMemoryReader> scopedLogger)
{
if (this.memoryScanner.Process is null ||
@@ -411,6 +422,35 @@ public sealed class GuildwarsMemoryReader : IGuildwarsMemoryReader
return inventoryData;
}
private WorldData? ReadWorldDataInternal()
{
var globalContext = this.memoryScanner.ReadPtrChain<GlobalContext>(this.memoryScanner.ModuleStartAddress, finalPointerOffset: 0x0, 0x00629204, 0x18);
if (!globalContext.GameContext.IsValid() ||
!globalContext.UserContext.IsValid() ||
!globalContext.InstanceContext.IsValid())
{
return default;
}
var userContext = this.memoryScanner.Read(globalContext.UserContext, UserContext.BaseOffset);
var instanceInfo = this.memoryScanner.ReadPtrChain<InstanceInfoContext>(this.GetInstanceInfoPointer(), 0x0, 0x0);
var areaInfo = this.memoryScanner.Read(instanceInfo.AreaInfo);
_ = Map.TryParse((int)userContext.MapId, out var map);
_ = Region.TryParse((int)areaInfo.RegionId, out var region);
_ = Continent.TryParse((int)areaInfo.ContinentId, out var continent);
_ = Campaign.TryParse((int)areaInfo.CampaignId, out var campaign);
return new WorldData
{
Campaign = campaign,
Continent = continent,
Region = region,
Map = map
};
}
private uint GetPlayerIdPointer()
{
if (this.playerIdPointer == 0)
@@ -451,6 +491,16 @@ public sealed class GuildwarsMemoryReader : IGuildwarsMemoryReader
return this.targetIdPointer;
}
private uint GetInstanceInfoPointer()
{
if (this.instanceInfoPointer == 0)
{
this.instanceInfoPointer = this.memoryScanner.ScanForPtr(new byte[] { 0x6A, 0x2C, 0x50, 0xE8, 0x00, 0x00, 0x00, 0x00, 0x83, 0xC4, 0x08, 0xC7 }, "xxxx????xxxx") + 0xD;
}
return this.instanceInfoPointer;
}
private Bag? GetBag(GuildwarsPointer<BagInfo> bagInfoPtr, uint expectedBagType, bool returnEmptyBag)
{
var bagInfo = this.memoryScanner.Read(bagInfoPtr);
@@ -0,0 +1,15 @@
using Daybreak.Models.Guildwars;
using System.Threading;
using System.Threading.Tasks;
namespace Daybreak.Services.Scanner;
public interface IGuildwarsMemoryCache
{
Task<LoginData?> ReadLoginData(CancellationToken cancellationToken);
Task<GameData?> ReadGameData(CancellationToken cancellationToken);
Task<PathingData?> ReadPathingData(CancellationToken cancellationToken);
Task<PathingMetadata?> ReadPathingMetaData(CancellationToken cancellationToken);
Task<InventoryData?> ReadInventoryData(CancellationToken cancellationToken);
Task<WorldData?> ReadWorldData(CancellationToken cancellationToken);
}
@@ -1,5 +1,4 @@
using Daybreak.Models;
using Daybreak.Models.Guildwars;
using Daybreak.Models.Guildwars;
using System.Threading;
using System.Threading.Tasks;
@@ -13,5 +12,6 @@ public interface IGuildwarsMemoryReader
Task<PathingData?> ReadPathingData(CancellationToken cancellationToken);
Task<PathingMetadata?> ReadPathingMetaData(CancellationToken cancellationToken);
Task<InventoryData?> ReadInventoryData(CancellationToken cancellationToken);
Task<WorldData?> ReadWorldData(CancellationToken cancellationToken);
void Stop();
}
@@ -0,0 +1,15 @@
using System;
namespace Daybreak.Services.Scanner.Models;
public sealed class CachedData<T>
{
public T? Data { get; private set; }
public DateTime SetTime { get; private set; } = DateTime.MinValue;
public void SetData(T data)
{
this.Data = data;
this.SetTime = DateTime.Now;
}
}
+2 -5
View File
@@ -1,13 +1,10 @@
using Daybreak.Models.Progress;
using Daybreak.Services.Mods;
using System.Threading.Tasks;
namespace Daybreak.Services.Toolbox;
public interface IToolboxService
public interface IToolboxService : IModService
{
bool ToolboxExists { get; }
bool Enabled { get; set; }
bool LoadToolboxFromDisk();
Task<bool> SetupToolbox(ToolboxInstallationStatus toolboxInstallationStatus);
+91 -3
View File
@@ -1,17 +1,25 @@
using Daybreak.Configuration.Options;
using Daybreak.Exceptions;
using Daybreak.Models.Progress;
using Daybreak.Services.Downloads;
using Daybreak.Utils;
using Microsoft.Extensions.Logging;
using Microsoft.Win32;
using System;
using System.Collections.Generic;
using System.Configuration;
using System.Core.Extensions;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace Daybreak.Services.Toolbox;
public sealed class ToolboxService : IToolboxService
{
private const int MaxRetries = 10;
private const string ToolboxLatestUri = "https://github.com/HasKha/GWToolboxpp/releases/download/6.0_Release/gwtoolbox.exe";
private const string ExecutableName = "GWToolboxpp.exe";
private const string ToolboxDestinationDirectory = "GWToolbox";
@@ -21,9 +29,7 @@ public sealed class ToolboxService : IToolboxService
private readonly ILiveUpdateableOptions<ToolboxOptions> toolboxOptions;
private readonly ILogger<ToolboxService> logger;
public bool ToolboxExists => File.Exists(this.toolboxOptions.Value.Path);
public bool Enabled
public bool IsEnabled
{
get => this.toolboxOptions.Value.Enabled;
set
@@ -32,6 +38,7 @@ public sealed class ToolboxService : IToolboxService
this.toolboxOptions.UpdateOption();
}
}
public bool IsInstalled => File.Exists(this.toolboxOptions.Value.Path);
public ToolboxService(
IDownloadService downloadService,
@@ -45,6 +52,21 @@ public sealed class ToolboxService : IToolboxService
this.logger = logger.ThrowIfNull();
}
public Task OnGuildwarsStarting(Process process)
{
return Task.CompletedTask;
}
public async Task OnGuildwarsStarted(Process process)
{
await this.LaunchToolbox();
}
public IEnumerable<string> GetCustomArguments()
{
return Enumerable.Empty<string>();
}
public bool LoadToolboxFromDisk()
{
var filePicker = new OpenFileDialog
@@ -96,4 +118,70 @@ public sealed class ToolboxService : IToolboxService
this.toolboxOptions.UpdateOption();
return true;
}
private async Task LaunchToolbox()
{
if (this.toolboxOptions.Value.Enabled is false)
{
return;
}
var executable = this.toolboxOptions.Value.Path;
if (File.Exists(executable) is false)
{
throw new ExecutableNotFoundException($"GWToolbox executable doesn't exist at {executable}");
}
if (Process.GetProcessesByName("GWToolboxpp").FirstOrDefault() is Process)
{
this.logger.LogInformation("GWToolboxpp is already running");
return;
}
await Task.Delay(5000);
this.logger.LogInformation($"Launching GWToolbox");
var process = new Process()
{
StartInfo = new ProcessStartInfo
{
FileName = executable
}
};
if (process.Start() is false)
{
throw new InvalidOperationException($"Unable to launch {executable}");
}
var retries = 0;
while (true)
{
await Task.Delay(100);
retries++;
var toolboxProcess = Process.GetProcessesByName("GWToolboxpp").FirstOrDefault();
if (toolboxProcess is null && retries < MaxRetries)
{
continue;
}
else if (toolboxProcess is null && retries >= MaxRetries)
{
throw new InvalidOperationException("Newly launched GWToolbox process not detected");
}
if (toolboxProcess!.MainWindowHandle == IntPtr.Zero)
{
continue;
}
var titleLength = NativeMethods.GetWindowTextLength(toolboxProcess.MainWindowHandle);
var titleBuffer = new StringBuilder(titleLength);
_ = NativeMethods.GetWindowText(toolboxProcess.MainWindowHandle, titleBuffer, titleLength + 1);
var title = titleBuffer.ToString();
if (title != "GWToolbox - Launch")
{
continue;
}
return;
}
}
}
+2 -5
View File
@@ -1,13 +1,10 @@
using Daybreak.Models.Progress;
using Daybreak.Services.Mods;
using System.Threading.Tasks;
namespace Daybreak.Services.UMod;
public interface IUModService
public interface IUModService : IModService
{
bool UModExists { get; }
bool Enabled { get; set; }
bool LoadUModFromDisk();
Task<bool> SetupUMod(UModInstallationStatus uModInstallationStatus);
+125 -31
View File
@@ -1,11 +1,17 @@
using Daybreak.Configuration.Options;
using Daybreak.Exceptions;
using Daybreak.Models;
using Daybreak.Models.Progress;
using Daybreak.Services.Downloads;
using Daybreak.Utils;
using Microsoft.Extensions.Logging;
using Microsoft.Win32;
using System;
using System.Collections.Generic;
using System.Configuration;
using System.Core.Extensions;
using System.Diagnostics;
using System.Extensions;
using System.IO;
using System.IO.Compression;
using System.Linq;
@@ -16,6 +22,7 @@ namespace Daybreak.Services.UMod;
public sealed class UModService : IUModService
{
private const int MaxRetries = 10;
private const string DownloadUrl = "https://storage.googleapis.com/google-code-archive-downloads/v2/code.google.com/texmod/uMod_v1_r44.zip";
private const string ArchiveName = "uMod_v1_r44.zip";
private const string UModDirectory = "uMod";
@@ -30,9 +37,7 @@ public sealed class UModService : IUModService
private readonly ILiveUpdateableOptions<UModOptions> uModOptions;
private readonly ILogger<UModService> logger;
public bool UModExists => File.Exists(this.uModOptions.Value.Path);
public bool Enabled
public bool IsEnabled
{
get => this.uModOptions.Value.Enabled;
set
@@ -42,6 +47,8 @@ public sealed class UModService : IUModService
}
}
public bool IsInstalled => File.Exists(this.uModOptions.Value.Path);
public UModService(
IDownloadService downloadService,
ILiveOptions<LauncherOptions> launcherOptions,
@@ -54,6 +61,21 @@ public sealed class UModService : IUModService
this.logger = logger.ThrowIfNull();
}
public IEnumerable<string> GetCustomArguments()
{
return Enumerable.Empty<string>();
}
public async Task OnGuildwarsStarting(Process process)
{
await this.LaunchUmod(process);
}
public Task OnGuildwarsStarted(Process process)
{
return Task.CompletedTask;
}
public bool LoadUModFromDisk()
{
var filePicker = new OpenFileDialog
@@ -82,32 +104,6 @@ public sealed class UModService : IUModService
return false;
}
uModInstallationStatus.CurrentStep = UModInstallationStatus.Installing;
var maybeGuildwarsPath = this.launcherOptions.Value.GuildwarsPaths.Where(path => path.Default).FirstOrDefault();
if (maybeGuildwarsPath is not GuildwarsPath guildwarsPath)
{
this.logger.LogError("No selected Guild Wars executable was found");
return false;
}
var guildWarsDirectory = Path.GetDirectoryName(guildwarsPath.Path);
var d3d9SourceFilePath = Path.Combine(UModDirectory, D3D9Dll);
var d3d9DestinationFilePath = Path.Combine(guildWarsDirectory, D3D9Dll);
if (MustBackupD3D9Dll(d3d9SourceFilePath, d3d9DestinationFilePath))
{
this.logger.LogInformation($"Found an existing {D3D9Dll} file. Saving it as {D3D9DllBackup}");
var d3d9DestinationBackupFilePath = Path.Combine(guildWarsDirectory, D3D9DllBackup);
if (File.Exists(d3d9DestinationBackupFilePath))
{
File.Delete(d3d9DestinationBackupFilePath);
}
File.Move(d3d9DestinationFilePath, d3d9DestinationBackupFilePath);
}
this.logger.LogInformation($"Copying {d3d9SourceFilePath} to {d3d9DestinationFilePath}");
File.Copy(d3d9SourceFilePath, d3d9DestinationFilePath);
uModInstallationStatus.CurrentStep = UModInstallationStatus.Finished;
return true;
}
@@ -175,6 +171,105 @@ public sealed class UModService : IUModService
await File.WriteAllLinesAsync(defaultTemplateFile, new string[] { finalSb.ToString() } );
}
private async Task LaunchUmod(Process gwProcess)
{
if (this.uModOptions.Value.Enabled is false)
{
throw new InvalidOperationException("Cannot launch uMod. uMod is disabled");
}
var executable = this.uModOptions.Value.Path;
if (File.Exists(executable) is false)
{
throw new ExecutableNotFoundException($"uMod executable doesn't exist at {executable}");
}
if (Process.GetProcessesByName("uMod").FirstOrDefault() is not null)
{
this.logger.LogInformation("uMod is already running");
return;
}
this.logger.LogInformation("Setting up uMod d3d9 dll");
this.SetupD3D9Dll(gwProcess);
this.logger.LogInformation($"Launching uMod");
var process = new Process()
{
StartInfo = new ProcessStartInfo
{
FileName = executable,
WorkingDirectory = Path.GetDirectoryName(executable)
}
};
if (process.Start() is false)
{
throw new InvalidOperationException($"Unable to launch {executable}");
}
var retries = 0;
while (true)
{
await Task.Delay(100);
retries++;
var uModProcess = Process.GetProcessesByName("uMod").FirstOrDefault();
if (uModProcess is null && retries < MaxRetries)
{
continue;
}
else if (uModProcess is null && retries >= MaxRetries)
{
throw new InvalidOperationException("Newly launched uMod process not detected");
}
if (uModProcess!.MainWindowHandle == IntPtr.Zero)
{
continue;
}
var titleLength = NativeMethods.GetWindowTextLength(uModProcess.MainWindowHandle);
var titleBuffer = new StringBuilder(titleLength);
_ = NativeMethods.GetWindowText(uModProcess.MainWindowHandle, titleBuffer, titleLength + 1);
var title = titleBuffer.ToString();
if (title != "uMod V 1.0")
{
continue;
}
return;
}
}
private void SetupD3D9Dll(Process gwProcess)
{
if (gwProcess.StartInfo.FileName?.IsNullOrWhiteSpace() is true)
{
throw new InvalidOperationException("Unable to start uMod. Invalid Guild Wars process");
}
var guildWarsDirectory = Path.GetDirectoryName(gwProcess.StartInfo.FileName);
var d3d9SourceFilePath = Path.Combine(UModDirectory, D3D9Dll);
var d3d9DestinationFilePath = Path.Combine(guildWarsDirectory!, D3D9Dll);
if (MustBackupD3D9Dll(d3d9SourceFilePath, d3d9DestinationFilePath))
{
this.logger.LogInformation($"Found an existing {D3D9Dll} file. Saving it as {D3D9DllBackup}");
var d3d9DestinationBackupFilePath = Path.Combine(guildWarsDirectory!, D3D9DllBackup);
if (File.Exists(d3d9DestinationBackupFilePath))
{
File.Delete(d3d9DestinationBackupFilePath);
}
File.Move(d3d9DestinationFilePath, d3d9DestinationBackupFilePath);
File.Delete(d3d9DestinationFilePath);
}
if (!File.Exists(d3d9DestinationFilePath))
{
this.logger.LogInformation($"Copying {d3d9SourceFilePath} to {d3d9DestinationFilePath}");
File.Copy(d3d9SourceFilePath, d3d9DestinationFilePath);
}
}
private static async Task SetupModListFile()
{
var modListPath = Path.Combine(UModDirectory, UModModListFile);
@@ -190,7 +285,6 @@ public sealed class UModService : IUModService
var sourceInfo = new FileInfo(sourcePath);
var destinationInfo = new FileInfo(destinationPath);
return sourceInfo.Length != destinationInfo.Length ||
sourceInfo.CreationTimeUtc != destinationInfo.CreationTimeUtc;
return sourceInfo.Length != destinationInfo.Length;
}
}
+10 -6
View File
@@ -17,8 +17,10 @@
x:Name="_this"
d:DesignHeight="450" d:DesignWidth="800">
<UserControl.Resources>
<BooleanToVisibilityConverter x:Key="BooleanToVisibilityConverter"></BooleanToVisibilityConverter>
<converters:BooleanToVisibilityConverter x:Key="InverseBooleanToVisibilityConverter" TriggerValue="True" ></converters:BooleanToVisibilityConverter>
<BooleanToVisibilityConverter x:Key="BooleanToVisibilityConverter" />
<converters:BooleanToVisibilityConverter x:Key="InverseBooleanToVisibilityConverter" TriggerValue="True" />
<converters:BooleanToGridLengthConverter x:Key="InventoryComponentVisibilityConverter" VisibleValue="1*" />
<converters:BooleanToGridLengthConverter x:Key="MinimapComponentVisibilityConverter" VisibleValue="1*" />
</UserControl.Resources>
<Grid>
<Grid.ColumnDefinitions>
@@ -69,8 +71,8 @@
<Grid Grid.Column="1"
Margin="5, 0, 5, 0">
<Grid.RowDefinitions>
<RowDefinition Height="auto"/>
<RowDefinition Height="1*"/>
<RowDefinition Height="{Binding ElementName=_this, Path=InventoryVisible, Mode=OneWay, Converter={StaticResource InventoryComponentVisibilityConverter}}" />
<RowDefinition Height="{Binding ElementName=_this, Path=MinimapVisible, Mode=OneWay, Converter={StaticResource MinimapComponentVisibilityConverter}}" />
<RowDefinition Height="1.5*"/>
</Grid.RowDefinitions>
<Grid x:Name="InventoryHolder"
@@ -83,7 +85,8 @@
MaximizeClicked="InventoryComponent_MaximizeClicked"
Visibility="{Binding ElementName=_this, Path=MainPlayerDataValid, Mode=OneWay, Converter={StaticResource BooleanToVisibilityConverter}}"
ItemWikiClicked="InventoryComponent_ItemWikiClicked"
PriceHistoryClicked="InventoryComponent_PriceHistoryClicked"/>
PriceHistoryClicked="InventoryComponent_PriceHistoryClicked"
IsEnabled="{Binding ElementName=_this, Path=InventoryVisible, Mode=OneWay}"/>
</Grid>
</Grid>
<Grid x:Name="MinimapHolder"
@@ -106,7 +109,8 @@
ClipToBounds="True"
Background="{DynamicResource Daybreak.Brushes.Background}"
BorderBrush="{DynamicResource MahApps.Brushes.ThemeForeground}"
BorderThickness="1" />
BorderThickness="1"
IsEnabled="{Binding ElementName=_this, Path=MinimapVisible, Mode=OneWay}"/>
<controls:CircularLoadingWidget Height="100"
Width="100"
Visibility="{Binding ElementName=_this,Path=LoadingPathingData,Mode=OneWay,Converter={StaticResource BooleanToVisibilityConverter}}" />
+49 -43
View File
@@ -27,7 +27,7 @@ public partial class FocusView : UserControl
{
private readonly IBuildTemplateManager buildTemplateManager;
private readonly IApplicationLauncher applicationLauncher;
private readonly IGuildwarsMemoryReader guildwarsMemoryReader;
private readonly IGuildwarsMemoryCache guildwarsMemoryCache;
private readonly IExperienceCalculator experienceCalculator;
private readonly IViewManager viewManager;
private readonly ILiveUpdateableOptions<FocusViewOptions> liveUpdateableOptions;
@@ -57,16 +57,18 @@ public partial class FocusView : UserControl
[GenerateDependencyProperty]
private bool inventoryVisible;
[GenerateDependencyProperty]
private bool minimapVisible;
private bool browserMaximized = false;
private bool minimapMaximized = false;
private bool inventoryMaximized = false;
private CancellationTokenSource? cancellationTokenSource;
private CancellationTokenSource? loadingPathingDataCancellationTokenSource;
public FocusView(
IBuildTemplateManager buildTemplateManager,
IApplicationLauncher applicationLauncher,
IGuildwarsMemoryReader guildwarsMemoryReader,
IGuildwarsMemoryCache guildwarsMemoryCache,
IExperienceCalculator experienceCalculator,
IViewManager viewManager,
ILiveUpdateableOptions<FocusViewOptions> liveUpdateableOptions,
@@ -74,7 +76,7 @@ public partial class FocusView : UserControl
{
this.buildTemplateManager = buildTemplateManager.ThrowIfNull();
this.applicationLauncher = applicationLauncher.ThrowIfNull();
this.guildwarsMemoryReader = guildwarsMemoryReader.ThrowIfNull();
this.guildwarsMemoryCache = guildwarsMemoryCache.ThrowIfNull();
this.experienceCalculator = experienceCalculator.ThrowIfNull();
this.viewManager = viewManager.ThrowIfNull();
this.liveUpdateableOptions = liveUpdateableOptions.ThrowIfNull();
@@ -97,12 +99,7 @@ public partial class FocusView : UserControl
base.OnPropertyChanged(e);
}
private TimeSpan GetMemoryReaderLatency()
{
return TimeSpan.FromMilliseconds(this.liveUpdateableOptions.Value.MemoryReaderFrequency);
}
private async Task UpdatePathingData(CancellationToken cancellationToken)
private async Task UpdatePathingData()
{
if (this.applicationLauncher.IsGuildwarsRunning is false)
{
@@ -112,24 +109,23 @@ public partial class FocusView : UserControl
return;
}
this.Dispatcher.Invoke(() =>
var pathingMeta = await this.guildwarsMemoryCache.ReadPathingMetaData(this.cancellationTokenSource?.Token ?? CancellationToken.None);
if (pathingMeta?.TrapezoidCount == this.PathingData?.Trapezoids?.Count ||
this.cancellationTokenSource?.IsCancellationRequested is not false)
{
this.LoadingPathingData = true;
});
return;
}
await this.guildwarsMemoryReader.EnsureInitialized(cancellationToken);
var maybePathingData = await this.guildwarsMemoryReader.ReadPathingData(cancellationToken);
await this.Dispatcher.InvokeAsync(() => this.LoadingPathingData = true);
var maybePathingData = await this.guildwarsMemoryCache.ReadPathingData(this.cancellationTokenSource?.Token ?? CancellationToken.None);
if (maybePathingData is not PathingData pathingData ||
pathingData.Trapezoids is null ||
pathingData.Trapezoids.Count == 0)
{
await Task.Delay(1000, cancellationToken);
await this.UpdatePathingData(cancellationToken);
return;
}
this.Dispatcher.Invoke(() =>
await this.Dispatcher.InvokeAsync(() =>
{
this.PathingData = pathingData;
this.LoadingPathingData = false;
@@ -146,9 +142,7 @@ public partial class FocusView : UserControl
return;
}
await this.guildwarsMemoryReader.EnsureInitialized(this.cancellationTokenSource?.Token ?? CancellationToken.None).ConfigureAwait(true);
var maybeGameData = await this.guildwarsMemoryReader.ReadGameData(this.cancellationTokenSource?.Token ?? CancellationToken.None).ConfigureAwait(true);
var maybeGameData = await this.guildwarsMemoryCache.ReadGameData(this.cancellationTokenSource?.Token ?? CancellationToken.None).ConfigureAwait(true);
if (maybeGameData is not GameData gameData)
{
this.MainPlayerDataValid = false;
@@ -168,19 +162,6 @@ public partial class FocusView : UserControl
this.GameData = gameData;
this.MainPlayerDataValid = true;
var pathingMeta = await this.guildwarsMemoryReader.ReadPathingMetaData(this.cancellationTokenSource?.Token ?? CancellationToken.None);
if (pathingMeta?.TrapezoidCount != this.PathingData.Trapezoids?.Count &&
this.loadingPathingDataCancellationTokenSource is null)
{
this.loadingPathingDataCancellationTokenSource = new CancellationTokenSource();
_ = Task.Run(() => this.UpdatePathingData(this.loadingPathingDataCancellationTokenSource.Token), this.loadingPathingDataCancellationTokenSource.Token)
.ContinueWith(_ =>
{
this.loadingPathingDataCancellationTokenSource?.Dispose();
this.loadingPathingDataCancellationTokenSource = null;
});
}
this.Browser.Visibility = this.MainPlayerDataValid is true ?
this.minimapMaximized ?
Visibility.Hidden :
@@ -188,9 +169,31 @@ public partial class FocusView : UserControl
Visibility.Collapsed;
}
private async void PeriodicallyReadPathingData(CancellationToken cancellationToken)
{
while (!cancellationToken.IsCancellationRequested)
{
try
{
if (cancellationToken.IsCancellationRequested)
{
return;
}
await Task.WhenAll(
this.UpdatePathingData(),
Task.Delay(1000, cancellationToken)).ConfigureAwait(true);
}
catch (Exception ex)
{
this.logger.LogError(ex, "Encountered non-terminating exception. Silently continuing");
}
}
}
private async void PeriodicallyReadGameData(CancellationToken cancellationToken)
{
var memoryReaderLatency = this.GetMemoryReaderLatency();
while (!cancellationToken.IsCancellationRequested)
{
try
@@ -202,7 +205,7 @@ public partial class FocusView : UserControl
await Task.WhenAll(
this.UpdateGameData(),
Task.Delay(memoryReaderLatency, cancellationToken)).ConfigureAwait(true);
Task.Delay(16, cancellationToken)).ConfigureAwait(true);
}
catch (Exception ex)
@@ -223,18 +226,18 @@ public partial class FocusView : UserControl
return;
}
var readInventoryTask = this.guildwarsMemoryReader.ReadInventoryData(cancellationToken);
var readInventoryTask = this.guildwarsMemoryCache.ReadInventoryData(cancellationToken);
await Task.WhenAll(
readInventoryTask,
Task.Delay(1000, cancellationToken)).ConfigureAwait(true);
var maybeInventoryData = await readInventoryTask;
if (!maybeInventoryData.HasValue)
if (maybeInventoryData is null)
{
continue;
}
this.InventoryData = maybeInventoryData.Value;
this.InventoryData = maybeInventoryData;
}
catch (Exception ex)
{
@@ -247,6 +250,7 @@ public partial class FocusView : UserControl
{
this.BrowserAddress = this.liveUpdateableOptions.Value.BrowserUrl;
this.InventoryVisible = this.liveUpdateableOptions.Value.InventoryComponentVisible;
this.MinimapVisible = this.liveUpdateableOptions.Value.MinimapComponentVisible;
this.cancellationTokenSource?.Dispose();
this.cancellationTokenSource = new CancellationTokenSource();
var cancellationToken = this.cancellationTokenSource.Token;
@@ -255,15 +259,17 @@ public partial class FocusView : UserControl
{
this.PeriodicallyReadInventoryData(cancellationToken);
}
if (this.MinimapVisible)
{
this.PeriodicallyReadPathingData(cancellationToken);
}
}
private void FocusView_Unloaded(object _, RoutedEventArgs e)
{
this.cancellationTokenSource?.Cancel();
this.cancellationTokenSource = null;
this.loadingPathingDataCancellationTokenSource?.Cancel();
this.loadingPathingDataCancellationTokenSource = null;
this.guildwarsMemoryReader?.Stop();
}
private void Browser_MaximizeClicked(object _, EventArgs e)
@@ -0,0 +1,20 @@
<UserControl x:Class="Daybreak.Views.Onboarding.DSOAL.DSOALBrowserView"
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.Onboarding.DSOAL"
xmlns:controls="clr-namespace:Daybreak.Controls"
x:Name="_this"
mc:Ignorable="d"
d:DesignHeight="450" d:DesignWidth="800">
<Grid>
<controls:ChromiumBrowserWrapper AddressBarReadonly="True"
ControlsEnabled="False"
Foreground="{StaticResource MahApps.Brushes.ThemeForeground}"
Address="{Binding ElementName=_this, Path=DataContext, Mode=OneWay}"
HomeButtonVisible="False"
CanNavigate="False"
CanDownloadFiles="False" />
</Grid>
</UserControl>
@@ -0,0 +1,13 @@
using System.Windows.Controls;
namespace Daybreak.Views.Onboarding.DSOAL;
/// <summary>
/// Interaction logic for DSOALBrowserView.xaml
/// </summary>
public partial class DSOALBrowserView : UserControl
{
public DSOALBrowserView()
{
this.InitializeComponent();
}
}
@@ -0,0 +1,17 @@
<UserControl x:Class="Daybreak.Views.Onboarding.DSOAL.DSOALHomepageView"
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.Onboarding.DSOAL"
xmlns:controls="clr-namespace:Daybreak.Controls"
mc:Ignorable="d"
d:DesignHeight="450" d:DesignWidth="800">
<Grid>
<controls:ChromiumBrowserWrapper AddressBarReadonly="True"
ControlsEnabled="False"
Address="https://www.gwtoolbox.com/"
CanNavigate="True"
Foreground="{StaticResource MahApps.Brushes.ThemeForeground}"></controls:ChromiumBrowserWrapper>
</Grid>
</UserControl>
@@ -0,0 +1,13 @@
using System.Windows.Controls;
namespace Daybreak.Views.Onboarding.DSOAL;
/// <summary>
/// Interaction logic for DSOALHomepageView.xaml
/// </summary>
public partial class DSOALHomepageView : UserControl
{
public DSOALHomepageView()
{
this.InitializeComponent();
}
}
@@ -0,0 +1,42 @@
<UserControl x:Class="Daybreak.Views.Onboarding.DSOAL.DSOALInstallingView"
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"
xmlns:controls="clr-namespace:Daybreak.Controls"
xmlns:buttons="clr-namespace:Daybreak.Controls.Buttons"
mc:Ignorable="d"
x:Name="_this"
Loaded="UserControl_Loaded"
d:DesignHeight="450" d:DesignWidth="800">
<UserControl.Resources>
<BooleanToVisibilityConverter x:Key="BooleanToVisibilityConverter"></BooleanToVisibilityConverter>
</UserControl.Resources>
<Grid VerticalAlignment="Center" HorizontalAlignment="Center" Background="{StaticResource Daybreak.Brushes.Background}" MinHeight="200" MinWidth="400">
<Border
BorderBrush="{StaticResource MahApps.Brushes.ThemeForeground}"
BorderThickness="1" />
<StackPanel VerticalAlignment="Center">
<TextBlock Text="{Binding ElementName=_this, Path=Description, Mode=OneWay}" Margin="10, 0, 10, 0" Foreground="{StaticResource MahApps.Brushes.ThemeForeground}"
TextWrapping="Wrap" HorizontalAlignment="Center" FontSize="16"></TextBlock>
<ProgressBar Minimum="0" Maximum="100" Value="{Binding ElementName=_this, Path=ProgressValue, Mode=OneWay}"
Width="300" Height="20" Visibility="{Binding ElementName=_this,Path=ProgressVisible, Mode=OneWay, Converter={StaticResource BooleanToVisibilityConverter}}"></ProgressBar>
<buttons:HighlightButton Title="Ok"
Grid.Column="1"
HorizontalAlignment="Center"
Height="30"
Width="60"
Margin="10"
HorizontalContentAlignment="Center"
VerticalContentAlignment="Center"
FontSize="16"
Foreground="{StaticResource MahApps.Brushes.ThemeForeground}"
Background="{StaticResource MahApps.Brushes.ThemeBackground}"
HighlightColor="{StaticResource MahApps.Brushes.Accent}"
BorderBrush="{StaticResource MahApps.Brushes.ThemeForeground}"
BorderThickness="1"
Clicked="OpaqueButton_Clicked"></buttons:HighlightButton>
</StackPanel>
</Grid>
</UserControl>
@@ -0,0 +1,70 @@
using Daybreak.Models.Progress;
using Daybreak.Services.DSOAL;
using Daybreak.Services.Navigation;
using Microsoft.Extensions.Logging;
using System.Core.Extensions;
using System.Extensions;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Extensions;
namespace Daybreak.Views.Onboarding.DSOAL;
/// <summary>
/// Interaction logic for UModInstallerView.xaml
/// </summary>
public partial class DSOALInstallingView : UserControl
{
private readonly ILogger<DSOALInstallingView> logger;
private readonly IViewManager viewManager;
private readonly IDSOALService dSOALService;
[GenerateDependencyProperty(InitialValue = "")]
private string description = string.Empty;
[GenerateDependencyProperty]
private double progressValue;
[GenerateDependencyProperty]
private bool continueButtonEnabled;
[GenerateDependencyProperty(InitialValue = false)]
private bool progressVisible;
public DSOALInstallingView(
IDSOALService dSOALService,
ILogger<DSOALInstallingView> logger,
IViewManager viewManager)
{
this.dSOALService = dSOALService.ThrowIfNull();
this.logger = logger.ThrowIfNull();
this.viewManager = viewManager.ThrowIfNull();
this.InitializeComponent();
}
private void DownloadStatus_PropertyChanged(object? sender, System.ComponentModel.PropertyChangedEventArgs e)
{
var installationStatus = sender?.As<DSOALInstallationStatus>();
this.Dispatcher.Invoke(() =>
{
this.ProgressVisible = false;
if (installationStatus!.CurrentStep is DownloadStatus.DownloadProgressStep downloadUpdateStep)
{
this.ProgressValue = downloadUpdateStep.Progress * 100;
this.ProgressVisible = true;
}
this.Description = installationStatus.CurrentStep.Description;
});
}
private async void UserControl_Loaded(object sender, RoutedEventArgs e)
{
var installationStatus = new DSOALInstallationStatus();
installationStatus.PropertyChanged += this.DownloadStatus_PropertyChanged;
await this.dSOALService.SetupDSOAL(installationStatus);
installationStatus.PropertyChanged -= this.DownloadStatus_PropertyChanged;
this.ContinueButtonEnabled = true;
}
private void OpaqueButton_Clicked(object sender, System.EventArgs e)
{
this.viewManager.ShowView<DSOALSwitchView>();
}
}
@@ -0,0 +1,17 @@
<UserControl x:Class="Daybreak.Views.Onboarding.DSOAL.DSOALOnboardingEntryView"
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.Onboarding.DSOAL"
mc:Ignorable="d"
Loaded="UserControl_Loaded"
d:DesignHeight="450" d:DesignWidth="800">
<Grid VerticalAlignment="Center" HorizontalAlignment="Center" Background="{StaticResource Daybreak.Brushes.Background}" MinHeight="200" MinWidth="400">
<Border
BorderBrush="{StaticResource MahApps.Brushes.ThemeForeground}"
BorderThickness="1"/>
<TextBlock Text="Checking DSOAL state" Margin="10, 0, 10, 0" Foreground="{StaticResource MahApps.Brushes.ThemeForeground}"
TextWrapping="Wrap" HorizontalAlignment="Center" VerticalAlignment="Center" FontSize="16"></TextBlock>
</Grid>
</UserControl>
@@ -0,0 +1,41 @@
using Daybreak.Services.DSOAL;
using Daybreak.Services.Navigation;
using Microsoft.Extensions.Logging;
using System.Core.Extensions;
using System.Windows;
using System.Windows.Controls;
namespace Daybreak.Views.Onboarding.DSOAL;
/// <summary>
/// Interaction logic for DSOALOnboardingEntryView.xaml
/// </summary>
public partial class DSOALOnboardingEntryView : UserControl
{
private readonly IDSOALService dsoalService;
private readonly IViewManager viewManager;
private readonly ILogger<DSOALOnboardingEntryView> logger;
public DSOALOnboardingEntryView(
IDSOALService dsoalService,
IViewManager viewManager,
ILogger<DSOALOnboardingEntryView> logger)
{
this.dsoalService = dsoalService.ThrowIfNull();
this.viewManager = viewManager.ThrowIfNull();
this.logger = logger.ThrowIfNull();
this.InitializeComponent();
}
private void UserControl_Loaded(object sender, RoutedEventArgs e)
{
if (this.dsoalService.IsInstalled)
{
this.viewManager.ShowView<DSOALSwitchView>();
}
else
{
this.viewManager.ShowView<DSOALInstallingView>();
}
}
}
@@ -0,0 +1,87 @@
<UserControl x:Class="Daybreak.Views.Onboarding.DSOAL.DSOALSwitchView"
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.Onboarding.DSOAL"
xmlns:controls="clr-namespace:Daybreak.Controls"
xmlns:buttons="clr-namespace:Daybreak.Controls.Buttons"
xmlns:converters="clr-namespace:Daybreak.Converters"
x:Name="_this"
mc:Ignorable="d"
d:DesignHeight="450" d:DesignWidth="800">
<UserControl.Resources>
<BooleanToVisibilityConverter x:Key="BooleanToVisibilityConverter"></BooleanToVisibilityConverter>
<converters:BooleanToVisibilityConverter x:Key="InverseBooleanToVisibilityConverter" TriggerValue="True"></converters:BooleanToVisibilityConverter>
</UserControl.Resources>
<Grid VerticalAlignment="Center" HorizontalAlignment="Center" Background="{StaticResource Daybreak.Brushes.Background}" MinHeight="200" MinWidth="400">
<Border
BorderBrush="{StaticResource MahApps.Brushes.ThemeForeground}"
BorderThickness="1"></Border>
<StackPanel VerticalAlignment="Center">
<WrapPanel Margin="10, 0, 10, 10" HorizontalAlignment="Center">
<TextBlock Text="Installation Guide: " Foreground="{StaticResource MahApps.Brushes.ThemeForeground}"
TextWrapping="Wrap" FontSize="16" HorizontalAlignment="Center"></TextBlock>
<TextBlock Text="Lemmy" Foreground="{StaticResource MahApps.Brushes.Accent}" Cursor="Hand"
TextWrapping="Wrap" FontSize="16" HorizontalAlignment="Center"
MouseLeftButtonDown="Lemmy_MouseLeftButtonDown"></TextBlock>
</WrapPanel>
<WrapPanel Margin="10, 0, 10, 0">
<TextBlock Text="DSOAL is currently " Foreground="{StaticResource MahApps.Brushes.ThemeForeground}"
TextWrapping="Wrap" FontSize="16" HorizontalAlignment="Center"
Visibility="{Binding ElementName=_this, Path=DsoalEnabled, Mode=OneWay, Converter={StaticResource BooleanToVisibilityConverter}}"></TextBlock>
<TextBlock Text="enabled" Foreground="{StaticResource MahApps.Brushes.Accent}"
TextWrapping="Wrap" FontSize="16" HorizontalAlignment="Center"
Visibility="{Binding ElementName=_this, Path=DsoalEnabled, Mode=OneWay, Converter={StaticResource BooleanToVisibilityConverter}}"></TextBlock>
<TextBlock Text=". Do you want to disable it?" Foreground="{StaticResource MahApps.Brushes.ThemeForeground}"
TextWrapping="Wrap" FontSize="16" HorizontalAlignment="Center"
Visibility="{Binding ElementName=_this, Path=DsoalEnabled, Mode=OneWay, Converter={StaticResource BooleanToVisibilityConverter}}"></TextBlock>
</WrapPanel>
<WrapPanel Margin="10, 0, 10, 0">
<TextBlock Text="DSOAL is currently " Foreground="{StaticResource MahApps.Brushes.ThemeForeground}"
TextWrapping="Wrap" FontSize="16" HorizontalAlignment="Center"
Visibility="{Binding ElementName=_this, Path=DsoalEnabled, Mode=OneWay, Converter={StaticResource InverseBooleanToVisibilityConverter}}"></TextBlock>
<TextBlock Text="disabled" Foreground="{StaticResource MahApps.Brushes.SystemControlErrorTextForeground}"
TextWrapping="Wrap" FontSize="16" HorizontalAlignment="Center"
Visibility="{Binding ElementName=_this, Path=DsoalEnabled, Mode=OneWay, Converter={StaticResource InverseBooleanToVisibilityConverter}}"></TextBlock>
<TextBlock Text=". Do you want to enable it?" Foreground="{StaticResource MahApps.Brushes.ThemeForeground}"
TextWrapping="Wrap" FontSize="16" HorizontalAlignment="Center"
Visibility="{Binding ElementName=_this, Path=DsoalEnabled, Mode=OneWay, Converter={StaticResource InverseBooleanToVisibilityConverter}}"></TextBlock>
</WrapPanel>
<Grid MaxWidth="600" Margin="10">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*"></ColumnDefinition>
<ColumnDefinition Width="*"></ColumnDefinition>
</Grid.ColumnDefinitions>
<buttons:HighlightButton Title="No"
Grid.Column="0"
HorizontalAlignment="Center"
Height="30"
Width="60"
HorizontalContentAlignment="Center"
VerticalContentAlignment="Center"
FontSize="16"
Foreground="{StaticResource MahApps.Brushes.ThemeForeground}"
Background="{StaticResource MahApps.Brushes.ThemeBackground}"
HighlightColor="{StaticResource MahApps.Brushes.Accent}"
BorderBrush="{StaticResource MahApps.Brushes.ThemeForeground}"
BorderThickness="1"
Clicked="OpaqueButtonNo_Clicked"></buttons:HighlightButton>
<buttons:HighlightButton Title="Yes"
Grid.Column="1"
HorizontalAlignment="Center"
Height="30"
Width="60"
HorizontalContentAlignment="Center"
VerticalContentAlignment="Center"
FontSize="16"
Foreground="{StaticResource MahApps.Brushes.ThemeForeground}"
Background="{StaticResource MahApps.Brushes.ThemeBackground}"
HighlightColor="{StaticResource MahApps.Brushes.Accent}"
BorderBrush="{StaticResource MahApps.Brushes.ThemeForeground}"
BorderThickness="1"
Clicked="OpaqueButtonYes_Clicked"></buttons:HighlightButton>
</Grid>
</StackPanel>
</Grid>
</UserControl>
@@ -0,0 +1,50 @@
using Daybreak.Services.DSOAL;
using Daybreak.Services.Navigation;
using Microsoft.Extensions.Logging;
using System.Core.Extensions;
using System.Windows.Controls;
using System.Windows.Extensions;
namespace Daybreak.Views.Onboarding.DSOAL;
/// <summary>
/// Interaction logic for DSOALSwitchView.xaml
/// </summary>
public partial class DSOALSwitchView : UserControl
{
private const string DSOALLemmyUrl = "https://lemmy.wtf/post/27911";
private readonly IDSOALService dSOALService;
private readonly IViewManager viewManager;
private readonly ILogger<DSOALSwitchView> logger;
[GenerateDependencyProperty]
private bool dsoalEnabled;
public DSOALSwitchView(
IDSOALService dSOALService,
IViewManager viewManager,
ILogger<DSOALSwitchView> logger)
{
this.dSOALService = dSOALService.ThrowIfNull();
this.viewManager = viewManager.ThrowIfNull();
this.logger = logger.ThrowIfNull();
this.InitializeComponent();
this.DsoalEnabled = this.dSOALService.IsEnabled;
}
private void OpaqueButtonNo_Clicked(object sender, System.EventArgs e)
{
this.viewManager.ShowView<LauncherView>();
}
private void OpaqueButtonYes_Clicked(object sender, System.EventArgs e)
{
this.dSOALService.IsEnabled = !this.dSOALService.IsEnabled;
this.viewManager.ShowView<LauncherView>();
}
private void Lemmy_MouseLeftButtonDown(object sender, System.Windows.Input.MouseButtonEventArgs e)
{
this.viewManager.ShowView<DSOALBrowserView>(DSOALLemmyUrl);
}
}
@@ -28,7 +28,7 @@ public partial class ToolboxOnboardingEntryView : UserControl
private void UserControl_Loaded(object sender, System.Windows.RoutedEventArgs e)
{
if (this.toolboxService.ToolboxExists)
if (this.toolboxService.IsInstalled)
{
this.viewManager.ShowView<ToolboxSwitchView>();
}
@@ -1,6 +1,5 @@
using Daybreak.Services.Navigation;
using Daybreak.Services.Toolbox;
using Daybreak.Views.Onboarding.UMod;
using Microsoft.Extensions.Logging;
using System.Core.Extensions;
using System.Windows.Controls;
@@ -29,7 +28,7 @@ public partial class ToolboxSwitchView : UserControl
this.logger = logger.ThrowIfNull();
this.InitializeComponent();
this.ToolboxEnabled = this.toolboxService.Enabled;
this.ToolboxEnabled = this.toolboxService.IsEnabled;
}
private void OpaqueButtonNo_Clicked(object sender, System.EventArgs e)
@@ -39,7 +38,7 @@ public partial class ToolboxSwitchView : UserControl
private void OpaqueButtonYes_Clicked(object sender, System.EventArgs e)
{
this.toolboxService.Enabled = !this.toolboxService.Enabled;
this.toolboxService.IsEnabled = !this.toolboxService.IsEnabled;
this.viewManager.ShowView<LauncherView>();
}
@@ -28,7 +28,7 @@ public partial class UModOnboardingEntryView : UserControl
private void UserControl_Loaded(object sender, System.Windows.RoutedEventArgs e)
{
if (this.uModService.UModExists)
if (this.uModService.IsInstalled)
{
this.viewManager.ShowView<UModSwitchView>();
}
@@ -31,7 +31,7 @@ public partial class UModSwitchView : UserControl
this.logger = logger.ThrowIfNull();
this.InitializeComponent();
this.UModEnabled = this.uModService.Enabled;
this.UModEnabled = this.uModService.IsEnabled;
}
private void OpaqueButtonNo_Clicked(object sender, System.EventArgs e)
@@ -41,7 +41,7 @@ public partial class UModSwitchView : UserControl
private void OpaqueButtonYes_Clicked(object sender, System.EventArgs e)
{
this.uModService.Enabled = !this.uModService.Enabled;
this.uModService.IsEnabled = !this.uModService.IsEnabled;
this.viewManager.ShowView<LauncherView>();
}