code:
#include "global.h"
#include "bg.h"
#include "decompress.h"
#include "event_data.h"
#include "event_object_movement.h"
#include "field_weather.h"
#include "gpu_regs.h"
#include "graphics.h"
#include "malloc.h"
#include "main.h"
#include "menu.h"
#include "overworld.h"
#include "palette.h"
#include "random.h"
#include "scanline_effect.h"
#include "script.h"
#include "sound.h"
#include "sprite.h"
#include "task.h"
#include "constants/event_objects.h"
#include "constants/rgb.h"
#include "constants/songs.h"
#include "constants/weather.h"

#define GOING_DOWN gSpecialVar_0x8004

#define STATE_END 0xFF

#define TAG_CABLE_CAR 1
#define TAG_DOOR      2
#define TAG_CABLE     3

struct CableCar
{
    u8 bgTaskId;
    u8 state;
    u8 weather;
    u16 weatherDelay;
    u16 timer;
    u8 bg0HorizontalOffset;
    u8 bg0VerticalOffset;
    u8 unused0[2];
    u8 bg1HorizontalOffset;
    u8 bg1VerticalOffset;
    u8 unused1[6];
    u8 bg3HorizontalOffset;
    u8 bg3VerticalOffset;
    u8 unused2[2];
    u8 groundTileIdx;
    u8 groundSegmentXStart;
    u8 groundSegmentYStart;
    u8 groundTilemapOffset;
    u8 groundTimer; // Incremented, but does nothing
    u8 groundXOffset;
    u8 groundYOffset;
    u8 groundXBase;
    u8 groundYBase;
    u16 groundTileBuffer[9][12];
    u8 unused3[2];
    u16 bgTilemapBuffers[4][BG_SCREEN_SIZE];
    u16 *groundTilemap;
    u16 *treesTilemap;
    u16 *bgMountainsTilemap;
    const u16 *pylonHookTilemapEntries;
    u8 *pylonStemTilemap;
};

static EWRAM_DATA struct CableCar *sCableCar = NULL;
static EWRAM_DATA u8 sGroundX_Up = 0;
static EWRAM_DATA u8 sGroundY_Up = 0;
static EWRAM_DATA u8 sGroundSegmentY_Up = 0;
static EWRAM_DATA u8 sGroundX_Down = 0;
static EWRAM_DATA u8 sGroundY_Down = 0;
static EWRAM_DATA u8 sGroundSegmentY_Down = 0;

static void CB2_LoadCableCar(void);
static void SetBgRegs(bool8);
static void CreateCableCarSprites(void);
static void InitGroundTilemapData(bool8);
static void Task_CableCar(u8);
static void Task_AnimateBgGoingUp(u8);
static void Task_AnimateBgGoingDown(u8);
static void VBlankCB_CableCar(void);
static void CB2_CableCar(void);
static void AnimateGroundGoingUp(void);
static void AnimateGroundGoingDown(void);
static void SpriteCB_CableCar(struct Sprite *);
static void SpriteCB_Cable(struct Sprite *);
static void DrawNextGroundSegmentGoingUp(void);
static void DrawNextGroundSegmentGoingDown(void);

static const struct BgTemplate sBgTemplates[4] = {
    {
        .bg = 0,
        .charBaseIndex = 0,
        .mapBaseIndex = 28,
        .screenSize = 0,
        .paletteMode = 0,
        .priority = 1,
        .baseTile = 0
    },
    {
        .bg = 1,
        .charBaseIndex = 0,
        .mapBaseIndex = 29,
        .screenSize = 0,
        .paletteMode = 0,
        .priority = 2,
        .baseTile = 0
    },
    {
        .bg = 2,
        .charBaseIndex = 0,
        .mapBaseIndex = 30,
        .screenSize = 0,
        .paletteMode = 0,
        .priority = 3,
        .baseTile = 0
    },
    {
        .bg = 3,
        .charBaseIndex = 0,
        .mapBaseIndex = 31,
        .screenSize = 0,
        .paletteMode = 0,
        .priority = 0,
        .baseTile = 0
    },
};

static const u8 sGround_Tilemap[] = INCBIN_U8("graphics/cable_car/ground.bin.lz");
static const u8 sTrees_Tilemap[] = INCBIN_U8("graphics/cable_car/trees.bin.lz");
static const u8 sBgMountains_Tilemap[] = INCBIN_U8("graphics/cable_car/bg_mountains.bin.lz");

static const u16 sPylonHook_TilemapEntries[] = {
    0x3000,
    0x3001,
    0x3002,
    0x3003,
    0x3004,
    0x3005,
    0x3006,
    0x3007,
    0x3008,
    0x3009,
};

static const u8 sPylonStems_Tilemap[] = INCBIN_U8("graphics/cable_car/pylons.bin.lz");

static const struct CompressedSpriteSheet sSpriteSheets[] = {
    { gCableCar_Gfx,      0x800, TAG_CABLE_CAR },
    { gCableCarDoor_Gfx,   0x40, TAG_DOOR },
    { gCableCarCable_Gfx,  0x80, TAG_CABLE },
    { },
};

static const struct SpritePalette sSpritePalettes[] = {
    { gCableCar_Pal, TAG_CABLE_CAR },
    { }
};

static const struct OamData sOam_CableCar =
{
    .y = 0,
    .affineMode = ST_OAM_AFFINE_DOUBLE,
    .objMode = ST_OAM_OBJ_NORMAL,
    .bpp = ST_OAM_4BPP,
    .shape = SPRITE_SHAPE(64x64),
    .x = 0,
    .size = SPRITE_SIZE(64x64),
    .tileNum = 0,
    .priority = 2,
    .paletteNum = 0,
};

static const struct OamData sOam_CableCarDoor =
{
    .y = 0,
    .affineMode = ST_OAM_AFFINE_DOUBLE,
    .objMode = ST_OAM_OBJ_NORMAL,
    .bpp = ST_OAM_4BPP,
    .shape = SPRITE_SHAPE(16x8),
    .x = 0,
    .size = SPRITE_SIZE(16x8),
    .tileNum = 0,
    .priority = 2,
    .paletteNum = 0,
};

static const struct OamData sOam_Cable =
{
    .y = 0,
    .affineMode = ST_OAM_AFFINE_DOUBLE,
    .objMode = ST_OAM_OBJ_NORMAL,
    .bpp = ST_OAM_4BPP,
    .shape = SPRITE_SHAPE(16x16),
    .x = 0,
    .size = SPRITE_SIZE(16x16),
    .tileNum = 0,
    .priority = 2,
    .paletteNum = 0,
};

static const struct SpriteTemplate sSpriteTemplate_CableCar[] =
{
    {
        .tileTag = TAG_CABLE_CAR,
        .paletteTag = TAG_CABLE_CAR,
        .oam = &sOam_CableCar,
        .anims = gDummySpriteAnimTable,
        .images = NULL,
        .affineAnims = gDummySpriteAffineAnimTable,
        .callback = SpriteCB_CableCar,
    },
    {
        .tileTag = TAG_DOOR,
        .paletteTag = TAG_CABLE_CAR,
        .oam = &sOam_CableCarDoor,
        .anims = gDummySpriteAnimTable,
        .images = NULL,
        .affineAnims = gDummySpriteAffineAnimTable,
        .callback = SpriteCB_CableCar,
    },
};

static const struct SpriteTemplate sSpriteTemplate_Cable = {
    .tileTag = TAG_CABLE,
    .paletteTag = TAG_CABLE_CAR,
    .oam = &sOam_Cable,
    .anims = gDummySpriteAnimTable,
    .images = NULL,
    .affineAnims = gDummySpriteAffineAnimTable,
    .callback = SpriteCB_Cable,
};

static void Task_LoadCableCar(u8 taskId)
{
    if (!gPaletteFade.active)
    {
        SetMainCallback2(CB2_LoadCableCar);
        DestroyTask(taskId);
    }
}

void CableCar(void)
{
    ScriptContext2_Enable();
    CreateTask(Task_LoadCableCar, 1);
    BeginNormalPaletteFade(0xFFFFFFFF, 0, 0, 16, RGB(0, 0, 0));
}

static void CB2_LoadCableCar(void)
{
    u16 imebak;
    u8 i = 0;
    u32 sizeOut = 0;

    switch (gMain.state)
    {
    case 0:
    default:
        SetVBlankCallback(NULL);
        SetBgRegs(FALSE);
        ScanlineEffect_Stop();
        DmaFillLarge16(3, 0, (void *)VRAM, VRAM_SIZE, 0x1000);
        DmaFill32Defvars(3, 0, (void *)OAM, OAM_SIZE);
        DmaFill16Defvars(3, 0, (void *)PLTT, PLTT_SIZE);
        sCableCar = AllocZeroed(sizeof(*sCableCar));
        gMain.state++;
        break;
    case 1:
        ResetSpriteData();
        ResetTasks();
        FreeAllSpritePalettes();
        ResetPaletteFade();
        ResetTempTileDataBuffers();
        StartWeather();
        for (i = 0; i < NUM_ASH_SPRITES; i++)
            gWeatherPtr->sprites.s2.ashSprites[i] = NULL;

        InitMapMusic();
        ResetMapMusic();
        ResetBgsAndClearDma3BusyFlags(0);
        InitBgsFromTemplates(0, sBgTemplates, ARRAY_COUNT(sBgTemplates));
        SetBgTilemapBuffer(0, sCableCar->bgTilemapBuffers[0]);
        SetBgTilemapBuffer(1, sCableCar->bgTilemapBuffers[1]);
        SetBgTilemapBuffer(2, sCableCar->bgTilemapBuffers[2]);
        SetBgTilemapBuffer(3, sCableCar->bgTilemapBuffers[3]);
        gSpriteCoordOffsetX = gSpriteCoordOffsetY = 0;
        gMain.state++;
        break;
    case 2:
        for (i = 0; i < ARRAY_COUNT(sSpriteSheets) - 1; i++)
            LoadCompressedSpriteSheet(&sSpriteSheets[i]);

        LoadSpritePalettes(sSpritePalettes);
        sCableCar->groundTilemap = malloc_and_decompress(sGround_Tilemap, &sizeOut);
        sCableCar->treesTilemap = malloc_and_decompress(sTrees_Tilemap, &sizeOut);
        sCableCar->bgMountainsTilemap = malloc_and_decompress(sBgMountains_Tilemap, &sizeOut);
        sCableCar->pylonStemTilemap = malloc_and_decompress(sPylonStems_Tilemap, &sizeOut);
        sCableCar->pylonHookTilemapEntries = sPylonHook_TilemapEntries;
        DecompressAndCopyTileDataToVram(0, gCableCarBg_Gfx, 0, 0, 0);
        gMain.state++;
        break;
    case 3:
        if (!FreeTempTileDataBuffersIfPossible())
        {
            LoadPalette(gCableCarBg_Pal, 0, 0x80);
            gMain.state++;
        }
        break;
    case 4:
        CreateCableCarSprites();
        RunTasks();
        gMain.state++;
        break;
    case 5:
        if (sCableCar->weather == WEATHER_VOLCANIC_ASH)
        {
            gMain.state++;
        }
        else if (gWeatherPtr->sprites.s2.ashSprites[0])
        {
            for (i = 0; i < NUM_ASH_SPRITES; i++)
            {
                if (gWeatherPtr->sprites.s2.ashSprites[i])
                    gWeatherPtr->sprites.s2.ashSprites[i]->oam.priority = 0;
            }

            gMain.state++;
        }
        break;
    case 6:
        CopyToBgTilemapBufferRect_ChangePalette(1, sCableCar->treesTilemap, 0, 17, 32, 15, 17);
        CopyToBgTilemapBufferRect_ChangePalette(2, sCableCar->bgMountainsTilemap, 0, 0, 30, 20, 17);
        CopyToBgTilemapBufferRect_ChangePalette(3, sCableCar->pylonHookTilemapEntries, 0, 0, 5, 2, 17);
        CopyToBgTilemapBufferRect_ChangePalette(3, sCableCar->pylonStemTilemap, 0, 2, 2, 20, 17);
        gMain.state++;
        break;
    case 7:
        InitGroundTilemapData(GOING_DOWN);
        CopyToBgTilemapBufferRect_ChangePalette(0, sCableCar->groundTilemap +  0x48,  0, 14, 12, 3, 17);
        CopyToBgTilemapBufferRect_ChangePalette(0, sCableCar->groundTilemap +  0x6C, 12, 17, 12, 3, 17);
        CopyToBgTilemapBufferRect_ChangePalette(0, sCableCar->groundTilemap +  0x90, 24, 20, 12, 3, 17);
        CopyToBgTilemapBufferRect_ChangePalette(0, sCableCar->groundTilemap +   0x0,  0, 17, 12, 3, 17);
        CopyToBgTilemapBufferRect_ChangePalette(0, sCableCar->groundTilemap +  0x24,  0, 20, 12, 3, 17);
        CopyToBgTilemapBufferRect_ChangePalette(0, sCableCar->groundTilemap +   0x0, 12, 20, 12, 3, 17);
        CopyToBgTilemapBufferRect_ChangePalette(0, sCableCar->groundTilemap +  0x24, 12, 23, 12, 3, 17);
        CopyToBgTilemapBufferRect_ChangePalette(0, sCableCar->groundTilemap +   0x0, 24, 23, 12, 3, 17);
        gMain.state++;
        break;
    case 8:
        BeginNormalPaletteFade(0xFFFFFFFF, 3, 16, 0, RGB(0, 0, 0));
        FadeInNewBGM(MUS_CABLE_CAR, 1);
        SetBgRegs(TRUE);
        gMain.state++;
        break;
    case 9:
        imebak = REG_IME;
        REG_IME = 0;
        REG_IE |= INTR_FLAG_VBLANK;
        REG_IME = imebak;
        SetVBlankCallback(VBlankCB_CableCar);
        SetMainCallback2(CB2_CableCar);
        CreateTask(Task_CableCar, 0);
        if (!GOING_DOWN)
            sCableCar->bgTaskId = CreateTask(Task_AnimateBgGoingUp, 1);
        else
            sCableCar->bgTaskId = CreateTask(Task_AnimateBgGoingDown, 1);
        break;
    }
}

static void CB2_CableCar(void)
{
    RunTasks();
    AnimateSprites();
    BuildOamBuffer();
    UpdatePaletteFade();
    MapMusicMain();
}

static void CB2_EndCableCar(void)
{
    u8 i = 0;

    HideBg(0);
    HideBg(1);
    HideBg(2);
    HideBg(3);
    SetBgRegs(FALSE);
    gSpriteCoordOffsetX = 0;
    SetCurrentAndNextWeatherNoDelay(WEATHER_NONE);
    for (i = 0; i < NUM_ASH_SPRITES; i++)
        gWeatherPtr->sprites.s2.ashSprites[i] = NULL;

    ResetTasks();
    ResetSpriteData();
    ResetPaletteFade();
    UnsetBgTilemapBuffer(0);
    UnsetBgTilemapBuffer(1);
    UnsetBgTilemapBuffer(2);
    UnsetBgTilemapBuffer(3);
    ResetBgsAndClearDma3BusyFlags(0);
    sCableCar->pylonHookTilemapEntries = NULL;
    FREE_AND_SET_NULL(sCableCar->pylonStemTilemap);
    FREE_AND_SET_NULL(sCableCar->bgMountainsTilemap);
    FREE_AND_SET_NULL(sCableCar->treesTilemap);
    FREE_AND_SET_NULL(sCableCar->groundTilemap);
    FREE_AND_SET_NULL(sCableCar);
    DmaFillLarge16(3, 0, (void *)VRAM, VRAM_SIZE, 0x1000);
    DmaFill32Defvars(3, 0, (void *)OAM, OAM_SIZE);
    DmaFill16Defvars(3, 0, (void *)PLTT, PLTT_SIZE);
    WarpIntoMap();
    gFieldCallback = NULL;
    SetMainCallback2(CB2_LoadMap);
}

static void Task_CableCar(u8 taskId)
{
    u8 i = 0;

    sCableCar->timer++;
    switch (sCableCar->state)
    {
    case 0:
        // Wait to change weather
        if (sCableCar->timer == sCableCar->weatherDelay)
        {
            SetNextWeather(sCableCar->weather);
            sCableCar->state = 1;
        }
        break;
    case 1:
        // Update ash sprites
        switch (sCableCar->weather)
        {
        case WEATHER_VOLCANIC_ASH:
            if (gWeatherPtr->sprites.s2.ashSprites[0] != NULL && gWeatherPtr->sprites.s2.ashSprites[0]->oam.priority != 0)
            {
                for (; i < NUM_ASH_SPRITES; i++)
                {
                    if (gWeatherPtr->sprites.s2.ashSprites[i])
                        gWeatherPtr->sprites.s2.ashSprites[i]->oam.priority = 0;
                }

                sCableCar->state = 2;
            }
            break;
        case WEATHER_SUNNY:
            if (gWeatherPtr->currWeather == WEATHER_SUNNY)
            {
                sCableCar->state = 2;
            }
            else if (sCableCar->timer >= sCableCar->weatherDelay + 8)
            {
                for (; i < NUM_ASH_SPRITES; i++)
                {
                    if (gWeatherPtr->sprites.s2.ashSprites[i])
                        gWeatherPtr->sprites.s2.ashSprites[i]->invisible ^= 1;
                }
            }
            break;
        }
        break;
    case 2:
        // Wait to fade out
        if (sCableCar->timer == 570)
        {
            sCableCar->state = 3;
            BeginNormalPaletteFade(0xFFFFFFFF, 3, 0, 16, RGB(0, 0, 0));
            FadeOutBGM(4);
        }
        break;
    case 3:
        // Wait for fade out
        if (!gPaletteFade.active)
            sCableCar->state = STATE_END;
        break;
    case STATE_END:
        SetVBlankCallback(NULL);
        DestroyTask(taskId);
        DestroyTask(sCableCar->bgTaskId);
        SetMainCallback2(CB2_EndCableCar);
        break;
    }
}

static void Task_AnimateBgGoingUp(u8 taskId)
{
    if (sCableCar->state != STATE_END)
    {
        sCableCar->bg3HorizontalOffset--;
        if ((sCableCar->timer % 2) == 0)
            sCableCar->bg3VerticalOffset--;

        if ((sCableCar->timer % 8) == 0)
        {
            sCableCar->bg1HorizontalOffset--;
            sCableCar->bg1VerticalOffset--;
        }

        switch (sCableCar->bg3HorizontalOffset)
        {
        case 175:
            FillBgTilemapBufferRect(3, 0, 0, 22, 2, 10, 17);
            break;
        case 40:
            FillBgTilemapBufferRect(3, 0, 3, 0, 2, 2, 17);
            break;
        case 32:
            FillBgTilemapBufferRect(3, 0, 2, 0, 1, 2, 17);
            break;
        case 16:
            CopyToBgTilemapBufferRect_ChangePalette(3, sCableCar->pylonHookTilemapEntries, 0, 0, 5,  2, 17);
            CopyToBgTilemapBufferRect_ChangePalette(3, sCableCar->pylonStemTilemap, 0, 2, 2, 30, 17);
            sCableCar->bg3VerticalOffset = 64;
            break;
        }
    }

    AnimateGroundGoingUp();
    gSpriteCoordOffsetX = (gSpriteCoordOffsetX + 1) % 128;
}

static void Task_AnimateBgGoingDown(u8 taskId)
{
    if (sCableCar->state != STATE_END)
    {
        sCableCar->bg3HorizontalOffset++;
        if ((sCableCar->timer % 2) == 0)
            sCableCar->bg3VerticalOffset++;

        if ((sCableCar->timer % 8) == 0)
        {
            sCableCar->bg1HorizontalOffset++;
            sCableCar->bg1VerticalOffset++;
        }

        switch (sCableCar->bg3HorizontalOffset)
        {
        case 176:
            CopyToBgTilemapBufferRect_ChangePalette(3, sCableCar->pylonStemTilemap, 0, 2, 2, 30, 17);
            break;
        case 16:
            FillBgTilemapBufferRect(3, 0, 2,  0, 3,  2, 17);
            FillBgTilemapBufferRect(3, 0, 0, 22, 2, 10, 17);
            sCableCar->bg3VerticalOffset = 192;
            break;
        case 32:
            FillBgTilemapBufferRect(3, sCableCar->pylonHookTilemapEntries[2], 2, 0, 1, 1, 17);
            FillBgTilemapBufferRect(3, sCableCar->pylonHookTilemapEntries[3], 3, 0, 1, 1, 17);
            FillBgTilemapBufferRect(3, sCableCar->pylonHookTilemapEntries[7], 2, 1, 1, 1, 17);
            FillBgTilemapBufferRect(3, sCableCar->pylonHookTilemapEntries[8], 3, 1, 1, 1, 17);
            break;
        case 40:
            FillBgTilemapBufferRect(3, sCableCar->pylonHookTilemapEntries[4], 4, 0, 1, 1, 17);
            FillBgTilemapBufferRect(3, sCableCar->pylonHookTilemapEntries[9], 4, 1, 1, 1, 17);
            break;
        }
    }

    AnimateGroundGoingDown();
    if (sCableCar->timer < sCableCar->weatherDelay)
        gSpriteCoordOffsetX = (gSpriteCoordOffsetX + 247) % 248;
    else
        gWeatherPtr->ashBaseSpritesX = (gWeatherPtr->ashBaseSpritesX + 247) % 248;
}

static void VBlankCB_CableCar(void)
{
    CopyBgTilemapBufferToVram(0);
    CopyBgTilemapBufferToVram(3);
    SetGpuReg(REG_OFFSET_BG3HOFS, sCableCar->bg3HorizontalOffset);
    SetGpuReg(REG_OFFSET_BG3VOFS, sCableCar->bg3VerticalOffset);
    SetGpuReg(REG_OFFSET_BG1HOFS, sCableCar->bg1HorizontalOffset);
    SetGpuReg(REG_OFFSET_BG1VOFS, sCableCar->bg1VerticalOffset);
    SetGpuReg(REG_OFFSET_BG0HOFS, sCableCar->bg0HorizontalOffset);
    SetGpuReg(REG_OFFSET_BG0VOFS, sCableCar->bg0VerticalOffset);
    LoadOam();
    ProcessSpriteCopyRequests();
    TransferPlttBuffer();
}

static void SpriteCB_Cable(struct Sprite *sprite)
{

}

#define sXPos  data[0]
#define sYPos  data[1]

static void SpriteCB_CableCar(struct Sprite *sprite)
{
    if (sCableCar->state != STATE_END)
    {
        if (!GOING_DOWN)
        {
            sprite->pos1.x = sprite->sXPos - (u8)(0.14f * S16TOPOSFLOAT(sCableCar->timer));
            sprite->pos1.y = sprite->sYPos - (u8)(0.067f * S16TOPOSFLOAT(sCableCar->timer));
        }
        else
        {
            sprite->pos1.x = sprite->sXPos + (u8)(0.14f * S16TOPOSFLOAT(sCableCar->timer));
            sprite->pos1.y = sprite->sYPos + (u8)(0.067f * S16TOPOSFLOAT(sCableCar->timer));
        }
    }
}

#define sState data[2]
#define sTimer data[3]

static void SpriteCB_Player(struct Sprite *sprite)
{
    if (sCableCar->state != STATE_END)
    {
        // Move along with cable car
        if (!GOING_DOWN)
        {
            sprite->pos1.x = sprite->sXPos - (u8)(0.14f * S16TOPOSFLOAT(sCableCar->timer));
            sprite->pos1.y = sprite->sYPos - (u8)(0.067f * S16TOPOSFLOAT(sCableCar->timer));
        }
        else
        {
            sprite->pos1.x = sprite->sXPos + (u8)(0.14f * S16TOPOSFLOAT(sCableCar->timer));
            sprite->pos1.y = sprite->sYPos + (u8)(0.067f * S16TOPOSFLOAT(sCableCar->timer));
        }

        // Bounce up and down
        switch (sprite->sState)
        {
        case 0:
            sprite->pos2.y = 17;
            if (sprite->sTimer++ > 9)
            {
                sprite->sTimer = 0;
                sprite->sState++;
            }
            break;
        default:
            sprite->pos2.y = 16;
            if (sprite->sTimer++ > 9)
            {
                sprite->sTimer = 0;
                sprite->sState = 0;
            }
            break;
        }
    }
}

#undef sState
#undef sTimer

#define sTimer      data[0]
#define sSameDir    data[1] // Whether or not the hiker is headed the same dir as the Cable Car
#define sDelay      data[2]

static void SpriteCB_HikerGoingUp(struct Sprite *sprite)
{
    if (sprite->sTimer == 0)
    {
        sprite->pos1.x += 2 * sprite->centerToCornerVecX;
        sprite->pos1.y += 16 + sprite->centerToCornerVecY;
    }

    if (++sprite->sTimer >= sprite->sDelay)
    {
        switch (sprite->sSameDir)
        {
        case FALSE:
            sprite->pos1.x++;
            if ((sprite->sTimer % 4) == 0)
                sprite->pos1.y++;
            break;
        case TRUE:
            // Hiker moves slower if travelling with the Cable Car
            if ((sprite->sTimer % 2) != 0)
            {
                sprite->pos1.x++;
                if ((sprite->pos1.x % 4) == 0)
                    sprite->pos1.y++;
            }
            break;
        }

        if (sprite->pos1.y > 160)
            DestroySprite(sprite);
    }
}

static void SpriteCB_HikerGoingDown(struct Sprite *sprite)
{
    if (sprite->sTimer == 0)
        sprite->pos1.y += 16 + sprite->centerToCornerVecY;

    if (++sprite->sTimer >= sprite->sDelay)
    {
        switch (sprite->sSameDir)
        {
        case FALSE:
            sprite->pos1.x--;
            if ((sprite->sTimer % 4) == 0)
                sprite->pos1.y--;
            break;
        case TRUE:
            // Hiker moves slower if travelling with the Cable Car
            if ((sprite->sTimer % 2) != 0)
            {
                sprite->pos1.x--;
                if ((sprite->pos1.x % 4) == 0)
                    sprite->pos1.y--;
            }
            break;
        }

        if (sprite->pos1.y < 80)
            DestroySprite(sprite);
    }
}

#undef sTimer

static void SetBgRegs(bool8 active)
{
    switch (active)
    {
    case FALSE:
    default:
        SetGpuReg(REG_OFFSET_WININ, 0);
        SetGpuReg(REG_OFFSET_WINOUT, 0);
        SetGpuReg(REG_OFFSET_WIN0H, 0);
        SetGpuReg(REG_OFFSET_WIN1H, 0);
        SetGpuReg(REG_OFFSET_WIN0V, 0);
        SetGpuReg(REG_OFFSET_WIN1V, 0);
        SetGpuReg(REG_OFFSET_DISPCNT, 0);
        SetGpuReg(REG_OFFSET_BG3CNT, 0);
        SetGpuReg(REG_OFFSET_BG2CNT, 0);
        SetGpuReg(REG_OFFSET_BG1CNT, 0);
        SetGpuReg(REG_OFFSET_BG0CNT, 0);
        SetGpuReg(REG_OFFSET_BG3HOFS, 0);
        SetGpuReg(REG_OFFSET_BG3VOFS, 0);
        SetGpuReg(REG_OFFSET_BG2HOFS, 0);
        SetGpuReg(REG_OFFSET_BG2VOFS, 0);
        SetGpuReg(REG_OFFSET_BG1HOFS, 0);
        SetGpuReg(REG_OFFSET_BG1VOFS, 0);
        SetGpuReg(REG_OFFSET_BG0HOFS, 0);
        SetGpuReg(REG_OFFSET_BG0VOFS, 0);
        SetGpuReg(REG_OFFSET_BLDCNT, 0);
        break;
    case TRUE:
        SetGpuReg(REG_OFFSET_WININ, 0);
        SetGpuReg(REG_OFFSET_WINOUT, 0);
        SetGpuReg(REG_OFFSET_WIN0H, 0);
        SetGpuReg(REG_OFFSET_WIN1H, 0);
        SetGpuReg(REG_OFFSET_WIN0V, 0);
        SetGpuReg(REG_OFFSET_WIN1V, 0);
        if (!GOING_DOWN)
        {
            sCableCar->bg3HorizontalOffset = 176;
            sCableCar->bg3VerticalOffset = 16;
            sCableCar->bg1HorizontalOffset = 0;
            sCableCar->bg1VerticalOffset = 80;
            sCableCar->bg0VerticalOffset = 0;
            sCableCar->bg0VerticalOffset = 0;
        }
        else
        {
            sCableCar->bg3HorizontalOffset = 96;
            sCableCar->bg3VerticalOffset = 232;
            sCableCar->bg1HorizontalOffset = 0;
            sCableCar->bg1VerticalOffset = 4;
            sCableCar->bg0VerticalOffset = 0;
            sCableCar->bg0VerticalOffset = 0;
        }

        SetGpuReg(REG_OFFSET_BG3HOFS, sCableCar->bg3HorizontalOffset);
        SetGpuReg(REG_OFFSET_BG3VOFS, sCableCar->bg3VerticalOffset);
        SetGpuReg(REG_OFFSET_BG2HOFS, 0);
        SetGpuReg(REG_OFFSET_BG2VOFS, 0);
        SetGpuReg(REG_OFFSET_BG1HOFS, sCableCar->bg1HorizontalOffset);
        SetGpuReg(REG_OFFSET_BG1VOFS, sCableCar->bg1VerticalOffset);
        SetGpuReg(REG_OFFSET_BG0HOFS, sCableCar->bg0HorizontalOffset);
        SetGpuReg(REG_OFFSET_BG0VOFS, sCableCar->bg0VerticalOffset);
        SetGpuReg(REG_OFFSET_DISPCNT, DISPCNT_OBJ_ON | DISPCNT_OBJ_1D_MAP);
        CopyBgTilemapBufferToVram(1);
        CopyBgTilemapBufferToVram(2);
        ShowBg(0);
        ShowBg(1);
        ShowBg(2);
        ShowBg(3);
        SetGpuReg(REG_OFFSET_BLDCNT, BLDCNT_TGT2_ALL);
        break;
    }
}

static void CreateCableCarSprites(void)
{
    u8 spriteId;
    u8 i;

    u8 playerGraphicsIds[2] = {
        [MALE]   = OBJ_EVENT_GFX_RIVAL_BRENDAN_NORMAL,
        [FEMALE] = OBJ_EVENT_GFX_RIVAL_MAY_NORMAL
    };
    u16 rval = Random();
    u8 hikerGraphicsIds[4] = {
        OBJ_EVENT_GFX_HIKER,
        OBJ_EVENT_GFX_CAMPER,
        OBJ_EVENT_GFX_PICNICKER,
        OBJ_EVENT_GFX_ZIGZAGOON_1
    };
    s16 hikerCoords[2][2] = {
        {   0,  80 }, // Going up
        { 240, 146 }  // Going down
    };
    u8 hikerMovementDelayTable[4] = { 0, 60, 120, 170};
    void (*hikerCallbacks[2])(struct Sprite *) = {
        SpriteCB_HikerGoingUp,
        SpriteCB_HikerGoingDown
    };

    switch (GOING_DOWN)
    {
        case FALSE:
        default:
            // Create player sprite
            spriteId = AddPseudoObjectEvent(playerGraphicsIds[gSaveBlock2Ptr->playerGender], SpriteCB_Player, 200, 73, 102);
            if (spriteId != MAX_SPRITES)
            {
                gSprites[spriteId].oam.priority = 2;
                gSprites[spriteId].pos2.x = 8;
                gSprites[spriteId].pos2.y = 16;
                gSprites[spriteId].sXPos = 200;
                gSprites[spriteId].sYPos = 73;
            }
            // Create car sprite
            spriteId = CreateSprite(&sSpriteTemplate_CableCar[0], 176, 43, 0x67);
            gSprites[spriteId].pos2.x = gSprites[spriteId].pos2.y = 32;
            gSprites[spriteId].sXPos = 176;
            gSprites[spriteId].sYPos = 43;
            // Create door sprite
            spriteId = CreateSprite(&sSpriteTemplate_CableCar[1], 200, 99, 0x65);
            gSprites[spriteId].pos2.x = 8;
            gSprites[spriteId].pos2.y = 4;
            gSprites[spriteId].sXPos = 200;
            gSprites[spriteId].sYPos = 99;
            // Init weather
            sCableCar->weather = WEATHER_VOLCANIC_ASH;
            sCableCar->weatherDelay = 350;
            SetCurrentAndNextWeatherNoDelay(WEATHER_SUNNY);
            break;
        case TRUE:
            CopyToBgTilemapBufferRect_ChangePalette(0, sCableCar->groundTilemap + 0x24, 24, 26, 12, 3, 17);
            // Create player sprite
            spriteId = AddPseudoObjectEvent(playerGraphicsIds[gSaveBlock2Ptr->playerGender], SpriteCB_Player, 128, 39, 102);
            if (spriteId != MAX_SPRITES)
            {
                gSprites[spriteId].oam.priority = 2;
                gSprites[spriteId].pos2.x = 8;
                gSprites[spriteId].pos2.y = 16;
                gSprites[spriteId].sXPos = 128;
                gSprites[spriteId].sYPos = 39;
            }
            // Create car sprite
            spriteId = CreateSprite(&sSpriteTemplate_CableCar[0], 104, 9, 0x67);
            gSprites[spriteId].pos2.x = gSprites[spriteId].pos2.y = 32;
            gSprites[spriteId].sXPos = 104;
            gSprites[spriteId].sYPos = 9;
            // Create door sprite
            spriteId = CreateSprite(&sSpriteTemplate_CableCar[1], 128, 65, 0x65);
            gSprites[spriteId].pos2.x = 8;
            gSprites[spriteId].pos2.y = 4;
            gSprites[spriteId].sXPos = 128;
            gSprites[spriteId].sYPos = 65;
            // Init weather
            sCableCar->weather = WEATHER_SUNNY;
            sCableCar->weatherDelay = 265;
            SetCurrentAndNextWeatherNoDelay(WEATHER_VOLCANIC_ASH);
            break;
    }
    for (i = 0; i < 9; i++)
    {
        spriteId = CreateSprite(&sSpriteTemplate_Cable, 16 * i + 96, 8 * i - 8, 0x68);
        gSprites[spriteId].pos2.x = 8;
        gSprites[spriteId].pos2.y = 8;
    }

    // 1/64 chance for an NPC to appear hiking on the ground below the Cable Car
    if ((rval % 64) == 0)
    {
        // Unclear if this was intentional, but the - 1 in the below ARRAY_COUNT means the Zigzagoon is never used
        spriteId = AddPseudoObjectEvent(hikerGraphicsIds[rval % (ARRAY_COUNT(hikerGraphicsIds) - 1)], hikerCallbacks[GOING_DOWN], hikerCoords[GOING_DOWN][0], hikerCoords[GOING_DOWN][1], 106);
        if (spriteId != MAX_SPRITES)
        {
            gSprites[spriteId].oam.priority = 2;
            gSprites[spriteId].pos2.x = -gSprites[spriteId].centerToCornerVecX;
            gSprites[spriteId].pos2.y = -gSprites[spriteId].centerToCornerVecY;

            // Randomly choose which direction the NPC is going
            if (!GOING_DOWN)
            {
                if (rval % 2)
                {
                    // Do walking west anim
                    StartSpriteAnim(&gSprites[spriteId], 6);
                    gSprites[spriteId].sSameDir = TRUE;
                    gSprites[spriteId].pos1.y += 2;
                }
                else
                {
                    // Do walking east anim
                    StartSpriteAnim(&gSprites[spriteId], 7);
                    gSprites[spriteId].sSameDir = FALSE;
                }
            }
            else
            {
                if (rval % 2)
                {
                    // Do walking east anim
                    StartSpriteAnim(&gSprites[spriteId], 7);
                    gSprites[spriteId].sSameDir = TRUE;
                    gSprites[spriteId].pos1.y += 2;
                }
                else
                {
                    // Do walking west anim
                    StartSpriteAnim(&gSprites[spriteId], 6);
                    gSprites[spriteId].sSameDir = FALSE;
                }
            }
            gSprites[spriteId].sDelay = hikerMovementDelayTable[rval % ARRAY_COUNT(hikerMovementDelayTable)];
        }
    }
}

static void BufferNextGroundSegment(void)
{
    u8 i, j, k;
    u8 offset;

    for (i = 0, k = 0, offset = 0x24 * (sCableCar->groundTilemapOffset + 2); i < 3; i++)
    {
        for (j = 0; j < ARRAY_COUNT(sCableCar->groundTileBuffer[0]); j++)
        {
            sCableCar->groundTileBuffer[i][j] = sCableCar->groundTilemap[offset++];
            sCableCar->groundTileBuffer[i + 3][j] = sCableCar->groundTilemap[k];
            sCableCar->groundTileBuffer[i + 6][j] = (sCableCar->groundTilemap + 0x24)[k];
            k++;
        }
    }

    sCableCar->groundTilemapOffset = (sCableCar->groundTilemapOffset + 1) % 3;
}

static void AnimateGroundGoingUp(void)
{
    sCableCar->groundTimer = (sCableCar->groundTimer + 1) % 96;
    sCableCar->bg0HorizontalOffset = sCableCar->groundXBase - sCableCar->groundXOffset;
    sCableCar->bg0VerticalOffset = sCableCar->groundYBase - sCableCar->groundYOffset;
    sCableCar->groundXOffset++;
    if ((sCableCar->groundXOffset % 4) == 0)
        sCableCar->groundYOffset++;

    if (sCableCar->groundXOffset > 16)
        DrawNextGroundSegmentGoingUp();
}

static void AnimateGroundGoingDown(void)
{
    sCableCar->groundTimer = (sCableCar->groundTimer + 1) % 96;
    sCableCar->bg0HorizontalOffset = sCableCar->groundXBase + sCableCar->groundXOffset;
    sCableCar->bg0VerticalOffset = sCableCar->groundYBase + sCableCar->groundYOffset;
    sCableCar->groundXOffset++;
    if ((sCableCar->groundXOffset % 4) == 0)
        sCableCar->groundYOffset++;

    if (sCableCar->groundXOffset > 16)
        DrawNextGroundSegmentGoingDown();
}

static void DrawNextGroundSegmentGoingUp(void)
{
    u8 i = 0;

    sCableCar->groundXOffset = sCableCar->groundYOffset = 0;
    sCableCar->groundXBase = sCableCar->bg0HorizontalOffset;
    sCableCar->groundYBase = sCableCar->bg0VerticalOffset;
    sCableCar->groundSegmentXStart = (sCableCar->groundSegmentXStart + 30) % 32;
    sCableCar->groundTileIdx -= 2;
    sGroundSegmentY_Up = (sCableCar->groundSegmentYStart + 23) % 32;

    // Draw next segment
    for (i = 0; i < ARRAY_COUNT(sCableCar->groundTileBuffer); i++)
    {
        sGroundX_Up = sCableCar->groundSegmentXStart;
        sGroundY_Up = (sGroundSegmentY_Up + i) % 32;
        FillBgTilemapBufferRect(0, sCableCar->groundTileBuffer[i][sCableCar->groundTileIdx], sGroundX_Up, sGroundY_Up, 1, 1, 17);
        sGroundX_Up = (sGroundX_Up + 1) % 32;
        FillBgTilemapBufferRect(0, sCableCar->groundTileBuffer[i][sCableCar->groundTileIdx + 1], sGroundX_Up, sGroundY_Up, 1, 1, 17);
    }

    // Erase old segment
    sGroundX_Up = (sCableCar->groundSegmentXStart + 30) % 32;
    FillBgTilemapBufferRect(0, 0, sGroundX_Up, 0, 2, 32, 17);
    if (sCableCar->groundTileIdx == 0)
    {
        sCableCar->groundSegmentYStart = (sCableCar->groundSegmentYStart + 29) % 32;
        sCableCar->groundTileIdx = 12;
        BufferNextGroundSegment();
        sGroundX_Up = (sCableCar->groundSegmentYStart + 1) % 32;
        FillBgTilemapBufferRect(0, 0, 0, sGroundX_Up, 32, 9, 17);
    }
}

static void DrawNextGroundSegmentGoingDown(void)
{
    u8 i = 0;

    sCableCar->groundXOffset = sCableCar->groundYOffset = 0;
    sCableCar->groundXBase = sCableCar->bg0HorizontalOffset;
    sCableCar->groundYBase = sCableCar->bg0VerticalOffset;
    sCableCar->groundSegmentXStart = (sCableCar->groundSegmentXStart + 2) % 32;
    sCableCar->groundTileIdx += 2;
    sGroundSegmentY_Down = sCableCar->groundSegmentYStart;

    // Draw next segment
    for (i = 0; i < ARRAY_COUNT(sCableCar->groundTileBuffer); i++)
    {
        sGroundX_Down = sCableCar->groundSegmentXStart;
        sGroundY_Down = (sGroundSegmentY_Down + i) % 32;
        FillBgTilemapBufferRect(0, sCableCar->groundTileBuffer[i][sCableCar->groundTileIdx], sGroundX_Down, sGroundY_Down, 1, 1, 17);
        sGroundX_Down = (sGroundX_Down + 1) % 32;
        FillBgTilemapBufferRect(0, sCableCar->groundTileBuffer[i][sCableCar->groundTileIdx + 1], sGroundX_Down, sGroundY_Down, 1, 1, 17);
    }

    // Erase old segment
    sGroundY_Down = (sCableCar->groundSegmentYStart + 23) % 32;
    FillBgTilemapBufferRect(0, 0, sCableCar->groundSegmentXStart, sGroundY_Down, 2, 9, 17);
    if (sCableCar->groundTileIdx == 10)
    {
        sCableCar->groundSegmentYStart = (sCableCar->groundSegmentYStart + 3) % 32;
        sCableCar->groundTileIdx = -2;
        BufferNextGroundSegment();
    }
}

static void InitGroundTilemapData(bool8 goingDown)
{
    switch (goingDown)
    {
        case FALSE:
        default:
            sCableCar->groundTilemapOffset = 2;
            sCableCar->groundSegmentXStart = 0;
            sCableCar->groundSegmentYStart = 20;
            sCableCar->groundTileIdx = 12;
            BufferNextGroundSegment();
            DrawNextGroundSegmentGoingUp();
            break;
        case TRUE:
            sCableCar->groundTilemapOffset = 2;
            sCableCar->groundSegmentXStart = 28;
            sCableCar->groundSegmentYStart = 20;
            sCableCar->groundTileIdx = 4;
            BufferNextGroundSegment();
            DrawNextGroundSegmentGoingDown();
            break;
    }

    sCableCar->groundTimer = 0;
}


code:
#include "global.h"
#include "main.h"
#include "battle.h"
#include "battle_records.h"
#include "battle_setup.h"
#include "cable_club.h"
#include "data.h"
#include "event_data.h"
#include "field_message_box.h"
#include "field_specials.h"
#include "field_weather.h"
#include "international_string_util.h"
#include "link.h"
#include "link_rfu.h"
#include "load_save.h"
#include "m4a.h"
#include "menu.h"
#include "overworld.h"
#include "palette.h"
#include "union_room.h"
#include "mevent2.h"
#include "script.h"
#include "script_pokemon_util.h"
#include "sound.h"
#include "start_menu.h"
#include "string_util.h"
#include "strings.h"
#include "task.h"
#include "trade.h"
#include "trainer_card.h"
#include "party_menu.h"
#include "window.h"
#include "constants/battle_frontier.h"
#include "constants/cable_club.h"
#include "constants/songs.h"
#include "constants/trainers.h"

static const struct WindowTemplate sWindowTemplate_LinkPlayerCount = {
    .bg = 0,
    .tilemapLeft = 16,
    .tilemapTop = 11,
    .width = 11,
    .height = 2,
    .paletteNum = 15,
    .baseBlock = 0x0125,
};

static const u8 *const sTrainerCardColorNames[] = {
    gText_BronzeCard,
    gText_CopperCard,
    gText_SilverCard,
    gText_GoldCard
};

static void Task_LinkupStart(u8 taskId);
static void Task_LinkupAwaitConnection(u8 taskId);
static void Task_LinkupConfirmWhenReady(u8 taskId);
static void Task_LinkupAwaitConfirmation(u8 taskId);
static void Task_LinkupTryConfirmation(u8 taskId);
static void Task_LinkupConfirm(u8 taskId);
static void Task_LinkupExchangeDataWithLeader(u8 taskId);
static void Task_LinkupCheckStatusAfterConfirm(u8 taskId);
static void Task_LinkupAwaitTrainerCardData(u8 taskId);
static void Task_StopLinkup(u8 taskId);
static void Task_LinkupFailed(u8 taskId);
static void Task_LinkupConnectionError(u8 taskId);
static bool8 TryLinkTimeout(u8 taskId);
static void Task_ValidateMixingGameLanguage(u8 taskId);
static void Task_ReestablishLink(u8 taskId);
static void Task_ReestablishLinkAwaitConnection(u8 taskId);
static void Task_ReestablishLinkLeader(u8 taskId);
static void Task_ReestablishLinkAwaitConfirmation(u8 taskId);

#define tState      data[0]

#define tMinPlayers data[1]
#define tMaxPlayers data[2]
#define tNumPlayers data[3]
#define tTimer      data[4]
#define tWindowId   data[5]

static void CreateLinkupTask(u8 minPlayers, u8 maxPlayers)
{
    if (FindTaskIdByFunc(Task_LinkupStart) == 0xFF)
    {
        u8 taskId1;

        taskId1 = CreateTask(Task_LinkupStart, 80);
        gTasks[taskId1].tMinPlayers = minPlayers;
        gTasks[taskId1].tMaxPlayers = maxPlayers;
    }
}

static void PrintNumPlayersInLink(u16 windowId, u32 numPlayers)
{
    u8 xPos;

    ConvertIntToDecimalStringN(gStringVar1, numPlayers, STR_CONV_MODE_LEFT_ALIGN, 1);
    SetStandardWindowBorderStyle(windowId, 0);
    StringExpandPlaceholders(gStringVar4, gText_NumPlayerLink);
    xPos = GetStringCenterAlignXOffset(1, gStringVar4, 88);
    AddTextPrinterParameterized(windowId, 1, gStringVar4, xPos, 1, 0xFF, NULL);
    CopyWindowToVram(windowId, 3);
}

static void ClearLinkPlayerCountWindow(u16 windowId)
{
    // Following this call with a copy-to-vram with mode 3 is identical to
    // calling ClearStdWindowAndFrame(windowId, TRUE).
    ClearStdWindowAndFrame(windowId, FALSE);
    CopyWindowToVram(windowId, 3);
}

static void UpdateLinkPlayerCountDisplay(u8 taskId, u8 numPlayers)
{
    s16 *data = gTasks[taskId].data;

    if (numPlayers != tNumPlayers)
    {
        if (numPlayers <= 1)
            ClearLinkPlayerCountWindow(tWindowId);
        else
            PrintNumPlayersInLink(tWindowId, numPlayers);
        tNumPlayers = numPlayers;
    }
}

static u32 ExchangeDataAndGetLinkupStatus(u8 minPlayers, u8 maxPlayers)
{
    int playerCount;

    switch (GetLinkPlayerDataExchangeStatusTimed(minPlayers, maxPlayers))
    {
    case EXCHANGE_COMPLETE:
        return LINKUP_SUCCESS;
    case EXCHANGE_DIFF_SELECTIONS:
        return LINKUP_DIFF_SELECTIONS;
    case EXCHANGE_PLAYER_NOT_READY:
        return LINKUP_PLAYER_NOT_READY;
    case EXCHANGE_PARTNER_NOT_READY:
        return LINKUP_PARTNER_NOT_READY;
    case EXCHANGE_WRONG_NUM_PLAYERS:
        ConvertIntToDecimalStringN(gStringVar1, GetLinkPlayerCount_2(), STR_CONV_MODE_LEFT_ALIGN, 1);
        return LINKUP_WRONG_NUM_PLAYERS;
    case EXCHANGE_STAT_7:
        return LINKUP_FAILED_CONTEST_GMODE;
    case EXCHANGE_TIMED_OUT:
    default:
        return LINKUP_ONGOING;
    }
}

static bool32 CheckLinkErrored(u8 taskId)
{
    if (HasLinkErrorOccurred() == TRUE)
    {
        gTasks[taskId].func = Task_LinkupConnectionError;
        return TRUE;
    }
    return FALSE;
}

static bool32 CheckLinkCanceledBeforeConnection(u8 taskId)
{
    if ((JOY_NEW(B_BUTTON))
     && IsLinkConnectionEstablished() == FALSE)
    {
        gLinkType = 0;
        gTasks[taskId].func = Task_LinkupFailed;
        return TRUE;
    }
    return FALSE;
}

static bool32 CheckLinkCanceled(u8 taskId)
{
    if (IsLinkConnectionEstablished())
        SetSuppressLinkErrorMessage(TRUE);

    if (JOY_NEW(B_BUTTON))
    {
        gLinkType = 0;
        gTasks[taskId].func = Task_LinkupFailed;
        return TRUE;
    }
    return FALSE;
}

static bool32 sub_80B25CC(u8 taskId)
{
    if (GetSioMultiSI() == 1)
    {
        gTasks[taskId].func = Task_LinkupConnectionError;
        return TRUE;
    }
    return FALSE;
}

// Unused
static void sub_80B2600(u8 taskId)
{
    gTasks[taskId].data[0]++;
    if (gTasks[taskId].data[0] == 10)
    {
        SendBlockRequest(2);
        DestroyTask(taskId);
    }
}

static void Task_LinkupStart(u8 taskId)
{
    s16 *data = gTasks[taskId].data;

    if (data[0] == 0)
    {
        OpenLinkTimed();
        ResetLinkPlayerCount();
        ResetLinkPlayers();
        tWindowId = AddWindow(&sWindowTemplate_LinkPlayerCount);
    }
    else if (data[0] > 9)
    {
        gTasks[taskId].func = Task_LinkupAwaitConnection;
    }
    data[0]++;
}

static void Task_LinkupAwaitConnection(u8 taskId)
{
    u32 playerCount = GetLinkPlayerCount_2();

    if (CheckLinkCanceledBeforeConnection(taskId) == TRUE
     || CheckLinkCanceled(taskId) == TRUE
     || playerCount < 2)
        return;

    SetSuppressLinkErrorMessage(TRUE);
    gTasks[taskId].data[3] = 0;
    if (IsLinkMaster() == TRUE)
    {
        PlaySE(SE_PIN);
        ShowFieldAutoScrollMessage(gText_ConfirmLinkWhenPlayersReady);
        gTasks[taskId].func = Task_LinkupConfirmWhenReady;
    }
    else
    {
        PlaySE(SE_BOO);
        ShowFieldAutoScrollMessage(gText_AwaitingLinkup);
        gTasks[taskId].func = Task_LinkupExchangeDataWithLeader;
    }
}

static void Task_LinkupConfirmWhenReady(u8 taskId)
{
    if (CheckLinkCanceledBeforeConnection(taskId) == TRUE
     || sub_80B25CC(taskId) == TRUE
     || CheckLinkErrored(taskId) == TRUE)
        return;

    if (GetFieldMessageBoxMode() == FIELD_MESSAGE_BOX_HIDDEN)
    {
        gTasks[taskId].tNumPlayers = 0;
        gTasks[taskId].func = Task_LinkupAwaitConfirmation;
    }
}

static void Task_LinkupAwaitConfirmation(u8 taskId)
{
    s16 *data = gTasks[taskId].data;
    s32 linkPlayerCount = GetLinkPlayerCount_2();

    if (CheckLinkCanceledBeforeConnection(taskId) == TRUE
     || sub_80B25CC(taskId) == TRUE
     || CheckLinkErrored(taskId) == TRUE)
        return;

    UpdateLinkPlayerCountDisplay(taskId, linkPlayerCount);

    if (!(JOY_NEW(A_BUTTON)))
        return;

    if (linkPlayerCount < tMinPlayers)
        return;

    SaveLinkPlayers(linkPlayerCount);
    ClearLinkPlayerCountWindow(tWindowId);
    ConvertIntToDecimalStringN(gStringVar1, linkPlayerCount, STR_CONV_MODE_LEFT_ALIGN, 1);
    ShowFieldAutoScrollMessage(gText_ConfirmStartLinkWithXPlayers);
    gTasks[taskId].func = Task_LinkupTryConfirmation;
}

static void Task_LinkupTryConfirmation(u8 taskId)
{
    if (CheckLinkCanceledBeforeConnection(taskId) == TRUE
     || sub_80B25CC(taskId) == TRUE
     || CheckLinkErrored(taskId) == TRUE)
        return;

    if (GetFieldMessageBoxMode() == FIELD_MESSAGE_BOX_HIDDEN)
    {
        if (GetSavedPlayerCount() != GetLinkPlayerCount_2())
        {
            ShowFieldAutoScrollMessage(gText_ConfirmLinkWhenPlayersReady);
            gTasks[taskId].func = Task_LinkupConfirmWhenReady;
        }
        else if (JOY_HELD(B_BUTTON))
        {
            ShowFieldAutoScrollMessage(gText_ConfirmLinkWhenPlayersReady);
            gTasks[taskId].func = Task_LinkupConfirmWhenReady;
        }
        else if (JOY_HELD(A_BUTTON))
        {
            PlaySE(SE_SELECT);
            CheckShouldAdvanceLinkState();
            gTasks[taskId].func = Task_LinkupConfirm;
        }
    }
}

static void Task_LinkupConfirm(u8 taskId)
{
    u8 minPlayers = gTasks[taskId].tMinPlayers;
    u8 maxPlayers = gTasks[taskId].tMaxPlayers;

    if (CheckLinkErrored(taskId) == TRUE
     || TryLinkTimeout(taskId) == TRUE)
        return;

    if (GetLinkPlayerCount_2() != GetSavedPlayerCount())
    {
        gTasks[taskId].func = Task_LinkupConnectionError;
    }
    else
    {
        gSpecialVar_Result = ExchangeDataAndGetLinkupStatus(minPlayers, maxPlayers);
        if (gSpecialVar_Result != LINKUP_ONGOING)
            gTasks[taskId].func = Task_LinkupCheckStatusAfterConfirm;
    }
}

static void Task_LinkupExchangeDataWithLeader(u8 taskId)
{
    u8 minPlayers, maxPlayers;
    struct TrainerCard *card;

    minPlayers = gTasks[taskId].tMinPlayers;
    maxPlayers = gTasks[taskId].tMaxPlayers;

    if (CheckLinkCanceledBeforeConnection(taskId) == TRUE
     || CheckLinkErrored(taskId) == TRUE)
        return;

    gSpecialVar_Result = ExchangeDataAndGetLinkupStatus(minPlayers, maxPlayers);
    if (gSpecialVar_Result == LINKUP_ONGOING)
        return;
    if (gSpecialVar_Result == LINKUP_DIFF_SELECTIONS
     || gSpecialVar_Result == LINKUP_WRONG_NUM_PLAYERS)
    {
        SetCloseLinkCallback();
        HideFieldMessageBox();
        gTasks[taskId].func = Task_StopLinkup;
    }
    else if (gSpecialVar_Result == LINKUP_PLAYER_NOT_READY
          || gSpecialVar_Result == LINKUP_PARTNER_NOT_READY)
    {
        CloseLink();
        HideFieldMessageBox();
        gTasks[taskId].func = Task_StopLinkup;
    }
    else
    {
        gFieldLinkPlayerCount = GetLinkPlayerCount_2();
        gLocalLinkPlayerId = GetMultiplayerId();
        SaveLinkPlayers(gFieldLinkPlayerCount);
        card = (struct TrainerCard *)gBlockSendBuffer;
        TrainerCard_GenerateCardForPlayer(card);
        card->monSpecies[0] = GetMonData(&gPlayerParty[gSelectedOrderFromParty[0] - 1], MON_DATA_SPECIES, NULL);
        card->monSpecies[1] = GetMonData(&gPlayerParty[gSelectedOrderFromParty[1] - 1], MON_DATA_SPECIES, NULL);
        gTasks[taskId].func = Task_LinkupAwaitTrainerCardData;
    }
}

static void Task_LinkupCheckStatusAfterConfirm(u8 taskId)
{
    struct TrainerCard *card;

    if (CheckLinkErrored(taskId) == TRUE)
        return;

    if (gSpecialVar_Result == LINKUP_WRONG_NUM_PLAYERS)
    {
        if (!Link_AnyPartnersPlayingRubyOrSapphire())
        {
            SetCloseLinkCallback();
            HideFieldMessageBox();
            gTasks[taskId].func = Task_StopLinkup;
        }
        else
        {
            CloseLink();
            HideFieldMessageBox();
            gTasks[taskId].func = Task_StopLinkup;
        }
    }
    else if (gSpecialVar_Result == LINKUP_DIFF_SELECTIONS)
    {
        SetCloseLinkCallback();
        HideFieldMessageBox();
        gTasks[taskId].func = Task_StopLinkup;
    }
    else if (gSpecialVar_Result == LINKUP_PLAYER_NOT_READY
          || gSpecialVar_Result == LINKUP_PARTNER_NOT_READY)
    {
        CloseLink();
        HideFieldMessageBox();
        gTasks[taskId].func = Task_StopLinkup;
    }
    else
    {
        gFieldLinkPlayerCount = GetLinkPlayerCount_2();
        gLocalLinkPlayerId = GetMultiplayerId();
        SaveLinkPlayers(gFieldLinkPlayerCount);
        card = (struct TrainerCard *)gBlockSendBuffer;
        TrainerCard_GenerateCardForPlayer(card);
        card->monSpecies[0] = GetMonData(&gPlayerParty[gSelectedOrderFromParty[0] - 1], MON_DATA_SPECIES, NULL);
        card->monSpecies[1] = GetMonData(&gPlayerParty[gSelectedOrderFromParty[1] - 1], MON_DATA_SPECIES, NULL);
        gTasks[taskId].func = Task_LinkupAwaitTrainerCardData;
        SendBlockRequest(2);
    }
}

bool32 AreBattleTowerLinkSpeciesSame(u16 *speciesList1, u16 *speciesList2)
{
    int i;
    int j;
    bool32 haveSameSpecies = FALSE;
    int numSameSpecies = 0;

    gStringVar1[0] = EOS;
    gStringVar2[0] = EOS;

    for (i = 0; i < FRONTIER_MULTI_PARTY_SIZE; i++)
    {
        for (j = 0; j < FRONTIER_MULTI_PARTY_SIZE; j++)
        {
            if (speciesList1[i] == speciesList2[j])
            {
                if (numSameSpecies == 0)
                {
                    StringCopy(gStringVar1, gSpeciesNames[speciesList1[i]]);
                    haveSameSpecies = TRUE;
                }

                if (numSameSpecies == 1)
                {
                    StringCopy(gStringVar2, gSpeciesNames[speciesList1[i]]);
                    haveSameSpecies = TRUE;
                }

                numSameSpecies++;
            }
        }
    }

    // var below is read by BattleFrontier_BattleTowerLobby_EventScript_AbortLink
    gSpecialVar_0x8005 = numSameSpecies;

    return haveSameSpecies;
}

static void FinishLinkup(u16 *linkupStatus, u32 taskId)
{
    struct TrainerCard *trainerCards = gTrainerCards;

    if (*linkupStatus == LINKUP_SUCCESS)
    {
        if (gLinkType == LINKTYPE_BATTLE_TOWER_50 || gLinkType == LINKTYPE_BATTLE_TOWER_OPEN)
        {
            if (AreBattleTowerLinkSpeciesSame(trainerCards[0].monSpecies, trainerCards[1].monSpecies))
            {
                // Unsuccessful battle tower linkup
                *linkupStatus = LINKUP_FAILED_BATTLE_TOWER;
                SetCloseLinkCallback();
                gTasks[taskId].func = Task_StopLinkup;
            }
            else
            {
                // Successful battle tower linkup
                ClearLinkPlayerCountWindow(gTasks[taskId].tWindowId);
                EnableBothScriptContexts();
                DestroyTask(taskId);
            }
        }
        else
        {
            // Successful linkup
            ClearLinkPlayerCountWindow(gTasks[taskId].tWindowId);
            EnableBothScriptContexts();
            DestroyTask(taskId);
        }
    }
    else
    {
        // Unsuccessful linkup
        SetCloseLinkCallback();
        gTasks[taskId].func = Task_StopLinkup;
    }
}

static void Task_LinkupAwaitTrainerCardData(u8 taskId)
{
    u8 index;
    struct TrainerCard *trainerCards;

    if (CheckLinkErrored(taskId) == TRUE)
        return;

    if (GetBlockReceivedStatus() != GetSavedLinkPlayerCountAsBitFlags())
        return;

    for (index = 0; index < GetLinkPlayerCount(); index++)
    {
        CopyTrainerCardData(&gTrainerCards[index], gBlockRecvBuffer[index], gLinkPlayers[index].version);
    }

    SetSuppressLinkErrorMessage(FALSE);
    ResetBlockReceivedFlags();
    FinishLinkup(&gSpecialVar_Result, taskId);
}

static void Task_StopLinkup(u8 taskId)
{
    if (!gReceivedRemoteLinkPlayers)
    {
        ClearLinkPlayerCountWindow(gTasks[taskId].tWindowId);
        EnableBothScriptContexts();
        RemoveWindow(gTasks[taskId].tWindowId);
        DestroyTask(taskId);
    }
}

static void Task_LinkupFailed(u8 taskId)
{
    gSpecialVar_Result = LINKUP_FAILED;
    ClearLinkPlayerCountWindow(gTasks[taskId].tWindowId);
    StopFieldMessage();
    RemoveWindow(gTasks[taskId].tWindowId);
    EnableBothScriptContexts();
    DestroyTask(taskId);
}

static void Task_LinkupConnectionError(u8 taskId)
{
    gSpecialVar_Result = LINKUP_CONNECTION_ERROR;
    ClearLinkPlayerCountWindow(gTasks[taskId].tWindowId);
    RemoveWindow(gTasks[taskId].tWindowId);
    HideFieldMessageBox();
    EnableBothScriptContexts();
    DestroyTask(taskId);
}

static bool8 TryLinkTimeout(u8 taskId)
{
    gTasks[taskId].tTimer++;
    if (gTasks[taskId].tTimer > 600)
    {
        gTasks[taskId].func = Task_LinkupConnectionError;
        return TRUE;
    }

    return FALSE;
}

void TryBattleLinkup(void)
{
    u8 minPlayers = 2;
    u8 maxPlayers = 2;

    switch (gSpecialVar_0x8004)
    {
    case USING_SINGLE_BATTLE:
        minPlayers = 2;
        gLinkType = LINKTYPE_SINGLE_BATTLE;
        break;
    case USING_DOUBLE_BATTLE:
        minPlayers = 2;
        gLinkType = LINKTYPE_DOUBLE_BATTLE;
        break;
    case USING_MULTI_BATTLE:
        minPlayers = 4;
        maxPlayers = 4;
        gLinkType = LINKTYPE_MULTI_BATTLE;
        break;
    case USING_BATTLE_TOWER:
        minPlayers = 2;
        if (gSaveBlock2Ptr->frontier.lvlMode == FRONTIER_LVL_50)
            gLinkType = LINKTYPE_BATTLE_TOWER_50;
        else
            gLinkType = LINKTYPE_BATTLE_TOWER_OPEN;

        break;
    }

    CreateLinkupTask(minPlayers, maxPlayers);
}

#undef tMinPlayers
#undef tMaxPlayers
#undef tNumPlayers
#undef tTimer
#undef tWindowId

void TryTradeLinkup(void)
{
    gLinkType = LINKTYPE_TRADE_SETUP;
    gBattleTypeFlags = 0;
    CreateLinkupTask(2, 2);
}

void TryRecordMixLinkup(void)
{
    gSpecialVar_Result = LINKUP_ONGOING;
    gLinkType = LINKTYPE_RECORD_MIX_BEFORE;
    gBattleTypeFlags = 0;
    CreateLinkupTask(2, 4);
}

void ValidateMixingGameLanguage(void)
{
    u32 taskId = FindTaskIdByFunc(Task_ValidateMixingGameLanguage);

    if (taskId == 0xFF)
    {
        taskId = CreateTask(Task_ValidateMixingGameLanguage, 80);
        gTasks[taskId].tState = 0;
    }
}

static void Task_ValidateMixingGameLanguage(u8 taskId)
{
    int playerCount;
    int i;

    switch (gTasks[taskId].tState)
    {
    case 0:
        if (gSpecialVar_Result == LINKUP_SUCCESS)
        {
            bool32 mixingForeignGames = FALSE;
            bool32 isEnglishRSLinked = FALSE;
            bool32 isJapaneseEmeraldLinked = FALSE;

            playerCount = GetLinkPlayerCount();
            for (i = 0; i < playerCount; i++)
            {
                u32 version = (u8)gLinkPlayers[i].version;
                u32 language = gLinkPlayers[i].language;

                if (version == VERSION_RUBY || version == VERSION_SAPPHIRE)
                {
                    if (language == LANGUAGE_JAPANESE)
                    {
                        mixingForeignGames = TRUE;
                        break;
                    }
                    else
                    {
                        isEnglishRSLinked = TRUE;
                    }
                }
                else if (version == VERSION_EMERALD)
                {
                    if (language == LANGUAGE_JAPANESE)
                    {
                        isJapaneseEmeraldLinked = TRUE;
                    }
                }
            }

            if (isEnglishRSLinked && isJapaneseEmeraldLinked)
            {
                mixingForeignGames = TRUE;
            }

            if (mixingForeignGames)
            {
                gSpecialVar_Result = LINKUP_FOREIGN_GAME;
                SetCloseLinkCallbackHandleJP();
                gTasks[taskId].tState = 1;
                return;
            }
        }
        EnableBothScriptContexts();
        DestroyTask(taskId);
        break;
    case 1:
        if (!gReceivedRemoteLinkPlayers)
        {
            EnableBothScriptContexts();
            DestroyTask(taskId);
        }
        break;
    }
}

void TryBerryBlenderLinkup(void)
{
    gLinkType = LINKTYPE_BERRY_BLENDER_SETUP;
    gBattleTypeFlags = 0;
    CreateLinkupTask(2, 4);
}

void TryContestGModeLinkup(void)
{
    gLinkType = LINKTYPE_CONTEST_GMODE;
    gBattleTypeFlags = 0;
    CreateLinkupTask(4, 4);
}

void TryContestEModeLinkup(void)
{
    gLinkType = LINKTYPE_CONTEST_EMODE;
    gBattleTypeFlags = 0;
    CreateLinkupTask(2, 4);
}

u8 CreateTask_ReestablishCableClubLink(void)
{
    if (FuncIsActiveTask(Task_ReestablishLink) != FALSE)
        return 0xFF;

    switch (gSpecialVar_0x8004)
    {
    case USING_SINGLE_BATTLE:
        gLinkType = LINKTYPE_SINGLE_BATTLE;
        break;
    case USING_DOUBLE_BATTLE:
        gLinkType = LINKTYPE_DOUBLE_BATTLE;
        break;
    case USING_MULTI_BATTLE:
        gLinkType = LINKTYPE_MULTI_BATTLE;
        break;
    case USING_BATTLE_TOWER:
        if (gSaveBlock2Ptr->frontier.lvlMode == FRONTIER_LVL_50)
            gLinkType = LINKTYPE_BATTLE_TOWER_50;
        else
            gLinkType = LINKTYPE_BATTLE_TOWER_OPEN;
        break;
    case USING_TRADE_CENTER:
        gLinkType = LINKTYPE_TRADE;
        break;
    case USING_RECORD_CORNER:
        gLinkType = LINKTYPE_RECORD_MIX_AFTER;
        break;
    }

    return CreateTask(Task_ReestablishLink, 80);
}

static void Task_ReestablishLink(u8 taskId)
{
    s16 *data = gTasks[taskId].data;

    if (data[0] == 0)
    {
        OpenLink();
        ResetLinkPlayers();
        CreateTask(Task_WaitForLinkPlayerConnection, 80);
    }
    else if (data[0] >= 10)
    {
        gTasks[taskId].func = Task_ReestablishLinkAwaitConnection;
    }
    data[0]++;
}

static void Task_ReestablishLinkAwaitConnection(u8 taskId)
{
    if (GetLinkPlayerCount_2() >= 2)
    {
        if (IsLinkMaster() == TRUE)
            gTasks[taskId].func = Task_ReestablishLinkLeader;
        else
            gTasks[taskId].func = Task_ReestablishLinkAwaitConfirmation;
    }
}

static void Task_ReestablishLinkLeader(u8 taskId)
{
    if (GetSavedPlayerCount() == GetLinkPlayerCount_2())
    {
        CheckShouldAdvanceLinkState();
        gTasks[taskId].func = Task_ReestablishLinkAwaitConfirmation;
    }
}

static void Task_ReestablishLinkAwaitConfirmation(u8 taskId)
{
    if (gReceivedRemoteLinkPlayers == TRUE
     && IsLinkPlayerDataExchangeComplete() == TRUE)
    {
        CheckLinkPlayersMatchSaved();
        StartSendingKeysToLink();
        DestroyTask(taskId);
    }
}

// Unused
void CableClubSaveGame(void)
{
    SaveGame();
}

static void SetLinkBattleTypeFlags(int linkService)
{
    switch (linkService)
    {
    case USING_SINGLE_BATTLE:
        gBattleTypeFlags = BATTLE_TYPE_LINK | BATTLE_TYPE_TRAINER;
        break;
    case USING_DOUBLE_BATTLE:
        gBattleTypeFlags = BATTLE_TYPE_DOUBLE | BATTLE_TYPE_LINK | BATTLE_TYPE_TRAINER;
        break;
    case USING_MULTI_BATTLE:
        ReducePlayerPartyToSelectedMons();
        gBattleTypeFlags = BATTLE_TYPE_DOUBLE | BATTLE_TYPE_LINK | BATTLE_TYPE_TRAINER | BATTLE_TYPE_MULTI;
        break;
    case USING_BATTLE_TOWER:
        gBattleTypeFlags = BATTLE_TYPE_BATTLE_TOWER | BATTLE_TYPE_DOUBLE | BATTLE_TYPE_LINK | BATTLE_TYPE_TRAINER | BATTLE_TYPE_MULTI;
        break;
    }
}

#define tTimer data[1]

static void Task_StartWiredCableClubBattle(u8 taskId)
{
    struct Task* task = &gTasks[taskId];

    switch (task->tState)
    {
    case 0:
        FadeScreen(FADE_TO_BLACK, 0);
        gLinkType = LINKTYPE_BATTLE;
        ClearLinkCallback_2();
        task->tState++;
        break;
    case 1:
        if (!gPaletteFade.active)
            task->tState++;
        break;
    case 2:
        task->tTimer++;
        if (task->tTimer > 20)
            task->tState++;
        break;
    case 3:
        SetCloseLinkCallback();
        task->tState++;
        break;
    case 4:
        if (!gReceivedRemoteLinkPlayers)
            task->tState++;
        break;
    case 5:
        if (gLinkPlayers[0].trainerId & 1)
            PlayMapChosenOrBattleBGM(MUS_VS_GYM_LEADER);
        else
            PlayMapChosenOrBattleBGM(MUS_VS_TRAINER);

        SetLinkBattleTypeFlags(gSpecialVar_0x8004);
        CleanupOverworldWindowsAndTilemaps();
        gTrainerBattleOpponent_A = TRAINER_LINK_OPPONENT;
        SetMainCallback2(CB2_InitBattle);
        gMain.savedCallback = CB2_ReturnFromCableClubBattle;
        DestroyTask(taskId);
        break;
    }
}

static void Task_StartWirelessCableClubBattle(u8 taskId)
{
    int i;
    s16* data = gTasks[taskId].data;

    switch (tState)
    {
    case 0:
        FadeScreen(FADE_TO_BLACK, 0);
        gLinkType = LINKTYPE_BATTLE;
        ClearLinkCallback_2();
        tState = 1;
        break;
    case 1:
        if (!gPaletteFade.active)
            tState = 2;
        break;
    case 2:
        SendBlock(0, &gLocalLinkPlayer, sizeof(gLocalLinkPlayer));
        tState = 3;
        break;
    case 3:
        if (GetBlockReceivedStatus() == GetLinkPlayerCountAsBitFlags())
        {
            for (i = 0; i < GetLinkPlayerCount(); i++)
            {
                struct LinkPlayer *player = (struct LinkPlayer *)gBlockRecvBuffer[i];
                gLinkPlayers[i] = *player;
                sub_800B524(&gLinkPlayers[i]);
                ResetBlockReceivedFlag(i);
            }
            tState = 4;
        }
        break;
    case 4:
        tTimer++;
        if (tTimer > 20)
            tState = 5;
        break;
    case 5:
        SetLinkStandbyCallback();
        tState = 6;
        break;
    case 6:
        if (IsLinkTaskFinished())
        {
            tState = 7;
        }
        break;
    case 7:
        if (gLinkPlayers[0].trainerId & 1)
            PlayMapChosenOrBattleBGM(MUS_VS_GYM_LEADER);
        else
            PlayMapChosenOrBattleBGM(MUS_VS_TRAINER);

        gLinkPlayers[0].linkType = LINKTYPE_BATTLE;
        SetLinkBattleTypeFlags(gSpecialVar_0x8004);
        CleanupOverworldWindowsAndTilemaps();
        gTrainerBattleOpponent_A = TRAINER_LINK_OPPONENT;
        SetMainCallback2(CB2_InitBattle);
        gMain.savedCallback = CB2_ReturnFromCableClubBattle;
        DestroyTask(taskId);
        break;
    }
}

#undef tTimer

static void CB2_ReturnFromUnionRoomBattle(void)
{
    u8 playerCount;
    int i;
    bool32 linkedWithFRLG;

    switch (gMain.state)
    {
    case 0:
        playerCount = GetLinkPlayerCount();
        linkedWithFRLG = FALSE;
        for (i = 0; i < playerCount; i++)
        {
            u32 version = (u8)gLinkPlayers[i].version;
            if (version == VERSION_FIRE_RED || version == VERSION_LEAF_GREEN)
            {
                linkedWithFRLG = TRUE;
                break;
            }
        }

        if (linkedWithFRLG)
        {
            gMain.state = 2;
        }
        else
        {
            SetCloseLinkCallback();
            gMain.state = 1;
        }
        break;
    case 1:
        if (!gReceivedRemoteLinkPlayers)
        {
            SetMainCallback2(CB2_ReturnToField);
        }
        break;
    case 2:
        SetMainCallback2(CB2_ReturnToField);
        break;
    }
    RunTasks();
}

void CB2_ReturnFromCableClubBattle(void)
{
    gBattleTypeFlags &= ~BATTLE_TYPE_20;
    Overworld_ResetMapMusic();
    LoadPlayerParty();
    SavePlayerBag();
    UpdateTrainerFansAfterLinkBattle();

    if (gSpecialVar_0x8004 == USING_SINGLE_BATTLE || gSpecialVar_0x8004 == USING_DOUBLE_BATTLE)
    {
        UpdatePlayerLinkBattleRecords(gLocalLinkPlayerId ^ 1);
        if (gWirelessCommType)
        {
            switch (gBattleOutcome)
            {
            case B_OUTCOME_WON:
                RecordIdOfWonderCardSenderByEventType(0, gLinkPlayers[GetMultiplayerId() ^ 1].trainerId);
                break;
            case B_OUTCOME_LOST:
                RecordIdOfWonderCardSenderByEventType(1, gLinkPlayers[GetMultiplayerId() ^ 1].trainerId);
                break;
            }
        }
    }

    if (InUnionRoom() == TRUE)
        gMain.savedCallback = CB2_ReturnFromUnionRoomBattle;
    else
        gMain.savedCallback = CB2_ReturnToFieldFromMultiplayer;

    SetMainCallback2(CB2_SetUpSaveAfterLinkBattle);
}

void CleanupLinkRoomState(void)
{
    if (gSpecialVar_0x8004 == USING_SINGLE_BATTLE
     || gSpecialVar_0x8004 == USING_DOUBLE_BATTLE
     || gSpecialVar_0x8004 == USING_MULTI_BATTLE
     || gSpecialVar_0x8004 == USING_BATTLE_TOWER)
    {
        LoadPlayerParty();
        SavePlayerBag();
    }
    SetWarpDestinationToDynamicWarp(0x7F);
}

void ExitLinkRoom(void)
{
    QueueExitLinkRoomKey();
}

// Note: gSpecialVar_0x8005 contains the id of the seat the player entered
static void Task_EnterCableClubSeat(u8 taskId)
{
    struct Task* task = &gTasks[taskId];

    switch (task->tState)
    {
    case 0:
        ShowFieldMessage(gText_PleaseWaitForLink);
        task->tState = 1;
        break;
    case 1:
        if (IsFieldMessageBoxHidden())
        {
            sub_8087288();
            SetLocalLinkPlayerId(gSpecialVar_0x8005);
            task->tState = 2;
        }
        break;
    case 2:
        switch (sub_8087214())
        {
        case 0:
            break;
        case 1:
            HideFieldMessageBox();
            task->tState = 0;
            sub_80872C4();
            SwitchTaskToFollowupFunc(taskId);
            break;
        case 2:
            task->tState = 3;
            break;
        }
        break;
    case 3:
        sub_808729C();
        sub_8197AE8(TRUE);
        DestroyTask(taskId);
        EnableBothScriptContexts();
        break;
    }
}

void CreateTask_EnterCableClubSeat(TaskFunc followupFunc)
{
    u8 taskId = CreateTask(Task_EnterCableClubSeat, 80);
    SetTaskFuncWithFollowupFunc(taskId, Task_EnterCableClubSeat, followupFunc);
    ScriptContext1_Stop();
}

static void Task_StartWiredTrade(u8 taskId)
{
    struct Task *task = &gTasks[taskId];

    switch (task->tState)
    {
    case 0:
        ScriptContext2_Enable();
        FadeScreen(FADE_TO_BLACK, 0);
        ClearLinkCallback_2();
        task->tState++;
        break;
    case 1:
        if (!gPaletteFade.active)
            task->tState++;
        break;
    case 2:
        gSelectedTradeMonPositions[TRADE_PLAYER] = 0;
        gSelectedTradeMonPositions[TRADE_PARTNER] = 0;
        m4aMPlayAllStop();
        SetCloseLinkCallback();
        task->tState++;
        break;
    case 3:
        if (!gReceivedRemoteLinkPlayers)
        {
            SetMainCallback2(CB2_StartCreateTradeMenu);
            DestroyTask(taskId);
        }
        break;
    }
}

static void Task_StartWirelessTrade(u8 taskId)
{
    s16 *data = gTasks[taskId].data;

    switch (tState)
    {
    case 0:
        ScriptContext2_Enable();
        FadeScreen(FADE_TO_BLACK, 0);
        ClearLinkRfuCallback();
        tState++;
        break;
    case 1:
        if (!gPaletteFade.active)
            tState++;
        break;
    case 2:
        gSelectedTradeMonPositions[TRADE_PLAYER] = 0;
        gSelectedTradeMonPositions[TRADE_PARTNER] = 0;
        m4aMPlayAllStop();
        SetLinkStandbyCallback();
        tState++;
        break;
    case 3:
        if (IsLinkTaskFinished())
        {
            CreateTask_CreateTradeMenu();
            DestroyTask(taskId);
        }
        break;
    }
}

void PlayerEnteredTradeSeat(void)
{
    if (gWirelessCommType != 0)
        CreateTask_EnterCableClubSeat(Task_StartWirelessTrade);
    else
        CreateTask_EnterCableClubSeat(Task_StartWiredTrade);
}

// Unused
static void CreateTask_StartWiredTrade(void)
{
    CreateTask(Task_StartWiredTrade, 80);
}

void nullsub_37(void)
{

}

void ColosseumPlayerSpotTriggered(void)
{
    gLinkType = LINKTYPE_BATTLE;

    if (gWirelessCommType)
        CreateTask_EnterCableClubSeat(Task_StartWirelessCableClubBattle);
    else
        CreateTask_EnterCableClubSeat(Task_StartWiredCableClubBattle);
}

// Unused
static void CreateTask_EnterCableClubSeatNoFollowup(void)
{
    u8 taskId = CreateTask(Task_EnterCableClubSeat, 80);
    ScriptContext1_Stop();
}

void Script_ShowLinkTrainerCard(void)
{
    ShowTrainerCardInLink(gSpecialVar_0x8006, CB2_ReturnToFieldContinueScriptPlayMapMusic);
}

// Returns FALSE if the player has no stars. Returns TRUE otherwise, and puts the name of the
// color into gStringVar2.
bool32 GetLinkTrainerCardColor(u8 linkPlayerIndex)
{
    u32 numStars;

    gSpecialVar_0x8006 = linkPlayerIndex;
    StringCopy(gStringVar1, gLinkPlayers[linkPlayerIndex].name);

    numStars = GetTrainerCardStars(linkPlayerIndex);
    if (numStars == 0)
        return FALSE;

    StringCopy(gStringVar2, sTrainerCardColorNames[numStars - 1]);
    return TRUE;
}

#define tTimer data[0]

void Task_WaitForLinkPlayerConnection(u8 taskId)
{
    struct Task *task = &gTasks[taskId];

    task->tTimer++;
    if (task->tTimer > 300)
    {
        CloseLink();
        SetMainCallback2(CB2_LinkError);
        DestroyTask(taskId);
    }

    if (gReceivedRemoteLinkPlayers)
    {
        // Players connected, destroy task
        if (gWirelessCommType == 0)
        {
            if (!DoesLinkPlayerCountMatchSaved())
            {
                CloseLink();
                SetMainCallback2(CB2_LinkError);
            }
            DestroyTask(taskId);
        }
        else
        {
            DestroyTask(taskId);
        }
    }
}

#undef tTimer

static void sub_80B3AAC(u8 taskId)
{
    if (!gReceivedRemoteLinkPlayers)
    {
        EnableBothScriptContexts();
        DestroyTask(taskId);
    }
}

// Unused
static void sub_80B3AD0(u8 taskId)
{
    SetCloseLinkCallback();
    gTasks[taskId].func = sub_80B3AAC;
}

#define tTimer data[1]

void sub_80B3AF8(u8 taskId)
{
    s16 *data = gTasks[taskId].data;

    switch (tState)
    {
    case 0:
        if (gWirelessCommType != 0)
        {
            DestroyTask(taskId);
        }
        else
        {
            OpenLink();
            CreateTask(Task_WaitForLinkPlayerConnection, 1);
            tState++;
        }
        break;
    case 1:
        if (++tTimer > 11)
        {
            tTimer = 0;
            tState++;
        }
        break;
    case 2:
        if (GetLinkPlayerCount_2() >= GetSavedPlayerCount())
        {
            if (IsLinkMaster())
            {
                if (++tTimer > 30)
                {
                    CheckShouldAdvanceLinkState();
                    tState++;
                }
            }
            else
            {
                tState++;
            }
        }
        break;
    case 3:
        if (gReceivedRemoteLinkPlayers == TRUE && IsLinkPlayerDataExchangeComplete() == TRUE)
        {
            DestroyTask(taskId);
        }
        break;
    }
}

#undef tTimer

void TrySetBattleTowerLinkType(void)
{
    if (gWirelessCommType == 0)
        gLinkType = LINKTYPE_BATTLE_TOWER;
}

#undef tState

repo_name: Lunos/pokeemerald

license: mit


code:
#include "global.h"
#include "task.h"
#include "text.h"
#include "menu.h"
#include "sound.h"
#include "main.h"
#include "save.h"
#include "palette.h"
#include "gpu_regs.h"
#include "bg.h"
#include "text_window.h"
#include "constants/songs.h"
#include "constants/rgb.h"

extern const u8 gText_ClearAllSaveData[];
extern const u8 gText_ClearingData[];

static void Task_DoClearSaveDataScreenYesNo(u8);
static void Task_ClearSaveDataScreenYesNoChoice(u8);
static void Task_ClearSaveData(u8);
static bool8 SetupClearSaveDataScreen(void);
static void CB2_FadeAndDoReset(void);
static void InitClearSaveDataScreenWindows(void);

static const struct BgTemplate sClearSaveBgTemplates[2] =
{
    {
        .bg = 0,
        .charBaseIndex = 0,
        .mapBaseIndex = 31,
        .screenSize = 0,
        .paletteMode = 0,
        .priority = 0,
        .baseTile = 0,
    },
    {
        .bg = 3,
        .charBaseIndex = 0,
        .mapBaseIndex = 30,
        .screenSize = 0,
        .paletteMode = 0,
        .priority = 1,
        .baseTile = 0,
    },
};

static const struct WindowTemplate sClearSaveTextWindow[] =
{
    {
        .bg = 0,
        .tilemapLeft = 3,
        .tilemapTop = 15,
        .width = 26,
        .height = 4,
        .paletteNum = 15,
        .baseBlock = 11,
    },
    DUMMY_WIN_TEMPLATE
};

static const struct WindowTemplate sClearSaveYesNo[] =
{
    {
        .bg = 0,
        .tilemapLeft = 3,
        .tilemapTop = 2,
        .width = 5,
        .height = 4,
        .paletteNum = 15,
        .baseBlock = 115,
    }
};

void CB2_InitClearSaveDataScreen(void)
{
    if (SetupClearSaveDataScreen())
        CreateTask(Task_DoClearSaveDataScreenYesNo, 0);
}

static void Task_DoClearSaveDataScreenYesNo(u8 taskId)
{
    DrawStdFrameWithCustomTileAndPalette(0, 0, 2, 14);
    AddTextPrinterParameterized(0, 1, gText_ClearAllSaveData, 0, 1, 0, 0);
    CreateYesNoMenu(sClearSaveYesNo, 2, 14, 1);
    gTasks[taskId].func = Task_ClearSaveDataScreenYesNoChoice;
}

static void Task_ClearSaveDataScreenYesNoChoice(u8 taskId)
{
    switch (Menu_ProcessInputNoWrapClearOnChoose())
    {
    case 0:
        FillWindowPixelBuffer(0, PIXEL_FILL(1));
        AddTextPrinterParameterized(0, 1, gText_ClearingData, 0, 1, 0, 0);
        gTasks[taskId].func = Task_ClearSaveData;
        break;
    case 1:
    case -1:
        PlaySE(SE_SELECT);
        DestroyTask(taskId);
        SetMainCallback2(CB2_FadeAndDoReset);
    }
}

static void Task_ClearSaveData(u8 taskId)
{
    ClearSaveData();
    DestroyTask(taskId);
    SetMainCallback2(CB2_FadeAndDoReset);
}

static void MainCB(void)
{
    RunTasks();
    UpdatePaletteFade();
}

static void VBlankCB(void)
{
    TransferPlttBuffer();
}

static bool8 SetupClearSaveDataScreen(void)
{
    u16 i;

    switch(gMain.state)
    {
    case 0:
    default:
        SetVBlankCallback(NULL);
        SetGpuReg(REG_OFFSET_DISPCNT, DISPCNT_MODE_0);
        SetGpuReg(REG_OFFSET_BG0HOFS, 0);
        SetGpuReg(REG_OFFSET_BG0VOFS, 0);
        SetGpuReg(REG_OFFSET_BG3HOFS, 0);
        SetGpuReg(REG_OFFSET_BG3VOFS, 0);
        SetGpuReg(REG_OFFSET_WIN0H, 0);
        SetGpuReg(REG_OFFSET_WIN0V, 0);
        SetGpuReg(REG_OFFSET_WININ, 0);
        SetGpuReg(REG_OFFSET_WINOUT, 0);
        SetGpuReg(REG_OFFSET_BLDCNT, 0);
        SetGpuReg(REG_OFFSET_BLDALPHA, 0);
        SetGpuReg(REG_OFFSET_BLDY, 0);
        DmaFill16(3, 0, (void *)VRAM, VRAM_SIZE);
        DmaFill32(3, 0, (void *)OAM, OAM_SIZE);
        DmaFill16(3, 0, (void *)(PLTT + 2), PLTT_SIZE - 2);
        ResetPaletteFade();
        gPlttBufferUnfaded[0] = RGB_WHITE;
        gPlttBufferFaded[0] = RGB_WHITE;
        gPlttBufferUnfaded[1] = RGB(5, 10, 14);
        gPlttBufferFaded[1] = RGB(5, 10, 14);
        for (i = 0; i < 0x10; i++)
            ((u16 *)(VRAM + 0x20))[i] = 0x1111;

        for (i = 0; i < 0x400; i++)
            ((u16 *)(BG_SCREEN_ADDR(30)))[i] = 0x0001;
        ResetTasks();
        ResetSpriteData();
        ResetBgsAndClearDma3BusyFlags(0);
        InitBgsFromTemplates(0, sClearSaveBgTemplates, ARRAY_COUNT(sClearSaveBgTemplates));
        SetGpuReg(REG_OFFSET_DISPCNT, DISPCNT_OBJ_ON | DISPCNT_OBJ_1D_MAP);
        ShowBg(0);
        ShowBg(3);
        SetGpuReg(REG_OFFSET_BLDCNT, 0);
        InitClearSaveDataScreenWindows();
        BeginNormalPaletteFade(0x0000FFFF, 0, 0x10, 0, RGB_WHITEALPHA);
        EnableInterrupts(INTR_FLAG_VBLANK);
        SetVBlankCallback(VBlankCB);
        gMain.state = 1;
        break;
    case 1:
        UpdatePaletteFade();
        if(!gPaletteFade.active)
        {
            SetMainCallback2(MainCB);
            return TRUE;
        }
    }
    return FALSE;
}

static void CB2_FadeAndDoReset(void)
{
    switch(gMain.state)
    {
    case 0:
    default:
        BeginNormalPaletteFade(0x0000FFFF, 0, 0, 0x10, RGB_WHITEALPHA);
        gMain.state = 1;
        break;
    case 1:
        UpdatePaletteFade();
        if(!gPaletteFade.active)
        {
            FreeAllWindowBuffers();
            DoSoftReset();
        }
        break;
    }
}

static void InitClearSaveDataScreenWindows(void)
{
    InitWindows(sClearSaveTextWindow);
    DeactivateAllTextPrinters();
    FillWindowPixelBuffer(0, PIXEL_FILL(0));
    LoadWindowGfx(0, 0, 2, 224);
    LoadPalette(gUnknown_0860F074, 0xF0, 0x20);
}


code:
#include "global.h"
#include "event_data.h"
#include "rtc.h"
#include "time_events.h"
#include "field_specials.h"
#include "lottery_corner.h"
#include "dewford_trend.h"
#include "tv.h"
#include "field_weather.h"
#include "berry.h"
#include "main.h"
#include "overworld.h"
#include "wallclock.h"

static void UpdatePerDay(struct Time *localTime);
static void UpdatePerMinute(struct Time *localTime);

static void InitTimeBasedEvents(void)
{
    FlagSet(FLAG_SYS_CLOCK_SET);
    RtcCalcLocalTime();
    gSaveBlock2Ptr->lastBerryTreeUpdate = gLocalTime;
    VarSet(VAR_DAYS, gLocalTime.days);
}

void DoTimeBasedEvents(void)
{
    if (FlagGet(FLAG_SYS_CLOCK_SET) && !InPokemonCenter())
    {
        RtcCalcLocalTime();
        UpdatePerDay(&gLocalTime);
        UpdatePerMinute(&gLocalTime);
    }
}

static void UpdatePerDay(struct Time *localTime)
{
    u16 *days = GetVarPointer(VAR_DAYS);
    u16 daysSince;

    if (*days != localTime->days && *days <= localTime->days)
    {
        daysSince = localTime->days - *days;
        ClearDailyFlags();
        UpdateDewfordTrendPerDay(daysSince);
        UpdateTVShowsPerDay(daysSince);
        UpdateWeatherPerDay(daysSince);
        UpdatePartyPokerusTime(daysSince);
        UpdateMirageRnd(daysSince);
        UpdateBirchState(daysSince);
        UpdateFrontierManiac(daysSince);
        UpdateFrontierGambler(daysSince);
        SetShoalItemFlag(daysSince);
        SetRandomLotteryNumber(daysSince);
        *days = localTime->days;
    }
}

static void UpdatePerMinute(struct Time *localTime)
{
    struct Time difference;
    int minutes;

    CalcTimeDifference(&difference, &gSaveBlock2Ptr->lastBerryTreeUpdate, localTime);
    minutes = 24 * 60 * difference.days + 60 * difference.hours + difference.minutes;
    if (minutes != 0)
    {
        if (minutes >= 0)
        {
            BerryTreeTimeUpdate(minutes);
            gSaveBlock2Ptr->lastBerryTreeUpdate = *localTime;
        }
    }
}

static void ReturnFromStartWallClock(void)
{
    InitTimeBasedEvents();
    SetMainCallback2(CB2_ReturnToFieldContinueScriptPlayMapMusic);
}

void StartWallClock(void)
{
    SetMainCallback2(CB2_StartWallClock);
    gMain.savedCallback = ReturnFromStartWallClock;
}


code:
#include "global.h"
#include "coins.h"
#include "text.h"
#include "window.h"
#include "strings.h"
#include "string_util.h"
#include "menu.h"
#include "international_string_util.h"
#include "constants/coins.h"

EWRAM_DATA u8 sCoinsWindowId = 0;

void PrintCoinsString(u32 coinAmount)
{
    u32 xAlign;

    ConvertIntToDecimalStringN(gStringVar1, coinAmount, STR_CONV_MODE_RIGHT_ALIGN, MAX_COIN_DIGITS);
    StringExpandPlaceholders(gStringVar4, gText_Coins);

    xAlign = GetStringRightAlignXOffset(1, gStringVar4, 0x40);
    AddTextPrinterParameterized(sCoinsWindowId, 1, gStringVar4, xAlign, 1, 0, NULL);
}

void ShowCoinsWindow(u32 coinAmount, u8 x, u8 y)
{
    struct WindowTemplate template;
    SetWindowTemplateFields(&template, 0, x, y, 8, 2, 0xF, 0x141);
    sCoinsWindowId = AddWindow(&template);
    FillWindowPixelBuffer(sCoinsWindowId, PIXEL_FILL(0));
    PutWindowTilemap(sCoinsWindowId);
    DrawStdFrameWithCustomTileAndPalette(sCoinsWindowId, FALSE, 0x214, 0xE);
    PrintCoinsString(coinAmount);
}

void HideCoinsWindow(void)
{
    ClearStdWindowAndFrame(sCoinsWindowId, TRUE);
    RemoveWindow(sCoinsWindowId);
}

u16 GetCoins(void)
{
    return gSaveBlock1Ptr->coins ^ gSaveBlock2Ptr->encryptionKey;
}

void SetCoins(u16 coinAmount)
{
    gSaveBlock1Ptr->coins = coinAmount ^ gSaveBlock2Ptr->encryptionKey;
}

bool8 AddCoins(u16 toAdd)
{
    u16 newAmount;
    u16 ownedCoins = GetCoins();
    if (ownedCoins >= MAX_COINS)
        return FALSE;
    // check overflow, can't have less coins than previously
    if (ownedCoins > ownedCoins + toAdd)
    {
        newAmount = MAX_COINS;
    }
    else
    {
        ownedCoins += toAdd;
        if (ownedCoins > MAX_COINS)
            ownedCoins = MAX_COINS;
        newAmount = ownedCoins;
    }
    SetCoins(newAmount);
    return TRUE;
}

bool8 RemoveCoins(u16 toSub)
{
    u16 ownedCoins = GetCoins();
    if (ownedCoins >= toSub)
    {
        SetCoins(ownedCoins - toSub);
        return TRUE;
    }
    return FALSE;
}


code:
#include "global.h"
#include "confetti_util.h"
#include "malloc.h"
#include "main.h"
#include "digit_obj_util.h"

static EWRAM_DATA struct
{
    u8 count;
    struct ConfettiUtil *array;
} *sWork = NULL;

static void sub_81520A8(void *dest, u16 value, u8 left, u8 top, u8 width, u8 height) // Unused.
{
    u8 i;
    u8 j;
    u8 x;
    u8 y;

    for (i = 0, y = top; i < height; i++)
    {
        for (x = left, j = 0; j < width; j++)
        {
            *(u16 *)((dest) + (y * 64 + x * 2)) = value;
            x = (x + 1) % 32;
        }
        y = (y + 1) % 32;
    }
}

static void sub_8152134(void *dest, const u16 *src, u8 left, u8 top, u8 width, u8 height) // Unused.
{
    u8 i;
    u8 j;
    u8 x;
    u8 y;
    const u16 *_src;

    for (i = 0, _src = src, y = top; i < height; i++)
    {
        for (x = left, j = 0; j < width; j++)
        {
            *(u16 *)((dest) + (y * 64 + x * 2)) = *(_src++);
            x = (x + 1) % 32;
        }
        y = (y + 1) % 32;
    }
}

bool32 ConfettiUtil_Init(u8 count)
{
    u8 i = 0;

    if (count == 0)
        return FALSE;
    if (count > 64)
        count = 64;

    sWork = AllocZeroed(sizeof(*sWork));
    if (sWork == NULL)
        return FALSE;
    sWork->array = AllocZeroed(count * sizeof(struct ConfettiUtil));
    if (sWork->array == NULL)
    {
        FREE_AND_SET_NULL(sWork);
        return FALSE;
    }

    sWork->count = count;
    for (i = 0; i < count; i++)
    {
        memcpy(&sWork->array[i].oam, &gDummyOamData, sizeof(struct OamData));
        sWork->array[i].dummied = TRUE;
    }

    return TRUE;
}

bool32 ConfettiUtil_Free(void)
{
    u8 i = 0;

    if (sWork == NULL)
        return FALSE;

    for (i = 0; i < sWork->count; i++)
        memcpy(&gMain.oamBuffer[i + 64], &gDummyOamData, sizeof(struct OamData));

    memset(sWork->array, 0, sWork->count * sizeof(struct ConfettiUtil));
    FREE_AND_SET_NULL(sWork->array);
    memset(sWork, 0, sizeof(*sWork));
    FREE_AND_SET_NULL(sWork);

    return TRUE;
}

bool32 ConfettiUtil_Update(void)
{
    u8 i = 0;

    if (sWork == NULL || sWork->array == NULL)
        return FALSE;

    for (i = 0; i < sWork->count; i++)
    {
        if (sWork->array[i].active && sWork->array[i].allowUpdates)
        {
            if (sWork->array[i].callback != NULL)
                sWork->array[i].callback(&sWork->array[i]);

            if (sWork->array[i].dummied)
            {
                memcpy(&gMain.oamBuffer[i + 64], &gDummyOamData, sizeof(struct OamData));
            }
            else
            {
                sWork->array[i].oam.y = sWork->array[i].y + sWork->array[i].yDelta;
                sWork->array[i].oam.x = sWork->array[i].x + sWork->array[i].xDelta;
                sWork->array[i].oam.priority = sWork->array[i].priority;
                sWork->array[i].oam.tileNum = sWork->array[i].tileNum;
                memcpy(&gMain.oamBuffer[i + 64], &sWork->array[i], sizeof(struct OamData));
            }
        }
    }

    return TRUE;
}

static bool32 SetAnimAndTileNum(struct ConfettiUtil *structPtr, u8 animNum)
{
    u16 tileStart;

    if (structPtr == NULL)
        return FALSE;

    tileStart = GetSpriteTileStartByTag(structPtr->tileTag);
    if (tileStart == 0xFFFF)
        return FALSE;

    structPtr->animNum = animNum;
    structPtr->tileNum = (GetTilesPerImage(structPtr->oam.shape, structPtr->oam.size) * animNum) + tileStart;
    return TRUE;
}

u8 ConfettiUtil_SetCallback(u8 id, void (*func)(struct ConfettiUtil *))
{
    if (sWork == NULL || id >= sWork->count)
        return 0xFF;
    else if (!sWork->array[id].active)
        return 0xFF;

    sWork->array[id].callback = func;
    return id;
}

u8 ConfettiUtil_SetData(u8 id, u8 dataArrayId, s16 dataValue)
{
    if (sWork == NULL || id >= sWork->count)
        return 0xFF;
    else if (!sWork->array[id].active || dataArrayId > ARRAY_COUNT(sWork->array[id].data) - 1) // - 1 b/c last slot is reserved for taskId
        return 0xFF;

    sWork->array[id].data[dataArrayId] = dataValue;
    return id;
}

u8 ConfettiUtil_AddNew(const struct OamData *oam, u16 tileTag, u16 palTag, s16 x, s16 y, u8 animNum, u8 priority)
{
    struct ConfettiUtil *structPtr = NULL;
    u8 i;

    if (sWork == NULL || oam == NULL)
        return 0xFF;

    for (i = 0; i < sWork->count; i++)
    {
        if (!sWork->array[i].active)
        {
            structPtr = &sWork->array[i];
            memset(structPtr, 0, sizeof(*structPtr));
            structPtr->id = i;
            structPtr->active = TRUE;
            structPtr->allowUpdates = TRUE;
            break;
        }
    }

    if (structPtr == NULL)
        return 0xFF;

    memcpy(&structPtr->oam, oam, sizeof(*oam));
    structPtr->tileTag = tileTag;
    structPtr->palTag = palTag;
    structPtr->x = x;
    structPtr->y = y;
    structPtr->oam.paletteNum = IndexOfSpritePaletteTag(palTag);
    if (priority < 4)
    {
        structPtr->priority = priority;
        structPtr->oam.priority = priority;
    }
    SetAnimAndTileNum(structPtr, animNum);

    return structPtr->id;
}

u8 ConfettiUtil_Remove(u8 id)
{
    if (sWork == NULL || !sWork->array[id].active)
        return 0xFF;

    memset(&sWork->array[id], 0, sizeof(struct ConfettiUtil));
    sWork->array[id].oam.y = 160;
    sWork->array[id].oam.x = 240;
    sWork->array[id].dummied = TRUE;
    memcpy(&gMain.oamBuffer[id + 64], &gDummyOamData, sizeof(struct OamData));
    return id;
}