RBY desyncs and Desync Clause Mod: technical explanation!
Disclaimer: the variables' names are taken from https://github.com/pret/pokeyellow.
The Desync Clause Mod (DCM) aims to resolve specific interactions that produce different outcomes in each player's game. In the past, whenever a desync occurred, DCM simply aborted the move being used. As new desyncs were discovered, this solution proved insufficient, so we switched to resolving them based on the acting player's point of view in a linked battle.
There are two types of desyncs:
Offline desyncs
These desyncs occur because the game's code is not perfectly mirrored between the player's and the opponent's sides.
1. Psywave
The player's Psywave rolls a number between 1 and
level × 1.5. The opponent's Psywave rolls a number between 0 and
level × 1.5.
Video footage
Desync Clause Mod never rolls a zero.
2. Bide
The player's accumulated Bide damage is not reset if the opponent faints. The opponent's accumulated Bide damage is reset if the player's Pokémon faints.
Video footage
Desync Clause Mod never resets accumulated Bide damage.
Online desyncs
These desyncs occur because of the way each game sends its selected action to the opponent's game. They never occur when playing against an NPC.
Each player has four variables:
- wPlayerMoveListIndex: Holds a number between 0 and 3 corresponding to the index of the selected move. In the original game, this value is updated whenever the cursor hovers over a move in the selection menu. For simplicity, Pokémon Showdown (PS) updates this value only when a move is selected. This variable is also reset to 0 when the player switches and is used for PP deduction. It is sent to the opponent at the beginning of a turn so that the opponent's game knows which move the opposing Pokémon is about to use.
- wPlayerSelectedMove: Holds the global number/identifier of the selected move. This is the variable checked for most operations, with the exception of PP deduction.
- wEnemyMoveListIndex: The same as
wPlayerMoveListIndex, but for the opponent. This variable is updated only when the opponent submits a move, sending their own wPlayerMoveListIndex, including a locked action submitted by clicking the Fight button.
- wEnemySelectedMove: The same as
wPlayerSelectedMove, but for the opponent. When wEnemyMoveListIndex is updated, this variable is set to the move stored in the corresponding opponent's slot.
A few important notes before continuing:
- At the beginning of the battle, both
wPlayerSelectedMove and wEnemySelectedMove are set to NO_MOVE, and both wPlayerMoveListIndex and wEnemyMoveListIndex are set to 0.
- When a Pokémon is asleep or frozen, or is locked into a move, such as Outrage, Solar Beam, or Wrap, none of the variables on that Pokémon's side are updated, but its game still sends
wPlayerMoveListIndex to the opponent.
- When a Pokémon is bound/partially trapped, its game sends that information to the opponent. Both
wPlayerSelectedMove and wEnemySelectedMove are updated to CANNOT_MOVE, but wPlayerMoveListIndex and wEnemyMoveListIndex remain unchanged.
- When a Pokémon uses Struggle, its game sends that information to the opponent. Both
wPlayerSelectedMove and wEnemySelectedMove are updated to STRUGGLE, but wPlayerMoveListIndex and wEnemyMoveListIndex remain unchanged.
The following desyncs always occur because
wPlayerMoveListIndex points to a slot that does not match the move stored in
wPlayerSelectedMove, but still sends it to the opponent. As a result,
wEnemySelectedMove is updated to a move that does not match
wPlayerSelectedMove, causing each player to see the Pokémon execute a different move.
Desync Clause Mod resolves all of these desyncs by using
wPlayerSelectedMove from the acting Pokémon's point of view.
3. Thawing after a switch
When a player switches to a Pokémon,
wPlayerMoveListIndex is reset to 0. On the following turn, if the new Pokemon is frozen,
wPlayerMoveListIndex is sent to the opponent, causing
wEnemySelectedMove to become the frozen Pokémon's first-slot move.
However,
wPlayerSelectedMove still contains the last move used by that Pokémon's team. If the frozen Pokémon is hit by a Fire-type move and can still act during that turn, it will execute the last move used by its team.
Example:
- P1's Cloyster uses Toxic.
- P1 switches Cloyster out for a frozen Poliwrath.
- P2's Arcanine hits Poliwrath with a Fire-type move and Poliwrath thaws.
- If in the same turn Poliwrath can still act, P1 sees it use Cloyster's Toxic, while P2 sees it use whichever move Poliwrath has in its first slot.
Video footage (the video description is technically accurate)
In this case, Desync Clause Mod would make Poliwrath use Toxic.
If Poliwrath's team has not selected a move yet, Poliwrath will use
NO_MOVE. Since
wPlayerSelectedMove is 0, the game fetches a supposed move at index
0xff (255) in the
Moves table, which lands outside the table. Immediately after
Moves in the ROM data is the
BaseStats table, and the lookup lands in the middle of Raticate's base stats, loading data from there:
wPlayerMoveNum: Raticate catch rate = 0x5A = Fissure
wPlayerMoveEffect: Raticate base EXP = 0x74 = invalid effect index
wPlayerMovePower: Raticate pic size = 0x66 = 102 base power
wPlayerMoveType: RaticatePicFront low = 0x31 = invalid type (Special category)
wPlayerMoveAccuracy: RaticatePicFront high = 0x51 = 81/256 accuracy (about 31.6%)
wPlayerMoveMaxPP: RaticatePicBack low = 0x0A = 10 max PP
4. Priority of a sleeping or frozen Pokémon
Using the same setup as in the previous desync, P1 sees Poliwrath act with the priority of the last move used by Cloyster, while P2 sees Poliwrath act with the priority of the move in Poliwrath's first slot.
Desync Clause Mod always uses the priority of the last move used by that Pokémon's team.
5. "Partial trapping move Mirror Move glitch" or "Fire Spin glitch"
When Mirror Move copies a binding move, the binding effects are applied to both the attacking and defending Pokémon as normal. When Mirror Move executes the binding move, the game manually updates both
wPlayerSelectedMove and
wEnemySelectedMove to that move. When the opponent switches out, the attacker's binding condition ends. Because P1 still has the binding move stored in
wPlayerSelectedMove, the Pokémon will use that move again, restarting the binding effect from the beginning. However,
wPlayerMoveListIndex still points to Mirror Move's slot and sends that information to the opponent at the beginning of the turn. As a result, the opponent sees the Pokémon use Mirror Move, causing a desync. This execution of Mirror Move always fails because switches clear the variable it uses to check the last move.
This does not happen with Metronome because the game contains a hardcoded instruction that resets
wPlayerSelectedMove to Metronome when the Pokémon is in the middle of a binding move and the opponent switches, resulting in Metronome being used on both views.
Article (not super clear; the replication is a bit overcomplicated)
Desync Clause Mod makes the Pokemon reuse the binding move.
6. Counter
Counter is affected by both online desyncs and offline disparities.
Counter also uses a different set of variables:
wPlayerMoveType
wPlayerMovePower
wEnemyMoveType
wEnemyMovePower
These variables are set whenever a move's usage message appears, so they are not updated if the Pokémon is fully paralyzed or flinches. However, on the player's side, these variables are also set as soon as the player selects a move. This does not happen when an NPC selects a move, which is why Counter is also affected by an offline disparity. For PS, however, we focus solely on the online case.
Counter executes successfully if all the following are true:
- Is
wEnemySelectedMove different from Counter?
- Is
wEnemyMovePower nonzero?
- Is
wEnemyMoveType either Normal or Fighting?
- Is
wDamage nonzero?
Two desyncs can occur:
6.1. Counter + paralysis
The first desync involves
wPlayerMovePower and
wPlayerMoveType. When P2 selects a move, its variables are immediately updated to that move. However, if P2 fails to act because of full paralysis, P1 does not update
wEnemyMovePower or
wEnemyMoveType. Those variables therefore continue to hold the values of the last move P2 used.
For example, suppose P2 previously used Tackle and then becomes fully paralyzed while attempting to use Fire Blast. From P2's perspective, Counter fails because
wPlayerMoveType contains the Fire type. From P1's perspective, however,
wEnemyMoveType still contains the Normal type, so Counter succeeds.
Video footage
In this case, Counter also succeeds on PS because P1 is the acting Pokémon using Counter.
6.2. Counter + sleep
Using the same setup from desyncs 3 and 4, if P1 uses Counter while P2's Pokémon is asleep/frozen, P1 performs the Counter check using P2's first-slot move, while P2 performs it using the last move used by its team.
Because PS uses the acting Pokémon's point of view—in this case, P1's—Counter will fail if P2's first move is Counter. This would not happen on the turn the P2's Pokémon switches in, but only on the following turn, when the Pokémon is asleep or frozen and tries to act, sending its
wPlayerMoveListIndex, with a value of 0, to the opponent.