diff --git a/.dockerignore b/.dockerignore index bbbfc1d2a..a47351064 100644 --- a/.dockerignore +++ b/.dockerignore @@ -24,3 +24,4 @@ LICENSE README.md **/appsettings.local.json +**/wwwroot/lib diff --git a/.editorconfig b/.editorconfig index 3ed917775..071ab9e50 100644 --- a/.editorconfig +++ b/.editorconfig @@ -43,7 +43,7 @@ dotnet_naming_symbols.private_static_readonly_symbols.required_modifiers = stati csharp_new_line_before_members_in_object_initializers = false csharp_preferred_modifier_order = public, private, protected, internal, new, static, abstract, virtual, sealed, readonly, override, extern, unsafe, volatile, async:suggestion -csharp_style_var_elsewhere = false:suggestion +csharp_style_var_elsewhere = true:suggestion csharp_style_var_for_built_in_types = true:suggestion csharp_style_var_when_type_is_apparent = true:suggestion dotnet_style_parentheses_in_arithmetic_binary_operators = never_if_unnecessary:suggestion @@ -71,8 +71,8 @@ resharper_csharp_keep_blank_lines_in_code = 1 resharper_csharp_keep_blank_lines_in_declarations = 1 resharper_csharp_wrap_after_declaration_lpar = true resharper_csharp_wrap_parameters_style = chop_if_long -resharper_for_built_in_types = use_var_when_evident -resharper_for_other_types = use_var_when_evident +resharper_for_built_in_types = use_var +resharper_for_other_types = use_var resharper_for_simple_types = use_var resharper_object_creation_when_type_not_evident = target_typed resharper_parentheses_redundancy_style = remove_if_not_clarifies_precedence @@ -149,6 +149,7 @@ dotnet_diagnostic.CA1304.severity = suggestion # Specify cultureInfo dotnet_diagnostic.CA1309.severity = suggestion # Use ordinal StringComparison dotnet_diagnostic.CA1311.severity = suggestion # Specify a culture or use an invariant version dotnet_diagnostic.CA1822.severity = suggestion # Mark member as static +dotnet_diagnostic.CA1859.severity = suggestion # Use concrete types when possible for improved performance [*.axaml] max_line_length = 160 diff --git a/Directory.Build.props b/Directory.Build.props index b3e429f57..cba5fd1ff 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -1,8 +1,13 @@  - net7.0 + net8.0 + net8.0-windows + + + + $(CommonTargetFramework) enable - 11 + 12 Recommended diff --git a/README.md b/README.md index 4c20e0169..47b40c1c7 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # WoWs-ShipBuilder

- +

## General Information @@ -24,7 +24,7 @@ Update checks only run on application startup so it won't mess up your system wi ## Telemetry data and error reports -We do not collect any personal data in our application because it's simply not necessary for the app to work. +We do not collect any personal data in our application because it's simply not necessary for the app to work. However, we do automatically collect error reports. If the application encounters an error, it automatically sends a report to [Sentry](https://sentry.io/) containing the error data. This data does not contain IP addresses or other personal information. An error is not always visible for you as most errors should be caught internally and handled using fallback actions. @@ -39,7 +39,7 @@ If you want to see the program translate in your language, you can follow the gu Hosting the server where we store the data used by the program has a monthly cost. If you like the program and would like to help us out, you can donate at https://ko-fi.com/wowsshipbuilder. The app will always be free and with no ads. When you donate, you will also get a special role in our discord server. ## Discord -We have a discord server that you can join by clicking [here](https://discord.gg/C8EaepZJDY) . +We have a discord server that you can join by clicking [here](https://discord.gg/C8EaepZJDY) . ## Sponsorships and Support diff --git a/WoWsShipBuilder.Common/Features/BallisticCharts/DispersionPlotHelper.cs b/WoWsShipBuilder.Common/Features/BallisticCharts/DispersionPlotHelper.cs index a5314140b..2d140a737 100644 --- a/WoWsShipBuilder.Common/Features/BallisticCharts/DispersionPlotHelper.cs +++ b/WoWsShipBuilder.Common/Features/BallisticCharts/DispersionPlotHelper.cs @@ -32,7 +32,7 @@ private static (double waterLineProjection, double perpendicularToWaterProjectio { double impactAngle; List> ballistic = BallisticHelper.CalculateBallistic(shell, maxRange, shell.Penetration).Where(x => x.Key >= aimingRange).ToList(); - if (ballistic.Any()) + if (ballistic.Count != 0) { impactAngle = ballistic[0].Value.ImpactAngle; } diff --git a/WoWsShipBuilder.Common/Features/Builds/BuildValidation.cs b/WoWsShipBuilder.Common/Features/Builds/BuildValidation.cs index 4fb9f166c..465393671 100644 --- a/WoWsShipBuilder.Common/Features/Builds/BuildValidation.cs +++ b/WoWsShipBuilder.Common/Features/Builds/BuildValidation.cs @@ -62,7 +62,7 @@ public static async Task ValidateBuildString(string List invalidChars = Path.GetInvalidFileNameChars().ToList(); invalidChars.Add(';'); List invalidCharsInBuildName = invalidChars.FindAll(buildName.Contains); - return invalidCharsInBuildName.Any() ? $"Invalid characters {string.Join(' ', invalidCharsInBuildName)}" : null; + return invalidCharsInBuildName.Count != 0 ? $"Invalid characters {string.Join(' ', invalidCharsInBuildName)}" : null; } public static async Task RetrieveLongUrlFromShortLink(string shortUrl) diff --git a/WoWsShipBuilder.Common/Features/DataContainers/Aircraft/CvAircraftDataContainer.cs b/WoWsShipBuilder.Common/Features/DataContainers/Aircraft/CvAircraftDataContainer.cs index 07a6f4c88..ef687f15a 100644 --- a/WoWsShipBuilder.Common/Features/DataContainers/Aircraft/CvAircraftDataContainer.cs +++ b/WoWsShipBuilder.Common/Features/DataContainers/Aircraft/CvAircraftDataContainer.cs @@ -110,7 +110,7 @@ public partial record CvAircraftDataContainer : DataContainerBase public static List? FromShip(Ship ship, List shipConfiguration, List modifiers) { - if (!ship.CvPlanes.Any()) + if (ship.CvPlanes.IsEmpty) { return null; } diff --git a/WoWsShipBuilder.Common/Features/DataContainers/Armament/PingerGunDataContainer.cs b/WoWsShipBuilder.Common/Features/DataContainers/Armament/PingerGunDataContainer.cs index f0395c8cc..a4c1a86fb 100644 --- a/WoWsShipBuilder.Common/Features/DataContainers/Armament/PingerGunDataContainer.cs +++ b/WoWsShipBuilder.Common/Features/DataContainers/Armament/PingerGunDataContainer.cs @@ -38,7 +38,7 @@ public partial record PingerGunDataContainer : DataContainerBase public static PingerGunDataContainer? FromShip(Ship ship, IEnumerable shipConfiguration, List modifiers) { - if (!ship.PingerGunList.Any()) + if (ship.PingerGunList.IsEmpty) { return null; } diff --git a/WoWsShipBuilder.Common/Features/DataContainers/DataContainerUtility.cs b/WoWsShipBuilder.Common/Features/DataContainers/DataContainerUtility.cs index 1870938c2..69fb678e9 100644 --- a/WoWsShipBuilder.Common/Features/DataContainers/DataContainerUtility.cs +++ b/WoWsShipBuilder.Common/Features/DataContainers/DataContainerUtility.cs @@ -28,8 +28,13 @@ public static int ApplyModifiers(this List modifierList, string proper public static void UpdateConsumableModifierValue(this List consumableModifierList, List modifierList, string propertySelector, string modifierName) { - var modifier = consumableModifierList.Find(x => x.Name.Equals(modifierName))!; - var newValue = (float)modifierList.ApplyModifiers(propertySelector, (decimal)modifier.Value); + var modifier = consumableModifierList.Find(x => x.Name.Equals(modifierName)); + var newValue = (float)modifierList.ApplyModifiers(propertySelector, (decimal)(modifier?.Value ?? 0)); + if (modifier == null) + { + return; + } + consumableModifierList.Remove(modifier); consumableModifierList.Add(new Modifier(modifier.Name, newValue, "", modifier)); } diff --git a/WoWsShipBuilder.Common/Features/DataContainers/Ship/ConsumableDataContainer.cs b/WoWsShipBuilder.Common/Features/DataContainers/Ship/ConsumableDataContainer.cs index b0c32dc13..83d444090 100644 --- a/WoWsShipBuilder.Common/Features/DataContainers/Ship/ConsumableDataContainer.cs +++ b/WoWsShipBuilder.Common/Features/DataContainers/Ship/ConsumableDataContainer.cs @@ -52,7 +52,7 @@ private static ConsumableDataContainer FromTypeAndVariant(string name, string va { var consumableIdentifier = $"{name} {variant}"; var usingFallback = false; - if (!(AppData.ConsumableList?.TryGetValue(consumableIdentifier, out var consumable) ?? false)) + if (!AppData.ConsumableList.TryGetValue(consumableIdentifier, out var consumable)) { Logging.Logger.LogError("Consumable {Identifier} not found in cached consumable list. Using dummy consumable instead", consumableIdentifier); usingFallback = true; @@ -99,7 +99,7 @@ private static ConsumableDataContainer FromTypeAndVariant(string name, string va consumableModifiers.UpdateConsumableModifierValue(modifiers, "ConsumableDataContainer.TimeDelayAttack.PCY035", "timeDelayAttack"); consumableModifiers.UpdateConsumableModifierValue(modifiers, "ConsumableDataContainer.TimeDelayAppear.PCY035", "timeFromHeaven"); - var plane = AppData.FindAircraft(consumable.PlaneName[..consumable.PlaneName.IndexOf("_", StringComparison.Ordinal)]); + var plane = AppData.FindAircraft(consumable.PlaneName[..consumable.PlaneName.IndexOf('_', StringComparison.Ordinal)]); var oldCruisingSpeed = consumableModifiers.Find(x => x.Name.Equals("cruisingSpeed", StringComparison.Ordinal)); if (oldCruisingSpeed is not null) { @@ -257,7 +257,7 @@ private static ConsumableDataContainer FromTypeAndVariant(string name, string va consumableModifiers.UpdateConsumableModifierValue(modifiers, "ConsumableDataContainer.ExtraFighters.PCY012.PCY03", "fightersNum"); var maxKills = consumableModifiers.First(x => x.Name.Equals("fightersNum", StringComparison.Ordinal)).Value; - var plane = AppData.FindAircraft(consumable.PlaneName[..consumable.PlaneName.IndexOf("_", StringComparison.Ordinal)]); + var plane = AppData.FindAircraft(consumable.PlaneName[..consumable.PlaneName.IndexOf('_', StringComparison.Ordinal)]); var oldCruisingModifier = consumableModifiers.Find(x => x.Name.Equals("cruisingSpeed", StringComparison.Ordinal)); if (oldCruisingModifier is not null) @@ -303,12 +303,15 @@ private static ConsumableDataContainer FromTypeAndVariant(string name, string va else if (name.Contains("PCY045", StringComparison.InvariantCultureIgnoreCase)) { // Hydrophone + // used prior to 13.1 consumableModifiers.UpdateConsumableModifierValue(modifiers, "ConsumableDataContainer.HydrophoneUpdateFrequency.PCY045", "hydrophoneUpdateFrequency"); + cooldown = modifiers.ApplyModifiers("ConsumableDataContainer.Reload.PCY045", cooldown); } else if (name.Contains("PCY048", StringComparison.InvariantCultureIgnoreCase)) { // Submarine Surveillance prepTime = modifiers.ApplyModifiers("ConsumableDataContainer.PrepTime.PCY048", prepTime); + cooldown = modifiers.ApplyModifiers("ConsumableDataContainer.Reload.PCY048", cooldown); } } else if (usingFallback) diff --git a/WoWsShipBuilder.Common/Features/ShipComparison/ShipComparisonViewModel.cs b/WoWsShipBuilder.Common/Features/ShipComparison/ShipComparisonViewModel.cs index 2e83616cf..4adc728a5 100644 --- a/WoWsShipBuilder.Common/Features/ShipComparison/ShipComparisonViewModel.cs +++ b/WoWsShipBuilder.Common/Features/ShipComparison/ShipComparisonViewModel.cs @@ -71,7 +71,7 @@ private Dictionary FilteredShipList public Dictionary PinnedShipList { get; } = new(); - public List DataSections { get; private set; } = new() { ShipComparisonDataSections.General }; + public List DataSections { get; private set; } = [ShipComparisonDataSections.General]; public ShipComparisonDataSections SelectedDataSection { get; set; } = ShipComparisonDataSections.General; @@ -163,11 +163,7 @@ public void ToggleShowPinnedShipOnly() public async Task ToggleTierSelection(int value) { - if (this.SelectedTiers.Contains(value)) - { - this.SelectedTiers.Remove(value); - } - else + if (!this.SelectedTiers.Remove(value)) { this.SelectedTiers.Add(value); } @@ -177,11 +173,7 @@ public async Task ToggleTierSelection(int value) public async Task ToggleClassSelection(ShipClass value) { - if (this.SelectedClasses.Contains(value)) - { - this.SelectedClasses.Remove(value); - } - else + if (!this.SelectedClasses.Remove(value)) { this.SelectedClasses.Add(value); } @@ -191,11 +183,7 @@ public async Task ToggleClassSelection(ShipClass value) public async Task ToggleNationSelection(Nation value) { - if (this.SelectedNations.Contains(value)) - { - this.SelectedNations.Remove(value); - } - else + if (!this.SelectedNations.Remove(value)) { this.SelectedNations.Add(value); } @@ -205,11 +193,7 @@ public async Task ToggleNationSelection(Nation value) public async Task ToggleCategorySelection(ShipCategory value) { - if (this.SelectedCategories.Contains(value)) - { - this.SelectedCategories.Remove(value); - } - else + if (!this.SelectedCategories.Remove(value)) { this.SelectedCategories.Add(value); } @@ -293,7 +277,7 @@ public Dictionary RemoveBuilds(IEnumerable x.Key, x => x.Value); foreach (var wrapper in buildList) { - if (this.FilteredShipList.Count(x => x.Value.Ship.Index.Equals(wrapper.Value.Ship.Index)) > 1) + if (this.FilteredShipList.Count(x => x.Value.Ship.Index.Equals(wrapper.Value.Ship.Index, StringComparison.Ordinal)) > 1) { this.FilteredShipList.Remove(wrapper.Key); @@ -341,11 +325,7 @@ public void ResetAllBuilds() public async Task AddPinnedShip(GridDataWrapper wrapper) { - if (!this.PinnedShipList.ContainsKey(wrapper.Id)) - { - this.PinnedShipList.Add(wrapper.Id, wrapper); - } - else + if (!this.PinnedShipList.TryAdd(wrapper.Id, wrapper)) { await this.RemovePinnedShip(wrapper); } @@ -355,11 +335,7 @@ public async Task AddPinnedShip(GridDataWrapper wrapper) public void AddSelectedShip(GridDataWrapper wrapper) { - if (!this.SelectedShipList.ContainsKey(wrapper.Id)) - { - this.SelectedShipList.Add(wrapper.Id, wrapper); - } - else + if (!this.SelectedShipList.TryAdd(wrapper.Id, wrapper)) { this.RemoveSelectedShip(wrapper); } @@ -438,14 +414,14 @@ public void DuplicateSelectedShips() this.PinnedShipList.Add(newWrapper.Id, newWrapper); } - if (this.MainBatteryDispersionCache.ContainsKey(selectedShip.Key)) + if (this.MainBatteryDispersionCache.TryGetValue(selectedShip.Key, out var value)) { - this.MainBatteryDispersionCache[newWrapper.Id] = this.MainBatteryDispersionCache[selectedShip.Key]; + this.MainBatteryDispersionCache[newWrapper.Id] = value; } - if (this.SecondaryBatteryDispersionCache.ContainsKey(selectedShip.Key)) + if (this.SecondaryBatteryDispersionCache.TryGetValue(selectedShip.Key, out var secondaryValue)) { - this.SecondaryBatteryDispersionCache[newWrapper.Id] = this.SecondaryBatteryDispersionCache[selectedShip.Key]; + this.SecondaryBatteryDispersionCache[newWrapper.Id] = secondaryValue; } } @@ -515,7 +491,7 @@ public void SetFiringRange(double value, bool isMainBattery) private Dictionary GetShipsToBeDisplayed(bool disableHideShipsIfNoSelectedSection) { - Dictionary list = this.ShowPinnedShipsOnly ? this.PinnedShipList : this.FilteredShipList; + var list = this.ShowPinnedShipsOnly ? this.PinnedShipList : this.FilteredShipList; if (!disableHideShipsIfNoSelectedSection) { @@ -582,7 +558,7 @@ private void ChangeModulesBatch() private List GetShipConfiguration(Ship ship) { - List shipConfiguration = this.UseUpgradedModules + var shipConfiguration = this.UseUpgradedModules ? ShipModuleHelper.GroupAndSortUpgrades(ship.ShipUpgradeInfo.ShipUpgrades) .OrderBy(entry => entry.Key) .Select(entry => entry.Value) @@ -608,7 +584,7 @@ private Dictionary HideShipsIfNoSelectedSection(IEnumerab return list.ToDictionary(x => x.Key, x => x.Value); } - Dictionary newList = this.SelectedDataSection switch + var newList = this.SelectedDataSection switch { ShipComparisonDataSections.MainBattery => list.Where(x => x.Value.ShipDataContainer.MainBatteryDataContainer is not null).ToDictionary(x => x.Key, x => x.Value), ShipComparisonDataSections.He => list.Where(x => x.Value.HeShell?.Damage is not null).ToDictionary(x => x.Key, x => x.Value), @@ -637,7 +613,7 @@ private Dictionary HideShipsIfNoSelectedSection(IEnumerab private void GetDataSectionsToDisplay() { var displayedShipList = this.GetShipsToBeDisplayed(true); - this.DataSections = !displayedShipList.Any() ? new() { ShipComparisonDataSections.General } : this.HideEmptyDataSections(displayedShipList); + this.DataSections = displayedShipList.Count == 0 ? [ShipComparisonDataSections.General] : this.HideEmptyDataSections(displayedShipList); } [SuppressMessage("Performance", "CA1822", Justification = "not static to preserve file structure")] diff --git a/WoWsShipBuilder.Common/Features/ShipStats/ViewModels/CaptainSkillSelectorViewModel.cs b/WoWsShipBuilder.Common/Features/ShipStats/ViewModels/CaptainSkillSelectorViewModel.cs index 2d4b3909a..7bec2be6f 100644 --- a/WoWsShipBuilder.Common/Features/ShipStats/ViewModels/CaptainSkillSelectorViewModel.cs +++ b/WoWsShipBuilder.Common/Features/ShipStats/ViewModels/CaptainSkillSelectorViewModel.cs @@ -105,7 +105,7 @@ public Captain? SelectedCaptain this.SkillList = this.ConvertSkillToViewModel(this.currentClass, newCaptain); this.CaptainTalentsList.Clear(); - if (newCaptain!.UniqueSkills.Any()) + if (!newCaptain!.UniqueSkills.IsEmpty) { this.CaptainWithTalents = true; foreach ((string _, UniqueSkill talent) in newCaptain.UniqueSkills) @@ -265,7 +265,7 @@ public void AddSkill(Skill skill) public List GetModifiersList() { var modifiers = this.SkillOrderList.ToList() - .Where(skill => skill.Modifiers.Any() && skill.SkillNumber != ArSkillNumber && skill.SkillNumber != ArSkillNumberSubs && skill.SkillNumber != FuriousSkillNumber && skill.SkillNumber != ImprovedRepairPartyReadinessSkillNumber && skill.SkillNumber != ManualSecondaryBatteryAimingSkillNumber) + .Where(skill => !skill.Modifiers.IsEmpty && skill.SkillNumber != ArSkillNumber && skill.SkillNumber != ArSkillNumberSubs && skill.SkillNumber != FuriousSkillNumber && skill.SkillNumber != ImprovedRepairPartyReadinessSkillNumber && skill.SkillNumber != ManualSecondaryBatteryAimingSkillNumber) .SelectMany(m => m.Modifiers) .ToList(); @@ -414,7 +414,7 @@ private IEnumerable CollectTalentModifiers() .SelectMany(talent => talent.Modifiers.Select(modifier => new Modifier(modifier.Name, float.Pow(modifier.Value, talent.ActivationNumbers), "", modifier))); modifiers.AddRange(talentMultipleActivationModifiers); - var talentFireChanceModifier = this.CaptainTalentsList.Where(talent => talent.Status && talent.Modifiers.Any(modifier => modifier.Name.Equals("burnProbabilityBonus", StringComparison.Ordinal))) + var talentFireChanceModifier = this.CaptainTalentsList.Where(talent => talent.Status && talent.Modifiers.Exists(modifier => modifier.Name.Equals("burnProbabilityBonus", StringComparison.Ordinal))) .SelectMany(talent => talent.Modifiers.Select(modifier => new Modifier(modifier.Name, float.Round(modifier.Value * talent.ActivationNumbers, 2), "", modifier))); modifiers.AddRange(talentFireChanceModifier); diff --git a/WoWsShipBuilder.Common/Features/ShipStats/ViewModels/SignalSelectorViewModel.cs b/WoWsShipBuilder.Common/Features/ShipStats/ViewModels/SignalSelectorViewModel.cs index 5160117a2..929f13fb2 100644 --- a/WoWsShipBuilder.Common/Features/ShipStats/ViewModels/SignalSelectorViewModel.cs +++ b/WoWsShipBuilder.Common/Features/ShipStats/ViewModels/SignalSelectorViewModel.cs @@ -49,9 +49,8 @@ private static List> LoadSignalList() public void SignalCommandExecute(Exterior flag) { - if (this.SelectedSignals.Contains(flag)) + if (this.SelectedSignals.Remove(flag)) { - this.SelectedSignals.Remove(flag); this.SignalsNumber--; } else diff --git a/WoWsShipBuilder.Common/WoWsShipBuilder.Common.csproj b/WoWsShipBuilder.Common/WoWsShipBuilder.Common.csproj index fe9486dfa..93c6caea1 100644 --- a/WoWsShipBuilder.Common/WoWsShipBuilder.Common.csproj +++ b/WoWsShipBuilder.Common/WoWsShipBuilder.Common.csproj @@ -11,7 +11,7 @@ - + @@ -63,4 +63,12 @@ + + + + + + + + diff --git a/WoWsShipBuilder.Data.Generator.Test/DataElementGeneratorTests/DataElementGeneratorTest.cs b/WoWsShipBuilder.Data.Generator.Test/DataElementGeneratorTests/DataElementGeneratorTest.cs index 45f6ae3c7..2221ad118 100644 --- a/WoWsShipBuilder.Data.Generator.Test/DataElementGeneratorTests/DataElementGeneratorTest.cs +++ b/WoWsShipBuilder.Data.Generator.Test/DataElementGeneratorTests/DataElementGeneratorTest.cs @@ -50,7 +50,7 @@ protected static bool ShouldAdd(object? value) (typeof(DataElementGenerator.DataElementGenerator), "DataElementFilteringAttribute.g.cs", AttributeHelper.DataElementFilteringAttribute), (typeof(DataElementGenerator.DataElementGenerator), "TestRecord.g.cs", expected), }, - ReferenceAssemblies = ReferenceAssemblies.Net.Net70, + ReferenceAssemblies = ReferenceAssemblies.Net.Net80, AdditionalReferences = { MetadataReference.CreateFromFile(typeof(IDataElement).GetTypeInfo().Assembly.Location) }, }, }; diff --git a/WoWsShipBuilder.Desktop.Test/WoWsShipBuilder.Desktop.Test.csproj b/WoWsShipBuilder.Desktop.Test/WoWsShipBuilder.Desktop.Test.csproj index 2fced84e8..dbf78b23e 100644 --- a/WoWsShipBuilder.Desktop.Test/WoWsShipBuilder.Desktop.Test.csproj +++ b/WoWsShipBuilder.Desktop.Test/WoWsShipBuilder.Desktop.Test.csproj @@ -1,7 +1,7 @@ - net7.0-windows + $(DesktopTargetFramework) enable enable diff --git a/WoWsShipBuilder.Desktop/App.axaml.cs b/WoWsShipBuilder.Desktop/App.axaml.cs index 4e4fffec6..c0a2b3cb7 100644 --- a/WoWsShipBuilder.Desktop/App.axaml.cs +++ b/WoWsShipBuilder.Desktop/App.axaml.cs @@ -1,6 +1,5 @@ using System; using System.Diagnostics.CodeAnalysis; -using System.Linq; using System.Reflection; using System.Runtime.Versioning; using System.Threading; @@ -140,7 +139,7 @@ private async Task UpdateCheck(AppNotificationService notificationService) { // Can throw a null-reference-exception, no idea why. var updateInfo = await updateManager.CheckForUpdate(); - if (!updateInfo.ReleasesToApply.Any()) + if (updateInfo.ReleasesToApply.Count == 0) { this.logger.LogInformation("No app update found"); return; diff --git a/WoWsShipBuilder.Desktop/Features/Updater/LocalDataUpdater.cs b/WoWsShipBuilder.Desktop/Features/Updater/LocalDataUpdater.cs index a5620aa8f..b3174db60 100644 --- a/WoWsShipBuilder.Desktop/Features/Updater/LocalDataUpdater.cs +++ b/WoWsShipBuilder.Desktop/Features/Updater/LocalDataUpdater.cs @@ -206,7 +206,7 @@ public async Task CheckJsonFileVersions(ServerType serverType shouldLocalizationUpdate = false; } - if (filesToDownload.Any()) + if (filesToDownload.Count != 0) { filesToDownload.Add((string.Empty, "VersionInfo.json")); } @@ -267,7 +267,7 @@ public async Task ValidateData(ServerType serverType, string d } } - if (!missingFiles.Any()) + if (missingFiles.Count == 0) { return new(true); } @@ -290,12 +290,12 @@ public async Task ShouldUpdaterRun(ServerType serverType) public async Task CheckInstalledLocalizations(ServerType serverType) { - List installedLocales = await this.appDataService.GetInstalledLocales(serverType, false); + var installedLocales = await this.appDataService.GetInstalledLocales(serverType, false); if (!installedLocales.Contains(this.appSettings.SelectedLanguage.LocalizationFileName)) { this.logger.LogInformation("Selected localization is not installed. Downloading file..."); string localizationFile = this.appSettings.SelectedLanguage.LocalizationFileName + ".json"; - await this.awsClient.DownloadFiles(serverType, new() { ("Localization", localizationFile) }); + await this.awsClient.DownloadFiles(serverType, [("Localization", localizationFile)]); this.logger.LogInformation("Downloaded localization file for selected localization. Updating localizer data..."); } else @@ -311,8 +311,8 @@ public async Task CheckInstalledLocalizations(ServerType serverType) /// An used to monitor the progress of the update. private async Task CheckFilesAndDownloadUpdates(ServerType serverType, IProgress<(int, string)> progressTracker) { - UpdateCheckResult checkResult = await this.CheckJsonFileVersions(serverType); - if (checkResult.AvailableFileUpdates.Any()) + var checkResult = await this.CheckJsonFileVersions(serverType); + if (checkResult.AvailableFileUpdates.Count != 0) { this.logger.LogInformation("Updating {AvailableUpdateCount} files...", checkResult.AvailableFileUpdates.Count); progressTracker.Report((1, nameof(Translation.SplashScreen_Json))); @@ -342,7 +342,7 @@ private async Task ImageUpdate(IProgress<(int, string)> progressTracker, bool ca { string imageBasePath = this.appDataService.AppDataImageDirectory; var shipImageDirectory = this.fileSystem.DirectoryInfo.New(this.fileSystem.Path.Combine(imageBasePath, "Ships")); - if (!shipImageDirectory.Exists || !shipImageDirectory.GetFiles().Any() || !canDeltaUpdate) + if (!shipImageDirectory.Exists || shipImageDirectory.GetFiles().Length == 0 || !canDeltaUpdate) { progressTracker.Report((2, nameof(Translation.SplashScreen_ShipImages))); await this.awsClient.DownloadImages(this.fileSystem); diff --git a/WoWsShipBuilder.Desktop/Infrastructure/Data/DesktopAppDataService.cs b/WoWsShipBuilder.Desktop/Infrastructure/Data/DesktopAppDataService.cs index 2a2ed1e29..4d11bd745 100644 --- a/WoWsShipBuilder.Desktop/Infrastructure/Data/DesktopAppDataService.cs +++ b/WoWsShipBuilder.Desktop/Infrastructure/Data/DesktopAppDataService.cs @@ -99,21 +99,22 @@ public async Task LoadLocalFilesAsync(ServerType serverType) var localVersionInfo = await this.GetCurrentVersionInfo(serverType) ?? throw new InvalidOperationException("No local data found"); AppData.DataVersion = localVersionInfo.CurrentVersion.MainVersion.ToString(3) + "#" + localVersionInfo.CurrentVersion.DataIteration; - var dataRootInfo = this.fileSystem.DirectoryInfo.New(this.GetDataPath(serverType)); - IDirectoryInfo[] categories = dataRootInfo.GetDirectories(); + var dataRootPath = this.GetDataPath(serverType); // Multiple categories can be loaded simultaneously without concurrency issues because every cache is only used by one category. - await Parallel.ForEachAsync(categories, async (category, ct) => + await Parallel.ForEachAsync(localVersionInfo.Categories, async (category, ct) => { - if (category.Name.Contains("Localization", StringComparison.InvariantCultureIgnoreCase)) + if (category.Key.Contains("Localization", StringComparison.InvariantCultureIgnoreCase)) { return; } - foreach (var file in category.GetFiles()) + var categoryPath = this.fileSystem.Path.Combine(dataRootPath, category.Key); + foreach (var file in category.Value.Select(file => file.FileName)) { - string content = await this.fileSystem.File.ReadAllTextAsync(file.FullName, ct); - await DataCacheHelper.AddToCache(file.Name, category.Name, content); + var filePath = this.fileSystem.Path.Combine(categoryPath, file); + string content = await this.fileSystem.File.ReadAllTextAsync(filePath, ct); + await DataCacheHelper.AddToCache(file, category.Key, content); } }); diff --git a/WoWsShipBuilder.Desktop/Infrastructure/HostApplicationBuilderExtensions.cs b/WoWsShipBuilder.Desktop/Infrastructure/HostApplicationBuilderExtensions.cs index 2869dde6e..8675fc8e6 100644 --- a/WoWsShipBuilder.Desktop/Infrastructure/HostApplicationBuilderExtensions.cs +++ b/WoWsShipBuilder.Desktop/Infrastructure/HostApplicationBuilderExtensions.cs @@ -27,7 +27,7 @@ public static HostApplicationBuilder UseShipBuilderDesktop(this HostApplicationB builder.Services.AddSingleton(); builder.Services.AddSingleton(); builder.Services.AddSingleton(); - builder.Services.AddSingleton(); + builder.Services.AddScoped(); builder.Services.AddSingleton(); builder.Services.AddSingleton(x => x.GetRequiredService()); builder.Services.AddSingleton(x => x.GetRequiredService()); diff --git a/WoWsShipBuilder.Desktop/Infrastructure/LocalizeConverter.cs b/WoWsShipBuilder.Desktop/Infrastructure/LocalizeConverter.cs index 901ef4e6d..9bd385cb7 100644 --- a/WoWsShipBuilder.Desktop/Infrastructure/LocalizeConverter.cs +++ b/WoWsShipBuilder.Desktop/Infrastructure/LocalizeConverter.cs @@ -98,10 +98,7 @@ public object ConvertBack(object? value, Type targetType, object? parameter, Cul private static string ToSnakeCase(string camelCaseString) { - if (camelCaseString == null) - { - throw new ArgumentNullException(nameof(camelCaseString)); - } + ArgumentNullException.ThrowIfNull(camelCaseString); if (camelCaseString.Length < 2) { diff --git a/WoWsShipBuilder.Desktop/Infrastructure/WebView/BlazorWebView.cs b/WoWsShipBuilder.Desktop/Infrastructure/WebView/BlazorWebView.cs index 856af3be5..f92b7ba27 100644 --- a/WoWsShipBuilder.Desktop/Infrastructure/WebView/BlazorWebView.cs +++ b/WoWsShipBuilder.Desktop/Infrastructure/WebView/BlazorWebView.cs @@ -2,6 +2,7 @@ using System.Diagnostics.CodeAnalysis; using Avalonia; using Avalonia.Controls; +using Avalonia.Interactivity; using Avalonia.Platform; using DynamicData; using Microsoft.AspNetCore.Components.WebView.WindowsForms; @@ -184,17 +185,8 @@ protected override IPlatformHandle CreateNativeControlCore(IPlatformHandle paren return base.CreateNativeControlCore(parent); } - private void CoreWebView2OnIsDefaultDownloadDialogOpenChanged(object? sender, object e) - { - if (this.blazorWebView?.WebView.CoreWebView2.IsDefaultDownloadDialogOpen == true) - { - this.blazorWebView.WebView.CoreWebView2.CloseDefaultDownloadDialog(); - } - } - private void WebViewOnCoreWebView2InitializationCompleted(object? sender, CoreWebView2InitializationCompletedEventArgs e) { - // blazorWebView!.WebView.CoreWebView2.IsDefaultDownloadDialogOpenChanged += CoreWebView2OnIsDefaultDownloadDialogOpenChanged; this.DefaultDownloadFolderPath = this.defaultDownloadPath; this.blazorWebView!.WebView.CoreWebView2InitializationCompleted -= this.WebViewOnCoreWebView2InitializationCompleted; } @@ -203,7 +195,6 @@ protected override void DestroyNativeControlCore(IPlatformHandle control) { if (OperatingSystem.IsWindows()) { - // blazorWebView!.WebView.CoreWebView2.IsDefaultDownloadDialogOpenChanged -= CoreWebView2OnIsDefaultDownloadDialogOpenChanged; this.blazorWebView?.Dispose(); this.blazorWebView = null; } @@ -213,14 +204,14 @@ protected override void DestroyNativeControlCore(IPlatformHandle control) } } - protected override void OnDetachedFromVisualTree(VisualTreeAttachmentEventArgs e) + protected override void OnUnloaded(RoutedEventArgs e) { - base.OnDetachedFromVisualTree(e); if (OperatingSystem.IsWindows()) { - // Do not use until dotnet 8 because disposing the webview will deadlock. see https://github.com/dotnet/maui/issues/7997#issuecomment-1258681003 - // blazorWebView?.Dispose(); + this.blazorWebView?.Dispose(); this.blazorWebView = null; } + + base.OnUnloaded(e); } } diff --git a/WoWsShipBuilder.Desktop/Properties/PublishProfiles/PublishWindows.pubxml b/WoWsShipBuilder.Desktop/Properties/PublishProfiles/PublishWindows.pubxml index c319c4a44..030c0bca6 100644 --- a/WoWsShipBuilder.Desktop/Properties/PublishProfiles/PublishWindows.pubxml +++ b/WoWsShipBuilder.Desktop/Properties/PublishProfiles/PublishWindows.pubxml @@ -7,7 +7,7 @@ https://go.microsoft.com/fwlink/?LinkID=208121. Any CPU bin\$(Configuration)\publish\ FileSystem - net7.0-windows + net8.0-windows true win-x64 true diff --git a/WoWsShipBuilder.Desktop/WoWsShipBuilder.Desktop.csproj b/WoWsShipBuilder.Desktop/WoWsShipBuilder.Desktop.csproj index 5d552172f..500bec051 100644 --- a/WoWsShipBuilder.Desktop/WoWsShipBuilder.Desktop.csproj +++ b/WoWsShipBuilder.Desktop/WoWsShipBuilder.Desktop.csproj @@ -1,7 +1,7 @@ WinExe - net7.0-windows + $(DesktopTargetFramework) Assets/ShipBuilderIcon_bg.ico false @@ -33,13 +33,14 @@ - + + diff --git a/WoWsShipBuilder.Web/Dockerfile b/WoWsShipBuilder.Web/Dockerfile index 6dbfb13ed..39f73d740 100644 --- a/WoWsShipBuilder.Web/Dockerfile +++ b/WoWsShipBuilder.Web/Dockerfile @@ -1,30 +1,35 @@ -FROM mcr.microsoft.com/dotnet/aspnet:7.0 AS base -WORKDIR /app -EXPOSE 80 -EXPOSE 443 - -FROM mcr.microsoft.com/dotnet/sdk:7.0 AS build +FROM mcr.microsoft.com/dotnet/sdk:8.0-alpine AS build-env +ARG TARGETARCH WORKDIR /src -COPY . . -RUN dotnet restore "WoWsShipBuilder.Web/WoWsShipBuilder.Web.csproj" -WORKDIR "/src/WoWsShipBuilder.Web" -RUN dotnet build "WoWsShipBuilder.Web.csproj" -c Release -o /app/build -FROM build AS publish -RUN dotnet publish "WoWsShipBuilder.Web.csproj" -c Release -o /app/publish /p:UseAppHost=false +# Copy all project files +COPY . ./ + +# Restore only projects needed for ShipBuilder Web +RUN dotnet restore "WoWsShipBuilder.Web/WoWsShipBuilder.Web.csproj" -a $TARGETARCH + +# Publish ShipBuilder Web +RUN dotnet publish "WoWsShipBuilder.Web/WoWsShipBuilder.Web.csproj" -a $TARGETARCH --no-restore -c Release -o /app -FROM base AS final -ENV APPLICATION_USER_ID 1001 -ENV APPLICATION_USER appuser -LABEL org.opencontainers.image.source=https://github.com/WoWs-Builder-Team/WoWs-ShipBuilder +FROM mcr.microsoft.com/dotnet/aspnet:8.0-alpine AS deploy-env +EXPOSE 8080 + +# Enable globalization and time zones: +# https://github.com/dotnet/dotnet-docker/blob/main/samples/enable-globalization.md +ENV \ + DOTNET_SYSTEM_GLOBALIZATION_INVARIANT=false \ + LC_ALL=en_US.UTF-8 \ + LANG=en_US.UTF-8 +RUN apk add --no-cache \ + icu-data-full \ + icu-libs + +LABEL org.opencontainers.image.source="https://github.com/WoWs-Builder-Team/WoWs-ShipBuilder" LABEL org.opencontainers.image.description="Container image for ShipBuilder Web" LABEL org.opencontainers.image.licenses=MIT - -RUN groupadd --gid $APPLICATION_USER_ID $APPLICATION_USER \ - && useradd --uid $APPLICATION_USER_ID --gid $APPLICATION_USER_ID -m $APPLICATION_USER +LABEL org.opencontainers.image.authors="WoWs Builder Team" WORKDIR /app -COPY --from=publish --chown=$APPLICATION_USER /app/publish . - -USER $APPLICATION_USER -ENTRYPOINT ["dotnet", "WoWsShipBuilder.Web.dll"] +COPY --from=build-env /app . +USER $APP_UID +ENTRYPOINT ["./WoWsShipBuilder.Web"] diff --git a/WoWsShipBuilder.Web/Features/Authentication/AuthenticationService.cs b/WoWsShipBuilder.Web/Features/Authentication/AuthenticationService.cs index 93bcc7221..26a97c4a2 100644 --- a/WoWsShipBuilder.Web/Features/Authentication/AuthenticationService.cs +++ b/WoWsShipBuilder.Web/Features/Authentication/AuthenticationService.cs @@ -39,11 +39,11 @@ public async Task VerifyToken(string accountId, string accessToken) if (response.IsSuccessStatusCode) { var responseData = await response.Content.ReadFromJsonAsync(); - if (responseData is not null && responseData.Status.Equals("ok")) + if (responseData is not null && responseData.Status.Equals("ok", StringComparison.Ordinal)) { - Dictionary? privateData = responseData.Data.FirstOrDefault().Value?.Private; + var privateData = responseData.Data.FirstOrDefault().Value?.Private; this.logger.LogInformation("Token-verification for account {} successful", accountId); - return privateData is not null && privateData.Any(); + return privateData is not null && privateData.Count != 0; } } diff --git a/WoWsShipBuilder.Web/Infrastructure/Data/WebUserDataService.cs b/WoWsShipBuilder.Web/Infrastructure/Data/WebUserDataService.cs index 532f5d0ff..ece6a8bc6 100644 --- a/WoWsShipBuilder.Web/Infrastructure/Data/WebUserDataService.cs +++ b/WoWsShipBuilder.Web/Infrastructure/Data/WebUserDataService.cs @@ -176,6 +176,7 @@ private async Task AddOrUpdateBuilds(List buildsList) var buildsUpdated = 0; var buildsNotNeedingUpdate = 0; + var buildAdded = false; foreach (var build in buildsList) { @@ -210,10 +211,11 @@ private async Task AddOrUpdateBuilds(List buildsList) else { this.savedBuilds.Insert(0, build); + buildAdded = true; } } - if (buildsUpdated == 0 || buildsNotNeedingUpdate == buildsList.Count) + if (!buildAdded && (buildsUpdated == 0 || buildsNotNeedingUpdate == buildsList.Count)) { return -1; } diff --git a/WoWsShipBuilder.sln b/WoWsShipBuilder.sln index b171abfb2..c3f3d79dc 100644 --- a/WoWsShipBuilder.sln +++ b/WoWsShipBuilder.sln @@ -13,6 +13,7 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Configs", "Configs", "{0F12 WoWsShipBuilder.sln.DotSettings = WoWsShipBuilder.sln.DotSettings global.json = global.json Directory.Build.props = Directory.Build.props + .dockerignore = .dockerignore EndProjectSection EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Docs", "Docs", "{43EE144D-CA52-44D5-B761-A27C0A37E1C5}" diff --git a/global.json b/global.json index b69e1fe54..88c19ffc0 100644 --- a/global.json +++ b/global.json @@ -1,6 +1,6 @@ { "sdk": { - "version": "7.0.400", + "version": "8.0.100", "rollForward": "latestFeature", "allowPrerelease": false }, diff --git a/installer/Tools/SquirrelBuildAndRelease.ps1 b/installer/Tools/SquirrelBuildAndRelease.ps1 index 69fd2ac33..d638ccf84 100644 --- a/installer/Tools/SquirrelBuildAndRelease.ps1 +++ b/installer/Tools/SquirrelBuildAndRelease.ps1 @@ -5,7 +5,7 @@ [string][Parameter(Mandatory=$false)]$signingPassword ) -$frameworkVersion="net7.0-windows" +$frameworkVersion="net8.0-windows" if ($skipBuild) { Write-Output "Skipping build"