From 617316d09368c7484dac3f46656f6382cfdeca08 Mon Sep 17 00:00:00 2001 From: Eppin <96589679+Eppin@users.noreply.github.com> Date: Sat, 27 Jun 2026 11:40:44 +0200 Subject: [PATCH 1/4] Initial Volcanion routine --- SysBot.Pokemon/Actions/PokeRoutineType.cs | 3 + .../BotEncounter/EncounterBotVolcanionLZA.cs | 108 ++++++++++++++++++ SysBot.Pokemon/LZA/BotFactory9LZA.cs | 2 + 3 files changed, 113 insertions(+) create mode 100644 SysBot.Pokemon/LZA/BotEncounter/EncounterBotVolcanionLZA.cs diff --git a/SysBot.Pokemon/Actions/PokeRoutineType.cs b/SysBot.Pokemon/Actions/PokeRoutineType.cs index 76873a6..9ed53d3 100644 --- a/SysBot.Pokemon/Actions/PokeRoutineType.cs +++ b/SysBot.Pokemon/Actions/PokeRoutineType.cs @@ -75,6 +75,9 @@ public enum PokeRoutineType /// Retrieves Diancie until the criteria is satisfied. EncounterDiancie = 7_006, + /// Retrieves Volcanion until the criteria is satisfied. + EncounterVolcanion = 7_007, + /// Keeps running circles until all party members have the Partner mark. PartnerMark = 7_050, diff --git a/SysBot.Pokemon/LZA/BotEncounter/EncounterBotVolcanionLZA.cs b/SysBot.Pokemon/LZA/BotEncounter/EncounterBotVolcanionLZA.cs new file mode 100644 index 0000000..c65fd73 --- /dev/null +++ b/SysBot.Pokemon/LZA/BotEncounter/EncounterBotVolcanionLZA.cs @@ -0,0 +1,108 @@ +namespace SysBot.Pokemon; + +using System; +using System.Threading; +using System.Threading.Tasks; +using Base; +using PKHeX.Core; +using static Base.SwitchButton; +using static Base.SwitchStick; + +public class EncounterBotVolcanionLZA(PokeBotState cfg, PokeTradeHub hub) : EncounterBotLZA(cfg, hub) +{ + private readonly ushort _volcanion = (ushort)Species.Volcanion; + + protected override async Task EncounterLoop(SAV9ZA sav, CancellationToken token) + { + const int maxDurationSeconds = 45 + 100; + + while (!token.IsCancellationRequested) + { + var later = DateTime.Now.AddSeconds(maxDurationSeconds); + await EnableAlwaysCatch(token).ConfigureAwait(false); + + Log("Starting Volcanion encounter sequence"); + await SetStick(LEFT, 0, 30_000, 0_500, token).ConfigureAwait(false); + await ResetStick(token).ConfigureAwait(false); + await RepeatClick(A, 100 * 1000, 100, token).ConfigureAwait(false); // 1 minute, 40 seconds + + Log($"Catching Volcanion, wait till [{later}] before we force a game restart", false); + + var result = EncounterResult.Unknown; + while (result != EncounterResult.VolcanionFound && DateTime.Now <= later) + { + await PressAndHold(ZL, 0_250, token).ConfigureAwait(false); + await Click(ZR, 0_100, token).ConfigureAwait(false); + await ReleaseHold(ZL, 0_250, token).ConfigureAwait(false); + + result = await LookupSlot(token).ConfigureAwait(false); + + if (result is EncounterResult.InvalidSpecies or EncounterResult.ResultFound) + return; // Exit the encounter loop entirely + + if (result is EncounterResult.VolcanionFound) + { + Log("Volcanion found in B1S1, rebooting the game..."); + break; + } + } + + if (DateTime.Now >= later) + Log("Force restart of the game..."); + + await ReOpenGame(Hub.Config, token).ConfigureAwait(false); + await Task.Delay(10_000, token).ConfigureAwait(false); + } + } + + public override async Task HardStop() + { + await ReleaseHold(ZL, 0_500, CancellationToken.None).ConfigureAwait(false); + await base.HardStop().ConfigureAwait(false); + } + + private async Task LookupSlot(CancellationToken token) + { + var (pa9, raw) = await ReadRawBoxPokemon(0, 0, token).ConfigureAwait(false); + if (pa9.Species > 0 && pa9.Species != _volcanion) + { + Log($"Detected species {(Species)pa9.Species}, which shouldn't be possible. Only 'none' or 'Volcanion' are expected"); + return EncounterResult.InvalidSpecies; + } + + if (pa9.Species == _volcanion && pa9 is { Valid: true, EncryptionConstant: > 0 }) + { + var (stop, success) = await HandleEncounter(pa9, token, raw, true).ConfigureAwait(false); + + if (success) + Log("Your Pokémon has been received and placed in B1S1. Auto-save will do the rest!"); + + if (stop) + return EncounterResult.ResultFound; + } + + return pa9.Species == _volcanion + ? EncounterResult.VolcanionFound + : EncounterResult.NextSlot; + } + + private async Task RepeatClick(SwitchButton button, int duration, int delay, CancellationToken token) + { + var endTime = DateTime.Now.AddMilliseconds(duration); + + while (DateTime.Now < endTime) + { + await Click(button, delay, token).ConfigureAwait(false); + } + } + + private enum EncounterResult + { + Unknown, + + InvalidSpecies, + ResultFound, + VolcanionFound, + NextSlot + } +} diff --git a/SysBot.Pokemon/LZA/BotFactory9LZA.cs b/SysBot.Pokemon/LZA/BotFactory9LZA.cs index fa8a03e..95fa6a3 100644 --- a/SysBot.Pokemon/LZA/BotFactory9LZA.cs +++ b/SysBot.Pokemon/LZA/BotFactory9LZA.cs @@ -9,6 +9,7 @@ public sealed class BotFactory9LZA : BotFactory { PokeRoutineType.EncounterDiancie => new EncounterBotDiancieLZA(cfg, hub), PokeRoutineType.EncounterFloette => new EncounterBotFloetteLZA(cfg, hub), + PokeRoutineType.EncounterVolcanion => new EncounterBotVolcanionLZA(cfg, hub), PokeRoutineType.EncounterOverworld => new EncounterBotOverworldScannerLZA(cfg, hub), PokeRoutineType.FossilBot => new EncounterBotFossilLZA(cfg, hub), @@ -21,6 +22,7 @@ public sealed class BotFactory9LZA : BotFactory { PokeRoutineType.EncounterDiancie => true, PokeRoutineType.EncounterFloette => true, + PokeRoutineType.EncounterVolcanion => true, PokeRoutineType.EncounterOverworld => true, PokeRoutineType.FossilBot => true, From 18b199cef6d0215f87d3fa1591cfd54dd8963e5c Mon Sep 17 00:00:00 2001 From: Eppin <96589679+Eppin@users.noreply.github.com> Date: Sat, 18 Jul 2026 12:57:54 +0200 Subject: [PATCH 2/4] Added routine for Genesect, Hoopa and Magearna --- .../Actions/PokeRoutineExecutorBase.cs | 2 +- SysBot.Pokemon/Actions/PokeRoutineType.cs | 12 ++ .../BotEncounter/EncounterBotGenesectLZA.cs | 126 ++++++++++++++++++ .../LZA/BotEncounter/EncounterBotHoopaLZA.cs | 68 ++++++++++ .../BotEncounter/EncounterBotMagearnaLZA.cs | 68 ++++++++++ .../BotEncounter/EncounterBotVolcanionLZA.cs | 92 ++++++++----- SysBot.Pokemon/LZA/BotFactory9LZA.cs | 27 ++-- 7 files changed, 347 insertions(+), 48 deletions(-) create mode 100644 SysBot.Pokemon/LZA/BotEncounter/EncounterBotGenesectLZA.cs create mode 100644 SysBot.Pokemon/LZA/BotEncounter/EncounterBotHoopaLZA.cs create mode 100644 SysBot.Pokemon/LZA/BotEncounter/EncounterBotMagearnaLZA.cs diff --git a/SysBot.Pokemon/Actions/PokeRoutineExecutorBase.cs b/SysBot.Pokemon/Actions/PokeRoutineExecutorBase.cs index ba02726..a0462a5 100644 --- a/SysBot.Pokemon/Actions/PokeRoutineExecutorBase.cs +++ b/SysBot.Pokemon/Actions/PokeRoutineExecutorBase.cs @@ -7,7 +7,7 @@ namespace SysBot.Pokemon; public abstract class PokeRoutineExecutorBase : SwitchRoutineExecutor { - public const decimal BotbaseVersion = 2.4m; + public const decimal BotbaseVersion = 2.5m; protected PokeRoutineExecutorBase(IConsoleBotManaged cfg) : base(cfg) { diff --git a/SysBot.Pokemon/Actions/PokeRoutineType.cs b/SysBot.Pokemon/Actions/PokeRoutineType.cs index 9ed53d3..56a7402 100644 --- a/SysBot.Pokemon/Actions/PokeRoutineType.cs +++ b/SysBot.Pokemon/Actions/PokeRoutineType.cs @@ -78,6 +78,18 @@ public enum PokeRoutineType /// Retrieves Volcanion until the criteria is satisfied. EncounterVolcanion = 7_007, + /// Retrieves Genesect until the criteria is satisfied. + EncounterGenesect = 7_008, + + /// Retrieves Magearna until the criteria is satisfied. + EncounterMagearna = 7_009, + + /// Retrieves Marshadow until the criteria is satisfied. + EncounterMarshadow = 7_010, + + /// Retrieves Hoopa until the criteria is satisfied. + EncounterHoopa = 7_011, + /// Keeps running circles until all party members have the Partner mark. PartnerMark = 7_050, diff --git a/SysBot.Pokemon/LZA/BotEncounter/EncounterBotGenesectLZA.cs b/SysBot.Pokemon/LZA/BotEncounter/EncounterBotGenesectLZA.cs new file mode 100644 index 0000000..da2f97b --- /dev/null +++ b/SysBot.Pokemon/LZA/BotEncounter/EncounterBotGenesectLZA.cs @@ -0,0 +1,126 @@ +namespace SysBot.Pokemon; + +using System; +using System.Threading; +using System.Threading.Tasks; +using Base; +using PKHeX.Core; +using static Base.SwitchButton; +using static Base.SwitchStick; + +public class EncounterBotGenesectLZA(PokeBotState cfg, PokeTradeHub hub) : EncounterBotLZA(cfg, hub) +{ + private const ushort GenesectSpecies = (ushort)Species.Genesect; + + private const int EncounterTimeoutSeconds = 46; + private const int MashDurationMs = 26_000; + private const int MashDelayMs = 100; + private const int ReopenDelayMs = 10_000; + + protected override async Task EncounterLoop(SAV9ZA sav, CancellationToken token) + { + while (!token.IsCancellationRequested) + { + var deadline = DateTime.Now.AddSeconds(EncounterTimeoutSeconds); + + await EnableAlwaysCatch(token).ConfigureAwait(false); + + Log("Starting Genesect encounter sequence"); + await SetStick(LEFT, 0, 30_000, 0_500, token).ConfigureAwait(false); + await ResetStick(token).ConfigureAwait(false); + await RepeatClick(A, MashDurationMs, MashDelayMs, token).ConfigureAwait(false); + + Log($"Catching Genesect, wait till [{deadline}] before we force a game restart", false); + + await SetStick(LEFT, 0, 30_000, 1_000, token).ConfigureAwait(false); + await ResetStick(token).ConfigureAwait(false); + + var result = await TryEncounterUntilDeadline(deadline, token).ConfigureAwait(false); + + if (result is EncounterResult.InvalidSpecies or EncounterResult.ResultFound) + return; + + if (result == EncounterResult.Timeout) + Log("Force restart of the game..."); + + await ReOpenGame(Hub.Config, token).ConfigureAwait(false); + await Task.Delay(ReopenDelayMs, token).ConfigureAwait(false); + } + } + + public override async Task HardStop() + { + await ReleaseHold(ZL, 0_500, CancellationToken.None).ConfigureAwait(false); + await base.HardStop().ConfigureAwait(false); + } + + private async Task TryEncounterUntilDeadline(DateTime deadline, CancellationToken token) + { + while (DateTime.Now <= deadline && !token.IsCancellationRequested) + { + await RunEncounterInput(token).ConfigureAwait(false); + + var result = await LookupSlot(token).ConfigureAwait(false); + if (result is EncounterResult.InvalidSpecies or EncounterResult.ResultFound) + return result; + + if (result == EncounterResult.GenesectFound) + { + Log("Genesect found in B1S1, rebooting the game..."); + return result; + } + } + + return EncounterResult.Timeout; + } + + private async Task RunEncounterInput(CancellationToken token) + { + await PressAndHold(ZL, 0_250, token).ConfigureAwait(false); + await Click(ZR, 0_100, token).ConfigureAwait(false); + await ReleaseHold(ZL, 0_250, token).ConfigureAwait(false); + } + + private async Task LookupSlot(CancellationToken token) + { + var (pa9, raw) = await ReadRawBoxPokemon(0, 0, token).ConfigureAwait(false); + + if (pa9.Species > 0 && pa9.Species != GenesectSpecies) + { + Log($"Detected species {(Species)pa9.Species}, which shouldn't be possible. Only 'none' or 'Genesect' are expected"); + return EncounterResult.InvalidSpecies; + } + + if (pa9.Species == GenesectSpecies && pa9 is { Valid: true, EncryptionConstant: > 0 }) + { + var (stop, success) = await HandleEncounter(pa9, token, raw, true).ConfigureAwait(false); + + if (success) + Log("Your Pokemon has been received and placed in B1S1. Auto-save will do the rest!"); + + if (stop) + return EncounterResult.ResultFound; + } + + return pa9.Species == GenesectSpecies + ? EncounterResult.GenesectFound + : EncounterResult.NextSlot; + } + + private async Task RepeatClick(SwitchButton button, int durationMs, int delayMs, CancellationToken token) + { + var endUtc = DateTime.UtcNow.AddMilliseconds(durationMs); + + while (DateTime.UtcNow < endUtc && !token.IsCancellationRequested) + await Click(button, delayMs, token).ConfigureAwait(false); + } + + private enum EncounterResult + { + InvalidSpecies, + ResultFound, + GenesectFound, + NextSlot, + Timeout + } +} diff --git a/SysBot.Pokemon/LZA/BotEncounter/EncounterBotHoopaLZA.cs b/SysBot.Pokemon/LZA/BotEncounter/EncounterBotHoopaLZA.cs new file mode 100644 index 0000000..4a5585a --- /dev/null +++ b/SysBot.Pokemon/LZA/BotEncounter/EncounterBotHoopaLZA.cs @@ -0,0 +1,68 @@ +namespace SysBot.Pokemon; + +using System.Threading; +using System.Threading.Tasks; +using PKHeX.Core; +using static Base.SwitchButton; + +public class EncounterBotHoopaLZA(PokeBotState cfg, PokeTradeHub hub) : EncounterBotLZA(cfg, hub) +{ + private readonly ushort _hoopa = (ushort)Species.Hoopa; + + protected override async Task EncounterLoop(SAV9ZA sav, CancellationToken token) + { + while (!token.IsCancellationRequested) + { + var dialogueCancellationTokenSource = new CancellationTokenSource(); + _ = DialogueWalking(dialogueCancellationTokenSource.Token, token).ConfigureAwait(false); + + PA9? pa9 = null; + while (pa9 == null || pa9.Species == 0 || !pa9.Valid || pa9.EncryptionConstant == 0) + { + (pa9, var raw) = await ReadRawBoxPokemon(0, 0, token).ConfigureAwait(false); + if (pa9.Species > 0 && pa9.Species != _hoopa) + { + await dialogueCancellationTokenSource.CancelAsync(); + Log($"Detected species {(Species)pa9.Species}, which shouldn't be possible. Only 'none' or 'Hoopa' are expected"); + return; + } + + if (pa9.Species == _hoopa) + { + var (stop, success) = await HandleEncounter(pa9, token, raw, true).ConfigureAwait(false); + + if (success) + { + await dialogueCancellationTokenSource.CancelAsync(); + Log("Your Pokémon has been received and placed in B1S1. Auto-save will do the rest!"); + } + + if (stop) + return; + } + + await Task.Delay(0_250, token); + } + + await dialogueCancellationTokenSource.CancelAsync(); + await ReOpenGame(Hub.Config, token).ConfigureAwait(false); + } + } + + private async Task DialogueWalking(CancellationToken dialogueToken, CancellationToken generalToken) + { + try + { + while (!dialogueToken.IsCancellationRequested && !generalToken.IsCancellationRequested) + { + await Click(A, 0_200, dialogueToken).ConfigureAwait(false); + } + + Log("Stopping dialogue... which is weird!"); + } + catch (TaskCanceledException) + { + Log("Dialogue is canceled"); + } + } +} diff --git a/SysBot.Pokemon/LZA/BotEncounter/EncounterBotMagearnaLZA.cs b/SysBot.Pokemon/LZA/BotEncounter/EncounterBotMagearnaLZA.cs new file mode 100644 index 0000000..a8cf29d --- /dev/null +++ b/SysBot.Pokemon/LZA/BotEncounter/EncounterBotMagearnaLZA.cs @@ -0,0 +1,68 @@ +namespace SysBot.Pokemon; + +using System.Threading; +using System.Threading.Tasks; +using PKHeX.Core; +using static Base.SwitchButton; + +public class EncounterBotMagearnaLZA(PokeBotState cfg, PokeTradeHub hub) : EncounterBotLZA(cfg, hub) +{ + private readonly ushort _magearna = (ushort)Species.Magearna; + + protected override async Task EncounterLoop(SAV9ZA sav, CancellationToken token) + { + while (!token.IsCancellationRequested) + { + var dialogueCancellationTokenSource = new CancellationTokenSource(); + _ = DialogueWalking(dialogueCancellationTokenSource.Token, token).ConfigureAwait(false); + + PA9? pa9 = null; + while (pa9 == null || pa9.Species == 0 || !pa9.Valid || pa9.EncryptionConstant == 0) + { + (pa9, var raw) = await ReadRawBoxPokemon(0, 0, token).ConfigureAwait(false); + if (pa9.Species > 0 && pa9.Species != _magearna) + { + await dialogueCancellationTokenSource.CancelAsync(); + Log($"Detected species {(Species)pa9.Species}, which shouldn't be possible. Only 'none' or 'Magearna' are expected"); + return; + } + + if (pa9.Species == _magearna) + { + var (stop, success) = await HandleEncounter(pa9, token, raw, true).ConfigureAwait(false); + + if (success) + { + await dialogueCancellationTokenSource.CancelAsync(); + Log("Your Pokémon has been received and placed in B1S1. Auto-save will do the rest!"); + } + + if (stop) + return; + } + + await Task.Delay(0_250, token); + } + + await dialogueCancellationTokenSource.CancelAsync(); + await ReOpenGame(Hub.Config, token).ConfigureAwait(false); + } + } + + private async Task DialogueWalking(CancellationToken dialogueToken, CancellationToken generalToken) + { + try + { + while (!dialogueToken.IsCancellationRequested && !generalToken.IsCancellationRequested) + { + await Click(A, 0_200, dialogueToken).ConfigureAwait(false); + } + + Log("Stopping dialogue... which is weird!"); + } + catch (TaskCanceledException) + { + Log("Dialogue is canceled"); + } + } +} diff --git a/SysBot.Pokemon/LZA/BotEncounter/EncounterBotVolcanionLZA.cs b/SysBot.Pokemon/LZA/BotEncounter/EncounterBotVolcanionLZA.cs index c65fd73..d67065a 100644 --- a/SysBot.Pokemon/LZA/BotEncounter/EncounterBotVolcanionLZA.cs +++ b/SysBot.Pokemon/LZA/BotEncounter/EncounterBotVolcanionLZA.cs @@ -10,48 +10,41 @@ namespace SysBot.Pokemon; public class EncounterBotVolcanionLZA(PokeBotState cfg, PokeTradeHub hub) : EncounterBotLZA(cfg, hub) { - private readonly ushort _volcanion = (ushort)Species.Volcanion; + private const ushort VolcanionSpecies = (ushort)Species.Volcanion; + + private const int EncounterTimeoutSeconds = 145; // 45 + 100 + private const int MashDurationMs = 100_000; + private const int MashDelayMs = 100; + private const int ReopenDelayMs = 10_000; protected override async Task EncounterLoop(SAV9ZA sav, CancellationToken token) { - const int maxDurationSeconds = 45 + 100; - while (!token.IsCancellationRequested) { - var later = DateTime.Now.AddSeconds(maxDurationSeconds); + var deadline = DateTime.Now.AddSeconds(EncounterTimeoutSeconds); + await EnableAlwaysCatch(token).ConfigureAwait(false); Log("Starting Volcanion encounter sequence"); await SetStick(LEFT, 0, 30_000, 0_500, token).ConfigureAwait(false); await ResetStick(token).ConfigureAwait(false); - await RepeatClick(A, 100 * 1000, 100, token).ConfigureAwait(false); // 1 minute, 40 seconds + await RepeatClick(A, MashDurationMs, MashDelayMs, token).ConfigureAwait(false); - Log($"Catching Volcanion, wait till [{later}] before we force a game restart", false); + Log($"Catching Volcanion, wait till [{deadline}] before we force a game restart", false); - var result = EncounterResult.Unknown; - while (result != EncounterResult.VolcanionFound && DateTime.Now <= later) - { - await PressAndHold(ZL, 0_250, token).ConfigureAwait(false); - await Click(ZR, 0_100, token).ConfigureAwait(false); - await ReleaseHold(ZL, 0_250, token).ConfigureAwait(false); - - result = await LookupSlot(token).ConfigureAwait(false); + await SetStick(LEFT, 0, 30_000, 1_000, token).ConfigureAwait(false); + await ResetStick(token).ConfigureAwait(false); - if (result is EncounterResult.InvalidSpecies or EncounterResult.ResultFound) - return; // Exit the encounter loop entirely + var result = await TryEncounterUntilDeadline(deadline, token).ConfigureAwait(false); - if (result is EncounterResult.VolcanionFound) - { - Log("Volcanion found in B1S1, rebooting the game..."); - break; - } - } + if (result is EncounterResult.InvalidSpecies or EncounterResult.ResultFound) + return; - if (DateTime.Now >= later) + if (result == EncounterResult.Timeout) Log("Force restart of the game..."); await ReOpenGame(Hub.Config, token).ConfigureAwait(false); - await Task.Delay(10_000, token).ConfigureAwait(false); + await Task.Delay(ReopenDelayMs, token).ConfigureAwait(false); } } @@ -61,48 +54,73 @@ public override async Task HardStop() await base.HardStop().ConfigureAwait(false); } + private async Task TryEncounterUntilDeadline(DateTime deadline, CancellationToken token) + { + while (DateTime.Now <= deadline && !token.IsCancellationRequested) + { + await RunEncounterInput(token).ConfigureAwait(false); + + var result = await LookupSlot(token).ConfigureAwait(false); + if (result is EncounterResult.InvalidSpecies or EncounterResult.ResultFound) + return result; + + if (result == EncounterResult.VolcanionFound) + { + Log("Volcanion found in B1S1, rebooting the game..."); + return result; + } + } + + return EncounterResult.Timeout; + } + + private async Task RunEncounterInput(CancellationToken token) + { + await PressAndHold(ZL, 0_250, token).ConfigureAwait(false); + await Click(ZR, 0_100, token).ConfigureAwait(false); + await ReleaseHold(ZL, 0_250, token).ConfigureAwait(false); + } + private async Task LookupSlot(CancellationToken token) { var (pa9, raw) = await ReadRawBoxPokemon(0, 0, token).ConfigureAwait(false); - if (pa9.Species > 0 && pa9.Species != _volcanion) + + if (pa9.Species > 0 && pa9.Species != VolcanionSpecies) { Log($"Detected species {(Species)pa9.Species}, which shouldn't be possible. Only 'none' or 'Volcanion' are expected"); return EncounterResult.InvalidSpecies; } - if (pa9.Species == _volcanion && pa9 is { Valid: true, EncryptionConstant: > 0 }) + if (pa9.Species == VolcanionSpecies && pa9 is { Valid: true, EncryptionConstant: > 0 }) { var (stop, success) = await HandleEncounter(pa9, token, raw, true).ConfigureAwait(false); if (success) - Log("Your Pokémon has been received and placed in B1S1. Auto-save will do the rest!"); + Log("Your Pokemon has been received and placed in B1S1. Auto-save will do the rest!"); if (stop) return EncounterResult.ResultFound; } - return pa9.Species == _volcanion + return pa9.Species == VolcanionSpecies ? EncounterResult.VolcanionFound : EncounterResult.NextSlot; } - private async Task RepeatClick(SwitchButton button, int duration, int delay, CancellationToken token) + private async Task RepeatClick(SwitchButton button, int durationMs, int delayMs, CancellationToken token) { - var endTime = DateTime.Now.AddMilliseconds(duration); + var endUtc = DateTime.UtcNow.AddMilliseconds(durationMs); - while (DateTime.Now < endTime) - { - await Click(button, delay, token).ConfigureAwait(false); - } + while (DateTime.UtcNow < endUtc && !token.IsCancellationRequested) + await Click(button, delayMs, token).ConfigureAwait(false); } private enum EncounterResult { - Unknown, - InvalidSpecies, ResultFound, VolcanionFound, - NextSlot + NextSlot, + Timeout } } diff --git a/SysBot.Pokemon/LZA/BotFactory9LZA.cs b/SysBot.Pokemon/LZA/BotFactory9LZA.cs index 95fa6a3..29562e4 100644 --- a/SysBot.Pokemon/LZA/BotFactory9LZA.cs +++ b/SysBot.Pokemon/LZA/BotFactory9LZA.cs @@ -5,24 +5,31 @@ namespace SysBot.Pokemon.ZA; public sealed class BotFactory9LZA : BotFactory { - public override PokeRoutineExecutorBase CreateBot(PokeTradeHub hub, PokeBotState cfg) => cfg.NextRoutineType switch - { - PokeRoutineType.EncounterDiancie => new EncounterBotDiancieLZA(cfg, hub), - PokeRoutineType.EncounterFloette => new EncounterBotFloetteLZA(cfg, hub), - PokeRoutineType.EncounterVolcanion => new EncounterBotVolcanionLZA(cfg, hub), - PokeRoutineType.EncounterOverworld => new EncounterBotOverworldScannerLZA(cfg, hub), - PokeRoutineType.FossilBot => new EncounterBotFossilLZA(cfg, hub), + public override PokeRoutineExecutorBase CreateBot(PokeTradeHub hub, PokeBotState cfg) => + cfg.NextRoutineType switch + { + PokeRoutineType.EncounterDiancie => new EncounterBotDiancieLZA(cfg, hub), + PokeRoutineType.EncounterFloette => new EncounterBotFloetteLZA(cfg, hub), + PokeRoutineType.EncounterVolcanion => new EncounterBotVolcanionLZA(cfg, hub), + PokeRoutineType.EncounterGenesect => new EncounterBotGenesectLZA(cfg, hub), + PokeRoutineType.EncounterHoopa => new EncounterBotHoopaLZA(cfg, hub), + PokeRoutineType.EncounterMagearna => new EncounterBotMagearnaLZA(cfg, hub), + PokeRoutineType.EncounterOverworld => new EncounterBotOverworldScannerLZA(cfg, hub), + PokeRoutineType.FossilBot => new EncounterBotFossilLZA(cfg, hub), - PokeRoutineType.RemoteControl => new RemoteControlBotLZA(cfg), + PokeRoutineType.RemoteControl => new RemoteControlBotLZA(cfg), - _ => throw new ArgumentException(nameof(cfg.NextRoutineType)), - }; + _ => throw new ArgumentException(nameof(cfg.NextRoutineType)), + }; public override bool SupportsRoutine(PokeRoutineType type) => type switch { PokeRoutineType.EncounterDiancie => true, PokeRoutineType.EncounterFloette => true, PokeRoutineType.EncounterVolcanion => true, + PokeRoutineType.EncounterGenesect => true, + PokeRoutineType.EncounterHoopa => true, + PokeRoutineType.EncounterMagearna => true, PokeRoutineType.EncounterOverworld => true, PokeRoutineType.FossilBot => true, From 121ae3ea640868da03986634312b58f73f055030 Mon Sep 17 00:00:00 2001 From: Eppin <96589679+Eppin@users.noreply.github.com> Date: Sat, 18 Jul 2026 13:31:01 +0200 Subject: [PATCH 3/4] Added Marshadow routine --- .../BotEncounter/EncounterBotMarshadowLZA.cs | 126 ++++++++++++++++++ SysBot.Pokemon/LZA/BotFactory9LZA.cs | 2 + 2 files changed, 128 insertions(+) create mode 100644 SysBot.Pokemon/LZA/BotEncounter/EncounterBotMarshadowLZA.cs diff --git a/SysBot.Pokemon/LZA/BotEncounter/EncounterBotMarshadowLZA.cs b/SysBot.Pokemon/LZA/BotEncounter/EncounterBotMarshadowLZA.cs new file mode 100644 index 0000000..5870d93 --- /dev/null +++ b/SysBot.Pokemon/LZA/BotEncounter/EncounterBotMarshadowLZA.cs @@ -0,0 +1,126 @@ +namespace SysBot.Pokemon; + +using System; +using System.Threading; +using System.Threading.Tasks; +using Base; +using PKHeX.Core; +using static Base.SwitchButton; +using static Base.SwitchStick; + +public class EncounterBotMarshadowLZA(PokeBotState cfg, PokeTradeHub hub) : EncounterBotLZA(cfg, hub) +{ + private const ushort MarshadowSpecies = (ushort)Species.Marshadow; + + private const int EncounterTimeoutSeconds = 100; + private const int MashDurationMs = 60_000; + private const int MashDelayMs = 100; + private const int ReopenDelayMs = 10_000; + + protected override async Task EncounterLoop(SAV9ZA sav, CancellationToken token) + { + while (!token.IsCancellationRequested) + { + var deadline = DateTime.Now.AddSeconds(EncounterTimeoutSeconds); + + await EnableAlwaysCatch(token).ConfigureAwait(false); + + Log("Starting Marshadow encounter sequence"); + await SetStick(LEFT, 0, 30_000, 0_500, token).ConfigureAwait(false); + await ResetStick(token).ConfigureAwait(false); + await RepeatClick(A, MashDurationMs, MashDelayMs, token).ConfigureAwait(false); + + Log($"Catching Marshadow, wait till [{deadline}] before we force a game restart", false); + + await SetStick(LEFT, 0, 30_000, 1_000, token).ConfigureAwait(false); + await ResetStick(token).ConfigureAwait(false); + + var result = await TryEncounterUntilDeadline(deadline, token).ConfigureAwait(false); + + if (result is EncounterResult.InvalidSpecies or EncounterResult.ResultFound) + return; + + if (result == EncounterResult.Timeout) + Log("Force restart of the game..."); + + await ReOpenGame(Hub.Config, token).ConfigureAwait(false); + await Task.Delay(ReopenDelayMs, token).ConfigureAwait(false); + } + } + + public override async Task HardStop() + { + await ReleaseHold(ZL, 0_500, CancellationToken.None).ConfigureAwait(false); + await base.HardStop().ConfigureAwait(false); + } + + private async Task TryEncounterUntilDeadline(DateTime deadline, CancellationToken token) + { + while (DateTime.Now <= deadline && !token.IsCancellationRequested) + { + await RunEncounterInput(token).ConfigureAwait(false); + + var result = await LookupSlot(token).ConfigureAwait(false); + if (result is EncounterResult.InvalidSpecies or EncounterResult.ResultFound) + return result; + + if (result == EncounterResult.MarshadowFound) + { + Log("Marshadow found in B1S1, rebooting the game..."); + return result; + } + } + + return EncounterResult.Timeout; + } + + private async Task RunEncounterInput(CancellationToken token) + { + await PressAndHold(ZL, 0_250, token).ConfigureAwait(false); + await Click(ZR, 0_100, token).ConfigureAwait(false); + await ReleaseHold(ZL, 0_250, token).ConfigureAwait(false); + } + + private async Task LookupSlot(CancellationToken token) + { + var (pa9, raw) = await ReadRawBoxPokemon(0, 0, token).ConfigureAwait(false); + + if (pa9.Species > 0 && pa9.Species != MarshadowSpecies) + { + Log($"Detected species {(Species)pa9.Species}, which shouldn't be possible. Only 'none' or 'Marshadow' are expected"); + return EncounterResult.InvalidSpecies; + } + + if (pa9.Species == MarshadowSpecies && pa9 is { Valid: true, EncryptionConstant: > 0 }) + { + var (stop, success) = await HandleEncounter(pa9, token, raw, true).ConfigureAwait(false); + + if (success) + Log("Your Pokemon has been received and placed in B1S1. Auto-save will do the rest!"); + + if (stop) + return EncounterResult.ResultFound; + } + + return pa9.Species == MarshadowSpecies + ? EncounterResult.MarshadowFound + : EncounterResult.NextSlot; + } + + private async Task RepeatClick(SwitchButton button, int durationMs, int delayMs, CancellationToken token) + { + var endUtc = DateTime.UtcNow.AddMilliseconds(durationMs); + + while (DateTime.UtcNow < endUtc && !token.IsCancellationRequested) + await Click(button, delayMs, token).ConfigureAwait(false); + } + + private enum EncounterResult + { + InvalidSpecies, + ResultFound, + MarshadowFound, + NextSlot, + Timeout + } +} diff --git a/SysBot.Pokemon/LZA/BotFactory9LZA.cs b/SysBot.Pokemon/LZA/BotFactory9LZA.cs index 29562e4..bdf22a2 100644 --- a/SysBot.Pokemon/LZA/BotFactory9LZA.cs +++ b/SysBot.Pokemon/LZA/BotFactory9LZA.cs @@ -14,6 +14,7 @@ public override PokeRoutineExecutorBase CreateBot(PokeTradeHub hub, PokeBot PokeRoutineType.EncounterGenesect => new EncounterBotGenesectLZA(cfg, hub), PokeRoutineType.EncounterHoopa => new EncounterBotHoopaLZA(cfg, hub), PokeRoutineType.EncounterMagearna => new EncounterBotMagearnaLZA(cfg, hub), + PokeRoutineType.EncounterMarshadow => new EncounterBotMarshadowLZA(cfg, hub), PokeRoutineType.EncounterOverworld => new EncounterBotOverworldScannerLZA(cfg, hub), PokeRoutineType.FossilBot => new EncounterBotFossilLZA(cfg, hub), @@ -30,6 +31,7 @@ public override PokeRoutineExecutorBase CreateBot(PokeTradeHub hub, PokeBot PokeRoutineType.EncounterGenesect => true, PokeRoutineType.EncounterHoopa => true, PokeRoutineType.EncounterMagearna => true, + PokeRoutineType.EncounterMarshadow => true, PokeRoutineType.EncounterOverworld => true, PokeRoutineType.FossilBot => true, From 8c627e7113ae5b79221deb031edbea62a71d3dd8 Mon Sep 17 00:00:00 2001 From: Eppin <96589679+Eppin@users.noreply.github.com> Date: Sat, 18 Jul 2026 13:51:46 +0200 Subject: [PATCH 4/4] Updated README/documentation --- .../16.-Catch-Volcanion-Legends-Z-A.md | 34 +++++++++++++++++++ .../17.-Catch-Genesect-Legends-Z-A.md | 34 +++++++++++++++++++ .../18.-Receive-Hoopa-Legends-Z-A.md | 30 ++++++++++++++++ .../19.-Receive-Magearna-Legends-Z-A.md | 30 ++++++++++++++++ .../20.-Catch-Marshadow-Legends-Z-A.md | 34 +++++++++++++++++++ Documentation/README.md | 7 +++- README.md | 5 +++ 7 files changed, 173 insertions(+), 1 deletion(-) create mode 100644 Documentation/16.-Catch-Volcanion-Legends-Z-A.md create mode 100644 Documentation/17.-Catch-Genesect-Legends-Z-A.md create mode 100644 Documentation/18.-Receive-Hoopa-Legends-Z-A.md create mode 100644 Documentation/19.-Receive-Magearna-Legends-Z-A.md create mode 100644 Documentation/20.-Catch-Marshadow-Legends-Z-A.md diff --git a/Documentation/16.-Catch-Volcanion-Legends-Z-A.md b/Documentation/16.-Catch-Volcanion-Legends-Z-A.md new file mode 100644 index 0000000..8a60125 --- /dev/null +++ b/Documentation/16.-Catch-Volcanion-Legends-Z-A.md @@ -0,0 +1,34 @@ +# Legends: Z-A: Catch Volcanion + +This guide covers catching Volcanion until a `StopCondition` is met (e.g., shiny, specific IVs, etc.). + +## Prerequisites + +- Have access to the Volcanion encounter +- Position yourself in front of Volcanion + +## Setup + +1. Stand directly in front of Volcanion +2. **Important**: If you start the routine and the battle does not begin, take one more step forward +3. Once positioned correctly, **save your game** +4. Start the bot + +## Catch Rate Cheat + +- If you select **Master Ball**: No cheat needed (100% catch rate) +- If you select **any other ball**: Enable the 100% catch rate cheat + +## How It Works + +The bot will: +1. Initiate the battle with Volcanion +2. Attempt to catch Volcanion with your selected ball +3. Check if the caught Volcanion in B1S1 matches your stop conditions +4. If not, reset and repeat the process + +## Tips + +- Make sure your positioning is correct before starting (battle should begin immediately) +- Use stop conditions to target specific Volcanion (shiny, nature, IVs, etc.) +- Master Ball guarantees a catch without needing cheats \ No newline at end of file diff --git a/Documentation/17.-Catch-Genesect-Legends-Z-A.md b/Documentation/17.-Catch-Genesect-Legends-Z-A.md new file mode 100644 index 0000000..303f639 --- /dev/null +++ b/Documentation/17.-Catch-Genesect-Legends-Z-A.md @@ -0,0 +1,34 @@ +# Legends: Z-A: Catch Genesect + +This guide covers catching Genesect until a `StopCondition` is met (e.g., shiny, specific IVs, etc.). + +## Prerequisites + +- Have access to the Genesect encounter +- Position yourself in front of Genesect + +## Setup + +1. Stand directly in front of Genesect +2. **Important**: If you start the routine and the battle does not begin, take one more step forward +3. Once positioned correctly, **save your game** +4. Start the bot + +## Catch Rate Cheat + +- If you select **Master Ball**: No cheat needed (100% catch rate) +- If you select **any other ball**: Enable the 100% catch rate cheat + +## How It Works + +The bot will: +1. Initiate the battle with Genesect +2. Attempt to catch Genesect with your selected ball +3. Check if the caught Genesect in B1S1 matches your stop conditions +4. If not, reset and repeat the process + +## Tips + +- Make sure your positioning is correct before starting (battle should begin immediately) +- Use stop conditions to target specific Genesect (shiny, nature, IVs, etc.) +- Master Ball guarantees a catch without needing cheats \ No newline at end of file diff --git a/Documentation/18.-Receive-Hoopa-Legends-Z-A.md b/Documentation/18.-Receive-Hoopa-Legends-Z-A.md new file mode 100644 index 0000000..18325cf --- /dev/null +++ b/Documentation/18.-Receive-Hoopa-Legends-Z-A.md @@ -0,0 +1,30 @@ +# Legends: Z-A: Receive Hoopa + +This guide covers receiving Hoopa until a `StopCondition` is met (e.g., specific IVs, etc.). + +## Prerequisites + +- Have access to the Hoopa event/interactions + +## Setup + +1. Stand directly in front of the interaction that gives Hoopa +2. Make sure slot 1 of box 1 (B1S1) is empty +3. **Save your game** +4. Start the bot + +## How It Works + +The bot will: +1. Repeatedly advance dialogue/interactions to receive Hoopa +2. Check B1S1 for the received Hoopa +3. Validate Hoopa against your stop conditions +4. If no match is found, reset and repeat the process + +Since Hoopa is available in B1S1 before the auto-save is triggered, this routine can run without any cheats needed. + +## Tips + +- Make sure you are positioned correctly before starting +- Keep B1S1 empty to avoid species mismatch errors +- Use stop conditions to target specific Hoopa (shiny, nature, IVs, etc.) \ No newline at end of file diff --git a/Documentation/19.-Receive-Magearna-Legends-Z-A.md b/Documentation/19.-Receive-Magearna-Legends-Z-A.md new file mode 100644 index 0000000..dbcdff1 --- /dev/null +++ b/Documentation/19.-Receive-Magearna-Legends-Z-A.md @@ -0,0 +1,30 @@ +# Legends: Z-A: Receive Magearna + +This guide covers receiving Magearna until a `StopCondition` is met (e.g., specific IVs, etc.). + +## Prerequisites + +- Have access to the Magearna event/interactions + +## Setup + +1. Stand directly in front of the interaction that gives Magearna +2. Make sure slot 1 of box 1 (B1S1) is empty +3. **Save your game** +4. Start the bot + +## How It Works + +The bot will: +1. Repeatedly advance dialogue/interactions to receive Magearna +2. Check B1S1 for the received Magearna +3. Validate Magearna against your stop conditions +4. If no match is found, reset and repeat the process + +Since Magearna is available in B1S1 before the auto-save is triggered, this routine can run without any cheats needed. + +## Tips + +- Make sure you are positioned correctly before starting +- Keep B1S1 empty to avoid species mismatch errors +- Use stop conditions to target specific Magearna (shiny, nature, IVs, etc.) \ No newline at end of file diff --git a/Documentation/20.-Catch-Marshadow-Legends-Z-A.md b/Documentation/20.-Catch-Marshadow-Legends-Z-A.md new file mode 100644 index 0000000..e0fe75c --- /dev/null +++ b/Documentation/20.-Catch-Marshadow-Legends-Z-A.md @@ -0,0 +1,34 @@ +# Legends: Z-A: Catch Marshadow + +This guide covers catching Marshadow until a `StopCondition` is met (e.g., shiny, specific IVs, etc.). + +## Prerequisites + +- Have access to the Marshadow encounter +- Position yourself in front of Marshadow + +## Setup + +1. Stand directly in front of Marshadow +2. **Important**: If you start the routine and the battle does not begin, take one more step forward +3. Once positioned correctly, **save your game** +4. Start the bot + +## Catch Rate Cheat + +- If you select **Master Ball**: No cheat needed (100% catch rate) +- If you select **any other ball**: Enable the 100% catch rate cheat + +## How It Works + +The bot will: +1. Initiate the battle with Marshadow +2. Attempt to catch Marshadow with your selected ball +3. Check if the caught Marshadow in B1S1 matches your stop conditions +4. If not, reset and repeat the process + +## Tips + +- Make sure your positioning is correct before starting (battle should begin immediately) +- Use stop conditions to target specific Marshadow (shiny, nature, IVs, etc.) +- Master Ball guarantees a catch without needing cheats \ No newline at end of file diff --git a/Documentation/README.md b/Documentation/README.md index a7f5194..fd77062 100644 --- a/Documentation/README.md +++ b/Documentation/README.md @@ -25,4 +25,9 @@ Welcome to the Sysbot.NET wiki! 12. [Wild Zone Entrance](12.-Wild-Zone-Entrance-Legends-Z-A.md) 13. [Fossil reviving](13.-Fossil-Revive-Legends-Z-A.md) 14. [Receiving Floette](14.-Receive-Floette-Legends-Z-A.md) -15. [Catch Diancie](15.-Catch-Diancie-Legends-Z-A.md) \ No newline at end of file +15. [Catch Diancie](15.-Catch-Diancie-Legends-Z-A.md) +16. [Catch Volcanion](16.-Catch-Volcanion-Legends-Z-A.md) +17. [Catch Genesect](17.-Catch-Genesect-Legends-Z-A.md) +18. [Receive Hoopa](18.-Receive-Hoopa-Legends-Z-A.md) +19. [Receive Magearna](19.-Receive-Magearna-Legends-Z-A.md) +20. [Catch Marshadow](20.-Catch-Marshadow-Legends-Z-A.md) \ No newline at end of file diff --git a/README.md b/README.md index cdd0425..a09986b 100644 --- a/README.md +++ b/README.md @@ -19,6 +19,11 @@ These routines are new compared to the original SysBot.NET. - Fossil reviving - Receiving Floette - Catching Diancie +- Catching Volcanion +- Catching Genesect +- Receiving Hoopa +- Receiving Magearna +- Catching Marshadow #### Scarlet/Violet