Add SUB_HIT check to tests (#8413)

Co-authored-by: Hedara <hedara90@gmail.com>
This commit is contained in:
hedara90 2025-12-16 19:19:34 +01:00 committed by GitHub
parent 3feeebce9b
commit ec1a283b1b
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
6 changed files with 267 additions and 0 deletions

View File

@ -464,6 +464,20 @@ If the expected status icon is parametrized the corresponding `STATUS1` constant
STATUS_ICON(player, status1);
```
### `SUB_HIT`
`SUB_HIT(battler, captureDamage: | subBreak:)`
Causes the test to fail the test to fail if a Substitute for the specified battler doesn't take damage.
If `captureDamage` is used, the damage the substitute takes is written to the supplied pointer.
```
u16 damage;
...
SUB_HIT(player, captureDamage: &damage);
```
If `subBreak` is set to `TRUE`, the test will fail unless the substitute breaks. And if set to `FALSE`, the test will fail unless the substitute survives.
```
SUB_HIT(player, subBreak: TRUE);
```
### `NOT`
`NOT sceneCommand`
Causes the test to fail if the `SCENE` command succeeds before the following command succeeds.

View File

@ -606,6 +606,7 @@ enum
QUEUED_ABILITY_POPUP_EVENT,
QUEUED_ANIMATION_EVENT,
QUEUED_HP_EVENT,
QUEUED_SUB_HIT_EVENT,
QUEUED_EXP_EVENT,
QUEUED_MESSAGE_EVENT,
QUEUED_STATUS_EVENT,
@ -635,6 +636,14 @@ struct QueuedHPEvent
u32 address:28;
};
struct QueuedSubHitEvent
{
u32 battlerId:3;
u32 checkBreak:1;
u32 breakSub:1;
u32 address:27;
};
struct QueuedExpEvent
{
u32 battlerId:3;
@ -664,6 +673,7 @@ struct QueuedEvent
struct QueuedAbilityEvent ability;
struct QueuedAnimationEvent animation;
struct QueuedHPEvent hp;
struct QueuedSubHitEvent subHit;
struct QueuedExpEvent exp;
struct QueuedMessageEvent message;
struct QueuedStatusEvent status;
@ -1173,6 +1183,7 @@ void SendOut(u32 sourceLine, struct BattlePokemon *, u32 partyIndex);
#define ABILITY_POPUP(battler, ...) QueueAbility(__LINE__, battler, (struct AbilityEventContext) { __VA_ARGS__ })
#define ANIMATION(type, id, ...) QueueAnimation(__LINE__, type, id, (struct AnimationEventContext) { __VA_ARGS__ })
#define HP_BAR(battler, ...) QueueHP(__LINE__, battler, (struct HPEventContext) { R_APPEND_TRUE(__VA_ARGS__) })
#define SUB_HIT(battler, ...) QueueSubHit(__LINE__, battler, (struct SubHitEventContext) { R_APPEND_TRUE(__VA_ARGS__) })
#define EXPERIENCE_BAR(battler, ...) QueueExp(__LINE__, battler, (struct ExpEventContext) { R_APPEND_TRUE(__VA_ARGS__) })
// Static const is needed to make the modern compiler put the pattern variable in the .rodata section, instead of putting it on stack(which can break the game).
#define MESSAGE(pattern) do {static const u8 msg[] = _(pattern); QueueMessage(__LINE__, msg);} while (0)
@ -1225,6 +1236,15 @@ struct HPEventContext
bool8 explicitCaptureDamage;
};
struct SubHitEventContext
{
u8 _;
bool8 subBreak;
bool8 explicitSubBreak;
u16 *captureDamage;
bool8 explicitCaptureDamage;
};
struct ExpEventContext
{
u8 _;
@ -1253,6 +1273,7 @@ void CloseQueueGroup(u32 sourceLine);
void QueueAbility(u32 sourceLine, struct BattlePokemon *battler, struct AbilityEventContext);
void QueueAnimation(u32 sourceLine, u32 type, u32 id, struct AnimationEventContext);
void QueueHP(u32 sourceLine, struct BattlePokemon *battler, struct HPEventContext);
void QueueSubHit(u32 sourceLine, struct BattlePokemon *battler, struct SubHitEventContext);
void QueueExp(u32 sourceLine, struct BattlePokemon *battler, struct ExpEventContext);
void QueueMessage(u32 sourceLine, const u8 *pattern);
void QueueStatus(u32 sourceLine, struct BattlePokemon *battler, struct StatusEventContext);

View File

@ -16,6 +16,7 @@ enum Gimmick;
void TestRunner_Battle_RecordAbilityPopUp(u32 battlerId, enum Ability ability);
void TestRunner_Battle_RecordAnimation(u32 animType, u32 animId);
void TestRunner_Battle_RecordHP(u32 battlerId, u32 oldHP, u32 newHP);
void TestRunner_Battle_RecordSubHit(u32 battlerId, u32 damage, bool32 broke);
void TestRunner_Battle_RecordExp(u32 battlerId, u32 oldExp, u32 newExp);
void TestRunner_Battle_RecordMessage(const u8 *message);
void TestRunner_Battle_RecordStatus1(u32 battlerId, u32 status1);
@ -39,6 +40,7 @@ u32 TestRunner_Battle_GetForcedEnvironment(void);
#define TestRunner_Battle_RecordAbilityPopUp(...) (void)0
#define TestRunner_Battle_RecordAnimation(...) (void)0
#define TestRunner_Battle_RecordHP(...) (void)0
#define TestRunner_Battle_RecordSubHit(...) (void)0
#define TestRunner_Battle_RecordExp(...) (void)0
#define TestRunner_Battle_RecordMessage(...) (void)0
#define TestRunner_Battle_RecordStatus1(...) (void)0

View File

@ -2365,10 +2365,12 @@ static void MoveDamageDataHpUpdate(u32 battler, u32 scriptBattler, const u8 *nex
{
if (gDisableStructs[battler].substituteHP >= gBattleStruct->moveDamage[battler])
{
TestRunner_Battle_RecordSubHit(battler, gBattleStruct->moveDamage[battler], FALSE);
gDisableStructs[battler].substituteHP -= gBattleStruct->moveDamage[battler];
}
else
{
TestRunner_Battle_RecordSubHit(battler, gDisableStructs[battler].substituteHP, TRUE);
gBattleStruct->moveDamage[battler] = gDisableStructs[battler].substituteHP;
gDisableStructs[battler].substituteHP = 0;
}

View File

@ -69,4 +69,111 @@ SINGLE_BATTLE_TEST("Substitute's HP cost doesn't trigger effects that trigger on
}
}
SINGLE_BATTLE_TEST("Substitute hits are detected by SUB_HIT")
{
GIVEN {
PLAYER(SPECIES_WOBBUFFET);
OPPONENT(SPECIES_WOBBUFFET);
} WHEN {
TURN { MOVE(player, MOVE_SUBSTITUTE); MOVE(opponent, MOVE_SCRATCH); }
} SCENE {
SUB_HIT(player);
}
}
SINGLE_BATTLE_TEST("Substitute hits are detected by SUB_HIT, break TRUE")
{
GIVEN {
PLAYER(SPECIES_WOBBUFFET) { Level(1); }
OPPONENT(SPECIES_WOBBUFFET);
} WHEN {
TURN { MOVE(player, MOVE_SUBSTITUTE); MOVE(opponent, MOVE_SCRATCH); }
} SCENE {
SUB_HIT(player, subBreak: TRUE);
}
}
SINGLE_BATTLE_TEST("Substitute hits are detected by SUB_HIT, break FALSE")
{
GIVEN {
PLAYER(SPECIES_WOBBUFFET) { Level(100); }
OPPONENT(SPECIES_WOBBUFFET);
} WHEN {
TURN { MOVE(player, MOVE_SUBSTITUTE); MOVE(opponent, MOVE_SCRATCH); }
} SCENE {
SUB_HIT(player, subBreak: FALSE);
}
}
SINGLE_BATTLE_TEST("Substitute hits are detected by SUB_HIT, records damage")
{
u16 damage;
GIVEN {
PLAYER(SPECIES_WOBBUFFET);
OPPONENT(SPECIES_WOBBUFFET);
} WHEN {
TURN { MOVE(player, MOVE_SUBSTITUTE); MOVE(opponent, MOVE_SCRATCH); }
} SCENE {
SUB_HIT(player, captureDamage: &damage);
} THEN {
EXPECT_GT(damage, 0);
}
}
SINGLE_BATTLE_TEST("Substitute hits are detected by SUB_HIT, records damage, break FALSE")
{
u16 damage;
GIVEN {
PLAYER(SPECIES_WOBBUFFET);
OPPONENT(SPECIES_WOBBUFFET);
} WHEN {
TURN { MOVE(player, MOVE_SUBSTITUTE); MOVE(opponent, MOVE_SCRATCH); }
} SCENE {
SUB_HIT(player, captureDamage: &damage, subBreak: FALSE);
} THEN {
EXPECT_GT(damage, 0);
}
}
SINGLE_BATTLE_TEST("Substitute hits are detected by SUB_HIT, records damage, break TRUE")
{
u16 damage;
GIVEN {
PLAYER(SPECIES_WOBBUFFET) { Level(1); }
OPPONENT(SPECIES_WOBBUFFET);
} WHEN {
TURN { MOVE(player, MOVE_SUBSTITUTE); MOVE(opponent, MOVE_SCRATCH); }
} SCENE {
SUB_HIT(player, captureDamage: &damage, subBreak: TRUE);
} THEN {
EXPECT_GT(damage, 0);
}
}
SINGLE_BATTLE_TEST("Substitute hits are detected by SUB_HIT, break TRUE, failing")
{
KNOWN_FAILING; // For testing purposes
GIVEN {
PLAYER(SPECIES_WOBBUFFET) { Level(100); }
OPPONENT(SPECIES_WOBBUFFET);
} WHEN {
TURN { MOVE(player, MOVE_SUBSTITUTE); MOVE(opponent, MOVE_SCRATCH); }
} SCENE {
SUB_HIT(player, subBreak: TRUE);
}
}
SINGLE_BATTLE_TEST("Substitute hits are detected by SUB_HIT, break FALSE, failing")
{
KNOWN_FAILING; // For testing purposes
GIVEN {
PLAYER(SPECIES_WOBBUFFET) { Level(1); }
OPPONENT(SPECIES_WOBBUFFET);
} WHEN {
TURN { MOVE(player, MOVE_SUBSTITUTE); MOVE(opponent, MOVE_SCRATCH); }
} SCENE {
SUB_HIT(player, subBreak: FALSE);
}
}
TO_DO_BATTLE_TEST("Baton Pass passes Substitutes");

View File

@ -23,6 +23,7 @@
#undef TestRunner_Battle_RecordAbilityPopUp
#undef TestRunner_Battle_RecordAnimation
#undef TestRunner_Battle_RecordHP
#undef TestRunner_Battle_RecordSubHit
#undef TestRunner_Battle_RecordMessage
#undef TestRunner_Battle_RecordStatus1
#undef TestRunner_Battle_AfterLastTurn
@ -946,6 +947,85 @@ void TestRunner_Battle_RecordHP(u32 battlerId, u32 oldHP, u32 newHP)
}
}
static s32 TrySubHit(s32 i, s32 n, u32 battlerId, u32 damage, bool32 broke)
{
struct QueuedSubHitEvent *event;
s32 iMax = i + n;
for (; i < iMax; i++)
{
if (DATA.queuedEvents[i].type != QUEUED_SUB_HIT_EVENT)
continue;
event = &DATA.queuedEvents[i].as.subHit;
if (event->battlerId == battlerId)
{
if (event->checkBreak)
{
if (event->breakSub && !broke)
return -1;
else if (!event->breakSub && broke)
return -1;
}
if (event->address <= 0xFFFF)
{
event->address = damage;
return i;
}
else
{
*(u16 *)(u32)(event->address) = damage;
return i;
}
}
}
return -1;
}
void TestRunner_Battle_RecordSubHit(u32 battlerId, u32 damage, bool32 broke)
{
s32 queuedEvent;
s32 match;
struct QueuedEvent *event;
if (DATA.trial.queuedEvent == DATA.queuedEventsCount)
return;
event = &DATA.queuedEvents[DATA.trial.queuedEvent];
switch (event->groupType)
{
case QUEUE_GROUP_NONE:
case QUEUE_GROUP_ONE_OF:
if (TrySubHit(DATA.trial.queuedEvent, event->groupSize, battlerId, damage, broke) != -1)
DATA.trial.queuedEvent += event->groupSize;
break;
case QUEUE_GROUP_NONE_OF:
queuedEvent = DATA.trial.queuedEvent;
do
{
if ((match = TrySubHit(queuedEvent, event->groupSize, battlerId, damage, broke)) != -1)
{
const char *filename = gTestRunnerState.test->filename;
u32 line = SourceLine(DATA.queuedEvents[match].sourceLineOffset);
Test_ExitWithResult(TEST_RESULT_FAIL, line, ":L%s:%d: Matched SUB_HIT", filename, line);
}
queuedEvent += event->groupSize;
if (queuedEvent == DATA.queuedEventsCount)
break;
event = &DATA.queuedEvents[queuedEvent];
if (event->groupType == QUEUE_GROUP_NONE_OF)
continue;
if (TrySubHit(queuedEvent, event->groupSize, battlerId, damage, broke) != -1)
DATA.trial.queuedEvent = queuedEvent + event->groupSize;
} while (FALSE);
break;
}
}
static const char *const sBattleActionNames[] =
{
[B_ACTION_USE_MOVE] = "MOVE",
@ -1505,6 +1585,7 @@ static const char *const sEventTypeMacros[] =
[QUEUED_ABILITY_POPUP_EVENT] = "ABILITY_POPUP",
[QUEUED_ANIMATION_EVENT] = "ANIMATION",
[QUEUED_HP_EVENT] = "HP_BAR",
[QUEUED_SUB_HIT_EVENT] = "SUB_HIT",
[QUEUED_EXP_EVENT] = "EXPERIENCE_BAR",
[QUEUED_MESSAGE_EVENT] = "MESSAGE",
[QUEUED_STATUS_EVENT] = "STATUS_ICON",
@ -2995,6 +3076,46 @@ void QueueHP(u32 sourceLine, struct BattlePokemon *battler, struct HPEventContex
};
}
void QueueSubHit(u32 sourceLine, struct BattlePokemon *battler, struct SubHitEventContext ctx)
{
s32 battlerId = battler - gBattleMons;
bool32 breakSub = FALSE;
bool32 checkBreak = FALSE;
uintptr_t address;
INVALID_IF(!STATE->runScene, "SUB_HIT outside of SCENE");
if (DATA.queuedEventsCount == MAX_QUEUED_EVENTS)
Test_ExitWithResult(TEST_RESULT_ERROR, sourceLine, ":L%s:%d: SUB_HIT exceeds MAX_QUEUED_EVENTS", gTestRunnerState.test->filename, sourceLine);
address = 0;
if (ctx.explicitCaptureDamage)
{
INVALID_IF(ctx.captureDamage == NULL, "captureDamage is NULL");
*ctx.captureDamage = 0;
address = (uintptr_t)ctx.captureDamage;
}
if (ctx.explicitSubBreak)
{
checkBreak = TRUE;
if (ctx.subBreak)
breakSub = TRUE;
}
DATA.queuedEvents[DATA.queuedEventsCount++] = (struct QueuedEvent) {
.type = QUEUED_SUB_HIT_EVENT,
.sourceLineOffset = SourceLineOffset(sourceLine),
.groupType = QUEUE_GROUP_NONE,
.groupSize = 1,
.as = { .subHit = {
.battlerId = battlerId,
.checkBreak = checkBreak,
.breakSub = breakSub,
.address = address,
}},
};
}
void QueueExp(u32 sourceLine, struct BattlePokemon *battler, struct ExpEventContext ctx)
{
s32 battlerId = battler - gBattleMons;