diff --git a/Documentation/15.-Catch-Diancie-Legends-Z-A.md b/Documentation/15.-Catch-Diancie-Legends-Z-A.md new file mode 100644 index 00000000..2d793cf0 --- /dev/null +++ b/Documentation/15.-Catch-Diancie-Legends-Z-A.md @@ -0,0 +1,34 @@ +# Legends: Z-A: Catch Diancie + +This guide covers catching Diancie until a `StopCondition` is met (e.g., shiny, specific IVs, etc.). + +## Prerequisites + +- Have access to Diancie encounter +- Position yourself in front of Diancie + +## Setup + +1. Stand directly in front of Diancie +2. **Important**: If you start the routine and the battle doesn't 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 Diancie +2. Attempt to catch Diancie with your selected ball +3. Check if the caught Diancie matches your stop conditions +4. If not, reset and repeat the process + +## Tips + +- Make sure you're positioned correctly before starting (battle should start immediately when routine begins) +- Use stop conditions to target specific Diancie (shiny, nature, IVs, etc.) +- Master Ball guarantees a catch without needing cheats diff --git a/Documentation/README.md b/Documentation/README.md index c0b1d11e..a7f5194b 100644 --- a/Documentation/README.md +++ b/Documentation/README.md @@ -24,4 +24,5 @@ Welcome to the Sysbot.NET wiki! 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) -14. [Receiving Floette](14.-Receive-Floette-Legends-Z-A.md) \ No newline at end of file +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 diff --git a/SysBot.Base/Control/SwitchRoutineExecutor.cs b/SysBot.Base/Control/SwitchRoutineExecutor.cs index 36af1de2..5d0ac1fd 100644 --- a/SysBot.Base/Control/SwitchRoutineExecutor.cs +++ b/SysBot.Base/Control/SwitchRoutineExecutor.cs @@ -1,4 +1,4 @@ -using System.Collections.Generic; +using System.Collections.Generic; using System.Diagnostics; using System.Linq; using System.Threading; @@ -35,6 +35,18 @@ public async Task PressAndHold(SwitchButton b, int hold, int delay, Cancellation await Task.Delay(delay, token).ConfigureAwait(false); } + public async Task PressAndHold(SwitchButton b, int delay, CancellationToken token) + { + await Connection.SendAsync(SwitchCommand.Hold(b, UseCRLF), token).ConfigureAwait(false); + await Task.Delay(delay, token).ConfigureAwait(false); + } + + public async Task ReleaseHold(SwitchButton b, int delay, CancellationToken token) + { + await Connection.SendAsync(SwitchCommand.Release(b, UseCRLF), token).ConfigureAwait(false); + await Task.Delay(delay, token).ConfigureAwait(false); + } + public async Task DaisyChainCommands(int delay, IEnumerable buttons, CancellationToken token) { SwitchCommand.Configure(SwitchConfigureParameter.mainLoopSleepTime, delay, UseCRLF); diff --git a/SysBot.Pokemon/Actions/PokeRoutineType.cs b/SysBot.Pokemon/Actions/PokeRoutineType.cs index 9033f3b7..76873a65 100644 --- a/SysBot.Pokemon/Actions/PokeRoutineType.cs +++ b/SysBot.Pokemon/Actions/PokeRoutineType.cs @@ -72,6 +72,9 @@ public enum PokeRoutineType /// Retrieves Floette until the criteria is satisfied. EncounterFloette = 7_005, + /// Retrieves Diancie until the criteria is satisfied. + EncounterDiancie = 7_006, + /// Keeps running circles until all party members have the Partner mark. PartnerMark = 7_050, diff --git a/SysBot.Pokemon/LZA/BotEncounter/EncounterBotDiancieLZA.cs b/SysBot.Pokemon/LZA/BotEncounter/EncounterBotDiancieLZA.cs new file mode 100644 index 00000000..c6bc195f --- /dev/null +++ b/SysBot.Pokemon/LZA/BotEncounter/EncounterBotDiancieLZA.cs @@ -0,0 +1,113 @@ +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 EncounterBotDiancieLZA(PokeBotState cfg, PokeTradeHub hub) : EncounterBotLZA(cfg, hub) +{ + private readonly ushort _diancie = (ushort)Species.Diancie; + private readonly ushort _carbink = (ushort)Species.Carbink; + + protected override async Task EncounterLoop(SAV9ZA sav, CancellationToken token) + { + const int maxDurationSeconds = 45; + + while (!token.IsCancellationRequested) + { + var later = DateTime.Now.AddSeconds(maxDurationSeconds); + await EnableAlwaysCatch(token).ConfigureAwait(false); + + Log("Starting Diancie encounter sequence"); + await SetStick(LEFT, 0, 30_000, 0_500, token).ConfigureAwait(false); + await ResetStick(token).ConfigureAwait(false); + await RepeatClick(A, 20_000, 100, token).ConfigureAwait(false); + + Log($"Catching Diancie, wait till [{later}] before we force a game restart", false); + + var result = EncounterResult.Unknown; + while (result != EncounterResult.DiancieFound && 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); + + for (var slot = 0; slot < 3; slot++) + { + result = await LookupSlot(slot, token).ConfigureAwait(false); + + if (result is EncounterResult.InvalidSpecies or EncounterResult.ResultFound) return; // Exit the encounter loop entirely + + if (result is EncounterResult.DiancieFound) + { + Log($"Diancie found in B1S{slot + 1}, 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(int slot, CancellationToken token) + { + (var pa9, var raw) = await ReadRawBoxPokemon(0, slot, token).ConfigureAwait(false); + if (pa9.Species > 0 && pa9.Species != _diancie && pa9.Species != _carbink) + { + Log($"Detected species {(Species)pa9.Species}, which shouldn't be possible. Only 'none', 'Carbink' or 'Diancie' are expected"); + return EncounterResult.InvalidSpecies; + } + + if (pa9.Species == _diancie && pa9.Valid && pa9.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 B1S{slot + 1}. Auto-save will do the rest!"); + } + + if (stop) + return EncounterResult.ResultFound; + } + + return pa9.Species == _diancie + ? EncounterResult.DiancieFound + : 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, + DiancieFound, + NextSlot + } +} diff --git a/SysBot.Pokemon/LZA/BotEncounter/EncounterBotLZA.cs b/SysBot.Pokemon/LZA/BotEncounter/EncounterBotLZA.cs index 0cce5bb0..a16938be 100644 --- a/SysBot.Pokemon/LZA/BotEncounter/EncounterBotLZA.cs +++ b/SysBot.Pokemon/LZA/BotEncounter/EncounterBotLZA.cs @@ -190,4 +190,21 @@ 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 } + + protected async Task EnableAlwaysCatch(CancellationToken token) + { + if (!Hub.Config.EncounterLZA.EnableCatchCheat) + return; + + Log("Enable 100% catch rate cheat", false); + // Source: https://gbatemp.net/threads/pokemon-legends-z-a-cheat-database.675579/ + + // Original cheat: + /* + * [№56. 100% Catch Rate] + * 040A0000 00447448 52800028 + */ + + await SwitchConnection.WriteBytesMainAsync(BitConverter.GetBytes(0x52800028), 0x00447448, token); + } } diff --git a/SysBot.Pokemon/LZA/BotEncounter/EncounterSettingsLZA.cs b/SysBot.Pokemon/LZA/BotEncounter/EncounterSettingsLZA.cs index 25c0536a..792d0e76 100644 --- a/SysBot.Pokemon/LZA/BotEncounter/EncounterSettingsLZA.cs +++ b/SysBot.Pokemon/LZA/BotEncounter/EncounterSettingsLZA.cs @@ -44,6 +44,9 @@ public class OverworldEncounterLZA [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 100% catch cheat will be enabled.")] + public bool EnableCatchCheat { get; set; } + [Category(Encounter), Description("When enabled, the screen will be turned off during normal bot loop operation to save power.")] public bool ScreenOff { get; set; } diff --git a/SysBot.Pokemon/LZA/BotFactory9LZA.cs b/SysBot.Pokemon/LZA/BotFactory9LZA.cs index b5940e4f..fa8a03e3 100644 --- a/SysBot.Pokemon/LZA/BotFactory9LZA.cs +++ b/SysBot.Pokemon/LZA/BotFactory9LZA.cs @@ -7,6 +7,7 @@ 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.EncounterOverworld => new EncounterBotOverworldScannerLZA(cfg, hub), PokeRoutineType.FossilBot => new EncounterBotFossilLZA(cfg, hub), @@ -18,6 +19,7 @@ public sealed class BotFactory9LZA : BotFactory public override bool SupportsRoutine(PokeRoutineType type) => type switch { + PokeRoutineType.EncounterDiancie => true, PokeRoutineType.EncounterFloette => true, PokeRoutineType.EncounterOverworld => true, PokeRoutineType.FossilBot => true, diff --git a/SysBot.Pokemon/LZA/Vision/PokeDataOffsetsLZA.cs b/SysBot.Pokemon/LZA/Vision/PokeDataOffsetsLZA.cs index 3561deae..f73227e3 100644 --- a/SysBot.Pokemon/LZA/Vision/PokeDataOffsetsLZA.cs +++ b/SysBot.Pokemon/LZA/Vision/PokeDataOffsetsLZA.cs @@ -7,14 +7,14 @@ namespace SysBot.Pokemon; /// public class PokeDataOffsetsLZA { - public const string ZAGameVersion = "2.0.0"; + public const string ZAGameVersion = "2.0.1"; public const string LegendsZAID = "0100F43008C44000"; - public IReadOnlyList BoxStartPokemonPointer { get; } = [0x6105710, 0xB0, 0x978, 0x0]; - public IReadOnlyList MyStatusPointer { get; } = [0x6105710, 0x80, 0x100]; - public IReadOnlyList KItemPointer { get; } = [0x6105670, 0x30, 0x08, 0x460]; - public IReadOnlyList KOverworldPointer { get; } = [0x6105670, 0x30, 0x08, 0x9E0]; - public IReadOnlyList KStoredShinyEntityPointer { get; } = [0x6105670, 0x30, 0x08, 0x1660]; + public IReadOnlyList BoxStartPokemonPointer { get; } = [0x610A710, 0xB0, 0x978, 0x0]; + public IReadOnlyList MyStatusPointer { get; } = [0x610A710, 0x80, 0x100]; + public IReadOnlyList KItemPointer { get; } = [0x610A670, 0x30, 0x08, 0x480]; + public IReadOnlyList KOverworldPointer { get; } = [0x610A670, 0x30, 0x08, 0xA00]; + public IReadOnlyList KStoredShinyEntityPointer { get; } = [0x610A670, 0x30, 0x08, 0x1680]; public const uint KItemKey = 0x21C9BD44; public const uint KOverworldKey = 0x5E8E1711; diff --git a/SysBot.Pokemon/Settings/FolderSettings.cs b/SysBot.Pokemon/Settings/FolderSettings.cs index c38100dc..2391224c 100644 --- a/SysBot.Pokemon/Settings/FolderSettings.cs +++ b/SysBot.Pokemon/Settings/FolderSettings.cs @@ -1,4 +1,4 @@ -using System.ComponentModel; +using System.ComponentModel; using System.IO; namespace SysBot.Pokemon; @@ -10,10 +10,10 @@ public class FolderSettings : IDumper public override string ToString() => "Folder / Dumping Settings"; [Category(FeatureToggle), Description("When enabled, dumps any received PKM files (trade results) to the DumpFolder.")] - public bool Dump { get; set; } + public bool Dump { get; set; } = false; [Category(FeatureToggle), Description("When enabled, dumps any raw/encrypted received PKM files (trade results) to the DumpFolder.")] - public bool DumpRaw { get; set; } + public bool DumpRaw { get; set; } = true; [Category(FeatureToggle), Description("When enabled with previous option, dumps only any received Shiny PKM files (trade results) to the DumpFolder.")] public bool DumpShinyOnly { get; set; }