#include <ultra64.h>

#include "sm64.h"
#include "game_init.h"
#include "mario.h"
#include "memory.h"
#include "save_file.h"
#include "engine/surface_collision.h"
#include "engine/graph_node.h"
#include "geo_misc.h"
#include "area.h"
#include "segment2.h"
#include "paintings.h"

/**
 * @file paintings.c
 *
 * Implements the rippling painting effect. Paintings are GraphNodes that exist without being connected
 * to any particular object.
 *
 * Paintings are defined in level data. Look at levels/castle_inside/painting.inc.c for examples.
 *
 * The ripple effect uses data that is split into several parts:
 *      The mesh positions are generated from a base mesh. See seg2_painting_triangle_mesh near the
 *          bottom of bin/segment2.c
 *
 *      The lighting for the ripple is also generated from a base table, seg2_painting_mesh_neighbor_tris
 *          in bin/segment2.c
 *
 *      Each painting's texture uses yet another table to map its texture to the mesh.
 *          These maps are in level data, see levels/castle_inside/painting.inc.c for example.
 *
 *      Finally, each painting has two display lists, normal and rippling, which are defined in the same
 *      level data file as the Painting itself. See levels/castle_inside/painting.inc.c.
 *
 *
 * Painting state machine:
 * Paintings spawn in the PAINTING_IDLE state
 *      From IDLE, paintings can change to PAINTING_RIPPLE or PAINTING_ENTERED
 *        - This state checks for ENTERED because if mario waits long enough, a PROXIMITY painting could
 *          reset to IDLE
 *
 * Paintings in the PAINTING_RIPPLE state are passively rippling.
 *      For RIPPLE_TRIGGER_PROXIMITY paintings, this means mario bumped the wall in front of the
 *          painting.
 *
 *      Paintings that use RIPPLE_TRIGGER_CONTINUOUS try to transition to this state as soon as possible,
 *          usually when mario enters the room.
 *
 *      A PROXIMITY painting will automatically reset to IDLE if its ripple magnitude becomes small
 *          enough.
 *
 * Paintings in the PAINTING_ENTERED state have been entered by mario.
 *      A CONTINUOUS painting will automatically reset to RIPPLE if its ripple magnitude becomes small
 *          enough.
 */

/**
 * Triggers a passive ripple on the left side of the painting.
 */
#define RIPPLE_LEFT 0x20

/**
 * Triggers a passive ripple in the middle the painting.
 */
#define RIPPLE_MIDDLE 0x10

/**
 * Triggers a passive ripple on the right side of the painting.
 */
#define RIPPLE_RIGHT 0x8

/**
 * Triggers an entry ripple on the left side of the painting.
 */
#define ENTER_LEFT 0x4

/**
 * Triggers an entry ripple in the middle of the painting.
 */
#define ENTER_MIDDLE 0x2

/**
 * Triggers an entry ripple on the right side of the painting.
 */
#define ENTER_RIGHT 0x1

/**
 * Use the 1/4th part of the painting that is nearest to mario's current floor.
 */
#define NEAREST_4TH 30

/**
 * Use mario's relative x position.
 * @see painting_mario_x
 */
#define MARIO_X 40

/**
 * Use the x center of the painting.
 */
#define MIDDLE_X 50

/**
 * Use mario's relative y position.
 * @see painting_mario_y
 */
#define MARIO_Y 60

/**
 * Use mario's relative z position.
 * @see painting_mario_z
 */
#define MARIO_Z 70

/**
 * Use the y center of the painting.
 */
#define MIDDLE_Y 80

/**
 * Does nothing to the timer.
 * Why -56 instead of false? Who knows.
 */
#define DONT_RESET -56

/**
 * Reset the timer to 0.
 */
#define RESET_TIMER 100

/// A copy of the type of floor mario is standing on.
s16 gPaintingMarioFloorType;
// A copy of mario's position
f32 gPaintingMarioXPos, gPaintingMarioYPos, gPaintingMarioZPos;

/**
 * When a painting is rippling, this mesh is generated each frame using the Painting's parameters.
 *
 * This mesh only contains the vertex positions and normals.
 * Paintings use an additional array to map textures to the mesh.
 */
struct PaintingMeshVertex *gPaintingMesh;

/**
 * The painting's surface normals, used to approximate each of the vertex normals (for gouraud shading).
 */
Vec3f *gPaintingTriNorms;

/**
 * The painting that is currently rippling. Only one painting can be rippling at once.
 */
struct Painting *gRipplingPainting;

/**
 * Whether the DDD painting is moved forward, should being moving backwards, or has already moved backwards.
 */
s8 gDddPaintingStatus;

struct Painting *sHmcPaintings[] = {
    &cotmc_painting,
    NULL,
};

struct Painting *sInsideCastlePaintings[] = {
    &bob_painting, &ccm_painting, &wf_painting,  &jrb_painting,      &lll_painting,
    &ssl_painting, &hmc_painting, &ddd_painting, &wdw_painting,      &thi_tiny_painting,
    &ttm_painting, &ttc_painting, &sl_painting,  &thi_huge_painting, NULL,
};

struct Painting *sTtmPaintings[] = {
    &ttm_slide_painting,
    NULL,
};

struct Painting **sPaintingGroups[] = {
    sHmcPaintings,
    sInsideCastlePaintings,
    sTtmPaintings,
};

s16 gPaintingUpdateCounter = 1;
s16 gLastPaintingUpdateCounter = 0;

/**
 * Stop paintings in paintingGroup from rippling if their id is different from *idptr.
 */
void stop_other_paintings(s16 *idptr, struct Painting *paintingGroup[]) {
    s16 index;
    s16 id = *idptr;

    index = 0;
    while (paintingGroup[index] != NULL) {
        struct Painting *painting = segmented_to_virtual(paintingGroup[index]);

        // stop all rippling except for the selected painting
        if (painting->id != id) {
            painting->state = 0;
        }
        index++;
    }
}

/**
 * @return mario's y position inside the painting (bounded).
 */
f32 painting_mario_y(struct Painting *painting) {
    //! Unnecessary use of double constants
    // Add 50 to make the ripple closer to mario's center of mass.
    f32 relY = gPaintingMarioYPos - painting->posY + 50.0;

    if (relY < 0.0) {
        relY = 0.0;
    } else if (relY > painting->size) {
        relY = painting->size;
    }
    return relY;
}

/**
 * @return mario's z position inside the painting (bounded).
 */
f32 painting_mario_z(struct Painting *painting) {
    f32 relZ = painting->posZ - gPaintingMarioZPos;

    if (relZ < 0.0) {
        relZ = 0.0;
    } else if (relZ > painting->size) {
        relZ = painting->size;
    }
    return relZ;
}

/**
 * @return The y origin for the ripple, based on ySource.
 *         For floor paintings, the z-axis is treated as y.
 */
f32 painting_ripple_y(struct Painting *painting, s8 ySource) {
    switch (ySource) {
        case MARIO_Y:
            return painting_mario_y(painting); // normal wall paintings
            break;
        case MARIO_Z:
            return painting_mario_z(painting); // floor paintings use X and Z
            break;
        case MIDDLE_Y:
            return painting->size / 2.0; // some concentric ripples don't care about Mario
            break;
    }
}

/**
 * Return the quarter of the painting that is closest to the floor mario entered.
 */
f32 painting_nearest_4th(struct Painting *painting) {
    f32 firstQuarter = painting->size / 4.0;       // 1/4 of the way across the painting
    f32 secondQuarter = painting->size / 2.0;      // 1/2 of the way across the painting
    f32 thirdQuarter = painting->size * 3.0 / 4.0; // 3/4 of the way across the painting

    if (painting->floorEntered & RIPPLE_LEFT) {
        return firstQuarter;
    } else if (painting->floorEntered & RIPPLE_MIDDLE) {
        return secondQuarter;
    } else if (painting->floorEntered & RIPPLE_RIGHT) {
        return thirdQuarter;

    // Same as ripple floors.
    } else if (painting->floorEntered & ENTER_LEFT) {
        return firstQuarter;
    } else if (painting->floorEntered & ENTER_MIDDLE) {
        return secondQuarter;
    } else if (painting->floorEntered & ENTER_RIGHT) {
        return thirdQuarter;
    }
}

/**
 * @return mario's x position inside the painting (bounded).
 */
f32 painting_mario_x(struct Painting *painting) {
    f32 relX = gPaintingMarioXPos - painting->posX;

    if (relX < 0.0) {
        relX = 0.0;
    } else if (relX > painting->size) {
        relX = painting->size;
    }
    return relX;
}

/**
 * @return The x origin for the ripple, based on xSource.
 */
f32 painting_ripple_x(struct Painting *painting, s8 xSource) {
    switch (xSource) {
        case NEAREST_4TH: // normal wall paintings
            return painting_nearest_4th(painting);
            break;
        case MARIO_X: // horizontally placed paintings use X and Z
            return painting_mario_x(painting);
            break;
        case MIDDLE_X: // concentric rippling may not care about Mario
            return painting->size / 2.0;
            break;
    }
}

/**
 * Set the painting's state, causing it to start a passive ripple or a ripple from mario entering.
 *
 * @param state The state to enter
 * @param painting,paintingGroup identifies the painting that is changing state
 * @param xSource,ySource what to use for the x and y origin of the ripple
 * @param resetTimer if 100, set the timer to 0
 */
void painting_state(s8 state, struct Painting *painting, struct Painting *paintingGroup[],
                    s8 xSource, s8 ySource, s8 resetTimer) {
    // make sure no other paintings are rippling
    stop_other_paintings(&painting->id, paintingGroup);

    // use a different set of variables depending on the state
    switch (state) {
        case PAINTING_RIPPLE:
            painting->currRippleMag    = painting->passiveRippleMag;
            painting->rippleDecay      = painting->passiveRippleDecay;
            painting->currRippleRate   = painting->passiveRippleRate;
            painting->dispersionFactor = painting->passiveDispersionFactor;
            break;

        case PAINTING_ENTERED:
            painting->currRippleMag    = painting->entryRippleMag;
            painting->rippleDecay      = painting->entryRippleDecay;
            painting->currRippleRate   = painting->entryRippleRate;
            painting->dispersionFactor = painting->entryDispersionFactor;
            break;
    }
    painting->state = state;
    painting->rippleX = painting_ripple_x(painting, xSource);
    painting->rippleY = painting_ripple_y(painting, ySource);
    gPaintingMarioYEntry = gPaintingMarioYPos;

    // Because true or false would be too simple...
    if (resetTimer == RESET_TIMER) {
        painting->rippleTimer = 0.0f;
    }
    gRipplingPainting = painting;
}

/**
 * Idle update function for wall paintings that use RIPPLE_TRIGGER_PROXIMITY.
 */
void wall_painting_proximity_idle(struct Painting *painting, struct Painting *paintingGroup[]) {
    // Check for mario triggering a ripple
    if (painting->floorEntered & RIPPLE_LEFT) {
        painting_state(PAINTING_RIPPLE, painting, paintingGroup, NEAREST_4TH, MARIO_Y, RESET_TIMER);
    } else if (painting->floorEntered & RIPPLE_MIDDLE) {
        painting_state(PAINTING_RIPPLE, painting, paintingGroup, NEAREST_4TH, MARIO_Y, RESET_TIMER);
    } else if (painting->floorEntered & RIPPLE_RIGHT) {
        painting_state(PAINTING_RIPPLE, painting, paintingGroup, NEAREST_4TH, MARIO_Y, RESET_TIMER);

    // Check for mario entering
    } else if (painting->floorEntered & ENTER_LEFT) {
        painting_state(PAINTING_ENTERED, painting, paintingGroup, NEAREST_4TH, MARIO_Y, RESET_TIMER);
    } else if (painting->floorEntered & ENTER_MIDDLE) {
        painting_state(PAINTING_ENTERED, painting, paintingGroup, NEAREST_4TH, MARIO_Y, RESET_TIMER);
    } else if (painting->floorEntered & ENTER_RIGHT) {
        painting_state(PAINTING_ENTERED, painting, paintingGroup, NEAREST_4TH, MARIO_Y, RESET_TIMER);
    }
}

/**
 * Rippling update function for wall paintings that use RIPPLE_TRIGGER_PROXIMITY.
 */
void wall_painting_proximity_rippling(struct Painting *painting, struct Painting *paintingGroup[]) {
    if (painting->floorEntered & ENTER_LEFT) {
        painting_state(PAINTING_ENTERED, painting, paintingGroup, NEAREST_4TH, MARIO_Y, RESET_TIMER);
    } else if (painting->floorEntered & ENTER_MIDDLE) {
        painting_state(PAINTING_ENTERED, painting, paintingGroup, NEAREST_4TH, MARIO_Y, RESET_TIMER);
    } else if (painting->floorEntered & ENTER_RIGHT) {
        painting_state(PAINTING_ENTERED, painting, paintingGroup, NEAREST_4TH, MARIO_Y, RESET_TIMER);
    }
}

/**
 * Idle update function for wall paintings that use RIPPLE_TRIGGER_CONTINUOUS.
 */
void wall_painting_continuous_idle(struct Painting *painting, struct Painting *paintingGroup[]) {
    // Check for mario triggering a ripple
    if (painting->floorEntered & RIPPLE_LEFT) {
        painting_state(PAINTING_RIPPLE, painting, paintingGroup, MIDDLE_X, MIDDLE_Y, RESET_TIMER);
    } else if (painting->floorEntered & RIPPLE_MIDDLE) {
        painting_state(PAINTING_RIPPLE, painting, paintingGroup, MIDDLE_X, MIDDLE_Y, RESET_TIMER);
    } else if (painting->floorEntered & RIPPLE_RIGHT) {
        painting_state(PAINTING_RIPPLE, painting, paintingGroup, MIDDLE_X, MIDDLE_Y, RESET_TIMER);

    // Check for mario entering
    } else if (painting->floorEntered & ENTER_LEFT) {
        painting_state(PAINTING_ENTERED, painting, paintingGroup, NEAREST_4TH, MARIO_Y, RESET_TIMER);
    } else if (painting->floorEntered & ENTER_MIDDLE) {
        painting_state(PAINTING_ENTERED, painting, paintingGroup, NEAREST_4TH, MARIO_Y, RESET_TIMER);
    } else if (painting->floorEntered & ENTER_RIGHT) {
        painting_state(PAINTING_ENTERED, painting, paintingGroup, NEAREST_4TH, MARIO_Y, RESET_TIMER);
    }
}

/**
 * Rippling update function for wall paintings that use RIPPLE_TRIGGER_CONTINUOUS.
 */
void wall_painting_continuous_rippling(struct Painting *painting, struct Painting *paintingGroup[]) {
    if (painting->floorEntered & ENTER_LEFT) {
        painting_state(PAINTING_ENTERED, painting, paintingGroup, NEAREST_4TH, MARIO_Y, DONT_RESET);
    } else if (painting->floorEntered & ENTER_MIDDLE) {
        painting_state(PAINTING_ENTERED, painting, paintingGroup, NEAREST_4TH, MARIO_Y, DONT_RESET);
    } else if (painting->floorEntered & ENTER_RIGHT) {
        painting_state(PAINTING_ENTERED, painting, paintingGroup, NEAREST_4TH, MARIO_Y, DONT_RESET);
    }
}

/**
 * Idle update function for floor paintings that use RIPPLE_TRIGGER_PROXIMITY.
 *
 * No floor paintings use RIPPLE_TRIGGER_PROXIMITY in the game.
 */
void floor_painting_proximity_idle(struct Painting *painting, struct Painting *paintingGroup[]) {
    // Check for mario triggering a ripple
    if (painting->floorEntered & RIPPLE_LEFT) {
        painting_state(PAINTING_RIPPLE, painting, paintingGroup, MARIO_X, MARIO_Z, RESET_TIMER);
    } else if (painting->floorEntered & RIPPLE_MIDDLE) {
        painting_state(PAINTING_RIPPLE, painting, paintingGroup, MARIO_X, MARIO_Z, RESET_TIMER);
    } else if (painting->floorEntered & RIPPLE_RIGHT) {
        painting_state(PAINTING_RIPPLE, painting, paintingGroup, MARIO_X, MARIO_Z, RESET_TIMER);

    // Only check for mario entering if he jumped below the surface
    } else if (painting->marioWentUnder) {
        if (painting->currFloor & ENTER_LEFT) {
            painting_state(PAINTING_ENTERED, painting, paintingGroup, MARIO_X, MARIO_Z, RESET_TIMER);
        } else if (painting->currFloor & ENTER_MIDDLE) {
            painting_state(PAINTING_ENTERED, painting, paintingGroup, MARIO_X, MARIO_Z, RESET_TIMER);
        } else if (painting->currFloor & ENTER_RIGHT) {
            painting_state(PAINTING_ENTERED, painting, paintingGroup, MARIO_X, MARIO_Z, RESET_TIMER);
        }
    }
}

/**
 * Rippling update function for floor paintings that use RIPPLE_TRIGGER_PROXIMITY.
 *
 * No floor paintings use RIPPLE_TRIGGER_PROXIMITY in the game.
 */
void floor_painting_proximity_rippling(struct Painting *painting, struct Painting *paintingGroup[]) {
    if (painting->marioWentUnder) {
        if (painting->currFloor & ENTER_LEFT) {
            painting_state(PAINTING_ENTERED, painting, paintingGroup, MARIO_X, MARIO_Z, RESET_TIMER);
        } else if (painting->currFloor & ENTER_MIDDLE) {
            painting_state(PAINTING_ENTERED, painting, paintingGroup, MARIO_X, MARIO_Z, RESET_TIMER);
        } else if (painting->currFloor & ENTER_RIGHT) {
            painting_state(PAINTING_ENTERED, painting, paintingGroup, MARIO_X, MARIO_Z, RESET_TIMER);
        }
    }
}

/**
 * Idle update function for floor paintings that use RIPPLE_TRIGGER_CONTINUOUS.
 *
 * Both floor paintings (HMC and CotMC) are hidden behind a door, which hides the ripple's start up.
 * The floor just inside the doorway is RIPPLE_LEFT, so the painting starts rippling as soon as mario
 * enters the room.
 */
void floor_painting_continuous_idle(struct Painting *painting, struct Painting *paintingGroup[]) {
    // Check for mario triggering a ripple
    if (painting->floorEntered & RIPPLE_LEFT) {
        painting_state(PAINTING_RIPPLE, painting, paintingGroup, MIDDLE_X, MIDDLE_Y, RESET_TIMER);
    } else if (painting->floorEntered & RIPPLE_MIDDLE) {
        painting_state(PAINTING_RIPPLE, painting, paintingGroup, MIDDLE_X, MIDDLE_Y, RESET_TIMER);
    } else if (painting->floorEntered & RIPPLE_RIGHT) {
        painting_state(PAINTING_RIPPLE, painting, paintingGroup, MIDDLE_X, MIDDLE_Y, RESET_TIMER);

    // Check for mario entering
    } else if (painting->currFloor & ENTER_LEFT) {
        painting_state(PAINTING_ENTERED, painting, paintingGroup, MARIO_X, MARIO_Z, RESET_TIMER);
    } else if (painting->currFloor & ENTER_MIDDLE) {
        painting_state(PAINTING_ENTERED, painting, paintingGroup, MARIO_X, MARIO_Z, RESET_TIMER);
    } else if (painting->currFloor & ENTER_RIGHT) {
        painting_state(PAINTING_ENTERED, painting, paintingGroup, MARIO_X, MARIO_Z, RESET_TIMER);
    }
}

/**
 * Rippling update function for floor paintings that use RIPPLE_TRIGGER_CONTINUOUS.
 */
void floor_painting_continuous_rippling(struct Painting *painting, struct Painting *paintingGroup[]) {
    if (painting->marioWentUnder) {
        if (painting->currFloor & ENTER_LEFT) {
            painting_state(PAINTING_ENTERED, painting, paintingGroup, MARIO_X, MARIO_Z, DONT_RESET);
        } else if (painting->currFloor & ENTER_MIDDLE) {
            painting_state(PAINTING_ENTERED, painting, paintingGroup, MARIO_X, MARIO_Z, DONT_RESET);
        } else if (painting->currFloor & ENTER_RIGHT) {
            painting_state(PAINTING_ENTERED, painting, paintingGroup, MARIO_X, MARIO_Z, DONT_RESET);
        }
    }
}

/**
 * Check for mario entering one of the special floors associated with the painting.
 */
void painting_update_floors(struct Painting *painting) {
    s16 paintingId = painting->id;
    s8 rippleLeft = 0;
    s8 rippleMiddle = 0;
    s8 rippleRight = 0;
    s8 enterLeft = 0;
    s8 enterMiddle = 0;
    s8 enterRight = 0;

    /* The area in front of every painting in the game (except HMC and CotMC, which   *\
    |* act a little differently) is made up of 3 special floor triangles with special *|
    |* (unique) surface types. This code checks which surface Mario is currently on   *|
    \* and sets a bitfield accordingly.                                               */

    // check if Mario's current floor is one of the special floors
    if (gPaintingMarioFloorType == paintingId * 3 + SURFACE_PAINTING_WOBBLE_A6) {
        rippleLeft = RIPPLE_LEFT;
    }
    if (gPaintingMarioFloorType == paintingId * 3 + SURFACE_PAINTING_WOBBLE_A7) {
        rippleMiddle = RIPPLE_MIDDLE;
    }
    if (gPaintingMarioFloorType == paintingId * 3 + SURFACE_PAINTING_WOBBLE_A8) {
        rippleRight = RIPPLE_RIGHT;
    }
    if (gPaintingMarioFloorType == paintingId * 3 + SURFACE_PAINTING_WARP_D3) {
        enterLeft = ENTER_LEFT;
    }
    if (gPaintingMarioFloorType == paintingId * 3 + SURFACE_PAINTING_WARP_D4) {
        enterMiddle = ENTER_MIDDLE;
    }
    if (gPaintingMarioFloorType == paintingId * 3 + SURFACE_PAINTING_WARP_D5) {
        enterRight = ENTER_RIGHT;
    }

    painting->lastFloor = painting->currFloor;
    // at most 1 of these will be nonzero;
    painting->currFloor = rippleLeft + rippleMiddle + rippleRight + enterLeft + enterMiddle + enterRight;

    // floorEntered is true iff currFloor is true and lastFloor is false
    // (Mario just entered the floor on this frame)
    painting->floorEntered = (painting->lastFloor ^ painting->currFloor) & painting->currFloor;

    painting->marioWasUnder = painting->marioIsUnder;
    // Check if mario has fallen below the painting (used for floor paintings)
    if (gPaintingMarioYPos < painting->posY) {
        painting->marioIsUnder = TRUE;
    } else {
        painting->marioIsUnder = FALSE;
    }

    // mario "went under" if he was not under last frame, but is under now
    painting->marioWentUnder = (painting->marioWasUnder ^ painting->marioIsUnder) & painting->marioIsUnder;
}

/**
 * Update the ripple's timer and magnitude, making it propagate outwards.
 *
 * Automatically changes the painting back to IDLE state (or RIPPLE for continuous paintings) if the
 * ripple's magnitude becomes small enough.
 */
void painting_update_ripple_state(struct Painting *painting) {
    if (gPaintingUpdateCounter != gLastPaintingUpdateCounter) {
        painting->currRippleMag *= painting->rippleDecay;

        //! After ~6.47 days, paintings with RIPPLE_TRIGGER_CONTINUOUS will increment this to
        //! 16777216 (1 << 24), at which point it will freeze (due to floating-point
        //! imprecision?) and the painting will stop rippling. This happens to HMC, DDD, and
        //! CotMC. This happens on Wii VC. Untested on N64 and Wii U VC.
        painting->rippleTimer += 1.0;
    }
    if (painting->rippleTrigger == RIPPLE_TRIGGER_PROXIMITY) {
        // if the painting is barely rippling, make it stop rippling
        if (painting->currRippleMag <= 1.0) {
            painting->state = PAINTING_IDLE;
            gRipplingPainting = NULL;
        }
    } else if (painting->rippleTrigger == RIPPLE_TRIGGER_CONTINUOUS) {

        // if the painting is doing the entry ripple but the ripples are as small as those from the
        // passive ripple, make it do a passive ripple
        // If mario goes below the surface but doesn't warp, the painting will eventually reset.
        if (painting->state == PAINTING_ENTERED && painting->currRippleMag <= painting->passiveRippleMag) {

            painting->state = PAINTING_RIPPLE;
            painting->currRippleMag = painting->passiveRippleMag;
            painting->rippleDecay = painting->passiveRippleDecay;
            painting->currRippleRate = painting->passiveRippleRate;
            painting->dispersionFactor = painting->passiveDispersionFactor;
        }
    }
}

/**
 * @return the ripple function at posX, posY
 * note that posX and posY correspond to a point on the face of the painting, not actual axes
 */
s16 calculate_ripple_at_point(struct Painting *painting, f32 posX, f32 posY) {
    /// Controls the peaks of the ripple.
    f32 rippleMag = painting->currRippleMag;
    /// Controls the ripple's frequency
    f32 rippleRate = painting->currRippleRate;
    /// Controls how fast the ripple spreads
    f32 dispersionFactor = painting->dispersionFactor;
    /// How far the ripple has spread
    f32 rippleTimer = painting->rippleTimer;
    /// x and y ripple origin
    f32 rippleX = painting->rippleX;
    f32 rippleY = painting->rippleY;

    f32 distanceToOrigin;
    f32 rippleDistance;

    posX *= painting->size / PAINTING_SIZE;
    posY *= painting->size / PAINTING_SIZE;
    distanceToOrigin = sqrtf((posX - rippleX) * (posX - rippleX) + (posY - rippleY) * (posY - rippleY));
    // A larger dispersionFactor makes the ripple spread slower
    rippleDistance = distanceToOrigin / dispersionFactor;
    if (rippleTimer < rippleDistance) {
        // if the ripple hasn't reached the point yet, make the point magnitude 0
        return 0;
    } else {
        // use a cosine wave to make the ripple go up and down,
        // scaled by the painting's ripple magnitude
        f32 rippleZ = rippleMag * cosf(rippleRate * (2 * M_PI) * (rippleTimer - rippleDistance));

        // round it to an int and return it
        return round_float(rippleZ);
    }
}

/**
 * If movable, return the ripple function at (posX, posY)
 * else return 0
 */
s16 ripple_if_movable(struct Painting *painting, s16 movable, s16 posX, s16 posY) {
    s16 rippleZ = 0;

    if (movable) {
        rippleZ = calculate_ripple_at_point(painting, posX, posY);
    }
    return rippleZ;
}

/**
 * Allocates and generates a mesh for the rippling painting effect by modifying the passed in `mesh`
 * based on the painting's current ripple state.
 *
 * The `mesh` table describes the location of mesh vertices, whether they move when rippling, and what
 * triangles they belong to.
 *
 * The static mesh passed in is organized into two lists. This function only uses the first list,
 * painting_calculate_triangle_normals below uses the second one.
 *
 * The first list describes the vertices in this format:
 *      numVertices
 *      v0 x, v0 y, movable
 *      ...
 *      vN x, vN y, movable
 *      Where x and y are from 0 to PAINTING_SIZE, movable is 0 or 1.
 *
 * The mesh used in game, seg2_painting_triangle_mesh, is in bin/segment2.c.
 */
void painting_generate_mesh(struct Painting *painting, s16 *mesh, s16 numTris) {
    s16 i;

    gPaintingMesh = mem_pool_alloc(gEffectsMemoryPool, numTris * sizeof(struct PaintingMeshVertex));
    if (gPaintingMesh == NULL) {
    }
    // accesses are off by 1 since the first entry is the number of vertices
    for (i = 0; i < numTris; i++) {
        gPaintingMesh[i].pos[0] = mesh[i * 3 + 1];
        gPaintingMesh[i].pos[1] = mesh[i * 3 + 2];
        // The "z coordinate" of each vertex in the mesh is either 1 or 0. Instead of being an
        // actual coordinate, it just determines whether the vertex moves
        gPaintingMesh[i].pos[2] = ripple_if_movable(painting, mesh[i * 3 + 3],
                                                    gPaintingMesh[i].pos[0], gPaintingMesh[i].pos[1]);
    }
}

/**
 * Calculate the surface normals of each triangle in the generated ripple mesh.
 *
 * The static mesh passed in is organized into two lists. This function uses the second list,
 * painting_generate_mesh above uses the first one.
 *
 * The second list in `mesh` describes the mesh's triangles in this format:
 *      numTris
 *      tri0 v0, tri0 v1, tri0 v2
 *      ...
 *      triN v0, triN v1, triN v2
 *      Where each v0, v1, v2 is an index into the first list in `mesh`.
 *
 * The mesh used in game, seg2_painting_triangle_mesh, is in bin/segment2.c.
 */
void painting_calculate_triangle_normals(s16 *mesh, s16 numVtx, s16 numTris) {
    s16 i;

    gPaintingTriNorms = mem_pool_alloc(gEffectsMemoryPool, numTris * sizeof(Vec3f));
    if (gPaintingTriNorms == NULL) {
    }
    for (i = 0; i < numTris; i++) {
        s16 tri = numVtx * 3 + i * 3 + 2; // Add 2 because of the 2 length entries preceding the list
        s16 v0 = mesh[tri];
        s16 v1 = mesh[tri + 1];
        s16 v2 = mesh[tri + 2];

        f32 x0 = gPaintingMesh[v0].pos[0];
        f32 y0 = gPaintingMesh[v0].pos[1];
        f32 z0 = gPaintingMesh[v0].pos[2];

        f32 x1 = gPaintingMesh[v1].pos[0];
        f32 y1 = gPaintingMesh[v1].pos[1];
        f32 z1 = gPaintingMesh[v1].pos[2];

        f32 x2 = gPaintingMesh[v2].pos[0];
        f32 y2 = gPaintingMesh[v2].pos[1];
        f32 z2 = gPaintingMesh[v2].pos[2];

        // Cross product to find each triangle's normal vector
        gPaintingTriNorms[i][0] = (y1 - y0) * (z2 - z1) - (z1 - z0) * (y2 - y1);
        gPaintingTriNorms[i][1] = (z1 - z0) * (x2 - x1) - (x1 - x0) * (z2 - z1);
        gPaintingTriNorms[i][2] = (x1 - x0) * (y2 - y1) - (y1 - y0) * (x2 - x1);
    }
}

/**
 * Rounds a floating-point component of a normal vector to an s8 by multiplying it by 127 or 128 and
 * rounding away from 0.
 */
s8 normalize_component(f32 comp) {
    s8 rounded;

    if (comp > 0.0) {
        rounded = comp * 127.0 + 0.5; // round up
    } else if (comp < 0.0) {
        rounded = comp * 128.0 - 0.5; // round down
    } else {
        rounded = 0;                  // don't round 0
    }
    return rounded;
}

/**
 * Approximates the painting mesh's vertex normals by averaging the normals of all triangles sharing a
 * vertex. Used for gouraud lighting.
 *
 * After each triangle's surface normal is calculated, the `neighborTris` table describes which triangles
 * each vertex should use when calculating the average normal vector.
 *
 * The table is a list of entries in this format:
 *      numNeighbors, tri0, tri1, ..., triN
 *
 *      Where each 'tri' is an index into gPaintingTriNorms.
 *      Entry i in `neighborTris` corresponds to the vertex at gPaintingMesh[i]
 *
 * The table used in game, seg2_painting_mesh_neighbor_tris, is in bin/segment2.c.
 */
void painting_average_vertex_normals(s16 *neighborTris, s16 numVtx) {
    UNUSED s16 unused;
    s16 tri;
    s16 i;
    s16 j;
    s16 neighbors;
    s16 entry = 0;

    for (i = 0; i < numVtx; i++) {
        f32 nx = 0.0f;
        f32 ny = 0.0f;
        f32 nz = 0.0f;
        f32 nlen;

        // The first number of each entry is the number of adjacent tris
        neighbors = neighborTris[entry];
        for (j = 0; j < neighbors; j++) {
            tri = neighborTris[entry + j + 1];
            nx += gPaintingTriNorms[tri][0];
            ny += gPaintingTriNorms[tri][1];
            nz += gPaintingTriNorms[tri][2];
        }
        // Move to the next vertex's entry
        entry += neighbors + 1;

        // average the surface normals from each neighboring tri
        nx /= neighbors;
        ny /= neighbors;
        nz /= neighbors;
        nlen = sqrtf(nx * nx + ny * ny + nz * nz);

        if (nlen == 0.0) {
            gPaintingMesh[i].norm[0] = 0;
            gPaintingMesh[i].norm[1] = 0;
            gPaintingMesh[i].norm[2] = 0;
        } else {
            gPaintingMesh[i].norm[0] = normalize_component(nx / nlen);
            gPaintingMesh[i].norm[1] = normalize_component(ny / nlen);
            gPaintingMesh[i].norm[2] = normalize_component(nz / nlen);
        }
    }
}

/**
 * Creates a display list that draws the rippling painting, with 'img' mapped to the painting's mesh,
 * using 'textureMap'.
 *
 * If the textureMap doesn't describe the whole mesh, then multiple calls are needed to draw the whole
 * painting.
 */
Gfx *render_painting(u8 *img, s16 tWidth, s16 tHeight, s16 *textureMap, s16 mapVerts, s16 mapTris, u8 alpha) {
    s16 group;
    s16 map;
    s16 triGroup;
    s16 mapping;
    s16 meshVtx;
    s16 tx;
    s16 ty;

    // We can fit 15 (16 / 3) vertices in the RSP's vertex buffer.
    // Group triangles by 5, with one remainder group.
    s16 triGroups = mapTris / 5;
    s16 remGroupTris = mapTris % 5;
    s16 numVtx = mapTris * 3;

    s16 commands = triGroups * 2 + remGroupTris + 7;
    Vtx *verts = alloc_display_list(numVtx * sizeof(Vtx));
    Gfx *dlist = alloc_display_list(commands * sizeof(Gfx));
    Gfx *gfx = dlist;

    if (verts == NULL || dlist == NULL) {
    }

    gLoadBlockTexture(gfx++, tWidth, tHeight, G_IM_FMT_RGBA, img);

    // Draw the groups of 5 first
    for (group = 0; group < triGroups; group++) {

        // The triangle groups are the second part of the texture map.
        // Each group is a list of 15 mappings
        triGroup = mapVerts * 3 + group * 15 + 2;
        for (map = 0; map < 15; map++) {
            // The mapping is just an index into the earlier part of the textureMap
            // Some mappings are repeated, for example, when multiple triangles share a vertex
            mapping = textureMap[triGroup + map];

            // The first entry is the ID of the vertex in the mesh
            meshVtx = textureMap[mapping * 3 + 1];

            // The next two are the texture coordinates for that vertex
            tx = textureMap[mapping * 3 + 2];
            ty = textureMap[mapping * 3 + 3];

            // Map the texture and place it in the verts array
            make_vertex(verts, group * 15 + map, gPaintingMesh[meshVtx].pos[0], gPaintingMesh[meshVtx].pos[1],
                        gPaintingMesh[meshVtx].pos[2], tx, ty, gPaintingMesh[meshVtx].norm[0],
                        gPaintingMesh[meshVtx].norm[1], gPaintingMesh[meshVtx].norm[2], alpha);
        }

        // Load the vertices and draw the 5 triangles
        gSPVertex(gfx++, VIRTUAL_TO_PHYSICAL(verts + group * 15), 15, 0);
        gSPDisplayList(gfx++, dl_paintings_draw_ripples);
    }

    // One group left with < 5 triangles
    triGroup = mapVerts * 3 + triGroups * 15 + 2;
    // Map the texture to the triangles
    for (map = 0; map < remGroupTris * 3; map++) {
        mapping = textureMap[triGroup + map];
        meshVtx = textureMap[mapping * 3 + 1];
        tx = textureMap[mapping * 3 + 2];
        ty = textureMap[mapping * 3 + 3];
        make_vertex(verts, triGroups * 15 + map, gPaintingMesh[meshVtx].pos[0], gPaintingMesh[meshVtx].pos[1],
                    gPaintingMesh[meshVtx].pos[2], tx, ty, gPaintingMesh[meshVtx].norm[0],
                    gPaintingMesh[meshVtx].norm[1], gPaintingMesh[meshVtx].norm[2], alpha);
    }

    // Draw the triangles individually
    gSPVertex(gfx++, VIRTUAL_TO_PHYSICAL(verts + triGroups * 15), remGroupTris * 3, 0);
    for (group = 0; group < remGroupTris; group++) {
        gSP1Triangle(gfx++, group * 3, group * 3 + 1, group * 3 + 2, 0);
    }

    gSPEndDisplayList(gfx);
    return dlist;
}

/**
 * Orient the painting mesh for rendering.
 */
Gfx *painting_model_view_transform(struct Painting *painting) {
    f32 sizeRatio = painting->size / PAINTING_SIZE;
    Mtx *rotX = alloc_display_list(sizeof(Mtx));
    Mtx *rotY = alloc_display_list(sizeof(Mtx));
    Mtx *translate = alloc_display_list(sizeof(Mtx));
    Mtx *scale = alloc_display_list(sizeof(Mtx));
    Gfx *dlist = alloc_display_list(5 * sizeof(Gfx));
    Gfx *gfx = dlist;

    if (rotX == NULL || rotY == NULL || translate == NULL || dlist == NULL) {
    }

    guTranslate(translate, painting->posX, painting->posY, painting->posZ);
    guRotate(rotX, painting->pitch, 1.0f, 0.0f, 0.0f);
    guRotate(rotY, painting->yaw, 0.0f, 1.0f, 0.0f);
    guScale(scale, sizeRatio, sizeRatio, sizeRatio);

    gSPMatrix(gfx++, translate, G_MTX_MODELVIEW | G_MTX_MUL | G_MTX_PUSH);
    gSPMatrix(gfx++, rotX,      G_MTX_MODELVIEW | G_MTX_MUL | G_MTX_NOPUSH);
    gSPMatrix(gfx++, rotY,      G_MTX_MODELVIEW | G_MTX_MUL | G_MTX_NOPUSH);
    gSPMatrix(gfx++, scale,     G_MTX_MODELVIEW | G_MTX_MUL | G_MTX_NOPUSH);
    gSPEndDisplayList(gfx);

    return dlist;
}

/**
 * Ripple a painting that has 1 or more images that need to be mapped
 */
Gfx *painting_ripple_image(struct Painting *painting) {
    s16 meshVerts;
    s16 meshTris;
    s16 i;
    s16 *textureMap;
    s16 imageCount = painting->imageCount;
    s16 tWidth = painting->textureWidth;
    s16 tHeight = painting->textureHeight;
    s16 **textureMaps = segmented_to_virtual(painting->textureMaps);
    u8 **textures = segmented_to_virtual(painting->textureArray);
    Gfx *dlist = alloc_display_list((imageCount + 6) * sizeof(Gfx));
    Gfx *gfx = dlist;

    if (dlist == NULL) {
        return dlist;
    }

    gSPDisplayList(gfx++, painting_model_view_transform(painting));
    gSPDisplayList(gfx++, dl_paintings_rippling_begin);
    gSPDisplayList(gfx++, painting->rippleDisplayList);

    // Map each image to the mesh's vertices
    for (i = 0; i < imageCount; i++) {
        textureMap = segmented_to_virtual(textureMaps[i]);
        meshVerts = textureMap[0];
        meshTris = textureMap[meshVerts * 3 + 1];
        gSPDisplayList(gfx++, render_painting(textures[i], tWidth, tHeight, textureMap, meshVerts, meshTris, painting->alpha));
    }

    // Update the ripple, may automatically reset the painting's state.
    painting_update_ripple_state(painting);

    gSPPopMatrix(gfx++, G_MTX_MODELVIEW);
    gSPDisplayList(gfx++, dl_paintings_rippling_end);
    gSPEndDisplayList(gfx);
    return dlist;
}

/**
 * Ripple a painting that has 1 "environment map" texture.
 */
Gfx *painting_ripple_env_mapped(struct Painting *painting) {
    s16 meshVerts;
    s16 meshTris;
    s16 *textureMap;
    s16 tWidth = painting->textureWidth;
    s16 tHeight = painting->textureHeight;
    s16 **textureMaps = segmented_to_virtual(painting->textureMaps);
    u8 **tArray = segmented_to_virtual(painting->textureArray);
    Gfx *dlist = alloc_display_list(7 * sizeof(Gfx));
    Gfx *gfx = dlist;

    if (dlist == NULL) {
        return dlist;
    }

    gSPDisplayList(gfx++, painting_model_view_transform(painting));
    gSPDisplayList(gfx++, dl_paintings_env_mapped_begin);
    gSPDisplayList(gfx++, painting->rippleDisplayList);

    // Map the image to the mesh's vertices
    textureMap = segmented_to_virtual(textureMaps[0]);
    meshVerts = textureMap[0];
    meshTris = textureMap[meshVerts * 3 + 1];
    gSPDisplayList(gfx++, render_painting(tArray[0], tWidth, tHeight, textureMap, meshVerts, meshTris, painting->alpha));

    // Update the ripple, may automatically reset the painting's state.
    painting_update_ripple_state(painting);

    gSPPopMatrix(gfx++, G_MTX_MODELVIEW);
    gSPDisplayList(gfx++, dl_paintings_env_mapped_end);
    gSPEndDisplayList(gfx);
    return dlist;
}

/**
 * Generates a mesh, calculates vertex normals for lighting, and renders a rippling painting.
 * The mesh and vertex normals are regenerated and freed every frame.
 */
Gfx *display_painting_rippling(struct Painting *painting) {
    s16 *mesh = segmented_to_virtual(seg2_painting_triangle_mesh);
    s16 *neighborTris = segmented_to_virtual(seg2_painting_mesh_neighbor_tris);
    s16 numVtx = mesh[0];
    s16 numTris = mesh[numVtx * 3 + 1];
    Gfx *dlist;

    // Generate the mesh and its lighting data
    painting_generate_mesh(painting, mesh, numVtx);
    painting_calculate_triangle_normals(mesh, numVtx, numTris);
    painting_average_vertex_normals(neighborTris, numVtx);

    // Map the painting's texture depending on the painting's texture type.
    switch (painting->textureType) {
        case PAINTING_IMAGE:
            dlist = painting_ripple_image(painting);
            break;
        case PAINTING_ENV_MAP:
            dlist = painting_ripple_env_mapped(painting);
            break;
    }

    // The mesh data is freed every frame.
    mem_pool_free(gEffectsMemoryPool, gPaintingMesh);
    mem_pool_free(gEffectsMemoryPool, gPaintingTriNorms);
    return dlist;
}

/**
 * Render a normal painting.
 */
Gfx *display_painting_not_rippling(struct Painting *painting) {
    Gfx *dlist = alloc_display_list(4 * sizeof(Gfx));
    Gfx *gfx = dlist;

    if (dlist == NULL) {
        return dlist;
    }
    gSPDisplayList(gfx++, painting_model_view_transform(painting));
    gSPDisplayList(gfx++, painting->normalDisplayList);
    gSPPopMatrix(gfx++, G_MTX_MODELVIEW);
    gSPEndDisplayList(gfx);
    return dlist;
}

/**
 * Clear mario-related state and clear gRipplingPainting.
 */
void reset_painting(struct Painting *painting) {
    painting->lastFloor = 0;
    painting->currFloor = 0;
    painting->floorEntered = 0;
    painting->marioWasUnder = 0;
    painting->marioIsUnder = 0;
    painting->marioWentUnder = 0;

    gRipplingPainting = NULL;
}

/**
 * Controls the x coordinate of the DDD painting.
 *
 * Before mario gets the "Board Bowser's Sub" star in DDD, the painting spawns at frontPos.
 *
 * If mario just got the star, the painting's x coordinate moves to backPos at a rate of `speed` units.
 *
 * When the painting reaches backPos, a save flag is set so that the painting will spawn at backPos
 * whenever it loads.
 *
 * This function also sets gDddPaintingStatus, which controls the warp:
 *  0 (0b00): set x coordinate to frontPos
 *  2 (0b10): set x coordinate to backPos
 *  3 (0b11): same as 2. Bit 0 is ignored
 */
void move_ddd_painting(struct Painting *painting, f32 frontPos, f32 backPos, f32 speed) {
    // Obtain the DDD star flags
    u32 dddFlags = save_file_get_star_flags(gCurrSaveFileNum - 1, COURSE_DDD - 1);
    // Get the other save file flags
    u32 saveFileFlags = save_file_get_flags();
    // Find out whether Board Bowser's Sub was collected
    u32 bowsersSubBeaten = dddFlags & BOARD_BOWSERS_SUB;
    // Check whether DDD has already moved back
    u32 dddBack = saveFileFlags & SAVE_FLAG_DDD_MOVED_BACK;

    if (!bowsersSubBeaten && !dddBack) {
        // If we haven't collected the star or moved the painting, put the painting at the front
        painting->posX = frontPos;
        gDddPaintingStatus = 0;
    } else if (bowsersSubBeaten && !dddBack) {
        // If we've collected the star but not moved the painting back,
        // Each frame, move the painting by a certain speed towards the back area.
        painting->posX += speed;
        gDddPaintingStatus = BOWSERS_SUB_BEATEN;
        if (painting->posX >= backPos) {
            painting->posX = backPos;
            // Tell the save file that we've moved DDD back.
            save_file_set_flags(SAVE_FLAG_DDD_MOVED_BACK);
        }
    } else if (bowsersSubBeaten && dddBack) {
        // If the painting has already moved back, place it in the back position.
        painting->posX = backPos;
        gDddPaintingStatus = BOWSERS_SUB_BEATEN | DDD_BACK;
    }
}
/**
 * Set the painting's node's layer based on its alpha
 */
void set_painting_layer(struct GraphNodeGenerated *gen, struct Painting *painting) {
    switch (painting->alpha) {
        case 0xFF: // Opaque
            gen->fnNode.node.flags = (gen->fnNode.node.flags & 0xFF) | (LAYER_OPAQUE << 8);
            break;
        default:
            gen->fnNode.node.flags = (gen->fnNode.node.flags & 0xFF) | (LAYER_TRANSPARENT << 8);
            break;
    }
}

/**
 * Display either a normal painting or a rippling one depending on the painting's ripple status
 */
Gfx *display_painting(struct Painting *painting) {
    switch (painting->state) {
        case PAINTING_IDLE:
            return display_painting_not_rippling(painting);
            break;
        default:
            return display_painting_rippling(painting);
            break;
    }
}

/**
 * Update function for wall paintings.
 * Calls a different update function depending on the painting's ripple trigger and current state.
 */
void wall_painting_update(struct Painting *painting, struct Painting *paintingGroup[]) {
    if (painting->rippleTrigger == RIPPLE_TRIGGER_PROXIMITY) {
        switch (painting->state) {
            case PAINTING_IDLE:
                wall_painting_proximity_idle(painting, paintingGroup);
                break;
            case PAINTING_RIPPLE:
                wall_painting_proximity_rippling(painting, paintingGroup);
                break;
        }
    } else if (painting->rippleTrigger == RIPPLE_TRIGGER_CONTINUOUS) {
        switch (painting->state) {
            case PAINTING_IDLE:
                wall_painting_continuous_idle(painting, paintingGroup);
                break;
            case PAINTING_RIPPLE:
                wall_painting_continuous_rippling(painting, paintingGroup);
                break;
        }
    }
}

/**
 * Update function for floor paintings (HMC and CotMC)
 * Calls a different update function depending on the painting's ripple trigger and current state.
 *
 * No floor paintings use RIPPLE_TRIGGER_PROXIMITY in the game.
 */
void floor_painting_update(struct Painting *painting, struct Painting *paintingGroup[]) {
    if (painting->rippleTrigger == RIPPLE_TRIGGER_PROXIMITY) {
        switch (painting->state) {
            case PAINTING_IDLE:
                floor_painting_proximity_idle(painting, paintingGroup);
                break;
            case PAINTING_RIPPLE:
                floor_painting_proximity_rippling(painting, paintingGroup);
                break;
        }
    } else if (painting->rippleTrigger == RIPPLE_TRIGGER_CONTINUOUS) {
        switch (painting->state) {
            case PAINTING_IDLE:
                floor_painting_continuous_idle(painting, paintingGroup);
                break;
            case PAINTING_RIPPLE:
                floor_painting_continuous_rippling(painting, paintingGroup);
                break;
        }
    }
}

/**
 * Render and update the painting whose id and group matches the values in the GraphNode's parameter.
 * Use PAINTING_ID(id, group) to set the right parameter in a level's geo layout.
 */
Gfx *geo_painting_draw(s32 callContext, struct GraphNode *node, UNUSED void *context) {
    struct GraphNodeGenerated *gen = (struct GraphNodeGenerated *) node;
    s32 group = (gen->parameter >> 8) & 0xFF;
    s32 id = gen->parameter & 0xFF;
    Gfx *paintingDlist = NULL;
    struct Painting **paintingGroup = sPaintingGroups[group];
    struct Painting *painting = segmented_to_virtual(paintingGroup[id]);

    if (callContext != GEO_CONTEXT_RENDER) {
        reset_painting(painting);
    } else if (callContext == GEO_CONTEXT_RENDER) {

        // Update the ddd painting before drawing
        if (group == 1 && id == PAINTING_ID_DDD) {
            move_ddd_painting(painting, 3456.0f, 5529.6f, 20.0f);
        }

        // Determine if the painting is transparent
        set_painting_layer(gen, painting);

        // Draw before updating
        paintingDlist = display_painting(painting);

        // Update the painting
        painting_update_floors(painting);
        switch ((s16) painting->pitch) {
            // only paintings with 0 pitch are treated as walls
            case 0:
                wall_painting_update(painting, paintingGroup);
                break;
            default:
                floor_painting_update(painting, paintingGroup);
                break;
        }
    }
    return paintingDlist;
}

/**
 * Update the painting system's local copy of mario's current floor and position.
 */
Gfx *geo_painting_update(s32 callContext, UNUSED struct GraphNode *node, UNUSED f32 c[4][4]) {
    struct Surface *surface;

    // Reset the update counter
    if (callContext != GEO_CONTEXT_RENDER) {
        gLastPaintingUpdateCounter = gAreaUpdateCounter - 1;
        gPaintingUpdateCounter = gAreaUpdateCounter;
    } else {
        gLastPaintingUpdateCounter = gPaintingUpdateCounter;
        gPaintingUpdateCounter = gAreaUpdateCounter;

        // Store mario's floor and position
        find_floor(gMarioObject->oPosX, gMarioObject->oPosY, gMarioObject->oPosZ, &surface);
        gPaintingMarioFloorType = surface->type;
        gPaintingMarioXPos = gMarioObject->oPosX;
        gPaintingMarioYPos = gMarioObject->oPosY;
        gPaintingMarioZPos = gMarioObject->oPosZ;
    }
    return NULL;
}
