diff --git a/asm/macros/event.inc b/asm/macros/event.inc index 62743b6f7c0e..f2af499b522c 100644 --- a/asm/macros/event.inc +++ b/asm/macros/event.inc @@ -1735,6 +1735,38 @@ .2byte \quantity .endm + .macro _dynmultichoice left:req, top:req, ignoreBPress:req, maxBeforeScroll:req, shouldSort:req, initialSelected:req, callbacks:req argv:vararg + .byte 0xe3 + .2byte \left + .2byte \top + .byte \ignoreBPress + .byte \maxBeforeScroll + .byte \shouldSort + .2byte \initialSelected + .byte \callbacks + .byte (.Ldynmultichoice_\@_2 - .Ldynmultichoice_\@_1) / 4 +.Ldynmultichoice_\@_1: + .4byte \argv +.Ldynmultichoice_\@_2: + .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, initialSelected:req, callbacks:req argv:vararg + _dynmultichoice \left, \top, \ignoreBPress, \maxBeforeScroll, FALSE, \initialSelected, \callbacks, \argv + .endm + + .macro dynmultipush name:req, id:req + .byte 0xe4 + .4byte \name + .2byte \id + .endm + + .macro dynmultistack left:req, top:req, ignoreBPress:req, maxBeforeScroll:req, shouldSort:req, initialSelected:req, callbacks:req + _dynmultichoice \left, \top, \ignoreBPress, \maxBeforeScroll, \shouldSort, \initialSelected, \callbacks, NULL + .endm + @ Supplementary diff --git a/data/script_cmd_table.inc b/data/script_cmd_table.inc index f50ce5322bee..794662e99381 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/constants/script_menu.h b/include/constants/script_menu.h index c58df7335f96..e9dc39d05824 100644 --- a/include/constants/script_menu.h +++ b/include/constants/script_menu.h @@ -165,4 +165,10 @@ #define STDSTRING_BATTLE_PIKE 28 #define STDSTRING_BATTLE_PYRAMID 29 +// Dynamic Multichoice Callbacks + +#define DYN_MULTICHOICE_CB_DEBUG 0 +#define DYN_MULTICHOICE_CB_SHOW_ITEM 1 +#define DYN_MULTICHOICE_CB_NONE 255 + #endif //GUARD_SCRIPT_MENU_CONSTANTS_H diff --git a/include/field_specials.h b/include/field_specials.h index d36f2686c286..95a91d543f06 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/list_menu.h b/include/list_menu.h index 9299ede6c806..23caaf4b4c75 100644 --- a/include/list_menu.h +++ b/include/list_menu.h @@ -126,5 +126,7 @@ u8 AddScrollIndicatorArrowPair(const struct ScrollArrowsTemplate *arrowInfo, u16 u8 AddScrollIndicatorArrowPairParameterized(u32 arrowType, s32 commonPos, s32 firstPos, s32 secondPos, s32 fullyDownThreshold, s32 tileTag, s32 palTag, u16 *currItemPtr); void RemoveScrollIndicatorArrowPair(u8 taskId); void Task_ScrollIndicatorArrowPairOnMainMenu(u8 taskId); +bool8 ListMenuChangeSelection(struct ListMenu *list, bool8 updateCursorAndCallCallback, u8 count, bool8 movingDown); +bool8 ListMenuChangeSelectionFull(struct ListMenu *list, bool32 updateCursor, bool32 callCallback, u8 count, bool8 movingDown); #endif //GUARD_LIST_MENU_H diff --git a/include/script.h b/include/script.h index 7c180e961b3a..4dc30ca74cfd 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 41b45000eb42..d690939faecb 100644 --- a/include/script_menu.h +++ b/include/script_menu.h @@ -1,11 +1,37 @@ #ifndef GUARD_SCRIPT_MENU_H #define GUARD_SCRIPT_MENU_H +#include "list_menu.h" #include "constants/script_menu.h" #include "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); +struct ListMenuItem *MultichoiceDynamic_PeekElementAt(u32 index); +void MultichoiceDynamic_DestroyStack(void); +bool8 ScriptMenu_MultichoiceDynamic(u8 left, u8 top, u8 argc, struct ListMenuItem *items, bool8 ignoreBPress, u8 maxBeforeScroll, u32 initialRow, u32 callbackSet); bool8 ScriptMenu_Multichoice(u8 left, u8 top, u8 multichoiceId, bool8 ignoreBPress); bool8 ScriptMenu_MultichoiceWithDefault(u8 left, u8 top, u8 multichoiceId, bool8 ignoreBPress, u8 defaultChoice); void DrawMultichoiceMenuInternal(u8 left, u8 top, u8 multichoiceId, bool8 ignoreBPress, u8 cursorPos, const struct MenuAction *actions, int count); diff --git a/src/field_specials.c b/src/field_specials.c index c4968a6cc827..a3f0c97d269a 100644 --- a/src/field_specials.c +++ b/src/field_specials.c @@ -87,7 +87,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; @@ -96,6 +96,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); @@ -2561,7 +2562,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); @@ -2635,7 +2636,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); @@ -2756,7 +2757,7 @@ static void ScrollableMultichoice_UpdateScrollArrows(u8 taskId) template.secondY = task->tHeight * 8 + 10; template.fullyUpThreshold = 0; template.fullyDownThreshold = task->tNumItems - task->tMaxItemsOnScreen; - task->tScrollArrowId = AddScrollIndicatorArrowPair(&template, &sScrollableMultichoice_ScrollOffset); + task->tScrollArrowId = AddScrollIndicatorArrowPair(&template, &gScrollableMultichoice_ScrollOffset); } } diff --git a/src/list_menu.c b/src/list_menu.c index e24442ca023c..e83f325161d7 100644 --- a/src/list_menu.c +++ b/src/list_menu.c @@ -70,7 +70,6 @@ struct RedArrowCursor // this file's functions static u8 ListMenuInitInternal(struct ListMenuTemplate *listMenuTemplate, u16 scrollOffset, u16 selectedRow); -static bool8 ListMenuChangeSelection(struct ListMenu *list, bool8 updateCursorAndCallCallback, u8 count, bool8 movingDown); static void ListMenuPrintEntries(struct ListMenu *list, u16 startIndex, u16 yOffset, u16 count); static void ListMenuDrawCursor(struct ListMenu *list); static void ListMenuCallSelectionChangedCallback(struct ListMenu *list, u8 onInit); @@ -837,7 +836,7 @@ static void ListMenuScroll(struct ListMenu *list, u8 count, bool8 movingDown) } } -static bool8 ListMenuChangeSelection(struct ListMenu *list, bool8 updateCursorAndCallCallback, u8 count, bool8 movingDown) +bool8 ListMenuChangeSelectionFull(struct ListMenu *list, bool32 updateCursor, bool32 callCallback, u8 count, bool8 movingDown) { u16 oldSelectedRow; u8 selectionChange, i, cursorCount; @@ -857,7 +856,7 @@ static bool8 ListMenuChangeSelection(struct ListMenu *list, bool8 updateCursorAn } while (list->template.items[list->scrollOffset + list->selectedRow].id == LIST_HEADER); } - if (updateCursorAndCallCallback) + if (updateCursor) { switch (selectionChange) { @@ -867,7 +866,8 @@ static bool8 ListMenuChangeSelection(struct ListMenu *list, bool8 updateCursorAn case 1: ListMenuErasePrintedCursor(list, oldSelectedRow); ListMenuDrawCursor(list); - ListMenuCallSelectionChangedCallback(list, FALSE); + if (callCallback) + ListMenuCallSelectionChangedCallback(list, FALSE); CopyWindowToVram(list->template.windowId, COPYWIN_GFX); break; case 2: @@ -875,7 +875,8 @@ static bool8 ListMenuChangeSelection(struct ListMenu *list, bool8 updateCursorAn ListMenuErasePrintedCursor(list, oldSelectedRow); ListMenuScroll(list, cursorCount, movingDown); ListMenuDrawCursor(list); - ListMenuCallSelectionChangedCallback(list, FALSE); + if (callCallback) + ListMenuCallSelectionChangedCallback(list, FALSE); CopyWindowToVram(list->template.windowId, COPYWIN_GFX); break; } @@ -884,6 +885,11 @@ static bool8 ListMenuChangeSelection(struct ListMenu *list, bool8 updateCursorAn return FALSE; } +bool8 ListMenuChangeSelection(struct ListMenu *list, bool8 updateCursorAndCallCallback, u8 count, bool8 movingDown) +{ + return ListMenuChangeSelectionFull(list, updateCursorAndCallCallback, updateCursorAndCallCallback, count, movingDown); +} + static void ListMenuCallSelectionChangedCallback(struct ListMenu *list, u8 onInit) { if (list->template.moveCursorFunc != NULL) diff --git a/src/scrcmd.c b/src/scrcmd.c index e8496ba35f36..11ae5f3d75ac 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); @@ -69,6 +71,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. @@ -1351,6 +1354,101 @@ 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; + u32 left = VarGet(ScriptReadHalfword(ctx)); + u32 top = VarGet(ScriptReadHalfword(ctx)); + bool32 ignoreBPress = ScriptReadByte(ctx); + u32 maxBeforeScroll = ScriptReadByte(ctx); + bool32 shouldSort = ScriptReadByte(ctx); + u32 initialSelected = VarGet(ScriptReadHalfword(ctx)); + u32 callbackSet = ScriptReadByte(ctx); + u32 initialRow = 0; + // Read vararg + u32 argc = ScriptReadByte(ctx); + struct ListMenuItem *items; + + if (argc == 0) + return FALSE; + + 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; + if (i == initialSelected) + initialRow = i; + } + } + else + { + argc = MultichoiceDynamic_StackSize(); + items = AllocZeroed(sizeof(struct ListMenuItem) * argc); + for (i = 0; i < argc; ++i) + { + struct ListMenuItem *currentItem = MultichoiceDynamic_PeekElementAt(i); + items[i] = *currentItem; + if (currentItem->id == initialSelected) + initialRow = i; + } + if (shouldSort) + DynamicMultichoiceSortList(items, argc); + MultichoiceDynamic_DestroyStack(); + } + + if (ScriptMenu_MultichoiceDynamic(left, top, argc, items, ignoreBPress, maxBeforeScroll, initialRow, callbackSet)) + { + ScriptContext_Stop(); + return TRUE; + } + else + { + return FALSE; + } +} + +bool8 ScrCmd_dynmultipush(struct ScriptContext *ctx) +{ + u8 *nameBuffer = Alloc(100); + const u8 *name = (const u8*) ScriptReadWord(ctx); + u32 id = VarGet(ScriptReadHalfword(ctx)); + struct ListMenuItem item; + StringExpandPlaceholders(nameBuffer, name); + item.name = nameBuffer; + item.id = id; + MultichoiceDynamic_PushElement(item); + return FALSE; +} + bool8 ScrCmd_multichoice(struct ScriptContext *ctx) { u8 left = ScriptReadByte(ctx); diff --git a/src/script.c b/src/script.c index c252c95f0446..b14d33d4b3ac 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 6444e382e92c..021f23053c6d 100644 --- a/src/script_menu.c +++ b/src/script_menu.c @@ -13,6 +13,10 @@ #include "strings.h" #include "task.h" #include "text.h" +#include "list_menu.h" +#include "malloc.h" +#include "util.h" +#include "item_icon.h" #include "constants/field_specials.h" #include "constants/items.h" #include "constants/script_menu.h" @@ -20,13 +24,35 @@ #include "data/script_menu.h" +struct DynamicListMenuEventArgs +{ + struct ListMenuTemplate *list; + u16 selectedItem; + u8 windowId; +}; + +typedef void (*DynamicListCallback)(struct DynamicListMenuEventArgs *eventArgs); + +struct DynamicListMenuEventCollection +{ + DynamicListCallback OnInit; + DynamicListCallback OnSelectionChanged; + DynamicListCallback OnDestroy; +}; + static EWRAM_DATA u8 sProcessInputDelay = 0; +static EWRAM_DATA u8 sDynamicMenuEventId = 0; +static EWRAM_DATA struct DynamicMultichoiceStack *sDynamicMultiChoiceStack = NULL; +static EWRAM_DATA u16 *sDynamicMenuEventScratchPad = 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, u32 initialRow, u8 maxBeforeScroll, u32 callbackSet); 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); @@ -35,6 +61,55 @@ static void CreateLilycoveSSTidalMultichoice(void); static bool8 IsPicboxClosed(void); static void CreateStartMenuForPokenavTutorial(void); static void InitMultichoiceNoWrap(bool8 ignoreBPress, u8 unusedCount, u8 windowId, u8 multichoiceId); +static void MultichoiceDynamicEventDebug_OnInit(struct DynamicListMenuEventArgs *eventArgs); +static void MultichoiceDynamicEventDebug_OnSelectionChanged(struct DynamicListMenuEventArgs *eventArgs); +static void MultichoiceDynamicEventDebug_OnDestroy(struct DynamicListMenuEventArgs *eventArgs); +static void MultichoiceDynamicEventShowItem_OnInit(struct DynamicListMenuEventArgs *eventArgs); +static void MultichoiceDynamicEventShowItem_OnSelectionChanged(struct DynamicListMenuEventArgs *eventArgs); +static void MultichoiceDynamicEventShowItem_OnDestroy(struct DynamicListMenuEventArgs *eventArgs); + +static const struct DynamicListMenuEventCollection sDynamicListMenuEventCollections[] = +{ + [DYN_MULTICHOICE_CB_DEBUG] = + { + .OnInit = MultichoiceDynamicEventDebug_OnInit, + .OnSelectionChanged = MultichoiceDynamicEventDebug_OnSelectionChanged, + .OnDestroy = MultichoiceDynamicEventDebug_OnDestroy + }, + [DYN_MULTICHOICE_CB_SHOW_ITEM] = + { + .OnInit = MultichoiceDynamicEventShowItem_OnInit, + .OnSelectionChanged = MultichoiceDynamicEventShowItem_OnSelectionChanged, + .OnDestroy = MultichoiceDynamicEventShowItem_OnDestroy + } +}; + +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, u32 initialRow, u32 callbackSet) +{ + if (FuncIsActiveTask(Task_HandleMultichoiceInput) == TRUE) + { + FreeListMenuItems(items, argc); + return FALSE; + } + else + { + gSpecialVar_Result = 0xFF; + DrawMultichoiceMenuDynamic(left, top, argc, items, ignoreBPress, initialRow, maxBeforeScroll, callbackSet); + return TRUE; + } +} bool8 ScriptMenu_Multichoice(u8 left, u8 top, u8 multichoiceId, bool8 ignoreBPress) { @@ -64,6 +139,85 @@ bool8 ScriptMenu_MultichoiceWithDefault(u8 left, u8 top, u8 multichoiceId, bool8 } } +static void MultichoiceDynamicEventDebug_OnInit(struct DynamicListMenuEventArgs *eventArgs) +{ + DebugPrintf("OnInit: %d", eventArgs->windowId); +} + +static void MultichoiceDynamicEventDebug_OnSelectionChanged(struct DynamicListMenuEventArgs *eventArgs) +{ + DebugPrintf("OnSelectionChanged: %d", eventArgs->selectedItem); +} + +static void MultichoiceDynamicEventDebug_OnDestroy(struct DynamicListMenuEventArgs *eventArgs) +{ + DebugPrintf("OnDestroy: %d", eventArgs->windowId); +} + +#define sAuxWindowId sDynamicMenuEventScratchPad[0] +#define sItemSpriteId sDynamicMenuEventScratchPad[1] +#define TAG_CB_ITEM_ICON 3000 + +static void MultichoiceDynamicEventShowItem_OnInit(struct DynamicListMenuEventArgs *eventArgs) +{ + struct WindowTemplate *template = &gWindows[eventArgs->windowId].window; + u32 baseBlock = template->baseBlock + template->width * template->height; + struct WindowTemplate auxTemplate = CreateWindowTemplate(0, template->tilemapLeft + template->width + 2, template->tilemapTop, 4, 4, 15, baseBlock); + u32 auxWindowId = AddWindow(&auxTemplate); + SetStandardWindowBorderStyle(auxWindowId, FALSE); + FillWindowPixelBuffer(auxWindowId, 0x11); + CopyWindowToVram(auxWindowId, COPYWIN_FULL); + sAuxWindowId = auxWindowId; + sItemSpriteId = MAX_SPRITES; +} + +static void MultichoiceDynamicEventShowItem_OnSelectionChanged(struct DynamicListMenuEventArgs *eventArgs) +{ + struct WindowTemplate *template = &gWindows[eventArgs->windowId].window; + u32 x = template->tilemapLeft * 8 + template->width * 8 + 36; + u32 y = template->tilemapTop * 8 + 20; + + if (sItemSpriteId != MAX_SPRITES) + { + FreeSpriteTilesByTag(TAG_CB_ITEM_ICON); + FreeSpritePaletteByTag(TAG_CB_ITEM_ICON); + DestroySprite(&gSprites[sItemSpriteId]); + } + + sItemSpriteId = AddItemIconSprite(TAG_CB_ITEM_ICON, TAG_CB_ITEM_ICON, eventArgs->selectedItem); + gSprites[sItemSpriteId].oam.priority = 0; + gSprites[sItemSpriteId].x = x; + gSprites[sItemSpriteId].y = y; +} + +static void MultichoiceDynamicEventShowItem_OnDestroy(struct DynamicListMenuEventArgs *eventArgs) +{ + ClearStdWindowAndFrame(sAuxWindowId, TRUE); + RemoveWindow(sAuxWindowId); + + if (sItemSpriteId != MAX_SPRITES) + { + FreeSpriteTilesByTag(TAG_CB_ITEM_ICON); + FreeSpritePaletteByTag(TAG_CB_ITEM_ICON); + DestroySprite(&gSprites[sItemSpriteId]); + } +} + +#undef sAuxWindowId +#undef sItemSpriteId +#undef TAG_CB_ITEM_ICON + +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); +} + static u16 UNUSED GetLengthWithExpandedPlayerName(const u8 *str) { u16 length = 0; @@ -89,6 +243,180 @@ static u16 UNUSED 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]; +} + +struct ListMenuItem *MultichoiceDynamic_PeekElementAt(u32 index) +{ + if (sDynamicMultiChoiceStack == NULL) + return NULL; + if (sDynamicMultiChoiceStack->top < index) + return NULL; + return &sDynamicMultiChoiceStack->elements[index]; +} + +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) + { + ListMenuGetScrollAndRow(gTasks[taskId].data[0], &gScrollableMultichoice_ScrollOffset, NULL); + if (sDynamicMenuEventId != DYN_MULTICHOICE_CB_NONE && sDynamicListMenuEventCollections[sDynamicMenuEventId].OnSelectionChanged && !onInit) + { + struct DynamicListMenuEventArgs eventArgs = {.selectedItem = itemIndex, .windowId = list->template.windowId, .list = &list->template}; + sDynamicListMenuEventCollections[sDynamicMenuEventId].OnSelectionChanged(&eventArgs); + } + } +} + +static void DrawMultichoiceMenuDynamic(u8 left, u8 top, u8 argc, struct ListMenuItem *items, bool8 ignoreBPress, u32 initialRow, u8 maxBeforeScroll, u32 callbackSet) +{ + u32 i; + u8 windowId; + s32 width = 0; + u8 newWidth; + u8 taskId; + u32 windowHeight; + struct ListMenu *list; + + for (i = 0; i < argc; ++i) + { + width = DisplayTextAndGetWidth(items[i].name, width); + } + LoadMessageBoxAndBorderGfx(); + 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); + + // I don't like this being global either, but I could not come up with another solution that + // does not invade the whole ListMenu infrastructure. + sDynamicMenuEventId = callbackSet; + sDynamicMenuEventScratchPad = AllocZeroed(100 * sizeof(u16)); + if (sDynamicMenuEventId != DYN_MULTICHOICE_CB_NONE && sDynamicListMenuEventCollections[sDynamicMenuEventId].OnInit) + { + struct DynamicListMenuEventArgs eventArgs = {.selectedItem = initialRow, .windowId = windowId, .list = NULL}; + sDynamicListMenuEventCollections[sDynamicMenuEventId].OnInit(&eventArgs); + } + + 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((u16*) &gTasks[taskId].data[3], (u32) items); + list = (void *) gTasks[gTasks[taskId].data[0]].data; + ListMenuChangeSelectionFull(list, TRUE, FALSE, initialRow, TRUE); + + if (sDynamicMenuEventId != DYN_MULTICHOICE_CB_NONE && sDynamicListMenuEventCollections[sDynamicMenuEventId].OnSelectionChanged) + { + struct DynamicListMenuEventArgs eventArgs = {.selectedItem = items[initialRow].id, .windowId = windowId, .list = &gMultiuseListMenuTemplate}; + sDynamicListMenuEventCollections[sDynamicMenuEventId].OnSelectionChanged(&eventArgs); + } + ListMenuGetScrollAndRow(gTasks[taskId].data[0], &gScrollableMultichoice_ScrollOffset, NULL); + 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 = top * 8 + 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); + } +} + void DrawMultichoiceMenuInternal(u8 left, u8 top, u8 multichoiceId, bool8 ignoreBPress, u8 cursorPos, const struct MenuAction *actions, int count) { int i; @@ -154,6 +482,59 @@ 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 = MULTI_B_PRESSED; + done = TRUE; + } + break; + default: + gSpecialVar_Result = input; + done = TRUE; + break; + } + + if (done) + { + struct ListMenuItem *items; + + PlaySE(SE_SELECT); + + if (sDynamicMenuEventId != DYN_MULTICHOICE_CB_NONE && sDynamicListMenuEventCollections[sDynamicMenuEventId].OnDestroy) + { + struct DynamicListMenuEventArgs eventArgs = {.selectedItem = input, .windowId = gTasks[taskId].data[2], .list = NULL}; + sDynamicListMenuEventCollections[sDynamicMenuEventId].OnDestroy(&eventArgs); + } + + sDynamicMenuEventId = DYN_MULTICHOICE_CB_NONE; + + if (gTasks[taskId].data[5] > gTasks[taskId].data[7]) + { + RemoveScrollIndicatorArrowPair(gTasks[taskId].data[6]); + } + + LoadWordFromTwoHalfwords((u16*) &gTasks[taskId].data[3], (u32* )(&items)); + FreeListMenuItems(items, gTasks[taskId].data[5]); + TRY_FREE_AND_SET_NULL(sDynamicMenuEventScratchPad); + 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;