Skip to content

MewTracker/bdsp-research

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

1 Commits
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

BDSP Research

License: GPL v3

This repository contains notes and tools developed during my research of Pokémon Brilliant Diamond/Shining Pearl games for Nintendo Switch. Everything provided here is meant for learning purposes and tools are proof of concept quality rather than actual user-friendly software, so please keep that in mind. However I do welcome questions, suggestions and contributions. Unless stated otherwise all research is based on Pokémon Shining Pearl 1.1.1.

0. Intro and general information

BDSP games were made using Unity game engine. This means a lot of information usually lost during compilation process can be recovered. If you aren't familiar with the process of reversing Unity games, key tools you can use are:

  • dnSpy - for viewing .NET assemblies (type information, names of classes, methods, etc.).
  • Il2CppDumper - to generate "fake" .NET assembly with all types, classes, methods (for viewing in dnSpy) but without actual implementations which are compiled to native AArch64 code. Besides mentioned assembly this tool will generate dump of all information you can later apply under IDA Pro/Ghidra.
  • IDA Pro/Ghidra with NSO loader (IDA Pro Loader, Ghidra Loader) - for actual reversing of interesting methods. This process is greatly simplified thanks to metadata generated by Il2CppDumper.

1. RNG of wild Pokémon encounters

Let's say you want to catch a shiny Ditto with 6 perfect IVs. What are the odds?

Perfect IV chance: 1/32
Shiny chance: 1/4096
6 IV shiny chance: (1/32)^6 * (1/4096) = 0,00000000000022737368 -> that's 1 in 4 398 046 511 104

That's rather... unlikely to ever happen, so let's explore other options of obtaining such Pokémon. One can use PKHeX to edit/create such Pokémon or use publicly available cheat codes to alter game's logic but those options aren't fun. I wanted a "perfectly legit" Pokémon, so let's investigate how Pokémon are generated during random encounters (e.g. in tall grass, while surfing). The idea is to learn how Pokémon with desired features can be generated by the game itself and find correct PRNG state that yields desired Pokémon. I will call this state "a perfect seed". Such perfect seed can be injected into our game at correct moment to control Pokémon we get. It's a data-only modification which doesn't alter logic of the game. Let's discuss PRNG algorithm used in BDSP.

PRNG

There are two types of generators used in BDSP:

  • Xorshift128 - provided by Unity's Random class
  • Xoroshiro128+ - also used in other Pokémon games e.g. Sword/Shield

In this case we'll be interested in Xorshift128 implementation only because that's the one used for random encounters. As described in Unity documentation there's only one instance of this generator with global 128-bit state. BDSP calls it via Pml.Local.Random$$GetValue function which calls UnityEngine.Random$$Range_485374058912. This functions clamps output of standard Xorshift128 to user-defined range. In BDSP that range is hardcoded to be from 0x80000000 to 0x7FFFFFFF.

Reversing

If you look around in dnSpy you can find several classes and types used to handle critical Pokémon structures. Here's a call tree we'll need to investigate in depth (organized in caller -> callee order):

Dpr.EncountTools$$CreateSimpleParty - creates a team with a single Pokémon in it, this gets called when we encounter a wild Pokémon
    Pml.PokePara.InitialSpec$$.ctor - constructor for empty InitialSpec structure which serves as a blueprint for basic parameters of a Pokémon, it's the most important structure in the process, constructor itself isn't interesting as real values will be filled in much later
    Pml.PokePara.PokemonParam$$.ctor_485369492432(InitialSpec) - initializes PokemonParam structure based on InitialSpec
        Pml.PokePara.CoreParam$$.ctor_485369459280(InitialSpec)
            Pml.PokePara.Factory$$CreateCoreData_485369459488(InitialSpec) - this function accepts our InitialSpec and creates new "fixed" (filled with actual values) based on it, from now on InitialSpec passed to this function will be called "OldInitialSpec"
                Pml.PokePara.Factory$$FixInitSpec(NewInitialSpec, OldInitialSpec) - all the magic happens here, old spec is copied to the new one and then new one gets filled with randomly-generated values we want to manipulate

Before we jump into Pml.PokePara.Factory$$FixInitSpec let's take a peek at InitialSpec structure itself to get a better understanding of what needs to be set from RNG (and manipulated later). I annotated some fields with more user-friendly names (taken from PKHeX) and offsets to matching fields inside final Pokémon data structure (which is organized into different blocks).

[FieldOffset(Offset = "0x10")]
public ulong randomSeed;

[FieldOffset(Offset = "0x18")]
public bool isRandomSeedEnable; -> this flag decides which generator should be used, we are interested in random encounters, so this will be always false resulting in Xorshift128 usage

[FieldOffset(Offset = "0x20")]
public ulong personalRnd;       -> EncryptionConstant(0x00)

[FieldOffset(Offset = "0x28")]
public ulong rareRnd;           -> PID(BlockA + 0x14)

[FieldOffset(Offset = "0x30")]
public ulong id;                -> TID(BlockA + 0x04)

[FieldOffset(Offset = "0x38")]
public MonsNo monsno;           -> Species(BlockA + 0x00)

[FieldOffset(Offset = "0x3C")]
public ushort formno;

[FieldOffset(Offset = "0x3E")]
public ushort level;

[FieldOffset(Offset = "0x40")]
public ushort sex;

[FieldOffset(Offset = "0x42")]
public ushort seikaku;

[FieldOffset(Offset = "0x44")]
public byte tokuseiIndex;

[FieldOffset(Offset = "0x45")]
public byte rareTryCount;

[FieldOffset(Offset = "0x48")]
public ushort[] talentPower;    -> IV32(BlockB + 0x34)

[FieldOffset(Offset = "0x50")]
public uint friendship;

[FieldOffset(Offset = "0x54")]
public byte talentVNum;

[FieldOffset(Offset = "0x56")]
public ushort weight;

[FieldOffset(Offset = "0x58")]
public ushort height;

Pml.PokePara.Factory$$FixInitSpec calls RNG and fills fields in following order:

  • personalRnd - EncryptionConstant
  • id - TID (Trainer ID)
  • rareRnd * rareTryCount - PID (Personality value, rareTryCount == 1 in our case, this feature is used to boost shiny chance e.g. for shiny eggs with shiny charm item)
  • talentPower - 6 IVs (HP, Atk, Def, SpAtk, SpDef, Speed/Agi), only lowest 5 bits used (0-31)

Getting perfect IVs seems simple now. It's a matter of brute-forcing RNG algorithm until we find a sequence where 4th, 5th, 6th, 7th, 8th and 9th generated value is equal to 31 (after masking). What about shininess though? How is that determined? The answer is inside Pml.PolePara.CalcTool$$IsRareColor(TID, PID) which looks like this after rewriting it in C:

bool Pml.PolePara.CalcTool$$IsRareColor(uint32_t TID, uint32_t PID)
{
    return ((TID >> 16) ^ (TID & 0xFFF0) ^ (PID >> 16) ^ (PID & 0xFFF0)) < 16;
}

Ok, that seems simple. It's based on random personality value we've just generated and trainer ID we've just generated?! That might seem odd, because after catching a Pokémon its TID value will change to TID of our player and that would affect shininess. To counter that, game tracks if a Pokémon is shiny and corrects it's personality value to match new TID using Pml.PokePara.CalcTool$$CorrectColorRndForRare(TID, PID). Undestanding this process isn't important for us but for the sake of completeness here's a reversed function:

uint32_t Pml.PokePara.CalcTool$$CorrectColorRndForRare(uint32_t TID, uint32_t PID)
{
    uint32_t Tmp = (TID >> 16) ^ TID ^ PID;
    return (PID & 0x0000FFFF) | (Tmp << 16);
}

Tooling

Now with all pieces of the puzzle in place it's time to calculate some perfect seeds and inject them into the game. As you may recall it's a 1 in 4398 billion chance, so those computations can take some time. To perform them I wrote BDSPCalc tool which went through a few iterations of development and each one uses a different technology to compute things faster:

  • BDSPCalc (chosen by compile-time option, optionally multi-threaded) - first naive implementation, basic CPU operations only, it's fast enough to find 6 IV Pokémon but not 6 IV shiny Pokémon, I used it for testing correctness of other implementations
  • BDSPCalc AVX2 (chosen by compile-time option, optionally multi-threaded) - uses AVX2 instruction set to run 8 generators in parallel (per thread), making things A LOT faster (way over 8 times faster because using AVX2 is already a speed boost)
  • BDSPCalc_CUDA - NVIDIA CUDA implementation that offloads computation process to GPU, reduces whole process to minutes

For best performance with CUDA implementation you should adjust THREAD_COUNT to utilize almost all CUDA cores of your GPU. I prefer to use MAX_CORES - 256. You can also disable TDR (Link 1, Link 2) and set KERNEL_ITERATIONS to a large number (e.g. a billion). This will reduce host-device communication but will also make your PC freeze completely until computation is finished. TDR shouldn't be an issue if you are using another GPU as your primary one (e.g. CPU-integrated). I don't recommend messing with TDR though because performance impact is minimal, so just adjust KERNEL_ITERATIONS to stay below standard 2 second time limit with your kernel execution.

Do not forget to adjust your STATE_SEED to something random!

State injection

Got your seeds? The final task is to inject them. There are different ways to achieve that and it also depends if you are using an emulator or actual hardware. What I prefer to do, is to use this little cheat code to inject PRNG state every time FixInitSpec is called (remove the comments before using):

[Inject PRNG State]
04000000 02469B34 97ADF76B ; BL GetXorShift128StateAddress
04000000 02469B38 10000001 ; ADR X1, .
04000000 02469B3C B94E1C22 ; LDR W2, [X1,#0xE1C]
04000000 02469B40 B94E2023 ; LDR W3, [X1,#0xE20]
04000000 02469B44 29000C02 ; STP W2, W3, [X0]
04000000 02469B48 B94E2422 ; LDR W2, [X1,#0xE24]
04000000 02469B4C B94F4023 ; LDR W3, [X1,#0xF40]
04000000 02469B50 29010C02 ; STP W2, W3, [X0,#8]
04000000 02469B54 1400000F ; B random_seed_is_disabled
04000000 0246A954 AAAAAAAA ; State0
04000000 0246A958 BBBBBBBB ; State1
04000000 0246A95C CCCCCCCC ; State2
04000000 0246AA78 DDDDDDDD ; State3

It's a rock-solid method (no timing issues) but the problem is that it overwrites some code inside FixInitSpec disabling Xoroshiro128+ completely, so everything that uses it (e.g. eggs) will be broken until you deactivate the cheat. I hope to find a better method in the future. That's all, enjoy your perfect Pokémon ;)

About

No description, website, or topics provided.

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published