summaryrefslogtreecommitdiff
path: root/mm/2s2h/Enhancements/Saving/NewFileSetup.cpp
diff options
context:
space:
mode:
Diffstat (limited to 'mm/2s2h/Enhancements/Saving/NewFileSetup.cpp')
-rw-r--r--mm/2s2h/Enhancements/Saving/NewFileSetup.cpp517
1 files changed, 517 insertions, 0 deletions
diff --git a/mm/2s2h/Enhancements/Saving/NewFileSetup.cpp b/mm/2s2h/Enhancements/Saving/NewFileSetup.cpp
new file mode 100644
index 000000000..4f69381ca
--- /dev/null
+++ b/mm/2s2h/Enhancements/Saving/NewFileSetup.cpp
@@ -0,0 +1,517 @@
+#include <libultraship/bridge/consolevariablebridge.h>
+#include <cstring>
+#include <string>
+#include <vector>
+
+#include <fstream>
+#include <unordered_map>
+
+#include "2s2h/Enhancements/FrameInterpolation/FrameInterpolation.h"
+#include "2s2h/GameInteractor/GameInteractor.h"
+#include "2s2h/PresetManager/PresetManager.h"
+#include "2s2h/ShipInit.hpp"
+#include "2s2h/ShipUtils.h"
+#include "2s2h/BenPort.h"
+#include "2s2h/Rando/MiscBehavior/MiscBehavior.h"
+#include "2s2h/Rando/Spoiler/Spoiler.h"
+
+extern "C" {
+#include "functions.h"
+#include "macros.h"
+#include "variables.h"
+#include "overlays/gamestates/ovl_file_choose/z_file_select.h"
+#include "misc/title_static/title_static.h"
+#include "2s2h_assets.h"
+
+extern FileSelectState* gFileSelectState;
+}
+
+#define CVAR_NAME "gEnhancements.Saving.NewFileSetup"
+#define CVAR CVarGetInteger(CVAR_NAME, 1)
+
+typedef enum {
+ NEW_FILE_SETUP_ROW_PRESET,
+ NEW_FILE_SETUP_ROW_MODE,
+ NEW_FILE_SETUP_ROW_SEED,
+ NEW_FILE_SETUP_ROW_MAX,
+} NewFileSetupRow;
+
+typedef struct {
+ const char* texture;
+ s16 width;
+ // Where the actual letters start inside the texture, so we can account for it when padding
+ s16 inkX;
+ s16 inkY;
+ s16 inkWidth;
+} SetupLabel;
+
+#define SETUP_CHAR_WIDTH 10
+#define SETUP_CHAR_HEIGHT 10
+#define SETUP_CHARS_PER_BATCH 8
+#define SETUP_LABEL_HEIGHT 16
+#define SETUP_TITLE_X -93
+#define SETUP_TITLE_Y 71
+#define SETUP_HEADING_X -97
+#define SETUP_SECTION_LIFT 4
+#define SETUP_SECTION_Y(row) (40 - ((row)*40) + ((row) > 0 ? SETUP_SECTION_LIFT : 0))
+#define SETUP_DIVIDER_Y(row) (SETUP_SECTION_Y(row) - 12)
+#define SETUP_VALUE_Y(row) (SETUP_SECTION_Y(row) - 17)
+#define SETUP_VALUE_X -95
+#define SETUP_VALUE_GAP 20
+#define SETUP_DIVIDER_LEFT -100
+#define SETUP_DIVIDER_WIDTH 204
+#define SETUP_DIVIDER_TEX_INK 204
+#define SETUP_ARROW_MARGIN 10
+#define SETUP_CYCLER_CENTER_X (SETUP_DIVIDER_LEFT + (SETUP_DIVIDER_WIDTH / 2))
+#define SETUP_PRESET_TEXT_WIDTH 160
+#define SETUP_SEED_ICONS 5
+#define SETUP_SEED_ICON_SIZE 16
+#define SETUP_SEED_ICON_GAP 4
+
+static s16 sRow = NEW_FILE_SETUP_ROW_PRESET;
+static s16 sAlpha = 0;
+static bool sIsRando = false;
+static s16 sPresetIndex = 0;
+static std::vector<std::string> sPresetNames;
+static std::string sPresetDisplay;
+static s16 sPresetDisplayWidth = 0;
+static s16 sSeedIndex = 0;
+static std::unordered_map<std::string, uint32_t> sSeedHashes;
+static SetupLabel sTitle = { gFileSelNewFileTex, 56, 4, 3, 48 };
+static SetupLabel sHeadings[NEW_FILE_SETUP_ROW_MAX] = {
+ { gFileSelPresetHeaderTex, 48, 3, 4, 41 },
+ { gFileSelModeHeaderTex, 40, 5, 4, 30 },
+ { gFileSelSeedHeaderTex, 32, 2, 4, 28 },
+};
+static SetupLabel sModeChoices[] = {
+ { gFileSelVanillaTex, 48, 5, 3, 38 },
+ { gFileSelRandomizerTex, 80, 5, 3, 69 },
+};
+static SetupLabel sGenerateNew = { gFileSelGenerateNewTex, 88, 3, 3, 82 };
+static SetupLabel sArrowLeft = { gFileSelArrowLeftTex, 16, 4, 4, 7 };
+static SetupLabel sArrowRight = { gFileSelArrowRightTex, 16, 4, 4, 7 };
+static s16 sCursorPrim[3] = { 255, 255, 255 };
+static s16 sCursorEnv[3] = { 0, 0, 0 };
+static s16 sCursorPulseDir = 1;
+static s16 sCursorTimer = 20;
+static const s16 sCursorPrimTargets[2][3] = { { 255, 255, 255 }, { 0, 255, 255 } };
+static const s16 sCursorEnvTargets[2][3] = { { 0, 0, 0 }, { 0, 150, 150 } };
+
+static bool RowIsVisible(s16 row) {
+ return (row != NEW_FILE_SETUP_ROW_SEED) || sIsRando;
+}
+
+static s16 NextVisibleRow(s16 row, s16 step) {
+ do {
+ row = (s16)((row + step + NEW_FILE_SETUP_ROW_MAX) % NEW_FILE_SETUP_ROW_MAX);
+ } while (!RowIsVisible(row));
+
+ return row;
+}
+
+static uint32_t SeedHashForSpoiler(const std::string& fileName) {
+ auto cached = sSeedHashes.find(fileName);
+ if (cached != sSeedHashes.end()) {
+ return cached->second;
+ }
+
+ uint32_t hash = 0;
+ try {
+ // performance optimization: only parse the top level of the spoiler file, since that's all we need
+ auto topLevelOnly = [](int depth, nlohmann::json::parse_event_t, nlohmann::json&) { return depth <= 1; };
+
+ std::ifstream stream(Ship::Context::GetPathRelativeToAppDirectory("randomizer/" + fileName, appShortName));
+ nlohmann::json spoiler = nlohmann::json::parse(stream, topLevelOnly);
+
+ if (spoiler.contains("finalSeed")) {
+ hash = spoiler["finalSeed"].get<uint32_t>();
+ }
+ } catch (...) { hash = 0; }
+
+ sSeedHashes[fileName] = hash;
+ return hash;
+}
+
+// Some sort of vtx black magic, archez would understand it
+static void SetQuadVtx(Vtx* vtx, s16 left, s16 top, s16 width, s16 height, s16 texWidth, s16 texHeight) {
+ vtx[0].v.ob[0] = vtx[2].v.ob[0] = left;
+ vtx[1].v.ob[0] = vtx[3].v.ob[0] = left + width;
+
+ vtx[0].v.ob[1] = vtx[1].v.ob[1] = top;
+ vtx[2].v.ob[1] = vtx[3].v.ob[1] = top - height;
+
+ vtx[0].v.tc[0] = vtx[0].v.tc[1] = vtx[1].v.tc[1] = vtx[2].v.tc[0] = 0;
+ vtx[1].v.tc[0] = vtx[3].v.tc[0] = texWidth << 5;
+ vtx[2].v.tc[1] = vtx[3].v.tc[1] = texHeight << 5;
+
+ for (s16 i = 0; i < 4; i++) {
+ vtx[i].v.ob[2] = 0;
+ vtx[i].v.flag = 0;
+ vtx[i].v.cn[0] = vtx[i].v.cn[1] = vtx[i].v.cn[2] = vtx[i].v.cn[3] = 255;
+ }
+}
+
+static void SetLabelCombine(GraphicsContext* gfxCtx) {
+ OPEN_DISPS(gfxCtx);
+
+ gDPPipeSync(POLY_OPA_DISP++);
+ gDPSetCombineLERP(POLY_OPA_DISP++, PRIMITIVE, ENVIRONMENT, TEXEL0, ENVIRONMENT, TEXEL0, 0, PRIMITIVE, 0, PRIMITIVE,
+ ENVIRONMENT, TEXEL0, ENVIRONMENT, TEXEL0, 0, PRIMITIVE, 0);
+
+ CLOSE_DISPS(gfxCtx);
+}
+
+// Cursor pulsing from white to blue like the rest of the menus
+static void UpdateCursorColor(void) {
+ for (s16 i = 0; i < 3; i++) {
+ s16 primStep = ABS_ALT(sCursorPrim[i] - sCursorPrimTargets[sCursorPulseDir][i]) / sCursorTimer;
+ s16 envStep = ABS_ALT(sCursorEnv[i] - sCursorEnvTargets[sCursorPulseDir][i]) / sCursorTimer;
+
+ sCursorPrim[i] += (sCursorPrim[i] >= sCursorPrimTargets[sCursorPulseDir][i]) ? -primStep : primStep;
+ sCursorEnv[i] += (sCursorEnv[i] >= sCursorEnvTargets[sCursorPulseDir][i]) ? -envStep : envStep;
+ }
+
+ if (--sCursorTimer == 0) {
+ for (s16 i = 0; i < 3; i++) {
+ sCursorPrim[i] = sCursorPrimTargets[sCursorPulseDir][i];
+ sCursorEnv[i] = sCursorEnvTargets[sCursorPulseDir][i];
+ }
+ sCursorTimer = 20;
+ sCursorPulseDir ^= 1;
+ }
+}
+
+static void SetValueColor(FileSelectState* fileSelect, bool isCurrent, bool rowSelected, u8 alpha) {
+ OPEN_DISPS(fileSelect->state.gfxCtx);
+
+ gDPPipeSync(POLY_OPA_DISP++);
+ if (!isCurrent) {
+ gDPSetPrimColor(POLY_OPA_DISP++, 0, 0, 120, 120, 120, alpha);
+ gDPSetEnvColor(POLY_OPA_DISP++, 0, 0, 0, 255);
+ } else if (rowSelected) {
+ gDPSetPrimColor(POLY_OPA_DISP++, 0, 0, sCursorPrim[0], sCursorPrim[1], sCursorPrim[2], alpha);
+ gDPSetEnvColor(POLY_OPA_DISP++, sCursorEnv[0], sCursorEnv[1], sCursorEnv[2], 255);
+ } else {
+ gDPSetPrimColor(POLY_OPA_DISP++, 0, 0, 255, 255, 255, alpha);
+ gDPSetEnvColor(POLY_OPA_DISP++, 0, 0, 0, 255);
+ }
+
+ CLOSE_DISPS(fileSelect->state.gfxCtx);
+}
+
+static s16 GlyphAdvance(char c) {
+ return (s16)(Ship_GetCharFontWidthNES((u8)c) * SETUP_CHAR_WIDTH / (f32)FONT_CHAR_TEX_WIDTH);
+}
+
+static s16 TextWidth(const char* text) {
+ s16 width = 0;
+
+ for (const char* c = text; *c != '\0'; c++) {
+ width += GlyphAdvance(*c);
+ }
+
+ return width;
+}
+
+static void DrawText(FileSelectState* fileSelect, const char* text, s16 x, s16 top) {
+ s16 length = (s16)strlen(text);
+
+ if (length <= 0) {
+ return;
+ }
+
+ Vtx* vtx = (Vtx*)GRAPH_ALLOC(fileSelect->state.gfxCtx, length * 4 * sizeof(Vtx));
+
+ s16 pen = x;
+ for (s16 i = 0; i < length; i++) {
+ SetQuadVtx(&vtx[i * 4], pen, top, SETUP_CHAR_WIDTH, SETUP_CHAR_HEIGHT, FONT_CHAR_TEX_WIDTH,
+ FONT_CHAR_TEX_HEIGHT);
+ pen += GlyphAdvance(text[i]);
+ }
+
+ OPEN_DISPS(fileSelect->state.gfxCtx);
+
+ gDPPipeSync(POLY_OPA_DISP++);
+ gDPSetCombineLERP(POLY_OPA_DISP++, 0, 0, 0, PRIMITIVE, TEXEL0, 0, PRIMITIVE, 0, 0, 0, 0, PRIMITIVE, TEXEL0, 0,
+ PRIMITIVE, 0);
+
+ for (s16 i = 0; i < length; i += SETUP_CHARS_PER_BATCH) {
+ s16 count = (s16)MIN(SETUP_CHARS_PER_BATCH, length - i);
+
+ gSPVertex(POLY_OPA_DISP++, (uintptr_t)&vtx[i * 4], count * 4, 0);
+
+ for (s16 j = 0; j < count; j++) {
+ FileSelect_DrawTexQuadI4(fileSelect->state.gfxCtx, Ship_GetCharFontTextureNES(text[i + j]), j * 4);
+ }
+ }
+
+ CLOSE_DISPS(fileSelect->state.gfxCtx);
+}
+
+static void DrawLabel(FileSelectState* fileSelect, const SetupLabel* label, s16 inkLeft, s16 inkTop) {
+ Vtx* vtx = (Vtx*)GRAPH_ALLOC(fileSelect->state.gfxCtx, 4 * sizeof(Vtx));
+
+ // Positioned by the lettering rather than the texture corner, so the padding around it never matters
+ SetQuadVtx(vtx, inkLeft - label->inkX, inkTop + label->inkY, label->width, SETUP_LABEL_HEIGHT, label->width,
+ SETUP_LABEL_HEIGHT);
+
+ OPEN_DISPS(fileSelect->state.gfxCtx);
+
+ gSPVertex(POLY_OPA_DISP++, (uintptr_t)vtx, 4, 0);
+ gDPLoadTextureBlock(POLY_OPA_DISP++, label->texture, G_IM_FMT_IA, G_IM_SIZ_8b, label->width, SETUP_LABEL_HEIGHT, 0,
+ G_TX_NOMIRROR | G_TX_WRAP, G_TX_NOMIRROR | G_TX_WRAP, G_TX_NOMASK, G_TX_NOMASK, G_TX_NOLOD,
+ G_TX_NOLOD);
+ gSP1Quadrangle(POLY_OPA_DISP++, 0, 2, 3, 1, 0);
+
+ CLOSE_DISPS(fileSelect->state.gfxCtx);
+}
+
+// These match the look of the dividers in the option menu
+static void DrawDivider(FileSelectState* fileSelect, s16 top, u8 alpha) {
+ Vtx* vtx = (Vtx*)GRAPH_ALLOC(fileSelect->state.gfxCtx, 4 * sizeof(Vtx));
+
+ SetQuadVtx(vtx, SETUP_DIVIDER_LEFT, top, SETUP_DIVIDER_WIDTH, 2, SETUP_DIVIDER_TEX_INK, 2);
+
+ OPEN_DISPS(fileSelect->state.gfxCtx);
+
+ SetLabelCombine(fileSelect->state.gfxCtx);
+ gDPSetPrimColor(POLY_OPA_DISP++, 0, 0, 0, 255, 255, alpha);
+ gDPSetEnvColor(POLY_OPA_DISP++, 0, 0, 0, 0);
+ gSPVertex(POLY_OPA_DISP++, (uintptr_t)vtx, 4, 0);
+ gDPLoadTextureBlock_4b(POLY_OPA_DISP++, gFileSelOptionsDividerTex, G_IM_FMT_IA, 256, 2, 0,
+ G_TX_NOMIRROR | G_TX_WRAP, G_TX_NOMIRROR | G_TX_WRAP, G_TX_NOMASK, G_TX_NOMASK, G_TX_NOLOD,
+ G_TX_NOLOD);
+ gSP1Quadrangle(POLY_OPA_DISP++, 0, 2, 3, 1, 0);
+
+ CLOSE_DISPS(fileSelect->state.gfxCtx);
+}
+
+static void DrawArrows(FileSelectState* fileSelect, s16 inkTop) {
+ DrawLabel(fileSelect, &sArrowLeft, SETUP_DIVIDER_LEFT + SETUP_ARROW_MARGIN, inkTop);
+ DrawLabel(fileSelect, &sArrowRight,
+ SETUP_DIVIDER_LEFT + SETUP_DIVIDER_WIDTH - SETUP_ARROW_MARGIN - sArrowRight.inkWidth, inkTop);
+}
+
+static void DrawSeedIcons(FileSelectState* fileSelect, uint32_t hash, s16 centerX, s16 centerY, u8 alpha) {
+ const s16 pitch = SETUP_SEED_ICON_SIZE + SETUP_SEED_ICON_GAP;
+ s16 left = centerX - (((SETUP_SEED_ICONS * pitch) - SETUP_SEED_ICON_GAP) / 2);
+
+ OPEN_DISPS(fileSelect->state.gfxCtx);
+
+ gDPPipeSync(POLY_OPA_DISP++);
+ gDPSetCombineMode(POLY_OPA_DISP++, G_CC_MODULATEIA_PRIM, G_CC_MODULATEIA_PRIM);
+ gDPSetPrimColor(POLY_OPA_DISP++, 0, 0, 255, 255, 255, alpha);
+
+ for (s16 i = 0; i < SETUP_SEED_ICONS; i++) {
+ Rando::MiscBehavior::Sprite* sprite = Rando::MiscBehavior::GetSeedTexture((u8)(hash % 100));
+ hash /= 100;
+
+ Vtx* vtx = (Vtx*)GRAPH_ALLOC(fileSelect->state.gfxCtx, 4 * sizeof(Vtx));
+ SetQuadVtx(vtx, left + (i * pitch), centerY + (SETUP_SEED_ICON_SIZE / 2), SETUP_SEED_ICON_SIZE,
+ SETUP_SEED_ICON_SIZE, sprite->width, sprite->height);
+
+ gSPVertex(POLY_OPA_DISP++, (uintptr_t)vtx, 4, 0);
+ gDPLoadTextureBlock(POLY_OPA_DISP++, sprite->tex, G_IM_FMT_RGBA, G_IM_SIZ_32b, sprite->width, sprite->height, 0,
+ G_TX_NOMIRROR | G_TX_CLAMP, G_TX_NOMIRROR | G_TX_CLAMP, G_TX_NOMASK, G_TX_NOMASK,
+ G_TX_NOLOD, G_TX_NOLOD);
+ gSP1Quadrangle(POLY_OPA_DISP++, 0, 2, 3, 1, 0);
+ }
+
+ CLOSE_DISPS(fileSelect->state.gfxCtx);
+}
+
+static void UpdatePresetDisplay(void) {
+ sPresetDisplay = sPresetNames.empty() ? "" : sPresetNames[sPresetIndex];
+
+ while (sPresetDisplay.length() > 4 && TextWidth(sPresetDisplay.c_str()) > SETUP_PRESET_TEXT_WIDTH) {
+ sPresetDisplay = sPresetDisplay.substr(0, sPresetDisplay.length() - 4) + "...";
+ }
+
+ sPresetDisplayWidth = TextWidth(sPresetDisplay.c_str());
+}
+
+static void RefreshPresetList(void) {
+ sPresetNames.clear();
+ sPresetNames.push_back("None");
+
+ for (const auto& name : PresetManager_GetPresetNames()) {
+ sPresetNames.push_back(name);
+ }
+
+ sPresetIndex = 0;
+ UpdatePresetDisplay();
+}
+
+static void RefreshSeedList(void) {
+ Rando::Spoiler::RefreshOptions();
+ // The list is never empty in practice, but clamping guards against indexing it with -1 if it were
+ s32 last = (s32)Rando::Spoiler::spoilerOptions.size() - 1;
+ sSeedIndex = (last < 0) ? 0 : (s16)CLAMP(CVarGetInteger("gRando.SpoilerFileIndex", 0), 0, last);
+}
+
+static void ApplySelection(void) {
+ if ((sPresetIndex > 0) && (sPresetIndex < (s16)sPresetNames.size())) {
+ PresetManager_ApplyPresetByName(sPresetNames[sPresetIndex]);
+ }
+
+ CVarSetInteger("gRando.Enabled", sIsRando ? 1 : 0);
+
+ // OnFileCreate reads the pair to decide whether to generate a seed or replay a spoiler
+ Rando::Spoiler::SelectSpoiler(sIsRando ? sSeedIndex : 0);
+
+ CVarSave();
+}
+
+static void LeaveSetup(FileSelectState* fileSelect, s16 configMode) {
+ sAlpha = 0;
+ fileSelect->configMode = configMode;
+}
+
+extern "C" void FileSelect_RotateToNewFileSetup(GameState* thisx) {
+ FileSelectState* fileSelect = (FileSelectState*)thisx;
+
+ fileSelect->windowRot += 50.0f;
+
+ if (fileSelect->windowRot >= 314.0f) {
+ fileSelect->windowRot = 314.0f;
+ fileSelect->configMode = CM_2S2H_NEW_FILE_SETUP;
+ }
+}
+
+extern "C" void FileSelect_UpdateNewFileSetup(GameState* thisx) {
+ FileSelectState* fileSelect = (FileSelectState*)thisx;
+ Input* input = CONTROLLER1(&fileSelect->state);
+ s16 presetCount = (s16)sPresetNames.size();
+
+ sAlpha = (s16)MIN(sAlpha + 25, 255);
+
+ if (CHECK_BTN_ALL(input->press.button, BTN_B)) {
+ Audio_PlaySfx(NA_SE_SY_FSEL_CLOSE);
+ LeaveSetup(fileSelect, CM_2S2H_NEW_FILE_SETUP_TO_MAIN);
+ return;
+ }
+
+ if (CHECK_BTN_ALL(input->press.button, BTN_A) || CHECK_BTN_ALL(input->press.button, BTN_START)) {
+ Audio_PlaySfx(NA_SE_SY_FSEL_DECIDE_L);
+ ApplySelection();
+ LeaveSetup(fileSelect, CM_START_NAME_ENTRY);
+ return;
+ }
+
+ bool up = (fileSelect->stickAdjY > 30) || CHECK_BTN_ALL(input->press.button, BTN_DUP);
+ bool down = (fileSelect->stickAdjY < -30) || CHECK_BTN_ALL(input->press.button, BTN_DDOWN);
+
+ if (up || down) {
+ Audio_PlaySfx(NA_SE_SY_FSEL_CURSOR);
+ sRow = NextVisibleRow(sRow, up ? -1 : 1);
+ return;
+ }
+
+ bool left = (fileSelect->stickAdjX < -30) || CHECK_BTN_ALL(input->press.button, BTN_DLEFT);
+ bool right = (fileSelect->stickAdjX > 30) || CHECK_BTN_ALL(input->press.button, BTN_DRIGHT);
+
+ if (!left && !right) {
+ return;
+ }
+
+ Audio_PlaySfx(NA_SE_SY_FSEL_CURSOR);
+
+ if (sRow == NEW_FILE_SETUP_ROW_MODE) {
+ sIsRando = !sIsRando;
+ } else if (sRow == NEW_FILE_SETUP_ROW_SEED) {
+ s16 seedCount = (s16)Rando::Spoiler::spoilerOptions.size();
+ sSeedIndex = (s16)((sSeedIndex + (right ? 1 : seedCount - 1)) % seedCount);
+ } else {
+ sPresetIndex = (s16)((sPresetIndex + (right ? 1 : presetCount - 1)) % presetCount);
+ UpdatePresetDisplay();
+ }
+}
+
+extern "C" void FileSelect_DrawNewFileSetup(GameState* thisx) {
+ FileSelectState* fileSelect = (FileSelectState*)thisx;
+ u8 alpha = (u8)sAlpha;
+
+ if (alpha == 0) {
+ return;
+ }
+
+ UpdateCursorColor();
+
+ OPEN_DISPS(fileSelect->state.gfxCtx);
+
+ Gfx_SetupDL42_Opa(fileSelect->state.gfxCtx);
+ SetLabelCombine(fileSelect->state.gfxCtx);
+
+ // Title and section headings are plain white; only values take the selection colours
+ gDPSetPrimColor(POLY_OPA_DISP++, 0, 0, 255, 255, 255, alpha);
+ gDPSetEnvColor(POLY_OPA_DISP++, 0, 0, 0, 255);
+ DrawLabel(fileSelect, &sTitle, SETUP_TITLE_X, SETUP_TITLE_Y);
+
+ for (s16 row = 0; row < NEW_FILE_SETUP_ROW_MAX; row++) {
+ if (!RowIsVisible(row)) {
+ continue;
+ }
+ gDPPipeSync(POLY_OPA_DISP++);
+ gDPSetPrimColor(POLY_OPA_DISP++, 0, 0, 255, 255, 255, alpha);
+ gDPSetEnvColor(POLY_OPA_DISP++, 0, 0, 0, 255);
+ DrawLabel(fileSelect, &sHeadings[row], SETUP_HEADING_X, SETUP_SECTION_Y(row));
+ DrawDivider(fileSelect, SETUP_DIVIDER_Y(row), alpha);
+ }
+
+ // Mode: both values side by side
+ s16 valueX = SETUP_VALUE_X;
+ for (s16 i = 0; i < (s16)ARRAY_COUNT(sModeChoices); i++) {
+ SetValueColor(fileSelect, i == (sIsRando ? 1 : 0), sRow == NEW_FILE_SETUP_ROW_MODE, alpha);
+ DrawLabel(fileSelect, &sModeChoices[i], valueX, SETUP_VALUE_Y(NEW_FILE_SETUP_ROW_MODE));
+ valueX += sModeChoices[i].inkWidth + SETUP_VALUE_GAP;
+ }
+
+ // Seed: "Generate New" first, then every spoiler in the folder as its own icons
+ if (RowIsVisible(NEW_FILE_SETUP_ROW_SEED)) {
+ bool seedRowSelected = (sRow == NEW_FILE_SETUP_ROW_SEED);
+ s16 seedY = SETUP_VALUE_Y(NEW_FILE_SETUP_ROW_SEED);
+
+ SetValueColor(fileSelect, true, seedRowSelected, alpha);
+ DrawArrows(fileSelect, seedY);
+
+ if (sSeedIndex == 0) {
+ DrawLabel(fileSelect, &sGenerateNew, SETUP_CYCLER_CENTER_X - (sGenerateNew.inkWidth / 2), seedY);
+ } else if (sSeedIndex < (s16)Rando::Spoiler::spoilerOptions.size()) {
+ DrawSeedIcons(fileSelect, SeedHashForSpoiler(Rando::Spoiler::spoilerOptions[sSeedIndex]),
+ SETUP_CYCLER_CENTER_X, seedY - 4, alpha);
+ SetLabelCombine(fileSelect->state.gfxCtx);
+ }
+ }
+
+ // Preset: one at a time
+ bool presetRowSelected = (sRow == NEW_FILE_SETUP_ROW_PRESET);
+ s16 presetY = SETUP_VALUE_Y(NEW_FILE_SETUP_ROW_PRESET);
+
+ SetValueColor(fileSelect, true, presetRowSelected, alpha);
+ DrawArrows(fileSelect, presetY);
+
+ // Inherits the primitive SetValueColor set for this row, which is what tints the arrows too
+ DrawText(fileSelect, sPresetDisplay.c_str(), SETUP_CYCLER_CENTER_X - (sPresetDisplayWidth / 2), presetY - 1);
+
+ gDPPipeSync(POLY_OPA_DISP++);
+ gDPSetCombineMode(POLY_OPA_DISP++, G_CC_MODULATEIDECALA, G_CC_MODULATEIDECALA);
+
+ CLOSE_DISPS(fileSelect->state.gfxCtx);
+}
+
+void RegisterNewFileSetup() {
+ COND_VB_SHOULD(VB_FILE_SELECT_ROTATE_TO_NAME_ENTRY, CVAR, {
+ FileSelectState* fileSelect = va_arg(args, FileSelectState*);
+
+ sRow = NEW_FILE_SETUP_ROW_PRESET;
+ sAlpha = 0;
+ sIsRando = CVarGetInteger("gRando.Enabled", 0) != 0;
+ RefreshPresetList();
+ RefreshSeedList();
+
+ fileSelect->configMode = CM_2S2H_ROTATE_TO_NEW_FILE_SETUP;
+ *should = false;
+ });
+}
+
+static RegisterShipInitFunc initFunc(RegisterNewFileSetup, { CVAR_NAME });