Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@
#include "Inference/PokemonFRLG_TrainerIdReader.h"
#include "Inference/PokemonFRLG_WildEncounterReader.h"
#include "PokemonFRLG_Navigation.h"
#include "Programs/RngManipulation/PokemonFRLG_RngPlanTracker_Tests.h"
#include "Programs/RngManipulation/PokemonFRLG_RngTargetSearch_Tests.h"
#include "Tests/TestUtils.h"

Expand Down Expand Up @@ -290,6 +291,7 @@ void add_tests(UnitTestDatabase& database){
add_tests_BagDetector(database);
add_tests_ExitWildBattle(database);
add_tests_RngTargetSearch(database);
add_tests_RngPlanTracker(database);
}


Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,159 @@
/* RNG Plan
*
* From: https://github.com/PokemonAutomation/
*
*/

#include <algorithm>
#include <cstdlib>
#include "VideoGames/PokemonFRLG/Programs/RngManipulation/PokemonFRLG_BlindNavigation.h"
#include "VideoGames/PokemonFRLG/Programs/RngManipulation/PokemonFRLG_RngCalibration.h"
#include "PokemonFRLG_RngPlan.h"

namespace PokemonAutomation{
namespace NintendoSwitch{
namespace PokemonFRLG{

using namespace Pokemon;


std::vector<SidCandidate> sid_helper_candidates(uint16_t tid, uint32_t target_advances, uint8_t count){
// As SidHelper's get_sid_messages(): from "count / 2" steps before the target.
const int start = -int(count) / 2;
const int end = start + int(count);
const int farthest = std::max(-start, end - 1);

std::vector<SidCandidate> candidates;
AdvRngSearcher searcher(tid, uint64_t(int64_t(target_advances) + 2 * start));
for (int i = start; i < end; i++){
candidates.push_back({uint16_t(searcher.state.s0 >> 16), double(farthest + 1 - std::abs(i))});
searcher.advance_state();
searcher.advance_state();
}
return candidates;
}


namespace{

// "targets" with only the shiny candidates still "left", and their share of
// the weight left.
std::vector<RngTarget> rescore(
const std::vector<RngTarget>& targets,
const std::vector<SidCandidate>& candidates,
const std::vector<bool>& left
){
double total = 0;
for (size_t c = 0; c < candidates.size(); c++){
if (left[c]){
total += candidates[c].weight;
}
}
std::vector<RngTarget> scored;
scored.reserve(targets.size());
for (const RngTarget& target : targets){
RngTarget copy = target;
copy.shiny_for.clear();
double weight = 0;
for (const ShinyCandidate& shiny : target.shiny_for){
if (left[shiny.candidate]){
copy.shiny_for.push_back(shiny);
weight += candidates[shiny.candidate].weight;
}
}
copy.shiny_probability = total > 0 ? weight / total : 0;
scored.emplace_back(std::move(copy));
}
return scored;
}

RngAttempt make_attempt(const RngTarget& target, const std::vector<SidCandidate>& candidates){
RngAttempt attempt{target, {}};
for (const ShinyCandidate& shiny : target.shiny_for){
attempt.sids.push_back(candidates[shiny.candidate].sid);
}
return attempt;
}

}


std::vector<RngAttempt> plan_rng_attempts(
const std::vector<RngTarget>& targets,
const std::vector<SidCandidate>& candidates,
bool shiny_required,
size_t max_attempts
){
std::vector<RngAttempt> attempts;
if (targets.empty() || max_attempts == 0){
return attempts;
}
std::vector<bool> left(candidates.size(), true);

if (!shiny_required){
// With no shiny chance, rank_rng_targets() sorts by time alone.
std::vector<RngTarget> by_time = targets;
for (RngTarget& target : by_time){
target.shiny_probability = 0;
}
rank_rng_targets(by_time);
const uint32_t quickest = by_time.front().state;
for (const RngTarget& target : rescore(targets, candidates, left)){
if (target.state == quickest){
attempts.push_back(make_attempt(target, candidates));
break;
}
}
return attempts;
}

while (attempts.size() < max_attempts){
std::vector<RngTarget> scored = rescore(targets, candidates, left);
rank_rng_targets(scored);
const RngTarget& best = scored.front();
if (best.shiny_for.empty()){
break;
}
for (const ShinyCandidate& shiny : best.shiny_for){
left[shiny.candidate] = false;
}
attempts.push_back(make_attempt(best, candidates));
if (std::find(left.begin(), left.end(), true) == left.end()){
break;
}
}
return attempts;
}


int64_t starter_rng_seed_wait_ms(const InitialSeedEntry& seed){
return int64_t(seed.seed_time_ms) + STARTER_RNG_FIXED_SEED_OFFSET_MS;
}

bool starter_rng_can_wait_for(const InitialSeedEntry& seed){
return starter_rng_seed_wait_ms(seed) >= STARTER_RNG_MIN_SEED_WAIT_MS;
}

int64_t starter_rng_ingame_ms(uint64_t advances){
const int64_t frames = int64_t(advances) - int64_t(STARTER_RNG_CONTINUE_SCREEN_FRAMES)
+ fixed_advances_offset(PokemonFRLG_RngTarget::starters);
return int64_t(frames * FRLG_FRAME_DURATION / 2);
}

std::vector<RngAttempt> plan_starter_rng(const StarterRngPlanInputs& inputs){
const SeedsDatabase database(seeds_json_path(inputs.firered, inputs.language));
// Only the English lists hold the blackout button's seeds (initial_seed_entries()).
const bool english_list = inputs.language != Language::Japanese;
std::vector<InitialSeedEntry> seeds = initial_seed_entries(database, inputs.sound, "Help", english_list);
std::erase_if(seeds, [](const InitialSeedEntry& seed){ return !starter_rng_can_wait_for(seed); });

RngTargetFilters filters = inputs.filters;
filters.min_advances = std::max(filters.min_advances, STARTER_RNG_PLAN_MIN_ADVANCES);
const std::vector<RngTarget> targets = search_rng_targets(seeds, inputs.tid, inputs.candidates, filters);
return plan_rng_attempts(targets, inputs.candidates, filters.shiny_required, inputs.max_attempts);
}


}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
/* RNG Plan
*
* From: https://github.com/PokemonAutomation/
*
* Which Starter RNG targets to ask for, in order, when the SID is
* only narrowed down to SID Helper's candidates, and what to do after each
* one. The loop is the one PokemonFRLG_RngTargetSearch.h describes:
* - Shiny, and the target was hit: the SID is one of those the target is
* shiny for. With one left, it is confirmed and later targets can be
* searched with it alone.
* - Shiny, but not the target (another advance): it says nothing about
* the SID. The plan ends with the shiny.
* - Nature and IVs match, but not shiny: the target was hit, so the SIDs it
* is shiny for are wrong. Go on to the next attempt.
* - Nature or IVs differ: the target was missed. Run it again, up to the
* retries, then go on to the next attempt.
*
*
*/

#ifndef PokemonAutomation_PokemonFRLG_RngPlan_H
#define PokemonAutomation_PokemonFRLG_RngPlan_H

#include <cstdint>
#include <string>
#include <vector>
#include "CommonFramework/Language.h"
#include "VideoGames/PokemonFRLG/Programs/RngManipulation/PokemonFRLG_RngTargetSearch.h"
#include "VideoGames/PokemonFRLG/Programs/RngManipulation/PokemonFRLG_SeedsDatabase.h"

namespace PokemonAutomation{
namespace NintendoSwitch{
namespace PokemonFRLG{


// The SIDs SID Helper lists for "tid": "count" of them, 2 advances apart,
// centered on "target_advances", as its get_sid_messages() lists them. The
// middle one, where SID Helper aims, weighs the most, and each step away one
// less, so the outermost weigh 1.
std::vector<SidCandidate> sid_helper_candidates(uint16_t tid, uint32_t target_advances, uint8_t count);


struct RngAttempt{
RngTarget target;
// The SIDs the target is shiny for, of those the attempts before it
// leave. Empty when the target isn't shiny for any of them.
std::vector<uint16_t> sids;
};

// The attempts to make, in order, for SID candidates "candidates" (the ones
// "targets" were searched with). Each is the best target by
// rank_rng_targets() over the candidates the attempts before it leave, so a
// target shiny for several candidates comes before one shiny for one, unless
// it takes much longer. Stops after "max_attempts", or once no target is
// shiny for a candidate left.
//
// With "shiny_required" off, a hit ends the plan whether it is shiny or not,
// so the plan is the one quickest target.
std::vector<RngAttempt> plan_rng_attempts(
const std::vector<RngTarget>& targets,
const std::vector<SidCandidate>& candidates,
bool shiny_required,
size_t max_attempts
);


// What Starter RNG's targets are searched with.
struct StarterRngPlanInputs{
bool firered;
Language language;
SoundSetting sound;
uint16_t tid;
std::vector<SidCandidate> candidates;
RngTargetFilters filters;
size_t max_attempts;
};

// Starter RNG's smallest Advances setting.
inline constexpr uint64_t STARTER_RNG_MIN_ADVANCES = 940;

// Starter RNG's title screen wait, before calibration: the seed list's
// delay plus this (FIXED_SEED_OFFSET in StarterRng's attempts).
inline constexpr int64_t STARTER_RNG_FIXED_SEED_OFFSET_MS = -845;
// The shortest title screen wait the plan uses. Starter RNG refuses waits
// under 29,650 ms, and its docs (pokemonautomation.github.io,
// docs/Programs/PokemonFRLG/StarterRng.md, "Avoid low seed delays when
// possible") warn that waits near 30,500 ms can miss the title screen
// press. 31,500 ms keeps a second clear of that for the seed calibration.
inline constexpr int64_t STARTER_RNG_MIN_SEED_WAIT_MS = 31500;
// Starter RNG's title screen wait for `seed`, as prepare_timings() makes it
// before calibration.
int64_t starter_rng_seed_wait_ms(const InitialSeedEntry& seed);
// Whether the wait is at least STARTER_RNG_MIN_SEED_WAIT_MS. (Console B,
// 2026-11-02, FireRed English: seeds 11A3 and 405C, at 30,473 and
// 30,488 ms in the table, are 29,628 and 29,643 ms waits, which Starter
// RNG refuses.)
bool starter_rng_can_wait_for(const InitialSeedEntry& seed);

// Starter RNG's Continue Screen frames, taken off the advances before the
// in-game wait (CONTINUE_SCREEN_FRAMES in StarterRng's attempts).
inline constexpr uint64_t STARTER_RNG_CONTINUE_SCREEN_FRAMES = 200;
// Starter RNG's in-game wait for `advances`, before calibration. It
// refuses one under 7,500 ms, which STARTER_RNG_MIN_ADVANCES just makes.
int64_t starter_rng_ingame_ms(uint64_t advances);
// Room for the in-game calibration, in advances (2 per frame of wait): the
// calibration moves the wait, and it went to -720 after one miss on console
// B (2026-11-02, seed B579 at 967 advances: an 8 s wait cut to 1.7 s). The
// plan's targets are at least this many advances past Starter RNG's
// minimum. To tune from the logs' calibrations.
inline constexpr uint64_t STARTER_RNG_INGAME_MARGIN_ADVANCES = 720;
inline constexpr uint64_t STARTER_RNG_PLAN_MIN_ADVANCES = STARTER_RNG_MIN_ADVANCES + STARTER_RNG_INGAME_MARGIN_ADVANCES;

// The attempts for Starter RNG, from the seed list of the version and
// language in Help button mode, the one Starter RNG uses.
std::vector<RngAttempt> plan_starter_rng(const StarterRngPlanInputs& inputs);


}
}
}
#endif
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
/* RNG Plan Test Targets
*
* From: https://github.com/PokemonAutomation/
*
*/

#include "PokemonFRLG_RngPlanTestTargets.h"

namespace PokemonAutomation{
namespace NintendoSwitch{
namespace PokemonFRLG{

using namespace Pokemon;


namespace RngPlanTestTargets{


std::string join(const std::vector<uint16_t>& values){
std::string text;
for (uint16_t value : values){
text += (text.empty() ? "" : ",") + std::to_string(value);
}
return "{" + text + "}";
}


// SIDs 100, 200 and 300 weigh 3, 2 and 1.
const std::vector<SidCandidate> CANDIDATES = {{100, 3}, {200, 2}, {300, 1}};

// A target reached "seconds" after the reset, shiny for the candidates at
// "shiny" (indices into CANDIDATES).
RngTarget make_target(uint32_t state, uint64_t seconds, std::vector<size_t> shiny){
RngTarget target{};
target.state = state;
target.seed = {uint16_t(state), seconds * 1000, SeedButton::A, BlackoutButton::None};
target.advances = 0;
target.pokemon = method1_pokemon(state);
for (size_t candidate : shiny){
target.shiny_for.push_back({candidate, AdvShinyType::Star});
}
return target;
}

// By expected wait per shiny over all three (weight 6): 1 = 50 s / (5/6) = 60,
// 2 = 20 s / (1/6) = 120, 3 = 40 s / (3/6) = 80. Then over SID 300 alone, 2 is
// the only one shiny for it. 4 is the quickest and shiny for none.
const std::vector<RngTarget> TARGETS = {
make_target(1, 50, {0, 1}),
make_target(2, 20, {2}),
make_target(3, 40, {0}),
make_target(4, 5, {}),
};


}


}
}
}
Loading
Loading