diff --git a/CHANGELOG.md b/CHANGELOG.md index 29cee641..59c12b87 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,13 @@ All notable changes to Chromatics are documented here. +## 4.4.0 + +- Added support for FFXIV patch 7.56. +- **New:** BST support. +- Minor bug fixes. +- Updated backend dependencies. + ## 4.3.36 - Updated to Sharlayan 9.2.1. diff --git a/Chromatics.DecoratorHarnessUI/Chromatics.DecoratorHarnessUI.csproj b/Chromatics.DecoratorHarnessUI/Chromatics.DecoratorHarnessUI.csproj index 27349f03..e911f0f3 100644 --- a/Chromatics.DecoratorHarnessUI/Chromatics.DecoratorHarnessUI.csproj +++ b/Chromatics.DecoratorHarnessUI/Chromatics.DecoratorHarnessUI.csproj @@ -50,10 +50,10 @@ - - - - + + + + diff --git a/Chromatics.Tests/Chromatics.Tests.csproj b/Chromatics.Tests/Chromatics.Tests.csproj index 8b58cfa7..695e0f1b 100644 --- a/Chromatics.Tests/Chromatics.Tests.csproj +++ b/Chromatics.Tests/Chromatics.Tests.csproj @@ -12,7 +12,7 @@ - + @@ -38,7 +38,7 @@ - + diff --git a/Chromatics.Tests/Models/JobCoverageTests.cs b/Chromatics.Tests/Models/JobCoverageTests.cs new file mode 100644 index 00000000..10b314b7 --- /dev/null +++ b/Chromatics.Tests/Models/JobCoverageTests.cs @@ -0,0 +1,65 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; +using Chromatics.Models; +using Sharlayan.Core.Enums; + +namespace Chromatics.Tests.Models; + +public class JobCoverageTests +{ + // Base classes upgrade into a job and are shown in that job's colours, so + // they carry no palette entry of their own. Unknown is the enum's sentinel + // for "no job read yet" and is never painted. + private static readonly HashSet NotPainted = + ["GLD", "PGL", "MRD", "LNC", "ARC", "CNJ", "THM", "ACN", "ROG", "Unknown"]; + + private static IEnumerable Jobs() + => Enum.GetNames().Where(name => !NotPainted.Contains(name)); + + [Fact] + public void EveryJobHasBaseAndHighlightColours() + { + // Guards the job-addition checklist: a new job in Sharlayan without + // palette entries here paints from whatever the switch falls through + // to, which is silent and easy to miss (BST arrived in 7.56 this way). + var fields = typeof(PaletteColorModel) + .GetFields(BindingFlags.Public | BindingFlags.Instance) + .Select(f => f.Name) + .ToHashSet(StringComparer.Ordinal); + + var missing = Jobs() + .Where(job => !fields.Contains($"Job{job}Base") || !fields.Contains($"Job{job}Highlight")) + .ToList(); + + Assert.True(missing.Count == 0, "Missing: " + string.Join(", ", missing)); + } + + [Fact] + public void EveryJobColourRoundTripsThroughTheLegacyPaletteFile() + { + // Legacy palette files map by name over reflection, so a job whose + // entry exists on the model but not here loads back as its default and + // quietly discards what the user picked. + var dtoProperties = typeof(LegacyColorMappings) + .GetProperties(BindingFlags.Public | BindingFlags.Instance) + .Select(p => p.Name) + .ToHashSet(StringComparer.Ordinal); + + var missing = Jobs() + .Where(job => !dtoProperties.Contains($"ColorMappingJob{job}Base") + || !dtoProperties.Contains($"ColorMappingJob{job}Highlight")) + .ToList(); + + Assert.True(missing.Count == 0, "Missing: " + string.Join(", ", missing)); + } + + [Fact] + public void BeastmasterIsRegistered() + { + Assert.Contains("BST", Enum.GetNames()); + Assert.NotNull(new PaletteColorModel().JobBSTBase); + Assert.NotNull(new PaletteColorModel().JobBSTHighlight); + } +} diff --git a/Chromatics.Tests/Models/PaletteRenameMigrationTests.cs b/Chromatics.Tests/Models/PaletteRenameMigrationTests.cs new file mode 100644 index 00000000..a7105ea5 --- /dev/null +++ b/Chromatics.Tests/Models/PaletteRenameMigrationTests.cs @@ -0,0 +1,55 @@ +using System.Drawing; +using Chromatics.Models; +using Newtonsoft.Json; + +namespace Chromatics.Tests.Models; + +public class PaletteRenameMigrationTests +{ + // Palette files are keyed on the field name, so renaming a field drops + // whatever the user had saved under the old one. The two weather fields + // renamed to their plural in-game spellings carry their old values across + // through setter-only aliases. + private const string LegacyPalette = """ + { + "version": "3", + "WeatherFirestormBase": { "Name": "Firestorm (Base)", "Type": 11, "Color": "Magenta" }, + "WeatherFirestormHighlight": { "Name": "Firestorm (Highlight)", "Type": 11, "Color": "Teal" }, + "WeatherUmbralDuststormBase": { "Name": "Umbral Duststorm (Base)", "Type": 11, "Color": "Navy" } + } + """; + + [Fact] + public void ColoursSavedUnderTheOldWeatherKeysSurviveTheRename() + { + var palette = JsonConvert.DeserializeObject(LegacyPalette); + + Assert.NotNull(palette); + Assert.Equal(Color.Magenta.ToArgb(), palette.WeatherFirestormsBase.Color.ToArgb()); + Assert.Equal(Color.Teal.ToArgb(), palette.WeatherFirestormsHighlight.Color.ToArgb()); + Assert.Equal(Color.Navy.ToArgb(), palette.WeatherUmbralDuststormsBase.Color.ToArgb()); + } + + [Fact] + public void AnUntouchedEntryKeepsItsDefault() + { + var palette = JsonConvert.DeserializeObject(LegacyPalette); + + // Absent from the legacy file, so it must fall back to the initialiser + // rather than being nulled by the partial deserialise. + Assert.NotNull(palette!.WeatherUmbralDuststormsHighlight); + Assert.Equal(Color.SandyBrown.ToArgb(), palette.WeatherUmbralDuststormsHighlight.Color.ToArgb()); + } + + [Fact] + public void SavingDropsTheOldKeys() + { + // The aliases are setter-only, so a re-saved palette carries the new + // spelling alone and the legacy keys retire. + var json = JsonConvert.SerializeObject(new PaletteColorModel()); + + Assert.Contains("WeatherFirestormsBase", json); + Assert.DoesNotContain("\"WeatherFirestormBase\"", json); + Assert.DoesNotContain("\"WeatherUmbralDuststormBase\"", json); + } +} diff --git a/Chromatics.Tests/Models/WeatherCoverageTests.cs b/Chromatics.Tests/Models/WeatherCoverageTests.cs new file mode 100644 index 00000000..13434d02 --- /dev/null +++ b/Chromatics.Tests/Models/WeatherCoverageTests.cs @@ -0,0 +1,76 @@ +using System; +using System.Linq; +using System.Reflection; +using Chromatics.Models; + +namespace Chromatics.Tests.Models; + +public class WeatherCoverageTests +{ + // ReactiveWeatherProcessor.GetWeatherColor builds the palette field name + // from the game's weather name verbatim: + // "Weather" + name.Replace(" ", "") + "Base" + // A field spelled even slightly differently never resolves, and the weather + // silently paints as Unknown instead. Firestorms, Umbral Duststorms and the + // three 7.5x additions all landed that way, which is invisible without a + // test because nothing throws. + // + // Names below are taken from the game's Weather sheet. + private static readonly string[] GameWeather = + [ + "Clear Skies", "Fair Skies", "Clouds", "Fog", "Rain", "Showers", + "Wind", "Gales", "Snow", "Blizzards", "Thunder", "Thunderstorms", + "Dust Storms", "Sandstorms", "Heat Waves", "Umbral Wind", + "Umbral Static", "Everlasting Light", + // Endwalker and Dawntrail zones. + // "Astromagnetic Storms" is deliberately absent: its palette field is + // singular, so the generic lookup misses it, but its own switch case + // assigns WeatherAstromagneticStormBase in both branches and never + // consults the generic path. Renaming the field to match would reset + // the colour every user has already saved under the old name, for no + // change in what they see. + "Atmospheric Phantasms", "Illusory Disturbances", + "Gravitational Flux", "Meteor Showers", "Sporing Mist", + "Annealing Winds", "Glass Storms", "Bubble Bloom", + // Plural spellings that previously fell through to Unknown. + "Firestorms", "Umbral Duststorms", + // Added in the 7.5x patches. + "Lyrical Catharsis", "Auroral Flares", "Floracane", + ]; + + private static string FieldFor(string weather, string suffix) + => "Weather" + weather.Replace(" ", "") + suffix; + + [Fact] + public void EveryWeatherResolvesToAPaletteColour() + { + var fields = typeof(PaletteColorModel) + .GetFields(BindingFlags.Public | BindingFlags.Instance) + .Select(f => f.Name) + .ToHashSet(StringComparer.Ordinal); + + var missing = GameWeather + .Where(w => !fields.Contains(FieldFor(w, "Base"))) + .ToList(); + + Assert.True(missing.Count == 0, + "Weather with no palette colour, so it paints as Unknown: " + string.Join(", ", missing)); + } + + [Fact] + public void WeatherColoursAreRegisteredUnderTheReactiveWeatherCategory() + { + var palette = new PaletteColorModel(); + + foreach (var weather in GameWeather) + { + var field = typeof(PaletteColorModel) + .GetField(FieldFor(weather, "Base"), BindingFlags.Public | BindingFlags.Instance); + + Assert.NotNull(field); + + var mapping = (ColorMapping)field!.GetValue(palette)!; + Assert.Equal(Chromatics.Enums.Palette.PaletteTypes.ReactiveWeather, mapping.Type); + } + } +} diff --git a/Chromatics/Chromatics.csproj b/Chromatics/Chromatics.csproj index c7302bd4..85be1080 100644 --- a/Chromatics/Chromatics.csproj +++ b/Chromatics/Chromatics.csproj @@ -10,7 +10,7 @@ net10.0-windows10.0.19041.0 10.0.17763.0 Chromatics.Program - 4.3.36.0 + 4.4.0.0 Danielle Thompson