Sabelette
from the river to the sea
First off, huge thanks to Shellnuts for helping me understand a whole lot of assembly instructions because I was literally looking up assembly as I dug through code to try to figure this shit out.
With that out of the way, story time! Yesterday on the RBY Discord, it came up that the first 10 random numbers (RNs) in a link battle are slightly rigged. Essentially, the first 10 RNs cannot be higher than 252, in a range that normally goes from 0-255, for 256 total numbers. This is not new - this has often been parroted in the RBY Discord and at various points on the forums, but nobody seemed to actually know where this idea came from, why it would be true, or have any proof of it being true. ABR was interested in the topic because well, this would mean you cannot miss a move due to a 255 roll on turn 1 (and often not on turn 2 either) because the number 255 cannot occur in the first 10 numbers of the link battle RNG, among other things. Allegedly, anyway.
Author's note: it was not, in fact, documented.
From there on, everyone in the conversation was pretty interested in where the proof was and whether this could be implemented, and so I started off by checking out the channels of some of RBY's famous glitch hunters/technical divers - Crystal_, TheZZAZZGlitch, ChickasaurusGL. Nothing. 13 years of videos, not a peep of this. On the forums, I could find people saying it but claiming it was the first 9 RNs, not 10, and of course, not a shred of proof there either. Not even a dead link to some old-ass forum or anything, just people saying it with supreme confidence. Ultimately, I decided to dive into the decomp to figure out where this came from and if it was real, as well as trying to figure out exactly what uses up RNs. Here's the initial questions I set out with.
I'll start off with what I've confirmed about RNs as of now, as that's probably the less interesting part:
1. RNs are called when the action occurs - if you try to attack but you paralyze, you don't call RNs for things like damage roll and crits
2. Sleep calls a single RN for duration
3. Damage rolls call RNs till they get a number between 217 and 255 - this is for the damage randomness. There are 39 possible rolls as we know, each linked to the numbers from 217-255, so a damage roll can use up any amount of RNs till it gets one in that range. Woo, headache!
4. Speed ties, confusion, paralysis, each of these use 1 RN. Stuff like Stoss does not waste RNs on a crit roll.
5. Multihit (2-5 hit) and partial trap moves call 2 RNs to determine number of hits.
After... quite some effort, I found some relevant bits of code for the 10 RNs thing:
if only we could all "pop af"
Isn't assembly... fun? You don't need to understand any of this to get to the point of this post, this was just me finding a reference to the fact that link battles use a different random number list than in-game stuff, which seemed to me like a good lead that there's something funky going on there. Anyway, we now could confirm 10 initial RNs are created, even if I couldn't figure out how tf those were generated and if they were truly random, or what.
The next couple finds, which I cannot explain to you remotely, were these:
To give you an idea of how out of my depth I was here, these were what I was consulting to get some sort of grasp on the assembly instructions:
http://z80-heaven.wikidot.com/instructions-set
http://marc.rawer.de/Gameboy/Docs/GBCPUman.pdf
To do my best here: In hexadecimal, FD = 253. FE = 254, FF = 255. This gave me a hunch that the screenshot on the left had something to do with why you couldn't get an RN above 252 initially, but I had less than zero proof; I just figured if FD was signaling the start of an array of bytes and FE was signaling the end and FF was signaling there was no connection, you probably don't want the game sending over FE prematurely or something and fucking up the data transfer. The bit on the right helped reinforce this in yet more ways I am not code-savvy enough to explain. This is where Shellnuts, the GOAT, comes in. Shell comes in and drops a pseudocode explanation of what's going on with the link battle RN list for my tiny brain:
The important bit is here:
.generateRandomNumberListLoop
call Random
cp SERIAL_PREAMBLE_BYTE ; all the random numbers have to be less than the preamble byte
jr nc, .generateRandomNumberListLoop
ld [hli], a
dec b
jr nz, .generateRandomNumberListLoop
ld hl, wSerialPartyMonsPatchList
ld a, SERIAL_PREAMBLE_BYTE
ld [hli], a
ld [hli], a
ld [hli], a
ld b, $c8
xor a
Remember the "preamble byte" screenshot above? My hunch was dead on. They reserve the last 3 values - FD/FE/FF or 253/254/255 - to signal the start and end of the initial data sent across the link cable for a battle, and for the error if there's no connection.
Shell also found something else that was very important. We all just assumed the RNG would reroll those numbers till it got something acceptable, like how the damage roll RNG call works, and so the first 10 rolls were constricted uniformly to a range of 0-253. This is not accurate:
This means 2 things:
1. Only moves with 100% actually gain accuracy; said moves gain, well, a 1/256 boost to accuracy. 242 corresponds to the threshold for 95% accurate moves like Razor Leaf; only a roll of 241 or below hits, so the 242/247/252 chances don't help at all.
2. Damage rolls are slightly lower on average if they use one of the first 10 RNs! This can slightly affect things like the chance of causing a recovery failure in the first couple turns. Screencapping myself in the midst of a ramble:
So, TLDR? This often-cited thing is true, but it does not give Hypnosis or Lovely Kiss an accuracy boost like everyone assumed. It would remove 256s on turn 1 (at least for whoever attacks first - who the fuck knows how many RNs a damage roll actually burns, could be 1 or could be 50 on any given roll), and it would slightly mess with damage rolls sometimes, but it really has almost no practical effect. With that, I think we have 3 options here:
1. Ignore this and keep playing Pokemon Showdown RBY exactly as before. This is probably what's gonna happen.
2. Partially implement this: Count RNs and block 1/256 misses until 10 RNs are used. Probably not gonna happen, but more likely than option 3.
3. Fully implement this, including fucking around with the damage rolls on turn 1 to make the game like, 0.1% more cart accurate. Somehow I think this is not gonna fly if we couldn't even make Sleep Clause cart accurate.
Anyway, I just wanted to tell a story about something that drove me nuts trying to figure out and share what is ultimately fairly useless RBY mechanics research, because I think it matters to just document this somewhere so the next time it comes up, we have some sort of reference to point to.
Hope you all enjoyed a strange bit of mechanics research I decided to embark on for zero reason. Thanks to Shellnuts and Enigami for contributing their technical knowledge to figuring out how some of this weird-ass stuff works.
With that out of the way, story time! Yesterday on the RBY Discord, it came up that the first 10 random numbers (RNs) in a link battle are slightly rigged. Essentially, the first 10 RNs cannot be higher than 252, in a range that normally goes from 0-255, for 256 total numbers. This is not new - this has often been parroted in the RBY Discord and at various points on the forums, but nobody seemed to actually know where this idea came from, why it would be true, or have any proof of it being true. ABR was interested in the topic because well, this would mean you cannot miss a move due to a 255 roll on turn 1 (and often not on turn 2 either) because the number 255 cannot occur in the first 10 numbers of the link battle RNG, among other things. Allegedly, anyway.
Author's note: it was not, in fact, documented.
From there on, everyone in the conversation was pretty interested in where the proof was and whether this could be implemented, and so I started off by checking out the channels of some of RBY's famous glitch hunters/technical divers - Crystal_, TheZZAZZGlitch, ChickasaurusGL. Nothing. 13 years of videos, not a peep of this. On the forums, I could find people saying it but claiming it was the first 9 RNs, not 10, and of course, not a shred of proof there either. Not even a dead link to some old-ass forum or anything, just people saying it with supreme confidence. Ultimately, I decided to dive into the decomp to figure out where this came from and if it was real, as well as trying to figure out exactly what uses up RNs. Here's the initial questions I set out with.
I'll start off with what I've confirmed about RNs as of now, as that's probably the less interesting part:
1. RNs are called when the action occurs - if you try to attack but you paralyze, you don't call RNs for things like damage roll and crits
2. Sleep calls a single RN for duration
3. Damage rolls call RNs till they get a number between 217 and 255 - this is for the damage randomness. There are 39 possible rolls as we know, each linked to the numbers from 217-255, so a damage roll can use up any amount of RNs till it gets one in that range. Woo, headache!
4. Speed ties, confusion, paralysis, each of these use 1 RN. Stuff like Stoss does not waste RNs on a crit roll.
5. Multihit (2-5 hit) and partial trap moves call 2 RNs to determine number of hits.
After... quite some effort, I found some relevant bits of code for the 10 RNs thing:
if only we could all "pop af"
Isn't assembly... fun? You don't need to understand any of this to get to the point of this post, this was just me finding a reference to the fact that link battles use a different random number list than in-game stuff, which seemed to me like a good lead that there's something funky going on there. Anyway, we now could confirm 10 initial RNs are created, even if I couldn't figure out how tf those were generated and if they were truly random, or what.
The next couple finds, which I cannot explain to you remotely, were these:
To give you an idea of how out of my depth I was here, these were what I was consulting to get some sort of grasp on the assembly instructions:
http://z80-heaven.wikidot.com/instructions-set
http://marc.rawer.de/Gameboy/Docs/GBCPUman.pdf
To do my best here: In hexadecimal, FD = 253. FE = 254, FF = 255. This gave me a hunch that the screenshot on the left had something to do with why you couldn't get an RN above 252 initially, but I had less than zero proof; I just figured if FD was signaling the start of an array of bytes and FE was signaling the end and FF was signaling there was no connection, you probably don't want the game sending over FE prematurely or something and fucking up the data transfer. The bit on the right helped reinforce this in yet more ways I am not code-savvy enough to explain. This is where Shellnuts, the GOAT, comes in. Shell comes in and drops a pseudocode explanation of what's going on with the link battle RN list for my tiny brain:
And then Shell found the goldmine.BattleRandom Pseudocode:
Load contents at memory location wLinkState into A register and check if you are in a link battle
If you are not in a link battle, jump to the Random function (located in the "math" folder in the PokeRed decomp).
Push the contents of the HL and BC registers to the stack (saving the contents
of these registers for later use).
Load contents at memory location wLinkBattleRandomNumberListIndex into the A and C registers and 0 into the B register.
Load the value for the address wLinkBattleRandomNumberList into the HL register.
Add the value in the BC register to the HL register and store it in the HL register.
Increment the value in the A register and store that value into memory at the address wLinkBattleRandomNumberListIndex
Compare the contents of the A register with 9.
Load the value at the address pointed to by the contents of the HL register into A
Retrieve the values you stored on the stack for the HL and BC registers
Some stuff I am unsure about involving virtual console hooks and returns for reasons I don't understand.
Push the contents of all registers to the stack
Bitwise XOR A with itself (sets it to zero and clears the carry bit in 1 instruction) and store that value into memory at the address wLinkBattleRandomNumberListIndex
Load the value for the address wLinkBattleRandomNumberList into the HL register.
Load 9 into the B register
Perform the following loop:
Load the value in the HL register into A register and multiply it by 5
Add 1 to the value in the A register
Store the value in the A register into memory at the address pointed to by the value in the HL register
Increment the HL register
Decrement B by 1, if B is nonzero then jump to the top of the loop.
Retrive the values for all the registers that you pushed to the stack.
Return from Function Call.
; This is called after completing a trade.
CableClub_DoBattleOrTradeAgain:
ld hl, wSerialPlayerDataBlock
ld a, SERIAL_PREAMBLE_BYTE
ld b, 6
.writePlayerDataBlockPreambleLoop
ld [hli], a
dec b
jr nz, .writePlayerDataBlockPreambleLoop
ld hl, wSerialRandomNumberListBlock
ld a, SERIAL_PREAMBLE_BYTE
ld b, 7
.writeRandomNumberListPreambleLoop
ld [hli], a
dec b
jr nz, .writeRandomNumberListPreambleLoop
ld b, 10
.generateRandomNumberListLoop
call Random
cp SERIAL_PREAMBLE_BYTE ; all the random numbers have to be less than the preamble byte
jr nc, .generateRandomNumberListLoop
ld [hli], a
dec b
jr nz, .generateRandomNumberListLoop
ld hl, wSerialPartyMonsPatchList
ld a, SERIAL_PREAMBLE_BYTE
ld [hli], a
ld [hli], a
ld [hli], a
ld b, $c8
xor a
.zeroPlayerDataPatchListLoop
ld [hli], a
dec b
jr nz, .zeroPlayerDataPatchListLoop
ld hl, wGrassRate
ld bc, wTrainerHeaderPtr - wGrassRate
.zeroEnemyPartyLoop
xor a
ld [hli], a
dec bc
ld a, b
or c
jr nz, .zeroEnemyPartyLoop
ld hl, wPartyMons - 1
ld de, wSerialPartyMonsPatchList + 10
ld bc, 0
.patchPartyMonsLoop
inc c
ld a, c
cp SERIAL_PREAMBLE_BYTE
jr z, .startPatchListPart2
ld a, b
dec a ; are we in part 2 of the patch list?
jr nz, .checkPlayerDataByte ; jump if in part 1
; if we're in part 2
ld a, c
cp (wPartyMonOT - (wPartyMons - 1)) - (SERIAL_PREAMBLE_BYTE - 1)
jr z, .finishedPatchingPlayerData
.checkPlayerDataByte
inc hl
ld a, [hl]
cp SERIAL_NO_DATA_BYTE
jr nz, .patchPartyMonsLoop
; if the player data byte matches SERIAL_NO_DATA_BYTE, patch it with $FF and record the offset in the patch list
ld a, c
ld [de], a
inc de
ld [hl], $ff
jr .patchPartyMonsLoop
.startPatchListPart2
ld a, SERIAL_PATCH_LIST_PART_TERMINATOR
ld [de], a ; end of part 1
inc de
lb bc, 1, 0
jr .patchPartyMonsLoop
.finishedPatchingPlayerData
ld a, SERIAL_PATCH_LIST_PART_TERMINATOR
ld [de], a ; end of part 2
call Serial_SyncAndExchangeNybble
ldh a, [hSerialConnectionStatus]
cp USING_INTERNAL_CLOCK
jr nz, .skipSendingTwoZeroBytes
; if using internal clock
; send two zero bytes for syncing purposes?
call Delay3
xor a
ldh [hSerialSendData], a
ld a, START_TRANSFER_INTERNAL_CLOCK
ldh [rSC], a
call DelayFrame
xor a
ldh [hSerialSendData], a
ld a, START_TRANSFER_INTERNAL_CLOCK
ldh [rSC], a
.skipSendingTwoZeroBytes
call Delay3
ld a, (1 << SERIAL)
ldh [rIE], a
ld hl, wSerialRandomNumberListBlock
ld de, wSerialOtherGameboyRandomNumberListBlock
ld bc, SERIAL_RN_PREAMBLE_LENGTH + SERIAL_RNS_LENGTH
vc_hook Wireless_ExchangeBytes_RNG_state_unknown_Type5
call Serial_ExchangeBytes
ld a, SERIAL_NO_DATA_BYTE
ld [de], a
ld hl, wSerialPlayerDataBlock
ld de, wSerialEnemyDataBlock
ld bc, SERIAL_PREAMBLE_LENGTH + NAME_LENGTH + 1 + PARTY_LENGTH + 1 + (PARTYMON_STRUCT_LENGTH + NAME_LENGTH * 2) * PARTY_LENGTH + 3
vc_hook Wireless_ExchangeBytes_party_structs
call Serial_ExchangeBytes
ld a, SERIAL_NO_DATA_BYTE
ld [de], a
ld hl, wSerialPartyMonsPatchList
ld de, wSerialEnemyMonsPatchList
ld bc, 200
vc_hook Wireless_ExchangeBytes_patch_lists
call Serial_ExchangeBytes
ld a, (1 << SERIAL) | (1 << TIMER) | (1 << VBLANK)
ldh [rIE], a
ld a, SFX_STOP_ALL_MUSIC
call PlaySound
ldh a, [hSerialConnectionStatus]
cp USING_INTERNAL_CLOCK
jr z, .skipCopyingRandomNumberList ; the list generated by the gameboy clocking the connection is used by both gameboys
ld hl, wSerialOtherGameboyRandomNumberListBlock
.findStartOfRandomNumberListLoop
ld a, [hli]
and a
jr z, .findStartOfRandomNumberListLoop
cp SERIAL_PREAMBLE_BYTE
jr z, .findStartOfRandomNumberListLoop
cp SERIAL_NO_DATA_BYTE
jr z, .findStartOfRandomNumberListLoop
dec hl
ld de, wLinkBattleRandomNumberList
ld c, 10
.copyRandomNumberListLoop
ld a, [hli]
cp SERIAL_NO_DATA_BYTE
jr z, .copyRandomNumberListLoop
ld [de], a
inc de
dec c
jr nz, .copyRandomNumberListLoop
.skipCopyingRandomNumberList
ld hl, wSerialEnemyDataBlock + 3
CableClub_DoBattleOrTradeAgain:
ld hl, wSerialPlayerDataBlock
ld a, SERIAL_PREAMBLE_BYTE
ld b, 6
.writePlayerDataBlockPreambleLoop
ld [hli], a
dec b
jr nz, .writePlayerDataBlockPreambleLoop
ld hl, wSerialRandomNumberListBlock
ld a, SERIAL_PREAMBLE_BYTE
ld b, 7
.writeRandomNumberListPreambleLoop
ld [hli], a
dec b
jr nz, .writeRandomNumberListPreambleLoop
ld b, 10
.generateRandomNumberListLoop
call Random
cp SERIAL_PREAMBLE_BYTE ; all the random numbers have to be less than the preamble byte
jr nc, .generateRandomNumberListLoop
ld [hli], a
dec b
jr nz, .generateRandomNumberListLoop
ld hl, wSerialPartyMonsPatchList
ld a, SERIAL_PREAMBLE_BYTE
ld [hli], a
ld [hli], a
ld [hli], a
ld b, $c8
xor a
.zeroPlayerDataPatchListLoop
ld [hli], a
dec b
jr nz, .zeroPlayerDataPatchListLoop
ld hl, wGrassRate
ld bc, wTrainerHeaderPtr - wGrassRate
.zeroEnemyPartyLoop
xor a
ld [hli], a
dec bc
ld a, b
or c
jr nz, .zeroEnemyPartyLoop
ld hl, wPartyMons - 1
ld de, wSerialPartyMonsPatchList + 10
ld bc, 0
.patchPartyMonsLoop
inc c
ld a, c
cp SERIAL_PREAMBLE_BYTE
jr z, .startPatchListPart2
ld a, b
dec a ; are we in part 2 of the patch list?
jr nz, .checkPlayerDataByte ; jump if in part 1
; if we're in part 2
ld a, c
cp (wPartyMonOT - (wPartyMons - 1)) - (SERIAL_PREAMBLE_BYTE - 1)
jr z, .finishedPatchingPlayerData
.checkPlayerDataByte
inc hl
ld a, [hl]
cp SERIAL_NO_DATA_BYTE
jr nz, .patchPartyMonsLoop
; if the player data byte matches SERIAL_NO_DATA_BYTE, patch it with $FF and record the offset in the patch list
ld a, c
ld [de], a
inc de
ld [hl], $ff
jr .patchPartyMonsLoop
.startPatchListPart2
ld a, SERIAL_PATCH_LIST_PART_TERMINATOR
ld [de], a ; end of part 1
inc de
lb bc, 1, 0
jr .patchPartyMonsLoop
.finishedPatchingPlayerData
ld a, SERIAL_PATCH_LIST_PART_TERMINATOR
ld [de], a ; end of part 2
call Serial_SyncAndExchangeNybble
ldh a, [hSerialConnectionStatus]
cp USING_INTERNAL_CLOCK
jr nz, .skipSendingTwoZeroBytes
; if using internal clock
; send two zero bytes for syncing purposes?
call Delay3
xor a
ldh [hSerialSendData], a
ld a, START_TRANSFER_INTERNAL_CLOCK
ldh [rSC], a
call DelayFrame
xor a
ldh [hSerialSendData], a
ld a, START_TRANSFER_INTERNAL_CLOCK
ldh [rSC], a
.skipSendingTwoZeroBytes
call Delay3
ld a, (1 << SERIAL)
ldh [rIE], a
ld hl, wSerialRandomNumberListBlock
ld de, wSerialOtherGameboyRandomNumberListBlock
ld bc, SERIAL_RN_PREAMBLE_LENGTH + SERIAL_RNS_LENGTH
vc_hook Wireless_ExchangeBytes_RNG_state_unknown_Type5
call Serial_ExchangeBytes
ld a, SERIAL_NO_DATA_BYTE
ld [de], a
ld hl, wSerialPlayerDataBlock
ld de, wSerialEnemyDataBlock
ld bc, SERIAL_PREAMBLE_LENGTH + NAME_LENGTH + 1 + PARTY_LENGTH + 1 + (PARTYMON_STRUCT_LENGTH + NAME_LENGTH * 2) * PARTY_LENGTH + 3
vc_hook Wireless_ExchangeBytes_party_structs
call Serial_ExchangeBytes
ld a, SERIAL_NO_DATA_BYTE
ld [de], a
ld hl, wSerialPartyMonsPatchList
ld de, wSerialEnemyMonsPatchList
ld bc, 200
vc_hook Wireless_ExchangeBytes_patch_lists
call Serial_ExchangeBytes
ld a, (1 << SERIAL) | (1 << TIMER) | (1 << VBLANK)
ldh [rIE], a
ld a, SFX_STOP_ALL_MUSIC
call PlaySound
ldh a, [hSerialConnectionStatus]
cp USING_INTERNAL_CLOCK
jr z, .skipCopyingRandomNumberList ; the list generated by the gameboy clocking the connection is used by both gameboys
ld hl, wSerialOtherGameboyRandomNumberListBlock
.findStartOfRandomNumberListLoop
ld a, [hli]
and a
jr z, .findStartOfRandomNumberListLoop
cp SERIAL_PREAMBLE_BYTE
jr z, .findStartOfRandomNumberListLoop
cp SERIAL_NO_DATA_BYTE
jr z, .findStartOfRandomNumberListLoop
dec hl
ld de, wLinkBattleRandomNumberList
ld c, 10
.copyRandomNumberListLoop
ld a, [hli]
cp SERIAL_NO_DATA_BYTE
jr z, .copyRandomNumberListLoop
ld [de], a
inc de
dec c
jr nz, .copyRandomNumberListLoop
.skipCopyingRandomNumberList
ld hl, wSerialEnemyDataBlock + 3
The important bit is here:
.generateRandomNumberListLoop
call Random
cp SERIAL_PREAMBLE_BYTE ; all the random numbers have to be less than the preamble byte
jr nc, .generateRandomNumberListLoop
ld [hli], a
dec b
jr nz, .generateRandomNumberListLoop
ld hl, wSerialPartyMonsPatchList
ld a, SERIAL_PREAMBLE_BYTE
ld [hli], a
ld [hli], a
ld [hli], a
ld b, $c8
xor a
Remember the "preamble byte" screenshot above? My hunch was dead on. They reserve the last 3 values - FD/FE/FF or 253/254/255 - to signal the start and end of the initial data sent across the link cable for a battle, and for the error if there's no connection.
Shell also found something else that was very important. We all just assumed the RNG would reroll those numbers till it got something acceptable, like how the damage roll RNG call works, and so the first 10 rolls were constricted uniformly to a range of 0-253. This is not accurate:
This means 2 things:
1. Only moves with 100% actually gain accuracy; said moves gain, well, a 1/256 boost to accuracy. 242 corresponds to the threshold for 95% accurate moves like Razor Leaf; only a roll of 241 or below hits, so the 242/247/252 chances don't help at all.
2. Damage rolls are slightly lower on average if they use one of the first 10 RNs! This can slightly affect things like the chance of causing a recovery failure in the first couple turns. Screencapping myself in the midst of a ramble:
So, TLDR? This often-cited thing is true, but it does not give Hypnosis or Lovely Kiss an accuracy boost like everyone assumed. It would remove 256s on turn 1 (at least for whoever attacks first - who the fuck knows how many RNs a damage roll actually burns, could be 1 or could be 50 on any given roll), and it would slightly mess with damage rolls sometimes, but it really has almost no practical effect. With that, I think we have 3 options here:
1. Ignore this and keep playing Pokemon Showdown RBY exactly as before. This is probably what's gonna happen.
2. Partially implement this: Count RNs and block 1/256 misses until 10 RNs are used. Probably not gonna happen, but more likely than option 3.
3. Fully implement this, including fucking around with the damage rolls on turn 1 to make the game like, 0.1% more cart accurate. Somehow I think this is not gonna fly if we couldn't even make Sleep Clause cart accurate.
Anyway, I just wanted to tell a story about something that drove me nuts trying to figure out and share what is ultimately fairly useless RBY mechanics research, because I think it matters to just document this somewhere so the next time it comes up, we have some sort of reference to point to.
Hope you all enjoyed a strange bit of mechanics research I decided to embark on for zero reason. Thanks to Shellnuts and Enigami for contributing their technical knowledge to figuring out how some of this weird-ass stuff works.
Attachments
-
39.1 KB Views: 34