diff --git a/Documentation/11.-Bench-Sitting-Legends-Z-A.md b/Documentation/11.-Bench-Sitting-Legends-Z-A.md new file mode 100644 index 00000000..41ede94f --- /dev/null +++ b/Documentation/11.-Bench-Sitting-Legends-Z-A.md @@ -0,0 +1,13 @@ +# Legends: Z-A: Bench Sitting + +This guide covers bench sitting in Pokémon Legends: Z-A. This mode automates overworld spawns until a `StopCondition` is matched. + +## Setup + +1. Set `Overworld` -> `Mode` to `BenchSit` +2. Position your character as shown in the screenshot below: + ![Character positioned facing bench](assets/bench-sit-position.jpg) + +## Notes + +The settings `StopOnMaxShiniesStored` and `OverworldSpawnCheck` are only used when the `StopCondition` contains records with `ShinyTarget` set to `AnyShiny`, `StarOnly`, or `SquareOnly`. Since there is a separate place in memory where the shinies are stored (maximum of 10), the bot doesn't have to save after each round. diff --git a/Documentation/12.-Wild-Zone-Entrance-Legends-Z-A.md b/Documentation/12.-Wild-Zone-Entrance-Legends-Z-A.md new file mode 100644 index 00000000..f8332fb2 --- /dev/null +++ b/Documentation/12.-Wild-Zone-Entrance-Legends-Z-A.md @@ -0,0 +1,13 @@ +# Legends: Z-A: Wild Zone Entrance + +This guide covers Wild Zone Entrance in Pokémon Legends: Z-A. This mode automates overworld spawns until a `StopCondition` is matched. + +## Setup + +1. Set `Overworld` -> `Mode` to `WildZoneEntrance` +2. Position your character as shown in the screenshot below: + ![Character positioned facing the entrance](assets/wild-zone-entrance-position.jpg) + +## Notes + +The settings `StopOnMaxShiniesStored` and `OverworldSpawnCheck` are only used when the `StopCondition` contains records with `ShinyTarget` set to `AnyShiny`, `StarOnly`, or `SquareOnly`. Since there is a separate place in memory where the shinies are stored (maximum of 10), the bot doesn't have to save after each round. \ No newline at end of file diff --git a/Documentation/13.-Fossil-Revive-Legends-Z-A.md b/Documentation/13.-Fossil-Revive-Legends-Z-A.md new file mode 100644 index 00000000..dd17f14c --- /dev/null +++ b/Documentation/13.-Fossil-Revive-Legends-Z-A.md @@ -0,0 +1,9 @@ +# Legends: Z-A: Revive Fossils + +This guide covers reviving Fossils in Pokémon Legends: Z-A until a `StopCondition` is matched. + +## Setup + +1. Make sure to have atleast one of each species +2. Choose the Fossil species to revive or choose 'Any' if you want to revive all Fossils in your item bag. +3. Position your character at the scientist and start the bot diff --git a/Documentation/README.md b/Documentation/README.md index 263af2d9..82ced44f 100644 --- a/Documentation/README.md +++ b/Documentation/README.md @@ -11,10 +11,16 @@ Welcome to the Sysbot.NET wiki! ## Sword/Shield -1. [Encounter Calyrex and Spectrier/Glastrier](7.-Encounter-Calyrex-and-Spectrier-Glastrier.md) -2. [Unlimited egg mode](8.-Unlimited-egg-mode-Sword-Shield.md) -3. [(beta) Den/raid](9.-(beta)-Den-raid.md) +7. [Encounter Calyrex and Spectrier/Glastrier](7.-Encounter-Calyrex-and-Spectrier-Glastrier.md) +8. [Unlimited egg mode](8.-Unlimited-egg-mode-Sword-Shield.md) +9. [(beta) Den/raid](9.-(beta)-Den-raid.md) ## Brilliant Diamond/Shining Pearl -10. [Eggs (including Unlimited egg mode)](10.-Eggs-(including-Unlimited-egg-mode)-Brilliant-Diamond-Shining-Pearl.md) \ No newline at end of file +10. [Eggs (including Unlimited egg mode)](10.-Eggs-(including-Unlimited-egg-mode)-Brilliant-Diamond-Shining-Pearl.md) + +## Legends: Z-A + +11. [Bench Sitting](11.-Bench-Sitting-Legends-Z-A.md) +12. [Wild Zone Entrance](12.-Wild-Zone-Entrance-Legends-Z-A.md) +13. [Fossil reviving](13.-Fossil-Revive-Legends-Z-A.md) \ No newline at end of file diff --git a/Documentation/assets/bench-sit-position.jpg b/Documentation/assets/bench-sit-position.jpg new file mode 100644 index 00000000..bc7457b1 Binary files /dev/null and b/Documentation/assets/bench-sit-position.jpg differ diff --git a/Documentation/assets/wild-zone-entrance-position.jpg b/Documentation/assets/wild-zone-entrance-position.jpg new file mode 100644 index 00000000..d5f776cb Binary files /dev/null and b/Documentation/assets/wild-zone-entrance-position.jpg differ diff --git a/README.md b/README.md index a1231c69..777b1e19 100644 --- a/README.md +++ b/README.md @@ -12,6 +12,12 @@ If this project does not suit your needs, you are welcome to use an alternative. These routines are new compared to the original SysBot.NET. +#### Legends: Z-A + +- Bench Sit +- Wild Zone Entrance +- Fossil reviving + #### Scarlet/Violet - Encounters (Ruinous, Loyal Three, Gimmighoul, and static/new Paradox Pokémon in Area Zero) @@ -40,6 +46,12 @@ Currently, the [latest](https://github.com/Eppin/sys-botbase/releases) version i ## Version compatibility +### Legends: Z-A + +| Version | SysBot Release | +| :-----: | :---------------------------------------------------------------------------: | +| 1.0.1 | [xx.xx.xx.xxx](https://github.com/Eppin/Sysbot.NET/releases/tag/xx.xx.xx.xxx) | + ### Scarlet/Violet | Version | SysBot Release | Egg-mod Release | diff --git a/SysBot.Base/SysBot.Base.csproj b/SysBot.Base/SysBot.Base.csproj index 1ce77c9e..4f8dabcc 100644 --- a/SysBot.Base/SysBot.Base.csproj +++ b/SysBot.Base/SysBot.Base.csproj @@ -1,7 +1,7 @@  - + diff --git a/SysBot.Pokemon.ConsoleApp/Program.cs b/SysBot.Pokemon.ConsoleApp/Program.cs index bfffd5a5..a79fbd36 100644 --- a/SysBot.Pokemon.ConsoleApp/Program.cs +++ b/SysBot.Pokemon.ConsoleApp/Program.cs @@ -5,6 +5,7 @@ using System.IO; using System.Text.Json; using System.Text.Json.Serialization; +using SysBot.Pokemon.ZA; namespace SysBot.Pokemon.ConsoleApp; @@ -63,10 +64,6 @@ private static void ExitNoConfig() } } -[JsonSerializable(typeof(ProgramConfig))] -[JsonSourceGenerationOptions(WriteIndented = true, DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull)] -public sealed partial class ProgramConfigContext : JsonSerializerContext; - public static class BotContainer { public static void RunBots(ProgramConfig prog) @@ -114,6 +111,7 @@ private static void WaitForExit() ProgramMode.BDSP => new PokeBotRunnerImpl(prog.Hub, new BotFactory8BS()), ProgramMode.LA => new PokeBotRunnerImpl(prog.Hub, new BotFactory8LA()), ProgramMode.SV => new PokeBotRunnerImpl(prog.Hub, new BotFactory9SV()), + ProgramMode.ZA => new PokeBotRunnerImpl(prog.Hub, new BotFactory9LZA()), _ => throw new IndexOutOfRangeException("Unsupported mode."), }; diff --git a/SysBot.Pokemon.Discord/Commands/Management/EmbedModule.cs b/SysBot.Pokemon.Discord/Commands/Management/EmbedModule.cs index b4be40c1..5fec6052 100644 --- a/SysBot.Pokemon.Discord/Commands/Management/EmbedModule.cs +++ b/SysBot.Pokemon.Discord/Commands/Management/EmbedModule.cs @@ -129,18 +129,12 @@ private static (string Text, Embed? Embed) Embed(T? pkm, bool success) return pkm switch { null => ("(no valid data to embed)", null), - PK8 pk8 => EmbedPk(pk8, success), - PB8 pb8 => EmbedPk(pb8, success), - PK9 pk9 => EmbedPk(pk9, success), - _ => ("(unsupported embed)", null) + _ => EmbedPk(pkm, success) }; } - private static (string Text, Embed? Embed) EmbedPk(PKM pkm, bool success) + private static (string Text, Embed? Embed) EmbedPk(T pk, bool success) { - if (pkm is not T pk) - throw new Exception(); - var url = OutputExtensions.PokeImage(pk, false, false); var is1of100 = (Species)pk.Species is Species.Dunsparce or Species.Tandemaus; diff --git a/SysBot.Pokemon.Discord/Helpers/ReusableActions.cs b/SysBot.Pokemon.Discord/Helpers/ReusableActions.cs index 9c2a7c12..3e7f4f7a 100644 --- a/SysBot.Pokemon.Discord/Helpers/ReusableActions.cs +++ b/SysBot.Pokemon.Discord/Helpers/ReusableActions.cs @@ -1,4 +1,4 @@ -using Discord; +using Discord; using Discord.WebSocket; using PKHeX.Core; using SysBot.Base; @@ -14,7 +14,7 @@ public static class ReusableActions { public static async Task SendPKMAsync(this IMessageChannel channel, PKM pkm, string msg = "") { - var tmp = Path.Combine(Path.GetTempPath(), Util.CleanFileName(pkm.FileName)); + var tmp = Path.Combine(Path.GetTempPath(), PathUtil.CleanFileName(pkm.FileName)); File.WriteAllBytes(tmp, pkm.DecryptedPartyData); await channel.SendFileAsync(tmp, msg).ConfigureAwait(false); File.Delete(tmp); @@ -22,7 +22,7 @@ public static async Task SendPKMAsync(this IMessageChannel channel, PKM pkm, str public static async Task SendPKMAsync(this IUser user, PKM pkm, string msg = "") { - var tmp = Path.Combine(Path.GetTempPath(), Util.CleanFileName(pkm.FileName)); + var tmp = Path.Combine(Path.GetTempPath(), PathUtil.CleanFileName(pkm.FileName)); File.WriteAllBytes(tmp, pkm.DecryptedPartyData); await user.SendFileAsync(tmp, msg).ConfigureAwait(false); File.Delete(tmp); diff --git a/SysBot.Pokemon.Discord/SysBot.Pokemon.Discord.csproj b/SysBot.Pokemon.Discord/SysBot.Pokemon.Discord.csproj index d4a9125b..f009b5af 100644 --- a/SysBot.Pokemon.Discord/SysBot.Pokemon.Discord.csproj +++ b/SysBot.Pokemon.Discord/SysBot.Pokemon.Discord.csproj @@ -1,10 +1,10 @@  - - - - + + + + diff --git a/SysBot.Pokemon.Twitch/SysBot.Pokemon.Twitch.csproj b/SysBot.Pokemon.Twitch/SysBot.Pokemon.Twitch.csproj index 3c7bd68a..b0f85df3 100644 --- a/SysBot.Pokemon.Twitch/SysBot.Pokemon.Twitch.csproj +++ b/SysBot.Pokemon.Twitch/SysBot.Pokemon.Twitch.csproj @@ -1,8 +1,8 @@  - - + + diff --git a/SysBot.Pokemon.WinForms/InitUtil.cs b/SysBot.Pokemon.WinForms/InitUtil.cs index 38fd9275..9f9279e7 100644 --- a/SysBot.Pokemon.WinForms/InitUtil.cs +++ b/SysBot.Pokemon.WinForms/InitUtil.cs @@ -13,6 +13,7 @@ public static void InitializeStubs(ProgramMode mode) ProgramMode.BDSP => new SAV8BS(), ProgramMode.LA => new SAV8LA(), ProgramMode.SV => new SAV9SV(), + ProgramMode.ZA => new SAV9ZA(), _ => throw new System.ArgumentOutOfRangeException(nameof(mode)), }; diff --git a/SysBot.Pokemon.WinForms/Main.cs b/SysBot.Pokemon.WinForms/Main.cs index 57009946..40eae8f2 100644 --- a/SysBot.Pokemon.WinForms/Main.cs +++ b/SysBot.Pokemon.WinForms/Main.cs @@ -11,6 +11,7 @@ using System.Threading; using System.Threading.Tasks; using System.Windows.Forms; +using SysBot.Pokemon.ZA; namespace SysBot.Pokemon.WinForms; @@ -81,6 +82,7 @@ public Main() ProgramMode.BDSP => new PokeBotRunnerImpl(cfg.Hub, new BotFactory8BS()), ProgramMode.LA => new PokeBotRunnerImpl(cfg.Hub, new BotFactory8LA()), ProgramMode.SV => new PokeBotRunnerImpl(cfg.Hub, new BotFactory9SV()), + ProgramMode.ZA => new PokeBotRunnerImpl(cfg.Hub, new BotFactory9LZA()), _ => throw new IndexOutOfRangeException("Unsupported mode."), }; @@ -185,10 +187,6 @@ private void SaveCurrentConfig() File.WriteAllText(Program.ConfigPath, lines); } - [JsonSerializable(typeof(ProgramConfig))] - [JsonSourceGenerationOptions(WriteIndented = true, DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull)] - public sealed partial class ProgramConfigContext : JsonSerializerContext { } - private void B_Start_Click(object sender, EventArgs e) { SaveCurrentConfig(); diff --git a/SysBot.Pokemon.WinForms/SysBot.Pokemon.WinForms.csproj b/SysBot.Pokemon.WinForms/SysBot.Pokemon.WinForms.csproj index cf96539d..038aa414 100644 --- a/SysBot.Pokemon.WinForms/SysBot.Pokemon.WinForms.csproj +++ b/SysBot.Pokemon.WinForms/SysBot.Pokemon.WinForms.csproj @@ -17,8 +17,8 @@ - - + + diff --git a/SysBot.Pokemon.YouTube/SysBot.Pokemon.YouTube.csproj b/SysBot.Pokemon.YouTube/SysBot.Pokemon.YouTube.csproj index c80afc49..949a54c6 100644 --- a/SysBot.Pokemon.YouTube/SysBot.Pokemon.YouTube.csproj +++ b/SysBot.Pokemon.YouTube/SysBot.Pokemon.YouTube.csproj @@ -1,12 +1,12 @@ - - - - + + + + - + diff --git a/SysBot.Pokemon.Z3/SysBot.Pokemon.Z3.csproj b/SysBot.Pokemon.Z3/SysBot.Pokemon.Z3.csproj index d7d120c1..39f10fb8 100644 --- a/SysBot.Pokemon.Z3/SysBot.Pokemon.Z3.csproj +++ b/SysBot.Pokemon.Z3/SysBot.Pokemon.Z3.csproj @@ -1,7 +1,7 @@  - + diff --git a/SysBot.Pokemon/Actions/PokeRoutineExecutor.cs b/SysBot.Pokemon/Actions/PokeRoutineExecutor.cs index b955aa9a..beb29009 100644 --- a/SysBot.Pokemon/Actions/PokeRoutineExecutor.cs +++ b/SysBot.Pokemon/Actions/PokeRoutineExecutor.cs @@ -7,15 +7,13 @@ using System.Threading; using System.Threading.Tasks; using static SysBot.Base.SwitchButton; +using static System.Buffers.Binary.BinaryPrimitives; namespace SysBot.Pokemon; -public abstract class PokeRoutineExecutor : PokeRoutineExecutorBase where T : PKM, new() +public abstract class PokeRoutineExecutor(IConsoleBotManaged cfg) + : PokeRoutineExecutorBase(cfg) where T : PKM, new() { - protected PokeRoutineExecutor(IConsoleBotManaged cfg) : base(cfg) - { - } - public abstract Task ReadPokemon(ulong offset, CancellationToken token); public abstract Task ReadPokemon(ulong offset, int size, CancellationToken token); public abstract Task ReadPokemonPointer(IEnumerable jumps, int size, CancellationToken token); @@ -115,6 +113,7 @@ public void DumpPokemon(string folder, string subfolder, PKM pk, byte[] bytes) { PK8 pk8 => StopConditionSettings.HasMark(pk8, out var mark) ? $"{mark.ToString().Replace("Mark", "")}Mark - " : "", PK9 pk9 => StopConditionSettings.HasMark(pk9, out var mark) ? $"{mark.ToString().Replace("Mark", "")}Mark - " : "", + PA9 pa9 => StopConditionSettings.HasMark(pa9, out var mark) ? $"{mark.ToString().Replace("Mark", "")}Mark - " : "", _ => markType }; @@ -128,6 +127,8 @@ public void DumpPokemon(string folder, string subfolder, PKM pk, byte[] bytes) filetype += "pa8"; if (pk is PK9) filetype += "pk9"; + if (pk is PA9) + filetype += "pa9"; if (!Directory.Exists(folder)) { @@ -342,4 +343,30 @@ private async Task BlockUser(CancellationToken token) await Click(A, 1_100, token).ConfigureAwait(false); await Click(A, 1_100, token).ConfigureAwait(false); } + + public async Task ReadEncryptedBlock(ulong keyAddress, uint blockKey, CancellationToken token) + { + const int warningSize = 1_024 * 10; // 10 KB + + var header = await SwitchConnection.ReadBytesAbsoluteAsync(keyAddress, 5, token).ConfigureAwait(false); + header = DecryptBlock(blockKey, header); + + var size = ReadUInt32LittleEndian(header.AsSpan()[1..]); + + if (size > warningSize) Log($"Retrieving {size/1024} KB (this may take a while, using WiFi, use USB if available)"); + + var data = await SwitchConnection.ReadBytesAbsoluteAsync(keyAddress, 5 + (int)size, token).ConfigureAwait(false); + var res = DecryptBlock(blockKey, data)[5..]; + + return res; + } + + public static byte[] DecryptBlock(uint key, byte[] block) + { + var rng = new SCXorShift32(key); + for (var i = 0; i < block.Length; i++) + block[i] = (byte)(block[i] ^ rng.Next()); + + return block; + } } diff --git a/SysBot.Pokemon/BDSP/BotEncounter/EncounterBotBS.cs b/SysBot.Pokemon/BDSP/BotEncounter/EncounterBotBS.cs index 1e631961..86f3cf27 100644 --- a/SysBot.Pokemon/BDSP/BotEncounter/EncounterBotBS.cs +++ b/SysBot.Pokemon/BDSP/BotEncounter/EncounterBotBS.cs @@ -2,6 +2,7 @@ namespace SysBot.Pokemon; using System; using System.IO; +using System.Linq; using System.Threading; using System.Threading.Tasks; using Base; @@ -95,7 +96,7 @@ public override async Task HardStop() if (!StopConditionSettings.EncounterFound(pk, Hub.Config.StopConditions, null)) { - if (folder.Equals("egg") && Hub.Config.StopConditions.ShinyTarget is TargetShinyType.AnyShiny or TargetShinyType.StarOnly or TargetShinyType.SquareOnly && pk.IsShiny) + if (folder.Equals("egg") && Hub.Config.StopConditions.SearchConditions.Any(sc => sc.IsEnabled && pk.IsShiny && sc.ShinyTarget is TargetShinyType.AnyShiny or TargetShinyType.StarOnly or TargetShinyType.SquareOnly)) Hub.LogEmbed(pk, false); return (false, false); diff --git a/SysBot.Pokemon/Helpers/AutoLegalityWrapper.cs b/SysBot.Pokemon/Helpers/AutoLegalityWrapper.cs index c6161016..5c42f022 100644 --- a/SysBot.Pokemon/Helpers/AutoLegalityWrapper.cs +++ b/SysBot.Pokemon/Helpers/AutoLegalityWrapper.cs @@ -1,9 +1,10 @@ -using PKHeX.Core; -using PKHeX.Core.AutoMod; using System; +using System.Collections.Generic; using System.IO; using System.Linq; using System.Threading; +using PKHeX.Core; +using PKHeX.Core.AutoMod; namespace SysBot.Pokemon; @@ -21,7 +22,6 @@ public static void EnsureInitialized(LegalitySettings cfg) private static void InitializeAutoLegality(LegalitySettings cfg) { - InitializeCoreStrings(); EncounterEvent.RefreshMGDB(cfg.MGDBPath); InitializeTrainerDatabase(cfg); InitializeSettings(cfg); @@ -39,8 +39,8 @@ private static void InitializeSettings(LegalitySettings cfg) Legalizer.EnableEasterEggs = cfg.EnableEasterEggs; APILegality.AllowTrainerOverride = cfg.AllowTrainerDataOverride; APILegality.AllowBatchCommands = cfg.AllowBatchCommands; - APILegality.PrioritizeGame = cfg.PrioritizeGame; - APILegality.PrioritizeGameVersion = cfg.PrioritizeGameVersion; + APILegality.GameVersionPriority = cfg.GameVersionPriority; + cfg.PriorityOrder = APILegality.PriorityOrder = SanitizePriorityOrder(cfg.PriorityOrder); // Clean this up because user can add duplicate or invalid entries. APILegality.SetBattleVersion = cfg.SetBattleVersion; APILegality.Timeout = cfg.Timeout; @@ -60,6 +60,20 @@ private static void InitializeSettings(LegalitySettings cfg) EncounterMovesetGenerator.PriorityList = cfg.PrioritizeEncounters; } + private static List SanitizePriorityOrder(List versionList) + { + var validVersions = Enum.GetValues().Where(GameUtil.IsValidSavedVersion).Reverse().ToList(); + + foreach (var ver in validVersions) + { + if (!versionList.Contains(ver)) + versionList.Add(ver); // Add any missing versions. + } + + // Remove any versions in versionList that are not in validVersions and clean up duplicates in the process. + return [.. versionList.Intersect(validVersions)]; + } + private static void InitializeTrainerDatabase(LegalitySettings cfg) { var externalSource = cfg.GeneratePathTrainerInfo; @@ -102,20 +116,11 @@ private static void RegisterIfNoneExist(SimpleTrainerInfo fallback, byte generat OT = fallback.OT, Generation = generation, }; - var exist = TrainerSettings.GetSavedTrainerData(version, generation, fallback); + var exist = TrainerSettings.GetSavedTrainerData(generation, version, fallback); if (exist is SimpleTrainerInfo) // not anything from files; this assumes ALM returns SimpleTrainerInfo for non-user-provided fake templates. TrainerSettings.Register(fallback); } - private static void InitializeCoreStrings() - { - var lang = Thread.CurrentThread.CurrentCulture.TwoLetterISOLanguageName[..2]; - LocalizationUtil.SetLocalization(typeof(LegalityCheckStrings), lang); - LocalizationUtil.SetLocalization(typeof(MessageStrings), lang); - RibbonStrings.ResetDictionary(GameInfo.Strings.ribbons); - ParseSettings.ChangeLocalizationStrings(GameInfo.Strings.movelist, GameInfo.Strings.specieslist); - } - public static bool CanBeTraded(this PKM pkm) { if (pkm.IsNicknamed && StringsUtil.IsSpammyString(pkm.Nickname)) @@ -144,13 +149,15 @@ public static bool CanBeTraded(this PKM pkm) public static ITrainerInfo GetTrainerInfo() where T : PKM, new() { if (typeof(T) == typeof(PK8)) - return TrainerSettings.GetSavedTrainerData(GameVersion.SWSH, 8); + return TrainerSettings.GetSavedTrainerData(GameVersion.SWSH); if (typeof(T) == typeof(PB8)) - return TrainerSettings.GetSavedTrainerData(GameVersion.BDSP, 8); + return TrainerSettings.GetSavedTrainerData(GameVersion.BDSP); if (typeof(T) == typeof(PA8)) - return TrainerSettings.GetSavedTrainerData(GameVersion.PLA, 8); + return TrainerSettings.GetSavedTrainerData(GameVersion.PLA); if (typeof(T) == typeof(PK9)) - return TrainerSettings.GetSavedTrainerData(GameVersion.SV, 9); + return TrainerSettings.GetSavedTrainerData(GameVersion.SV); + if (typeof(T) == typeof(PA9)) + return TrainerSettings.GetSavedTrainerData(GameVersion.ZA); throw new ArgumentException("Type does not have a recognized trainer fetch.", typeof(T).Name); } diff --git a/SysBot.Pokemon/Helpers/OutputExtensions.cs b/SysBot.Pokemon/Helpers/OutputExtensions.cs index f2b19864..3b1855c9 100644 --- a/SysBot.Pokemon/Helpers/OutputExtensions.cs +++ b/SysBot.Pokemon/Helpers/OutputExtensions.cs @@ -80,8 +80,11 @@ public static void EncounterLogs(PKM pk, string filepath = "") } } - public static void EncounterScaleLogs(PK9 pk, string filepath = "") + public static void EncounterScaleLogs(PKM pk, string filepath = "") { + if (pk is not IScaledSize3 scaledSize3) + return; + if (filepath == "") filepath = "EncounterScaleLogPretty.txt"; @@ -97,9 +100,9 @@ public static void EncounterScaleLogs(PK9 pk, string filepath = "") var splitTotal = content[0].Split(','); content.RemoveRange(0, 3); - var isMini = pk.Scale == 0; - var isJumbo = pk.Scale == 255; - var isMisc = pk.Scale is > 0 and < 255; + var isMini = scaledSize3.Scale == 0; + var isJumbo = scaledSize3.Scale == 255; + var isMisc = scaledSize3.Scale is > 0 and < 255; var pokeTotal = int.Parse(splitTotal[0].Split(' ')[1]) + 1; var miniTotal = int.Parse(splitTotal[1].Split(' ')[1]) + (isMini ? 1 : 0); var jumboTotal = int.Parse(splitTotal[2].Split(' ')[1]) + (isJumbo ? 1 : 0); diff --git a/SysBot.Pokemon/LZA/BotEncounter/EncounterBotFossil/EncounterBotFossilLZA.cs b/SysBot.Pokemon/LZA/BotEncounter/EncounterBotFossil/EncounterBotFossilLZA.cs new file mode 100644 index 00000000..dee7f905 --- /dev/null +++ b/SysBot.Pokemon/LZA/BotEncounter/EncounterBotFossil/EncounterBotFossilLZA.cs @@ -0,0 +1,134 @@ +namespace SysBot.Pokemon; + +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using PKHeX.Core; +using static Base.SwitchButton; +using static PokeDataOffsetsLZA; + +public class EncounterBotFossilLZA(PokeBotState cfg, PokeTradeHub hub) : EncounterBotLZA(cfg, hub) +{ + private new readonly FossilSettingsLZA Settings = hub.Config.EncounterLZA.Fossil; + + private bool _itemKeyInitialized; + private byte _box; + private byte _slot; + + protected override async Task EncounterLoop(SAV9ZA sav, CancellationToken token) + { + Log("Make sure the first box is selected and there's enough free space!"); + + Log("Checking item counts..."); + var pouchData = await GetPouchData(token).ConfigureAwait(false); + var counts = FossilCountLZA.GetFossilCounts(pouchData); + + var reviveCount = counts.PossibleRevives(Settings.Species); + if (reviveCount == 0) + { + Log("Insufficient fossil pieces. Please obtain at least one of each required fossil piece first."); + return; + } + + Log($"Enough fossil pieces are available to revive {reviveCount} {(Settings.Species is FossilSpeciesLZA.Any ? "fossils" : Settings.Species)}."); + + PA9? prev = null; + while (!token.IsCancellationRequested) + { + if (EncounterCount != 0 && EncounterCount % reviveCount == 0) + { + Log("Fossil pieces have been depleted. Resetting the game."); + _box = _slot = 0; + await CloseGame(Hub.Config, token).ConfigureAwait(false); + await StartGame(Hub.Config, token).ConfigureAwait(false); + } + + await ReviveFossil(token).ConfigureAwait(false); + Log("Fossil revived. Checking details..."); + + var (pa9, raw) = await ReadRawBoxPokemon(_box, _slot, token).ConfigureAwait(false); + if (pa9.Species == 0 || !pa9.ChecksumValid || pa9.EncryptionConstant == prev?.EncryptionConstant) + { + Log($"No fossil found in Box {_box + 1}, slot {_slot + 1}. Ensure that the party is full. Restarting loop."); + continue; + } + + if (new[] { (int)Species.Aerodactyl, (int)Species.Tyrunt, (int)Species.Amaura }.Contains(pa9.Species) == false) + { + Log($"Fossil revival appears to have failed, found {(Species)pa9.Species}."); + return; + } + + var (stop, success) = await HandleEncounter(pa9, token, raw).ConfigureAwait(false); + + if (success) Log($"You're fossil has been claimed and placed in B{_box + 1}S{_slot + 1}. Be sure to save your game!"); + + _slot += 1; + if (_slot == 30) + { + _box++; + _slot = 0; + } + + if (stop) + return; + + prev = pa9; + } + } + + private async Task GetPouchData(CancellationToken token) + { + _itemKeyInitialized = false; + var bytes = await ReadEncryptedBlock(Offsets.KItemPointer, KItemKey, !_itemKeyInitialized, token).ConfigureAwait(false); + _itemKeyInitialized = true; + + return bytes; + } + + private async Task ReviveFossil(CancellationToken token) + { + Log("Starting fossil revival routine..."); + + if (Settings.Species == FossilSpeciesLZA.Any) + { + // Just mash the buttons through the menus if any fossil is acceptable. + for (var i = 0; i < 14; i++) + await Click(A, 0_500, token).ConfigureAwait(false); + + await Task.Delay(3_000, token).ConfigureAwait(false); + + for (var i = 0; i < 16; i++) + await Click(B, 0_500, token).ConfigureAwait(false); + + return; + } + + for (var i = 0; i < 4; i++) + await Click(A, 1_100, token).ConfigureAwait(false); + + switch (Settings.Species) + { + // Selecting second fossil. + case FossilSpeciesLZA.Amaura: + await Click(DDOWN, 0_300, token).ConfigureAwait(false); + break; + + // Selecting third fossil. + case FossilSpeciesLZA.Aerodactyl: + { + for (var i = 0; i < 2; i++) await Click(DDOWN, 0_300, token).ConfigureAwait(false); + break; + } + } + + // A spam through accepting the fossil and agreeing to revive. + for (var i = 0; i < 6; i++) + await Click(A, 0_500, token).ConfigureAwait(false); + + await Task.Delay(3_000, token).ConfigureAwait(false); + + for (var i = 0; i < 16; i++) + await Click(B, 0_500, token).ConfigureAwait(false); + } +} diff --git a/SysBot.Pokemon/LZA/BotEncounter/EncounterBotFossil/FossilCountLZA.cs b/SysBot.Pokemon/LZA/BotEncounter/EncounterBotFossil/FossilCountLZA.cs new file mode 100644 index 00000000..ca324061 --- /dev/null +++ b/SysBot.Pokemon/LZA/BotEncounter/EncounterBotFossil/FossilCountLZA.cs @@ -0,0 +1,59 @@ +namespace SysBot.Pokemon; + +using System; +using PKHeX.Core; + +public class FossilCountLZA +{ + private int Jaw; + private int Sail; + private int OldAmber; + + private void SetCount(int item, int count) + { + switch (item) + { + case 710: Jaw = count; break; + case 711: Sail = count; break; + case 103: OldAmber = count; break; + } + } + + public static FossilCountLZA GetFossilCounts(byte[] itemsBlock) + { + var pouch = GetTreasurePouch(itemsBlock); + return ReadCounts(pouch); + } + + private static FossilCountLZA ReadCounts(InventoryPouch pouch) + { + var counts = new FossilCountLZA(); + foreach (var item in pouch.Items) + counts.SetCount(item.Index, item.Count); + return counts; + } + + private static InventoryPouch9a GetTreasurePouch(byte[] itemsBlock) + { + var pouch = new InventoryPouch9a(InventoryType.Items, ItemStorage9ZA.Instance, 999, 0); + pouch.GetPouch(itemsBlock); + return pouch; + } + + public int PossibleRevives(FossilSpeciesLZA species) + { + if (species == FossilSpeciesLZA.Any) return Jaw + Sail + OldAmber; + + // Requirement: at least one of each fossil must be present to perform any revives. + if (Jaw <= 0 || Sail <= 0 || OldAmber <= 0) + return 0; + + return species switch + { + FossilSpeciesLZA.Tyrunt => Jaw, + FossilSpeciesLZA.Amaura => Sail, + FossilSpeciesLZA.Aerodactyl => OldAmber, + _ => throw new ArgumentOutOfRangeException(nameof(species), species, "Fossil species was invalid."), + }; + } +} diff --git a/SysBot.Pokemon/LZA/BotEncounter/EncounterBotFossil/FossilSettingsLZA.cs b/SysBot.Pokemon/LZA/BotEncounter/EncounterBotFossil/FossilSettingsLZA.cs new file mode 100644 index 00000000..6da150c1 --- /dev/null +++ b/SysBot.Pokemon/LZA/BotEncounter/EncounterBotFossil/FossilSettingsLZA.cs @@ -0,0 +1,12 @@ +using System.ComponentModel; + +namespace SysBot.Pokemon; + +public class FossilSettingsLZA +{ + private const string Fossil = nameof(Fossil); + public override string ToString() => "Fossil Bot Settings"; + + [Category(Fossil), Description("Species of fossil Pokémon to hunt for.")] + public FossilSpeciesLZA Species { get; set; } = FossilSpeciesLZA.Any; +} diff --git a/SysBot.Pokemon/LZA/BotEncounter/EncounterBotFossil/FossilSpeciesLZA.cs b/SysBot.Pokemon/LZA/BotEncounter/EncounterBotFossil/FossilSpeciesLZA.cs new file mode 100644 index 00000000..46d60824 --- /dev/null +++ b/SysBot.Pokemon/LZA/BotEncounter/EncounterBotFossil/FossilSpeciesLZA.cs @@ -0,0 +1,24 @@ +namespace SysBot.Pokemon; + +public enum FossilSpeciesLZA +{ + /// + /// Bot will revive any fossil species + /// + Any, + + /// + /// Bot will revive Tyrunt + /// + Tyrunt, + + /// + /// Bot will revive Amaura + /// + Amaura, + + /// + /// Bot will revive Aerodactyl + /// + Aerodactyl, +} diff --git a/SysBot.Pokemon/LZA/BotEncounter/EncounterBotLZA.cs b/SysBot.Pokemon/LZA/BotEncounter/EncounterBotLZA.cs new file mode 100644 index 00000000..0cce5bb0 --- /dev/null +++ b/SysBot.Pokemon/LZA/BotEncounter/EncounterBotLZA.cs @@ -0,0 +1,193 @@ +namespace SysBot.Pokemon; + +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using Base; +using PKHeX.Core; +using ZA; +using static Base.SwitchButton; +using static Base.SwitchStick; + +public abstract class EncounterBotLZA : PokeRoutineExecutor9LZA, IEncounterBot +{ + protected readonly PokeTradeHub Hub; + protected readonly EncounterSettingsLZA Settings; + protected readonly IDumper DumpSetting; + + public ICountSettings Counts => Settings; + public readonly IReadOnlyList UnwantedMarks; + + protected EncounterBotLZA(PokeBotState cfg, PokeTradeHub hub) : base(cfg) + { + Hub = hub; + Settings = Hub.Config.EncounterLZA; + DumpSetting = Hub.Config.Folder; + StopConditionSettings.ReadUnwantedMarks(Hub.Config.StopConditions, out UnwantedMarks); + } + + protected int EncounterCount; + + public override async Task MainLoop(CancellationToken token) + { + var settings = Hub.Config.EncounterSV; + Log("Identifying trainer data of the host console."); + var sav = await IdentifyTrainer(token).ConfigureAwait(false); + await InitializeHardware(settings, token).ConfigureAwait(false); + + try + { + Log($"Starting main {GetType().Name} loop."); + Config.IterateNextRoutine(); + + // Clear out any residual stick weirdness. + await ResetStick(token).ConfigureAwait(false); + await EncounterLoop(sav, token).ConfigureAwait(false); + } + catch (Exception e) + { + Log(e.Message); + } + + Log($"Ending {GetType().Name} loop."); + await HardStop().ConfigureAwait(false); + } + + public override async Task HardStop() + { + await ResetStick(CancellationToken.None).ConfigureAwait(false); + await CleanExit(CancellationToken.None).ConfigureAwait(false); + } + + protected abstract Task EncounterLoop(SAV9ZA sav, CancellationToken token); + + // Return true if breaking loop + protected async Task<(bool Stop, bool Success)> HandleEncounter(PA9 pk, CancellationToken token, byte[]? raw = null, bool minimize = false, bool skipDump = false) + { + EncounterCount++; + var print = Hub.Config.StopConditions.GetPrintName(pk); + Log($"Encounter: {EncounterCount}"); + + if (!string.IsNullOrWhiteSpace(print)) + Log($"{print}{Environment.NewLine}", !minimize); + + var folder = IncrementAndGetDumpFolder(pk); + + if (!skipDump && pk.Valid) + { + switch (DumpSetting) + { + case { Dump: true, DumpShinyOnly: true } when pk.IsShiny: + case { Dump: true, DumpShinyOnly: false }: + DumpPokemon(DumpSetting.DumpFolder, folder, pk); + break; + } + + if (raw != null) + { + switch (DumpSetting) + { + case { DumpRaw: true, DumpShinyOnly: true } when pk.IsShiny: + case { DumpRaw: true, DumpShinyOnly: false }: + DumpPokemon(DumpSetting.DumpFolder, folder, pk, raw); + break; + } + } + } + + if (!StopConditionSettings.EncounterFound(pk, Hub.Config.StopConditions, UnwantedMarks)) + { + if (Hub.Config.StopConditions.SearchConditions.Any(sc => sc.IsEnabled && pk.IsShiny && sc.ShinyTarget is TargetShinyType.AnyShiny or TargetShinyType.StarOnly or TargetShinyType.SquareOnly)) + Hub.LogEmbed(pk, false); + + return (false, false); + } + + if (Settings.MinMaxScaleOnly && pk.Scale is > 0 and < 255) + { + Hub.LogEmbed(pk, false); + return (false, false); + } + + if (Hub.Config.StopConditions.CaptureVideoClip) + { + await Task.Delay(Hub.Config.StopConditions.ExtraTimeWaitCaptureVideo, token).ConfigureAwait(false); + await PressAndHold(CAPTURE, 2_000, 0, token).ConfigureAwait(false); + } + + var mode = Settings.ContinueAfterMatch; + var msg = $"Result found!\n{print}\n" + mode switch + { + ContinueAfterMatch.Continue => "Continuing...", + ContinueAfterMatch.PauseWaitAcknowledge => "Waiting for instructions to continue.", + ContinueAfterMatch.StopExit => "Stopping routine execution; restart the bot to search again.", + _ => throw new ArgumentOutOfRangeException("Match result type was invalid.", nameof(ContinueAfterMatch)) + }; + + if (!string.IsNullOrWhiteSpace(Hub.Config.StopConditions.MatchFoundEchoMention)) + msg = $"{Hub.Config.StopConditions.MatchFoundEchoMention} {msg}"; + EchoUtil.Echo(msg); + Hub.LogEmbed(pk, true); + + if (mode == ContinueAfterMatch.StopExit) + return (true, true); + if (mode == ContinueAfterMatch.Continue) + return (false, true); + + _isWaiting = true; + while (_isWaiting) + await Task.Delay(1_000, token).ConfigureAwait(false); + + return (false, true); + } + + private string IncrementAndGetDumpFolder(PA9 pk) + { + try + { + var loggingFolder = string.IsNullOrWhiteSpace(Hub.Config.LoggingFolder) + ? string.Empty + : Hub.Config.LoggingFolder; + + var legendary = SpeciesCategory.IsLegendary(pk.Species) || SpeciesCategory.IsMythical(pk.Species) || SpeciesCategory.IsSubLegendary(pk.Species); + if (legendary) + { + Settings.AddCompletedLegends(); + OutputExtensions.EncounterLogs(pk, Path.Combine(loggingFolder, "EncounterLogPretty_LegendZA.txt")); + OutputExtensions.EncounterScaleLogs(pk, Path.Combine(loggingFolder, "EncounterLogScale_LegendZA.txt")); + return "legends"; + } + + if (new[] { (int)Species.Aerodactyl, (int)Species.Tyrunt, (int)Species.Amaura }.Contains(pk.Species)) + { + Settings.AddCompletedFossils(); + OutputExtensions.EncounterLogs(pk, Path.Combine(loggingFolder, "EncounterLogPretty_FosilZA.txt")); + OutputExtensions.EncounterScaleLogs(pk, Path.Combine(loggingFolder, "EncounterLogScale_FosilZA.txt")); + + return "fossil"; + } + + Settings.AddCompletedEncounters(); + OutputExtensions.EncounterLogs(pk, Path.Combine(loggingFolder, "EncounterLogPretty_EncounterZA.txt")); + OutputExtensions.EncounterScaleLogs(pk, Path.Combine(loggingFolder, "EncounterLogScale_EncounterZA.txt")); + return "encounters"; + } + catch (Exception e) + { + Log($"Couldn't update encounters:\n{e.Message}\n{e.StackTrace}"); + return "random"; + } + } + + private bool _isWaiting; + public void Acknowledge() => _isWaiting = false; + + protected async Task ResetStick(CancellationToken token) + { + // If aborting the sequence, we might have the stick set at some position. Clear it just in case. + await SetStick(LEFT, 0, 0, 0_500, token).ConfigureAwait(false); // reset + } +} diff --git a/SysBot.Pokemon/LZA/BotEncounter/EncounterBotOverworldScannerLZA.cs b/SysBot.Pokemon/LZA/BotEncounter/EncounterBotOverworldScannerLZA.cs new file mode 100644 index 00000000..5be3831d --- /dev/null +++ b/SysBot.Pokemon/LZA/BotEncounter/EncounterBotOverworldScannerLZA.cs @@ -0,0 +1,249 @@ +namespace SysBot.Pokemon; + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using PKHeX.Core; +using static PokeDataOffsetsLZA; +using static Base.SwitchButton; +using static Base.SwitchStick; + +public class EncounterBotOverworldScannerLZA(PokeBotState cfg, PokeTradeHub hub) : EncounterBotLZA(cfg, hub) +{ + private bool _overworldKeyInitialized; + private bool _shinyEntityKeyInitialized; + + private ulong _speciesCount; + private ulong _actionCount; + + private readonly List _previous = []; + protected override async Task EncounterLoop(SAV9ZA sav, CancellationToken token) + { + _speciesCount = _actionCount = 0; + _overworldKeyInitialized = _shinyEntityKeyInitialized = false; + _previous.Clear(); + + while (!token.IsCancellationRequested) + { + var task = Settings.Overworld.Mode switch + { + EncounterSettingsLZA.OverworldModeLZA.BenchSit => BenchSit(token), + EncounterSettingsLZA.OverworldModeLZA.WildZoneEntrance => WildZoneEntrance(token), + _ => throw new ArgumentOutOfRangeException() + }; + await task.ConfigureAwait(false); + + await WalkInOverworld(token).ConfigureAwait(false); + + if (await PerformOverworldScan(token).ConfigureAwait(false)) + return; + } + } + + private async Task WalkInOverworld(CancellationToken token) + { + var walk = Settings.Overworld.WalkDurationMs; + if (walk > 0) + { + Log($"Walking forward for {walk} milliseconds.", false); + await Run(0, short.MaxValue, walk, token).ConfigureAwait(false); + + Log($"Walking back for {walk} milliseconds.", false); + await Run(0, short.MinValue, walk, token).ConfigureAwait(false); + } + } + + private async Task Run(short x, short y, int walk, CancellationToken token) + { + const int defaultDelay = 0_100; + + await SetStick(LEFT, x, y, defaultDelay, token).ConfigureAwait(false); + + // Only press B if the configured walk duration is large enough to fit both stick movement and the B press + const int clickDelay = defaultDelay * 2; + if (walk > clickDelay) + await Click(B, defaultDelay, token).ConfigureAwait(false); + + await Task.Delay(walk, token).ConfigureAwait(false); + await SetStick(LEFT, 0, 0, 0_500, token).ConfigureAwait(false); + } + + private Task PerformOverworldScan(CancellationToken token) + { + // Determine if slow mode is needed based on shiny search conditions + // When searching for non-shiny or disabling shiny options, slow mode is required (because only a max. of 10 shinies are stored in a separate block) + var useSlowMode = Hub.Config.StopConditions.SearchConditions.Any(sc => sc is { IsEnabled: true, ShinyTarget: TargetShinyType.DisableOption or TargetShinyType.NonShiny }); + + if (useSlowMode) + { + Log("Using the slower, save and full scan, mode", false); + return DoSlowOverworldScanning(token); + } + + Log("Using the faster, shiny-only scan, mode", false); + return DoShinyOverworldScanning(token); + } + + private async Task DoSlowOverworldScanning(CancellationToken token) + { + //await Bench(token).ConfigureAwait(false); + await SaveGame(token).ConfigureAwait(false); + Log("Scanning overworld..."); + + await Click(HOME, 0, token).ConfigureAwait(false); + var results = await GetAllOverworld(token); + + if (await HandleEncounters(results, token)) return true; + + await Click(HOME, 0_500, token).ConfigureAwait(false); + Log($"Resuming, species found: {_speciesCount}"); + + return false; + } + + private async Task DoShinyOverworldScanning(CancellationToken token) + { + // Overworld spawn check disabled + var overworld = Settings.Overworld; + if (overworld.OverworldSpawnCheck == 0) + return false; + + // Not the time to check yet + if (_actionCount % (ulong)overworld.OverworldSpawnCheck != 0) + return false; + + await SaveGame(token).ConfigureAwait(false); + Log("Scanning overworld..."); + + await Click(HOME, 0, token).ConfigureAwait(false); + var results = await GetShinyOverworld(token); + + if (await HandleEncounters(results, token)) return true; + + if (overworld.StopOnMaxShiniesStored && results.Count >= 10) + { + Log("Maximum number of shinies stored in overworld block reached, stopping bot."); + return true; + } + + await Click(HOME, 0_500, token).ConfigureAwait(false); + Log($"Resuming, species found: {_speciesCount}"); + + return false; + } + + private async Task BenchSit(CancellationToken token) + { + Log("Moving towards the bench", false); + + await SetStick(LEFT, 0, -30000, 1_000, token).ConfigureAwait(false); + await SetStick(LEFT, 0, 0, 0_500, token).ConfigureAwait(false); + + var later = DateTime.Now.AddSeconds(27); + Log($"Repeatedly pressing 'A' until [{later}]", false); + while (DateTime.Now <= later) + await Click(A, 0_200, token); + + _actionCount++; + } + + private async Task WildZoneEntrance(CancellationToken token) + { + Log("Moving towards the entrance", false); + await SetStick(LEFT, 0, -30000, 1_000, token).ConfigureAwait(false); + await SetStick(LEFT, 0, 0, 0_500, token).ConfigureAwait(false); + + var later = DateTime.Now.AddSeconds(3); + Log("Pass entrance", false); + while (DateTime.Now <= later) + await Click(A, 0_200, token); + + Log("Moving towards the entrance, again", false); + await SetStick(LEFT, 0, -30000, 1_000, token).ConfigureAwait(false); + await SetStick(LEFT, 0, 0, 0_500, token).ConfigureAwait(false); + + later = DateTime.Now.AddSeconds(3); + Log("Pass entrance, again", false); + while (DateTime.Now <= later) + await Click(A, 0_200, token); + + _actionCount++; + } + + private async Task HandleEncounters(List results, CancellationToken token) + { + foreach (var current in results) + { + if (_previous.Any(p => p.Species == current.Species && p.EncryptionConstant == current.EncryptionConstant && p.PID == current.PID)) + continue; + + var (stop, success) = await HandleEncounter(current, token, minimize: true, skipDump: true).ConfigureAwait(false); + _speciesCount++; + + if (success) + Log("Your Pokémon has been found in the overworld!"); + + if (stop) + return true; + } + + _previous.Clear(); + _previous.AddRange(results); + + return false; + } + + private async Task> GetAllOverworld(CancellationToken token) + { + var bytes = (await ReadEncryptedBlock(Offsets.KOverworldPointer, KOverworldKey, !_overworldKeyInitialized, token).ConfigureAwait(false)).AsSpan(); + + // Only need to initialize once + _overworldKeyInitialized = true; + + var list = new List(); + + // Really hacky way to scan for Pokémon in the overworld block + // just slide over every possible offset and see if a valid PKM is found + for (var i = 0; i < bytes.Length - FormatSlotSize; i++) + { + var entry = bytes.Slice(i, FormatSlotSize); + + if (!EntityDetection.IsPresent(entry)) continue; + + var pa9 = new PA9(entry.ToArray()); + if (!pa9.Valid || pa9.Species <= 0 || pa9.Checksum == 0 || !PersonalTable.ZA.IsSpeciesInGame(pa9.Species)) continue; + + list.Add(pa9); + } + + return list; + } + + private async Task> GetShinyOverworld(CancellationToken token) + { + const int size = 0x1F0; + + var bytes = (await ReadEncryptedBlock(Offsets.KStoredShinyEntityPointer, KStoredShinyEntityKey, !_shinyEntityKeyInitialized, token).ConfigureAwait(false)).AsSpan(); + + // Only need to initialize once + _shinyEntityKeyInitialized = true; + + var list = new List(); + for (var i = 0; i < 10; i++) + { + var ofs = i * size + 8; + var entry = bytes.Slice(ofs, FormatSlotSize); + if (EntityDetection.IsPresent(entry)) + { + var pa9 = new PA9(entry.ToArray()); + list.Add(pa9); + } + else + break; + } + + return list; + } +} diff --git a/SysBot.Pokemon/LZA/BotEncounter/EncounterSettingsLZA.cs b/SysBot.Pokemon/LZA/BotEncounter/EncounterSettingsLZA.cs new file mode 100644 index 00000000..25c0536a --- /dev/null +++ b/SysBot.Pokemon/LZA/BotEncounter/EncounterSettingsLZA.cs @@ -0,0 +1,99 @@ +using System.Collections.Generic; +using System.ComponentModel; +using System.Threading; +using SysBot.Base; + +namespace SysBot.Pokemon; + +public class EncounterSettingsLZA : IBotStateSettings, ICountSettings +{ + private const string Counts = nameof(Counts); + private const string Encounter = nameof(Encounter); + private const string Settings = nameof(Settings); + public override string ToString() => "Encounter Bot ZA Settings"; + + [Category(Encounter), Description("When enabled, the bot will continue after finding a suitable match.")] + public ContinueAfterMatch ContinueAfterMatch { get; set; } = ContinueAfterMatch.StopExit; + + [Category(Settings)] + [TypeConverter(typeof(ExpandableObjectConverter))] + public FossilSettingsLZA Fossil { get; set; } = new(); + + [Category(Settings)] + [TypeConverter(typeof(ExpandableObjectConverter))] + public OverworldEncounterLZA Overworld { get; set; } = new(); + + [Category(Encounter)] + public class OverworldEncounterLZA + { + public override string ToString() => "Overworld Bot Settings"; + + [Category(Encounter), Description("Which mode is used to find the target in the overworld.")] + public OverworldModeLZA Mode { get; set; } + + [Category(Encounter), Description("Stop when maximum (10) shinies are stored (applicable when ONLY searching for shinies)")] + public bool StopOnMaxShiniesStored { get; set; } = true; + + [Category(Encounter), Description("Check overworld after amount of bench sitting (applicable when ONLY searching for shinies), use '0' to disable")] + public int OverworldSpawnCheck { get; set; } = 1; + + [Category(Encounter), Description("Duration in milliseconds to walk forward, then back, after a bench sit or when passing a Wild Zone entrance.")] + public int WalkDurationMs { get; set; } + } + + [Category(Encounter), Description("When enabled, the bot will only stop when encounter has a Scale of XXXS or XXXL.")] + public bool MinMaxScaleOnly { get; set; } = false; + + [Category(Encounter), Description("When enabled, the screen will be turned off during normal bot loop operation to save power.")] + public bool ScreenOff { get; set; } + + private int _completedWild; + private int _completedLegend; + private int _completedFossils; + + [Category(Counts), Description("Encountered Wild Pokémon")] + public int CompletedEncounters + { + get => _completedWild; + set => _completedWild = value; + } + + [Category(Counts), Description("Encountered Legendary Pokémon")] + public int CompletedLegends + { + get => _completedLegend; + set => _completedLegend = value; + } + + [Category(Counts), Description("Fossil Pokémon Revived")] + public int CompletedFossils + { + get => _completedFossils; + set => _completedFossils = value; + } + + [Category(Counts), Description("When enabled, the counts will be emitted when a status check is requested.")] + public bool EmitCountsOnStatusCheck { get; set; } + + public int AddCompletedEncounters() => Interlocked.Increment(ref _completedWild); + public int AddCompletedLegends() => Interlocked.Increment(ref _completedLegend); + public int AddCompletedFossils() => Interlocked.Increment(ref _completedFossils); + + public IEnumerable GetNonZeroCounts() + { + if (!EmitCountsOnStatusCheck) + yield break; + if (CompletedEncounters != 0) + yield return $"Wild Encounters: {CompletedEncounters}"; + if (CompletedLegends != 0) + yield return $"Legendary Encounters: {CompletedLegends}"; + if (CompletedFossils != 0) + yield return $"Completed Fossils: {CompletedFossils}"; + } + + public enum OverworldModeLZA + { + BenchSit, + WildZoneEntrance + } +} diff --git a/SysBot.Pokemon/LZA/BotFactory9LZA.cs b/SysBot.Pokemon/LZA/BotFactory9LZA.cs new file mode 100644 index 00000000..4458c922 --- /dev/null +++ b/SysBot.Pokemon/LZA/BotFactory9LZA.cs @@ -0,0 +1,27 @@ +namespace SysBot.Pokemon.ZA; + +using System; +using PKHeX.Core; + +public sealed class BotFactory9LZA : BotFactory +{ + public override PokeRoutineExecutorBase CreateBot(PokeTradeHub hub, PokeBotState cfg) => cfg.NextRoutineType switch + { + PokeRoutineType.EncounterOverworld => new EncounterBotOverworldScannerLZA(cfg, hub), + PokeRoutineType.FossilBot => new EncounterBotFossilLZA(cfg, hub), + + PokeRoutineType.RemoteControl => new RemoteControlBotLZA(cfg), + + _ => throw new ArgumentException(nameof(cfg.NextRoutineType)), + }; + + public override bool SupportsRoutine(PokeRoutineType type) => type switch + { + PokeRoutineType.EncounterOverworld => true, + PokeRoutineType.FossilBot => true, + + PokeRoutineType.RemoteControl => true, + + _ => false, + }; +} diff --git a/SysBot.Pokemon/LZA/BotRemoteControl/RemoteControlBotLZA.cs b/SysBot.Pokemon/LZA/BotRemoteControl/RemoteControlBotLZA.cs new file mode 100644 index 00000000..dd42a6b0 --- /dev/null +++ b/SysBot.Pokemon/LZA/BotRemoteControl/RemoteControlBotLZA.cs @@ -0,0 +1,40 @@ +using System; +using System.Threading; +using System.Threading.Tasks; +using SysBot.Base; +using SysBot.Pokemon.ZA; + +namespace SysBot.Pokemon; + +public class RemoteControlBotLZA(PokeBotState cfg) : PokeRoutineExecutor9LZA(cfg) +{ + public override async Task MainLoop(CancellationToken token) + { + try + { + Log("Identifying trainer data of the host console."); + await IdentifyTrainer(token).ConfigureAwait(false); + + Log("Starting main loop, then waiting for commands."); + Config.IterateNextRoutine(); + while (!token.IsCancellationRequested) + { + await Task.Delay(1_000, token).ConfigureAwait(false); + ReportStatus(); + } + } + catch (Exception e) + { + Log(e.Message); + } + + Log($"Ending {nameof(RemoteControlBotLA)} loop."); + await HardStop().ConfigureAwait(false); + } + + public override async Task HardStop() + { + await SetStick(SwitchStick.LEFT, 0, 0, 0_500, CancellationToken.None).ConfigureAwait(false); // reset + await CleanExit(CancellationToken.None).ConfigureAwait(false); + } +} diff --git a/SysBot.Pokemon/LZA/PokeRoutineExecutor9LZA.cs b/SysBot.Pokemon/LZA/PokeRoutineExecutor9LZA.cs new file mode 100644 index 00000000..41dab413 --- /dev/null +++ b/SysBot.Pokemon/LZA/PokeRoutineExecutor9LZA.cs @@ -0,0 +1,210 @@ +namespace SysBot.Pokemon; + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using Base; +using PKHeX.Core; +using static Base.SwitchButton; +using static PokeDataOffsetsLZA; + +public abstract class PokeRoutineExecutor9LZA(PokeBotState cfg) : PokeRoutineExecutor(cfg) +{ + protected PokeDataOffsetsLZA Offsets { get; } = new(); + + public override async Task ReadPokemon(ulong offset, CancellationToken token) => await ReadPokemon(offset, FormatSlotSize, token).ConfigureAwait(false); + + public override async Task ReadPokemon(ulong offset, int size, CancellationToken token) + { + var data = await SwitchConnection.ReadBytesAbsoluteAsync(offset, size, token).ConfigureAwait(false); + return new PA9(data); + } + + public override async Task ReadPokemonPointer(IEnumerable jumps, int size, CancellationToken token) + { + var (valid, offset) = await ValidatePointerAll(jumps, token).ConfigureAwait(false); + if (!valid) + return new PA9(); + + return await ReadPokemon(offset, token).ConfigureAwait(false); + } + + public async Task<(PA9, byte[]?)> ReadRawBoxPokemon(int box, int slot, CancellationToken token) + { + var jumps = Offsets.BoxStartPokemonPointer.ToArray(); + var (valid, b1s1) = await ValidatePointerAll(jumps, token).ConfigureAwait(false); + if (!valid) + return (new PA9(), null); + + const int boxSize = 30 * BoxSlotSize; + var boxStart = b1s1 + (ulong)(box * boxSize); + var slotStart = boxStart + (ulong)(slot * BoxSlotSize); + + var copiedData = new byte[BoxSlotSize]; + var data = await SwitchConnection.ReadBytesAbsoluteAsync(slotStart, BoxSlotSize, token).ConfigureAwait(false); + + data.CopyTo(copiedData, 0); + + if (!data.SequenceEqual(copiedData)) + throw new InvalidOperationException("Raw data is not copied correctly"); + + return (new PA9(data), copiedData); + } + + public override async Task ReadBoxPokemon(int box, int slot, CancellationToken token) + { + var (pa9, _) = await ReadRawBoxPokemon(box, slot, token).ConfigureAwait(false); + return pa9; + } + + public async Task SetBoxPokemonAbsolute(ulong offset, PK9 pkm, CancellationToken token, ITrainerInfo? sav = null) + { + if (sav != null) + { + // Update PKM to the current save's handler data + pkm.UpdateHandler(sav); + pkm.RefreshChecksum(); + } + + pkm.ResetPartyStats(); + + var encrypted = pkm.EncryptedBoxData; + var boxData = new byte[encrypted.Length + 0x40]; + Buffer.BlockCopy(encrypted, 0, boxData, 0, encrypted.Length); + + await SwitchConnection.WriteBytesAbsoluteAsync(boxData, offset, token).ConfigureAwait(false); + } + + public async Task IdentifyTrainer(CancellationToken token) + { + // Check if botbase is on the correct version or later. + await VerifyBotbaseVersion(token).ConfigureAwait(false); + + // Check title so we can warn if mode is incorrect. + string title = await SwitchConnection.GetTitleID(token).ConfigureAwait(false); + if (title != LegendsZAID) + throw new Exception($"{title} is not a valid Pokémon Legends: ZA title. Is your mode correct?"); + + // Verify the game version. + var game_version = await SwitchConnection.GetGameInfo("version", token).ConfigureAwait(false); + if (!game_version.SequenceEqual(ZAGameVersion)) + throw new Exception($"Game version is not supported. Expected version {ZAGameVersion}, and current game version is {game_version}."); + + var sav = await GetFakeTrainerSAV(token).ConfigureAwait(false); + InitSaveData(sav); + + if (!IsValidTrainerData()) + { + await CheckForRAMShiftingApps(token).ConfigureAwait(false); + throw new Exception("Refer to the SysBot.NET wiki (https://github.com/kwsch/SysBot.NET/wiki/Troubleshooting) for more information."); + } + + return sav; + } + + public async Task GetFakeTrainerSAV(CancellationToken token) + { + var sav = new SAV9ZA(); + var info = sav.MyStatus; + var read = await SwitchConnection.PointerPeek(info.Data.Length, Offsets.MyStatusPointer, token).ConfigureAwait(false); + read.CopyTo(info.Data); + return sav; + } + + public async Task InitializeHardware(IBotStateSettings settings, CancellationToken token) + { + Log("Detaching on startup."); + await DetachController(token).ConfigureAwait(false); + if (settings.ScreenOff) + { + Log("Turning off screen."); + await SetScreen(ScreenState.Off, token).ConfigureAwait(false); + } + } + + public async Task CleanExit(CancellationToken token) + { + await SetScreen(ScreenState.On, token).ConfigureAwait(false); + Log("Detaching controllers on routine exit."); + await DetachController(token).ConfigureAwait(false); + } + + public async Task CloseGame(PokeTradeHubConfig config, CancellationToken token) + { + var timing = config.Timings; + // Close out of the game + await Click(B, 0_500, token).ConfigureAwait(false); + await Click(HOME, 2_000 + timing.ExtraTimeReturnHome, token).ConfigureAwait(false); + await Click(X, 1_000, token).ConfigureAwait(false); + await Click(A, 5_000 + timing.ExtraTimeCloseGame, token).ConfigureAwait(false); + Log("Closed out of the game!"); + } + + public async Task StartGame(PokeTradeHubConfig config, CancellationToken token) + { + var timing = config.Timings; + // Open game. + await Click(A, 1_000 + timing.ExtraTimeLoadProfile, token).ConfigureAwait(false); + + // Menus here can go in the order: Update Prompt -> Profile -> Starts Game + // The user can optionally turn on the setting if they know of a breaking system update incoming. + if (timing.AvoidSystemUpdate) + { + await Task.Delay(1_000, token).ConfigureAwait(false); // Reduce the chance of misclicking here. + await Click(DUP, 0_600, token).ConfigureAwait(false); + await Click(A, 1_000 + timing.ExtraTimeLoadProfile, token).ConfigureAwait(false); + } + + await Click(DUP, 0_600, token).ConfigureAwait(false); + await Click(A, 0_600, token).ConfigureAwait(false); + + Log("Restarting the game!"); + + // Switch Logo... + await Task.Delay(12_000 + timing.ExtraTimeLoadGame, token).ConfigureAwait(false); + + // ... and game load screen + await Click(A, 1_000, token).ConfigureAwait(false); + + await Task.Delay(4_000 + timing.ExtraTimeLoadOverworld, token).ConfigureAwait(false); + Log("Back in the overworld!"); + } + + public async Task SaveGame(CancellationToken token) + { + Log("Saving the game"); + await Click(X, 1_000, token).ConfigureAwait(false); + await Click(R, 0_500, token).ConfigureAwait(false); + await Click(A, 3_500, token).ConfigureAwait(false); + + for (var i = 0; i < 4; i++) + await Click(B, 0_400, token).ConfigureAwait(false); + } + + private readonly Dictionary _cacheBlockAddresses = new(); + public async Task ReadEncryptedBlock(IEnumerable pointer, uint blockKey, bool init, CancellationToken token) + { + var exists = _cacheBlockAddresses.TryGetValue(blockKey, out var cachedAddress); + if (init || !exists) + { + var address = await SwitchConnection.PointerAll(pointer, token); + address = BitConverter.ToUInt64(await SwitchConnection.ReadBytesAbsoluteAsync(address + 8, 0x8, token).ConfigureAwait(false), 0); + cachedAddress = address; + + if (exists) + { + _cacheBlockAddresses[blockKey] = cachedAddress; + Log($"Refreshed address for {blockKey:X8} found at {cachedAddress:X8}"); + } + else + { + _cacheBlockAddresses.Add(blockKey, cachedAddress); + Log($"Initial address for {blockKey:X8} found at {cachedAddress:X8}"); + } + } + + return await ReadEncryptedBlock(cachedAddress, blockKey, token); + } +} diff --git a/SysBot.Pokemon/LZA/Vision/PokeDataOffsetsLZA.cs b/SysBot.Pokemon/LZA/Vision/PokeDataOffsetsLZA.cs new file mode 100644 index 00000000..5d16a044 --- /dev/null +++ b/SysBot.Pokemon/LZA/Vision/PokeDataOffsetsLZA.cs @@ -0,0 +1,25 @@ +using System.Collections.Generic; + +namespace SysBot.Pokemon; + +/// +/// Pokémon Legends: Z-A RAM offsets +/// +public class PokeDataOffsetsLZA +{ + public const string ZAGameVersion = "1.0.2"; + public const string LegendsZAID = "0100F43008C44000"; + + public IReadOnlyList BoxStartPokemonPointer { get; } = [0x5F0C250, 0xB0, 0x978, 0x0]; + public IReadOnlyList MyStatusPointer { get; } = [0x5F0C250, 0x80, 0x100]; + public IReadOnlyList KItemPointer { get; } = [0x5F0C1B0, 0x30, 0x08, 0x400]; + public IReadOnlyList KOverworldPointer { get; } = [0x5F0C1B0, 0x30, 0x08, 0x8A0]; + public IReadOnlyList KStoredShinyEntityPointer { get; } = [0x5F0C1B0, 0x30, 0x08, 0x1380]; + + public const uint KItemKey = 0x21C9BD44; + public const uint KOverworldKey = 0x5E8E1711; + public const uint KStoredShinyEntityKey = 0xF3A8569D; + + public const int FormatSlotSize = 0x158; // Party format size + public const int BoxSlotSize = 0x198; // Size between box entries +} diff --git a/SysBot.Pokemon/SV/BotEncounter/EncounterBotOverworldScanner.cs b/SysBot.Pokemon/SV/BotEncounter/EncounterBotOverworldScannerSV.cs similarity index 95% rename from SysBot.Pokemon/SV/BotEncounter/EncounterBotOverworldScanner.cs rename to SysBot.Pokemon/SV/BotEncounter/EncounterBotOverworldScannerSV.cs index 3ec22d0f..ca3cc079 100644 --- a/SysBot.Pokemon/SV/BotEncounter/EncounterBotOverworldScanner.cs +++ b/SysBot.Pokemon/SV/BotEncounter/EncounterBotOverworldScannerSV.cs @@ -10,7 +10,7 @@ namespace SysBot.Pokemon; using static Base.SwitchButton; using static Base.SwitchStick; -public class EncounterBotOverworldScanner(PokeBotState cfg, PokeTradeHub hub) : EncounterBotSV(cfg, hub) +public class EncounterBotOverworldScannerSV(PokeBotState cfg, PokeTradeHub hub) : EncounterBotSV(cfg, hub) { private bool _saveKeyInitialized; private ulong _baseBlockKeyPointer; @@ -27,31 +27,31 @@ protected override async Task EncounterLoop(SAV9SV sav, CancellationToken token) { switch (Settings.Overworld) { - case EncounterSettingsSV.OverworldMode.Scanner: + case EncounterSettingsSV.OverworldModeSV.Scanner: if (await DoOverworldScanning(token).ConfigureAwait(false)) return; await Task.Delay(1000, token); break; - case EncounterSettingsSV.OverworldMode.ResearchStation: + case EncounterSettingsSV.OverworldModeSV.ResearchStation: if (await DoResearchStation(token).ConfigureAwait(false)) return; break; - case EncounterSettingsSV.OverworldMode.Outbreak: + case EncounterSettingsSV.OverworldModeSV.Outbreak: if (await DoMassOutbreakResetting(token).ConfigureAwait(false)) return; break; - case EncounterSettingsSV.OverworldMode.KOCount: + case EncounterSettingsSV.OverworldModeSV.KOCount: if (await DoKOCounting(token).ConfigureAwait(false)) return; await Task.Delay(5000, token); break; - case EncounterSettingsSV.OverworldMode.Picnic: + case EncounterSettingsSV.OverworldModeSV.Picnic: if (await DoPicnicResetting(token).ConfigureAwait(false)) return; break; diff --git a/SysBot.Pokemon/SV/BotEncounter/EncounterBotSV.cs b/SysBot.Pokemon/SV/BotEncounter/EncounterBotSV.cs index c53c277a..6f0fcc7f 100644 --- a/SysBot.Pokemon/SV/BotEncounter/EncounterBotSV.cs +++ b/SysBot.Pokemon/SV/BotEncounter/EncounterBotSV.cs @@ -1,12 +1,13 @@ namespace SysBot.Pokemon; -using Base; -using PKHeX.Core; using System; using System.Collections.Generic; using System.IO; +using System.Linq; using System.Threading; using System.Threading.Tasks; +using Base; +using PKHeX.Core; using static Base.SwitchButton; using static Base.SwitchStick; @@ -98,7 +99,7 @@ public override async Task HardStop() if (!StopConditionSettings.EncounterFound(pk, Hub.Config.StopConditions, UnwantedMarks)) { - if (folder.Equals("egg") && Hub.Config.StopConditions.ShinyTarget is TargetShinyType.AnyShiny or TargetShinyType.StarOnly or TargetShinyType.SquareOnly && pk.IsShiny) + if (folder.Equals("egg") && Hub.Config.StopConditions.SearchConditions.Any(sc => sc.IsEnabled && pk.IsShiny && sc.ShinyTarget is TargetShinyType.AnyShiny or TargetShinyType.StarOnly or TargetShinyType.SquareOnly)) Hub.LogEmbed(pk, false); return (false, false); diff --git a/SysBot.Pokemon/SV/BotEncounter/EncounterSettingsSV.cs b/SysBot.Pokemon/SV/BotEncounter/EncounterSettingsSV.cs index 3e5f4c8f..8341bab5 100644 --- a/SysBot.Pokemon/SV/BotEncounter/EncounterSettingsSV.cs +++ b/SysBot.Pokemon/SV/BotEncounter/EncounterSettingsSV.cs @@ -11,7 +11,6 @@ public class EncounterSettingsSV : IBotStateSettings, ICountSettings { private const string Counts = nameof(Counts); private const string Encounter = nameof(Encounter); - private const string Settings = nameof(Settings); public override string ToString() => "Encounter Bot SV Settings"; [Category(Encounter), Description("When enabled, the bot will continue after finding a suitable match.")] @@ -36,7 +35,7 @@ public class EncounterSettingsSV : IBotStateSettings, ICountSettings public string UnlimitedParentsFolder { get; set; } = string.Empty; [Category(Encounter), Description("When mode is Scanner, keep saving the game to let the bot scan the overworld.")] - public OverworldMode Overworld { get; set; } + public OverworldModeSV Overworld { get; set; } [Category(Encounter), Description("Stop condition for Mass Outbreak only.")] public List MassOutbreakSearchConditions { get; set; } = new(); @@ -106,7 +105,7 @@ public void CreateDefaults(string path) UnlimitedParentsFolder = unlimited; } - public enum OverworldMode + public enum OverworldModeSV { Scanner, ResearchStation, diff --git a/SysBot.Pokemon/SV/BotFactory9SV.cs b/SysBot.Pokemon/SV/BotFactory9SV.cs index 2bb24da4..a5d45730 100644 --- a/SysBot.Pokemon/SV/BotFactory9SV.cs +++ b/SysBot.Pokemon/SV/BotFactory9SV.cs @@ -13,7 +13,7 @@ public sealed class BotFactory9SV : BotFactory PokeRoutineType.EncounterGimmighoul => new EncounterBotGimmighoulSV(cfg, Hub), PokeRoutineType.EncounterLoyal => new EncounterBotLoyalSV(cfg, Hub), PokeRoutineType.EncounterParadox => new EncounterBotParadoxSV(cfg, Hub), - PokeRoutineType.EncounterOverworld => new EncounterBotOverworldScanner(cfg, Hub), + PokeRoutineType.EncounterOverworld => new EncounterBotOverworldScannerSV(cfg, Hub), PokeRoutineType.RemoteControl => new RemoteControlBotSV(cfg), PokeRoutineType.Pointer => new PointerBotSV(cfg, Hub), PokeRoutineType.PartnerMark => new PartnerMarkBot(cfg, Hub), diff --git a/SysBot.Pokemon/SV/PokeRoutineExecutor9SV.cs b/SysBot.Pokemon/SV/PokeRoutineExecutor9SV.cs index 95c3b5ab..25ecb825 100644 --- a/SysBot.Pokemon/SV/PokeRoutineExecutor9SV.cs +++ b/SysBot.Pokemon/SV/PokeRoutineExecutor9SV.cs @@ -407,14 +407,7 @@ public async Task ReadEncryptedBlock(ulong baseBlock, uint blockKey, boo Log($"Initial address found at {_saveKeyAddress:X8}"); } - var header = await SwitchConnection.ReadBytesAbsoluteAsync(_saveKeyAddress, 5, token).ConfigureAwait(false); - header = DecryptBlock(blockKey, header); - - var size = ReadUInt32LittleEndian(header.AsSpan()[1..]); - var data = await SwitchConnection.ReadBytesAbsoluteAsync(_saveKeyAddress, 5 + (int)size, token).ConfigureAwait(false); - var res = DecryptBlock(blockKey, data)[5..]; - - return res; + return await ReadEncryptedBlock(_saveKeyAddress, blockKey, token); } private readonly Dictionary _cacheBlockArrays = new(); @@ -525,13 +518,4 @@ public async Task SearchSaveKey(ulong baseBlock, uint key, CancellationTo throw new Exception("Can't be possible to reach this!"); } - - private static byte[] DecryptBlock(uint key, byte[] block) - { - var rng = new SCXorShift32(key); - for (var i = 0; i < block.Length; i++) - block[i] = (byte)(block[i] ^ rng.Next()); - - return block; - } } diff --git a/SysBot.Pokemon/SWSH/BotEncounter/EncounterBotFossil/FossilCount.cs b/SysBot.Pokemon/SWSH/BotEncounter/EncounterBotFossil/FossilCount.cs index 7b953cb0..6658af27 100644 --- a/SysBot.Pokemon/SWSH/BotEncounter/EncounterBotFossil/FossilCount.cs +++ b/SysBot.Pokemon/SWSH/BotEncounter/EncounterBotFossil/FossilCount.cs @@ -1,4 +1,4 @@ -using PKHeX.Core; +using PKHeX.Core; using System; using static SysBot.Pokemon.FossilSpecies; diff --git a/SysBot.Pokemon/SWSH/BotEncounter/EncounterBotSWSH.cs b/SysBot.Pokemon/SWSH/BotEncounter/EncounterBotSWSH.cs index de539f1b..32d34fc4 100644 --- a/SysBot.Pokemon/SWSH/BotEncounter/EncounterBotSWSH.cs +++ b/SysBot.Pokemon/SWSH/BotEncounter/EncounterBotSWSH.cs @@ -1,10 +1,11 @@ -using PKHeX.Core; -using SysBot.Base; using System; using System.Collections.Generic; using System.IO; +using System.Linq; using System.Threading; using System.Threading.Tasks; +using PKHeX.Core; +using SysBot.Base; using static SysBot.Base.SwitchButton; using static SysBot.Base.SwitchStick; @@ -102,7 +103,7 @@ public override async Task HardStop() if (!StopConditionSettings.EncounterFound(pk, Hub.Config.StopConditions, UnwantedMarks)) { - if (folder.Equals("egg") && Hub.Config.StopConditions.ShinyTarget is TargetShinyType.AnyShiny or TargetShinyType.StarOnly or TargetShinyType.SquareOnly && pk.IsShiny) + if (folder.Equals("egg") && Hub.Config.StopConditions.SearchConditions.Any(sc => sc.IsEnabled && pk.IsShiny && sc.ShinyTarget is TargetShinyType.AnyShiny or TargetShinyType.StarOnly or TargetShinyType.SquareOnly)) Hub.LogEmbed(pk, false); return (false, false); diff --git a/SysBot.Pokemon/SWSH/BotEncounter/EncounterSettings.cs b/SysBot.Pokemon/SWSH/BotEncounter/EncounterSettingsSWSH.cs similarity index 100% rename from SysBot.Pokemon/SWSH/BotEncounter/EncounterSettings.cs rename to SysBot.Pokemon/SWSH/BotEncounter/EncounterSettingsSWSH.cs diff --git a/SysBot.Pokemon/Settings/LegalitySettings.cs b/SysBot.Pokemon/Settings/LegalitySettings.cs index 5f6e171b..26a5b34b 100644 --- a/SysBot.Pokemon/Settings/LegalitySettings.cs +++ b/SysBot.Pokemon/Settings/LegalitySettings.cs @@ -1,6 +1,9 @@ -using PKHeX.Core; +using System; using System.Collections.Generic; using System.ComponentModel; +using System.Linq; +using PKHeX.Core; +using PKHeX.Core.AutoMod; namespace SysBot.Pokemon; @@ -38,11 +41,11 @@ public string GenerateOT [Category(Generate), Description("Default language for PKM files that don't match any of the provided PKM files.")] public LanguageID GenerateLanguage { get; set; } = LanguageID.English; - [Category(Generate), Description("If PrioritizeGame is set to \"True\", uses PrioritizeGameVersion to start looking for encounters. If \"False\", uses newest game as the version. It is recommended to leave this as \"True\".")] - public bool PrioritizeGame { get; set; } = true; + [Category(Generate), Description("Method of searching for encounters when generating Pokémon. \"NativeOnly\" searches current game pair only, \"NewestFirst\" searches from most recent game, and \"PriorityOrder\" uses the order designated in the \"GameVersionPriority\" setting.")] + public GameVersionPriorityType GameVersionPriority { get; set; } = GameVersionPriorityType.NativeOnly; - [Category(Generate), Description("Specifies the first game to use to generate encounters, or current game if this field is set to \"Any\". Set PrioritizeGame to \"true\" to enable. It is recommended to leave this as \"Any\".")] - public GameVersion PrioritizeGameVersion { get; set; } = GameVersion.Any; + [Category(Generate), Description("Specifies the order of games to use to generate encounters. Set PrioritizeGame to \"true\" to enable.")] + public List PriorityOrder { get; set; } = Enum.GetValues().Where(GameUtil.IsValidSavedVersion).Reverse().ToList(); [Category(Generate), Description("Set all possible legal ribbons for any generated Pokémon.")] public bool SetAllLegalRibbons { get; set; } diff --git a/SysBot.Pokemon/Settings/StopConditionSettings.cs b/SysBot.Pokemon/Settings/StopConditionSettings.cs index c30a5ec6..5431bd5c 100644 --- a/SysBot.Pokemon/Settings/StopConditionSettings.cs +++ b/SysBot.Pokemon/Settings/StopConditionSettings.cs @@ -1,10 +1,10 @@ -using PKHeX.Core; +namespace SysBot.Pokemon; + using System; using System.Collections.Generic; using System.ComponentModel; using System.Linq; - -namespace SysBot.Pokemon; +using PKHeX.Core; public class StopConditionSettings { @@ -20,9 +20,6 @@ public class StopConditionSettings [Category(StopConditions), Description("Desired spreads, search for nature and IVs. In the format HP/Atk/Def/SpA/SpD/Spe. Use \"x\" for unchecked IVs and \"/\" as a separator.")] public List SearchConditions { get; set; } = new(); - [Category(StopConditions), Description("Selects the shiny type to stop on.")] - public TargetShinyType ShinyTarget { get; set; } = TargetShinyType.DisableOption; - [Category(StopConditions), Description("Stop only on Pokémon that have a mark.")] public bool MarkOnly { get; set; } @@ -35,9 +32,6 @@ public class StopConditionSettings [Category(StopConditions), Description("Extra time in milliseconds to wait after an encounter is matched before pressing Capture for EncounterBot or Fossilbot.")] public int ExtraTimeWaitCaptureVideo { get; set; } = 10000; - [Category(StopConditions), Description("If set to TRUE, matches both ShinyTarget and TargetIVs settings. Otherwise, looks for either ShinyTarget or TargetIVs match.")] - public bool MatchShinyAndIV { get; set; } = true; - [Category(StopConditions), Description("If not empty, the provided string will be prepended to the result found log message to Echo alerts for whomever you specify. For Discord, use <@userIDnumber> to mention.")] public string MatchFoundEchoMention { get; set; } = string.Empty; @@ -52,32 +46,40 @@ public override string ToString() ? $"{TargetMinIVs} - {TargetMaxIVs}" : $"Flawless IVs: {Convert(FlawlessIVs)}"; - return $"{Nature}, {StopOnSpecies}, {ivsStr}"; + var isAlpha = AlphaTarget == TargetAlphaType.AnyAlpha ? " (A)" : ""; + + return $"{Nature}, {StopOnSpecies}{isAlpha}, {ivsStr}"; } - [Category(StopConditions), DisplayName("1. Enabled")] + [Category(StopConditions), DisplayName("a. Enabled")] public bool IsEnabled { get; set; } = true; - [Category(StopConditions), DisplayName("2. Species")] - public Species StopOnSpecies { get; set; } + [Category(StopConditions), DisplayName("b. Species")] + public Species StopOnSpecies { get; set; } = Species.None; + + [Category(StopConditions), DisplayName("c. Alpha (if applicable)")] + public TargetAlphaType AlphaTarget { get; set; } = TargetAlphaType.DisableOption; - [Category(StopConditions), DisplayName("3. Nature")] - public Nature Nature { get; set; } + [Category(StopConditions), DisplayName("d. Selects the shiny type to stop on.")] + public TargetShinyType ShinyTarget { get; set; } = TargetShinyType.DisableOption; - [Category(StopConditions), DisplayName("4. Ability")] + [Category(StopConditions), DisplayName("e. Nature")] + public Nature Nature { get; set; } = Nature.Random; + + [Category(StopConditions), DisplayName("f. Ability")] public TargetAbilityType AbilityTarget { get; set; } = TargetAbilityType.Any; - [Category(StopConditions), DisplayName("5. Gender")] + [Category(StopConditions), DisplayName("g. Gender")] public TargetGenderType GenderTarget { get; set; } = TargetGenderType.Any; - [Category(StopConditions), DisplayName("6. Minimum flawless IVs")] + [Category(StopConditions), DisplayName("h. Minimum flawless IVs")] [TypeConverter(typeof(DescriptionAttributeConverter))] public TargetFlawlessIVsType FlawlessIVs { get; set; } = TargetFlawlessIVsType.Disabled; - [Category(StopConditions), DisplayName("7. Minimum accepted IVs")] + [Category(StopConditions), DisplayName("i. Minimum accepted IVs")] public string TargetMinIVs { get; set; } = ""; - [Category(StopConditions), DisplayName("8. Maximum accepted IVs")] + [Category(StopConditions), DisplayName("j. Maximum accepted IVs")] public string TargetMaxIVs { get; set; } = ""; } @@ -96,27 +98,6 @@ public static bool EncounterFound(T pk, StopConditionSettings settings, IRead if (settings.MarkOnly && (unmarked || unwanted)) return false; - if (settings.ShinyTarget != TargetShinyType.DisableOption) - { - bool shinyMatch = settings.ShinyTarget switch - { - TargetShinyType.AnyShiny => pk.IsShiny, - TargetShinyType.NonShiny => !pk.IsShiny, - TargetShinyType.StarOnly => pk.IsShiny && pk.ShinyXor != 0, - TargetShinyType.SquareOnly => pk.ShinyXor == 0, - TargetShinyType.DisableOption => true, - _ => throw new ArgumentException(nameof(TargetShinyType)), - }; - - // If we only needed to match one of the criteria and it shinymatch'd, return true. - // If we needed to match both criteria and it didn't shinymatch, return false. - if (!settings.MatchShinyAndIV && shinyMatch) - return true; - - if (settings.MatchShinyAndIV && !shinyMatch) - return false; - } - // Reorder the speed to be last. Span pkIVList = stackalloc int[6]; pk.GetIVs(pkIVList); @@ -133,9 +114,38 @@ public static bool EncounterFound(T pk, StopConditionSettings settings, IRead (skipSpeciesCheck || s.StopOnSpecies == (Species)pk.Species || s.StopOnSpecies == Species.None) && MatchGender(s.GenderTarget, (Gender)pk.Gender) && MatchAbility(s.AbilityTarget, pk.Ability) && + MatchAlpha(s.AlphaTarget, pk as IAlpha) && + MatchShiny(s.ShinyTarget, pk) && s.IsEnabled); } + private static bool MatchAlpha(TargetAlphaType alphaTarget, IAlpha? pk) + { + if (pk is null) + return true; + + return alphaTarget switch + { + TargetAlphaType.NonAlpha => !pk.IsAlpha, + TargetAlphaType.AnyAlpha => pk.IsAlpha, + TargetAlphaType.DisableOption => true, + _ => throw new ArgumentOutOfRangeException(nameof(alphaTarget), alphaTarget, null) + }; + } + + private static bool MatchShiny(TargetShinyType shinyTarget, T pk) where T : PKM + { + return shinyTarget switch + { + TargetShinyType.AnyShiny => pk.IsShiny, + TargetShinyType.NonShiny => !pk.IsShiny, + TargetShinyType.StarOnly => pk.IsShiny && pk.ShinyXor != 0, + TargetShinyType.SquareOnly => pk.ShinyXor == 0, + TargetShinyType.DisableOption => true, + _ => throw new ArgumentException(nameof(TargetShinyType)), + }; + } + private static bool MatchAbility(TargetAbilityType target, int result) { return target switch @@ -238,9 +248,31 @@ public static bool HasMark(IRibbonIndex pk, out RibbonIndex result) return false; } + public static ReadOnlySpan TokenOrder => + [ + BattleTemplateToken.FirstLine, + BattleTemplateToken.Shiny, + BattleTemplateToken.Nature, + BattleTemplateToken.IVs, + ]; + public string GetPrintName(PKM pk) { - var set = ShowdownParsing.GetShowdownText(pk); + const LanguageID lang = LanguageID.English; + var settings = new BattleTemplateExportSettings(TokenOrder, lang); + var set = ShowdownParsing.GetShowdownText(pk, settings); + + // Since we can match on Min/Max Height for transfer to future games, display it. + var scales = new List(); + if (pk is IScaledSize p) + scales.Add($"Height: {p.HeightScalar}"); + + if (pk is IScaledSize3 p3) + scales.Add($"Scale: {p3.Scale}"); + + if (scales.Count > 0) + set += $"\n{string.Join(", ", scales)}"; + if (pk is IRibbonIndex r) { var rstring = GetMarkName(r); @@ -251,7 +283,7 @@ public string GetPrintName(PKM pk) } public static void ReadUnwantedMarks(StopConditionSettings settings, out IReadOnlyList marks) => - marks = settings.UnwantedMarks.Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries).Select(s => s.Trim()).ToList(); + marks = settings.UnwantedMarks.Split([','], StringSplitOptions.RemoveEmptyEntries).Select(s => s.Trim()).ToList(); public virtual bool IsUnwantedMark(string mark, IReadOnlyList marklist) => marklist.Contains(mark); @@ -260,7 +292,7 @@ public static string GetMarkName(IRibbonIndex pk) for (var mark = RibbonIndex.MarkLunchtime; mark <= RibbonIndex.MarkSlump; mark++) { if (pk.GetRibbon((int)mark)) - return RibbonStrings.GetName($"Ribbon{mark}"); + return GameInfo.Strings.Ribbons.GetName($"Ribbon{mark}"); } return ""; } @@ -307,6 +339,13 @@ public enum TargetGenderType Genderless, // Match genderless only } +public enum TargetAlphaType +{ + DisableOption, // Doesn't care + NonAlpha, // Match non alpha only + AnyAlpha, // Match alpha only +} + public enum TargetFlawlessIVsType { Disabled, diff --git a/SysBot.Pokemon/Structures/ProgramConfig.cs b/SysBot.Pokemon/Structures/ProgramConfig.cs index 208f5c2f..883aecfb 100644 --- a/SysBot.Pokemon/Structures/ProgramConfig.cs +++ b/SysBot.Pokemon/Structures/ProgramConfig.cs @@ -1,10 +1,11 @@ -using SysBot.Base; +using System.Text.Json.Serialization; +using SysBot.Base; namespace SysBot.Pokemon; public class ProgramConfig : BotList { - public ProgramMode Mode { get; set; } = ProgramMode.SV; + public ProgramMode Mode { get; set; } = ProgramMode.ZA; public PokeTradeHubConfig Hub { get; set; } = new(); } @@ -15,4 +16,9 @@ public enum ProgramMode BDSP = 2, LA = 3, SV = 4, + ZA = 5 } + +[JsonSerializable(typeof(ProgramConfig))] +[JsonSourceGenerationOptions(WriteIndented = true, DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull)] +public sealed partial class ProgramConfigContext : JsonSerializerContext; diff --git a/SysBot.Pokemon/SysBot.Pokemon.csproj b/SysBot.Pokemon/SysBot.Pokemon.csproj index cce22fab..0c41c5de 100644 --- a/SysBot.Pokemon/SysBot.Pokemon.csproj +++ b/SysBot.Pokemon/SysBot.Pokemon.csproj @@ -2,7 +2,7 @@ - + diff --git a/SysBot.Pokemon/TradeHub/PokeTradeHubConfig.cs b/SysBot.Pokemon/TradeHub/PokeTradeHubConfig.cs index ca21f7d6..e8ec62e2 100644 --- a/SysBot.Pokemon/TradeHub/PokeTradeHubConfig.cs +++ b/SysBot.Pokemon/TradeHub/PokeTradeHubConfig.cs @@ -41,6 +41,10 @@ public sealed class PokeTradeHubConfig : BaseConfig // Encounter Bots - For finding or hosting Pokémon in-game. + [Category(BotEncounter)] + [TypeConverter(typeof(ExpandableObjectConverter))] + public EncounterSettingsLZA EncounterLZA { get; set; } = new(); + [Category(BotEncounter)] [TypeConverter(typeof(ExpandableObjectConverter))] public EncounterSettingsSV EncounterSV { get; set; } = new(); diff --git a/SysBot.Pokemon/deps/PKHeX.Core.AutoMod.dll b/SysBot.Pokemon/deps/PKHeX.Core.AutoMod.dll index d2fd4ae4..15c3c401 100644 Binary files a/SysBot.Pokemon/deps/PKHeX.Core.AutoMod.dll and b/SysBot.Pokemon/deps/PKHeX.Core.AutoMod.dll differ diff --git a/SysBot.Tests/SysBot.Tests.csproj b/SysBot.Tests/SysBot.Tests.csproj index b1efb781..fe02afb8 100644 --- a/SysBot.Tests/SysBot.Tests.csproj +++ b/SysBot.Tests/SysBot.Tests.csproj @@ -14,9 +14,9 @@ - + - + all runtime; build; native; contentfiles; analyzers; buildtransitive