Optional high-quality RNG (#3780)

High-quality RNG, behind the HQ_RANDOM flag, enabled by default

Makes the shuffle test error a bit more tolerant
This commit is contained in:
tertu 2023-12-22 11:39:15 -06:00 committed by GitHub
parent 63316daf30
commit fb28ce50ae
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
20 changed files with 324 additions and 50 deletions

View file

@ -15,6 +15,7 @@
#include "pokeball.h"
#include "battle_debug.h"
#include "battle_dynamax.h"
#include "random.h" // for rng_value_t
// Used to exclude moves learned temporarily by Transform or Mimic
#define MOVE_IS_PERMANENT(battler, moveSlot) \
@ -580,6 +581,13 @@ struct LostItem
u16 stolen:1;
};
#if HQ_RANDOM == TRUE
struct BattleVideo {
u32 battleTypeFlags;
rng_value_t rngSeed;
};
#endif
struct BattleStruct
{
u8 turnEffectsTracker;
@ -651,7 +659,12 @@ struct BattleStruct
u16 lastTakenMoveFrom[MAX_BATTLERS_COUNT][MAX_BATTLERS_COUNT]; // a 2-D array [target][attacker]
union {
struct LinkBattlerHeader linkBattlerHeader;
#if HQ_RANDOM == FALSE
u32 battleVideo[2];
#else
struct BattleVideo battleVideo;
#endif
} multiBuffer;
u8 wishPerishSongState;
u8 wishPerishSongBattlerId;

View file

@ -79,5 +79,6 @@
#define EXPANSION_INTRO TRUE // If TRUE, a custom RHH intro will play after the vanilla copyright screen.
#define POKEDEX_PLUS_HGSS FALSE // If TRUE, enables the custom HGSS style Pokedex.
#define SUMMARY_SCREEN_NATURE_COLORS TRUE // If TRUE, nature-based stat boosts and reductions will be red and blue in the summary screen.
#define HQ_RANDOM TRUE // If TRUE, replaces the default RNG with an implementation of SFC32 RNG. May break code that relies on RNG.
#endif // GUARD_CONFIG_H

View file

@ -3,6 +3,7 @@
#include "palette.h"
#include "constants/contest.h"
#include "random.h" // for rng_value_t
enum
{
@ -327,7 +328,7 @@ extern struct ContestResources *gContestResources;
extern struct ContestWinner gCurContestWinner;
extern u8 gCurContestWinnerIsForArtist;
extern u8 gCurContestWinnerSaveIdx;
extern u32 gContestRngValue;
extern rng_value_t gContestRngValue;
// contest.c
void ResetLinkContestBoolean(void);

View file

@ -1,8 +1,74 @@
#ifndef GUARD_RANDOM_H
#define GUARD_RANDOM_H
extern u32 gRngValue;
extern u32 gRng2Value;
// The number 1103515245 comes from the example implementation of rand and srand
// in the ISO C standard.
#define ISO_RANDOMIZE1(val)(1103515245 * (val) + 24691)
#define ISO_RANDOMIZE2(val)(1103515245 * (val) + 12345)
/* Some functions have been added to support HQ_RANDOM.
*
* If using HQ_RANDOM, you cannot call Random() in interrupt handlers safely.
* AdvanceRandom() is provided to handle burning numbers in the VBlank handler
* if you choose to do that, and can be used regardless of HQ_RANDOM setting.
* If you need to use random numbers in the VBlank handler, a local state
* should be used instead.
*
* LocalRandom(*val) allows you to have local random states that are the same
* type as the global states regardless of HQ_RANDOM setting, which is useful
* if you want to be able to set them from or assign them to gRngValue.
*
* Random2_32() was added to HQ_RANDOM because the output of the generator is
* always 32 bits and Random()/Random2() are just wrappers in that mode. It is
* also available in non-HQ mode for consistency.
*/
#if HQ_RANDOM == TRUE
struct Sfc32State {
u32 a;
u32 b;
u32 c;
u32 ctr;
};
typedef struct Sfc32State rng_value_t;
#define RNG_VALUE_EMPTY {}
// Calling this function directly is discouraged.
// Use LocalRandom() instead.
static inline u32 _SFC32_Next(struct Sfc32State *state)
{
const u32 result = state->a + state->b + state->ctr++;
state->a = state->b ^ (state->b >> 9);
state->b = state->c * 9;
state->c = result + ((state->c << 21) | (state->c >> 11));
return result;
}
static inline u16 LocalRandom(rng_value_t *val)
{
return _SFC32_Next(val) >> 16;
}
u32 Random32(void);
u32 Random2_32(void);
static inline u16 Random(void)
{
return Random32() >> 16;
}
static inline u16 Random2(void)
{
return Random2_32() >> 16;
}
void AdvanceRandom(void);
#else
typedef u32 rng_value_t;
#define RNG_VALUE_EMPTY 0
//Returns a 16-bit pseudorandom number
u16 Random(void);
@ -10,11 +76,23 @@ u16 Random2(void);
//Returns a 32-bit pseudorandom number
#define Random32() (Random() | (Random() << 16))
#define Random2_32() (Random2() | (Random2() << 16))
// The number 1103515245 comes from the example implementation of rand and srand
// in the ISO C standard.
#define ISO_RANDOMIZE1(val)(1103515245 * (val) + 24691)
#define ISO_RANDOMIZE2(val)(1103515245 * (val) + 12345)
static inline u16 LocalRandom(rng_value_t *val)
{
*val = ISO_RANDOMIZE1(*val);
return *val >> 16;
}
static inline void AdvanceRandom(void)
{
Random();
}
#endif
extern rng_value_t gRngValue;
extern rng_value_t gRng2Value;
//Sets the initial seed value of the pseudorandom number generator
void SeedRng(u16 seed);

View file

@ -2,6 +2,7 @@
#define GUARD_RECORDED_BATTLE_H
#include "constants/battle.h"
#include "random.h"
#define BATTLER_RECORD_SIZE 664
@ -13,7 +14,7 @@ struct RecordedBattleSave
u8 playersGender[MAX_BATTLERS_COUNT];
u32 playersTrainerId[MAX_BATTLERS_COUNT];
u8 playersLanguage[MAX_BATTLERS_COUNT];
u32 rngSeed;
rng_value_t rngSeed;
u32 battleFlags;
u8 playersBattlers[MAX_BATTLERS_COUNT];
u16 opponentA;
@ -49,8 +50,8 @@ enum
RECORDED_ITEM_MOVE,
};
extern u32 gRecordedBattleRngSeed;
extern u32 gBattlePalaceMoveSelectionRngValue;
extern rng_value_t gRecordedBattleRngSeed;
extern rng_value_t gBattlePalaceMoveSelectionRngValue;
extern u8 gRecordedBattleMultiplayerId;
#define B_RECORD_MODE_RECORDING 1

View file

@ -855,7 +855,7 @@ void ClearFlagAfterTest(void);
void OpenPokemon(u32 sourceLine, u32 side, u32 species);
void ClosePokemon(u32 sourceLine);
void RNGSeed_(u32 sourceLine, u32 seed);
void RNGSeed_(u32 sourceLine, rng_value_t seed);
void AIFlags_(u32 sourceLine, u32 flags);
void AILogScores(u32 sourceLine);
void Gender_(u32 sourceLine, u32 gender);

View file

@ -28,6 +28,7 @@
#include "constants/songs.h"
#include "constants/trainers.h"
#include "recorded_battle.h"
#include "random.h"
static void LinkOpponentHandleLoadMonSprite(u32 battler);
static void LinkOpponentHandleSwitchInAnim(u32 battler);

View file

@ -28,6 +28,7 @@
#include "constants/songs.h"
#include "constants/trainers.h"
#include "recorded_battle.h"
#include "random.h"
static void LinkPartnerHandleLoadMonSprite(u32 battler);
static void LinkPartnerHandleSwitchInAnim(u32 battler);

View file

@ -1713,9 +1713,16 @@ static void CB2_HandleStartMultiBattle(void)
case 8:
if (IsLinkTaskFinished())
{
#if HQ_RANDOM == TRUE
struct BattleVideo *ptr = &gBattleStruct->multiBuffer.battleVideo;
ptr->battleTypeFlags = gBattleTypeFlags;
ptr->rngSeed = gRecordedBattleRngSeed;
#else
u32 *ptr = gBattleStruct->multiBuffer.battleVideo;
ptr[0] = gBattleTypeFlags;
ptr[1] = gRecordedBattleRngSeed; // UB: overwrites berry data
#endif
SendBlock(BitmaskAllOtherLinkPlayers(), ptr, sizeof(gBattleStruct->multiBuffer.battleVideo));
gBattleCommunication[MULTIUSE_STATE]++;
}
@ -2053,7 +2060,7 @@ void VBlankCB_Battle(void)
{
// Change gRngSeed every vblank unless the battle could be recorded.
if (!(gBattleTypeFlags & (BATTLE_TYPE_LINK | BATTLE_TYPE_FRONTIER | BATTLE_TYPE_RECORDED)))
Random();
AdvanceRandom();
SetGpuReg(REG_OFFSET_BG0HOFS, gBattle_BG0_X);
SetGpuReg(REG_OFFSET_BG0VOFS, gBattle_BG0_Y);

View file

@ -358,7 +358,7 @@ EWRAM_DATA bool8 gCurContestWinnerIsForArtist = 0;
EWRAM_DATA u8 gCurContestWinnerSaveIdx = 0;
// IWRAM common vars.
u32 gContestRngValue;
rng_value_t gContestRngValue;
extern const u8 gText_LinkStandby4[];
extern const u8 gText_BDot[];
@ -1709,7 +1709,7 @@ static void Task_AppealSetup(u8 taskId)
if (++gTasks[taskId].data[0] > 19)
{
eContest.turnNumber = 0;
eContest.unusedRng = gRngValue;
eContest.unusedRng = 0;
if ((gLinkContestFlags & LINK_CONTEST_FLAG_IS_LINK) && IsPlayerLinkLeader())
{
s32 i;
@ -6109,4 +6109,3 @@ void StripPlayerAndMonNamesForLinkContest(struct ContestPokemon *mon, s32 langua
name[PLAYER_NAME_LENGTH] = EOS;
}
}

View file

@ -2655,8 +2655,7 @@ void GenerateContestRand(void)
if (gLinkContestFlags & LINK_CONTEST_FLAG_IS_LINK)
{
gContestRngValue = ISO_RANDOMIZE1(gContestRngValue);
random = gContestRngValue >> 16;
random = LocalRandom(&gContestRngValue);
result = &gSpecialVar_Result;
}
else
@ -2669,8 +2668,7 @@ void GenerateContestRand(void)
u16 GetContestRand(void)
{
gContestRngValue = ISO_RANDOMIZE1(gContestRngValue);
return gContestRngValue >> 16;
return LocalRandom(&gContestRngValue);
}
bool8 LinkContestWaitForConnection(void)

View file

@ -2144,7 +2144,7 @@ static void DebugAction_Util_Player_Gender(u8 taskId)
static void DebugAction_Util_Player_Id(u8 taskId)
{
u32 trainerId = ((Random() << 16) | Random());
u32 trainerId = Random32();
SetTrainerId(trainerId, gSaveBlock2Ptr->playerTrainerId);
Debug_DestroyMenu_Full(taskId);
ScriptContext_Enable();

View file

@ -127,7 +127,7 @@ void MoveSaveBlocks_ResetHeap(void)
gMain.vblankCallback = vblankCB;
// create a new encryption key
encryptionKey = (Random() << 16) + (Random());
encryptionKey = Random32();
ApplyNewEncryptionKeyToAllEncryptedData(encryptionKey);
gSaveBlock2Ptr->encryptionKey = encryptionKey;
}

View file

@ -23,9 +23,7 @@ static u8 GetMatchingDigits(u16, u16);
void ResetLotteryCorner(void)
{
u16 rand = Random();
SetLotteryNumber((Random() << 16) | rand);
SetLotteryNumber(Random32());
VarSet(VAR_POKELOT_PRIZE_ITEM, 0);
}

View file

@ -373,7 +373,7 @@ static void VBlankIntr(void)
TryReceiveLinkBattleData();
if (!gMain.inBattle || !(gBattleTypeFlags & (BATTLE_TYPE_LINK | BATTLE_TYPE_FRONTIER | BATTLE_TYPE_RECORDED)))
Random();
AdvanceRandom();
UpdateWirelessStatusIndicatorSprite();

View file

@ -4,12 +4,108 @@
#include <alloca.h>
#endif
EWRAM_DATA static u8 sUnknown = 0;
EWRAM_DATA static u32 sRandCount = 0;
// IWRAM common
u32 gRngValue;
u32 gRng2Value;
rng_value_t gRngValue;
rng_value_t gRng2Value;
#if HQ_RANDOM == TRUE
EWRAM_DATA static volatile bool8 sRngLoopUnlocked;
// Streams allow generators seeded the same to have separate outputs.
#define STREAM1 1
#define STREAM2 29
// A variant of SFC32 that lets you change the stream.
// stream can be any odd number.
static inline u32 _SFC32_Next_Stream(struct Sfc32State *state, const u8 stream)
{
const u32 result = state->a + state->b + state->ctr;
state->ctr += stream;
state->a = state->b ^ (state->b >> 9);
state->b = state->c * 9;
state->c = result + ((state->c << 21) | (state->c >> 11));
return result;
}
static void SFC32_Seed(struct Sfc32State *state, u16 seed, u8 stream)
{
u32 i;
state->a = state->b = 0;
state->c = seed;
state->ctr = stream;
for(i = 0; i < 16; i++)
{
_SFC32_Next_Stream(state, stream);
}
}
/*This ASM implementation uses some shortcuts and is generally faster on the GBA.
* It's not necessarily faster if inlined, or on other platforms. */
u32 NAKED Random32(void)
{
asm(".thumb\n\
push {r4, r5, r6}\n\
mov r6, #11\n\
ldr r5, =gRngValue\n\
ldmia r5!, {r1, r2, r3, r4}\n\
@ e (result) = a + b + d++\n\
add r1, r1, r2\n\
add r0, r1, r4\n\
add r4, r4, #" STR(STREAM1) "\n\
@ a = b ^ (b >> 9)\n\
lsr r1, r2, #9\n\
eor r1, r1, r2\n\
@ b = c + (c << 3) [c * 9]\n\
lsl r2, r3, #3\n\
add r2, r2, r3\n\
@ c = rol(c, 21) + e\n\
ror r3, r3, r6\n\
add r3, r3, r0\n\
sub r5, r5, #16\n\
stmia r5!, {r1, r2, r3, r4}\n\
pop {r4, r5, r6}\n\
bx lr\n\
.ltorg"
);
}
u32 Random2_32(void)
{
return _SFC32_Next_Stream(&gRng2Value, STREAM2);
}
void SeedRng(u16 seed)
{
struct Sfc32State state;
SFC32_Seed(&state, seed, STREAM1);
sRngLoopUnlocked = FALSE;
gRngValue = state;
sRngLoopUnlocked = TRUE;
}
void SeedRng2(u16 seed)
{
SFC32_Seed(&gRng2Value, seed, STREAM2);
}
void AdvanceRandom(void)
{
if (sRngLoopUnlocked == TRUE)
Random32();
}
#define LOOP_RANDOM_START \
struct Sfc32State *const state = &gRngValue; \
sRngLoopUnlocked = FALSE;
#define LOOP_RANDOM_END sRngLoopUnlocked = TRUE;
#define LOOP_RANDOM ((u16)(_SFC32_Next(state) >> 16))
#else
EWRAM_DATA static u32 sRandCount = 0;
u16 Random(void)
{
@ -21,7 +117,6 @@ u16 Random(void)
void SeedRng(u16 seed)
{
gRngValue = seed;
sUnknown = 0;
}
void SeedRng2(u16 seed)
@ -35,15 +130,24 @@ u16 Random2(void)
return gRng2Value >> 16;
}
#define LOOP_RANDOM_START
#define LOOP_RANDOM_END
#define LOOP_RANDOM (Random())
#endif
#define SHUFFLE_IMPL \
u32 tmp; \
LOOP_RANDOM_START; \
--n; \
while (n > 1) \
{ \
int j = (Random() * (n+1)) >> 16; \
int j = (LOOP_RANDOM * (n+1)) >> 16; \
SWAP(data[n], data[j], tmp); \
--n; \
}
} \
LOOP_RANDOM_END
void Shuffle8(void *data_, size_t n)
{
@ -66,15 +170,19 @@ void Shuffle32(void *data_, size_t n)
void ShuffleN(void *data, size_t n, size_t size)
{
void *tmp = alloca(size);
LOOP_RANDOM_START;
--n;
while (n > 1)
{
int j = (Random() * (n+1)) >> 16;
int j = (LOOP_RANDOM * (n+1)) >> 16;
memcpy(tmp, (u8 *)data + n*size, size); // tmp = data[n];
memcpy((u8 *)data + n*size, (u8 *)data + j*size, size); // data[n] = data[j];
memcpy((u8 *)data + j*size, tmp, size); // data[j] = tmp;
--n;
}
LOOP_RANDOM_END;
}
__attribute__((weak, alias("RandomUniformDefault")))
@ -96,12 +204,14 @@ u32 RandomUniformDefault(enum RandomTag tag, u32 lo, u32 hi)
u32 RandomUniformExceptDefault(enum RandomTag tag, u32 lo, u32 hi, bool32 (*reject)(u32))
{
LOOP_RANDOM_START;
while (TRUE)
{
u32 n = RandomUniformDefault(tag, lo, hi);
u32 n = lo + (((hi - lo + 1) * LOOP_RANDOM) >> 16);
if (!reject(n))
return n;
}
LOOP_RANDOM_END;
}
u32 RandomWeightedArrayDefault(enum RandomTag tag, u32 sum, u32 n, const u8 *weights)

View file

@ -33,8 +33,8 @@ struct PlayerInfo
// Save data using TryWriteSpecialSaveSector is allowed to exceed SECTOR_DATA_SIZE (up to the counter field)
STATIC_ASSERT(sizeof(struct RecordedBattleSave) <= SECTOR_COUNTER_OFFSET, RecordedBattleSaveFreeSpace);
EWRAM_DATA u32 gRecordedBattleRngSeed = 0;
EWRAM_DATA u32 gBattlePalaceMoveSelectionRngValue = 0;
EWRAM_DATA rng_value_t gRecordedBattleRngSeed = RNG_VALUE_EMPTY;
EWRAM_DATA rng_value_t gBattlePalaceMoveSelectionRngValue = RNG_VALUE_EMPTY;
EWRAM_DATA static u8 sBattleRecords[MAX_BATTLERS_COUNT][BATTLER_RECORD_SIZE] = {0};
EWRAM_DATA static u16 sBattlerRecordSizes[MAX_BATTLERS_COUNT] = {0};
EWRAM_DATA static u16 sBattlerPrevRecordSizes[MAX_BATTLERS_COUNT] = {0};

View file

@ -25,7 +25,7 @@ static void SetMirageRnd(u32 rnd)
// unused
void InitMirageRnd(void)
{
SetMirageRnd((Random() << 16) | Random());
SetMirageRnd(Random32());
}
void UpdateMirageRnd(u16 days)

View file

@ -17,7 +17,7 @@
error = 0; \
for (i = 0; i < ARRAY_COUNT(indexSum); i++) \
error += abs(3584 - indexSum[i]); \
EXPECT_LT(error, (int)(28672 * 0.025));
EXPECT_LT(error, (int)(28672 * 0.03));
TEST("Shuffle randomizes the array [Shuffle8]")
{
@ -196,6 +196,13 @@ TEST("RandomElement generates a uniform distribution")
TEST("RandomUniform mul-based faster than mod-based (compile-time)")
{
#if HQ_RANDOM == TRUE
const u32 expectedMulSum = 6;
const u32 expectedModSum = 4;
#else
const u32 expectedMulSum = 3;
const u32 expectedModSum = 4;
#endif
struct Benchmark mulBenchmark, modBenchmark;
u32 mulSum = 0, modSum = 0;
@ -221,12 +228,19 @@ TEST("RandomUniform mul-based faster than mod-based (compile-time)")
// These numbers are different because multiplication and modulus
// have subtly different biases (so subtle that it's irrelevant for
// our purposes).
EXPECT_EQ(mulSum, 3);
EXPECT_EQ(modSum, 4);
EXPECT_EQ(mulSum, expectedMulSum);
EXPECT_EQ(modSum, expectedModSum);
}
TEST("RandomUniform mul-based faster than mod-based (run-time)")
{
#if HQ_RANDOM == TRUE
const u32 expectedMulSum = 289;
const u32 expectedModSum = 205;
#else
const u32 expectedMulSum = 232;
const u32 expectedModSum = 249;
#endif
u32 i;
struct Benchmark mulBenchmark, modBenchmark;
u32 mulSum = 0, modSum = 0;
@ -246,6 +260,30 @@ TEST("RandomUniform mul-based faster than mod-based (run-time)")
EXPECT_FASTER(mulBenchmark, modBenchmark);
// Reference mulSum/modSum to prevent optimization.
EXPECT_EQ(mulSum, 232);
EXPECT_EQ(modSum, 249);
EXPECT_EQ(mulSum, expectedMulSum);
EXPECT_EQ(modSum, expectedModSum);
}
#if HQ_RANDOM == TRUE
TEST("Thumb and C SFC32 implementations produce the same results")
{
u32 thumbSum;
u32 cSum;
int i;
rng_value_t localState;
thumbSum = 0;
cSum = 0;
SeedRng(0);
localState = gRngValue;
for(i = 0; i < 32; i++)
{
thumbSum += Random32();
cSum += _SFC32_Next(&localState);
}
EXPECT_EQ(thumbSum, cSum);
}
#endif

View file

@ -33,8 +33,20 @@
#define STATE gBattleTestRunnerState
#define DATA gBattleTestRunnerState->data
#define RNG_SEED_DEFAULT 0x00000000
#if HQ_RANDOM == TRUE
#define RNG_SEED_DEFAULT {0, 0, 0, 0}
static inline bool32 RngSeedNotDefault(const rng_value_t *seed)
{
return (seed->a | seed->b | seed->c | seed->ctr) != 0;
}
#else
#define RNG_SEED_DEFAULT 0x00000000
static inline bool32 RngSeedNotDefault(const rng_value_t *seed)
{
return *seed != RNG_SEED_DEFAULT;
}
#endif
#undef Q_4_12
#define Q_4_12(n) (s32)((n) * 4096)
@ -257,11 +269,12 @@ static void BattleTest_Run(void *data)
s32 i;
u32 requiredPlayerPartySize;
u32 requiredOpponentPartySize;
const rng_value_t defaultSeed = RNG_SEED_DEFAULT;
const struct BattleTest *test = data;
memset(&DATA, 0, sizeof(DATA));
DATA.recordedBattle.rngSeed = RNG_SEED_DEFAULT;
DATA.recordedBattle.rngSeed = defaultSeed;
DATA.recordedBattle.textSpeed = OPTIONS_TEXT_SPEED_FAST;
// Set battle flags and opponent ids.
switch (test->type)
@ -1349,6 +1362,20 @@ static void CB2_BattleTest_NextParameter(void)
}
}
static inline rng_value_t MakeRngValue(const u16 seed)
{
#if HQ_RANDOM == TRUE
int i;
rng_value_t result = {0, 0, seed, 1};
for (i = 0; i < 16; i++)
{
_SFC32_Next(&result);
}
return result;
#else
return ISO_RANDOMIZE1(seed);
#endif
}
static void CB2_BattleTest_NextTrial(void)
{
ClearFlagAfterTest();
@ -1373,7 +1400,7 @@ static void CB2_BattleTest_NextTrial(void)
{
PrintTestName();
gTestRunnerState.result = TEST_RESULT_PASS;
DATA.recordedBattle.rngSeed = ISO_RANDOMIZE1(STATE->runTrial);
DATA.recordedBattle.rngSeed = MakeRngValue(STATE->runTrial);
DATA.queuedEvent = 0;
DATA.lastActionTurn = 0;
SetVariablesForRecordedBattle(&DATA.recordedBattle);
@ -1446,16 +1473,17 @@ void Randomly(u32 sourceLine, u32 passes, u32 trials, struct RandomlyContext ctx
}
else
{
INVALID_IF(DATA.recordedBattle.rngSeed != RNG_SEED_DEFAULT, "RNG seed already set");
const rng_value_t defaultSeed = RNG_SEED_DEFAULT;
INVALID_IF(RngSeedNotDefault(&DATA.recordedBattle.rngSeed), "RNG seed already set");
STATE->trials = 50;
STATE->trialRatio = Q_4_12(1) / STATE->trials;
DATA.recordedBattle.rngSeed = 0;
DATA.recordedBattle.rngSeed = defaultSeed;
}
}
void RNGSeed_(u32 sourceLine, u32 seed)
void RNGSeed_(u32 sourceLine, rng_value_t seed)
{
INVALID_IF(DATA.recordedBattle.rngSeed != RNG_SEED_DEFAULT, "RNG seed already set");
INVALID_IF(RngSeedNotDefault(&DATA.recordedBattle.rngSeed), "RNG seed already set");
DATA.recordedBattle.rngSeed = seed;
}