Repository: rh-hideout/pokeemerald-expansion
Pull request #6376: Fix Red Card overwriting gBattlerAttacker
Author: login: ghoulslash
Labels: name: category: battle-mechanic
description: Pertains to battle mechanics
name: category: battle-tests
description: Related to the automated test environment
Created: 2025-03-03T19:59:41Z | Merged: 2025-03-03T22:18:56Z

If red card triggers, it calls `swapattackerwithtarget` for the forcerandomswitch call. However, the attacker is never restored so this can mess up subsequent MOVEEND cases, e.g. Dancer. In addition, simply restoring gBattlerAttacker is insufficient because, if a switch occurs, `SwitchInClearSetData` clears gCurrentMove, so e.g. Dancer would not trigger, and the sameMoveTurns field wouldnt update properly. A `savedMove` field is added in to backup the gCurrentMove for red card

Future Work:
 - Dancer also overwrites gBattlerAttacker, but restoring it might not be straightforward due to `jumptocalledmove`.. maybe another MOVEEND_RESTORE_ATTACKER case afterwards.


Diff:
diff --git a/asm/macros/battle_script.inc b/asm/macros/battle_script.inc
index a169b3580ba9..e50cbcbe4338 100644
--- a/asm/macros/battle_script.inc
+++ b/asm/macros/battle_script.inc
@@ -2376,6 +2376,10 @@
 	callnative BS_SwapStats
 	.byte \stat
 	.endm
+    
+	.macro restoresavedmove
+	callnative BS_RestoreSavedMove
+	.endm
 
 @ helpful macros
 	.macro setstatchanger stat:req, stages:req, down:req
diff --git a/data/battle_scripts_1.s b/data/battle_scripts_1.s
index 748fa2c5bee5..aaa03f0ac6b3 100644
--- a/data/battle_scripts_1.s
+++ b/data/battle_scripts_1.s
@@ -6045,6 +6045,8 @@ BattleScript_RoarSuccessSwitch::
 BattleScript_RoarSuccessSwitch_Ret:
 	swapattackerwithtarget  @ continuation of RedCardActivates
 	restoretarget
+	restoreattacker
+	restoresavedmove
 	setbyte sSWITCH_CASE, B_SWITCH_NORMAL
 	return
 
@@ -9519,6 +9521,8 @@ BattleScript_RedCardActivationNoSwitch::
 	waitmessage B_WAIT_TIME_LONG
 	removeitem BS_SCRIPTING
 	restoretarget
+	restoreattacker
+	restoresavedmove
 	return
 
 BattleScript_RedCardActivates::
@@ -9537,22 +9541,17 @@ BattleScript_RedCardEnd:
 	return
 BattleScript_RedCardIngrain:
 	printstring STRINGID_PKMNANCHOREDITSELF
+BattleScript_RedCardIngrainContinue:
 	waitmessage B_WAIT_TIME_LONG
 	removeitem BS_SCRIPTING
 	restoretarget
 	return
 BattleScript_RedCardSuctionCups:
-	printstring STRINGID_PKMNANCHORSITSELFWITH
-	waitmessage B_WAIT_TIME_LONG
-	removeitem BS_SCRIPTING
-	restoretarget
-	return
+    printstring STRINGID_PKMNANCHORSITSELFWITH
+    goto BattleScript_RedCardIngrainContinue
 BattleScript_RedCardDynamaxed:
 	printstring STRINGID_MOVEBLOCKEDBYDYNAMAX
-	waitmessage B_WAIT_TIME_LONG
-	removeitem BS_SCRIPTING
-	restoretarget
-	return
+    goto BattleScript_RedCardIngrainContinue
 
 BattleScript_EjectButtonActivates::
 	makevisible BS_ATTACKER
diff --git a/include/battle.h b/include/battle.h
index 27227544d9e7..c9517b9b717d 100644
--- a/include/battle.h
+++ b/include/battle.h
@@ -836,6 +836,7 @@ struct BattleStruct
     struct MessageStatus slideMessageStatus;
     u8 trainerSlideSpriteIds[MAX_BATTLERS_COUNT];
     u8 embodyAspectBoost[NUM_BATTLE_SIDES];
+    u16 savedMove; // backup current move for mid-turn switching, e.g. Red Card
 };
 
 // The palaceFlags member of struct BattleStruct contains 1 flag per move to indicate which moves the AI should consider,
diff --git a/src/battle_script_commands.c b/src/battle_script_commands.c
index be57f4ae1828..331ff44ff2ad 100644
--- a/src/battle_script_commands.c
+++ b/src/battle_script_commands.c
@@ -7075,6 +7075,8 @@ static void Cmd_moveend(void)
                         {
                             gLastUsedItem = gBattleMons[battler].item;
                             SaveBattlerTarget(battler); // save battler with red card
+                            SaveBattlerAttacker(gBattlerAttacker);
+                            gBattleStruct->savedMove = gCurrentMove;
                             gBattleScripting.battler = battler;
                             gEffectBattler = gBattlerAttacker;
                             gBattleStruct->redCardActivates = TRUE;
@@ -18637,3 +18639,16 @@ void BS_SetSteelsurge(void)
         gBattlescriptCurrInstr = cmd->nextInstr;
     }
 }
+
+void BS_RestoreSavedMove(void)
+{
+    NATIVE_ARGS();
+
+    if (gBattleStruct->savedMove == MOVE_NONE)
+        DebugPrintfLevel(MGBA_LOG_WARN, "restoresavedmove was called with no move saved!");
+
+    gCurrentMove = gBattleStruct->savedMove;
+    gBattleStruct->savedMove = MOVE_NONE;
+    gBattlescriptCurrInstr = cmd->nextInstr;
+}
+
diff --git a/test/battle/ability/dancer.c b/test/battle/ability/dancer.c
index 657a126470ee..1e4c61dba0b3 100644
--- a/test/battle/ability/dancer.c
+++ b/test/battle/ability/dancer.c
@@ -246,3 +246,57 @@ DOUBLE_BATTLE_TEST("Dancer doesn't call a move that didn't execute due to Powder
         }
     }
 }
+
+
+DOUBLE_BATTLE_TEST("Dancer still activates after Red Card")
+{
+    GIVEN {
+        PLAYER(SPECIES_WOBBUFFET) ;
+        PLAYER(SPECIES_ORICORIO) { Ability(ABILITY_DANCER); }
+        PLAYER(SPECIES_CHANSEY);
+        OPPONENT(SPECIES_WOBBUFFET) { Item(ITEM_RED_CARD); }
+        OPPONENT(SPECIES_BULBASAUR);
+        OPPONENT(SPECIES_SHUCKLE);
+    } WHEN {
+        TURN { MOVE(playerLeft, MOVE_FIERY_DANCE, target: opponentLeft); }
+    } SCENE {
+        MESSAGE("Wobbuffet used Fiery Dance!");
+        ANIMATION(ANIM_TYPE_MOVE, MOVE_FIERY_DANCE, playerLeft);
+        HP_BAR(opponentLeft);
+        // Red card trigger
+        ANIMATION(ANIM_TYPE_GENERAL, B_ANIM_HELD_ITEM_EFFECT, opponentLeft);
+        MESSAGE("The opposing Wobbuffet held up its Red Card against Wobbuffet!");
+        MESSAGE("Chansey was dragged out!");
+        // Dancer
+        ABILITY_POPUP(playerRight, ABILITY_DANCER);
+        ANIMATION(ANIM_TYPE_MOVE, MOVE_FIERY_DANCE, playerRight);
+        HP_BAR(opponentLeft);
+    }
+}
+
+DOUBLE_BATTLE_TEST("Dancer still activate after Red Card even if blocked by Suction Cups")
+{
+    GIVEN {
+        PLAYER(SPECIES_WOBBUFFET) { Ability(ABILITY_SUCTION_CUPS); }
+        PLAYER(SPECIES_ORICORIO) { Ability(ABILITY_DANCER); }
+        PLAYER(SPECIES_CHANSEY);
+        OPPONENT(SPECIES_WOBBUFFET) { Item(ITEM_RED_CARD); }
+        OPPONENT(SPECIES_BULBASAUR);
+        OPPONENT(SPECIES_SHUCKLE);
+    } WHEN {
+        TURN { MOVE(playerLeft, MOVE_FIERY_DANCE, target: opponentLeft); }
+    } SCENE {
+        MESSAGE("Wobbuffet used Fiery Dance!");
+        ANIMATION(ANIM_TYPE_MOVE, MOVE_FIERY_DANCE, playerLeft);
+        HP_BAR(opponentLeft);
+        // red card trigger
+        ANIMATION(ANIM_TYPE_GENERAL, B_ANIM_HELD_ITEM_EFFECT, opponentLeft);
+        MESSAGE("The opposing Wobbuffet held up its Red Card against Wobbuffet!");
+        MESSAGE("Wobbuffet anchors itself with Suction Cups!");
+        NOT MESSAGE("Chansey was dragged out!");
+        // Dancer
+        ABILITY_POPUP(playerRight, ABILITY_DANCER);
+        ANIMATION(ANIM_TYPE_MOVE, MOVE_FIERY_DANCE, playerRight);
+        HP_BAR(opponentLeft);
+    }
+}
diff --git a/test/battle/hold_effect/red_card.c b/test/battle/hold_effect/red_card.c
index 88b208cf508e..8513e795b9f1 100644
--- a/test/battle/hold_effect/red_card.c
+++ b/test/battle/hold_effect/red_card.c
@@ -520,3 +520,4 @@ SINGLE_BATTLE_TEST("Red Card activates before Eject Pack")
         ANIMATION(ANIM_TYPE_MOVE, MOVE_TACKLE, opponent);
     }
 }
+


Reviews:
user: login: AlexOn1ine
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-03-03T20:26:00Z
user: login: ghoulslash
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-03-03T22:01:28Z
user: login: AlexOn1ine
state: APPROVED
author_association: COLLABORATOR
submitted_at: 2025-03-03T22:02:44Z

Review comments:
diff_hunk: @@ -520,3 +520,57 @@ SINGLE_BATTLE_TEST("Red Card activates before Eject Pack")
         ANIMATION(ANIM_TYPE_MOVE, MOVE_TACKLE, opponent);
     }
 }
+
+DOUBLE_BATTLE_TEST("Dancer still activates after Red Card")
path: test/battle/hold_effect/red_card.c
user: login: AlexOn1ine
body: I feel like both tests are more suited in the dancer test file. 
created_at: 2025-03-03T20:26:00Z
author_association: COLLABORATOR
diff_hunk: @@ -520,3 +520,57 @@ SINGLE_BATTLE_TEST("Red Card activates before Eject Pack")
         ANIMATION(ANIM_TYPE_MOVE, MOVE_TACKLE, opponent);
     }
 }
+
+DOUBLE_BATTLE_TEST("Dancer still activates after Red Card")
path: test/battle/hold_effect/red_card.c
user: login: ghoulslash
body: done
created_at: 2025-03-03T22:01:28Z
author_association: COLLABORATOR

PR comments:
user: login: AlexOn1ine
created_at: 2025-03-03T20:30:55Z
body: > Dancer also overwrites gBattlerAttacker, but restoring it might not be straightforward due to jumptocalledmove.. maybe another MOVEEND_RESTORE_ATTACKER case afterwards.

I'm not sure if there is an issue in general. Once Dancer activates moveend for previous attacker is over and we shouldn't need the restored attacker, at least I can't think of a situation where it would be needed. Maybe I'm overlooking something.
author_association: COLLABORATOR
user: login: ghoulslash
created_at: 2025-03-03T20:54:00Z
body: > > Dancer also overwrites gBattlerAttacker, but restoring it might not be straightforward due to jumptocalledmove.. maybe another MOVEEND_RESTORE_ATTACKER case afterwards.
> 
> I'm not sure if there is an issue in general. Once Dancer activates moveend for previous attacker is over and we shouldn't need the restored attacker, at least I can't think of a situation where it would be needed. Maybe I'm overlooking something.

The only case I could think of is `MOVEEND_SAME_MOVE_TURNS` occurs after Dancer, so Echoed Voice/Metronome would not get updated properly
author_association: COLLABORATOR
user: login: AlexOn1ine
created_at: 2025-03-03T21:12:44Z
body: > > > Dancer also overwrites gBattlerAttacker, but restoring it might not be straightforward due to jumptocalledmove.. maybe another MOVEEND_RESTORE_ATTACKER case afterwards.
> > 
> > 
> > I'm not sure if there is an issue in general. Once Dancer activates moveend for previous attacker is over and we shouldn't need the restored attacker, at least I can't think of a situation where it would be needed. Maybe I'm overlooking something.
> 
> The only case I could think of is `MOVEEND_SAME_MOVE_TURNS` occurs after Dancer, so Echoed Voice/Metronome would not get updated properly

oh, damn. ye then we need to open an issue for that. 
author_association: COLLABORATOR