-- name: Character Select
-- description:\\#ffff33\\-- Character Select Coop v1.15.1 --\n\n\\#dcdcdc\\A Library / API made to make adding and using Custom Characters as simple as possible!\nUse\\#ffff33\\ /char-select\\#dcdcdc\\ to get started!\n\nCreated by:\\#008800\\ Squishy6094\n\n\\#AAAAFF\\Updates can be found on\nCharacter Select's Github:\n\\#6666FF\\Squishy6094/character-select-coop
-- pausable: false
-- category: cs

if incompatibleClient then return 0 end

---@param hookEventType LuaHookedEventType
local function create_hook_wrapper(hookEventType)
    local callbacks = {}

    hook_event(hookEventType, function(...)
        for _, func in pairs(callbacks) do
            func(...)
        end
    end)

    return function(func)
        table.insert(callbacks, func)
    end
end

cs_hook_mario_update = create_hook_wrapper(HOOK_MARIO_UPDATE)

-- localize functions to improve performance - main.lua
local mod_storage_load,tonumber,mod_storage_save,djui_popup_create,tostring,djui_chat_message_create,is_game_paused,obj_get_first_with_behavior_id,djui_hud_is_pause_menu_created,camera_freeze,hud_hide,vec3f_copy,set_mario_action,set_character_animation,camera_unfreeze,hud_show,type,get_id_from_behavior,obj_has_behavior_id,network_local_index_from_global,obj_has_model_extended,obj_set_model_extended,nearest_player_to_object,math_random,djui_hud_set_resolution,djui_hud_set_font,djui_hud_get_screen_width,maxf,djui_hud_set_color,djui_hud_render_rect,djui_hud_measure_text,djui_hud_print_text,min,math_min,math_ceil,math_abs,math_sin,minf,djui_hud_set_rotation,table_insert,djui_hud_print_text_interpolated,math_max,play_sound,play_character_sound,string_lower = mod_storage_load,tonumber,mod_storage_save,djui_popup_create,tostring,djui_chat_message_create,is_game_paused,obj_get_first_with_behavior_id,djui_hud_is_pause_menu_created,camera_freeze,hud_hide,vec3f_copy,set_mario_action,set_character_animation,camera_unfreeze,hud_show,type,get_id_from_behavior,obj_has_behavior_id,network_local_index_from_global,obj_has_model_extended,obj_set_model_extended,nearest_player_to_object,math.random,djui_hud_set_resolution,djui_hud_set_font,djui_hud_get_screen_width,maxf,djui_hud_set_color,djui_hud_render_rect,djui_hud_measure_text,djui_hud_print_text,min,math.min,math.ceil,math.abs,math.sin,minf,djui_hud_set_rotation,table.insert,djui_hud_print_text_interpolated,math.max,play_sound,play_character_sound,string.lower

menu = false
menuAndTransition = false
options = false
local credits = false
local creditsAndTransition = false
currChar = 1
local prevChar = 1
currCharRender = 1
currCategory = 1
local currOption = 1
local creditScroll = 0
local prevCreditScroll = creditScroll
local creditScrollRange = 0

local menuCrossFade = 7
local menuCrossFadeCap = menuCrossFade
local menuCrossFadeMath = 255 / menuCrossFade

local creditsCrossFade = 7
local creditsCrossFadeCap = creditsCrossFade
local creditsCrossFadeMath = 255 / creditsCrossFade

local TYPE_FUNCTION = "function"
local TYPE_BOOLEAN = "boolean"
local TYPE_STRING = "string"
local TYPE_INTEGER = "number"
local TYPE_TABLE = "table"

local TEX_HEADER = get_texture_info("char-select-text")
local TEX_OVERRIDE_HEADER = nil

---@param texture TextureInfo?
function header_set_texture(texture)
    TEX_OVERRIDE_HEADER = texture
end

CS_ANIM_MENU = CHAR_ANIM_MAX + 1

local TEXT_PREF_LOAD_NAME = "Default"
local TEXT_PREF_LOAD_ALT = 1

--[[
    Note: Do NOT add characters via the characterTable below,
    We highly recommend you create your own mod and use the
    API to add characters, this ensures your pack is easy
    to use for anyone and low on file space!
]]

characterTable = {
    [1] = {
        saveName = "Default",
        category = "All_CoopDX",
        ogNum = 1,
        currAlt = 1,
        hasMoveset = false,
        locked = false,
        [1] = {
            name = "Mario",
            description = {
                "The iconic Italian plumber himself!",
                "He's quite confident and brave,",
                "always prepared to jump into action",
                "to save the Mushroom Kingdom!",
            },
            credit = "Nintendo / Coop Team",
            color = { r = 255, g = 50,  b = 50  },
            model = E_MODEL_MARIO,
            ogModel = E_MODEL_MARIO,
            baseChar = CT_MARIO,
            lifeIcon = gTextures.mario_head,
            starIcon = gTextures.star,
            camScale = 1.0,
        },
        [2] = {
            name = "Luigi",
            description = {
                "The other iconic Italian plumber!",
                "He's a bit shy and scares easily,",
                "but he's willing to follow his brother",
                "Mario through any battle that may",
                "come their way!",
            },
            credit = "Nintendo / Coop Team",
            color = { r = 50,  g = 255, b = 50  },
            model = E_MODEL_LUIGI,
            ogModel = E_MODEL_LUIGI,
            baseChar = CT_LUIGI,
            lifeIcon = gTextures.luigi_head,
            starIcon = gTextures.star,
            camScale = 1.0,
            healthTexture = {
                label = {
                    left = get_texture_info("char-select-luigi-meter-left"),
                    right = get_texture_info("char-select-luigi-meter-right"),
                }
            }
        },
        [3] = {
            name = "Toad",
            description = {
                "Princess Peach's little attendant!",
                "He's an energetic little mushroom",
                "that's never afraid to follow",
                "Mario and Luigi on their adventures!",
            },
            credit = "Nintendo / Coop Team",
            color = { r = 50,  g = 50,  b = 255 },
            model = E_MODEL_TOAD_PLAYER,
            ogModel = E_MODEL_TOAD_PLAYER,
            baseChar = CT_TOAD,
            lifeIcon = gTextures.toad_head,
            starIcon = gTextures.star,
            camScale = 0.8,
            healthTexture = {
                label = {
                    left = get_texture_info("char-select-toad-meter-left"),
                    right = get_texture_info("char-select-toad-meter-right"),
                }
            }
        },
        [4] = {
            name = "Waluigi",
            description = {
                "The mischievous rival of Luigi!",
                "He's a narcissistic competitor",
                "that takes great taste in others",
                "getting pummeled from his success!",
            },
            credit = "Nintendo / Coop Team",
            color = { r = 130, g = 25,  b = 130 },
            model = E_MODEL_WALUIGI,
            ogModel = E_MODEL_WALUIGI,
            baseChar = CT_WALUIGI,
            lifeIcon = gTextures.waluigi_head,
            starIcon = gTextures.star,
            camScale = 1.1,
            healthTexture = {
                label = {
                    left = get_texture_info("char-select-waluigi-meter-left"),
                    right = get_texture_info("char-select-waluigi-meter-right"),
                }
            }
        },
        [5] = {
            name = "Wario",
            description = {
                "The mischievous rival of Mario!",
                "He's a greed-filled treasure hunter",
                "obsessed with money and gold coins.",
                "He's always ready for a brawl if his",
                "money is on the line!",
            },
            credit = "Nintendo / Coop Team",
            color = { r = 255, g = 255, b = 50  },
            model = E_MODEL_WARIO,
            ogModel = E_MODEL_WARIO,
            baseChar = CT_WARIO,
            lifeIcon = gTextures.wario_head,
            starIcon = gTextures.star,
            camScale = 1.0,
            healthTexture = {
                label = {
                    left = get_texture_info("char-select-wario-meter-left"),
                    right = get_texture_info("char-select-wario-meter-right"),
                }
            }
        },
    },
}

characterCategories = {
    "All",
    "CoopDX",
    "Locked",
}

local characterTableRender = {}

local function update_character_render_table()
    local ogNum = currChar
    --currChar = 1
    currCharRender = 1
    local category = characterCategories[currCategory]
    if category == nil then return false end
    characterTableRender = {}
    for i = 1, #characterTable do
        local charCategories = string_split(characterTable[i].category, "_")
        if not characterTable[i].locked then
            for c = 1, #charCategories do
                if category == charCategories[c] then
                    table_insert(characterTableRender, characterTable[i])
                    if ogNum == i then
                        currChar = ogNum
                        currCharRender = #characterTableRender
                    end
                end
            end
        end
    end
    if #characterTableRender > 1 then
        currChar = (characterTableRender[currCharRender] and characterTableRender[currCharRender].ogNum or characterTableRender[1].ogNum)
        return true
    else
        return false
    end
end

function force_set_character(charNum, charAlt)
    if not charAlt then charAlt = 1 end
    currCategory = 1
    currChar = charNum
    characterTable[currChar].currAlt = charAlt
    currCharRender = charNum
    charBeingSet = true
    update_character_render_table()
end

characterCaps = {}
characterCelebrationStar = {}
characterColorPresets = {}
characterAnims = {}
characterMovesets = {[1] = {}}
characterUnlock = {}

tableRefNum = 0
local function make_table_ref_num()
    tableRefNum = tableRefNum + 1
    return tableRefNum
end

optionTableRef = {
    -- Menu
    openInputs = make_table_ref_num(),
    notification = make_table_ref_num(),
    menuColor = make_table_ref_num(),
    anims = make_table_ref_num(),
    inputLatency = make_table_ref_num(),
    -- Characters
    localMoveset = make_table_ref_num(),
    localModels = make_table_ref_num(),
    localVoices = make_table_ref_num(),
    -- CS
    credits = make_table_ref_num(),
    debugInfo = make_table_ref_num(),
    resetSaveData = make_table_ref_num(),
    -- Moderation
    --restrictPalettes = make_table_ref_num(),
    restrictMovesets = make_table_ref_num(),
}

optionTable = {
    [optionTableRef.openInputs] = {
        name = "Menu Bind",
        toggle = tonumber(mod_storage_load("MenuInput")),
        toggleSaveName = "MenuInput",
        toggleDefault = 1,
        toggleMax = 2,
        toggleNames = {"None", "Z (Pause Menu)", ommActive and "D-pad Down + R" or "D-pad Down"},
        description = {"Sets a Bind to Open the Menu", "rather than using the command."}
    },
    [optionTableRef.notification] = {
        name = "Notifications",
        toggle = tonumber(mod_storage_load("notifs")),
        toggleSaveName = "notifs",
        toggleDefault = 1,
        toggleMax = 2,
        toggleNames = {"Off", "On", "Pop-ups Only"},
        description = {"Toggles whether Pop-ups and", "Chat Messages display"}
    },
    [optionTableRef.menuColor] = {
        name = "Menu Color",
        toggle = tonumber(mod_storage_load("MenuColor")),
        toggleSaveName = "MenuColor",
        toggleDefault = 0,
        toggleMax = 10,
        toggleNames = {"Auto", "Saved", "Red", "Orange", "Yellow", "Green", "Blue", "Pink", "Purple", "White", "Black"},
        description = {"Toggles the Menu Color"}
    },
    [optionTableRef.anims] = {
        name = "Menu Anims",
        toggle = tonumber(mod_storage_load("Anims")),
        toggleSaveName = "Anims",
        toggleDefault = 1,
        toggleMax = 1,
        toggleNames = {"Off", "On"},
        description = {"Toggles Animations In-Menu,", "Turning these off may", "Save Performance"}
    },
    [optionTableRef.inputLatency] = {
        name = "Scroll Speed",
        toggle = tonumber(mod_storage_load("Latency")),
        toggleSaveName = "Latency",
        toggleDefault = 1,
        toggleMax = 2,
        toggleNames = {"Slow", "Normal", "Fast"},
        description = {"Sets how fast you scroll", "throughout the Menu"}
    },
    [optionTableRef.localMoveset] = {
        name = "Character Moveset",
        toggle = tonumber(mod_storage_load("localMoveset")),
        toggleSaveName = "localMoveset",
        toggleDefault = 1,
        toggleMax = 1,
        description = {"Toggles if Custom Movesets", "are active on compatible", "characters"},
        lock = function ()
            if gGlobalSyncTable.charSelectRestrictMovesets ~= 0 then
                return "Forced Off"
            end
        end,
    },
    [optionTableRef.localModels] = {
        name = "Character Models",
        toggle = tonumber(mod_storage_load("localModels")),
        toggleSaveName = "localModels",
        toggleDefault = 1,
        toggleMax = 1,
        description = {"Toggles if Custom Models display", "on your client, practically", "disables Character Select if", "Toggled Off"}
    },
    [optionTableRef.localVoices] = {
        name = "Character Voices",
        toggle = tonumber(mod_storage_load("localVoices")),
        toggleSaveName = "localVoices",
        toggleDefault = 1,
        toggleMax = 1,
        description = {"Toggle if Custom Voicelines play", "for Characters who support it"}
    },
    [optionTableRef.credits] = {
        name = "Credits",
        toggle = 0,
        toggleDefault = 0,
        toggleMax = 1,
        toggleNames = {"", ""},
        description = {"Thank you for choosing", "Character Select!"}
    },
    [optionTableRef.debugInfo] = {
        name = "Developer Mode",
        toggle = tonumber(mod_storage_load("debuginfo")),
        toggleSaveName = "debuginfo",
        toggleDefault = 0,
        toggleMax = 1,
        description = {"Replaces the Character", "Description with Character", "Debugging Information,", "And shows hidden console logs."}
    },
    [optionTableRef.resetSaveData] = {
        name = "Reset Save Data",
        toggle = 0,
        toggleDefault = 0,
        toggleMax = 1,
        toggleNames = {"", ""},
        description = {"Resets Character Select's", "Save Data"}
    },
    [optionTableRef.restrictMovesets] = {
        name = "Restrict Movesets",
        toggle = 0,
        toggleDefault = 1,
        toggleMax = 1,
        description = {"Restricts turning on", "movesets", "(Host Only)"},
        lock = function ()
            if gGlobalSyncTable.charSelectRestrictMovesets < 2 then
                if not network_is_server() then
                    return "Host Only"
                end
            else
                return "API Only"
            end
        end,
    },
}

---@description A function that gets an option's status from the Character Select Options Menu
---@param tableNum integer The table position of the option
---@return number?
function get_options_status(tableNum)
    if type(tableNum) ~= TYPE_INTEGER then return nil end
    return optionTable[tableNum].toggle
end

function dev_mode_log_to_console(message, level)
    if get_options_status(optionTableRef.debugInfo) == 0 then return end
    log_to_console(message, level and level or CONSOLE_MESSAGE_WARNING)
end

creditTable = {
    {
        packName = "Character Select Coop",
        {creditTo = "Squishy6094",     creditFor = "Creator"},
        {creditTo = "AngelicMiracles", creditFor = "Concepts / CoopDX"},
        {creditTo = "AgentX",          creditFor = "Main Contributer / CoopDX"},
        {creditTo = "xLuigiGamerx",    creditFor = "Main Contributer"},
        {creditTo = "Wibblus",         creditFor = "Contributer"},
        {creditTo = "OneCalledRPG",    creditFor = "Contributer"},
        {creditTo = "SuperKirbyLover", creditFor = "Custom Health Meters"},
    }
}

local defaultOptionCount = #optionTable

local latencyValueTable = {12, 6, 3}

local menuColorTable = {
    { r = 255, g = 50,  b = 50  },
    { r = 255, g = 100, b = 50  },
    { r = 255, g = 255, b = 50  },
    { r = 50,  g = 255, b = 50  },
    { r = 50,  g = 50,  b = 255 },
    { r = 251, g = 148, b = 220 },
    { r = 130, g = 25,  b = 130 },
    { r = 255, g = 255, b = 255 },
    { r = 50,  g = 50,  b = 50  }
}

---@param m MarioState
local function nullify_inputs(m)
    local c = m.controller
    _G.charSelect.controller = {
        buttonDown = c.buttonDown,
        buttonPressed = c.buttonPressed & ~_G.charSelect.controller.buttonDown,
        extStickX = c.extStickX,
        extStickY = c.extStickY,
        rawStickX = c.rawStickX,
        rawStickY = c.rawStickY,
        stickMag = c.stickMag,
        stickX = c.stickX,
        stickY = c.stickY
    }
    c.buttonDown = 0
    c.buttonPressed = 0
    c.extStickX = 0
    c.extStickY = 0
    c.rawStickX = 0
    c.rawStickY = 0
    c.stickMag = 0
    c.stickX = 0
    c.stickY = 0
end

local prefCharColor = {r = 255, g = 50, b = 50}

local function load_preferred_char()
    local savedChar = mod_storage_load("PrefChar")
    local savedAlt = tonumber(mod_storage_load("PrefAlt"))
    local savedPalette = tonumber(mod_storage_load("PrefPalette"))
    if savedChar == nil or savedChar == "" then
        mod_storage_save("PrefChar", "Default")
        savedChar = "Default"
    end
    if savedAlt == nil then
        mod_storage_save("PrefAlt", "1")
        savedAlt = 1
    end
    if savedPalette == nil then
        local paletteSave = savedChar ~= "Default" and 1 or 0
        mod_storage_save("PrefAlt", tostring(paletteSave))
        savedPalette = paletteSave
    end
    if savedChar ~= "Default" and optionTable[optionTableRef.localModels].toggle == 1 then
        for i = 2, #characterTable do
            local char = characterTable[i]
            if char.saveName == savedChar and not char.locked then
                currChar = i
                currCharRender = i
                if savedAlt > 0 and savedAlt <= #char then
                    char.currAlt = savedAlt
                end
                savedAlt = math.clamp(savedAlt, 1, #characterTable[currChar])
                local model = characterTable[currChar][savedAlt].model
                if characterColorPresets[model] ~= nil then
                    gCSPlayers[0].presetPalette = savedPalette
                    characterColorPresets[model].currPalette = savedPalette
                end
                if optionTable[optionTableRef.notification].toggle > 0 then
                    djui_popup_create('Character Select:\nYour Preferred Character\n"' .. string_underscore_to_space(char[char.currAlt].name) .. '"\nwas applied successfully!', 4)
                end
                break
            end
        end
    end
    
    characterTable[1].currAlt = gNetworkPlayers[0].modelIndex + 1

    local savedCharColors = mod_storage_load("PrefCharColor")
    if savedCharColors ~= nil and savedCharColors ~= "" then
        local savedCharColorsTable = string_split(savedCharColors, "_")
        prefCharColor = {
            r = tonumber(savedCharColorsTable[1]),
            g = tonumber(savedCharColorsTable[2]),
            b = tonumber(savedCharColorsTable[3])
        }
    else
        mod_storage_save("PrefCharColor", "255_50_50")
    end

    if #characterTable == 1 then
        if optionTable[optionTableRef.notification].toggle > 0 then
            djui_popup_create("Character Select:\nNo Characters were Found", 2)
        end
    end
    TEXT_PREF_LOAD_NAME = savedChar
    TEXT_PREF_LOAD_ALT = savedAlt
    update_character_render_table()
end

local function mod_storage_save_pref_char(charTable)
    mod_storage_save("PrefChar", charTable.saveName)
    mod_storage_save("PrefAlt", tostring(charTable.currAlt))
    mod_storage_save("PrefCharColor", tostring(charTable[charTable.currAlt].color.r) .. "_" .. tostring(charTable[charTable.currAlt].color.g) .. "_" .. tostring(charTable[charTable.currAlt].color.b))
    mod_storage_save("PrefPalette", tostring(gCSPlayers[0].presetPalette))
    TEXT_PREF_LOAD_NAME = charTable.saveName
    TEXT_PREF_LOAD_ALT = charTable.currAlt
    prefCharColor = charTable[charTable.currAlt].color
end

function failsafe_options()
    for i = 1, #optionTable do
        if optionTable[i].toggle == nil or optionTable[i].toggle == "" then
            local load = optionTable[i].toggleSaveName and mod_storage_load(optionTable[i].toggleSaveName) or nil
            if load == "" then
                load = nil
            end
            optionTable[i].toggle = load and tonumber(load) or optionTable[i].toggleDefault
        end
        if optionTable[i].toggleNames == nil then
            optionTable[i].toggleNames = {"Off", "On"}
        end
    end
    if optionTable[optionTableRef.openInputs].toggle == 2 and ommActive then
        djui_popup_create('Character Select:\nYour Open bind has changed to:\nD-pad Down + R\nDue to OMM Rebirth being active!', 4)
    end
end

local promptedAreYouSure = false

local function reset_options(wasChatTriggered)
    if not promptedAreYouSure then
        djui_chat_message_create("\\#ffdcdc\\Are you sure you want to reset your Save Data for Character Select, including your Preferred Character\nand Settings?\n" .. (wasChatTriggered and "Type \\#ff3333\\/char-select reset\\#ffdcdc\\ to confirm." or "Press the \\#ff3333\\" .. optionTable[optionTableRef.resetSaveData].name .. "\\#ffdcdc\\ Option again to confirm." ))
        promptedAreYouSure = true
    else
        djui_chat_message_create("\\#ff3333\\Character Select Save Data Reset!")
        djui_chat_message_create("Note: If your issue has not been resolved, you may need to manually delete your save data via the directory below:\n\\#dcdcFF\\%appdata%/sm64coopdx/sav/character-select-coop.sav")
        for i = 1, #optionTable do
            optionTable[i].toggle = optionTable[i].toggleDefault
            if optionTable[i].toggleSaveName ~= nil then
                mod_storage_save(optionTable[i].toggleSaveName, tostring(optionTable[i].toggle))
            end
            if optionTable[i].toggleNames == nil then
                optionTable[i].toggleNames = { "Off", "On" }
            end
        end
        currChar = 1
        for i = 1, #characterTable do
            characterTable[i].currAlt = 1
        end
        mod_storage_save_pref_char(characterTable[1])
        promptedAreYouSure = false
    end
end

local function boot_note()
    if #characterTable > 1 then
        djui_chat_message_create("Character Select has " .. (#characterTable - 1) .. " character" .. (#characterTable > 2 and "s" or "") .." available!\nYou can use \\#ffff33\\/char-select \\#ffffff\\to open the menu!")
        if #characterTable > 32 and network_is_server() then
            djui_chat_message_create("\\#FFAAAA\\Warning: Having more than 32 Characters\nmay be unstable, For a better experience please\ndisable a few packs!")
        end
    else
        djui_chat_message_create("Character Select is active!\nYou can use \\#ffff33\\/char-select \\#ffffff\\to open the menu!")
    end
end

local function menu_is_allowed(m)
    if m == nil then m = gMarioStates[0] end
    -- API Check
    for _, func in pairs(allowMenu) do
        if not func() then
            return false
        end
    end

    -- C-up Failsafe (Camera Softlocks)
    if m.action == ACT_FIRST_PERSON or (m.prevAction == ACT_FIRST_PERSON and is_game_paused()) then
        return false
    elseif m.prevAction == ACT_FIRST_PERSON and not is_game_paused() then
        m.prevAction = ACT_WALKING
    end

    -- Cutscene Check
    if gNetworkPlayers[0].currActNum == 99 then return false end
    if m.action == ACT_INTRO_CUTSCENE then return false end
    if obj_get_first_with_behavior_id(id_bhvActSelector) ~= nil then return false end

    return true
end

hookTableOnCharacterChange = {
    [1] = function (prevChar, currChar)
        -- Check for Non-Vanilla Actions when switching Characters
        local m = gMarioStates[0]
        if is_mario_in_vanilla_action(m) or m.health < 256 then return end
        if m.action & ACT_FLAG_RIDING_SHELL ~= 0 then
            set_mario_action(m, ACT_RIDING_SHELL_FALL, 0)
        elseif m.action & ACT_FLAG_ALLOW_FIRST_PERSON ~= 0 then
            set_mario_action(m, ACT_IDLE, 0)
        elseif m.action & ACT_GROUP_MOVING ~= 0 or m.action & ACT_FLAG_MOVING ~= 0 then
            set_mario_action(m, ACT_WALKING, 0)
        elseif m.action & ACT_GROUP_SUBMERGED ~= 0 or m.action & ACT_FLAG_SWIMMING ~= 0 then
            -- Need to fix upwarping
            set_mario_action(m, ACT_WATER_IDLE, 0)
        else
            set_mario_action(m, ACT_FREEFALL, 0)
        end
    end
}

local function on_character_change(prevChar, currChar)
    for i = 1, #hookTableOnCharacterChange do
        hookTableOnCharacterChange[i](prevChar, currChar)
    end
end

-------------------
-- Model Handler --
-------------------

local stallFrame = 0
local stallComplete = 3

CUTSCENE_CS_MENU = 0xFA

local MATH_PI = math.pi

local prevBaseCharFrame = gNetworkPlayers[0].modelIndex
local faceAngle = 0
local eyeState = MARIO_EYES_OPEN
---@param m MarioState
local function mario_update(m)
    local np = gNetworkPlayers[m.playerIndex]
    local p = gCSPlayers[m.playerIndex]
    if stallFrame == 1 or queueStorageFailsafe then
        failsafe_options()
        if not queueStorageFailsafe then
            load_preferred_char()
            if optionTable[optionTableRef.notification].toggle == 1 then
                boot_note()
            end
        end
        queueStorageFailsafe = false
    end

    if network_is_server() and gGlobalSyncTable.charSelectRestrictMovesets < 2 then
        gGlobalSyncTable.charSelectRestrictMovesets = optionTable[optionTableRef.restrictMovesets].toggle
    end

    if stallFrame < stallComplete then
        stallFrame = stallFrame + 1
    end

    if m.playerIndex == 0 and stallFrame > 1 then
        if djui_hud_is_pause_menu_created() and prevBaseCharFrame ~= np.modelIndex then
            characterTable[1].currAlt = np.modelIndex + 1
            currChar = 1
            p.presetPalette = 0
        end
        prevBaseCharFrame = np.modelIndex

        if optionTable[optionTableRef.localModels].toggle == 0 then
            currCategory = 1
            currChar = 1
            currCharRender = 1
        end

        local charTable = characterTable[currChar]
        p.saveName = charTable.saveName
        p.currAlt = charTable.currAlt
    
        p.modelId = charTable[charTable.currAlt].model
        if charTable[charTable.currAlt].baseChar ~= nil then
            p.baseChar = charTable[charTable.currAlt].baseChar
        end
        p.modelEditOffset = charTable[charTable.currAlt].model - charTable[charTable.currAlt].ogModel
        m.marioObj.hookRender = 1

        if menu and m.action == ACT_SLEEPING then
            set_mario_action(m, ACT_WAKING_UP, m.actionArg)
        end

        if menuAndTransition then
            camera_freeze()
            hud_hide()
            if m.area.camera.cutscene == 0 then
                m.area.camera.cutscene = CUTSCENE_CS_MENU
            end
            local camScale = charTable[charTable.currAlt].camScale
            local focusPos = {
                x = m.pos.x,
                y = m.pos.y + 120 * camScale,
                z = m.pos.z,
            }
            vec3f_copy(gLakituState.focus, focusPos)
            m.marioBodyState.eyeState = eyeState
            gLakituState.pos.x = m.pos.x + sins(faceAngle) * 500 * camScale
            gLakituState.pos.y = m.pos.y + 100 * camScale
            gLakituState.pos.z = m.pos.z + coss(faceAngle) * 500 * camScale
            p.inMenu = true
        else
            if p.inMenu then
                --stop_secondary_music(50)
                camera_unfreeze()
                hud_show()
                if m.area.camera.cutscene == CUTSCENE_CS_MENU then
                    m.area.camera.cutscene = CUTSCENE_STOP
                end
                p.inMenu = false
            end
        end

        -- Check for Locked Chars
        for i = 2, #characterTable do
            local currChar = characterTable[i]
            if currChar.locked then
                local unlock = characterUnlock[i].check
                local notif = characterUnlock[i].notif
                if type(unlock) == TYPE_FUNCTION then
                    if unlock() then
                        currChar.locked = false
                    end
                elseif type(unlock) == TYPE_BOOLEAN then
                    currChar.locked = unlock
                end
                if not currChar.locked then -- Character was unlocked
                    update_character_render_table()
                    if stallFrame == stallComplete and notif then
                        if optionTable[optionTableRef.notification].toggle > 0 then
                            djui_popup_create('Character Select:\nUnlocked '..tostring(currChar[1].name)..'\nas a Playable Character!', 3)
                        end
                    end
                end
            end
        end

        --Open Credits
        if optionTable[optionTableRef.credits].toggle > 0 then
            credits = true
            optionTable[optionTableRef.credits].toggle = 0
        end

        --Reset Save Data Check
        if optionTable[optionTableRef.resetSaveData].toggle > 0 then
            reset_options(false)
            optionTable[optionTableRef.resetSaveData].toggle = 0
        end
        charBeingSet = false
        for i = 1, #optionTable do
            optionTable[i].optionBeingSet = false
        end

        p.movesetToggle = optionTable[optionTableRef.localMoveset].toggle ~= 0
        if prevChar ~= currChar then
            on_character_change(prevChar, currChar)
            prevChar = currChar
        end
    end

    if p.inMenu and m.action & ACT_FLAG_ALLOW_FIRST_PERSON ~= 0 then
        m.action = ACT_IDLE
        m.actionArg = 0
        m.actionState = 0xFFFF

        -- reset menu anim on character change, starts them at frame 0 and prevents lua anim issues
        if p.prevModelId ~= p.modelId then
            p.prevModelId = p.modelId
            m.marioObj.header.gfx.animInfo.animID = -1
        end
        set_character_animation(m, (characterAnims[p.modelId] and characterAnims[p.modelId][CS_ANIM_MENU]) and CS_ANIM_MENU or CHAR_ANIM_FIRST_PERSON)

        m.marioObj.header.gfx.angle.y = m.faceAngle.y
    elseif m.actionState == 0xFFFF and m.action == ACT_IDLE then
        -- snap back to normal idle when out of the menu
        m.actionState = 0
    end

    np.overrideModelIndex = p.baseChar ~= nil and p.baseChar or CT_MARIO

    -- Character Animations
    if characterAnims[p.modelId] then
        local animID = characterAnims[p.modelId][m.marioObj.header.gfx.animInfo.animID]
        if animID then
            smlua_anim_util_set_animation(m.marioObj, animID)
        end
    end
end

local sCapBhvs = {
    [id_bhvWingCap] = true,
    [id_bhvVanishCap] = true,
    [id_bhvMetalCap] = true
}

---@param o Object
---@param model integer
local BowserKey = false
local function on_star_or_key_grab(m, o, type)
    if type == INTERACT_STAR_OR_KEY then
        if get_id_from_behavior(o.behavior) == id_bhvBowserKey then
            BowserKey = true
        else
            BowserKey = false
        end
    end
end

function set_model(o, model)
    if optionTable[optionTableRef.localModels].toggle == 0 then return end

    -- Player Models
    if obj_has_behavior_id(o, id_bhvMario) ~= 0 then
        local i = network_local_index_from_global(o.globalPlayerIndex)
        local prevModelData = obj_get_model_id_extended(o)
        local localModelData = nil
        for c = 1, #characterTable do
            if gCSPlayers[i].saveName == characterTable[c].saveName then
                if gCSPlayers[i].currAlt <= #characterTable[c] then
                    localModelData = characterTable[c][gCSPlayers[i].currAlt].ogModel + gCSPlayers[i].modelEditOffset
                end
            end
        end
        if localModelData ~= nil then
            if obj_has_model_extended(o, localModelData) == 0 then
                obj_set_model_extended(o, localModelData)
            end
        else
            -- Original/Backup
            if gCSPlayers[i].modelId ~= nil and obj_has_model_extended(o, gCSPlayers[i].modelId) == 0 then
                obj_set_model_extended(o, gCSPlayers[i].modelId)
            end
        end
        return
    end

    -- Star Models
    if obj_has_behavior_id(o, id_bhvCelebrationStar) ~= 0 and o.parentObj ~= nil then
        local i = network_local_index_from_global(o.parentObj.globalPlayerIndex)
        local starModel = characterCelebrationStar[gCSPlayers[i].modelId]
        if gCSPlayers[i].modelId ~= nil and starModel ~= nil and obj_has_model_extended(o, starModel) == 0 and not BowserKey then
            obj_set_model_extended(o, starModel)
        end
        return
    end

    if sCapBhvs[get_id_from_behavior(o.behavior)] then
        local playerToObj = nearest_player_to_object(o.parentObj)
        o.globalPlayerIndex = playerToObj and playerToObj.globalPlayerIndex or 0
    end
    local i = network_local_index_from_global(o.globalPlayerIndex)

    local c = gMarioStates[i].character
    if model == c.capModelId or
       model == c.capWingModelId or
       model == c.capMetalModelId or
       model == c.capMetalWingModelId then
        local capModels = characterCaps[gCSPlayers[i].modelId]
        if capModels ~= nil then
            local capModel = E_MODEL_NONE
            if model == c.capModelId then
                capModel = capModels.normal
            elseif model == c.capWingModelId then
                capModel = capModels.wing
            elseif model == c.capMetalModelId then
                capModel = capModels.metal
            elseif model == c.capMetalWingModelId then
                capModel = capModels.metalWing
            end
            if capModel ~= E_MODEL_NONE and capModel ~= E_MODEL_ERROR_MODEL and capModel ~= nil then
                obj_set_model_extended(o, capModel)
            end
        end
    end
end

--hook_event(HOOK_MARIO_UPDATE, mario_update)
cs_hook_mario_update(mario_update)
hook_event(HOOK_ON_INTERACT, on_star_or_key_grab)
hook_event(HOOK_OBJECT_SET_MODEL, set_model)

------------------
-- Menu Handler --
------------------

local buttonAnimTimer = 0
local buttonScroll = 0
local buttonScrollCap = 30

local optionAnimTimer = -200
local optionAnimTimerCap = optionAnimTimer

local inputStallTimerButton = 0
local inputStallTimerDirectional = 0
local inputStallToDirectional = 12
local inputStallToButton = 10

--Basic Menu Text
local TEXT_OPTIONS_HEADER = "Menu Options"
local TEXT_OPTIONS_HEADER_API = "API Options"
local yearsOfCS = get_date_and_time().year - 123 -- Zero years as of 2023
local TEXT_VERSION = "Version: " .. MOD_VERSION_STRING .. " | sm64coopdx" .. (seasonalEvent == SEASON_EVENT_BIRTHDAY and (" | " .. tostring(yearsOfCS) .. " year" .. (yearsOfCS > 1 and "s" or "") .. " of Character Select!") or "")
local TEXT_RATIO_UNSUPPORTED = "Your Current Aspect-Ratio isn't Supported!"
local TEXT_DESCRIPTION = "Character Description:"
local TEXT_PREF_SAVE = "Preferred Char (A)"
local TEXT_PREF_PALETTE = "Toggle Palette (Y)"
local TEXT_MOVESET_INFO = "Moveset Info (Z)"
local TEXT_PAUSE_Z_OPEN = "Z Button - Character Select"
local TEXT_PAUSE_UNAVAILABLE = "Character Select is Unavailable"
local TEXT_PAUSE_CURR_CHAR = "Current Character: "
local TEXT_MOVESET_RESTRICTED = "Movesets are Restricted"
local TEXT_PALETTE_RESTRICTED = "Palettes are Restricted"
local TEXT_MOVESET_AND_PALETTE_RESTRICTED = "Moveset and Palettes are Restricted"
local TEXT_CHAR_LOCKED = "Locked"
-- Easter Egg if you get lucky loading the mod
-- Referencing the original sm64ex DynOS options by PeachyPeach >v<
if math_random(100) == 64 then
    TEXT_PAUSE_Z_OPEN = "Z - DynOS"
    TEXT_PAUSE_CURR_CHAR = "Model: "
end

--Debug Text
local TEXT_DEBUGGING = "Character Debug"
local TEXT_DESCRIPTION_SHORT = "Description:"
local TEXT_LIFE_ICON = "Life Icon:"
local TEXT_STAR_ICON = "Star Icon:"
local TEXT_FORCED_CHAR = "Base: "
local TEXT_TABLE_POS = "Table Position: "
local TEXT_PALETTE = "Palette: "
local baseCharStrings = {
    [CT_MARIO] = "CT_MARIO",
    [CT_LUIGI] = "CT_LUIGI",
    [CT_TOAD] = "CT_TOAD",
    [CT_WALUIGI] = "CT_WALUIGI",
    [CT_WARIO] = "CT_WARIO"
}

--Options Text
local TEXT_OPTIONS_OPEN = "Press START to open Options"
local TEXT_MENU_CLOSE = "Press B to Exit Menu"
local TEXT_OPTIONS_SELECT = "A - Select | B - Exit  "
local TEXT_LOCAL_MODEL_OFF = "Locally Display Models is Off"
local TEXT_LOCAL_MODEL_OFF_OPTIONS = "You can turn it back on in the Options Menu"
local TEXT_LOCAL_MODEL_ERROR = "Failed to find a Character Model"
local TEXT_LOCAL_MODEL_ERROR_FIX = "Please Verify the Integrity of the Pack!"

--Credit Text
local TEXT_CREDITS_HEADER = "Credits"

local MATH_DIVIDE_320 = 1/320
local MATH_DIVIDE_64 = 1/64
local MATH_DIVIDE_32 = 1/32
local MATH_DIVIDE_30 = 1/30
local MATH_DIVIDE_16 = 1/16

local targetMenuColor = {r = 0 , g = 0, b = 0}
menuColor = targetMenuColor
local menuColorHalf = menuColor
local transSpeed = 0.1
local prevBindText = ""
local bindText = 1
local bindTextTimerLoop = 150
local bindTextTimer = 0
local bindTextOpacity = -255
function update_menu_color()
    if optionTable[optionTableRef.menuColor].toggle == nil then return end
    if optionTable[optionTableRef.localModels].toggle == 1 then
        if optionTable[optionTableRef.menuColor].toggle > 1 then
            targetMenuColor = menuColorTable[optionTable[optionTableRef.menuColor].toggle - 1]
        elseif optionTable[optionTableRef.menuColor].toggle == 1 then
            optionTable[optionTableRef.menuColor].toggleNames[2] = string_underscore_to_space(TEXT_PREF_LOAD_NAME) .. ((TEXT_PREF_LOAD_ALT ~= 1 and currChar ~= 1) and " ("..TEXT_PREF_LOAD_ALT..")" or "") .. " (Pref)"
            targetMenuColor = prefCharColor
        elseif characterTable[currChar] ~= nil then
            local char = characterTable[currChar]
            targetMenuColor = char[char.currAlt].color
        end
    else
        targetMenuColor = menuColorTable[9]
    end
    if optionTable[optionTableRef.anims].toggle > 0 then
        menuColor.r = math.lerp(menuColor.r, targetMenuColor.r, transSpeed)
        menuColor.g = math.lerp(menuColor.g, targetMenuColor.g, transSpeed)
        menuColor.b = math.lerp(menuColor.b, targetMenuColor.b, transSpeed)
    else
        menuColor.r = targetMenuColor.r
        menuColor.g = targetMenuColor.g
        menuColor.b = targetMenuColor.b
    end
    menuColorHalf = {
        r = menuColor.r * 0.5 + 127,
        g = menuColor.g * 0.5 + 127,
        b = menuColor.b * 0.5 + 127
    }
    return menuColor
end

local TEX_TRIANGLE = get_texture_info("char-select-triangle")
local function djui_hud_render_triangle(x, y, width, height)
    djui_hud_render_texture(TEX_TRIANGLE, x, y, width*MATH_DIVIDE_64, height*MATH_DIVIDE_32)
end

local buttonAltAnim = 0
local menuOpacity = 245
local menuText = {}
local function on_hud_render()
    local FONT_USER = djui_menu_get_font()
    djui_hud_set_font(FONT_ALIASED)
    djui_hud_set_resolution(RESOLUTION_DJUI)
    local djuiWidth = djui_hud_get_screen_width()
    local djuiHeight = djui_hud_get_screen_height()
    djui_hud_set_resolution(RESOLUTION_N64)
    local width = djuiWidth * (240/djuiHeight) -- Get accurate, unrounded width
    local height = 240
    local widthHalf = width * 0.5
    local heightHalf = height * 0.5
    local widthScale = maxf(width, 320) * MATH_DIVIDE_320

    if stallFrame == stallComplete then
        update_menu_color()
        if not menu_is_allowed() then
            menu = false
        end
    end

    if menuAndTransition then

        if optionTable[optionTableRef.localModels].toggle == 0 then
            djui_hud_set_color(0, 0, 0, 200)
            djui_hud_render_rect(0, 0, width, height)
            djui_hud_set_color(255, 255, 255, 255)
            djui_hud_print_text(TEXT_LOCAL_MODEL_OFF, widthHalf - djui_hud_measure_text(TEXT_LOCAL_MODEL_OFF) * 0.15 * widthScale, heightHalf, 0.3 * widthScale)
            djui_hud_print_text(TEXT_LOCAL_MODEL_OFF_OPTIONS, widthHalf - djui_hud_measure_text(TEXT_LOCAL_MODEL_OFF_OPTIONS) * 0.1 * widthScale, heightHalf + 10 * widthScale, 0.2 * widthScale)
        end

        if characterTable[currChar][characterTable[currChar].currAlt].model == E_MODEL_ARMATURE then
            djui_hud_set_color(0, 0, 0, 200)
            djui_hud_render_rect(0, 0, width, height)
            djui_hud_set_color(255, 255, 255, 255)
            djui_hud_print_text(TEXT_LOCAL_MODEL_ERROR, widthHalf - djui_hud_measure_text(TEXT_LOCAL_MODEL_ERROR) * 0.15 * widthScale, heightHalf, 0.3 * widthScale)
            djui_hud_print_text(TEXT_LOCAL_MODEL_ERROR_FIX, widthHalf - djui_hud_measure_text(TEXT_LOCAL_MODEL_ERROR_FIX) * 0.1 * widthScale, heightHalf + 10 * widthScale, 0.2 * widthScale)
        end

        local x = 135 * widthScale * 0.8

        -- Render All Black Squares Behind Below API
        djui_hud_set_color(menuColorHalf.r * 0.1, menuColorHalf.g * 0.1, menuColorHalf.b * 0.1, menuOpacity)
        -- Description
        djui_hud_render_rect(width - x + 2, 2 + 46, x - 4, height - 4 - 46)
        -- Buttons
        djui_hud_render_rect(2, 2 + 46, x - 4, height - 4 - 46)
        -- Header
        djui_hud_render_rect(2, 2, width - 4, 46)

        -- API Rendering (Below Text)
        if #hookTableRenderInMenu.back > 0 then
            for i = 1, #hookTableRenderInMenu.back do
                hookTableRenderInMenu.back[i]()
            end
        end

        --Character Description
        djui_hud_set_color(menuColor.r, menuColor.g, menuColor.b, 255)
        djui_hud_render_rect(width - x, 50, 2, height - 50)
        djui_hud_render_rect(width - x, height - 2, x, 2)
        djui_hud_render_rect(width - 2, 50, 2, height - 50)
        djui_hud_set_color(menuColorHalf.r, menuColorHalf.g, menuColorHalf.b, 255)
        djui_hud_set_font(FONT_ALIASED)
        local character = characterTable[currChar]
        local TEXT_SAVE_NAME = "Save Name: " .. character.saveName
        local TEXT_MOVESET = "Has Moveset: "..(character.hasMoveset and "Yes" or "No")
        local TEXT_ALT = "Alt: " .. character.currAlt .. "/" .. #character
        character = characterTable[currChar][character.currAlt]
        local paletteCount = characterColorPresets[gCSPlayers[0].modelId] ~= nil and #characterColorPresets[gCSPlayers[0].modelId] or 0
        local currPaletteTable = characterColorPresets[gCSPlayers[0].modelId] and characterColorPresets[gCSPlayers[0].modelId] or {currPalette = 0}
        if optionTable[optionTableRef.debugInfo].toggle == 0 then
            -- Actual Description --
            local TEXT_NAME = string_underscore_to_space(character.name)
            local TEXT_CREDIT = "Credit: " .. character.credit
            local TEXT_DESCRIPTION_TABLE = character.description
            local TEXT_PREF_LOAD_NAME = string_underscore_to_space(TEXT_PREF_LOAD_NAME) .. ((TEXT_PREF_LOAD_ALT ~= 1 and TEXT_PREF_LOAD_NAME ~= "Default" and currChar ~= 1) and " ("..TEXT_PREF_LOAD_ALT..")" or "")

            local textX = x * 0.5
            djui_hud_print_text(TEXT_NAME, width - textX - djui_hud_measure_text(TEXT_NAME) * 0.3, 55, 0.6)
            djui_hud_set_font(FONT_TINY)
            local creditScale = 0.6 
            creditScale = math_min(creditScale, 100/djui_hud_measure_text(TEXT_CREDIT))
            djui_hud_print_text(TEXT_CREDIT, width - textX - djui_hud_measure_text(TEXT_CREDIT) * creditScale *0.5, 74, creditScale)
            djui_hud_set_font(FONT_ALIASED)
            djui_hud_print_text(TEXT_DESCRIPTION, width - textX - djui_hud_measure_text(TEXT_DESCRIPTION) * 0.2, 85, 0.4)
            if widthScale < 1.65 then
                for i = 1, #TEXT_DESCRIPTION_TABLE do
                    djui_hud_print_text(TEXT_DESCRIPTION_TABLE[i], width - textX - djui_hud_measure_text(TEXT_DESCRIPTION_TABLE[i]) * 0.15, 90 + i * 9, 0.3)
                end
            else
                for i = 1, math_ceil(#TEXT_DESCRIPTION_TABLE*0.5) do
                    local tablePos = (i * 2) - 1
                    if TEXT_DESCRIPTION_TABLE[tablePos] and TEXT_DESCRIPTION_TABLE[tablePos + 1] then
                        local TEXT_STRING = TEXT_DESCRIPTION_TABLE[tablePos] .. " " .. TEXT_DESCRIPTION_TABLE[tablePos + 1]
                        djui_hud_print_text(TEXT_STRING, width - textX - djui_hud_measure_text(TEXT_STRING) * 0.15, 90 + i * 9, 0.3)
                    elseif TEXT_DESCRIPTION_TABLE[tablePos] then
                        local TEXT_STRING = TEXT_DESCRIPTION_TABLE[tablePos]
                        djui_hud_print_text(TEXT_STRING, width - textX - djui_hud_measure_text(TEXT_STRING) * 0.15, 90 + i * 9, 0.3)
                    end
                end
            end

            menuText = {
                TEXT_PREF_SAVE .. " - " .. TEXT_PREF_LOAD_NAME
            }
            local modelId = gCSPlayers[0].modelId
            local TEXT_PRESET_TOGGLE = ((currPaletteTable[currPaletteTable.currPalette] ~= nil and currPaletteTable[currPaletteTable.currPalette].name ~= nil) and (currPaletteTable[currPaletteTable.currPalette].name .. " - ") or "") .. ((paletteCount > 1 and "("..currPaletteTable.currPalette.."/"..paletteCount..")" or (currPaletteTable.currPalette > 0 and "On" or "Off")) or "Off")
            if characterColorPresets[modelId] and gGlobalSyncTable.charSelectRestrictPalettes == 0 then
                table_insert(menuText, TEXT_PREF_PALETTE .. " - " .. TEXT_PRESET_TOGGLE)
            elseif gGlobalSyncTable.charSelectRestrictPalettes > 0 then
                table_insert(menuText, TEXT_PALETTE_RESTRICTED)
            end
            if #menuText > 1 then
                bindTextTimer = (bindTextTimer + 1)%(bindTextTimerLoop)
            end
            if bindTextTimer == 0 then
                bindText = bindText + 1
                bindTextOpacity = -254
            end
            if bindText > #menuText or not menuText[bindText] then
                bindText = 1
            end
            if menuText[bindText] ~= prevBindText and bindTextOpacity == -255 then
                bindTextOpacity = -254
            end
            if bindTextOpacity > -255 and bindTextOpacity < 255 then
                bindTextOpacity = math.min(bindTextOpacity + 25, 255)
                if bindTextOpacity == 255 then
                    bindTextOpacity = -255
                    prevBindText = menuText[bindText]
                end
            end
            --local bindTextOpacity = math.clamp(math.abs(math.sin(bindTextTimer*MATH_PI/bindTextTimerLoop)), 0, 0.2) * 5 * 255
            local fadeOut = math_abs(math.clamp(bindTextOpacity, -255, 0))
            local fadeIn = math_abs(math.clamp(bindTextOpacity, 0, 255))
            local bindTextScale = math.min((x - 10)/(djui_hud_measure_text(menuText[bindText]) * 0.3), 1)*0.3
            local prevBindTextScale = math.min((x - 10)/(djui_hud_measure_text(prevBindText) * 0.3), 1)*0.3
            djui_hud_set_color(menuColorHalf.r, menuColorHalf.g, menuColorHalf.b, fadeOut)
            djui_hud_print_text(prevBindText, width - textX - djui_hud_measure_text(prevBindText) * prevBindTextScale*0.5, height - 15, prevBindTextScale)
            djui_hud_set_color(menuColorHalf.r, menuColorHalf.g, menuColorHalf.b, fadeIn)
            djui_hud_print_text(menuText[bindText], width - textX - djui_hud_measure_text(menuText[bindText]) * bindTextScale*0.5, height - 15, bindTextScale)
            djui_hud_set_color(menuColorHalf.r, menuColorHalf.g, menuColorHalf.b, 255)
        else
            -- Debugging Info --
            local TEXT_NAME = "Name: " .. character.name
            local TEXT_CREDIT = "Credit: " .. character.credit
            local TEXT_DESCRIPTION_TABLE = character.description
            local TEXT_COLOR = "Color: R-" .. character.color.r ..", G-" ..character.color.g ..", B-"..character.color.b
            local TEX_LIFE_ICON = character.lifeIcon
            local TEX_STAR_ICON = character.starIcon
            local TEXT_SCALE = "Camera Scale: " .. character.camScale
            local TEXT_PRESET = "Preset Palette: ("..currPaletteTable.currPalette.."/"..paletteCount..")"
            local TEXT_PREF = "Preferred: " .. TEXT_PREF_LOAD_NAME .. " ("..TEXT_PREF_LOAD_ALT..")"
            local TEXT_PREF_COLOR = "Pref Color: R-" .. prefCharColor.r .. ", G-" .. prefCharColor.g .. ", B-" .. prefCharColor.b

            local textX = x * 0.5
            djui_hud_print_text(TEXT_DEBUGGING, width - textX - djui_hud_measure_text(TEXT_DEBUGGING) * 0.3, 55, 0.6)
            djui_hud_set_font(FONT_TINY)
            local y = 72
            djui_hud_print_text(TEXT_NAME, width - x + 8, y, 0.5)
            y = y + 7
            djui_hud_print_text(TEXT_SAVE_NAME, width - x + 8, y, 0.5)
            y = y + 7
            djui_hud_print_text(TEXT_ALT, width - x + 8, y, 0.5)
            y = y + 7
            djui_hud_print_text(TEXT_CREDIT, width - x + 8, y, 0.5)
            y = y + 7
            if TEXT_DESCRIPTION_TABLE[1] ~= "No description has been provided" then
                djui_hud_print_text(TEXT_DESCRIPTION_SHORT, width - x + 8, y, 0.5)
                y = y + 2
                local removeLine = 0
                for i = 1, #TEXT_DESCRIPTION_TABLE do
                    if TEXT_DESCRIPTION_TABLE[i] ~= "" then
                        djui_hud_set_font(FONT_ALIASED)
                        local TEXT_DESCRIPTION_LINE = TEXT_DESCRIPTION_TABLE[i]
                        if (djui_hud_measure_text(TEXT_DESCRIPTION_TABLE[i]) * 0.3 > 100) then
                            TEXT_DESCRIPTION_LINE = "(!) " .. TEXT_DESCRIPTION_LINE
                        else
                            TEXT_DESCRIPTION_LINE = "    " .. TEXT_DESCRIPTION_LINE
                        end
                        djui_hud_set_font(FONT_TINY)
                        djui_hud_print_text(TEXT_DESCRIPTION_LINE, width - x + 5, y + (i-removeLine) * 5, 0.4)
                    else
                        removeLine = removeLine + 1
                    end
                end
                local descriptionOffset = (#TEXT_DESCRIPTION_TABLE - removeLine) * 5
                y = y + 5 + descriptionOffset
            end
            djui_hud_set_color(character.color.r, character.color.g, character.color.b, 255)
            djui_hud_print_text(TEXT_COLOR, width - x + 8, y, 0.5)
            djui_hud_set_color(menuColorHalf.r, menuColorHalf.g, menuColorHalf.b, 255)
            y = y + 7
            if type(TEX_LIFE_ICON) ~= TYPE_STRING then
                djui_hud_print_text(TEXT_LIFE_ICON .. "    (" .. TEX_LIFE_ICON.width .. "x" .. TEX_LIFE_ICON.height .. ")", width - x + 8, y, 0.5)
                djui_hud_set_color(255, 255, 255, 255)
                djui_hud_render_texture(TEX_LIFE_ICON, width - x + 33, y + 1, 0.4 / (TEX_LIFE_ICON.width * MATH_DIVIDE_16), 0.4 / (TEX_LIFE_ICON.height * MATH_DIVIDE_16))
            else
                djui_hud_print_text(TEXT_LIFE_ICON .. "    (FONT_HUD)", width - x + 8, y, 0.5)
                djui_hud_set_font(FONT_HUD)
                djui_hud_set_color(255, 255, 255, 255)
                djui_hud_print_text(TEX_LIFE_ICON, width - x + 33, y + 1, 0.4)
                djui_hud_set_font(FONT_TINY)
            end
            y = y + 7
            djui_hud_set_color(menuColorHalf.r, menuColorHalf.g, menuColorHalf.b, 255)
            djui_hud_print_text(TEXT_STAR_ICON .. "    (" .. TEX_STAR_ICON.width .. "x" .. TEX_STAR_ICON.height .. ")", width - x + 8, y, 0.5)
            djui_hud_set_color(255, 255, 255, 255)
            djui_hud_render_texture(TEX_STAR_ICON, width - x + 35, y + 1, 0.4 / (TEX_STAR_ICON.width * MATH_DIVIDE_16), 0.4 / (TEX_STAR_ICON.height * MATH_DIVIDE_16))
            y = y + 7
            djui_hud_set_color(menuColorHalf.r, menuColorHalf.g, menuColorHalf.b, 255)
            djui_hud_print_text(TEXT_FORCED_CHAR .. baseCharStrings[character.baseChar], width - x + 8, y, 0.5)
            y = y + 7
            djui_hud_print_text(TEXT_TABLE_POS .. currChar, width - x + 8, y, 0.5)
            y = y + 7
            djui_hud_print_text(TEXT_SCALE, width - x + 8, y, 0.5)
            local modelId = gCSPlayers[0].modelId
            y = y + 7
            if characterColorPresets[modelId] ~= nil then
                djui_hud_print_text(TEXT_PALETTE, width - x + 8, y, 0.5)
                local x = x - djui_hud_measure_text(TEXT_PALETTE)*0.5
                local currPalette = currPaletteTable.currPalette > 0 and currPaletteTable.currPalette or 1
                local paletteTable = currPaletteTable[currPalette]
                for i = 0, #paletteTable do
                    djui_hud_set_color(menuColorHalf.r, menuColorHalf.g, menuColorHalf.b, 255)
                    djui_hud_render_rect(width - x + 6.5 + (6.5 * i), y + 1.5, 6, 6)
                    djui_hud_set_color(paletteTable[i].r, paletteTable[i].g, paletteTable[i].b, 255)
                    djui_hud_render_rect(width - x + 7 + (6.5 * i), y + 2, 5, 5)
                end
                y = y + 7
                djui_hud_set_color(menuColorHalf.r, menuColorHalf.g, menuColorHalf.b, 255)
            end
            djui_hud_print_text(TEXT_MOVESET, width - x + 8, y, 0.5)
            y = y + 7
            djui_hud_print_text(TEXT_PRESET, width - x + 8, height - 29, 0.5)
            djui_hud_print_text(TEXT_PREF, width - x + 8, height - 22, 0.5)
            djui_hud_set_color(prefCharColor.r, prefCharColor.g, prefCharColor.b, 255)
            djui_hud_print_text(TEXT_PREF_COLOR, width - x + 8, height - 15, 0.5)
            djui_hud_set_color(menuColorHalf.r, menuColorHalf.g, menuColorHalf.b, 255)
        end

        --Character Buttons
        djui_hud_set_color(menuColor.r, menuColor.g, menuColor.b, 255)
        djui_hud_render_rect(0, 50, 2, height - 50)
        djui_hud_render_rect(x - 2, 50, 2, height - 50)
        djui_hud_render_rect(0, height - 2, x, 2)
        
        local leftRightAnim = 0
        if optionTable[optionTableRef.anims].toggle > 0 then
            buttonAnimTimer = buttonAnimTimer + 1
            leftRightAnim = buttonAltAnim/inputStallToDirectional
            if buttonAltAnim ~= 0 then
                if buttonAltAnim > 0 then
                    buttonAltAnim = buttonAltAnim - 3
                else
                    buttonAltAnim = buttonAltAnim + 3
                end
            end
        end
        if optionTable[optionTableRef.anims].toggle == 0 then
            buttonScroll = 0
        elseif math_abs(buttonScroll) > 0.1 then
            buttonScroll = buttonScroll * 0.05 * inputStallToDirectional
        end

        local buttonColor = {}
        local buttonX = 20 * widthScale
        local buttonAnimX = buttonX + math_sin(buttonAnimTimer * 0.05) * 2.5 + 5
        local charNum = -1
        for i = -1, 4 do
            -- Hide Locked Characters based on Toggle
            charNum = currCharRender + i
            local char = characterTableRender[charNum]
            if char ~= nil then
                if not char.locked then
                    buttonColor = char[char.currAlt].color
                else
                    buttonColor = {r = char[char.currAlt].color.r*0.5, g = char[char.currAlt].color.g*0.5, b = char[char.currAlt].color.b*0.5}
                end
                djui_hud_set_color(buttonColor.r, buttonColor.g, buttonColor.b, 255)
                local x = buttonX
                local y = 104 + buttonScroll
                if i == 0 then
                    if optionTable[optionTableRef.anims].toggle > 0 then
                        x = buttonAnimX
                    else
                        x = buttonX + 5
                    end
                    if #char > 1 then
                        djui_hud_set_rotation(0x4000, 0, 0)
                        djui_hud_render_triangle(x - 6 + math_min(leftRightAnim, 0), y, 8, 4)
                        djui_hud_set_rotation(-0x4000, 0, 0)
                        djui_hud_render_triangle(x + 76 + math_max(leftRightAnim, 0), y - 8 - 1*MATH_DIVIDE_16, 8, 4)
                        djui_hud_set_rotation(0, 0, 0)
                    end
                end
                local y = (i + 3) * 30 + buttonScroll
                djui_hud_render_rect(x, y, 1, 20)
                djui_hud_render_rect(x, y, 70, 1)
                djui_hud_render_rect(x + 69, y, 1, 20)
                djui_hud_render_rect(x, y + 19, 70, 1)
                djui_hud_set_color(buttonColor.r * 0.1, buttonColor.g * 0.1, buttonColor.b * 0.1, menuOpacity)
                djui_hud_render_rect(x + 1, y + 1, 68, 18)
                djui_hud_set_font(FONT_TINY)
                djui_hud_set_color(buttonColor.r, buttonColor.g, buttonColor.b, 255)
                local charName = char[char.currAlt].name
                if char.locked then
                    charName = TEXT_CHAR_LOCKED
                end
                djui_hud_set_color(buttonColor.r * 0.5 + 127, buttonColor.g * 0.5 + 127, buttonColor.b * 0.5 + 127, 255)
                djui_hud_print_text(charName, x + 5, y + 5, 0.6)
            end
        end

        -- Scroll Bar
        local MATH_DIVIDE_CHARACTERS = 1/#characterTableRender
        local MATH_7_WIDTHSCALE = 7 * widthScale
        djui_hud_set_color(menuColor.r, menuColor.g, menuColor.b, 255)
        djui_hud_render_rect(MATH_7_WIDTHSCALE, 55, 1, 170)
        djui_hud_render_rect(MATH_7_WIDTHSCALE, 55, 7, 1)
        djui_hud_render_rect(MATH_7_WIDTHSCALE + 6, 55, 1, 170)
        djui_hud_render_rect(MATH_7_WIDTHSCALE, 224, 7, 1)
        djui_hud_set_color(menuColorHalf.r, menuColorHalf.g, menuColorHalf.b, 255)
        djui_hud_render_rect(MATH_7_WIDTHSCALE + 2, 57 + 166 * ((currCharRender - 1) * MATH_DIVIDE_CHARACTERS) - (buttonScroll * MATH_DIVIDE_30) * (166 * MATH_DIVIDE_CHARACTERS), 3, 166 * MATH_DIVIDE_CHARACTERS)
        djui_hud_set_font(FONT_TINY)
        local TEXT_CHAR_COUNT = currCharRender .. "/" .. #characterTableRender
        djui_hud_print_text(TEXT_CHAR_COUNT, (11 - djui_hud_measure_text(TEXT_CHAR_COUNT) * 0.2) * widthScale, height - 12, 0.4)
        djui_hud_print_text("- "..characterCategories[currCategory] .. " (L/R)", (11 + djui_hud_measure_text(TEXT_CHAR_COUNT) * 0.2) * widthScale, height - 12, 0.4)

        --Character Select Header
        djui_hud_set_color(menuColor.r, menuColor.g, menuColor.b, 255)
        djui_hud_render_rect(0, 0, width, 2)
        djui_hud_render_rect(0, 0, 2, 50)
        djui_hud_render_rect(0, 48, width, 2)
        djui_hud_render_rect(width - 2, 0, 2, 50)
        djui_hud_set_color(menuColorHalf.r, menuColorHalf.g, menuColorHalf.b, 255)
        if TEX_OVERRIDE_HEADER ~= nil then -- Render Override Header
            djui_hud_render_texture(TEX_OVERRIDE_HEADER, widthHalf - 128, 10, 1 / (TEX_OVERRIDE_HEADER.height*MATH_DIVIDE_32), 1 / (TEX_OVERRIDE_HEADER.height*MATH_DIVIDE_32))
        else
            djui_hud_render_texture(TEX_HEADER, widthHalf - 128, 10, 1, 1)
        end
        djui_hud_set_color(menuColorHalf.r, menuColorHalf.g, menuColorHalf.b, 255)
        djui_hud_set_font(FONT_TINY)
        djui_hud_print_text(optionTable[optionTableRef.debugInfo].toggle == 0 and TEXT_VERSION or MOD_VERSION_DEBUG, 5, 3, 0.5)

        --Unsupported Res Warning
        if width < 319 or width > 575 then
            djui_hud_print_text(TEXT_RATIO_UNSUPPORTED, 5, 39, 0.5)
        end

        -- API Rendering (Above Text)
        djui_hud_set_color(menuColor.r, menuColor.g, menuColor.b, 255)
        if #hookTableRenderInMenu.front > 0 then
            for i = 1, #hookTableRenderInMenu.front do
                hookTableRenderInMenu.front[i]()
            end
        end
        djui_hud_set_resolution(RESOLUTION_N64)

        --Options display
        local optionTableCount = #optionTable
        if options or optionAnimTimer > optionAnimTimerCap then
            djui_hud_set_color(menuColor.r * 0.25, menuColor.g * 0.25, menuColor.b * 0.25, 205 + maxf(-200, optionAnimTimer))
            djui_hud_render_rect(0, 0, width, height)
            djui_hud_set_color(menuColor.r, menuColor.g, menuColor.b, 255)
            djui_hud_render_rect(width * 0.5 - 50 * widthScale, minf(55 - optionAnimTimer, height - 25 * widthScale), 100 * widthScale, 200)
            djui_hud_set_color(menuColor.r * 0.1, menuColor.g * 0.1, menuColor.b * 0.1, menuOpacity)
            djui_hud_render_rect(width * 0.5 - 50 * widthScale + 2, minf(55 - optionAnimTimer + 2, height - 25 * widthScale + 2), 100 * widthScale - 4, 196)
            djui_hud_set_font(FONT_ALIASED)

            if not creditsAndTransition then
                local widthScaleLimited = minf(widthScale, 1.5)
                -- Up Arrow
                if currOption > 3 then
                    djui_hud_set_color(menuColorHalf.r, menuColorHalf.g, menuColorHalf.b, 255)
                    djui_hud_render_triangle(widthHalf - 3.5*widthScaleLimited, 94 - optionAnimTimer, 6*widthScaleLimited, 3*widthScaleLimited)
                end

                -- Down Arrow
                if currOption < optionTableCount - 2 then
                    local yOffset = 90 - optionAnimTimer + 45 * widthScaleLimited
                    djui_hud_set_color(menuColorHalf.r, menuColorHalf.g, menuColorHalf.b, 255)
                    djui_hud_set_rotation(0x8000, 0.5, 0.5)
                    djui_hud_render_triangle(widthHalf - 3.5*widthScaleLimited, yOffset + 10 + 3*widthScaleLimited, 6*widthScaleLimited, 3*widthScaleLimited)
                    djui_hud_set_rotation(0, 0, 0)
                end

                -- Options 
                for i = currOption - 2, currOption + 2 do
                    if not (i < 1 or i > optionTableCount) then
                        local toggleName = optionTable[i].name
                        local scale = 0.5
                        local yOffset = 100 - optionAnimTimer + (i - currOption + 2) * 9 * widthScaleLimited

                        local lockName = nil
                        if optionTable[i].lock ~= nil then
                            lockName = optionTable[i].lock()
                        end

                        if i == currOption then
                            djui_hud_set_font(FONT_ALIASED)
                            scale = 0.3
                            yOffset = yOffset - 1
                            local currToggleName = optionTable[i].toggleNames[optionTable[i].toggle + 1]
                            currToggleName = currToggleName and currToggleName or "???"
                            if lockName ~= nil then
                                currToggleName = lockName
                            end
                            if currToggleName ~= "" then
                                toggleName = toggleName .. " - " .. currToggleName
                            end
                        else
                            djui_hud_set_font(FONT_TINY)
                        end
                        djui_hud_set_color(menuColorHalf.r * (lockName ~= nil and 0.5 or 1), menuColorHalf.g * (lockName ~= nil and 0.5 or 1), menuColorHalf.b * (lockName ~= nil and 0.5 or 1), 255)
                        scale = scale * widthScaleLimited
                        djui_hud_print_text(toggleName, widthHalf - djui_hud_measure_text(toggleName) * scale * 0.5, yOffset, scale)
                    end
                end

                -- Description
                if optionTable[currOption].description ~= nil then
                    djui_hud_set_color(menuColorHalf.r, menuColorHalf.g, menuColorHalf.b, 255)
                    for i = 1, #optionTable[currOption].description do
                        djui_hud_set_font(FONT_ALIASED)
                        local line = optionTable[currOption].description[i]
                        djui_hud_print_text(line, widthHalf - djui_hud_measure_text(line) * 0.15, 180 - optionAnimTimer + 15 * widthScaleLimited + 8 * i - 8 * #optionTable[currOption].description, 0.3)
                    end
                end
                -- Footer
                djui_hud_set_font(FONT_TINY)
                djui_hud_set_color(menuColorHalf.r, menuColorHalf.g, menuColorHalf.b, 255)
                djui_hud_print_text(TEXT_OPTIONS_SELECT, widthHalf - djui_hud_measure_text(TEXT_OPTIONS_SELECT) * 0.3, height - 20 - optionAnimTimer, 0.6)
            else
                local renderList = {}
                for i = 1, #creditTable do
                    local credit = creditTable[i]
                    table_insert(renderList, {textLeft = credit.packName, font = FONT_ALIASED})
                    for i = 1, #credit do
                        local credit = credit[i]
                        table_insert(renderList, {textLeft = credit.creditTo, textRight = credit.creditFor, font = FONT_NORMAL})
                    end
                end

                local xLeft = widthHalf - 50 * widthScale + 8
                local xRight = widthHalf + 50 * widthScale - 8
                local y = 80 + 10*widthScale - optionAnimTimer - creditScroll
                local prevY = 80 + 10*widthScale - optionAnimTimer - prevCreditScroll
                for i = 1, #renderList do
                    local credit = renderList[i]
                    local header = (credit.font == FONT_ALIASED)
                    if y > 62 and y < height then 
                        djui_hud_set_font(credit.font)
                        if not header then
                            djui_hud_set_color(menuColorHalf.r, menuColorHalf.g, menuColorHalf.b, 255)
                        else
                            djui_hud_set_color(menuColor.r, menuColor.g, menuColor.b, 255)
                        end
                        local x = xLeft - (header and 3 or 0)
                        local scale = (header and 0.3 or 0.2)*widthScale
                        djui_hud_print_text_interpolated(credit.textLeft, x, prevY, scale, x, y, scale)
                        if credit.textRight then
                            local x = xRight - djui_hud_measure_text(credit.textRight)*scale
                            local scale = 0.2*widthScale
                            djui_hud_print_text_interpolated(credit.textRight, x, prevY, scale, x, y, scale)
                        end
                    end
                    y = y + (header and 9 or 6)*widthScale
                    prevY = prevY + (header and 9 or 6)*widthScale
                    if renderList[i + 1] ~= nil and renderList[i + 1].font == FONT_ALIASED then
                        y = y + 2
                        prevY = prevY + 2
                    end
                end
                creditScrollRange = math_max(((y + creditScroll)) - (height - 36), 0)
                prevCreditScroll = creditScroll

                for i = 1, 8 do
                    djui_hud_set_color(menuColor.r * 0.1, menuColor.g * 0.1, menuColor.b * 0.1, 100)
                    djui_hud_render_rect(widthHalf - 50 * widthScale + 2, 60 - optionAnimTimer, 100 * widthScale - 4, i*4)
                    djui_hud_render_rect(widthHalf - 50 * widthScale + 2, height - 2 - i*4, 96 * widthScale, i*4)
                end
            end

            -- Render Header
            djui_hud_set_font(FONT_ALIASED)
            djui_hud_set_color(menuColor.r * 0.5 + 127, menuColor.g * 0.5 + 127, menuColor.b * 0.5 + 127, 255)
            local text = TEXT_OPTIONS_HEADER
            if creditsAndTransition then
                text = TEXT_CREDITS_HEADER
            elseif currOption > defaultOptionCount then
                text = TEXT_OPTIONS_HEADER_API
            end
            djui_hud_print_text(text, widthHalf - djui_hud_measure_text(text) * 0.3 * minf(widthScale, 1.5), 65 + optionAnimTimer * -1, 0.6 * minf(widthScale, 1.5))

            -- Fade in/out of credits
            if optionTable[optionTableRef.anims].toggle == 1 then
                if credits and creditsCrossFade > -creditsCrossFadeCap then
                    creditsCrossFade = creditsCrossFade - 1
                    if creditsCrossFade == 0 then creditsCrossFade = creditsCrossFade - 1 end
                end
                if not credits and creditsCrossFade < creditsCrossFadeCap then
                    creditsCrossFade = creditsCrossFade + 1
                    if creditsCrossFade == 0 then creditsCrossFade = creditsCrossFade + 1 end
                end
                if creditsCrossFade < 0 then
                    creditsAndTransition = true
                else
                    creditsAndTransition = false
                end
            else
                if credits then
                    creditsCrossFade = -creditsCrossFadeCap
                else
                    creditsCrossFade = creditsCrossFadeCap
                end
                creditsAndTransition = credits
            end
            
            djui_hud_set_resolution(RESOLUTION_N64)
            djui_hud_set_color(0, 0, 0, (math_abs(creditsCrossFade)) * -creditsCrossFadeMath)
            djui_hud_render_rect(width * 0.5 - 50 * widthScale + 2, minf(55 - optionAnimTimer + 2, height - 25 * widthScale + 2), 100 * widthScale - 4, 196)
        else
            -- How to open options display
            local widthScaleLimited = minf(widthScale, 1.42)
            djui_hud_set_color(menuColor.r, menuColor.g, menuColor.b, 255)
            djui_hud_render_rect(widthHalf - 50 * widthScale, height - 25 * widthScaleLimited, 100 * widthScale, 26 * widthScaleLimited)
            djui_hud_set_color(menuColorHalf.r * 0.1, menuColorHalf.g * 0.1, menuColorHalf.b * 0.1, menuOpacity)
            djui_hud_render_rect(widthHalf - 50 * widthScale + 2, height - 25 * widthScaleLimited + 2, 100 * widthScale - 4, 22 * widthScaleLimited)
            djui_hud_set_color(menuColorHalf.r, menuColorHalf.g, menuColorHalf.b, 255)
            djui_hud_render_rect(widthHalf - 50 * widthScale, height - 2, 100 * widthScale, 2)
            djui_hud_set_font(FONT_ALIASED)
            djui_hud_print_text(TEXT_OPTIONS_OPEN, widthHalf - djui_hud_measure_text(TEXT_OPTIONS_OPEN) * 0.175 * widthScaleLimited, height - 23 * widthScaleLimited + optionAnimTimer + 202, 0.35 * widthScaleLimited)
            djui_hud_set_font(FONT_TINY)
            djui_hud_print_text(TEXT_MENU_CLOSE, widthHalf - djui_hud_measure_text(TEXT_MENU_CLOSE) * 0.25 * widthScaleLimited, height - 13 * widthScaleLimited + optionAnimTimer + 202, 0.5 * widthScaleLimited)
        end
        djui_hud_set_color(menuColor.r, menuColor.g, menuColor.b, 255)
        djui_hud_render_rect(width * 0.5 - 50 * widthScale, height - 2, 100 * widthScale, 2)


        -- Anim logic
        if options then
            if optionTable[optionTableRef.anims].toggle > 0 then
                if optionAnimTimer < -1 then
                    optionAnimTimer = optionAnimTimer * 0.9
                end
            else
                optionAnimTimer = -1
            end
        else
            if optionTable[optionTableRef.anims].toggle > 0 then
                if optionAnimTimer > optionAnimTimerCap then
                    optionAnimTimer = optionAnimTimer * 1.3
                end
            else
                optionAnimTimer = optionAnimTimerCap
            end
        end
        optionAnimTimer = maxf(optionAnimTimer, -200)
    else
        options = false
        optionAnimTimer = optionAnimTimerCap
        credits = false
        creditsCrossFade = 0
        bindTextTimer = 0
    end

    -- Fade in/out of menu
    if optionTable[optionTableRef.anims].toggle == 1 then
        if menu and menuCrossFade > -menuCrossFadeCap then
            menuCrossFade = menuCrossFade - 1
            if menuCrossFade == 0 then menuCrossFade = menuCrossFade - 1 end
        end
        if not menu and menuCrossFade < menuCrossFadeCap then
            menuCrossFade = menuCrossFade + 1
            if menuCrossFade == 0 then menuCrossFade = menuCrossFade + 1 end
        end
        if menuCrossFade < 0 then
            menuAndTransition = true
        else
            menuAndTransition = false
        end
    else
        if menu then
            menuCrossFade = -menuCrossFadeCap
        else
            menuCrossFade = menuCrossFadeCap
        end
        menuAndTransition = menu
    end

    -- Info / Z Open Bind on Pause Menu
    if is_game_paused() and not djui_hud_is_pause_menu_created() and gMarioStates[0].action ~= ACT_EXIT_LAND_SAVE_DIALOG then
        local currCharY = 0
        djui_hud_set_resolution(RESOLUTION_DJUI)
        djui_hud_set_font(FONT_USER)
        if optionTable[optionTableRef.openInputs].toggle == 1 then
            currCharY = 27
            local text = menu_is_allowed() and TEXT_PAUSE_Z_OPEN or TEXT_PAUSE_UNAVAILABLE
            width = djui_hud_get_screen_width() - djui_hud_measure_text(text)
            djui_hud_set_color(255, 255, 255, 255)
            djui_hud_print_text(text, width - 20, 16, 1)
        end

        if optionTable[optionTableRef.localModels].toggle == 1 then
            local character = characterTable[currChar][characterTable[currChar].currAlt]
            local charName = string_underscore_to_space(character.name)
            local TEXT_PAUSE_CURR_CHAR_WITH_NAME = TEXT_PAUSE_CURR_CHAR .. charName
            width = djui_hud_get_screen_width() - djui_hud_measure_text(TEXT_PAUSE_CURR_CHAR_WITH_NAME)
            local charColor = character.color
            djui_hud_set_color(255, 255, 255, 255)
            djui_hud_print_text(TEXT_PAUSE_CURR_CHAR, width - 20, 16 + currCharY, 1)
            djui_hud_set_color(charColor.r, charColor.g, charColor.b, 255)
            djui_hud_print_text(charName, djui_hud_get_screen_width() - djui_hud_measure_text(charName) - 20, 16 + currCharY, 1)
        else
            width = djui_hud_get_screen_width() - djui_hud_measure_text(TEXT_LOCAL_MODEL_OFF)
            djui_hud_set_color(255, 255, 255, 255)
            djui_hud_print_text(TEXT_LOCAL_MODEL_OFF, width - 20, 16 + currCharY, 1)
        end

        local text = nil
        if gGlobalSyncTable.charSelectRestrictMovesets > 0 and gGlobalSyncTable.charSelectRestrictPalettes > 0 then
            text = TEXT_MOVESET_AND_PALETTE_RESTRICTED
        elseif gGlobalSyncTable.charSelectRestrictMovesets > 0 then
            text = TEXT_MOVESET_RESTRICTED
        elseif gGlobalSyncTable.charSelectRestrictPalettes > 0 then
            text = TEXT_PALETTE_RESTRICTED
        end
        if text ~= nil then
            width = djui_hud_get_screen_width() - djui_hud_measure_text(text)
            djui_hud_set_color(255, 255, 255, 255)
            currCharY = currCharY + 27
            djui_hud_print_text(text, width - 20, 16 + currCharY, 1)
        end
    end

    -- Cross Fade to Menu
    djui_hud_set_resolution(RESOLUTION_N64)
    djui_hud_set_color(0, 0, 0, (math_abs(menuCrossFade)) * -menuCrossFadeMath)
    djui_hud_render_rect(0, 0, width, height)
end

local prevMouseScroll = 0
local mouseScroll = 0
local function before_mario_update(m)
    if m.playerIndex ~= 0 then return end
    local controller = m.controller
    if inputStallTimerButton > 0 then inputStallTimerButton = inputStallTimerButton - 1 end
    if inputStallTimerDirectional > 0 then inputStallTimerDirectional = inputStallTimerDirectional - 1 end

    if menu and inputStallToDirectional ~= latencyValueTable[optionTable[optionTableRef.inputLatency].toggle + 1] then
        inputStallToDirectional = latencyValueTable[optionTable[optionTableRef.inputLatency].toggle + 1]
    end

    -- Menu Inputs
    if is_game_paused() and m.action ~= ACT_EXIT_LAND_SAVE_DIALOG and (controller.buttonPressed & Z_TRIG) ~= 0 and optionTable[optionTableRef.openInputs].toggle == 1 then
        menu = true
    end
    if not menu and (controller.buttonDown & D_JPAD) ~= 0 and m.action ~= ACT_EXIT_LAND_SAVE_DIALOG and optionTable[optionTableRef.openInputs].toggle == 2 then
        if (controller.buttonDown & R_TRIG) ~= 0 or not ommActive then
            menu = true
        end
        inputStallTimerDirectional = inputStallToDirectional
    end

    if not menu_is_allowed(m) then
        menu = false
        return
    end

    mouseScroll = mouseScroll + djui_hud_get_mouse_scroll_y()

    local cameraToObject = m.marioObj.header.gfx.cameraToObject
    if menuAndTransition and not options then
        if menu then
            if inputStallTimerDirectional == 0 and not charBeingSet then
                -- Alt switcher
                if #characterTable[currChar] > 1 then
                    local character = characterTable[currChar]
                    if (controller.buttonPressed & R_JPAD) ~= 0 or controller.stickX > 60 then
                        character.currAlt = character.currAlt + 1
                        inputStallTimerDirectional = inputStallToDirectional
                        play_sound(SOUND_MENU_CLICK_CHANGE_VIEW, cameraToObject)
                        buttonAltAnim = inputStallToDirectional
                    end
                    if (controller.buttonPressed & L_JPAD) ~= 0 or controller.stickX < -60 then
                        character.currAlt = character.currAlt ~= 0 and character.currAlt - 1 or #character
                        inputStallTimerDirectional = inputStallToDirectional
                        play_sound(SOUND_MENU_CLICK_CHANGE_VIEW, cameraToObject)
                        buttonAltAnim = -inputStallToDirectional
                    end
                    if character.currAlt > #character then character.currAlt = 1 end
                    if character.currAlt < 1 then character.currAlt = #character end
                end

                if optionTable[optionTableRef.localModels].toggle ~= 0 then    
                    if (controller.buttonPressed & D_JPAD) ~= 0 or (controller.buttonPressed & D_CBUTTONS) ~= 0 or controller.stickY < -60 or prevMouseScroll < mouseScroll then
                        currCharRender = currCharRender + 1
                        --[[
                        local character = characterTableRender[currCharRender]
                        if character ~= nil and character.locked then
                            currCharRender = get_next_unlocked_char()
                        end
                        ]]
                        if (controller.buttonPressed & D_CBUTTONS) == 0 then
                            inputStallTimerDirectional = inputStallToDirectional
                        else
                            inputStallTimerDirectional = 3 -- C-Scrolling
                        end
                        if currCharRender > #characterTableRender then
                            buttonScroll = -buttonScrollCap * #characterTableRender
                        else
                            buttonScroll = buttonScrollCap
                        end
                        play_sound(SOUND_MENU_MESSAGE_NEXT_PAGE, cameraToObject)
                        if currCharRender > #characterTableRender then currCharRender = 1 end
                        currChar = characterTableRender[currCharRender].ogNum
                        if characterColorPresets[characterTable[currChar]] ~= nil then
                            characterColorPresets[characterTable[currChar]].currPalette = 0
                        end
                        prevMouseScroll = mouseScroll
                    end
                    if (controller.buttonPressed & U_JPAD) ~= 0 or (controller.buttonPressed & U_CBUTTONS) ~= 0 or controller.stickY > 60 or prevMouseScroll > mouseScroll then
                        currCharRender = currCharRender - 1
                        --[[
                        local character = characterTableRender[currCharRender]
                        if character ~= nil and character.locked then
                            currCharRender = get_last_unlocked_char()
                        end
                        ]]
                        if (controller.buttonPressed & U_CBUTTONS) == 0 then
                            inputStallTimerDirectional = inputStallToDirectional
                        else
                            inputStallTimerDirectional = 3 -- C-Scrolling
                        end
                        if currCharRender < 1 then
                            buttonScroll = buttonScrollCap * (#characterTableRender - 1)
                        else
                            buttonScroll = -buttonScrollCap
                        end
                        play_sound(SOUND_MENU_MESSAGE_NEXT_PAGE, cameraToObject)
                        if currCharRender < 1 then currCharRender = #characterTableRender end
                        currChar = characterTableRender[currCharRender].ogNum
                        if characterColorPresets[characterTable[currChar]] ~= nil then
                            characterColorPresets[characterTable[currChar]].currPalette = 0
                        end
                        prevMouseScroll = mouseScroll
                    end

                    -- Tab Switcher
                    if (controller.buttonPressed & L_TRIG) ~= 0 then
                        local renderEmpty = true
                        while renderEmpty do
                            currCategory = currCategory - 1
                            if currCategory < 1 then currCategory = #characterCategories end
                            renderEmpty = not update_character_render_table()
                        end
                        inputStallTimerDirectional = inputStallToDirectional
                        play_sound(SOUND_MENU_CAMERA_TURN, cameraToObject)
                    end
                    if (controller.buttonPressed & R_TRIG) ~= 0 then
                        local renderEmpty = true
                        while renderEmpty do
                            currCategory = currCategory + 1
                            if currCategory > #characterCategories then currCategory = 1 end
                            renderEmpty = not update_character_render_table()
                        end
                        inputStallTimerDirectional = inputStallToDirectional
                        play_sound(SOUND_MENU_CAMERA_TURN, cameraToObject)
                    end
                end
            end

            if inputStallTimerButton == 0 then
                if (controller.buttonPressed & A_BUTTON) ~= 0 then
                    if characterTable[currChar] ~= nil then
                        mod_storage_save_pref_char(characterTable[currChar])
                        inputStallTimerButton = inputStallToButton
                        play_sound(SOUND_MENU_CLICK_FILE_SELECT, cameraToObject)
                    else
                        play_sound(SOUND_MENU_CAMERA_BUZZ, cameraToObject)
                    end
                    
                    -- Set bottom right text
                    bindText = 1
                    bindTextTimer = 1
                end
                if (controller.buttonPressed & B_BUTTON) ~= 0 then
                    menu = false
                end
                if (controller.buttonPressed & START_BUTTON) ~= 0 then
                    options = true
                end
                local modelId = gCSPlayers[0].modelId
                local paletteCount = characterColorPresets[gCSPlayers[0].modelId] ~= nil and #characterColorPresets[gCSPlayers[0].modelId] or 0
                local currPaletteTable = characterColorPresets[gCSPlayers[0].modelId] and characterColorPresets[gCSPlayers[0].modelId] or {currPalette = 0}

                if (controller.buttonPressed & Y_BUTTON) ~= 0 then
                    if characterColorPresets[modelId] and optionTable[optionTableRef.localModels].toggle > 0 and gGlobalSyncTable.charSelectRestrictPalettes == 0 then
                        play_sound(SOUND_MENU_CLICK_FILE_SELECT, cameraToObject)
                        currPaletteTable.currPalette = currPaletteTable.currPalette + 1
                        inputStallTimerButton = inputStallToButton
                    else
                        play_sound(SOUND_MENU_CAMERA_BUZZ, cameraToObject)
                        inputStallTimerButton = inputStallToButton
                    end

                    -- Set bottom right text
                    bindText = 2
                    bindTextTimer = 1
                end
                if characterColorPresets[gCSPlayers[0].modelId] ~= nil then
                    if paletteCount < currPaletteTable.currPalette then currPaletteTable.currPalette = 0 end
                end
            end
        end

        -- Handles Camera Posistioning
        faceAngle = m.faceAngle.y
        eyeState = MARIO_EYES_OPEN
        if controller.buttonPressed & R_CBUTTONS ~= 0 then
            faceAngle = faceAngle + 0x1000
            eyeState = MARIO_EYES_LOOK_RIGHT
        end
        if controller.buttonPressed & L_CBUTTONS ~= 0 then
            faceAngle = faceAngle - 0x1000
            eyeState = MARIO_EYES_LOOK_LEFT
        end

        nullify_inputs(m)
        if is_game_paused() then
            controller.buttonPressed = START_BUTTON
        end
    end

    if options and not creditsAndTransition then
        if inputStallTimerDirectional == 0 then
            if (controller.buttonPressed & D_JPAD) ~= 0 or controller.stickY < -60 then
                currOption = currOption + 1
                inputStallTimerDirectional = inputStallToDirectional
                play_sound(SOUND_MENU_MESSAGE_NEXT_PAGE, cameraToObject)
            end
            if (controller.buttonPressed & U_JPAD) ~= 0 or controller.stickY > 60 then
                currOption = currOption - 1
                inputStallTimerDirectional = inputStallToDirectional
                play_sound(SOUND_MENU_MESSAGE_NEXT_PAGE, cameraToObject)
            end
        end

        if inputStallTimerButton == 0 then
            if (controller.buttonPressed & A_BUTTON) ~= 0 and not optionTable[currOption].optionBeingSet and (optionTable[currOption].lock == nil or optionTable[currOption].lock() == nil) then
                optionTable[currOption].toggle = optionTable[currOption].toggle + 1
                if optionTable[currOption].toggle > optionTable[currOption].toggleMax then optionTable[currOption].toggle = 0 end
                if optionTable[currOption].toggleSaveName ~= nil then
                    mod_storage_save(optionTable[currOption].toggleSaveName, tostring(optionTable[currOption].toggle))
                end
                inputStallTimerButton = inputStallToButton
                play_sound(SOUND_MENU_CHANGE_SELECT, cameraToObject)
            end
            if (controller.buttonPressed & B_BUTTON) ~= 0 then
                options = false
                inputStallTimerButton = inputStallToButton
            end
        end
        if currOption > #optionTable then currOption = 1 end
        if currOption < 1 then currOption = #optionTable end
        nullify_inputs(m)
    end

    if creditsAndTransition then
        if (controller.buttonDown & U_JPAD) ~= 0 then
            creditScroll = creditScroll - 1.5
        elseif (controller.buttonDown & D_JPAD) ~= 0 then
            creditScroll = creditScroll + 1.5
        elseif math.abs(controller.stickY) > 30 then
            creditScroll = creditScroll + controller.stickY*-0.03
        end

        if inputStallTimerButton == 0 then
            if (controller.buttonPressed & A_BUTTON) ~= 0 or (controller.buttonPressed & B_BUTTON) ~= 0 or (controller.buttonPressed & START_BUTTON) ~= 0 then
                credits = false
            end
        end
        nullify_inputs(m)
        if creditScroll < 0 then creditScroll = 0 end
        if creditScroll > creditScrollRange then creditScroll = creditScrollRange end
    else
        creditScroll = 0
    end

end

hook_event(HOOK_BEFORE_MARIO_UPDATE, before_mario_update)
hook_event(HOOK_ON_HUD_RENDER, on_hud_render)

--------------
-- Commands --
--------------

promptedAreYouSure = false

local function chat_command(msg)
    msg = string_lower(msg)

    -- Open Menu Check
    if (msg == "" or msg == "menu") then
        if menu_is_allowed(gMarioStates[0]) then
            menu = not menu
            return true
        else
            djui_chat_message_create(TEXT_PAUSE_UNAVAILABLE)
            return true
        end
    end

    -- Help Prompt Check
    if msg == "?" or msg == "help" then
        djui_chat_message_create("Character Select's Avalible Commands:" ..
        "\n\\#ffff33\\/char-select help\\#ffffff\\ - Returns Avalible Commands" ..
        "\n\\#ffff33\\/char-select menu\\#ffffff\\ - Opens the Menu" ..
        "\n\\#ffff33\\/char-select [name/num]\\#ffffff\\ - Switches to Character" ..
        "\n\\#ff3333\\/char-select reset\\#ffffff\\ - Resets your Save Data")
        return true
    end

    -- Reset Save Data Check
    if msg == "reset" or (msg == "confirm" and promptedAreYouSure) then
        reset_options(true)
        return true
    end

    -- Stop Character checks if API disallows it 
    if not menu_is_allowed() or charBeingSet then
        djui_chat_message_create("Character Cannot be Changed")
        return true
    end

    -- Name Check
    for i = 1, #characterTable do
        if not characterTable[i].locked then
            local saveName = string_underscore_to_space(string_lower(characterTable[i].saveName))
            for a = 1, #characterTable[i] do
                if msg == string_lower(characterTable[i][a].name) or msg == saveName then
                    force_set_character(i)
                    if msg ~= saveName then
                        characterTable[i].currAlt = a
                    end
                    djui_chat_message_create('Character set to "' .. characterTable[i][characterTable[i].currAlt].name .. '" Successfully!')
                    return true
                end
            end
        end
    end

    -- Number Check
    msgSplit = string_split(msg)
    if tonumber(msgSplit[1]) then
        local charNum = tonumber(msgSplit[1])
        local altNum = tonumber(msgSplit[2])
        altNum = altNum and altNum or 1
        if charNum > 0 and charNum <= #characterTable and not characterTable[charNum].locked then
            currChar = charNum
            characterTable[charNum].currAlt = altNum
            djui_chat_message_create('Character set to "' .. characterTable[charNum][altNum].name .. '" Successfully!')
            return true
        end
    end
    djui_chat_message_create("Character Not Found")
    return true
end

hook_chat_command("char-select", "- Opens the Character Select Menu", chat_command)

--[[
local function mod_menu_open_cs()
    local m = gMarioStates[0]
    if menu_is_allowed(m) then
        gMarioStates[0].controller.buttonPressed = START_BUTTON
        menu = true
    else
        play_sound(SOUND_MENU_CAMERA_BUZZ, m.pos)
    end
end
hook_mod_menu_button("Open Menu", mod_menu_open_cs)
]]
