From c6221fa50ea7d5c8ca5b577f1cdca26c9878333b Mon Sep 17 00:00:00 2001 From: Bivurnum <147376167+Bivurnum@users.noreply.github.com> Date: Wed, 25 Jun 2025 14:13:43 -0500 Subject: [PATCH] Add debug options for follower NPCs (#7215) --- data/scripts/debug.inc | 18 +++++++ include/follower_npc.h | 2 + src/debug.c | 108 ++++++++++++++++++++++++++++++++++++++++- src/follower_npc.c | 101 ++++++++++++++++++++------------------ 4 files changed, 181 insertions(+), 48 deletions(-) diff --git a/data/scripts/debug.inc b/data/scripts/debug.inc index d82ce2f358..05eeb4ecaf 100644 --- a/data/scripts/debug.inc +++ b/data/scripts/debug.inc @@ -270,6 +270,24 @@ Debug_ShowExpansionVersion:: Debug_ExpansionVersion: .string "pokeemerald-expansion {STR_VAR_1}$" +Debug_Follower_NPC_Not_Enabled:: + msgbox Debug_Enable_To_Use_Follower_NPCs, MSGBOX_DEFAULT + release + end + +Debug_Enable_To_Use_Follower_NPCs: + .string "Feature unavailable.\nSet FNPC_ENABLE_NPC_FOLLOWERS to\lTRUE in 'include/config/follower_npc.h'.$" + +Debug_Follower_NPC_Event_Script:: + lock + facefollowernpc + msgbox Debug_Follower_NPC_Test_Message, MSGBOX_DEFAULT + releaseall + end + +Debug_Follower_NPC_Test_Message: + .string "This is a Follower NPC test.$" + Debug_EventScript_Steven_Multi:: call MossdeepCity_SpaceCenter_2F_EventScript_ChoosePartyForMultiBattle release diff --git a/include/follower_npc.h b/include/follower_npc.h index ec41fa1364..a9be81d62c 100644 --- a/include/follower_npc.h +++ b/include/follower_npc.h @@ -82,6 +82,8 @@ const u8 *GetFollowerNPCScriptPointer(void); u32 GetFollowerNPCData(enum FollowerNPCDataTypes type); void ClearFollowerNPCData(void); +void CreateFollowerNPC(u32 gfx, u32 followerFlags, const u8 *scriptPtr); +void DestroyFollowerNPC(void); u32 DetermineFollowerNPCState(struct ObjectEvent *follower, u32 state, u32 direction); void SetFollowerNPCSprite(u32 spriteIndex); diff --git a/src/debug.c b/src/debug.c index df604413b9..bc2b56aec1 100644 --- a/src/debug.c +++ b/src/debug.c @@ -23,6 +23,7 @@ #include "field_message_box.h" #include "field_screen_effect.h" #include "field_weather.h" +#include "follower_npc.h" #include "international_string_util.h" #include "item.h" #include "item_icon.h" @@ -58,6 +59,7 @@ #include "constants/battle_ai.h" #include "constants/battle_frontier.h" #include "constants/coins.h" +#include "constants/event_objects.h" #include "constants/expansion.h" #include "constants/flags.h" #include "constants/items.h" @@ -71,6 +73,17 @@ #include "fake_rtc.h" #include "save.h" +enum FollowerNPCCreateDebugMenu +{ + DEBUG_FNPC_BRENDAN, + DEBUG_FNPC_MAY, + DEBUG_FNPC_STEVEN, + DEBUG_FNPC_WALLY, + DEBUG_FNPC_RED, + DEBUG_FNPC_LEAF, + DEBUG_FNPC_COUNT, +}; + enum FlagsVarsDebugMenu { DEBUG_FLAGVAR_MENU_ITEM_FLAGS, @@ -223,6 +236,7 @@ static void Debug_RefreshListMenu(u8 taskId); static void DebugAction_OpenSubMenu(u8 taskId, const struct DebugMenuOption *items); static void DebugAction_OpenSubMenuFlagsVars(u8 taskId); static void DebugAction_OpenSubMenuFakeRTC(u8 taskId, const struct DebugMenuOption *items); +static void DebugAction_OpenSubMenuCreateFollowerNPC(u8 taskId, const struct DebugMenuOption *items); static void DebugAction_ExecuteScript(u8 taskId, const u8 *script); static void DebugTask_HandleMenuInput_General(u8 taskId); @@ -241,6 +255,9 @@ static void DebugAction_Util_CheatStart(u8 taskId); static void DebugAction_TimeMenu_ChangeTimeOfDay(u8 taskId); static void DebugAction_TimeMenu_ChangeWeekdays(u8 taskId); +static void DebugAction_CreateFollowerNPC(u8 taskId); +static void DebugAction_DestroyFollowerNPC(u8 taskId); + static void DebugAction_PCBag_Fill_PCBoxes_Fast(u8 taskId); static void DebugAction_PCBag_Fill_PCBoxes_Slow(u8 taskId); static void DebugAction_PCBag_Fill_PCItemStorage(u8 taskId); @@ -345,6 +362,8 @@ extern const u8 Debug_CheckROMSpace[]; extern const u8 Debug_BoxFilledMessage[]; extern const u8 Debug_ShowExpansionVersion[]; extern const u8 Debug_EventScript_EWRAMCounters[]; +extern const u8 Debug_Follower_NPC_Event_Script[]; +extern const u8 Debug_Follower_NPC_Not_Enabled[]; extern const u8 Debug_EventScript_Steven_Multi[]; extern const u8 Debug_EventScript_PrintTimeOfDay[]; extern const u8 Debug_EventScript_TellTheTime[]; @@ -393,6 +412,17 @@ static const u8 *const gTimeOfDayStringsTable[TIMES_OF_DAY_COUNT] = { COMPOUND_STRING("Night"), }; +// Follower NPC + +static const u8 *const gFollowerNPCStringsTable[DEBUG_FNPC_COUNT] = { + COMPOUND_STRING("Brendan"), + COMPOUND_STRING("May"), + COMPOUND_STRING("Steven"), + COMPOUND_STRING("Wally"), + COMPOUND_STRING("Red"), + COMPOUND_STRING("Leaf"), +}; + // Flags/Vars Menu static const u8 sDebugText_FlagsVars_VariableHex[] = _("{STR_VAR_1}{CLEAR_TO 90}\n0x{STR_VAR_2}{CLEAR_TO 90}"); static const u8 sDebugText_FlagsVars_Variable[] = _("Var: {STR_VAR_1}{CLEAR_TO 90}\nVal: {STR_VAR_3}{CLEAR_TO 90}\n{STR_VAR_2}"); @@ -454,6 +484,17 @@ static const struct DebugMenuOption sDebugMenu_Actions_TimeMenu_Weekdays[] = { NULL } }; +static const struct DebugMenuOption sDebugMenu_Actions_FollowerNPCMenu_Create[] = +{ + [DEBUG_FNPC_BRENDAN] = { gFollowerNPCStringsTable[DEBUG_FNPC_BRENDAN], DebugAction_CreateFollowerNPC }, + [DEBUG_FNPC_MAY] = { gFollowerNPCStringsTable[DEBUG_FNPC_MAY], DebugAction_CreateFollowerNPC }, + [DEBUG_FNPC_STEVEN] = { gFollowerNPCStringsTable[DEBUG_FNPC_STEVEN], DebugAction_CreateFollowerNPC }, + [DEBUG_FNPC_WALLY] = { gFollowerNPCStringsTable[DEBUG_FNPC_WALLY], DebugAction_CreateFollowerNPC }, + [DEBUG_FNPC_RED] = { gFollowerNPCStringsTable[DEBUG_FNPC_RED], DebugAction_CreateFollowerNPC }, + [DEBUG_FNPC_LEAF] = { gFollowerNPCStringsTable[DEBUG_FNPC_LEAF], DebugAction_CreateFollowerNPC }, + { NULL } +}; + static const struct DebugMenuOption sDebugMenu_Actions_TimeMenu[] = { { COMPOUND_STRING("Get time…"), DebugAction_ExecuteScript, Debug_EventScript_TellTheTime }, @@ -475,6 +516,13 @@ static const struct DebugMenuOption sDebugMenu_Actions_BerryFunctions[] = { NULL } }; +static const struct DebugMenuOption sDebugMenu_Actions_FollowerNPCMenu[] = +{ + { COMPOUND_STRING("Create Follower"), DebugAction_OpenSubMenuCreateFollowerNPC, sDebugMenu_Actions_FollowerNPCMenu_Create }, + { COMPOUND_STRING("Destroy Follower"), DebugAction_DestroyFollowerNPC }, + { NULL } +}; + static const struct DebugMenuOption sDebugMenu_Actions_Utilities[] = { { COMPOUND_STRING("Fly to map…"), DebugAction_Util_Fly }, @@ -486,6 +534,7 @@ static const struct DebugMenuOption sDebugMenu_Actions_Utilities[] = { COMPOUND_STRING("Cheat start"), DebugAction_Util_CheatStart }, { COMPOUND_STRING("Berry Functions…"), DebugAction_OpenSubMenu, sDebugMenu_Actions_BerryFunctions }, { COMPOUND_STRING("EWRAM Counters…"), DebugAction_ExecuteScript, Debug_EventScript_EWRAMCounters }, + { COMPOUND_STRING("Follower NPC…"), DebugAction_OpenSubMenu, sDebugMenu_Actions_FollowerNPCMenu }, { COMPOUND_STRING("Steven Multi"), DebugAction_ExecuteScript, Debug_EventScript_Steven_Multi }, { NULL } }; @@ -766,7 +815,8 @@ static bool32 IsSubMenuAction(const void *action) { return action == DebugAction_OpenSubMenu || action == DebugAction_OpenSubMenuFlagsVars - || action == DebugAction_OpenSubMenuFakeRTC; + || action == DebugAction_OpenSubMenuFakeRTC + || action == DebugAction_OpenSubMenuCreateFollowerNPC; } static void Debug_ShowMenu(DebugFunc HandleInput, const struct DebugMenuOption *items) @@ -1206,6 +1256,19 @@ static void DebugAction_ExecuteScript(u8 taskId, const u8 *script) Debug_DestroyMenu_Full_Script(taskId, script); } +static void DebugAction_OpenSubMenuCreateFollowerNPC(u8 taskId, const struct DebugMenuOption *items) +{ + if (FNPC_ENABLE_NPC_FOLLOWERS) + { + Debug_DestroyMenu(taskId); + Debug_ShowMenu(DebugTask_HandleMenuInput_General, items); + } + else + { + Debug_DestroyMenu_Full_Script(taskId, Debug_Follower_NPC_Not_Enabled); + } +} + // ******************************* // Actions Utilities @@ -3272,6 +3335,49 @@ static void DebugAction_Sound_MUS_SelectId(u8 taskId) } } +static const u32 gDebugFollowerNPCGraphics[] = +{ + OBJ_EVENT_GFX_RIVAL_BRENDAN_NORMAL, + OBJ_EVENT_GFX_RIVAL_MAY_NORMAL, + OBJ_EVENT_GFX_STEVEN, + OBJ_EVENT_GFX_WALLY, + OBJ_EVENT_GFX_RED, + OBJ_EVENT_GFX_LEAF, +}; + +static void DebugAction_CreateFollowerNPC(u8 taskId) +{ + u32 input = ListMenu_ProcessInput(gTasks[taskId].tMenuTaskId); + u32 gfx = gDebugFollowerNPCGraphics[input]; + + Debug_DestroyMenu_Full(taskId); + LockPlayerFieldControls(); + DestroyFollowerNPC(); + SetFollowerNPCData(FNPC_DATA_BATTLE_PARTNER, PARTNER_STEVEN); + CreateFollowerNPC(gfx, FNPC_ALL, Debug_Follower_NPC_Event_Script); + if (gfx != OBJ_EVENT_GFX_RIVAL_BRENDAN_NORMAL && gfx != OBJ_EVENT_GFX_RIVAL_MAY_NORMAL) + { + u32 flags = GetFollowerNPCData(FNPC_DATA_FOLLOWER_FLAGS); + SetFollowerNPCData(FNPC_DATA_FOLLOWER_FLAGS, (flags &= ~FNPC_RUNNING)); + } + UnlockPlayerFieldControls(); +} + +static void DebugAction_DestroyFollowerNPC(u8 taskId) +{ + if (FNPC_ENABLE_NPC_FOLLOWERS) + { + Debug_DestroyMenu_Full(taskId); + LockPlayerFieldControls(); + DestroyFollowerNPC(); + UnlockPlayerFieldControls(); + } + else + { + Debug_DestroyMenu_Full_Script(taskId, Debug_Follower_NPC_Not_Enabled); + } +} + #undef tCurrentSong #undef tMenuTaskId diff --git a/src/follower_npc.c b/src/follower_npc.c index 67129548ac..0b2a4b70b3 100644 --- a/src/follower_npc.c +++ b/src/follower_npc.c @@ -49,7 +49,6 @@ static void SetFollowerNPCScriptPointer(const u8 *script); static void PlayerLogCoordinates(struct ObjectEvent *player); static void TurnNPCIntoFollower(u32 localId, u32 followerFlags, u32 setScript, const u8 *script); -static void CreateFollowerNPC(u32 gfx, u32 followerFlags, const u8 *scriptPtr); static u32 GetFollowerNPCSprite(void); static bool32 IsStateMovement(u32 state); static u32 GetPlayerFaceToDoorDirection(struct ObjectEvent *player, struct ObjectEvent *follower); @@ -227,45 +226,6 @@ static void TurnNPCIntoFollower(u32 localId, u32 followerFlags, u32 setScript, c FollowerNPC_HandleSprite(); } -static void CreateFollowerNPC(u32 gfx, u32 followerFlags, const u8 *scriptPtr) -{ - struct ObjectEvent *player = &gObjectEvents[gPlayerAvatar.objectEventId]; - struct ObjectEvent *follower; - struct ObjectEventTemplate npc = - { - .localId = OBJ_EVENT_ID_NPC_FOLLOWER, - .graphicsId = gfx, - .x = player->currentCoords.x, - .y = player->currentCoords.y, - .elevation = PlayerGetElevation(), - .script = scriptPtr - }; - - SetFollowerNPCData(FNPC_DATA_OBJ_ID, TrySpawnObjectEventTemplate(&npc, gSaveBlock1Ptr->location.mapNum, gSaveBlock1Ptr->location.mapGroup, player->currentCoords.x, player->currentCoords.y)); - follower = &gObjectEvents[GetFollowerNPCData(FNPC_DATA_OBJ_ID)]; - follower->movementType = MOVEMENT_TYPE_NONE; - gSprites[follower->spriteId].callback = MovementType_None; - - SetFollowerNPCData(FNPC_DATA_IN_PROGRESS, TRUE); - SetFollowerNPCData(FNPC_DATA_GFX_ID, follower->graphicsId); - SetFollowerNPCData(FNPC_DATA_FOLLOWER_FLAGS, followerFlags); - SetFollowerNPCData(FNPC_DATA_SURF_BLOB, FNPC_SURF_BLOB_NONE); - SetFollowerNPCData(FNPC_DATA_COME_OUT_DOOR, FNPC_DOOR_NONE); - SetFollowerNPCScriptPointer(scriptPtr); - - // If the player is biking and the follower flags prohibit biking, force the player to dismount the bike. - if (!CheckFollowerNPCFlag(FOLLOWER_NPC_FLAG_CAN_BIKE) - && TestPlayerAvatarFlags(PLAYER_AVATAR_FLAG_BIKE)) - SetPlayerAvatarTransitionFlags(PLAYER_AVATAR_FLAG_ON_FOOT); - - // Set the follower sprite to match the player state. - if (!TestPlayerAvatarFlags(PLAYER_AVATAR_FLAG_ON_FOOT)) - FollowerNPC_HandleSprite(); - - HideNPCFollower(); - SetFollowerNPCData(FNPC_DATA_WARP_END, FNPC_WARP_REAPPEAR); -} - static u32 GetFollowerNPCSprite(void) { u32 i; @@ -712,6 +672,58 @@ static void CalculateFollowerNPCEscalatorTrajectoryDown(struct Task *task) #undef tCounter +void CreateFollowerNPC(u32 gfx, u32 followerFlags, const u8 *scriptPtr) +{ + if (PlayerHasFollowerNPC()) + return; + + struct ObjectEvent *player = &gObjectEvents[gPlayerAvatar.objectEventId]; + struct ObjectEvent *follower; + struct ObjectEventTemplate npc = + { + .localId = OBJ_EVENT_ID_NPC_FOLLOWER, + .graphicsId = gfx, + .x = player->currentCoords.x, + .y = player->currentCoords.y, + .elevation = PlayerGetElevation(), + .script = scriptPtr + }; + + SetFollowerNPCData(FNPC_DATA_OBJ_ID, TrySpawnObjectEventTemplate(&npc, gSaveBlock1Ptr->location.mapNum, gSaveBlock1Ptr->location.mapGroup, player->currentCoords.x, player->currentCoords.y)); + follower = &gObjectEvents[GetFollowerNPCData(FNPC_DATA_OBJ_ID)]; + follower->movementType = MOVEMENT_TYPE_NONE; + gSprites[follower->spriteId].callback = MovementType_None; + + SetFollowerNPCData(FNPC_DATA_IN_PROGRESS, TRUE); + SetFollowerNPCData(FNPC_DATA_GFX_ID, follower->graphicsId); + SetFollowerNPCData(FNPC_DATA_FOLLOWER_FLAGS, followerFlags); + SetFollowerNPCData(FNPC_DATA_SURF_BLOB, FNPC_SURF_BLOB_NONE); + SetFollowerNPCData(FNPC_DATA_COME_OUT_DOOR, FNPC_DOOR_NONE); + SetFollowerNPCScriptPointer(scriptPtr); + + // If the player is biking and the follower flags prohibit biking, force the player to dismount the bike. + if (!CheckFollowerNPCFlag(FOLLOWER_NPC_FLAG_CAN_BIKE) + && TestPlayerAvatarFlags(PLAYER_AVATAR_FLAG_BIKE)) + SetPlayerAvatarTransitionFlags(PLAYER_AVATAR_FLAG_ON_FOOT); + + // Set the follower sprite to match the player state. + if (!TestPlayerAvatarFlags(PLAYER_AVATAR_FLAG_ON_FOOT)) + FollowerNPC_HandleSprite(); + + HideNPCFollower(); + SetFollowerNPCData(FNPC_DATA_WARP_END, FNPC_WARP_REAPPEAR); +} + +void DestroyFollowerNPC(void) +{ + if (!PlayerHasFollowerNPC()) + return; + + RemoveObjectEvent(&gObjectEvents[GetFollowerNPCData(FNPC_DATA_OBJ_ID)]); + ClearFollowerNPCData(); + UpdateFollowingPokemon(); +} + #define RETURN_STATE(state, dir) return newState == MOVEMENT_INVALID ? state + (dir - 1) : ReturnFollowerNPCDelayedState(dir - 1); u32 DetermineFollowerNPCState(struct ObjectEvent *follower, u32 state, u32 direction) { @@ -1642,7 +1654,7 @@ void ScriptCreateFollowerNPC(struct ScriptContext *ctx) const u8 *script = (const u8 *)ScriptReadWord(ctx); u32 battlePartner = ScriptReadHalfword(ctx); - if (PlayerHasFollowerNPC()) + if (!FNPC_ENABLE_NPC_FOLLOWERS || PlayerHasFollowerNPC()) return; SetFollowerNPCData(FNPC_DATA_BATTLE_PARTNER, battlePartner); @@ -1651,12 +1663,7 @@ void ScriptCreateFollowerNPC(struct ScriptContext *ctx) void ScriptDestroyFollowerNPC(struct ScriptContext *ctx) { - if (!PlayerHasFollowerNPC()) - return; - - RemoveObjectEvent(&gObjectEvents[GetFollowerNPCData(FNPC_DATA_OBJ_ID)]); - ClearFollowerNPCData(); - UpdateFollowingPokemon(); + DestroyFollowerNPC(); } void ScriptFaceFollowerNPC(struct ScriptContext *ctx)