diff --git a/asm/macros/event.inc b/asm/macros/event.inc index 6c0c3856b9..069477485a 100644 --- a/asm/macros/event.inc +++ b/asm/macros/event.inc @@ -1729,6 +1729,31 @@ .2byte \quantity .endm + @ Displays a multichoice box from which the user can choose a selection, and blocks script execution until a selection is made. + @ Lists of options are provided in argv. + @ If ignoreBPress is set to a non-zero value, then the user will not be allowed to back out of the multichoice with the B button. + .macro dynmultichoice left:req, top:req, ignoreBPress:req, maxBeforeScroll:req argv:vararg + .byte 0xe3 + .byte \left + .byte \top + .byte \ignoreBPress + .byte \maxBeforeScroll + .byte (.Ldynmultichoice_\@_2 - .Ldynmultichoice_\@_1) / 4 +.Ldynmultichoice_\@_1: + .4byte \argv +.Ldynmultichoice_\@_2: + .endm + + .macro dynmultipush name:req, id:req + .byte 0xe4 + .4byte \name + .byte \id + .endm + + .macro dynmultistack left:req, top:req, ignoreBPress:req, maxBeforeScroll:req + dynmultichoice \left, \top, \ignoreBPress, \maxBeforeScroll, NULL + .endm + @ Supplementary diff --git a/data/script_cmd_table.inc b/data/script_cmd_table.inc index 51b7f966e4..537a5126c7 100644 --- a/data/script_cmd_table.inc +++ b/data/script_cmd_table.inc @@ -227,6 +227,8 @@ gScriptCmdTable:: .4byte ScrCmd_warpwhitefade @ 0xe0 .4byte ScrCmd_buffercontestname @ 0xe1 .4byte ScrCmd_bufferitemnameplural @ 0xe2 + .4byte ScrCmd_dynmultichoice @ 0xe3 + .4byte ScrCmd_dynmultipush @ 0xe4 gScriptCmdTableEnd:: .4byte ScrCmd_nop diff --git a/include/field_specials.h b/include/field_specials.h index faf71e9c08..9b28b1d26b 100644 --- a/include/field_specials.h +++ b/include/field_specials.h @@ -3,6 +3,7 @@ extern bool8 gBikeCyclingChallenge; extern u8 gBikeCollisions; +extern u16 gScrollableMultichoice_ScrollOffset; u8 GetLeadMonIndex(void); u8 IsDestinationBoxFull(void); diff --git a/include/script.h b/include/script.h index 7c180e961b..4dc30ca74c 100644 --- a/include/script.h +++ b/include/script.h @@ -31,6 +31,7 @@ void ScriptCall(struct ScriptContext *ctx, const u8 *ptr); void ScriptReturn(struct ScriptContext *ctx); u16 ScriptReadHalfword(struct ScriptContext *ctx); u32 ScriptReadWord(struct ScriptContext *ctx); +u32 ScriptPeekWord(struct ScriptContext *ctx); void LockPlayerFieldControls(void); void UnlockPlayerFieldControls(void); bool8 ArePlayerFieldControlsLocked(void); diff --git a/include/script_menu.h b/include/script_menu.h index 36b66bf987..e4e4836097 100644 --- a/include/script_menu.h +++ b/include/script_menu.h @@ -1,8 +1,34 @@ #ifndef GUARD_SCRIPT_MENU_H #define GUARD_SCRIPT_MENU_H +#include "list_menu.h" + +// The default size the stack for dynamic multichoice is initialized to +// If you try to push an element when the stack is full, it will be reallocated +// With increasing capacity of MULTICHOICE_DYNAMIC_STACK_INC + +#define MULTICHOICE_DYNAMIC_STACK_SIZE 5 +#define MULTICHOICE_DYNAMIC_STACK_INC 5 + extern const u8 *const gStdStrings[]; +struct DynamicMultichoiceStack +{ + s32 top; + u32 capacity; + struct ListMenuItem *elements; +}; + +void MultichoiceDynamic_InitStack(u32 capacity); +void MultichoiceDynamic_ReallocStack(u32 newCapacity); +bool32 MultichoiceDynamic_StackFull(void); +bool32 MultichoiceDynamic_StackEmpty(void); +u32 MultichoiceDynamic_StackSize(void); +void MultichoiceDynamic_PushElement(struct ListMenuItem item); +struct ListMenuItem *MultichoiceDynamic_PopElement(void); +struct ListMenuItem *MultichoiceDynamic_PeekElement(void); +void MultichoiceDynamic_DestroyStack(void); +bool8 ScriptMenu_MultichoiceDynamic(u8 left, u8 top, u8 argc, struct ListMenuItem *items, bool8 ignoreBPress, u8 maxBeforeScroll); bool8 ScriptMenu_Multichoice(u8 left, u8 top, u8 multichoiceId, bool8 ignoreBPress); bool8 ScriptMenu_MultichoiceWithDefault(u8 left, u8 top, u8 multichoiceId, bool8 ignoreBPress, u8 defaultChoice); bool8 ScriptMenu_YesNo(u8 left, u8 top); diff --git a/src/field_specials.c b/src/field_specials.c index 5c7d079163..2f09c0f0f2 100644 --- a/src/field_specials.c +++ b/src/field_specials.c @@ -75,7 +75,7 @@ static EWRAM_DATA u8 sTutorMoveAndElevatorWindowId = 0; static EWRAM_DATA u16 sLilycoveDeptStore_NeverRead = 0; static EWRAM_DATA u16 sLilycoveDeptStore_DefaultFloorChoice = 0; static EWRAM_DATA struct ListMenuItem *sScrollableMultichoice_ListMenuItem = NULL; -static EWRAM_DATA u16 sScrollableMultichoice_ScrollOffset = 0; + static EWRAM_DATA u16 sFrontierExchangeCorner_NeverRead = 0; static EWRAM_DATA u8 sScrollableMultichoice_ItemSpriteId = 0; static EWRAM_DATA u8 sBattlePointsWindowId = 0; @@ -84,6 +84,7 @@ static EWRAM_DATA u8 sPCBoxToSendMon = 0; static EWRAM_DATA u32 sBattleTowerMultiBattleTypeFlags = 0; struct ListMenuTemplate gScrollableMultichoice_ListMenuTemplate; +EWRAM_DATA u16 gScrollableMultichoice_ScrollOffset = 0; void TryLoseFansFromPlayTime(void); void SetPlayerGotFirstFans(void); @@ -2477,7 +2478,7 @@ static void Task_ShowScrollableMultichoice(u8 taskId) struct Task *task = &gTasks[taskId]; LockPlayerFieldControls(); - sScrollableMultichoice_ScrollOffset = 0; + gScrollableMultichoice_ScrollOffset = 0; sScrollableMultichoice_ItemSpriteId = MAX_SPRITES; FillFrontierExchangeCornerWindowAndItemIcon(task->tScrollMultiId, 0); ShowBattleFrontierTutorWindow(task->tScrollMultiId, 0); @@ -2551,7 +2552,7 @@ static void ScrollableMultichoice_MoveCursor(s32 itemIndex, bool8 onInit, struct u16 selection; struct Task *task = &gTasks[taskId]; ListMenuGetScrollAndRow(task->tListTaskId, &selection, NULL); - sScrollableMultichoice_ScrollOffset = selection; + gScrollableMultichoice_ScrollOffset = selection; ListMenuGetCurrentItemArrayId(task->tListTaskId, &selection); HideFrontierExchangeCornerItemIcon(task->tScrollMultiId, sFrontierExchangeCorner_NeverRead); FillFrontierExchangeCornerWindowAndItemIcon(task->tScrollMultiId, selection); @@ -2672,7 +2673,7 @@ static void ScrollableMultichoice_UpdateScrollArrows(u8 taskId) template.secondY = task->tHeight * 8 + 10; template.fullyUpThreshold = 0; template.fullyDownThreshold = task->data[1] - task->tMaxItemsOnScreen; - task->tScrollArrowId = AddScrollIndicatorArrowPair(&template, &sScrollableMultichoice_ScrollOffset); + task->tScrollArrowId = AddScrollIndicatorArrowPair(&template, &gScrollableMultichoice_ScrollOffset); } } diff --git a/src/scrcmd.c b/src/scrcmd.c index 01e1a8f09a..177c27277b 100644 --- a/src/scrcmd.c +++ b/src/scrcmd.c @@ -48,6 +48,8 @@ #include "trainer_see.h" #include "tv.h" #include "window.h" +#include "list_menu.h" +#include "malloc.h" #include "constants/event_objects.h" typedef u16 (*SpecialFunc)(void); @@ -68,6 +70,7 @@ extern const u8 *gStdScripts[]; extern const u8 *gStdScripts_End[]; static void CloseBrailleWindow(void); +static void DynamicMultichoiceSortList(struct ListMenuItem *items, u32 count); // This is defined in here so the optimizer can't see its value when compiling // script.c. @@ -1350,6 +1353,90 @@ bool8 ScrCmd_yesnobox(struct ScriptContext *ctx) } } +static void DynamicMultichoiceSortList(struct ListMenuItem *items, u32 count) +{ + u32 i,j; + struct ListMenuItem tmp; + for (i = 0; i < count - 1; ++i) + { + for (j = 0; j < count - i - 1; ++j) + { + if (items[j].id > items[j+1].id) + { + tmp = items[j]; + items[j] = items[j+1]; + items[j+1] = tmp; + } + } + } +} + +#define DYN_MULTICHOICE_DEFAULT_MAX_BEFORE_SCROLL 6 + +bool8 ScrCmd_dynmultichoice(struct ScriptContext *ctx) +{ + u32 i; + u8 left = ScriptReadByte(ctx); + u8 top = ScriptReadByte(ctx); + bool8 ignoreBPress = ScriptReadByte(ctx); + u8 maxBeforeScroll = ScriptReadByte(ctx); + // Read vararg + u8 argc = ScriptReadByte(ctx); + struct ListMenuItem *items; + + if (argc == 0) + return; + + if (maxBeforeScroll == 0xFF) + maxBeforeScroll = DYN_MULTICHOICE_DEFAULT_MAX_BEFORE_SCROLL; + + if ((const u8*) ScriptPeekWord(ctx) != NULL) + { + items = AllocZeroed(sizeof(struct ListMenuItem) * argc); + for (i = 0; i < argc; ++i) + { + u8 *nameBuffer = Alloc(100); + const u8 *arg = (const u8 *) ScriptReadWord(ctx); + StringExpandPlaceholders(nameBuffer, arg); + items[i].name = nameBuffer; + items[i].id = i; + } + } + else + { + argc = MultichoiceDynamic_StackSize(); + items = AllocZeroed(sizeof(struct ListMenuItem) * argc); + for (i = 0; i < argc; ++i) + { + u8 *nameBuffer = Alloc(100); + struct ListMenuItem *currentItem = MultichoiceDynamic_PopElement(); + StringExpandPlaceholders(nameBuffer, currentItem->name); + items[i].name = nameBuffer; + items[i].id = currentItem->id; + } + DynamicMultichoiceSortList(items, argc); + MultichoiceDynamic_DestroyStack(); + } + + if (ScriptMenu_MultichoiceDynamic(left, top, argc, items, ignoreBPress, maxBeforeScroll)) + { + ScriptContext_Stop(); + return TRUE; + } + else + { + return FALSE; + } +} + +bool8 ScrCmd_dynmultipush(struct ScriptContext *ctx) +{ + const u8 *name = (const u8*) ScriptReadWord(ctx); + u32 id = ScriptReadByte(ctx); + struct ListMenuItem item = {.name = name, .id = id}; + MultichoiceDynamic_PushElement(item); +} + bool8 ScrCmd_multichoice(struct ScriptContext *ctx) { u8 left = ScriptReadByte(ctx); diff --git a/src/script.c b/src/script.c index c252c95f04..b14d33d4b3 100644 --- a/src/script.c +++ b/src/script.c @@ -179,6 +179,15 @@ u32 ScriptReadWord(struct ScriptContext *ctx) return (((((value3 << 8) + value2) << 8) + value1) << 8) + value0; } +u32 ScriptPeekWord(struct ScriptContext *ctx) +{ + u32 value0 = *(ctx->scriptPtr); + u32 value1 = *(ctx->scriptPtr + 1); + u32 value2 = *(ctx->scriptPtr + 2); + u32 value3 = *(ctx->scriptPtr + 3); + return (((((value3 << 8) + value2) << 8) + value1) << 8) + value0; +} + void LockPlayerFieldControls(void) { sLockFieldControls = TRUE; diff --git a/src/script_menu.c b/src/script_menu.c index 6633332f3f..ce0b378193 100644 --- a/src/script_menu.c +++ b/src/script_menu.c @@ -13,6 +13,9 @@ #include "strings.h" #include "task.h" #include "text.h" +#include "list_menu.h" +#include "malloc.h" +#include "util.h" #include "constants/field_specials.h" #include "constants/items.h" #include "constants/script_menu.h" @@ -21,12 +24,16 @@ #include "data/script_menu.h" static EWRAM_DATA u8 sProcessInputDelay = 0; +static EWRAM_DATA struct DynamicMultichoiceStack *sDynamicMultiChoiceStack = NULL; static u8 sLilycoveSSTidalSelections[SSTIDAL_SELECTION_COUNT]; +static void FreeListMenuItems(struct ListMenuItem *items, u32 count); +static void Task_HandleScrollingMultichoiceInput(u8 taskId); static void Task_HandleMultichoiceInput(u8 taskId); static void Task_HandleYesNoInput(u8 taskId); static void Task_HandleMultichoiceGridInput(u8 taskId); +static void DrawMultichoiceMenuDynamic(u8 left, u8 top, u8 argc, struct ListMenuItem *items, bool8 ignoreBPress, u8 cursorPos, u8 maxBeforeScroll); static void DrawMultichoiceMenu(u8 left, u8 top, u8 multichoiceId, bool8 ignoreBPress, u8 cursorPos); static void InitMultichoiceCheckWrap(bool8 ignoreBPress, u8 count, u8 windowId, u8 multichoiceId); static void DrawLinkServicesMultichoiceMenu(u8 multichoiceId); @@ -36,6 +43,33 @@ static bool8 IsPicboxClosed(void); static void CreateStartMenuForPokenavTutorial(void); static void InitMultichoiceNoWrap(bool8 ignoreBPress, u8 unusedCount, u8 windowId, u8 multichoiceId); +static const struct ListMenuTemplate sScriptableListMenuTemplate = +{ + .item_X = 8, + .upText_Y = 1, + .cursorPal = 2, + .fillValue = 1, + .cursorShadowPal = 3, + .lettersSpacing = 1, + .scrollMultiple = LIST_NO_MULTIPLE_SCROLL, + .fontId = FONT_NORMAL, +}; + +bool8 ScriptMenu_MultichoiceDynamic(u8 left, u8 top, u8 argc, struct ListMenuItem *items, bool8 ignoreBPress, u8 maxBeforeScroll) +{ + if (FuncIsActiveTask(Task_HandleMultichoiceInput) == TRUE) + { + FreeListMenuItems(items, argc); + return FALSE; + } + else + { + gSpecialVar_Result = 0xFF; + DrawMultichoiceMenuDynamic(left, top, argc, items, ignoreBPress, 0, maxBeforeScroll); + return TRUE; + } +} + bool8 ScriptMenu_Multichoice(u8 left, u8 top, u8 multichoiceId, bool8 ignoreBPress) { if (FuncIsActiveTask(Task_HandleMultichoiceInput) == TRUE) @@ -64,6 +98,17 @@ bool8 ScriptMenu_MultichoiceWithDefault(u8 left, u8 top, u8 multichoiceId, bool8 } } +static void FreeListMenuItems(struct ListMenuItem *items, u32 count) +{ + u32 i; + for (i = 0; i < count; ++i) + { + // All items were dynamically allocated, so items[i].name is not actually constant. + Free((void *)items[i].name); + } + Free(items); +} + // Unused static u16 GetLengthWithExpandedPlayerName(const u8 *str) { @@ -90,6 +135,148 @@ static u16 GetLengthWithExpandedPlayerName(const u8 *str) return length; } +void MultichoiceDynamic_InitStack(u32 capacity) +{ + AGB_ASSERT(sDynamicMultiChoiceStack == NULL); + sDynamicMultiChoiceStack = AllocZeroed(sizeof(*sDynamicMultiChoiceStack)); + AGB_ASSERT(sDynamicMultiChoiceStack != NULL); + sDynamicMultiChoiceStack->capacity = capacity; + sDynamicMultiChoiceStack->top = -1; + sDynamicMultiChoiceStack->elements = AllocZeroed(capacity * sizeof(struct ListMenuItem)); +} + +void MultichoiceDynamic_ReallocStack(u32 newCapacity) +{ + struct ListMenuItem *newElements; + AGB_ASSERT(sDynamicMultiChoiceStack != NULL); + AGB_ASSERT(sDynamicMultiChoiceStack->capacity < newCapacity); + newElements = AllocZeroed(newCapacity * sizeof(struct ListMenuItem)); + AGB_ASSERT(newElements != NULL); + memcpy(newElements, sDynamicMultiChoiceStack->elements, sDynamicMultiChoiceStack->capacity * sizeof(struct ListMenuItem)); + Free(sDynamicMultiChoiceStack->elements); + sDynamicMultiChoiceStack->elements = newElements; + sDynamicMultiChoiceStack->capacity = newCapacity; +} + +bool32 MultichoiceDynamic_StackFull(void) +{ + AGB_ASSERT(sDynamicMultiChoiceStack != NULL); + return sDynamicMultiChoiceStack->top == sDynamicMultiChoiceStack->capacity - 1; +} + +bool32 MultichoiceDynamic_StackEmpty(void) +{ + AGB_ASSERT(sDynamicMultiChoiceStack != NULL); + return sDynamicMultiChoiceStack->top == -1; +} + +u32 MultichoiceDynamic_StackSize(void) +{ + AGB_ASSERT(sDynamicMultiChoiceStack != NULL); + return sDynamicMultiChoiceStack->top + 1; +} + +void MultichoiceDynamic_PushElement(struct ListMenuItem item) +{ + if (sDynamicMultiChoiceStack == NULL) + MultichoiceDynamic_InitStack(MULTICHOICE_DYNAMIC_STACK_SIZE); + if (MultichoiceDynamic_StackFull()) + MultichoiceDynamic_ReallocStack(sDynamicMultiChoiceStack->capacity + MULTICHOICE_DYNAMIC_STACK_INC); + sDynamicMultiChoiceStack->elements[++sDynamicMultiChoiceStack->top] = item; +} + +struct ListMenuItem *MultichoiceDynamic_PopElement(void) +{ + if (sDynamicMultiChoiceStack == NULL) + return NULL; + if (MultichoiceDynamic_StackEmpty()) + return NULL; + return &sDynamicMultiChoiceStack->elements[sDynamicMultiChoiceStack->top--]; +} + +struct ListMenuItem *MultichoiceDynamic_PeekElement(void) +{ + if (sDynamicMultiChoiceStack == NULL) + return NULL; + if (MultichoiceDynamic_StackEmpty()) + return NULL; + return &sDynamicMultiChoiceStack->elements[sDynamicMultiChoiceStack->top]; +} + +void MultichoiceDynamic_DestroyStack(void) +{ + TRY_FREE_AND_SET_NULL(sDynamicMultiChoiceStack->elements); + TRY_FREE_AND_SET_NULL(sDynamicMultiChoiceStack); +} + +static void MultichoiceDynamic_MoveCursor(s32 itemIndex, bool8 onInit, struct ListMenu *list) +{ + u8 taskId; + PlaySE(SE_SELECT); + taskId = FindTaskIdByFunc(Task_HandleScrollingMultichoiceInput); + if (taskId != TASK_NONE) + { + u16 scrollOffset; + ListMenuGetScrollAndRow(gTasks[taskId].data[0], &scrollOffset, NULL); + gScrollableMultichoice_ScrollOffset = scrollOffset; + } +} + +static void DrawMultichoiceMenuDynamic(u8 left, u8 top, u8 argc, struct ListMenuItem *items, bool8 ignoreBPress, u8 cursorPos, u8 maxBeforeScroll) +{ + u32 i; + u8 windowId; + s32 width = 0; + u8 newWidth; + u8 taskId; + u32 windowHeight; + + for (i = 0; i < argc; ++i) + { + width = DisplayTextAndGetWidth(items[i].name, width); + } + windowHeight = (argc < maxBeforeScroll) ? argc * 2 : maxBeforeScroll * 2; + newWidth = ConvertPixelWidthToTileWidth(width); + left = ScriptMenu_AdjustLeftCoordFromWidth(left, newWidth); + windowId = CreateWindowFromRect(left, top, newWidth, windowHeight); + SetStandardWindowBorderStyle(windowId, FALSE); + CopyWindowToVram(windowId, COPYWIN_FULL); + + gMultiuseListMenuTemplate = sScriptableListMenuTemplate; + gMultiuseListMenuTemplate.windowId = windowId; + gMultiuseListMenuTemplate.items = items; + gMultiuseListMenuTemplate.totalItems = argc; + gMultiuseListMenuTemplate.maxShowed = maxBeforeScroll; + gMultiuseListMenuTemplate.moveCursorFunc = MultichoiceDynamic_MoveCursor; + + taskId = CreateTask(Task_HandleScrollingMultichoiceInput, 80); + gTasks[taskId].data[0] = ListMenuInit(&gMultiuseListMenuTemplate, 0, 0); + gTasks[taskId].data[1] = ignoreBPress; + gTasks[taskId].data[2] = windowId; + gTasks[taskId].data[5] = argc; + gTasks[taskId].data[7] = maxBeforeScroll; + StoreWordInTwoHalfwords(&gTasks[taskId].data[3], (u32) items); + + if (argc > maxBeforeScroll) + { + // Create Scrolling Arrows + struct ScrollArrowsTemplate template; + template.firstX = (newWidth / 2) * 8 + 12 + (left) * 8; + template.firstY = top * 8 + 5; + template.secondX = template.firstX; + template.secondY = windowHeight * 8 + 12; + template.fullyUpThreshold = 0; + template.fullyDownThreshold = argc - maxBeforeScroll; + template.firstArrowType = SCROLL_ARROW_UP; + template.secondArrowType = SCROLL_ARROW_DOWN; + template.tileTag = 2000; + template.palTag = 100, + template.palNum = 0; + + gTasks[taskId].data[6] = AddScrollIndicatorArrowPair(&template, &gScrollableMultichoice_ScrollOffset); + } +} + static void DrawMultichoiceMenu(u8 left, u8 top, u8 multichoiceId, bool8 ignoreBPress, u8 cursorPos) { int i; @@ -152,6 +339,50 @@ static void InitMultichoiceCheckWrap(bool8 ignoreBPress, u8 count, u8 windowId, DrawLinkServicesMultichoiceMenu(multichoiceId); } +static void Task_HandleScrollingMultichoiceInput(u8 taskId) +{ + bool32 done = FALSE; + s32 input = ListMenu_ProcessInput(gTasks[taskId].data[0]); + + switch (input) + { + case LIST_HEADER: + case LIST_NOTHING_CHOSEN: + break; + case LIST_CANCEL: + if (gTasks[taskId].data[1]) + { + gSpecialVar_Result = 0x7F; + done = TRUE; + } + break; + default: + gSpecialVar_Result = input; + done = TRUE; + break; + } + + if (done) + { + struct ListMenuItem *items; + + PlaySE(SE_SELECT); + if (gTasks[taskId].data[5] > gTasks[taskId].data[7]) + { + RemoveScrollIndicatorArrowPair(gTasks[taskId].data[6]); + } + + LoadWordFromTwoHalfwords(&gTasks[taskId].data[3], (u32* )(&items)); + FreeListMenuItems(items, gTasks[taskId].data[5]); + + DestroyListMenuTask(gTasks[taskId].data[0], NULL, NULL); + ClearStdWindowAndFrame(gTasks[taskId].data[2], TRUE); + RemoveWindow(gTasks[taskId].data[2]); + ScriptContext_Enable(); + DestroyTask(taskId); + } +} + static void Task_HandleMultichoiceInput(u8 taskId) { s8 selection;