Skip to content

Show a throbber animation while the game is saving

meejle edited this page Mar 19, 2023 · 2 revisions

Credit to me (meejle), and to Anon822 for helping me finish it off when I got stuck. 😬

ezgif com-video-to-gif

Step 1: Add throbber.png to your graphics/text_window folder

Get it here.

It's designed to fit with the default Pokémon Emerald text window, so you might want to redesign it. Just keep to the usual 16-colour palette, and keep each frame of animation contained within a 32×64px "block" (like below), and you can't go wrong.

Screenshot from 2023-03-19 17-45-42

Step 2: Make changes to src/start_menu.c

First, add #include "decompress.h" to the includes at the top of the file.

Then, add this big block of code to start_menu.c. It doesn't matter where you put it, as long as it comes before static u8 SaveSavingMessageCallback(void). I put it at the top, just above // Menu actions.

#define TAG_THROBBER 0x1000
static const u16 sThrobber_Pal[] = INCBIN_U16("graphics/text_window/throbber.gbapal");
const u32 gThrobber_Gfx[] = INCBIN_U32("graphics/text_window/throbber.4bpp.lz");
static u8 spriteId;

static const struct OamData sOam_Throbber =
{
    .y = DISPLAY_HEIGHT,
    .affineMode = ST_OAM_AFFINE_OFF,
    .objMode = ST_OAM_OBJ_NORMAL,
    .mosaic = FALSE,
    .bpp = ST_OAM_4BPP,
    .shape = SPRITE_SHAPE(32x64),
    .x = 0,
    .matrixNum = 0,
    .size = SPRITE_SIZE(32x64),
    .tileNum = 0,
    .priority = 0,
    .paletteNum = 0,
    .affineParam = 0,
};

static const union AnimCmd sAnim_Throbber[] =
{
    ANIMCMD_FRAME(0, 4),
    ANIMCMD_FRAME(32, 4),
    ANIMCMD_FRAME(64, 4),
    ANIMCMD_FRAME(96, 4),
    ANIMCMD_FRAME(128, 4),
    ANIMCMD_FRAME(160, 4),
    ANIMCMD_FRAME(192, 4),
    ANIMCMD_FRAME(224, 4),
    ANIMCMD_JUMP(0),
};

static const union AnimCmd * const sAnims_Throbber[] = { sAnim_Throbber, };

static const struct CompressedSpriteSheet sSpriteSheet_Throbber[] =
{
    {
        .data = gThrobber_Gfx,
        .size = 0x3200,
        .tag = TAG_THROBBER
    },
    {}
};

static const struct SpritePalette sSpritePalettes_Throbber[] =
{
    {
        .data = sThrobber_Pal,
        .tag = TAG_THROBBER
    },
    {},
};

static const struct SpriteTemplate sSpriteTemplate_Throbber =
{
    .tileTag = TAG_THROBBER,
    .paletteTag = TAG_THROBBER,
    .oam = &sOam_Throbber,
    .anims = sAnims_Throbber,
    .images = NULL,
    .affineAnims = gDummySpriteAffineAnimTable,
    .callback = SpriteCallbackDummy
};

void ShowThrobber(void)
{
    LoadCompressedSpriteSheet(&sSpriteSheet_Throbber[0]);
    LoadSpritePalettes(sSpritePalettes_Throbber);

    // 217 and 123 are the x and y coordinates (in pixels)
    spriteId = CreateSprite(&sSpriteTemplate_Throbber, 217, 123, 2);
};

Next, find the SaveSavingMessageCallback function. We just need to add ShowThrobber(); to the very top of the function, like this:

static u8 SaveSavingMessageCallback(void)
{
    ShowThrobber();
    ShowSaveMessage(gText_SavingDontTurnOff, SaveDoSaveCallback);
    return SAVE_IN_PROGRESS;
}

Finally, in the SaveDoSaveCallback function, we just need to replace this if statement:

    if (saveStatus == SAVE_STATUS_OK)
        ShowSaveMessage(gText_PlayerSavedGame, SaveSuccessCallback);
    else
        ShowSaveMessage(gText_SaveError, SaveErrorCallback);

With this one:

    if (saveStatus == SAVE_STATUS_OK)
    {
        ShowSaveMessage(gText_PlayerSavedGame, SaveSuccessCallback);
        DestroySprite(&gSprites[spriteId]);
    }
    else
    {
        ShowSaveMessage(gText_SaveError, SaveErrorCallback);
        DestroySprite(&gSprites[spriteId]);
    }

Step 3: Make changes to src/save.c

We're almost done. In save.c, right before the WriteSaveSectorOrSlot function, we'll add this:

static void VBlankCB_Saving(void)
{
    AnimateSprites();
    BuildOamBuffer();
    LoadOam();
    ProcessSpriteCopyRequests();
}

Then, in the WriteSaveSectorOrSlot function itself, we need to add IntrCallback prevVblankCB; right at the top, so it looks like this:

static u8 WriteSaveSectorOrSlot(u16 sectorId, const struct SaveSectorLocation *locations)
{
    IntrCallback prevVblankCB;
    u32 status;
    u16 i;

And finally, we're going to add some lines before and after this for statement:

        for (i = 0; i < NUM_SECTORS_PER_SLOT; i++)
            HandleWriteSector(i, locations);

So that it looks like this:

        prevVblankCB = gMain.vblankCallback;
        SetVBlankCallback(VBlankCB_Saving);
        for (i = 0; i < NUM_SECTORS_PER_SLOT; i++)
            HandleWriteSector(i, locations);
        SetVBlankCallback(prevVblankCB);

Then build, save, and enjoy!

Clone this wiki locally