diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 942f1f1f5..5393aa155 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -29,7 +29,7 @@ jobs: repository: pret/agbcc - name: Install binutils - run: sudo apt install gcc-arm-none-eabi binutils-arm-none-eabi + run: sudo apt install gcc-arm-none-eabi binutils-arm-none-eabi libelf-dev # build-essential, git, and libpng-dev are already installed # gcc-arm-none-eabi is only needed for the modern build # as an alternative to dkP @@ -41,10 +41,15 @@ jobs: working-directory: agbcc - name: Agbcc - run: make -j${nproc} all + run: make -j${nproc} -O all - name: Modern env: MODERN: 1 COMPARE: 0 - run: make -j${nproc} all + run: make -j${nproc} -O all + + - name: Test + run: | + make -j${nproc} -O pokeemerald-test.elf + make -j${nproc} check diff --git a/Makefile b/Makefile index 2644a9acd..696820cd9 100644 --- a/Makefile +++ b/Makefile @@ -79,6 +79,9 @@ ELF = $(ROM:.gba=.elf) MAP = $(ROM:.gba=.map) SYM = $(ROM:.gba=.sym) +TESTELF = $(ROM:.gba=-test.elf) +HEADLESSELF = $(ROM:.gba=-test-headless.elf) + C_SUBDIR = src GFLIB_SUBDIR = gflib ASM_SUBDIR = asm @@ -88,6 +91,7 @@ SONG_SUBDIR = sound/songs MID_SUBDIR = sound/songs/midi SAMPLE_SUBDIR = sound/direct_sound_samples CRY_SUBDIR = sound/direct_sound_samples/cries +TEST_SUBDIR = test C_BUILDDIR = $(OBJ_DIR)/$(C_SUBDIR) GFLIB_BUILDDIR = $(OBJ_DIR)/$(GFLIB_SUBDIR) @@ -95,6 +99,7 @@ ASM_BUILDDIR = $(OBJ_DIR)/$(ASM_SUBDIR) DATA_ASM_BUILDDIR = $(OBJ_DIR)/$(DATA_ASM_SUBDIR) SONG_BUILDDIR = $(OBJ_DIR)/$(SONG_SUBDIR) MID_BUILDDIR = $(OBJ_DIR)/$(MID_SUBDIR) +TEST_BUILDDIR = $(OBJ_DIR)/$(TEST_SUBDIR) ASFLAGS := -mcpu=arm7tdmi --defsym MODERN=$(MODERN) @@ -131,10 +136,13 @@ RAMSCRGEN := tools/ramscrgen/ramscrgen$(EXE) FIX := tools/gbafix/gbafix$(EXE) MAPJSON := tools/mapjson/mapjson$(EXE) JSONPROC := tools/jsonproc/jsonproc$(EXE) +PATCHELF := tools/patchelf/patchelf$(EXE) +ROMTEST ?= $(shell { command -v mgba-rom-test || command -v tools/mgba/mgba-rom-test$(EXE); } 2>/dev/null) +ROMTESTHYDRA := tools/mgba-rom-test-hydra/mgba-rom-test-hydra$(EXE) PERL := perl -TOOLDIRS := $(filter-out tools/agbcc tools/binutils,$(wildcard tools/*)) +TOOLDIRS := $(filter-out tools/mgba tools/agbcc tools/binutils,$(wildcard tools/*)) TOOLBASE = $(TOOLDIRS:tools/%=%) TOOLS = $(foreach tool,$(TOOLBASE),tools/$(tool)/$(tool)$(EXE)) @@ -150,7 +158,7 @@ MAKEFLAGS += --no-print-directory # Secondary expansion is required for dependency variables in object rules. .SECONDEXPANSION: -.PHONY: all rom clean compare tidy tools mostlyclean clean-tools $(TOOLDIRS) libagbsyscall modern tidymodern tidynonmodern +.PHONY: all rom clean compare tidy tools mostlyclean clean-tools $(TOOLDIRS) libagbsyscall modern tidymodern tidynonmodern check infoshell = $(foreach line, $(shell $1 | sed "s/ /__SPACE__/g"), $(info $(subst __SPACE__, ,$(line)))) @@ -158,7 +166,7 @@ infoshell = $(foreach line, $(shell $1 | sed "s/ /__SPACE__/g"), $(info $(subst # Disable dependency scanning for clean/tidy/tools # Use a separate minimal makefile for speed # Since we don't need to reload most of this makefile -ifeq (,$(filter-out all rom compare modern libagbsyscall syms,$(MAKECMDGOALS))) +ifeq (,$(filter-out all rom compare modern check libagbsyscall syms,$(MAKECMDGOALS))) $(call infoshell, $(MAKE) -f make_tools.mk) else NODEP ?= 1 @@ -182,6 +190,11 @@ C_SRCS_IN := $(wildcard $(C_SUBDIR)/*.c $(C_SUBDIR)/*/*.c $(C_SUBDIR)/*/*/*.c) C_SRCS := $(foreach src,$(C_SRCS_IN),$(if $(findstring .inc.c,$(src)),,$(src))) C_OBJS := $(patsubst $(C_SUBDIR)/%.c,$(C_BUILDDIR)/%.o,$(C_SRCS)) +TEST_SRCS_IN := $(wildcard $(TEST_SUBDIR)/*.c $(TEST_SUBDIR)/*/*.c $(TEST_SUBDIR)/*/*/*.c) +TEST_SRCS := $(foreach src,$(TEST_SRCS_IN),$(if $(findstring .inc.c,$(src)),,$(src))) +TEST_OBJS := $(patsubst $(TEST_SUBDIR)/%.c,$(TEST_BUILDDIR)/%.o,$(TEST_SRCS)) +TEST_OBJS_REL := $(patsubst $(OBJ_DIR)/%,%,$(TEST_OBJS)) + GFLIB_SRCS := $(wildcard $(GFLIB_SUBDIR)/*.c) GFLIB_OBJS := $(patsubst $(GFLIB_SUBDIR)/%.c,$(GFLIB_BUILDDIR)/%.o,$(GFLIB_SRCS)) @@ -206,7 +219,7 @@ MID_OBJS := $(patsubst $(MID_SUBDIR)/%.mid,$(MID_BUILDDIR)/%.o,$(MID_SRCS)) OBJS := $(C_OBJS) $(GFLIB_OBJS) $(C_ASM_OBJS) $(ASM_OBJS) $(DATA_ASM_OBJS) $(SONG_OBJS) $(MID_OBJS) OBJS_REL := $(patsubst $(OBJ_DIR)/%,%,$(OBJS)) -SUBDIRS := $(sort $(dir $(OBJS))) +SUBDIRS := $(sort $(dir $(OBJS) $(dir $(TEST_OBJS)))) $(shell mkdir -p $(SUBDIRS)) endif @@ -407,6 +420,14 @@ $(OBJ_DIR)/sym_common.ld: sym_common.txt $(C_OBJS) $(wildcard common_syms/*.txt) $(OBJ_DIR)/sym_ewram.ld: sym_ewram.txt $(RAMSCRGEN) ewram_data $< ENGLISH > $@ +# NOTE: Based on C_DEP above, but without NODEP and KEEP_TEMPS handling. +define TEST_DEP +$1: $2 $$(shell $(SCANINC) -I include -I tools/agbcc/include -I gflib -I test $2) + @echo "$$(CC1) -o $$@ $$<" + @$$(CPP) $$(CPPFLAGS) $$< | $$(PREPROC) $$< charmap.txt -i | $$(CC1) $$(CFLAGS) -o - - | cat - <(echo -e ".text\n\t.align\t2, 0") | $$(AS) $$(ASFLAGS) -o $$@ - +endef +$(foreach src, $(TEST_SRCS), $(eval $(call TEST_DEP,$(patsubst $(TEST_SUBDIR)/%.c,$(TEST_BUILDDIR)/%.o,$(src)),$(src),$(patsubst $(TEST_SUBDIR)/%.c,%,$(src))))) + ifeq ($(MODERN),0) LD_SCRIPT := ld_script.txt LD_SCRIPT_DEPS := $(OBJ_DIR)/sym_bss.ld $(OBJ_DIR)/sym_common.ld $(OBJ_DIR)/sym_ewram.ld @@ -429,6 +450,28 @@ $(ROM): $(ELF) modern: all +LD_SCRIPT_TEST := ld_script_test.txt + +$(OBJ_DIR)/ld_script_test.ld: $(LD_SCRIPT_TEST) $(LD_SCRIPT_DEPS) + cd $(OBJ_DIR) && sed "s#tools/#../../tools/#g" ../../$(LD_SCRIPT_TEST) > ld_script_test.ld + +$(TESTELF): $(OBJ_DIR)/ld_script_test.ld $(OBJS) $(TEST_OBJS) libagbsyscall + @echo "cd $(OBJ_DIR) && $(LD) -T ld_script_test.ld -o ../../$@ " + @cd $(OBJ_DIR) && $(LD) $(TESTLDFLAGS) -T ld_script_test.ld -o ../../$@ $(OBJS_REL) $(TEST_OBJS_REL) $(LIB) + $(FIX) $@ -t"$(TITLE)" -c$(GAME_CODE) -m$(MAKER_CODE) -r$(REVISION) --silent + $(PATCHELF) pokeemerald-test.elf gTestRunnerArgv "$(TESTS)\0" + +ifeq ($(GITHUB_REPOSITORY_OWNER),rh-hideout) +TEST_SKIP_IS_FAIL := \x01 +else +TEST_SKIP_IS_FAIL := \x00 +endif + +check: $(TESTELF) + @cp $< $(HEADLESSELF) + $(PATCHELF) $(HEADLESSELF) gTestRunnerHeadless '\x01' gTestRunnerSkipIsFail "$(TEST_SKIP_IS_FAIL)" + $(ROMTESTHYDRA) $(ROMTEST) $(HEADLESSELF) + libagbsyscall: @$(MAKE) -C libagbsyscall TOOLCHAIN=$(TOOLCHAIN) MODERN=$(MODERN) diff --git a/common_syms/main.txt b/common_syms/main.txt index a620083d1..f1f8076ad 100644 --- a/common_syms/main.txt +++ b/common_syms/main.txt @@ -7,3 +7,4 @@ gIntrTable gLinkVSyncDisabled IntrMain_Buffer gPcmDmaCounter +gAgbMainLoop_sp diff --git a/include/global.h b/include/global.h index 8e307e867..7324335d2 100644 --- a/include/global.h +++ b/include/global.h @@ -139,6 +139,14 @@ #define NUM_FLAG_BYTES ROUND_BITS_TO_BYTES(FLAGS_COUNT) #define NUM_ADDITIONAL_PHRASE_BYTES ROUND_BITS_TO_BYTES(NUM_ADDITIONAL_PHRASES) +// Calls m0/m1/.../m8 depending on how many arguments are passed. +#define VARARG_8(m, ...) CAT(m, NARG_8(__VA_ARGS__))(__VA_ARGS__) +#define NARG_8(...) NARG_8_(_, ##__VA_ARGS__, 8, 7, 6, 5, 4, 3, 2, 1, 0) +#define NARG_8_(_, a, b, c, d, e, f, g, h, N, ...) N + +#define CAT(a, b) CAT_(a, b) +#define CAT_(a, b) a ## b + // This produces an error at compile-time if expr is zero. // It looks like file.c:line: size of array `id' is negative #define STATIC_ASSERT(expr, id) typedef char id[(expr) ? 1 : -1]; diff --git a/include/main.h b/include/main.h index 5ccb20df8..eba04cbaa 100644 --- a/include/main.h +++ b/include/main.h @@ -57,6 +57,7 @@ extern u32 IntrMain_Buffer[]; extern s8 gPcmDmaCounter; void AgbMain(void); +void AgbMainLoop(void); void SetMainCallback2(MainCallback callback); void InitKeys(void); void SetVBlankCallback(IntrCallback callback); diff --git a/include/pokemon.h b/include/pokemon.h index e15faa3c3..89910d6ee 100644 --- a/include/pokemon.h +++ b/include/pokemon.h @@ -563,5 +563,6 @@ u16 MonTryLearningNewMoveEvolution(struct Pokemon *mon, bool8 firstMove); bool32 ShouldShowFemaleDifferences(u16 species, u32 personality); void TryToSetBattleFormChangeMoves(struct Pokemon *mon); u32 GetMonFriendshipScore(struct Pokemon *pokemon); +void UpdateMonPersonality(struct BoxPokemon *boxMon, u32 personality); #endif // GUARD_POKEMON_H diff --git a/include/recorded_battle.h b/include/recorded_battle.h index 9b8939403..fbe14a656 100644 --- a/include/recorded_battle.h +++ b/include/recorded_battle.h @@ -1,6 +1,51 @@ #ifndef GUARD_RECORDED_BATTLE_H #define GUARD_RECORDED_BATTLE_H +#include "constants/battle.h" + +#define BATTLER_RECORD_SIZE 664 + +struct RecordedBattleSave +{ + struct Pokemon playerParty[PARTY_SIZE]; + struct Pokemon opponentParty[PARTY_SIZE]; + u8 playersName[MAX_BATTLERS_COUNT][PLAYER_NAME_LENGTH + 1]; + u8 playersGender[MAX_BATTLERS_COUNT]; + u32 playersTrainerId[MAX_BATTLERS_COUNT]; + u8 playersLanguage[MAX_BATTLERS_COUNT]; + u32 rngSeed; + u32 battleFlags; + u8 playersBattlers[MAX_BATTLERS_COUNT]; + u16 opponentA; + u16 opponentB; + u16 partnerId; + u16 multiplayerId; + u8 lvlMode; + u8 frontierFacility; + u8 frontierBrainSymbol; + u8 battleScene:1; + u8 textSpeed:3; + u32 AI_scripts; + u8 recordMixFriendName[PLAYER_NAME_LENGTH + 1]; + u8 recordMixFriendClass; + u8 apprenticeId; + u16 easyChatSpeech[EASY_CHAT_BATTLE_WORDS_COUNT]; + u8 recordMixFriendLanguage; + u8 apprenticeLanguage; + u8 battleRecord[MAX_BATTLERS_COUNT][BATTLER_RECORD_SIZE]; + u32 checksum; +}; + +enum +{ + RECORDED_BYTE, // Generic. + RECORDED_ACTION_TYPE, + RECORDED_MOVE_SLOT, + RECORDED_MOVE_TARGET, + RECORDED_PARTY_INDEX, + RECORDED_BATTLE_PALACE_ACTION, +}; + extern u32 gRecordedBattleRngSeed; extern u32 gBattlePalaceMoveSelectionRngValue; extern u8 gRecordedBattleMultiplayerId; @@ -12,11 +57,12 @@ void RecordedBattle_Init(u8 mode); void RecordedBattle_SetTrainerInfo(void); void RecordedBattle_SetBattlerAction(u8 battlerId, u8 action); void RecordedBattle_ClearBattlerAction(u8 battlerId, u8 bytesToClear); -u8 RecordedBattle_GetBattlerAction(u8 battlerId); +u8 RecordedBattle_GetBattlerAction(u32 actionType, u8 battlerId); u8 RecordedBattle_BufferNewBattlerData(u8 *dst); void RecordedBattle_RecordAllBattlerData(u8 *data); bool32 CanCopyRecordedBattleSaveData(void); bool32 MoveRecordedBattleToSaveData(void); +void SetVariablesForRecordedBattle(struct RecordedBattleSave *); void PlayRecordedBattle(void (*CB2_After)(void)); u8 GetRecordedBattleFrontierFacility(void); u8 GetRecordedBattleFronterBrainSymbol(void); diff --git a/include/test_runner.h b/include/test_runner.h new file mode 100644 index 000000000..2fc0a55e3 --- /dev/null +++ b/include/test_runner.h @@ -0,0 +1,17 @@ +#ifndef GUARD_TEST_RUNNER_H +#define GUARD_TEST_RUNNER_H + +extern const bool8 gTestRunnerEnabled; +extern const bool8 gTestRunnerHeadless; +extern const bool8 gTestRunnerSkipIsFail; + +void TestRunner_Battle_RecordAbilityPopUp(u32 battlerId, u32 ability); +void TestRunner_Battle_RecordAnimation(u32 animType, u32 animId); +void TestRunner_Battle_RecordHP(u32 battlerId, u32 oldHP, u32 newHP); +void TestRunner_Battle_RecordMessage(const u8 *message); +void TestRunner_Battle_RecordStatus1(u32 battlerId, u32 status1); +void TestRunner_Battle_AfterLastTurn(void); + +void BattleTest_CheckBattleRecordActionType(u32 battlerId, u32 recordIndex, u32 actionType); + +#endif diff --git a/ld_script.txt b/ld_script.txt index 629649adf..4c3085f59 100644 --- a/ld_script.txt +++ b/ld_script.txt @@ -2,6 +2,7 @@ ENTRY(Start) gNumMusicPlayers = 4; gMaxLines = 0; +gInitialMainCB2 = CB2_InitCopyrightScreenAfterBootup; SECTIONS { . = 0x2000000; diff --git a/ld_script_modern.txt b/ld_script_modern.txt index dd4eeed6a..082d69429 100644 --- a/ld_script_modern.txt +++ b/ld_script_modern.txt @@ -2,6 +2,7 @@ ENTRY(Start) gNumMusicPlayers = 4; gMaxLines = 0; +gInitialMainCB2 = CB2_InitCopyrightScreenAfterBootup; SECTIONS { . = 0x2000000; diff --git a/ld_script_test.txt b/ld_script_test.txt new file mode 100644 index 000000000..3fcac51d4 --- /dev/null +++ b/ld_script_test.txt @@ -0,0 +1,140 @@ +ENTRY(Start) + +gNumMusicPlayers = 4; +gMaxLines = 0; +gInitialMainCB2 = CB2_TestRunner; + +SECTIONS { + . = 0x2000000; + + ewram (NOLOAD) : + ALIGN(4) + { + gHeap = .; + + . = 0x1C000; + + src/*.o(ewram_data); + gflib/*.o(ewram_data); + test/*.o(ewram_data); + + . = 0x40000; + } + + . = 0x3000000; + + iwram (NOLOAD) : + ALIGN(4) + { + /* .bss starts at 0x3000000 */ + src/*.o(.bss); + gflib/*.o(.bss); + data/*.o(.bss); + test/*.o(.bss); + *libc.a:*.o(.bss*); + *libgcc.a:*.o(.bss*); + *libnosys.a:*.o(.bss*); + + /* .bss.code starts at 0x3001AA8 */ + src/m4a.o(.bss.code); + + /* COMMON starts at 0x30022A8 */ + src/*.o(COMMON); + gflib/*.o(COMMON); + data/*.o(COMMON); + test/*.o(COMMON); + *libc.a:sbrkr.o(COMMON); + end = .; + . = 0x8000; + } + + . = 0x8000000; + + .text : + ALIGN(4) + { + src/rom_header.o(.text); + src/rom_header_gf.o(.text.*); + src/*.o(.text); + gflib/*.o(.text); + } =0 + + script_data : + ALIGN(4) + { + data/*.o(script_data); + } =0 + + lib_text : + ALIGN(4) + { + *libagbsyscall.a:*.o(.text*); + *libgcc.a:*.o(.text*); + *libc.a:*.o(.text*); + *libnosys.a:*.o(.text*); + } =0 + + .rodata : + ALIGN(4) + { + src/*.o(.rodata); + gflib/*.o(.rodata); + data/*.o(.rodata); + } =0 + + song_data : + ALIGN(4) + { + sound/songs/*.o(.rodata); + } =0 + + lib_rodata : + SUBALIGN(4) + { + *libgcc.a:*.o(.rodata*); + *libc.a:*.o(.rodata*); + *libc.a:*.o(.data*); + src/libisagbprn.o(.rodata); + } =0 + + tests : + ALIGN(4) + { + __start_tests = .; + test/*.o(.tests); + __stop_tests = .; + test/*.o(.text); + test/*.o(.rodata); + } =0 + + /* DWARF debug sections. + Symbols in the DWARF debugging sections are relative to the beginning + of the section so we begin them at 0. */ + + /* DWARF 1 */ + .debug 0 : { *(.debug) } + .line 0 : { *(.line) } + + /* GNU DWARF 1 extensions */ + .debug_srcinfo 0 : { *(.debug_srcinfo) } + .debug_sfnames 0 : { *(.debug_sfnames) } + + /* DWARF 1.1 and DWARF 2 */ + .debug_aranges 0 : { *(.debug_aranges) } + .debug_pubnames 0 : { *(.debug_pubnames) } + + /* DWARF 2 */ + .debug_info 0 : { *(.debug_info .gnu.linkonce.wi.*) } + .debug_abbrev 0 : { *(.debug_abbrev) } + .debug_line 0 : { *(.debug_line) } + .debug_frame 0 : { *(.debug_frame) } + .debug_str 0 : { *(.debug_str) } + .debug_loc 0 : { *(.debug_loc) } + .debug_macinfo 0 : { *(.debug_macinfo) } + + /* Discard everything not specifically mentioned above. */ + /DISCARD/ : + { + *(*); + } +} diff --git a/make_tools.mk b/make_tools.mk index 697897a69..36dbe8c90 100644 --- a/make_tools.mk +++ b/make_tools.mk @@ -1,7 +1,7 @@ MAKEFLAGS += --no-print-directory -TOOLDIRS := $(filter-out tools/agbcc tools/binutils,$(wildcard tools/*)) +TOOLDIRS := $(filter-out tools/mgba tools/agbcc tools/binutils,$(wildcard tools/*)) .PHONY: all $(TOOLDIRS) diff --git a/src/battle_anim.c b/src/battle_anim.c index d91f06d21..610c24321 100644 --- a/src/battle_anim.c +++ b/src/battle_anim.c @@ -16,6 +16,7 @@ #include "sound.h" #include "sprite.h" #include "task.h" +#include "test_runner.h" #include "constants/battle_anim.h" #include "constants/moves.h" @@ -217,12 +218,27 @@ void DoMoveAnim(u16 move) LaunchBattleAnimation(ANIM_TYPE_MOVE, move); } +static void Nop(void) +{ +} + void LaunchBattleAnimation(u32 animType, u32 animId) { s32 i; const u8 *const *animsTable; bool32 hideHpBoxes; + if (gTestRunnerEnabled) + { + TestRunner_Battle_RecordAnimation(animType, animId); + if (gTestRunnerHeadless) + { + gAnimScriptCallback = Nop; + gAnimScriptActive = FALSE; + return; + } + } + switch (animType) { case ANIM_TYPE_GENERAL: @@ -239,7 +255,7 @@ void LaunchBattleAnimation(u32 animType, u32 animId) break; } - hideHpBoxes = !(animType == ANIM_TYPE_MOVE && animId == MOVE_TRANSFORM); + hideHpBoxes = !(animType == ANIM_TYPE_MOVE && animId == MOVE_TRANSFORM); if (animType != ANIM_TYPE_MOVE) { switch (animId) diff --git a/src/battle_controller_recorded_opponent.c b/src/battle_controller_recorded_opponent.c index f1243ee2b..2e93d625d 100644 --- a/src/battle_controller_recorded_opponent.c +++ b/src/battle_controller_recorded_opponent.c @@ -22,6 +22,7 @@ #include "sound.h" #include "string_util.h" #include "task.h" +#include "test_runner.h" #include "text.h" #include "util.h" #include "window.h" @@ -1386,6 +1387,17 @@ static void RecordedOpponentHandlePrintString(void) gBattle_BG0_Y = 0; stringId = (u16 *)(&gBattleResources->bufferA[gActiveBattler][2]); BufferStringBattle(*stringId); + + if (gTestRunnerEnabled) + { + TestRunner_Battle_RecordMessage(gDisplayedStringBattle); + if (gTestRunnerHeadless) + { + RecordedOpponentBufferExecCompleted(); + return; + } + } + BattlePutTextOnWindow(gDisplayedStringBattle, B_WIN_MSG); gBattlerControllerFuncs[gActiveBattler] = CompleteOnInactiveTextPrinter; } @@ -1397,7 +1409,7 @@ static void RecordedOpponentHandlePrintSelectionString(void) static void RecordedOpponentHandleChooseAction(void) { - BtlController_EmitTwoReturnValues(BUFFER_B, RecordedBattle_GetBattlerAction(gActiveBattler), 0); + BtlController_EmitTwoReturnValues(BUFFER_B, RecordedBattle_GetBattlerAction(RECORDED_ACTION_TYPE, gActiveBattler), 0); RecordedOpponentBufferExecCompleted(); } @@ -1414,8 +1426,8 @@ static void RecordedOpponentHandleChooseMove(void) } else { - u8 moveId = RecordedBattle_GetBattlerAction(gActiveBattler); - u8 target = RecordedBattle_GetBattlerAction(gActiveBattler); + u8 moveId = RecordedBattle_GetBattlerAction(RECORDED_MOVE_SLOT, gActiveBattler); + u8 target = RecordedBattle_GetBattlerAction(RECORDED_MOVE_TARGET, gActiveBattler); BtlController_EmitTwoReturnValues(BUFFER_B, 10, moveId | (target << 8)); } @@ -1429,7 +1441,7 @@ static void RecordedOpponentHandleChooseItem(void) static void RecordedOpponentHandleChoosePokemon(void) { - *(gBattleStruct->monToSwitchIntoId + gActiveBattler) = RecordedBattle_GetBattlerAction(gActiveBattler); + *(gBattleStruct->monToSwitchIntoId + gActiveBattler) = RecordedBattle_GetBattlerAction(RECORDED_PARTY_INDEX, gActiveBattler); BtlController_EmitChosenMonReturnValue(BUFFER_B, *(gBattleStruct->monToSwitchIntoId + gActiveBattler), NULL); RecordedOpponentBufferExecCompleted(); } @@ -1442,22 +1454,23 @@ static void RecordedOpponentHandleCmd23(void) static void RecordedOpponentHandleHealthBarUpdate(void) { s16 hpVal; + s32 maxHP, curHP; LoadBattleBarGfx(0); hpVal = gBattleResources->bufferA[gActiveBattler][2] | (gBattleResources->bufferA[gActiveBattler][3] << 8); + maxHP = GetMonData(&gEnemyParty[gBattlerPartyIndexes[gActiveBattler]], MON_DATA_MAX_HP); + curHP = GetMonData(&gEnemyParty[gBattlerPartyIndexes[gActiveBattler]], MON_DATA_HP); + if (hpVal != INSTANT_HP_BAR_DROP) { - u32 maxHP = GetMonData(&gEnemyParty[gBattlerPartyIndexes[gActiveBattler]], MON_DATA_MAX_HP); - u32 curHP = GetMonData(&gEnemyParty[gBattlerPartyIndexes[gActiveBattler]], MON_DATA_HP); - SetBattleBarStruct(gActiveBattler, gHealthboxSpriteIds[gActiveBattler], maxHP, curHP, hpVal); + TestRunner_Battle_RecordHP(gActiveBattler, curHP, min(maxHP, max(0, curHP - hpVal))); } else { - u32 maxHP = GetMonData(&gEnemyParty[gBattlerPartyIndexes[gActiveBattler]], MON_DATA_MAX_HP); - SetBattleBarStruct(gActiveBattler, gHealthboxSpriteIds[gActiveBattler], maxHP, 0, hpVal); + TestRunner_Battle_RecordHP(gActiveBattler, curHP, 0); } gBattlerControllerFuncs[gActiveBattler] = CompleteOnHealthbarDone; @@ -1478,6 +1491,9 @@ static void RecordedOpponentHandleStatusIconUpdate(void) battlerId = gActiveBattler; gBattleSpritesDataPtr->healthBoxesData[battlerId].statusAnimActive = 0; gBattlerControllerFuncs[gActiveBattler] = CompleteOnFinishedStatusAnimation; + + if (gTestRunnerEnabled) + TestRunner_Battle_RecordStatus1(battlerId, GetMonData(&gEnemyParty[gBattlerPartyIndexes[battlerId]], MON_DATA_STATUS)); } } diff --git a/src/battle_controller_recorded_player.c b/src/battle_controller_recorded_player.c index 82d0a4334..1906df8a8 100644 --- a/src/battle_controller_recorded_player.c +++ b/src/battle_controller_recorded_player.c @@ -19,6 +19,7 @@ #include "sound.h" #include "string_util.h" #include "task.h" +#include "test_runner.h" #include "text.h" #include "util.h" #include "window.h" @@ -1394,6 +1395,17 @@ static void RecordedPlayerHandlePrintString(void) gBattle_BG0_Y = 0; stringId = (u16 *)(&gBattleResources->bufferA[gActiveBattler][2]); BufferStringBattle(*stringId); + + if (gTestRunnerEnabled) + { + TestRunner_Battle_RecordMessage(gDisplayedStringBattle); + if (gTestRunnerHeadless) + { + RecordedPlayerBufferExecCompleted(); + return; + } + } + BattlePutTextOnWindow(gDisplayedStringBattle, B_WIN_MSG); gBattlerControllerFuncs[gActiveBattler] = CompleteOnInactiveTextPrinter; } @@ -1407,7 +1419,7 @@ static void ChooseActionInBattlePalace(void) { if (gBattleCommunication[4] >= gBattlersCount / 2) { - BtlController_EmitTwoReturnValues(BUFFER_B, RecordedBattle_GetBattlerAction(gActiveBattler), 0); + BtlController_EmitTwoReturnValues(BUFFER_B, RecordedBattle_GetBattlerAction(RECORDED_BATTLE_PALACE_ACTION, gActiveBattler), 0); RecordedPlayerBufferExecCompleted(); } } @@ -1420,7 +1432,7 @@ static void RecordedPlayerHandleChooseAction(void) } else { - BtlController_EmitTwoReturnValues(BUFFER_B, RecordedBattle_GetBattlerAction(gActiveBattler), 0); + BtlController_EmitTwoReturnValues(BUFFER_B, RecordedBattle_GetBattlerAction(RECORDED_ACTION_TYPE, gActiveBattler), 0); RecordedPlayerBufferExecCompleted(); } } @@ -1438,8 +1450,8 @@ static void RecordedPlayerHandleChooseMove(void) } else { - u8 moveId = RecordedBattle_GetBattlerAction(gActiveBattler); - u8 target = RecordedBattle_GetBattlerAction(gActiveBattler); + u8 moveId = RecordedBattle_GetBattlerAction(RECORDED_MOVE_SLOT, gActiveBattler); + u8 target = RecordedBattle_GetBattlerAction(RECORDED_MOVE_TARGET, gActiveBattler); BtlController_EmitTwoReturnValues(BUFFER_B, 10, moveId | (target << 8)); } @@ -1453,7 +1465,7 @@ static void RecordedPlayerHandleChooseItem(void) static void RecordedPlayerHandleChoosePokemon(void) { - *(gBattleStruct->monToSwitchIntoId + gActiveBattler) = RecordedBattle_GetBattlerAction(gActiveBattler); + *(gBattleStruct->monToSwitchIntoId + gActiveBattler) = RecordedBattle_GetBattlerAction(RECORDED_PARTY_INDEX, gActiveBattler); BtlController_EmitChosenMonReturnValue(BUFFER_B, *(gBattleStruct->monToSwitchIntoId + gActiveBattler), NULL); RecordedPlayerBufferExecCompleted(); } @@ -1466,23 +1478,24 @@ static void RecordedPlayerHandleCmd23(void) static void RecordedPlayerHandleHealthBarUpdate(void) { s16 hpVal; + s32 maxHP, curHP; LoadBattleBarGfx(0); hpVal = gBattleResources->bufferA[gActiveBattler][2] | (gBattleResources->bufferA[gActiveBattler][3] << 8); + maxHP = GetMonData(&gPlayerParty[gBattlerPartyIndexes[gActiveBattler]], MON_DATA_MAX_HP); + curHP = GetMonData(&gPlayerParty[gBattlerPartyIndexes[gActiveBattler]], MON_DATA_HP); + if (hpVal != INSTANT_HP_BAR_DROP) { - u32 maxHP = GetMonData(&gPlayerParty[gBattlerPartyIndexes[gActiveBattler]], MON_DATA_MAX_HP); - u32 curHP = GetMonData(&gPlayerParty[gBattlerPartyIndexes[gActiveBattler]], MON_DATA_HP); - SetBattleBarStruct(gActiveBattler, gHealthboxSpriteIds[gActiveBattler], maxHP, curHP, hpVal); + TestRunner_Battle_RecordHP(gActiveBattler, curHP, min(maxHP, max(0, curHP - hpVal))); } else { - u32 maxHP = GetMonData(&gPlayerParty[gBattlerPartyIndexes[gActiveBattler]], MON_DATA_MAX_HP); - SetBattleBarStruct(gActiveBattler, gHealthboxSpriteIds[gActiveBattler], maxHP, 0, hpVal); UpdateHpTextInHealthbox(gHealthboxSpriteIds[gActiveBattler], HP_CURRENT, 0, maxHP); + TestRunner_Battle_RecordHP(gActiveBattler, curHP, 0); } gBattlerControllerFuncs[gActiveBattler] = CompleteOnHealthbarDone; @@ -1503,6 +1516,9 @@ static void RecordedPlayerHandleStatusIconUpdate(void) battlerId = gActiveBattler; gBattleSpritesDataPtr->healthBoxesData[battlerId].statusAnimActive = 0; gBattlerControllerFuncs[gActiveBattler] = CompleteOnFinishedStatusAnimation; + + if (gTestRunnerEnabled) + TestRunner_Battle_RecordStatus1(battlerId, GetMonData(&gPlayerParty[gBattlerPartyIndexes[battlerId]], MON_DATA_STATUS)); } } diff --git a/src/battle_interface.c b/src/battle_interface.c index 06973f621..9c0db8b07 100644 --- a/src/battle_interface.c +++ b/src/battle_interface.c @@ -28,6 +28,7 @@ #include "item.h" #include "item_icon.h" #include "item_use.h" +#include "test_runner.h" #include "constants/battle_anim.h" #include "constants/rgb.h" #include "constants/songs.h" @@ -3072,6 +3073,13 @@ void CreateAbilityPopUp(u8 battlerId, u32 ability, bool32 isDoubleBattle) const s16 (*coords)[2]; u8 spriteId1, spriteId2, battlerPosition, taskId; + if (gTestRunnerEnabled) + { + TestRunner_Battle_RecordAbilityPopUp(battlerId, ability); + if (gTestRunnerHeadless) + return; + } + if (gBattleScripting.abilityPopupOverwrite != 0) ability = gBattleScripting.abilityPopupOverwrite; @@ -3187,9 +3195,12 @@ static void SpriteCb_AbilityPopUp(struct Sprite *sprite) void DestroyAbilityPopUp(u8 battlerId) { - gSprites[gBattleStruct->abilityPopUpSpriteIds[battlerId][0]].tFrames = 0; - gSprites[gBattleStruct->abilityPopUpSpriteIds[battlerId][1]].tFrames = 0; - gBattleScripting.fixedPopup = FALSE; + if (gBattleStruct->activeAbilityPopUps & gBitTable[battlerId]) + { + gSprites[gBattleStruct->abilityPopUpSpriteIds[battlerId][0]].tFrames = 0; + gSprites[gBattleStruct->abilityPopUpSpriteIds[battlerId][1]].tFrames = 0; + gBattleScripting.fixedPopup = FALSE; + } } static void Task_FreeAbilityPopUpGfx(u8 taskId) diff --git a/src/battle_main.c b/src/battle_main.c index b7306eb16..f316846c6 100644 --- a/src/battle_main.c +++ b/src/battle_main.c @@ -45,6 +45,7 @@ #include "string_util.h" #include "strings.h" #include "task.h" +#include "test_runner.h" #include "text.h" #include "trig.h" #include "tv.h" @@ -474,7 +475,8 @@ const u8 *const gStatusConditionStringsTable[][2] = void CB2_InitBattle(void) { - MoveSaveBlocks_ResetHeap(); + if (!gTestRunnerEnabled) + MoveSaveBlocks_ResetHeap(); AllocateBattleResources(); AllocateBattleSpritesData(); AllocateMonSpritesGfx(); @@ -1805,6 +1807,8 @@ void CB2_QuitRecordedBattle(void) { m4aMPlayStop(&gMPlayInfo_SE1); m4aMPlayStop(&gMPlayInfo_SE2); + if (gTestRunnerEnabled) + TestRunner_Battle_AfterLastTurn(); FreeRestoreBattleData(); FreeAllWindowBuffers(); SetMainCallback2(gMain.savedCallback); @@ -5233,6 +5237,8 @@ static void HandleEndTurn_FinishBattle(void) } RecordedBattle_SetPlaybackFinished(); + if (gTestRunnerEnabled) + TestRunner_Battle_AfterLastTurn(); BeginFastPaletteFade(3); FadeOutMapMusic(5); #if B_TRAINERS_KNOCK_OFF_ITEMS == TRUE diff --git a/src/battle_message.c b/src/battle_message.c index 03641c77e..34542e58d 100644 --- a/src/battle_message.c +++ b/src/battle_message.c @@ -524,11 +524,11 @@ static const u8 sText_Trainer2LoseText[]; static const s8 sText_EnduredViaSturdy[] = _("{B_DEF_NAME_WITH_PREFIX} endured\nthe hit using {B_DEF_ABILITY}!"); static const s8 sText_PowerHerbActivation[] = _("{B_ATK_NAME_WITH_PREFIX} became fully charged\ndue to its {B_LAST_ITEM}!"); static const s8 sText_HurtByItem[] = _("{B_ATK_NAME_WITH_PREFIX} was hurt\nby its {B_LAST_ITEM}!"); -static const s8 sText_BadlyPoisonedByItem[] = _("{B_EFF_NAME_WITH_PREFIX} was badly \npoisoned by the {B_LAST_ITEM}!"); +static const s8 sText_BadlyPoisonedByItem[] = _("{B_EFF_NAME_WITH_PREFIX} was badly\npoisoned by the {B_LAST_ITEM}!"); static const s8 sText_BurnedByItem[] = _("{B_EFF_NAME_WITH_PREFIX} was burned\nby the {B_LAST_ITEM}!"); static const s8 sText_TargetAbilityActivates[] = _("{B_DEF_NAME_WITH_PREFIX}'s {B_DEF_ABILITY} activates!"); static const u8 sText_GravityIntensified[] = _("Gravity intensified!"); -static const u8 sText_TargetIdentified[] = _("{B_DEF_NAME_WITH_PREFIX} was \nidentified!"); +static const u8 sText_TargetIdentified[] = _("{B_DEF_NAME_WITH_PREFIX} was\nidentified!"); static const u8 sText_TargetWokeUp[] = _("{B_DEF_NAME_WITH_PREFIX} woke up!"); static const u8 sText_PkmnStoleAndAteItem[] = _("{B_ATK_NAME_WITH_PREFIX} stole and\nate {B_DEF_NAME_WITH_PREFIX}'s {B_LAST_ITEM}!"); static const u8 sText_TailWindBlew[] = _("The tailwind blew from\nbehind {B_ATK_TEAM2} team!"); @@ -597,7 +597,7 @@ static const u8 sText_TargetsStatWasMaxedOut[] = _("{B_DEF_NAME_WITH_PREFIX}'s { static const u8 sText_PoisonHealHpUp[] = _("The poisoning healed {B_ATK_NAME_WITH_PREFIX}\na little bit!"); static const u8 sText_BadDreamsDmg[] = _("{B_DEF_NAME_WITH_PREFIX} is tormented!"); static const u8 sText_MoldBreakerEnters[] = _("{B_SCR_ACTIVE_NAME_WITH_PREFIX} breaks the mold!"); -static const u8 sText_TeravoltEnters[] = _("{B_SCR_ACTIVE_NAME_WITH_PREFIX} is radiating \na bursting aura!"); +static const u8 sText_TeravoltEnters[] = _("{B_SCR_ACTIVE_NAME_WITH_PREFIX} is radiating\na bursting aura!"); static const u8 sText_TurboblazeEnters[] = _("{B_SCR_ACTIVE_NAME_WITH_PREFIX} is radiating\na blazing aura!"); static const u8 sText_SlowStartEnters[] = _("{B_SCR_ACTIVE_NAME_WITH_PREFIX} can't get it going!"); static const u8 sText_SlowStartEnd[] = _("{B_ATK_NAME_WITH_PREFIX} finally got\nits act together!"); @@ -642,7 +642,7 @@ static const u8 sText_TargetElectrified[] = _("The {B_DEF_NAME_WITH_PREFIX}'s mo static const u8 sText_AssaultVestDoesntAllow[] = _("{B_LAST_ITEM}'s effects prevent\nstatus moves from being used!\p"); static const u8 sText_GravityPreventsUsage[] = _("{B_ATK_NAME_WITH_PREFIX} can't use {B_CURRENT_MOVE}\nbecause of gravity!\p"); static const u8 sText_HealBlockPreventsUsage[] = _("{B_ATK_NAME_WITH_PREFIX} was\nprevented from healing!\p"); -static const u8 sText_MegaEvoReacting[] = _("{B_ATK_NAME_WITH_PREFIX}'s {B_LAST_ITEM} is \nreacting to {B_ATK_TRAINER_NAME}'s Mega Ring!"); +static const u8 sText_MegaEvoReacting[] = _("{B_ATK_NAME_WITH_PREFIX}'s {B_LAST_ITEM} is\nreacting to {B_ATK_TRAINER_NAME}'s Mega Ring!"); static const u8 sText_FerventWishReached[] = _("{B_ATK_TRAINER_NAME}'s fervent wish\nhas reached {B_ATK_NAME_WITH_PREFIX}!"); static const u8 sText_MegaEvoEvolved[] = _("{B_ATK_NAME_WITH_PREFIX} has Mega Evolved into\nMega {B_BUFF1}!"); static const u8 sText_drastically[] = _("drastically "); diff --git a/src/link_rfu_3.c b/src/link_rfu_3.c index 0d028cd48..2ad7a738e 100644 --- a/src/link_rfu_3.c +++ b/src/link_rfu_3.c @@ -134,7 +134,7 @@ static const u8 sWireless_ASCIItoRSETable[256] = { 0x8d, 0x8e, 0x8f, 0x90, 0x91, 0x92, 0x93, 0x94 }; -static const u8 sWireless_RSEtoASCIITable[256] = { +const u8 gWireless_RSEtoASCIITable[256] = { [CHAR_SPACE] = ' ', 0x86, 0x87, 0x88, 0x89, 0x8a, 0x8b, 0x8c, 0x8d, 0x8e, 0x8f, 0x90, 0x91, 0x92, 0x93, 0x94, 0x95, @@ -612,7 +612,7 @@ static void PkmnStrToASCII(u8 *asciiStr, const u8 *pkmnStr) s32 i; for (i = 0; pkmnStr[i] != EOS; i++) - asciiStr[i] = sWireless_RSEtoASCIITable[pkmnStr[i]]; + asciiStr[i] = gWireless_RSEtoASCIITable[pkmnStr[i]]; asciiStr[i] = 0; } diff --git a/src/main.c b/src/main.c index 3a306b2cf..69c6c7a16 100644 --- a/src/main.c +++ b/src/main.c @@ -31,6 +31,9 @@ static void VCountIntr(void); static void SerialIntr(void); static void IntrDummy(void); +// Defined in the linker script so that the test build can override it. +extern void gInitialMainCB2(void); + const u8 gGameVersion = GAME_VERSION; const u8 gGameLanguage = GAME_LANGUAGE; // English @@ -68,6 +71,7 @@ IntrFunc gIntrTable[INTR_COUNT]; u8 gLinkVSyncDisabled; u32 IntrMain_Buffer[0x200]; s8 gPcmDmaCounter; +void *gAgbMainLoop_sp; static EWRAM_DATA u16 sTrainerId = 0; @@ -126,6 +130,12 @@ void AgbMain() AGBPrintfInit(); #endif #endif + gAgbMainLoop_sp = __builtin_frame_address(0); + AgbMainLoop(); +} + +void AgbMainLoop(void) +{ for (;;) { ReadKeys(); @@ -178,7 +188,7 @@ static void InitMainCallbacks(void) gTrainerHillVBlankCounter = NULL; gMain.vblankCounter2 = 0; gMain.callback1 = NULL; - SetMainCallback2(CB2_InitCopyrightScreenAfterBootup); + SetMainCallback2(gInitialMainCB2); gSaveBlock2Ptr = &gSaveblock2.block; gPokemonStoragePtr = &gPokemonStorage.block; } diff --git a/src/pokemon.c b/src/pokemon.c index 15f9b15a0..71dcb5bcb 100644 --- a/src/pokemon.c +++ b/src/pokemon.c @@ -8681,3 +8681,32 @@ u32 GetMonFriendshipScore(struct Pokemon *pokemon) return FRIENDSHIP_NONE; } + +void UpdateMonPersonality(struct BoxPokemon *boxMon, u32 personality) +{ + struct PokemonSubstruct0 *old0, *new0; + struct PokemonSubstruct1 *old1, *new1; + struct PokemonSubstruct2 *old2, *new2; + struct PokemonSubstruct3 *old3, *new3; + struct BoxPokemon old; + + old = *boxMon; + old0 = &(GetSubstruct(&old, old.personality, 0)->type0); + old1 = &(GetSubstruct(&old, old.personality, 1)->type1); + old2 = &(GetSubstruct(&old, old.personality, 2)->type2); + old3 = &(GetSubstruct(&old, old.personality, 3)->type3); + + new0 = &(GetSubstruct(boxMon, personality, 0)->type0); + new1 = &(GetSubstruct(boxMon, personality, 1)->type1); + new2 = &(GetSubstruct(boxMon, personality, 2)->type2); + new3 = &(GetSubstruct(boxMon, personality, 3)->type3); + + DecryptBoxMon(&old); + boxMon->personality = personality; + *new0 = *old0; + *new1 = *old1; + *new2 = *old2; + *new3 = *old3; + boxMon->checksum = CalculateBoxMonChecksum(boxMon); + EncryptBoxMon(boxMon); +} diff --git a/src/recorded_battle.c b/src/recorded_battle.c index 33228255d..4cfa0e4c0 100644 --- a/src/recorded_battle.c +++ b/src/recorded_battle.c @@ -14,14 +14,13 @@ #include "malloc.h" #include "util.h" #include "task.h" +#include "test_runner.h" #include "text.h" #include "battle_setup.h" #include "frontier_util.h" #include "constants/trainers.h" #include "constants/rgb.h" -#define BATTLER_RECORD_SIZE 664 - struct PlayerInfo { u32 trainerId; @@ -31,37 +30,6 @@ struct PlayerInfo u16 language; }; -struct RecordedBattleSave -{ - struct Pokemon playerParty[PARTY_SIZE]; - struct Pokemon opponentParty[PARTY_SIZE]; - u8 playersName[MAX_BATTLERS_COUNT][PLAYER_NAME_LENGTH + 1]; - u8 playersGender[MAX_BATTLERS_COUNT]; - u32 playersTrainerId[MAX_BATTLERS_COUNT]; - u8 playersLanguage[MAX_BATTLERS_COUNT]; - u32 rngSeed; - u32 battleFlags; - u8 playersBattlers[MAX_BATTLERS_COUNT]; - u16 opponentA; - u16 opponentB; - u16 partnerId; - u16 multiplayerId; - u8 lvlMode; - u8 frontierFacility; - u8 frontierBrainSymbol; - u8 battleScene:1; - u8 textSpeed:3; - u32 AI_scripts; - u8 recordMixFriendName[PLAYER_NAME_LENGTH + 1]; - u8 recordMixFriendClass; - u8 apprenticeId; - u16 easyChatSpeech[EASY_CHAT_BATTLE_WORDS_COUNT]; - u8 recordMixFriendLanguage; - u8 apprenticeLanguage; - u8 battleRecord[MAX_BATTLERS_COUNT][BATTLER_RECORD_SIZE]; - u32 checksum; -}; - // Save data using TryWriteSpecialSaveSector is allowed to exceed SECTOR_DATA_SIZE (up to the counter field) STATIC_ASSERT(sizeof(struct RecordedBattleSave) <= SECTOR_COUNTER_OFFSET, RecordedBattleSaveFreeSpace); @@ -205,8 +173,11 @@ void RecordedBattle_ClearBattlerAction(u8 battlerId, u8 bytesToClear) } } -u8 RecordedBattle_GetBattlerAction(u8 battlerId) +u8 RecordedBattle_GetBattlerAction(u32 actionType, u8 battlerId) { + if (gTestRunnerEnabled) + BattleTest_CheckBattleRecordActionType(battlerId, sBattlerRecordSizes[battlerId], actionType); + // Trying to read past array or invalid action byte, battle is over. if (sBattlerRecordSizes[battlerId] >= BATTLER_RECORD_SIZE || sBattleRecords[battlerId][sBattlerRecordSizes[battlerId]] == 0xFF) { @@ -522,7 +493,7 @@ static void Task_StartAfterCountdown(u8 taskId) } } -static void SetVariablesForRecordedBattle(struct RecordedBattleSave *src) +void SetVariablesForRecordedBattle(struct RecordedBattleSave *src) { bool8 var; s32 i, j; @@ -755,14 +726,14 @@ void RecordedBattle_CheckMovesetChanges(u8 mode) // We know the current action is ACTION_MOVE_CHANGE, retrieve // it without saving it to move on to the next action. - RecordedBattle_GetBattlerAction(battlerId); + RecordedBattle_GetBattlerAction(RECORDED_BYTE, battlerId); for (j = 0; j < MAX_MON_MOVES; j++) ppBonuses[j] = ((gBattleMons[battlerId].ppBonuses & (3 << (j << 1))) >> (j << 1)); for (j = 0; j < MAX_MON_MOVES; j++) { - moveSlots[j] = RecordedBattle_GetBattlerAction(battlerId); + moveSlots[j] = RecordedBattle_GetBattlerAction(RECORDED_BYTE, battlerId); movePp.moves[j] = gBattleMons[battlerId].moves[moveSlots[j]]; movePp.currentPp[j] = gBattleMons[battlerId].pp[moveSlots[j]]; movePp.maxPp[j] = ppBonuses[moveSlots[j]]; diff --git a/src/test_runner_stub.c b/src/test_runner_stub.c new file mode 100644 index 000000000..2c8b54020 --- /dev/null +++ b/src/test_runner_stub.c @@ -0,0 +1,46 @@ +#include "global.h" +#include "test_runner.h" + +__attribute__((weak)) +const bool8 gTestRunnerEnabled = FALSE; + +// The Makefile patches gTestRunnerHeadless as part of make test. +// This allows us to open the ROM in an mgba with a UI and see the +// animations and messages play, which helps when debugging a test. +const bool8 gTestRunnerHeadless = FALSE; +const bool8 gTestRunnerSkipIsFail = FALSE; + +__attribute__((weak)) +void TestRunner_Battle_RecordAbilityPopUp(u32 battlerId, u32 ability) +{ +} + +__attribute__((weak)) +void TestRunner_Battle_RecordAnimation(u32 animType, u32 animId) +{ +} + +__attribute__((weak)) +void TestRunner_Battle_RecordHP(u32 battlerId, u32 oldHP, u32 newHP) +{ +} + +__attribute__((weak)) +void TestRunner_Battle_RecordMessage(const u8 *string) +{ +} + +__attribute__((weak)) +void TestRunner_Battle_RecordStatus1(u32 battlerId, u32 status1) +{ +} + +__attribute__((weak)) +void TestRunner_Battle_AfterLastTurn(void) +{ +} + +__attribute__((weak)) +void BattleTest_CheckBattleRecordActionType(u32 battlerId, u32 recordIndex, u32 actionType) +{ +} diff --git a/test/ability_blaze.c b/test/ability_blaze.c new file mode 100644 index 000000000..259b863ec --- /dev/null +++ b/test/ability_blaze.c @@ -0,0 +1,20 @@ +#include "global.h" +#include "test_battle.h" + +SINGLE_BATTLE_TEST("Blaze boosts Fire-type moves in a pinch", s16 damage) +{ + u16 hp; + PARAMETRIZE { hp = 99; } + PARAMETRIZE { hp = 33; } + GIVEN { + ASSUME(gBattleMoves[MOVE_EMBER].type == TYPE_FIRE); + PLAYER(SPECIES_CHARMANDER) { Ability(ABILITY_BLAZE); MaxHP(99); HP(hp); } + OPPONENT(SPECIES_WOBBUFFET); + } WHEN { + TURN { MOVE(player, MOVE_EMBER); } + } SCENE { + HP_BAR(opponent, captureDamage: &results[i].damage); + } FINALLY { + EXPECT_GT(results[1].damage, results[0].damage); + } +} diff --git a/test/ability_cute_charm.c b/test/ability_cute_charm.c new file mode 100644 index 000000000..5e089efdf --- /dev/null +++ b/test/ability_cute_charm.c @@ -0,0 +1,48 @@ +#include "global.h" +#include "test_battle.h" + +// TODO: Currently PASSES_RANDOMLY is incapable of testing Cute Charm +// because it only activates 33% of the time, but we only want to +// measure the 50% of the time that the infatuation prevents our move. +SINGLE_BATTLE_TEST("Cute Charm inflicts infatuation on contact") +{ + u32 move; + PARAMETRIZE { move = MOVE_TACKLE; } + PARAMETRIZE { move = MOVE_SWIFT; } + GIVEN { + ASSUME(gBattleMoves[MOVE_TACKLE].flags & FLAG_MAKES_CONTACT); + ASSUME(!(gBattleMoves[MOVE_SWIFT].flags & FLAG_MAKES_CONTACT)); + PLAYER(SPECIES_WOBBUFFET) { Gender(MON_MALE); } + OPPONENT(SPECIES_CLEFAIRY) { Gender(MON_FEMALE); Ability(ABILITY_CUTE_CHARM); } + } WHEN { + TURN { MOVE(player, move); } + TURN { MOVE(player, move); } + } SCENE { + if (gBattleMoves[move].flags & FLAG_MAKES_CONTACT) { + ABILITY_POPUP(opponent, ABILITY_CUTE_CHARM); + ANIMATION(ANIM_TYPE_STATUS, B_ANIM_STATUS_INFATUATION, player); + MESSAGE("Foe Clefairy's Cute Charm infatuated Wobbuffet!"); + MESSAGE("Wobbuffet is in love with Foe Clefairy!"); + } else { + NOT ABILITY_POPUP(opponent, ABILITY_CUTE_CHARM); + NOT ANIMATION(ANIM_TYPE_STATUS, B_ANIM_STATUS_INFATUATION, player); + NOT MESSAGE("Foe Clefairy's Cute Charm infatuated Wobbuffet!"); + NOT MESSAGE("Wobbuffet is in love with Foe Clefairy!"); + } + } +} + +SINGLE_BATTLE_TEST("Cute Charm cannot infatuate same gender") +{ + GIVEN { + PLAYER(SPECIES_WOBBUFFET) { Gender(MON_MALE); } + OPPONENT(SPECIES_CLEFAIRY) { Gender(MON_MALE); Ability(ABILITY_CUTE_CHARM); } + } WHEN { + TURN { MOVE(player, MOVE_TACKLE); } + TURN { MOVE(player, MOVE_TACKLE); } + } SCENE { + ANIMATION(ANIM_TYPE_MOVE, MOVE_TACKLE, player); + NOT ABILITY_POPUP(opponent, ABILITY_CUTE_CHARM); + ANIMATION(ANIM_TYPE_MOVE, MOVE_TACKLE, player); + } +} diff --git a/test/ability_flame_body.c b/test/ability_flame_body.c new file mode 100644 index 000000000..303337693 --- /dev/null +++ b/test/ability_flame_body.c @@ -0,0 +1,29 @@ +#include "global.h" +#include "test_battle.h" + +SINGLE_BATTLE_TEST("Flame Body inflicts burn on contact") +{ + u32 move; + PARAMETRIZE { move = MOVE_TACKLE; } + PARAMETRIZE { move = MOVE_SWIFT; } + GIVEN { + ASSUME(gBattleMoves[MOVE_TACKLE].flags & FLAG_MAKES_CONTACT); + ASSUME(!(gBattleMoves[MOVE_SWIFT].flags & FLAG_MAKES_CONTACT)); + PLAYER(SPECIES_WOBBUFFET); + OPPONENT(SPECIES_MAGMAR) { Ability(ABILITY_FLAME_BODY); } + } WHEN { + TURN { MOVE(player, move); } + } SCENE { + if (gBattleMoves[move].flags & FLAG_MAKES_CONTACT) { + ABILITY_POPUP(opponent, ABILITY_FLAME_BODY); + ANIMATION(ANIM_TYPE_STATUS, B_ANIM_STATUS_BRN, player); + MESSAGE("Foe Magmar's Flame Body burned Wobbuffet!"); + STATUS_ICON(player, burn: TRUE); + } else { + NOT ABILITY_POPUP(opponent, ABILITY_FLAME_BODY); + NOT ANIMATION(ANIM_TYPE_STATUS, B_ANIM_STATUS_BRN, player); + NOT MESSAGE("Foe Magmar's Flame Body burned Wobbuffet!"); + NOT STATUS_ICON(player, burn: TRUE); + } + } +} diff --git a/test/ability_immunity.c b/test/ability_immunity.c new file mode 100644 index 000000000..fea2eb522 --- /dev/null +++ b/test/ability_immunity.c @@ -0,0 +1,47 @@ +#include "global.h" +#include "test_battle.h" + +SINGLE_BATTLE_TEST("Immunity prevents Poison Sting poison") +{ + GIVEN { + ASSUME(gBattleMoves[MOVE_POISON_STING].effect == EFFECT_POISON_HIT); + PLAYER(SPECIES_WOBBUFFET); + OPPONENT(SPECIES_SNORLAX) { Ability(ABILITY_IMMUNITY); } + } WHEN { + TURN { MOVE(player, MOVE_POISON_STING); } + } SCENE { + ANIMATION(ANIM_TYPE_MOVE, MOVE_POISON_STING, player); + NOT STATUS_ICON(opponent, poison: TRUE); + } +} + +SINGLE_BATTLE_TEST("Immunity prevents Toxic bad poison") +{ + GIVEN { + ASSUME(gBattleMoves[MOVE_TOXIC].effect == EFFECT_TOXIC); + PLAYER(SPECIES_WOBBUFFET); + OPPONENT(SPECIES_SNORLAX) { Ability(ABILITY_IMMUNITY); } + } WHEN { + TURN { MOVE(player, MOVE_TOXIC); } + } SCENE { + MESSAGE("Wobbuffet used Toxic!"); + ABILITY_POPUP(opponent, ABILITY_IMMUNITY); + MESSAGE("Foe Snorlax's Immunity prevents poisoning!"); + NOT STATUS_ICON(opponent, poison: TRUE); + } +} + +SINGLE_BATTLE_TEST("Immunity prevents Toxic Spikes poison") +{ + GIVEN { + ASSUME(gBattleMoves[MOVE_TOXIC_SPIKES].effect == EFFECT_TOXIC_SPIKES); + PLAYER(SPECIES_WOBBUFFET); + OPPONENT(SPECIES_WOBBUFFET); + OPPONENT(SPECIES_SNORLAX) { Ability(ABILITY_IMMUNITY); } + } WHEN { + TURN { MOVE(player, MOVE_TOXIC_SPIKES); } + TURN { SWITCH(opponent, 1); } + } SCENE { + NOT STATUS_ICON(opponent, poison: TRUE); + } +} diff --git a/test/ability_pastel_veil.c b/test/ability_pastel_veil.c new file mode 100644 index 000000000..5d7a8f020 --- /dev/null +++ b/test/ability_pastel_veil.c @@ -0,0 +1,169 @@ +#include "global.h" +#include "test_battle.h" + +SINGLE_BATTLE_TEST("Pastel Veil prevents Poison Sting poison") +{ + GIVEN { + ASSUME(gBattleMoves[MOVE_POISON_STING].effect == EFFECT_POISON_HIT); + PLAYER(SPECIES_WOBBUFFET); + OPPONENT(SPECIES_PONYTA_GALARIAN) { Ability(ABILITY_PASTEL_VEIL); } + } WHEN { + TURN { MOVE(player, MOVE_POISON_STING); } + } SCENE { + ANIMATION(ANIM_TYPE_MOVE, MOVE_POISON_STING, player); + NOT STATUS_ICON(opponent, poison: TRUE); + } +} + +DOUBLE_BATTLE_TEST("Pastel Veil prevents Poison Sting poison on partner") +{ + GIVEN { + ASSUME(gBattleMoves[MOVE_POISON_STING].effect == EFFECT_POISON_HIT); + PLAYER(SPECIES_WOBBUFFET); + PLAYER(SPECIES_WYNAUT); + OPPONENT(SPECIES_PONYTA_GALARIAN) { Ability(ABILITY_PASTEL_VEIL); } + OPPONENT(SPECIES_WYNAUT); + } WHEN { + TURN { MOVE(playerLeft, MOVE_POISON_STING, target: opponentRight); } + } SCENE { + ANIMATION(ANIM_TYPE_MOVE, MOVE_POISON_STING, playerLeft); + NOT STATUS_ICON(opponentRight, poison: TRUE); + } +} + +SINGLE_BATTLE_TEST("Pastel Veil immediately cures Mold Breaker poison") +{ + GIVEN { + ASSUME(gBattleMoves[MOVE_TOXIC].effect == EFFECT_TOXIC); + PLAYER(SPECIES_DRILBUR) { Ability(ABILITY_MOLD_BREAKER); } + OPPONENT(SPECIES_PONYTA_GALARIAN) { Ability(ABILITY_PASTEL_VEIL); } + } WHEN { + TURN { MOVE(player, MOVE_TOXIC); } + } SCENE { + ANIMATION(ANIM_TYPE_MOVE, MOVE_TOXIC, player); + STATUS_ICON(opponent, badPoison: TRUE); + ABILITY_POPUP(opponent, ABILITY_PASTEL_VEIL); + MESSAGE("Foe Ponyta's Pastel Veil cured its poison problem!"); + STATUS_ICON(opponent, none: TRUE); + } +} + +DOUBLE_BATTLE_TEST("Pastel Veil does not cure Mold Breaker poison on partner") +{ + GIVEN { + ASSUME(gBattleMoves[MOVE_TOXIC].effect == EFFECT_TOXIC); + PLAYER(SPECIES_DRILBUR) { Ability(ABILITY_MOLD_BREAKER); } + PLAYER(SPECIES_WYNAUT); + OPPONENT(SPECIES_PONYTA_GALARIAN) { Ability(ABILITY_PASTEL_VEIL); } + OPPONENT(SPECIES_WYNAUT); + } WHEN { + TURN { MOVE(playerLeft, MOVE_TOXIC, target: opponentRight); } + } SCENE { + ANIMATION(ANIM_TYPE_MOVE, MOVE_TOXIC, playerLeft, target: opponentRight); + STATUS_ICON(opponentRight, badPoison: TRUE); + NOT STATUS_ICON(opponentRight, none: TRUE); + } +} + +SINGLE_BATTLE_TEST("Pastel Veil prevents Toxic bad poison") +{ + GIVEN { + ASSUME(gBattleMoves[MOVE_TOXIC].effect == EFFECT_TOXIC); + PLAYER(SPECIES_WOBBUFFET); + OPPONENT(SPECIES_PONYTA_GALARIAN) { Ability(ABILITY_PASTEL_VEIL); } + } WHEN { + TURN { MOVE(player, MOVE_TOXIC); } + } SCENE { + MESSAGE("Wobbuffet used Toxic!"); + ABILITY_POPUP(opponent, ABILITY_PASTEL_VEIL); + MESSAGE("Foe Ponyta is protected by a pastel veil!"); + NOT STATUS_ICON(opponent, badPoison: TRUE); + } +} + +DOUBLE_BATTLE_TEST("Pastel Veil prevents Toxic bad poison on partner") +{ + GIVEN { + ASSUME(gBattleMoves[MOVE_TOXIC].effect == EFFECT_TOXIC); + PLAYER(SPECIES_WOBBUFFET); + PLAYER(SPECIES_WYNAUT); + OPPONENT(SPECIES_PONYTA_GALARIAN) { Ability(ABILITY_PASTEL_VEIL); } + OPPONENT(SPECIES_WYNAUT); + } WHEN { + TURN { MOVE(playerLeft, MOVE_TOXIC, target: opponentRight); } + } SCENE { + MESSAGE("Wobbuffet used Toxic!"); + ABILITY_POPUP(opponentLeft, ABILITY_PASTEL_VEIL); + MESSAGE("Foe Wynaut is protected by a pastel veil!"); + NOT STATUS_ICON(opponentRight, badPoison: TRUE); + } +} + +SINGLE_BATTLE_TEST("Pastel Veil prevents Toxic Spikes poison") +{ + GIVEN { + ASSUME(gBattleMoves[MOVE_TOXIC_SPIKES].effect == EFFECT_TOXIC_SPIKES); + PLAYER(SPECIES_WOBBUFFET); + OPPONENT(SPECIES_WOBBUFFET); + OPPONENT(SPECIES_PONYTA_GALARIAN) { Ability(ABILITY_PASTEL_VEIL); } + } WHEN { + TURN { MOVE(player, MOVE_TOXIC_SPIKES); } + TURN { SWITCH(opponent, 1); } + } SCENE { + MESSAGE("2 sent out Ponyta!"); + NOT STATUS_ICON(opponent, poison: TRUE); + } +} + +DOUBLE_BATTLE_TEST("Pastel Veil prevents Toxic Spikes poison on partner") +{ + GIVEN { + ASSUME(gBattleMoves[MOVE_TOXIC_SPIKES].effect == EFFECT_TOXIC_SPIKES); + PLAYER(SPECIES_WOBBUFFET); + PLAYER(SPECIES_WYNAUT); + OPPONENT(SPECIES_PONYTA_GALARIAN) { Ability(ABILITY_PASTEL_VEIL); } + OPPONENT(SPECIES_WOBBUFFET); + OPPONENT(SPECIES_WYNAUT); + } WHEN { + TURN { MOVE(playerLeft, MOVE_TOXIC_SPIKES); } + TURN { SWITCH(opponentRight, 2); } + } SCENE { + MESSAGE("2 sent out Wynaut!"); + NOT STATUS_ICON(opponentRight, poison: TRUE); + } +} + +DOUBLE_BATTLE_TEST("Pastel Veil cures partner's poison on initial switch in") +{ + GIVEN { + PLAYER(SPECIES_WOBBUFFET); + PLAYER(SPECIES_WYNAUT); + OPPONENT(SPECIES_WOBBUFFET) { Status1(STATUS1_POISON); } + OPPONENT(SPECIES_PONYTA_GALARIAN) { Ability(ABILITY_PASTEL_VEIL); } + } WHEN { + TURN {} + } SCENE { + MESSAGE("2 sent out Wobbuffet and Ponyta!"); + ABILITY_POPUP(opponentRight, ABILITY_PASTEL_VEIL); + MESSAGE("Foe Wobbuffet was cured of its poisoning!"); + STATUS_ICON(opponentLeft, none: TRUE); + } +} + +DOUBLE_BATTLE_TEST("Pastel Veil cures partner's poison on switch in") +{ + GIVEN { + PLAYER(SPECIES_WOBBUFFET); + PLAYER(SPECIES_WYNAUT); + OPPONENT(SPECIES_WOBBUFFET) { Status1(STATUS1_POISON); } + OPPONENT(SPECIES_WYNAUT); + OPPONENT(SPECIES_PONYTA_GALARIAN) { Ability(ABILITY_PASTEL_VEIL); } + } WHEN { + TURN { SWITCH(opponentRight, 2); } + } SCENE { + MESSAGE("2 sent out Ponyta!"); + ABILITY_POPUP(opponentRight, ABILITY_PASTEL_VEIL); + MESSAGE("Foe Wobbuffet was cured of its poisoning!"); + STATUS_ICON(opponentLeft, none: TRUE); + } +} diff --git a/test/ability_poison_point.c b/test/ability_poison_point.c new file mode 100644 index 000000000..a85edd85b --- /dev/null +++ b/test/ability_poison_point.c @@ -0,0 +1,30 @@ +#include "global.h" +#include "test_battle.h" + +SINGLE_BATTLE_TEST("Poison Point inflicts poison on contact") +{ + u32 move; + PARAMETRIZE { move = MOVE_TACKLE; } + PARAMETRIZE { move = MOVE_SWIFT; } + GIVEN { + ASSUME(gBattleMoves[MOVE_TACKLE].flags & FLAG_MAKES_CONTACT); + ASSUME(!(gBattleMoves[MOVE_SWIFT].flags & FLAG_MAKES_CONTACT)); + PLAYER(SPECIES_WOBBUFFET); + OPPONENT(SPECIES_NIDORAN_M) { Ability(ABILITY_POISON_POINT); } + } WHEN { + TURN { MOVE(player, move); } + TURN {} + } SCENE { + if (gBattleMoves[move].flags & FLAG_MAKES_CONTACT) { + ABILITY_POPUP(opponent, ABILITY_POISON_POINT); + ANIMATION(ANIM_TYPE_STATUS, B_ANIM_STATUS_PSN, player); + MESSAGE("Wobbuffet was poisoned by Foe Nidoran♂'s Poison Point!"); + STATUS_ICON(player, poison: TRUE); + } else { + NOT ABILITY_POPUP(opponent, ABILITY_POISON_POINT); + NOT ANIMATION(ANIM_TYPE_STATUS, B_ANIM_STATUS_PSN, player); + NOT MESSAGE("Wobbuffet was poisoned by Foe Nidoran♂'s Poison Point!"); + NOT STATUS_ICON(player, poison: TRUE); + } + } +} diff --git a/test/ability_static.c b/test/ability_static.c new file mode 100644 index 000000000..022efa5bb --- /dev/null +++ b/test/ability_static.c @@ -0,0 +1,29 @@ +#include "global.h" +#include "test_battle.h" + +SINGLE_BATTLE_TEST("Static inflicts paralysis on contact") +{ + u32 move; + PARAMETRIZE { move = MOVE_TACKLE; } + PARAMETRIZE { move = MOVE_SWIFT; } + GIVEN { + ASSUME(gBattleMoves[MOVE_TACKLE].flags & FLAG_MAKES_CONTACT); + ASSUME(!(gBattleMoves[MOVE_SWIFT].flags & FLAG_MAKES_CONTACT)); + PLAYER(SPECIES_WOBBUFFET); + OPPONENT(SPECIES_PIKACHU) { Ability(ABILITY_STATIC); } + } WHEN { + TURN { MOVE(player, move); } + } SCENE { + if (gBattleMoves[move].flags & FLAG_MAKES_CONTACT) { + ABILITY_POPUP(opponent, ABILITY_STATIC); + ANIMATION(ANIM_TYPE_STATUS, B_ANIM_STATUS_PRZ, player); + MESSAGE("Foe Pikachu's Static paralyzed Wobbuffet! It may be unable to move!"); + STATUS_ICON(player, paralysis: TRUE); + } else { + NOT ABILITY_POPUP(opponent, ABILITY_STATIC); + NOT ANIMATION(ANIM_TYPE_STATUS, B_ANIM_STATUS_PRZ, player); + NOT MESSAGE("Foe Pikachu's Static paralyzed Wobbuffet! It may be unable to move!"); + NOT STATUS_ICON(player, paralysis: TRUE); + } + } +} diff --git a/test/ability_sturdy.c b/test/ability_sturdy.c new file mode 100644 index 000000000..42cdab11f --- /dev/null +++ b/test/ability_sturdy.c @@ -0,0 +1,47 @@ +#include "global.h" +#include "test_battle.h" + +SINGLE_BATTLE_TEST("Sturdy prevents OHKO moves") +{ + GIVEN { + ASSUME(gBattleMoves[MOVE_FISSURE].effect == EFFECT_OHKO); + PLAYER(SPECIES_GEODUDE) { Ability(ABILITY_STURDY); } + OPPONENT(SPECIES_WOBBUFFET); + } WHEN { + TURN { MOVE(opponent, MOVE_FISSURE); } + } SCENE { + MESSAGE("Foe Wobbuffet used Fissure!"); + ABILITY_POPUP(player, ABILITY_STURDY); + MESSAGE("Geodude was protected by Sturdy!"); + } THEN { + EXPECT_EQ(player->hp, player->maxHP); + } +} + +SINGLE_BATTLE_TEST("Sturdy prevents OHKOs") +{ + GIVEN { + PLAYER(SPECIES_GEODUDE) { Ability(ABILITY_STURDY); MaxHP(100); HP(100); } + OPPONENT(SPECIES_WOBBUFFET); + } WHEN { + TURN { MOVE(opponent, MOVE_SEISMIC_TOSS); } + } SCENE { + ANIMATION(ANIM_TYPE_MOVE, MOVE_SEISMIC_TOSS, opponent); + HP_BAR(player, hp: 1); + ABILITY_POPUP(player, ABILITY_STURDY); + MESSAGE("Geodude endured the hit using Sturdy!"); + } +} + +SINGLE_BATTLE_TEST("Sturdy does not prevent non-OHKOs") +{ + GIVEN { + PLAYER(SPECIES_GEODUDE) { Ability(ABILITY_STURDY); MaxHP(100); HP(99); } + OPPONENT(SPECIES_WOBBUFFET); + } WHEN { + TURN { MOVE(opponent, MOVE_SEISMIC_TOSS); } + } SCENE { + ANIMATION(ANIM_TYPE_MOVE, MOVE_SEISMIC_TOSS, opponent); + HP_BAR(player, hp: 0); + } +} diff --git a/test/hold_effect_leftovers.c b/test/hold_effect_leftovers.c new file mode 100644 index 000000000..ee6520587 --- /dev/null +++ b/test/hold_effect_leftovers.c @@ -0,0 +1,54 @@ +#include "global.h" +#include "test_battle.h" + +ASSUMPTIONS +{ + gItems[ITEM_LEFTOVERS].holdEffect == HOLD_EFFECT_LEFTOVERS; +}; + +SINGLE_BATTLE_TEST("Leftovers recovers 1/16th HP at end of turn") +{ + GIVEN { + PLAYER(SPECIES_WOBBUFFET) { MaxHP(100); HP(1); Item(ITEM_LEFTOVERS); } + OPPONENT(SPECIES_WOBBUFFET); + } WHEN { + TURN {} + } SCENE { + s32 maxHP = GetMonData(&PLAYER_PARTY[0], MON_DATA_MAX_HP); + ANIMATION(ANIM_TYPE_GENERAL, B_ANIM_HELD_ITEM_EFFECT, player); + MESSAGE("Wobbuffet's Leftovers restored its HP a little!"); + HP_BAR(player, damage: -maxHP / 16); + } +} + +SINGLE_BATTLE_TEST("Leftovers does nothing if max HP") +{ + GIVEN { + PLAYER(SPECIES_WOBBUFFET) { Item(ITEM_LEFTOVERS); } + OPPONENT(SPECIES_WOBBUFFET); + } WHEN { + TURN {} + } SCENE { + NONE_OF { + ANIMATION(ANIM_TYPE_GENERAL, B_ANIM_HELD_ITEM_EFFECT, player); + MESSAGE("Wobbuffet's Leftovers restored its HP a little!"); + HP_BAR(player); + } + } +} + +SINGLE_BATTLE_TEST("Leftovers does nothing if Heal Block applies") +{ + GIVEN { + PLAYER(SPECIES_WOBBUFFET) { MaxHP(100); HP(1); Item(ITEM_LEFTOVERS); } + OPPONENT(SPECIES_WOBBUFFET); + } WHEN { + TURN { MOVE(opponent, MOVE_HEAL_BLOCK); } + } SCENE { + NONE_OF { + ANIMATION(ANIM_TYPE_GENERAL, B_ANIM_HELD_ITEM_EFFECT, player); + MESSAGE("Wobbuffet's Leftovers restored its HP a little!"); + HP_BAR(player); + } + } +} diff --git a/test/mega_evolution.c b/test/mega_evolution.c new file mode 100644 index 000000000..13e9cd5b2 --- /dev/null +++ b/test/mega_evolution.c @@ -0,0 +1,68 @@ +#include "global.h" +#include "test_battle.h" + +SINGLE_BATTLE_TEST("Venusaur can Mega Evolve holding Venusaurite") +{ + GIVEN { + PLAYER(SPECIES_VENUSAUR) { Item(ITEM_VENUSAURITE); } + OPPONENT(SPECIES_WOBBUFFET); + } WHEN { + TURN { MOVE(player, MOVE_CELEBRATE, megaEvolve: TRUE); } + } SCENE { + MESSAGE("Venusaur's Venusaurite is reacting to 1's Mega Ring!"); + ANIMATION(ANIM_TYPE_GENERAL, B_ANIM_MEGA_EVOLUTION, player); + MESSAGE("Venusaur has Mega Evolved into Mega Venusaur!"); + } THEN { + EXPECT_EQ(player->species, SPECIES_VENUSAUR_MEGA); + } +} + +SINGLE_BATTLE_TEST("Rayquaza can Mega Evolve knowing Dragon Ascent") +{ + GIVEN { + PLAYER(SPECIES_RAYQUAZA) { Moves(MOVE_DRAGON_ASCENT, MOVE_CELEBRATE); } + OPPONENT(SPECIES_WOBBUFFET); + } WHEN { + TURN { MOVE(player, MOVE_CELEBRATE, megaEvolve: TRUE); } + } SCENE { + MESSAGE("1's fervent wish has reached Rayquaza!"); + ANIMATION(ANIM_TYPE_GENERAL, B_ANIM_MEGA_EVOLUTION, player); + MESSAGE("Rayquaza has Mega Evolved into Mega Rayquaza!"); + } THEN { + EXPECT_EQ(player->species, SPECIES_RAYQUAZA_MEGA); + } +} + +SINGLE_BATTLE_TEST("Mega Evolution affects turn order") +{ + GIVEN { + ASSUME(B_MEGA_EVO_TURN_ORDER); + PLAYER(SPECIES_DIANCIE) { Item(ITEM_DIANCITE); Speed(105); } + OPPONENT(SPECIES_WOBBUFFET) { Speed(106); } + } WHEN { + TURN { MOVE(player, MOVE_CELEBRATE, megaEvolve: TRUE); } + } SCENE { + MESSAGE("Diancie used Celebrate!"); + MESSAGE("Foe Wobbuffet used Celebrate!"); + } THEN { + ASSUME(player->speed == 225); + } +} + +SINGLE_BATTLE_TEST("Abilities replaced by Mega Evolution do not affect turn order") +{ + GIVEN { + ASSUME(B_MEGA_EVO_TURN_ORDER); + ASSUME(gSpeciesInfo[SPECIES_SABLEYE_MEGA].abilities[0] != ABILITY_STALL + && gSpeciesInfo[SPECIES_SABLEYE_MEGA].abilities[1] != ABILITY_STALL); + PLAYER(SPECIES_SABLEYE) { Item(ITEM_SABLENITE); Ability(ABILITY_STALL); Speed(105); } + OPPONENT(SPECIES_WOBBUFFET) { Speed(44); } + } WHEN { + TURN { MOVE(player, MOVE_CELEBRATE, megaEvolve: TRUE); } + } SCENE { + MESSAGE("Sableye used Celebrate!"); + MESSAGE("Foe Wobbuffet used Celebrate!"); + } THEN { + ASSUME(player->speed == 45); + } +} diff --git a/test/move.c b/test/move.c new file mode 100644 index 000000000..d7e759f27 --- /dev/null +++ b/test/move.c @@ -0,0 +1,158 @@ +#include "global.h" +#include "test_battle.h" + +SINGLE_BATTLE_TEST("Accuracy controls the proportion of misses") +{ + u32 move; + PARAMETRIZE { move = MOVE_DYNAMIC_PUNCH; } + PARAMETRIZE { move = MOVE_THUNDER; } + PARAMETRIZE { move = MOVE_HYDRO_PUMP; } + PARAMETRIZE { move = MOVE_RAZOR_LEAF; } + PARAMETRIZE { move = MOVE_SCRATCH; } + ASSUME(0 < gBattleMoves[move].accuracy && gBattleMoves[move].accuracy <= 100); + PASSES_RANDOMLY(gBattleMoves[move].accuracy, 100); + GIVEN { + PLAYER(SPECIES_WOBBUFFET); + OPPONENT(SPECIES_WOBBUFFET); + } WHEN { + TURN { MOVE(player, move); } + } SCENE { + ANIMATION(ANIM_TYPE_MOVE, move, player); + } +} + +SINGLE_BATTLE_TEST("Secondary Effect Chance controls the proportion of secondary effects") +{ + u32 move; + PARAMETRIZE { move = MOVE_THUNDER_SHOCK; } + PARAMETRIZE { move = MOVE_DISCHARGE; } + PARAMETRIZE { move = MOVE_NUZZLE; } + ASSUME(gBattleMoves[move].accuracy == 100); + ASSUME(gBattleMoves[move].effect == EFFECT_PARALYZE_HIT); + ASSUME(0 < gBattleMoves[move].secondaryEffectChance && gBattleMoves[move].secondaryEffectChance <= 100); + PASSES_RANDOMLY(gBattleMoves[move].secondaryEffectChance, 100); + GIVEN { + PLAYER(SPECIES_WOBBUFFET); + OPPONENT(SPECIES_WOBBUFFET); + } WHEN { + TURN { MOVE(player, move); } + } SCENE { + STATUS_ICON(opponent, paralysis: TRUE); + } +} + +SINGLE_BATTLE_TEST("Turn order is determined by priority") +{ + GIVEN { + ASSUME(gBattleMoves[MOVE_QUICK_ATTACK].priority > gBattleMoves[MOVE_TACKLE].priority); + PLAYER(SPECIES_WOBBUFFET); + OPPONENT(SPECIES_WOBBUFFET); + } WHEN { + TURN { MOVE(player, MOVE_QUICK_ATTACK); MOVE(opponent, MOVE_TACKLE); } + } SCENE { + ANIMATION(ANIM_TYPE_MOVE, MOVE_QUICK_ATTACK, player); + ANIMATION(ANIM_TYPE_MOVE, MOVE_TACKLE, opponent); + } +} + +SINGLE_BATTLE_TEST("Turn order is determined by speed if priority ties") +{ + GIVEN { + PLAYER(SPECIES_WOBBUFFET) { Speed(2); } + OPPONENT(SPECIES_WOBBUFFET) { Speed(1); } + } WHEN { + TURN { MOVE(player, MOVE_QUICK_ATTACK); MOVE(opponent, MOVE_QUICK_ATTACK); } + } SCENE { + ANIMATION(ANIM_TYPE_MOVE, MOVE_QUICK_ATTACK, player); + ANIMATION(ANIM_TYPE_MOVE, MOVE_QUICK_ATTACK, opponent); + } +} + +SINGLE_BATTLE_TEST("Turn order is determined randomly if priority and speed tie") +{ + PASSES_RANDOMLY(1, 2); + GIVEN { + PLAYER(SPECIES_WOBBUFFET) { Speed(1); } + OPPONENT(SPECIES_WOBBUFFET) { Speed(1); } + } WHEN { + TURN { MOVE(player, MOVE_QUICK_ATTACK); MOVE(opponent, MOVE_QUICK_ATTACK); } + } SCENE { + ANIMATION(ANIM_TYPE_MOVE, MOVE_QUICK_ATTACK, player); + ANIMATION(ANIM_TYPE_MOVE, MOVE_QUICK_ATTACK, opponent); + } +} + +SINGLE_BATTLE_TEST("Critical hits occur at a 1/24 rate") +{ + ASSUME(B_CRIT_CHANCE >= GEN_7); + ASSUME(gBattleMoves[MOVE_SCRATCH].accuracy == 100); + PASSES_RANDOMLY(100 / 24, 100); + GIVEN { + PLAYER(SPECIES_WOBBUFFET); + OPPONENT(SPECIES_WOBBUFFET); + } WHEN { + TURN { MOVE(player, MOVE_SCRATCH); } + } SCENE { + MESSAGE("It's a critical hit!"); + } +} + +SINGLE_BATTLE_TEST("Critical hits deal 50% more damage", s16 damage) +{ + bool32 criticalHit; + PARAMETRIZE { criticalHit = FALSE; } + PARAMETRIZE { criticalHit = TRUE; } + GIVEN { + ASSUME(B_CRIT_MULTIPLIER >= GEN_6); + PLAYER(SPECIES_WOBBUFFET); + OPPONENT(SPECIES_WOBBUFFET); + } WHEN { + TURN { MOVE(player, MOVE_SCRATCH, criticalHit: criticalHit); } + } SCENE { + HP_BAR(opponent, captureDamage: &results[i].damage); + } FINALLY { + EXPECT_MUL_EQ(results[0].damage, Q_4_12(1.5), results[1].damage); + } +} + +SINGLE_BATTLE_TEST("Critical hits do not ignore positive stat stages", s16 damage) +{ + u32 move; + PARAMETRIZE { move = MOVE_CELEBRATE; } + PARAMETRIZE { move = MOVE_HOWL; } + PARAMETRIZE { move = MOVE_TAIL_WHIP; } + GIVEN { + ASSUME(gBattleMoves[MOVE_SCRATCH].split == SPLIT_PHYSICAL); + PLAYER(SPECIES_WOBBUFFET); + OPPONENT(SPECIES_WOBBUFFET); + } WHEN { + TURN { MOVE(player, move); } + TURN { MOVE(player, MOVE_SCRATCH, criticalHit: TRUE); } + } SCENE { + HP_BAR(opponent, captureDamage: &results[i].damage); + } THEN { + if (i > 0) + EXPECT_LT(results[0].damage, results[i].damage); + } +} + +SINGLE_BATTLE_TEST("Critical hits ignore negative stat stages", s16 damage) +{ + u32 move; + PARAMETRIZE { move = MOVE_CELEBRATE; } + PARAMETRIZE { move = MOVE_HARDEN; } + PARAMETRIZE { move = MOVE_GROWL; } + GIVEN { + ASSUME(gBattleMoves[MOVE_SCRATCH].split == SPLIT_PHYSICAL); + PLAYER(SPECIES_WOBBUFFET); + OPPONENT(SPECIES_WOBBUFFET); + } WHEN { + TURN { MOVE(opponent, move); } + TURN { MOVE(player, MOVE_SCRATCH, criticalHit: TRUE); } + } SCENE { + HP_BAR(opponent, captureDamage: &results[i].damage); + } THEN { + if (i > 0) + EXPECT_EQ(results[0].damage, results[i].damage); + } +} diff --git a/test/move_effect_absorb.c b/test/move_effect_absorb.c new file mode 100644 index 000000000..2ea49ef9e --- /dev/null +++ b/test/move_effect_absorb.c @@ -0,0 +1,41 @@ +#include "global.h" +#include "test_battle.h" + +ASSUMPTIONS +{ + ASSUME(gBattleMoves[MOVE_ABSORB].effect == EFFECT_ABSORB); +} + +SINGLE_BATTLE_TEST("Absorb recovers 50% of the damage dealt") +{ + s16 damage; + s16 healed; + GIVEN { + PLAYER(SPECIES_WOBBUFFET) { HP(1); } + OPPONENT(SPECIES_WOBBUFFET); + } WHEN { + TURN { MOVE(player, MOVE_ABSORB); } + } SCENE { + ANIMATION(ANIM_TYPE_MOVE, MOVE_ABSORB, player); + HP_BAR(opponent, captureDamage: &damage); + HP_BAR(player, captureDamage: &healed); + } THEN { + EXPECT_MUL_EQ(damage, Q_4_12(-0.5), healed); + } +} + +SINGLE_BATTLE_TEST("Absorb fails if Heal Block applies") +{ + ASSUME(B_HEAL_BLOCKING >= GEN_6); + GIVEN { + PLAYER(SPECIES_WOBBUFFET) { HP(1); } + OPPONENT(SPECIES_WOBBUFFET); + } WHEN { + TURN { MOVE(opponent, MOVE_HEAL_BLOCK); MOVE(player, MOVE_ABSORB); } + } SCENE { + MESSAGE("Wobbuffet was prevented from healing!"); + NOT ANIMATION(ANIM_TYPE_MOVE, MOVE_ABSORB, player); + NOT HP_BAR(opponent); + NOT HP_BAR(player); + } +} diff --git a/test/move_effect_accuracy_down.c b/test/move_effect_accuracy_down.c new file mode 100644 index 000000000..2a90d8ea2 --- /dev/null +++ b/test/move_effect_accuracy_down.c @@ -0,0 +1,24 @@ +#include "global.h" +#include "test_battle.h" + +ASSUMPTIONS +{ + ASSUME(gBattleMoves[MOVE_SAND_ATTACK].effect == EFFECT_ACCURACY_DOWN); +} + +SINGLE_BATTLE_TEST("Sand Attack lowers Accuracy") +{ + ASSUME(gBattleMoves[MOVE_SCRATCH].accuracy == 100); + PASSES_RANDOMLY(gBattleMoves[MOVE_SCRATCH].accuracy * 3 / 4, 100); + GIVEN { + PLAYER(SPECIES_WOBBUFFET); + OPPONENT(SPECIES_WOBBUFFET); + } WHEN { + TURN { MOVE(player, MOVE_SAND_ATTACK); MOVE(opponent, MOVE_SCRATCH); } + } SCENE { + ANIMATION(ANIM_TYPE_MOVE, MOVE_SAND_ATTACK, player); + ANIMATION(ANIM_TYPE_GENERAL, B_ANIM_STATS_CHANGE, opponent); + MESSAGE("Foe Wobbuffet's accuracy fell!"); + ANIMATION(ANIM_TYPE_MOVE, MOVE_SCRATCH, opponent); + } +} diff --git a/test/move_effect_after_you.c b/test/move_effect_after_you.c new file mode 100644 index 000000000..400fc053f --- /dev/null +++ b/test/move_effect_after_you.c @@ -0,0 +1,54 @@ +#include "global.h" +#include "test_battle.h" + +ASSUMPTIONS +{ + ASSUME(gBattleMoves[MOVE_AFTER_YOU].effect == EFFECT_AFTER_YOU); +} + +DOUBLE_BATTLE_TEST("After You makes the target move after user") +{ + if (B_RECALC_TURN_AFTER_ACTIONS >= GEN_8) KNOWN_FAILING; // #2615. + GIVEN { + PLAYER(SPECIES_WOBBUFFET) { Speed(4); } + PLAYER(SPECIES_WYNAUT) { Speed(1); } + OPPONENT(SPECIES_WOBBUFFET) { Speed(3); } + OPPONENT(SPECIES_WYNAUT) { Speed(2); } + } WHEN { + TURN { + MOVE(playerLeft, MOVE_AFTER_YOU, target: playerRight); + MOVE(playerRight, MOVE_CELEBRATE); + MOVE(opponentLeft, MOVE_CELEBRATE); + MOVE(opponentRight, MOVE_CELEBRATE); + } + } SCENE { + ANIMATION(ANIM_TYPE_MOVE, MOVE_AFTER_YOU, playerLeft); + MESSAGE("Wynaut took the kind offer!"); + ANIMATION(ANIM_TYPE_MOVE, MOVE_CELEBRATE, playerRight); + ANIMATION(ANIM_TYPE_MOVE, MOVE_CELEBRATE, opponentLeft); + ANIMATION(ANIM_TYPE_MOVE, MOVE_CELEBRATE, opponentRight); + } +} + +DOUBLE_BATTLE_TEST("After You does nothing if the target has already moved") +{ + GIVEN { + PLAYER(SPECIES_WOBBUFFET) { Speed(4); } + PLAYER(SPECIES_WYNAUT) { Speed(1); } + OPPONENT(SPECIES_WOBBUFFET) { Speed(3); } + OPPONENT(SPECIES_WYNAUT) { Speed(2); } + } WHEN { + TURN { + MOVE(playerLeft, MOVE_CELEBRATE); + MOVE(playerRight, MOVE_CELEBRATE); + MOVE(opponentLeft, MOVE_CELEBRATE); + MOVE(opponentRight, MOVE_AFTER_YOU, target: opponentLeft); + } + } SCENE { + ANIMATION(ANIM_TYPE_MOVE, MOVE_CELEBRATE, playerLeft); + ANIMATION(ANIM_TYPE_MOVE, MOVE_CELEBRATE, opponentLeft); + MESSAGE("Foe Wynaut used After You!"); + MESSAGE("But it failed!"); + ANIMATION(ANIM_TYPE_MOVE, MOVE_CELEBRATE, playerRight); + } +} diff --git a/test/move_effect_attack_down.c b/test/move_effect_attack_down.c new file mode 100644 index 000000000..6333bbea1 --- /dev/null +++ b/test/move_effect_attack_down.c @@ -0,0 +1,32 @@ +#include "global.h" +#include "test_battle.h" + +ASSUMPTIONS +{ + ASSUME(gBattleMoves[MOVE_GROWL].effect == EFFECT_ATTACK_DOWN); +} + +SINGLE_BATTLE_TEST("Growl lowers Attack", s16 damage) +{ + bool32 lowerAttack; + PARAMETRIZE { lowerAttack = FALSE; } + PARAMETRIZE { lowerAttack = TRUE; } + GIVEN { + ASSUME(gBattleMoves[MOVE_TACKLE].split == SPLIT_PHYSICAL); + PLAYER(SPECIES_WOBBUFFET); + OPPONENT(SPECIES_WOBBUFFET); + } WHEN { + if (lowerAttack) TURN { MOVE(player, MOVE_GROWL); } + TURN { MOVE(opponent, MOVE_TACKLE); } + } SCENE { + if (lowerAttack) { + ANIMATION(ANIM_TYPE_MOVE, MOVE_GROWL, player); + ANIMATION(ANIM_TYPE_GENERAL, B_ANIM_STATS_CHANGE, opponent); + MESSAGE("Foe Wobbuffet's attack fell!"); + } + ANIMATION(ANIM_TYPE_MOVE, MOVE_TACKLE, opponent); + HP_BAR(player, captureDamage: &results[i].damage); + } FINALLY { + EXPECT_MUL_EQ(results[1].damage, Q_4_12(1.5), results[0].damage); + } +} diff --git a/test/move_effect_attack_up.c b/test/move_effect_attack_up.c new file mode 100644 index 000000000..7b57fa0d2 --- /dev/null +++ b/test/move_effect_attack_up.c @@ -0,0 +1,32 @@ +#include "global.h" +#include "test_battle.h" + +ASSUMPTIONS +{ + ASSUME(gBattleMoves[MOVE_MEDITATE].effect == EFFECT_ATTACK_UP); +} + +SINGLE_BATTLE_TEST("Meditate raises Attack", s16 damage) +{ + bool32 raiseAttack; + PARAMETRIZE { raiseAttack = FALSE; } + PARAMETRIZE { raiseAttack = TRUE; } + GIVEN { + ASSUME(gBattleMoves[MOVE_TACKLE].split == SPLIT_PHYSICAL); + PLAYER(SPECIES_WOBBUFFET); + OPPONENT(SPECIES_WOBBUFFET); + } WHEN { + if (raiseAttack) TURN { MOVE(player, MOVE_MEDITATE); } + TURN { MOVE(player, MOVE_TACKLE); } + } SCENE { + if (raiseAttack) { + ANIMATION(ANIM_TYPE_MOVE, MOVE_MEDITATE, player); + ANIMATION(ANIM_TYPE_GENERAL, B_ANIM_STATS_CHANGE, player); + MESSAGE("Wobbuffet's attack rose!"); + } + ANIMATION(ANIM_TYPE_MOVE, MOVE_TACKLE, player); + HP_BAR(opponent, captureDamage: &results[i].damage); + } FINALLY { + EXPECT_MUL_EQ(results[0].damage, Q_4_12(1.5), results[1].damage); + } +} diff --git a/test/move_effect_bide.c b/test/move_effect_bide.c new file mode 100644 index 000000000..3a5c38735 --- /dev/null +++ b/test/move_effect_bide.c @@ -0,0 +1,34 @@ +#include "global.h" +#include "test_battle.h" + +ASSUMPTIONS +{ + ASSUME(gBattleMoves[MOVE_BIDE].effect == EFFECT_BIDE); +} + +SINGLE_BATTLE_TEST("Bide deals twice the taken damage over two turns") +{ + s16 damage1; + s16 damage2; + s16 bideDamage; + GIVEN { + PLAYER(SPECIES_WOBBUFFET); + OPPONENT(SPECIES_WOBBUFFET); + } WHEN { + TURN { MOVE(player, MOVE_BIDE); MOVE(opponent, MOVE_TACKLE); } + TURN { SKIP_TURN(player); MOVE(opponent, MOVE_TACKLE); } + TURN { SKIP_TURN(player); } + } SCENE { + ANIMATION(ANIM_TYPE_MOVE, MOVE_BIDE, player); + ANIMATION(ANIM_TYPE_MOVE, MOVE_TACKLE, opponent); + HP_BAR(player, captureDamage: &damage1); + MESSAGE("Wobbuffet is storing energy!"); + ANIMATION(ANIM_TYPE_MOVE, MOVE_TACKLE, opponent); + HP_BAR(player, captureDamage: &damage2); + MESSAGE("Wobbuffet unleashed energy!"); + ANIMATION(ANIM_TYPE_MOVE, MOVE_BIDE, player); + HP_BAR(opponent, captureDamage: &bideDamage); + } FINALLY { + EXPECT_EQ(bideDamage, damage1 + damage2); + } +} diff --git a/test/move_effect_burn_hit.c b/test/move_effect_burn_hit.c new file mode 100644 index 000000000..88fef17cb --- /dev/null +++ b/test/move_effect_burn_hit.c @@ -0,0 +1,38 @@ +#include "global.h" +#include "test_battle.h" + +ASSUMPTIONS +{ + ASSUME(gBattleMoves[MOVE_EMBER].effect == EFFECT_BURN_HIT); +} + +SINGLE_BATTLE_TEST("Ember inflicts burn") +{ + GIVEN { + PLAYER(SPECIES_WOBBUFFET); + OPPONENT(SPECIES_WOBBUFFET); + } WHEN { + TURN { MOVE(player, MOVE_EMBER); } + } SCENE { + ANIMATION(ANIM_TYPE_MOVE, MOVE_EMBER, player); + HP_BAR(opponent); + ANIMATION(ANIM_TYPE_STATUS, B_ANIM_STATUS_BRN, opponent); + STATUS_ICON(opponent, burn: TRUE); + } +} + +SINGLE_BATTLE_TEST("Ember cannot burn a Fire-type") +{ + GIVEN { + ASSUME(gSpeciesInfo[SPECIES_CHARMANDER].types[0] == TYPE_FIRE); + PLAYER(SPECIES_WOBBUFFET); + OPPONENT(SPECIES_CHARMANDER); + } WHEN { + TURN { MOVE(player, MOVE_EMBER); } + } SCENE { + ANIMATION(ANIM_TYPE_MOVE, MOVE_EMBER, player); + HP_BAR(opponent); + NOT ANIMATION(ANIM_TYPE_STATUS, B_ANIM_STATUS_BRN, opponent); + NOT STATUS_ICON(opponent, burn: TRUE); + } +} diff --git a/test/move_effect_defense_down.c b/test/move_effect_defense_down.c new file mode 100644 index 000000000..6e5a45e84 --- /dev/null +++ b/test/move_effect_defense_down.c @@ -0,0 +1,32 @@ +#include "global.h" +#include "test_battle.h" + +ASSUMPTIONS +{ + ASSUME(gBattleMoves[MOVE_TAIL_WHIP].effect == EFFECT_DEFENSE_DOWN); +} + +SINGLE_BATTLE_TEST("Tail Whip lowers Defense", s16 damage) +{ + bool32 lowerDefense; + PARAMETRIZE { lowerDefense = FALSE; } + PARAMETRIZE { lowerDefense = TRUE; } + GIVEN { + ASSUME(gBattleMoves[MOVE_TACKLE].split == SPLIT_PHYSICAL); + PLAYER(SPECIES_WOBBUFFET); + OPPONENT(SPECIES_WOBBUFFET); + } WHEN { + if (lowerDefense) TURN { MOVE(player, MOVE_TAIL_WHIP); } + TURN { MOVE(player, MOVE_TACKLE); } + } SCENE { + if (lowerDefense) { + ANIMATION(ANIM_TYPE_MOVE, MOVE_TAIL_WHIP, player); + ANIMATION(ANIM_TYPE_GENERAL, B_ANIM_STATS_CHANGE, opponent); + MESSAGE("Foe Wobbuffet's defense fell!"); + } + ANIMATION(ANIM_TYPE_MOVE, MOVE_TACKLE, player); + HP_BAR(opponent, captureDamage: &results[i].damage); + } FINALLY { + EXPECT_MUL_EQ(results[0].damage, Q_4_12(1.5), results[1].damage); + } +} diff --git a/test/move_effect_defense_up.c b/test/move_effect_defense_up.c new file mode 100644 index 000000000..8db9a7f7e --- /dev/null +++ b/test/move_effect_defense_up.c @@ -0,0 +1,32 @@ +#include "global.h" +#include "test_battle.h" + +ASSUMPTIONS +{ + ASSUME(gBattleMoves[MOVE_HARDEN].effect == EFFECT_DEFENSE_UP); +} + +SINGLE_BATTLE_TEST("Harden raises Defense", s16 damage) +{ + bool32 raiseDefense; + PARAMETRIZE { raiseDefense = FALSE; } + PARAMETRIZE { raiseDefense = TRUE; } + GIVEN { + ASSUME(gBattleMoves[MOVE_TACKLE].split == SPLIT_PHYSICAL); + PLAYER(SPECIES_WOBBUFFET); + OPPONENT(SPECIES_WOBBUFFET); + } WHEN { + if (raiseDefense) TURN { MOVE(player, MOVE_HARDEN); } + TURN { MOVE(opponent, MOVE_TACKLE); } + } SCENE { + if (raiseDefense) { + ANIMATION(ANIM_TYPE_MOVE, MOVE_HARDEN, player); + ANIMATION(ANIM_TYPE_GENERAL, B_ANIM_STATS_CHANGE, player); + MESSAGE("Wobbuffet's defense rose!"); + } + ANIMATION(ANIM_TYPE_MOVE, MOVE_TACKLE, opponent); + HP_BAR(player, captureDamage: &results[i].damage); + } FINALLY { + EXPECT_MUL_EQ(results[1].damage, Q_4_12(1.5), results[0].damage); + } +} diff --git a/test/move_effect_dream_eater.c b/test/move_effect_dream_eater.c new file mode 100644 index 000000000..fa17b94a6 --- /dev/null +++ b/test/move_effect_dream_eater.c @@ -0,0 +1,54 @@ +#include "global.h" +#include "test_battle.h" + +ASSUMPTIONS +{ + ASSUME(gBattleMoves[MOVE_DREAM_EATER].effect == EFFECT_DREAM_EATER); +} + +SINGLE_BATTLE_TEST("Dream Eater recovers 50% of the damage dealt") +{ + s16 damage; + s16 healed; + GIVEN { + PLAYER(SPECIES_WOBBUFFET) { HP(1); } + OPPONENT(SPECIES_WOBBUFFET) { Status1(STATUS1_SLEEP); } + } WHEN { + TURN { MOVE(player, MOVE_DREAM_EATER); } + } SCENE { + ANIMATION(ANIM_TYPE_MOVE, MOVE_DREAM_EATER, player); + HP_BAR(opponent, captureDamage: &damage); + HP_BAR(player, captureDamage: &healed); + } THEN { + EXPECT_MUL_EQ(damage, Q_4_12(-1.0/2.0), healed); + } +} + +SINGLE_BATTLE_TEST("Dream Eater fails on awake targets") +{ + GIVEN { + PLAYER(SPECIES_WOBBUFFET); + OPPONENT(SPECIES_WOBBUFFET); + } WHEN { + TURN { MOVE(player, MOVE_DREAM_EATER); } + } SCENE { + MESSAGE("Wobbuffet used Dream Eater!"); + MESSAGE("Foe Wobbuffet wasn't affected!"); + } +} + +SINGLE_BATTLE_TEST("Dream Eater fails if Heal Block applies") +{ + ASSUME(B_HEAL_BLOCKING >= GEN_6); + GIVEN { + PLAYER(SPECIES_WOBBUFFET) { HP(1); } + OPPONENT(SPECIES_WOBBUFFET); + } WHEN { + TURN { MOVE(opponent, MOVE_HEAL_BLOCK); MOVE(player, MOVE_DREAM_EATER); } + } SCENE { + MESSAGE("Wobbuffet was prevented from healing!"); + NOT ANIMATION(ANIM_TYPE_MOVE, MOVE_DREAM_EATER, player); + NOT HP_BAR(opponent); + NOT HP_BAR(player); + } +} diff --git a/test/move_effect_encore.c b/test/move_effect_encore.c new file mode 100644 index 000000000..f8a178512 --- /dev/null +++ b/test/move_effect_encore.c @@ -0,0 +1,54 @@ +#include "global.h" +#include "test_battle.h" + +ASSUMPTIONS +{ + ASSUME(gBattleMoves[MOVE_ENCORE].effect == EFFECT_ENCORE); +} + +SINGLE_BATTLE_TEST("Encore forces consecutive move uses for 2 turns") +{ + GIVEN { + PLAYER(SPECIES_WOBBUFFET); + OPPONENT(SPECIES_WOBBUFFET); + } WHEN { + TURN { MOVE(player, MOVE_CELEBRATE); MOVE(opponent, MOVE_ENCORE); } + TURN { FORCED_MOVE(player); } + TURN { FORCED_MOVE(player); } + TURN { MOVE(player, MOVE_SPLASH); } + } SCENE { + ANIMATION(ANIM_TYPE_MOVE, MOVE_CELEBRATE, player); + ANIMATION(ANIM_TYPE_MOVE, MOVE_ENCORE, opponent); + ANIMATION(ANIM_TYPE_MOVE, MOVE_CELEBRATE, player); + ANIMATION(ANIM_TYPE_MOVE, MOVE_CELEBRATE, player); + ANIMATION(ANIM_TYPE_MOVE, MOVE_SPLASH, player); + } +} + +SINGLE_BATTLE_TEST("Encore has no effect if no previous move") +{ + GIVEN { + PLAYER(SPECIES_WOBBUFFET); + OPPONENT(SPECIES_WOBBUFFET); + } WHEN { + TURN { MOVE(opponent, MOVE_ENCORE); MOVE(player, MOVE_CELEBRATE); } + } SCENE { + MESSAGE("Foe Wobbuffet used Encore!"); + MESSAGE("But it failed!"); + } +} + +SINGLE_BATTLE_TEST("Encore overrides the chosen move if it occurs first") +{ + GIVEN { + PLAYER(SPECIES_WOBBUFFET); + OPPONENT(SPECIES_WOBBUFFET); + } WHEN { + TURN { MOVE(player, MOVE_CELEBRATE); } + TURN { MOVE(opponent, MOVE_ENCORE); MOVE(player, MOVE_SPLASH); } + } SCENE { + ANIMATION(ANIM_TYPE_MOVE, MOVE_CELEBRATE, player); + ANIMATION(ANIM_TYPE_MOVE, MOVE_ENCORE, opponent); + ANIMATION(ANIM_TYPE_MOVE, MOVE_CELEBRATE, player); + } +} diff --git a/test/move_effect_evasion_up.c b/test/move_effect_evasion_up.c new file mode 100644 index 000000000..d14d15334 --- /dev/null +++ b/test/move_effect_evasion_up.c @@ -0,0 +1,24 @@ +#include "global.h" +#include "test_battle.h" + +ASSUMPTIONS +{ + ASSUME(gBattleMoves[MOVE_DOUBLE_TEAM].effect == EFFECT_EVASION_UP); +} + +SINGLE_BATTLE_TEST("Double Team raises Evasion") +{ + ASSUME(gBattleMoves[MOVE_SCRATCH].accuracy == 100); + PASSES_RANDOMLY(gBattleMoves[MOVE_SCRATCH].accuracy * 3 / 4, 100); + GIVEN { + PLAYER(SPECIES_WOBBUFFET); + OPPONENT(SPECIES_WOBBUFFET); + } WHEN { + TURN { MOVE(player, MOVE_DOUBLE_TEAM); MOVE(opponent, MOVE_SCRATCH); } + } SCENE { + ANIMATION(ANIM_TYPE_MOVE, MOVE_DOUBLE_TEAM, player); + ANIMATION(ANIM_TYPE_GENERAL, B_ANIM_STATS_CHANGE, player); + MESSAGE("Wobbuffet's evasiveness rose!"); + ANIMATION(ANIM_TYPE_MOVE, MOVE_SCRATCH, opponent); + } +} diff --git a/test/move_effect_explosion.c b/test/move_effect_explosion.c new file mode 100644 index 000000000..872f3f709 --- /dev/null +++ b/test/move_effect_explosion.c @@ -0,0 +1,53 @@ +#include "global.h" +#include "test_battle.h" + +ASSUMPTIONS +{ + ASSUME(gBattleMoves[MOVE_EXPLOSION].effect == EFFECT_EXPLOSION); +} + +SINGLE_BATTLE_TEST("Explosion causes the user to faint") +{ + u16 remainingHP; + GIVEN { + PLAYER(SPECIES_WOBBUFFET); + OPPONENT(SPECIES_WOBBUFFET); + } WHEN { + TURN { MOVE(player, MOVE_EXPLOSION); } + } SCENE { + HP_BAR(player, hp: 0); + ANIMATION(ANIM_TYPE_MOVE, MOVE_EXPLOSION, player); + } +} + +SINGLE_BATTLE_TEST("Explosion causes the user to faint even if it misses") +{ + u16 remainingHP; + GIVEN { + PLAYER(SPECIES_WOBBUFFET); + OPPONENT(SPECIES_WOBBUFFET); + } WHEN { + TURN { MOVE(player, MOVE_EXPLOSION, hit: FALSE); } + } SCENE { + HP_BAR(player, hp: 0); + ANIMATION(ANIM_TYPE_MOVE, MOVE_EXPLOSION, player); + } +} + +SINGLE_BATTLE_TEST("Explosion causes the user to faint even if it has no effect") +{ + u16 remainingHP; + GIVEN { + ASSUME(gBattleMoves[MOVE_EXPLOSION].type == TYPE_NORMAL); + ASSUME(gSpeciesInfo[SPECIES_GASTLY].types[0] == TYPE_GHOST); + PLAYER(SPECIES_WOBBUFFET); + OPPONENT(SPECIES_GASTLY); + } WHEN { + TURN { MOVE(player, MOVE_EXPLOSION); } + } SCENE { + HP_BAR(player, hp: 0); + ANIMATION(ANIM_TYPE_MOVE, MOVE_EXPLOSION, player); + MESSAGE("It doesn't affect Foe Gastly…"); + NOT HP_BAR(opponent); + } +} diff --git a/test/move_effect_freeze_hit.c b/test/move_effect_freeze_hit.c new file mode 100644 index 000000000..bb0878d0d --- /dev/null +++ b/test/move_effect_freeze_hit.c @@ -0,0 +1,38 @@ +#include "global.h" +#include "test_battle.h" + +ASSUMPTIONS +{ + ASSUME(gBattleMoves[MOVE_POWDER_SNOW].effect == EFFECT_FREEZE_HIT); +} + +SINGLE_BATTLE_TEST("Powder Snow inflicts freeze") +{ + GIVEN { + PLAYER(SPECIES_WOBBUFFET); + OPPONENT(SPECIES_WOBBUFFET); + } WHEN { + TURN { MOVE(player, MOVE_POWDER_SNOW); } + } SCENE { + ANIMATION(ANIM_TYPE_MOVE, MOVE_POWDER_SNOW, player); + HP_BAR(opponent); + ANIMATION(ANIM_TYPE_STATUS, B_ANIM_STATUS_FRZ, opponent); + STATUS_ICON(opponent, freeze: TRUE); + } +} + +SINGLE_BATTLE_TEST("Powder Snow cannot freeze an Ice-type") +{ + GIVEN { + ASSUME(gSpeciesInfo[SPECIES_SNORUNT].types[0] == TYPE_ICE); + PLAYER(SPECIES_WOBBUFFET); + OPPONENT(SPECIES_SNORUNT); + } WHEN { + TURN { MOVE(player, MOVE_POWDER_SNOW); } + } SCENE { + ANIMATION(ANIM_TYPE_MOVE, MOVE_POWDER_SNOW, player); + HP_BAR(opponent); + NOT ANIMATION(ANIM_TYPE_STATUS, B_ANIM_STATUS_FRZ, opponent); + NOT STATUS_ICON(opponent, freeze: TRUE); + } +} diff --git a/test/move_effect_haze.c b/test/move_effect_haze.c new file mode 100644 index 000000000..bd43c6947 --- /dev/null +++ b/test/move_effect_haze.c @@ -0,0 +1,32 @@ +#include "global.h" +#include "test_battle.h" + +ASSUMPTIONS +{ + ASSUME(gBattleMoves[MOVE_HAZE].effect == EFFECT_HAZE); +} + +SINGLE_BATTLE_TEST("Haze resets stat changes", s16 damage) +{ + bool32 haze; + PARAMETRIZE { haze = FALSE; } + PARAMETRIZE { haze = TRUE; } + GIVEN { + ASSUME(gBattleMoves[MOVE_MEDITATE].effect == EFFECT_ATTACK_UP); + ASSUME(gBattleMoves[MOVE_TACKLE].split == SPLIT_PHYSICAL); + PLAYER(SPECIES_WOBBUFFET); + OPPONENT(SPECIES_WOBBUFFET); + } WHEN { + if (haze) TURN { MOVE(player, MOVE_MEDITATE); MOVE(opponent, MOVE_HAZE); } + TURN { MOVE(player, MOVE_TACKLE); } + } SCENE { + if (haze) { + ANIMATION(ANIM_TYPE_MOVE, MOVE_HAZE, opponent); + MESSAGE("All stat changes were eliminated!"); + } + ANIMATION(ANIM_TYPE_MOVE, MOVE_TACKLE, player); + HP_BAR(opponent, captureDamage: &results[i].damage); + } FINALLY { + EXPECT_EQ(results[0].damage, results[1].damage); + } +} diff --git a/test/move_effect_hex.c b/test/move_effect_hex.c new file mode 100644 index 000000000..2847aeb17 --- /dev/null +++ b/test/move_effect_hex.c @@ -0,0 +1,33 @@ +#include "global.h" +#include "test_battle.h" + +ASSUMPTIONS +{ + ASSUME(gBattleMoves[MOVE_HEX].effect == EFFECT_HEX); +} + +SINGLE_BATTLE_TEST("Hex deals double damage to foes with a status", s16 damage) +{ + u32 status1; + PARAMETRIZE { status1 = STATUS1_NONE; } + PARAMETRIZE { status1 = STATUS1_SLEEP; } + PARAMETRIZE { status1 = STATUS1_POISON; } + PARAMETRIZE { status1 = STATUS1_BURN; } + PARAMETRIZE { status1 = STATUS1_FREEZE; } + PARAMETRIZE { status1 = STATUS1_PARALYSIS; } + PARAMETRIZE { status1 = STATUS1_TOXIC_POISON; } + GIVEN { + PLAYER(SPECIES_WOBBUFFET); + OPPONENT(SPECIES_WOBBUFFET) { Status1(status1); } + } WHEN { + TURN { MOVE(player, MOVE_HEX); } + } SCENE { + ANIMATION(ANIM_TYPE_MOVE, MOVE_HEX, player); + HP_BAR(opponent, captureDamage: &results[i].damage); + } THEN { + if (i > 0) + EXPECT_MUL_EQ(results[0].damage, Q_4_12(2.0), results[i].damage); + if (i > 1) + EXPECT_EQ(results[i-1].damage, results[i].damage); + } +} diff --git a/test/move_effect_hit_escape.c b/test/move_effect_hit_escape.c new file mode 100644 index 000000000..cc34db2e2 --- /dev/null +++ b/test/move_effect_hit_escape.c @@ -0,0 +1,96 @@ +#include "global.h" +#include "test_battle.h" + +ASSUMPTIONS +{ + ASSUME(gBattleMoves[MOVE_U_TURN].effect == EFFECT_HIT_ESCAPE); +} + +SINGLE_BATTLE_TEST("U-turn switches the user out") +{ + GIVEN { + PLAYER(SPECIES_WOBBUFFET); + PLAYER(SPECIES_WYNAUT); + OPPONENT(SPECIES_WOBBUFFET); + } WHEN { + TURN { MOVE(player, MOVE_U_TURN); SEND_OUT(player, 1); } + } SCENE { + ANIMATION(ANIM_TYPE_MOVE, MOVE_U_TURN, player); + HP_BAR(opponent); + MESSAGE("Go! Wynaut!"); + } +} + +SINGLE_BATTLE_TEST("U-turn does not switch the user out if the battle ends") +{ + GIVEN { + PLAYER(SPECIES_WOBBUFFET); + PLAYER(SPECIES_WYNAUT); + OPPONENT(SPECIES_WOBBUFFET) { HP(1); } + } WHEN { + TURN { MOVE(player, MOVE_U_TURN); } + } SCENE { + ANIMATION(ANIM_TYPE_MOVE, MOVE_U_TURN, player); + HP_BAR(opponent); + } +} + +SINGLE_BATTLE_TEST("U-turn does not switch the user out if no replacements") +{ + GIVEN { + PLAYER(SPECIES_WOBBUFFET); + OPPONENT(SPECIES_WOBBUFFET); + } WHEN { + TURN { MOVE(player, MOVE_U_TURN); } + } SCENE { + ANIMATION(ANIM_TYPE_MOVE, MOVE_U_TURN, player); + HP_BAR(opponent); + } +} + +SINGLE_BATTLE_TEST("U-turn does not switch the user out if replacements fainted") +{ + GIVEN { + PLAYER(SPECIES_WOBBUFFET); + PLAYER(SPECIES_WYNAUT) { HP(0); } + OPPONENT(SPECIES_WOBBUFFET); + } WHEN { + TURN { MOVE(player, MOVE_U_TURN); } + } SCENE { + ANIMATION(ANIM_TYPE_MOVE, MOVE_U_TURN, player); + HP_BAR(opponent); + } +} + +SINGLE_BATTLE_TEST("U-turn does not switch the user out if Wimp Out activates") +{ + GIVEN { + PLAYER(SPECIES_WOBBUFFET); + PLAYER(SPECIES_WYNAUT); + OPPONENT(SPECIES_WIMPOD) { MaxHP(100); HP(51); Ability(ABILITY_WIMP_OUT); } + OPPONENT(SPECIES_WOBBUFFET); + } WHEN { + TURN { MOVE(player, MOVE_U_TURN); SEND_OUT(opponent, 1); } + } SCENE { + ANIMATION(ANIM_TYPE_MOVE, MOVE_U_TURN, player); + HP_BAR(opponent); + ABILITY_POPUP(opponent, ABILITY_WIMP_OUT); + MESSAGE("2 sent out Wobbuffet!"); + } +} + +SINGLE_BATTLE_TEST("U-turn switches the user out if Wimp Out fails to activate") +{ + GIVEN { + PLAYER(SPECIES_WOBBUFFET); + PLAYER(SPECIES_WYNAUT); + OPPONENT(SPECIES_WIMPOD) { MaxHP(100); HP(51); Ability(ABILITY_WIMP_OUT); } + } WHEN { + TURN { MOVE(player, MOVE_U_TURN); SEND_OUT(player, 1); } + } SCENE { + ANIMATION(ANIM_TYPE_MOVE, MOVE_U_TURN, player); + HP_BAR(opponent); + NOT ABILITY_POPUP(opponent); + MESSAGE("Your foe's weak! Get 'em, Wynaut!"); + } +} diff --git a/test/move_effect_paralyze_hit.c b/test/move_effect_paralyze_hit.c new file mode 100644 index 000000000..8e7d259f7 --- /dev/null +++ b/test/move_effect_paralyze_hit.c @@ -0,0 +1,39 @@ +#include "global.h" +#include "test_battle.h" + +ASSUMPTIONS +{ + ASSUME(gBattleMoves[MOVE_THUNDER_SHOCK].effect == EFFECT_PARALYZE_HIT); +} + +SINGLE_BATTLE_TEST("Thunder Shock inflicts paralysis") +{ + GIVEN { + PLAYER(SPECIES_WOBBUFFET); + OPPONENT(SPECIES_WOBBUFFET); + } WHEN { + TURN { MOVE(player, MOVE_THUNDER_SHOCK); } + } SCENE { + ANIMATION(ANIM_TYPE_MOVE, MOVE_THUNDER_SHOCK, player); + HP_BAR(opponent); + ANIMATION(ANIM_TYPE_STATUS, B_ANIM_STATUS_PRZ, opponent); + STATUS_ICON(opponent, paralysis: TRUE); + } +} + +SINGLE_BATTLE_TEST("Thunder Shock cannot paralyze an Electric-type") +{ + GIVEN { + ASSUME(B_PARALYZE_ELECTRIC >= GEN_6); + ASSUME(gSpeciesInfo[SPECIES_PIKACHU].types[0] == TYPE_ELECTRIC); + PLAYER(SPECIES_WOBBUFFET); + OPPONENT(SPECIES_PIKACHU); + } WHEN { + TURN { MOVE(player, MOVE_THUNDER_SHOCK); } + } SCENE { + ANIMATION(ANIM_TYPE_MOVE, MOVE_THUNDER_SHOCK, player); + HP_BAR(opponent); + NOT ANIMATION(ANIM_TYPE_STATUS, B_ANIM_STATUS_PRZ, opponent); + NOT STATUS_ICON(opponent, paralysis: TRUE); + } +} diff --git a/test/move_effect_poison_hit.c b/test/move_effect_poison_hit.c new file mode 100644 index 000000000..b1a154810 --- /dev/null +++ b/test/move_effect_poison_hit.c @@ -0,0 +1,39 @@ +#include "global.h" +#include "test_battle.h" + +ASSUMPTIONS +{ + ASSUME(gBattleMoves[MOVE_POISON_STING].effect == EFFECT_POISON_HIT); +} + +SINGLE_BATTLE_TEST("Poison Sting inflicts poison") +{ + GIVEN { + PLAYER(SPECIES_WOBBUFFET); + OPPONENT(SPECIES_WOBBUFFET); + } WHEN { + TURN { MOVE(player, MOVE_POISON_STING); } + TURN {} + } SCENE { + ANIMATION(ANIM_TYPE_MOVE, MOVE_POISON_STING, player); + HP_BAR(opponent); + ANIMATION(ANIM_TYPE_STATUS, B_ANIM_STATUS_PSN, opponent); + STATUS_ICON(opponent, poison: TRUE); + } +} + +SINGLE_BATTLE_TEST("Poison Sting cannot poison Poison-type") +{ + GIVEN { + ASSUME(gSpeciesInfo[SPECIES_NIDORAN_M].types[0] == TYPE_POISON); + PLAYER(SPECIES_WOBBUFFET); + OPPONENT(SPECIES_NIDORAN_M); + } WHEN { + TURN { MOVE(player, MOVE_POISON_STING); } + } SCENE { + ANIMATION(ANIM_TYPE_MOVE, MOVE_POISON_STING, player); + HP_BAR(opponent); + NOT ANIMATION(ANIM_TYPE_STATUS, B_ANIM_STATUS_PSN, opponent); + NOT STATUS_ICON(opponent, poison: TRUE); + } +} diff --git a/test/move_effect_rampage.c b/test/move_effect_rampage.c new file mode 100644 index 000000000..a3afebf6b --- /dev/null +++ b/test/move_effect_rampage.c @@ -0,0 +1,91 @@ +#include "global.h" +#include "test_battle.h" + +ASSUMPTIONS +{ + ASSUME(gBattleMoves[MOVE_THRASH].effect == EFFECT_RAMPAGE); +} + +SINGLE_BATTLE_TEST("Thrash lasts for 2 or 3 turns") +{ + PASSES_RANDOMLY(1, 2); + GIVEN { + PLAYER(SPECIES_WOBBUFFET); + OPPONENT(SPECIES_WOBBUFFET); + } WHEN { + TURN { MOVE(player, MOVE_THRASH); } + TURN { SKIP_TURN(player); } + TURN { SKIP_TURN(player); } + } SCENE { + ANIMATION(ANIM_TYPE_MOVE, MOVE_THRASH, player); + ANIMATION(ANIM_TYPE_MOVE, MOVE_THRASH, player); + ANIMATION(ANIM_TYPE_MOVE, MOVE_THRASH, player); + } +} + +SINGLE_BATTLE_TEST("Thrash confuses the user after it finishes") +{ + GIVEN { + RNGSeed(0x00000000); + PLAYER(SPECIES_WOBBUFFET); + OPPONENT(SPECIES_WOBBUFFET); + } WHEN { + TURN { MOVE(player, MOVE_THRASH); } + TURN { SKIP_TURN(player); } + TURN { SKIP_TURN(player); } + } SCENE { + ANIMATION(ANIM_TYPE_MOVE, MOVE_THRASH, player); + ANIMATION(ANIM_TYPE_MOVE, MOVE_THRASH, player); + ANIMATION(ANIM_TYPE_MOVE, MOVE_THRASH, player); + ANIMATION(ANIM_TYPE_STATUS, B_ANIM_STATUS_CONFUSION, player); + } +} + +SINGLE_BATTLE_TEST("Thrash does not confuse the user if it is canceled on turn 1 of 3") +{ + GIVEN { + ASSUME(B_RAMPAGE_CANCELLING >= GEN_5); + RNGSeed(0x00000000); + PLAYER(SPECIES_WOBBUFFET); + OPPONENT(SPECIES_WOBBUFFET); + } WHEN { + TURN { MOVE(player, MOVE_THRASH); } + TURN { MOVE(opponent, MOVE_PROTECT); SKIP_TURN(player); } + TURN { SKIP_TURN(player); } + } SCENE { + NOT ANIMATION(ANIM_TYPE_STATUS, B_ANIM_STATUS_CONFUSION, player); + } +} + +SINGLE_BATTLE_TEST("Thrash does not confuse the user if it is canceled on turn 2 of 3") +{ + GIVEN { + ASSUME(B_RAMPAGE_CANCELLING >= GEN_5); + RNGSeed(0x00000000); + PLAYER(SPECIES_WOBBUFFET); + OPPONENT(SPECIES_WOBBUFFET); + } WHEN { + TURN { MOVE(player, MOVE_THRASH); } + TURN { MOVE(opponent, MOVE_PROTECT); SKIP_TURN(player); } + TURN { SKIP_TURN(player); } + } SCENE { + NOT ANIMATION(ANIM_TYPE_STATUS, B_ANIM_STATUS_CONFUSION, player); + } +} + +SINGLE_BATTLE_TEST("Thrash confuses the user if it is canceled on turn 3 of 3") +{ + KNOWN_FAILING; + GIVEN { + ASSUME(B_RAMPAGE_CANCELLING >= GEN_5); + RNGSeed(0x00000000); + PLAYER(SPECIES_WOBBUFFET); + OPPONENT(SPECIES_WOBBUFFET); + } WHEN { + TURN { MOVE(player, MOVE_THRASH); } + TURN { SKIP_TURN(player); } + TURN { MOVE(opponent, MOVE_PROTECT); SKIP_TURN(player); } + } SCENE { + ANIMATION(ANIM_TYPE_STATUS, B_ANIM_STATUS_CONFUSION, player); + } +} diff --git a/test/move_effect_recoil_if_miss.c b/test/move_effect_recoil_if_miss.c new file mode 100644 index 000000000..5c1f1a61a --- /dev/null +++ b/test/move_effect_recoil_if_miss.c @@ -0,0 +1,57 @@ +#include "global.h" +#include "test_battle.h" + +ASSUMPTIONS +{ + ASSUME(gBattleMoves[MOVE_JUMP_KICK].effect == EFFECT_RECOIL_IF_MISS); +} + +SINGLE_BATTLE_TEST("Jump Kick has 50% recoil on miss") +{ + s16 recoil; + GIVEN { + PLAYER(SPECIES_WOBBUFFET); + OPPONENT(SPECIES_WOBBUFFET); + } WHEN { + TURN { MOVE(player, MOVE_JUMP_KICK, hit: FALSE); } + } SCENE { + s32 maxHP = GetMonData(&PLAYER_PARTY[0], MON_DATA_MAX_HP); + MESSAGE("Wobbuffet used Jump Kick!"); + MESSAGE("Wobbuffet's attack missed!"); + MESSAGE("Wobbuffet kept going and crashed!"); + HP_BAR(player, damage: maxHP / 2); + } +} + +SINGLE_BATTLE_TEST("Jump Kick has 50% recoil on protect") +{ + s16 recoil; + GIVEN { + ASSUME(gBattleMoves[MOVE_JUMP_KICK].flags & FLAG_PROTECT_AFFECTED); + PLAYER(SPECIES_WOBBUFFET); + OPPONENT(SPECIES_WOBBUFFET); + } WHEN { + TURN { MOVE(opponent, MOVE_PROTECT); MOVE(player, MOVE_JUMP_KICK, hit: FALSE); } + } SCENE { + s32 maxHP = GetMonData(&PLAYER_PARTY[0], MON_DATA_MAX_HP); + ANIMATION(ANIM_TYPE_MOVE, MOVE_PROTECT, opponent); + NOT ANIMATION(ANIM_TYPE_MOVE, MOVE_JUMP_KICK, player); + HP_BAR(player, damage: maxHP / 2); + } +} + +SINGLE_BATTLE_TEST("Jump Kick has no recoil if no target") +{ + KNOWN_FAILING; // #2596. + GIVEN { + PLAYER(SPECIES_WOBBUFFET); + OPPONENT(SPECIES_WOBBUFFET); + OPPONENT(SPECIES_WYNAUT); + } WHEN { + TURN { MOVE(opponent, MOVE_HEALING_WISH); MOVE(player, MOVE_JUMP_KICK, hit: FALSE); SEND_OUT(opponent, 1); } + } SCENE { + s32 maxHP = GetMonData(&PLAYER_PARTY[0], MON_DATA_MAX_HP); + ANIMATION(ANIM_TYPE_MOVE, MOVE_HEALING_WISH, opponent); + NOT HP_BAR(player, damage: maxHP / 2); + } +} diff --git a/test/move_effect_reflect.c b/test/move_effect_reflect.c new file mode 100644 index 000000000..4ea875f35 --- /dev/null +++ b/test/move_effect_reflect.c @@ -0,0 +1,77 @@ +#include "global.h" +#include "test_battle.h" + +ASSUMPTIONS +{ + ASSUME(gBattleMoves[MOVE_REFLECT].effect == EFFECT_REFLECT); +} + +SINGLE_BATTLE_TEST("Reflect reduces physical damage", s16 damage) +{ + u32 move; + PARAMETRIZE { move = MOVE_CELEBRATE; } + PARAMETRIZE { move = MOVE_REFLECT; } + GIVEN { + PLAYER(SPECIES_WOBBUFFET); + OPPONENT(SPECIES_WOBBUFFET); + } WHEN { + TURN { MOVE(player, move); MOVE(opponent, MOVE_TACKLE); } + } SCENE { + ANIMATION(ANIM_TYPE_MOVE, move, player); + ANIMATION(ANIM_TYPE_MOVE, MOVE_TACKLE, opponent); + HP_BAR(player, captureDamage: &results[i].damage); + } FINALLY { + EXPECT_LT(results[1].damage, results[0].damage); + } +} + +SINGLE_BATTLE_TEST("Reflect applies for 5 turns") +{ + u16 damage[6]; + GIVEN { + PLAYER(SPECIES_WOBBUFFET); + OPPONENT(SPECIES_WOBBUFFET); + } WHEN { + TURN { MOVE(player, MOVE_REFLECT); MOVE(opponent, MOVE_TACKLE); } + TURN { MOVE(opponent, MOVE_TACKLE); } + TURN { MOVE(opponent, MOVE_TACKLE); } + TURN { MOVE(opponent, MOVE_TACKLE); } + TURN { MOVE(opponent, MOVE_TACKLE); } + TURN { MOVE(opponent, MOVE_TACKLE); } + } SCENE { + ANIMATION(ANIM_TYPE_MOVE, MOVE_REFLECT, player); + ANIMATION(ANIM_TYPE_MOVE, MOVE_TACKLE, opponent); + HP_BAR(player, captureDamage: &damage[0]); + ANIMATION(ANIM_TYPE_MOVE, MOVE_TACKLE, opponent); + HP_BAR(player, captureDamage: &damage[1]); + ANIMATION(ANIM_TYPE_MOVE, MOVE_TACKLE, opponent); + HP_BAR(player, captureDamage: &damage[2]); + ANIMATION(ANIM_TYPE_MOVE, MOVE_TACKLE, opponent); + HP_BAR(player, captureDamage: &damage[3]); + ANIMATION(ANIM_TYPE_MOVE, MOVE_TACKLE, opponent); + HP_BAR(player, captureDamage: &damage[4]); + ANIMATION(ANIM_TYPE_MOVE, MOVE_TACKLE, opponent); + HP_BAR(player, captureDamage: &damage[5]); + } THEN { + EXPECT_MUL_EQ(damage[0], Q_4_12(1.0), damage[1]); + EXPECT_MUL_EQ(damage[0], Q_4_12(1.0), damage[2]); + EXPECT_MUL_EQ(damage[0], Q_4_12(1.0), damage[3]); + EXPECT_MUL_EQ(damage[0], Q_4_12(1.0), damage[4]); + EXPECT_LT(damage[0], damage[5]); + } +} + +SINGLE_BATTLE_TEST("Reflect fails if already active") +{ + GIVEN { + PLAYER(SPECIES_WOBBUFFET); + OPPONENT(SPECIES_WOBBUFFET); + } WHEN { + TURN { MOVE(player, MOVE_REFLECT); } + TURN { MOVE(player, MOVE_REFLECT); } + } SCENE { + ANIMATION(ANIM_TYPE_MOVE, MOVE_REFLECT, player); + MESSAGE("Wobbuffet used Reflect!"); + MESSAGE("But it failed!"); + } +} diff --git a/test/move_effect_sleep.c b/test/move_effect_sleep.c new file mode 100644 index 000000000..c80faf4bd --- /dev/null +++ b/test/move_effect_sleep.c @@ -0,0 +1,21 @@ +#include "global.h" +#include "test_battle.h" + +ASSUMPTIONS +{ + ASSUME(gBattleMoves[MOVE_HYPNOSIS].effect == EFFECT_SLEEP); +} + +SINGLE_BATTLE_TEST("Hypnosis inflicts sleep") +{ + GIVEN { + PLAYER(SPECIES_WOBBUFFET); + OPPONENT(SPECIES_WOBBUFFET); + } WHEN { + TURN { MOVE(player, MOVE_HYPNOSIS); } + } SCENE { + ANIMATION(ANIM_TYPE_MOVE, MOVE_HYPNOSIS, player); + ANIMATION(ANIM_TYPE_STATUS, B_ANIM_STATUS_SLP, opponent); + STATUS_ICON(opponent, sleep: TRUE); + } +} diff --git a/test/move_effect_special_attack_down.c b/test/move_effect_special_attack_down.c new file mode 100644 index 000000000..a20b8558e --- /dev/null +++ b/test/move_effect_special_attack_down.c @@ -0,0 +1,32 @@ +#include "global.h" +#include "test_battle.h" + +ASSUMPTIONS +{ + ASSUME(gBattleMoves[MOVE_CONFIDE].effect == EFFECT_SPECIAL_ATTACK_DOWN); +} + +SINGLE_BATTLE_TEST("Confide lowers Special Attack", s16 damage) +{ + bool32 lowerSpecialAttack; + PARAMETRIZE { lowerSpecialAttack = FALSE; } + PARAMETRIZE { lowerSpecialAttack = TRUE; } + GIVEN { + ASSUME(gBattleMoves[MOVE_GUST].split == SPLIT_SPECIAL); + PLAYER(SPECIES_WOBBUFFET); + OPPONENT(SPECIES_WOBBUFFET); + } WHEN { + if (lowerSpecialAttack) TURN { MOVE(player, MOVE_CONFIDE); } + TURN { MOVE(opponent, MOVE_GUST); } + } SCENE { + if (lowerSpecialAttack) { + ANIMATION(ANIM_TYPE_MOVE, MOVE_CONFIDE, player); + ANIMATION(ANIM_TYPE_GENERAL, B_ANIM_STATS_CHANGE, opponent); + MESSAGE("Foe Wobbuffet's sp. attack fell!"); + } + ANIMATION(ANIM_TYPE_MOVE, MOVE_GUST, opponent); + HP_BAR(player, captureDamage: &results[i].damage); + } FINALLY { + EXPECT_MUL_EQ(results[1].damage, Q_4_12(1.5), results[0].damage); + } +} diff --git a/test/move_effect_special_attack_up_3.c b/test/move_effect_special_attack_up_3.c new file mode 100644 index 000000000..f7e7e11d1 --- /dev/null +++ b/test/move_effect_special_attack_up_3.c @@ -0,0 +1,32 @@ +#include "global.h" +#include "test_battle.h" + +ASSUMPTIONS +{ + ASSUME(gBattleMoves[MOVE_TAIL_GLOW].effect == EFFECT_SPECIAL_ATTACK_UP_3); +} + +SINGLE_BATTLE_TEST("Tail Glow drastically raises Special Attack", s16 damage) +{ + bool32 raiseSpecialAttack; + PARAMETRIZE { raiseSpecialAttack = FALSE; } + PARAMETRIZE { raiseSpecialAttack = TRUE; } + GIVEN { + ASSUME(gBattleMoves[MOVE_GUST].split == SPLIT_SPECIAL); + PLAYER(SPECIES_WOBBUFFET); + OPPONENT(SPECIES_WOBBUFFET); + } WHEN { + if (raiseSpecialAttack) TURN { MOVE(player, MOVE_TAIL_GLOW); } + TURN { MOVE(player, MOVE_GUST); } + } SCENE { + if (raiseSpecialAttack) { + ANIMATION(ANIM_TYPE_MOVE, MOVE_TAIL_GLOW, player); + ANIMATION(ANIM_TYPE_GENERAL, B_ANIM_STATS_CHANGE, player); + MESSAGE("Wobbuffet's sp. attack drastically rose!"); + } + ANIMATION(ANIM_TYPE_MOVE, MOVE_GUST, player); + HP_BAR(opponent, captureDamage: &results[i].damage); + } FINALLY { + EXPECT_MUL_EQ(results[0].damage, Q_4_12(2.5), results[1].damage); + } +} diff --git a/test/move_effect_spikes.c b/test/move_effect_spikes.c new file mode 100644 index 000000000..33b0bad4b --- /dev/null +++ b/test/move_effect_spikes.c @@ -0,0 +1,135 @@ +#include "global.h" +#include "test_battle.h" + +ASSUMPTIONS +{ + ASSUME(gBattleMoves[MOVE_SPIKES].effect == EFFECT_SPIKES); +} + +SINGLE_BATTLE_TEST("Spikes damage on switch in") +{ + u32 layers; + u32 divisor; + PARAMETRIZE { layers = 1; divisor = 8; } + PARAMETRIZE { layers = 2; divisor = 6; } + PARAMETRIZE { layers = 3; divisor = 4; } + GIVEN { + PLAYER(SPECIES_WOBBUFFET); + OPPONENT(SPECIES_WOBBUFFET); + OPPONENT(SPECIES_WYNAUT); + } WHEN { + u32 count; + for (count = 0; count < layers; ++count) { + TURN { MOVE(player, MOVE_SPIKES); } + } + TURN { SWITCH(opponent, 1); } + } SCENE { + u32 count; + s32 maxHP = GetMonData(&OPPONENT_PARTY[1], MON_DATA_MAX_HP); + for (count = 0; count < layers; ++count) { + ANIMATION(ANIM_TYPE_MOVE, MOVE_SPIKES, player); + MESSAGE("Spikes were scattered all around the opponent's side!"); + } + MESSAGE("2 sent out Wynaut!"); + HP_BAR(opponent, damage: maxHP / divisor); + MESSAGE("Foe Wynaut is hurt by spikes!"); + } +} + +SINGLE_BATTLE_TEST("Spikes fails after 3 layers") +{ + GIVEN { + PLAYER(SPECIES_WOBBUFFET); + OPPONENT(SPECIES_WOBBUFFET); + OPPONENT(SPECIES_WYNAUT); + } WHEN { + TURN { MOVE(player, MOVE_SPIKES); } + TURN { MOVE(player, MOVE_SPIKES); } + TURN { MOVE(player, MOVE_SPIKES); } + TURN { MOVE(player, MOVE_SPIKES); } + TURN { SWITCH(opponent, 1); } + } SCENE { + s32 maxHP = GetMonData(&OPPONENT_PARTY[1], MON_DATA_MAX_HP); + ANIMATION(ANIM_TYPE_MOVE, MOVE_SPIKES, player); + MESSAGE("Spikes were scattered all around the opponent's side!"); + ANIMATION(ANIM_TYPE_MOVE, MOVE_SPIKES, player); + MESSAGE("Spikes were scattered all around the opponent's side!"); + ANIMATION(ANIM_TYPE_MOVE, MOVE_SPIKES, player); + MESSAGE("Spikes were scattered all around the opponent's side!"); + MESSAGE("Wobbuffet used Spikes!"); + MESSAGE("But it failed!"); + MESSAGE("2 sent out Wynaut!"); + HP_BAR(opponent, damage: maxHP / 4); + MESSAGE("Foe Wynaut is hurt by spikes!"); + } +} + +SINGLE_BATTLE_TEST("Spikes damage on subsequent switch ins") +{ + GIVEN { + PLAYER(SPECIES_WOBBUFFET); + OPPONENT(SPECIES_WOBBUFFET); + OPPONENT(SPECIES_WYNAUT); + } WHEN { + TURN { MOVE(player, MOVE_SPIKES); } + TURN { SWITCH(opponent, 1); } + TURN { SWITCH(opponent, 0); } + } SCENE { + s32 maxHP0 = GetMonData(&OPPONENT_PARTY[0], MON_DATA_MAX_HP); + s32 maxHP1 = GetMonData(&OPPONENT_PARTY[1], MON_DATA_MAX_HP); + MESSAGE("2 sent out Wynaut!"); + HP_BAR(opponent, damage: maxHP1 / 8); + MESSAGE("Foe Wynaut is hurt by spikes!"); + MESSAGE("2 sent out Wobbuffet!"); + HP_BAR(opponent, damage: maxHP0 / 8); + MESSAGE("Foe Wobbuffet is hurt by spikes!"); + } +} + +SINGLE_BATTLE_TEST("Spikes do not damage airborne Pokemon") +{ + u32 species = SPECIES_WOBBUFFET; + u32 item = ITEM_NONE; + u32 move1 = MOVE_CELEBRATE; + u32 move2 = MOVE_CELEBRATE; + bool32 airborne; + + ASSUME(gSpeciesInfo[SPECIES_PIDGEY].types[1] == TYPE_FLYING); + PARAMETRIZE { species = SPECIES_PIDGEY; airborne = TRUE; } + PARAMETRIZE { species = SPECIES_PIDGEY; item = ITEM_IRON_BALL; airborne = FALSE; } + PARAMETRIZE { species = SPECIES_PIDGEY; move1 = MOVE_GRAVITY; airborne = FALSE; } + PARAMETRIZE { species = SPECIES_PIDGEY; move1 = MOVE_INGRAIN; airborne = FALSE; } + + ASSUME(gSpeciesInfo[SPECIES_UNOWN].abilities[0] == ABILITY_LEVITATE); + PARAMETRIZE { species = SPECIES_UNOWN; airborne = TRUE; } + PARAMETRIZE { species = SPECIES_UNOWN; item = ITEM_IRON_BALL; airborne = FALSE; } + PARAMETRIZE { species = SPECIES_UNOWN; move1 = MOVE_GRAVITY; airborne = FALSE; } + PARAMETRIZE { species = SPECIES_UNOWN; move1 = MOVE_INGRAIN; airborne = FALSE; } + + PARAMETRIZE { move1 = MOVE_MAGNET_RISE; airborne = TRUE; } + PARAMETRIZE { move1 = MOVE_MAGNET_RISE; item = ITEM_IRON_BALL; airborne = FALSE; } + PARAMETRIZE { move1 = MOVE_MAGNET_RISE; move2 = MOVE_GRAVITY; airborne = FALSE; } + // Magnet Rise fails under Gravity. + // Magnet Rise fails under Ingrain and vice-versa. + + PARAMETRIZE { item = ITEM_AIR_BALLOON; airborne = TRUE; } + PARAMETRIZE { item = ITEM_AIR_BALLOON; move1 = MOVE_GRAVITY; airborne = FALSE; } + PARAMETRIZE { item = ITEM_AIR_BALLOON; move1 = MOVE_INGRAIN; airborne = FALSE; } + + GIVEN { + PLAYER(SPECIES_WOBBUFFET); + OPPONENT(SPECIES_WOBBUFFET); + OPPONENT(species) { Item(item); } + } WHEN { + TURN { MOVE(player, MOVE_SPIKES); MOVE(opponent, move1); } + TURN { MOVE(opponent, move2); } + TURN { MOVE(opponent, MOVE_BATON_PASS); SEND_OUT(opponent, 1); } + } SCENE { + s32 maxHP = GetMonData(&OPPONENT_PARTY[1], MON_DATA_MAX_HP); + if (airborne) { + NOT HP_BAR(opponent, damage: maxHP / 8); + } else { + HP_BAR(opponent, damage: maxHP / 8); + } + } +} diff --git a/test/move_effect_tailwind.c b/test/move_effect_tailwind.c new file mode 100644 index 000000000..7dfffdbe0 --- /dev/null +++ b/test/move_effect_tailwind.c @@ -0,0 +1,55 @@ +#include "global.h" +#include "test_battle.h" + +ASSUMPTIONS +{ + ASSUME(gBattleMoves[MOVE_TAILWIND].effect == EFFECT_TAILWIND); +} + +SINGLE_BATTLE_TEST("Tailwind applies for 4 turns") +{ + GIVEN { + ASSUME(B_TAILWIND_TURNS >= GEN_5); + PLAYER(SPECIES_WOBBUFFET) { Speed(10); } + OPPONENT(SPECIES_WOBBUFFET) { Speed(15); } + } WHEN { + TURN { MOVE(opponent, MOVE_CELEBRATE); MOVE(player, MOVE_TAILWIND); } + TURN {} + TURN {} + TURN {} + TURN {} + } SCENE { + MESSAGE("Foe Wobbuffet used Celebrate!"); + MESSAGE("Wobbuffet used Tailwind!"); + + MESSAGE("Wobbuffet used Celebrate!"); + MESSAGE("Foe Wobbuffet used Celebrate!"); + + MESSAGE("Wobbuffet used Celebrate!"); + MESSAGE("Foe Wobbuffet used Celebrate!"); + + MESSAGE("Wobbuffet used Celebrate!"); + MESSAGE("Foe Wobbuffet used Celebrate!"); + + MESSAGE("Foe Wobbuffet used Celebrate!"); + MESSAGE("Wobbuffet used Celebrate!"); + } +} + +DOUBLE_BATTLE_TEST("Tailwind affects partner on first turn") +{ + GIVEN { + ASSUME(B_RECALC_TURN_AFTER_ACTIONS); + PLAYER(SPECIES_WOBBUFFET) { Speed(20); } + PLAYER(SPECIES_WYNAUT) { Speed(10); } + OPPONENT(SPECIES_WOBBUFFET) { Speed(15); } + OPPONENT(SPECIES_WYNAUT) { Speed(14); } + } WHEN { + TURN { MOVE(playerLeft, MOVE_TAILWIND); } + } SCENE { + MESSAGE("Wobbuffet used Tailwind!"); + MESSAGE("Wynaut used Celebrate!"); + MESSAGE("Foe Wobbuffet used Celebrate!"); + MESSAGE("Foe Wynaut used Celebrate!"); + } +} diff --git a/test/move_effect_torment.c b/test/move_effect_torment.c new file mode 100644 index 000000000..43f05e29f --- /dev/null +++ b/test/move_effect_torment.c @@ -0,0 +1,53 @@ +#include "global.h" +#include "test_battle.h" + +ASSUMPTIONS +{ + ASSUME(gBattleMoves[MOVE_TORMENT].effect == EFFECT_TORMENT); +} + +SINGLE_BATTLE_TEST("Torment prevents consecutive move uses") +{ + GIVEN { + PLAYER(SPECIES_WOBBUFFET); + OPPONENT(SPECIES_WOBBUFFET) { Moves(MOVE_SPLASH, MOVE_CELEBRATE); } + } WHEN { + TURN { MOVE(player, MOVE_TORMENT); MOVE(opponent, MOVE_SPLASH); } + TURN { MOVE(opponent, MOVE_SPLASH, allowed: FALSE); MOVE(opponent, MOVE_CELEBRATE); } + } SCENE { + ANIMATION(ANIM_TYPE_MOVE, MOVE_TORMENT, player); + MESSAGE("Foe Wobbuffet was subjected to torment!"); + ANIMATION(ANIM_TYPE_MOVE, MOVE_SPLASH, opponent); + ANIMATION(ANIM_TYPE_MOVE, MOVE_CELEBRATE, opponent); + } +} + +SINGLE_BATTLE_TEST("Torment forces Struggle if the only move is prevented") +{ + GIVEN { + PLAYER(SPECIES_WOBBUFFET); + OPPONENT(SPECIES_WOBBUFFET) { Moves(MOVE_SPLASH); } + } WHEN { + TURN { MOVE(player, MOVE_TORMENT); MOVE(opponent, MOVE_SPLASH); } + TURN { MOVE(opponent, MOVE_SPLASH, allowed: FALSE); } + } SCENE { + ANIMATION(ANIM_TYPE_MOVE, MOVE_SPLASH, opponent); + ANIMATION(ANIM_TYPE_MOVE, MOVE_STRUGGLE, opponent); + } +} + +SINGLE_BATTLE_TEST("Torment allows non-consecutive move uses") +{ + GIVEN { + PLAYER(SPECIES_WOBBUFFET); + OPPONENT(SPECIES_WOBBUFFET); + } WHEN { + TURN { MOVE(player, MOVE_TORMENT); MOVE(opponent, MOVE_SPLASH); } + TURN { MOVE(opponent, MOVE_CELEBRATE); } + TURN { MOVE(opponent, MOVE_SPLASH); } + } SCENE { + ANIMATION(ANIM_TYPE_MOVE, MOVE_SPLASH, opponent); + ANIMATION(ANIM_TYPE_MOVE, MOVE_CELEBRATE, opponent); + ANIMATION(ANIM_TYPE_MOVE, MOVE_SPLASH, opponent); + } +} diff --git a/test/move_effect_toxic.c b/test/move_effect_toxic.c new file mode 100644 index 000000000..f71d2a972 --- /dev/null +++ b/test/move_effect_toxic.c @@ -0,0 +1,48 @@ +#include "global.h" +#include "test_battle.h" + +ASSUMPTIONS +{ + ASSUME(gBattleMoves[MOVE_TOXIC].effect == EFFECT_TOXIC); +} + +SINGLE_BATTLE_TEST("Toxic inflicts bad poison") +{ + GIVEN { + PLAYER(SPECIES_WOBBUFFET); + OPPONENT(SPECIES_WOBBUFFET); + } WHEN { + TURN { MOVE(player, MOVE_TOXIC); } + TURN {} + } SCENE { + ANIMATION(ANIM_TYPE_MOVE, MOVE_TOXIC, player); + ANIMATION(ANIM_TYPE_STATUS, B_ANIM_STATUS_PSN, opponent); + STATUS_ICON(opponent, badPoison: TRUE); + } +} + +SINGLE_BATTLE_TEST("Toxic cannot miss if used by a Poison-type") +{ + u32 species; + bool32 hit; + PARAMETRIZE { species = SPECIES_WOBBUFFET; hit = FALSE; } + PARAMETRIZE { species = SPECIES_NIDORAN_M; hit = TRUE; } + GIVEN { + ASSUME(B_TOXIC_NEVER_MISS >= GEN_6); + ASSUME(gSpeciesInfo[SPECIES_NIDORAN_M].types[0] == TYPE_POISON); + PLAYER(species); + OPPONENT(SPECIES_WOBBUFFET); + } WHEN { + TURN { MOVE(player, MOVE_TOXIC, hit: FALSE); } + } SCENE { + if (hit) { + ANIMATION(ANIM_TYPE_MOVE, MOVE_TOXIC, player); + ANIMATION(ANIM_TYPE_STATUS, B_ANIM_STATUS_PSN, opponent); + STATUS_ICON(opponent, badPoison: TRUE); + } else { + NOT ANIMATION(ANIM_TYPE_MOVE, MOVE_TOXIC, player); + NOT ANIMATION(ANIM_TYPE_STATUS, B_ANIM_STATUS_PSN, opponent); + NOT STATUS_ICON(opponent, badPoison: TRUE); + } + } +} diff --git a/test/move_effect_toxic_spikes.c b/test/move_effect_toxic_spikes.c new file mode 100644 index 000000000..44780da21 --- /dev/null +++ b/test/move_effect_toxic_spikes.c @@ -0,0 +1,210 @@ +#include "global.h" +#include "test_battle.h" + +ASSUMPTIONS +{ + ASSUME(gBattleMoves[MOVE_TOXIC_SPIKES].effect == EFFECT_TOXIC_SPIKES); +} + +SINGLE_BATTLE_TEST("Toxic Spikes inflicts poison on switch in") +{ + GIVEN { + PLAYER(SPECIES_WOBBUFFET); + OPPONENT(SPECIES_WOBBUFFET); + OPPONENT(SPECIES_WYNAUT); + } WHEN { + TURN { MOVE(player, MOVE_TOXIC_SPIKES); } + TURN { SWITCH(opponent, 1); } + TURN {} + } SCENE { + ANIMATION(ANIM_TYPE_MOVE, MOVE_TOXIC_SPIKES, player); + MESSAGE("Poison Spikes were scattered all around the opposing team's feet!"); + MESSAGE("2 sent out Wynaut!"); + ANIMATION(ANIM_TYPE_STATUS, B_ANIM_STATUS_PSN, opponent); + STATUS_ICON(opponent, poison: TRUE); + } +} + +SINGLE_BATTLE_TEST("Toxic Spikes inflicts bad poison on switch in") +{ + GIVEN { + PLAYER(SPECIES_WOBBUFFET); + OPPONENT(SPECIES_WOBBUFFET); + OPPONENT(SPECIES_WYNAUT); + } WHEN { + TURN { MOVE(player, MOVE_TOXIC_SPIKES); } + TURN { MOVE(player, MOVE_TOXIC_SPIKES); } + TURN { SWITCH(opponent, 1); } + TURN {} + } SCENE { + ANIMATION(ANIM_TYPE_MOVE, MOVE_TOXIC_SPIKES, player); + MESSAGE("Poison Spikes were scattered all around the opposing team's feet!"); + ANIMATION(ANIM_TYPE_MOVE, MOVE_TOXIC_SPIKES, player); + MESSAGE("Poison Spikes were scattered all around the opposing team's feet!"); + MESSAGE("2 sent out Wynaut!"); + ANIMATION(ANIM_TYPE_STATUS, B_ANIM_STATUS_PSN, opponent); + STATUS_ICON(opponent, badPoison: TRUE); + } +} + +SINGLE_BATTLE_TEST("Toxic Spikes fails after 2 layers") +{ + GIVEN { + PLAYER(SPECIES_WOBBUFFET); + OPPONENT(SPECIES_WOBBUFFET); + OPPONENT(SPECIES_WYNAUT); + } WHEN { + TURN { MOVE(player, MOVE_TOXIC_SPIKES); } + TURN { MOVE(player, MOVE_TOXIC_SPIKES); } + TURN { MOVE(player, MOVE_TOXIC_SPIKES); } + TURN { SWITCH(opponent, 1); } + TURN {} + } SCENE { + ANIMATION(ANIM_TYPE_MOVE, MOVE_TOXIC_SPIKES, player); + MESSAGE("Poison Spikes were scattered all around the opposing team's feet!"); + ANIMATION(ANIM_TYPE_MOVE, MOVE_TOXIC_SPIKES, player); + MESSAGE("Poison Spikes were scattered all around the opposing team's feet!"); + MESSAGE("Wobbuffet used Toxic Spikes!"); + MESSAGE("But it failed!"); + MESSAGE("2 sent out Wynaut!"); + ANIMATION(ANIM_TYPE_STATUS, B_ANIM_STATUS_PSN, opponent); + STATUS_ICON(opponent, badPoison: TRUE); + } +} + +SINGLE_BATTLE_TEST("Toxic Spikes inflicts poison on subsequent switch ins") +{ + GIVEN { + PLAYER(SPECIES_WOBBUFFET); + OPPONENT(SPECIES_WOBBUFFET); + OPPONENT(SPECIES_WYNAUT); + } WHEN { + TURN { MOVE(player, MOVE_TOXIC_SPIKES); } + TURN { SWITCH(opponent, 1); } + TURN { SWITCH(opponent, 0); } + TURN {} + } SCENE { + MESSAGE("2 sent out Wynaut!"); + STATUS_ICON(opponent, poison: TRUE); + } +} + +SINGLE_BATTLE_TEST("Toxic Spikes do not poison airborne Pokemon") +{ + u32 species = SPECIES_WOBBUFFET; + u32 item = ITEM_NONE; + u32 move1 = MOVE_CELEBRATE; + u32 move2 = MOVE_CELEBRATE; + bool32 airborne; + + ASSUME(gSpeciesInfo[SPECIES_PIDGEY].types[1] == TYPE_FLYING); + PARAMETRIZE { species = SPECIES_PIDGEY; airborne = TRUE; } + PARAMETRIZE { species = SPECIES_PIDGEY; item = ITEM_IRON_BALL; airborne = FALSE; } + PARAMETRIZE { species = SPECIES_PIDGEY; move1 = MOVE_GRAVITY; airborne = FALSE; } + PARAMETRIZE { species = SPECIES_PIDGEY; move1 = MOVE_INGRAIN; airborne = FALSE; } + + ASSUME(gSpeciesInfo[SPECIES_UNOWN].abilities[0] == ABILITY_LEVITATE); + PARAMETRIZE { species = SPECIES_UNOWN; airborne = TRUE; } + PARAMETRIZE { species = SPECIES_UNOWN; item = ITEM_IRON_BALL; airborne = FALSE; } + PARAMETRIZE { species = SPECIES_UNOWN; move1 = MOVE_GRAVITY; airborne = FALSE; } + PARAMETRIZE { species = SPECIES_UNOWN; move1 = MOVE_INGRAIN; airborne = FALSE; } + + PARAMETRIZE { move1 = MOVE_MAGNET_RISE; airborne = TRUE; } + PARAMETRIZE { move1 = MOVE_MAGNET_RISE; item = ITEM_IRON_BALL; airborne = FALSE; } + PARAMETRIZE { move1 = MOVE_MAGNET_RISE; move2 = MOVE_GRAVITY; airborne = FALSE; } + // Magnet Rise fails under Gravity. + // Magnet Rise fails under Ingrain and vice-versa. + + PARAMETRIZE { item = ITEM_AIR_BALLOON; airborne = TRUE; } + PARAMETRIZE { item = ITEM_AIR_BALLOON; move1 = MOVE_GRAVITY; airborne = FALSE; } + PARAMETRIZE { item = ITEM_AIR_BALLOON; move1 = MOVE_INGRAIN; airborne = FALSE; } + + GIVEN { + PLAYER(SPECIES_WOBBUFFET); + OPPONENT(SPECIES_WOBBUFFET); + OPPONENT(species) { Item(item); } + } WHEN { + TURN { MOVE(player, MOVE_TOXIC_SPIKES); MOVE(opponent, move1); } + TURN { MOVE(opponent, move2); } + TURN { MOVE(opponent, MOVE_BATON_PASS); SEND_OUT(opponent, 1); } + } SCENE { + if (airborne) { + NOT STATUS_ICON(opponent, poison: TRUE); + } else { + STATUS_ICON(opponent, poison: TRUE); + } + } +} + +SINGLE_BATTLE_TEST("Toxic Spikes do not affect Steel-types") +{ + GIVEN { + ASSUME(gSpeciesInfo[SPECIES_STEELIX].types[0] == TYPE_STEEL); + PLAYER(SPECIES_WOBBUFFET); + OPPONENT(SPECIES_WOBBUFFET); + OPPONENT(SPECIES_STEELIX); + } WHEN { + TURN { MOVE(player, MOVE_TOXIC_SPIKES); } + TURN { SWITCH(opponent, 1); } + } SCENE { + NOT STATUS_ICON(opponent, poison: TRUE); + } +} + +SINGLE_BATTLE_TEST("Toxic Spikes are removed by grounded Poison-types") +{ + u32 species; + u32 item = ITEM_NONE; + u32 move = MOVE_CELEBRATE; + bool32 grounded; + PARAMETRIZE { species = SPECIES_EKANS; grounded = TRUE; } + PARAMETRIZE { species = SPECIES_ZUBAT; grounded = FALSE; } + PARAMETRIZE { species = SPECIES_ZUBAT; item = ITEM_IRON_BALL; grounded = TRUE; } + PARAMETRIZE { species = SPECIES_ZUBAT; move = MOVE_GRAVITY; grounded = TRUE; } + PARAMETRIZE { species = SPECIES_ZUBAT; move = MOVE_INGRAIN; grounded = TRUE; } + GIVEN { + ASSUME(gSpeciesInfo[SPECIES_EKANS].types[0] == TYPE_POISON); + ASSUME(gSpeciesInfo[SPECIES_ZUBAT].types[0] == TYPE_POISON); + ASSUME(gSpeciesInfo[SPECIES_ZUBAT].types[1] == TYPE_FLYING); + PLAYER(SPECIES_WOBBUFFET); + OPPONENT(SPECIES_WOBBUFFET); + OPPONENT(species) { Item(item); } + } WHEN { + TURN { MOVE(player, MOVE_TOXIC_SPIKES); MOVE(opponent, move); } + TURN { MOVE(opponent, MOVE_BATON_PASS); SEND_OUT(opponent, 1); } + TURN { SWITCH(opponent, 0); } + } SCENE { + if (grounded) { + NOT STATUS_ICON(opponent, poison: TRUE); + MESSAGE("The poison spikes disappeared from around the opposing team's feet!"); + NOT STATUS_ICON(opponent, poison: TRUE); + } else { + NOT STATUS_ICON(opponent, poison: TRUE); + ANIMATION(ANIM_TYPE_MOVE, MOVE_BATON_PASS, opponent); + STATUS_ICON(opponent, poison: TRUE); + } + } +} + +// This would test for what I believe to be a bug in the mainline games. +// A Pokémon that gets passed magnet rise should still remove the Toxic +// Spikes even though it is airborne. +// The test currently fails, because we don't incorporate this bug. +SINGLE_BATTLE_TEST("Toxic Spikes are removed by Poison-types affected by Magnet Rise") +{ + KNOWN_FAILING; + GIVEN { + ASSUME(gSpeciesInfo[SPECIES_EKANS].types[0] == TYPE_POISON); + PLAYER(SPECIES_WOBBUFFET); + OPPONENT(SPECIES_WOBBUFFET); + OPPONENT(SPECIES_EKANS); + } WHEN { + TURN { MOVE(opponent, MOVE_MAGNET_RISE); } + TURN { MOVE(player, MOVE_TOXIC_SPIKES); MOVE(opponent, MOVE_BATON_PASS); SEND_OUT(opponent, 1); } + TURN { SWITCH(opponent, 0); } + } SCENE { + NOT STATUS_ICON(opponent, poison: TRUE); + MESSAGE("The poison spikes disappeared from around the opposing team's feet!"); + NOT STATUS_ICON(opponent, poison: TRUE); + } +} diff --git a/test/status1.c b/test/status1.c new file mode 100644 index 000000000..63a9cd041 --- /dev/null +++ b/test/status1.c @@ -0,0 +1,194 @@ +#include "global.h" +#include "test_battle.h" + +SINGLE_BATTLE_TEST("Sleep prevents the battler from using a move") +{ + u32 turns; + PARAMETRIZE { turns = 1; } + PARAMETRIZE { turns = 2; } + PARAMETRIZE { turns = 3; } + GIVEN { + PLAYER(SPECIES_WOBBUFFET) { Status1(STATUS1_SLEEP_TURN(turns)); } + OPPONENT(SPECIES_WOBBUFFET); + } WHEN { + for (i = 0; i < turns; i++) + TURN { MOVE(player, MOVE_CELEBRATE); } + } SCENE { + for (i = 0; i < turns - 1; i++) + MESSAGE("Wobbuffet is fast asleep."); + MESSAGE("Wobbuffet woke up!"); + STATUS_ICON(player, none: TRUE); + MESSAGE("Wobbuffet used Celebrate!"); + } +} + +SINGLE_BATTLE_TEST("Poison deals 1/8th damage per turn") +{ + GIVEN { + PLAYER(SPECIES_WOBBUFFET) { Status1(STATUS1_POISON); } + OPPONENT(SPECIES_WOBBUFFET); + } WHEN { + for (i = 0; i < 4; i++) + TURN {} + } SCENE { + s32 maxHP = GetMonData(&PLAYER_PARTY[0], MON_DATA_MAX_HP); + for (i = 0; i < 4; i++) + HP_BAR(player, damage: maxHP / 8); + } +} + +SINGLE_BATTLE_TEST("Burn deals 1/16th damage per turn") +{ + GIVEN { + ASSUME(B_BURN_DAMAGE >= GEN_LATEST); + PLAYER(SPECIES_WOBBUFFET) { Status1(STATUS1_BURN); } + OPPONENT(SPECIES_WOBBUFFET); + } WHEN { + for (i = 0; i < 4; i++) + TURN {} + } SCENE { + s32 maxHP = GetMonData(&PLAYER_PARTY[0], MON_DATA_MAX_HP); + for (i = 0; i < 4; i++) + HP_BAR(player, damage: maxHP / 16); + } +} + +SINGLE_BATTLE_TEST("Burn reduces attack by 50%", s16 damage) +{ + bool32 burned; + PARAMETRIZE { burned = FALSE; } + PARAMETRIZE { burned = TRUE; } + GIVEN { + PLAYER(SPECIES_WOBBUFFET) { if (burned) Status1(STATUS1_BURN); } + OPPONENT(SPECIES_WOBBUFFET); + } WHEN { + TURN { MOVE(player, MOVE_TACKLE); } + } SCENE { + HP_BAR(opponent, captureDamage: &results[i].damage); + } FINALLY { + EXPECT_MUL_EQ(results[0].damage, Q_4_12(0.5), results[1].damage); + } +} + +SINGLE_BATTLE_TEST("Freeze has a 20% chance of being thawed") +{ + PASSES_RANDOMLY(20, 100); + GIVEN { + PLAYER(SPECIES_WOBBUFFET) { Status1(STATUS1_FREEZE); } + OPPONENT(SPECIES_WOBBUFFET); + } WHEN { + TURN { MOVE(player, MOVE_CELEBRATE); } + } SCENE { + STATUS_ICON(player, none: TRUE); + } +} + +SINGLE_BATTLE_TEST("Freeze is thawed by opponent's Fire-type attacks") +{ + GIVEN { + ASSUME(gBattleMoves[MOVE_EMBER].type == TYPE_FIRE); + PLAYER(SPECIES_WOBBUFFET) { Status1(STATUS1_FREEZE); } + OPPONENT(SPECIES_WOBBUFFET); + } WHEN { + TURN { MOVE(player, MOVE_CELEBRATE); MOVE(opponent, MOVE_EMBER); } + } SCENE { + MESSAGE("Wobbuffet is frozen solid!"); + MESSAGE("Foe Wobbuffet used Ember!"); + MESSAGE("Wobbuffet was defrosted!"); + STATUS_ICON(player, none: TRUE); + } +} + +SINGLE_BATTLE_TEST("Freeze is thawed by user's Flame Wheel") +{ + GIVEN { + ASSUME(gBattleMoves[MOVE_FLAME_WHEEL].flags & FLAG_THAW_USER); + PLAYER(SPECIES_WOBBUFFET) { Status1(STATUS1_FREEZE); } + OPPONENT(SPECIES_WOBBUFFET); + } WHEN { + TURN { MOVE(player, MOVE_FLAME_WHEEL); } + } SCENE { + MESSAGE("Wobbuffet was defrosted by Flame Wheel!"); + STATUS_ICON(player, none: TRUE); + MESSAGE("Wobbuffet used Flame Wheel!"); + } +} + +SINGLE_BATTLE_TEST("Paralysis reduces speed by 50%") +{ + u16 playerSpeed; + bool32 playerFirst; + PARAMETRIZE { playerSpeed = 98; playerFirst = FALSE; } + PARAMETRIZE { playerSpeed = 102; playerFirst = TRUE; } + GIVEN { + ASSUME(B_PARALYSIS_SPEED >= GEN_7); + PLAYER(SPECIES_WOBBUFFET) { Status1(STATUS1_PARALYSIS); Speed(playerSpeed); } + OPPONENT(SPECIES_WOBBUFFET) { Speed(50); } + } WHEN { + TURN { MOVE(player, MOVE_CELEBRATE); MOVE(opponent, MOVE_CELEBRATE); } + } SCENE { + if (playerFirst) { + ONE_OF { + MESSAGE("Wobbuffet used Celebrate!"); + MESSAGE("Wobbuffet is paralyzed! It can't move!"); + } + MESSAGE("Foe Wobbuffet used Celebrate!"); + } else { + MESSAGE("Foe Wobbuffet used Celebrate!"); + ONE_OF { + MESSAGE("Wobbuffet used Celebrate!"); + MESSAGE("Wobbuffet is paralyzed! It can't move!"); + } + } + } +} + +SINGLE_BATTLE_TEST("Paralysis has a 25% chance of skipping the turn") +{ + PASSES_RANDOMLY(25, 100); + GIVEN { + PLAYER(SPECIES_WOBBUFFET) { Status1(STATUS1_PARALYSIS); } + OPPONENT(SPECIES_WOBBUFFET); + } WHEN { + TURN { MOVE(player, MOVE_CELEBRATE); } + } SCENE { + MESSAGE("Wobbuffet is paralyzed! It can't move!"); + } +} + +SINGLE_BATTLE_TEST("Bad poison deals 1/16th cumulative damage per turn") +{ + GIVEN { + PLAYER(SPECIES_WOBBUFFET) { Status1(STATUS1_TOXIC_POISON); } + OPPONENT(SPECIES_WOBBUFFET); + } WHEN { + for (i = 0; i < 4; i++) + TURN {} + } SCENE { + s32 maxHP = GetMonData(&PLAYER_PARTY[0], MON_DATA_MAX_HP); + for (i = 0; i < 4; i++) + HP_BAR(player, damage: maxHP / 16 * (i + 1)); + } +} + +SINGLE_BATTLE_TEST("Bad poison cumulative damage resets on switch") +{ + GIVEN { + PLAYER(SPECIES_WOBBUFFET) { Status1(STATUS1_TOXIC_POISON); } + PLAYER(SPECIES_WYNAUT); + OPPONENT(SPECIES_WOBBUFFET); + } WHEN { + TURN {} + TURN {} + TURN { SWITCH(player, 1); } + TURN { SWITCH(player, 0); } + TURN {} + TURN {} + } SCENE { + s32 maxHP = GetMonData(&PLAYER_PARTY[0], MON_DATA_MAX_HP); + for (i = 0; i < 2; i++) + HP_BAR(player, damage: maxHP / 16 * (i + 1)); + for (i = 0; i < 2; i++) + HP_BAR(player, damage: maxHP / 16 * (i + 1)); + } +} diff --git a/test/test.h b/test/test.h new file mode 100644 index 000000000..cbcf11717 --- /dev/null +++ b/test/test.h @@ -0,0 +1,142 @@ +#ifndef GUARD_TEST_H +#define GUARD_TEST_H + +#include "test_runner.h" + +#define MAX_PROCESSES 32 // See also tools/mgba-rom-test-hydra/main.c + +enum TestResult +{ + TEST_RESULT_FAIL, + TEST_RESULT_PASS, + TEST_RESULT_SKIP, + TEST_RESULT_INVALID, + TEST_RESULT_ERROR, + TEST_RESULT_TIMEOUT, +}; + +struct TestRunner +{ + u32 (*estimateCost)(void *); + void (*setUp)(void *); + void (*run)(void *); + void (*tearDown)(void *); + bool32 (*checkProgress)(void *); + bool32 (*handleExitWithResult)(void *, enum TestResult); +}; + +struct Test +{ + const char *name; + const char *filename; + const struct TestRunner *runner; + void *data; +}; + +struct TestRunnerState +{ + u8 state; + u8 exitCode; + s32 tests; + s32 passes; + s32 skips; + const char *skipFilename; + const struct Test *test; + u32 processCosts[MAX_PROCESSES]; + + u8 result; + u8 expectedResult; + u32 timeoutSeconds; +}; + +extern const u8 gTestRunnerN; +extern const u8 gTestRunnerI; +extern const char gTestRunnerArgv[256]; + +extern const struct TestRunner gAssumptionsRunner; +extern struct TestRunnerState gTestRunnerState; + +void CB2_TestRunner(void); + +void Test_ExpectedResult(enum TestResult); +void Test_ExitWithResult(enum TestResult, const char *fmt, ...); + +s32 MgbaPrintf_(const char *fmt, ...); + +#define ASSUMPTIONS \ + static void Assumptions(void); \ + __attribute__((section(".tests"))) static const struct Test sAssumptions = \ + { \ + .name = "ASSUMPTIONS: " __FILE__, \ + .filename = __FILE__, \ + .runner = &gAssumptionsRunner, \ + .data = Assumptions, \ + }; \ + static void Assumptions(void) + +#define ASSUME(c) \ + do \ + { \ + if (!(c)) \ + Test_ExitWithResult(TEST_RESULT_SKIP, "%s:%d: ASSUME failed", gTestRunnerState.test->filename, __LINE__); \ + } while (0) + +#define EXPECT(c) \ + do \ + { \ + if (!(c)) \ + Test_ExitWithResult(TEST_RESULT_FAIL, "%s:%d: EXPECT failed", gTestRunnerState.test->filename, __LINE__); \ + } while (0) + +#define EXPECT_EQ(a, b) \ + do \ + { \ + typeof(a) _a = (a), _b = (b); \ + if (_a != _b) \ + Test_ExitWithResult(TEST_RESULT_FAIL, "%s:%d: EXPECT_EQ(%d, %d) failed", gTestRunnerState.test->filename, __LINE__, _a, _b); \ + } while (0) + +#define EXPECT_NE(a, b) \ + do \ + { \ + typeof(a) _a = (a), _b = (b); \ + if (_a == _b) \ + Test_ExitWithResult(TEST_RESULT_FAIL, "%s:%d: EXPECT_NE(%d, %d) failed", gTestRunnerState.test->filename, __LINE__, _a, _b); \ + } while (0) + +#define EXPECT_LT(a, b) \ + do \ + { \ + typeof(a) _a = (a), _b = (b); \ + if (_a >= _b) \ + Test_ExitWithResult(TEST_RESULT_FAIL, "%s:%d: EXPECT_LT(%d, %d) failed", gTestRunnerState.test->filename, __LINE__, _a, _b); \ + } while (0) + +#define EXPECT_LE(a, b) \ + do \ + { \ + typeof(a) _a = (a), _b = (b); \ + if (_a > _b) \ + Test_ExitWithResult(TEST_RESULT_FAIL, "%s:%d: EXPECT_LE(%d, %d) failed", gTestRunnerState.test->filename, __LINE__, _a, _b); \ + } while (0) + +#define EXPECT_GT(a, b) \ + do \ + { \ + typeof(a) _a = (a), _b = (b); \ + if (_a <= _b) \ + Test_ExitWithResult(TEST_RESULT_FAIL, "%s:%d: EXPECT_GT(%d, %d) failed", gTestRunnerState.test->filename, __LINE__, _a, _b); \ + } while (0) + +#define EXPECT_GE(a, b) \ + do \ + { \ + typeof(a) _a = (a), _b = (b); \ + if (_a < _b) \ + Test_ExitWithResult(TEST_RESULT_FAIL, "%s:%d: EXPECT_GE(%d, %d) failed", gTestRunnerState.test->filename, __LINE__, _a, _b); \ + } while (0) + +#define KNOWN_FAILING \ + Test_ExpectedResult(TEST_RESULT_FAIL) + +#endif diff --git a/test/test_battle.h b/test/test_battle.h new file mode 100644 index 000000000..8fb1ce6d3 --- /dev/null +++ b/test/test_battle.h @@ -0,0 +1,826 @@ +/* Embedded DSL for automated black-box testing of battle mechanics. + * + * To run all the tests use: + * make check + * To run specific tests, e.g. Spikes ones, use: + * make check TESTS='Spikes' + * To build a ROM (pokemerald-test.elf) that can be opened in mgba to + * view specific tests, e.g. Spikes ones, use: + * make pokeemerald-test.elf TESTS='Spikes' + * + * Manually testing a battle mechanic often follows this pattern: + * 1. Create a party which can activate the mechanic. + * 2. Start a battle and play a few turns which activate the mechanic. + * 3. Look at the UI outputs to decide if the mechanic works. + * + * Automated testing follows the same pattern: + * 1. Initialize the party in GIVEN. + * 2. Play the turns in WHEN. + * 3. Check the UI outputs in SCENE. + * + * As a concrete example, to manually test EFFECT_PARALYZE, e.g. the + * effect of Stun Spore you might: + * 1. Put a Wobbuffet that knows Stun Spore in your party. + * 2. Battle a wild Wobbuffet. + * 3. Use Stun Spore. + * 4. Check that the Wobbuffet is paralyzed. + * + * This can be translated to an automated test as follows: + * + * ASSUMPTIONS + * { + * ASSUME(gBattleMoves[MOVE_STUN_SPORE].effect == EFFECT_PARALYZE); + * } + * + * SINGLE_BATTLE_TEST("Stun Spore inflicts paralysis") + * { + * GIVEN { + * PLAYER(SPECIES_WOBBUFFET); // 1. + * OPPONENT(SPECIES_WOBBUFFET); // 2. + * } WHEN { + * TURN { MOVE(player, MOVE_STUN_SPORE); } // 3. + * } SCENE { + * ANIMATION(ANIM_TYPE_MOVE, MOVE_STUN_SPORE, player); + * MESSAGE("Foe Wobbuffet is paralyzed! It may be unable to move!"); // 4 + * STATUS_ICON(opponent, paralysis: TRUE); // 4. + * } + * } + * + * The ASSUMPTIONS block documents that Stun Spore has EFFECT_PARALYZE. + * If Stun Spore did not have that effect it would cause the tests in + * the file to be skipped. We write our tests like this so that hackers + * can change the effects of moves without causing tests to fail. + * + * SINGLE_BATTLE_TEST defines the name of the test. Related tests should + * start with the same prefix, e.g. Stun Spore tests should start with + * "Stun Spore", this allows just the Stun Spore-related tests to be run + * with: + * make check TESTS='Stun Spore' + * + * GIVEN initializes the parties, PLAYER and OPPONENT add a Pokémon to + * their respective parties. They can both accept a block which further + * customizes the Pokémon's stats, moves, item, ability, etc. + * + * WHEN describes the turns, and TURN describes the choices made in a + * single turn. MOVE causes the player to use Stun Spore and adds the + * move to the Pokémon's moveset if an explicit Moves was not specified. + * Pokémon that are not mentioned in a TURN use Celebrate. + * The test runner attempts to rig the RNG so that the first move used + * in a turn does not miss and activates its secondary effects (if any). + * + * SCENE describes the player-visible output of the battle. In this case + * ANIMATION checks that the Stun Spore animation played, MESSAGE checks + * the paralysis message was shown, and STATUS_ICON checks that the + * opponent's HP bar shows a PRZ icon. + * + * As a second example, to manually test that Stun Spore does not effect + * Grass-types you might: + * 1. Put a Wobbuffet that knows Stun Spore in your party. + * 2. Battle a wild Oddish. + * 3. Use Stun Spore. + * 4. Check that the move animation does not play. + * 5. Check that a "It doesn't affect Foe Oddish…" message is shown. + * + * This can again be translated as follows: + * + * SINGLE_BATTLE_TEST("Stun Spore does not affect Grass-types") + * { + * GIVEN { + * ASSUME(gBattleMoves[MOVE_STUN_SPORE].flags & FLAG_POWDER); + * ASSUME(gSpeciesInfo[SPECIES_ODDISH].types[0] == TYPE_GRASS); + * PLAYER(SPECIES_ODDISH); // 1. + * OPPONENT(SPECIES_ODDISH); // 2. + * } WHEN { + * TURN { MOVE(player, MOVE_STUN_SPORE); } // 3. + * } SCENE { + * NOT ANIMATION(ANIM_TYPE_MOVE, MOVE_STUN_SPORE, player); // 4. + * MESSAGE("It doesn't affect Foe Oddish…"); // 5. + * } + * } + * + * The ASSUMEs are documenting the reasons why Stun Spore does not + * affect Oddish, namely that Stun Spore is a powder move, and Oddish + * is a Grass-type. These ASSUMEs function similarly to the ones in + * ASSUMPTIONS but apply only to the one test. + * + * NOT inverts the meaning of a SCENE check, so applying it to ANIMATION + * requires that the Stun Spore animation does not play. MESSAGE checks + * that the message was shown. The checks in SCENE are ordered, so + * together this says "The doesn't affect message is shown, and the Stun + * Spore animation does not play at any time before that". Normally you + * would only test one or the other, or even better, just + * NOT STATUS_ICON(opponent, paralysis: TRUE); to say that Oddish was + * not paralyzed without specifying the exact outputs which led to that. + * + * As a final example, to test that Howl works you might: + * 1. Put a Wobbuffet that knows Howl and Tackle in your party. + * 2. Battle a wild Wobbuffet. + * 3. Use Tackle and note the amount the HP bar reduced. + * 4. Battle a wild Wobbuffet. + * 5. Use Howl and that that the stat change animation and message play. + * 6. Use Tackle and check that the HP bar reduced by more than in 3. + * + * This can be translated to an automated test as follows: + * + * SINGLE_BATTLE_TEST("Howl raises Attack", s16 damage) + * { + * bool32 raiseAttack; + * PARAMETRIZE { raiseAttack = FALSE; } + * PARAMETRIZE { raiseAttack = TRUE; } + * GIVEN { + * ASSUME(gBattleMoves[MOVE_TACKLE].split == SPLIT_PHYSICAL); + * PLAYER(SPECIES_WOBBUFFET); + * OPPONENT(SPECIES_WOBBUFFET); + * } WHEN { + * if (raiseAttack) TURN { MOVE(player, MOVE_HOWL); } // 5. + * TURN { MOVE(player, MOVE_TACKLE); } // 3 & 6. + * } SCENE { + * if (raiseAttack) { + * ANIMATION(ANIM_TYPE_MOVE, MOVE_HOWL, player); + * ANIMATION(ANIM_TYPE_GENERAL, B_ANIM_STATS_CHANGE, player); // 5. + * MESSAGE("Wobbuffet's attack rose!"); // 5. + * } + * ANIMATION(ANIM_TYPE_MOVE, MOVE_TACKLE, player); + * HP_BAR(opponent, captureDamage: &results[i].damage); // 3 & 6. + * } FINALLY { + * EXPECT_MUL_EQ(results[0].damage, Q_4_12(1.5), results[1].damage); // 6. + * } + * } + * + * PARAMETRIZE causes a test to run multiple times, once per PARAMETRIZE + * block (e.g. once with raiseAttack = FALSE and once with raiseAttack = + * TRUE). + * HP_BAR's captureDamage causes the change in HP to be stored in a + * variable, and the variable chosen is results[i].damage. results[i] + * contains all the variables defined at the end of SINGLE_BATTLE_TEST, + * i is the current PARAMETRIZE index. + * FINALLY runs after the last parameter has finished, and uses + * EXPECT_MUL_EQ to check that the second battle deals 1.5× the damage + * of the first battle (with a small tolerance to account for rounding). + * + * You might notice that all the tests check the outputs the player + * could see rather than the internal battle state. e.g. the Howl test + * could have used gBattleMons[B_POSITION_OPPONENT_LEFT].hp instead of + * using HP_BAR to capture the damage. This is a deliberate choice, by + * checking what the player can observe the tests are more robust to + * refactoring, e.g. if gBattleMons got moved into gBattleStruct then + * any test that used it would need to be updated. + * + * REFERENCE + * ========= + * + * ASSUME(cond) + * Causes the test to be skipped if cond is false. Used to document any + * prerequisites of the test, e.g. to test Burn reducing the Attack of a + * Pokémon we can observe the damage of a physical attack with and + * without the burn. To document that this test assumes the attack is + * physical we can use: + * ASSUME(gBattleMoves[MOVE_WHATEVER].split == SPLIT_PHYSICAL); + * + * ASSUMPTIONS + * Should be placed immediately after any #includes and contain any + * ASSUMEs which should apply to the whole file, e.g. to test + * EFFECT_POISON_HIT we need to choose a move with that effect, if + * we chose to use Poison Sting in every test then the top of + * move_effect_poison_hit.c should be: + * ASSUMPTIONS + * { + * ASSUME(gBattleMoves[MOVE_POISON_STING].effect == EFFECT_POISON_HIT); + * } + * + * SINGLE_BATTLE_TEST(name, results...) and DOUBLE_BATTLE_TEST(name, results...) + * Define single- and double- battles. The names should start with the + * name of the mechanic being tested so that it is easier to run all the + * related tests. results contains variable declarations to be placed + * into the results array which is available in PARAMETRIZEd tests. + * The main differences for doubles are: + * - Move targets sometimes need to be explicit. + * - Instead of player and opponent there is playerLeft, playerRight, + * opponentLeft, and opponentRight. + * + * KNOWN_FAILING + * Marks a test as not passing due to a bug. If there is an issue number + * associated with the bug it should be included in a comment. If the + * test passes the developer will be notified to remove KNOWN_FAILING. + * For example: + * SINGLE_BATTLE_TEST("Jump Kick has no recoil if no target") + * { + * KNOWN_FAILING; // #2596. + * + * PARAMETRIZE + * Runs a test multiple times. i will be set to which parameter is + * running, and results will contain an entry for each parameter, e.g.: + * SINGLE_BATTLE_TEST("Blaze boosts Fire-type moves in a pinch", s16 damage) + * { + * u16 hp; + * PARAMETRIZE { hp = 99; } + * PARAMETRIZE { hp = 33; } + * GIVEN { + * ASSUME(gBattleMoves[MOVE_EMBER].type == TYPE_FIRE); + * PLAYER(SPECIES_CHARMANDER) { Ability(ABILITY_BLAZE); MaxHP(99); HP(hp); } + * OPPONENT(SPECIES_WOBBUFFET); + * } WHEN { + * TURN { MOVE(player, MOVE_EMBER); } + * } SCENE { + * HP_BAR(opponent, captureDamage: &results[i].damage); + * } FINALLY { + * EXPECT(results[1].damage > results[0].damage); + * } + * } + * + * PASSES_RANDOMLY(successes, trials) + * Checks that the test passes approximately successes/trials. Used for + * testing RNG-based attacks, e.g.: + * PASSES_RANDOMLY(gBattleMoves[move].accuracy, 100); + * Note that PASSES_RANDOMLY makes the tests run very slowly and should + * be avoided where possible. + * + * GIVEN + * Contains the initial state of the parties before the battle. + * + * RNGSeed(seed) + * Explicitly sets the RNG seed. Try to avoid using this because it is a + * very fragile tool. + * Example: + * GIVEN { + * RNGSeed(0xC0DEIDEA); + * + * PLAYER(species) and OPPONENT(species) + * Adds the species to the player's or opponent's party respectively. + * The Pokémon can be further customized with the following functions: + * - Gender(MON_MALE | MON_FEMALE) + * - Nature(nature) + * - Ability(ability) + * - Level(level) + * - MaxHP(n), HP(n), Attack(n), Defense(n), SpAttack(n), SpDefense(n) + * - Speed(n) + * - Item(item) + * - Moves(moves...) + * - Friendship(friendship) + * - Status1(status1) + * For example to create a Wobbuffet that is poisoned: + * PLAYER(SPECIES_WOBBUFFET) { Status1(STATUS1_POISON); } + * Note if Speed is specified for any Pokémon then it must be specified + * for all Pokémon. + * Note if Moves is specified then MOVE will not automatically add moves + * to the moveset. + * + * WHEN + * Contains the choices that battlers make during the battle. + * + * TURN + * Groups the choices made by the battlers on a single turn. If Speeds + * have not been explicitly specified then the order of the MOVEs in the + * TURN will be used to infer the Speeds of the Pokémon, e.g.: + * // player's speed will be greater than opponent's speed. + * TURN { MOVE(player, MOVE_SPLASH); MOVE(opponent, MOVE_SPLASH); } + * // opponent's speed will be greater than player's speed. + * TURN { MOVE(opponent, MOVE_SPLASH); MOVE(player, MOVE_SPLASH); } + * The inference process is naive, if your test contains anything that + * modifies the speed of a battler you should specify them explicitly. + * + * MOVE(battler, move | moveSlot:, [megaEvolve:], [hit:], [criticalHit:], [target:], [allowed:]) + * Used when the battler chooses Fight. Either the move ID or move slot + * must be specified. megaEvolve: TRUE causes the battler to Mega Evolve + * if able, hit: FALSE causes the move to miss, criticalHit: TRUE causes + * the move to land a critical hit, target: is used in double battles to + * choose the target (when necessary), and allowed: FALSE is used to + * reject an illegal move e.g. a Disabled one. + * MOVE(playerLeft, MOVE_TACKLE, target: opponentRight); + * If the battler does not have an explicit Moves specified the moveset + * will be populated based on the MOVEs it uses. + * + * FORCED_MOVE(battler) + * Used when the battler chooses Fight and then their move is chosen for + * them, e.g. when affected by Encore. + * FORCED_MOVE(player); + * + * SWITCH(battler, partyIndex) + * Used when the battler chooses Switch. + * SWITCH(player, 1); + * + * SKIP_TURN(battler) + * Used when the battler cannot choose an action, e.g. when locked into + * Thrash. + * SKIP_TURN(player); + * + * SEND_OUT(battler, partyIndex) + * Used when the battler chooses to switch to another Pokémon but not + * via Switch, e.g. after fainting or due to a U-turn. + * SEND_OUT(player, 1); + * + * SCENE + * Contains an abridged description of the UI during the THEN. The order + * of the description must match too, e.g. + * // ABILITY_POPUP followed by a MESSAGE + * ABILITY_POPUP(player, ABILITY_STURDY); + * MESSAGE("Geodude was protected by Sturdy!"); + * + * ABILITY_POPUP(battler, [ability]) + * Causes the test to fail if the battler's ability pop-up is not shown. + * If specified, ability is the ability shown in the pop-up. + * ABILITY_POPUP(opponent, ABILITY_MOLD_BREAKER); + * + * ANIMATION(type, animId, [battler], [target:]) + * Causes the test to fail if the animation does not play. A common use + * of this command is to check if a move was successful, e.g.: + * ANIMATION(ANIM_TYPE_MOVE, MOVE_TACKLE, player); + * target can only be specified for ANIM_TYPE_MOVE. + * + * HP_BAR(battler, [damage: | hp: | captureDamage: | captureHP:]) + * If hp: or damage: are used, causes the test to fail if that amount of + * damage is not dealt, e.g.: + * HP_BAR(player, hp: 0); + * If captureDamage: or captureHP: are used, causes the test to fail if + * the HP bar does not change, and then writes that change to the + * pointer, e.g.: + * s16 damage; + * HP_BAR(player, captureDamage: &damage); + * If none of the above are used, causes the test to fail if the HP + * changes at all. + * + * MESSAGE(pattern) + * Causes the test to fail if the message in pattern is not displayed. + * Spaces in pattern match newlines (\n, \l, and \p) in the message. + * Often used to check that a battler took its turn but it failed, e.g.: + * MESSAGE("Wobbuffet used Dream Eater!"); + * MESSAGE("Foe Wobbuffet wasn't affected!"); + * + * STATUS_ICON(battler, status1 | none: | sleep: | poison: | burn: | freeze: | paralysis:, badPoison:) + * Causes the test to fail if the battler's status is not changed to the + * specified status. + * STATUS_ICON(player, badPoison: TRUE); + * If the expected status icon is parametrized the corresponding STATUS1 + * constant can be provided, e.g.: + * u32 status1; + * PARAMETRIZE { status1 = STATUS1_NONE; } + * PARAMETRIZE { status1 = STATUS1_BURN; } + * ... + * STATUS_ICON(player, status1); + * + * NOT + * Causes the test to fail if the SCENE command succeeds before the + * following command succeeds. + * // Our Wobbuffet does not Celebrate before the foe's. + * NOT MESSAGE("Wobbuffet used Celebrate!"); + * MESSAGE("Foe Wobbuffet used Celebrate!"); + * WARNING: NOT is an alias of NONE_OF, so it behaves surprisingly when + * applied to multiple commands wrapped in braces. + * + * ONE_OF + * Causes the test to fail unless one of the SCENE commands succeeds. + * ONE_OF { + * MESSAGE("Wobbuffet used Celebrate!"); + * MESSAGE("Wobbuffet is paralyzed! It can't move!"); + * } + * + * NONE_OF + * Causes the test to fail if one of the SCENE commands succeeds before + * the command after the NONE_OF succeeds. + * // Our Wobbuffet does not move before the foe's. + * NONE_OF { + * MESSAGE("Wobbuffet used Celebrate!"); + * MESSAGE("Wobbuffet is paralyzed! It can't move!"); + * } + * MESSAGE("Foe Wobbuffet used Celebrate!"); + * + * PLAYER_PARTY and OPPONENT_PARTY + * Refer to the party members defined in GIVEN, e.g.: + * s32 maxHP = GetMonData(&OPPONENT_PARTY[0], MON_DATA_MAX_HP); + * HP_BAR(opponent, damage: maxHP / 2); + * + * THEN + * Contains code to run after the battle has finished. If the test is + * PARAMETRIZEd then EXPECTs between the results should go here. Is also + * occasionally used to check the internal battle state when checking + * the behavior via a SCENE is too difficult, verbose, or error-prone. + * + * FINALLY + * Contains checks to run after all PARAMETERIZEs have run. Prefer to + * write your checks in THEN where possible, because a failure in THEN + * will be tagged with which parameter it corresponds to. + * + * EXPECT(cond) + * Causes the test to fail if cond is false. + * + * EXPECT_EQ(a, b), EXPECT_NE(a, b), EXPECT_LT(a, b), EXPECT_LE(a, b), EXPECT_GT(a, b), EXPECT_GE(a, b) + * Causes the test to fail if a and b compare incorrectly, e.g. + * EXPECT_EQ(results[0].damage, results[1].damage); + * + * EXPECT_MUL_EQ(a, m, b) + * Causes the test to fail if a*m != b (within a threshold), e.g. + * // Expect results[0].damage * 1.5 == results[1].damage. + * EXPECT_EQ(results[0].damage, Q_4_12(1.5), results[1].damage); */ + +#ifndef GUARD_TEST_BATTLE_H +#define GUARD_TEST_BATTLE_H + +#include "battle.h" +#include "battle_anim.h" +#include "data.h" +#include "item.h" +#include "recorded_battle.h" +#include "test.h" +#include "util.h" +#include "constants/abilities.h" +#include "constants/battle_anim.h" +#include "constants/battle_move_effects.h" +#include "constants/hold_effects.h" +#include "constants/items.h" +#include "constants/moves.h" +#include "constants/species.h" + +// NOTE: If the stack is too small the test runner will probably crash +// or loop. +#define BATTLE_TEST_STACK_SIZE 1024 +#define MAX_QUEUED_EVENTS 16 + +enum { BATTLE_TEST_SINGLES, BATTLE_TEST_DOUBLES }; + +typedef void (*SingleBattleTestFunction)(void *, u32, struct BattlePokemon *, struct BattlePokemon *); +typedef void (*DoubleBattleTestFunction)(void *, u32, struct BattlePokemon *, struct BattlePokemon *, struct BattlePokemon *, struct BattlePokemon *); + +struct BattleTest +{ + u8 type; + u16 sourceLine; + union + { + SingleBattleTestFunction singles; + DoubleBattleTestFunction doubles; + } function; + size_t resultsSize; +}; + +enum +{ + QUEUED_ABILITY_POPUP_EVENT, + QUEUED_ANIMATION_EVENT, + QUEUED_HP_EVENT, + QUEUED_MESSAGE_EVENT, + QUEUED_STATUS_EVENT, +}; + +struct QueuedAbilityEvent +{ + u8 battlerId; + u16 ability; +}; + +struct QueuedAnimationEvent +{ + u8 type; + u16 id; + u8 attacker:4; + u8 target:4; +}; + +enum { HP_EVENT_NEW_HP, HP_EVENT_DELTA_HP }; + +struct QueuedHPEvent +{ + u32 battlerId:3; + u32 type:1; + u32 address:28; +}; + +struct QueuedMessageEvent +{ + const u8 *pattern; +}; + +struct QueuedStatusEvent +{ + u32 battlerId:3; + u32 mask:8; + u32 unused_01:21; +}; + +struct QueuedEvent +{ + u8 type; + u8 sourceLineOffset; + u8 groupType:2; + u8 groupSize:6; + union + { + struct QueuedAbilityEvent ability; + struct QueuedAnimationEvent animation; + struct QueuedHPEvent hp; + struct QueuedMessageEvent message; + struct QueuedStatusEvent status; + } as; +}; + +struct BattleTestData +{ + u8 stack[BATTLE_TEST_STACK_SIZE]; + + u8 playerPartySize; + u8 opponentPartySize; + u8 explicitMoves[NUM_BATTLE_SIDES]; + bool8 hasExplicitSpeeds; + u8 explicitSpeeds[NUM_BATTLE_SIDES]; + u16 slowerThan[NUM_BATTLE_SIDES][PARTY_SIZE]; + u8 currentSide; + u8 currentPartyIndex; + struct Pokemon *currentMon; + u8 gender; + u8 nature; + + u8 currentMonIndexes[MAX_BATTLERS_COUNT]; + u8 turnState; + u8 turns; + u8 actionBattlers; + u8 moveBattlers; + bool8 hasRNGActions:1; + + struct RecordedBattleSave recordedBattle; + u8 battleRecordTypes[MAX_BATTLERS_COUNT][BATTLER_RECORD_SIZE]; + u8 battleRecordSourceLineOffsets[MAX_BATTLERS_COUNT][BATTLER_RECORD_SIZE]; + u16 recordIndexes[MAX_BATTLERS_COUNT]; + u8 lastActionTurn; + u8 nextRNGTurn; + + u8 queuedEventsCount; + u8 queueGroupType; + u8 queueGroupStart; + u8 queuedEvent; + struct QueuedEvent queuedEvents[MAX_QUEUED_EVENTS]; +}; + +struct BattleTestRunnerState +{ + u8 battlersCount; + u8 parametersCount; // Valid only in BattleTest_Setup. + u8 parameters; + u8 runParameter; + u8 trials; + u8 expectedPasses; + u8 observedPasses; + u8 skippedTrials; + u8 runTrial; + bool8 runRandomly:1; + bool8 runGiven:1; + bool8 runWhen:1; + bool8 runScene:1; + bool8 runThen:1; + bool8 runFinally:1; + bool8 runningFinally:1; + struct BattleTestData data; + u8 *results; + u8 checkProgressParameter; + u8 checkProgressTrial; + u8 checkProgressTurn; +}; + +extern const struct TestRunner gBattleTestRunner; +extern struct BattleTestRunnerState *gBattleTestRunnerState; + +#define MEMBERS(...) VARARG_8(MEMBERS_, __VA_ARGS__) +#define MEMBERS_0() +#define MEMBERS_1(a) a; +#define MEMBERS_2(a, b) a; b; +#define MEMBERS_3(a, b, c) a; b; c; +#define MEMBERS_4(a, b, c, d) a; b; c; d; +#define MEMBERS_5(a, b, c, d, e) a; b; c; d; e; +#define MEMBERS_6(a, b, c, d, e, f) a; b; c; d; e; f; +#define MEMBERS_7(a, b, c, d, e, f, g) a; b; c; d; e; f; g; +#define MEMBERS_8(a, b, c, d, e, f, g, h) a; b; c; d; e; f; g; h; + +#define APPEND_TRUE(...) VARARG_8(APPEND_TRUE_, __VA_ARGS__) +#define APPEND_TRUE_0() +#define APPEND_TRUE_1(a) a, TRUE +#define APPEND_TRUE_2(a, b) a, TRUE, b, TRUE +#define APPEND_TRUE_3(a, b, c) a, TRUE, b, TRUE, c, TRUE +#define APPEND_TRUE_4(a, b, c, d) a, TRUE, b, TRUE, c, TRUE, d, TRUE +#define APPEND_TRUE_5(a, b, c, d, e) a, TRUE, b, TRUE, c, TRUE, d, TRUE, e, TRUE +#define APPEND_TRUE_6(a, b, c, d, e, f) a, TRUE, b, TRUE, c, TRUE, d, TRUE, e, TRUE, f, TRUE +#define APPEND_TRUE_7(a, b, c, d, e, f, g) a, TRUE, b, TRUE, c, TRUE, d, TRUE, e, TRUE, f, TRUE, g, TRUE +#define APPEND_TRUE_8(a, b, c, d, e, f, g, h) a, TRUE, b, TRUE, c, TRUE, d, TRUE, e, TRUE, f, TRUE, g, TRUE, h, TRUE + +/* Test */ + +#define SINGLE_BATTLE_TEST(_name, ...) \ + struct CAT(Result, __LINE__) { MEMBERS(__VA_ARGS__) }; \ + static void CAT(Test, __LINE__)(struct CAT(Result, __LINE__) *, u32, struct BattlePokemon *, struct BattlePokemon *); \ + __attribute__((section(".tests"))) static const struct Test CAT(sTest, __LINE__) = \ + { \ + .name = _name, \ + .filename = __FILE__, \ + .runner = &gBattleTestRunner, \ + .data = (void *)&(const struct BattleTest) \ + { \ + .type = BATTLE_TEST_SINGLES, \ + .sourceLine = __LINE__, \ + .function = { .singles = (SingleBattleTestFunction)CAT(Test, __LINE__) }, \ + .resultsSize = sizeof(struct CAT(Result, __LINE__)), \ + }, \ + }; \ + static void CAT(Test, __LINE__)(struct CAT(Result, __LINE__) *results, u32 i, struct BattlePokemon *player, struct BattlePokemon *opponent) + +#define DOUBLE_BATTLE_TEST(_name, ...) \ + struct CAT(Result, __LINE__) { MEMBERS(__VA_ARGS__) }; \ + static void CAT(Test, __LINE__)(struct CAT(Result, __LINE__) *, u32, struct BattlePokemon *, struct BattlePokemon *, struct BattlePokemon *, struct BattlePokemon *); \ + __attribute__((section(".tests"))) static const struct Test CAT(sTest, __LINE__) = \ + { \ + .name = _name, \ + .filename = __FILE__, \ + .runner = &gBattleTestRunner, \ + .data = (void *)&(const struct BattleTest) \ + { \ + .type = BATTLE_TEST_DOUBLES, \ + .sourceLine = __LINE__, \ + .function = { .doubles = (DoubleBattleTestFunction)CAT(Test, __LINE__) }, \ + .resultsSize = sizeof(struct CAT(Result, __LINE__)), \ + }, \ + }; \ + static void CAT(Test, __LINE__)(struct CAT(Result, __LINE__) *results, u32 i, struct BattlePokemon *playerLeft, struct BattlePokemon *opponentLeft, struct BattlePokemon *playerRight, struct BattlePokemon *opponentRight) + +/* Parametrize */ + +#define PARAMETRIZE if (gBattleTestRunnerState->parametersCount++ == i) + +/* Randomly */ + +#define PASSES_RANDOMLY(passes, trials) for (; gBattleTestRunnerState->runRandomly; gBattleTestRunnerState->runRandomly = FALSE) Randomly(__LINE__, passes, trials) + +void Randomly(u32 sourceLine, u32 passes, u32 trials); + +/* Given */ + +#define GIVEN for (; gBattleTestRunnerState->runGiven; gBattleTestRunnerState->runGiven = FALSE) + +#define RNGSeed(seed) RNGSeed_(__LINE__, seed) + +#define PLAYER(species) for (OpenPokemon(__LINE__, B_SIDE_PLAYER, species); gBattleTestRunnerState->data.currentMon; ClosePokemon(__LINE__)) +#define OPPONENT(species) for (OpenPokemon(__LINE__, B_SIDE_OPPONENT, species); gBattleTestRunnerState->data.currentMon; ClosePokemon(__LINE__)) + +#define Gender(gender) Gender_(__LINE__, gender) +#define Nature(nature) Nature_(__LINE__, nature) +#define Ability(ability) Ability_(__LINE__, ability) +#define Level(level) Level_(__LINE__, level) +#define MaxHP(maxHP) MaxHP_(__LINE__, maxHP) +#define HP(hp) HP_(__LINE__, hp) +#define Attack(attack) Attack_(__LINE__, attack) +#define Defense(defense) Defense_(__LINE__, defense) +#define SpAttack(spAttack) SpAttack_(__LINE__, spAttack) +#define SpDefense(spDefense) SpDefense_(__LINE__, spDefense) +#define Speed(speed) Speed_(__LINE__, speed) +#define Item(item) Item_(__LINE__, item) +#define Moves(move1, ...) Moves_(__LINE__, (const u16 [MAX_MON_MOVES]) { move1, __VA_ARGS__ }) +#define Friendship(friendship) Friendship_(__LINE__, friendship) +#define Status1(status1) Status1_(__LINE__, status1) + +void OpenPokemon(u32 sourceLine, u32 side, u32 species); +void ClosePokemon(u32 sourceLine); + +void RNGSeed_(u32 sourceLine, u32 seed); +void Gender_(u32 sourceLine, u32 gender); +void Nature_(u32 sourceLine, u32 nature); +void Ability_(u32 sourceLine, u32 ability); +void Level_(u32 sourceLine, u32 level); +void MaxHP_(u32 sourceLine, u32 maxHP); +void HP_(u32 sourceLine, u32 hp); +void Attack_(u32 sourceLine, u32 attack); +void Defense_(u32 sourceLine, u32 defense); +void SpAttack_(u32 sourceLine, u32 spAttack); +void SpDefense_(u32 sourceLine, u32 spDefense); +void Speed_(u32 sourceLine, u32 speed); +void Item_(u32 sourceLine, u32 item); +void Moves_(u32 sourceLine, const u16 moves[MAX_MON_MOVES]); +void Friendship_(u32 sourceLine, u32 friendship); +void Status1_(u32 sourceLine, u32 status1); + +#define PLAYER_PARTY (gBattleTestRunnerState->data.recordedBattle.playerParty) +#define OPPONENT_PARTY (gBattleTestRunnerState->data.recordedBattle.opponentParty) + +/* When */ + +#define WHEN for (; gBattleTestRunnerState->runWhen; gBattleTestRunnerState->runWhen = FALSE) + +enum { TURN_CLOSED, TURN_OPEN, TURN_CLOSING }; + +#define TURN for (OpenTurn(__LINE__); gBattleTestRunnerState->data.turnState == TURN_OPEN; CloseTurn(__LINE__)) + +#define MOVE(battler, ...) Move(__LINE__, battler, (struct MoveContext) { APPEND_TRUE(__VA_ARGS__) }) +#define FORCED_MOVE(battler) ForcedMove(__LINE__, battler) +#define SWITCH(battler, partyIndex) Switch(__LINE__, battler, partyIndex) +#define SKIP_TURN(battler) SkipTurn(__LINE__, battler) +#define SEND_OUT(battler, partyIndex) SendOut(__LINE__, battler, partyIndex) + +struct MoveContext +{ + u16 move; + u16 explicitMove:1; + u16 moveSlot:2; + u16 explicitMoveSlot:1; + u16 hit:1; + u16 explicitHit:1; + u16 criticalHit:1; + u16 explicitCriticalHit:1; + u16 megaEvolve:1; + u16 explicitMegaEvolve:1; + // TODO: u8 zMove:1; + u16 allowed:1; + u16 explicitAllowed:1; + struct BattlePokemon *target; + bool8 explicitTarget; +}; + +void OpenTurn(u32 sourceLine); +void CloseTurn(u32 sourceLine); +void Move(u32 sourceLine, struct BattlePokemon *, struct MoveContext); +void ForcedMove(u32 sourceLine, struct BattlePokemon *); +void Switch(u32 sourceLine, struct BattlePokemon *, u32 partyIndex); +void SkipTurn(u32 sourceLine, struct BattlePokemon *); + +void SendOut(u32 sourceLine, struct BattlePokemon *, u32 partyIndex); + +/* Scene */ + +#define SCENE for (; gBattleTestRunnerState->runScene; gBattleTestRunnerState->runScene = FALSE) + +#define ONE_OF for (OpenQueueGroup(__LINE__, QUEUE_GROUP_ONE_OF); gBattleTestRunnerState->data.queueGroupType != QUEUE_GROUP_NONE; CloseQueueGroup(__LINE__)) +#define NONE_OF for (OpenQueueGroup(__LINE__, QUEUE_GROUP_NONE_OF); gBattleTestRunnerState->data.queueGroupType != QUEUE_GROUP_NONE; CloseQueueGroup(__LINE__)) +#define NOT NONE_OF + +#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) { APPEND_TRUE(__VA_ARGS__) }) +#define MESSAGE(pattern) QueueMessage(__LINE__, (const u8 []) _(pattern)) +#define STATUS_ICON(battler, status) QueueStatus(__LINE__, battler, (struct StatusEventContext) { status }) + +enum QueueGroupType +{ + QUEUE_GROUP_NONE, + QUEUE_GROUP_ONE_OF, + QUEUE_GROUP_NONE_OF, +}; + +struct AbilityEventContext +{ + u16 ability; +}; + +struct AnimationEventContext +{ + struct BattlePokemon *attacker; + struct BattlePokemon *target; +}; + +struct HPEventContext +{ + u8 _; + u16 hp; + bool8 explicitHP; + s16 damage; + bool8 explicitDamage; + u16 *captureHP; + bool8 explicitCaptureHP; + s16 *captureDamage; + bool8 explicitCaptureDamage; +}; + +struct StatusEventContext +{ + u8 status1; + bool8 none:1; + bool8 sleep:1; + bool8 poison:1; + bool8 burn:1; + bool8 freeze:1; + bool8 paralysis:1; + bool8 badPoison:1; +}; + +void OpenQueueGroup(u32 sourceLine, enum QueueGroupType); +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 QueueMessage(u32 sourceLine, const u8 *pattern); +void QueueStatus(u32 sourceLine, struct BattlePokemon *battler, struct StatusEventContext); + +/* Then */ + +#define THEN for (; gBattleTestRunnerState->runThen; gBattleTestRunnerState->runThen = FALSE) + +/* Finally */ + +#define FINALLY for (; gBattleTestRunnerState->runFinally; gBattleTestRunnerState->runFinally = FALSE) if ((gBattleTestRunnerState->runningFinally = TRUE)) + +/* Expect */ + +#define EXPECT_MUL_EQ(a, m, b) \ + do \ + { \ + s32 _a = (a), _m = (m), _b = (b); \ + s32 _am = Q_4_12_TO_INT(_a * _m); \ + s32 _t = Q_4_12_TO_INT(abs(_m) + Q_4_12_ROUND); \ + if (abs(_am-_b) > _t) \ + Test_ExitWithResult(TEST_RESULT_FAIL, "%s:%d: EXPECT_MUL_EQ(%d, %q, %d) failed: %d not in [%d..%d]", gTestRunnerState.test->filename, __LINE__, _a, _m, _b, _am, _b-_t, _b+_t); \ + } while (0) + +#endif diff --git a/test/test_runner.c b/test/test_runner.c new file mode 100644 index 000000000..a10a3a89d --- /dev/null +++ b/test/test_runner.c @@ -0,0 +1,439 @@ +#include +#include "global.h" +#include "characters.h" +#include "gpu_regs.h" +#include "main.h" +#include "malloc.h" +#include "test.h" +#include "test_runner.h" + +#define TIMEOUT_SECONDS 30 + +void CB2_TestRunner(void); + +EWRAM_DATA struct TestRunnerState gTestRunnerState; + +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); + +extern const struct Test __start_tests[]; +extern const struct Test __stop_tests[]; + +static bool32 PrefixMatch(const char *pattern, const char *string) +{ + if (string == NULL) + return TRUE; + + while (TRUE) + { + if (!*pattern) + return TRUE; + if (*pattern != *string) + return FALSE; + pattern++; + string++; + } +} + +enum { STATE_INIT, STATE_NEXT_TEST, STATE_REPORT_RESULT, STATE_EXIT }; + +void CB2_TestRunner(void) +{ + switch (gTestRunnerState.state) + { + case STATE_INIT: + if (!MgbaOpen_()) + { + gTestRunnerState.state = STATE_EXIT; + gTestRunnerState.exitCode = 2; + return; + } + + gIntrTable[7] = Intr_Timer2; + + gTestRunnerState.state = STATE_NEXT_TEST; + gTestRunnerState.exitCode = 0; + gTestRunnerState.tests = 0; + gTestRunnerState.passes = 0; + gTestRunnerState.skips = 0; + gTestRunnerState.skipFilename = NULL; + gTestRunnerState.test = __start_tests - 1; + break; + + case STATE_NEXT_TEST: + gTestRunnerState.test++; + + if (gTestRunnerState.test == __stop_tests) + { + MgbaPrintf_("%s%d/%d PASSED\e[0m", gTestRunnerState.exitCode == 0 ? "\e[32m" : "\e[31m", gTestRunnerState.passes, gTestRunnerState.tests); + if (gTestRunnerState.skips) + { + if (gTestRunnerSkipIsFail) + MgbaPrintf_("\e[31m%d SKIPPED\e[0m", gTestRunnerState.skips); + else + MgbaPrintf_("%d SKIPPED", gTestRunnerState.skips); + } + gTestRunnerState.state = STATE_EXIT; + return; + } + + if (!PrefixMatch(gTestRunnerArgv, gTestRunnerState.test->name)) + return; + + // Greedily assign tests to processes based on estimated cost. + // TODO: Make processCosts a min heap. + if (gTestRunnerState.test->runner != &gAssumptionsRunner) + { + u32 i; + u32 minCost, minCostProcess; + minCost = gTestRunnerState.processCosts[0]; + minCostProcess = 0; + for (i = 1; i < gTestRunnerN; i++) + { + if (gTestRunnerState.processCosts[i] < minCost) + { + minCost = gTestRunnerState.processCosts[i]; + minCostProcess = i; + } + } + + if (gTestRunnerState.test->runner->estimateCost) + gTestRunnerState.processCosts[minCostProcess] += gTestRunnerState.test->runner->estimateCost(gTestRunnerState.test->data); + else + gTestRunnerState.processCosts[minCostProcess] += 1; + + if (minCostProcess != gTestRunnerI) + return; + } + + gTestRunnerState.state = STATE_REPORT_RESULT; + gTestRunnerState.result = TEST_RESULT_PASS; + gTestRunnerState.expectedResult = TEST_RESULT_PASS; + if (gTestRunnerHeadless) + gTestRunnerState.timeoutSeconds = TIMEOUT_SECONDS; + else + gTestRunnerState.timeoutSeconds = UINT_MAX; + InitHeap(gHeap, HEAP_SIZE); + EnableInterrupts(INTR_FLAG_TIMER2); + REG_TM2CNT_L = UINT16_MAX - (274 * 60); // Approx. 1 second. + REG_TM2CNT_H = TIMER_ENABLE | TIMER_INTR_ENABLE | TIMER_1024CLK; + + // NOTE: Assumes that the compiler interns __FILE__. + if (gTestRunnerState.skipFilename == gTestRunnerState.test->filename) + { + gTestRunnerState.result = TEST_RESULT_SKIP; + } + else + { + MgbaPrintf_(":N%s", gTestRunnerState.test->name); + if (gTestRunnerState.test->runner->setUp) + gTestRunnerState.test->runner->setUp(gTestRunnerState.test->data); + gTestRunnerState.test->runner->run(gTestRunnerState.test->data); + } + break; + + case STATE_REPORT_RESULT: + REG_TM2CNT_H = 0; + + gTestRunnerState.state = STATE_NEXT_TEST; + + if (gTestRunnerState.test->runner->tearDown) + gTestRunnerState.test->runner->tearDown(gTestRunnerState.test->data); + + if (gTestRunnerState.test->runner == &gAssumptionsRunner) + { + if (gTestRunnerState.result != TEST_RESULT_PASS) + gTestRunnerState.skipFilename = gTestRunnerState.test->filename; + } + else if (gTestRunnerState.result == TEST_RESULT_SKIP) + { + gTestRunnerState.skips++; + if (gTestRunnerSkipIsFail) + gTestRunnerState.exitCode = 1; + } + else + { + const char *color; + const char *result; + + gTestRunnerState.tests++; + + if (gTestRunnerState.result == gTestRunnerState.expectedResult) + { + gTestRunnerState.passes++; + color = "\e[32m"; + MgbaPrintf_(":N%s", gTestRunnerState.test->name); + } + else if (gTestRunnerState.result != TEST_RESULT_SKIP || gTestRunnerSkipIsFail) + { + gTestRunnerState.exitCode = 1; + color = "\e[31m"; + } + else + { + 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: result = "FAIL"; break; + case TEST_RESULT_PASS: result = "PASS"; break; + case TEST_RESULT_SKIP: result = "SKIP"; break; + case TEST_RESULT_INVALID: result = "INVALID"; break; + case TEST_RESULT_ERROR: result = "ERROR"; break; + case TEST_RESULT_TIMEOUT: result = "TIMEOUT"; break; + default: result = "UNKNOWN"; break; + } + + MgbaPrintf_(":R%s%s\e[0m", color, result); + } + + break; + + case STATE_EXIT: + MgbaExit_(gTestRunnerState.exitCode); + break; + } +} + +void Test_ExpectedResult(enum TestResult result) +{ + gTestRunnerState.expectedResult = result; +} + +static void Assumptions_Run(void *data) +{ + void (*function)(void) = data; + function(); +} + +const struct TestRunner gAssumptionsRunner = +{ + .run = Assumptions_Run, +}; + +#define IRQ_LR (*(vu32 *)0x3007F9C) + +/* Returns to AgbMainLoop. + * Similar to a longjmp except that we only restore sp (and cpsr via + * overwriting the value of lr_irq on the stack). + * + * WARNING: This could potentially be flaky because other global state + * will not be cleaned up, we may decide to Exit on a timeout instead. */ +static NAKED void JumpToAgbMainLoop(void) +{ + asm(".arm\n\ + .word 0xe3104778\n\ + ldr r0, =gAgbMainLoop_sp\n\ + ldr sp, [r0]\n\ + ldr r0, =AgbMainLoop\n\ + bx r0\n\ + .pool"); +} + +void ReinitCallbacks(void) +{ + gMain.callback1 = NULL; + SetMainCallback2(CB2_TestRunner); + gMain.vblankCallback = NULL; + gMain.hblankCallback = NULL; +} + +static void Intr_Timer2(void) +{ + if (--gTestRunnerState.timeoutSeconds == 0) + { + if (gTestRunnerState.test->runner->checkProgress + && gTestRunnerState.test->runner->checkProgress(gTestRunnerState.test->data)) + { + gTestRunnerState.timeoutSeconds = TIMEOUT_SECONDS; + } + else + { + gTestRunnerState.result = TEST_RESULT_TIMEOUT; + ReinitCallbacks(); + IRQ_LR = ((uintptr_t)JumpToAgbMainLoop & ~1) + 4; + } + } +} + +void Test_ExitWithResult(enum TestResult result, const char *fmt, ...) +{ + gTestRunnerState.result = result; + ReinitCallbacks(); + if (gTestRunnerState.test->runner->handleExitWithResult + && !gTestRunnerState.test->runner->handleExitWithResult(gTestRunnerState.test->data, result) + && gTestRunnerState.result != gTestRunnerState.expectedResult) + { + va_list va; + va_start(va, fmt); + MgbaVPrintf_(fmt, va); + } + JumpToAgbMainLoop(); +} + +#define REG_DEBUG_ENABLE (*(vu16 *)0x4FFF780) +#define REG_DEBUG_FLAGS (*(vu16 *)0x4FFF700) +#define REG_DEBUG_STRING ((char *)0x4FFF600) + +static bool32 MgbaOpen_(void) +{ + REG_DEBUG_ENABLE = 0xC0DE; + return REG_DEBUG_ENABLE == 0x1DEA; +} + +static void MgbaExit_(u8 exitCode) +{ + register u32 _exitCode asm("r0") = exitCode; + asm("swi 0x3" :: "r" (_exitCode)); +} + +static s32 MgbaPuts_(const char *s) +{ + return MgbaPrintf_("%s", s); +} + +s32 MgbaPrintf_(const char *fmt, ...) +{ + va_list va; + va_start(va, fmt); + return MgbaVPrintf_(fmt, va); +} + +static s32 MgbaPutchar_(s32 i, s32 c) +{ + REG_DEBUG_STRING[i++] = c; + if (i == 255) + { + REG_DEBUG_STRING[i] = '\0'; + REG_DEBUG_FLAGS = MGBA_LOG_INFO | 0x100; + i = 0; + } + return i; +} + +extern const u8 gWireless_RSEtoASCIITable[]; + +// Bare-bones, only supports plain %s, %S, and %d. +static s32 MgbaVPrintf_(const char *fmt, va_list va) +{ + s32 i = 0; + s32 c, d; + const char *s; + while (*fmt) + { + switch ((c = *fmt++)) + { + case '%': + switch (*fmt++) + { + case '%': + i = MgbaPutchar_(i, '%'); + break; + case 'd': + d = va_arg(va, int); + if (d == 0) + { + i = MgbaPutchar_(i, '0'); + } + else + { + char buffer[10]; + s32 n = 0; + u32 u = abs(d); + if (d < 0) + i = MgbaPutchar_(i, '-'); + while (u > 0) + { + buffer[n++] = '0' + (u % 10); + u /= 10; + } + while (n > 0) + i = MgbaPutchar_(i, buffer[--n]); + } + break; + case 'q': + d = va_arg(va, int); + { + char buffer[10]; + s32 n = 0; + u32 u = abs(d) >> 12; + if (u == 0) + { + i = MgbaPutchar_(i, '0'); + } + else + { + if (d < 0) + i = MgbaPutchar_(i, '-'); + while (u > 0) + { + buffer[n++] = '0' + (u % 10); + u /= 10; + } + while (n > 0) + i = MgbaPutchar_(i, buffer[--n]); + } + + n = 0; + i = MgbaPutchar_(i, '.'); + u = d & 0xFFF; + while (TRUE) + { + u *= 10; + i = MgbaPutchar_(i, '0' + (u >> 12)); + u &= 0xFFF; + if (u == 0) + break; + if (++n == 2) + { + u *= 10; + i = MgbaPutchar_(i, '0' + ((u + UQ_4_12_ROUND) >> 12)); + break; + } + } + } + break; + case 's': + s = va_arg(va, const char *); + while ((c = *s++) != '\0') + i = MgbaPutchar_(i, c); + break; + case 'S': + s = va_arg(va, const u8 *); + while ((c = *s++) != EOS) + { + if ((c = gWireless_RSEtoASCIITable[c]) != '\0') + i = MgbaPutchar_(i, c); + else + i = MgbaPutchar_(i, '?'); + } + break; + } + break; + case '\n': + i = 254; + i = MgbaPutchar_(i, '\0'); + break; + default: + i = MgbaPutchar_(i, c); + break; + } + } + if (i != 0) + { + REG_DEBUG_FLAGS = MGBA_LOG_INFO | 0x100; + } + return i; +} diff --git a/test/test_runner_args.c b/test/test_runner_args.c new file mode 100644 index 000000000..9c0bf2ee5 --- /dev/null +++ b/test/test_runner_args.c @@ -0,0 +1,8 @@ +#include "global.h" + +// These values are patched by patchelf. Therefore we have put them in +// their own TU so that the optimizer cannot inline them. +const bool8 gTestRunnerEnabled = TRUE; +const u8 gTestRunnerN = 0; +const u8 gTestRunnerI = 0; +const char gTestRunnerArgv[256] = {'\0'}; diff --git a/test/test_runner_battle.c b/test/test_runner_battle.c new file mode 100644 index 000000000..e40ed3e50 --- /dev/null +++ b/test/test_runner_battle.c @@ -0,0 +1,1545 @@ +#include "global.h" +#include "battle.h" +#include "battle_anim.h" +#include "battle_controllers.h" +#include "characters.h" +#include "main.h" +#include "malloc.h" +#include "random.h" +#include "test_battle.h" +#include "window.h" + +#define INVALID(fmt, ...) Test_ExitWithResult(TEST_RESULT_INVALID, "%s:%d: " fmt, gTestRunnerState.test->filename, sourceLine, ##__VA_ARGS__) +#define INVALID_IF(c, fmt, ...) do { if (c) Test_ExitWithResult(TEST_RESULT_INVALID, "%s:%d: " fmt, gTestRunnerState.test->filename, sourceLine, ##__VA_ARGS__); } while (0) + +#define STATE gBattleTestRunnerState +#define DATA gBattleTestRunnerState->data + +/* RNG seeds for controlling the first move of the turn. + * Found via brute force. */ + +/* Default seed, triggers most things. + * The 1st roll % 100 is <= 29, to make 30%+ accuracycheck pass. + * The 2nd roll is not a critical hit at the regular crit stage. + * The 3rd roll is consumed by damagecalc. + * The 4th roll is consumed by adjustdamage. + * The 5th roll % 100 is <= 9, to make 10%+ seteffectwithchance pass + * and % 3 is == 0, to make Poison Point and other 1/3s pass. */ +#define RNG_SEED_DEFAULT 0x000002BE + +/* Causes the first attack to critical hit if B_CRIT_CHANCE >= GEN_6. + * The 2nd roll % 24 == 0 to be a critical hit at any stage. + * The other rolls match RNG_SEED_DEFAULT. */ +#define RNG_SEED_CRITICAL_HIT 0x0000A9F4 + +/* Causes the first attack to miss if possible. + * The 1st roll % 100 is 99, to make 99%- accuracycheck fail. */ +#define RNG_SEED_MISS 0x00000074 + +EWRAM_DATA struct BattleTestRunnerState *gBattleTestRunnerState = NULL; + +static void CB2_BattleTest_NextParameter(void); +static void CB2_BattleTest_NextTrial(void); +static void PushBattlerAction(u32 sourceLine, s32 battlerId, u32 actionType, u32 byte); + +NAKED static void InvokeSingleTestFunctionWithStack(void *results, u32 i, struct BattlePokemon *player, struct BattlePokemon *opponent, SingleBattleTestFunction function, void *stack) +{ + asm("push {r4-r6,lr}\n\ + ldr r4, [sp, #16] @ function\n\ + ldr r5, [sp, #20] @ stack\n\ + mov r6, sp\n\ + mov sp, r5\n\ + push {r6}\n\ + ldr r6, =SingleRestoreSP + 1\n\ + mov lr, r6\n\ + bx r4\n\ + SingleRestoreSP:\n\ + pop {r0}\n\ + mov sp, r0\n\ + pop {r4-r6}\n\ + pop {r0}\n\ + bx r0\n\ + .pool"); +} + +NAKED static void InvokeDoubleTestFunctionWithStack(void *results, u32 i, struct BattlePokemon *playerLeft, struct BattlePokemon *opponentLeft, struct BattlePokemon *playerRight, struct BattlePokemon *opponentRight, SingleBattleTestFunction function, void *stack) +{ + asm("push {r4-r7,lr}\n\ + ldr r4, [sp, #28] @ function\n\ + ldr r5, [sp, #32] @ stack\n\ + mov r6, sp\n\ + mov sp, r5\n\ + push {r6}\n\ + add r6, #20\n\ + ldmia r6, {r6, r7} @ playerRight, opponentRight\n\ + push {r6, r7}\n\ + ldr r6, =DoubleRestoreSP + 1\n\ + mov lr, r6\n\ + bx r4\n\ + DoubleRestoreSP:\n\ + add sp, #8\n\ + pop {r0}\n\ + mov sp, r0\n\ + pop {r4-r7}\n\ + pop {r0}\n\ + bx r0\n\ + .pool"); +} + +// Calls test->function, but pointing its stack at DATA.stack so that +// local variables are live after the function returns (and so can be +// referenced by HP_BAR, or the next call, etc). +// NOTE: This places the stack in EWRAM which has longer waitstates than +// IWRAM so could be much slower, but a) not that much code executes, +// and b) mga-rom-test isn't meaningfully limited by the GBA frame rate. +static void InvokeTestFunction(const struct BattleTest *test) +{ + STATE->parametersCount = 0; + switch (test->type) + { + case BATTLE_TEST_SINGLES: + InvokeSingleTestFunctionWithStack(STATE->results, STATE->runParameter, &gBattleMons[B_POSITION_PLAYER_LEFT], &gBattleMons[B_POSITION_OPPONENT_LEFT], test->function.singles, &DATA.stack[BATTLE_TEST_STACK_SIZE]); + break; + case BATTLE_TEST_DOUBLES: + InvokeDoubleTestFunctionWithStack(STATE->results, STATE->runParameter, &gBattleMons[B_POSITION_PLAYER_LEFT], &gBattleMons[B_POSITION_OPPONENT_LEFT], &gBattleMons[B_POSITION_PLAYER_RIGHT], &gBattleMons[B_POSITION_OPPONENT_RIGHT], test->function.singles, &DATA.stack[BATTLE_TEST_STACK_SIZE]); + break; + } +} + +static u32 SourceLine(u32 sourceLineOffset) +{ + const struct BattleTest *test = gTestRunnerState.test->data; + return test->sourceLine + sourceLineOffset; +} + +static u32 SourceLineOffset(u32 sourceLine) +{ + const struct BattleTest *test = gTestRunnerState.test->data; + if (sourceLine - test->sourceLine > 0xFF) + return 0; + else + return sourceLine - test->sourceLine; +} + +static u32 BattleTest_EstimateCost(void *data) +{ + u32 cost; + const struct BattleTest *test = data; + STATE = AllocZeroed(sizeof(*STATE)); + if (!STATE) + return 0; + STATE->runRandomly = TRUE; + DATA.recordedBattle.rngSeed = RNG_SEED_DEFAULT; + InvokeTestFunction(test); + cost = 1; + if (STATE->parametersCount != 0) + cost *= STATE->parametersCount; + if (STATE->trials != 0) + cost *= STATE->trials; + FREE_AND_SET_NULL(STATE); + return cost; +} + +static void BattleTest_SetUp(void *data) +{ + const struct BattleTest *test = data; + STATE = AllocZeroed(sizeof(*STATE)); + if (!STATE) + Test_ExitWithResult(TEST_RESULT_ERROR, "OOM: STATE = AllocZerod(%d)", sizeof(*STATE)); + InvokeTestFunction(test); + STATE->parameters = STATE->parametersCount; + STATE->results = AllocZeroed(test->resultsSize * STATE->parameters); + if (!STATE->results) + Test_ExitWithResult(TEST_RESULT_ERROR, "OOM: STATE->results = AllocZerod(%d)", sizeof(test->resultsSize * STATE->parameters)); + switch (test->type) + { + case BATTLE_TEST_SINGLES: + STATE->battlersCount = 2; + break; + case BATTLE_TEST_DOUBLES: + STATE->battlersCount = 4; + break; + } +} + +// This does not take into account priority, statuses, or any other +// modifiers. +static void SetImplicitSpeeds(void) +{ + s32 i, j; + u32 speed = 12; + u32 hasSpeeds = 0; + u32 allSpeeds = ((1 << DATA.playerPartySize) - 1) | (((1 << DATA.opponentPartySize) - 1) << 6); + bool32 madeProgress; + while (hasSpeeds != allSpeeds) + { + madeProgress = FALSE; + for (i = 0; i < DATA.playerPartySize; i++) + { + if (!(hasSpeeds & (1 << i)) + && !(DATA.slowerThan[B_SIDE_PLAYER][i] & ~hasSpeeds)) + { + SetMonData(&DATA.recordedBattle.playerParty[i], MON_DATA_SPEED, &speed); + speed--; + hasSpeeds |= 1 << i; + madeProgress = TRUE; + } + } + for (i = 0; i < DATA.opponentPartySize; i++) + { + if (!(hasSpeeds & ((1 << 6) << i)) + && !(DATA.slowerThan[B_SIDE_OPPONENT][i] & ~hasSpeeds)) + { + SetMonData(&DATA.recordedBattle.opponentParty[i], MON_DATA_SPEED, &speed); + speed--; + hasSpeeds |= (1 << 6) << i; + madeProgress = TRUE; + } + } + if (!madeProgress) + Test_ExitWithResult(TEST_RESULT_INVALID, "TURNs have contradictory speeds"); + } +} + +static void BattleTest_Run(void *data) +{ + s32 i; + u32 requiredPlayerPartySize; + u32 requiredOpponentPartySize; + const struct BattleTest *test = data; + + memset(&DATA, 0, sizeof(DATA)); + + DATA.recordedBattle.rngSeed = RNG_SEED_DEFAULT; + + DATA.recordedBattle.textSpeed = OPTIONS_TEXT_SPEED_FAST; + DATA.recordedBattle.battleFlags = BATTLE_TYPE_RECORDED_IS_MASTER | BATTLE_TYPE_RECORDED_LINK | BATTLE_TYPE_TRAINER | BATTLE_TYPE_IS_MASTER; + if (test->type == BATTLE_TEST_DOUBLES) + DATA.recordedBattle.battleFlags |= BATTLE_TYPE_DOUBLE; + for (i = 0; i < STATE->battlersCount; i++) + { + DATA.recordedBattle.playersName[i][0] = CHAR_1 + i; + DATA.recordedBattle.playersName[i][1] = EOS; + DATA.recordedBattle.playersLanguage[i] = GAME_LANGUAGE; + DATA.recordedBattle.playersBattlers[i] = i; + + DATA.currentMonIndexes[i] = (i & BIT_FLANK) == B_FLANK_LEFT ? 0 : 1; + } + + STATE->runRandomly = TRUE; + STATE->runGiven = TRUE; + STATE->runWhen = TRUE; + STATE->runScene = TRUE; + InvokeTestFunction(test); + STATE->runRandomly = FALSE; + STATE->runGiven = FALSE; + STATE->runWhen = FALSE; + STATE->runScene = FALSE; + + requiredPlayerPartySize = 0; + requiredOpponentPartySize = 0; + for (i = 0; i < STATE->battlersCount; i++) + { + if ((i & BIT_SIDE) == B_SIDE_PLAYER) + requiredPlayerPartySize = DATA.currentMonIndexes[i] + 1; + else + requiredOpponentPartySize = DATA.currentMonIndexes[i] + 1; + } + if (DATA.playerPartySize < requiredPlayerPartySize) + Test_ExitWithResult(TEST_RESULT_INVALID, "%d PLAYER Pokemon required", requiredPlayerPartySize); + if (DATA.opponentPartySize < requiredOpponentPartySize) + Test_ExitWithResult(TEST_RESULT_INVALID, "%d OPPONENT Pokemon required", requiredOpponentPartySize); + + for (i = 0; i < STATE->battlersCount; i++) + PushBattlerAction(0, i, RECORDED_BYTE, 0xFF); + + if (DATA.hasExplicitSpeeds) + { + if (DATA.explicitSpeeds[B_SIDE_PLAYER] != (1 << DATA.playerPartySize) - 1 + && DATA.explicitSpeeds[B_SIDE_OPPONENT] != (1 << DATA.opponentPartySize) - 1) + { + Test_ExitWithResult(TEST_RESULT_INVALID, "Speed required for all PLAYERs and OPPONENTs"); + } + } + else + { + SetImplicitSpeeds(); + } + + SetVariablesForRecordedBattle(&DATA.recordedBattle); + + if (STATE->trials) + gMain.savedCallback = CB2_BattleTest_NextTrial; + else if (STATE->parameters) + gMain.savedCallback = CB2_BattleTest_NextParameter; + else + gMain.savedCallback = CB2_TestRunner; + SetMainCallback2(CB2_InitBattle); + + STATE->checkProgressParameter = 0; + STATE->checkProgressTrial = 0; + STATE->checkProgressTurn = 0; + + if (STATE->trials && STATE->parameters) + MgbaPrintf_(":N%s %d/%d (%d/%d)", gTestRunnerState.test->name, STATE->runParameter + 1, STATE->parameters, STATE->runTrial + 1, STATE->trials); + else if (STATE->trials) + MgbaPrintf_(":N%s (%d/%d)", gTestRunnerState.test->name, STATE->runTrial + 1, STATE->trials); + else if (STATE->parameters) + MgbaPrintf_(":N%s %d/%d", gTestRunnerState.test->name, STATE->runParameter + 1, STATE->parameters); +} + +static s32 TryAbilityPopUp(s32 i, s32 n, u32 battlerId, u32 ability) +{ + struct QueuedAbilityEvent *event; + s32 iMax = i + n; + for (; i < iMax; i++) + { + if (DATA.queuedEvents[i].type != QUEUED_ABILITY_POPUP_EVENT) + continue; + + event = &DATA.queuedEvents[i].as.ability; + + if (event->battlerId == battlerId + && (event->ability == ABILITY_NONE || event->ability == ability)) + return i; + } + return -1; +} + +void TestRunner_Battle_RecordAbilityPopUp(u32 battlerId, u32 ability) +{ + s32 queuedEvent; + s32 match; + struct QueuedEvent *event; + + if (DATA.queuedEvent == DATA.queuedEventsCount) + return; + + event = &DATA.queuedEvents[DATA.queuedEvent]; + switch (event->groupType) + { + case QUEUE_GROUP_NONE: + case QUEUE_GROUP_ONE_OF: + if (TryAbilityPopUp(DATA.queuedEvent, event->groupSize, battlerId, ability) != -1) + DATA.queuedEvent += event->groupSize; + break; + case QUEUE_GROUP_NONE_OF: + queuedEvent = DATA.queuedEvent; + do + { + if ((match = TryAbilityPopUp(queuedEvent, event->groupSize, battlerId, ability)) != -1) + { + const char *filename = gTestRunnerState.test->filename; + u32 line = SourceLine(DATA.queuedEvents[match].sourceLineOffset); + Test_ExitWithResult(TEST_RESULT_FAIL, "%s:%d: Matched ABILITY_POPUP", filename, line); + } + + queuedEvent += event->groupSize; + if (queuedEvent == DATA.queuedEventsCount) + break; + + event = &DATA.queuedEvents[queuedEvent]; + if (event->groupType == QUEUE_GROUP_NONE_OF) + continue; + + if (TryAbilityPopUp(queuedEvent, event->groupSize, battlerId, ability) != -1) + DATA.queuedEvent = queuedEvent + event->groupSize; + } while (FALSE); + break; + } +} + +static s32 TryAnimation(s32 i, s32 n, u32 animType, u32 animId) +{ + struct QueuedAnimationEvent *event; + s32 iMax = i + n; + for (; i < iMax; i++) + { + if (DATA.queuedEvents[i].type != QUEUED_ANIMATION_EVENT) + continue; + + event = &DATA.queuedEvents[i].as.animation; + + if (event->type == animType + && event->id == animId + && (event->attacker == 0xF || event->attacker == gBattleAnimAttacker) + && (event->target == 0xF || event->target == gBattleAnimTarget)) + return i; + } + return -1; +} + +void TestRunner_Battle_RecordAnimation(u32 animType, u32 animId) +{ + s32 queuedEvent; + s32 match; + struct QueuedEvent *event; + + if (DATA.queuedEvent == DATA.queuedEventsCount) + return; + + event = &DATA.queuedEvents[DATA.queuedEvent]; + switch (event->groupType) + { + case QUEUE_GROUP_NONE: + case QUEUE_GROUP_ONE_OF: + if (TryAnimation(DATA.queuedEvent, event->groupSize, animType, animId) != -1) + DATA.queuedEvent += event->groupSize; + break; + case QUEUE_GROUP_NONE_OF: + queuedEvent = DATA.queuedEvent; + do + { + if ((match = TryAnimation(queuedEvent, event->groupSize, animType, animId)) != -1) + { + const char *filename = gTestRunnerState.test->filename; + u32 line = SourceLine(DATA.queuedEvents[match].sourceLineOffset); + Test_ExitWithResult(TEST_RESULT_FAIL, "%s:%d: Matched ANIMATION", filename, line); + } + + queuedEvent += event->groupSize; + if (queuedEvent == DATA.queuedEventsCount) + break; + + event = &DATA.queuedEvents[queuedEvent]; + if (event->groupType == QUEUE_GROUP_NONE_OF) + continue; + + if (TryAnimation(queuedEvent, event->groupSize, animType, animId) != -1) + DATA.queuedEvent = queuedEvent + event->groupSize; + } while (FALSE); + break; + } +} + +static s32 TryHP(s32 i, s32 n, u32 battlerId, u32 oldHP, u32 newHP) +{ + struct QueuedHPEvent *event; + s32 iMax = i + n; + for (; i < iMax; i++) + { + if (DATA.queuedEvents[i].type != QUEUED_HP_EVENT) + continue; + + event = &DATA.queuedEvents[i].as.hp; + + if (event->battlerId == battlerId) + { + if (event->address <= 0xFFFF) + { + switch (event->type) + { + case HP_EVENT_NEW_HP: + if (event->address == newHP) + return i; + break; + case HP_EVENT_DELTA_HP: + if (event->address == 0) + return i; + else if ((s16)event->address == oldHP - newHP) + return i; + break; + } + } + else + { + switch (event->type) + { + case HP_EVENT_NEW_HP: + *(u16 *)event->address = newHP; + break; + case HP_EVENT_DELTA_HP: + *(s16 *)event->address = oldHP - newHP; + break; + } + return i; + } + } + } + return -1; +} + +void TestRunner_Battle_RecordHP(u32 battlerId, u32 oldHP, u32 newHP) +{ + s32 queuedEvent; + s32 match; + struct QueuedEvent *event; + + if (DATA.queuedEvent == DATA.queuedEventsCount) + return; + + event = &DATA.queuedEvents[DATA.queuedEvent]; + switch (event->groupType) + { + case QUEUE_GROUP_NONE: + case QUEUE_GROUP_ONE_OF: + if (TryHP(DATA.queuedEvent, event->groupSize, battlerId, oldHP, newHP) != -1) + DATA.queuedEvent += event->groupSize; + break; + case QUEUE_GROUP_NONE_OF: + queuedEvent = DATA.queuedEvent; + do + { + if ((match = TryHP(queuedEvent, event->groupSize, battlerId, oldHP, newHP)) != -1) + { + const char *filename = gTestRunnerState.test->filename; + u32 line = SourceLine(DATA.queuedEvents[match].sourceLineOffset); + Test_ExitWithResult(TEST_RESULT_FAIL, "%s:%d: Matched HP_BAR", filename, line); + } + + queuedEvent += event->groupSize; + if (queuedEvent == DATA.queuedEventsCount) + break; + + event = &DATA.queuedEvents[queuedEvent]; + if (event->groupType == QUEUE_GROUP_NONE_OF) + continue; + + if (TryHP(queuedEvent, event->groupSize, battlerId, oldHP, newHP) != -1) + DATA.queuedEvent = queuedEvent + event->groupSize; + } while (FALSE); + break; + } +} + +static s32 TryMessage(s32 i, s32 n, const u8 *string) +{ + s32 j, k; + struct QueuedMessageEvent *event; + s32 iMax = i + n; + for (; i < iMax; i++) + { + if (DATA.queuedEvents[i].type != QUEUED_MESSAGE_EVENT) + continue; + + event = &DATA.queuedEvents[i].as.message; + for (j = k = 0; ; j++, k++) + { + if (event->pattern[k] == CHAR_SPACE) + { + switch (string[j]) + { + case CHAR_SPACE: + case CHAR_PROMPT_SCROLL: + case CHAR_PROMPT_CLEAR: + case CHAR_NEWLINE: + j++; + k++; + break; + } + } + if (event->pattern[k] == EOS) + { + // Consume any trailing '\p'. + if (string[j] == CHAR_PROMPT_CLEAR) + j++; + } + if (string[j] != event->pattern[k]) + { + break; + } + else if (string[j] == EOS) + { + return i; + } + } + } + return -1; +} + +void TestRunner_Battle_RecordMessage(const u8 *string) +{ + s32 queuedEvent; + s32 match; + struct QueuedEvent *event; + + if (DATA.queuedEvent == DATA.queuedEventsCount) + return; + + event = &DATA.queuedEvents[DATA.queuedEvent]; + switch (event->groupType) + { + case QUEUE_GROUP_NONE: + case QUEUE_GROUP_ONE_OF: + if (TryMessage(DATA.queuedEvent, event->groupSize, string) != -1) + DATA.queuedEvent += event->groupSize; + break; + case QUEUE_GROUP_NONE_OF: + queuedEvent = DATA.queuedEvent; + do + { + if ((match = TryMessage(queuedEvent, event->groupSize, string)) != -1) + { + const char *filename = gTestRunnerState.test->filename; + u32 line = SourceLine(DATA.queuedEvents[match].sourceLineOffset); + Test_ExitWithResult(TEST_RESULT_FAIL, "%s:%d: Matched MESSAGE", filename, line); + } + + queuedEvent += event->groupSize; + if (queuedEvent == DATA.queuedEventsCount) + break; + + event = &DATA.queuedEvents[queuedEvent]; + if (event->groupType == QUEUE_GROUP_NONE_OF) + continue; + + if (TryMessage(queuedEvent, event->groupSize, string) != -1) + DATA.queuedEvent = queuedEvent + event->groupSize; + } while (FALSE); + break; + } +} + +static s32 TryStatus(s32 i, s32 n, u32 battlerId, u32 status1) +{ + struct QueuedStatusEvent *event; + s32 iMax = i + n; + for (; i < iMax; i++) + { + if (DATA.queuedEvents[i].type != QUEUED_STATUS_EVENT) + continue; + + event = &DATA.queuedEvents[i].as.status; + + if (event->battlerId == battlerId) + { + if (event->mask == 0 && status1 == STATUS1_NONE) + return i; + else if (event->mask & status1) + return i; + } + } + return -1; +} + +void TestRunner_Battle_RecordStatus1(u32 battlerId, u32 status1) +{ + s32 queuedEvent; + s32 match; + struct QueuedEvent *event; + + if (DATA.queuedEvent == DATA.queuedEventsCount) + return; + + event = &DATA.queuedEvents[DATA.queuedEvent]; + switch (event->groupType) + { + case QUEUE_GROUP_NONE: + case QUEUE_GROUP_ONE_OF: + if (TryStatus(DATA.queuedEvent, event->groupSize, battlerId, status1) != -1) + DATA.queuedEvent += event->groupSize; + break; + case QUEUE_GROUP_NONE_OF: + queuedEvent = DATA.queuedEvent; + do + { + if ((match = TryStatus(queuedEvent, event->groupSize, battlerId, status1)) != -1) + { + const char *filename = gTestRunnerState.test->filename; + u32 line = SourceLine(DATA.queuedEvents[match].sourceLineOffset); + Test_ExitWithResult(TEST_RESULT_FAIL, "%s:%d: Matched STATUS_ICON", filename, line); + } + + queuedEvent += event->groupSize; + if (queuedEvent == DATA.queuedEventsCount) + break; + + event = &DATA.queuedEvents[queuedEvent]; + if (event->groupType == QUEUE_GROUP_NONE_OF) + continue; + + if (TryStatus(queuedEvent, event->groupSize, battlerId, status1) != -1) + DATA.queuedEvent = queuedEvent + event->groupSize; + } while (FALSE); + break; + } +} + +static const char *const sEventTypeMacros[] = +{ + [QUEUED_ABILITY_POPUP_EVENT] = "ABILITY_POPUP", + [QUEUED_ANIMATION_EVENT] = "ANIMATION", + [QUEUED_HP_EVENT] = "HP_BAR", + [QUEUED_MESSAGE_EVENT] = "MESSAGE", + [QUEUED_STATUS_EVENT] = "STATUS_ICON", +}; + +void TestRunner_Battle_AfterLastTurn(void) +{ + const struct BattleTest *test = gTestRunnerState.test->data; + + if (DATA.turns - 1 != DATA.lastActionTurn) + { + const char *filename = gTestRunnerState.test->filename; + Test_ExitWithResult(TEST_RESULT_FAIL, "%s:%d: %d TURNs specified, but %d ran", filename, SourceLine(0), DATA.turns, DATA.lastActionTurn + 1); + } + + while (DATA.queuedEvent < DATA.queuedEventsCount + && DATA.queuedEvents[DATA.queuedEvent].groupType == QUEUE_GROUP_NONE_OF) + { + DATA.queuedEvent += DATA.queuedEvents[DATA.queuedEvent].groupSize; + } + if (DATA.queuedEvent != DATA.queuedEventsCount) + { + const char *filename = gTestRunnerState.test->filename; + u32 line = SourceLine(DATA.queuedEvents[DATA.queuedEvent].sourceLineOffset); + const char *macro = sEventTypeMacros[DATA.queuedEvents[DATA.queuedEvent].type]; + Test_ExitWithResult(TEST_RESULT_FAIL, "%s:%d: Unmatched %s", filename, line, macro); + } + + STATE->runThen = TRUE; + STATE->runFinally = STATE->runParameter + 1 == STATE->parameters; + InvokeTestFunction(test); + STATE->runThen = FALSE; + STATE->runFinally = FALSE; +} + +static void CB2_BattleTest_NextParameter(void) +{ + if (++STATE->runParameter >= STATE->parameters) + SetMainCallback2(CB2_TestRunner); + else + BattleTest_Run(gTestRunnerState.test->data); +} + +static void CB2_BattleTest_NextTrial(void) +{ + FreeMonSpritesGfx(); + FreeBattleSpritesData(); + FreeBattleResources(); + FreeAllWindowBuffers(); + + SetMainCallback2(CB2_BattleTest_NextParameter); + + if (++STATE->runTrial < STATE->trials) + { + switch (gTestRunnerState.result) + { + case TEST_RESULT_FAIL: + break; + case TEST_RESULT_PASS: + STATE->observedPasses++; + break; + case TEST_RESULT_SKIP: + STATE->skippedTrials++; + if (STATE->skippedTrials > STATE->trials / 4) + Test_ExitWithResult(TEST_RESULT_INVALID, "25%% of the trials were SKIPed"); + break; + default: + return; + } + if (STATE->parameters) + MgbaPrintf_(":N%s %d/%d (%d/%d)", gTestRunnerState.test->name, STATE->runParameter + 1, STATE->parameters, STATE->runTrial + 1, STATE->trials); + else + MgbaPrintf_(":N%s (%d/%d)", gTestRunnerState.test->name, STATE->runTrial + 1, STATE->trials); + gTestRunnerState.result = TEST_RESULT_PASS; + DATA.recordedBattle.rngSeed = ISO_RANDOMIZE1(STATE->runTrial); + DATA.queuedEvent = 0; + DATA.lastActionTurn = 0; + DATA.nextRNGTurn = 0; + SetVariablesForRecordedBattle(&DATA.recordedBattle); + SetMainCallback2(CB2_InitBattle); + } + else + { + // This is a tolerance of +/- 4%. + if (abs(STATE->observedPasses - STATE->expectedPasses) <= 2) + gTestRunnerState.result = TEST_RESULT_PASS; + else + Test_ExitWithResult(TEST_RESULT_FAIL, "Expected %d/%d passes, observed %d/%d", STATE->expectedPasses, STATE->trials, STATE->observedPasses, STATE->trials); + } +} + +static void BattleTest_TearDown(void *data) +{ + if (STATE) + { + FREE_AND_SET_NULL(STATE->results); + FREE_AND_SET_NULL(STATE); + } +} + +static bool32 BattleTest_CheckProgress(void *data) +{ + bool32 madeProgress + = STATE->checkProgressParameter < STATE->runParameter + || STATE->checkProgressTrial < STATE->runTrial + || STATE->checkProgressTurn < gBattleResults.battleTurnCounter; + STATE->checkProgressParameter = STATE->runParameter; + STATE->checkProgressTrial = STATE->runTrial; + STATE->checkProgressTurn = gBattleResults.battleTurnCounter; + return madeProgress; +} + +static bool32 BattleTest_HandleExitWithResult(void *data, enum TestResult result) +{ + if (result != TEST_RESULT_INVALID + && result != TEST_RESULT_ERROR + && result != TEST_RESULT_TIMEOUT + && STATE->runTrial < STATE->trials) + { + SetMainCallback2(CB2_BattleTest_NextTrial); + return TRUE; + } + else + { + return FALSE; + } +} + +void Randomly(u32 sourceLine, u32 passes, u32 trials) +{ + INVALID_IF(DATA.recordedBattle.rngSeed != RNG_SEED_DEFAULT, "RNG seed already set"); + // This is a precision of 2%. + STATE->trials = 50; + STATE->expectedPasses = STATE->trials * passes / trials; + STATE->observedPasses = 0; + STATE->skippedTrials = 0; + STATE->runTrial = 0; + DATA.recordedBattle.rngSeed = 0; +} + +void RNGSeed_(u32 sourceLine, u32 seed) +{ + INVALID_IF(DATA.recordedBattle.rngSeed != RNG_SEED_DEFAULT, "RNG seed already set"); + DATA.recordedBattle.rngSeed = seed; +} + +const struct TestRunner gBattleTestRunner = +{ + .estimateCost = BattleTest_EstimateCost, + .setUp = BattleTest_SetUp, + .run = BattleTest_Run, + .tearDown = BattleTest_TearDown, + .checkProgress = BattleTest_CheckProgress, + .handleExitWithResult = BattleTest_HandleExitWithResult, +}; + +void OpenPokemon(u32 sourceLine, u32 side, u32 species) +{ + s32 i, data; + u8 *partySize; + struct Pokemon *party; + INVALID_IF(species >= SPECIES_EGG, "Invalid species: %d", species); + if (side == B_SIDE_PLAYER) + { + partySize = &DATA.playerPartySize; + party = DATA.recordedBattle.playerParty; + } + else + { + partySize = &DATA.opponentPartySize; + party = DATA.recordedBattle.opponentParty; + } + INVALID_IF(*partySize == PARTY_SIZE, "Too many Pokemon in party"); + DATA.currentSide = side; + DATA.currentPartyIndex = *partySize; + DATA.currentMon = &party[DATA.currentPartyIndex]; + DATA.gender = MON_MALE; + DATA.nature = NATURE_HARDY; + (*partySize)++; + + CreateMon(DATA.currentMon, species, 100, 0, TRUE, 0, OT_ID_PRESET, 0); + data = MOVE_NONE; + for (i = 0; i < MAX_MON_MOVES; i++) + SetMonData(DATA.currentMon, MON_DATA_MOVE1 + i, &data); +} + +// (sNaturePersonalities[i] % NUM_NATURES) == i +// (sNaturePersonalities[i] & 0xFF) == 0 +// NOTE: Using 25 << 8 rather than 0 << 8 to prevent shiny females. +static const u16 sNaturePersonalities[NUM_NATURES] = +{ + 25 << 8, 21 << 8, 17 << 8, 13 << 8, 9 << 8, + 5 << 8, 1 << 8, 22 << 8, 18 << 8, 14 << 8, + 10 << 8, 6 << 8, 2 << 8, 23 << 8, 19 << 8, + 15 << 8, 11 << 8, 7 << 8, 3 << 8, 24 << 8, + 20 << 8, 16 << 8, 12 << 8, 8 << 8, 4 << 8, +}; + +static u32 GenerateNature(u32 nature, u32 offset) +{ + int i; + if (offset <= nature) + nature -= offset; + else + nature = nature + NUM_NATURES - offset; + return sNaturePersonalities[nature]; +} + +void ClosePokemon(u32 sourceLine) +{ + s32 i; + INVALID_IF(DATA.hasExplicitSpeeds && !(DATA.explicitSpeeds[DATA.currentSide] & (1 << DATA.currentPartyIndex)), "Speed required"); + for (i = 0; i < STATE->battlersCount; i++) + { + if ((i & BIT_SIDE) == DATA.currentSide + && DATA.currentMonIndexes[i] == DATA.currentPartyIndex) + { + INVALID_IF(GetMonData(DATA.currentMon, MON_DATA_HP) == 0, "Battlers cannot be fainted"); + } + } + UpdateMonPersonality(&DATA.currentMon->box, GenerateNature(DATA.nature, DATA.gender % NUM_NATURES) | DATA.gender); + DATA.currentMon = NULL; +} + +void Gender_(u32 sourceLine, u32 gender) +{ + const struct SpeciesInfo *info; + INVALID_IF(!DATA.currentMon, "Gender outside of PLAYER/OPPONENT"); + info = &gSpeciesInfo[GetMonData(DATA.currentMon, MON_DATA_SPECIES)]; + switch (gender) + { + case MON_MALE: + DATA.gender = 0xFF; + INVALID_IF(info->genderRatio == MON_GENDERLESS || info->genderRatio == MON_FEMALE, "Illegal male"); + break; + case MON_FEMALE: + DATA.gender = 0x00; + INVALID_IF(info->genderRatio == MON_GENDERLESS || info->genderRatio == MON_MALE, "Illegal female"); + break; + case MON_GENDERLESS: + INVALID_IF(info->genderRatio != gender, "Illegal genderless"); + break; + } +} + +void Nature_(u32 sourceLine, u32 nature) +{ + INVALID_IF(!DATA.currentMon, "Nature outside of PLAYER/OPPONENT"); + INVALID_IF(nature >= NUM_NATURES, "Illegal nature: %d", nature); + DATA.nature = nature; +} + +void Ability_(u32 sourceLine, u32 ability) +{ + s32 i; + u32 species; + const struct SpeciesInfo *info; + INVALID_IF(!DATA.currentMon, "Ability outside of PLAYER/OPPONENT"); + species = GetMonData(DATA.currentMon, MON_DATA_SPECIES); + info = &gSpeciesInfo[species]; + for (i = 0; i < NUM_ABILITY_SLOTS; i++) + { + if (info->abilities[i] == ability) + { + SetMonData(DATA.currentMon, MON_DATA_ABILITY_NUM, &i); + break; + } + } + INVALID_IF(i == NUM_ABILITY_SLOTS, "%S cannot have %S", gSpeciesNames[species], gAbilityNames[ability]); +} + +void Level_(u32 sourceLine, u32 level) +{ + // TODO: Preserve any explicitly-set stats. + INVALID_IF(!DATA.currentMon, "Level outside of PLAYER/OPPONENT"); + INVALID_IF(level == 0 || level > MAX_LEVEL, "Illegal level: %d", level); + SetMonData(DATA.currentMon, MON_DATA_LEVEL, &level); +} + +void MaxHP_(u32 sourceLine, u32 maxHP) +{ + INVALID_IF(!DATA.currentMon, "MaxHP outside of PLAYER/OPPONENT"); + INVALID_IF(maxHP == 0, "Illegal max HP: %d", maxHP); + SetMonData(DATA.currentMon, MON_DATA_MAX_HP, &maxHP); +} + +void HP_(u32 sourceLine, u32 hp) +{ + INVALID_IF(!DATA.currentMon, "HP outside of PLAYER/OPPONENT"); + if (hp > GetMonData(DATA.currentMon, MON_DATA_MAX_HP)) + SetMonData(DATA.currentMon, MON_DATA_MAX_HP, &hp); + SetMonData(DATA.currentMon, MON_DATA_HP, &hp); +} + +void Attack_(u32 sourceLine, u32 attack) +{ + INVALID_IF(!DATA.currentMon, "Attack outside of PLAYER/OPPONENT"); + INVALID_IF(attack == 0, "Illegal attack: %d", attack); + SetMonData(DATA.currentMon, MON_DATA_ATK, &attack); +} + +void Defense_(u32 sourceLine, u32 defense) +{ + INVALID_IF(!DATA.currentMon, "Defense outside of PLAYER/OPPONENT"); + INVALID_IF(defense == 0, "Illegal defense: %d", defense); + SetMonData(DATA.currentMon, MON_DATA_DEF, &defense); +} + +void SpAttack_(u32 sourceLine, u32 spAttack) +{ + INVALID_IF(!DATA.currentMon, "SpAttack outside of PLAYER/OPPONENT"); + INVALID_IF(spAttack == 0, "Illegal special attack: %d", spAttack); + SetMonData(DATA.currentMon, MON_DATA_SPATK, &spAttack); +} + +void SpDefense_(u32 sourceLine, u32 spDefense) +{ + INVALID_IF(!DATA.currentMon, "SpDefense outside of PLAYER/OPPONENT"); + INVALID_IF(spDefense == 0, "Illegal special defense: %d", spDefense); + SetMonData(DATA.currentMon, MON_DATA_SPDEF, &spDefense); +} + +void Speed_(u32 sourceLine, u32 speed) +{ + INVALID_IF(!DATA.currentMon, "Speed outside of PLAYER/OPPONENT"); + INVALID_IF(speed == 0, "Illegal speed: %d", speed); + SetMonData(DATA.currentMon, MON_DATA_SPEED, &speed); + DATA.hasExplicitSpeeds = TRUE; + DATA.explicitSpeeds[DATA.currentSide] |= 1 << DATA.currentPartyIndex; +} + +void Item_(u32 sourceLine, u32 item) +{ + INVALID_IF(!DATA.currentMon, "Item outside of PLAYER/OPPONENT"); + INVALID_IF(item >= ITEMS_COUNT, "Illegal item: %d", item); + SetMonData(DATA.currentMon, MON_DATA_HELD_ITEM, &item); +} + +void Moves_(u32 sourceLine, const u16 moves[MAX_MON_MOVES]) +{ + s32 i; + INVALID_IF(!DATA.currentMon, "Moves outside of PLAYER/OPPONENT"); + for (i = 0; i < MAX_MON_MOVES; i++) + { + if (moves[i] == MOVE_NONE) + break; + INVALID_IF(moves[i] >= MOVES_COUNT, "Illegal move: %d", moves[i]); + SetMonData(DATA.currentMon, MON_DATA_MOVE1 + i, &moves[i]); + SetMonData(DATA.currentMon, MON_DATA_PP1 + i, &gBattleMoves[moves[i]].pp); + } + DATA.explicitMoves[DATA.currentSide] |= 1 << DATA.currentPartyIndex; +} + +void Friendship_(u32 sourceLine, u32 friendship) +{ + INVALID_IF(!DATA.currentMon, "Friendship outside of PLAYER/OPPONENT"); + SetMonData(DATA.currentMon, MON_DATA_FRIENDSHIP, &friendship); +} + +void Status1_(u32 sourceLine, u32 status1) +{ + INVALID_IF(!DATA.currentMon, "Status1 outside of PLAYER/OPPONENT"); + INVALID_IF(status1 & STATUS1_TOXIC_COUNTER, "Illegal status1: has TOXIC_TURN"); + SetMonData(DATA.currentMon, MON_DATA_STATUS, &status1); +} + +static void PushBattlerAction(u32 sourceLine, s32 battlerId, u32 actionType, u32 byte) +{ + u32 recordIndex = DATA.recordIndexes[battlerId]++; + if (recordIndex >= BATTLER_RECORD_SIZE) + Test_ExitWithResult(TEST_RESULT_INVALID, "Too many actions"); + DATA.battleRecordTypes[battlerId][recordIndex] = actionType; + DATA.battleRecordSourceLineOffsets[battlerId][recordIndex] = SourceLineOffset(sourceLine); + DATA.recordedBattle.battleRecord[battlerId][recordIndex] = byte; +} + +void BattleTest_CheckBattleRecordActionType(u32 battlerId, u32 recordIndex, u32 actionType) +{ + // TODO: Support explicit seeds for each turn? + if (DATA.nextRNGTurn == gBattleResults.battleTurnCounter + && (DATA.recordedBattle.rngSeed == RNG_SEED_DEFAULT + || DATA.recordedBattle.rngSeed == RNG_SEED_CRITICAL_HIT + || DATA.recordedBattle.rngSeed == RNG_SEED_MISS)) + { + gRngValue = DATA.recordedBattle.rngSeed; + DATA.nextRNGTurn++; + } + + // An illegal move choice will cause the battle to request a new + // move slot and target. This detects the move slot. + if (actionType == RECORDED_MOVE_SLOT + && recordIndex > 0 + && DATA.battleRecordTypes[battlerId][recordIndex-1] != RECORDED_ACTION_TYPE) + { + s32 i; + const char *filename = gTestRunnerState.test->filename; + for (i = recordIndex; i > 0; i--) + { + if (DATA.battleRecordTypes[battlerId][i-1] == RECORDED_ACTION_TYPE + && DATA.recordedBattle.battleRecord[battlerId][i-1] == B_ACTION_USE_MOVE) + { + u32 line = SourceLine(DATA.battleRecordSourceLineOffsets[battlerId][i-1]); + Test_ExitWithResult(TEST_RESULT_INVALID, "%s:%d: Illegal MOVE", filename, line); + } + } + Test_ExitWithResult(TEST_RESULT_INVALID, "%s:%d: Illegal MOVE", filename, SourceLine(0)); + } + + if (DATA.battleRecordTypes[battlerId][recordIndex] != RECORDED_BYTE) + { + DATA.lastActionTurn = gBattleResults.battleTurnCounter; + + if (actionType != DATA.battleRecordTypes[battlerId][recordIndex]) + { + const char *actualMacro = NULL; + const char *filename = gTestRunnerState.test->filename; + u32 line = SourceLine(DATA.battleRecordSourceLineOffsets[battlerId][recordIndex]); + + switch (DATA.battleRecordTypes[battlerId][recordIndex]) + { + case RECORDED_ACTION_TYPE: + switch (DATA.recordedBattle.battleRecord[battlerId][recordIndex]) + { + case B_ACTION_USE_MOVE: + actualMacro = "MOVE"; + break; + case B_ACTION_SWITCH: + actualMacro = "SWITCH"; + break; + } + break; + case RECORDED_PARTY_INDEX: + actualMacro = "SEND_OUT"; + break; + } + + if (actualMacro) + { + switch (actionType) + { + case RECORDED_ACTION_TYPE: + Test_ExitWithResult(TEST_RESULT_INVALID, "%s:%d: Expected MOVE/SWITCH, got %s", filename, line, actualMacro); + case RECORDED_PARTY_INDEX: + Test_ExitWithResult(TEST_RESULT_INVALID, "%s:%d: Expected SEND_OUT, got %s", filename, line, actualMacro); + } + } + + Test_ExitWithResult(TEST_RESULT_ERROR, "%s:%d: Illegal battle record", filename, line); + } + } + else + { + if (DATA.lastActionTurn == gBattleResults.battleTurnCounter) + { + const char *filename = gTestRunnerState.test->filename; + Test_ExitWithResult(TEST_RESULT_FAIL, "%s:%d: TURN %d incomplete", filename, SourceLine(0), gBattleResults.battleTurnCounter + 1); + } + } +} + +void OpenTurn(u32 sourceLine) +{ + INVALID_IF(DATA.turnState != TURN_CLOSED, "Nested TURN"); + DATA.turnState = TURN_OPEN; + DATA.actionBattlers = 0x00; + DATA.moveBattlers = 0x00; + DATA.hasRNGActions = FALSE; +} + +static void SetSlowerThan(s32 battlerId) +{ + s32 i, slowerThan; + slowerThan = 0; + for (i = 0; i < STATE->battlersCount; i++) + { + if (i == battlerId) + continue; + if (DATA.moveBattlers & (1 << i)) + { + if ((i & BIT_SIDE) == B_SIDE_PLAYER) + slowerThan |= 1 << DATA.currentMonIndexes[i]; + else + slowerThan |= (1 << 6) << DATA.currentMonIndexes[i]; + } + } + DATA.slowerThan[battlerId & BIT_SIDE][DATA.currentMonIndexes[battlerId]] |= slowerThan; +} + +void CloseTurn(u32 sourceLine) +{ + s32 i; + INVALID_IF(DATA.turnState != TURN_OPEN, "Nested TURN"); + DATA.turnState = TURN_CLOSING; + for (i = 0; i < STATE->battlersCount; i++) + { + if (!(DATA.actionBattlers & (1 << i))) + Move(sourceLine, &gBattleMons[i], (struct MoveContext) { move: MOVE_CELEBRATE, explicitMove: TRUE }); + } + DATA.turnState = TURN_CLOSED; + DATA.turns++; +} + +static struct Pokemon *CurrentMon(s32 battlerId) +{ + struct Pokemon *party; + if ((battlerId & BIT_SIDE) == B_SIDE_PLAYER) + party = DATA.recordedBattle.playerParty; + else + party = DATA.recordedBattle.opponentParty; + return &party[DATA.currentMonIndexes[battlerId]]; +} + +void Move(u32 sourceLine, struct BattlePokemon *battler, struct MoveContext ctx) +{ + s32 i; + s32 battlerId = battler - gBattleMons; + struct Pokemon *mon = CurrentMon(battlerId); + u32 moveId, moveSlot; + s32 target; + + INVALID_IF(DATA.turnState == TURN_CLOSED, "MOVE outside TURN"); + + if (ctx.explicitMove) + { + INVALID_IF(ctx.move == MOVE_NONE || ctx.move >= MOVES_COUNT, "Illegal move: %d", ctx.move); + for (i = 0; i < MAX_MON_MOVES; i++) + { + moveId = GetMonData(mon, MON_DATA_MOVE1 + i); + if (moveId == ctx.move) + { + moveSlot = i; + break; + } + else if (moveId == MOVE_NONE) + { + INVALID_IF(DATA.explicitMoves[battlerId & BIT_SIDE] & (1 << DATA.currentMonIndexes[battlerId]), "Missing explicit %S", gMoveNames[ctx.move]); + INVALID_IF(i == MAX_MON_MOVES, "Too many different moves"); + SetMonData(mon, MON_DATA_MOVE1 + i, &ctx.move); + SetMonData(DATA.currentMon, MON_DATA_PP1 + i, &gBattleMoves[ctx.move].pp); + moveSlot = i; + moveId = ctx.move; + break; + } + } + } + else if (ctx.explicitMoveSlot) + { + moveSlot = ctx.moveSlot; + moveId = GetMonData(mon, MON_DATA_MOVE1 + moveSlot); + INVALID_IF(moveId == MOVE_NONE, "Empty moveSlot: %d", ctx.moveSlot); + } + else + { + INVALID("No move or moveSlot"); + } + + if (ctx.explicitMegaEvolve && ctx.megaEvolve) + moveSlot |= RET_MEGA_EVOLUTION; + + if (ctx.explicitTarget) + { + target = ctx.target - gBattleMons; + } + else + { + const struct BattleMove *move = &gBattleMoves[moveId]; + if (move->target == MOVE_TARGET_RANDOM + || move->target == MOVE_TARGET_BOTH + || move->target == MOVE_TARGET_FOES_AND_ALLY + || move->target == MOVE_TARGET_OPPONENTS_FIELD + || move->target == MOVE_TARGET_ALL_BATTLERS) + { + target = BATTLE_OPPOSITE(battlerId); + } + else if (move->target == MOVE_TARGET_SELECTED) + { + INVALID_IF(STATE->battlersCount > 2, "%S requires explicit target", gMoveNames[moveId]); + + target = BATTLE_OPPOSITE(battlerId); + } + else if (move->target == MOVE_TARGET_USER) + { + target = battlerId; + } + else if (move->target == MOVE_TARGET_ALLY) + { + target = BATTLE_PARTNER(battlerId); + } + else + { + INVALID("%S requires explicit target", gMoveNames[moveId]); + } + } + + if (ctx.explicitHit && !ctx.hit) + { + if (DATA.hasRNGActions != 0) + Test_ExitWithResult(TEST_RESULT_ERROR, "%s:%d: hit only supported on the first move", gTestRunnerState.test->filename, sourceLine); + INVALID_IF(DATA.recordedBattle.rngSeed != RNG_SEED_DEFAULT, "RNG seed already set"); + DATA.recordedBattle.rngSeed = RNG_SEED_MISS; + } + + if (ctx.explicitCriticalHit && ctx.criticalHit) + { + if (DATA.hasRNGActions != 0) + Test_ExitWithResult(TEST_RESULT_ERROR, "%s:%d: criticalHit only supported on the first move", gTestRunnerState.test->filename, sourceLine); + INVALID_IF(DATA.recordedBattle.rngSeed != RNG_SEED_DEFAULT, "RNG seed already set"); + DATA.recordedBattle.rngSeed = RNG_SEED_CRITICAL_HIT; + } + + if (!(DATA.actionBattlers & (1 << battlerId))) + { + PushBattlerAction(sourceLine, battlerId, RECORDED_ACTION_TYPE, B_ACTION_USE_MOVE); + } + + if (!ctx.explicitAllowed || ctx.allowed) + { + PushBattlerAction(sourceLine, battlerId, RECORDED_MOVE_SLOT, moveSlot); + PushBattlerAction(sourceLine, battlerId, RECORDED_MOVE_TARGET, target); + } + + if (DATA.turnState == TURN_OPEN) + { + if (!DATA.hasExplicitSpeeds) + SetSlowerThan(battlerId); + + DATA.actionBattlers |= 1 << battlerId; + DATA.moveBattlers |= 1 << battlerId; + } + + // WARNING: Approximation. The move could still cause the RNG to + // advance. + if (gBattleMoves[moveId].accuracy != 0 + || gBattleMoves[moveId].split != SPLIT_STATUS) + { + DATA.hasRNGActions = TRUE; + } +} + +void ForcedMove(u32 sourceLine, struct BattlePokemon *battler) +{ + s32 battlerId = battler - gBattleMons; + INVALID_IF(DATA.turnState == TURN_CLOSED, "SKIP_TURN outside TURN"); + PushBattlerAction(sourceLine, battlerId, RECORDED_ACTION_TYPE, B_ACTION_USE_MOVE); + if (DATA.turnState == TURN_OPEN) + { + if (!DATA.hasExplicitSpeeds) + SetSlowerThan(battlerId); + + DATA.actionBattlers |= 1 << battlerId; + DATA.moveBattlers |= 1 << battlerId; + } +} + +void Switch(u32 sourceLine, struct BattlePokemon *battler, u32 partyIndex) +{ + s32 i; + s32 battlerId = battler - gBattleMons; + INVALID_IF(DATA.turnState == TURN_CLOSED, "SWITCH outside TURN"); + INVALID_IF(DATA.actionBattlers & (1 << battlerId), "Multiple battler actions"); + INVALID_IF(partyIndex >= ((battlerId & BIT_SIDE) == B_SIDE_PLAYER ? DATA.playerPartySize : DATA.opponentPartySize), "SWITCH to invalid party index"); + + for (i = 0; i < STATE->battlersCount; i++) + { + if (battlerId != i && (battlerId & BIT_SIDE) == (i & BIT_SIDE)) + INVALID_IF(DATA.currentMonIndexes[i] == partyIndex, "SWITCH to battler"); + } + + PushBattlerAction(sourceLine, battlerId, RECORDED_ACTION_TYPE, B_ACTION_SWITCH); + PushBattlerAction(sourceLine, battlerId, RECORDED_PARTY_INDEX, partyIndex); + DATA.currentMonIndexes[battlerId] = partyIndex; + + DATA.actionBattlers |= 1 << battlerId; +} + +void SkipTurn(u32 sourceLine, struct BattlePokemon *battler) +{ + s32 battlerId = battler - gBattleMons; + INVALID_IF(DATA.turnState == TURN_CLOSED, "SKIP_TURN outside TURN"); + DATA.actionBattlers |= 1 << battlerId; +} + +void SendOut(u32 sourceLine, struct BattlePokemon *battler, u32 partyIndex) +{ + s32 i; + s32 battlerId = battler - gBattleMons; + INVALID_IF(DATA.turnState == TURN_CLOSED, "SEND_OUT outside TURN"); + INVALID_IF(partyIndex >= ((battlerId & BIT_SIDE) == B_SIDE_PLAYER ? DATA.playerPartySize : DATA.opponentPartySize), "SWITCH to invalid party index"); + for (i = 0; i < STATE->battlersCount; i++) + { + if (battlerId != i && (battlerId & BIT_SIDE) == (i & BIT_SIDE)) + INVALID_IF(DATA.currentMonIndexes[i] == partyIndex, "SEND_OUT to battler"); + } + if (!(DATA.actionBattlers & (1 << battlerId))) + Move(sourceLine, battler, (struct MoveContext) { move: MOVE_CELEBRATE, explicitMove: TRUE }); + PushBattlerAction(sourceLine, battlerId, RECORDED_PARTY_INDEX, partyIndex); + DATA.currentMonIndexes[battlerId] = partyIndex; +} + +static const char *const sQueueGroupTypeMacros[] = +{ + [QUEUE_GROUP_NONE] = NULL, + [QUEUE_GROUP_ONE_OF] = "ONE_OF", + [QUEUE_GROUP_NONE_OF] = "NONE_OF", +}; + +void OpenQueueGroup(u32 sourceLine, enum QueueGroupType type) +{ + INVALID_IF(DATA.queueGroupType, "%s inside %s", sQueueGroupTypeMacros[type], sQueueGroupTypeMacros[DATA.queueGroupType]); + DATA.queueGroupType = type; + DATA.queueGroupStart = DATA.queuedEventsCount; +} + +void CloseQueueGroup(u32 sourceLine) +{ + u32 groupSize = DATA.queuedEventsCount - DATA.queueGroupStart; + if (groupSize > 0) + { + DATA.queuedEvents[DATA.queueGroupStart].groupType = DATA.queueGroupType; + DATA.queuedEvents[DATA.queueGroupStart].groupSize = groupSize; + DATA.queueGroupType = QUEUE_GROUP_NONE; + } +} + +void QueueAbility(u32 sourceLine, struct BattlePokemon *battler, struct AbilityEventContext ctx) +{ +#if B_ABILITY_POP_UP + s32 battlerId = battler - gBattleMons; + INVALID_IF(!STATE->runScene, "ABILITY_POPUP outside of SCENE"); + if (DATA.queuedEventsCount == MAX_QUEUED_EVENTS) + Test_ExitWithResult(TEST_RESULT_ERROR, "%s:%d: ABILITY exceeds MAX_QUEUED_EVENTS", gTestRunnerState.test->filename, sourceLine); + DATA.queuedEvents[DATA.queuedEventsCount++] = (struct QueuedEvent) { + .type = QUEUED_ABILITY_POPUP_EVENT, + .sourceLineOffset = SourceLineOffset(sourceLine), + .groupType = QUEUE_GROUP_NONE, + .groupSize = 1, + .as = { .ability = { + .battlerId = battlerId, + .ability = ctx.ability, + }}, + }; +#endif +} + +void QueueAnimation(u32 sourceLine, u32 type, u32 id, struct AnimationEventContext ctx) +{ + s32 attackerId, targetId; + + INVALID_IF(!STATE->runScene, "ANIMATION outside of SCENE"); + if (DATA.queuedEventsCount == MAX_QUEUED_EVENTS) + Test_ExitWithResult(TEST_RESULT_ERROR, "%s:%d: ANIMATION exceeds MAX_QUEUED_EVENTS", gTestRunnerState.test->filename, sourceLine); + + attackerId = ctx.attacker ? ctx.attacker - gBattleMons : 0xF; + if (type == ANIM_TYPE_MOVE) + { + targetId = ctx.target ? ctx.target - gBattleMons : 0xF; + } + else + { + INVALID_IF(ctx.target, "ANIMATION target set for non-ANIM_TYPE_MOVE"); + targetId = 0xF; + } + + DATA.queuedEvents[DATA.queuedEventsCount++] = (struct QueuedEvent) { + .type = QUEUED_ANIMATION_EVENT, + .sourceLineOffset = SourceLineOffset(sourceLine), + .groupType = QUEUE_GROUP_NONE, + .groupSize = 1, + .as = { .animation = { + .type = type, + .id = id, + .attacker = attackerId, + .target = targetId, + }}, + }; +} + +void QueueHP(u32 sourceLine, struct BattlePokemon *battler, struct HPEventContext ctx) +{ + s32 battlerId = battler - gBattleMons; + u32 type; + uintptr_t address; + + INVALID_IF(!STATE->runScene, "HP_BAR outside of SCENE"); + if (DATA.queuedEventsCount == MAX_QUEUED_EVENTS) + Test_ExitWithResult(TEST_RESULT_ERROR, "%s:%d: HP_BAR exceeds MAX_QUEUED_EVENTS", gTestRunnerState.test->filename, sourceLine); + + if (ctx.explicitHP) + { + type = HP_EVENT_NEW_HP; + address = (u16)ctx.hp; + } + else if (ctx.explicitDamage) + { + INVALID_IF(ctx.damage == 0, "damage is 0"); + type = HP_EVENT_DELTA_HP; + address = (u16)ctx.damage; + } + else if (ctx.explicitCaptureHP) + { + INVALID_IF(ctx.captureHP == NULL, "captureHP is NULL"); + type = HP_EVENT_NEW_HP; + address = (uintptr_t)ctx.captureHP; + } + else if (ctx.explicitCaptureDamage) + { + INVALID_IF(ctx.captureDamage == NULL, "captureDamage is NULL"); + type = HP_EVENT_DELTA_HP; + *ctx.captureDamage = 0; + address = (uintptr_t)ctx.captureDamage; + } + else + { + type = HP_EVENT_DELTA_HP; + address = 0; + } + + DATA.queuedEvents[DATA.queuedEventsCount++] = (struct QueuedEvent) { + .type = QUEUED_HP_EVENT, + .sourceLineOffset = SourceLineOffset(sourceLine), + .groupType = QUEUE_GROUP_NONE, + .groupSize = 1, + .as = { .hp = { + .battlerId = battlerId, + .type = type, + .address = address, + }}, + }; +} + +void QueueMessage(u32 sourceLine, const u8 *pattern) +{ + INVALID_IF(!STATE->runScene, "MESSAGE outside of SCENE"); + if (DATA.queuedEventsCount == MAX_QUEUED_EVENTS) + Test_ExitWithResult(TEST_RESULT_ERROR, "%s:%d: MESSAGE exceeds MAX_QUEUED_EVENTS", gTestRunnerState.test->filename, sourceLine); + DATA.queuedEvents[DATA.queuedEventsCount++] = (struct QueuedEvent) { + .type = QUEUED_MESSAGE_EVENT, + .sourceLineOffset = SourceLineOffset(sourceLine), + .groupType = QUEUE_GROUP_NONE, + .groupSize = 1, + .as = { .message = { + .pattern = pattern, + }}, + }; +} + + +void QueueStatus(u32 sourceLine, struct BattlePokemon *battler, struct StatusEventContext ctx) +{ + s32 battlerId = battler - gBattleMons; + u32 mask; + + INVALID_IF(!STATE->runScene, "STATUS_ICON outside of SCENE"); + if (DATA.queuedEventsCount == MAX_QUEUED_EVENTS) + Test_ExitWithResult(TEST_RESULT_ERROR, "%s:%d: STATUS_ICON exceeds MAX_QUEUED_EVENTS", gTestRunnerState.test->filename, sourceLine); + + if (ctx.none) + mask = 0; + else if (ctx.sleep) + mask = STATUS1_SLEEP; + else if (ctx.poison) + mask = STATUS1_POISON; + else if (ctx.burn) + mask = STATUS1_BURN; + else if (ctx.freeze) + mask = STATUS1_FREEZE; + else if (ctx.paralysis) + mask = STATUS1_PARALYSIS; + else if (ctx.badPoison) + mask = STATUS1_TOXIC_POISON; + else + mask = ctx.status1; + + DATA.queuedEvents[DATA.queuedEventsCount++] = (struct QueuedEvent) { + .type = QUEUED_STATUS_EVENT, + .sourceLineOffset = SourceLineOffset(sourceLine), + .groupType = QUEUE_GROUP_NONE, + .groupSize = 1, + .as = { .status = { + .battlerId = battlerId, + .mask = mask, + }}, + }; +} diff --git a/tools/mgba-rom-test-hydra/.gitignore b/tools/mgba-rom-test-hydra/.gitignore new file mode 100644 index 000000000..b3b89034b --- /dev/null +++ b/tools/mgba-rom-test-hydra/.gitignore @@ -0,0 +1 @@ +mgba-rom-test-hydra diff --git a/tools/mgba-rom-test-hydra/Makefile b/tools/mgba-rom-test-hydra/Makefile new file mode 100644 index 000000000..5f33f001b --- /dev/null +++ b/tools/mgba-rom-test-hydra/Makefile @@ -0,0 +1,18 @@ +.PHONY: all clean + +SRCS = main.c + +ifeq ($(OS),Windows_NT) +EXE := .exe +else +EXE := +endif + +all: mgba-rom-test-hydra$(EXE) + @: + +mgba-rom-test-hydra$(EXE): $(SRCS) + $(CC) $(SRCS) -o $@ $(LDFLAGS) + +clean: + $(RM) mgba-rom-test-hydra$(EXE) diff --git a/tools/mgba-rom-test-hydra/main.c b/tools/mgba-rom-test-hydra/main.c new file mode 100644 index 000000000..f9c51d505 --- /dev/null +++ b/tools/mgba-rom-test-hydra/main.c @@ -0,0 +1,421 @@ +/* mgba-rom-test-hydra. Runs multiple mgba-rom-test processes and + * parses the output to display human-readable progress. + * + * Output lines starting with "GBA Debug: :" are parsed as commands to + * Hydra, other output lines starting with "GBA Debug: " are parsed as + * output from the current test, and any other lines are parsed as + * output from the mgba-rom-test process itself. + * + * COMMANDS + * N: Sets the test name to the remainder of the line. + * R: Sets the result to the remainder of the line, and flushes any + * output buffered since the previous R. */ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#define MAX_PROCESSES 32 // See also test/test.h + +struct Runner +{ + pid_t pid; + int outfd; + char rom_path[L_tmpnam]; + char test_name[256]; + size_t input_buffer_size; + size_t input_buffer_capacity; + char *input_buffer; + size_t output_buffer_size; + size_t output_buffer_capacity; + char *output_buffer; +}; + +static unsigned nrunners = 0; +static struct Runner *runners = NULL; + +static void handle_read(struct Runner *runner) +{ + char *sol = runner->input_buffer; + char *eol; + size_t consumed = 0; + size_t remaining = runner->input_buffer_size; + while ((eol = memchr(sol, '\n', remaining))) + { + eol++; + size_t n = eol - sol; + if (runner->input_buffer_size >= strlen("GBA Debug: ") + && !strncmp(sol, "GBA Debug: ", strlen("GBA Debug: "))) + { + char *soc = sol + strlen("GBA Debug: "); + if (soc[0] == ':') + { + switch (soc[1]) + { + case 'N': + soc += 2; + if (sizeof(runner->test_name) <= eol - soc - 1) + { + fprintf(stderr, "test_name too long\n"); + exit(2); + } + strncpy(runner->test_name, soc, eol - soc - 1); + runner->test_name[eol - soc - 1] = '\0'; + break; + + case 'R': + soc += 2; + fprintf(stdout, "%s: ", runner->test_name); + fwrite(soc, 1, eol - soc, stdout); + fwrite(runner->output_buffer, 1, runner->output_buffer_size, stdout); + strcpy(runner->test_name, "WAITING..."); + runner->output_buffer_size = 0; + break; + + default: + goto buffer_output; + } + } + else + { +buffer_output: + if (runner->output_buffer_size + eol - soc >= runner->output_buffer_capacity) + { + runner->output_buffer_capacity *= 2; + if (runner->output_buffer_capacity < runner->output_buffer_size + eol - soc) + runner->output_buffer_capacity = runner->output_buffer_size + eol - soc; + runner->output_buffer = realloc(runner->output_buffer, runner->output_buffer_capacity); + if (!runner->output_buffer) + { + perror("realloc output_buffer failed"); + exit(2); + } + } + memcpy(runner->output_buffer + runner->output_buffer_size, soc, eol - soc); + runner->output_buffer_size += eol - soc; + } + } + else + { + if (write(STDOUT_FILENO, sol, eol - sol) == -1) + { + perror("write failed"); + exit(2); + } + } + sol += n; + consumed += n; + remaining -= n; + } + + memcpy(runner->input_buffer, sol, remaining); + runner->input_buffer_size -= consumed; + + if (runner->input_buffer_size == runner->input_buffer_capacity) + { + runner->input_buffer_capacity *= 2; + runner->input_buffer = realloc(runner->input_buffer, runner->input_buffer_capacity); + if (!runner->input_buffer) + { + perror("realloc input_buffer failed"); + exit(2); + } + } +} + +static void unlink_roms(void) +{ + for (int i = 0; i < nrunners; i++) + { + if (runners[i].rom_path[0]) + { + if (unlink(runners[i].rom_path) == -1) + perror("unlink rom_path failed"); + } + } +} + +static void exit2(int _) +{ + exit(2); +} + +int main(int argc, char *argv[]) +{ + if (argc < 3) + { + fprintf(stderr, "usage %s mgba-rom-test rom\n", argv[0]); + exit(2); + } + + bool tty = isatty(STDOUT_FILENO); + if (!tty) + { + const char *v = getenv("MAKE_TERMOUT"); + tty = v && v[0] == '\0'; + } + + if (tty) + { + char *stdout_buffer = malloc(64 * 1024); + if (!stdout_buffer) + { + perror("malloc stdout_buffer failed"); + exit(2); + } + setvbuf(stdout, stdout_buffer, _IOFBF, 64 * 1024); + } + else + { + setvbuf(stdout, NULL, _IONBF, 0); + } + + int elffd; + if ((elffd = open(argv[2], O_RDONLY)) == -1) + { + perror("open elffd failed"); + exit(2); + } + + struct stat elfst; + if (fstat(elffd, &elfst) == -1) + { + perror("stat elffd failed"); + exit(2); + } + + void *elf; + if ((elf = mmap(NULL, elfst.st_size, PROT_READ, MAP_PRIVATE, elffd, 0)) == MAP_FAILED) + { + perror("mmap elffd failed"); + exit(2); + } + + nrunners = sysconf(_SC_NPROCESSORS_ONLN); + if (nrunners > MAX_PROCESSES) + nrunners = MAX_PROCESSES; + runners = calloc(nrunners, sizeof(*runners)); + if (!runners) + { + perror("calloc runners failed"); + exit(2); + } + for (int i = 0; i < nrunners; i++) + { + runners[i].input_buffer_capacity = 4096; + runners[i].input_buffer = malloc(runners[i].input_buffer_capacity); + runners[i].output_buffer_capacity = 4096; + runners[i].output_buffer = malloc(runners[i].output_buffer_capacity); + strcpy(runners[i].test_name, "WAITING..."); + if (tty) + fprintf(stdout, "%s\n", runners[i].test_name); + } + fflush(stdout); + atexit(unlink_roms); + signal(SIGINT, exit2); + signal(SIGTERM, exit2); + + // Start test runners. + pid_t parent_pid = getpid(); + for (int i = 0; i < nrunners; i++) + { + int pipefds[2]; + if (pipe(pipefds) == -1) + { + perror("pipe failed"); + exit(2); + } + if (!tmpnam(runners[i].rom_path)) + { + perror("tmpnam rom_path failed"); + exit(2); + } + int tmpfd; + if ((tmpfd = open(runners[i].rom_path, O_WRONLY | O_CREAT | O_EXCL, S_IRUSR | S_IWUSR)) == -1) + { + perror("open tmpfd failed"); + _exit(2); + } + if ((write(tmpfd, elf, elfst.st_size)) == -1) + { + perror("write tmpfd failed"); + _exit(2); + } + pid_t patchelfpid = fork(); + if (patchelfpid == -1) + { + perror("fork patchelf failed"); + _exit(2); + } + else if (patchelfpid == 0) + { + char n_arg[5], i_arg[5]; + snprintf(n_arg, sizeof(n_arg), "\\x%02x", nrunners); + snprintf(i_arg, sizeof(i_arg), "\\x%02x", i); + if (execlp("tools/patchelf/patchelf", "tools/patchelf/patchelf", runners[i].rom_path, "gTestRunnerN", n_arg, "gTestRunnerI", i_arg, NULL) == -1) + { + perror("execlp patchelf failed"); + _exit(2); + } + } + else + { + int wstatus; + if (waitpid(patchelfpid, &wstatus, 0) == -1) + { + perror("waitpid patchelfpid failed"); + _exit(2); + } + if (!WIFEXITED(wstatus) || WEXITSTATUS(wstatus) != 0) + { + fprintf(stderr, "patchelf exited with an error\n"); + _exit(2); + } + } + pid_t pid = fork(); + if (pid == -1) { + perror("fork mgba-rom-test failed"); + exit(2); + } else if (pid == 0) { + if (prctl(PR_SET_PDEATHSIG, SIGTERM) == -1) + { + perror("prctl failed"); + _exit(2); + } + if (getppid() != parent_pid) // Parent died. + { + _exit(2); + } + if (close(pipefds[0]) == -1) + { + perror("close pipefds[0] failed"); + _exit(2); + } + if (dup2(pipefds[1], STDOUT_FILENO) == -1) + { + perror("dup2 stdout failed"); + _exit(2); + } + if (close(pipefds[1]) == -1) + { + perror("close pipefds[1] failed"); + _exit(2); + } + // stdbuf is required because otherwise mgba never flushes + // stdout. + if (execlp("stdbuf", "stdbuf", "-oL", argv[1], "-l15", "-ClogLevel.gba.dma=16", "-Rr0", runners[i].rom_path, NULL) == -1) + { + perror("execl stdbuf mgba-rom-test failed"); + _exit(2); + } + } else { + runners[i].pid = pid; + runners[i].outfd = pipefds[0]; + if (close(pipefds[1]) == -1) + { + perror("close pipefds[1] failed"); + exit(2); + } + } + } + + // Process test runner output. + int openfds = nrunners; + struct pollfd *pollfds = calloc(nrunners, sizeof(*pollfds)); + if (!pollfds) + { + perror("calloc pollfds failed"); + exit(2); + } + for (int i = 0; i < nrunners; i++) + { + pollfds[i].fd = runners[i].outfd; + pollfds[i].events = POLLIN; + } + while (openfds > 0) + { + if (tty) + { + struct winsize winsize; + if (ioctl(STDOUT_FILENO, TIOCGWINSZ, &winsize) == -1) + { + perror("ioctl TIOCGWINSZ failed"); + exit(2); + } + int scrollback = 0; + for (int i = 0; i < nrunners; i++) + { + if (runners[i].outfd >= 0) + scrollback += (strlen(runners[i].test_name) + winsize.ws_col - 1) / winsize.ws_col; + } + if (scrollback > 0) + fprintf(stdout, "\e[%dF\e[J", scrollback); + } + + if (poll(pollfds, nrunners, -1) == -1) + { + perror("poll failed"); + exit(2); + } + for (int i = 0; i < nrunners; i++) + { + if (pollfds[i].revents & POLLIN) + { + int n; + if ((n = read(pollfds[i].fd, runners[i].input_buffer + runners[i].input_buffer_size, runners[i].input_buffer_capacity - runners[i].input_buffer_size)) == -1) + { + perror("read pollfds[i] failed"); + exit(2); + } + runners[i].input_buffer_size += n; + handle_read(&runners[i]); + } + + if (pollfds[i].revents & (POLLERR | POLLHUP)) + { + if (close(pollfds[i].fd) == -1) + { + perror("close pollfds[i] failed"); + exit(2); + } + runners[i].outfd = pollfds[i].fd = -pollfds[i].fd; + openfds--; + } + } + + if (tty) + { + for (int i = 0; i < nrunners; i++) + { + if (runners[i].outfd >= 0) + fprintf(stdout, "%s\n", runners[i].test_name); + } + + fflush(stdout); + } + } + + // Reap test runners and collate exit codes. + int exit_code = 0; + for (int i = 0; i < nrunners; i++) + { + int wstatus; + if (waitpid(runners[i].pid, &wstatus, 0) == -1) + { + perror("waitpid runners[i] failed"); + exit(2); + } + if (WIFEXITED(wstatus) && WEXITSTATUS(wstatus) > exit_code) + exit_code = WEXITSTATUS(wstatus); + } + return exit_code; +} diff --git a/tools/mgba/README.md b/tools/mgba/README.md new file mode 100644 index 000000000..617e6e058 --- /dev/null +++ b/tools/mgba/README.md @@ -0,0 +1,7 @@ +# mGBA + +The binaries in this folder are built from `mGBA`, an emulator for running Game Boy Advance games. The source code is available here: . +The source code for these specific builds is available from: + + - Windows: + - Linux: diff --git a/tools/mgba/mgba-rom-test b/tools/mgba/mgba-rom-test new file mode 100755 index 000000000..09c683a2f Binary files /dev/null and b/tools/mgba/mgba-rom-test differ diff --git a/tools/mgba/mgba-rom-test.exe b/tools/mgba/mgba-rom-test.exe new file mode 100755 index 000000000..5b25e16f6 Binary files /dev/null and b/tools/mgba/mgba-rom-test.exe differ diff --git a/tools/patchelf/.gitignore b/tools/patchelf/.gitignore new file mode 100644 index 000000000..fca468008 --- /dev/null +++ b/tools/patchelf/.gitignore @@ -0,0 +1 @@ +patchelf diff --git a/tools/patchelf/Makefile b/tools/patchelf/Makefile new file mode 100644 index 000000000..4e60bd631 --- /dev/null +++ b/tools/patchelf/Makefile @@ -0,0 +1,18 @@ +.PHONY: all clean + +SRCS = patchelf.c + +ifeq ($(OS),Windows_NT) +EXE := .exe +else +EXE := +endif + +all: patchelf$(EXE) + @: + +patchelf$(EXE): $(SRCS) + $(CC) $(SRCS) -o $@ $(LDFLAGS) + +clean: + $(RM) patchelf$(EXE) diff --git a/tools/patchelf/patchelf.c b/tools/patchelf/patchelf.c new file mode 100644 index 000000000..19363c55f --- /dev/null +++ b/tools/patchelf/patchelf.c @@ -0,0 +1,191 @@ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +static bool try_patch_value(const char *sym, char *dest, const char *source, size_t size); + +int main(int argc, char *argv[]) +{ + int exit_code = 1; + + int fd = -1; + char *f = MAP_FAILED; + + if (argc < 2 || argc % 2 != 0) + { + fprintf(stderr, "Usage: %s [ ]...\n", argv[0]); + goto error; + } + + if ((fd = open(argv[1], O_RDWR)) == -1) + { + fprintf(stderr, "open(%s, O_RDWR) failed: %s\n", argv[1], strerror(errno)); + goto error; + } + + struct stat st; + if (fstat(fd, &st) == -1) + { + perror("stat failed"); + goto error; + } + + if ((f = mmap(NULL, st.st_size, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0)) == MAP_FAILED) + { + perror("mmap failed"); + goto error; + } + + if (memcmp(f, ELFMAG, 4) != 0) + { + fprintf(stderr, "not an ELF file\n"); + goto error; + } + + const Elf32_Ehdr *ehdr = (Elf32_Ehdr *)f; + const Elf32_Shdr *shdrs = (Elf32_Shdr *)(f + ehdr->e_shoff); + + if (ehdr->e_shstrndx == SHN_UNDEF) + { + fprintf(stderr, "no section name string table\n"); + goto error; + } + const Elf32_Shdr *shdr_shstr = &shdrs[ehdr->e_shstrndx]; + const char *shstr = (const char *)(f + shdr_shstr->sh_offset); + const Elf32_Shdr *shdr_symtab = NULL; + const Elf32_Shdr *shdr_strtab = NULL; + for (int i = 0; i < ehdr->e_shnum; i++) + { + const char *sh_name = shstr + shdrs[i].sh_name; + if (strcmp(sh_name, ".symtab") == 0) + shdr_symtab = &shdrs[i]; + else if (strcmp(sh_name, ".strtab") == 0) + shdr_strtab = &shdrs[i]; + } + if (!shdr_symtab) + { + fprintf(stderr, "no .symtab section\n"); + goto error; + } + if (!shdr_strtab) + { + fprintf(stderr, "no .strtab section\n"); + goto error; + } + + const Elf32_Sym *symtab = (Elf32_Sym *)(f + shdr_symtab->sh_offset); + const char *strtab = (const char *)(f + shdr_strtab->sh_offset); + for (int i = 0; i < shdr_symtab->sh_size / shdr_symtab->sh_entsize; i++) + { + if (symtab[i].st_name == 0) continue; + if (symtab[i].st_shndx > ehdr->e_shnum) continue; + const char *st_name = strtab + symtab[i].st_name; + const Elf32_Shdr *shdr = &shdrs[symtab[i].st_shndx]; + uint32_t sym_offset = symtab[i].st_value - shdr->sh_addr; + for (int j = 2; j < argc; j += 2) + { + const char *arg_name = argv[j + 0]; + const char *arg_value = argv[j + 1]; + if (strcmp(st_name, arg_name) == 0) + { + char *value = (char *)(f + shdr->sh_offset + sym_offset); + if (!try_patch_value(st_name, value, arg_value, symtab[i].st_size)) + goto error; + } + } + } + + exit_code = 0; + +error: + if (f != MAP_FAILED) + { + if (msync(f, st.st_size, MS_SYNC) == -1) + { + perror("msync failed"); + f = MAP_FAILED; + } + } + if (f != MAP_FAILED) + { + if (munmap(f, st.st_size) == -1) + { + perror("munmap failed"); + } + } + if (fd != -1) + { + if (close(fd) == -1) + { + perror("close failed"); + } + } + + return exit_code; +} + +static int parsexdigit(char c) +{ + if ('0' <= c && c <= '9') + return c - '0'; + else if ('a' <= c && c <= 'f') + return c - 'a' + 10; + else if ('A' <= c && c <= 'F') + return c - 'A' + 10; + else + return -1; +} + +static bool try_patch_value(const char *sym, char *dest, const char *source, size_t size) +{ + int i = 0; + while (*source) + { + if (i == size) + { + fprintf(stderr, "%s: overflows size (%lu)\n", sym, size); + return false; + } + char c, value; + switch ((c = *source++)) + { + case '\\': + switch ((c = *source++)) + { + case '0': + value = 0; + break; + case 'x': + if (!isxdigit((c = *source++))) + { + fprintf(stderr, "%s: illegal escape \\x%c\n", sym, c); + return false; + } + value = parsexdigit(c); + if (!isxdigit((c = *source++))) + { + fprintf(stderr, "%s: illegal escape \\x%c%c\n", sym, *(source - 2), c); + return false; + } + value = value * 16 + parsexdigit(c); + break; + default: + fprintf(stderr, "%s: illegal escape \\%c\n", sym, c); + return false; + } + break; + default: + value = c; + break; + } + dest[i++] = value; + } + return true; +}