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