Ocarina of Time
Miscellaneous
Text option buffering
Shop menu input buffering
Sometimes speedrunners have run into an issue where inputting a diagonal cursor movement in the shop too early (i.e. while the shop camera is panning from the shopkeeper to the right/left shelves) causes the cursor to only move vertically. Upon more precise examination, this occurs when holding a diagonal input too early — the issue is that, while the camera is panning the code responsible for "buffering" cursor inputs always fails a check for horizontal inputs but always passes a check for vertical inputs.
The Shopkeeper actor (which handles all the relevant code for shops and shop menus) is called EnOssan in decomp. Its struct has four relevant members:
stickAccumXstickAccumYmoveHorizontalmoveVertical
EnOssan_UpdateJoystickInputState sets moveHorizontal and moveVertical to false each frame it runs before continuing, then it increments stickAccumX and stickAccumY by some amounts based on your control stick input (on the corresponding axis). Functionally it always ignores relative stick values (i.e. raw stick values minus 7) of less than 31 magnitude (i.e. 38 on input display), and sets the corresponding accumulator to 0 if the input is that small on that axis.
If stickAccum was 0 or in the opposite direction that you are now holding: it just sets stickAccum to your current stick X or Y value, because it considers you to have just started holding the stick that frame. Otherwise, it continues to increment the accumulator magnitude each frame, but it only sets moveHorizontal and moveVertical the frame you start holding the stick (or switch directions on the stick).
Moving onto the cursor movement: the relevant functions are EnOssan_State_BrowseRightShelf and EnOssan_State_BrowseLeftShelf, which run when you move from the shopkeeper left or right to the shelves. Both contain a large conditional block (included below) that only runs once the current message state is TEXT_STATE_EVENT, which occurs when a shop item is selected and the price/quantity info is displaying. This happens several frames after the blue cursor has already appeared around the item.
Click to expand relevant codeblock
if (this->moveHorizontal) {
if (this->stickAccumX > 0) {
a = EnOssan_CursorRight(this, this->cursorIndex, 4);
if (a != CURSOR_INVALID) {
this->cursorIndex = a;
} else {
EnOssan_SetLookToShopkeeperFromShelf(play, this);
return;
}
} else if (this->stickAccumX < 0) {
b = EnOssan_CursorLeft(this, this->cursorIndex, 8);
if (b != CURSOR_INVALID) {
this->cursorIndex = b;
}
}
} else {
if (this->stickAccumX > 0 && this->stickAccumX > 500) {
c = EnOssan_CursorRight(this, this->cursorIndex, 4);
if (c != CURSOR_INVALID) {
this->cursorIndex = c;
} else {
EnOssan_SetLookToShopkeeperFromShelf(play, this);
return;
}
} else if (this->stickAccumX < 0 && this->stickAccumX < -500) {
d = EnOssan_CursorLeft(this, this->cursorIndex, 8);
if (d != CURSOR_INVALID) {
this->cursorIndex = d;
}
}
}
EnOssan_CursorUpDown(this);
The main issue is that it checks moveHorizontal OR stickAccumX > 500 / stickAccumX < -500 for whether to move the cursor horizontally, but stickAccumX gets set to 0 every frame while the camera is panning towards the shelves so you can never reach 500 beforehand by holding left or right. Consequently moveHorizontal actually gets set every frame the camera is panning, but at the end when the accumulator is allowed to increase instead of zeroing, moveHorizontal stops being set, while stickAccumX can not reach 500 in time.
However, it does not perform the same checks on the Y-axis for some reason — it calls EnOssan_CursorUpDown (which also handles some unrelated logic) which only requires stickAccumY != 0 to move the cursor in a direction; therefore, the vertical movement is always possible frame 1 (if your Y input magnitude is 38 or greater), while the horizontal movement always fails.
Of course, this does not just apply to diagonal inputs — a consequence of this is that horizontal movements will be suboptimal if buffered (while vertical movements will come out frame 1). Timing a horizontal shop movement can be up to 3 frames faster than holding the stick.
Note the following in the video here, which demonstrates an early diagonal flick attempt:
- Watches are
moveHorizontalu8-EnOssan + 0x21cstickAccumXs32-EnOssan + 0x214stickAccumYs32-EnOssan + 0x218
moveHorizontalis set 1 frame too early andstickAccumXis below 500 at that point so it won't move horizontally, in a sense failing "between" the two conditionsstickAccumYis> 0so the Up/Down check passes
