diff --git a/SysBot.Pokemon/Helpers/TargetFlawlessIVsConverter.cs b/SysBot.Pokemon/Helpers/TargetFlawlessIVsConverter.cs new file mode 100644 index 00000000..865be125 --- /dev/null +++ b/SysBot.Pokemon/Helpers/TargetFlawlessIVsConverter.cs @@ -0,0 +1,36 @@ +using System; +using System.ComponentModel; +using System.Globalization; + +namespace SysBot.Pokemon; + +public class TargetFlawlessIVsConverter(Type type) : EnumConverter(type) +{ + public override object? ConvertTo(ITypeDescriptorContext? context, CultureInfo? culture, object? value, Type destinationType) + { + if (value == null) return base.ConvertTo(context, culture, value, destinationType); + + var name = Enum.GetName(type, value); + if (string.IsNullOrWhiteSpace(name)) + return value.ToString(); + + var fieldInfo = type.GetField(name); + if (fieldInfo == null) + return value.ToString(); + + return Attribute.GetCustomAttribute(fieldInfo, typeof(DescriptionAttribute)) is DescriptionAttribute dna + ? dna.Description + : value.ToString(); + } + + public override object? ConvertFrom(ITypeDescriptorContext? context, CultureInfo? culture, object value) + { + foreach (var fieldInfo in type.GetFields()) + { + if (Attribute.GetCustomAttribute(fieldInfo, typeof(DescriptionAttribute)) is DescriptionAttribute dna && (string)value == dna.Description) + return Enum.Parse(type, fieldInfo.Name); + } + + return Enum.Parse(type, (string)value); + } +} diff --git a/SysBot.Pokemon/Settings/StopConditionSettings.cs b/SysBot.Pokemon/Settings/StopConditionSettings.cs index 723d9420..06699c8f 100644 --- a/SysBot.Pokemon/Settings/StopConditionSettings.cs +++ b/SysBot.Pokemon/Settings/StopConditionSettings.cs @@ -44,7 +44,16 @@ public class StopConditionSettings [Category(StopConditions)] public class SearchCondition { - public override string ToString() => $"{(!IsEnabled ? $"{Nature}, condition is disabled" : $"{Nature}, {StopOnSpecies}, {TargetMinIVs} - {TargetMaxIVs}")}"; + public override string ToString() + { + if (!IsEnabled) return $"{Nature}, condition is disabled"; + + var ivsStr = FlawlessIVs == TargetFlawlessIVsType.Disabled + ? $"{TargetMinIVs} - {TargetMaxIVs}" + : $"Flawless IVs: {Convert(FlawlessIVs)}"; + + return $"{Nature}, {StopOnSpecies}, {ivsStr}"; + } [Category(StopConditions), DisplayName("1. Enabled")] public bool IsEnabled { get; set; } = true; @@ -58,10 +67,14 @@ public class SearchCondition [Category(StopConditions), DisplayName("4. Gender")] public TargetGenderType GenderTarget { get; set; } = TargetGenderType.Any; - [Category(StopConditions), DisplayName("5. Minimum accepted IVs")] + [Category(StopConditions), DisplayName("5. Minimum flawless IVs")] + [TypeConverter(typeof(TargetFlawlessIVsConverter))] + public TargetFlawlessIVsType FlawlessIVs { get; set; } = TargetFlawlessIVsType.Disabled; + + [Category(StopConditions), DisplayName("6. Minimum accepted IVs")] public string TargetMinIVs { get; set; } = ""; - [Category(StopConditions), DisplayName("6. Maximum accepted IVs")] + [Category(StopConditions), DisplayName("7. Maximum accepted IVs")] public string TargetMaxIVs { get; set; } = ""; } @@ -112,13 +125,13 @@ public static bool EncounterFound(T pk, StopConditionSettings settings, IRead return true; return settings.SearchConditions.Any(s => - MatchIVs(pkIVsArr, s.TargetMinIVs, s.TargetMaxIVs) && + (MatchIVs(pkIVsArr, s.TargetMinIVs, s.TargetMaxIVs, s.FlawlessIVs) || MatchFlawlessIVs(pkIVsArr, s.FlawlessIVs)) && (s.Nature == pk.Nature || s.Nature == Nature.Random) && (s.StopOnSpecies == (Species)pk.Species || s.StopOnSpecies == Species.None) && MatchGender(s.GenderTarget, (Gender)pk.Gender) && s.IsEnabled); } - + private static bool MatchGender(TargetGenderType target, Gender result) { return target switch @@ -131,8 +144,28 @@ private static bool MatchGender(TargetGenderType target, Gender result) }; } - private static bool MatchIVs(IReadOnlyList pkIVs, string targetMinIVsStr, string targetMaxIVsStr) + private static bool MatchFlawlessIVs(IReadOnlyList pkIVs, TargetFlawlessIVsType targetFlawlessIVs) + { + var count = pkIVs.Count(iv => iv == 31); + + return targetFlawlessIVs switch + { + TargetFlawlessIVsType.Disabled => false, + TargetFlawlessIVsType._0 => count >= 0, + TargetFlawlessIVsType._1 => count >= 1, + TargetFlawlessIVsType._2 => count >= 2, + TargetFlawlessIVsType._3 => count >= 3, + TargetFlawlessIVsType._4 => count >= 4, + TargetFlawlessIVsType._5 => count >= 5, + TargetFlawlessIVsType._6 => count == 6, + _ => throw new ArgumentOutOfRangeException(nameof(targetFlawlessIVs), targetFlawlessIVs, null) + }; + } + + private static bool MatchIVs(IReadOnlyList pkIVs, string targetMinIVsStr, string targetMaxIVsStr, TargetFlawlessIVsType targetFlawlessIVs) { + if (targetFlawlessIVs != TargetFlawlessIVsType.Disabled) return false; + var targetMinIVs = ReadTargetIVs(targetMinIVsStr, true); var targetMaxIVs = ReadTargetIVs(targetMaxIVsStr, false); @@ -215,6 +248,25 @@ public static string GetMarkName(IRibbonIndex pk) } return ""; } + + // Quite ugly solution to display DescriptionAttribute + private static string Convert(T value) where T : Enum + { + var k = typeof(T); + var g = k.Name; + + var name = Enum.GetName(typeof(T), value); + if (string.IsNullOrWhiteSpace(name)) + return value.ToString(); + + var fieldInfo = typeof(T).GetField(name); + if (fieldInfo == null) + return value.ToString(); + + return Attribute.GetCustomAttribute(fieldInfo, typeof(DescriptionAttribute)) is DescriptionAttribute dna + ? dna.Description + : value.ToString(); + } } public enum TargetShinyType @@ -233,3 +285,15 @@ public enum TargetGenderType Female, // Match female only Genderless, // Match genderless only } + +public enum TargetFlawlessIVsType +{ + Disabled, + [Description("0")] _0, + [Description("1")] _1, + [Description("2")] _2, + [Description("3")] _3, + [Description("4")] _4, + [Description("5")] _5, + [Description("6")] _6, +} diff --git a/SysBot.Tests/GenerateTests.cs b/SysBot.Tests/GenerateTests.cs index e3a75643..b062f17b 100644 --- a/SysBot.Tests/GenerateTests.cs +++ b/SysBot.Tests/GenerateTests.cs @@ -2,6 +2,7 @@ using PKHeX.Core; using SysBot.Pokemon; using Xunit; +using LegalitySettings = SysBot.Pokemon.LegalitySettings; namespace SysBot.Tests; @@ -63,48 +64,7 @@ public void TestAbilityTwitch(string set, int abilNumber) } } - [Theory] - [InlineData(InvalidSpec)] - public void ShouldNotGenerate(string set) - { - _ = AutoLegalityWrapper.GetTrainerInfo(); - var s = ShowdownUtil.ConvertToShowdown(set); - s.Should().BeNull(); - } - - [Theory] - [InlineData(Torkoal2, 2)] - [InlineData(Charizard4, 4)] - public void TestAbility(string set, int abilNumber) - { - var sav = AutoLegalityWrapper.GetTrainerInfo(); - for (int i = 0; i < 10; i++) - { - var s = new ShowdownSet(set); - var template = AutoLegalityWrapper.GetTemplate(s); - var pk = sav.GetLegal(template, out _); - pk.AbilityNumber.Should().Be(abilNumber); - } - } - - [Theory] - [InlineData(Torkoal2, 2)] - [InlineData(Charizard4, 4)] - public void TestAbilityTwitch(string set, int abilNumber) - { - var sav = AutoLegalityWrapper.GetTrainerInfo(); - for (int i = 0; i < 10; i++) - { - var twitch = set.Replace("\r\n", " ").Replace("\n", " "); - var s = ShowdownUtil.ConvertToShowdown(twitch); - var template = s == null ? null : AutoLegalityWrapper.GetTemplate(s); - var pk = template == null ? null : sav.GetLegal(template, out _); - pk.Should().NotBeNull(); - pk!.AbilityNumber.Should().Be(abilNumber); - } - } - - private const string Gengar = + private const string Gengar = @"Gengar-Gmax @ Life Orb Ability: Cursed Body Shiny: Yes @@ -159,7 +119,6 @@ Timid Nature - Solar Beam - Beat Up"; - private const string InvalidSpec = + private const string InvalidSpec = "(Pikachu)"; - } } diff --git a/SysBot.Tests/Resources/0133 - Eevee - D7F3BFC57EC0.pk9 b/SysBot.Tests/Resources/0133 - Eevee - D7F3BFC57EC0.pk9 new file mode 100644 index 00000000..557b7a49 Binary files /dev/null and b/SysBot.Tests/Resources/0133 - Eevee - D7F3BFC57EC0.pk9 differ diff --git a/SysBot.Tests/Resources/0813 - Scorbunny - 4F320450C78B.pk9 b/SysBot.Tests/Resources/0813 - Scorbunny - 4F320450C78B.pk9 new file mode 100644 index 00000000..26b5e02e Binary files /dev/null and b/SysBot.Tests/Resources/0813 - Scorbunny - 4F320450C78B.pk9 differ diff --git a/SysBot.Tests/StopConditionsTests.cs b/SysBot.Tests/StopConditionsTests.cs new file mode 100644 index 00000000..d04cc9c3 --- /dev/null +++ b/SysBot.Tests/StopConditionsTests.cs @@ -0,0 +1,67 @@ +using System.IO; +using System.Reflection; +using System.Threading.Tasks; +using PKHeX.Core; +using SysBot.Pokemon; +using Xunit; + +namespace SysBot.Tests; + +public class StopConditionsTests +{ + [Theory] + [InlineData(TargetFlawlessIVsType.Any, true)] + [InlineData(TargetFlawlessIVsType._0, true)] + [InlineData(TargetFlawlessIVsType._1, true)] + [InlineData(TargetFlawlessIVsType._2, true)] + [InlineData(TargetFlawlessIVsType._3, true)] + [InlineData(TargetFlawlessIVsType._4, true)] + [InlineData(TargetFlawlessIVsType._5, false)] + [InlineData(TargetFlawlessIVsType._6, false)] + public async Task TestEncounterFound_Scorbunny_FlawlessIVs(TargetFlawlessIVsType targetFlawlessIVs, bool expected) + { + // Arrange + var bytes = await GetResource("0813 - Scorbunny - 4F320450C78B.pk9"); + + var pk9 = new PK9(bytes); + var sc = new StopConditionSettings { SearchConditions = [new() { FlawlessIVs = targetFlawlessIVs }] }; + + // Act + var result = StopConditionSettings.EncounterFound(pk9, sc, null); + + // Assert + Assert.Equal(expected, result); + } + + [Theory] + [InlineData("x/x/x/x/x/x", "x/x/x/x/x/x", true)] + [InlineData("x/31/31/31/x/31", "x/31/31/31/x/31", true)] + [InlineData("31/x/x/x/x/x", "31/x/x/x/x/x", false)] + public async Task TestEncounterFound_Scorbunny_MatchIVs(string targetMinIVs, string targetMaxIVs, bool expected) + { + // Arrange + var bytes = await GetResource("0813 - Scorbunny - 4F320450C78B.pk9"); + + var pk9 = new PK9(bytes); + var sc = new StopConditionSettings { SearchConditions = [new() { TargetMinIVs = targetMinIVs, TargetMaxIVs = targetMaxIVs, FlawlessIVs = TargetFlawlessIVsType.Any }] }; + + // Act + var result = StopConditionSettings.EncounterFound(pk9, sc, null); + + // Assert + Assert.Equal(expected, result); + } + + private static async Task GetResource(string file) + { + var info = Assembly.GetExecutingAssembly().GetName(); + + await using var stream = Assembly + .GetExecutingAssembly() + .GetManifestResourceStream($"{info.Name}.Resources.{file}")!; + + using var memoryStream = new MemoryStream(); + await stream.CopyToAsync(memoryStream); + return memoryStream.ToArray(); + } +} diff --git a/SysBot.Tests/SysBot.Tests.csproj b/SysBot.Tests/SysBot.Tests.csproj index c88570a5..c049e26d 100644 --- a/SysBot.Tests/SysBot.Tests.csproj +++ b/SysBot.Tests/SysBot.Tests.csproj @@ -1,9 +1,17 @@ - + false + + + + + + + +