diff options
| author | Garrett Cox <garrettjcox@gmail.com> | 2026-07-31 23:47:26 -0500 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2026-07-31 23:47:26 -0500 |
| commit | f740fe0506483200142108b64fb2abf2eb2f735e (patch) | |
| tree | 97e9cf92e1f05fa5abbb55c7bc3146412e329605 | |
| parent | 901fde33b6509403bea0ade78150837108ddb233 (diff) | |
Add new file setup screen, for selecting preset, rando/vanilla and seed (#1829)
23 files changed, 672 insertions, 24 deletions
diff --git a/mm/2s2h/BenGui/BenMenu.cpp b/mm/2s2h/BenGui/BenMenu.cpp index 56dad0d58..bff7bafe6 100644 --- a/mm/2s2h/BenGui/BenMenu.cpp +++ b/mm/2s2h/BenGui/BenMenu.cpp @@ -1211,6 +1211,12 @@ void BenMenu::AddEnhancements() { AddWidget(path, "3rd Save File Slot", WIDGET_CVAR_CHECKBOX) .CVar("gEnhancements.Saving.FileSlot3") .Options(CheckboxOptions().Tooltip("Adds a 3rd file slot that can be used for saves").DefaultValue(true)); + AddWidget(path, "New File Setup Steps", WIDGET_CVAR_CHECKBOX) + .CVar("gEnhancements.Saving.NewFileSetup") + .Options(CheckboxOptions() + .Tooltip("After picking an empty file, asks whether it should be a randomizer file and lets " + "you apply one of your loaded presets before entering a name.") + .DefaultValue(true)); AddWidget(path, "Persistent Owl Saves", WIDGET_CVAR_CHECKBOX) .CVar("gEnhancements.Saving.PersistentOwlSaves") .Options(CheckboxOptions().Tooltip("Continuing a save will not remove the owl save. Playing Song of " diff --git a/mm/2s2h/Enhancements/Saving/FileSlot3.cpp b/mm/2s2h/Enhancements/Saving/FileSlot3.cpp index 14a92acf2..82f753c6b 100644 --- a/mm/2s2h/Enhancements/Saving/FileSlot3.cpp +++ b/mm/2s2h/Enhancements/Saving/FileSlot3.cpp @@ -11,8 +11,15 @@ extern FileSelectState* gFileSelectState; #define CVAR CVarGetInteger(CVAR_NAME, true) void RegisterFileSlot3() { - // Reset the file select state to reload the save metadata - if (gFileSelectState != NULL) { + static int lastValue = -1; + int value = CVAR; + bool changed = (lastValue != -1) && (lastValue != value); + lastValue = value; + + // When we're adding the third file slot we need to refresh the entire file select state. We're only wanting + // to do this when the value changes, as this hook gets run when a preset is applied, and the user might + // have been in the middle of a file creation when they applied the preset. + if (changed && gFileSelectState != NULL) { STOP_GAMESTATE(&gFileSelectState->state); SET_NEXT_GAMESTATE(&gFileSelectState->state, FileSelect_Init, sizeof(FileSelectState)); } 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 }); diff --git a/mm/2s2h/GameInteractor/GameInteractor_VanillaBehavior.h b/mm/2s2h/GameInteractor/GameInteractor_VanillaBehavior.h index f2067542f..1fc58ca1d 100644 --- a/mm/2s2h/GameInteractor/GameInteractor_VanillaBehavior.h +++ b/mm/2s2h/GameInteractor/GameInteractor_VanillaBehavior.h @@ -554,6 +554,14 @@ typedef enum { // true // ``` // #### `args` + // - `*FileSelectState` + VB_FILE_SELECT_ROTATE_TO_NAME_ENTRY, + + // #### `result` + // ```c + // true + // ``` + // #### `args` // - `*ItemId` // - `s32` (slot) // - `s32` (isDpad) diff --git a/mm/2s2h/PresetManager/PresetManager.cpp b/mm/2s2h/PresetManager/PresetManager.cpp index 44dea18be..9121944ce 100644 --- a/mm/2s2h/PresetManager/PresetManager.cpp +++ b/mm/2s2h/PresetManager/PresetManager.cpp @@ -1,5 +1,6 @@ #include "PresetManager.h" #include <libultraship/bridge/consolevariablebridge.h> +#include <algorithm> #include <filesystem> #include <fstream> #include <set> @@ -815,6 +816,32 @@ void PresetManager_CreatePreset(std::string presetName) { } catch (...) { Notification::Emit({ .suffix = "Failed to create preset" }); } } +std::vector<std::string> PresetManager_GetPresetNames() { + std::vector<std::string> names; + names.reserve(presets.size()); + + for (const auto& [name, pair] : presets) { + names.push_back(name); + } + + // presets is an unordered map, sort it + std::sort(names.begin(), names.end()); + + return names; +} + +bool PresetManager_ApplyPresetByName(const std::string& name) { + auto it = presets.find(name); + + if (it == presets.end()) { + return false; + } + + PresetManager_ApplyPreset(it->second.first); + + return true; +} + bool PresetManager_HandleFileDropped(char* filePath) { try { std::ifstream fileStream(filePath); diff --git a/mm/2s2h/PresetManager/PresetManager.h b/mm/2s2h/PresetManager/PresetManager.h index 7b55c97f7..484dedf31 100644 --- a/mm/2s2h/PresetManager/PresetManager.h +++ b/mm/2s2h/PresetManager/PresetManager.h @@ -3,8 +3,12 @@ #define PRESET_MANAGER_H #include <string> +#include <vector> bool PresetManager_HandleFileDropped(const std::string& filePath); void PresetManager_Draw(); +std::vector<std::string> PresetManager_GetPresetNames(); +bool PresetManager_ApplyPresetByName(const std::string& name); + #endif // PRESET_MANAGER_H diff --git a/mm/2s2h/Rando/Menu.cpp b/mm/2s2h/Rando/Menu.cpp index 5be6cbd1f..9a05a6ecf 100644 --- a/mm/2s2h/Rando/Menu.cpp +++ b/mm/2s2h/Rando/Menu.cpp @@ -372,12 +372,7 @@ static void DrawGeneralTab() { UIWidgets::CVarCheckbox("Enable Rando (Randomizes new files upon creation)", "gRando.Enabled"); if (UIWidgets::CVarCombobox("Seed", "gRando.SpoilerFileIndex", Rando::Spoiler::spoilerOptions)) { - if (CVarGetInteger("gRando.SpoilerFileIndex", 0) == 0) { - CVarSetString("gRando.SpoilerFile", ""); - } else { - CVarSetString("gRando.SpoilerFile", - Rando::Spoiler::spoilerOptions[CVarGetInteger("gRando.SpoilerFileIndex", 0)].c_str()); - } + Rando::Spoiler::SelectSpoiler(CVarGetInteger("gRando.SpoilerFileIndex", 0)); } if (CVarGetInteger("gRando.SpoilerFileIndex", 0) == 0) { diff --git a/mm/2s2h/Rando/MiscBehavior/FileSelect.cpp b/mm/2s2h/Rando/MiscBehavior/FileSelect.cpp index 32084a872..d7cda75f1 100644 --- a/mm/2s2h/Rando/MiscBehavior/FileSelect.cpp +++ b/mm/2s2h/Rando/MiscBehavior/FileSelect.cpp @@ -13,22 +13,13 @@ extern s16 sWindowContentColors[3]; extern FileSelectState* gFileSelectState; } -typedef struct { - char tex[512]; - uint16_t width; - uint16_t height; - uint8_t im_fmt; - uint8_t im_siz; - uint8_t id; -} Sprite; - // Image Icons #include <array> #include "assets/archives/icon_item_static/icon_item_static_yar.h" #include "assets/archives/icon_item_24_static/icon_item_24_static_yar.h" -inline std::array<Sprite, 100> gSeedTextures = { { +inline std::array<Rando::MiscBehavior::Sprite, 100> gSeedTextures = { { { dgItemIconOcarinaOfTimeTex, 32, 32, G_IM_FMT_RGBA, G_IM_SIZ_32b, 0 }, { dgItemIconBowTex, 32, 32, G_IM_FMT_RGBA, G_IM_SIZ_32b, 1 }, { dgItemIconFireArrowTex, 32, 32, G_IM_FMT_RGBA, G_IM_SIZ_32b, 2 }, @@ -131,7 +122,7 @@ inline std::array<Sprite, 100> gSeedTextures = { { { dgQuestIconSmallKeyTex, 24, 24, G_IM_FMT_RGBA, G_IM_SIZ_32b, 99 }, } }; -Sprite* GetSeedTexture(const uint8_t index) { +Rando::MiscBehavior::Sprite* Rando::MiscBehavior::GetSeedTexture(const uint8_t index) { return &gSeedTextures[index]; } @@ -248,7 +239,7 @@ void SetRandSaveTypeVtxData() { } } -void SpriteLoad(Sprite* sprite) { +void SpriteLoad(Rando::MiscBehavior::Sprite* sprite) { OPEN_DISPS(gFileSelectState->state.gfxCtx); /* @@ -274,7 +265,7 @@ void SpriteLoad(Sprite* sprite) { CLOSE_DISPS(gFileSelectState->state.gfxCtx); } -void SpriteDraw(Sprite* sprite, int left, int top, int width, int height) { +void SpriteDraw(Rando::MiscBehavior::Sprite* sprite, int left, int top, int width, int height) { int width_factor = (1 << 10) * sprite->width / width; int height_factor = (1 << 10) * sprite->height / height; @@ -305,8 +296,8 @@ void DrawSeedHashSprites() { for (int i = 0; i < 5; i++) { u8 hashPart = fileHash % 100; fileHash /= 100; - SpriteLoad(GetSeedTexture(hashPart)); - SpriteDraw(GetSeedTexture(hashPart), xStart + (40 * i), 10, 24, 24); + SpriteLoad(Rando::MiscBehavior::GetSeedTexture(hashPart)); + SpriteDraw(Rando::MiscBehavior::GetSeedTexture(hashPart), xStart + (40 * i), 10, 24, 24); } } } diff --git a/mm/2s2h/Rando/MiscBehavior/MiscBehavior.h b/mm/2s2h/Rando/MiscBehavior/MiscBehavior.h index 5d026bf9e..128674f3f 100644 --- a/mm/2s2h/Rando/MiscBehavior/MiscBehavior.h +++ b/mm/2s2h/Rando/MiscBehavior/MiscBehavior.h @@ -26,6 +26,16 @@ void SariasSongHint(); void BankSignHint(); void InitTycoonWallet(); +typedef struct { + char tex[512]; + uint16_t width; + uint16_t height; + uint8_t im_fmt; + uint8_t im_siz; + uint8_t id; +} Sprite; +Sprite* GetSeedTexture(const uint8_t index); + } // namespace MiscBehavior } // namespace Rando diff --git a/mm/2s2h/Rando/Spoiler/RefreshOptions.cpp b/mm/2s2h/Rando/Spoiler/RefreshOptions.cpp index 2ca62b82d..4c14f01e0 100644 --- a/mm/2s2h/Rando/Spoiler/RefreshOptions.cpp +++ b/mm/2s2h/Rando/Spoiler/RefreshOptions.cpp @@ -9,6 +9,13 @@ std::vector<std::string> Rando::Spoiler::spoilerOptions; const std::filesystem::path randomizerFolderPath(Ship::Context::GetPathRelativeToAppDirectory("randomizer", appShortName)); +void Rando::Spoiler::SelectSpoiler(s32 index) { + bool generateNew = (index <= 0) || (index >= (s32)Rando::Spoiler::spoilerOptions.size()); + + CVarSetInteger("gRando.SpoilerFileIndex", generateNew ? 0 : index); + CVarSetString("gRando.SpoilerFile", generateNew ? "" : Rando::Spoiler::spoilerOptions[index].c_str()); +} + // This function refreshes the list of spoiler files in the randomizer folder, this list is used in the Randomizer UI, // and also includes an option to generate a new seed at the top of the list. void Rando::Spoiler::RefreshOptions() { diff --git a/mm/2s2h/Rando/Spoiler/Spoiler.h b/mm/2s2h/Rando/Spoiler/Spoiler.h index 4cb145796..5939e54ba 100644 --- a/mm/2s2h/Rando/Spoiler/Spoiler.h +++ b/mm/2s2h/Rando/Spoiler/Spoiler.h @@ -11,6 +11,7 @@ namespace Spoiler { extern std::vector<std::string> spoilerOptions; void RefreshOptions(); +void SelectSpoiler(s32 index); nlohmann::json GenerateFromSaveContext(); void SaveToFile(const std::string& fileName, nlohmann::json spoiler); nlohmann::json LoadFromFile(const std::string& filePath); diff --git a/mm/assets/2s2h_assets.h b/mm/assets/2s2h_assets.h index 21905427e..d5d739f79 100644 --- a/mm/assets/2s2h_assets.h +++ b/mm/assets/2s2h_assets.h @@ -140,6 +140,34 @@ static const ALIGN_ASSET(2) char gFileSelCheatingDayTex[] = dgFileSelCheatingDay #define dgFileSelRandIconTex "__OTR__misc/title_static/gFileSelRandIconTex" static const ALIGN_ASSET(2) char gFileSelRandIconTex[] = dgFileSelRandIconTex; +// File select new file setup labels, generated by tools/label_gen/genlabel.py +#define dgFileSelNewFileTex "__OTR__misc/title_static/gFileSelNewFileTex" +static const ALIGN_ASSET(2) char gFileSelNewFileTex[] = dgFileSelNewFileTex; + +#define dgFileSelModeHeaderTex "__OTR__misc/title_static/gFileSelModeHeaderTex" +static const ALIGN_ASSET(2) char gFileSelModeHeaderTex[] = dgFileSelModeHeaderTex; + +#define dgFileSelSeedHeaderTex "__OTR__misc/title_static/gFileSelSeedHeaderTex" +static const ALIGN_ASSET(2) char gFileSelSeedHeaderTex[] = dgFileSelSeedHeaderTex; + +#define dgFileSelPresetHeaderTex "__OTR__misc/title_static/gFileSelPresetHeaderTex" +static const ALIGN_ASSET(2) char gFileSelPresetHeaderTex[] = dgFileSelPresetHeaderTex; + +#define dgFileSelGenerateNewTex "__OTR__misc/title_static/gFileSelGenerateNewTex" +static const ALIGN_ASSET(2) char gFileSelGenerateNewTex[] = dgFileSelGenerateNewTex; + +#define dgFileSelVanillaTex "__OTR__misc/title_static/gFileSelVanillaTex" +static const ALIGN_ASSET(2) char gFileSelVanillaTex[] = dgFileSelVanillaTex; + +#define dgFileSelRandomizerTex "__OTR__misc/title_static/gFileSelRandomizerTex" +static const ALIGN_ASSET(2) char gFileSelRandomizerTex[] = dgFileSelRandomizerTex; + +#define dgFileSelArrowLeftTex "__OTR__misc/title_static/gFileSelArrowLeftTex" +static const ALIGN_ASSET(2) char gFileSelArrowLeftTex[] = dgFileSelArrowLeftTex; + +#define dgFileSelArrowRightTex "__OTR__misc/title_static/gFileSelArrowRightTex" +static const ALIGN_ASSET(2) char gFileSelArrowRightTex[] = dgFileSelArrowRightTex; + #define dgBoxChestCornerHealthTex "__OTR__objects/object_box/gBoxChestCornerHealthTex" static const ALIGN_ASSET(2) char gBoxChestCornerHealthTex[] = dgBoxChestCornerHealthTex; diff --git a/mm/assets/custom/misc/title_static/gFileSelArrowLeftTex.ia8.png b/mm/assets/custom/misc/title_static/gFileSelArrowLeftTex.ia8.png Binary files differnew file mode 100644 index 000000000..690182903 --- /dev/null +++ b/mm/assets/custom/misc/title_static/gFileSelArrowLeftTex.ia8.png diff --git a/mm/assets/custom/misc/title_static/gFileSelArrowRightTex.ia8.png b/mm/assets/custom/misc/title_static/gFileSelArrowRightTex.ia8.png Binary files differnew file mode 100644 index 000000000..af6bd25d0 --- /dev/null +++ b/mm/assets/custom/misc/title_static/gFileSelArrowRightTex.ia8.png diff --git a/mm/assets/custom/misc/title_static/gFileSelGenerateNewTex.ia8.png b/mm/assets/custom/misc/title_static/gFileSelGenerateNewTex.ia8.png Binary files differnew file mode 100644 index 000000000..baec5d05e --- /dev/null +++ b/mm/assets/custom/misc/title_static/gFileSelGenerateNewTex.ia8.png diff --git a/mm/assets/custom/misc/title_static/gFileSelModeHeaderTex.ia8.png b/mm/assets/custom/misc/title_static/gFileSelModeHeaderTex.ia8.png Binary files differnew file mode 100644 index 000000000..c27f67aad --- /dev/null +++ b/mm/assets/custom/misc/title_static/gFileSelModeHeaderTex.ia8.png diff --git a/mm/assets/custom/misc/title_static/gFileSelNewFileTex.ia8.png b/mm/assets/custom/misc/title_static/gFileSelNewFileTex.ia8.png Binary files differnew file mode 100644 index 000000000..ac0959a54 --- /dev/null +++ b/mm/assets/custom/misc/title_static/gFileSelNewFileTex.ia8.png diff --git a/mm/assets/custom/misc/title_static/gFileSelPresetHeaderTex.ia8.png b/mm/assets/custom/misc/title_static/gFileSelPresetHeaderTex.ia8.png Binary files differnew file mode 100644 index 000000000..affafa431 --- /dev/null +++ b/mm/assets/custom/misc/title_static/gFileSelPresetHeaderTex.ia8.png diff --git a/mm/assets/custom/misc/title_static/gFileSelRandomizerTex.ia8.png b/mm/assets/custom/misc/title_static/gFileSelRandomizerTex.ia8.png Binary files differnew file mode 100644 index 000000000..f3f47db26 --- /dev/null +++ b/mm/assets/custom/misc/title_static/gFileSelRandomizerTex.ia8.png diff --git a/mm/assets/custom/misc/title_static/gFileSelSeedHeaderTex.ia8.png b/mm/assets/custom/misc/title_static/gFileSelSeedHeaderTex.ia8.png Binary files differnew file mode 100644 index 000000000..13169f9f6 --- /dev/null +++ b/mm/assets/custom/misc/title_static/gFileSelSeedHeaderTex.ia8.png diff --git a/mm/assets/custom/misc/title_static/gFileSelVanillaTex.ia8.png b/mm/assets/custom/misc/title_static/gFileSelVanillaTex.ia8.png Binary files differnew file mode 100644 index 000000000..4664b358d --- /dev/null +++ b/mm/assets/custom/misc/title_static/gFileSelVanillaTex.ia8.png diff --git a/mm/src/overlays/gamestates/ovl_file_choose/z_file_choose_NES.c b/mm/src/overlays/gamestates/ovl_file_choose/z_file_choose_NES.c index 4a6ad1af0..cf7a65182 100644 --- a/mm/src/overlays/gamestates/ovl_file_choose/z_file_choose_NES.c +++ b/mm/src/overlays/gamestates/ovl_file_choose/z_file_choose_NES.c @@ -437,6 +437,10 @@ void FileSelect_UnusedCMDelay(GameState* thisx) { void FileSelect_RotateToNameEntry(GameState* thisx) { FileSelectState* this = (FileSelectState*)thisx; + if (!GameInteractor_Should(VB_FILE_SELECT_ROTATE_TO_NAME_ENTRY, true, this)) { + return; + } + this->windowRot += 50.0f; if (this->windowRot >= 314.0f) { @@ -527,6 +531,10 @@ void (*sConfigModeUpdateFuncs[])(GameState*) = { FileSelect_RotateToMain, // CM_OPTIONS_TO_MAIN // Possible Debug FileSelect_UnusedCMDelay, // CM_UNUSED_DELAY + // 2S2H [Enhancement] New File Setup + FileSelect_RotateToNewFileSetup, // CM_2S2H_ROTATE_TO_NEW_FILE_SETUP + FileSelect_UpdateNewFileSetup, // CM_2S2H_NEW_FILE_SETUP + FileSelect_RotateToMain, // CM_2S2H_NEW_FILE_SETUP_TO_MAIN }; s16 sCursorAlphaTargets[] = { 70, 200 }; @@ -1970,6 +1978,36 @@ void FileSelect_ConfigModeDraw(GameState* thisx) { FileSelect_DrawOptions(&this->state); } + // #region 2S2H [Enhancement] + if ((this->configMode >= CM_2S2H_ROTATE_TO_NEW_FILE_SETUP) && + (this->configMode <= CM_2S2H_NEW_FILE_SETUP_TO_MAIN)) { + gDPPipeSync(POLY_OPA_DISP++); + gDPSetCombineMode(POLY_OPA_DISP++, G_CC_MODULATEIA_PRIM, G_CC_MODULATEIA_PRIM); + gDPSetPrimColorOverride(POLY_OPA_DISP++, 0, 0, this->windowColor[0], this->windowColor[1], this->windowColor[2], + this->windowAlpha, COSMETIC_ID("Menus.FileWindow")); + gDPSetEnvColor(POLY_OPA_DISP++, 0, 0, 0, 0); + + Matrix_Translate(0.0f, 0.0f, -93.6f, MTXMODE_NEW); + Matrix_Scale(0.78f, 0.78f, 0.78f, MTXMODE_APPLY); + Matrix_RotateXFApply((this->windowRot - 314.0f) / 100.0f); + + MATRIX_FINALIZE_AND_LOAD(POLY_OPA_DISP++, this->state.gfxCtx); + + gSPVertex(POLY_OPA_DISP++, &this->windowVtx[0], 32, 0); + gSPDisplayList(POLY_OPA_DISP++, gFileSelWindow1DL); + + gSPVertex(POLY_OPA_DISP++, &this->windowVtx[32], 32, 0); + gSPDisplayList(POLY_OPA_DISP++, gFileSelWindow2DL); + + gSPVertex(POLY_OPA_DISP++, &this->windowVtx[64], 16, 0); + gSPDisplayList(POLY_OPA_DISP++, gFileSelWindow3DL); + + gDPPipeSync(POLY_OPA_DISP++); + + FileSelect_DrawNewFileSetup(&this->state); + } + // #endregion + gDPPipeSync(POLY_OPA_DISP++); FileSelect_SetView(this, 0.0f, 0.0f, 64.0f); diff --git a/mm/src/overlays/gamestates/ovl_file_choose/z_file_select.h b/mm/src/overlays/gamestates/ovl_file_choose/z_file_select.h index ad0f6be52..614e5b963 100644 --- a/mm/src/overlays/gamestates/ovl_file_choose/z_file_select.h +++ b/mm/src/overlays/gamestates/ovl_file_choose/z_file_select.h @@ -78,7 +78,11 @@ typedef enum { /* 0x29 */ CM_OPTIONS_MENU, /* 0x2A */ CM_OPTIONS_WAIT_FOR_FLASH_SAVE, /* 0x2B */ CM_OPTIONS_TO_MAIN, - /* 0x2C */ CM_UNUSED_DELAY + /* 0x2C */ CM_UNUSED_DELAY, + // 2S2H [Enhancement] New file setup, shown between picking an empty file and entering a name. + /* 0x2D */ CM_2S2H_ROTATE_TO_NEW_FILE_SETUP, + /* 0x2E */ CM_2S2H_NEW_FILE_SETUP, + /* 0x2F */ CM_2S2H_NEW_FILE_SETUP_TO_MAIN } ConfigMode; typedef enum { @@ -326,6 +330,11 @@ void FileSelect_StartOptions(GameState* thisx); void FileSelect_UpdateOptionsMenu(GameState* thisx); void FileSelect_OptionsWaitForFlashSave(GameState* thisx); +// 2S2H [Enhancement] source: mm/2s2h/Enhancements/Saving/NewFileSetup.cpp +void FileSelect_RotateToNewFileSetup(GameState* thisx); +void FileSelect_UpdateNewFileSetup(GameState* thisx); +void FileSelect_DrawNewFileSetup(GameState* thisx); + extern u8 D_808141F0[]; extern s16 D_80814280[]; |
