[script-command] add dynmultichoice

* supports variable length arguments
 * automatically scrolls
 * supports building list menus from a stack
This commit is contained in:
sbird 2023-01-15 13:41:10 +01:00
parent d5a4bfcd6f
commit 569fa0a60a
9 changed files with 387 additions and 4 deletions

View file

@ -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

View file

@ -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

View file

@ -3,6 +3,7 @@
extern bool8 gBikeCyclingChallenge;
extern u8 gBikeCollisions;
extern u16 gScrollableMultichoice_ScrollOffset;
u8 GetLeadMonIndex(void);
u8 IsDestinationBoxFull(void);

View file

@ -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);

View file

@ -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);

View file

@ -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);
}
}

View file

@ -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);

View file

@ -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;

View file

@ -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;