From 127a7e96007a6fda833604f777ffc99af4c114d3 Mon Sep 17 00:00:00 2001 From: Eduardo Quezada Date: Sun, 28 Jan 2024 20:43:14 -0300 Subject: [PATCH 1/5] Backported gHeap alignment fix from upstream pret --- ld_script_modern.ld | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/ld_script_modern.ld b/ld_script_modern.ld index f3bf7b6798..5d9a7daf07 100644 --- a/ld_script_modern.ld +++ b/ld_script_modern.ld @@ -15,6 +15,11 @@ SECTIONS { ewram 0x2000000 (NOLOAD) : ALIGN(4) { + /* + We link malloc.o here to prevent `gHeap` from landing in the middle of EWRAM. + Otherwise this causes corruption issues on some ld versions + */ + gflib/malloc.o(ewram_data); src/*.o(ewram_data); gflib/*.o(ewram_data); From a2a38a57e0023b4d9b1dd3a823c5491c3f481a6b Mon Sep 17 00:00:00 2001 From: Eduardo Quezada D'Ottone Date: Mon, 29 Jan 2024 17:01:50 -0300 Subject: [PATCH 2/5] Added passing "KNOWN_FAILING" to test summary (#4063) * Added passing "KNOWN_FAILING" to test summary * Added test names for passing KNOWN_FAILING to summary * Removed unused function --- test/test_runner.c | 24 ++++++------- tools/mgba-rom-test-hydra/main.c | 58 ++++++++++++++++++++++++-------- 2 files changed, 54 insertions(+), 28 deletions(-) diff --git a/test/test_runner.c b/test/test_runner.c index 5ca397cd6c..901c6c86c4 100644 --- a/test/test_runner.c +++ b/test/test_runner.c @@ -30,7 +30,6 @@ void TestRunner_Battle(const struct Test *); static bool32 MgbaOpen_(void); static void MgbaExit_(u8 exitCode); -static s32 MgbaPuts_(const char *s); static s32 MgbaVPrintf_(const char *fmt, va_list va); static void Intr_Timer2(void); @@ -293,12 +292,6 @@ top: color = ""; } - if (gTestRunnerState.result == TEST_RESULT_PASS - && gTestRunnerState.result != gTestRunnerState.expectedResult) - { - MgbaPuts_("\e[31mPlease remove KNOWN_FAILING if this test intentionally PASSes\e[0m"); - } - switch (gTestRunnerState.result) { case TEST_RESULT_FAIL: @@ -313,7 +306,10 @@ top: } break; case TEST_RESULT_PASS: - result = "PASS"; + if (gTestRunnerState.result != gTestRunnerState.expectedResult) + result = "KNOWN_FAILING_PASS"; + else + result = "PASS"; break; case TEST_RESULT_ASSUMPTION_FAIL: result = "ASSUMPTION_FAIL"; @@ -341,7 +337,12 @@ top: } if (gTestRunnerState.result == TEST_RESULT_PASS) - MgbaPrintf_(":P%s%s\e[0m", color, result); + { + if (gTestRunnerState.result != gTestRunnerState.expectedResult) + MgbaPrintf_(":U%s%s\e[0m", color, result); + else + MgbaPrintf_(":P%s%s\e[0m", color, result); + } else if (gTestRunnerState.result == TEST_RESULT_ASSUMPTION_FAIL) MgbaPrintf_(":A%s%s\e[0m", color, result); else if (gTestRunnerState.result == TEST_RESULT_TODO) @@ -503,11 +504,6 @@ static void MgbaExit_(u8 exitCode) asm("swi 0x3" :: "r" (_exitCode)); } -static s32 MgbaPuts_(const char *s) -{ - return MgbaPrintf_("%s", s); -} - s32 MgbaPrintf_(const char *fmt, ...) { va_list va; diff --git a/tools/mgba-rom-test-hydra/main.c b/tools/mgba-rom-test-hydra/main.c index c2189ac0df..d4f39feb73 100644 --- a/tools/mgba-rom-test-hydra/main.c +++ b/tools/mgba-rom-test-hydra/main.c @@ -35,7 +35,7 @@ #define min(a, b) ((a) < (b) ? (a) : (b)) #define MAX_PROCESSES 32 // See also test/test.h -#define MAX_FAILED_TESTS_TO_LIST 100 +#define MAX_SUMMARY_TESTS_TO_LIST 50 #define MAX_TEST_LIST_BUFFER_LENGTH 256 #define ARRAY_COUNT(arr) (sizeof((arr)) / sizeof((arr)[0])) @@ -54,11 +54,13 @@ struct Runner char *output_buffer; int passes; int knownFails; + int knownFailsPassing; int todos; int assumptionFails; int fails; int results; - char failedTestNames[MAX_FAILED_TESTS_TO_LIST][MAX_TEST_LIST_BUFFER_LENGTH]; + char failedTestNames[MAX_SUMMARY_TESTS_TO_LIST][MAX_TEST_LIST_BUFFER_LENGTH]; + char knownFailingPassedTestNames[MAX_SUMMARY_TESTS_TO_LIST][MAX_TEST_LIST_BUFFER_LENGTH]; }; static unsigned nrunners = 0; @@ -107,6 +109,11 @@ static void handle_read(int i, struct Runner *runner) case 'K': runner->knownFails++; goto add_to_results; + case 'U': + if (runner->knownFailsPassing < MAX_SUMMARY_TESTS_TO_LIST) + strcpy(runner->knownFailingPassedTestNames[runner->knownFailsPassing], runner->test_name); + runner->knownFailsPassing++; + goto add_to_results; case 'T': runner->todos++; goto add_to_results; @@ -114,7 +121,7 @@ static void handle_read(int i, struct Runner *runner) runner->assumptionFails++; goto add_to_results; case 'F': - if (runner->fails < MAX_FAILED_TESTS_TO_LIST) + if (runner->fails < MAX_SUMMARY_TESTS_TO_LIST) strcpy(runner->failedTestNames[runner->fails], runner->test_name); runner->fails++; add_to_results: @@ -519,12 +526,14 @@ int main(int argc, char *argv[]) int exit_code = 0; int passes = 0; int knownFails = 0; + int knownFailsPassing = 0; int todos = 0; int assumptionFails = 0; int fails = 0; int results = 0; - char failedTestNames[MAX_FAILED_TESTS_TO_LIST * MAX_PROCESSES][MAX_TEST_LIST_BUFFER_LENGTH]; + char failedTestNames[MAX_SUMMARY_TESTS_TO_LIST * MAX_PROCESSES][MAX_TEST_LIST_BUFFER_LENGTH]; + char knownFailingPassedTestNames[MAX_SUMMARY_TESTS_TO_LIST * MAX_PROCESSES][MAX_TEST_LIST_BUFFER_LENGTH]; for (int i = 0; i < nrunners; i++) { @@ -540,18 +549,25 @@ int main(int argc, char *argv[]) exit_code = WEXITSTATUS(wstatus); passes += runners[i].passes; knownFails += runners[i].knownFails; + for (int j = 0; j < runners[i].knownFailsPassing; j++) + { + if (j < MAX_SUMMARY_TESTS_TO_LIST) + strcpy(knownFailingPassedTestNames[fails], runners[i].knownFailingPassedTestNames[j]); + knownFailsPassing++; + } todos += runners[i].todos; assumptionFails += runners[i].assumptionFails; for (int j = 0; j < runners[i].fails; j++) { - if (j < MAX_FAILED_TESTS_TO_LIST) + if (j < MAX_SUMMARY_TESTS_TO_LIST) strcpy(failedTestNames[fails], runners[i].failedTestNames[j]); fails++; } results += runners[i].results; } - qsort(failedTestNames, min(fails, MAX_FAILED_TESTS_TO_LIST), sizeof(char) * MAX_TEST_LIST_BUFFER_LENGTH, compare_strings); + qsort(failedTestNames, min(fails, MAX_SUMMARY_TESTS_TO_LIST), sizeof(char) * MAX_TEST_LIST_BUFFER_LENGTH, compare_strings); + qsort(knownFailingPassedTestNames, min(fails, MAX_SUMMARY_TESTS_TO_LIST), sizeof(char) * MAX_TEST_LIST_BUFFER_LENGTH, compare_strings); if (results == 0) { @@ -559,28 +575,42 @@ int main(int argc, char *argv[]) } else { + fprintf(stdout, "\n"); if (fails > 0) { - fprintf(stdout, "\n- Tests \e[31mFAILED\e[0m : %d Add TESTS='X' to run tests with the defined prefix.\n", fails); + fprintf(stdout, "- Tests \e[31mFAILED\e[0m : %d Add TESTS='X' to run tests with the defined prefix.\n", fails); for (int i = 0; i < fails; i++) { - if (i >= MAX_FAILED_TESTS_TO_LIST) + if (i >= MAX_SUMMARY_TESTS_TO_LIST) { - fprintf(stdout, " - \e[31mand %d more...\e[0m\n", fails - MAX_FAILED_TESTS_TO_LIST); + fprintf(stdout, " - \e[31mand %d more...\e[0m\n", fails - MAX_SUMMARY_TESTS_TO_LIST); break; } fprintf(stdout, " - \e[31m%s\e[0m.\n", failedTestNames[i]); } } - fprintf(stdout, "- Tests \e[32mPASSED\e[0m: %d\n", passes); + if (knownFailsPassing > 0) + { + fprintf(stdout, "- \e[31mKNOWN_FAILING_PASSED\e[0m: %d \e[31mPlease remove KNOWN_FAILING if these tests intentionally PASS\e[0m\n", knownFailsPassing); + for (int i = 0; i < knownFailsPassing; i++) + { + if (i >= MAX_SUMMARY_TESTS_TO_LIST) + { + fprintf(stdout, " - \e[31mand %d more...\e[0m\n", knownFailsPassing - MAX_SUMMARY_TESTS_TO_LIST); + break; + } + fprintf(stdout, " - \e[31m%s\e[0m.\n", knownFailingPassedTestNames[i]); + } + } + fprintf(stdout, "- Tests \e[32mPASSED\e[0m: %d\n", passes); if (knownFails > 0) - fprintf(stdout, "- Tests \e[33mKNOWN_FAILING\e[0m: %d\n", knownFails); + fprintf(stdout, "- Tests \e[33mKNOWN_FAILING\e[0m: %d\n", knownFails); if (todos > 0) - fprintf(stdout, "- Tests \e[33mTO_DO\e[0m: %d\n", todos); + fprintf(stdout, "- Tests \e[33mTO_DO\e[0m: %d\n", todos); if (assumptionFails > 0) - fprintf(stdout, "- \e[33mASSUMPTIONS_FAILED\e[0m: %d\n", assumptionFails); + fprintf(stdout, "- \e[33mASSUMPTIONS_FAILED\e[0m: %d\n", assumptionFails); - fprintf(stdout, "- Tests \e[34mTOTAL\e[0m: %d\n", results); + fprintf(stdout, "- Tests \e[34mTOTAL\e[0m: %d\n", results); } fprintf(stdout, "\n"); From 8b62828e3fe18c3e787272c0d3c1b205bb25c463 Mon Sep 17 00:00:00 2001 From: ravepossum <145081120+ravepossum@users.noreply.github.com> Date: Mon, 29 Jan 2024 15:51:10 -0500 Subject: [PATCH 3/5] Fix HGSS Dex dark mode search palette (#4095) Co-authored-by: ravepossum Co-authored-by: Eduardo Quezada D'Ottone --- graphics/pokedex/hgss/palette_search_results_dark.pal | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/graphics/pokedex/hgss/palette_search_results_dark.pal b/graphics/pokedex/hgss/palette_search_results_dark.pal index 5889ede7ac..1523b7d25f 100644 --- a/graphics/pokedex/hgss/palette_search_results_dark.pal +++ b/graphics/pokedex/hgss/palette_search_results_dark.pal @@ -7,11 +7,11 @@ JASC-PAL 201 201 201 169 169 169 129 129 129 -249 153 161 -233 49 49 -193 33 41 -145 17 33 -249 153 161 +106 106 106 +37 37 37 +106 106 106 +0 0 0 +106 106 106 193 33 41 141 251 184 52 66 162 From f5623595a0efff112a43eb183931f368169ab060 Mon Sep 17 00:00:00 2001 From: DizzyEggg Date: Tue, 30 Jan 2024 16:14:33 +0100 Subject: [PATCH 4/5] Fix AI trying to switch into the same mon 2 times (#4098) --- include/battle.h | 2 +- src/battle_ai_main.c | 23 +++++++++--- src/battle_ai_switch_items.c | 72 ++++++++++++++++++------------------ src/battle_main.c | 2 +- test/battle/ai.c | 28 ++++++++++++++ 5 files changed, 83 insertions(+), 44 deletions(-) diff --git a/include/battle.h b/include/battle.h index 21b3727ec3..0eba532cd9 100644 --- a/include/battle.h +++ b/include/battle.h @@ -323,7 +323,7 @@ struct AiLogicData bool8 shouldSwitchMon; // Because all available moves have no/little effect. Each bit per battler. u8 monToSwitchId[MAX_BATTLERS_COUNT]; // ID of the mon to switch. bool8 weatherHasEffect; // The same as WEATHER_HAS_EFFECT. Stored here, so it's called only once. - u8 mostSuitableMonId; // Stores result of GetMostSuitableMonToSwitchInto, which decides which generic mon the AI would switch into if they decide to switch. This can be overruled by specific mons found in ShouldSwitch; the final resulting mon is stored in AI_monToSwitchIntoId. + u8 mostSuitableMonId[MAX_BATTLERS_COUNT]; // Stores result of GetMostSuitableMonToSwitchInto, which decides which generic mon the AI would switch into if they decide to switch. This can be overruled by specific mons found in ShouldSwitch; the final resulting mon is stored in AI_monToSwitchIntoId. struct SwitchinCandidate switchinCandidate; // Struct used for deciding which mon to switch to in battle_ai_switch_items.c }; diff --git a/src/battle_ai_main.c b/src/battle_ai_main.c index d2e3816872..130770fec4 100644 --- a/src/battle_ai_main.c +++ b/src/battle_ai_main.c @@ -5,6 +5,7 @@ #include "battle_anim.h" #include "battle_ai_util.h" #include "battle_ai_main.h" +#include "battle_controllers.h" #include "battle_factory.h" #include "battle_setup.h" #include "battle_z_move.h" @@ -414,11 +415,21 @@ void SetAiLogicDataForTurn(struct AiLogicData *aiData) } } -static bool32 AI_SwitchMonIfSuitable(u32 battler) +static bool32 AI_SwitchMonIfSuitable(u32 battler, bool32 doubleBattle) { - u32 monToSwitchId = AI_DATA->mostSuitableMonId; - if (monToSwitchId != PARTY_SIZE) + u32 monToSwitchId = AI_DATA->mostSuitableMonId[battler]; + if (monToSwitchId != PARTY_SIZE && IsValidForBattle(&GetBattlerParty(battler)[monToSwitchId])) { + gBattleMoveDamage = monToSwitchId; + // Edge case: See if partner already chose to switch into the same mon + if (doubleBattle) + { + u32 partner = BATTLE_PARTNER(battler); + if (AI_DATA->shouldSwitchMon & gBitTable[partner] && AI_DATA->monToSwitchId[partner] == monToSwitchId) + { + return FALSE; + } + } AI_DATA->shouldSwitchMon |= gBitTable[battler]; AI_DATA->monToSwitchId[battler] = monToSwitchId; return TRUE; @@ -455,7 +466,7 @@ static bool32 AI_ShouldSwitchIfBadMoves(u32 battler, bool32 doubleBattle) break; } } - if (i == MAX_BATTLERS_COUNT && AI_SwitchMonIfSuitable(battler)) + if (i == MAX_BATTLERS_COUNT && AI_SwitchMonIfSuitable(battler, doubleBattle)) return TRUE; } else @@ -466,7 +477,7 @@ static bool32 AI_ShouldSwitchIfBadMoves(u32 battler, bool32 doubleBattle) break; } - if (i == MAX_MON_MOVES && AI_SwitchMonIfSuitable(battler)) + if (i == MAX_MON_MOVES && AI_SwitchMonIfSuitable(battler, doubleBattle)) return TRUE; } @@ -478,7 +489,7 @@ static bool32 AI_ShouldSwitchIfBadMoves(u32 battler, bool32 doubleBattle) && IsTruantMonVulnerable(battler, gBattlerTarget) && gDisableStructs[battler].truantCounter && gBattleMons[battler].hp >= gBattleMons[battler].maxHP / 2 - && AI_SwitchMonIfSuitable(battler)) + && AI_SwitchMonIfSuitable(battler, doubleBattle)) { return TRUE; } diff --git a/src/battle_ai_switch_items.c b/src/battle_ai_switch_items.c index f4ea291767..bdde562c5c 100644 --- a/src/battle_ai_switch_items.c +++ b/src/battle_ai_switch_items.c @@ -65,7 +65,7 @@ void GetAIPartyIndexes(u32 battler, s32 *firstId, s32 *lastId) } // Note that as many return statements as possible are INTENTIONALLY put after all of the loops; -// the function can take a max of about 0.06s to run, and this prevents the player from identifying +// the function can take a max of about 0.06s to run, and this prevents the player from identifying // whether the mon will switch or not by seeing how long the delay is before they select a move static bool8 HasBadOdds(u32 battler) @@ -82,12 +82,12 @@ static bool8 HasBadOdds(u32 battler) return FALSE; // Won't bother configuring this for double battles - if (gBattleTypeFlags & BATTLE_TYPE_DOUBLE) + if (gBattleTypeFlags & BATTLE_TYPE_DOUBLE) return FALSE; - + opposingPosition = BATTLE_OPPOSITE(GetBattlerPosition(battler)); opposingBattler = GetBattlerAtPosition(opposingPosition); - + // Gets types of player (opposingBattler) and computer (battler) atkType1 = gBattleMons[opposingBattler].type1; atkType2 = gBattleMons[opposingBattler].type2; @@ -102,7 +102,7 @@ static bool8 HasBadOdds(u32 battler) if (aiMove != MOVE_NONE) { // Check if mon has an "important" status move - if (aiMoveEffect == EFFECT_REFLECT || aiMoveEffect == EFFECT_LIGHT_SCREEN + if (aiMoveEffect == EFFECT_REFLECT || aiMoveEffect == EFFECT_LIGHT_SCREEN || aiMoveEffect == EFFECT_SPIKES || aiMoveEffect == EFFECT_TOXIC_SPIKES || aiMoveEffect == EFFECT_STEALTH_ROCK || aiMoveEffect == EFFECT_STICKY_WEB || aiMoveEffect == EFFECT_LEECH_SEED || aiMoveEffect == EFFECT_EXPLOSION || aiMoveEffect == EFFECT_SLEEP || aiMoveEffect == EFFECT_YAWN || aiMoveEffect == EFFECT_TOXIC || aiMoveEffect == EFFECT_WILL_O_WISP || aiMoveEffect == EFFECT_PARALYZE @@ -168,23 +168,23 @@ static bool8 HasBadOdds(u32 battler) } // If we don't have any other viable options, don't switch out - if (AI_DATA->mostSuitableMonId == PARTY_SIZE) + if (AI_DATA->mostSuitableMonId[battler] == PARTY_SIZE) return FALSE; // Start assessing whether or not mon has bad odds // Jump straight to swtiching out in cases where mon gets OHKO'd if (((getsOneShot && gBattleMons[opposingBattler].speed > gBattleMons[battler].speed) // If the player OHKOs and outspeeds OR OHKOs, doesn't outspeed but isn't 2HKO'd - || (getsOneShot && gBattleMons[opposingBattler].speed <= gBattleMons[battler].speed && maxDamageDealt < gBattleMons[opposingBattler].hp / 2)) + || (getsOneShot && gBattleMons[opposingBattler].speed <= gBattleMons[battler].speed && maxDamageDealt < gBattleMons[opposingBattler].hp / 2)) && (gBattleMons[battler].hp >= gBattleMons[battler].maxHP / 2 // And the current mon has at least 1/2 their HP, or 1/4 HP and Regenerator - || (aiAbility == ABILITY_REGENERATOR - && gBattleMons[battler].hp >= gBattleMons[battler].maxHP / 4))) + || (aiAbility == ABILITY_REGENERATOR + && gBattleMons[battler].hp >= gBattleMons[battler].maxHP / 4))) { // 50% chance to stay in regardless - if (Random() % 2 == 0) + if (Random() % 2 == 0) return FALSE; // Switch mon out - *(gBattleStruct->AI_monToSwitchIntoId + battler) = PARTY_SIZE; + *(gBattleStruct->AI_monToSwitchIntoId + battler) = PARTY_SIZE; BtlController_EmitTwoReturnValues(battler, 1, B_ACTION_SWITCH, 0); return TRUE; } @@ -194,19 +194,19 @@ static bool8 HasBadOdds(u32 battler) { if (!hasSuperEffectiveMove // If the AI doesn't have a super effective move && (gBattleMons[battler].hp >= gBattleMons[battler].maxHP / 2 // And the current mon has at least 1/2 their HP, or 1/4 HP and Regenerator - || (aiAbility == ABILITY_REGENERATOR - && gBattleMons[battler].hp >= gBattleMons[battler].maxHP / 4))) + || (aiAbility == ABILITY_REGENERATOR + && gBattleMons[battler].hp >= gBattleMons[battler].maxHP / 4))) { // Then check if they have an important status move, which is worth using even in a bad matchup if(hasStatusMove) return FALSE; // 50% chance to stay in regardless - if (Random() % 2 == 0) + if (Random() % 2 == 0) return FALSE; // Switch mon out - *(gBattleStruct->AI_monToSwitchIntoId + battler) = PARTY_SIZE; + *(gBattleStruct->AI_monToSwitchIntoId + battler) = PARTY_SIZE; BtlController_EmitTwoReturnValues(battler, 1, B_ACTION_SWITCH, 0); return TRUE; } @@ -593,12 +593,12 @@ static bool8 ShouldSwitchIfAbilityBenefit(u32 battler) moduloChance = 4; //25% //Attempt to cure bad ailment if (gBattleMons[battler].status1 & (STATUS1_SLEEP | STATUS1_FREEZE | STATUS1_TOXIC_POISON) - && AI_DATA->mostSuitableMonId != PARTY_SIZE) + && AI_DATA->mostSuitableMonId[battler] != PARTY_SIZE) break; //Attempt to cure lesser ailment if ((gBattleMons[battler].status1 & STATUS1_ANY) && (gBattleMons[battler].hp >= gBattleMons[battler].maxHP / 2) - && AI_DATA->mostSuitableMonId != PARTY_SIZE + && AI_DATA->mostSuitableMonId[battler] != PARTY_SIZE && Random() % (moduloChance*chanceReducer) == 0) break; @@ -610,7 +610,7 @@ static bool8 ShouldSwitchIfAbilityBenefit(u32 battler) if (gBattleMons[battler].status1 & STATUS1_ANY) return FALSE; if ((gBattleMons[battler].hp <= ((gBattleMons[battler].maxHP * 2) / 3)) - && AI_DATA->mostSuitableMonId != PARTY_SIZE + && AI_DATA->mostSuitableMonId[battler] != PARTY_SIZE && Random() % (moduloChance*chanceReducer) == 0) break; @@ -785,7 +785,7 @@ static bool32 CanMonSurviveHazardSwitchin(u32 battler) if (ability == ABILITY_REGENERATOR) battlerHp = (battlerHp * 133) / 100; // Account for Regenerator healing - + hazardDamage = GetSwitchinHazardsDamage(battler, &gBattleMons[battler]); // Battler will faint to hazards, check to see if another mon can clear them @@ -840,13 +840,13 @@ static bool32 CanMonSurviveHazardSwitchin(u32 battler) } static bool32 ShouldSwitchIfEncored(u32 battler) -{ +{ // Only use this if AI_FLAG_SMART_SWITCHING is set for the trainer if (!(AI_THINKING_STRUCT->aiFlags & AI_FLAG_SMART_SWITCHING)) return FALSE; // If not Encored or if no good switchin, don't switch - if (gDisableStructs[battler].encoredMove == MOVE_NONE || AI_DATA->mostSuitableMonId == PARTY_SIZE) + if (gDisableStructs[battler].encoredMove == MOVE_NONE || AI_DATA->mostSuitableMonId[battler] == PARTY_SIZE) return FALSE; // Otherwise 50% chance to switch out @@ -879,7 +879,7 @@ static bool8 AreAttackingStatsLowered(u32 battler) // 50% chance if attack at -2 and have a good candidate mon else if (attackingStage == DEFAULT_STAT_STAGE - 2) { - if (AI_DATA->mostSuitableMonId != PARTY_SIZE && (Random() & 1)) + if (AI_DATA->mostSuitableMonId[battler] != PARTY_SIZE && (Random() & 1)) { *(gBattleStruct->AI_monToSwitchIntoId + battler) = PARTY_SIZE; BtlController_EmitTwoReturnValues(battler, 1, B_ACTION_SWITCH, 0); @@ -904,7 +904,7 @@ static bool8 AreAttackingStatsLowered(u32 battler) // 50% chance if attack at -2 and have a good candidate mon else if (spAttackingStage == DEFAULT_STAT_STAGE - 2) { - if (AI_DATA->mostSuitableMonId != PARTY_SIZE && (Random() & 1)) + if (AI_DATA->mostSuitableMonId[battler] != PARTY_SIZE && (Random() & 1)) { *(gBattleStruct->AI_monToSwitchIntoId + battler) = PARTY_SIZE; BtlController_EmitTwoReturnValues(battler, 1, B_ACTION_SWITCH, 0); @@ -1053,7 +1053,7 @@ void AI_TrySwitchOrUseItem(u32 battler) { if (*(gBattleStruct->AI_monToSwitchIntoId + battler) == PARTY_SIZE) { - s32 monToSwitchId = AI_DATA->mostSuitableMonId; + s32 monToSwitchId = AI_DATA->mostSuitableMonId[battler]; if (monToSwitchId == PARTY_SIZE) { if (!(gBattleTypeFlags & BATTLE_TYPE_DOUBLE)) @@ -1367,7 +1367,7 @@ static s32 GetSwitchinWeatherImpact(void) // Gets one turn of recurring healing static u32 GetSwitchinRecurringHealing(void) -{ +{ u32 recurringHealing = 0, maxHP = AI_DATA->switchinCandidate.battleMon.maxHP, ability = AI_DATA->switchinCandidate.battleMon.ability; u16 item = AI_DATA->switchinCandidate.battleMon.item; @@ -1439,9 +1439,9 @@ static u32 GetSwitchinStatusDamage(u32 battler) u32 statusDamage = 0; // Status condition damage - if ((status != 0) && AI_DATA->switchinCandidate.battleMon.ability != ABILITY_MAGIC_GUARD) + if ((status != 0) && AI_DATA->switchinCandidate.battleMon.ability != ABILITY_MAGIC_GUARD) { - if (status & STATUS1_BURN) + if (status & STATUS1_BURN) { #if B_BURN_DAMAGE >= GEN_7 statusDamage = maxHP / 16; @@ -1484,7 +1484,7 @@ static u32 GetSwitchinStatusDamage(u32 battler) if (tSpikesLayers != 0 && (defType1 != TYPE_POISON && defType2 != TYPE_POISON && ability != ABILITY_IMMUNITY && ability != ABILITY_POISON_HEAL && status == 0 - && !(heldItemEffect == HOLD_EFFECT_HEAVY_DUTY_BOOTS + && !(heldItemEffect == HOLD_EFFECT_HEAVY_DUTY_BOOTS && (((gFieldStatuses & STATUS_FIELD_MAGIC_ROOM) || ability == ABILITY_KLUTZ))) && heldItemEffect != HOLD_EFFECT_CURE_PSN && heldItemEffect != HOLD_EFFECT_CURE_STATUS && IsMonGrounded(heldItemEffect, ability, defType1, defType2))) @@ -1559,7 +1559,7 @@ static u32 GetSwitchinHitsToKO(s32 damageTaken, u32 battler) singleUseItemHeal = 1; } } - else if (currentHP < maxHP / CONFUSE_BERRY_HP_FRACTION + else if (currentHP < maxHP / CONFUSE_BERRY_HP_FRACTION && opposingAbility != ABILITY_UNNERVE && (item == ITEM_AGUAV_BERRY || item == ITEM_FIGY_BERRY || item == ITEM_IAPAPA_BERRY || item == ITEM_MAGO_BERRY || item == ITEM_WIKI_BERRY)) { @@ -1608,7 +1608,7 @@ static u16 GetSwitchinTypeMatchup(u32 opposingBattler, struct BattlePokemon batt // Check type matchup u16 typeEffectiveness = UQ_4_12(1.0); - u8 atkType1 = gSpeciesInfo[gBattleMons[opposingBattler].species].types[0], atkType2 = gSpeciesInfo[gBattleMons[opposingBattler].species].types[1], + u8 atkType1 = gSpeciesInfo[gBattleMons[opposingBattler].species].types[0], atkType2 = gSpeciesInfo[gBattleMons[opposingBattler].species].types[1], defType1 = battleMon.type1, defType2 = battleMon.type2; // Multiply type effectiveness by a factor depending on type matchup @@ -1669,7 +1669,7 @@ static s32 GetMaxDamagePlayerCouldDealToSwitchin(u32 battler, u32 opposingBattle // the Type Matchup code will prioritize switching into a mon with the best type matchup and also a super effective move, or just best type matchup if no super effective move is found // the Most Defensive code will prioritize switching into the mon that takes the most hits to KO, with a minimum of 4 hits required to be considered a valid option // the Baton Pass code will prioritize switching into a mon with Baton Pass if it can get in, boost, and BP out without being KO'd, and randomizes between multiple valid options -// the Revenge Killer code will prioritize, in order, OHKO and outspeeds / OHKO, slower but not 2HKO'd / 2HKO, outspeeds and not OHKO'd / 2HKO, slower but not 3HKO'd +// the Revenge Killer code will prioritize, in order, OHKO and outspeeds / OHKO, slower but not 2HKO'd / 2HKO, outspeeds and not OHKO'd / 2HKO, slower but not 3HKO'd // the Most Damage code will prioritize switching into whatever mon deals the most damage, which is generally not as good as having a good Type Matchup // Everything runs in the same loop to minimize computation time. This makes it harder to read, but hopefully the comments can guide you! @@ -1712,7 +1712,7 @@ static u32 GetBestMonIntegrated(struct Pokemon *party, int firstId, int lastId, InitializeSwitchinCandidate(&party[i]); // While not really invalid per say, not really wise to switch into this mon - if (AI_DATA->switchinCandidate.battleMon.ability == ABILITY_TRUANT && IsTruantMonVulnerable(battler, opposingBattler)) + if (AI_DATA->switchinCandidate.battleMon.ability == ABILITY_TRUANT && IsTruantMonVulnerable(battler, opposingBattler)) continue; // Get max number of hits for player to KO AI mon @@ -1748,7 +1748,7 @@ static u32 GetBestMonIntegrated(struct Pokemon *party, int firstId, int lastId, // Only do damage calc if switching after KO, don't need it otherwise and saves ~0.02s per turn if (isSwitchAfterKO && aiMove != MOVE_NONE && gBattleMoves[aiMove].power != 0) damageDealt = AI_CalcPartyMonDamage(aiMove, battler, opposingBattler, AI_DATA->switchinCandidate.battleMon, TRUE); - + // Check for Baton Pass; hitsToKO requirements mean mon can boost and BP without dying whether it's slower or not if (aiMove == MOVE_BATON_PASS && ((hitsToKO > hitsToKOThreshold + 1 && AI_DATA->switchinCandidate.battleMon.speed < playerMonSpeed) || (hitsToKO > hitsToKOThreshold && AI_DATA->switchinCandidate.battleMon.speed > playerMonSpeed))) bits |= gBitTable[i]; @@ -1857,7 +1857,7 @@ static u32 GetBestMonIntegrated(struct Pokemon *party, int firstId, int lastId, else if (typeMatchupId != PARTY_SIZE) return typeMatchupId; - + else if (batonPassId != PARTY_SIZE) return batonPassId; @@ -1932,7 +1932,7 @@ u8 GetMostSuitableMonToSwitchInto(u32 battler, bool32 switchAfterMonKOd) if (AI_THINKING_STRUCT->aiFlags & AI_FLAG_SMART_MON_CHOICES) { bestMonId = GetBestMonIntegrated(party, firstId, lastId, battler, opposingBattler, battlerIn1, battlerIn2, switchAfterMonKOd); - return bestMonId; + return bestMonId; } // This all handled by the GetBestMonIntegrated function if the AI_FLAG_SMART_MON_CHOICES flag is set @@ -2138,4 +2138,4 @@ static bool32 AI_OpponentCanFaintAiWithMod(u32 battler, u32 healAmount) } } return FALSE; -} \ No newline at end of file +} diff --git a/src/battle_main.c b/src/battle_main.c index d5608ec21b..49b8e8285a 100644 --- a/src/battle_main.c +++ b/src/battle_main.c @@ -4037,7 +4037,7 @@ static void HandleTurnActionSelectionState(void) if ((gBattleTypeFlags & BATTLE_TYPE_HAS_AI || IsWildMonSmart()) && (BattlerHasAi(battler) && !(gBattleTypeFlags & BATTLE_TYPE_PALACE))) { - AI_DATA->mostSuitableMonId = GetMostSuitableMonToSwitchInto(battler, FALSE); + AI_DATA->mostSuitableMonId[battler] = GetMostSuitableMonToSwitchInto(battler, FALSE); gBattleStruct->aiMoveOrAction[battler] = ComputeBattleAiScores(battler); } // fallthrough diff --git a/test/battle/ai.c b/test/battle/ai.c index 953cf639f7..2a11d1b272 100644 --- a/test/battle/ai.c +++ b/test/battle/ai.c @@ -660,3 +660,31 @@ AI_SINGLE_BATTLE_TEST("AI_FLAG_SMART_SWITCHING: AI will not switch out if Pokemo } } } + +AI_DOUBLE_BATTLE_TEST("AI will not try to switch for the same pokemon for 2 spots in a double battle") +{ + u32 flags; + + PARAMETRIZE {flags = AI_FLAG_SMART_SWITCHING; } + PARAMETRIZE {flags = 0; } + + GIVEN { + AI_FLAGS(AI_FLAG_CHECK_BAD_MOVE | AI_FLAG_CHECK_VIABILITY | AI_FLAG_TRY_TO_FAINT | flags); + PLAYER(SPECIES_RATTATA); + PLAYER(SPECIES_RATTATA); + // No moves to damage player. + OPPONENT(SPECIES_GENGAR) { Moves(MOVE_SHADOW_BALL); } + OPPONENT(SPECIES_HAUNTER) { Moves(MOVE_SHADOW_BALL); } + OPPONENT(SPECIES_GENGAR) { Moves(MOVE_SHADOW_BALL); } + OPPONENT(SPECIES_RATICATE) { Moves(MOVE_HEADBUTT); } + } WHEN { + TURN { EXPECT_SWITCH(opponentLeft, 3); }; + } SCENE { + MESSAGE("{PKMN} TRAINER LEAF withdrew Gengar!"); + MESSAGE("{PKMN} TRAINER LEAF sent out Raticate!"); + NONE_OF { + MESSAGE("{PKMN} TRAINER LEAF withdrew Haunter!"); + MESSAGE("{PKMN} TRAINER LEAF sent out Raticate!"); + } + } +} From 1a65894967b3d8ad21ce0a0e754a05480b30abd2 Mon Sep 17 00:00:00 2001 From: Alex <93446519+AlexOn1ine@users.noreply.github.com> Date: Wed, 31 Jan 2024 13:33:04 +0100 Subject: [PATCH 5/5] Fixes test description + small fix follow up (#4100) * Fixes test description + small fix follow up * fix fix * Apply suggestions from code review --------- Co-authored-by: Eduardo Quezada D'Ottone --- src/battle_util.c | 2 +- test/battle/ability/emergency_exit.c | 27 +++++++++++++++++++-------- 2 files changed, 20 insertions(+), 9 deletions(-) diff --git a/src/battle_util.c b/src/battle_util.c index ec7654caf0..167d5bf599 100644 --- a/src/battle_util.c +++ b/src/battle_util.c @@ -6794,7 +6794,7 @@ static u8 ItemHealHp(u32 battler, u32 itemId, bool32 end2, bool32 percentHeal) gBattlescriptCurrInstr = BattleScript_ItemHealHP_RemoveItemRet; } if (gBattleResources->flags->flags[battler] & RESOURCE_FLAG_EMERGENCY_EXIT - && GetNonDynamaxMaxHP(battler) > gBattleMons[battler].maxHP / 2) + && GetNonDynamaxHP(battler) >= GetNonDynamaxMaxHP(battler) / 2) gBattleResources->flags->flags[battler] &= ~RESOURCE_FLAG_EMERGENCY_EXIT; return ITEM_HP_CHANGE; diff --git a/test/battle/ability/emergency_exit.c b/test/battle/ability/emergency_exit.c index 73c7ebbe00..68724450e1 100644 --- a/test/battle/ability/emergency_exit.c +++ b/test/battle/ability/emergency_exit.c @@ -8,10 +8,7 @@ SINGLE_BATTLE_TEST("Emergency Exit switches out when taking 50% max-hp damage") OPPONENT(SPECIES_GOLISOPOD) { Ability(ABILITY_EMERGENCY_EXIT); MaxHP(263); HP(262); }; OPPONENT(SPECIES_WOBBUFFET); } WHEN { - TURN { - MOVE(player, MOVE_SUPER_FANG); - SEND_OUT(opponent, 1); - } + TURN { MOVE(player, MOVE_SUPER_FANG); SEND_OUT(opponent, 1); } } SCENE { ANIMATION(ANIM_TYPE_MOVE, MOVE_SUPER_FANG, player); HP_BAR(opponent); @@ -19,16 +16,14 @@ SINGLE_BATTLE_TEST("Emergency Exit switches out when taking 50% max-hp damage") } } -SINGLE_BATTLE_TEST("Emergency Exit switches out when taking 50% max-hp damage after a restore hp hold effect was used") +SINGLE_BATTLE_TEST("Emergency Exit does not switch out when going below 50% max-HP but healed via held item back above the threshold") { GIVEN { PLAYER(SPECIES_WOBBUFFET) OPPONENT(SPECIES_GOLISOPOD) { Ability(ABILITY_EMERGENCY_EXIT); MaxHP(263); HP(262); Item(ITEM_SITRUS_BERRY); }; OPPONENT(SPECIES_WOBBUFFET); } WHEN { - TURN { - MOVE(player, MOVE_SUPER_FANG); - } + TURN { MOVE(player, MOVE_SUPER_FANG); } } SCENE { ANIMATION(ANIM_TYPE_MOVE, MOVE_SUPER_FANG, player); HP_BAR(opponent); @@ -36,3 +31,19 @@ SINGLE_BATTLE_TEST("Emergency Exit switches out when taking 50% max-hp damage af NOT ABILITY_POPUP(opponent, ABILITY_EMERGENCY_EXIT); } } + +SINGLE_BATTLE_TEST("Emergency Exit switches out when going below 50% max-HP but healing via held item is not enough to go back above the threshold") +{ + GIVEN { + PLAYER(SPECIES_WOBBUFFET) + OPPONENT(SPECIES_GOLISOPOD) { Ability(ABILITY_EMERGENCY_EXIT); MaxHP(263); HP(133); Item(ITEM_ORAN_BERRY); }; + OPPONENT(SPECIES_WOBBUFFET); + } WHEN { + TURN { MOVE(player, MOVE_SUPER_FANG); SEND_OUT(opponent, 1); } + } SCENE { + ANIMATION(ANIM_TYPE_MOVE, MOVE_SUPER_FANG, player); + HP_BAR(opponent); + ANIMATION(ANIM_TYPE_GENERAL, B_ANIM_HELD_ITEM_EFFECT, opponent); + ABILITY_POPUP(opponent, ABILITY_EMERGENCY_EXIT); + } +}